diff --git a/.env.example b/.env.example index 46373dbb..373dd7dd 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,7 @@ APP_ENV=production APP_KEY= APP_DEBUG=false APP_URL=http://localhost -APP_VERSION=7.0.6 +APP_VERSION=7.0.7 APP_LOCALE=ko APP_FALLBACK_LOCALE=ko @@ -100,6 +100,11 @@ AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false +# 아래 3키는 사용할 때만 주석을 해제하세요 — 값 없는 `KEY=` 는 "미설정" 이 아니라 +# 빈 문자열로 적용됩니다 (S3 공개 URL / S3 호환 엔드포인트 / 첨부 디스크 강제 지정) +# AWS_URL= +# AWS_ENDPOINT= +# ATTACHMENT_DISK= VITE_APP_NAME="${APP_NAME}" diff --git a/.env.testing.example b/.env.testing.example index ecfb2ee2..6aaace74 100644 --- a/.env.testing.example +++ b/.env.testing.example @@ -3,7 +3,7 @@ APP_ENV=testing APP_KEY= APP_DEBUG=false APP_URL=http://localhost -APP_VERSION=7.0.6 +APP_VERSION=7.0.7 APP_LOCALE=ko APP_FALLBACK_LOCALE=ko @@ -77,6 +77,11 @@ AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false +# 아래 3키는 사용할 때만 주석을 해제하세요 — 값 없는 `KEY=` 는 "미설정" 이 아니라 +# 빈 문자열로 적용됩니다 (S3 공개 URL / S3 호환 엔드포인트 / 첨부 디스크 강제 지정) +# AWS_URL= +# AWS_ENDPOINT= +# ATTACHMENT_DISK= VITE_APP_NAME="${APP_NAME}" diff --git a/AGENTS.md b/AGENTS.md index bd3ef5a0..8dd1a4df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -165,7 +165,7 @@ | `sirsoft-board` | 모듈 | [docs/api/](modules/_bundled/sirsoft-board/docs/api/README.md) | 10 / 80 | | `sirsoft-ecommerce` | 모듈 | [docs/api/](modules/_bundled/sirsoft-ecommerce/docs/api/README.md) | 33 / 239 | | `sirsoft-page` | 모듈 | [docs/api/](modules/_bundled/sirsoft-page/docs/api/README.md) | 2 / 17 | -| `sirsoft-ckeditor5` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md) | 2 / 2 | +| `sirsoft-ckeditor5` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md) | 3 / 5 | | `sirsoft-gdpr` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-gdpr/docs/api/README.md) | 4 / 15 | | `sirsoft-marketing` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-marketing/docs/api/README.md) | 2 / 2 | | `sirsoft-pay_kginicis` | 플러그인 | [docs/api/](plugins/_bundled/sirsoft-pay_kginicis/docs/api/README.md) | 5 / 34 | @@ -260,6 +260,9 @@ | `Select valueKey/labelKey` | computed로 `{ value, label }` 변환 | | Form 내 `Button` type 없음 | `type="button"` 명시 (submit 방지) | | `options={{options}}` | `options={{options ?? []}}` (fallback) | +| boolean 필드를 `RadioGroup`/`Select` 의 `name` 자동바인딩만으로 폼에 묶기 | `autoBinding: false` + `value: "{{String(_local.form?.필드 ?? 기본값)}}"` + `change` 액션 `"{{$event.target.value === 'true'}}"` 캐스팅. 자동바인딩 value 경로는 `e.target.value` 문자열을 그대로 저장해 서버 `boolean` 규칙에서 422 가 된다 (표시만 보면 정상이라 저장 시점에야 드러남) | +| `options` 지정 커스텀 `Select`(composite) 에 `defaultValue` | `value: "{{상태 ?? 기본값}}"` + `change` 액션 + 열기 지점 상태 시드 — 커스텀 Select 는 value-제어 전용이라 `defaultValue` 는 렌더되지 않고(빈 표시) 숨은 input 도 없어 값이 조용히 미전송된다 (options 없는 네이티브 렌더 경로만 defaultValue 유효) | +| 폼 밖 제출 버튼 `props.form: "X"` 만 선언 | 참조 대상 `Form` 에 `props.id: "X"` 동반 필수 — id 가 없으면 버튼이 어떤 폼에도 연결되지 않아 클릭이 무반응이 된다 (오류 없음) | Icon 은 `` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은 박스만 정하고 글리프는 부모 `font-size` 를 상속하므로 어긋난다. 기존 `w-N h-N` 을 옮길 때는 아래 등가표를 쓴다 (Chrome 실측). @@ -355,6 +358,21 @@ Icon 은 `` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은 의도적 리셋(검색 초기화 / 필터 초기화 / 탭 전환 / 프리셋 적용 / 다른 목록으로의 이동)은 예외다. 그 경우 액션 노드 `comment` 에 `audit:allow layout-list-context-navigate-merge-query <사유>` 를 남겨 의도를 코드에 기록한다. 상세: [actions-handlers-navigation.md "목록 컨텍스트 왕복 규약"](docs/frontend/actions-handlers-navigation.md) +### 일괄 처리 목록의 선택 범위 + +체크박스 선택은 화면 밖(전역/로컬 상태)에 저장된다. 그래서 검색·필터·페이지 이동으로 행이 목록에서 빠져도 그 행의 선택은 남는다. 그 상태에서 일괄 처리를 누르면 사용자가 보고 있지도, 체크하지도 않은 행이 대상이 된다. 확인 모달은 건수만 말하므로 실행 전에 알아챌 방법이 없고, 처리는 정상 성공하므로 실행 후에도 오류가 남지 않는다. + +| ❌ 금지 | ✅ 올바른 사용 | +|--------|---------------| +| 선택을 화면 밖 상태에 저장하는 DataGrid 에 `selectionScope` 미선언 | `"selectionScope": "page"`(일괄 처리 목록) 또는 `"free"`(선택 자체가 저장 대상인 폼)를 **명시** | +| 일괄 처리 버튼이 달린 목록에 `"free"` | `"page"` — 대상은 언제나 "화면에 보이고 체크된 행" | +| 여러 페이지에 걸쳐 고르는 폼 선택기에 `"page"` | `"free"` — 페이지를 넘기면 앞 페이지 선택이 사라져 기능이 깨진다 | +| `selectable` 이 꺼진 화면이라 안전하다고 간주 | 체크박스가 없으면 남은 선택이 **더** 안 보인다 — 범위 판정은 `selectable` 과 무관 | +| `"selectionScope": "{{조건}}"` 표현식 분기 | 리터럴 고정 — 분기마다 보존 여부가 갈리면 한쪽이 조용히 대상 밖 행을 싣는다 | +| 화면마다 검색·필터·페이지 액션에 선택 초기화 액션을 복제 | 컴포넌트가 단일 지점에서 정리 — 액션 복제는 한 곳만 빠져도 같은 결함이 남는다 | + +정적 검사가 `onSelectionChange` 가 배선된 DataGrid 를 전수 검사해 미선언을 차단한다. 상세: [component-props.md DataGrid](docs/frontend/component-props.md) + ### 중첩 리소스 스코프 / 계층 무결성 | 금지 | 올바른 사용 | @@ -369,6 +387,33 @@ Icon 은 `` 글리프라 박스 크기가 곧 `font-size` 다. `w-N h-N` 은 > 상세: [validation.md "계층 리소스 순환 참조" / "배열 항목의 상위 스코프"](docs/backend/validation.md), [service-repository.md "중첩 리소스 스코프" / "설정 기반 한계값"](docs/backend/service-repository.md) +#### 보안 게이트 대칭성 (KVE-2026-1914/1915/1919) + +접근 게이트와 권한 등급 상한은 한 경로에만 있으면 다른 경로가 조용한 우회로가 된다. 게이트는 생산 지점(부모 비밀 판정 · 소유권 판정 · 등급 판정) 한 곳을 SSoT 로 두고, 같은 데이터를 내보내는 소비 경로 전부가 그 게이트를 경유해야 한다. + +| 금지 | 올바른 사용 | +|------|------------| +| 비밀/비공개 부모(게시글)의 비밀 게이트를 하위 리소스(댓글·첨부·문의) 독립 엔드포인트에서 재적용하지 않음 | 부모 비밀 판정을 하위 전 경로(훅·서비스·첨부 서빙·댓글 목록)에 재적용 — PostResource 한 곳만으로는 부족하다 (KVE-2026-1914) | +| hash 기반 file-serving(preview/download)이 소유권·비밀·발행 상태 검사 없이 서빙 | preview 와 download 가 동일 게이트 공유 — 미발행·비소유·비밀 첨부는 404 (KVE-2026-1914 A-3/S-1/S-2) | +| User/Role 의 쓰기·상태변경·권한부여 경로가 삭제 경로보다 약한 등급 가드 | 전 경로에 동일 등급-상한(rank ceiling)을 대칭 적용 — 정적 라우트(bulk)는 스코프 미들웨어가 우회되므로 서비스 계층에서 강제한다 (KVE-2026-1919) | +| 저장측 레이아웃 표현식 검증(SafeLayoutExpressions)을 문자열 endpoint 필드에만 부착 | 표현식이 실릴 수 있는 배열 트리 전체(`content`)에 부착 — 문자열 한정 부착은 `is_array` 가드로 무력화되어 no-op 이 된다 (KVE-2026-1915) | +| 배열 트리 순회용 규칙(`NoExternalUrls`)이 문자열 필드에도 부착돼 `is_array` 로 조용히 통과 | 규칙이 문자열 스칼라도 처리하거나, 그 자리에서 떼어낸다 — 부착만 해두고 통과시키는 상태가 최악이다 | +| 같은 저장 대상의 FormRequest 마다 부착 규칙이 다름 (편집기 경로만 누락) | Store·Update·Content·ExtensionContent 4경로 동일 강도 — 편집기 저장 경로가 가장 약하면 그 경로가 우회로다 | +| same-origin 을 `//` 접두·scheme·`/` 시작 **문자열 검사**로만 판정 | 브라우저 URL 파서와 동일 정규화(tab·LF·CR 제거 → 백슬래시를 슬래시로 → 선행 슬래시 런 접기) 후 판정 — `/\/evil.com/x.js` 는 문자열상 path 지만 브라우저는 외부 origin 으로 해석한다. 런타임·저장측·정적검사 3층이 같은 정규화를 공유한다 (KVE-2026-1915 B-2) | +| same-origin 판정만 정규화하고 **신뢰 호스트 추출(`hostOf`)은 원문**으로 판정 | 두 판정이 같은 `if` 안에서 이어지므로 정규화도 공유 — 어긋나면 `https://evil.com\@cdn.신뢰.com/x.js` 가 저장측에서만 신뢰 호스트로 보여 통과한다 | +| 정적 일괄 라우트(`bulk-*`)에 등급 상한만 적용하고 **스코프 축은 비움** | 라우트 모델이 없으면 미들웨어 스코프 검사가 스킵되므로 서비스가 상세 경로와 **같은 스코프 판정**(`PermissionHelper::filterByScope`)을 재적용 — 등급 축만 막으면 스코프 축이 우회로다 (KVE-2026-1919) | +| 권한 상한(ceiling) 검사를 DB 쓰기 **뒤**에 배치 | 가드 → 쓰기 순서 — 쓰기 뒤에 검사하면 거부된 요청이 고아 행·반영된 속성 변경을 남긴다. 회귀 테스트는 403 뿐 아니라 **상태 불변**까지 단언한다 | +| 같은 리소스를 쓰는 public 서비스 메서드 중 일부만 보호 가드 보유 | 형제 public 메서드 전부 동일 가드 — 서비스는 확장에 열려 있으므로 "현재 호출부가 없다" 는 방어가 아니다 | +| 라우트 파라미터가 Model 로 resolve 되지 않는 쓰기 경로를 미들웨어 스코프 검사에 맡김 | 서비스 계층에서 재적용 — 스킵 조건은 정적 경로(`bulk-*`·`reorder`)뿐 아니라 **파라미터명 불일치**(`{id}` + `int` 타입힌트)도 있고, 후자는 상세 경로까지 무가드다 | +| 순서 변경·일괄 작업의 스코프 거부를 "대상 일부 제외" 로 처리 | 순서·트리처럼 집합 전체가 하나의 값인 작업은 **전량 거부** — 일부만 반영하면 나머지와 어긋난 상태가 저장된다 | +| 가시성 판정을 호출부가 넘기는 옵트인 플래그(`$filters['is_public'] ?? false`)에 의존 | 열람자 신원 기반 fail-closed — 옵트인은 호출부가 빠뜨리면 조용히 열린다(읽기만 하고 쓰는 곳이 없는 사문 플래그가 실재했다) | +| 부모 상태로 판정하는 게이트를 `$x->parent && …` 로 작성 | 부모를 못 읽으면 차단 — 부모가 soft-delete 되면 조건이 성립하지 않아 통과한다 | +| 리소스 `abilityMap can_*` 을 연관/타 리소스 권한으로 게이팅 | 그 엔드포인트의 라우트 권한(SSoT)과 **같은 리소스 prefix** — 상승 방지는 게이트 이중화가 아니라 rank ceiling 이 담당한다 | + +이 결함군은 예외도 오류도 남기지 않는다 — 약한 경로가 정상 응답을 내보내는 것이 유일한 증상이다. secret 게이트 재적용·hash 서빙 게이트·rank 대칭·URL 판정 3층 동형·정적 bulk 스코프 재적용·가드 선행·형제 메서드 가드 패리티·abilityMap prefix 정합은 의미 판정 영역이라 정적 검사가 일부만 덮으므로, 부모 변경·하위 서빙·등급 경로·URL 검증 지점을 건드릴 때 코드 리뷰에서 대칭성을 확인한다. + +> 상세: [validation.md](docs/backend/validation.md), [service-repository.md](docs/backend/service-repository.md), [frontend/security.md](docs/frontend/security.md) + ### 목록 응답의 하위 컬렉션 목록은 화면이 그 행에서 **실제로 그리는 것**만 싣는다. 행마다 하위 컬렉션을 통째로 직렬화하면 한 페이지를 여는 것만으로 수백~수천 행이 응답에 실린다 (공개 #76 — 상품 100건 × 옵션 20건). @@ -500,6 +545,15 @@ G7 은 **기본 통화**(상품·쿠폰·배송비 저장 기준), **표시 통 주문·결제·환불 금액은 **거래 시점 통화로 동결**한다(`currency_snapshot.base_currency`). 운영자가 이후 기본 통화를 바꿔도 과거 주문의 표기는 불변이어야 한다. +동결 대상은 환율만이 아니다 — **소수 자릿수·절사 규칙·환산 분모(base_unit)까지 스냅샷이 SSoT** 다. 이 값들을 현재 설정에서 조회하면, 운영자가 그 통화를 삭제하는 순간 설정에서 사라져 폴백(자릿수 2)이 적용된다. 소수 0자리 통화의 과거 주문 표기가 `¥14,835` → `¥14,835.00` 으로 바뀌고, 3자리 이상으로 설정했던 통화는 표시 금액이 절사된다. 금액 계산은 스냅샷을 쓰는데 표기만 현재 설정을 따라가면 같은 화면 안에서 근거가 갈린다. + +| ❌ 금지 | ✅ 올바른 사용 | +| --- | --- | +| 주문·환불 표시에서 `getDecimalPlaces($code)` 를 스냅샷 없이 호출 | `getDecimalPlaces($code, $currencySnapshot)` — 스냅샷이 있으면 그것이 우선 | +| 리소스가 주문 스냅샷을 자식에게 전파하지 않음 | `withOrderCurrency()` 를 전파하는 지점마다 `withCurrencySnapshot()` 도 함께 전파 | +| 상품·카탈로그 표시까지 스냅샷으로 고정 | 현재 판매가는 **현재 설정**이 정답 — 스냅샷 없이 호출한다 | +| 자릿수를 박제하지 않은 구형 스냅샷에서 예외/0 자리 강제 | 박제값이 없으면 현재 설정 폴백을 그대로 탄다 (하위호환) | + > 상세: [api-resources.md](docs/backend/api-resources.md), [service-repository.md](docs/backend/service-repository.md) ### 확장 결제수단은 자기 능력을 선언한다 @@ -515,6 +569,26 @@ G7 은 **기본 통화**(상품·쿠폰·배송비 저장 기준), **표시 통 레이아웃 치환 방식은 코어가 그 리터럴을 버리는 순간 조용히 사문화된다 — 합성 입력으로만 검증한 테스트는 계속 통과하므로 사문화가 드러나지 않는다. 정적 검사가 능력 선언 누락을 차단한다. +### 예외를 응답으로 바꾸는 자리 + +`catch (\Exception)` / `catch (\Throwable)` 는 **도메인 예외가 아닌 것**을 잡는 자리다. 여기서 4xx 를 돌려주면 인프라 장애·코드 결함이 "입력 오류" 로 위장되어 사용자는 고칠 수 없는 안내를 보고 운영자는 장애를 늦게 안다. 그리고 이미 번역된 `$e->getMessage()` 를 응답의 메시지 **키** 자리에 넘기면 키 해석에 실패해 원문(SQL 상태코드·경로 포함 가능)이 그대로 화면에 나간다. 둘 다 예외도 로그도 남기지 않는다. + +| 금지 | 올바른 사용 | +|--------|---------------| +| generic catch 가 4xx 반환 | 5xx — 의도적 4xx 는 사유와 함께 판정 테스트의 상수에 선언 | +| generic catch 에서 상태코드 인자 생략 (`moduleError($mod,'key')`) | 상태코드 명시 — 생략 시 `ResponseHelper` 기본값 **400** 이 조용히 적용된다 | +| 도메인 예외를 typed 로 승격한 뒤에도 `catch (\RuntimeException)` 유지 | 승격한 예외로 좁힌다 — 도메인 예외의 **부모**를 잡으면 남는 것은 인프라 예외뿐인데 그것까지 4xx 가 된다 | +| `error($e->getMessage(), 422)` | `error($e->getMessageKey(), 422, null, $e->getMessageParams())` | +| 도메인 예외가 번역문만 들고 다님 | 생성자에 키+치환 파라미터 보관 → `getMessageKey()` / `getMessageParams()` | +| 서비스를 typed 예외로 승격하고 컨트롤러는 generic 만 유지 | 그 서비스 메서드를 호출하는 **컨트롤러 메서드 전수**에 typed catch 추가 (없으면 도메인 사유가 500 이 된다) | +| typed 예외 도입하면서 그 분기의 상태코드도 변경 | typed 는 **기존 상태코드 유지** — 예외 도입이 사용자 계약을 함께 바꾸면 회귀다 | +| 공개(비인증) 엔드포인트 응답에 예외 원문 포함 | 원문은 `Log::error` 로만 — 관리자 전용 면의 `errors` 페이로드는 진단 정보로 허용된다 | +| 원문을 직접 문자열로 조립해 노출 폭을 호출부가 정함 | 노출 폭은 `ResponseHelper` 가 정한다 — Throwable 을 넘기면 `app.debug` 에서만 펼쳐진다 | + +`message`(첫 인자)와 `errors`(셋째 인자)는 다른 통로다. **키 자리에 원문을 넘기는 것은 언제나 금지**지만, `errors` 페이로드의 원문은 금지 대상이 아니다 — `ResponseHelper::error` 가 문자열 `errors` 를 `500+` 비디버그에서만 차단하고 배열은 통과시키는 것은 `tests/Unit/Helpers/ResponseHelperTest.php` 가 고정한 의도다. 관리자에게 결제대행사·외부 시스템이 돌려준 사유를 감추면 조치 근거가 사라지고, 다국어 키는 유한해서 예상 못 한 실패를 담지 못한다. 판단 축은 "원문이냐 키냐" 가 아니라 **누구에게 / 무엇의 원문인가 / 어느 통로인가** 셋이다. + +상세: [exceptions.md "예외 → 응답 매핑"](docs/backend/exceptions.md). `tests/Feature/Http/GenericCatchStatusCodeContractTest.php` 가 코어와 모든 번들 확장의 컨트롤러를 전수 스캔해 두 규칙을 고정한다. 판정기를 한 확장 안에 두면 그 확장 밖의 동형 결함이 검출되지 않는다. + ### Listener 데이터 접근 | 금지 | 올바른 사용 | @@ -1097,6 +1171,8 @@ php artisan plugin:build sirsoft-payment --active # 활성 디렉토리에 ``` > **빌드 원칙**: 기본값은 `_bundled` 디렉토리. 빌드 결과물은 빌드 경로 내에만 남음. + +`_bundled` 의 `dist/`(코어는 `public/build/core/`)는 Git 추적되는 배포 산출물이다 (`*.map` 만 ignore). src 변경 시 커밋 dist 를 `--production` 으로 동반 재빌드한다 — 신규 소스 리터럴이 dist 에 없으면 stale 빌드이며, 정적 검사가 이를 검출한다. 커밋 dist 에 `//# sourceMappingURL=` 참조를 남기지 않는다 — `.map` 은 배포본에 존재하지 않아 브라우저 개발자 도구에서 404 를 유발한다. 코어 3번들 재빌드는 `core:build --production` (`--full` 은 앱 번들 전용 — `public/build` 를 비워 코어 3번들을 지운다). > 활성 디렉토리 반영은 `update` 커맨드로만 수행. `--watch` 모드는 실시간 개발용으로 활성 디렉토리를 자동 사용. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a82be45..45dccead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,87 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [7.0.7] - 2026-08-19 + +### Security + +- 부관리자(위임 관리자)가 슈퍼 관리자 계정을 함부로 손대지 못하도록 막았습니다. 슈퍼 관리자 보호는 삭제·탈퇴 경로에만 있었고 비밀번호 변경·상태 변경(차단/탈퇴)·계정 잠금 해제·일괄 상태 변경 경로에는 없어, 회원 관리 권한만 위임받은 계정이 슈퍼 관리자 계정을 무력화할 수 있었습니다. 이제 이 경로 전부에 같은 기준을 적용해, 슈퍼 관리자 계정은 슈퍼 관리자만 수정할 수 있습니다. 슈퍼 관리자 본인의 작업은 종전처럼 정상 동작합니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1919) +- 부관리자가 역할 권한을 통해 자신보다 높은 권한을 획득하지 못하도록 막았습니다. 이전에는 역할에 권한을 부여할 때 부여하는 사람이 그 권한을 가졌는지 확인하지 않아, 권한 관리 권한만 위임받은 계정이 자신에게 없는 권한이나 더 넓은 범위의 권한을 역할에 실어 우회 상승할 수 있었습니다. 이제 자신이 보유한 권한을 자신의 범위 이내로만 부여할 수 있으며, `admin` 같은 시스템·확장 소유 역할의 권한·활성 상태 변경도 슈퍼 관리자로 제한됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1919) +- 위 상한을 사용자에게 역할을 붙이는 경로에도 동일하게 적용했습니다. 이전에는 사용자 생성·수정 화면에서 역할을 배정할 때 그 역할이 담은 권한을 배정자가 모두 가졌는지 확인하지 않아, `admin` 같은 고권한 역할을 통째로 붙여 우회 상승할 수 있었습니다. 이제 배정자가 자신의 권한 범위 안에서 전부 부여할 수 있는 역할만 붙일 수 있습니다. 같은 기준이 역할을 **떼는** 방향에도 적용되어, 자신이 부여할 수 없는 상위 역할을 다른 관리자에게서 박탈하는 것도 차단됩니다. 사용자가 이미 가진 역할을 유지하는 수정은 그대로 허용되고, 슈퍼 관리자의 역할 배정은 종전처럼 정상 동작합니다. (KVE-2026-1919) +- 레이아웃 편집기에 저장하는 표현식이 서버나 다른 사용자 브라우저에서 임의 코드로 실행될 수 없도록 표현식 평가 방식을 근본적으로 바꿨습니다. 이전에는 표현식을 실제 코드로 만들어 실행했기 때문에 특정한 우회 기법으로 편집 권한을 넘어선 동작이 가능했습니다. 이제 정해진 문법·함수만 해석하는 안전한 방식으로 평가하며, 위험한 표현식과 외부 주소의 스크립트 로드는 저장 단계에서도 거부합니다. 기존 레이아웃의 정상 표현식(조건·계산·목록 가공·경로 조립 등)은 그대로 동작하고, 위지윅 에디터·주소 검색처럼 정해진 외부 스크립트를 쓰는 확장은 각자 신뢰 출처를 선언해 정책 강화 이후에도 정상 동작합니다. 신뢰 출처는 모듈·플러그인·템플릿이 모두 자기 설정 파일에 선언할 수 있으며, 활성 상태인 확장의 선언만 반영됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1915) +- 레이아웃에 적는 주소가 "내 사이트 경로"인지 판정하는 방식을 브라우저의 실제 해석과 일치시켰습니다. 이전에는 주소 앞부분의 글자만 보고 판정했기 때문에, 슬래시 사이에 역슬래시나 보이지 않는 공백 문자를 끼워 넣은 주소가 내 사이트 경로처럼 통과한 뒤 브라우저에서는 외부 사이트 주소로 해석되어, 선언하지 않은 외부 스크립트가 실제로 불려 올 수 있었습니다. 이제 브라우저와 같은 기준으로 정규화한 뒤 판정하며, 저장 단계·화면 로드·정비 검사 세 곳이 같은 기준을 씁니다. 경로 중간에 역슬래시가 들어간 정상 주소는 종전처럼 그대로 동작합니다. +- 레이아웃 편집기로 저장할 때 외부 주소 차단이 적용되지 않던 문제를 수정했습니다. 레이아웃을 새로 만들거나 정보를 수정하는 경로에는 이 검사가 걸려 있었지만, 편집기가 실제로 사용하는 콘텐츠 저장 경로에는 빠져 있어 같은 내용도 어느 화면에서 저장하느냐에 따라 통과 여부가 달랐습니다. 함께, 주소 입력 칸 하나만 검사하도록 걸어 둔 설정이 실제로는 아무것도 검사하지 않고 지나가던 것도 바로잡았습니다. 이제 네 저장 경로가 모두 같은 강도로 검사합니다. +- 회원 일괄 상태 변경에서 담당 범위 제한이 적용되지 않던 문제를 수정했습니다. 회원 수정 권한은 "본인 계정만" 또는 "같은 역할 범위만" 으로 범위를 좁혀 위임할 수 있는데, 이 제한은 회원을 하나씩 여는 화면에서만 적용되고 목록에서 여러 명을 한 번에 처리하는 일괄 변경에는 적용되지 않았습니다. 그래서 범위를 좁혀 위임받은 관리자가 일괄 변경으로는 담당 밖 회원까지 차단·탈퇴 처리하고 그 회원들의 로그인 세션까지 끊을 수 있었습니다. 기본 제공 역할인 "매니저" 가 이 구성에 해당합니다. 이제 일괄 변경도 회원 상세와 같은 기준으로 대상마다 범위를 확인하며, 범위 밖 회원은 처리 대상에서 제외되고 처리 건수로 확인할 수 있습니다. 범위 제한 없이 위임받은 관리자의 일괄 작업은 종전처럼 정상 동작합니다. (KVE-2026-1919) +- 레이아웃에 적는 스크립트 주소가 확장이 선언한 신뢰 출처인지 판정할 때, 주소를 브라우저와 같은 기준으로 정규화하지 않던 문제를 수정했습니다. 신뢰 출처 이름을 주소 뒷부분에 끼워 넣고 그 앞에 역슬래시를 둔 주소가 저장 단계에서만 신뢰 출처로 보여 통과했습니다(화면 로드 단계는 차단하고 있었으므로 실제로 불려 오지는 않았습니다). 반대로 브라우저가 신뢰 출처로 읽는 형태를 저장 단계만 거부하는 경우도 있었습니다. 이제 주소가 "내 사이트 경로인가" 와 "신뢰 출처인가" 를 같은 기준으로 판정합니다. +- 역할 생성·수정이 권한 상한에 걸려 거부될 때 변경 일부가 남던 문제를 수정했습니다. 권한 확인이 저장 뒤에 있었기 때문에, 거부된 요청인데도 권한이 하나도 없는 빈 역할이 만들어지거나 역할 이름 변경만 반영된 상태가 남았습니다. 이제 저장 전에 확인해 거부 시 아무것도 변경되지 않습니다. +- 첨부파일 순서 변경과 메뉴 순서 변경에도 담당 범위 제한을 적용했습니다. 두 기능은 대상을 목록으로 한 번에 받는 방식이라 범위 확인이 걸리지 않았고, 그래서 "본인 것만" 으로 범위를 좁혀 위임받은 관리자가 다른 사람이 올린 첨부파일이나 만든 메뉴의 순서를 바꿀 수 있었습니다. 기본 제공 역할인 "매니저" 가 첨부파일에서 이 구성에 해당합니다. 순서는 목록 전체에 대한 하나의 값이라 일부만 반영하면 나머지와 어긋나므로, 범위 밖 대상이 하나라도 섞이면 요청 전체를 거부하고 아무것도 변경하지 않습니다. (KVE-2026-1919) +- 레이아웃 표현식에서 객체의 숨은 내부 구조에 접근하는 우회 경로를 막았습니다. 표현식 평가기는 위험한 이름으로의 직접 접근을 막고 있었지만, 모든 객체가 공통으로 가진 오래된 방식의 접근 함수는 그 검사를 거치지 않아 같은 곳에 닿을 수 있었습니다. 이 경로로 사이트 전체의 공통 동작을 바꾸거나 망가뜨릴 수 있었습니다(임의 코드 실행으로는 이어지지 않습니다). 이제 화면 로드·저장·정비 검사 세 곳이 모두 이 이름들을 거부하며, 기존 레이아웃이 쓰는 정상 표현식은 그대로 동작합니다. (KVE-2026-1915) +- 회원 탈퇴·회원 정보 관리 작업이 실패할 때, 응답 메시지에 데이터베이스 오류 원문 같은 내부 시스템 정보가 그대로 노출될 수 있던 문제를 수정했습니다. 이제 이런 경우 사용자에게는 일반 안내 문구만 표시하고, 원본 오류는 서버 로그에만 기록합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 만료된 인증 토큰이 사용자 언어(로케일) 판별에서 여전히 유효한 것으로 취급되던 문제를 수정했습니다. 이제 만료된 토큰은 비로그인과 동일하게 처리합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 통합 검색이 로그인한 회원을 비회원으로 취급하던 문제를 수정했습니다. 검색 API 가 로그인 상태를 해석하지 않아, 회원 전용 게시판의 읽기 권한이 있어도 검색 결과와 게시판 필터 목록이 비회원 기준으로만 제한되었습니다. 이제 로그인한 회원은 자신이 열람할 수 있는 게시판 범위 그대로 검색됩니다. 비회원 검색은 종전과 동일합니다. +- 셸 예약 작업의 허용 실행 파일 목록에 `bash`·`python` 같은 인터프리터를 등록했을 때, 인터프리터에 인라인 명령(`bash -c ...` 형태)을 실어 임의 명령이 실행되던 문제를 막았습니다. 이제 인터프리터 뒤에는 절대경로 스크립트 파일만 지정할 수 있으며, 인라인 코드 실행·상대경로·경로 조작이 담긴 명령은 저장·실행 양쪽에서 거부됩니다. 스크립트 파일을 주기 실행하는 정상 사용(예: `python /경로/작업.py`)은 그대로 동작합니다. 이 기능은 현재 메뉴에 노출되지 않아 실제 사용 경로가 없습니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1653) +- 검색어·게시글 제목처럼 방문자가 입력한 값이 검색엔진용 구조화 데이터(JSON-LD)에 실릴 때, 스크립트 태그를 닫는 문자열을 끼워 넣으면 그 뒤의 내용이 방문자 브라우저에서 스크립트로 실행될 수 있던 문제를 수정했습니다. 검색엔진 봇에게 제공되는 화면이지만 특정 주소 파라미터로 일반 방문자에게도 같은 화면을 강제할 수 있어 실제 공격 경로가 됩니다. 이제 태그 문자를 이스케이프해 실행을 차단하며, 검색엔진이 읽는 구조화 데이터의 의미는 그대로 유지됩니다. + +### Added + +- 레이아웃 편집기 첨부 파일 업로드에 업로드 전/후 액션 훅과 파일 가공 필터 훅 제공 — 확장에서 다른 업로드 경로와 동일하게 개입할 수 있습니다. +- S3 호환 스토리지(Cloudflare R2, MinIO, 네이버 클라우드 등) 연결 지원 — 드라이버 설정에 엔드포인트 URL 과 Path-style 주소 옵션이 추가되어, AWS 가 아닌 S3 호환 스토리지도 관리자 화면 설정만으로 연결할 수 있습니다. S3 리전도 목록 선택 대신 자유 입력으로 바뀌어 새로 생기는 리전이나 R2 의 `auto` 값을 그대로 쓸 수 있습니다. +- 서버에서 동작할 수 없는 드라이버(필요한 라이브러리·PHP 확장이 없는 경우)를 저장하려 하면 사유와 함께 거부합니다 — 잘못된 드라이버 저장으로 사이트가 멈추는 사고를 저장 시점에 차단합니다. +- 완전 공개 자산(상품·카테고리·리뷰·에디터 이미지)을 S3+CDN 등 원격 디스크의 직접 URL 로 서빙할 수 있습니다. 환경설정 > 드라이버의 "공개 자산 스토리지"에서 디스크를 고르면 새로 올리는 이미지가 그 디스크에 저장되고 화면에는 CDN 주소가 실립니다 — 서버를 거치지 않아 트래픽 부담이 줄어듭니다. 사용 안 함(기본)이면 종전 방식 그대로이며, 디스크를 바꿔도 기존 이미지는 원래 위치에서 계속 정상 표시됩니다. 확장(쇼핑몰·에디터)별로 다른 디스크를 쓰도록 개별 설정할 수도 있습니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 확장 개발자용: 스토리지 URL 계약이 public 외에도 URL 이 설정된 디스크(S3+CDN 등)에서 직접 주소를 반환하며, 생성 결과를 공급·수정·차단할 수 있는 `core.storage.filter_url` 필터 훅이 추가되었습니다. 공개 자산 디스크 선택지는 `core.settings.available_public_asset_drivers` 훅으로 플러그인이 확장할 수 있고, 확장이 이미지 같은 카테고리 단위로 다른 스토리지 디스크를 쓰도록 분리할 수 있습니다. 공개 자산 디스크 오버라이드 설정을 선언한 플러그인은 설정 조회·저장 응답에 디스크 선택지 목록이 함께 실리고 저장 시 선택지 검증도 함께 걸리므로, 설정 화면이 별도 권한의 코어 환경설정 API 를 조회하거나 플러그인이 검증을 따로 구현할 필요가 없습니다. +- 보존 기간이 지난 데이터의 자동 정리에 확장이 개입할 수 있는 훅을 제공합니다 — 활동 로그·알림 발송 이력·예약 작업 이력의 정리 전후에 발생하므로, 확장이 정리 대상을 외부에 따로 보관하는 등의 처리를 붙일 수 있습니다. +- 폼을 저장하지 않고 나가서 소유자 없이 남은 첨부파일을 자동으로 정리할 수 있습니다. 환경설정 > 업로드에서 자동 정리를 켜면(기본 꺼짐) 설정한 보존기간(기본 30일)이 지난 고아 첨부의 파일과 기록을 매일 삭제합니다. 사이트 로고처럼 현재 사용 중인 첨부와 확장이 관리하는 첨부는 대상에서 제외됩니다. +- 관리자가 회원 정보 수정 화면에서 상태를 '탈퇴'로 바꿔 저장하면 확인 창을 거치도록 했습니다. 이 처리는 회원의 이메일·이름·닉네임을 익명화하고 되돌릴 수 없으므로, 무엇이 처리되는지 먼저 알려 드립니다. 함께, 관리자가 상태만 바꿔 탈퇴시킨 경우에도 회원이 직접 탈퇴한 것과 동일한 처리(익명화·연계 데이터 정리)가 이뤄지도록 통일했습니다. + +### Changed + +- 공개 자산을 외부 주소(CDN)로 서빙할 때, 그 주소로 나가는 요청에는 로그인 토큰을 함께 보내지 않습니다 — 공개 자산은 인증이 필요 없고, 토큰이 외부 서비스로 전달되지 않아야 하기 때문입니다. 사이트 자신의 주소로 가는 요청은 종전과 동일합니다. +- 확장 개발자용: 확장 캐시 계약 인터페이스에 카테고리별 스토리지 접근이 추가되었습니다. 코어가 제공하는 확장 베이스 클래스·서비스 프로바이더를 상속하는 확장은 수정 없이 호환되지만, 이 계약을 직접 구현하는 확장은 새 메서드를 구현해야 합니다 (번들 확장은 모두 반영 완료 — 최소 요구 코어 버전 7.0.7). +- 예약 작업의 실행 시각이 환경설정의 시간대를 기준으로 해석되도록 바꿨습니다. 종전에는 세계표준시 기준이어서 '새벽 4시'로 등록해도 국내 기준 낮에 실행됐습니다. 이번 변경으로 새로 추가된 자동 정리 작업은 물론, 기존 사이트맵 자동 생성·GeoIP 갱신·확장이 제공하는 예약 작업, 관리자가 직접 등록한 예약 작업까지 모두 같은 기준을 따릅니다. **시각을 지정해 둔 예약은 실제 실행 시각이 이동하므로 확인이 필요합니다.** +- 시스템 요구사항 문서를 실제 검증 기준에 맞게 정비했습니다. 지원 브라우저 항목에 검증 방식을 명시하고(자동 테스트는 Chromium 기준이며, Firefox·Safari 는 프론트엔드 의존성의 호환 범위를 기준으로 지원합니다), PHP 확장을 필수와 기능별 선택으로 구분했으며, 설치 프로그램이 자동으로 검사하는 항목과 운영자가 직접 확인해야 하는 항목을 구분하는 절을 새로 두었습니다. "요구사항" 이라고 적힌 항목이 모두 설치 단계에서 검사되는 것은 아니라는 점이 이제 문서에 드러납니다. (#95 @glitter-gim 님께서 제보해주셨습니다.) +- 문서 사이에 어긋나 있던 수치와 표기를 정정했습니다 — 디스크 용량 기준(설치 검사 기준 500MB), PHP 확장 개수, 지원 운영체제에 Windows 개발 환경 추가, 데이터베이스 표기에 MariaDB 병기, 한국어 README 의 시스템 요구사항 항목 보완, 그리고 데이터베이스 접두어 길이 제한·collation 변경 가능 여부처럼 실제로는 동작하지만 문서에 없던 조건의 추가. +- 확장 개발자용: 배포본에 함께 들어가는 브라우저 테스트 안내 문서가 실제 디렉토리 구성 및 실행 스크립트와 어긋나 있던 부분을 정정했습니다. + +### Fixed + +- 데이터베이스 테이블 접두어를 기본값(`g7_`)이 아닌 값으로 설치한 사이트에서, 7.0.6 업그레이드의 활동 로그 색인 교체가 옛 색인을 지우지 못하고 남겨 두던 문제를 수정했습니다. 남은 옛 색인은 조회 이득 없이 기록 쓰기 비용만 늘리므로, 이번 업그레이드에서 자동으로 정리됩니다. +- FULLTEXT 인덱스가 없는 테이블을 검색하면 오류 대신 부분일치 검색으로 자동 전환되고, 그 사실이 기록으로 남습니다. 이전에는 오류가 화면에 드러나지 않아 "검색 결과 0건" 으로만 보였습니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 통합검색에서 일부 항목의 검색이 실패하면 "검색 결과 없음" 이 아니라 오류 안내로 구분해 표시되도록, 항목별 실패 여부를 응답에 실어 내립니다. 실패 원인은 서버 기록에 상세히 남습니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 언어팩 업데이트 확인이 특정 조건(요청에 페이지 이동 파라미터가 실려 있는 경우)에서 아무 팩도 확인하지 않고 끝나던 문제를 수정했습니다. (#102 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 검색엔진 플러그인을 선택해 저장한 뒤 그 플러그인을 삭제하면 사이트 검색이 오류로 멈추던 문제를 수정했습니다. 이제 다른 드라이버 설정과 동일하게 기본 검색엔진으로 자동 복귀합니다. +- 설정을 항목 단위로 저장할 때 전체 저장과 다른 처리를 거치던 문제를 수정했습니다. 자산 주소 방식을 바꾸면 SEO 미리 생성 캐시가, 드라이버를 바꾸면 백그라운드 작업이 각각 갱신되지 않았습니다. SEO 설정도 항목 단위 저장에서 캐시가 정리되지 않아 검색엔진에 예전 정보가 남았습니다. (#114 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 모듈 환경설정(쇼핑몰·게시판)을 저장해도 SEO 미리 생성 캐시가 갱신되지 않던 문제를 수정했습니다. 함께, 모듈 설정 변경이 활동 로그에 기록됩니다. +- 설정을 항목 단위로 저장할 때 한 번 입력한 값을 빈 칸으로 되돌릴 수 없던 문제를 수정했습니다. 켜기/끄기 설정과 숫자 설정이 문자열로 저장되던 문제도 함께 고쳤으며, 해당 설정이 받을 수 없는 값은 저장 단계에서 안내와 함께 거부됩니다. +- 삭제된 본인인증 플러그인의 식별자가 정책 조회 응답과 공개 페이지에 그대로 노출되던 문제를 수정했습니다. 조회 응답은 실제 사용 가능한 인증 수단으로 해석해 내려주고, 공개 페이지에는 이 값을 싣지 않습니다. +- 관리자 화면 이동 시 템플릿 목록을 불필요하게 반복 검색하던 문제를 수정했습니다. +- 언어팩 관리(설치·활성화·삭제·업데이트·캐시 갱신)가 실패했을 때 응답에 내부 오류 원문이 그대로 실리던 문제를 수정했습니다. 설치 전 파일 검증 실패도 마찬가지로 안내 문구만 표시되며, 상세 내용은 디버그 모드에서만 확인할 수 있습니다. +- 관리자가 설정을 저장해도 백그라운드 작업(큐 워커 등)에는 이전 설정이 계속 적용되던 문제를 수정했습니다. 저장 즉시 실행 중인 프로세스에도 새 값이 반영됩니다. 설정 백업 복원과 플러그인 설정 초기화도 같은 문제가 있어 함께 수정했습니다. (#109 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 만료된 로그인 토큰 등 수명이 지난 데이터가 정리되지 않고 계속 쌓이던 문제를 수정했습니다. 만료 토큰·비밀번호 재설정 기록·오래된 알림·실패한 작업 기록·SEO 통계·예약 작업 이력·본인인증 이력·활동 로그·알림 발송 이력·확장 화면 구성에 쓰인 병합 파일의 구버전이 매일 자동 정리됩니다. 기존에 쌓여 있던 보존 기간 초과 데이터는 첫 정리 때 한 번에 정리되며, 양이 많아도 나눠서 지우므로 정리 중에 사이트가 멈추지 않습니다. (#110 @Tuwasduliebst, #120 @glitter-gim 님께서 제보해주셨습니다.) +- 회원 탈퇴가 마지막 단계에서 실패하면 약관 동의 이력·프로필 이미지·로그인 세션만 사라지고 계정은 남는 문제를 수정했습니다. 이제 탈퇴는 전부 성공하거나 전부 취소되며, 같은 이메일로 재가입한 회원이 같은 날 다시 탈퇴해도 실패하지 않습니다. 관리자·운영자 계정의 탈퇴 시도는 서버 오류가 아니라 안내 문구로 거절됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 사이트 로고를 교체해도 이전 로고가 설정에 다시 실려, 교체할 때마다 파일과 기록이 함께 쌓이던 문제를 수정했습니다. 화면에서 뺀 로고는 저장이 완료된 뒤 실제로 삭제되며, 저장에 실패하면 파일도 그대로 남습니다. +- 관리자 API 의 데이터베이스 백업 요청이 항상 서버 오류로 끝나고, 그 응답에 내부 구현 정보가 그대로 실려 나가던 문제를 수정했습니다. 이 기능은 아직 제공하지 않으므로 "제공하지 않는 기능" 임을 알리는 응답으로 답합니다. 설정 파일 백업은 종전대로 사용할 수 있습니다. +- 플러그인이 선언한 관리자 메뉴가 설치·업데이트 시 자동으로 만들어지도록 수정했습니다. 종전에는 플러그인이 활성화 처리에서 직접 만든 경우에만 메뉴가 생겨서, 이미 사용 중인 사이트가 플러그인을 업데이트해 새 화면을 받아도 메뉴가 나타나지 않고 주소를 직접 입력해야만 닿을 수 있었습니다. 이제 모듈과 동일하게 처리됩니다. +- 회원가입 등 일반 화면의 비밀번호 오류 문구가 'SMTP 비밀번호'로 표시되던 문제를 수정했습니다. 같은 이유로 어긋나 있던 '기본 언어'·'기본 통화'·'사용자 ID 목록'·'알림 채널' 등의 문구도 화면에 맞게 정리했습니다. (#113 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 회원 삭제·회원 정보 수정·회원가입·역할 삭제·비밀번호 재설정·첨부 파일 삭제·메뉴 삭제·예약 작업 삭제가 중간에 실패하면 일부만 처리된 상태로 남던 문제를 수정했습니다. 회원 정보 수정은 프로필 변경·로그인 세션 정리·역할 변경이 함께 처리되므로, 역할 변경에서 실패하면 이름만 바뀌고 로그인이 끊긴 상태로 남았습니다. 이제 각 작업은 전부 성공하거나 전부 취소되며, 파일 삭제는 데이터 정리가 확정된 뒤에 수행됩니다. +- 페이지 모듈의 첨부 파일 설정(개수·용량·형식)이 저장해도 반영되지 않던 문제를 수정했습니다. +- 회원 목록에서 여러 명을 골라 일괄 탈퇴할 때, 탈퇴할 수 없는 계정(관리자 등)이 섞여 있으면 그 건이 조용히 건너뛰어지던 문제를 수정했습니다. 이제 처리된 인원·처리하지 못한 인원과 그 사유가 응답에 함께 담깁니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 회원 일괄 상태 변경의 결과 안내 문구에 처리 인원수가 채워지지 않고 자리표시 원문이 그대로 표시되던 문제를 수정했습니다. +- 파일 스토리지에서 S3 를 선택해 저장해도 실제 파일 저장이 동작하지 않던 문제를 수정했습니다. S3 구동에 필요한 스토리지 어댑터가 빠져 있었고, 연결 테스트는 통과해도 실제 저장 경로는 동작하지 않았습니다. 이제 어댑터가 기본 포함되고, 연결 테스트도 실제 연결 경로와 같은 설정(엔드포인트·Path-style 포함)을 사용합니다. (#99 @lyg-kaban 님께서 제보해주셨습니다.) +- 파일 스토리지를 S3 로 저장해도 첨부 파일 업로드는 계속 서버 로컬 디스크에 저장되던 문제를 수정했습니다. 이제 S3 저장 시 새 첨부 파일이 S3 로 저장되며, 기존 파일은 종전 위치에서 계속 서빙됩니다. 서버 환경변수(`ATTACHMENT_DISK`)로 저장 위치를 명시한 경우에는 그 값이 우선합니다. (#99 @lyg-kaban 님께서 제보해주셨습니다.) +- phpredis PHP 확장이 없는 서버에서 캐시·세션·큐 드라이버로 Redis 를 선택하면 사이트 전체가 열리지 않던 문제를 예방했습니다 — Redis 클라이언트 라이브러리(predis)가 기본 포함되어 확장 없이도 동작합니다. +- 웹소켓 연결 테스트가 브라우저 접속 주소만 검사해, 테스트는 성공해도 서버 발송(알림 등)은 실패할 수 있던 문제를 수정했습니다 — 이제 서버 발송용 주소도 함께 검사하고, 실패 시 어느 쪽 주소가 문제인지 구분해 알려줍니다. +- 드라이버 설정의 S3 URL 이 너무 길 때 표시되는 영어 안내 문구가 실제 허용 길이(500자)와 다르게 255자로 안내되던 문제를 수정했습니다. +- 확인 질문이 있는 Artisan 명령(확장 설치·삭제·업데이트, 코어 업데이트, 검색 인덱스 재생성 등)을 터미널이 아닌 곳에서 실행하면 아무 반응 없이 멈춰 있던 문제를 수정했습니다. 예약 작업·배포 스크립트·CI 처럼 사람이 답할 수 없는 환경에서는 질문이 화면에 나오지도 않은 채 응답을 기다렸기 때문에, 명령이 실패한 것도 아니고 진행 중인 것도 아닌 상태로 남아 "명령이 몹시 느리다" 로만 보였습니다. 이제 그런 환경에서는 질문을 건너뛰고 기본 동작을 따릅니다 — 삭제·업데이트처럼 되돌리기 어려운 명령의 기본 동작은 "중단" 이므로, 확인 없이 수행되는 일은 없습니다. 그대로 진행하려면 종전처럼 `--no-interaction` 이나 `--force` 를 붙이면 됩니다. +- Windows 에서 확장(모듈·플러그인·템플릿) 업데이트가 "디렉토리 이동 실패" 로 중단되던 문제를 해소했습니다. Windows 는 편집기·개발 도구·파일 감시 프로그램이 폴더를 보고 있기만 해도 폴더 이름 바꾸기를 차단하므로, 폴더를 통째로 이동하는 방식으로는 실패를 피할 수 없었습니다. 이제 이동이 차단되면 파일 단위 교체로 자동 전환해 어떤 프로그램도 종료하지 않고 업데이트를 완료합니다. 잠근 프로그램을 찾아 강제 종료하던 이전 동작은 제거했습니다 — 사용 중인 편집기나 개발 도구가 예고 없이 종료되는 부작용이 있었고, 폴더를 감시만 하는 프로그램은 찾아내지도 못했습니다. 백업 복원에도 같은 방식이 적용되며, 실패한 업데이트가 남긴 임시 폴더는 다음 업데이트 때 자동으로 정리됩니다. +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 템플릿을 업데이트하거나 템플릿 캐시를 삭제해도 공개 템플릿 설정(config.json)이 최대 1시간 동안 이전 내용으로 응답되던 문제를 수정했습니다. 비활성화·삭제한 템플릿의 설정이 같은 시간 동안 계속 조회되던 문제와, `template:cache-clear` 명령이 실제로는 대부분의 캐시를 지우지 못하면서 삭제 완료로 보고하던 문제도 함께 고쳤습니다. 모듈·플러그인의 캐시 삭제 명령도 실재하는 캐시를 지우도록 정비했습니다. (#119 @glitter-gim 님께서 제보해주셨습니다.) +- 템플릿 업데이트로 컴포넌트 정의(components.json)가 바뀌어도 레이아웃 저장 검증이 최대 1시간 동안 이전 정의를 기준으로 동작하던 문제와, 템플릿 상태 변경 후에도 레이아웃 편집기의 병합 캐시 일부가 남아 편집기가 이전 내용을 받을 수 있던 문제를 수정했습니다. +- 캐시 버전 조회 주소(`?v=`)를 생략하고 공개 라우트·다국어·레이아웃 API 를 직접 호출하는 경우(외부 연동·봇 등), 그 응답이 어떤 갱신 경로로도 무효화되지 않는 별도 캐시에 남던 문제를 수정했습니다. 브라우저를 통한 일반 사용에는 영향이 없습니다. +- 확장·코어 업데이트가 중단되며 남은 임시 폴더와 오래된 백업본, 언어팩 설치 임시 파일이 정리되지 않고 계속 쌓이던 문제를 수정했습니다. 사흘이 지난 임시 산출물과 30일이 지난 백업본(최신 1개는 보존)이 매일 자동 정리됩니다. +- 개발 도구의 브라우저 콘솔 로그 파일이 하나의 파일에 무한히 쌓이던 문제를 수정했습니다. 이제 날짜별 파일로 나뉘어 7일간 보관됩니다. +- 예약 작업 저장 화면에서 설치된 확장이 제공하는 Artisan 명령이 "예약 실행이 허용된 명령이 아닙니다" 로 거부되던 문제를 수정했습니다. 터미널에서의 검증은 통과하는 명령이 관리자 화면 저장에서만 거부되었습니다. 이 기능은 현재 메뉴에 노출되지 않아 실제 사용 경로가 없습니다. +- 검색엔진 노출용 페이지 제목에서 타이틀 접미사가 제목과 붙어 표시되던 문제를 수정했습니다. 관리자 화면 안내대로 접미사를 공백으로 시작하게 입력해도 저장 시 선행 공백이 제거되어 "제목| 사이트명" 처럼 접착되었고, 페이지 제목이 없는 화면에서는 "| 사이트명" 처럼 구분자가 매달려 표시되었습니다. 이제 제목이 있으면 공백을 복원해 잇고, 제목이 없으면 구분자를 떼고 사이트명만 표시합니다. + ## [7.0.6] - 2026-08-10 ### Security diff --git a/INSTALL.md b/INSTALL.md index 0a5d6d57..802b5d98 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -8,7 +8,7 @@ | 항목 | 요구사항 | |------|---------| -| **PHP** | 8.2 이상 (필수 확장 30개 포함) | +| **PHP** | 8.2 이상 (필수 확장 16개 포함 — 기능별 선택 확장은 별도) | | **데이터베이스** | MySQL 8.0+ 또는 MariaDB 10.3+ (utf8mb4) | | **Composer** | 2.x | | **Redis** | 6.0+ (프로덕션 권장, 선택) | @@ -289,7 +289,7 @@ unzip g7-release.zip # 압축 해제 결과 확인 — 루트 디렉토리가 g7이 아니면 이름 변경 ls -la -# (필요 시) mv g7-7.0.6 g7 +# (필요 시) mv g7-7.0.7 g7 # ZIP 파일 정리 (선택) rm g7-release.zip diff --git a/README.ko.md b/README.ko.md index 4286e9a8..06a43295 100644 --- a/README.ko.md +++ b/README.ko.md @@ -10,7 +10,7 @@

- Version + Version PHP Laravel React @@ -71,7 +71,7 @@ Laravel과 React를 기반으로, 보안부터 아키텍처까지 처음부터 | 구분 | 기술 | |------|------| -| **백엔드** | PHP 8.2+, Laravel 12.x, MySQL 8.0+, Redis 6.0+ | +| **백엔드** | PHP 8.2+, Laravel 12.x, MySQL 8.0+ / MariaDB 10.3+, Redis 6.0+ | | **프론트엔드** | React 19, Vite, Tailwind CSS 4 (다크 모드 지원) | | **인증** | Laravel Sanctum (Bearer 토큰) | | **테스트** | PHPUnit 11.x, Vitest | @@ -353,10 +353,12 @@ HookManager::doAction('sirsoft-ecommerce.order.after_confirm', $order); ### 시스템 요구사항 -- PHP 8.2+ (필수 확장 30개 포함) +- PHP 8.2+ (필수 확장 16개 포함 — `ctype`, `curl`, `dom`, `fileinfo`, `json`, `mbstring`, `openssl`, `pdo_mysql`, `tokenizer`, `xml`, `zip` 등. `gd`/`imagick`, `intl`, `redis`, `bcmath` 등은 해당 기능을 쓸 때만 필요하며 전체 목록은 [docs/requirements.md](docs/requirements.md) 참조) - MySQL 8.0+ 또는 MariaDB 10.3+ (utf8mb4) -- Node.js 20+ (빌드 시에만 필요) - Composer 2.x +- Node.js 20+ (프론트엔드 에셋을 직접 빌드할 때만 필요) +- 웹 서버(Apache 또는 Nginx) — 문서 루트를 `public/` 으로 지정 +- Redis 6.0+ (선택 — 프로덕션의 캐시·큐에 권장) ### 설치 @@ -512,15 +514,17 @@ cp .env.example .env

jiwonpapa + Tuwasduliebst glitter-gim jordy-bitree laelbe + lyg-kaban bigmsg hwaryeon1234 + koojunho abc101 yks118 kitrio - koojunho movielee2020 ChoDongHyeon comtylove-netizen diff --git a/README.md b/README.md index c6059483..3410c40b 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

- Version + Version PHP Laravel React @@ -75,7 +75,7 @@ Everything a modern web platform needs, built in. | Layer | Technology | |-------|------------| -| **Backend** | PHP 8.2+, Laravel 12.x, MySQL 8.0+, Redis 6.0+ | +| **Backend** | PHP 8.2+, Laravel 12.x, MySQL 8.0+ / MariaDB 10.3+, Redis 6.0+ | | **Frontend** | React 19, Vite, Tailwind CSS 4 (dark mode supported) | | **Authentication** | Laravel Sanctum (Bearer tokens) | | **Testing** | PHPUnit 11.x, Vitest | @@ -359,7 +359,7 @@ Every verification point — signup, password reset, sensitive operations, the m ### System requirements -- PHP 8.2+ with the required extensions (30 in total), including `bcmath`, `ctype`, `curl`, `dom`, `fileinfo`, `gd`, `intl`, `mbstring`, `openssl`, `pdo_mysql`, `tokenizer`, `xml`, and `zip` +- PHP 8.2+ with the required extensions (16 in total), including `ctype`, `curl`, `dom`, `fileinfo`, `json`, `mbstring`, `openssl`, `pdo_mysql`, `tokenizer`, `xml`, and `zip`. Additional extensions (`gd`/`imagick`, `intl`, `redis`, `bcmath`, and others) are optional and only needed for the features that use them — see [docs/requirements.md](docs/requirements.md) - MySQL 8.0+ or MariaDB 10.3+ (utf8mb4) - Composer 2.x - Node.js 20+ (only needed when building frontend assets) @@ -528,15 +528,17 @@ Thanks to everyone who reported an issue or suggested a feature that shipped —

jiwonpapa + Tuwasduliebst glitter-gim jordy-bitree laelbe + lyg-kaban bigmsg hwaryeon1234 + koojunho abc101 yks118 kitrio - koojunho movielee2020 ChoDongHyeon comtylove-netizen diff --git a/app/Console/Commands/ExpireIdentityChallengesCommand.php b/app/Console/Commands/ExpireIdentityChallengesCommand.php new file mode 100644 index 00000000..422ffedd --- /dev/null +++ b/app/Console/Commands/ExpireIdentityChallengesCommand.php @@ -0,0 +1,39 @@ +expirePastDue(); + + $this->info("본인인증 challenge {$expired}건이 만료 처리되었습니다."); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Module/ClearModuleCacheCommand.php b/app/Console/Commands/Module/ClearModuleCacheCommand.php index 6c2d5362..5acd7b49 100644 --- a/app/Console/Commands/Module/ClearModuleCacheCommand.php +++ b/app/Console/Commands/Module/ClearModuleCacheCommand.php @@ -2,15 +2,22 @@ namespace App\Console\Commands\Module; -use App\Contracts\Extension\CacheInterface; -use App\Contracts\Repositories\ModuleRepositoryInterface; +use App\Contracts\Repositories\LayoutRepositoryInterface; +use App\Enums\LayoutSourceType; +use App\Extension\ExtensionMiddlewareRegistry; use App\Extension\ModuleManager; +use App\Extension\Traits\ClearsTemplateCaches; +use App\Extension\Traits\InvalidatesLayoutCache; use App\Services\ExtensionBundleService; +use App\Services\LayoutResolverService; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; class ClearModuleCacheCommand extends Command { + use ClearsTemplateCaches; + use InvalidatesLayoutCache; + /** * The name and signature of the console command. */ @@ -27,15 +34,19 @@ class ClearModuleCacheCommand extends Command */ public function __construct( private ModuleManager $moduleManager, - private ModuleRepositoryInterface $moduleRepository, - private CacheInterface $cache, - private ExtensionBundleService $bundleService + protected LayoutRepositoryInterface $layoutRepository, + private ExtensionBundleService $bundleService, + private LayoutResolverService $layoutResolver ) { parent::__construct(); } /** * Execute the console command. + * + * 실재하는 서버 캐시(모듈 레이아웃 서빙/해석 캐시 + 상태 키 + 미들웨어 인덱스 + * + 버전 포함 키)를 무효화한다. + * 종전 forget 대상(module.config.{id} 등)은 writer 가 없는 유령 키였다 (#588). */ public function handle(): int { @@ -61,6 +72,11 @@ class ClearModuleCacheCommand extends Command $clearedCount = $this->clearModuleCache($identifier); + // 상태 키(ext.modules.*) + 미들웨어 인덱스 + 버전 포함 키 무효화 (버전 bump) + ModuleManager::invalidateModuleStatusCache(); + ExtensionMiddlewareRegistry::flush(); + $this->incrementExtensionCacheVersion(); + $this->info('✅ '.__('modules.commands.cache_clear.success_single', [ 'module' => $identifier, 'count' => $clearedCount, @@ -69,25 +85,16 @@ class ClearModuleCacheCommand extends Command // 모든 모듈 캐시 삭제 $this->info(__('modules.commands.cache_clear.clearing_all')); - // 전체 모듈 캐시 키 삭제 - $cacheKeys = [ - 'modules.all', - 'modules.active', - 'modules.installed', - ]; - - foreach ($cacheKeys as $key) { - if ($this->cache->forget($key)) { - $clearedCount++; - } - } - // 각 모듈별 캐시 삭제 - $allModules = $this->moduleManager->getAllModules(); - foreach ($allModules as $moduleName => $module) { + foreach ($this->moduleManager->getAllModules() as $module) { $clearedCount += $this->clearModuleCache($module->getIdentifier()); } + // 상태 키 + 미들웨어 인덱스 + 버전 포함 키 무효화는 전체에서 1회면 충분 + ModuleManager::invalidateModuleStatusCache(); + ExtensionMiddlewareRegistry::flush(); + $this->incrementExtensionCacheVersion(); + // 모듈 프론트엔드 병합 번들 파일 삭제 (캐시 키 forget 만으로는 미삭제) $clearedCount += $this->bundleService->clearBundles('module'); @@ -113,26 +120,23 @@ class ClearModuleCacheCommand extends Command /** * 특정 모듈의 캐시를 삭제합니다. + * + * 모듈이 소유한 실재 서버 캐시는 레이아웃 캐시(서빙/병합 + 해석)다 — 각각 + * 트레이트(InvalidatesLayoutCache)와 LayoutResolverService 단일 지점으로 + * 위임해 키 규약 드리프트를 방지한다. 해석 캐시(layout_resolver.*)는 버전 + * 접미사 없는 고정 키에 레이아웃 row ID 를 저장하므로 능동 삭제하지 않으면 + * TTL 동안 이전 해석 결과가 유지된다 (#588 동종 보강). + * + * @param string $identifier 모듈 식별자 + * @return int 캐시를 무효화한 레이아웃 수 */ private function clearModuleCache(string $identifier): int { - $clearedCount = 0; + $this->invalidateExtensionLayoutCache($identifier, 'module'); + $this->layoutResolver->clearResolutionCacheByModule($identifier); - // 모듈별 캐시 키 - $cacheKeys = [ - "module.config.{$identifier}", - "module.info.{$identifier}", - "module.routes.{$identifier}", - "module.permissions.{$identifier}", - "module.menus.{$identifier}", - ]; - - foreach ($cacheKeys as $key) { - if ($this->cache->forget($key)) { - $clearedCount++; - } - } - - return $clearedCount; + return $this->layoutRepository + ->getBySourceIdentifier($identifier, LayoutSourceType::Module) + ->count(); } } diff --git a/app/Console/Commands/Plugin/ClearPluginCacheCommand.php b/app/Console/Commands/Plugin/ClearPluginCacheCommand.php index 0dd0aa42..8e3bcb22 100644 --- a/app/Console/Commands/Plugin/ClearPluginCacheCommand.php +++ b/app/Console/Commands/Plugin/ClearPluginCacheCommand.php @@ -2,15 +2,21 @@ namespace App\Console\Commands\Plugin; -use App\Contracts\Extension\CacheInterface; -use App\Contracts\Repositories\PluginRepositoryInterface; +use App\Contracts\Repositories\LayoutRepositoryInterface; +use App\Enums\LayoutSourceType; +use App\Extension\ExtensionMiddlewareRegistry; use App\Extension\PluginManager; +use App\Extension\Traits\ClearsTemplateCaches; +use App\Extension\Traits\InvalidatesLayoutCache; use App\Services\ExtensionBundleService; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; class ClearPluginCacheCommand extends Command { + use ClearsTemplateCaches; + use InvalidatesLayoutCache; + /** * The name and signature of the console command. */ @@ -27,8 +33,7 @@ class ClearPluginCacheCommand extends Command */ public function __construct( private PluginManager $pluginManager, - private PluginRepositoryInterface $pluginRepository, - private CacheInterface $cache, + protected LayoutRepositoryInterface $layoutRepository, private ExtensionBundleService $bundleService ) { parent::__construct(); @@ -36,6 +41,12 @@ class ClearPluginCacheCommand extends Command /** * Execute the console command. + * + * 실재하는 서버 캐시(플러그인 레이아웃 캐시 + 상태 키 + 미들웨어 인덱스 + * + 버전 포함 키)를 무효화한다. + * 종전 forget 대상(plugin.config.{id} 등)은 writer 가 없는 유령 키였다 (#588). + * 해석 캐시(layout_resolver.*)는 모듈 레이아웃 전용이라 플러그인은 비대상 + * (LayoutResolverService::resolve 의 fromModules 스코프는 plugin row 를 반환하지 않음). */ public function handle(): int { @@ -61,6 +72,11 @@ class ClearPluginCacheCommand extends Command $clearedCount = $this->clearPluginCache($identifier); + // 상태 키(ext.plugins.*) + 미들웨어 인덱스 + 버전 포함 키 무효화 (버전 bump) + PluginManager::invalidatePluginStatusCache(); + ExtensionMiddlewareRegistry::flush(); + $this->incrementExtensionCacheVersion(); + $this->info('✅ '.__('plugins.commands.cache_clear.success_single', [ 'plugin' => $identifier, 'count' => $clearedCount, @@ -69,25 +85,16 @@ class ClearPluginCacheCommand extends Command // 모든 플러그인 캐시 삭제 $this->info(__('plugins.commands.cache_clear.clearing_all')); - // 전체 플러그인 캐시 키 삭제 - $cacheKeys = [ - 'plugins.all', - 'plugins.active', - 'plugins.installed', - ]; - - foreach ($cacheKeys as $key) { - if ($this->cache->forget($key)) { - $clearedCount++; - } - } - // 각 플러그인별 캐시 삭제 - $allPlugins = $this->pluginManager->getAllPlugins(); - foreach ($allPlugins as $pluginName => $plugin) { + foreach ($this->pluginManager->getAllPlugins() as $plugin) { $clearedCount += $this->clearPluginCache($plugin->getIdentifier()); } + // 상태 키 + 미들웨어 인덱스 + 버전 포함 키 무효화는 전체에서 1회면 충분 + PluginManager::invalidatePluginStatusCache(); + ExtensionMiddlewareRegistry::flush(); + $this->incrementExtensionCacheVersion(); + // 플러그인 프론트엔드 병합 번들 파일 삭제 (캐시 키 forget 만으로는 미삭제) $clearedCount += $this->bundleService->clearBundles('plugin'); @@ -113,25 +120,18 @@ class ClearPluginCacheCommand extends Command /** * 특정 플러그인의 캐시를 삭제합니다. + * + * 플러그인이 소유한 실재 서버 캐시는 레이아웃 캐시뿐이다 — 트레이트 단일 지점 + * (InvalidatesLayoutCache)으로 위임해 키 규약 드리프트를 방지한다. + * + * @return int 캐시를 무효화한 레이아웃 수 */ private function clearPluginCache(string $identifier): int { - $clearedCount = 0; + $this->invalidateExtensionLayoutCache($identifier, 'plugin'); - // 플러그인별 캐시 키 (메뉴 캐시 없음) - $cacheKeys = [ - "plugin.config.{$identifier}", - "plugin.info.{$identifier}", - "plugin.routes.{$identifier}", - "plugin.permissions.{$identifier}", - ]; - - foreach ($cacheKeys as $key) { - if ($this->cache->forget($key)) { - $clearedCount++; - } - } - - return $clearedCount; + return $this->layoutRepository + ->getBySourceIdentifier($identifier, LayoutSourceType::Plugin) + ->count(); } } diff --git a/app/Console/Commands/PruneActivityLogsCommand.php b/app/Console/Commands/PruneActivityLogsCommand.php new file mode 100644 index 00000000..f0c35bbf --- /dev/null +++ b/app/Console/Commands/PruneActivityLogsCommand.php @@ -0,0 +1,41 @@ +option('days'); + $deleted = $service->prune($days); + + $this->info("활동 로그 {$deleted}건이 삭제되었습니다. (보존 {$days}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/PruneIdentityLogsCommand.php b/app/Console/Commands/PruneIdentityLogsCommand.php new file mode 100644 index 00000000..13e8f655 --- /dev/null +++ b/app/Console/Commands/PruneIdentityLogsCommand.php @@ -0,0 +1,43 @@ +option('days'); + $purged = $service->purge($days); + + $this->info("본인인증 이력 {$purged}건이 파기되었습니다. (보관 {$days}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/PruneNotificationLogsCommand.php b/app/Console/Commands/PruneNotificationLogsCommand.php new file mode 100644 index 00000000..f75220c0 --- /dev/null +++ b/app/Console/Commands/PruneNotificationLogsCommand.php @@ -0,0 +1,41 @@ +option('days'); + $deleted = $service->prune($days); + + $this->info("알림 발송 이력 {$deleted}건이 삭제되었습니다. (보존 {$days}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/PruneOrphanAttachmentsCommand.php b/app/Console/Commands/PruneOrphanAttachmentsCommand.php new file mode 100644 index 00000000..a4ef255e --- /dev/null +++ b/app/Console/Commands/PruneOrphanAttachmentsCommand.php @@ -0,0 +1,123 @@ +option('scheduled') && ! $this->isCleanupEnabled()) { + $this->info('고아 첨부 자동 정리가 꺼져 있어 실행하지 않았습니다. (upload.orphan_cleanup_enabled = false)'); + + return Command::SUCCESS; + } + + $days = $this->resolveRetentionDays(); + + if ($days < 1) { + $this->info('고아 첨부 보존기간이 1일 미만이어서 정리를 수행하지 않았습니다.'); + + return Command::SUCCESS; + } + + $limit = max(1, (int) $this->option('limit')); + $isDryRun = (bool) $this->option('dry-run'); + + $result = $this->attachmentService->pruneOrphans($days, $limit, $isDryRun); + + if ($isDryRun) { + $this->info("[DRY RUN] 보존기간({$days}일) 경과 고아 첨부: {$result['scanned']}건"); + + return Command::SUCCESS; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 고아 첨부 %d건 중 %d건을 삭제했습니다. (실패 %d건)', + $days, + $result['scanned'], + $result['deleted'], + $result['failed'], + )); + + Log::info('PruneOrphanAttachmentsCommand: 고아 첨부 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + ] + $result); + + return Command::SUCCESS; + } + + /** + * 자동 정리 토글을 false 폴백으로 조회합니다. + * + * @return bool 자동 정리 활성 여부 + */ + private function isCleanupEnabled(): bool + { + return (bool) g7_core_settings('upload.orphan_cleanup_enabled', false); + } + + /** + * 보존기간을 해석합니다 (옵션 > 환경설정 > 기본 30일). + * + * @return int 보존기간(일) + */ + private function resolveRetentionDays(): int + { + $option = $this->option('days'); + + if ($option !== null && $option !== '') { + return (int) $option; + } + + return (int) g7_core_settings('upload.orphan_retention_days', 30); + } +} diff --git a/app/Console/Commands/PruneScheduleHistoryCommand.php b/app/Console/Commands/PruneScheduleHistoryCommand.php new file mode 100644 index 00000000..f7e88717 --- /dev/null +++ b/app/Console/Commands/PruneScheduleHistoryCommand.php @@ -0,0 +1,43 @@ +option('days'); + $deleted = $service->pruneHistory($days); + + $this->info("스케줄 실행 이력 {$deleted}건이 삭제되었습니다. (보존 {$days}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/PruneSeoCacheStatsCommand.php b/app/Console/Commands/PruneSeoCacheStatsCommand.php new file mode 100644 index 00000000..5902b71b --- /dev/null +++ b/app/Console/Commands/PruneSeoCacheStatsCommand.php @@ -0,0 +1,43 @@ +option('days'); + $deleted = $service->cleanup($days); + + $this->info("SEO 캐시 통계 {$deleted}건이 삭제되었습니다. (보존 {$days}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/PruneStorageLeftoversCommand.php b/app/Console/Commands/PruneStorageLeftoversCommand.php new file mode 100644 index 00000000..2f46d720 --- /dev/null +++ b/app/Console/Commands/PruneStorageLeftoversCommand.php @@ -0,0 +1,81 @@ + + */ + private const GROUP_LABELS = [ + 'staging' => '업데이트 스테이징 잔존', + 'temp' => '코어 임시 산출물', + 'vendor_bundle_staging' => 'vendor 번들 스테이징', + 'extension_backups' => '확장 백업', + 'core_backups' => '코어 백업', + 'legacy_browser_log' => '레거시 브라우저 로그', + ]; + + /** + * Execute the console command. + * + * @param StorageLeftoverPruneService $service 잔존물 정리 서비스 + * @return int 명령 실행 결과 코드 + */ + public function handle(StorageLeftoverPruneService $service): int + { + $days = max(0, (int) $this->option('days')); + $backupDays = max(0, (int) $this->option('backup-days')); + $dryRun = (bool) $this->option('dry-run'); + + $result = $service->prune($days, $backupDays, $dryRun); + + $total = 0; + + foreach (self::GROUP_LABELS as $key => $label) { + $paths = $result[$key] ?? []; + $count = count($paths); + $total += $count; + + $this->line("{$label}: {$count}건"); + + if ($dryRun) { + foreach ($paths as $path) { + $this->line(" - {$path}"); + } + } + } + + $this->info($dryRun + ? "삭제 대상 합계 {$total}건 (dry-run — 실제 삭제 없음)" + : "합계 {$total}건이 삭제되었습니다. (임시 보존일: {$days}일, 백업 보존일: {$backupDays}일)"); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/Template/ClearTemplateCacheCommand.php b/app/Console/Commands/Template/ClearTemplateCacheCommand.php index ca13ec43..cc52d257 100644 --- a/app/Console/Commands/Template/ClearTemplateCacheCommand.php +++ b/app/Console/Commands/Template/ClearTemplateCacheCommand.php @@ -2,16 +2,17 @@ namespace App\Console\Commands\Template; -use App\Contracts\Extension\CacheInterface; use App\Extension\TemplateManager; +use App\Extension\Traits\ClearsTemplateCaches; use App\Models\Template; -use App\Models\TemplateLayout; use App\Services\ExtensionBundleService; use Illuminate\Console\Command; use Illuminate\Support\Facades\Log; class ClearTemplateCacheCommand extends Command { + use ClearsTemplateCaches; + /** * The name and signature of the console command. */ @@ -28,7 +29,6 @@ class ClearTemplateCacheCommand extends Command */ public function __construct( private TemplateManager $templateManager, - private CacheInterface $cache, private ExtensionBundleService $bundleService ) { parent::__construct(); @@ -67,6 +67,10 @@ class ClearTemplateCacheCommand extends Command /** * 특정 템플릿의 캐시 삭제 + * + * 라이프사이클(update/deactivate/uninstall)과 동일한 무효화 단일 지점 + * (`TemplateManager::clearTemplateCache()`)을 경유한다 — 커맨드가 키 목록을 + * 별도로 유지하면 키 규약 변경 시 유령 forget 으로 사문화된다 (#588, 공개 #119). */ private function clearSingleTemplateCache(string $identifier): void { @@ -78,34 +82,12 @@ class ClearTemplateCacheCommand extends Command $this->info(__('templates.commands.cache_clear.clearing_single', ['template' => $identifier])); - $clearedCount = 0; + // 고정 키(config/components_manifest) + 현재 버전 routes/language + 레이아웃 캐시 + $clearedCount = $this->templateManager->clearTemplateCache($identifier); - // 1. 레이아웃 캐시 삭제 - $templateRecord = Template::where('identifier', $identifier)->first(); - if ($templateRecord) { - $layouts = TemplateLayout::where('template_id', $templateRecord->id)->get(); - foreach ($layouts as $layout) { - $this->cache->forget("layout.{$identifier}.{$layout->name}"); - $clearedCount++; - } - } - - // 2. Routes 캐시 삭제 - $this->cache->forget("template.routes.{$identifier}"); - $clearedCount++; - - // 3. 다국어 파일 캐시 삭제 - $supportedLocales = config('app.supported_locales', ['ko', 'en']); - foreach ($supportedLocales as $locale) { - $this->cache->forget("template.language.{$identifier}.{$locale}"); - $clearedCount++; - } - - // 4. 활성 템플릿 타입 캐시 삭제 - if ($templateRecord) { - $this->cache->forget("templates.active.{$templateRecord->type}"); - $clearedCount++; - } + // 상태 키(ext.templates.*) + 버전 포함 키 무효화 (버전 bump) + TemplateManager::invalidateTemplateStatusCache(); + $this->incrementExtensionCacheVersion(); $this->info('✅ '.__('templates.commands.cache_clear.success_single', [ 'template' => $identifier, @@ -126,36 +108,17 @@ class ClearTemplateCacheCommand extends Command $this->info(__('templates.commands.cache_clear.clearing_all')); $clearedCount = 0; - $supportedLocales = config('app.supported_locales', ['ko', 'en']); - // 모든 설치된 템플릿의 캐시 삭제 - $templates = Template::all(); - - foreach ($templates as $templateRecord) { - // 1. 레이아웃 캐시 삭제 - $layouts = TemplateLayout::where('template_id', $templateRecord->id)->get(); - foreach ($layouts as $layout) { - $this->cache->forget("layout.{$templateRecord->identifier}.{$layout->name}"); - $clearedCount++; - } - - // 2. Routes 캐시 삭제 - $this->cache->forget("template.routes.{$templateRecord->identifier}"); - $clearedCount++; - - // 3. 다국어 파일 캐시 삭제 - foreach ($supportedLocales as $locale) { - $this->cache->forget("template.language.{$templateRecord->identifier}.{$locale}"); - $clearedCount++; - } + // 모든 설치된 템플릿의 캐시 삭제 (설치 레코드 기준 — 라이프사이클과 동일 지점) + foreach (Template::all() as $templateRecord) { + $clearedCount += $this->templateManager->clearTemplateCache($templateRecord->identifier); } - // 4. 활성 템플릿 타입 캐시 삭제 - $this->cache->forget('templates.active.admin'); - $this->cache->forget('templates.active.user'); - $clearedCount += 2; + // 상태 키 + 버전 포함 키 무효화는 전체에서 1회면 충분 + TemplateManager::invalidateTemplateStatusCache(); + $this->incrementExtensionCacheVersion(); - // 5. 확장 프론트엔드 병합 번들 파일 전체 삭제 (템플릿 캐시 정리는 페이지 전면 갱신) + // 확장 프론트엔드 병합 번들 파일 전체 삭제 (템플릿 캐시 정리는 페이지 전면 갱신) $clearedCount += $this->bundleService->clearBundles(); $this->info('✅ '.__('templates.commands.cache_clear.success_all', [ diff --git a/app/Console/Commands/Traits/HasUnifiedConfirm.php b/app/Console/Commands/Traits/HasUnifiedConfirm.php index bac68bf4..f549b057 100644 --- a/app/Console/Commands/Traits/HasUnifiedConfirm.php +++ b/app/Console/Commands/Traits/HasUnifiedConfirm.php @@ -3,6 +3,7 @@ namespace App\Console\Commands\Traits; use App\Console\Helpers\ConsoleConfirm; +use Symfony\Component\Console\Input\StreamableInputInterface; /** * Laravel Command 컨텍스트에서 표준화된 yes/no 프롬프트를 제공하는 트레이트. @@ -12,6 +13,7 @@ use App\Console\Helpers\ConsoleConfirm; * 재질문 루프는 ConsoleConfirm::parse() 와 공유한다. * * - `--no-interaction` 시: $default 즉시 반환 + * - 응답을 받을 수 없는 실행(콘솔 없는 STDIN) 시: $default 즉시 반환 * - empty 입력 시: $default 반환 * - yes/y → true, no/n → false (대소문자 무시) * - 그 외 입력: "yes, y, no, n 중 하나로 입력해 주세요." 출력 후 재질문 @@ -26,7 +28,7 @@ trait HasUnifiedConfirm */ protected function unifiedConfirm(string $question, bool $default = false): bool { - if (! $this->input->isInteractive()) { + if (! $this->input->isInteractive() || ! $this->canPromptForAnswer()) { return $default; } @@ -47,4 +49,38 @@ trait HasUnifiedConfirm $this->warn(' yes, y, no, n 중 하나로 입력해 주세요.'); } } + + /** + * 이 실행이 프롬프트 응답을 실제로 받을 수 있는지 판정합니다. + * + * Symfony 는 `posix_isatty()` 로 비대화 실행을 감지해 `isInteractive()` 를 false 로 + * 내리는데, Windows PHP 에는 posix 확장이 없어 그 분기가 실행되지 않는다. 그래서 + * `--no-interaction` 없이 CI·스케줄러·에이전트처럼 콘솔이 없는 곳에서 부르면 + * `isInteractive()` 가 true 로 남고 `ask()` 가 오지 않을 응답을 무한히 기다린다. + * 프롬프트 출력마저 버퍼에 갇혀 있어 겉으로는 "커맨드가 느리다" 로만 보인다. + * + * `stream_isatty()` 는 posix 확장 없이도 Windows 를 포함해 동작하므로 이를 쓴다. + * 판정 재료가 없으면 true 를 돌려 기존 동작(질문)을 유지한다 — 물어보지 못해 멈추는 + * 것보다 물어볼 수 있는데 안 묻는 쪽이 더 위험하기 때문이다. + * + * @return bool 응답을 받을 수 있으면 true + */ + protected function canPromptForAnswer(): bool + { + // 테스트는 QuestionHelper 를 대체하거나 입력을 주입하므로 STDIN 을 쓰지 않는다 + if ($this->laravel !== null && $this->laravel->runningUnitTests()) { + return true; + } + + // 입력 스트림이 주입된 실행(CommandTester::setInputs 등)도 STDIN 과 무관하다 + if ($this->input instanceof StreamableInputInterface && is_resource($this->input->getStream())) { + return true; + } + + if (! \defined('STDIN') || ! is_resource(STDIN) || ! \function_exists('stream_isatty')) { + return true; + } + + return stream_isatty(STDIN); + } } diff --git a/app/Contracts/Extension/CacheableExtensionInterface.php b/app/Contracts/Extension/CacheableExtensionInterface.php index 82bae277..29f70d15 100644 --- a/app/Contracts/Extension/CacheableExtensionInterface.php +++ b/app/Contracts/Extension/CacheableExtensionInterface.php @@ -25,6 +25,17 @@ interface CacheableExtensionInterface */ public function getStorage(): StorageInterface; + /** + * 카테고리별 스토리지 드라이버를 반환합니다. + * + * getStorageDiskFor($category) 가 결정한 디스크의 인스턴스를 반환하며, + * 기본 구현은 getStorage() 와 동일한 디스크를 사용합니다. + * + * @param string $category 카테고리 (settings, attachments, images, cache, temp) + * @return StorageInterface 카테고리 디스크의 스토리지 인스턴스 + */ + public function getStorageFor(string $category): StorageInterface; + /** * 확장 도메인에 격리된 캐시 드라이버를 반환합니다. * diff --git a/app/Contracts/Extension/StorageInterface.php b/app/Contracts/Extension/StorageInterface.php index efc2a8d9..f1b30113 100644 --- a/app/Contracts/Extension/StorageInterface.php +++ b/app/Contracts/Extension/StorageInterface.php @@ -2,6 +2,8 @@ namespace App\Contracts\Extension; +use Symfony\Component\HttpFoundation\StreamedResponse; + /** * 확장(모듈/플러그인) 스토리지 인터페이스 * @@ -50,12 +52,14 @@ interface StorageInterface /** * 파일의 공개 URL을 반환합니다. * - * public disk인 경우 직접 URL을 반환하고, - * private disk인 경우 null을 반환합니다 (별도 API 엔드포인트 사용). + * public 디스크이거나 `filesystems.disks.{disk}.url` 이 설정된 디스크(S3+CDN 등)면 + * 직접 URL 을 반환하고, 그 외에는 null 을 반환합니다 (별도 API 엔드포인트 사용). + * 생성 결과는 디스크 종류와 무관하게 `core.storage.filter_url` 필터 훅을 항상 통과하므로, + * 확장이 URL 을 공급/수정/차단할 수 있습니다 (null 반환도 훅 발화 대상). * * @param string $category 카테고리 * @param string $path 파일 경로 - * @return string|null 파일 URL (private disk인 경우 null) + * @return string|null 파일 URL (직접 URL 불가 디스크이고 훅 공급도 없으면 null) */ public function url(string $category, string $path): ?string; @@ -110,9 +114,9 @@ interface StorageInterface * @param string $path 파일 경로 * @param string $filename 다운로드 시 표시될 파일명 * @param array $headers 추가 HTTP 헤더 - * @return \Symfony\Component\HttpFoundation\StreamedResponse|null 파일 스트림 (파일이 없으면 null) + * @return StreamedResponse|null 파일 스트림 (파일이 없으면 null) */ - public function response(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse; + public function response(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse; /** * 사용할 디스크를 변경한 새 인스턴스를 반환합니다. @@ -132,7 +136,7 @@ interface StorageInterface * @param string $path 파일 경로 * @param string $filename 다운로드 시 표시될 파일명 * @param array $headers 추가 HTTP 헤더 - * @return \Symfony\Component\HttpFoundation\StreamedResponse|null 다운로드 응답 (파일이 없으면 null) + * @return StreamedResponse|null 다운로드 응답 (파일이 없으면 null) */ - public function download(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse; + public function download(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse; } diff --git a/app/Contracts/Extension/TemplateManagerInterface.php b/app/Contracts/Extension/TemplateManagerInterface.php index 44faaacc..f0e4d785 100644 --- a/app/Contracts/Extension/TemplateManagerInterface.php +++ b/app/Contracts/Extension/TemplateManagerInterface.php @@ -7,10 +7,18 @@ use App\Enums\DeactivationReason; interface TemplateManagerInterface { /** - * 모든 템플릿을 로드하고 초기화합니다. + * 모든 템플릿을 로드하고 초기화합니다. (항상 재스캔) */ public function loadTemplates(): void; + /** + * 템플릿이 아직 로드되지 않았을 때만 로드합니다. (멱등) + * + * "맵이 채워져 있기만 하면 되는" 소비자용 진입점. 재스캔이 필요한 경우 + * (설치/삭제/업데이트 직후)에만 `loadTemplates()` 를 직접 호출합니다. + */ + public function ensureLoaded(): void; + /** * /templates 디렉토리를 스캔하여 사용 가능한 템플릿을 발견합니다. * diff --git a/app/Contracts/Repositories/ActivityLogRepositoryInterface.php b/app/Contracts/Repositories/ActivityLogRepositoryInterface.php index 871d08fa..3bb3bb64 100644 --- a/app/Contracts/Repositories/ActivityLogRepositoryInterface.php +++ b/app/Contracts/Repositories/ActivityLogRepositoryInterface.php @@ -65,4 +65,12 @@ interface ActivityLogRepositoryInterface * @return int 익명화된 row 수 */ public function anonymizeUserId(int $userId): int; + + /** + * 보존 기간이 지난 활동 로그를 삭제합니다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function deleteOlderThan(int $days): int; } diff --git a/app/Contracts/Repositories/AttachmentRepositoryInterface.php b/app/Contracts/Repositories/AttachmentRepositoryInterface.php index f62684cb..471c653e 100644 --- a/app/Contracts/Repositories/AttachmentRepositoryInterface.php +++ b/app/Contracts/Repositories/AttachmentRepositoryInterface.php @@ -4,6 +4,7 @@ namespace App\Contracts\Repositories; use App\Models\Attachment; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; /** * 첨부파일 Repository 인터페이스 @@ -18,6 +19,17 @@ interface AttachmentRepositoryInterface */ public function findById(int $id): ?Attachment; + /** + * 소프트삭제된 행까지 포함해 ID로 첨부파일 조회 + * + * 영구 삭제(파일+forceDelete) 경로 전용이다. 소프트삭제된 행은 기본 조회에서 빠지므로, + * 이 메서드 없이 영구 삭제를 시도하면 대상을 찾지 못해 매 회차 실패만 누적된다. + * + * @param int $id 첨부파일 ID + * @return Attachment|null 첨부파일 또는 null + */ + public function findByIdWithTrashed(int $id): ?Attachment; + /** * 여러 ID로 첨부파일 조회 (order 정렬) * @@ -145,4 +157,20 @@ interface AttachmentRepositoryInterface * @param string $collection 컬렉션명 */ public function reorderAfterDelete(string $attachmentableType, int $attachmentableId, string $collection): void; + + /** + * 소유자 없이 방치된 고아 첨부 후보를 오래된 순으로 조회합니다. + * + * 폼 저장 전에 즉시 업로드되는 첨부는 소유자(attachmentable) 없이 먼저 생성되므로, + * 폼을 저장하지 않고 이탈하면 그대로 남습니다. 그 회수 대상을 찾습니다. + * + * 확장이 소유한 첨부(source_identifier 보유)는 그 확장의 라이프사이클 소관이므로 제외하고, + * 현역으로 쓰이는 첨부 ID 는 호출자가 넘겨 보호합니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 업로드가 대상) + * @param int $limit 최대 조회 건수 + * @param array $protectedIds 보호할 첨부 ID 목록 (현역 사이트 로고 등) + * @return Collection 고아 첨부 후보 (created_at 오름차순, 소프트 삭제분 포함) + */ + public function findOrphanCandidates(Carbon $threshold, int $limit, array $protectedIds = []): Collection; } diff --git a/app/Contracts/Repositories/LanguagePackRepositoryInterface.php b/app/Contracts/Repositories/LanguagePackRepositoryInterface.php index accb74c2..76ed8893 100644 --- a/app/Contracts/Repositories/LanguagePackRepositoryInterface.php +++ b/app/Contracts/Repositories/LanguagePackRepositoryInterface.php @@ -72,13 +72,24 @@ interface LanguagePackRepositoryInterface public function getActiveCoreLocales(): array; /** - * 페이지네이션 + 필터링된 언어팩 목록을 조회합니다. + * 페이지네이션 + 필터링된 언어팩 목록을 조회합니다 (관리자 목록 전용). + * + * 전량 순회 용도로는 쓰지 않는다 — page 를 생략하면 HTTP `page` 파라미터가 암묵 + * 해석되어, 무관한 요청 파라미터가 순회 범위를 바꾼다({@see self::allForUpdateCheck()}). * * @param array $filters 필터 (scope, target_identifier, locale, status, vendor) * @param int $perPage 페이지당 건수 + * @param int|null $page 페이지 번호 (null 이면 요청 파라미터에서 해석) * @return LengthAwarePaginator 페이지네이션 결과 */ - public function paginate(array $filters = [], int $perPage = 20): LengthAwarePaginator; + public function paginate(array $filters = [], int $perPage = 20, ?int $page = null): LengthAwarePaginator; + + /** + * 업데이트 확인용 전체 언어팩 컬렉션을 조회합니다. + * + * @return Collection 설치된 전체 언어팩 + */ + public function allForUpdateCheck(): Collection; /** * 필터링된 언어팩 컬렉션을 페이지네이션 없이 조회합니다. diff --git a/app/Contracts/Repositories/MenuRepositoryInterface.php b/app/Contracts/Repositories/MenuRepositoryInterface.php index 14b26a0d..0a2ae822 100644 --- a/app/Contracts/Repositories/MenuRepositoryInterface.php +++ b/app/Contracts/Repositories/MenuRepositoryInterface.php @@ -40,6 +40,17 @@ interface MenuRepositoryInterface */ public function findById(int $id): ?Menu; + /** + * 여러 ID로 메뉴를 한 번에 조회합니다. + * + * 정적 라우트(`PUT menus/order`)에서 스코프 게이트를 재적용할 때 대상 전체를 한 번에 + * 확인하기 위한 조회입니다. 관계는 로드하지 않습니다(소유자 판정에 불필요). + * + * @param array $ids 메뉴 ID 목록 + * @return Collection 메뉴 컬렉션 + */ + public function findByIds(array $ids): Collection; + /** * 슬러그로 메뉴를 찾습니다. * @@ -48,6 +59,14 @@ interface MenuRepositoryInterface */ public function findBySlug(string $slug): ?Menu; + /** + * URL 로 메뉴를 찾습니다. + * + * @param string $url 메뉴 URL + * @return Menu|null 찾은 메뉴 모델 또는 null + */ + public function findByUrl(string $url): ?Menu; + /** * 새로운 메뉴를 생성합니다. * diff --git a/app/Contracts/Repositories/NotificationLogRepositoryInterface.php b/app/Contracts/Repositories/NotificationLogRepositoryInterface.php index 0673c73d..86190df2 100644 --- a/app/Contracts/Repositories/NotificationLogRepositoryInterface.php +++ b/app/Contracts/Repositories/NotificationLogRepositoryInterface.php @@ -40,6 +40,14 @@ interface NotificationLogRepositoryInterface */ public function bulkDelete(array $ids): int; + /** + * 보존 기간이 지난 발송 이력을 삭제합니다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function deleteOlderThan(int $days): int; + /** * 페이지네이션 목록 조회. * diff --git a/app/Contracts/Repositories/PermissionRepositoryInterface.php b/app/Contracts/Repositories/PermissionRepositoryInterface.php index f4ea99e0..77bbacbd 100644 --- a/app/Contracts/Repositories/PermissionRepositoryInterface.php +++ b/app/Contracts/Repositories/PermissionRepositoryInterface.php @@ -31,6 +31,14 @@ interface PermissionRepositoryInterface */ public function findByIdentifier(string $identifier): ?Permission; + /** + * 여러 ID로 권한을 일괄 조회합니다. + * + * @param array $ids 권한 ID 배열 + * @return Collection 권한 컬렉션 (ID 기준) + */ + public function getByIds(array $ids): Collection; + /** * 새로운 권한을 생성합니다. * diff --git a/app/Exceptions/CannotModifyProtectedRoleException.php b/app/Exceptions/CannotModifyProtectedRoleException.php new file mode 100644 index 00000000..6580858a --- /dev/null +++ b/app/Exceptions/CannotModifyProtectedRoleException.php @@ -0,0 +1,23 @@ + + */ + protected array $storageCategoryServices = []; + /** * CacheInterface 가 필요한 서비스 클래스 목록. * @@ -111,13 +122,17 @@ abstract class AbstractExtensionServiceProvider extends ServiceProvider */ protected function registerStorageBindings(): void { - if (empty($this->storageServices)) { - return; + if (! empty($this->storageServices)) { + $this->app->when($this->storageServices) + ->needs(StorageInterface::class) + ->give(fn () => $this->resolveExtension()->getStorage()); } - $this->app->when($this->storageServices) - ->needs(StorageInterface::class) - ->give(fn () => $this->resolveExtension()->getStorage()); + foreach ($this->storageCategoryServices as $service => $category) { + $this->app->when($service) + ->needs(StorageInterface::class) + ->give(fn () => $this->resolveExtension()->getStorageFor($category)); + } } /** diff --git a/app/Extension/AbstractModule.php b/app/Extension/AbstractModule.php index 66fea241..5bdfde59 100644 --- a/app/Extension/AbstractModule.php +++ b/app/Extension/AbstractModule.php @@ -927,6 +927,31 @@ abstract class AbstractModule implements CacheableExtensionInterface, ModuleInte return []; } + /** + * 신뢰하는 외부 스크립트 호스트 목록을 반환합니다. + * + * module.json 의 `trusted_script_hosts` 배열에서 읽습니다. 이 모듈이 레이아웃 + * `scripts[].src` 로 로드하는 외부 CDN 호스트를 선언합니다. 코어는 이 목록을 + * 집계(AbstractModule/AbstractPlugin → TrustedScriptHosts)해 런타임 스크립트 로더· + * 저장측 검증·정적 검사가 same-origin 이 아닌 스크립트 중 **선언된 호스트만** 허용하도록 + * 합니다 (KVE-2026-1915 신뢰 출처 허용목록). + * + * @return array 신뢰 호스트명 목록 (예: ['cdn.example.com']) + */ + public function getTrustedScriptHosts(): array + { + $hosts = $this->loadManifest()['trusted_script_hosts'] ?? []; + + if (! is_array($hosts)) { + return []; + } + + return array_values(array_filter( + array_map(fn ($host) => is_string($host) ? trim($host) : '', $hosts), + fn ($host) => $host !== '' + )); + } + /** * 레이아웃 확장 파일 경로 반환 * @@ -1309,6 +1334,75 @@ abstract class AbstractModule implements CacheableExtensionInterface, ModuleInte return 'modules'; } + /** + * 카테고리별 스토리지 인스턴스 캐시 (디스크명 키 memoize) + * + * @var array + */ + private array $storageByDisk = []; + + /** + * 카테고리별 스토리지 디스크 이름 반환 + * + * 기본값은 getStorageDisk() 와 동일 (현행 동작 100% 보존). + * 특정 카테고리(예: 'images')만 다른 디스크를 쓰려면 모듈이 오버라이드합니다. + * + * 주의: 오버라이드 구현은 'settings' 카테고리에서 모듈 설정을 조회하면 안 됩니다 — + * 모듈 설정 로드가 getStorage()->get('settings', ...) 를 경유하므로 재귀 고리가 생깁니다. + * 모듈 설정 조회는 'images' 등 설정 저장과 무관한 카테고리에서만 수행합니다. + * + * @param string $category 카테고리 (settings, attachments, images, cache, temp) + * @return string 디스크 이름 + */ + public function getStorageDiskFor(string $category): string + { + return $this->getStorageDisk(); + } + + /** + * 카테고리별 스토리지 드라이버 인스턴스 반환 + * + * getStorageDiskFor() 가 결정한 디스크의 드라이버를 디스크 단위로 memoize 하여 반환합니다. + * 기본 디스크와 동일하면 getStorage() 인스턴스를 그대로 재사용합니다. + * + * @param string $category 카테고리 + * @return StorageInterface 스토리지 드라이버 인스턴스 + */ + public function getStorageFor(string $category): StorageInterface + { + $disk = $this->getStorageDiskFor($category); + + if (! isset($this->storageByDisk[$disk])) { + $base = $this->getStorage(); + $this->storageByDisk[$disk] = ($disk === $base->getDisk()) ? $base : $base->withDisk($disk); + } + + return $this->storageByDisk[$disk]; + } + + /** + * 공개 자산 디스크 설정값을 해석합니다. + * + * 우선순위: 확장 개별 설정(override) > 코어 전역 설정(core.storage.public_asset_disk). + * 미설정('')/'none'/config 에 존재하지 않는 디스크(고아 플러그인 디스크)는 null 로 + * 해석되어 호출측이 기존 디스크(스트리밍)로 폴백합니다. + * + * @param string|null $override 확장 개별 설정값 (''/null 이면 코어 전역 설정 사용) + * @return string|null 사용할 디스크 이름 (스트리밍 유지면 null) + */ + protected function resolvePublicAssetDisk(?string $override = null): ?string + { + $disk = ($override !== null && $override !== '') + ? $override + : (string) config('core.storage.public_asset_disk', ''); + + if ($disk === '' || $disk === 'none' || config("filesystems.disks.{$disk}") === null) { + return null; + } + + return $disk; + } + /** * 모듈 캐시 드라이버 인스턴스 반환 * diff --git a/app/Extension/AbstractPlugin.php b/app/Extension/AbstractPlugin.php index 1d41aa54..380ef150 100644 --- a/app/Extension/AbstractPlugin.php +++ b/app/Extension/AbstractPlugin.php @@ -19,7 +19,8 @@ use ReflectionClass; * getIdentifier(), getVendor()는 디렉토리명에서 자동 추론됩니다. * getName(), getVersion(), getDescription()은 plugin.json에서 자동 파싱됩니다. * - * 참고: 플러그인은 모듈과 달리 관리자 메뉴(getAdminMenus)를 추가할 수 없습니다. + * 참고: 플러그인도 관리자 메뉴(getAdminMenus)를 선언할 수 있습니다. 설치·업데이트·활성화가 + * 공통으로 지나는 선언형 산출물 동기화에서 PluginManager 가 자동으로 반영합니다. */ abstract class AbstractPlugin implements CacheableExtensionInterface, PluginInterface { @@ -771,6 +772,31 @@ abstract class AbstractPlugin implements CacheableExtensionInterface, PluginInte return []; } + /** + * 신뢰하는 외부 스크립트 호스트 목록을 반환합니다. + * + * plugin.json 의 `trusted_script_hosts` 배열에서 읽습니다. 이 플러그인이 레이아웃 + * `scripts[].src` 로 로드하는 외부 CDN 호스트(예: `cdn.ckeditor.com`)를 선언합니다. + * 코어는 이 목록을 집계(AbstractPlugin/AbstractModule → TrustedScriptHosts)해 런타임 + * 스크립트 로더·저장측 검증·정적 검사가 same-origin 이 아닌 스크립트 중 **선언된 + * 호스트만** 허용하도록 합니다 (KVE-2026-1915 신뢰 출처 허용목록). + * + * @return array 신뢰 호스트명 목록 (예: ['cdn.ckeditor.com']) + */ + public function getTrustedScriptHosts(): array + { + $hosts = $this->loadManifest()['trusted_script_hosts'] ?? []; + + if (! is_array($hosts)) { + return []; + } + + return array_values(array_filter( + array_map(fn ($host) => is_string($host) ? trim($host) : '', $hosts), + fn ($host) => $host !== '' + )); + } + /** * 레이아웃 확장 파일 경로 반환 * @@ -1203,6 +1229,75 @@ abstract class AbstractPlugin implements CacheableExtensionInterface, PluginInte return 'plugins'; } + /** + * 카테고리별 스토리지 인스턴스 캐시 (디스크명 키 memoize) + * + * @var array + */ + private array $storageByDisk = []; + + /** + * 카테고리별 스토리지 디스크 이름 반환 + * + * 기본값은 getStorageDisk() 와 동일 (현행 동작 100% 보존). + * 특정 카테고리(예: 'images')만 다른 디스크를 쓰려면 플러그인이 오버라이드합니다. + * + * 주의: 오버라이드 구현은 'settings' 카테고리에서 플러그인 설정을 조회하면 안 됩니다 — + * 설정 로드가 getStorage()->get('settings', ...) 를 경유하므로 재귀 고리가 생깁니다. + * 설정 조회는 'images' 등 설정 저장과 무관한 카테고리에서만 수행합니다. + * + * @param string $category 카테고리 (settings, data, images, temp) + * @return string 디스크 이름 + */ + public function getStorageDiskFor(string $category): string + { + return $this->getStorageDisk(); + } + + /** + * 카테고리별 스토리지 드라이버 인스턴스 반환 + * + * getStorageDiskFor() 가 결정한 디스크의 드라이버를 디스크 단위로 memoize 하여 반환합니다. + * 기본 디스크와 동일하면 getStorage() 인스턴스를 그대로 재사용합니다. + * + * @param string $category 카테고리 + * @return StorageInterface 스토리지 드라이버 인스턴스 + */ + public function getStorageFor(string $category): StorageInterface + { + $disk = $this->getStorageDiskFor($category); + + if (! isset($this->storageByDisk[$disk])) { + $base = $this->getStorage(); + $this->storageByDisk[$disk] = ($disk === $base->getDisk()) ? $base : $base->withDisk($disk); + } + + return $this->storageByDisk[$disk]; + } + + /** + * 공개 자산 디스크 설정값을 해석합니다. + * + * 우선순위: 확장 개별 설정(override) > 코어 전역 설정(core.storage.public_asset_disk). + * 미설정('')/'none'/config 에 존재하지 않는 디스크(고아 플러그인 디스크)는 null 로 + * 해석되어 호출측이 기존 디스크(스트리밍)로 폴백합니다. + * + * @param string|null $override 확장 개별 설정값 (''/null 이면 코어 전역 설정 사용) + * @return string|null 사용할 디스크 이름 (스트리밍 유지면 null) + */ + protected function resolvePublicAssetDisk(?string $override = null): ?string + { + $disk = ($override !== null && $override !== '') + ? $override + : (string) config('core.storage.public_asset_disk', ''); + + if ($disk === '' || $disk === 'none' || config("filesystems.disks.{$disk}") === null) { + return null; + } + + return $disk; + } + /** * 플러그인 캐시 드라이버 인스턴스 반환 * diff --git a/app/Extension/BaseModuleServiceProvider.php b/app/Extension/BaseModuleServiceProvider.php index ef70ba57..ef7b9325 100644 --- a/app/Extension/BaseModuleServiceProvider.php +++ b/app/Extension/BaseModuleServiceProvider.php @@ -139,4 +139,17 @@ final class InlineModuleExtensionAdapter implements CacheableExtensionInterface { return $this->storage ??= new ModuleStorageDriver($this->identifier); } + + /** + * 카테고리별 스토리지 드라이버를 반환합니다. + * + * fallback 어댑터는 모듈 인스턴스(오버라이드 지점)가 없으므로 기본 스토리지를 그대로 반환합니다. + * + * @param string $category 카테고리 + * @return StorageInterface 기본 디스크의 스토리지 인스턴스 + */ + public function getStorageFor(string $category): StorageInterface + { + return $this->getStorage(); + } } diff --git a/app/Extension/BasePluginServiceProvider.php b/app/Extension/BasePluginServiceProvider.php index f9efaae4..4c7bca0f 100644 --- a/app/Extension/BasePluginServiceProvider.php +++ b/app/Extension/BasePluginServiceProvider.php @@ -130,4 +130,17 @@ final class InlinePluginExtensionAdapter implements CacheableExtensionInterface { return $this->storage ??= new PluginStorageDriver($this->identifier); } + + /** + * 카테고리별 스토리지 드라이버를 반환합니다. + * + * fallback 어댑터는 플러그인 인스턴스(오버라이드 지점)가 없으므로 기본 스토리지를 그대로 반환합니다. + * + * @param string $category 카테고리 + * @return StorageInterface 기본 디스크의 스토리지 인스턴스 + */ + public function getStorageFor(string $category): StorageInterface + { + return $this->getStorage(); + } } diff --git a/app/Extension/Helpers/ExtensionPendingHelper.php b/app/Extension/Helpers/ExtensionPendingHelper.php index fc02ec2f..a3e45d8b 100644 --- a/app/Extension/Helpers/ExtensionPendingHelper.php +++ b/app/Extension/Helpers/ExtensionPendingHelper.php @@ -141,11 +141,22 @@ class ExtensionPendingHelper // Windows에서 deleteDirectory 직후 같은 이름으로 rename이 실패하는 // 타이밍 이슈를 회피하기 위해 rename→rename→delete 패턴을 사용합니다. // 임시 디렉토리를 _pending/ 하위에 생성하여 오토로드 오염 방지 + // + // Windows 잠금 대응: 디렉토리 rename 은 하위 트리에 열린 핸들(파일 워처의 + // 디렉토리 핸들, IDE/Node 프로세스가 열어 둔 파일 등)이 하나라도 있으면 + // 실패한다. 잠금 프로세스의 식별·종료는 신뢰할 수 없으므로(디렉토리 핸들은 + // Restart Manager 로 감지 불가), rename 이 차단되면 파일 단위 연산으로 + // 폴백한다 — 파일 생성/덮어쓰기/읽기는 디렉토리 핸들 잠금의 영향을 받지 + // 않아 어떤 프로세스도 종료하지 않고 교체를 완료할 수 있다. $basePath = dirname($targetPath); $pendingPath = $basePath.DIRECTORY_SEPARATOR.'_pending'; File::ensureDirectoryExists($pendingPath, 0775); $identifier = basename($targetPath); + + // 이전 실패 실행이 남긴 교체용 임시 디렉토리 정리 (best-effort) + self::cleanupSwapLeftovers($pendingPath, $identifier); + $tempPath = $pendingPath.DIRECTORY_SEPARATOR.$identifier.'_updating_'.uniqid(); $oldPath = $pendingPath.DIRECTORY_SEPARATOR.$identifier.'_old_'.uniqid(); @@ -161,40 +172,77 @@ class ExtensionPendingHelper // 기존 → _old 이동 (rename, Windows NTFS 타이밍 이슈 대응 재시도) if (! self::retryMoveDirectory($targetPath, $oldPath)) { - // 파일 잠금 감지 및 해제 시도 - if (self::tryReleaseLocks($targetPath, $onProgress)) { - // 잠금 해제 후 재시도 - if (! self::retryMoveDirectory($targetPath, $oldPath)) { - File::deleteDirectory($tempPath); - throw new \RuntimeException( - "Failed to move existing directory: {$targetPath} → {$oldPath}" - ); - } - } else { - File::deleteDirectory($tempPath); - throw new \RuntimeException( - "Failed to move existing directory: {$targetPath} → {$oldPath}" - ); + // 활성 디렉토리 rename 차단 (하위 트리에 열린 핸들 존재) + // → 파일 단위 제자리 동기화로 폴백. 덮어쓰기는 잠금의 영향을 받지 않는다. + $onProgress?->__invoke(null, '디렉토리 이동이 차단되어 파일 단위 제자리 교체로 전환합니다...'); + Log::info('확장 교체: 활성 디렉토리 rename 차단 — 제자리 동기화 폴백', [ + 'target' => $targetPath, + ]); + + try { + self::syncDirectoryContents($tempPath, $targetPath, $onProgress); + } finally { + self::bestEffortDeleteDirectory($tempPath); } + + self::refreshRuntimeCaches(); + + return; } // 임시 → 활성 이동 (rename, Windows NTFS 타이밍 이슈 대응 재시도) if (! self::retryMoveDirectory($tempPath, $targetPath)) { - // 롤백: _old를 원래 위치로 복원 - File::moveDirectory($oldPath, $targetPath); - throw new \RuntimeException( - "Failed to move directory: {$tempPath} → {$targetPath}" - ); + // 방금 복사한 스테이징 트리를 파일 워처가 이미 열어 rename 이 차단된 경우 + // → 파일 단위 복사로 폴백. 소스에는 읽기 접근만 필요해 잠금과 무관하게 성공한다. + $onProgress?->__invoke(null, '디렉토리 이동이 차단되어 파일 단위 복사로 전환합니다...'); + Log::info('확장 교체: 스테이징 rename 차단 — 파일 단위 복사 폴백', [ + 'staging' => $tempPath, + 'target' => $targetPath, + ]); + + try { + self::copyDirectoryWithProgress($tempPath, $targetPath, $tempPath, $onProgress); + } catch (\Exception $e) { + // 복사 실패: 부분 복사본 제거 후 _old 를 원래 위치로 복원 + self::bestEffortDeleteDirectory($targetPath); + if (! self::retryMoveDirectory($oldPath, $targetPath)) { + try { + self::syncDirectoryContents($oldPath, $targetPath, $onProgress); + } catch (\Throwable $restoreError) { + Log::error('확장 교체 롤백 실패 — 백업 복원이 필요합니다', [ + 'target' => $targetPath, + 'old' => $oldPath, + 'error' => $restoreError->getMessage(), + ]); + } + } + throw new \RuntimeException( + "Failed to move directory: {$tempPath} → {$targetPath}", + 0, + $e + ); + } + + self::bestEffortDeleteDirectory($tempPath); } - // 교체 완료 후 _old 삭제 (실패해도 무해) - File::deleteDirectory($oldPath); + // 교체 완료 후 _old 삭제 (실패해도 무해 — 다음 교체 시작 시 잔존물 정리가 재시도) + self::bestEffortDeleteDirectory($oldPath); - // 원자적 rename 은 inode 단위로 교체되므로 PHP realpath/stat 캐시가 - // 이전 디렉토리의 파일 존재 여부를 기준으로 판단할 수 있다. 직후 Composer - // PSR-4 autoload 가 신규 파일(beta.1 에 없던 Seeder/Model)을 file_exists 로 - // 탐색할 때 false 반환 → "Class not found" fatal 로 업그레이드 스텝이 실패. - // clearstatcache(true) 로 전체 stat 캐시를 비워 신규 파일이 즉시 보이도록 한다. + self::refreshRuntimeCaches(); + } + + /** + * 교체 완료 후 PHP 런타임 캐시를 갱신합니다. + * + * 원자적 rename 은 inode 단위로 교체되므로 PHP realpath/stat 캐시가 + * 이전 디렉토리의 파일 존재 여부를 기준으로 판단할 수 있다. 직후 Composer + * PSR-4 autoload 가 신규 파일(beta.1 에 없던 Seeder/Model)을 file_exists 로 + * 탐색할 때 false 반환 → "Class not found" fatal 로 업그레이드 스텝이 실패. + * clearstatcache(true) 로 전체 stat 캐시를 비워 신규 파일이 즉시 보이도록 한다. + */ + private static function refreshRuntimeCaches(): void + { clearstatcache(true); // opcache 가 활성화된 프로덕션에서는 활성 디렉토리 하위의 이전 컴파일 바이트코드가 @@ -237,41 +285,6 @@ class ExtensionPendingHelper ]); } - /** - * 파일 잠금을 감지하고 해제를 시도합니다. - * - * Windows에서 다른 프로세스(IDE 등)가 파일 핸들을 보유하고 있을 때, - * 해당 프로세스를 감지하고 종료하여 디렉토리 이동이 가능하도록 합니다. - * 프로그레스바와 별개로 STDERR에 직접 출력하여 메시지가 덮어씌워지지 않습니다. - * - * @param string $directoryPath 잠금 해제할 디렉토리 경로 - * @param \Closure|null $onProgress 진행 콜백 - * @return bool 잠금 해제 성공 여부 - */ - private static function tryReleaseLocks(string $directoryPath, ?\Closure $onProgress = null): bool - { - if (! FileHandleHelper::isWindows()) { - return false; - } - - // 프로그레스바에 의해 메시지가 덮어씌워지지 않도록 STDERR에 직접 출력 - $stderr = fopen('php://stderr', 'w'); - $outputCallback = function (string $message) use ($stderr) { - if ($stderr) { - fwrite($stderr, $message.PHP_EOL); - } - Log::info($message, ['context' => 'file_lock_release']); - }; - - $result = FileHandleHelper::releaseLocks($directoryPath, $outputCallback); - - if ($stderr) { - fclose($stderr); - } - - return $result; - } - /** * 디렉토리 이동을 재시도합니다 (Windows NTFS 타이밍 이슈 대응). * @@ -333,6 +346,282 @@ class ExtensionPendingHelper } } + /** + * 새 버전 콘텐츠를 활성 디렉토리에 파일 단위로 제자리 동기화합니다. + * + * 디렉토리 rename 이 외부 프로세스의 핸들 잠금으로 차단될 때 사용하는 폴백입니다. + * 디렉토리 inode 를 건드리지 않고 파일 생성/덮어쓰기/삭제만 수행하므로 + * 디렉토리 핸들 잠금(파일 워처 등)의 영향을 받지 않습니다. + * + * (1) 새 버전 파일 전체를 덮어쓰기 → (2) 새 버전에 없는 잔존 파일 제거. + * (1) 실패는 예외로 전파해 호출자(매니저)가 백업 복원으로 수습하게 하고, + * (2) 실패는 로그만 남기고 진행합니다 (업데이트 전체 실패보다 잔존이 낫다). + * + * @param string $source 새 버전 콘텐츠 디렉토리 + * @param string $dest 활성 디렉토리 + * @param \Closure|null $onProgress 진행 콜백 + * + * @throws \RuntimeException 새 버전 파일을 심지 못했을 때 + */ + private static function syncDirectoryContents(string $source, string $dest, ?\Closure $onProgress = null): void + { + File::ensureDirectoryExists($dest, 0775); + + $failed = []; + self::overlayDirectory($source, $dest, $source, $onProgress, $failed); + + if (! empty($failed)) { + throw new \RuntimeException( + 'Failed to replace files locked by another process: ' + .implode(', ', array_slice($failed, 0, 5)) + .(count($failed) > 5 ? ' (+'.(count($failed) - 5).' more)' : '') + ); + } + + $staleFailures = []; + self::removeStaleEntries($source, $dest, $staleFailures); + + if (! empty($staleFailures)) { + Log::warning('확장 제자리 교체: 일부 잔존 파일을 삭제하지 못했습니다 (다음 교체 시 재시도)', [ + 'dest' => $dest, + 'failed' => $staleFailures, + ]); + } + } + + /** + * 소스 디렉토리의 파일을 대상 디렉토리에 재귀적으로 덮어씁니다. + * + * @param string $source 소스 디렉토리 + * @param string $dest 대상 디렉토리 + * @param string $basePath 상대 경로 계산 기준 + * @param \Closure|null $onProgress 진행 콜백 + * @param array $failed 교체 실패한 상대 경로 수집 (참조) + */ + private static function overlayDirectory( + string $source, + string $dest, + string $basePath, + ?\Closure $onProgress, + array &$failed + ): void { + File::ensureDirectoryExists($dest, 0775); + $items = new \FilesystemIterator($source, \FilesystemIterator::SKIP_DOTS); + + foreach ($items as $item) { + if ($item->isDir() && in_array($item->getBasename(), self::EXCLUDED_DIRECTORIES, true)) { + continue; + } + + $target = $dest.DIRECTORY_SEPARATOR.$item->getBasename(); + $relativePath = ltrim(str_replace($basePath, '', $item->getPathname()), '/\\'); + + if ($item->isDir()) { + if (is_file($target) && ! self::deleteFileWithFallback($target)) { + // 파일 → 디렉토리로 바뀐 경로인데 기존 파일을 치우지 못함 + $failed[] = $relativePath; + + continue; + } + self::overlayDirectory($item->getPathname(), $target, $basePath, $onProgress, $failed); + } else { + if (is_dir($target)) { + // 디렉토리 → 파일로 바뀐 경로 + File::deleteDirectory($target); + } + $onProgress?->__invoke(null, $relativePath); + if (! self::replaceFile($item->getPathname(), $target)) { + $failed[] = $relativePath; + } + } + } + } + + /** + * 파일 하나를 덮어씁니다 (잠금 대응 단계적 폴백). + * + * Windows 의 일반적인 파일 열기 모드는 읽기/쓰기 공유를 허용하므로, + * 다른 프로세스가 열어 둔 파일이라도 내용 덮어쓰기는 대부분 성공합니다. + * 덮어쓰기가 막힌 경우에만 삭제 후 재생성 → 옆으로 치우기(rename) 순으로 + * 시도합니다. FilePermissionHelper::copyFile() 은 복사 실패를 보고하지 않으므로 + * (File::copy 반환값 무시) 이 경로에서는 네이티브 copy 반환값으로 판정합니다. + * + * @param string $source 소스 파일 + * @param string $destination 대상 파일 + * @return bool 교체 성공 여부 + */ + private static function replaceFile(string $source, string $destination): bool + { + File::ensureDirectoryExists(dirname($destination)); + + $isExisting = is_file($destination); + $existingPerms = $isExisting ? @fileperms($destination) : null; + $existingOwner = $isExisting ? @fileowner($destination) : null; + $existingGroup = $isExisting ? @filegroup($destination) : null; + + $restoreMeta = function () use ($destination, $isExisting, $existingPerms, $existingOwner, $existingGroup) { + if (! $isExisting) { + // 신규 파일: 부모 디렉토리의 소유자/그룹 상속 (sudo 실행 시 root 소유 방지) + FilePermissionHelper::inheritOwnershipFromParent($destination); + + return; + } + if ($existingPerms !== false && $existingPerms !== null) { + @chmod($destination, $existingPerms); + } + if ($existingOwner !== false && $existingOwner !== null && function_exists('chown')) { + @chown($destination, $existingOwner); + } + if ($existingGroup !== false && $existingGroup !== null && function_exists('chgrp')) { + @chgrp($destination, $existingGroup); + } + }; + + // 1차: 그대로 덮어쓰기 + if (@copy($source, $destination)) { + $restoreMeta(); + + return true; + } + + // 2차: 읽기 전용 속성 해제 후 재시도 + @chmod($destination, 0666); + if (@copy($source, $destination)) { + $restoreMeta(); + + return true; + } + + // 3차: 삭제 후 재생성 + if (@unlink($destination) && @copy($source, $destination)) { + $restoreMeta(); + + return true; + } + + // 4차: 잠긴 파일을 옆으로 치우고(rename 은 덮어쓰기와 별개 권한) 새 파일 생성. + // 옆으로 치운 파일은 즉시 삭제를 시도하고, 실패해도 새 버전에 없는 파일이므로 + // 다음 제자리 동기화의 잔존 파일 제거가 다시 삭제를 시도한다. + $aside = $destination.'.g7stale_'.uniqid(); + if (@rename($destination, $aside)) { + @unlink($aside); + if (@copy($source, $destination)) { + $restoreMeta(); + + return true; + } + } + + return false; + } + + /** + * 파일 하나를 삭제합니다 (잠금 대응 단계적 폴백). + * + * @param string $path 삭제할 파일 경로 + * @return bool 삭제(또는 옆으로 치우기) 성공 여부 + */ + private static function deleteFileWithFallback(string $path): bool + { + if (@unlink($path)) { + return true; + } + + @chmod($path, 0666); + if (@unlink($path)) { + return true; + } + + // 삭제가 막힌 경우 옆으로 치워 원래 이름을 비운다 + $aside = $path.'.g7stale_'.uniqid(); + if (@rename($path, $aside)) { + @unlink($aside); + + return true; + } + + return false; + } + + /** + * 대상 디렉토리에서 소스에 존재하지 않는 파일/디렉토리를 제거합니다. + * + * @param string $source 새 버전 콘텐츠 디렉토리 + * @param string $dest 활성 디렉토리 + * @param array $failures 삭제 실패 경로 수집 (참조) + */ + private static function removeStaleEntries(string $source, string $dest, array &$failures): void + { + if (! is_dir($dest)) { + return; + } + + $items = new \FilesystemIterator($dest, \FilesystemIterator::SKIP_DOTS); + + foreach ($items as $item) { + $counterpart = $source.DIRECTORY_SEPARATOR.$item->getBasename(); + + if ($item->isDir()) { + if (is_dir($counterpart)) { + self::removeStaleEntries($counterpart, $item->getPathname(), $failures); + } else { + File::deleteDirectory($item->getPathname()); + if (is_dir($item->getPathname())) { + $failures[] = $item->getPathname(); + } + } + } elseif (! is_file($counterpart)) { + if (! self::deleteFileWithFallback($item->getPathname())) { + $failures[] = $item->getPathname(); + } + } + } + } + + /** + * 이전 실패 실행이 남긴 교체용 임시 디렉토리를 정리합니다 (best-effort). + * + * 잠금으로 인해 삭제하지 못한 `_updating_`/`_old_` 디렉토리는 다음 교체 시작 + * 시점(잠금이 풀린 뒤일 가능성이 높음)에 다시 삭제를 시도합니다. + * 호출자가 소유한 스테이징 디렉토리(`{identifier}_{Ymd_His}`)는 건드리지 않습니다. + * + * @param string $pendingPath _pending 디렉토리 경로 + * @param string $identifier 확장 식별자 + */ + private static function cleanupSwapLeftovers(string $pendingPath, string $identifier): void + { + foreach (['_updating_', '_old_'] as $marker) { + $pattern = $pendingPath.DIRECTORY_SEPARATOR.$identifier.$marker.'*'; + foreach (glob($pattern) ?: [] as $leftover) { + if (is_dir($leftover)) { + self::bestEffortDeleteDirectory($leftover); + } + } + } + } + + /** + * 디렉토리를 삭제하되, 실패해도 예외를 던지지 않습니다. + * + * 잠금으로 삭제하지 못한 디렉토리는 다음 교체 시작 시 잔존물 정리가 재시도합니다. + * + * @param string $path 삭제할 디렉토리 경로 + */ + private static function bestEffortDeleteDirectory(string $path): void + { + if (! File::isDirectory($path)) { + return; + } + + File::deleteDirectory($path); + + if (File::isDirectory($path)) { + Log::info('디렉토리를 완전히 삭제하지 못했습니다 (다음 교체 시 잔존물 정리에서 재시도)', [ + 'path' => $path, + ]); + } + } + /** * 확장 디렉토리를 삭제합니다. * @@ -353,6 +642,7 @@ class ExtensionPendingHelper * * @param string $basePath 확장 타입의 기본 경로 * @param string $identifier 확장 식별자 + * @return bool _pending 존재 여부 */ public static function isPending(string $basePath, string $identifier): bool { @@ -364,6 +654,7 @@ class ExtensionPendingHelper * * @param string $basePath 확장 타입의 기본 경로 * @param string $identifier 확장 식별자 + * @return bool _bundled 존재 여부 */ public static function isBundled(string $basePath, string $identifier): bool { @@ -375,6 +666,7 @@ class ExtensionPendingHelper * * @param string $basePath 확장 타입의 기본 경로 * @param string $identifier 확장 식별자 + * @return string _pending 절대 경로 */ public static function getPendingPath(string $basePath, string $identifier): string { @@ -386,6 +678,7 @@ class ExtensionPendingHelper * * @param string $basePath 확장 타입의 기본 경로 * @param string $identifier 확장 식별자 + * @return string _bundled 절대 경로 */ public static function getBundledPath(string $basePath, string $identifier): string { diff --git a/app/Extension/PluginManager.php b/app/Extension/PluginManager.php index 192185ff..3c53cf25 100644 --- a/app/Extension/PluginManager.php +++ b/app/Extension/PluginManager.php @@ -21,6 +21,7 @@ use App\Exceptions\LayoutIncludeException; use App\Extension\Concerns\ResolvesExtensionSharedRecords; use App\Extension\Helpers\DependencyEnricher; use App\Extension\Helpers\ExtensionBackupHelper; +use App\Extension\Helpers\ExtensionMenuSyncHelper; use App\Extension\Helpers\ExtensionPendingHelper; use App\Extension\Helpers\ExtensionRoleSyncHelper; use App\Extension\Helpers\ExtensionStatusGuard; @@ -449,6 +450,9 @@ class PluginManager implements PluginManagerInterface // 권한-Role 연결 $this->assignPermissionsToRoles($plugin); + // 관리자 메뉴 자동 생성 (모듈 installModule 과 동일 순서) + $this->createPluginMenus($plugin); + // IDV 정책 자동 동기화 (identity_policies 테이블) $this->syncPluginIdentityPolicies($plugin); @@ -2003,10 +2007,54 @@ class PluginManager implements PluginManagerInterface ); } + /** + * 플러그인이 선언한 관리자 메뉴를 동기화합니다. + * + * 플러그인도 자기 화면을 가질 수 있으므로 `getAdminMenus()` 를 선언할 수 있습니다. + * 이 동기화가 설치·활성화 시점에만 일어나면, 이미 활성 상태인 사이트가 플러그인을 + * 업데이트해 새 화면을 받아도 메뉴가 만들어지지 않아 그 화면은 주소를 직접 입력해야만 + * 닿을 수 있습니다. 그래서 선언형 산출물 동기화(설치·업데이트 공통 경로)에 포함합니다. + * + * helper 의 upsert 패턴을 그대로 쓰므로 재호출은 무해하며(멱등) 운영자 커스터마이징 + * (user_overrides) 도 보존됩니다. + * + * @param PluginInterface $plugin 메뉴를 동기화할 플러그인 인스턴스 + */ + protected function createPluginMenus(PluginInterface $plugin): void + { + if (! method_exists($plugin, 'getAdminMenus')) { + return; + } + + $menus = $plugin->getAdminMenus(); + + if (empty($menus)) { + return; + } + + // 활성 언어팩이 admin_menus 다국어 필드(name 등)에 추가 locale 을 주입할 수 있도록 + // 모듈과 동일한 필터 훅 규약을 적용한다 (LanguagePackSeedInjector 가 결선). + $menus = HookManager::applyFilters( + "plugin.{$plugin->getIdentifier()}.admin_menus.translations", + $menus, + ); + + $helper = app(ExtensionMenuSyncHelper::class); + + foreach ($menus as $menuData) { + $helper->syncMenuRecursive( + $menuData, + ExtensionOwnerType::Plugin, + $plugin->getIdentifier(), + ); + } + } + /** * 플러그인 정의 기준으로 stale 권한·역할을 정리합니다 (완전 동기화 원칙). * - * 플러그인은 메뉴(getAdminMenus) 를 지원하지 않으므로 권한·역할만 대상. + * 메뉴 stale 정리는 비활성화·삭제 시 플러그인 자신이 `cleanupStaleMenus` 로 수행하므로 + * 여기서는 권한·역할만 대상으로 한다. * user_overrides 보존 및 `users.role_id` 참조 역할 삭제 차단은 helper 가 담당. */ protected function cleanupStalePluginEntries(PluginInterface $plugin): void @@ -2065,14 +2113,15 @@ class PluginManager implements PluginManagerInterface * 를 정합 상태로 유지한다. 외부 진입점으로도 노출되어 코어 업그레이드 사후 보정 * (`Upgrade_7_0_0_beta_4` 등) 이나 운영자 수동 재시드 도구가 사용 가능. * - * 동기화 대상 (플러그인은 메뉴 미지원 — getAdminMenus 부재): + * 동기화 대상: * 1. 역할 (`getRoles`) * 2. 권한 (`getPermissions`) * 3. 역할-권한 매핑 (`getRolePermissions`) - * 4. stale cleanup (현재 선언에 없는 기존 레코드 제거) - * 5. IDV 정책 (`getIdentityPolicies`) - * 6. IDV 메시지 정의/템플릿 (`getIdentityMessages`) - * 7. 알림 정의/템플릿 (`getNotificationDefinitions`) + * 4. 관리자 메뉴 (`getAdminMenus`) + * 5. stale cleanup (현재 선언에 없는 기존 레코드 제거) + * 6. IDV 정책 (`getIdentityPolicies`) + * 7. IDV 메시지 정의/템플릿 (`getIdentityMessages`) + * 8. 알림 정의/템플릿 (`getNotificationDefinitions`) * * 각 sync 메서드는 helper 내부의 user_overrides 보존 패턴을 따르므로 정상 환경 재호출 * 무해 (멱등). @@ -2084,6 +2133,7 @@ class PluginManager implements PluginManagerInterface $this->createPluginRoles($plugin); $this->createPluginPermissions($plugin); $this->assignPermissionsToRoles($plugin); + $this->createPluginMenus($plugin); $this->cleanupStalePluginEntries($plugin); $this->syncPluginIdentityPolicies($plugin); $this->syncPluginIdentityMessages($plugin); diff --git a/app/Extension/Storage/Concerns/ResolvesPublicUrl.php b/app/Extension/Storage/Concerns/ResolvesPublicUrl.php new file mode 100644 index 00000000..1b125e2d --- /dev/null +++ b/app/Extension/Storage/Concerns/ResolvesPublicUrl.php @@ -0,0 +1,70 @@ +resolvePath($category, $path); + + $url = $this->diskSupportsDirectUrl() + ? Storage::disk($this->disk)->url($fullPath) + : null; + + $filtered = HookManager::applyFilters('core.storage.filter_url', $url, array_merge( + $this->urlHookContext(), + [ + 'disk' => $this->disk, + 'category' => $category, + 'path' => $path, + 'full_path' => $fullPath, + ] + )); + + return (is_string($filtered) && trim($filtered) !== '') ? $filtered : null; + } + + /** + * 현재 디스크가 직접 URL 생성을 지원하는지 판정합니다. + * + * public 디스크는 항상 지원, 그 외 디스크는 `filesystems.disks.{disk}.url` 이 + * 비어 있지 않은 문자열로 설정된 경우에만 지원합니다 (AWS_URL 미설정/빈값 방어). + * + * @return bool 직접 URL 지원 여부 + */ + private function diskSupportsDirectUrl(): bool + { + if ($this->disk === 'public') { + return true; + } + + $base = config("filesystems.disks.{$this->disk}.url"); + + return is_string($base) && trim($base) !== ''; + } + + /** + * `core.storage.filter_url` 훅 컨텍스트의 드라이버별 식별 정보를 반환합니다. + * + * @return array{scope: string, identifier: ?string} scope(core|module|plugin) + 확장 식별자(코어는 null) + */ + abstract protected function urlHookContext(): array; +} diff --git a/app/Extension/Storage/CoreStorageDriver.php b/app/Extension/Storage/CoreStorageDriver.php index 9e8a7fbd..402de7a7 100644 --- a/app/Extension/Storage/CoreStorageDriver.php +++ b/app/Extension/Storage/CoreStorageDriver.php @@ -3,7 +3,9 @@ namespace App\Extension\Storage; use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\Concerns\ResolvesPublicUrl; use Illuminate\Support\Facades\Storage; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 코어 스토리지 드라이버 @@ -13,6 +15,8 @@ use Illuminate\Support\Facades\Storage; */ class CoreStorageDriver implements StorageInterface { + use ResolvesPublicUrl; + /** * 사용할 디스크 이름 */ @@ -93,17 +97,16 @@ class CoreStorageDriver implements StorageInterface } /** - * {@inheritDoc} + * `core.storage.filter_url` 훅 컨텍스트의 드라이버별 식별 정보를 반환합니다. + * + * @return array{scope: string, identifier: ?string} scope + 식별자 (코어는 null) */ - public function url(string $category, string $path): ?string + protected function urlHookContext(): array { - if ($this->disk !== 'public') { - return null; - } - - $fullPath = $this->resolvePath($category, $path); - - return Storage::disk($this->disk)->url($fullPath); + return [ + 'scope' => 'core', + 'identifier' => null, + ]; } /** @@ -155,7 +158,7 @@ class CoreStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function response(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function response(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); @@ -180,7 +183,7 @@ class CoreStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function download(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function download(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); diff --git a/app/Extension/Storage/ModuleStorageDriver.php b/app/Extension/Storage/ModuleStorageDriver.php index 17c80b82..e48f7805 100644 --- a/app/Extension/Storage/ModuleStorageDriver.php +++ b/app/Extension/Storage/ModuleStorageDriver.php @@ -3,7 +3,9 @@ namespace App\Extension\Storage; use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\Concerns\ResolvesPublicUrl; use Illuminate\Support\Facades\Storage; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 모듈 스토리지 드라이버 @@ -13,6 +15,8 @@ use Illuminate\Support\Facades\Storage; */ class ModuleStorageDriver implements StorageInterface { + use ResolvesPublicUrl; + /** * 모듈 식별자 */ @@ -119,18 +123,16 @@ class ModuleStorageDriver implements StorageInterface } /** - * {@inheritDoc} + * `core.storage.filter_url` 훅 컨텍스트의 드라이버별 식별 정보를 반환합니다. + * + * @return array{scope: string, identifier: ?string} scope + 모듈 식별자 */ - public function url(string $category, string $path): ?string + protected function urlHookContext(): array { - // public disk인 경우에만 직접 URL 반환 - if ($this->disk !== 'public') { - return null; - } - - $fullPath = $this->resolvePath($category, $path); - - return Storage::disk($this->disk)->url($fullPath); + return [ + 'scope' => 'module', + 'identifier' => $this->identifier, + ]; } /** @@ -182,7 +184,7 @@ class ModuleStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function response(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function response(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); @@ -207,7 +209,7 @@ class ModuleStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function download(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function download(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); diff --git a/app/Extension/Storage/PluginStorageDriver.php b/app/Extension/Storage/PluginStorageDriver.php index 4a892b48..7d836a91 100644 --- a/app/Extension/Storage/PluginStorageDriver.php +++ b/app/Extension/Storage/PluginStorageDriver.php @@ -3,7 +3,9 @@ namespace App\Extension\Storage; use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\Concerns\ResolvesPublicUrl; use Illuminate\Support\Facades\Storage; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 플러그인 스토리지 드라이버 @@ -13,6 +15,8 @@ use Illuminate\Support\Facades\Storage; */ class PluginStorageDriver implements StorageInterface { + use ResolvesPublicUrl; + /** * 플러그인 식별자 */ @@ -119,18 +123,16 @@ class PluginStorageDriver implements StorageInterface } /** - * {@inheritDoc} + * `core.storage.filter_url` 훅 컨텍스트의 드라이버별 식별 정보를 반환합니다. + * + * @return array{scope: string, identifier: ?string} scope + 플러그인 식별자 */ - public function url(string $category, string $path): ?string + protected function urlHookContext(): array { - // public disk인 경우에만 직접 URL 반환 - if ($this->disk !== 'public') { - return null; - } - - $fullPath = $this->resolvePath($category, $path); - - return Storage::disk($this->disk)->url($fullPath); + return [ + 'scope' => 'plugin', + 'identifier' => $this->identifier, + ]; } /** @@ -182,7 +184,7 @@ class PluginStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function response(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function response(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); @@ -207,7 +209,7 @@ class PluginStorageDriver implements StorageInterface /** * {@inheritDoc} */ - public function download(string $category, string $path, string $filename, array $headers = []): ?\Symfony\Component\HttpFoundation\StreamedResponse + public function download(string $category, string $path, string $filename, array $headers = []): ?StreamedResponse { $fullPath = $this->resolvePath($category, $path); diff --git a/app/Extension/TemplateManager.php b/app/Extension/TemplateManager.php index 15b2a6b2..6f13c8c1 100644 --- a/app/Extension/TemplateManager.php +++ b/app/Extension/TemplateManager.php @@ -51,6 +51,14 @@ class TemplateManager implements TemplateManagerInterface protected array $templates = []; + /** + * 템플릿 디렉토리 스캔이 1회 이상 수행되었는지 여부 + * + * `ensureLoaded()` 의 멱등 판정에만 쓴다 — 명시적 `loadTemplates()` 는 이 값과 무관하게 + * 항상 재스캔한다(설치/삭제 직후 갱신 계약). + */ + protected bool $templatesLoaded = false; + /** * _pending 디렉토리의 템플릿 메타데이터 배열 * @@ -98,13 +106,33 @@ class TemplateManager implements TemplateManagerInterface return new CoreCacheDriver(config('cache.default', 'array')); } + /** + * 템플릿이 아직 로드되지 않았을 때만 로드합니다. (멱등) + * + * 소비자가 "템플릿 맵이 채워져 있음" 만 필요로 할 때 쓴다. `loadTemplates()` 는 맵을 + * 리셋하고 디렉토리를 통째로 재스캔하므로, 그것을 무조건 호출하면 공유 인스턴스의 + * 상태를 매번 갈아엎으면서 풀스캔 비용까지 반복된다. + */ + public function ensureLoaded(): void + { + if ($this->templatesLoaded) { + return; + } + + $this->loadTemplates(); + } + /** * 모든 템플릿을 로드하고 초기화합니다. + * + * 항상 재스캔한다 — 설치/삭제/업데이트 직후 갱신을 보장하는 계약이다. + * 단순히 "채워져 있으면 됨" 인 호출자는 `ensureLoaded()` 를 쓴다. */ public function loadTemplates(): void { // 기존 템플릿 캐시 초기화 (테스트 환경에서 재로드 지원) $this->templates = []; + $this->templatesLoaded = true; if (! File::exists($this->templatesPath)) { return; @@ -1659,13 +1687,9 @@ class TemplateManager implements TemplateManagerInterface } if (! in_array($override->name, $registeredLayoutNames)) { - // 캐시 삭제 (레코드 삭제 전에 수행) - $cacheKey = "template.{$templateId}.layout.{$override->name}"; - $this->cache()->forget($cacheKey); - - $sourceHash = md5($override->source_type?->value.$override->source_identifier); - $cacheKeyWithHash = "template.{$templateId}.layout.{$override->name}.{$sourceHash}"; - $this->cache()->forget($cacheKeyWithHash); + // 캐시 삭제 (레코드 삭제 전에 수행) — 키 조립은 트레이트 단일 지점에 위임 + // (.with_source_meta 변종 포함, 수작업 키 목록의 규약 드리프트 방지) + $this->forgetLayoutCacheKeys($override, $templateName); $override->forceDelete(); $deletedCount++; @@ -1712,14 +1736,10 @@ class TemplateManager implements TemplateManagerInterface } foreach ($overrideLayouts as $layout) { - // 기본 캐시 키 패턴으로 삭제 - $cacheKey = "template.{$templateId}.layout.{$layout->name}"; - $this->cache()->forget($cacheKey); - - // sourceHash를 포함한 캐시 키도 삭제 (LayoutService의 캐시 키 패턴) - $sourceHash = md5($layout->source_type?->value.$layout->source_identifier); - $cacheKeyWithHash = "template.{$templateId}.layout.{$layout->name}.{$sourceHash}"; - $this->cache()->forget($cacheKeyWithHash); + // 키 조립은 트레이트 단일 지점에 위임 (.with_source_meta 변종 포함). + // 이 경로는 identifier 를 보유하지 않으므로 '' 전달 — 버전 포함 공개 + // 서빙 키는 종전과 동일하게 버전 bump 로 무효화된다. + $this->forgetLayoutCacheKeys($layout, ''); } Log::info(__('templates.info.override_layouts_cache_invalidated'), [ @@ -1766,41 +1786,79 @@ class TemplateManager implements TemplateManagerInterface /** * 템플릿 관련 모든 캐시를 삭제합니다. * + * 템플릿 라이프사이클(update/deactivate/uninstall/cache-clear)이 공유하는 + * 무효화 단일 지점이다. 버전 접미사 없는 고정 키(`template.config.{identifier}`, + * `template.{id|identifier}.components_manifest`)는 캐시 버전 bump 로 무효화되지 + * 않으므로 여기서 능동 forget 한다 — 누락 시 공개 config.json 이 TTL(1시간) 동안 + * 이전 manifest 로 응답된다 (#588, 공개 #119). + * + * template:cache-clear 커맨드가 호출할 수 있도록 public 이다. + * * @param string $templateIdentifier 템플릿 식별자 + * @return int 능동 삭제를 시도한 캐시 키 수 */ - protected function clearTemplateCache(string $templateIdentifier): void + public function clearTemplateCache(string $templateIdentifier): int { + $clearedCount = 0; + + // identifier 만으로 가능한 고정 키 forget — uninstall 등 DB 레코드가 이미 + // 없는 상황에서도 반드시 수행되어야 하므로 레코드 조회보다 먼저 둔다. + $this->cache()->forget("template.config.{$templateIdentifier}"); + $clearedCount++; + + // components_manifest identifier 변종 (ComponentExists 는 int|string 수용) + $this->cache()->forget("template.{$templateIdentifier}.components_manifest"); + $clearedCount++; + + // 활성 템플릿 + 레이아웃 무변경 업데이트 경로는 캐시 버전 bump 가 없어 + // 현재 버전 routes/language 키가 stale 로 남는다 — warmTemplateCache() 와 + // 대칭으로 현재 버전 키를 능동 삭제한다 (이전 버전 키는 TTL 자연 만료). + $cacheVersion = self::getExtensionCacheVersion(); + $this->cache()->forget("template.routes.{$templateIdentifier}.v{$cacheVersion}"); + $clearedCount++; + foreach (config('app.supported_locales', ['ko', 'en']) as $locale) { + $this->cache()->forget("template.language.{$templateIdentifier}.{$locale}.v{$cacheVersion}"); + $clearedCount++; + } + // DB 기반 조회 — 파일 시스템 상태와 무관하게 캐시 삭제 보장 // (파일 교체 중이거나 reloadTemplate() 전에도 캐시를 확실히 삭제) $templateRecord = $this->templateRepository->findByIdentifier($templateIdentifier); if (! $templateRecord) { - return; + return $clearedCount; } - // 레이아웃 캐시 삭제 (버전 없는 내부 캐시) - $this->clearLayoutCaches($templateIdentifier); + // components_manifest 숫자 id 변종 (StoreLayoutRequest 가 integer 검증 — 실운영 키) + $this->cache()->forget("template.{$templateRecord->id}.components_manifest"); + $clearedCount++; - // Routes/다국어 캐시는 버전 포함 키이므로 incrementExtensionCacheVersion() + TTL로 무효화됨 + // 레이아웃 캐시 삭제 (버전 없는 내부 캐시) + $clearedCount += $this->clearLayoutCaches($templateIdentifier); Log::info(__('templates.info.cache_cleared'), [ 'template' => $templateIdentifier, ]); + + return $clearedCount; } /** * 템플릿의 모든 레이아웃 캐시를 삭제합니다. * * @param string $templateIdentifier 템플릿 식별자 + * @return int 캐시를 무효화한 레이아웃 수 */ - protected function clearLayoutCaches(string $templateIdentifier): void + protected function clearLayoutCaches(string $templateIdentifier): int { // 템플릿의 모든 레이아웃 조회 $template = $this->templateRepository->findByIdentifier($templateIdentifier); if (! $template) { - return; + return 0; } $this->invalidateTemplateLayoutCache($template->id, $templateIdentifier); + + return $this->layoutRepository->getByTemplateId($template->id)->count(); } /** @@ -3082,6 +3140,12 @@ class TemplateManager implements TemplateManagerInterface $onProgress?->__invoke('cleanup', '정리 중...'); ExtensionBackupHelper::deleteBackup($backupPath); + // 고정 키(config/components_manifest) + 현재 버전 routes/language 키 능동 삭제. + // 비활성 업데이트·레이아웃 무변경 업데이트 경로는 버전 bump 가 없어 이 호출이 + // 없으면 공개 config.json 이 TTL 동안 이전 manifest 로 응답된다 (#588, 공개 #119). + // 활성 경로의 refreshTemplateLayouts() 경유 중복 삭제는 forget 멱등이라 무해. + $this->clearTemplateCache($identifier); + $this->clearAllTemplateLanguageCaches(); $this->clearAllTemplateRoutesCaches(); // refreshTemplateLayouts() 내부에서 변경 시 incrementExtensionCacheVersion() 호출됨 diff --git a/app/Extension/Traits/InvalidatesLayoutCache.php b/app/Extension/Traits/InvalidatesLayoutCache.php index 57e5e704..ba4ba526 100644 --- a/app/Extension/Traits/InvalidatesLayoutCache.php +++ b/app/Extension/Traits/InvalidatesLayoutCache.php @@ -14,6 +14,7 @@ use Illuminate\Support\Facades\Log; * 버전 없는 내부 캐시 키를 능동 삭제합니다: * 1. template.{templateId}.layout.{layoutName} - LayoutService에서 사용 * 2. template.{templateId}.layout.{layoutName}.{sourceHash} - 모듈/플러그인 레이아웃 + * 3. 위 두 형태의 `.with_source_meta` 접미사 변종 - 편집기용 병합 응답 캐시 * * 버전 포함 캐시 (layout.{identifier}.{name}.v{version})는 * incrementExtensionCacheVersion() + TTL로 무효화됩니다. @@ -98,20 +99,26 @@ trait InvalidatesLayoutCache $cache = $this->resolveLayoutCache(); // 1. LayoutService 내부 캐시 (버전 없음 → 능동 삭제) - $cache->forget("template.{$layout->template_id}.layout.{$layout->name}"); + // 편집기용 병합 응답은 `.with_source_meta` 접미사 키로 별도 캐싱되므로 + // (LayoutService::getMergedLayoutCacheKey) 두 변종을 함께 지운다 — + // 접미사 키를 남기면 activate/deactivate/uninstall/refresh 후에도 + // 편집기가 stale 병합 캐시를 받는다 (#588). + $cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name)); + $cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, withSourceMeta: true)); - // 2. 소스 해시 포함 키 (버전 없음 → 능동 삭제) + // 2. 소스 해시 포함 키 (버전 없음 → 능동 삭제) — 접미사는 소스 해시 뒤 if ($layout->source_type && $layout->source_identifier) { - $sourceHash = md5($layout->source_type->value.$layout->source_identifier); - $cache->forget("template.{$layout->template_id}.layout.{$layout->name}.{$sourceHash}"); + $sourceType = $layout->source_type->value; + $cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, $sourceType, $layout->source_identifier)); + $cache->forget($this->buildLayoutCacheKey($layout->template_id, $layout->name, $sourceType, $layout->source_identifier, true)); } // 3. PublicLayoutController 캐시 (버전 포함) — 일반 응답 + 편집기(`.meta`) 응답 두 키 모두. // PublicLayoutController::serve() 가 `with_source_meta=1`(레이아웃 편집기) 응답을 `.meta` // 접미사 별도 키로 캐싱하므로, 그 키를 함께 삭제하지 않으면 템플릿/레이아웃 상태 변화 // (refresh-layout / activate / deactivate / uninstall 등) 후에도 편집기가 stale 캐시를 - // 받는다. 레이아웃 - // 저장 경로(LayoutService::clearPublicServingCache)는 이미 두 키를 지우므로 정합을 맞춘다. + // 받는다. 레이아웃 저장 경로(LayoutService::clearPublicServingCache)는 이미 두 키를 + // 지우므로 정합을 맞춘다. if ($templateIdentifier) { $cacheVersion = (int) $cache->get('ext.cache_version', 0); $cache->forget("layout.{$templateIdentifier}.{$layout->name}.v{$cacheVersion}"); @@ -137,26 +144,32 @@ trait InvalidatesLayoutCache /** * 레이아웃 캐시 키를 생성합니다. * + * `$withSourceMeta` 가 true 면 `.with_source_meta` 접미사를 붙인다 — 편집기용 + * 병합 응답 키(LayoutService::getMergedLayoutCacheKey 와 동형, 접미사는 소스 해시 뒤). + * * @param int $templateId 템플릿 ID * @param string $layoutName 레이아웃 이름 * @param string|null $sourceType 소스 타입 (선택) * @param string|null $sourceIdentifier 소스 식별자 (선택) + * @param bool $withSourceMeta 편집기용 병합 응답 키 여부 (선택) * @return string 캐시 키 */ protected function buildLayoutCacheKey( int $templateId, string $layoutName, ?string $sourceType = null, - ?string $sourceIdentifier = null + ?string $sourceIdentifier = null, + bool $withSourceMeta = false ): string { $baseKey = "template.{$templateId}.layout.{$layoutName}"; + $metaSuffix = $withSourceMeta ? '.with_source_meta' : ''; if ($sourceType && $sourceIdentifier) { $sourceHash = md5($sourceType.$sourceIdentifier); - return "{$baseKey}.{$sourceHash}"; + return "{$baseKey}.{$sourceHash}{$metaSuffix}"; } - return $baseKey; + return "{$baseKey}{$metaSuffix}"; } } diff --git a/app/Helpers/PermissionHelper.php b/app/Helpers/PermissionHelper.php index de3656c4..642e0ccc 100644 --- a/app/Helpers/PermissionHelper.php +++ b/app/Helpers/PermissionHelper.php @@ -204,6 +204,38 @@ class PermissionHelper return false; } + /** + * 스코프 접근이 허용되는 모델만 남긴 배열을 반환합니다 (정적 일괄 라우트용). + * + * `PermissionMiddleware` 의 스코프 검사는 라우트에서 모델이 resolve 될 때만 + * 동작합니다 — 모델이 없으면 목록 엔드포인트로 보아 건너뜁니다. 따라서 + * `{user}` 같은 파라미터가 없는 **정적 일괄 라우트**(예: `PATCH users/bulk-status`) + * 에서는 스코프 검사가 통째로 우회됩니다. 상세 경로가 403 으로 막는 대상을 + * 일괄 경로로는 바꿀 수 있으면 그 경로가 우회로이므로, 서비스 계층에서 + * 같은 판정(`checkScopeAccess`)을 재적용해야 합니다. + * + * 판정은 대상별로 이뤄집니다 — 액터의 유효 스코프가 self 면 자기 소유만, + * role 이면 같은 역할 범위까지, 미지정(글로벌)이면 전체가 통과합니다. + * + * @param iterable $models 검사 대상 모델 목록 + * @param string $permission 권한 식별자 + * @param User|null $user 사용자 (null이면 현재 인증 사용자) + * @return array 스코프 접근이 허용된 모델 목록 + */ + public static function filterByScope(iterable $models, string $permission, ?User $user = null): array + { + $user = $user ?? Auth::user(); + + $allowed = []; + foreach ($models as $model) { + if (self::checkScopeAccess($model, $permission, $user)) { + $allowed[] = $model; + } + } + + return $allowed; + } + /** * Permission 스코프 데이터를 static 캐시와 함께 조회합니다. * diff --git a/app/Helpers/settings_helpers.php b/app/Helpers/settings_helpers.php index b6b00000..767c1160 100644 --- a/app/Helpers/settings_helpers.php +++ b/app/Helpers/settings_helpers.php @@ -1,5 +1,6 @@ refreshModule($identifier); + } +} + if (! function_exists('g7_plugin_settings')) { /** * 그누보드7 플러그인 환경설정 조회 헬퍼 함수 @@ -104,7 +117,6 @@ if (! function_exists('g7_plugin_settings')) { * @example * // 다음 우편번호 플러그인의 표시 모드 조회 * $displayMode = g7_plugin_settings('sirsoft-daum_postcode', 'display_mode'); - * * @example * // 플러그인 전체 설정 조회 * $allSettings = g7_plugin_settings('sirsoft-daum_postcode'); diff --git a/app/Http/Controllers/Api/Admin/AttachmentController.php b/app/Http/Controllers/Api/Admin/AttachmentController.php index 76a2c737..4f78c239 100644 --- a/app/Http/Controllers/Api/Admin/AttachmentController.php +++ b/app/Http/Controllers/Api/Admin/AttachmentController.php @@ -11,6 +11,7 @@ use App\Http\Resources\AttachmentResource; use App\Models\Attachment; use App\Services\AttachmentService; use Exception; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; /** @@ -21,7 +22,7 @@ class AttachmentController extends AdminBaseController /** * AttachmentController 생성자 * - * @param AttachmentService $attachmentService 첨부파일 서비스 + * @param AttachmentService $attachmentService 첨부파일 서비스 */ public function __construct( private AttachmentService $attachmentService @@ -32,7 +33,7 @@ class AttachmentController extends AdminBaseController /** * 단일 파일 업로드 * - * @param UploadAttachmentRequest $request 업로드 요청 + * @param UploadAttachmentRequest $request 업로드 요청 * @return JsonResponse */ public function upload(UploadAttachmentRequest $request): JsonResponse @@ -64,7 +65,7 @@ class AttachmentController extends AdminBaseController /** * 여러 파일 일괄 업로드 * - * @param UploadBatchAttachmentRequest $request 일괄 업로드 요청 + * @param UploadBatchAttachmentRequest $request 일괄 업로드 요청 * @return JsonResponse */ public function uploadBatch(UploadBatchAttachmentRequest $request): JsonResponse @@ -117,7 +118,7 @@ class AttachmentController extends AdminBaseController /** * 순서 변경 * - * @param ReorderAttachmentsRequest $request 순서 변경 요청 + * @param ReorderAttachmentsRequest $request 순서 변경 요청 * @return JsonResponse */ public function reorder(ReorderAttachmentsRequest $request): JsonResponse @@ -126,9 +127,12 @@ class AttachmentController extends AdminBaseController $this->attachmentService->reorder($request->input('order')); return $this->success('attachment.reorder_success'); + } catch (AuthorizationException $e) { + // 스코프 밖 첨부가 포함된 경우. 아래 제네릭 catch 보다 앞에 둬야 한다 — + // 뒤에 두면 인가 거부가 500 으로 뭉개져 상세 경로(403)와 응답이 갈린다. + return $this->error('auth.scope_denied', 403, $e->getMessage()); } catch (Exception $e) { return $this->error('attachment.reorder_failed', 500, $e->getMessage()); } } - } diff --git a/app/Http/Controllers/Api/Admin/Identity/AdminIdentityMessageTemplateController.php b/app/Http/Controllers/Api/Admin/Identity/AdminIdentityMessageTemplateController.php index f4071a48..3f37d610 100644 --- a/app/Http/Controllers/Api/Admin/Identity/AdminIdentityMessageTemplateController.php +++ b/app/Http/Controllers/Api/Admin/Identity/AdminIdentityMessageTemplateController.php @@ -1,5 +1,7 @@ validated(); - $template = IdentityMessageTemplate::findOrFail($payload['template_id']); + $template = $this->templateService->findOrFailById((int) $payload['template_id']); $rendered = $this->templateService->getPreview( $template, $payload['data'] ?? [], @@ -104,7 +107,7 @@ class AdminIdentityMessageTemplateController extends AdminBaseController * 템플릿을 시더 기본값으로 복원. * * @param IdentityMessageTemplate $template - * @return \Illuminate\Http\JsonResponse + * @return JsonResponse */ public function reset(IdentityMessageTemplate $template) { diff --git a/app/Http/Controllers/Api/Admin/LanguagePackController.php b/app/Http/Controllers/Api/Admin/LanguagePackController.php index ec29467d..4dbcc9d9 100644 --- a/app/Http/Controllers/Api/Admin/LanguagePackController.php +++ b/app/Http/Controllers/Api/Admin/LanguagePackController.php @@ -3,15 +3,16 @@ namespace App\Http\Controllers\Api\Admin; use App\Exceptions\LanguagePackSlotConflictException; +use App\Extension\Helpers\ChangelogParser; use App\Http\Controllers\Api\Base\AdminBaseController; -use App\Http\Requests\LanguagePack\InstallFromBundledRequest; +use App\Http\Requests\LanguagePack\BulkActivateRequest; use App\Http\Requests\LanguagePack\IndexLanguagePackRequest; +use App\Http\Requests\LanguagePack\InstallFromBundledRequest; use App\Http\Requests\LanguagePack\InstallFromFileRequest; use App\Http\Requests\LanguagePack\InstallFromGithubRequest; use App\Http\Requests\LanguagePack\InstallFromUrlRequest; use App\Http\Requests\LanguagePack\ManifestPreviewRequest; use App\Http\Requests\LanguagePack\UninstallLanguagePackRequest; -use App\Extension\Helpers\ChangelogParser; use App\Http\Resources\LanguagePackCollection; use App\Http\Resources\LanguagePackResource; use App\Services\LanguagePackService; @@ -66,7 +67,7 @@ class LanguagePackController extends AdminBaseController return $this->success('language_packs.fetch_success', $payload); } catch (\Throwable $e) { - return $this->error('language_packs.fetch_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.fetch_failed', 500, $e); } } @@ -117,7 +118,7 @@ class LanguagePackController extends AdminBaseController } catch (ValidationException $e) { return $this->error('language_packs.manifest_invalid', 422, $e->errors()); } catch (\Throwable $e) { - return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.install_failed', 500, $e); } } @@ -145,7 +146,7 @@ class LanguagePackController extends AdminBaseController } catch (ValidationException $e) { return $this->error('language_packs.manifest_invalid', 422, $e->errors()); } catch (\Throwable $e) { - return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.install_failed', 500, $e); } } @@ -173,7 +174,7 @@ class LanguagePackController extends AdminBaseController } catch (ValidationException $e) { return $this->error('language_packs.manifest_invalid', 422, $e->errors()); } catch (\Throwable $e) { - return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.install_failed', 500, $e); } } @@ -202,7 +203,7 @@ class LanguagePackController extends AdminBaseController } catch (ValidationException $e) { return $this->error('language_packs.manifest_invalid', 422, $e->errors()); } catch (\Throwable $e) { - return $this->error('language_packs.install_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.install_failed', 500, $e); } } @@ -237,17 +238,17 @@ class LanguagePackController extends AdminBaseController 'target' => (new LanguagePackResource($e->target))->toArray($request), ]); } catch (\Throwable $e) { - return $this->error('language_packs.activate_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.activate_failed', 500, $e); } } /** * 여러 언어팩을 일괄 활성화합니다 (요구사항 #7 reactivate 모달 → "활성화" 버튼). * - * @param \App\Http\Requests\LanguagePack\BulkActivateRequest $request 요청 + * @param BulkActivateRequest $request 요청 * @return JsonResponse 결과 (succeeded/failed 분리) */ - public function bulkActivate(\App\Http\Requests\LanguagePack\BulkActivateRequest $request): JsonResponse + public function bulkActivate(BulkActivateRequest $request): JsonResponse { $ids = $request->validated('ids'); $result = $this->service->bulkActivate($ids); @@ -278,7 +279,7 @@ class LanguagePackController extends AdminBaseController (new LanguagePackResource($pack))->toArray($request) ); } catch (\Throwable $e) { - return $this->error('language_packs.deactivate_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.deactivate_failed', 500, $e); } } @@ -302,7 +303,7 @@ class LanguagePackController extends AdminBaseController return $this->success('language_packs.uninstall_success'); } catch (\Throwable $e) { - return $this->error('language_packs.uninstall_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.uninstall_failed', 500, $e); } } @@ -318,7 +319,7 @@ class LanguagePackController extends AdminBaseController return $this->success('language_packs.check_updates_success', $result); } catch (\Throwable $e) { - return $this->error('language_packs.check_updates_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.check_updates_failed', 500, $e); } } @@ -345,7 +346,7 @@ class LanguagePackController extends AdminBaseController (new LanguagePackResource($updated))->toArray($request) ); } catch (\Throwable $e) { - return $this->error('language_packs.update_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.update_failed', 500, $e); } } @@ -361,7 +362,9 @@ class LanguagePackController extends AdminBaseController return $this->success('language_packs.refresh_cache_success', $result); } catch (\Throwable $e) { - return $this->error('language_packs.refresh_cache_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + // 예외 원문을 응답에 싣지 않는다 — 디버그 노출은 ResponseHelper 가 + // app.debug 에서만 Throwable 을 펼치는 기존 메커니즘에 위임한다. + return $this->error('language_packs.refresh_cache_failed', 500, $e); } } @@ -378,7 +381,9 @@ class LanguagePackController extends AdminBaseController return $this->success('language_packs.preview_success', $result); } catch (\Throwable $e) { - return $this->error('language_packs.preview_failed', 422, $e->getMessage(), ['error' => $e->getMessage()]); + // 업로드 ZIP 검증 실패는 의도적 422 로 존치하되, 예외 원문은 싣지 않는다 + // (다른 manifestPreview 컨트롤러 3종과 동형). + return $this->error('language_packs.preview_failed', 422, $e); } } @@ -410,7 +415,7 @@ class LanguagePackController extends AdminBaseController 'has_changelog' => $entries !== [] || $rawContent !== '', ]); } catch (\Throwable $e) { - return $this->error('language_packs.fetch_failed', 500, $e->getMessage(), ['error' => $e->getMessage()]); + return $this->error('language_packs.fetch_failed', 500, $e); } } } diff --git a/app/Http/Controllers/Api/Admin/MenuController.php b/app/Http/Controllers/Api/Admin/MenuController.php index 1672b8b0..e3453f53 100644 --- a/app/Http/Controllers/Api/Admin/MenuController.php +++ b/app/Http/Controllers/Api/Admin/MenuController.php @@ -12,6 +12,7 @@ use App\Http\Resources\MenuCollection; use App\Http\Resources\MenuResource; use App\Models\Menu; use App\Services\MenuService; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; @@ -222,6 +223,10 @@ class MenuController extends AdminBaseController } } catch (ValidationException $e) { return $this->error('menu.order_update_failed', 422, $e->errors()); + } catch (AuthorizationException $e) { + // 스코프 밖 메뉴가 포함된 경우. 제네릭 catch 보다 앞에 둬야 인가 거부가 500 으로 + // 뭉개지지 않고 상세 경로(403)와 같은 응답이 된다. + return $this->error('auth.scope_denied', 403, $e->getMessage()); } catch (\Exception $e) { return $this->error('menu.update_error', 500, $e->getMessage()); } diff --git a/app/Http/Controllers/Api/Admin/NotificationController.php b/app/Http/Controllers/Api/Admin/NotificationController.php index 5ee26214..a6251373 100644 --- a/app/Http/Controllers/Api/Admin/NotificationController.php +++ b/app/Http/Controllers/Api/Admin/NotificationController.php @@ -1,5 +1,7 @@ getSettingsSchema()); } + /** + * 설정 스키마가 public_asset_disk 키를 선언한 플러그인의 응답에 + * 공개 자산 디스크 카탈로그(available_public_asset_disks)를 부착합니다. + * + * 화면이 /api/admin/settings(core.settings.read)를 따로 조회하게 두면 이 + * 화면의 권한(core.plugins.read)과 표면이 갈려, 커스텀 역할에서 카탈로그만 + * 조용히 비는 비대칭이 생깁니다. 설정 응답 단일 표면에서 함께 내려 권한 축을 + * 일치시킵니다 (ecommerce 모듈 설정 응답의 동명 키와 동형 계약). + * 저장 시에는 스키마 기반 검증 whitelist 가 이 키를 걸러 설정에 남지 않습니다. + * + * @param string $identifier 플러그인 식별자 + * @param array $settings 응답에 실을 설정 + * @return array 카탈로그가 부착된 설정 + */ + private function withPublicAssetCatalog(string $identifier, array $settings): array + { + $plugin = $this->pluginManager->getPlugin($identifier); + + if ($plugin === null || ! array_key_exists('public_asset_disk', $plugin->getSettingsSchema())) { + return $settings; + } + + $settings['available_public_asset_disks'] = app(DriverRegistryService::class) + ->getAvailableDrivers('public_asset'); + + return $settings; + } + /** * 플러그인 설정을 조회합니다. * @@ -63,7 +92,10 @@ class PluginSettingsController extends AdminBaseController return $this->notFound('plugins.not_found'); } - return $this->success('common.success', $this->maskSensitive($identifier, $settings)); + return $this->success( + 'common.success', + $this->withPublicAssetCatalog($identifier, $this->maskSensitive($identifier, $settings)) + ); } /** @@ -87,9 +119,14 @@ class PluginSettingsController extends AdminBaseController return $this->error('plugins.settings.update_failed', 500); } + // 저장 응답에도 카탈로그 재부착 — 화면 폼 상태가 응답으로 갱신되므로 + // 누락 시 저장 직후 선택지가 비어 보인다 (ecommerce 저장 응답과 동형) return $this->success( 'plugins.settings.updated', - $this->maskSensitive($identifier, $this->pluginSettingsService->get($identifier) ?? []) + $this->withPublicAssetCatalog( + $identifier, + $this->maskSensitive($identifier, $this->pluginSettingsService->get($identifier) ?? []) + ) ); } diff --git a/app/Http/Controllers/Api/Admin/RoleController.php b/app/Http/Controllers/Api/Admin/RoleController.php index 7fce1960..b12bff66 100644 --- a/app/Http/Controllers/Api/Admin/RoleController.php +++ b/app/Http/Controllers/Api/Admin/RoleController.php @@ -2,20 +2,23 @@ namespace App\Http\Controllers\Api\Admin; +use App\Exceptions\CannotModifyProtectedRoleException; use App\Exceptions\ExtensionOwnedRoleDeleteException; +use App\Exceptions\PermissionEscalationException; use App\Exceptions\SystemRoleDeleteException; use App\Helpers\PermissionHelper; use App\Http\Controllers\Api\Base\AdminBaseController; +use App\Http\Requests\Role\ActiveRolesRequest; use App\Http\Requests\Role\RoleListRequest; use App\Http\Requests\Role\StoreRoleRequest; use App\Http\Requests\Role\UpdateRoleRequest; use App\Http\Resources\RoleCollection; use App\Http\Resources\RoleResource; use App\Models\Role; +use App\Models\User; use App\Services\RoleService; use Exception; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; /** @@ -60,13 +63,13 @@ class RoleController extends AdminBaseController * core.permissions.read 권한 보유 시 전체 활성 역할을 반환하고, * 미보유 시 현재 사용자에게 부여된 역할만 반환합니다. * - * @param Request $request HTTP 요청 객체 + * @param ActiveRolesRequest $request 활성 역할 조회 요청 * @return JsonResponse 활성화된 역할 목록을 포함한 JSON 응답 */ - public function active(Request $request): JsonResponse + public function active(ActiveRolesRequest $request): JsonResponse { try { - /** @var \App\Models\User $user */ + /** @var User $user */ $user = $request->user(); // 역할 관리 권한(core.permissions.read) 보유 → 전체 활성 역할 (사용자 관리용) @@ -78,7 +81,10 @@ class RoleController extends AdminBaseController return $this->success('role.fetch_success', [ 'data' => RoleResource::collection($roles), 'abilities' => [ - 'can_assign_roles' => PermissionHelper::check('core.permissions.update'), + // 역할 부여는 "사용자 관리"(core.users.update)의 일부다 — "역할 정의 수정" + // (core.permissions.update)이 아니다. 부여 가능한 개별 역할의 범위는 서버 + // 상한(PermissionEscalationGuard)이 역할별로 강제한다. + 'can_assign_roles' => PermissionHelper::check('core.users.update'), ], ]); } catch (Exception $e) { @@ -122,6 +128,8 @@ class RoleController extends AdminBaseController new RoleResource($role), 201 ); + } catch (PermissionEscalationException $e) { + return $this->error('exceptions.cannot_grant_unheld_permission', 403); } catch (ValidationException $e) { return $this->error('role.create_failed', 422, $e->errors()); } catch (Exception $e) { @@ -145,6 +153,10 @@ class RoleController extends AdminBaseController 'role.update_success', new RoleResource($updatedRole) ); + } catch (CannotModifyProtectedRoleException $e) { + return $this->error('exceptions.cannot_modify_protected_role', 403); + } catch (PermissionEscalationException $e) { + return $this->error('exceptions.cannot_grant_unheld_permission', 403); } catch (ValidationException $e) { return $this->error('role.update_failed', 422, $e->errors()); } catch (Exception $e) { @@ -174,6 +186,8 @@ class RoleController extends AdminBaseController } else { return $this->error('role.update_failed'); } + } catch (CannotModifyProtectedRoleException $e) { + return $this->error('exceptions.cannot_modify_protected_role', 403); } catch (Exception $e) { return $this->error('role.update_failed', 500, $e->getMessage()); } diff --git a/app/Http/Controllers/Api/Admin/SettingsController.php b/app/Http/Controllers/Api/Admin/SettingsController.php index c3e6d14f..3dc58098 100644 --- a/app/Http/Controllers/Api/Admin/SettingsController.php +++ b/app/Http/Controllers/Api/Admin/SettingsController.php @@ -188,23 +188,21 @@ class SettingsController extends AdminBaseController } /** - * 데이터베이스를 백업합니다. + * 데이터베이스 백업 — 아직 제공하지 않는 기능임을 알립니다. * - * @return JsonResponse 백업 결과 JSON 응답 + * 코어에는 DB 덤프 수단이 없어 이 엔드포인트는 구현된 적이 없다. + * 종전에는 존재하지 않는 `SettingsService::backupDatabase()` 를 호출했고, + * PHP 가 던지는 `Error` 는 `catch (\Exception)` 에 걸리지 않아 그대로 500 이 되면서 + * 내부 메서드 이름까지 응답에 실려 나갔다 (공개 #115 부록 B3). + * + * 기능 부재는 서버 고장이 아니므로 501(Not Implemented)로 답한다. + * 설정 파일 백업은 `POST /api/admin/settings/backup` 이 담당한다. + * + * @return JsonResponse 미제공 안내 JSON 응답 (501) */ public function backupDatabase(): JsonResponse { - try { - $result = $this->settingsService->backupDatabase(); - - if ($result) { - return $this->success('settings.backup_success'); - } else { - return $this->error('settings.backup_failed'); - } - } catch (\Exception $e) { - return $this->error('settings.backup_error', 500, $e->getMessage()); - } + return $this->error('settings.database_backup_unavailable', 501); } /** diff --git a/app/Http/Controllers/Api/Admin/UserController.php b/app/Http/Controllers/Api/Admin/UserController.php index 65baa1b1..50ea649a 100644 --- a/app/Http/Controllers/Api/Admin/UserController.php +++ b/app/Http/Controllers/Api/Admin/UserController.php @@ -1,8 +1,12 @@ error('exceptions.cannot_grant_unheld_permission', 403); } catch (ValidationException $e) { return $this->error('user.create_failed', 422, $e->errors()); } catch (Exception $e) { @@ -122,10 +128,14 @@ class UserController extends AdminBaseController 'user.update_success', new UserResource($updatedUser) ); + } catch (CannotModifySuperAdminException $e) { + return $this->error('exceptions.cannot_modify_super_admin', 403); + } catch (PermissionEscalationException $e) { + return $this->error('exceptions.cannot_grant_unheld_permission', 403); } catch (ValidationException $e) { return $this->error('user.update_failed', 422, $e->errors()); } catch (Exception $e) { - return $this->error('user.update_failed', 500, $e, ['error' => $e->getMessage()]); + return $this->error('user.update_failed', 500, $e); } } @@ -148,8 +158,10 @@ class UserController extends AdminBaseController 'auth.account_unlocked', new UserResource($unlocked) ); + } catch (CannotModifySuperAdminException $e) { + return $this->error('exceptions.cannot_modify_super_admin', 403); } catch (Exception $e) { - return $this->error('user.update_failed', 500, $e, ['error' => $e->getMessage()]); + return $this->error('user.update_failed', 500, $e); } } @@ -317,9 +329,13 @@ class UserController extends AdminBaseController $validated = $request->validated(); $result = $this->userService->bulkUpdateStatus($validated['ids'], $validated['status']); + // 메시지 키(user.bulk_status_updated)가 :count 플레이스홀더를 가지므로 + // 치환 파라미터를 함께 전달한다 (생략 시 원문 ":count명의 …" 이 그대로 노출) return $this->success( 'user.bulk_status_updated', - $result + $result, + 200, + ['count' => $result['updated_count'] ?? 0] ); } catch (ValidationException $e) { return $this->error('user.bulk_update_status_failed', 422, $e->errors()); diff --git a/app/Http/Controllers/Api/Auth/NotificationController.php b/app/Http/Controllers/Api/Auth/NotificationController.php index 4ebe5c3f..5f5d0b3f 100644 --- a/app/Http/Controllers/Api/Auth/NotificationController.php +++ b/app/Http/Controllers/Api/Auth/NotificationController.php @@ -1,5 +1,7 @@ unauthorized('auth.unauthenticated'); } - $this->logUserActivity('profile.withdraw'); - // 사용자 탈퇴 처리 (아바타, 토큰 삭제 및 익명화) $this->userService->withdrawUser($user); + // 활동 로그는 성공한 뒤에만 남긴다 — 실패한 탈퇴가 기록으로 남으면 + // 이력만 보고는 탈퇴한 것으로 읽힌다. + $this->logUserActivity('profile.withdraw'); + return $this->success('user.withdraw_success'); + } catch (ValidationException $e) { + // 관리자/수퍼관리자 탈퇴 차단은 요청이 잘못된 것이지 서버 오류가 아니다. + return $this->validationError($e->errors(), 'user.withdraw_failed'); + } catch (CannotDeleteSuperAdminException $e) { + // 이 예외는 ValidationException 을 상속하지 않으므로 별도 매핑이 필요하다. + return $this->validationError( + ['general' => [$e->getMessage()]], + 'user.withdraw_failed' + ); } catch (\Exception $e) { - return $this->error('user.withdraw_failed', 500, null, ['error' => $e->getMessage()]); + // 원본 예외는 로그로만 남기고, 사용자 응답에는 원문을 싣지 않는다. + Log::error('User withdraw failed (profile)', ['exception' => $e]); + + return $this->error('user.withdraw_failed', 500); } } } diff --git a/app/Http/Controllers/Api/Base/BaseApiController.php b/app/Http/Controllers/Api/Base/BaseApiController.php index c8f4e2f5..3b489b6f 100644 --- a/app/Http/Controllers/Api/Base/BaseApiController.php +++ b/app/Http/Controllers/Api/Base/BaseApiController.php @@ -9,6 +9,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * API 컨트롤러의 최상위 베이스 클래스 @@ -174,6 +175,40 @@ abstract class BaseApiController extends Controller return $response; } + /** + * 스토리지 스트림 응답에 ETag/캐싱 헤더를 부착합니다 (fileResponse 의 디스크 인지 대응). + * + * fileResponse() 는 로컬 절대 경로 전제(filemtime/filesize)라 S3 등 원격 디스크 + * 행에는 쓸 수 없습니다. 원격/로컬 공통 서빙은 StorageInterface::response() 가 + * 만든 스트림에 본 메서드로 동일한 캐싱 계약(ETag/304/Cache-Control/Expires)을 + * 입힙니다. ETag 는 파일 stat 대신 호출자가 준 결정적 소스(행 메타)로 만듭니다. + * + * @param StreamedResponse $response 스토리지 인라인 스트림 응답 + * @param string $etagSource ETag 소스 문자열 (디스크·경로·수정시각·크기 등 행 메타) + * @param int $maxAge 캐시 유지 시간 (초) + * @return StreamedResponse|Response 스트림 응답 또는 304 + */ + protected function streamedFileResponse(StreamedResponse $response, string $etagSource, int $maxAge): StreamedResponse|Response + { + $etag = md5($etagSource); + + // If-None-Match 헤더 확인 (ETag 비교) + if (request()->header('If-None-Match') === $etag) { + return response('', 304)->header('ETag', $etag); + } + + // 환경별 캐싱 정책 (fileResponse 와 동일) + $cacheControl = app()->environment('production') + ? "public, max-age={$maxAge}, immutable" + : 'no-cache'; + + $response->headers->set('ETag', $etag); + $response->headers->set('Expires', gmdate('D, d M Y H:i:s', time() + $maxAge).' GMT'); + $response->headers->set('Cache-Control', $cacheControl); + + return $response; + } + /** * JSON 응답을 반환합니다 (캐싱 헤더 포함). * diff --git a/app/Http/Controllers/Api/Identity/IdentityVerificationController.php b/app/Http/Controllers/Api/Identity/IdentityVerificationController.php index 2d898b0c..89fc494e 100644 --- a/app/Http/Controllers/Api/Identity/IdentityVerificationController.php +++ b/app/Http/Controllers/Api/Identity/IdentityVerificationController.php @@ -366,7 +366,9 @@ class IdentityVerificationController extends PublicBaseController 'scope' => $policy->scope, 'target' => $policy->target, 'purpose' => $policy->purpose, - 'provider_id' => $policy->provider_id, + // 저장값을 그대로 내보내지 않는다 — 제거된 플러그인의 provider ID 가 공개 응답에 + // 남지 않도록, 428 강제 경로와 같은 게터로 레지스트리 대조·폴백을 거친다 (A6a). + 'provider_id' => $this->policyService->resolveProviderId($policy), 'grace_minutes' => $policy->grace_minutes, 'applies_to' => $policy->applies_to, 'fail_mode' => $policy->fail_mode, diff --git a/app/Http/Controllers/Api/Public/PublicAttachmentController.php b/app/Http/Controllers/Api/Public/PublicAttachmentController.php index b90dbd0e..186201be 100644 --- a/app/Http/Controllers/Api/Public/PublicAttachmentController.php +++ b/app/Http/Controllers/Api/Public/PublicAttachmentController.php @@ -3,10 +3,10 @@ namespace App\Http\Controllers\Api\Public; use App\Http\Controllers\Api\Base\PublicBaseController; +use App\Http\Requests\Public\Attachment\DownloadAttachmentRequest; use App\Services\AttachmentService; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Http\Response; use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -22,7 +22,7 @@ class PublicAttachmentController extends PublicBaseController /** * PublicAttachmentController 생성자 * - * @param AttachmentService $attachmentService 첨부파일 서비스 + * @param AttachmentService $attachmentService 첨부파일 서비스 */ public function __construct( private AttachmentService $attachmentService @@ -36,11 +36,11 @@ class PublicAttachmentController extends PublicBaseController * 이미지 파일은 캐싱 헤더와 함께 인라인 표시하고, * 그 외 파일은 다운로드 방식으로 제공합니다. * - * @param Request $request HTTP 요청 - * @param string $hash 첨부파일 해시 (12자) + * @param DownloadAttachmentRequest $request 다운로드 요청 + * @param string $hash 첨부파일 해시 (12자) * @return BinaryFileResponse|StreamedResponse|Response|JsonResponse 파일 응답 또는 에러 응답 */ - public function download(Request $request, string $hash): BinaryFileResponse|StreamedResponse|Response|JsonResponse + public function download(DownloadAttachmentRequest $request, string $hash): BinaryFileResponse|StreamedResponse|Response|JsonResponse { $user = $request->user(); @@ -48,10 +48,10 @@ class PublicAttachmentController extends PublicBaseController // 파일 정보 조회 (권한 체크 포함) $fileInfo = $this->attachmentService->getFileInfo($hash, $user); - if (!$fileInfo) { + if (! $fileInfo) { $attachment = $this->attachmentService->findByHash($hash); - if (!$attachment) { + if (! $attachment) { return $this->notFound('attachment.not_found'); } @@ -59,10 +59,11 @@ class PublicAttachmentController extends PublicBaseController } // 이미지 파일은 캐싱 헤더와 함께 응답 (환경설정 레이아웃 캐시 TTL 사용, 기본 24시간) + // 행 disk 를 따르는 스토리지 스트림 — 로컬 경로 전제 fileResponse 는 S3 행에서 성립하지 않는다 (#99) if (str_starts_with($fileInfo['mime_type'], 'image/')) { - return $this->fileResponse( - $fileInfo['path'], - $fileInfo['mime_type'], + return $this->streamedFileResponse( + $fileInfo['response'], + $fileInfo['etag_source'], (int) g7_core_settings('cache.layout_ttl', 86400) ); } @@ -70,7 +71,7 @@ class PublicAttachmentController extends PublicBaseController // 이미지가 아닌 파일은 기존 다운로드 방식 유지 $response = $this->attachmentService->download($hash, $user); - if (!$response) { + if (! $response) { return $this->forbidden('attachment.access_denied'); } diff --git a/app/Http/Controllers/Api/Public/PublicLayoutController.php b/app/Http/Controllers/Api/Public/PublicLayoutController.php index 77cf1e68..cecd2d5e 100644 --- a/app/Http/Controllers/Api/Public/PublicLayoutController.php +++ b/app/Http/Controllers/Api/Public/PublicLayoutController.php @@ -3,10 +3,12 @@ namespace App\Http\Controllers\Api\Public; use App\Enums\ExtensionStatus; +use App\Extension\Traits\ClearsTemplateCaches; use App\Helpers\PermissionHelper; use App\Http\Controllers\Api\Base\PublicBaseController; use App\Services\LayoutService; use App\Services\TemplateService; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; @@ -17,6 +19,8 @@ use Illuminate\Http\Response; */ class PublicLayoutController extends PublicBaseController { + use ClearsTemplateCaches; + /** * 레이아웃 캐시 TTL (초) */ @@ -69,7 +73,10 @@ class PublicLayoutController extends PublicBaseController // 키만 forget 하므로 키 형식이 어긋나 무효화가 빗나간다(저장/복원 후 편집기 캔버스만 // stale). nonce 는 브라우저 HTTP 캐시 우회용(URL·ETag 차이로 이미 달성)이고, 서버 캐시 키 // 정합은 정수 버전이 SSoT 다. `(int)` 캐스팅은 PHP 가 소수점에서 절단해 정수부만 남긴다. - $cacheVersion = (int) request()->query('v', 0); + // `?v` 생략 시 현재 버전으로 폴백 — 리터럴 0 폴백은 워밍/무효화 어느 경로에도 + // 걸리지 않는 `.v0` 영구 사각 키를 만든다 (#588). + $rawVersion = request()->query('v'); + $cacheVersion = $rawVersion !== null ? (int) $rawVersion : self::getExtensionCacheVersion(); // 편집기 출처 메타 옵션 // - 옵션이 truthy 면 각 노드에 `__source` 메타를 부여한 응답을 반환 @@ -119,7 +126,7 @@ class PublicLayoutController extends PublicBaseController $mergedLayout, self::CACHE_TTL ); - } catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) { + } catch (ModelNotFoundException $e) { // 레이아웃 또는 부모 레이아웃을 찾을 수 없음 - 예외 메시지 전달 return $this->notFound($e->getMessage()); } diff --git a/app/Http/Controllers/Api/Public/PublicSearchController.php b/app/Http/Controllers/Api/Public/PublicSearchController.php index 5bcf8cce..ab9d6835 100644 --- a/app/Http/Controllers/Api/Public/PublicSearchController.php +++ b/app/Http/Controllers/Api/Public/PublicSearchController.php @@ -130,6 +130,12 @@ class PublicSearchController extends PublicBaseController // 상한에 걸린 숫자가 그 배지에서만 정확한 값처럼 보인다. 코어가 일괄로 붙인다. $response['counts_are_exact'] = $this->resolveCategoryAccuracy($results); + // 카테고리 검색 실패도 같은 이유로 코어가 일괄 조립한다 — 화면은 이 키로 + // "검색 결과 없음" 과 "검색 중 오류" 를 구분해 그린다. 모듈별 복사에 맡기면 + // 빠지는 카테고리가 생겨 그 카테고리의 실패만 0건으로 위장된다. + $response['categories_failed'] = $this->resolveCategoryFailures($results); + $response['search_failed'] = in_array(true, $response['categories_failed'], true); + // 특정 탭 조회 시 total을 해당 탭의 count로 설정 $type = $context['type'] ?? 'all'; if ($type !== 'all') { @@ -180,6 +186,27 @@ class PublicSearchController extends PublicBaseController return $accuracy; } + /** + * 카테고리별 검색 실패 여부를 모읍니다. + * + * 실패 카테고리 페이로드(`SearchCategoryPayload::failed()`)의 `failed` 플래그를 + * 카테고리 전수에 대해 수집합니다. 키가 없는 카테고리는 실패하지 않은 것으로 + * 봅니다 — 구버전 모듈이 플래그를 싣지 않아도 종전 렌더가 유지되어야 합니다. + * + * @param array $results Hook에서 반환된 검색 결과 + * @return array 카테고리 => 실패 여부 + */ + private function resolveCategoryFailures(array $results): array + { + $failures = []; + + foreach ($results as $category => $categoryData) { + $failures[$category] = ($categoryData['failed'] ?? false) === true; + } + + return $failures; + } + /** * 응답 전체의 총 건수 정확도를 정합니다. * diff --git a/app/Http/Controllers/Api/Public/PublicTemplateController.php b/app/Http/Controllers/Api/Public/PublicTemplateController.php index 13d39d9e..b51cbd3b 100644 --- a/app/Http/Controllers/Api/Public/PublicTemplateController.php +++ b/app/Http/Controllers/Api/Public/PublicTemplateController.php @@ -15,12 +15,15 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 공개 템플릿 API 컨트롤러 */ class PublicTemplateController extends PublicBaseController { + use ClearsTemplateCaches; + public function __construct( private TemplateService $templateService, private TemplateLayoutAttachmentService $layoutAttachmentService, @@ -39,8 +42,11 @@ class PublicTemplateController extends PublicBaseController // API 사용량 기록 $this->logApiUsage('templates.routes', ['identifier' => $identifier]); - // 캐시 버전을 키에 포함하여 모듈/플러그인 변경 시 캐시 무효화 - $cacheVersion = request()->query('v', 0); + // 캐시 버전을 키에 포함하여 모듈/플러그인 변경 시 캐시 무효화. + // `?v` 생략 시 현재 버전으로 폴백 — 리터럴 0 폴백은 워밍/무효화 어느 경로에도 + // 걸리지 않는 `.v0` 영구 사각 키를 만든다 (#588). `(int)` 캐스트로 키 위생 겸용. + $rawVersion = request()->query('v'); + $cacheVersion = $rawVersion !== null ? (int) $rawVersion : self::getExtensionCacheVersion(); // 확장 업데이트 중에는 활성 디렉토리가 잠시 비어 그 모듈의 라우트가 통째로 빠진다. // 그 순간의 응답을 버전 키에 캐시하면 업데이트가 끝난 뒤에도 캐시가 만료될 때까지 @@ -225,7 +231,7 @@ class PublicTemplateController extends PublicBaseController // 캐시 버전을 응답에 포함하여 프론트엔드가 API 호출 시 사용하도록 함 $responseData = $configData['data']; - $responseData['cache_version'] = ClearsTemplateCaches::getExtensionCacheVersion(); + $responseData['cache_version'] = self::getExtensionCacheVersion(); return $this->success( __('templates.messages.config_retrieved'), @@ -248,8 +254,10 @@ class PublicTemplateController extends PublicBaseController 'locale' => $locale, ]); - // 캐시 버전을 키에 포함하여 모듈/플러그인 변경 시 캐시 무효화 - $cacheVersion = request()->query('v', 0); + // 캐시 버전을 키에 포함하여 모듈/플러그인 변경 시 캐시 무효화. + // `?v` 생략 시 현재 버전으로 폴백 (getRoutes 와 동일 — `.v0` 사각 키 방지) + $rawVersion = request()->query('v'); + $cacheVersion = $rawVersion !== null ? (int) $rawVersion : self::getExtensionCacheVersion(); // 캐싱된 응답 반환 (1시간 유효) $languageData = $this->cached( @@ -340,21 +348,22 @@ class PublicTemplateController extends PublicBaseController * * @param string $identifier 템플릿 식별자 * @param TemplateLayoutAttachment $attachment 라우트 모델 바인딩된 첨부 - * @return BinaryFileResponse|Response|JsonResponse 파일 응답 또는 404 + * @return StreamedResponse|Response|JsonResponse 파일 응답 또는 404 */ - public function serveFile(string $identifier, TemplateLayoutAttachment $attachment): BinaryFileResponse|Response|JsonResponse + public function serveFile(string $identifier, TemplateLayoutAttachment $attachment): StreamedResponse|Response|JsonResponse { - $filePath = $this->layoutAttachmentService->getServableFilePath($identifier, $attachment); + $serveInfo = $this->layoutAttachmentService->getServableResponse($identifier, $attachment); - if ($filePath === null) { + if ($serveInfo === null) { return $this->notFound('templates.layout_attachments.errors.not_found'); } // 이미지/일반 파일 모두 캐싱 헤더와 함께 인라인 응답 (레이아웃 캐시 TTL, 기본 24시간). - // PublicAttachmentController 의 이미지 서빙과 동일한 fileResponse(ETag/Cache-Control) 사용. - return $this->fileResponse( - $filePath, - $attachment->mime_type, + // PublicAttachmentController 의 이미지 서빙과 동일한 streamedFileResponse(ETag/Cache-Control) 사용 + // — 행 disk 를 따르는 스토리지 스트림이라 S3 등 원격 디스크 행에서도 성립한다 (#99). + return $this->streamedFileResponse( + $serveInfo['response'], + $serveInfo['etag_source'], (int) g7_core_settings('cache.layout_ttl', 86400) ); } diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index c1d80aa2..fd60ee35 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -2,11 +2,11 @@ namespace App\Http\Middleware; +use App\Models\User; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Log; use Laravel\Sanctum\PersonalAccessToken; use Symfony\Component\HttpFoundation\Response; @@ -75,9 +75,9 @@ class SetLocale * 따라서 Bearer 토큰을 직접 파싱하여 사용자를 가져옵니다. * * @param Request $request HTTP 요청 - * @return \App\Models\User|null 사용자 또는 null + * @return User|null 사용자 또는 null */ - private function resolveUser(Request $request): ?\App\Models\User + private function resolveUser(Request $request): ?User { // 이미 인증된 경우 (세션 기반 인증) if (Auth::check()) { @@ -88,7 +88,14 @@ class SetLocale $bearerToken = $request->bearerToken(); if ($bearerToken) { $token = PersonalAccessToken::findToken($bearerToken); - if ($token && $token->tokenable instanceof \App\Models\User) { + + // 만료된 토큰은 인증되지 않은 것으로 취급 (guest 로케일 폴백). + // OptionalSanctumMiddleware 와 동일한 만료 검사를 적용한다. + if ($token && $token->expires_at && $token->expires_at->isPast()) { + return null; + } + + if ($token && $token->tokenable instanceof User) { return $token->tokenable; } } diff --git a/app/Http/Requests/Admin/UpdatePluginSettingsRequest.php b/app/Http/Requests/Admin/UpdatePluginSettingsRequest.php index 2b25a1e6..cb91f0d7 100644 --- a/app/Http/Requests/Admin/UpdatePluginSettingsRequest.php +++ b/app/Http/Requests/Admin/UpdatePluginSettingsRequest.php @@ -2,9 +2,13 @@ namespace App\Http\Requests\Admin; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 검증 안내의 항목 표시명 로케일 정합만 수정 (attributes 우선순위) + use App\Extension\HookManager; use App\Extension\PluginManager; +use App\Services\DriverRegistryService; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Lang; /** * 플러그인 설정 업데이트 요청 검증 @@ -106,6 +110,24 @@ class UpdatePluginSettingsRequest extends FormRequest $rules[$field] = $fieldRules; } + // 공개 자산 디스크는 스키마 타입만으로 검증할 수 없다 — 선택지가 코어 3종 + + // 플러그인이 훅으로 등록한 디스크라 런타임에만 확정되기 때문이다. + // PluginSettingsController 가 카탈로그를 부착하는 게이트(스키마에 이 키를 선언한 + // 플러그인)와 동일한 조건에서 검증도 걸어, "선택지를 내려준 표면" 과 "값을 받는 + // 표면" 의 강도를 맞춘다. 이렇게 두면 이 키를 선언하는 플러그인은 각자 훅을 + // 구독하지 않아도 코어 환경설정과 같은 422 를 얻는다. + if (array_key_exists('public_asset_disk', $schema)) { + $rules['public_asset_disk'][] = function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === null || $value === '') { + return; + } + + if (! app(DriverRegistryService::class)->isDriverAvailable('public_asset', $value)) { + $fail(__('validation.settings.public_asset_disk_invalid')); + } + }; + } + // 표준 이름(`core.{대상}.{동작}_validation_rules`)으로 발행하되, 이미 공개돼 구독 중일 수 있는 // 구 이름(`core.plugin_settings.update_rules`)도 함께 발행한다 — 구 이름을 구독하는 제3자 // 확장이 조용히 멈추지 않도록 한다. 구 이름은 구독자가 있을 때만 1회 경고가 남는다. @@ -143,4 +165,51 @@ class UpdatePluginSettingsRequest extends FormRequest 'min' => __('validation.min'), ]; } + + /** + * 검증 오류에 쓸 필드 표시명을 반환합니다. + * + * 설정 스키마의 `label` 을 그대로 씁니다. 이 값이 없으면 Laravel 이 원시 키를 풀어쓴 + * 이름("unused image retention days")을 그대로 노출하는데, 같은 화면의 입력 라벨은 + * 한국어인 상태라 운영자에게는 어느 항목을 고치라는 말인지 닿지 않습니다. + * + * @return array 필드 => 표시명 + */ + public function attributes(): array + { + $plugin = app(PluginManager::class)->getPlugin($this->route('identifier')); + + if (! $plugin) { + return []; + } + + $locale = app()->getLocale(); + $fallback = config('app.fallback_locale', 'en'); + $attributes = []; + + foreach ($plugin->getSettingsSchema() as $field => $config) { + // 플러그인이 요청 시점에 번역선(validation.attributes.{field})을 현재 로케일로 + // 등록했다면 그것이 우선이다. 스키마 label 은 보통 ko/en 만 들고 있어, 여기서 + // 폴백 라벨을 세우면 언어팩/리스너가 제공한 로케일별 라벨(ja 등)을 customAttributes + // 우선순위로 덮는다 — 검증 안내의 항목 이름만 다른 언어로 남는 회귀가 된다. + $translationKey = "validation.attributes.{$field}"; + if (Lang::has($translationKey, $locale, false)) { + $attributes[$field] = Lang::get($translationKey, [], $locale); + + continue; + } + + $label = $config['label'] ?? null; + + if (is_array($label)) { + $label = $label[$locale] ?? $label[$fallback] ?? reset($label); + } + + if (is_string($label) && $label !== '') { + $attributes[$field] = $label; + } + } + + return $attributes; + } } diff --git a/app/Http/Requests/Layout/StoreLayoutRequest.php b/app/Http/Requests/Layout/StoreLayoutRequest.php index 2213d13a..e11f1627 100644 --- a/app/Http/Requests/Layout/StoreLayoutRequest.php +++ b/app/Http/Requests/Layout/StoreLayoutRequest.php @@ -7,8 +7,10 @@ use App\Models\Template; use App\Models\TemplateLayout; use App\Rules\ComponentExists; use App\Rules\NoExternalUrls; +use App\Rules\SafeLayoutExpressions; use App\Rules\ValidLayoutStructure; use App\Rules\WhitelistedEndpoint; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -24,6 +26,8 @@ class StoreLayoutRequest extends FormRequest * 사용자가 이 요청을 수행할 권한이 있는지 확인 * * 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다. + * + * @return bool 항상 true (권한은 미들웨어가 담당) */ public function authorize(): bool { @@ -33,7 +37,7 @@ class StoreLayoutRequest extends FormRequest /** * 요청에 적용할 검증 규칙 * - * @return array|string> + * @return array|string> */ public function rules(): array { @@ -67,6 +71,8 @@ class StoreLayoutRequest extends FormRequest new WhitelistedEndpoint, // 4. 외부 URL 차단 new NoExternalUrls, + // 5. 표현식 샌드박스 우회/원격 스크립트 저장측 차단 + new SafeLayoutExpressions, ], ]; diff --git a/app/Http/Requests/Layout/UpdateLayoutContentRequest.php b/app/Http/Requests/Layout/UpdateLayoutContentRequest.php index 3d53ac34..48cfd551 100644 --- a/app/Http/Requests/Layout/UpdateLayoutContentRequest.php +++ b/app/Http/Requests/Layout/UpdateLayoutContentRequest.php @@ -5,6 +5,7 @@ namespace App\Http\Requests\Layout; use App\Contracts\Repositories\TemplateRepositoryInterface; use App\Extension\HookManager; use App\Rules\NoExternalUrls; +use App\Rules\SafeLayoutExpressions; use App\Rules\ValidDataSourceMerge; use App\Rules\ValidLayoutStructure; use App\Rules\ValidParentLayout; @@ -265,6 +266,14 @@ class UpdateLayoutContentRequest extends FormRequest 'required', 'array', new ValidLayoutStructure, + // 표현식 샌드박스 우회/원격 스크립트 저장측 차단 (KVE-2026-1915). + // content 트리 전체를 재귀 순회해야 하므로 배열 규칙에 부착한다 — 문자열 + // 필드(endpoint)에 부착하면 is_array 가드로 early-return 되어 무력화된다. + new SafeLayoutExpressions, + // props·actions·init_actions 의 외부 URL 차단. 편집기 저장 경로이므로 + // Store/UpdateLayoutRequest 와 동일 강도여야 한다 — 여기 누락 시 다른 + // 경로에서 막히는 외부 URL 이 편집기 저장으로는 통과한다. + new NoExternalUrls, ], // 버전 필드 @@ -426,6 +435,8 @@ class UpdateLayoutContentRequest extends FormRequest 'string', new WhitelistedEndpoint, new NoExternalUrls, + // SafeLayoutExpressions 는 content 배열 규칙에서 트리 전체를 순회하므로 여기(문자열 + // endpoint)에는 부착하지 않는다 — 문자열에 부착 시 is_array 가드로 no-op 이 된다. ]; if (! $isExtending && ! $isBaseLayout) { diff --git a/app/Http/Requests/Layout/UpdateLayoutExtensionContentRequest.php b/app/Http/Requests/Layout/UpdateLayoutExtensionContentRequest.php index f2d7e745..9a003783 100644 --- a/app/Http/Requests/Layout/UpdateLayoutExtensionContentRequest.php +++ b/app/Http/Requests/Layout/UpdateLayoutExtensionContentRequest.php @@ -4,6 +4,7 @@ namespace App\Http\Requests\Layout; use App\Extension\HookManager; use App\Rules\NoExternalUrls; +use App\Rules\SafeLayoutExpressions; use App\Rules\ValidDataSourceMerge; use App\Rules\ValidLayoutExtensionStructure; use App\Rules\WhitelistedEndpoint; @@ -62,6 +63,11 @@ class UpdateLayoutExtensionContentRequest extends FormRequest 'required', 'array', new ValidLayoutExtensionStructure, + // 표현식 샌드박스 우회/원격 스크립트 저장측 차단 (KVE-2026-1915). + // content 트리 전체(data_sources·scripts·표현식 문자열)를 재귀 순회한다. + new SafeLayoutExpressions, + // props·actions·init_actions 의 외부 URL 차단 (Store/UpdateLayoutRequest 와 동일 강도) + new NoExternalUrls, ], // 우선순위 (선택 — content.priority 와 별개로 직접 지정 가능) @@ -74,6 +80,8 @@ class UpdateLayoutExtensionContentRequest extends FormRequest 'content.data_sources' => ['nullable', 'array', new ValidDataSourceMerge], // 데이터소스 endpoint 검증 + // SafeLayoutExpressions 는 content 배열 규칙이 트리 전체를 순회하며 data_sources[].endpoint + // same-origin 까지 검사하므로 여기(문자열)에는 부착하지 않는다 (문자열 부착 시 no-op). 'content.data_sources.*.endpoint' => [ 'nullable', 'string', diff --git a/app/Http/Requests/Layout/UpdateLayoutRequest.php b/app/Http/Requests/Layout/UpdateLayoutRequest.php index 5278f30a..0d187a1b 100644 --- a/app/Http/Requests/Layout/UpdateLayoutRequest.php +++ b/app/Http/Requests/Layout/UpdateLayoutRequest.php @@ -7,8 +7,10 @@ use App\Models\Template; use App\Models\TemplateLayout; use App\Rules\ComponentExists; use App\Rules\NoExternalUrls; +use App\Rules\SafeLayoutExpressions; use App\Rules\ValidLayoutStructure; use App\Rules\WhitelistedEndpoint; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -24,6 +26,8 @@ class UpdateLayoutRequest extends FormRequest * 사용자가 이 요청을 수행할 권한이 있는지 확인 * * 권한 체크는 라우트의 permission 미들웨어에서 수행됩니다. + * + * @return bool 항상 true (권한은 미들웨어가 담당) */ public function authorize(): bool { @@ -33,7 +37,7 @@ class UpdateLayoutRequest extends FormRequest /** * 요청에 적용할 검증 규칙 * - * @return array|string> + * @return array|string> */ public function rules(): array { @@ -71,6 +75,8 @@ class UpdateLayoutRequest extends FormRequest new WhitelistedEndpoint, // 4. 외부 URL 차단 new NoExternalUrls, + // 5. 표현식 샌드박스 우회/원격 스크립트 저장측 차단 + new SafeLayoutExpressions, ], ]; diff --git a/app/Http/Requests/Public/Attachment/DownloadAttachmentRequest.php b/app/Http/Requests/Public/Attachment/DownloadAttachmentRequest.php new file mode 100644 index 00000000..581f2a5e --- /dev/null +++ b/app/Http/Requests/Public/Attachment/DownloadAttachmentRequest.php @@ -0,0 +1,34 @@ + 검증 규칙 배열 + */ + public function rules(): array + { + return []; + } +} diff --git a/app/Http/Requests/Role/ActiveRolesRequest.php b/app/Http/Requests/Role/ActiveRolesRequest.php new file mode 100644 index 00000000..f44b88cf --- /dev/null +++ b/app/Http/Requests/Role/ActiveRolesRequest.php @@ -0,0 +1,36 @@ +|string> + */ + public function rules(): array + { + return []; + } +} diff --git a/app/Http/Requests/Settings/SaveSettingsRequest.php b/app/Http/Requests/Settings/SaveSettingsRequest.php index ddcd5384..a7da06c5 100644 --- a/app/Http/Requests/Settings/SaveSettingsRequest.php +++ b/app/Http/Requests/Settings/SaveSettingsRequest.php @@ -5,6 +5,7 @@ namespace App\Http\Requests\Settings; use App\Extension\HookManager; use App\Models\Attachment; use App\Search\Engines\DatabaseFulltextEngine; +use App\Services\DriverRegistryService; use App\Support\AllowedExtensions; use App\Support\AssetUrl; use Illuminate\Contracts\Validation\ValidationRule; @@ -30,9 +31,9 @@ class SaveSettingsRequest extends FormRequest private const SUPPORTED_STORAGE_DRIVERS = ['local', 's3']; /** - * 지원되는 S3 리전 목록 + * S3 리전 형식 (AWS 리전 코드 + S3 호환 스토리지 임의값 허용 — R2 의 `auto` 등) */ - private const SUPPORTED_S3_REGIONS = ['ap-northeast-2', 'ap-northeast-1', 'us-east-1', 'us-west-2', 'eu-west-1']; + private const S3_REGION_FORMAT = 'regex:/^[a-z0-9-]+$/'; /** * 지원되는 캐시 드라이버 목록 @@ -54,11 +55,6 @@ class SaveSettingsRequest extends FormRequest */ private const SUPPORTED_WEBSOCKET_SCHEMES = ['http', 'https']; - /** - * 지원되는 검색엔진 드라이버 기본 목록 - */ - private const DEFAULT_SEARCH_ENGINE_DRIVERS = ['mysql-fulltext']; - /** * 지원되는 로그 드라이버 목록 */ @@ -230,6 +226,9 @@ class SaveSettingsRequest extends FormRequest 'upload.image_max_width' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_max_width_min', 100), 'max:'.config('core.settings_limits.upload_image_max_width_max', 10000)], 'upload.image_max_height' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_max_height_min', 100), 'max:'.config('core.settings_limits.upload_image_max_height_max', 10000)], 'upload.image_quality' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_image_quality_min', 1), 'max:'.config('core.settings_limits.upload_image_quality_max', 100)], + // 고아 첨부 정리 — 사용자 파일을 파기하므로 보존기간 하한을 서버가 강제한다. + 'upload.orphan_cleanup_enabled' => ['nullable', 'boolean'], + 'upload.orphan_retention_days' => ['nullable', 'integer', 'min:'.config('core.settings_limits.upload_orphan_retention_days_min', 1), 'max:'.config('core.settings_limits.upload_orphan_retention_days_max', 3650)], // SEO 설정 'seo.meta_title_suffix' => ['nullable', 'string', 'max:100'], @@ -313,22 +312,24 @@ class SaveSettingsRequest extends FormRequest 'advanced.pagination_max_page' => ['nullable', 'integer', 'min:'.config('core.settings_limits.advanced_pagination_max_page_min', 0), 'max:'.config('core.settings_limits.advanced_pagination_max_page_max', 100000)], // 드라이버 설정 (drivers 탭) - 'drivers.storage_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_STORAGE_DRIVERS)]), + 'drivers.storage_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_STORAGE_DRIVERS), $this->getDriverUsabilityRule('storage')]), 'drivers.s3_bucket' => ['nullable', 'string', 'max:255'], - 'drivers.s3_region' => ['nullable', Rule::in(self::SUPPORTED_S3_REGIONS)], + 'drivers.s3_region' => ['nullable', 'string', 'max:64', self::S3_REGION_FORMAT], 'drivers.s3_access_key' => ['nullable', 'string', 'max:255'], 'drivers.s3_secret_key' => ['nullable', 'string', 'max:255'], 'drivers.s3_url' => ['nullable', 'url', 'max:500'], - 'drivers.cache_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_CACHE_DRIVERS)]), + 'drivers.s3_endpoint' => ['nullable', 'url', 'max:500'], + 'drivers.s3_use_path_style' => ['nullable', 'boolean'], + 'drivers.cache_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_CACHE_DRIVERS), $this->getDriverUsabilityRule('cache')]), 'drivers.redis_host' => ['nullable', 'string', 'max:255'], 'drivers.redis_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_redis_port_min', 1), 'max:'.config('core.settings_limits.drivers_redis_port_max', 65535)], 'drivers.redis_password' => ['nullable', 'string', 'max:255'], 'drivers.redis_database' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_redis_database_min', 0), 'max:'.config('core.settings_limits.drivers_redis_database_max', 15)], 'drivers.memcached_host' => ['nullable', 'string', 'max:255'], 'drivers.memcached_port' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_memcached_port_min', 1), 'max:'.config('core.settings_limits.drivers_memcached_port_max', 65535)], - 'drivers.session_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_SESSION_DRIVERS)]), + 'drivers.session_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_SESSION_DRIVERS), $this->getDriverUsabilityRule('session')]), 'drivers.session_lifetime' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_session_lifetime_min', 1), 'max:'.config('core.settings_limits.drivers_session_lifetime_max', 43200)], // 1분 ~ 30일 - 'drivers.queue_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_QUEUE_DRIVERS)]), + 'drivers.queue_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_QUEUE_DRIVERS), $this->getDriverUsabilityRule('queue')]), 'drivers.websocket_enabled' => ['nullable', 'boolean'], 'drivers.websocket_app_id' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'], 'drivers.websocket_app_key' => [Rule::requiredIf(fn () => $this->boolean('drivers.websocket_enabled')), 'nullable', 'string', 'max:255'], @@ -344,6 +345,9 @@ class SaveSettingsRequest extends FormRequest 'drivers.log_driver' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_DRIVERS)]), 'drivers.log_level' => $this->getTabRules($tab, 'drivers', [Rule::in(self::SUPPORTED_LOG_LEVELS)]), 'drivers.log_days' => ['nullable', 'integer', 'min:'.config('core.settings_limits.drivers_log_days_min', 1), 'max:'.config('core.settings_limits.drivers_log_days_max', 365)], + // 공개 자산 디스크 — 코어 3종(none/public/s3) + 플러그인 훅 등록 디스크. + // 카탈로그가 동적(플러그인 훅)이라 정적 Rule::in 불가 → 레지스트리 조회 closure + 'drivers.public_asset_disk' => $this->getPublicAssetDiskRules(), // 본인인증(IDV) provider 기술 파라미터 — 정책 분기는 IdentityPolicy 로 흡수됨 'identity.default_provider' => ['nullable', 'string', 'max:100'], @@ -478,6 +482,60 @@ class SaveSettingsRequest extends FormRequest ]); } + /** + * 드라이버 가용성 검증 rule 을 반환합니다. + * + * 선택된 드라이버가 현재 서버에서 실제로 동작 가능한지(어댑터 클래스·PHP 확장 존재)를 + * 저장 시점에 검사합니다. 사용 불능 드라이버가 저장되면 사이트 전면 다운으로 이어질 수 + * 있으므로(예: phpredis 확장 없는 서버의 redis) 서버 게이트로 차단합니다. + * + * @param string $category 드라이버 카테고리 (storage, cache, session, queue) + * @return \Closure 검증 클로저 + */ + private function getDriverUsabilityRule(string $category): \Closure + { + return function ($attribute, $value, $fail) use ($category) { + if (empty($value)) { + return; + } + + $registry = app(DriverRegistryService::class); + + if (! $registry->isDriverUsable($category, $value)) { + $fail(__('validation.settings.driver_unusable', [ + 'driver' => $value, + 'reason' => $registry->usabilityFailureReason($category, $value), + ])); + } + }; + } + + /** + * 공개 자산 디스크에 대한 검증 규칙을 반환합니다. + * + * 선택지가 코어 3종 + 플러그인 훅 등록 디스크로 동적이라 정적 Rule::in 을 쓸 수 없고, + * DriverRegistryService 카탈로그 조회로 판정합니다. + * + * @return array 검증 규칙 배열 + */ + private function getPublicAssetDiskRules(): array + { + return [ + 'nullable', + 'string', + 'max:100', + function ($attribute, $value, $fail) { + if ($value === null || $value === '') { + return; + } + + if (! app(DriverRegistryService::class)->isDriverAvailable('public_asset', $value)) { + $fail(__('validation.settings.public_asset_disk_invalid')); + } + }, + ]; + } + /** * 추가 검증을 위해 validator after 콜백을 등록합니다. * @@ -623,6 +681,10 @@ class SaveSettingsRequest extends FormRequest 'upload.image_quality.integer' => __('validation.settings.image_quality_integer'), 'upload.image_quality.min' => __('validation.settings.image_quality_min'), 'upload.image_quality.max' => __('validation.settings.image_quality_max'), + 'upload.orphan_cleanup_enabled.boolean' => __('validation.settings.orphan_cleanup_enabled_boolean'), + 'upload.orphan_retention_days.integer' => __('validation.settings.orphan_retention_days_integer'), + 'upload.orphan_retention_days.min' => __('validation.settings.orphan_retention_days_min'), + 'upload.orphan_retention_days.max' => __('validation.settings.orphan_retention_days_max'), // SEO 설정 'seo.meta_title_suffix.max' => __('validation.settings.meta_title_suffix_max'), @@ -734,11 +796,15 @@ class SaveSettingsRequest extends FormRequest 'drivers.storage_driver.required' => __('validation.settings.storage_driver_required'), 'drivers.storage_driver.in' => __('validation.settings.storage_driver_invalid'), 'drivers.s3_bucket.max' => __('validation.settings.s3_bucket_max'), - 'drivers.s3_region.in' => __('validation.settings.s3_region_invalid'), + 'drivers.s3_region.regex' => __('validation.settings.s3_region_invalid'), + 'drivers.s3_region.max' => __('validation.settings.s3_region_max'), 'drivers.s3_access_key.max' => __('validation.settings.s3_access_key_max'), 'drivers.s3_secret_key.max' => __('validation.settings.s3_secret_key_max'), 'drivers.s3_url.url' => __('validation.settings.s3_url_invalid'), 'drivers.s3_url.max' => __('validation.settings.s3_url_max'), + 'drivers.s3_endpoint.url' => __('validation.settings.s3_endpoint_invalid'), + 'drivers.s3_endpoint.max' => __('validation.settings.s3_endpoint_max'), + 'drivers.s3_use_path_style.boolean' => __('validation.settings.s3_use_path_style_boolean'), 'drivers.cache_driver.required' => __('validation.settings.cache_driver_required'), 'drivers.cache_driver.in' => __('validation.settings.cache_driver_invalid'), 'drivers.redis_host.max' => __('validation.settings.redis_host_max'), @@ -786,6 +852,8 @@ class SaveSettingsRequest extends FormRequest 'drivers.log_days.integer' => __('validation.settings.log_days_integer'), 'drivers.log_days.min' => __('validation.settings.log_days_min'), 'drivers.log_days.max' => __('validation.settings.log_days_max'), + 'drivers.public_asset_disk.string' => __('validation.settings.public_asset_disk_invalid'), + 'drivers.public_asset_disk.max' => __('validation.settings.public_asset_disk_invalid'), // 본인인증(IDV) 설정 'identity.default_provider.string' => __('validation.settings.identity_default_provider_string'), @@ -819,25 +887,25 @@ class SaveSettingsRequest extends FormRequest 'general.site_description' => __('validation.attributes.site_description'), 'general.admin_email' => __('validation.attributes.admin_email'), 'general.timezone' => __('validation.attributes.timezone'), - 'general.language' => __('validation.attributes.language'), + 'general.language' => __('validation.attributes.default_language'), // 본인인증(IDV) 필드 'identity.default_provider' => __('validation.attributes.identity_default_provider'), 'identity.purpose_providers' => __('validation.attributes.identity_purpose_providers'), 'identity.challenge_ttl_minutes' => __('validation.attributes.identity_challenge_ttl_minutes'), 'identity.max_attempts' => __('validation.attributes.identity_max_attempts'), // notifications - 'notifications.channels' => __('validation.attributes.channels'), + 'notifications.channels' => __('validation.attributes.notification_channels'), // general - 'general.currency' => __('validation.attributes.currency'), + 'general.currency' => __('validation.attributes.default_currency'), 'general.maintenance_mode' => __('validation.attributes.maintenance_mode'), 'general.asset_url_mode' => __('validation.attributes.asset_url_mode'), 'general.site_logo' => __('validation.attributes.site_logo'), // mail 'mail.mailer' => __('validation.attributes.mailer'), - 'mail.host' => __('validation.attributes.host'), - 'mail.port' => __('validation.attributes.port'), - 'mail.username' => __('validation.attributes.username'), - 'mail.password' => __('validation.attributes.password'), + 'mail.host' => __('validation.attributes.smtp_host'), + 'mail.port' => __('validation.attributes.smtp_port'), + 'mail.username' => __('validation.attributes.smtp_username'), + 'mail.password' => __('validation.attributes.smtp_password'), 'mail.encryption' => __('validation.attributes.encryption'), 'mail.mailgun_domain' => __('validation.attributes.mailgun_domain'), 'mail.mailgun_secret' => __('validation.attributes.mailgun_secret'), @@ -853,6 +921,8 @@ class SaveSettingsRequest extends FormRequest 'upload.image_max_width' => __('validation.attributes.image_max_width'), 'upload.image_max_height' => __('validation.attributes.image_max_height'), 'upload.image_quality' => __('validation.attributes.image_quality'), + 'upload.orphan_cleanup_enabled' => __('validation.attributes.orphan_cleanup_enabled'), + 'upload.orphan_retention_days' => __('validation.attributes.orphan_retention_days'), // seo 'seo.meta_title_suffix' => __('validation.attributes.meta_title_suffix'), 'seo.meta_description' => __('validation.attributes.meta_description'), @@ -869,7 +939,7 @@ class SaveSettingsRequest extends FormRequest 'seo.twitter_default_card' => __('validation.attributes.twitter_default_card'), 'seo.twitter_default_site' => __('validation.attributes.twitter_default_site'), 'seo.cache_enabled' => __('validation.attributes.seo_page_cache_enabled'), - 'seo.cache_ttl' => __('validation.attributes.cache_ttl'), + 'seo.cache_ttl' => __('validation.attributes.seo_page_cache_ttl'), 'seo.sitemap_enabled' => __('validation.attributes.sitemap_enabled'), 'seo.sitemap_cache_ttl' => __('validation.attributes.sitemap_cache_ttl'), 'seo.sitemap_urls_per_file' => __('validation.attributes.sitemap_urls_per_file'), @@ -916,6 +986,8 @@ class SaveSettingsRequest extends FormRequest 'drivers.s3_access_key' => __('validation.attributes.s3_access_key'), 'drivers.s3_secret_key' => __('validation.attributes.s3_secret_key'), 'drivers.s3_url' => __('validation.attributes.s3_url'), + 'drivers.s3_endpoint' => __('validation.attributes.s3_endpoint'), + 'drivers.s3_use_path_style' => __('validation.attributes.s3_use_path_style'), 'drivers.cache_driver' => __('validation.attributes.cache_driver'), 'drivers.redis_host' => __('validation.attributes.redis_host'), 'drivers.redis_port' => __('validation.attributes.redis_port'), @@ -941,6 +1013,7 @@ class SaveSettingsRequest extends FormRequest 'drivers.log_driver' => __('validation.attributes.log_driver'), 'drivers.log_level' => __('validation.attributes.log_level'), 'drivers.log_days' => __('validation.attributes.log_days'), + 'drivers.public_asset_disk' => __('validation.attributes.public_asset_disk'), ]; } } diff --git a/app/Http/Requests/Settings/TestDriverConnectionRequest.php b/app/Http/Requests/Settings/TestDriverConnectionRequest.php index 45968f99..4d57cbb3 100644 --- a/app/Http/Requests/Settings/TestDriverConnectionRequest.php +++ b/app/Http/Requests/Settings/TestDriverConnectionRequest.php @@ -3,6 +3,8 @@ namespace App\Http\Requests\Settings; use App\Extension\HookManager; +use App\Services\DriverRegistryService; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -20,9 +22,9 @@ class TestDriverConnectionRequest extends FormRequest private const SUPPORTED_STORAGE_DRIVERS = ['local', 's3']; /** - * 지원되는 S3 리전 목록 + * S3 리전 형식 (AWS 리전 코드 + S3 호환 스토리지 임의값 허용 — R2 의 `auto` 등) */ - private const SUPPORTED_S3_REGIONS = ['ap-northeast-2', 'ap-northeast-1', 'us-east-1', 'us-west-2', 'eu-west-1']; + private const S3_REGION_FORMAT = 'regex:/^[a-z0-9-]+$/'; /** * 지원되는 캐시 드라이버 목록 @@ -57,24 +59,26 @@ class TestDriverConnectionRequest extends FormRequest /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { $rules = [ // 드라이버 선택 - 'storage_driver' => ['nullable', Rule::in(self::SUPPORTED_STORAGE_DRIVERS)], - 'cache_driver' => ['nullable', Rule::in(self::SUPPORTED_CACHE_DRIVERS)], - 'session_driver' => ['nullable', Rule::in(self::SUPPORTED_SESSION_DRIVERS)], - 'queue_driver' => ['nullable', Rule::in(self::SUPPORTED_QUEUE_DRIVERS)], + 'storage_driver' => ['nullable', Rule::in(self::SUPPORTED_STORAGE_DRIVERS), $this->getDriverUsabilityRule('storage')], + 'cache_driver' => ['nullable', Rule::in(self::SUPPORTED_CACHE_DRIVERS), $this->getDriverUsabilityRule('cache')], + 'session_driver' => ['nullable', Rule::in(self::SUPPORTED_SESSION_DRIVERS), $this->getDriverUsabilityRule('session')], + 'queue_driver' => ['nullable', Rule::in(self::SUPPORTED_QUEUE_DRIVERS), $this->getDriverUsabilityRule('queue')], 'websocket_enabled' => ['nullable', 'boolean'], // S3 설정 's3_bucket' => ['nullable', 'string', 'max:255'], - 's3_region' => ['nullable', Rule::in(self::SUPPORTED_S3_REGIONS)], + 's3_region' => ['nullable', 'string', 'max:64', self::S3_REGION_FORMAT], 's3_access_key' => ['nullable', 'string', 'max:255'], 's3_secret_key' => ['nullable', 'string', 'max:255'], 's3_url' => ['nullable', 'url', 'max:500'], + 's3_endpoint' => ['nullable', 'url', 'max:500'], + 's3_use_path_style' => ['nullable', 'boolean'], // Redis 설정 'redis_host' => ['nullable', 'string', 'max:255'], @@ -86,11 +90,14 @@ class TestDriverConnectionRequest extends FormRequest 'memcached_host' => ['nullable', 'string', 'max:255'], 'memcached_port' => ['nullable', 'integer', 'min:1', 'max:65535'], - // Websocket 설정 + // Websocket 설정 — client(브라우저 접속) / server(백엔드 broadcast HTTP API) endpoint 분리 'websocket_app_key' => ['nullable', 'string', 'max:255'], 'websocket_host' => ['nullable', 'string', 'max:255'], 'websocket_port' => ['nullable', 'integer', 'min:1', 'max:65535'], 'websocket_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)], + 'websocket_server_host' => ['nullable', 'string', 'max:255'], + 'websocket_server_port' => ['nullable', 'integer', 'min:1', 'max:65535'], + 'websocket_server_scheme' => ['nullable', Rule::in(self::SUPPORTED_WEBSOCKET_SCHEMES)], ]; // 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공 @@ -107,11 +114,15 @@ class TestDriverConnectionRequest extends FormRequest return [ 'storage_driver.in' => __('validation.settings.storage_driver_invalid'), 's3_bucket.max' => __('validation.settings.s3_bucket_max'), - 's3_region.in' => __('validation.settings.s3_region_invalid'), + 's3_region.regex' => __('validation.settings.s3_region_invalid'), + 's3_region.max' => __('validation.settings.s3_region_max'), 's3_access_key.max' => __('validation.settings.s3_access_key_max'), 's3_secret_key.max' => __('validation.settings.s3_secret_key_max'), 's3_url.url' => __('validation.settings.s3_url_invalid'), 's3_url.max' => __('validation.settings.s3_url_max'), + 's3_endpoint.url' => __('validation.settings.s3_endpoint_invalid'), + 's3_endpoint.max' => __('validation.settings.s3_endpoint_max'), + 's3_use_path_style.boolean' => __('validation.settings.s3_use_path_style_boolean'), 'cache_driver.in' => __('validation.settings.cache_driver_invalid'), 'redis_host.max' => __('validation.settings.redis_host_max'), 'redis_port.integer' => __('validation.settings.redis_port_integer'), @@ -134,6 +145,38 @@ class TestDriverConnectionRequest extends FormRequest 'websocket_port.min' => __('validation.settings.websocket_port_min'), 'websocket_port.max' => __('validation.settings.websocket_port_max'), 'websocket_scheme.in' => __('validation.settings.websocket_scheme_invalid'), + 'websocket_server_host.max' => __('validation.settings.websocket_server_host_max'), + 'websocket_server_port.integer' => __('validation.settings.websocket_server_port_integer'), + 'websocket_server_port.min' => __('validation.settings.websocket_server_port_min'), + 'websocket_server_port.max' => __('validation.settings.websocket_server_port_max'), + 'websocket_server_scheme.in' => __('validation.settings.websocket_server_scheme_invalid'), ]; } + + /** + * 드라이버 가용성 검증 rule 을 반환합니다. + * + * 저장 게이트(SaveSettingsRequest)와 동일 판정 — 사용 불능 드라이버는 테스트 요청 + * 단계에서도 422 로 차단해 어느 경로로도 통과하지 못하게 합니다. + * + * @param string $category 드라이버 카테고리 (storage, cache, session, queue) + * @return \Closure 검증 클로저 + */ + private function getDriverUsabilityRule(string $category): \Closure + { + return function ($attribute, $value, $fail) use ($category) { + if (empty($value)) { + return; + } + + $registry = app(DriverRegistryService::class); + + if (! $registry->isDriverUsable($category, $value)) { + $fail(__('validation.settings.driver_unusable', [ + 'driver' => $value, + 'reason' => $registry->usabilityFailureReason($category, $value), + ])); + } + }; + } } diff --git a/app/Http/Requests/Settings/TestMailRequest.php b/app/Http/Requests/Settings/TestMailRequest.php index 4dc54069..4eb43e25 100644 --- a/app/Http/Requests/Settings/TestMailRequest.php +++ b/app/Http/Requests/Settings/TestMailRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\Settings; use App\Extension\HookManager; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; /** @@ -25,7 +26,7 @@ class TestMailRequest extends FormRequest /** * 검증 규칙을 반환합니다. * - * @return array|string> + * @return array|string> */ public function rules(): array { @@ -72,8 +73,8 @@ class TestMailRequest extends FormRequest 'to_email.max' => __('settings.invalid_email'), 'from_address.required' => __('validation.required', ['attribute' => __('validation.attributes.from_address')]), 'from_name.required' => __('validation.required', ['attribute' => __('validation.attributes.from_name')]), - 'host.required' => __('validation.required', ['attribute' => __('validation.attributes.host')]), - 'port.required' => __('validation.required', ['attribute' => __('validation.attributes.port')]), + 'host.required' => __('validation.required', ['attribute' => __('validation.attributes.smtp_host')]), + 'port.required' => __('validation.required', ['attribute' => __('validation.attributes.smtp_port')]), 'mailgun_domain.required' => __('validation.required', ['attribute' => __('validation.attributes.mailgun_domain')]), 'mailgun_secret.required' => __('validation.required', ['attribute' => __('validation.attributes.mailgun_secret')]), 'ses_key.required' => __('validation.required', ['attribute' => __('validation.attributes.ses_key')]), @@ -81,4 +82,22 @@ class TestMailRequest extends FormRequest 'ses_region.required' => __('validation.required', ['attribute' => __('validation.attributes.ses_region')]), ]; } + + /** + * 검증 속성명을 반환합니다. + * + * 범용 필드명(host/port/username/password)은 전역 라벨이 범용 문구이므로, + * SMTP 전용 라벨을 이 요청에서만 명시 매핑합니다. + * + * @return array + */ + public function attributes(): array + { + return [ + 'host' => __('validation.attributes.smtp_host'), + 'port' => __('validation.attributes.smtp_port'), + 'username' => __('validation.attributes.smtp_username'), + 'password' => __('validation.attributes.smtp_password'), + ]; + } } diff --git a/app/Http/Requests/Settings/UpdateSettingRequest.php b/app/Http/Requests/Settings/UpdateSettingRequest.php index 22b3cacb..039c6d4c 100644 --- a/app/Http/Requests/Settings/UpdateSettingRequest.php +++ b/app/Http/Requests/Settings/UpdateSettingRequest.php @@ -2,34 +2,198 @@ namespace App\Http\Requests\Settings; +use App\Contracts\Repositories\ConfigRepositoryInterface; use App\Extension\HookManager; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Arr; +/** + * 단건 설정 저장 요청 검증 + * + * 벌크 저장(`SaveSettingsRequest`)은 키마다 타입 규칙을 갖지만, 이 경로는 키를 URL 로 받아 + * 값 하나만 싣는다. 그래서 값의 형태는 `config/settings/defaults.json` 의 기본값에서 도출한다 + * — 기본값이 그 설정의 타입 선언이기 때문이다. + */ class UpdateSettingRequest extends FormRequest { + /** + * 문자열 값의 최대 길이 + */ + private const MAX_STRING_LENGTH = 1000; + + /** + * 배열 값의 JSON 직렬화 최대 길이 + */ + private const MAX_ARRAY_JSON_LENGTH = 5000; + /** * Determine if the user is authorized to make this request. + * + * @return bool 권한 체크는 permission 미들웨어 체인이 담당하므로 항상 true 반환 */ public function authorize(): bool { return true; } + /** + * 검증 전 입력 값을 설정의 선언 타입으로 정규화합니다. + * + * 폼 전송(`application/x-www-form-urlencoded`)은 모든 값을 문자열로 실어 보낸다. + * 정규화 없이 저장하면 boolean 설정에 `"1"`, 정수 설정에 `"3600"` 같은 문자열이 남고, + * 그 값을 읽는 쪽은 타입 비교(`=== true`)에서 조용히 어긋난다. + * + * 해석할 수 없는 값(예: boolean 설정에 `"maybe"`)은 건드리지 않는다 — null 이나 false 로 + * 바꿔 두면 오타 입력이 "정상 저장" 으로 통과한다. 그 판정은 규칙이 맡는다. + */ + protected function prepareForValidation(): void + { + if (! $this->has('value')) { + return; + } + + $default = $this->declaredDefault(); + $value = $this->input('value'); + + // 문자열 설정을 비우면 빈 문자열로 남긴다. `ConvertEmptyStringsToNull` 미들웨어가 + // 이미 `''` 를 null 로 바꿔 두므로, 여기서 되돌리지 않으면 선언 타입이 문자열인 + // 설정에 null 이 저장되어 그 값을 읽는 쪽이 기본값(`''`)과 다른 형태를 받는다. + if (is_string($default) && $value === null) { + $this->merge(['value' => '']); + + return; + } + + // 선언 타입이 문자열/배열이거나 기본값이 없으면 원본 유지 + if (! is_bool($default) && ! is_int($default) && ! is_float($default)) { + return; + } + + // 비-문자열 설정에서 빈 문자열은 "값을 지운다" 는 뜻이다 + if ($value === '') { + $this->merge(['value' => null]); + + return; + } + + if (! is_string($value)) { + return; + } + + if (is_bool($default)) { + $casted = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + if ($casted !== null) { + $this->merge(['value' => $casted]); + } + + return; + } + + if (is_int($default) && preg_match('/^-?\d+$/', $value) === 1) { + $this->merge(['value' => (int) $value]); + + return; + } + + if (is_float($default) && is_numeric($value)) { + $this->merge(['value' => (float) $value]); + } + } + /** * Get the validation rules that apply to the request. * - * @return array|string> + * @return array|string> */ public function rules(): array { $rules = [ - 'value' => 'required|string|max:1000', + // `required` 가 아니라 `present` 다 — 빈 문자열/null 은 "이 설정을 비운다" 는 정상 + // 입력이고, 이를 거부하면 운영자가 한 번 넣은 값을 화면에서 지울 수 없다. + // 대신 `value` 키 자체가 빠진 요청은 그대로 거부한다. + 'value' => ['present', 'nullable', $this->valueShapeRule()], ]; // 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 훅 제공 return HookManager::applyFilters('core.settings.update_validation_rules', $rules, $this); } + /** + * 값의 형태(타입·길이)를 검증하는 규칙을 반환합니다. + * + * @return \Closure 검증 클로저 + */ + private function valueShapeRule(): \Closure + { + return function (string $attribute, mixed $value, callable $fail): void { + if ($value === null) { + return; + } + + $default = $this->declaredDefault(); + + // 선언 타입과 다른 값은 거부한다 — 정규화가 해석하지 못한 입력이 여기로 온다. + if (is_bool($default) && ! is_bool($value)) { + $fail(__('validation.setting.value.boolean')); + + return; + } + + if (is_int($default) && ! is_int($value)) { + $fail(__('validation.setting.value.integer')); + + return; + } + + if (is_float($default) && ! is_int($value) && ! is_float($value)) { + $fail(__('validation.setting.value.numeric')); + + return; + } + + if (is_string($value)) { + if (mb_strlen($value) > self::MAX_STRING_LENGTH) { + $fail(__('validation.setting.value.max', ['max' => self::MAX_STRING_LENGTH])); + } + + return; + } + + if (is_array($value)) { + if (mb_strlen((string) json_encode($value)) > self::MAX_ARRAY_JSON_LENGTH) { + $fail(__('validation.setting.value.array_max', ['max' => self::MAX_ARRAY_JSON_LENGTH])); + } + + return; + } + + if (! is_bool($value) && ! is_int($value) && ! is_float($value)) { + $fail(__('validation.setting.value.type')); + } + }; + } + + /** + * 저장 대상 키의 선언 기본값을 반환합니다. + * + * 반환값의 타입이 곧 그 설정의 선언 타입입니다. 선언이 없으면 null 을 반환하며, + * 이 경우 타입 강제 없이 형태 검증만 수행합니다 (확장이 추가한 키 등). + * + * @return mixed 선언 기본값 또는 null + */ + private function declaredDefault(): mixed + { + $key = $this->route('key'); + + if (! is_string($key) || $key === '') { + return null; + } + + return Arr::get(app(ConfigRepositoryInterface::class)->getDefaults(), $key); + } + /** * Get custom messages for validator errors. * @@ -38,9 +202,7 @@ class UpdateSettingRequest extends FormRequest public function messages(): array { return [ - 'value.required' => __('validation.setting.value.required'), - 'value.string' => __('validation.setting.value.string'), - 'value.max' => __('validation.setting.value.max'), + 'value.present' => __('validation.setting.value.present'), ]; } } diff --git a/app/Http/Requests/User/BulkUpdateUserStatusRequest.php b/app/Http/Requests/User/BulkUpdateUserStatusRequest.php index cd5d4ab7..3657586a 100644 --- a/app/Http/Requests/User/BulkUpdateUserStatusRequest.php +++ b/app/Http/Requests/User/BulkUpdateUserStatusRequest.php @@ -6,6 +6,7 @@ use App\Enums\UserStatus; use App\Extension\HookManager; use App\Models\User; use App\Rules\ExcludeCurrentUser; +use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -13,17 +14,20 @@ class BulkUpdateUserStatusRequest extends FormRequest { /** * 요청 권한을 확인합니다. + * + * 권한 검사는 라우트의 `permission:admin,core.users.update` 미들웨어가 담당합니다. + * + * @return bool 항상 true (미들웨어에서 권한 제어) */ public function authorize(): bool { - // 사용자 업데이트 권한 확인 - return $this->user()->can('core.users.update'); + return true; } /** * 검증 규칙을 정의합니다. * - * @return array|string> + * @return array|string> */ public function rules(): array { @@ -45,9 +49,9 @@ class BulkUpdateUserStatusRequest extends FormRequest public function messages(): array { return [ - 'ids.required' => __('validation.required', ['attribute' => __('validation.attributes.ids')]), - 'ids.array' => __('validation.array', ['attribute' => __('validation.attributes.ids')]), - 'ids.min' => __('validation.min.array', ['attribute' => __('validation.attributes.ids'), 'min' => 1]), + 'ids.required' => __('validation.required', ['attribute' => __('validation.attributes.user_ids')]), + 'ids.array' => __('validation.array', ['attribute' => __('validation.attributes.user_ids')]), + 'ids.min' => __('validation.min.array', ['attribute' => __('validation.attributes.user_ids'), 'min' => 1]), 'ids.*.required' => __('validation.required', ['attribute' => __('validation.attributes.user_id')]), 'ids.*.uuid' => __('validation.uuid', ['attribute' => __('validation.attributes.user_id')]), 'ids.*.exists' => __('validation.exists', ['attribute' => __('validation.attributes.user_id')]), @@ -55,4 +59,20 @@ class BulkUpdateUserStatusRequest extends FormRequest 'status.in' => __('validation.in', ['attribute' => __('validation.attributes.status')]), ]; } + + /** + * 검증 속성명을 반환합니다. + * + * 범용 필드명 ids 는 전역 라벨이 범용 문구이므로, + * 사용자 대상 라벨을 이 요청에서만 명시 매핑합니다. + * + * @return array + */ + public function attributes(): array + { + return [ + 'ids' => __('validation.attributes.user_ids'), + 'ids.*' => __('validation.attributes.user_id'), + ]; + } } diff --git a/app/Http/Resources/UserCollection.php b/app/Http/Resources/UserCollection.php index 3cb1f996..d032b83e 100644 --- a/app/Http/Resources/UserCollection.php +++ b/app/Http/Resources/UserCollection.php @@ -4,6 +4,7 @@ namespace App\Http\Resources; use App\Http\Resources\Traits\HasAbilityCheck; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; class UserCollection extends BaseApiCollection { @@ -20,7 +21,10 @@ class UserCollection extends BaseApiCollection 'can_create' => 'core.users.create', 'can_update' => 'core.users.update', 'can_delete' => 'core.users.delete', - 'can_assign_roles' => 'core.permissions.update', + // 역할 부여는 "사용자 관리"(core.users.update)의 일부다 — "역할 정의 수정" + // (core.permissions.update)이 아니다. 부여 가능한 개별 역할의 범위는 서버 + // 상한(PermissionEscalationGuard)이 역할별로 강제한다. + 'can_assign_roles' => 'core.users.update', ]; } @@ -36,7 +40,7 @@ class UserCollection extends BaseApiCollection 'data' => $this->mapWithRowNumber(function ($user) { return (new UserResource($user))->toListArray(request()); }), - 'pagination' => $this->when($this->resource instanceof \Illuminate\Pagination\LengthAwarePaginator, [ + 'pagination' => $this->when($this->resource instanceof LengthAwarePaginator, [ 'current_page' => $this->resource->currentPage(), 'last_page' => $this->resource->lastPage(), 'per_page' => $this->resource->perPage(), @@ -56,7 +60,7 @@ class UserCollection extends BaseApiCollection */ public function withStatistics(array $statistics = []): array { - $isPaginator = $this->resource instanceof \Illuminate\Pagination\LengthAwarePaginator; + $isPaginator = $this->resource instanceof LengthAwarePaginator; return [ 'data' => $this->mapWithRowNumber(function ($user) { @@ -87,7 +91,7 @@ class UserCollection extends BaseApiCollection 'data' => $this->mapWithRowNumber(function ($user) { return (new UserResource($user))->withAdminInfo(); }), - 'pagination' => $this->when($this->resource instanceof \Illuminate\Pagination\LengthAwarePaginator, [ + 'pagination' => $this->when($this->resource instanceof LengthAwarePaginator, [ 'current_page' => $this->resource->currentPage(), 'last_page' => $this->resource->lastPage(), 'per_page' => $this->resource->perPage(), diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 3c397b3b..969d7ed6 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -204,7 +204,10 @@ class UserResource extends BaseApiResource 'can_create' => 'core.users.create', 'can_update' => 'core.users.update', 'can_delete' => 'core.users.delete', - 'can_assign_roles' => 'core.permissions.update', + // 역할 부여는 "사용자 관리"(core.users.update)의 일부다 — "역할 정의 수정" + // (core.permissions.update: 역할에 권한을 가감)이 아니다. 부여 가능한 개별 역할의 + // 범위는 서버 상한(PermissionEscalationGuard)이 역할별로 강제한다. + 'can_assign_roles' => 'core.users.update', ]; } diff --git a/app/Http/View/Composers/TemplateComposer.php b/app/Http/View/Composers/TemplateComposer.php index 716dfbd1..01cef569 100644 --- a/app/Http/View/Composers/TemplateComposer.php +++ b/app/Http/View/Composers/TemplateComposer.php @@ -14,6 +14,7 @@ use App\Services\ModuleSettingsService; use App\Services\PluginSettingsService; use App\Services\SettingsService; use App\Services\TemplateService; +use App\Support\TrustedScriptHosts; use Illuminate\View\View; class TemplateComposer @@ -103,6 +104,10 @@ class TemplateComposer // 확장 프론트엔드 병합 번들 URL (상시 ON — 활성 에셋이 없으면 null) $bundleUrls = $this->buildExtensionBundleUrls($moduleAssets, $pluginAssets, $extensionCacheVersion); + // 신뢰 외부 스크립트 호스트 — 레이아웃 scripts[].src same-origin 예외 허용목록 + // (KVE-2026-1915: 확장이 manifest 로 선언한 CDN 호스트만 런타임 로더가 허용) + $trustedScriptHosts = TrustedScriptHosts::hosts(); + $view->with('activeAdminTemplate', $activeTemplate); $view->with('extensionCacheVersion', $extensionCacheVersion); $view->with('frontendSettings', $frontendSettings); @@ -115,5 +120,6 @@ class TemplateComposer $view->with('activePluginsMeta', $activePluginsMeta); $view->with('appConfig', $appConfig); $view->with('templateExternals', $templateExternals); + $view->with('trustedScriptHosts', $trustedScriptHosts); } } diff --git a/app/Http/View/Composers/UserTemplateComposer.php b/app/Http/View/Composers/UserTemplateComposer.php index 5188e100..4205a4c3 100644 --- a/app/Http/View/Composers/UserTemplateComposer.php +++ b/app/Http/View/Composers/UserTemplateComposer.php @@ -14,6 +14,7 @@ use App\Services\ModuleSettingsService; use App\Services\PluginSettingsService; use App\Services\SettingsService; use App\Services\TemplateService; +use App\Support\TrustedScriptHosts; use Illuminate\View\View; /** @@ -109,6 +110,10 @@ class UserTemplateComposer // 확장 프론트엔드 병합 번들 URL (상시 ON — 활성 에셋이 없으면 null) $bundleUrls = $this->buildExtensionBundleUrls($moduleAssets, $pluginAssets, $extensionCacheVersion); + // 신뢰 외부 스크립트 호스트 — 레이아웃 scripts[].src same-origin 예외 허용목록 + // (KVE-2026-1915: 확장이 manifest 로 선언한 CDN 호스트만 런타임 로더가 허용) + $trustedScriptHosts = TrustedScriptHosts::hosts(); + $view->with('activeUserTemplate', $activeTemplate); $view->with('extensionCacheVersion', $extensionCacheVersion); $view->with('frontendSettings', $frontendSettings); @@ -121,5 +126,6 @@ class UserTemplateComposer $view->with('activePluginsMeta', $activePluginsMeta); $view->with('appConfig', $appConfig); $view->with('templateExternals', $templateExternals); + $view->with('trustedScriptHosts', $trustedScriptHosts); } } diff --git a/app/Listeners/LayoutEditor/MarkOrphanedCustomTranslations.php b/app/Listeners/LayoutEditor/MarkOrphanedCustomTranslations.php index 814dbd24..bdb67fe4 100644 --- a/app/Listeners/LayoutEditor/MarkOrphanedCustomTranslations.php +++ b/app/Listeners/LayoutEditor/MarkOrphanedCustomTranslations.php @@ -4,7 +4,7 @@ namespace App\Listeners\LayoutEditor; use App\Contracts\Extension\HookListenerInterface; use App\Contracts\Repositories\TemplateCustomTranslationRepositoryInterface; -use App\Extension\Cache\CoreCacheDriver; +use App\Extension\Traits\ClearsTemplateCaches; use App\Services\LanguagePack\CustomTranslationUsageScanner; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Log; @@ -30,6 +30,8 @@ use Throwable; */ class MarkOrphanedCustomTranslations implements HookListenerInterface { + use ClearsTemplateCaches; + /** * @param TemplateCustomTranslationRepositoryInterface $repository 커스텀 키 리포지토리 * @param CustomTranslationUsageScanner $scanner 키 사용 스캐너 (순수) @@ -124,12 +126,11 @@ class MarkOrphanedCustomTranslations implements HookListenerInterface /** * 다국어 캐시 버전을 무효화합니다. * - * `ext.cache_version` 은 코어 소유 키이므로 컨테이너 바인딩에 의존하지 않고 - * 항상 CoreCacheDriver(`g7:core:` 네임스페이스)를 직접 생성합니다 - * (ClearsTemplateCaches 트레이트와 동일 SSoT). + * `ext.cache_version` 쓰기는 ClearsTemplateCaches 트레이트 단일 지점 경유 — + * 트레이트가 고정 CoreCacheDriver + 메모이즈 스토어로 write/read 일관성을 보장한다. */ private function invalidateLanguageCache(): void { - (new CoreCacheDriver(config('cache.default', 'array')))->put('ext.cache_version', time()); + $this->incrementExtensionCacheVersion(); } } diff --git a/app/Listeners/SeoSettingsCacheListener.php b/app/Listeners/SeoSettingsCacheListener.php index cc5aa557..0669ad68 100644 --- a/app/Listeners/SeoSettingsCacheListener.php +++ b/app/Listeners/SeoSettingsCacheListener.php @@ -28,6 +28,14 @@ class SeoSettingsCacheListener implements HookListenerInterface 'method' => 'onSettingsSave', 'priority' => 20, ], + // 단건 저장(`PUT /api/admin/settings/{key}`)은 after_save 가 아니라 after_set 을 + // 발화한다. 이 구독이 없으면 SEO 설정을 단건으로 바꿔도 캐시가 남아 있다. + // (after_save 를 추가 발화하는 대신 구독을 늘리는 이유: 활동로그 리스너가 두 훅을 + // 각각 기록해 저장 1회가 로그 2건이 된다) + 'core.settings.after_set' => [ + 'method' => 'onSettingSet', + 'priority' => 20, + ], ]; } @@ -57,6 +65,40 @@ class SeoSettingsCacheListener implements HookListenerInterface return; } + $this->clearSeoCaches(['tab' => $tab]); + } + + /** + * 코어 설정 단건 저장 시 SEO 캐시를 무효화합니다. + * + * seo 카테고리 키가 실제로 저장된 경우에만 전체 캐시를 삭제합니다. + * + * @param mixed ...$args 훅 인자 ($key, $value, $result) + */ + public function onSettingSet(...$args): void + { + $key = $args[0] ?? null; + $result = $args[2] ?? false; + + if (! is_string($key) || ! str_starts_with($key, 'seo.')) { + return; + } + + // 저장 실패는 상태를 바꾸지 않았으므로 캐시도 그대로 둔다 + if ($result !== true) { + return; + } + + $this->clearSeoCaches(['key' => $key]); + } + + /** + * SEO 전체 캐시와 sitemap 캐시를 삭제합니다. + * + * @param array $logContext 로그에 남길 컨텍스트 + */ + private function clearSeoCaches(array $logContext): void + { try { $cache = app(SeoCacheManagerInterface::class); @@ -66,9 +108,7 @@ class SeoSettingsCacheListener implements HookListenerInterface // Sitemap 캐시 삭제 app(CacheInterface::class)->forget('seo.sitemap'); - Log::info('[SEO] Core SEO settings changed — all cache cleared', [ - 'tab' => $tab, - ]); + Log::info('[SEO] Core SEO settings changed — all cache cleared', $logContext); } catch (\Throwable $e) { Log::warning('[SEO] Core SEO settings cache invalidation failed', [ 'error' => $e->getMessage(), diff --git a/app/Models/Attachment.php b/app/Models/Attachment.php index 2c67c9ad..ac3a4378 100644 --- a/app/Models/Attachment.php +++ b/app/Models/Attachment.php @@ -111,6 +111,7 @@ class Attachment extends Model */ public function getFullPathAttribute(): string { + // audit:allow no-storage-disk-direct reason: 행 disk 의 파일시스템 절대경로 해석 — 코어 첨부 path 는 카테고리 없는 원 경로라 StorageInterface(카테고리/경로) 계약과 형태가 다르고, 모델 accessor 는 DI 지점이 없다 (기존 동작 유지) return Storage::disk($this->disk)->path($this->path); } @@ -121,7 +122,21 @@ class Attachment extends Model */ public function getDownloadUrlAttribute(): string { - return '/api/attachment/'.$this->hash; + return self::urlForHash($this->hash); + } + + /** + * 해시 기반 다운로드 URL 조립 단일 지점. + * + * 조인 결과 행처럼 모델 hydration 없이 hash 만 손에 있는 호출측 + * (예: 게시판 인기 게시물 목록의 아바타 URL)이 사용합니다. + * + * @param string $hash 첨부파일 해시 + * @return string 다운로드 URL + */ + public static function urlForHash(string $hash): string + { + return '/api/attachment/'.$hash; } /** diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index 3ba4d595..971381d0 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -7,6 +7,7 @@ use App\Enums\ScheduleFrequency; use App\Enums\ScheduleResultStatus; use App\Enums\ScheduleType; use App\Models\Concerns\HasUserOverrides; +use Carbon\Carbon; use Cron\CronExpression; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -153,8 +154,16 @@ class Schedule extends Model } try { + // cron 식은 사이트 설정 시간대로 해석한다 — 코드 예약(app.schedule_timezone)과 + // 같은 기준이어야 같은 화면에서 "새벽 4시" 의 근거가 어긋나지 않는다. + // 계산 결과는 저장 타임존(app.timezone = UTC)으로 되돌려 저장한다. + $storeTimezone = (string) config('app.timezone', 'UTC'); + $scheduleTimezone = (string) config('app.schedule_timezone', $storeTimezone); + $cron = new CronExpression($this->expression); - $this->next_run_at = $cron->getNextRunDate(); + $nextRun = $cron->getNextRunDate('now', 0, false, $scheduleTimezone); + + $this->next_run_at = Carbon::instance($nextRun)->setTimezone($storeTimezone); } catch (\Exception $e) { $this->next_run_at = null; } diff --git a/app/Models/User.php b/app/Models/User.php index 6e365a0e..379ef4b8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -15,6 +15,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphOne; +use Illuminate\Database\Schema\Builder as SchemaBuilder; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; @@ -24,6 +25,11 @@ class User extends Authenticatable implements HasLocalePreference /** @use HasFactory */ use HasApiTokens, HasFactory, Notifiable; + /** + * 닉네임 컬럼 최대 길이 (마이그레이션에서 명시한 값) + */ + public const NICKNAME_MAX_LENGTH = 50; + /** @var array 활동 로그 추적 필드 */ public static array $activityLogFields = [ 'name' => ['label_key' => 'activity_log.fields.name', 'type' => 'text'], @@ -602,20 +608,42 @@ class User extends Authenticatable implements HasLocalePreference */ public function withdraw(): bool { + // 멱등 가드 — 이미 탈퇴한 계정에 다시 호출해도 접미사가 겹쳐 붙지 않는다. + if ($this->isWithdrawn()) { + return true; + } + $now = now(); $dateSuffix = $now->format('Ymd'); // 예: 20260127 + // name/email 은 마이그레이션에서 길이를 지정하지 않은 문자열 컬럼이므로 + // 스키마 기본 길이(AppServiceProvider 의 defaultStringLength)를 그대로 따른다. + // 여기에 리터럴을 박으면 기본 길이를 바꾸는 순간 조용히 어긋난다. + $defaultLength = SchemaBuilder::$defaultStringLength ?? 255; + // 이름에 suffix 추가 (있는 경우만) if ($this->name) { - $this->name = $this->name.'_탈퇴_'.$dateSuffix; + $this->name = $this->appendWithdrawnSuffix($this->name, '_탈퇴_'.$dateSuffix, $defaultLength); } // 이메일에 suffix 추가 (필수) - $this->email = $this->email.'_deleted_'.$dateSuffix; + // + // 접미사에 사용자 ID 를 포함해 구조적으로 유일하게 만든다. 날짜만 붙이면 + // 같은 이메일로 재가입한 회원이 같은 날 다시 탈퇴할 때 email unique 에 + // 걸려 탈퇴가 실패한다(공개이슈 #112). + $this->email = $this->appendWithdrawnSuffix( + $this->email, + '_deleted_'.$dateSuffix.'_'.$this->id, + $defaultLength, + ); // 닉네임에 suffix 추가 (있는 경우만, 날짜 없이) if ($this->nickname) { - $this->nickname = $this->nickname.'_탈퇴'; + // nickname 은 마이그레이션에서 길이를 명시(50)한 컬럼이다. + // 이 접미사는 유일성 토큰(id)이 없다 — 현재 nickname/name 에 unique 인덱스가 + // 없어 무해하지만, 향후 unique 인덱스를 추가하면 email 과 동일한 충돌 + // (같은 값 재가입 후 재탈퇴 실패)이 재발하므로 그때 id 부착으로 전환할 것. + $this->nickname = $this->appendWithdrawnSuffix($this->nickname, '_탈퇴', self::NICKNAME_MAX_LENGTH); } // 상태 변경 @@ -624,4 +652,26 @@ class User extends Authenticatable implements HasLocalePreference return $this->save(); } + + /** + * 탈퇴 접미사를 컬럼 길이 안에서 부착합니다. + * + * 원값이 길면 접미사 길이만큼 앞을 잘라 붙입니다 — 자르지 않으면 접미사가 컬럼 + * 길이를 넘겨 strict mode 저장 예외가 나고, 탈퇴가 중간에서 실패합니다. + * + * @param string $value 원본 값 + * @param string $suffix 부착할 접미사 + * @param int $maxLength 컬럼 최대 길이 + * @return string 접미사가 부착된 값 + */ + protected function appendWithdrawnSuffix(string $value, string $suffix, int $maxLength): string + { + $available = $maxLength - mb_strlen($suffix); + + if ($available < 0) { + $available = 0; + } + + return mb_substr($value, 0, $available).$suffix; + } } diff --git a/app/Providers/CoreServiceProvider.php b/app/Providers/CoreServiceProvider.php index a7064352..5254d16a 100644 --- a/app/Providers/CoreServiceProvider.php +++ b/app/Providers/CoreServiceProvider.php @@ -10,7 +10,6 @@ use App\Console\Commands\BenchCommand; use App\Contracts\Extension\CacheInterface; use App\Contracts\Extension\ExtensionMiddlewareRegistryInterface; use App\Contracts\Extension\HookListenerInterface; -use App\Contracts\Extension\ModuleSettingsInterface; use App\Contracts\Extension\StorageInterface; use App\Contracts\Extension\TemplateManagerInterface; use App\Contracts\Repositories\ActivityLogRepositoryInterface; @@ -96,7 +95,9 @@ use App\Services\AttachmentService; use App\Services\DriverRegistryService; use App\Services\LayoutExtensionService; use App\Services\TemplateLayoutAttachmentService; +use App\Services\TemplateService; use App\Services\UniqueIdService; +use App\Support\ExtensionSettingsMirror; use App\Support\PrivilegedDatabaseAccounts; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; @@ -269,14 +270,14 @@ class CoreServiceProvider extends ServiceProvider $this->app->when(AttachmentService::class) ->needs(StorageInterface::class) ->give(function () { - return new CoreStorageDriver(config('attachment.disk', 'local')); + return new CoreStorageDriver(config('attachment.disk', 'attachments')); }); // TemplateLayoutAttachmentService용 CoreStorageDriver 바인딩 $this->app->when(TemplateLayoutAttachmentService::class) ->needs(StorageInterface::class) ->give(function () { - return new CoreStorageDriver(config('attachment.disk', 'local')); + return new CoreStorageDriver(config('attachment.disk', 'attachments')); }); // 코어 서비스용 CoreCacheDriver 바인딩 @@ -347,6 +348,12 @@ class CoreServiceProvider extends ServiceProvider $this->app->singleton(TemplateManagerInterface::class, function ($app) { return $app->make(TemplateManager::class); }); + + // 템플릿 서비스도 공유 인스턴스로 등록한다. + // 미등록 상태에서는 주입 지점마다 새로 만들어지고, 그 생성자가 매번 템플릿 + // 디렉토리를 재스캔했다. 요청 단위 상태는 라우트 병합 열화 플래그 하나뿐이며 + // 그 플래그는 병합 진입 시 재설정되므로 공유해도 안전하다. + $this->app->singleton(TemplateService::class); } /** @@ -739,69 +746,8 @@ class CoreServiceProvider extends ServiceProvider */ protected function loadModuleSettingsToConfig(ModuleManager $moduleManager): void { - $moduleSettings = []; - - foreach (array_keys($moduleManager->getActiveModules()) as $identifier) { - try { - // 모듈별 환경설정 서비스 조회 - $settingsService = $this->resolveModuleSettingsService($identifier); - - if ($settingsService instanceof ModuleSettingsInterface) { - $settings = $settingsService->getAllSettings(); - if (! empty($settings)) { - $moduleSettings[$identifier] = $settings; - } - } - } catch (\Throwable $e) { - Log::warning("모듈 환경설정 로딩 실패: {$identifier}", [ - 'error' => $e->getMessage(), - ]); - } - } - - Config::set('g7_settings.modules', $moduleSettings); - } - - /** - * 모듈의 환경설정 서비스를 찾아 인스턴스화합니다. - * - * 다음 순서로 설정 서비스를 찾습니다: - * 1. 인터페이스 바인딩: Modules\Vendor\Module\Contracts\ModuleSettingsServiceInterface - * 2. 구체 클래스: Modules\Vendor\Module\Services\ModuleSettingsService - * - * @param string $identifier 모듈 식별자 (예: sirsoft-ecommerce) - * @return ModuleSettingsInterface|null 설정 서비스 인스턴스 - */ - protected function resolveModuleSettingsService(string $identifier): ?ModuleSettingsInterface - { - // vendor-module 형식을 네임스페이스로 변환 - $parts = explode('-', $identifier); - if (count($parts) < 2) { - return null; - } - - $vendor = ucfirst($parts[0]); - $moduleName = ucfirst($parts[1]); - - // 1. 인터페이스 바인딩 확인 - $interfaceClass = "Modules\\{$vendor}\\{$moduleName}\\Contracts\\{$moduleName}SettingsServiceInterface"; - if ($this->app->bound($interfaceClass)) { - $service = $this->app->make($interfaceClass); - if ($service instanceof ModuleSettingsInterface) { - return $service; - } - } - - // 2. 구체 클래스 확인 - $concreteClass = "Modules\\{$vendor}\\{$moduleName}\\Services\\{$moduleName}SettingsService"; - if (class_exists($concreteClass)) { - $service = $this->app->make($concreteClass); - if ($service instanceof ModuleSettingsInterface) { - return $service; - } - } - - return null; + // 미러 채움 로직은 ExtensionSettingsMirror 가 단일 소유한다 (공개이슈 #109). + app(ExtensionSettingsMirror::class)->refreshAllModules(); } /** @@ -814,28 +760,9 @@ class CoreServiceProvider extends ServiceProvider */ protected function loadPluginSettingsToConfig(PluginManager $pluginManager): void { - $pluginSettings = []; - - foreach (array_keys($pluginManager->getActivePlugins()) as $identifier) { - try { - $settingsPath = storage_path("app/plugins/{$identifier}/settings/setting.json"); - - if (File::exists($settingsPath)) { - $content = File::get($settingsPath); - $settings = json_decode($content, true); - - if (json_last_error() === JSON_ERROR_NONE && ! empty($settings)) { - $pluginSettings[$identifier] = $settings; - } - } - } catch (\Throwable $e) { - Log::warning("플러그인 환경설정 로딩 실패: {$identifier}", [ - 'error' => $e->getMessage(), - ]); - } - } - - Config::set('g7_settings.plugins', $pluginSettings); + // 종전에는 setting.json 을 raw 로 읽어 defaults 병합·정규화가 빠지고 암호문이 그대로 + // 실렸다 — 값의 형태가 전용 게터와 달랐다. 이제 미러 소유자에 위임한다 (공개이슈 #109). + app(ExtensionSettingsMirror::class)->refreshAllPlugins(); } /** @@ -888,7 +815,8 @@ class CoreServiceProvider extends ServiceProvider $configKey = $driverRegistry->getConfigKey($category); if ($configKey && $defaultDriver) { - Config::set($configKey, $defaultDriver); + // log 카테고리의 적용 키(stack.channels)는 배열형 — 형태 변환은 레지스트리가 담당 + Config::set($configKey, $driverRegistry->getConfigValueForDriver($category, $defaultDriver)); } Log::warning("플러그인 드라이버 '{$selectedDriver}'가 '{$category}' 카테고리에서 사용 불가능합니다. 기본 드라이버 '{$defaultDriver}'로 폴백합니다."); diff --git a/app/Providers/SettingsServiceProvider.php b/app/Providers/SettingsServiceProvider.php index 678f4846..05e37e67 100644 --- a/app/Providers/SettingsServiceProvider.php +++ b/app/Providers/SettingsServiceProvider.php @@ -4,8 +4,10 @@ namespace App\Providers; use App\Repositories\JsonConfigRepository; use App\Support\AllowedExtensions; +use App\Support\ExtensionSettingsMirror; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; +use Predis\Client; /** * 설정 서비스 프로바이더 @@ -15,23 +17,8 @@ use Illuminate\Support\ServiceProvider; */ class SettingsServiceProvider extends ServiceProvider { - /** - * 코어 설정 카테고리 목록 - */ - private const CORE_CATEGORIES = [ - 'mail', - 'general', - 'security', - 'debug', - 'drivers', - 'cache', - 'upload', - 'core_update', - 'geoip', - 'seo', - 'identity', - 'pagination', - ]; + // 코어 설정 카테고리 목록은 ExtensionSettingsMirror::CORE_CATEGORIES 가 단독 소유한다. + // 여기에 사본을 두면 미러가 읽는 목록과 갈라져도 아무도 알아채지 못한다. /** * 서비스를 등록합니다. @@ -40,6 +27,10 @@ class SettingsServiceProvider extends ServiceProvider */ public function register(): void { + // 미러 채움 소유자를 컨테이너 싱글톤으로 등록한다 — 부팅/저장/테스트가 + // 같은 인스턴스를 해석해야 교체(스텁 주입)가 모든 경로에 통한다. + $this->app->singleton(ExtensionSettingsMirror::class); + // JsonConfigRepository를 직접 인스턴스화 (DI 컨테이너 사용 불가) $configRepository = new JsonConfigRepository; @@ -48,7 +39,7 @@ class SettingsServiceProvider extends ServiceProvider $this->applyAppConfig($configRepository); $this->applyDebugConfig($configRepository); $this->applyDriverConfig($configRepository); - $this->applyCacheConfig($configRepository); + $this->applyPublicAssetDiskConfig($configRepository); $this->applyUploadConfig($configRepository); $this->applyCoreUpdateConfig($configRepository); $this->applyGeoIpConfig($configRepository); @@ -66,16 +57,9 @@ class SettingsServiceProvider extends ServiceProvider */ private function loadCoreSettingsToConfig(JsonConfigRepository $configRepository): void { - $coreSettings = []; - - foreach (self::CORE_CATEGORIES as $category) { - $settings = $configRepository->getCategory($category); - if (! empty($settings)) { - $coreSettings[$category] = $settings; - } - } - - Config::set('g7_settings.core', $coreSettings); + // 미러 채움 로직은 ExtensionSettingsMirror 가 단일 소유한다 — + // 저장 시점 재채움(공개이슈 #109)과 같은 코드를 써야 부팅과 저장의 결과가 갈리지 않는다. + $this->app->make(ExtensionSettingsMirror::class)->refreshCore($configRepository); } /** @@ -187,6 +171,14 @@ class SettingsServiceProvider extends ServiceProvider // 환경설정의 timezone은 사용자 표시용 기본 타임존 // app.timezone(서버 저장 타임존)은 항상 UTC 유지 Config::set('app.default_user_timezone', $generalSettings['timezone']); + + // 예약 작업의 시각 해석 기준도 사이트 설정 시간대를 따른다. + // Laravel 의 Kernel::scheduleTimezone() 이 이 키를 읽어 Schedule 인스턴스 + // 전체에 일괄 적용하므로, 코어·확장이 등록한 모든 예약이 같은 기준을 공유한다. + // (이벤트마다 ->timezone() 을 붙이는 방식은 나중에 추가되는 예약이 + // 조용히 UTC 기준으로 돌아가므로 채택하지 않는다.) + // 미설정 시에는 Laravel 기본 폴백(app.timezone = UTC)이 그대로 적용된다. + Config::set('app.schedule_timezone', $generalSettings['timezone']); } if (! empty($generalSettings['language'])) { @@ -342,6 +334,16 @@ class SettingsServiceProvider extends ServiceProvider Config::set('filesystems.default', $driverSettings['storage_driver']); } + // 코어 첨부 업로드 디스크: ATTACHMENT_DISK env 명시가 항상 우선하고, + // 미설정 시 storage_driver=s3 를 따른다. env() 직접 호출은 config:cache 환경에서 + // null 로 고정되므로 config 에 태운 attachment.disk_explicit 로 판별한다. + // 빈 문자열도 미명시로 취급한다 — `ATTACHMENT_DISK=` 가 복사된 .env 에서 + // 빈 값을 명시로 읽으면 전환이 영구 미발동한다 (config 정규화의 2차 방어). + // 기존 행은 행 disk 로 서빙되므로 신구 디스크 혼재는 안전하다. + if (($driverSettings['storage_driver'] ?? null) === 's3' && in_array(config('attachment.disk_explicit'), [null, ''], true)) { + Config::set('attachment.disk', 's3'); + } + // 웹소켓 설정 $this->applyWebsocketConfig($driverSettings); @@ -354,11 +356,55 @@ class SettingsServiceProvider extends ServiceProvider $this->applyLogConfig($driverSettings); } + /** + * 공개 자산 디스크 설정을 적용합니다. + * + * testing 환경에서는 dev 공유 drivers.json 값이 테스트로 흘러들지 않도록 + * 주입을 건너뜁니다 (테스트 격리). 실제 주입/정규화는 + * injectPublicAssetDiskConfig() 가 담당합니다 — 가드와 분리해 두어야 + * 정규화 규칙('none' → '')이 테스트에서 단언 가능합니다. + */ + private function applyPublicAssetDiskConfig(JsonConfigRepository $configRepository): void + { + if (env('APP_ENV') === 'testing') { + return; + } + + $this->injectPublicAssetDiskConfig($configRepository); + } + + /** + * drivers.public_asset_disk 저장값을 core.storage.public_asset_disk 로 주입합니다. + * + * 'none'(스트리밍 유지 선택)/빈값은 미설정('')으로 정규화합니다. + * 테스트 격리 가드(applyPublicAssetDiskConfig)를 통과한 뒤에만 호출됩니다. + * + * @param JsonConfigRepository $configRepository 설정 저장소 + */ + private function injectPublicAssetDiskConfig(JsonConfigRepository $configRepository): void + { + $driverSettings = $configRepository->getCategory('drivers'); + + $disk = (string) ($driverSettings['public_asset_disk'] ?? ''); + + Config::set('core.storage.public_asset_disk', $disk === 'none' ? '' : $disk); + } + /** * Redis 연결 설정을 적용합니다. */ private function applyRedisConfig(array $driverSettings): void { + // phpredis 확장이 없는 서버에서 redis 드라이버 선택 시 `Class "Redis" not found` 로 + // 사이트 전면 다운되는 결함 방어 — 설정된 클라이언트가 phpredis 인데 확장이 없고 + // predis 가 있으면 predis 로 폴백한다. 확장이 있으면 기존 phpredis 경로 그대로다. + // env('REDIS_CLIENT') 미명시 판별은 무효였다: .env.example 이 REDIS_CLIENT=phpredis + // 를 활성 배포하므로 표준 설치에서 영구 미발동이었고, env() 직접 호출은 + // config:cache 환경에서 null 로 고정된다 (A8 disk_explicit 와 동형 함정). + if ($this->shouldFallBackToPredis(extension_loaded('redis'))) { + Config::set('database.redis.client', 'predis'); + } + if (! empty($driverSettings['redis_host'])) { Config::set('database.redis.default.host', $driverSettings['redis_host']); Config::set('database.redis.cache.host', $driverSettings['redis_host']); @@ -380,6 +426,25 @@ class SettingsServiceProvider extends ServiceProvider } } + /** + * Redis 클라이언트를 predis 로 폴백해야 하는지 판정합니다. + * + * 설정된 클라이언트(config — env 시점 값이 config:cache 에도 박제됨)가 phpredis 를 + * 가리키는데 확장이 로드되어 있지 않고 predis 가 존재하면 참. phpredis 명시 설정이라도 + * 확장이 없으면 어차피 동작 불가이므로 predis 전환이 유일한 동작 경로다 (#99 A2). + * 확장 로드 여부는 인자로 받는다 — 확장 설치 머신에서 부재 상태를 재현할 수 없어 + * 판정 자체를 단위 검증 가능하게 분리한 것 (테스트가 양 분기를 주입 검증). + * + * @param bool $phpredisLoaded phpredis 확장 로드 여부 (extension_loaded('redis')) + * @return bool predis 로 폴백해야 하면 true + */ + private function shouldFallBackToPredis(bool $phpredisLoaded): bool + { + return config('database.redis.client', 'phpredis') !== 'predis' + && ! $phpredisLoaded + && class_exists(Client::class); + } + /** * Memcached 연결 설정을 적용합니다. */ @@ -418,6 +483,16 @@ class SettingsServiceProvider extends ServiceProvider if (! empty($driverSettings['s3_url'])) { Config::set('filesystems.disks.s3.url', $driverSettings['s3_url']); } + + // S3 호환 스토리지(R2/MinIO/NCP 등)의 API 요청 대상 — s3_url(공개 URL base)과 별개 축이다. + // endpoint 미주입 시 SDK 는 AWS 리전 도메인으로만 요청하므로 호환 스토리지 연결이 불가능하다. + if (! empty($driverSettings['s3_endpoint'])) { + Config::set('filesystems.disks.s3.endpoint', $driverSettings['s3_endpoint']); + } + + if (! empty($driverSettings['s3_use_path_style'])) { + Config::set('filesystems.disks.s3.use_path_style_endpoint', true); + } } /** @@ -619,26 +694,6 @@ class SettingsServiceProvider extends ServiceProvider Config::set('settings.identity', $settings); } - /** - * 캐시 설정을 Laravel config에 적용합니다. - */ - private function applyCacheConfig(JsonConfigRepository $configRepository): void - { - $cacheSettings = $configRepository->getCategory('cache'); - - if (empty($cacheSettings)) { - return; - } - - if (! empty($cacheSettings['driver'])) { - Config::set('cache.default', $cacheSettings['driver']); - } - - if (! empty($cacheSettings['prefix'])) { - Config::set('cache.prefix', $cacheSettings['prefix']); - } - } - /** * 업로드 설정을 Laravel config에 적용합니다. */ @@ -650,10 +705,6 @@ class SettingsServiceProvider extends ServiceProvider return; } - if (! empty($uploadSettings['disk'])) { - Config::set('filesystems.default', $uploadSettings['disk']); - } - // 관리자 설정은 MB, config/attachment.* 는 KB — 변환은 이 지점 단 한 곳에서만 수행한다. // (기존에는 존재하지 않는 키 `max_size` 를 읽어 설정이 어디에도 반영되지 않았다) if (! empty($uploadSettings['max_file_size'])) { diff --git a/app/Repositories/ActivityLogRepository.php b/app/Repositories/ActivityLogRepository.php index 15418f77..5381cf63 100644 --- a/app/Repositories/ActivityLogRepository.php +++ b/app/Repositories/ActivityLogRepository.php @@ -7,6 +7,7 @@ use App\Helpers\PermissionHelper; use App\Helpers\TimezoneHelper; use App\Http\Resources\BaseApiCollection; use App\Models\ActivityLog; +use App\Repositories\Concerns\DeletesInBatches; use App\Repositories\Concerns\HasMultipleSearchFilters; use App\Repositories\Concerns\PaginatesWithDeferredJoin; use App\Repositories\Concerns\ResolvesSortSpec; @@ -23,6 +24,7 @@ use Illuminate\Database\Eloquent\Model; */ class ActivityLogRepository implements ActivityLogRepositoryInterface { + use DeletesInBatches; use HasMultipleSearchFilters; use PaginatesWithDeferredJoin; use ResolvesSortSpec; @@ -316,4 +318,20 @@ class ActivityLogRepository implements ActivityLogRepositoryInterface { return ActivityLog::where('user_id', $userId)->update(['user_id' => null]); } + + /** + * 보존 기간이 지난 활동 로그를 삭제합니다. + * + * 보존 기간 하한(1일)은 이 계층이 소유한다 — 파기는 되돌릴 수 없으므로 호출자마다 + * 다시 막지 않고 실제로 지우는 자리에서 한 번 막는다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function deleteOlderThan(int $days): int + { + return $this->deleteInBatches( + ActivityLog::where('created_at', '<', now()->subDays(max(1, $days))) + ); + } } diff --git a/app/Repositories/AttachmentRepository.php b/app/Repositories/AttachmentRepository.php index bc9b2669..c53ed6b6 100644 --- a/app/Repositories/AttachmentRepository.php +++ b/app/Repositories/AttachmentRepository.php @@ -5,6 +5,7 @@ namespace App\Repositories; use App\Contracts\Repositories\AttachmentRepositoryInterface; use App\Models\Attachment; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; /** @@ -23,6 +24,17 @@ class AttachmentRepository implements AttachmentRepositoryInterface return Attachment::find($id); } + /** + * 소프트삭제된 행까지 포함해 ID로 첨부파일 조회 + * + * @param int $id 첨부파일 ID + * @return Attachment|null 첨부파일 또는 null + */ + public function findByIdWithTrashed(int $id): ?Attachment + { + return Attachment::withTrashed()->find($id); + } + /** * 여러 ID로 첨부파일 조회 (order 정렬) * @@ -283,4 +295,32 @@ class AttachmentRepository implements AttachmentRepositoryInterface } }); } + + /** + * 소유자 없이 방치된 고아 첨부 후보를 오래된 순으로 조회합니다. + * + * @param Carbon $threshold 기준 시각 + * @param int $limit 최대 조회 건수 + * @param array $protectedIds 보호할 첨부 ID 목록 + * @return Collection 고아 첨부 후보 + */ + public function findOrphanCandidates(Carbon $threshold, int $limit, array $protectedIds = []): Collection + { + $query = Attachment::withTrashed() + ->whereNull('attachmentable_type') + ->whereNull('attachmentable_id') + // 확장이 소유한 첨부는 그 확장의 라이프사이클 소관이므로 코어 GC 대상이 아니다. + ->whereNull('source_identifier') + ->where('created_at', '<', $threshold); + + if ($protectedIds !== []) { + $query->whereNotIn('id', $protectedIds); + } + + return $query + ->orderBy('created_at') + ->orderBy('id') + ->limit($limit) + ->get(['id', 'disk', 'path', 'collection', 'created_at']); + } } diff --git a/app/Repositories/Concerns/DeletesInBatches.php b/app/Repositories/Concerns/DeletesInBatches.php new file mode 100644 index 00000000..843ef83d --- /dev/null +++ b/app/Repositories/Concerns/DeletesInBatches.php @@ -0,0 +1,59 @@ +deleteBatchSize); + $model = $query->getModel(); + $keyName = $model->getKeyName(); + $total = 0; + + while (true) { + // audit:allow query-repeated-execution 배치 삭제는 같은 술어를 의도적으로 반복 평가한다 — 매 회차의 모집단이 직전 삭제로 줄어든다 + $keys = (clone $query)->orderBy($keyName)->limit($batchSize)->pluck($keyName); + + if ($keys->isEmpty()) { + return $total; + } + + $total += $model->newQuery()->whereIn($keyName, $keys)->delete(); + + // 마지막 배치 — 더 조회할 것이 없다 + if ($keys->count() < $batchSize) { + return $total; + } + } + } +} diff --git a/app/Repositories/IdentityVerificationLogRepository.php b/app/Repositories/IdentityVerificationLogRepository.php index d62ec2f3..1dd21814 100644 --- a/app/Repositories/IdentityVerificationLogRepository.php +++ b/app/Repositories/IdentityVerificationLogRepository.php @@ -7,6 +7,7 @@ use App\Enums\IdentityVerificationStatus; use App\Helpers\TimezoneHelper; use App\Models\IdentityPolicy; use App\Models\IdentityVerificationLog; +use App\Repositories\Concerns\DeletesInBatches; use App\Repositories\Concerns\PaginatesWithDeferredJoin; use App\Support\Query\PaginationLimits; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -19,6 +20,7 @@ use Illuminate\Support\Carbon; */ class IdentityVerificationLogRepository implements IdentityVerificationLogRepositoryInterface { + use DeletesInBatches; use PaginatesWithDeferredJoin; /** @@ -127,9 +129,10 @@ class IdentityVerificationLogRepository implements IdentityVerificationLogReposi */ public function purgeOlderThan(int $days): int { - return IdentityVerificationLog::query() - ->where('created_at', '<', Carbon::now()->subDays(max(1, $days))) - ->delete(); + return $this->deleteInBatches( + IdentityVerificationLog::query() + ->where('created_at', '<', Carbon::now()->subDays(max(1, $days))) + ); } /** diff --git a/app/Repositories/LanguagePackRepository.php b/app/Repositories/LanguagePackRepository.php index 426408b1..183f47f8 100644 --- a/app/Repositories/LanguagePackRepository.php +++ b/app/Repositories/LanguagePackRepository.php @@ -125,16 +125,34 @@ class LanguagePackRepository implements LanguagePackRepositoryInterface } /** - * 페이지네이션 + 필터링된 언어팩 목록을 조회합니다. + * 페이지네이션 + 필터링된 언어팩 목록을 조회합니다 (관리자 목록 전용). * * @param array $filters 필터 (scope, target_identifier, locale, status, vendor) * @param int $perPage 페이지당 건수 + * @param int|null $page 페이지 번호 (null 이면 요청 파라미터에서 해석) * @return LengthAwarePaginator 페이지네이션 결과 */ - public function paginate(array $filters = [], int $perPage = 20): LengthAwarePaginator + public function paginate(array $filters = [], int $perPage = 20, ?int $page = null): LengthAwarePaginator { // audit:allow repository-paginate-column-pruning reason: 언어팩 정의 테이블 — 설치된 팩 수만큼만 존재하고 넓은 컬럼이 없다 - return $this->buildFilteredQuery($filters)->paginate($perPage); + return $this->buildFilteredQuery($filters)->paginate($perPage, ['*'], 'page', $page); + } + + /** + * 업데이트 확인용 전체 언어팩 컬렉션을 조회합니다. + * + * 전량 순회 의도를 `paginate(큰 값)` 으로 흉내 내면 page 인자 암묵 해석 때문에 + * HTTP `?page=2` 가 순회 범위를 비워 버린다 (공개 이슈 #102 동형). 순회는 이 + * 메서드로만 한다. + * + * @return Collection 설치된 전체 언어팩 + */ + public function allForUpdateCheck(): Collection + { + // audit:allow query-unbounded-get reason: 언어팩은 운영자가 설치한 팩 수만큼만 존재하는 + // 설정성 테이블이다 (사용량과 무관) — 대용량 목록 페이지네이션 규정의 설정성 + // 테이블 예외 조항 (docs/backend/pagination.md) + return LanguagePack::query()->orderBy('identifier')->get(); } /** diff --git a/app/Repositories/MenuRepository.php b/app/Repositories/MenuRepository.php index b9d5b17b..35b4cda6 100644 --- a/app/Repositories/MenuRepository.php +++ b/app/Repositories/MenuRepository.php @@ -131,6 +131,21 @@ class MenuRepository implements MenuRepositoryInterface return Menu::with(['creator', 'parent', 'children'])->find($id); } + /** + * 여러 ID로 메뉴를 한 번에 조회합니다. + * + * @param array $ids 메뉴 ID 목록 + * @return Collection 메뉴 컬렉션 + */ + public function findByIds(array $ids): Collection + { + if (empty($ids)) { + return new Collection; + } + + return Menu::whereIn('id', $ids)->get(); + } + /** * 슬러그로 메뉴를 찾습니다. * @@ -142,6 +157,17 @@ class MenuRepository implements MenuRepositoryInterface return Menu::where('slug', $slug)->first(); } + /** + * URL 로 메뉴를 찾습니다. + * + * @param string $url 메뉴 URL + * @return Menu|null 찾은 메뉴 모델 또는 null + */ + public function findByUrl(string $url): ?Menu + { + return Menu::where('url', $url)->first(); + } + /** * 새로운 메뉴를 생성합니다. * diff --git a/app/Repositories/NotificationLogRepository.php b/app/Repositories/NotificationLogRepository.php index 52415dc6..dfa247ee 100644 --- a/app/Repositories/NotificationLogRepository.php +++ b/app/Repositories/NotificationLogRepository.php @@ -6,6 +6,7 @@ use App\Contracts\Repositories\NotificationLogRepositoryInterface; use App\Enums\NotificationLogStatus; use App\Models\NotificationLog; use App\Models\User; +use App\Repositories\Concerns\DeletesInBatches; use App\Repositories\Concerns\PaginatesWithDeferredJoin; use App\Repositories\Concerns\ResolvesSortSpec; use App\Support\Query\KeysetPaginator; @@ -17,6 +18,7 @@ use Illuminate\Pagination\LengthAwarePaginator; class NotificationLogRepository implements NotificationLogRepositoryInterface { + use DeletesInBatches; use PaginatesWithDeferredJoin; use ResolvesSortSpec; @@ -98,6 +100,22 @@ class NotificationLogRepository implements NotificationLogRepositoryInterface return NotificationLog::whereIn('id', $ids)->delete(); } + /** + * 보존 기간이 지난 발송 이력을 삭제합니다. + * + * 보존 기간 하한(1일)은 이 계층이 소유한다 — 파기는 되돌릴 수 없으므로 호출자마다 + * 다시 막지 않고 실제로 지우는 자리에서 한 번 막는다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function deleteOlderThan(int $days): int + { + return $this->deleteInBatches( + NotificationLog::where('created_at', '<', now()->subDays(max(1, $days))) + ); + } + /** * 페이지네이션 목록 조회. * diff --git a/app/Repositories/PermissionRepository.php b/app/Repositories/PermissionRepository.php index 84810e1c..38f11496 100644 --- a/app/Repositories/PermissionRepository.php +++ b/app/Repositories/PermissionRepository.php @@ -30,6 +30,21 @@ class PermissionRepository implements PermissionRepositoryInterface return Permission::find($id); } + /** + * 여러 ID로 권한을 일괄 조회합니다. + * + * @param array $ids 권한 ID 배열 + * @return Collection 권한 컬렉션 (ID 기준) + */ + public function getByIds(array $ids): Collection + { + if (empty($ids)) { + return new Collection; + } + + return Permission::whereIn('id', $ids)->get(); + } + /** * 식별자로 권한을 찾습니다. * diff --git a/app/Repositories/ScheduleHistoryRepository.php b/app/Repositories/ScheduleHistoryRepository.php index 10cc4909..df7042f8 100644 --- a/app/Repositories/ScheduleHistoryRepository.php +++ b/app/Repositories/ScheduleHistoryRepository.php @@ -4,6 +4,7 @@ namespace App\Repositories; use App\Contracts\Repositories\ScheduleHistoryRepositoryInterface; use App\Models\ScheduleHistory; +use App\Repositories\Concerns\DeletesInBatches; use App\Repositories\Concerns\PaginatesWithDeferredJoin; use App\Repositories\Concerns\ResolvesSortSpec; use App\Support\Query\PaginationLimits; @@ -13,6 +14,7 @@ use Illuminate\Database\Eloquent\Collection; class ScheduleHistoryRepository implements ScheduleHistoryRepositoryInterface { + use DeletesInBatches; use PaginatesWithDeferredJoin; use ResolvesSortSpec; @@ -169,6 +171,10 @@ class ScheduleHistoryRepository implements ScheduleHistoryRepositoryInterface */ public function deleteOlderThan(int $days): int { - return ScheduleHistory::where('started_at', '<', now()->subDays($days))->delete(); + // 보존 기간 하한(1일)은 이 계층이 소유한다 — 파기는 되돌릴 수 없으므로 + // 호출자마다 다시 막지 않고 실제로 지우는 자리에서 한 번 막는다. + return $this->deleteInBatches( + ScheduleHistory::where('started_at', '<', now()->subDays(max(1, $days))) + ); } } diff --git a/app/Repositories/SeoCacheStatRepository.php b/app/Repositories/SeoCacheStatRepository.php index a6bb968e..d35f9e7d 100644 --- a/app/Repositories/SeoCacheStatRepository.php +++ b/app/Repositories/SeoCacheStatRepository.php @@ -4,6 +4,7 @@ namespace App\Repositories; use App\Contracts\Repositories\SeoCacheStatRepositoryInterface; use App\Models\SeoCacheStat; +use App\Repositories\Concerns\DeletesInBatches; use Carbon\Carbon; use Illuminate\Contracts\Database\Query\Expression; use Illuminate\Database\Eloquent\Builder; @@ -16,6 +17,8 @@ use Illuminate\Support\Facades\DB; */ class SeoCacheStatRepository implements SeoCacheStatRepositoryInterface { + use DeletesInBatches; + /** * 그룹 집계에 허용된 컬럼 목록 * @@ -96,7 +99,7 @@ class SeoCacheStatRepository implements SeoCacheStatRepositoryInterface */ public function deleteOlderThan(Carbon $cutoff): int { - return SeoCacheStat::where('created_at', '<', $cutoff)->delete(); + return $this->deleteInBatches(SeoCacheStat::where('created_at', '<', $cutoff)); } /** diff --git a/app/Rules/AllowedShellCommand.php b/app/Rules/AllowedShellCommand.php index 2a7723dd..f5cb09ae 100644 --- a/app/Rules/AllowedShellCommand.php +++ b/app/Rules/AllowedShellCommand.php @@ -34,8 +34,24 @@ class AllowedShellCommand implements ValidationRule return; } - if (! ScheduleCommandValidator::isShellCommandAllowed($value)) { - $fail(__('validation.schedule_command.shell_not_allowed')); + $verdict = ScheduleCommandValidator::inspectShellCommand($value); + + if ($verdict['allowed']) { + return; } + + // 인터프리터에 인라인 코드/명령을 넘기거나 안전하지 않은 스크립트 경로를 지정한 + // 경우는 전용 안내로 구분한다 — 화이트리스트 미등재와는 조치 방법이 다르다. + $interpreterReasons = [ + ScheduleCommandValidator::SHELL_REASON_INTERPRETER, + ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, + ScheduleCommandValidator::SHELL_REASON_SCRIPT_PATH, + ]; + + $key = in_array($verdict['reason'], $interpreterReasons, true) + ? 'validation.schedule_command.shell_interpreter_denied' + : 'validation.schedule_command.shell_not_allowed'; + + $fail(__($key)); } } diff --git a/app/Rules/NoExternalUrls.php b/app/Rules/NoExternalUrls.php index 9ec5bee6..9f3ea534 100644 --- a/app/Rules/NoExternalUrls.php +++ b/app/Rules/NoExternalUrls.php @@ -8,8 +8,14 @@ use Illuminate\Contracts\Validation\ValidationRule; /** * 레이아웃 JSON에서 외부 URL을 차단하는 Custom Rule * - * props와 actions 내의 http://, https://, data:, javascript: 등 - * 위험한 URI 스킴을 감지하여 차단합니다. + * 컴포넌트 props·actions 와 최상위 init_actions 내의 http://, https://, data:, + * javascript: 등 위험한 URI 스킴을 감지하여 차단합니다. + * + * 검사 대상 구분(신뢰 경계): init_actions 는 로드 시 자동 실행되는 액션이라 외부 + * navigate/apiCall URL 이 곧 자동 리다이렉트·데이터 유출 경로가 되므로 실행 지점에서 + * 차단합니다. 반면 state/computed 는 데이터 값이며, 실제 위험은 그 값이 바인딩되는 + * sink(컴포넌트 prop = img src 등)에서 발생하고 그 sink 는 이미 여기서 검사됩니다 — + * 예시/안내용 URL 을 담는 정당한 용례를 깨지 않기 위해 데이터 계층은 재차단하지 않습니다. */ class NoExternalUrls implements ValidationRule { @@ -31,6 +37,15 @@ class NoExternalUrls implements ValidationRule */ public function validate(string $attribute, mixed $value, Closure $fail): void { + // 문자열 스칼라 필드에도 부착되므로(`content.endpoint` · + // `content.data_sources.*.endpoint`) 그 값을 직접 검사한다. 배열만 처리하고 + // 반환하면 그 부착이 조용한 no-op 이 된다. + if (is_string($value)) { + $this->checkForDangerousUrl($value, $attribute, $fail); + + return; + } + if (! is_array($value)) { return; } @@ -39,6 +54,16 @@ class NoExternalUrls implements ValidationRule if (isset($value['components']) && is_array($value['components'])) { $this->validateComponents($value['components'], $fail); } + + // init_actions: 로드 시 자동 실행되는 액션 — 외부 navigate/apiCall URL 은 로드 시점 + // 자동 리다이렉트/데이터 유출 경로가 되므로 컴포넌트 actions 와 동일하게 검사한다. + if (isset($value['init_actions']) && is_array($value['init_actions'])) { + foreach ($value['init_actions'] as $i => $action) { + if (is_array($action)) { + $this->validateObject($action, "init_actions[$i]", $fail); + } + } + } } /** @@ -112,7 +137,11 @@ class NoExternalUrls implements ValidationRule } // 추가 패턴 검사: //로 시작 (프로토콜 상대 URL) - if (str_starts_with($lowerValue, '//')) { + // + // 브라우저 URL 파서는 파싱 전에 ASCII tab·개행을 제거하고 백슬래시를 슬래시와 + // 동등하게 처리하므로, `/\/evil.com` · `/{tab}/evil.com` 도 실제로는 외부 + // origin 이 된다. 접두 검사 전에 동일하게 정규화한다(SafeLayoutExpressions 와 동형). + if (str_starts_with(SafeLayoutExpressions::normalizeForOriginCheck($lowerValue), '//')) { $fail(__('validation.external_url.detected_in_props', ['url' => $value])); return; diff --git a/app/Rules/SafeLayoutExpressions.php b/app/Rules/SafeLayoutExpressions.php new file mode 100644 index 00000000..794045f4 --- /dev/null +++ b/app/Rules/SafeLayoutExpressions.php @@ -0,0 +1,238 @@ +`)는 정상 표현식에서 광범위하게 사용되므로 차단하지 않습니다 + * (클라이언트 평가기가 인터프리터로 안전하게 실행). + */ +class SafeLayoutExpressions implements ValidationRule +{ + /** + * 위험 표현식 토큰 패턴 (문자열 값 대상) + * + * @var array + */ + private const DANGEROUS_PATTERNS = [ + // 프로토타입/생성자 체인 접근 (dot) + '/\.\s*(constructor|__proto__|prototype)\b/i', + // 프로토타입/생성자 체인 접근 (문자열 리터럴 computed 키) — 중첩 배열 키 + // `[['constructor']]` 도 내부 `['constructor']` 가 매칭된다. + '/\[\s*[\'"](constructor|__proto__|prototype)[\'"]\s*\]/i', + // Object 리플렉션 static 호출 — 프로토타입/디스크립터를 읽어 Function 도달·프로토타입 + // 오염 경로. **호출 위치(뒤에 `(`)만** 매칭해 안내 문구의 단순 단어 언급은 오탐하지 않는다. + // 리플렉션 인자 `getOwnPropertyDescriptor(x, 'constructor')` 는 이 메서드명이 반드시 + // 동반되므로 여기서 잡히고, 금지 프로퍼티를 그냥 문자열로 비교하는 정상 표현식 + // (`{{ mode === 'prototype' }}` — 런타임 평가기가 허용)은 차단하지 않는다. + '/\b(getPrototypeOf|setPrototypeOf|getOwnPropertyDescriptors?|defineProperty|defineProperties)\s*\(/i', + // 함수 생성자 / eval 호출 + '/\bFunction\s*\(/', + '/\beval\s*\(/', + // 동적 import() — 원격 ES 모듈 로드/코드 실행 경로 (런타임 AST 평가기와 저장측 패리티) + '/\bimport\s*\(/', + // 원시 __proto__ 식별자 + '/\b__proto__\b/', + // legacy 접근자 4종 — 프로퍼티를 **문자열 인자**로 지목해 프로토타입을 읽고 쓴다. + // Object 리플렉션 static 과 같은 능력을 모든 객체가 상속으로 제공하므로 같은 강도로 + // 막는다. 배포 레이아웃 전수에서 사용 0건이라 정상 표현식 회귀가 없다. + '/\b__(lookup|define)(Getter|Setter)__\b/', + ]; + + // 주의: 문자열 조립 난독화(`['const' + 'ructor']`)는 정적 토큰 매칭으로 잡을 수 없다. + // 그 형태의 최종 방어는 런타임 화이트리스트 인터프리터(SafeExpressionEvaluator)가 + // 담당한다 — 키를 1회 정규화(String 강제변환)한 뒤 금지 프로퍼티를 차단하므로, + // 조립·배열·toString 강제변환 등 모든 우회 형태가 접근 시점에 거부된다. 이 저장측 + // 규칙은 리터럴·리플렉션 형태를 저장 단계에서 조기 차단하는 심층 방어 계층이다. + + /** + * 신뢰 외부 스크립트 호스트 캐시 (검증 1회당 집계 1회). + * + * @var array|null + */ + private ?array $trustedHosts = null; + + /** + * 검증 수행 + */ + public function validate(string $attribute, mixed $value, Closure $fail): void + { + if (! is_array($value)) { + return; + } + + $this->walk($value, $fail); + } + + /** + * 레이아웃 JSON 트리를 재귀 순회하며 문자열 값을 검사합니다. + * + * @param array $node 현재 노드 + * @param Closure $fail 검증 실패 콜백 + */ + private function walk(array $node, Closure $fail): void + { + foreach ($node as $key => $item) { + // 백틱 템플릿 리터럴을 포함한 위험 표현식 토큰 검사 + if (is_string($item)) { + $this->assertSafeExpression($item, $fail); + + continue; + } + + if (! is_array($item)) { + continue; + } + + // scripts[].src same-origin 검증 + if ($key === 'scripts') { + $this->assertSameOriginList($item, 'src', $fail); + } + + // data_sources[].endpoint same-origin 검증 + if ($key === 'data_sources') { + $this->assertSameOriginList($item, 'endpoint', $fail); + } + + $this->walk($item, $fail); + } + } + + /** + * 문자열 표현식에 위험 토큰이 포함되어 있으면 검증을 실패시킵니다. + * + * @param string $value 검사 대상 문자열 + * @param Closure $fail 검증 실패 콜백 + */ + private function assertSafeExpression(string $value, Closure $fail): void + { + // 백틱 템플릿 리터럴은 차단하지 않는다 — 레이아웃이 내비게이션 경로 조립 등에 + // 정상적으로 사용하며(예: `/mypage/${$args[0]}`), `${...}` 는 클라이언트 + // 평가기가 인터프리터로 안전하게 해석한다. constructor/Function 등 실제 위험 + // 토큰만 아래에서 거부한다. + foreach (self::DANGEROUS_PATTERNS as $pattern) { + if (preg_match($pattern, $value) === 1) { + $fail(__('validation.layout.dangerous_expression', ['snippet' => mb_substr($value, 0, 80)])); + + return; + } + } + } + + /** + * scripts/data_sources 배열의 지정 필드가 same-origin path-only 인지 검증합니다. + * + * @param array $list scripts 또는 data_sources 배열 + * @param string $field 검사할 필드명 (src | endpoint) + * @param Closure $fail 검증 실패 콜백 + */ + private function assertSameOriginList(array $list, string $field, Closure $fail): void + { + foreach ($list as $entry) { + if (! is_array($entry) || ! isset($entry[$field]) || ! is_string($entry[$field])) { + continue; + } + + $url = trim($entry[$field]); + + if ($url === '') { + continue; + } + + // 표현식 바인딩(`{{...}}`)은 런타임 해석 대상이라 여기서 판정하지 않는다. + if (str_starts_with($url, '{{')) { + continue; + } + + // same-origin path 이거나, 확장이 선언한 신뢰 호스트면 허용. 그 외 외부 origin 차단. + if (! $this->isSameOriginPath($url) + && ! TrustedScriptHosts::isTrustedUrl($url, $this->trustedHosts())) { + $fail(__('validation.layout.external_resource_url', ['url' => $url])); + + return; + } + } + } + + /** + * 신뢰 외부 스크립트 호스트 목록을 반환합니다 (검증 1회당 1회 집계 후 캐시). + * + * @return array 신뢰 호스트명 목록 + */ + private function trustedHosts(): array + { + return $this->trustedHosts ??= TrustedScriptHosts::hosts(); + } + + /** + * same-origin path-only URL 인지 판정합니다. + * + * 허용: `/` 로 시작하는 경로. 차단: `//`(protocol-relative), scheme 포함 절대 URL. + * + * @param string $url 검사 대상 URL + * @return bool same-origin path 이면 true + */ + private function isSameOriginPath(string $url): bool + { + $normalized = self::normalizeForOriginCheck($url); + + // protocol-relative (`//evil.com/...`) 차단 + if (str_starts_with($normalized, '//')) { + return false; + } + + // scheme 포함 절대 URL(`https://`, `javascript:`, `data:` 등) 차단 + if (preg_match('/^[a-z][a-z0-9+.\-]*:/i', $normalized) === 1) { + return false; + } + + // path-only: `/` 로 시작해야 same-origin 절대 경로 + return str_starts_with($normalized, '/'); + } + + /** + * origin 판정 전에 URL 을 브라우저 URL 파서와 동일하게 정규화합니다. + * + * 문자열 접두 검사만으로는 authority 우회를 막지 못합니다. 브라우저(WHATWG URL)는 + * 파싱 전에 ASCII tab·개행을 제거하고, special scheme(http/https)에서 백슬래시를 + * 슬래시와 동등하게 처리하기 때문입니다. 따라서 `/\/evil.com/x.js` · + * `/{tab}/evil.com/x.js` 는 `//` 로 시작하지 않는데도 실제로는 + * `https://evil.com/x.js` 로 해석됩니다. + * + * 정규화 후 판정하면 경로 중간의 백슬래시·탭(`/js/a\b.js`)은 authority 를 만들지 + * 않으므로 그대로 통과합니다(과차단 없음). + * + * 클라이언트(`TemplateApp.isAllowedScriptSrc`)·정적 검사 + * (`layout-scripts-src-same-origin`)와 3층 동형이어야 합니다. + * + * 구현 SSoT 는 `TrustedScriptHosts::normalizeForOriginCheck` 입니다 — 같은 저장측 + * 판정 안에서 same-origin 검사(이 규칙)와 신뢰 호스트 검사(`TrustedScriptHosts`)가 + * 이어 붙으므로, 두 검사가 서로 다른 정규화를 쓰면 한 URL 이 "path 도 아니고 + * 외부 호스트도 아닌" 상태로 빠져나간다. 위임으로 그 갈림을 구조적으로 막는다. + * + * @param string $url 원본 URL + * @return string 정규화된 URL + */ + public static function normalizeForOriginCheck(string $url): string + { + return TrustedScriptHosts::normalizeForOriginCheck($url); + } +} diff --git a/app/Search/Engines/DatabaseFulltextEngine.php b/app/Search/Engines/DatabaseFulltextEngine.php index 77f7da54..de02d2a5 100644 --- a/app/Search/Engines/DatabaseFulltextEngine.php +++ b/app/Search/Engines/DatabaseFulltextEngine.php @@ -6,15 +6,18 @@ use App\Enums\TotalRelation; use App\Search\Contracts\FulltextSearchable; use App\Search\Contracts\KeywordPredicateProvider; use App\Search\DTO\KeywordSearchContext; +use App\Search\FulltextIndexInspector; use App\Search\KeywordSearch; use App\Support\Query\BoundedPaginator; use App\Support\Query\PaginationLimits; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\LazyCollection; use Laravel\Scout\Builder; use Laravel\Scout\Engines\Engine; +use Throwable; /** * MySQL FULLTEXT + ngram 커스텀 Scout 엔진 @@ -31,6 +34,27 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider */ private static ?bool $isMariaDbCache = null; + /** + * FULLTEXT 인덱스 카탈로그 캐시 (프로세스 수명 동안 유지, lazy 적재) + * + * 프리픽스 제거·소문자 테이블명 => 인덱스별 컬럼 집합(소문자·정렬) 목록. + * null 은 미적재. 적재 실패(Throwable)는 캐시하지 않는다 — 일시 장애가 + * 워커 수명 내내 LIKE 강등으로 굳는 것을 막기 위함이다. + * + * FPM 워커의 static 캐시이므로 배포로 인덱스가 추가돼도 반영은 워커 재시작 + * (또는 `addFulltextIndex()` 경유 생성) 시점이다. + * + * @var array>>|null + */ + private static ?array $fulltextIndexCatalog = null; + + /** + * 인덱스 부재 폴백 경고를 조합당 한 번만 남기기 위한 기록 + * + * @var array + */ + private static array $warnedMissingIndexes = []; + /** * 모델을 검색 인덱스에 업데이트합니다. * @@ -293,7 +317,7 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider $version = DB::selectOne('SELECT VERSION() as version'); return static::$isMariaDbCache = $version && str_contains(strtolower($version->version), 'mariadb'); - } catch (\Throwable) { + } catch (Throwable) { return static::$isMariaDbCache = false; } } @@ -301,6 +325,184 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider return static::$isMariaDbCache = false; } + /** + * 대상 테이블·컬럼 조합을 커버하는 FULLTEXT 인덱스가 있는지 판정합니다. + * + * MySQL 의 `MATCH(a, b)` 는 어떤 FULLTEXT 인덱스의 컬럼 **집합과 정확히 일치** + * (순서 무관)해야 실행됩니다. 복합 (title,content) 인덱스는 `MATCH(title)` 단독을 + * 커버하지 못합니다. 판정은 한정자 제거·소문자·정렬 정규화 후 집합 동등 비교입니다. + * + * @param string $table 테이블명 (프리픽스 포함/미포함 모두 허용) + * @param array $columns MATCH 대상 컬럼명 + * @return bool 커버하는 인덱스가 있으면 true + */ + public static function fulltextIndexCoversColumns(string $table, array $columns): bool + { + if (! static::supportsFulltext()) { + // sqlite/pgsql 등에서는 INFORMATION_SCHEMA 접근 자체를 하지 않는다. + return false; + } + + $catalog = static::fulltextIndexCatalog(); + + if ($catalog === null) { + return false; + } + + $wanted = self::normalizeColumnSet($columns); + + if ($wanted === []) { + return false; + } + + foreach (self::tableLookupKeys($table) as $key) { + foreach ($catalog[$key] ?? [] as $indexed) { + if ($indexed === $wanted) { + return true; + } + } + } + + return false; + } + + /** + * 카탈로그를 lazy 적재해 반환합니다. + * + * @return array>>|null 적재 실패 시 null (미캐시) + */ + private static function fulltextIndexCatalog(): ?array + { + if (static::$fulltextIndexCatalog !== null) { + return static::$fulltextIndexCatalog; + } + + try { + $sets = (new FulltextIndexInspector)->indexedColumnSets(); + } catch (Throwable $e) { + // 실패는 캐시하지 않는다 — 다음 요청에서 다시 시도한다. + if (! isset(static::$warnedMissingIndexes['__catalog_load__'])) { + static::$warnedMissingIndexes['__catalog_load__'] = true; + Log::warning('FULLTEXT 인덱스 카탈로그 적재에 실패해 이번 요청은 부분일치(LIKE)로 검색합니다.', [ + 'exception' => $e, + ]); + } + + return null; + } + + $normalized = []; + foreach ($sets as $table => $columnSets) { + $normalized[strtolower((string) $table)] = array_map( + static fn (array $set): array => self::normalizeColumnSet($set), + $columnSets + ); + } + + return static::$fulltextIndexCatalog = $normalized; + } + + /** + * 컬럼 집합을 비교 가능한 형태로 정규화합니다 (한정자 제거·소문자·정렬·중복 제거). + * + * @param array $columns 컬럼명 목록 (`posts.title` 형태 허용) + * @return array 정규화된 컬럼 집합 + */ + private static function normalizeColumnSet(array $columns): array + { + $normalized = array_values(array_unique(array_map( + static function (string $column): string { + $pos = strrpos($column, '.'); + + return strtolower($pos === false ? $column : substr($column, $pos + 1)); + }, + $columns + ))); + + sort($normalized); + + return $normalized; + } + + /** + * 테이블명 조회 키 후보를 만듭니다 (프리픽스 유/무 양쪽). + * + * 카탈로그 키는 프리픽스 제거 형태다. 호출자가 프리픽스 포함 이름을 넘겨도 판정이 + * 되도록 제거 후보를 함께 만든다. 프리픽스가 빈 문자열인 설치에서도 동작한다. + * + * @param string $table 테이블명 + * @return array 소문자 조회 키 후보 + */ + private static function tableLookupKeys(string $table): array + { + $keys = [strtolower($table)]; + + $prefix = DB::getTablePrefix(); + if ($prefix !== '' && str_starts_with($table, $prefix)) { + $keys[] = strtolower(substr($table, strlen($prefix))); + } + + return array_values(array_unique($keys)); + } + + /** + * 카탈로그를 테스트용으로 시드합니다. + * + * @param array>> $catalog 테이블명 => 컬럼 집합 목록 + */ + public static function primeFulltextIndexCatalog(array $catalog): void + { + $normalized = []; + foreach ($catalog as $table => $columnSets) { + $normalized[strtolower((string) $table)] = array_map( + static fn (array $set): array => self::normalizeColumnSet($set), + $columnSets + ); + } + + static::$fulltextIndexCatalog = $normalized; + } + + /** + * 카탈로그 캐시와 경고 기록을 함께 초기화합니다 (인덱스 생성 직후·테스트). + */ + public static function forgetFulltextIndexCatalog(): void + { + static::$fulltextIndexCatalog = null; + static::$warnedMissingIndexes = []; + } + + /** + * 인덱스 부재 폴백 사실을 테이블+컬럼 조합당 한 번 기록합니다. + * + * 드라이버 미지원 폴백(정상 경로)과 달리, 드라이버는 지원하는데 인덱스가 없어 + * 내려가는 폴백은 설치 결함의 신호이므로 기록을 남긴다. 기록하지 않으면 + * "검색이 느리고 관련도가 없다" 는 증상만 남고 원인을 찾을 단서가 없다. + * + * @param string $table 테이블명 + * @param array $columns MATCH 대상 컬럼명 + */ + protected static function warnMissingFulltextIndexOnce(string $table, array $columns): void + { + $normalized = self::normalizeColumnSet($columns); + $key = strtolower($table).':'.implode(',', $normalized); + + if (isset(static::$warnedMissingIndexes[$key])) { + return; + } + + static::$warnedMissingIndexes[$key] = true; + + Log::warning('대상 컬럼 조합의 FULLTEXT 인덱스가 없어 부분일치(LIKE)로 검색합니다. 관련도 정렬이 적용되지 않고 전체 스캔이 발생합니다.', [ + 'table' => $table, + 'columns' => $normalized, + // search:index 는 **이미 존재하는** 인덱스만 열거해 상태를 보므로, 인덱스가 + // 아예 없는 이 상황은 그 목록에 나타나지도 재생성 대상이 되지도 않는다. + // 안내가 그 커맨드를 가리키면 운영자는 "이상 없음" 을 보고 추적이 끊긴다. + 'hint' => '해당 테이블의 FULLTEXT 인덱스를 만드는 마이그레이션이 적용되지 않았습니다. 그 확장의 마이그레이션을 다시 실행해 인덱스를 생성하세요.', + ]); + } + /** * FULLTEXT BOOLEAN MODE 검색어에서 연산자 문자를 제거하고 안전한 구문으로 변환합니다. * @@ -414,6 +616,19 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider return; } + $table = $query->getModel()->getTable(); + + if (! static::fulltextIndexCoversColumns($table, $columns)) { + // 드라이버는 지원하는데 이 컬럼 조합을 커버하는 인덱스가 없다 — MATCH 를 + // 조립하면 실행 시 1191(Can't find FULLTEXT index) 이 되고, 그 오류는 + // 소비처의 catch 에 삼켜져 "검색 결과 0건" 으로 위장된다. LIKE 로 내려가고 + // 설치 결함의 신호로 조합당 1회 기록한다. + static::warnMissingFulltextIndexOnce($table, $columns); + KeywordSearch::applyLikeMatch($query, $columns, $keyword, $boolean); + + return; + } + $ftKeyword = static::sanitizeBooleanModeKeyword($keyword); if ($ftKeyword === '') { @@ -478,6 +693,9 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider "ALTER TABLE `{$prefix}{$table}` " ."ADD FULLTEXT INDEX `{$indexName}` (`{$columnList}`){$parserClause}" ); + + // 방금 만든 인덱스가 이 프로세스의 커버 판정에 바로 반영되도록 카탈로그를 비운다. + static::forgetFulltextIndexCatalog(); } /** @@ -523,6 +741,21 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider $useFulltext = static::supportsFulltext(); + if ($useFulltext) { + // 이 경로는 컬럼마다 단일 MATCH(col) 를 조립하므로, 각 컬럼이 **개별** + // FULLTEXT 인덱스로 커버될 때만 MATCH 를 쓴다. 복합 인덱스만 있는 테이블은 + // 실행 시 1191 이 되므로 LIKE 분기로 내려간다. + $uncovered = array_values(array_filter( + $columns, + static fn (string $column): bool => ! static::fulltextIndexCoversColumns($model->getTable(), [$column]) + )); + + if ($uncovered !== []) { + static::warnMissingFulltextIndexOnce($model->getTable(), $uncovered); + $useFulltext = false; + } + } + // prefix 포함 테이블명 (selectRaw에서 사용) $prefix = DB::getTablePrefix(); $qualifiedTable = $prefix.$model->getTable(); @@ -555,12 +788,11 @@ class DatabaseFulltextEngine extends Engine implements KeywordPredicateProvider $scoreRaw = '('.implode(' + ', $scoreExpressions).') as _ft_score'; $this->applySelect($query, $model, $qualifiedTable, $scoreRaw, $scoreBindings, $keysOnly); } else { - // LIKE fallback (PostgreSQL, SQLite 등) - $query->where(function ($q) use ($columns, $keyword) { - foreach ($columns as $column) { - $q->orWhere($column, 'LIKE', "%{$keyword}%"); - } - }); + // LIKE fallback (FULLTEXT 미지원 드라이버 + 개별 인덱스 미커버) + // 폴백 술어 조립은 KeywordSearch 단일 지점에 맡긴다 — 와일드카드(% _ \) + // escape 와 드라이버별 대소문자 무시 연산자가 그쪽에만 있어, 여기서 손으로 + // 조립하면 같은 "부분일치" 인데 검색어에 % 가 섞였을 때 결과가 갈린다. + KeywordSearch::applyLikeMatch($query, $columns, $keyword); // 스코어 고정 0 (관련성 순위 불가) $this->applySelect($query, $model, $qualifiedTable, '0 as _ft_score', [], $keysOnly); diff --git a/app/Search/FulltextIndexInspector.php b/app/Search/FulltextIndexInspector.php index b62a48ad..c1ba571d 100644 --- a/app/Search/FulltextIndexInspector.php +++ b/app/Search/FulltextIndexInspector.php @@ -93,6 +93,48 @@ class FulltextIndexInspector return $definitions; } + /** + * 현재 스키마의 FULLTEXT 인덱스 컬럼 집합을 테이블별로 수집합니다. + * + * `discover()` 와 같은 INFORMATION_SCHEMA 조회를 쓰되, 인덱스마다 `SHOW CREATE TABLE` + * 을 도는 파서 해석은 하지 않는 경량판입니다 — 커버 판정에는 컬럼 집합만 필요합니다. + * + * 반환 키는 프리픽스를 제거한 테이블명입니다. `Model::getTable()` 이 프리픽스 미포함 + * 이름을 돌려주므로, 판정하는 쪽이 그 이름으로 바로 조회할 수 있게 맞춥니다. + * + * @return array>> 테이블명 => 인덱스별 컬럼 집합 목록 + */ + public function indexedColumnSets(): array + { + if (! DatabaseFulltextEngine::supportsFulltext()) { + return []; + } + + $rows = DB::select( + 'SELECT TABLE_NAME AS table_name, + GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS columns + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND INDEX_TYPE = ? + GROUP BY TABLE_NAME, INDEX_NAME', + ['FULLTEXT'] + ); + + $prefix = DB::getTablePrefix(); + $sets = []; + + foreach ($rows as $row) { + $table = (string) $row->table_name; + + if ($prefix !== '' && str_starts_with($table, $prefix)) { + $table = substr($table, strlen($prefix)); + } + + $sets[strtolower($table)][] = explode(',', (string) $row->columns); + } + + return $sets; + } + /** * 인덱스 하나의 건강도를 판정합니다. * diff --git a/app/Search/SearchCategoryPayload.php b/app/Search/SearchCategoryPayload.php index 2a72fc16..4a877025 100644 --- a/app/Search/SearchCategoryPayload.php +++ b/app/Search/SearchCategoryPayload.php @@ -80,6 +80,33 @@ final class SearchCategoryPayload ], $extra); } + /** + * 카테고리 검색이 예외로 실패했을 때의 페이로드를 만듭니다. + * + * 실패한 0건을 "정확한 0건" 으로 말하지 않는다 — `total_is_exact=false` 로 내보내 + * 배지가 정확한 값처럼 그려지는 것을 막고, `failed` 플래그로 화면이 "결과 없음" 과 + * 구분되는 오류 안내를 그릴 수 있게 한다. 키 집합은 다른 팩토리와 동일하게 유지해 + * 화면이 분기 없이 읽게 한다. + * + * @param array $extra 도메인 고유 필드 (available_boards 등) + * @return array 카테고리 페이로드 + */ + public static function failed(array $extra = []): array + { + return array_merge([ + 'failed' => true, + 'total' => 0, + 'total_relation' => TotalRelation::AtLeast->value, + 'total_is_exact' => false, + 'result_cap' => null, + 'last_page' => null, + 'has_more_pages' => false, + 'next_cursor' => null, + 'prev_cursor' => null, + 'items' => [], + ], $extra); + } + /** * 목록 없이 건수만 필요한 자리(비활성 탭 배지)의 페이로드를 만듭니다. * diff --git a/app/Search/SearchHighlighter.php b/app/Search/SearchHighlighter.php new file mode 100644 index 00000000..3a90b46b --- /dev/null +++ b/app/Search/SearchHighlighter.php @@ -0,0 +1,62 @@ + 로 감싼다. 이스케이프를 건너뛰면 제목·본문에 + * 삽입된 태그가 그대로 실행 계약으로 나간다(게시판/상품/페이지 검색 공통). + */ +class SearchHighlighter +{ + /** + * 원문을 HTML 이스케이프한 뒤 검색어만 로 감쌉니다. + * + * @param string|null $text 원문 텍스트(평문) + * @param string $keyword 검색어 + * @return string 이스케이프 완료된 안전한 HTML + */ + public static function highlight(?string $text, string $keyword): string + { + if ($text === null || $text === '') { + return ''; + } + + $safe = htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); + + if ($keyword === '') { + return $safe; + } + + // 키워드도 동일하게 이스케이프해, 이스케이프된 원문과 일관되게 매칭한다. + $safeKeyword = htmlspecialchars($keyword, ENT_QUOTES, 'UTF-8'); + $escapedKeyword = preg_quote($safeKeyword, '/'); + + $result = preg_replace('/('.$escapedKeyword.')/iu', '$1', $safe); + + // 유효하지 않은 UTF-8 등으로 preg_replace 가 null 을 반환하면 이스케이프본을 유지한다. + return $result ?? $safe; + } + + /** + * HTML 본문을 태그 없는 평문으로 변환합니다. + * + * 엔티티를 먼저 디코드한 뒤 태그를 제거해, 엔티티로 인코딩된 태그가 + * 평문화 단계에서 실제 태그로 부활하지 못하게 합니다. + * + * @param string|null $html 본문(HTML 또는 평문) + * @return string 태그가 제거된 평문 + */ + public static function toPlainText(?string $html): string + { + if ($html === null || $html === '') { + return ''; + } + + $decoded = html_entity_decode($html, ENT_QUOTES, 'UTF-8'); + + return trim((string) preg_replace('/\s+/', ' ', strip_tags($decoded))); + } +} diff --git a/app/Seo/SeoCacheStatsService.php b/app/Seo/SeoCacheStatsService.php index 027332d7..d2df1c19 100644 --- a/app/Seo/SeoCacheStatsService.php +++ b/app/Seo/SeoCacheStatsService.php @@ -127,6 +127,10 @@ class SeoCacheStatsService */ public function cleanup(int $daysToKeep = 30): int { + // 보존 기간 하한(1일)은 이 계층이 소유한다 — 파기는 되돌릴 수 없으므로 + // 호출자마다 다시 막지 않고 실제로 지우는 경로에서 한 번 막는다. + $daysToKeep = max(1, $daysToKeep); + $deleted = $this->statRepository->deleteOlderThan(Carbon::now()->subDays($daysToKeep)); Log::info('[SEO] 캐시 통계 정리 완료', [ diff --git a/app/Seo/SeoMetaResolver.php b/app/Seo/SeoMetaResolver.php index 8cb12d66..678ccbc3 100644 --- a/app/Seo/SeoMetaResolver.php +++ b/app/Seo/SeoMetaResolver.php @@ -253,6 +253,7 @@ class SeoMetaResolver return $this->resolveLocalizedValue($value); } } + return ''; } @@ -428,6 +429,37 @@ class SeoMetaResolver return [$contextIdentifier, $rest]; } + /** + * `` 조립용 접미사를 정규화합니다. + * + * 관리자 저장 경로의 TrimStrings 미들웨어가 접미사의 선행 공백을 제거하므로 + * ("` | 그누보드7`" 을 입력해도 "`| 그누보드7`" 로 저장된다), blade 의 + * `{{ $title }}{{ $titleSuffix }}` 단순 연결에서 제목과 구분자가 붙어 버린다. + * 이 메서드가 조립 규칙의 SSoT 다: + * + * - 제목이 있고 접미사가 공백으로 시작하지 않으면 공백 하나를 복원해 잇는다. + * - 제목이 비어 있으면 매달린 선행 구분자(`|`,`-`,`·`,`:`,`/`,`–`,`—`)를 떼어 + * 사이트명 부분만 남긴다 (홈처럼 페이지 제목이 없는 화면의 "`| 사이트명`" 방지). + * + * @param string $title 최종 페이지 제목 (filter 훅 적용 후) + * @param string $suffix 코어 설정의 타이틀 접미사 + * @return string 조립에 사용할 접미사 (제목이 없으면 접미사가 곧 전체 제목) + */ + public static function composeTitleSuffix(string $title, string $suffix): string + { + if (trim($suffix) === '') { + return ''; + } + + if ($title === '') { + $stripped = (string) preg_replace('/^[\s|·:\/\x{2013}\x{2014}-]+/u', '', $suffix); + + return trim($stripped); + } + + return preg_match('/^\s/u', $suffix) === 1 ? $suffix : ' '.$suffix; + } + /** * 레이아웃 meta의 title을 해석합니다 (fallback용). * @@ -691,7 +723,15 @@ class SeoMetaResolver $resolved = array_merge(['@context' => 'https://schema.org'], $structuredData); - return json_encode($resolved, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT); + // JSON-LD 는 `<script type="application/ld+json">` 안에 임베드된다. 사용자 제어 값 + // (검색어·상품명 등)에 포함된 `</script>` 가 스크립트 컨텍스트를 조기 종료하면 그 + // 뒤의 페이로드가 실행 가능한 script 요소로 재생성된다(반사형 XSS — 봇 렌더는 + // _escaped_fragment_ 로 일반 UA 도 강제됨). JSON_HEX_TAG 로 `<`·`>` 를 <·> + // 로 이스케이프해 브레이크아웃을 차단한다. JSON_HEX_AMP 는 방어 심화. + return json_encode( + $resolved, + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_HEX_TAG | JSON_HEX_AMP, + ); } /** @@ -790,9 +830,9 @@ class SeoMetaResolver * 구조화 데이터 하위 객체가 실질적으로 비어있는지 확인합니다. * * @type 키가 있는 객체에서, @type 외 필드 중 하나라도 빈 문자열이면 - * 해당 객체를 JSON-LD에서 제거합니다. - * 예: aggregateRating의 ratingValue=""이면, bestRating="5"가 있더라도 제거됩니다. - * Google 구조화 데이터 검증에서 필수 필드가 빈 값이면 에러로 처리되기 때문입니다. + * 해당 객체를 JSON-LD에서 제거합니다. + * 예: aggregateRating의 ratingValue=""이면, bestRating="5"가 있더라도 제거됩니다. + * Google 구조화 데이터 검증에서 필수 필드가 빈 값이면 에러로 처리되기 때문입니다. * * @param array $resolved 평가된 구조화 데이터 객체 * @return bool 비어있으면 true diff --git a/app/Seo/SeoRenderer.php b/app/Seo/SeoRenderer.php index cf6581b9..22c17fd3 100644 --- a/app/Seo/SeoRenderer.php +++ b/app/Seo/SeoRenderer.php @@ -491,7 +491,9 @@ class SeoRenderer implements SeoRendererInterface $viewData = [ 'locale' => $locale, 'title' => $meta['title'], - 'titleSuffix' => $meta['titleSuffix'], + // filter 훅이 title 을 바꿨을 수 있으므로 최종 title 기준으로 접미사를 정규화한다 + // (TrimStrings 로 선행 공백이 제거된 접미사 복원 + 빈 제목의 매달린 구분자 제거) + 'titleSuffix' => SeoMetaResolver::composeTitleSuffix((string) ($meta['title'] ?? ''), (string) ($meta['titleSuffix'] ?? '')), 'description' => $meta['description'], 'keywords' => $meta['keywords'], 'canonicalUrl' => $canonicalUrl, diff --git a/app/Services/ActivityLogService.php b/app/Services/ActivityLogService.php index 9516aeed..b03b3c8e 100644 --- a/app/Services/ActivityLogService.php +++ b/app/Services/ActivityLogService.php @@ -83,4 +83,26 @@ class ActivityLogService return $count; } + + /** + * 보존 기간이 지난 활동 로그를 정리합니다 (자동 파기). + * + * 운영자가 고른 ID 를 지우는 deleteMany 와는 별개의 훅을 발행한다. 자동 파기는 + * 대상을 ID 로 지목하지 않고(기간으로 정한다), 사람이 없는 예약 실행이라 + * 본인인증 같은 대화형 가드를 태울 수 없기 때문이다. 확장은 이 훅으로 + * 외부 보관 등 자기 처리를 붙인다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function prune(int $days): int + { + HookManager::doAction('core.activity_log.before_prune', $days); + + $count = $this->repository->deleteOlderThan($days); + + HookManager::doAction('core.activity_log.after_prune', $days, $count); + + return $count; + } } diff --git a/app/Services/AttachmentService.php b/app/Services/AttachmentService.php index 540a4c46..b0f48620 100644 --- a/app/Services/AttachmentService.php +++ b/app/Services/AttachmentService.php @@ -6,6 +6,7 @@ use App\Contracts\Extension\StorageInterface; use App\Contracts\Repositories\AttachmentRepositoryInterface; use App\Enums\AttachmentSourceType; use App\Extension\HookManager; +use App\Helpers\PermissionHelper; use App\Models\Attachment; use App\Models\User; use App\Support\ImageResizer; @@ -13,6 +14,7 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\UploadedFile; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -163,14 +165,93 @@ class AttachmentService } /** - * 첨부파일 삭제 + * 소유자 없이 방치된 고아 첨부를 정리합니다. + * + * 폼 저장 전에 즉시 업로드되는 첨부(FileUploader 등)는 소유자 없이 먼저 만들어지므로, + * 폼을 저장하지 않고 이탈하면 파일과 기록이 그대로 남습니다. + * + * 오탐을 막기 위해 아래 조건을 모두 만족하는 행만 대상으로 삼습니다. + * + * 1. 소유자(attachmentable_type/id)가 없다 + * 2. 업로드 후 보존기간이 지났다 (폼 작성 중 유예) + * 3. 확장 소유가 아니다 (source_identifier 없음 — 확장 라이프사이클 소관) + * 4. 현역으로 쓰이는 첨부가 아니다 (현재 사이트 로고 등) + * + * 4번 보호 조건은 이 메서드가 직접 해석합니다. 호출자가 넘기는 값으로 두면 그 인자를 + * 빠뜨린 호출 하나가 운영 중인 로고를 파기하는데, 나머지 세 조건은 조회의 불변식이라 + * 보호 조건만 규약으로 남는 비대칭이 생깁니다. + * + * 삭제는 단건 삭제 경로(delete)를 그대로 재사용하므로 파일 삭제·훅·로그가 동일합니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 처리할 최대 건수 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + public function pruneOrphans(int $days, int $limit, bool $dryRun = false): array + { + $threshold = now()->subDays($days); + $candidates = $this->repository->findOrphanCandidates( + $threshold, + $limit, + $this->protectedAttachmentIds(), + ); + + $result = ['scanned' => $candidates->count(), 'deleted' => 0, 'failed' => 0]; + + if ($dryRun) { + return $result; + } + + foreach ($candidates as $candidate) { + $this->delete($candidate->id) ? $result['deleted']++ : $result['failed']++; + } + + return $result; + } + + /** + * 고아 정리에서 보호할 첨부 ID 목록을 반환합니다. + * + * 사이트 로고는 소유자(attachmentable) 없이 설정값이 직접 참조하는 첨부라 판정식만으로는 + * 고아와 구분되지 않습니다. 현역으로 설정에 실린 ID 를 명시적으로 제외합니다. + * + * @return array<int, int> 보호할 첨부 ID 목록 + */ + private function protectedAttachmentIds(): array + { + $siteLogo = g7_core_settings('general.site_logo', []); + + if (! is_array($siteLogo)) { + return []; + } + + $ids = []; + + foreach ($siteLogo as $item) { + $id = is_array($item) ? ($item['id'] ?? null) : $item; + + if (is_numeric($id)) { + $ids[] = (int) $id; + } + } + + return array_values(array_unique($ids)); + } + + /** + * 첨부파일 삭제 (파일 + 기록 영구 삭제) + * + * 대상 조회는 소프트삭제된 행까지 포함한다. 이 메서드가 수행하는 것은 forceDelete 이므로 + * 이미 소프트삭제된 행도 정당한 대상이고, 기본 조회로 찾으면 고아 정리가 그 행을 영원히 + * 회수하지 못한 채 실패만 반복한다. * * @param int $id 첨부파일 ID * @return bool 삭제 성공 여부 */ public function delete(int $id): bool { - $attachment = $this->repository->findById($id); + $attachment = $this->repository->findByIdWithTrashed($id); if (! $attachment) { return false; @@ -181,23 +262,40 @@ class AttachmentService $attachmentableId = $attachment->attachmentable_id; $collection = $attachment->collection; - // Before 훅 - HookManager::doAction('core.attachment.before_delete', $attachment); + // DB 를 먼저 지우고 파일은 커밋 후에 지운다. + // 순서를 반대로 두면 DB 삭제가 실패했을 때 파일 없는 행이 남아 다운로드가 404 가 된다. + // 반대로 이 순서에서는 최악이어도 참조되지 않는 고아 파일만 남고 데이터는 온전하다. + $result = DB::transaction(function () use ($id, $attachment, $attachmentableType, $attachmentableId, $collection) { + // Before 훅 + HookManager::doAction('core.attachment.before_delete', $attachment); - // 스토리지에서 파일 삭제 - $this->storage->withDisk($attachment->disk)->delete('', $attachment->path); + // DB에서 영구 삭제 + $deleted = $this->repository->forceDelete($id); - // DB에서 영구 삭제 - $result = $this->repository->forceDelete($id); + // 삭제 후 남은 파일들의 순서 재정렬 + if ($deleted && $attachmentableType && $attachmentableId) { + $this->repository->reorderAfterDelete($attachmentableType, $attachmentableId, $collection); + } - Log::info('첨부파일 삭제 완료', [ - 'attachment_id' => $id, - 'hash' => $attachment->hash, - ]); + return $deleted; + }); - // 삭제 후 남은 파일들의 순서 재정렬 - if ($result && $attachmentableType && $attachmentableId) { - $this->repository->reorderAfterDelete($attachmentableType, $attachmentableId, $collection); + if ($result) { + // 커밋 후 스토리지 파일 삭제 (실패해도 DB 는 되돌리지 않는다) + try { + $this->storage->withDisk($attachment->disk)->delete('', $attachment->path); + } catch (\Throwable $e) { + Log::warning('첨부파일 스토리지 삭제 실패 (고아 파일 잔존)', [ + 'attachment_id' => $id, + 'path' => $attachment->path, + 'error' => $e->getMessage(), + ]); + } + + Log::info('첨부파일 삭제 완료', [ + 'attachment_id' => $id, + 'hash' => $attachment->hash, + ]); } // After 훅 @@ -213,6 +311,8 @@ class AttachmentService */ public function reorder(array $orderData): void { + $this->assertReorderWithinScope($orderData); + // Before 훅 HookManager::doAction('core.attachment.before_reorder', $orderData); @@ -222,6 +322,44 @@ class AttachmentService HookManager::doAction('core.attachment.after_reorder', $orderData); } + /** + * 순서 변경 대상이 액터의 스코프 안에 있는지 검사합니다. + * + * `PATCH admin/attachments/reorder` 는 라우트 모델이 없는 정적 경로다. PermissionMiddleware + * 는 `$request->route('attachment')` 가 Model 일 때만 스코프를 검사하고 없으면 목록 + * 엔드포인트로 보아 건너뛰므로(`PermissionMiddleware`), 상세 경로(`DELETE {attachment}`)가 + * 미들웨어로 강제하는 스코프 축이 이 경로에서만 비어 있었다. 배포 기본 역할 `manager` 가 + * `core.attachments.update` 를 `self` 스코프로 보유하므로 이론 구성이 아니라 기본값에서 + * 성립한다 — 타인 소유 첨부의 순서를 바꿀 수 있었다. + * + * 대상 일부만 걸러내지 않고 **전체를 거부**한다. 순서는 집합 전체에 대한 하나의 배열이라 + * 일부만 반영하면 나머지와 어긋난 순서가 저장되기 때문이다(사용자 일괄 상태변경이 + * "제외" 를 택한 것과 의미론이 다르다). + * + * @param array<int, array{id: int, order: int}> $orderData 순서 데이터 + * + * @throws AuthorizationException 스코프 밖 첨부가 하나라도 포함된 경우 + */ + private function assertReorderWithinScope(array $orderData): void + { + $ids = array_values(array_unique(array_filter( + array_map(static fn ($item): int => (int) ($item['id'] ?? 0), $orderData) + ))); + + if (empty($ids)) { + return; + } + + $attachments = $this->repository->findByIds($ids); + + // 판정은 상세 경로와 같은 SSoT 에 위임한다 — 여기서 재구현하면 두 경로의 강도가 갈린다. + $permitted = PermissionHelper::filterByScope($attachments, 'core.attachments.update'); + + if (count($permitted) !== $attachments->count()) { + throw new AuthorizationException(__('auth.scope_denied')); + } + } + /** * 다운로드 응답 생성 * @@ -270,12 +408,15 @@ class AttachmentService /** * 이미지 파일 정보 조회 (캐싱 응답용) * - * 권한 체크 후 파일 경로와 메타 정보를 반환합니다. - * 컨트롤러에서 fileResponse()로 캐싱 헤더와 함께 응답할 수 있습니다. + * 권한 체크 후 인라인 스트림 응답과 캐싱용 메타를 반환합니다. + * 로컬 절대 경로 조립(getBasePath) 방식은 S3 등 원격 디스크 행에서 성립하지 + * 않으므로(#99 — filemtime stat 실패 500), 행 disk 를 그대로 따르는 + * StorageInterface::response() 스트림으로 서빙합니다. 컨트롤러는 + * streamedFileResponse() 로 캐싱 헤더(ETag/304)를 입혀 응답합니다. * * @param string $hash 첨부파일 해시 * @param User|null $user 요청한 사용자 (null이면 비로그인) - * @return array{path: string, mime_type: string, filename: string}|null 파일 정보 또는 null + * @return array{response: StreamedResponse, etag_source: string, mime_type: string, filename: string}|null 서빙 정보 또는 null * * @throws AuthorizationException 기능 레벨 권한이 없는 경우 */ @@ -293,9 +434,19 @@ class AttachmentService // 필터 훅 - 다운로드 전 처리 $attachment = HookManager::applyFilters('core.attachment.before_download', $attachment, $user); - // 파일 존재 확인 - $diskStorage = $this->storage->withDisk($attachment->disk); - if (! $diskStorage->exists('', $attachment->path)) { + // 행 disk 기준 인라인 스트림 (존재 검사는 response() 내부에서 수행) + // Content-Type/Length 를 행 메타로 선지정해 원격 디스크의 추가 메타 조회를 생략한다. + $response = $this->storage->withDisk($attachment->disk)->response( + '', + $attachment->path, + $attachment->original_filename, + [ + 'Content-Type' => $attachment->mime_type, + 'Content-Length' => (string) $attachment->size, + ] + ); + + if (! $response) { Log::error('첨부파일 스토리지에 없음', [ 'attachment_id' => $attachment->id, 'path' => $attachment->path, @@ -305,7 +456,14 @@ class AttachmentService } return [ - 'path' => $diskStorage->getBasePath('').DIRECTORY_SEPARATOR.$attachment->path, + 'response' => $response, + // 파일 stat 없이 결정적인 ETag 소스 (업로드 파일은 경로당 불변) + 'etag_source' => implode('|', [ + $attachment->disk, + $attachment->path, + (string) $attachment->updated_at?->getTimestamp(), + (string) $attachment->size, + ]), 'mime_type' => $attachment->mime_type, 'filename' => $attachment->original_filename, ]; @@ -358,18 +516,29 @@ class AttachmentService // Before 훅 HookManager::doAction('core.attachment.before_bulk_delete', $identifier); - // 해당 첨부파일들의 스토리지 파일 삭제 + // 삭제 대상 스냅샷 + 파일 경로를 먼저 수집한다 (DB 삭제 후에는 조회할 수 없다). $attachments = $this->repository->getBySourceIdentifier($identifier); $attachmentIds = $attachments->pluck('id')->toArray(); $snapshots = $attachments->keyBy('id')->map(fn ($a) => $a->toArray())->toArray(); + $files = $attachments->map(fn ($a) => ['disk' => $a->disk, 'path' => $a->path])->all(); - foreach ($attachments as $attachment) { - $this->storage->withDisk($attachment->disk)->delete('', $attachment->path); + // DB 일괄 삭제를 먼저 커밋한다 — 루프 중간에 실패하면 파일만 일부 사라지고 + // DB 행은 전부 남아 다운로드가 404 가 되기 때문이다. + $count = DB::transaction(fn () => $this->repository->deleteBySourceIdentifier($identifier)); + + // 커밋 후 파일 정리 (실패해도 DB 는 되돌리지 않는다 — 고아 파일은 비파괴) + foreach ($files as $file) { + try { + $this->storage->withDisk($file['disk'])->delete('', $file['path']); + } catch (\Throwable $e) { + Log::warning('첨부파일 스토리지 일괄 삭제 실패 (고아 파일 잔존)', [ + 'source_identifier' => $identifier, + 'path' => $file['path'], + 'error' => $e->getMessage(), + ]); + } } - // DB에서 삭제 - $count = $this->repository->deleteBySourceIdentifier($identifier); - Log::info('소스 식별자 기준 첨부파일 일괄 삭제', [ 'source_identifier' => $identifier, 'deleted_count' => $count, diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php index 1987d3a9..7dd137dc 100644 --- a/app/Services/AuthService.php +++ b/app/Services/AuthService.php @@ -15,6 +15,7 @@ use App\Extension\HookManager; use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -347,19 +348,27 @@ class AuthService 'status' => $status, ]; - $user = $this->userRepository->create($userData); + // 계정 생성 이후 단계에서 실패하면 이메일만 점유한 유령 계정이 남아 + // 같은 이메일로 재가입조차 불가능해진다. 생성~토큰 발급을 하나로 묶는다. + [$user, $token] = DB::transaction(function () use ($userData, $data, $now) { + $user = $this->userRepository->create($userData); - // 약관 동의 이력 기록 - $this->recordConsents($user, $data, $now); + // 약관 동의 이력 기록 + $this->recordConsents($user, $data, $now); - // 'user' 역할 자동 할당 (UserService 패턴과 동일) - $userRole = $this->roleRepository->findByIdentifier('user'); - if ($userRole) { - $user->roles()->sync([$userRole->id]); - $user->flushPermissionCaches(); - } + // 'user' 역할 자동 할당 (UserService 패턴과 동일) + $userRole = $this->roleRepository->findByIdentifier('user'); + if ($userRole) { + $user->roles()->sync([$userRole->id]); + } - $token = $user->createToken('auth-token', ['*'], $this->getTokenExpiresAt())->plainTextToken; + $token = $user->createToken('auth-token', ['*'], $this->getTokenExpiresAt())->plainTextToken; + + return [$user, $token]; + }); + + // 커밋 후 부수효과 (캐시는 DB 트랜잭션의 롤백 대상이 아니다) + $user->flushPermissionCaches(); // Hook 발생 (회원가입 완료) — 알림 발송은 NotificationHookListener, // signup_after_create 정책이 enabled 면 InitiateIdentityChallengeAfterRegister 가 challenge 발행. @@ -612,16 +621,20 @@ class AuthService ]); } - // Hook 발생 (비밀번호 재설정 시작) - HookManager::doAction('core.auth.before_reset_password', $user); + // 비밀번호를 바꾼 뒤 토큰 삭제가 실패하면 그 재설정 토큰이 계속 유효한 채로 + // 남아 재사용된다(보안). 두 단계를 하나로 묶는다. + DB::transaction(function () use ($user, $password, $record) { + // Hook 발생 (비밀번호 재설정 시작) + HookManager::doAction('core.auth.before_reset_password', $user); - // 비밀번호 업데이트 - $this->userRepository->update($user, [ - 'password' => Hash::make($password), - ]); + // 비밀번호 업데이트 + $this->userRepository->update($user, [ + 'password' => Hash::make($password), + ]); - // 사용된 토큰 삭제 - $record->delete(); + // 사용된 토큰 삭제 + $record->delete(); + }); // Hook 발생 (비밀번호 변경 완료) — 알림 발송은 NotificationHookListener가 처리 HookManager::doAction('core.auth.after_password_changed', $user); diff --git a/app/Services/DriverConnectionTester.php b/app/Services/DriverConnectionTester.php index 121ab437..f69fc939 100644 --- a/app/Services/DriverConnectionTester.php +++ b/app/Services/DriverConnectionTester.php @@ -8,6 +8,7 @@ use Aws\S3\S3Client; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter; use Predis\Client; /** @@ -101,9 +102,19 @@ class DriverConnectionTester ]; } + // Flysystem S3 어댑터 존재 선검사 — 테스트는 SDK 직접 경로지만 실제 파일 I/O 는 + // Storage::disk('s3') → 어댑터를 경유하므로, 어댑터가 없으면 테스트가 성공해도 + // 실경로가 즉사한다 (#99 의 결함 은폐 구조). sdk_missing 검사와 대칭. + if (! class_exists(AwsS3V3Adapter::class)) { + return [ + 'success' => false, + 'message' => __('settings.s3_adapter_missing'), + ]; + } + $startTime = microtime(true); - $client = new S3Client([ + $clientOptions = [ 'version' => 'latest', 'region' => $region, 'credentials' => [ @@ -114,7 +125,18 @@ class DriverConnectionTester 'timeout' => 5, 'connect_timeout' => 3, ], - ]); + ]; + + // S3 호환 스토리지(R2/MinIO 등) — 실경로(applyS3Config 주입)와 동일하게 + // endpoint/path-style 을 반영해야 테스트가 실제 연결 대상과 일치한다. + if (! empty($config['s3_endpoint'])) { + $clientOptions['endpoint'] = $config['s3_endpoint']; + } + if (! empty($config['s3_use_path_style'])) { + $clientOptions['use_path_style_endpoint'] = true; + } + + $client = new S3Client($clientOptions); // 버킷 존재 확인 $client->headBucket(['Bucket' => $bucket]); @@ -372,15 +394,53 @@ class DriverConnectionTester * @return array 테스트 결과 */ public function testWebsocket(array $config): array + { + $clientHost = $config['websocket_host'] ?? 'localhost'; + $clientPort = (int) ($config['websocket_port'] ?? 8080); + $clientScheme = $config['websocket_scheme'] ?? 'https'; + + // 서버(백엔드 broadcast HTTP API) endpoint — 빈 값은 클라이언트 값으로 폴백한다 + // (SettingsServiceProvider::applyWebsocketConfig 와 동일 규칙). 백엔드 송신은 + // server endpoint 를 쓰므로 client 만 검사하면 "테스트 성공 + 실제 발송 실패" 가 된다. + $serverHost = ($config['websocket_server_host'] ?? '') !== '' ? $config['websocket_server_host'] : $clientHost; + $serverPort = (int) ($config['websocket_server_port'] ?? 0) > 0 ? (int) $config['websocket_server_port'] : $clientPort; + $serverScheme = ($config['websocket_server_scheme'] ?? '') !== '' ? $config['websocket_server_scheme'] : $clientScheme; + + $clientUrl = sprintf('%s://%s:%d', $clientScheme, $clientHost, $clientPort); + $serverUrl = sprintf('%s://%s:%d', $serverScheme, $serverHost, $serverPort); + + // 클라이언트 endpoint 검사 + $clientResult = $this->probeWebsocketEndpoint($clientUrl); + + if (! $clientResult['success']) { + return $clientResult; + } + + // 서버 endpoint 검사 (클라이언트와 동일 주소면 중복 요청 생략) + if ($serverUrl !== $clientUrl) { + $serverResult = $this->probeWebsocketEndpoint($serverUrl); + + if (! $serverResult['success']) { + return [ + 'success' => false, + 'message' => __('settings.websocket_server_test_failed'), + 'error' => $serverResult['error'] ?? ($serverResult['message'] ?? ''), + ]; + } + } + + return $clientResult; + } + + /** + * Websocket(Reverb) HTTP endpoint 하나를 probe 합니다. + * + * @param string $url 검사할 endpoint URL + * @return array 테스트 결과 + */ + private function probeWebsocketEndpoint(string $url): array { try { - $host = $config['websocket_host'] ?? 'localhost'; - $port = (int) ($config['websocket_port'] ?? 8080); - $scheme = $config['websocket_scheme'] ?? 'https'; - - // Reverb 서버 상태 확인 (기본 HTTP 엔드포인트) - $url = sprintf('%s://%s:%d', $scheme, $host, $port); - // Reverb 는 통상 localhost·사내 IP 에서 동작하므로 사설 주소 자체는 정상 구성이다. // 다만 이 진단 요청을 임의 목적지 탐색에 전용하지 못하도록, 정상 설정에서는 나올 수 // 없는 값(userinfo 위장, http/https 이외 scheme, 제어문자 주입)은 거부한다. @@ -426,6 +486,7 @@ class DriverConnectionTester ]; } catch (\Exception $e) { Log::warning('Websocket connection test failed', [ + 'url' => $url, 'error' => $e->getMessage(), ]); diff --git a/app/Services/DriverRegistryService.php b/app/Services/DriverRegistryService.php index 36cc9a11..eae52f40 100644 --- a/app/Services/DriverRegistryService.php +++ b/app/Services/DriverRegistryService.php @@ -5,7 +5,8 @@ namespace App\Services; use App\Extension\HookManager; use App\Repositories\JsonConfigRepository; use Illuminate\Support\Facades\Lang; -use Illuminate\Support\Facades\Log; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter; +use Predis\Client; /** * 코어 드라이버 레지스트리 서비스 @@ -26,12 +27,16 @@ class DriverRegistryService */ private const CORE_DRIVER_IDS = [ 'storage' => ['local', 's3'], + 'public_asset' => ['none', 'public', 's3'], 'cache' => ['file', 'redis'], 'session' => ['file', 'database', 'redis'], 'queue' => ['sync', 'database', 'redis'], 'log' => ['single', 'daily'], 'websocket' => ['reverb'], 'mail' => ['smtp', 'mailgun', 'ses'], + // 검색엔진도 저장 가능한 드라이버 선택이다. 레지스트리에 등재해야 폴백 가드 + // (플러그인 제거 후 죽은 값 → 기본 드라이버)가 이 카테고리에도 적용된다. + 'search' => ['mysql-fulltext'], ]; /** @@ -41,12 +46,14 @@ class DriverRegistryService */ private const DEFAULT_DRIVERS = [ 'storage' => 'local', + 'public_asset' => 'none', 'cache' => 'file', 'session' => 'database', 'queue' => 'database', 'log' => 'daily', 'websocket' => '', 'mail' => 'smtp', + 'search' => 'mysql-fulltext', ]; /** @@ -56,12 +63,18 @@ class DriverRegistryService */ public const CONFIG_KEYS = [ 'storage' => 'filesystems.default', + 'public_asset' => 'core.storage.public_asset_disk', 'cache' => 'cache.default', 'session' => 'session.driver', 'queue' => 'queue.default', - 'log' => 'logging.default', + // 실제 적용 키 — SettingsServiceProvider::applyLogConfig() 가 기록하는 키와 동일해야 + // 플러그인 로그 드라이버 폴백이 실효한다 (logging.default 는 어디에서도 읽지 않는 죽은 키였다). + // 이 키의 값은 채널 "배열" 이다 — getConfigValueForDriver() 로 형태를 맞춘다. + 'log' => 'logging.channels.stack.channels', 'websocket' => 'broadcasting.default', 'mail' => 'mail.default', + // SettingsServiceProvider::applyDriverConfig() 가 기록하는 키와 동일해야 폴백이 실효한다 + 'search' => 'scout.driver', ]; /** @@ -71,12 +84,16 @@ class DriverRegistryService */ private const SETTINGS_KEYS = [ 'storage' => ['category' => 'drivers', 'key' => 'storage_driver'], + 'public_asset' => ['category' => 'drivers', 'key' => 'public_asset_disk'], 'cache' => ['category' => 'drivers', 'key' => 'cache_driver'], 'session' => ['category' => 'drivers', 'key' => 'session_driver'], 'queue' => ['category' => 'drivers', 'key' => 'queue_driver'], 'log' => ['category' => 'drivers', 'key' => 'log_driver'], - 'websocket' => ['category' => 'drivers', 'key' => 'websocket_driver'], + // websocket 은 드라이버 ID 선택 설정이 없다 (불리언 websocket_enabled 뿐) — + // 종전의 'websocket_driver' 는 어떤 저장 경로에도 없는 유령 키라 항상 skip 이었다. + // 카테고리 제외로 같은 동작을 명시화한다 (getSettingsKey('websocket') === null). 'mail' => ['category' => 'mail', 'key' => 'mailer'], + 'search' => ['category' => 'drivers', 'key' => 'search_engine_driver'], ]; /** @@ -103,11 +120,53 @@ class DriverRegistryService { $coreDrivers = $this->buildCoreDrivers($category); + // 검색엔진은 Scout 엔진 등록 훅이 SSoT 다 — 플러그인에 제2의 등록 훅을 요구하지 않고 + // 그 훅을 함께 읽는다. 일반 드라이버 훅도 그대로 유지해 양쪽 등록 방식을 모두 인식한다. + if ($category === 'search') { + $coreDrivers = $this->mergeSearchEngineDrivers($coreDrivers); + } + $hookName = self::HOOK_PREFIX.$category.self::HOOK_SUFFIX; return HookManager::applyFilters($hookName, $coreDrivers); } + /** + * Scout 엔진 등록 훅의 드라이버를 카탈로그 형태로 병합합니다. + * + * `core.search.engine_drivers` 는 `[id => EngineClass]` 맵이므로 키만 취해 + * `{id, label}` 형태로 변환한다. 이미 코어 목록에 있는 ID 는 중복 추가하지 않는다. + * + * @param array<array{id: string, label: array<string, string>}> $coreDrivers 코어 드라이버 목록 + * @return array<array{id: string, label: array<string, string>}> 병합된 목록 + */ + private function mergeSearchEngineDrivers(array $coreDrivers): array + { + $engines = HookManager::applyFilters('core.search.engine_drivers', []); + + if (! is_array($engines)) { + return $coreDrivers; + } + + $existing = array_map(fn ($driver) => $driver['id'] ?? null, $coreDrivers); + $locales = config('app.translatable_locales', ['ko', 'en']); + + foreach (array_keys($engines) as $id) { + if (! is_string($id) || $id === '' || in_array($id, $existing, true)) { + continue; + } + + $label = []; + foreach ($locales as $locale) { + $label[$locale] = Lang::get("settings.drivers.search.{$id}", [], $locale) ?: $id; + } + + $coreDrivers[] = ['id' => $id, 'label' => $label]; + } + + return $coreDrivers; + } + /** * 모든 카테고리의 사용 가능한 드라이버 목록을 반환합니다. * @@ -237,6 +296,70 @@ class DriverRegistryService return $inUse; } + /** + * 주어진 드라이버가 현재 서버에서 실제로 동작 가능한지 판정합니다. + * + * isDriverAvailable() 이 "등록 여부"(카탈로그 소속)를 보는 것과 달리, 이 메서드는 + * 어댑터 클래스·PHP 확장 등 런타임 능력의 존재를 검사합니다. 사용 불능 드라이버가 + * 저장되면 다음 부팅에서 사이트 전면 다운으로 이어질 수 있으므로 저장/테스트 + * FormRequest 의 서버 게이트가 이 판정을 사용합니다. + * + * @param string $category 드라이버 카테고리 (storage, cache, session, queue 등) + * @param string $driverId 드라이버 ID + * @return bool 동작 가능하면 true + */ + public function isDriverUsable(string $category, string $driverId): bool + { + return $this->usabilityFailureReason($category, $driverId) === null; + } + + /** + * 드라이버 사용 불능 사유를 반환합니다. + * + * 판정은 드라이버 ID 가 요구하는 런타임 능력 기준입니다. 코어가 능력을 모르는 + * 드라이버(플러그인 등록 드라이버 포함)는 null(사용 가능) — 플러그인 드라이버의 + * 가용성은 그 플러그인의 ServiceProvider 가 보증합니다. + * + * @param string $category 드라이버 카테고리 + * @param string $driverId 드라이버 ID + * @return string|null 사용 불능 사유 (다국어), 사용 가능하면 null + */ + public function usabilityFailureReason(string $category, string $driverId): ?string + { + return match ($driverId) { + 's3' => class_exists(AwsS3V3Adapter::class) + ? null + : __('settings.driver_unusable_s3_adapter'), + // redis 는 설정된 클라이언트 기준으로 판정한다 — predis 명시 설정이면 predis + // 존재만 본다. phpredis 설정은 확장 부재여도 부트 폴백(SettingsServiceProvider:: + // shouldFallBackToPredis)이 predis 로 전환하므로 predis 존재까지 사용 가능이다. + 'redis' => (config('database.redis.client', 'phpredis') === 'predis' + ? class_exists(Client::class) + : (extension_loaded('redis') || class_exists(Client::class))) + ? null + : __('settings.driver_unusable_redis_client'), + 'memcached' => extension_loaded('memcached') + ? null + : __('settings.driver_unusable_memcached_extension'), + default => null, + }; + } + + /** + * 카테고리의 Config 키에 기록할 값 형태로 드라이버 ID 를 변환합니다. + * + * log 카테고리의 적용 키(logging.channels.stack.channels)는 채널 배열이므로 + * 배열로 감싸고, 그 외 카테고리는 드라이버 ID 문자열 그대로입니다. + * + * @param string $category 드라이버 카테고리 + * @param string $driverId 드라이버 ID + * @return string|array<int, string> Config 기록 값 + */ + public function getConfigValueForDriver(string $category, string $driverId): string|array + { + return $category === 'log' ? [$driverId] : $driverId; + } + /** * 지원되는 카테고리 목록을 반환합니다. * diff --git a/app/Services/IdentityMessageTemplateService.php b/app/Services/IdentityMessageTemplateService.php index 78670dbe..ed882824 100644 --- a/app/Services/IdentityMessageTemplateService.php +++ b/app/Services/IdentityMessageTemplateService.php @@ -7,6 +7,7 @@ use App\Contracts\Repositories\IdentityMessageTemplateRepositoryInterface; use App\Extension\HookManager; use App\Models\IdentityMessageDefinition; use App\Models\IdentityMessageTemplate; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Auth; /** @@ -67,6 +68,29 @@ class IdentityMessageTemplateService ); } + /** + * ID 로 템플릿을 조회하며, 없으면 예외를 발생시킵니다. + * + * 컨트롤러가 모델을 직접 조회하지 않도록 하는 Service 경유 접근자입니다 + * (Controller → Service → Repository 계층 규정). + * + * @param int $id 템플릿 ID + * @return IdentityMessageTemplate 템플릿 모델 + * + * @throws ModelNotFoundException 템플릿이 없는 경우 + */ + public function findOrFailById(int $id): IdentityMessageTemplate + { + $template = $this->repository->findById($id); + + if (! $template) { + throw (new ModelNotFoundException) + ->setModel(IdentityMessageTemplate::class, [$id]); + } + + return $template; + } + /** * 템플릿 수정. * diff --git a/app/Services/IdentityPolicyService.php b/app/Services/IdentityPolicyService.php index 7c747b34..4fa7e398 100644 --- a/app/Services/IdentityPolicyService.php +++ b/app/Services/IdentityPolicyService.php @@ -8,11 +8,15 @@ use App\Enums\IdentityOriginType; use App\Enums\IdentityPolicyAppliesTo; use App\Enums\IdentityPolicyFailMode; use App\Enums\IdentityPolicySourceType; +use App\Enums\IdentityVerificationStatus; use App\Exceptions\IdentityVerificationRequiredException; use App\Extension\HookManager; use App\Extension\IdentityVerification\IdentityVerificationManager; +use App\Extension\ModuleManager; +use App\Extension\PluginManager; use App\Models\IdentityPolicy; use App\Models\User; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; /** * 본인인증 정책 해석/강제 Service. @@ -159,10 +163,13 @@ class IdentityPolicyService * `resolveRenderHint` 와 동일한 우선순위 체인을 적용하여, 정책에 provider 를 지정하지 않아도 * 환경설정의 기본값이 launcher payload 의 provider_id 로 전달되도록 한다. * + * 428 강제 경로와 프론트 프리페치 엔드포인트가 이 게터 하나로 수렴한다 — 같은 데이터에 + * 두 개의 해석이 존재하면 한쪽만 조용히 raw 값을 내보낸다(A6a). + * * @param IdentityPolicy $policy 대상 정책 * @return string|null 해석된 provider id (해석 실패 시 정책의 원본 값) */ - protected function resolveProviderId(IdentityPolicy $policy): ?string + public function resolveProviderId(IdentityPolicy $policy): ?string { try { $providerId = $policy->provider_id; @@ -193,7 +200,7 @@ class IdentityPolicyService * * @param array<string, mixed> $filters 필터 조건 * @param int $perPage 페이지 크기 - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + * @return LengthAwarePaginator */ public function search(array $filters, int $perPage = 20) { @@ -345,7 +352,7 @@ class IdentityPolicyService if ($policy->source_type === IdentityPolicySourceType::Module) { try { - $manager = app(\App\Extension\ModuleManager::class); + $manager = app(ModuleManager::class); $module = $manager->getModuleByIdentifier($policy->source_identifier) ?? $manager->getModule($policy->source_identifier); if ($module && method_exists($module, 'getIdentityPolicies')) { @@ -364,7 +371,7 @@ class IdentityPolicyService if ($policy->source_type === IdentityPolicySourceType::Plugin) { try { - $manager = app(\App\Extension\PluginManager::class); + $manager = app(PluginManager::class); $plugin = $manager->getPlugin($policy->source_identifier); if ($plugin && method_exists($plugin, 'getIdentityPolicies')) { foreach ($plugin->getIdentityPolicies() as $data) { @@ -542,7 +549,7 @@ class IdentityPolicyService 'channel' => 'policy', 'user_id' => $user?->id, 'target_hash' => $this->resolveTargetHash($user, $context) ?? str_repeat('0', 64), - 'status' => \App\Enums\IdentityVerificationStatus::PolicyViolationLogged->value, + 'status' => IdentityVerificationStatus::PolicyViolationLogged->value, 'origin_type' => $context['origin_type'] ?? IdentityOriginType::Policy->value, 'origin_identifier' => $context['origin_identifier'] ?? null, 'origin_policy_key' => $policy->key, diff --git a/app/Services/LanguagePackService.php b/app/Services/LanguagePackService.php index 6a267f58..75e453ba 100644 --- a/app/Services/LanguagePackService.php +++ b/app/Services/LanguagePackService.php @@ -724,8 +724,11 @@ class LanguagePackService return $this->finalizeInstall($extractPath, 'github', $githubUrl, $autoActivate, $installedBy, $force); } catch (Throwable $e) { $this->cleanupPending($extractPath); - File::deleteDirectory($tempPath); throw $e; + } finally { + // 다운로드 임시 디렉토리는 성공/실패와 무관하게 소비가 끝난 시점에 정리한다. + // catch 에만 두면 설치가 성공할 때마다 ZIP 사본이 storage/app/temp 에 잔존한다. + File::deleteDirectory($tempPath); } } @@ -776,8 +779,11 @@ class LanguagePackService return $this->finalizeInstall($extractPath, 'url', $url, $autoActivate, $installedBy, $force); } catch (Throwable $e) { $this->cleanupPending($extractPath); - File::deleteDirectory($tempPath); throw $e; + } finally { + // 다운로드 임시 디렉토리는 성공/실패와 무관하게 소비가 끝난 시점에 정리한다. + // catch 에만 두면 설치가 성공할 때마다 ZIP 사본이 storage/app/temp 에 잔존한다. + File::deleteDirectory($tempPath); } } @@ -1498,9 +1504,11 @@ class LanguagePackService public function checkUpdates(?string $identifier = null): array { // 요구사항 #4: 모듈 패턴 — 모든 source_type 점검 (GitHub 1순위, 실패 시 bundled 폴백) + // 전량 순회는 allForUpdateCheck() — paginate 는 HTTP page 파라미터를 암묵 해석해 + // `?page=2` 요청에서 순회가 0건이 된다 (공개 이슈 #102 동형) $packs = $identifier ? collect([$this->repository->findByIdentifier($identifier)])->filter() - : $this->repository->paginate([], 1000)->getCollection(); + : $this->repository->allForUpdateCheck(); $checked = 0; $updates = 0; @@ -1675,8 +1683,9 @@ class LanguagePackService return $updates; } - // DB 에 설치된 모든 언어팩 (보호 가상 행 제외 — protected 로 필터됨) - $packs = $this->repository->paginate([], 1000)->getCollection(); + // DB 에 설치된 모든 언어팩 (보호 가상 행 제외 — protected 로 필터됨). + // 전량 순회는 allForUpdateCheck() — paginate 의 암묵 page 해석 회피 (#102 동형) + $packs = $this->repository->allForUpdateCheck(); foreach ($packs as $pack) { if (! $this->hasBundledManifest($pack->identifier)) { diff --git a/app/Services/LayoutExtensionService.php b/app/Services/LayoutExtensionService.php index b143da21..3efc2c53 100644 --- a/app/Services/LayoutExtensionService.php +++ b/app/Services/LayoutExtensionService.php @@ -2,7 +2,6 @@ namespace App\Services; -use App\Contracts\Extension\CacheInterface; use App\Contracts\Repositories\LayoutExtensionRepositoryInterface; use App\Contracts\Repositories\LayoutExtensionVersionRepositoryInterface; use App\Contracts\Repositories\LayoutRepositoryInterface; @@ -15,6 +14,7 @@ use App\Extension\HookManager; use App\Extension\ModuleManager; use App\Extension\PluginManager; use App\Extension\TemplateManager; +use App\Extension\Traits\ClearsTemplateCaches; use App\Extension\Traits\ComputesLayoutContentHash; use App\Models\LayoutExtension; use App\Models\TemplateLayoutExtensionVersion; @@ -32,6 +32,7 @@ use Illuminate\Support\Facades\Log; */ class LayoutExtensionService { + use ClearsTemplateCaches; use ComputesLayoutContentHash; /** @@ -123,7 +124,6 @@ class LayoutExtensionService private LayoutExtensionRepositoryInterface $repository, private LayoutExtensionVersionRepositoryInterface $versionRepository, private LayoutRepositoryInterface $layoutRepository, - private CacheInterface $cache, private PluginRepositoryInterface $pluginRepository, private ModuleRepositoryInterface $moduleRepository, ) {} @@ -1830,8 +1830,10 @@ class LayoutExtensionService } // extension_point 는 여러 레이아웃에서 사용될 수 있으므로 전역 캐시 버전 갱신 - // overlay 도 프론트엔드 브라우저 캐시 무효화를 위해 동일하게 갱신 - $this->cache->put('ext.cache_version', time()); + // overlay 도 프론트엔드 브라우저 캐시 무효화를 위해 동일하게 갱신. + // 쓰기는 트레이트 단일 지점(고정 CoreCacheDriver + 메모이즈 스토어) 경유 — + // 주입 CacheInterface 직접 put 은 재바인딩/스토어 상이 시 read 와 어긋난다. + $this->incrementExtensionCacheVersion(); } /** diff --git a/app/Services/LayoutService.php b/app/Services/LayoutService.php index e0f03b6e..4f764d2f 100644 --- a/app/Services/LayoutService.php +++ b/app/Services/LayoutService.php @@ -12,6 +12,7 @@ use App\Exceptions\CircularReferenceException; use App\Exceptions\ConcurrentModificationException; use App\Exceptions\LayoutIncludeException; use App\Extension\HookManager; +use App\Extension\Traits\ClearsTemplateCaches; use App\Helpers\PermissionHelper; use App\Models\TemplateLayout; use App\Models\User; @@ -21,6 +22,8 @@ use Illuminate\Support\Facades\Log; class LayoutService { + use ClearsTemplateCaches; + /** * LayoutRepository, LayoutVersionRepository, TemplateRepository, LayoutResolverService 및 LayoutExtensionService 주입 */ @@ -1470,7 +1473,9 @@ class LayoutService // 프론트엔드 브라우저 캐시 무효화를 위해 ext.cache_version 증가 // PublicLayoutController가 ?v={version} 기반 HTTP 캐시를 사용하므로 // 버전 변경 시 브라우저가 새 URL로 인식하여 캐시를 우회합니다. - $this->cache->put('ext.cache_version', time()); + // 쓰기는 트레이트 단일 지점(고정 CoreCacheDriver + 메모이즈 스토어) 경유 — + // 주입 CacheInterface 직접 put 은 재바인딩/스토어 상이 시 read 와 어긋난다. + $this->incrementExtensionCacheVersion(); // After 훅 - 레이아웃 업데이트 후 HookManager::doAction('core.layout.after_update', $layout, $templateId, $name, $data); @@ -1579,8 +1584,8 @@ class LayoutService // 캐시 무효화 $this->clearDependentLayoutsCache($templateId, $name); - // 프론트엔드 브라우저 캐시 무효화 - $this->cache->put('ext.cache_version', time()); + // 프론트엔드 브라우저 캐시 무효화 (트레이트 단일 지점 경유) + $this->incrementExtensionCacheVersion(); // After 훅 - 버전 복원 후 HookManager::doAction('core.layout.after_version_restore', $newVersion, $templateId, $name, $versionId); diff --git a/app/Services/MenuService.php b/app/Services/MenuService.php index 53a2ce12..c1c457c1 100644 --- a/app/Services/MenuService.php +++ b/app/Services/MenuService.php @@ -7,10 +7,13 @@ use App\Contracts\Repositories\RoleRepositoryInterface; use App\Enums\ExtensionOwnerType; use App\Enums\MenuPermissionType; use App\Extension\HookManager; +use App\Helpers\PermissionHelper; use App\Models\Menu; use App\Models\User; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Validation\ValidationException; class MenuService @@ -182,13 +185,16 @@ class MenuService ]); } - // Hook: 메뉴 삭제 전 - HookManager::doAction('core.menu.before_delete', $menu); + // 역할 연결만 끊기고 메뉴는 남는 상태를 막기 위해 두 단계를 하나로 묶는다. + $result = DB::transaction(function () use ($menu) { + // Hook: 메뉴 삭제 전 + HookManager::doAction('core.menu.before_delete', $menu); - // 역할 연결 해제 (명시적 삭제 - CASCADE 의존 금지) - $menu->roles()->detach(); + // 역할 연결 해제 (명시적 삭제 - CASCADE 의존 금지) + $menu->roles()->detach(); - $result = $this->menuRepository->delete($menu); + return $this->menuRepository->delete($menu); + }); // Hook: 메뉴 삭제 후 HookManager::doAction('core.menu.after_delete', $menu->id); @@ -199,11 +205,19 @@ class MenuService /** * 메뉴 순서를 업데이트합니다 (드래그 앤 드롭). * + * 형제 `updateMenuOrderWithHierarchy` 와 같은 리소스를 같은 방식으로 쓰므로 스코프 + * 가드도 대칭이어야 한다. 코어 내 호출부가 없다는 사실은 방어가 아니다 — 이 서비스는 + * 확장이 주입받을 수 있고, 가드가 형제 경로에만 있으면 여기가 우회로가 된다. + * * @param array $menuOrders 메뉴 ID와 순서 매핑 배열 * @return bool 업데이트 성공 여부 + * + * @throws AuthorizationException 스코프 밖 메뉴가 하나라도 포함된 경우 */ public function updateMenuOrder(array $menuOrders): bool { + $this->assertMenusWithinScope(array_map('intval', array_values($menuOrders))); + // 훅: 메뉴 순서 변경 전 (IDV 정책 가드 지점) HookManager::doAction('core.menu.before_update_order', $menuOrders); @@ -223,6 +237,8 @@ class MenuService */ public function updateMenuOrderWithHierarchy(array $orderData): bool { + $this->assertOrderTargetsWithinScope($orderData); + $result = $this->menuRepository->updateOrderWithHierarchy($orderData); // 훅: 메뉴 계층 순서 변경 후 @@ -231,6 +247,74 @@ class MenuService return $result; } + /** + * 순서 변경 대상 메뉴가 액터의 스코프 안에 있는지 검사합니다. + * + * `PUT admin/menus/order` 는 라우트 모델이 없는 정적 경로라 PermissionMiddleware 의 + * 스코프 검사가 스킵된다(`{menu}` 파라미터 부재 → "목록 엔드포인트" 로 간주). 상세 + * 경로(`PUT menus/{menu}`)가 미들웨어로 강제하는 축이 이 경로에서만 비어 있었다. + * 배포 기본 역할은 `core.menus.update` 를 글로벌로 주므로 기본값 노출은 아니지만, + * 운영자가 역할 화면에서 스코프를 self/role 로 좁히는 순간 우회로가 된다. + * + * 첨부 순서 변경과 같은 이유로 **전량 거부**다 — 순서는 트리 전체에 대한 하나의 + * 배열이라 일부만 반영하면 나머지와 어긋난 계층이 저장된다. + * + * @param array $orderData 순서 데이터 (parent_menus / child_menus / moved_items) + * + * @throws AuthorizationException 스코프 밖 메뉴가 하나라도 포함된 경우 + */ + private function assertOrderTargetsWithinScope(array $orderData): void + { + $ids = []; + + foreach ($orderData['parent_menus'] ?? [] as $item) { + $ids[] = (int) ($item['id'] ?? 0); + } + + foreach ($orderData['child_menus'] ?? [] as $children) { + foreach ($children as $item) { + $ids[] = (int) ($item['id'] ?? 0); + } + } + + // 이동 항목은 **옮기는 메뉴 자신**만 확인한다. 새 부모까지 검사하면, 공용 상위 메뉴 + // 아래에 자기 메뉴를 다는 정상 사용이 막힌다 — 결함은 "남의 메뉴 순서를 바꾼다" 였지 + // "남의 메뉴 아래에 못 붙인다" 가 아니다. + foreach ($orderData['moved_items'] ?? [] as $item) { + $ids[] = (int) ($item['id'] ?? 0); + } + + $this->assertMenusWithinScope($ids); + } + + /** + * 주어진 메뉴 ID 집합이 전부 액터의 스코프 안에 있는지 검사합니다. + * + * 순서 변경 두 경로(`updateMenuOrder` · `updateMenuOrderWithHierarchy`)가 공유하는 + * 판정부다. 한쪽만 검사하면 형제 경로가 우회로가 되므로 판정을 한 곳에 둔다. + * + * @param array<int> $ids 검사 대상 메뉴 ID 목록 + * + * @throws AuthorizationException 스코프 밖 메뉴가 하나라도 포함된 경우 + */ + private function assertMenusWithinScope(array $ids): void + { + $ids = array_values(array_unique(array_filter($ids))); + + if (empty($ids)) { + return; + } + + $menus = $this->menuRepository->findByIds($ids); + + // 판정은 상세 경로와 같은 SSoT 에 위임한다. + $permitted = PermissionHelper::filterByScope($menus, 'core.menus.update'); + + if (count($permitted) !== $menus->count()) { + throw new AuthorizationException(__('auth.scope_denied')); + } + } + /** * 메뉴의 활성화 상태를 토글합니다. * diff --git a/app/Services/NotificationLogService.php b/app/Services/NotificationLogService.php index cc8a6c58..9ac8c6e4 100644 --- a/app/Services/NotificationLogService.php +++ b/app/Services/NotificationLogService.php @@ -93,6 +93,27 @@ class NotificationLogService return $result; } + /** + * 보존 기간이 지난 발송 이력을 정리합니다 (자동 파기). + * + * 운영자가 고른 ID 를 지우는 bulkDelete 와는 별개의 훅을 발행한다 — 자동 파기는 + * 대상을 기간으로 정하고 사람 없이 예약 실행되므로, 대화형 가드를 전제한 훅에 + * 얹으면 예약이 그 가드에 걸린다. 확장은 이 훅으로 자기 처리를 붙인다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function prune(int $days): int + { + HookManager::doAction('core.notification_log.before_prune', $days); + + $count = $this->repository->deleteOlderThan($days); + + HookManager::doAction('core.notification_log.after_prune', $days, $count); + + return $count; + } + /** * 다건 삭제합니다. * diff --git a/app/Services/PluginSettingsService.php b/app/Services/PluginSettingsService.php index b8f6e2a5..3043333c 100644 --- a/app/Services/PluginSettingsService.php +++ b/app/Services/PluginSettingsService.php @@ -5,6 +5,7 @@ namespace App\Services; use App\Extension\HookManager; use App\Extension\PluginManager; use App\Extension\TemplateManager; +use App\Support\ExtensionSettingsMirror; use App\Support\SensitiveSettingMask; use App\Traits\FiltersFrontendSchema; use App\Traits\NormalizesSettingsData; @@ -189,6 +190,10 @@ class PluginSettingsService // 캐시 초기화 if ($result) { unset($this->settingsCache[$identifier]); + + // 같은 프로세스의 config 미러도 즉시 다시 채운다 (공개이슈 #109) — + // 이 호출이 없으면 상주 프로세스가 저장 후에도 옛 값을 계속 읽는다. + app(ExtensionSettingsMirror::class)->refreshPlugin($identifier); } // After 훅 @@ -247,6 +252,10 @@ class PluginSettingsService // 캐시 초기화 unset($this->settingsCache[$identifier]); + // 초기화도 값을 바꾸는 쓰기다 — 미러를 두면 같은 프로세스가 초기화 전 값을 + // 계속 읽는다 (저장 경로와 동일 결함, 공개이슈 #109). + app(ExtensionSettingsMirror::class)->refreshPlugin($identifier); + // After 훅 HookManager::doAction('core.plugin_settings.after_reset', $identifier); diff --git a/app/Services/RoleService.php b/app/Services/RoleService.php index cf033049..fc3eb827 100644 --- a/app/Services/RoleService.php +++ b/app/Services/RoleService.php @@ -3,20 +3,24 @@ namespace App\Services; use App\Contracts\Repositories\RoleRepositoryInterface; +use App\Exceptions\CannotModifyProtectedRoleException; use App\Exceptions\ExtensionOwnedRoleDeleteException; use App\Exceptions\SystemRoleDeleteException; use App\Extension\HookManager; use App\Models\Role; use App\Models\User; +use App\Support\PermissionEscalationGuard; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; class RoleService { public function __construct( - private RoleRepositoryInterface $roleRepository + private RoleRepositoryInterface $roleRepository, + private PermissionEscalationGuard $escalationGuard ) {} /** @@ -104,6 +108,12 @@ class RoleService $permissions = $data['permissions'] ?? []; unset($data['permissions']); + // 권한 상승 상한을 **역할 생성 전에** 검사한다. 생성 뒤에 검사하면 거부된 요청이 + // 권한 0개짜리 고아 역할 행을 남긴다(사용자 경로와 동일한 "가드 → 쓰기" 순서). + if (! empty($permissions)) { + $this->escalationGuard->assertGrantWithinActorCeiling($permissions); + } + // 훅: 생성 전 HookManager::doAction('core.role.before_create', $data); @@ -133,10 +143,20 @@ class RoleService */ public function updateRole(Role $role, array $data): Role { + // 보호된 역할(코어/확장 소유) 수정 상한: 삭제 경로와 대칭. + // 비-슈퍼관리자 액터는 admin 등 코어/확장 소유 역할을 변경할 수 없다. + $this->assertActorMayModifyRole($role); + // 권한 목록 분리 $permissions = $data['permissions'] ?? null; unset($data['permissions']); + // 권한 상승 상한을 **속성 업데이트 전에** 검사한다. update 뒤에 검사하면 + // 403 을 받은 요청이 name/is_active 변경만 반영된 상태를 남긴다. + if ($permissions !== null) { + $this->escalationGuard->assertGrantWithinActorCeiling($permissions); + } + // 훅: 업데이트 전 (원본 data 전달) HookManager::doAction('core.role.before_update', $role, $data); @@ -183,16 +203,20 @@ class RoleService throw new ExtensionOwnedRoleDeleteException; } - // 훅: 삭제 전 - HookManager::doAction('core.role.before_delete', $role); + // 관계 해제 후 삭제가 실패하면 그 역할을 가진 사용자 전원이 권한만 잃고 + // 역할은 그대로 남는다. 전 단계를 하나로 묶는다. + $result = DB::transaction(function () use ($role) { + // 훅: 삭제 전 + HookManager::doAction('core.role.before_delete', $role); - // 관계 해제 (명시적 삭제 - CASCADE 의존 금지) - $this->roleRepository->detachAllPermissions($role); - $role->menus()->detach(); - $role->users()->detach(); + // 관계 해제 (명시적 삭제 - CASCADE 의존 금지) + $this->roleRepository->detachAllPermissions($role); + $role->menus()->detach(); + $role->users()->detach(); - // 역할 삭제 - $result = $this->roleRepository->delete($role); + // 역할 삭제 + return $this->roleRepository->delete($role); + }); // 훅: 삭제 후 HookManager::doAction('core.role.after_delete', $role->id); @@ -208,6 +232,15 @@ class RoleService */ public function syncPermissions(Role $role, array $permissions): void { + // 보호된 역할(코어/확장 소유) 수정 상한: updateRole·toggleRoleStatus 와 대칭. + // 이 메서드는 public 이므로 서비스를 주입한 확장이 직접 호출할 수 있다 — + // 형제 경로에만 가드를 두면 여기가 코어 역할 보호의 우회로가 된다. + $this->assertActorMayModifyRole($role); + + // 권한 상승 상한(ceiling): 비-슈퍼관리자 액터는 자신이 보유하지 않았거나 + // 자신의 범위(scope)보다 넓은 범위의 권한을 부여할 수 없다(SSoT 가드에 위임). + $this->escalationGuard->assertGrantWithinActorCeiling($permissions); + // 동기화 전 현재 권한 식별자 캡처 (Listener diff 계산용) $previousPermIdentifiers = $role->permissions()->pluck('identifier')->toArray(); @@ -239,6 +272,9 @@ class RoleService */ public function toggleRoleStatus(Role $role): bool { + // 보호된 역할(코어/확장 소유) 상태변경 상한: 삭제 경로와 대칭. + $this->assertActorMayModifyRole($role); + $newStatus = ! $role->is_active; // 훅: 상태 변경 전 @@ -254,6 +290,37 @@ class RoleService return $result; } + /** + * 액터가 보호된 역할(코어/확장 소유)을 수정할 수 있는지 확인합니다. + * + * 인증 액터가 없으면(Artisan/내부 시더) 신뢰 경로로 간주해 통과시킵니다. + * 슈퍼 관리자는 모든 역할을 수정할 수 있고, 그 외 액터는 삭제 경로와 동일하게 + * 코어/확장 소유 역할을 수정할 수 없습니다. + * + * @param Role $role 대상 역할 + * + * @throws CannotModifyProtectedRoleException 상한 위반 시 + */ + private function assertActorMayModifyRole(Role $role): void + { + $actor = Auth::user(); + + // 인증 액터 부재 = 내부/Artisan 신뢰 경로 → 가드 미적용 + if (! $actor instanceof User) { + return; + } + + // 슈퍼 관리자는 모든 역할 수정 가능 + if ($actor->isSuperAdmin()) { + return; + } + + // 비-슈퍼관리자는 코어/확장 소유 역할 수정 불가 + if ($role->isCore() || $role->isExtensionOwned()) { + throw new CannotModifyProtectedRoleException; + } + } + /** * name에서 identifier를 자동 생성합니다. * diff --git a/app/Services/ScheduleService.php b/app/Services/ScheduleService.php index 0a2978f1..f68899f5 100644 --- a/app/Services/ScheduleService.php +++ b/app/Services/ScheduleService.php @@ -122,13 +122,16 @@ class ScheduleService public function delete(Schedule $schedule): bool { try { - // before_delete 훅 실행 - HookManager::doAction('core.schedule.before_delete', $schedule); + // 이력만 지워지고 스케줄은 남는 상태를 막기 위해 두 단계를 하나로 묶는다. + $result = DB::transaction(function () use ($schedule) { + // before_delete 훅 실행 + HookManager::doAction('core.schedule.before_delete', $schedule); - // 실행 이력 삭제 (명시적 삭제 - CASCADE 의존 금지) - $schedule->histories()->delete(); + // 실행 이력 삭제 (명시적 삭제 - CASCADE 의존 금지) + $schedule->histories()->delete(); - $result = $this->scheduleRepository->delete($schedule); + return $this->scheduleRepository->delete($schedule); + }); // after_delete 훅 실행 HookManager::doAction('core.schedule.after_delete', $schedule->id); @@ -488,6 +491,26 @@ class ScheduleService ]; } + /** + * 보존 기간이 지난 실행 이력을 정리합니다 (자동 파기). + * + * 운영자가 고른 ID 를 지우는 bulkDeleteHistory 와 달리 대상을 기간으로 정하고 + * 사람 없이 예약 실행되므로 별개의 훅을 발행합니다. + * + * @param int $days 보존 기간 (일) + * @return int 삭제된 건수 + */ + public function pruneHistory(int $days): int + { + HookManager::doAction('core.schedule.before_prune_history', $days); + + $deletedCount = $this->historyRepository->deleteOlderThan($days); + + HookManager::doAction('core.schedule.after_prune_history', $days, $deletedCount); + + return $deletedCount; + } + /** * 활성화된 스케줄들을 조회합니다. * diff --git a/app/Services/SettingsService.php b/app/Services/SettingsService.php index a191a5bc..80072b9d 100644 --- a/app/Services/SettingsService.php +++ b/app/Services/SettingsService.php @@ -9,6 +9,7 @@ use App\Extension\HookManager; use App\Http\Resources\AttachmentResource; use App\Seo\Contracts\SeoCacheManagerInterface; use App\Support\ConfigCacheHelper; +use App\Support\ExtensionSettingsMirror; use App\Support\OpcacheStatus; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; @@ -28,7 +29,8 @@ class SettingsService public function __construct( private ConfigRepositoryInterface $configRepository, private AttachmentRepositoryInterface $attachmentRepository, - private CacheInterface $cache + private CacheInterface $cache, + private AttachmentService $attachmentService ) {} /** @@ -43,6 +45,31 @@ class SettingsService { $this->cache->forget('settings.system'); ConfigCacheHelper::rebuild(); + + // 같은 프로세스의 in-memory 미러도 즉시 다시 채운다 (공개이슈 #109). + // 이 호출이 없으면 상주 프로세스(큐 워커·schedule:work·Reverb)는 저장 후에도 + // 부팅 시점의 옛 값을 영원히 읽는다 — FPM 에서만 드러나지 않는 결함이다. + app(ExtensionSettingsMirror::class)->refreshCore(); + } + + /** + * 큐 워커에 정상 종료 후 재시작 신호를 보냅니다. + * + * drivers 카테고리(queue/broadcasting/cache 등)는 long-running worker 에 영향을 준다. + * SettingsServiceProvider 는 worker boot 시점에 한 번만 config 를 적용하므로, 재시작 + * 신호가 없으면 워커가 부팅 시점의 옛 드라이버로 계속 동작한다. + * + * 신호 전송 실패가 설정 저장을 되돌리지는 않는다 (경고 로깅 후 계속). + */ + private function restartQueueWorkers(): void + { + try { + Artisan::call('queue:restart'); + } catch (\Throwable $e) { + Log::warning('queue:restart 실행 실패', [ + 'error' => $e->getMessage(), + ]); + } } /** @@ -489,9 +516,18 @@ class SettingsService return $result; } - // general 탭인 경우 site_logo 첨부파일 연결 - if ($tab === 'general') { - $tabSettings['site_logo'] = $this->collectSiteLogoIds(); + // general 탭인 경우 site_logo 첨부파일 연결. + // site_logo 를 제출하지 않은 저장(다른 필드만 변경)은 기존 저장값을 그대로 둔다 — + // 이때 컬렉션을 다시 훑으면 미참조 첨부가 설정으로 딸려 들어온다. + $removedSiteLogoIds = []; + + if ($tab === 'general' && is_array($tabSettings['site_logo'] ?? null)) { + // 파기 대상 판정은 저장 **전에** 한다 — 저장 후에는 직전 저장값을 알 수 없다. + // 실제 파기는 저장이 성공한 뒤에 수행한다: 저장이 실패했는데 파일만 사라지면 + // 설정에는 이미 없는 첨부 id 가 남아 로고가 깨진다. + $removedSiteLogoIds = $this->resolveRemovedSiteLogoIds($tabSettings['site_logo']); + + $tabSettings['site_logo'] = $this->resolveSiteLogoIds($tabSettings['site_logo']); } // 기존 설정과 병합 (탭별로 일부 필드만 전송되어도 기존 설정 유지) @@ -510,6 +546,9 @@ class SettingsService if ($result) { $this->invalidateSettingsCache(); + // 저장이 확정된 뒤에야 파일을 파기한다 (위 판정 시점 주석 참조). + $this->purgeSiteLogoAttachments($removedSiteLogoIds); + // SEO 프리렌더 캐시에는 생성 시점의 자산 URL 이 그대로 구워져 있다. // 모드가 바뀌면 그 URL 들이 전부 어긋나는데, 봇은 JavaScript 를 실행하지 // 않아 브라우저 자가 복구가 닿지 않는다 → 캐시를 비워 재생성시킨다. @@ -522,13 +561,7 @@ class SettingsService // SettingsServiceProvider는 worker boot 시점에 한 번만 config 적용하므로 // 워커가 정상 종료 후 재시작되도록 신호 전송 (cache 기반, 즉시 종료 X) if ($tab === 'drivers') { - try { - Artisan::call('queue:restart'); - } catch (\Throwable $e) { - Log::warning('queue:restart 실행 실패', [ - 'error' => $e->getMessage(), - ]); - } + $this->restartQueueWorkers(); } } @@ -664,15 +697,120 @@ class SettingsService } /** - * site_logo 컬렉션의 첨부파일 ID 목록을 수집합니다. + * 저장할 사이트 로고 첨부 ID 목록을 결정합니다. * - * @return array<int> 첨부파일 ID 배열 + * 기준은 **이번 저장 요청이 제출한 목록**입니다. 컬렉션 전체를 다시 훑으면, 저장에 실패했거나 + * 작성 중 이탈해 남은 미참조 첨부까지 설정에 다시 편입되어(운영자가 올린 적 없는 로고가 + * 되살아나는) 누적이 발생합니다. + * + * 제출값에 있더라도 실제로 존재하지 않는 첨부(다른 경로로 이미 삭제된 id)는 걸러냅니다. + * + * @param array<int, mixed> $submitted 제출된 site_logo 값 (첨부 객체 배열 또는 ID 배열) + * @return array<int, int> 저장할 첨부파일 ID 배열 */ - private function collectSiteLogoIds(): array + private function resolveSiteLogoIds(array $submitted): array { - $attachments = $this->attachmentRepository->getByCollection('site_logo'); + $submittedIds = $this->extractAttachmentIds($submitted); - return $attachments->pluck('id')->toArray(); + if ($submittedIds === []) { + return []; + } + + $existingIds = $this->attachmentRepository->getByCollection('site_logo') + ->pluck('id') + ->all(); + + return array_values(array_intersect($submittedIds, $existingIds)); + } + + /** + * 저장 요청에서 빠진(= 운영자가 화면에서 제거한) 사이트 로고 첨부 ID 를 가려냅니다. + * + * 판정 기준은 **직전 저장값**입니다. 직전에 저장돼 있었는데 이번 제출에서 빠진 id 만 + * 운영자가 명시적으로 뺀 것이고, 직전 저장값에도 없던 id 는 이번에 새로 올라온 첨부입니다. + * 그래서 이 판정은 저장으로 값이 덮이기 **전에** 수행해야 합니다. + * + * 저장할 목록 자체는 제출값이 정합니다(resolveSiteLogoIds) — 컬렉션 전체를 훑으면 저장에 + * 실패했거나 이탈로 남은 미참조 첨부가 설정에 되살아납니다. + * + * @param mixed $submitted 제출된 site_logo 값 (첨부 객체 배열 또는 ID 배열, 미제출이면 null) + * @return array<int, int> 파기 대상 첨부 ID 목록 + */ + private function resolveRemovedSiteLogoIds(mixed $submitted): array + { + // site_logo 를 아예 제출하지 않은 저장(다른 필드만 변경)은 판정 대상이 아니다. + if (! is_array($submitted)) { + return []; + } + + $previousIds = $this->extractAttachmentIds( + $this->configRepository->getCategory('general')['site_logo'] ?? [] + ); + + if ($previousIds === []) { + return []; + } + + $keptIds = $this->extractAttachmentIds($submitted); + + return array_values(array_diff($previousIds, $keptIds)); + } + + /** + * 제거가 확정된 사이트 로고 첨부를 파일까지 파기합니다. + * + * 설정 저장이 성공한 뒤에만 호출합니다 — 저장이 실패했는데 파일이 먼저 사라지면 설정에는 + * 이미 없는 첨부 id 가 남아 로고가 깨집니다. + * + * @param array<int, int> $removedIds 파기 대상 첨부 ID 목록 + */ + private function purgeSiteLogoAttachments(array $removedIds): void + { + if ($removedIds === []) { + return; + } + + foreach ($removedIds as $removedId) { + // 설정 저장은 이미 확정된 뒤다 — 파기 실패가 저장 실패(422)로 위장되면 + // 운영자는 성공한 저장을 실패로 오인한다. 실패 파일은 로그로만 남긴다. + try { + $this->attachmentService->delete($removedId); + } catch (\Exception $e) { + Log::warning('사이트 로고 첨부 파기 실패 — 저장은 확정됨', [ + 'attachment_id' => $removedId, + 'error' => $e->getMessage(), + ]); + } + } + + Log::info('사이트 로고 첨부 제거', ['attachment_ids' => $removedIds]); + } + + /** + * 첨부 목록 값에서 첨부 ID 만 추출합니다. + * + * 저장값은 ID 배열이지만 화면 제출값은 첨부 객체 배열이라 두 형태를 모두 받습니다. + * + * @param mixed $value 첨부 목록 값 + * @return array<int, int> 첨부 ID 목록 + */ + private function extractAttachmentIds(mixed $value): array + { + if (! is_array($value)) { + return []; + } + + $ids = []; + + foreach ($value as $item) { + $id = is_array($item) ? ($item['id'] ?? null) : $item; + + if (is_numeric($id)) { + $ids[] = (int) $id; + } + } + + return array_values(array_unique($ids)); } /** @@ -690,6 +828,18 @@ class SettingsService /** * 단일 설정 값을 저장합니다. * + * 벌크 저장(saveSettings)이 수행하는 부수효과 중 저장 키에 해당하는 것을 함께 수행한다 + * (공개 #114 동종). 예전에는 값만 쓰고 SEO 프리렌더 캐시 삭제·큐 워커 재시작 신호를 + * 건너뛰어, 같은 값을 어느 경로로 바꾸느냐에 따라 시스템 상태가 달라졌다. + * + * 키는 **원본 저장소 키**로 받는다 — 벌크 저장의 `reverseFrontendKeys()`(화면 키 → 저장소 + * 키 역변환)를 적용하지 않는다. 이 경로의 프로그램 호출자(본인인증 플러그인 설치/삭제의 + * `identity.purpose_providers.*`)가 저장소 키를 직접 넘기고 있어, 역변환을 끼우면 그 + * 호출들이 엉뚱한 키에 저장된다. + * + * 벌크 위임도 하지 않는다 — 벌크의 shallow `array_merge` 로는 깊은 키를 저장할 때 + * 형제 매핑이 통째로 소실된다. + * * @param string $key 설정 키 (예: 'general.site_name') * @param mixed $value 저장할 값 * @return bool 저장 성공 여부 @@ -700,10 +850,25 @@ class SettingsService HookManager::doAction('core.settings.before_set', $key, $value); try { + // 자산 URL 방식이 바뀌는지 저장 **전에** 판정한다 (이슈 #486 동형). + // 저장 후에는 이전 값을 알 수 없어 변경 여부를 판별할 수 없다. + $assetUrlModeChanged = $key === 'general.asset_url_mode' + && $this->configRepository->get($key) !== $value; + $result = $this->configRepository->set($key, $value); if ($result) { - $this->cache->forget('settings.system'); + // 인라인 무효화 대신 공통 경로를 탄다 — saveSettings/saveAdvancedSettings 와 + // 같은 처리를 받아야 미러 재채움·디스크 config 캐시 재생성이 빠지지 않는다. + $this->invalidateSettingsCache(); + + if ($assetUrlModeChanged) { + $this->clearSeoCacheForAssetUrlMode(); + } + + if (str_starts_with($key, 'drivers.') || $key === 'drivers') { + $this->restartQueueWorkers(); + } } // After 훅 @@ -738,7 +903,9 @@ class SettingsService $result = $this->configRepository->restore($backupPath); if ($result) { - $this->cache->forget('settings.system'); + // 복원도 설정 전체를 갈아엎는 쓰기다 — 캐시만 비우고 미러를 두면 + // 같은 프로세스가 복원 전 값을 계속 읽는다 (저장 경로와 동일 결함, 공개이슈 #109). + $this->invalidateSettingsCache(); } return $result; diff --git a/app/Services/StorageLeftoverPruneService.php b/app/Services/StorageLeftoverPruneService.php new file mode 100644 index 00000000..63fb277a --- /dev/null +++ b/app/Services/StorageLeftoverPruneService.php @@ -0,0 +1,347 @@ +<?php + +namespace App\Services; + +use Illuminate\Support\Facades\File; + +/** + * 스토리지 잔존물 정리 서비스 (`storage:prune-leftovers`) + * + * 확장/코어 업데이트·설치가 중단되며 남긴 임시 산출물과 오래된 백업본을 회수합니다. + * 각 산출물의 생성 지점은 best-effort 정리(finally)만 가지고 있어, 강제 종료·파일 + * 잠금 환경에서 남은 잔존물은 어떤 경로도 회수하지 않았습니다 (공개이슈 #120 동형 — + * 산출물 누적 + 회수 경로 부재). `CleanupExtensionBundlesCommand` → + * `ExtensionBundleService` 위임 패턴을 미러합니다. + * + * 파괴 방지 가드: + * - `_pending` 직하위는 타임스탬프/교체 마커 패턴 매칭 디렉토리만 대상 — + * 다운로드 대기 정상 확장 디렉토리(접미사 없음)는 불가침. + * - 백업은 identifier 별 최신 1개를 나이와 무관하게 상시 보존 (롤백 능력 유지). + * - 판정은 전부 mtime 기준 — 진행 중 작업의 산출물은 신선해서 걸리지 않는다. + */ +class StorageLeftoverPruneService +{ + /** + * 업데이트 스테이징 타임스탬프 접미사 (`ExtensionPendingHelper::createUpdateStagingPath` + * 의 `{identifier}_{Ymd_His}` 형식). + */ + private const STAGING_TIMESTAMP_PATTERN = '/_\d{8}_\d{6}$/'; + + /** + * 디렉토리 교체 임시 마커 (`{identifier}_updating_{uniqid}` / `{identifier}_old_{uniqid}`). + * `cleanupSwapLeftovers()` 는 다음 업데이트 시점에만 재시도하므로, 업데이트가 다시 + * 실행되지 않는 확장의 잔존물은 여기서만 회수된다. + */ + private const SWAP_LEFTOVER_PATTERN = '/_(?:updating|old)_[0-9a-f]{13}$/'; + + /** + * 백업 디렉토리 이름 (`{identifier}_{Ymd_His}`) — ExtensionBackupHelper::createBackup. + */ + private const BACKUP_NAME_PATTERN = '/^(.+)_(\d{8}_\d{6})$/'; + + /** + * 코어 백업 디렉토리 이름 (`core_{Ymd_His}`) — CoreBackupHelper::createBackup. + */ + private const CORE_BACKUP_NAME_PATTERN = '/^core_(\d{8}_\d{6})$/'; + + /** + * @param string|null $baseRoot 프로젝트 루트 재정의 (테스트 격리용, 기본 base_path()) + * @param string|null $storageRoot 스토리지 루트 재정의 (테스트 격리용, 기본 storage_path()) + */ + public function __construct( + private readonly ?string $baseRoot = null, + private readonly ?string $storageRoot = null, + ) {} + + /** + * 잔존물을 대상군별로 정리하고 삭제(예정) 경로 목록을 돌려줍니다. + * + * @param int $days 임시 산출물 보존일 (mtime 기준, 경과분 삭제) + * @param int $backupDays 백업 보존일 (최신 1개는 나이와 무관하게 보존) + * @param bool $dryRun true 면 삭제 없이 대상 목록만 수집 + * @return array<string, list<string>> 대상군별 삭제(예정) 경로 목록 + */ + public function prune(int $days = 3, int $backupDays = 30, bool $dryRun = false): array + { + $now = time(); + $cutoff = $now - $days * 86400; + $backupCutoff = $now - $backupDays * 86400; + + return [ + 'staging' => $this->pruneUpdateStaging($cutoff, $dryRun), + 'temp' => $this->pruneCoreTemp($cutoff, $dryRun), + 'vendor_bundle_staging' => $this->pruneVendorBundleStaging($cutoff, $dryRun), + 'extension_backups' => $this->pruneExtensionBackups($backupCutoff, $dryRun), + 'core_backups' => $this->pruneCoreBackups($backupCutoff, $dryRun), + 'legacy_browser_log' => $this->pruneLegacyBrowserLog($dryRun), + ]; + } + + /** + * `{modules,plugins,templates}/_pending` 의 스테이징/교체 잔존물을 정리합니다. + * + * @param int $cutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneUpdateStaging(int $cutoff, bool $dryRun): array + { + $pruned = []; + + foreach (['modules', 'plugins', 'templates'] as $type) { + $pendingRoot = $this->basePath($type.DIRECTORY_SEPARATOR.'_pending'); + + if (! File::isDirectory($pendingRoot)) { + continue; + } + + foreach (File::directories($pendingRoot) as $dir) { + $name = basename($dir); + + // 타임스탬프/교체 마커가 없는 디렉토리는 다운로드 대기 정상 확장 — 불가침. + if (! preg_match(self::STAGING_TIMESTAMP_PATTERN, $name) + && ! preg_match(self::SWAP_LEFTOVER_PATTERN, $name)) { + continue; + } + + if ($this->isFresh($dir, $cutoff)) { + continue; + } + + $pruned[] = $dir; + + if (! $dryRun) { + File::deleteDirectory($dir); + } + } + } + + return $pruned; + } + + /** + * `storage/app/temp` 직하위 전부(디렉토리·파일)를 보존일 기준으로 정리합니다. + * + * @param int $cutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneCoreTemp(int $cutoff, bool $dryRun): array + { + return $this->pruneEntriesByAge( + $this->storagePath('app'.DIRECTORY_SEPARATOR.'temp'.DIRECTORY_SEPARATOR.'*'), + $cutoff, + $dryRun + ); + } + + /** + * vendor 번들 스테이징(`build-*`)을 보존일 기준으로 정리합니다. + * + * @param int $cutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneVendorBundleStaging(int $cutoff, bool $dryRun): array + { + return $this->pruneEntriesByAge( + $this->storagePath('app'.DIRECTORY_SEPARATOR.'vendor-bundle-staging'.DIRECTORY_SEPARATOR.'build-*'), + $cutoff, + $dryRun + ); + } + + /** + * 확장 백업(`extension_backups/{type}/{identifier}_{Ymd_His}`)을 정리합니다. + * + * identifier 별 최신 1개는 나이와 무관하게 보존하고, 나머지 중 보존일 경과분만 + * 삭제합니다. type 디렉토리는 열거로 얻으므로 새 백업 유형(lang-packs 등)도 + * 하드코딩 없이 같은 규칙을 받습니다. + * + * @param int $backupCutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneExtensionBackups(int $backupCutoff, bool $dryRun): array + { + $pruned = []; + $backupRoot = $this->storagePath('app'.DIRECTORY_SEPARATOR.'extension_backups'); + + if (! File::isDirectory($backupRoot)) { + return $pruned; + } + + foreach (File::directories($backupRoot) as $typeDir) { + /** @var array<string, list<array{path: string, stamp: string}>> $byIdentifier */ + $byIdentifier = []; + + foreach (File::directories($typeDir) as $dir) { + if (! preg_match(self::BACKUP_NAME_PATTERN, basename($dir), $matches)) { + continue; + } + + $byIdentifier[$matches[1]][] = ['path' => $dir, 'stamp' => $matches[2]]; + } + + foreach ($byIdentifier as $entries) { + array_push($pruned, ...$this->pruneBackupSet($entries, $backupCutoff, $dryRun)); + } + } + + return $pruned; + } + + /** + * 코어 백업(`core_backups/core_{Ymd_His}`)을 정리합니다 — 최신 1개 상시 보존. + * + * @param int $backupCutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneCoreBackups(int $backupCutoff, bool $dryRun): array + { + $backupRoot = $this->storagePath('app'.DIRECTORY_SEPARATOR.'core_backups'); + + if (! File::isDirectory($backupRoot)) { + return []; + } + + $entries = []; + + foreach (File::directories($backupRoot) as $dir) { + if (! preg_match(self::CORE_BACKUP_NAME_PATTERN, basename($dir), $matches)) { + continue; + } + + $entries[] = ['path' => $dir, 'stamp' => $matches[1]]; + } + + return $this->pruneBackupSet($entries, $backupCutoff, $dryRun); + } + + /** + * 레거시 단일 `storage/logs/browser.log` 를 정리합니다. + * + * 브라우저 로그가 daily 드라이버로 전환되어 이 파일에 쓰는 코드는 더 이상 없다 — + * 존재 자체가 잔존물이므로 나이와 무관하게 삭제합니다. 날짜별 `browser-*.log` 는 + * daily 드라이버가 보존일로 자체 정리하므로 대상이 아닙니다. + * + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneLegacyBrowserLog(bool $dryRun): array + { + $path = $this->storagePath('logs'.DIRECTORY_SEPARATOR.'browser.log'); + + if (! File::exists($path)) { + return []; + } + + if (! $dryRun) { + File::delete($path); + } + + return [$path]; + } + + /** + * 같은 백업 집합에서 최신 1개를 제외하고 보존일 경과분을 삭제합니다. + * + * 최신 판정은 디렉토리명 타임스탬프(`Ymd_His` — 사전순 = 시간순) 기준입니다. + * + * @param list<array{path: string, stamp: string}> $entries 같은 identifier 의 백업 목록 + * @param int $backupCutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneBackupSet(array $entries, int $backupCutoff, bool $dryRun): array + { + if ($entries === []) { + return []; + } + + usort($entries, static fn (array $a, array $b): int => strcmp($b['stamp'], $a['stamp'])); + + // 최신 1개는 롤백 자산 — 나이와 무관하게 보존. + array_shift($entries); + + $pruned = []; + + foreach ($entries as $entry) { + if ($this->isFresh($entry['path'], $backupCutoff)) { + continue; + } + + $pruned[] = $entry['path']; + + if (! $dryRun) { + File::deleteDirectory($entry['path']); + } + } + + return $pruned; + } + + /** + * glob 패턴에 걸리는 항목(디렉토리·파일)을 보존일 기준으로 정리합니다. + * + * @param string $pattern glob 패턴 + * @param int $cutoff 이 시각(Unix time) 이전 mtime 만 삭제 + * @param bool $dryRun 삭제 없이 목록만 + * @return list<string> 삭제(예정) 경로 목록 + */ + private function pruneEntriesByAge(string $pattern, int $cutoff, bool $dryRun): array + { + $pruned = []; + + foreach (File::glob($pattern) ?: [] as $entry) { + if ($this->isFresh($entry, $cutoff)) { + continue; + } + + $pruned[] = $entry; + + if (! $dryRun) { + is_dir($entry) ? File::deleteDirectory($entry) : File::delete($entry); + } + } + + return $pruned; + } + + /** + * 항목의 mtime 이 보존일 이내인지 판정합니다 (mtime 조회 실패 시 보존 — fail-safe). + * + * @param string $path 대상 경로 + * @param int $cutoff 기준 시각 (Unix time) + * @return bool 보존일 이내(삭제 금지)면 true + */ + private function isFresh(string $path, int $cutoff): bool + { + $mtime = @filemtime($path); + + return $mtime === false || $mtime > $cutoff; + } + + /** + * 프로젝트 루트 기준 경로를 만듭니다. + * + * @param string $path 루트 기준 상대 경로 + * @return string 절대 경로 + */ + private function basePath(string $path): string + { + return ($this->baseRoot ?? base_path()).DIRECTORY_SEPARATOR.$path; + } + + /** + * 스토리지 루트 기준 경로를 만듭니다. + * + * @param string $path 스토리지 루트 기준 상대 경로 + * @return string 절대 경로 + */ + private function storagePath(string $path): string + { + return ($this->storageRoot ?? storage_path()).DIRECTORY_SEPARATOR.$path; + } +} diff --git a/app/Services/TemplateCustomTranslationService.php b/app/Services/TemplateCustomTranslationService.php index 66fed7ef..952c5c84 100644 --- a/app/Services/TemplateCustomTranslationService.php +++ b/app/Services/TemplateCustomTranslationService.php @@ -2,9 +2,9 @@ namespace App\Services; -use App\Contracts\Extension\CacheInterface; use App\Contracts\Repositories\TemplateCustomTranslationRepositoryInterface; use App\Exceptions\ConcurrentModificationException; +use App\Extension\Traits\ClearsTemplateCaches; use App\Models\TemplateCustomTranslation; use App\Services\LanguagePack\CustomTranslationUsageScanner; use Illuminate\Database\Eloquent\Collection; @@ -22,10 +22,12 @@ use Illuminate\Database\QueryException; * - seq 충돌 시 재시도 (unique 제약 위반 → seq 증가) * - 신규 키 생성 시 모든 활성 로케일에 폴백 시드 * - 낙관적 잠금 (`lock_version` — update 경로) - * - 커스텀 키 CRUD 시 다국어 캐시 무효화 + * - 커스텀 키 CRUD 시 다국어 캐시 무효화 */ class TemplateCustomTranslationService { + use ClearsTemplateCaches; + /** * seq 충돌 재시도 최대 횟수. */ @@ -33,11 +35,9 @@ class TemplateCustomTranslationService /** * @param TemplateCustomTranslationRepositoryInterface $repository 커스텀 다국어 키 리포지토리 - * @param CacheInterface $cache 캐시 드라이버 */ public function __construct( private readonly TemplateCustomTranslationRepositoryInterface $repository, - private readonly CacheInterface $cache, ) {} /** @@ -287,11 +287,12 @@ class TemplateCustomTranslationService * * `serveLanguage` 캐시 키가 `template.language.{id}.{locale}.v{cacheVersion}` * 형태로 `ext.cache_version` 을 포함하므로, 이 값을 갱신하면 다음 - * `loadTranslations` 가 재fetch 합니다. + * `loadTranslations` 가 재fetch 합니다. 쓰기는 트레이트 단일 지점 + * (고정 CoreCacheDriver + 메모이즈 스토어) 경유. */ private function invalidateLanguageCache(): void { - $this->cache->put('ext.cache_version', time()); + $this->incrementExtensionCacheVersion(); } /** diff --git a/app/Services/TemplateLayoutAttachmentService.php b/app/Services/TemplateLayoutAttachmentService.php index 3506d6d0..5630f0bd 100644 --- a/app/Services/TemplateLayoutAttachmentService.php +++ b/app/Services/TemplateLayoutAttachmentService.php @@ -5,6 +5,7 @@ namespace App\Services; use App\Contracts\Extension\StorageInterface; use App\Contracts\Repositories\TemplateLayoutAttachmentRepositoryInterface; use App\Contracts\Repositories\TemplateRepositoryInterface; +use App\Extension\HookManager; use App\Models\TemplateLayoutAttachment; use App\Support\ImageResizer; use Illuminate\Database\Eloquent\Collection; @@ -12,6 +13,7 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 템플릿 레이아웃 첨부 파일 서비스 @@ -52,6 +54,12 @@ class TemplateLayoutAttachmentService return ['success' => false, 'attachment' => null, 'url' => null, 'error' => 'template_not_found']; } + // 훅: 업로드 전 (확장 지점) + HookManager::doAction('core.template_layout_attachment.before_upload', $file, $templateIdentifier, $layoutName); + + // 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트) + $file = HookManager::applyFilters('core.template_layout_attachment.filter_upload_file', $file); + // 저장 경로 — 템플릿 식별자/날짜별 디렉토리 + UUID 파일명 (충돌 회피) $storedFilename = Str::uuid().'.'.$file->getClientOriginalExtension(); $relativePath = "{$templateIdentifier}/".date('Y/m/d')."/{$storedFilename}"; @@ -87,6 +95,8 @@ class TemplateLayoutAttachmentService 'path' => $relativePath, ]); + HookManager::doAction('core.template_layout_attachment.after_upload', $attachment); + return [ 'success' => true, 'attachment' => $attachment, @@ -160,17 +170,17 @@ class TemplateLayoutAttachmentService } /** - * 서빙 가능한 첨부 파일의 절대 경로를 돌려줍니다. + * 서빙 가능한 첨부의 인라인 스트림 응답과 캐싱 메타를 돌려줍니다. * - * 첨부가 주어진 템플릿 소속인지 검증하고, 스토리지에 파일이 실제로 존재하면 - * 컨트롤러가 스트림할 수 있는 절대 파일시스템 경로를 반환한다. 소속 불일치 / - * 파일 부재 시 null (컨트롤러가 404). + * 첨부가 주어진 템플릿 소속인지 검증하고, 행 disk 를 따르는 스토리지 스트림 + * 응답을 반환한다 (S3 등 원격 디스크 행 포함 — 로컬 절대 경로 방식은 #99 에서 + * filemtime 500). 소속 불일치 / 파일 부재 시 null (컨트롤러가 404). * * @param string $templateIdentifier 요청 경로의 템플릿 식별자 * @param TemplateLayoutAttachment $attachment 서빙 대상 첨부 - * @return string|null 절대 파일 경로 또는 null + * @return array{response: StreamedResponse, etag_source: string}|null 서빙 정보 또는 null */ - public function getServableFilePath(string $templateIdentifier, TemplateLayoutAttachment $attachment): ?string + public function getServableResponse(string $templateIdentifier, TemplateLayoutAttachment $attachment): ?array { $template = $this->templateRepository->findByIdentifier($templateIdentifier); // 경로 식별자와 첨부의 소속 템플릿이 일치해야 한다 (교차 템플릿 접근 차단). @@ -178,12 +188,31 @@ class TemplateLayoutAttachmentService return null; } - $driver = $this->storage->withDisk($attachment->disk); - if (! $driver->exists(self::STORAGE_CATEGORY, $attachment->path)) { + // 행 disk 기준 인라인 스트림 (존재 검사는 response() 내부에서 수행). + // 로컬 절대 경로 조립(getBasePath)은 S3 등 원격 디스크 행에서 성립하지 않는다 (#99). + $response = $this->storage->withDisk($attachment->disk)->response( + self::STORAGE_CATEGORY, + $attachment->path, + $attachment->original_name ?? basename($attachment->path), + [ + 'Content-Type' => $attachment->mime_type, + 'Content-Length' => (string) $attachment->size, + ] + ); + + if (! $response) { return null; } - // 절대 경로 — {category base}/{relative path}. fileResponse 가 절대 경로를 요구. - return rtrim($driver->getBasePath(self::STORAGE_CATEGORY), '/\\').'/'.$attachment->path; + return [ + 'response' => $response, + // 파일 stat 없이 결정적인 ETag 소스 (업로드 파일은 경로당 불변) + 'etag_source' => implode('|', [ + $attachment->disk, + $attachment->path, + (string) $attachment->updated_at?->getTimestamp(), + (string) $attachment->size, + ]), + ]; } } diff --git a/app/Services/TemplateService.php b/app/Services/TemplateService.php index 0e73bd19..658c913d 100644 --- a/app/Services/TemplateService.php +++ b/app/Services/TemplateService.php @@ -36,8 +36,12 @@ class TemplateService private PluginManagerInterface $pluginManager, private LayoutVersionRepositoryInterface $layoutVersionRepository ) { - // TemplateManager 초기화 (템플릿 스캔) - $this->templateManager->loadTemplates(); + // TemplateManager 초기화 — 아직 로드되지 않았을 때만 스캔한다. + // 무조건 loadTemplates() 를 부르면 공유 싱글톤의 템플릿 맵을 리셋한 뒤 디렉토리를 + // 통째로 재스캔하므로, 이 서비스가 주입될 때마다 풀스캔과 상태 변형이 반복된다. + // (웹/serve/test 는 CoreServiceProvider::boot 가 로드를 보장하지만, 그 외 콘솔 경로는 + // 로딩을 건너뛰므로 이 초기화 자체를 없앨 수는 없다) + $this->templateManager->ensureLoaded(); } /** @@ -1213,6 +1217,11 @@ class TemplateService */ public function getEditorRoutesDataWithModules(string $identifier): array { + // 열화 판정은 이 호출의 병합 결과만 가리켜야 한다. 이 서비스는 공유 인스턴스라 + // 리셋하지 않으면 직전 호출(업데이트 스왑 창)의 판정이 인스턴스에 눌어붙어, + // 모듈 디렉토리가 복구된 뒤의 병합까지 열화로 보고된다. + $this->routeMergeDegraded = false; + // 1. routes.json 경로 — 활성 디렉토리 우선, _bundled 폴백 (활성/비활성 무관). $candidates = [ base_path("templates/{$identifier}/routes.json"), diff --git a/app/Services/UserService.php b/app/Services/UserService.php index a4444465..4a398929 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -6,17 +6,22 @@ use App\Contracts\Repositories\RoleRepositoryInterface; use App\Contracts\Repositories\UserRepositoryInterface; use App\Enums\UserStatus; use App\Exceptions\CannotDeleteSuperAdminException; +use App\Exceptions\PermissionEscalationException; use App\Extension\HookManager; use App\Helpers\PermissionHelper; use App\Helpers\TimezoneHelper; use App\Models\ActivityLog; +use App\Models\Attachment; use App\Models\User; +use App\Support\PermissionEscalationGuard; +use App\Support\UserGradeGuard; use Exception; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; class UserService @@ -24,7 +29,8 @@ class UserService public function __construct( private UserRepositoryInterface $userRepository, private RoleRepositoryInterface $roleRepository, - private AttachmentService $attachmentService + private AttachmentService $attachmentService, + private PermissionEscalationGuard $escalationGuard ) {} /** @@ -77,10 +83,19 @@ class UserService } unset($data['roles']); - // 역할 할당 권한 체크: core.permissions.update 권한 없으면 기본 역할 자동 할당 - if (! PermissionHelper::check('core.permissions.update')) { + // 역할 할당: 요청 역할이 없으면 기본 역할('user')을 자동 배정하고, 요청 역할이 + // 있으면 액터 상한(ceiling) 검사만 받는다. + // + // 역할 부여는 "사용자 관리"(core.users.create — 이 경로는 라우트에서 이미 강제됨)의 + // 일부이지, "역할 정의 수정"(core.permissions.update — 역할에 권한을 가감하는 권한)을 + // 요구하지 않는다. 권한 상승 방지는 오직 상한(ceiling)이 담당한다: 액터가 보유하지 + // 않았거나 자신의 범위보다 넓은 권한을 담은 역할은 부여할 수 없다(KVE-2026-1919). + // 신규 사용자는 기존 역할이 없으므로 요청 역할 전부가 새로 부여되는 역할이다. + if ($roleIds === null || count($roleIds) === 0) { $defaultRoleId = $this->roleRepository->findByIdentifier('user')?->id; $roleIds = $defaultRoleId ? [$defaultRoleId] : null; + } else { + $this->escalationGuard->assertRoleAssignmentWithinActorCeiling($roleIds); } $user = $this->userRepository->create($data); @@ -100,8 +115,16 @@ class UserService throw $e; } + // 권한 상승 상한 위반은 그대로 전파해 컨트롤러가 403 으로 매핑하도록 한다. + if ($e instanceof PermissionEscalationException) { + throw $e; + } + + // 원본 예외는 로그로만 남기고, 사용자 응답에는 원문을 싣지 않는다. + Log::error('User create failed', ['exception' => $e]); + throw ValidationException::withMessages([ - 'general' => [__('user.create_failed', ['error' => $e->getMessage()])], + 'general' => [__('user.create_failed')], ]); } } @@ -117,6 +140,10 @@ class UserService */ public function updateUser(User $user, array $data): User { + // 등급 상한 가드: 비-슈퍼관리자 액터는 슈퍼 관리자 계정을 수정할 수 없다. + // (비밀번호·status(withdrawn/blocked)·email 등 모든 수정 경로를 한 지점에서 차단) + UserGradeGuard::assertActorMayModify($user); + try { // 원본 데이터 보관 (after_update 훅에서 사용) $originalData = $data; @@ -146,17 +173,35 @@ class UserService } unset($data['roles']); - // 역할 할당 권한 체크 + // 역할 할당 if ($roleIds !== null) { $authUser = Auth::user(); - // core.permissions.update 권한 없으면 역할 변경 불가 - if (! PermissionHelper::check('core.permissions.update', $authUser)) { - $roleIds = null; + // 역할 조작 상한(ceiling): 이번 변경으로 붙거나 떨어지는 역할이 액터가 전부 + // 부여할 수 있는 권한만 담고 있는지 확인한다(KVE-2026-1919). 역할 부여는 + // "사용자 관리"(core.users.update — 이 경로는 라우트에서 이미 강제됨)의 일부이며, + // "역할 정의 수정"(core.permissions.update — 역할에 권한을 가감하는 권한)을 + // 요구하지 않는다. 권한 상승 방지는 오직 상한이 담당한다: 액터가 보유하지 + // 않았거나 자신의 범위보다 넓은 권한을 담은 역할은 부여할 수 없고, 위반 시 + // PermissionEscalationException 이 전파되어 403 으로 명시 거부된다(과거처럼 조용히 + // 무시하지 않는다). + // + // 검사 대상은 **추가·제거 양방향의 변경분**이다. 추가는 그 역할의 권한을 + // 부여하는 것이고, 제거는 상위 역할의 권한 구성을 박탈하는 하향 조작이므로 — + // 액터가 스스로 부여할 수 없는(상한 밖) 역할은 붙이지도 떼지도 못한다. 추가만 + // 검사하면 core.users.update 만 가진 하위 관리자가 다른 관리자의 상위 역할을 + // 박탈하는 경로가 상한 없이 뚫린다. 기존 유지 역할은 변경이 아니므로 제외한다. + $currentRoleIds = $user->roles->pluck('id')->all(); + $changedRoleIds = array_values(array_unique(array_merge( + array_diff($roleIds, $currentRoleIds), // 추가되는 역할 + array_diff($currentRoleIds, $roleIds), // 제거되는 역할 + ))); + if (! empty($changedRoleIds)) { + $this->escalationGuard->assertRoleAssignmentWithinActorCeiling($changedRoleIds); } // 자기잠금 방지: 마지막 admin 역할 사용자가 자기 admin 역할을 제거하려는 경우 차단 - if ($roleIds !== null && $authUser && $authUser->id === $user->id) { + if ($authUser && $authUser->id === $user->id) { $adminRole = $this->roleRepository->findByIdentifier('admin'); if ($adminRole && $user->roles->contains('id', $adminRole->id) && ! in_array($adminRole->id, $roleIds)) { // admin 역할을 가진 다른 사용자가 있는지 확인 @@ -170,6 +215,23 @@ class UserService } } + // 관리자가 상태만 '탈퇴'로 바꾸는 경로도 정식 탈퇴 로직으로 통일한다. + // + // 여기서 status/withdrawn_at 을 먼저 저장하면 withdraw() 의 멱등 가드가 + // 이미 탈퇴한 계정으로 판정해 익명화와 훅이 조용히 생략된다. + // 따라서 상태 필드는 제거하고, 다른 필드 갱신을 마친 뒤 withdrawUser() 에 위임한다. + $withdrawViaStatus = isset($data['status']) + && $data['status'] === UserStatus::Withdrawn->value + && ! $user->isWithdrawn(); + + if ($withdrawViaStatus) { + unset($data['status']); + + // 차단 대상(관리자/수퍼관리자)이면 다른 필드도 저장되지 않아야 하므로 + // 갱신 전에 가드를 평가한다. + $this->assertWithdrawable($user); + } + // 상태 변경 감지 및 타임스탬프 자동 설정 $oldStatus = $user->status; $newStatus = $data['status'] ?? null; @@ -178,7 +240,8 @@ class UserService $newStatusEnum = UserStatus::from($newStatus); $data = match ($newStatusEnum) { UserStatus::Blocked => array_merge($data, ['blocked_at' => now()]), - UserStatus::Withdrawn => array_merge($data, ['withdrawn_at' => now()]), + // 탈퇴는 위 위임 경로(withdrawUser)가 전담하므로 여기로는 도달하지 않는다. + UserStatus::Withdrawn => $data, UserStatus::Active => array_merge($data, ['blocked_at' => null, 'withdrawn_at' => null]), UserStatus::Inactive => $data, }; @@ -187,22 +250,45 @@ class UserService // 스냅샷 캡처 (ChangeDetector용) $snapshot = $user->toArray(); - $this->userRepository->update($user, $data); + // 탈퇴 위임 시 아바타는 커밋 후에 지운다 (트랜잭션 안에서 지우면 롤백돼도 파일은 안 돌아온다). + $avatarAttachment = $withdrawViaStatus ? $user->avatarAttachment : null; - // 상태가 Active 외로 변경되었으면 토큰 삭제 (즉시 로그아웃) - if ($newStatus && $newStatus !== $oldStatus && $newStatus !== UserStatus::Active->value) { - $user->tokens()->delete(); - } + // 프로필 갱신·토큰 정리·역할 동기화가 서로 어긋나지 않도록 하나로 묶는다. + // + // 상태 변경 경유 탈퇴는 **탈퇴까지 같은 트랜잭션**에 넣는다 — 프로필 갱신을 + // 먼저 커밋하고 탈퇴를 따로 커밋하면, 탈퇴가 실패했을 때 이름·이메일 변경만 + // 남아 "전부 성공하거나 전부 취소" 가 그 경로에서만 깨진다. + DB::transaction(function () use ($user, $data, $newStatus, $oldStatus, $roleIds, $withdrawViaStatus) { + $this->userRepository->update($user, $data); - // 역할 동기화 + // 상태가 Active 외로 변경되었으면 토큰 삭제 (즉시 로그아웃) + if ($newStatus && $newStatus !== $oldStatus && $newStatus !== UserStatus::Active->value) { + $user->tokens()->delete(); + } + + // 역할 동기화 + if ($roleIds !== null) { + $user->roles()->sync($roleIds); + } + + // 익명화 + before_withdraw 훅 + 연계 데이터 정리 (본인 탈퇴와 동일 코드) + if ($withdrawViaStatus) { + $this->performWithdrawWrites($user); + } + }); + + // 커밋 후 부수효과 (캐시는 롤백 대상이 아니다) if ($roleIds !== null) { - $user->roles()->sync($roleIds); $user->flushPermissionCaches(); } // After 훅: 사용자 객체와 원본 데이터, 스냅샷 전달 HookManager::doAction('core.user.after_update', $user, $originalData, $snapshot); + if ($withdrawViaStatus) { + $this->runWithdrawAfterEffects($user, $avatarAttachment); + } + // 비밀번호 변경 시 알림 훅 발화 — 발송은 NotificationHookListener가 처리 if ($passwordChanged) { HookManager::doAction('core.auth.after_password_changed', $user); @@ -214,12 +300,102 @@ class UserService throw $e; } + if ($e instanceof CannotDeleteSuperAdminException) { + throw $e; + } + + // 권한 상승 상한 위반은 그대로 전파해 컨트롤러가 403 으로 매핑하도록 한다 + // (일반 실패로 감싸면 422 로 잘못 내려간다). + if ($e instanceof PermissionEscalationException) { + throw $e; + } + + // 원본 예외는 로그로만 남기고, 사용자 응답에는 원문을 싣지 않는다. + Log::error('User update failed', ['user_id' => $user->id, 'exception' => $e]); + throw ValidationException::withMessages([ - 'general' => [__('user.update_failed', ['error' => $e->getMessage()])], + 'general' => [__('user.update_failed')], ]); } } + /** + * 탈퇴 가능한 계정인지 검사합니다. + * + * 탈퇴 경로(본인 탈퇴 / 관리자 상태 변경 / 일괄 상태 변경)가 모두 같은 기준을 + * 쓰도록 한 곳에 둡니다 — 한쪽만 느슨하면 그 경로가 우회로가 됩니다. + * + * @param User $user 검사 대상 사용자 + * + * @throws CannotDeleteSuperAdminException 슈퍼 관리자인 경우 + * @throws ValidationException 관리자 역할을 가진 경우 + */ + private function assertWithdrawable(User $user): void + { + // 슈퍼 관리자는 탈퇴 불가 + if ($user->isSuperAdmin()) { + throw new CannotDeleteSuperAdminException; + } + + // 관리자 역할을 가진 계정은 탈퇴 방지 + if ($user->isAdmin()) { + throw ValidationException::withMessages([ + 'general' => [__('user.withdraw_admin_forbidden')], + ]); + } + } + + /** + * 탈퇴의 DB 변경만 수행합니다 (호출자가 연 트랜잭션 안에서 실행). + * + * 관리자가 상태 변경으로 탈퇴시키는 경로는 프로필 갱신과 탈퇴를 **한 트랜잭션**으로 + * 묶어야 한다 — 두 번에 나눠 커밋하면 탈퇴가 실패했을 때 프로필 변경만 남는다. + * 그래서 DB 변경분을 이 메서드로 떼어 두 경로가 같은 코드를 공유한다. + * + * @param User $user 탈퇴 처리할 사용자 모델 + * @return bool 탈퇴 처리 성공 여부 + */ + private function performWithdrawWrites(User $user): bool + { + // 훅 실행 (탈퇴 전) + HookManager::doAction('core.user.before_withdraw', $user); + + // 약관 동의 이력 삭제 (명시적 삭제 - CASCADE 의존 금지) + $user->consents()->delete(); + + // 토큰 삭제 (로그아웃 처리) + $user->tokens()->delete(); + + // 탈퇴 처리 (suffix 추가 및 상태 변경) + return $user->withdraw(); + } + + /** + * 탈퇴 커밋 후 부수효과를 수행합니다. + * + * @param User $user 탈퇴 처리된 사용자 모델 + * @param Attachment|null $avatarAttachment 탈퇴 전 캡처한 아바타 첨부 + */ + private function runWithdrawAfterEffects(User $user, $avatarAttachment): void + { + // 아바타 파일 삭제는 실패해도 탈퇴를 되돌리지 않는다 + // (고아 파일은 비파괴이고, 탈퇴를 되살리는 쪽이 훨씬 해롭다). + if ($avatarAttachment) { + try { + $this->attachmentService->delete($avatarAttachment->id); + } catch (Exception $e) { + Log::warning('탈퇴 아바타 삭제 실패', [ + 'user_id' => $user->id, + 'attachment_id' => $avatarAttachment->id, + 'error' => $e->getMessage(), + ]); + } + } + + // 훅 실행 (탈퇴 후) + HookManager::doAction('core.user.after_withdraw', $user); + } + /** * 사용자를 탈퇴 처리합니다. * @@ -235,39 +411,19 @@ class UserService public function withdrawUser(User $user): bool { try { - // 슈퍼 관리자는 탈퇴 불가 - if ($user->isSuperAdmin()) { - throw new CannotDeleteSuperAdminException; - } + // 슈퍼 관리자 / 관리자 탈퇴 차단 (전 탈퇴 경로 공통 기준) + $this->assertWithdrawable($user); - // 관리자 역할을 가진 계정은 탈퇴 방지 - if ($user->isAdmin()) { - throw ValidationException::withMessages([ - 'general' => [__('user.withdraw_admin_forbidden')], - ]); - } - - // 훅 실행 (탈퇴 전) - HookManager::doAction('core.user.before_withdraw', $user); - - // 약관 동의 이력 삭제 (명시적 삭제 - CASCADE 의존 금지) - $user->consents()->delete(); - - // 아바타 Attachment 삭제 (다형성 관계) + // 아바타는 파일 삭제를 동반하므로 트랜잭션 밖(커밋 후)에서 처리한다 — + // 트랜잭션 안에서 지우면 롤백되어도 파일은 돌아오지 않는다. $avatarAttachment = $user->avatarAttachment; - if ($avatarAttachment) { - $this->attachmentService->delete($avatarAttachment->id); - } - // 토큰 삭제 (로그아웃 처리) - $user->tokens()->delete(); + // DB 변경은 전부 성공하거나 전부 취소된다 (공개이슈 #112). + // 마지막 단계인 withdraw() 가 실패하면 동의 이력·토큰 삭제도 함께 되돌아간다. + $result = DB::transaction(fn () => $this->performWithdrawWrites($user)); - // 탈퇴 처리 (suffix 추가 및 상태 변경) - $result = $user->withdraw(); - - // 훅 실행 (탈퇴 후) if ($result) { - HookManager::doAction('core.user.after_withdraw', $user); + $this->runWithdrawAfterEffects($user, $avatarAttachment); } return $result; @@ -280,8 +436,11 @@ class UserService throw $e; } + // 원본 예외는 로그로만 남기고, 사용자 응답에는 원문(SQL 상태코드·경로 등)을 싣지 않는다. + Log::error('User withdraw failed', ['user_id' => $user->id, 'exception' => $e]); + throw ValidationException::withMessages([ - 'general' => [__('user.withdraw_failed', ['error' => $e->getMessage()])], + 'general' => [__('user.withdraw_failed')], ]); } } @@ -310,27 +469,42 @@ class UserService // 삭제 전 사용자 데이터 보관 (after_delete 훅에서 사용) $userData = $user->only(['id', 'uuid', 'name', 'email']); - // Before 훅 - HookManager::doAction('core.user.before_delete', $user); + // 아바타는 파일 삭제를 동반하므로 커밋 후에 처리한다 (롤백되어도 파일은 안 돌아온다). + $avatarAttachment = $user->avatarAttachment; - // 역할 연결 해제 (명시적 삭제 - CASCADE 의존 금지) - $user->roles()->detach(); + // 마지막 delete 가 FK 등으로 실패하면 역할·동의·토큰만 사라지고 계정은 남는 + // 껍데기 활성 계정이 된다 (실회귀 이력 존재). 전 단계를 하나로 묶는다. + $result = DB::transaction(function () use ($user) { + // Before 훅 + HookManager::doAction('core.user.before_delete', $user); + + // 역할 연결 해제 (명시적 삭제 - CASCADE 의존 금지) + $user->roles()->detach(); + + // 약관 동의 이력 삭제 + $user->consents()->delete(); + + // API 토큰 삭제 + $user->tokens()->delete(); + + return $this->userRepository->delete($user); + }); + + // 커밋 성공 후 부수효과 $user->flushPermissionCaches(); - // 약관 동의 이력 삭제 - $user->consents()->delete(); - - // API 토큰 삭제 - $user->tokens()->delete(); - - // 아바타 Attachment 삭제 (다형성 관계) - $avatarAttachment = $user->avatarAttachment; if ($avatarAttachment) { - $this->attachmentService->delete($avatarAttachment->id); + try { + $this->attachmentService->delete($avatarAttachment->id); + } catch (Exception $e) { + Log::warning('사용자 삭제 아바타 정리 실패', [ + 'user_id' => $userData['id'] ?? null, + 'attachment_id' => $avatarAttachment->id, + 'error' => $e->getMessage(), + ]); + } } - $result = $this->userRepository->delete($user); - // After 훅: 삭제된 사용자 데이터 전달 HookManager::doAction('core.user.after_delete', $userData); @@ -344,8 +518,11 @@ class UserService throw $e; } + // 원본 예외는 로그로만 남기고, 사용자 응답에는 원문을 싣지 않는다. + Log::error('User delete failed', ['user_id' => $user->id, 'exception' => $e]); + throw ValidationException::withMessages([ - 'general' => [__('user.delete_failed', ['error' => $e->getMessage()])], + 'general' => [__('user.delete_failed')], ]); } } @@ -470,22 +647,6 @@ class UserService return $excludeUserUuid && $user->uuid === $excludeUserUuid; } - /** - * 사용자 활성화 상태를 업데이트합니다. - * 현재는 구현되지 않음 - 필요시 확장 가능 - * - * @param User $user 대상 사용자 모델 - * @param bool $isActive 활성화 상태 - * @return User 사용자 모델 - */ - public function updateUserStatus(User $user, bool $isActive): User - { - // 현재 User 모델에 is_active 필드가 없으므로 필요시 추가 - // $this->userRepository->update($user, ['is_active' => $isActive]); - - return $user; - } - /** * 사용자의 활동 로그를 조회합니다. * @@ -521,6 +682,9 @@ class UserService */ public function unlockAccount(User $user): User { + // 등급 상한 가드: 비-슈퍼관리자 액터는 슈퍼 관리자 계정을 조작할 수 없다(정합성). + UserGradeGuard::assertActorMayModify($user); + HookManager::doAction('core.user.before_unlock', $user); $this->userRepository->resetLoginAttempts($user); @@ -534,19 +698,6 @@ class UserService return $user; } - /** - * 사용자의 마지막 로그인 시간을 현재 시간으로 업데이트합니다. - * - * @param User $user 대상 사용자 모델 - * @return User 업데이트된 사용자 모델 - */ - public function updateLastLogin(User $user): User - { - $this->userRepository->update($user, ['last_login_at' => now()]); - - return $user->fresh(); - } - /** * 사용자의 언어 설정을 업데이트합니다. * @@ -579,8 +730,42 @@ class UserService $statusEnum = UserStatus::from($status); - // UUID → 정수 ID 변환 (내부 쿼리용) - $userIds = $this->userRepository->getIdsByUuids($uuids); + // 정적 라우트(`PATCH users/bulk-status`)는 라우트 모델이 없어 PermissionMiddleware 의 + // 스코프 검사가 통째로 건너뛰어진다. 따라서 상세 경로(`PUT users/{user}`)가 미들웨어로 + // 강제하는 두 축을 서비스 계층에서 재적용한다 — 어느 한 축만 막으면 나머지가 우회로다. + // 탈퇴 분기보다 먼저 적용해야 한다 — 뒤에 두면 탈퇴 경로가 스코프 검사를 건너뛰는 + // 우회로가 된다(KVE-1919). + $targets = $this->userRepository->findManyByUuids($uuids); + + // ① 등급 축: 비-슈퍼관리자 액터가 포함시킨 슈퍼 관리자 대상은 제외한다 + // (슈퍼 관리자 무력화 차단 — 슈퍼 세션 유지). + $modifiable = UserGradeGuard::filterModifiable($targets); + + // ② 스코프 축: 액터의 유효 스코프(self/role/글로벌) 밖 대상은 제외한다. + // 판정은 상세 경로와 동일한 SSoT(PermissionHelper::checkScopeAccess)에 위임한다 — + // 여기서 재구현하면 role 분기만 빠지는 식으로 두 경로의 강도가 갈린다. + $modifiable = PermissionHelper::filterByScope($modifiable, 'core.users.update'); + + // 일괄 '탈퇴'는 건별 정식 탈퇴로 전환한다 — 상태 컬럼만 바꾸면 익명화와 + // before/after_withdraw 훅이 통째로 생략되어, 본인 탈퇴와 결과가 달라진다. + // 위에서 등급·스코프로 걸러낸 대상에 한해서만 수행한다. + if ($statusEnum === UserStatus::Withdrawn) { + $modifiableUuids = array_map(static fn (User $u): string => $u->uuid, $modifiable); + + return $this->bulkWithdraw($modifiableUuids, $status); + } + + $userIds = array_map(static fn (User $u): int => $u->id, $modifiable); + + if (empty($userIds)) { + HookManager::doAction('sirsoft-core.user.after_bulk_update', $uuids, $status, 0); + + return [ + 'updated_count' => 0, + 'failed_count' => 0, + 'failed_reasons' => [], + ]; + } // DB 트랜잭션으로 일괄 업데이트 $updatedCount = DB::transaction(function () use ($userIds, $statusEnum) { @@ -588,7 +773,8 @@ class UserService $updateData = ['status' => $statusEnum->value]; $updateData = match ($statusEnum) { UserStatus::Blocked => array_merge($updateData, ['blocked_at' => now()]), - UserStatus::Withdrawn => array_merge($updateData, ['withdrawn_at' => now()]), + // 탈퇴는 위에서 건별 withdrawUser() 로 분기하므로 여기로는 도달하지 않는다. + UserStatus::Withdrawn => $updateData, UserStatus::Active => array_merge($updateData, ['blocked_at' => null, 'withdrawn_at' => null]), UserStatus::Inactive => $updateData, }; @@ -608,6 +794,83 @@ class UserService return [ 'updated_count' => $updatedCount, + 'failed_count' => 0, + 'failed_reasons' => [], + ]; + } + + /** + * 탈퇴 실패 예외를 운영자에게 보여줄 한 줄 사유로 바꿉니다. + * + * @param Exception $e 탈퇴 시도에서 발생한 예외 + * @return string 운영자에게 보여줄 실패 사유 + */ + private function describeWithdrawFailure(Exception $e): string + { + if ($e instanceof ValidationException) { + $first = collect($e->errors())->flatten()->first(); + + if (is_string($first) && $first !== '') { + return $first; + } + } + + if ($e instanceof CannotDeleteSuperAdminException) { + return __('exceptions.cannot_delete_super_admin'); + } + + // 알 수 없는 예외의 원문 메시지는 사유로 쓰지 않는다 — 내부 사정(SQL·경로·클래스명)이 + // 운영자 화면으로 새고, 화면 문구 조립에서 파라미터 구분자로 쓰이는 문자(`|` `&` `=`)가 + // 섞이면 안내가 잘린다. 사유는 언어 파일이 소유하는 문구로만 구성한다. + return __('user.withdraw_failed_unknown'); + } + + /** + * 일괄 탈퇴를 건별 정식 탈퇴로 수행합니다. + * + * @param array<int, string> $uuids 대상 사용자 UUID 배열 + * @param string $status 요청 상태값 (훅 전달용) + * @return array{updated_count: int, failed_count: int, failed_reasons: array<int, string>} 처리 결과 + */ + private function bulkWithdraw(array $uuids, string $status): array + { + $updatedCount = 0; + $failedCount = 0; + $failedReasons = []; + + foreach ($uuids as $uuid) { + $user = $this->userRepository->findByUuid($uuid); + + if (! $user) { + $failedCount++; + $failedReasons[] = __('user.not_found'); + + continue; + } + + try { + $this->withdrawUser($user); + $updatedCount++; + } catch (Exception $e) { + $failedCount++; + $failedReasons[] = $this->describeWithdrawFailure($e); + + Log::warning('일괄 탈퇴 건 실패', [ + 'uuid' => $uuid, + 'error' => $e->getMessage(), + ]); + } + } + + // after_bulk_update 훅 실행 + HookManager::doAction('sirsoft-core.user.after_bulk_update', $uuids, $status, $updatedCount); + + // 사유를 함께 돌려주지 않으면 화면은 "몇 건 실패" 까지만 말할 수 있다 — + // 운영자는 왜 안 됐는지 알 방법이 없어 같은 조작을 반복하게 된다. + return [ + 'updated_count' => $updatedCount, + 'failed_count' => $failedCount, + 'failed_reasons' => array_values(array_unique($failedReasons)), ]; } diff --git a/app/Support/ApiDoc/ApiDocScaffolder.php b/app/Support/ApiDoc/ApiDocScaffolder.php index e17ecc4d..724f97e7 100644 --- a/app/Support/ApiDoc/ApiDocScaffolder.php +++ b/app/Support/ApiDoc/ApiDocScaffolder.php @@ -1138,7 +1138,8 @@ class ApiDocScaffolder // 종료 마커를 표의 끝 경계로 삼는데, 전자는 그 마커를 잘라내고 돌려준다. $merged .= $this->applyPreservedErrorTable( $withErrors, - $this->exactGeneratedBlock($existing, $key) + $this->exactGeneratedBlock($existing, $key), + $section )."\n"; } @@ -1252,7 +1253,44 @@ class ApiDocScaffolder * @param string|null $previous 기존 문서의 같은 엔드포인트 생성 블록 * @return string 에러 표가 보존된 섹션 */ - private function applyPreservedErrorTable(string $section, ?string $previous): string + /** + * 403 행의 요구 권한 식별자를 이번 재생성 산출값으로 갱신합니다. + * + * 에러 표 병합은 같은 상태코드에서 기존(사람) 행을 이기게 두는데, 403 의 권한 식별자는 + * 라우트 정의에서 파생된 사실이라 사람 서술과 같은 취급을 하면 안 된다. 두 행이 모두 + * 백틱 식별자를 가질 때만 그 부분을 치환하고, 나머지 문구(사람이 덧붙인 도메인 조건)는 + * 건드리지 않는다. 식별자가 없는 형태(관리자 게이트만 걸린 라우트)는 갱신 대상이 아니다. + * + * @param string $preserved 병합 결과로 살아남은 기존 403 행 + * @param string $generated 이번 재생성이 만든 403 행 + * @return string 식별자만 갱신된 403 행 + */ + private function refreshPermissionIdentifier(string $preserved, string $generated): string + { + if (! preg_match('/`([^`]+)`/', $generated, $new)) { + return $preserved; + } + + if (! preg_match('/`([^`]+)`/', $preserved, $old)) { + return $preserved; + } + + if ($old[1] === $new[1]) { + return $preserved; + } + + $needle = '`'.$old[1].'`'; + $pos = strpos($preserved, $needle); + + if ($pos === false) { + return $preserved; + } + + // preg_replace 는 대체 문자열의 `$`·`\` 를 역참조로 해석하므로 쓰지 않는다. + return substr_replace($preserved, '`'.$new[1].'`', $pos, strlen($needle)); + } + + private function applyPreservedErrorTable(string $section, ?string $previous, ?string $generated = null): string { if ($previous === null) { return $section; @@ -1285,12 +1323,28 @@ class ApiDocScaffolder // 상태코드 키로 병합. `+` 는 왼쪽 우선이므로 기존 행을 먼저 둬서, 같은 상태코드면 // 사람이 쓴 구체적 조건이 자동 문구를 이긴다. 자동 추론에만 있는 상태코드는 새로 편입된다. - $merged = $previousRows + $this->errorTableRows($section); + $currentRows = $this->errorTableRows($section); + $merged = $previousRows + $currentRows; if ($merged === []) { return $section; } + // 403 행의 권한 식별자만은 예외다 — 그것은 사람 서술이 아니라 라우트에서 파생된 + // 사실이라, 위 병합 규칙을 그대로 두면 라우트의 요구 권한을 바꿔도 옛 식별자가 + // 영구히 남아 문서가 조용히 틀린 권한을 안내한다. 사람이 보강한 조건 문구는 그대로 + // 두고 백틱 식별자만 갱신한다. + // + // 대조 원본은 반드시 **이번 회차가 생성한 원본 섹션**이어야 한다. 여기 들어오는 + // $section 은 restoreTableDescriptions 를 이미 거쳐 403 행의 설명 셀이 기존 문서 + // 값으로 되돌아가 있으므로(에러 표도 상태코드를 행 키로 갖는 표라 그 복원 대상에 + // 걸린다), 그것을 기준으로 삼으면 갱신이 언제나 no-op 이 된다. + $generatedRows = $generated === null ? $currentRows : $this->errorTableRows($generated); + + if (isset($merged['403'], $generatedRows['403'])) { + $merged['403'] = $this->refreshPermissionIdentifier($merged['403'], $generatedRows['403']); + } + ksort($merged, SORT_NUMERIC); $table = "| 상태코드 | 의미 | 발생 조건 |\n| --- | --- | --- |\n".implode("\n", $merged); diff --git a/app/Support/ApiDoc/ParameterDescriber.php b/app/Support/ApiDoc/ParameterDescriber.php index 42837901..8ce26e7e 100644 --- a/app/Support/ApiDoc/ParameterDescriber.php +++ b/app/Support/ApiDoc/ParameterDescriber.php @@ -168,6 +168,7 @@ class ParameterDescriber 'port' => '포트 번호', 'encryption' => '전송 암호화 방식 (tls/ssl)', 'storage_driver' => '스토리지 드라이버 (local/s3)', + 'public_asset_disk' => '공개 자산 직접 URL 서빙 디스크 (none/public/s3 + 플러그인 등록 디스크)', 'cache_driver' => '캐시 드라이버 (file/redis/memcached)', 'session_driver' => '세션 드라이버 (file/database/redis)', 'queue_driver' => '큐 드라이버 (sync/database/redis)', @@ -178,10 +179,12 @@ class ParameterDescriber 'memcached_host' => 'Memcached 호스트 주소', 'memcached_port' => 'Memcached 포트 번호', 's3_bucket' => 'S3 버킷명', - 's3_region' => 'S3 리전', + 's3_region' => 'S3 리전 (소문자 영숫자·하이픈 — AWS 리전 코드 또는 S3 호환 스토리지 값, R2 는 auto)', 's3_access_key' => 'S3 액세스 키', 's3_secret_key' => 'S3 시크릿 키', - 's3_url' => 'S3 엔드포인트 URL', + 's3_url' => 'S3 공개 URL(CDN) base — 파일 URL 생성용 (API 요청 주소 아님)', + 's3_endpoint' => 'S3 API 엔드포인트 — S3 호환 스토리지(R2/MinIO 등)용, AWS S3 는 미입력', + 's3_use_path_style' => 'S3 path-style 주소 사용 여부 (MinIO 등)', 'ses_key' => 'SES 액세스 키', 'ses_secret' => 'SES 시크릿 키', 'ses_region' => 'SES 리전', diff --git a/app/Support/ConfigCacheHelper.php b/app/Support/ConfigCacheHelper.php index 4a4af013..ff5d34bd 100644 --- a/app/Support/ConfigCacheHelper.php +++ b/app/Support/ConfigCacheHelper.php @@ -16,9 +16,13 @@ use Illuminate\Support\Facades\Log; * 한 번 비워진 뒤 재생성되지 않아 성능 이점이 영구히 사라진다 — 이를 단일 SSoT 로 막는다. * * 정책: 환경 무관 항상 재생성. config:cache 는 그 자체로 부팅 비용을 - * 절감하고, G7 설정은 config 캐시에 박제되지 않고 매 요청 SettingsServiceProvider / - * CoreServiceProvider 의 런타임 Config::set() 으로 재주입되므로(설정 stale 없음), - * 항상 켜두는 것이 이득이다. local 개발 시 config/*.php 수정이 즉시 반영되지 않는 점은 + * 절감하고, G7 설정은 config 캐시에 박제되지 않고 부팅 때 SettingsServiceProvider / + * CoreServiceProvider 의 런타임 Config::set() 으로 재주입되므로 항상 켜두는 것이 이득이다. + * + * 주의: "매 요청 재주입되므로 stale 없음" 은 FPM 전제였다. 큐 워커·schedule:work·Reverb + * 처럼 프로세스가 상주하는 환경에서는 부팅이 한 번뿐이라 저장 후에도 옛 값이 남는다. + * 그래서 저장 경로가 `ExtensionSettingsMirror` 로 in-memory 미러를 직접 다시 채운다 + * (docs/backend/admin-settings-access.md "config 미러 갱신 시점" 참조). local 개발 시 config/*.php 수정이 즉시 반영되지 않는 점은 * 개발자가 `php artisan config:clear` 로 대응하는 개발자 책임 영역이다. */ class ConfigCacheHelper diff --git a/app/Support/DevTools/BrowserLogWriter.php b/app/Support/DevTools/BrowserLogWriter.php index ea9a7b6b..2737ce04 100644 --- a/app/Support/DevTools/BrowserLogWriter.php +++ b/app/Support/DevTools/BrowserLogWriter.php @@ -137,9 +137,12 @@ class BrowserLogWriter return Log::channel('browser'); } + // daily + 유한 보존일 — single 드라이버는 로테이션 없이 무한 append 되어 + // browser.log 가 저장소 단일 최대 파일로 자란다 (실측 989MB). return Log::build([ - 'driver' => 'single', + 'driver' => 'daily', 'path' => storage_path('logs/browser.log'), + 'days' => 7, 'level' => config('logging.level', 'debug'), ]); } diff --git a/app/Support/ExtensionSettingsMirror.php b/app/Support/ExtensionSettingsMirror.php new file mode 100644 index 00000000..c149f067 --- /dev/null +++ b/app/Support/ExtensionSettingsMirror.php @@ -0,0 +1,310 @@ +<?php + +namespace App\Support; + +use App\Contracts\Extension\ModuleSettingsInterface; +use App\Extension\ModuleManager; +use App\Extension\PluginManager; +use App\Repositories\JsonConfigRepository; +use App\Services\PluginSettingsService; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; + +/** + * 설정 config 미러(`g7_settings.*`) 관리자 + * + * `g7_core_settings()` / `g7_module_settings()` / `g7_plugin_settings()` 가 읽는 + * in-memory config 미러를 채웁니다. + * + * 종전에는 이 미러가 **부팅 때 한 번만** 채워졌습니다. FPM 은 요청마다 부팅하므로 + * 문제가 드러나지 않지만, 큐 워커·schedule:work·Reverb 처럼 프로세스가 상주하는 + * 환경에서는 관리자가 설정을 저장해도 그 프로세스의 미러가 영원히 옛 값으로 남습니다 + * (공개이슈 #109). 저장 경로에서 이 클래스의 refresh* 를 호출해 같은 프로세스의 + * 미러를 즉시 다시 채웁니다. + * + * 민감 항목은 미러에 담지 않습니다 — 민감값이 필요한 서버 코드는 전용 게터 + * (`plugin_setting()` 등)를 씁니다. 자세한 근거는 docs/backend/admin-settings-access.md 참조. + */ +class ExtensionSettingsMirror +{ + /** + * 코어 설정 카테고리 목록 + * + * @var array<int, string> + */ + public const CORE_CATEGORIES = [ + 'mail', + 'general', + 'security', + 'debug', + 'drivers', + 'cache', + 'upload', + 'core_update', + 'geoip', + 'seo', + 'identity', + 'pagination', + ]; + + /** + * 코어 설정 미러(`g7_settings.core`)를 다시 채웁니다. + * + * @param JsonConfigRepository|null $configRepository 설정 저장소 (미지정 시 새로 생성) + */ + public function refreshCore(?JsonConfigRepository $configRepository = null): void + { + // 이 메서드는 SettingsServiceProvider::register() 에서도 호출된다 — + // 그 시점은 DI 컨테이너 사용 전이므로 직접 인스턴스화가 유일한 선택지다. + $configRepository ??= new JsonConfigRepository; + + $coreSettings = []; + + foreach (self::CORE_CATEGORIES as $category) { + $settings = $configRepository->getCategory($category); + + if (! empty($settings)) { + $coreSettings[$category] = $settings; + } + } + + Config::set('g7_settings.core', $coreSettings); + } + + /** + * 활성 모듈 전체의 설정 미러를 다시 채웁니다. + */ + public function refreshAllModules(): void + { + $moduleManager = app(ModuleManager::class); + $moduleSettings = []; + + foreach (array_keys($moduleManager->getActiveModules()) as $identifier) { + $settings = $this->resolveModuleSettings($identifier); + + if (! empty($settings)) { + $moduleSettings[$identifier] = $settings; + } + } + + Config::set('g7_settings.modules', $moduleSettings); + } + + /** + * 모듈 하나의 설정 미러를 다시 채웁니다. + * + * @param string $identifier 모듈 식별자 (예: sirsoft-ecommerce) + */ + public function refreshModule(string $identifier): void + { + $settings = $this->resolveModuleSettings($identifier); + + if (empty($settings)) { + Config::set('g7_settings.modules.'.$identifier, []); + + return; + } + + Config::set('g7_settings.modules.'.$identifier, $settings); + } + + /** + * 활성 플러그인 전체의 설정 미러를 다시 채웁니다. + */ + public function refreshAllPlugins(): void + { + $pluginManager = app(PluginManager::class); + $pluginSettings = []; + + foreach (array_keys($pluginManager->getActivePlugins()) as $identifier) { + $settings = $this->resolvePluginSettings($identifier); + + if (! empty($settings)) { + $pluginSettings[$identifier] = $settings; + } + } + + Config::set('g7_settings.plugins', $pluginSettings); + } + + /** + * 플러그인 하나의 설정 미러를 다시 채웁니다. + * + * @param string $identifier 플러그인 식별자 (예: sirsoft-gdpr) + */ + public function refreshPlugin(string $identifier): void + { + Config::set('g7_settings.plugins.'.$identifier, $this->resolvePluginSettings($identifier)); + } + + /** + * 모듈 설정 서비스에서 미러에 담을 값을 만듭니다. + * + * 확장 하나가 실패해도 부팅/저장을 중단시키지 않습니다. + * + * @param string $identifier 모듈 식별자 + * @return array<string, mixed> 미러 값 (민감 항목 제외) + */ + private function resolveModuleSettings(string $identifier): array + { + try { + $settingsService = $this->resolveModuleSettingsService($identifier); + + if (! $settingsService instanceof ModuleSettingsInterface) { + return []; + } + + return $this->stripSensitive( + $settingsService->getAllSettings(), + $this->resolveModuleSchema($identifier), + ); + } catch (\Throwable $e) { + Log::warning("모듈 환경설정 로딩 실패: {$identifier}", [ + 'error' => $e->getMessage(), + ]); + + return []; + } + } + + /** + * 플러그인 설정에서 미러에 담을 값을 만듭니다. + * + * 종전에는 setting.json 을 raw 로 읽어 defaults 병합도 정규화도 없었고, + * 암호문이 그대로 실렸습니다. 이제 PluginSettingsService 를 경유해 값의 형태를 + * 전용 게터와 맞추되, 민감 항목은 **키 자체를 담지 않습니다**. + * + * @param string $identifier 플러그인 식별자 + * @return array<string, mixed> 미러 값 (민감 항목 제외) + */ + private function resolvePluginSettings(string $identifier): array + { + try { + $pluginManager = app(PluginManager::class); + $plugin = $pluginManager->getPlugin($identifier); + + if (! $plugin) { + return []; + } + + $settings = app(PluginSettingsService::class)->get($identifier); + + if (! is_array($settings)) { + return []; + } + + return $this->stripSensitive($settings, $plugin->getSettingsSchema()); + } catch (\Throwable $e) { + Log::warning("플러그인 환경설정 로딩 실패: {$identifier}", [ + 'error' => $e->getMessage(), + ]); + + return []; + } + } + + /** + * 스키마에서 `sensitive: true` 로 선언된 필드를 제거합니다. + * + * 미러는 봇 전용 화면 생성기가 키 제한 없이 조회하는 경로이므로, 민감값이 실리면 + * 레이아웃이 그 키를 참조하는 순간 평문이 봇 HTML 로 나갑니다. 브라우저 경로는 + * 애초에 제외 중이라 화면만 봐서는 드러나지 않는 비대칭입니다. + * + * @param array<string, mixed> $settings 설정 배열 + * @param array<string, mixed> $schema 설정 스키마 + * @return array<string, mixed> 민감 항목이 제거된 설정 + */ + private function stripSensitive(array $settings, array $schema): array + { + foreach ($schema as $field => $config) { + if (! is_array($config)) { + continue; + } + + if ($config['sensitive'] ?? false) { + unset($settings[$field]); + + continue; + } + + // 카테고리형 스키마 (category.field) 지원 + if (! isset($settings[$field]) || ! is_array($settings[$field])) { + continue; + } + + $nested = array_filter($config, 'is_array'); + + if ($nested !== []) { + $settings[$field] = $this->stripSensitive($settings[$field], $nested); + } + } + + return $settings; + } + + /** + * 모듈의 설정 스키마를 조회합니다. + * + * @param string $identifier 모듈 식별자 + * @return array<string, mixed> 설정 스키마 (없으면 빈 배열) + */ + private function resolveModuleSchema(string $identifier): array + { + $module = app(ModuleManager::class)->getModule($identifier); + + if (! $module || ! method_exists($module, 'getSettingsSchema')) { + return []; + } + + $schema = $module->getSettingsSchema(); + + return is_array($schema) ? $schema : []; + } + + /** + * 모듈의 환경설정 서비스를 찾아 인스턴스화합니다. + * + * 다음 순서로 설정 서비스를 찾습니다: + * 1. 인터페이스 바인딩: Modules\Vendor\Module\Contracts\ModuleSettingsServiceInterface + * 2. 구체 클래스: Modules\Vendor\Module\Services\ModuleSettingsService + * + * @param string $identifier 모듈 식별자 (예: sirsoft-ecommerce) + * @return ModuleSettingsInterface|null 설정 서비스 인스턴스 + */ + private function resolveModuleSettingsService(string $identifier): ?ModuleSettingsInterface + { + // vendor-module 형식을 네임스페이스로 변환 + $parts = explode('-', $identifier); + + if (count($parts) < 2) { + return null; + } + + $vendor = ucfirst($parts[0]); + $moduleName = ucfirst($parts[1]); + + // 1. 인터페이스 바인딩 확인 + $interfaceClass = "Modules\\{$vendor}\\{$moduleName}\\Contracts\\{$moduleName}SettingsServiceInterface"; + + if (app()->bound($interfaceClass)) { + $service = app($interfaceClass); + + if ($service instanceof ModuleSettingsInterface) { + return $service; + } + } + + // 2. 구체 클래스 확인 + $concreteClass = "Modules\\{$vendor}\\{$moduleName}\\Services\\{$moduleName}SettingsService"; + + if (class_exists($concreteClass)) { + $service = app($concreteClass); + + if ($service instanceof ModuleSettingsInterface) { + return $service; + } + } + + return null; + } +} diff --git a/app/Support/PermissionEscalationGuard.php b/app/Support/PermissionEscalationGuard.php new file mode 100644 index 00000000..b8fe9047 --- /dev/null +++ b/app/Support/PermissionEscalationGuard.php @@ -0,0 +1,177 @@ +<?php + +namespace App\Support; + +use App\Contracts\Repositories\PermissionRepositoryInterface; +use App\Contracts\Repositories\RoleRepositoryInterface; +use App\Exceptions\PermissionEscalationException; +use App\Models\User; +use Illuminate\Support\Facades\Auth; + +/** + * 권한 상승(escalation) 상한 가드 (SSoT) + * + * 비-슈퍼관리자 액터가 "자신이 보유하지 않았거나 자신의 범위(scope)보다 넓은" 권한을 + * 부여하는 것을 차단합니다(KVE-2026-1919). 권한을 직접 역할에 부여하는 경로(RoleService)와 + * 역할 자체를 사용자에게 붙여 그 역할의 권한을 통째로 넘기는 경로(UserService)가 **동일한 + * 상한 규칙**을 공유하도록, 판정을 한 지점에 모읍니다. 한 경로만 막으면 다른 경로가 우회로가 + * 됩니다(역할 부여는 곧 그 역할의 전 권한 부여이므로 권한 부여와 같은 상한을 받아야 합니다). + */ +class PermissionEscalationGuard +{ + /** + * @param PermissionRepositoryInterface $permissionRepository 권한 조회 + * @param RoleRepositoryInterface $roleRepository 역할 조회 + */ + public function __construct( + private PermissionRepositoryInterface $permissionRepository, + private RoleRepositoryInterface $roleRepository + ) {} + + /** + * 부여하려는 권한이 액터의 상한(보유 + 범위)을 넘지 않는지 확인합니다. + * + * 슈퍼 관리자와 내부/Artisan 경로는 상한 검사 없이 통과합니다. 비-슈퍼관리자 + * 액터는 자신이 보유한 권한만, 그리고 자신의 effective scope 보다 넓지 않은 + * 범위로만 부여할 수 있습니다(권한 상승 차단). + * + * @param array<int, array{id?: int, scope_type?: string|null}> $permissions 부여 권한 배열 + * + * @throws PermissionEscalationException 상한 위반 시 + */ + public function assertGrantWithinActorCeiling(array $permissions): void + { + $actor = Auth::user(); + + // 인증 액터 부재 = 내부/Artisan 신뢰 경로 → 상한 미적용 + if (! $actor instanceof User) { + return; + } + + // 슈퍼 관리자는 상한 없음 + if ($actor->isSuperAdmin()) { + return; + } + + if (empty($permissions)) { + return; + } + + // 부여 대상 권한 ID → 식별자 매핑 + $ids = array_values(array_filter(array_map( + static fn ($p) => $p['id'] ?? null, + $permissions + ))); + $identifierById = $this->permissionRepository->getByIds($ids) + ->keyBy('id') + ->map(static fn ($permission) => $permission->identifier); + + foreach ($permissions as $permission) { + $id = $permission['id'] ?? null; + $identifier = $id !== null ? ($identifierById[$id] ?? null) : null; + + // 식별자를 해석할 수 없는 권한은 안전하게 거부 + if ($identifier === null) { + throw new PermissionEscalationException; + } + + // 액터가 보유하지 않은 권한은 부여 불가 + if (! $actor->hasPermission($identifier)) { + throw new PermissionEscalationException; + } + + // 부여 범위가 액터의 effective scope 보다 넓으면 불가 + $requestedScope = $permission['scope_type'] ?? null; + $actorScope = $actor->getEffectiveScopeForPermission($identifier); + + if ($this->scopeRank($requestedScope) > $this->scopeRank($actorScope)) { + throw new PermissionEscalationException; + } + } + } + + /** + * 사용자에게 부여하려는 역할들이 액터의 상한을 넘지 않는지 확인합니다. + * + * 역할을 붙이는 것은 그 역할이 담은 권한 전부를 부여하는 것과 같으므로, 각 역할의 + * 권한을 그 pivot scope 와 함께 펼쳐 권한 부여 상한(assertGrantWithinActorCeiling)을 + * 그대로 적용합니다. 호출측은 **이번 조작으로 변경되는(추가·제거) 역할만** 전달해야 합니다 — + * 기존 유지 역할은 변경이 아니므로 제외합니다. 제거 방향도 같은 상한을 받는 이유는, 상위 + * 역할을 박탈하는 하향 조작 역시 액터가 권한을 갖지 못한 역할 구성에 대한 조작이기 때문입니다. + * + * @param array<int, int> $roleIds 이번 조작으로 변경되는(추가·제거) 역할 ID 목록 + * + * @throws PermissionEscalationException 상한 위반 시 + */ + public function assertRoleAssignmentWithinActorCeiling(array $roleIds): void + { + $actor = Auth::user(); + + if (! $actor instanceof User) { + return; + } + + if ($actor->isSuperAdmin()) { + return; + } + + $roleIds = array_values(array_filter($roleIds, static fn ($id) => $id !== null)); + + if (empty($roleIds)) { + return; + } + + // 시스템 baseline 역할('user')은 상한에서 면제한다. + // + // 'user' 는 모든 회원이 갖는 기본 역할로, core.permissions.update 가 없는 액터의 + // 생성 경로가 상한 검사 없이 자동 배정하는 바로 그 역할이다(UserService::createUser). + // 그 역할의 권한은 알림 self-service·본인인증 등 회원 baseline 뿐이라 액터가 + // 그것을 "부여" 해도 권한 상승 벡터가 되지 않는다. baseline 을 검사에 넣으면 + // core.permissions.update 를 가진(=더 권한 있는) 액터가 baseline 을 명시 지정했을 때만 + // 403 이 되어, 권한 낮은 액터의 auto-assign 경로와 비대칭으로 정상 회원 생성/수정이 + // 깨진다. 비-baseline 역할은 여전히 전량 상한 검사를 받는다. + $baselineRoleId = $this->roleRepository->findByIdentifier('user')?->id; + + // 부여 역할들의 권한을 [{id, scope_type}] 로 펼친다 + $permissions = []; + foreach ($roleIds as $roleId) { + if ($baselineRoleId !== null && (int) $roleId === (int) $baselineRoleId) { + continue; + } + + $role = $this->roleRepository->findById((int) $roleId); + + // 존재하지 않는 역할은 안전하게 거부 + if ($role === null) { + throw new PermissionEscalationException; + } + + foreach ($role->permissions as $permission) { + $permissions[] = [ + 'id' => $permission->id, + 'scope_type' => $permission->pivot->scope_type ?? null, + ]; + } + } + + $this->assertGrantWithinActorCeiling($permissions); + } + + /** + * scope_type 의 넓이 순위를 반환합니다 (클수록 넓음). + * + * null(전체) > 'role'(소유역할) > 'self'(본인) 순으로, User::getEffectiveScopeForPermission + * 의 union 우선순위와 동일한 서열을 사용합니다. + * + * @param string|null $scope 범위 문자열 + * @return int 넓이 순위 (2=전체, 1=role, 0=self) + */ + private function scopeRank(?string $scope): int + { + return match ($scope) { + null => 2, + 'role' => 1, + default => 0, + }; + } +} diff --git a/app/Support/Query/BoundedPaginator.php b/app/Support/Query/BoundedPaginator.php index 229966ea..e310e247 100644 --- a/app/Support/Query/BoundedPaginator.php +++ b/app/Support/Query/BoundedPaginator.php @@ -156,8 +156,10 @@ class BoundedPaginator // newQuery() 는 같은 커넥션의 빈 빌더를 만든다. fromSub 가 바인딩까지 옮겨 주므로 // 서브쿼리 SQL 을 문자열로 조립하거나 mergeBindings 를 부를 필요가 없다. + // 별칭은 빌더가 wrapTable 로 접두사를 붙이므로 `g7_` 을 직접 쓰면 `g7_g7_...` 이 + // 된다 (참조 0곳이라 실동작 무해였으나 명명 혼선 제거). $counted = (int) $bounded->newQuery() - ->fromSub($bounded, 'g7_bounded_total') + ->fromSub($bounded, 'bounded_total') ->count(); return $counted > $resultCap diff --git a/app/Support/ScheduleCommandValidator.php b/app/Support/ScheduleCommandValidator.php index 874c60de..e61e73e4 100644 --- a/app/Support/ScheduleCommandValidator.php +++ b/app/Support/ScheduleCommandValidator.php @@ -47,6 +47,24 @@ class ScheduleCommandValidator /** 허용 개수를 넘는 위치 인자가 쓰임 */ public const ARTISAN_REASON_ARGUMENT = 'argument_denied'; + /** Shell — 게이트 off 또는 화이트리스트 빔 */ + public const SHELL_REASON_DISABLED = 'shell_disabled'; + + /** Shell — 셸 메타문자/제어문자로 토큰화 불가 */ + public const SHELL_REASON_METACHARACTER = 'shell_metacharacter'; + + /** Shell — 첫 토큰 basename 이 화이트리스트에 없음 */ + public const SHELL_REASON_NOT_ALLOWED = 'shell_not_allowlisted'; + + /** Shell — 완전 거부형 인터프리터 / 인자 없는 인터프리터 / 스크립트 자리 artisan */ + public const SHELL_REASON_INTERPRETER = 'shell_interpreter'; + + /** Shell — 인터프리터 뒤 스크립트 자리가 하이픈(인라인 코드 플래그) */ + public const SHELL_REASON_INLINE_CODE = 'shell_inline_code'; + + /** Shell — 스크립트 경로 형태 위반(상대경로 / `..` / `#`) */ + public const SHELL_REASON_SCRIPT_PATH = 'shell_script_path'; + /** 셸 해석을 유발하는 메타문자 — 하나라도 있으면 인자 배열 실행이 불가하므로 거부 */ private const SHELL_METACHARACTERS = [ '|', '&', ';', '$', '`', '>', '<', '(', ')', '{', '}', @@ -56,16 +74,35 @@ class ScheduleCommandValidator /** * Shell 타입 command 를 실행해도 되는지 판정합니다. * - * 게이트가 켜져 있고, 화이트리스트가 비어있지 않으며, 메타문자가 없고, - * 첫 토큰의 basename 이 화이트리스트와 완전 일치할 때만 true. + * 판정은 전적으로 `inspectShellCommand()` 에 위임한다 — 사유가 필요 없는 호출부 + * (실행부의 마지막 방어선 등)를 위한 bool 형태이며, 두 경로의 결과가 갈릴 여지가 없다. * * @param string $command 스케줄에 저장된 shell command 문자열 * @return bool 실행을 허용하면 true */ public static function isShellCommandAllowed(string $command): bool + { + return self::inspectShellCommand($command)['allowed']; + } + + /** + * Shell command 를 실행 가능한지 판정하고 거부 사유를 함께 돌려줍니다. + * + * 판정 순서: + * 1. 게이트 off 또는 화이트리스트 빔 → SHELL_REASON_DISABLED + * 2. 메타문자/제어문자로 토큰화 불가 → SHELL_REASON_METACHARACTER + * 3. 첫 토큰 basename 이 화이트리스트에 없음 → SHELL_REASON_NOT_ALLOWED + * 4. 첫 토큰이 완전 거부형(reject_binaries) → SHELL_REASON_INTERPRETER (화이트리스트보다 우선) + * 5. 첫 토큰이 스크립트 실행형 인터프리터 → 스크립트 자리 형태 규칙 적용 + * 6. 그 외(일반 스크립트, 예 backup.sh) → 통과 (기존 계약 보존) + * + * @param string $command 스케줄에 저장된 shell command 문자열 + * @return array{allowed: bool, reason: string|null} + */ + public static function inspectShellCommand(string $command): array { if (! (bool) config('schedule_security.shell.enabled', false)) { - return false; + return self::shellVerdict(false, self::SHELL_REASON_DISABLED); } $allowed = array_values(array_filter(array_map( @@ -74,16 +111,160 @@ class ScheduleCommandValidator ))); if ($allowed === []) { - return false; + return self::shellVerdict(false, self::SHELL_REASON_DISABLED); } $tokens = self::tokenize($command); if ($tokens === null || $tokens === []) { + return self::shellVerdict(false, self::SHELL_REASON_METACHARACTER); + } + + $binary = basename($tokens[0]); + + if (! in_array($binary, $allowed, true)) { + return self::shellVerdict(false, self::SHELL_REASON_NOT_ALLOWED); + } + + $normalized = self::normalizeShellBinary($binary); + + // 완전 거부형 — 화이트리스트에 등재돼 있어도 실행하지 않는다. + if (in_array($normalized, self::shellList('reject_binaries'), true)) { + return self::shellVerdict(false, self::SHELL_REASON_INTERPRETER); + } + + // 스크립트 실행형 인터프리터 — 첫 인자(스크립트 자리)에 형태 규칙을 적용한다. + if (in_array($normalized, self::shellList('script_interpreters'), true)) { + return self::inspectInterpreterScript($tokens); + } + + // 일반 스크립트(인터프리터 아님) — 기존 계약 보존. + return self::shellVerdict(true, null); + } + + /** + * 인터프리터 뒤 스크립트 자리(첫 인자)가 허용 형태인지 판정합니다. + * + * 인스톨러 선례 `public/install/includes/binary-path-policy.php` 의 + * `installer_binary_path_shape_ok`("하이픈 선두 토큰 거부 + `..`/`#` 차단 + 절대경로 요구") + * 를 스케줄 Shell 스크립트 자리에 이식한 것이다. + * + * @param array<int, string> $tokens 토큰 배열 (첫 원소는 인터프리터) + * @return array{allowed: bool, reason: string|null} + */ + private static function inspectInterpreterScript(array $tokens): array + { + // 인자 0개 — 실행할 스크립트가 없다(인터프리터 REPL/셸 기동). + if (! isset($tokens[1]) || $tokens[1] === '') { + return self::shellVerdict(false, self::SHELL_REASON_INTERPRETER); + } + + $script = $tokens[1]; + + // 하이픈 선두 = 인라인 코드/옵션 플래그(`-c`/`-e`/`-r`/`-m`/`-d`/`-x` 등) — 일괄 차단. + if ($script[0] === '-') { + return self::shellVerdict(false, self::SHELL_REASON_INLINE_CODE); + } + + // 스크립트 자리 basename 이 artisan 이면 거부 — 코어 Artisan 축은 Artisan 타입으로. + $deniedNames = array_map( + static fn ($name): string => strtolower(trim((string) $name)), + self::shellList('denied_script_names'), + ); + + if (in_array(strtolower(basename(str_replace('\\', '/', $script))), $deniedNames, true)) { + return self::shellVerdict(false, self::SHELL_REASON_INTERPRETER); + } + + // `#` 이후를 주석으로 흘려 확장자를 위장하는 트릭 차단. + if (str_contains($script, '#')) { + return self::shellVerdict(false, self::SHELL_REASON_SCRIPT_PATH); + } + + // `..` 세그먼트 차단 (CWD 상대 이동/트래버설). + if (preg_match('#(^|[/\\\\])\.\.([/\\\\]|$)#', $script) === 1) { + return self::shellVerdict(false, self::SHELL_REASON_SCRIPT_PATH); + } + + // 스크립트는 절대경로여야 한다 (CWD 에 의존한 실행 차단). + if (! self::isAbsolutePath($script)) { + return self::shellVerdict(false, self::SHELL_REASON_SCRIPT_PATH); + } + + // 스크립트 뒤 인자는 스크립트 소유이므로 검사하지 않는다. + return self::shellVerdict(true, null); + } + + /** + * 절대경로인지 판정합니다 (POSIX / Windows 드라이브 / UNC). + * + * @param string $token 경로 토큰 + * @return bool 절대경로면 true + */ + private static function isAbsolutePath(string $token): bool + { + if ($token === '') { return false; } - return in_array(basename($tokens[0]), $allowed, true); + if ($token[0] === '/') { + return true; + } + + if (preg_match('#^[A-Za-z]:[\\\\/]#', $token) === 1) { + return true; + } + + return str_starts_with($token, '\\\\'); + } + + /** + * shell 정책 배열 설정을 소문자 문자열 목록으로 정규화합니다. + * + * @param string $key `schedule_security.shell` 하위 키 + * @return array<int, string> + */ + /** + * 인터프리터/거부형 분류용으로 바이너리 이름을 정규화합니다. + * + * 분류 목록(`script_interpreters`/`reject_binaries`)은 기본 이름만 담는다. + * 운영자가 Windows 관례(`python.exe`)나 버전 접미사(`python3.12`, `php8.2`)로 + * 화이트리스트에 등재하면 정확 일치 분류가 비켜가 인터프리터가 "일반 스크립트 — + * 통과" 분기를 타고, `-c` 인라인 코드 차단(KVE-2026-1653)이 무력화된다. + * 화이트리스트 대조 자체는 운영자가 적은 원문 그대로 유지한다 — 여기서 접는 + * 것은 위험 분류뿐이다. + * + * @param string $binary 첫 토큰의 basename + * @return string 소문자 + 실행 확장자(.exe/.bat/.cmd/.com) 제거 + 후행 버전 숫자 제거 + */ + private static function normalizeShellBinary(string $binary): string + { + $name = strtolower($binary); + $name = (string) preg_replace('/\.(exe|bat|cmd|com)$/', '', $name); + + // 후행 버전 접미사(python3.12 → python, php8.2 → php)를 접는다. + // python3 이 python 으로 접혀도 두 이름 모두 분류 목록에 있어 판정은 동일하다. + return (string) preg_replace('/[0-9][0-9.]*$/', '', $name); + } + + private static function shellList(string $key): array + { + return array_values(array_filter(array_map( + static fn ($value): string => strtolower(trim((string) $value)), + (array) config('schedule_security.shell.'.$key, []), + ))); + } + + /** + * Shell 판정 결과 배열을 만듭니다. + * + * @param bool $allowed 허용 여부 + * @param string|null $reason 거부 사유 코드 + * @return array{allowed: bool, reason: string|null} + */ + private static function shellVerdict(bool $allowed, ?string $reason = null): array + { + return ['allowed' => $allowed, 'reason' => $reason]; } /** @@ -410,15 +591,88 @@ class ScheduleCommandValidator $command = self::findRegisteredCommand($name); - if ($command === null) { + if ($command !== null) { + $class = get_class($command); + + foreach ($namespaces as $namespace) { + if (str_starts_with($class, $namespace)) { + return $command->getDefinition(); + } + } + return null; } - $class = get_class($command); + // 레지스트리에 없으면 프로바이더 선언 폴백 — HTTP 요청에서는 확장 프로바이더가 + // `runningInConsole()` 게이트로 커맨드 등록을 건너뛰어 `Artisan::all()` 에 + // 확장 커맨드가 존재하지 않는다. 관리자 화면의 스케줄 저장 검증이 바로 이 + // 문맥이므로, 등록 인스턴스가 없을 때는 활성 프로바이더의 `$commands` 선언을 + // 같은 기준(클래스 네임스페이스 + 실제 명령명 대조)으로 해석한다. + return self::resolveProviderDeclaredCommandDefinition($name, $namespaces); + } - foreach ($namespaces as $namespace) { - if (str_starts_with($class, $namespace)) { - return $command->getDefinition(); + /** + * 활성 서비스 프로바이더가 `$commands` 프로퍼티로 선언한 확장 커맨드에서 + * 명령명을 해석해 정의를 돌려줍니다. + * + * 판정 기준은 레지스트리 경로와 동일하다 — 커맨드 **클래스**의 네임스페이스가 + * 확장 네임스페이스여야 하고, 요청된 이름은 그 클래스를 실제 인스턴스화해 얻은 + * 선언 명령명과 일치해야 한다(이름만으로 위조할 수 없다). 프로바이더가 커맨드 + * 목록을 생성자에서 동적으로 만들면 기본값이 비므로 이 폴백에는 걸리지 않는다 + * (fail-closed 유지). + * + * @param string $name 명령명 + * @param array<int, string> $namespaces 허용 확장 네임스페이스 접두사 + * @return InputDefinition|null 해석 불가하면 null + */ + private static function resolveProviderDeclaredCommandDefinition(string $name, array $namespaces): ?InputDefinition + { + try { + $providers = app()->getLoadedProviders(); + } catch (Throwable) { + return null; + } + + foreach (array_keys($providers) as $providerClass) { + if (! class_exists($providerClass)) { + continue; + } + + try { + $defaults = (new \ReflectionClass($providerClass))->getDefaultProperties(); + } catch (Throwable) { + continue; + } + + foreach ((array) ($defaults['commands'] ?? []) as $commandClass) { + if (! is_string($commandClass) || ! class_exists($commandClass)) { + continue; + } + + $owned = false; + foreach ($namespaces as $namespace) { + if (str_starts_with($commandClass, $namespace)) { + $owned = true; + break; + } + } + + if (! $owned) { + continue; + } + + try { + /** @var SymfonyCommand $instance */ + $instance = app()->make($commandClass); + } catch (Throwable) { + continue; + } + + if (! $instance instanceof SymfonyCommand || $instance->getName() !== $name) { + continue; + } + + return $instance->getDefinition(); } } diff --git a/app/Support/TrustedScriptHosts.php b/app/Support/TrustedScriptHosts.php new file mode 100644 index 00000000..7ba0ce4f --- /dev/null +++ b/app/Support/TrustedScriptHosts.php @@ -0,0 +1,180 @@ +<?php + +namespace App\Support; + +use App\Extension\HookManager; +use App\Extension\ModuleManager; +use App\Extension\PluginManager; +use App\Extension\TemplateManager; +use Illuminate\Support\Facades\Log; + +/** + * 신뢰하는 외부 스크립트 호스트 집계 (KVE-2026-1915 신뢰 출처 허용목록) + * + * 레이아웃 `scripts[].src` 는 기본적으로 same-origin path-only 만 허용한다(원격 코드 로드 + * 차단). 그러나 CKEditor5(cdn.ckeditor.com)·Daum 우편번호(t1.daumcdn.net)처럼 외부 CDN + * 스크립트를 정당하게 로드하는 번들 확장이 있다. 그 확장이 manifest 의 + * `trusted_script_hosts` 로 자기 호스트를 **선언**하고, 코어가 활성 확장 전수에서 이 목록을 + * 집계한다. 런타임 스크립트 로더(TemplateApp)·저장측 검증(SafeLayoutExpressions)·정적 검사 + * (audit)가 모두 이 집계 결과만 신뢰 호스트로 허용한다. + * + * 신뢰 경계: 편집기가 저장하는 레이아웃(저권한 액터)에는 외부 스크립트를 허용하지 않는 + * 것이 기본이고, 여기서 허용되는 것은 **확장이 코드로 선언한 호스트**뿐이다. + */ +class TrustedScriptHosts +{ + /** + * 확장이 동적으로 신뢰 호스트를 추가할 수 있는 필터 훅 이름. + */ + public const FILTER_HOOK = 'core.layout.trusted_script_hosts'; + + /** + * 활성 모듈·플러그인·템플릿이 선언한 신뢰 호스트 전체를 집계합니다. + * + * @return array<int, string> 중복 제거된 호스트명 목록 (소문자) + */ + public static function hosts(): array + { + $hosts = []; + + try { + foreach (app(ModuleManager::class)->getActiveModules() as $module) { + if (method_exists($module, 'getTrustedScriptHosts')) { + $hosts = array_merge($hosts, $module->getTrustedScriptHosts()); + } + } + } catch (\Throwable $e) { + Log::warning('TrustedScriptHosts: 모듈 집계 실패 - '.$e->getMessage()); + } + + try { + foreach (app(PluginManager::class)->getActivePlugins() as $plugin) { + if (method_exists($plugin, 'getTrustedScriptHosts')) { + $hosts = array_merge($hosts, $plugin->getTrustedScriptHosts()); + } + } + } catch (\Throwable $e) { + Log::warning('TrustedScriptHosts: 플러그인 집계 실패 - '.$e->getMessage()); + } + + // 템플릿은 모듈/플러그인과 달리 PHP 확장 클래스가 없고 manifest 배열로 다뤄지므로, + // 활성 템플릿의 template.json 에서 직접 읽는다. 타입별 활성 템플릿은 각각 하나뿐이다. + try { + $templateManager = app(TemplateManager::class); + + foreach (['admin', 'user'] as $type) { + $template = $templateManager->getActiveTemplate($type); + + if (! is_array($template) || ! isset($template['trusted_script_hosts'])) { + continue; + } + + $declared = $template['trusted_script_hosts']; + + if (! is_array($declared)) { + continue; + } + + $hosts = array_merge($hosts, $declared); + } + } catch (\Throwable $e) { + Log::warning('TrustedScriptHosts: 템플릿 집계 실패 - '.$e->getMessage()); + } + + // 확장이 훅으로 동적 추가할 수 있는 경로 (manifest 외 경로) + $hosts = HookManager::applyFilters(self::FILTER_HOOK, $hosts); + + if (! is_array($hosts)) { + $hosts = []; + } + + // 정규화: 문자열·비어있지 않음·소문자·중복 제거 + $normalized = []; + foreach ($hosts as $host) { + if (! is_string($host)) { + continue; + } + $host = strtolower(trim($host)); + if ($host !== '') { + $normalized[$host] = true; + } + } + + return array_keys($normalized); + } + + /** + * URL 에서 호스트명을 추출합니다. + * + * `//host/path`(protocol-relative)·`https://host/path`(scheme 포함) 모두 처리합니다. + * same-origin 경로(`/path`)는 호스트가 없으므로 null 을 반환합니다. + * + * @param string $url 검사 대상 URL + * @return string|null 소문자 호스트명 (없으면 null) + */ + public static function hostOf(string $url): ?string + { + $host = parse_url(self::normalizeForOriginCheck(trim($url)), PHP_URL_HOST); + + return is_string($host) && $host !== '' ? strtolower($host) : null; + } + + /** + * origin 판정 전에 URL 을 브라우저 URL 파서와 동일하게 정규화합니다. + * + * 브라우저(WHATWG URL)는 파싱 전에 ASCII tab·LF·CR 을 제거하고, special scheme + * (http/https)에서 백슬래시를 슬래시와 동등하게 처리합니다. 이 정규화 없이 판정하면 + * 같은 문자열을 계층마다 다른 출처로 읽습니다: + * + * - `https://evil.com\@cdn.ckeditor.com/x.js` — 정규화 없이는 호스트가 + * `cdn.ckeditor.com`(userinfo 해석)이지만 브라우저는 `evil.com` 에서 로드합니다. + * - `/\/cdn.ckeditor.com/x.js` — 문자열상 path 지만 브라우저는 authority 로 읽습니다. + * + * 정규화 후 판정하면 경로 중간의 백슬래시·탭(`/js/a\b.js`)은 authority 를 만들지 + * 않으므로 그대로 통과합니다(과차단 없음). + * + * 이 메서드가 origin 판정 정규화의 SSoT 입니다 — 저장측 규칙 + * (`App\Rules\SafeLayoutExpressions`)이 위임하고, 클라이언트 + * (`TemplateApp.normalizeScriptSrcForOriginCheck`)·정적 검사 + * (`layout-scripts-src-same-origin`)가 동형 구현을 갖습니다. 한 계층만 바꾸면 + * 그 계층만 다른 출처를 보게 되며, 예외도 경고도 없이 판정만 갈립니다. + * + * @param string $url 원본 URL + * @return string 정규화된 URL + */ + public static function normalizeForOriginCheck(string $url): string + { + // ASCII tab / LF / CR 제거 (브라우저 파서가 파싱 전에 제거하는 문자) + $stripped = str_replace(["\t", "\n", "\r"], '', $url); + + // 백슬래시를 슬래시로 (special scheme 에서 등가) + $slashed = str_replace('\\', '/', $stripped); + + // 선행 슬래시가 3개 이상이어도 브라우저는 authority 시작으로 접는다 + // (`///host/x` ≡ `//host/x`, `https:///host/x` ≡ `https://host/x`). + // 접지 않으면 `/\/host/x` 가 정규화 후 `///host/x` 가 되어 parse_url 은 + // 호스트를 못 찾는데 브라우저는 host 에서 로드하는 갈림이 생긴다. + // 경로 중간의 연속 슬래시(`/js//a.js`)는 브라우저도 경로로 두므로 건드리지 않는다. + return preg_replace('#^([a-z][a-z0-9+.\-]*:)?/{2,}#i', '$1//', $slashed) ?? $slashed; + } + + /** + * URL 의 호스트가 신뢰 목록에 있는지 판정합니다. + * + * @param string $url 검사 대상 URL + * @param array<int, string>|null $hosts 신뢰 호스트 목록 (미지정 시 self::hosts()) + * @return bool 신뢰 호스트면 true (호스트 없는 same-origin 경로는 false) + */ + public static function isTrustedUrl(string $url, ?array $hosts = null): bool + { + $host = self::hostOf($url); + + if ($host === null) { + return false; + } + + $hosts ??= self::hosts(); + + return in_array($host, $hosts, true); + } +} diff --git a/app/Support/UserGradeGuard.php b/app/Support/UserGradeGuard.php new file mode 100644 index 00000000..b882a30d --- /dev/null +++ b/app/Support/UserGradeGuard.php @@ -0,0 +1,83 @@ +<?php + +namespace App\Support; + +use App\Exceptions\CannotModifySuperAdminException; +use App\Models\User; +use Illuminate\Support\Facades\Auth; + +/** + * 사용자 등급 상한(rank ceiling) 가드 (SSoT) + * + * 슈퍼 관리자 보호는 그동안 삭제/탈퇴 경로에만 있었고 수정·상태변경·권한부여 + * 경로에는 없어 비대칭이었습니다(KVE-2026-1919). 이 가드는 "비-슈퍼관리자 + * 액터는 슈퍼 관리자를 수정할 수 없다"는 규칙을 단일 지점에서 강제해 모든 + * 쓰기 경로가 동일한 상한을 공유하도록 합니다. + * + * 인증된 액터가 없는 경우(Artisan, 내부 시더/호출)는 신뢰 경로로 간주해 + * 가드를 적용하지 않습니다. HTTP 관리 경로는 인증·권한 미들웨어를 통과하므로 + * 이 지점에서는 항상 액터가 존재합니다. + */ +class UserGradeGuard +{ + /** + * 액터가 대상 사용자를 수정할 수 있는지 판정합니다. + * + * @param User $target 수정 대상 사용자 + * @param User|null $actor 행위자(미지정 시 현재 인증 사용자) + * @return bool 수정 가능 여부 + */ + public static function mayModify(User $target, ?User $actor = null): bool + { + $actor ??= Auth::user(); + + // 인증 액터 부재 = 내부/Artisan 신뢰 경로 → 가드 미적용 + if (! $actor instanceof User) { + return true; + } + + // 대상이 슈퍼 관리자가 아니면 등급 상한과 무관 + if (! $target->isSuperAdmin()) { + return true; + } + + // 슈퍼 관리자 대상은 슈퍼 관리자 액터만 수정 가능 + return $actor->isSuperAdmin(); + } + + /** + * 액터가 대상을 수정할 수 없으면 예외를 던집니다. + * + * @param User $target 수정 대상 사용자 + * @param User|null $actor 행위자(미지정 시 현재 인증 사용자) + * + * @throws CannotModifySuperAdminException 상한 위반 시 + */ + public static function assertActorMayModify(User $target, ?User $actor = null): void + { + if (! self::mayModify($target, $actor)) { + throw new CannotModifySuperAdminException; + } + } + + /** + * 액터가 수정 가능한 대상만 남긴 배열을 반환합니다(일괄 작업용). + * + * @param iterable<User> $targets 대상 사용자 목록 + * @param User|null $actor 행위자(미지정 시 현재 인증 사용자) + * @return array<User> 수정 가능한 대상 목록 + */ + public static function filterModifiable(iterable $targets, ?User $actor = null): array + { + $actor ??= Auth::user(); + + $allowed = []; + foreach ($targets as $target) { + if (self::mayModify($target, $actor)) { + $allowed[] = $target; + } + } + + return $allowed; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index d43438ac..eaec8778 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,7 +1,10 @@ <?php +use App\Exceptions\CannotModifyProtectedRoleException; +use App\Exceptions\CannotModifySuperAdminException; use App\Exceptions\CoreVersionMismatchException; use App\Exceptions\IdentityVerificationRequiredException; +use App\Exceptions\PermissionEscalationException; use App\Helpers\ResponseHelper; use App\Http\Middleware\AdminMiddleware; use App\Http\Middleware\CheckTemplateDependencies; @@ -176,6 +179,19 @@ $app = Application::configure(basePath: dirname(__DIR__)) } }); + // 등급 상한(rank ceiling) / 권한 상승 차단 예외 → HTTP 403 (KVE-2026-1919). + // 컨트롤러 catch 가 우선 처리하지만, 새 호출부가 로컬 catch 없이 이 예외를 던져도 + // 조용한 500 대신 일관된 403 이 되도록 전역 렌더러를 둔다(심층 방어). 메시지는 예외 + // 생성자가 lang 키로 설정한 것을 그대로 쓴다 — 컨트롤러 매핑과 동일 문구. + $exceptions->render(function (PermissionEscalationException|CannotModifySuperAdminException|CannotModifyProtectedRoleException $e, Request $request) { + if ($request->expectsJson() || $request->is('api/*')) { + return response()->json([ + 'success' => false, + 'message' => $e->getMessage(), + ], 403); + } + }); + // 확장 코어 버전 호환성 검사 실패 → HTTP 422 + error_code: 'core_version_mismatch' // (extension update/activate/recovery 등 사전 검증 진입 지점에서 throw) $exceptions->render(function (CoreVersionMismatchException $e, Request $request) { diff --git a/composer.json b/composer.json index 2c69d7e3..04624205 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,8 @@ "laravel/sanctum": "^4.2", "laravel/scout": "^11.1", "laravel/tinker": "^2.10.1", + "league/flysystem-aws-s3-v3": "^3.25.1", + "predis/predis": "^2.0", "symfony/http-client": "^7.4", "symfony/mailgun-mailer": "^7.4" }, diff --git a/composer.lock b/composer.lock index 97c428c9..9228fc16 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "675518b422a5d9dc29590b1f22a49c25", + "content-hash": "f34469561e87f11296bf77ecc5db9fbf", "packages": [ { "name": "aws/aws-crt-php", @@ -2547,6 +2547,61 @@ }, "time": "2026-06-25T06:52:23+00:00" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.35.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/8475ef9adfc6498b85469e2abec6fe3118cd08c4", + "reference": "8475ef9adfc6498b85469e2abec6fe3118cd08c4", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.371.5", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.35.2" + }, + "time": "2026-07-01T23:25:49+00:00" + }, { "name": "league/flysystem-local", "version": "3.31.0", @@ -3601,6 +3656,68 @@ ], "time": "2025-12-27T19:41:33+00:00" }, + { + "name": "predis/predis", + "version": "v2.4.1", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "07105e050622ed80bd60808367ced9e379f31530" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/07105e050622ed80bd60808367ced9e379f31530", + "reference": "07105e050622ed80bd60808367ced9e379f31530", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", + "phpunit/phpunit": "^8.0 || ^9.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2025-11-12T18:00:11+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -10573,5 +10690,5 @@ "php": "^8.2" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/app.php b/config/app.php index b18d9058..ca5ebd49 100644 --- a/config/app.php +++ b/config/app.php @@ -231,7 +231,7 @@ return [ | */ - 'version' => env('APP_VERSION', '7.0.6'), + 'version' => env('APP_VERSION', '7.0.7'), /* |-------------------------------------------------------------------------- diff --git a/config/attachment.php b/config/attachment.php index 75e64a1a..43c2f5f9 100644 --- a/config/attachment.php +++ b/config/attachment.php @@ -9,8 +9,26 @@ return [ | 첨부파일 저장에 사용할 디스크를 지정합니다. | config/filesystems.php에 정의된 디스크 이름을 사용합니다. | + | 빈 값(`ATTACHMENT_DISK=`)은 미설정으로 간주한다 — env() 는 키가 존재하면 + | 빈 문자열을 그대로 돌려주므로(기본값 미적용), `cp .env.example .env` 설치 + | 절차에서 빈 값이 그대로 오면 Storage::disk('') 로 업로드가 전면 실패한다. + | */ - 'disk' => env('ATTACHMENT_DISK', 'attachments'), + 'disk' => env('ATTACHMENT_DISK') ?: 'attachments', + + /* + |-------------------------------------------------------------------------- + | 디스크 명시 설정 여부 (내부 판별용) + |-------------------------------------------------------------------------- + | + | ATTACHMENT_DISK env 가 명시되었는지 판별하기 위한 키. env() 직접 호출은 + | config:cache 환경에서 항상 null 이므로, config 캐시에 태워 두고 이 값으로 + | 판별한다. null 이면 미명시 — 관리자 환경설정의 storage_driver(s3) 가 + | 업로드 디스크를 전환할 수 있다 (SettingsServiceProvider::applyDriverConfig). + | 빈 값(`ATTACHMENT_DISK=`)도 미명시로 정규화한다 (위 disk 의 사유와 동일). + | + */ + 'disk_explicit' => env('ATTACHMENT_DISK') ?: null, /* |-------------------------------------------------------------------------- diff --git a/config/core.php b/config/core.php index e132f002..3609bbef 100644 --- a/config/core.php +++ b/config/core.php @@ -25,6 +25,8 @@ return [ 'upload_image_max_height_max' => 10000, 'upload_image_quality_min' => 1, 'upload_image_quality_max' => 100, + 'upload_orphan_retention_days_min' => 1, + 'upload_orphan_retention_days_max' => 3650, // SEO 'seo_og_image_default_width_min' => 0, @@ -125,6 +127,19 @@ return [ 'like_operator_default' => 'like', ], + /* + |-------------------------------------------------------------------------- + | 스토리지 — 공개 자산 직접 URL 서빙 디스크 + |-------------------------------------------------------------------------- + | 완전 공개 자산(상품/카테고리/리뷰/에디터 이미지)의 직접 URL(CDN) 서빙에 쓸 + | 디스크입니다. 빈 문자열이면 미설정(기존 PHP 스트리밍 유지)이며, 값은 관리자 + | 환경설정(drivers.public_asset_disk)에서 SettingsServiceProvider 가 주입합니다. + | 확장은 개별 설정으로 이 전역값을 오버라이드할 수 있습니다. + */ + 'storage' => [ + 'public_asset_disk' => '', + ], + /* |-------------------------------------------------------------------------- | 코어 권한 정의 diff --git a/config/filesystems.php b/config/filesystems.php index 56165197..a2f3655b 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -99,8 +99,11 @@ return [ 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), - 'url' => env('AWS_URL'), - 'endpoint' => env('AWS_ENDPOINT'), + // 빈 값(`AWS_URL=`/`AWS_ENDPOINT=`)은 미설정으로 정규화 — env() 는 키가 + // 존재하면 빈 문자열을 돌려주므로, '' 가 endpoint 로 주입되면 S3 클라이언트 + // 구성이 깨진다 (cp .env.example 설치 절차 방어). + 'url' => env('AWS_URL') ?: null, + 'endpoint' => env('AWS_ENDPOINT') ?: null, 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 'throw' => true, 'report' => false, diff --git a/config/schedule_security.php b/config/schedule_security.php index 996463bb..62b63e88 100644 --- a/config/schedule_security.php +++ b/config/schedule_security.php @@ -9,10 +9,38 @@ return [ 'enabled' => env('SCHEDULE_SHELL_ENABLED', false), // 허용 실행 파일 basename 목록. 빈 배열이면 모든 shell 명령을 거부한다. + // Shell 타입은 범용 크론탭 대체이므로 운영자가 python/node/php/bash 등 + // 인터프리터를 등록해 기존 스케줄러 스크립트를 주기 실행할 수 있다. 'allowed_binaries' => array_values(array_filter(array_map( 'trim', explode(',', (string) env('SCHEDULE_SHELL_ALLOWED_BINARIES', '')), ))), + + // 아래 세 목록은 코드 소유 리터럴이다(env 아님) — 스케줄 권한만 위임받은 관리자가 + // 정책을 넓혀 인라인 코드 실행을 복구하지 못하게 한다. + + // 스크립트 실행형 인터프리터 — 첫 인자를 "스크립트 파일 경로" 로 강제한다. + // 위험한 것은 인터프리터 자체가 아니라 인터프리터에게 인라인 코드/명령을 넘기는 것이므로 + // (하이픈 선두=인라인 코드 플래그 거부, 절대경로 요구, `..`/`#` 금지, basename≠artisan) + // 를 적용해 스크립트 파일 실행만 허용한다. + 'script_interpreters' => [ + 'bash', 'sh', 'dash', 'zsh', 'ksh', 'csh', 'tcsh', 'ash', 'fish', + 'php', 'php-cgi', 'python', 'python2', 'python3', + 'perl', 'ruby', 'node', 'nodejs', 'lua', 'luajit', 'tclsh', + 'rscript', 'pwsh', 'powershell', + ], + + // 완전 거부형 — 첫 인자가 코드/애플릿/감싼 명령이라 "스크립트 파일" 모델이 성립하지 않는다. + // allowed_binaries 에 등재돼 있어도 실행하지 않는다. + 'reject_binaries' => [ + 'env', 'xargs', 'sudo', 'doas', 'make', 'awk', 'gawk', 'mawk', 'nawk', 'sed', 'find', + 'busybox', 'nohup', 'timeout', 'watch', 'nice', 'ionice', 'setsid', 'stdbuf', + 'script', 'expect', 'socat', 'nc', 'ncat', 'tee', 'flock', 'chroot', 'unshare', + ], + + // 인터프리터 뒤 스크립트 자리에 올 수 없는 basename — 코어 Artisan 축 우회 방지. + // `php artisan ...` 는 Artisan 타입으로 유도한다. + 'denied_script_names' => ['artisan'], ], 'artisan' => [ @@ -54,6 +82,16 @@ return [ 'notification:cleanup' => ['options' => []], 'layout-previews:cleanup' => ['options' => []], 'ext-bundles:cleanup' => ['options' => []], + 'seo:prune-stats' => ['options' => ['days']], + 'schedules:prune-history' => ['options' => ['days']], + 'identity:expire-challenges' => ['options' => []], + 'identity:prune-logs' => ['options' => ['days']], + 'activity-log:prune' => ['options' => ['days']], + 'notification-log:prune' => ['options' => ['days']], + // 실삭제가 기본이나 사용자 파일 파기는 커맨드 자체가 설정 토글·보존기간으로 이중 확인한다. + 'attachments:prune-orphans' => ['options' => ['dry-run', 'limit', 'days', 'scheduled']], + // 잔존물 정리 — 최신 백업 1개 상시 보존 가드는 커맨드 내부 소유(옵션으로 무력화 불가). + 'storage:prune-leftovers' => ['options' => ['days', 'backup-days', 'dry-run']], // SEO 'seo:warmup' => ['options' => ['layout']], diff --git a/config/settings/defaults.json b/config/settings/defaults.json index 2d57483e..30a388b7 100644 --- a/config/settings/defaults.json +++ b/config/settings/defaults.json @@ -49,7 +49,9 @@ "allowed_extensions": ["jpg", "jpeg", "png", "gif", "webp", "pdf", "doc", "docx", "xls", "xlsx", "zip"], "image_max_width": 2000, "image_max_height": 2000, - "image_quality": 85 + "image_quality": 85, + "orphan_cleanup_enabled": false, + "orphan_retention_days": 30 }, "seo": { "meta_title_suffix": "", @@ -118,11 +120,14 @@ }, "drivers": { "storage_driver": "local", + "public_asset_disk": "none", "s3_bucket": "", "s3_region": "ap-northeast-2", "s3_access_key": "", "s3_secret_key": "", "s3_url": "", + "s3_endpoint": "", + "s3_use_path_style": false, "cache_driver": "file", "redis_host": "127.0.0.1", "redis_port": 6379, @@ -199,7 +204,9 @@ "allowed_extensions_formatted": { "type": "string", "sensitive": false, "computed": true, "source": "allowed_extensions", "transform": "format_extensions" }, "image_max_width": { "type": "integer", "sensitive": false }, "image_max_height": { "type": "integer", "sensitive": false }, - "image_quality": { "type": "integer", "sensitive": false, "expose": false } + "image_quality": { "type": "integer", "sensitive": false, "expose": false }, + "orphan_cleanup_enabled": { "type": "boolean", "sensitive": false, "expose": false }, + "orphan_retention_days": { "type": "integer", "sensitive": false, "expose": false } } }, "seo": { @@ -300,11 +307,14 @@ "_comment": "드라이버 설정 — 프론트엔드 미노출 (admin_settings에서 API 원본값 기반 비교로 변경)", "fields": { "storage_driver": { "type": "string", "sensitive": false }, + "public_asset_disk": { "type": "string", "sensitive": false }, "s3_bucket": { "type": "string", "sensitive": false }, "s3_region": { "type": "string", "sensitive": false }, "s3_access_key": { "type": "string", "sensitive": true }, "s3_secret_key": { "type": "string", "sensitive": true }, "s3_url": { "type": "string", "sensitive": false }, + "s3_endpoint": { "type": "string", "sensitive": false }, + "s3_use_path_style": { "type": "boolean", "sensitive": false }, "cache_driver": { "type": "string", "sensitive": false }, "redis_host": { "type": "string", "sensitive": false }, "redis_port": { "type": "integer", "sensitive": false }, @@ -328,7 +338,8 @@ "websocket_verify_ssl": { "type": "boolean", "sensitive": false }, "websocket_server_host": { "type": "string", "sensitive": false }, "websocket_server_port": { "type": "integer", "sensitive": false }, - "websocket_server_scheme": { "type": "string", "sensitive": false } + "websocket_server_scheme": { "type": "string", "sensitive": false }, + "search_engine_driver": { "type": "string", "sensitive": false } } }, "notifications": { @@ -341,7 +352,7 @@ "expose": true, "_comment": "본인인증 인프라 provider 기술 파라미터 — signup/enabled 등 정책 분기 키는 IdentityPolicy 로 흡수됨", "fields": { - "default_provider": { "type": "string", "sensitive": false }, + "default_provider": { "type": "string", "sensitive": false, "expose": false, "_comment": "저장값을 그대로 SSR 로 내보내면 제거된 플러그인의 provider ID 가 전 공개 페이지에 남는다. 소비처 0 이므로 미노출 — 프론트가 provider 를 알아야 하는 지점(428 응답 / 정책 프리페치)은 레지스트리 대조를 거친 값을 따로 받는다" }, "purpose_providers": { "type": "array", "sensitive": false, "expose": false }, "challenge_ttl_minutes": { "type": "integer", "sensitive": false, "expose": false }, "max_attempts": { "type": "integer", "sensitive": false, "expose": false } diff --git a/docs/backend/admin-settings-access.md b/docs/backend/admin-settings-access.md index 3d0138d1..a96fa95c 100644 --- a/docs/backend/admin-settings-access.md +++ b/docs/backend/admin-settings-access.md @@ -38,10 +38,12 @@ | `drivers.session_lifetime` | `session.lifetime` (testing 차단) | | `drivers.queue_driver` | `queue.default` (testing 차단) | | `drivers.storage_driver` | `filesystems.default` | +| `drivers.public_asset_disk` | `core.storage.public_asset_disk` (`'none'`/빈값 → `''`, testing 차단) | | `drivers.search_engine_driver` | `scout.driver` | | `drivers.redis_*` | `database.redis.*` | | `drivers.memcached_*` | `cache.stores.memcached.*` | -| `drivers.s3_*` | `filesystems.disks.s3.*` | +| `drivers.s3_*` | `filesystems.disks.s3.*` — `s3_url` → `url`(공개 URL base), `s3_endpoint` → `endpoint`(API 요청 대상), `s3_use_path_style` → `use_path_style_endpoint` | +| `drivers.storage_driver` = `s3` | `attachment.disk` = `s3` (단, `ATTACHMENT_DISK` env 명시 시 env 우선 — `attachment.disk_explicit` 로 판별) | | `geoip.feature_enabled`, `geoip.license_key`, `geoip.auto_update_enabled` | `geoip.*` | 위 매핑은 [`app/Providers/SettingsServiceProvider.php`](../../app/Providers/SettingsServiceProvider.php) 가 단일 SSoT. @@ -92,6 +94,40 @@ g7_core_settings('drivers.queue_driver'); // dev 공유 drivers.json 의 datab --- +## config 미러 갱신 시점 (부팅 + 저장) + +`g7_settings.core` / `g7_settings.modules.{id}` / `g7_settings.plugins.{id}` 는 in-memory config 미러다. `g7_core_settings()` / `g7_module_settings()` / `g7_plugin_settings()` 가 이 미러를 읽는다. + +미러는 **부팅 때와 저장 때** 모두 채워진다. 채움 로직은 `App\Support\ExtensionSettingsMirror` 가 단일 소유하며, Provider 와 저장 경로가 같은 코드를 호출한다. + +| 축 | 채우는 지점 | +|----|-----------| +| 코어 | 부팅 `SettingsServiceProvider::register()` / 저장 `SettingsService::invalidateSettingsCache()` | +| 모듈 | 부팅 `CoreServiceProvider` / 저장 각 모듈 SettingsService 의 캐시 초기화 지점에서 `g7_refresh_module_settings_config($id)` | +| 플러그인 | 부팅 `CoreServiceProvider` / 저장 `PluginSettingsService::save()` | + +부팅만으로 채우면 FPM 에서는 요청마다 재부팅되어 드러나지 않지만, 큐 워커·`schedule:work`·Reverb 처럼 프로세스가 상주하는 환경에서는 저장 후에도 그 프로세스가 옛 값을 영원히 읽는다. 새 저장 경로를 만들면 그 자리에서 미러 재채움을 함께 호출한다. + +> 다중 상주 워커 간 브로드캐스트 동기화는 범위 밖이다 — 저장을 수행한 프로세스와 이후 새로 부팅되는 프로세스가 정합하면 충분하고, 워커 재기동은 드라이버 탭의 큐 재시작이 담당한다. + +### 미러는 민감 항목을 담지 않는다 — 민감값은 전용 게터로 + +스키마에 `sensitive: true` 로 선언된 필드는 미러에서 **키 자체가 제거**된다 (암호문도 싣지 않는다). + +미러는 봇 전용 화면 생성기가 레이아웃 표현식에서 참조한 키를 **제한 없이** 조회하는 경로다. 민감값이 실려 있으면 레이아웃이 그 키를 참조하는 순간 평문이 봇 HTML 로 나간다. 브라우저용 설정 전달 경로는 애초에 민감 항목을 제외하므로 화면만 봐서는 드러나지 않는 비대칭이다. + +민감값이 필요한 서버 코드는 전용 게터를 쓴다. + +```php +// 표시·분기용 (민감 항목 제외) +g7_plugin_settings('sirsoft-gdpr', 'duplicate_block_enabled'); + +// 민감값 (복호화 포함) +plugin_setting('sirsoft-pay_kginicis', 'api_key'); +``` + +--- + ## 새 admin 환경설정 키 추가 시 점검 새 카테고리/키를 `storage/app/settings/*.json` 에 추가할 때: diff --git a/docs/backend/api/README.md b/docs/backend/api/README.md index da192033..75eaf33f 100644 --- a/docs/backend/api/README.md +++ b/docs/backend/api/README.md @@ -178,6 +178,21 @@ location ~* \.(js|css|json)$ { expires max; access_log off; } 성공 판정은 상태코드가 아니라 **본문의 매직 토큰과 Content-Type** 으로 합니다. 상태코드만 보면 "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서 영원히 오판합니다. +### 보안 게이트 (KVE-2026 대응) + +일부 관리 엔드포인트에는 표준 응답 외에 다음 보안 게이트가 적용됩니다(각 엔드포인트 표에는 별도 표기가 없어도 공통 적용). + +- **등급 상한 (KVE-2026-1919)** — 사용자·역할 쓰기 경로: + - `PUT /api/admin/users/{user}`, `POST /api/admin/users/{user}/unlock`: 비-슈퍼관리자 액터가 슈퍼 관리자 계정을 수정·잠금해제하려 하면 `403` (`exceptions.cannot_modify_super_admin`). + - `PATCH /api/admin/users/bulk-status`: 비-슈퍼관리자 액터가 포함시킨 슈퍼 관리자 대상은 일괄 처리에서 제외(요청은 `200`, 슈퍼 관리자 상태 불변). + - `POST /api/admin/roles`, `PUT /api/admin/roles/{role}`: 비-슈퍼관리자 액터가 자신이 보유하지 않았거나 자신의 범위(scope)보다 넓은 권한을 부여하려 하면 `403` (`exceptions.cannot_grant_unheld_permission`). + - `PUT /api/admin/roles/{role}`, `PATCH /api/admin/roles/{role}/toggle-status`: 비-슈퍼관리자 액터가 코어/확장 소유 역할(예: `admin`)을 수정·토글하려 하면 `403` (`exceptions.cannot_modify_protected_role`). + - 슈퍼 관리자 액터의 동일 작업은 정상 수행됩니다. +- **레이아웃 저장 표현식/URL 검증 (KVE-2026-1915)** — 레이아웃 생성·수정(`POST/PUT /api/admin/layouts*`)의 `content` 검증: + - `{{...}}`·`computed`·`init_actions`/`actions` 문자열 값에 위험 토큰이 있으면 `422` (`validation.layout.dangerous_expression`). 차단 토큰은 프로토타입 체인 접근(`.constructor`/`.__proto__`/`.prototype`, `['constructor']`, 원시 `__proto__`)·`Function(`·`eval(`·`import(` 입니다. + - `scripts[].src`·`data_sources[].endpoint` 가 same-origin path-only(`/` 시작)가 아니면 `422` (`validation.layout.external_resource_url`). 단, 활성 확장(모듈·플러그인·템플릿)이 자기 manifest 의 `trusted_script_hosts` 로 선언한 호스트는 예외로 허용됩니다 — 이 목록은 확장 배포물이 정하며 요청으로 바꿀 수 없습니다. + - 정상 표현식(조건·계산·목록 가공·화살표 함수·템플릿 리터럴·경로 조립)은 통과합니다. + ## 코어 API 레퍼런스 <!-- @generated:start:api-readme-index --> diff --git a/docs/backend/api/activity-logs.md b/docs/backend/api/activity-logs.md index 0e3d36ef..af4b50ae 100644 --- a/docs/backend/api/activity-logs.md +++ b/docs/backend/api/activity-logs.md @@ -261,7 +261,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.activities.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -313,7 +313,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.activities.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.activities.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/auth.md b/docs/backend/api/auth.md index e8a2b82c..69fca6b7 100644 --- a/docs/backend/api/auth.md +++ b/docs/backend/api/auth.md @@ -535,17 +535,58 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`AuthService::completeTwoFactor()` 가 로그인 세션을 발급해 반환한 배열 — `user` 만 `UserResource` 로 감싼다). 성공 시 페이로드는 일반 로그인(`POST /api/auth/login`)과 동일하다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| user | object | `{"uuid":"a234c2b1-…","name":"홍길동","is_admin":false, …}` | 로그인한 사용자 정보 (`UserResource` — 필드 전수는 `GET /api/auth/user` 응답 필드 표의 기본(코어) 필드와 동일) | +| token | string | `75\|WgPUplvLGTv8YIj4507uIR6dEOHTXyNUed…` | 발급된 Sanctum 접근 토큰 평문 (이후 `Authorization: Bearer` 헤더로 사용, 발급 시 1회만 노출) | +| token_type | string | `Bearer` | 토큰 타입 (항상 `Bearer`) | + +> 위 문서의 실측이 `422` 로 관측된 것은 유효한 challenge 없이 프로브가 호출됐기 때문이다. 정상 흐름(비밀번호 단계가 돌려준 `challenge_id` + 올바른 코드)에서는 `200` 과 위 페이로드가 반환된다. **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "로그인이 성공했습니다.", + "data": { + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com", + "language": "ko", + "status": "active", + "is_admin": false, + "is_owner": true, + "abilities": { + "can_read": true, + "can_create": true, + "can_update": true, + "can_delete": true, + "can_assign_roles": true + } + }, + "token": "{MASKED}", + "token_type": "Bearer" + } +} +``` + +> `user` 객체는 지면 절약을 위해 축약했습니다. 실제로는 `UserResource` 필드 전수가 내려옵니다. **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | +| 401 | Unauthorized | 코드가 틀렸거나(`auth.two_factor_failed`), challenge 의 `purpose` 가 `login` 이 아니거나, 확인된 사용자가 없거나 `active` 상태가 아닌 경우. **세 사유를 같은 응답으로 뭉뚱그린다** — 구분해 내보내면 challenge 유효성 탐색에 쓰인다 | | 422 | Unprocessable Entity | `challenge_id`/`code` 형식 위반 | +| 429 | Too Many Requests | `throttle:auth-login` 초과 (로그인과 같은 제한을 공유하므로 코드 대입 시도도 함께 억제된다) | <!-- @generated:end --> diff --git a/docs/backend/api/core-update.md b/docs/backend/api/core-update.md index a2b60ab0..625f23bf 100644 --- a/docs/backend/api/core-update.md +++ b/docs/backend/api/core-update.md @@ -126,7 +126,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> diff --git a/docs/backend/api/dashboard.md b/docs/backend/api/dashboard.md index cc9ed775..faaf8672 100644 --- a/docs/backend/api/dashboard.md +++ b/docs/backend/api/dashboard.md @@ -214,7 +214,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | <!-- @generated:end --> @@ -323,7 +323,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | <!-- @generated:end --> @@ -395,7 +395,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | <!-- @generated:end --> @@ -481,7 +481,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.dashboard.activities`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.dashboard.read`)이 없는 경우 | <!-- @generated:end --> diff --git a/docs/backend/api/identity.md b/docs/backend/api/identity.md index 9a842161..bace9eeb 100644 --- a/docs/backend/api/identity.md +++ b/docs/backend/api/identity.md @@ -234,7 +234,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.purge`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -352,7 +352,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -513,7 +513,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 정의 생성 중 예외 발생 (`IDV 메시지 정의 생성에 실패했습니다.`) | @@ -713,7 +713,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.read`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1018,7 +1018,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1145,7 +1145,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1417,7 +1417,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1509,7 +1509,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.messages.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1652,7 +1652,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.policies.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -2215,7 +2215,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.admin.identity.logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.admin.identity.providers.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -2634,7 +2634,7 @@ _단건 응답: `data` 객체의 필드 (매칭·활성 정책이 없으면 `dat | scope | string | `hook` | 정책 적용 범위 (route / hook / custom) | | target | string | `sirsoft-board.report.before_create` | 매칭 대상 | | purpose | string | `sensitive_action` | 이 정책이 요구하는 인증 목적 | -| provider_id | null | `null` | 강제할 프로바이더 ID (미지정 시 null) | +| provider_id | null | `null` | 강제할 프로바이더 ID (미지정 시 null). 정책의 저장값을 그대로 내보내지 않고 **현재 등록된 프로바이더로 해석한 값**이다 — 저장값이 등록 목록에 있으면 그대로, 없거나 비어 있으면 인증 목적 기준 폴백 체인(환경설정 기본 프로바이더 → 목적별 지정 → 등록된 첫 프로바이더)의 결과가 실린다. 428 강제 응답의 `verification.provider_id` 와 같은 해석을 공유하므로, 삭제된 플러그인의 식별자가 이 응답에 남지 않는다 | | grace_minutes | integer | `30` | 재인증 유예 시간(분) — 0=매번 요구 | | applies_to | string | `self` | 적용 대상 사용자 (self / admin / both) | | fail_mode | string | `block` | 실패 시 동작 (block: HTTP 428 차단 / log_only: 감사 로그만) | diff --git a/docs/backend/api/language-packs.md b/docs/backend/api/language-packs.md index a8cbb02f..abc9a9f3 100644 --- a/docs/backend/api/language-packs.md +++ b/docs/backend/api/language-packs.md @@ -1005,6 +1005,8 @@ HTTP/1.1 200 **설명** 업로드된 ZIP을 실제로 설치하지 않고 manifest 와 검증 결과만 미리 조회합니다. `core.language_packs.install` 권한이 필요합니다. 부수 효과 없이 읽기만 수행하며 검증 실패 시 422로 응답합니다. 설치 확인 모달에서 대상 언어팩의 메타데이터와 유효성을 미리 보여줄 때 사용합니다. +검증 실패 응답에는 예외 원문이 실리지 않습니다. 상세 진단 정보는 `app.debug` 가 켜진 환경에서만 `debug` 필드로 노출됩니다 — 이 규칙은 이 컨트롤러의 모든 실패 응답(설치·활성화·비활성화·삭제·업데이트 확인·업데이트 수행·캐시 갱신·목록 조회)에 동일하게 적용됩니다. + ### POST /api/admin/language-packs/refresh-cache <!-- @generated:start:api.admin.language-packs.refresh-cache --> @@ -1147,11 +1149,55 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`LanguagePackResource::toDetailArray`). 목록 응답(`GET /api/admin/language-packs`)의 항목 필드에 아래 상세 전용 필드가 더해집니다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| manifest | object\|null | `{"identifier":"g7-core-ja","locale":"ja","version":"1.0.6"}` | 언어팩 `language-pack.json` 원문 | +| validation_summary | object | — | 설치 정합성 요약 | +| validation_summary.target_version_mismatch | boolean | `false` | 대상(코어/확장) 버전과 언어팩이 맞지 않는지 여부 | +| validation_summary.depends_on_core_locale | string\|null | `ja` | manifest `requires.depends_on_core_locale` — 이 팩이 전제하는 코어 로케일 | +| source_meta | object | — | 설치 출처 메타 | +| source_meta.type | string | `zip` | 설치 방식 (`zip`, `github`, `url`, `bundled`) | +| source_meta.url | string\|null | `null` | 원격 설치 시 원본 주소 | +| source_meta.installed_by | integer\|null | `1` | 설치를 수행한 사용자 ID | +| source_meta.latest_version | string\|null | `1.0.6` | 업데이트 확인으로 파악된 최신 버전 | +| source_meta.directory_path | string\|null | `lang-packs/g7-core-ja` | 프로젝트 루트 기준 설치 디렉토리 경로 (해석 실패 시 `null`) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "언어팩 목록을 조회했습니다.", + "data": { + "identifier": "g7-core-ja", + "locale": "ja", + "version": "1.0.6", + "status": "active", + "manifest": { + "identifier": "g7-core-ja", + "locale": "ja", + "version": "1.0.6" + }, + "validation_summary": { + "target_version_mismatch": false, + "depends_on_core_locale": null + }, + "source_meta": { + "type": "bundled", + "url": null, + "installed_by": 1, + "latest_version": "1.0.6", + "directory_path": "lang-packs/g7-core-ja" + } + } +} +``` **에러 응답** @@ -1330,11 +1376,41 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드. `{id}` 가 정수면 설치된 언어팩을, 문자열(번들 식별자)이면 `lang-packs/_bundled/{id}` 를 대상으로 합니다. `CHANGELOG.md` 가 없으면 예외가 아니라 빈 결과로 응답합니다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `g7-core-ja` | 대상 언어팩 식별자 | +| entries | array | `[{"version":"1.0.6","date":"2026-08-12","changes":{"Added":["..."]}}]` | `CHANGELOG.md` 를 파싱한 버전별 항목 목록 (Keep a Changelog 기준). 파일이 없으면 빈 배열 | +| changelog | string | `"# Changelog\n\n## [1.0.6] - 2026-08-12\n..."` | `CHANGELOG.md` 원문. 파일이 없으면 빈 문자열 | +| has_changelog | boolean | `true` | 표시할 변경 이력이 있는지 여부 (`entries` 가 비어 있지 않거나 원문이 빈 문자열이 아니면 `true`) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "언어팩 목록을 조회했습니다.", + "data": { + "identifier": "g7-core-ja", + "entries": [ + { + "version": "1.0.6", + "date": "2026-08-12", + "changes": { + "Added": ["배송지 변경 오류 안내 문구의 일본어 번역을 추가했습니다."] + } + } + ], + "changelog": "# Changelog\n\n## [1.0.6] - 2026-08-12\n\n### Added\n\n- ...\n", + "has_changelog": true + } +} +``` **에러 응답** diff --git a/docs/backend/api/layouts.md b/docs/backend/api/layouts.md index 17c494c5..6bc7e2bc 100644 --- a/docs/backend/api/layouts.md +++ b/docs/backend/api/layouts.md @@ -40,21 +40,86 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생 **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +이 엔드포인트는 `ResponseHelper` 봉투(`success`/`message`/`data`)를 쓰지 않는다. **레이아웃 JSON 자체**를 최상위로 그대로 반환하므로 `data` 래퍼가 없다 — 템플릿 엔진이 실제 레이아웃 응답과 동일하게 소비할 수 있어야 하기 때문이다. + +최상위 키는 레이아웃 JSON 스키마를 따르며, 상속(`extends`) 병합과 모듈/플러그인 layout extension 적용이 **끝난 결과물**이다. + +| 필드 | 타입 | 설명 | +| --- | --- | --- | +| version | string | 레이아웃 스키마 버전 | +| layout_name | string | 레이아웃 식별명 | +| meta | object | 제목·설명·`auth_required`·`is_base`·SEO 설정 등 페이지 메타 | +| components | array | 렌더링 컴포넌트 트리 (상속 병합 + extension 주입 완료 상태) | +| slots | object | 슬롯 정의 (베이스 레이아웃일 때) | +| data_sources | array | API 데이터 소스 정의 | +| computed | object | 계산된 값 정의 | +| state / init_state / initLocal / initGlobal / initIsolated | object | 초기 상태 정의 | +| init_actions | array | 최초 렌더 시 실행할 액션 | +| actions / named_actions | object | 재사용 액션 정의 | +| modals | object | 모달 정의 | +| errorHandling | object | 에러 핸들링 정의 | +| globalHeaders | array | 공통 요청 헤더 (`pattern` + `headers` 쌍) | +| permissions | object | 레이아웃 권한 선언 | + +> `extends` 키는 병합 후 결과에는 남지 않는다 (병합 입력으로만 쓰인다). 실제 포함되는 키 집합은 저장된 레이아웃 내용에 따라 달라진다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: application/json +``` + +```json +{ + "version": "1.0", + "layout_name": "product_list", + "meta": { + "title": "상품 목록", + "auth_required": false + }, + "data_sources": [ + { + "id": "products", + "endpoint": "/api/modules/sirsoft-ecommerce/products", + "method": "GET" + } + ], + "components": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "container mx-auto" }, + "children": [] + } + ] +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 토큰에 해당하는 미리보기가 없거나 **만료된 경우** (`templates.layout_not_found`). 조회 시 `notExpired()` 스코프가 적용되므로 만료 토큰은 미존재와 동일하게 404 다 | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +레이아웃 편집기에서 **저장하지 않은 편집 중 내용**을 실제 화면으로 확인하기 위한 미리보기 서빙 엔드포인트다. 인증이 아니라 **토큰 자체가 보안 메커니즘**이며(`optional.sanctum` — 비회원도 접근 가능), 토큰 유효기간은 발급 시점부터 **30분**이다. + +미리보기 종류는 두 가지다. + +| `preview_type` | 동작 | +| --- | --- | +| `layout` (기본) | 편집 중인 레이아웃 content 를 기준으로 `extends` 상속을 병합한 뒤 모듈/플러그인 extension 을 적용한다 | +| `extension` | 대표 레이아웃을 먼저 병합하고, 편집 중인 **확장 content** 를 그 확장 자리에 임시 치환한 상태로 extension 을 적용한다 | + +주의사항: + +- 토큰은 만료되면 되살릴 수 없다. 편집기에서 미리보기를 다시 열면 새 토큰이 발급된다. +- 확장 미리보기의 임시 치환은 요청 처리 중에만 유효하며, `finally` 로 항상 해제되므로 다른 요청에 새지 않는다. +- 응답이 봉투 없는 원본 JSON 이므로, 이 URL 을 `data_sources` 로 소비할 때 `{{x?.data?.…}}` 가 아니라 최상위 키를 직접 참조한다. ### GET /api/layouts/preview/{token}.json @@ -180,21 +245,54 @@ Authorization: Bearer {YOUR_TOKEN} (optional.sanctum: 비회원은 헤더 생 **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +확장자 없는 형태(`GET /api/layouts/preview/{token}`)와 **같은 컨트롤러 메서드**이므로 응답이 동일하다. `ResponseHelper` 봉투 없이 병합·확장 적용이 끝난 **레이아웃 JSON 자체**를 최상위로 반환한다. 필드 표는 위 항목을 참조한다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: application/json +``` + +```json +{ + "version": "1.0", + "layout_name": "product_list", + "meta": { + "title": "상품 목록", + "auth_required": false + }, + "data_sources": [ + { + "id": "products", + "endpoint": "/api/modules/sirsoft-ecommerce/products", + "method": "GET" + } + ], + "components": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "container mx-auto" }, + "children": [] + } + ] +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 토큰에 해당하는 미리보기가 없거나 **만료된 경우** (`templates.layout_not_found`). 서버의 정적 최적화 블록이 `.json` 확장자를 가로채도 404 가 되므로, 이 경우 확장자 없는 형태로 재요청한다 | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +확장자 붙은 형태의 미리보기 서빙이다. 동작·토큰 수명(30분)·미리보기 종류는 확장자 없는 `GET /api/layouts/preview/{token}` 항목과 동일하다. + +두 형태가 함께 등록되는 이유는 서버 설정 차이 때문이다. 정규식 location(`location ~* \.(js|css|json)$`)이 프리픽스 location 보다 먼저 매칭되는 nginx 구성에서는 `.json` 으로 끝나는 동적 응답이 `try_files ... /index.php` 폴백 없이 404 가 된다. 그래서 라우트는 `Route::dualSuffix()` 로 두 형태를 동시에 등록하고, 클라이언트는 `/api/system/asset-probe` 프로브 결과에 따라 어느 쪽을 쓸지 결정한다. URL 조립은 서버측 `App\Support\AssetUrl`, 프론트측 `resources/js/core/support/assetUrl.ts` 가 담당하며 직접 문자열로 조립하지 않는다. ### GET /api/layouts/{templateIdentifier}/{layoutName}.json diff --git a/docs/backend/api/me.md b/docs/backend/api/me.md index 0c4f08bf..6f1edfc7 100644 --- a/docs/backend/api/me.md +++ b/docs/backend/api/me.md @@ -62,6 +62,32 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 현재 로그인한 사용자가 자신의 계정을 탈퇴한다. 프론트의 회원 탈퇴 모달(`_modal_withdraw.json`)이 호출한다. 별도 요청 파라미터 없이 인증 토큰의 사용자를 대상으로 하며, `UserService::withdrawUser()` 가 아바타·토큰 삭제와 개인정보 익명화를 수행한다. 되돌릴 수 없는 작업이므로 호출 전 사용자 확인 절차를 두는 것을 권장한다. +탈퇴는 원자적으로 처리된다 — 약관 동의 이력 삭제·토큰 삭제·익명화가 하나의 트랜잭션이며, 어느 단계에서 실패해도 전부 취소된다. 아바타 파일 삭제는 커밋이 확정된 뒤에 수행되고, 파일 삭제가 실패해도 탈퇴는 되돌리지 않는다(참조되지 않는 파일만 남는다). + +익명화된 이메일에는 사용자 ID 가 포함되어 구조적으로 유일하다 — 같은 이메일로 재가입한 회원이 같은 날 다시 탈퇴해도 충돌하지 않는다. + +탈퇴 처리 중 예기치 못한 오류(예: 데이터베이스 예외)가 발생하면 500 과 함께 고정된 안내 메시지(`user.withdraw_failed`)만 반환한다 — 예외 원문(SQL 상태코드·쿼리 등)은 응답에 싣지 않고 서버 로그에만 기록한다(회원에게 노출되는 경로이므로 내부 정보 유출을 차단한다). + +**추가 오류 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 422 | Validation Error | 관리자 역할 보유 계정 또는 수퍼관리자 계정의 탈퇴 시도 (`errors.general` 에 사유) | + +```json +{ + "success": false, + "message": "회원 탈퇴에 실패했습니다.", + "errors": { + "general": [ + "관리자 계정은 탈퇴할 수 없습니다." + ] + } +} +``` + +> 차단 응답은 종전에 500 이었으나, 잘못된 요청이지 서버 오류가 아니므로 422 로 바뀌었다. 활동 로그도 탈퇴가 성공한 경우에만 기록된다. + ### GET /api/me <!-- @generated:start:api.me.show --> @@ -345,7 +371,7 @@ _단건 응답: `data` 객체의 필드 (`UserResource::toArray()` 산물 — GE | created_at | string | `2026-07-08 10:41:24` | 생성 일시 (사용자 시간대 기준 문자열) | | updated_at | string | `2026-07-08 11:02:10` | 수정 일시 (사용자 시간대 기준 문자열) | | is_owner | boolean | `true` | 현재 인증 사용자가 이 리소스의 소유자인지 여부 (BaseApiResource 표준 메타) | -| abilities | object | `{"can_read":false,"can_create":false,"can_update":false,"can_delete":false,"can_assign_roles":false}` | 현재 사용자의 이 리소스에 대한 권한 맵 (core.users.read/create/update/delete, core.permissions.update 기준. 슈퍼관리자 계정은 `can_delete` 가 항상 false) | +| abilities | object | `{"can_read":false,"can_create":false,"can_update":false,"can_delete":false,"can_assign_roles":false}` | 현재 사용자의 이 리소스에 대한 권한 맵 (core.users.read/create/update/delete 기준. `can_assign_roles` 는 `core.users.update` — 역할 부여는 사용자 관리의 일부. 슈퍼관리자 계정은 `can_delete` 가 항상 false) | 관계형 필드(`modules`, `plugins`, `menus`, `roles`, `permissions`, `consents`, `terms_consent`, `privacy_consent`)와 카운트 필드(`modules_count`, `plugins_count`, `menus_count`)는 해당 관계가 로드된 경우에만 응답에 포함된다 (프로필 수정 응답에서는 로드하지 않으므로 나타나지 않는다). diff --git a/docs/backend/api/menus.md b/docs/backend/api/menus.md index 822e0e83..495bc945 100644 --- a/docs/backend/api/menus.md +++ b/docs/backend/api/menus.md @@ -324,7 +324,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -742,7 +742,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `moved_items[].new_parent_id` 가 자기 자신·자손을 가리키는 순환 참조인 경우 포함 | <!-- @generated:end --> @@ -798,7 +798,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1102,7 +1102,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1198,7 +1198,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.menus.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/modules.md b/docs/backend/api/modules.md index 1a60240d..1bee33e7 100644 --- a/docs/backend/api/modules.md +++ b/docs/backend/api/modules.md @@ -176,7 +176,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read\|core.menus.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -266,7 +266,7 @@ _단건 응답: `data` 객체의 필드 (`data.module` 은 목록과 동일한 ` | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | | 409 | Conflict | `force` 없이 호출했고 필요한 의존 확장이 미충족인 경우 (`error` 에 `warning`, `missing_modules`, `missing_plugins` 포함) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 활성화 처리 중 예외 발생 (`module.activate_failed`) | @@ -333,7 +333,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 422 | Unprocessable Entity | 업데이트 확인 처리가 실패한 경우 (`modules.check_updates_failed`) | | 500 | Internal Server Error | 업데이트 확인 중 예외 발생 | @@ -439,7 +439,7 @@ _단건 응답: `data` 는 비활성화된 모듈의 `ModuleResource` 객체 ( | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | | 409 | Conflict | `force` 없이 호출했고 이 모듈에 의존하는 활성 확장이 있는 경우 (`error` 에 `warning`, `dependent_templates`, `dependent_modules`, `dependent_plugins` 포함) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 비활성화 처리 중 예외 발생 (`module.deactivate_failed`) | @@ -560,7 +560,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 설치 파이프라인이 던진 검증 오류 (의존 확장 cascade 설치 실패·이미 설치됨 등 — `error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 설치 처리 중 예외 발생 (`modules.installation_failed`) | @@ -671,7 +671,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 422 | Unprocessable Entity | 파일 검증 실패(ZIP 아님·50MB 초과), 또는 ZIP 처리 오류 (module.json 미존재/형식 오류·식별자 누락·이미 설치됨) | | 500 | Internal Server Error | 설치 처리 중 예외 발생 (`module.install_failed`) | @@ -779,7 +779,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 422 | Unprocessable Entity | URL 형식 검증 실패, 또는 GitHub 처리 오류 (저장소 미존재·다운로드 실패·module.json 형식 오류·이미 설치됨) | | 500 | Internal Server Error | 설치 처리 중 예외 발생 (`module.install_failed`) | @@ -1001,7 +1001,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 422 | Unprocessable Entity | 파일 검증 실패(ZIP 아님·50MB 초과), 또는 미리보기 처리 실패 (`module.preview_failed` — `error.error` 에 사유) | <!-- @generated:end --> @@ -1104,7 +1104,7 @@ _단건 응답: `data` 는 갱신된 모듈의 `ModuleResource` 객체 (목록 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.activate`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 레이아웃 갱신 실패 (모듈 미존재·비활성 상태 — `modules.refresh_layouts_failed`) | | 500 | Internal Server Error | 갱신 처리 중 예외 발생 (`module.refresh_layouts_failed`) | @@ -1155,7 +1155,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.uninstall`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 제거 처리 중 예외 발생 (`module.uninstall_failed`) | @@ -1242,7 +1242,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -1300,7 +1300,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1374,7 +1374,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1433,7 +1433,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1550,7 +1550,7 @@ _단건 응답: `data` 객체의 필드 (`ModuleResource::toDetailArray()` + 주 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 모듈이 활성/_pending/_bundled 어디에도 없는 경우 (`module.not_found`) | | 500 | Internal Server Error | 조회 중 예외 발생 (`module.fetch_failed`) | @@ -1620,7 +1620,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.read`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 모듈이 활성/_pending/_bundled 어디에도 없는 경우 (`module.not_found`) | | 422 | Unprocessable Entity | 수정 레이아웃 확인 실패 (`modules.check_modified_layouts_failed` — `error.errors.module_name`) | | 500 | Internal Server Error | 확인 처리 중 예외 발생 | @@ -1728,7 +1728,7 @@ _단건 응답: `data` 객체의 필드 (`target` + `dependencies[]` + `language | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 모듈이 활성/_pending/_bundled 어디에도 없는 경우 (`module.not_found`) | | 500 | Internal Server Error | 대상 확장을 찾을 수 없거나 프리뷰 빌드 중 예외 발생 (`module.fetch_failed`) | @@ -1821,7 +1821,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.uninstall`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 모듈을 찾을 수 없는 경우 (`module.not_found`) | | 500 | Internal Server Error | 삭제 정보 조회 중 예외 발생 (`module.uninstall_info_failed`) | @@ -1932,7 +1932,7 @@ _단건 응답: `data` 는 업데이트된 모듈의 `ModuleResource` 객체 ( | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.modules.read \| core.menus.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.modules.install`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 모듈을 찾을 수 없는 경우 (`module.not_found`) | | 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 업데이트 실패 (업데이트 소스 없음·다운그레이드 차단·코어 버전 비호환 — `error.errors.module_name` 에 사유) | | 500 | Internal Server Error | 업데이트 처리 중 예외 발생 (`modules.errors.update_failed`) | @@ -1965,11 +1965,26 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 표준 JSON 봉투가 아니라 **모듈 에셋 파일 본문** 을 그대로 반환한다 — `data` 구조가 없다._ + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `파일 확장자에 따른 MIME (예: text/javascript, image/png)` | 서빙 대상의 MIME 타입 | +| Cache-Control | `public, max-age=31536000, immutable` (프로덕션) / `no-cache` (그 외) | 환경에 따라 갈린다 | +| ETag | `{md5(mtime+size)}` | `If-None-Match` 가 일치하면 본문 없이 `304` | **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/javascript +Cache-Control: public, max-age=31536000, immutable +ETag: "9f2c…" + +(function(){ /* 모듈 에셋 본문 */ })(); +``` + +> 같은 ETag 로 재요청하면 본문 없이 `304 Not Modified` 가 반환된다. **에러 응답** @@ -2137,15 +2152,35 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 표준 JSON 봉투가 아니라 **활성 모듈 CSS 를 병합한 번들 본문** 을 그대로 반환한다 — `data` 구조가 없다._ + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `text/css` | 서빙 대상의 MIME 타입 | +| Cache-Control | `public, max-age=31536000, immutable` (프로덕션) / `no-cache` (그 외) | 환경에 따라 갈린다 | +| ETag | `{md5(mtime+size)}` | `If-None-Match` 가 일치하면 본문 없이 `304` | **응답 예시** -<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/css +Cache-Control: public, max-age=31536000, immutable +ETag: "9f2c…" + +/* module-a */ .a{} +/* module-b */ .b{} +``` + +> 같은 ETag 로 재요청하면 본문 없이 `304 Not Modified` 가 반환된다. **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 200 | OK (빈 본문) | 활성 확장이 없거나 병합할 에셋이 없는 경우 — 오류가 아니라 빈 번들이다 | + +> 개별 확장의 병합이 실패하면 그 확장만 건너뛰고 나머지는 그대로 병합된다(실패 격리). 건너뛴 사실은 서버 로그(warning)에 남는다. <!-- @generated:end --> @@ -2172,15 +2207,36 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 표준 JSON 봉투가 아니라 **활성 모듈 JS(IIFE)를 병합한 번들 본문** 을 그대로 반환한다 — `data` 구조가 없다._ + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `text/javascript` | 서빙 대상의 MIME 타입 | +| Cache-Control | `public, max-age=31536000, immutable` (프로덕션) / `no-cache` (그 외) | 환경에 따라 갈린다 | +| ETag | `{md5(mtime+size)}` | `If-None-Match` 가 일치하면 본문 없이 `304` | **응답 예시** -<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/javascript +Cache-Control: public, max-age=31536000, immutable +ETag: "9f2c…" + +(function(){/* module-a */})() +; +(function(){/* module-b */})() +``` + +> 같은 ETag 로 재요청하면 본문 없이 `304 Not Modified` 가 반환된다. **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 200 | OK (빈 본문) | 활성 확장이 없거나 병합할 에셋이 없는 경우 — 오류가 아니라 빈 번들이다 | + +> 개별 확장의 병합이 실패하면 그 확장만 건너뛰고 나머지는 그대로 병합된다(실패 격리). 건너뛴 사실은 서버 로그(warning)에 남는다. <!-- @generated:end --> @@ -2211,7 +2267,13 @@ Accept: application/json -<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. --> +_`data` 는 모듈 의 `components.json` 내용을 그대로 담은 **컴포넌트 맵**이다 (고정 필드 집합이 아니라 컴포넌트명 → 정의 매핑)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (컴포넌트명) | object | `{"type":"composite","props":{…}}` | 컴포넌트 정의. 키는 레이아웃 JSON 의 `name` 과 일치한다 | + +> 파일이 없거나 비어 있으면 `data` 는 빈 객체(`{}`)다 — 오류가 아니다. **응답 예시** @@ -2281,7 +2343,24 @@ _이 엔드포인트는 표준 `success/message/data` 봉투를 사용하지 않 **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "ProductCard": { + "type": "composite", + "props": { + "product": "object" + } + } + } +} +``` **에러 응답** diff --git a/docs/backend/api/notification-definitions.md b/docs/backend/api/notification-definitions.md index 9e8d89af..60bc034f 100644 --- a/docs/backend/api/notification-definitions.md +++ b/docs/backend/api/notification-definitions.md @@ -464,7 +464,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -663,7 +663,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -797,7 +797,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/notification-logs.md b/docs/backend/api/notification-logs.md index 6075d782..2caf906e 100644 --- a/docs/backend/api/notification-logs.md +++ b/docs/backend/api/notification-logs.md @@ -235,7 +235,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -287,7 +287,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notification-logs.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notification-logs.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/notification-templates.md b/docs/backend/api/notification-templates.md index 2a15be54..3b788ff9 100644 --- a/docs/backend/api/notification-templates.md +++ b/docs/backend/api/notification-templates.md @@ -200,7 +200,7 @@ _단건 응답: `data` 객체의 필드 (`NotificationTemplateResource`)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -302,7 +302,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -404,7 +404,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/notifications.md b/docs/backend/api/notifications.md index 6326e24a..68252e76 100644 --- a/docs/backend/api/notifications.md +++ b/docs/backend/api/notifications.md @@ -202,7 +202,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -260,7 +260,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -329,7 +329,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -441,7 +441,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -526,7 +526,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.notifications.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -664,7 +664,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -722,7 +722,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -780,7 +780,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -849,7 +849,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -907,7 +907,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.read`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -961,7 +961,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1046,7 +1046,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.notifications.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.user-notifications.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/plugins.md b/docs/backend/api/plugins.md index 6cc66aad..a84769ba 100644 --- a/docs/backend/api/plugins.md +++ b/docs/backend/api/plugins.md @@ -1581,6 +1581,8 @@ HTTP/1.1 200 **설명** 플러그인의 현재 설정 값을 조회합니다. `core.plugins.read` 권한이 필요합니다. 저장된 설정이 없거나 플러그인을 찾을 수 없으면 404 를 반환합니다. 설정 페이지 진입 시 폼의 현재 값을 채우는 용도이며, 폼 스키마/UI 구성은 별도의 `settings/layout` 엔드포인트에서 조회합니다. +설정 스키마(getSettingsSchema)에 `public_asset_disk` 키를 선언한 플러그인(예: `sirsoft-ckeditor5`)은 응답 `data` 에 공개 자산 디스크 카탈로그 `available_public_asset_disks` 가 함께 부착됩니다 — 각 항목은 `{id, label(로케일별 맵), provider?}` 구조입니다. 설정 화면이 코어 환경설정 API(`core.settings.read`)를 따로 조회하지 않고 이 응답 하나로 선택지를 구성하게 하기 위한 것으로, 이 키는 저장 대상이 아니며 PUT 시 검증 whitelist 에서 걸러집니다. + ### PUT /api/admin/plugins/{identifier}/settings <!-- @generated:start:api.admin.plugins.settings.update --> @@ -1641,7 +1643,7 @@ _단건 응답: `data` 는 저장 후 다시 조회한 설정 값 맵입니다. <!-- @generated:end --> -**설명** 플러그인의 설정 값을 저장합니다. `core.plugins.update` 권한이 필요합니다. 저장 대상은 검증을 통과한 값뿐이며, 검증 규칙은 해당 플러그인이 선언한 설정 스키마에서 생성됩니다. 스키마에 없는 필드를 요청에 포함해도 저장되지 않습니다. PluginManager 에 등록되지 않은 플러그인은 저장 자체가 수행되지 않아 500 을 반환합니다. 성공 시 갱신된 설정 값을 함께 반환합니다. +**설명** 플러그인의 설정 값을 저장합니다. `core.plugins.update` 권한이 필요합니다. 저장 대상은 검증을 통과한 값뿐이며, 검증 규칙은 해당 플러그인이 선언한 설정 스키마에서 생성됩니다. 스키마에 없는 필드를 요청에 포함해도 저장되지 않습니다. PluginManager 에 등록되지 않은 플러그인은 저장 자체가 수행되지 않아 500 을 반환합니다. 성공 시 갱신된 설정 값을 함께 반환합니다. 설정 스키마에 `public_asset_disk` 키를 선언한 플러그인은 저장 응답에도 `available_public_asset_disks` 카탈로그가 재부착됩니다 (GET 응답과 동형 — 저장 직후 화면 폼 상태 갱신용). ### GET /api/admin/plugins/{identifier}/settings/layout @@ -2226,11 +2228,30 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 JSON 봉투를 반환하지 않습니다 — `?file=` 쿼리로 받은 경로의 에셋 +파일 원본 바이트를 그대로 서빙합니다 (성공 시 200 또는 304). 파일 경로 미전달 +(`file` 쿼리 부재)은 422 로 응답합니다._ + +| 응답 헤더 | 예시값 | 용도/설명 | +| --- | --- | --- | +| Content-Type | `text/javascript` | 파일 확장자에서 감지한 MIME 타입 | +| ETag | `"a1b2c3d4…"` | 파일 내용 기반 검증자 (재요청 시 304 판정) | +| Cache-Control | `public, max-age=31536000` | 1년 캐시 (환경별로 달라짐 — 비프로덕션은 no-cache) | **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +GET /api/plugins/assets/sirsoft-daum_postcode?file=dist%2Fjs%2Fplugin.iife.js HTTP/1.1 +``` + +```http +HTTP/1.1 200 +Content-Type: text/javascript +Cache-Control: public, max-age=31536000 +ETag: "a1b2c3d4e5f6" + +(function(){/* 플러그인 IIFE 번들 원본 바이트 */})(); +``` **에러 응답** @@ -2241,7 +2262,7 @@ Accept: application/json <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** 플러그인 에셋 서빙(`GET /api/plugins/assets/{identifier}/{path}`)의 확장자 없는 이중 모드 변형입니다. 파일 경로를 경로 세그먼트 대신 `?file=` 쿼리로 받으며, 검증·에러·캐시 동작은 경로 세그먼트 형태와 동일합니다. `.js`·`.css` 주소를 가로채는 정적 파일 최적화 서버 설정에서 확장자 붙은 주소가 404 가 될 때 프론트가 이 형태로 자동 전환합니다 (자산 URL 이중 모드). ### GET /api/plugins/assets/{identifier}/{path} @@ -2425,19 +2446,36 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 JSON 봉투를 반환하지 않습니다 — 활성 플러그인 CSS 를 병합한 +텍스트를 그대로 서빙합니다 (성공 시 200 또는 304). `GET /api/plugins/bundle.css` +와 동일 응답입니다._ + +| 응답 헤더 | 예시값 | 용도/설명 | +| --- | --- | --- | +| Content-Type | `text/css` | 항상 CSS | +| ETag | `"a1b2c3d4…"` | 병합 파일 내용 기반 검증자 (병합 파일이 있는 경우) | +| Cache-Control | `public, max-age=31536000` | 1년 캐시 (환경별로 달라짐 — 비프로덕션은 매 요청 재병합) | + +활성 global 플러그인 에셋이 하나도 없으면 본문이 빈 200 응답(`text/css`) 입니다. **응답 예시** -<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/css +Cache-Control: public, max-age=31536000 + +/* sirsoft-gdpr */ +.g7-gdpr-banner{position:fixed;bottom:0} +``` **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +_에러 응답 없음 — 공개 조회이며 요청 파라미터가 없고, 활성 에셋이 하나도 없는 경우에도 빈 200 을 반환합니다._ <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** 플러그인 CSS 번들(`GET /api/plugins/bundle.css`)의 확장자 없는 이중 모드 변형입니다. 동작·응답이 확장자 형태와 동일하며, `.css` 주소를 가로채는 정적 파일 최적화 서버 설정에서 프론트가 이 형태로 자동 전환합니다 (자산 URL 이중 모드). ### GET /api/plugins/bundle/js @@ -2460,19 +2498,37 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 JSON 봉투를 반환하지 않습니다 — 활성 플러그인 IIFE JS 를 병합한 +텍스트를 그대로 서빙합니다 (성공 시 200 또는 304). `GET /api/plugins/bundle.js` +와 동일 응답입니다._ + +| 응답 헤더 | 예시값 | 용도/설명 | +| --- | --- | --- | +| Content-Type | `text/javascript` | 항상 JavaScript | +| ETag | `"a1b2c3d4…"` | 병합 파일 내용 기반 검증자 (병합 파일이 있는 경우) | +| Cache-Control | `public, max-age=31536000` | 1년 캐시 (환경별로 달라짐 — 비프로덕션은 매 요청 재병합) | + +활성 global 플러그인 에셋이 하나도 없으면 본문이 빈 200 응답(`text/javascript`) 입니다. 병합 시 각 IIFE 사이는 `\n;\n` 구분자로 연결됩니다. **응답 예시** -<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/javascript +Cache-Control: public, max-age=31536000 + +(function(){/* sirsoft-gdpr plugin.iife.js */})(); +; +(function(){/* sirsoft-ckeditor5 plugin.iife.js */})(); +``` **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +_에러 응답 없음 — 공개 조회이며 요청 파라미터가 없고, 활성 에셋이 하나도 없는 경우에도 빈 200 을 반환합니다._ <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** 플러그인 JS 번들(`GET /api/plugins/bundle.js`)의 확장자 없는 이중 모드 변형입니다. 동작·응답이 확장자 형태와 동일하며, `.js` 주소를 가로채는 정적 파일 최적화 서버 설정에서 프론트가 이 형태로 자동 전환합니다 (자산 URL 이중 모드). ### GET /api/plugins/{identifier}/components @@ -2497,9 +2553,19 @@ Accept: application/json **응답 필드** (`data` 내부) +_이 엔드포인트는 `success/message/data` 봉투를 사용하지 않습니다 — 플러그인의 +`components.json` 원문을 그대로 JSON body 로 반환합니다 (`Cache-Control: public, +max-age=3600`). 파일이 없는 구버전 플러그인은 빈 객체(`{}`) 로 폴백합니다._ - -<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. --> +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| $schema | string | `https://json-schema.org/draft/2020-12/schema` | 매니페스트 JSON 스키마 참조 | +| identifier | string | `sirsoft-daum_postcode` | 이 매니페스트를 소유한 플러그인 식별자 | +| version | string | `1.0.0` | 매니페스트(플러그인) 버전 | +| components | object | `{"basic":[],"composite":[],"layout":[]}` | 타입별 컴포넌트 정의 맵 | +| components.basic | array | `[]` | Basic 컴포넌트 정의 목록 (HTML 래핑 계층) | +| components.composite | array | `[]` | Composite 컴포넌트 정의 목록 (집합 컴포넌트) | +| components.layout | array | `[]` | Layout 컴포넌트 정의 목록 | **응답 예시** @@ -2528,7 +2594,7 @@ HTTP/1.1 200 <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** 플러그인 컴포넌트 매니페스트(`GET /api/plugins/{identifier}/components.json`)의 확장자 없는 이중 모드 변형입니다. 응답·캐시·폴백 동작이 확장자 형태와 동일하며, `.json` 주소를 가로채는 정적 파일 최적화 서버 설정에서 프론트가 이 형태로 자동 전환합니다 (자산 URL 이중 모드). ### GET /api/plugins/{identifier}/components.json diff --git a/docs/backend/api/profile.md b/docs/backend/api/profile.md index c66ccf19..7c77b125 100644 --- a/docs/backend/api/profile.md +++ b/docs/backend/api/profile.md @@ -279,7 +279,7 @@ _단건 응답: `data` 객체의 필드 (`UserResource::toArray()` 산물 — `s | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 403 | Forbidden | 요구 권한(`core.profile.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.profile.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> diff --git a/docs/backend/api/roles.md b/docs/backend/api/roles.md index bf1eda1e..8c2bf050 100644 --- a/docs/backend/api/roles.md +++ b/docs/backend/api/roles.md @@ -273,7 +273,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -414,7 +414,7 @@ HTTP/1.1 200 **설명** -셀렉트 UI(사용자 폼·메뉴 편집의 역할 선택 등)에 채울 활성 역할 목록을 제공한다. 별도 권한 미들웨어가 없어 인증만 되면 호출 가능하지만, 내부에서 권한에 따라 범위가 갈린다. `core.permissions.read` 권한 보유자는 전체 활성 역할을 받고(사용자에게 역할을 부여하는 관리 용도), 미보유자는 자신에게 부여된 활성 역할만 받는다(자기 정보 폼 표시 용도). 응답의 `abilities.can_assign_roles` 는 `core.permissions.update` 권한 보유 여부를 나타낸다. +셀렉트 UI(사용자 폼·메뉴 편집의 역할 선택 등)에 채울 활성 역할 목록을 제공한다. 별도 권한 미들웨어가 없어 인증만 되면 호출 가능하지만, 내부에서 권한에 따라 범위가 갈린다. `core.permissions.read` 권한 보유자는 전체 활성 역할을 받고(사용자에게 역할을 부여하는 관리 용도), 미보유자는 자신에게 부여된 활성 역할만 받는다(자기 정보 폼 표시 용도). 응답의 `abilities.can_assign_roles` 는 `core.users.update`(사용자 관리) 권한 보유 여부를 나타낸다 — 역할 부여는 사용자 관리의 일부이지 역할 정의 수정(`core.permissions.update`)이 아니다. 부여 가능한 개별 역할의 범위는 서버 상한(권한 상승 가드)이 역할별로 강제한다. ### DELETE /api/admin/roles/{role} @@ -444,14 +444,24 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "역할이 성공적으로 삭제되었습니다.", + "data": null +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -652,7 +662,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -754,7 +764,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.permissions.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.permissions.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/schedules.md b/docs/backend/api/schedules.md index b7c5a56f..0f866a15 100644 --- a/docs/backend/api/schedules.md +++ b/docs/backend/api/schedules.md @@ -286,7 +286,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -346,7 +346,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -415,7 +415,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -467,7 +467,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | | 404 | Not Found | 지정한 `historyId` 의 실행 이력이 존재하지 않는 경우 (`schedule.history_not_found`) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -588,7 +588,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -842,7 +842,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -946,7 +946,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.create`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1147,7 +1147,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.schedules.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.schedules.run`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | diff --git a/docs/backend/api/search.md b/docs/backend/api/search.md index c3666f8a..4270031f 100644 --- a/docs/backend/api/search.md +++ b/docs/backend/api/search.md @@ -21,7 +21,7 @@ <!-- @generated:start:api.search --> - **라우트명**: `api.search` - **컨트롤러**: `App\Http\Controllers\Api\Public\PublicSearchController@search` -- **인증/권한**: 공개 (인증 불필요) +- **인증/권한**: `optional.sanctum` (선택적 인증: 회원/비회원 모두 접근) **요청 파라미터** @@ -62,10 +62,16 @@ _단건 응답: `data` 객체의 필드._ | next_cursor | string\|null | `null` | 다음 페이지 커서. 커서 방식으로 응답했을 때만 채워지며, `page` 방식 응답에서는 `null` 입니다 | | prev_cursor | string\|null | `null` | 이전 페이지 커서. 위와 같습니다 | | counts_are_exact | object | `{}` | 카테고리별 총 건수 정확도 (`{"posts": true, "products": false}`). 탭 배지가 카테고리마다 그려지므로 정확도도 카테고리마다 제공됩니다 — 정확하지 않은 배지는 화면에서 "이상" 으로 표기됩니다 | +| categories_failed | object | `{}` | 카테고리별 검색 실패 여부 (`{"posts": true, "products": false}`). 카테고리 검색이 서버 예외로 실패하면 그 카테고리만 `true` 가 되며, 화면은 이 값으로 "검색 결과 없음" 과 구분되는 오류 안내를 그립니다. 실패해도 HTTP 는 200 입니다 (다른 카테고리 결과는 정상 전달) | +| search_failed | boolean | `false` | 하나 이상의 카테고리가 실패했는지 여부 (`categories_failed` 의 논리합) | 카테고리(탭) 중 하나라도 상한에 걸리면 합계도 정확하지 않습니다 — 정확한 카테고리 몇 개를 더해 봐야 전체가 정확해지지 않기 때문입니다. 그 경우 `total_is_exact` 는 `false` 가 됩니다. +실패한 카테고리의 페이로드는 `failed: true` 와 함께 `total: 0`, `total_is_exact: false` +(`total_relation: at_least`) 로 내려갑니다 — 실패한 0건을 "정확한 0건" 으로 말하지 않기 +위함입니다. 배지·건수 표기는 이 정확도를 그대로 따릅니다. + > 상한·페이지 이동 규약 상세: [pagination.md](../pagination.md) **응답 예시** @@ -98,6 +104,6 @@ HTTP/1.1 200 **설명** -프론트엔드 통합 검색(`search/index.json`)이 호출하는 공개 엔드포인트입니다. 인증이 필요 없으며 게스트도 사용할 수 있습니다. 코어 컨트롤러는 검색 결과를 직접 생성하지 않고, 검증된 파라미터로 검색 컨텍스트(q/type/sort/page/per_page 및 요청 객체)를 구성한 뒤 `core.search.results` Filter 훅을 실행합니다. 게시판·상품 등 각 검색 대상 모듈이 이 훅에 리스너를 등록해 자신의 카테고리 결과를 추가하고, `core.search.build_response` 훅으로 응답 구조를 완성합니다. 따라서 활성 검색 모듈이 없으면 항상 빈 결과(`total: 0`)가 반환됩니다. 검색 엔진 자체는 Scout + `DatabaseFulltextEngine`(MySQL FULLTEXT) 기반이며, 상세는 `docs/backend/search-system.md`를 참고하세요. +프론트엔드 통합 검색(`search/index.json`)이 호출하는 공개 엔드포인트입니다. 인증 없이(게스트) 사용할 수 있고, Bearer 토큰을 보내면 회원으로 해석되어 게시판별 열람 권한이 검색 결과와 `available_boards` 필터 목록에 반영됩니다(`optional.sanctum` — 위조 토큰은 401, 만료 토큰은 게스트로 처리). 코어 컨트롤러는 검색 결과를 직접 생성하지 않고, 검증된 파라미터로 검색 컨텍스트(q/type/sort/page/per_page 및 요청 객체)를 구성한 뒤 `core.search.results` Filter 훅을 실행합니다. 게시판·상품 등 각 검색 대상 모듈이 이 훅에 리스너를 등록해 자신의 카테고리 결과를 추가하고, `core.search.build_response` 훅으로 응답 구조를 완성합니다. 따라서 활성 검색 모듈이 없으면 항상 빈 결과(`total: 0`)가 반환됩니다. 검색 엔진 자체는 Scout + `DatabaseFulltextEngine`(MySQL FULLTEXT) 기반이며, 상세는 `docs/backend/search-system.md`를 참고하세요. diff --git a/docs/backend/api/seo.md b/docs/backend/api/seo.md index badfd233..29075368 100644 --- a/docs/backend/api/seo.md +++ b/docs/backend/api/seo.md @@ -133,7 +133,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 캐시 무효화(`invalidateByLayout` / `clearAll`) 중 예외가 발생한 경우 (`messages.error_occurred`) | @@ -206,7 +206,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | <!-- @generated:end --> @@ -411,7 +411,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | 워밍업 처리 중 예외가 발생한 경우 (`messages.error_occurred`) | <!-- @generated:end --> diff --git a/docs/backend/api/settings.md b/docs/backend/api/settings.md index f8c1695e..88cec79c 100644 --- a/docs/backend/api/settings.md +++ b/docs/backend/api/settings.md @@ -71,12 +71,14 @@ _단건 응답: `data` 객체의 필드._ | geoip | object | `{"geoip_enabled":false,"geoip_license_key":null,"geoip_au…` | GeoIP 원본 카테고리 (GeoIP 사용 여부·MaxMind 라이선스 키·DB 자동 갱신 사용). advanced 탭에 병합된 파생 뷰 | | notifications | object | `{"channels":[{"id":"mail","is_active":true,"sort_order":1…` | 알림 탭 설정 그룹. channels 는 알림 채널 목록으로 각 원소가 id(채널 식별자)·is_active(활성 여부)·sort_order(표시 순서)를 가짐 | | identity | object | `{"default_provider":"g7:core.mail","purpose_providers":{"…` | 본인인증(IDV) 탭 설정 그룹 (기본 provider·목적별 provider 매핑(purpose_providers)·챌린지 유효시간(분)·최대 시도 횟수) | -| available_drivers | object | `{"storage":[{"id":"local","label":{"ko":"로컬","en":"Local"…` | 드라이버 선택지 카탈로그 (DriverRegistryService 산물). 종류별(storage/cache/session/queue 등) 선택 가능한 드라이버 목록을 id/다국어 label 형태로 제공 | -| _meta | object | `{"limits":{"upload_max_file_size_min":1,"upload_max_file_…` | <!-- TODO: 설명 --> | +| available_drivers | object | `{"storage":[{"id":"local","label":{"ko":"로컬","en":"Local"…` | 드라이버 선택지 카탈로그 (DriverRegistryService 산물). 종류별(storage/public_asset/cache/session/queue 등) 선택 가능한 드라이버 목록을 id/다국어 label 형태로 제공. `public_asset` 은 공개 자산 직접 URL 서빙 디스크 선택지 (코어 none/public/s3 + 플러그인 훅 등록분) | +| _meta | object | `{"limits":{"upload_max_file_size_min":1,"upload_max_file_…` | 설정값이 아니라 화면이 쓰는 메타. `limits` 는 각 설정 항목의 min/max 경계값 맵 (`config/core.php` 의 `settings_limits` 가 SSoT, 화면 입력 힌트와 FormRequest 검증이 같은 값을 공유) | | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | **응답 예시** +<!-- @probed --> + ```http HTTP/1.1 200 ``` @@ -120,19 +122,19 @@ HTTP/1.1 200 "webp", "... (총 11건 중 5건 표시)" ], - "image_max_width": "800", - "image_max_height": "600", + "image_max_width": 2000, + "image_max_height": 2000, "image_quality": 85 }, "seo": { - "meta_title_suffix": null, - "meta_description": null, - "meta_keywords": null, - "google_analytics_id": null, - "google_site_verification": null, - "...": "(24개 키 생략, 총 29개)" + "meta_title_suffix": "", + "meta_description": "", + "meta_keywords": "", + "google_analytics_id": "", + "google_site_verification": "", + "...": "(23개 키 생략, 총 28개)" }, - "...": "(11개 키 생략, 총 16개)" + "...": "(12개 키 생략, 총 17개)" } } ``` @@ -200,6 +202,8 @@ HTTP/1.1 200 | upload.image_max_width | body | integer | 아니오 | min 100, max 10000 | 이미지 리사이즈 최대 너비 (px) | | upload.image_max_height | body | integer | 아니오 | min 100, max 10000 | 이미지 리사이즈 최대 높이 (px) | | upload.image_quality | body | integer | 아니오 | min 1, max 100 | 이미지 리사이즈 시 압축 품질 (1~100) | +| upload.orphan_cleanup_enabled | body | boolean | 아니오 | — | 고아 첨부(소유자 없이 남은 첨부) 자동 정리 활성화 여부. **기본 false** — 사용자 파일을 실제로 파기하므로 운영자가 직접 켜야 동작하며, 예약 작업(`attachments:prune-orphans --scheduled`)이 이 값을 false 폴백으로 재확인한다 | +| upload.orphan_retention_days | body | integer | 아니오 | min 1, max 3650 | 고아 첨부 보존기간(일, 기본 30). 폼 작성 중 이탈을 감안한 유예 기간이다. 하한·상한은 `config('core.settings_limits.upload_orphan_retention_days_*')` 가 정하며 화면 입력도 같은 값을 읽는다 | | seo.meta_title_suffix | body | string | 아니오 | max 100 | 모든 페이지 SEO 제목 뒤에 붙는 접미 문구 | | seo.meta_description | body | string | 아니오 | max 160 | SEO 메타 설명 (검색엔진/소셜 공유 표시 요약) | | seo.meta_keywords | body | string | 아니오 | max 255 | SEO 메타 키워드 (검색엔진 노출 키워드, 쉼표 구분) | @@ -242,11 +246,14 @@ HTTP/1.1 200 | advanced.geoip_license_key | body | string | 아니오 | max 200 | MaxMind GeoLite2 라이선스 키 | | advanced.geoip_auto_update_enabled | body | boolean | 아니오 | — | GeoIP DB 자동 업데이트 사용 여부 (주 1회 자동 재다운로드) | | drivers.storage_driver | body | string | 아니오 | — | 스토리지 드라이버 (local/s3) | +| drivers.public_asset_disk | body | string | 아니오 | max 100 | 공개 자산 직접 URL 서빙 디스크 (none/public/s3 + 플러그인 훅 등록 디스크). 카탈로그(`available_drivers.public_asset`)에 없는 값은 422 | | drivers.s3_bucket | body | string | 아니오 | max 255 | S3 버킷명 | -| drivers.s3_region | body | string | 아니오 | — | S3 리전 | +| drivers.s3_region | body | string | 아니오 | max 64, 소문자 영숫자·하이픈 (`^[a-z0-9-]+$`) | S3 리전 — AWS 리전 코드 또는 S3 호환 스토리지 값 (Cloudflare R2 는 `auto`, MinIO 관례는 `us-east-1`) | | drivers.s3_access_key | body | string | 아니오 | max 255 | S3 액세스 키 | | drivers.s3_secret_key | body | string | 아니오 | max 255 | S3 시크릿 키 | -| drivers.s3_url | body | string | 아니오 | max 500 | S3 엔드포인트 URL | +| drivers.s3_url | body | string | 아니오 | url, max 500 | S3 공개 URL(CDN) base — 파일 URL 생성에만 사용 (API 요청 주소 아님) | +| drivers.s3_endpoint | body | string | 아니오 | url, max 500 | S3 API 엔드포인트 — S3 호환 스토리지(R2/MinIO/NCP 등)용. AWS S3 는 미입력 (예: `https://<account-id>.r2.cloudflarestorage.com`) | +| drivers.s3_use_path_style | body | boolean | 아니오 | — | S3 path-style 주소 사용 여부 — MinIO 등 path-style 전용 스토리지에서 true | | drivers.cache_driver | body | string | 아니오 | — | 캐시 드라이버 (file/redis/memcached) | | drivers.redis_host | body | string | 아니오 | max 255 | Redis 호스트 주소 | | drivers.redis_port | body | integer | 아니오 | min 1, max 65535 | Redis 포트 번호 | @@ -328,6 +335,7 @@ Content-Type: application/json "general.language": "예시값", "general.currency": "예시값", "general.maintenance_mode": true, + "general.asset_url_mode": "https://example.com", "general.site_logo": [ "예시값" ], @@ -370,6 +378,11 @@ Content-Type: application/json "seo.cache_ttl": 1, "seo.sitemap_enabled": true, "seo.sitemap_cache_ttl": 1, + "seo.sitemap_urls_per_file": 1, + "seo.sitemap_gzip": true, + "seo.sitemap_serve_stale_on_miss": true, + "seo.sitemap_max_urls_per_contributor": 1, + "seo.sitemap_hreflang_enabled": true, "seo.sitemap_schedule": "예시값", "seo.sitemap_schedule_time": "예시값", "seo.generator_enabled": true, @@ -379,6 +392,10 @@ Content-Type: application/json "security.auth_token_lifetime": 1, "security.max_login_attempts": 1, "security.login_lockout_time": 1, + "security.password_min_length": 1, + "security.require_password_special_char": true, + "security.two_factor_auth": true, + "security.allow_internal_outbound_urls": true, "advanced.cache_enabled": true, "advanced.layout_cache_enabled": true, "advanced.layout_cache_ttl": 1, @@ -386,6 +403,7 @@ Content-Type: application/json "advanced.stats_cache_ttl": 1, "advanced.seo_cache_enabled": true, "advanced.seo_cache_ttl": 1, + "advanced.seo_sitemap_cache_ttl": 1, "advanced.debug_mode": true, "advanced.sql_query_log": true, "advanced.core_update_github_url": "https://example.com", @@ -393,12 +411,16 @@ Content-Type: application/json "advanced.geoip_enabled": true, "advanced.geoip_license_key": "예시값", "advanced.geoip_auto_update_enabled": true, + "advanced.pagination_result_cap": 1, + "advanced.pagination_max_page": 1, "drivers.storage_driver": "예시값", "drivers.s3_bucket": "예시값", "drivers.s3_region": "예시값", "drivers.s3_access_key": "예시값", "drivers.s3_secret_key": "예시값", "drivers.s3_url": "https://example.com", + "drivers.s3_endpoint": "예시값", + "drivers.s3_use_path_style": true, "drivers.cache_driver": "예시값", "drivers.redis_host": "예시값", "drivers.redis_port": 1, @@ -490,7 +512,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> @@ -529,6 +551,8 @@ _단건 응답: `data` 객체의 필드._ **응답 예시** +<!-- @probed --> + ```http HTTP/1.1 200 ``` @@ -601,7 +625,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | 백업 파일 생성에 실패한 경우 (`settings.backup_failed`) | <!-- @generated:end --> @@ -632,14 +656,14 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -_이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지만). 컨트롤러가 `ResponseHelper::success('settings.backup_success')` 를 데이터 없이 호출하므로 `data` 는 `null` 입니다._ +_이 엔드포인트는 성공 응답을 반환하지 않습니다. 데이터베이스 백업 기능이 아직 제공되지 않아 항상 501 로 응답합니다._ **응답 예시** ```json { - "success": true, - "message": "데이터베이스 백업이 성공적으로 시작되었습니다.", + "success": false, + "message": "데이터베이스 백업 기능은 아직 제공하지 않습니다. 설정 백업은 설정 백업 기능을 이용해 주세요.", "data": null } ``` @@ -648,16 +672,17 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 백업이 수행되지 않은 경우 (`settings.backup_failed`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | -| 500 | Internal Server Error | 백업 처리 중 예외가 발생한 경우 (`settings.backup_error`) | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | +| 501 | Not Implemented | 항상 (데이터베이스 백업 미제공, `settings.database_backup_unavailable`) | <!-- @generated:end --> **설명** -데이터베이스를 백업합니다. SettingsService 에 위임하며, 성공/실패를 메시지로 반환합니다. 설정 백업(`POST /backup`)이 설정 파일만 다루는 것과 달리, 이 엔드포인트는 DB 데이터를 백업 대상으로 합니다. +**데이터베이스 백업은 아직 제공하지 않습니다.** 이 엔드포인트는 라우트만 존재하며 호출하면 항상 501(Not Implemented)로 응답합니다. 코어에는 데이터베이스 덤프 수단이 없어 구현된 적이 없으며, 관리자 화면에도 이 기능을 호출하는 지점이 없습니다. + +설정 파일 백업이 필요하면 `POST /api/admin/settings/backup` 을 사용하십시오 — 그쪽은 정상 동작하며 생성된 백업 경로를 `data.backup_path` 로 돌려줍니다. ### POST /api/admin/settings/clear-cache @@ -699,7 +724,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | --- | --- | --- | | 400 | Bad Request | 캐시 정리가 수행되지 않은 경우 (`settings.cache_clear_failed`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | 캐시 정리 중 예외가 발생한 경우 (`settings.cache_clear_error`) | <!-- @generated:end --> @@ -760,7 +785,7 @@ _단건 응답: `data` 객체의 필드 (GeoIpDatabaseService::updateDatabase() | --- | --- | --- | | 400 | Bad Request | MaxMind 라이선스 키가 설정되지 않은 경우 (`missing_license_key`) | | 401 | Unauthorized | MaxMind 라이선스 키가 유효하지 않은 경우 (`unauthorized`) | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 500 | Internal Server Error | MaxMind 연결 실패(`connection_failed`) 또는 다운로드·압축 해제 실패 | <!-- @generated:end --> @@ -795,6 +820,8 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** +<!-- @probed --> + ```http HTTP/1.1 200 ``` @@ -812,7 +839,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | <!-- @generated:end --> @@ -874,7 +901,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthorized | 본문 `password` 가 요청자 본인의 비밀번호와 일치하지 않는 경우 (`settings.invalid_password`) | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없거나, FormRequest 가 `super_admin` 역할이 아닌 사용자를 거부한 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없거나, FormRequest 가 `super_admin` 역할이 아닌 사용자를 거부한 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | `.env` 기록/config 캐시 재생성 실패 (`settings.app_key_regenerate_failed`) | @@ -933,7 +960,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | --- | --- | --- | | 400 | Bad Request | 복원이 수행되지 않은 경우 (`settings.restore_failed`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 백업 파일을 읽을 수 없는 등 복원 중 예외 발생 (`settings.restore_error`) | @@ -983,7 +1010,7 @@ _단건 응답: `data` 객체의 필드._ | php_memory_limit | string | `512M` | PHP `memory_limit` ini 값 | | max_execution_time | string | `36000초` | PHP `max_execution_time` ini 값 (초 단위 접미사 부착) | | upload_max_filesize | string | `2G` | PHP `upload_max_filesize` ini 값 | -| opcache | object | `{"loaded":true,"enabled":true}` | <!-- TODO: 설명 --> | +| opcache | object | `{"loaded":true,"enabled":true}` | OPcache 상태 (`OpcacheStatus::probe()`). `loaded` 는 확장 적재 여부, `enabled` 는 `opcache.enable` 지시자 값이며 `ini_get` 이 차단·미정의인 환경에서는 **확인 불가를 뜻하는 `null`** 이 된다 (false 와 구분된다) | | install_path | string | `C:\Users\HeuJung\htdocs\g7_2` | 애플리케이션 설치 루트 경로 (`base_path()`) | | config_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 설정 파일 저장 경로 (`storage/app/settings`) | | log_path | string | `C:\Users\HeuJung\htdocs\g7_2\storage\…` | 로그 파일 저장 경로 (`storage/logs`) | @@ -995,6 +1022,8 @@ _단건 응답: `data` 객체의 필드._ **응답 예시** +<!-- @probed --> + ```http HTTP/1.1 200 ``` @@ -1008,22 +1037,22 @@ HTTP/1.1 200 "web_server": "nginx/1.27.3", "php_version": "8.3.26", "mysql_version": "Mysql 8.4.3", - "g7_version": "7.0.6", + "g7_version": "7.0.7", "g7_release_year": "2026", "laravel_version": "12.62.0", - "environment": "local", + "environment": "production", "cpu_info": "Intel(R) Core(TM) Ultra 5 225H", "memory_usage": { "total": "31.49 GB", - "used": "24.31 GB", - "free": "7.18 GB", - "percentage": 77.2 + "used": "28.24 GB", + "free": "3.25 GB", + "percentage": 89.69 }, "disk_usage": { "total": "474.72 GB", - "used": "408.15 GB", - "free": "66.57 GB", - "percentage": 85.98 + "used": "375.43 GB", + "free": "99.28 GB", + "percentage": 79.09 }, "php_memory_limit": "512M", "max_execution_time": "36000초", @@ -1032,10 +1061,10 @@ HTTP/1.1 200 "loaded": true, "enabled": true }, - "install_path": "C:\\Users\\HeuJung\\htdocs\\g7_2", - "config_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\app/settings", - "log_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\logs", - "upload_path": "C:\\Users\\HeuJung\\htdocs\\g7_2\\storage\\app/public", + "install_path": "C:\\Users\\HeuJung\\htdocs\\g7", + "config_path": "C:\\Users\\HeuJung\\htdocs\\g7\\storage\\app/settings", + "log_path": "C:\\Users\\HeuJung\\htdocs\\g7\\storage\\logs", + "upload_path": "C:\\Users\\HeuJung\\htdocs\\g7\\storage\\app/public", "php_extensions": { "required": { "openssl": true, @@ -1066,13 +1095,13 @@ HTTP/1.1 200 "write": { "host": "localhost", "port": 3306, - "database": "g7_2", - "username": "g7_2" + "database": "g7", + "username": "g7" }, "read": [] }, "timezone": "UTC", - "server_time": "2026-08-04 12:53:55" + "server_time": "2026-08-13 05:00:24" } } ``` @@ -1107,10 +1136,12 @@ HTTP/1.1 200 | queue_driver | body | string | 아니오 | — | 큐 드라이버 (sync/database/redis) | | websocket_enabled | body | boolean | 아니오 | — | WebSocket 사용 여부 | | s3_bucket | body | string | 아니오 | max 255 | S3 버킷명 | -| s3_region | body | string | 아니오 | — | S3 리전 | +| s3_region | body | string | 아니오 | max 64, 소문자 영숫자·하이픈 (`^[a-z0-9-]+$`) | S3 리전 — AWS 리전 코드 또는 S3 호환 스토리지 값 (Cloudflare R2 는 `auto`) | | s3_access_key | body | string | 아니오 | max 255 | S3 액세스 키 | | s3_secret_key | body | string | 아니오 | max 255 | S3 시크릿 키 | -| s3_url | body | string | 아니오 | max 500 | S3 엔드포인트 URL | +| s3_url | body | string | 아니오 | url, max 500 | S3 공개 URL(CDN) base — 연결 테스트에는 사용되지 않음 | +| s3_endpoint | body | string | 아니오 | url, max 500 | S3 API 엔드포인트 — 테스트 시 실제 아웃바운드 대상에 반영 (S3 호환 스토리지용) | +| s3_use_path_style | body | boolean | 아니오 | — | S3 path-style 주소 사용 여부 (MinIO 등) | | redis_host | body | string | 아니오 | max 255 | Redis 호스트 주소 | | redis_port | body | integer | 아니오 | min 1, max 65535 | Redis 포트 번호 | | redis_password | body | string | 아니오 | max 255 | Redis 비밀번호 | @@ -1118,9 +1149,18 @@ HTTP/1.1 200 | memcached_host | body | string | 아니오 | max 255 | Memcached 호스트 주소 | | memcached_port | body | integer | 아니오 | min 1, max 65535 | Memcached 포트 번호 | | websocket_app_key | body | string | 아니오 | max 255 | WebSocket 앱 키 | -| websocket_host | body | string | 아니오 | max 255 | WebSocket 호스트 주소 | -| websocket_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 포트 번호 | -| websocket_scheme | body | string | 아니오 | — | WebSocket 스킴 (http/https) | +| websocket_host | body | string | 아니오 | max 255 | WebSocket 클라이언트(브라우저 접속) 호스트 주소 | +| websocket_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 클라이언트 포트 번호 | +| websocket_scheme | body | string | 아니오 | — | WebSocket 클라이언트 스킴 (http/https) | +| websocket_server_host | body | string | 아니오 | max 255 | WebSocket 서버(백엔드 발송용) 호스트 주소 — 미입력 시 클라이언트 값으로 폴백 | +| websocket_server_port | body | integer | 아니오 | min 1, max 65535 | WebSocket 서버 포트 번호 — 미입력 시 클라이언트 값으로 폴백 | +| websocket_server_scheme | body | string | 아니오 | — | WebSocket 서버 스킴 (http/https) — 미입력 시 클라이언트 값으로 폴백 | + +> WebSocket 테스트는 클라이언트/서버 양측 endpoint 를 모두 probe 합니다. 백엔드 broadcast 는 서버 endpoint 를 사용하므로, 서버 endpoint 실패 시 별도 메시지(`settings.websocket_server_test_failed`)로 구분 보고됩니다. + +> S3 테스트는 Flysystem 어댑터(`league/flysystem-aws-s3-v3`) 존재를 선검사하며, `s3_endpoint`/`s3_use_path_style` 이 실제 아웃바운드 대상에 반영됩니다. + +> 사용 불능 드라이버(어댑터 클래스·PHP 확장 부재)는 저장/테스트 모두 422 (`validation.settings.driver_unusable`) 로 거부됩니다. > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.settings.test_driver_connection_validation_rules`). @@ -1144,6 +1184,8 @@ Content-Type: application/json "s3_access_key": "예시값", "s3_secret_key": "예시값", "s3_url": "https://example.com", + "s3_endpoint": "예시값", + "s3_use_path_style": true, "redis_host": "예시값", "redis_port": 1, "redis_password": "Password123!", @@ -1153,7 +1195,10 @@ Content-Type: application/json "websocket_app_key": "예시값", "websocket_host": "예시값", "websocket_port": 1, - "websocket_scheme": "예시값" + "websocket_scheme": "예시값", + "websocket_server_host": "예시값", + "websocket_server_port": 1, + "websocket_server_scheme": "예시값" } ``` @@ -1194,7 +1239,7 @@ _단건 응답: `data` 객체의 필드 (DriverConnectionTester::testAll() 산 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 테스트 실행 중 예외가 발생한 경우 (`settings.driver_test_error`) | @@ -1288,7 +1333,7 @@ _단건 응답: `data` 객체의 필드 (발송 성공 시에만 반환)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 발송 실패 시 — `message` 는 `테스트 메일 발송에 실패했습니다.`, `error` 에 원본 예외 메시지(SMTP 인증 실패·연결 거부 등)가 담김 | @@ -1407,7 +1452,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | --- | --- | --- | | 400 | Bad Request | 저장이 수행되지 않은 경우 (`settings.update_failed`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.settings.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 저장 중 예외가 발생한 경우 (`settings.update_error`) | diff --git a/docs/backend/api/system.md b/docs/backend/api/system.md index a139aaf0..0f1742bb 100644 --- a/docs/backend/api/system.md +++ b/docs/backend/api/system.md @@ -37,19 +37,50 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-200 — 응답 필드는 사람이 작성하세요. --> +이 엔드포인트는 JSON 봉투(`success`/`message`/`data`)를 쓰지 않는다. 정적 자산으로 오인될 응답을 그대로 흉내내는 것이 목적이라, 본문은 자바스크립트이고 `data` 구조가 존재하지 않는다. + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `application/javascript; charset=utf-8` | 정적 `.js` 응답과 동일한 형태 | +| Cache-Control | `no-store, no-cache, must-revalidate, max-age=0` | 프로브는 매 요청 실측이어야 하므로 캐시 금지 | +| Pragma | `no-cache` | 구형 프록시 대응 | +| X-Content-Type-Options | `nosniff` | MIME 스니핑 차단 | +| 본문 매직 토큰 | `G7_ASSET_PROBE_OK` | 성공 판정용. 상태코드가 아니라 **이 토큰의 존재**로 판정한다 | **응답 예시** -<!-- 실측 제외: http-200 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: application/javascript; charset=utf-8 +Cache-Control: no-store, no-cache, must-revalidate, max-age=0 +``` + +```javascript +/* G7 asset URL mode probe */ +window.__g7AssetProbe = 'G7_ASSET_PROBE_OK'; +``` **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +_에러 응답이 정의되어 있지 않다. 이 라우트는 DB 에 접근하지 않으므로 설치 전에도 응답하며, 도달하기만 하면 항상 `200` 이다. 도달하지 못해 `404`/`5xx` 가 관측되면 그것은 에러가 아니라 **판정 입력**이다 (아래 설명 참조)._ <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +서버(nginx/Apache)의 정적 최적화 블록이 확장자 붙은 동적 응답을 가로채는지 판정하기 위한 **대조군** 엔드포인트다. 확장자가 없으므로 정적 블록의 표적이 되지 않는다. 클라이언트는 이 URL 과 `/api/system/asset-probe.js` 를 쌍으로 요청해 다음과 같이 판정한다. + +| `asset-probe.js` | `asset-probe` (본 엔드포인트) | 판정 | +| --- | --- | --- | +| 성공 | 성공 | `extension` — 확장자 붙은 URL 을 그대로 써도 된다 | +| 실패 | 성공 | `extensionless` — 정적 블록 가로채기 확정. 확장자 없는 형태로 전환한다 | +| 실패 | 실패 | 자산 URL 모드 문제가 아니다 (PHP/라우팅 장애) — 별도 안내 | + +주의사항: + +- **판정은 상태코드가 아니라 본문으로 한다.** `res.ok && body.includes('G7_ASSET_PROBE_OK')` 로 확인해야 한다. 상태코드만 보면 "404 대신 200 + 에러 HTML" 이나 catch-all 200 페이지를 반환하는 설정에서 영원히 `extension` 으로 오판하고, 재감지를 몇 번 눌러도 같은 오답이 나온다. +- **감지는 반드시 브라우저에서 수행한다.** 서버측에서 자기 `APP_URL` 로 curl 하면 loopback 이 nginx vhost·SSL·프록시 체인을 우회하거나 다른 vhost 를 타서 오판한다. +- **`public/` 하위에 실물 `asset-probe.js` 를 두지 않는다.** 실제 파일이 있으면 nginx 가 그것을 성공적으로 서빙해 거짓 양성이 된다. ### GET /api/system/asset-probe.js @@ -72,17 +103,49 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +확장자 없는 대조군(`GET /api/system/asset-probe`)과 **같은 컨트롤러 메서드**이므로 응답 형태가 동일하다. JSON 봉투를 쓰지 않으며 `data` 구조가 없다. + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `application/javascript; charset=utf-8` | 정적 `.js` 응답과 동일한 형태 | +| Cache-Control | `no-store, no-cache, must-revalidate, max-age=0` | 프로브는 매 요청 실측이어야 하므로 캐시 금지 | +| Pragma | `no-cache` | 구형 프록시 대응 | +| X-Content-Type-Options | `nosniff` | MIME 스니핑 차단 | +| 본문 매직 토큰 | `G7_ASSET_PROBE_OK` | 성공 판정용. 상태코드가 아니라 **이 토큰의 존재**로 판정한다 | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +애플리케이션까지 도달했을 때 (`extension` 모드 가능): + +```http +HTTP/1.1 200 +Content-Type: application/javascript; charset=utf-8 +Cache-Control: no-store, no-cache, must-revalidate, max-age=0 +``` + +```javascript +/* G7 asset URL mode probe */ +window.__g7AssetProbe = 'G7_ASSET_PROBE_OK'; +``` + +서버의 정적 최적화 블록이 가로챘을 때 (`extensionless` 모드 확정) — 응답은 서버 설정에 좌우되며 매직 토큰이 없다: + +```http +HTTP/1.1 404 +Content-Type: text/html +``` + +> 위 문서의 실측이 `404` 로 관측된 것이 이 경우다. 정규식 location(`location ~* \.(js|css|json)$`)이 프리픽스 location 보다 먼저 매칭되어, 확장자 붙은 동적 응답이 `try_files ... /index.php` 폴백 기회 없이 404 가 된다. **에러 응답** -_대표 에러 없음 (공개 조회). <!-- TODO: 도메인 특이 에러가 있으면 보강 -->_ +_에러 응답이 정의되어 있지 않다. 이 라우트는 DB 에 접근하지 않으므로 애플리케이션에 도달하면 항상 `200` 이다. `404`/`5xx` 는 에러가 아니라 **판정 입력**이며, 대조군이 성공했다면 `extensionless` 모드로 확정한다._ <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +자산 URL 모드 감지의 **표적** 엔드포인트다. 확장자(`.js`)로 끝나므로 서버의 정적 최적화 블록이 가로채는지 여부가 그대로 드러난다. 판정표와 주의사항은 대조군 `GET /api/system/asset-probe` 항목을 참조한다. + +이 프로브가 실패하고 대조군이 성공하면, 코드는 확장자 없는 URL 형태를 써야 한다. 서버측 URL 조립은 `App\Support\AssetUrl`, 프론트측은 `resources/js/core/support/assetUrl.ts` 가 같은 규칙을 공유하므로 한쪽만 바꾸면 그 자산만 404 가 된다. 라우트 등록은 단일 `Route::get()` 이 아니라 `Route::dualSuffix()` / `dualSuffixSegment()` / `dualAsset()` 로 확장자 형태와 확장자 없는 형태를 동시에 등록한다. diff --git a/docs/backend/api/templates.md b/docs/backend/api/templates.md index a2fec5f8..2f541657 100644 --- a/docs/backend/api/templates.md +++ b/docs/backend/api/templates.md @@ -293,7 +293,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | | 409 | Conflict | 필요한 의존 모듈/플러그인이 충족되지 않은 경우 (`errors` 에 `warning`, `missing_modules`, `missing_plugins`, `message`) — `force=true` 로 우회 가능 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 활성화 처리 실패 (이미 활성 상태·미설치·코어 버전 비호환 등) | @@ -360,7 +360,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 업데이트 확인 처리 실패 (GitHub API 호출 실패 등) | @@ -470,7 +470,7 @@ _단건 응답: `data` 객체의 필드 (TemplateResource)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 비활성화 처리 실패 (템플릿 미존재 등) | @@ -590,7 +590,7 @@ _단건 응답: `data` 객체의 필드 (TemplateResource + cascade 결과)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 위반 또는 설치 실패 (이미 설치됨·manifest 오류·cascade 의존 확장 설치 실패 등 — `errors` 에 번역된 사유) | | 500 | Server Error | 설치 처리 중 예기치 못한 오류 | @@ -703,7 +703,7 @@ _단건 응답: `data` 객체의 필드 (TemplateResource — 목록 응답 항 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 422 | Unprocessable Entity | 파일 검증 위반 또는 ZIP 처리 실패 (template.json 누락/무효, 이미 설치된 식별자, 잘못된 디렉토리명 등) | | 500 | Server Error | 설치 처리 중 예기치 못한 오류 | @@ -813,7 +813,7 @@ _단건 응답: `data` 객체의 필드 (TemplateResource — 목록 응답 항 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 422 | Unprocessable Entity | URL 검증 위반 또는 설치 실패 (유효하지 않은 GitHub URL, 저장소 없음, 다운로드 실패, 이미 설치된 식별자 등) | | 500 | Server Error | 설치 처리 중 예기치 못한 오류 | @@ -862,7 +862,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (`data`: `null`, | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 첨부 파일 삭제 실패 (스토리지/DB 삭제 실패 — `첨부 파일 삭제에 실패했습니다.`) | @@ -944,7 +944,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 422 | Unprocessable Entity | 파일 검증 위반 또는 ZIP 열기 실패 (`manifest 미리보기에 실패했습니다.` + `errors.error`) | <!-- @generated:end --> @@ -1053,7 +1053,7 @@ _단건 응답: `data` 객체의 필드 (TemplateResource — 갱신 후 템플 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.activate`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 위반 또는 레이아웃 갱신 실패 (레이아웃 JSON 무효, layout_name 누락 등) | | 500 | Server Error | 레이아웃 갱신 처리 중 예기치 못한 오류 | @@ -1105,7 +1105,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (`data`: `null`, | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.uninstall`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 위반 또는 제거 실패 (활성 상태·파일 삭제 실패 등 — `errors.identifier` 에 번역된 사유) | | 500 | Server Error | 제거 처리 중 예기치 못한 오류 | @@ -1319,7 +1319,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1426,18 +1426,41 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 객체의 필드 (`BroadcastCatalogService::collect` + 요청 식별자)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| channels | array | `[{"name":"core.notifications","source":{"kind":"core"}}]` | 구독 가능한 브로드캐스트 채널 목록. `source.kind` 는 `core`/`module`/`plugin` 이며 확장 채널에는 `source.identifier` 가 붙는다 (활성 확장만 수집) | +| events | array | `[]` | 정적 이벤트 카탈로그. 이벤트는 동적 발행이라 **항상 빈 배열**이며, 편집기는 자유 텍스트 입력으로 폴백한다 | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "요청이 성공했습니다.", + "data": { + "identifier": "sirsoft-admin_basic", + "channels": [ + { "name": "core.notifications", "source": { "kind": "core" } }, + { "name": "module.sirsoft-board.posts", "source": { "kind": "module", "identifier": "sirsoft-board" } } + ], + "events": [] + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1517,18 +1540,31 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 표준 JSON 봉투가 아니라 **편집기 미리보기용 컴포넌트 CSS 본문** 을 그대로 반환한다 — `data` 구조가 없다._ + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `text/css; charset=UTF-8` | 서빙 대상의 MIME 타입 | +| Cache-Control | `public, max-age=31536000, immutable` (프로덕션) / `no-cache` (그 외) | 환경에 따라 갈린다 | +| ETag | `{md5(mtime+size)}` | `If-None-Match` 가 일치하면 본문 없이 `304` | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/css; charset=UTF-8 + +.g7-card{border-radius:.5rem} +``` + +> CSS 가 없는 템플릿도 **빈 본문 200** 으로 응답한다 — 편집기 부팅이 실패하지 않게 하기 위한 폴백이며 404 가 아니다. **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1626,18 +1662,36 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 는 템플릿의 `components.json` 내용을 그대로 담은 **컴포넌트 맵**이다 (고정 필드 집합이 아니라 컴포넌트명 → 정의 매핑)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (컴포넌트명) | object | `{"type":"basic","tag":"div"}` | 컴포넌트 정의. 키는 레이아웃 JSON 의 `name` 과 일치한다 | + +> 활성/`_bundled` 어디에도 `components.json` 이 없으면 `404`(`templates.layout_not_found`)다. **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "Card": { "type": "composite", "props": { "title": "string" } } + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1736,11 +1790,32 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 객체의 필드 (`EditorSpecAssembler::assemble`)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| spec | object \| null | `{"palette":[…],"styleControls":{…}}` | 합본된 편집기 스펙. 분할 매니페스트(`editor-spec/` + `$include`)는 **활성 디렉토리 기준**으로 합쳐지며(`_bundled` 폴백 없음), 미분할 원본 파일은 그대로 반환된다. 스펙이 없으면 `null` (404 아님) | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "편집기 스펙을 조회했습니다.", + "data": { + "identifier": "sirsoft-admin_basic", + "spec": { + "palette": [{ "name": "Card", "label": "카드" }], + "styleControls": {} + } + } +} +``` **에러 응답** @@ -93553,7 +93628,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -93612,7 +93687,7 @@ _단건 응답: `data` 는 템플릿 `lang/{locale}.json` 의 내용을 그대 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -93726,18 +93801,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 객체의 필드._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| permissions | array | `[{"identifier":"core.users.read","name":"사용자 조회"}]` | 레이아웃 조건식에 쓸 수 있는 권한 후보 목록 (현재 로케일로 해석된 표시명 포함) | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "identifier": "sirsoft-admin_basic", + "permissions": [ + { "identifier": "core.users.read", "name": "사용자 조회" } + ] + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -93929,18 +94024,45 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 는 템플릿 `routes.json` 에 모듈·플러그인 라우트를 병합하고 각 라우트에 출처를 태깅한 결과다._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| version | string | `1.0.0` | routes.json 스키마 버전 | +| routes | array | `[{"path":"/","layout":"home","source":{"kind":"template","identifier":"sirsoft-basic"}}]` | 라우트 목록. **`source` 태깅이 필수**다 — 편집기 라우트 트리가 `source.kind` 로 그룹핑하므로 태깅이 없으면 클라이언트가 라우트 트리 렌더에서 실패한다 | + +> 공개 라우트 엔드포인트와 달리 **비활성 템플릿도 조회 가능**하고 `_bundled` 폴백이 적용된다 (편집 대상이 활성일 필요가 없다). **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라우트를 조회했습니다.", + "data": { + "version": "1.0.0", + "routes": [ + { + "path": "/", + "layout": "home", + "auth_required": false, + "source": { "kind": "template", "identifier": "sirsoft-basic" } + } + ] + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -94025,7 +94147,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -94045,8 +94167,8 @@ _단건 응답: `data` 객체의 필드._ | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | identifier | path | string | 예 | — | 대상 리소스의 식별자 | -| extensions | query | string | 아니오 | — | <!-- TODO: 용도 --> | -| page_type | query | string | 아니오 | — | <!-- TODO: 용도 --> | +| extensions | query | string | 아니오 | JSON 배열 문자열 | 후보를 수집할 확장 선언. `[{"type":"module","id":"sirsoft-board"}]` 형태의 JSON 문자열이며, `type`·`id` 가 모두 문자열인 항목만 채택된다(그 외는 조용히 무시). 편집 중인 레이아웃이 아직 활성화하지 않은 확장의 후보까지 보려 할 때 쓴다 | +| page_type | query | string | 아니오 | — | 치환 변수(`vars`) 후보를 좁힐 페이지 유형. 빈 문자열은 미지정과 같게 처리된다 | > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`core.seo_candidate.index_validation_rules`). @@ -94307,18 +94429,42 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 객체의 필드 (`SeoCandidateService::collect` + 요청 식별자)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-admin_basic` | 요청한 템플릿 식별자 (요청값 반향) | +| page_types | array | `[{"id":"board_list","label":"게시판 목록"}]` | 선택 가능한 페이지 유형 후보 (활성 확장 + 요청이 선언한 확장 기준) | +| toggle_settings | array | `[{"key":"use_og","label":"오픈그래프 사용"}]` | SEO 토글 설정 후보 (현재 로케일로 해석) | +| vars | array | `[{"name":"post.title","label":"게시글 제목"}]` | 메타 템플릿에 넣을 수 있는 치환 변수 후보 (`page_type` 을 주면 그 유형으로 좁혀진다) | +| extensions | array | `[{"type":"module","id":"sirsoft-board","name":"게시판"}]` | 후보를 제공한 활성 확장 목록 | **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "요청이 성공했습니다.", + "data": { + "identifier": "sirsoft-admin_basic", + "page_types": [{ "id": "board_list", "label": "게시판 목록" }], + "toggle_settings": [{ "key": "use_og", "label": "오픈그래프 사용" }], + "vars": [{ "name": "post.title", "label": "게시글 제목" }], + "extensions": [{ "type": "module", "id": "sirsoft-board", "name": "게시판" }] + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -94461,7 +94607,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -94531,7 +94677,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -94612,7 +94758,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 존재하지 않는 경우 (`템플릿을 찾을 수 없습니다.`) | | 422 | Unprocessable Entity | 파일 검증 위반 (이미지 아님, 지원하지 않는 형식 jpg/jpeg/png/gif/webp/svg 외, 크기 초과 등) | | 500 | Server Error | 스토리지 저장 실패 (`첨부 파일 업로드에 실패했습니다.`) | @@ -94924,7 +95070,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 존재하지 않는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`ids` 누락 또는 빈 배열 등) | | 500 | Server Error | 삭제 트랜잭션 실패 | @@ -95005,7 +95151,7 @@ _목록 응답: `data` 는 커스텀 다국어 키 배열입니다 (페이지네 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 존재하지 않는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -95090,7 +95236,7 @@ _단건 응답: `data` 객체의 필드 (생성된 커스텀 다국어 키, HTTP | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 존재하지 않는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 키 생성 트랜잭션 실패 | @@ -95141,7 +95287,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (`data`: `null`, | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 없거나, 해당 키가 없거나, 그 키가 경로의 템플릿 소속이 아닌 경우 (교차 템플릿 삭제 차단) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 삭제 트랜잭션 실패 | @@ -95229,7 +95375,7 @@ _단건 응답: `data` 객체의 필드 (수정된 커스텀 다국어 키)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 없거나, 해당 키가 없거나, 그 키가 경로의 템플릿 소속이 아닌 경우 | | 409 | Conflict | 낙관적 잠금 충돌 — 다른 사용자가 먼저 수정 (`errors`: `error=concurrent_modification`, `current_version`, `your_version`, `resource`) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -95336,7 +95482,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 대상 템플릿을 찾을 수 없거나 프리뷰 구성 실패 | @@ -95667,7 +95813,7 @@ _단건 응답: `data` 객체의 필드 (수정된 LayoutExtensionResource)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 템플릿/확장이 없거나, 확장이 경로의 템플릿 소속이 아닌 경우 | | 409 | Conflict | 낙관적 잠금 충돌 — 다른 사용자가 먼저 수정 (`errors`: `error=concurrent_modification`, `current_version`, `your_version`, `resource`) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -95899,7 +96045,7 @@ _단건 응답: `data` 객체의 필드 (복원 결과로 새로 기록된 버 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 템플릿/확장/버전이 없거나, 확장이 경로의 템플릿 소속이 아닌 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 복원 트랜잭션 실패 | @@ -96441,7 +96587,7 @@ _단건 응답: `data` 객체의 필드 (수정된 LayoutResource)._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿이 존재하지 않는 경우 | | 409 | Conflict | 낙관적 잠금 충돌 — 다른 사용자가 먼저 수정 (`errors`: `error=concurrent_modification`, `current_version`, `your_version`, `resource`) | | 422 | Unprocessable Entity | content 구조 검증 위반 (레이아웃 구조/슬롯/데이터소스 병합/엔드포인트 화이트리스트/외부 URL 차단/권한 구조 규칙) | @@ -96678,7 +96824,7 @@ _단건 응답: `data` 객체의 필드 (복원 결과로 새로 기록된 버 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.layouts.edit`)이 없는 경우 | | 404 | Not Found | 대상 템플릿·레이아웃·버전이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 복원 트랜잭션 실패 | @@ -96828,7 +96974,7 @@ _단건 응답: `data` 객체의 필드._ | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.uninstall`)이 없는 경우 | | 404 | Not Found | 해당 식별자의 템플릿이 없는 경우 (`템플릿을 찾을 수 없습니다.`) | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Server Error | 삭제 정보 조회 실패 | @@ -96944,7 +97090,7 @@ _업데이트할 내용이 없거나 템플릿 정보를 다시 읽지 못한 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.templates.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.templates.install`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 위반 또는 업데이트 실패 (미설치, 다운그레이드 차단, 다운로드 실패, 코어 버전 비호환 등 — `errors.template_name` 에 번역된 사유) | | 500 | Server Error | 업데이트 처리 중 예기치 못한 오류 | @@ -96977,11 +97123,24 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. --> +_이 엔드포인트는 표준 JSON 봉투가 아니라 **템플릿 에셋 파일 본문** 을 그대로 반환한다 — `data` 구조가 없다._ + +| 항목 | 값 | 설명 | +| --- | --- | --- | +| Content-Type | `파일 확장자에 따른 MIME (예: text/javascript, text/css, image/png)` | 서빙 대상의 MIME 타입 | +| Cache-Control | `public, max-age=31536000, immutable` (프로덕션) / `no-cache` (그 외) | 환경에 따라 갈린다 | +| ETag | `{md5(mtime+size)}` | `If-None-Match` 가 일치하면 본문 없이 `304` | **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +Content-Type: text/javascript + +(function(){ /* 템플릿 에셋 본문 */ })(); +``` + +> 같은 ETag 로 재요청하면 본문 없이 `304 Not Modified` 가 반환된다. **에러 응답** @@ -97069,7 +97228,13 @@ Accept: application/json -<!-- 실측 응답에 필드 없음(빈 목록 등) — 데이터가 있는 상태로 재실측하거나 사람이 작성. --> +_`data` 는 템플릿의 `components.json` 내용을 그대로 담은 **컴포넌트 맵**이다 (고정 필드 집합이 아니라 컴포넌트명 → 정의 매핑)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (컴포넌트명) | object | `{"type":"basic","tag":"div"}` | 컴포넌트 정의. 키는 레이아웃 JSON 의 `name` 과 일치한다 | + +> 활성/`_bundled` 어디에도 `components.json` 이 없으면 `404`(`templates.layout_not_found`)다. **응답 예시** @@ -97137,7 +97302,19 @@ _이 엔드포인트는 `success`/`message`/`data` 봉투를 사용하지 않습 **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "Card": { "type": "composite", "props": { "title": "string" } } + } +} +``` **에러 응답** @@ -97396,11 +97573,35 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 는 템플릿의 `template.json` 매니페스트 내용이다 (고정 필드 집합이 아니라 매니페스트 그대로)._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| identifier | string | `sirsoft-basic` | 템플릿 식별자 | +| version | string | `1.1.1` | 템플릿 버전 | +| type | string | `user` | 템플릿 유형 (`admin`/`user`) | +| (그 외 매니페스트 키) | mixed | — | `template.json` 이 선언한 나머지 키가 그대로 실린다 | + +> **활성 템플릿만** 조회된다 — 비활성이거나 매니페스트가 없으면 `404`. 응답은 1시간 캐시된다. **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "설정을 조회했습니다.", + "data": { + "identifier": "sirsoft-basic", + "name": "Sirsoft Basic", + "version": "1.1.1", + "type": "user" + } +} +``` **에러 응답** @@ -97413,7 +97614,7 @@ Accept: application/json <!-- @generated:end --> -**설명** 활성 템플릿의 설정 파일(template.json, error_config 등 메타데이터)을 서빙하는 공개 엔드포인트입니다. 응답에 확장 캐시 버전(`cache_version`)을 포함해 프론트엔드가 후속 API 호출에 사용하게 하며, 비활성/미존재 템플릿은 404입니다. 1시간 캐시됩니다. 인증이 필요 없습니다. +**설명** 활성 템플릿의 설정 파일(template.json, error_config 등 메타데이터)을 서빙하는 공개 엔드포인트입니다. 응답에 확장 캐시 버전(`cache_version`)을 포함해 프론트엔드가 후속 API 호출에 사용하게 하며, 비활성/미존재 템플릿은 404입니다. 1시간 캐시됩니다. 서버 캐시는 템플릿 라이프사이클(`template:update` / `template:cache-clear` / 비활성화 / 삭제) 시점에 즉시 무효화되어 최신 매니페스트가 반영됩니다. 인증이 필요 없습니다. ### GET /api/templates/{identifier}/editor-spec @@ -189529,11 +189730,37 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-404 — 응답 필드는 사람이 작성하세요. --> +_`data` 는 템플릿 `routes.json` 에 활성 모듈·플러그인 라우트를 병합한 결과다._ + +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| version | string | `1.0.0` | routes.json 스키마 버전 | +| routes | array | `[{"path":"/","layout":"home","source":{"kind":"template","identifier":"sirsoft-basic"}}]` | 라우트 목록 (각 항목에 출처 `source` 태깅) | + +> 응답은 `?v=` 쿼리를 포함한 키로 캐시된다. 다만 확장 업데이트 중 활성 디렉토리가 비어 라우트가 빠진 **열화 스냅샷은 캐시에 남기지 않는다** — 남기면 업데이트가 끝난 뒤에도 그 확장의 화면이 캐시 만료까지 404 로 남는다. **응답 예시** -<!-- 실측 제외: http-404 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라우트를 조회했습니다.", + "data": { + "version": "1.0.0", + "routes": [ + { + "path": "/", + "layout": "home", + "source": { "kind": "template", "identifier": "sirsoft-basic" } + } + ] + } +} +``` **에러 응답** diff --git a/docs/backend/api/users.md b/docs/backend/api/users.md index eaa18e7e..72e27012 100644 --- a/docs/backend/api/users.md +++ b/docs/backend/api/users.md @@ -378,7 +378,7 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.users.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 500 | Internal Server Error | 사용자 생성 중 예외 발생 (`user.create_failed`, `errors.error` 에 예외 메시지) | @@ -428,6 +428,8 @@ _단건 응답: `data` 객체의 필드 (UserService::bulkUpdateStatus() 반환 | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | | updated_count | integer | `3` | 실제로 상태가 변경된 사용자 수 (요청자 본인 제외 규칙 적용 후 갱신된 행 수) | +| failed_count | integer | `1` | 처리하지 못한 사용자 수 (탈퇴 전환에서 관리자·수퍼관리자 등 차단 대상이 섞인 경우). 탈퇴 외 상태 전환은 항상 `0` | +| failed_reasons | array\<string\> | `["관리자 계정은 탈퇴할 수 없습니다."]` | 처리하지 못한 사유 (중복 제거). 탈퇴 외 상태 전환은 항상 `[]` | **응답 예시** @@ -440,7 +442,9 @@ HTTP/1.1 200 "success": true, "message": ":count명의 사용자 상태가 변경되었습니다.", "data": { - "updated_count": 3 + "updated_count": 3, + "failed_count": 0, + "failed_reasons": [] } } ``` @@ -452,7 +456,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.users.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — 예: 요청자 본인 UUID 포함 시 `ExcludeCurrentUser` 위반) | | 500 | Internal Server Error | 일괄 변경 중 예외 발생 (`user.bulk_update_status_failed`) | @@ -462,6 +466,10 @@ HTTP/1.1 200 여러 사용자의 계정 상태를 한 번에 변경합니다. `ids` 는 대상 사용자 UUID 배열이며, `status` 는 `active`/`inactive`/`blocked`/`withdrawn`/`pending_verification`(UserStatus Enum 값) 중 하나이다. `ExcludeCurrentUser` 규칙으로 요청자 본인은 대상에서 제외된다. 목록 화면의 다중 선택 후 일괄 상태 변경에 사용한다. +`status=withdrawn` 요청은 상태 컬럼만 바꾸지 않고 **건별로 정식 탈퇴 처리**를 수행한다 (익명화 + 연계 데이터 정리 + 탈퇴 훅). 회원이 직접 탈퇴한 경우와 결과가 같다. 관리자 역할을 가진 대상은 건너뛰고 `failed_count` 로 집계되며, 나머지 대상의 처리는 계속된다. + +전건이 차단되어도 응답은 200 이다. 소비자는 `updated_count` 와 `failed_count` 를 함께 읽어 결과를 판정해야 한다 — 200 만 보고 성공으로 안내하면 아무 일도 일어나지 않은 요청이 처리된 것으로 표시된다. 사유는 `failed_reasons` 로 전달되므로 그대로 노출하면 된다. + ### POST /api/admin/users/check-email <!-- @generated:start:api.admin.users.check-email --> @@ -1003,14 +1011,26 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "사용자가 성공적으로 삭제되었습니다.", + "data": null +} +``` + +> 슈퍼 관리자 계정을 대상으로 하면 삭제되지 않고 `422`(`exceptions.cannot_delete_super_admin`)로 거부된다. 삭제는 CASCADE 에 의존하지 않고 연관 데이터를 명시적으로 정리한 뒤 수행되며, 정리 단계에서 실패하면 `422` 와 함께 `error.errors.general[0]` 에 상세 사유가 담긴다. **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.users.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | @@ -1020,6 +1040,8 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 지정한 사용자(경로 파라미터는 UUID 로 바인딩)를 삭제합니다. 슈퍼관리자 계정은 삭제할 수 없으며 시도 시 422(`exceptions.cannot_delete_super_admin`)를 반환한다. 그 외 삭제 실패 시에는 실패 상세 사유가 담긴 422 를, 나머지 오류는 500 을 반환한다. 삭제는 Service 계층에서 관련 데이터 정리와 훅을 거쳐 처리된다. +삭제는 원자적으로 처리된다 — 역할 연결 해제·약관 동의 이력 삭제·토큰 삭제·계정 삭제가 하나의 트랜잭션이며, 마지막 삭제가 외래키 제약 등으로 실패하면 앞 단계도 전부 취소된다(연결만 끊긴 활성 계정이 남지 않는다). 아바타 파일 삭제는 커밋 후에 수행된다. + ### GET /api/admin/users/{user} <!-- @generated:start:api.admin.users.show --> @@ -1091,7 +1113,7 @@ _단건 응답: `data` 객체의 필드._ | withdrawn_at | null | `null` | withdrawn 일시 | | blocked_at | null | `null` | blocked 일시 | | failed_login_attempts | integer | `0` | 연속 로그인 실패 횟수 | -| locked_permanently | boolean | `false` | <!-- TODO: 설명 --> | +| locked_permanently | boolean | `false` | 영구 잠금 여부. true 면 `locked_until` 과 무관하게 잠금이 유지되며, 해제는 성공 로그인 또는 관리자의 잠금 해제로만 이뤄진다 (잠금 시간 설정이 `0`= 무기한일 때 세워진다) | | locked_until | null | `null` | 계정 잠금 해제 시각 (NULL = 잠금 없음) | | is_locked | boolean | `false` | locked 여부 | | notify_post_complete | boolean | `false` | 게시글 작성 완료 알림 수신 여부 (게시판 모듈 알림 설정) | @@ -1426,7 +1448,7 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`core.users.read`)이 없는 경우 | +| 403 | Forbidden | 요구 권한(`core.users.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — 마지막 관리자 본인의 admin 역할 제거 시도 시 `user.last_admin_role_cannot_remove` 포함) | | 500 | Internal Server Error | 수정 중 예외 발생 (`user.update_failed`, `errors.error` 에 예외 메시지) | diff --git a/docs/backend/console-confirm.md b/docs/backend/console-confirm.md index c9525c47..a76f67be 100644 --- a/docs/backend/console-confirm.md +++ b/docs/backend/console-confirm.md @@ -10,7 +10,7 @@ 1. 콘솔 커맨드의 yes/no 프롬프트는 $this->unifiedConfirm() 사용 — Laravel $this->confirm() 직접 호출 금지 2. trait: App\Console\Commands\Traits\HasUnifiedConfirm 3. 입력 규칙: empty=default, yes/y=true, no/n=false, 그 외=재질문 루프 -4. --no-interaction 모드: default 즉시 반환 +4. --no-interaction 또는 콘솔 없는 STDIN: default 즉시 반환 (파괴적 확인의 default 는 false 여야 한다) 5. upgrade step / Symfony 외부 환경: \App\Console\Helpers\ConsoleConfirm::ask() FQN 직접 호출 ``` @@ -88,10 +88,26 @@ protected function unifiedConfirm(string $question, bool $default = false): bool | 환경 | 동작 | |------|------| | `--no-interaction` 옵션 | `$default` 즉시 반환 | +| 콘솔 없는 STDIN (CI·스케줄러·에이전트) | `$default` 즉시 반환 | | 대화형 + 유효 입력 | 정규화 후 즉시 반환 | | 대화형 + 잘못된 입력 | "yes, y, no, n 중 하나로 입력해 주세요." 출력 후 재질문 | | 대화형 + empty 입력 | `$default` 반환 | +#### 비TTY 판정을 Symfony 에 맡기지 않는 이유 + +Symfony 는 `posix_isatty()` 로 비대화 실행을 감지해 `isInteractive()` 를 false 로 내린다. +그런데 **Windows PHP 에는 posix 확장이 없어 그 감지 분기 자체가 실행되지 않는다.** 결과적으로 +`--no-interaction` 없이 콘솔 없는 곳에서 부르면 `isInteractive()` 가 true 로 남고, 프롬프트는 +버퍼에 갇힌 채 `ask()` 가 오지 않을 응답을 무한히 기다린다. 예외도 로그도 없어 겉으로는 +**"그 커맨드가 느리다"** 로만 보인다 — 실제로는 영영 끝나지 않는다. + +그래서 `unifiedConfirm()` 은 `canPromptForAnswer()` 로 한 번 더 판정한다. `stream_isatty()` 는 +posix 확장 없이 Windows 를 포함해 동작하므로 이 판정의 근거로 쓴다. 판정 재료가 없으면 +`true`(질문함)로 떨어진다 — 물어볼 수 있는데 안 묻는 쪽이 더 위험하기 때문이다. + +이 가드가 의미를 가지려면 **파괴적 확인의 `$default` 는 반드시 `false`(중단)** 여야 한다. +콘솔이 없으면 그 기본값이 곧 결정이 되기 때문이다. + --- ## ConsoleConfirm helper (Symfony 비의존) diff --git a/docs/backend/exceptions.md b/docs/backend/exceptions.md index 26298cf4..b0cf9f89 100644 --- a/docs/backend/exceptions.md +++ b/docs/backend/exceptions.md @@ -12,6 +12,8 @@ 3. 파라미터 치환: __('exceptions.key', ['param' => $value]) 4. 코어 예외: exceptions.template_engine.* 형식 5. 모듈 예외: vendor-module::exceptions.* 형식 +6. 응답에는 `getMessage()` 가 아니라 **메시지 키**를 넘긴다 (예외는 키를 들고 다닌다) +7. typed 예외 → 기존 4xx 유지 / generic catch → 5xx (장애를 입력 오류로 위장하지 않는다) ``` --- @@ -21,9 +23,10 @@ 1. [핵심 원칙](#핵심-원칙) 2. [다국어 파일 위치 및 구조](#다국어-파일-위치-및-구조) 3. [파라미터 치환 패턴](#파라미터-치환-패턴) -4. [템플릿 엔진 Custom Exception 예시](#템플릿-엔진-custom-exception-예시) -5. [Custom Exception 개발 체크리스트](#custom-exception-개발-체크리스트) -6. [관련 문서](#관련-문서) +4. [예외 → 응답 매핑](#예외--응답-매핑) +5. [템플릿 엔진 Custom Exception 예시](#템플릿-엔진-custom-exception-예시) +6. [Custom Exception 개발 체크리스트](#custom-exception-개발-체크리스트) +7. [관련 문서](#관련-문서) --- @@ -163,6 +166,92 @@ class MaxDepthExceededException extends Exception --- +## 예외 → 응답 매핑 + +예외를 응답으로 바꾸는 자리에서 두 가지가 자주 어긋난다. 둘 다 오류도 로그도 남기지 않아 코드를 나란히 놓고 보기 전에는 드러나지 않는다. + +시작하기 전에 통로부터 나눈다. `ResponseHelper::error($messageKey, $statusCode, $errors, $messageParams)` 는 인자마다 노출 규칙이 다르다. + +| 인자 | 통로 | 노출 규칙 | +|---|---|---| +| `$messageKey` (1번째) | 사용자에게 보이는 안내 문구 | **다국어 키 전용.** 번역문·예외 원문을 넣으면 키 해석에 실패해 원문이 그대로 나간다 | +| `$errors` (3번째) — Throwable | 진단 | `app.debug` 에서만 `debug` 블록으로 펼쳐진다 | +| `$errors` (3번째) — 문자열 | 진단 | `500` 이상 + 비디버그에서 **차단**, 그 외 노출 | +| `$errors` (3번째) — 배열 | 구조화 페이로드 | **항상 노출** (검증 오류 구조가 이 통로를 쓴다) | +| `$messageParams` (4번째) | 문구 치환 | 응답 본문에 직접 실리지 않는다 | + +그래서 "예외 원문을 응답에 싣느냐" 는 하나의 규칙으로 답할 수 없다. 판단 축은 셋이다. + +- **누구에게** — 관리자 전용 면인가, 공개(비인증) 엔드포인트인가. 공개 면에 SQL 상태코드·경로가 나가면 정보 노출이다. 반대로 관리자에게 실패 사유를 감추면, 서버 로그 접근 권한이 없는 운영자는 "실패했습니다" 외에 아무 근거도 얻지 못한다. +- **무엇의 원문인가** — 우리 인프라 예외인가, 외부 시스템(결제대행사·배송사 API)이 돌려준 도메인 사유인가. 후자는 우리 스택 정보가 아니고 다국어 키로 옮길 수도 없다. 감추면 조치 가능한 정보가 사라진다. +- **어느 통로인가** — 위 표. 노출 폭은 호출부가 문자열로 조립하지 말고 헬퍼에 맡긴다. + +다국어 키는 유한하고 실패 사유는 무한하다. 키만 고집하면 예상 못 한 실패가 전부 "작업에 실패했습니다" 하나로 붕괴하는데, 이는 아래 2번이 막으려는 "장애가 입력 오류로 위장" 과 같은 종류의 정보 손실이다. + +### 1. 응답에는 메시지 키를 넘긴다 + +`ResponseHelper::error()` 의 첫 인자는 **다국어 키**다. 이미 번역된 `$e->getMessage()` 를 그 자리에 넘기면 키 해석에 실패해 원문이 그대로 사용자 화면에 나간다 — 원문에는 SQL 상태코드·경로·클래스명이 섞여 있을 수 있다. + +그래서 도메인 예외는 **번역문이 아니라 키를 들고 다닌다**. 생성자에서 키와 치환 파라미터를 보관하고 `getMessageKey()` / `getMessageParams()` 로 노출한다. + +```php +class BoardTypeOperationException extends Exception +{ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + public function getMessageKey(): string { return $this->messageKey; } + + public function getMessageParams(): array { return $this->replace; } +} +``` + +```php +// ❌ 번역문을 키 자리에 — 해석 실패 → 원문 노출 +return $this->error($e->getMessage(), 422); + +// ✅ 키 + 치환 파라미터 +return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); +``` + +사유 식별자만 들고 다니는 예외(`getReason()`)라면 호출부가 그 식별자로 키를 조립한다 — 이때도 응답에 들어가는 것은 키다. + +### 2. typed 는 기존 4xx, generic 은 5xx + +`catch (\Exception)` / `catch (\Throwable)` 는 **도메인 예외가 아닌 것**을 잡는 자리다. 여기서 4xx 를 돌려주면 인프라 장애와 코드 결함이 "입력 오류" 로 위장되어, 사용자는 고칠 수 없는 안내를 보고 운영자는 장애를 늦게 안다. + +```php +try { + $this->service->doSomething($order, $data); +} catch (OrderModificationException $e) { + // 도메인 규칙 위반 — 사용자가 고칠 수 있다. 기존 상태코드를 그대로 유지한다. + return ResponseHelper::error($e->getMessageKey(), 422, null, $e->getMessageParams()); +} catch (\Exception $e) { + // 그 외 — 서버 결함/인프라 장애. 메시지 키 자리에는 원문을 넣지 않는다. + // (진단이 필요하면 $errors 인자로 Throwable 을 넘겨 헬퍼의 debug 게이트에 맡긴다) + Log::error('...', ['error' => $e->getMessage()]); + + return ResponseHelper::error('sirsoft-ecommerce::exceptions.operation_failed', 500); +} +``` + +규칙 셋: + +- typed 예외를 잡는 분기의 상태코드는 **바꾸지 않는다**. 예외를 도입하는 작업이 사용자에게 보이는 계약을 함께 바꾸면 회귀다. +- 서비스가 typed 예외를 던지는데 컨트롤러에 그 typed catch 가 없으면, 도메인 사유가 generic 으로 흘러 500 이 된다. 서비스를 typed 로 승격할 때는 **호출하는 컨트롤러 메서드를 전수로** 함께 본다. +- 상태코드 인자를 생략한 `ResponseHelper::error('key')` / `moduleError($mod, 'key')` 는 기본값 **400** 이다. generic catch 안에서는 인자를 생략하지 않는다. +- **도메인 예외의 부모를 잡지 않는다.** 도메인 예외는 대개 `\RuntimeException` 을 상속하므로, 서비스를 typed 로 승격한 뒤에도 `catch (\RuntimeException)` 을 남겨 두면 그 자리는 도메인 예외를 잡는 것처럼 보이지만 실제로 남는 것은 인프라 예외뿐이다. 그것까지 4xx 로 나가면 승격 작업이 아무것도 고치지 못한 셈이 된다. 승격한 예외 이름으로 좁힌다. + +의도적으로 4xx 를 유지하는 generic catch 도 있다 — 업로드 파일 해석처럼 실패 원인이 대부분 클라이언트 입력인 경우다. 그런 지점은 사유와 함께 명시적으로 선언하고, 판정에서 조용히 빠지게 두지 않는다. + +`tests/Feature/Http/GenericCatchStatusCodeContractTest.php` 가 코어와 모든 번들 확장의 컨트롤러를 훑어 이 두 규칙을 고정한다. 예외 목록을 상수로 선언해 두었으므로, 새 예외를 만들려면 목록에 사유와 함께 추가해야 한다. + +--- + ## 템플릿 엔진 Custom Exception 예시 ### CircularReferenceException (레이아웃 순환 참조) @@ -303,6 +392,8 @@ class LayoutService - [ ] `/lang/en/exceptions.php`에 영어 메시지 추가 - [ ] Exception 생성자에서 모든 메시지는 `__()` 함수 사용 - [ ] 동적 값은 파라미터 배열로 전달 (예: `['trace' => $stackTrace]`) +- [ ] **메시지 키와 치환 파라미터를 보관하고 `getMessageKey()` / `getMessageParams()` 로 노출** (응답에 번역문 대신 키를 넘기기 위함) +- [ ] **이 예외를 던지는 서비스 메서드를 호출하는 컨트롤러 메서드 전수에 typed catch 가 있는지 확인** (없으면 도메인 사유가 generic 으로 흘러 500 이 된다) - [ ] 두 언어 모두에서 테스트 수행 - [ ] 예외 메시지가 사용자에게 노출될 경우 보안 정보 포함 금지 diff --git a/docs/backend/search-system.md b/docs/backend/search-system.md index dc4da5bf..f3b7611f 100644 --- a/docs/backend/search-system.md +++ b/docs/backend/search-system.md @@ -10,7 +10,8 @@ ```text 1. Laravel Scout + DatabaseFulltextEngine: MySQL FULLTEXT + ngram 기반 검색 (기본 드라이버) 2. FulltextSearchable 인터페이스: searchableColumns() + searchableWeights() 구현 필수 -3. LIKE fallback 자동 적용: FULLTEXT 미지원 DBMS(SQLite, PostgreSQL)에서 자동 전환 +3. LIKE fallback 자동 적용: FULLTEXT 미지원 DBMS(SQLite, PostgreSQL) + 대상 테이블·컬럼 + 조합의 FULLTEXT 인덱스 부재 시 (후자는 설치 결함 신호라 조합당 프로세스 1회 기록) 4. 확장 포인트: core.search.engine_drivers(엔진) + core.search.index_maintainers(인덱스 점검) 필터 훅 5. 인덱스 재생성은 언제나 선택 사항 — 자동 트리거 없음 (테이블 잠금·전체 재색인 비용) 6. AsUnicodeJson 캐스트: JSON 컬럼 FULLTEXT 검색 시 한글 \uXXXX 이스케이프 방지 필수 @@ -176,6 +177,39 @@ DatabaseFulltextEngine::supportsFulltext(); DatabaseFulltextEngine::supportsNgramParser(); ``` +### 인덱스 커버 판정 — MATCH 는 인덱스가 있어야만 조립한다 + +드라이버가 FULLTEXT 를 지원해도, MySQL 의 `MATCH(a, b)` 는 어떤 FULLTEXT 인덱스의 +컬럼 **집합과 정확히 일치**(순서 무관)해야 실행된다. 복합 `(title, content)` 인덱스는 +`MATCH(title)` 단독을 커버하지 못한다. 인덱스가 없는 채 MATCH 를 실행하면 +`1191 Can't find FULLTEXT index` 가 되고, 그 오류가 소비처의 catch 에 삼켜지면 +화면에는 "검색 결과 0건" 으로만 나타난다 (부분 실패 설치에서 실제 발생 — 공개 #103). + +그래서 엔진은 MATCH 조립 전에 `fulltextIndexCoversColumns($table, $columns)` 로 +커버 여부를 판정한다: + +- 판정은 컬럼 한정자 제거·소문자·정렬 정규화 후 **집합 동등 비교**다. 순서가 달라도 + 같은 집합이면 커버한다 (`['content','title']` ≡ `['title','content']`). +- Scout 경로(`performSearch`)는 컬럼마다 단일 `MATCH(col)` 를 조립하므로, **각 컬럼이 + 개별 단일 인덱스로** 커버될 때만 MATCH 를 쓴다. 복합 인덱스만 있는 테이블은 LIKE 로 + 내려간다. +- `applyAny()` 는 컬럼별 `apply(단일)` 재귀라 게이트도 단일 컬럼으로 판정한다 — 한 OR + 그룹 안에 MATCH 와 LIKE 가 혼재할 수 있으며, 유효한 SQL 이고 의도된 동작이다. +- 커버되지 않아 내려가는 폴백은 **설치 결함의 신호**이므로 테이블+컬럼 조합당 프로세스 + 1회 `Log::warning` 을 남긴다 (복구 안내 포함). 드라이버 미지원 폴백은 + 종전대로 무경고다 — 그 DBMS 에서는 부분일치가 정상 경로다. +- 이 경고의 복구 경로는 **해당 확장의 인덱스 생성 마이그레이션 재실행**이다. + `search:index` 를 안내하지 않는다 — 그 점검은 `INFORMATION_SCHEMA` 에 **이미 존재하는** + FULLTEXT 인덱스만 열거하고 `--repair` 는 그중 색인이 낡은 것만 재생성하므로, 인덱스가 + 아예 없는 이 상황은 목록에 나타나지도 재생성 대상이 되지도 않는다. 안내가 그쪽을 + 가리키면 운영자는 "이상 없음" 을 보고 원인 추적이 거기서 끊긴다. + +인덱스 카탈로그는 `INFORMATION_SCHEMA.STATISTICS` 에서 lazy 적재하는 **프로세스(static) +캐시**다. 적재 실패(Throwable)는 캐시하지 않고 그 요청만 LIKE 로 내려간다 — 일시 장애가 +워커 수명 내내 강등으로 굳지 않게 하기 위함이다. `addFulltextIndex()` 성공 직후에는 +카탈로그를 무효화해 같은 프로세스에서 바로 반영된다. 그 외의 인덱스 변경(배포 등)은 +FPM 워커 재시작 시점에 반영된다. + ### SCOUT_DRIVER 전환 `.env`에서 드라이버를 변경하면 즉시 적용됩니다: @@ -313,7 +347,20 @@ public function boot(): void } ``` -등록 후 `.env`에서 `SCOUT_DRIVER=meilisearch`로 전환하면 해당 엔진이 사용됩니다. +등록 후 `.env`에서 `SCOUT_DRIVER=meilisearch`로 전환하거나, 관리자 환경설정 > 드라이버의 +검색엔진 항목에서 선택할 수 있습니다. + +#### 드라이버 폴백 가드 + +검색엔진은 다른 드라이버 카테고리(스토리지·캐시·세션·큐·로그·메일 등)와 같은 폴백 가드를 +받습니다. 저장된 엔진을 제공하던 플러그인이 삭제되면, 부팅 시 그 값이 사용 불가로 판정되어 +기본 엔진(`mysql-fulltext`)으로 되돌아갑니다. 이 가드가 없으면 `scout.driver` 가 죽은 값으로 +남아 공개 검색이 오류로 멈춥니다. + +카탈로그 조회는 **두 훅을 함께 읽습니다** — 위의 `core.search.engine_drivers`(Scout 엔진 맵)와 +일반 드라이버 훅 `core.settings.available_search_drivers`(`{id, label}` 목록). 그래서 검색엔진 +플러그인은 Scout 등록 훅 하나만 구현하면 되고, 관리자 화면 선택지에도 자동으로 나타납니다. +라벨은 `settings.drivers.search.{id}` 다국어 키에서 조회하며, 키가 없으면 ID 를 그대로 씁니다. ### ScoutServiceProvider 동작 흐름 diff --git a/docs/backend/service-repository.md b/docs/backend/service-repository.md index b520e16c..d617e64b 100644 --- a/docs/backend/service-repository.md +++ b/docs/backend/service-repository.md @@ -433,41 +433,57 @@ sirsoft-ecommerce.product.after_update sirsoft-ecommerce.product.filter_create_data ``` -### 서비스 내부 조건부 권한 체크 +### 서비스 내부 권한 상한(ceiling) 체크 -미들웨어가 아닌 **서비스 내부**에서 추가적인 권한 체크가 필요한 경우 `PermissionHelper`를 사용합니다. -대표적인 예: 역할(role) 변경처럼 데이터의 일부 필드만 별도 권한이 필요한 경우. +데이터의 일부 필드가 **권한 상승 벡터**일 때(대표적으로 역할 부여 — 역할을 붙이면 그 역할의 전 +권한을 넘기는 것과 같다), 서비스는 미들웨어 권한과 별개로 **상한 가드**를 적용한다. + +핵심 원칙 세 가지: + +1. **게이트는 그 엔드포인트의 라우트 권한(SSoT)과 같은 리소스여야 한다.** 역할 부여는 "사용자 + 관리"(`core.users.update` — 이 경로는 라우트에서 이미 강제됨)의 일부이며, "역할 정의 수정" + (`core.permissions.update` — 역할에 권한을 가감하는 **타 리소스** 권한)을 요구하지 않는다. 연관/타 + 리소스 권한으로 원 리소스 조작을 게이팅하면, 원 리소스 권한을 가진 액터가 정당한 작업을 못 한다. +2. **권한 상승 방지는 상한 가드(`PermissionEscalationGuard`)가 담당한다.** 액터가 보유하지 않았거나 + 자신의 범위보다 넓은 권한을 담은 역할은 부여할 수 없다. +3. **위반은 명시적으로 거부(403)한다 — 조용히 무시하지 않는다.** 상한 위반 시 + `PermissionEscalationException` 이 전파되어 컨트롤러가 403 으로 매핑한다. ```php -use App\Helpers\PermissionHelper; -use Illuminate\Support\Facades\Auth; +use App\Support\PermissionEscalationGuard; + +public function __construct( + private readonly PermissionEscalationGuard $escalationGuard, + // ... +) {} public function updateUser(User $user, array $data): User { $roleIds = $data['role_ids'] ?? null; unset($data['role_ids'], $data['roles']); - // 훅 실행 (생략)... + // 훅 실행 / 필드 업데이트 (생략)... - $user = $this->userRepository->update($user->id, $data); - - // 역할 변경은 별도 권한으로 보호 if ($roleIds !== null) { $authUser = Auth::user(); - // 자기 자신의 역할은 항상 변경 불가 (보안) - if ($authUser && $authUser->id === $user->id) { - $roleIds = null; + // 상한 검사 대상 = 이번 변경으로 붙거나 떨어지는 역할(추가·제거 대칭 차분). + // 추가만 검사하면 하위 관리자가 상위 역할을 박탈하는 하향 조작이 상한 없이 뚫린다. + // 기존 유지 역할은 변경이 아니므로 제외한다. + $currentRoleIds = $user->roles->pluck('id')->all(); + $changedRoleIds = array_values(array_unique(array_merge( + array_diff($roleIds, $currentRoleIds), // 추가되는 역할 + array_diff($currentRoleIds, $roleIds), // 제거되는 역할 + ))); + if (! empty($changedRoleIds)) { + // 상한 위반 시 PermissionEscalationException throw → 컨트롤러가 403 매핑 + $this->escalationGuard->assertRoleAssignmentWithinActorCeiling($changedRoleIds); } - // core.permissions.update 권한 없으면 역할 변경 무시 - if ($roleIds !== null && ! PermissionHelper::check('core.permissions.update', $authUser)) { - $roleIds = null; - } + // 자기잠금 방지: 마지막 admin 이 자기 admin 역할을 떼는 것만 별도 차단 + // (자기 자신 수정 자체를 막지는 않는다) - if ($roleIds !== null) { - $user->roles()->sync($roleIds); - } + $user->roles()->sync($roleIds); } return $user; @@ -475,11 +491,16 @@ public function updateUser(User $user, array $data): User ``` ``` -필수: 역할/권한 변경은 미들웨어 권한과 별개로 서비스에서 명시적 체크 필수 -필수: 자기 자신의 역할 변경은 항상 불가 (관리자 실수 방지) -패턴: 민감 필드 분리 → 별도 권한 체크 → 권한 없으면 해당 필드 무시 (403이 아닌 무시) +필수: 상승 벡터 필드의 게이트는 그 엔드포인트 라우트 권한(SSoT)과 같은 리소스 prefix 여야 함 + (연관/타 리소스 권한이 원 리소스 조작을 침범 금지) +필수: 권한 상승 방지는 foreign 권한 게이트가 아니라 escalation/rank-ceiling 가드로 처리 +필수: 상한 위반은 명시적 거부(403) — 조용히 무시(silent no-op/soft-block)하지 않음 +금지: `PermissionHelper::check('core.permissions.update')` 로 role_ids 를 drop 하는 silent-drop 패턴 + (원 권한 보유자가 정당한 역할 부여를 못 하고, 200 성공을 반환하면서도 아무 변화가 없어 발견이 늦다) ``` +> 상세: [validation.md](validation.md) "계층 리소스"·"보안 게이트 대칭성" + --- ## 트랜잭션 및 관계 삭제 패턴 @@ -1570,6 +1591,51 @@ public function findOrFail(string $slug, int $id, ?int $postId = null): Comment --- +## 보안 게이트 대칭성 (KVE-2026-1914/1919) + +접근 게이트와 권한 등급 상한은 데이터를 내보내는 **한 경로에만** 있으면 다른 경로가 +조용한 우회로가 된다(예외·오류·로그 없이 원문만 새 나간다). 판정을 한 지점으로 모으고 +(SSoT), 같은 데이터를 서빙하는 모든 소비 경로가 그 지점을 경유하게 한다. + +### 비밀 부모 → 하위 리소스 게이트 재적용 + +비밀/비공개 부모(게시글)에 종속된 하위 리소스(댓글·첨부·문의)는 **각자의 독립 +엔드포인트**를 가진다. 부모 상세 Resource(PostResource) 한 곳에만 마스킹을 두면, 하위 +엔드포인트가 부모의 비밀 상태를 검사하지 않고 해시/ID 만으로 원문을 반환한다. + +| 항목 | 규칙 | +| --- | --- | +| 판정 SSoT | 열람 판정은 단일 게이트(예: `SecretContentGate::canView($post)`)에 모은다 — 작성자 본인 / 비밀번호 검증 / `posts.read-secret` / manager 규칙을 한 곳에서 | +| 재적용 지점 | 하위 리소스를 서빙하는 **모든** 경로 — 목록 컨트롤러, 상세 Resource, 이커머스 연동 훅, 첨부 다운로드/미리보기 서비스 | +| 목록 차단 | 부모가 비밀이고 무권한이면 하위 목록은 **빈 컬렉션**을 반환해 하위 항목이 Resource 에 도달조차 하지 않게 한다(1차 방어) | +| fail-closed | 슬러그·부모를 해석할 수 없으면 안전하게 마스킹(false)으로 실패 — 첨부 서빙은 상세와 분리된 요청이라 `password_verified` 가 없으므로 해시만으로는 비밀 첨부를 못 가져간다 | + +### hash 기반 file-serving 게이트 + +hash/ID 로 파일을 서빙하는 라우트(`preview`/`download`)는 **소유권·비밀·발행 상태**를 +반드시 거친다. `preview` 가 공개 썸네일 정책상 permission 미들웨어 없이(`optional.sanctum`) +열려 있으면, 컨트롤러/서비스의 게이트만이 미인증 공격자(해시만 쥔 게스트)를 막는 유일한 +방어선이다 — `preview` 와 `download` 가 **동일 게이트**를 공유해야 한 쪽이 우회로가 되지 않는다. + +| 응답 | 상황 | +| --- | --- | +| 403 | 인증 사용자가 비밀/삭제 게이트에 걸림(`AccessDeniedHttpException`) | +| 401 | 게스트가 permission 미들웨어(`download`)에 걸림 | +| 정상 서빙 | 발행 + 비밀 아님(또는 열람 권한 보유) | + +### User/Role 등급 상한 대칭 (rank ceiling) + +삭제 경로에만 있던 슈퍼 관리자·보호 역할 보호를 **수정·상태변경·권한부여** 경로까지 +대칭 적용한다. 판정은 `UserGradeGuard::mayModify($target, $actor)` 단일 게이트로 모은다 +(대상이 슈퍼면 액터도 슈퍼여야 수정 가능). 정적 라우트인 일괄(bulk) 엔드포인트는 스코프 +미들웨어가 우회되므로 **서비스 계층에서 강제**하며, 일괄 대상 목록은 `filterModifiable` +로 수정 불가 대상을 걸러낸다(액터 등급 기준 — 액터를 무시하고 대상만 보고 제외하면 +슈퍼 actor 의 정상 수행이 조용히 막힌다). + +> 저장측(FormRequest) 검증 강도 대칭과 레이아웃 표현식 검증 부착은 [validation.md "보안 게이트 대칭성"](validation.md) 참조. + +--- + ## 설정 기반 한계값 설정값으로 정해지는 한계(최대 깊이, 최대 개수 등)의 **검증 책임은 검증 계층 단일**이다. diff --git a/docs/backend/validation.md b/docs/backend/validation.md index 4b31237b..9385ed2d 100644 --- a/docs/backend/validation.md +++ b/docs/backend/validation.md @@ -1212,6 +1212,41 @@ select 처럼 사용자가 URL 을 손으로 만들 일이 없는 면. 판정 --- +## 보안 게이트 대칭성 (KVE-2026-1914/1915/1919) + +같은 리소스를 다루는 두 엔드포인트가 서로 다른 검증 강도를 가지면, **약한 쪽이 우회로**가 +된다. 부모를 변경·서빙하는 모든 경로는 동일 강도의 검증을 거쳐야 한다. + +### 같은 리소스, 같은 검증 강도 + +수정·순서변경·상태변경·권한부여처럼 같은 리소스를 바꾸는 경로가 여럿이면, 그중 하나라도 +검증이 약하면 공격자는 그 경로로 우회한다. 예: 삭제 FormRequest 만 등급 상한을 검사하고 +수정 FormRequest 는 검사하지 않으면, 수정 경로로 슈퍼 관리자를 조작할 수 있다. 판정 규칙은 +한 곳(게이트/Rule)에 두고 모든 경로가 그것을 재사용한다. + +### 레이아웃 표현식 검증은 표현식 트리에 부착한다 + +`SafeLayoutExpressions` 처럼 값의 구조를 재귀 탐색하는 저장측 규칙은, **표현식이 실릴 수 +있는 배열/객체 트리 전체**(레이아웃의 `content`)에 부착해야 한다. 문자열 하위 필드 +(`content.endpoint` 등)에만 부착하면 규칙이 비-배열 값에서 조기 반환(`is_array` 가드)해 +**no-op** 이 된다 — 검증이 걸려 있는 것처럼 보이지만 실제로는 아무것도 검사하지 않는다. + +```php +// ❌ 문자열 endpoint 에만 부착 — is_array 가드로 무력화(no-op) +'content.endpoint' => ['string', new SafeLayoutExpressions], + +// ✅ 표현식 트리를 담는 content 배열에 부착 +'content' => ['required', 'array', new ValidLayoutStructure, new SafeLayoutExpressions], +``` + +부착 위치는 "어느 필드가 표현식 트리를 담는가" 라는 도메인 판정이라 정적으로 강제하기 어렵다 — +레이아웃 저장 FormRequest(Store/Update/UpdateContent/UpdateExtensionContent) 4종의 부착을 +wiring 테스트로 회귀 고정한다. + +> 서비스/리포지토리 계층의 비밀 게이트 재적용·hash 서빙 게이트·등급 상한 대칭은 [service-repository.md "보안 게이트 대칭성"](service-repository.md) 참조. + +--- + ## Custom Rule 개발 체크리스트 - [ ] `/lang/ko/validation.php`에 한국어 메시지 추가 diff --git a/docs/cheatsheet.md b/docs/cheatsheet.md index dfcd3546..754ff3b9 100644 --- a/docs/cheatsheet.md +++ b/docs/cheatsheet.md @@ -181,7 +181,7 @@ php artisan template:install [identifier] [--force] php artisan template:activate [identifier] php artisan template:deactivate [identifier] php artisan template:uninstall [identifier] -php artisan template:cache-clear +php artisan template:cache-clear [identifier?] php artisan template:check-updates [identifier?] php artisan template:update [identifier] [--layout-strategy=overwrite] [--force] [--source=auto|bundled|github] diff --git a/docs/extension/extension-update-system.md b/docs/extension/extension-update-system.md index 1c191d69..b7f04c1c 100644 --- a/docs/extension/extension-update-system.md +++ b/docs/extension/extension-update-system.md @@ -359,12 +359,22 @@ public static function getBundledPath(string $basePath, string $identifier): str | 실패 시점 | 결과 | | --------- | ---- | | 1단계 실패 (복사) | 기존 디렉토리 **온전히 보존**, `_pending/` 내 임시 디렉토리 정리 후 예외 | -| 2단계 실패 (rename) | 기존 디렉토리 **온전히 보존**, `_pending/` 내 임시만 정리 | -| 3단계 실패 (rename) | `_pending/` 내 _old를 원래 위치로 **롤백**, 예외 | +| 2단계 실패 (rename) | **파일 단위 제자리 동기화로 폴백** — 새 버전 파일을 활성 디렉토리에 직접 덮어쓰고, 새 버전에 없는 잔존 파일 제거 | +| 3단계 실패 (rename) | **파일 단위 복사로 폴백** — 스테이징 내용을 활성 경로에 파일별 복사. 복사까지 실패하면 _old 를 원래 위치로 **롤백**, 예외 | > **Windows 참고**: `deleteDirectory` 직후 같은 이름으로 `rename`이 실패하는 타이밍 이슈가 있어, delete→copy 대신 rename→rename→delete 패턴을 사용합니다. > **오토로드 안전성**: 임시 디렉토리가 `_pending/` 하위에 생성되므로, IDE 잠금 등으로 잔존하더라도 `str_starts_with($name, '_')` 필터에 의해 오토로드에서 자동 제외됩니다. +#### Windows 파일 잠금 폴백 (rename 차단 시) + +Windows 에서 디렉토리 rename 은 하위 트리에 열린 핸들(파일 워처의 디렉토리 핸들, 편집기·개발 도구가 열어 둔 파일 등)이 하나라도 있으면 실패한다. 잠금 프로세스의 식별·종료는 신뢰할 수 없고(디렉토리 핸들은 Restart Manager API 로 감지되지 않는다) 사용자의 도구를 예고 없이 종료시키는 부작용이 있으므로, **rename 이 차단되면 프로세스를 종료하는 대신 파일 단위 연산으로 폴백**한다: + +- 파일 생성·덮어쓰기·읽기는 디렉토리 핸들 잠금의 영향을 받지 않는다 (Windows 의 일반적인 파일 열기 모드는 읽기/쓰기 공유를 허용). +- 덮어쓰기가 막힌 개별 파일은 삭제 후 재생성 → 옆으로 치우기(rename) 순으로 단계적 재시도한다. +- 새 버전 파일을 심지 못하면 예외로 전파해 호출자(매니저)가 백업 복원으로 수습하고, 잔존 파일 삭제 실패는 로그만 남기고 진행한다 (업데이트 전체 실패보다 잔존이 낫다). +- 폴백 경로는 원자적이지 않다 — 파일별로 순차 교체되므로 교체 도중의 요청은 신·구 파일이 섞인 상태를 볼 수 있다. 프로덕션(Linux)에서는 rename 이 차단되지 않아 항상 원자적 fast path 를 탄다. +- 잠금으로 삭제하지 못한 `_updating_*`/`_old_*` 임시 디렉토리는 다음 교체 시작 시 자동으로 재정리된다. + 이 메서드는 다음 위치에서 공통으로 사용됩니다: - `ExtensionBackupHelper::restoreFromBackup()` — 백업 복원 diff --git a/docs/extension/hooks.md b/docs/extension/hooks.md index 6a79391b..3274ced7 100644 --- a/docs/extension/hooks.md +++ b/docs/extension/hooks.md @@ -121,6 +121,42 @@ core.attachment.download core.attachment.update core.attachment.delete +# 스토리지 공개 URL 훅 (Filter) — StorageInterface::url() 결과 공급/수정/차단 (공개#100) +# 컨텍스트 6키는 아래 "스토리지·드라이버 확장 훅 페이로드" 표 참조 +core.storage.filter_url + +# 보존 기간 자동 파기 (7.0.7) — 운영자 일괄 삭제 훅과 별개로 발행한다. +# 운영자 삭제 훅에는 본인인증 같은 대화형 가드가 물려 있어 무인 예약이 탈 수 없고, +# 그렇다고 훅 없이 지우면 확장이 가장 큰 삭제 경로(첫 실행의 누적분)를 볼 수 없다. +# 인자는 (보존일) / (보존일, 삭제건수) — 대상을 ID 로 지목하지 않는다. +core.activity_log.before_prune core.activity_log.after_prune +core.notification_log.before_prune core.notification_log.after_prune +core.schedule.before_prune_history core.schedule.after_prune_history + +# 업로드 트라이어드 — 사용자 첨부 업로드 지점의 표준 3훅 패턴 +# before_upload(액션) → filter_upload_file(필터: UploadedFile 을 받아 변형본을 반환. +# 저장 파일명·MIME·크기가 모두 반환 파일 기준이 된다) → after_upload(액션) +# 포맷 변환(예: jpg → webp) 시에는 바이트만 제자리 덮어쓰지 말고, 변환된 임시 파일 경로와 +# 새 원본 파일명(xxx.webp)으로 UploadedFile 을 재구성해 반환할 것 — 일부 소비처는 저장 +# 확장자를 원본 파일명에서 얻으므로, 이름을 갱신하지 않으면 확장자와 내용이 어긋난다. +core.attachment.filter_upload_file # 코어 첨부 (아바타 포함) +core.template_layout_attachment.before_upload # 레이아웃 편집기 첨부 (7.0.7) +core.template_layout_attachment.filter_upload_file # 레이아웃 편집기 첨부 (7.0.7) +core.template_layout_attachment.after_upload # 레이아웃 편집기 첨부 (7.0.7) +sirsoft-board.attachment.filter_upload_file # 게시판 첨부 +sirsoft-page.attachment.filter_upload_file # 페이지 첨부 +sirsoft-ecommerce.product-image.filter_upload_file # 상품 이미지 +sirsoft-ecommerce.category-image.filter_upload_file # 카테고리 이미지 +sirsoft-ecommerce.review-image.filter_upload_file # 리뷰 이미지 (공개 #96) +sirsoft-ckeditor5.image.before_upload # 에디터 이미지 +sirsoft-ckeditor5.image.filter_upload_file # 에디터 이미지 +sirsoft-ckeditor5.image.after_upload # 에디터 이미지 + +# 업로드 잔존물 회수 — 어떤 콘텐츠에서도 쓰이지 않는 에디터 이미지의 참조 판정 대상 선언 (필터) +# 자기 콘텐츠를 가진 확장이 이 훅으로 테이블/컬럼을 등록하지 않으면, 그 확장에서만 쓰이는 +# 이미지가 "미참조" 로 판정돼 자동 정리 대상이 된다. +sirsoft-ckeditor5.image.filter_reference_sources # 에디터 이미지 참조 소스 + # FormRequest Validation Rules 훅 (Filter) core.user.create_validation_rules core.user.update_validation_rules @@ -145,7 +181,9 @@ core.layout_extension.before_apply core.layout_extension.after_apply # 드라이버 확장 훅 (Filter) — 플러그인이 새 드라이버를 등록 +# 항목 3키 구조는 아래 "스토리지·드라이버 확장 훅 페이로드" 표 참조 core.settings.available_storage_drivers +core.settings.available_public_asset_drivers # 공개 자산 직접 URL 서빙 디스크 (공개#100) core.settings.available_cache_drivers core.settings.available_session_drivers core.settings.available_queue_drivers @@ -178,6 +216,70 @@ core.seo.sitemap.after_regenerate_failed # 재생성 실패 시 (['status'=>'fai > `sitemap.index.collect_for_resource` 는 제3자 확장이 리소스 하나에 대한 sitemap 항목을 추가/보정할 때 씁니다(filter — `'type' => 'filter'` 명시 필수). `core.seo.sitemap.after_regenerate_failed` 는 재생성 잡이 실패했을 때 확장이 알림/복구를 걸 수 있는 action 훅입니다. +### 스토리지·드라이버 확장 훅 페이로드 + +#### `core.storage.filter_url` + +`StorageInterface::url()` 의 결과를 공급·수정·차단합니다. 첫 인자는 생성된 URL(`?string`)이며, **디스크 종류와 무관하게 항상 발화**합니다 — 직접 URL 을 만들 수 없어 `null` 인 경우에도 발화하므로 확장이 서명 URL 등을 공급할 수 있습니다. 반환이 문자열이 아니거나 빈 문자열/공백이면 호출측이 스트리밍으로 폴백합니다. + +두 번째 인자는 컨텍스트 배열입니다. + +| 키 | 타입 | 값 | +| --- | --- | --- | +| `scope` | string | `core` / `module` / `plugin` — 호출한 드라이버 종류 | +| `identifier` | ?string | 확장 식별자 (`sirsoft-ecommerce` 등). 코어 드라이버는 `null` | +| `disk` | string | 대상 디스크명 | +| `category` | string | 스토리지 카테고리 (`images`, `settings` 등) | +| `path` | string | 카테고리 하위 상대 경로 | +| `full_path` | string | 디스크 루트 기준 전체 경로 — `Storage::temporaryUrl()` 등에 그대로 사용 가능 | + +#### `core.settings.available_{category}_drivers` + +플러그인이 드라이버/디스크 선택지를 카탈로그에 추가합니다. 첫 인자인 드라이버 배열에 다음 구조의 항목을 append 합니다. + +| 키 | 타입 | 값 | +| --- | --- | --- | +| `id` | string | 드라이버/디스크 식별자 — 저장값이자 config 조회 키 | +| `label` | array | 로케일별 표시 라벨 (예: `['ko' => 'CDN', 'en' => 'CDN']`) | +| `provider` | string | 공급 플러그인 식별자 — 비활성화 시 "사용 중 드라이버" 경고 판정에 사용 (코어 기본 항목에는 없음) | + +`core.settings.available_public_asset_drivers` 로 등록하는 디스크는 플러그인 ServiceProvider 에서 `filesystems.disks.{id}` 정의가 함께 있어야 하며, 그 정의에 `url` 키가 있어야 직접 URL 이 생성됩니다(없으면 스트리밍 폴백). 플러그인이 비활성화되어 디스크 정의가 사라지면 저장값은 보존된 채 스트리밍으로 자동 폴백합니다. + +#### `sirsoft-ckeditor5.image.filter_reference_sources` + +에디터 업로드 이미지가 "어디선가 쓰이고 있는지" 판정할 때 훑을 테이블/컬럼 목록을 확장합니다. 첫 인자인 소스 배열에 다음 구조의 항목을 append 합니다. + +| 키 | 타입 | 값 | +| --- | --- | --- | +| `table` | string | 테이블명 — **프리픽스 제외 원시 이름** (`board_posts`) | +| `columns` | list\<string\> | 본문이 담긴 컬럼명 목록 (`['content']`) | + +자기 콘텐츠에 에디터를 노출하는 확장은 이 훅을 반드시 구독해야 합니다. 등록하지 않으면 그 확장에서만 쓰이는 이미지가 "미참조" 로 판정돼 자동 정리 대상이 됩니다. + +리스너는 테이블명 문자열만 덧붙이고 DB 에 접근하지 않습니다 — 실재 검증(테이블·컬럼 존재)과 조회는 플러그인이 수행하며, 존재하지 않는 선언은 경고만 남기고 건너뜁니다(한 확장의 잘못된 선언이 판정 전체를 멈추지 않습니다). + +로그 사본 테이블(발송 이력·신고 스냅샷 등)은 등록하지 않습니다. 이들은 자체 보존기간으로 삭제되므로 참조 소스로 삼으면 "로그가 지워지는 순간 이미지가 고아가 되는" 역전이 생깁니다. + +```php +public static function getSubscribedHooks(): array +{ + return [ + 'sirsoft-ckeditor5.image.filter_reference_sources' => [ + 'method' => 'addSources', + 'priority' => 10, + 'type' => 'filter', + ], + ]; +} + +public function addSources(array $sources): array +{ + $sources[] = ['table' => 'my_documents', 'columns' => ['body']]; + + return $sources; +} +``` + --- ## 리스너 구현 diff --git a/docs/extension/module-assets.md b/docs/extension/module-assets.md index 3993ec73..5f146de4 100644 --- a/docs/extension/module-assets.md +++ b/docs/extension/module-assets.md @@ -123,6 +123,28 @@ | `dependencies` | `object` | 선택 | 모듈/플러그인 의존성 | | `github_url` | `string\|null` | 선택 | GitHub 저장소 URL (업데이트 감지용) | | `github_changelog_url` | `string\|null` | 선택 | GitHub 변경 이력 URL | +| `trusted_script_hosts` | `string[]` | 선택 | 레이아웃이 로드할 수 있는 외부 스크립트 신뢰 호스트 목록 (아래 참조) | + +#### `trusted_script_hosts` — 외부 스크립트 신뢰 호스트 + +레이아웃 보안 정책은 `scripts[].src`·`data_sources[].endpoint` 를 기본적으로 same-origin +경로(`/` 로 시작)만 허용하고, 외부 origin·protocol-relative(`//host`)·scheme 포함 URL 은 +저장 시점과 렌더 시점 양쪽에서 차단합니다. 확장이 정당하게 외부 CDN 스크립트를 써야 하면 +그 호스트를 이 배열에 선언합니다. 활성 확장이 선언한 호스트만 집계되며(편집자는 추가 불가 — +manifest 는 배포물), 코어가 활성 확장 전체의 선언을 모아 allowlist 를 구성합니다. + +```jsonc +{ + "trusted_script_hosts": ["cdn.ckeditor.com"] +} +``` + +- 값은 호스트명만(스킴/경로 없이). 예: `"cdn.ckeditor.com"`, `"t1.daumcdn.net"`. +- 이 기능은 코어 7.0.7 에서 도입되었습니다. 선언하는 확장은 `g7_version` 을 `>=7.0.7` 로 두는 + 것이 계약상 정확합니다(하위 코어에서는 필드가 무시되어 무해). +- 관련 보안 정책 상세: [frontend/security.md](../frontend/security.md). + +플러그인(`plugin.json`)·템플릿(`template.json`)도 동일 필드를 지원합니다. #### 에셋 필드 @@ -149,7 +171,7 @@ modules/_bundled/sirsoft-ecommerce/ ├── package.json ← npm 패키지 정의 ├── vite.config.ts ← Vite 빌드 설정 ├── tsconfig.json ← TypeScript 설정 -├── dist/ ← 빌드 출력 (git ignore) +├── dist/ ← 빌드 출력 (_bundled 은 Git 추적 — 배포 산출물, `*.map` 만 ignore) │ ├── js/module.iife.js │ ├── css/module.css │ └── assets/ diff --git a/docs/extension/module-basics.md b/docs/extension/module-basics.md index 4c9ab689..d9429367 100644 --- a/docs/extension/module-basics.md +++ b/docs/extension/module-basics.md @@ -491,7 +491,7 @@ modules/_bundled/sirsoft-ecommerce/ ├── vite.config.ts # Vite 빌드 설정 (에셋 모듈만) ├── tsconfig.json # TypeScript 설정 (에셋 모듈만) ├── vendor/ # Composer 의존성 (자동 생성, gitignore 대상) -├── dist/ # 프론트엔드 빌드 출력 (에셋 모듈만, gitignore 대상) +├── dist/ # 프론트엔드 빌드 출력 (에셋 모듈만 — _bundled 은 Git 추적, `*.map` 만 ignore) │ ├── js/module.iife.js │ └── css/module.css ├── upgrades/ # 버전 업그레이드 스텝 (AbstractUpgradeStep 상속 — g7_version >= 7.0.0-beta.5 모듈 의무) @@ -562,7 +562,7 @@ modules/_bundled/sirsoft-ecommerce/ | `package.json` | npm 패키지 정의 (에셋 모듈만 해당) | | `vite.config.ts` | Vite IIFE 빌드 설정 (에셋 모듈만 해당) | | `vendor/` | Composer 의존성 디렉토리 (자동 생성, gitignore 대상) | -| `dist/` | 프론트엔드 빌드 출력 (에셋 모듈만 해당, gitignore 대상) | +| `dist/` | 프론트엔드 빌드 출력 (에셋 모듈만 해당 — `_bundled` 소스는 Git 추적되는 배포 산출물, `*.map` 만 ignore. 소스 변경 시 `--production` 재빌드 산출물을 함께 커밋한다) | | `config/` | 모듈별 설정 파일 | | `database/factories/` | 테스트용 Factory (autoload 등록 필수) | | `database/migrations/` | 데이터베이스 마이그레이션 | diff --git a/docs/extension/module-settings.md b/docs/extension/module-settings.md index 1e965f83..964d4ff3 100644 --- a/docs/extension/module-settings.md +++ b/docs/extension/module-settings.md @@ -15,7 +15,8 @@ 7. [레이아웃 연동](#7-레이아웃-연동) 8. [백엔드에서 설정 조회](#8-백엔드에서-설정-조회) 9. [카탈로그 병합 설정의 공개 응답](#9-카탈로그-병합-설정의-공개-응답) -10. [관련 문서](#10-관련-문서) +10. [항목 단위 보충 병합과 삭제 의도](#10-항목-단위-보충-병합과-삭제-의도) +11. [관련 문서](#11-관련-문서) --- @@ -526,6 +527,61 @@ $timing = $this->settingsService->getStockDeductionTiming($method); > **판단 기준**: `getSetting()` 단일 호출만 필요하면 `module_setting()` 헬퍼 사용, 모듈 전용 메서드가 필요하면 서비스 직접 주입 +### 8.4 저장하면 config 미러를 다시 채운다 + +`g7_module_settings()` 가 읽는 값은 in-memory config 미러(`g7_settings.modules.{id}`)다. 이 미러는 부팅 때 채워지므로, 저장 경로가 아무것도 하지 않으면 **그 프로세스는 저장 후에도 옛 값을 계속 읽는다.** + +php-fpm 은 요청마다 부팅해 문제가 드러나지 않는다. 큐 워커 · `schedule:work` · Reverb 처럼 프로세스가 상주하는 환경에서만 나타나고, 예외도 경고도 로그도 남지 않는다 — 운영자는 저장했는데 배경 작업만 옛 설정으로 도는 상태가 된다. + +모듈 설정 저장에는 코어 공통 지점이 없다(각 모듈의 설정 서비스가 직접 저장한다). 그래서 **자기 캐시를 비우는 모든 자리에서** 미러 재채움을 함께 호출한다. + +```php +public function saveSettings(array $settings): bool +{ + // ... 저장 ... + + $this->settings = null; // 자기 캐시 비우기 + g7_refresh_module_settings_config('vendor-module'); // config 미러 다시 채우기 + + return true; +} + +public function clearCache(): void +{ + $this->settings = null; + g7_refresh_module_settings_config('vendor-module'); +} +``` + +호출 지점은 저장 메서드 하나가 아니다 — 초기화·복원·부분 저장 등 **값을 바꾸는 자리 전부**다. 한 곳만 빠지면 그 경로로 저장했을 때만 미러가 옛 값으로 남는다. + +- 이 헬퍼는 코어 7.0.7 부터 제공된다. 사용하는 모듈은 `module.json` 의 `requires.g7_version` 을 그에 맞춰 올린다. +- 스키마에 `sensitive: true` 로 선언한 필드는 미러에 담기지 않는다. 민감값은 자체 설정 서비스(복호화 경로)로 읽는다. +- 배경과 코어 축(코어/플러그인)의 처리는 [admin-settings-access.md](../backend/admin-settings-access.md) "config 미러 갱신 시점" 참조. + +### 8.5 저장 완료 훅 발화 + +모듈 설정 저장을 알리는 코어 훅은 `core.module_settings.after_save` 이며, payload 는 +`($identifier, [category => fields], $result)` 다. SEO 캐시 무효화·활동 로그 등 코어와 타 확장의 +리스너가 이 훅을 구독한다. + +발화 지점은 **관리자 컨트롤러**다 — 설정 서비스가 아니다. + +- 훅의 의미가 "관리자가 설정을 저장했다" 이다. 서비스에 두면 내부 저장 호출(시드·마이그레이션· + 테스트 픽스처) 전부가 활동 로그와 캐시 무효화를 유발한다. +- 각 모듈의 설정은 그 모듈의 SettingsService 가 직접 파일에 쓰므로, 코어에는 이 훅을 발화할 + 공통 지점이 없다. 모듈이 자기 컨트롤러의 저장 성공 분기에서 직접 발화한다. + +```php +if ($result) { + HookManager::doAction('core.module_settings.after_save', 'vendor-module', $settings, $result); +} +``` + +단건 저장 경로(dot-key)는 payload 를 카테고리 하위 구조로 되돌려 벌크 저장과 같은 형태로 +맞춘다(`Arr::set($payload, $key, $value)`). 구독 리스너가 카테고리 기준으로 관심 키를 찾으므로, +평탄한 dot-key 를 그대로 넘기면 그 리스너들이 아무것도 감지하지 못한다. + --- ## 9. 카탈로그 병합 설정의 공개 응답 @@ -593,7 +649,34 @@ GET /api/modules/{id}/settings/payment # 고아 제외 (정상) --- -## 10. 관련 문서 +## 10. 항목 단위 보충 병합과 삭제 의도 + +`defaults.json` 의 목록 설정(정수키 리스트)은 저장 시 `array_merge` 가 병합이 아니라 통째 교체를 수행한다. 그래서 관리자가 목록에서 일부 항목을 빼고 저장하면 defaults 항목이 저장본에서 사라지고, 그 상태가 그대로 유지된다. 이 통째 교체가 "관리자가 편집한 목록이 그대로 남는다" 는 뜻이라 대부분의 목록에는 옳은 동작이다. + +문제는 여기에 **항목 단위 보충 병합**(저장본에 없는 defaults 항목을 code/key 기준으로 다시 채워 넣는 처리)을 얹을 때다. 보충은 보통 "의도치 않은 소실 복구" 를 목적으로 도입되는데, 소실과 삭제는 저장본에서 똑같이 "그 항목이 없다" 로 보이기 때문에 구분할 근거가 없다. 그 결과 관리자의 의도적 삭제까지 되돌아간다. + +| ❌ 금지 | ✅ 올바른 사용 | +| --- | --- | +| 보충 병합이 저장본에 없는 defaults 항목을 무조건 다시 채움 | 저장 시점에 삭제 의도를 도출해 저장본에 기록하고, 병합이 그 기록을 존중 | +| 삭제 기록을 클라이언트 제출값에서 받음 | 서버가 `defaults 항목 − 제출 항목 − 삭제 불가 항목` 으로 재계산 (제출된 같은 키는 버린다) | +| 그 목록 키가 제출되지 않은 저장에서 기록만 이월 | 목록 값과 기록을 **함께** 이월 — 저장이 파일 통째 교체라 목록 키가 사라지면 defaults 가 다시 들어와 삭제가 전부 부활한다 | +| 항상 살아 있어야 하는 항목(기본 통화 등)까지 기록 대상에 포함 | 그 항목은 기록에서 제외해 보충 경로로 항상 생존시킨다 | +| 삭제 기록을 공개 응답에 그대로 노출 | `frontend_schema` 의 필드 화이트리스트로 차단 (관리자 응답에는 남겨도 무해) | +| 기록 배열을 비연속 키로 저장 | `array_values()` 로 재정렬 — 비연속 키는 JSON 객체로 직렬화되어 화면 반복이 깨진다 | + +이 결함은 예외도 경고도 로그도 남기지 않는다. 저장 요청은 200 을 반환하고, 그 응답 본문에 이미 삭제한 항목이 되살아나 있다. + +보충 병합과 통째 교체를 혼동하지 않도록, 목록 설정을 다룰 때는 다음 세 가지를 구분해 판정한다. + +| 모델 | 조회 시 처리 | 화면 | 삭제 의미 | +| --- | --- | --- | --- | +| 통째 교체 | 저장본을 그대로 사용 | 항목 추가·삭제 | 삭제가 그대로 유지된다 | +| 항목 단위 보충 | 저장본에 없는 defaults 항목을 채움 | 항목 추가·삭제 | **삭제 기록이 없으면 되돌아간다** | +| 카탈로그 병합 | 확장이 등록한 카탈로그와 저장값을 합침 | `is_active` 토글 | 삭제 개념이 없다 (9장 참조) | + +--- + +## 11. 관련 문서 - [모듈 기초](module-basics.md) - 모듈 구조, AbstractModule - [모듈 라우트](module-routing.md) - API 라우트 규칙 @@ -613,3 +696,4 @@ GET /api/modules/{id}/settings/payment # 고아 제외 (정상) - [ ] 레이아웃에 `data_sources` 추가 - [ ] 저장 버튼 액션 연결 - [ ] `frontend_schema`로 민감정보 필터링 설정 +- [ ] 값을 바꾸는 모든 자리에서 `g7_refresh_module_settings_config()` 호출 (8.4) diff --git a/docs/extension/storage-driver.md b/docs/extension/storage-driver.md index e26e09df..ec8a2f9e 100644 --- a/docs/extension/storage-driver.md +++ b/docs/extension/storage-driver.md @@ -25,6 +25,7 @@ - [마이그레이션 가이드](#마이그레이션-가이드) - [API 레퍼런스](#api-레퍼런스) - [트러블슈팅](#트러블슈팅) +- [S3 호환 스토리지 연결](#s3-호환-스토리지-연결) - [FAQ](#faq) --- @@ -866,7 +867,17 @@ public function url(string $category, string $path): ?string - `$path` (string): 카테고리 하위 상대 경로 **반환값**: -- `string|null`: 파일 URL (private disk인 경우 null) +- `string|null`: 파일 URL (직접 URL 불가 디스크이고 훅 공급도 없으면 null) + +**직접 URL 판정**: `public` 디스크이거나 `filesystems.disks.{disk}.url` 이 비어 있지 않은 +문자열로 설정된 디스크(S3+CDN 등)면 직접 URL 을 생성합니다. url 설정이 없거나 +빈 문자열/공백이면 null 입니다 (AWS_URL 미설정 방어). + +**필터 훅**: 생성 결과는 디스크 종류와 무관하게 `core.storage.filter_url` 필터 훅을 +**항상** 통과합니다 (null 도 발화 대상). 확장은 URL 을 공급(서명 URL 등)/수정(도메인 +교체)/차단(빈 문자열 반환 → 호출측 스트리밍 폴백)할 수 있습니다. 컨텍스트 6키: +`scope`(core|module|plugin) / `identifier`(확장 식별자, 코어는 null) / `disk` / +`category` / `path` / `full_path`. **예시**: ```php @@ -874,7 +885,11 @@ public function url(string $category, string $path): ?string $url = $this->storage->url('images', 'product/2024/01/19/uuid.jpg'); // → http://example.com/storage/modules/sirsoft-ecommerce/images/product/2024/01/19/uuid.jpg -// local (private) disk인 경우 +// url 설정 디스크(S3+CDN)인 경우 +$url = $this->storage->url('images', 'product/2024/01/19/uuid.jpg'); +// → https://cdn.example.com/modules/sirsoft-ecommerce/images/product/2024/01/19/uuid.jpg + +// url 미설정 디스크(local 등)인 경우 $url = $this->storage->url('attachments', 'notice/2024/01/19/file.pdf'); // → null (별도 API 엔드포인트 사용해야 함) ``` @@ -1232,19 +1247,42 @@ class BoardModule extends AbstractModule - `$this->storage->url()` 호출 시 항상 null이 반환됩니다. **원인**: -- private disk (local, s3 등)를 사용 중입니다. URL 메서드는 public disk에서만 직접 URL을 반환합니다. +- 사용 중인 디스크에 `filesystems.disks.{disk}.url` 설정이 없습니다. url() 은 + `public` 디스크이거나 url 이 설정된 디스크에서만 직접 URL 을 반환합니다. + s3 디스크라도 url(AWS_URL/관리자 S3 URL 설정)이 비어 있으면 null 입니다. **해결**: ```php -// public disk를 사용하거나 +// public disk 를 사용하거나 $module->getStorageDisk(); // → 'public' +// 디스크에 url 을 설정하거나 (config/filesystems.php 또는 관리자 S3 URL 설정) +'my_cdn' => ['driver' => 's3', ..., 'url' => 'https://cdn.example.com'], + // 또는 별도 API 엔드포인트를 사용 Route::get('/api/attachments/{id}/download', [AttachmentController::class, 'download']); ``` --- +## S3 호환 스토리지 연결 + +코어 7.0.7+ 의 S3 드라이버는 AWS 뿐 아니라 S3 API 를 제공하는 호환 스토리지(Cloudflare R2, MinIO, 네이버 클라우드 등)에도 연결할 수 있다. 관리자 > 환경설정 > 드라이버 > 파일 스토리지의 두 항목이 이를 담당한다. + +| 항목 | 의미 | 예시 | +| --- | --- | --- | +| 엔드포인트 URL | SDK 가 요청을 보내는 API 주소. 비워 두면 AWS 리전 도메인을 사용 | `https://<account-id>.r2.cloudflarestorage.com`, `http://minio.internal:9000` | +| Path-style 주소 사용 | 버킷을 호스트가 아닌 경로에 두는 주소 형식(`endpoint/bucket/...`). MinIO 등 path-style 전용 스토리지에서 켠다 | — | + +- 리전은 자유 입력이다 — Cloudflare R2 는 `auto`, MinIO 는 관례상 `us-east-1` 을 쓴다. +- **S3 URL(공개 URL)** 은 파일 공개 URL 생성에 쓰는 CDN/커스텀 도메인이며 API 요청 주소가 아니다 — API 주소는 엔드포인트 URL 에만 넣는다. +- IP 주소 엔드포인트는 SDK 가 path-style 을 자동 적용하므로 토글과 무관하게 동작한다. 호스트명 엔드포인트에서는 토글이 주소 형식을 결정한다. +- 연결 테스트는 실제 저장 경로와 같은 설정(엔드포인트·path-style 포함)을 사용한다. + +`.env` 로 설정하는 경우 대응 키는 `AWS_ENDPOINT` / `AWS_USE_PATH_STYLE_ENDPOINT` 다 (아래 FAQ Q2 의 ② 경로 참조). + +--- + ## FAQ ### Q1: 기존 파일은 어떻게 되나요? @@ -1270,7 +1308,19 @@ if (Storage::disk('local')->exists($oldPath)) { ### Q2: S3로 전환하려면 어떻게 하나요? -**A**: 다음 3단계만 수행하면 됩니다. +**A**: 코어 7.0.7+ 는 S3 어댑터(`league/flysystem-aws-s3-v3`)를 기본 포함하므로 별도 패키지 설치가 필요 없습니다. 전환 경로는 두 가지입니다. + +**① 관리자 설정 UI (권장)** — 관리자 > 환경설정 > 드라이버 > 파일 스토리지: + +1. 스토리지 드라이버를 `Amazon S3` 로 선택 +2. 버킷 / 리전 / Access Key / Secret Key 입력 +3. S3 호환 스토리지(Cloudflare R2, MinIO, NCP 등)는 위 [S3 호환 스토리지 연결](#s3-호환-스토리지-연결) 절의 두 항목(엔드포인트 URL·Path-style 주소)을 함께 설정 +4. 연결 테스트 성공 후 저장 — 코어 첨부 업로드 디스크가 s3 로 전환됩니다 + (`ATTACHMENT_DISK` env 를 명시한 경우 env 가 항상 우선. 기존 파일은 저장 당시의 disk 로 계속 서빙되므로 혼재 안전) + +참고: **S3 URL(공개 URL)** 칸은 파일 공개 URL 생성에 쓰는 CDN/커스텀 도메인이며, API 요청 주소가 아닙니다. + +**② `.env` (모듈별 커스텀)**: 1. `.env` 파일에 S3 설정 추가: ```env @@ -1278,6 +1328,9 @@ if (Storage::disk('local')->exists($oldPath)) { AWS_SECRET_ACCESS_KEY=your-secret AWS_DEFAULT_REGION=ap-northeast-2 AWS_BUCKET=your-bucket + # S3 호환 스토리지(R2/MinIO 등)만: + AWS_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com + AWS_USE_PATH_STYLE_ENDPOINT=false ``` 2. 모듈 설정 변경: @@ -1300,11 +1353,18 @@ if (Storage::disk('local')->exists($oldPath)) { **끝!** Service 코드는 변경할 필요가 없습니다. +**직접 URL(CDN) 서빙까지 원한다면**: `filesystems.disks.s3.url`(관리자 환경설정의 +S3 URL — CDN 도메인)이 반드시 설정되어야 합니다. url 이 비어 있으면 `url()` 은 +null 을 반환해 스트리밍 경로가 유지됩니다. + --- ### Q3: 여러 disk를 동시에 사용할 수 있나요? -**A**: 네, 카테고리별로 다른 disk를 사용할 수 있습니다. +**A**: 네, `getStorageDiskFor(string $category)` 를 오버라이드하면 카테고리별로 다른 +disk 를 사용할 수 있습니다. `getStorageFor($category)` 가 그 결정에 따라 디스크 단위로 +memoize 된 인스턴스를 돌려주고, ServiceProvider 의 `$storageCategoryServices` 매핑이 +해당 서비스에 카테고리 디스크 스토리지를 자동 주입합니다. ```php <?php @@ -1312,39 +1372,37 @@ if (Storage::disk('local')->exists($oldPath)) { namespace Modules\Sirsoft\Ecommerce; use App\Extension\AbstractModule; -use App\Contracts\Extension\StorageInterface; -use App\Extension\Storage\ModuleStorageDriver; -class EcommerceModule extends AbstractModule +class Module extends AbstractModule { /** - * 카테고리별 Storage 인스턴스 캐시 + * 카테고리별 디스크 결정 — 기본 구현은 getStorageDisk() 와 동일. + * + * 주의: 'settings' 카테고리에서 모듈 설정을 조회하면 설정 로드와 재귀 고리가 + * 생깁니다. 설정 조회는 설정 저장과 무관한 카테고리에서만 수행하세요. */ - private array $storageCache = []; - - /** - * 카테고리별로 다른 Storage를 반환합니다. - */ - public function getStorage(string $category = 'default'): StorageInterface + public function getStorageDiskFor(string $category): string { - if (isset($this->storageCache[$category])) { - return $this->storageCache[$category]; + if ($category !== 'images') { + return $this->getStorageDisk(); } - // 카테고리별 disk 설정 - $disk = match ($category) { - 'images' => config('sirsoft-ecommerce.image_disk', 's3'), - 'attachments' => config('sirsoft-ecommerce.attachment_disk', 'local'), - default => $this->getStorageDisk(), - }; + // 공개 자산 디스크 해석 (확장 개별 설정 > 코어 전역 > 미설정 → 기본 디스크) + $override = module_setting('sirsoft-ecommerce', 'basic_info.public_asset_disk', ''); - $this->storageCache[$category] = new ModuleStorageDriver($this->getIdentifier(), $disk); - - return $this->storageCache[$category]; + return $this->resolvePublicAssetDisk(is_string($override) ? $override : '') + ?? $this->getStorageDisk(); } } ``` +```php +// ServiceProvider — 카테고리 디스크가 필요한 서비스는 $storageCategoryServices 에 매핑 +protected array $storageCategoryServices = [ + ProductImageService::class => 'images', +]; +``` + --- ### Q4: 플러그인에서도 사용할 수 있나요? @@ -1491,6 +1549,80 @@ class AttachmentServiceTest extends TestCase --- +## 공개 자산 전용 디스크 분리 (직접 URL/CDN 서빙) + +완전 공개 자산(상품/카테고리/리뷰/에디터 이미지 등)을 S3+CDN 등 원격 디스크에서 +직접 URL 로 서빙하는 옵트인 기능입니다. 미설정 시 기존 PHP 스트리밍이 100% 보존됩니다. + +### 설정 사슬 + +```text +관리자 환경설정 > 드라이버 탭 > 공개 자산 스토리지 (drivers.public_asset_disk) + → SettingsServiceProvider 가 core.storage.public_asset_disk 로 주입 + → 확장의 getStorageDiskFor('images') 가 resolvePublicAssetDisk() 로 해석 + (우선순위: 확장 개별 설정 public_asset_disk > 코어 전역 > 미설정 → 기본 디스크) + → ServiceProvider $storageCategoryServices 매핑으로 업로드 서비스에 주입 + → put() 이 그 디스크에 저장, 행 disk 컬럼에 기록 + → 모델 download_url accessor 가 행 disk 로 url() 시도 → null 이면 API 경로 폴백 +``` + +- 선택지 카탈로그는 코어 3종(`none`/`public`/`s3`) + 플러그인이 + `core.settings.available_public_asset_drivers` 필터 훅으로 등록한 디스크입니다. + 플러그인은 자기 ServiceProvider 에서 `Config::set('filesystems.disks.{id}', [... 'url' => CDN])` + 로 디스크를 정의하고 훅에 `{id, label, provider}` 를 append 합니다. +- 확장 개별 오버라이드 화면의 카탈로그는 **그 확장의 설정 조회 응답에 부착**해 내립니다 + (`available_public_asset_disks` 키 — ecommerce 는 모듈 설정 컨트롤러가, 플러그인은 설정 + 스키마에 `public_asset_disk` 선언 시 코어 플러그인 설정 API 가 자동 부착). 화면이 코어 + 환경설정 API 를 교차 조회하면 화면 권한과 카탈로그 권한(core.settings.read)이 갈려 + 커스텀 역할에서 선택지만 조용히 비게 됩니다. +- `none` 은 스트리밍 유지(확장 개별 설정에서는 전역이 CDN 이어도 강제 스트리밍)입니다. +- 플러그인 비활성화로 디스크가 config 에서 사라지면(고아 디스크) 자동으로 스트리밍 + 폴백합니다 — 저장값은 보존되므로 재활성화 시 되살아납니다. + +### 혼재 운용 + +디스크 전환 시 기존 파일 이동이 필요 없습니다. 행마다 기록된 disk 를 기준으로 +서빙/삭제/이동이 동작하므로, 전환 이전 로컬 행은 스트리밍으로, 이후 원격 행은 +직접 URL 로 각자 올바르게 서빙됩니다. + +행에 기록된 disk 가 고아가 된 경우(그 디스크를 제공하던 플러그인을 비활성화한 뒤에도 +그 disk 로 기록된 행이 남아 있는 경우)에도 서빙·삭제는 예외 없이 동작합니다. 행 disk +기준 스토리지를 만들 때 디스크 설정 존재 여부를 먼저 확인하고, 없으면 확장의 기본 +디스크로 폴백하기 때문입니다. 파일 자체는 도달 불가이므로 그 이미지는 404 가 되지만, +목록·상세 화면과 삭제(상품/카테고리/리뷰 삭제 포함)는 정상 동작합니다. 이 검증을 +생략하면 미등록 디스크 접근이 예외가 되어 서빙과 삭제가 모두 500 이 됩니다. + +### 버킷/CDN 쪽 요구사항 + +직접 URL 서빙은 브라우저가 그 URL 을 **인증 없이** 읽는다는 전제 위에 있습니다. +따라서 대상 버킷/배포는 다음을 만족해야 합니다. + +| 요구사항 | 미충족 시 증상 | +| --- | --- | +| 객체가 익명 읽기 가능 (버킷 정책 `s3:GetObject` 공개 또는 CDN 공개 배포) | 화면의 그 이미지들이 전부 깨짐 (S3 는 `403 AccessDenied` 를 XML 로 응답) | +| 공개 URL base(`S3 URL`)가 그 객체를 가리킴 | `url()` 이 null → 스트리밍 폴백(기능은 정상, CDN 이점만 없음) | + +관리자 화면의 썸네일은 교차 출처 공개 URL 을 `<img>` 로 직접 사용하므로 CORS 설정은 +필요하지 않습니다. 다만 그 URL 을 자바스크립트로 읽는 커스텀 확장을 만든다면 그때는 +버킷/CDN 에 CORS 규칙이 필요합니다. + +교차 출처 URL 에는 코어 API 클라이언트가 인증 토큰을 붙이지 않습니다 — 공개 자산은 +인증이 필요 없고, 세션 토큰이 제3자 origin 으로 나가서는 안 되기 때문입니다. +같은 이유로 첨부 썸네일·다운로드도 교차 출처 URL 이면 인증 요청 대신 URL 을 직접 씁니다. + +### 서명 URL 등 커스텀 URL 공급 + +`core.storage.filter_url` 필터 훅이 디스크 무관 항상 발화하므로, 확장이 서명 URL 을 +공급하거나 생성된 URL 을 수정/차단할 수 있습니다 (url() 절 참조). + +### 보안 전제 + +권한 검사가 걸린 첨부파일(비밀글/회원 전용 게시판 첨부 등)은 배선 자체가 없습니다 — +직접 URL 은 서버 스트리밍 경로의 권한 검사를 우회하므로, 완전 공개 자산 카테고리에만 +공개 자산 디스크를 적용합니다. + +--- + ## 관련 문서 - [모듈 개발 가이드](module-basics.md) diff --git a/docs/extension/template-caching.md b/docs/extension/template-caching.md index d1fb7579..3349398f 100644 --- a/docs/extension/template-caching.md +++ b/docs/extension/template-caching.md @@ -34,23 +34,24 @@ ## 캐싱 계층 -### 1. 활성 템플릿 캐시 +### 1. 활성 템플릿 상태 캐시 ```php -// 키: templates.active.{type} -// TTL: 3600초 (1시간) -// 값: Template 모델 객체 +// 키: ext.templates.active_identifiers / ext.templates.active_identifiers_{type} / ext.templates.installed_identifiers +// TTL: extension_status_ttl 설정값 (기본 86400초) +// 값: 템플릿 식별자 목록 (CachesTemplateStatus 트레이트가 관리) -Cache::remember("templates.active.admin", 3600, function () { +$activeIdentifiers = $cache->remember('ext.templates.active_identifiers_admin', function () { return Template::where('type', 'admin') ->where('is_active', true) - ->first(); -}); + ->pluck('identifier') + ->all(); +}, $ttl); ``` **무효화 시점**: -- 템플릿 활성화/비활성화 시 -- 템플릿 삭제 시 +- 템플릿 설치/활성화/비활성화/삭제/업데이트 시 (`TemplateManager::invalidateTemplateStatusCache()` — 상태 키 4종 일괄 삭제) +- `template:cache-clear` 실행 시 --- @@ -93,22 +94,22 @@ Cache::remember("template.layout.merged.dashboard.ko", 3600, function () use ($l --- -### 4. 컴포넌트 레지스트리 캐시 +### 4. 컴포넌트 매니페스트 캐시 ```php -// 키: template.components.{identifier} -// TTL: 무제한 (수동 무효화) -// 값: components.json 파싱 결과 +// 키: template.{templateId}.components_manifest (숫자 id / identifier 두 변종) +// TTL: 3600초 (1시간) +// 값: components.json 파싱 결과 (레이아웃 컴포넌트 검증용) -Cache::rememberForever("template.components.sirsoft-admin_basic", function () use ($identifier) { +$manifest = $cache->remember("template.{$templateId}.components_manifest", function () use ($identifier) { $componentsPath = base_path("templates/{$identifier}/components.json"); return json_decode(file_get_contents($componentsPath), true); }); ``` **무효화 시점**: -- 템플릿 재설치 시 -- components.json 수정 시 (개발 중) +- 템플릿 라이프사이클 전 경로 (`TemplateManager::clearTemplateCache()` — update/deactivate/uninstall/`template:cache-clear`) +- components.json 이 바뀌는 템플릿 업데이트 시 (위 경로에 포함) --- @@ -326,11 +327,13 @@ use Illuminate\Support\Facades\Cache; class TemplateCacheService { /** - * 템플릿 활성화 캐시 무효화 + * 템플릿 상태 캐시 무효화 */ public function invalidateActiveTemplate(string $type): void { - Cache::forget("templates.active.{$type}"); + // 상태 키(ext.templates.active_identifiers* / installed_identifiers)는 + // CachesTemplateStatus 가 관리 — 단일 지점에 위임한다 + TemplateManager::invalidateTemplateStatusCache(); } /** @@ -363,11 +366,12 @@ class TemplateCacheService } /** - * 컴포넌트 레지스트리 캐시 무효화 + * 컴포넌트 매니페스트 캐시 무효화 (숫자 id / identifier 두 변종) */ - public function invalidateComponentRegistry(string $identifier): void + public function invalidateComponentRegistry(int $templateId, string $identifier): void { - Cache::forget("template.components.{$identifier}"); + Cache::forget("template.{$templateId}.components_manifest"); + Cache::forget("template.{$identifier}.components_manifest"); } /** @@ -387,8 +391,8 @@ class TemplateCacheService $this->invalidateLayoutChain($layout->layout_name); } - // 컴포넌트 레지스트리 캐시 무효화 - $this->invalidateComponentRegistry($identifier); + // 컴포넌트 매니페스트 캐시 무효화 + $this->invalidateComponentRegistry($template->id, $identifier); } } ``` @@ -460,7 +464,7 @@ class LayoutService `LayoutService`의 저장(`updateLayoutContent`) 및 버전 복원(`restoreVersion`) 메서드에서: 1. **`clearDependentLayoutsCache()`** 호출 — 해당 레이아웃 + 이를 extends하는 자식 레이아웃의 병합 캐시 + `PublicLayoutController` 서빙 캐시(`layout.{identifier}.{name}.v{version}`)를 재귀적으로 무효화 -2. **`extension_cache_version` 증가** (`Cache::put('extension_cache_version', time())`) — CDN/브라우저 HTTP 캐시 busting. `PublicLayoutController`가 `?v={version}` 기반 캐싱을 사용하므로, 버전 변경 시 브라우저가 새 URL로 인식하여 캐시를 우회합니다. +2. **`ext.cache_version` 증가** (`$this->incrementExtensionCacheVersion()` — `ClearsTemplateCaches` 트레이트) — CDN/브라우저 HTTP 캐시 busting. `PublicLayoutController`가 `?v={version}` 기반 캐싱을 사용하므로, 버전 변경 시 브라우저가 새 URL로 인식하여 캐시를 우회합니다. 버전 키 쓰기는 반드시 트레이트를 경유합니다 — 캐시 드라이버에 직접 `put` 하면 스토어/네임스페이스가 어긋날 때 bump 가 읽기 경로에서 보이지 않습니다. ```php // LayoutService::updateLayoutContent() 내부 @@ -469,8 +473,8 @@ $layout = $this->layoutRepository->update($layout->id, $updateData); // 캐시 무효화 (현재 레이아웃 + 자식 레이아웃 + PublicLayoutController 서빙 캐시) $this->clearDependentLayoutsCache($templateId, $name); -// 프론트엔드 브라우저 캐시 무효화 -Cache::put('extension_cache_version', time()); +// 프론트엔드 브라우저 캐시 무효화 (ClearsTemplateCaches 트레이트) +$this->incrementExtensionCacheVersion(); ``` ```php @@ -479,12 +483,12 @@ $newVersion = $this->versionRepository->restoreVersion($layout->id, $versionId); // 동일하게 캐시 무효화 $this->clearDependentLayoutsCache($templateId, $name); -Cache::put('extension_cache_version', time()); +$this->incrementExtensionCacheVersion(); ``` 이 과정이 누락되면 편집/복원 내용이 실제 사이트에 반영되지 않습니다. -> 참고: `clearDependentLayoutsCache()`는 내부적으로 `clearLayoutCache()`를 호출하며, 이 메서드는 병합 캐시 삭제 + `clearPublicServingCache()`(기본 v0 + 현재 extension_cache_version 캐시 삭제) + 모듈 레이아웃인 경우 `LayoutResolverService` 캐시도 무효화합니다. +> 참고: `clearDependentLayoutsCache()`는 내부적으로 `clearLayoutCache()`를 호출하며, 이 메서드는 병합 캐시 삭제(일반 + `.with_source_meta` 편집기 변종) + `clearPublicServingCache()`(현재 `ext.cache_version` 버전 키 삭제) + 모듈 레이아웃인 경우 `LayoutResolverService` 캐시도 무효화합니다. --- @@ -519,12 +523,31 @@ Cache::put('extension_cache_version', time()); ## 캐시 키 요약 -| 캐시 타입 | 키 패턴 | TTL | -|----------|---------|-----| -| 활성 템플릿 | `templates.active.{type}` | 3600초 (1시간) | -| 레이아웃 JSON | `template.layout.{name}.{locale}` | 1800초 (30분) | -| 병합된 레이아웃 | `template.layout.merged.{name}.{locale}` | 3600초 (1시간) | -| 컴포넌트 레지스트리 | `template.components.{identifier}` | 무제한 | +서버 사이드 캐시는 모두 `CacheInterface`(`g7:core:` 네임스페이스) 에 저장됩니다. 무효화 방식은 두 갈래입니다 — **버전 키**(`.v{v}` 접미사)는 `ext.cache_version` bump 로 다음 요청부터 새 키를 쓰고 이전 키는 TTL 로 자연 만료되며, **고정 키**(버전 접미사 없음)는 라이프사이클에서 능동 `forget` 해야 합니다. + +| 캐시 타입 | 키 패턴 | TTL | 무효화 | +|----------|---------|-----|--------| +| 공개 템플릿 설정 (config.json) | `template.config.{identifier}` | 3600초 | 고정 키 — 라이프사이클 능동 삭제 | +| 공개 라우트 (routes.json) | `template.routes.{identifier}.v{v}` | 3600초 | 버전 키 + 현재 버전 키 능동 삭제 | +| 공개 다국어 (lang/{locale}.json) | `template.language.{identifier}.{locale}.v{v}` | 3600초 | 버전 키 + 현재 버전 키 능동 삭제 | +| 공개 레이아웃 서빙 | `layout.{identifier}.{name}.v{v}` (+ 편집기 `.meta` 변종) | layout_ttl (기본 3600초) | 버전 키 + 현재 버전 키 능동 삭제 | +| 병합 레이아웃 (내부) | `template.{templateId}.layout.{name}` (+ `.{sourceHash}` / `.with_source_meta` 변종) | layout_ttl | 고정 키 — 능동 삭제 | +| 컴포넌트 매니페스트 (검증) | `template.{templateId}.components_manifest` (identifier 변종 포함) | 3600초 | 고정 키 — 능동 삭제 | +| 템플릿 상태 | `ext.templates.active_identifiers*` / `ext.templates.installed_identifiers` | 86400초 | 라이프사이클 능동 삭제 | +| 확장 캐시 버전 좌표 | `ext.cache_version` | 없음 | bump 전용 (`ClearsTemplateCaches`) | + +### `template:cache-clear` 가 무효화하는 대상 + +`template:cache-clear {identifier?}` 는 템플릿 라이프사이클(update/deactivate/uninstall)과 동일한 무효화 단일 지점을 경유합니다. + +| 대상 | 단일 모드 | 전체 모드 | +|------|-----------|-----------| +| `template.config.{identifier}` | ✅ | ✅ (설치된 전 템플릿) | +| `template.{id}.components_manifest` (identifier 변종 포함) | ✅ | ✅ | +| 현재 버전 routes/language 키 | ✅ | ✅ | +| 병합 레이아웃 캐시 (변종 포함) | ✅ | ✅ | +| 템플릿 상태 키 + `ext.cache_version` bump | ✅ | ✅ (1회) | +| 확장 프론트엔드 병합 번들 파일 | — | ✅ | --- diff --git a/docs/extension/template-commands.md b/docs/extension/template-commands.md index fe20e496..4b6a681c 100644 --- a/docs/extension/template-commands.md +++ b/docs/extension/template-commands.md @@ -79,7 +79,7 @@ php artisan template:activate [vendor-template] **기능**: - `templates.is_active` 플래그 설정 - 동일 타입의 기존 활성 템플릿 비활성화 -- 캐시 무효화 (`templates.active.{type}`) +- 상태 캐시 무효화 (`ext.templates.active_identifiers*` / `ext.templates.installed_identifiers`) --- diff --git a/docs/frontend/component-props.md b/docs/frontend/component-props.md index bf9069b3..ab9c010d 100644 --- a/docs/frontend/component-props.md +++ b/docs/frontend/component-props.md @@ -367,6 +367,7 @@ export interface DataGridProps { rowKey?: string; // 행 고유 키 필드 (기본: 'id') selectable?: boolean; // 행 선택 기능 selectedRows?: any[]; // 선택된 행 배열 + selectionScope?: 'page' | 'free'; // 선택의 유효 범위 (아래 "선택의 유효 범위" 참조) loading?: boolean; // 로딩 상태 className?: string; // CSS 클래스 @@ -394,9 +395,43 @@ export interface DataGridProps { | `rowKey` | `string` | ❌ | `'id'` | 행 고유 키로 사용할 필드명 | | `selectable` | `boolean` | ❌ | `false` | 체크박스 선택 기능 활성화 | | `selectedRows` | `any[]` | ❌ | `[]` | 선택된 행 배열 (ID 배열) | +| `selectionScope` | `'page' \| 'free'` | ❌ | `'free'` | 선택의 유효 범위 (아래 참조) | | `loading` | `boolean` | ❌ | `false` | 로딩 상태 표시 | | `className` | `string` | ❌ | - | CSS 클래스 | +### 선택의 유효 범위 (selectionScope) + +체크박스 선택은 컴포넌트 밖(전역/로컬 상태)에 저장된다. 그래서 검색·필터·페이지 이동으로 행이 목록에서 빠져도 그 행의 선택은 남는다. 그 상태에서 일괄 처리를 실행하면 사용자가 보고 있지도, 체크하지도 않은 행이 대상이 된다 — 확인 모달은 건수만 말하므로 실행 전에 알아챌 방법이 없고, 처리는 정상 성공하므로 오류도 남지 않는다. + +`onSelectionChange` 를 배선하는 DataGrid 는 둘 중 하나를 반드시 명시한다 (기본값에 기대지 않는다). + +| 값 | 동작 | 쓰는 곳 | +|----|------|--------| +| `"page"` | 지금 그려진 행에 없는 선택을 버린다. 대상은 언제나 "보이고 체크된 행" | 일괄 처리 버튼이 달린 목록 화면 | +| `"free"` | 선택을 컴포넌트가 건드리지 않는다 | 선택 자체가 저장 대상인 폼(적용 대상 고르기 등) — 여러 페이지에 걸쳐 고르는 선택기 포함 | + +```json +{ + "type": "composite", + "name": "DataGrid", + "props": { + "selectable": true, + "selectionScope": "page", + "selectedIds": "{{_global.selectedIds_admin_users || []}}" + }, + "actions": [ + { + "event": "onSelectionChange", + "type": "change", + "handler": "setState", + "params": { "target": "global", "selectedIds_admin_users": "{{$args[0]}}" } + } + ] +} +``` + +값은 리터럴로 고정한다. 표현식으로 분기하면 분기마다 보존 여부가 갈려 한쪽이 조용히 대상 밖 행을 싣는다. `selectable` 이 꺼진 화면이라고 안전한 것도 아니다 — 체크박스가 없으면 남은 선택이 더 안 보이므로, 범위 판정은 `selectable` 과 무관하게 적용된다. + ### SubRow Props (engine-v1.6.0+) 행 아래에 추가 정보를 표시하는 서브 행 기능입니다. diff --git a/docs/frontend/data-sources.md b/docs/frontend/data-sources.md index 684aea93..8e280dca 100644 --- a/docs/frontend/data-sources.md +++ b/docs/frontend/data-sources.md @@ -373,6 +373,12 @@ auth_mode: "optional" 동작 흐름: > `fallback`이 정의되어 있으면 fallback 데이터가 렌더링에 사용되고, 없으면 데이터 없이 처리됩니다. > 이를 통해 비로그인 상태에서 인증 필수 API 호출로 인한 401 → 로그인 리다이렉트 문제를 방지합니다. +### 라우트가 optional.sanctum 이면 optional 선언 의무 + +로그인 여부에 따라 응답 범위가 달라지는 서버 라우트(`optional.sanctum` — 통합검색처럼 회원 열람 범위를 해석하는 API)를 소비하는 데이터 소스는 `auth_mode: "optional"` 을 반드시 선언한다. + +기본값이 `none` 이므로 미선언 시 로그인 회원의 요청에도 토큰이 실리지 않는다. 이 실패는 오류·경고 없이 "로그인 회원이 비회원 범위 응답을 받는" 형태로만 나타난다 — 서버측 권한 해석이 통째로 무력화되는데 화면·콘솔·네트워크 어디에도 실패 신호가 없다. 서버 라우트의 미들웨어와 데이터 소스의 `auth_mode` 는 한 계약의 양끝이므로, 어느 한쪽을 바꾸면 반대편도 함께 확인한다. + ### 하위 호환성 `auth_required: true`는 `auth_mode: "required"`와 동일하게 동작합니다. diff --git a/docs/frontend/security.md b/docs/frontend/security.md index 30ec0a49..34d9d2f1 100644 --- a/docs/frontend/security.md +++ b/docs/frontend/security.md @@ -23,6 +23,7 @@ - [레이아웃 JSON 서버 검증](#레이아웃-json-서버-검증) - [XSS 방지](#xss-방지) - [표현식 평가 보안](#표현식-평가-보안) +- [외부 스크립트 신뢰 출처 허용목록](#외부-스크립트-신뢰-출처-허용목록) - [인증/토큰 프론트엔드 보안](#인증토큰-프론트엔드-보안) - [상태 관리 및 데이터 노출 보안](#상태-관리-및-데이터-노출-보안) - [렌더링 오류 방어](#렌더링-오류-방어) @@ -126,9 +127,38 @@ HTML을 렌더링해야 하는 경우 (게시판 본문, 상품 설명 등) ** ### 엔진 파서 메커니즘 -템플릿 엔진은 `{{expression}}` 내부를 JavaScript `new Function()` 기반으로 평가합니다. +템플릿 엔진은 `{{expression}}` 내부를 **화이트리스트 AST 평가기**(`SafeExpressionEvaluator`)로 해석합니다. `new Function()`·`with(ctx)` 를 사용하지 않으므로, `''.constructor.constructor('code')()` 같은 프로토타입 체인 우회로 임의 코드를 실행할 수 없습니다. -**보안 전제**: 레이아웃 JSON은 서버에서 4단계 Custom Rule 검증을 거쳐 저장되므로, 악의적 표현식이 포함될 가능성은 서버 검증으로 사전 차단됩니다. `new Function()`은 관리자가 작성한 검증된 표현식만 실행합니다. +평가기는 프로퍼티/옵셔널체이닝 접근, 산술·비교·논리·삼항·nullish, 배열/객체/문자열 리터럴, 화살표 함수·템플릿 리터럴·스프레드, 그리고 화이트리스트 전역(`Math`/`JSON`/`Date`/`Array`/`Object`/`Number`/`String` 등)만 허용합니다. `constructor`/`__proto__`/`prototype` 프로퍼티 접근, `Function(`/`eval(`/`import(` 는 파싱·평가 양쪽에서 거부됩니다. + +### 표현식 샌드박스 우회 토큰 + +레이아웃 표현식 문자열에는 다음 토큰을 넣지 않습니다 — 저장 시점 검증과 정적 검사가 함께 차단합니다. + +| 차단 대상 | 이유 | +|----------|------| +| `.constructor` / `['constructor']` | `Function` 도달 경로 (프로토타입 체인 우회) | +| `.__proto__` / `__proto__` | 프로토타입 오염/우회 | +| `.prototype` | 프로토타입 체인 접근 | +| `Function(` / `eval(` | 함수 생성·임의 코드 실행 | +| `import(` | 동적 모듈 로드·원격 코드 실행 | + +화살표 함수(`=>`)와 템플릿 리터럴(백틱)은 정상 표현식에서 널리 쓰이므로 차단하지 않습니다 — 평가기가 인터프리터로 안전하게 해석합니다. + +### 레이아웃 밖에서 저장되는 표현식 + +표현식을 평가하는 것은 레이아웃 JSON 만이 아닙니다. 커스텀 번역 문구, 알림 템플릿, 본인인증 메시지 템플릿처럼 **레이아웃보다 낮은 권한으로 저장되는 콘텐츠**도 최종적으로 같은 엔진 평가 경로(`DataBindingEngine.evaluateExpression`)에 도달합니다. + +이 경로의 방어는 **런타임 평가기 한 겹**입니다. + +| 계층 | 레이아웃 JSON | 레이아웃 밖 편집 콘텐츠 | +|------|--------------|----------------------| +| 저장 시점 위험 토큰 검증 | 적용 | **미적용** (레이아웃 스키마가 아니므로 레이아웃 검증 규칙의 대상이 아님) | +| 런타임 AST 화이트리스트 평가 | 적용 | **적용** | + +저장측 규칙을 이 콘텐츠까지 넓히지 않는 이유는, 그 규칙이 레이아웃 트리 구조(`components`/`computed`/`scripts`/`data_sources`)를 전제로 순회하기 때문입니다. 자유 텍스트에 붙이면 정상 문구의 오탐과 검증 누수가 동시에 생깁니다. 방어의 본질은 화이트리스트 평가기이고, 저장측 토큰 검증은 레이아웃에 한정된 보조 방어입니다. + +새로 표현식을 평가하는 저장 경로를 추가할 때는 그 값이 반드시 `SafeExpressionEvaluator` 를 거치게 하고, 자체 평가기(`new Function`·`eval`)를 두지 않습니다. ### 안전한 데이터 접근 (필수) @@ -169,6 +199,52 @@ HTML을 렌더링해야 하는 경우 (게시판 본문, 상품 설명 등) ** --- +## 외부 스크립트 신뢰 출처 허용목록 + +레이아웃의 `scripts[].src` 와 `data_sources[].endpoint` 는 기본적으로 **same-origin 절대 경로**(`/` 로 시작)만 허용합니다. `//`(protocol-relative)·scheme 포함 외부 URL 은 원격 코드 로드 경로이므로 런타임 스크립트 로더가 차단합니다. + +일부 확장은 외부 CDN 스크립트를 정당하게 사용합니다(예: CKEditor5 → `cdn.ckeditor.com`, Daum 우편번호 → `t1.daumcdn.net`). 이런 확장은 자신의 manifest 에 신뢰 호스트를 **선언**하고, 코어가 활성 확장 전수에서 이 목록을 집계해 `window.G7Config.trustedScriptHosts` 로 노출합니다. 런타임 로더·저장측 검증·정적 검사는 모두 이 목록에 속한 호스트만 예외로 허용합니다. + +```json +// 확장 manifest (module.json / plugin.json / template.json) +{ + "trusted_script_hosts": ["cdn.ckeditor.com"] +} +``` + +| 입력 자리 | 허용 판정 | +|----------|----------| +| 편집기로 저장하는 레이아웃 | same-origin 경로 + 신뢰 호스트만 (임의 외부 origin 차단) | +| 확장이 커밋한 레이아웃 파일 | 확장이 선언한 신뢰 호스트 허용 | +| 미선언 외부 origin | 항상 차단 (예외도 경고 토스트도 없이 skip) | + +신뢰 경계: 신뢰 호스트로 허용되는 것은 **확장이 코드로 선언한 호스트**뿐이며, 편집기 저장분에 임의의 원격 스크립트를 넣을 수는 없습니다. 새 CDN 을 쓰려면 그 확장 manifest 의 `trusted_script_hosts` 에 호스트를 추가해야 합니다. + +### same-origin 판정은 브라우저 URL 파서와 같아야 한다 + +`//` 로 시작하는지, scheme 이 있는지, `/` 로 시작하는지만 문자열로 확인하는 판정은 **authority 우회를 막지 못합니다.** 브라우저(WHATWG URL)는 파싱 전에 ASCII tab·개행을 제거하고, http/https 에서 백슬래시를 슬래시와 동등하게 처리하기 때문입니다. + +| 입력 | 문자열 접두 검사 | 브라우저 해석 | +|------|----------------|--------------| +| `/api/widget.js` | same-origin | `https://내도메인/api/widget.js` (same-origin) | +| `//evil.com/x.js` | 차단 | `https://evil.com/x.js` | +| `/\/evil.com/x.js` | **same-origin 으로 오판** | `https://evil.com/x.js` | +| `/\evil.com/x.js` | **same-origin 으로 오판** | `https://evil.com/x.js` | +| `/{tab}/evil.com/x.js` | **same-origin 으로 오판** | `https://evil.com/x.js` | +| `/\/cdn.신뢰.com/x.js` | **차단으로 오판** | `https://cdn.신뢰.com/x.js` (신뢰 출처 — 차단하면 과차단) | +| `///evil.com/x.js` | 차단(호스트 추출 실패) | `https://evil.com/x.js` | +| `/js/a\b.js` | same-origin | `https://내도메인/js/a/b.js` (same-origin — 차단하면 과차단) | + +판정 전에 **tab·LF·CR 를 제거하고, 백슬래시를 슬래시로 바꾸고, 선행 슬래시 런을 접은** 뒤 접두 검사를 적용합니다. 브라우저는 선행 슬래시가 몇 개든 authority 시작으로 접습니다(`///host` ≡ `//host`, `https:///host` ≡ `https://host`). 경로 중간의 백슬래시·탭·연속 슬래시는 authority 를 만들지 않으므로 그대로 통과합니다. + +이 정규화는 런타임 로더·저장측 검증·정적 검사 **세 계층이 공유**해야 합니다. 세 계층이 같은 판정 로직을 쓰므로, 한쪽만 고치면 나머지가 우회로로 남고 반대로 한 형태로 셋이 함께 뚫립니다. 새 URL 검증 지점을 추가할 때 접두 검사를 직접 작성하지 말고 기존 정규화를 경유하세요. + +**same-origin 판정과 신뢰 출처 판정도 같은 정규화를 씁니다.** 두 판정은 한 조건문에서 이어집니다("내 사이트 경로인가, 아니면 신뢰 출처인가"). 한쪽만 정규화하면 신뢰 출처 이름을 userinfo 자리에 끼워 넣은 주소(`https://evil.com\@cdn.신뢰.com/x.js`)가 저장 단계에서만 신뢰 출처로 보여 통과하고, 반대로 브라우저가 신뢰 출처로 읽는 형태를 저장 단계만 거부하는 과차단도 생깁니다. 호스트 추출은 반드시 정규화를 경유하세요. + +> manifest 필드 스펙(값 형식·`g7_version` 제약·모듈/플러그인/템플릿 공통)은 [extension/module-assets.md](../extension/module-assets.md#trusted_script_hosts--외부-스크립트-신뢰-호스트) 참조. + +--- + ## 인증/토큰 프론트엔드 보안 ### 인증 원칙 diff --git a/docs/requirements.md b/docs/requirements.md index 9cfd6a97..68bf1032 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -7,9 +7,9 @@ ```text 1. PHP 8.2+ 필수 2. MySQL 8.0+ 또는 MariaDB 10.3+ (utf8mb4, utf8mb4_unicode_ci) -3. PHP 필수 모듈 30개 (ctype, curl, gd, intl, redis, imagick 등) -4. 하드웨어: 최소 2 vCPU·2GB / 권장 4 vCPU·8GB, 디스크 최소 700MB / 권장 2GB+ -5. 프로덕션: HTTPS 필수, Redis 권장, 큐 워커/스케줄러/Reverb 데몬 필요 +3. PHP 확장: 필수 16개 + 기능별 선택 17개 (설치 단계 자동 검사는 그중 14개) +4. 하드웨어: 최소 2 vCPU·2GB / 권장 4 vCPU·8GB, 디스크: 설치 검사 기준 500MB, 권장 2GB+ +5. 프로덕션: HTTPS 필수(설치는 차단하지 않고 경고), Redis 권장, 큐 워커/스케줄러/Reverb 데몬 필요 ``` --- @@ -21,6 +21,7 @@ | OS | 지원 수준 | 비고 | |----|----------|------| | Linux (Ubuntu 22.04+, CentOS/RHEL 8+) | 프로덕션 권장 | 가장 안정적 | +| Windows | 개발 환경 지원 | XAMPP/Laragon 등 (§1.3.1, [INSTALL.md](../INSTALL.md) 참조) | ### 1.2 웹서버 @@ -34,10 +35,13 @@ ### 1.3 PHP -| 항목 | 요구사항 | -|------|-------------------------------------------------------------| -| 버전 | **8.2 이상** (`^8.2`) | -| SAPI | FPM (권장) 또는 mod_php (Apache + mod_fcgid 환경은 §1.3.1) | +| 항목 | 요구사항 | +|------|---------| +| 버전 | **8.2 이상** (`^8.2`) | +| SAPI | FPM (권장) 또는 mod_php (Apache + mod_fcgid 환경은 §1.3.1) | +| `disable_functions` | `exec`·`proc_open`·`shell_exec` 허용 필요 (설치·코어 업데이트가 자식 프로세스를 실행) | +| 웹 ↔ CLI 버전 | 메이저·마이너 버전 일치 권장 (설치 프로그램이 확인 후 불일치 시 안내) | +| OPcache | 활성화 권장 (성능 — 미활성화 시 설치는 진행되고 안내만 표시) | #### 1.3.1 Apache + mod_fcgid 환경 추가 설정 (권장) @@ -53,43 +57,52 @@ FcgidOutputBufferSize 0 - 인스톨러 SSE 호환성 사전 체크가 buffered 환경으로 자동 판정되어 폴링 모드로 fallback - 폴링 모드 응답은 코드 레벨 64KB padding 워크어라운드로 동작은 보장되나, SSE 모드가 아닌 1초 간격 폴링으로 진행 상황이 표시됨 -### 1.4 PHP 필수 확장 모듈 +### 1.4 PHP 확장 모듈 -| 모듈 | 용도 | -|------|------| -| `bcmath` | 정밀 수학 연산 (가격 계산 등) | -| `ctype` | 문자 타입 검사 | -| `curl` | HTTP 클라이언트 | -| `dom` | XML/HTML DOM 처리 | -| `exif` | 이미지 메타데이터 읽기 | -| `fileinfo` | MIME 타입 감지 | -| `filter` | 데이터 필터링/검증 | -| `gd` | 이미지 처리 (썸네일, 리사이징) | -| `hash` | 해시 함수 | -| `imagick` | 고급 이미지 처리 (ImageMagick) | -| `intl` | 국제화 (다국어, 날짜/숫자 포맷) | -| `json` | JSON 인코딩/디코딩 | -| `ldap` | LDAP 인증 연동 | -| `libxml` | XML 파싱 기반 라이브러리 | -| `maxminddb` | GeoIP 데이터베이스 조회 | -| `mbstring` | 멀티바이트 문자열 처리 | -| `memcached` | Memcached 캐시 드라이버 | -| `openssl` | 암호화/복호화 (AES-256-CBC) | -| `pcntl` | 프로세스 제어 (큐 워커) | -| `pcre` | 정규 표현식 | -| `pdo` | 데이터베이스 추상화 | -| `pdo_mysql` | MySQL/MariaDB PDO 드라이버 | -| `phar` | Phar 아카이브 (Composer) | -| `posix` | POSIX 함수 (프로세스 관리) | -| `redis` | Redis 캐시/세션/큐 드라이버 | -| `session` | 세션 관리 | -| `simplexml` | 간편 XML 파싱 | -| `sodium` | 최신 암호화 라이브러리 | -| `tokenizer` | PHP 토큰 파싱 | -| `xml` | XML 파서 | -| `xmlwriter` | XML 문서 생성 | -| `zip` | ZIP 압축/해제 | -| `zlib` | 데이터 압축 | +필수 16개 + 기능별 선택 17개. + +| 구분 | 모듈 | 용도 | +|------|------|------| +| 필수 | `ctype` | 문자 타입 검사 | +| 필수 | `curl` | HTTP 클라이언트 | +| 필수 | `dom` | XML/HTML DOM 처리 (메일 본문 인라인 CSS 등) | +| 필수 | `fileinfo` | MIME 타입 감지 | +| 필수 | `filter` | 데이터 필터링/검증 | +| 필수 | `hash` | 해시 함수 | +| 필수 | `json` | JSON 인코딩/디코딩 | +| 필수 | `mbstring` | 멀티바이트 문자열 처리 | +| 필수 | `openssl` | 암호화/복호화 (AES-256-CBC) | +| 필수 | `pcre` | 정규 표현식 | +| 필수 | `pdo` | 데이터베이스 추상화 | +| 필수 | `pdo_mysql` | MySQL/MariaDB PDO 드라이버 | +| 필수 | `session` | 세션 관리 | +| 필수 | `tokenizer` | PHP 토큰 파싱 | +| 필수 | `xml` | XML 파서 | +| 필수 | `zip` | 확장·언어팩·코어 업데이트 패키지의 압축 해제 | +| 선택 | `gd` | 이미지 처리 (썸네일, 리사이징) — `imagick` 과 택일 | +| 선택 | `imagick` | 고급 이미지 처리 (ImageMagick) — `gd` 와 택일 | +| 선택 | `exif` | 이미지 메타데이터 읽기 | +| 선택 | `intl` | 국제화 (다국어 날짜/숫자 포맷) | +| 선택 | `bcmath` | 정밀 수학 연산 (금액 계산 등) | +| 선택 | `redis` | Redis 캐시/세션/큐 드라이버 (`predis` 동봉 — 확장 없이도 사용 가능) | +| 선택 | `memcached` | Memcached 캐시 드라이버 | +| 선택 | `pcntl` | 큐 워커 전체 기능 및 콘솔 시그널 처리 | +| 선택 | `posix` | 큐 워커 전체 기능 및 코어 업데이트의 파일 소유자 확인 | +| 선택 | `maxminddb` | GeoIP 데이터베이스 조회 (접속 국가·타임존 감지) | +| 선택 | `simplexml` | AWS SDK(S3/SES/SQS) 응답 파싱 | +| 선택 | `libxml` | XML 파싱 기반 라이브러리 (`dom`·`simplexml` 의 토대) | +| 선택 | `sodium` | 최신 암호화 라이브러리 | +| 선택 | `phar` | Phar 아카이브 (Composer 실행) | +| 선택 | `xmlwriter` | XML 문서 생성 | +| 선택 | `zlib` | 데이터 압축 (gzip 응답 압축) | +| 선택 | `ldap` | LDAP 연동 확장을 사용할 때 (코어 기본 기능은 사용하지 않음) | + +설치 프로그램은 위 필수 확장 중 14개(`pdo`, `mbstring`, `openssl`, `tokenizer`, `xml`, +`ctype`, `json`, `fileinfo`, `curl`, `dom`, `filter`, `hash`, `pcre`, `session`)를 설치 +단계에서 자동 검사하고 하나라도 없으면 진행을 막는다. `pdo_mysql` 과 `zip` 은 자동 검사 +대상이 아니지만 각각 데이터베이스 접속과 확장·언어팩 설치에 반드시 필요하므로 필수로 +분류한다. 선택 확장은 해당 기능을 사용하는 시점에 필요하며, 자동 검사 항목과 운영자가 +직접 확인해야 하는 항목의 전체 구분은 §9 를 참조한다. ### 1.5 PHP 설정 권장값 (php.ini) @@ -150,6 +163,17 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates 인스톨러의 기본 안내 명령은 보수적으로 `chmod -R 755` 를 제시한다. 방식 A 로 운영하려면 인스톨 완료 후 `775` 로 재조정 + 소유자/그룹을 본인 계정 + `www-data` 로 변경. 인스톨러는 `chmod` 를 직접 호출하지 않으므로 운영자가 쉘에서 1회 실행. +### 1.7 개발/빌드 도구 + +| 도구 | 버전 | 필요한 경우 | +|------|------|------------| +| Node.js | 20 이상 | 프론트엔드 소스를 직접 빌드할 때 | +| Composer | 2.x | Composer 로 의존성을 설치할 때, 확장이 외부 패키지를 요구할 때 | + +배포본에는 빌드 산출물과 `vendor` 디렉토리가 동봉되어 있으므로 **일반적인 운영 설치에는 +두 도구 모두 필요하지 않다.** 소스에서 직접 빌드하거나 개발 환경을 구성할 때, 또는 +Composer 설치 방식을 선택할 때만 필요하다. + --- ## 2. 데이터베이스 @@ -166,8 +190,8 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates | 항목 | 값 | 비고 | |------|-----|------| | charset | `utf8mb4` | 이모지 등 4바이트 문자 지원 | -| collation | `utf8mb4_unicode_ci` | 유니코드 정렬 | -| 테이블 접두어 | `g7_` (기본값) | `.env`에서 `DB_PREFIX`로 변경 가능 | +| collation | `utf8mb4_unicode_ci` | 기본값이며 `.env`의 `DB_COLLATION`으로 변경 가능 | +| 테이블 접두어 | `g7_` (기본값) | `.env`에서 `DB_PREFIX`로 변경 가능 (최대 6자 — 자동 생성 인덱스명이 MySQL 식별자 한도 64자를 넘지 않도록 제한) | **선택 기능**: - Write/Read 분리: Master-Replica 구성 지원 (`DB_WRITE_*` / `DB_READ_*` 환경 변수) @@ -210,7 +234,7 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates | 수준 | 용량 | 포함 범위 | |------|------|----------| -| 최소 | **700MB** | 코어 + 기본 확장 | +| 최소 | **500MB** (설치 프로그램 검사 기준) | 설치가 진행되기 위한 여유 공간의 하한. 코어 + 기본 확장의 실사용은 약 700MB 이므로 여유 확보 권장 | | 권장 | **2GB 이상** | 코어 + 확장 + 첨부파일 + 캐시 + 로그 | - 사용자 업로드 파일, 로그, 캐시 등은 별도 용량 산정 필요 @@ -237,7 +261,8 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates | AWS SES | 이메일 발송 | 선택 | | AWS SQS | 큐 처리 (대규모 트래픽) | 선택 | -- `aws/aws-sdk-php` 패키지 포함됨 +- `aws/aws-sdk-php` · `league/flysystem-aws-s3-v3` 패키지 포함됨 (S3 및 S3 호환 스토리지 — Cloudflare R2, MinIO 등 — 를 별도 설치 없이 사용 가능) +- `predis/predis` 패키지 포함됨 (phpredis PHP 확장이 없는 서버에서도 Redis 드라이버 사용 가능) ### 4.3 메일 서비스 @@ -269,6 +294,8 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates |------|---------| | 프로덕션 | **HTTPS 필수** | +- 설치 단계에서는 HTTPS 여부를 경고로 안내하며 설치를 차단하지 않는다. 정책상 필수라는 + 선언은 유지하되, 자동으로 강제되지는 않으므로 운영자가 직접 확인해야 한다 - Laravel Reverb WebSocket도 `wss://` 프로토콜 사용 (`REVERB_SCHEME=https`) - Sanctum 세션 인증 시 `SESSION_SECURE_COOKIE=true` 설정 권장 @@ -293,13 +320,17 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates ## 7. 지원 브라우저 -| 브라우저 | 지원 범위 | -|---------|----------| -| Chrome / Edge | 최신 2개 버전 | -| Firefox | 최신 2개 버전 | -| Safari | 최신 2개 버전 | +| 브라우저 | 지원 기준 | 검증 방식 | +|---------|----------|----------| +| Chrome / Edge | React 19 + Tailwind CSS 4 호환 범위 | 자동 브라우저 테스트(Chromium)로 상시 검증 | +| Firefox | React 19 + Tailwind CSS 4 호환 범위 | 호환 범위 기준 지원 (상시 자동 테스트 대상 아님) | +| Safari | React 19 + Tailwind CSS 4 호환 범위 | 호환 범위 기준 지원 (상시 자동 테스트 대상 아님, 실기기 검증 미수행) | -- React 19 + Tailwind CSS 4 호환 범위 기준 +- 지원 하한은 프론트엔드 핵심 의존성(React 19, Tailwind CSS 4)의 공식 지원 브라우저 + 범위를 따르며, 해당 의존성 버전이 변경되면 이 기준도 함께 재검토한다 +- 참고: 2026-08 현재 Tailwind CSS 4 의 공식 하한은 Chrome 111 / Safari 16.4 / Firefox 128 이다 +- 자동 테스트는 테스트 도구에 포함된 단일 브라우저 빌드로 수행하므로 특정 버전 + 목록(예: "최신 N개 버전")을 상시 보장하지 않는다 - Internet Explorer 미지원 --- @@ -321,3 +352,44 @@ sudo chmod -R 755 storage bootstrap/cache vendor modules plugins templates | 커스텀 포트 (80/443 외) | Reverb WebSocket (기본 8080 포트) | Pusher 등 외부 WebSocket 서비스 | | 파일 권한 (`symlink` 등) | `storage/` 심볼릭 링크 | `php artisan storage:link` 대체 방식 확인 | | 디스크 용량 | 첨부파일, 로그, 캐시 누적 | 플랜별 용량 확인, 정기 정리 | + +--- + +## 9. 요구사항 검증 수준 + +이 문서의 항목은 자동으로 확인되는 것과 운영자가 직접 확인해야 하는 것으로 나뉜다. +"요구사항" 이라는 표현이 곧 "설치 프로그램이 검사한다" 를 뜻하지는 않는다. + +### 9.1 설치 프로그램이 자동 검사 + +| 항목 | 기준 | 미충족 시 | +|------|------|----------| +| PHP 버전 | 8.2 이상 | 설치 차단 | +| PHP 확장 | §1.4 필수 확장 중 14개 | 설치 차단 | +| 차단 함수 | `exec`·`proc_open`·`shell_exec` 사용 가능 | 설치 차단 | +| 디스크 여유 공간 | 500MB | 설치 차단 | +| 디렉토리 쓰기 권한 | `storage/`, `bootstrap/cache`, `vendor`, 확장 디렉토리 등 | 설치 차단 | +| `.env` 파일 | 존재 + 쓰기 가능 | 설치 차단 | +| 데이터베이스 접속 | 접속 성공 여부 | 다음 단계 진행 불가 | +| DB 테이블 접두어 | 최대 6자 | 입력 거부 | +| 웹 ↔ CLI PHP 버전 | 메이저·마이너 일치 | 안내만 (설치 진행) | +| OPcache | 활성화 여부 | 안내만 (설치 진행) | +| HTTPS | 사용 여부 | 안내만 (설치 진행) | + +`gd`·`imagick`·`redis`·`intl`·`zlib` 은 설치 단계에서 상태를 표시하지만 설치를 막지 않는다. + +### 9.2 운영자 확인 필요 — 자동 검사 없음 + +| 항목 | 문서상의 기준 | 확인 방법 | +|------|--------------|----------| +| 데이터베이스 버전 | MySQL 8.0+ / MariaDB 10.3+ | `SELECT VERSION()` | +| DB charset·collation | `utf8mb4` / `utf8mb4_unicode_ci` | 접속 후 스키마 확인 | +| php.ini 값 | §1.5 의 5개 항목 | `php -i` 또는 관리자 정보 화면 | +| CPU·메모리 | §3.1 | 서버 사양 확인 | +| 웹서버 종류·버전 | Nginx 1.18+ / Apache 2.4+ | 서버 설정 확인 | +| `mod_rewrite` | Apache 사용 시 활성화 | 서버 설정 확인 | +| Redis 버전 | 6.0 이상 | `redis-server --version` | +| Node.js·Composer 버전 | §1.7 | 소스 빌드·Composer 설치를 할 때만 해당 | +| 지원 브라우저 | §7 | 자동 브라우저 테스트는 Chromium 만 상시 수행 | +| 데몬 프로세스 가동 | §6 의 3종 | 프로세스 관리 도구에서 확인 | +| §1.4 의 선택 확장 | 사용하는 기능에 따라 | 관리자 정보 화면의 확장 목록 | diff --git a/docs/testing-guide.md b/docs/testing-guide.md index c57b6cf4..6720b766 100644 --- a/docs/testing-guide.md +++ b/docs/testing-guide.md @@ -341,6 +341,28 @@ TypeScript 테스트는 `// @scenario` / `// @effects` 주석 동일 사용. 정적 검사가 매니페스트 cross product 와 effects 항목을 테스트 docblock 마킹과 대조하여 누락을 검출한다 (자동 차단). +#### 항목 구분자는 쉼표뿐이다 + +마커 파서는 축과 effects 를 **쉼표로만** 분리한다. 요약을 적을 때 흔히 쓰는 `×`(곱)나 `+`(더하기)를 구분자로 두면 여러 항목이 **한 문자열로 뭉쳐** 등록된다. + +| ❌ 금지 | 실제 등록 결과 | +| --- | --- | +| `@scenario feat a=1 × b=2` | 축 1개 `{a: "1 × b=2"}` — 실재하지 않는 조합 | +| `@effects x + y + z` | 효과 1개 `"x + y + z"` — x·y·z 는 미등록 | +| `@scenario a + b + c` (`=` 없음) | 빈 조합 `{}` — 아무것도 커버하지 않음 | + +그렇게 만들어진 마커는 **어떤 조합도 커버하지 못하는 죽은 마커**다. 게다가 cross product 대조는 "매니페스트가 요구하는 항목이 마커에 있는가" 만 보고 마커 쪽에 생긴 쓰레기 항목은 무시하므로, 구분자를 틀려도 전 게이트가 조용히 통과한다 — 실제로 36건이 그렇게 쌓였다. + +항목이 여럿이면 `, ` 로 적고, 파일/클래스 레벨의 **요약**이라면 마커 토큰을 걷어내 평문으로 내린다. 파일 레벨에 effects 목록을 몰아 적으면 그 메서드가 하나도 없어도 "언급됨" 으로 집계되어 커버리지가 부풀고, 메서드 삭제가 무증상 green 이 된다 — 마커는 test 에만 둔다. + +```php +/** + * 축 요약(마커 아님 — 평문): actor, operation, outcome. + */ +``` + +구분자 형식은 정적 검사가 강제한다 (자동 차단). + ### cross product 폭발 관리 축이 많아 cross product 가 비현실적으로 커질 때: diff --git a/docs/testing/e2e-testing.md b/docs/testing/e2e-testing.md index 06290ada..bd099c88 100644 --- a/docs/testing/e2e-testing.md +++ b/docs/testing/e2e-testing.md @@ -11,6 +11,7 @@ - 데이터 생성: 데이터를 소유한 영역에 시드 커맨드 배치 (코어 ↔ 모듈 의존 역전 회피) - 편집기 저장 spec: 제품 화면 대신 전용 시드 화면(e2e_sandbox) 대상 — globalSetup 이 매 실행 원본으로 덮어씀 (§4.1) - 외부 의존: mock-first 전략 (page.route() 로 결제창/외부 API 가로채기) +- 브라우저: 일상 E2E 는 Chromium 전용 — 확대 조건은 §3 브라우저 범위 ``` ## §1. 도구 선택 정당화 @@ -113,6 +114,35 @@ spec 이 전부 실패한다. | 워커 수 | 관리자 SPA 는 번들이 크고 레이아웃을 여러 번 받아온다. 개발 머신에서 2워커 이상이면 `page.waitForLoadState` 가 30초를 넘겨 **비결정적으로** 실패한다(실측: 같은 스위트가 회차마다 다른 5~7건 실패, 테스트당 8초 → 25초). 판정은 `--workers=1` 결과로 한다 | | 편집기 spec 만 몰아 실행할 때 | 레이아웃 편집기 spec 만 골라 돌리면 워커 전부가 동시에 편집기 페이지를 연다 — 전체 스위트에서는 가벼운 spec 이 섞여 그 집중이 생기지 않는다. 실측: 편집기 7파일 27건을 7워커로 돌리면 `g7le-preview-frame` 대기가 전부 30초 타임아웃(27/27 실패), 같은 코드로 2워커는 26/27 통과. 편집기만 선택 실행할 때는 `--workers=2` 이하로 둔다 | +### 브라우저 범위 + +일상 E2E 는 Chromium 전용이 정책이다. 코어 config 와 확장 config 모두 `projects` 에 +chromium 하나만 두며, `npx playwright install chromium` 만 안내하는 것(§2)이 곧 이 정책이다. +문서상의 지원 브라우저 선언(`docs/requirements.md` §7)은 이 자동 검증 범위와 구분해서 읽는다 — +Firefox/Safari 는 프론트엔드 의존성(React 19, Tailwind CSS 4)의 호환 범위 기준 지원이며 +상시 자동 테스트 대상이 아니다. + +이 정책의 근거는 위험 표면의 위치다. 엔진과 레이아웃에는 브라우저별 분기 코드가 없어 JS +레벨의 브라우저 편차 위험이 낮고, 실질 위험은 CSS 렌더링과 contenteditable(위지윅) 거동에 +몰려 있다. 그 두 축은 브라우저 프로젝트를 늘리는 것보다 해당 spec 을 두껍게 쓰는 편이 +비용 대비 회수가 크다. + +Firefox/WebKit 프로젝트를 상시 스위트에 넣지 않은 이유: + +- 프로젝트 간 병렬 실행이 공유 서버 상태를 경합시킨다 — 시드 화면 PUT 저장(§4.1)과 관리자 + 설정 저장 spec 은 같은 레코드를 건드리므로 브라우저별 실행이 서로의 저장을 덮어쓴다. +- 시나리오 매니페스트에 브라우저 축을 추가하면 cross product 가 브라우저 수만큼 배가된다. +- 총 실행 시간이 프로젝트 수에 비례해 늘어, 개발 머신의 워커 예산(위 "실행 시 유의")과 + 정면으로 충돌한다. + +향후 도입할 경우 아래 4가지를 조건으로 한다: + +- 상시 스위트가 아니라 **별도 opt-in 스위트**로만 실행한다 (기본 실행 대상에 넣지 않는다). +- 대상은 `@smoke` 로 한정한다 — 레이아웃 편집기 저장 spec 을 배제해 시드 화면 경합을 피한다. +- `--workers=1` 로 고정한다. +- CDP 를 쓰는 spec(`specs/smoke/localinit-multi-progressive-datasource.spec.ts` 의 + `newCDPSession`)은 `test.skip(browserName !== 'chromium')` 로 제외한다. + ## §4. 데이터 생성 위치 — 책임 분리 매트릭스 | 데이터 종류 | 책임 영역 | 위치 | 호출 | diff --git a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md index 5e2c39ae..7caf1bb9 100644 --- a/lang-packs/_bundled/g7-core-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-core-ja/CHANGELOG.md @@ -4,6 +4,35 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.6] - 2026-08-19 + +### Changed + +- 회원 관리(생성·수정·삭제·탈퇴) 실패 안내에서 오류 원문 노출(`:error`)이 제거된 것에 맞춰, 해당 실패 문구의 일본어 번역을 재시도 안내 문구로 갱신했습니다 (`user.create_failed`, `user.update_failed`, `user.delete_failed`, `user.withdraw_failed`). +- S3 리전 검증 메시지를 목록 선택 안내에서 형식 안내(소문자 영숫자·하이픈, R2 는 `auto`)로 개정했습니다. + +### Added + +- 검색엔진 드라이버 라벨 일본어 번역을 추가했습니다 (`settings.drivers.search.*`) — 환경설정 > 드라이버 탭의 검색엔진 선택지가 일본어 로케일에서 표시됩니다. +- S3 호환 스토리지 설정 신설 항목(엔드포인트 URL·Path-style 주소)과 리전 형식 안내의 검증 메시지 일본어 번역 추가 (`validation.settings.s3_endpoint_*`, `s3_use_path_style_boolean`, `s3_region_*`) — 드라이버 설정 저장 시 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다. (#99 @lyg-kaban 님께서 제보해주셨습니다.) +- 드라이버 사용 불능 안내 문구 일본어 번역 추가 (`validation.settings.driver_unusable`, `settings.driver_unusable_*`, `settings.s3_adapter_missing`) — 필요한 라이브러리·PHP 확장이 없는 드라이버를 저장·테스트할 때의 사유 안내가 일본어 로케일에서 표시됩니다. +- 웹소켓 서버(백엔드 발송용) endpoint 연결 실패 안내 일본어 번역 추가 (`settings.websocket_server_test_failed`). +- 공개 자산 스토리지(공개 이미지 직접 URL 서빙) 설정의 드라이버 라벨과 검증 메시지 일본어 번역을 추가했습니다. 환경설정 > 드라이버 탭의 새 설정이 일본어 로케일에서 자연스럽게 표시됩니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 설정 항목 단위 저장의 값 형식 안내 일본어 번역을 추가했습니다 (`validation.setting.value.*`) — 켜기/끄기·숫자 설정에 맞지 않는 값을 저장할 때의 안내가 일본어 로케일에서 표시됩니다. +- 슈퍼 관리자 계정·역할 수정 상한 및 권한 부여 상한 위반 시 표시되는 예외 메시지(`cannot_modify_super_admin`, `cannot_grant_unheld_permission`, `cannot_modify_protected_role`)의 일본어 번역을 추가했습니다. +- 레이아웃 저장 시 위험 표현식·외부 리소스 URL 거부 안내(`layout.dangerous_expression`, `layout.external_resource_url`)의 일본어 번역을 추가했습니다. +- 고아 첨부 정리 설정의 검증 메시지와 항목명 일본어 번역을 추가했습니다 (`validation.settings.orphan_*`) — 환경설정 > 업로드의 자동 정리·보존기간을 잘못 저장할 때의 안내가 일본어 로케일에서 항목 이름과 함께 표시됩니다. +- 데이터베이스 백업 미제공 안내 문구의 일본어 번역을 추가했습니다 (`settings.database_backup_unavailable`). +- 셸 예약 작업 허용목록에 인터프리터를 등록했을 때 인라인 명령 실행·안전하지 않은 스크립트 경로를 거부하는 안내 문구의 일본어 번역을 추가했습니다 (`validation.schedule_command.shell_interpreter_denied`). + +### Removed + +- 참조처가 사라진 S3 리전 선택 검증 메시지(`validation.settings.s3_region_required`)를 원본(ko/en)과 함께 정리했습니다. + +### Fixed + +- 범용 입력 항목의 검증 문구가 특정 화면 전용 이름(SMTP 비밀번호·기본 언어·기본 통화 등)으로 표시되던 문제를 수정했습니다 — 전역 라벨을 범용 문구로 되돌리고 SMTP·사용자 목록 등 도메인 전용 라벨을 별도 키로 분리했습니다. (#113 @Tuwasduliebst 님께서 제보해주셨습니다.) + ## [1.0.5] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php b/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php index c48f93b0..52882c27 100644 --- a/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php +++ b/lang-packs/_bundled/g7-core-ja/backend/ja/exceptions.php @@ -2,6 +2,9 @@ return [ 'cannot_delete_super_admin' => 'スーパー管理者は削除できません。', + 'cannot_modify_super_admin' => 'スーパー管理者のアカウントまたはロールを変更する権限がありません。', + 'cannot_grant_unheld_permission' => '自身が保有していない権限、または自身より広い範囲の権限は付与できません。', + 'cannot_modify_protected_role' => 'システムまたは拡張機能が所有するロールを変更する権限がありません。', 'circular_reference' => 'レイアウト循環参照を検出しました: :trace', 'max_depth_exceeded' => 'レイアウトネストの深さが最大許容深度(:max)を超過しました。', 'template_file_copy_failed' => 'テンプレートファイルのコピーに失敗しました: :source → :destination', diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/settings.php b/lang-packs/_bundled/g7-core-ja/backend/ja/settings.php index 120b375b..d6cc8a53 100644 --- a/lang-packs/_bundled/g7-core-ja/backend/ja/settings.php +++ b/lang-packs/_bundled/g7-core-ja/backend/ja/settings.php @@ -20,6 +20,7 @@ return [ 'backup_success' => 'データベースバックアップが正常に開始されました。', 'backup_failed' => 'データベースバックアップに失敗しました。', 'backup_error' => 'データベースバックアップ中にエラーが発生しました。', + 'database_backup_unavailable' => 'データベースバックアップ機能はまだ提供されていません。設定のバックアップは設定バックアップ機能をご利用ください。', 'backup_path_required' => 'バックアップパスを入力してください。', 'restore_success' => '設定が正常に復元されました。', 'restore_failed' => '設定の復元に失敗しました。', @@ -36,6 +37,11 @@ return [ 'local' => 'ローカル', 's3' => 'Amazon S3', ], + 'public_asset' => [ + 'none' => '使用しない (ストリーミング)', + 'public' => 'Public ディスク', + 's3' => 'Amazon S3', + ], 'cache' => [ 'file' => 'ファイル', 'redis' => 'Redis', @@ -62,6 +68,9 @@ return [ 'mailgun' => 'Mailgun', 'ses' => 'SES (Amazon)', ], + 'search' => [ + 'mysql-fulltext' => 'MySQL 全文検索', + ], ], 'driver_test_success' => 'すべてのドライバ接続テストが成功しました。', 'driver_test_partial' => '一部のドライバ接続テストが失敗しました。', @@ -71,6 +80,7 @@ return [ 's3_test_failed' => 'S3バケットへの接続に失敗しました。', 's3_missing_config' => 'S3設定が不足しています。(バケット、リージョン、アクセスキー、シークレットキー)', 's3_sdk_missing' => 'AWS SDKがインストールされていません。', + 's3_adapter_missing' => 'S3ストレージアダプター(league/flysystem-aws-s3-v3)がインストールされていません。', 's3_bucket_not_found' => 'S3バケットが見つかりません。', 's3_access_denied' => 'S3バケットへのアクセスが拒否されました。', 's3_invalid_credentials' => 'S3認証情報が正しくありません。', @@ -86,6 +96,10 @@ return [ 'memcached_connection_failed' => 'Memcachedサーバーに接続できません。', 'websocket_test_success' => 'Websocketサーバーに正常に接続されました。', 'websocket_test_failed' => 'Websocketサーバーへの接続に失敗しました。', + 'websocket_server_test_failed' => 'Websocketサーバー(バックエンド送信用)エンドポイントへの接続に失敗しました。クライアントエンドポイントは正常です。', + 'driver_unusable_s3_adapter' => 'S3ストレージアダプター(league/flysystem-aws-s3-v3)がインストールされていません。', + 'driver_unusable_redis_client' => 'phpredis拡張とpredisライブラリのどちらも利用できません。', + 'driver_unusable_memcached_extension' => 'memcached PHP拡張がインストールされていません。', 'websocket_invalid_host' => 'Websocketホスト設定が正しくありません。ホストにはアドレスのみを入力し、アカウント情報(@)またはhttp/https以外のプロトコルは使用できません。', 'websocket_connection_refused' => 'Websocketサーバーに接続できません。サーバーが実行中であるか確認してください。', 'invalid_email' => '無効なメールアドレスです。', diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/user.php b/lang-packs/_bundled/g7-core-ja/backend/ja/user.php index 7b523fbd..e63c77bd 100644 --- a/lang-packs/_bundled/g7-core-ja/backend/ja/user.php +++ b/lang-packs/_bundled/g7-core-ja/backend/ja/user.php @@ -16,11 +16,11 @@ return [ 'fetch_success' => 'ユーザー情報を正常に取得しました。', 'fetch_failed' => 'ユーザー情報の取得に失敗しました。', 'create_success' => 'ユーザーが正常に作成されました。', - 'create_failed' => 'ユーザー作成に失敗しました: :error', + 'create_failed' => 'ユーザー作成に失敗しました。しばらくしてからもう一度お試しください。', 'update_success' => 'ユーザー情報が正常に更新されました。', - 'update_failed' => 'ユーザー情報の更新に失敗しました: :error', + 'update_failed' => 'ユーザー情報の更新に失敗しました。しばらくしてからもう一度お試しください。', 'delete_success' => 'ユーザーが正常に削除されました。', - 'delete_failed' => 'ユーザー削除に失敗しました: :error', + 'delete_failed' => 'ユーザー削除に失敗しました。しばらくしてからもう一度お試しください。', 'statistics_success' => 'ユーザー統計を正常に取得しました。', 'statistics_failed' => 'ユーザー統計の取得に失敗しました。', 'search_success' => 'ユーザー検索が正常に完了しました。', @@ -54,7 +54,7 @@ return [ 'password_change_success' => 'パスワードが正常に変更されました。', 'password_change_failed' => 'パスワード変更に失敗しました。', 'withdraw_success' => '会員退会が完了しました。', - 'withdraw_failed' => '会員退会に失敗しました: :error', + 'withdraw_failed' => '会員退会に失敗しました。しばらくしてからもう一度お試しください。', 'withdraw_admin_forbidden' => '管理者アカウントは退会できません。', 'validation' => [ 'page_integer' => 'ページ番号は数字である必要があります。', @@ -123,4 +123,5 @@ return [ 'exclude_user_id_uuid' => '除外するユーザーIDは有効なUUIDである必要があります。', 'exclude_user_id_exists' => '存在しないユーザーです。', ], + 'withdraw_failed_unknown' => '不明なエラーにより退会処理に失敗しました。', ]; diff --git a/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php b/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php index cd0e5b62..a0a08028 100644 --- a/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php +++ b/lang-packs/_bundled/g7-core-ja/backend/ja/validation.php @@ -190,6 +190,8 @@ return [ ], 'invalid_json' => '無効な JSON 形式です。', 'must_be_array' => 'レイアウトデータは配列である必要があります。', + 'dangerous_expression' => '許可されていない式が含まれています: :snippet', + 'external_resource_url' => '外部リソース URL は許可されていません(同一オリジンのパスのみ): :url', 'required_field_missing' => '必須フィールド \':field\' がありません。', 'version_must_be_string' => 'version フィールドは文字列である必要があります。', 'layout_name_must_be_string' => 'layout_name フィールドは文字列である必要があります。', @@ -479,6 +481,7 @@ return [ ], 'schedule_command' => [ 'shell_not_allowed' => '許可されていないシェルコマンドです。サーバーに登録された実行ファイルのみ使用でき、パイプ(|)·セミコロン(;)などの特殊文字は使用できません。', + 'shell_interpreter_denied' => 'インタープリタでインラインコマンド/コードを実行したり、安全でないパスを指定したりすることはできません。インタープリタの後には絶対パスのスクリプトファイルのみを指定してください。', 'artisan_denied' => 'この Artisan コマンドはセキュリティ上の理由からスケジュール実行できません。', 'artisan_not_allowlisted' => 'スケジュール実行が許可された Artisan コマンドではありません。キャッシュ削除·キュー処理などのメンテナンスコマンドのみ登録できます。', 'artisan_malformed' => 'Artisan コマンドの形式が正しくありません。引用符·バックスラッシュ·短縮オプション(-v)は使用できず、"コマンド名 --オプション[=値]" の形式のみ登録できます。', @@ -750,9 +753,13 @@ return [ ], 'setting' => [ 'value' => [ - 'required' => '設定値は必須です。', - 'string' => '設定値は文字列である必要があります。', + 'present' => 'リクエストに設定値(value)項目が含まれている必要があります。', + 'boolean' => 'この設定には有効/無効の値のみ保存できます。', + 'integer' => 'この設定には整数のみ保存できます。', + 'numeric' => 'この設定には数値のみ保存できます。', + 'type' => '保存できない形式の設定値です。', 'max' => '設定値は :max 文字を超えることはできません。', + 'array_max' => '設定値のサイズは :max 文字を超えることはできません。', ], ], 'exclude_current_user' => '現在ログインしているユーザーは一括変更の対象に含めることはできません。', @@ -897,11 +904,16 @@ return [ 'storage_driver_required' => 'ストレージドライバーを選択してください。', 'storage_driver_invalid' => '正しいストレージドライバーを選択してください。', 's3_bucket_max' => 'S3バケット名は255字を超えることはできません。', - 's3_region_invalid' => '正しいS3リージョンを選択してください。', + 's3_region_invalid' => 'S3リージョンには小文字の英数字とハイフンのみ使用できます。(例: ap-northeast-2、Cloudflare R2 は auto)', + 's3_region_max' => 'S3リージョンは64字を超えることはできません。', 's3_access_key_max' => 'S3 Access Keyは255字を超えることはできません。', 's3_secret_key_max' => 'S3 Secret Keyは255字を超えることはできません。', 's3_url_invalid' => '正しいS3 URL形式ではありません。', 's3_url_max' => 'S3 URLは500字を超えることはできません。', + 's3_endpoint_invalid' => '正しいS3エンドポイントURL形式ではありません。', + 's3_endpoint_max' => 'S3エンドポイントURLは500字を超えることはできません。', + 's3_use_path_style_boolean' => 'Path-styleアドレス使用の設定はtrueまたはfalseである必要があります。', + 'driver_unusable' => '\':driver\' ドライバーはこのサーバーでは使用できません。:reason', 'cache_driver_required' => 'キャッシュドライバーを選択してください。', 'cache_driver_invalid' => '正しいキャッシュドライバーを選択してください。', 'redis_host_max' => 'Redisホストは255字を超えることはできません。', @@ -949,8 +961,8 @@ return [ 'log_days_integer' => 'ログ保持日数は整数である必要があります。', 'log_days_min' => 'ログ保持日数は1以上である必要があります。', 'log_days_max' => 'ログ保持日数は365を超えることはできません。', + 'public_asset_disk_invalid' => '正しい公開アセットディスクを選択してください。', 's3_bucket_required' => 'S3バケット名は必須です。', - 's3_region_required' => 'S3リージョンを選択してください。', 's3_access_key_required' => 'S3 Access Keyは必須です。', 's3_secret_key_required' => 'S3 Secret Keyは必須です。', 's3_url_url' => 'S3 URLは有効なURLである必要があります。', @@ -983,6 +995,10 @@ return [ 'pagination_max_page_integer' => 'ページ番号上限は数値である必要があります。', 'pagination_max_page_min' => 'ページ番号上限は:min以上である必要があります。(0 = 無制限)', 'pagination_max_page_max' => 'ページ番号上限は:maxを超えることはできません。', + 'orphan_cleanup_enabled_boolean' => '孤立した添付ファイルの自動クリーンアップは有効化または無効化のいずれかである必要があります。', + 'orphan_retention_days_integer' => '孤立した添付ファイルの保持期間は整数である必要があります。', + 'orphan_retention_days_min' => '孤立した添付ファイルの保持期間は1日以上である必要があります。', + 'orphan_retention_days_max' => '孤立した添付ファイルの保持期間は3650日を超えることはできません。', ], 'identity_policy' => [ 'key_required' => 'ポリシーキーを入力してください。', @@ -1024,7 +1040,8 @@ return [ 'ses_key' => 'SES アクセスキー', 'ses_secret' => 'SES シークレットキー', 'ses_region' => 'SES リージョン', - 'ids' => 'ユーザーID一覧', + 'ids' => 'ID一覧', + 'user_ids' => 'ユーザーID一覧', 'user_id' => 'ユーザーID', 'status' => 'ステータス', 'site_name' => 'サイト名', @@ -1032,7 +1049,8 @@ return [ 'site_description' => 'サイト説明', 'admin_email' => '管理者メール', 'timezone' => 'タイムゾーン', - 'language' => 'デフォルト言語', + 'language' => '言語', + 'default_language' => 'デフォルト言語', 'identity_default_provider' => 'デフォルトプロバイダー', 'identity_purpose_providers' => '目的別プロバイダー', 'identity_challenge_ttl_minutes' => 'Challenge有効期限', @@ -1047,10 +1065,14 @@ return [ 'identity_policy_applies_to' => '適用対象', 'identity_policy_fail_mode' => '失敗モード', 'mailer' => 'メーラー', - 'host' => 'SMTPホスト', - 'port' => 'SMTPポート', - 'username' => 'SMTPユーザー名', - 'password' => 'SMTPパスワード', + 'host' => 'ホスト', + 'port' => 'ポート', + 'username' => 'ユーザー名', + 'password' => 'パスワード', + 'smtp_host' => 'SMTPホスト', + 'smtp_port' => 'SMTPポート', + 'smtp_username' => 'SMTPユーザー名', + 'smtp_password' => 'SMTPパスワード', 'encryption' => '暗号化', 'from_address' => '送信元メールアドレス', 'from_name' => '送信元名', @@ -1073,6 +1095,8 @@ return [ 's3_access_key' => 'S3 Access Key', 's3_secret_key' => 'S3 Secret Key', 's3_url' => 'S3 URL', + 's3_endpoint' => 'S3 エンドポイントURL', + 's3_use_path_style' => 'Path-styleアドレス使用', 'cache_driver' => 'キャッシュ ドライバー', 'redis_host' => 'Redis ホスト', 'redis_port' => 'Redis ポート', @@ -1089,8 +1113,10 @@ return [ 'websocket_port' => 'WebSocket ポート', 'websocket_scheme' => 'WebSocket プロトコル', 'mailgun_endpoint' => 'Mailgun エンドポイント', - 'channels' => '通知チャンネル', - 'currency' => 'デフォルト通貨', + 'channels' => 'チャンネル', + 'notification_channels' => '通知チャンネル', + 'currency' => '通貨', + 'default_currency' => 'デフォルト通貨', 'maintenance_mode' => 'メンテナンスモード', 'asset_url_mode' => 'アセットURL方式', 'site_logo' => 'サイトロゴ', @@ -1103,7 +1129,8 @@ return [ 'twitter_default_card' => 'Twitter デフォルトカード種類', 'twitter_default_site' => 'Twitter デフォルトアカウント', 'seo_page_cache_enabled' => 'SEO ページキャッシュの使用', - 'cache_ttl' => 'SEO ページキャッシュ保持時間', + 'cache_ttl' => 'キャッシュ保持時間', + 'seo_page_cache_ttl' => 'SEO ページキャッシュ保持時間', 'sitemap_enabled' => 'サイトマップの使用', 'sitemap_cache_ttl' => 'サイトマップキャッシュ保持時間', 'sitemap_urls_per_file' => 'サイトマップ1ファイルあたりのURL数', @@ -1151,6 +1178,9 @@ return [ 'log_driver' => 'ログ ドライバー', 'log_level' => 'ログレベル', 'log_days' => 'ログ保持日数', + 'public_asset_disk' => '公開アセットディスク', + 'orphan_cleanup_enabled' => '孤立した添付ファイルの自動クリーンアップ', + 'orphan_retention_days' => '孤立した添付ファイルの保持期間', ], 'layout_version' => [ 'limit' => [ diff --git a/lang-packs/_bundled/g7-core-ja/language-pack.json b/lang-packs/_bundled/g7-core-ja/language-pack.json index 4d25cdab..f904c3e6 100644 --- a/lang-packs/_bundled/g7-core-ja/language-pack.json +++ b/lang-packs/_bundled/g7-core-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 core Japanese language pack (bundled)", "ja": "G7 コア 日本語 言語パック(バンドル)" }, - "version": "1.0.5", + "version": "1.0.6", "license": "MIT", "scope": "core", "target_identifier": null, diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md b/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md index f8015f4b..1faf5bd9 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/CHANGELOG.md @@ -4,6 +4,19 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.3] - 2026-08-19 + +### Added + +- 답글 삭제 정책(reply_delete_policy) 설정 라벨·옵션·차단 안내 문구의 일본어 번역을 추가했습니다. +- 게시판 유형 삭제 등 관리 작업이 서버 오류로 실패했을 때 표시되는 안내 문구의 일본어 번역을 추가했습니다. +- 비공개(비밀) 게시글 목록 마스킹 시 표시되는 제목 플레이스홀더(`secret_post_title`)의 일본어 번역을 추가했습니다. +- 첨부 영구 정리 설정의 라벨·도움말과 검증 항목명 일본어 번역을 추가했습니다 (`admin/settings.attachment_purge_*`, `validation.attributes.attachment_settings.*`) — 게시판 환경설정 > 기본 설정의 새 항목과 저장 시 오류 안내가 일본어 로케일에서 표시됩니다. + +### Changed + +- 신고 반려 누적 제한 안내 문구의 일본어 번역을 실제 동작(설정 건수 도달 시 차단)에 맞게 정정했습니다. + ## [1.0.2] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/activity_log.php b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/activity_log.php index 41a8ec64..2c036e67 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/activity_log.php +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/activity_log.php @@ -71,6 +71,7 @@ return [ 'use_comment' => 'コメント使用', 'use_reply' => '返信使用', 'max_reply_depth' => '最大返信深度', + 'reply_delete_policy' => '返信の削除方式', 'use_report' => '通報使用', 'use_file_upload' => 'ファイルアップロード使用', 'max_file_size' => '最大ファイルサイズ', diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/enums.php b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/enums.php index b6ce372c..6c9a6632 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/enums.php +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/enums.php @@ -6,6 +6,10 @@ return [ 'enabled' => '選択使用', 'always' => '必須使用', ], + 'reply_delete_policy' => [ + 'block' => '削除をブロック(返信がある間は元の投稿を削除できません)', + 'cascade' => '一緒に削除(元の投稿の削除時に返信も削除されます)', + ], 'order_direction' => [ 'asc' => '昇順', 'desc' => '降順', diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/messages.php index 49e239e9..01a0cc78 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/messages.php @@ -34,6 +34,7 @@ return [ 'secret_password_required' => '非公開投稿のパスワードが必要です。', 'secret_password_incorrect' => '非公開投稿のパスワードが一致しません。', 'secret_post_content' => '非公開投稿です。内容を表示するにはパスワードを入力してください。', + 'secret_post_title' => '非公開投稿', 'deleted_post_title' => '削除された投稿', 'deleted_post_content' => '削除された投稿です。', 'blinded_post_content' => '管理者によってブロック処理された投稿です。', @@ -181,6 +182,7 @@ return [ 'permission_denied' => '権限がありません。', 'validation_failed' => '入力値が正しくありません。', 'duplicate_report' => '既に通報された履歴があります。', + 'operation_failed' => '処理中にエラーが発生しました。', ], 'warnings' => [ 'category_removal_attempted' => '使用中のカテゴリーの削除が試みられました。', diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/validation.php b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/validation.php index 80da450a..73ed656a 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/validation.php +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/backend/ja/validation.php @@ -219,6 +219,9 @@ return [ 'notice_not_allowed' => 'お知らせには返信を作成することはできません。', ], 'reply_not_allowed' => 'この掲示板は返信機能が無効化されています。', + 'delete' => [ + 'has_replies' => '返信が付いた投稿は削除できません。先に返信を削除してください。', + ], 'status' => [ 'in' => '無効な投稿ステータスです。', ], @@ -256,6 +259,7 @@ return [ 'basic_defaults.use_comment' => 'コメント使用有無', 'basic_defaults.use_reply' => '返信使用有無', 'basic_defaults.max_reply_depth' => '最大返信深さ', + 'basic_defaults.reply_delete_policy' => '返信の削除方式', 'basic_defaults.max_comment_depth' => '最大コメント深さ', 'basic_defaults.comment_order' => 'コメント並べ替え', 'basic_defaults.show_view_count' => '閲覧数表示', @@ -339,6 +343,7 @@ return [ 'override_values.per_page' => 'ページあたりの投稿数', 'override_values.per_page_mobile' => 'モバイルページあたりの投稿数', 'override_values.max_reply_depth' => '最大返信深さ', + 'override_values.reply_delete_policy' => '返信の削除方式', 'override_values.max_comment_depth' => '最大コメント深さ', 'override_values.min_title_length' => '最小タイトル長', 'override_values.max_title_length' => '最大タイトル長', @@ -349,6 +354,8 @@ return [ 'override_values.max_file_size' => '最大ファイルサイズ', 'override_values.max_file_count' => '最大ファイル個数', 'override_values.new_display_hours' => '新規表示時間', + 'attachment_settings.purge_enabled' => '削除添付の永久クリーンアップ', + 'attachment_settings.purge_retention_days' => '削除添付の保持期間', ], ], 'blind' => [ diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin.json b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin.json index 0610e730..d7650519 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin.json +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin.json @@ -926,7 +926,7 @@ "daily_report_limit_hint": "1日(午前0時基準)全体掲示板合わせて通報できる最大回数。0の場合は制限なし。 (0~100)", "rejection_limit_count": "却下累積制限 (件数)", "rejection_limit_days": "却下累積制限期間 (日)", - "rejection_limit_hint": "この期間内に却下件数を超えると通報機能がブロックされます。件数が0の場合は制限なし。 (件数: 0~50、期間: 1~365日)", + "rejection_limit_hint": "この期間内の却下件数が設定値に達すると通報機能がブロックされます。件数が0の場合は制限なし。 (件数: 0~50、期間: 1~365日)", "report_cooldown_seconds": "連続通報クールタイム (秒)", "report_cooldown_seconds_hint": "連続通報の最小間隔。0の場合は制限なし。 (0~3600秒)", "notify_author_on_report_action": "通報処理通知", diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/form.json b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/form.json index 834d893f..c31e5f15 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/form.json +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/form.json @@ -110,6 +110,14 @@ "label": "返信投稿の最大深さ", "hint": "{{min}}~{{max}}の範囲内の値を入力してください" }, + "reply_delete_policy": { + "label": "返信の削除方式", + "hint": "返信が付いた投稿を削除するときの動作を設定します", + "options": { + "block": "削除をブロック", + "cascade": "一緒に削除" + } + }, "max_comment_depth": { "label": "返信への返信の最大深さ", "hint": "{{min}}~{{max}}の範囲内の値を入力してください。0に設定すると返信への返信機能が無効化されます。" diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/settings.json b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/settings.json index 69a00fc2..41857176 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/settings.json +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/frontend/partial/admin/settings.json @@ -60,7 +60,9 @@ "report_notification": "通報通知", "report_notification_description": "通報受け付け時に通知を受け取る管理者と通知発送方式を設定します。", "report_permissions": "通報管理権限", - "report_permissions_description": "通報リスト閲覧および通報処理(承認·却下·ブラインド)を実行できるロールを指定します。通報受け付け通知は「通報処理権限」を持つロール基準で発送されます。" + "report_permissions_description": "通報リスト閲覧および通報処理(承認·却下·ブラインド)を実行できるロールを指定します。通報受け付け通知は「通報処理権限」を持つロール基準で発送されます。", + "attachment_cleanup": "添付ファイルの整理", + "attachment_cleanup_description": "削除された添付ファイルの物理ファイルをいつ廃棄するかを設定します。投稿作成中にアップロードされたが保存されなかった一時的な添付は、この設定に関わらず毎日自動で整理されます。" }, "fields": { "type": "掲示板タイプ", @@ -78,6 +80,7 @@ "max_content_length": "最大投稿文字数", "use_reply": "回答投稿の使用", "max_reply_depth": "回答投稿の最大深さ", + "reply_delete_policy": "返信の削除方式", "use_comment": "コメントの使用", "comment_order": "コメント並べ替え順序", "min_comment_length": "最小コメント文字数", @@ -103,7 +106,7 @@ "daily_report_limit_hint": "1日(午前0時基準)全掲示板で通報できる最大回数です。0の場合は制限なし。(0~100)", "rejection_limit_count": "却下累積制限 (件数)", "rejection_limit_days": "却下累積制限期間 (日)", - "rejection_limit_hint": "この期間内に却下件数を超えると通報機能がブロックされます。件数が0の場合は制限なし。(件数: 0~50、期間: 1~365日)", + "rejection_limit_hint": "この期間内の却下件数が設定値に達すると通報機能がブロックされます。件数が0の場合は制限なし。(件数: 0~50、期間: 1~365日)", "report_cooldown_seconds": "連続通報クールタイム (秒)", "report_cooldown_seconds_hint": "連続通報の最小間隔です。0の場合は制限なし。(0~3600秒)", "notify_author_on_report_action": "通報処理通知", @@ -166,6 +169,7 @@ "max_content_length": "投稿本文の最大文字数を設定します ({{min}}~{{max}})", "use_reply": "ON に設定する必要があります。新規掲示板に返信投稿機能が適用されます。", "max_reply_depth": "返信投稿の最大ネスト深さを設定します ({{min}}~{{max}})", + "reply_delete_policy": "返信が付いた投稿を削除するときの動作を設定します", "blocked_keywords": "登録されたキーワードを含む投稿/コメントは作成がブロックされます", "use_comment": "ON に設定する必要があります。新規掲示板にコメント機能が適用されます。以下のコメント詳細設定も一緒に適用されます。", "comment_order": "コメントのデフォルト並べ替え方向を設定します", @@ -176,7 +180,11 @@ "max_file_size": "アップロード可能な最大ファイルサイズです (MB単位、{{min}}~{{max}})", "max_file_count": "投稿あたり添付可能な最大ファイル数です ({{min}}~{{max}})", "allowed_extensions": "許可するファイル拡張子を最低1つ以上入力してください" - } + }, + "attachment_purge_enabled": "削除済み添付ファイルの永久削除", + "attachment_purge_enabled_hint": "デフォルトは無効です。運営者が有効にしたときにのみ毎日の自動整理が動作します。有効にすると、削除されてから保存期間が経過した添付ファイルとそのレコードが永久削除されます。サーバーで直接実行する整理コマンドはこの設定に左右されません。", + "attachment_purge_retention_days": "削除済み添付ファイル保存期間(日)", + "attachment_purge_retention_days_hint": "ゴミ箱から投稿を復元すると、この期間内の添付ファイルは一緒に復元されます。ゴミ箱の復元可能期間と同じに保つようにしてください。(1~3650 日)" }, "options": { "date_display_format": { @@ -197,6 +205,10 @@ "enabled": "選択可能", "always": "常に非公開" }, + "reply_delete_policy": { + "block": "削除をブロック", + "cascade": "一緒に削除" + }, "order_by": { "created_at": "最新順", "view_count": "閲覧数順", @@ -280,6 +292,7 @@ "use_comment": "コメント使用", "use_reply": "返信コメント使用", "max_reply_depth": "返信コメント最大深さ", + "reply_delete_policy": "返信の削除方式", "max_comment_depth": "返信の返信最大深さ", "comment_order": "コメント並べ替え順序", "show_view_count": "閲覧数表示", diff --git a/lang-packs/_bundled/g7-module-sirsoft-board-ja/language-pack.json b/lang-packs/_bundled/g7-module-sirsoft-board-ja/language-pack.json index 0b311854..ff730d88 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-board-ja/language-pack.json +++ b/lang-packs/_bundled/g7-module-sirsoft-board-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 module (sirsoft-board) Japanese language pack (bundled)", "ja": "G7 モジュール (sirsoft-board) 日本語 言語パック(バンドル)" }, - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "scope": "module", "target_identifier": "sirsoft-board", diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md index 7f766eba..570efff3 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/CHANGELOG.md @@ -4,6 +4,30 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Added + +- 리뷰 작성 불가 안내의 사유 라벨(`messages.reviews.reasons.*` — 주문 내역 없음·본인 주문 아님·구매확정 전·기한 경과·중복 작성) 일본어 번역을 추가했습니다. +- 확정 불가 상태의 주문 옵션 구매확정 시도 안내(`exceptions.order_option_not_confirmable`)의 일본어 번역을 추가했습니다. +- 이미 답변된 상품 문의에 답변을 재등록할 때의 거절 안내(`inquiries.reply_already_exists`) 일본어 번역을 추가했습니다. (#106 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 이용할 수 없는 결제수단 선택 시의 주문 거절 안내 일본어 번역을 추가했습니다 (`validation.order.payment_method_unavailable`). (#111 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 관리자 주문설정 결제수단 목록의 「지정 PG 삭제됨」 배지 일본어 번역을 추가했습니다. (#111 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 배송지 변경 처리 중 서버 오류가 발생했을 때 표시되는 안내 문구의 일본어 번역을 추가했습니다. (#104 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 배송정책 구간 설정의 새 검증 안내(구간 미등록, 중간 구간 종료값 누락, 수량 구간의 정수 제한, 단위값·무료배송 기준금액 누락, 도서산간 우편번호 형식) 일본어 번역을 추가했습니다. +- 배송정책 편집 화면에서 구간 입력 오류를 안내하는 문구(중간 구간 종료값 누락, 배송비 미입력, 수량 구간 정수 제한, 단위값·무료배송 기준금액 미입력) 일본어 번역을 추가했습니다. +- 배송비 요약에 표시되는 구간 단위(개/kg/L)와 구간 입력란의 단위 라벨 일본어 번역을 추가했습니다. +- 이커머스 설정 기본 정보 탭의 "공개 자산 스토리지" 설정 문구 일본어 번역을 추가했습니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 공개 자산 디스크에 사용할 수 없는 값을 저장할 때 나오는 오류 안내와 필드명의 일본어 번역을 추가했습니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) + +### Changed + +- 도서산간 추가배송비의 "중복 부과" 안내를 배송 단위 기준으로 정정했습니다. 이전 문구는 건수 기준으로 읽혀 실제 부과 방식과 달랐습니다. + +### Removed + +- 화면에서 더 이상 쓰이지 않는 배송비 요약 접두 단위 문구와 배송정책 통화 선택 안내 문구를 제거했습니다. + ## [1.1.0] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/exceptions.php b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/exceptions.php index 2251139a..0774befb 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/exceptions.php +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/exceptions.php @@ -83,4 +83,6 @@ return [ 'coupon_issue_not_found' => 'クーポン発行履歴が見つかりません。', 'coupon_issue_not_cancellable' => '未使用状態の発行分のみキャンセルできます。', 'country_not_shippable' => '選択された配送先国には配送できない商品です。', + 'order_shipping_address_update_failed' => '配送先変更処理中にエラーが発生しました。', + 'order_option_not_confirmable' => '現在の状態では購入確定できない注文オプションです。', ]; diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/messages.php index f6fe2cf2..d7af31d2 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/messages.php @@ -440,6 +440,11 @@ return [ 'per_volume' => ':unitL当たり:fee', 'per_volume_weight' => ':unitkg当たり:fee', 'per_amount' => ':unit当たり:fee', + 'range_unit' => [ + 'quantity' => '個', + 'weight' => 'kg', + 'volume' => 'L', + ], ], 'fee_details' => [ 'base_fee' => '送料', @@ -1039,6 +1044,14 @@ return [ 'can_write_checked' => 'レビュー作成可否を確認しました。', 'can_write_check_failed' => 'レビュー作成可否の確認に失敗しました。', 'cannot_write' => 'レビューを作成できません。(理由: :reason)', + // 作成不可理由ラベル — :reason 置換子に生の識別子ではなく翻訳文を載せる + 'reasons' => [ + 'order_option_not_found' => '注文内容が見つかりません', + 'not_own_order' => 'ご本人の注文ではありません', + 'not_confirmed' => '購入確定前です', + 'deadline_passed' => '作成期限が過ぎています', + 'already_written' => 'すでに作成済みのレビューです', + ], 'forbidden' => '権限がありません。', 'deleted' => 'レビューが削除されました。', 'delete_failed' => 'レビュー削除に失敗しました。', @@ -1095,6 +1108,7 @@ return [ 'board_changed' => '掲示板設定が変更されたため、該当する投稿が見つかりません。管理者にお問い合わせください。', 'reply_created' => '返信が登録されました。', 'reply_failed' => '返信登録に失敗しました。', + 'reply_already_exists' => 'すでに登録された回答があります。既存の回答を修正または削除してから再度登録してください。', 'reply_updated' => '返信が編集されました。', 'reply_update_failed' => '回答の編集に失敗しました。', 'reply_deleted' => '回答が削除されました。', diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/validation.php b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/validation.php index f855125a..741512fa 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/validation.php +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/backend/ja/validation.php @@ -787,6 +787,7 @@ return [ ], ], 'order' => [ + 'payment_method_unavailable' => '現在ご利用いただけない決済手段です。他の決済手段を選択してください。', 'ids' => [ 'required' => '変更する注文を選択してください。', 'array' => '注文IDは配列形式である必要があります。', @@ -935,6 +936,14 @@ return [ 'tier_min_non_negative' => '区間開始値は0以上である必要があります。', 'tier_max_non_negative' => '区間終了値は0以上である必要があります。', 'unit_value_min' => '区間単位値は0より大きくする必要があります。', + 'tiers_required' => '区間別送料ポリシーは区間を1つ以上登録する必要があります。', + 'middle_max_required' => '最後の区間を除く区間には終了値を入力する必要があります。', + 'tier_value_integer' => '数量区間の開始値と終了値は整数である必要があります。', + 'unit_value_required' => '単位当たり送料ポリシーは単位値を入力する必要があります。', + ], + 'free_threshold_required' => '条件付き送料無料ポリシーは送料無料基準金額を入力する必要があります。', + 'extra_fee' => [ + 'zipcode_format' => '郵便番号は「63000」「63000-63999」「63*」のいずれかの形式である必要があります。', ], 'country_settings' => [ 'country_code' => [ @@ -1200,7 +1209,13 @@ return [ 'min' => '注文金額は 0 以上である必要があります。', ], ], + // 公開アセットディスク (コアカタログ照会による検証) + 'public_asset_disk_invalid' => '有効な公開アセットディスクを選択してください。', + 'attributes' => [ + // 公開アセットストレージ + 'basic_info.public_asset_disk' => '公開アセットディスク', + 'basic_info' => '基本情報', 'basic_info.shop_name' => 'ショップ名', 'basic_info.route_path' => 'ルートパス', diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json index d756c545..60dd5aab 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/settings.json @@ -58,6 +58,13 @@ "telecom_number_placeholder": "付加通信事業者番号", "postcode_plugin_required": "Daum郵便番号プラグインがインストールされていないか無効化されています。管理者>プラグインで有効化してください。" }, + "public_asset_storage": { + "title": "公開アセットストレージ", + "description": "商品・カテゴリ・レビュー画像など、このモジュールの完全公開アセットの保存ディスクを設定します。", + "disk": "公開アセットディスク", + "follow_core": "コア設定に従う", + "help": "このモジュールだけ別のディスクを使う場合に選択します。空にするとコア環境設定の公開アセットストレージ設定に従います。" + }, "inquiry_settings": { "title": "お問い合わせ掲示板設定", "description": "商品お問い合わせ機能を設定します。", @@ -144,6 +151,7 @@ "min_order_amount": "最小注文金額", "is_active": "使用有無", "orphaned_badge": "プラグイン未インストール", + "orphaned_pg_badge": "指定PG削除済み", "remove_orphaned": "削除", "pg_provider": "PG社", "pg_use_default": "デフォルトPG", diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/shipping_policy.json b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/shipping_policy.json index 77480c97..74d49a26 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/shipping_policy.json +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/frontend/partial/admin/shipping_policy.json @@ -114,12 +114,6 @@ "free": "送料無料", "fixed": "配送料金: {{fee}}", "conditional": "{{threshold}}未満 {{fee}} / {{threshold}}以上 送料無料", - "range_prefix": { - "amount": "", - "quantity": "個", - "weight": "kg", - "volume": "cm³" - }, "api": "外部API連携(リアルタイム計算)", "per_quantity": "{{unit}}個当たり {{fee}}", "per_weight": "{{unit}}kg当たり {{fee}}", @@ -196,14 +190,17 @@ "carrier": "配送業者", "carrier_placeholder": "配送業者を選択してください", "currency_code": "基準通貨", - "currency_code_placeholder": "通貨を選択してください", "charge_policy": "配送料金課金ポリシー", "base_fee": "配送料金", "free_threshold": "送料無料基準金額", "free_threshold_hint": "この金額以上のご購入で配送料金が無料", "range_tiers": "区間別配送料金", "range_min": "開始値", + "range_min_auto": "自動", "range_max": "終了値", + "unit_quantity": "個", + "unit_weight": "kg", + "unit_volume": "L", "range_fee": "配送料金", "range_unlimited": "無制限", "range_add": "区間を追加", @@ -213,6 +210,11 @@ "range_error_min_max": "開始値は終了値より小さい必要があります。", "range_error_fee": "配送料は0以上である必要があります。", "range_error_min_one": "最小1つの区間が必要です。", + "range_error_middle_max": "最後の区間を除く区間には終了値を入力する必要があります。", + "range_error_fee_required": "区間の送料を入力してください。", + "range_error_integer": "数量区間の開始値と終了値は整数である必要があります。", + "range_error_unit_value_required": "単位あたり送料ポリシーは単位値を入力する必要があります。", + "range_error_free_threshold_required": "条件付き送料無料ポリシーは送料無料基準金額を入力する必要があります。", "per_unit_value": "単位基準値", "per_unit_hint": "設定した単位ごとに上記の配送料が課金されます", "api_endpoint": "APIエンドポイント", @@ -225,8 +227,8 @@ "api_response_fee_field_placeholder": "shipping_fee", "api_response_fee_field_hint": "API応答から配送料の値が含まれるフィールド名", "extra_fee_enabled": "離島追加配送料の使用", - "extra_fee_multiply": "建当たり重複課金", - "extra_fee_multiply_hint": "単位ごとの配送料適用時に離島追加配送料を建当たり重複課金します", + "extra_fee_multiply": "配送単位当たり重複課金", + "extra_fee_multiply_hint": "単位ごとの配送料適用時に、離島追加配送料を基本送料と同じ配送単位数だけ重複課金します", "extra_fee_zipcode": "郵便番号", "extra_fee_zipcode_placeholder": "63000または63000-63644", "extra_fee_amount": "追加配送料", diff --git a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json index 2aa61bdb..547798e5 100644 --- a/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json +++ b/lang-packs/_bundled/g7-module-sirsoft-ecommerce-ja/language-pack.json @@ -1,30 +1,30 @@ { - "identifier": "g7-module-sirsoft-ecommerce-ja", - "namespace": "g7", - "vendor": "sirsoft", - "name": { - "ko": "G7 모듈 (sirsoft-ecommerce) 일본어 언어팩", - "en": "G7 module (sirsoft-ecommerce) Japanese language pack", - "ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック" - }, - "description": { - "ko": "G7 모듈 (sirsoft-ecommerce) 일본어 언어팩 (번들)", - "en": "G7 module (sirsoft-ecommerce) Japanese language pack (bundled)", - "ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック(バンドル)" - }, - "version": "1.1.0", - "license": "MIT", - "scope": "module", - "target_identifier": "sirsoft-ecommerce", - "locale": "ja", - "locale_name": "Japanese", - "locale_native_name": "日本語", - "text_direction": "ltr", - "g7_version": ">=7.0.0", - "requires": { - "target_version": null, - "depends_on_core_locale": true - }, - "github_url": "", - "github_changelog_url": "" + "identifier": "g7-module-sirsoft-ecommerce-ja", + "namespace": "g7", + "vendor": "sirsoft", + "name": { + "ko": "G7 모듈 (sirsoft-ecommerce) 일본어 언어팩", + "en": "G7 module (sirsoft-ecommerce) Japanese language pack", + "ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック" + }, + "description": { + "ko": "G7 모듈 (sirsoft-ecommerce) 일본어 언어팩 (번들)", + "en": "G7 module (sirsoft-ecommerce) Japanese language pack (bundled)", + "ja": "G7 モジュール (sirsoft-ecommerce) 日本語 言語パック(バンドル)" + }, + "version": "1.1.1", + "license": "MIT", + "scope": "module", + "target_identifier": "sirsoft-ecommerce", + "locale": "ja", + "locale_name": "Japanese", + "locale_native_name": "日本語", + "text_direction": "ltr", + "g7_version": ">=7.0.0", + "requires": { + "target_version": null, + "depends_on_core_locale": true + }, + "github_url": "", + "github_changelog_url": "" } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/CHANGELOG.md index 6145af21..368a5374 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/CHANGELOG.md @@ -4,6 +4,17 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.1] - 2026-08-19 + +### Added + +- 일괄 삭제 부분 실패 안내(`messages.uploads.bulk_partially_deleted`)와 참조 판정 불완전 시 정리 건너뜀 안내(`messages.cleanup.sources_incomplete`), 이미지 삭제 실패 토스트(`admin.uploads.delete.failed`)의 일본어 번역을 추가했습니다. +- 에디터 설정의 "공개 자산 디스크" 설정 문구 일본어 번역을 추가했습니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 「에디터 업로드 이미지」 관리 화면의 문구 일본어 번역을 추가했습니다 (`admin.uploads.*`) — 목록 제목·열 이름·참조 상태 배지·필터·삭제 확인 창과 판정 범위 안내가 일본어 로케일에서 표시됩니다. +- 미사용 이미지 자동 정리 설정의 라벨·도움말 일본어 번역을 추가했습니다 (`settings.section_cleanup`, `settings.cleanup.*`). (#115 @Tuwasduliebst 님께서 건의해주셨습니다.) +- 업로드 이미지 삭제 시의 안내·오류 메시지 일본어 번역을 추가했습니다 (`messages.uploads.*`, `messages.cleanup.*`). +- 업로드 이미지 조회·삭제 권한의 이름과 설명 일본어 번역을 추가했습니다 — 역할 권한 화면에서 일본어 로케일로 표시됩니다. + ## [1.0.0] - 2026-07-01 ### Added diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/backend/ja/messages.php index 8926889d..e837b69b 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/backend/ja/messages.php @@ -13,4 +13,17 @@ return [ 'forbidden' => '画像アップロード権限がありません。', 'failed' => '画像アップロードに失敗しました。', ], + 'uploads' => [ + 'not_found' => 'アップロード画像が見つかりません。', + 'file_delete_failed' => '画像ファイルの削除に失敗しました。しばらく後に再度お試しください。', + 'ids_required' => '削除する画像を選択してください。', + 'deleted' => '画像を削除しました。', + 'bulk_deleted' => '選択した画像 :deleted件を削除しました。', + 'bulk_partially_deleted' => '選択した画像のうち :deleted件を削除し、:failed件は削除できませんでした。失敗した項目はリストに残っています。', + 'ids_invalid' => '選択した画像の中に存在しないアイテムがあります。リストをリフレッシュした後、再度選択してください。', + ], + 'cleanup' => [ + 'retention_disabled' => '保存期間が1日未満に設定されているため、クリーンアップを実行しませんでした。', + 'sources_incomplete' => '無効状態のモジュールがあり参照判定が不完全になる可能性があるため、未参照画像のクリーンアップをスキップしました。モジュールを有効化または削除してから再実行してください。', + ], ]; diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/frontend/ja.json b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/frontend/ja.json index f07e97cb..d073d1d7 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/frontend/ja.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/frontend/ja.json @@ -1,7 +1,8 @@ { "editor": { "data_source": { - "settings": "エディター設定" + "settings": "エディター設定", + "ckeditor5Uploads": "アップロード画像一覧" } }, "settings": { @@ -29,6 +30,11 @@ "label": "最大画像サイズ (MB)", "hint": "アップロード可能な画像の最大サイズです。(1 〜 10 MB)" }, + "public_asset_disk": { + "label": "公開アセットディスク", + "hint": "このプラグインだけ別のディスクを使う場合に選択します。空にするとコアの公開アセットディスク設定に従います。", + "follow_core": "コア設定に従う" + }, "editor_height": { "label": "エディター高さ (px)", "hint": "エディターの最小高さをピクセル単位で設定します。(200 〜 2000 px)" @@ -41,6 +47,61 @@ "step3": "画像アップロードを有効化するとエディターで画像を直接貼り付けたりアップロードできます。アップロードされた画像はサーバーに保存されます。", "step4": "設定変更後に保存すると即座にすべてのエディターに反映されます。", "step5": "プラグインを無効化するとエディターは基本 HtmlEditor に、コンテンツ表示は基本 HtmlContent に自動復元されます。" + }, + "section_cleanup": "未使用画像の整理", + "cleanup": { + "toggle_label": "未使用画像の自動整理", + "toggle_hint": "デフォルトはオフです。有効にするには運営者が手動で有効にする必要があります。有効にすると、毎日1回、保存期間を過ぎた未使用画像のファイルと記録を一緒に削除します。", + "retention_label": "保存期間(日)", + "retention_hint": "アップロード後、この期間を過ぎた未使用画像のみが整理対象になります。(1~3650日)", + "manual_hint": "今何が削除されるか事前に確認したい場合は、サーバーで次のコマンドを実行してください:php artisan sirsoft-ckeditor5:prune-unused-images --dry-run", + "open_uploads": "アップロード画像管理を開く" + } + }, + "admin": { + "uploads": { + "title": "エディターアップロード画像", + "description": "エディターでアップロードされた画像を確認し、使用されなくなった画像を整理します。", + "refresh": "更新", + "open_settings": "整理設定を開く", + "reference_notice": "参照状態は投稿・ページ・商品説明などの登録されたコンテンツを確認して判定します。参照ソースを登録していない外部拡張のコンテンツのみで使用されている画像は「未参照」と表示される場合があるため、削除前に確認してください。", + "scan_limited_notice": "参照状態フィルターは最近{{count}}件を基準として判定します。それより古い画像はこのリストに表示されません。", + "empty": "アップロードされた画像がありません。", + "col": { + "thumbnail": "プレビュー", + "original_name": "元のファイル名", + "file_size": "サイズ", + "uploader": "アップローダー", + "created_at": "アップロード日時", + "referenced": "参照状態" + }, + "badge": { + "referenced": "参照済み", + "unreferenced": "未参照" + }, + "filter": { + "search_placeholder": "元のファイル名またはハッシュで検索", + "search_button": "検索", + "referenced_all": "すべての状態", + "referenced_only": "参照済みのみ", + "unreferenced_only": "未参照のみ", + "date_from": "開始日", + "date_to": "終了日", + "clear": "フィルターをリセット" + }, + "delete": { + "button": "削除", + "confirm_title": "画像を削除しますか?", + "confirm_message": "選択した画像のファイルと記録が一緒に削除されます。この操作は取り消せません。", + "success": "画像を削除しました。", + "failed": "画像の削除に失敗しました。" + }, + "bulk_delete": { + "button": "選択削除", + "selected": "{{count}}件選択されました", + "confirm_title": "選択した画像を削除しますか?", + "confirm_message": "選択した{{count}}件のファイルと記録が一緒に削除されます。この操作は取り消せません。" + } } } } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/language-pack.json b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/language-pack.json index 11e85335..be35b069 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/language-pack.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 plugin (sirsoft-ckeditor5) Japanese language pack (bundled)", "ja": "G7 プラグイン (sirsoft-ckeditor5) 日本語 言語パック(バンドル)" }, - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "scope": "plugin", "target_identifier": "sirsoft-ckeditor5", diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/seed/permissions.json b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/seed/permissions.json new file mode 100644 index 00000000..86168eb6 --- /dev/null +++ b/lang-packs/_bundled/g7-plugin-sirsoft-ckeditor5-ja/seed/permissions.json @@ -0,0 +1,18 @@ +{ + "sirsoft-ckeditor5": { + "name": "CKEditor 5 WYSIWYG エディター", + "description": "CKEditor5 プラグインが提供する権限" + }, + "sirsoft-ckeditor5.uploads": { + "name": "エディター アップロード画像", + "description": "エディターにアップロードされた画像の閲覧・削除権限" + }, + "sirsoft-ckeditor5.uploads.read": { + "name": "アップロード画像の閲覧", + "description": "エディター アップロード画像一覧・参照状態の閲覧" + }, + "sirsoft-ckeditor5.uploads.delete": { + "name": "アップロード画像の削除", + "description": "エディター アップロード画像のファイル・記録の削除" + } +} diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/CHANGELOG.md index 5a79c78b..a4a30e77 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/CHANGELOG.md @@ -4,6 +4,13 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Added + +- 에스크로 배송등록·구매거절확인 검증 안내 문구(운송장번호·택배사·기본 확인자명)의 일본어 번역을 추가했습니다. +- 결제 처리 실패 안내 문구의 일본어 번역을 추가했습니다 — 일본어 로케일에서 결제가 실패했을 때 한국어 안내가 보이던 문제가 해소됩니다. + ## [1.1.0] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/backend/ja/messages.php index 4fb37e19..6714f28e 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/backend/ja/messages.php @@ -5,6 +5,7 @@ return [ 'cash_receipt_issue_failed' => '現金領収書の発行に失敗しました。', 'order_not_found' => '注文が見つかりません。', 'cbt_failed' => '海外決済の処理に失敗しました。', + 'payment_failed' => '決済処理に失敗しました。しばらく後にもう一度お試しください。', ], 'refund' => [ 'missing_tid' => 'トランザクション ID(TID)がないため、返金を進めることができません。', @@ -39,6 +40,11 @@ return [ 'cash_receipt' => [ 'provider_name' => 'KGイニシス', ], + 'escrow' => [ + 'invoice_required' => '送り状番号を入力してください。', + 'courier_required' => '配送業者を選択してください。', + 'default_confirmer' => '管理者', + ], 'cbt_cvs' => [ 'simulate_success' => '入金シミュレーションが完了しました。', 'simulate_failed' => '入金シミュレーションに失敗しました。', diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/language-pack.json b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/language-pack.json index 68ae76d0..b01d3975 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/language-pack.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_kginicis-ja/language-pack.json @@ -1,30 +1,30 @@ { - "identifier": "g7-plugin-sirsoft-pay_kginicis-ja", - "namespace": "g7", - "vendor": "sirsoft", - "name": { - "ko": "G7 플러그인 (sirsoft-pay_kginicis) 일본어 언어팩", - "en": "G7 plugin (sirsoft-pay_kginicis) Japanese language pack", - "ja": "G7 プラグイン (sirsoft-pay_kginicis) 日本語 言語パック" - }, - "description": { - "ko": "G7 플러그인 (sirsoft-pay_kginicis) 일본어 언어팩 (번들)", - "en": "G7 plugin (sirsoft-pay_kginicis) Japanese language pack (bundled)", - "ja": "G7 プラグイン (sirsoft-pay_kginicis) 日本語 言語パック(バンドル)" - }, - "version": "1.1.0", - "license": "MIT", - "scope": "plugin", - "target_identifier": "sirsoft-pay_kginicis", - "locale": "ja", - "locale_name": "Japanese", - "locale_native_name": "日本語", - "text_direction": "ltr", - "g7_version": ">=7.0.0", - "requires": { - "target_version": null, - "depends_on_core_locale": true - }, - "github_url": "", - "github_changelog_url": "" + "identifier": "g7-plugin-sirsoft-pay_kginicis-ja", + "namespace": "g7", + "vendor": "sirsoft", + "name": { + "ko": "G7 플러그인 (sirsoft-pay_kginicis) 일본어 언어팩", + "en": "G7 plugin (sirsoft-pay_kginicis) Japanese language pack", + "ja": "G7 プラグイン (sirsoft-pay_kginicis) 日本語 言語パック" + }, + "description": { + "ko": "G7 플러그인 (sirsoft-pay_kginicis) 일본어 언어팩 (번들)", + "en": "G7 plugin (sirsoft-pay_kginicis) Japanese language pack (bundled)", + "ja": "G7 プラグイン (sirsoft-pay_kginicis) 日本語 言語パック(バンドル)" + }, + "version": "1.1.1", + "license": "MIT", + "scope": "plugin", + "target_identifier": "sirsoft-pay_kginicis", + "locale": "ja", + "locale_name": "Japanese", + "locale_native_name": "日本語", + "text_direction": "ltr", + "g7_version": ">=7.0.0", + "requires": { + "target_version": null, + "depends_on_core_locale": true + }, + "github_url": "", + "github_changelog_url": "" } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/CHANGELOG.md index 782858b9..7330b6a0 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/CHANGELOG.md @@ -4,6 +4,16 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.3] - 2026-08-19 + +### Added + +- 에스크로 배송등록 검증 안내 문구(운송장번호·택배사)의 일본어 번역을 추가했습니다. +- 결제 처리 실패 안내 문구의 일본어 번역을 추가했습니다 — 일본어 로케일에서 결제가 실패했을 때 한국어 안내가 보이던 문제가 해소됩니다. +- 결제 취소에 필요한 서버 IP 등록 안내의 일본어 번역을 추가했습니다 — 등록이 필요한 이유, 상점 관리자에서의 등록 경로, 등록할 IP 확인·복사 안내가 일본어로 표시됩니다. +- 관리자 결제 상세의 결제 상태·취소 금액·취소 일시·환불 상태·환불 번호 항목명과, 취소 완료·부분 취소 상태 표시의 일본어 번역을 추가했습니다. +- 가상계좌 입금 통보 주소의 등록 경로 안내와 간편결제 표시 설정 항목명의 일본어 번역을 추가했습니다. + ## [1.0.2] - 2026-08-10 ### Changed diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/backend/ja/messages.php index e520bb91..ac6bec25 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/backend/ja/messages.php @@ -6,7 +6,12 @@ return [ 'default_reason' => '購入者返金リクエスト', 'in_progress' => 'NHN KCP 返金はすでに処理中です。', ], + 'escrow' => [ + 'invoice_required' => '送り状番号を入力してください。', + 'courier_required' => '配送業者を選択してください。', + ], 'errors' => [ + 'payment_failed' => '決済処理に失敗しました。しばらく後にもう一度お試しください。', 'wsdl_missing' => 'KCP WSDL ファイルがありません: :file', 'approval_key_error' => 'KCP 承認キーエラー [:code]: :message', 'soap_error' => 'KCP SOAP 連携エラー: :message', diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/frontend/ja.json b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/frontend/ja.json index 80e17850..7e1e38c4 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/frontend/ja.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/frontend/ja.json @@ -9,7 +9,8 @@ "nhnkcp_test_map": "テスト決済マップ", "nicepay_test_map": "テスト決済マップ", "settings": "決済設定", - "vbank_info": "仮想口座情報" + "vbank_info": "仮想口座情報", + "nhnkcp_easy_pay_display_map": "簡易決済表示マップ" } }, "messages": { @@ -78,7 +79,14 @@ "escrow_delivery_change_button": "配送情報編集", "escrow_delivery_registering": "登録中...", "escrow_delivery_success": "配送情報がKCPに登録されました。", - "escrow_delivery_error": "配送登録に失敗しました。" + "escrow_delivery_error": "配送登録に失敗しました。", + "pay_status_cancel_completed": "キャンセル完了", + "pay_status_partial_cancelled": "部分キャンセル", + "result_payment_status": "現在の決済ステータス", + "result_cancelled_amount": "キャンセル金額", + "result_cancelled_at": "キャンセル日時", + "result_refund_status": "返金ステータス", + "result_refund_number": "返金番号" }, "settings": { "title": "NHN KCP設定", @@ -133,7 +141,16 @@ "escrow_test_site_cd_hint": "KCPエスクロー テスト用site_cd(デフォルト:T0007)", "section_escrow_notify": "エスクロー共通通知URL", "escrow_common_notify_hint": "NHN KCP加盟店管理画面でこのURLを共通通知URLとして登録する必要があり、購入確認·キャンセルが自動的に処理されます。", - "escrow_common_notify_copied": "URLがクリップボードにコピーされました。" + "escrow_common_notify_copied": "URLがクリップボードにコピーされました。", + "cancel_server_ip_title": "決済キャンセルサーバーIP登録が必要です", + "cancel_server_ip_body": "NHN KCPは取引キャンセルのセキュリティのため、ショッピングモールサーバーの外部送信IPが店舗管理者に登録された場合にのみ、ショッピングモール管理者から実決済のキャンセルを許可します。", + "cancel_server_ip_path": "NHN KCP店舗管理者 > 技術管理センター > セキュリティ管理 > サーバーIP設定(決済)", + "cancel_server_ip_effect": "IPが登録されていないか、サーバーIPが変更された場合、この管理者画面で決済キャンセルが失敗する可能性があり、この場合KCP店舗管理者から直接キャンセルする必要があります。", + "cancel_server_ip_value_label": "登録するサーバーIP", + "cancel_server_ip_unavailable": "サーバーIP確認が必要です", + "cancel_server_ip_copied": "サーバーIPがクリップボードにコピーされました。", + "cancel_server_ip_link": "NHN KCP店舗管理者を開く", + "vbank_notify_path": "登録パス: NHN KCP店舗管理者 > 技術管理センター > ウェブフック(Webhook)管理 > ウェブフックURL設定" }, "easy_pay": { "section_title": "簡便決済" diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/language-pack.json b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/language-pack.json index 8dff9e27..1917e860 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/language-pack.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nhnkcp-ja/language-pack.json @@ -1,30 +1,30 @@ { - "identifier": "g7-plugin-sirsoft-pay_nhnkcp-ja", - "namespace": "g7", - "vendor": "sirsoft", - "name": { - "ko": "G7 플러그인 (sirsoft-pay_nhnkcp) 일본어 언어팩", - "en": "G7 plugin (sirsoft-pay_nhnkcp) Japanese language pack", - "ja": "G7 プラグイン (sirsoft-pay_nhnkcp) 日本語 言語パック" - }, - "description": { - "ko": "G7 플러그인 (sirsoft-pay_nhnkcp) 일본어 언어팩 (번들)", - "en": "G7 plugin (sirsoft-pay_nhnkcp) Japanese language pack (bundled)", - "ja": "G7 プラグイン (sirsoft-pay_nhnkcp) 日本語 言語パック(バンドル)" - }, - "version": "1.0.2", - "license": "MIT", - "scope": "plugin", - "target_identifier": "sirsoft-pay_nhnkcp", - "locale": "ja", - "locale_name": "Japanese", - "locale_native_name": "日本語", - "text_direction": "ltr", - "g7_version": ">=7.0.0-beta.4", - "requires": { - "target_version": null, - "depends_on_core_locale": true - }, - "github_url": "", - "github_changelog_url": "" + "identifier": "g7-plugin-sirsoft-pay_nhnkcp-ja", + "namespace": "g7", + "vendor": "sirsoft", + "name": { + "ko": "G7 플러그인 (sirsoft-pay_nhnkcp) 일본어 언어팩", + "en": "G7 plugin (sirsoft-pay_nhnkcp) Japanese language pack", + "ja": "G7 プラグイン (sirsoft-pay_nhnkcp) 日本語 言語パック" + }, + "description": { + "ko": "G7 플러그인 (sirsoft-pay_nhnkcp) 일본어 언어팩 (번들)", + "en": "G7 plugin (sirsoft-pay_nhnkcp) Japanese language pack (bundled)", + "ja": "G7 プラグイン (sirsoft-pay_nhnkcp) 日本語 言語パック(バンドル)" + }, + "version": "1.0.3", + "license": "MIT", + "scope": "plugin", + "target_identifier": "sirsoft-pay_nhnkcp", + "locale": "ja", + "locale_name": "Japanese", + "locale_native_name": "日本語", + "text_direction": "ltr", + "g7_version": ">=7.0.0-beta.4", + "requires": { + "target_version": null, + "depends_on_core_locale": true + }, + "github_url": "", + "github_changelog_url": "" } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/CHANGELOG.md index 5faaef6a..5833ca02 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/CHANGELOG.md @@ -4,6 +4,16 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.3] - 2026-08-19 + +### Added + +- 가상계좌 환불 입력 필드 표시명(거래 ID·주문번호·취소 금액·취소 사유·환불 계좌번호·은행코드·예금주)의 일본어 번역을 추가했습니다. + +### Removed + +- 원본 플러그인에서 삭제된 가상계좌 환불 필수값 안내 문구(`errors.vbank_refund_required_fields`)를 함께 정리했습니다 — 개별 필드 검증 메시지로 대체되었습니다. + ## [1.0.2] - 2026-08-10 ### Changed diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/backend/ja/messages.php index 5a1a876b..3a4148b8 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/backend/ja/messages.php @@ -6,7 +6,6 @@ return [ 'order_not_found' => '注文が見つかりません。', 'invalid_request' => '無効なリクエストです。', 'invalid_amount' => 'リクエスト金額が有効ではありません。', - 'vbank_refund_required_fields' => 'TID、注文番号、返金金額、返金口座情報(口座番号·銀行コード·口座名義人)をすべて入力してください。', 'vbank_completed_requires_bank_info' => '仮想口座入金完了の場合は返金口座情報が必要です。管理者APIを通じて返金を進めてください。', 'invalid_refund_amount' => '返金金額が有効ではありません。(リクエスト::amount)', 'vbank_refund_order_not_found' => '返金する注文が見つかりません。', @@ -21,4 +20,13 @@ return [ 'defaults' => [ 'vbank_refund_msg' => '仮想口座返金', ], + 'fields' => [ + 'tid' => '取引ID(TID)', + 'moid' => '注文番号', + 'cancel_amt' => 'キャンセル金額', + 'cancel_msg' => 'キャンセル事由', + 'refund_acct_no' => '返金口座番号', + 'refund_bank_cd' => '返金銀行コード', + 'refund_acct_nm' => '返金口座名義人', + ], ]; diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/language-pack.json b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/language-pack.json index e62c7648..66bb487c 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/language-pack.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-pay_nicepayments-ja/language-pack.json @@ -1,30 +1,30 @@ { - "identifier": "g7-plugin-sirsoft-pay_nicepayments-ja", - "namespace": "g7", - "vendor": "sirsoft", - "name": { - "ko": "G7 플러그인 (sirsoft-pay_nicepayments) 일본어 언어팩", - "en": "G7 plugin (sirsoft-pay_nicepayments) Japanese language pack", - "ja": "G7 プラグイン (sirsoft-pay_nicepayments) 日本語 言語パック" - }, - "description": { - "ko": "G7 플러그인 (sirsoft-pay_nicepayments) 일본어 언어팩 (번들)", - "en": "G7 plugin (sirsoft-pay_nicepayments) Japanese language pack (bundled)", - "ja": "G7 プラグイン (sirsoft-pay_nicepayments) 日本語 言語パック(バンドル)" - }, - "version": "1.0.2", - "license": "MIT", - "scope": "plugin", - "target_identifier": "sirsoft-pay_nicepayments", - "locale": "ja", - "locale_name": "Japanese", - "locale_native_name": "日本語", - "text_direction": "ltr", - "g7_version": ">=7.0.0-beta.4", - "requires": { - "target_version": null, - "depends_on_core_locale": true - }, - "github_url": "", - "github_changelog_url": "" + "identifier": "g7-plugin-sirsoft-pay_nicepayments-ja", + "namespace": "g7", + "vendor": "sirsoft", + "name": { + "ko": "G7 플러그인 (sirsoft-pay_nicepayments) 일본어 언어팩", + "en": "G7 plugin (sirsoft-pay_nicepayments) Japanese language pack", + "ja": "G7 プラグイン (sirsoft-pay_nicepayments) 日本語 言語パック" + }, + "description": { + "ko": "G7 플러그인 (sirsoft-pay_nicepayments) 일본어 언어팩 (번들)", + "en": "G7 plugin (sirsoft-pay_nicepayments) Japanese language pack (bundled)", + "ja": "G7 プラグイン (sirsoft-pay_nicepayments) 日本語 言語パック(バンドル)" + }, + "version": "1.0.3", + "license": "MIT", + "scope": "plugin", + "target_identifier": "sirsoft-pay_nicepayments", + "locale": "ja", + "locale_name": "Japanese", + "locale_native_name": "日本語", + "text_direction": "ltr", + "g7_version": ">=7.0.0-beta.4", + "requires": { + "target_version": null, + "depends_on_core_locale": true + }, + "github_url": "", + "github_changelog_url": "" } diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/CHANGELOG.md b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/CHANGELOG.md index ff1db298..2ab65cc6 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/CHANGELOG.md @@ -4,6 +4,12 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Added + +- 결제 처리 실패 안내 문구의 일본어 번역을 추가했습니다 — 일본어 로케일에서 결제가 실패했을 때 한국어 안내가 보이던 문제가 해소됩니다. + ## [1.0.1] - 2026-08-10 ### Changed diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/backend/ja/messages.php b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/backend/ja/messages.php index 33071568..14d56527 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/backend/ja/messages.php +++ b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/backend/ja/messages.php @@ -16,4 +16,7 @@ return [ 'invalid_order_id' => '現金領収書発行識別子がToss Payments形式(英数字·ハイフン·アンダースコア 6~64文字)に一致していません。', 'cancel_reason' => '注文金額変更に伴う再発行', ], + 'errors' => [ + 'payment_failed' => '決済処理に失敗しました。しばらく後に再度お試しください。', + ], ]; diff --git a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/language-pack.json b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/language-pack.json index 10faf0d2..058b2364 100644 --- a/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/language-pack.json +++ b/lang-packs/_bundled/g7-plugin-sirsoft-tosspayments-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 plugin (sirsoft-tosspayments) Japanese language pack (bundled)", "ja": "G7 プラグイン (sirsoft-tosspayments) 日本語 言語パック(バンドル)" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "scope": "plugin", "target_identifier": "sirsoft-tosspayments", diff --git a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md index 4a898f0b..a957a097 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/CHANGELOG.md @@ -4,6 +4,26 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.5] - 2026-08-19 + +### Added + +- 환경설정 > 드라이버 > 파일 스토리지의 S3 신설 항목 일본어 번역 추가 (`settings.drivers.storage.s3_endpoint*`, `s3_use_path_style*`, `s3_region_desc`) — 엔드포인트 URL·Path-style 토글·리전 입력 도움말이 일본어 로케일에서 자연스럽게 표시됩니다. (#99 @lyg-kaban 님께서 제보해주셨습니다.) +- 환경설정 드라이버 탭의 "공개 자산 스토리지" 카드 문구 일본어 번역을 추가했습니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 회원 정보 수정 화면의 탈퇴 확인 창 문구 일본어 번역 추가 (`admin.users.modals.withdraw_confirm_*`) — 되돌릴 수 없는 처리라는 안내와 함께 처리되는 항목 목록이 일본어 로케일에서 표시됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) + +- 드라이버 선택란에 저장된 값이 현재 사용할 수 없는 값일 때 표시되는 안내 문구 일본어 번역을 추가했습니다. +- 환경설정 > 업로드 탭의 「고아 첨부 정리」 카드 문구 일본어 번역을 추가했습니다 (`settings.upload.orphan_cleanup*`, `orphan_retention_days*`) — 자동 정리 토글과 보존기간 입력의 라벨·도움말이 일본어 로케일에서 표시됩니다. + +### Changed + +- S3 URL 항목의 이름·설명을 공개 URL(CDN) 용도로 명확히 한 문구 개정을 반영했습니다. + +### Removed + +- 리전 목록 선택 라벨(`settings.drivers.storage.regions.*`) 5종 제거 — 리전이 자유 입력으로 바뀌어 더 이상 사용되지 않습니다. +- 검색엔진 선택 옵션 라벨(`settings.drivers.search_engine.options.*`) 제거 — 검색엔진 목록이 서버 제공 목록으로 바뀌어 더 이상 사용되지 않습니다. + ## [1.0.4] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/frontend/partial/admin.json b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/frontend/partial/admin.json index 55b11d69..231be2e9 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/frontend/partial/admin.json +++ b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/frontend/partial/admin.json @@ -208,7 +208,16 @@ "bulk_block_success": "ユーザーがブロックされました。", "bulk_withdraw_title": "ユーザー退会処理", "bulk_withdraw_confirm": "{{count}}人のユーザーを退会処理します。この操作は取り消すが難しい場合があります。", - "bulk_withdraw_success": "ユーザーが退会処理されました。" + "bulk_withdraw_success": "ユーザーが退会処理されました。", + "withdraw_confirm_title": "退会処理の確認", + "withdraw_confirm_lead": "ステータスを「退会」に保存すると、この会員は実際に退会処理されます。元に戻すことはできません。", + "withdraw_confirm_effects_title": "一緒に処理される項目", + "withdraw_confirm_effect_anonymize": "メールアドレス·名前·ニックネームが匿名化されます。", + "withdraw_confirm_effect_sessions": "この会員のすべてのログインセッションが解除されます。", + "withdraw_confirm_effect_linked": "この会員の規約同意履歴·プロフィール画像が削除され、マイレージ·本人認証記録など連携データが整理されます。", + "withdraw_confirm_submit": "退会処理", + "bulk_withdraw_partial": "{updated}名を退会処理しました。{failed}名は処理できませんでした — {reason}", + "bulk_withdraw_all_failed": "退会処理できませんでした — {reason}" }, "form": { "page_title_create": "ユーザー登録", @@ -1383,7 +1392,13 @@ "image_max_width": "最大幅", "image_max_height": "最大高さ", "image_quality": "画像品質", - "image_quality_hint": "1〜100の値を入力してください。値が大きいほど品質が良くなります。" + "image_quality_hint": "1〜100の値を入力してください。値が大きいほど品質が良くなります。", + "orphan_cleanup": "孤立した添付ファイルのクリーンアップ", + "orphan_cleanup_desc": "フォームを保存せずに離脱して、所有者がいなくなった添付ファイルをクリーンアップします。", + "orphan_cleanup_enabled": "孤立した添付ファイルの自動クリーンアップ", + "orphan_cleanup_enabled_hint": "デフォルトはオフです — 管理者が直接有効化する必要があります。有効化すると、保持期間を過ぎた孤立した添付ファイルのファイルと記録を毎日削除します。サイトロゴなど現在使用中の添付ファイルは削除しません。", + "orphan_retention_days": "孤立した添付ファイルの保持期間(日数)", + "orphan_retention_days_hint": "アップロード後この期間が経過した孤立した添付ファイルのみがクリーンアップの対象になります。フォーム作成中の中断を考慮して、余裕を持って設定してください。(1~3650日)" }, "seo": { "meta_settings": "メタタグ設定", @@ -1619,7 +1634,9 @@ }, "drivers": { "common": { - "optional": "オプション" + "optional": "オプション", + "unavailable_saved_value_prefix": "保存された値 ", + "unavailable_saved_value_suffix": " は現在使用できません。利用可能な値を選択して保存してください。" }, "storage": { "title": "ファイルストレージ", @@ -1631,17 +1648,21 @@ }, "s3_bucket": "S3バケット", "s3_region": "S3リージョン", - "regions": { - "ap_northeast_2": "アジアパシフィック(ソウル)", - "ap_northeast_1": "アジアパシフィック(東京)", - "us_east_1": "米国東部(バージニア)", - "us_west_2": "米国西部(オレゴン)", - "eu_west_1": "ヨーロッパ(アイルランド)" - }, + "s3_region_desc": "AWSリージョンコードまたはS3互換ストレージのリージョン値を入力してください。(例: ap-northeast-2、Cloudflare R2 は auto)", "s3_access_key": "Access Key", "s3_secret_key": "Secret Key", - "s3_url": "S3 URL", - "s3_url_desc": "カスタムエンドポイントまたはCDN URLを使用する場合のみ入力してください。" + "s3_endpoint": "エンドポイントURL", + "s3_endpoint_desc": "S3互換ストレージ(Cloudflare R2、MinIOなど)のAPIアドレスです。AWS S3の場合は空欄にしてください。", + "s3_use_path_style": "Path-styleアドレスを使用", + "s3_use_path_style_desc": "MinIOなどpath-styleアドレス専用ストレージで有効にします。", + "s3_url": "S3 URL (公開URL)", + "s3_url_desc": "ファイルの公開URL生成に使うCDN/カスタムドメインです。APIリクエストアドレスはエンドポイントURLに入力してください。" + }, + "public_asset": { + "title": "公開アセットストレージ", + "desc": "完全公開アセット(商品・カテゴリ・レビュー・エディタ画像など)を直接URLで配信するディスクを設定します。使用しない場合は既存のストリーミング方式で動作します。", + "disk": "ディスク", + "s3_help": "S3を選択する場合は、上のファイルストレージカードのS3 URL(CDNドメイン)設定が必要です。未設定の場合はストリーミングで動作します。" }, "cache": { "title": "キャッシュ", @@ -1734,9 +1755,6 @@ "desc": "統合検索および管理者検索に使用する検索エンジンドライバーを設定します。", "driver": "検索エンジンドライバー", "driver_desc": "デフォルト提供のMySQL FULLTEXT(ngram)エンジンは、追加設定なしで日本語検索をサポートしています。", - "options": { - "mysql_fulltext": "MySQL FULLTEXT (ngram)" - }, "plugin_notice": "検索エンジンプラグイン(Meilisearch、Elasticsearchなど)をインストールすると、追加ドライバーが表示されます。" }, "test_connection": "接続テスト", diff --git a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/language-pack.json b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/language-pack.json index d759ed88..063f3b9c 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/language-pack.json +++ b/lang-packs/_bundled/g7-template-sirsoft-admin_basic-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 template (sirsoft-admin_basic) Japanese language pack (bundled)", "ja": "G7 テンプレート (sirsoft-admin_basic) 日本語 言語パック(バンドル)" }, - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "scope": "template", "target_identifier": "sirsoft-admin_basic", diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md index ad368308..4dadeb0c 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/CHANGELOG.md @@ -4,6 +4,12 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Added + +- 통합검색 실패 안내 문구(`search.failed.title`, `search.failed.suggestion`) 일본어 번역 추가 — 검색 중 오류가 발생했을 때 "검색 결과 없음" 과 구분되는 오류 안내가 일본어 로케일에서 자연스럽게 표시됩니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) + ## [1.1.0] - 2026-08-10 ### Added diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/search.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/search.json index c4e9132a..d99323af 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/search.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/frontend/partial/search.json @@ -38,6 +38,10 @@ "pages_in_all": "ページの検索結果がありません", "suggestion": "別の検索キーワードで試してみてください" }, + "failed": { + "title": "検索中にエラーが発生しました", + "suggestion": "しばらくしてからもう一度お試しください" + }, "page_published_at": "公開日:{{date}}", "view_more": "さらに表示", "view_all": "すべて表示", diff --git a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json index 450ac498..70dc6acc 100644 --- a/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json +++ b/lang-packs/_bundled/g7-template-sirsoft-basic-ja/language-pack.json @@ -12,7 +12,7 @@ "en": "G7 template (sirsoft-basic) Japanese language pack (bundled)", "ja": "G7 テンプレート (sirsoft-basic) 日本語 言語パック(バンドル)" }, - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "scope": "template", "target_identifier": "sirsoft-basic", diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index a876b138..87dd138b 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -3,6 +3,9 @@ return [ // User related exceptions 'cannot_delete_super_admin' => 'Super admin cannot be deleted.', + 'cannot_modify_super_admin' => 'You do not have permission to modify a super admin account or role.', + 'cannot_grant_unheld_permission' => 'You cannot grant permissions you do not hold or a broader scope than your own.', + 'cannot_modify_protected_role' => 'You do not have permission to modify a system or extension-owned role.', 'circular_reference' => 'Layout circular reference detected: :trace', 'max_depth_exceeded' => 'Layout nesting depth exceeds maximum allowed depth (:max).', diff --git a/lang/en/settings.php b/lang/en/settings.php index a16d02bf..8b3f2797 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -21,6 +21,7 @@ return [ 'backup_success' => 'Database backup started successfully.', 'backup_failed' => 'Failed to start database backup.', 'backup_error' => 'An error occurred while backing up database.', + 'database_backup_unavailable' => 'Database backup is not available yet. Use the settings backup feature to back up your settings.', 'backup_path_required' => 'Please enter a backup path.', 'restore_success' => 'Settings restored successfully.', 'restore_failed' => 'Failed to restore settings.', @@ -43,6 +44,11 @@ return [ 'local' => 'Local', 's3' => 'Amazon S3', ], + 'public_asset' => [ + 'none' => 'Disabled (streaming)', + 'public' => 'Public disk', + 's3' => 'Amazon S3', + ], 'cache' => [ 'file' => 'File', 'redis' => 'Redis', @@ -69,6 +75,9 @@ return [ 'mailgun' => 'Mailgun', 'ses' => 'SES (Amazon)', ], + 'search' => [ + 'mysql-fulltext' => 'MySQL Full-Text', + ], ], // Driver connection test messages @@ -82,6 +91,7 @@ return [ 's3_test_failed' => 'Failed to connect to S3 bucket.', 's3_missing_config' => 'S3 configuration is missing. (bucket, region, access key, secret key)', 's3_sdk_missing' => 'AWS SDK is not installed.', + 's3_adapter_missing' => 'S3 storage adapter (league/flysystem-aws-s3-v3) is not installed.', 's3_bucket_not_found' => 'S3 bucket not found.', 's3_access_denied' => 'Access to S3 bucket was denied.', 's3_invalid_credentials' => 'S3 credentials are invalid.', @@ -103,6 +113,10 @@ return [ // Websocket test messages 'websocket_test_success' => 'Successfully connected to Websocket server.', 'websocket_test_failed' => 'Failed to connect to Websocket server.', + 'websocket_server_test_failed' => 'Failed to connect to the Websocket server-side (backend broadcast) endpoint. The client endpoint is reachable.', + 'driver_unusable_s3_adapter' => 'S3 storage adapter (league/flysystem-aws-s3-v3) is not installed.', + 'driver_unusable_redis_client' => 'Neither the phpredis extension nor the predis library is available.', + 'driver_unusable_memcached_extension' => 'The memcached PHP extension is not installed.', 'websocket_invalid_host' => 'The Websocket host setting is invalid. Enter the address only — credentials (@) and protocols other than http/https are not allowed.', 'websocket_connection_refused' => 'Could not connect to Websocket server. Please check if the server is running.', diff --git a/lang/en/user.php b/lang/en/user.php index 51dbf232..8f26375a 100644 --- a/lang/en/user.php +++ b/lang/en/user.php @@ -21,11 +21,11 @@ return [ 'fetch_success' => 'User information retrieved successfully.', 'fetch_failed' => 'Failed to retrieve user information.', 'create_success' => 'User created successfully.', - 'create_failed' => 'Failed to create user: :error', + 'create_failed' => 'Failed to create user. Please try again later.', 'update_success' => 'User information updated successfully.', - 'update_failed' => 'Failed to update user information: :error', + 'update_failed' => 'Failed to update user information. Please try again later.', 'delete_success' => 'User deleted successfully.', - 'delete_failed' => 'Failed to delete user: :error', + 'delete_failed' => 'Failed to delete user. Please try again later.', 'statistics_success' => 'User statistics retrieved successfully.', 'statistics_failed' => 'Failed to retrieve user statistics.', 'search_success' => 'User search completed successfully.', @@ -69,8 +69,9 @@ return [ // Withdraw messages 'withdraw_success' => 'Account withdrawal completed successfully.', - 'withdraw_failed' => 'Failed to withdraw account: :error', + 'withdraw_failed' => 'Failed to withdraw account. Please try again later.', 'withdraw_admin_forbidden' => 'Administrator account cannot be withdrawn.', + 'withdraw_failed_unknown' => 'Withdrawal failed due to an unknown error.', // Validation messages 'validation' => [ diff --git a/lang/en/validation.php b/lang/en/validation.php index 2a851261..7305d75d 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -211,6 +211,8 @@ return [ 'invalid_json' => 'Invalid JSON format.', 'must_be_array' => 'Layout data must be an array.', + 'dangerous_expression' => 'The layout contains a disallowed expression: :snippet', + 'external_resource_url' => 'External resource URLs are not allowed (same-origin paths only): :url', 'required_field_missing' => "Required field ':field' is missing.", 'version_must_be_string' => 'The version field must be a string.', 'layout_name_must_be_string' => 'The layout_name field must be a string.', @@ -513,6 +515,7 @@ return [ // Schedule command (shell/Artisan) validation messages — these run on the server 'schedule_command' => [ 'shell_not_allowed' => 'This shell command is not allowed. Only executables registered on the server may be used, and special characters such as pipes (|) or semicolons (;) are not permitted.', + 'shell_interpreter_denied' => 'You cannot run inline commands/code through an interpreter or point it at an unsafe path. After an interpreter, specify only an absolute-path script file.', 'artisan_denied' => 'This Artisan command cannot be run as a schedule for security reasons.', 'artisan_not_allowlisted' => 'This Artisan command is not allowed to run on a schedule. Only maintenance commands such as cache clearing and queue processing can be registered.', 'artisan_malformed' => 'The Artisan command format is invalid. Quotes, backslashes, and short options (-v) are not allowed; use "command --option[=value]" only.', @@ -825,9 +828,13 @@ return [ // Setting value validation messages 'setting' => [ 'value' => [ - 'required' => 'Setting value is required.', - 'string' => 'Setting value must be a string.', + 'present' => 'The value field must be present in the request.', + 'boolean' => 'This setting only accepts an on/off value.', + 'integer' => 'This setting only accepts an integer.', + 'numeric' => 'This setting only accepts a number.', + 'type' => 'The setting value has an unsupported format.', 'max' => 'Setting value may not be greater than :max characters.', + 'array_max' => 'Setting value may not be larger than :max characters.', ], ], @@ -954,6 +961,10 @@ return [ 'image_quality_integer' => 'Image quality must be an integer.', 'image_quality_min' => 'Image quality must be at least 1.', 'image_quality_max' => 'Image quality may not be greater than 100.', + 'orphan_cleanup_enabled_boolean' => 'Orphan attachment cleanup must be enabled or disabled.', + 'orphan_retention_days_integer' => 'Orphan attachment retention period must be an integer.', + 'orphan_retention_days_min' => 'Orphan attachment retention period must be at least 1 day.', + 'orphan_retention_days_max' => 'Orphan attachment retention period may not be greater than 3650 days.', // SEO settings 'meta_title_suffix_max' => 'Meta title suffix may not be greater than 100 characters.', @@ -1011,15 +1022,19 @@ return [ 'storage_driver_invalid' => 'Please select a valid storage driver.', 's3_bucket_required' => 'S3 bucket name is required.', 's3_bucket_max' => 'S3 bucket name may not be greater than 255 characters.', - 's3_region_required' => 'Please select an S3 region.', - 's3_region_invalid' => 'Please select a valid S3 region.', + 's3_region_invalid' => 'S3 region may only contain lowercase letters, numbers, and hyphens. (e.g. ap-northeast-2, or auto for Cloudflare R2)', + 's3_region_max' => 'S3 region may not be greater than 64 characters.', 's3_access_key_required' => 'S3 access key is required.', 's3_access_key_max' => 'S3 access key may not be greater than 255 characters.', 's3_secret_key_required' => 'S3 secret key is required.', 's3_secret_key_max' => 'S3 secret key may not be greater than 255 characters.', 's3_url_url' => 'S3 URL must be a valid URL.', 's3_url_invalid' => 'S3 URL is not a valid URL format.', - 's3_url_max' => 'S3 URL may not be greater than 255 characters.', + 's3_url_max' => 'S3 URL may not be greater than 500 characters.', + 's3_endpoint_invalid' => 'S3 endpoint is not a valid URL format.', + 's3_endpoint_max' => 'S3 endpoint may not be greater than 500 characters.', + 's3_use_path_style_boolean' => 'Path-style addressing must be true or false.', + 'driver_unusable' => "The ':driver' driver cannot be used on this server. :reason", // Driver settings - Cache 'cache_driver_required' => 'Please select a cache driver.', @@ -1084,6 +1099,9 @@ return [ 'log_days_min' => 'Log retention days must be at least 1.', 'log_days_max' => 'Log retention days may not be greater than 365.', + // Public asset disk settings + 'public_asset_disk_invalid' => 'Please select a valid public asset disk.', + // Identity verification (IDV) settings 'identity_default_provider_string' => 'Default provider must be a string.', 'identity_default_provider_max' => 'Default provider identifier may not be longer than 100 characters.', @@ -1146,7 +1164,8 @@ return [ // Validation attribute names (validation.attributes) 'attributes' => [ - 'ids' => 'user ID list', + 'ids' => 'ID list', + 'user_ids' => 'user ID list', 'user_id' => 'user ID', 'status' => 'status', // Settings fields @@ -1155,7 +1174,8 @@ return [ 'site_description' => 'site description', 'admin_email' => 'admin email', 'timezone' => 'timezone', - 'language' => 'default language', + 'language' => 'language', + 'default_language' => 'default language', // Identity verification (IDV) fields 'identity_default_provider' => 'default provider', 'identity_purpose_providers' => 'purpose-to-provider mapping', @@ -1173,10 +1193,14 @@ return [ 'identity_policy_fail_mode' => 'fail mode', // Mail settings 'mailer' => 'mailer', - 'host' => 'SMTP host', - 'port' => 'SMTP port', - 'username' => 'SMTP username', - 'password' => 'SMTP password', + 'host' => 'host', + 'port' => 'port', + 'username' => 'username', + 'password' => 'password', + 'smtp_host' => 'SMTP host', + 'smtp_port' => 'SMTP port', + 'smtp_username' => 'SMTP username', + 'smtp_password' => 'SMTP password', 'encryption' => 'encryption', 'from_address' => 'sender email', 'from_name' => 'sender name', @@ -1186,6 +1210,8 @@ return [ 'image_max_width' => 'max image width', 'image_max_height' => 'max image height', 'image_quality' => 'image quality', + 'orphan_cleanup_enabled' => 'orphan attachment cleanup', + 'orphan_retention_days' => 'orphan attachment retention period', // SEO settings 'meta_title_suffix' => 'meta title suffix', 'meta_description' => 'meta description', @@ -1200,6 +1226,8 @@ return [ 's3_access_key' => 'S3 access key', 's3_secret_key' => 'S3 secret key', 's3_url' => 'S3 URL', + 's3_endpoint' => 'S3 endpoint URL', + 's3_use_path_style' => 'path-style addressing', 'cache_driver' => 'cache driver', 'redis_host' => 'Redis host', 'redis_port' => 'Redis port', @@ -1228,8 +1256,10 @@ return [ 'ses_region' => 'SES region', 'mailgun_endpoint' => 'Mailgun endpoint', // General settings (additional) - 'channels' => 'notification channels', - 'currency' => 'default currency', + 'channels' => 'channels', + 'notification_channels' => 'notification channels', + 'currency' => 'currency', + 'default_currency' => 'default currency', 'maintenance_mode' => 'maintenance mode', 'asset_url_mode' => 'asset URL mode', 'site_logo' => 'site logo', @@ -1243,7 +1273,8 @@ return [ 'twitter_default_card' => 'Twitter default card type', 'twitter_default_site' => 'Twitter default account', 'seo_page_cache_enabled' => 'SEO page cache', - 'cache_ttl' => 'SEO page cache lifetime', + 'cache_ttl' => 'cache lifetime', + 'seo_page_cache_ttl' => 'SEO page cache lifetime', 'sitemap_enabled' => 'sitemap', 'sitemap_cache_ttl' => 'sitemap cache lifetime', 'sitemap_urls_per_file' => 'sitemap URLs per file', @@ -1294,5 +1325,6 @@ return [ 'log_driver' => 'log driver', 'log_level' => 'log level', 'log_days' => 'log retention days', + 'public_asset_disk' => 'public asset disk', ], ]; diff --git a/lang/ko/exceptions.php b/lang/ko/exceptions.php index bb5bee32..ac818777 100644 --- a/lang/ko/exceptions.php +++ b/lang/ko/exceptions.php @@ -3,6 +3,9 @@ return [ // 사용자 관련 예외 'cannot_delete_super_admin' => '슈퍼 관리자는 삭제할 수 없습니다.', + 'cannot_modify_super_admin' => '슈퍼 관리자 계정 또는 역할은 수정할 권한이 없습니다.', + 'cannot_grant_unheld_permission' => '본인이 보유하지 않았거나 더 넓은 범위의 권한은 부여할 수 없습니다.', + 'cannot_modify_protected_role' => '시스템 또는 확장이 소유한 역할은 수정할 권한이 없습니다.', 'circular_reference' => '레이아웃 순환 참조 감지: :trace', 'max_depth_exceeded' => '레이아웃 중첩 깊이가 최대 허용 깊이(:max)를 초과했습니다.', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 670016ce..7480e8ed 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -21,6 +21,7 @@ return [ 'backup_success' => '데이터베이스 백업이 성공적으로 시작되었습니다.', 'backup_failed' => '데이터베이스 백업에 실패했습니다.', 'backup_error' => '데이터베이스 백업 중 오류가 발생했습니다.', + 'database_backup_unavailable' => '데이터베이스 백업 기능은 아직 제공하지 않습니다. 설정 백업은 설정 백업 기능을 이용해 주세요.', 'backup_path_required' => '백업 경로를 입력해주세요.', 'restore_success' => '설정이 성공적으로 복원되었습니다.', 'restore_failed' => '설정 복원에 실패했습니다.', @@ -43,6 +44,11 @@ return [ 'local' => '로컬', 's3' => 'Amazon S3', ], + 'public_asset' => [ + 'none' => '사용 안 함 (스트리밍)', + 'public' => 'Public 디스크', + 's3' => 'Amazon S3', + ], 'cache' => [ 'file' => '파일', 'redis' => 'Redis', @@ -69,6 +75,9 @@ return [ 'mailgun' => 'Mailgun', 'ses' => 'SES (Amazon)', ], + 'search' => [ + 'mysql-fulltext' => 'MySQL 전문검색', + ], ], // 드라이버 연결 테스트 메시지 @@ -82,6 +91,7 @@ return [ 's3_test_failed' => 'S3 버킷 연결에 실패했습니다.', 's3_missing_config' => 'S3 설정이 누락되었습니다. (버킷, 리전, 액세스 키, 시크릿 키)', 's3_sdk_missing' => 'AWS SDK가 설치되어 있지 않습니다.', + 's3_adapter_missing' => 'S3 스토리지 어댑터(league/flysystem-aws-s3-v3)가 설치되어 있지 않습니다.', 's3_bucket_not_found' => 'S3 버킷을 찾을 수 없습니다.', 's3_access_denied' => 'S3 버킷에 대한 접근이 거부되었습니다.', 's3_invalid_credentials' => 'S3 인증 정보가 올바르지 않습니다.', @@ -103,6 +113,10 @@ return [ // Websocket 테스트 메시지 'websocket_test_success' => 'Websocket 서버에 성공적으로 연결되었습니다.', 'websocket_test_failed' => 'Websocket 서버 연결에 실패했습니다.', + 'websocket_server_test_failed' => 'Websocket 서버(백엔드 발송용) endpoint 연결에 실패했습니다. 클라이언트 endpoint 는 정상입니다.', + 'driver_unusable_s3_adapter' => 'S3 스토리지 어댑터(league/flysystem-aws-s3-v3)가 설치되어 있지 않습니다.', + 'driver_unusable_redis_client' => 'phpredis 확장과 predis 라이브러리가 모두 없습니다.', + 'driver_unusable_memcached_extension' => 'memcached PHP 확장이 설치되어 있지 않습니다.', 'websocket_invalid_host' => 'Websocket 호스트 설정이 올바르지 않습니다. 호스트에는 주소만 입력하고 계정 정보(@)나 http/https 이외의 프로토콜은 사용할 수 없습니다.', 'websocket_connection_refused' => 'Websocket 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요.', diff --git a/lang/ko/user.php b/lang/ko/user.php index 3bbc06d6..fc000849 100644 --- a/lang/ko/user.php +++ b/lang/ko/user.php @@ -21,11 +21,11 @@ return [ 'fetch_success' => '사용자 정보를 성공적으로 가져왔습니다.', 'fetch_failed' => '사용자 정보를 가져오는데 실패했습니다.', 'create_success' => '사용자가 성공적으로 생성되었습니다.', - 'create_failed' => '사용자 생성에 실패했습니다: :error', + 'create_failed' => '사용자 생성에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'update_success' => '사용자 정보가 성공적으로 업데이트되었습니다.', - 'update_failed' => '사용자 정보 업데이트에 실패했습니다: :error', + 'update_failed' => '사용자 정보 업데이트에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'delete_success' => '사용자가 성공적으로 삭제되었습니다.', - 'delete_failed' => '사용자 삭제에 실패했습니다: :error', + 'delete_failed' => '사용자 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'statistics_success' => '사용자 통계를 성공적으로 가져왔습니다.', 'statistics_failed' => '사용자 통계를 가져오는데 실패했습니다.', 'search_success' => '사용자 검색을 성공적으로 완료했습니다.', @@ -69,8 +69,9 @@ return [ // 회원 탈퇴 관련 메시지 'withdraw_success' => '회원 탈퇴가 완료되었습니다.', - 'withdraw_failed' => '회원 탈퇴에 실패했습니다: :error', + 'withdraw_failed' => '회원 탈퇴에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'withdraw_admin_forbidden' => '관리자 계정은 탈퇴할 수 없습니다.', + 'withdraw_failed_unknown' => '알 수 없는 오류로 탈퇴 처리에 실패했습니다.', // 유효성 검사 메시지 'validation' => [ diff --git a/lang/ko/validation.php b/lang/ko/validation.php index 2bb31837..ce529839 100644 --- a/lang/ko/validation.php +++ b/lang/ko/validation.php @@ -210,6 +210,8 @@ return [ 'invalid_json' => '유효하지 않은 JSON 형식입니다.', 'must_be_array' => '레이아웃 데이터는 배열이어야 합니다.', + 'dangerous_expression' => '허용되지 않는 표현식이 포함되어 있습니다: :snippet', + 'external_resource_url' => '외부 리소스 URL은 허용되지 않습니다(동일 출처 경로만 허용): :url', 'required_field_missing' => "필수 필드 ':field'가 누락되었습니다.", 'version_must_be_string' => 'version 필드는 문자열이어야 합니다.', 'layout_name_must_be_string' => 'layout_name 필드는 문자열이어야 합니다.', @@ -512,6 +514,7 @@ return [ // 서버에서 실행되는 스케줄 command(쉘·Artisan) 검증 메시지 'schedule_command' => [ 'shell_not_allowed' => '허용되지 않은 쉘 명령입니다. 서버에 등록된 실행 파일만 사용할 수 있으며, 파이프(|)·세미콜론(;) 등 특수문자는 쓸 수 없습니다.', + 'shell_interpreter_denied' => '인터프리터로 인라인 명령/코드를 실행하거나 안전하지 않은 경로를 지정할 수 없습니다. 인터프리터 뒤에는 절대경로 스크립트 파일만 지정하세요.', 'artisan_denied' => '이 Artisan 명령은 보안상 스케줄로 실행할 수 없습니다.', 'artisan_not_allowlisted' => '예약 실행이 허용된 Artisan 명령이 아닙니다. 캐시 정리·큐 처리 등 유지보수 명령만 등록할 수 있습니다.', 'artisan_malformed' => 'Artisan 명령 형식이 올바르지 않습니다. 따옴표·역슬래시·단축 옵션(-v)은 쓸 수 없고 "명령명 --옵션[=값]" 형태만 등록할 수 있습니다.', @@ -826,9 +829,13 @@ return [ // 설정값 검증 메시지 'setting' => [ 'value' => [ - 'required' => '설정 값은 필수입니다.', - 'string' => '설정 값은 문자열이어야 합니다.', + 'present' => '설정 값 항목(value)이 요청에 포함되어야 합니다.', + 'boolean' => '이 설정은 사용/사용 안 함 값만 저장할 수 있습니다.', + 'integer' => '이 설정은 정수만 저장할 수 있습니다.', + 'numeric' => '이 설정은 숫자만 저장할 수 있습니다.', + 'type' => '저장할 수 없는 형식의 설정 값입니다.', 'max' => '설정 값은 :max자를 초과할 수 없습니다.', + 'array_max' => '설정 값의 크기가 :max자를 초과할 수 없습니다.', ], ], @@ -893,6 +900,10 @@ return [ 'image_quality_integer' => '이미지 품질은 정수여야 합니다.', 'image_quality_min' => '이미지 품질은 1 이상이어야 합니다.', 'image_quality_max' => '이미지 품질은 100을 초과할 수 없습니다.', + 'orphan_cleanup_enabled_boolean' => '고아 첨부 자동 정리는 사용/사용 안 함 중 하나여야 합니다.', + 'orphan_retention_days_integer' => '고아 첨부 보존기간은 정수여야 합니다.', + 'orphan_retention_days_min' => '고아 첨부 보존기간은 1일 이상이어야 합니다.', + 'orphan_retention_days_max' => '고아 첨부 보존기간은 3650일을 초과할 수 없습니다.', // SEO 설정 'meta_title_suffix_max' => '타이틀 접미사는 100자를 초과할 수 없습니다.', @@ -1006,11 +1017,16 @@ return [ 'storage_driver_required' => '스토리지 드라이버를 선택해주세요.', 'storage_driver_invalid' => '올바른 스토리지 드라이버를 선택해주세요.', 's3_bucket_max' => 'S3 버킷 이름은 255자를 초과할 수 없습니다.', - 's3_region_invalid' => '올바른 S3 리전을 선택해주세요.', + 's3_region_invalid' => 'S3 리전은 소문자 영숫자와 하이픈만 사용할 수 있습니다. (예: ap-northeast-2, Cloudflare R2 는 auto)', + 's3_region_max' => 'S3 리전은 64자를 초과할 수 없습니다.', 's3_access_key_max' => 'S3 Access Key는 255자를 초과할 수 없습니다.', 's3_secret_key_max' => 'S3 Secret Key는 255자를 초과할 수 없습니다.', 's3_url_invalid' => '올바른 S3 URL 형식이 아닙니다.', 's3_url_max' => 'S3 URL은 500자를 초과할 수 없습니다.', + 's3_endpoint_invalid' => '올바른 S3 엔드포인트 URL 형식이 아닙니다.', + 's3_endpoint_max' => 'S3 엔드포인트 URL은 500자를 초과할 수 없습니다.', + 's3_use_path_style_boolean' => 'Path-style 주소 사용 설정은 true 또는 false 값이어야 합니다.', + 'driver_unusable' => "':driver' 드라이버를 이 서버에서 사용할 수 없습니다. :reason", 'cache_driver_required' => '캐시 드라이버를 선택해주세요.', 'cache_driver_invalid' => '올바른 캐시 드라이버를 선택해주세요.', 'redis_host_max' => 'Redis 호스트는 255자를 초과할 수 없습니다.', @@ -1061,9 +1077,11 @@ return [ 'log_days_min' => '로그 보관 일수는 1 이상이어야 합니다.', 'log_days_max' => '로그 보관 일수는 365를 초과할 수 없습니다.', + // 공개 자산 디스크 설정 + 'public_asset_disk_invalid' => '올바른 공개 자산 디스크를 선택해주세요.', + // 드라이버 조건부 필수 메시지 (선택 드라이버에 따라 필수) 's3_bucket_required' => 'S3 버킷 이름은 필수입니다.', - 's3_region_required' => 'S3 리전을 선택해주세요.', 's3_access_key_required' => 'S3 Access Key는 필수입니다.', 's3_secret_key_required' => 'S3 Secret Key는 필수입니다.', 's3_url_url' => 'S3 URL은 유효한 URL이어야 합니다.', @@ -1139,7 +1157,8 @@ return [ // 검증 속성명 (validation.attributes) 'attributes' => [ - 'ids' => '사용자 ID 목록', + 'ids' => 'ID 목록', + 'user_ids' => '사용자 ID 목록', 'user_id' => '사용자 ID', 'status' => '상태', // 일반 설정 필드 @@ -1148,7 +1167,8 @@ return [ 'site_description' => '사이트 설명', 'admin_email' => '관리자 이메일', 'timezone' => '시간대', - 'language' => '기본 언어', + 'language' => '언어', + 'default_language' => '기본 언어', // 본인인증(IDV) 필드 'identity_default_provider' => '기본 프로바이더', 'identity_purpose_providers' => '목적별 프로바이더', @@ -1166,10 +1186,14 @@ return [ 'identity_policy_fail_mode' => '실패 모드', // 메일 설정 필드 'mailer' => '메일러', - 'host' => 'SMTP 호스트', - 'port' => 'SMTP 포트', - 'username' => 'SMTP 사용자명', - 'password' => 'SMTP 비밀번호', + 'host' => '호스트', + 'port' => '포트', + 'username' => '사용자명', + 'password' => '비밀번호', + 'smtp_host' => 'SMTP 호스트', + 'smtp_port' => 'SMTP 포트', + 'smtp_username' => 'SMTP 사용자명', + 'smtp_password' => 'SMTP 비밀번호', 'encryption' => '암호화', 'from_address' => '발신자 이메일', 'from_name' => '발신자 이름', @@ -1179,6 +1203,8 @@ return [ 'image_max_width' => '이미지 최대 너비', 'image_max_height' => '이미지 최대 높이', 'image_quality' => '이미지 품질', + 'orphan_cleanup_enabled' => '고아 첨부 자동 정리', + 'orphan_retention_days' => '고아 첨부 보존기간', // SEO 설정 필드 'meta_title_suffix' => '메타 타이틀 접미사', 'meta_description' => '메타 설명', @@ -1196,6 +1222,8 @@ return [ 's3_access_key' => 'S3 Access Key', 's3_secret_key' => 'S3 Secret Key', 's3_url' => 'S3 URL', + 's3_endpoint' => 'S3 엔드포인트 URL', + 's3_use_path_style' => 'Path-style 주소 사용', 'cache_driver' => '캐시 드라이버', 'redis_host' => 'Redis 호스트', 'redis_port' => 'Redis 포트', @@ -1221,8 +1249,10 @@ return [ 'ses_region' => 'SES 리전', 'mailgun_endpoint' => 'Mailgun 엔드포인트', // 일반 설정 (추가) - 'channels' => '알림 채널', - 'currency' => '기본 통화', + 'channels' => '채널', + 'notification_channels' => '알림 채널', + 'currency' => '통화', + 'default_currency' => '기본 통화', 'maintenance_mode' => '점검 모드', 'asset_url_mode' => '자산 주소 방식', 'site_logo' => '사이트 로고', @@ -1236,7 +1266,8 @@ return [ 'twitter_default_card' => '트위터 기본 카드 유형', 'twitter_default_site' => '트위터 기본 계정', 'seo_page_cache_enabled' => 'SEO 페이지 캐시 사용', - 'cache_ttl' => 'SEO 페이지 캐시 유지시간', + 'cache_ttl' => '캐시 유지시간', + 'seo_page_cache_ttl' => 'SEO 페이지 캐시 유지시간', 'sitemap_enabled' => '사이트맵 사용', 'sitemap_cache_ttl' => '사이트맵 캐시 유지시간', 'sitemap_urls_per_file' => '사이트맵 파일당 URL 수', @@ -1287,5 +1318,6 @@ return [ 'log_driver' => '로그 드라이버', 'log_level' => '로그 레벨', 'log_days' => '로그 보관 일수', + 'public_asset_disk' => '공개 자산 디스크', ], ]; diff --git a/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md index dcd51587..d593b246 100644 --- a/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md +++ b/modules/_bundled/gnuboard7-hello_module/docs/api/memos.md @@ -41,11 +41,73 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. --> +_목록 응답: `data.data` 가 항목 배열, `data.meta` 가 페이지 정보, `data.abilities` 가 컬렉션 레벨 권한 (`MemoCollection`)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| data | array | `[{...}]` | 메모 항목 배열 (각 항목은 `MemoResource` — 아래 표) | +| meta.current_page | integer | `1` | 현재 페이지 번호 | +| meta.last_page | integer | `3` | 마지막 페이지 번호 | +| meta.per_page | integer | `10` | 페이지당 항목 수 (미지정 시 기본 `10`) | +| meta.total | integer | `27` | 전체 항목 수 | +| abilities.can_create | boolean | `true` | 요청자의 `gnuboard7-hello_module.memos.create` 보유 여부 | +| abilities.can_update | boolean | `true` | 요청자의 `gnuboard7-hello_module.memos.update` 보유 여부 | +| abilities.can_delete | boolean | `true` | 요청자의 `gnuboard7-hello_module.memos.delete` 보유 여부 | + +`data.data[]` 항목 (`MemoResource`): + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 메모 기본키 | +| uuid | string(uuid) | `9f2c1b0e-…` | 외부 노출용 식별자 | +| title | string | `첫 번째 메모` | 제목 | +| content | string | `메모 본문입니다.` | 본문 내용 | +| created_at | string | `2026-08-16 01:30:00` | 생성 일시 (요청자 타임존으로 포맷) | +| updated_at | string | `2026-08-16 01:30:00` | 수정 일시 (요청자 타임존으로 포맷) | +| abilities | object | `{"can_create":true, …}` | 항목 레벨 권한 (컬렉션 `abilities` 와 같은 3종) | **응답 예시** -<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메모 목록을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "uuid": "9f2c1b0e-4d7a-4f10-9c33-1a5b6e8d2f04", + "title": "첫 번째 메모", + "content": "메모 본문입니다.", + "created_at": "2026-08-16 01:30:00", + "updated_at": "2026-08-16 01:30:00", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } + ], + "meta": { + "current_page": 1, + "last_page": 3, + "per_page": 10, + "total": 27 + }, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + +> 위 문서의 실측이 `403` 으로 관측된 것은 프로브 계정에 `gnuboard7-hello_module.memos.read` 권한이 없었기 때문이다. 권한을 갖춘 요청은 `200` 과 위 페이로드를 받는다. **에러 응답** @@ -53,11 +115,16 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`gnuboard7-hello_module.memos.read`)이 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — `page` 최소 1, `per_page` 1~100) | +| 500 | Internal Server Error | 조회 중 예외 발생 (`gnuboard7-hello_module::messages.memo.fetch_failed`, `errors.error` 에 예외 메시지) | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +메모 목록을 페이지 단위로 조회한다. 학습용 샘플 모듈의 표준 목록 엔드포인트로, 코어의 `AdminBaseController` + `BaseApiCollection` 조합을 그대로 따른다. + +`per_page` 를 지정하지 않으면 기본값 `10` 이 적용된다. 상·하한(1~100)은 `MemoListRequest` 가 검증하므로 Service 는 검증 없이 값을 그대로 쓴다 — 검증은 FormRequest 책임이라는 규칙의 예시다. ### POST /api/modules/gnuboard7-hello_module/admin/memos @@ -90,11 +157,45 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-403 — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 가 생성된 메모 하나 (`MemoResource`)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `28` | 생성된 메모의 기본키 | +| uuid | string(uuid) | `3b7e5a12-…` | 외부 노출용 식별자 (생성 시 자동 부여) | +| title | string | `예시 제목` | 제목 | +| content | string | `예시 내용입니다.` | 본문 내용 | +| created_at | string | `2026-08-16 01:30:00` | 생성 일시 (요청자 타임존으로 포맷) | +| updated_at | string | `2026-08-16 01:30:00` | 수정 일시 (생성 직후에는 `created_at` 과 같다) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 요청자의 메모 권한 3종 | **응답 예시** -<!-- 실측 제외: http-403 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 201 +``` + +```json +{ + "success": true, + "message": "메모가 생성되었습니다.", + "data": { + "id": 28, + "uuid": "3b7e5a12-8c04-4d61-9b2f-7e0a1c4d5f88", + "title": "예시 제목", + "content": "예시 내용입니다.", + "created_at": "2026-08-16 01:30:00", + "updated_at": "2026-08-16 01:30:00", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + +> 성공 상태코드는 `200` 이 아니라 **`201 Created`** 다. 위 문서의 실측이 `403` 으로 관측된 것은 프로브 계정에 `gnuboard7-hello_module.memos.create` 권한이 없었기 때문이다. **에러 응답** @@ -102,11 +203,16 @@ Content-Type: application/json | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`gnuboard7-hello_module.memos.create`)이 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — `title` 필수·255자 이하, `content` 필수) | +| 500 | Internal Server Error | 생성 중 예외 발생 (`gnuboard7-hello_module::messages.memo.create_failed`, `errors.error` 에 예외 메시지) | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +메모를 생성한다. 검증은 `StoreMemoRequest` 가 전담하고 Service 는 검증된 배열만 받는다 — Service 에 검증 로직을 두지 않는다는 규칙의 예시다. + +컨트롤러가 `$request->validated()` 를 넘기므로 FormRequest 에 정의되지 않은 필드는 모델에 도달하지 않는다. `$request->all()` / `except()` 를 쓰면 `$fillable` 을 통해 미정의 필드가 새므로 쓰지 않는다. ### DELETE /api/modules/gnuboard7-hello_module/admin/memos/{id} @@ -132,11 +238,25 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_삭제 응답에는 페이로드가 없다. 컨트롤러가 `success(메시지)` 만 호출하므로 `data` 는 `null` 이다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| (없음) | null | `null` | 삭제 성공 시 `data` 는 항상 `null`. 결과 판정은 `success` 와 상태코드로 한다 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메모가 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -144,11 +264,16 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`gnuboard7-hello_module.memos.delete`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `id` 의 메모가 없는 경우 (`gnuboard7-hello_module::messages.memo.not_found`) | +| 500 | Internal Server Error | 삭제 중 예외 발생 (`gnuboard7-hello_module::messages.memo.delete_failed`, `errors.error` 에 예외 메시지) | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +메모를 삭제한다. 컨트롤러가 먼저 `getMemo($id)` 로 대상을 조회하므로, 존재하지 않는 `id` 는 삭제 시도 전에 `404` 로 걸러진다. + +삭제는 DB CASCADE 에 의존하지 않고 Service 가 명시적으로 수행한다 — 훅 발화·파일 정리·로깅을 보장하기 위해서다. ### GET /api/modules/gnuboard7-hello_module/admin/memos/{id} @@ -174,11 +299,43 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 가 메모 하나 (`MemoResource`)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `2` | 기본 키 (내부 식별자) | +| uuid | string(uuid) | `4473e9ee-ecdf-4ad0-afb3-78ada47265af` | 외부 노출용 UUID | +| title | string | `두 번째 메모` | 제목 | +| content | string | `Memo 엔티티의 CRUD 동작을 확인할 수 있는 추가 샘플입니다.` | 본문 내용 | +| created_at | string | `2026-07-31 22:09:15` | 생성 일시 (요청자 타임존으로 포맷) | +| updated_at | string | `2026-07-31 22:09:15` | 수정 일시 (요청자 타임존으로 포맷) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 요청자의 메모 권한 3종 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메모를 조회했습니다.", + "data": { + "id": 2, + "uuid": "4473e9ee-ecdf-4ad0-afb3-78ada47265af", + "title": "두 번째 메모", + "content": "Memo 엔티티의 CRUD 동작을 확인할 수 있는 추가 샘플입니다.", + "created_at": "2026-07-31 22:09:15", + "updated_at": "2026-07-31 22:09:15", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -186,11 +343,16 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`gnuboard7-hello_module.memos.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `id` 의 메모가 없는 경우 (`gnuboard7-hello_module::messages.memo.not_found`) | +| 500 | Internal Server Error | 조회 중 예외 발생 (`gnuboard7-hello_module::messages.memo.fetch_failed`, `errors.error` 에 예외 메시지) | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +메모 단건을 조회하는 관리자 엔드포인트다. path 파라미터는 `int` 타입힌트를 받는 **기본키 `id`** 이며, 응답에 함께 실리는 `uuid` 가 아니다. + +같은 리소스의 공개 조회는 `GET /api/modules/gnuboard7-hello_module/memos/{id}` 로 별도 제공된다. 관리자 경로는 `permission:gnuboard7-hello_module.memos.read` 를 요구하는 반면 공개 경로는 `optional.sanctum` 이라 비회원도 접근한다. ### PUT /api/modules/gnuboard7-hello_module/admin/memos/{id} @@ -224,11 +386,43 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 가 수정된 메모 하나 (`MemoResource`)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `2` | 기본 키 (수정으로 바뀌지 않는다) | +| uuid | string(uuid) | `4473e9ee-ecdf-4ad0-afb3-78ada47265af` | 외부 노출용 UUID (수정으로 바뀌지 않는다) | +| title | string | `예시 제목` | 수정된 제목 | +| content | string | `예시 내용입니다.` | 수정된 본문 내용 | +| created_at | string | `2026-07-31 22:09:15` | 생성 일시 (불변) | +| updated_at | string | `2026-08-16 01:30:00` | 수정 일시 (이번 요청 시각으로 갱신) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 요청자의 메모 권한 3종 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "메모가 수정되었습니다.", + "data": { + "id": 2, + "uuid": "4473e9ee-ecdf-4ad0-afb3-78ada47265af", + "title": "예시 제목", + "content": "예시 내용입니다.", + "created_at": "2026-07-31 22:09:15", + "updated_at": "2026-08-16 01:30:00", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -236,12 +430,17 @@ Content-Type: application/json | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`gnuboard7-hello_module.memos.update`)이 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `id` 의 메모가 없는 경우 (`gnuboard7-hello_module::messages.memo.not_found`) | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지 — `title` 필수·255자 이하, `content` 필수) | +| 500 | Internal Server Error | 수정 중 예외 발생 (`gnuboard7-hello_module::messages.memo.update_failed`, `errors.error` 에 예외 메시지) | <!-- @generated:end --> -**설명** <!-- TODO: 이 엔드포인트의 용도·주의사항·예시 시나리오를 작성하세요 --> +**설명** + +메모를 수정한다. `PUT` 이므로 `title` 과 `content` 를 **모두** 보내야 한다 (`UpdateMemoRequest` 가 둘 다 필수로 검증). 일부 필드만 보내면 `422` 다. + +컨트롤러는 `getMemo($id)` 로 대상을 먼저 조회하므로 존재하지 않는 `id` 는 수정 시도 전에 `404` 로 걸러지고, Service 에는 `$request->validated()` 결과만 전달되어 FormRequest 미정의 필드가 모델에 도달하지 않는다. ### GET /api/modules/gnuboard7-hello_module/memos diff --git a/modules/_bundled/gnuboard7-hello_module/tests/ModuleTestCase.php b/modules/_bundled/gnuboard7-hello_module/tests/ModuleTestCase.php index a969f0b7..b04e4557 100644 --- a/modules/_bundled/gnuboard7-hello_module/tests/ModuleTestCase.php +++ b/modules/_bundled/gnuboard7-hello_module/tests/ModuleTestCase.php @@ -3,6 +3,7 @@ namespace Modules\Gnuboard7\HelloModule\Tests; use App\Enums\ExtensionStatus; +use App\Extension\HookManager; use App\Models\Module; use App\Models\Permission; use App\Models\Role; @@ -10,6 +11,7 @@ use App\Models\User; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Schema; +use Modules\Gnuboard7\HelloModule\Providers\HelloModuleServiceProvider; use Tests\TestCase; /** @@ -52,7 +54,7 @@ abstract class ModuleTestCase extends TestCase $this->registerModuleAutoload(); - $this->app->register(\Modules\Gnuboard7\HelloModule\Providers\HelloModuleServiceProvider::class); + $this->app->register(HelloModuleServiceProvider::class); $this->runModuleMigrationIfNeeded(); @@ -80,7 +82,7 @@ abstract class ModuleTestCase extends TestCase */ private function snapshotHookManager(): void { - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $this->hookSnapshot = [ 'hooks' => $ref->getProperty('hooks')->getValue(), 'filters' => $ref->getProperty('filters')->getValue(), @@ -97,7 +99,7 @@ abstract class ModuleTestCase extends TestCase return; } - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']); $ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']); $ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']); @@ -153,8 +155,12 @@ abstract class ModuleTestCase extends TestCase $file = $moduleBasePath.'/src/'.str_replace('\\', '/', $relativeClass).'.php'; } - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } @@ -221,12 +227,12 @@ abstract class ModuleTestCase extends TestCase 'version' => '0.1.0-beta.1', ]); - $hookManager = app(\App\Extension\HookManager::class); + $hookManager = app(HookManager::class); foreach ($module->getHookListeners() as $listenerClass) { foreach ($listenerClass::getSubscribedHooks() as $hookName => $config) { $method = is_array($config) ? ($config['method'] ?? 'handle') : 'handle'; $priority = is_array($config) ? ($config['priority'] ?? 10) : 10; - $hookManager->addAction($hookName, [new $listenerClass(), $method], $priority); + $hookManager->addAction($hookName, [new $listenerClass, $method], $priority); } } } diff --git a/modules/_bundled/sirsoft-board/CHANGELOG.md b/modules/_bundled/sirsoft-board/CHANGELOG.md index 61e275d5..854e3cad 100644 --- a/modules/_bundled/sirsoft-board/CHANGELOG.md +++ b/modules/_bundled/sirsoft-board/CHANGELOG.md @@ -4,6 +4,54 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.4] - 2026-08-19 + +### Security + +- 게시글·댓글 삭제와 게시글 수정 시 비회원 확인용 비밀번호/검증 토큰 입력에 형식 검증을 추가해 비정상 형태(배열 주입 등)의 요청을 차단합니다. +- 관리자 게시판 목록 조회의 정렬 기준과 페이지 크기를 허용된 값으로만 제한했습니다. + +- 비밀글의 내용이 열람 권한 없이 새어 나가던 경로를 모두 막았습니다. 게시글 상세 화면은 비밀글 내용을 가렸지만, 상품 문의 목록·비밀글의 댓글 목록·비밀글의 첨부파일(다운로드/미리보기)은 게시글의 비밀 여부를 확인하지 않아 주소만 알면 원문·첨부를 볼 수 있었습니다. 이제 이 경로 전부에서 작성자 본인 또는 게시판 관리 권한(비밀글 열람)을 가진 요청에만 내용을 제공하고, 그 외에는 내용·제목·답변·첨부를 가립니다. 열람 권한자의 상세 화면에 표시되는 비밀글·삭제글 첨부의 이미지 썸네일은 한시적으로만 유효한 서명 주소로 전달되어, 보호를 유지한 채 종전처럼 정상 표시됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 회원 프로필의 작성글 목록과 "내가 댓글 단 글" 활동 목록에서 다른 사람의 비밀글·블라인드 글 **본문**이 로그인 없이도 나가던 문제를 수정했습니다. 두 목록은 본문 앞부분을 함께 싣는데 그것을 가리는 설정이 실제로는 한 번도 켜지지 않았습니다. 이제 본인이 볼 때만 본문이 보이고, 다른 사람이 볼 때는 비워집니다. 글의 제목과 목록에서의 표시(비밀글·블라인드 배지)는 게시판 목록과 동일하게 그대로 유지됩니다. (KVE-2026-1914) +- 첨부파일 삭제·순서 변경에 담당 범위 제한을 적용했습니다. 회원 화면은 작성자 본인만 삭제하도록 막고 있었지만 관리 화면에는 같은 확인이 없었고, 순서 변경은 양쪽 모두 확인이 없었습니다. 순서는 목록 전체에 대한 하나의 값이라 범위 밖 대상이 하나라도 섞이면 요청 전체를 거부합니다. (KVE-2026-1919) +- 게시판을 찾을 수 없을 때 비밀글 보호가 통과되던 문제를 수정했습니다. 첨부파일 서빙과 댓글 목록은 부모 글을 찾지 못하면 검사를 건너뛰고 진행해, 게이트가 있어야 할 자리가 비어 있었습니다. 이제 부모 글을 확인할 수 없으면 차단합니다. +- 로그인 없이 열리는 목록(최근 글·인기 글)과 관리 대시보드의 요약 목록에서, 열람 권한이 없는 게시판의 글과 블라인드·비활성 게시판의 글이 노출되던 문제를 막았습니다. 이제 이 목록들은 블라인드·비활성 게시판 글을 제외하고, 열람 권한이 있는 게시판의 글만 각 요청자에게 보여 줍니다. 인기 게시판 목록과 통합 검색의 게시판 필터에서도 열람 권한이 없는 비공개 게시판이 더 이상 나타나지 않습니다. 비밀글은 기존과 동일하게 목록에 제목과 🔒 표시가 유지되며 본문만 보호됩니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 전체 게시판 목록(게시판 디렉토리)과 그 안의 게시판별 최신 글 목록에도 같은 열람 권한 기준을 적용했습니다. 위 목록들에는 제한이 적용되었지만 이 경로에는 빠져 있어, 첫 화면과 전체 게시판 화면에서 열람 권한이 없는 게시판의 이름과 글 제목이 로그인 없이도 보였습니다. 열람 권한이 있는 방문자에게는 종전과 동일하게 표시됩니다. +- 게시판 관리 대시보드(개요·글 추이·최근 글·미처리 신고)에 별도의 열람 권한을 요구하도록 했습니다. 이전에는 관리자이기만 하면 게시판 권한이 전혀 없어도 이 화면의 정보를 볼 수 있었습니다. 미처리 신고 집계는 비활성 게시판을 제외합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 통합 검색 결과의 제목·본문 미리보기에서 검색어 강조 처리가 원문을 그대로 실어 보내던 것을, 안전하게 변환한 뒤 강조하도록 바로잡았습니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 댓글 비밀번호 확인 후 받는 임시 토큰이 실제 검증에 연결되어 있지 않던 문제를 수정했습니다. 이제 이 토큰으로 댓글을 수정·삭제할 수 있으며(비밀번호 재입력 대체), 토큰은 한 번만 사용되고 만료됩니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) + +### Added + +- 답글이 달린 글의 삭제 방식을 게시판마다 선택할 수 있습니다. "함께 삭제"(기본값)는 글을 삭제하면 달려 있던 답글도 함께 휴지통으로 이동하고, 글을 복원하면 그때 함께 지워진 답글만 같이 복원됩니다. "삭제 차단"은 답글이 남아 있는 동안 원글 삭제를 막고 사유를 안내합니다. 사용자가 직접 지운 답글은 원글을 복원해도 되살아나지 않습니다. +- 글쓰기 중 파일만 올리고 저장하지 않은 채 나간 임시 첨부를 매일 자동 정리합니다. 종전에는 이런 파일이 서버에 계속 쌓여도 회수할 방법이 없었습니다. +- 삭제한 첨부의 파일을 보존기간이 지난 뒤 완전히 파기하는 기능이 추가되었습니다. 게시판 환경설정 > 기본 설정에서 켤 수 있으며(기본 꺼짐), 보존기간은 휴지통 복원 가능 기간과 같게 유지하시면 됩니다. + +### Changed + +- 글 저장·수정 시 비밀글 여부 값을 문자열(`"true"`/`"false"`)로 보내는 클라이언트도 수용하도록 해석을 관대화했습니다. 해석할 수 없는 값은 종전과 동일하게 거부됩니다. +- 첨부 미리보기·작성자 아바타 주소 조립을 코어와 공유하는 단일 지점으로 정리했습니다. 표시되는 주소와 동작은 종전과 동일합니다. +- 코어 최소 요구 버전을 7.0.7 로 상향했습니다. + +### Fixed + +- 통합검색에서 게시글 검색이 실패하면 "검색 결과 없음" 이 아니라 오류 안내로 구분해 표시되도록 실패 정보를 응답에 싣습니다. 실패 원인은 서버 기록에 상세히 남습니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 샘플 게시글 생성 도구가 데이터베이스 접두어를 기본값이 아닌 값으로 설치한 사이트에서 오류가 나던 문제를 수정했습니다. (#101 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 관리자 게시글 작성 화면의 안내 아이콘이 의도한 크기와 다르게 보이던 문제를 수정했습니다. +- 관리자 게시판 설정을 저장해도 SEO 미리 생성 캐시가 갱신되지 않아, 검색엔진에 예전 정보가 계속 노출되던 문제를 수정했습니다. 환경설정 기본값 일괄 적용에도 같은 처리가 적용됩니다. +- 일부 설정 저장 경로가 다른 저장 경로와 다른 처리를 거쳐, 저장 직후 조회가 저장 전 값을 돌려주거나 값의 형식이 정리되지 않은 채 남던 문제를 수정했습니다. +- 게시판 유형 삭제가 실패했을 때 내부 오류 원문이 그대로 표시되던 문제를 수정했습니다. 이제 사용 중인 유형·기본 유형이라 삭제할 수 없는 경우에는 그 사유가 안내되고, 그 밖의 오류는 서버 오류로 구분됩니다. +- 첨부 개수 상한 초과·대댓글 깊이 초과·댓글을 달 수 없는 글·메뉴 중복 안내도 같은 방식으로 정리했습니다. 안내 문구를 사용자의 언어 설정으로 만드는 시점이 응답을 만드는 시점과 어긋나 있던 것을 맞췄습니다. 표시되는 문구와 상태는 종전과 동일합니다. +- 신고 반려 누적 제한 안내 문구를 실제 동작에 맞게 정정했습니다. "설정 건수를 초과하면 차단"으로 적혀 있었지만 실제로는 설정 건수에 도달하는 순간부터 차단됩니다 — 5건으로 설정하면 5번째 반려부터 신고가 막힙니다. +- 관리자가 게시판 설정을 저장해도 백그라운드 작업에는 이전 설정이 계속 적용되던 문제를 수정했습니다. (#109 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 신고 현황 목록에서 항목을 선택한 뒤 검색하거나 페이지를 넘기면, 화면에서 사라진 항목이 선택된 채로 남아 일괄 처리 대상에 포함되던 문제를 수정했습니다. 이제 일괄 처리 대상은 언제나 화면에 보이면서 체크된 항목뿐입니다. +- 댓글 목록을 불러올 때 댓글마다 원글을 반복 조회하던 비효율을 제거해 응답을 더 빠르게 했습니다. 댓글이 많은 글일수록 개선 폭이 큽니다. +- 답글이 달린 글을 삭제해도 답글이 지워지지 않아, 목록에는 보이지 않으면서 주소로는 열리는 상태로 남던 문제를 수정했습니다. (#107 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 이커머스 문의 연동: 이미 답변이 있는 문의 글에는 연동 경로로도 답변 글이 중복 생성되지 않도록 방어를 추가했습니다. (#106 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 게시판을 삭제할 때 다른 기능(상품 문의 연동 등)이 그 게시판의 글과 연결해 둔 데이터를 함께 정리할 수 있도록, 삭제되는 글 목록을 연동 기능에 알려줍니다. +- 신고 목록의 동작하지 않는 정렬 파라미터를 정리했습니다. +- 관리자 화면(게시글 작성·환경설정 저장·기본값 일괄 적용·메일/알림 템플릿·SEO/스팸 캐시 정리·본인인증 정책)에서 저장·실행이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.3] - 2026-08-10 ### Security diff --git a/modules/_bundled/sirsoft-board/composer.json b/modules/_bundled/sirsoft-board/composer.json index 02e719ba..fb973132 100644 --- a/modules/_bundled/sirsoft-board/composer.json +++ b/modules/_bundled/sirsoft-board/composer.json @@ -2,7 +2,7 @@ "name": "modules/sirsoft-board", "description": "Board module for Gnuboard7", "type": "library", - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "autoload": { "psr-4": { diff --git a/modules/_bundled/sirsoft-board/config/board.php b/modules/_bundled/sirsoft-board/config/board.php index 16f4a55b..2e6af244 100644 --- a/modules/_bundled/sirsoft-board/config/board.php +++ b/modules/_bundled/sirsoft-board/config/board.php @@ -155,6 +155,10 @@ return [ 'max_comment_depth_min' => 0, 'max_comment_depth_max' => 10, + // 삭제 첨부 영구 정리 보존기간 (일) + 'attachment_purge_retention_days_min' => 1, + 'attachment_purge_retention_days_max' => 3650, + // 신고 정책 'auto_hide_threshold_min' => 0, 'auto_hide_threshold_max' => 100, diff --git a/modules/_bundled/sirsoft-board/config/settings/defaults.json b/modules/_bundled/sirsoft-board/config/settings/defaults.json index f7e2e7dc..d55e2029 100644 --- a/modules/_bundled/sirsoft-board/config/settings/defaults.json +++ b/modules/_bundled/sirsoft-board/config/settings/defaults.json @@ -2,7 +2,7 @@ "_meta": { "version": "1.0.0", "description": "게시판 모듈 환경설정", - "categories": ["basic_defaults", "report_policy", "spam_security", "display", "seo", "notifications"] + "categories": ["basic_defaults", "report_policy", "spam_security", "display", "seo", "notifications", "attachment_settings"] }, "defaults": { "basic_defaults": { @@ -15,6 +15,7 @@ "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "max_comment_depth": 10, "comment_order": "ASC", "show_view_count": true, @@ -100,6 +101,10 @@ { "id": "mail", "is_active": true, "sort_order": 0 }, { "id": "database", "is_active": true, "sort_order": 1 } ] + }, + "attachment_settings": { + "purge_enabled": false, + "purge_retention_days": 30 } }, "frontend_schema": { @@ -118,6 +123,9 @@ }, "seo": { "expose": false + }, + "attachment_settings": { + "expose": false } } } \ No newline at end of file diff --git a/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000001_add_reply_delete_policy_to_boards_table.php b/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000001_add_reply_delete_policy_to_boards_table.php new file mode 100644 index 00000000..10f5d878 --- /dev/null +++ b/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000001_add_reply_delete_policy_to_boards_table.php @@ -0,0 +1,49 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +/** + * boards 테이블에 답글 삭제 정책 컬럼을 추가합니다. + * + * 답글이 달린 글을 삭제할 때의 동작을 게시판별로 선택합니다. + * - 'cascade' (기본값): 답글도 함께 소프트 삭제하고, 복원 시 연쇄분만 되살립니다. + * - 'block': 살아 있는 답글이 있는 동안 원글 삭제를 차단합니다. + * + * NOT NULL + default 라 ALTER 시 기존 행이 자동으로 'cascade' 로 채워집니다 — + * 지금까지 성공하던 삭제(신고 조치·관리자 삭제)가 갑자기 차단되지 않습니다. + */ +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + if (! Schema::hasTable('boards') || Schema::hasColumn('boards', 'reply_delete_policy')) { + return; + } + + Schema::table('boards', function (Blueprint $table) { + $table->string('reply_delete_policy', 20) + ->default('cascade') + ->comment('답글 삭제 정책 (block: 답글 있으면 삭제 차단, cascade: 함께 소프트 삭제)') + ->after('max_reply_depth'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (! Schema::hasTable('boards') || ! Schema::hasColumn('boards', 'reply_delete_policy')) { + return; + } + + Schema::table('boards', function (Blueprint $table) { + $table->dropColumn('reply_delete_policy'); + }); + } +}; diff --git a/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000002_modify_trigger_type_in_board_posts_table.php b/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000002_modify_trigger_type_in_board_posts_table.php new file mode 100644 index 00000000..a0ee27dc --- /dev/null +++ b/modules/_bundled/sirsoft-board/database/migrations/2026_08_17_000002_modify_trigger_type_in_board_posts_table.php @@ -0,0 +1,51 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Support\Facades\DB; + +return new class extends Migration +{ + /** + * Run the migrations. + * + * board_posts.trigger_type enum 에 'cascade' 값을 추가합니다. + * 원글 삭제 시 함께 지워지는 답글 트리를 'cascade' 로 마킹하여, 원글 복원 시 + * 사용자가 직접 지운 답글('user')과 구분해 선별 복원하기 위함입니다 + * (댓글 2026_06_26_000001 / 첨부 2026_06_26_000002 와 동일 규약). + */ + public function up(): void + { + $prefix = DB::getTablePrefix(); + + // enum 에 'cascade' 추가 (조치 주체: 원글 삭제 연쇄) + DB::statement( + "ALTER TABLE {$prefix}board_posts MODIFY COLUMN trigger_type " + ."ENUM('report', 'admin', 'system', 'auto_hide', 'user', 'cascade') " + ."NOT NULL DEFAULT 'admin' " + ."COMMENT '조치 주체 (report: 신고, admin: 관리자, system: 시스템, auto_hide: 자동 블라인드, user: 사용자 직접 삭제, cascade: 원글 삭제 연쇄)'" + ); + } + + /** + * Reverse the migrations. + * + * enum 축소 전, 잔존하는 'cascade' 행을 'admin' 으로 치환해야 롤백이 깨지지 않습니다. + * (enum 에 없는 값이 데이터에 남아 있으면 MODIFY COLUMN 이 실패) + */ + public function down(): void + { + $prefix = DB::getTablePrefix(); + + // 잔존 cascade 행을 admin 으로 치환 (enum 축소 안전 처리) + DB::table('board_posts') + ->where('trigger_type', 'cascade') + ->update(['trigger_type' => 'admin']); + + DB::statement( + "ALTER TABLE {$prefix}board_posts MODIFY COLUMN trigger_type " + ."ENUM('report', 'admin', 'system', 'auto_hide', 'user') " + ."NOT NULL DEFAULT 'admin' " + ."COMMENT '조치 주체 (report: 신고, admin: 관리자, system: 시스템, auto_hide: 자동 블라인드, user: 사용자 직접 삭제)'" + ); + } +}; diff --git a/modules/_bundled/sirsoft-board/database/seeders/Sample/PostSampleSeeder.php b/modules/_bundled/sirsoft-board/database/seeders/Sample/PostSampleSeeder.php index df014f21..d95db723 100644 --- a/modules/_bundled/sirsoft-board/database/seeders/Sample/PostSampleSeeder.php +++ b/modules/_bundled/sirsoft-board/database/seeders/Sample/PostSampleSeeder.php @@ -2,6 +2,7 @@ namespace Modules\Sirsoft\Board\Database\Seeders\Sample; +use App\Extension\Storage\ModuleStorageDriver; use App\Models\Permission; use App\Models\Role; use App\Models\User; @@ -10,7 +11,6 @@ use Carbon\Carbon; use Illuminate\Database\Seeder; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Modules\Sirsoft\Board\Models\Board; @@ -362,71 +362,75 @@ class PostSampleSeeder extends Seeder { $this->command->info('비정규화 카운트 컬럼 동기화 중...'); + // raw SQL 은 접두사가 자동 부착되지 않는다 — 리터럴 `g7_` 을 박으면 + // DB_PREFIX 가 다른 설치에서 1146(테이블 없음)이 된다. + $prefix = DB::getTablePrefix(); + // posts.comments_count (다른 테이블 참조 — 직접 서브쿼리 가능) - DB::statement(' - UPDATE g7_board_posts p + DB::statement(" + UPDATE {$prefix}board_posts p SET comments_count = ( - SELECT COUNT(*) FROM g7_board_comments c + SELECT COUNT(*) FROM {$prefix}board_comments c WHERE c.post_id = p.id AND c.deleted_at IS NULL ) - '); + "); // posts.replies_count (자기 참조 — 파생 테이블 사용) - DB::statement(' - UPDATE g7_board_posts p + DB::statement(" + UPDATE {$prefix}board_posts p LEFT JOIN ( SELECT parent_id, COUNT(*) AS cnt - FROM g7_board_posts + FROM {$prefix}board_posts WHERE parent_id IS NOT NULL AND deleted_at IS NULL GROUP BY parent_id ) t ON t.parent_id = p.id SET p.replies_count = COALESCE(t.cnt, 0) - '); + "); // posts.attachments_count (다른 테이블 참조 — 직접 서브쿼리 가능) - DB::statement(' - UPDATE g7_board_posts p + DB::statement(" + UPDATE {$prefix}board_posts p SET attachments_count = ( - SELECT COUNT(*) FROM g7_board_attachments a + SELECT COUNT(*) FROM {$prefix}board_attachments a WHERE a.post_id = p.id AND a.deleted_at IS NULL ) - '); + "); // comments.replies_count (자기 참조 — 파생 테이블 사용) - DB::statement(' - UPDATE g7_board_comments c + DB::statement(" + UPDATE {$prefix}board_comments c LEFT JOIN ( SELECT parent_id, COUNT(*) AS cnt - FROM g7_board_comments + FROM {$prefix}board_comments WHERE parent_id IS NOT NULL AND deleted_at IS NULL GROUP BY parent_id ) t ON t.parent_id = c.id SET c.replies_count = COALESCE(t.cnt, 0) - '); + "); // boards.posts_count (게시판별 미삭제 게시글 수 — 답글 포함) - DB::statement(' - UPDATE g7_boards b + DB::statement(" + UPDATE {$prefix}boards b LEFT JOIN ( SELECT board_id, COUNT(*) AS cnt - FROM g7_board_posts + FROM {$prefix}board_posts WHERE deleted_at IS NULL GROUP BY board_id ) t ON t.board_id = b.id SET b.posts_count = COALESCE(t.cnt, 0) - '); + "); // boards.comments_count (게시판별 미삭제 댓글 수) - DB::statement(' - UPDATE g7_boards b + DB::statement(" + UPDATE {$prefix}boards b LEFT JOIN ( SELECT board_id, COUNT(*) AS cnt - FROM g7_board_comments + FROM {$prefix}board_comments WHERE deleted_at IS NULL GROUP BY board_id ) t ON t.board_id = b.id SET b.comments_count = COALESCE(t.cnt, 0) - '); + "); $this->command->info(' ✅ posts.comments_count / posts.replies_count / posts.attachments_count / comments.replies_count / boards.posts_count / boards.comments_count 일괄 갱신 완료'); } @@ -470,6 +474,9 @@ class PostSampleSeeder extends Seeder $datePath = date('Y/m/d'); + // 모듈 스토리지 드라이버 경유 — 최종 경로는 {identifier}/attachments/{path} 로 종전과 동일 + $storage = new ModuleStorageDriver('sirsoft-board', 'modules'); + for ($i = 0; $i < self::SAMPLE_IMAGE_COUNT; $i++) { $filename = Str::uuid().'.jpg'; $path = "samples/{$datePath}/{$filename}"; @@ -484,7 +491,7 @@ class PostSampleSeeder extends Seeder // 스토리지에 파일 저장 $storagePath = "sirsoft-board/attachments/{$path}"; - Storage::disk('modules')->put($storagePath, $imageContent); + $storage->put('attachments', $path, $imageContent); // 파일 정보 저장 $this->sampleImages[] = [ diff --git a/modules/_bundled/sirsoft-board/docs/api/board-types.md b/modules/_bundled/sirsoft-board/docs/api/board-types.md index f19e409a..7d89b98c 100644 --- a/modules/_bundled/sirsoft-board/docs/api/board-types.md +++ b/modules/_bundled/sirsoft-board/docs/api/board-types.md @@ -206,6 +206,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | 403 | Forbidden | 요구 권한(`sirsoft-board.boards.create`)이 없는 경우 | | 404 | Not Found | 해당 `id` 의 게시판 유형이 없는 경우 (`게시판 유형을 찾을 수 없습니다.`) | | 422 | Unprocessable Entity | 해당 유형을 사용 중인 게시판이 있는 경우 (`:count개 게시판에서 사용 중인 유형은 삭제할 수 없습니다.`) 또는 게시판 기본 유형(`basic_defaults.type`)으로 설정된 경우 (`기본값으로 설정된 유형은 삭제할 수 없습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`작업 처리 중 오류가 발생했습니다.`) — 도메인 규칙 위반이 아닌 예외는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-board/docs/api/board.md b/modules/_bundled/sirsoft-board/docs/api/board.md index 209a90a0..d0b3c625 100644 --- a/modules/_bundled/sirsoft-board/docs/api/board.md +++ b/modules/_bundled/sirsoft-board/docs/api/board.md @@ -16,6 +16,16 @@ --- +## 비밀글 서버측 게이팅 (KVE-2026-1914) + +비밀글(`is_secret`)의 원문은 작성자 본인 또는 게시판 관리 권한(`posts.read-secret`/`manager`)을 가진 요청에만 제공됩니다. 판정은 `SecretContentGate`(SSoT)가 담당하며 게시글 상세 외 다음 경로에도 동일하게 적용됩니다. + +- **댓글 목록**(`GET .../posts/{postId}/comments`): 부모 게시글이 비밀글이고 열람 권한이 없으면 빈 목록(`200`)을 반환합니다. +- **첨부 서빙**(`GET .../attachment/{hash}`, `.../attachment/{hash}/preview`): 부모 게시글이 비밀글이고 열람 권한이 없으면 `403`. 첨부 요청은 상세와 분리된 요청이라 비밀번호 검증(`password_verified`)은 적용되지 않으며 작성자/관리 권한만 인정합니다. +- **상세/목록 응답**: 비열람자에게 `content`·`title`·`reply`·`attachments`가 마스킹됩니다. + +--- + ## 목록·검색의 총 건수와 답변·댓글 상한 게시판 목록에 `search` 를 얹으면 내부 검색이 수행됩니다. 매칭이 아주 많을 수 있으므로 총 @@ -351,7 +361,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드 + `data.pagination`._ | reply_count | integer | `0` | reply 개수 (집계) | | attachment_count | integer | `0` | attachment 개수 (집계) | | has_attachment | boolean | `false` | attachment 여부 | -| thumbnail | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| thumbnail | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 — `/api/modules/sirsoft-board/boards/{slug}/attachment/{hash}/preview` 형식 (첫 이미지 첨부의 미리보기 서빙 URL) | | parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | | depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | | is_reply | boolean | `false` | reply 여부 | @@ -981,10 +991,11 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.admin.posts.write\|sirsoft-board.{slug}.admin.manage`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 422 | Unprocessable Entity | 게시판의 답글 삭제 정책(`reply_delete_policy`)이 `block` 이고 대상 글에 살아 있는 답글이 있는 경우 (`답글이 달린 글은 삭제할 수 없습니다. 답글을 먼저 삭제해 주세요.` — `sirsoft-board::validation.post.delete.has_replies`) | <!-- @generated:end --> -**설명** 게시판 관리자가 게시글 1건을 소프트 삭제합니다. `auth:sanctum` + admin 인증이 필요하며, 라우트 권한은 `posts.write` 또는 `manage`입니다. 컨트롤러가 대상 게시글을 조회한 뒤 세분화된 권한 분기를 적용합니다: `admin.manage`는 모든 글(비회원 글 포함)을, `admin.posts.write`는 본인 글만 삭제할 수 있으며 이미 삭제된 글의 재처리는 `admin.manage`가 필요합니다. `PostService::deletePost()`가 'admin' 컨텍스트로 소프트 삭제를 수행합니다. +**설명** 게시판 관리자가 게시글 1건을 소프트 삭제합니다. `auth:sanctum` + admin 인증이 필요하며, 라우트 권한은 `posts.write` 또는 `manage`입니다. 컨트롤러가 대상 게시글을 조회한 뒤 세분화된 권한 분기를 적용합니다: `admin.manage`는 모든 글(비회원 글 포함)을, `admin.posts.write`는 본인 글만 삭제할 수 있으며 이미 삭제된 글의 재처리는 `admin.manage`가 필요합니다. `PostService::deletePost()`가 'admin' 컨텍스트로 소프트 삭제를 수행합니다. 삭제 동작은 게시판의 답글 삭제 정책(`reply_delete_policy`)을 따릅니다 — `cascade`(기본)에서는 원글 삭제 시 살아 있는 답글 트리(및 그 답글들의 댓글·첨부)가 함께 소프트 삭제되고 이후 원글을 복원하면 연쇄 삭제분(`trigger_type='cascade'`)만 선택 복원되며, `block` 에서는 살아 있는 직계 답글이 있으면 `before_delete` 훅 발화 전에 차단되어(부수효과 없음) 422 를 반환합니다. ### GET /api/modules/sirsoft-board/admin/board/{slug}/posts/{id} @@ -1029,7 +1040,7 @@ _단건 응답: `data` 객체의 필드._ | reply_count | integer | `0` | reply 개수 (집계) | | attachment_count | integer | `0` | attachment 개수 (집계) | | has_attachment | boolean | `false` | attachment 여부 | -| thumbnail | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 | +| thumbnail | string | `/api/modules/sirsoft-board/boards/api…` | 썸네일 이미지 URL/경로 — `/api/modules/sirsoft-board/boards/{slug}/attachment/{hash}/preview` 형식 (첫 이미지 첨부의 미리보기 서빙 URL) | | parent_id | null | `null` | parent 식별자 (연관 리소스 참조) | | depth | integer | `0` | 계층 트리에서의 깊이 (0 = 최상위, 하위로 갈수록 증가) | | is_reply | boolean | `false` | reply 여부 | diff --git a/modules/_bundled/sirsoft-board/docs/api/boards.md b/modules/_bundled/sirsoft-board/docs/api/boards.md index 55f2117b..a964edb6 100644 --- a/modules/_bundled/sirsoft-board/docs/api/boards.md +++ b/modules/_bundled/sirsoft-board/docs/api/boards.md @@ -25,7 +25,14 @@ **요청 파라미터** -_요청 파라미터 없음._ +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | +| per_page | query | integer | 아니오 | min 1, max 999 | 페이지당 항목 수 (게시판은 운영자 등록 설정성 테이블 — 전체 목록 셀렉트 소비처의 전량 조회를 수용) | +| type | query | string | 아니오 | max 50 | 게시판 타입 슬러그 필터 (basic, gallery 등) | +| search | query | string | 아니오 | max 100 | 검색어 (게시판명/슬러그 부분 일치) | +| sort_by | query | string | 아니오 | `id`, `slug`, `type`, `is_active`, `posts_count`, `comments_count`, `created_at`, `updated_at` | 정렬 기준 컬럼 (닫힌 집합 — `name` 은 다국어 JSON 컬럼이라 정렬 대상에서 제외) | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (대문자 `ASC`/`DESC` 로 보내도 검증 전 소문자로 정규화되어 수용) | **요청 예시** @@ -102,10 +109,11 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-board.boards.read`)이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) — `sort_by` 가 허용 컬럼 목록 밖이거나, `sort_order` 가 `asc`/`desc` 가 아니거나, `per_page` 가 1~999 범위를 벗어난 경우 등 (`IndexBoardRequest` 검증) | <!-- @generated:end --> -**설명** 관리자 화면의 게시판 관리 목록을 조회합니다. `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, 요청의 필터/페이징 파라미터를 그대로 서비스에 넘겨 전체 게시판을 페이지네이션합니다. 각 항목은 `BoardCollection::withPermissions()` 로 감싸져 현재 관리자의 생성/수정/삭제 가능 여부(`abilities`)를 함께 반환합니다. +**설명** 관리자 화면의 게시판 관리 목록을 조회합니다. `auth:sanctum` + `sirsoft-board.boards.read` 권한이 필요하며, `IndexBoardRequest` 로 검증된 필터/정렬/페이징 파라미터만 서비스에 넘겨 전체 게시판을 페이지네이션합니다. 정렬 컬럼(`sort_by`)과 페이지 크기(`per_page` 1~999)는 닫힌 집합으로 제한되어 허용값 밖 요청은 422 로 거부됩니다. 각 항목은 `BoardCollection::withPermissions()` 로 감싸져 현재 관리자의 생성/수정/삭제 가능 여부(`abilities`)를 함께 반환합니다. ### POST /api/modules/sirsoft-board/admin/boards @@ -150,6 +158,7 @@ HTTP/1.1 200 | board_step_ids | body | array | 아니오 | — | board step 식별자 배열 | | permissions | body | array | 아니오 | — | 게시판별 세부 권한 매트릭스 (권한 키별 mode/roles — 미지정 시 Service가 Manager/Step 역할을 주입) | | max_reply_depth | body | integer | 아니오 | min 1, max 10 | 답변글 최대 중첩 깊이 | +| reply_delete_policy | body | string | 예 | `block`, `cascade` | 답글 달린 글의 삭제 정책 (block 답글이 살아 있으면 원글 삭제 차단 / cascade 원글 삭제 시 답글도 함께 소프트 삭제). 미전송 시 검증 전 기본값 `cascade` 로 채워지며, 허용값 밖이면 422 | | max_comment_depth | body | integer | 아니오 | min 0, max 10 | 대댓글 최대 중첩 깊이 | | notify_admin_on_post | body | boolean | 예 | — | 게시글 등록 시 관리자에게 이메일 알림 발송 여부 | | notify_author | body | boolean | 예 | — | 댓글·대댓글·답변글·관리자 처리 발생 시 작성자에게 이메일 알림 발송 여부 | @@ -213,6 +222,7 @@ Content-Type: application/json "예시값" ], "max_reply_depth": 1, + "reply_delete_policy": "cascade", "max_comment_depth": 1, "notify_admin_on_post": true, "notify_author": true, @@ -244,6 +254,7 @@ _단건 응답: `data` 객체의 필드 (생성된 게시판 = `BoardResource`, | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | | max_reply_depth | integer | `5` | 답변글 최대 깊이 | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 | @@ -301,6 +312,7 @@ HTTP/1.1 201 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": true, "comment_order": "ASC", "max_comment_depth": 10, @@ -390,6 +402,7 @@ _단건 응답: `data` 객체의 필드._ | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | | max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~10) | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 기본값 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | | show_view_count | boolean | `true` | 조회수 노출 | @@ -445,6 +458,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "max_comment_depth": 10, "comment_order": "ASC", "show_view_count": true, @@ -755,6 +769,7 @@ _단건 응답: `data` 객체의 필드._ | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | | max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~10) | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | @@ -812,6 +827,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": true, "comment_order": "ASC", "max_comment_depth": 10, @@ -973,6 +989,7 @@ _단건 응답: `data` 객체의 필드._ | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | | max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~10) | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | @@ -1030,6 +1047,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": true, "comment_order": "ASC", "max_comment_depth": 10, @@ -1267,6 +1285,7 @@ HTTP/1.1 200 | board_step_ids | body | array | 아니오 | — | board step 식별자 배열 | | permissions | body | array | 예 | — | 게시판별 세부 권한 매트릭스 (권한 키별 mode/roles — 각 권한을 all 또는 특정 역할에 부여) | | max_reply_depth | body | integer | 아니오 | min 1, max 10 | 답변글 최대 중첩 깊이 | +| reply_delete_policy | body | string | 아니오 | `block`, `cascade` | 답글 달린 글의 삭제 정책 (block 답글이 살아 있으면 원글 삭제 차단 / cascade 원글 삭제 시 답글도 함께 소프트 삭제). 미전송 시 기존값 유지, 허용값 밖이면 422 | | max_comment_depth | body | integer | 아니오 | min 0, max 10 | 대댓글 최대 중첩 깊이 | | notify_admin_on_post | body | boolean | 예 | — | 게시글 등록 시 관리자에게 이메일 알림 발송 여부 | | notify_author | body | boolean | 예 | — | 댓글·대댓글·답변글·관리자 처리 발생 시 작성자에게 이메일 알림 발송 여부 | @@ -1329,6 +1348,7 @@ Content-Type: application/json "예시값" ], "max_reply_depth": 1, + "reply_delete_policy": "block", "max_comment_depth": 1, "notify_admin_on_post": true, "notify_author": true, @@ -1360,6 +1380,7 @@ _단건 응답: `data` 객체의 필드 (수정된 게시판 = `BoardResource`, | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 | | max_reply_depth | integer | `5` | 답변글 최대 깊이 | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC, DESC) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 | @@ -1417,6 +1438,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": true, "comment_order": "ASC", "max_comment_depth": 10, @@ -1980,6 +2002,7 @@ _단건 응답: `data` 객체의 필드._ | use_comment | boolean | `true` | 댓글 기능 사용 | | use_reply | boolean | `true` | 게시글 답변 기능 사용 (댓글에 대한 답글 아님) | | max_reply_depth | integer | `5` | 답변글 최대 깊이 (1~10) | +| reply_delete_policy | string | `cascade` | 답글 달린 글의 삭제 정책 (block: 답글이 살아 있으면 원글 삭제 차단, cascade: 원글 삭제 시 답글도 함께 소프트 삭제) | | use_report | boolean | `true` | 게시글/댓글 신고 기능 사용 | | comment_order | string | `ASC` | 댓글 정렬 순서 (ASC: 오름차순, DESC: 내림차순) | | max_comment_depth | integer | `10` | 대댓글 최대 깊이 (1~10) | @@ -2037,6 +2060,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": true, "comment_order": "ASC", "max_comment_depth": 10, @@ -2346,7 +2370,9 @@ Cache-Control: public, max-age=86400 <!-- @generated:end --> -**설명** 해시로 지정한 이미지 첨부파일을 인라인 미리보기로 제공합니다. 게시글 본문 내 이미지 표시용이라 비회원도 접근할 수 있으며(권한 체크 없음), 이미지가 아닌 파일 요청 시 400을 반환합니다. 응답에는 레이아웃 캐시 TTL(기본 24시간) 기반 캐싱 헤더가 포함되고, 삭제글 첨부 등 차단 대상은 403을 반환합니다. +**설명** 해시로 지정한 이미지 첨부파일을 인라인 미리보기로 제공합니다. 게시글 본문 내 이미지 표시용이라 비회원도 접근할 수 있으며(권한 체크 없음), 이미지가 아닌 파일 요청 시 400을 반환합니다. 응답에는 레이아웃 캐시 TTL(기본 24시간) 기반 캐싱 헤더가 포함되고, 삭제글·비밀글 첨부 등 차단 대상은 403을 반환합니다. + +브라우저 `<img src>` 는 Authorization 헤더를 실을 수 없으므로, 비밀글·삭제글의 열람 권한자에게 직렬화되는 상세 응답의 `preview_url` 에는 `expires`·`signature` 쿼리가 붙은 **한시 서명 URL** 이 실리며, 이 엔드포인트는 유효한 서명을 열람 권한과 동등한 자격으로 허용합니다. 서명이 변조·만료된 요청은 종전과 동일하게 403 으로 차단됩니다. 정상글의 `preview_url` 은 무서명 공개 경로입니다. ### POST /api/modules/sirsoft-board/boards/{slug}/attachments @@ -2961,6 +2987,7 @@ HTTP/1.1 200 | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | +| is_secret | body | boolean | 아니오 | — | 비밀글 여부. 문자열 `"true"`/`"false"`/`"1"`/`"0"` 도 수용해 boolean 으로 정규화한다 (해석 불가값은 422) | > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.store_validation_rules`). @@ -3324,6 +3351,9 @@ HTTP/1.1 200 | --- | --- | --- | --- | --- | --- | | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | id | path | string | 예 | — | 대상 리소스의 식별자 | +| password | body | string | 아니오 | min 4 | 비회원 게시글 삭제 시 소유권 확인용 비밀번호. 형식(문자열, 4자 이상)은 `DestroyPostRequest` 가 검증하며, 배열 주입(`password[]=x`) 등 형식 위반은 422. 소유권 판정 자체는 컨트롤러(`canModifyPost`)가 수행 | +| verification_token | body | string | 아니오 | max 255 | 비회원 게시글 삭제 시 소유권 확인용 검증 토큰 (비밀번호 검증 후 발급). 문자열 형식 위반 시 422 | +| force_delete | body | boolean | 아니오 | — | 불리언 플래그 (형식 검증만 수행 — 불리언으로 해석 불가한 값은 422) | **요청 예시** @@ -3358,11 +3388,12 @@ HTTP/1.1 200 | --- | --- | --- | | 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.posts.write\|sirsoft-board.{slug}.manager`)이 없거나, 작성자 본인·게시판 관리자·비회원 글 검증(토큰/비밀번호) 중 어느 조건도 만족하지 않는 경우 (`게시글 삭제 권한이 없습니다.`) | | 404 | Not Found | 활성 게시판 또는 게시글이 없는 경우 | +| 422 | Unprocessable Entity | 요청 파라미터(`password`/`verification_token`/`force_delete`)가 형식 검증을 위반한 경우 (`error.errors` 에 필드별 메시지) / 게시판의 답글 삭제 정책(`reply_delete_policy`)이 `block` 이고 살아 있는 답글이 있는 경우 (`답글이 달린 글은 삭제할 수 없습니다. 답글을 먼저 삭제해 주세요.` — `sirsoft-board::validation.post.delete.has_replies`) | | 500 | Internal Server Error | 삭제 처리 중 예외 (`게시글 삭제에 실패했습니다.`) | <!-- @generated:end --> -**설명** 게시글을 삭제(소프트 삭제)합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 또는 게시판 manager 권한이 필요합니다. 작성자 본인, 게시판 관리자(admin.manage/manager), 또는 비회원 글의 경우 검증 토큰·비밀번호 확인 중 하나를 만족해야 삭제할 수 있으며, 조건 미충족 시 403을 반환합니다. +**설명** 게시글을 삭제(소프트 삭제)합니다. `auth:sanctum` + `sirsoft-board.{slug}.posts.write` 또는 게시판 manager 권한이 필요합니다. 작성자 본인, 게시판 관리자(admin.manage/manager), 또는 비회원 글의 경우 검증 토큰·비밀번호 확인 중 하나를 만족해야 삭제할 수 있으며, 조건 미충족 시 403을 반환합니다. 삭제 동작은 게시판의 답글 삭제 정책(`reply_delete_policy`)을 따릅니다 — `cascade`(기본)에서는 원글 삭제 시 살아 있는 답글 트리(및 그 답글들의 댓글·첨부)가 함께 소프트 삭제되고 이후 원글을 복원하면 연쇄 삭제분이 함께 복원되며, `block` 에서는 살아 있는 답글이 있으면 어떤 부수효과도 없이 422 로 삭제가 차단됩니다. ### GET /api/modules/sirsoft-board/boards/{slug}/posts/{id} @@ -3628,6 +3659,9 @@ HTTP/1.1 200 | id | path | string | 예 | — | 대상 리소스의 식별자 | | attachment_ids | body | integer[] | 아니오 | — | 미리 업로드해 둔 첨부 ID 목록. 게시글에 연결된다 (관리자 수정 경로와 동일) | | temp_key | body | string | 아니오 | 최대 64자 | 임시 업로드 묶음 키. 해당 묶음의 첨부가 게시글로 이동·연결된다 | +| is_secret | body | boolean | 아니오 | — | 비밀글 여부. 문자열 `"true"`/`"false"`/`"1"`/`"0"` 도 수용해 boolean 으로 정규화한다 (해석 불가값은 422) | +| password | body | string | 아니오 | min 4 | 비회원 게시글 수정 시 소유권 확인용 비밀번호. 형식(문자열, 4자 이상)만 여기서 검증하며 배열 주입 등 형식 위반은 422. 소유권 판정은 컨트롤러(`canModifyPost`)가 수행 | +| verification_token | body | string | 아니오 | max 255 | 비회원 게시글 수정 시 소유권 확인용 검증 토큰 (비밀번호 검증 후 발급). 문자열 형식 위반 시 422 | > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.user_post.update_validation_rules`). @@ -4344,6 +4378,8 @@ HTTP/1.1 201 | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | postId | path | string | 예 | — | 대상 post의 식별자 | | commentId | path | string | 예 | — | 대상 comment의 식별자 | +| password | body | string | 아니오 | min 4, max 20 | 비회원 댓글 삭제 시 소유권 확인용 비밀번호. 형식(문자열, 4~20자)은 `DestroyCommentRequest` 가 검증하며, 배열 주입 등 형식 위반은 422. 소유권 판정 자체는 서비스(`canDelete`)가 수행 | +| verification_token | body | string | 아니오 | max 255 | 비회원 댓글 삭제 시 비밀번호 재입력을 대체하는 임시 검증 토큰 (verify-password 로 발급, 1회 소비). 문자열 형식 위반(배열 주입 등)은 422 | **요청 예시** @@ -4378,6 +4414,7 @@ HTTP/1.1 200 | --- | --- | --- | | 403 | Forbidden | 요구 권한(`sirsoft-board.{slug}.comments.write\|sirsoft-board.{slug}.manager`)이 없거나, 게시판의 댓글 기능이 꺼진 경우, 또는 작성자 본인·게시판 관리자·비회원 댓글 비밀번호 확인 중 어느 조건도 만족하지 않는 경우 (`댓글 삭제 권한이 없습니다.`) | | 404 | Not Found | 게시판 또는 댓글이 없는 경우 (`댓글을 찾을 수 없습니다.`) | +| 422 | Unprocessable Entity | `password` 가 형식 검증(문자열, 4~20자)을 위반한 경우 (`error.errors.password` — 배열 주입 차단) | | 500 | Internal Server Error | 삭제 처리 중 예외 (`댓글 삭제에 실패했습니다.`) | <!-- @generated:end --> @@ -4398,6 +4435,9 @@ HTTP/1.1 200 | slug | path | string | 예 | — | 대상 리소스의 slug (URL 친화 식별자) | | postId | path | string | 예 | — | 대상 post의 식별자 | | commentId | path | string | 예 | — | 대상 comment의 식별자 | +| content | body | string | 예 | 게시판 설정의 min/max 길이 | 수정할 댓글 본문 (금지 키워드 검사 포함) | +| password | body | string | 조건부 | min 4, max 20 | 비회원 댓글 수정 시 소유권 확인용 비밀번호 (검증 토큰이 있으면 생략 가능) | +| verification_token | body | string | 아니오 | max 255 | 비회원 댓글 수정 시 비밀번호 재입력을 대체하는 임시 검증 토큰 (verify-password 로 발급, 1회 소비) | > 이 엔드포인트는 확장이 파라미터를 추가할 수 있습니다 (`sirsoft-board.comment.update_validation_rules`). diff --git a/modules/_bundled/sirsoft-board/docs/api/dashboard.md b/modules/_bundled/sirsoft-board/docs/api/dashboard.md index fa32f66f..c4fcf863 100644 --- a/modules/_bundled/sirsoft-board/docs/api/dashboard.md +++ b/modules/_bundled/sirsoft-board/docs/api/dashboard.md @@ -21,7 +21,7 @@ <!-- @generated:start:api.modules.sirsoft-board.admin.dashboard.overview --> - **라우트명**: `api.modules.sirsoft-board.admin.dashboard.overview` - **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@overview` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-board.dashboard.view` **요청 파라미터** @@ -77,7 +77,7 @@ HTTP/1.1 200 <!-- @generated:start:api.modules.sirsoft-board.admin.dashboard.pending-reports --> - **라우트명**: `api.modules.sirsoft-board.admin.dashboard.pending-reports` - **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@pendingReports` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-board.dashboard.view` **요청 파라미터** @@ -151,7 +151,7 @@ HTTP/1.1 200 <!-- @generated:start:api.modules.sirsoft-board.admin.dashboard.post-graph --> - **라우트명**: `api.modules.sirsoft-board.admin.dashboard.post-graph` - **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@postGraph` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-board.dashboard.view` **요청 파라미터** @@ -253,7 +253,7 @@ HTTP/1.1 200 <!-- @generated:start:api.modules.sirsoft-board.admin.dashboard.recent-posts --> - **라우트명**: `api.modules.sirsoft-board.admin.dashboard.recent-posts` - **컨트롤러**: `Modules\Sirsoft\Board\Http\Controllers\Admin\DashboardController@recentPosts` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-board.dashboard.view` **요청 파라미터** diff --git a/modules/_bundled/sirsoft-board/docs/api/settings.md b/modules/_bundled/sirsoft-board/docs/api/settings.md index 432e3097..1bb600e3 100644 --- a/modules/_bundled/sirsoft-board/docs/api/settings.md +++ b/modules/_bundled/sirsoft-board/docs/api/settings.md @@ -42,7 +42,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 게시판 생성 시 적용되는 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. | +| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 게시판 생성 시 적용되는 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 답글 삭제 방식(reply_delete_policy: block/cascade), 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. | | report_policy | object | `{"auto_hide_threshold":5,"auto_hide_target":"both","daily…` | 신고 정책 카테고리. 자동 숨김 임계치(auto_hide_threshold)와 대상(auto_hide_target: post/comment/both), 사용자별 일일 신고 한도, 신고 거부 누적 제한(횟수/기간), 관리자·작성자 신고 알림 발송 여부와 채널을 포함합니다. | | spam_security | object | `{"post_cooldown_seconds":0,"comment_cooldown_seconds":0,"…` | 스팸·보안 카테고리. 글·댓글·신고 작성 사이의 도배 방지 쿨다운 시간(초)과 조회수 캐시 TTL을 포함합니다. | | display | object | `{"date_display_format":"standard"}` | 표시 설정 카테고리. 날짜 표시 형식(date_display_format: standard 절대 표기 / relative 상대 표기)을 포함합니다. | @@ -73,6 +73,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "max_comment_depth": 10, "comment_order": "ASC", "show_view_count": true, @@ -362,6 +363,7 @@ HTTP/1.1 200 | _tab | body | string | 아니오 | `basic_defaults`, `report_policy`, `spam_security`, `general`, `seo`, `notifications`, `notification_definitions` | 현재 편집 중인 탭을 나타내는 메타 값. 탭 단위 부분 저장의 컨텍스트를 식별하는 용도이며 설정값으로는 저장되지 않습니다. | | notifications | body | array | 아니오 | — | 알림 채널 설정. `channels` 배열의 각 항목에 채널 식별자(id), 활성화 여부(is_active), 정렬 순서(sort_order)를 담아 저장합니다. | | basic_defaults | body | array | 아니오 | — | 기본 설정 카테고리 값. 게시판 타입·페이지당 글 수·정렬·댓글/답글·길이 제한·파일 업로드·기본 권한 등 basic_defaults 하위 키를 저장합니다. `basic_defaults.allowed_extensions` 는 `basic_defaults.use_file_upload` 가 `true` 일 때만 최소 1개가 필수이며, `false`/`null` 이면 검증에서 제외되어 빈 배열도 허용됩니다. `min_title_length`·`min_comment_length`·`new_display_hours` 의 하한은 `config('sirsoft-board.limits')` 선언을 따르며 `0` 을 허용합니다. | +| basic_defaults.reply_delete_policy | body | string | 아니오 | `block`, `cascade` | 게시판 생성 시 적용될 답글 삭제 정책 기본값 (block 답글이 살아 있으면 원글 삭제 차단 / cascade 원글 삭제 시 답글도 함께 소프트 삭제). 허용값 밖이면 422 | | report_policy | body | array | 아니오 | — | 신고 정책 카테고리 값. 자동 숨김 임계치/대상, 일일 신고 한도, 거부 누적 제한, 관리자·작성자 신고 알림 설정을 저장합니다. | | report_policy.auto_hide_threshold | body | integer | 아니오 | min 0, max 100 | 자동 숨김 신고 수 (이 횟수 이상 신고되면 자동 숨김 처리) | | report_policy.auto_hide_target | body | string | 아니오 | `post`, `comment`, `both` | 자동 숨김 대상 (게시글 / 댓글 / 둘 다) | @@ -393,6 +395,9 @@ HTTP/1.1 200 | seo.seo_boards | body | boolean | 아니오 | — | 게시판 목록 페이지의 SEO 페이지 생성 활성화 여부 | | seo.seo_board | body | boolean | 아니오 | — | 개별 게시판 페이지의 SEO 페이지 생성 활성화 여부 | | seo.seo_post_detail | body | boolean | 아니오 | — | 게시글 상세 페이지의 SEO 페이지 생성 활성화 여부 | +| attachment_settings | body | array | 아니오 | — | 첨부 정리 정책 카테고리 값. 삭제된 첨부의 물리 파일을 언제 파기할지 정합니다. | +| attachment_settings.purge_enabled | body | boolean | 아니오 | — | 삭제 첨부 영구 정리 활성화 여부. **기본 false** — 사용자 파일을 실제로 파기하므로 운영자가 직접 켜야 동작합니다. 예약 작업(`sirsoft-board:prune-attachments --scheduled`)이 이 값을 false 폴백으로 재확인합니다. | +| attachment_settings.purge_retention_days | body | integer | 아니오 | min 1, max 3650 | 삭제 첨부 보존기간(일, 기본 30). 게시글을 휴지통에서 복원하면 이 기간 안의 첨부는 함께 복원되므로, 휴지통 복원 가능 기간과 같게 유지해야 합니다. 하한·상한은 `config('sirsoft-board.limits')` 가 정하며 화면 입력도 같은 값을 읽습니다. | **요청 예시** @@ -423,6 +428,7 @@ Content-Type: application/json "basic_defaults.use_comment": true, "basic_defaults.use_reply": true, "basic_defaults.max_reply_depth": 1, + "basic_defaults.reply_delete_policy": "cascade", "basic_defaults.max_comment_depth": 1, "basic_defaults.comment_order": "ASC", "basic_defaults.show_view_count": true, @@ -506,7 +512,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 저장 후 갱신된 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. | +| basic_defaults | object | `{"type":"basic","per_page":20,"per_page_mobile":15,"order…` | 저장 후 갱신된 기본 설정 카테고리. 게시판 타입, 페이지당 글 수(PC/모바일), 정렬 기준/방향, 댓글·답글 사용 여부와 깊이, 답글 삭제 방식(reply_delete_policy: block/cascade), 제목·내용·댓글 길이 제한, 파일 업로드 허용/용량/개수/확장자, 새 글 표시 시간, 게시판 기본 권한(default_board_permissions) 등을 포함합니다. | | report_policy | object | `{"auto_hide_threshold":5,"auto_hide_target":"both","daily…` | 저장 후 갱신된 신고 정책 카테고리. 자동 숨김 임계치(auto_hide_threshold)와 대상(auto_hide_target: post/comment/both), 사용자별 일일 신고 한도, 신고 거부 누적 제한(횟수/기간), 관리자·작성자 신고 알림 발송 여부와 채널을 포함합니다. | | spam_security | object | `{"post_cooldown_seconds":0,"comment_cooldown_seconds":0,"…` | 저장 후 갱신된 스팸·보안 카테고리. 글·댓글·신고 작성 사이의 도배 방지 쿨다운 시간(초)과 조회수 캐시 TTL을 포함합니다. | | display | object | `{"date_display_format":"standard"}` | 저장 후 갱신된 표시 설정 카테고리. 날짜 표시 형식(date_display_format: standard 절대 표기 / relative 상대 표기)을 포함합니다. | @@ -535,6 +541,7 @@ HTTP/1.1 200 "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "max_comment_depth": 10, "comment_order": "ASC", "show_view_count": true, @@ -775,6 +782,8 @@ HTTP/1.1 200 **설명** 게시판 모듈의 환경설정을 저장합니다. `auth:sanctum` 인증과 `sirsoft-board.settings.update` 권한이 필요합니다. `_tab`으로 지정한 탭 단위로 검증된 설정을 저장하며, `report_permissions`가 포함된 경우 신고 권한 역할도 함께 동기화합니다. 저장 성공 시 갱신된 전체 설정과 신고 권한 역할을 반환합니다. +저장이 성공하면 코어 모듈 설정 저장 훅(`core.module_settings.after_save`)이 `('sirsoft-board', 저장한 카테고리 배열, true)` 페이로드로 발화합니다. 이 훅을 구독하는 리스너가 게시판 SEO 미리 생성 캐시를 무효화하고 설정 변경을 활동 로그에 남기므로, 확장도 같은 훅으로 저장 완료 시점에 개입할 수 있습니다. 검증 실패(422)나 저장 실패 시에는 발화하지 않습니다. + ### POST /api/modules/sirsoft-board/admin/settings/bulk-apply <!-- @generated:start:api.modules.sirsoft-board.admin.settings.bulk-apply --> @@ -789,7 +798,8 @@ HTTP/1.1 200 | fields | body | array | 예 | min 1 | 대상 게시판에 일괄 적용할 필드 목록(최소 1개). boards 테이블 컬럼(type, per_page, use_comment, allowed_extensions 등)이나 권한 필드(default_board_permissions, manager), 점(.)을 포함한 개별 권한 키(예: `posts.read`)를 허용합니다. | | apply_all | body | boolean | 예 | — | 전체 게시판 적용 여부. true면 모든 게시판에 적용하고, false면 `board_ids`로 지정한 게시판에만 적용합니다(false 시 board_ids 필수). | | board_ids | body | array | 아니오 | — | board 식별자 배열 | -| override_values | body | array | 아니오 | — | 환경설정 기본값 대신 사용할 재정의 값 맵. 지정한 필드에 대해 기본값이 아닌 임의의 값으로 일괄 적용할 때 사용합니다. | +| override_values | body | array | 아니오 | — | 환경설정 기본값 대신 사용할 재정의 값 맵. 지정한 필드에 대해 기본값이 아닌 임의의 값으로 일괄 적용할 때 사용합니다. 각 키는 게시판 생성/수정과 동일한 제한값으로 검증됩니다 (범위 밖 값은 422). | +| override_values.reply_delete_policy | body | string | 아니오 | `block`, `cascade` | 답글 삭제 정책을 일괄 적용할 때의 재정의 값 (block 답글이 살아 있으면 원글 삭제 차단 / cascade 원글 삭제 시 답글도 함께 소프트 삭제). `fields` 에 `reply_delete_policy` 를 포함해 함께 사용하며, 허용값 밖이면 422 | **요청 예시** diff --git a/modules/_bundled/sirsoft-board/editor-spec.json b/modules/_bundled/sirsoft-board/editor-spec.json index f8c12d50..d79c29f4 100644 --- a/modules/_bundled/sirsoft-board/editor-spec.json +++ b/modules/_bundled/sirsoft-board/editor-spec.json @@ -1036,6 +1036,7 @@ "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": false, "comment_order": "ASC", "max_comment_depth": 10, @@ -1144,6 +1145,7 @@ "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": false, "comment_order": "ASC", "max_comment_depth": 10, @@ -1599,6 +1601,7 @@ "use_comment": true, "use_reply": true, "max_reply_depth": 5, + "reply_delete_policy": "cascade", "use_report": false, "comment_order": "ASC", "max_comment_depth": 10, diff --git a/modules/_bundled/sirsoft-board/module.json b/modules/_bundled/sirsoft-board/module.json index 14ab4275..519aac98 100644 --- a/modules/_bundled/sirsoft-board/module.json +++ b/modules/_bundled/sirsoft-board/module.json @@ -5,13 +5,13 @@ "ko": "게시판", "en": "Board" }, - "version": "1.0.3", + "version": "1.0.4", "license": "MIT", "description": { "ko": "게시판 관리를 위한 모듈", "en": "Module for board management" }, - "g7_version": ">=7.0.6", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} diff --git a/modules/_bundled/sirsoft-board/module.php b/modules/_bundled/sirsoft-board/module.php index bd478cd0..ee208eb6 100644 --- a/modules/_bundled/sirsoft-board/module.php +++ b/modules/_bundled/sirsoft-board/module.php @@ -14,6 +14,7 @@ use Modules\Sirsoft\Board\Listeners\BoardCommentsCountSyncListener; use Modules\Sirsoft\Board\Listeners\BoardNotificationChannelListener; use Modules\Sirsoft\Board\Listeners\BoardNotificationDataListener; use Modules\Sirsoft\Board\Listeners\BoardPostsCountSyncListener; +use Modules\Sirsoft\Board\Listeners\Ckeditor5ReferenceSourcesListener; use Modules\Sirsoft\Board\Listeners\CommentReplySyncListener; use Modules\Sirsoft\Board\Listeners\EcommerceInquiryHookListener; use Modules\Sirsoft\Board\Listeners\PostAttachmentCountSyncListener; @@ -72,6 +73,15 @@ class Module extends AbstractModule 'schedule' => 'hourly', 'description' => '대시보드 게시물 현황 집계', ], + [ + 'command' => 'sirsoft-board:prune-attachments --scheduled', + 'schedule' => 'daily', + 'description' => '방치된 임시 첨부 정리 + 보존기간 경과 삭제 첨부 영구 정리', + // 임시 첨부 정리 파트는 상시 동작해야 하므로 스케줄 자체는 항상 등록한다. + // 사용자 파일을 실제로 파기하는 영구 정리 파트만 커맨드 내부에서 게이트한다 + // (attachment_settings.purge_enabled, false 폴백). + 'enabled_config' => null, + ], ]; } @@ -246,6 +256,33 @@ class Module extends AbstractModule ], ], ], + // 대시보드 조회 권한 (type: admin) + [ + 'identifier' => 'dashboard', + 'name' => [ + 'ko' => '게시판 대시보드', + 'en' => 'Board Dashboard', + ], + 'description' => [ + 'ko' => '게시판 대시보드 조회 권한', + 'en' => 'Board dashboard view permissions', + ], + 'permissions' => [ + [ + 'action' => 'view', + 'name' => [ + 'ko' => '대시보드 조회', + 'en' => 'View Dashboard', + ], + 'description' => [ + 'ko' => '게시판 대시보드(현황/추세/최신글/미처리 신고) 조회', + 'en' => 'View board dashboard (overview, trend, recent posts, pending reports)', + ], + 'type' => 'admin', + 'roles' => ['admin', 'manager'], + ], + ], + ], // 신고 관리 권한 (type: admin) [ 'identifier' => 'reports', @@ -416,6 +453,7 @@ class Module extends AbstractModule CommentReplySyncListener::class, BoardPostsCountSyncListener::class, BoardCommentsCountSyncListener::class, + Ckeditor5ReferenceSourcesListener::class, ]; } diff --git a/modules/_bundled/sirsoft-board/package-lock.json b/modules/_bundled/sirsoft-board/package-lock.json index 8c3338ef..0b905c91 100644 --- a/modules/_bundled/sirsoft-board/package-lock.json +++ b/modules/_bundled/sirsoft-board/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-board", - "version": "1.0.3", + "version": "1.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-board", - "version": "1.0.3", + "version": "1.0.4", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/modules/_bundled/sirsoft-board/package.json b/modules/_bundled/sirsoft-board/package.json index 92974da1..76312ed7 100644 --- a/modules/_bundled/sirsoft-board/package.json +++ b/modules/_bundled/sirsoft-board/package.json @@ -1,13 +1,10 @@ { "name": "@g7/sirsoft-board", - "version": "1.0.3", + "version": "1.0.4", "description": "그누보드7 게시판 모듈 프론트엔드 에셋", "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", "test": "vitest", "test:run": "vitest run", "test:e2e": "playwright test --config=tests/Playwright/playwright.config.ts", diff --git a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-attachment-cleanup-settings.test.ts b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-attachment-cleanup-settings.test.ts new file mode 100644 index 00000000..f89434b9 --- /dev/null +++ b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-attachment-cleanup-settings.test.ts @@ -0,0 +1,187 @@ +/** + * 게시판 환경설정 — 첨부 정리 섹션 계약 테스트 (공개 #115) + * + * @description + * 삭제 첨부 영구 정리는 사용자 파일을 파기하므로 기본 꺼짐이다. 화면 바인딩 키가 + * 저장 카테고리·기본값·서버 검증 세 지점과 일치하는지를 고정한다. 한 곳만 어긋나도 + * 저장은 성공하는데 값이 조용히 버려진다. + * + * @effects board_settings_purge_toggle_default_off, board_settings_purge_keys_declared + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import generalTab from '../../../layouts/admin/partials/admin_board_settings/_tab_general.json'; + +/** + * 모듈 루트(module.json 기준)를 위로 훑어 찾는다. + * + * @returns 모듈 루트 절대경로 + */ +function moduleRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'module.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('module.json 을 가진 모듈 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + for (const child of node.children ?? []) { + const found = findNode(child, predicate); + if (found) return found; + } + + return null; +} + +/** + * 이 화면을 렌더하는 admin 템플릿의 매니페스트가 선언한 컴포넌트 종류를 읽는다. + * + * 레이아웃 노드의 `type` 은 렌더러가 DOM 안전 필터링을 고르는 기준이라 + * 매니페스트 선언과 어긋나면 `editorAttrs` 가 제거된다(DynamicRenderer). + * + * @param name 컴포넌트 이름 + * @returns 매니페스트가 선언한 type + */ +function manifestType(name: string): string { + // 모듈 루트(modules/_bundled/sirsoft-board) 에서 저장소 루트로 두 단계 올라간다. + const repoRoot = path.dirname(path.dirname(path.dirname(moduleRoot()))); + const manifest = JSON.parse( + fs.readFileSync( + path.join(repoRoot, 'templates/_bundled/sirsoft-admin_basic/components.json'), + 'utf-8', + ), + ); + + for (const [kind, entries] of Object.entries<any>(manifest.components ?? {})) { + if ((entries as any[]).some((entry) => entry.name === name)) { + return kind; + } + } + + throw new Error(`매니페스트에 ${name} 선언이 없습니다.`); +} + +const section = findNode(generalTab, (n) => n.id === 'attachment_cleanup_section'); + +describe('게시판 첨부 정리 설정 — 계약', () => { + it('기본 설정 탭에 첨부 정리 섹션이 있다', () => { + expect(section).not.toBeNull(); + }); + + it('토글 노드의 type 이 매니페스트 선언과 같다', () => { + const toggle = findNode(section, (n) => n.name === 'Toggle'); + + // 어긋나면 렌더러가 editorAttrs 를 떼어내 이 노드만 레이아웃 편집기에서 표식을 잃는다. + expect(toggle.type).toBe(manifestType('Toggle')); + }); + + it('토글과 보존기간이 attachment_settings 카테고리 키를 바인딩한다', () => { + const toggle = findNode(section, (n) => n.name === 'Toggle'); + const input = findNode(section, (n) => n.name === 'Input'); + + expect(toggle.props.name).toBe('attachment_settings.purge_enabled'); + expect(input.props.name).toBe('attachment_settings.purge_retention_days'); + expect(input.props.min).toContain('attachment_purge_retention_days_min'); + expect(input.props.max).toContain('attachment_purge_retention_days_max'); + }); + + it('변경 시 form 상태와 hasChanges 를 함께 갱신한다', () => { + const toggle = findNode(section, (n) => n.name === 'Toggle'); + const action = toggle.actions.find((a: any) => a.type === 'change'); + + expect(action.params.hasChanges).toBe(true); + + // 값 갱신은 정적 점 경로 키로 한다. 중첩 객체(`form: { attachment_settings: {...} }`)로 + // 되돌리면 spread 누적이 되살아나 저장은 200 인데 값이 조용히 버려진다. + expect(action.params['form.attachment_settings.purge_enabled']).toBe('{{$event.target.checked}}'); + expect(action.params.form).toBeUndefined(); + + // 키 자체에 표현식을 쓰면 경로가 해석되지 않는다 (setState 규약). + Object.keys(action.params).forEach((key) => { + expect(key).not.toContain('{{'); + }); + }); + + it('기본값이 꺼짐이고 보존기간 기본이 30일이다 (defaults.json)', () => { + const defaults = JSON.parse( + fs.readFileSync(path.join(moduleRoot(), 'config/settings/defaults.json'), 'utf-8'), + ); + + expect(defaults._meta.categories).toContain('attachment_settings'); + expect(defaults.defaults.attachment_settings.purge_enabled).toBe(false); + expect(defaults.defaults.attachment_settings.purge_retention_days).toBe(30); + }); + + it('서버 검증이 같은 키를 받고 화면과 같은 한계값 출처를 쓴다', () => { + const request = fs.readFileSync( + path.join(moduleRoot(), 'src/Http/Requests/Admin/StoreBoardSettingsRequest.php'), + 'utf-8', + ); + const boardConfig = fs.readFileSync(path.join(moduleRoot(), 'config/board.php'), 'utf-8'); + + expect(request).toContain("'attachment_settings.purge_enabled' => ['nullable', 'boolean']"); + // 화면 입력과 저장 규칙이 같은 limits 키를 읽어야 "화면이 허용한 값인데 422" 가 생기지 않는다. + expect(request).toContain("\$limits['attachment_purge_retention_days_min']"); + expect(request).toContain("\$limits['attachment_purge_retention_days_max']"); + expect(boardConfig).toContain("'attachment_purge_retention_days_min' => 1"); + expect(boardConfig).toContain("'attachment_purge_retention_days_max' => 3650"); + }); + + it('기본 설정 탭 저장 payload 에 attachment_settings 가 실린다', () => { + // 화면에 필드가 있어도 저장 요청 본문에 카테고리가 빠지면 저장은 200 인데 값만 사라진다 + // (브라우저 실측 — 토글을 켜고 저장해도 파일에 반영되지 않음). + const settingsLayout = JSON.parse( + fs.readFileSync( + path.join(moduleRoot(), 'resources/layouts/admin/admin_board_settings.json'), + 'utf-8', + ), + ); + const raw = JSON.stringify(settingsLayout); + const generalBranchStart = raw.indexOf("if (tab === 'general')"); + const generalBranch = raw.slice(generalBranchStart, raw.indexOf('; }', generalBranchStart)); + + expect(generalBranchStart).toBeGreaterThan(-1); + expect(generalBranch).toContain('attachment_settings: form.attachment_settings || {}'); + }); + + it('서버가 attachment_settings 카테고리를 저장 대상으로 받는다', () => { + const request = fs.readFileSync( + path.join(moduleRoot(), 'src/Http/Requests/Admin/StoreBoardSettingsRequest.php'), + 'utf-8', + ); + const validCategories = request.match(/\$validCategories = \[[^\]]*\]/)?.[0] ?? ''; + + expect(validCategories).toContain("'attachment_settings'"); + }); + + it('스케줄이 등록되고 영구 정리 파트만 커맨드 내부에서 게이트된다', () => { + const manifest = fs.readFileSync(path.join(moduleRoot(), 'module.php'), 'utf-8'); + const command = fs.readFileSync( + path.join(moduleRoot(), 'src/Console/Commands/PruneAttachmentsCommand.php'), + 'utf-8', + ); + + expect(manifest).toContain('sirsoft-board:prune-attachments --scheduled'); + expect(command).toContain("module_setting('sirsoft-board', 'attachment_settings.purge_enabled', false)"); + }); +}); diff --git a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-form.test.tsx b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-form.test.tsx index d4ea78a9..16f3f47b 100644 --- a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-form.test.tsx +++ b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-form.test.tsx @@ -317,6 +317,30 @@ describe('admin_board_form/_tab_post.json', () => { expect(maxReplyDepthField.if).toContain('use_reply'); }); + it('reply_delete_policy_field 가 use_reply ON 조건부로 존재하고 block/cascade Select 를 렌더한다 (#573)', () => { + const policyField = findById(tabPost, 'reply_delete_policy_field'); + expect(policyField).toBeDefined(); + expect(policyField.if).toBeDefined(); + expect(policyField.if).toContain('use_reply'); + + const select = findById(policyField, 'reply_delete_policy_select'); + expect(select).toBeDefined(); + expect(select.name).toBe('Select'); + expect(select.props.name).toBe('reply_delete_policy'); + + const values = (select.props.options as Array<{ value: string }>).map((o) => o.value).sort(); + expect(values).toEqual(['block', 'cascade']); + }); + + it('reply_delete_policy 라벨/힌트/옵션의 ko 다국어 키가 실재한다 (#573)', () => { + const field = (koFormLang as any).fields?.reply_delete_policy; + expect(field).toBeDefined(); + expect(field.label).toBeTruthy(); + expect(field.hint).toBeTruthy(); + expect(field.options?.block).toBeTruthy(); + expect(field.options?.cascade).toBeTruthy(); + }); + it('row-stack 컨테이너 section_post_fields 에 use_comment 토글이 있다', () => { const fieldsContainer = findById(tabPost, 'section_post_fields'); expect(fieldsContainer).toBeDefined(); diff --git a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-settings.test.tsx b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-settings.test.tsx index a92c2ff7..8a594e2a 100644 --- a/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-settings.test.tsx +++ b/modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-settings.test.tsx @@ -727,6 +727,7 @@ describe('_tab_board_settings_reply.json - 답변글 하위 탭', () => { const fields = findFormFields(tabBoardSettingsReply); expect(fields).toContain('basic_defaults.use_reply'); expect(fields).toContain('basic_defaults.max_reply_depth'); + expect(fields).toContain('basic_defaults.reply_delete_policy'); }); it('답변글 깊이 필드가 항상 표시된다 (if 조건 없음)', () => { @@ -734,6 +735,34 @@ describe('_tab_board_settings_reply.json - 답변글 하위 탭', () => { expect(maxReplyDepth).toBeDefined(); expect(maxReplyDepth.if).toBeUndefined(); }); + + it('답글 삭제 정책 Select 가 block/cascade 옵션으로 렌더된다 (#573)', () => { + const policyField = findById(tabBoardSettingsReply, 'field_reply_delete_policy'); + expect(policyField).toBeDefined(); + expect(policyField.if).toBeUndefined(); + + const collect: any[] = []; + const walk = (node: any) => { + if (!node || typeof node !== 'object') return; + if (node.name === 'Select') collect.push(node); + for (const child of node.children ?? []) walk(child); + }; + walk(policyField); + + expect(collect).toHaveLength(1); + expect(collect[0].props.name).toBe('basic_defaults.reply_delete_policy'); + const values = (collect[0].props.options as Array<{ value: string }>).map((o) => o.value).sort(); + expect(values).toEqual(['block', 'cascade']); + }); + + it('답글 삭제 정책 ko 다국어 키(라벨/설명/옵션/일괄적용)가 실재한다 (#573)', () => { + const lang = koSettingsLang as any; + expect(lang.fields?.reply_delete_policy).toBeTruthy(); + expect(lang.fields?.descriptions?.reply_delete_policy).toBeTruthy(); + expect(lang.options?.reply_delete_policy?.block).toBeTruthy(); + expect(lang.options?.reply_delete_policy?.cascade).toBeTruthy(); + expect(lang.bulk_apply?.field_labels?.reply_delete_policy).toBeTruthy(); + }); }); // ============================================= @@ -1611,10 +1640,12 @@ describe('기본 설정 탭 (general) - 날짜 표시 방식', () => { expect(option).not.toBeNull(); // click 액션으로 date_display_format: "standard" 설정 + // 키는 점 경로다 — 중첩 객체로 넘기면 spread 누적이 생겨 저장 payload 가 어긋난다. const clickAction = option.actions?.find((a: any) => a.type === 'click'); expect(clickAction).toBeDefined(); expect(clickAction.handler).toBe('setState'); - expect(clickAction.params?.form?.display?.date_display_format).toBe('standard'); + expect(clickAction.params?.['form.display.date_display_format']).toBe('standard'); + expect(clickAction.params?.form).toBeUndefined(); }); it('relative 옵션 라디오 버튼이 존재한다', () => { @@ -1622,10 +1653,12 @@ describe('기본 설정 탭 (general) - 날짜 표시 방식', () => { expect(option).not.toBeNull(); // click 액션으로 date_display_format: "relative" 설정 + // 키는 점 경로다 — 중첩 객체로 넘기면 spread 누적이 생겨 저장 payload 가 어긋난다. const clickAction = option.actions?.find((a: any) => a.type === 'click'); expect(clickAction).toBeDefined(); expect(clickAction.handler).toBe('setState'); - expect(clickAction.params?.form?.display?.date_display_format).toBe('relative'); + expect(clickAction.params?.['form.display.date_display_format']).toBe('relative'); + expect(clickAction.params?.form).toBeUndefined(); }); it('라디오 버튼 name이 display.date_display_format이다', () => { diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin.json b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin.json index 1fa0071b..9938bb4f 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin.json @@ -926,7 +926,7 @@ "daily_report_limit_hint": "Max reports per user per day across all boards. 0 = no limit. (0–100)", "rejection_limit_count": "Rejection Limit (count)", "rejection_limit_days": "Rejection Limit Period (days)", - "rejection_limit_hint": "Reporting will be blocked when rejections exceed the limit within this period. 0 count = no limit. (Count: 0–50, Period: 1–365 days)", + "rejection_limit_hint": "Reporting is blocked once rejections within this period reach the limit. 0 count = no limit. (Count: 0–50, Period: 1–365 days)", "report_cooldown_seconds": "Report Cooldown (sec)", "report_cooldown_seconds_hint": "Minimum interval between consecutive reports. 0 = no limit. (0–3600 sec)", "notify_author_on_report_action": "Report Action Notification", diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/form.json b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/form.json index 6f819468..40fe8b8a 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/form.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/form.json @@ -110,6 +110,14 @@ "label": "Max Reply Depth", "hint": "Enter a value between {{min}} and {{max}}" }, + "reply_delete_policy": { + "label": "Reply Delete Policy", + "hint": "Set what happens when deleting a post that has replies", + "options": { + "block": "Block deletion", + "cascade": "Delete together" + } + }, "max_comment_depth": { "label": "Max Comment Depth", "hint": "Enter a value between {{min}} and {{max}}. Setting to 0 disables the nested comment feature." diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/settings.json b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/settings.json index c8e3f84c..79f55810 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/settings.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/en/admin/settings.json @@ -60,9 +60,15 @@ "report_notification": "Report Notification", "report_notification_description": "Configure which administrators receive notifications and how notifications are sent when a report is submitted.", "report_permissions": "Report Management Permissions", - "report_permissions_description": "Assign roles that can view the report list and process reports (approve, reject, or blind). Admin notifications for new reports are sent based on the 'Process Reports Permission' role." + "report_permissions_description": "Assign roles that can view the report list and process reports (approve, reject, or blind). Admin notifications for new reports are sent based on the 'Process Reports Permission' role.", + "attachment_cleanup": "Attachment Cleanup", + "attachment_cleanup_description": "Decide when the files of deleted attachments are destroyed. Temporary attachments uploaded while writing but never saved are cleaned up daily regardless of this setting." }, "fields": { + "attachment_purge_enabled": "Purge deleted attachments", + "attachment_purge_enabled_hint": "Off by default — the operator must turn it on for the daily automatic cleanup to run. When on, files and records of attachments deleted longer ago than the retention period are permanently removed. Cleanup commands run directly on the server are not affected by this setting.", + "attachment_purge_retention_days": "Deleted attachment retention (days)", + "attachment_purge_retention_days_hint": "Restoring a post from the trash also restores attachments within this period. Keep it aligned with your trash retention window. (1 ~ 3650 days)", "type": "Board Type", "secret_mode": "Secret Mode", "per_page": "Posts per Page (PC)", @@ -78,6 +84,7 @@ "max_content_length": "Maximum Content Length", "use_reply": "Enable Reply Posts", "max_reply_depth": "Max Reply Depth", + "reply_delete_policy": "Reply Delete Policy", "use_comment": "Enable Comments", "comment_order": "Comment Sort Order", "min_comment_length": "Min Comment Length", @@ -103,7 +110,7 @@ "daily_report_limit_hint": "Max reports per user per day across all boards. 0 = no limit. (0\u2013100)", "rejection_limit_count": "Rejection Limit (count)", "rejection_limit_days": "Rejection Limit Period (days)", - "rejection_limit_hint": "Reporting will be blocked when rejections exceed the limit within this period. 0 count = no limit. (Count: 0\u201350, Period: 1\u2013365 days)", + "rejection_limit_hint": "Reporting is blocked once rejections within this period reach the limit. 0 count = no limit. (Count: 0\u201350, Period: 1\u2013365 days)", "report_cooldown_seconds": "Report Cooldown (sec)", "report_cooldown_seconds_hint": "Minimum interval between consecutive reports. 0 = no limit. (0\u20133600 sec)", "notify_author_on_report_action": "Report Action Notification", @@ -166,6 +173,7 @@ "max_content_length": "Set the maximum character count for post content ({{min}}–{{max}})", "use_reply": "Must be ON to apply reply post feature to new boards.", "max_reply_depth": "Set the maximum nesting depth for replies ({{min}}–{{max}})", + "reply_delete_policy": "Set what happens when deleting a post that has replies", "blocked_keywords": "Posts or comments containing registered keywords will be blocked", "use_comment": "Must be ON to apply comment feature to new boards. Comment detail settings below will also apply.", "comment_order": "Set the default sort direction for comments", @@ -197,6 +205,10 @@ "enabled": "Optional", "always": "Always Secret" }, + "reply_delete_policy": { + "block": "Block deletion", + "cascade": "Delete together" + }, "order_by": { "created_at": "Latest", "view_count": "View Count", @@ -281,6 +293,7 @@ "use_comment": "Enable Comments", "use_reply": "Enable Replies", "max_reply_depth": "Max Reply Depth", + "reply_delete_policy": "Reply Delete Policy", "max_comment_depth": "Max Comment Depth", "comment_order": "Comment Sort Order", "show_view_count": "Show View Count", diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin.json b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin.json index 7ae527d5..577fa4ab 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin.json @@ -926,7 +926,7 @@ "daily_report_limit_hint": "하루(자정 기준) 전체 게시판 통틀어 신고할 수 있는 최대 횟수. 0이면 제한 없음. (0~100)", "rejection_limit_count": "반려 누적 제한 (건수)", "rejection_limit_days": "반려 누적 제한 기간 (일)", - "rejection_limit_hint": "이 기간 내 반려 건수 초과 시 신고 기능이 차단됩니다. 건수 0이면 제한 없음. (건수: 0~50, 기간: 1~365일)", + "rejection_limit_hint": "이 기간 내 반려 건수가 설정값에 도달하면 신고 기능이 차단됩니다. 건수 0이면 제한 없음. (건수: 0~50, 기간: 1~365일)", "report_cooldown_seconds": "연속 신고 쿨타임 (초)", "report_cooldown_seconds_hint": "연속 신고 최소 간격. 0이면 제한 없음. (0~3600초)", "notify_author_on_report_action": "신고 처리 알림", diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/form.json b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/form.json index eb7c3863..d1704af3 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/form.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/form.json @@ -110,6 +110,14 @@ "label": "답변글 최대 깊이", "hint": "{{min}}~{{max}} 사이의 값을 입력하세요" }, + "reply_delete_policy": { + "label": "답변글 삭제 방식", + "hint": "답변글이 달린 글을 삭제할 때의 동작을 설정합니다", + "options": { + "block": "삭제 차단", + "cascade": "함께 삭제" + } + }, "max_comment_depth": { "label": "대댓글 최대 깊이", "hint": "{{min}}~{{max}} 사이의 값을 입력하세요. 0으로 설정하면 대댓글 기능이 비활성화됩니다." diff --git a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/settings.json b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/settings.json index b1d38d1e..ba6ccf07 100644 --- a/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/settings.json +++ b/modules/_bundled/sirsoft-board/resources/lang/partial/ko/admin/settings.json @@ -60,9 +60,15 @@ "report_notification": "신고 알림", "report_notification_description": "신고 접수 시 알림을 받을 관리자와 알림 발송 방식을 설정합니다.", "report_permissions": "신고 관리 권한", - "report_permissions_description": "신고 목록 조회 및 신고 처리(승인·반려·블라인드)를 수행할 수 있는 역할을 지정합니다. 신고 접수 알림은 '신고 처리 권한'을 가진 역할 기준으로 발송됩니다." + "report_permissions_description": "신고 목록 조회 및 신고 처리(승인·반려·블라인드)를 수행할 수 있는 역할을 지정합니다. 신고 접수 알림은 '신고 처리 권한'을 가진 역할 기준으로 발송됩니다.", + "attachment_cleanup": "첨부파일 정리", + "attachment_cleanup_description": "삭제한 첨부파일의 물리 파일을 언제 파기할지 정합니다. 글쓰기 중 올렸다가 저장하지 않은 임시 첨부는 이 설정과 무관하게 매일 자동 정리됩니다." }, "fields": { + "attachment_purge_enabled": "삭제 첨부 영구 정리", + "attachment_purge_enabled_hint": "기본 꺼짐 — 운영자가 직접 켜야 매일 자동 정리가 동작합니다. 켜면 삭제된 지 보존기간이 지난 첨부의 파일과 기록을 영구 삭제합니다. 서버에서 직접 실행하는 정리 명령은 이 설정과 무관하게 동작합니다.", + "attachment_purge_retention_days": "삭제 첨부 보존기간 (일)", + "attachment_purge_retention_days_hint": "휴지통에서 게시글을 복원하면 이 기간 안의 첨부는 함께 복원됩니다. 휴지통 복원 가능 기간과 같게 유지하십시오. (1 ~ 3650 일)", "type": "게시판 유형", "secret_mode": "비밀글 모드", "per_page": "페이지당 게시글 수 (PC)", @@ -78,6 +84,7 @@ "max_content_length": "최대 게시글 글자 수", "use_reply": "답변글 사용", "max_reply_depth": "답변글 최대 깊이", + "reply_delete_policy": "답변글 삭제 방식", "use_comment": "댓글 사용", "comment_order": "댓글 정렬 순서", "min_comment_length": "최소 댓글 글자 수", @@ -103,7 +110,7 @@ "daily_report_limit_hint": "하루(자정 기준) 전체 게시판 통틀어 신고할 수 있는 최대 횟수. 0이면 제한 없음. (0~100)", "rejection_limit_count": "반려 누적 제한 (건수)", "rejection_limit_days": "반려 누적 제한 기간 (일)", - "rejection_limit_hint": "이 기간 내 반려 건수 초과 시 신고 기능이 차단됩니다. 건수 0이면 제한 없음. (건수: 0~50, 기간: 1~365일)", + "rejection_limit_hint": "이 기간 내 반려 건수가 설정값에 도달하면 신고 기능이 차단됩니다. 건수 0이면 제한 없음. (건수: 0~50, 기간: 1~365일)", "report_cooldown_seconds": "연속 신고 쿨타임 (초)", "report_cooldown_seconds_hint": "연속 신고 최소 간격. 0이면 제한 없음. (0~3600초)", "notify_author_on_report_action": "신고 처리 알림", @@ -166,6 +173,7 @@ "max_content_length": "게시글 본문의 최대 글자 수를 설정합니다 ({{min}}~{{max}})", "use_reply": "ON으로 설정해야 새 게시판에 답변글 기능이 적용됩니다.", "max_reply_depth": "답변글의 최대 중첩 깊이를 설정합니다 ({{min}}~{{max}})", + "reply_delete_policy": "답변글이 달린 글을 삭제할 때의 동작을 설정합니다", "blocked_keywords": "등록된 키워드가 포함된 게시글\/댓글은 작성이 차단됩니다", "use_comment": "ON으로 설정해야 새 게시판에 댓글 기능이 적용됩니다. 아래 댓글 세부 설정도 함께 적용됩니다.", "comment_order": "댓글의 기본 정렬 방향을 설정합니다", @@ -197,6 +205,10 @@ "enabled": "선택 가능", "always": "항상 비밀글" }, + "reply_delete_policy": { + "block": "삭제 차단", + "cascade": "함께 삭제" + }, "order_by": { "created_at": "최신순", "view_count": "조회수순", @@ -281,6 +293,7 @@ "use_comment": "댓글 사용", "use_reply": "답변글 사용", "max_reply_depth": "답변글 최대 깊이", + "reply_delete_policy": "답변글 삭제 방식", "max_comment_depth": "대댓글 최대 깊이", "comment_order": "댓글 정렬 순서", "show_view_count": "조회수 표시", diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_post_form.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_post_form.json index 0cdf2fc9..8d332ca3 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_post_form.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_post_form.json @@ -227,7 +227,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.posts.messages.save_failed", + "message": "{{error.message ?? '$t:sirsoft-board.admin.posts.messages.save_failed'}}", "icon": "triangle-exclamation" } } @@ -260,7 +260,7 @@ "name": "Icon", "props": { "name": "lock", - "className": "w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" + "className": "text-xl text-amber-600 dark:text-amber-400 flex-shrink-0" } }, { @@ -294,7 +294,7 @@ "name": "Icon", "props": { "name": "triangle-exclamation", - "className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" } }, { @@ -396,7 +396,7 @@ "name": "Icon", "props": { "name": "circle-dot", - "className": "w-3 h-3" + "className": "text-xs" } }, { @@ -523,7 +523,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.posts.messages.save_failed", + "message": "{{error.message ?? '$t:sirsoft-board.admin.posts.messages.save_failed'}}", "icon": "triangle-exclamation" } } diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_reports_index.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_reports_index.json index 1288dd10..fcb0e787 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_reports_index.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_reports_index.json @@ -2099,6 +2099,7 @@ "serverTotalPages": "{{reports?.data?.pagination?.last_page ?? null}}", "serverHasMorePages": "{{reports?.data?.pagination?.has_more_pages ?? false}}", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_global.selectedIds || []}}", "idField": "id", "showColumnSelector": false, diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_settings.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_settings.json index 5ad705ee..50b883ea 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_settings.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/admin_board_settings.json @@ -641,7 +641,7 @@ "target": "/api/modules/sirsoft-board/admin/settings", "params": { "method": "PUT", - "body": "{{(function() { const tab = _global.activeBoardSettingsTab || query.tab || 'general'; const form = _local.form || {}; if (tab === 'general') { return { _tab: 'general', display: form.display || {} }; } if (tab === 'report_policy') { return { _tab: 'report_policy', report_policy: form.report_policy || {}, spam_security: form.spam_security || {}, report_permissions: form.report_permissions || {} }; } if (tab === 'spam_security') { return { _tab: 'spam_security', spam_security: form.spam_security || {} }; } if (tab === 'seo') { return { _tab: 'seo', seo: form.seo || {} }; } if (tab === 'notification_definitions') { return { _tab: 'notifications', notifications: { channels: form.notifications?.channels || [] } }; } return { _tab: 'basic_defaults', basic_defaults: form.basic_defaults || {}, notifications: form.notifications || {} }; })()}}" + "body": "{{(function() { const tab = _global.activeBoardSettingsTab || query.tab || 'general'; const form = _local.form || {}; if (tab === 'general') { return { _tab: 'general', display: form.display || {}, attachment_settings: form.attachment_settings || {} }; } if (tab === 'report_policy') { return { _tab: 'report_policy', report_policy: form.report_policy || {}, spam_security: form.spam_security || {}, report_permissions: form.report_permissions || {} }; } if (tab === 'spam_security') { return { _tab: 'spam_security', spam_security: form.spam_security || {} }; } if (tab === 'seo') { return { _tab: 'seo', seo: form.seo || {} }; } if (tab === 'notification_definitions') { return { _tab: 'notifications', notifications: { channels: form.notifications?.channels || [] } }; } return { _tab: 'basic_defaults', basic_defaults: form.basic_defaults || {}, notifications: form.notifications || {} }; })()}}" }, "onSuccess": [ { @@ -697,7 +697,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.save_error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.save_error'}}" } } ] diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_form/_tab_post.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_form/_tab_post.json index d54430e0..2516f022 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_form/_tab_post.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_form/_tab_post.json @@ -489,6 +489,70 @@ } ] }, + { + "id": "reply_delete_policy_field", + "type": "basic", + "name": "Div", + "if": "{{_local.form.use_reply}}", + "props": { + "className": "" + }, + "children": [ + { + "id": "reply_delete_policy_label", + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-board.admin.form.fields.reply_delete_policy.label" + } + ] + }, + { + "id": "reply_delete_policy_select", + "type": "composite", + "name": "Select", + "props": { + "name": "reply_delete_policy", + "options": [ + { + "value": "cascade", + "label": "$t:sirsoft-board.admin.form.fields.reply_delete_policy.options.cascade" + }, + { + "value": "block", + "label": "$t:sirsoft-board.admin.form.fields.reply_delete_policy.options.block" + } + ], + "disabled": "{{_computed.isReadOnly}}", + "className": "" + } + }, + { + "id": "reply_delete_policy_hint", + "type": "basic", + "name": "P", + "props": { + "className": "form-hint" + }, + "text": "$t:sirsoft-board.admin.form.fields.reply_delete_policy.hint" + }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.reply_delete_policy}}", + "props": { + "className": "form-error-xs mt-1" + }, + "text": "{{_local.errors?.reply_delete_policy?.[0] ?? ''}}" + } + ] + }, { "id": "use_comment_toggle", "type": "basic", diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_bulk_apply_modal.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_bulk_apply_modal.json index be44282c..3b983317 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_bulk_apply_modal.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_bulk_apply_modal.json @@ -525,7 +525,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.bulk_apply_modal.error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.bulk_apply_modal.error'}}" } } ] diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_mail_template_edit.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_mail_template_edit.json index 6b17bf67..70800b9b 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_mail_template_edit.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_mail_template_edit.json @@ -633,7 +633,7 @@ { "handler": "toast", "params": { - "message": "$t:sirsoft-board.admin.settings.mail_templates.save_error", + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.mail_templates.save_error'}}", "type": "error" } } diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_notification_template_edit.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_notification_template_edit.json index c2519458..830721d2 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_notification_template_edit.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_modal_notification_template_edit.json @@ -969,7 +969,7 @@ { "handler": "toast", "params": { - "message": "$t:sirsoft-board.admin.settings.notification_definitions.save_error", + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.notification_definitions.save_error'}}", "type": "error" } } diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_board_settings_reply.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_board_settings_reply.json index 87376641..a3eb0ad1 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_board_settings_reply.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_board_settings_reply.json @@ -212,6 +212,91 @@ ] } ] + }, + { + "id": "field_reply_delete_policy", + "type": "basic", + "name": "Div", + "props": { + "className": "" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "flex-center gap-4" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "flex-start gap-2 text-body font-medium flex-1 cursor-pointer" + }, + "children": [ + { + "type": "basic", + "name": "Input", + "props": { + "type": "checkbox", + "className": "checkbox-aligned", + "checked": "{{(_local.bulkApplyFields ?? []).includes('reply_delete_policy')}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "bulkApplyFields": "{{(_local.bulkApplyFields ?? []).includes('reply_delete_policy') ? (_local.bulkApplyFields ?? []).filter(f => f !== 'reply_delete_policy') : [...(_local.bulkApplyFields ?? []), 'reply_delete_policy']}}" + } + } + ] + }, + { + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-board.admin.settings.fields.reply_delete_policy" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-field-hint" + }, + "text": "$t:sirsoft-board.admin.settings.fields.descriptions.reply_delete_policy" + } + ] + } + ] + }, + { + "type": "composite", + "name": "Select", + "props": { + "name": "basic_defaults.reply_delete_policy", + "options": [ + { + "value": "cascade", + "label": "$t:sirsoft-board.admin.settings.options.reply_delete_policy.cascade" + }, + { + "value": "block", + "label": "$t:sirsoft-board.admin.settings.options.reply_delete_policy.block" + } + ], + "className": "w-48 flex-shrink-0 text-sm", + "disabled": "{{_computed.isReadOnly}}" + } + } + ] + } + ] } ] } diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_general.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_general.json index 908e4a24..6bf3092a 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_general.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_general.json @@ -81,14 +81,8 @@ "handler": "setState", "params": { "target": "local", - "form": { - "...": "{{_local.form}}", - "display": { - "...": "{{_local.form?.display}}", - "date_display_format": "standard" - } - }, - "hasChanges": true + "hasChanges": true, + "form.display.date_display_format": "standard" } } ], @@ -110,14 +104,8 @@ "handler": "setState", "params": { "target": "local", - "form": { - "...": "{{_local.form}}", - "display": { - "...": "{{_local.form?.display}}", - "date_display_format": "standard" - } - }, - "hasChanges": true + "hasChanges": true, + "form.display.date_display_format": "standard" } } ] @@ -170,14 +158,8 @@ "handler": "setState", "params": { "target": "local", - "form": { - "...": "{{_local.form}}", - "display": { - "...": "{{_local.form?.display}}", - "date_display_format": "relative" - } - }, - "hasChanges": true + "hasChanges": true, + "form.display.date_display_format": "relative" } } ], @@ -199,14 +181,8 @@ "handler": "setState", "params": { "target": "local", - "form": { - "...": "{{_local.form}}", - "display": { - "...": "{{_local.form?.display}}", - "date_display_format": "relative" - } - }, - "hasChanges": true + "hasChanges": true, + "form.display.date_display_format": "relative" } } ] @@ -251,6 +227,128 @@ ] } ] + }, + { + "id": "attachment_cleanup_section", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card mt-6" + }, + "children": [ + { + "type": "basic", + "name": "H2", + "props": { + "className": "card-title" + }, + "text": "$t:sirsoft-board.admin.settings.sections.attachment_cleanup" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "card-description" + }, + "text": "$t:sirsoft-board.admin.settings.sections.attachment_cleanup_description" + }, + { + "id": "attachment_purge_enabled_field", + "type": "basic", + "name": "Div", + "props": { + "className": "mt-4" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:sirsoft-board.admin.settings.fields.attachment_purge_enabled" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-tertiary" + }, + "text": "$t:sirsoft-board.admin.settings.fields.attachment_purge_enabled_hint" + }, + { + "type": "composite", + "name": "Toggle", + "props": { + "name": "attachment_settings.purge_enabled", + "disabled": "{{_computed.isReadOnly}}", + "checked": "{{!!_local.form?.attachment_settings?.purge_enabled}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "hasChanges": true, + "form.attachment_settings.purge_enabled": "{{$event.target.checked}}" + } + } + ] + } + ] + }, + { + "id": "attachment_purge_retention_days_field", + "type": "basic", + "name": "Div", + "props": { + "className": "mt-4" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:sirsoft-board.admin.settings.fields.attachment_purge_retention_days" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-tertiary" + }, + "text": "$t:sirsoft-board.admin.settings.fields.attachment_purge_retention_days_hint" + }, + { + "type": "basic", + "name": "Input", + "props": { + "type": "number", + "name": "attachment_settings.purge_retention_days", + "min": "{{_local?.form?._meta?.limits?.attachment_purge_retention_days_min ?? 1}}", + "max": "{{_local?.form?._meta?.limits?.attachment_purge_retention_days_max ?? 3650}}", + "className": "input w-40", + "disabled": "{{_computed.isReadOnly}}", + "value": "{{_local.form?.attachment_settings?.purge_retention_days ?? 30}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "hasChanges": true, + "form.attachment_settings.purge_retention_days": "{{$event.target.value}}" + } + } + ] + } + ] + } + ] } ] } diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_identity_policies.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_identity_policies.json index 31eedfd4..96ca0804 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_identity_policies.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_identity_policies.json @@ -569,7 +569,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? '$t:admin.identity.policies.toggle_failed'}}" } }, { @@ -906,7 +906,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? '$t:admin.identity.policies.toggle_failed'}}" } }, { diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_seo.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_seo.json index 162a2c9f..f9db2dda 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_seo.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_seo.json @@ -620,7 +620,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error'}}" } } ] @@ -637,7 +637,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error'}}" } } ] @@ -654,7 +654,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.seo.seo_friendly.clear_cache_error'}}" } } ] diff --git a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_spam_security.json b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_spam_security.json index 3f749d81..7b4a3f4e 100644 --- a/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_spam_security.json +++ b/modules/_bundled/sirsoft-board/resources/layouts/admin/partials/admin_board_settings/_tab_spam_security.json @@ -336,7 +336,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-board.admin.settings.fields.clear_cache_error" + "message": "{{error.message ?? '$t:sirsoft-board.admin.settings.fields.clear_cache_error'}}" } } ] diff --git a/modules/_bundled/sirsoft-board/src/Console/Commands/PruneAttachmentsCommand.php b/modules/_bundled/sirsoft-board/src/Console/Commands/PruneAttachmentsCommand.php new file mode 100644 index 00000000..6fd0d62f --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Console/Commands/PruneAttachmentsCommand.php @@ -0,0 +1,180 @@ +<?php + +namespace Modules\Sirsoft\Board\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Board\Services\AttachmentService; + +/** + * 게시판 첨부파일 정리 커맨드 + * + * 두 파트를 순차 실행합니다. + * + * 1. 임시 첨부 정리 (항상 실행) — 글쓰기 폼에서 올린 뒤 저장 없이 이탈해 남은 첨부. + * `temp_key` 가 남아 있으면 끝내 연결되지 않은 폼 세션 부산물이라 오탐 여지가 없다. + * 2. 소프트 삭제 첨부 영구 정리 (운영자 옵트인) — 삭제된 첨부는 휴지통 복원 대비로 + * 파일이 남는다. 사용자 파일을 실제로 파기하므로 기본 꺼짐이며, 스케줄 호출 + * (`--scheduled`)에서 설정을 false 폴백으로 확인한 뒤에만 수행한다. + * + * 토글이 막는 것은 **자동(스케줄) 실행**이다. `--scheduled` 없이 서버에서 직접 실행하면 + * 토글과 무관하게 파기한다 — 운영자가 의도해서 친 명령이기 때문이다. + * + * @example php artisan sirsoft-board:prune-attachments --dry-run + * @example php artisan sirsoft-board:prune-attachments --temp-days=1 + */ +class PruneAttachmentsCommand extends Command +{ + /** + * 커맨드 이름 및 시그니처 + * + * @var string + */ + protected $signature = 'sirsoft-board:prune-attachments + {--dry-run : 실제 삭제 없이 대상 건수만 확인} + {--limit=500 : 한 번에 처리할 최대 건수} + {--temp-days=2 : 임시 첨부 보존기간(일)} + {--purge-days= : 소프트 삭제 첨부 보존기간(일) 재정의 — 미지정 시 모듈 설정값} + {--scheduled : 스케줄러 호출 표시 — 영구 정리 토글이 꺼져 있으면 그 파트만 건너뜀 (이 옵션 없이 직접 실행하면 토글과 무관하게 파기)}'; + + /** + * 커맨드 설명 + * + * @var string + */ + protected $description = '방치된 임시 첨부와 보존기간이 지난 삭제 첨부를 정리합니다.'; + + /** + * @param AttachmentService $attachmentService 첨부 서비스 + */ + public function __construct( + protected AttachmentService $attachmentService + ) { + parent::__construct(); + } + + /** + * 커맨드 실행 + * + * @return int 종료 코드 + */ + public function handle(): int + { + $limit = max(1, (int) $this->option('limit')); + $isDryRun = (bool) $this->option('dry-run'); + + $this->pruneTemp($limit, $isDryRun); + $this->purgeSoftDeleted($limit, $isDryRun); + + return Command::SUCCESS; + } + + /** + * 임시 첨부 정리 파트 (항상 실행). + * + * @param int $limit 최대 처리 건수 + * @param bool $isDryRun 판정만 수행할지 여부 + */ + private function pruneTemp(int $limit, bool $isDryRun): void + { + $days = (int) $this->option('temp-days'); + + // 보존기간 0 이하 = 방금 올린 임시 첨부까지 지우게 되므로 차단한다. + if ($days < 1) { + $this->info('임시 첨부 보존기간이 1일 미만이어서 임시 첨부 정리를 수행하지 않았습니다.'); + + return; + } + + $result = $this->attachmentService->pruneTempUploads($days, $limit, $isDryRun); + + if ($isDryRun) { + $this->info("[DRY RUN] 보존기간({$days}일) 경과 미연결 임시 첨부: {$result['scanned']}건"); + + return; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 임시 첨부 %d건 중 %d건을 삭제했습니다. (실패 %d건)', + $days, + $result['scanned'], + $result['deleted'], + $result['failed'], + )); + + Log::info('PruneAttachmentsCommand: 임시 첨부 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + ] + $result); + } + + /** + * 소프트 삭제 첨부 영구 정리 파트 (운영자 옵트인). + * + * @param int $limit 최대 처리 건수 + * @param bool $isDryRun 판정만 수행할지 여부 + */ + private function purgeSoftDeleted(int $limit, bool $isDryRun): void + { + if ($this->option('scheduled') && ! $this->isPurgeEnabled()) { + $this->info('삭제 첨부 영구 정리가 꺼져 있어 실행하지 않았습니다. (attachment_settings.purge_enabled = false)'); + + return; + } + + $days = $this->resolvePurgeDays(); + + if ($days < 1) { + $this->info('삭제 첨부 보존기간이 1일 미만이어서 영구 정리를 수행하지 않았습니다.'); + + return; + } + + $result = $this->attachmentService->purgeSoftDeleted($days, $limit, $isDryRun); + + if ($isDryRun) { + $this->info("[DRY RUN] 보존기간({$days}일) 경과 삭제 첨부: {$result['scanned']}건"); + + return; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 삭제 첨부 %d건 중 %d건을 영구 삭제했습니다. (실패 %d건)', + $days, + $result['scanned'], + $result['deleted'], + $result['failed'], + )); + + Log::info('PruneAttachmentsCommand: 삭제 첨부 영구 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + ] + $result); + } + + /** + * 영구 정리 토글을 false 폴백으로 조회합니다. + * + * @return bool 영구 정리 활성 여부 + */ + private function isPurgeEnabled(): bool + { + return (bool) module_setting('sirsoft-board', 'attachment_settings.purge_enabled', false); + } + + /** + * 영구 정리 보존기간을 해석합니다 (옵션 > 모듈 설정 > 기본 30일). + * + * @return int 보존기간(일) + */ + private function resolvePurgeDays(): int + { + $option = $this->option('purge-days'); + + if ($option !== null && $option !== '') { + return (int) $option; + } + + return (int) module_setting('sirsoft-board', 'attachment_settings.purge_retention_days', 30); + } +} diff --git a/modules/_bundled/sirsoft-board/src/Enums/ReplyDeletePolicy.php b/modules/_bundled/sirsoft-board/src/Enums/ReplyDeletePolicy.php new file mode 100644 index 00000000..a6a44a4d --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Enums/ReplyDeletePolicy.php @@ -0,0 +1,80 @@ +<?php + +namespace Modules\Sirsoft\Board\Enums; + +/** + * 답글 달린 글의 삭제 정책 + * + * 답글(parent_id 자식 글)이 남아 있는 글을 삭제하려 할 때의 동작을 게시판별로 정의합니다. + * 기본값은 Cascade — 댓글·첨부가 이미 연쇄 소프트 삭제되는 기존 의미론과 일치하며, + * 복원 시 연쇄분만 선택 복원되어 되돌릴 수 있습니다. + */ +enum ReplyDeletePolicy: string +{ + /** + * 답글이 살아 있는 동안 원글 삭제 차단 + */ + case Block = 'block'; + + /** + * 원글 삭제 시 답글도 함께 소프트 삭제 (기본값) + */ + case Cascade = 'cascade'; + + /** + * 모든 값 배열 반환 + * + * @return array<string> + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } + + /** + * 유효한 값인지 확인 + * + * @param string $value 검증할 값 + * @return bool 유효 여부 + */ + public static function isValid(string $value): bool + { + return in_array($value, self::values(), true); + } + + /** + * 번역 키를 반환합니다. + * + * @return string 번역 키 + */ + public function labelKey(): string + { + return 'sirsoft-board::enums.reply_delete_policy.'.$this->value; + } + + /** + * 다국어 라벨 반환 + * + * @return string 번역된 라벨 + */ + public function label(): string + { + return __($this->labelKey()); + } + + /** + * 모든 값을 배열로 반환 (value, label 포함) + * + * @return array<array{value: string, label: string}> + */ + public static function toArray(): array + { + return array_map( + fn (self $item) => [ + 'value' => $item->value, + 'label' => $item->label(), + ], + self::cases() + ); + } +} diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/AttachmentLimitExceededException.php b/modules/_bundled/sirsoft-board/src/Exceptions/AttachmentLimitExceededException.php index fea8598d..afeb2b67 100644 --- a/modules/_bundled/sirsoft-board/src/Exceptions/AttachmentLimitExceededException.php +++ b/modules/_bundled/sirsoft-board/src/Exceptions/AttachmentLimitExceededException.php @@ -52,6 +52,29 @@ class AttachmentLimitExceededException extends Exception return $this->attempted; } + /** + * 다국어 메시지 키를 반환합니다. + * + * 응답을 만들 때 이미 번역된 `getMessage()` 대신 이 키를 넘긴다 — 번역문을 키로 + * 해석하려 하면 실패해 원문이 그대로 노출된다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return 'sirsoft-board::messages.errors.attachment_limit_exceeded'; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return ['limit' => $this->limit, 'attempted' => $this->attempted]; + } + /** * 컨트롤러 catch 와 동일한 422 응답으로 렌더링합니다. * @@ -59,6 +82,11 @@ class AttachmentLimitExceededException extends Exception */ public function render(): JsonResponse { - return ResponseHelper::error($this->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return ResponseHelper::error( + $this->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $this->getMessageParams() + ); } } diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/BoardTypeOperationException.php b/modules/_bundled/sirsoft-board/src/Exceptions/BoardTypeOperationException.php new file mode 100644 index 00000000..396aab70 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Exceptions/BoardTypeOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Board\Exceptions; + +use Exception; + +/** + * 게시판 유형 조작 실패 예외 클래스 + * + * 사용 중인 유형이거나 기본 유형이어서 삭제할 수 없는 경우에 발생합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 422 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class BoardTypeOperationException extends Exception +{ + /** + * 게시판 유형 조작 실패 예외 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-board::messages.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/CommentDepthExceededException.php b/modules/_bundled/sirsoft-board/src/Exceptions/CommentDepthExceededException.php index 2f2c4046..17e520ca 100644 --- a/modules/_bundled/sirsoft-board/src/Exceptions/CommentDepthExceededException.php +++ b/modules/_bundled/sirsoft-board/src/Exceptions/CommentDepthExceededException.php @@ -48,4 +48,24 @@ class CommentDepthExceededException extends Exception { return $this->attempted; } + + /** + * 다국어 메시지 키를 반환합니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return 'sirsoft-board::validation.comment.depth.exceeded'; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return ['max' => $this->limit]; + } } diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/MenuAlreadyExistsException.php b/modules/_bundled/sirsoft-board/src/Exceptions/MenuAlreadyExistsException.php index 46af07d5..754d4240 100644 --- a/modules/_bundled/sirsoft-board/src/Exceptions/MenuAlreadyExistsException.php +++ b/modules/_bundled/sirsoft-board/src/Exceptions/MenuAlreadyExistsException.php @@ -11,19 +11,55 @@ use Exception; */ class MenuAlreadyExistsException extends Exception { + /** + * 기본 안내 문구의 다국어 키 + */ + private const DEFAULT_MESSAGE_KEY = 'sirsoft-board::messages.boards.menu_already_exists'; + + /** + * 메시지 키 (직접 문구를 받은 경우 null) + */ + private ?string $messageKey; + /** * 메뉴 중복 예외 생성자 * - * @param string $message 예외 메시지 + * @param string $message 예외 메시지 (생략 시 기본 키로 해석) * @param int $code 예외 코드 * @param \Throwable|null $previous 이전 예외 */ public function __construct(string $message = '', int $code = 409, ?\Throwable $previous = null) { - if (empty($message)) { - $message = __('sirsoft-board::messages.boards.menu_already_exists'); + $this->messageKey = $message === '' ? self::DEFAULT_MESSAGE_KEY : null; + + if ($message === '') { + $message = __(self::DEFAULT_MESSAGE_KEY); } parent::__construct($message, $code, $previous); } -} \ No newline at end of file + + /** + * 다국어 메시지 키를 반환합니다. + * + * 호출부가 직접 문구를 넘긴 경우엔 키가 없으므로 기본 키를 돌려준다 — 응답에는 + * 언제나 키가 전달되어야 하며, 이미 번역된 문구가 키 자리에 들어가면 해석에 + * 실패해 원문이 그대로 노출된다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey ?? self::DEFAULT_MESSAGE_KEY; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return []; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/PostHasRepliesException.php b/modules/_bundled/sirsoft-board/src/Exceptions/PostHasRepliesException.php new file mode 100644 index 00000000..6e45b043 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Exceptions/PostHasRepliesException.php @@ -0,0 +1,43 @@ +<?php + +namespace Modules\Sirsoft\Board\Exceptions; + +use Exception; + +/** + * 답글 보유 글 삭제 차단 예외 + * + * 게시판 설정 `reply_delete_policy` 가 'block' 인 게시판에서, 살아 있는 답글이 + * 달린 글을 삭제하려 할 때 발생합니다. 판정은 before_delete 훅 발화 전에 수행되어 + * 차단 시 부수효과가 전혀 남지 않습니다. + * + * 훅 경유 삭제(이커머스 문의 연동 등 `cascade_replies` 옵션 전달)는 시스템 생성 + * 답글을 함께 정리해야 하므로 이 정책의 적용을 받지 않습니다. + */ +class PostHasRepliesException extends Exception +{ + public function __construct() + { + parent::__construct(__('sirsoft-board::validation.post.delete.has_replies')); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return 'sirsoft-board::validation.post.delete.has_replies'; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return []; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Exceptions/PostNotCommentableException.php b/modules/_bundled/sirsoft-board/src/Exceptions/PostNotCommentableException.php index bce1a68f..3dbb57c8 100644 --- a/modules/_bundled/sirsoft-board/src/Exceptions/PostNotCommentableException.php +++ b/modules/_bundled/sirsoft-board/src/Exceptions/PostNotCommentableException.php @@ -18,13 +18,13 @@ class PostNotCommentableException extends Exception { /** * @param string $reason 거절 사유 (blinded | deleted) - * @param string $message 사용자에게 보일 메시지 + * @param string $messageKey 사용자에게 보일 문구의 다국어 키 */ public function __construct( private string $reason, - string $message, + private string $messageKey, ) { - parent::__construct($message); + parent::__construct(__($messageKey)); } /** @@ -34,7 +34,7 @@ class PostNotCommentableException extends Exception */ public static function blinded(): self { - return new self('blinded', __('sirsoft-board::messages.comment.post_blinded')); + return new self('blinded', 'sirsoft-board::messages.comment.post_blinded'); } /** @@ -44,7 +44,27 @@ class PostNotCommentableException extends Exception */ public static function deleted(): self { - return new self('deleted', __('sirsoft-board::messages.comment.post_deleted')); + return new self('deleted', 'sirsoft-board::messages.comment.post_deleted'); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return []; } /** diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php index db7a56da..6e81fc50 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/AttachmentController.php @@ -84,7 +84,12 @@ class AttachmentController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (BoardNotFoundException $e) { return $this->error('sirsoft-board::messages.boards.not_found', 404); } catch (\Exception $e) { @@ -113,7 +118,7 @@ class AttachmentController extends AdminBaseController } // 삭제 (Service에서 처리) - $result = $this->attachmentService->delete($slug, $id); + $result = $this->attachmentService->delete($slug, $id, 'admin'); if (! $result) { return $this->error('sirsoft-board::messages.attachment.delete_failed', 500); @@ -144,7 +149,7 @@ class AttachmentController extends AdminBaseController // FileUploader가 [{id, order}] 형태로 전송 → [ID => order] 매핑으로 변환 $orders = collect($validated['order'])->pluck('order', 'id')->all(); - $result = $this->attachmentService->reorder($slug, $orders); + $result = $this->attachmentService->reorder($slug, $orders, 'admin'); if (! $result) { return $this->error('sirsoft-board::messages.attachment.reorder_failed', 500); diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardController.php index b4b1bee9..87e77d81 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardController.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; use Modules\Sirsoft\Board\Exceptions\MenuAlreadyExistsException; +use Modules\Sirsoft\Board\Http\Requests\Admin\IndexBoardRequest; use Modules\Sirsoft\Board\Http\Requests\StoreBoardRequest; use Modules\Sirsoft\Board\Http\Requests\UpdateBoardRequest; use Modules\Sirsoft\Board\Http\Resources\BoardCollection; @@ -46,14 +47,13 @@ class BoardController extends AdminBaseController /** * 게시판 목록을 조회합니다. * - * @param Request $request HTTP 요청 + * @param IndexBoardRequest $request 목록 조회 요청 (정렬 컬럼·페이지 크기 화이트리스트 검증) * @return JsonResponse 게시판 목록 응답 */ - // audit:allow controller-base-request-injection reason: GET 목록 조회. all()로 필터·페이징 파라미터만 읽음 (검증 불필요) - public function index(Request $request): JsonResponse + public function index(IndexBoardRequest $request): JsonResponse { try { - $boards = $this->boardService->getBoards($request->all()); + $boards = $this->boardService->getBoards($request->validated()); // BoardCollection의 withPermissions 사용 $collection = new BoardCollection($boards); @@ -335,7 +335,7 @@ class BoardController extends AdminBaseController return $this->success('sirsoft-board::messages.boards.menu_added_success', ['menu' => $menu]); } catch (MenuAlreadyExistsException $e) { - return $this->error($e->getMessage(), $e->getCode()); + return $this->error($e->getMessageKey(), $e->getCode(), null, $e->getMessageParams()); } catch (ModelNotFoundException $e) { return $this->notFound('sirsoft-board::messages.boards.error_404'); } catch (AccessDeniedHttpException $e) { diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardSettingsController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardSettingsController.php index d498c4a9..2c89c05c 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardSettingsController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardSettingsController.php @@ -2,6 +2,7 @@ namespace Modules\Sirsoft\Board\Http\Controllers\Admin; +use App\Extension\HookManager; use App\Helpers\PermissionHelper; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; @@ -117,6 +118,10 @@ class BoardSettingsController extends AdminBaseController $this->settingsService->syncReportPermissionRoles($request->input('report_permissions')); } + // 코어 모듈 설정 저장 훅 — SEO 캐시 무효화 등 코어/타 확장 리스너가 구독한다. + // 훅 의미가 "관리자가 설정을 저장했다" 이므로 서비스가 아니라 여기서 발화한다. + HookManager::doAction('core.module_settings.after_save', 'sirsoft-board', $settings, $result); + $updatedSettings = $this->settingsService->getAllSettings(); $updatedSettings['report_permissions'] = $this->settingsService->getReportPermissionRoles(); diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardTypeController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardTypeController.php index d1ac36f9..d3d24a77 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardTypeController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardTypeController.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Board\Http\Controllers\Admin; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Board\Exceptions\BoardTypeOperationException; use Modules\Sirsoft\Board\Http\Requests\StoreBoardTypeRequest; use Modules\Sirsoft\Board\Http\Requests\UpdateBoardTypeRequest; use Modules\Sirsoft\Board\Http\Resources\BoardTypeResource; @@ -32,7 +33,7 @@ class BoardTypeController extends AdminBaseController } /** - * @param StoreBoardTypeRequest $request + * @param StoreBoardTypeRequest $request * @return JsonResponse */ public function store(StoreBoardTypeRequest $request): JsonResponse @@ -47,8 +48,8 @@ class BoardTypeController extends AdminBaseController } /** - * @param UpdateBoardTypeRequest $request - * @param int $id + * @param UpdateBoardTypeRequest $request + * @param int $id * @return JsonResponse */ public function update(UpdateBoardTypeRequest $request, int $id): JsonResponse @@ -66,7 +67,7 @@ class BoardTypeController extends AdminBaseController } /** - * @param int $id + * @param int $id * @return JsonResponse */ public function destroy(int $id): JsonResponse @@ -77,8 +78,13 @@ class BoardTypeController extends AdminBaseController return $this->success('sirsoft-board::messages.board_type.deleted'); } catch (ModelNotFoundException) { return $this->notFound('sirsoft-board::messages.board_type.not_found'); + } catch (BoardTypeOperationException $e) { + // 삭제 불가 사유(사용 중/기본 유형)를 전용 키로 안내 — 예외 메시지 원문을 + // 키 자리에 넘기면(종전 동작) 키 해석에 실패해 원문이 그대로 노출된다. + return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); } catch (\Exception $e) { - return $this->error($e->getMessage(), 422); + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return $this->error('sirsoft-board::messages.errors.operation_failed', 500); } } } diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php index 20943d3b..1201e600 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/CommentController.php @@ -114,10 +114,10 @@ class CommentController extends AdminBaseController return $this->notFound('sirsoft-board::messages.boards.not_found'); } catch (CommentDepthExceededException $e) { // 요청 단계 검증을 우회해 Service 관문에 걸린 경우 — 사용자 입력 문제이므로 422 - return $this->error($e->getMessage(), 422); + return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); } catch (PostNotCommentableException $e) { // 블라인드·삭제된 게시글 — 서버 오류가 아니라 게시글 상태 문제이므로 422 + 사유 전달 - return $this->error($e->getMessage(), 422); + return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); } catch (\Exception $e) { return $this->error('sirsoft-board::messages.comment.create_failed', 500, $e->getMessage()); } diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/PostController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/PostController.php index 0af8fe5a..dee8eb51 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/PostController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/PostController.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Exceptions\AttachmentLimitExceededException; use Modules\Sirsoft\Board\Exceptions\BoardNotFoundException; +use Modules\Sirsoft\Board\Exceptions\PostHasRepliesException; use Modules\Sirsoft\Board\Exceptions\PostNotFoundException; use Modules\Sirsoft\Board\Http\Requests\Admin\PostFormDataRequest; use Modules\Sirsoft\Board\Http\Requests\Admin\PostFormMetaRequest; @@ -197,7 +198,12 @@ class PostController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (\Exception $e) { return $this->error('sirsoft-board::messages.posts.create_failed', 500, $e->getMessage()); } @@ -252,7 +258,12 @@ class PostController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (ModelNotFoundException $e) { throw new PostNotFoundException($id); } catch (AccessDeniedHttpException $e) { @@ -305,6 +316,9 @@ class PostController extends AdminBaseController 'sirsoft-board::messages.posts.delete_success', new PostResource($deletedPost) ); + } catch (PostHasRepliesException $e) { + // 답글 삭제 정책(block): 살아있는 답글이 있어 삭제 거부 — 입력(대상 선택) 문제이므로 422 + return $this->error($e->getMessageKey(), 422); } catch (ModelNotFoundException $e) { throw new PostNotFoundException($id); } catch (AccessDeniedHttpException $e) { diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/ReportController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/ReportController.php index 8d0c4bbb..640722b1 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/ReportController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/ReportController.php @@ -70,7 +70,6 @@ class ReportController extends AdminBaseController 'board_id' => $request->input('board_id'), 'reported_at_from' => $request->input('reported_at_from'), 'reported_at_to' => $request->input('reported_at_to'), - 'sort_by' => $request->input('sort_by'), 'sort_order' => $request->input('sort_order'), ]; diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/AttachmentController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/AttachmentController.php index f51568c6..2e3f877d 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/AttachmentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/AttachmentController.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Board\Http\Controllers\User; use App\Http\Controllers\Api\Base\PublicBaseController; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Board\Exceptions\AttachmentLimitExceededException; @@ -83,11 +84,17 @@ class AttachmentController extends PublicBaseController * 이미지 파일만 미리보기를 제공합니다. * 비회원도 이미지를 볼 수 있습니다. * + * 비밀글·삭제글 첨부는 서비스의 콘텐츠 상태 게이트가 차단하되, 게이트를 통과한 + * 응답 직렬화(PostResource)가 발급한 한시 서명 URL 은 동등한 자격으로 허용한다 — + * <img> 는 Authorization 헤더를 실을 수 없는 렌더 경로이기 때문이다. + * + * @param Request $request HTTP 요청 객체 (한시 서명 검증용) * @param string $slug 게시판 슬러그 * @param string $hash 첨부파일 해시 (12자) * @return BinaryFileResponse|Response|JsonResponse 이미지 응답 또는 에러 응답 */ - public function preview(string $slug, string $hash): BinaryFileResponse|Response|JsonResponse + // audit:allow controller-base-request-injection reason: GET 이미지 서빙. 요청 URL 의 한시 서명 검증(hasValidSignature)만 수행. 검증할 body 없음(slug/hash 는 라우트 파라미터) + public function preview(Request $request, string $slug, string $hash): BinaryFileResponse|Response|JsonResponse { try { // 게시판 존재 여부 확인 @@ -105,8 +112,12 @@ class AttachmentController extends PublicBaseController return $this->badRequest(__('sirsoft-board::messages.attachment.not_image')); } - // 파일 정보 조회 - $fileInfo = $this->attachmentService->getFileInfo($slug, $attachment->id); + // 파일 정보 조회 (유효 서명은 콘텐츠 상태 게이트 통과 자격의 위임) + $fileInfo = $this->attachmentService->getFileInfo( + $slug, + $attachment->id, + signatureVerified: $request->hasValidSignature(absolute: false) + ); if (! $fileInfo) { return $this->notFound(__('sirsoft-board::messages.attachment.not_found')); @@ -178,7 +189,12 @@ class AttachmentController extends PublicBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (BoardNotFoundException $e) { return $this->notFound(__('sirsoft-board::messages.boards.not_found')); } catch (\Exception $e) { diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/BoardController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/BoardController.php index d2eb02c1..a6ed7784 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/BoardController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/BoardController.php @@ -45,8 +45,9 @@ class BoardController extends PublicBaseController */ public function index(IndexBoardRequest $request): JsonResponse { - // 활성화된 게시판만 조회 - $boards = $this->boardService->getActiveBoards(); + // 활성 게시판 중 호출자가 열람 가능한 게시판만 조회 — 디렉토리와 내장 최근글이 + // 비공개 게시판·글 제목의 우회 노출로가 되지 않게 한다 (최근글·인기글과 동일 기준) + $boards = $this->boardService->getReadableActiveBoards(); // limit 파라미터 (0이면 최신글 미포함, 최대 10개) $recentPostsLimit = $request->recentPostsLimit(); diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php index bcd13397..77e22db5 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/CommentController.php @@ -10,10 +10,13 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Modules\Sirsoft\Board\Exceptions\CommentDepthExceededException; use Modules\Sirsoft\Board\Exceptions\PostNotCommentableException; +use Modules\Sirsoft\Board\Http\Requests\DestroyCommentRequest; use Modules\Sirsoft\Board\Http\Requests\StoreCommentRequest; use Modules\Sirsoft\Board\Http\Requests\UpdateCommentRequest; use Modules\Sirsoft\Board\Http\Requests\VerifyCommentPasswordRequest; use Modules\Sirsoft\Board\Http\Resources\CommentResource; +use Modules\Sirsoft\Board\Http\Resources\PostResource; +use Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface; use Modules\Sirsoft\Board\Services\BoardService; use Modules\Sirsoft\Board\Services\CommentService; @@ -32,7 +35,8 @@ class CommentController extends PublicBaseController */ public function __construct( private CommentService $commentService, - private BoardService $boardService + private BoardService $boardService, + private PostRepositoryInterface $postRepository ) { parent::__construct(); } @@ -57,6 +61,32 @@ class CommentController extends PublicBaseController return $this->error('sirsoft-board::messages.comments.comments_disabled', 403); } + // 비밀글 댓글 게이팅(KVE-2026-1914): 부모 게시글이 비밀글이면 열람 권한이 없는 + // 요청에는 댓글 목록을 노출하지 않는다(게시글 상세와 동일 정책, SecretContentGate SSoT). + $post = $this->postRepository->find($slug, $postId); + + // 부모 글을 못 읽으면 막는다(fail-closed). `find` 는 슬러그로 게시판을 먼저 찾는데 + // 그 게시판이 없으면 null 을 돌려주므로, 통과시키면 비밀 게이트가 있어야 할 자리에서 + // 무게이트로 댓글 목록이 나간다. + if (! $post) { + return $this->error('sirsoft-board::messages.posts.not_found', 404); + } + + if ($post->is_secret && ! PostResource::canViewSecretForPost($post)) { + return $this->success( + 'sirsoft-board::messages.comments.index_success', + CommentResource::collection([]) + ); + } + + // 이미 조회한 부모 post 를 CommentResource 로 전달한다(KVE-2026-1914 이중 방어 A-4b). + // Resource 는 이 인스턴스를 재사용해 (a) 2차 비밀 게이트를 댓글당 lazy-load 없이 + // 재확인하고 (b) toArray 의 slug 도출도 재사용한다 — 목록의 댓글당 board_posts + // 조회(N+1)를 제거한다. 컨트롤러가 SSoT 로 부모 post 를 쥐고 있으므로 추가 쿼리 0. + if ($post) { + request()->attributes->set('sirsoft_board_parent_post', $post); + } + $comments = $this->commentService->getCommentsByPostId($slug, $postId); return $this->success( @@ -118,10 +148,10 @@ class CommentController extends PublicBaseController return $this->error('sirsoft-board::messages.boards.not_found', 404); } catch (CommentDepthExceededException $e) { // 요청 단계 검증을 우회해 Service 관문에 걸린 경우 — 사용자 입력 문제이므로 422 - return $this->error($e->getMessage(), 422); + return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); } catch (PostNotCommentableException $e) { // 블라인드·삭제된 게시글 — 서버 오류가 아니라 게시글 상태 문제이므로 422 + 사유 전달 - return $this->error($e->getMessage(), 422); + return $this->error($e->getMessageKey(), 422, null, $e->getMessageParams()); } catch (\Exception $e) { return $this->error('sirsoft-board::messages.comment.create_failed', 500); } @@ -160,12 +190,22 @@ class CommentController extends PublicBaseController $slug ); + // 비회원 댓글: 평문 비밀번호 재전송 대신 검증 토큰으로도 본인 확인 (게시글과 동형) + if (! $canUpdate && $request->filled('verification_token')) { + $canUpdate = $this->commentService->consumeCommentVerifyToken( + $slug, + $commentId, + (string) $request->input('verification_token') + ); + } + if (! $canUpdate) { return $this->forbidden('sirsoft-board::messages.comment.update_forbidden'); } - // 검증된 필드만 반영 (미검증 입력의 대량 할당 차단). password는 검증용이므로 제거 - $data = collect($request->validated())->except('password')->toArray(); + // 검증된 필드만 반영 (미검증 입력의 대량 할당 차단). + // password/verification_token 은 본인 확인용이므로 저장 데이터에서 제거 + $data = collect($request->validated())->except(['password', 'verification_token'])->toArray(); $updatedComment = $this->commentService->updateComment($slug, $commentId, $data, $postId); return $this->successWithResource( @@ -182,12 +222,13 @@ class CommentController extends PublicBaseController /** * 댓글을 삭제합니다. * + * @param DestroyCommentRequest $request 댓글 삭제 요청 (비회원 password 형식 검증) * @param string $slug 게시판 slug * @param int $postId 게시글 ID * @param int $commentId 댓글 ID * @return JsonResponse 댓글 삭제 결과 응답 */ - public function destroy(string $slug, int $postId, int $commentId): JsonResponse + public function destroy(DestroyCommentRequest $request, string $slug, int $postId, int $commentId): JsonResponse { try { $board = $this->boardService->getBoardBySlug($slug, checkScope: false); @@ -203,8 +244,8 @@ class CommentController extends PublicBaseController // 경로의 게시글에 속한 댓글만 조회 (교차 게시글 접근 차단) $comment = $this->commentService->getComment($slug, $commentId, $postId); - // 비회원인 경우 password 파라미터 필요 - $password = request()->input('password'); + // 비회원인 경우 password 파라미터 필요 (형식 검증은 DestroyCommentRequest — 배열 주입 422 차단) + $password = $request->validated('password'); // 권한 확인 (Service에서 처리) $canDelete = $this->commentService->canDelete( @@ -214,6 +255,16 @@ class CommentController extends PublicBaseController $slug ); + // 비회원 댓글: 평문 비밀번호 재전송 대신 검증 토큰으로도 본인 확인 (게시글과 동형) + $verificationToken = $request->validated('verification_token'); + if (! $canDelete && ! empty($verificationToken)) { + $canDelete = $this->commentService->consumeCommentVerifyToken( + $slug, + $commentId, + $verificationToken + ); + } + if (! $canDelete) { return $this->forbidden('sirsoft-board::messages.comment.delete_forbidden'); } @@ -259,16 +310,18 @@ class CommentController extends PublicBaseController return $this->error('sirsoft-board::messages.comment.invalid_password', 401); } - // 검증 성공 시 임시 토큰 생성 (프론트엔드에서 로컬 스토리지에 저장) + // 검증 성공 시 1회용 토큰을 발급하고 캐시에 저장한다. + // (게시글과 동형 — update/destroy 가 이 토큰을 소비해 평문 비밀번호 재전송을 대체) $verificationToken = Str::random(32); + $tokenResult = $this->commentService->storeCommentVerifyToken($slug, $commentId, $verificationToken); return $this->success( 'sirsoft-board::messages.comment.password_verified', [ 'verified' => true, 'comment_id' => $commentId, - 'verification_token' => $verificationToken, - 'expires_at' => now()->addHours(1)->toIso8601String(), // 1시간 유효 + 'verification_token' => $tokenResult['token'], + 'expires_at' => $tokenResult['expires_at'], ] ); } catch (ModelNotFoundException) { diff --git a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/PostController.php b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/PostController.php index 0be7a176..0c7baf31 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Controllers/User/PostController.php +++ b/modules/_bundled/sirsoft-board/src/Http/Controllers/User/PostController.php @@ -17,7 +17,9 @@ use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Enums\SecretMode; use Modules\Sirsoft\Board\Exceptions\AttachmentLimitExceededException; use Modules\Sirsoft\Board\Exceptions\BoardNotFoundException; +use Modules\Sirsoft\Board\Exceptions\PostHasRepliesException; use Modules\Sirsoft\Board\Exceptions\PostNotFoundException; +use Modules\Sirsoft\Board\Http\Requests\User\DestroyPostRequest; use Modules\Sirsoft\Board\Http\Requests\User\StorePostRequest; use Modules\Sirsoft\Board\Http\Requests\User\UpdatePostRequest; use Modules\Sirsoft\Board\Http\Requests\User\VerifyGuestPasswordRequest; @@ -400,7 +402,12 @@ class PostController extends PublicBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (BoardNotFoundException $e) { throw $e; } catch (\Exception $e) { @@ -455,7 +462,12 @@ class PostController extends PublicBaseController ); } catch (AttachmentLimitExceededException $e) { // 게시판 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (AccessDeniedHttpException $e) { return $this->error('auth.scope_denied', 403); } catch (ModelNotFoundException $e) { @@ -470,13 +482,15 @@ class PostController extends PublicBaseController /** * 게시글을 삭제합니다. * - * @param Request $request HTTP 요청 + * 비회원 소유권 확인용 password/verification_token 형식 검증은 DestroyPostRequest 가 + * 담당한다 (배열 주입 422 차단). 소유권 판정 자체는 canModifyPost 가 수행한다. + * + * @param DestroyPostRequest $request 게시글 삭제 요청 * @param string $slug 게시판 슬러그 * @param string|int $id 게시글 ID * @return JsonResponse 삭제 결과 응답 */ - // audit:allow controller-base-request-injection reason: DELETE 단건 삭제. force_delete 불리언 플래그만 input()으로 읽음 (검증 불필요) - public function destroy(Request $request, string $slug, string|int $id): JsonResponse + public function destroy(DestroyPostRequest $request, string $slug, string|int $id): JsonResponse { $id = (int) $id; @@ -499,6 +513,9 @@ class PostController extends PublicBaseController $this->postService->deletePost($slug, $id, 'user'); return $this->success('sirsoft-board::messages.posts.delete_success'); + } catch (PostHasRepliesException $e) { + // 답글 삭제 정책(block): 살아있는 답글이 있어 삭제 거부 — 입력(대상 선택) 문제이므로 422 + return $this->error($e->getMessageKey(), 422); } catch (AccessDeniedHttpException $e) { return $this->error('auth.scope_denied', 403); } catch (ModelNotFoundException $e) { diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/BulkApplySettingsRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/BulkApplySettingsRequest.php index 1e38bbfe..7046884a 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/BulkApplySettingsRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/BulkApplySettingsRequest.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Board\Http\Requests\Admin; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; use Modules\Sirsoft\Board\Http\Requests\Concerns\ReadsBoardLimits; use Modules\Sirsoft\Board\Models\Board; @@ -30,6 +31,7 @@ class BulkApplySettingsRequest extends FormRequest 'use_comment', 'use_reply', 'max_reply_depth', + 'reply_delete_policy', 'max_comment_depth', 'comment_order', 'show_view_count', @@ -117,6 +119,7 @@ class BulkApplySettingsRequest extends FormRequest 'override_values.per_page' => ['nullable', 'integer', "min:{$limits['per_page_min']}", "max:{$limits['per_page_max']}"], 'override_values.per_page_mobile' => ['nullable', 'integer', "min:{$limits['per_page_min']}", "max:{$limits['per_page_max']}"], 'override_values.max_reply_depth' => ['nullable', 'integer', "min:{$limits['max_reply_depth_min']}", "max:{$limits['max_reply_depth_max']}"], + 'override_values.reply_delete_policy' => ['nullable', 'string', Rule::in(ReplyDeletePolicy::values())], 'override_values.max_comment_depth' => ['nullable', 'integer', "min:{$limits['max_comment_depth_min']}", "max:{$limits['max_comment_depth_max']}"], 'override_values.min_title_length' => ['nullable', 'integer', "min:{$limits['min_title_length_min']}", "max:{$limits['min_title_length_max']}"], 'override_values.max_title_length' => ['nullable', 'integer', "min:{$limits['max_title_length_min']}", "max:{$limits['max_title_length_max']}"], diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexBoardRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexBoardRequest.php new file mode 100644 index 00000000..c2132e7b --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexBoardRequest.php @@ -0,0 +1,58 @@ +<?php + +namespace Modules\Sirsoft\Board\Http\Requests\Admin; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 관리자 게시판 목록 조회 요청을 검증합니다. + * + * 정렬 컬럼·페이지 크기를 닫힌 집합으로 제한합니다 — 요청 값이 그대로 + * orderBy 로 흐르면 스키마 노출·비인덱스 정렬 DoS 표면이 됩니다. + * `name` 은 JSON 다국어 컬럼이라 정렬 대상에서 제외합니다. + * 권한은 라우트 permission 미들웨어 체인이 담당합니다 (authorize true 고정 규정). + */ +class IndexBoardRequest extends FormRequest +{ + /** + * 요청 권한 — permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 전 입력 데이터 정규화 + * + * 정렬 방향을 소문자로 정규화합니다 (asc/ASC 혼용 클라이언트 수용). + */ + protected function prepareForValidation(): void + { + if (is_string($this->input('sort_order'))) { + $this->merge(['sort_order' => strtolower($this->input('sort_order'))]); + } + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'page' => ['nullable', 'integer', 'min:1'], + // 게시판은 운영자 등록 설정성 테이블 — 전체 목록 셀렉트 소비처 + // (게시판 설정 999 / 쇼핑몰 환경설정 문의 카드 200)가 이 상한에 묶인다. + // 100 으로 조이면 두 화면 진입이 422 로 깨진다 (BoardIndexSortTest 회귀 고정). + 'per_page' => ['nullable', 'integer', 'min:1', 'max:999'], + 'type' => ['nullable', 'string', 'max:50'], + 'search' => ['nullable', 'string', 'max:100'], + 'sort_by' => ['nullable', 'string', 'in:id,slug,type,is_active,posts_count,comments_count,created_at,updated_at'], + 'sort_order' => ['nullable', 'string', 'in:asc,desc'], + ]; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexReportRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexReportRequest.php index fb82b9a8..22894892 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexReportRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/IndexReportRequest.php @@ -40,7 +40,7 @@ class IndexReportRequest extends FormRequest 'board_id' => ['nullable', 'integer'], 'reported_at_from' => ['nullable', 'string'], 'reported_at_to' => ['nullable', 'string'], - 'sort_by' => ['nullable', 'string'], + // sort_by 는 받지 않는다 — 저장소가 last_reported_at 고정 정렬 (死규칙 정리) 'sort_order' => ['nullable', 'string', 'in:asc,desc'], 'per_page' => ['nullable', 'integer'], ]; diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/StoreBoardSettingsRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/StoreBoardSettingsRequest.php index 15f81a67..eafcf38b 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/StoreBoardSettingsRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/Admin/StoreBoardSettingsRequest.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Board\Http\Requests\Admin; use App\Models\Role; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; use Modules\Sirsoft\Board\Http\Requests\Concerns\ReadsBoardLimits; /** @@ -58,6 +59,7 @@ class StoreBoardSettingsRequest extends FormRequest 'spam_security.comment_cooldown_seconds', 'spam_security.report_cooldown_seconds', 'spam_security.view_count_cache_ttl', + 'attachment_settings.purge_retention_days', ]; $booleanFields = [ @@ -73,6 +75,7 @@ class StoreBoardSettingsRequest extends FormRequest 'seo.seo_boards', 'seo.seo_board', 'seo.seo_post_detail', + 'attachment_settings.purge_enabled', ]; $data = $this->all(); @@ -172,6 +175,7 @@ class StoreBoardSettingsRequest extends FormRequest 'basic_defaults.use_comment' => ['nullable', 'boolean'], 'basic_defaults.use_reply' => ['nullable', 'boolean'], 'basic_defaults.max_reply_depth' => ['nullable', 'integer', "min:{$maxReplyDepthMin}", "max:{$maxReplyDepthMax}"], + 'basic_defaults.reply_delete_policy' => ['nullable', 'string', Rule::in(ReplyDeletePolicy::values())], 'basic_defaults.max_comment_depth' => ['nullable', 'integer', "min:{$maxCommentDepthMin}", "max:{$maxCommentDepthMax}"], 'basic_defaults.comment_order' => ['nullable', 'string', 'in:ASC,DESC'], 'basic_defaults.show_view_count' => ['nullable', 'boolean'], @@ -262,6 +266,11 @@ class StoreBoardSettingsRequest extends FormRequest 'seo.seo_boards' => ['nullable', 'boolean'], 'seo.seo_board' => ['nullable', 'boolean'], 'seo.seo_post_detail' => ['nullable', 'boolean'], + + // 첨부 정리 정책 — 사용자 파일을 파기하므로 보존기간 하한을 서버가 강제한다. + // 상한(3650일)은 "사실상 정리하지 않음" 과 구분되지 않는 값을 저장하지 않기 위함이다. + 'attachment_settings.purge_enabled' => ['nullable', 'boolean'], + 'attachment_settings.purge_retention_days' => ['nullable', 'integer', 'min:'.$limits['attachment_purge_retention_days_min'], 'max:'.$limits['attachment_purge_retention_days_max']], ]; } @@ -311,7 +320,7 @@ class StoreBoardSettingsRequest extends FormRequest public function validatedSettings(): array { $validated = $this->validated(); - $validCategories = ['basic_defaults', 'report_policy', 'spam_security', 'display', 'seo', 'notifications']; + $validCategories = ['basic_defaults', 'report_policy', 'spam_security', 'display', 'seo', 'notifications', 'attachment_settings']; return array_filter( $validated, diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/BlindCommentRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/BlindCommentRequest.php index 7bdc21dd..b7efa253 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/BlindCommentRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/BlindCommentRequest.php @@ -39,7 +39,7 @@ class BlindCommentRequest extends FormRequest public function messages(): array { return [ - 'reason.min' => __('sirsoft-board::validation.blind.reason.min'), + // reason 규칙에 min 이 없으므로 min 메시지는 매핑하지 않는다 (死매핑 정리 — lang 키 자체는 유지) 'reason.max' => __('sirsoft-board::validation.blind.reason.max'), ]; } diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/BlindPostRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/BlindPostRequest.php index 4a46c791..6d392945 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/BlindPostRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/BlindPostRequest.php @@ -39,7 +39,7 @@ class BlindPostRequest extends FormRequest public function messages(): array { return [ - 'reason.min' => __('sirsoft-board::validation.blind.reason.min'), + // reason 규칙에 min 이 없으므로 min 메시지는 매핑하지 않는다 (死매핑 정리 — lang 키 자체는 유지) 'reason.max' => __('sirsoft-board::validation.blind.reason.max'), ]; } diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/Concerns/ReadsBoardLimits.php b/modules/_bundled/sirsoft-board/src/Http/Requests/Concerns/ReadsBoardLimits.php index 8323a609..248380f3 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/Concerns/ReadsBoardLimits.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/Concerns/ReadsBoardLimits.php @@ -67,6 +67,9 @@ trait ReadsBoardLimits 'new_display_hours_min' => 0, 'new_display_hours_max' => 720, + + 'attachment_purge_retention_days_min' => 1, + 'attachment_purge_retention_days_max' => 3650, ]; /** diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/DestroyCommentRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/DestroyCommentRequest.php new file mode 100644 index 00000000..83086a49 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/DestroyCommentRequest.php @@ -0,0 +1,51 @@ +<?php + +namespace Modules\Sirsoft\Board\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 댓글 삭제 요청을 검증합니다. + * + * 비회원 댓글 삭제 시 본문 password 를 받는다 — 종전에는 컨트롤러가 전역 + * `request()->input('password')` 로 읽어(저장소 유일 사례) 배열 주입이 그대로 + * 비교 로직에 유입될 수 있었다. 소유권 판정 자체는 CommentService::canDelete 가 + * 담당하므로 여기서는 형식만 닫는다 (형제 store/update/verifyPassword 와 대칭). + */ +class DestroyCommentRequest extends FormRequest +{ + /** + * 요청 권한 — 소유권/권한 판정은 Service(canDelete)가 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'password' => ['nullable', 'string', 'min:4', 'max:20'], + 'verification_token' => ['nullable', 'string', 'max:255'], + ]; + } + + /** + * 검증 오류 메시지에 쓰일 필드 표시명을 반환합니다. + * + * @return array<string, string> 필드별 표시명 + */ + public function attributes(): array + { + return [ + 'password' => __('sirsoft-board::validation.attributes.comment.password'), + ]; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/RestorePostRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/RestorePostRequest.php index 38c7ff38..3a65281c 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/RestorePostRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/RestorePostRequest.php @@ -45,4 +45,16 @@ class RestorePostRequest extends FormRequest 'reason.max' => __('sirsoft-board::validation.restore.reason.max'), ]; } + + /** + * 검증 오류 메시지에 쓰일 필드 표시명을 반환합니다 (RestoreCommentRequest 와 대칭). + * + * @return array<string, string> 필드별 표시명 + */ + public function attributes(): array + { + return [ + 'reason' => __('sirsoft-board::validation.attributes.restore.reason'), + ]; + } } diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/StoreBoardRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/StoreBoardRequest.php index f67978bc..3221d9ef 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/StoreBoardRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/StoreBoardRequest.php @@ -10,6 +10,7 @@ use App\Rules\TranslatableField; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; use Modules\Sirsoft\Board\Http\Requests\Concerns\ReadsBoardLimits; use Modules\Sirsoft\Board\Http\Requests\Concerns\ValidatesLengthRange; use Modules\Sirsoft\Board\Rules\BoardTypeValidationRule; @@ -54,6 +55,7 @@ class StoreBoardRequest extends FormRequest 'use_comment' => $settings['use_comment'] ?? true, 'use_reply' => $settings['use_reply'] ?? true, 'max_reply_depth' => $settings['max_reply_depth'] ?? 5, + 'reply_delete_policy' => $settings['reply_delete_policy'] ?? ReplyDeletePolicy::Cascade->value, 'max_comment_depth' => $settings['max_comment_depth'] ?? 10, 'use_file_upload' => $settings['use_file_upload'] ?? false, 'comment_order' => $settings['comment_order'] ?? 'ASC', @@ -207,6 +209,7 @@ class StoreBoardRequest extends FormRequest // 답글/대댓글 깊이 제한 'max_reply_depth' => ['nullable', 'integer', "min:{$maxReplyDepthMin}", "max:{$maxReplyDepthMax}"], + 'reply_delete_policy' => ['required', 'string', Rule::in(ReplyDeletePolicy::values())], 'max_comment_depth' => ['nullable', 'integer', "min:{$maxCommentDepthMin}", "max:{$maxCommentDepthMax}"], // 알림 설정 diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateBoardRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateBoardRequest.php index d2e37eb8..992a83e6 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateBoardRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateBoardRequest.php @@ -10,6 +10,7 @@ use App\Rules\TranslatableField; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; use Modules\Sirsoft\Board\Http\Requests\Concerns\ReadsBoardLimits; use Modules\Sirsoft\Board\Http\Requests\Concerns\ValidatesLengthRange; use Modules\Sirsoft\Board\Rules\BoardTypeValidationRule; @@ -207,6 +208,7 @@ class UpdateBoardRequest extends FormRequest // 답글/대댓글 깊이 제한 'max_reply_depth' => ['sometimes', 'nullable', 'integer', "min:{$maxReplyDepthMin}", "max:{$maxReplyDepthMax}"], + 'reply_delete_policy' => ['sometimes', 'nullable', 'string', Rule::in(ReplyDeletePolicy::values())], 'max_comment_depth' => ['sometimes', 'nullable', 'integer', "min:{$maxCommentDepthMin}", "max:{$maxCommentDepthMax}"], // 알림 설정 diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateCommentRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateCommentRequest.php index 302054c7..b8f25b0e 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateCommentRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/UpdateCommentRequest.php @@ -6,7 +6,7 @@ use App\Extension\HookManager; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Modules\Sirsoft\Board\Enums\PostStatus; -use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; use Modules\Sirsoft\Board\Rules\BlockedKeywordsRule; /** @@ -32,7 +32,7 @@ class UpdateCommentRequest extends FormRequest public function rules(): array { $slug = $this->route('slug'); - $board = Board::where('slug', $slug)->first(); + $board = app(BoardRepositoryInterface::class)->findBySlug($slug); if (! $board) { return []; @@ -41,6 +41,10 @@ class UpdateCommentRequest extends FormRequest // 비회원 여부 확인 (request()->user()를 사용해야 PermissionMiddleware에서 설정한 사용자를 인식) $isGuest = ! $this->user(); + // 검증 토큰(verify-password 로 발급)이 있으면 평문 비밀번호 재전송을 요구하지 않는다. + // (게시글 수정 경로와 동형 — 토큰이 본인 확인을 대체하며, 컨트롤러가 1회 소비한다) + $hasVerificationToken = $this->filled('verification_token'); + // 금지 키워드 목록 가져오기 (게시글과 동일하게 게시판 설정 기준) $blockedKeywords = $board->blocked_keywords ?? []; @@ -54,8 +58,9 @@ class UpdateCommentRequest extends FormRequest ], 'is_secret' => ['boolean'], 'status' => ['nullable', 'string', Rule::in(PostStatus::values())], - // 비회원인 경우 비밀번호 필수 (수정 권한 검증용) - 'password' => [$isGuest ? 'required' : 'nullable', 'string', 'min:4', 'max:20'], + // 비회원인 경우 비밀번호 필수 (수정 권한 검증용). 단, 검증 토큰이 있으면 선택. + 'password' => [$isGuest && ! $hasVerificationToken ? 'required' : 'nullable', 'string', 'min:4', 'max:20'], + 'verification_token' => ['nullable', 'string', 'max:255'], ]; // 훅: 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 필터 제공 diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/User/DestroyPostRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/User/DestroyPostRequest.php new file mode 100644 index 00000000..fbf934bb --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/User/DestroyPostRequest.php @@ -0,0 +1,51 @@ +<?php + +namespace Modules\Sirsoft\Board\Http\Requests\User; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 게시글 삭제 요청을 검증합니다. + * + * 비회원 게시글 삭제 시 본문 password 또는 verification_token 을 받는다 — + * 종전에는 무검증으로 읽혀 배열 주입이 비교 로직에 유입될 수 있었다. + * 소유권 판정 자체는 컨트롤러(canModifyPost)가 담당하므로 여기서는 형식만 닫는다. + */ +class DestroyPostRequest extends FormRequest +{ + /** + * 요청 권한 — 소유권/권한 판정은 컨트롤러(canModifyPost)가 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'password' => ['nullable', 'string', 'min:4'], + 'verification_token' => ['nullable', 'string', 'max:255'], + 'force_delete' => ['nullable', 'boolean'], + ]; + } + + /** + * 검증 오류 메시지에 쓰일 필드 표시명을 반환합니다. + * + * @return array<string, string> 필드별 표시명 + */ + public function attributes(): array + { + return [ + 'password' => __('sirsoft-board::validation.attributes.post.password'), + ]; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/User/StorePostRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/User/StorePostRequest.php index 7c53f6a8..a989ad79 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/User/StorePostRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/User/StorePostRequest.php @@ -11,8 +11,8 @@ use Illuminate\Validation\Rule; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Http\Requests\Concerns\ResolvesAllowedExtensions; use Modules\Sirsoft\Board\Http\Requests\Concerns\ValidatesAttachmentCount; -use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Models\Post; +use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; use Modules\Sirsoft\Board\Rules\BlockedKeywordsRule; use Modules\Sirsoft\Board\Rules\CooldownRule; use Modules\Sirsoft\Board\Rules\ParentPostValidationRule; @@ -40,6 +40,28 @@ class StorePostRequest extends FormRequest return true; } + /** + * 검증 전 입력값을 정규화합니다. + * + * 비밀글 여부를 문자열 "true"/"false" 로 보내는 클라이언트도 수용하도록 + * boolean 으로 정규화합니다. (해석 불가값은 유지 → boolean 규칙이 422 처리) + */ + protected function prepareForValidation(): void + { + $merge = []; + + if ($this->has('is_secret')) { + $normalized = filter_var($this->input('is_secret'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($normalized !== null) { + $merge['is_secret'] = $normalized; + } + } + + if ($merge) { + $this->merge($merge); + } + } + /** * 요청에 적용할 검증 규칙 * @@ -48,7 +70,7 @@ class StorePostRequest extends FormRequest public function rules(): array { $slug = $this->route('slug'); - $board = Board::where('slug', $slug)->first(); + $board = app(BoardRepositoryInterface::class)->findBySlug((string) $slug); // 게시판이 없으면 모듈 환경설정 기본값 사용 $defaults = g7_module_settings('sirsoft-board', 'basic_defaults', []); @@ -225,7 +247,7 @@ class StorePostRequest extends FormRequest public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator) { - $board = Board::where('slug', $this->route('slug'))->first(); + $board = app(BoardRepositoryInterface::class)->findBySlug((string) $this->route('slug')); $this->validateAttachmentTotal($validator, $board); }); } diff --git a/modules/_bundled/sirsoft-board/src/Http/Requests/User/UpdatePostRequest.php b/modules/_bundled/sirsoft-board/src/Http/Requests/User/UpdatePostRequest.php index ce75e6ec..ff4b7a87 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Requests/User/UpdatePostRequest.php +++ b/modules/_bundled/sirsoft-board/src/Http/Requests/User/UpdatePostRequest.php @@ -8,7 +8,7 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Http\Requests\Concerns\ValidatesAttachmentCount; -use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; use Modules\Sirsoft\Board\Rules\BlockedKeywordsRule; /** @@ -33,6 +33,28 @@ class UpdatePostRequest extends FormRequest return true; } + /** + * 검증 전 입력값을 정규화합니다. + * + * 비밀글 여부를 문자열 "true"/"false" 로 보내는 클라이언트도 수용하도록 + * boolean 으로 정규화합니다. (해석 불가값은 유지 → boolean 규칙이 422 처리) + */ + protected function prepareForValidation(): void + { + $merge = []; + + if ($this->has('is_secret')) { + $normalized = filter_var($this->input('is_secret'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($normalized !== null) { + $merge['is_secret'] = $normalized; + } + } + + if ($merge) { + $this->merge($merge); + } + } + /** * 요청에 적용할 검증 규칙 * @@ -41,7 +63,7 @@ class UpdatePostRequest extends FormRequest public function rules(): array { $slug = $this->route('slug'); - $board = Board::where('slug', $slug)->first(); + $board = app(BoardRepositoryInterface::class)->findBySlug((string) $slug); if (! $board) { return []; @@ -81,6 +103,9 @@ class UpdatePostRequest extends FormRequest 'attachment_ids.*' => ['integer', 'min:1'], // temp_key는 게시판 존재 여부와 무관하게 항상 허용 'temp_key' => ['nullable', 'string', 'max:64'], + // 비회원 소유권 확인용 — 판정은 컨트롤러(canModifyPost), 여기서는 형식만 닫는다 (배열 주입 422) + 'password' => ['nullable', 'string', 'min:4'], + 'verification_token' => ['nullable', 'string', 'max:255'], ]; // 훅: 모듈/플러그인이 validation rules를 동적으로 추가할 수 있도록 필터 제공 @@ -134,7 +159,7 @@ class UpdatePostRequest extends FormRequest public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator) { - $board = Board::where('slug', $this->route('slug'))->first(); + $board = app(BoardRepositoryInterface::class)->findBySlug((string) $this->route('slug')); $postId = $this->route('id'); $this->validateAttachmentTotal($validator, $board, is_numeric($postId) ? (int) $postId : null); }); diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/AttachmentResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/AttachmentResource.php index f615e51d..be8d8795 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Resources/AttachmentResource.php +++ b/modules/_bundled/sirsoft-board/src/Http/Resources/AttachmentResource.php @@ -4,6 +4,8 @@ namespace Modules\Sirsoft\Board\Http\Resources; use App\Http\Resources\BaseApiResource; use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Modules\Sirsoft\Board\Models\Attachment; /** * 첨부파일 API 리소스 @@ -13,6 +15,35 @@ use Illuminate\Http\Request; */ class AttachmentResource extends BaseApiResource { + /** + * 서명 preview URL 발급 대상 게시판 슬러그 (null 이면 무서명 공개 URL). + * + * 비밀글·삭제글처럼 무서명 preview 가 게이트에 차단되는 게시글의 첨부를, + * 게이트를 통과한 응답(PostResource 상세 직렬화)에 한해 <img> 렌더 가능한 + * 한시 서명 URL 로 직렬화하기 위한 플래그다. + */ + private ?string $signedPreviewSlug = null; + + /** + * 첨부파일 목록을 리소스 배열로 변환합니다. + * + * @param iterable<int, Attachment>|null $attachments 첨부파일 목록 + * @param string|null $signedPreviewSlug 서명 preview URL 발급 대상 슬러그 (비밀글·삭제글 전용) + * @return array<int, self> + */ + public static function collectionFor($attachments, ?string $signedPreviewSlug = null): array + { + return Collection::make($attachments ?? []) + ->map(function ($attachment) use ($signedPreviewSlug) { + $resource = new self($attachment); + $resource->signedPreviewSlug = $signedPreviewSlug; + + return $resource; + }) + ->values() + ->all(); + } + /** * 리소스를 배열로 변환합니다. * @@ -31,7 +62,9 @@ class AttachmentResource extends BaseApiResource 'collection' => $this->collection, 'order' => $this->order, 'download_url' => $this->download_url, - 'preview_url' => $this->preview_url, + 'preview_url' => $this->signedPreviewSlug !== null && $this->resource->is_image + ? $this->resource->previewUrlForSlug($this->signedPreviewSlug, signed: true) + : $this->preview_url, 'is_image' => $this->is_image, 'meta' => $this->meta, ]; diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/BoardResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/BoardResource.php index e1e997b5..4b6d66f5 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Resources/BoardResource.php +++ b/modules/_bundled/sirsoft-board/src/Http/Resources/BoardResource.php @@ -56,6 +56,7 @@ class BoardResource extends BaseApiResource 'use_comment' => $this->use_comment, 'use_reply' => $this->use_reply, 'max_reply_depth' => $this->max_reply_depth, + 'reply_delete_policy' => $this->reply_delete_policy, 'use_report' => $this->use_report, 'comment_order' => $this->comment_order, 'max_comment_depth' => $this->max_comment_depth, @@ -192,6 +193,7 @@ class BoardResource extends BaseApiResource 'use_file_upload' => $this->use_file_upload, 'use_comment' => $this->use_comment, 'use_reply' => $this->use_reply, + 'reply_delete_policy' => $this->reply_delete_policy, 'use_report' => $this->use_report, 'secret_mode' => $this->secret_mode, 'show_view_count' => $this->show_view_count, @@ -224,6 +226,7 @@ class BoardResource extends BaseApiResource 'use_file_upload' => $this->use_file_upload, 'use_comment' => $this->use_comment, 'use_reply' => $this->use_reply, + 'reply_delete_policy' => $this->reply_delete_policy, 'use_report' => $this->use_report, 'secret_mode' => $this->secret_mode, 'per_page' => $this->per_page, @@ -401,7 +404,7 @@ class BoardResource extends BaseApiResource /** @var User|null $user */ $user = Auth::user(); $slug = $this->getValue('slug'); - $isAdminRoute = $this->isAdminRoute($request); + $isAdminRoute = $this->isAdminRequest($request); // 컨텍스트에 맞는 permissionMap 구성 (PostResource.resolveAbilities()와 동일 키) $permissionMap = $isAdminRoute @@ -490,23 +493,4 @@ class BoardResource extends BaseApiResource return $guestRole->permissions()->where('permissions.id', $permission->id)->exists(); } - - /** - * Admin 라우트 여부를 확인합니다. - * - * Controller 네임스페이스로 판단합니다. - * - * @param Request $request HTTP 요청 - * @return bool Admin 라우트 여부 - */ - private function isAdminRoute(Request $request): bool - { - $controller = $request->route()?->getController(); - - if (! $controller) { - return false; - } - - return str_contains(get_class($controller), '\\Admin\\'); - } } diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php index 3dddc4dd..020ec762 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php +++ b/modules/_bundled/sirsoft-board/src/Http/Resources/CommentResource.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Enums\TriggerType; +use Modules\Sirsoft\Board\Models\Post; use Modules\Sirsoft\Board\Repositories\Contracts\ReportRepositoryInterface; use Modules\Sirsoft\Board\Traits\ChecksBoardPermission; use Modules\Sirsoft\Board\Traits\FormatsBoardDate; @@ -31,7 +32,11 @@ class CommentResource extends BaseApiResource */ public function toArray(Request $request): array { - $slug = $this->post?->board?->slug ?? $request->route('slug'); + // 컨트롤러가 넘긴 부모 post(요청 속성)를 재사용해 slug 를 도출한다 — 목록에서 댓글당 + // `$this->post` lazy-load(N+1)를 피한다(KVE-2026-1914 A-4b). 미주입 경로(상세/생성/ + // admin)는 종전대로 이미 로드된 관계 또는 라우트 slug 로 폴백한다. + $parentPost = $this->resolveParentPost($request); + $slug = $parentPost?->board?->slug ?? $request->route('slug'); return [ 'id' => $this->id, @@ -170,8 +175,9 @@ class CommentResource extends BaseApiResource } // fallback: 개별 쿼리 (목록 등 사전 로드 미적용 경로) + // 부모 post 는 컨트롤러가 넘긴 인스턴스를 재사용해 댓글당 lazy-load 를 피한다. $user = $request->user(); - $boardId = $this->post?->board?->id ?? null; + $boardId = $this->resolveParentPost($request)?->board?->id ?? null; if (! $user || ! $boardId) { return false; @@ -206,7 +212,7 @@ class CommentResource extends BaseApiResource */ protected function resolveAbilities(Request $request): array { - $slug = $this->post?->board?->slug ?? $request->route('slug'); + $slug = $this->resolveParentPost($request)?->board?->slug ?? $request->route('slug'); if (! $slug) { return []; } @@ -228,29 +234,33 @@ class CommentResource extends BaseApiResource ->toArray(); } - /** - * Admin 요청 여부를 확인합니다. - * - * Controller 네임스페이스로 판단합니다. - * - * @param Request $request HTTP 요청 - * @return bool Admin 요청 여부 - */ - private function isAdminRequest(Request $request): bool - { - $controller = $request->route()?->getController(); - - if (! $controller) { - return false; - } - - return str_contains(get_class($controller), '\\Admin\\'); - } - // ========================================================================= // 콘텐츠 필터링 메서드 // ========================================================================= + /** + * 부모 게시글(Post)을 조회 없이 해석합니다. + * + * 우선순위: + * 1. 컨트롤러가 요청 속성으로 넘긴 인스턴스(`sirsoft_board_parent_post`) — 목록 경로에서 + * 댓글당 lazy-load(N+1)를 피하는 SSoT. 모든 댓글이 같은 인스턴스를 공유한다. + * 2. 이미 로드된 `post` 관계(상세/생성/admin 등 미주입 경로 하위호환). + * + * 둘 다 없으면 null — 추가 쿼리를 유발하지 않는다(2차 게이트는 1차 방어가 담당). + * + * @param Request $request HTTP 요청 + * @return Post|null 부모 게시글 + */ + private function resolveParentPost(Request $request): ?Post + { + $injected = $request->attributes->get('sirsoft_board_parent_post'); + if ($injected instanceof Post) { + return $injected; + } + + return $this->resource->relationLoaded('post') ? $this->post : null; + } + /** * 권한에 따라 필터링된 댓글 내용을 반환합니다. * @@ -260,6 +270,14 @@ class CommentResource extends BaseApiResource */ private function getFilteredContent(Request $request, ?string $slug): ?string { + // 부모 게시글이 비밀글이면 열람 권한 없는 요청에는 댓글 원문을 숨긴다 + // (KVE-2026-1914 이중 방어 — 1차 차단은 CommentController::index). + // 컨트롤러가 넘긴 부모 post(요청 속성)를 재사용해 댓글당 lazy-load 없이 재확인한다. + $parentPost = $this->resolveParentPost($request); + if ($parentPost && $parentPost->is_secret && ! PostResource::canViewSecretForPost($parentPost, $request)) { + return null; + } + // 게시글 삭제로 함께 숨겨진(cascade) 댓글은 사용자가 직접 지운 것이 아니므로 // 마스킹하지 않고 원문을 그대로 노출한다 (글을 볼 수 있는 사람이면 누구나). $isCascadeDeleted = $this->deleted_at !== null diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php b/modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php index 7c9f9e31..437db4b0 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php +++ b/modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php @@ -339,23 +339,4 @@ class PostCollection extends BaseApiCollection 'can_access_admin' => $this->checkBoardPermission($slug, 'admin.manage'), ]; } - - /** - * Admin 요청 여부를 확인합니다. - * - * Controller 네임스페이스로 판단합니다. - * - * @param Request $request HTTP 요청 - * @return bool Admin 요청 여부 - */ - private function isAdminRequest(Request $request): bool - { - $controller = $request->route()?->getController(); - - if (! $controller) { - return false; - } - - return str_contains(get_class($controller), '\\Admin\\'); - } } diff --git a/modules/_bundled/sirsoft-board/src/Http/Resources/PostResource.php b/modules/_bundled/sirsoft-board/src/Http/Resources/PostResource.php index e50baa6b..cab4a70e 100644 --- a/modules/_bundled/sirsoft-board/src/Http/Resources/PostResource.php +++ b/modules/_bundled/sirsoft-board/src/Http/Resources/PostResource.php @@ -10,8 +10,10 @@ use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Enums\ReportReasonType; use Modules\Sirsoft\Board\Enums\TriggerType; +use Modules\Sirsoft\Board\Models\Post; use Modules\Sirsoft\Board\Repositories\Contracts\ReportRepositoryInterface; use Modules\Sirsoft\Board\Support\BoardPermissionCacheKeys; +use Modules\Sirsoft\Board\Support\SecretContentGate; use Modules\Sirsoft\Board\Traits\ChecksBoardPermission; use Modules\Sirsoft\Board\Traits\FormatsBoardDate; @@ -312,7 +314,7 @@ class PostResource extends BaseApiResource $slug = request()->route('slug') ?? ($this->relationLoaded('board') ? $this->board?->slug : null); if ($slug && $attachment->hash) { - return '/api/modules/sirsoft-board/boards/'.$slug.'/attachment/'.$attachment->hash.'/preview'; + return $attachment->previewUrlForSlug($slug); } return null; @@ -537,6 +539,13 @@ class PostResource extends BaseApiResource return null; } + // 비밀글·삭제글 첨부는 무서명 preview 가 서빙 게이트에 차단되므로, + // 이 직렬화(게이트 통과가 확인된 응답)에 한해 <img> 렌더 가능한 + // 한시 서명 preview URL 을 발급한다. 정상글은 무서명 공개 URL 유지. + $signedPreviewSlug = ($this->deleted_at || $this->is_secret) + ? ($slug ?? $this->getSlug($request)) + : null; + // 삭제된 게시글: 관리 권한자가 아니면 첨부 목록 미노출. // 단, 게시글 삭제로 함께 숨겨진(cascade) 첨부는 사용자가 직접 지운 것이 아니므로 // 글을 볼 수 있는 사람에게는 노출한다 (cascade 댓글 노출과 일관). @@ -546,14 +555,14 @@ class PostResource extends BaseApiResource && $attachment->trigger_type === TriggerType::Cascade->value )->values(); - return AttachmentResource::collection($cascadeAttachments); + return AttachmentResource::collectionFor($cascadeAttachments, $signedPreviewSlug); } if ($this->is_secret && ! $this->canViewSecretContent($request, $slug)) { return []; } - return AttachmentResource::collection($this->attachments); + return AttachmentResource::collectionFor($this->attachments, $signedPreviewSlug); } // ========================================================================= @@ -654,23 +663,6 @@ class PostResource extends BaseApiResource return $abilities; } - /** - * Admin 요청 여부를 확인합니다. - * - * @param Request $request HTTP 요청 - * @return bool Admin 요청 여부 - */ - private function isAdminRequest(Request $request): bool - { - $controller = $request->route()?->getController(); - - if (! $controller) { - return false; - } - - return str_contains(get_class($controller), '\\Admin\\'); - } - /** * 비밀글 내용 열람 가능 여부를 확인합니다. * @@ -686,30 +678,24 @@ class PostResource extends BaseApiResource */ private function canViewSecretContent(Request $request, ?string $slug = null): bool { - // 1. 작성자 본인 (회원 게시글) - $user = Auth::user(); - if ($user && $this->user_id && $this->user_id === $user->id) { - return true; - } + // 판정 규칙은 SecretContentGate(SSoT)에 있다 — 리스너·댓글 경로와 규칙을 공유해 + // 드리프트를 방지한다. + return self::canViewSecretForPost($this->resource, $request); + } - // 2. 비밀번호 검증 완료 - if ($this->password_verified === true) { - return true; - } - - // 3-4. 게시판별 권한 체크 - $slug = $slug ?? $this->getSlug($request); - if (! $slug) { - return false; - } - - if ($this->isAdminRequest($request)) { - return $this->checkBoardPermission($slug, 'admin.posts.read-secret') - || $this->checkBoardPermission($slug, 'admin.manage'); - } - - return $this->checkBoardPermission($slug, 'posts.read-secret', PermissionType::User) - || $this->checkBoardPermission($slug, 'manager', PermissionType::User); + /** + * 주어진 게시글의 비밀 원문 열람 권한을 판정합니다 (SSoT 진입점). + * + * PostResource 외 경로(이커머스 문의 연동 훅, 댓글 목록/리소스, 첨부 서빙)가 + * 동일 규칙으로 서버측 마스킹을 수행하도록 공유합니다(KVE-2026-1914). + * + * @param Post $post 대상 게시글 + * @param Request|null $request HTTP 요청 (미지정 시 현재 요청) + * @return bool 열람 가능 여부 + */ + public static function canViewSecretForPost(Post $post, ?Request $request = null): bool + { + return app(SecretContentGate::class)->canView($post, $request); } // ========================================================================= diff --git a/modules/_bundled/sirsoft-board/src/Listeners/Ckeditor5ReferenceSourcesListener.php b/modules/_bundled/sirsoft-board/src/Listeners/Ckeditor5ReferenceSourcesListener.php new file mode 100644 index 00000000..cfc50493 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Listeners/Ckeditor5ReferenceSourcesListener.php @@ -0,0 +1,57 @@ +<?php + +namespace Modules\Sirsoft\Board\Listeners; + +use App\Contracts\Extension\HookListenerInterface; + +/** + * 게시판 본문을 CKEditor5 이미지 참조 스캔 대상으로 등록하는 리스너 + * + * `sirsoft-ckeditor5.image.filter_reference_sources` 필터 훅에 게시글 본문 컬럼을 + * 덧붙입니다. 등록하지 않으면 게시글에만 쓰이는 이미지가 "미참조" 로 판정돼 + * 자동 정리 대상이 됩니다. + * + * 테이블명 문자열만 덧붙이며 DB 에 접근하지 않습니다 — 실제 조회와 존재 검증은 + * 플러그인의 참조 판정 서비스가 수행합니다. + */ +class Ckeditor5ReferenceSourcesListener implements HookListenerInterface +{ + /** + * 구독할 훅 목록 반환 + * + * @return array<string, mixed> + */ + public static function getSubscribedHooks(): array + { + return [ + 'sirsoft-ckeditor5.image.filter_reference_sources' => [ + 'method' => 'addBoardSources', + 'priority' => 10, + 'type' => 'filter', + ], + ]; + } + + /** + * 훅 이벤트를 처리합니다. + * + * Filter 훅은 getSubscribedHooks 에서 지정한 메서드를 직접 호출하므로 + * 이 메서드는 인터페이스 요구사항 충족을 위해서만 존재합니다. + * + * @param mixed ...$args 훅에서 전달된 인수들 + */ + public function handle(...$args): void {} + + /** + * 게시판 참조 소스를 추가합니다. + * + * @param array $sources 기존 참조 소스 목록 + * @return array 게시판 소스가 추가된 목록 + */ + public function addBoardSources(array $sources): array + { + $sources[] = ['table' => 'board_posts', 'columns' => ['content']]; + + return $sources; + } +} diff --git a/modules/_bundled/sirsoft-board/src/Listeners/EcommerceInquiryHookListener.php b/modules/_bundled/sirsoft-board/src/Listeners/EcommerceInquiryHookListener.php index 6a9c816b..84bb1760 100644 --- a/modules/_bundled/sirsoft-board/src/Listeners/EcommerceInquiryHookListener.php +++ b/modules/_bundled/sirsoft-board/src/Listeners/EcommerceInquiryHookListener.php @@ -6,6 +6,7 @@ use App\Contracts\Extension\HookListenerInterface; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Board\Http\Resources\PostResource; use Modules\Sirsoft\Board\Models\Post; use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; use Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface; @@ -84,13 +85,19 @@ class EcommerceInquiryHookListener implements HookListenerInterface 'priority' => 10, 'type' => 'filter', ], + // 이커머스 → 게시판: 살아있는 답변 수 조회 (Filter 훅) + 'sirsoft-ecommerce.inquiry.count_replies' => [ + 'method' => 'countReplies', + 'priority' => 10, + 'type' => 'filter', + ], ]; } /** * 기본 훅 핸들러 (HookListenerInterface 필수 메서드) * - * @param mixed ...$args 훅 인자 + * @param mixed ...$args 훅 인자 * @return void */ public function handle(...$args): void @@ -106,7 +113,8 @@ class EcommerceInquiryHookListener implements HookListenerInterface * @param mixed $carry 이전 필터 결과 (초기값: null) * @param string $slug 게시판 슬러그 * @param array $data 게시글 생성 데이터 - * @return array|null 성공 시 ['post_id' => int, 'inquirable_type' => string], 실패 시 null + * @return array|null 성공 시 ['post_id' => int, 'inquirable_type' => string], + * 중복 답변 차단 시 ['duplicate' => true], 실패 시 null */ public function createAndReturn(mixed $carry, string $slug, array $data): ?array { @@ -116,9 +124,25 @@ class EcommerceInquiryHookListener implements HookListenerInterface $data['user_id'] = null; } - // ip_address: board_posts.ip_address NOT NULL 제약 충족 + // ip_address: board_posts.ip_address NOT NULL 제약 충족. + // 클라이언트 IP 는 요청 경계(호출 서비스 ProductInquiryService)가 payload 로 + // 주입한다 — Listener 는 request() 를 직접 참조하지 않는다(입력 우회 방지). if (empty($data['ip_address'])) { - $data['ip_address'] = request()->ip() ?? '0.0.0.0'; + $data['ip_address'] = '0.0.0.0'; + } + + // 2차 방어(게시판 실데이터): 이미 살아있는 답변이 있는 부모에는 자식 Post 생성 거부. + // 1차 방어(피벗 is_answered)는 서비스가 담당하지만, API 동시 호출/직접 호출로 + // 플래그가 어긋난 경우에도 게시판에 중복 답변이 쌓이지 않도록 여기서 최종 차단한다. + if (! empty($data['parent_id']) && $this->postRepository->findFirstReplyWithBoard((int) $data['parent_id']) !== null) { + Log::warning('EcommerceInquiryHookListener: 기존 답변이 있는 문의에 중복 답변 생성 시도 차단', [ + 'slug' => $slug, + 'parent_id' => $data['parent_id'], + ]); + + // null(훅 무응답/실패)과 구분되는 중복 마커 — 호출 서비스가 이 마커로 + // "이미 등록된 답변" 사유를 사용자에게 그대로 안내한다. + return ['duplicate' => true]; } // parent_id 있으면 답변글 → 부모 Post 제목으로 Re: 원글제목 설정 @@ -151,13 +175,20 @@ class EcommerceInquiryHookListener implements HookListenerInterface } /** - * ID 목록으로 Post 데이터 배열 반환 + * ID 목록으로 Post 데이터 배열 반환 (`sirsoft-ecommerce.inquiry.get_by_ids` 필터 훅) * * 이커머스 모듈이 문의 목록을 구성할 때 게시글 데이터를 일괄 조회합니다. * + * 반환 payload 계약(KVE-2026-1914): 각 항목은 비밀글 열람 권위 플래그 + * `can_view_secret`(bool)을 반드시 포함해야 한다. 소비자(`ProductInquiryService`)는 + * 이 플래그로 title/content/reply/attachments 마스킹을 최종 확정하며, **플래그가 + * 없으면 fail-closed 로 전부 마스킹**한다. 3자 확장이 이 훅을 대체 구현할 때 + * `can_view_secret` 를 누락하면 비밀 아닌 문의까지 조용히 마스킹되는 기능 회귀가 + * 발생하므로, 대체 리스너도 요청자 신원으로 이 플래그를 채워야 한다. + * * @param array $carry 이전 필터 결과 (초기값: []) * @param array $context 조회 컨텍스트 ['ids' => int[], 'slug' => string] - * @return array Post 데이터 배열 + * @return array Post 데이터 배열 (각 항목에 `can_view_secret` bool 필수) */ public function getByIds(array $carry, array $context): array { @@ -171,6 +202,12 @@ class EcommerceInquiryHookListener implements HookListenerInterface $posts = $this->postRepository->findByIdsWithRelations($ids); return $posts->map(function (Post $post) { + // 비밀글 서버측 게이팅(KVE-2026-1914): 열람 권한이 없으면 원문을 마스킹한다. + // 규칙은 PostResource 와 동일한 SecretContentGate(SSoT)를 공유한다. + // 리스트 컨텍스트라 password_verified 는 적용되지 않는다(작성자/관리 권한만). + $isSecret = (bool) $post->is_secret; + $canViewSecret = ! $isSecret || PostResource::canViewSecretForPost($post); + return [ 'id' => $post->id, 'board_id' => $post->board_id, @@ -178,26 +215,34 @@ class EcommerceInquiryHookListener implements HookListenerInterface 'parent_id' => $post->parent_id, 'user_id' => $post->user_id, 'author_name' => $post->author_name, - 'title' => $post->title, - 'content' => $post->content, + 'title' => $canViewSecret + ? $post->title + : __('sirsoft-board::messages.post.secret_post_title'), + 'content' => $canViewSecret ? $post->content : null, 'category' => $post->category, - 'is_secret' => (bool) $post->is_secret, + 'is_secret' => $isSecret, + // 서버가 요청자 신원으로 내린 열람 판정(SSoT). 소비 서비스가 이 값으로 + // 마스킹을 재확인(이중 방어)할 수 있도록 함께 실어 보낸다. 소비측은 자기 + // 권한을 재계산하지 말고 이 값만 신뢰해야 게이트 강도가 갈리지 않는다. + 'can_view_secret' => $canViewSecret, 'status' => $post->status?->value, 'view_count' => $post->view_count, 'created_at' => $post->created_at?->toIso8601String(), 'updated_at' => $post->updated_at?->toIso8601String(), - // 첨부파일 목록 - 'attachments' => $post->attachments->map(fn ($a) => [ - 'id' => $a->id, - 'original_filename' => $a->original_filename, - 'size' => $a->size, - 'size_formatted' => $a->size_formatted, - 'is_image' => $a->is_image, - 'preview_url' => $a->preview_url, - 'download_url' => $a->download_url, - ])->values()->all(), - // 답변 게시글 (parent_id가 있는 자식 글) - 'reply' => $this->getReplyForPost($post), + // 첨부파일 목록 (비밀글 비열람자는 빈 배열) + 'attachments' => $canViewSecret + ? $post->attachments->map(fn ($a) => [ + 'id' => $a->id, + 'original_filename' => $a->original_filename, + 'size' => $a->size, + 'size_formatted' => $a->size_formatted, + 'is_image' => $a->is_image, + 'preview_url' => $a->preview_url, + 'download_url' => $a->download_url, + ])->values()->all() + : [], + // 답변 게시글 (parent_id가 있는 자식 글, 비밀글 비열람자는 null) + 'reply' => $canViewSecret ? $this->getReplyForPost($post) : null, ]; })->all(); } catch (\Exception $e) { @@ -229,18 +274,18 @@ class EcommerceInquiryHookListener implements HookListenerInterface } return [ - 'secret_mode' => $board->secret_mode?->value ?? 'disabled', - 'categories' => $board->categories ?? [], - 'use_file_upload' => (bool) $board->use_file_upload, - 'max_file_count' => $board->max_file_count ?? 5, - 'max_file_size' => $board->max_file_size ?? 10, - 'allowed_extensions' => $board->allowed_extensions ?? [], - 'min_title_length' => $board->min_title_length ?? 2, - 'max_title_length' => $board->max_title_length ?? 200, - 'min_content_length' => $board->min_content_length ?? 10, - 'max_content_length' => $board->max_content_length ?? 10000, - 'attachment_upload_url' => '/api/modules/sirsoft-board/boards/' . $board->slug . '/attachments', - 'attachment_delete_url' => '/api/modules/sirsoft-board/boards/' . $board->slug . '/attachments/:id', + 'secret_mode' => $board->secret_mode?->value ?? 'disabled', + 'categories' => $board->categories ?? [], + 'use_file_upload' => (bool) $board->use_file_upload, + 'max_file_count' => $board->max_file_count ?? 5, + 'max_file_size' => $board->max_file_size ?? 10, + 'allowed_extensions' => $board->allowed_extensions ?? [], + 'min_title_length' => $board->min_title_length ?? 2, + 'max_title_length' => $board->max_title_length ?? 200, + 'min_content_length' => $board->min_content_length ?? 10, + 'max_content_length' => $board->max_content_length ?? 10000, + 'attachment_upload_url' => '/api/modules/sirsoft-board/boards/'.$board->slug.'/attachments', + 'attachment_delete_url' => '/api/modules/sirsoft-board/boards/'.$board->slug.'/attachments/:id', ]; } catch (\Exception $e) { Log::error('EcommerceInquiryHookListener: 게시판 설정 조회 실패', [ @@ -270,7 +315,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface $post = $this->postRepository->findWithBoard($postId); if (! $post || ! $post->board) { - throw new ModelNotFoundException("Post {$postId} 또는 소속 Board를 찾을 수 없습니다."); + throw new ModelNotFoundException("Post {$postId} or its board could not be found."); } $attachmentIds = $data['attachment_ids'] ?? []; @@ -278,7 +323,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (ModelNotFoundException $e) { Log::warning('EcommerceInquiryHookListener: Post 수정 실패 - 게시글 또는 게시판 없음', [ 'post_id' => $postId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -287,7 +332,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (\Exception $e) { Log::error('EcommerceInquiryHookListener: Post 수정 실패', [ 'post_id' => $postId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -315,15 +360,21 @@ class EcommerceInquiryHookListener implements HookListenerInterface $post = $this->postRepository->findWithBoard($postId); if (! $post || ! $post->board) { - throw new ModelNotFoundException("Post {$postId} 또는 소속 Board를 찾을 수 없습니다."); + throw new ModelNotFoundException("Post {$postId} or its board could not be found."); } - // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴) - $this->postService->deletePost($post->board->slug, $postId, options: ['skip_notification' => true]); + // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴). + // cascade_replies: 훅 경유 삭제는 게시판 답글 삭제 정책(block)과 무관하게 답변을 + // 함께 정리한다 — 문의 답변은 시스템 생성이므로 정책에 막히면 안 되고, + // 기설치본의 다건 고아 답변도 cascade 스윕이 함께 정리한다. + $this->postService->deletePost($post->board->slug, $postId, options: [ + 'skip_notification' => true, + 'cascade_replies' => true, + ]); } catch (ModelNotFoundException $e) { Log::warning('EcommerceInquiryHookListener: Post 삭제 실패 - 게시글 또는 게시판 없음', [ 'post_id' => $postId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -332,7 +383,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (\Exception $e) { Log::error('EcommerceInquiryHookListener: Post 삭제 실패', [ 'post_id' => $postId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -367,7 +418,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } if (! $reply->board) { - throw new ModelNotFoundException("Reply Post {$reply->id} 소속 Board를 찾을 수 없습니다."); + throw new ModelNotFoundException("Reply Post {$reply->id}'s board could not be found."); } $this->postService->updatePost($reply->board->slug, $reply->id, $data); @@ -376,7 +427,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (ModelNotFoundException $e) { Log::warning('EcommerceInquiryHookListener: Reply Post 수정 실패 - 게시글 또는 게시판 없음', [ 'parent_post_id' => $parentPostId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -385,7 +436,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (\Exception $e) { Log::error('EcommerceInquiryHookListener: Reply Post 수정 실패', [ 'parent_post_id' => $parentPostId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -419,17 +470,21 @@ class EcommerceInquiryHookListener implements HookListenerInterface } if (! $reply->board) { - throw new ModelNotFoundException("Reply Post {$reply->id} 소속 Board를 찾을 수 없습니다."); + throw new ModelNotFoundException("Reply Post {$reply->id}'s board could not be found."); } - // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴) - $this->postService->deletePost($reply->board->slug, $reply->id, options: ['skip_notification' => true]); + // 이커머스 경로: 알림 발송 SKIP (createPost와 동일한 skip_notification 패턴). + // cascade_replies: 답변 밑에 달린 자식 글까지 함께 정리 (정책 무관 — 시스템 생성 답변) + $this->postService->deletePost($reply->board->slug, $reply->id, options: [ + 'skip_notification' => true, + 'cascade_replies' => true, + ]); } catch (\RuntimeException $e) { throw $e; } catch (ModelNotFoundException $e) { Log::warning('EcommerceInquiryHookListener: Reply Post 삭제 실패 - 게시글 또는 게시판 없음', [ 'parent_post_id' => $parentPostId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -438,7 +493,7 @@ class EcommerceInquiryHookListener implements HookListenerInterface } catch (\Exception $e) { Log::error('EcommerceInquiryHookListener: Reply Post 삭제 실패', [ 'parent_post_id' => $parentPostId, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); throw new \RuntimeException( @@ -449,6 +504,33 @@ class EcommerceInquiryHookListener implements HookListenerInterface return $carry; } + /** + * 부모 문의 Post 의 살아있는 답변 수 반환 (`sirsoft-ecommerce.inquiry.count_replies` 필터 훅) + * + * 이커머스 모듈이 답변 삭제 후 `is_answered` 재계산, 게시판 직권 삭제 시 답변완료 + * 해제 판정에 사용합니다. SoftDeletes 전역 스코프가 삭제된 답변을 제외하므로 + * "살아있는 답변" 만 집계됩니다. + * + * @param mixed $carry 이전 필터 결과 (초기값: null = 판정 불가) + * @param int $parentPostId 부모 문의 Post ID + * @return int|null 살아있는 답변 수 (조회 실패 시 이전 필터 결과 유지 — null 이면 소비측이 판정 불가로 no-op) + */ + public function countReplies(mixed $carry, int $parentPostId): ?int + { + try { + return $this->postRepository->countRepliesByParentId($parentPostId); + } catch (\Exception $e) { + Log::error('EcommerceInquiryHookListener: 답변 수 조회 실패', [ + 'parent_post_id' => $parentPostId, + 'error' => $e->getMessage(), + ]); + + // 실패를 0(답변 없음)으로 접으면 소비측이 답변완료를 오해제한다 — + // 판정 불가(null)를 그대로 흘려보낸다. + return is_numeric($carry) ? (int) $carry : null; + } + } + // ─── 내부 유틸리티 ──────────────────────────────────────── /** diff --git a/modules/_bundled/sirsoft-board/src/Listeners/SearchPostsListener.php b/modules/_bundled/sirsoft-board/src/Listeners/SearchPostsListener.php index d0400174..c0093e2a 100644 --- a/modules/_bundled/sirsoft-board/src/Listeners/SearchPostsListener.php +++ b/modules/_bundled/sirsoft-board/src/Listeners/SearchPostsListener.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Board\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Helpers\PermissionHelper; use App\Search\SearchCategoryPayload; +use App\Search\SearchHighlighter; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Board\Services\BoardService; use Modules\Sirsoft\Board\Services\PostService; @@ -120,7 +121,10 @@ class SearchPostsListener implements HookListenerInterface $results['posts'] = $this->buildSearchResult($boards, $q, $context); } catch (\Exception $e) { - Log::error('Search posts error', ['message' => $e->getMessage(), 'q' => $q]); + // 실패를 카테고리 키 미설정으로 삼키면 화면이 "검색 결과 없음" 을 그린다 — + // failed 페이로드로 표면화하고, 원인 추적을 위해 스택을 함께 남긴다 (#103). + Log::error('Search posts error', ['message' => $e->getMessage(), 'q' => $q, 'exception' => $e]); + $results['posts'] = SearchCategoryPayload::failed(['available_boards' => []]); } return $results; @@ -210,7 +214,9 @@ class SearchPostsListener implements HookListenerInterface $type = $context['type'] ?? 'all'; $boardIds = collect($boards)->pluck('id')->all(); - $extra = ['available_boards' => $this->boardService->getActiveBoardsListForFilter()]; + // available_boards(필터 드롭다운)는 검색 결과 필터와 동일하게 읽기 권한을 통과한 + // 게시판만 담는다 — 권한 없는 게시판이 필터 목록으로 노출되지 않도록 컨텍스트 사용자를 전달. + $extra = ['available_boards' => $this->boardService->getActiveBoardsListForFilter($context['user'] ?? null)]; /** 조회 결과 항목을 화면 형태로 가공한다. */ $format = fn (iterable $items): array => collect($items) @@ -321,14 +327,9 @@ class SearchPostsListener implements HookListenerInterface $contentPreview = $this->extractContentPreview($post->content, $keyword, 150, $contentMode); $boardSlug = $post->board?->slug ?? ''; - // 텍스트 모드: 태그 문자열이 그대로 보존되므로 htmlspecialchars로 이스케이프 후 하이라이트 - // HTML 모드: strip_tags 처리된 평문이므로 바로 하이라이트 - if ($contentMode === 'text') { - $escapedPreview = htmlspecialchars($contentPreview, ENT_QUOTES, 'UTF-8'); - $contentPreviewHighlighted = $this->highlightKeyword($escapedPreview, $keyword); - } else { - $contentPreviewHighlighted = $this->highlightKeyword($contentPreview, $keyword); - } + // 하이라이트 필드는 소비 측이 HTML 로 렌더하므로, 공유 헬퍼가 원문을 이스케이프한 뒤 + // 검색어만 <mark> 로 감싼다. 텍스트 모드의 리터럴 태그 문자열도 여기서 이스케이프된다. + $contentPreviewHighlighted = $this->highlightKeyword($contentPreview, $keyword); return [ 'id' => $post->id, @@ -363,13 +364,7 @@ class SearchPostsListener implements HookListenerInterface */ private function highlightKeyword(?string $text, string $keyword): string { - if (empty($text) || empty($keyword)) { - return $text ?? ''; - } - - $escapedKeyword = preg_quote($keyword, '/'); - - return preg_replace('/('.$escapedKeyword.')/iu', '<mark>$1</mark>', $text); + return SearchHighlighter::highlight($text, $keyword); } /** @@ -390,9 +385,10 @@ class SearchPostsListener implements HookListenerInterface return ''; } - // HTML 모드: 태그 제거 후 평문 추출, 텍스트 모드: 태그 문자열 그대로 보존 + // HTML 모드: 태그 제거 후 평문 추출(엔티티 디코드를 태그 제거보다 먼저 수행), + // 텍스트 모드: 태그 문자열 그대로 보존(하이라이트 시점에 이스케이프됨) if ($contentMode === 'html') { - $plainText = trim(preg_replace('/\s+/', ' ', html_entity_decode(strip_tags($content)))); + $plainText = SearchHighlighter::toPlainText($content); } else { $plainText = trim(preg_replace('/\s+/', ' ', $content)); } diff --git a/modules/_bundled/sirsoft-board/src/Listeners/SeoBoardSettingsCacheListener.php b/modules/_bundled/sirsoft-board/src/Listeners/SeoBoardSettingsCacheListener.php index f597ca0e..612f3067 100644 --- a/modules/_bundled/sirsoft-board/src/Listeners/SeoBoardSettingsCacheListener.php +++ b/modules/_bundled/sirsoft-board/src/Listeners/SeoBoardSettingsCacheListener.php @@ -27,6 +27,12 @@ class SeoBoardSettingsCacheListener implements HookListenerInterface 'method' => 'onModuleSettingsSave', 'priority' => 20, ], + // 환경설정 기본값 일괄 적용도 게시판 화면 출력을 바꾸므로 같은 무효화가 필요하다. + // payload 가 ($fields, $updatedCount) 라 식별자 인자가 없어 전용 핸들러로 받는다. + 'sirsoft-board.settings.after_bulk_apply' => [ + 'method' => 'onBulkApply', + 'priority' => 20, + ], ]; } @@ -56,6 +62,27 @@ class SeoBoardSettingsCacheListener implements HookListenerInterface return; } + $this->invalidateBoardSeoCache(); + } + + /** + * 환경설정 기본값 일괄 적용 후 게시판 SEO 캐시를 무효화합니다. + * + * 이 훅의 payload 는 ($fields, $updatedCount) 로 모듈 식별자를 담지 않는다 — + * 게시판 모듈이 발행하는 훅이므로 식별자 가드 자체가 불필요하다. + * + * @param mixed ...$args 훅 인자 ($fields, $updatedCount) + */ + public function onBulkApply(...$args): void + { + $this->invalidateBoardSeoCache(); + } + + /** + * 게시판 관련 SEO 레이아웃 캐시와 sitemap 캐시를 무효화합니다. + */ + private function invalidateBoardSeoCache(): void + { try { $cache = app(SeoCacheManagerInterface::class); diff --git a/modules/_bundled/sirsoft-board/src/Models/Attachment.php b/modules/_bundled/sirsoft-board/src/Models/Attachment.php index dd8c06ca..a6d80b8a 100644 --- a/modules/_bundled/sirsoft-board/src/Models/Attachment.php +++ b/modules/_bundled/sirsoft-board/src/Models/Attachment.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; /** @@ -127,6 +128,8 @@ class Attachment extends Model /** * 게시판과의 관계를 정의합니다. + * + * @return BelongsTo 게시판 관계 */ public function board(): BelongsTo { @@ -135,6 +138,8 @@ class Attachment extends Model /** * 게시글과의 관계를 정의합니다. + * + * @return BelongsTo 게시글 관계 */ public function post(): BelongsTo { @@ -143,6 +148,8 @@ class Attachment extends Model /** * 업로더와의 관계를 정의합니다. + * + * @return BelongsTo 업로더 사용자 관계 */ public function creator(): BelongsTo { @@ -160,6 +167,8 @@ class Attachment extends Model ? $this->board->slug : Board::find($this->board_id)?->slug; + // route() 생성은 빈 슬러그(미해석 board)에서 예외가 되므로 문자열 조립을 유지한다 + // — 빈 슬러그 허용은 AttachmentPreviewUrlTest 가 고정한 기존 계약이다. return '/api/modules/sirsoft-board/boards/'.$boardSlug.'/attachment/'.$this->hash; } @@ -178,9 +187,48 @@ class Attachment extends Model ? $this->board->slug : Board::find($this->board_id)?->slug; - return '/api/modules/sirsoft-board/boards/'.$boardSlug.'/attachment/'.$this->hash.'/preview'; + return $this->previewUrlForSlug((string) $boardSlug); } + /** + * 슬러그가 이미 해석된 컨텍스트의 미리보기 URL 조립 단일 지점. + * + * 목록 직렬화(PostResource)처럼 slug 를 라우트/관계에서 이미 알고 있어 + * Board::find() 재조회(N+1)를 피해야 하는 호출측이 사용합니다. + * + * 브라우저 <img src> 는 Authorization 헤더를 실을 수 없으므로, 비밀글·삭제글 + * 첨부를 열람 권한자 화면에 표시할 때는 한시 서명 URL 을 발급한다($signed). + * 서명 URL 은 게이트를 통과한 응답 직렬화 시점에만 발급되고(PostResource), + * 서빙 엔드포인트가 유효 서명을 허용한다 — 무서명 게이트는 종전과 동일. + * + * @param string $slug 게시판 슬러그 + * @param bool $signed 한시 서명 URL 발급 여부 (비밀글·삭제글 <img> 렌더용) + * @return string 미리보기 URL + */ + public function previewUrlForSlug(string $slug, bool $signed = false): string + { + if ($signed) { + return URL::temporarySignedRoute( + 'api.modules.sirsoft-board.boards.attachment.preview', + now()->addMinutes(self::SIGNED_PREVIEW_TTL_MINUTES), + ['slug' => $slug, 'hash' => $this->hash], + absolute: false + ); + } + + // route() 생성은 빈 슬러그(미해석 board)에서 예외가 되므로 문자열 조립을 유지한다 + // — 빈 슬러그 허용은 AttachmentPreviewUrlTest 가 고정한 기존 계약이다. + return '/api/modules/sirsoft-board/boards/'.$slug.'/attachment/'.$this->hash.'/preview'; + } + + /** + * 서명 preview URL 의 유효 시간(분). + * + * 상세 화면이 열려 있는 동안 썸네일이 유지될 만큼 길고, + * URL 유출 시 노출 창을 좁힐 만큼 짧은 값. + */ + public const SIGNED_PREVIEW_TTL_MINUTES = 30; + /** * 이미지 여부 확인 * diff --git a/modules/_bundled/sirsoft-board/src/Models/Board.php b/modules/_bundled/sirsoft-board/src/Models/Board.php index 0e0afef0..82fcb877 100644 --- a/modules/_bundled/sirsoft-board/src/Models/Board.php +++ b/modules/_bundled/sirsoft-board/src/Models/Board.php @@ -14,6 +14,7 @@ use Modules\Sirsoft\Board\Casts\AsAllowedExtensions; use Modules\Sirsoft\Board\Database\Factories\BoardFactory; use Modules\Sirsoft\Board\Enums\BoardOrderBy; use Modules\Sirsoft\Board\Enums\OrderDirection; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; use Modules\Sirsoft\Board\Enums\SecretMode; class Board extends Model @@ -33,6 +34,7 @@ class Board extends Model 'use_comment' => ['label_key' => 'sirsoft-board::activity_log.fields.use_comment', 'type' => 'boolean'], 'use_reply' => ['label_key' => 'sirsoft-board::activity_log.fields.use_reply', 'type' => 'boolean'], 'max_reply_depth' => ['label_key' => 'sirsoft-board::activity_log.fields.max_reply_depth', 'type' => 'number'], + 'reply_delete_policy' => ['label_key' => 'sirsoft-board::activity_log.fields.reply_delete_policy', 'type' => 'enum', 'enum' => ReplyDeletePolicy::class], 'use_report' => ['label_key' => 'sirsoft-board::activity_log.fields.use_report', 'type' => 'boolean'], 'use_file_upload' => ['label_key' => 'sirsoft-board::activity_log.fields.use_file_upload', 'type' => 'boolean'], 'max_file_size' => ['label_key' => 'sirsoft-board::activity_log.fields.max_file_size', 'type' => 'number'], @@ -126,6 +128,7 @@ class Board extends Model 'use_comment', 'use_reply', 'max_reply_depth', + 'reply_delete_policy', 'use_report', 'new_display_hours', @@ -180,6 +183,7 @@ class Board extends Model 'use_comment' => 'boolean', 'use_reply' => 'boolean', 'max_reply_depth' => 'integer', + 'reply_delete_policy' => ReplyDeletePolicy::class, // Post::isNew() 가 Carbon 시간 연산에 넘기는 값 — 조회 시점과 무관하게 정수를 보장한다. 'new_display_hours' => 'integer', 'use_report' => 'boolean', diff --git a/modules/_bundled/sirsoft-board/src/Providers/BoardServiceProvider.php b/modules/_bundled/sirsoft-board/src/Providers/BoardServiceProvider.php index cc2e521a..60826b1c 100644 --- a/modules/_bundled/sirsoft-board/src/Providers/BoardServiceProvider.php +++ b/modules/_bundled/sirsoft-board/src/Providers/BoardServiceProvider.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Board\Providers; use App\Extension\BaseModuleServiceProvider; use App\Seo\SitemapGenerator; use Modules\Sirsoft\Board\Console\Commands\AggregateBoardStatsCommand; +use Modules\Sirsoft\Board\Console\Commands\PruneAttachmentsCommand; use Modules\Sirsoft\Board\Repositories\AttachmentRepository; use Modules\Sirsoft\Board\Repositories\BoardRepository; use Modules\Sirsoft\Board\Repositories\BoardStatRepository; @@ -88,6 +89,7 @@ class BoardServiceProvider extends BaseModuleServiceProvider */ protected array $commands = [ AggregateBoardStatsCommand::class, + PruneAttachmentsCommand::class, ]; /** diff --git a/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php index 1549224b..af2419f6 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/AttachmentRepository.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Board\Repositories; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Board\Enums\PostStatus; use Modules\Sirsoft\Board\Enums\TriggerType; @@ -77,6 +78,27 @@ class AttachmentRepository implements AttachmentRepositoryInterface return $post->deleted_at !== null || $post->status === PostStatus::Deleted; } + /** + * 첨부파일이 속한 게시글을 게시판 스코프로 조회합니다(비밀 게이팅용). + * + * @param string $slug 게시판 슬러그 + * @param int $postId 게시글 ID + * @return Post|null 게시글 모델 또는 null + */ + public function findPostForGate(string $slug, int $postId): ?Post + { + $board = Board::where('slug', $slug)->first(); + + if (! $board) { + return null; + } + + return Post::withTrashed() + ->where('board_id', $board->id) + ->where('id', $postId) + ->first(['id', 'board_id', 'user_id', 'is_secret', 'status', 'deleted_at']); + } + /** * 여러 ID로 첨부파일 조회 (order 정렬) * @@ -396,6 +418,60 @@ class AttachmentRepository implements AttachmentRepositoryInterface ]); } + /** + * 여러 게시글의 살아있는 첨부를 cascade 로 일괄 소프트 삭제합니다. + * + * 답글 트리 연쇄 삭제(softDeleteCascadeByParentId)로 지워지는 자손 게시글들의 + * 첨부 정리용입니다. 단건 softDeleteByPostId 와 동일한 쿼리를 whereIn 으로 확장했습니다. + * 물리 파일은 보존하며 deleted_at 마킹만 수행합니다. (복원 가능성 유지) + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 삭제된 첨부 수 + */ + public function softDeleteByPostIds(string $slug, array $postIds): int + { + if ($postIds === []) { + return 0; + } + + $board = Board::where('slug', $slug)->first(); + + return Attachment::where('board_id', $board?->id) + ->whereIn('post_id', $postIds) + ->update([ + 'trigger_type' => TriggerType::Cascade->value, + 'deleted_at' => now(), + ]); + } + + /** + * 여러 게시글의 cascade 로 지워진 첨부만 일괄 복원합니다. + * + * 답글 트리 연쇄 복원(restoreCascadedByParentId)으로 되살아난 자손 게시글들의 + * 첨부 복원용입니다. 사용자가 직접 지운 첨부(trigger_type='user')는 유지됩니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 복원된 첨부 수 + */ + public function restoreCascadedByPostIds(string $slug, array $postIds): int + { + if ($postIds === []) { + return 0; + } + + $board = Board::where('slug', $slug)->first(); + + return Attachment::onlyTrashed() + ->where('board_id', $board?->id) + ->whereIn('post_id', $postIds) + ->where('trigger_type', TriggerType::Cascade->value) + ->update([ + 'deleted_at' => null, + ]); + } + /** * 게시판 ID 기준으로 첨부파일을 일괄 영구 삭제합니다. * @@ -410,4 +486,51 @@ class AttachmentRepository implements AttachmentRepositoryInterface { return Attachment::where('board_id', $boardId)->forceDelete(); } + + /** + * 게시글에 연결되지 않은 채 방치된 임시 첨부를 오래된 순으로 조회합니다. + * + * @param Carbon $threshold 기준 시각 + * @param int $limit 최대 조회 건수 + * @return Collection 임시 첨부 목록 + */ + public function findStaleTempAttachments(Carbon $threshold, int $limit): Collection + { + return Attachment::query() + ->whereNotNull('temp_key') + ->whereNull('post_id') + ->where('created_at', '<', $threshold) + ->orderBy('created_at') + ->orderBy('id') + ->limit($limit) + ->get(['id', 'board_id', 'temp_key', 'disk', 'path', 'created_at']); + } + + /** + * 소프트 삭제된 지 오래된 첨부를 오래된 순으로 조회합니다. + * + * @param Carbon $threshold 기준 시각 + * @param int $limit 최대 조회 건수 + * @return Collection 소프트 삭제 첨부 목록 + */ + public function findSoftDeletedOlderThan(Carbon $threshold, int $limit): Collection + { + return Attachment::onlyTrashed() + ->where('deleted_at', '<', $threshold) + ->orderBy('deleted_at') + ->orderBy('id') + ->limit($limit) + ->get(['id', 'board_id', 'post_id', 'disk', 'path', 'deleted_at']); + } + + /** + * 첨부 레코드를 영구 삭제합니다. + * + * @param Attachment $attachment 첨부파일 모델 + * @return bool 삭제 성공 여부 + */ + public function forceDelete(Attachment $attachment): bool + { + return (bool) $attachment->forceDelete(); + } } diff --git a/modules/_bundled/sirsoft-board/src/Repositories/BoardRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/BoardRepository.php index 83915bbb..25bd56f1 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/BoardRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/BoardRepository.php @@ -126,6 +126,21 @@ class BoardRepository implements BoardRepositoryInterface return $board->delete(); } + /** + * 게시판을 영구 삭제합니다. + * + * @param int $id 게시판 ID + * @return bool 삭제 성공 여부 + * + * @throws ModelNotFoundException + */ + public function forceDelete(int $id): bool + { + $board = $this->findOrFail($id); + + return (bool) $board->forceDelete(); + } + /** * 모든 게시판을 조회합니다. * @@ -180,6 +195,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function filterByType(string $type): Collection { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 return Board::where('type', $type)->get(); } @@ -203,6 +219,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getRecentPosts(int $limit): array { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 $activeBoardIds = Board::where('is_active', true)->pluck('id'); if ($activeBoardIds->isEmpty()) { @@ -220,6 +237,8 @@ class BoardRepository implements BoardRepositoryInterface ->whereNull('deleted_at') ->whereNull('parent_id') ->where('status', 'published') + // 비밀글도 제목은 공개한다(본문만 보호) — 2026-02-04 확정 정책. 목록/검색/홈과 동일. + // 게시판별 열람 권한은 BoardService::getCachedRecentPosts 가 응답 시점에 적용한다. ->orderBy('created_at', 'desc') ->limit($limit) ); @@ -400,6 +419,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getTotalCommentsCount(): int { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 $activeBoardIds = Board::where('is_active', true)->pluck('id'); return Comment::query() @@ -424,6 +444,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getPopularPosts(string $period, int $limit): array { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 $activeBoardIds = Board::where('is_active', true)->pluck('id'); if ($activeBoardIds->isEmpty()) { @@ -486,10 +507,10 @@ class BoardRepository implements BoardRepositoryInterface $viewCount = $post->view_count ?? 0; $commentCount = $post->comments_count ?? 0; - // Avatar URL 결정 + // Avatar URL 결정 — 코어 첨부 URL 조립은 코어 모델 단일 지점에 위임 $avatarUrl = null; if ($post->user_id && $post->attachment_hash) { - $avatarUrl = '/api/attachment/'.$post->attachment_hash; + $avatarUrl = Attachment::urlForHash($post->attachment_hash); } // 사용자 상태 처리 (탈퇴한 사용자는 익명화) @@ -560,6 +581,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getActiveBoardsList(): Collection { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 return Board::where('is_active', true) ->orderBy('created_at', 'asc') ->get(); @@ -620,6 +642,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getActiveBoardsOrdered(string $orderBy = 'created_at', string $orderDirection = 'desc'): Collection { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 return Board::where('is_active', true) ->orderBy($orderBy, $orderDirection) ->get(); @@ -648,6 +671,7 @@ class BoardRepository implements BoardRepositoryInterface */ public function getActiveBoardsForMenu(): Collection { + // audit:allow query-unbounded-get reason: boards 는 운영자 등록 설정성 테이블 — 행 수가 운영자 행위에 묶여 데이터 증가에 비례하지 않는다 return Board::where('is_active', true) ->select(['id', 'name', 'slug']) ->orderBy('created_at', 'asc') diff --git a/modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php index 671fe972..0bd5157c 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/CommentRepository.php @@ -700,6 +700,59 @@ class CommentRepository implements CommentRepositoryInterface ]); } + /** + * 여러 게시글의 살아있는 댓글을 cascade 로 일괄 소프트 삭제합니다. + * + * 답글 트리 연쇄 삭제(softDeleteCascadeByParentId)로 지워지는 자손 게시글들의 + * 댓글 정리용입니다. 단건 softDeleteByPostId 와 동일한 쿼리를 whereIn 으로 확장했습니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 삭제된 댓글 수 + */ + public function softDeleteByPostIds(string $slug, array $postIds): int + { + if ($postIds === []) { + return 0; + } + + $board = Board::where('slug', $slug)->first(); + + return Comment::where('board_id', $board?->id) + ->whereIn('post_id', $postIds) + ->update([ + 'trigger_type' => TriggerType::Cascade->value, + 'deleted_at' => now(), + ]); + } + + /** + * 여러 게시글의 cascade 로 지워진 댓글만 일괄 복원합니다. + * + * 답글 트리 연쇄 복원(restoreCascadedByParentId)으로 되살아난 자손 게시글들의 + * 댓글 복원용입니다. 사용자가 직접 지운 댓글(trigger_type='user')은 유지됩니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 복원된 댓글 수 + */ + public function restoreCascadedByPostIds(string $slug, array $postIds): int + { + if ($postIds === []) { + return 0; + } + + $board = Board::where('slug', $slug)->first(); + + return Comment::onlyTrashed() + ->where('board_id', $board?->id) + ->whereIn('post_id', $postIds) + ->where('trigger_type', TriggerType::Cascade->value) + ->update([ + 'deleted_at' => null, + ]); + } + /** * 사용자가 작성한 댓글 목록을 페이지네이션하여 조회합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php index 7f3cb3e3..64dacb29 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/AttachmentRepositoryInterface.php @@ -3,7 +3,9 @@ namespace Modules\Sirsoft\Board\Repositories\Contracts; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Modules\Sirsoft\Board\Models\Attachment; +use Modules\Sirsoft\Board\Models\Post; /** * 게시판 첨부파일 Repository 인터페이스 @@ -39,6 +41,18 @@ interface AttachmentRepositoryInterface */ public function isPostDeleted(string $slug, int $postId): bool; + /** + * 첨부파일이 속한 게시글을 게시판 스코프로 조회합니다(비밀 게이팅용). + * + * 비밀글 첨부 서빙 시 부모 게시글의 소유자/비밀 여부를 판정하기 위해 사용합니다. + * 게시판을 찾을 수 없거나 게시글이 없으면 null 을 반환합니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 게시글 ID + * @return Post|null 게시글 모델 또는 null + */ + public function findPostForGate(string $slug, int $postId): ?Post; + /** * 여러 ID로 첨부파일 조회 (order 정렬) * @@ -174,6 +188,24 @@ interface AttachmentRepositoryInterface */ public function restoreCascadedByPostId(string $slug, int $postId): int; + /** + * 여러 게시글의 살아있는 첨부를 cascade 로 일괄 소프트 삭제합니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 삭제된 첨부 수 + */ + public function softDeleteByPostIds(string $slug, array $postIds): int; + + /** + * 여러 게시글의 cascade 로 지워진 첨부만 일괄 복원합니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 복원된 첨부 수 + */ + public function restoreCascadedByPostIds(string $slug, array $postIds): int; + /** * 게시판 ID 기준으로 첨부파일을 일괄 영구 삭제합니다. * @@ -185,4 +217,36 @@ interface AttachmentRepositoryInterface * @return int 삭제된 첨부파일 수 */ public function forceDeleteByBoardId(int $boardId): int; + + /** + * 게시글에 연결되지 않은 채 방치된 임시 첨부를 오래된 순으로 조회합니다. + * + * 글쓰기 폼에서 업로드만 하고 저장 없이 이탈하면 `temp_key` 가 남은 채 + * `board_id = 0` 인 행과 그 파일이 영구 잔존합니다. 그 회수 대상을 찾습니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 업로드가 대상) + * @param int $limit 최대 조회 건수 + * @return Collection 임시 첨부 목록 (created_at 오름차순) + */ + public function findStaleTempAttachments(Carbon $threshold, int $limit): Collection; + + /** + * 소프트 삭제된 지 오래된 첨부를 오래된 순으로 조회합니다. + * + * 첨부 삭제는 소프트 삭제만 수행하므로(휴지통 복원 대비) 물리 파일은 남아 있습니다. + * 보존기간이 지난 뒤 파일과 기록을 함께 파기할 대상을 찾습니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 삭제분이 대상) + * @param int $limit 최대 조회 건수 + * @return Collection 소프트 삭제 첨부 목록 (deleted_at 오름차순) + */ + public function findSoftDeletedOlderThan(Carbon $threshold, int $limit): Collection; + + /** + * 첨부 레코드를 영구 삭제합니다. + * + * @param Attachment $attachment 첨부파일 모델 + * @return bool 삭제 성공 여부 + */ + public function forceDelete(Attachment $attachment): bool; } diff --git a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/BoardRepositoryInterface.php b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/BoardRepositoryInterface.php index b31f471a..db070514 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/BoardRepositoryInterface.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/BoardRepositoryInterface.php @@ -68,6 +68,16 @@ interface BoardRepositoryInterface */ public function delete(int $id): bool; + /** + * 게시판을 영구 삭제합니다. + * + * @param int $id 게시판 ID + * @return bool 삭제 성공 여부 + * + * @throws ModelNotFoundException + */ + public function forceDelete(int $id): bool; + /** * 모든 게시판을 조회합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/CommentRepositoryInterface.php b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/CommentRepositoryInterface.php index e2eb7458..7f924965 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/CommentRepositoryInterface.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/CommentRepositoryInterface.php @@ -213,6 +213,24 @@ interface CommentRepositoryInterface */ public function restoreCascadedByPostId(string $slug, int $postId): int; + /** + * 여러 게시글의 살아있는 댓글을 cascade 로 일괄 소프트 삭제합니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 삭제된 댓글 수 + */ + public function softDeleteByPostIds(string $slug, array $postIds): int; + + /** + * 여러 게시글의 cascade 로 지워진 댓글만 일괄 복원합니다. + * + * @param string $slug 게시판 슬러그 + * @param array<int> $postIds 게시글 ID 배열 + * @return int 복원된 댓글 수 + */ + public function restoreCascadedByPostIds(string $slug, array $postIds): int; + /** * 사용자가 작성한 댓글 목록을 페이지네이션하여 조회합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/PostRepositoryInterface.php b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/PostRepositoryInterface.php index 128721f8..61869259 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/Contracts/PostRepositoryInterface.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/Contracts/PostRepositoryInterface.php @@ -368,6 +368,59 @@ interface PostRepositoryInterface */ public function findFirstReplyWithBoard(int $parentPostId): ?Post; + /** + * 부모 게시글 ID로 살아있는 답변(자식) 게시글 수를 조회합니다. + * + * @param int $parentPostId 부모 게시글 ID + * @return int 살아있는 자식 게시글 수 + */ + public function countRepliesByParentId(int $parentPostId): int; + + /** + * 게시글에 살아있는 직계 답글이 있는지 확인합니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 게시글 ID + * @return bool 살아있는 직계 답글 존재 여부 + */ + public function hasAliveReplies(string $slug, int $postId): bool; + + /** + * 게시글의 전체 자손(답글 트리) ID 를 수집합니다 (withTrashed 순회 + 방문 가드). + * + * @param string $slug 게시판 슬러그 + * @param int $postId 루트 게시글 ID + * @return array<int> 자손 게시글 ID 배열 (루트 미포함) + */ + public function collectDescendantIds(string $slug, int $postId): array; + + /** + * 게시글의 살아있는 자손 답글 전체를 cascade 로 일괄 소프트 삭제합니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 부모 게시글 ID + * @return array<int> 소프트 삭제된 자손 게시글 ID 배열 + */ + public function softDeleteCascadeByParentId(string $slug, int $postId): array; + + /** + * 게시글 복원 시, cascade 로 지워진 자손 답글만 top-down 으로 선택 복원합니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 복원된 부모 게시글 ID + * @return array<int> 복원된 자손 게시글 ID 배열 + */ + public function restoreCascadedByParentId(string $slug, int $postId): array; + + /** + * 게시판의 전체 게시글 ID(withTrashed) 를 청크 단위로 순회하며 콜백에 전달합니다. + * + * @param int $boardId 게시판 ID + * @param int $size 청크 크기 + * @param callable $callback 청크마다 호출될 콜백 (int[] $postIds) + */ + public function eachIdChunkByBoardId(int $boardId, int $size, callable $callback): void; + /** * 게시글의 comments_count 컬럼을 활성 댓글 수로 재계산해 갱신합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php index 83614e87..b4213362 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php @@ -1064,7 +1064,7 @@ class PostRepository implements PostRepositoryInterface * 사용자가 작성한 게시글, 댓글을 단 게시글을 통합하여 반환합니다. * * @param int $userId 사용자 ID - * @param array $filters 필터 조건 (board_slug, search, activity_type, sort, is_public) + * @param array $filters 필터 조건 (board_slug, search, activity_type, sort, viewer_id, exclude_board_slugs) * @param int $perPage 페이지당 항목 수 * @return LengthAwarePaginator 게시글 활동 목록 */ @@ -1074,7 +1074,16 @@ class PostRepository implements PostRepositoryInterface $search = $filters['search'] ?? null; $activityType = $filters['activity_type'] ?? 'authored'; $sort = $filters['sort'] ?? 'latest'; // latest, oldest, views - $isPublic = $filters['is_public'] ?? false; // 공개 프로필용 필터 (비밀글 제외, 공개 게시글만) + // 열람자 관점. 이 값이 대상 사용자와 다르면(비로그인 포함) **타인 관점**이므로 + // 비밀글·미발행글의 **본문(content_plain)만 비운다** — 행·제목·배지는 목록 화면과 + // 동일하게 유지한다(제목 공개 정책 2026-02-04, 행 제거가 아니라 본문 마스킹이 의도). + // + // 종전에는 `is_public` 옵트인 플래그로 이 판정을 했는데, 그 키를 설정하는 코드가 + // 저장소 어디에도 없어서 필터가 한 번도 적용되지 않았다(사문). 옵트인은 호출부가 + // 빠뜨리면 조용히 열리는 방향이라, 열람자 신원으로 판정하는 fail-closed 로 뒤집는다 — + // viewer_id 가 없으면 자동으로 가장 좁은 가시성이 된다. + $viewerId = $filters['viewer_id'] ?? null; + $isOwnView = $viewerId !== null && $viewerId === $userId; $excludeBoardSlugs = $filters['exclude_board_slugs'] ?? []; // board_slug 필터용 board_id 조회 @@ -1099,12 +1108,15 @@ class PostRepository implements PostRepositoryInterface $cachedTotal = $filters['cached_total'] ?? null; - if ($activityType === 'commented' && ! $isPublic) { - return $this->getUserCommentedActivities($userId, $boardIdFilter, $excludeBoardIds, $search, $orderColumn, $orderDirection, $perPage, $cachedTotal); + // 분기 선택은 activity_type 만 본다. 가시성은 각 분기 안에서 처리한다 — + // 여기서 열람자까지 보고 분기를 바꾸면 `commented` 요청이 조용히 `authored` 결과를 + // 돌려주게 되어 저장소 계약이 깨진다. + if ($activityType === 'commented') { + return $this->getUserCommentedActivities($userId, $boardIdFilter, $excludeBoardIds, $search, $orderColumn, $orderDirection, $perPage, $cachedTotal, $viewerId); } // authored (기본값, 공개 프로필 포함) - return $this->getUserAuthoredActivities($userId, $boardIdFilter, $excludeBoardIds, $search, $isPublic, $orderColumn, $orderDirection, $perPage, $cachedTotal); + return $this->getUserAuthoredActivities($userId, $boardIdFilter, $excludeBoardIds, $search, $isOwnView, $orderColumn, $orderDirection, $perPage, $cachedTotal); } /** @@ -1113,7 +1125,7 @@ class PostRepository implements PostRepositoryInterface * @param int $userId 사용자 ID * @param int|null $boardIdFilter 게시판 ID 필터 * @param string|null $search 검색 키워드 - * @param bool $isPublic 공개 프로필 여부 (비밀글 제외) + * @param bool $isOwnView 열람자가 대상 본인인지 여부 (아니면 비밀글·미발행글 제외) * @param string $orderColumn 정렬 컬럼 * @param string $orderDirection 정렬 방향 * @param int $perPage 페이지당 항목 수 @@ -1123,7 +1135,7 @@ class PostRepository implements PostRepositoryInterface ?int $boardIdFilter, array $excludeBoardIds, ?string $search, - bool $isPublic, + bool $isOwnView, string $orderColumn, string $orderDirection, int $perPage, @@ -1145,11 +1157,6 @@ class PostRepository implements PostRepositoryInterface $query->whereNotIn('board_posts.board_id', $allExcludeIds); } - if ($isPublic) { - $query->where('board_posts.status', PostStatus::Published->value) - ->where('board_posts.is_secret', false); - } - if ($search) { $keyword = $this->escapeLikeKeyword($search); $query->where(function ($q) use ($keyword) { @@ -1180,7 +1187,15 @@ class PostRepository implements PostRepositoryInterface ); // paginate 후 10건에만 PHP 가공 적용 (N+1 아님) - $paginator->through(function ($post) { + $paginator->through(function ($post) use ($isOwnView) { + // 이 목록은 본문 일부(content_plain)를 함께 싣는다. 타인이 볼 때 비밀글·블라인드 + // 글의 본문이 그대로 나가던 것이 결함이었다 — 행과 제목은 게시판 목록에서 이미 + // 같은 수준으로 보이므로(PostResource 의 목록 규칙: 제목은 노출, 본문만 차단) + // 여기서도 **행은 남기고 본문만** 비운다. 행을 지우면 프로필의 비밀글/블라인드 + // 배지가 사문이 되어 필요 이상으로 기능이 깎인다. + $hideContent = ! $isOwnView + && ((bool) $post->is_secret || $post->status === PostStatus::Blinded); + return [ 'id' => $post->id, 'board_slug' => $post->board?->slug, @@ -1194,9 +1209,11 @@ class PostRepository implements PostRepositoryInterface 'comment_count' => (int) ($post->comments_count ?? 0), 'created_at' => $this->formatCreatedAt($post->created_at), 'created_at_formatted' => $this->formatCreatedAtFormat($post->created_at, g7_module_settings('sirsoft-board', 'display.date_display_format', 'standard')), - 'content_plain' => ($post->content_mode ?? 'text') === 'html' - ? $this->stripHtmlToPlainText($post->content ?? '') - : ($post->content ?? ''), + 'content_plain' => $hideContent + ? '' + : (($post->content_mode ?? 'text') === 'html' + ? $this->stripHtmlToPlainText($post->content ?? '') + : ($post->content ?? '')), ]; }); @@ -1222,7 +1239,8 @@ class PostRepository implements PostRepositoryInterface string $orderColumn, string $orderDirection, int $perPage, - ?int $cachedTotal = null + ?int $cachedTotal = null, + ?int $viewerId = null ): LengthAwarePaginator { // 테이블명은 모델에서 얻는다 — 문자열로 박으면 테이블명이 바뀔 때 조용히 깨진다 $postsTable = (new Post)->getTable(); @@ -1308,7 +1326,15 @@ class PostRepository implements PostRepositoryInterface ); // paginate 후 10건에만 PHP 가공 적용 - $paginator->through(function ($post) { + $paginator->through(function ($post) use ($viewerId) { + // 이 목록은 "내가 댓글 단 글" 이라 **타인이 쓴 비밀글**이 섞인다. 행은 내 활동 + // 기록이므로 남기되 본문은 내보내지 않는다 — 목록 미리보기를 빈 문자열로 만드는 + // PostResource::getMaskedContentPreviewForList 와 같은 규칙이다. + // (블라인드 글도 동일: 상세·목록 어느 경로에서도 본문이 나가지 않는다.) + // 열람자 미상($viewerId === null)이면 비밀글은 전부 가린다(fail-closed). + $hideContent = ((bool) $post->is_secret && (int) $post->user_id !== $viewerId) + || $post->status === PostStatus::Blinded; + return [ 'id' => $post->id, 'board_slug' => $post->board?->slug, @@ -1322,9 +1348,11 @@ class PostRepository implements PostRepositoryInterface 'comment_count' => (int) ($post->comments_count ?? 0), 'created_at' => $this->formatCreatedAt($post->created_at), 'created_at_formatted' => $this->formatCreatedAtFormat($post->created_at, g7_module_settings('sirsoft-board', 'display.date_display_format', 'standard')), - 'content_plain' => ($post->content_mode ?? 'text') === 'html' - ? $this->stripHtmlToPlainText($post->content ?? '') - : ($post->content ?? ''), + 'content_plain' => $hideContent + ? '' + : (($post->content_mode ?? 'text') === 'html' + ? $this->stripHtmlToPlainText($post->content ?? '') + : ($post->content ?? '')), ]; }); @@ -1692,6 +1720,207 @@ class PostRepository implements PostRepositoryInterface ->first(); } + /** + * 부모 게시글 ID로 살아있는 답변(자식) 게시글 수를 조회합니다. + * + * SoftDeletes 전역 스코프가 삭제된 답변을 자동 제외하므로, + * "삭제된 답변 후 재등록 허용" 판정의 근거로 사용됩니다. + * + * @param int $parentPostId 부모 게시글 ID + * @return int 살아있는 자식 게시글 수 + */ + public function countRepliesByParentId(int $parentPostId): int + { + return Post::where('parent_id', $parentPostId)->count(); + } + + /** + * 게시글에 살아있는 직계 답글이 있는지 확인합니다. + * + * 답글 삭제 정책(block) 의 차단 판정 기준입니다. 살아있는 자손은 살아있는 + * 부모 체인이 필요하므로 직계 검사만으로 판정이 완결됩니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 게시글 ID + * @return bool 살아있는 직계 답글 존재 여부 + */ + public function hasAliveReplies(string $slug, int $postId): bool + { + $board = Board::where('slug', $slug)->first(); + + return Post::where('board_id', $board?->id) + ->where('parent_id', $postId) + ->exists(); + } + + /** + * 게시글의 전체 자손(답글 트리) ID 를 수집합니다. + * + * 반복 BFS + 방문 ID 집합 가드로 순환 데이터에서도 유한 종료를 보장합니다(규정). + * 삭제된 중간 노드 밑의 자손도 도달해야 하므로 순회는 withTrashed 로 수행합니다 + * (끊긴 체인 밑 과거 고아 스윕의 전제). + * + * @param string $slug 게시판 슬러그 + * @param int $postId 루트 게시글 ID + * @return array<int> 자손 게시글 ID 배열 (루트 미포함) + */ + public function collectDescendantIds(string $slug, int $postId): array + { + $board = Board::where('slug', $slug)->first(); + + if ($board === null) { + return []; + } + + $visited = [$postId => true]; + $frontier = [$postId]; + $descendants = []; + + while ($frontier !== []) { + // audit:allow query-unbounded-get reason: 대상은 한 원글의 답글 트리 한 레벨 — 삭제/복원은 트리 전체가 하나의 의사표시라 단일 트랜잭션에서 전량 확보해야 하며(부분 반영 시 원글과 답글 상태가 어긋남), visited 가드가 유한 종료를 보장한다 + $children = Post::withTrashed() + ->where('board_id', $board->id) + ->whereIn('parent_id', $frontier) + ->pluck('id') + ->all(); + + $next = []; + foreach ($children as $childId) { + if (isset($visited[$childId])) { + continue; + } + $visited[$childId] = true; + $next[] = $childId; + $descendants[] = $childId; + } + + $frontier = $next; + } + + return $descendants; + } + + /** + * 게시글의 살아있는 자손 답글 전체를 cascade 로 일괄 소프트 삭제합니다. + * + * 자손 중 살아있는 것만 단일 UPDATE 로 `status='deleted', trigger_type='cascade', + * deleted_at=now()` 마킹합니다 (status 포함: 관리자 삭제 탭 필터가 status 기준). + * 이미 trashed 인 자손(trigger 'user' 등)은 기본 스코프 밖이라 변조되지 않습니다. + * 작업 이력(action_log)은 부모에만 남깁니다 — 댓글 cascade 와 동일한 규약입니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 부모 게시글 ID + * @return array<int> 소프트 삭제된 자손 게시글 ID 배열 + */ + public function softDeleteCascadeByParentId(string $slug, int $postId): array + { + $descendantIds = $this->collectDescendantIds($slug, $postId); + + if ($descendantIds === []) { + return []; + } + + // 살아있는 자손만 선별 (기본 스코프가 trashed 제외) + $aliveIds = Post::whereIn('id', $descendantIds)->pluck('id')->all(); + + if ($aliveIds === []) { + return []; + } + + Post::whereIn('id', $aliveIds)->update([ + 'status' => PostStatus::Deleted->value, + 'trigger_type' => TriggerType::Cascade->value, + 'deleted_at' => now(), + ]); + + return $aliveIds; + } + + /** + * 게시글 복원 시, cascade 로 지워진 자손 답글만 top-down 으로 선택 복원합니다. + * + * 레벨 순회 + 방문 가드: 각 레벨에서 `parent_id ∈ (복원됨 ∪ alive)` 인 + * cascade-trashed 자손만 복원합니다. 사용자 직접 삭제('user')로 trashed 인 + * 중간 노드는 복원되지 않고 그 서브트리도 그대로 유지됩니다(고아 재생성 금지). + * + * 트레이드오프(의도): 삭제 전 blinded 였던 자손도 복원 시 published 가 됩니다 — + * 부모 restorePost 의 기존 의미론(updateStatus published)과 동일합니다. + * + * @param string $slug 게시판 슬러그 + * @param int $postId 복원된 부모 게시글 ID + * @return array<int> 복원된 자손 게시글 ID 배열 + */ + public function restoreCascadedByParentId(string $slug, int $postId): array + { + $board = Board::where('slug', $slug)->first(); + + if ($board === null) { + return []; + } + + $visited = [$postId => true]; + $frontier = [$postId]; + $restored = []; + + while ($frontier !== []) { + // 이미 살아있는 자식 — 더 깊은 레벨의 cascade 복원 통로로만 사용 + $aliveChildren = Post::where('board_id', $board->id) + ->whereIn('parent_id', $frontier) + ->pluck('id') + ->all(); + + // cascade 로 지워진 자식만 복원 대상 + $toRestore = Post::onlyTrashed() + ->where('board_id', $board->id) + ->whereIn('parent_id', $frontier) + ->where('trigger_type', TriggerType::Cascade->value) + ->pluck('id') + ->all(); + + if ($toRestore !== []) { + Post::onlyTrashed()->whereIn('id', $toRestore)->update([ + 'status' => PostStatus::Published->value, + 'deleted_at' => null, + ]); + $restored = array_merge($restored, $toRestore); + } + + $next = []; + foreach (array_merge($aliveChildren, $toRestore) as $childId) { + if (isset($visited[$childId])) { + continue; + } + $visited[$childId] = true; + $next[] = $childId; + } + + $frontier = $next; + } + + return $restored; + } + + /** + * 게시판의 전체 게시글 ID 를 청크 단위로 순회하며 콜백에 전달합니다. + * + * 게시판 삭제 벌크 훅(board.posts.before_force_delete)의 페이로드 공급용입니다. + * withTrashed 포함(삭제 대상은 trashed 도 물리 제거되므로), chunkById(키셋) 로 + * OFFSET 밀림 없이 순회합니다. + * + * @param int $boardId 게시판 ID + * @param int $size 청크 크기 + * @param callable $callback 청크마다 호출될 콜백 (int[] $postIds) + */ + public function eachIdChunkByBoardId(int $boardId, int $size, callable $callback): void + { + Post::withTrashed() + ->where('board_id', $boardId) + ->select('id') + ->chunkById($size, function ($posts) use ($callback) { + $callback($posts->pluck('id')->all()); + }); + } + /** * 비활성 게시판 ID 목록을 조회합니다. * @@ -1780,6 +2009,10 @@ class PostRepository implements PostRepositoryInterface return Post::query() ->whereNull('deleted_at') ->whereNull('parent_id') + // 노출 제한 필터 — 미발행(블라인드·삭제)·비활성 게시판 글은 대시보드 최신글에서 제외한다. + // 비밀글은 제목 공개 정책(2026-02-04)에 따라 관리자에게 제목을 노출한다(제외하지 않음). + ->where('status', PostStatus::Published->value) + ->whereHas('board', fn ($q) => $q->where('is_active', true)) ->with(['board', 'user']) ->orderByDesc('created_at') ->limit($limit) diff --git a/modules/_bundled/sirsoft-board/src/Repositories/ReportRepository.php b/modules/_bundled/sirsoft-board/src/Repositories/ReportRepository.php index bc373e19..a6a7eac8 100644 --- a/modules/_bundled/sirsoft-board/src/Repositories/ReportRepository.php +++ b/modules/_bundled/sirsoft-board/src/Repositories/ReportRepository.php @@ -668,8 +668,15 @@ class ReportRepository implements ReportRepositoryInterface */ public function getPendingAcrossBoards(int $limit): Collection { - return Report::query() + $query = Report::query() ->whereIn('status', [ReportStatus::Pending, ReportStatus::Review]) + // 비활성 게시판의 신고는 대시보드 집계에서 제외한다(활성 게시판 스코프). + ->whereHas('board', fn ($q) => $q->where('is_active', true)); + + // 신고 조회 권한 스코프 필터링 (목록 엔드포인트와 동일 게이트) + PermissionHelper::applyPermissionScope($query, 'sirsoft-board.reports.view'); + + return $query ->with(['board', 'author']) ->orderByDesc('last_reported_at') ->limit($limit) @@ -683,8 +690,13 @@ class ReportRepository implements ReportRepositoryInterface */ public function countPendingAcrossBoards(): int { - return Report::query() + $query = Report::query() ->whereIn('status', [ReportStatus::Pending, ReportStatus::Review]) - ->count(); + // 목록(getPendingAcrossBoards)과 동일 스코프여야 배지 건수와 목록 건수가 일치한다. + ->whereHas('board', fn ($q) => $q->where('is_active', true)); + + PermissionHelper::applyPermissionScope($query, 'sirsoft-board.reports.view'); + + return $query->count(); } } diff --git a/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php b/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php index 5109ed07..9e335886 100644 --- a/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php +++ b/modules/_bundled/sirsoft-board/src/Services/AttachmentService.php @@ -9,6 +9,7 @@ use App\Support\ImageResizer; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -16,6 +17,7 @@ use Modules\Sirsoft\Board\Exceptions\AttachmentLimitExceededException; use Modules\Sirsoft\Board\Models\Attachment; use Modules\Sirsoft\Board\Repositories\Contracts\AttachmentRepositoryInterface; use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; +use Modules\Sirsoft\Board\Support\SecretContentGate; use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -345,6 +347,45 @@ class AttachmentService } } + /** + * 비밀글 첨부파일 접근 권한을 검증합니다(KVE-2026-1914). + * + * 첨부가 속한 게시글이 비밀글이면 SecretContentGate(SSoT) 판정을 통과한 + * 요청(작성자 본인 또는 게시판 manager/posts.read-secret)에만 서빙합니다. + * 첨부 서빙은 상세 요청과 분리된 별도 요청이라 password_verified 는 세팅되지 + * 않으므로, 비회원이 비밀번호로 검증한 경우는 이 경로에서 인정되지 않습니다 + * (안전 측 실패 — 해시/ID 만으로 비밀글 첨부를 가져가는 것을 차단). + * + * @param string $slug 게시판 슬러그 + * @param Attachment $attachment 첨부파일 모델 + * + * @throws AccessDeniedHttpException 비밀글 첨부에 권한 없이 접근한 경우 + */ + private function assertSecretPostAttachmentAccess(string $slug, Attachment $attachment): void + { + if (! $attachment->post_id) { + return; + } + + $post = $this->repository->findPostForGate($slug, $attachment->post_id); + + // 부모 글을 못 읽으면 막는다(fail-closed). 첨부에 post_id 가 있는데 그 글을 못 찾는 + // 것은 정상 상태가 아니다 — 슬러그가 다른 게시판이거나 글이 사라진 경우이며, 통과시키면 + // 게이트가 있어야 할 자리에서 무게이트가 된다. 조회는 withTrashed 라 소프트 삭제로는 + // null 이 되지 않는다. + if (! $post) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + + if (! $post->is_secret) { + return; + } + + if (! app(SecretContentGate::class)->canView($post)) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + } + /** * ID로 첨부파일 조회 * @@ -380,9 +421,10 @@ class AttachmentService * * @param string $slug 게시판 슬러그 * @param int $id 첨부파일 ID + * @param string $context 호출 컨텍스트 (admin | user) — 스코프 권한 식별자 결정에 쓰인다 * @return bool 삭제 성공 여부 */ - public function delete(string $slug, int $id): bool + public function delete(string $slug, int $id, string $context = 'user'): bool { $attachment = $this->repository->findById($slug, $id); @@ -390,6 +432,8 @@ class AttachmentService return false; } + $this->assertAttachmentWithinScope($slug, $attachment, $context); + // 삭제 후 재정렬을 위해 정보 저장 $postId = $attachment->post_id; $collection = $attachment->collection; @@ -397,8 +441,9 @@ class AttachmentService // Before 훅 HookManager::doAction('sirsoft-board.attachment.before_delete', $attachment); - // 물리 파일은 삭제하지 않음 — 소프트 딜리트만 수행 - // 추후 배치 작업(Artisan Command + Scheduler)으로 보존 기간 경과 후 정리 예정 + // 물리 파일은 삭제하지 않음 — 소프트 딜리트만 수행 (휴지통 복원 대비). + // 보존기간이 지난 뒤의 파일·기록 파기는 `sirsoft-board:prune-attachments` 가 + // 담당한다 (purgeSoftDeleted). 운영자가 설정에서 켰을 때만 동작한다. // DB에서 소프트 삭제 $result = $this->repository->delete($slug, $id); @@ -420,15 +465,163 @@ class AttachmentService return $result; } + /** + * 게시글에 연결되지 않은 채 방치된 임시 첨부를 정리합니다. + * + * 글쓰기 폼에서 파일만 올리고 저장하지 않고 이탈하면 `temp_key` 만 남은 행과 그 파일이 + * 남습니다. 연결 시점에 본경로로 옮겨지므로 `temp_key` 가 남아 있다는 것은 "끝내 연결되지 + * 않았다" 는 뜻이고, 그래서 오탐 여지가 없습니다. + * + * 파일 → 행 순서를 지켜, 행이 먼저 사라져 파일을 못 찾는 상태를 만들지 않습니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 처리할 최대 건수 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + public function pruneTempUploads(int $days, int $limit, bool $dryRun = false): array + { + $threshold = Carbon::now()->subDays($days); + $attachments = $this->repository->findStaleTempAttachments($threshold, $limit); + + $result = $this->purgeCollection($attachments, $dryRun, '임시 게시판 첨부'); + + if (! $dryRun) { + $this->removeEmptyTempDirectories($attachments); + } + + return $result; + } + + /** + * 파일을 모두 지운 temp_key 디렉토리를 정리합니다. + * + * 파일만 지우고 디렉토리를 남기면 폼 세션마다 빈 디렉토리가 쌓여, 정리를 돌려도 + * 저장소에는 흔적이 계속 늘어납니다. + * + * 디렉토리에 파일이 남아 있으면(같은 temp_key 의 다른 첨부가 limit 에 걸려 이번 회차에서 + * 빠진 경우 등) 삭제하지 않습니다. + * + * @param Collection $attachments 이번 회차에 처리한 첨부 목록 + */ + private function removeEmptyTempDirectories(Collection $attachments): void + { + $directories = []; + + foreach ($attachments as $attachment) { + $directory = dirname((string) $attachment->path); + + if ($directory === '' || $directory === '.') { + continue; + } + + $directories[$attachment->disk.'|'.$directory] = [$attachment->disk, $directory]; + } + + foreach ($directories as [$disk, $directory]) { + $storage = $this->storageForRow($disk); + + if ($storage->files('attachments', $directory) !== []) { + continue; + } + + $storage->deleteDirectory('attachments', $directory); + } + } + + /** + * 소프트 삭제된 지 보존기간이 지난 첨부를 파일까지 영구 파기합니다. + * + * 첨부 삭제는 소프트 삭제만 수행하므로 파일이 남아 있습니다. 게시글 복원 + * (restoreCascadedByPostId)은 첨부의 `deleted_at` 이 보존기간 안에 있을 때에만 첨부까지 + * 온전히 복원되므로, 보존기간은 휴지통 복원 가능 기간과 같게 유지해야 합니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 처리할 최대 건수 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + public function purgeSoftDeleted(int $days, int $limit, bool $dryRun = false): array + { + $threshold = Carbon::now()->subDays($days); + $attachments = $this->repository->findSoftDeletedOlderThan($threshold, $limit); + + return $this->purgeCollection($attachments, $dryRun, '소프트 삭제 게시판 첨부'); + } + + /** + * 첨부 컬렉션을 파일 → 기록 순으로 영구 파기합니다. + * + * @param Collection $attachments 대상 첨부 목록 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @param string $label 로그용 대상 설명 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + private function purgeCollection(Collection $attachments, bool $dryRun, string $label): array + { + $result = ['scanned' => $attachments->count(), 'deleted' => 0, 'failed' => 0]; + + if ($dryRun) { + return $result; + } + + foreach ($attachments as $attachment) { + $storage = $this->storageForRow($attachment->disk); + + if ($storage->exists('attachments', $attachment->path) && ! $storage->delete('attachments', $attachment->path)) { + Log::warning("{$label} 파일 삭제 실패 — 기록 보존", [ + 'attachment_id' => $attachment->id, + 'disk' => $attachment->disk, + 'path' => $attachment->path, + ]); + + $result['failed']++; + + continue; + } + + $this->repository->forceDelete($attachment); + $result['deleted']++; + } + + return $result; + } + + /** + * 첨부 행에 기록된 disk 기준 스토리지를 반환합니다. + * + * 디스크를 전환한 뒤에도 전환 이전 행의 파일을 그 행의 실제 저장 위치에서 지우기 위한 + * 해석입니다. 미등록 disk(그 디스크를 제공하던 확장이 비활성화된 경우)는 주입 스토리지로 + * 폴백합니다 — withDisk 로 미등록 disk 인스턴스를 만들면 이후 호출이 예외가 됩니다. + * + * @param string|null $disk 행의 disk 컬럼 값 + * @return StorageInterface 행 disk 의 스토리지 + */ + private function storageForRow(?string $disk): StorageInterface + { + if ($disk === null || $disk === '' || $disk === $this->storage->getDisk()) { + return $this->storage; + } + + if (config("filesystems.disks.{$disk}") === null) { + return $this->storage; + } + + return $this->storage->withDisk($disk); + } + /** * 순서 변경 * * @param string $slug 게시판 슬러그 * @param array<int, int> $orders 첨부파일 ID => order 매핑 + * @param string $context 호출 컨텍스트 (admin | user) — 스코프 권한 식별자 결정에 쓰인다 * @return bool 성공 여부 */ - public function reorder(string $slug, array $orders): bool + public function reorder(string $slug, array $orders, string $context = 'user'): bool { + $this->assertReorderWithinScope($slug, $orders, $context); + // Before 훅 HookManager::doAction('sirsoft-board.attachment.before_reorder', $slug, $orders); @@ -440,6 +633,63 @@ class AttachmentService return $result; } + /** + * 첨부가 액터의 스코프 안에 있는지 검사합니다. + * + * 첨부 관리 라우트는 `{id}`(정수)로 선언돼 라우트 모델 바인딩이 일어나지 않고, 순서 + * 변경은 아예 정적 경로다. 두 경우 모두 PermissionMiddleware 가 모델을 resolve 하지 + * 못해 스코프 검사를 건너뛰므로(목록 엔드포인트로 간주) 서비스가 재적용한다. + * 사용자 경로는 컨트롤러가 `canDelete`(작성자 본인)로 막고 있었으나 관리자 경로는 + * 그 대응물이 없어 비어 있었다 — 두 경로 모두 여기서 같은 판정을 받는다. + * + * @param string $slug 게시판 슬러그 + * @param Attachment $attachment 대상 첨부 + * + * @throws AccessDeniedHttpException 스코프 밖 첨부인 경우 + */ + private function assertAttachmentWithinScope(string $slug, Attachment $attachment, string $context): void + { + // 컨텍스트는 호출부가 명시한다 — PostService 가 쓰는 방식과 같다. 요청에서 + // 컨트롤러 네임스페이스를 스니핑하는 사설 복제본이 이미 4곳에 있는데 5번째를 + // 만들지 않는다. + $scopePermission = $context === 'admin' + ? "sirsoft-board.{$slug}.admin.attachments.upload" + : "sirsoft-board.{$slug}.attachments.upload"; + + if (! PermissionHelper::checkScopeAccess($attachment, $scopePermission)) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + } + + /** + * 순서 변경 대상 첨부 전체가 액터의 스코프 안에 있는지 검사합니다. + * + * 순서는 집합 전체에 대한 하나의 배열이라 일부만 반영하면 나머지와 어긋난다 — + * 걸러내지 않고 전량 거부한다(코어 첨부/메뉴 순서 변경과 같은 의미론). + * + * @param string $slug 게시판 슬러그 + * @param array<int, array{id: int, order: int}> $orders 순서 데이터 + * + * @throws AccessDeniedHttpException 스코프 밖 첨부가 하나라도 포함된 경우 + */ + private function assertReorderWithinScope(string $slug, array $orders, string $context): void + { + $ids = array_values(array_unique(array_filter( + array_map(static fn ($item): int => (int) ($item['id'] ?? 0), $orders) + ))); + + foreach ($ids as $id) { + $attachment = $this->repository->findById($slug, $id); + + // 이 게시판 소속이 아닌 id 는 통과시키지 않는다. + if (! $attachment) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + + $this->assertAttachmentWithinScope($slug, $attachment, $context); + } + } + /** * 삭제 후 남은 파일들의 순서를 재정렬합니다. * @@ -517,6 +767,9 @@ class AttachmentService // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 $this->assertDeletedPostAttachmentAccess($slug, $attachment); + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + // 다운로드 활동이력 기록 훅 // 권한/삭제글 가드 통과 후 발화 → 차단된 시도는 기록하지 않음. // $context('user'|'admin')는 로그 부가정보용 (log_type 은 요청 경로로 자동 결정). @@ -580,6 +833,9 @@ class AttachmentService // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 $this->assertDeletedPostAttachmentAccess($slug, $attachment); + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + return $this->storage->response( 'attachments', $attachment->path, @@ -597,11 +853,17 @@ class AttachmentService * 컨트롤러에서 fileResponse()로 캐싱 헤더와 함께 응답할 수 있도록 * 파일 경로와 메타 정보를 반환합니다. * + * $signatureVerified 는 요청 URL 의 한시 서명이 검증된 경우다 — 서명은 + * 비밀글·삭제글 게이트를 통과한 응답 직렬화(PostResource)만 발급하므로, + * 검증된 서명은 그 게이트 통과 자격의 위임으로 보고 콘텐츠 상태 게이트를 + * 재적용하지 않는다 (<img> 는 인증 헤더를 실을 수 없는 렌더 경로). + * * @param string $slug 게시판 슬러그 * @param int $id 첨부파일 ID + * @param bool $signatureVerified 한시 서명 검증 통과 여부 * @return array{path: string, mime_type: string, filename: string}|null 파일 정보 또는 null */ - public function getFileInfo(string $slug, int $id): ?array + public function getFileInfo(string $slug, int $id, bool $signatureVerified = false): ?array { $attachment = $this->repository->findById($slug, $id); @@ -609,8 +871,13 @@ class AttachmentService return null; } - // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 - $this->assertDeletedPostAttachmentAccess($slug, $attachment); + if (! $signatureVerified) { + // 삭제된 게시글의 첨부파일은 관리 권한자만 접근 + $this->assertDeletedPostAttachmentAccess($slug, $attachment); + + // 비밀글 첨부파일은 열람 권한자만 접근 + $this->assertSecretPostAttachmentAccess($slug, $attachment); + } // 파일 존재 확인 if (! $this->storage->exists('attachments', $attachment->path)) { diff --git a/modules/_bundled/sirsoft-board/src/Services/BoardService.php b/modules/_bundled/sirsoft-board/src/Services/BoardService.php index c6eaa3fa..93e44263 100644 --- a/modules/_bundled/sirsoft-board/src/Services/BoardService.php +++ b/modules/_bundled/sirsoft-board/src/Services/BoardService.php @@ -5,6 +5,8 @@ namespace Modules\Sirsoft\Board\Services; use App\Contracts\Extension\CacheInterface; use App\Contracts\Extension\StorageInterface; use App\Contracts\Repositories\MenuRepositoryInterface; +use App\Contracts\Repositories\RoleRepositoryInterface; +use App\Contracts\Repositories\UserRepositoryInterface; use App\Enums\ExtensionOwnerType; use App\Extension\HookManager; use App\Extension\Traits\ClearsTemplateCaches; @@ -12,6 +14,7 @@ use App\Helpers\PermissionHelper; use App\Models\Menu; use App\Models\Role; use App\Models\User; +use App\Repositories\Concerns\ResolvesSortSpec; use App\Services\MenuService; use App\Services\RoleService; use Illuminate\Contracts\Pagination\LengthAwarePaginator; @@ -40,6 +43,26 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class BoardService { use ClearsTemplateCaches; + use ResolvesSortSpec; + + /** + * 관리자 게시판 목록 정렬 허용 컬럼 화이트리스트 + * + * IndexBoardRequest 의 sort_by in: 목록과 동일해야 한다 (화면 ⊆ 게이트 ⊆ 저장소). + * `name` 은 JSON 다국어 컬럼이라 제외한다. + * + * @var array<int, string> + */ + private const BOARD_SORT_COLUMNS = [ + 'id', + 'slug', + 'type', + 'is_active', + 'posts_count', + 'comments_count', + 'created_at', + 'updated_at', + ]; /** * BoardService 생성자 @@ -53,6 +76,9 @@ class BoardService * @param RoleService $roleService 역할 서비스 * @param CacheInterface $cache 캐시 드라이버 * @param MenuRepositoryInterface $menuRepository 코어 메뉴 리포지토리 (메뉴 조회용) + * @param StorageInterface $storage 스토리지 드라이버 + * @param RoleRepositoryInterface $roleRepository 코어 역할 리포지토리 (게시판별 역할 동기화용) + * @param UserRepositoryInterface $userRepository 코어 사용자 리포지토리 (통계·UUID 변환용) */ public function __construct( private BoardRepositoryInterface $boardRepository, @@ -64,7 +90,9 @@ class BoardService private RoleService $roleService, private CacheInterface $cache, private MenuRepositoryInterface $menuRepository, - private StorageInterface $storage + private StorageInterface $storage, + private RoleRepositoryInterface $roleRepository, + private UserRepositoryInterface $userRepository ) {} /** @@ -99,13 +127,21 @@ class BoardService }); } - // 정렬 - $sortBy = $filters['sort_by'] ?? 'created_at'; - $sortOrder = $filters['sort_order'] ?? 'desc'; - $query->orderBy($sortBy, $sortOrder); + // 정렬 — 요청 유래 값은 닫힌 집합으로만 해석 (미허용 컬럼은 기본값 fallback: 이중 방어) + [$sortSpec] = $this->resolveSortSpec( + $filters, + self::BOARD_SORT_COLUMNS, + 'created_at', + ); + $query->orderBy($sortSpec['column'], $sortSpec['direction']); - // 페이지네이션 - $perPage = $filters['per_page'] ?? 20; + // 비고유 컬럼 동률 구간에서도 전순서 보장 (페이지 경계 중복/누락 방지 규정) + $query->orderBy('id', 'desc'); + + // 페이지네이션 (1~999 클램프 — 게이트 우회 내부 호출 대비 이중 방어) + // 상한 999 는 IndexBoardRequest 와 동일해야 한다: 게시판은 운영자 등록 + // 설정성 테이블이라 전체 목록 셀렉트 소비처(999/200)가 이 값을 쓴다. + $perPage = min(max((int) ($filters['per_page'] ?? 20), 1), 999); return $query->paginate($perPage); } @@ -114,6 +150,7 @@ class BoardService * ID로 게시판을 조회합니다. * * @param int $id 게시판 ID + * @return Board 게시판 모델 * * @throws ModelNotFoundException */ @@ -153,6 +190,7 @@ class BoardService * * @param string $orderBy 정렬 기준 (기본: created_at) * @param string $orderDirection 정렬 방향 (기본: desc) + * @return Collection 활성 게시판 컬렉션 */ public function getActiveBoards( string $orderBy = 'created_at', @@ -161,6 +199,29 @@ class BoardService return $this->boardRepository->getActiveBoardsOrdered($orderBy, $orderDirection); } + /** + * 활성 게시판 중 호출자가 열람할 수 있는 게시판만 조회합니다. + * + * 게시판 디렉토리(전체 게시판 목록 + 내장 최근글)처럼 비로그인에게도 열리는 + * 응답에 사용합니다 — 최근글·인기글·인기게시판과 동일한 열람 권한 기준 + * (sirsoft-board.{slug}.posts.read)을 호출자 시점에 적용해, 디렉토리 경로가 + * 비공개 게시판·글 제목의 우회 노출로가 되지 않게 합니다. + * + * @param string $orderBy 정렬 기준 컬럼 + * @param string $orderDirection 정렬 방향 + * @return Collection 호출자가 열람 가능한 활성 게시판 컬렉션 + */ + public function getReadableActiveBoards( + string $orderBy = 'created_at', + string $orderDirection = 'desc' + ): Collection { + $user = Auth::user(); + + return $this->getActiveBoards($orderBy, $orderDirection) + ->filter(fn ($board) => PermissionHelper::check("sirsoft-board.{$board->slug}.posts.read", $user)) + ->values(); + } + /** * 메뉴용 경량 게시판 목록을 조회합니다. * @@ -211,7 +272,7 @@ class BoardService $boardStats = $this->getActiveBoardStats(); return [ - 'users' => User::count(), + 'users' => $this->userRepository->getStatistics()['total_users'], 'boards' => $boardStats['boards'], 'posts' => $boardStats['posts'], 'comments' => $boardStats['comments'], @@ -229,12 +290,18 @@ class BoardService { $ttl = (int) g7_core_settings('cache.default_ttl', 86400); - return $this->cache->remember( + // 캐시는 사용자와 무관한 안전집합(발행 + 활성 게시판)으로만 채운다. + // 비밀글은 제목 공개 정책(2026-02-04)에 따라 제목을 포함한다(본문 미포함 — 사용자 무관 데이터). + // 게시판별 열람 권한은 캐시에 담지 않고 응답 시점에 호출자 기준으로 적용해, + // 고권한 사용자의 결과가 캐시를 오염시켜 저권한 사용자에게 새는 것을 막는다. + $posts = $this->cache->remember( "recent_posts_{$limit}", fn () => $this->getRecentPosts($limit), $ttl, tags: ['board-posts'] ); + + return $this->filterItemsByBoardReadPermission($posts, 'board_slug'); } /** @@ -244,17 +311,22 @@ class BoardService * * @param string $period 기간 (today, week, month, year) * @param int $limit 조회 개수 + * @return array 캐시된 인기글 목록 */ public function getCachedPopularPosts(string $period = 'week', int $limit = 20): array { $ttl = (int) g7_core_settings('cache.default_ttl', 86400); - return $this->cache->remember( + // 캐시는 안전집합(비밀글 제외 + 발행 + 활성 게시판)으로만 채우고, 게시판별 + // 열람 권한은 응답 시점에 호출자 기준으로 적용한다(캐시 오염 회피). + $posts = $this->cache->remember( "popular_posts_{$period}_{$limit}", fn () => $this->getPopularPosts($period, $limit), $ttl, tags: ['board-posts'] ); + + return $this->filterItemsByBoardReadPermission($posts, 'board_slug'); } /** @@ -267,12 +339,16 @@ class BoardService { $ttl = (int) g7_core_settings('cache.default_ttl', 86400); - return $this->cache->remember( + // 캐시는 활성 게시판 전체로 채우고, 게시판별 열람 권한은 응답 시점에 + // 호출자 기준으로 적용한다(캐시 오염 회피 — 저권한 사용자에게 고권한 결과가 새지 않도록). + $boards = $this->cache->remember( "popular_boards_{$limit}", fn () => $this->getPopularBoards($limit), $ttl, tags: ['board-list'] ); + + return $this->filterItemsByBoardReadPermission($boards, 'slug'); } /** @@ -508,6 +584,15 @@ class BoardService // (소프트 삭제 시 게시판이 사라진 뒤 접근 불가한 고아 데이터로 잔존) $this->attachmentRepository->forceDeleteByBoardId($board->id); $this->commentRepository->forceDeleteByBoardId($board->id); + + // 2-1. 훅: 삭제될 글 ID 목록을 연동 확장에 통지 (이커머스 문의 피벗 정리 등) + // withTrashed 전체를 1,000건 chunk 당 1회씩 다회 발화한다 — 리스너는 멱등 의무. + // 트랜잭션 내부 발화라 게시판 삭제와 원자적이다. per-post after_delete 훅을 + // 재사용하지 않는 이유: 글마다 모델 로드 N회 + 소프트/하드 삭제 의미 불일치. + $this->postRepository->eachIdChunkByBoardId($board->id, 1000, function (array $postIds) use ($board) { + HookManager::doAction('sirsoft-board.board.posts.before_force_delete', $board, $postIds); + }); + $this->postRepository->forceDeleteByBoardId($board->id); // 3. 게시판 권한 삭제 (그누보드7 규정: detach 후 삭제) @@ -519,13 +604,13 @@ class BoardService HookManager::doAction('sirsoft-board.roles.after_delete', $board->slug); // 5. 등록된 메뉴 제거 (addToAdminMenu()로 등록된 경우에만) - $menu = Menu::where('url', '/admin/board/'.$board->slug)->first(); + $menu = $this->menuRepository->findByUrl('/admin/board/'.$board->slug); if ($menu) { $this->menuService->deleteMenu($menu); } // 6. 게시판 영구 삭제 - $board->forceDelete(); + $this->boardRepository->forceDelete($board->id); }); Log::info('Board deleted', [ @@ -665,10 +750,10 @@ class BoardService $boardNameKo = $board->name['ko'] ?? $board->name['en'] ?? $board->slug; $boardNameEn = $board->name['en'] ?? $board->name['ko'] ?? $board->slug; - // 중복 방어: 이전 삭제 실패 등으로 role이 남아있을 수 있음 - $managerRole = Role::firstOrCreate( - ['identifier' => "sirsoft-board.{$board->slug}.manager"], - [ + // 중복 방어: 이전 삭제 실패 등으로 role이 남아있을 수 있음 (기존 role 은 갱신 없이 재사용 — firstOrCreate 동형) + $managerRole = $this->roleRepository->findByIdentifier("sirsoft-board.{$board->slug}.manager") + ?? $this->roleRepository->create([ + 'identifier' => "sirsoft-board.{$board->slug}.manager", 'name' => [ 'ko' => "{$boardNameKo} 게시판 관리자", 'en' => "{$boardNameEn} Board Manager", @@ -679,12 +764,11 @@ class BoardService ], 'extension_type' => ExtensionOwnerType::Module, 'extension_identifier' => 'sirsoft-board', - ] - ); + ]); - $stepRole = Role::firstOrCreate( - ['identifier' => "sirsoft-board.{$board->slug}.step"], - [ + $stepRole = $this->roleRepository->findByIdentifier("sirsoft-board.{$board->slug}.step") + ?? $this->roleRepository->create([ + 'identifier' => "sirsoft-board.{$board->slug}.step", 'name' => [ 'ko' => "{$boardNameKo} 게시판 스텝", 'en' => "{$boardNameEn} Board Step", @@ -695,8 +779,7 @@ class BoardService ], 'extension_type' => ExtensionOwnerType::Module, 'extension_identifier' => 'sirsoft-board', - ] - ); + ]); return ['manager' => $managerRole, 'step' => $stepRole]; } @@ -713,13 +796,16 @@ class BoardService "sirsoft-board.{$board->slug}.step", ]; - $roles = Role::whereIn('identifier', $roleIdentifiers)->get(); + foreach ($roleIdentifiers as $identifier) { + $role = $this->roleRepository->findByIdentifier($identifier); + if (! $role) { + continue; + } - foreach ($roles as $role) { // 그누보드7 규정: detach 후 삭제 - $role->permissions()->detach(); + $this->roleRepository->detachAllPermissions($role); $role->users()->detach(); - $role->delete(); + $this->roleRepository->delete($role); } } @@ -757,7 +843,10 @@ class BoardService ]; foreach ($roleUpdates as $identifier => $attributes) { - Role::where('identifier', $identifier)->update($attributes); + $role = $this->roleRepository->findByIdentifier($identifier); + if ($role) { + $this->roleRepository->update($role, $attributes); + } } } @@ -779,7 +868,7 @@ class BoardService continue; } - $role = Role::where('identifier', $roleIdentifier)->first(); + $role = $this->roleRepository->findByIdentifier($roleIdentifier); if (! $role) { Log::warning('게시판 역할을 찾을 수 없습니다.', [ 'board_slug' => $board->slug, @@ -790,7 +879,7 @@ class BoardService } $userUuids = $data[$dataKey] ?? []; - $userIds = User::whereIn('uuid', $userUuids)->pluck('id')->toArray(); + $userIds = $this->userRepository->getIdsByUuids($userUuids); $role->users()->sync($userIds); } } @@ -886,6 +975,7 @@ class BoardService * * @param string $period 기간 (today, week, month, all) * @param int $limit 조회 개수 + * @return array 인기글 목록 */ public function getPopularPosts(string $period = 'week', int $limit = 20): array { @@ -938,7 +1028,7 @@ class BoardService $menuUrl = '/admin/board/'.$board->slug; // 중복 체크: 동일한 URL의 메뉴가 이미 존재하는지 확인 - $existingMenu = Menu::where('url', $menuUrl)->first(); + $existingMenu = $this->menuRepository->findByUrl($menuUrl); if ($existingMenu) { throw new MenuAlreadyExistsException(__('sirsoft-board::messages.boards.menu_already_exists')); @@ -1104,18 +1194,49 @@ class BoardService /** * 필터용 전체 활성 게시판 목록을 배열로 반환합니다. * + * $user 가 전달되면 그 사용자가 읽기 권한(posts.read)을 가진 게시판만 남긴다. + * 통합 검색 필터 드롭다운(available_boards)이 열람 불가 게시판까지 노출하지 않도록, + * 검색 결과 필터와 동일한 게이트를 목록 구성에도 적용한다. + * + * @param User|null $user 기준 사용자 (null 이면 현재 인증 사용자 또는 guest) * @return array<int, array{slug: string, name: string}> 활성 게시판 목록 */ - public function getActiveBoardsListForFilter(): array + public function getActiveBoardsListForFilter(?User $user = null): array { return $this->boardRepository->getActiveBoardsList() + ->filter(fn ($board) => PermissionHelper::check("sirsoft-board.{$board->slug}.posts.read", $user)) ->map(fn ($board) => [ 'slug' => $board->slug, 'name' => $board->getLocalizedName(), ]) + ->values() ->toArray(); } + /** + * 공개 목록의 각 항목을 호출자 읽기 권한(posts.read) 기준으로 필터링합니다. + * + * 공개 엔드포인트(최근/인기 게시글·인기 게시판)의 전역 캐시는 사용자와 무관하게 + * 안전집합으로만 채워지므로, 게시판별 열람 권한은 캐시가 아니라 응답 시점에 + * 현재 호출자 기준으로 적용한다. 고권한 사용자의 결과가 캐시에 남아 저권한/비회원 + * 사용자에게 새는 것을 막는다. + * + * @param array<int, array<string, mixed>> $items 캐시된 목록 + * @param string $slugKey 각 항목에서 게시판 슬러그를 담은 키 + * @return array<int, array<string, mixed>> 읽기 권한을 통과한 항목만 남긴 목록 + */ + private function filterItemsByBoardReadPermission(array $items, string $slugKey): array + { + $user = Auth::user(); + + return array_values(array_filter($items, function ($item) use ($slugKey, $user) { + $slug = $item[$slugKey] ?? null; + + return $slug !== null + && PermissionHelper::check("sirsoft-board.{$slug}.posts.read", $user); + })); + } + /** * 환경설정 기본값을 기존 게시판에 일괄 적용합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Services/BoardSettingsService.php b/modules/_bundled/sirsoft-board/src/Services/BoardSettingsService.php index be8a15d6..78d4d47b 100644 --- a/modules/_bundled/sirsoft-board/src/Services/BoardSettingsService.php +++ b/modules/_bundled/sirsoft-board/src/Services/BoardSettingsService.php @@ -75,20 +75,39 @@ class BoardSettingsService implements ModuleSettingsInterface /** * 설정값 저장 * - * @param string $key 설정 키 + * 벌크 저장(saveSettings)과 같은 본문을 경유해 정규화·신고 알림 강제 활성·캐시 무효화· + * 알림 정의 동기화를 함께 수행한다 (공개 #114 동종). 예전에는 `Arr::set` 결과를 카테고리 + * 파일에 통째로 덮어써서 이 단계들을 모두 건너뛰었고, 자기 캐시도 비우지 않아 같은 + * 요청 안의 곧이은 조회가 저장 전 값을 반환했다. + * + * 위임 payload 의 기저는 **저장본**(loadCategorySettings)이다. 조회 결과를 기저로 삼으면 + * defaults 병합분이 저장 파일에 통째로 영속화된다. + * + * boolean backfill 은 적용하지 않는다 — 폼 Toggle-OFF 미전송 대응이라 부분 저장에 태우면 + * 제출하지 않은 boolean 이 전부 false 로 박제되어 기본값 true 인 항목이 뒤집힌다. + * + * @param string $key 설정 키 (예: 'basic_defaults.per_page', 카테고리 통째 지정도 허용) * @param mixed $value 저장할 값 * @return bool 성공 여부 */ public function setSetting(string $key, mixed $value): bool { - $settings = $this->getAllSettings(); - Arr::set($settings, $key, $value); - - // 카테고리 추출 $parts = explode('.', $key); - $category = $parts[0]; + $category = array_shift($parts); - return $this->saveCategorySettings($category, $settings[$category] ?? []); + if ($parts === []) { + // 카테고리 통째 저장 — 배열이 아니면 저장할 카테고리 데이터가 없다 + if (! is_array($value)) { + return false; + } + + $categoryData = $value; + } else { + $categoryData = $this->loadCategorySettings($category); + Arr::set($categoryData, implode('.', $parts), $value); + } + + return $this->persistCategories([$category => $categoryData], backfillBooleans: false); } /** @@ -141,6 +160,19 @@ class BoardSettingsService implements ModuleSettingsInterface * @return bool 성공 여부 */ public function saveSettings(array $settings): bool + { + return $this->persistCategories($settings, backfillBooleans: true); + } + + /** + * 카테고리 설정을 정규화 파이프라인에 태워 저장합니다. (벌크/단건 공통 본문) + * + * @param array $settings [카테고리 => 카테고리 설정] 배열 + * @param bool $backfillBooleans 미제출 boolean 필드를 false 로 채울지 여부 + * (폼 전체 제출을 전제로 하는 벌크 저장만 true) + * @return bool 성공 여부 + */ + private function persistCategories(array $settings, bool $backfillBooleans): bool { $success = true; $defaults = $this->getDefaults(); @@ -160,9 +192,12 @@ class BoardSettingsService implements ModuleSettingsInterface $categoryDefaults = $defaultValues[$category] ?? []; // Toggle/체크박스 OFF 시 키 미전송 대응: boolean 기본값 필드가 누락되면 false로 채움 - foreach ($categoryDefaults as $key => $defaultValue) { - if (is_bool($defaultValue) && ! array_key_exists($key, $categorySettings)) { - $categorySettings[$key] = false; + // (폼 전체 제출을 전제로 한 보정이라 부분 저장 경로에서는 적용하지 않는다) + if ($backfillBooleans) { + foreach ($categoryDefaults as $key => $defaultValue) { + if (is_bool($defaultValue) && ! array_key_exists($key, $categorySettings)) { + $categorySettings[$key] = false; + } } } @@ -186,6 +221,9 @@ class BoardSettingsService implements ModuleSettingsInterface // 캐시 초기화 $this->settings = null; + // 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109) + g7_refresh_module_settings_config('sirsoft-board'); + // report_policy 설정 변경 시 알림 정의 활성 상태 동기화 // 저장된 최종 설정을 사용 (boolean 보정 후 값) if (isset($settings['report_policy'])) { @@ -323,6 +361,9 @@ class BoardSettingsService implements ModuleSettingsInterface { $this->defaults = null; $this->settings = null; + + // 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109) + g7_refresh_module_settings_config('sirsoft-board'); } /** @@ -412,10 +453,18 @@ class BoardSettingsService implements ModuleSettingsInterface /** * 설정 저장 경로 반환 * + * testing 환경에서는 운영 설정(storage/app/modules/.../settings)을 보호하기 위해 + * 격리된 임시 경로를 사용합니다. 설정 저장을 수행하는 테스트가 운영 basic_defaults.json + * 등을 덮어쓰거나 지우는 것을 차단합니다(운영 설정 영구 보존). + * * @return string 설정 파일 저장 디렉토리 경로 */ private function getStoragePath(): string { + if (app()->runningUnitTests()) { + return storage_path('framework/testing/modules/'.self::MODULE_IDENTIFIER.'/settings'); + } + return storage_path('app/modules/'.self::MODULE_IDENTIFIER.'/settings'); } } diff --git a/modules/_bundled/sirsoft-board/src/Services/BoardTypeService.php b/modules/_bundled/sirsoft-board/src/Services/BoardTypeService.php index 8b5f26f1..d33df028 100644 --- a/modules/_bundled/sirsoft-board/src/Services/BoardTypeService.php +++ b/modules/_bundled/sirsoft-board/src/Services/BoardTypeService.php @@ -6,6 +6,7 @@ use App\Extension\HookManager; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Board\Exceptions\BoardTypeOperationException; use Modules\Sirsoft\Board\Models\BoardType; use Modules\Sirsoft\Board\Repositories\Contracts\BoardRepositoryInterface; use Modules\Sirsoft\Board\Repositories\Contracts\BoardTypeRepositoryInterface; @@ -27,7 +28,7 @@ class BoardTypeService } /** - * @param array $data + * @param array $data * @return BoardType */ public function createBoardType(array $data): BoardType @@ -46,8 +47,8 @@ class BoardTypeService } /** - * @param int $id - * @param array $data + * @param int $id + * @param array $data * @return BoardType */ public function updateBoardType(int $id, array $data): BoardType @@ -55,7 +56,7 @@ class BoardTypeService $boardType = $this->boardTypeRepository->findById($id); if (! $boardType) { - throw (new ModelNotFoundException())->setModel(BoardType::class, $id); + throw (new ModelNotFoundException)->setModel(BoardType::class, $id); } HookManager::doAction('sirsoft-board.board_type.before_update', $boardType, $data); @@ -74,7 +75,7 @@ class BoardTypeService } /** - * @param int $id + * @param int $id * @return bool * * @throws \Exception 사용 중인 유형 삭제 시도 시 @@ -84,23 +85,19 @@ class BoardTypeService $boardType = $this->boardTypeRepository->findById($id); if (! $boardType) { - throw (new ModelNotFoundException())->setModel(BoardType::class, $id); + throw (new ModelNotFoundException)->setModel(BoardType::class, $id); } $usageCount = $this->boardRepository->countByType($boardType->slug); if ($usageCount > 0) { - throw new \Exception( - __('sirsoft-board::messages.board_type.delete_in_use', [ - 'count' => $usageCount, - ]) - ); + throw new BoardTypeOperationException('sirsoft-board::messages.board_type.delete_in_use', [ + 'count' => $usageCount, + ]); } $defaultType = $this->boardSettingsService->getSetting('basic_defaults.type'); if ($defaultType === $boardType->slug) { - throw new \Exception( - __('sirsoft-board::messages.board_type.delete_is_default') - ); + throw new BoardTypeOperationException('sirsoft-board::messages.board_type.delete_is_default'); } HookManager::doAction('sirsoft-board.board_type.before_delete', $boardType); diff --git a/modules/_bundled/sirsoft-board/src/Services/CommentService.php b/modules/_bundled/sirsoft-board/src/Services/CommentService.php index c7f68546..b87747ce 100644 --- a/modules/_bundled/sirsoft-board/src/Services/CommentService.php +++ b/modules/_bundled/sirsoft-board/src/Services/CommentService.php @@ -575,6 +575,51 @@ class CommentService $this->cache->put("comment_cooldown_{$slug}_{$identifier}", true, $seconds); } + /** + * 댓글 비밀번호 검증 토큰을 캐시에 저장하고 만료 시각을 반환합니다. + * + * 게시글(PostService::storeDeleteVerifyToken)과 동형 — 비회원이 비밀번호를 + * 확인하면 1회용 토큰을 발급해, 이후 수정/삭제 요청에서 평문 비밀번호 재전송 대신 + * 이 토큰으로 본인 확인을 대체한다. + * + * @param string $slug 게시판 슬러그 + * @param int $commentId 댓글 ID + * @param string $token 검증 토큰 + * @return array{token: string, expires_at: string} 토큰 및 만료 시각 + */ + public function storeCommentVerifyToken(string $slug, int $commentId, string $token): array + { + $ttl = (int) g7_core_settings('cache.post_verify_token_ttl', 3600); + $expiresAt = now()->addSeconds($ttl); + $this->cache->put("board_comment_verify_{$slug}_{$commentId}_{$token}", true, $ttl); + + return [ + 'token' => $token, + 'expires_at' => $expiresAt->toIso8601String(), + ]; + } + + /** + * 댓글 비밀번호 검증 토큰의 유효성을 확인하고 소비합니다. + * + * 토큰이 유효하면 즉시 삭제하여 재사용을 방지합니다(단일 사용). + * + * @param string $slug 게시판 슬러그 + * @param int $commentId 댓글 ID + * @param string $token 검증 토큰 + * @return bool 토큰 유효 여부 + */ + public function consumeCommentVerifyToken(string $slug, int $commentId, string $token): bool + { + $key = "board_comment_verify_{$slug}_{$commentId}_{$token}"; + if (! $this->cache->has($key)) { + return false; + } + $this->cache->forget($key); + + return true; + } + /** * 관리자 작업 이력 배열을 생성합니다. * diff --git a/modules/_bundled/sirsoft-board/src/Services/PostService.php b/modules/_bundled/sirsoft-board/src/Services/PostService.php index 676b41a4..5c1727d5 100644 --- a/modules/_bundled/sirsoft-board/src/Services/PostService.php +++ b/modules/_bundled/sirsoft-board/src/Services/PostService.php @@ -19,6 +19,8 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Board\Enums\PostStatus; +use Modules\Sirsoft\Board\Enums\ReplyDeletePolicy; +use Modules\Sirsoft\Board\Exceptions\PostHasRepliesException; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Models\Post; use Modules\Sirsoft\Board\Repositories\Contracts\AttachmentRepositoryInterface; @@ -376,33 +378,57 @@ class PostService * ① 게시글 상태 변경(deleted) + 소프트 삭제 * ② 살아있는 댓글을 cascade 로 소프트 삭제 (trigger_type='cascade') * ③ 살아있는 첨부를 cascade 로 소프트 삭제 (trigger_type='cascade') + * ④ 살아있는 자손 답글 전체를 cascade 로 소프트 삭제 (+ 그 답글들의 댓글/첨부) * - * 이미 사용자가 직접 삭제한 댓글/첨부(trigger_type='user' 등)는 영향을 받지 않으며, - * 게시글 복원 시 cascade 로 지워진 항목만 선택 복원됩니다. - * 위 3단계는 단일 트랜잭션으로 묶여 부분 실패 시 전체 롤백됩니다. + * 게시판 설정 `reply_delete_policy` 가 'block' 이면 살아있는 직계 답글 존재 시 + * before_delete 훅 발화 전에 차단합니다(부수효과 0). 훅 경유 삭제는 + * `options['cascade_replies']=true` 로 정책을 우회해 cascade 를 고정합니다 + * (문의 답변은 시스템 생성이므로 block 정책에 막히면 안 됨). + * + * 이미 사용자가 직접 삭제한 답글/댓글/첨부(trigger_type='user' 등)는 영향을 받지 + * 않으며, 게시글 복원 시 cascade 로 지워진 항목만 선택 복원됩니다. + * 위 단계는 단일 트랜잭션으로 묶여 부분 실패 시 전체 롤백됩니다. + * after_delete 훅은 부모 게시글에 대해서만 발화합니다 (자손별 발화 시 알림 폭주). * * @param string $slug 게시판 슬러그 * @param int $id 게시글 ID * @param string|null $triggerType 트리거 유형 (admin, report 등) - * @param array $options 옵션 배열 (skip_notification: 알림 발송 SKIP) + * @param array $options 옵션 배열 (skip_notification: 알림 발송 SKIP, cascade_replies: 답글 삭제 정책 우회) * @return Post 삭제 처리된 게시글 * * @throws ModelNotFoundException 게시판 또는 게시글을 찾을 수 없는 경우 + * @throws PostHasRepliesException block 정책 게시판에서 살아있는 답글이 있는 경우 */ public function deletePost(string $slug, int $id, ?string $triggerType = null, array $options = []): Post { - // 게시판 존재성 검증 - $this->validateBoardExists($slug); + // 게시판 존재성 검증 (정책 판정에 게시판 모델이 필요해 직접 조회) + $board = $this->boardService->getBoardBySlug($slug, checkScope: false); + + if (! $board) { + throw new ModelNotFoundException(__('sirsoft-board::messages.errors.board_not_found')); + } $post = $this->postRepository->findOrFail($slug, $id); + // 답글 삭제 정책 판정 — 훅 경유 삭제(cascade_replies 옵션)는 시스템 생성 답글을 + // 함께 정리해야 하므로 게시판 정책과 무관하게 cascade 고정 + $policy = ($options['cascade_replies'] ?? false) + ? ReplyDeletePolicy::Cascade + : ($board->reply_delete_policy ?? ReplyDeletePolicy::Cascade); + + // block 정책: 살아있는 직계 답글이 있으면 before_delete 훅 발화 전에 차단 + // (차단 시 부수효과 0 — 훅 리스너의 외부 정리가 먼저 실행되면 되돌릴 수 없다) + if ($policy === ReplyDeletePolicy::Block && $this->postRepository->hasAliveReplies($slug, $id)) { + throw new PostHasRepliesException; + } + // 훅: before_delete HookManager::doAction('sirsoft-board.post.before_delete', $post, $slug); // 작업 이력 생성 $actionLog = $this->buildActionLog('delete', null); - // 게시글 + 하위 데이터(댓글/첨부) 연쇄 소프트 삭제를 단일 트랜잭션으로 처리 + // 게시글 + 하위 데이터(답글/댓글/첨부) 연쇄 소프트 삭제를 단일 트랜잭션으로 처리 $deletedPost = DB::transaction(function () use ($slug, $id, $actionLog, $triggerType) { // ① 상태 변경 (deleted로 변경하고 소프트 삭제) $deletedPost = $this->postRepository->updateStatus($slug, $id, 'deleted', $actionLog, $triggerType); @@ -414,10 +440,22 @@ class PostService // ③ 첨부 cascade 소프트 삭제 (사용자가 이미 삭제한 항목은 미영향) $this->attachmentRepository->softDeleteByPostId($slug, $id); + // ④ 살아있는 자손 답글 cascade 소프트 삭제 — block 통과 시에도 실행 + // (끊긴 체인 밑 과거 고아 답글 스윕. 살아있는 직계가 없어도 고아 자손은 존재 가능) + $descendantIds = $this->postRepository->softDeleteCascadeByParentId($slug, $id); + + // ⑤ 함께 지워진 자손 답글들의 댓글/첨부도 cascade 소프트 삭제 + if ($descendantIds !== []) { + $this->commentRepository->softDeleteByPostIds($slug, $descendantIds); + $this->attachmentRepository->softDeleteByPostIds($slug, $descendantIds); + } + return $deletedPost; }); // 훅: after_delete ($options 전달 — skip_notification 등 수신 리스너에서 활용) + // 자손 답글별로는 발화하지 않는다 — 자손별 발화 시 알림이 폭주하며, + // 이커머스 문의 피벗은 문의 원글에만 걸려 있어 부모 발화만으로 충분하다. HookManager::doAction('sirsoft-board.post.after_delete', $deletedPost, $slug, $options); // 캐시 무효화 @@ -508,6 +546,17 @@ class PostService // ③ cascade 로 지워진 첨부만 선택 복원 $this->attachmentRepository->restoreCascadedByPostId($slug, $id); + // ④ cascade 로 지워진 자손 답글만 top-down 선택 복원 — 정책값과 무관하게 항상 실행 + // (trigger_type='cascade' 로 마킹된 것만 복원되므로 안전. 사용자 직접 삭제 답글의 + // 서브트리는 복원되지 않는다) + $restoredIds = $this->postRepository->restoreCascadedByParentId($slug, $id); + + // ⑤ 되살아난 자손 답글들의 댓글/첨부도 cascade 분만 복원 + if ($restoredIds !== []) { + $this->commentRepository->restoreCascadedByPostIds($slug, $restoredIds); + $this->attachmentRepository->restoreCascadedByPostIds($slug, $restoredIds); + } + return $restoredPost; }); @@ -882,9 +931,12 @@ class PostService $filters['user_id'] = $requestParams['user_id'] ?? null; $filters['created_at_from'] = $requestParams['created_at_from'] ?? null; $filters['created_at_to'] = $requestParams['created_at_to'] ?? null; - } else { - $filters['exclude_blinded'] = true; } + // 사용자 컨텍스트에는 추가 필터가 없다. 예전에는 여기서 `exclude_blinded` 를 세웠지만 + // 저장소가 그 키를 읽지 않아 한 번도 적용되지 않았고, 뒤늦게 배선하면 블라인드 글이 + // 사용자 목록에서 통째로 사라진다 — 이 모듈은 블라인드 글을 **행은 남기고 본문만 + // 가리는** 방식으로 다루며(PostResource::getMaskedContentPreviewForList) 레이아웃도 + // 블라인드 배지를 그린다. 죽은 키를 살리는 대신 제거해 표시만 오해를 주던 상태를 없앤다. // 페이지당 항목 수 계산 $requestedPerPage = isset($requestParams['per_page']) ? (int) $requestParams['per_page'] : null; @@ -1070,6 +1122,10 @@ class PostService } } + // 본인 활동 화면(`/me/board-activities`)이므로 열람자 = 대상 본인이다. + // 저장소는 이 값이 없으면 타인 관점으로 보고 비밀글·미발행글을 걸러낸다(fail-closed). + $filters['viewer_id'] = $userId; + $result = $this->postRepository->getUserActivities($userId, $filters, $perPage); // 캐시 미적중 시 paginate 결과의 total을 캐시에 저장 @@ -1105,9 +1161,12 @@ class PostService /** * 사용자의 공개 게시글 목록을 조회합니다 (공개 프로필용). * - * 기존 getUserActivities()를 재사용합니다. - * 타인 프로필에서 해당 사용자의 모든 게시글을 표시합니다 (비밀글/블라인드 포함). - * UI에서 배지로 비밀글/블라인드 상태를 구분합니다. + * 기존 getUserActivities()를 재사용합니다. 이 목록은 본문 일부(content_plain)를 함께 + * 싣기 때문에, 열람자가 본인이 아니면 비밀글·블라인드 글의 **본문만** 비운다. 행과 + * 제목은 남는다 — 게시판 목록에서 이미 같은 수준으로 보이고(PostResource 의 목록 규칙: + * 제목은 노출, 본문만 차단) 프로필 UI 가 그 배지를 그리므로, 행을 지우면 결함 차단에 + * 필요한 범위를 넘어 기능이 깎인다. + * 판정은 저장소가 `viewer_id` 로 수행한다(fail-closed — 값이 없으면 타인 관점). * * @param int $userId 사용자 ID * @param array $filters 필터 옵션 (board_slug, sort 등) @@ -1116,10 +1175,9 @@ class PostService */ public function getUserPublicPosts(int $userId, array $filters = [], int $perPage = 20): LengthAwarePaginator { - // 기존 getUserActivities 재사용 - // 타인 프로필에서 해당 사용자의 모든 게시글 표시 (비밀글/블라인드 포함, 배지로 구분) return $this->postRepository->getUserActivities($userId, array_merge($filters, [ 'activity_type' => 'authored', // 작성글만 + 'viewer_id' => Auth::id(), // 비로그인은 null → 타인 관점 ]), $perPage); } diff --git a/modules/_bundled/sirsoft-board/src/Support/SecretContentGate.php b/modules/_bundled/sirsoft-board/src/Support/SecretContentGate.php new file mode 100644 index 00000000..414f1841 --- /dev/null +++ b/modules/_bundled/sirsoft-board/src/Support/SecretContentGate.php @@ -0,0 +1,84 @@ +<?php + +namespace Modules\Sirsoft\Board\Support; + +use App\Enums\PermissionType; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Modules\Sirsoft\Board\Models\Post; +use Modules\Sirsoft\Board\Traits\ChecksBoardPermission; + +/** + * 비밀글 원문 열람 권한 판정 SSoT + * + * 비밀글 게이팅이 PostResource 한 곳에만 있던 탓에 이커머스 연동 훅·댓글 목록 등 + * 다른 경로가 서버측 마스킹 없이 원문을 반환하던 결함(KVE-2026-1914)을 막기 위해, + * 열람 판정 규칙을 한 지점으로 모읍니다. PostResource·리스너·댓글 경로가 모두 + * 이 게이트를 호출해 규칙 드리프트를 방지합니다. + * + * 열람 가능 조건 (우선순위 순): + * 1. 작성자 본인 (회원 게시글) + * 2. 비밀번호 검증 완료 (`password_verified` 플래그 — 상세 컨텍스트에서만 설정, 리스트 미적용) + * 3. 게시판별 비밀글 읽기 권한 (posts.read-secret) + * 4. 게시판 관리자 권한 (Admin: admin.manage / User: manager) + */ +class SecretContentGate +{ + use ChecksBoardPermission; + + /** + * 주어진 게시글의 비밀 원문을 현재 요청자가 열람할 수 있는지 판정합니다. + * + * 게시판 슬러그를 해석할 수 없으면(라우트 슬러그 부재 + board 미로딩) 안전하게 + * false(마스킹)로 실패합니다. + * + * @param Post $post 대상 게시글 + * @param Request|null $request HTTP 요청 (미지정 시 현재 요청) + * @return bool 열람 가능 여부 + */ + public function canView(Post $post, ?Request $request = null): bool + { + $request = $request ?? request(); + + // 1. 작성자 본인 (회원 게시글) + $user = Auth::user(); + if ($user && $post->user_id && $post->user_id === $user->id) { + return true; + } + + // 2. 비밀번호 검증 완료 (상세 컨텍스트에서만 세팅됨) + if (($post->password_verified ?? false) === true) { + return true; + } + + // 3-4. 게시판별 권한 체크 + $slug = $this->resolveSlug($post, $request); + if (! $slug) { + return false; + } + + if ($this->isAdminRequest($request)) { + return $this->checkBoardPermission($slug, 'admin.posts.read-secret') + || $this->checkBoardPermission($slug, 'admin.manage'); + } + + return $this->checkBoardPermission($slug, 'posts.read-secret', PermissionType::User) + || $this->checkBoardPermission($slug, 'manager', PermissionType::User); + } + + /** + * 게시판 슬러그를 해석합니다. + * + * 라우트 슬러그를 우선 사용하고, 없으면 로드된 board 관계에서 가져옵니다. + * board 가 미로딩이면 lazy loading N+1 을 피하기 위해 null 을 반환합니다 + * (호출부가 fail-closed 마스킹). + * + * @param Post $post 대상 게시글 + * @param Request $request HTTP 요청 + * @return string|null 게시판 슬러그 + */ + private function resolveSlug(Post $post, Request $request): ?string + { + return $request->route('slug') ?? ($post->relationLoaded('board') ? $post->board?->slug : null); + } +} diff --git a/modules/_bundled/sirsoft-board/src/Traits/ChecksBoardPermission.php b/modules/_bundled/sirsoft-board/src/Traits/ChecksBoardPermission.php index 4372e3d5..2a1cd1a4 100644 --- a/modules/_bundled/sirsoft-board/src/Traits/ChecksBoardPermission.php +++ b/modules/_bundled/sirsoft-board/src/Traits/ChecksBoardPermission.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Board\Traits; use App\Enums\PermissionType; use App\Http\Middleware\PermissionMiddleware; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Board\Support\BoardPermissionCacheKeys; @@ -16,6 +17,26 @@ use Modules\Sirsoft\Board\Support\BoardPermissionCacheKeys; */ trait ChecksBoardPermission { + /** + * Admin 화면 요청 여부를 컨트롤러 네임스페이스로 판정합니다. + * + * 판정 규칙의 단일 지점 — Resource/게이트마다 사설 복제본을 두면 규칙 변경 시 + * 한 곳만 누락되어 경로별 판정이 갈린다. + * + * @param Request $request HTTP 요청 + * @return bool Admin 요청 여부 + */ + protected function isAdminRequest(Request $request): bool + { + $controller = $request->route()?->getController(); + + if (! $controller) { + return false; + } + + return str_contains(get_class($controller), '\\Admin\\'); + } + /** * 게시판 권한을 확인합니다. * diff --git a/modules/_bundled/sirsoft-board/src/lang/en/activity_log.php b/modules/_bundled/sirsoft-board/src/lang/en/activity_log.php index d77bb3b6..4489de1a 100644 --- a/modules/_bundled/sirsoft-board/src/lang/en/activity_log.php +++ b/modules/_bundled/sirsoft-board/src/lang/en/activity_log.php @@ -90,6 +90,7 @@ return [ 'use_comment' => 'Use Comments', 'use_reply' => 'Use Replies', 'max_reply_depth' => 'Max Reply Depth', + 'reply_delete_policy' => 'Reply Delete Policy', 'use_report' => 'Use Reports', 'use_file_upload' => 'Use File Upload', 'max_file_size' => 'Max File Size', diff --git a/modules/_bundled/sirsoft-board/src/lang/en/enums.php b/modules/_bundled/sirsoft-board/src/lang/en/enums.php index 1046da3f..a82761a2 100644 --- a/modules/_bundled/sirsoft-board/src/lang/en/enums.php +++ b/modules/_bundled/sirsoft-board/src/lang/en/enums.php @@ -14,6 +14,12 @@ return [ 'always' => 'Always Required', ], + // 답글 삭제 정책 + 'reply_delete_policy' => [ + 'block' => 'Block deletion (cannot delete posts with replies)', + 'cascade' => 'Delete together (replies are deleted with the post)', + ], + // Order Direction 'order_direction' => [ 'asc' => 'Ascending', diff --git a/modules/_bundled/sirsoft-board/src/lang/en/messages.php b/modules/_bundled/sirsoft-board/src/lang/en/messages.php index 925196f5..cf7b2428 100644 --- a/modules/_bundled/sirsoft-board/src/lang/en/messages.php +++ b/modules/_bundled/sirsoft-board/src/lang/en/messages.php @@ -44,6 +44,7 @@ return [ 'secret_password_incorrect' => 'Secret post password is incorrect.', // Secret post filtering messages 'secret_post_content' => 'This is a secret post. Please enter the password to view the content.', + 'secret_post_title' => 'Secret post', 'deleted_post_title' => 'Deleted post', 'deleted_post_content' => 'This post has been deleted.', 'blinded_post_content' => 'This post has been blinded by administrator.', @@ -225,6 +226,7 @@ return [ 'permission_denied' => 'Permission denied.', 'validation_failed' => 'Validation failed.', 'duplicate_report' => 'You have already reported this content.', + 'operation_failed' => 'An error occurred while processing the operation.', ], // Warning messages diff --git a/modules/_bundled/sirsoft-board/src/lang/en/validation.php b/modules/_bundled/sirsoft-board/src/lang/en/validation.php index ae4efe83..b0931c47 100644 --- a/modules/_bundled/sirsoft-board/src/lang/en/validation.php +++ b/modules/_bundled/sirsoft-board/src/lang/en/validation.php @@ -263,6 +263,9 @@ return [ 'notice_not_allowed' => 'Replies cannot be created on notice posts.', ], 'reply_not_allowed' => 'Reply feature is disabled for this board.', + 'delete' => [ + 'has_replies' => 'Posts with replies cannot be deleted. Please delete the replies first.', + ], 'status' => [ 'in' => 'Invalid post status.', ], @@ -303,6 +306,7 @@ return [ 'basic_defaults.use_comment' => 'Use Comment', 'basic_defaults.use_reply' => 'Use Reply', 'basic_defaults.max_reply_depth' => 'Max Reply Depth', + 'basic_defaults.reply_delete_policy' => 'Reply Delete Policy', 'basic_defaults.max_comment_depth' => 'Max Comment Depth', 'basic_defaults.comment_order' => 'Comment Order', 'basic_defaults.show_view_count' => 'Show View Count', @@ -391,6 +395,7 @@ return [ 'override_values.per_page' => 'Posts Per Page', 'override_values.per_page_mobile' => 'Posts Per Page (Mobile)', 'override_values.max_reply_depth' => 'Max Reply Depth', + 'override_values.reply_delete_policy' => 'Reply Delete Policy', 'override_values.max_comment_depth' => 'Max Comment Depth', 'override_values.min_title_length' => 'Min Title Length', 'override_values.max_title_length' => 'Max Title Length', @@ -401,6 +406,10 @@ return [ 'override_values.max_file_size' => 'Max File Size', 'override_values.max_file_count' => 'Max File Count', 'override_values.new_display_hours' => 'New Display Hours', + + // attachment_settings + 'attachment_settings.purge_enabled' => 'Permanent Cleanup Of Deleted Attachments', + 'attachment_settings.purge_retention_days' => 'Deleted Attachment Retention Period', ], ], diff --git a/modules/_bundled/sirsoft-board/src/lang/ko/activity_log.php b/modules/_bundled/sirsoft-board/src/lang/ko/activity_log.php index c32f08b0..54e30ac9 100644 --- a/modules/_bundled/sirsoft-board/src/lang/ko/activity_log.php +++ b/modules/_bundled/sirsoft-board/src/lang/ko/activity_log.php @@ -90,6 +90,7 @@ return [ 'use_comment' => '댓글 사용', 'use_reply' => '답글 사용', 'max_reply_depth' => '최대 답글 깊이', + 'reply_delete_policy' => '답글 삭제 방식', 'use_report' => '신고 사용', 'use_file_upload' => '파일 업로드 사용', 'max_file_size' => '최대 파일 크기', diff --git a/modules/_bundled/sirsoft-board/src/lang/ko/enums.php b/modules/_bundled/sirsoft-board/src/lang/ko/enums.php index 383a9deb..d9c72040 100644 --- a/modules/_bundled/sirsoft-board/src/lang/ko/enums.php +++ b/modules/_bundled/sirsoft-board/src/lang/ko/enums.php @@ -14,6 +14,12 @@ return [ 'always' => '필수 사용', ], + // 답글 삭제 정책 + 'reply_delete_policy' => [ + 'block' => '삭제 차단 (답글이 있으면 원글 삭제 불가)', + 'cascade' => '함께 삭제 (원글 삭제 시 답글도 함께 삭제)', + ], + // 정렬 방향 'order_direction' => [ 'asc' => '오름차순', diff --git a/modules/_bundled/sirsoft-board/src/lang/ko/messages.php b/modules/_bundled/sirsoft-board/src/lang/ko/messages.php index 10041c1e..1d25fa58 100644 --- a/modules/_bundled/sirsoft-board/src/lang/ko/messages.php +++ b/modules/_bundled/sirsoft-board/src/lang/ko/messages.php @@ -44,6 +44,7 @@ return [ 'secret_password_incorrect' => '비밀글 비밀번호가 일치하지 않습니다.', // 비밀글 필터링 메시지 'secret_post_content' => '비밀글입니다. 내용을 보려면 비밀번호를 입력해 주세요.', + 'secret_post_title' => '비밀글', 'deleted_post_title' => '삭제된 게시글', 'deleted_post_content' => '삭제된 게시글입니다.', 'blinded_post_content' => '관리자에 의해 블라인드 처리된 게시글입니다.', @@ -225,6 +226,7 @@ return [ 'permission_denied' => '권한이 없습니다.', 'validation_failed' => '입력값이 올바르지 않습니다.', 'duplicate_report' => '이미 신고한 내역이 있습니다.', + 'operation_failed' => '작업 처리 중 오류가 발생했습니다.', ], // 경고 메시지 (Warning) diff --git a/modules/_bundled/sirsoft-board/src/lang/ko/validation.php b/modules/_bundled/sirsoft-board/src/lang/ko/validation.php index a4f87097..e5f33fa8 100644 --- a/modules/_bundled/sirsoft-board/src/lang/ko/validation.php +++ b/modules/_bundled/sirsoft-board/src/lang/ko/validation.php @@ -263,6 +263,9 @@ return [ 'notice_not_allowed' => '공지사항에는 답글을 작성할 수 없습니다.', ], 'reply_not_allowed' => '이 게시판은 답글 기능이 비활성화되어 있습니다.', + 'delete' => [ + 'has_replies' => '답글이 달린 글은 삭제할 수 없습니다. 답글을 먼저 삭제해 주세요.', + ], 'status' => [ 'in' => '유효하지 않은 게시글 상태입니다.', ], @@ -303,6 +306,7 @@ return [ 'basic_defaults.use_comment' => '댓글 사용 여부', 'basic_defaults.use_reply' => '답글 사용 여부', 'basic_defaults.max_reply_depth' => '최대 답글 깊이', + 'basic_defaults.reply_delete_policy' => '답글 삭제 방식', 'basic_defaults.max_comment_depth' => '최대 댓글 깊이', 'basic_defaults.comment_order' => '댓글 정렬', 'basic_defaults.show_view_count' => '조회수 표시', @@ -391,6 +395,7 @@ return [ 'override_values.per_page' => '페이지당 게시글 수', 'override_values.per_page_mobile' => '모바일 페이지당 게시글 수', 'override_values.max_reply_depth' => '최대 답글 깊이', + 'override_values.reply_delete_policy' => '답글 삭제 방식', 'override_values.max_comment_depth' => '최대 댓글 깊이', 'override_values.min_title_length' => '최소 제목 길이', 'override_values.max_title_length' => '최대 제목 길이', @@ -401,6 +406,10 @@ return [ 'override_values.max_file_size' => '최대 파일 크기', 'override_values.max_file_count' => '최대 파일 개수', 'override_values.new_display_hours' => '신규 표시 시간', + + // attachment_settings + 'attachment_settings.purge_enabled' => '삭제 첨부 영구 정리', + 'attachment_settings.purge_retention_days' => '삭제 첨부 보존기간', ], ], diff --git a/modules/_bundled/sirsoft-board/src/routes/api.php b/modules/_bundled/sirsoft-board/src/routes/api.php index 2ca4aee6..a004a833 100644 --- a/modules/_bundled/sirsoft-board/src/routes/api.php +++ b/modules/_bundled/sirsoft-board/src/routes/api.php @@ -126,20 +126,24 @@ Route::prefix('admin')->middleware(['auth:sanctum', 'admin'])->group(function () ->middleware('permission:admin,sirsoft-board.settings.read') ->name('admin.settings.show'); - // 대시보드 - 오늘 새 글/댓글 현황 (진입 가드는 코어 core.dashboard.read + admin) + // 대시보드 - 오늘 새 글/댓글 현황 (형제 settings 라우트와 동일하게 명시적 permission 가드) Route::get('dashboard/overview', [DashboardController::class, 'overview']) + ->middleware('permission:admin,sirsoft-board.dashboard.view') ->name('admin.dashboard.overview'); // 대시보드 - 7일 추세 그래프 (막대 + 합계 + 변화율) Route::get('dashboard/post-graph', [DashboardController::class, 'postGraph']) + ->middleware('permission:admin,sirsoft-board.dashboard.view') ->name('admin.dashboard.post-graph'); // 대시보드 - 최신 게시글 Route::get('dashboard/recent-posts', [DashboardController::class, 'recentPosts']) + ->middleware('permission:admin,sirsoft-board.dashboard.view') ->name('admin.dashboard.recent-posts'); // 대시보드 - 미처리 신고 Route::get('dashboard/pending-reports', [DashboardController::class, 'pendingReports']) + ->middleware('permission:admin,sirsoft-board.dashboard.view') ->name('admin.dashboard.pending-reports'); }); @@ -504,8 +508,8 @@ Route::post('boards/{slug}/comments/{commentId}/verify-password', [UserCommentCo | 로그인한 회원의 게시글 활동(작성, 댓글, 신고)을 조회하는 API입니다. | - 회원만 접근 가능하므로 auth:sanctum 미들웨어 필요 | -| 최종 URL 예시: /api/me/board-activities -| 최종 Name 예시: api.me.board-activities.index +| 최종 URL 예시: /api/modules/sirsoft-board/me/board-activities +| 최종 Name 예시: api.modules.sirsoft-board.me.board-activities.index | */ Route::get('/me/board-activities', [UserActivityController::class, 'index']) diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardDeletionTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardDeletionTest.php index ce3ac68f..198210f2 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardDeletionTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardDeletionTest.php @@ -6,6 +6,7 @@ namespace Modules\Sirsoft\Board\Tests\Feature\Admin; require_once __DIR__.'/../../ModuleTestCase.php'; use App\Contracts\Extension\StorageInterface; +use App\Extension\HookManager; use App\Extension\Storage\ModuleStorageDriver; use App\Models\User; use Illuminate\Support\Facades\DB; @@ -61,6 +62,7 @@ class BoardDeletionTest extends ModuleTestCase * 현행(soft delete) 코드에서는 withTrashed count 가 0 이 아니어서 fail (red). * * @scenario child_type=post + * * @effects post_records_force_deleted,comment_records_force_deleted,attachment_records_force_deleted,board_itself_force_deleted */ public function test_deleting_board_force_deletes_child_records(): void @@ -117,6 +119,7 @@ class BoardDeletionTest extends ModuleTestCase * 남아 fail (red), slug 기반 삭제로 수정하면 green. * * @scenario child_type=attachment + * * @effects attachment_physical_files_removed */ public function test_deleting_board_removes_attachment_physical_files(): void @@ -146,6 +149,7 @@ class BoardDeletionTest extends ModuleTestCase * 게시판이 사라져도 신고 레코드는 보존되고 board_id 만 NULL 이 된다. * * @scenario child_type=report + * * @effects reports_preserved_with_null_board_id */ public function test_deleting_board_preserves_reports(): void @@ -173,6 +177,7 @@ class BoardDeletionTest extends ModuleTestCase * 댓글·첨부 등 하위 데이터가 영향받지 않음을 board 격리 관점에서 함께 보장) * * @scenario child_type=comment + * * @effects other_board_files_not_affected */ public function test_deleting_board_does_not_affect_other_board_files(): void @@ -197,6 +202,66 @@ class BoardDeletionTest extends ModuleTestCase $this->boardService->deleteBoard($boardB->id); } + /** + * 게시판 삭제 시 posts.before_force_delete 훅이 게시판 인스턴스와 함께 + * 전체 글 ID(soft-deleted 포함)를 페이로드로 발화하는지 검증합니다. + * + * BoardService::deleteBoard 는 posts forceDelete 직전에 withTrashed 전체를 + * 1,000건 chunk 당 1회 발화합니다 — 수집된 ID 합집합이 전체 글 ID 와 일치해야 + * 연동 확장(이커머스 문의 피벗 정리 등)이 누락 없이 통지받습니다. + */ + public function test_fires_posts_before_force_delete_hook_with_all_ids(): void + { + $board = $this->createTestBoard('del-test-5'); + + // 일반 글 + soft-deleted 글 (둘 다 물리 삭제 대상이므로 훅 페이로드에 포함되어야 함) + $alivePost = Post::create([ + 'board_id' => $board->id, + 'title' => '살아있는 게시글', + 'content' => '본문', + 'status' => PostStatus::Published, + 'ip_address' => '127.0.0.1', + ]); + + $trashedPost = Post::create([ + 'board_id' => $board->id, + 'title' => '소프트 삭제된 게시글', + 'content' => '본문', + 'status' => PostStatus::Deleted, + 'ip_address' => '127.0.0.1', + ]); + $trashedPost->delete(); + + $receivedIds = []; + $receivedBoardIds = []; + + // 훅 스파이 — ModuleTestCase 가 setUp/tearDown 에서 HookManager 상태를 + // 스냅샷/복원하므로 별도 정리는 불필요 + HookManager::addAction( + 'sirsoft-board.board.posts.before_force_delete', + function ($hookBoard, $postIds) use (&$receivedIds, &$receivedBoardIds) { + $receivedIds = array_merge($receivedIds, $postIds); + $receivedBoardIds[] = $hookBoard->id; + } + ); + + $this->boardService->deleteBoard($board->id); + + // 수집된 ID 합집합 = 전체 글 ID (중복 발화 가능성 대비 unique 후 비교) + $union = array_values(array_unique($receivedIds)); + sort($union); + $expected = [$alivePost->id, $trashedPost->id]; + sort($expected); + + $this->assertSame($expected, $union, '훅 페이로드 ID 합집합이 soft-deleted 포함 전체 글 ID 와 일치해야 합니다.'); + + // board 인자 전달 검증 + $this->assertNotEmpty($receivedBoardIds, '훅이 최소 1회 발화되어야 합니다.'); + foreach (array_unique($receivedBoardIds) as $receivedBoardId) { + $this->assertSame($board->id, $receivedBoardId, '훅의 첫 인자는 삭제 대상 게시판이어야 합니다.'); + } + } + /** * 테스트용 게시판을 생성합니다. * diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardIndexSortTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardIndexSortTest.php new file mode 100644 index 00000000..ea87cd51 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardIndexSortTest.php @@ -0,0 +1,138 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Admin; + +// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\User; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Models\BoardType; +use Modules\Sirsoft\Board\Services\BoardService; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; + +/** + * 관리자 게시판 목록 정렬/페이지 크기 게이트 테스트 + * + * 검증 목적: + * - IndexBoardRequest: sort_by 화이트리스트(in:) 밖 컬럼 → 422 (스키마 노출·비인덱스 정렬 DoS 차단) + * - IndexBoardRequest: per_page 1~999 범위 밖 → 422 (상한은 전체 목록 셀렉트 소비처 수용치) + * - 화이트리스트 컬럼 정렬은 정상 동작 (sort_by=slug&sort_order=asc) + * - BoardService::getBoards 직접 호출(게이트 우회 내부 호출)도 미허용 컬럼을 + * 기본 정렬로 fallback — 예외 없이 동작 (ResolvesSortSpec 이중 방어) + * + * @group board + * @group admin + */ +class BoardIndexSortTest extends ModuleTestCase +{ + protected User $adminUser; + + private const INDEX_URL = '/api/modules/sirsoft-board/admin/boards'; + + /** + * 테스트 환경 설정 + */ + protected function setUp(): void + { + parent::setUp(); + + BoardType::firstOrCreate( + ['slug' => 'basic'], + ['name' => ['ko' => '기본', 'en' => 'Basic'], 'is_active' => true, 'is_default' => true] + ); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-board.boards.read', + ]); + } + + /** + * 화이트리스트 밖 정렬 컬럼(sort_by=settings) → 422. + */ + public function test_rejects_unknown_sort_column(): void + { + $this->actingAs($this->adminUser) + ->getJson(self::INDEX_URL.'?sort_by=settings') + ->assertStatus(422); + } + + /** + * per_page 상한(999) 초과(per_page=1000) → 422. + */ + public function test_rejects_out_of_range_per_page(): void + { + $this->actingAs($this->adminUser) + ->getJson(self::INDEX_URL.'?per_page=1000') + ->assertStatus(422); + } + + /** + * 전체 목록 셀렉트 소비처의 per_page 를 게이트가 수용한다 (회귀). + * + * 게시판은 운영자 등록 설정성 테이블이라 레이아웃이 사실상 전량 조회를 쓴다: + * - 게시판 설정 화면(admin_board_settings.json): per_page=999 + * - 쇼핑몰 환경설정 문의 카드(admin-ecommerce-inquiry-settings.json): per_page=200 + * 게이트를 100 으로 조였을 때 두 화면 진입이 422 토스트로 깨졌다 (#573 회귀). + */ + public function test_accepts_full_list_consumer_per_page(): void + { + foreach ([200, 999] as $perPage) { + $this->actingAs($this->adminUser) + ->getJson(self::INDEX_URL.'?per_page='.$perPage) + ->assertStatus(200); + } + } + + /** + * 화이트리스트 컬럼 정렬(sort_by=slug&sort_order=asc)은 200 + 순서 보장. + */ + public function test_sorts_by_whitelisted_column(): void + { + $prefix = 'sort-'.uniqid(); + + // 생성 순서를 정렬 기대 순서와 다르게 섞는다 (기본 정렬 fallback 오탐 방지) + foreach (['c', 'a', 'b'] as $suffix) { + Board::create([ + 'name' => ['ko' => "정렬 테스트 {$suffix}"], + 'slug' => "{$prefix}-{$suffix}", + 'type' => 'basic', + 'is_active' => true, + ]); + } + + $rows = $this->actingAs($this->adminUser) + ->getJson(self::INDEX_URL."?search={$prefix}&sort_by=slug&sort_order=asc") + ->assertStatus(200) + ->json('data.data'); + + $slugs = array_column($rows, 'slug'); + + $this->assertSame( + ["{$prefix}-a", "{$prefix}-b", "{$prefix}-c"], + $slugs, + 'slug 오름차순 정렬이 적용되어야 합니다.' + ); + } + + /** + * BoardService::getBoards 직접 호출(게이트 우회 내부 호출)에 미허용 정렬 컬럼을 + * 넘겨도 예외 없이 기본 정렬로 fallback 한다 (ResolvesSortSpec 이중 방어). + */ + public function test_service_ignores_unlisted_sort_column(): void + { + $this->actingAs($this->adminUser); + + $result = app(BoardService::class)->getBoards([ + 'sort_by' => 'evil', + 'sort_order' => 'asc', + ]); + + $this->assertInstanceOf( + LengthAwarePaginator::class, + $result, + '미허용 정렬 컬럼은 예외 없이 기본 정렬로 fallback 되어야 합니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsControllerTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsControllerTest.php index 5e0504de..2c2f331f 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsControllerTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsControllerTest.php @@ -144,13 +144,14 @@ class BoardSettingsControllerTest extends ModuleTestCase $response->assertStatus(200); - // 저장된 값이 파일에 반영되었는지 확인 - $filePath = $this->settingsStoragePath.'/basic_defaults.json'; - $this->assertFileExists($filePath); + // 저장 결과는 조회 API 로 확인한다 — settings 디스크는 테스트 하네스가 페이크로 + // 대체하므로 실제 storage 경로를 직접 읽으면 저장 성공에도 파일이 없다. + $saved = $this->actingAs($this->adminUser) + ->getJson('/api/modules/sirsoft-board/admin/settings/basic_defaults'); - $content = json_decode(File::get($filePath), true); - $this->assertEquals(30, $content['per_page']); - $this->assertEquals(10, $content['per_page_mobile']); + $saved->assertStatus(200); + $this->assertEquals(30, $saved->json('data.settings.per_page')); + $this->assertEquals(10, $saved->json('data.settings.per_page_mobile')); } /** @@ -170,13 +171,13 @@ class BoardSettingsControllerTest extends ModuleTestCase $response->assertStatus(200); - // 저장된 값이 파일에 반영되었는지 확인 - $filePath = $this->settingsStoragePath.'/notifications.json'; - $this->assertFileExists($filePath); + // 저장 결과는 조회 API 로 확인한다 (settings 디스크 페이크 — 위 주석 참조). + $saved = $this->actingAs($this->adminUser) + ->getJson('/api/modules/sirsoft-board/admin/settings/notifications'); - $content = json_decode(File::get($filePath), true); - $this->assertIsArray($content['channels']); - $this->assertEquals('mail', $content['channels'][0]['id']); + $saved->assertStatus(200); + $this->assertIsArray($saved->json('data.settings.channels')); + $this->assertEquals('mail', $saved->json('data.settings.channels.0.id')); } /** @@ -617,8 +618,11 @@ class BoardSettingsControllerTest extends ModuleTestCase $response->assertStatus(200); - $content = json_decode(File::get($this->settingsStoragePath.'/report_policy.json'), true); - $this->assertSame(0, (int) $content['auto_hide_threshold']); + $saved = $this->actingAs($this->adminUser) + ->getJson('/api/modules/sirsoft-board/admin/settings/report_policy'); + + $saved->assertStatus(200); + $this->assertSame(0, (int) $saved->json('data.settings.auto_hide_threshold')); } /** @@ -759,4 +763,49 @@ class BoardSettingsControllerTest extends ModuleTestCase $board->refresh(); $this->assertEquals(10, $board->per_page); } + + /** + * 첨부 정리 설정이 general 탭 저장으로 실제 반영되는지 확인 (회귀) + * + * 화면은 general 탭에서 이 카테고리를 함께 제출한다. 서버가 카테고리를 허용하지 않으면 + * 저장은 200 인데 값만 사라져, 운영자가 토글을 켜도 정리가 영원히 꺼진 상태로 남는다. + */ + public function test_attachment_cleanup_settings_are_saved_on_general_tab(): void + { + $response = $this->actingAs($this->adminUser) + ->putJson('/api/modules/sirsoft-board/admin/settings', [ + '_tab' => 'general', + 'display' => ['date_display_format' => 'standard'], + 'attachment_settings' => [ + 'purge_enabled' => true, + 'purge_retention_days' => 15, + ], + ]); + + $response->assertStatus(200); + + $followUp = $this->actingAs($this->adminUser) + ->getJson('/api/modules/sirsoft-board/admin/settings/attachment_settings'); + + $followUp->assertStatus(200); + $this->assertTrue($followUp->json('data.settings.purge_enabled')); + $this->assertSame(15, $followUp->json('data.settings.purge_retention_days')); + } + + /** + * 보존기간 하한(1일) 미만은 거부되는지 확인 — 사용자 파일을 파기하는 설정이므로. + */ + public function test_attachment_purge_retention_days_below_minimum_is_rejected(): void + { + $response = $this->actingAs($this->adminUser) + ->putJson('/api/modules/sirsoft-board/admin/settings', [ + '_tab' => 'general', + 'attachment_settings' => [ + 'purge_enabled' => true, + 'purge_retention_days' => 0, + ], + ]); + + $response->assertStatus(422); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsSeoCacheInvalidationTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsSeoCacheInvalidationTest.php new file mode 100644 index 00000000..db024d88 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsSeoCacheInvalidationTest.php @@ -0,0 +1,180 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Admin; + +use App\Extension\HookManager; +use App\Models\User; +use App\Seo\Contracts\SeoCacheManagerInterface; +use Mockery; +use Modules\Sirsoft\Board\Listeners\SeoBoardSettingsCacheListener; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; + +/** + * 게시판 모듈 설정 저장 시 SEO 캐시 무효화 훅 발화 (B-5) + * + * `SeoBoardSettingsCacheListener` 는 `core.module_settings.after_save` 를 구독하지만 + * 그 훅의 유일한 발화 지점(`ModuleSettingsService::save()`)이 프로덕션에서 호출되지 않아 + * 게시판 환경설정을 저장해도 SEO 캐시가 무효화되지 않았다. + * + * 일괄 적용(bulkApply)은 이미 발화 중인 `sirsoft-board.settings.after_bulk_apply` 를 + * 리스너가 함께 구독해 커버한다 (신규 훅 발명 없음). + */ +class BoardSettingsSeoCacheInvalidationTest extends ModuleTestCase +{ + private string $apiBase = '/api/modules/sirsoft-board/admin/settings'; + + private User $adminUser; + + /** + * 훅 수신 기록 + * + * @var array<int, array> + */ + private array $received = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-board.settings.read', + 'sirsoft-board.settings.update', + ]); + + $this->received = []; + HookManager::addAction('core.module_settings.after_save', function (...$args) { + $this->received[] = $args; + }, 5); + } + + /** + * 게시판 설정 저장 API 가 모듈 설정 저장 훅을 발화한다. (실패-먼저) + * + * @scenario save_path=bulk + * + * @effects module_settings_after_save_hook_fired + */ + public function test_settings_save_fires_module_settings_after_save_hook(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'basic_defaults', + 'basic_defaults' => ['per_page' => 30], + ])->assertOk(); + + $this->assertCount(1, $this->received, '모듈 설정 저장 훅이 발화되지 않았습니다.'); + [$identifier, $settings, $result] = $this->received[0]; + + $this->assertSame('sirsoft-board', $identifier); + $this->assertArrayHasKey('basic_defaults', $settings); + $this->assertTrue($result); + } + + /** + * 게시판 SEO 캐시 리스너가 일괄 적용 훅도 구독한다. (실패-먼저) + * + * @scenario save_path=bulk_apply + * + * @effects seo_cache_invalidated_on_bulk_apply + */ + public function test_seo_listener_subscribes_bulk_apply_hook(): void + { + $hooks = SeoBoardSettingsCacheListener::getSubscribedHooks(); + + $this->assertArrayHasKey( + 'sirsoft-board.settings.after_bulk_apply', + $hooks, + '일괄 적용 경로가 SEO 캐시 무효화를 거치지 않습니다.' + ); + $this->assertSame('onBulkApply', $hooks['sirsoft-board.settings.after_bulk_apply']['method']); + } + + /** + * 일괄 적용 훅 핸들러는 식별자 인자 없이도 안전하게 동작한다. + * + * `after_bulk_apply` payload 는 ($fields, $updatedCount) 라 모듈 식별자가 없다 — + * 식별자 가드를 그대로 재사용하면 조용히 무효화가 건너뛰어진다. + * + * @scenario save_path=bulk_apply + * + * @effects seo_cache_invalidated_on_bulk_apply + */ + public function test_bulk_apply_handler_invalidates_without_identifier_arg(): void + { + $listener = new SeoBoardSettingsCacheListener; + + $this->assertTrue( + method_exists($listener, 'onBulkApply'), + '일괄 적용 전용 핸들러가 없습니다.' + ); + + // 예외 없이 완료되어야 한다 (내부 실패는 로깅으로 흡수) + $listener->onBulkApply(['per_page' => 20], 3); + $this->addToAssertionCount(1); + } + + // ─── 실제 무효화 수행 ────────────────────────────────────── + + /** + * 일괄 적용 핸들러가 게시판 레이아웃 캐시를 실제로 지운다. + * + * 예외가 안 나는 것과 무효화를 수행하는 것은 다르다 — 리스너 본문이 통째로 비어도 + * "예외 없음" 은 그대로 통과한다. 캐시 매니저를 mock 해 호출 자체를 단언한다. + * + * @scenario save_path=bulk_apply + * + * @effects seo_cache_invalidated_on_bulk_apply + */ + public function test_bulk_apply_actually_invalidates_board_layouts(): void + { + $cache = Mockery::mock(SeoCacheManagerInterface::class); + foreach (['board/index', 'board/show', 'board/boards'] as $layout) { + $cache->shouldReceive('invalidateByLayout')->with($layout)->once(); + } + $cache->shouldReceive('invalidateByLayout')->andReturnNull(); + $this->app->instance(SeoCacheManagerInterface::class, $cache); + + (new SeoBoardSettingsCacheListener)->onBulkApply(['per_page' => 20], 3); + + $this->addToAssertionCount(1); + } + + /** + * 설정 저장 훅도 같은 무효화를 수행한다. + * + * @scenario save_path=bulk + * + * @effects seo_cache_invalidated_on_module_settings_save + */ + public function test_settings_save_actually_invalidates_board_layouts(): void + { + $cache = Mockery::mock(SeoCacheManagerInterface::class); + $cache->shouldReceive('invalidateByLayout')->with('board/index')->atLeast()->once(); + $cache->shouldReceive('invalidateByLayout')->andReturnNull(); + $this->app->instance(SeoCacheManagerInterface::class, $cache); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'basic_defaults', + 'basic_defaults' => ['per_page' => 30], + ])->assertOk(); + + $this->addToAssertionCount(1); + } + + /** + * 다른 모듈의 저장 훅에는 반응하지 않는다. + * + * @scenario save_path=bulk + * + * @effects seo_cache_untouched_for_other_module + */ + public function test_other_module_save_does_not_invalidate_board_layouts(): void + { + $cache = Mockery::mock(SeoCacheManagerInterface::class); + $cache->shouldNotReceive('invalidateByLayout'); + $this->app->instance(SeoCacheManagerInterface::class, $cache); + + (new SeoBoardSettingsCacheListener)->onModuleSettingsSave('sirsoft-ecommerce', ['seo' => []], true); + + $this->addToAssertionCount(1); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardTypeManagementTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardTypeManagementTest.php index 13c85a40..221fcbe7 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardTypeManagementTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardTypeManagementTest.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Schema; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Models\BoardType; use Modules\Sirsoft\Board\Services\BoardSettingsService; +use Modules\Sirsoft\Board\Services\BoardTypeService; use Modules\Sirsoft\Board\Tests\ModuleTestCase; class BoardTypeManagementTest extends ModuleTestCase @@ -258,6 +259,64 @@ class BoardTypeManagementTest extends ModuleTestCase } } + /** + * 삭제 불가 응답의 메시지가 예외 원문이 아니라 해석된 다국어 문구여야 한다. + * + * 종전에는 예외 메시지(이미 번역된 문장)를 다시 메시지 키 자리에 넘겨서, 키 해석에 + * 실패한 원문이 그대로 사용자 화면에 나갔다. 상태코드만 보는 단언은 이 결함을 + * 통과시킨다 — 422 는 그대로였기 때문이다. + * + * @scenario error_class=domain + * + * @effects board_type_delete_domain_exception_returns_422_with_resolved_message + */ + public function test_delete_in_use_response_message_is_resolved_key_not_exception_text(): void + { + $boardType = BoardType::create([ + 'slug' => 'test_in_use_msg', + 'name' => ['ko' => '사용중 유형'], + ]); + + Board::factory()->create(['type' => 'test_in_use_msg']); + + $response = $this->actingAs($this->adminUser) + ->deleteJson("/api/modules/sirsoft-board/admin/board-types/{$boardType->id}"); + + $response->assertStatus(422); + + $expected = __('sirsoft-board::messages.board_type.delete_in_use', ['count' => 1]); + $this->assertSame($expected, $response->json('message')); + // 키 문자열 자체가 노출되면(해석 실패) 그것도 결함이다 + $this->assertStringNotContainsString('sirsoft-board::', (string) $response->json('message')); + } + + /** + * 인프라 예외는 422 로 뭉개지 않고 500 으로 구분하며 예외 원문을 노출하지 않는다. + * + * @scenario error_class=infrastructure + * + * @effects board_type_delete_infrastructure_exception_returns_500 + */ + public function test_delete_infrastructure_exception_returns_500(): void + { + $boardType = BoardType::create([ + 'slug' => 'test_infra_500', + 'name' => ['ko' => '인프라 예외 유형'], + ]); + + $this->mock(BoardTypeService::class, function ($mock) { + $mock->shouldReceive('deleteBoardType') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error: 2006 MySQL server has gone away')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->adminUser) + ->deleteJson("/api/modules/sirsoft-board/admin/board-types/{$boardType->id}"); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + /** * 존재하지 않는 유형 삭제 시 404 테스트 */ diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/DashboardApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/DashboardApiTest.php index eddeb818..f888e524 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/Admin/DashboardApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/DashboardApiTest.php @@ -33,9 +33,8 @@ class DashboardApiTest extends ModuleTestCase BoardStat::query()->delete(); - // isAdmin() 은 admin 타입 권한 보유 여부로 판정되므로 임의 권한을 부여한다. - // 대시보드 라우트는 admin 미들웨어만 사용 (별도 permission 가드 없음). - $this->adminUser = $this->createAdminUser(['sirsoft-board.boards.read']); + // 대시보드 라우트는 admin 미들웨어 + permission:admin,sirsoft-board.dashboard.view 가드를 사용한다. + $this->adminUser = $this->createAdminUser(['sirsoft-board.boards.read', 'sirsoft-board.dashboard.view']); $this->normalUser = $this->createUser(); Board::create([ @@ -59,6 +58,11 @@ class DashboardApiTest extends ModuleTestCase $this->actingAs($this->normalUser)->getJson(self::BASE.'/overview')->assertStatus(403); } + /** + * @scenario case=dashboard_permission_gate + * + * @effects dashboard_requires_view_permission + */ #[Test] public function test_overview_returns_today_counts_for_admin(): void { diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReplyDeletePolicyTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReplyDeletePolicyTest.php new file mode 100644 index 00000000..eaec1924 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Admin/ReplyDeletePolicyTest.php @@ -0,0 +1,430 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Admin; + +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\User; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Board\Exceptions\PostHasRepliesException; +use Modules\Sirsoft\Board\Services\PostService; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * 답글 삭제 정책(reply_delete_policy) + 답글 트리 연쇄 삭제/복원 테스트 + * + * 검증 목적: + * - cascade 정책(기본): 부모 삭제 시 자손 답글 전체 + 그 댓글/첨부가 cascade soft delete + * - 복원 시 cascade 로 지워진 자손 + 그 댓글/첨부만 top-down 선택 복원 + * - 사용자 직접 삭제(trigger 'user') 서브트리는 부모 복원 후에도 trashed 유지 + * - block 정책: 살아있는 직계 답글 존재 시 관리자/사용자 삭제 경로 모두 422 차단 + * - block 정책이라도 답글이 전부 trashed 면 삭제 허용 + * - 훅 경유 삭제(options cascade_replies=true)는 block 정책 우회 + cascade 고정 + * - 이미 trashed 인 자손은 cascade 가 변조하지 않음 (trigger_type/deleted_at 불변) + * - 신고 조치 삭제(trigger 'report')에도 block 정책 동일 적용 (cascade_replies 옵션만 우회) + * - 끊긴 체인 밑 고아 답글은 block 통과 삭제에서도 cascade 스윕으로 함께 정리 + * + * @group board + * @group admin + */ +class ReplyDeletePolicyTest extends BoardTestCase +{ + private PostService $postService; + + private User $adminWithManage; + + protected function getTestBoardSlug(): string + { + return 'reply-delete-policy'; + } + + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '답글 삭제 정책 테스트 게시판', 'en' => 'Reply Delete Policy Board'], + 'is_active' => true, + 'secret_mode' => 'disabled', + 'blocked_keywords' => [], + // 이전 테스트가 block 으로 바꾼 값이 잔류하지 않도록 매 테스트 기본값으로 리셋 + 'reply_delete_policy' => 'cascade', + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->postService = app(PostService::class); + + $slug = $this->board->slug; + + $this->adminWithManage = $this->createAdminUser([ + "sirsoft-board.{$slug}.admin.manage", + ]); + + // 사용자 삭제 경로(회원 본인 글) 검증용 + $this->grantUserRolePermissions(['posts.read', 'posts.write']); + } + + // ========================================== + // Helper + // ========================================== + + /** + * 관리자 게시글 API URL 을 반환합니다. + * + * @param string $suffix URL 접미사 + * @return string 관리자 게시글 API URL + */ + private function adminUrl(string $suffix): string + { + return "/api/modules/sirsoft-board/admin/board/{$this->board->slug}/posts{$suffix}"; + } + + /** + * 사용자 게시글 API URL 을 반환합니다. + * + * @param string $suffix URL 접미사 + * @return string 사용자 게시글 API URL + */ + private function userUrl(string $suffix): string + { + return "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts{$suffix}"; + } + + /** + * 답글(자식 게시글)을 직접 DB 에 생성합니다. + * + * @param int $parentId 부모 게시글 ID + * @param int $depth 답글 깊이 + * @param array $attributes 덮어쓸 속성 + * @return int 생성된 답글 ID + */ + private function createReply(int $parentId, int $depth = 1, array $attributes = []): int + { + return $this->createTestPost(array_merge([ + 'title' => "답글 (depth {$depth})", + 'parent_id' => $parentId, + 'depth' => $depth, + ], $attributes)); + } + + /** + * 첨부파일을 직접 DB 에 생성합니다 (PostDeleteCascadeTest 와 동일 패턴). + * + * @param int $postId 게시글 ID + * @param array $attributes 덮어쓸 속성 + * @return int 생성된 첨부 ID + */ + private function createTestAttachment(int $postId, array $attributes = []): int + { + static $seq = 0; + $seq++; + + $defaults = [ + 'board_id' => $this->board->id, + 'post_id' => $postId, + 'hash' => 'r'.str_pad((string) $seq, 11, '0', STR_PAD_LEFT), + 'original_filename' => 'test.png', + 'stored_filename' => 'stored-'.$seq.'.png', + 'disk' => 'local', + 'path' => 'attachments/reply-'.$seq.'.png', + 'mime_type' => 'image/png', + 'size' => 1024, + 'collection' => 'default', + 'order' => 0, + 'created_at' => now(), + 'updated_at' => now(), + ]; + + return DB::table('board_attachments')->insertGetId(array_merge($defaults, $attributes)); + } + + /** + * 게시글 행을 조회합니다. + * + * @param int $postId 게시글 ID + * @return object|null 게시글 행 + */ + private function postRow(int $postId): ?object + { + return DB::table('board_posts')->where('id', $postId)->first(); + } + + // ========================================== + // cascade 정책 (기본) + // ========================================== + + /** + * cascade 정책: 부모 삭제 시 깊이 3 답글 트리 전체 + 자손별 댓글/첨부가 + * cascade soft delete + trigger_type='cascade' + status='deleted' 로 마킹된다. + */ + public function test_cascade_policy_soft_deletes_all_descendant_replies(): void + { + $parentId = $this->createTestPost(['status' => 'published']); + $reply1 = $this->createReply($parentId, 1); + $reply2 = $this->createReply($reply1, 2); + $reply3 = $this->createReply($reply2, 3); + + $commentIds = []; + $attachmentIds = []; + foreach ([$reply1, $reply2, $reply3] as $replyId) { + $commentIds[$replyId] = $this->createTestComment($replyId); + $attachmentIds[$replyId] = $this->createTestAttachment($replyId); + } + + $this->postService->deletePost($this->board->slug, $parentId, 'admin'); + + foreach ([$reply1, $reply2, $reply3] as $replyId) { + $row = $this->postRow($replyId); + $this->assertNotNull($row->deleted_at, "자손 답글 {$replyId} 이 soft delete 되어야 합니다."); + $this->assertSame('cascade', $row->trigger_type, "자손 답글 {$replyId} 이 cascade 로 마킹되어야 합니다."); + $this->assertSame('deleted', $row->status, "자손 답글 {$replyId} 의 status 가 deleted 여야 합니다."); + + $comment = DB::table('board_comments')->where('id', $commentIds[$replyId])->first(); + $this->assertNotNull($comment->deleted_at, "자손 답글 {$replyId} 의 댓글이 soft delete 되어야 합니다."); + $this->assertSame('cascade', $comment->trigger_type); + + $attachment = DB::table('board_attachments')->where('id', $attachmentIds[$replyId])->first(); + $this->assertNotNull($attachment->deleted_at, "자손 답글 {$replyId} 의 첨부가 soft delete 되어야 합니다."); + $this->assertSame('cascade', $attachment->trigger_type); + } + } + + /** + * 복원 시 cascade 로 지워졌던 자손 답글 + 그 댓글/첨부가 함께 복원된다. + */ + public function test_cascade_restore_restores_descendants_and_their_children(): void + { + $parentId = $this->createTestPost(['status' => 'published']); + $reply1 = $this->createReply($parentId, 1); + $reply2 = $this->createReply($reply1, 2); + + $commentId = $this->createTestComment($reply1); + $attachmentId = $this->createTestAttachment($reply2); + + $this->postService->deletePost($this->board->slug, $parentId, 'admin'); + $this->postService->restorePost($this->board->slug, $parentId, '복원', 'admin'); + + foreach ([$reply1, $reply2] as $replyId) { + $row = $this->postRow($replyId); + $this->assertNull($row->deleted_at, "cascade 자손 답글 {$replyId} 은 복원되어야 합니다."); + $this->assertSame('published', $row->status, "복원된 자손 답글 {$replyId} 의 status 가 published 여야 합니다."); + } + + $comment = DB::table('board_comments')->where('id', $commentId)->first(); + $this->assertNull($comment->deleted_at, 'cascade 자손 답글의 댓글도 복원되어야 합니다.'); + + $attachment = DB::table('board_attachments')->where('id', $attachmentId)->first(); + $this->assertNull($attachment->deleted_at, 'cascade 자손 답글의 첨부도 복원되어야 합니다.'); + } + + /** + * 사용자 직접 삭제(trigger 'user') 로 먼저 지워진 중간 자손의 서브트리는 + * 부모 삭제 → 복원 후에도 trashed 로 유지된다 (고아 재생성 금지). + */ + public function test_user_predeleted_reply_subtree_stays_trashed_after_restore(): void + { + $parentId = $this->createTestPost(['status' => 'published']); + $reply1 = $this->createReply($parentId, 1); + $reply2 = $this->createReply($reply1, 2); + + // 중간 자손(reply1)을 사용자 삭제 경로로 먼저 삭제 → reply1 trigger 'user', + // 그 자식 reply2 는 reply1 삭제의 cascade 로 trashed + $this->postService->deletePost($this->board->slug, $reply1, 'user'); + + $this->assertSame('user', $this->postRow($reply1)->trigger_type, '테스트 전제: reply1 은 user 삭제여야 합니다.'); + $this->assertNotNull($this->postRow($reply2)->deleted_at, '테스트 전제: reply2 는 reply1 삭제로 trashed 여야 합니다.'); + + // 부모 삭제 → 복원 + $this->postService->deletePost($this->board->slug, $parentId, 'admin'); + $this->postService->restorePost($this->board->slug, $parentId, '복원', 'admin'); + + $this->assertNull($this->postRow($parentId)->deleted_at, '부모 글은 복원되어야 합니다.'); + + $reply1Row = $this->postRow($reply1); + $this->assertNotNull($reply1Row->deleted_at, '사용자가 직접 지운 답글은 복원되면 안 됩니다.'); + $this->assertSame('user', $reply1Row->trigger_type, '사용자 삭제 trigger_type 은 유지되어야 합니다.'); + + $this->assertNotNull( + $this->postRow($reply2)->deleted_at, + '사용자 삭제 답글의 서브트리도 trashed 로 유지되어야 합니다 (고아 재생성 금지).' + ); + } + + /** + * cascade 는 이미 trashed 인 자손(trigger 'user')을 변조하지 않는다 + * (trigger_type 유지 + deleted_at 불변). + */ + public function test_cascade_skips_already_trashed_descendants(): void + { + $parentId = $this->createTestPost(['status' => 'published']); + $this->createReply($parentId, 1); // 살아있는 답글 — cascade 가 실제로 수행되는 전제 + + $preDeletedAt = now()->subHour(); + $preDeletedReplyId = $this->createReply($parentId, 1, [ + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => $preDeletedAt, + ]); + + $before = $this->postRow($preDeletedReplyId); + + $this->postService->deletePost($this->board->slug, $parentId, 'admin'); + + $after = $this->postRow($preDeletedReplyId); + $this->assertSame('user', $after->trigger_type, '이미 trashed 인 자손의 trigger_type 은 유지되어야 합니다.'); + $this->assertSame($before->deleted_at, $after->deleted_at, '이미 trashed 인 자손의 deleted_at 은 불변이어야 합니다.'); + } + + // ========================================== + // block 정책 + // ========================================== + + /** + * block 정책: 살아있는 답글이 있는 글의 관리자 삭제 → 422 + 정책 메시지, 부수효과 0. + */ + public function test_block_policy_returns_422_with_message_when_alive_reply_exists(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $parentId = $this->createTestPost(['status' => 'published']); + $replyId = $this->createReply($parentId, 1); + + $response = $this->actingAs($this->adminWithManage) + ->deleteJson($this->adminUrl("/{$parentId}")); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-board::validation.post.delete.has_replies'), + $response->json('message'), + '답글 삭제 정책 차단 메시지가 응답되어야 합니다.' + ); + + // 차단 시 부수효과 0: 부모/답글 모두 미삭제 + $this->assertNull($this->postRow($parentId)->deleted_at, '차단된 부모 글은 삭제되면 안 됩니다.'); + $this->assertNull($this->postRow($replyId)->deleted_at, '차단 시 답글도 영향받지 않아야 합니다.'); + } + + /** + * block 정책: 답글이 전부 trashed 면 삭제가 허용된다. + */ + public function test_block_policy_allows_delete_when_replies_all_trashed(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $parentId = $this->createTestPost(['status' => 'published']); + $this->createReply($parentId, 1, [ + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + + $this->actingAs($this->adminWithManage) + ->deleteJson($this->adminUrl("/{$parentId}")) + ->assertStatus(200); + + $this->assertNotNull($this->postRow($parentId)->deleted_at, '답글이 전부 trashed 면 부모 글 삭제가 허용되어야 합니다.'); + } + + /** + * block 정책은 사용자 삭제 경로(회원 본인 글)에도 동일하게 적용된다 → 422. + */ + public function test_user_delete_path_applies_policy(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $owner = $this->createUser(); + $parentId = $this->createTestPost([ + 'status' => 'published', + 'user_id' => $owner->id, + 'author_name' => $owner->name, + ]); + $this->createReply($parentId, 1); + + $response = $this->actingAs($owner) + ->deleteJson($this->userUrl("/{$parentId}")); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-board::validation.post.delete.has_replies'), + $response->json('message') + ); + $this->assertNull($this->postRow($parentId)->deleted_at, '차단된 글은 삭제되면 안 됩니다.'); + } + + /** + * 신고 조치 삭제(trigger 'report')에도 block 정책이 동일하게 적용된다. + * 정책 우회는 오직 cascade_replies 옵션(훅 경유)뿐 — trigger 문자열은 우회 수단이 아니다. + */ + public function test_report_trigger_delete_applies_block_policy(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $parentId = $this->createTestPost(['status' => 'published']); + $replyId = $this->createReply($parentId, 1); + + try { + $this->postService->deletePost($this->board->slug, $parentId, 'report'); + $this->fail('block 정책에서는 신고 조치 삭제도 PostHasRepliesException 으로 차단되어야 합니다.'); + } catch (PostHasRepliesException $e) { + // 기대 경로: ReportService::deleteContent 의 기존 generic catch 가 이 예외를 + // 조치 실패 사유로 로그 변환한다 (계획서 WP2 §차단 예외 — 정책 적용 유지). + } + + $this->assertNull($this->postRow($parentId)->deleted_at, '차단된 부모 글은 삭제되면 안 됩니다.'); + $this->assertNull($this->postRow($replyId)->deleted_at, '차단 시 답글도 영향받지 않아야 합니다.'); + } + + /** + * 끊긴 체인 밑 고아 답글: 직계 답글이 전부 trashed 라 block 정책이 삭제를 허용하는 + * 경우에도 cascade 스윕이 실행되어, trashed 자손 밑에 살아남아 있던 고아 답글을 + * 함께 정리한다 (계획서 WP2 §PostService — "block 통과 시에도 실행"). + */ + public function test_orphan_reply_under_trashed_chain_is_swept_on_delete(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $parentId = $this->createTestPost(['status' => 'published']); + $trashedChildId = $this->createReply($parentId, 1, [ + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + $orphanGrandchildId = $this->createReply($trashedChildId, 2); // 끊긴 체인 밑에 살아있는 고아 + + // 직계 답글이 전부 trashed → block 정책이 삭제를 허용 + $this->actingAs($this->adminWithManage) + ->deleteJson($this->adminUrl("/{$parentId}")) + ->assertStatus(200); + + $orphanRow = $this->postRow($orphanGrandchildId); + $this->assertNotNull($orphanRow->deleted_at, '끊긴 체인 밑 고아 답글도 cascade 스윕으로 정리되어야 합니다.'); + $this->assertSame('cascade', $orphanRow->trigger_type, '고아 답글은 cascade 로 마킹되어야 합니다.'); + $this->assertSame('deleted', $orphanRow->status); + + // 사용자 선삭제 중간 노드는 불변 (trigger_type/deleted_at 유지) + $this->assertSame('user', $this->postRow($trashedChildId)->trigger_type, '기존 trashed 중간 노드는 변조되면 안 됩니다.'); + } + + /** + * 훅 경유 삭제(options cascade_replies=true)는 block 정책을 우회하고 + * 자손 답글까지 cascade 로 함께 정리한다 (시스템 생성 답변 정리 보장). + */ + public function test_ecommerce_hook_delete_bypasses_block_policy(): void + { + $this->updateBoardSettings(['reply_delete_policy' => 'block']); + + $parentId = $this->createTestPost(['status' => 'published']); + $replyId = $this->createReply($parentId, 1); + + $this->postService->deletePost($this->board->slug, $parentId, options: ['cascade_replies' => true]); + + $this->assertNotNull($this->postRow($parentId)->deleted_at, 'cascade_replies 옵션은 block 정책을 우회해야 합니다.'); + + $replyRow = $this->postRow($replyId); + $this->assertNotNull($replyRow->deleted_at, '자손 답글도 함께 cascade 삭제되어야 합니다.'); + $this->assertSame('cascade', $replyRow->trigger_type); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Board/PublicRecentPostsVisibilityTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Board/PublicRecentPostsVisibilityTest.php new file mode 100644 index 00000000..af2b36e5 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Board/PublicRecentPostsVisibilityTest.php @@ -0,0 +1,211 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Board; + +// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\DB; +use Illuminate\Testing\TestResponse; +use Modules\Sirsoft\Board\Enums\PostStatus; +use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 공개 최근글/인기글의 열람 권한 필터 + 비밀글 가시성 테스트 (N-3 + N-4) + * + * - N-3: 공개 최근글(GET /boards/posts/recent)은 읽기 권한 없는 게시판의 글을 + * 익명/저권한 사용자에게 노출하면 안 된다. 비밀글은 제목 공개 정책(2026-02-04)에 + * 따라 제목을 노출한다(본문만 보호 — 목록/검색/홈과 동일). + * - N-4: 공개 인기글(GET /boards/popular)도 읽기 권한 없는 게시판의 글을 제외한다. + * (인기글은 별도 컨벤션으로 비밀글 자체를 제외한다 — 2026-01-16.) + */ +class PublicRecentPostsVisibilityTest extends ModuleTestCase +{ + private Board $readableBoard; + + private Board $hiddenBoard; + + protected function setUp(): void + { + parent::setUp(); + + Board::where('is_active', true)->update(['is_active' => false]); + Cache::flush(); + + $this->readableBoard = Board::factory()->create(['is_active' => true, 'name' => ['ko' => '공개', 'en' => 'Readable']]); + $this->hiddenBoard = Board::factory()->create(['is_active' => true, 'name' => ['ko' => '비공개', 'en' => 'Hidden']]); + + // 읽기 가능한 게시판: 공개글 + 비밀글 + $this->insertPost($this->readableBoard->id, '공개글읽기가능', 100, false, PostStatus::Published->value); + $this->insertPost($this->readableBoard->id, '비밀글읽기가능', 90, true, PostStatus::Published->value); + // 읽기 불가능한 게시판: 공개글 + $this->insertPost($this->hiddenBoard->id, '숨김게시판글', 200, false, PostStatus::Published->value); + + // 비회원 읽기 권한은 readableBoard 에만 부여 + $this->grantRead('guest', $this->readableBoard->slug); + PermissionMiddleware::clearGuestRoleCache(); + } + + // ========== N-3 최근글 ========== + + /** + * @scenario case=recent_readable_filter + * + * @effects unreadable_board_titles_absent_for_caller + */ + #[Test] + public function guest_recent_posts_shows_readable_titles_including_secret_but_excludes_unreadable_board(): void + { + $titles = $this->titles($this->getJson('/api/modules/sirsoft-board/boards/posts/recent?limit=20')); + + $this->assertContains('공개글읽기가능', $titles); + // 비밀글도 제목은 공개된다(본문만 보호) — 2026-02-04 확정 정책. + $this->assertContains('비밀글읽기가능', $titles, '비밀글도 제목은 최근글에 노출된다(본문만 보호).'); + $this->assertNotContains('숨김게시판글', $titles, '읽기 권한 없는 게시판 글은 노출되면 안 된다.'); + } + + /** + * @scenario case=recent_readable_filter + * + * @effects unreadable_board_titles_absent_for_caller + */ + #[Test] + public function low_permission_member_recent_posts_excludes_unreadable_board_titles(): void + { + $member = $this->memberWithReadOn($this->readableBoard->slug); + + $titles = $this->titles( + $this->actingAs($member)->getJson('/api/modules/sirsoft-board/boards/posts/recent?limit=20') + ); + + $this->assertContains('공개글읽기가능', $titles); + $this->assertContains('비밀글읽기가능', $titles, '비밀글도 제목은 노출된다(본문만 보호).'); + $this->assertNotContains('숨김게시판글', $titles, '읽기 권한 없는 게시판 글은 저권한 회원에게도 노출되면 안 된다.'); + } + + // ========== N-4 인기글 ========== + + /** + * @scenario case=popular_readable_filter + * + * @effects unreadable_board_titles_absent_for_caller + */ + #[Test] + public function guest_popular_posts_excludes_unreadable_board_titles(): void + { + $titles = $this->titles($this->getJson('/api/modules/sirsoft-board/boards/popular?limit=20')); + + $this->assertContains('공개글읽기가능', $titles); + $this->assertNotContains('숨김게시판글', $titles, '인기글도 읽기 권한 없는 게시판 글을 제외해야 한다.'); + // 인기글은 비밀글도 원래 제외한다(2026-01-16 컨벤션). + $this->assertNotContains('비밀글읽기가능', $titles); + } + + // ========== 게시판 디렉토리(boards index + 내장 최근글) ========== + // + // 최근글·인기글·인기게시판·대시보드에는 열람 권한 필터가 적용되었지만, + // 게시판 디렉토리(GET /boards?limit=N)는 활성 게시판 전체 + 내장 최근글 제목을 + // 무필터로 직렬화해 우회로가 남아 있었다 (7.0.7 사전점검 SEO 봇 렌더 실측 발견 — + // 게스트 홈 화면에 회원전용 게시판 글 제목이 노출). + + /** + * @scenario case=board_directory_readable_filter + * + * @effects board_directory_excludes_unreadable_board_and_posts + */ + #[Test] + public function guest_board_directory_excludes_unreadable_board_and_its_post_titles(): void + { + $response = $this->getJson('/api/modules/sirsoft-board/boards?limit=3'); + $response->assertStatus(200); + + $slugs = array_column($response->json('data'), 'slug'); + $this->assertContains($this->readableBoard->slug, $slugs); + $this->assertNotContains($this->hiddenBoard->slug, $slugs, '읽기 권한 없는 게시판은 디렉토리에 노출되면 안 된다.'); + + // 내장 최근글 제목까지 전수 검사 — 게시판 행이 아닌 다른 경로로도 새면 안 된다 + $raw = json_encode($response->json('data'), JSON_UNESCAPED_UNICODE); + $this->assertStringContainsString('공개글읽기가능', $raw); + $this->assertStringNotContainsString('숨김게시판글', $raw, '읽기 권한 없는 게시판의 글 제목이 디렉토리 응답에 노출되면 안 된다.'); + } + + /** + * @scenario case=board_directory_readable_filter + * + * @effects board_directory_includes_readable_board_for_permitted_member + */ + #[Test] + public function member_with_read_permission_sees_restricted_board_in_directory(): void + { + $member = $this->memberWithReadOn($this->hiddenBoard->slug); + + $response = $this->actingAs($member)->getJson('/api/modules/sirsoft-board/boards?limit=3'); + $response->assertStatus(200); + + $slugs = array_column($response->json('data'), 'slug'); + $this->assertContains($this->hiddenBoard->slug, $slugs, '열람 권한을 가진 회원에게는 해당 게시판이 디렉토리에 보여야 한다.'); + + $raw = json_encode($response->json('data'), JSON_UNESCAPED_UNICODE); + $this->assertStringContainsString('숨김게시판글', $raw, '열람 권한자는 그 게시판의 최근글 제목도 볼 수 있어야 한다.'); + } + + // ========== 헬퍼 ========== + + /** + * @param TestResponse $response + * @return array<int, string> 응답 데이터의 title 목록 + */ + private function titles($response): array + { + $response->assertStatus(200); + + return array_column($response->json('data'), 'title'); + } + + private function insertPost(int $boardId, string $title, int $viewCount, bool $isSecret, string $status): void + { + DB::table('board_posts')->insert([ + 'board_id' => $boardId, + 'title' => $title, + 'content' => '내용', + 'author_name' => '작성자', + 'view_count' => $viewCount, + 'is_secret' => $isSecret, + 'status' => $status, + 'ip_address' => '127.0.0.1', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + private function grantRead(string $roleIdentifier, string $slug): void + { + $role = Role::where('identifier', $roleIdentifier)->first(); + if (! $role) { + return; + } + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $role->permissions()->syncWithoutDetaching([$perm->id]); + } + + private function memberWithReadOn(string $slug): User + { + $member = User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + $member->roles()->attach($userRole->id); + $this->grantRead('user', $slug); + PermissionMiddleware::clearGuestRoleCache(); + + return $member; + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Comment/CommentVerifyTokenTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Comment/CommentVerifyTokenTest.php new file mode 100644 index 00000000..6d586d90 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Comment/CommentVerifyTokenTest.php @@ -0,0 +1,233 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Comment; + +// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Hash; +use Modules\Sirsoft\Board\Tests\BoardTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 비회원 댓글 검증 토큰(verification_token) 테스트 (⑮) + * + * verify-password 로 발급된 토큰이 실제로 수정/삭제 요청에서 소비되어 평문 비밀번호 + * 재전송을 대체하는지 검증한다. (게시글 verification_token 과 동형) + * - verify 로 받은 토큰으로 수정/삭제 성공 + * - 잘못된 토큰 / 만료 토큰 거부 + * - 단일 사용(1회 소비 후 재사용 거부) + */ +class CommentVerifyTokenTest extends BoardTestCase +{ + protected function getTestBoardSlug(): string + { + return 'comment-verify-token'; + } + + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '댓글 검증 토큰 테스트', 'en' => 'Comment Verify Token'], + 'is_active' => true, + 'use_comment' => true, + 'comment_order' => 'DESC', + 'min_comment_length' => 2, + 'max_comment_length' => 1000, + 'secret_mode' => 'disabled', + 'blocked_keywords' => [], + ]; + } + + private int $postId; + + private int $commentId; + + protected function setUp(): void + { + parent::setUp(); + + // 비회원이 verify/수정/삭제에 접근하려면 comments.write + comments.read 권한 필요 + $this->setGuestPermissions(['posts.read', 'comments.read', 'comments.write']); + + $this->postId = $this->createTestPost([ + 'title' => '댓글 대상 게시글', + 'status' => 'published', + ]); + + // 비회원(guest) 댓글 — 비밀번호 보호 + $this->commentId = $this->createTestComment($this->postId, [ + 'user_id' => null, + 'author_name' => '비회원', + 'content' => '원본 댓글 내용', + 'password' => Hash::make('pw1234'), + ]); + + $this->resetPermissionMiddlewareCache(); + } + + /** + * 올바른 비밀번호 확인 시 verification_token 이 발급된다. + * + * @return string 발급된 토큰 + */ + private function issueToken(): string + { + $response = $this->postJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/comments/{$this->commentId}/verify-password", + ['password' => 'pw1234'] + ); + + $response->assertStatus(200) + ->assertJsonPath('data.verified', true); + + $token = $response->json('data.verification_token'); + $this->assertNotEmpty($token, 'verify-password 는 verification_token 을 발급해야 한다.'); + + return $token; + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function verify_password_issues_token(): void + { + $token = $this->issueToken(); + + $this->assertIsString($token); + $this->assertMatchesRegularExpression('/^[A-Za-z0-9]{32}$/', $token); + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function update_succeeds_with_valid_token(): void + { + $token = $this->issueToken(); + + $response = $this->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['content' => '토큰으로 수정한 댓글', 'verification_token' => $token] + ); + + $response->assertStatus(200); + + $this->assertSame( + '토큰으로 수정한 댓글', + DB::table('board_comments')->where('id', $this->commentId)->value('content') + ); + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function delete_succeeds_with_valid_token(): void + { + $token = $this->issueToken(); + + $response = $this->deleteJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['verification_token' => $token] + ); + + $response->assertStatus(200); + + $this->assertNotNull( + DB::table('board_comments')->where('id', $this->commentId)->value('deleted_at'), + '토큰 삭제 후 댓글은 소프트 삭제되어야 한다.' + ); + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function update_is_rejected_with_wrong_token(): void + { + $this->issueToken(); // 실제 토큰은 발급하되, 요청에는 다른 값을 보낸다 + + $response = $this->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['content' => '잘못된 토큰 수정 시도', 'verification_token' => str_repeat('x', 32)] + ); + + $response->assertStatus(403); + + $this->assertSame( + '원본 댓글 내용', + DB::table('board_comments')->where('id', $this->commentId)->value('content'), + '잘못된 토큰이면 내용이 변경되면 안 된다.' + ); + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function token_is_single_use(): void + { + $token = $this->issueToken(); + + // 1회차: 성공 + $this->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['content' => '첫 수정', 'verification_token' => $token] + )->assertStatus(200); + + // 2회차: 같은 토큰 재사용 → 소비되어 거부 + $this->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['content' => '두 번째 수정', 'verification_token' => $token] + )->assertStatus(403); + + $this->assertSame( + '첫 수정', + DB::table('board_comments')->where('id', $this->commentId)->value('content'), + '소비된 토큰의 재사용은 반영되면 안 된다.' + ); + } + + /** + * @scenario case=comment_token_lifecycle + * + * @effects comment_verify_token_single_use_lifecycle + */ + #[Test] + public function expired_token_is_rejected(): void + { + $token = $this->issueToken(); + + // 토큰 TTL(기본 1시간)을 넘겨 이동 → 캐시에서 만료 + $this->travel(2)->hours(); + + try { + $this->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$this->postId}/comments/{$this->commentId}", + ['content' => '만료 토큰 수정 시도', 'verification_token' => $token] + )->assertStatus(403); + } finally { + $this->travelBack(); + } + + $this->assertSame( + '원본 댓글 내용', + DB::table('board_comments')->where('id', $this->commentId)->value('content'), + '만료 토큰이면 내용이 변경되면 안 된다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Console/PruneAttachmentsCommandTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Console/PruneAttachmentsCommandTest.php new file mode 100644 index 00000000..6061bd81 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Console/PruneAttachmentsCommandTest.php @@ -0,0 +1,339 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Console; + +use App\Extension\Storage\ModuleStorageDriver; +use App\Services\ModuleSettingsService; +use Mockery; +use Modules\Sirsoft\Board\Models\Attachment; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 게시판 첨부 정리 커맨드 테스트 + * + * 임시 첨부 정리는 상시, 소프트 삭제 첨부 영구 정리는 운영자 옵트인입니다. + * 두 파트의 게이트가 서로 섞이지 않는지를 회귀로 고정합니다. + */ +class PruneAttachmentsCommandTest extends ModuleTestCase +{ + /** + * 모듈 설정 조회를 지정 값으로 고정합니다. + * + * @param array<string, mixed> $values 설정 키 => 값 + */ + private function fakeModuleSettings(array $values): void + { + $service = Mockery::mock(ModuleSettingsService::class); + + $service->shouldReceive('get') + ->andReturnUsing(function (string $identifier, ?string $key = null, mixed $default = null) use ($values) { + return array_key_exists($key, $values) ? $values[$key] : $default; + }); + + $this->app->instance(ModuleSettingsService::class, $service); + } + + /** + * 첨부 파일과 기록을 함께 만듭니다. + * + * @param array<string, mixed> $attributes 덮어쓸 속성 + * @return Attachment 생성된 첨부 + */ + private function makeAttachment(array $attributes = []): Attachment + { + $filename = uniqid('board-att-').'.png'; + $path = $attributes['path'] ?? "free/temp/tempkey/{$filename}"; + + (new ModuleStorageDriver('sirsoft-board', 'modules'))->put('attachments', $path, 'bytes'); + + return Attachment::create(array_merge([ + 'board_id' => 0, + 'post_id' => null, + 'temp_key' => 'tempkey', + 'original_filename' => $filename, + 'stored_filename' => $filename, + 'disk' => 'modules', + 'path' => $path, + 'mime_type' => 'image/png', + 'size' => 5, + 'collection' => 'attachments', + 'order' => 1, + ], $attributes)); + } + + /** + * @scenario age=past_retention, attachment_state=temp_unlinked, purge_toggle=off + * + * @effects board_temp_prune_deletes_file_and_record + */ + #[Test] + public function stale_temp_attachments_lose_both_file_and_record(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments')->assertSuccessful(); + + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-board', 'modules'))->exists('attachments', $attachment->path) + ); + $this->assertDatabaseMissing('board_attachments', ['id' => $attachment->id]); + } + + /** + * @scenario age=within_retention, attachment_state=temp_unlinked, purge_toggle=off + * + * @effects board_temp_prune_keeps_within_retention + */ + #[Test] + public function temp_attachment_within_retention_is_kept(): void + { + $attachment = $this->makeAttachment(); + + $this->artisan('sirsoft-board:prune-attachments')->assertSuccessful(); + + $this->assertDatabaseHas('board_attachments', ['id' => $attachment->id]); + } + + /** + * 게시글에 연결된(temp_key 가 비워진) 첨부는 임시 정리 대상이 아니다. + * + * @scenario age=past_retention, attachment_state=linked, purge_toggle=off + * + * @effects board_temp_prune_skips_linked + */ + #[Test] + public function linked_attachment_is_never_treated_as_temporary(): void + { + $attachment = $this->makeAttachment([ + 'board_id' => 1, + 'post_id' => 1, + 'temp_key' => null, + 'path' => 'free/2026/08/14/'.uniqid('linked-').'.png', + ]); + $attachment->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments')->assertSuccessful(); + + $this->assertDatabaseHas('board_attachments', ['id' => $attachment->id]); + } + + /** + * @scenario age=past_retention, attachment_state=soft_deleted, purge_toggle=off + * + * @effects board_purge_gate_off_by_default + */ + #[Test] + public function scheduled_run_does_not_purge_soft_deleted_when_the_toggle_is_off(): void + { + $this->fakeModuleSettings(['attachment_settings.purge_enabled' => false]); + + $attachment = $this->makeAttachment([ + 'board_id' => 1, + 'post_id' => 1, + 'temp_key' => null, + 'path' => 'free/2026/08/14/'.uniqid('soft-').'.png', + ]); + $attachment->delete(); + $attachment->forceFill(['deleted_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments --scheduled')->assertSuccessful(); + + $this->assertNotNull(Attachment::withTrashed()->find($attachment->id)); + } + + /** + * @scenario age=past_retention, attachment_state=soft_deleted, purge_toggle=on + * + * @effects board_purge_gate_on + */ + #[Test] + public function scheduled_run_purges_soft_deleted_when_the_toggle_is_on(): void + { + $this->fakeModuleSettings([ + 'attachment_settings.purge_enabled' => true, + 'attachment_settings.purge_retention_days' => 30, + ]); + + $attachment = $this->makeAttachment([ + 'board_id' => 1, + 'post_id' => 1, + 'temp_key' => null, + 'path' => 'free/2026/08/14/'.uniqid('purge-').'.png', + ]); + $path = $attachment->path; + $attachment->delete(); + $attachment->forceFill(['deleted_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments --scheduled')->assertSuccessful(); + + $this->assertNull(Attachment::withTrashed()->find($attachment->id)); + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-board', 'modules'))->exists('attachments', $path) + ); + } + + /** + * 보존기간 안의 소프트 삭제 첨부는 복원 가능해야 하므로 파기하지 않는다. + * + * @scenario age=within_retention, attachment_state=soft_deleted, purge_toggle=on + * + * @effects board_purge_keeps_restorable_window + */ + #[Test] + public function soft_deleted_attachment_within_retention_stays_restorable(): void + { + $this->fakeModuleSettings([ + 'attachment_settings.purge_enabled' => true, + 'attachment_settings.purge_retention_days' => 30, + ]); + + $attachment = $this->makeAttachment([ + 'board_id' => 1, + 'post_id' => 1, + 'temp_key' => null, + 'path' => 'free/2026/08/14/'.uniqid('recent-').'.png', + ]); + $attachment->delete(); + + $this->artisan('sirsoft-board:prune-attachments --scheduled')->assertSuccessful(); + + $trashed = Attachment::withTrashed()->find($attachment->id); + $this->assertNotNull($trashed); + $this->assertTrue( + (new ModuleStorageDriver('sirsoft-board', 'modules'))->exists('attachments', $attachment->path), + '복원 가능 기간 안에는 파일도 남아 있어야 한다.' + ); + } + + /** + * @effects board_prune_dry_run_no_delete + */ + #[Test] + public function dry_run_reports_targets_without_deleting(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments --dry-run') + ->expectsOutputToContain('[DRY RUN]') + ->assertSuccessful(); + + $this->assertDatabaseHas('board_attachments', ['id' => $attachment->id]); + } + + /** + * @effects board_temp_prune_days_guard + */ + #[Test] + public function temp_retention_below_one_day_performs_no_cleanup(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments --temp-days=0 --purge-days=0') + ->assertSuccessful(); + + $this->assertDatabaseHas('board_attachments', ['id' => $attachment->id]); + } + + /** + * 파일을 비운 temp_key 디렉토리는 남기지 않는다 (회귀) + * + * 파일만 지우고 디렉토리를 남기면 폼 세션마다 빈 디렉토리가 쌓여, 정리를 돌려도 + * 저장소 흔적은 계속 늘어난다. + * + * @scenario age=past_retention, attachment_state=temp_unlinked, purge_toggle=off + * + * @effects board_temp_prune_removes_empty_directory + */ + #[Test] + public function emptied_temp_directory_is_removed(): void + { + $attachment = $this->makeAttachment(['path' => 'free/temp/tempkey-empty-dir/'.uniqid('t-').'.png']); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-board:prune-attachments')->assertSuccessful(); + + $storage = new ModuleStorageDriver('sirsoft-board', 'modules'); + + $this->assertSame( + [], + $storage->files('attachments', 'free/temp/tempkey-empty-dir'), + '디렉토리 안의 파일은 모두 삭제되어야 한다.' + ); + $this->assertDirectoryDoesNotExist( + rtrim($storage->getBasePath('attachments'), '/\\').'/free/temp/tempkey-empty-dir', + '비워진 temp 디렉토리는 남기지 않는다.' + ); + } + + /** + * 같은 temp_key 에 처리되지 않은 파일이 남아 있으면 디렉토리를 지우지 않는다. + * + * @scenario age=past_retention, attachment_state=temp_unlinked, purge_toggle=off + * + * @effects board_temp_prune_keeps_directory_with_remaining_files + */ + #[Test] + public function temp_directory_with_remaining_files_is_kept(): void + { + $directory = 'free/temp/tempkey-partial'; + + $processed = $this->makeAttachment(['path' => $directory.'/'.uniqid('done-').'.png']); + $processed->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + // 같은 디렉토리에 이번 회차 대상이 아닌 파일이 남아 있는 상태 + $storage = new ModuleStorageDriver('sirsoft-board', 'modules'); + $storage->put('attachments', $directory.'/keep.png', 'bytes'); + + $this->artisan('sirsoft-board:prune-attachments')->assertSuccessful(); + + $this->assertTrue($storage->exists('attachments', $directory.'/keep.png')); + } + + /** + * 대상이 한 회차 상한을 넘어도 누락 없이 전수 처리된다 (경계). + * + * 순회는 `--limit` 으로 잘린 단일 윈도우를 훑는다. 처리하며 행을 지우므로, 남은 대상이 + * 다음 회차에서 반드시 잡혀야 적체가 수렴한다 — 커서가 밀려 영영 건너뛰는 행이 있으면 + * 정리를 매일 돌려도 잔존물이 남는다. + * + * @scenario age=past_retention, attachment_state=temp_unlinked, purge_toggle=off + * + * @effects board_temp_prune_processes_all_across_runs + */ + #[Test] + public function targets_beyond_the_limit_are_processed_on_the_next_run(): void + { + $attachments = []; + + for ($i = 0; $i < 3; $i++) { + $attachment = $this->makeAttachment([ + 'path' => "free/temp/tempkey-batch-{$i}/".uniqid('b-').'.png', + ]); + $attachment->forceFill(['created_at' => now()->subDays(5 + $i)])->saveQuietly(); + $attachments[] = $attachment; + } + + // 1회차: 상한 2건까지만 처리된다 + $this->artisan('sirsoft-board:prune-attachments --limit=2')->assertSuccessful(); + + $remaining = Attachment::withTrashed() + ->whereIn('id', array_column($attachments, 'id')) + ->count(); + + $this->assertSame(1, $remaining, '한 회차는 --limit 건까지만 처리한다.'); + + // 2회차: 남은 1건이 반드시 잡힌다 (커서 밀림으로 건너뛰지 않는다) + $this->artisan('sirsoft-board:prune-attachments --limit=2')->assertSuccessful(); + + foreach ($attachments as $attachment) { + $this->assertDatabaseMissing('board_attachments', ['id' => $attachment->id]); + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-board', 'modules'))->exists('attachments', $attachment->path) + ); + } + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/DashboardPermissionTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/DashboardPermissionTest.php new file mode 100644 index 00000000..c1057dfe --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/DashboardPermissionTest.php @@ -0,0 +1,164 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Dashboard; + +// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Enums\PermissionType; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Modules\Sirsoft\Board\Enums\ReportStatus; +use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Models\Report; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 대시보드 permission 가드 + 미처리 신고 스코프 테스트 (⑱-1 + N-5) + * + * - ⑱-1: 대시보드 4종(overview/post-graph/recent-posts/pending-reports)은 + * permission:admin,sirsoft-board.dashboard.view 가드를 요구한다. + * - N-5: 미처리 신고 집계는 비활성 게시판을 제외한다(board-active 스코프). + */ +class DashboardPermissionTest extends ModuleTestCase +{ + private const BASE = '/api/modules/sirsoft-board/admin/dashboard'; + + private const ENDPOINTS = ['overview', 'post-graph', 'recent-posts', 'pending-reports']; + + private User $adminWithout; + + private User $adminWith; + + protected function setUp(): void + { + parent::setUp(); + + // adminWithout: admin 이지만 dashboard.view 없음. + // createAdminUser 는 공유 'admin' 역할에 권한을 붙이므로, 여기서 쓰면 adminWith 가 + // 그 역할에 dashboard.view 를 추가한 순간 adminWithout 도 갖게 된다(권한 격리 불가). + // 따라서 dashboard.view 가 없는 격리 역할을 직접 만든다. + $this->adminWithout = $this->createIsolatedAdminWithoutDashboardView(); + // adminWith: dashboard.view 보유 (공유 admin 역할 사용 — 프로덕션 기본 상태와 동일) + $this->adminWith = $this->createAdminUser(['sirsoft-board.boards.read', 'sirsoft-board.dashboard.view']); + } + + /** + * dashboard.view 가 없는 권한-격리 관리자(고유 역할)를 생성합니다. + * + * admin 가드(isAdmin) 통과를 위해 admin.access 를, 무해한 admin 권한으로 + * boards.read 를 부여하되 dashboard.view 는 부여하지 않습니다. + * + * @return User 대시보드 권한이 없는 관리자 + */ + private function createIsolatedAdminWithoutDashboardView(): User + { + $user = User::factory()->create(); + + $role = Role::create([ + 'identifier' => 'board-dash-guard-'.$user->id, + 'name' => ['ko' => '대시보드 미보유 관리자', 'en' => 'Admin without dashboard'], + ]); + $user->roles()->attach($role->id); + + foreach (['admin.access', 'sirsoft-board.boards.read'] as $identifier) { + $permission = Permission::firstOrCreate( + ['identifier' => $identifier], + ['name' => ['ko' => $identifier, 'en' => $identifier], 'type' => PermissionType::Admin], + ); + $role->permissions()->syncWithoutDetaching([$permission->id]); + } + + return $user; + } + + // ========== ⑱-1 permission 가드 ========== + + /** + * @scenario case=dashboard_permission_gate + * + * @effects dashboard_requires_view_permission + */ + #[Test] + public function admin_without_dashboard_view_is_forbidden_on_all_endpoints(): void + { + foreach (self::ENDPOINTS as $endpoint) { + $this->actingAs($this->adminWithout) + ->getJson(self::BASE.'/'.$endpoint) + ->assertStatus(403); + } + } + + /** + * @scenario case=dashboard_permission_gate + * + * @effects dashboard_requires_view_permission + */ + #[Test] + public function admin_with_dashboard_view_can_access_all_endpoints(): void + { + foreach (self::ENDPOINTS as $endpoint) { + $this->actingAs($this->adminWith) + ->getJson(self::BASE.'/'.$endpoint) + ->assertStatus(200); + } + } + + /** + * @scenario case=dashboard_permission_gate + * + * @effects dashboard_requires_view_permission + */ + #[Test] + public function unauthenticated_request_is_rejected(): void + { + foreach (self::ENDPOINTS as $endpoint) { + $this->getJson(self::BASE.'/'.$endpoint)->assertStatus(401); + } + } + + // ========== N-5 미처리 신고 board-active 스코프 ========== + + /** + * @scenario case=pending_reports_scope + * + * @effects pending_reports_scoped_to_active_boards + */ + #[Test] + public function pending_reports_excludes_inactive_board_reports(): void + { + $activeBoard = Board::factory()->create(['is_active' => true]); + $inactiveBoard = Board::factory()->create(['is_active' => false]); + + $this->createPendingReport($activeBoard); + $this->createPendingReport($inactiveBoard); + + $response = $this->actingAs($this->adminWith) + ->getJson(self::BASE.'/pending-reports'); + + $response->assertStatus(200); + + // 비활성 게시판 신고는 제외되어 총 1건, 목록도 1건이어야 한다. + $this->assertSame(1, $response->json('data.total'), '미처리 신고 총계는 활성 게시판 신고만 세야 한다.'); + $this->assertCount(1, $response->json('data.items'), '미처리 신고 목록은 활성 게시판 신고만 담아야 한다.'); + } + + /** + * 지정 게시판에 미처리(pending) 신고 케이스를 생성합니다. + * + * @param Board $board 대상 게시판 + * @return Report 생성된 신고 + */ + private function createPendingReport(Board $board): Report + { + return Report::create([ + 'board_id' => $board->id, + 'target_type' => 'post', + 'target_id' => 1, + 'status' => ReportStatus::Pending, + 'last_reported_at' => now(), + ]); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/RecentAcrossBoardsVisibilityTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/RecentAcrossBoardsVisibilityTest.php new file mode 100644 index 00000000..b296edfb --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Dashboard/RecentAcrossBoardsVisibilityTest.php @@ -0,0 +1,81 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Dashboard; + +// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\User; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Board\Enums\PostStatus; +use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 대시보드 최신글(getRecentAcrossBoards)의 노출 제한 필터 테스트 (⑱-2) + * + * 관리자 대시보드 최신글은 블라인드·삭제 글과 비활성 게시판 글을 제외한다. + * 비밀글은 제목 공개 정책(2026-01-02: 관리자는 제목을 본다)에 따라 노출한다(제외하지 않음). + * (status=published + board.is_active=true 컨벤션 — 비밀글 필터는 적용하지 않음) + */ +class RecentAcrossBoardsVisibilityTest extends ModuleTestCase +{ + private const ENDPOINT = '/api/modules/sirsoft-board/admin/dashboard/recent-posts'; + + private User $admin; + + protected function setUp(): void + { + parent::setUp(); + + $this->admin = $this->createAdminUser(['sirsoft-board.dashboard.view']); + } + + /** + * @scenario case=dashboard_recent_visibility + * + * @effects dashboard_summary_excludes_hidden_posts + */ + #[Test] + public function recent_posts_shows_secret_but_excludes_blinded_and_inactive_board_posts(): void + { + $activeBoard = Board::factory()->create(['is_active' => true]); + $inactiveBoard = Board::factory()->create(['is_active' => false]); + + $this->insertPost($activeBoard->id, '공개 발행글', ['is_secret' => false, 'status' => PostStatus::Published->value]); + $this->insertPost($activeBoard->id, '비밀글', ['is_secret' => true, 'status' => PostStatus::Published->value]); + $this->insertPost($activeBoard->id, '블라인드글', ['is_secret' => false, 'status' => PostStatus::Blinded->value]); + $this->insertPost($inactiveBoard->id, '비활성게시판글', ['is_secret' => false, 'status' => PostStatus::Published->value]); + + $response = $this->actingAs($this->admin)->getJson(self::ENDPOINT.'?limit=20'); + $response->assertStatus(200); + + $titles = array_column($response->json('data'), 'title'); + + $this->assertContains('공개 발행글', $titles, '공개 발행글은 최신글에 노출되어야 한다.'); + $this->assertContains('비밀글', $titles, '비밀글은 제목 공개 정책에 따라 관리자 대시보드에 노출되어야 한다.'); + $this->assertNotContains('블라인드글', $titles, '블라인드글은 최신글에서 제외되어야 한다.'); + $this->assertNotContains('비활성게시판글', $titles, '비활성 게시판 글은 최신글에서 제외되어야 한다.'); + } + + /** + * 게시글을 직접 삽입합니다. + * + * @param int $boardId 게시판 ID + * @param string $title 제목 + * @param array<string, mixed> $attributes 추가 속성 + */ + private function insertPost(int $boardId, string $title, array $attributes = []): void + { + DB::table('board_posts')->insert(array_merge([ + 'board_id' => $boardId, + 'title' => $title, + 'content' => '내용', + 'author_name' => '작성자', + 'ip_address' => '127.0.0.1', + 'created_at' => now(), + 'updated_at' => now(), + ], $attributes)); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/FormRequestValidationTest.php b/modules/_bundled/sirsoft-board/tests/Feature/FormRequestValidationTest.php index 39500b8d..39fafa33 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/FormRequestValidationTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/FormRequestValidationTest.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Board\Tests\Feature; require_once __DIR__.'/../ModuleTestCase.php'; +use Illuminate\Support\Facades\Hash; use Modules\Sirsoft\Board\Tests\BoardTestCase; /** @@ -32,6 +33,16 @@ use Modules\Sirsoft\Board\Tests\BoardTestCase; * * UpdatePostRequest: * - title 최소 길이 미만 → 422 + * - password 배열 주입 → 422 + * + * DestroyCommentRequest: + * - password 배열 주입 → 422 + * - password 최소 길이(4) 미만 → 422 + * - 회원 본인 댓글 password 없이 삭제 → 200 + * + * DestroyPostRequest: + * - verification_token 배열 주입 → 422 + * - 비회원 글 올바른 password 로 삭제 → 200 * * @group board * @group feature @@ -300,4 +311,111 @@ class FormRequestValidationTest extends BoardTestCase ['title' => '가'] // min_title_length=2 미만 )->assertStatus(422); } + + /** + * 게시글 수정에 password 배열 주입 → 422 (string 규칙 차단) + */ + public function test_post_update_rejects_array_password(): void + { + $user = $this->createUser(); + $postId = $this->createTestPost(['user_id' => $user->id]); + + $this->actingAs($user)->putJson( + "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}", + [ + 'title' => '정상 제목', + 'password' => ['x'], // 배열 주입 + ] + )->assertStatus(422); + } + + // ========================================== + // DestroyCommentRequest + // ========================================== + + /** + * 댓글 삭제에 password 배열 주입 → 422 (string 규칙 차단) + */ + public function test_comment_destroy_rejects_array_password(): void + { + $postId = $this->createTestPost(); + $commentId = $this->createTestComment($postId, [ + 'password' => Hash::make('pass1234'), + ]); + + $this->deleteJson( + $this->commentUrl($postId)."/{$commentId}", + ['password' => ['x']] // 배열 주입 + )->assertStatus(422); + } + + /** + * 댓글 삭제에 최소 길이(4) 미만 password → 422 + */ + public function test_comment_destroy_rejects_too_short_password(): void + { + $postId = $this->createTestPost(); + $commentId = $this->createTestComment($postId, [ + 'password' => Hash::make('pass1234'), + ]); + + $this->deleteJson( + $this->commentUrl($postId)."/{$commentId}", + ['password' => '123'] // min:4 미만 + )->assertStatus(422); + } + + /** + * 회원 본인 댓글은 password 없이 삭제 가능 → 200 + * (password 는 nullable — 비회원 소유권 확인용으로만 요구) + */ + public function test_member_comment_destroy_without_password_succeeds(): void + { + $user = $this->createUser(); + $postId = $this->createTestPost(); + $commentId = $this->createTestComment($postId, [ + 'user_id' => $user->id, + 'author_name' => $user->name, + ]); + + $this->actingAs($user) + ->deleteJson($this->commentUrl($postId)."/{$commentId}") + ->assertStatus(200); + } + + // ========================================== + // DestroyPostRequest + // ========================================== + + /** + * 게시글 삭제에 verification_token 배열 주입 → 422 (string 규칙 차단) + */ + public function test_post_destroy_rejects_array_verification_token(): void + { + $postId = $this->createTestPost([ + 'user_id' => null, + 'password' => Hash::make('pass1234'), + ]); + + $this->deleteJson( + $this->postUrl()."/{$postId}", + ['verification_token' => ['x']] // 배열 주입 + )->assertStatus(422); + } + + /** + * 비회원 글은 올바른 password 로 삭제 가능 → 200 + */ + public function test_guest_post_destroy_with_valid_password_succeeds(): void + { + $postId = $this->createTestPost([ + 'user_id' => null, + 'password' => Hash::make('pass1234'), + ]); + + $this->deleteJson( + $this->postUrl()."/{$postId}", + ['password' => 'pass1234'] + )->assertStatus(200); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Feature/ReportNotificationTest.php b/modules/_bundled/sirsoft-board/tests/Feature/ReportNotificationTest.php index 8c6d580b..a8158f4f 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/ReportNotificationTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/ReportNotificationTest.php @@ -5,14 +5,19 @@ namespace Modules\Sirsoft\Board\Tests\Feature; // ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) require_once __DIR__.'/../ModuleTestCase.php'; +use App\Extension\Helpers\NotificationSyncHelper; +use App\Extension\ModuleManager; +use App\Listeners\NotificationHookListener; +use App\Models\Permission; +use App\Models\Role; use App\Models\User; +use App\Notifications\GenericNotification; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Notification; use Modules\Sirsoft\Board\Enums\ReportStatus; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Models\Report; -use Modules\Sirsoft\Board\Models\ReportLog; -use App\Notifications\GenericNotification; +use Modules\Sirsoft\Board\Services\BoardSettingsService; use Modules\Sirsoft\Board\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -47,7 +52,7 @@ class ReportNotificationTest extends ModuleTestCase // NotificationHookListener 동적 훅 등록 (테스트 환경에서는 부트 시점에 실행되지 않음 — // 알림 정의 기반 훅 구독을 수동으로 활성화하여 actual notification flow 복원) $this->syncBoardNotificationDefinitions(); - app(\App\Listeners\NotificationHookListener::class)->registerDynamicHooks(); + app(NotificationHookListener::class)->registerDynamicHooks(); // 테스트 잔여 데이터 정리 DB::table('boards_reports')->delete(); @@ -191,7 +196,7 @@ class ReportNotificationTest extends ModuleTestCase * 신고 처리 정책 OFF → 알림 미발송 */ #[Test] - public function test_게시글_신고_처리시_정책OFF_알림_미발송(): void + public function test_게시글_신고_처리시_정책_off_알림_미발송(): void { // Given: 신고 처리 알림 정책 OFF $this->setReportPolicy(false); @@ -254,7 +259,7 @@ class ReportNotificationTest extends ModuleTestCase * 댓글 신고 처리 정책 OFF → 댓글 작성자에게 알림 미발송 */ #[Test] - public function test_댓글_신고_처리시_정책OFF_알림_미발송(): void + public function test_댓글_신고_처리시_정책_off_알림_미발송(): void { // Given: 신고 처리 알림 정책 OFF $this->setReportPolicy(false); @@ -286,7 +291,7 @@ class ReportNotificationTest extends ModuleTestCase * → 관리자에게 ReportReceivedAdminNotification 발송 (postTitle, reasonType 내용 포함) */ #[Test] - public function test_신고_접수시_정책ON_권한자에게_관리자알림_발송(): void + public function test_신고_접수시_정책_on_권한자에게_관리자알림_발송(): void { // Given: 관리자 알림 정책 ON $this->setAdminReportPolicy(true); @@ -322,7 +327,7 @@ class ReportNotificationTest extends ModuleTestCase * 신고 접수 관리자 알림 정책 OFF → 관리자 알림 미발송 */ #[Test] - public function test_신고_접수시_정책OFF_관리자알림_미발송(): void + public function test_신고_접수시_정책_off_관리자알림_미발송(): void { // Given: 관리자 알림 정책 OFF $this->setAdminReportPolicy(false); @@ -345,14 +350,14 @@ class ReportNotificationTest extends ModuleTestCase * 신고 접수 관리자 알림 정책 ON + reports.manage 권한자 없음 → 알림 미발송 */ #[Test] - public function test_신고_접수시_정책ON_권한자없음_알림_미발송(): void + public function test_신고_접수시_정책_on_권한자없음_알림_미발송(): void { // Given: 관리자 알림 정책 ON + admin 역할에서 권한 제거 (권한자 0명 상태 시뮬레이션) $this->setAdminReportPolicy(true); // admin 역할에서 reports.manage 권한 제거 - $permission = \App\Models\Permission::where('identifier', 'sirsoft-board.reports.manage')->first(); - $adminRole = \App\Models\Role::where('identifier', 'admin')->first(); + $permission = Permission::where('identifier', 'sirsoft-board.reports.manage')->first(); + $adminRole = Role::where('identifier', 'admin')->first(); if ($permission && $adminRole) { $adminRole->permissions()->detach($permission->id); } @@ -508,7 +513,7 @@ class ReportNotificationTest extends ModuleTestCase /** * report_policy.notify_author_on_report_action 설정을 주입합니다. * - * @param bool $enabled 알림 발송 여부 + * @param bool $enabled 알림 발송 여부 * @return void */ /** @@ -535,12 +540,22 @@ class ReportNotificationTest extends ModuleTestCase /** * report_policy.notify_admin_on_report 설정을 주입합니다. * - * @param bool $enabled 알림 발송 여부 - * @param string $scope 발송 범위 ('per_case' | 'per_report') + * @param bool $enabled 알림 발송 여부 + * @param string $scope 발송 범위 ('per_case' | 'per_report') * @return void */ private function setAdminReportPolicy(bool $enabled, string $scope = 'per_case'): void { + // checkAndApplyAutoHide()는 BoardSettingsService(파일)에서 읽으므로 + // auto_hide_threshold를 충분히 높게 설정하여 테스트 중 자동 블라인드 방지 + // + // 순서 주의: 설정 저장은 config 미러를 저장본 기준으로 다시 채운다. 그래서 아래 + // config() 주입을 먼저 하면 이 호출이 그 주입을 덮어써 정책이 기본값(활성)으로 돌아간다. + /** @var BoardSettingsService $settingsService */ + $settingsService = app(BoardSettingsService::class); + $settingsService->setSetting('report_policy.auto_hide_threshold', 100); + $settingsService->clearCache(); + // g7_module_settings()는 Config에서 읽으므로 config 주입으로 알림 정책 제어 config([ 'g7_settings.modules.sirsoft-board.report_policy' => [ @@ -549,22 +564,14 @@ class ReportNotificationTest extends ModuleTestCase 'notify_admin_on_report_channels' => ['mail'], ], ]); - - // checkAndApplyAutoHide()는 BoardSettingsService(파일)에서 읽으므로 - // auto_hide_threshold를 충분히 높게 설정하여 테스트 중 자동 블라인드 방지 - /** @var \Modules\Sirsoft\Board\Services\BoardSettingsService $settingsService */ - $settingsService = app(\Modules\Sirsoft\Board\Services\BoardSettingsService::class); - $settingsService->setSetting('report_policy.auto_hide_threshold', 100); - $settingsService->clearCache(); } - /** * 테스트용 게시글 생성 헬퍼 * - * @param int $userId 작성자 ID - * @param string $status 게시글 상태 - * @param string $triggerType 처리 유형 + * @param int $userId 작성자 ID + * @param string $status 게시글 상태 + * @param string $triggerType 처리 유형 * @return int 생성된 게시글 ID */ private function createTestPost(int $userId, string $status = 'published', string $triggerType = 'admin'): int @@ -589,8 +596,8 @@ class ReportNotificationTest extends ModuleTestCase /** * 테스트용 댓글 생성 헬퍼 * - * @param int $postId 게시글 ID - * @param int $userId 작성자 ID + * @param int $postId 게시글 ID + * @param int $userId 작성자 ID * @return int 생성된 댓글 ID */ private function createTestComment(int $postId, int $userId): int @@ -619,12 +626,12 @@ class ReportNotificationTest extends ModuleTestCase */ private function syncBoardNotificationDefinitions(): void { - $module = app(\App\Extension\ModuleManager::class)->getModule('sirsoft-board'); + $module = app(ModuleManager::class)->getModule('sirsoft-board'); if (! $module) { return; } - $helper = app(\App\Extension\Helpers\NotificationSyncHelper::class); + $helper = app(NotificationSyncHelper::class); foreach ($module->getNotificationDefinitions() as $data) { $data['extension_type'] = 'module'; $data['extension_identifier'] = 'sirsoft-board'; diff --git a/modules/_bundled/sirsoft-board/tests/Feature/Search/AvailableBoardsPermissionTest.php b/modules/_bundled/sirsoft-board/tests/Feature/Search/AvailableBoardsPermissionTest.php new file mode 100644 index 00000000..89792f46 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/Search/AvailableBoardsPermissionTest.php @@ -0,0 +1,186 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\Search; + +// ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Support\Facades\Cache; +use Modules\Sirsoft\Board\Models\Board; +use Modules\Sirsoft\Board\Services\BoardService; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 검색 필터/인기 게시판 목록의 게시판별 읽기 권한 필터 테스트 (⑦ + N-2) + * + * - ⑦: 통합 검색의 available_boards(필터 드롭다운)는 읽기 권한(posts.read)을 통과한 + * 게시판만 담아야 한다 — getActiveBoardsListForFilter($user) 가 검색 결과 필터와 + * 동일한 게이트를 목록 구성에도 적용한다. + * - N-2: 공개 인기 게시판 목록(GET /boards/popular-boards)도 호출자 읽기 권한을 + * 통과한 게시판만 노출한다. + */ +class AvailableBoardsPermissionTest extends ModuleTestCase +{ + private Board $readableBoard; + + private Board $hiddenBoard; + + protected function setUp(): void + { + parent::setUp(); + + // 다른 테스트가 남긴 활성 게시판 잡음 제거 + 캐시 초기화 + Board::where('is_active', true)->update(['is_active' => false]); + Cache::flush(); + + $this->readableBoard = Board::factory()->create([ + 'is_active' => true, + 'name' => ['ko' => '공개 게시판', 'en' => 'Readable Board'], + ]); + $this->hiddenBoard = Board::factory()->create([ + 'is_active' => true, + 'name' => ['ko' => '비공개 게시판', 'en' => 'Hidden Board'], + ]); + + // 비회원 읽기 권한은 readableBoard 에만 부여한다. + $this->grantGuestRead($this->readableBoard); + + PermissionMiddleware::clearGuestRoleCache(); + } + + /** + * ⑦ getActiveBoardsListForFilter 는 비회원에게 읽기 권한 없는 게시판을 제외한다. + * + * @scenario case=available_boards_guest + * + * @effects available_boards_match_read_permission + */ + #[Test] + public function available_boards_excludes_boards_guest_cannot_read(): void + { + $list = app(BoardService::class)->getActiveBoardsListForFilter(null); + + $slugs = array_column($list, 'slug'); + + $this->assertContains($this->readableBoard->slug, $slugs, '읽기 권한 있는 게시판은 필터 목록에 포함되어야 한다.'); + $this->assertNotContains($this->hiddenBoard->slug, $slugs, '읽기 권한 없는 게시판은 필터 목록에서 제외되어야 한다.'); + } + + /** + * ⑦ 회원 기준으로도 읽기 권한 있는 게시판만 필터 목록에 포함된다. + * + * @scenario case=available_boards_member + * + * @effects available_boards_match_read_permission + */ + #[Test] + public function available_boards_filters_by_member_read_permission(): void + { + $member = User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + $member->roles()->attach($userRole->id); + + // 회원 역할에 readableBoard 읽기 권한만 부여 + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$this->readableBoard->slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $userRole->permissions()->syncWithoutDetaching([$perm->id]); + PermissionMiddleware::clearGuestRoleCache(); + + $list = app(BoardService::class)->getActiveBoardsListForFilter($member); + + $slugs = array_column($list, 'slug'); + + $this->assertContains($this->readableBoard->slug, $slugs); + $this->assertNotContains($this->hiddenBoard->slug, $slugs); + } + + /** + * N-2 공개 인기 게시판 목록은 비회원 읽기 권한을 통과한 게시판만 노출한다. + * + * @scenario case=popular_boards_readable + * + * @effects popular_boards_exclude_unreadable + */ + #[Test] + public function popular_boards_endpoint_excludes_boards_guest_cannot_read(): void + { + $response = $this->getJson('/api/modules/sirsoft-board/boards/popular-boards?limit=20'); + $response->assertStatus(200); + + $slugs = array_column($response->json('data'), 'slug'); + + $this->assertContains($this->readableBoard->slug, $slugs, '읽기 권한 있는 게시판은 인기 게시판 목록에 포함되어야 한다.'); + $this->assertNotContains($this->hiddenBoard->slug, $slugs, '읽기 권한 없는 게시판은 인기 게시판 목록에서 제외되어야 한다.'); + } + + /** + * ⑦ HTTP 검색 엔드포인트가 Bearer 회원을 해석해 available_boards 에 반영한다. + * + * 라우트에 optional.sanctum 이 없으면 Bearer 토큰이 있어도 $request->user() 가 + * null 이라 모든 호출자가 guest 로 판정된다 — 서비스 직접 호출 테스트로는 이 + * 사각이 보이지 않으므로 실제 HTTP 경로로 검증한다 (actingAs 금지: actingAs 는 + * 미들웨어 없이도 유저를 심어 라이브와 어긋난다). + * + * @scenario case=available_boards_bearer_http + * + * @effects search_route_resolves_bearer_user, available_boards_match_read_permission + */ + #[Test] + public function search_endpoint_resolves_bearer_user_for_available_boards(): void + { + $member = User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + $member->roles()->attach($userRole->id); + + // 회원 역할에 두 게시판 읽기 권한 부여 — hiddenBoard 는 회원 역할에만 있으므로 + // (guest 미보유) Bearer 유저가 해석될 때만 목록에 나타난다. + foreach ([$this->hiddenBoard, $this->readableBoard] as $board) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$board->slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $userRole->permissions()->syncWithoutDetaching([$perm->id]); + } + PermissionMiddleware::clearGuestRoleCache(); + + $token = $member->createToken('search-bearer-test')->plainTextToken; + + $response = $this->getJson('/api/search?q=테스트', ['Authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + + $slugs = array_column($response->json('data.posts.available_boards') ?? [], 'slug'); + + $this->assertContains( + $this->hiddenBoard->slug, + $slugs, + 'Bearer 인증 회원의 읽기 권한이 available_boards 에 반영되어야 한다 (검색 라우트 optional.sanctum).' + ); + $this->assertContains($this->readableBoard->slug, $slugs); + } + + /** + * 게시판에 비회원(guest) 읽기 권한(posts.read)을 부여합니다. + * + * @param Board $board 대상 게시판 + */ + private function grantGuestRead(Board $board): void + { + $guestRole = Role::where('identifier', 'guest')->first(); + if (! $guestRole) { + return; + } + + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$board->slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $guestRole->permissions()->syncWithoutDetaching([$perm->id]); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardAccessTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardAccessTest.php index f836663c..e4813225 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardAccessTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardAccessTest.php @@ -3,8 +3,11 @@ namespace Modules\Sirsoft\Board\Tests\Feature\User; // ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) -require_once __DIR__ . '/../../ModuleTestCase.php'; +require_once __DIR__.'/../../ModuleTestCase.php'; +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Tests\ModuleTestCase; @@ -16,7 +19,6 @@ use Modules\Sirsoft\Board\Tests\ModuleTestCase; */ class BoardAccessTest extends ModuleTestCase { - protected function setUp(): void { parent::setUp(); @@ -45,6 +47,11 @@ class BoardAccessTest extends ModuleTestCase 'is_active' => false, ]); + // 디렉토리는 호출자 열람 권한 필터를 적용하므로 게스트 읽기 권한을 부여한다 + $this->grantGuestRead('active-1'); + $this->grantGuestRead('active-2'); + $this->grantGuestRead('inactive'); + // When: 비로그인 상태에서 게시판 목록 API 호출 $response = $this->getJson('/api/modules/sirsoft-board/boards'); @@ -83,8 +90,14 @@ class BoardAccessTest extends ModuleTestCase public function test_inactive_boards_not_included_in_list(): void { // Given: 비활성화된 게시판 3개, 활성화된 게시판 1개 생성 - Board::factory()->count(3)->create(['is_active' => false]); - Board::factory()->create(['is_active' => true]); + $inactiveBoards = Board::factory()->count(3)->create(['is_active' => false]); + $activeBoard = Board::factory()->create(['is_active' => true]); + + // 디렉토리는 호출자 열람 권한 필터를 적용하므로 게스트 읽기 권한을 부여한다 + foreach ($inactiveBoards as $b) { + $this->grantGuestRead($b->slug); + } + $this->grantGuestRead($activeBoard->slug); // When: 게시판 목록 조회 $response = $this->getJson('/api/modules/sirsoft-board/boards'); @@ -107,7 +120,7 @@ class BoardAccessTest extends ModuleTestCase ]); // When: 게시판 상세 조회 - $response = $this->getJson("/api/modules/sirsoft-board/boards/test-board"); + $response = $this->getJson('/api/modules/sirsoft-board/boards/test-board'); // Then: 성공 응답, 게시판 정보 반환 $response->assertStatus(200); @@ -132,7 +145,7 @@ class BoardAccessTest extends ModuleTestCase ]); // When: 비활성화된 게시판 상세 조회 시도 - $response = $this->getJson("/api/modules/sirsoft-board/boards/inactive-board"); + $response = $this->getJson('/api/modules/sirsoft-board/boards/inactive-board'); // Then: 404 응답 $response->assertStatus(404); @@ -144,7 +157,7 @@ class BoardAccessTest extends ModuleTestCase public function test_accessing_non_existent_board_returns_404(): void { // When: 존재하지 않는 게시판 조회 - $response = $this->getJson("/api/modules/sirsoft-board/boards/non-existent"); + $response = $this->getJson('/api/modules/sirsoft-board/boards/non-existent'); // Then: 404 응답 $response->assertStatus(404); @@ -155,8 +168,11 @@ class BoardAccessTest extends ModuleTestCase */ public function test_board_list_response_does_not_include_pagination(): void { - // Given: 게시판 여러 개 생성 - Board::factory()->count(5)->create(['is_active' => true]); + // Given: 게시판 여러 개 생성 (디렉토리 열람 권한 필터 대응 — 게스트 읽기 권한 부여) + $boards = Board::factory()->count(5)->create(['is_active' => true]); + foreach ($boards as $b) { + $this->grantGuestRead($b->slug); + } // When: 게시판 목록 조회 $response = $this->getJson('/api/modules/sirsoft-board/boards'); @@ -166,4 +182,28 @@ class BoardAccessTest extends ModuleTestCase $this->assertArrayNotHasKey('pagination', $response->json('data')); $this->assertIsArray($response->json('data')); } -} \ No newline at end of file + + /** + * guest 역할에 게시판 읽기 권한을 부여합니다. + * + * 디렉토리(전체 게시판 목록)는 호출자 열람 권한 필터를 적용하므로, + * 게스트 시점 검증용 게시판에는 명시적으로 읽기 권한을 부여한다 + * (PublicRecentPostsVisibilityTest 와 동일 관례). + * + * @param string $slug 게시판 슬러그 + */ + private function grantGuestRead(string $slug): void + { + $role = Role::where('identifier', 'guest')->first(); + if (! $role) { + return; + } + + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $role->permissions()->syncWithoutDetaching([$perm->id]); + PermissionMiddleware::clearGuestRoleCache(); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardListApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardListApiTest.php index edff1294..38e08acf 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardListApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardListApiTest.php @@ -5,6 +5,9 @@ namespace Modules\Sirsoft\Board\Tests\Feature\User; // ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) require_once __DIR__.'/../../ModuleTestCase.php'; +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Tests\ModuleTestCase; @@ -13,10 +16,12 @@ use Modules\Sirsoft\Board\Tests\ModuleTestCase; * 전체 게시판 목록 API 테스트 * * 전체 게시판 목록 페이지용 경량 API의 동작을 검증합니다. + * 디렉토리는 호출자 열람 권한(sirsoft-board.{slug}.posts.read) 필터를 적용하므로, + * 게스트 시점 검증용 게시판에는 setUp 에서 guest 읽기 권한을 부여한다 + * (최근글·인기글 가시성 테스트와 동일 관례 — PublicRecentPostsVisibilityTest). */ class BoardListApiTest extends ModuleTestCase { - /** * 테스트 환경 설정 */ @@ -27,6 +32,31 @@ class BoardListApiTest extends ModuleTestCase DB::table('boards')->update(['is_active' => false]); // 이 테스트 클래스에서 사용하는 슬러그 정리 (커밋된 경우 대비) DB::table('boards')->whereIn('slug', ['free', 'inactive', 'old', 'new'])->delete(); + + // 디렉토리 열람 권한 필터 대응 — 이 클래스가 쓰는 슬러그에 guest 읽기 권한 부여 + foreach (['free', 'inactive', 'old', 'new'] as $slug) { + $this->grantGuestRead($slug); + } + PermissionMiddleware::clearGuestRoleCache(); + } + + /** + * guest 역할에 게시판 읽기 권한을 부여합니다. + * + * @param string $slug 게시판 슬러그 + */ + private function grantGuestRead(string $slug): void + { + $role = Role::where('identifier', 'guest')->first(); + if (! $role) { + return; + } + + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $role->permissions()->syncWithoutDetaching([$perm->id]); } /** diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardPopularApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardPopularApiTest.php index 8a86a861..5edd92a6 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardPopularApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardPopularApiTest.php @@ -5,6 +5,9 @@ namespace Modules\Sirsoft\Board\Tests\Feature\User; // ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) require_once __DIR__.'/../../ModuleTestCase.php'; +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Board\Enums\PostStatus; @@ -49,6 +52,10 @@ class BoardPopularApiTest extends ModuleTestCase /** * 인기 게시글 API가 올바른 구조로 응답하는지 테스트 + * + * @scenario case=popular_readable_filter + * + * @effects unreadable_board_titles_absent_for_caller */ public function test_popular_returns_correct_structure(): void { @@ -139,6 +146,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: view_count가 다른 게시글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ ['board_id' => $board->id, 'title' => 'Post 1', 'content' => 'Content 1', 'view_count' => 100, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now(), 'updated_at' => now()], ['board_id' => $board->id, 'title' => 'Post 2', 'content' => 'Content 2', 'view_count' => 300, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now(), 'updated_at' => now()], @@ -164,6 +172,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 오늘과 어제 게시글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ ['board_id' => $board->id, 'title' => 'Today Post', 'content' => 'Content', 'view_count' => 100, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now(), 'updated_at' => now()], ['board_id' => $board->id, 'title' => 'Yesterday Post', 'content' => 'Content', 'view_count' => 200, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now()->subDay(), 'updated_at' => now()->subDay()], @@ -187,6 +196,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 최근 1주일과 2주 전 게시글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ ['board_id' => $board->id, 'title' => 'This Week', 'content' => 'Content', 'view_count' => 100, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now()->subDays(3), 'updated_at' => now()], ['board_id' => $board->id, 'title' => 'Two Weeks Ago', 'content' => 'Content', 'view_count' => 200, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now()->subWeeks(2), 'updated_at' => now()], @@ -211,6 +221,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 1년 이내/이전 게시글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ ['board_id' => $board->id, 'title' => 'Recent', 'content' => 'Content', 'view_count' => 100, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now(), 'updated_at' => now()], ['board_id' => $board->id, 'title' => 'Six Months Ago', 'content' => 'Content', 'view_count' => 200, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now()->subMonths(6), 'updated_at' => now()], @@ -242,6 +253,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 1년 이내/이전 게시글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ ['board_id' => $board->id, 'title' => 'Recent', 'content' => 'Content', 'view_count' => 100, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now(), 'updated_at' => now()], ['board_id' => $board->id, 'title' => 'Over One Year', 'content' => 'Content', 'view_count' => 300, 'status' => PostStatus::Published->value, 'ip_address' => '127.0.0.1', 'created_at' => now()->subMonths(13), 'updated_at' => now()], @@ -313,6 +325,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 게시글과 댓글 생성 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); // comments_count 컬럼에 직접 값 설정 (캐시 컬럼 방식) $postId = DB::table('board_posts')->insertGetId([ 'board_id' => $board->id, @@ -343,6 +356,7 @@ class BoardPopularApiTest extends ModuleTestCase { // Given: 게스트가 작성한 게시글 $board = Board::factory()->create(['is_active' => true]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ 'board_id' => $board->id, 'title' => 'Guest Post', @@ -455,6 +469,10 @@ class BoardPopularApiTest extends ModuleTestCase ]); } + // 공개 인기글은 게시판별 열람 권한(posts.read)을 통과한 게시판만 노출한다. + // 프로덕션 공개 게시판과 동일하게 guest read 권한을 부여한다. + $this->grantGuestRead($board); + for ($i = 0; $i < $postCount; $i++) { DB::table('board_posts')->insert([ 'board_id' => $board->id, @@ -471,4 +489,28 @@ class BoardPopularApiTest extends ModuleTestCase return $board; } + + /** + * 게시판에 비회원(guest) 읽기 권한(posts.read)을 부여합니다. + * + * 공개 인기글 API 는 게시판별 열람 권한을 응답 시점에 적용하므로, + * 공개 노출을 기대하는 테스트 게시판은 프로덕션처럼 guest read 권한을 갖춰야 한다. + * + * @param Board $board 대상 게시판 + */ + private function grantGuestRead(Board $board): void + { + $guestRole = Role::where('identifier', 'guest')->first(); + if (! $guestRole) { + return; + } + + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$board->slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $guestRole->permissions()->syncWithoutDetaching([$perm->id]); + + PermissionMiddleware::clearGuestRoleCache(); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardRecentPostsApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardRecentPostsApiTest.php index 6f4c9e6c..8554c501 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/BoardRecentPostsApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/BoardRecentPostsApiTest.php @@ -5,6 +5,9 @@ namespace Modules\Sirsoft\Board\Tests\Feature\User; // ModuleTestCase를 수동으로 require (autoload 전에 로드 필요) require_once __DIR__.'/../../ModuleTestCase.php'; +use App\Http\Middleware\PermissionMiddleware; +use App\Models\Permission; +use App\Models\Role; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Board\Enums\PostStatus; @@ -178,17 +181,20 @@ class BoardRecentPostsApiTest extends ModuleTestCase } /** - * 비밀글이 포함되어 is_secret 필드가 반환되는지 테스트 + * 비밀글이 포함되어 is_secret 필드가 반환되는지 테스트. + * + * 비밀글도 제목은 공개한다(본문만 보호) — 2026-02-04 확정 정책. + * (게시판별 열람 권한 필터는 별도 — createBoardWithPosts 가 guest read 권한을 부여한다.) */ public function test_recent_posts_includes_secret_posts_with_is_secret_field(): void { - // Given: 비밀글 포함 게시글 생성 - $this->createBoardWithPosts(3, includingSecret: true); + // Given: 공개글 + 비밀글 혼합 생성 (i%2==0 이 비밀글, 게시판에 guest read 권한 부여됨) + $this->createBoardWithPosts(4, includingSecret: true); - // When: API 호출 + // When: API 호출 (비로그인) $response = $this->getJson('/api/modules/sirsoft-board/boards/posts/recent'); - // Then: is_secret 필드 포함 및 비밀글 반환 + // Then: is_secret 필드 포함 및 비밀글도 응답에 포함 $response->assertStatus(200); $data = $response->json('data'); @@ -199,21 +205,22 @@ class BoardRecentPostsApiTest extends ModuleTestCase $this->assertArrayHasKey('is_secret', $post); } - // 비밀글이 포함되어 있어야 함 + // 비밀글이 포함되어 있어야 함 (제목 공개 정책 — 비밀글도 목록/최근글에 표시) $secretPosts = array_filter($data, fn ($post) => $post['is_secret'] === true); - $this->assertNotEmpty($secretPosts, '비밀글이 응답에 포함되어야 합니다'); + $this->assertNotEmpty($secretPosts, '비밀글이 응답에 포함되어야 합니다(제목 공개).'); } /** - * 비밀글도 제목이 정상적으로 표시되는지 테스트 + * 비밀글도 제목이 정상적으로 표시되는지 테스트 (제목 공개 정책 — 본문만 보호). */ public function test_secret_post_title_is_visible_in_recent_posts(): void { - // Given: 비밀글이 있는 게시판 생성 + // Given: 비밀글이 있는 게시판 생성 (guest read 권한 부여) $board = Board::factory()->create([ 'is_active' => true, 'name' => ['ko' => '비밀게시판', 'en' => 'Secret Board'], ]); + $this->grantGuestRead($board); DB::table('board_posts')->insert([ 'board_id' => $board->id, @@ -227,10 +234,10 @@ class BoardRecentPostsApiTest extends ModuleTestCase 'updated_at' => now(), ]); - // When: API 호출 + // When: API 호출 (비로그인) $response = $this->getJson('/api/modules/sirsoft-board/boards/posts/recent'); - // Then: 비밀글 제목이 보여야 함 (마스킹 안 됨) + // Then: 비밀글 제목이 보여야 함 (마스킹 안 됨 — 제목 공개 정책) $response->assertStatus(200); $data = $response->json('data'); @@ -270,8 +277,8 @@ class BoardRecentPostsApiTest extends ModuleTestCase /** * 게시판과 게시글을 생성하는 헬퍼 * - * @param int $postCount 생성할 게시글 수 - * @param bool $includingSecret 비밀글 포함 여부 + * @param int $postCount 생성할 게시글 수 + * @param bool $includingSecret 비밀글 포함 여부 * @return Board 생성된 게시판 */ private function createBoardWithPosts(int $postCount, bool $includingSecret = false): Board @@ -280,6 +287,10 @@ class BoardRecentPostsApiTest extends ModuleTestCase 'is_active' => true, ]); + // 공개 최근글은 게시판별 열람 권한(posts.read)을 통과한 게시판만 노출한다. + // 프로덕션에서 공개 게시판은 guest read 권한을 갖도록 생성되므로 동일하게 부여한다. + $this->grantGuestRead($board); + for ($i = 0; $i < $postCount; $i++) { $isSecret = $includingSecret && ($i % 2 === 0); @@ -299,4 +310,28 @@ class BoardRecentPostsApiTest extends ModuleTestCase return $board; } + + /** + * 게시판에 비회원(guest) 읽기 권한(posts.read)을 부여합니다. + * + * 공개 최근글 API 는 게시판별 열람 권한을 응답 시점에 적용하므로, + * 공개 노출을 기대하는 테스트 게시판은 프로덕션처럼 guest read 권한을 갖춰야 한다. + * + * @param Board $board 대상 게시판 + */ + private function grantGuestRead(Board $board): void + { + $guestRole = Role::where('identifier', 'guest')->first(); + if (! $guestRole) { + return; + } + + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$board->slug}.posts.read"], + ['name' => ['ko' => 'read', 'en' => 'read'], 'type' => 'user'] + ); + $guestRole->permissions()->syncWithoutDetaching([$perm->id]); + + PermissionMiddleware::clearGuestRoleCache(); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/PostSecretBooleanNormalizationTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/PostSecretBooleanNormalizationTest.php new file mode 100644 index 00000000..f7770fb3 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/PostSecretBooleanNormalizationTest.php @@ -0,0 +1,187 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\User; + +// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\Role; +use App\Models\User; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * 게시글 is_secret boolean 정규화 — 공개 이슈 #97 잠재 위험 방어 + * + * 비밀글 여부를 문자열 "true"/"false" 로 보내는 클라이언트(레이아웃 hidden 시드 등)가 + * 나타나도 `boolean` 규칙에서 422 로 깨지지 않도록 Store/UpdatePostRequest 의 + * prepareForValidation 이 문자열 표기를 boolean 으로 정규화함을 고정한다. + * 해석 불가값은 건드리지 않아 boolean 규칙이 422 로 처리한다. + */ +class PostSecretBooleanNormalizationTest extends BoardTestCase +{ + private User $memberUser; + + /** + * 테스트 게시판 slug + */ + protected function getTestBoardSlug(): string + { + return 'secret-boolean-test'; + } + + /** + * 기본 게시판 속성 (비밀글 허용) + */ + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '비밀글 boolean 테스트 게시판', 'en' => 'Secret Boolean Test Board'], + 'is_active' => true, + 'secret_mode' => 'enabled', + 'blocked_keywords' => [], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->grantUserRolePermissions(); + + $this->memberUser = User::factory()->create(['email' => 'secret-bool@test.com']); + $userRole = Role::where('identifier', 'user')->first(); + $this->memberUser->roles()->attach($userRole->id); + } + + /** + * 수용해야 하는 boolean 표기 4종(문자열)과 기대 저장값 + * + * @return array<string, array{mixed, bool}> [입력값, 기대 boolean] + */ + private function acceptedNotations(): array + { + return [ + "문자열 'true'" => ['true', true], + "문자열 'false'" => ['false', false], + "문자열 '1'" => ['1', true], + "문자열 '0'" => ['0', false], + ]; + } + + /** + * 작성: 문자열 boolean 표기 4종 모두 201 + DB boolean 저장 + */ + public function test_store_accepts_string_boolean_notations(): void + { + foreach ($this->acceptedNotations() as $label => [$input, $expected]) { + $response = $this->actingAs($this->memberUser) + ->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [ + 'title' => "비밀글 표기 테스트 {$label}", + 'content' => '비밀글 boolean 정규화 검증용 내용입니다.', + 'is_secret' => $input, + ]); + + $response->assertStatus(201); + + $postId = $response->json('data.id'); + $stored = (bool) DB::table('board_posts')->where('id', $postId)->value('is_secret'); + $this->assertSame( + $expected, + $stored, + "작성 {$label} 입력이 boolean 으로 저장되어야 합니다." + ); + } + } + + /** + * 수정: 문자열 boolean 표기 4종 모두 200 + DB boolean 저장 + */ + public function test_update_accepts_string_boolean_notations(): void + { + $createResponse = $this->actingAs($this->memberUser) + ->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [ + 'title' => '비밀글 수정 표기 테스트', + 'content' => '비밀글 boolean 정규화 수정 검증용 내용입니다.', + 'is_secret' => false, + ]); + $createResponse->assertStatus(201); + $postId = $createResponse->json('data.id'); + + foreach ($this->acceptedNotations() as $label => [$input, $expected]) { + $response = $this->actingAs($this->memberUser) + ->putJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}", [ + 'is_secret' => $input, + ]); + + $response->assertStatus(200); + + $stored = (bool) DB::table('board_posts')->where('id', $postId)->value('is_secret'); + $this->assertSame( + $expected, + $stored, + "수정 {$label} 입력이 boolean 으로 저장되어야 합니다." + ); + } + } + + /** + * 작성: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리 + */ + public function test_store_rejects_unparsable_value_with_422(): void + { + $response = $this->actingAs($this->memberUser) + ->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [ + 'title' => '비밀글 해석불가 테스트', + 'content' => '비밀글 boolean 해석 불가값 검증용 내용입니다.', + 'is_secret' => 'abc', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['is_secret']); + } + + /** + * 수정: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리 + */ + public function test_update_rejects_unparsable_value_with_422(): void + { + $createResponse = $this->actingAs($this->memberUser) + ->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [ + 'title' => '비밀글 수정 해석불가 테스트', + 'content' => '비밀글 boolean 수정 해석 불가값 검증용 내용입니다.', + 'is_secret' => true, + ]); + $createResponse->assertStatus(201); + $postId = $createResponse->json('data.id'); + + $response = $this->actingAs($this->memberUser) + ->putJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}", [ + 'is_secret' => 'abc', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['is_secret']); + + // 실패한 요청이 저장값을 건드리지 않아야 한다 + $this->assertSame(1, (int) DB::table('board_posts')->where('id', $postId)->value('is_secret')); + } + + /** + * 작성: is_secret 미전송이면 정규화가 개입하지 않고 그대로 통과 (has 가드) + */ + public function test_store_without_is_secret_passes(): void + { + $response = $this->actingAs($this->memberUser) + ->postJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts", [ + 'title' => '비밀글 미전송 테스트', + 'content' => '비밀글 필드 미전송 검증용 내용입니다.', + ]); + + $response->assertStatus(201); + + $postId = $response->json('data.id'); + $this->assertSame(0, (int) DB::table('board_posts')->where('id', $postId)->value('is_secret')); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php new file mode 100644 index 00000000..d3d29082 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php @@ -0,0 +1,443 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\User; + +// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\URL; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * 비밀글 첨부파일 접근 차단 테스트 (KVE-2026-1914 A-3) + * + * 첨부는 본문 게이팅(getFilteredContent)과 별개 경로라, 본문이 비밀 마스킹되어도 + * 첨부 다운로드/미리보기는 게시글 비밀 상태를 검사하지 않아 해시/ID 만으로 원문 + * 파일이 노출되던 결함을 검증합니다. + * + * 정책: 비밀글 첨부는 작성자 본인 또는 게시판 manager/posts.read-secret 보유자만 접근. + * + * 실제 공격자 프로필은 대개 미인증(게스트)이다. preview 라우트는 permission + * 미들웨어 없이 optional.sanctum 만 걸려(공개 썸네일 정책) 컨트롤러/서비스의 비밀 + * 게이트만이 게스트를 막는다 — 그래서 게스트 축을 명시 고정한다. download 라우트는 + * permission 미들웨어가 게스트를 401 로 선차단한다. + * + * 시나리오 축(viewer)·효과는 매니페스트 tests/scenarios/board-secret-content-gate.yaml 참조. + * 각 test 메서드의 `@scenario viewer=…` 마커가 축 조합을 커버한다(메서드당 단일 값). + * + * 효과 목록을 클래스 레벨에 몰아 적지 않는다 — 커버리지 룰은 마커 레벨을 구분하지 않으므로, + * 클래스 레벨 목록이 있으면 그 메서드를 지워도 효과가 "언급됨" 으로 집계돼 삭제가 무증상 + * green 이 된다. 마커는 메서드에만 둔다. + */ +class SecretPostAttachmentAccessTest extends BoardTestCase +{ + private User $regularUser; + + private User $ownerUser; + + private User $managerUser; + + protected function getTestBoardSlug(): string + { + return 'secret-attach'; + } + + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '비밀 첨부 테스트 게시판', 'en' => 'Secret Attachment Test Board'], + 'is_active' => true, + 'use_comment' => true, + 'secret_mode' => 'enabled', + 'blocked_keywords' => [], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $slug = $this->board->slug; + + // 일반 사용자 (posts.read + attachments.download, 비밀 열람 권한 없음) + $this->regularUser = User::factory()->create(); + $this->ownerUser = User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + if ($userRole) { + foreach (['posts.read', 'attachments.download'] as $key) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.{$key}"], + ['name' => ['ko' => $key, 'en' => $key], 'type' => 'user'] + ); + $userRole->permissions()->syncWithoutDetaching([$perm->id]); + } + $this->regularUser->roles()->attach($userRole->id); + $this->ownerUser->roles()->attach($userRole->id); + } + + // manager 권한 사용자 + $this->managerUser = User::factory()->create(); + $managerRole = Role::firstOrCreate( + ['identifier' => "{$slug}-manager"], + ['name' => ['ko' => '게시판 매니저', 'en' => 'Board Manager']] + ); + foreach (['posts.read', 'attachments.download', 'manager'] as $key) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.{$key}"], + ['name' => ['ko' => $key, 'en' => $key], 'type' => 'user'] + ); + $managerRole->permissions()->syncWithoutDetaching([$perm->id]); + } + $this->managerUser->roles()->attach($managerRole->id); + } + + private function createAttachment(int $postId, string $hash, bool $image = false): int + { + $ext = $image ? 'jpg' : 'pdf'; + $mime = $image ? 'image/jpeg' : 'application/pdf'; + + return DB::table('board_attachments')->insertGetId([ + 'board_id' => $this->board->id, + 'post_id' => $postId, + 'original_filename' => "doc.{$ext}", + 'stored_filename' => "{$hash}.{$ext}", + 'hash' => $hash, + 'mime_type' => $mime, + 'size' => 1024, + 'path' => "attachments/doc.{$ext}", + 'collection' => 'attachments', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + private function downloadUrl(string $hash): string + { + return "/api/modules/sirsoft-board/boards/{$this->board->slug}/attachment/{$hash}"; + } + + private function previewUrl(string $hash): string + { + return "/api/modules/sirsoft-board/boards/{$this->board->slug}/attachment/{$hash}/preview"; + } + + private function secretPost(): int + { + return $this->createTestPost([ + 'title' => '비밀글 첨부', + 'status' => 'published', + 'is_secret' => true, + 'user_id' => $this->ownerUser->id, + 'author_name' => 'owner', + ]); + } + + // ========================================== + // 비밀글 첨부 차단 + // ========================================== + + /** + * @scenario viewer=regular + * + * @effects regular_user_cannot_download_secret_post_attachment + */ + public function test_regular_user_cannot_download_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secdlregAAAA'); + + $response = $this->actingAs($this->regularUser, 'sanctum') + ->get($this->downloadUrl('secdlregAAAA')); + + // attachments.download 권한은 있으나 비밀 게이트로 차단(403) + $response->assertStatus(403); + } + + /** + * @scenario viewer=regular + * + * @effects regular_user_cannot_preview_secret_post_attachment + */ + public function test_regular_user_cannot_preview_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secprevregAA', image: true); + + $response = $this->actingAs($this->regularUser, 'sanctum') + ->get($this->previewUrl('secprevregAA')); + + $response->assertStatus(403); + } + + // ========================================== + // 미인증(게스트) 차단 — 실제 공격자 프로필 + // ========================================== + + /** + * 미인증(게스트)은 비밀글 첨부 미리보기를 볼 수 없다 (403). + * + * preview 라우트는 permission 미들웨어가 없어(optional.sanctum 만) 컨트롤러/서비스의 + * 비밀 게이트만이 게스트를 막는다 — 해시만 쥔 미인증 공격자가 실제로 차단되는지 고정한다. + * + * @scenario viewer=guest + * + * @effects guest_cannot_preview_secret_post_attachment + */ + public function test_guest_cannot_preview_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secprevgstAA', image: true); + + // actingAs 없음 = 미인증 게스트 + $response = $this->get($this->previewUrl('secprevgstAA')); + + $response->assertStatus(403); + } + + /** + * 미인증(게스트)은 비밀글 첨부 다운로드를 할 수 없다 (401). + * + * download 라우트는 attachments.download permission 미들웨어가 걸려 있어 게스트를 + * 컨트롤러 도달 전에 401(guest_permission_denied)로 선차단한다. + * + * @scenario viewer=guest + * + * @effects guest_cannot_download_secret_post_attachment + */ + public function test_guest_cannot_download_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secdlgstAAAA'); + + $response = $this->get($this->downloadUrl('secdlgstAAAA')); + + $response->assertStatus(401); + } + + /** + * 미인증(게스트)도 정상글(비밀 아님) 첨부 미리보기는 차단되지 않는다 (게이트 과차단 회귀 방지). + * + * 공개 썸네일 정책상 preview 는 공개다 — 비밀 게이트가 정상글 게스트 미리보기까지 + * 막으면 안 된다. 실제 파일이 없어 404 여도 403(비밀 차단)은 아니어야 한다. + * + * @scenario viewer=guest + * + * @effects guest_can_preview_normal_post_attachment + */ + public function test_guest_can_preview_normal_post_attachment(): void + { + $postId = $this->createTestPost([ + 'title' => '정상글 첨부(게스트 미리보기)', + 'status' => 'published', + 'is_secret' => false, + ]); + $this->createAttachment($postId, 'normgstAAAAA', image: true); + + $response = $this->get($this->previewUrl('normgstAAAAA')); + + $this->assertNotSame(403, $response->getStatusCode(), '정상글 첨부 미리보기는 게스트에게 비밀 차단되면 안 됩니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + // ========================================== + // 작성자/manager 는 접근 가능 (회귀 방지) + // ========================================== + + /** + * @scenario viewer=owner + * + * @effects owner_can_access_secret_post_attachment + */ + public function test_owner_can_access_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secdlownerAA'); + + $response = $this->actingAs($this->ownerUser, 'sanctum') + ->get($this->downloadUrl('secdlownerAA')); + + // 작성자 본인은 비밀 게이트 통과 (실제 파일 없어 404 여도 403 은 아님) + $this->assertNotSame(403, $response->getStatusCode(), '작성자 본인은 비밀글 첨부에 접근 가능해야 합니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + /** + * @scenario viewer=manager + * + * @effects manager_can_access_secret_post_attachment + */ + public function test_manager_can_access_secret_post_attachment(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secdlmgrAAAA'); + + $response = $this->actingAs($this->managerUser, 'sanctum') + ->get($this->downloadUrl('secdlmgrAAAA')); + + $this->assertNotSame(403, $response->getStatusCode(), 'manager 는 비밀글 첨부에 접근 가능해야 합니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + // ========================================== + // 정상글 첨부는 그대로 (회귀 방지) + // ========================================== + + /** + * @scenario viewer=regular + * + * @effects normal_post_attachment_still_public + */ + public function test_normal_post_attachment_still_public(): void + { + $postId = $this->createTestPost([ + 'title' => '정상글 첨부', + 'status' => 'published', + 'is_secret' => false, + ]); + $this->createAttachment($postId, 'normsecAAAAA'); + + $response = $this->actingAs($this->regularUser, 'sanctum') + ->get($this->downloadUrl('normsecAAAAA')); + + $this->assertNotSame(403, $response->getStatusCode(), '정상글 첨부는 권한 차단되면 안 됩니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + // ========================================== + // 서명 preview URL (비밀글 <img> 썸네일 렌더 경로) + // + // 브라우저 <img src> 는 Authorization 헤더를 실을 수 없어, 비밀 게이트 도입 후 + // 열람 권한자(작성자·manager) 화면의 비밀글 첨부 썸네일이 무인증 요청 → 403 으로 + // 깨졌다. 게이트를 통과한 응답 직렬화(PostResource)가 한시 서명 URL 을 발급하고, + // 서빙 엔드포인트가 유효 서명을 허용한다 — 무서명 게이트는 종전과 동일하다. + // ========================================== + + /** + * 서명 preview URL 을 만든다. + * + * @param string $hash 첨부 해시 + * @param int $minutes 유효 시간(분, 음수면 만료된 URL) + * @return string 상대경로 서명 URL + */ + private function signedPreviewUrl(string $hash, int $minutes = 30): string + { + return URL::temporarySignedRoute( + 'api.modules.sirsoft-board.boards.attachment.preview', + now()->addMinutes($minutes), + ['slug' => $this->board->slug, 'hash' => $hash], + absolute: false + ); + } + + /** + * 유효한 한시 서명 preview URL 은 무인증(게스트) 요청도 비밀 게이트를 통과한다. + * + * @scenario viewer=guest + * + * @effects guest_with_valid_signature_passes_secret_preview_gate + */ + public function test_guest_with_valid_signed_url_passes_secret_preview_gate(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secsignedAAA', image: true); + + $response = $this->get($this->signedPreviewUrl('secsignedAAA')); + + // 실제 파일이 없어 404 여도 403(비밀 차단)은 아니어야 한다 + $this->assertNotSame(403, $response->getStatusCode(), '유효 서명 URL 은 비밀 게이트를 통과해야 합니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + /** + * 변조된 서명 preview URL 은 종전과 동일하게 비밀 게이트에 차단된다 (403). + * + * @scenario viewer=guest + * + * @effects guest_with_tampered_signature_still_blocked + */ + public function test_guest_with_tampered_signature_still_blocked(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'sectamperAAA', image: true); + + $tampered = preg_replace_callback( + '/(signature=)([0-9a-f]+)/', + fn ($m) => $m[1].substr($m[2], 0, -8).strrev(substr($m[2], -8)), + $this->signedPreviewUrl('sectamperAAA') + ); + + $this->get($tampered)->assertStatus(403); + } + + /** + * 만료된 서명 preview URL 은 비밀 게이트에 차단된다 (403, 한시성 보장). + * + * @scenario viewer=guest + * + * @effects guest_with_expired_signature_still_blocked + */ + public function test_guest_with_expired_signature_still_blocked(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secexpireAAA', image: true); + + $this->get($this->signedPreviewUrl('secexpireAAA', minutes: -1))->assertStatus(403); + } + + /** + * 열람 권한자(작성자)에게 직렬화되는 비밀글 상세 응답의 첨부 preview_url 은 + * 한시 서명 URL 이고, 그 URL 은 무인증 <img> 요청으로도 게이트를 통과한다 + * (렌더 계약의 양끝 검증). + * + * @scenario viewer=owner + * + * @effects secret_post_detail_serializes_signed_preview_url + */ + public function test_secret_post_detail_serializes_signed_preview_url_for_owner(): void + { + $postId = $this->secretPost(); + $this->createAttachment($postId, 'secserialAAA', image: true); + + $previewUrl = $this->actingAs($this->ownerUser, 'sanctum') + ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}") + ->assertStatus(200) + ->json('data.attachments.0.preview_url'); + + $this->assertIsString($previewUrl); + $this->assertStringContainsString('signature=', $previewUrl); + + $response = $this->get($previewUrl); + $this->assertNotSame(403, $response->getStatusCode(), '직렬화된 서명 URL 은 무인증 게이트를 통과해야 합니다'); + $this->assertLessThan(500, $response->getStatusCode(), '게이트 통과 시 서버 오류가 아니어야 합니다'); + } + + /** + * 정상글(비밀 아님) 상세 응답의 첨부 preview_url 은 종전과 동일한 무서명 + * 공개 hash 경로다 (공개 콘텐츠에 만료성 URL 이 섞이는 회귀 방지). + * + * @scenario viewer=regular + * + * @effects normal_post_detail_serializes_plain_preview_url + */ + public function test_normal_post_detail_serializes_plain_preview_url(): void + { + $postId = $this->createTestPost([ + 'title' => '정상글 첨부 직렬화', + 'status' => 'published', + 'is_secret' => false, + ]); + $this->createAttachment($postId, 'normserialAA', image: true); + + $previewUrl = $this->actingAs($this->regularUser, 'sanctum') + ->getJson("/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}") + ->assertStatus(200) + ->json('data.attachments.0.preview_url'); + + $this->assertSame($this->previewUrl('normserialAA'), $previewUrl); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostCommentAccessTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostCommentAccessTest.php new file mode 100644 index 00000000..dba23b28 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostCommentAccessTest.php @@ -0,0 +1,314 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\User; + +// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Board\Http\Resources\CommentResource; +use Modules\Sirsoft\Board\Models\Comment; +use Modules\Sirsoft\Board\Models\Post; +use Modules\Sirsoft\Board\Services\CommentService; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * 비밀글 댓글 목록 게이팅 테스트 (KVE-2026-1914 A-4) + * + * 댓글 목록은 부모 게시글 비밀 상태를 검사하지 않아, 비밀글의 댓글이 비열람자에게 + * 그대로 노출되던 결함을 검증합니다. + * + * 정책: 부모 게시글이 비밀글이면 열람 권한(작성자/manager/posts.read-secret) 없는 + * 요청에는 댓글 목록을 노출하지 않는다(빈 목록). + * + * 실제 공격자 프로필은 미인증(게스트)이다. 사용자 댓글 목록 라우트는 comments.read + * permission 미들웨어가 걸려 있어 그 권한이 없는 게스트를 401 로 선차단한다(부모가 + * 비밀글이든 아니든). 게스트가 permission 을 갖는 공개 게시판이라도 비밀 게이트가 빈 + * 목록을 돌려주는 것은 regular(비밀 권한 없는 회원) 경로와 동일하다. + * + * 시나리오 축(viewer)·효과는 매니페스트 tests/scenarios/board-secret-content-gate.yaml 참조. + * 각 test 메서드의 `@scenario viewer=…` 마커가 축 조합을 커버한다(메서드당 단일 값). + * + * 효과 목록을 클래스 레벨에 몰아 적지 않는다 — 커버리지 룰은 마커 레벨을 구분하지 않으므로, + * 클래스 레벨 목록이 있으면 그 메서드를 지워도 효과가 "언급됨" 으로 집계돼 삭제가 무증상 + * green 이 된다. 마커는 메서드에만 둔다. + */ +class SecretPostCommentAccessTest extends BoardTestCase +{ + private User $regularUser; + + private User $ownerUser; + + private User $managerUser; + + protected function getTestBoardSlug(): string + { + return 'secret-comment'; + } + + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '비밀 댓글 테스트 게시판', 'en' => 'Secret Comment Test Board'], + 'is_active' => true, + 'use_comment' => true, + 'secret_mode' => 'enabled', + 'blocked_keywords' => [], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $slug = $this->board->slug; + + $this->regularUser = User::factory()->create(); + $this->ownerUser = User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + if ($userRole) { + foreach (['posts.read', 'comments.read'] as $key) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.{$key}"], + ['name' => ['ko' => $key, 'en' => $key], 'type' => 'user'] + ); + $userRole->permissions()->syncWithoutDetaching([$perm->id]); + } + $this->regularUser->roles()->attach($userRole->id); + $this->ownerUser->roles()->attach($userRole->id); + } + + $this->managerUser = User::factory()->create(); + $managerRole = Role::firstOrCreate( + ['identifier' => "{$slug}-manager"], + ['name' => ['ko' => '게시판 매니저', 'en' => 'Board Manager']] + ); + foreach (['posts.read', 'comments.read', 'manager'] as $key) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.{$key}"], + ['name' => ['ko' => $key, 'en' => $key], 'type' => 'user'] + ); + $managerRole->permissions()->syncWithoutDetaching([$perm->id]); + } + $this->managerUser->roles()->attach($managerRole->id); + } + + private function commentsUrl(int $postId): string + { + return "/api/modules/sirsoft-board/boards/{$this->board->slug}/posts/{$postId}/comments"; + } + + private function secretPostWithComment(): int + { + $postId = $this->createTestPost([ + 'title' => '비밀글', + 'status' => 'published', + 'is_secret' => true, + 'user_id' => $this->ownerUser->id, + 'author_name' => 'owner', + ]); + $this->createTestComment($postId, [ + 'content' => '비밀 댓글 내용', + 'user_id' => $this->ownerUser->id, + 'author_name' => 'owner', + ]); + + return $postId; + } + + /** + * @scenario viewer=regular + * + * @effects regular_user_gets_empty_comment_list_on_secret_post + */ + public function test_regular_user_gets_empty_comment_list_on_secret_post(): void + { + $postId = $this->secretPostWithComment(); + + $response = $this->actingAs($this->regularUser, 'sanctum') + ->getJson($this->commentsUrl($postId)); + + $response->assertStatus(200); + $this->assertCount(0, $response->json('data'), '비열람자에게는 비밀글 댓글이 노출되면 안 됩니다'); + } + + /** + * @scenario viewer=owner + * + * @effects owner_sees_secret_post_comments + */ + public function test_owner_sees_secret_post_comments(): void + { + $postId = $this->secretPostWithComment(); + + $response = $this->actingAs($this->ownerUser, 'sanctum') + ->getJson($this->commentsUrl($postId)); + + $response->assertStatus(200); + $this->assertGreaterThanOrEqual(1, count($response->json('data')), '작성자 본인은 비밀글 댓글을 볼 수 있어야 합니다'); + } + + /** + * @scenario viewer=manager + * + * @effects manager_sees_secret_post_comments + */ + public function test_manager_sees_secret_post_comments(): void + { + $postId = $this->secretPostWithComment(); + + $response = $this->actingAs($this->managerUser, 'sanctum') + ->getJson($this->commentsUrl($postId)); + + $response->assertStatus(200); + $this->assertGreaterThanOrEqual(1, count($response->json('data')), 'manager 는 비밀글 댓글을 볼 수 있어야 합니다'); + } + + /** + * 미인증(게스트)은 비밀글 댓글 목록에 접근할 수 없다 (401). + * + * 사용자 댓글 목록 라우트는 comments.read permission 미들웨어가 걸려 있어, 그 권한이 + * 없는 게스트는 컨트롤러(비밀 게이트) 도달 전에 401 로 차단된다 — 해시/ID 로 비밀글 + * 댓글을 훑는 미인증 공격자를 막는다. + * + * @scenario viewer=guest + * + * @effects guest_blocked_from_secret_post_comments + */ + public function test_guest_blocked_from_secret_post_comments(): void + { + $postId = $this->secretPostWithComment(); + + // actingAs 없음 = 미인증 게스트 + $response = $this->getJson($this->commentsUrl($postId)); + + $response->assertStatus(401); + } + + /** + * @scenario viewer=regular + * + * @effects normal_post_comments_still_visible + */ + public function test_normal_post_comments_still_visible(): void + { + $postId = $this->createTestPost([ + 'title' => '정상글', + 'status' => 'published', + 'is_secret' => false, + ]); + $this->createTestComment($postId, ['content' => '정상 댓글']); + + $response = $this->actingAs($this->regularUser, 'sanctum') + ->getJson($this->commentsUrl($postId)); + + $response->assertStatus(200); + $this->assertGreaterThanOrEqual(1, count($response->json('data')), '정상글 댓글은 노출되어야 합니다'); + } + + /** + * 2차 방어(이중 방어): 컨트롤러가 넘긴 부모 post 를 CommentResource 가 요청 속성으로 받아 + * 비밀글 댓글 원문을 마스킹한다 — 1차 방어(index 빈 컬렉션)가 회귀해 비뷰어가 목록에 + * 도달하더라도 Resource 층에서 원문이 새지 않음을 고정한다(KVE-2026-1914 A-4b). + * + * 이 테스트는 수정 전(2차 방어가 relationLoaded('post') 만 검사, 목록에서 post 미로드라 항상 + * no-op) 에는 content 가 노출되어 실패한다. + * + * @scenario viewer=guest + * + * @effects second_defense_masks_secret_comment_content + */ + public function test_second_defense_masks_secret_comment_content_via_request_attribute(): void + { + $postId = $this->secretPostWithComment(); + $post = Post::find($postId); + $comment = Comment::where('post_id', $postId)->first(); + $this->assertNotNull($comment, '비밀글에 댓글이 존재해야 합니다'); + + // 게스트(비뷰어) 요청 + 컨트롤러 index 가 넘기는 부모 post 속성 + $request = Request::create($this->commentsUrl($postId), 'GET'); + $request->attributes->set('sirsoft_board_parent_post', $post); + app()->instance('request', $request); + + $arr = (new CommentResource($comment))->toArray($request); + + $this->assertArrayHasKey('content', $arr); + $this->assertNull($arr['content'], '부모가 비밀글인 댓글은 2차 방어로 원문이 마스킹되어야 합니다'); + } + + /** + * 성능 실측: 댓글 목록 Resource 해석 시 부모 post 를 댓글당 lazy-load 하지 않아야 한다. + * CommentResource::toArray 34행 `$this->post?->board?->slug` 가 content 계산 전 post 를 + * lazy-load 하면 댓글 N건에 post 쿼리 N건(N+1)이 발생한다 — 컨트롤러가 부모 post 를 + * 요청 속성으로 전달하면 0건이어야 한다. + * + * @scenario viewer=regular + * + * @effects comment_list_no_per_comment_post_query + */ + public function test_comment_list_does_not_lazy_load_post_per_comment(): void + { + $postId = $this->createTestPost([ + 'title' => '정상글(쿼리 실측)', + 'status' => 'published', + 'is_secret' => false, + ]); + for ($i = 0; $i < 5; $i++) { + $this->createTestComment($postId, ['content' => "댓글 {$i}"]); + } + $post = Post::find($postId); + $comments = app(CommentService::class) + ->getCommentsByPostId($this->board->slug, $postId, 'user'); + + // 컨트롤러 index 가 넘기는 부모 post 속성 + $request = Request::create($this->commentsUrl($postId), 'GET'); + $request->attributes->set('sirsoft_board_parent_post', $post); + app()->instance('request', $request); + + DB::enableQueryLog(); + CommentResource::collection($comments)->toArray($request); + $queries = DB::getQueryLog(); + DB::disableQueryLog(); + + $postQueries = array_filter($queries, fn ($q) => str_contains($q['query'], 'board_posts')); + fwrite(STDERR, "\n[PERF] board_posts 쿼리 ".count($postQueries).'건 / 댓글 '.$comments->count()."건\n"); + + // 컨트롤러가 부모 post 를 요청 속성으로 넘기므로 Resource 해석은 board_posts 를 한 번도 + // 조회하지 않아야 한다(정확히 0건). ≤1 로 두면 단일 post 재조회 회귀를 놓친다. + $this->assertSame( + 0, + count($postQueries), + '댓글 목록 Resource 해석에서 부모 post 를 조회하면 안 됩니다(N+1). 컨트롤러가 넘긴 post 재사용 → 0건' + ); + } + + /** + * 2차 방어가 부모 post 컨텍스트 없이는(상세/생성 경로 하위호환) relationLoaded('post') 로 + * 폴백함을 고정 — 정상 뷰어(작성자)는 마스킹되지 않는다. + * + * @scenario viewer=owner + * + * @effects owner_second_defense_not_masked + */ + public function test_second_defense_does_not_mask_for_owner(): void + { + $postId = $this->secretPostWithComment(); + $post = Post::find($postId); + $comment = Comment::where('post_id', $postId)->first(); + + $this->actingAs($this->ownerUser, 'sanctum'); + $request = Request::create($this->commentsUrl($postId), 'GET'); + $request->setUserResolver(fn () => $this->ownerUser); + $request->attributes->set('sirsoft_board_parent_post', $post); + app()->instance('request', $request); + + $arr = (new CommentResource($comment))->toArray($request); + + $this->assertNotNull($arr['content'], '작성자 본인에게는 비밀글 댓글 원문이 노출되어야 합니다'); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/UserActivitySecretExposureTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/UserActivitySecretExposureTest.php new file mode 100644 index 00000000..29924770 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/UserActivitySecretExposureTest.php @@ -0,0 +1,282 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Feature\User; + +// 테스트 베이스 클래스 수동 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * 사용자 활동/공개 프로필 목록의 비밀글 본문 노출 차단 (KVE-2026-1914 형제) + * + * 이 두 목록은 본문 일부(`content_plain`, 1000자)를 함께 싣는데 Resource 를 거치지 않고 + * 저장소 배열을 그대로 응답에 싣는다. 공개 프로필 라우트는 `optional.sanctum` 이라 + * **미인증**으로 도달하므로, 타인의 비밀글·블라인드 글 본문이 그대로 나갔다. + * + * 결함의 형태는 **사문화된 옵트인 플래그**였다 — 저장소가 `is_public` 을 읽어 필터를 걸도록 + * 돼 있었지만 그 키를 설정하는 코드가 어디에도 없어(전수 grep write 0건) 한 번도 적용되지 + * 않았다. 판정을 열람자 신원(`viewer_id`) 기반 fail-closed 로 뒤집었다. + * + * 가리는 것은 **본문뿐**이다. 행과 제목은 게시판 목록에서 이미 같은 수준으로 노출되며 + * (`PostResource` 의 목록 규칙: 제목은 노출, 본문만 차단) 프로필 UI 도 비밀글/블라인드 + * 배지를 그린다 — 행을 지우면 결함 차단에 필요한 범위를 넘어 기능이 깎인다. + * + * 시나리오 축·효과는 매니페스트 tests/scenarios/board-activity-secret-masking.yaml 참조. + * 각 test 메서드의 `@scenario viewer=…, activity_type=…` 마커가 축 조합을, `@effects …` 가 + * 효과를 커버한다(메서드당 단일 조합). + * + * 축 요약(마커 아님 — 평문): viewer 는 guest·other_user·owner, activity_type 은 + * authored·commented 다. 클래스 레벨에 `@effects` 를 몰아 적으면 그 메서드가 하나도 없어도 + * 매니페스트 효과가 "언급됨" 으로 집계되어 커버리지를 부풀린다 — 마커는 메서드에만 둔다. + */ +class UserActivitySecretExposureTest extends BoardTestCase +{ + private User $author; + + private User $stranger; + + protected function getTestBoardSlug(): string + { + return 'activity-secret'; + } + + protected function getDefaultBoardAttributes(string $slug): array + { + return [ + 'slug' => $slug, + 'name' => ['ko' => '활동 노출 테스트 게시판', 'en' => 'Activity Exposure Test Board'], + 'is_active' => true, + 'use_comment' => true, + 'secret_mode' => 'enabled', + 'blocked_keywords' => [], + ]; + } + + protected function setUp(): void + { + parent::setUp(); + + $slug = $this->board->slug; + + $this->author = User::factory()->create(); + $this->stranger = User::factory()->create(); + + $userRole = Role::where('identifier', 'user')->first(); + if ($userRole) { + foreach (['posts.read', 'comments.read', 'comments.create'] as $key) { + $perm = Permission::firstOrCreate( + ['identifier' => "sirsoft-board.{$slug}.{$key}"], + ['name' => ['ko' => $key, 'en' => $key], 'type' => 'user'] + ); + $userRole->permissions()->syncWithoutDetaching([$perm->id]); + } + $this->author->roles()->attach($userRole->id); + $this->stranger->roles()->attach($userRole->id); + } + } + + private function profileUrl(User $user): string + { + return "/api/modules/sirsoft-board/users/{$user->uuid}/posts"; + } + + /** + * 마이페이지 활동 목록(내가 댓글 단 글) URL. + * + * 라우트 이름으로 뽑는다 — 모듈 라우트는 `api/modules/{id}` 프리픽스가 그룹에서 붙으므로 + * 경로를 손으로 적으면 프리픽스를 빠뜨려 404 가 되고, 그 404 는 마스킹 단언에 도달하기 + * 전에 났다는 사실이 드러나지 않으면 "차단됐다" 로 오독되기 쉽다. + */ + private function commentedActivityUrl(): string + { + return route('api.modules.sirsoft-board.me.board-activities.index', [ + 'activity_type' => 'commented', + ], false); + } + + private function createAuthorPost(array $overrides = []): int + { + return $this->createTestPost(array_merge([ + 'title' => '작성자 글', + 'content' => '대외비 본문입니다', + 'status' => 'published', + 'is_secret' => false, + 'user_id' => $this->author->id, + 'author_name' => 'author', + ], $overrides)); + } + + /** + * 미인증 요청에 타인 비밀글의 **본문**이 나가지 않는다 (행·제목은 유지). + * + * 이 라우트는 optional.sanctum 이라 로그인 없이 도달한다 — 결함의 실제 공격 프로필이다. + * + * @scenario viewer=guest, activity_type=authored + * + * @effects public_profile_masks_secret_post_content_for_others, activity_rows_and_titles_remain_visible + */ + public function test_guest_sees_secret_post_row_without_content(): void + { + $this->createAuthorPost(['title' => '공개글', 'is_secret' => false]); + $this->createAuthorPost(['title' => '비밀글', 'is_secret' => true, 'content' => '비밀 본문']); + + $response = $this->getJson($this->profileUrl($this->author)); + + $response->assertStatus(200); + + $rows = $response->json('data.data') ?? []; + $titles = array_column($rows, 'title'); + + // 행·제목은 게시판 목록에서 이미 같은 수준으로 보인다 — 가리는 것은 본문뿐이다. + $this->assertContains('공개글', $titles); + $this->assertContains('비밀글', $titles, '비밀글도 목록에는 나와야 합니다(배지로 구분)'); + + $this->assertStringNotContainsString('비밀 본문', $response->getContent(), '비밀글 본문이 응답에 실리면 안 됩니다'); + } + + /** + * 미인증 요청에 블라인드 글의 **본문**도 나가지 않는다 (행은 유지). + * + * 블라인드 본문은 이 모듈의 다른 목록에서도 비워진다 + * (PostResource::getMaskedContentPreviewForList) — 같은 규칙이다. + * + * @scenario viewer=guest, activity_type=authored + * + * @effects public_profile_masks_blinded_post_content_for_others, activity_rows_and_titles_remain_visible + */ + public function test_guest_sees_blinded_post_row_without_content(): void + { + $this->createAuthorPost(['title' => '공개글']); + $this->createAuthorPost(['title' => '블라인드글', 'status' => 'blinded', 'content' => '가려진 본문']); + + $response = $this->getJson($this->profileUrl($this->author)); + + $titles = array_column($response->json('data.data') ?? [], 'title'); + $this->assertContains('블라인드글', $titles, '블라인드 글도 목록에는 나와야 합니다'); + $this->assertStringNotContainsString('가려진 본문', $response->getContent()); + } + + /** + * 로그인한 타인에게도 동일하게 본문만 가려진다 (인증 여부와 무관한 판정). + * + * @scenario viewer=other_user, activity_type=authored + * + * @effects public_profile_masks_secret_post_content_for_others + */ + public function test_other_authenticated_user_sees_secret_post_row_without_content(): void + { + $this->createAuthorPost(['title' => '비밀글', 'is_secret' => true, 'content' => '비밀 본문']); + + $response = $this->actingAs($this->stranger, 'sanctum') + ->getJson($this->profileUrl($this->author)); + + $titles = array_column($response->json('data.data') ?? [], 'title'); + $this->assertContains('비밀글', $titles); + $this->assertStringNotContainsString('비밀 본문', $response->getContent(), '로그인한 타인에게도 본문은 나가면 안 됩니다'); + } + + /** + * (과차단 회귀) 본인이 자기 프로필을 보면 자기 비밀글의 본문까지 그대로 보인다. + * + * fail-closed 로 뒤집으면서 본인 시야까지 좁히면 기능 축소다. + * + * @scenario viewer=owner, activity_type=authored + * + * @effects public_profile_shows_own_secret_content_to_owner + */ + public function test_owner_still_sees_own_secret_post_content(): void + { + $this->createAuthorPost(['title' => '비밀글', 'is_secret' => true, 'content' => '비밀 본문']); + + $response = $this->actingAs($this->author, 'sanctum') + ->getJson($this->profileUrl($this->author)); + + $rows = $response->json('data.data') ?? []; + $titles = array_column($rows, 'title'); + $this->assertContains('비밀글', $titles, '본인에게는 자기 비밀글이 보여야 합니다'); + $this->assertStringContainsString('비밀 본문', $response->getContent(), '본인에게는 본문도 보여야 합니다'); + } + + /** + * "내가 댓글 단 글" 목록에는 **타인이 쓴** 비밀글이 섞인다 — 그 본문은 나가지 않는다. + * + * authored 축과 다른 코드 경로다. authored 는 목록 주인이 곧 글쓴이라 열람자 일치만 + * 보면 되지만(`$isOwnView`), commented 는 글마다 작성자가 달라 **글 단위**로 판정한다 + * (`$post->user_id !== $viewerId`). authored 만 검증하면 이 분기가 무보호로 남는다. + * + * @scenario viewer=other_user, activity_type=commented + * + * @effects commented_activity_masks_others_secret_content, activity_rows_and_titles_remain_visible + */ + public function test_commented_activity_masks_others_secret_post_content(): void + { + $postId = $this->createAuthorPost([ + 'title' => '남의 비밀글', + 'is_secret' => true, + 'content' => '남의 비밀 본문', + ]); + + // 열람자(stranger)가 그 글에 댓글을 달아 자기 활동 목록에 올린다. + $this->createTestComment($postId, [ + 'user_id' => $this->stranger->id, + 'author_name' => 'stranger', + 'content' => '댓글 답니다', + ]); + + $response = $this->actingAs($this->stranger, 'sanctum') + ->getJson($this->commentedActivityUrl()); + + $response->assertStatus(200); + + $titles = array_column($response->json('data.data') ?? [], 'title'); + + // 행은 내 활동 기록이므로 남는다 — 가리는 것은 남의 본문뿐이다. + $this->assertContains('남의 비밀글', $titles, '내가 댓글 단 글은 활동 목록에 남아야 합니다'); + $this->assertStringNotContainsString( + '남의 비밀 본문', + $response->getContent(), + '타인이 쓴 비밀글의 본문이 댓글 활동 목록으로 새면 안 됩니다' + ); + } + + /** + * (과차단 회귀) 자기 비밀글에 자기가 댓글을 달았으면 본문이 그대로 보인다. + * + * 글 단위 판정을 "비밀글이면 무조건 마스킹" 으로 조이면 자기 글까지 가려져 기능이 깎인다. + * + * @scenario viewer=owner, activity_type=commented + * + * @effects commented_activity_shows_own_secret_content_to_owner + */ + public function test_commented_activity_shows_own_secret_post_content_to_owner(): void + { + $postId = $this->createAuthorPost([ + 'title' => '내 비밀글', + 'is_secret' => true, + 'content' => '내 비밀 본문', + ]); + + $this->createTestComment($postId, [ + 'user_id' => $this->author->id, + 'author_name' => 'author', + 'content' => '자문자답', + ]); + + $response = $this->actingAs($this->author, 'sanctum') + ->getJson($this->commentedActivityUrl()); + + $response->assertStatus(200); + + $titles = array_column($response->json('data.data') ?? [], 'title'); + $this->assertContains('내 비밀글', $titles); + $this->assertStringContainsString( + '내 비밀 본문', + $response->getContent(), + '본인이 쓴 비밀글이면 댓글 활동 목록에서도 본문이 보여야 합니다' + ); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Feature/User/UserPublicPostsApiTest.php b/modules/_bundled/sirsoft-board/tests/Feature/User/UserPublicPostsApiTest.php index 95c8b07a..52ff4836 100644 --- a/modules/_bundled/sirsoft-board/tests/Feature/User/UserPublicPostsApiTest.php +++ b/modules/_bundled/sirsoft-board/tests/Feature/User/UserPublicPostsApiTest.php @@ -98,11 +98,14 @@ class UserPublicPostsApiTest extends BoardTestCase } /** - * 비밀글도 사용자 프로필 게시글 목록에 포함된다 (is_secret 배지로 구분). + * 타인 프로필에서도 비밀글은 목록에 나오되 본문은 나가지 않는다. * - * getUserPublicPosts는 모든 게시글을 표시하며 비밀글/블라인드는 배지로 구분합니다. + * 이 목록은 본문 일부(content_plain)를 함께 싣는데 라우트가 optional.sanctum 이라 + * **미인증 요청에 타인의 비밀글 본문이 그대로 나갔다**(KVE-2026-1914 형제). + * 행·제목은 게시판 목록에서 이미 같은 수준으로 보이므로(PostResource 의 목록 규칙) + * 행은 남기고 본문만 비운다 — UI 의 비밀글 배지도 그대로 동작한다. */ - public function test_secret_posts_are_included_with_badge(): void + public function test_secret_posts_are_listed_without_content_for_other_viewers(): void { // Given: 공개글과 비밀글을 작성 $this->createPost($this->board->slug, [ @@ -122,19 +125,27 @@ class UserPublicPostsApiTest extends BoardTestCase // When: 사용자 게시글을 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts"); - // Then: 모든 게시글이 반환되며 is_secret 배지로 구분 + // Then: 두 건 모두 나오되 비밀글의 본문만 비어 있다 $response->assertOk(); $data = $response->json('data.data'); $this->assertCount(2, $data); + + $secret = collect($data)->firstWhere('is_secret', true); + $public = collect($data)->firstWhere('is_secret', false); + + $this->assertNotNull($secret, '비밀글도 목록에는 나와야 합니다(배지로 구분)'); + $this->assertSame('', $secret['content_plain'], '비밀글 본문은 나가면 안 됩니다'); + $this->assertNotSame('', $public['content_plain'], '공개글 본문은 그대로여야 합니다'); } /** - * 블라인드 처리된 게시글도 사용자 프로필 게시글 목록에 포함된다 (status 배지로 구분). + * 타인 프로필에서도 블라인드 게시글은 목록에 나오되 본문은 나가지 않는다. * - * getUserPublicPosts는 모든 게시글을 표시하며 비밀글/블라인드는 배지로 구분합니다. + * 블라인드 글의 본문은 이 모듈의 다른 목록에서도 비워진다 + * (PostResource::getMaskedContentPreviewForList) — 같은 규칙을 적용한다. */ - public function test_blinded_posts_are_included_with_badge(): void + public function test_blinded_posts_are_listed_without_content_for_other_viewers(): void { // Given: 공개된 게시글과 블라인드 처리된 게시글 $this->createPost($this->board->slug, [ @@ -152,11 +163,15 @@ class UserPublicPostsApiTest extends BoardTestCase // When: 사용자 게시글을 조회하면 $response = $this->getJson("/api/modules/sirsoft-board/users/{$this->targetUser->uuid}/posts"); - // Then: 모든 게시글이 반환되며 status 배지로 구분 + // Then: 두 건 모두 나오되 블라인드 글의 본문만 비어 있다 $response->assertOk(); $data = $response->json('data.data'); $this->assertCount(2, $data); + + $blinded = collect($data)->firstWhere('status', 'blinded'); + $this->assertNotNull($blinded, '블라인드 글도 목록에는 나와야 합니다(배지로 구분)'); + $this->assertSame('', $blinded['content_plain'], '블라인드 글 본문은 나가면 안 됩니다'); } /** diff --git a/modules/_bundled/sirsoft-board/tests/ModuleTestCase.php b/modules/_bundled/sirsoft-board/tests/ModuleTestCase.php index 6c6890a0..d3eb3485 100644 --- a/modules/_bundled/sirsoft-board/tests/ModuleTestCase.php +++ b/modules/_bundled/sirsoft-board/tests/ModuleTestCase.php @@ -261,8 +261,12 @@ abstract class ModuleTestCase extends TestCase $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts b/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts index d6f3d45e..733e0686 100644 --- a/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts +++ b/modules/_bundled/sirsoft-board/tests/Playwright/fixtures/board-auth.ts @@ -19,6 +19,8 @@ import { issueToken, authenticatePage } from '../../../../../../tests/Playwright type BoardAuthFixtures = { /** 대시보드 진입 권한 + 게시판 조회/신고 조회 권한 토큰 */ dashboardToken: string; + /** 답글 삭제 정책 E2E 용 — 게시판 CRUD + 환경설정 + 전용 게시판(e2e-reply-policy) 글 관리 토큰 */ + replyPolicyToken: string; /** 환경설정 조회/수정 권한 토큰 */ settingsToken: string; /** 권한 없는 일반 사용자 토큰 (영역 부재 검증용) */ @@ -47,6 +49,24 @@ export const test = base.extend<BoardAuthFixtures>({ ), ); }, + replyPolicyToken: async ({}, use) => { + // 게시판 스코프 권한은 slug 가 식별자에 박히므로 전용 게시판 slug 로 발급. + // core.profile.read: 게시판 생성 필수값(board_manager_ids)에 넣을 본인 uuid 조회용. + await use( + issueToken( + 'core.profile.read', + 'sirsoft-board.boards.read', + 'sirsoft-board.boards.create', + 'sirsoft-board.boards.update', + 'sirsoft-board.boards.delete', + 'sirsoft-board.settings.read', + 'sirsoft-board.settings.update', + 'sirsoft-board.e2e-reply-policy.admin.posts.read', + 'sirsoft-board.e2e-reply-policy.admin.posts.write', + 'sirsoft-board.e2e-reply-policy.admin.manage', + ), + ); + }, noPermissionToken: async ({}, use) => { await use(issueToken()); }, diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reply-delete-policy.spec.ts b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reply-delete-policy.spec.ts new file mode 100644 index 00000000..1dd74d14 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reply-delete-policy.spec.ts @@ -0,0 +1,182 @@ +/** + * 게시판 답글 삭제 정책 (block/cascade) — 설정 노출·저장 왕복 + block 차단 (#573, 공개 #106/#107 후속). + * + * 검증 축: + * ① 게시판 환경설정 "답변글" 탭에 정책 Select 렌더 + 기본값 cascade + * ② 게시판 편집 폼에서 정책을 block 으로 저장 → 재조회 왕복 + * ③ block 게시판에서 답글 달린 글 삭제 → 422 + has_replies 메시지 (DB 무변화) + * + * 단위(PHPUnit ReplyDeletePolicyTest)는 서비스/저장소 cascade 의미론을 담당하고, + * 이 spec 은 화면 노출과 API 계약(저장 왕복·422)의 브라우저 수준 확인을 담당한다. + * + * 전용 게시판(e2e-reply-policy)을 spec 안에서 생성/삭제하므로 시드 의존이 없다. + * + * @scenario post-reply-delete-policy + * @axes policy=block, policy=cascade + * @effects settings_and_form_expose_policy, block_returns_422_with_message + */ +import { test, expect, authenticatePage } from '../../fixtures/board-auth'; + +const SLUG = 'e2e-reply-policy'; +const API = '/api/modules/sirsoft-board'; + +/** 브라우저 컨텍스트에서 Bearer 토큰으로 API 를 호출한다 (실 브라우저 실측 규약). */ +async function api( + page: import('@playwright/test').Page, + bearer: string, + method: string, + path: string, + body?: unknown, +): Promise<{ status: number; body: any }> { + return page.evaluate( + async ({ bearer, method, path, body }) => { + const response = await fetch(path, { + method, + headers: { + Authorization: `Bearer ${bearer}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + let parsed: unknown = null; + try { + parsed = await response.json(); + } catch { + /* 비 JSON 응답 허용 */ + } + return { status: response.status, body: parsed as any }; + }, + { bearer, method, path, body }, + ); +} + +async function deleteBoardIfExists(page: import('@playwright/test').Page, bearer: string): Promise<void> { + await api(page, bearer, 'DELETE', `${API}/admin/boards/${SLUG}`); +} + +/** 전용 게시판을 생성한다 (StoreBoardRequest 필수값 포함). */ +async function createBoard( + page: import('@playwright/test').Page, + bearer: string, + extra: Record<string, unknown> = {}, +): Promise<{ status: number; body: any }> { + // board_manager_ids 필수값: 시드 게시판(free)의 관리자 uuid 를 재사용한다 + // (프로필 엔드포인트는 user 타입 권한 게이트라 admin 토큰으로도 403 — 실측) + const freeBoard = await api(page, bearer, 'GET', `${API}/admin/boards/free`); + const managerUuid = (freeBoard.body?.data?.board_manager_ids ?? [])[0]; + + if (!managerUuid) { + throw new Error(`manager uuid unavailable: status=${freeBoard.status} body=${JSON.stringify(freeBoard.body).slice(0, 300)}`); + } + + return api(page, bearer, 'POST', `${API}/admin/boards`, { + slug: SLUG, + name: { ko: '[검수용] 답글정책', en: 'Reply Policy E2E' }, + type: 'basic', + use_reply: true, + show_view_count: true, + use_report: false, + board_manager_ids: [managerUuid], + ...extra, + }); +} + +// 두 테스트가 같은 전용 게시판 slug 를 생성/삭제하므로 병렬 실행 시 경합한다 — 직렬 고정 +test.describe.configure({ mode: 'serial' }); + +test.describe('게시판 답글 삭제 정책 (#573)', () => { + test('환경설정 답변글 탭에 정책 Select 가 렌더되고 기본값이 cascade 다', async ({ page, replyPolicyToken }) => { + await authenticatePage(page, replyPolicyToken); + + // 답변글 서브탭은 basic_defaults 탭의 스크롤 섹션으로 함께 렌더된다 (TabNavigationScroll) + await page.goto('/admin/boards/settings?tab=basic_defaults'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + // 필드 라벨 + Select 렌더 (admin_basic Select 는 커스텀 드롭다운 — name 루트로 조회) + await expect(page.getByText('답변글 삭제 방식').first()).toBeVisible({ timeout: 15_000 }); + + // 저장값(기본 cascade)이 응답에 실려 있는지 API 로 대조 + const settings = await api(page, replyPolicyToken, 'GET', `${API}/admin/settings`); + expect(settings.status).toBe(200); + const basicDefaults = + settings.body?.data?.settings?.basic_defaults ?? settings.body?.data?.basic_defaults ?? {}; + expect(basicDefaults.reply_delete_policy ?? 'cascade').toBe('cascade'); + }); + + test('게시판 편집 폼 저장 왕복 — block 저장 후 재조회에 반영된다', async ({ page, replyPolicyToken }) => { + await authenticatePage(page, replyPolicyToken); + await page.goto('/admin/boards'); + await deleteBoardIfExists(page, replyPolicyToken); + + // 전용 게시판 생성 (기본 cascade) + const created = await createBoard(page, replyPolicyToken); + expect([200, 201], `board create failed: ${JSON.stringify(created.body)}`).toContain(created.status); + expect(created.body?.data?.reply_delete_policy ?? 'cascade').toBe('cascade'); + + // 편집 폼에서 필드 렌더 확인 (use_reply ON 조건부) + await page.goto(`/admin/boards/${SLUG}/edit`); + await expect(page.getByText('답변글 삭제 방식').first()).toBeVisible({ timeout: 20_000 }); + + // block 으로 저장 왕복 (API PUT — 폼과 동일 엔드포인트/검증 경로) + const updated = await api(page, replyPolicyToken, 'PUT', `${API}/admin/boards/${SLUG}`, { + reply_delete_policy: 'block', + }); + expect(updated.status).toBe(200); + + const fetched = await api(page, replyPolicyToken, 'GET', `${API}/admin/boards/${SLUG}`); + expect(fetched.body?.data?.reply_delete_policy).toBe('block'); + + // 허용 밖 값은 422 (닫힌 집합) + const invalid = await api(page, replyPolicyToken, 'PUT', `${API}/admin/boards/${SLUG}`, { + reply_delete_policy: 'evil', + }); + expect(invalid.status).toBe(422); + + await deleteBoardIfExists(page, replyPolicyToken); + }); + + test('block 게시판에서 답글 달린 글 삭제는 422 로 거부되고 DB 가 변하지 않는다', async ({ page, replyPolicyToken }) => { + await authenticatePage(page, replyPolicyToken); + await page.goto('/admin/boards'); + await deleteBoardIfExists(page, replyPolicyToken); + + const created = await createBoard(page, replyPolicyToken, { reply_delete_policy: 'block' }); + expect([200, 201], `board create failed: ${JSON.stringify(created.body)}`).toContain(created.status); + + // 부모 글 + 답글 생성 (admin posts API) + const parent = await api(page, replyPolicyToken, 'POST', `${API}/admin/board/${SLUG}/posts`, { + title: '[검수용] 원글', + content: '답글 삭제 정책 검증용 원글입니다.', + }); + expect([200, 201]).toContain(parent.status); + const parentId = parent.body?.data?.id; + expect(parentId).toBeTruthy(); + + const reply = await api(page, replyPolicyToken, 'POST', `${API}/admin/board/${SLUG}/posts`, { + title: 'Re: [검수용] 원글', + content: '답글 삭제 정책 검증용 답글입니다.', + parent_id: parentId, + }); + expect([200, 201], `reply create failed: ${JSON.stringify(reply.body).slice(0, 400)}`).toContain(reply.status); + const replyId = reply.body?.data?.id; + + // block: 원글 삭제 422 + 메시지 + const blocked = await api(page, replyPolicyToken, 'DELETE', `${API}/admin/board/${SLUG}/posts/${parentId}`); + expect(blocked.status).toBe(422); + expect(String(blocked.body?.message ?? '')).toContain('답글'); + + // DB 무변화 — 원글이 여전히 조회된다 + const stillThere = await api(page, replyPolicyToken, 'GET', `${API}/admin/board/${SLUG}/posts/${parentId}`); + expect(stillThere.status).toBe(200); + + // 답글 먼저 삭제하면 원글 삭제가 허용된다 + const replyDeleted = await api(page, replyPolicyToken, 'DELETE', `${API}/admin/board/${SLUG}/posts/${replyId}`); + expect(replyDeleted.status).toBe(200); + + const parentDeleted = await api(page, replyPolicyToken, 'DELETE', `${API}/admin/board/${SLUG}/posts/${parentId}`); + expect(parentDeleted.status).toBe(200); + + await deleteBoardIfExists(page, replyPolicyToken); + }); +}); diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reports-search.spec.ts b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reports-search.spec.ts index 86b0f4f6..9c5de1b9 100644 --- a/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reports-search.spec.ts +++ b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reports-search.spec.ts @@ -13,11 +13,7 @@ * * @scenario board-reports-search * @axes field=all field=post_title field=board_name field=author_name field=reporter_name - * @effects search_keyword_propagated_to_url_query, - * search_field_propagated_to_url_query, - * search_input_value_retained_after_navigation, - * reset_clears_search_keyword, - * mobile_search_propagates_to_url_query + * 효과 요약(마커 아님 — 평문): search_keyword_propagated_to_url_query, search_field_propagated_to_url_query, search_input_value_retained_after_navigation, reset_clears_search_keyword, mobile_search_propagates_to_url_query. * * 활성화 절차: PlaywrightIssueToken 발급이 가능한 환경에서 test.describe.skip → test.describe. */ @@ -104,6 +100,35 @@ test.describe.skip('게시판 신고현황 — 검색 기능 동작 (#413-72)', expect(url.searchParams.get('filters[0][value]')).toBeFalsy(); }); + // @scenario field=reporter_name + // @effects search_field_propagated_to_url_query, search_keyword_propagated_to_url_query, search_input_value_retained_after_navigation + test('검색 필드(신고자명) 선택 + 검색어가 URL query 에 전달되고 재진입 시 복원된다', async ({ + page, + settingsToken, + }) => { + await authenticatePage(page, settingsToken); + await page.goto(REPORTS_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + // 검색 필드 select → reporter_name (게시글 작성자가 아니라 신고한 사람 기준) + await page.locator('#search_field_select select, #search_field_select').first() + .selectOption('reporter_name'); + await page.locator('#search_input input, #search_input').first().fill('김신고'); + await page.locator('#search_button').click(); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const url = new URL(page.url()); + expect(url.searchParams.get('filters[0][field]')).toBe('reporter_name'); + expect(url.searchParams.get('filters[0][value]')).toBe('김신고'); + + // author_name 과 값 공간이 겹치므로, 선택한 필드가 그대로 유지되는지까지 확인한다 + // (필드가 조용히 all/author_name 으로 되돌아가면 다른 행이 걸려 결과가 맞는 것처럼 보인다) + const fieldSelect = page.locator('#search_field_select select, #search_field_select').first(); + await expect(fieldSelect).toHaveValue('reporter_name', { timeout: 5_000 }); + const searchInput = page.locator('#search_input input, #search_input').first(); + await expect(searchInput).toHaveValue('김신고', { timeout: 5_000 }); + }); + // @scenario field=post_title // @effects mobile_search_propagates_to_url_query test('모바일 뷰포트에서 검색어가 URL query 에 전달된다', async ({ page, settingsToken }) => { diff --git a/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-settings-attachment-cleanup.spec.ts b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-settings-attachment-cleanup.spec.ts new file mode 100644 index 00000000..0156f33e --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-settings-attachment-cleanup.spec.ts @@ -0,0 +1,129 @@ +/** + * E2E: 게시판 환경설정 > 기본 설정 — "삭제 첨부 영구 정리" (공개 #115) + * + * @scenario board-settings-attachment-cleanup + * @effects board_settings_purge_toggle_default_off, board_settings_purge_keys_declared + * + * 배경: 삭제한 첨부의 파일을 보존기간 경과 후 완전히 파기하는 운영자 스위치다. 저장 payload + * 빌더가 이 카테고리를 싣지 않아 "저장 200 인데 값이 사라지던" 결함이 있었고, 그 증상은 + * 응답이 정상이라 화면만 봐서는 드러나지 않았다. 단위 테스트는 레이아웃 JSON 의 바인딩만 + * 보므로, 실제 요청 본문에 카테고리가 실리는지는 브라우저로만 확인된다. + * + * 검증: + * 1. 기본 설정 탭에 토글·보존기간 필드가 번역문과 함께 마운트된다 + * 2. 토글은 기본 꺼짐으로 렌더된다 (사용자 파일을 지우는 기능의 인수 기준) + * 3. 값을 바꿔 저장하면 요청 본문에 `attachment_settings` 가 실제로 실린다 + * + */ +import { test, expect, authenticatePage } from '../../fixtures/board-auth'; + +// 탭 판정은 `_global.activeBoardSettingsTab || query.tab || 'general'` 이다. +// 다른 값을 실으면 탭 콘텐츠 자체가 렌더되지 않아 섹션을 찾을 수 없다. +const SETTINGS_URL = '/admin/boards/settings?tab=general'; + +const SECTION = '#attachment_cleanup_section'; +const TOGGLE_ROW = '#attachment_purge_enabled_field'; +const RETENTION_ROW = '#attachment_purge_retention_days_field'; + +/** 설정 저장 PUT 인지 판정한다. */ +const isSettingsPut = (url: string, method: string) => url.includes('/settings') && method === 'PUT'; + +/** + * 보존기간 값을 바꿔 저장하고, 요청 본문을 돌려준다. + * + * 응답 상태까지 확인한다 — 요청만 관찰하면 서버가 4xx 로 거부해도 단언이 통과해 + * "저장됐다" 는 잘못된 결론이 남는다. + * + * @param page 대상 페이지 + * @param input 보존기간 입력 + * @param saveButton 저장 버튼 + * @param value 채워 넣을 값 + * @returns 저장 요청 본문 + */ +async function save( + page: import('@playwright/test').Page, + input: import('@playwright/test').Locator, + saveButton: import('@playwright/test').Locator, + value: string, +): Promise<Record<string, unknown>> { + await input.fill(value); + await expect(saveButton).toBeEnabled({ timeout: 20_000 }); + + const requestPromise = page.waitForRequest( + (req) => isSettingsPut(req.url(), req.method()), + { timeout: 20_000 }, + ); + const responsePromise = page.waitForResponse( + (res) => isSettingsPut(res.url(), res.request().method()), + { timeout: 20_000 }, + ); + + await saveButton.click(); + + const payload = (await requestPromise).postDataJSON() as Record<string, unknown>; + expect((await responsePromise).status()).toBe(200); + + return payload; +} + +test.describe('게시판 환경설정 — 삭제 첨부 영구 정리 (#115)', () => { + test('토글·보존기간 필드가 번역문과 함께 마운트되고 기본 꺼짐이다', async ({ + page, + settingsToken, + }) => { + await authenticatePage(page, settingsToken); + await page.goto(SETTINGS_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + await expect(page.locator(SECTION)).toBeAttached({ timeout: 20_000 }); + await expect(page.locator(TOGGLE_ROW)).toBeAttached(); + await expect(page.locator(RETENTION_ROW)).toBeAttached(); + + const text = (await page.locator(SECTION).innerText()).trim(); + expect(text).not.toContain('$t:'); + expect(text).not.toContain('purge_enabled'); + + const toggle = page.locator(TOGGLE_ROW).locator('input[type="checkbox"]').first(); + expect(await toggle.isChecked()).toBe(false); + }); + + test('저장 요청 본문에 attachment_settings 카테고리가 실린다', async ({ + page, + settingsToken, + }) => { + await authenticatePage(page, settingsToken); + await page.goto(SETTINGS_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + await expect(page.locator(RETENTION_ROW)).toBeAttached({ timeout: 20_000 }); + + const input = page.locator(RETENTION_ROW).locator('input').first(); + const saveButton = page.locator('#footer_save_button'); + const original = (await input.inputValue()) || '30'; + + // 현재값을 그대로 채우면 hasChanges 가 서지 않아 저장 버튼이 비활성인 채로 남고, + // PUT 이 영원히 오지 않아 테스트가 타임아웃한다. 반드시 다른 값으로 바꾼다. + const target = original === '15' ? '20' : '15'; + + const payload = await save(page, input, saveButton, target); + + // 카테고리 자체가 빠지면 서버는 200 을 돌려주고 값만 조용히 사라진다. + expect(payload).toHaveProperty('attachment_settings'); + + // number 입력의 값은 와이어에 문자열로 실리고(서버가 정수로 캐스팅한다) — + // 여기서 검증할 것은 표현 타입이 아니라 "그 값이 실제로 실렸는가" 다. + expect( + Number((payload.attachment_settings as Record<string, unknown>).purge_retention_days), + ).toBe(Number(target)); + + // 요청 본문만 보면 서버가 그 값을 버려도 통과한다. 새로고침 후 값이 살아 있어야 + // "실제로 저장됐다" 가 증명된다 (F4 가 정확히 이 지점에서 200 인 채 값을 잃었다). + await page.reload(); + await expect(input).toHaveValue(target, { timeout: 20_000 }); + + // 검증 때문에 운영 설정이 바뀐 채로 남지 않도록 원래 값으로 되돌린다. + await save(page, input, saveButton, original); + await page.reload(); + await expect(input).toHaveValue(original, { timeout: 20_000 }); + }); +}); diff --git a/modules/_bundled/sirsoft-board/tests/Unit/AttachmentPreviewUrlTest.php b/modules/_bundled/sirsoft-board/tests/Unit/AttachmentPreviewUrlTest.php new file mode 100644 index 00000000..4f18582f --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/AttachmentPreviewUrlTest.php @@ -0,0 +1,46 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit; + +use Modules\Sirsoft\Board\Models\Attachment; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 게시판 첨부 미리보기 URL 조립 단일 지점(previewUrlForSlug) 회귀 테스트 + * + * URL 중복 조립 정리(#100) 후에도 응답 문자열이 기존과 동일함을 고정합니다. + * board 는 직접 URL 배선 대상이 아니므로 문자열 회귀 0 이 계약입니다. + */ +class AttachmentPreviewUrlTest extends ModuleTestCase +{ + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function preview_url_for_slug_builds_board_preview_url(): void + { + $attachment = new Attachment; + $attachment->hash = 'abc123def456'; + + $this->assertSame( + '/api/modules/sirsoft-board/boards/notice/attachment/abc123def456/preview', + $attachment->previewUrlForSlug('notice') + ); + } + + #[Test] + public function preview_url_accessor_delegates_to_single_assembly_point(): void + { + $attachment = new Attachment; + $attachment->hash = 'abc123def456'; + $attachment->mime_type = 'image/png'; + $attachment->board_id = 0; + + // board 관계 미로딩 + 존재하지 않는 board_id → slug 는 빈 문자열로 조립 (기존 동작 보존) + $this->assertSame( + '/api/modules/sirsoft-board/boards//attachment/abc123def456/preview', + $attachment->preview_url + ); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/BoardSetSettingPipelineTest.php b/modules/_bundled/sirsoft-board/tests/Unit/BoardSetSettingPipelineTest.php new file mode 100644 index 00000000..8a917200 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/BoardSetSettingPipelineTest.php @@ -0,0 +1,253 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit; + +use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface; +use App\Models\NotificationDefinition; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Board\Services\BoardPermissionService; +use Modules\Sirsoft\Board\Services\BoardSettingsService; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; + +/** + * 게시판 단건 설정 저장(setSetting)의 파이프라인 경유 / 캐시 정합 테스트 (공개 #114 동종) + * + * 이커머스와 같은 결함에 더해 한 단계 더 나쁜 상태였다 — 저장 후 자기 캐시(`$settings`)를 + * 비우지 않아, 같은 요청 안에서 곧이은 조회가 저장 전 값을 반환했다. 기존 테스트는 + * `clearCache()` 를 수동으로 끼워 넣어 그 결함을 우회하고 있었다. + * + * 벌크 저장의 boolean backfill 은 폼 Toggle-OFF 미전송 대응이라 단건 저장에 그대로 태우면 + * 안 된다 — 제출하지 않은 boolean 이 전부 false 로 박제되어 기본값 true 인 항목이 뒤집힌다. + * 그래서 공통 본문만 공유하고 backfill 여부만 갈라 놓는다. + */ +class BoardSetSettingPipelineTest extends ModuleTestCase +{ + private BoardSettingsService $service; + + private string $storagePath; + + protected function setUp(): void + { + parent::setUp(); + + $permissionService = $this->createMock(BoardPermissionService::class); + $notificationDefinitionRepository = $this->createMock(NotificationDefinitionRepositoryInterface::class); + $notificationDefinitionRepository->method('getByExtension')->willReturn(new Collection); + + $this->service = new BoardSettingsService($permissionService, $notificationDefinitionRepository); + $this->storagePath = storage_path('framework/testing/modules/sirsoft-board/settings'); + + if (File::isDirectory($this->storagePath)) { + File::deleteDirectory($this->storagePath); + } + } + + protected function tearDown(): void + { + if (File::isDirectory($this->storagePath)) { + File::deleteDirectory($this->storagePath); + } + + parent::tearDown(); + } + + /** + * 카테고리 저장 파일을 그대로 읽습니다. + * + * @param string $category 카테고리명 + * @return array 저장 파일의 디코드 결과 (파일 부재 시 빈 배열) + */ + private function savedFile(string $category): array + { + $path = $this->storagePath.'/'.$category.'.json'; + + if (! File::exists($path)) { + return []; + } + + return json_decode(File::get($path), true) ?? []; + } + + /** + * 단건 저장 직후 같은 인스턴스의 조회가 신값을 반환한다. (실패-먼저) + * + * @scenario save_path=single_key + * + * @effects single_key_save_invalidates_service_cache + */ + public function test_single_key_save_invalidates_cache_without_manual_clear(): void + { + // 조회로 캐시를 채운 뒤 저장 + $this->assertNotSame(31, $this->service->getSetting('basic_defaults.per_page')); + + $this->service->setSetting('basic_defaults.per_page', 31); + + $this->assertSame( + 31, + $this->service->getSetting('basic_defaults.per_page'), + '저장 후에도 조회가 저장 전 값을 반환했습니다 (캐시 무효화 누락).' + ); + } + + /** + * 단건 저장은 미제출 boolean 을 false 로 박제하지 않는다. + * + * 벌크 저장의 backfill 은 폼 전체 제출을 전제로 한 보정이므로, 부분 저장에 태우면 + * 기본값 true 인 항목이 통째로 뒤집힌다. + * + * @scenario save_path=single_key + * + * @effects single_key_save_does_not_backfill_booleans + */ + public function test_single_key_save_does_not_backfill_unsubmitted_booleans(): void + { + $this->service->setSetting('basic_defaults.per_page', 25); + + $saved = $this->savedFile('basic_defaults'); + + $this->assertArrayNotHasKey( + 'use_comment', + $saved, + '제출하지 않은 boolean 이 단건 저장으로 박제되었습니다.' + ); + $this->assertTrue( + $this->service->getSetting('basic_defaults.use_comment'), + '기본값 true 인 boolean 설정이 단건 저장으로 뒤집혔습니다.' + ); + } + + /** + * 벌크 저장의 boolean backfill 은 그대로 유지된다. (비회귀 pin) + * + * @scenario save_path=bulk + * + * @effects bulk_save_backfills_unsubmitted_booleans + */ + public function test_bulk_save_still_backfills_unsubmitted_booleans(): void + { + $this->service->saveSettings(['basic_defaults' => ['per_page' => 25]]); + + $saved = $this->savedFile('basic_defaults'); + + $this->assertArrayHasKey('use_comment', $saved, '벌크 저장의 boolean backfill 이 사라졌습니다.'); + $this->assertFalse($saved['use_comment']); + } + + /** + * 신고 정책 단건 저장도 알림 강제 활성 규칙을 거친다. (실패-먼저) + * + * @scenario save_path=single_key + * + * @effects report_notification_flags_forced_on_single_key_save + */ + public function test_single_key_save_forces_report_notification_flags(): void + { + $this->service->setSetting('report_policy', [ + 'auto_hide_threshold' => 7, + 'notify_admin_on_report' => false, + 'notify_author_on_report_action' => false, + ]); + + $saved = $this->savedFile('report_policy'); + + $this->assertTrue( + $saved['notify_admin_on_report'] ?? false, + '신고 알림 강제 활성 규칙이 단건 저장 경로에서 적용되지 않았습니다.' + ); + $this->assertTrue($saved['notify_author_on_report_action'] ?? false); + $this->assertSame(7, $saved['auto_hide_threshold'] ?? null); + } + + /** + * 신고 정책 단건 저장은 알림 정의의 활성 상태까지 동기화한다. + * + * 저장 파일만 보면 강제 활성 규칙이 통과한 것처럼 보인다. 그러나 알림 정의가 비활성인 + * 채로 남으면 `NotificationHookListener` 가 훅을 구독하지 않아 설정은 켜져 있는데 + * 알림만 오지 않는다 — 화면에도 로그에도 흔적이 없다. + * + * @scenario save_path=single_key + * + * @effects report_notification_flags_forced_on_single_key_save + */ + public function test_single_key_save_syncs_notification_definition_status(): void + { + $definition = new NotificationDefinition(['type' => 'report_received_admin']); + $definition->is_active = false; + + $repository = $this->createMock(NotificationDefinitionRepositoryInterface::class); + $repository->method('getByExtension')->willReturn(new Collection([$definition])); + $repository->expects($this->once()) + ->method('update') + ->with($definition, ['is_active' => true]); + + $service = new BoardSettingsService($this->createMock(BoardPermissionService::class), $repository); + + $service->setSetting('report_policy', [ + 'auto_hide_threshold' => 7, + 'notify_admin_on_report' => false, + ]); + } + + /** + * 이미 원하는 상태인 알림 정의는 다시 쓰지 않는다. (불필요 쓰기 차단) + * + * @scenario save_path=single_key + * + * @effects report_notification_flags_forced_on_single_key_save + */ + public function test_single_key_save_skips_notification_definition_update_when_already_active(): void + { + $definition = new NotificationDefinition(['type' => 'report_received_admin']); + $definition->is_active = true; + + $repository = $this->createMock(NotificationDefinitionRepositoryInterface::class); + $repository->method('getByExtension')->willReturn(new Collection([$definition])); + $repository->expects($this->never())->method('update'); + + $service = new BoardSettingsService($this->createMock(BoardPermissionService::class), $repository); + + $service->setSetting('report_policy', [ + 'auto_hide_threshold' => 7, + 'notify_admin_on_report' => true, + ]); + } + + /** + * 단건 저장도 defaults 스키마 기준 숫자 정규화를 거친다. (실패-먼저) + * + * @scenario save_path=single_key + * + * @effects numeric_string_normalized_on_single_key_save + */ + public function test_single_key_save_normalizes_numeric_strings(): void + { + $this->service->setSetting('basic_defaults.per_page', '42'); + + $saved = $this->savedFile('basic_defaults'); + + $this->assertSame(42, $saved['per_page'] ?? null, '숫자 문자열이 정규화되지 않고 저장되었습니다.'); + } + + /** + * 테스트 실행 중에는 운영 설정 경로를 쓰지 않는다. (실패-먼저) + * + * @effects board_settings_storage_isolated_in_tests + */ + public function test_storage_path_is_isolated_during_tests(): void + { + $productionPath = storage_path('app/modules/sirsoft-board/settings/basic_defaults.json'); + // 운영 파일의 존재 여부·내용을 그대로 스냅샷 (환경마다 상태가 다르므로 변화 없음만 단언) + $before = File::exists($productionPath) ? File::get($productionPath) : null; + + $this->service->setSetting('basic_defaults.per_page', 33); + + $this->assertFileExists( + $this->storagePath.'/basic_defaults.json', + '테스트 격리 경로에 저장되지 않았습니다.' + ); + + $after = File::exists($productionPath) ? File::get($productionPath) : null; + $this->assertSame($before, $after, '테스트가 운영 설정 파일을 생성/변경했습니다.'); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/BoardSettingsServiceTest.php b/modules/_bundled/sirsoft-board/tests/Unit/BoardSettingsServiceTest.php index 66556251..08d319fd 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/BoardSettingsServiceTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/BoardSettingsServiceTest.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Board\Tests\Unit; use App\Contracts\Repositories\NotificationDefinitionRepositoryInterface; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\File; use Modules\Sirsoft\Board\Services\BoardPermissionService; use Modules\Sirsoft\Board\Services\BoardSettingsService; @@ -27,9 +28,10 @@ class BoardSettingsServiceTest extends ModuleTestCase $notificationDefinitionRepository = $this->createMock(NotificationDefinitionRepositoryInterface::class); // report_policy 저장 시 호출되는 알림 정의 동기화 경로가 빈 컬렉션을 안전하게 다루도록 기본 stub $notificationDefinitionRepository->method('getByExtension') - ->willReturn(new \Illuminate\Database\Eloquent\Collection()); + ->willReturn(new Collection); $this->service = new BoardSettingsService($permissionService, $notificationDefinitionRepository); - $this->storagePath = storage_path('app/modules/sirsoft-board/settings'); + // 테스트 격리 경로 — 운영 설정(storage/app/modules/...)을 지우거나 덮어쓰지 않는다 + $this->storagePath = storage_path('framework/testing/modules/sirsoft-board/settings'); // 테스트 전 저장소 정리 if (File::isDirectory($this->storagePath)) { @@ -213,8 +215,7 @@ class BoardSettingsServiceTest extends ModuleTestCase $this->assertTrue($result); - // 캐시 초기화 후 다시 조회 - $this->service->clearCache(); + // 단건 저장이 자기 캐시를 비우므로 수동 초기화 없이 신값이 조회된다 (공개 #114 동종) $perPage = $this->service->getSetting('basic_defaults.per_page'); $this->assertEquals(30, $perPage); } @@ -359,8 +360,17 @@ class BoardSettingsServiceTest extends ModuleTestCase // 첫 번째 조회로 캐시 생성 $first = $this->service->getAllSettings(); - // 파일 직접 수정 (캐시 우회) - $this->service->setSetting('basic_defaults.per_page', 77); + // 파일 직접 수정 (서비스를 경유하지 않아 캐시가 갱신되지 않는 상태를 만든다) + File::ensureDirectoryExists($this->storagePath); + File::put( + $this->storagePath.'/basic_defaults.json', + json_encode(['per_page' => 77], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + $this->assertEquals( + $first['basic_defaults']['per_page'], + $this->service->getAllSettings()['basic_defaults']['per_page'], + '캐시 초기화 전인데 파일 변경이 조회에 반영되었습니다.' + ); // 캐시 초기화 $this->service->clearCache(); diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php new file mode 100644 index 00000000..192eefee --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php @@ -0,0 +1,60 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit\Listeners; + +use Modules\Sirsoft\Board\Listeners\Ckeditor5ReferenceSourcesListener; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 게시판 참조 소스 등록 리스너 테스트 + * + * 이 리스너가 빠지면 게시글에만 쓰이는 이미지가 "미참조" 로 판정돼 자동 정리 대상이 되므로, + * 구독 형식(filter)과 append 계약을 회귀로 고정합니다. + */ +class Ckeditor5ReferenceSourcesListenerTest extends ModuleTestCase +{ + /** + * @effects reference_source_listener_subscribes_as_filter + */ + #[Test] + public function it_subscribes_to_the_reference_source_filter_hook(): void + { + $hooks = Ckeditor5ReferenceSourcesListener::getSubscribedHooks(); + + $this->assertArrayHasKey('sirsoft-ckeditor5.image.filter_reference_sources', $hooks); + $this->assertSame('filter', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['type']); + $this->assertSame('addBoardSources', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['method']); + } + + /** + * @effects reference_source_listener_appends_without_dropping + */ + #[Test] + public function it_appends_board_sources_while_preserving_existing_ones(): void + { + $existing = [['table' => 'mail_templates', 'columns' => ['body']]]; + + $result = (new Ckeditor5ReferenceSourcesListener)->addBoardSources($existing); + + $this->assertContains($existing[0], $result); + $this->assertContains(['table' => 'board_posts', 'columns' => ['content']], $result); + } + + /** + * 선언 형태가 계약(table/columns)을 지켜야 플러그인 판정부가 해석할 수 있다. + * + * @effects reference_source_listener_schema_contract + */ + #[Test] + public function appended_sources_follow_the_declared_schema(): void + { + foreach ((new Ckeditor5ReferenceSourcesListener)->addBoardSources([]) as $source) { + $this->assertArrayHasKey('table', $source); + $this->assertArrayHasKey('columns', $source); + $this->assertIsString($source['table']); + $this->assertIsArray($source['columns']); + $this->assertNotEmpty($source['columns']); + } + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php index fdefc73a..a9025fa3 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php @@ -4,6 +4,8 @@ namespace Modules\Sirsoft\Board\Tests\Unit\Listeners; require_once __DIR__.'/../../ModuleTestCase.php'; +use App\Models\User; +use Illuminate\Http\Request; use Modules\Sirsoft\Board\Listeners\EcommerceInquiryHookListener; use Modules\Sirsoft\Board\Models\Board; use Modules\Sirsoft\Board\Models\Post; @@ -13,12 +15,14 @@ use Modules\Sirsoft\Board\Tests\BoardTestCase; * EcommerceInquiryHookListener 단위 테스트 * * 검증 목적: - * - getSubscribedHooks: 7개 훅 등록, 모두 filter 타입 - * - createAndReturn: 성공(post_id/inquirable_type 반환), title 자동생성, parent_id Re: 처리, 예외 시 null + * - getSubscribedHooks: 8개 훅 등록, 모두 filter 타입 + * - createAndReturn: 성공(post_id/inquirable_type 반환), title 자동생성, parent_id Re: 처리, 예외 시 null, + * 기존 살아있는 답변 존재 시 중복 생성 차단(duplicate 마커), 답변 soft delete 후 재등록 허용 * - getByIds: 빈 배열 → carry 그대로, 유효 ID 목록 → 필드 포함 배열 반환 * - getBoardSettings: 존재하지 않는 slug → carry 그대로, 유효 slug → 설정 배열 반환 - * - deletePost: 성공 → carry 반환, 존재하지 않는 PostId → RuntimeException + * - deletePost: 성공 → carry 반환, 답글 자식까지 cascade soft delete, 존재하지 않는 PostId → RuntimeException * - deleteReplyPost: reply 없음 → RuntimeException, 성공 → carry 반환 + * - countReplies: 살아있는 자식만 집계 * * @group board * @group unit @@ -63,9 +67,9 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase // ========================================== /** - * 7개 훅이 모두 등록되어 있고, 모두 filter 타입이어야 합니다. + * 8개 훅이 모두 등록되어 있고, 모두 filter 타입이어야 합니다. */ - public function test_subscribed_hooks_registers_all_seven_filter_hooks(): void + public function test_subscribed_hooks_registers_all_eight_filter_hooks(): void { $hooks = EcommerceInquiryHookListener::getSubscribedHooks(); @@ -77,6 +81,7 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase 'sirsoft-ecommerce.inquiry.delete_reply', 'sirsoft-ecommerce.inquiry.get_by_ids', 'sirsoft-ecommerce.inquiry.get_settings', + 'sirsoft-ecommerce.inquiry.count_replies', ]; foreach ($expectedHooks as $hookName) { @@ -84,7 +89,7 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertSame('filter', $hooks[$hookName]['type'], "훅 {$hookName}은 filter 타입이어야 합니다."); } - $this->assertCount(7, $hooks, '총 7개의 훅이 등록되어 있어야 합니다.'); + $this->assertCount(8, $hooks, '총 8개의 훅이 등록되어 있어야 합니다.'); } // ========================================== @@ -146,6 +151,60 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertStringContainsString('원본 문의 제목', $post->title); } + /** + * createAndReturn: 저장되는 ip_address 는 호출 서비스가 payload 로 넘긴 값이어야 한다. + * 요청 경계인 ProductInquiryService 가 IP 를 주입하고 Listener 는 그것을 그대로 쓴다. + * + * 입력 경계는 viewer 축과 교차하지 않는 별개 관심사라 매니페스트 sub_flow + * (`listener_ip_boundary`)로 두고 여기서는 효과만 마킹한다 — 매니페스트 axes 에 없는 + * 축을 @scenario 에 적으면 어떤 cross product 조합도 커버하지 못하는 죽은 마커가 된다. + * + * @effects listener_uses_service_provided_ip_not_request + */ + public function test_create_and_return_persists_ip_from_payload(): void + { + $result = $this->listener->createAndReturn(null, $this->board->slug, [ + 'title' => '문의', + 'content' => '내용', + 'ip_address' => '198.51.100.7', + ]); + + $post = Post::find($result['post_id']); + $this->assertSame('198.51.100.7', $post->ip_address, 'Listener 는 payload 의 ip_address 를 그대로 저장해야 합니다'); + } + + /** + * createAndReturn: payload 에 ip_address 가 없으면 '0.0.0.0' 폴백을 쓰고 request()->ip() 로 + * 되돌아가지 않는다(입력 우회 방지 경계 — 정정2 회귀 가드). + * + * request IP 를 **비-0.0.0.0** 으로 세팅한 상태에서 폴백값(0.0.0.0)이 나와야 한다. Listener 가 + * request()->ip() 를 재도입하면 이 테스트가 request IP('203.0.113.99')를 잡아 fail 한다. + * (테스트 기본 request IP 0.0.0.0 으로는 재도입해도 폴백과 같아 silent green — 그래서 비-0 IP 사용) + * + * 축이 아닌 sub_flow (`listener_ip_boundary`) 소속이라 효과만 마킹한다. + * + * @effects listener_does_not_reach_into_request_for_ip + */ + public function test_create_and_return_does_not_fall_back_to_request_ip(): void + { + $request = Request::create('/'.$this->board->slug, 'POST', server: ['REMOTE_ADDR' => '203.0.113.99']); + $this->app->instance('request', $request); + $this->assertSame('203.0.113.99', $request->ip(), '테스트 전제: request IP 가 비-0.0.0.0 이어야 재도입을 잡는다'); + + $result = $this->listener->createAndReturn(null, $this->board->slug, [ + 'title' => '문의', + 'content' => '내용', + // ip_address 의도적 누락 + ]); + + $post = Post::find($result['post_id']); + $this->assertSame( + '0.0.0.0', + $post->ip_address, + 'ip_address 미제공 시 폴백(0.0.0.0)이어야 하며 request()->ip() 로 되돌아가면 안 됩니다' + ); + } + /** * createAndReturn: 존재하지 않는 slug → 예외 발생하지 않고 null 반환 */ @@ -160,6 +219,65 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertNull($result); } + /** + * createAndReturn: 부모에 이미 살아있는 답변이 있으면 중복 생성 차단 → null 반환 + * + * 2차 방어(게시판 실데이터 기준): 피벗 is_answered 플래그가 어긋난 경우에도 + * 게시판에 중복 답변이 쌓이지 않아야 합니다. + */ + public function test_create_and_return_returns_duplicate_marker_when_parent_already_has_reply(): void + { + $parentPostId = $this->createTestPost(['title' => '원본 문의']); + $this->createTestPost([ + 'title' => 'Re: 원본 문의', + 'parent_id' => $parentPostId, + 'depth' => 1, + ]); + + $result = $this->listener->createAndReturn(null, $this->board->slug, [ + 'content' => '두 번째 답변 시도', + 'parent_id' => $parentPostId, + 'ip_address' => '127.0.0.1', + ]); + + // null(무응답/실패)이 아닌 중복 마커 — 호출 서비스가 "이미 등록된 답변" 사유로 + // 변환한다. null 로 되돌리면 사유가 generic 실패로 위장된다 (운영 실측 제보 회귀). + $this->assertSame( + ['duplicate' => true], + $result, + '살아있는 답변이 이미 있으면 중복 마커로 차단되어야 합니다.' + ); + } + + /** + * createAndReturn: 기존 답변이 soft delete 된 뒤에는 답변 재등록이 허용된다. + * + * 중복 차단 판정은 살아있는 답변만 대상으로 해야 합니다 (SoftDeletes 기본 스코프). + */ + public function test_create_and_return_allows_reply_after_previous_reply_soft_deleted(): void + { + $parentPostId = $this->createTestPost(['title' => '원본 문의']); + $replyPostId = $this->createTestPost([ + 'title' => 'Re: 원본 문의', + 'parent_id' => $parentPostId, + 'depth' => 1, + ]); + + // 기존 답변 soft delete + Post::find($replyPostId)->delete(); + + $result = $this->listener->createAndReturn(null, $this->board->slug, [ + 'content' => '삭제 후 재등록 답변', + 'parent_id' => $parentPostId, + 'ip_address' => '127.0.0.1', + ]); + + $this->assertIsArray($result, '기존 답변이 삭제된 뒤에는 답변 재등록이 허용되어야 합니다.'); + $this->assertArrayHasKey('post_id', $result); + $this->assertIsInt($result['post_id']); + $this->assertNotSame($replyPostId, $result['post_id']); + } + // ========================================== // getByIds // ========================================== @@ -214,6 +332,68 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertSame($postId, $result[0]['id']); } + /** + * getByIds: 비밀글은 비열람자에게 content/title/reply/attachments 가 마스킹된다 (KVE-2026-1914 A-1) + * + * @scenario layer=hook, viewer=non_viewer + * + * @effects hook_masks_secret_post_for_non_viewer, hook_emits_authoritative_can_view_secret_flag + */ + public function test_get_by_ids_masks_secret_post_for_non_viewer(): void + { + $owner = User::factory()->create(); + $postId = $this->createTestPost([ + 'title' => '비밀 문의 제목', + 'content' => '비밀 문의 내용', + 'is_secret' => true, + 'user_id' => $owner->id, + 'author_name' => 'owner', + ]); + + // 비인증(비열람자) 컨텍스트 + $result = $this->listener->getByIds([], ['ids' => [$postId], 'slug' => $this->board->slug]); + + $this->assertCount(1, $result); + $item = $result[0]; + $this->assertNull($item['content'], '비밀글 content 는 비열람자에게 null 이어야 합니다'); + $this->assertNotSame('비밀 문의 제목', $item['title'], '비밀글 title 은 플레이스홀더로 대체되어야 합니다'); + $this->assertNull($item['reply'], '비밀글 reply 는 비열람자에게 null 이어야 합니다'); + $this->assertSame([], $item['attachments'], '비밀글 attachments 는 비열람자에게 빈 배열이어야 합니다'); + $this->assertTrue($item['is_secret']); + // 소비 서비스의 이중 방어(A-2)를 위해 서버 열람 판정을 함께 실어 보낸다 + $this->assertFalse($item['can_view_secret'], '비열람자에게 can_view_secret 은 false 여야 합니다'); + } + + /** + * getByIds: 작성자 본인에게는 비밀글 원문이 그대로 노출된다 (회귀 방지) + * + * @scenario layer=hook, viewer=owner + * + * @effects hook_exposes_secret_post_to_owner + */ + public function test_get_by_ids_shows_secret_post_to_owner(): void + { + $owner = User::factory()->create(); + $postId = $this->createTestPost([ + 'title' => '내 비밀 문의', + 'content' => '내 비밀 내용', + 'is_secret' => true, + 'user_id' => $owner->id, + 'author_name' => 'owner', + ]); + + $this->actingAs($owner, 'sanctum'); + $result = $this->listener->getByIds([], ['ids' => [$postId], 'slug' => $this->board->slug]); + + $this->assertSame('내 비밀 내용', $result[0]['content']); + $this->assertSame('내 비밀 문의', $result[0]['title']); + $this->assertTrue($result[0]['can_view_secret'], '작성자 본인에게 can_view_secret 은 true 여야 합니다'); + } + + // 게시판 manager 의 비밀글 원문 열람은 HTTP 컨텍스트(라우트 slug)가 필요한 + // PermissionMiddleware 경로라, SecretPostAttachmentAccessTest·SecretPostCommentAccessTest + // 에서 실제 요청으로 검증한다(여기서는 소유자/비열람자 마스킹만 단위 검증). + // ========================================== // getBoardSettings // ========================================== @@ -266,6 +446,27 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertSoftDeleted('board_posts', ['id' => $postId]); } + /** + * deletePost: 부모 문의 삭제 시 답글 자식도 함께 cascade soft delete 된다. + * + * 훅 경유 삭제는 cascade_replies 옵션으로 게시판 답글 삭제 정책과 무관하게 + * 시스템 생성 답변을 함께 정리해야 합니다. + */ + public function test_delete_post_soft_deletes_reply_children_too(): void + { + $parentPostId = $this->createTestPost(['title' => '원본 문의']); + $replyPostId = $this->createTestPost([ + 'title' => 'Re: 원본 문의', + 'parent_id' => $parentPostId, + 'depth' => 1, + ]); + + $this->listener->deletePost(null, $this->board->slug, $parentPostId); + + $this->assertSoftDeleted('board_posts', ['id' => $parentPostId]); + $this->assertSoftDeleted('board_posts', ['id' => $replyPostId]); + } + /** * deletePost: 존재하지 않는 postId → RuntimeException */ @@ -306,4 +507,34 @@ class EcommerceInquiryHookListenerTest extends BoardTestCase $this->assertSame($carry, $result); $this->assertSoftDeleted('board_posts', ['id' => $replyPostId]); } + + // ========================================== + // countReplies + // ========================================== + + /** + * countReplies: 살아있는 자식만 집계한다 (soft delete 된 답변 제외). + */ + public function test_count_replies_counts_only_live_children(): void + { + $parentPostId = $this->createTestPost(['title' => '원본 문의']); + $this->createTestPost([ + 'title' => 'Re: 살아있는 답변', + 'parent_id' => $parentPostId, + 'depth' => 1, + ]); + $trashedReplyId = $this->createTestPost([ + 'title' => 'Re: 삭제될 답변', + 'parent_id' => $parentPostId, + 'depth' => 1, + ]); + + Post::find($trashedReplyId)->delete(); + + $this->assertSame( + 1, + $this->listener->countReplies(0, $parentPostId), + 'soft delete 된 답변은 집계에서 제외되어야 합니다.' + ); + } } diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php index 35e3724c..3041f24a 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php @@ -7,15 +7,16 @@ use App\Models\User; use App\Support\Query\BoundedPage; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Board\Listeners\SearchPostsListener; use Modules\Sirsoft\Board\Services\BoardService; use Modules\Sirsoft\Board\Services\PostService; -use Tests\TestCase; +use Modules\Sirsoft\Board\Tests\ModuleTestCase; /** * SearchPostsListener 단위 테스트 — 게시판별 권한 필터링 및 날짜 포맷 검증 */ -class SearchPostsListenerTest extends TestCase +class SearchPostsListenerTest extends ModuleTestCase { /** * 검색 결과 페이지(BoundedPage)를 만듭니다. @@ -224,6 +225,109 @@ class SearchPostsListenerTest extends TestCase $this->assertNotEmpty($item['created_at_formatted']); } + /** + * 검색 실패가 "결과 0건" 으로 위장되지 않고 failed 페이로드로 표면화되는지 확인 (#103) + * + * 수정 전에는 catch 가 로그만 남기고 카테고리 키를 설정하지 않아, 화면이 + * "검색 결과가 없습니다" 를 그렸다. + * + * @effects failed_flag_in_response, exception_stack_logged + */ + public function test_search_failure_surfaces_failed_payload_and_logs_exception(): void + { + Log::spy(); + + $exception = new \RuntimeException('DB 오류 재현'); + $this->boardService + ->method('getActiveBoardsForSearch') + ->willThrowException($exception); + + $context = [ + 'type' => 'posts', + 'q' => '문의', + 'sort' => 'relevance', + 'page' => 1, + 'per_page' => 10, + 'user' => null, + 'request' => null, + ]; + + $result = $this->listener->searchPosts([], $context); + + $this->assertArrayHasKey('posts', $result); + $this->assertTrue($result['posts']['failed'] ?? false, '실패 카테고리에는 failed 플래그가 실려야 합니다.'); + $this->assertFalse($result['posts']['total_is_exact'], '실패한 0건을 "정확한 0건" 으로 말하면 안 됩니다.'); + $this->assertSame([], $result['posts']['items']); + $this->assertSame([], $result['posts']['available_boards'], '기존 응답 키 집합(available_boards)은 유지되어야 합니다.'); + + Log::shouldHaveReceived('error') + ->withArgs(fn (string $message, array $ctx = []) => ($ctx['exception'] ?? null) === $exception) + ->once(); + } + + /** + * 제목/본문에 삽입된 태그가 하이라이트 필드에서 이스케이프되는지 확인 (⑧/N-8) + * + * @scenario case=search_highlight_escape + * + * @effects highlighted_fields_escaped + */ + public function test_format_post_result_escapes_markup_in_highlighted_fields(): void + { + $user = User::factory()->make(['id' => 9999]); + + $board = $this->createBoardStub(1, 'notice', '공지사항'); + + $this->boardService + ->method('getActiveBoardsForSearch') + ->willReturn(new Collection([$board])); + + $this->boardService + ->method('getActiveBoardsListForFilter') + ->willReturn([]); + + $post = (object) [ + 'id' => 1, + 'title' => '<img src=x onerror=alert(1)> 테스트', + // 엔티티로 인코딩된 태그가 html 모드 프리뷰에서 부활하면 안 된다 (N-8). + 'content' => '<script>alert(1)</script> 테스트 본문', + 'content_mode' => 'html', + 'author_name' => '작성자', + 'created_at' => now(), + 'view_count' => 0, + 'comments_count' => 0, + 'user' => null, + 'board' => $this->createBoardStub(1, 'notice', '공지사항'), + ]; + + $this->postService + ->method('searchAcrossBoards') + ->willReturn($this->boundedPage(new Collection([$post]), 1)); + + Gate::before(fn ($u) => $u->id === 9999 ? true : null); + + $result = $this->listener->searchPosts([], [ + 'type' => 'all', + 'q' => '테스트', + 'sort' => 'relevance', + 'page' => 1, + 'per_page' => 10, + 'user' => $user, + 'request' => null, + ]); + + $item = $result['posts']['items'][0]; + + // 제목 하이라이트: 태그 이스케이프 + 검색어만 <mark> + $this->assertStringNotContainsString('<img', $item['title_highlighted']); + $this->assertStringContainsString('<img', $item['title_highlighted']); + $this->assertStringContainsString('<mark>테스트</mark>', $item['title_highlighted']); + + // 본문 프리뷰: 부활한 <script> 없음 + $this->assertStringNotContainsString('<script>', $item['content_preview']); + $this->assertStringNotContainsString('<script>', $item['content_preview_highlighted']); + } + /** * id를 포함하는 Board 스텁 생성 * diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Repositories/BoardRepositoryPopularAvatarTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/BoardRepositoryPopularAvatarTest.php new file mode 100644 index 00000000..5f8153b2 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/BoardRepositoryPopularAvatarTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit\Repositories; + +use App\Models\Attachment as CoreAttachment; +use App\Models\User; +use Modules\Sirsoft\Board\Repositories\BoardRepository; +use Modules\Sirsoft\Board\Tests\BoardTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 인기 게시물 아바타 URL 호출측 회귀 테스트 (#100 URL 조립 단일화) + * + * urlForHash() 헬퍼 자체가 아니라 **호출측**(BoardRepository getPopularPosts)이 + * 조인으로 얻은 hash 를 코어 단일 지점에 위임해 응답 문자열이 기존과 동일하게 + * 직렬화되는지 고정합니다. + */ +class BoardRepositoryPopularAvatarTest extends BoardTestCase +{ + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function popular_posts_avatar_serializes_core_attachment_url(): void + { + $user = User::factory()->create(); + + $avatar = CoreAttachment::create([ + 'attachmentable_type' => User::class, + 'attachmentable_id' => $user->id, + 'original_filename' => 'avatar.png', + 'stored_filename' => 'stored-avatar.png', + 'disk' => 'local', + 'path' => 'avatars/avatar.png', + 'mime_type' => 'image/png', + 'size' => 100, + 'collection' => 'avatar', + ]); + + $postId = $this->createTestPost([ + 'user_id' => $user->id, + 'view_count' => 99, + 'comments_count' => 0, + 'replies_count' => 0, + 'attachments_count' => 0, + ]); + + $posts = app(BoardRepository::class)->getPopularPosts('year', 10); + + $target = collect($posts)->firstWhere('id', $postId); + $this->assertNotNull($target, '생성한 게시글이 인기 게시물 목록에 있어야 합니다'); + $this->assertSame('/api/attachment/'.$avatar->hash, $target['author']['avatar']); + } + + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function popular_posts_avatar_is_null_without_avatar_attachment(): void + { + $user = User::factory()->create(); + + $postId = $this->createTestPost([ + 'user_id' => $user->id, + 'view_count' => 98, + 'comments_count' => 0, + 'replies_count' => 0, + 'attachments_count' => 0, + ]); + + $posts = app(BoardRepository::class)->getPopularPosts('year', 10); + + $target = collect($posts)->firstWhere('id', $postId); + $this->assertNotNull($target); + $this->assertNull($target['author']['avatar']); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Repositories/PostRepositoryReplyLookupTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/PostRepositoryReplyLookupTest.php new file mode 100644 index 00000000..a843c462 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/Repositories/PostRepositoryReplyLookupTest.php @@ -0,0 +1,149 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit\Repositories; + +// ModuleTestCase 수동 로드 (autoload 전에 로드 필요) +require_once __DIR__.'/../../ModuleTestCase.php'; + +use Modules\Sirsoft\Board\Repositories\PostRepository; +use Modules\Sirsoft\Board\Tests\BoardTestCase; + +/** + * PostRepository 답글 조회 메서드 테스트 + * + * 검증 목적: + * - countRepliesByParentId: 살아있는 직계 자식만 집계 (SoftDeletes 기본 스코프) + * - hasAliveReplies: 살아있는 직계 답글 존재 여부 판정 (답글 삭제 정책 block 의 차단 기준) + * - 직계 살아있는 답글 존재 → true + * - 직계 답글 전부 trashed → false + * - 직계는 trashed 이고 손자만 살아있음(끊긴 체인 고아) → false (직계만 판정) + * + * @group board + * @group unit + * @group repository + */ +class PostRepositoryReplyLookupTest extends BoardTestCase +{ + private PostRepository $repository; + + protected function getTestBoardSlug(): string + { + return 'post-repository-reply-lookup'; + } + + protected function setUp(): void + { + parent::setUp(); + + $this->repository = app(PostRepository::class); + } + + // ========================================== + // countRepliesByParentId + // ========================================== + + /** + * countRepliesByParentId: 살아있는 자식만 집계한다 (trashed 자식 제외). + */ + public function test_count_replies_by_parent_id_counts_only_live_children(): void + { + $parentId = $this->createTestPost(['title' => '부모 글']); + + $this->createTestPost([ + 'title' => '살아있는 답글', + 'parent_id' => $parentId, + 'depth' => 1, + ]); + $this->createTestPost([ + 'title' => '삭제된 답글', + 'parent_id' => $parentId, + 'depth' => 1, + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + + $this->assertSame( + 1, + $this->repository->countRepliesByParentId($parentId), + 'trashed 자식은 집계에서 제외되어야 합니다.' + ); + } + + // ========================================== + // hasAliveReplies + // ========================================== + + /** + * hasAliveReplies: 살아있는 직계 답글이 있으면 true. + */ + public function test_has_alive_replies_returns_true_for_direct_live_reply(): void + { + $parentId = $this->createTestPost(['title' => '부모 글']); + $this->createTestPost([ + 'title' => '살아있는 답글', + 'parent_id' => $parentId, + 'depth' => 1, + ]); + + $this->assertTrue($this->repository->hasAliveReplies($this->board->slug, $parentId)); + } + + /** + * hasAliveReplies: 직계 답글이 전부 trashed 이면 false. + */ + public function test_has_alive_replies_returns_false_when_all_replies_trashed(): void + { + $parentId = $this->createTestPost(['title' => '부모 글']); + $this->createTestPost([ + 'title' => '삭제된 답글 1', + 'parent_id' => $parentId, + 'depth' => 1, + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + $this->createTestPost([ + 'title' => '삭제된 답글 2', + 'parent_id' => $parentId, + 'depth' => 1, + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + + $this->assertFalse( + $this->repository->hasAliveReplies($this->board->slug, $parentId), + '직계 답글이 전부 trashed 이면 살아있는 답글이 없는 것으로 판정되어야 합니다.' + ); + } + + /** + * hasAliveReplies: 직계는 trashed 이고 손자만 살아있으면 false (직계만 판정). + * + * 끊긴 체인 밑 고아 손자는 별도로 cascade 스윕이 정리하며, block 정책 차단 + * 판정은 직계 검사만으로 완결됩니다. + */ + public function test_has_alive_replies_returns_false_when_only_grandchild_alive(): void + { + $parentId = $this->createTestPost(['title' => '부모 글']); + $trashedChildId = $this->createTestPost([ + 'title' => '삭제된 직계 답글', + 'parent_id' => $parentId, + 'depth' => 1, + 'status' => 'deleted', + 'trigger_type' => 'user', + 'deleted_at' => now()->subHour(), + ]); + $this->createTestPost([ + 'title' => '살아있는 손자 답글', + 'parent_id' => $trashedChildId, + 'depth' => 2, + ]); + + $this->assertFalse( + $this->repository->hasAliveReplies($this->board->slug, $parentId), + '손자만 살아있는 경우 직계 판정은 false 여야 합니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Resources/PostResourceThumbnailUrlTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Resources/PostResourceThumbnailUrlTest.php new file mode 100644 index 00000000..38120b6d --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/Unit/Resources/PostResourceThumbnailUrlTest.php @@ -0,0 +1,78 @@ +<?php + +namespace Modules\Sirsoft\Board\Tests\Unit\Resources; + +use Illuminate\Http\Request; +use Modules\Sirsoft\Board\Http\Resources\PostResource; +use Modules\Sirsoft\Board\Models\Attachment; +use Modules\Sirsoft\Board\Models\Post; +use Modules\Sirsoft\Board\Tests\BoardTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * PostResource 썸네일 URL 호출측 회귀 테스트 (#100 URL 조립 단일화) + * + * previewUrlForSlug() 헬퍼 자체가 아니라 **호출측**(PostResource + * getThumbnailUrlFromRelations)이 slug 인자를 올바르게 전달해 응답 문자열이 + * 기존과 동일하게 직렬화되는지 고정합니다 — 헬퍼 출력만 고정하면 인자 전달 + * 오류(슬러그 누락 등)가 전부 green 으로 통과합니다. + */ +class PostResourceThumbnailUrlTest extends BoardTestCase +{ + /** + * 이미지 첨부를 가진 게시글을 생성합니다. + * + * @param string $mimeType 첨부 MIME 타입 + * @return array{post: Post, attachment: Attachment} 게시글/첨부 + */ + private function createPostWithAttachment(string $mimeType): array + { + $postId = $this->createTestPost(); + + $attachment = Attachment::create([ + 'board_id' => $this->board->id, + 'post_id' => $postId, + 'original_filename' => 'thumb.jpg', + 'stored_filename' => 'stored-thumb.jpg', + 'disk' => 'modules', + 'path' => 'attachments/thumb.jpg', + 'mime_type' => $mimeType, + 'size' => 100, + 'collection' => 'attachments', + 'order' => 0, + ]); + + $post = Post::with(['thumbnailAttachment', 'board'])->findOrFail($postId); + + return ['post' => $post, 'attachment' => $attachment]; + } + + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function thumbnail_serializes_preview_url_with_board_slug(): void + { + ['post' => $post, 'attachment' => $attachment] = $this->createPostWithAttachment('image/jpeg'); + + $response = (new PostResource($post))->toArray(Request::create('/')); + + $this->assertSame( + '/api/modules/sirsoft-board/boards/'.$this->board->slug.'/attachment/'.$attachment->hash.'/preview', + $response['thumbnail'], + ); + } + + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function thumbnail_is_null_for_non_image_attachment(): void + { + ['post' => $post] = $this->createPostWithAttachment('application/pdf'); + + $response = (new PostResource($post))->toArray(Request::create('/')); + + $this->assertNull($response['thumbnail']); + } +} diff --git a/modules/_bundled/sirsoft-board/tests/Unit/Traits/ChecksBoardPermissionTest.php b/modules/_bundled/sirsoft-board/tests/Unit/Traits/ChecksBoardPermissionTest.php index bd599147..66c64aff 100644 --- a/modules/_bundled/sirsoft-board/tests/Unit/Traits/ChecksBoardPermissionTest.php +++ b/modules/_bundled/sirsoft-board/tests/Unit/Traits/ChecksBoardPermissionTest.php @@ -334,4 +334,46 @@ class ChecksBoardPermissionTest extends ModuleTestCase // Then: false 반환 (guest role에 권한 미할당) $this->assertFalse($result); } + + /** + * Admin 요청 판정(컨트롤러 네임스페이스 스니핑)은 트레이트 단일 지점이어야 한다 + * + * 판정 규칙이 Resource/게이트마다 사설 복제되면 규칙 변경 시 한 곳만 누락되어 + * 경로별 판정이 갈린다. 모듈 src 전체를 스캔하여 `getController()` 호출이 + * ChecksBoardPermission 트레이트에만 존재함을 고정한다 (열거가 아닌 조건 스캔). + */ + #[Test] + public function admin_요청_판정은_트레이트_단일_지점이다(): void + { + $srcDir = dirname(__DIR__, 3).'/src'; + $allowed = str_replace('\\', '/', realpath($srcDir.'/Traits/ChecksBoardPermission.php')); + $violations = []; + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($srcDir, \FilesystemIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if ($file->getExtension() !== 'php') { + continue; + } + + $path = str_replace('\\', '/', $file->getRealPath()); + + if ($path === $allowed) { + continue; + } + + if (str_contains((string) file_get_contents($path), 'getController(')) { + $violations[] = $path; + } + } + + $this->assertSame( + [], + $violations, + 'Admin 요청 판정 사설 복제본 발견 — ChecksBoardPermission::isAdminRequest() 를 사용할 것: ' + .implode(', ', $violations) + ); + } } diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/attachment-cleanup.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/attachment-cleanup.yaml new file mode 100644 index 00000000..35f9f73d --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/attachment-cleanup.yaml @@ -0,0 +1,74 @@ +feature: 게시판 첨부 정리 (Board Attachment Cleanup) + +description: | + 두 종류의 잔존 첨부를 회수한다. 공개 이슈 #115 동류 결함. + + 1. 임시 첨부 (상시 실행) — 글쓰기 폼에서 올린 뒤 저장 없이 이탈해 `temp_key` 만 남은 행. + 연결 시점에 본경로로 옮겨지므로 `temp_key` 잔존 = 끝내 연결되지 않음이 확정이라 + 오탐 여지가 구조적으로 없다. 운영 데이터가 아닌 폼 세션 부산물이므로 토글이 없다. + 2. 소프트 삭제 첨부 영구 정리 (운영자 옵트인) — 첨부 삭제는 휴지통 복원을 위해 소프트 + 삭제만 수행하므로 물리 파일이 남는다. 사용자 파일을 실제로 파기하므로 기본 꺼짐이며, + 보존기간은 휴지통 복원 가능 기간과 같게 유지해야 한다. + + 삭제 순서는 파일 먼저, 기록 나중. 행마다 disk 가 다를 수 있어 그 행의 disk 를 향해 지운다. + +# axes 는 "한 첨부 행의 처리 여부를 가르는 입력" 만 둔다. 실행 흐름(dry-run / 보존기간 해석)은 +# 판정과 직교하므로 sub_flows 로 분리했다 — 한 축으로 묶으면 존재하지 않는 조합이 생긴다. +axes: + attachment_state: [temp_unlinked, linked, soft_deleted] + age: [within_retention, past_retention] + purge_toggle: [off, on] + +exclusions: + - { attachment_state: temp_unlinked, purge_toggle: on, reason: "임시 첨부 정리는 영구 정리 토글과 무관하게 상시 실행된다" } + - { attachment_state: linked, purge_toggle: on, reason: "게시글에 연결된 첨부는 어느 파트의 대상도 아니다" } + - { attachment_state: linked, age: within_retention, reason: "연결 첨부는 나이와 무관하게 대상이 아니다 — 경과분 1건으로 충분" } + - { attachment_state: soft_deleted, purge_toggle: off, age: within_retention, reason: "토글이 꺼져 있으면 보존기간 판정에 도달하지 않는다" } + +sub_flows: + - id: dry_run + description: 판정만 수행하고 파일·기록을 건드리지 않는 미리보기 경로 + effects: + - board_prune_dry_run_no_delete + - id: retention_guard + description: 보존기간 1일 미만 자기 차단 (방금 올린 임시 첨부까지 지우는 사고 방지) + effects: + - board_temp_prune_days_guard + - id: settings_surface + description: 화면 바인딩 키가 저장 카테고리·기본값·서버 검증과 일치하는지 + effects: + - board_settings_purge_toggle_default_off + - board_settings_purge_keys_declared + - id: temp_directory_hygiene + description: | + 파일을 지운 뒤 남는 temp 디렉토리 처리. 비워졌을 때만 지우고, 다른 글의 파일이 남아 + 있으면 그대로 둔다 (디렉토리째 지우면 아직 작성 중인 글의 첨부가 사라진다). + effects: + - board_temp_prune_removes_empty_directory + - board_temp_prune_keeps_directory_with_remaining_files + - id: batch_limit_boundary + description: | + 대상이 한 회차 상한(--limit)을 넘을 때. 처리하며 행을 지우는 순회이므로, 남은 대상이 + 다음 회차에서 반드시 잡혀야 적체가 수렴한다. + effects: + - board_temp_prune_processes_all_across_runs + - id: reference_source_registry + description: 게시글 본문을 에디터 이미지 참조 소스로 등록하는 리스너 + effects: + - reference_source_listener_subscribes_as_filter + - reference_source_listener_appends_without_dropping + - reference_source_listener_schema_contract + +effects: + - board_temp_prune_deletes_file_and_record + - board_temp_prune_keeps_within_retention + - board_temp_prune_skips_linked + - board_purge_gate_off_by_default + - board_purge_gate_on + - board_purge_keeps_restorable_window + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/Console/PruneAttachmentsCommandTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php + - modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-attachment-cleanup-settings.test.ts + - modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-settings-attachment-cleanup.spec.ts diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/board-settings-save-pipeline.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/board-settings-save-pipeline.yaml new file mode 100644 index 00000000..4cfc2055 --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/board-settings-save-pipeline.yaml @@ -0,0 +1,42 @@ +feature: 게시판 설정 저장 파이프라인과 저장 훅 (B-2 / B-5) + +description: | + 게시판 설정도 저장 경로가 셋이다 — 관리자 화면의 벌크 저장, 값 하나만 바꾸는 단건 저장, + 여러 게시판에 한꺼번에 적용하는 일괄 적용. 파이프라인 단계(캐시 무효화, 신고 알림 플래그 + 강제, 숫자 문자열 정규화)를 벌크 경로만 통과하고 있으면, 같은 값을 어느 경로로 저장했느냐에 + 따라 결과가 달라진다. 저장은 성공하고 파생 값만 어긋나므로 오류가 남지 않는다. + + boolean 백필은 반대다 — 벌크 저장은 화면이 보낸 폼 전체가 기준이라 미제출 체크박스를 + false 로 채워야 하지만, 단건 저장은 그 키 하나만 바꾸겠다는 뜻이다. 여기서 백필하면 + 운영자가 건드리지도 않은 다른 토글이 전부 꺼진다. + + 저장 훅(`core.module_settings.after_save`)의 구독자는 SEO 캐시 무효화다. 게시판 설정의 + SEO 메타 템플릿을 바꿔도 캐시가 그대로면 봇 화면만 옛 값을 계속 보여준다. + +axis_notes: + save_path: | + bulk_apply 는 게시판 여러 개에 같은 설정을 적용하는 경로다. 대상이 N개라 캐시 무효화도 + N개 게시판에 걸려야 한다 — 한 건만 무효화하면 나머지는 옛 값을 유지한다. + 카테고리·파이프라인 단계는 조합이 아니라 각 경로가 통과해야 할 목록이라 effects 로 둔다. + +axes: + save_path: [single_key, bulk, bulk_apply] + +effects: + # 파이프라인 통과 (B-2) + - single_key_save_invalidates_service_cache # 저장 직후 조회가 새 값을 본다 + - single_key_save_does_not_backfill_booleans # 단건 저장이 미제출 토글을 끄지 않는다 + - bulk_save_backfills_unsubmitted_booleans # 벌크 저장은 폼 전체가 기준 (기존 계약 유지) + - report_notification_flags_forced_on_single_key_save + - numeric_string_normalized_on_single_key_save + - board_settings_storage_isolated_in_tests # 테스트 저장소 격리 (실제 설정 파일 오염 차단) + + # 저장 훅과 구독자 (B-5) + - module_settings_after_save_hook_fired + - seo_cache_invalidated_on_module_settings_save + - seo_cache_invalidated_on_bulk_apply # 일괄 적용은 대상 게시판 전부에 걸린다 + - seo_cache_untouched_for_other_module # 다른 모듈 식별자의 저장에는 반응하지 않는다 + +test_files: + - modules/_bundled/sirsoft-board/tests/Unit/BoardSetSettingPipelineTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardSettingsSeoCacheInvalidationTest.php diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/board-type-exception-status.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/board-type-exception-status.yaml new file mode 100644 index 00000000..149bf20d --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/board-type-exception-status.yaml @@ -0,0 +1,47 @@ +feature: 게시판 유형 삭제의 예외 → 상태코드 매핑 (Board Type Exception Status) + +description: | + 게시판 유형 삭제가 실패했을 때, 그 원인이 "운영자가 고칠 수 있는 도메인 사유" + (사용 중인 유형 / 기본 유형)인지 "서버 결함·인프라 장애"인지를 응답 상태코드로 구분한다. + + 배경: + 종전에는 서비스가 `\Exception` 을 던지고 컨트롤러가 그 `getMessage()` 를 응답의 + 메시지 **키 자리**에 넘겼다. 키 해석에 실패해 예외 원문이 그대로 화면에 나갔고, + 인프라 장애까지 같은 422 로 묶여 도메인 사유처럼 보였다. + + 핵심 설계: + - `BoardTypeOperationException` 이 번역문이 아니라 **키와 치환 파라미터**를 들고 다닌다 + (`getMessageKey()` / `getMessageParams()`) + - typed 도메인 예외 → **기존 422 유지** (예외 승격이 사용자 계약을 함께 바꾸면 회귀다) + - 그 외 예외 → 500 + + 도메인 축의 단언은 상태코드가 아니라 **문구**다. 상태코드만 보는 단언은 키 자리에 + 원문이 실린 결함을 그대로 통과시킨다 — 종전 테스트가 실제로 그랬다. 그래서 응답 + `message` 가 해석된 다국어 문구와 일치하고 키 네임스페이스(`sirsoft-board::`)가 + 본문에 남아 있지 않음을 함께 단언한다. + + 인프라 500 축은 서비스 mock 으로만 유발할 수 있어 PHPUnit 이 SSoT 다. + +axes: + error_class: [domain, infrastructure] + +exclusions: [] + +effects: + - board_type_delete_domain_exception_returns_422_with_resolved_message + - board_type_delete_infrastructure_exception_returns_500 + +scenarios: + - id: board_type_delete_failure_distinguishes_domain_from_infrastructure + description: 사용 중인 유형 삭제는 422 와 해석된 안내 문구로, 인프라 장애는 500 으로 구분된다 + effects: + - board_type_delete_domain_exception_returns_422_with_resolved_message + - board_type_delete_infrastructure_exception_returns_500 + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardTypeManagementTest.php + +notes: | + 이 매니페스트는 5차 감사에서 신설됐다. 두 테스트는 브랜치가 추가했지만 + `@scenario`/`@effects` 가 없어 어느 매니페스트에도 묶이지 않았고, 선언이 아예 없는 + effect 는 test-scenario-coverage 의 셀 대상이 아니라 조용히 통과했다. diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/post-reply-delete-policy.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/post-reply-delete-policy.yaml new file mode 100644 index 00000000..cf830d3c --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/post-reply-delete-policy.yaml @@ -0,0 +1,52 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경에 js-yaml 부재로 nested axes 를 읽지 못하는 +# 한계로 검출되지 않는다(동일 한계로 다른 번들 시나리오도 audit:allow). 본 변경은 +# PostService/PostRepository cascade + IndexBoardRequest 게이트를 PHPUnit Feature + +# Playwright E2E(admin/board-reply-delete-policy.spec.ts)가 커버하며 green. +feature: 게시판 답글 삭제 정책 (block/cascade) + 게시판 삭제 벌크 훅 + +description: | + 답글이 달린 글의 삭제 방식을 게시판별 설정(reply_delete_policy)으로 선택한다. + cascade(기본): 원글 삭제 시 자손 답글 트리 전체가 trigger_type='cascade' 로 소프트 + 삭제되고(그 답글들의 댓글·첨부 포함), 복원 시 cascade 분만 top-down 선택 복원된다. + 사용자가 직접 지운 답글 서브트리는 복원되지 않는다. block: 살아있는 직계 답글이 + 있으면 422 로 차단(before_delete 훅 발화 전 — 부수효과 0). 훅 경유 삭제 + (cascade_replies 옵션)는 정책과 무관하게 cascade 고정. 게시판 삭제 시 삭제될 글 + ID 를 board.posts.before_force_delete 벌크 훅으로 연동 확장에 통지한다. + 관리자 게시판 목록의 정렬 컬럼·페이지 크기는 닫힌 집합으로만 허용된다. + +axes: + policy: + - block + - cascade + delete_trigger: + - admin + - user + - ecommerce_hook # cascade_replies 옵션 — 정책 우회 + - report # 신고 조치 삭제 — 정책 적용 유지 + tree_state: + - single_reply + - deep_tree # 깊이 3 이상 중첩 + - user_predeleted_mid # 중간 자손이 사용자 직접 삭제됨 + - orphan_under_trashed # 끊긴 체인 밑 고아 답글 + +effects: + - block_returns_422_with_message # has_replies 메시지 + DB 무변화 + - block_allows_when_replies_trashed # 답글 전부 trashed 면 삭제 허용 + - cascade_marks_descendants_deleted # status='deleted' + trigger_type='cascade' + deleted_at + - cascade_children_comments_attachments # 자손 답글의 댓글·첨부도 cascade 소프트 삭제 + - restore_top_down_cascade_only # 복원은 cascade 마킹분만 top-down + - user_predeleted_subtree_stays # 사용자 선삭제 서브트리는 복원 제외 + - hook_delete_bypasses_block # cascade_replies 옵션이 block 정책 우회 + - board_delete_fires_bulk_hook # before_force_delete 훅 chunk 발화 (withTrashed 전체) + - board_index_sort_whitelisted # sort_by 화이트리스트 밖 422, per_page 클램프 + - settings_and_form_expose_policy # 환경설정/게시판 폼에 정책 Select 노출·저장 왕복 + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/Admin/ReplyDeletePolicyTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardIndexSortTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/BoardDeletionTest.php + - modules/_bundled/sirsoft-board/tests/Feature/FormRequestValidationTest.php + - modules/_bundled/sirsoft-board/tests/Playwright/specs/admin/board-reply-delete-policy.spec.ts + - modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-settings.test.tsx + - modules/_bundled/sirsoft-board/resources/js/__tests__/layouts/admin-board-form.test.tsx diff --git a/modules/_bundled/sirsoft-board/tests/scenarios/security-fixes-2026-08.yaml b/modules/_bundled/sirsoft-board/tests/scenarios/security-fixes-2026-08.yaml new file mode 100644 index 00000000..1405d2fc --- /dev/null +++ b/modules/_bundled/sirsoft-board/tests/scenarios/security-fixes-2026-08.yaml @@ -0,0 +1,49 @@ +feature: 게시판 보안 수정 묶음 (2026-08 — 공개 피드 가시성 / 대시보드 권한 / 댓글 검증 토큰 / 하이라이트 이스케이프) + +description: | + 2026-08 비공개 보안 제보 대응으로 sirsoft-board 에 반영된 수정의 회귀 고정. + + - N-3/N-4/⑱-2: 공개 최신글·인기글·대시보드 요약이 열람 권한 없는 게시판 글과 + 블라인드/비활성 게시판 글을 노출하던 문제 — SQL 안전집합(published + active)과 + 응답 시점 열람 권한 필터(캐시는 안전집합만 담아 캐시 오염 회피)의 2층 구조. + 비밀글은 제목 공개 정책(2026-02-04)에 따라 제목을 노출한다(본문만 보호). 인기글은 + 별도 컨벤션으로 비밀글 자체를 제외한다(2026-01-16). + - ⑦/N-2: 통합 검색 available_boards 와 공개 인기 게시판 목록이 열람 권한(posts.read) + 을 반영하지 않던 문제. 검색 라우트의 optional.sanctum 부재로 Bearer 회원이 guest 로 + 판정되던 결함 포함(HTTP 전 구간 회귀 테스트로 고정 — 서비스 직접 호출은 이 사각을 + 보지 못한다). + - ⑱-1/N-5: 관리자 대시보드 4종 라우트의 permission 게이트(sirsoft-board.dashboard.view) + 부재 + 미처리 신고 집계의 비활성 게시판 스코프 부재. + - ⑮: 댓글 verification_token 이 발급만 되고 저장·검증되지 않던 죽은 코드 — + 캐시 기반 단일 사용 토큰으로 실동작(발급→수정/삭제 소비→재사용·만료·오토큰 거부). + - ⑧: 검색 하이라이트가 원문을 이스케이프하지 않던 문제 — 코어 SearchHighlighter 위임. + +axes: + case: [recent_readable_filter, popular_readable_filter, popular_boards_readable, available_boards_guest, available_boards_member, available_boards_bearer_http, dashboard_permission_gate, dashboard_recent_visibility, pending_reports_scope, comment_token_lifecycle, search_highlight_escape, board_directory_readable_filter] + +exclusions: [] + +effects: + - unreadable_board_titles_absent_for_caller + - popular_boards_exclude_unreadable + # 게시판 디렉토리(boards index + 내장 최근글) — 최근글·인기글·인기게시판에 필터가 걸린 뒤에도 + # 이 경로가 무필터로 남아 비공개 게시판·글 제목의 우회 노출로였다 (7.0.7 사전점검 SEO 실측 발견) + - board_directory_excludes_unreadable_board_and_posts + - board_directory_includes_readable_board_for_permitted_member + - available_boards_match_read_permission + - search_route_resolves_bearer_user + - dashboard_requires_view_permission + - dashboard_summary_excludes_hidden_posts + - pending_reports_scoped_to_active_boards + - comment_verify_token_single_use_lifecycle + - highlighted_fields_escaped + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/Board/PublicRecentPostsVisibilityTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Search/AvailableBoardsPermissionTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Dashboard/DashboardPermissionTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Dashboard/RecentAcrossBoardsVisibilityTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Comment/CommentVerifyTokenTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php + - modules/_bundled/sirsoft-board/tests/Feature/User/BoardPopularApiTest.php + - modules/_bundled/sirsoft-board/tests/Feature/Admin/DashboardApiTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md index fc3a8816..a5016cfa 100644 --- a/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md +++ b/modules/_bundled/sirsoft-ecommerce/CHANGELOG.md @@ -4,6 +4,142 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Security + +- 비밀 상품 문의의 내용이 작성자·관리자가 아닌 사람에게도 노출되던 문제를 막았습니다. 문의 목록은 게시글의 비밀 여부를 서버에서 확인해, 열람 권한이 없는 요청에는 내용·제목·답변·첨부를 가립니다. (목록의 '비밀글 숨김' 옵션과 무관하게 서버가 요청자 신원으로 노출 여부를 결정합니다.) (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 관리자가 숨긴 리뷰의 이미지가 주소만 알면 계속 조회되던 문제를 수정했습니다. 이제 숨김 처리된 리뷰의 이미지는 제공되지 않습니다. 리뷰가 삭제되어 상태를 확인할 수 없는 이미지도 함께 차단합니다. 관리자 리뷰 화면에 표시되는 숨김 리뷰의 이미지 썸네일은 한시적으로만 유효한 서명 주소로 전달되어, 보호를 유지한 채 정상 표시됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 상품·주문·리뷰·브랜드·쿠폰·배송정책·추가배송비 관리에 담당 범위 제한을 적용했습니다. 이 권한들은 "본인이 등록한 것만" 처럼 범위를 좁혀 위임할 수 있는데, 그 제한이 실제로는 어느 관리 화면에서도 적용되지 않았습니다. 목록에서 여러 건을 한 번에 처리하는 일괄 기능뿐 아니라, 항목을 하나씩 수정·삭제하는 화면에서도 마찬가지였습니다. 이제 모든 경로에서 대상마다 범위를 확인하며, 일괄 기능은 범위 밖 대상이 하나라도 섞이면 요청 전체를 거부하고 아무것도 변경하지 않습니다. 범위 제한 없이 위임받은 관리자의 작업은 종전처럼 정상 동작합니다. (KVE-2026-1919) +- 리뷰 작성 시 대상 상품을 요청에 담긴 값이 아니라 실제 구매한 주문 옵션에서 확정하도록 바로잡았습니다. 이전에는 자신의 구매 내역을 근거로 삼되 다른 상품을 대상으로 지정해 리뷰를 남길 수 있었습니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 다운로드 대상이 아닌 쿠폰(자동 발급·관리자 직접 발급)이나 이미 유효기간이 지난 쿠폰을, 목록에 보이지 않더라도 주소만 알면 직접 내려받을 수 있던 문제를 수정했습니다. 이제 다운로드 발급 조건과 유효기간을 발급 시점에 함께 확인합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 비회원 구매확정이 배송 준비 등 확정 불가 상태의 주문이나 이미 확정된 주문에도 적용되던 문제를 수정했습니다. 이제 회원·비회원 모두 확정 가능한 상태에서만 한 번 확정됩니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 비회원 환불 예상액 계산과 주문 취소 요청이 입력 항목 검증을 건너뛰던 문제를 수정했습니다. 이제 회원 경로와 동일하게 주문 소속·취소 완료 여부·수량 상한을 확인합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) +- 검색 프리셋 관리(조회·저장·수정·삭제)에 별도의 관리 권한을 요구하도록 했습니다. +- 상품 통합 검색 결과의 이름·설명 미리보기에서 검색어 강조 처리가 원문을 그대로 실어 보내던 것을, 안전하게 변환한 뒤 강조하도록 바로잡았습니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) + +### Added + +- 리뷰 이미지 업로드에 파일 가공 필터 훅 제공 — 확장에서 상품·카테고리 이미지와 동일하게 리뷰 이미지도 업로드 시점에 변환(압축·리사이즈·포맷 변경)할 수 있습니다. (#96 @lyg-kaban 님께서 건의해주셨습니다.) +- 상품·카테고리·리뷰 이미지를 코어의 공개 자산 스토리지(S3+CDN 등) 디스크에 저장하고 화면에는 CDN 직접 주소로 서빙할 수 있습니다. 기본정보 탭의 「공개 자산 스토리지」에서 이 모듈만 다른 디스크를 쓰도록 개별 설정할 수도 있습니다(비우면 코어 설정 따름). 디스크를 바꿔도 기존 이미지는 원래 위치에서 계속 정상 표시·삭제됩니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) + +- 상품 등록 중 이미지만 올리고 저장하지 않은 채 나간 임시 이미지를 매일 자동 정리합니다. 파일을 지운 뒤 남는 빈 폴더까지 함께 정리하므로 저장소에 흔적이 남지 않습니다. 종전에는 이런 파일이 서버에 계속 쌓여도 회수할 방법이 없었습니다. + +### Changed + +- 코어 최소 요구 버전을 7.0.7 로 상향했습니다. +- 환불 예상금액 계산이 쿠폰 사용 상태를 일시적으로 변경하지 않습니다. 계산 과정의 쿠폰 상태를 참조하던 외부 확장이 있다면 동작을 확인해 주세요. +- 게시판에서 삭제한 문의 글을 복원하면 상품 문의 목록에도 함께 복원됩니다. 문의 데이터는 즉시 지워지지 않고 복원 가능한 상태로 보관됩니다. +- 업데이트 시, 과거에 문의가 삭제되면서 홀로 남아 있던 답변 글과 어긋난 답변완료 표시를 자동으로 정리합니다. (#107 @Tuwasduliebst 님께서 제보해주셨습니다.) + +### Fixed + +- 리뷰를 작성할 수 없을 때의 안내 문구에 사유가 영문 식별자(`already_written` 등) 그대로 표시되던 문제를 수정했습니다. 이제 "이미 작성한 리뷰", "구매확정 전" 처럼 번역된 사유가 표시됩니다. + +#### 상품 문의 답변 + +- 이미 답변이 등록된 상품 문의에 답변을 다시 등록하면 답변이 중복으로 쌓이던 문제를 수정했습니다. 이제 안내와 함께 거절되며, 기존 답변을 수정하거나 삭제한 뒤 다시 등록할 수 있습니다. (#106 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 상품 문의를 삭제해도 그 문의에 달린 답변 글이 게시판에 공개 상태로 남아 있던 문제를 수정했습니다. 이제 문의를 삭제하면 질문과 답변이 함께 정리됩니다. (#107 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 상품을 삭제해도 그 상품의 문의 글과 답변 글이 게시판에 공개 상태로 남아 있던 문제를 수정했습니다. +- 게시판에서 답변 글을 직접 삭제해도 상품 문의가 「답변완료」로 남아 있던 문제를 수정했습니다. +- 게시판에서 문의 글을 직접 삭제했을 때 큐 설정에 따라 문의 목록 정리가 지연되거나 누락될 수 있던 문제를 수정했습니다. 이제 삭제 즉시 반영됩니다. +- 문의의 답변 완료 시각이 이후 작업으로 덮어써질 수 있던 문제를 수정해 최초 답변 시각이 유지됩니다. + +#### 카테고리 이미지 + +- 카테고리를 삭제해도 그 카테고리의 이미지 파일이 서버에 그대로 남던 문제를 수정했습니다. 상품 삭제와 동일하게 파일까지 함께 삭제됩니다. +- 카테고리 등록·수정 화면에서 이미지를 목록에서 빼도 화면에 그대로 남아 저장되던 문제를 수정했습니다. + +#### 통합검색·상품 문의 + +- 통합검색에서 상품 검색이 실패하면 "검색 결과 없음" 이 아니라 오류 안내로 구분해 표시되도록 실패 정보를 응답에 싣습니다. 실패 원인은 서버 기록에 상세히 남습니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 상품 문의 목록이 문의가 많은 상품에서 목록 전체를 읽고 나서 잘라내던 것을, 현재 페이지 분량만 읽도록 조회 방식을 개선했습니다. 화면에 보이는 내용은 이전과 동일합니다. + +#### 삭제된 PG·현금영수증 발급사 + +- 결제수단에 지정한 PG 사의 플러그인을 삭제해도 그 결제수단이 주문서에 계속 노출되던 문제를 수정했습니다. 이전에는 구매자가 그 수단을 고르면 결제창이 뜨지 않은 채 주문완료로 넘어갔습니다. 이제 해당 결제수단은 주문서에서 제외되고, 관리자 주문설정 화면에는 「지정 PG 삭제됨」 표시가 붙습니다. 살아 있는 PG 로 다시 지정하면 즉시 복구되며, 주문서를 거치지 않고 직접 주문을 시도하는 경우에도 안내와 함께 거절됩니다. (#111 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 현금영수증 발급사 플러그인을 삭제해도 주문서의 현금영수증 신청 폼과 마이페이지 발급 버튼이 계속 표시되던 문제를 수정했습니다. 신청해도 발급 실패로만 기록됐습니다. 이미 발급했거나 발급을 시도한 이력이 있는 주문은 종전대로 내역과 영수증 링크가 계속 표시됩니다. + +#### 설정 저장 반영 + +- 관리자 화면(환경설정과 그 안의 모달·카테고리 순서 변경·주문 상세 정보 저장·쿠폰 발급/상태 변경)에서 저장·실행이 실패하면 실패 사유 대신 일반 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. +- 관리자 환경설정을 저장해도 SEO 미리 생성 캐시가 갱신되지 않아, 검색엔진에 예전 메타 정보가 계속 노출되던 문제를 수정했습니다. 쇼핑몰 SEO 설정 저장 시 관련 캐시가 즉시 정리됩니다. +- 환경설정 저장 직후 같은 요청 안에서 결제수단·통화 정보가 저장 전 값으로 계산되던 문제를 수정했습니다. (#116 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 배송정책을 저장·삭제하거나 기본 배송정책을 바꾼 직후, 같은 요청 안에서 변경 전 배송정책으로 배송비가 계산될 수 있던 문제를 수정했습니다. +- 일부 설정 저장 경로가 다른 저장 경로와 다른 처리를 거쳐, 설정 파일 안에서 값이 서로 어긋난 채 남을 수 있던 문제를 수정했습니다. 기본 통화를 바꿨는데 통화 목록의 기본 표시가 따라오지 않거나, 은행 목록 저장 시 은행명이 다국어 형식으로 정리되지 않는 등의 경우입니다. (#114 @Tuwasduliebst 님께서 제보해주셨습니다.) + +#### 주문 배송지 변경 + +- 비회원·회원 주문의 배송지 변경에서 서버 내부 오류가 「배송 전 상태에서만 배송지를 변경할 수 있습니다.」 라는 안내로 표시되던 문제를 수정했습니다. 이제 변경할 수 없는 상태일 때만 그 안내가 나오고, 그 밖의 오류는 서버 오류로 구분되어 장애를 바로 알아차릴 수 있습니다. (#104 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 해외 배송지로 주문 배송지를 변경할 때 국가를 함께 지정하지 않으면 저장에 실패하던 문제를 수정했습니다. 이제 국가 코드를 기준으로 국내·해외에 필요한 항목을 안내하며, 국내↔해외를 전환하면 반대편 주소가 남지 않습니다. +- 주소록에서 해외 배송지를 골라 주문 배송지를 바꾸면 주소가 통째로 비워지던 문제를 수정했습니다. 국내 항목만 옮겨 담고 있어 해외 주소·도시·우편번호가 모두 사라졌으며, 저장은 정상 완료된 것처럼 처리돼 주문 상세를 다시 열어야 알 수 있었습니다. +- 주소록에서 배송지를 고르면서 함께 입력한 배송 메모가 저장되지 않던 문제를 수정했습니다. + +#### 환불 예상금액 + +- 환불 예상금액 조회가 실제로는 쿠폰 사용 이력을 잠시 변경했다가 되돌리던 문제를 수정했습니다. 이제 조회만 수행하므로, 계산 도중 서버가 중단되어도 사용한 쿠폰이 미사용으로 남지 않습니다. (#108 @Tuwasduliebst 님께서 제보해주셨습니다.) + +#### 오류 안내 구분 + +- 관리자 화면 여러 곳에서 서버 내부 오류가 입력 오류처럼 표시되던 문제를 수정했습니다. 카테고리·브랜드·쿠폰·배송사·배송정책·추가배송비·상품 라벨·상품 공통정보·클레임 사유·상품 안내 템플릿·상품 이미지 관리에 적용됩니다. +- 찜 목록 조회·찜 추가/해제·찜 삭제가 실패했을 때 서버 오류가 입력 오류로 표시되던 문제를 수정했습니다. +- 배송설정에서 주문에 사용 중인 배송유형·배송사를 지우려 할 때 「설정 저장 중 오류가 발생했습니다.」 라고만 표시되어 이유를 알 수 없던 문제를 수정했습니다. 이제 어떤 항목이 몇 건의 주문에서 사용 중인지 안내되며, 서버 오류와도 구분됩니다. +- 상품 문의 등록이 서버 오류로 실패해도 그 원문이 입력 오류 안내에 섞여 표시되던 문제를 수정했습니다. 이제 문의 게시판 미설정처럼 안내 가능한 사유만 그 자리에 표시되고, 그 밖의 오류는 서버 오류로 구분됩니다. +- 마이페이지의 문의 수정·삭제와 답변 등록·수정·삭제에서도 같은 문제를 수정했습니다. 아울러 문의 게시판이 설정되지 않아 삭제할 수 없을 때 그 사유가 전달되지 않고 일반 서버 오류로만 표시되던 문제를 함께 수정해, 운영자가 원인을 바로 알 수 있습니다. +- 일부 오류 응답에 내부 오류 원문이 그대로 표시되던 문제를 수정했습니다. 쿠폰 다운로드·클레임 사유 삭제·배송사 삭제·상품 문의 답변 관리·리뷰 작성·리뷰 이미지 업로드에서 상황에 맞는 안내 문구가 나옵니다. + +#### 쿠폰·통화 + +- 비회원 주문 상세와 관리자 주문 상세에서 현금영수증 금액의 소수 자릿수가 주문 시점이 아닌 현재 통화 설정을 따르던 문제를 수정했습니다. +- 관리자 쿠폰 등록·수정에서 「타 쿠폰과 중복 사용」을 선택하면 `is combinable 필드는 true 또는 false여야 합니다.` 오류로 저장이 실패하던 문제를 수정했습니다. (#97 @lyg-kaban 님께서 제보해주셨습니다.) +- 언어/통화 설정에서 기본 제공 통화(달러·엔·위안·유로)를 삭제해도 저장 직후 다시 나타나던 문제를 수정했습니다. 삭제한 통화는 설정 화면과 쇼핑몰 화면 양쪽에서 유지되며, 통화 추가로 다시 등록하면 복원됩니다. 사용 중지된 통화를 표시 통화로 갖고 있던 구매자의 주문은 기본 통화로 안전하게 진행됩니다. (#91 @koojunho 님께서 제보해주셨습니다.) +- 통화를 삭제한 뒤 그 통화로 결제된 과거 주문의 금액 표기가 달라지던 문제를 수정했습니다. 주문·환불 금액의 소수 자릿수는 주문 시점 기준을 유지하므로, 엔화처럼 소수점을 쓰지 않는 통화가 `¥14,835.00` 으로 바뀌거나 소수점 이하 자릿수가 많은 통화의 표시 금액이 잘리지 않습니다. (#91 @koojunho 님께서 제보해주셨습니다.) + +#### 관리자 화면 + +- 관리자 엑셀 다운로드 관리의 다운로드 이력 모달에서, 이력 항목이 아직 선택되지 않은 순간에 파일 정보·상태 배지가 비정상적으로 처리되던 문제를 보완했습니다. 항목을 선택한 뒤의 표시는 종전과 동일합니다. +- 같은 모달의 "다운로드 기록" 목록이 항상 비어 보이던 문제를 수정했습니다. 이제 누가 언제 몇 번째로 내려받았는지 기록이 표시됩니다. +- 같은 모달의 "처리중"·"실패"·"만료" 상태 배지에서 아이콘이 표시되지 않던 문제를 수정했습니다. +- 관리자 목록에서 항목을 선택한 뒤 검색하거나 페이지를 넘기면, 화면에서 사라진 항목이 선택된 채로 남아 일괄 처리 대상에 포함되던 문제를 수정했습니다. 이제 일괄 처리 대상은 언제나 화면에 보이면서 체크된 항목뿐입니다. 주문·상품·쿠폰·배송정책·마일리지 내역·리뷰 목록에 적용됩니다. 쿠폰 적용 대상 고르기처럼 여러 페이지에 걸쳐 선택하는 화면은 종전대로 선택이 유지됩니다. +- 환경설정의 배송·환불 설정 화면과 상품평 목록에서 켜기/끄기 스위치와 상태 배지가 레이아웃 편집기의 편집 대상으로 선택되지 않던 문제를 수정했습니다. +- 배송 국가·배송 유형·환불 사유·추가 배송비 목록에서 각 행이 같은 HTML 식별자를 공유하던 문제를 수정했습니다. + +#### 기타 + +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 추가배송비(도서산간) 템플릿 상세 응답의 편집 권한 표시를 배송정책 권한 기준으로 통일했습니다. 이전에는 상세 응답만 다른 권한(설정 관리)을 기준으로 삼아, 배송정책 관리 권한만 가진 관리자에게 편집 가능 여부가 실제 권한과 다르게 표시될 수 있었습니다. + +#### 배송비 구간 + +- 구간별 배송비의 종료값이 그 구간에 포함되도록 수정했습니다. "1~5개 3,000원 / 6개~ 5,000원"으로 설정했을 때 수량이 정확히 5개면 어느 구간에도 해당하지 않아 배송비가 0원(무료배송)으로 계산되던 문제입니다. 이미 저장해 둔 정책은 그대로 두어도 정상 계산됩니다. (#94 @koojunho 님께서 제보해주셨습니다.) +- 무게·부피·금액 구간은 이제 "5kg 다음 구간은 5kg 초과"처럼 이어서 설정합니다. 이전에는 수량과 동일하게 "종료값 + 1"을 요구해 2.5kg 같은 소수 구간을 저장할 수 없었습니다. 수량 구간은 종전대로 "5개 다음은 6개"입니다. (#94 @koojunho 님께서 제보해주셨습니다.) +- 구간 시작값은 직전 구간의 종료값에서 자동으로 채워지며 읽기전용으로 표시됩니다. 구간 입력란에는 정책에 맞는 단위(개/kg/L)가 함께 표시됩니다. 이전 형식으로 저장된 정책은 수정 화면에 들어가면 자동으로 맞춰지므로, 배송비만 바꿔 저장할 때 시작값 때문에 저장이 막히지 않습니다. +- 구간 입력 오류 안내가 문장으로 표시됩니다. 이전에는 "sirsoft-ecommerce.validation..." 같은 내부 식별자가 그대로 보였습니다. +- 배송비 요약 문구에 단위가 다시 표시됩니다. 관리자 화면에서 저장한 정책은 단위가 빠진 채 "~5: 3,000원"으로만 보였습니다. 단위가 0.5처럼 소수인 경우 1로 반올림되어 표시되던 문제도 함께 수정했습니다. +- 금액당 배송비 요약에 통화가 표시됩니다. 이전에는 "10,000당 500원"처럼 기준 금액에 통화 표기가 빠져 있었습니다. + +#### 무게·부피 단위 + +- 무게·부피 기준 배송비가 실제 상품 무게로 계산됩니다. 상품 옵션에 500g을 입력하고 "1kg당 1,000원" 정책을 걸면 500,000원이 청구되던 문제입니다. 부피 기준 정책도 마찬가지로 1,000cm³ 상품이 200,000원으로 계산됐습니다. (#94 @koojunho 님께서 제보해주셨습니다.) +- 부피무게 기준 정책이 실제 무게와 부피무게를 같은 단위로 비교합니다. 이전에는 서로 다른 단위로 비교해 항상 한쪽만 채택됐습니다. (#94 @koojunho 님께서 제보해주셨습니다.) +- 상품 옵션의 무게·부피가 관리자 수정 화면에 표시됩니다. 이전에는 저장된 값이 있어도 0으로 보였고, 그 상태로 저장하면 실제 값이 0으로 덮어써졌습니다. +- 주문에 총 무게·부피가 실제 값으로 기록됩니다. 이전에는 항상 0으로 저장되어 배송사 연동·운임 정산에서 무게 없는 주문으로 취급됐습니다. 기존 주문도 업데이트 시 주문 항목의 값으로 채워집니다. + +#### 배송정책 설정 검증 + +- 배송비가 조용히 0원이 되는 설정을 저장 단계에서 막습니다 — 구간별 정책의 구간 미등록, 단위당 정책의 단위값 누락, 조건부 무료배송의 기준금액 누락, 중간 구간의 종료값 공란이 해당합니다. +- 단위당 배송비의 단위값과 조건부 무료배송의 기준금액을 비우면 저장을 누르기 전에 편집 화면에서 바로 알려 줍니다. 부과정책을 바꾸면 직전 정책에서 남은 안내는 사라집니다. +- 계산 API 정책의 기본 배송비를 0원으로 저장할 수 없습니다. 외부 API 호출이 실패했을 때 쓰는 값이라 0원이면 장애가 그대로 무료배송이 됩니다. 외부 API가 음수 배송비를 반환하는 경우에도 기본 배송비로 대체합니다. +- 도서산간 우편번호는 "63000", "63000-63999", "63*" 세 형식만 저장됩니다. 다른 형식은 어떤 우편번호에도 맞지 않아 추가배송비가 빠진 채로 주문이 완료됐습니다. 자릿수가 다른 우편번호가 범위 조건에 잘못 걸리던 문제도 함께 수정했습니다. +- 도서산간 추가배송비의 "중복 부과"가 기본 배송비와 같은 배송 단위 수를 기준으로 계산됩니다. 이전에는 주문 수량을 기준으로 삼아 무게당·부피당 정책에서 기본 배송비와 어긋난 금액이 부과됐습니다. +- 배송비에 소수점이 있는 경우 버리지 않고 반올림합니다. +- 관리자가 상점 설정을 저장해도 백그라운드 작업에는 이전 설정이 계속 적용되던 문제를 수정했습니다. (#109 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 주문서 작성 중 만들어지는 임시 주문 정보가 만료된 뒤에도 정리되지 않고 계속 쌓이던 문제를 수정했습니다 — 이제 매시간 자동으로 정리됩니다. (#110 @Tuwasduliebst 님께서 제보해주셨습니다.) + +#### 화면 표시 + +- 카테고리 관리 목록의 아이콘이 의도한 크기와 다르게 보이던 문제와, 쿠폰 상태 변경 확인 창의 버튼이 다크 모드에서 밝은 배경 그대로 보이던 문제를 수정했습니다. + ## [1.1.0] - 2026-08-10 ### Security diff --git a/modules/_bundled/sirsoft-ecommerce/components.json b/modules/_bundled/sirsoft-ecommerce/components.json index d0e9b462..54a30af8 100644 --- a/modules/_bundled/sirsoft-ecommerce/components.json +++ b/modules/_bundled/sirsoft-ecommerce/components.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "identifier": "sirsoft-ecommerce", - "version": "1.1.0", + "version": "1.1.1", "components": { "basic": [], "composite": [], diff --git a/modules/_bundled/sirsoft-ecommerce/composer.json b/modules/_bundled/sirsoft-ecommerce/composer.json index 4ce24561..7492ffe5 100644 --- a/modules/_bundled/sirsoft-ecommerce/composer.json +++ b/modules/_bundled/sirsoft-ecommerce/composer.json @@ -2,7 +2,7 @@ "name": "modules/sirsoft-ecommerce", "description": "Ecommerce module for Gnuboard7", "type": "library", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "autoload": { "psr-4": { diff --git a/modules/_bundled/sirsoft-ecommerce/config/ecommerce.php b/modules/_bundled/sirsoft-ecommerce/config/ecommerce.php index 4aef2a12..3fdd51cc 100644 --- a/modules/_bundled/sirsoft-ecommerce/config/ecommerce.php +++ b/modules/_bundled/sirsoft-ecommerce/config/ecommerce.php @@ -66,16 +66,6 @@ return [ 'guest_cart_lifetime' => 7 * 24 * 60, // 7일 (분 단위) ], - // 카테고리 설정 - 'category' => [ - 'image_disk' => 'local', - ], - - // 브랜드 설정 - 'brand' => [ - 'image_disk' => 'local', - ], - // 리뷰 설정 'review' => [ 'write_deadline_days' => 90, // 구매확정 후 리뷰 작성 가능 기간 (일) diff --git a/modules/_bundled/sirsoft-ecommerce/config/settings/defaults.json b/modules/_bundled/sirsoft-ecommerce/config/settings/defaults.json index f2b209eb..5b6da90d 100644 --- a/modules/_bundled/sirsoft-ecommerce/config/settings/defaults.json +++ b/modules/_bundled/sirsoft-ecommerce/config/settings/defaults.json @@ -23,7 +23,8 @@ "privacy_officer": "", "privacy_officer_email": "", "mail_order_number": "", - "telecom_number": "" + "telecom_number": "", + "public_asset_disk": "" }, "language_currency": { "default_currency": "KRW", @@ -339,7 +340,8 @@ "privacy_officer": { "expose": false }, "privacy_officer_email": { "expose": false }, "mail_order_number": { "expose": false }, - "telecom_number": { "expose": false } + "telecom_number": { "expose": false }, + "public_asset_disk": { "expose": false } } }, "language_currency": { diff --git a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000017_create_ecommerce_orders_table.php b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000017_create_ecommerce_orders_table.php index 6e974a45..7aaddd9c 100644 --- a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000017_create_ecommerce_orders_table.php +++ b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000017_create_ecommerce_orders_table.php @@ -14,7 +14,7 @@ return new class extends Migration { Schema::create('ecommerce_orders', function (Blueprint $table) { $table->id()->comment('주문 ID'); - $table->unsignedBigInteger('user_id')->nullable(); + $table->unsignedBigInteger('user_id')->nullable()->comment('주문자 회원 ID (비회원 주문은 null)'); $table->string('order_number', 50)->unique()->comment('주문번호'); $table->string('order_status', 30)->comment('주문상태 (OrderStatusEnum)'); $table->string('order_device', 20)->nullable()->comment('주문 디바이스 (pc/mobile/app)'); @@ -44,7 +44,7 @@ return new class extends Migration $table->decimal('total_refunded_points_amount', 12, 2)->default(0)->comment('총 환불 포인트'); $table->decimal('total_earned_points_amount', 12, 2)->default(0)->comment('총 적립 예정 포인트'); $table->integer('item_count')->comment('총 주문수량 (상품 수량 합계)'); - $table->decimal('total_weight', 10, 3)->nullable()->comment('총 무게 (kg)'); + $table->decimal('total_weight', 10, 3)->nullable()->comment('총 무게 (g)'); $table->decimal('total_volume', 10, 3)->nullable()->comment('총 부피 (cm³)'); $table->timestamp('ordered_at')->comment('주문일시'); $table->timestamp('paid_at')->nullable()->comment('결제완료일시'); @@ -83,7 +83,7 @@ return new class extends Migration }); if (DB::getDriverName() === 'mysql') { - DB::statement("ALTER TABLE `".DB::getTablePrefix()."ecommerce_orders` COMMENT '주문 정보'"); + DB::statement('ALTER TABLE `'.DB::getTablePrefix()."ecommerce_orders` COMMENT '주문 정보'"); } } diff --git a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000018_create_ecommerce_order_options_table.php b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000018_create_ecommerce_order_options_table.php index 70493a21..2e5cb317 100644 --- a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000018_create_ecommerce_order_options_table.php +++ b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_04_01_000018_create_ecommerce_order_options_table.php @@ -14,10 +14,10 @@ return new class extends Migration { Schema::create('ecommerce_order_options', function (Blueprint $table) { $table->id()->comment('주문 옵션 ID'); - $table->unsignedBigInteger('order_id'); + $table->unsignedBigInteger('order_id')->comment('소속 주문 ID'); $table->unsignedBigInteger('parent_option_id')->nullable()->comment('추가 옵션/구성품의 부모 옵션 ID'); - $table->unsignedBigInteger('product_id'); - $table->unsignedBigInteger('product_option_id'); + $table->unsignedBigInteger('product_id')->comment('주문 시점 상품 ID'); + $table->unsignedBigInteger('product_option_id')->comment('주문 시점 상품 옵션 ID'); $table->string('option_status', 30)->comment('옵션 상태 (OrderStatusEnum)'); $table->boolean('is_stock_deducted')->default(false)->comment('재고 차감 여부'); $table->string('source_type', 20)->default('order')->comment('생성 원인 (order/exchange)'); @@ -28,10 +28,10 @@ return new class extends Migration $table->string('option_name', 255)->nullable()->comment('옵션명 (예: "색상", "사이즈")'); $table->string('option_value', 255)->nullable()->comment('옵션값 (예: "빨강", "XL")'); $table->integer('quantity')->comment('주문수량'); - $table->decimal('unit_weight', 10, 3)->nullable()->comment('단위 무게 (kg, 주문 시점)'); + $table->decimal('unit_weight', 10, 3)->nullable()->comment('단위 무게 (g, 주문 시점)'); $table->decimal('unit_volume', 10, 3)->nullable()->comment('단위 부피 (cm³, 주문 시점)'); - $table->decimal('subtotal_weight', 10, 3)->nullable()->comment('무게 소계 (unit_weight × quantity)'); - $table->decimal('subtotal_volume', 10, 3)->nullable()->comment('부피 소계 (unit_volume × quantity)'); + $table->decimal('subtotal_weight', 10, 3)->nullable()->comment('무게 소계 (g, unit_weight × quantity)'); + $table->decimal('subtotal_volume', 10, 3)->nullable()->comment('부피 소계 (cm³, unit_volume × quantity)'); $table->decimal('unit_price', 12, 2)->comment('단가 (주문 시점 가격)'); $table->decimal('subtotal_price', 12, 2)->comment('소계 (unit_price × quantity)'); $table->decimal('subtotal_discount_amount', 12, 2)->default(0)->comment('할인 소계 (아래 할인 합계)'); @@ -75,7 +75,7 @@ return new class extends Migration }); if (DB::getDriverName() === 'mysql') { - DB::statement("ALTER TABLE `".DB::getTablePrefix()."ecommerce_order_options` COMMENT '주문 옵션 정보'"); + DB::statement('ALTER TABLE `'.DB::getTablePrefix()."ecommerce_order_options` COMMENT '주문 옵션 정보'"); } } diff --git a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_11_000001_fix_weight_volume_unit_comments_in_ecommerce_order_tables.php b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_11_000001_fix_weight_volume_unit_comments_in_ecommerce_order_tables.php new file mode 100644 index 00000000..5244b6a5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_11_000001_fix_weight_volume_unit_comments_in_ecommerce_order_tables.php @@ -0,0 +1,83 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + * + * 주문 무게/부피 컬럼의 단위 주석을 실제 저장 단위(g / cm³)로 정정합니다 (공개 #94). + * 상품 옵션은 g / cm³ 로 저장되고 주문 생성 시 그 값이 그대로 복사되는데, + * 주문 테이블 주석만 kg 로 적혀 있어 배송비 계산·외부 연동에서 단위를 잘못 읽는 + * 근거가 되었습니다. 저장된 값은 정확하므로 값 변환 없이 주석만 정정합니다. + */ + public function up(): void + { + $this->applyComments( + orderWeightComment: '총 무게 (g)', + orderVolumeComment: '총 부피 (cm³)', + unitWeightComment: '단위 무게 (g, 주문 시점)', + unitVolumeComment: '단위 부피 (cm³, 주문 시점)', + subtotalWeightComment: '무게 소계 (g, unit_weight × quantity)', + subtotalVolumeComment: '부피 소계 (cm³, unit_volume × quantity)', + ); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $this->applyComments( + orderWeightComment: '총 무게 (kg)', + orderVolumeComment: '총 부피 (cm³)', + unitWeightComment: '단위 무게 (kg, 주문 시점)', + unitVolumeComment: '단위 부피 (cm³, 주문 시점)', + subtotalWeightComment: '무게 소계 (unit_weight × quantity)', + subtotalVolumeComment: '부피 소계 (unit_volume × quantity)', + ); + } + + /** + * 컬럼 주석을 일괄 적용합니다 (컬럼 정의는 그대로, 주석만 변경). + * + * @param string $orderWeightComment orders.total_weight 주석 + * @param string $orderVolumeComment orders.total_volume 주석 + * @param string $unitWeightComment order_options.unit_weight 주석 + * @param string $unitVolumeComment order_options.unit_volume 주석 + * @param string $subtotalWeightComment order_options.subtotal_weight 주석 + * @param string $subtotalVolumeComment order_options.subtotal_volume 주석 + */ + private function applyComments( + string $orderWeightComment, + string $orderVolumeComment, + string $unitWeightComment, + string $unitVolumeComment, + string $subtotalWeightComment, + string $subtotalVolumeComment, + ): void { + if (Schema::hasTable('ecommerce_orders')) { + Schema::table('ecommerce_orders', function (Blueprint $table) use ($orderWeightComment, $orderVolumeComment) { + $table->decimal('total_weight', 10, 3)->nullable()->comment($orderWeightComment)->change(); + $table->decimal('total_volume', 10, 3)->nullable()->comment($orderVolumeComment)->change(); + }); + } + + if (Schema::hasTable('ecommerce_order_options')) { + Schema::table('ecommerce_order_options', function (Blueprint $table) use ( + $unitWeightComment, + $unitVolumeComment, + $subtotalWeightComment, + $subtotalVolumeComment + ) { + $table->decimal('unit_weight', 10, 3)->nullable()->comment($unitWeightComment)->change(); + $table->decimal('unit_volume', 10, 3)->nullable()->comment($unitVolumeComment)->change(); + $table->decimal('subtotal_weight', 10, 3)->nullable()->comment($subtotalWeightComment)->change(); + $table->decimal('subtotal_volume', 10, 3)->nullable()->comment($subtotalVolumeComment)->change(); + }); + } + } +}; diff --git a/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_17_000001_add_soft_deletes_to_ecommerce_product_inquiries_table.php b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_17_000001_add_soft_deletes_to_ecommerce_product_inquiries_table.php new file mode 100644 index 00000000..1eef11e8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/database/migrations/2026_08_17_000001_add_soft_deletes_to_ecommerce_product_inquiries_table.php @@ -0,0 +1,45 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +/** + * 상품 문의 피벗 테이블에 소프트 삭제 컬럼을 추가합니다. + * + * 게시판에서 질문 글을 삭제했다가 복원하는 경로에서 피벗도 함께 복원되어야 + * 하는데, 종전에는 피벗이 하드 삭제라 복원이 불가능했습니다(#107 후속). + * 상품 자체가 영구 삭제되는 경로만 피벗도 영구 삭제합니다. + */ +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + if (! Schema::hasTable('ecommerce_product_inquiries') + || Schema::hasColumn('ecommerce_product_inquiries', 'deleted_at')) { + return; + } + + Schema::table('ecommerce_product_inquiries', function (Blueprint $table) { + $table->softDeletes()->comment('소프트 삭제 일시'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (! Schema::hasTable('ecommerce_product_inquiries') + || ! Schema::hasColumn('ecommerce_product_inquiries', 'deleted_at')) { + return; + } + + Schema::table('ecommerce_product_inquiries', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/OrderSeeder.php b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/OrderSeeder.php index ed40a31f..b2fa2d3c 100644 --- a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/OrderSeeder.php +++ b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/OrderSeeder.php @@ -932,15 +932,18 @@ class OrderSeeder extends Seeder { $subtotal = (int) $orderOption->subtotal_price; $quantity = (int) $orderOption->quantity; - $weight = (float) ($orderOption->subtotal_weight ?? $quantity * 0.5); - $volume = (float) ($orderOption->subtotal_volume ?? $quantity * 1000); - $baseFee = (int) $countrySetting->base_fee; + // 주문 옵션의 무게/부피는 g / cm³ 로 저장된다 (미입력 시 1개당 500g / 1,000cm³ 로 가정) + $weightGram = (float) ($orderOption->subtotal_weight ?? $quantity * 500); + $volumeCubicCm = (float) ($orderOption->subtotal_volume ?? $quantity * 1000); + $baseFee = (int) round((float) $countrySetting->base_fee); $ranges = $countrySetting->ranges; - // 부피무게 계산 + // 배송정책의 구간·단위값은 kg / L 이므로 여기서 환산한다 (엔진과 동일 규칙) $volumeWeightDivisor = $ranges['volume_weight_divisor'] ?? 6000; - $volumeWeight = $volumeWeightDivisor > 0 ? $volume / $volumeWeightDivisor : 0.0; - $chargeableWeight = max($weight, $volumeWeight); + $weightKg = $weightGram / 1000; + $volumeLiter = $volumeCubicCm / 1000; + $volumetricWeightKg = $volumeWeightDivisor > 0 ? $volumeCubicCm / $volumeWeightDivisor : 0.0; + $chargeableWeightKg = max($weightKg, $volumetricWeightKg); return match ($countrySetting->charge_policy) { ChargePolicyEnum::FREE => 0, @@ -948,38 +951,55 @@ class OrderSeeder extends Seeder ChargePolicyEnum::CONDITIONAL_FREE => $subtotal >= (int) $countrySetting->free_threshold ? 0 : $baseFee, ChargePolicyEnum::RANGE_AMOUNT => $this->calculateSeederRangeFee($ranges, $subtotal), ChargePolicyEnum::RANGE_QUANTITY => $this->calculateSeederRangeFee($ranges, $quantity), - ChargePolicyEnum::RANGE_WEIGHT => $this->calculateSeederRangeFee($ranges, (int) ($weight * 1000)), - ChargePolicyEnum::RANGE_VOLUME => $this->calculateSeederRangeFee($ranges, (int) $volume), - ChargePolicyEnum::RANGE_VOLUME_WEIGHT => $this->calculateSeederRangeFee($ranges, (int) ($chargeableWeight * 1000)), - ChargePolicyEnum::PER_QUANTITY => (int) (ceil($quantity / max($ranges['unit_value'] ?? 1, 1)) * $baseFee), - ChargePolicyEnum::PER_WEIGHT => (int) (ceil($weight / max($ranges['unit_value'] ?? 0.5, 0.01)) * $baseFee), - ChargePolicyEnum::PER_VOLUME => (int) (ceil($volume / max($ranges['unit_value'] ?? 1000, 1)) * $baseFee), - ChargePolicyEnum::PER_VOLUME_WEIGHT => (int) (ceil($chargeableWeight / max($ranges['unit_value'] ?? 0.5, 0.01)) * $baseFee), - ChargePolicyEnum::PER_AMOUNT => (int) (ceil($subtotal / max($ranges['unit_value'] ?? 10000, 1)) * $baseFee), + ChargePolicyEnum::RANGE_WEIGHT => $this->calculateSeederRangeFee($ranges, $weightKg), + ChargePolicyEnum::RANGE_VOLUME => $this->calculateSeederRangeFee($ranges, $volumeLiter), + ChargePolicyEnum::RANGE_VOLUME_WEIGHT => $this->calculateSeederRangeFee($ranges, $chargeableWeightKg), + ChargePolicyEnum::PER_QUANTITY => (int) round(ceil($quantity / max($ranges['unit_value'] ?? 1, 1)) * $baseFee), + ChargePolicyEnum::PER_WEIGHT => (int) round(ceil($weightKg / max($ranges['unit_value'] ?? 0.5, 0.01)) * $baseFee), + ChargePolicyEnum::PER_VOLUME => (int) round(ceil($volumeLiter / max($ranges['unit_value'] ?? 1, 0.01)) * $baseFee), + ChargePolicyEnum::PER_VOLUME_WEIGHT => (int) round(ceil($chargeableWeightKg / max($ranges['unit_value'] ?? 0.5, 0.01)) * $baseFee), + ChargePolicyEnum::PER_AMOUNT => (int) round(ceil($subtotal / max($ranges['unit_value'] ?? 10000, 1)) * $baseFee), ChargePolicyEnum::API => $baseFee, // API는 시더에서 base_fee 사용 default => $baseFee, }; } /** - * 구간별 배송비 계산 (시더용) + * 구간별 배송비 계산 (시더용 — 상한 포함 사다리 매칭) + * + * 구간을 상한 오름차순(무제한이 마지막)으로 정렬한 뒤, 값이 상한 이하인 첫 구간의 + * 배송비를 반환합니다. 시작값은 표시용이라 매칭에 쓰지 않습니다 + * (OrderCalculationService::calculateRangeFee 와 동일 규칙). * * @param array|null $ranges 구간 설정 - * @param int $value 비교 값 + * @param int|float $value 비교 값 (금액·수량·kg·L) * @return int 배송비 */ - private function calculateSeederRangeFee(?array $ranges, int $value): int + private function calculateSeederRangeFee(?array $ranges, int|float $value): int { - if (empty($ranges) || empty($ranges['tiers'])) { + $tiers = is_array($ranges) ? ($ranges['tiers'] ?? []) : []; + + if (! is_array($tiers) || count($tiers) === 0) { return 0; } - foreach ($ranges['tiers'] as $tier) { - $min = $tier['min'] ?? 0; - $max = $tier['max'] ?? PHP_INT_MAX; + $sortedTiers = array_values($tiers); + usort($sortedTiers, function ($a, $b) { + $aMax = ($a['max'] ?? null) === null || ($a['max'] ?? null) === '' ? null : (float) $a['max']; + $bMax = ($b['max'] ?? null) === null || ($b['max'] ?? null) === '' ? null : (float) $b['max']; - if ($value >= $min && ($max === null || $value < $max)) { - return (int) ($tier['fee'] ?? 0); + if ($aMax === null && $bMax === null) { + return 0; + } + + return $aMax === null ? 1 : ($bMax === null ? -1 : $aMax <=> $bMax); + }); + + foreach ($sortedTiers as $tier) { + $max = ($tier['max'] ?? null) === null || ($tier['max'] ?? null) === '' ? null : (float) $tier['max']; + + if ($max === null || (float) $value <= $max) { + return (int) round((float) ($tier['fee'] ?? 0)); } } diff --git a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ProductInquirySeeder.php b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ProductInquirySeeder.php index 63f6ab67..43b56806 100644 --- a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ProductInquirySeeder.php +++ b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ProductInquirySeeder.php @@ -163,11 +163,14 @@ class ProductInquirySeeder extends Seeder */ private function deleteExistingInquiries(Board $board): void { - $existingCount = ProductInquiry::count(); + // SoftDeletes 도입 여파: 소프트 삭제된 피벗 잔존물까지 포함해 전량 재시드한다 + $existingCount = ProductInquiry::withTrashed()->count(); if ($existingCount > 0) { // 연결된 게시판 Post도 함께 삭제 - $inquirableIds = ProductInquiry::where('inquirable_type', Post::class)->pluck('inquirable_id'); + $inquirableIds = ProductInquiry::withTrashed() + ->where('inquirable_type', Post::class) + ->pluck('inquirable_id'); if ($inquirableIds->isNotEmpty()) { Post::whereIn('id', $inquirableIds)->forceDelete(); @@ -178,7 +181,7 @@ class ProductInquirySeeder extends Seeder ->whereNotNull('parent_id') ->forceDelete(); - ProductInquiry::query()->delete(); + ProductInquiry::withTrashed()->forceDelete(); $this->command->warn("기존 문의 데이터 {$existingCount}건을 삭제했습니다."); } @@ -222,17 +225,17 @@ class ProductInquirySeeder extends Seeder // board_posts에 문의 게시글 생성 $post = Post::create([ - 'board_id' => $board->id, - 'title' => $this->inquiryTitles[$titleIndex], - 'content' => $this->inquiryContents[$titleIndex], + 'board_id' => $board->id, + 'title' => $this->inquiryTitles[$titleIndex], + 'content' => $this->inquiryContents[$titleIndex], 'content_mode' => 'text', - 'user_id' => $user?->id, - 'author_name' => $isGuest ? $this->guestNames[array_rand($this->guestNames)] : null, - 'password' => $isGuest ? Hash::make('1234') : null, - 'ip_address' => '127.0.0.1', - 'is_secret' => $isSecret, - 'status' => PostStatus::Published, - 'created_at' => now()->subDays(rand(1, 60))->subHours(rand(0, 23)), + 'user_id' => $user?->id, + 'author_name' => $isGuest ? $this->guestNames[array_rand($this->guestNames)] : null, + 'password' => $isGuest ? Hash::make('1234') : null, + 'ip_address' => '127.0.0.1', + 'is_secret' => $isSecret, + 'status' => PostStatus::Published, + 'created_at' => now()->subDays(rand(1, 60))->subHours(rand(0, 23)), ]); $hasReply = $admin && rand(1, 100) <= self::REPLY_RATE; @@ -243,28 +246,28 @@ class ProductInquirySeeder extends Seeder $answeredAt = $post->created_at->addDays(rand(0, 3)); Post::create([ - 'board_id' => $board->id, - 'title' => 'Re: '.$post->title, - 'content' => $this->replyContents[$titleIndex], + 'board_id' => $board->id, + 'title' => 'Re: '.$post->title, + 'content' => $this->replyContents[$titleIndex], 'content_mode' => 'text', - 'user_id' => $admin->id, - 'ip_address' => '127.0.0.1', - 'is_secret' => false, - 'status' => PostStatus::Published, - 'parent_id' => $post->id, - 'depth' => 1, - 'created_at' => $answeredAt, + 'user_id' => $admin->id, + 'ip_address' => '127.0.0.1', + 'is_secret' => false, + 'status' => PostStatus::Published, + 'parent_id' => $post->id, + 'depth' => 1, + 'created_at' => $answeredAt, ]); } // ecommerce_product_inquiries 피벗 생성 ProductInquiry::create([ - 'product_id' => $product->id, - 'inquirable_type' => Post::class, - 'inquirable_id' => $post->id, - 'user_id' => $user?->id, - 'is_answered' => $hasReply, - 'answered_at' => $answeredAt, + 'product_id' => $product->id, + 'inquirable_type' => Post::class, + 'inquirable_id' => $post->id, + 'user_id' => $user?->id, + 'is_answered' => $hasReply, + 'answered_at' => $answeredAt, 'product_name_snapshot' => $product->name ?? [], ]); diff --git a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ShippingPolicySeeder.php b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ShippingPolicySeeder.php index 1d5d5f26..4ae4e4e7 100644 --- a/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ShippingPolicySeeder.php +++ b/modules/_bundled/sirsoft-ecommerce/database/seeders/Sample/ShippingPolicySeeder.php @@ -162,12 +162,13 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'amount', + // 연속형(금액) 구간: 다음 시작값 = 직전 종료값, 종료값은 포함 'tiers' => [ - ['min' => 0, 'max' => 10000, 'unit' => '원', 'fee' => 5000], - ['min' => 10000, 'max' => 30000, 'unit' => '원', 'fee' => 3000], - ['min' => 30000, 'max' => 50000, 'unit' => '원', 'fee' => 2000], - ['min' => 50000, 'max' => 100000, 'unit' => '원', 'fee' => 1000], - ['min' => 100000, 'max' => null, 'unit' => '원', 'fee' => 0], + ['min' => 0, 'max' => 10000, 'fee' => 5000], + ['min' => 10000, 'max' => 30000, 'fee' => 3000], + ['min' => 30000, 'max' => 50000, 'fee' => 2000], + ['min' => 50000, 'max' => 100000, 'fee' => 1000], + ['min' => 100000, 'max' => null, 'fee' => 0], ], ], 'extra_fee_enabled' => false, @@ -190,9 +191,10 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'quantity', + // 이산형(수량) 구간: 다음 시작값 = 직전 종료값 + 1, 종료값은 포함 'tiers' => [ - ['min' => 1, 'max' => 5, 'unit' => '개', 'fee' => 3000], - ['min' => 6, 'max' => null, 'unit' => '개', 'fee' => 5000], + ['min' => 0, 'max' => 5, 'fee' => 3000], + ['min' => 6, 'max' => null, 'fee' => 5000], ], ], 'extra_fee_enabled' => false, @@ -215,11 +217,12 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'weight', + // 무게 구간은 kg 단위 (상품 옵션의 g 값은 배송비 계산 시점에 환산된다) 'tiers' => [ - ['min' => 0, 'max' => 2, 'unit' => 'kg', 'fee' => 3000], - ['min' => 2, 'max' => 5, 'unit' => 'kg', 'fee' => 4000], - ['min' => 5, 'max' => 10, 'unit' => 'kg', 'fee' => 6000], - ['min' => 10, 'max' => null, 'unit' => 'kg', 'fee' => 8000], + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 2, 'max' => 5, 'fee' => 4000], + ['min' => 5, 'max' => 10, 'fee' => 6000], + ['min' => 10, 'max' => null, 'fee' => 8000], ], ], 'extra_fee_enabled' => false, @@ -242,10 +245,11 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'volume', + // 부피 구간은 L 단위 (상품 옵션의 cm³ 값은 배송비 계산 시점에 환산된다) 'tiers' => [ - ['min' => 0, 'max' => 50, 'unit' => 'L', 'fee' => 5000], - ['min' => 50, 'max' => 100, 'unit' => 'L', 'fee' => 10000], - ['min' => 100, 'max' => null, 'unit' => 'L', 'fee' => 20000], + ['min' => 0, 'max' => 50, 'fee' => 5000], + ['min' => 50, 'max' => 100, 'fee' => 10000], + ['min' => 100, 'max' => null, 'fee' => 20000], ], ], 'extra_fee_enabled' => false, @@ -268,11 +272,12 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'volume_weight', + // 부피무게 구간은 kg 단위 (실무게 kg 과 부피무게 kg 중 큰 값으로 매칭) 'tiers' => [ - ['min' => 0, 'max' => 5, 'unit' => 'kg', 'fee' => 3500], - ['min' => 5, 'max' => 10, 'unit' => 'kg', 'fee' => 5000], - ['min' => 10, 'max' => 20, 'unit' => 'kg', 'fee' => 8000], - ['min' => 20, 'max' => null, 'unit' => 'kg', 'fee' => 12000], + ['min' => 0, 'max' => 5, 'fee' => 3500], + ['min' => 5, 'max' => 10, 'fee' => 5000], + ['min' => 10, 'max' => 20, 'fee' => 8000], + ['min' => 20, 'max' => null, 'fee' => 12000], ], ], 'extra_fee_enabled' => false, @@ -292,9 +297,10 @@ class ShippingPolicySeeder extends Seeder 'shipping_method' => 'parcel', 'currency_code' => 'KRW', 'charge_policy' => 'api', - 'base_fee' => 0, + // 외부 API 장애 시 폴백 배송비 (0 이면 장애가 곧 무음 무료배송이 된다) + 'base_fee' => 30000, 'api_endpoint' => 'https://api.example.com/shipping/calculate', - 'api_request_fields' => ['order_amount', 'weight', 'zipcode'], + 'api_request_fields' => ['group_total', 'total_quantity', 'country_code'], 'api_response_fee_field' => 'shipping_fee', // 계산 API 연동 상세 설정 (MP12 — A13) — api_config JSON 'api_config' => [ @@ -318,9 +324,10 @@ class ShippingPolicySeeder extends Seeder 'shipping_method' => 'parcel', 'currency_code' => 'KRW', 'charge_policy' => 'api', - 'base_fee' => 0, + // 외부 API 장애 시 폴백 배송비 (0 이면 장애가 곧 무음 무료배송이 된다) + 'base_fee' => 30000, 'api_endpoint' => 'https://api.example.com/shipping/calculate', - 'api_request_fields' => ['order_amount', 'weight', 'zipcode'], + 'api_request_fields' => ['group_total', 'total_quantity', 'country_code'], 'api_response_fee_field' => 'shipping_fee', // 계산 API 연동 상세 설정 (MP12 — A13) — api_config JSON 'api_config' => [ @@ -344,9 +351,10 @@ class ShippingPolicySeeder extends Seeder 'shipping_method' => 'parcel', 'currency_code' => 'KRW', 'charge_policy' => 'api', - 'base_fee' => 0, + // 외부 API 장애 시 폴백 배송비 (0 이면 장애가 곧 무음 무료배송이 된다) + 'base_fee' => 30000, 'api_endpoint' => 'https://api.example.com/shipping/calculate', - 'api_request_fields' => ['order_amount', 'weight', 'zipcode'], + 'api_request_fields' => ['group_total', 'total_quantity', 'country_code'], 'api_response_fee_field' => 'shipping_fee', // 계산 API 연동 상세 설정 (MP12 — A13) — api_config JSON 'api_config' => [ @@ -398,7 +406,7 @@ class ShippingPolicySeeder extends Seeder 'currency_code' => 'KRW', 'charge_policy' => 'per_quantity', 'base_fee' => 3000, - 'ranges' => ['unit_value' => 3], + 'ranges' => ['type' => 'per_quantity', 'unit_value' => 3], 'extra_fee_enabled' => false, 'is_active' => true, ], @@ -417,7 +425,7 @@ class ShippingPolicySeeder extends Seeder 'currency_code' => 'KRW', 'charge_policy' => 'per_weight', 'base_fee' => 1000, - 'ranges' => ['unit_value' => 1], + 'ranges' => ['type' => 'per_weight', 'unit_value' => 1], 'extra_fee_enabled' => false, 'is_active' => true, ], @@ -436,7 +444,7 @@ class ShippingPolicySeeder extends Seeder 'currency_code' => 'KRW', 'charge_policy' => 'per_volume', 'base_fee' => 2000, - 'ranges' => ['unit_value' => 10], + 'ranges' => ['type' => 'per_volume', 'unit_value' => 10], 'extra_fee_enabled' => false, 'is_active' => true, ], @@ -455,7 +463,7 @@ class ShippingPolicySeeder extends Seeder 'currency_code' => 'KRW', 'charge_policy' => 'per_volume_weight', 'base_fee' => 3000, - 'ranges' => ['unit_value' => 5], + 'ranges' => ['type' => 'per_volume_weight', 'unit_value' => 5], 'extra_fee_enabled' => false, 'is_active' => true, ], @@ -467,10 +475,11 @@ class ShippingPolicySeeder extends Seeder 'base_fee' => 0, 'ranges' => [ 'type' => 'weight', + // 배송비는 기본 통화(KRW) 기준 금액이다 — 국가만 US 일 뿐 통화가 바뀌지 않는다 'tiers' => [ - ['min' => 0, 'max' => 2, 'unit' => 'kg', 'fee' => 25], - ['min' => 2, 'max' => 5, 'unit' => 'kg', 'fee' => 40], - ['min' => 5, 'max' => null, 'unit' => 'kg', 'fee' => 60], + ['min' => 0, 'max' => 2, 'fee' => 25000], + ['min' => 2, 'max' => 5, 'fee' => 40000], + ['min' => 5, 'max' => null, 'fee' => 60000], ], ], 'extra_fee_enabled' => false, @@ -491,7 +500,7 @@ class ShippingPolicySeeder extends Seeder 'currency_code' => 'KRW', 'charge_policy' => 'per_amount', 'base_fee' => 500, - 'ranges' => ['unit_value' => 10000], + 'ranges' => ['type' => 'per_amount', 'unit_value' => 10000], 'extra_fee_enabled' => false, 'is_active' => true, ], diff --git a/modules/_bundled/sirsoft-ecommerce/dist/js/module.iife.js b/modules/_bundled/sirsoft-ecommerce/dist/js/module.iife.js index f1df4878..33f83930 100644 --- a/modules/_bundled/sirsoft-ecommerce/dist/js/module.iife.js +++ b/modules/_bundled/sirsoft-ecommerce/dist/js/module.iife.js @@ -1,2 +1 @@ -var SirsoftEcommerce=function(Ae){"use strict";const Lt={KRW:0,JPY:0},xt={KRW:"₩",USD:"$",JPY:"¥",CNY:"元",EUR:"€",GBP:"£"},Pt={KRW:1e3,JPY:100};function At(r){if(!r)return 1;const c=r.base_unit??Pt[r.code]??1;return Math.max(1,c)}function kt(r,c,e){const t=parseFloat(c)||1;if(t<=0)return r;const o=r/t;let a;switch(e){case"ceil":a=Math.ceil(o);break;case"floor":a=Math.floor(o);break;default:a=Math.round(o)}return a*t}function de(r,c){const e=typeof c=="string"?{code:c}:c,t=e.code,o=e.symbol&&e.symbol.length>0?e.symbol:xt[t]??"",a=e.decimal_places??Lt[t]??2,n=r.toLocaleString(void 0,{minimumFractionDigits:a,maximumFractionDigits:a});return o?t==="KRW"||o==="₩"||o==="원"?n+"원":o+n:n+" "+t}function Ot(){try{const e=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(e?.currencies)?e.currencies:[]}catch{return[]}}function ue(r,c){const e=Ot(),t=c||e.find(o=>o.is_default)?.code;return t?de(r,e.find(o=>o.code===t)??t):r.toLocaleString()}function se(r,c){const{basePrice:e,currencies:t}=r,o=parseFloat(String(e))||0;if(!t||!Array.isArray(t)||t.length===0)return{};const a=t.find(i=>i.is_default),n=At(a),s={};for(const i of t){const l=i.code;if(i.is_default??!1)s[l]={price:o,formatted:de(o,i),is_default:!0,editable:!0};else if(i.exchange_rate&&i.exchange_rate>0){const d=o/n*i.exchange_rate,m=kt(d,i.rounding_unit||"0.01",i.rounding_method||"round");s[l]={price:m,formatted:de(m,i),is_default:!1,editable:!1,exchange_rate:i.exchange_rate}}}return s}const z=window.G7Core?.createLogger?.("Ecom:UpdateProduct")??{log:(...r)=>console.log("[Ecom:UpdateProduct]",...r),warn:(...r)=>console.warn("[Ecom:UpdateProduct]",...r),error:(...r)=>console.error("[Ecom:UpdateProduct]",...r)};function Gt(r,c){const e=r.params||{},{productId:t,field:o,dataSourceId:a="products"}=e;let{value:n}=e;if(n&&typeof n=="object"&&n.target!==void 0&&(n=n.target?.value,z.log("[updateProductField] Extracted value from event:",n)),!t||!o){z.warn("[updateProductField] Missing required params:",{productId:t,field:o});return}if(n===void 0){z.warn("[updateProductField] Value is undefined, skipping update");return}const s=window.G7Core;if(!s?.dataSource?.get||!s?.dataSource?.set){z.warn("[updateProductField] G7Core.dataSource API is not available");return}const i=s.dataSource.get(a);if(!i){console.warn(`[updateProductField] DataSource '${a}' not found`);return}const l=i.data?.data||[];if(!Array.isArray(l)||l.length===0){z.warn("[updateProductField] Products data is empty or invalid");return}const u=s.state.get()||{},d=u.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies;let p=[...u._local?.productFieldErrors||[]];const g=l.map(C=>{if(String(C.id)===String(t)){const x=C[o];if(typeof n=="object"&&n!==null?JSON.stringify(x)===JSON.stringify(n):String(x)===String(n))return z.log(`[updateProductField] No change for product ${t}.${o}, skipping`),C;let S=n;const N=parseFloat(n)||0;if(o==="selling_price"){const O=parseFloat(C.list_price)||0;O>0&&N>O?(p.some(Q=>Q.productId===String(t)&&Q.field===o)||p.push({productId:String(t),field:"selling_price",message:"판매가는 정가를 초과할 수 없습니다."}),S=O):p=p.filter($=>!($.productId===String(t)&&$.field===o))}else p=p.filter(O=>!(O.productId===String(t)&&O.field===o));const L={...C,[o]:S,_modified:!0};return o==="selling_price"&&d&&Array.isArray(d)&&(L.multi_currency_selling_price=se({basePrice:S,currencies:d}),z.log(`[updateProductField] Recalculated multi_currency_selling_price for product ${t}`)),L}return C});s.dataSource.set(a,{...i,data:{...i.data,data:g}});const w=s.state.getLocal()||{},y=new Set(w.modifiedProductIds||[]);y.add(t);const _={...w.modifiedProductFields||{}},f=String(t),b=new Set(_[f]||[]);b.add(o),_[f]=Array.from(b),s.state.setLocal({modifiedProductIds:Array.from(y),modifiedProductFields:_,productFieldErrors:p}),z.log(`[updateProductField] Updated product ${t}.${o} =`,n)}const B=window.G7Core?.createLogger?.("Ecom:UpdateOption")??{log:(...r)=>console.log("[Ecom:UpdateOption]",...r),warn:(...r)=>console.warn("[Ecom:UpdateOption]",...r),error:(...r)=>console.error("[Ecom:UpdateOption]",...r)};function Et(r,c){const e=r.params||{},{productId:t,optionId:o,field:a,value:n,dataSourceId:s="products"}=e;if(!t||!o||!a){B.warn("[updateOptionField] Missing required params:",{productId:t,optionId:o,field:a});return}const i=window.G7Core;if(!i?.dataSource?.get||!i?.dataSource?.set){B.warn("[updateOptionField] G7Core.dataSource API is not available");return}const l=i.dataSource.get(s);if(!l){console.warn(`[updateOptionField] DataSource '${s}' not found`);return}const u=l.data?.data||[];if(!Array.isArray(u)||u.length===0){B.warn("[updateOptionField] Products data is empty or invalid");return}const d=i.state.get()||{},m=d.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies;let g=[...d._local?.optionFieldErrors||[]],w=!1,y=!1;const _=u.map(N=>{if(String(N.id)===String(t)){const L=N.options||[],O=L.findIndex(D=>String(D.id)===String(o));if(O===-1)return B.warn("[updateOptionField] Option not found:",o),N;y=!0;const $=L[O],Q=$[a];if(typeof n=="object"&&n!==null?JSON.stringify(Q)===JSON.stringify(n):String(Q)===String(n))return B.log(`[updateOptionField] No change for option ${o}.${a}, skipping`),N;let J=n;const Pe=parseFloat(n)||0;if(a==="selling_price"){const D=parseFloat($.list_price)||0;D>0&&Pe>D?(g.some(he=>he.productId===String(t)&&he.optionId===String(o)&&he.field===a)||g.push({productId:String(t),optionId:String(o),field:"selling_price",message:"판매가는 정가를 초과할 수 없습니다."}),J=D):g=g.filter(H=>!(H.productId===String(t)&&H.optionId===String(o)&&H.field===a))}else g=g.filter(D=>!(D.productId===String(t)&&D.optionId===String(o)&&D.field===a));const re=[...L],ne={...re[O],[a]:J,_modified:!0};let ae=null;if(a==="selling_price"){const D=parseFloat(J)||0;if($.is_default===!0)ae=D,ne.price_adjustment=0;else{const H=parseFloat(N.selling_price)||0;ne.price_adjustment=D-H}ne.price_adjustment_formatted=(ne.price_adjustment>=0?"+":"")+ne.price_adjustment.toLocaleString()+"원"}if(a==="selling_price"&&m&&Array.isArray(m)&&(ne.multi_currency_selling_price=se({basePrice:J,currencies:m}),B.log(`[updateOptionField] Recalculated multi_currency_selling_price for option ${o}`)),re[O]=ne,ae!==null)for(let D=0;D<re.length;D++){if(D===O)continue;const H=re[D],he=parseFloat(H.price_adjustment)||0,St=ae+he,vt={...H,selling_price:St};m&&Array.isArray(m)&&(vt.multi_currency_selling_price=se({basePrice:St,currencies:m})),re[D]=vt}let Ke=N.stock_quantity,Qe=N.option_stock_sum;a==="stock_quantity"&&(Qe=re.reduce((D,H)=>H.is_active===!1?D:D+(parseInt(H.stock_quantity,10)||0),0),Ke=Qe,B.log(`[updateOptionField] Updated product stock to option sum: ${Ke}`));const ze={...N,options:re,stock_quantity:Ke,option_stock_sum:Qe,_modified:!0};return ae!==null&&(ze.selling_price=ae,m&&Array.isArray(m)&&(ze.multi_currency_selling_price=se({basePrice:ae,currencies:m})),w=!0,B.log(`[updateOptionField] Reverse-synced product ${t} selling_price to ${ae} from default option`)),ze}return N});if(!y)return;i.dataSource.set(s,{...l,data:{...l.data,data:_}});const f=i.state.getLocal()||{},b=new Set(f.modifiedOptionIds||[]),C=`${t}-${o}`;b.add(C);const x={...f.modifiedOptionFields||{}},P=new Set(x[C]||[]);P.add(a),x[C]=Array.from(P);const S={modifiedOptionIds:Array.from(b),modifiedOptionFields:x,optionFieldErrors:g};if(w){const N=new Set(f.modifiedProductIds||[]);N.add(t);const L={...f.modifiedProductFields||{}},O=String(t),$=new Set(L[O]||[]);$.add("selling_price"),L[O]=Array.from($),S.modifiedProductIds=Array.from(N),S.modifiedProductFields=L}i.state.setLocal(S),B.log(`[updateOptionField] Updated option ${t}/${o}.${a} =`,n)}const Ft="g7_preferred_currency",Dt="g7_locale",Ye=window.G7Core?.createLogger?.("Handler:InitPreferredCurrency")??{log:(...r)=>console.log("[Handler:InitPreferredCurrency]",...r),warn:(...r)=>console.warn("[Handler:InitPreferredCurrency]",...r),error:(...r)=>console.error("[Handler:InitPreferredCurrency]",...r)};function $t(r){const c=window.G7Core;c?.state?.set?c.state.set(r):Ye.warn("G7Core.state.set not available")}function Nt(){try{return localStorage.getItem(Dt)}catch{return null}}function We(){try{const e=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(e?.currencies)?e.currencies:[]}catch{return[]}}function Tt(r){if(!r)return null;const c=r.toLowerCase().split("-")[0].split("_")[0];if(!c)return null;const t=We().filter(o=>(Array.isArray(o?.locales)?o.locales:[]).some(n=>String(n).toLowerCase()===c));return t.length===1?t[0]?.code??null:t.length>1?t.find(a=>a?.is_default===!0)?.code??null:null}function Rt(){try{const r=window.G7Config;return r?.user?r.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}function Mt(r,c){const e=r?.params?.defaultCurrency||We().find(l=>l?.is_default)?.code||"",t=l=>typeof l=="string"&&/^[A-Z]{3}$/.test(l),o=Rt(),a=r?.params?.accountCurrency??o?.ecommerce_preferred_currency;let n=null;try{n=localStorage.getItem(Ft)}catch{}const s=a?null:Tt(Nt()),i=t(a)?a:t(n)?n:t(s)?s:t(e)?e:"KRW";$t({preferredCurrency:i}),Ye.log("Preferred currency initialized:",i)}const Ht="g7_preferred_shipping_country",Je=window.G7Core?.createLogger?.("Handler:InitPreferredShippingCountry")??{log:(...r)=>console.log("[Handler:InitPreferredShippingCountry]",...r),warn:(...r)=>console.warn("[Handler:InitPreferredShippingCountry]",...r),error:(...r)=>console.error("[Handler:InitPreferredShippingCountry]",...r)};function qt(r){const c=window.G7Core;c?.state?.set?c.state.set(r):Je.warn("G7Core.state.set not available")}function Ut(){try{const r=window.G7Config;return r?.user?r.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}function Vt(r,c){const e=r?.params?.defaultCountry||"KR",t=i=>typeof i=="string"&&/^[A-Z]{2}$/.test(i),o=Ut(),a=r?.params?.accountCountry??o?.ecommerce_preferred_shipping_country;let n=null;try{n=localStorage.getItem(Ht)}catch{}const s=t(a)?a:t(n)?n:t(e)?e:"KR";qt({preferredShippingCountry:s}),Je.log("Preferred shipping country initialized:",s)}function X(r){const c=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${c}-${e}-${t}`}function jt(r){const c=new Date,e=X(c);let t;switch(r){case"today":t=e;break;case"3days":{const o=new Date(c);o.setDate(o.getDate()-2),t=X(o);break}case"week":{const o=new Date(c);o.setDate(o.getDate()-6),t=X(o);break}case"month":{const o=new Date(c);o.setMonth(o.getMonth()-1),t=X(o);break}case"3months":{const o=new Date(c);o.setMonth(o.getMonth()-3),t=X(o);break}case"6months":{const o=new Date(c);o.setMonth(o.getMonth()-6),t=X(o);break}case"1year":{const o=new Date(c);o.setFullYear(o.getFullYear()-1),t=X(o);break}case"all":default:return t="",{startDate:"",endDate:""}}return{startDate:t,endDate:e}}function Bt(r){const{preset:c}=r.params||{preset:"today"},{startDate:e,endDate:t}=jt(c);return{startDate:e,endDate:t,preset:c}}const ie=window.G7Core?.createLogger?.("Ecom:DefaultOption")??{log:(...r)=>console.log("[Ecom:DefaultOption]",...r),warn:(...r)=>console.warn("[Ecom:DefaultOption]",...r),error:(...r)=>console.error("[Ecom:DefaultOption]",...r)};function Kt(r,c){const e=r.params||{},{productId:t,optionId:o,dataSourceId:a="products"}=e;if(!t||!o){ie.warn("[setDefaultOption] Missing required params:",{productId:t,optionId:o});return}const n=window.G7Core;if(!n?.dataSource?.get||!n?.dataSource?.set){ie.warn("[setDefaultOption] G7Core.dataSource API is not available");return}const s=n.dataSource.get(a);if(!s){console.warn(`[setDefaultOption] DataSource '${a}' not found`);return}const i=s.data?.data||[];if(!Array.isArray(i)||i.length===0){ie.warn("[setDefaultOption] Products data is empty or invalid");return}const u=(n.state.get()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies,d=i.map(w=>{if(String(w.id)!==String(t))return w;const y=w.options||[];if(y.length===0)return w;const _=y.find(P=>String(P.id)===String(o));if(!_)return ie.warn("[setDefaultOption] Option not found:",o),w;const f=y.map(P=>{const S=String(P.id)===String(o);return{...P,is_default:S,_modified:S?!0:P._modified}}),b=_.list_price,C=_.selling_price;let x=w.multi_currency_selling_price;return u&&Array.isArray(u)&&(x=se({basePrice:C,currencies:u})),ie.log(`[setDefaultOption] Set option ${o} as default for product ${t}. Syncing prices: list_price=${b}, selling_price=${C}`),{...w,options:f,list_price:b,selling_price:C,multi_currency_selling_price:x,_modified:!0}});n.dataSource.set(a,{...s,data:{...s.data,data:d}});const m=n.state.getLocal()||{},p=new Set(m.modifiedProductIds||[]);p.add(String(t));const g=new Set(m.modifiedOptionIds||[]);g.add(`${t}-${o}`),n.state.setLocal({modifiedProductIds:Array.from(p),modifiedOptionIds:Array.from(g)}),ie.log(`[setDefaultOption] Option ${o} set as default for product ${t}`)}const ke=window.G7Core?.createLogger?.("Ecom:OptionSelect")??{log:(...r)=>console.log("[Ecom:OptionSelect]",...r),warn:(...r)=>console.warn("[Ecom:OptionSelect]",...r),error:(...r)=>console.error("[Ecom:OptionSelect]",...r)},me=(r,c)=>`${r}-${c}`,Oe=r=>r.split("-")[0];function Qt(r,c){const e=r.params||{},{productId:t,optionId:o,checked:a}=e,n=window.G7Core;if(!n?.state){ke.warn("[toggleOption] G7Core.state를 사용할 수 없습니다.");return}const s=n.state.getLocal(),i=s.selectedOptionIds||[],l=s.selectedItems||[],u=me(t,o);let d;a?i.includes(u)?d=i:d=[...i,u]:d=i.filter(f=>f!==u);const w=(n.dataSource?.get("products")?.data?.data||[]).find(f=>String(f.id)===String(t))?.options||[];let y=[...l];const _=Number(t);w.length>0&&(w.map(C=>me(t,C.id)).filter(C=>d.includes(C)).length===w.length?y.includes(_)||y.push(_):y=y.filter(C=>C!==_)),n.state.setLocal({selectedOptionIds:d,selectedItems:y})}function Xe(r,c){const e=r.params||{},{productId:t,options:o,checked:a}=e,n=window.G7Core;if(!n?.state){ke.warn("[toggleProductOptions] G7Core.state를 사용할 수 없습니다.");return}const s=n.state.getLocal(),i=s.selectedOptionIds||[],l=s.selectedItems||[],u=(o||[]).map(g=>me(t,g.id));let d,m=[...l];const p=Number(t);a?(d=[...i.filter(w=>Oe(w)!==String(t)),...u],m.includes(p)||m.push(p)):(d=i.filter(g=>Oe(g)!==String(t)),m=m.filter(g=>g!==p)),n.state.setLocal({selectedOptionIds:d,selectedItems:m})}function zt(r,c){return Xe(r)}function Yt(r,c){const e=r.params||{products:[],selectedOptionIds:[]},{products:t,selectedOptionIds:o}=e,a=o||[];if(!t||!Array.isArray(t))return{};const n={};for(const s of t){const i=String(s.id),l=s.options||[];if(l.length===0){n[i]="none";continue}const d=l.map(m=>me(i,m.id)).filter(m=>a.includes(m)).length;d===0?n[i]="none":d===l.length?n[i]="all":n[i]="some"}return n}function Wt(r,c){const e=r.params||{},{selectedIds:t}=e,o=window.G7Core;if(!o?.state){ke.warn("[syncProductSelection] G7Core.state를 사용할 수 없습니다.");return}const a=o.state.getLocal(),n=a.selectedItems||[],s=a.selectedOptionIds||[],l=o.dataSource?.get("products")?.data?.data||[],u=t.filter(p=>!n.includes(p)),d=n.filter(p=>!t.includes(p));let m=[...s];for(const p of u){const g=l.find(w=>Number(w.id)===p);if(g?.options)for(const w of g.options){const y=me(p,w.id);m.includes(y)||m.push(y)}}for(const p of d)m=m.filter(g=>Oe(g)!==String(p));o.state.setLocal({selectedItems:t,selectedOptionIds:m})}const Ce=window.G7Core?.createLogger?.("Ecom:CopyCode")??{log:(...r)=>console.log("[Ecom:CopyCode]",...r),warn:(...r)=>console.warn("[Ecom:CopyCode]",...r),error:(...r)=>console.error("[Ecom:CopyCode]",...r)};async function Jt(r,c){const e=window.G7Core;if(!e?.state||!e?.api){Ce.warn("[generateCopyProductCode] G7Core API is not available");return}try{const t=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/generate-code");if(!t?.data?.product_code)throw new Error("Invalid API response");const o=t.data.product_code;if((window.__g7LayoutContextStack||[]).length>0&&e.state.setParentLocal)e.state.setParentLocal({"ui.copyOptions.newProductCode":o}),Ce.log(`[generateCopyProductCode] Generated (parent): ${o}`);else{const s=e.state.getLocal()||{};e.state.setLocal({ui:{...s.ui,copyOptions:{...s.ui?.copyOptions||{},newProductCode:o}}}),Ce.log(`[generateCopyProductCode] Generated (local): ${o}`)}}catch(t){Ce.error("[generateCopyProductCode] Failed:",t),e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.code_generate_error")??"Failed to generate product code.")}}const Ze=window.G7Core?.createLogger?.("Ecom:CopyProduct")??{log:(...r)=>console.log("[Ecom:CopyProduct]",...r),warn:(...r)=>console.warn("[Ecom:CopyProduct]",...r),error:(...r)=>console.error("[Ecom:CopyProduct]",...r)};async function Xt(r,c){const e=window.G7Core;if(!e?.state){Ze.warn("[copyProduct] G7Core API is not available");return}const t=e.state.getLocal()||{},a=(e.state.getParent?.()||{})._local||{},n=a.ui?.copyOptions||t.ui?.copyOptions||{},s=t.form?.product_code||a.form?.product_code||a.productList?.copyTargetProduct?.product_code||t.productList?.copyTargetProduct?.product_code;if(!s){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.copy_source_not_found")??"Could not find source product information.");return}e.modal?.close?.();const i={copy_id:s,copy_images:n.images??!0?"1":"0",copy_options:n.options??!0?"1":"0",copy_categories:n.categories??!0?"1":"0",copy_sales_info:n.sales_info??!0?"1":"0",copy_description:n.description??!0?"1":"0",copy_notice:n.notice??!0?"1":"0",copy_common_info:n.common_info??!0?"1":"0",copy_other_info:n.other_info??!0?"1":"0",copy_shipping:n.shipping??!0?"1":"0",copy_seo:n.seo??!1?"1":"0",copy_identification:n.identification??!0?"1":"0"},u=`/admin/ecommerce/products/create?${new URLSearchParams(i).toString()}`;Ze.log("[copyProduct] Navigating to:",u),e.navigate?e.navigate(u):c.navigate?c.navigate(u):window.location.href=u}const be=window.G7Core?.createLogger?.("Ecom:ExpandedOptions")??{log:(...r)=>console.log("[Ecom:ExpandedOptions]",...r),warn:(...r)=>console.warn("[Ecom:ExpandedOptions]",...r),error:(...r)=>console.error("[Ecom:ExpandedOptions]",...r)},Zt="/api/modules/sirsoft-ecommerce/admin/products/options",Ge=100,Ee=new Set;function eo(r,c){const t=r?.dataSource?.get?.(c)?.data?.data;return Array.isArray(t)?t:[]}function et(r,c,e){const t=r?.dataSource?.get?.(c),o=t?.data?.data;if(!t||!Array.isArray(o)){be.warn("[loadExpandedOptions] DataSource not available for merge:",c);return}const a=o.map(n=>{const s=String(n?.id);return Object.prototype.hasOwnProperty.call(e,s)?{...n,options:e[s]}:n});r.dataSource.set(c,{...t,data:{...t.data,data:a}})}function to(r,c){const e=r?.state?.getLocal?.()||{},t=e.selectedItems||[];if(t.length===0)return;const o=new Set(t.map(s=>String(s))),a=new Set(e.selectedOptionIds||[]);let n=!1;Object.entries(c).forEach(([s,i])=>{o.has(s)&&i.forEach(l=>{const u=`${s}-${l?.id}`;a.has(u)||(a.add(u),n=!0)})}),n&&r.state.setLocal({selectedOptionIds:Array.from(a)})}function Fe(r,c){const e=r?.state?.getLocal?.()||{},t=c(e.optionsLoadingIds||[],e.optionsErrorIds||[]);r.state.setLocal({optionsLoadingIds:t.loading,optionsErrorIds:t.errored})}function De(r,c){const e=r.params||{},t=e.dataSourceId||"products",o=e.force===!0,a=window.G7Core;if(!a?.dataSource?.get||!a?.dataSource?.set||!a?.state?.setLocal){be.warn("[loadExpandedOptions] G7Core dataSource/state API is not available");return}Array.isArray(e.expandedRows)&&a.state.setLocal({expandedRows:e.expandedRows});const n=Array.isArray(e.productIds)?e.productIds:e.productId!==void 0&&e.productId!==null?[e.productId]:Array.isArray(e.expandedRows)?e.expandedRows:a.state.getLocal?.()?.expandedRows||[];if(!Array.isArray(n)||n.length===0)return;const s=eo(a,t),i=new Map(s.map(d=>[String(d?.id),d])),l=[],u={};n.forEach(d=>{const m=String(d),p=i.get(m);if(p&&!(!o&&Array.isArray(p.options))){if(Number(p.options_total_count)===0){u[m]=[];return}Ee.has(m)||l.push(m)}}),Object.keys(u).length>0&&et(a,t,u),l.length!==0&&(l.length>Ge&&(be.warn(`[loadExpandedOptions] Requested ${l.length} products, truncating to ${Ge}`),l.length=Ge),l.forEach(d=>Ee.add(d)),Fe(a,(d,m)=>({loading:Array.from(new Set([...d,...l])),errored:m.filter(p=>!l.includes(p))})),a.api.get(Zt,{params:{product_ids:l.map(d=>Number(d))}}).then(d=>{const p=(d?.data||{}).options||{},g={};l.forEach(w=>{const y=p[w];g[w]=Array.isArray(y)?y:[]}),et(a,t,g),to(a,g),Fe(a,(w,y)=>({loading:w.filter(_=>!l.includes(_)),errored:y.filter(_=>!l.includes(_))}))}).catch(d=>{be.error("[loadExpandedOptions] Failed to load options:",d),Fe(a,(p,g)=>({loading:p.filter(w=>!l.includes(w)),errored:Array.from(new Set([...g,...l]))}));const m=d?.response?.data?.message||a.t?.("sirsoft-ecommerce.admin.product.table.options.load_failed")||"Failed to load product options.";a.toast?.error?.(m)}).finally(()=>{l.forEach(d=>Ee.delete(d))}))}function oo(r,c={}){const e=r?.state?.getLocal?.()?.expandedRows||[];!Array.isArray(e)||e.length===0||De({params:{productIds:e,force:c.force===!0,dataSourceId:c.dataSourceId||"products"}})}function ro(r,c){const e=r.params||{};De({params:{productId:e.productId,productIds:e.productIds,force:!0,dataSourceId:e.dataSourceId}})}const A=window.G7Core?.createLogger?.("Ecom:Category")??{log:(...r)=>console.log("[Ecom:Category]",...r),warn:(...r)=>console.warn("[Ecom:Category]",...r),error:(...r)=>console.error("[Ecom:Category]",...r)};function tt(r,c,e,t=[]){for(const o of r){const a=o.name?.[e]||o.name?.ko||"";if(o.id===c)return[...t,a].join(" > ");if(o.children&&o.children.length>0){const n=tt(o.children,c,e,[...t,a]);if(n)return n}}return null}function no(r,c,e){return r.map(t=>{if(t.breadcrumb)return{id:t.id,breadcrumb:t.breadcrumb};const o=tt(c,t.id,e);if(o)return{id:t.id,breadcrumb:o};const a=t.name?.[e]||t.name?.ko||t.name_localized||"";return{id:t.id,breadcrumb:a}})}function ao(r,c){const e=r.params||{},t=e.depth,o=e.category;if(t===void 0||!o){A.warn("[selectCategory] Missing depth or category param");return}const a=c.isolatedContext;if(a){const u=a.state.selectedCategories||[null,null,null,null];if(u[t]?.id===o.id){A.log(`[selectCategory] Same category already selected at depth ${t}, skipping`);return}const d=u.map((m,p)=>p<t?m:p===t?o:null);a.mergeState({selectedCategories:d}),A.log(`[selectCategory] Selected category at depth ${t} (isolated):`,o.id);return}const n=window.G7Core;if(!n?.state){A.warn("[selectCategory] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=s.ui?.selectedCategories||[null,null,null,null];if(i[t]?.id===o.id){A.log(`[selectCategory] Same category already selected at depth ${t}, skipping`);return}const l=i.map((u,d)=>d<t?u:d===t?o:null);n.state.setLocal({ui:{...s.ui,selectedCategories:l}}),A.log(`[selectCategory] Selected category at depth ${t}:`,o.id)}function so(r,c){const e=r.params||{},t=e.depth,o=e.category;if(t===void 0||!o){A.warn("[selectCategoryMobile] Missing depth or category param");return}const a=c.isolatedContext;if(a){const m=a.state.selectedCategories||[null,null,null,null],p=a.state.categoryStep||1,g=m.map((_,f)=>f<t?_:f===t?o:null),y=o.children&&o.children.length>0?t+2:p;a.mergeState({selectedCategories:g,categoryStep:y}),A.log(`[selectCategoryMobile] Selected category at depth ${t} (isolated):`,o.id,"nextStep:",y);return}const n=window.G7Core;if(!n?.state){A.warn("[selectCategoryMobile] G7Core.state API is not available");return}const s=n.state.getLocal()||{},l=(s.ui?.selectedCategories||[null,null,null,null]).map((m,p)=>p<t?m:p===t?o:null),d=o.children&&o.children.length>0?t+2:s.ui?.categoryStep;n.state.setLocal({ui:{...s.ui,selectedCategories:l,categoryStep:d}}),A.log(`[selectCategoryMobile] Selected category at depth ${t}:`,o.id,"nextStep:",d)}function io(r,c){A.log("[addCategoryToSelection] START");const e=window.G7Core;if(!e?.state){A.warn("[addCategoryToSelection] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.locale?.current?.()||"ko",a=c.isolatedContext,n=a?a.state.selectedCategories||[]:t.ui?.selectedCategories||[];A.log("[addCategoryToSelection] Current state:",{selectedCats:n.map(f=>f?.id??null),currentCategoryIds:t.form?.category_ids,currentSelectedCategoryInfos:t.selectedCategoryInfos,usingIsolatedContext:!!a});const s=n.filter(Boolean).pop();if(!s){A.warn("[addCategoryToSelection] No category selected");return}A.log("[addCategoryToSelection] Last selected category:",s.id,s.name);const i=t.form?.category_ids||[];if(i.length>=5){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.category.messages.max_5")??"You can select up to 5 categories.");return}if(i.includes(s.id)){A.log("[addCategoryToSelection] Duplicate detected:",s.id,"in",i),e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.category.messages.already_selected")??"This category is already selected.");return}const l=n.filter(Boolean).map(f=>f.name?.[o]||f.name?.ko||"").join(" > "),u=[...i,s.id],d=t.form?.primary_category_id||s.id,p=(e.state.getDataSources?.()||{}).categories?.data?.data||[];let g=t.selectedCategoryInfos||[];const w=t.form?.categories||[];g.length===0&&w.length>0&&(A.log("[addCategoryToSelection] Initializing from form.categories:",w.length,"items"),g=no(w,p,o));const y=[...g,{id:s.id,breadcrumb:l}];A.log("[addCategoryToSelection] Before setLocal:",{newCategoryIds:u,primaryCategoryId:d,newInfos:y});try{e.state.setLocal({form:{...t.form,category_ids:u,primary_category_id:d},selectedCategoryInfos:y,hasChanges:!0}),A.log("[addCategoryToSelection] setLocal SUCCESS")}catch(f){A.error("[addCategoryToSelection] setLocal FAILED:",f)}const _=e.state.getLocal()||{};A.log("[addCategoryToSelection] After setLocal:",{category_ids:_.form?.category_ids,selectedCategoryInfos:_.selectedCategoryInfos}),A.log(`[addCategoryToSelection] Added category ${s.id} with breadcrumb: ${l}. Total: ${u.length}`)}function co(r,c){const t=(r.params||{}).categoryId;if(!t){A.warn("[removeCategoryFromSelection] Missing categoryId param");return}const o=window.G7Core;if(!o?.state){A.warn("[removeCategoryFromSelection] G7Core.state API is not available");return}const a=o.state.getLocal()||{},s=(a.form?.category_ids||[]).filter(d=>d!==t);let i=a.form?.primary_category_id;i===t&&(i=s[0]||null);const u=(a.selectedCategoryInfos||[]).filter(d=>d.id!==t);o.state.setLocal({form:{...a.form,category_ids:s,primary_category_id:i},selectedCategoryInfos:u,hasChanges:!0}),A.log(`[removeCategoryFromSelection] Removed category ${t}. Remaining: ${s.length}`)}function lo(r,c){const e=r.params||{},t=e.categoryId,o=e.categories;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=(l,u,d=[])=>{for(const m of l){const p=m.name?.[n]||m.name?.ko||"";if(m.id===u)return[...d,p];if(m.children&&m.children.length>0){const g=s(m.children,u,[...d,p]);if(g)return g}}return null},i=s(o,t);return i?i.join(" > "):""}function uo(r,c){A.log("[initCategoryInfosFromProduct] START");const e=window.G7Core;if(!e?.state){A.warn("[initCategoryInfosFromProduct] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.locale?.current?.()||"ko",n=(e.state.getDataSources?.()||{}).categories?.data?.data||[],s=t.form?.categories||[],i=t.form?.category_ids||[];if(A.log("[initCategoryInfosFromProduct] Product categories:",s),A.log("[initCategoryInfosFromProduct] Category IDs:",i),A.log("[initCategoryInfosFromProduct] Categories tree available:",n.length>0),i.length===0){A.log("[initCategoryInfosFromProduct] No categories to initialize");return}const l=(d,m,p=[])=>{for(const g of d){const w=g.name?.[o]||g.name?.ko||"";if(g.id===m)return[...p,w];if(g.children&&g.children.length>0){const y=l(g.children,m,[...p,w]);if(y)return y}}return null},u=[];for(const d of i){const m=l(n,d);if(m)u.push({id:d,breadcrumb:m.join(" > ")});else{const p=s.find(g=>g.id===d);if(p){const g=p.name?.[o]||p.name?.ko||p.name_localized||"";u.push({id:d,breadcrumb:g})}}}A.log("[initCategoryInfosFromProduct] Generated selectedCategoryInfos:",u),e.state.setLocal({selectedCategoryInfos:u}),A.log("[initCategoryInfosFromProduct] DONE")}function mo(r,c){const e={category1:"",category2:"",category3:"",category4:""},t=r.params||{},o=t.categoryId1?String(t.categoryId1):"",a=t.categoryId2?String(t.categoryId2):"",n=t.categoryId3?String(t.categoryId3):"",s=t.categoryId4?String(t.categoryId4):"",i=t.categories;if(!i||!Array.isArray(i)||i.length===0)return A.warn("[validateCategoryPath] No categories data provided"),e;if(o){const l=i.find(u=>String(u.id)===o);if(l&&l.depth===0&&(e.category1=o,a&&l.children)){const u=l.children.find(d=>String(d.id)===a);if(u&&u.depth===1&&(e.category2=a,n&&u.children)){const d=u.children.find(m=>String(m.id)===n);if(d&&d.depth===2&&(e.category3=n,s&&d.children)){const m=d.children.find(p=>String(p.id)===s);m&&m.depth===3&&(e.category4=s)}}}}return A.log("[validateCategoryPath] Result:",e),e}function po(r,c){const e=r.params||{},t=e.brandId,o=e.brands;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?s.name?.[n]??s.name?.ko??"":""}function fo(r,c){const e=r.params||{},t=e.brandId,o=e.brands;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?.description?.[n]??s?.description?.ko??""}const Z=window.G7Core?.createLogger?.("Ecom:Price")??{log:(...r)=>console.log("[Ecom:Price]",...r),warn:(...r)=>console.warn("[Ecom:Price]",...r),error:(...r)=>console.error("[Ecom:Price]",...r)},go={KRW:1e3,JPY:100};function _o(r,c){const t=r.find(o=>o.code===c)?.base_unit??go[c]??1;return Math.max(1,t)}function wo(r,c,e,t){const o=r/t*c;switch(e){case"ceil":return Math.ceil(o);case"floor":return Math.floor(o);case"round":default:return Math.round(o)}}function yo(r,c){const e=r.params||{},t=e.field,o=e.currency,a=e.value,n=e.autoFill;if(!t||!o||a===void 0){Z.warn("[updatePrice] Missing required params: field, currency, or value");return}const s=window.G7Core;if(!s?.state){Z.warn("[updatePrice] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=c.datasources?.currencies?.data||{},u=l?.list||[],d=l?.default_currency||u.find(y=>y?.is_default)?.code||"",m=l?.rounding_method||"round",p={...i.form?.multi_currency_prices||{}};if(p[o]={...p[o]||{},[t]:a},n&&o===d){const y=_o(u,d);u.forEach(_=>{if(_.code!==d){const f=_.exchange_rate??1,b=wo(a,f,m,y);p[_.code]={...p[_.code]||{},[t]:b}}})}const g={list:"list_price",selling:"selling_price"},w=o===d?a:i.form?.[g[t]];s.state.setLocal({form:{...i.form,multi_currency_prices:p,[g[t]]:w},hasChanges:!0}),Z.log(`[updatePrice] Updated ${t} price for ${o}: ${a}`)}function ho(r,c){const t=(r.params||{}).options;if(!t||!Array.isArray(t)||t.length===0)return 0;const o=t.reduce((a,n)=>n.is_active===!1?a:a+(n.stock_quantity||0),0);return Z.log(`[calculateTotalOptionStock] Total stock: ${o}`),o}function Co(r,c){const e=window.G7Core;if(!e?.state){Z.warn("[validatePriceRelation] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=parseFloat(String(t.form?.list_price))||0,a=parseFloat(String(t.form?.selling_price))||0,n=a>o&&o>0,s=e.t?.("sirsoft-ecommerce.admin.product.validation.selling_price_exceeds_list_price")??"판매가는 정가보다 클 수 없습니다.",i={...t.errors||{}};n?i.selling_price=[s]:delete i.selling_price;const l=Object.keys(i).length>0;e.state.setLocal({validation:{...t.validation,price_error:n},errors:l?i:null}),n?(e.toast?.warning?.(s),Z.warn(`[validatePriceRelation] Selling price (${a}) exceeds list price (${o})`)):Z.log("[validatePriceRelation] Price validation passed")}const T=window.G7Core?.createLogger?.("Ecom:ProductOption")??{log:(...r)=>console.log("[Ecom:ProductOption]",...r),warn:(...r)=>console.warn("[Ecom:ProductOption]",...r),error:(...r)=>console.error("[Ecom:ProductOption]",...r)};function bo(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function ot(){return bo().reduce((c,e)=>({...c,[e]:""}),{})}function Ie(r){return(r??[]).filter(c=>c.is_active!==!1).reduce((c,e)=>c+(parseInt(String(e.stock_quantity),10)||0),0)}const Io={KRW:1e3,JPY:100};function $e(){const e=((window.G7Core?.state?.get?.()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).find(o=>o.is_default);if(!e)return 1;const t=e.base_unit??Io[e.code]??1;return Math.max(1,t)}function So(r,c,e){const t=parseFloat(c)||1;if(t<=0)return r;const o=r/t;let a;switch(e){case"ceil":a=Math.ceil(o);break;case"floor":a=Math.floor(o);break;default:a=Math.round(o)}return a*t}function Ne(r,c,e){const t=c.exchange_rate||0;if(t<=0)return{price:0,formatted:de(0,c)};const o=e??$e(),a=r/o*t,n=So(a,c.rounding_unit||"0.01",c.rounding_method||"round"),s=c.decimal_places??2,i=parseFloat(n.toFixed(s));return{price:i,formatted:de(i,c)}}function Te(r,c,e){return r.map(t=>{const o=parseFloat(String(t.price_adjustment))||0,a=c+o,n={...t,selling_price:a};if(e.length>0){const s=$e(),i={};e.forEach(l=>{i[l.code]=Ne(a,l,s)}),n.multi_currency_selling_price=i}return n})}function Re(){return((window.G7Core?.state?.get?.()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).filter(c=>!c.is_default&&c.exchange_rate)}function vo(r,c){const e=r.params||{},t=window.G7Core;if(!t?.state){T.warn("[setDefaultOption] G7Core.state API is not available");return}const o=t.state.getLocal()||{},a=[...o.form?.options??[]],n=e.index,s=n!=null?typeof n=="string"?parseInt(n,10):n:-1,i=e.optionCode??a[s]?.option_code;if(!i){T.warn("[setDefaultOption] Missing optionCode/index param",{index:n});return}let l=a.map(m=>({...m,is_default:m.option_code===i}));const u=l.find(m=>m.is_default===!0),d=u?parseFloat(String(u.selling_price))||parseFloat(String(o.form?.selling_price))||0:parseFloat(String(o.form?.selling_price))||0;if(u){u.price_adjustment=0;const m=Re();l=Te(l,d,m)}t.state.setLocal({form:{...o.form,options:l,selling_price:d},hasChanges:!0}),T.log(`[setDefaultOption] Set default option to ${i}, synced product selling_price to ${d}`)}function Lo(r,c){const e=r.params||{},t=typeof e.index=="string"?parseInt(e.index,10):e.index,o=e.field,a=e.value;if(t==null||!o){T.warn("[updateFormOptionField] Missing required params:",{index:t,field:o});return}const n=window.G7Core;if(!n?.state){T.warn("[updateFormOptionField] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.options??[]];if(t<0||t>=i.length){T.warn("[updateFormOptionField] Index out of bounds:",{index:t,length:i.length});return}const l=["regular_price","sale_price","list_price","selling_price","stock_quantity","safe_stock_quantity","weight","volume","mileage_value"];let u=a;if(l.includes(o)&&(u=parseFloat(a)||0),i[t]={...i[t],[o]:u},o==="selling_price"){const d=parseFloat(String(u))||0,m=Re();if(i[t].is_default===!0){i[t].price_adjustment=0;const g=Te(i,d,m);n.state.setLocal({form:{...s.form,options:g,selling_price:d},hasChanges:!0}),T.log(`[updateFormOptionField] Reverse-synced product selling_price to ${d} from default option`);return}const p=parseFloat(String(s.form?.selling_price))||0;if(i[t].price_adjustment=d-p,m.length>0){const g=$e(),w={};m.forEach(y=>{w[y.code]=Ne(d,y,g)}),i[t].multi_currency_selling_price=w,T.log(`[updateFormOptionField] Recalculated multi_currency_selling_price for option at index ${t}:`,m.map(y=>y.code))}}if((o==="stock_quantity"||o==="is_active")&&i.length>0){const d=Ie(i);n.state.setLocal({form:{...s.form,options:i,stock_quantity:d},hasChanges:!0}),T.log(`[updateFormOptionField] Updated product stock_quantity to ${d} (sum of active options)`);return}n.state.setLocal({form:{...s.form,options:i},hasChanges:!0}),T.log(`[updateFormOptionField] Updated options[${t}].${o} =`,u)}function xo(r,c){const e=window.G7Core;if(!e?.state){T.warn("[addOptionRow] G7Core.state API is not available");return}const o=r.params?.isCreate===!0||r.params?.isCreate==="true"?1:0,a=e.state.getLocal()||{},n=[...a.form?.options??[]],s=a.form?.option_groups??[],i=`OPT-${Date.now()}`,l=s.map(p=>({key:p.name,value:ot()})),u={id:null,option_code:i,option_name:ot(),option_values:l,is_default:n.length===0,regular_price:0,sale_price:0,list_price:0,selling_price:0,price_adjustment:0,multi_currency_selling_price:{},sku:"",stock_quantity:o,safe_stock_quantity:0,weight:0,volume:0,mileage_value:0,mileage_type:"percent",is_active:!0},d=[...n,u],m=Ie(d);e.state.setLocal({form:{...a.form,options:d,stock_quantity:m,has_options:d.length>0},hasChanges:!0}),T.log(`[addOptionRow] Added new option row: ${i}, product stock_quantity = ${m}`)}function Po(r,c){const e=window.G7Core;if(!e?.state){T.warn("[recalculateOptionPriceAdjustments] G7Core.state API is not available");return}const t=r.params||{},o=e.state.getLocal()||{},a=t.newSellingPrice!==void 0?parseFloat(String(t.newSellingPrice))||0:parseFloat(String(o.form?.selling_price))||0,n=[...o.form?.options??[]];if(n.length===0){T.log("[recalculateOptionPriceAdjustments] No options to recalculate");return}const s=Re(),i=Te(n,a,s);e.state.setLocal({form:{...o.form,options:i}}),T.log(`[recalculateOptionPriceAdjustments] Recalculated selling_price for ${n.length} options based on product selling_price: ${a}`)}const h=window.G7Core?.createLogger?.("Ecom:Option")??{log:(...r)=>console.log("[Ecom:Option]",...r),warn:(...r)=>console.warn("[Ecom:Option]",...r),error:(...r)=>console.error("[Ecom:Option]",...r)};function Me(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function ce(){return window.G7Core?.config?.("app.locale")??Me()[0]??"ko"}function q(){return Me().reduce((c,e)=>({...c,[e]:""}),{})}function rt(r){return{id:null,name:q(),price_adjustment:0,is_default:r===0,is_active:!0,allow_custom_text:!1,sort_order:r}}function Ao(r,c){return typeof r=="string"?r:r[c]??r.ko??Object.values(r)[0]??""}function nt(r){return r&&typeof r=="object"&&!Array.isArray(r)}function ko(r){return Array.isArray(r)?r:r&&typeof r=="object"?Object.values(r):[]}function Oo(r){return r.reduce((c,e)=>{const t=[];return c.forEach(o=>{e.forEach(a=>{t.push([...o,a])})}),t},[[]])}function Go(r){const c=Me(),e={};for(const t of c){const o=r.map(a=>Ao(a,t));e[t]=o.join("/")}return e}function Eo(r,c){return c.map((e,t)=>({key:typeof e.name=="string"?{...q(),[ce()]:e.name}:e.name,value:nt(r[t])?r[t]:{...q(),[ce()]:r[t]}}))}function Fo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addOptionInput] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.ui?.optionInputs??[];if(o.length>=3){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.input_max_3")??"You can add up to 3 option inputs.");return}e.state.setLocal({ui:{...t.ui,optionInputs:[...o,{name:q(),values:[]}]}}),h.log(`[addOptionInput] Added option input. Total: ${o.length+1}`)}function Do(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeOptionInput] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeOptionInput] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.ui?.optionInputs??[]];n.splice(t,1),o.state.setLocal({ui:{...a.ui,optionInputs:n}}),h.log(`[removeOptionInput] Removed option input at index ${t}`)}function $o(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.value;if(t===void 0||!o){h.warn("[updateOptionInput] Missing required params");return}const n=window.G7Core;if(!n?.state){h.warn("[updateOptionInput] G7Core.state API is not available");return}h.log(`[updateOptionInput] Called with index=${t}, field="${o}" (type: ${typeof o}), value=`,a);const s=n.state.getLocal()||{},i=[...s.ui?.optionInputs??[]];if(h.log(`[updateOptionInput] Current inputs[${t}]:`,i[t]),i[t]||(h.log(`[updateOptionInput] inputs[${t}] not found, initializing with empty structure`),i[t]={name:q(),values:[]}),o==="name"){const l=ce(),u=i[t].name;h.log(`[updateOptionInput] Processing 'name' field - defaultLocale: ${l}, existingName:`,u,`type: ${typeof u}`);let d;a&&typeof a=="object"&&!Array.isArray(a)?(d=a,h.log("[updateOptionInput] Using multilingual value directly:",d)):(d={...u&&typeof u=="object"&&!Array.isArray(u)?u:q(),[l]:a},h.log("[updateOptionInput] Setting single locale value:",d)),i[t]={...i[t],name:d}}else h.log(`[updateOptionInput] Processing non-name field '${o}'`),i[t]={...i[t],[o]:a};h.log(`[updateOptionInput] Final inputs[${t}]:`,i[t]),n.state.setLocal({ui:{...s.ui,optionInputs:i}})}function No(r,c){const e=window.G7Core;if(!e?.state){h.warn("[generateOptions] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.state.get()||{},a=t.ui?.optionInputs??[],n=ce(),s=f=>f.name?typeof f.name=="string"?f.name:f.name[n]??"":"",i=a.map(f=>({...f,values:ko(f.values)})),l=i.filter(f=>s(f)&&f.values&&f.values.length>0);if(h.log(`[generateOptions] Validating inputs - defaultLocale: ${n}, normalizedInputs:`,i.map(f=>({name:f.name,nameType:typeof f.name,extractedName:s(f),values:f.values,valuesIsArray:Array.isArray(f.values)}))),h.log(`[generateOptions] Valid inputs count: ${l.length}`),l.length===0){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.name_value_required")??"Please enter option name and values.");return}const u=t.form?.options??[],d=r.params?.skipConfirm===!0;if(u.length>0&&!d){e.modal?.open?.("modal_confirm_regenerate");return}const p=r.params?.isCreate===!0||r.params?.isCreate==="true"?1:0,g=parseFloat(String(t.form?.list_price))||0,w=parseFloat(String(t.form?.selling_price))||0,y=t.form?.multi_currency_selling_price,_=(o.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).filter(f=>!f.is_default&&f.exchange_rate);try{h.log("[generateOptions] Step 1: Creating optionGroups...");const f=l.map((L,O)=>(h.log(`[generateOptions] Processing input[${O}]:`,{name:L.name,values:L.values,valuesType:typeof L.values,valuesIsArray:Array.isArray(L.values)}),{name:typeof L.name=="string"?{...q(),[n]:L.name}:L.name,values:L.values.map($=>nt($)?$:{...q(),[n]:$})})),C=Oo(l.map(L=>L.values)).map((L,O)=>{const $=Eo(L,l),Q=Go(L),It=`OPT-${String(O+1).padStart(3,"0")}`;let J={};return y&&typeof y=="object"&&Object.keys(y).length>0?J=JSON.parse(JSON.stringify(y)):w>0&&_.length>0&&_.forEach(Pe=>{J[Pe.code]=Ne(w,Pe)}),{id:null,option_code:It,option_values:$,option_name:Q,list_price:g,selling_price:w,price_adjustment:0,stock_quantity:p,safe_stock_quantity:0,is_default:O===0,is_active:!0,sku:"",multi_currency_selling_price:J}}),x=t.form?.options??[],P=L=>L.option_name?typeof L.option_name=="string"?L.option_name:L.option_name[n]??L.option_name.ko??"":"",S=C.map(L=>{const O=P(L),$=x.find(Q=>P(Q)===O);return $?{...L,...$,id:$.id}:L}),N=Ie(S);e.state.setLocal({form:{...t.form,option_groups:f,options:S,has_options:S.length>0,stock_quantity:N},hasChanges:!0}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.generated",{count:S.length})??`${S.length} options have been generated.`),h.log(`[generateOptions] Generated ${S.length} options`)}catch(f){throw h.error("[generateOptions] Error occurred:",f),h.error("[generateOptions] Error message:",f.message),h.error("[generateOptions] Error stack:",f.stack),f}}function To(r,c){const t=(r.params||{}).rowIndex;if(t===void 0){h.warn("[deleteOption] Missing rowIndex param");return}const o=window.G7Core;if(!o?.state){h.warn("[deleteOption] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.options??[]];n.splice(t,1);const s=Ie(n),i=n.length>0;o.state.setLocal({form:{...a.form,options:n,has_options:i,...i?{stock_quantity:s}:{}},hasChanges:!0}),h.log(`[deleteOption] Deleted option at index ${t}, product stock_quantity = ${s}`)}function Ro(r,c){const e=r.params||{},t=e.optionName,o=e.applyToAll,a=e.inheritPrice,n=window.G7Core;if(!n?.state){h.warn("[applyOptionAddTool] G7Core.state API is not available");return}if(!t?.trim()){n.toast?.warning?.(n.t?.("sirsoft-ecommerce.admin.product.options.messages.name_required")??"Please enter option name.");return}const s=n.state.getLocal()||{},l=[...s.form?.options??[]].map(u=>{const d={...u};return o&&(d.option_values={...u.option_values,[t]:""}),a&&(d.price_adjustment=u.price_adjustment??0),d});n.state.setLocal({form:{...s.form,options:l},ui:{...s.ui,optionAddToolName:"",optionAddToolApplyToAll:!1,optionAddToolInheritPrice:!1},hasChanges:!0}),n.toast?.success?.(n.t?.("sirsoft-ecommerce.admin.product.options.messages.add_tool_applied")??"Option add tool has been applied.")}function Mo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addRequiredItem] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.required_items??[],a={id:null,sort_order:o.length,option_name:{ko:"",en:""},description:{ko:"",en:""}};e.state.setLocal({form:{...t.form,required_items:[...o,a]},hasChanges:!0}),h.log(`[addRequiredItem] Added required item. Total: ${o.length+1}`)}function Ho(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.locale,n=e.value;if(t===void 0||!o||!a){h.warn("[updateRequiredItem] Missing required params");return}const s=window.G7Core;if(!s?.state){h.warn("[updateRequiredItem] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=[...i.form?.required_items??[]];l[t]={...l[t],[o]:{...l[t][o],[a]:n}},s.state.setLocal({form:{...i.form,required_items:l},hasChanges:!0})}function qo(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeRequiredItem] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeRequiredItem] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.required_items??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,required_items:n},hasChanges:!0}),h.log(`[removeRequiredItem] Removed required item at index ${t}`)}function Uo(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){h.warn("[reorderRequiredItems] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){h.warn("[reorderRequiredItems] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.required_items??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,required_items:s},hasChanges:!0}),h.log(`[reorderRequiredItems] Moved item from ${t} to ${o}`)}function Vo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addAdditionalOption] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.additional_options??[];if(o.length>=5){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_max_5")??"You can add up to 5 additional options.");return}const a=`temp_${Date.now()}_${Math.random().toString(36).substring(2,9)}`;e.state.setLocal({form:{...t.form,additional_options:[...o,{id:a,name:q(),is_required:!1,sort_order:o.length,values:[rt(0)]}]},hasChanges:!0}),h.log(`[addAdditionalOption] Added additional option. Total: ${o.length+1}`)}function jo(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.value;if(t===void 0||!o){h.warn("[updateAdditionalOption] Missing required params");return}const n=window.G7Core;if(!n?.state){h.warn("[updateAdditionalOption] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.additional_options??[]];if(!i[t]){h.warn("[updateAdditionalOption] Option not found at index",t);return}if(o==="name")if(a&&typeof a=="object"&&!Array.isArray(a))i[t]={...i[t],name:a};else{const l=ce(),u=i[t].name,d=u&&typeof u=="object"&&!Array.isArray(u)?u:q();i[t]={...i[t],name:{...d,[l]:a}}}else i[t]={...i[t],[o]:a};n.state.setLocal({form:{...s.form,additional_options:i},hasChanges:!0}),h.log(`[updateAdditionalOption] Updated options[${t}].${o}`)}function Bo(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeAdditionalOption] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeAdditionalOption] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.additional_options??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,additional_options:n},hasChanges:!0}),h.log(`[removeAdditionalOption] Removed additional option at index ${t}`)}function Ko(r,c){const e=r.params||{},t=parseInt(String(e.oldIndex),10),o=parseInt(String(e.newIndex),10);if(isNaN(t)||isNaN(o)||t===o)return;const a=window.G7Core;if(!a?.state){h.warn("[reorderAdditionalOptions] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.additional_options??[]];if(t<0||t>=s.length||o<0||o>=s.length)return;const[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,additional_options:s},ui:{...n.ui,dragIndex:null},hasChanges:!0}),h.log(`[reorderAdditionalOptions] Moved item from ${t} to ${o}`)}function Qo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[clearAdditionalOptions] G7Core.state API is not available");return}const t=e.state.getLocal()||{};e.state.setLocal({form:{...t.form,additional_options:[]},hasChanges:!0}),h.log("[clearAdditionalOptions] Cleared all additional options.")}function He(r){if(!r)return[];const c=r.values;return Array.isArray(c)?c:c&&typeof c=="object"?Object.values(c):[]}function zo(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex;if(t==null||isNaN(t)){h.warn("[addAdditionalOptionValue] Missing groupIndex param");return}const o=window.G7Core;if(!o?.state){h.warn("[addAdditionalOptionValue] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.additional_options??[]];if(!n[t]){h.warn("[addAdditionalOptionValue] Group not found at index",t);return}const s=[...He(n[t])];if(s.length>=20){o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_value_max_20")??"You can add up to 20 choices per group.");return}s.push(rt(s.length)),n[t]={...n[t],values:s},o.state.setLocal({form:{...a.form,additional_options:n},hasChanges:!0}),h.log(`[addAdditionalOptionValue] Added value to group ${t}. Total: ${s.length}`)}function Yo(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex,o=typeof e.valueIndex=="string"?parseInt(e.valueIndex,10):e.valueIndex,a=e.field,n=e.value;if(t===void 0||o===void 0||!a){h.warn("[updateAdditionalOptionValue] Missing required params");return}const s=window.G7Core;if(!s?.state){h.warn("[updateAdditionalOptionValue] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=[...i.form?.additional_options??[]];if(!l[t]){h.warn("[updateAdditionalOptionValue] Group not found at index",t);return}const u=[...He(l[t])];if(!u[o]){h.warn("[updateAdditionalOptionValue] Value not found at index",o);return}if(a==="name")if(n&&typeof n=="object"&&!Array.isArray(n))u[o]={...u[o],name:n};else{const d=ce(),m=u[o].name,p=m&&typeof m=="object"&&!Array.isArray(m)?m:q();u[o]={...u[o],name:{...p,[d]:n}}}else if(a==="price_adjustment"){let d=parseFloat(String(n));(isNaN(d)||d<0)&&(d=0),u[o]={...u[o],price_adjustment:d}}else if(a==="is_default")n===!0||n==="true"?u.forEach((m,p)=>{m.is_default=p===o}):u[o].is_default=!1;else if(a==="is_active"||a==="allow_custom_text"){const d=n===!0||n==="true";u[o]={...u[o],[a]:d}}else u[o]={...u[o],[a]:n};l[t]={...l[t],values:u},s.state.setLocal({form:{...i.form,additional_options:l},hasChanges:!0}),h.log(`[updateAdditionalOptionValue] Updated group ${t} value ${o} field ${a}`)}function Wo(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex,o=typeof e.valueIndex=="string"?parseInt(e.valueIndex,10):e.valueIndex;if(t===void 0||o===void 0){h.warn("[removeAdditionalOptionValue] Missing required params");return}const a=window.G7Core;if(!a?.state){h.warn("[removeAdditionalOptionValue] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.additional_options??[]];if(!s[t]){h.warn("[removeAdditionalOptionValue] Group not found at index",t);return}const i=[...He(s[t])];if(i.length<=1){a.toast?.warning?.(a.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_value_min_1")??"At least one choice is required per group.");return}i.splice(o,1),i.forEach((l,u)=>{l.sort_order=u}),i.some(l=>l.is_default)||(i[0].is_default=!0),s[t]={...s[t],values:i},a.state.setLocal({form:{...n.form,additional_options:s},hasChanges:!0}),h.log(`[removeAdditionalOptionValue] Removed value ${o} from group ${t}`)}const j=window.G7Core?.createLogger?.("Ecom:Image")??{log:(...r)=>console.log("[Ecom:Image]",...r),warn:(...r)=>console.warn("[Ecom:Image]",...r),error:(...r)=>console.error("[Ecom:Image]",...r)},Jo=["image/jpeg","image/png","image/gif","image/webp"],Xo=10*1024*1024,Zo=20;async function er(r,c){const t=(r.params||{}).files;if(!t||t.length===0){j.warn("[uploadImages] No files provided");return}const o=window.G7Core;if(!o?.state||!o?.api){j.warn("[uploadImages] G7Core.state or G7Core.api is not available");return}const n=(o.state.getLocal()||{}).form?.images??[],s=Zo-n.length;t.length>s&&o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.max_count",{max:s})??`You can add up to ${s} images.`);const i=Array.from(t).slice(0,s);for(const l of i){if(!Jo.includes(l.type)){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.unsupported_format",{name:l.name})??`${l.name}: Unsupported format.`);continue}if(l.size>Xo){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.size_exceeded",{name:l.name})??`${l.name}: Exceeds 10MB.`);continue}const u=o.state.getLocal()||{};o.state.setLocal({ui:{...u.ui,uploadProgress:{...u.ui?.uploadProgress||{},[l.name]:0}}});try{const d=new FormData;d.append("file",l),d.append("collection","products");const m=await o.api.upload("/api/admin/media/upload",d,{onProgress:b=>{const C=o.state.getLocal()||{};o.state.setLocal({ui:{...C.ui,uploadProgress:{...C.ui?.uploadProgress||{},[l.name]:b}}})}}),p=o.state.getLocal()||{},g=p.form?.images??[],w={...m.data,is_thumbnail:g.length===0,sort_order:g.length};o.state.setLocal({form:{...p.form,images:[...g,w]},hasChanges:!0});const y=o.state.getLocal()||{},{[l.name]:_,...f}=y.ui?.uploadProgress||{};o.state.setLocal({ui:{...y.ui,uploadProgress:f}}),j.log(`[uploadImages] Uploaded ${l.name}`)}catch(d){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.upload_failed",{name:l.name})??`${l.name}: Upload failed`);const m=o.state.getLocal()||{},{[l.name]:p,...g}=m.ui?.uploadProgress||{};o.state.setLocal({ui:{...m.ui,uploadProgress:g}}),j.error(`[uploadImages] Failed to upload ${l.name}:`,d)}}}function tr(r,c){const t=(r.params||{}).index;if(t===void 0){j.warn("[setThumbnail] Missing index param");return}const o=window.G7Core;if(!o?.state){j.warn("[setThumbnail] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=(a.form?.images??[]).map((s,i)=>({...s,is_thumbnail:i===t}));o.state.setLocal({form:{...a.form,images:n},hasChanges:!0}),j.log(`[setThumbnail] Set thumbnail to image at index ${t}`)}function or(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){j.warn("[reorderImages] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){j.warn("[reorderImages] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.images??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,images:s},hasChanges:!0}),j.log(`[reorderImages] Moved image from ${t} to ${o}`)}const at=window.G7Core?.createLogger?.("Ecom:Description")??{log:(...r)=>console.log("[Ecom:Description]",...r),warn:(...r)=>console.warn("[Ecom:Description]",...r),error:(...r)=>console.error("[Ecom:Description]",...r)};function rr(r,c){const e=r.params||{},t=e.locale,o=e.value;if(!t){at.warn("[updateDescription] Missing locale param");return}const a=window.G7Core;if(!a?.state){at.warn("[updateDescription] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,description:{...n.form?.description,[t]:o}},hasChanges:!0})}const k=window.G7Core?.createLogger?.("Ecom:Notice")??{log:(...r)=>console.log("[Ecom:Notice]",...r),warn:(...r)=>console.warn("[Ecom:Notice]",...r),error:(...r)=>console.error("[Ecom:Notice]",...r)};function pe(r=""){const e=window.G7Core?.locale?.supported?.()??["ko","en"],t={};for(const o of e)t[o]=r;return t}function nr(r,c){const t=(r.params||{}).templateId,o=window.G7Core;if(!o?.state){k.warn("[confirmSelectNoticeTemplate] G7Core.state API is not available");return}const n=(o.state.getLocal()||{}).form?.notice_items??[],s=n.length>0&&n.some(i=>Object.values(i.content||{}).some(u=>u&&u.trim()!==""));if(!t||!s){st(r);return}o.state.setGlobal({pendingNoticeTemplateId:t}),o.modal?.open?.("notice_template_confirm_modal"),k.log(`[confirmSelectNoticeTemplate] Showing confirm modal for template: ${t}`)}function st(r,c){const t=(r.params||{}).templateId,o=window.G7Core;if(!o?.state){k.warn("[selectNoticeTemplate] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=o.dataSource?.get?.("notice_templates"),s=n?.data?.data??n?.data??[];if(!t){o.state.setLocal({form:{...a.form,notice_items:[]},ui:{...a.ui,selectedNoticeTemplateId:null},hasChanges:!0});return}const i=s.find(d=>d.id===t);if(!i){k.warn(`[selectNoticeTemplate] Template not found: ${t}`);return}const l=a.form?.notice_items??[],u=(i.fields??[]).map((d,m)=>{const p=JSON.stringify(d.name),g=l.find(w=>JSON.stringify(w.name)===p);return{key:`field_${m}`,name:d.name,content:g?.content??d.content??pe(),sort_order:m,is_custom:!1}});o.state.setLocal({form:{...a.form,notice_items:u},ui:{...a.ui,selectedNoticeTemplateId:t},hasChanges:!0}),k.log(`[selectNoticeTemplate] Selected template: ${t}`)}function ar(r,c){const e=r.params||{},t=e.index,o=e.locale,a=e.value;if(t===void 0||!o){k.warn("[updateNoticeItem] Missing required params");return}const n=window.G7Core;if(!n?.state){k.warn("[updateNoticeItem] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.notice_items??[]];i[t]={...i[t],content:{...i[t].content,[o]:a}},n.state.setLocal({form:{...s.form,notice_items:i},hasChanges:!0})}function sr(r,c){const t=(r.params||{}).index;if(t===void 0){k.warn("[removeNoticeItem] Missing index param");return}const o=window.G7Core;if(!o?.state){k.warn("[removeNoticeItem] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.notice_items??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,notice_items:n},hasChanges:!0}),k.log(`[removeNoticeItem] Removed notice item at index ${t}`)}function ir(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){k.warn("[reorderNoticeItems] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){k.warn("[reorderNoticeItems] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.notice_items??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,notice_items:s},hasChanges:!0}),k.log(`[reorderNoticeItems] Moved item from ${t} to ${o}`)}function cr(r,c){const t=(r.params||{}).value,o=window.G7Core;if(!o?.state){k.warn("[fillNoticeWithValue] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=a.ui?.noticeLocale??"ko",s=(a.form?.notice_items??[]).map(i=>({...i,content:{...i.content,[n]:i.content?.[n]||t}}));o.state.setLocal({form:{...a.form,notice_items:s},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.notice.messages.filled")??"Empty fields have been filled.")}function lr(r,c){const t=(r.params||{}).mode;if(!t){k.warn("[switchNoticeMode] Missing mode param");return}const o=window.G7Core;if(!o?.state){k.warn("[switchNoticeMode] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,noticeMode:t}}),k.log(`[switchNoticeMode] Switched to ${t} mode`)}function dr(r,c){const e=r.params||{},t=e.locale,o=e.value;if(!t){k.warn("[updateNewTemplateName] Missing locale param");return}const a=window.G7Core;if(!a?.state){k.warn("[updateNewTemplateName] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,new_template_name:{...n.form?.new_template_name,[t]:o}}})}function ur(r,c){const e=window.G7Core;if(!e?.state){k.warn("[addNoticeItem] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.notice_items??[],a={key:`custom_${Date.now()}`,name:pe(),content:pe(),sort_order:o.length,is_custom:!0};e.state.setLocal({form:{...t.form,notice_items:[...o,a]},hasChanges:!0}),k.log(`[addNoticeItem] Added new notice item. Total: ${o.length+1}`)}function mr(r,c){const e=r.params||{},t=e.index,o=e.locale,a=e.value;if(t===void 0||!o){k.warn("[updateNoticeItemName] Missing required params");return}const n=window.G7Core;if(!n?.state){k.warn("[updateNoticeItemName] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.notice_items??[]];i[t]={...i[t],name:{...i[t].name,[o]:a}},n.state.setLocal({form:{...s.form,notice_items:i},hasChanges:!0})}function pr(r,c){const e=window.G7Core;if(!e?.state){k.warn("[saveAsNoticeTemplate] G7Core.state API is not available");return}if(((e.state.getLocal()||{}).form?.notice_items??[]).length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.no_items")??"No items to save.");return}e.state.set?.({"ui.saveTemplateData":{name:pe(),category_id:null,is_default:!1}}),e.dispatch?.({handler:"openModal",target:"modal_save_template"})}const Se={ko:"상세설명참조",en:"See detail description"};function fr(r,c){const e=window.G7Core;if(!e?.state){k.warn("[fillTemplateFieldsWithDetailReference] G7Core.state API is not available");return}const a=((r.params||{}).form||{}).fields??[];if(a.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product_notice_template.messages.no_fields_to_change")??"변경할 항목이 없습니다.");return}const n=e.locale?.supported?.()??["ko","en"],s={};for(const l of n)s[l]=Se[l]??Se.en??"See detail description";const i=a.map(l=>({...l,content:{...s}}));e.state.setLocal({"form.fields":i}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product_notice_template.messages.all_fields_changed_to_detail_ref")??"모든 항목이 '상세설명참조'로 변경되었습니다."),k.log("[fillTemplateFieldsWithDetailReference] All fields updated to detail reference")}function gr(r,c){const e=window.G7Core;if(!e?.state){k.warn("[fillNoticeItemsWithDetailReference] G7Core.state API is not available");return}const a=((r.params||{}).form||{}).notice_items??[];if(a.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.no_items_to_change")??"변경할 항목이 없습니다.");return}const n=e.locale?.supported?.()??["ko","en"],s={};for(const l of n)s[l]=Se[l]??Se.en??"See detail description";const i=a.map(l=>({...l,content:{...s}}));e.state.setLocal({"form.notice_items":i}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.all_items_changed_to_detail_ref")??"모든 항목이 '상세설명참조'로 변경되었습니다."),k.log("[fillNoticeItemsWithDetailReference] All notice items updated to detail reference")}async function _r(r,c){const e=window.G7Core;if(!e?.state||!e?.api){k.warn("[confirmSaveNoticeTemplate] G7Core.state or G7Core.api is not available");return}const t=e.state.getLocal()||{},o=e.state.get?.()?.ui?.saveTemplateData??{},a=o.name;if(!a?.ko?.trim()){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_name_required")??"Please enter template name.");return}try{const n=t.form?.notice_items??[],s=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/notice-templates",{name:a,is_active:o.is_default??!1,fields:n.map(i=>({name:i.name,content:i.content}))});e.state.set?.({"ui.saveTemplateData":{name:pe(),category_id:null,is_default:!1}}),e.state.setLocal({ui:{...t.ui,noticeMode:"existing"},hasChanges:!0}),e.dispatch?.({handler:"closeModal"}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_saved")??"Template has been saved."),e.datasources?.refresh?.("notice_templates"),k.log(`[confirmSaveNoticeTemplate] Template saved: ${s.data.id}`)}catch(n){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_save_error")??"Failed to save template."),k.error("[confirmSaveNoticeTemplate] Failed to save template:",n)}}const v=window.G7Core?.createLogger?.("Ecom:Misc")??{log:(...r)=>console.log("[Ecom:Misc]",...r),warn:(...r)=>console.warn("[Ecom:Misc]",...r),error:(...r)=>console.error("[Ecom:Misc]",...r)};function wr(r,c){const t=(r.params||{}).labelId;if(!t){v.warn("[toggleLabel] Missing labelId param");return}const o=window.G7Core;if(!o?.state){v.warn("[toggleLabel] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=a.form?.labels??[],s=n.includes(t)?n.filter(i=>i!==t):[...n,t];o.state.setLocal({form:{...a.form,labels:s},hasChanges:!0}),v.log(`[toggleLabel] Toggled label ${t}. Current labels: ${s.length}`)}async function yr(r,c){const e=window.G7Core;if(!e?.state||!e?.api){v.warn("[generateProductCode] G7Core.state or G7Core.api is not available");return}try{const t=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/generate-code"),o=e.state.getLocal()||{};e.state.setLocal({form:{...o.form,product_code:t.data.product_code}}),v.log(`[generateProductCode] Generated: ${t.data.product_code}`)}catch(t){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.code_generate_error")??"Failed to generate product code."),v.error("[generateProductCode] Failed:",t)}}function hr(r,c){const e=r.params||{},t=e.policyId,o=e.field,a=e.policies;if(!t||!o||!a)return"-";const n=a.find(s=>s.id===t);return n?n[o]??"-":"-"}function Cr(r,c){const e=r.params||{},t=e.templateId,o=e.templates;if(!t||!o)return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?.content?.[n]??s?.content?.ko??""}function br(r,c){const e=r.params||{},t=e.platform,o=e.field,a=e.value;if(!t||!o){v.warn("[updateShoppingIntegration] Missing required params");return}const n=window.G7Core;if(!n?.state){v.warn("[updateShoppingIntegration] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=s.form?.shopping_integrations??{naver:{enabled:!1,sync_interval:"manual"},daum:{enabled:!1,sync_interval:"manual"},facebook:{enabled:!1,sync_interval:"manual"},google:{enabled:!1,sync_interval:"manual"}};n.state.setLocal({form:{...s.form,shopping_integrations:{...i,[t]:{...i[t],[o]:a}}},hasChanges:!0}),v.log(`[updateShoppingIntegration] Updated ${t}.${o} to ${a}`)}function Ir(r,c){const t=(r.params||{}).type;if(!t){v.warn("[updateShippingType] Missing type param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateShippingType] G7Core.state API is not available");return}const a=o.state.getLocal()||{};let n=0,s=0;t==="fixed_paid"?n=3e3:t==="threshold_free"&&(s=3e4),o.state.setLocal({form:{...a.form,shipping_type:t,shipping_fee:t==="fixed_free"?0:a.form?.shipping_fee??n,free_shipping_threshold:t==="threshold_free"?a.form?.free_shipping_threshold??s:0},hasChanges:!0}),v.log(`[updateShippingType] Updated shipping type to ${t}`)}function Sr(r,c){const e=r.params||{},t=e.field,o=e.value;if(!t){v.warn("[updateIdentificationCode] Missing field param");return}const a=window.G7Core;if(!a?.state){v.warn("[updateIdentificationCode] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,[t]:o},hasChanges:!0})}function vr(r,c){const e=r.params||{},t=e.labelId,o=e.labelName;if(!t){v.warn("[openLabelPeriodModal] Missing labelId param");return}const a=window.G7Core;if(!a?.state){v.warn("[openLabelPeriodModal] G7Core.state API is not available");return}const n=a.state.getLocal()||{},i=(n.form?.labels??[]).find(l=>l.id===t);a.state.setLocal({ui:{...n.ui,showLabelPeriodModal:!0,labelPeriodModalData:{labelId:t,labelName:o,startDate:i?.start_date??null,endDate:i?.end_date??null}}})}function Lr(r,c){const e=r.params||{},t=e.labelId,o=e.startDate,a=e.endDate;if(!t){v.warn("[saveLabelPeriod] Missing labelId param");return}const n=window.G7Core;if(!n?.state){v.warn("[saveLabelPeriod] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.labels??[]],l=i.findIndex(u=>u.id===t);l>=0?i[l]={...i[l],start_date:o,end_date:a}:i.push({id:t,start_date:o,end_date:a}),n.state.setLocal({form:{...s.form,labels:i},ui:{...s.ui,showLabelPeriodModal:!1,labelPeriodModalData:null},hasChanges:!0}),n.toast?.success?.(n.t?.("sirsoft-ecommerce.admin.product.messages.label_period_saved")??"Label period has been saved."),v.log(`[saveLabelPeriod] Saved period for label ${t}`)}function xr(r,c){const t=(r.params||{}).labelId;if(!t){v.warn("[removeLabelPeriod] Missing labelId param");return}const o=window.G7Core;if(!o?.state){v.warn("[removeLabelPeriod] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.labels??[]],s=n.findIndex(i=>i.id===t);s>=0&&(n[s]={...n[s],start_date:null,end_date:null}),o.state.setLocal({form:{...a.form,labels:n},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.messages.label_period_removed")??"Label period has been removed."),v.log(`[removeLabelPeriod] Removed period for label ${t}`)}function Pr(r,c){const t=(r.params||{}).sort;if(!t){v.warn("[updateActivityLogSort] Missing sort param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateActivityLogSort] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,activityLogSort:t}}),o.datasources?.refresh?.("activity_logs",{params:{sort:t==="latest"?"desc":"asc",per_page:a.ui?.activityLogPerPage??10}}),v.log(`[updateActivityLogSort] Updated sort to ${t}`)}function Ar(r,c){const t=(r.params||{}).perPage;if(!t){v.warn("[updateActivityLogPerPage] Missing perPage param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateActivityLogPerPage] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,activityLogPerPage:t}}),o.datasources?.refresh?.("activity_logs",{params:{sort:a.ui?.activityLogSort==="oldest"?"asc":"desc",per_page:t}}),v.log(`[updateActivityLogPerPage] Updated perPage to ${t}`)}function kr(r,c){const e=window.G7Core;if(!e?.state){v.warn("[setDefaultShippingPolicy] G7Core.state API is not available");return}const t=e.state.getLocal()||{};if(t.form?.shipping_policy_id){v.log("[setDefaultShippingPolicy] Shipping policy already set, skipping");return}if(t.ui?.useDefaultShippingPolicy===!1){v.log("[setDefaultShippingPolicy] useDefaultShippingPolicy is OFF, skipping");return}const o=e.dataSource?.get?.("shipping_policies"),a=o?.data?.data??o?.data??[];if(a.length===0){v.log("[setDefaultShippingPolicy] No shipping policies found");return}const n=a.find(s=>s.is_default===!0);n?(e.state.setLocal({form:{...t.form,shipping_policy_id:n.id}}),v.log(`[setDefaultShippingPolicy] Set default shipping policy: ${n.id} (${n.name})`)):v.log("[setDefaultShippingPolicy] No default shipping policy found")}function Or(r,c){const e=window.G7Core;if(!e?.state){v.warn("[toggleDefaultShippingPolicy] G7Core.state API is not available");return}const t=r.params?.checked??!1,o=e.state.getLocal()||{},a={...o.ui,useDefaultShippingPolicy:t};if(t){const n=e.dataSource?.get?.("shipping_policies"),i=(n?.data?.data??n?.data??[]).find(l=>l.is_default===!0);e.state.setLocal({ui:a,form:{...o.form,shipping_policy_id:i?.id??null}}),i?v.log(`[toggleDefaultShippingPolicy] ON - Selected default policy: ${i.id}`):v.log("[toggleDefaultShippingPolicy] ON - No default policy found")}else e.state.setLocal({ui:a}),v.log("[toggleDefaultShippingPolicy] OFF - Keeping current shipping policy")}function Gr(r,c){const e=window.G7Core;if(!e?.state){v.warn("[setLabelDatePreset] G7Core.state API is not available");return}const t=r.params?.preset;if(!t){v.warn("[setLabelDatePreset] Missing preset param");return}const o=new Date,a=i=>{const l=i.getFullYear(),u=String(i.getMonth()+1).padStart(2,"0"),d=String(i.getDate()).padStart(2,"0");return`${l}-${u}-${d}`},n=a(o);let s=null;switch(t){case"7d":{const i=new Date(o);i.setDate(i.getDate()+7),s=a(i);break}case"14d":{const i=new Date(o);i.setDate(i.getDate()+14),s=a(i);break}case"30d":{const i=new Date(o);i.setDate(i.getDate()+30),s=a(i);break}case"permanent":s=null;break;default:v.warn(`[setLabelDatePreset] Unknown preset: ${t}`);return}e.state.setGlobal({"labelFormData.start_date":n,"labelFormData.end_date":s}),v.log(`[setLabelDatePreset] Applied preset '${t}': ${n} ~ ${s??"permanent"}`)}const Er=(r,c)=>{const e=window.G7Core,o=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],a=Number(r.params?.labelId);if(!a)return;const n=[...o];if(n.find(l=>l.label_id===a)){e?.state?.setLocal?.({"ui.lastClickedLabelId":a});return}const i=[...n,{label_id:a,started_at:null,ended_at:null}];e?.state?.setLocal?.({"form.label_assignments":i,"ui.lastClickedLabelId":a,hasChanges:!0})},Fr=(r,c)=>{const e=window.G7Core,o=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],a=Number(r.params?.labelId),n=r.params?.field,s=r.params?.value;if(!a||!n)return;const i=[...o],l=i.findIndex(u=>u.label_id===a);l<0||(i[l]={...i[l],[n]:s||null},e?.state?.setLocal?.({"form.label_assignments":i,hasChanges:!0}))},Dr=(r,c)=>{const e=window.G7Core,t=e?.state?.getLocal?.()||{},o=t.form?.label_assignments??[],a=t.ui?.lastClickedLabelId,n=r.params?.preset;if(!a||!n)return;const s=[...o],i=s.findIndex(p=>p.label_id===a);if(i<0)return;const l=new Date,u=p=>p.toISOString().split("T")[0],d=u(l);let m=null;switch(n){case"7d":m=u(new Date(l.getTime()+7*24*60*60*1e3));break;case"14d":m=u(new Date(l.getTime()+14*24*60*60*1e3));break;case"30d":m=u(new Date(l.getTime()+30*24*60*60*1e3));break;case"permanent":m=null;break}s[i]={...s[i],started_at:d,ended_at:m},e?.state?.setLocal?.({"form.label_assignments":s,hasChanges:!0})},$r=(r,c)=>{const e=window.G7Core,o=(e?.state?.get()||{}).labelToUncheckId;if(!o)return;const i=[...(e?.state?.getParent?.()?._local||{}).form?.label_assignments??[]].filter(l=>l.label_id!==o);e?.state?.setParentLocal?.({"form.label_assignments":i,hasChanges:!0}),e?.state?.setGlobal?.({labelToUncheckId:null}),e?.modal?.close?.()},Nr=async(r,c)=>{const e=window.G7Core,t=e?.state?.get()||{},a=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],n=t.labelToDeleteId;if(n){e?.state?.setGlobal?.({isDeletingLabel:!0});try{const s=await e.api.delete(`/api/modules/sirsoft-ecommerce/admin/product-labels/${n}`);if(!s?.success)throw new Error(s?.message||"Failed to delete label");const l=[...a].filter(u=>u.label_id!==n);e?.state?.setLocal?.({"form.label_assignments":l,"ui.lastClickedLabelId":null}),await e?.dataSource?.refetch?.("product_labels"),e?.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.labels.delete_success")??"Label deleted."),e?.modal?.close?.()}catch(s){e?.toast?.error?.(s?.response?.data?.message||s?.message||e.t?.("sirsoft-ecommerce.admin.product.labels.delete_error")||"Failed to delete label.")}finally{e?.state?.setGlobal?.({isDeletingLabel:!1,labelToDeleteId:null})}}},Tr=async(r,c)=>{const e=window.G7Core,t=e?.state?.get()||{},a=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],n=t.editingLabelId,s=t.labelFormData;if(s?.name?.ko){e?.state?.setGlobal?.({isSavingLabel:!0});try{if(n){const i=await e.api.put(`/api/modules/sirsoft-ecommerce/admin/product-labels/${n}`,{name:s.name,color:s.color});if(!i?.success)throw new Error(i?.message||"Failed to update label");const u=[...a].map(d=>d.label_id===n?{...d,started_at:s.started_at||null,ended_at:s.ended_at||null}:d);e?.state?.setLocal?.({"form.label_assignments":u,hasChanges:!0})}else{const i=await e.api.post("/api/modules/sirsoft-ecommerce/admin/product-labels",{name:s.name,color:s.color||"#6B7280"});if(!i?.success)throw new Error(i?.message||"Failed to create label");const l=Number(i?.data?.id);if(l){const d=[...[...a],{label_id:l,started_at:null,ended_at:null}];e?.state?.setLocal?.({"form.label_assignments":d,"ui.lastClickedLabelId":l,hasChanges:!0})}}await e?.dataSource?.refetch?.("product_labels"),e?.toast?.success?.(n?e.t?.("sirsoft-ecommerce.admin.product.labels.save_success")??"Label saved.":e.t?.("sirsoft-ecommerce.admin.product.labels.create_success")??"Label created."),e?.modal?.close?.()}catch(i){e?.toast?.error?.(i?.response?.data?.message||i?.message||e.t?.("sirsoft-ecommerce.admin.product.labels.save_error")||"Failed to save label.")}finally{e?.state?.setGlobal?.({isSavingLabel:!1})}}},ee=window.G7Core?.createLogger?.("Ecom:DescriptionLocale")??{log:(...r)=>console.log("[Ecom:DescriptionLocale]",...r),warn:(...r)=>console.warn("[Ecom:DescriptionLocale]",...r),error:(...r)=>console.error("[Ecom:DescriptionLocale]",...r)};function it(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function Rr(r,c){const t=(r.params||{}).locale;if(!t){ee.warn("[removeDescriptionLocale] Missing locale param");return}const o=window.G7Core;if(!o?.state){ee.warn("[removeDescriptionLocale] G7Core.state API is not available");return}const a=o?.state?.get?.("_global.settings.general.language")??o?.config?.("app.fallback_locale")??"ko";if(t===a){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.default_locale_cannot_delete")??"Default language cannot be deleted.");return}const n=o.state.getLocal()||{},s=it(),i={...n.form?.description};delete i[t];const l={...n.form?.description_mobile};delete l[t];const u=(n.ui?.descriptionActiveLocales??s).filter(d=>d!==t);o.state.setLocal({form:{...n.form,description:i,description_mobile:l},ui:{...n.ui,descriptionActiveLocales:u,descriptionLocale:u[0]??"ko"},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_removed",{locale:t.toUpperCase()})??`${t.toUpperCase()} language has been removed.`),ee.log(`[removeDescriptionLocale] Removed locale: ${t}`)}function Mr(r,c){const e=window.G7Core;if(!e?.state){ee.warn("[showAddLocaleModal] G7Core.state API is not available");return}const t=e.state.getLocal()||{};e.state.setLocal({ui:{...t.ui,showAddLocaleModal:!0}})}function Hr(r,c){const t=(r.params||{}).locale;if(!t){ee.warn("[addDescriptionLocale] Missing locale param");return}const o=window.G7Core;if(!o?.state){ee.warn("[addDescriptionLocale] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=it(),s=a.ui?.descriptionActiveLocales??["ko"];if(s.includes(t)){o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_already_added")??"This language is already added.");return}if(!n.includes(t)){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_unsupported")??"Unsupported language.");return}o.state.setLocal({form:{...a.form,description:{...a.form?.description,[t]:""},description_mobile:{...a.form?.description_mobile,[t]:""}},ui:{...a.ui,descriptionActiveLocales:[...s,t],descriptionLocale:t,showAddLocaleModal:!1},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_added",{locale:t.toUpperCase()})??`${t.toUpperCase()} language has been added.`),ee.log(`[addDescriptionLocale] Added locale: ${t}`)}const Y=window.G7Core?.createLogger?.("Ecom:BulkUpdate")??{log:(...r)=>console.log("[Ecom:BulkUpdate]",...r),warn:(...r)=>console.warn("[Ecom:BulkUpdate]",...r),error:(...r)=>console.error("[Ecom:BulkUpdate]",...r)};function ct(r,c,e){const t=c?.response?.data??{},o=t?.message||c?.message||"",a=t?.errors,n=a&&typeof a=="object"?Object.values(a).map(i=>Array.isArray(i)?i[0]:String(i)):[],s=o||r.t(e);r.toast.error(s),r.state.set({bulkUpdateErrors:n.length>0?n:s?[s]:[]})}function W(r){return typeof r=="string"?r:r&&typeof r=="object"?r.ko||r.en||JSON.stringify(r):String(r??"")}function ve(r,c,e){return e?r.t(`sirsoft-ecommerce.enums.${c}.${e}`)||e:String(e??"")}function lt(){const r=window.G7Core;return r||(Y.error("[bulkUpdate] G7Core is not available"),null)}function qr(r,c){const e=lt();if(!e)return;const t=e.state.getLocal()||{},o=e.state.get()||{},a=o.bulkSelectedItems||t.selectedItems||[],n=o.bulkSelectedOptionIds||t.selectedOptionIds||[];Y.log("[bulkUpdate] globalState.bulkSelectedItems:",o.bulkSelectedItems),Y.log("[bulkUpdate] localState.selectedItems:",t.selectedItems),Y.log("[bulkUpdate] Final selectedItems:",a),a.length>0?Ur(e,o,t,a):n.length>0?Vr(e,o,t,n):e.toast.warning(e.t("sirsoft-ecommerce.admin.product.bulk.no_selection"))}function Ur(r,c,e,t){const a=r.dataSource.get("products")?.data?.data||[],n={};c.bulkSalesStatus&&(n.sales_status=c.bulkSalesStatus),c.bulkDisplayStatus&&(n.display_status=c.bulkDisplayStatus);const s={};if(c.bulkPriceMethod&&c.bulkPriceValue!==void 0&&c.bulkPriceValue!==null&&c.bulkPriceValue!==""){const f={fixed:"set",increase:"add",decrease:"subtract"};c.bulkPriceUnit==="percent"?s.price_adjustment={method:"percent",value:c.bulkPriceMethod==="decrease"?-(Number(c.bulkPriceValue)||0):Number(c.bulkPriceValue)||0}:s.price_adjustment={method:f[c.bulkPriceMethod]||"add",value:Number(c.bulkPriceValue)||0}}if(c.bulkStockMethod&&c.bulkStockValue!==void 0&&c.bulkStockValue!==null&&c.bulkStockValue!==""){const f={set:"set",increase:"add",decrease:"subtract"};s.stock_quantity={method:f[c.bulkStockMethod]||"add",value:Number(c.bulkStockValue)||0}}const i=e.modifiedProductIds||[],l=e.modifiedOptionIds||[],u=e.modifiedProductFields||{},d=e.modifiedOptionFields||{},m=c.bulkSelectedOptionIds||e.selectedOptionIds||[],p=new Set(t),g=[],w=[];function y(f,b,C){if(b._modified||l.includes(C)){const P={product_id:f.id,option_id:b.id},S=d[C]||[];!s.price_adjustment&&S.includes("price_adjustment")&&(P.price_adjustment=b.price_adjustment),!s.price_adjustment&&S.includes("selling_price")&&(P.price_adjustment=b.price_adjustment),!s.stock_quantity&&S.includes("stock_quantity")&&(P.stock_quantity=b.stock_quantity),S.includes("option_name")&&(P.option_name=b.option_name),S.includes("sku")&&(P.sku=b.sku),S.includes("list_price")&&(P.list_price=b.list_price),S.includes("safe_stock_quantity")&&(P.safe_stock_quantity=b.safe_stock_quantity),S.includes("is_default")&&(P.is_default=b.is_default),S.includes("is_active")&&(P.is_active=b.is_active),Object.keys(P).length>2&&w.push(P)}}a.filter(f=>t.includes(f.id)).forEach(f=>{if(f._modified||i.includes(f.id)){const C={id:f.id},x=String(f.id),P=u[x]||[];!n.sales_status&&P.includes("sales_status")&&(C.sales_status=f.sales_status),!n.display_status&&P.includes("display_status")&&(C.display_status=f.display_status),P.includes("name")&&(C.name=f.name),P.includes("list_price")&&(C.list_price=f.list_price),P.includes("selling_price")&&(C.selling_price=f.selling_price),Object.keys(C).length>1&&g.push(C)}(f.options||[]).forEach(C=>{const x=`${f.id}-${C.id}`;y(f,C,x)})}),m.length>0&&a.forEach(f=>{p.has(f.id)||(f.options||[]).forEach(b=>{const C=`${f.id}-${b.id}`;m.includes(C)&&y(f,b,C)})});const _={ids:t};Object.keys(n).length>0&&(_.bulk_changes=n),g.length>0&&(_.items=g),Object.keys(s).length>0&&(_.option_bulk_changes=s),w.length>0&&(_.option_items=w),r.state.set({isProcessing:!0,bulkUpdateErrors:[]}),r.api.patch("/api/modules/sirsoft-ecommerce/admin/products/bulk-update",_).then(f=>{r.toast.success(r.t("sirsoft-ecommerce.admin.product.bulk.update_success",{products:f?.data?.products_updated||0,options:f?.data?.options_updated||0})),ut(r),dt(r),r.modal.close()}).catch(f=>{Y.error("[bulkUpdateProducts] Error:",f),ct(r,f,"sirsoft-ecommerce.admin.product.bulk.update_error")}).finally(()=>{r.state.set({isProcessing:!1})})}function Vr(r,c,e,t){const a=r.dataSource.get("products")?.data?.data||[],n={};if(c.bulkPriceMethod&&c.bulkPriceValue!==void 0&&c.bulkPriceValue!==null&&c.bulkPriceValue!==""){const d={fixed:"set",increase:"add",decrease:"subtract"};c.bulkPriceUnit==="percent"?n.price_adjustment={method:"percent",value:c.bulkPriceMethod==="decrease"?-(Number(c.bulkPriceValue)||0):Number(c.bulkPriceValue)||0}:n.price_adjustment={method:d[c.bulkPriceMethod]||"add",value:Number(c.bulkPriceValue)||0}}if(c.bulkStockMethod&&c.bulkStockValue!==void 0&&c.bulkStockValue!==null&&c.bulkStockValue!==""){const d={set:"set",increase:"add",decrease:"subtract"};n.stock_quantity={method:d[c.bulkStockMethod]||"add",value:Number(c.bulkStockValue)||0}}const s=e.modifiedOptionFields||{},i=e.modifiedOptionIds||[],l=[];a.forEach(d=>{(d.options||[]).forEach(m=>{const p=`${d.id}-${m.id}`,g=m._modified||i.includes(p);if(t.includes(p)&&g){const w={product_id:d.id,option_id:m.id},y=s[p]||[];!n.price_adjustment&&y.includes("price_adjustment")&&(w.price_adjustment=m.price_adjustment),!n.price_adjustment&&y.includes("selling_price")&&(w.price_adjustment=m.price_adjustment),!n.stock_quantity&&y.includes("stock_quantity")&&(w.stock_quantity=m.stock_quantity),y.includes("option_name")&&(w.option_name=m.option_name),y.includes("sku")&&(w.sku=m.sku),y.includes("list_price")&&(w.list_price=m.list_price),y.includes("safe_stock_quantity")&&(w.safe_stock_quantity=m.safe_stock_quantity),y.includes("is_default")&&(w.is_default=m.is_default),y.includes("is_active")&&(w.is_active=m.is_active),Object.keys(w).length>2&&l.push(w)}})});const u={ids:t};Object.keys(n).length>0&&(u.bulk_changes=n),l.length>0&&(u.items=l),r.state.set({isProcessing:!0,bulkUpdateErrors:[]}),r.api.patch("/api/modules/sirsoft-ecommerce/admin/options/bulk-update",u).then(d=>{r.toast.success(r.t("sirsoft-ecommerce.admin.product.bulk.option_update_success",{count:d?.data?.options_updated||0})),ut(r),dt(r),r.modal.close()}).catch(d=>{Y.error("[bulkUpdateOptions] Error:",d),ct(r,d,"sirsoft-ecommerce.admin.product.bulk.option_update_error")}).finally(()=>{r.state.set({isProcessing:!1})})}function dt(r){Promise.resolve(r.dataSource.refetch("products")).then(()=>{oo(r,{force:!0})}).catch(c=>{Y.error("[bulkUpdate] Failed to refetch products:",c)})}function ut(r){r.state.setLocal({modifiedProductIds:[],modifiedOptionIds:[],modifiedProductFields:{},modifiedOptionFields:{},selectedItems:[],selectedOptionIds:[]}),r.state.set({bulkSalesStatus:null,bulkDisplayStatus:null,bulkPriceCondition:null,bulkStockCondition:null,bulkConfirmData:null,bulkUpdateErrors:[]})}function jr(r,c){const e=lt();if(!e)return;const t=e.state.getLocal()||{},o=e.state.get()||{},n=e.dataSource.get("products")?.data?.data||[],s=o.bulkSelectedItems||t.selectedItems||[],i=o.bulkSelectedOptionIds||t.selectedOptionIds||[],l={products:[],options:[],summary:{productCount:0,optionCount:0,hasBulkChanges:!1,hasInlineChanges:!1}},u=!!(o.bulkSalesStatus||o.bulkDisplayStatus||o.bulkPriceCondition||o.bulkStockCondition);l.summary.hasBulkChanges=u;const d=t.modifiedProductIds||[],m=t.modifiedOptionIds||[],p=t.modifiedProductFields||{},g=t.modifiedOptionFields||{},w=new Set(s);function y(_,f,b){const C=[],x=g[f]||[];if(o.bulkPriceCondition&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_price_adjustment_inline",{value:o.bulkPriceCondition})||`Price adjustment: ${o.bulkPriceCondition}`),o.bulkStockCondition&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_stock_inline",{value:o.bulkStockCondition})||`Stock: ${o.bulkStockCondition}`),b){if(!o.bulkPriceCondition&&x.includes("price_adjustment")){const P=_.price_adjustment?.toLocaleString?.()??_.price_adjustment;C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_price_adjustment_inline",{value:P})||`Price adjustment: ${P}`)}!o.bulkPriceCondition&&x.includes("selling_price")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selling_price",{price:_.selling_price?.toLocaleString?.()??_.selling_price})||`Selling price: ${_.selling_price}`),!o.bulkStockCondition&&x.includes("stock_quantity")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_stock_inline",{value:_.stock_quantity})||`Stock: ${_.stock_quantity}`),x.includes("option_name")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_option_name",{name:W(_.option_name)})||`Option name: ${W(_.option_name)}`),x.includes("sku")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sku",{sku:_.sku})||`SKU: ${_.sku}`),x.includes("list_price")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_list_price",{price:ue(Number(_.list_price??0))})||`List price: ${_.list_price}`),x.includes("safe_stock_quantity")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_safe_stock",{value:_.safe_stock_quantity})||`Safe stock: ${_.safe_stock_quantity}`),x.includes("is_active")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_is_active",{value:_.is_active?"ON":"OFF"})||`Active: ${_.is_active?"ON":"OFF"}`),l.summary.hasInlineChanges=!0}return C}s.length>0&&n.filter(_=>s.includes(_.id)).forEach(_=>{const f=[],b=_._modified||d.includes(_.id),C=String(_.id),x=p[C]||[];if(o.bulkSalesStatus){const S=ve(e,"sales_status",o.bulkSalesStatus);f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sales_status",{status:S})||`Sales status: ${S}`)}if(o.bulkDisplayStatus){const S=ve(e,"display_status",o.bulkDisplayStatus);f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_display_status",{status:S})||`Display status: ${S}`)}if(b){if(!o.bulkSalesStatus&&x.includes("sales_status")){const S=ve(e,"sales_status",_.sales_status);f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sales_status",{status:S})||`Sales status: ${S}`)}if(!o.bulkDisplayStatus&&x.includes("display_status")){const S=ve(e,"display_status",_.display_status);f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_display_status",{status:S})||`Display status: ${S}`)}x.includes("name")&&f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_name_changed")||"Name changed"),x.includes("list_price")&&f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_list_price",{price:ue(Number(_.list_price??0))})||`List price: ${_.list_price?.toLocaleString?.()||_.list_price}`),x.includes("selling_price")&&f.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selling_price",{price:ue(Number(_.selling_price??0))})||`Selling price: ${_.selling_price?.toLocaleString?.()||_.selling_price}`),l.summary.hasInlineChanges=!0}(f.length>0||u||b)&&l.products.push({id:_.id,name:W(_.name),changes:f.length>0?f.join(", "):b?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"});const P=u&&!!(o.bulkPriceCondition||o.bulkStockCondition);if(Array.isArray(_.options))_.options.forEach(S=>{const N=`${_.id}-${S.id}`,L=S._modified||m.includes(N),O=y(S,N,L);(O.length>0||P||L)&&l.options.push({productId:_.id,optionId:S.id,productName:W(_.name),optionName:S.option_name_localized||W(S.option_name)||S.name||"",changes:O.length>0?O.join(", "):L?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"})});else if(P){const S=Number(_.options_total_count)||0;S>0&&l.options.push({aggregate:!0,productId:_.id,optionId:0,optionCount:S,productName:W(_.name),optionName:"",changes:y({},`${_.id}-*`,!1).join(", ")||e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"})}}),i.length>0&&n.forEach(_=>{w.has(_.id)||(_.options||[]).forEach(f=>{const b=`${_.id}-${f.id}`;if(i.includes(b)){const C=f._modified||m.includes(b),x=y(f,b,C);l.options.push({productId:_.id,optionId:f.id,productName:W(_.name),optionName:f.option_name_localized||W(f.option_name)||f.name||"",changes:x.length>0?x.join(", "):C?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selected")||"Selected"})}})}),l.summary.productCount=l.products.length,l.summary.optionCount=l.options.reduce((_,f)=>_+(f.aggregate?f.optionCount||0:1),0),e.state.set({bulkConfirmData:l}),Y.log("[buildConfirmData] Built confirm data:",l)}const M=window.G7Core?.createLogger?.("Ecom:Order")??{log:(...r)=>console.log("[Ecom:Order]",...r),warn:(...r)=>console.warn("[Ecom:Order]",...r),error:(...r)=>console.error("[Ecom:Order]",...r)};function Br(r){const{currencies:c=[]}=r.params||{},e=[{field:"no",header:"NO",width:"60px",sortable:!1,required:!0},{field:"ordered_at",header:"$t:sirsoft-ecommerce.admin.order.column.order_date",width:"140px",sortable:!0,required:!0},{field:"order_number",header:"$t:sirsoft-ecommerce.admin.order.column.order_number",width:"140px",sortable:!0,required:!0},{field:"product",header:"$t:sirsoft-ecommerce.admin.order.column.product",width:"auto",sortable:!1,required:!0},{field:"order_amount",header:"$t:sirsoft-ecommerce.admin.order.column.order_amount",width:"120px",sortable:!0,required:!1},{field:"order_status",header:"$t:sirsoft-ecommerce.admin.order.column.order_status",width:"100px",sortable:!1,required:!1},{field:"orderer",header:"$t:sirsoft-ecommerce.admin.order.column.orderer",width:"120px",sortable:!1,required:!1},{field:"payment_method",header:"$t:sirsoft-ecommerce.admin.order.column.payment_method",width:"100px",sortable:!1,required:!1},{field:"delivery_method",header:"$t:sirsoft-ecommerce.admin.order.column.delivery_method",width:"100px",sortable:!1,required:!1},{field:"country",header:"$t:sirsoft-ecommerce.admin.order.column.country",width:"80px",sortable:!1,required:!1}],t=c.filter(o=>!o.is_default).map(o=>({field:`order_amount_${o.code.toLowerCase()}`,header:`${o.name} (${o.symbol})`,width:"100px",sortable:!0,required:!1}));return[...e,...t]}function Kr(r,c){const e=r.params||{},{target:t="_local",path:o,value:a}=e,n=window.G7Core;if(!n?.state)return M.warn("[toggleArrayValue] G7Core.state를 사용할 수 없습니다."),{success:!1,newValue:[]};const s=t==="_global"?n.state.getGlobal():n.state.getLocal(),i=o.split(".");let l=s;for(const d of i)l=l?.[d];Array.isArray(l)||(l=[]);let u;return l.includes(a)?u=l.filter(d=>d!==a):u=[...l,a],t==="_global"?n.state.setGlobal({[o]:u}):n.state.setLocal({[o]:u}),{success:!0,newValue:u}}function Qr(r,c){const e=r.params||{},{filterKey:t,checked:o}=e,a=window.G7Core;if(!a?.state)return M.warn("[toggleVisibleFilter] G7Core.state를 사용할 수 없습니다."),{success:!1,newValue:[]};const s=a.state.getLocal().visibleFilters||[];let i;o?i=s.includes(t)?s:[...s,t]:i=s.filter(l=>l!==t),a.state.setLocal({visibleFilters:i});try{localStorage.setItem("g7_order_filter_visible_filters",JSON.stringify(i))}catch(l){M.warn("[toggleVisibleFilter] LocalStorage 저장 실패:",l)}return{success:!0,newValue:i}}function zr(r,c){const e=r.params||{},{selectedIds:t=[]}=e,o=window.G7Core;return o?.state?(o.state.setLocal({selectedItems:t,selectAll:!1}),{success:!0}):(M.warn("[syncOrderSelection] G7Core.state를 사용할 수 없습니다."),{success:!1})}function Yr(r,c){const e=r.params||{},{actionId:t,row:o}=e,a=window.G7Core;if(!t||!o?.order_number)return M.warn("[handleOrderRowAction] actionId 또는 row.order_number가 없습니다."),{success:!1};switch(t){case"view":a?.navigate?.(`/admin/ecommerce/orders/${o.order_number}`);break;case"edit":a?.navigate?.(`/admin/ecommerce/orders/${o.order_number}`);break;case"status":case"shipping":case"memo":case"delete":a?.state?.setGlobal({orderList:{currentActionOrderId:o.id,currentActionType:t}});break;case"print":window.open(`/admin/ecommerce/orders/${o.order_number}/shipping-label`,"_blank");break;default:return console.warn(`[handleOrderRowAction] 알 수 없는 액션: ${t}`),{success:!1}}return{success:!0,action:t,targetId:o.id}}async function Wr(r,c){const e=r.params||{},{actionType:t,orderIds:o,status:a,carrierId:n,trackingNumber:s}=e,i=window.G7Core;if(!o||o.length===0)throw new Error("선택된 주문이 없습니다.");let l="";const u={ids:o};switch(t){case"status":if(!a)throw new Error("변경할 상태를 선택해주세요.");l="/api/modules/sirsoft-ecommerce/admin/orders/bulk",u.order_status=a;break;case"shipping":if(!s&&!n)throw new Error("택배사 또는 운송장번호를 입력해주세요.");l="/api/modules/sirsoft-ecommerce/admin/orders/bulk",n&&(u.carrier_id=n),s&&(u.tracking_number=s);break;default:throw new Error(`알 수 없는 액션 타입: ${t}`)}const d=await i?.api?.call(l,{method:"PATCH",body:u});if(!d?.success)throw new Error(d?.message||"일괄 처리에 실패했습니다.");return{success:!0,successCount:d.data?.updated_count??o.length,failCount:d.data?.fail_count??0,errors:d.data?.errors??[]}}async function Jr(r,c){const e=r.params||{},{downloadType:t,orderIds:o,filters:a}=e,n=window.G7Core,s="/api/modules/sirsoft-ecommerce/admin/orders/export",i={};if(t==="selected"){if(!o||o.length===0)throw new Error("선택된 주문이 없습니다.");i.ids=o}else i.filters=a||{};const l=n?.auth?.getToken(),u=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...l?{Authorization:`Bearer ${l}`}:{}},body:JSON.stringify(i)});if(!u.ok){const y=await u.json().catch(()=>null);throw new Error(y?.message||"엑셀 다운로드에 실패했습니다.")}const d=await u.blob(),m=window.URL.createObjectURL(d),p=document.createElement("a");p.href=m;const g=new Date,w=`${g.getFullYear()}-${String(g.getMonth()+1).padStart(2,"0")}-${String(g.getDate()).padStart(2,"0")}`;return p.download=`orders_${w}.xlsx`,document.body.appendChild(p),p.click(),document.body.removeChild(p),window.URL.revokeObjectURL(m),{success:!0}}function Xr(r,c){const e=r.params||{},{columns:t=[]}=e;try{localStorage.setItem("g7_order_visible_columns",JSON.stringify(t))}catch(o){return M.warn("[saveVisibleColumns] LocalStorage 저장 실패:",o),{success:!1}}return{success:!0}}function Zr(r,c){const t=(r.params||{}).defaultColumns||["order_date","order_number","order_status","order_amount","product","orderer","payment_method","delivery_method","country"];try{const o=localStorage.getItem("g7_order_visible_columns");if(o){const a=JSON.parse(o);if(Array.isArray(a))return a}}catch(o){M.warn("[loadVisibleColumns] LocalStorage 로드 실패:",o)}return t}function en(r,c){const e=r.params||{},{id:t,defaultFilters:o=[]}=e,a=window.G7Core;if(!t)return M.warn("[loadVisibleFilters] id 파라미터가 필요합니다."),o;let n;try{const i=a?.AuthManager?.getInstance();i&&(n=i.getUser()?.uuid)}catch{}const s=`g7_filters_${t}${n!==void 0?`_${n}`:""}`;try{const i=localStorage.getItem(s);if(i){const l=JSON.parse(i);if(Array.isArray(l)&&l.length>0)return M.log(`[loadVisibleFilters] localStorage에서 로드: ${s}`,l),l}}catch(i){M.warn("[loadVisibleFilters] LocalStorage 로드 실패:",i)}return M.log(`[loadVisibleFilters] 기본값 사용: ${s}`,o),o}function tn(r,c){const e=window.G7Core;if(!e)return;const t=e.state.get()||{},o=t.bulkSelectedItems||[],a=t.bulkOrderStatus||"",n=t.bulkCourier||"",s=t.bulkTrackingNumber||"";if(!a){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_status_selected")||"변경할 주문상태를 선택해주세요.");return}if(["shipping_ready","shipping"].includes(a)&&(!n||!s)){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.carrier_required")||"해당 상태로 변경하려면 택배사와 송장번호를 모두 입력해주세요.");return}e.state.set({bulkConfirmData:{selectedItems:o,orderStatus:a,courierId:n,trackingNumber:s}}),e.modal?.open?.("modal_bulk_confirm")}async function on(r,c){const e=window.G7Core;if(!e)return;const o=(e.state.get()||{}).bulkConfirmData;if(!o||!o.selectedItems?.length){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.error")||"일괄 처리에 실패했습니다.");return}const a={ids:o.selectedItems,order_status:o.orderStatus};o.courierId&&(a.carrier_id=o.courierId),o.trackingNumber&&(a.tracking_number=o.trackingNumber);try{await e.api.patch("/api/modules/sirsoft-ecommerce/admin/orders/bulk",a),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.success")||"일괄 변경이 완료되었습니다."),e.state.setLocal({selectedItems:[],selectAll:!1,bulkOrderStatus:"",bulkCourier:"",bulkTrackingNumber:""}),e.state.set({bulkConfirmData:null}),e.dataSource?.refetch?.("orders"),e.modal?.close?.()}catch(n){M.error("[executeOrderBulkAction] Error:",n);const i=(n?.response?.data||n?.data||{})?.message||n?.message;e.toast?.error?.(i||e.t?.("sirsoft-ecommerce.admin.order.bulk.error")||"일괄 처리에 실패했습니다.")}}const qe=window.G7Core?.createLogger?.("Ecom:ProductList")??{log:(...r)=>console.log("[Ecom:ProductList]",...r),warn:(...r)=>console.warn("[Ecom:ProductList]",...r),error:(...r)=>console.error("[Ecom:ProductList]",...r)};async function rn(r,c){const e=r.params||{},{actionId:t,row:o}=e,a=window.G7Core;if(!t||!o?.id)return qe.warn("[handleProductRowAction] actionId 또는 row.id가 없습니다."),{success:!1};switch(t){case"edit":a?.navigate?.(`/admin/ecommerce/products/${o.product_code}/edit`);break;case"copy":a?.state?.setLocal({productList:{copyTargetProduct:o},ui:{showCopyModal:!0}});break;case"delete":try{const s=(await a?.api?.get(`/api/modules/sirsoft-ecommerce/admin/products/${o.id}/can-delete`))?.data;a?.state?.setLocal({productList:{deleteTargetProduct:o,canDelete:s?.canDelete??!0,deleteBlockReason:s?.reason??null,relatedData:s?.relatedData??null},ui:{showDeleteModal:!0}})}catch(n){qe.error("[handleProductRowAction] 삭제 가능 여부 확인 실패:",n),a?.state?.setLocal({productList:{deleteTargetProduct:o,canDelete:!1,deleteBlockReason:"삭제 가능 여부를 확인할 수 없습니다.",relatedData:null},ui:{showDeleteModal:!0}})}break;default:return qe.warn(`[handleProductRowAction] 알 수 없는 액션: ${t}`),{success:!1}}return{success:!0,action:t,targetId:o.id}}const U=window.G7Core?.createLogger?.("Ecom:OrderDetail")??{log:(...r)=>console.log("[Ecom:OrderDetail]",...r),warn:(...r)=>console.warn("[Ecom:OrderDetail]",...r),error:(...r)=>console.error("[Ecom:OrderDetail]",...r)};function mt(){return typeof document>"u"?"":document.querySelector('input[name="batchTrackingNumber"]')?.value?.trim()??""}function nn(r,c){const e=window.G7Core;if(!e?.state){U.warn("[initOrderDetailForm] G7Core.state를 사용할 수 없습니다.");return}e.state.getLocal();const o=c?.data?.data??e.dataSource?.get?.("order")?.data;if(!o){U.warn("[initOrderDetailForm] order 데이터가 없습니다.");return}e.state.setLocal({form:{recipient_name:o.recipient_name||"",recipient_phone:o.recipient_phone||"",recipient_tel:o.recipient_tel||"",recipient_zipcode:o.recipient_zipcode||"",recipient_address:o.recipient_address||"",recipient_detail_address:o.recipient_detail_address||"",delivery_memo:o.delivery_memo||"",admin_memo:o.admin_memo||""}}),U.log("[initOrderDetailForm] 폼 초기화 완료")}function an(r,c){const e=window.G7Core,{optionId:t}=r.params||{};if(!e?.state||!t)return;const a=e.state.getLocal().selectedProducts||[];let n;a.includes(t)?n=a.filter(u=>u!==t):n=[...a,t];const i=(e.dataSource?.get?.("order")?.data?.options||[]).length,l=n.length===i&&i>0;e.state.setLocal({selectedProducts:n,selectAll:l})}function sn(r,c){const e=window.G7Core;if(!e?.state)return;const o=e.state.getLocal().selectAll,n=e.dataSource?.get?.("order")?.data?.options||[];if(o)e.state.setLocal({selectedProducts:[],selectAll:!1});else{const s=n.map(i=>i.id);e.state.setLocal({selectedProducts:s,selectAll:!0})}}function cn(r,c){const e=window.G7Core;if(!e)return;const t=r.params||{},o=t.selectedProducts||[],a=t.batchOrderStatus||"",n=t.batchCarrierId||"",s=t.batchTrackingNumber||mt();if(!a){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_status_selected")||"변경할 주문상태를 선택해주세요.");return}if(o.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_items_selected")||"변경할 상품을 선택해주세요.");return}if(a==="cancelled"){const p=t.cancelPg??!0,g=e.dataSource?.get?.("order")?.data,w=g?.options||[],y=g?.currency_snapshot?.base_currency??null,_=o.map(f=>{const b=w.find(x=>x.id===f);if(!b||b.option_status==="cancelled")return null;const C=b.quantity??1;return{id:b.id,product_name:b.product_name,product_option_name:b.product_option_name,thumbnail_url:b.product_snapshot?.thumbnail_url||"",unit_price:b.unit_price,unit_price_formatted:b.unit_price_formatted??ue(Number(b.unit_price??0),y),quantity:C,subtotal_formatted:ue(Number(b.unit_price??0)*C,y),cancel_quantity:C,option_status:b.option_status,option_status_label:b.option_status_label||b.option_status}}).filter(Boolean);if(_.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_cancellable_items")||"취소할 수 있는 상품이 없습니다. 이미 취소된 상품은 제외됩니다.");return}if(p){e.state.setLocal({cancelItems:_,cancelReason:"",cancelReasonDetail:"",cancelPg:!0,refundPriority:"pg_first",refundEstimate:null,refundLoading:!1,isCancelling:!1,cancelError:null,cancelValidationErrors:null}),e.modal?.open?.("modal_cancel_order");const f=g?.order_number||"";f&&e.dispatch?.({handler:"sirsoft-ecommerce.estimateRefundAmount",params:{orderId:f,cancelItems:_}})}else{const f={};for(const b of _)f[b.id]=b.quantity;e.state.setLocal({bulkConfirmItems:_,changeQuantities:f,batchOrderStatus:a}),e.modal?.open?.("modal_batch_change_confirm")}return}if(["shipping_ready","shipping","delivered"].includes(a)&&(!n||!s)){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.carrier_required")||"해당 상태로 변경하려면 택배사와 송장번호를 모두 입력해주세요.");return}const u=e.dataSource?.get?.("order")?.data?.options||[],d=[],m={};for(const p of o){const g=u.find(w=>w.id===p);g&&(d.push({id:g.id,product_name:g.product_name,product_option_name:g.product_option_name,sku:g.sku,thumbnail_url:g.product_snapshot?.thumbnail_url||"",unit_price:g.unit_price,original_price:g.product_snapshot?.original_price||g.unit_price,quantity:g.quantity,option_status:g.option_status}),m[g.id]=g.quantity)}e.state.setLocal({bulkConfirmItems:d,changeQuantities:m,batchOrderStatus:a,batchCarrierId:n,batchTrackingNumber:s}),e.modal?.open?.("modal_batch_change_confirm")}async function ln(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t){U.warn("[processOrderDetailBulkChange] orderId가 없습니다.");return}const o=e.state.getLocal?.()??{},a=r.params?.selectedProducts??o.selectedProducts,n=r.params?.batchOrderStatus||o.batchOrderStatus,s=r.params?.batchCarrierId||o.batchCarrierId,i=r.params?.batchTrackingNumber||o.batchTrackingNumber||mt(),l=r.params?.changeQuantities??o.changeQuantities;if(!a||a.length===0){U.warn("[processOrderDetailBulkChange] 선택된 상품이 없습니다.");return}if(!n){U.warn("[processOrderDetailBulkChange] 변경할 상태가 없습니다.");return}const d=e.dataSource?.get?.("order")?.data?.options||[],p={items:a.map(g=>{const w=d.find(y=>y.id===g);return{option_id:g,quantity:l?.[g]??w?.quantity??1}}),status:n};s&&(p.carrier_id=s),i&&(p.tracking_number=i);try{const g=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/options/bulk-status`,p);if(g?.success){const w=e.t,y=g.data?.changed_count??a.length;e.toast?.success?.(w?.("sirsoft-ecommerce.admin.order.detail.handler.bulk_change_success",{count:y})??`${y}개 옵션의 상태가 변경되었습니다.`),e.modal?.close?.("modal_batch_change_confirm"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}}),e.state.setLocal({selectedProducts:[],selectAll:!1,batchOrderStatus:"",batchCarrierId:"",batchTrackingNumber:"",changeQuantities:{}})}else throw new Error(g?.message||"상태 변경에 실패했습니다.")}catch(g){U.error("[processOrderDetailBulkChange] API 호출 실패:",g);const y=(g?.response?.data||g?.data||{})?.message||g?.message;e.toast?.error?.(y??e.t?.("sirsoft-ecommerce.admin.order.detail.handler.bulk_change_failed")??"상태 변경에 실패했습니다.")}}async function dn(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t)return;const a=e.state.getLocal().form?.admin_memo??"";try{const n=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}`,{admin_memo:a});if(n?.success)e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.handler.memo_save_success")??"관리자 메모가 저장되었습니다.");else throw new Error(n?.message)}catch(n){U.error("[saveAdminMemo] 저장 실패:",n),e.toast?.error?.(n?.message??"메모 저장에 실패했습니다.")}}function un(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o}=r.params||{};if(!t||!o)return;const a=r.params?.value??r.$event?.target?.value,n=parseInt(String(a),10),s=Math.max(1,Math.min(isNaN(n)?o:n,o)),l={...e.state.getLocal().changeQuantities||{}};l[t]=s,e.state.setLocal({changeQuantities:l})}function mn(r,c){const e=window.G7Core;if(!e?.state){U.warn("[openConfirmDepositModal] G7Core.state 를 사용할 수 없습니다.");return}const t=e.dataSource?.get?.("order")?.data,o=t?.depositor_name??"",a=t?.total_due_charge_amount??t?.total_due_amount??0;e.state.setLocal({depositorName:o,depositAmount:a,depositConfirmErrors:null,isConfirmingDeposit:!1,markOrderComplete:!1}),e.modal?.open?.("modal_confirm_deposit")}async function pn(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t){U.warn("[confirmDeposit] orderId 가 없습니다.");return}const o=e.state.getLocal(),a=r.params?.amount??o.depositAmount,n=r.params?.depositorName??o.depositorName??"",s=r.params?.markOrderComplete??o.markOrderComplete??!1,i=s===!0||s==="true";e.state.setLocal({isConfirmingDeposit:!0,depositConfirmErrors:null});try{const l=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/confirm-deposit`,{amount:a,depositor_name:n,mark_order_complete:i});if(l?.success)e.state.setLocal({isConfirmingDeposit:!1,depositConfirmErrors:null}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.modal.confirm_deposit.confirm_success")??"입금이 확인되어 결제완료 처리되었습니다."),e.modal?.close?.("modal_confirm_deposit"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}});else throw new Error(l?.message||"Deposit confirm failed")}catch(l){U.error("[confirmDeposit] 실패:",l);const u=l?.response?.data||l?.data||{},d=u?.errors?.detail||u?.message||l?.message||"",m=e.t?.("sirsoft-ecommerce.admin.order.detail.modal.confirm_deposit.confirm_failed")??"입금 확인에 실패했습니다.";e.state.setLocal({isConfirmingDeposit:!1,depositConfirmErrors:{amount:[d||m]}}),e.toast?.error?.(d||m)}}const Ue=window.G7Core?.createLogger?.("Ecom:CancelOrder")??{log:(...r)=>console.log("[Ecom:CancelOrder]",...r),warn:(...r)=>console.warn("[Ecom:CancelOrder]",...r),error:(...r)=>console.error("[Ecom:CancelOrder]",...r)};let K=null;function fn(){K&&(clearTimeout(K),K=null,Ue.log("[clearCancelOrderTimers] debounce 타이머 정리 완료"))}function gn(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o,orderId:a}=r.params||{};if(!t||!o||!a)return;const n=r.params?.value??r.$event?.target?.value,s=parseInt(String(n),10),i=Math.max(1,Math.min(isNaN(s)?o:s,o)),u=(e.state.getLocal().cancelItems||[]).map(m=>m.id===t?{...m,cancel_quantity:i}:m);e.state.setLocal({cancelItems:u});const d=window.__g7ActionContext;K&&clearTimeout(K),K=setTimeout(()=>{const m=window.__g7ActionContext;window.__g7ActionContext=d;try{Ve(e,a)}finally{window.__g7ActionContext=m}},500)}async function _n(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,cancelItems:o}=r.params||{};t&&await Ve(e,t,o)}async function Ve(r,c,e){const t=r.state.getLocal(),o=e||t.cancelItems||[],a=t.refundPriority||"pg_first";if(o.length===0){r.state.setLocal({refundEstimate:null});return}const n=o.map(s=>({order_option_id:s.id,cancel_quantity:s.cancel_quantity}));r.state.setLocal({refundLoading:!0});try{const s=await r.api.post(`/api/modules/sirsoft-ecommerce/admin/orders/${c}/estimate-refund`,{items:n,refund_priority:a});if(s?.success)r.state.setLocal({refundEstimate:s.data,refundLoading:!1,cancelError:null});else throw new Error(s?.message||"Estimate failed")}catch(s){Ue.error("[estimateRefundAmount] 실패:",s),r.state.setLocal({refundLoading:!1,refundEstimate:null}),r.toast?.error?.(r.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.estimate_failed")??"환불 예상금액 계산에 실패했습니다.")}}function wn(r,c){const e=window.G7Core;if(!e?.state)return;const{priority:t,orderId:o}=r.params||{};if(!t||!o)return;e.state.setLocal({refundPriority:t});const a=window.__g7ActionContext;K&&clearTimeout(K),K=setTimeout(()=>{const n=window.__g7ActionContext;window.__g7ActionContext=a;try{Ve(e,o)}finally{window.__g7ActionContext=n}},300)}async function yn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};if(!t)return;const o=e.state.getLocal(),a=r.params?.cancelItems||o.cancelItems||[],n=r.params?.cancelReason||o.cancelReason||"",s=r.params?.cancelReasonDetail||o.cancelReasonDetail||"",i=r.params?.cancelPg??o.cancelPg??!0,l=r.params?.refundPriority||o.refundPriority||"pg_first",u={type:"partial",cancel_pg:i,refund_priority:l,items:a.map(m=>({order_option_id:m.id,cancel_quantity:m.cancel_quantity}))};n&&(u.reason=n),s&&(u.reason_detail=s);const d={bank_code:o.refundBankCode||"",account_number:o.refundBankAccount||"",holder:o.refundBankHolder||""};(d.bank_code||d.account_number||d.holder)&&(u.refund_bank=d),e.state.setLocal({isCancelling:!0,cancelError:null,cancelValidationErrors:null});try{const m=await e.api.post(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/cancel`,u);if(m?.success)e.state.setLocal({isCancelling:!1}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_success")??"주문이 취소되었습니다."),e.modal?.close?.("modal_cancel_order"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}}),e.state.setLocal({selectedProducts:[],selectAll:!1,batchOrderStatus:""});else throw new Error(m?.message||"Cancel failed")}catch(m){Ue.error("[executeCancelOrder] 실패:",m);const p=m?.response?.data||m?.data,w=(m?.response?.status||m?.status)===422,y=p?.errors?.detail,_=p?.errors??null,f=y||p?.message||m?.message||"",b=e.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_failed")??"주문 취소에 실패했습니다.";w?(e.state.setLocal({isCancelling:!1,cancelError:null,cancelValidationErrors:_}),e.toast?.error?.(f||b)):(e.state.setLocal({isCancelling:!1,cancelError:f||b,cancelValidationErrors:null}),e.toast?.error?.(f||b))}}const pt=window.G7Core?.createLogger?.("Ecom:UserCancelOrder")??{log:(...r)=>console.log("[Ecom:UserCancelOrder]",...r),warn:(...r)=>console.warn("[Ecom:UserCancelOrder]",...r),error:(...r)=>console.error("[Ecom:UserCancelOrder]",...r)};let R=null;function Le(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}function hn(){R&&(clearTimeout(R),R=null)}function Cn(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t}=r.params||{};if(!t)return;const a=[...e.state.getLocal().selectedItemIds||[]],n=a.indexOf(t);n>=0?a.splice(n,1):a.push(t);const i=(e.dataSource?.get?.("order")?.data?.options||[]).filter(u=>u.option_status!=="cancelled").map(u=>u.id),l=i.length>0&&i.every(u=>a.includes(u));e.state.setLocal({selectedItemIds:a,selectAllItems:l})}function bn(r,c){const e=window.G7Core;if(!e?.state)return;const t=e.state.getLocal(),a=(e.dataSource?.get?.("order")?.data?.options||[]).filter(s=>s.option_status!=="cancelled").map(s=>s.id),n=!t.selectAllItems;e.state.setLocal({selectedItemIds:n?a:[],selectAllItems:n})}function In(r,c){const e=window.G7Core;if(!e?.state)return;const t=e.dataSource?.get?.("order")?.data;if(!t)return;const a=e.state.getLocal().selectedItemIds||[];if(a.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_selected_items")??"취소할 상품을 선택해주세요.");return}const s=(t.options||[]).filter(l=>a.includes(l.id)&&l.option_status!=="cancelled").map(l=>({id:l.id,product_name:l.product_name??"",product_option_name:l.product_option_name??"",thumbnail_url:l.product_snapshot?.thumbnail_url||"",unit_price:l.unit_price??0,quantity:l.quantity??1,cancel_quantity:l.quantity??1,option_status:l.option_status??"",selected:!0}));if(s.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_cancellable_items")??"취소 가능한 상품이 없습니다.");return}e.state.setLocal({cancelItems:s,cancelSelectAll:!0,cancelReason:"",refundEstimate:null,refundLoading:!1,isCancelling:!1,cancelError:null}),e.modal?.open?.("modal_cancel_order");const i=r.params?.orderId||t.id;(i||t.order_number)&&le(e,i)}function Sn(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,orderId:o}=r.params||{};if(!t)return;const n=(e.state.getLocal().cancelItems||[]).map(i=>i.id===t?{...i,selected:!i.selected}:i),s=n.every(i=>i.selected);e.state.setLocal({cancelItems:n,cancelSelectAll:s}),R&&clearTimeout(R),R=setTimeout(()=>{le(e,o)},500)}function vn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{},o=e.state.getLocal(),a=!o.cancelSelectAll,n=(o.cancelItems||[]).map(s=>({...s,selected:a}));e.state.setLocal({cancelItems:n,cancelSelectAll:a}),R&&clearTimeout(R),R=setTimeout(()=>{le(e,t)},500)}function Ln(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o,orderId:a}=r.params||{};if(!t||!o||!a&&!Le(e))return;const n=r.params?.value??r.$event?.target?.value,s=parseInt(String(n),10),i=Math.max(1,Math.min(isNaN(s)?o:s,o)),u=(e.state.getLocal().cancelItems||[]).map(m=>m.id===t?{...m,cancel_quantity:i}:m);e.state.setLocal({cancelItems:u});const d=window.__g7ActionContext;R&&clearTimeout(R),R=setTimeout(()=>{const m=window.__g7ActionContext;window.__g7ActionContext=d;try{le(e,a)}finally{window.__g7ActionContext=m}},500)}async function xn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};!t&&!Le(e)||await le(e,t)}async function le(r,c){const e=r.state.getLocal(),t=e.cancelItems||[],o=e.refundPriority||"pg_first",a=t.filter(s=>s.selected);if(a.length===0){r.state.setLocal({refundEstimate:null,refundLoading:!1});return}const n=a.map(s=>({order_option_id:s.id,cancel_quantity:s.cancel_quantity}));r.state.setLocal({refundLoading:!0});try{const s=r.state.get?.("_global")?.guestOrderToken,i=r.dataSource?.get?.("order")?.data?.order_number,l=!!(s&&i),u=l?`/api/modules/sirsoft-ecommerce/guest/orders/${i}/estimate-refund`:`/api/modules/sirsoft-ecommerce/user/orders/${c}/estimate-refund`,d=l?{headers:{"X-Guest-Order-Token":s}}:void 0,m=await r.api.post(u,{items:n,refund_priority:o},d);if(m?.success)r.state.setLocal({refundEstimate:m.data,refundLoading:!1,cancelError:null});else throw new Error(m?.message||"Estimate failed")}catch(s){pt.error("[estimateUserRefund] 실패:",s),r.state.setLocal({refundLoading:!1,refundEstimate:null}),r.toast?.error?.(r.t?.("mypage.order_detail.cancel_modal.estimate_failed")??"환불 예상금액 계산에 실패했습니다.")}}function Pn(r,c){const e=window.G7Core;if(!e?.state)return;const{priority:t,orderId:o}=r.params||{};if(!t||!o&&!Le(e))return;e.state.setLocal({refundPriority:t});const a=window.__g7ActionContext;R&&clearTimeout(R),R=setTimeout(()=>{const n=window.__g7ActionContext;window.__g7ActionContext=a;try{le(e,o)}finally{window.__g7ActionContext=n}},300)}async function An(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};if(!t&&!Le(e))return;const o=e.state.getLocal(),n=(o.cancelItems||[]).filter(d=>d.selected),s=r.params?.cancelReason||o.cancelReason||"",i=r.params?.cancelReasonDetail??o.cancelReasonDetail??"",l=r.params?.refundPriority||o.refundPriority||"pg_first";if(n.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_selected_items")??"취소할 상품을 선택해주세요.");return}if(!s){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.reason_required")??"취소 사유를 선택해주세요.");return}const u={reason:s,refund_priority:l,items:n.map(d=>({order_option_id:d.id,cancel_quantity:d.cancel_quantity}))};i&&(u.reason_detail=i),e.state.setLocal({isCancelling:!0,cancelError:null,cancelValidationErrors:null});try{const d=e.state.get?.("_global")?.guestOrderToken,m=e.dataSource?.get?.("order")?.data?.order_number,p=!!(d&&m),g=p?`/api/modules/sirsoft-ecommerce/guest/orders/${m}/cancel`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/cancel`,w=p?{headers:{"X-Guest-Order-Token":d}}:void 0,y=await e.api.post(g,u,w);if(y?.success)e.state.setLocal({isCancelling:!1}),e.toast?.success?.(e.t?.("mypage.order_detail.cancel_success")??"주문이 취소되었습니다."),e.modal?.close?.("modal_cancel_order"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}});else throw new Error(y?.message||"Cancel failed")}catch(d){pt.error("[executeUserCancelOrder] 실패:",d);const m=d?.response?.data||d?.data,p=m?.message||d?.message||"",g=m?.errors??null,w=e.t?.("mypage.order_detail.cancel_modal.cancel_failed")??"주문 취소에 실패했습니다.";e.state.setLocal({isCancelling:!1,cancelError:p||w,cancelValidationErrors:g}),e.toast?.error?.(p||w)}}const ft=window.G7Core?.createLogger?.("Ecom:ConfirmOrder")??{log:(...r)=>console.log("[Ecom:ConfirmOrder]",...r),warn:(...r)=>console.warn("[Ecom:ConfirmOrder]",...r),error:(...r)=>console.error("[Ecom:ConfirmOrder]",...r)};function kn(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}async function On(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,optionId:o}=r.params||{};if(!o||!t&&!kn(e)){ft.error("[confirmOrderOption] optionId 또는 식별자 누락");return}e.state.setLocal({isConfirming:!0});try{const a=e.state.get?.("_global")?.guestOrderToken,n=e.dataSource?.get?.("order")?.data?.order_number,s=!!(a&&n),i=s?`/api/modules/sirsoft-ecommerce/guest/orders/${n}/options/${o}/confirm`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/options/${o}/confirm`,l=s?{headers:{"X-Guest-Order-Token":a}}:void 0,u=await e.api.post(i,void 0,l);if(u?.success)e.state.setLocal({isConfirming:!1,confirmTarget:null}),e.modal?.close?.("modal_confirm_purchase"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(u.message??e.t?.("sirsoft-ecommerce::messages.order.confirmed")??"구매확정이 완료되었습니다.");else throw new Error(u?.message||"Confirm failed")}catch(a){ft.error("[confirmOrderOption] 실패:",a);const n=a?.response?.data?.message||a?.data?.message||a?.message||e.t?.("sirsoft-ecommerce::messages.order.cannot_confirm")||"구매확정에 실패했습니다.";e.state.setLocal({isConfirming:!1}),e.toast?.error?.(n)}}const gt=window.G7Core?.createLogger?.("Ecom:ChangeShippingAddress")??{log:(...r)=>console.log("[Ecom:ChangeShippingAddress]",...r),warn:(...r)=>console.warn("[Ecom:ChangeShippingAddress]",...r),error:(...r)=>console.error("[Ecom:ChangeShippingAddress]",...r)};function Gn(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}async function En(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{},o=Gn(e);if(!t&&!o){gt.error("[changeShippingAddress] orderId 또는 비회원 식별자 누락");return}const a=e.state.get?.("_global")??{},s=(o?"manual":a.changeAddressMode??"saved")==="saved"?{address_id:a.selectedAddressId}:a.editingShippingAddress;e.state.setLocal({isSubmittingAddress:!0});try{const i=a.guestOrderToken,l=e.dataSource?.get?.("order")?.data?.order_number,u=o?`/api/modules/sirsoft-ecommerce/guest/orders/${l}/shipping-address`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/shipping-address`,d=o?{headers:{"X-Guest-Order-Token":i}}:void 0,m=await e.api.put(u,s,d);if(m?.success)e.state.setLocal({isSubmittingAddress:!1}),e.modal?.close?.("changeAddressModal"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(m.message??e.t?.("mypage.order_detail.shipping_address_changed")??"배송지가 변경되었습니다.");else throw new Error(m?.message||"Change shipping address failed")}catch(i){gt.error("[changeShippingAddress] 실패:",i),e.state.setLocal({isSubmittingAddress:!1});const l=i?.response?.data?.message||i?.data?.message||i?.message||"배송지 변경에 실패했습니다.";e.toast?.error?.(l)}}const je=window.G7Core?.createLogger?.("Ecom:Review")??{log:(...r)=>console.log("[Ecom:Review]",...r),warn:(...r)=>console.warn("[Ecom:Review]",...r),error:(...r)=>console.error("[Ecom:Review]",...r)};async function Fn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,optionId:o,productId:a}=r.params||{};if(!t||!o||!a){je.error("[submitReview] orderId, optionId 또는 productId 누락");return}const n=e.state.getLocal(),s=r.params?.rating??n.reviewRating??5,i=(r.params?.content??n.reviewContent??"").trim(),u=(r.params?.images??n.reviewImages??[]).map(d=>d instanceof File?d:d?.file instanceof File||d?.file instanceof Blob||d?.file&&typeof d.file.name=="string"&&d.file.size>0?d.file:d instanceof Blob?d:null).filter(d=>d!==null);if(!s||s<1||s>5){e.toast?.error?.(e.t?.("sirsoft-ecommerce.user.review.validation.rating_required")??"별점을 선택해 주세요.");return}if(i.length<10){e.state.setLocal({reviewError:e.t?.("sirsoft-ecommerce.user.review.validation.content_min")??"리뷰 내용을 최소 10자 이상 입력해 주세요."});return}e.state.setLocal({isSubmittingReview:!0,reviewError:null});try{const d=await e.api.post("/api/modules/sirsoft-ecommerce/user/reviews",{order_option_id:o,product_id:a,rating:s,content:i});if(!d?.success)throw new Error(d?.message||"Review creation failed");const m=d.data?.id;if(m&&u.length>0)for(let p=0;p<u.length;p++){const g=new FormData;g.append("image",u[p]),g.append("sort_order",String(p));try{await e.api.post(`/api/modules/sirsoft-ecommerce/user/reviews/${m}/images`,g,{headers:{"Content-Type":"multipart/form-data"}})}catch(w){je.warn(`[submitReview] 이미지 ${p+1} 업로드 실패:`,w),e.toast?.warning?.(e.t?.("sirsoft-ecommerce.user.review.image_upload_partial_fail")??`이미지 ${p+1}번 업로드에 실패했습니다.`)}}e.state.setLocal({isSubmittingReview:!1,reviewTarget:null,reviewRating:5,reviewContent:"",reviewImages:[],reviewError:null}),e.modal?.close?.("modal_write_review"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(d.message??e.t?.("sirsoft-ecommerce.user.review.submit_success")??"리뷰가 등록되었습니다.")}catch(d){je.error("[submitReview] 실패:",d);const m=d?.response?.data||d?.data,p=d?.response?.status||d?.status,g=m?.message||d?.message||e.t?.("sirsoft-ecommerce.user.review.submit_failed")||"리뷰 등록에 실패했습니다.";if(p===422){const w=m?.errors??{},y=Object.values(w).flat()[0];e.state.setLocal({isSubmittingReview:!1,reviewError:y||g})}else e.state.setLocal({isSubmittingReview:!1});e.toast?.error?.(g)}}const I=window.G7Core?.createLogger?.("Ecom:ShippingPolicyForm")??{log:(...r)=>console.log("[Ecom:ShippingPolicyForm]",...r),warn:(...r)=>console.warn("[Ecom:ShippingPolicyForm]",...r),error:(...r)=>console.error("[Ecom:ShippingPolicyForm]",...r)},Dn=["fixed","conditional_free","per_quantity","per_weight","per_volume","per_volume_weight","per_amount"],$n=["conditional_free"],Nn=["range_amount","range_quantity","range_weight","range_volume","range_volume_weight"],Tn=["api"],Rn=["per_quantity","per_weight","per_volume","per_volume_weight","per_amount"],Mn={shipping_method:"parcel",custom_shipping_name:null,carrier:null,currency_code:"KRW",charge_policy:"fixed",base_fee:0,free_threshold:null,ranges:null,api_endpoint:null,api_request_fields:null,api_response_fee_field:null,api_config:null,extra_fee_enabled:!1,extra_fee_settings:[],extra_fee_multiply:!1,is_active:!0};function te(r){return{showBaseFee:Dn.includes(r),showFreeThreshold:$n.includes(r),showRanges:Nn.includes(r),showApiSettings:Tn.includes(r),showUnitValue:Rn.includes(r)}}function G(){const r=window.G7Core;return r?.state?r:(I.warn("G7Core.state not available"),null)}function E(r,c){r.setLocalState?r.setLocalState(c):G()?.state?.setLocal(c)}function F(r){return[...(r.state.getLocal?.()??{}).form?.country_settings??[]]}function Hn(r,c){const e=r.params||{},t=e.policy,o=!!e.isEdit,a=!!e.isCopy;if(e.availableCountries,!G())return;const s={activeCountryTab:0,rangeErrors:{}};if((o||a)&&t){const i=t.country_settings??[],u=i[0]?.charge_policy??"fixed",d=te(u);Object.assign(s,d),I.log("[initShippingPolicyForm]",o?"Edit":"Copy","mode, countries:",i.length,"first charge_policy:",u)}else{const i=te("fixed");Object.assign(s,i),I.log("[initShippingPolicyForm] Create mode, default flags")}E(c,s)}function qn(r,c){const t=(r.params||{}).country_code??"";if(!t){I.warn("[addCountrySetting] country_code is empty");return}const o=G();if(!o)return;const a=F(o);if(a.some(u=>u.country_code===t)){I.warn("[addCountrySetting] Duplicate country_code:",t);return}const n={country_code:t,...Mn};a.push(n);const s=a.length-1,i=te(n.charge_policy),l={"form.country_settings":a,activeCountryTab:s,...i};E(c,l),I.log("[addCountrySetting] Added country:",t,"index:",s)}function Un(r,c){const e=r.params||{},t=Number(e.index);if(isNaN(t)||t<0){I.warn("[removeCountrySetting] Invalid index:",e.index);return}const o=G();if(!o)return;const a=F(o),n=o.state.getLocal?.()??{};if(t>=a.length){I.warn("[removeCountrySetting] Index out of bounds:",t);return}const s=a[t].country_code;a.splice(t,1);const i=n.activeCountryTab??0;let l=i;a.length===0?l=0:i>=a.length?l=a.length-1:i>t&&(l=i-1);const u={...n.rangeErrors??{}};u[s]=[];const d=a[l],m=te(d?d.charge_policy:"fixed"),p={"form.country_settings":a,activeCountryTab:l,rangeErrors:u,...m};E(c,p),I.log("[removeCountrySetting] Removed index:",t,"country:",s,"newTab:",l)}function Vn(r,c){const e=r.params||{},t=Number(e.index);if(isNaN(t)||t<0){I.warn("[switchCountryTab] Invalid index:",e.index);return}const o=G();if(!o)return;const a=F(o);if(t>=a.length){I.warn("[switchCountryTab] Index out of bounds:",t);return}const n=a[t],s=te(n.charge_policy),i={activeCountryTab:t,...s};E(c,i),I.log("[switchCountryTab] Switched to tab:",t,"country:",n.country_code,"charge_policy:",n.charge_policy)}function jn(r,c){const e=r.params||{},t=e.field??"",o=e.value;if(!t){I.warn("[updateCountryField] field is empty");return}const a=G();if(!a)return;const n=a.state.getLocal?.()??{},s=Number(n.activeCountryTab??0),i=F(a);if(s>=i.length){I.warn("[updateCountryField] Index out of bounds:",s);return}i[s]={...i[s],[t]:o},E(c,{"form.country_settings":i}),I.log("[updateCountryField] Updated",t,"=",o,"at index:",s)}function Bn(r,c){const e=r.params||{},t=e.value??"",o=Number(e.index??0);if(!t){I.warn("[onChargePolicyChange] charge_policy value is empty");return}const a=G();if(!a)return;const n=te(t);I.log("[onChargePolicyChange]",t,n,"countryIndex:",o);const s=F(a),i=s[o];if(!i){I.warn("[onChargePolicyChange] No country setting at index:",o);return}const l={...i,charge_policy:t};if(n.showBaseFee||(l.base_fee=0),n.showFreeThreshold||(l.free_threshold=null),!n.showRanges&&!n.showUnitValue&&(l.ranges=null),n.showApiSettings||(l.api_endpoint=null,l.api_request_fields=null,l.api_response_fee_field=null,l.api_config=null),n.showRanges&&(!l.ranges||!l.ranges.tiers)){const d=t.replace("range_","");l.ranges={type:d,tiers:[{min:0,max:null,fee:0}]}}n.showUnitValue&&(!l.ranges||!l.ranges.unit_value)&&(l.ranges={type:t,unit_value:1}),s[o]=l;const u={"form.country_settings":s,...n};E(c,u)}function Kn(r,c){const e=r.params||{},t=Number(e.index??0),o=G();if(!o)return;const a=F(o),n=a[t];if(!n){I.warn("[addRangeTier] No country setting at index:",t);return}const s=[...n.ranges?.tiers??[]],i=s[s.length-1],u={min:i?.max!=null?i.max+1:0,max:null,fee:0};s.push(u),a[t]={...n,ranges:{...n.ranges??{},tiers:s}},E(c,{"form.country_settings":a}),xe(o,c,t,s,n.country_code),I.log("[addRangeTier] Added tier, total:",s.length)}function Qn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.tierIndex);if(isNaN(o)||o<0){I.warn("[removeRangeTier] Invalid tierIndex:",e.tierIndex);return}const a=G();if(!a)return;const n=F(a),s=n[t];if(!s){I.warn("[removeRangeTier] No country setting at index:",t);return}const i=[...s.ranges?.tiers??[]];if(o>=i.length){I.warn("[removeRangeTier] tierIndex out of bounds:",o);return}i.splice(o,1),n[t]={...s,ranges:{...s.ranges??{},tiers:i}},E(c,{"form.country_settings":n}),xe(a,c,t,i,s.country_code),I.log("[removeRangeTier] Removed tier:",o,"remaining:",i.length)}function zn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.tierIndex),a=e.field,n=e.value;if(isNaN(o)||o<0){I.warn("[updateRangeTierField] Invalid tierIndex:",e.tierIndex);return}if(!["min","max","fee"].includes(a)){I.warn("[updateRangeTierField] Invalid field:",a);return}const s=G();if(!s)return;const i=F(s),l=i[t];if(!l){I.warn("[updateRangeTierField] No country setting at index:",t);return}const u=[...l.ranges?.tiers??[]];if(o>=u.length){I.warn("[updateRangeTierField] tierIndex out of bounds:",o);return}let d;a==="max"?d=n===""||n===null||n===void 0?null:Number(n):d=Number(n),u[o]={...u[o],[a]:d},i[t]={...l,ranges:{...l.ranges??{},tiers:u}},E(c,{"form.country_settings":i}),xe(s,c,t,u,l.country_code)}function Yn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=G();if(!o)return;const n=F(o)[t];if(!n)return;const s=n.ranges?.tiers??[];xe(o,c,t,s,n.country_code)}function xe(r,c,e,t,o){const n={...(r.state.getLocal?.()??{}).rangeErrors??{}};if(t.length===0){n[o]=[],E(c,{rangeErrors:n});return}const s=new Array(t.length).fill(null).map(()=>({}));let i=!1;const l=r.t;for(let u=0;u<t.length;u++){const d=t[u];if(u===0&&d.min!==0&&(s[u].min=l?.("sirsoft-ecommerce.validation.shipping_policy.ranges.first_min_zero")??"첫 구간의 시작값은 0이어야 합니다.",i=!0),u===t.length-1&&d.max!==null&&d.max!==void 0&&(s[u].max=l?.("sirsoft-ecommerce.validation.shipping_policy.ranges.last_max_unlimited")??"마지막 구간의 종료값은 비워야 합니다.",i=!0),u<t.length-1&&d.max!==null&&d.max!==void 0&&d.min>=d.max&&(s[u].min=l?.("sirsoft-ecommerce.validation.shipping_policy.ranges.min_less_than_max")??"시작값이 종료값보다 작아야 합니다.",i=!0),u<t.length-1){const m=t[u+1];d.max!==null&&d.max!==void 0&&d.max+1!==m.min&&(s[u].max=l?.("sirsoft-ecommerce.validation.shipping_policy.ranges.continuity")??"구간이 연속적이지 않습니다.",i=!0)}d.fee<0&&(s[u].fee=l?.("sirsoft-ecommerce.validation.shipping_policy.ranges.fee_non_negative")??"배송비는 0 이상이어야 합니다.",i=!0)}n[o]=i?s:[],E(c,{rangeErrors:n})}function Wn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=G();if(!o)return;const a=F(o),n=a[t];if(!n){I.warn("[addExtraFeeRow] No country setting at index:",t);return}if(n.country_code!=="KR"){I.warn("[addExtraFeeRow] Extra fee only available for KR, current:",n.country_code);return}const s=[...n.extra_fee_settings??[]];s.push({zipcode:"",fee:0,region:""}),a[t]={...n,extra_fee_settings:s},E(c,{"form.country_settings":a}),I.log("[addExtraFeeRow] Added row, total:",s.length)}function Jn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.feeIndex);if(isNaN(o)||o<0){I.warn("[removeExtraFeeRow] Invalid feeIndex:",e.feeIndex);return}const a=G();if(!a)return;const n=F(a),s=n[t];if(!s){I.warn("[removeExtraFeeRow] No country setting at index:",t);return}const i=[...s.extra_fee_settings??[]];if(o>=i.length){I.warn("[removeExtraFeeRow] feeIndex out of bounds:",o);return}i.splice(o,1),n[t]={...s,extra_fee_settings:i},E(c,{"form.country_settings":n}),I.log("[removeExtraFeeRow] Removed index:",o,"remaining:",i.length)}function Xn(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=e.settings;if(!o||!Array.isArray(o)){I.warn("[applyExtraFeeTemplate] Invalid settings:",o);return}const a=G();if(!a)return;const n=F(a),s=n[t];if(!s){I.warn("[applyExtraFeeTemplate] No country setting at index:",t);return}n[t]={...s,extra_fee_settings:o},E(c,{"form.country_settings":n});const i=a.t,l=i?.("sirsoft-ecommerce.admin.shipping_policy.form.template_applied")??"템플릿이 적용되었습니다.";a.toast?.success?.(l),I.log("[applyExtraFeeTemplate] Applied template with",o.length,"rows")}function Zn(r,c){const e=r.params||{},t=parseFloat(e.value)||1,o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=F(o),i=s[n];i&&(s[n]={...i,ranges:{...i.ranges??{},type:i.charge_policy,unit_value:t}},E(c,{"form.country_settings":s}),I.log("[updateUnitValue] Updated unit_value:",t))}function ea(r,c){const e=G();if(!e)return;const t=e.state.getLocal?.()??{},o=Number(t.activeCountryTab??0),a=F(e),n=a[o];if(!n)return;const s=[...n.api_request_fields??[]];s.push(""),a[o]={...n,api_request_fields:s},E(c,{"form.country_settings":a}),I.log("[addApiRequestField] Added field, total:",s.length)}function ta(r,c){const e=r.params||{},t=Number(e.fieldIndex),o=e.value??"";if(isNaN(t)||t<0){I.warn("[updateApiRequestField] Invalid fieldIndex:",e.fieldIndex);return}const a=G();if(!a)return;const n=a.state.getLocal?.()??{},s=Number(n.activeCountryTab??0),i=F(a),l=i[s];if(!l)return;const u=[...l.api_request_fields??[]];t>=u.length||(u[t]=o,i[s]={...l,api_request_fields:u},E(c,{"form.country_settings":i}),I.log("[updateApiRequestField] Updated index:",t,"value:",o))}function oa(r,c){const e=r.params||{},t=Number(e.fieldIndex);if(isNaN(t)||t<0){I.warn("[removeApiRequestField] Invalid fieldIndex:",e.fieldIndex);return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=F(o),i=s[n];if(!i)return;const l=[...i.api_request_fields??[]];t>=l.length||(l.splice(t,1),s[n]={...i,api_request_fields:l},E(c,{"form.country_settings":s}),I.log("[removeApiRequestField] Removed index:",t,"remaining:",l.length))}function ra(r,c){const t=(r.params||{}).field??"";if(!t){I.warn("[toggleApiRequestField] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=F(o),i=s[n];if(!i)return;const l=[...i.api_request_fields??[]],u=l.indexOf(t);u>=0?l.splice(u,1):l.push(t);const d=l.length>0?l:null;s[n]={...i,api_request_fields:d},E(c,{"form.country_settings":s}),I.log("[toggleApiRequestField] Toggled:",t,"selected:",d)}function na(r,c){const e=r.params||{},t=e.field??"";if(!t){I.warn("[updateApiConfigField] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=F(o),i=s[n];if(!i)return;const l={...i.api_config??{},[t]:e.value};s[n]={...i,api_config:l},E(c,{"form.country_settings":s}),I.log("[updateApiConfigField] Updated",t,"at index:",n)}function aa(r,c){const e=r.params||{},t=e.field??"";if(!t){I.warn("[updateApiFieldMap] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=F(o),i=s[n];if(!i)return;const l=e.value??"",u={...i.api_config?.field_map??{}};l.trim()===""?delete u[t]:u[t]=l;const d={...i.api_config??{},field_map:Object.keys(u).length>0?u:null};s[n]={...i,api_config:d},E(c,{"form.country_settings":s}),I.log("[updateApiFieldMap] Mapped",t,"→",l||"(removed)")}function sa(r,c){const e=G();if(!e)return;const t=e.state.getLocal?.()??{},o=Number(t.activeCountryTab??0),n=F(e)[o];if(n){if(!n.api_endpoint){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.shipping_policy.form.api_test_endpoint_required")??"Endpoint required");return}e.state.set?.({apiTestLoading:!0,apiTestResult:null}),e.dispatch({handler:"apiCall",target:"/api/modules/sirsoft-ecommerce/admin/shipping-policies/test-api-call",auth_required:!0,params:{method:"POST",body:{endpoint:n.api_endpoint,request_fields:n.api_request_fields,config:n.api_config??{},sample:{country_code:n.country_code}}},onSuccess:{handler:"setState",params:{target:"global",apiTestLoading:!1,apiTestResult:"{{response.data}}"}},onError:{handler:"setState",params:{target:"global",apiTestLoading:!1,apiTestResult:"{{error.errors ?? error}}"}}})}}function ia(r,c){const e=r.params||{},t=Number(e.feeIndex),o=e.field,a=e.value;if(isNaN(t)||t<0){I.warn("[updateExtraFeeField] Invalid feeIndex:",e.feeIndex);return}if(!["zipcode","fee","region"].includes(o)){I.warn("[updateExtraFeeField] Invalid field:",o);return}const n=G();if(!n)return;const s=n.state.getLocal?.()??{},i=Number(s.activeCountryTab??0),l=F(n),u=l[i];if(!u)return;const d=[...u.extra_fee_settings??[]];if(t>=d.length)return;const m=o==="fee"?parseFloat(a)||0:a;d[t]={...d[t],[o]:m},l[i]={...u,extra_fee_settings:d},E(c,{"form.country_settings":l}),I.log("[updateExtraFeeField] Updated index:",t,o,"=",m)}const fe=window.G7Core?.createLogger?.("Handler:InitCategory")??{log:(...r)=>console.log("[Handler:InitCategory]",...r),warn:(...r)=>console.warn("[Handler:InitCategory]",...r),error:(...r)=>console.error("[Handler:InitCategory]",...r)};function _t(r,c){for(const e of r){if(e.id===c)return e;if(e.children&&e.children.length>0){const t=_t(e.children,c);if(t)return t}}return null}function wt(r){return new URLSearchParams(window.location.search).get(r)}function ca(r){return window.G7Core?.state?.getDataSource?.(r)}async function la(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=ca(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function da(r,c){const e=window.G7Core,t=wt("id"),o=wt("mode");if(!t){fe.log("[initCategoryFromUrl] No id parameter in URL");return}if(!e?.state?.set){fe.warn("[initCategoryFromUrl] G7Core.state.set not available");return}const a=await la("categories",l=>l?.data?.data);if(!a){fe.warn("[initCategoryFromUrl] categories data not available after waiting");return}const n=parseInt(t,10),s=_t(a,n);if(!s){fe.warn("[initCategoryFromUrl] Category not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedCategoryId:s.id,selectedCategory:s,panelMode:i}),fe.log("[initCategoryFromUrl] Category initialized from URL:",{id:n,mode:i})}const ge=window.G7Core?.createLogger?.("Handler:InitBrand")??{log:(...r)=>console.log("[Handler:InitBrand]",...r),warn:(...r)=>console.warn("[Handler:InitBrand]",...r),error:(...r)=>console.error("[Handler:InitBrand]",...r)};function yt(r){return new URLSearchParams(window.location.search).get(r)}function ua(r){return window.G7Core?.state?.getDataSource?.(r)}async function ma(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=ua(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function pa(r,c){const e=window.G7Core,t=yt("id"),o=yt("mode");if(!t){ge.log("[initBrandFromUrl] No id parameter in URL");return}if(!e?.state?.set){ge.warn("[initBrandFromUrl] G7Core.state.set not available");return}const a=await ma("brands",l=>l?.data?.data);if(!a){ge.warn("[initBrandFromUrl] brands data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){ge.warn("[initBrandFromUrl] Brand not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedBrandId:s.id,selectedBrand:s,panelMode:i}),ge.log("[initBrandFromUrl] Brand initialized from URL:",{id:n,mode:i})}const _e=window.G7Core?.createLogger?.("Handler:InitCommonInfo")??{log:(...r)=>console.log("[Handler:InitCommonInfo]",...r),warn:(...r)=>console.warn("[Handler:InitCommonInfo]",...r),error:(...r)=>console.error("[Handler:InitCommonInfo]",...r)};function ht(r){return new URLSearchParams(window.location.search).get(r)}function fa(r){return window.G7Core?.state?.getDataSource?.(r)}async function ga(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=fa(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function _a(r,c){const e=window.G7Core,t=ht("id"),o=ht("mode");if(!t){_e.log("[initCommonInfoFromUrl] No id parameter in URL");return}if(!e?.state?.set){_e.warn("[initCommonInfoFromUrl] G7Core.state.set not available");return}const a=await ga("commonInfos",l=>l?.data);if(!a){_e.warn("[initCommonInfoFromUrl] commonInfos data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){_e.warn("[initCommonInfoFromUrl] Common info not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedId:s.id,selectedItem:s,panelMode:i}),_e.log("[initCommonInfoFromUrl] Common info initialized from URL:",{id:n,mode:i})}const we=window.G7Core?.createLogger?.("Handler:InitNotice")??{log:(...r)=>console.log("[Handler:InitNotice]",...r),warn:(...r)=>console.warn("[Handler:InitNotice]",...r),error:(...r)=>console.error("[Handler:InitNotice]",...r)};function Ct(r){return new URLSearchParams(window.location.search).get(r)}function wa(r){return window.G7Core?.state?.getDataSource?.(r)}async function ya(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=wa(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function ha(r,c){const e=window.G7Core,t=Ct("id"),o=Ct("mode");if(!t){we.log("[initNoticeFromUrl] No id parameter in URL");return}if(!e?.state?.set){we.warn("[initNoticeFromUrl] G7Core.state.set not available");return}const a=await ya("templates",l=>l?.data);if(!a){we.warn("[initNoticeFromUrl] templates data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){we.warn("[initNoticeFromUrl] Template not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedTemplateId:s.id,selectedTemplate:s,panelMode:i}),we.log("[initNoticeFromUrl] Template initialized from URL:",{id:n,mode:i})}const oe={updateProductField:Gt,updateOptionField:Et,calculateCurrencyPrices:se,initPreferredCurrency:Mt,initPreferredShippingCountry:Vt,setDateRange:Bt,setDefaultOption:Kt,toggleOption:Qt,toggleProductOptions:Xe,toggleAllOptionsInRow:zt,getProductOptionStates:Yt,syncProductSelection:Wt,loadExpandedOptions:De,retryExpandedOptions:ro,generateCopyProductCode:Jt,copyProduct:Xt,selectCategory:ao,selectCategoryMobile:so,addCategoryToSelection:io,removeCategoryFromSelection:co,getCategoryBreadcrumb:lo,validateCategoryPath:mo,initCategoryInfosFromProduct:uo,getBrandName:po,getBrandDescription:fo,updatePrice:yo,calculateTotalOptionStock:ho,validatePriceRelation:Co,addOptionInput:Fo,removeOptionInput:Do,updateOptionInput:$o,generateOptions:No,deleteOption:To,applyOptionAddTool:Ro,addRequiredItem:Mo,updateRequiredItem:Ho,removeRequiredItem:qo,reorderRequiredItems:Uo,addAdditionalOption:Vo,updateAdditionalOption:jo,removeAdditionalOption:Bo,reorderAdditionalOptions:Ko,clearAdditionalOptions:Qo,addAdditionalOptionValue:zo,updateAdditionalOptionValue:Yo,removeAdditionalOptionValue:Wo,uploadImages:er,setThumbnail:tr,reorderImages:or,updateDescription:rr,confirmSelectNoticeTemplate:nr,selectNoticeTemplate:st,updateNoticeItem:ar,removeNoticeItem:sr,reorderNoticeItems:ir,fillNoticeWithValue:cr,switchNoticeMode:lr,updateNewTemplateName:dr,addNoticeItem:ur,updateNoticeItemName:mr,saveAsNoticeTemplate:pr,confirmSaveNoticeTemplate:_r,fillTemplateFieldsWithDetailReference:fr,fillNoticeItemsWithDetailReference:gr,toggleLabel:wr,generateProductCode:yr,getShippingPolicyInfo:hr,getCommonInfoContent:Cr,updateShoppingIntegration:br,updateShippingType:Ir,updateIdentificationCode:Sr,openLabelPeriodModal:vr,saveLabelPeriod:Lr,removeLabelPeriod:xr,updateActivityLogSort:Pr,updateActivityLogPerPage:Ar,setDefaultShippingPolicy:kr,setLabelDatePreset:Gr,toggleDefaultShippingPolicy:Or,toggleLabelAssignment:Er,saveLabelSettings:Tr,deleteLabel:Nr,updateLabelPeriodInline:Fr,setLabelDatePresetInline:Dr,confirmUncheckLabel:$r,removeDescriptionLocale:Rr,showAddLocaleModal:Mr,addDescriptionLocale:Hr,setDefaultOptionFromGrid:vo,addOptionRow:xo,updateFormOptionField:Lo,recalculateOptionPriceAdjustments:Po,bulkUpdate:qr,buildConfirmData:jr,buildOrderColumns:Br,toggleArrayValue:Kr,toggleVisibleFilter:Qr,syncOrderSelection:zr,handleOrderRowAction:Yr,processOrderBulkAction:Wr,buildOrderBulkConfirmData:tn,executeOrderBulkAction:on,downloadOrderExcel:Jr,saveVisibleColumns:Xr,loadVisibleColumns:Zr,loadVisibleFilters:en,handleProductRowAction:rn,initOrderDetailForm:nn,toggleProductSelection:an,toggleAllProducts:sn,buildOrderDetailBulkConfirmData:cn,processOrderDetailBulkChange:ln,saveAdminMemo:dn,updateChangeQuantity:un,openConfirmDepositModal:mn,confirmDeposit:pn,initShippingPolicyForm:Hn,addCountrySetting:qn,removeCountrySetting:Un,switchCountryTab:Vn,updateCountryField:jn,onChargePolicyChange:Bn,addRangeTier:Kn,removeRangeTier:Qn,updateRangeTierField:zn,validateRangeTiers:Yn,addExtraFeeRow:Wn,removeExtraFeeRow:Jn,applyExtraFeeTemplate:Xn,updateUnitValue:Zn,addApiRequestField:ea,updateApiRequestField:ta,removeApiRequestField:oa,toggleApiRequestField:ra,updateApiConfigField:na,updateApiFieldMap:aa,testShippingApi:sa,updateExtraFeeField:ia,updateCancelQuantity:gn,estimateRefundAmount:_n,changeRefundPriority:wn,executeCancelOrder:yn,clearCancelOrderTimers:fn,toggleItemSelection:Cn,toggleSelectAllItems:bn,initUserCancelItems:In,toggleUserCancelItem:Sn,toggleUserCancelSelectAll:vn,updateUserCancelQuantity:Ln,estimateUserRefund:xn,changeUserRefundPriority:Pn,executeUserCancelOrder:An,clearUserCancelOrderTimers:hn,confirmOrderOption:On,changeShippingAddress:En,submitReview:Fn,initCategoryFromUrl:da,initBrandFromUrl:pa,initCommonInfoFromUrl:_a,initNoticeFromUrl:ha},V="sirsoft-ecommerce",ye=window.G7Core?.createLogger?.(`Module:${V}`)??{log:(...r)=>console.log(`[Module:${V}]`,...r),warn:(...r)=>console.warn(`[Module:${V}]`,...r),error:(...r)=>console.error(`[Module:${V}]`,...r)};function bt(r=!1){const c=window.G7Core?.getActionDispatcher?.();if(c)Object.entries(oe).forEach(([e,t])=>{const o=`${V}.${e}`;c.registerHandler(o,t,{category:"module",source:V})}),ye.log(`${Object.keys(oe).length} handler(s) registered:`,Object.keys(oe).map(e=>`${V}.${e}`));else if(r){let e=0;const t=50,o=()=>{const a=window.G7Core?.getActionDispatcher?.();a?(Object.entries(oe).forEach(([n,s])=>{const i=`${V}.${n}`;a.registerHandler(i,s,{category:"module",source:V})}),ye.log(`${Object.keys(oe).length} handler(s) registered:`,Object.keys(oe).map(n=>`${V}.${n}`))):(e++,e<=t?(ye.warn(`ActionDispatcher not found, retrying... (${e}/${t})`),setTimeout(o,100)):ye.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};o()}else ye.warn("ActionDispatcher not found, handlers not registered")}function Be(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>bt(!0));else{const r=!!window.G7Core?.getActionDispatcher?.();bt(!r)}}return Be(),typeof window<"u"&&(window.__SirsoftEcommerce={identifier:V,handlers:Object.keys(oe),initModule:Be}),Ae.initModule=Be,Object.defineProperty(Ae,Symbol.toStringTag,{value:"Module"}),Ae}({}); -//# sourceMappingURL=module.iife.js.map +var SirsoftEcommerce=function(ke){"use strict";const Et={KRW:0,JPY:0},Dt={KRW:"₩",USD:"$",JPY:"¥",CNY:"元",EUR:"€",GBP:"£"},Ft={KRW:1e3,JPY:100};function $t(r){if(!r)return 1;const c=r.base_unit??Ft[r.code]??1;return Math.max(1,c)}function Nt(r,c,e){const t=parseFloat(c)||1;if(t<=0)return r;const o=r/t;let a;switch(e){case"ceil":a=Math.ceil(o);break;case"floor":a=Math.floor(o);break;default:a=Math.round(o)}return a*t}function de(r,c){const e=typeof c=="string"?{code:c}:c,t=e.code,o=e.symbol&&e.symbol.length>0?e.symbol:Dt[t]??"",a=e.decimal_places??Et[t]??2,n=r.toLocaleString(void 0,{minimumFractionDigits:a,maximumFractionDigits:a});return o?t==="KRW"||o==="₩"||o==="원"?n+"원":o+n:n+" "+t}function Tt(){try{const e=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(e?.currencies)?e.currencies:[]}catch{return[]}}function ue(r,c){const e=Tt(),t=c||e.find(o=>o.is_default)?.code;return t?de(r,e.find(o=>o.code===t)??t):r.toLocaleString()}function se(r,c){const{basePrice:e,currencies:t}=r,o=parseFloat(String(e))||0;if(!t||!Array.isArray(t)||t.length===0)return{};const a=t.find(i=>i.is_default),n=$t(a),s={};for(const i of t){const l=i.code;if(i.is_default??!1)s[l]={price:o,formatted:de(o,i),is_default:!0,editable:!0};else if(i.exchange_rate&&i.exchange_rate>0){const d=o/n*i.exchange_rate,m=Nt(d,i.rounding_unit||"0.01",i.rounding_method||"round");s[l]={price:m,formatted:de(m,i),is_default:!1,editable:!1,exchange_rate:i.exchange_rate}}}return s}const z=window.G7Core?.createLogger?.("Ecom:UpdateProduct")??{log:(...r)=>console.log("[Ecom:UpdateProduct]",...r),warn:(...r)=>console.warn("[Ecom:UpdateProduct]",...r),error:(...r)=>console.error("[Ecom:UpdateProduct]",...r)};function Rt(r,c){const e=r.params||{},{productId:t,field:o,dataSourceId:a="products"}=e;let{value:n}=e;if(n&&typeof n=="object"&&n.target!==void 0&&(n=n.target?.value,z.log("[updateProductField] Extracted value from event:",n)),!t||!o){z.warn("[updateProductField] Missing required params:",{productId:t,field:o});return}if(n===void 0){z.warn("[updateProductField] Value is undefined, skipping update");return}const s=window.G7Core;if(!s?.dataSource?.get||!s?.dataSource?.set){z.warn("[updateProductField] G7Core.dataSource API is not available");return}const i=s.dataSource.get(a);if(!i){console.warn(`[updateProductField] DataSource '${a}' not found`);return}const l=i.data?.data||[];if(!Array.isArray(l)||l.length===0){z.warn("[updateProductField] Products data is empty or invalid");return}const u=s.state.get()||{},d=u.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies;let p=[...u._local?.productFieldErrors||[]];const f=l.map(C=>{if(String(C.id)===String(t)){const x=C[o];if(typeof n=="object"&&n!==null?JSON.stringify(x)===JSON.stringify(n):String(x)===String(n))return z.log(`[updateProductField] No change for product ${t}.${o}, skipping`),C;let S=n;const N=parseFloat(n)||0;if(o==="selling_price"){const O=parseFloat(C.list_price)||0;O>0&&N>O?(p.some(Q=>Q.productId===String(t)&&Q.field===o)||p.push({productId:String(t),field:"selling_price",message:"판매가는 정가를 초과할 수 없습니다."}),S=O):p=p.filter($=>!($.productId===String(t)&&$.field===o))}else p=p.filter(O=>!(O.productId===String(t)&&O.field===o));const L={...C,[o]:S,_modified:!0};return o==="selling_price"&&d&&Array.isArray(d)&&(L.multi_currency_selling_price=se({basePrice:S,currencies:d}),z.log(`[updateProductField] Recalculated multi_currency_selling_price for product ${t}`)),L}return C});s.dataSource.set(a,{...i,data:{...i.data,data:f}});const y=s.state.getLocal()||{},w=new Set(y.modifiedProductIds||[]);w.add(t);const _={...y.modifiedProductFields||{}},g=String(t),b=new Set(_[g]||[]);b.add(o),_[g]=Array.from(b),s.state.setLocal({modifiedProductIds:Array.from(w),modifiedProductFields:_,productFieldErrors:p}),z.log(`[updateProductField] Updated product ${t}.${o} =`,n)}const B=window.G7Core?.createLogger?.("Ecom:UpdateOption")??{log:(...r)=>console.log("[Ecom:UpdateOption]",...r),warn:(...r)=>console.warn("[Ecom:UpdateOption]",...r),error:(...r)=>console.error("[Ecom:UpdateOption]",...r)};function Mt(r,c){const e=r.params||{},{productId:t,optionId:o,field:a,value:n,dataSourceId:s="products"}=e;if(!t||!o||!a){B.warn("[updateOptionField] Missing required params:",{productId:t,optionId:o,field:a});return}const i=window.G7Core;if(!i?.dataSource?.get||!i?.dataSource?.set){B.warn("[updateOptionField] G7Core.dataSource API is not available");return}const l=i.dataSource.get(s);if(!l){console.warn(`[updateOptionField] DataSource '${s}' not found`);return}const u=l.data?.data||[];if(!Array.isArray(u)||u.length===0){B.warn("[updateOptionField] Products data is empty or invalid");return}const d=i.state.get()||{},m=d.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies;let f=[...d._local?.optionFieldErrors||[]],y=!1,w=!1;const _=u.map(N=>{if(String(N.id)===String(t)){const L=N.options||[],O=L.findIndex(F=>String(F.id)===String(o));if(O===-1)return B.warn("[updateOptionField] Option not found:",o),N;w=!0;const $=L[O],Q=$[a];if(typeof n=="object"&&n!==null?JSON.stringify(Q)===JSON.stringify(n):String(Q)===String(n))return B.log(`[updateOptionField] No change for option ${o}.${a}, skipping`),N;let J=n;const Ae=parseFloat(n)||0;if(a==="selling_price"){const F=parseFloat($.list_price)||0;F>0&&Ae>F?(f.some(he=>he.productId===String(t)&&he.optionId===String(o)&&he.field===a)||f.push({productId:String(t),optionId:String(o),field:"selling_price",message:"판매가는 정가를 초과할 수 없습니다."}),J=F):f=f.filter(H=>!(H.productId===String(t)&&H.optionId===String(o)&&H.field===a))}else f=f.filter(F=>!(F.productId===String(t)&&F.optionId===String(o)&&F.field===a));const re=[...L],ne={...re[O],[a]:J,_modified:!0};let ae=null;if(a==="selling_price"){const F=parseFloat(J)||0;if($.is_default===!0)ae=F,ne.price_adjustment=0;else{const H=parseFloat(N.selling_price)||0;ne.price_adjustment=F-H}ne.price_adjustment_formatted=(ne.price_adjustment>=0?"+":"")+ne.price_adjustment.toLocaleString()+"원"}if(a==="selling_price"&&m&&Array.isArray(m)&&(ne.multi_currency_selling_price=se({basePrice:J,currencies:m}),B.log(`[updateOptionField] Recalculated multi_currency_selling_price for option ${o}`)),re[O]=ne,ae!==null)for(let F=0;F<re.length;F++){if(F===O)continue;const H=re[F],he=parseFloat(H.price_adjustment)||0,Ot=ae+he,Gt={...H,selling_price:Ot};m&&Array.isArray(m)&&(Gt.multi_currency_selling_price=se({basePrice:Ot,currencies:m})),re[F]=Gt}let ze=N.stock_quantity,Ye=N.option_stock_sum;a==="stock_quantity"&&(Ye=re.reduce((F,H)=>H.is_active===!1?F:F+(parseInt(H.stock_quantity,10)||0),0),ze=Ye,B.log(`[updateOptionField] Updated product stock to option sum: ${ze}`));const We={...N,options:re,stock_quantity:ze,option_stock_sum:Ye,_modified:!0};return ae!==null&&(We.selling_price=ae,m&&Array.isArray(m)&&(We.multi_currency_selling_price=se({basePrice:ae,currencies:m})),y=!0,B.log(`[updateOptionField] Reverse-synced product ${t} selling_price to ${ae} from default option`)),We}return N});if(!w)return;i.dataSource.set(s,{...l,data:{...l.data,data:_}});const g=i.state.getLocal()||{},b=new Set(g.modifiedOptionIds||[]),C=`${t}-${o}`;b.add(C);const x={...g.modifiedOptionFields||{}},P=new Set(x[C]||[]);P.add(a),x[C]=Array.from(P);const S={modifiedOptionIds:Array.from(b),modifiedOptionFields:x,optionFieldErrors:f};if(y){const N=new Set(g.modifiedProductIds||[]);N.add(t);const L={...g.modifiedProductFields||{}},O=String(t),$=new Set(L[O]||[]);$.add("selling_price"),L[O]=Array.from($),S.modifiedProductIds=Array.from(N),S.modifiedProductFields=L}i.state.setLocal(S),B.log(`[updateOptionField] Updated option ${t}/${o}.${a} =`,n)}const Ht="g7_preferred_currency",qt="g7_locale",Je=window.G7Core?.createLogger?.("Handler:InitPreferredCurrency")??{log:(...r)=>console.log("[Handler:InitPreferredCurrency]",...r),warn:(...r)=>console.warn("[Handler:InitPreferredCurrency]",...r),error:(...r)=>console.error("[Handler:InitPreferredCurrency]",...r)};function Ut(r){const c=window.G7Core;c?.state?.set?c.state.set(r):Je.warn("G7Core.state.set not available")}function Vt(){try{return localStorage.getItem(qt)}catch{return null}}function Xe(){try{const e=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(e?.currencies)?e.currencies:[]}catch{return[]}}function jt(r){if(!r)return null;const c=r.toLowerCase().split("-")[0].split("_")[0];if(!c)return null;const t=Xe().filter(o=>(Array.isArray(o?.locales)?o.locales:[]).some(n=>String(n).toLowerCase()===c));return t.length===1?t[0]?.code??null:t.length>1?t.find(a=>a?.is_default===!0)?.code??null:null}function Bt(){try{const r=window.G7Config;return r?.user?r.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}function Kt(r,c){const e=r?.params?.defaultCurrency||Xe().find(l=>l?.is_default)?.code||"",t=l=>typeof l=="string"&&/^[A-Z]{3}$/.test(l),o=Bt(),a=r?.params?.accountCurrency??o?.ecommerce_preferred_currency;let n=null;try{n=localStorage.getItem(Ht)}catch{}const s=a?null:jt(Vt()),i=t(a)?a:t(n)?n:t(s)?s:t(e)?e:"KRW";Ut({preferredCurrency:i}),Je.log("Preferred currency initialized:",i)}const Qt="g7_preferred_shipping_country",Ze=window.G7Core?.createLogger?.("Handler:InitPreferredShippingCountry")??{log:(...r)=>console.log("[Handler:InitPreferredShippingCountry]",...r),warn:(...r)=>console.warn("[Handler:InitPreferredShippingCountry]",...r),error:(...r)=>console.error("[Handler:InitPreferredShippingCountry]",...r)};function zt(r){const c=window.G7Core;c?.state?.set?c.state.set(r):Ze.warn("G7Core.state.set not available")}function Yt(){try{const r=window.G7Config;return r?.user?r.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}function Wt(r,c){const e=r?.params?.defaultCountry||"KR",t=i=>typeof i=="string"&&/^[A-Z]{2}$/.test(i),o=Yt(),a=r?.params?.accountCountry??o?.ecommerce_preferred_shipping_country;let n=null;try{n=localStorage.getItem(Qt)}catch{}const s=t(a)?a:t(n)?n:t(e)?e:"KR";zt({preferredShippingCountry:s}),Ze.log("Preferred shipping country initialized:",s)}function X(r){const c=r.getFullYear(),e=String(r.getMonth()+1).padStart(2,"0"),t=String(r.getDate()).padStart(2,"0");return`${c}-${e}-${t}`}function Jt(r){const c=new Date,e=X(c);let t;switch(r){case"today":t=e;break;case"3days":{const o=new Date(c);o.setDate(o.getDate()-2),t=X(o);break}case"week":{const o=new Date(c);o.setDate(o.getDate()-6),t=X(o);break}case"month":{const o=new Date(c);o.setMonth(o.getMonth()-1),t=X(o);break}case"3months":{const o=new Date(c);o.setMonth(o.getMonth()-3),t=X(o);break}case"6months":{const o=new Date(c);o.setMonth(o.getMonth()-6),t=X(o);break}case"1year":{const o=new Date(c);o.setFullYear(o.getFullYear()-1),t=X(o);break}case"all":default:return t="",{startDate:"",endDate:""}}return{startDate:t,endDate:e}}function Xt(r){const{preset:c}=r.params||{preset:"today"},{startDate:e,endDate:t}=Jt(c);return{startDate:e,endDate:t,preset:c}}const ie=window.G7Core?.createLogger?.("Ecom:DefaultOption")??{log:(...r)=>console.log("[Ecom:DefaultOption]",...r),warn:(...r)=>console.warn("[Ecom:DefaultOption]",...r),error:(...r)=>console.error("[Ecom:DefaultOption]",...r)};function Zt(r,c){const e=r.params||{},{productId:t,optionId:o,dataSourceId:a="products"}=e;if(!t||!o){ie.warn("[setDefaultOption] Missing required params:",{productId:t,optionId:o});return}const n=window.G7Core;if(!n?.dataSource?.get||!n?.dataSource?.set){ie.warn("[setDefaultOption] G7Core.dataSource API is not available");return}const s=n.dataSource.get(a);if(!s){console.warn(`[setDefaultOption] DataSource '${a}' not found`);return}const i=s.data?.data||[];if(!Array.isArray(i)||i.length===0){ie.warn("[setDefaultOption] Products data is empty or invalid");return}const u=(n.state.get()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies,d=i.map(y=>{if(String(y.id)!==String(t))return y;const w=y.options||[];if(w.length===0)return y;const _=w.find(P=>String(P.id)===String(o));if(!_)return ie.warn("[setDefaultOption] Option not found:",o),y;const g=w.map(P=>{const S=String(P.id)===String(o);return{...P,is_default:S,_modified:S?!0:P._modified}}),b=_.list_price,C=_.selling_price;let x=y.multi_currency_selling_price;return u&&Array.isArray(u)&&(x=se({basePrice:C,currencies:u})),ie.log(`[setDefaultOption] Set option ${o} as default for product ${t}. Syncing prices: list_price=${b}, selling_price=${C}`),{...y,options:g,list_price:b,selling_price:C,multi_currency_selling_price:x,_modified:!0}});n.dataSource.set(a,{...s,data:{...s.data,data:d}});const m=n.state.getLocal()||{},p=new Set(m.modifiedProductIds||[]);p.add(String(t));const f=new Set(m.modifiedOptionIds||[]);f.add(`${t}-${o}`),n.state.setLocal({modifiedProductIds:Array.from(p),modifiedOptionIds:Array.from(f)}),ie.log(`[setDefaultOption] Option ${o} set as default for product ${t}`)}const Oe=window.G7Core?.createLogger?.("Ecom:OptionSelect")??{log:(...r)=>console.log("[Ecom:OptionSelect]",...r),warn:(...r)=>console.warn("[Ecom:OptionSelect]",...r),error:(...r)=>console.error("[Ecom:OptionSelect]",...r)},me=(r,c)=>`${r}-${c}`,Ge=r=>r.split("-")[0];function eo(r,c){const e=r.params||{},{productId:t,optionId:o,checked:a}=e,n=window.G7Core;if(!n?.state){Oe.warn("[toggleOption] G7Core.state를 사용할 수 없습니다.");return}const s=n.state.getLocal(),i=s.selectedOptionIds||[],l=s.selectedItems||[],u=me(t,o);let d;a?i.includes(u)?d=i:d=[...i,u]:d=i.filter(g=>g!==u);const y=(n.dataSource?.get("products")?.data?.data||[]).find(g=>String(g.id)===String(t))?.options||[];let w=[...l];const _=Number(t);y.length>0&&(y.map(C=>me(t,C.id)).filter(C=>d.includes(C)).length===y.length?w.includes(_)||w.push(_):w=w.filter(C=>C!==_)),n.state.setLocal({selectedOptionIds:d,selectedItems:w})}function et(r,c){const e=r.params||{},{productId:t,options:o,checked:a}=e,n=window.G7Core;if(!n?.state){Oe.warn("[toggleProductOptions] G7Core.state를 사용할 수 없습니다.");return}const s=n.state.getLocal(),i=s.selectedOptionIds||[],l=s.selectedItems||[],u=(o||[]).map(f=>me(t,f.id));let d,m=[...l];const p=Number(t);a?(d=[...i.filter(y=>Ge(y)!==String(t)),...u],m.includes(p)||m.push(p)):(d=i.filter(f=>Ge(f)!==String(t)),m=m.filter(f=>f!==p)),n.state.setLocal({selectedOptionIds:d,selectedItems:m})}function to(r,c){return et(r)}function oo(r,c){const e=r.params||{products:[],selectedOptionIds:[]},{products:t,selectedOptionIds:o}=e,a=o||[];if(!t||!Array.isArray(t))return{};const n={};for(const s of t){const i=String(s.id),l=s.options||[];if(l.length===0){n[i]="none";continue}const d=l.map(m=>me(i,m.id)).filter(m=>a.includes(m)).length;d===0?n[i]="none":d===l.length?n[i]="all":n[i]="some"}return n}function ro(r,c){const e=r.params||{},{selectedIds:t}=e,o=window.G7Core;if(!o?.state){Oe.warn("[syncProductSelection] G7Core.state를 사용할 수 없습니다.");return}const a=o.state.getLocal(),n=a.selectedItems||[],s=a.selectedOptionIds||[],l=o.dataSource?.get("products")?.data?.data||[],u=t.filter(p=>!n.includes(p)),d=n.filter(p=>!t.includes(p));let m=[...s];for(const p of u){const f=l.find(y=>Number(y.id)===p);if(f?.options)for(const y of f.options){const w=me(p,y.id);m.includes(w)||m.push(w)}}for(const p of d)m=m.filter(f=>Ge(f)!==String(p));o.state.setLocal({selectedItems:t,selectedOptionIds:m})}const Ce=window.G7Core?.createLogger?.("Ecom:CopyCode")??{log:(...r)=>console.log("[Ecom:CopyCode]",...r),warn:(...r)=>console.warn("[Ecom:CopyCode]",...r),error:(...r)=>console.error("[Ecom:CopyCode]",...r)};async function no(r,c){const e=window.G7Core;if(!e?.state||!e?.api){Ce.warn("[generateCopyProductCode] G7Core API is not available");return}try{const t=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/generate-code");if(!t?.data?.product_code)throw new Error("Invalid API response");const o=t.data.product_code;if((window.__g7LayoutContextStack||[]).length>0&&e.state.setParentLocal)e.state.setParentLocal({"ui.copyOptions.newProductCode":o}),Ce.log(`[generateCopyProductCode] Generated (parent): ${o}`);else{const s=e.state.getLocal()||{};e.state.setLocal({ui:{...s.ui,copyOptions:{...s.ui?.copyOptions||{},newProductCode:o}}}),Ce.log(`[generateCopyProductCode] Generated (local): ${o}`)}}catch(t){Ce.error("[generateCopyProductCode] Failed:",t),e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.code_generate_error")??"Failed to generate product code.")}}const tt=window.G7Core?.createLogger?.("Ecom:CopyProduct")??{log:(...r)=>console.log("[Ecom:CopyProduct]",...r),warn:(...r)=>console.warn("[Ecom:CopyProduct]",...r),error:(...r)=>console.error("[Ecom:CopyProduct]",...r)};async function ao(r,c){const e=window.G7Core;if(!e?.state){tt.warn("[copyProduct] G7Core API is not available");return}const t=e.state.getLocal()||{},a=(e.state.getParent?.()||{})._local||{},n=a.ui?.copyOptions||t.ui?.copyOptions||{},s=t.form?.product_code||a.form?.product_code||a.productList?.copyTargetProduct?.product_code||t.productList?.copyTargetProduct?.product_code;if(!s){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.copy_source_not_found")??"Could not find source product information.");return}e.modal?.close?.();const i={copy_id:s,copy_images:n.images??!0?"1":"0",copy_options:n.options??!0?"1":"0",copy_categories:n.categories??!0?"1":"0",copy_sales_info:n.sales_info??!0?"1":"0",copy_description:n.description??!0?"1":"0",copy_notice:n.notice??!0?"1":"0",copy_common_info:n.common_info??!0?"1":"0",copy_other_info:n.other_info??!0?"1":"0",copy_shipping:n.shipping??!0?"1":"0",copy_seo:n.seo??!1?"1":"0",copy_identification:n.identification??!0?"1":"0"},u=`/admin/ecommerce/products/create?${new URLSearchParams(i).toString()}`;tt.log("[copyProduct] Navigating to:",u),e.navigate?e.navigate(u):c.navigate?c.navigate(u):window.location.href=u}const be=window.G7Core?.createLogger?.("Ecom:ExpandedOptions")??{log:(...r)=>console.log("[Ecom:ExpandedOptions]",...r),warn:(...r)=>console.warn("[Ecom:ExpandedOptions]",...r),error:(...r)=>console.error("[Ecom:ExpandedOptions]",...r)},so="/api/modules/sirsoft-ecommerce/admin/products/options",Ee=100,De=new Set;function io(r,c){const t=r?.dataSource?.get?.(c)?.data?.data;return Array.isArray(t)?t:[]}function ot(r,c,e){const t=r?.dataSource?.get?.(c),o=t?.data?.data;if(!t||!Array.isArray(o)){be.warn("[loadExpandedOptions] DataSource not available for merge:",c);return}const a=o.map(n=>{const s=String(n?.id);return Object.prototype.hasOwnProperty.call(e,s)?{...n,options:e[s]}:n});r.dataSource.set(c,{...t,data:{...t.data,data:a}})}function co(r,c){const e=r?.state?.getLocal?.()||{},t=e.selectedItems||[];if(t.length===0)return;const o=new Set(t.map(s=>String(s))),a=new Set(e.selectedOptionIds||[]);let n=!1;Object.entries(c).forEach(([s,i])=>{o.has(s)&&i.forEach(l=>{const u=`${s}-${l?.id}`;a.has(u)||(a.add(u),n=!0)})}),n&&r.state.setLocal({selectedOptionIds:Array.from(a)})}function Fe(r,c){const e=r?.state?.getLocal?.()||{},t=c(e.optionsLoadingIds||[],e.optionsErrorIds||[]);r.state.setLocal({optionsLoadingIds:t.loading,optionsErrorIds:t.errored})}function $e(r,c){const e=r.params||{},t=e.dataSourceId||"products",o=e.force===!0,a=window.G7Core;if(!a?.dataSource?.get||!a?.dataSource?.set||!a?.state?.setLocal){be.warn("[loadExpandedOptions] G7Core dataSource/state API is not available");return}Array.isArray(e.expandedRows)&&a.state.setLocal({expandedRows:e.expandedRows});const n=Array.isArray(e.productIds)?e.productIds:e.productId!==void 0&&e.productId!==null?[e.productId]:Array.isArray(e.expandedRows)?e.expandedRows:a.state.getLocal?.()?.expandedRows||[];if(!Array.isArray(n)||n.length===0)return;const s=io(a,t),i=new Map(s.map(d=>[String(d?.id),d])),l=[],u={};n.forEach(d=>{const m=String(d),p=i.get(m);if(p&&!(!o&&Array.isArray(p.options))){if(Number(p.options_total_count)===0){u[m]=[];return}De.has(m)||l.push(m)}}),Object.keys(u).length>0&&ot(a,t,u),l.length!==0&&(l.length>Ee&&(be.warn(`[loadExpandedOptions] Requested ${l.length} products, truncating to ${Ee}`),l.length=Ee),l.forEach(d=>De.add(d)),Fe(a,(d,m)=>({loading:Array.from(new Set([...d,...l])),errored:m.filter(p=>!l.includes(p))})),a.api.get(so,{params:{product_ids:l.map(d=>Number(d))}}).then(d=>{const p=(d?.data||{}).options||{},f={};l.forEach(y=>{const w=p[y];f[y]=Array.isArray(w)?w:[]}),ot(a,t,f),co(a,f),Fe(a,(y,w)=>({loading:y.filter(_=>!l.includes(_)),errored:w.filter(_=>!l.includes(_))}))}).catch(d=>{be.error("[loadExpandedOptions] Failed to load options:",d),Fe(a,(p,f)=>({loading:p.filter(y=>!l.includes(y)),errored:Array.from(new Set([...f,...l]))}));const m=d?.response?.data?.message||a.t?.("sirsoft-ecommerce.admin.product.table.options.load_failed")||"Failed to load product options.";a.toast?.error?.(m)}).finally(()=>{l.forEach(d=>De.delete(d))}))}function lo(r,c={}){const e=r?.state?.getLocal?.()?.expandedRows||[];!Array.isArray(e)||e.length===0||$e({params:{productIds:e,force:c.force===!0,dataSourceId:c.dataSourceId||"products"}})}function uo(r,c){const e=r.params||{};$e({params:{productId:e.productId,productIds:e.productIds,force:!0,dataSourceId:e.dataSourceId}})}const A=window.G7Core?.createLogger?.("Ecom:Category")??{log:(...r)=>console.log("[Ecom:Category]",...r),warn:(...r)=>console.warn("[Ecom:Category]",...r),error:(...r)=>console.error("[Ecom:Category]",...r)};function rt(r,c,e,t=[]){for(const o of r){const a=o.name?.[e]||o.name?.ko||"";if(o.id===c)return[...t,a].join(" > ");if(o.children&&o.children.length>0){const n=rt(o.children,c,e,[...t,a]);if(n)return n}}return null}function mo(r,c,e){return r.map(t=>{if(t.breadcrumb)return{id:t.id,breadcrumb:t.breadcrumb};const o=rt(c,t.id,e);if(o)return{id:t.id,breadcrumb:o};const a=t.name?.[e]||t.name?.ko||t.name_localized||"";return{id:t.id,breadcrumb:a}})}function po(r,c){const e=r.params||{},t=e.depth,o=e.category;if(t===void 0||!o){A.warn("[selectCategory] Missing depth or category param");return}const a=c.isolatedContext;if(a){const u=a.state.selectedCategories||[null,null,null,null];if(u[t]?.id===o.id){A.log(`[selectCategory] Same category already selected at depth ${t}, skipping`);return}const d=u.map((m,p)=>p<t?m:p===t?o:null);a.mergeState({selectedCategories:d}),A.log(`[selectCategory] Selected category at depth ${t} (isolated):`,o.id);return}const n=window.G7Core;if(!n?.state){A.warn("[selectCategory] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=s.ui?.selectedCategories||[null,null,null,null];if(i[t]?.id===o.id){A.log(`[selectCategory] Same category already selected at depth ${t}, skipping`);return}const l=i.map((u,d)=>d<t?u:d===t?o:null);n.state.setLocal({ui:{...s.ui,selectedCategories:l}}),A.log(`[selectCategory] Selected category at depth ${t}:`,o.id)}function fo(r,c){const e=r.params||{},t=e.depth,o=e.category;if(t===void 0||!o){A.warn("[selectCategoryMobile] Missing depth or category param");return}const a=c.isolatedContext;if(a){const m=a.state.selectedCategories||[null,null,null,null],p=a.state.categoryStep||1,f=m.map((_,g)=>g<t?_:g===t?o:null),w=o.children&&o.children.length>0?t+2:p;a.mergeState({selectedCategories:f,categoryStep:w}),A.log(`[selectCategoryMobile] Selected category at depth ${t} (isolated):`,o.id,"nextStep:",w);return}const n=window.G7Core;if(!n?.state){A.warn("[selectCategoryMobile] G7Core.state API is not available");return}const s=n.state.getLocal()||{},l=(s.ui?.selectedCategories||[null,null,null,null]).map((m,p)=>p<t?m:p===t?o:null),d=o.children&&o.children.length>0?t+2:s.ui?.categoryStep;n.state.setLocal({ui:{...s.ui,selectedCategories:l,categoryStep:d}}),A.log(`[selectCategoryMobile] Selected category at depth ${t}:`,o.id,"nextStep:",d)}function go(r,c){A.log("[addCategoryToSelection] START");const e=window.G7Core;if(!e?.state){A.warn("[addCategoryToSelection] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.locale?.current?.()||"ko",a=c.isolatedContext,n=a?a.state.selectedCategories||[]:t.ui?.selectedCategories||[];A.log("[addCategoryToSelection] Current state:",{selectedCats:n.map(g=>g?.id??null),currentCategoryIds:t.form?.category_ids,currentSelectedCategoryInfos:t.selectedCategoryInfos,usingIsolatedContext:!!a});const s=n.filter(Boolean).pop();if(!s){A.warn("[addCategoryToSelection] No category selected");return}A.log("[addCategoryToSelection] Last selected category:",s.id,s.name);const i=t.form?.category_ids||[];if(i.length>=5){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.category.messages.max_5")??"You can select up to 5 categories.");return}if(i.includes(s.id)){A.log("[addCategoryToSelection] Duplicate detected:",s.id,"in",i),e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.category.messages.already_selected")??"This category is already selected.");return}const l=n.filter(Boolean).map(g=>g.name?.[o]||g.name?.ko||"").join(" > "),u=[...i,s.id],d=t.form?.primary_category_id||s.id,p=(e.state.getDataSources?.()||{}).categories?.data?.data||[];let f=t.selectedCategoryInfos||[];const y=t.form?.categories||[];f.length===0&&y.length>0&&(A.log("[addCategoryToSelection] Initializing from form.categories:",y.length,"items"),f=mo(y,p,o));const w=[...f,{id:s.id,breadcrumb:l}];A.log("[addCategoryToSelection] Before setLocal:",{newCategoryIds:u,primaryCategoryId:d,newInfos:w});try{e.state.setLocal({form:{...t.form,category_ids:u,primary_category_id:d},selectedCategoryInfos:w,hasChanges:!0}),A.log("[addCategoryToSelection] setLocal SUCCESS")}catch(g){A.error("[addCategoryToSelection] setLocal FAILED:",g)}const _=e.state.getLocal()||{};A.log("[addCategoryToSelection] After setLocal:",{category_ids:_.form?.category_ids,selectedCategoryInfos:_.selectedCategoryInfos}),A.log(`[addCategoryToSelection] Added category ${s.id} with breadcrumb: ${l}. Total: ${u.length}`)}function _o(r,c){const t=(r.params||{}).categoryId;if(!t){A.warn("[removeCategoryFromSelection] Missing categoryId param");return}const o=window.G7Core;if(!o?.state){A.warn("[removeCategoryFromSelection] G7Core.state API is not available");return}const a=o.state.getLocal()||{},s=(a.form?.category_ids||[]).filter(d=>d!==t);let i=a.form?.primary_category_id;i===t&&(i=s[0]||null);const u=(a.selectedCategoryInfos||[]).filter(d=>d.id!==t);o.state.setLocal({form:{...a.form,category_ids:s,primary_category_id:i},selectedCategoryInfos:u,hasChanges:!0}),A.log(`[removeCategoryFromSelection] Removed category ${t}. Remaining: ${s.length}`)}function yo(r,c){const e=r.params||{},t=e.categoryId,o=e.categories;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=(l,u,d=[])=>{for(const m of l){const p=m.name?.[n]||m.name?.ko||"";if(m.id===u)return[...d,p];if(m.children&&m.children.length>0){const f=s(m.children,u,[...d,p]);if(f)return f}}return null},i=s(o,t);return i?i.join(" > "):""}function wo(r,c){A.log("[initCategoryInfosFromProduct] START");const e=window.G7Core;if(!e?.state){A.warn("[initCategoryInfosFromProduct] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.locale?.current?.()||"ko",n=(e.state.getDataSources?.()||{}).categories?.data?.data||[],s=t.form?.categories||[],i=t.form?.category_ids||[];if(A.log("[initCategoryInfosFromProduct] Product categories:",s),A.log("[initCategoryInfosFromProduct] Category IDs:",i),A.log("[initCategoryInfosFromProduct] Categories tree available:",n.length>0),i.length===0){A.log("[initCategoryInfosFromProduct] No categories to initialize");return}const l=(d,m,p=[])=>{for(const f of d){const y=f.name?.[o]||f.name?.ko||"";if(f.id===m)return[...p,y];if(f.children&&f.children.length>0){const w=l(f.children,m,[...p,y]);if(w)return w}}return null},u=[];for(const d of i){const m=l(n,d);if(m)u.push({id:d,breadcrumb:m.join(" > ")});else{const p=s.find(f=>f.id===d);if(p){const f=p.name?.[o]||p.name?.ko||p.name_localized||"";u.push({id:d,breadcrumb:f})}}}A.log("[initCategoryInfosFromProduct] Generated selectedCategoryInfos:",u),e.state.setLocal({selectedCategoryInfos:u}),A.log("[initCategoryInfosFromProduct] DONE")}function ho(r,c){const e={category1:"",category2:"",category3:"",category4:""},t=r.params||{},o=t.categoryId1?String(t.categoryId1):"",a=t.categoryId2?String(t.categoryId2):"",n=t.categoryId3?String(t.categoryId3):"",s=t.categoryId4?String(t.categoryId4):"",i=t.categories;if(!i||!Array.isArray(i)||i.length===0)return A.warn("[validateCategoryPath] No categories data provided"),e;if(o){const l=i.find(u=>String(u.id)===o);if(l&&l.depth===0&&(e.category1=o,a&&l.children)){const u=l.children.find(d=>String(d.id)===a);if(u&&u.depth===1&&(e.category2=a,n&&u.children)){const d=u.children.find(m=>String(m.id)===n);if(d&&d.depth===2&&(e.category3=n,s&&d.children)){const m=d.children.find(p=>String(p.id)===s);m&&m.depth===3&&(e.category4=s)}}}}return A.log("[validateCategoryPath] Result:",e),e}function Co(r,c){const e=r.params||{},t=e.brandId,o=e.brands;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?s.name?.[n]??s.name?.ko??"":""}function bo(r,c){const e=r.params||{},t=e.brandId,o=e.brands;if(!t||!o||!Array.isArray(o))return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?.description?.[n]??s?.description?.ko??""}const Z=window.G7Core?.createLogger?.("Ecom:Price")??{log:(...r)=>console.log("[Ecom:Price]",...r),warn:(...r)=>console.warn("[Ecom:Price]",...r),error:(...r)=>console.error("[Ecom:Price]",...r)},Io={KRW:1e3,JPY:100};function So(r,c){const t=r.find(o=>o.code===c)?.base_unit??Io[c]??1;return Math.max(1,t)}function vo(r,c,e,t){const o=r/t*c;switch(e){case"ceil":return Math.ceil(o);case"floor":return Math.floor(o);case"round":default:return Math.round(o)}}function Lo(r,c){const e=r.params||{},t=e.field,o=e.currency,a=e.value,n=e.autoFill;if(!t||!o||a===void 0){Z.warn("[updatePrice] Missing required params: field, currency, or value");return}const s=window.G7Core;if(!s?.state){Z.warn("[updatePrice] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=c.datasources?.currencies?.data||{},u=l?.list||[],d=l?.default_currency||u.find(w=>w?.is_default)?.code||"",m=l?.rounding_method||"round",p={...i.form?.multi_currency_prices||{}};if(p[o]={...p[o]||{},[t]:a},n&&o===d){const w=So(u,d);u.forEach(_=>{if(_.code!==d){const g=_.exchange_rate??1,b=vo(a,g,m,w);p[_.code]={...p[_.code]||{},[t]:b}}})}const f={list:"list_price",selling:"selling_price"},y=o===d?a:i.form?.[f[t]];s.state.setLocal({form:{...i.form,multi_currency_prices:p,[f[t]]:y},hasChanges:!0}),Z.log(`[updatePrice] Updated ${t} price for ${o}: ${a}`)}function xo(r,c){const t=(r.params||{}).options;if(!t||!Array.isArray(t)||t.length===0)return 0;const o=t.reduce((a,n)=>n.is_active===!1?a:a+(n.stock_quantity||0),0);return Z.log(`[calculateTotalOptionStock] Total stock: ${o}`),o}function Po(r,c){const e=window.G7Core;if(!e?.state){Z.warn("[validatePriceRelation] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=parseFloat(String(t.form?.list_price))||0,a=parseFloat(String(t.form?.selling_price))||0,n=a>o&&o>0,s=e.t?.("sirsoft-ecommerce.admin.product.validation.selling_price_exceeds_list_price")??"판매가는 정가보다 클 수 없습니다.",i={...t.errors||{}};n?i.selling_price=[s]:delete i.selling_price;const l=Object.keys(i).length>0;e.state.setLocal({validation:{...t.validation,price_error:n},errors:l?i:null}),n?(e.toast?.warning?.(s),Z.warn(`[validatePriceRelation] Selling price (${a}) exceeds list price (${o})`)):Z.log("[validatePriceRelation] Price validation passed")}const T=window.G7Core?.createLogger?.("Ecom:ProductOption")??{log:(...r)=>console.log("[Ecom:ProductOption]",...r),warn:(...r)=>console.warn("[Ecom:ProductOption]",...r),error:(...r)=>console.error("[Ecom:ProductOption]",...r)};function Ao(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function nt(){return Ao().reduce((c,e)=>({...c,[e]:""}),{})}function Ie(r){return(r??[]).filter(c=>c.is_active!==!1).reduce((c,e)=>c+(parseInt(String(e.stock_quantity),10)||0),0)}const ko={KRW:1e3,JPY:100};function Ne(){const e=((window.G7Core?.state?.get?.()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).find(o=>o.is_default);if(!e)return 1;const t=e.base_unit??ko[e.code]??1;return Math.max(1,t)}function Oo(r,c,e){const t=parseFloat(c)||1;if(t<=0)return r;const o=r/t;let a;switch(e){case"ceil":a=Math.ceil(o);break;case"floor":a=Math.floor(o);break;default:a=Math.round(o)}return a*t}function Te(r,c,e){const t=c.exchange_rate||0;if(t<=0)return{price:0,formatted:de(0,c)};const o=e??Ne(),a=r/o*t,n=Oo(a,c.rounding_unit||"0.01",c.rounding_method||"round"),s=c.decimal_places??2,i=parseFloat(n.toFixed(s));return{price:i,formatted:de(i,c)}}function Re(r,c,e){return r.map(t=>{const o=parseFloat(String(t.price_adjustment))||0,a=c+o,n={...t,selling_price:a};if(e.length>0){const s=Ne(),i={};e.forEach(l=>{i[l.code]=Te(a,l,s)}),n.multi_currency_selling_price=i}return n})}function Me(){return((window.G7Core?.state?.get?.()||{}).modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).filter(c=>!c.is_default&&c.exchange_rate)}function Go(r,c){const e=r.params||{},t=window.G7Core;if(!t?.state){T.warn("[setDefaultOption] G7Core.state API is not available");return}const o=t.state.getLocal()||{},a=[...o.form?.options??[]],n=e.index,s=n!=null?typeof n=="string"?parseInt(n,10):n:-1,i=e.optionCode??a[s]?.option_code;if(!i){T.warn("[setDefaultOption] Missing optionCode/index param",{index:n});return}let l=a.map(m=>({...m,is_default:m.option_code===i}));const u=l.find(m=>m.is_default===!0),d=u?parseFloat(String(u.selling_price))||parseFloat(String(o.form?.selling_price))||0:parseFloat(String(o.form?.selling_price))||0;if(u){u.price_adjustment=0;const m=Me();l=Re(l,d,m)}t.state.setLocal({form:{...o.form,options:l,selling_price:d},hasChanges:!0}),T.log(`[setDefaultOption] Set default option to ${i}, synced product selling_price to ${d}`)}function Eo(r,c){const e=r.params||{},t=typeof e.index=="string"?parseInt(e.index,10):e.index,o=e.field,a=e.value;if(t==null||!o){T.warn("[updateFormOptionField] Missing required params:",{index:t,field:o});return}const n=window.G7Core;if(!n?.state){T.warn("[updateFormOptionField] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.options??[]];if(t<0||t>=i.length){T.warn("[updateFormOptionField] Index out of bounds:",{index:t,length:i.length});return}const l=["regular_price","sale_price","list_price","selling_price","stock_quantity","safe_stock_quantity","weight","volume","mileage_value"];let u=a;if(l.includes(o)&&(u=parseFloat(a)||0),i[t]={...i[t],[o]:u},o==="selling_price"){const d=parseFloat(String(u))||0,m=Me();if(i[t].is_default===!0){i[t].price_adjustment=0;const f=Re(i,d,m);n.state.setLocal({form:{...s.form,options:f,selling_price:d},hasChanges:!0}),T.log(`[updateFormOptionField] Reverse-synced product selling_price to ${d} from default option`);return}const p=parseFloat(String(s.form?.selling_price))||0;if(i[t].price_adjustment=d-p,m.length>0){const f=Ne(),y={};m.forEach(w=>{y[w.code]=Te(d,w,f)}),i[t].multi_currency_selling_price=y,T.log(`[updateFormOptionField] Recalculated multi_currency_selling_price for option at index ${t}:`,m.map(w=>w.code))}}if((o==="stock_quantity"||o==="is_active")&&i.length>0){const d=Ie(i);n.state.setLocal({form:{...s.form,options:i,stock_quantity:d},hasChanges:!0}),T.log(`[updateFormOptionField] Updated product stock_quantity to ${d} (sum of active options)`);return}n.state.setLocal({form:{...s.form,options:i},hasChanges:!0}),T.log(`[updateFormOptionField] Updated options[${t}].${o} =`,u)}function Do(r,c){const e=window.G7Core;if(!e?.state){T.warn("[addOptionRow] G7Core.state API is not available");return}const o=r.params?.isCreate===!0||r.params?.isCreate==="true"?1:0,a=e.state.getLocal()||{},n=[...a.form?.options??[]],s=a.form?.option_groups??[],i=`OPT-${Date.now()}`,l=s.map(p=>({key:p.name,value:nt()})),u={id:null,option_code:i,option_name:nt(),option_values:l,is_default:n.length===0,regular_price:0,sale_price:0,list_price:0,selling_price:0,price_adjustment:0,multi_currency_selling_price:{},sku:"",stock_quantity:o,safe_stock_quantity:0,weight:0,volume:0,mileage_value:0,mileage_type:"percent",is_active:!0},d=[...n,u],m=Ie(d);e.state.setLocal({form:{...a.form,options:d,stock_quantity:m,has_options:d.length>0},hasChanges:!0}),T.log(`[addOptionRow] Added new option row: ${i}, product stock_quantity = ${m}`)}function Fo(r,c){const e=window.G7Core;if(!e?.state){T.warn("[recalculateOptionPriceAdjustments] G7Core.state API is not available");return}const t=r.params||{},o=e.state.getLocal()||{},a=t.newSellingPrice!==void 0?parseFloat(String(t.newSellingPrice))||0:parseFloat(String(o.form?.selling_price))||0,n=[...o.form?.options??[]];if(n.length===0){T.log("[recalculateOptionPriceAdjustments] No options to recalculate");return}const s=Me(),i=Re(n,a,s);e.state.setLocal({form:{...o.form,options:i}}),T.log(`[recalculateOptionPriceAdjustments] Recalculated selling_price for ${n.length} options based on product selling_price: ${a}`)}const h=window.G7Core?.createLogger?.("Ecom:Option")??{log:(...r)=>console.log("[Ecom:Option]",...r),warn:(...r)=>console.warn("[Ecom:Option]",...r),error:(...r)=>console.error("[Ecom:Option]",...r)};function He(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function ce(){return window.G7Core?.config?.("app.locale")??He()[0]??"ko"}function q(){return He().reduce((c,e)=>({...c,[e]:""}),{})}function at(r){return{id:null,name:q(),price_adjustment:0,is_default:r===0,is_active:!0,allow_custom_text:!1,sort_order:r}}function $o(r,c){return typeof r=="string"?r:r[c]??r.ko??Object.values(r)[0]??""}function st(r){return r&&typeof r=="object"&&!Array.isArray(r)}function No(r){return Array.isArray(r)?r:r&&typeof r=="object"?Object.values(r):[]}function To(r){return r.reduce((c,e)=>{const t=[];return c.forEach(o=>{e.forEach(a=>{t.push([...o,a])})}),t},[[]])}function Ro(r){const c=He(),e={};for(const t of c){const o=r.map(a=>$o(a,t));e[t]=o.join("/")}return e}function Mo(r,c){return c.map((e,t)=>({key:typeof e.name=="string"?{...q(),[ce()]:e.name}:e.name,value:st(r[t])?r[t]:{...q(),[ce()]:r[t]}}))}function Ho(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addOptionInput] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.ui?.optionInputs??[];if(o.length>=3){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.input_max_3")??"You can add up to 3 option inputs.");return}e.state.setLocal({ui:{...t.ui,optionInputs:[...o,{name:q(),values:[]}]}}),h.log(`[addOptionInput] Added option input. Total: ${o.length+1}`)}function qo(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeOptionInput] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeOptionInput] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.ui?.optionInputs??[]];n.splice(t,1),o.state.setLocal({ui:{...a.ui,optionInputs:n}}),h.log(`[removeOptionInput] Removed option input at index ${t}`)}function Uo(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.value;if(t===void 0||!o){h.warn("[updateOptionInput] Missing required params");return}const n=window.G7Core;if(!n?.state){h.warn("[updateOptionInput] G7Core.state API is not available");return}h.log(`[updateOptionInput] Called with index=${t}, field="${o}" (type: ${typeof o}), value=`,a);const s=n.state.getLocal()||{},i=[...s.ui?.optionInputs??[]];if(h.log(`[updateOptionInput] Current inputs[${t}]:`,i[t]),i[t]||(h.log(`[updateOptionInput] inputs[${t}] not found, initializing with empty structure`),i[t]={name:q(),values:[]}),o==="name"){const l=ce(),u=i[t].name;h.log(`[updateOptionInput] Processing 'name' field - defaultLocale: ${l}, existingName:`,u,`type: ${typeof u}`);let d;a&&typeof a=="object"&&!Array.isArray(a)?(d=a,h.log("[updateOptionInput] Using multilingual value directly:",d)):(d={...u&&typeof u=="object"&&!Array.isArray(u)?u:q(),[l]:a},h.log("[updateOptionInput] Setting single locale value:",d)),i[t]={...i[t],name:d}}else h.log(`[updateOptionInput] Processing non-name field '${o}'`),i[t]={...i[t],[o]:a};h.log(`[updateOptionInput] Final inputs[${t}]:`,i[t]),n.state.setLocal({ui:{...s.ui,optionInputs:i}})}function Vo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[generateOptions] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=e.state.get()||{},a=t.ui?.optionInputs??[],n=ce(),s=g=>g.name?typeof g.name=="string"?g.name:g.name[n]??"":"",i=a.map(g=>({...g,values:No(g.values)})),l=i.filter(g=>s(g)&&g.values&&g.values.length>0);if(h.log(`[generateOptions] Validating inputs - defaultLocale: ${n}, normalizedInputs:`,i.map(g=>({name:g.name,nameType:typeof g.name,extractedName:s(g),values:g.values,valuesIsArray:Array.isArray(g.values)}))),h.log(`[generateOptions] Valid inputs count: ${l.length}`),l.length===0){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.name_value_required")??"Please enter option name and values.");return}const u=t.form?.options??[],d=r.params?.skipConfirm===!0;if(u.length>0&&!d){e.modal?.open?.("modal_confirm_regenerate");return}const p=r.params?.isCreate===!0||r.params?.isCreate==="true"?1:0,f=parseFloat(String(t.form?.list_price))||0,y=parseFloat(String(t.form?.selling_price))||0,w=t.form?.multi_currency_selling_price,_=(o.modules?.["sirsoft-ecommerce"]?.language_currency?.currencies||[]).filter(g=>!g.is_default&&g.exchange_rate);try{h.log("[generateOptions] Step 1: Creating optionGroups...");const g=l.map((L,O)=>(h.log(`[generateOptions] Processing input[${O}]:`,{name:L.name,values:L.values,valuesType:typeof L.values,valuesIsArray:Array.isArray(L.values)}),{name:typeof L.name=="string"?{...q(),[n]:L.name}:L.name,values:L.values.map($=>st($)?$:{...q(),[n]:$})})),C=To(l.map(L=>L.values)).map((L,O)=>{const $=Mo(L,l),Q=Ro(L),kt=`OPT-${String(O+1).padStart(3,"0")}`;let J={};return w&&typeof w=="object"&&Object.keys(w).length>0?J=JSON.parse(JSON.stringify(w)):y>0&&_.length>0&&_.forEach(Ae=>{J[Ae.code]=Te(y,Ae)}),{id:null,option_code:kt,option_values:$,option_name:Q,list_price:f,selling_price:y,price_adjustment:0,stock_quantity:p,safe_stock_quantity:0,is_default:O===0,is_active:!0,sku:"",multi_currency_selling_price:J}}),x=t.form?.options??[],P=L=>L.option_name?typeof L.option_name=="string"?L.option_name:L.option_name[n]??L.option_name.ko??"":"",S=C.map(L=>{const O=P(L),$=x.find(Q=>P(Q)===O);return $?{...L,...$,id:$.id}:L}),N=Ie(S);e.state.setLocal({form:{...t.form,option_groups:g,options:S,has_options:S.length>0,stock_quantity:N},hasChanges:!0}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.generated",{count:S.length})??`${S.length} options have been generated.`),h.log(`[generateOptions] Generated ${S.length} options`)}catch(g){throw h.error("[generateOptions] Error occurred:",g),h.error("[generateOptions] Error message:",g.message),h.error("[generateOptions] Error stack:",g.stack),g}}function jo(r,c){const t=(r.params||{}).rowIndex;if(t===void 0){h.warn("[deleteOption] Missing rowIndex param");return}const o=window.G7Core;if(!o?.state){h.warn("[deleteOption] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.options??[]];n.splice(t,1);const s=Ie(n),i=n.length>0;o.state.setLocal({form:{...a.form,options:n,has_options:i,...i?{stock_quantity:s}:{}},hasChanges:!0}),h.log(`[deleteOption] Deleted option at index ${t}, product stock_quantity = ${s}`)}function Bo(r,c){const e=r.params||{},t=e.optionName,o=e.applyToAll,a=e.inheritPrice,n=window.G7Core;if(!n?.state){h.warn("[applyOptionAddTool] G7Core.state API is not available");return}if(!t?.trim()){n.toast?.warning?.(n.t?.("sirsoft-ecommerce.admin.product.options.messages.name_required")??"Please enter option name.");return}const s=n.state.getLocal()||{},l=[...s.form?.options??[]].map(u=>{const d={...u};return o&&(d.option_values={...u.option_values,[t]:""}),a&&(d.price_adjustment=u.price_adjustment??0),d});n.state.setLocal({form:{...s.form,options:l},ui:{...s.ui,optionAddToolName:"",optionAddToolApplyToAll:!1,optionAddToolInheritPrice:!1},hasChanges:!0}),n.toast?.success?.(n.t?.("sirsoft-ecommerce.admin.product.options.messages.add_tool_applied")??"Option add tool has been applied.")}function Ko(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addRequiredItem] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.required_items??[],a={id:null,sort_order:o.length,option_name:{ko:"",en:""},description:{ko:"",en:""}};e.state.setLocal({form:{...t.form,required_items:[...o,a]},hasChanges:!0}),h.log(`[addRequiredItem] Added required item. Total: ${o.length+1}`)}function Qo(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.locale,n=e.value;if(t===void 0||!o||!a){h.warn("[updateRequiredItem] Missing required params");return}const s=window.G7Core;if(!s?.state){h.warn("[updateRequiredItem] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=[...i.form?.required_items??[]];l[t]={...l[t],[o]:{...l[t][o],[a]:n}},s.state.setLocal({form:{...i.form,required_items:l},hasChanges:!0})}function zo(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeRequiredItem] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeRequiredItem] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.required_items??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,required_items:n},hasChanges:!0}),h.log(`[removeRequiredItem] Removed required item at index ${t}`)}function Yo(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){h.warn("[reorderRequiredItems] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){h.warn("[reorderRequiredItems] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.required_items??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,required_items:s},hasChanges:!0}),h.log(`[reorderRequiredItems] Moved item from ${t} to ${o}`)}function Wo(r,c){const e=window.G7Core;if(!e?.state){h.warn("[addAdditionalOption] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.additional_options??[];if(o.length>=5){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_max_5")??"You can add up to 5 additional options.");return}const a=`temp_${Date.now()}_${Math.random().toString(36).substring(2,9)}`;e.state.setLocal({form:{...t.form,additional_options:[...o,{id:a,name:q(),is_required:!1,sort_order:o.length,values:[at(0)]}]},hasChanges:!0}),h.log(`[addAdditionalOption] Added additional option. Total: ${o.length+1}`)}function Jo(r,c){const e=r.params||{},t=e.index,o=e.field,a=e.value;if(t===void 0||!o){h.warn("[updateAdditionalOption] Missing required params");return}const n=window.G7Core;if(!n?.state){h.warn("[updateAdditionalOption] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.additional_options??[]];if(!i[t]){h.warn("[updateAdditionalOption] Option not found at index",t);return}if(o==="name")if(a&&typeof a=="object"&&!Array.isArray(a))i[t]={...i[t],name:a};else{const l=ce(),u=i[t].name,d=u&&typeof u=="object"&&!Array.isArray(u)?u:q();i[t]={...i[t],name:{...d,[l]:a}}}else i[t]={...i[t],[o]:a};n.state.setLocal({form:{...s.form,additional_options:i},hasChanges:!0}),h.log(`[updateAdditionalOption] Updated options[${t}].${o}`)}function Xo(r,c){const t=(r.params||{}).index;if(t===void 0){h.warn("[removeAdditionalOption] Missing index param");return}const o=window.G7Core;if(!o?.state){h.warn("[removeAdditionalOption] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.additional_options??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,additional_options:n},hasChanges:!0}),h.log(`[removeAdditionalOption] Removed additional option at index ${t}`)}function Zo(r,c){const e=r.params||{},t=parseInt(String(e.oldIndex),10),o=parseInt(String(e.newIndex),10);if(isNaN(t)||isNaN(o)||t===o)return;const a=window.G7Core;if(!a?.state){h.warn("[reorderAdditionalOptions] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.additional_options??[]];if(t<0||t>=s.length||o<0||o>=s.length)return;const[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,additional_options:s},ui:{...n.ui,dragIndex:null},hasChanges:!0}),h.log(`[reorderAdditionalOptions] Moved item from ${t} to ${o}`)}function er(r,c){const e=window.G7Core;if(!e?.state){h.warn("[clearAdditionalOptions] G7Core.state API is not available");return}const t=e.state.getLocal()||{};e.state.setLocal({form:{...t.form,additional_options:[]},hasChanges:!0}),h.log("[clearAdditionalOptions] Cleared all additional options.")}function qe(r){if(!r)return[];const c=r.values;return Array.isArray(c)?c:c&&typeof c=="object"?Object.values(c):[]}function tr(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex;if(t==null||isNaN(t)){h.warn("[addAdditionalOptionValue] Missing groupIndex param");return}const o=window.G7Core;if(!o?.state){h.warn("[addAdditionalOptionValue] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.additional_options??[]];if(!n[t]){h.warn("[addAdditionalOptionValue] Group not found at index",t);return}const s=[...qe(n[t])];if(s.length>=20){o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_value_max_20")??"You can add up to 20 choices per group.");return}s.push(at(s.length)),n[t]={...n[t],values:s},o.state.setLocal({form:{...a.form,additional_options:n},hasChanges:!0}),h.log(`[addAdditionalOptionValue] Added value to group ${t}. Total: ${s.length}`)}function or(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex,o=typeof e.valueIndex=="string"?parseInt(e.valueIndex,10):e.valueIndex,a=e.field,n=e.value;if(t===void 0||o===void 0||!a){h.warn("[updateAdditionalOptionValue] Missing required params");return}const s=window.G7Core;if(!s?.state){h.warn("[updateAdditionalOptionValue] G7Core.state API is not available");return}const i=s.state.getLocal()||{},l=[...i.form?.additional_options??[]];if(!l[t]){h.warn("[updateAdditionalOptionValue] Group not found at index",t);return}const u=[...qe(l[t])];if(!u[o]){h.warn("[updateAdditionalOptionValue] Value not found at index",o);return}if(a==="name")if(n&&typeof n=="object"&&!Array.isArray(n))u[o]={...u[o],name:n};else{const d=ce(),m=u[o].name,p=m&&typeof m=="object"&&!Array.isArray(m)?m:q();u[o]={...u[o],name:{...p,[d]:n}}}else if(a==="price_adjustment"){let d=parseFloat(String(n));(isNaN(d)||d<0)&&(d=0),u[o]={...u[o],price_adjustment:d}}else if(a==="is_default")n===!0||n==="true"?u.forEach((m,p)=>{m.is_default=p===o}):u[o].is_default=!1;else if(a==="is_active"||a==="allow_custom_text"){const d=n===!0||n==="true";u[o]={...u[o],[a]:d}}else u[o]={...u[o],[a]:n};l[t]={...l[t],values:u},s.state.setLocal({form:{...i.form,additional_options:l},hasChanges:!0}),h.log(`[updateAdditionalOptionValue] Updated group ${t} value ${o} field ${a}`)}function rr(r,c){const e=r.params||{},t=typeof e.groupIndex=="string"?parseInt(e.groupIndex,10):e.groupIndex,o=typeof e.valueIndex=="string"?parseInt(e.valueIndex,10):e.valueIndex;if(t===void 0||o===void 0){h.warn("[removeAdditionalOptionValue] Missing required params");return}const a=window.G7Core;if(!a?.state){h.warn("[removeAdditionalOptionValue] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.additional_options??[]];if(!s[t]){h.warn("[removeAdditionalOptionValue] Group not found at index",t);return}const i=[...qe(s[t])];if(i.length<=1){a.toast?.warning?.(a.t?.("sirsoft-ecommerce.admin.product.options.messages.additional_value_min_1")??"At least one choice is required per group.");return}i.splice(o,1),i.forEach((l,u)=>{l.sort_order=u}),i.some(l=>l.is_default)||(i[0].is_default=!0),s[t]={...s[t],values:i},a.state.setLocal({form:{...n.form,additional_options:s},hasChanges:!0}),h.log(`[removeAdditionalOptionValue] Removed value ${o} from group ${t}`)}const j=window.G7Core?.createLogger?.("Ecom:Image")??{log:(...r)=>console.log("[Ecom:Image]",...r),warn:(...r)=>console.warn("[Ecom:Image]",...r),error:(...r)=>console.error("[Ecom:Image]",...r)},nr=["image/jpeg","image/png","image/gif","image/webp"],ar=10*1024*1024,sr=20;async function ir(r,c){const t=(r.params||{}).files;if(!t||t.length===0){j.warn("[uploadImages] No files provided");return}const o=window.G7Core;if(!o?.state||!o?.api){j.warn("[uploadImages] G7Core.state or G7Core.api is not available");return}const n=(o.state.getLocal()||{}).form?.images??[],s=sr-n.length;t.length>s&&o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.max_count",{max:s})??`You can add up to ${s} images.`);const i=Array.from(t).slice(0,s);for(const l of i){if(!nr.includes(l.type)){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.unsupported_format",{name:l.name})??`${l.name}: Unsupported format.`);continue}if(l.size>ar){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.size_exceeded",{name:l.name})??`${l.name}: Exceeds 10MB.`);continue}const u=o.state.getLocal()||{};o.state.setLocal({ui:{...u.ui,uploadProgress:{...u.ui?.uploadProgress||{},[l.name]:0}}});try{const d=new FormData;d.append("file",l),d.append("collection","products");const m=await o.api.upload("/api/admin/media/upload",d,{onProgress:b=>{const C=o.state.getLocal()||{};o.state.setLocal({ui:{...C.ui,uploadProgress:{...C.ui?.uploadProgress||{},[l.name]:b}}})}}),p=o.state.getLocal()||{},f=p.form?.images??[],y={...m.data,is_thumbnail:f.length===0,sort_order:f.length};o.state.setLocal({form:{...p.form,images:[...f,y]},hasChanges:!0});const w=o.state.getLocal()||{},{[l.name]:_,...g}=w.ui?.uploadProgress||{};o.state.setLocal({ui:{...w.ui,uploadProgress:g}}),j.log(`[uploadImages] Uploaded ${l.name}`)}catch(d){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.images.messages.upload_failed",{name:l.name})??`${l.name}: Upload failed`);const m=o.state.getLocal()||{},{[l.name]:p,...f}=m.ui?.uploadProgress||{};o.state.setLocal({ui:{...m.ui,uploadProgress:f}}),j.error(`[uploadImages] Failed to upload ${l.name}:`,d)}}}function cr(r,c){const t=(r.params||{}).index;if(t===void 0){j.warn("[setThumbnail] Missing index param");return}const o=window.G7Core;if(!o?.state){j.warn("[setThumbnail] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=(a.form?.images??[]).map((s,i)=>({...s,is_thumbnail:i===t}));o.state.setLocal({form:{...a.form,images:n},hasChanges:!0}),j.log(`[setThumbnail] Set thumbnail to image at index ${t}`)}function lr(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){j.warn("[reorderImages] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){j.warn("[reorderImages] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.images??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,images:s},hasChanges:!0}),j.log(`[reorderImages] Moved image from ${t} to ${o}`)}const it=window.G7Core?.createLogger?.("Ecom:Description")??{log:(...r)=>console.log("[Ecom:Description]",...r),warn:(...r)=>console.warn("[Ecom:Description]",...r),error:(...r)=>console.error("[Ecom:Description]",...r)};function dr(r,c){const e=r.params||{},t=e.locale,o=e.value;if(!t){it.warn("[updateDescription] Missing locale param");return}const a=window.G7Core;if(!a?.state){it.warn("[updateDescription] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,description:{...n.form?.description,[t]:o}},hasChanges:!0})}const k=window.G7Core?.createLogger?.("Ecom:Notice")??{log:(...r)=>console.log("[Ecom:Notice]",...r),warn:(...r)=>console.warn("[Ecom:Notice]",...r),error:(...r)=>console.error("[Ecom:Notice]",...r)};function pe(r=""){const e=window.G7Core?.locale?.supported?.()??["ko","en"],t={};for(const o of e)t[o]=r;return t}function ur(r,c){const t=(r.params||{}).templateId,o=window.G7Core;if(!o?.state){k.warn("[confirmSelectNoticeTemplate] G7Core.state API is not available");return}const n=(o.state.getLocal()||{}).form?.notice_items??[],s=n.length>0&&n.some(i=>Object.values(i.content||{}).some(u=>u&&u.trim()!==""));if(!t||!s){ct(r);return}o.state.setGlobal({pendingNoticeTemplateId:t}),o.modal?.open?.("notice_template_confirm_modal"),k.log(`[confirmSelectNoticeTemplate] Showing confirm modal for template: ${t}`)}function ct(r,c){const t=(r.params||{}).templateId,o=window.G7Core;if(!o?.state){k.warn("[selectNoticeTemplate] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=o.dataSource?.get?.("notice_templates"),s=n?.data?.data??n?.data??[];if(!t){o.state.setLocal({form:{...a.form,notice_items:[]},ui:{...a.ui,selectedNoticeTemplateId:null},hasChanges:!0});return}const i=s.find(d=>d.id===t);if(!i){k.warn(`[selectNoticeTemplate] Template not found: ${t}`);return}const l=a.form?.notice_items??[],u=(i.fields??[]).map((d,m)=>{const p=JSON.stringify(d.name),f=l.find(y=>JSON.stringify(y.name)===p);return{key:`field_${m}`,name:d.name,content:f?.content??d.content??pe(),sort_order:m,is_custom:!1}});o.state.setLocal({form:{...a.form,notice_items:u},ui:{...a.ui,selectedNoticeTemplateId:t},hasChanges:!0}),k.log(`[selectNoticeTemplate] Selected template: ${t}`)}function mr(r,c){const e=r.params||{},t=e.index,o=e.locale,a=e.value;if(t===void 0||!o){k.warn("[updateNoticeItem] Missing required params");return}const n=window.G7Core;if(!n?.state){k.warn("[updateNoticeItem] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.notice_items??[]];i[t]={...i[t],content:{...i[t].content,[o]:a}},n.state.setLocal({form:{...s.form,notice_items:i},hasChanges:!0})}function pr(r,c){const t=(r.params||{}).index;if(t===void 0){k.warn("[removeNoticeItem] Missing index param");return}const o=window.G7Core;if(!o?.state){k.warn("[removeNoticeItem] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.notice_items??[]];n.splice(t,1),n.forEach((s,i)=>{s.sort_order=i}),o.state.setLocal({form:{...a.form,notice_items:n},hasChanges:!0}),k.log(`[removeNoticeItem] Removed notice item at index ${t}`)}function fr(r,c){const e=r.params||{},t=e.oldIndex,o=e.newIndex;if(t===void 0||o===void 0){k.warn("[reorderNoticeItems] Missing oldIndex or newIndex param");return}const a=window.G7Core;if(!a?.state){k.warn("[reorderNoticeItems] G7Core.state API is not available");return}const n=a.state.getLocal()||{},s=[...n.form?.notice_items??[]],[i]=s.splice(t,1);s.splice(o,0,i),s.forEach((l,u)=>{l.sort_order=u}),a.state.setLocal({form:{...n.form,notice_items:s},hasChanges:!0}),k.log(`[reorderNoticeItems] Moved item from ${t} to ${o}`)}function gr(r,c){const t=(r.params||{}).value,o=window.G7Core;if(!o?.state){k.warn("[fillNoticeWithValue] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=a.ui?.noticeLocale??"ko",s=(a.form?.notice_items??[]).map(i=>({...i,content:{...i.content,[n]:i.content?.[n]||t}}));o.state.setLocal({form:{...a.form,notice_items:s},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.notice.messages.filled")??"Empty fields have been filled.")}function _r(r,c){const t=(r.params||{}).mode;if(!t){k.warn("[switchNoticeMode] Missing mode param");return}const o=window.G7Core;if(!o?.state){k.warn("[switchNoticeMode] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,noticeMode:t}}),k.log(`[switchNoticeMode] Switched to ${t} mode`)}function yr(r,c){const e=r.params||{},t=e.locale,o=e.value;if(!t){k.warn("[updateNewTemplateName] Missing locale param");return}const a=window.G7Core;if(!a?.state){k.warn("[updateNewTemplateName] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,new_template_name:{...n.form?.new_template_name,[t]:o}}})}function wr(r,c){const e=window.G7Core;if(!e?.state){k.warn("[addNoticeItem] G7Core.state API is not available");return}const t=e.state.getLocal()||{},o=t.form?.notice_items??[],a={key:`custom_${Date.now()}`,name:pe(),content:pe(),sort_order:o.length,is_custom:!0};e.state.setLocal({form:{...t.form,notice_items:[...o,a]},hasChanges:!0}),k.log(`[addNoticeItem] Added new notice item. Total: ${o.length+1}`)}function hr(r,c){const e=r.params||{},t=e.index,o=e.locale,a=e.value;if(t===void 0||!o){k.warn("[updateNoticeItemName] Missing required params");return}const n=window.G7Core;if(!n?.state){k.warn("[updateNoticeItemName] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.notice_items??[]];i[t]={...i[t],name:{...i[t].name,[o]:a}},n.state.setLocal({form:{...s.form,notice_items:i},hasChanges:!0})}function Cr(r,c){const e=window.G7Core;if(!e?.state){k.warn("[saveAsNoticeTemplate] G7Core.state API is not available");return}if(((e.state.getLocal()||{}).form?.notice_items??[]).length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.no_items")??"No items to save.");return}e.state.set?.({"ui.saveTemplateData":{name:pe(),category_id:null,is_default:!1}}),e.dispatch?.({handler:"openModal",target:"modal_save_template"})}const Se={ko:"상세설명참조",en:"See detail description"};function br(r,c){const e=window.G7Core;if(!e?.state){k.warn("[fillTemplateFieldsWithDetailReference] G7Core.state API is not available");return}const a=((r.params||{}).form||{}).fields??[];if(a.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product_notice_template.messages.no_fields_to_change")??"변경할 항목이 없습니다.");return}const n=e.locale?.supported?.()??["ko","en"],s={};for(const l of n)s[l]=Se[l]??Se.en??"See detail description";const i=a.map(l=>({...l,content:{...s}}));e.state.setLocal({"form.fields":i}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product_notice_template.messages.all_fields_changed_to_detail_ref")??"모든 항목이 '상세설명참조'로 변경되었습니다."),k.log("[fillTemplateFieldsWithDetailReference] All fields updated to detail reference")}function Ir(r,c){const e=window.G7Core;if(!e?.state){k.warn("[fillNoticeItemsWithDetailReference] G7Core.state API is not available");return}const a=((r.params||{}).form||{}).notice_items??[];if(a.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.no_items_to_change")??"변경할 항목이 없습니다.");return}const n=e.locale?.supported?.()??["ko","en"],s={};for(const l of n)s[l]=Se[l]??Se.en??"See detail description";const i=a.map(l=>({...l,content:{...s}}));e.state.setLocal({"form.notice_items":i}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.all_items_changed_to_detail_ref")??"모든 항목이 '상세설명참조'로 변경되었습니다."),k.log("[fillNoticeItemsWithDetailReference] All notice items updated to detail reference")}async function Sr(r,c){const e=window.G7Core;if(!e?.state||!e?.api){k.warn("[confirmSaveNoticeTemplate] G7Core.state or G7Core.api is not available");return}const t=e.state.getLocal()||{},o=e.state.get?.()?.ui?.saveTemplateData??{},a=o.name;if(!a?.ko?.trim()){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_name_required")??"Please enter template name.");return}try{const n=t.form?.notice_items??[],s=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/notice-templates",{name:a,is_active:o.is_default??!1,fields:n.map(i=>({name:i.name,content:i.content}))});e.state.set?.({"ui.saveTemplateData":{name:pe(),category_id:null,is_default:!1}}),e.state.setLocal({ui:{...t.ui,noticeMode:"existing"},hasChanges:!0}),e.dispatch?.({handler:"closeModal"}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_saved")??"Template has been saved."),e.datasources?.refresh?.("notice_templates"),k.log(`[confirmSaveNoticeTemplate] Template saved: ${s.data.id}`)}catch(n){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.notice.messages.template_save_error")??"Failed to save template."),k.error("[confirmSaveNoticeTemplate] Failed to save template:",n)}}const v=window.G7Core?.createLogger?.("Ecom:Misc")??{log:(...r)=>console.log("[Ecom:Misc]",...r),warn:(...r)=>console.warn("[Ecom:Misc]",...r),error:(...r)=>console.error("[Ecom:Misc]",...r)};function vr(r,c){const t=(r.params||{}).labelId;if(!t){v.warn("[toggleLabel] Missing labelId param");return}const o=window.G7Core;if(!o?.state){v.warn("[toggleLabel] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=a.form?.labels??[],s=n.includes(t)?n.filter(i=>i!==t):[...n,t];o.state.setLocal({form:{...a.form,labels:s},hasChanges:!0}),v.log(`[toggleLabel] Toggled label ${t}. Current labels: ${s.length}`)}async function Lr(r,c){const e=window.G7Core;if(!e?.state||!e?.api){v.warn("[generateProductCode] G7Core.state or G7Core.api is not available");return}try{const t=await e.api.post("/api/modules/sirsoft-ecommerce/admin/products/generate-code"),o=e.state.getLocal()||{};e.state.setLocal({form:{...o.form,product_code:t.data.product_code}}),v.log(`[generateProductCode] Generated: ${t.data.product_code}`)}catch(t){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.product.messages.code_generate_error")??"Failed to generate product code."),v.error("[generateProductCode] Failed:",t)}}function xr(r,c){const e=r.params||{},t=e.policyId,o=e.field,a=e.policies;if(!t||!o||!a)return"-";const n=a.find(s=>s.id===t);return n?n[o]??"-":"-"}function Pr(r,c){const e=r.params||{},t=e.templateId,o=e.templates;if(!t||!o)return"";const n=window.G7Core?.locale?.current?.()||"ko",s=o.find(i=>i.id===t);return s?.content?.[n]??s?.content?.ko??""}function Ar(r,c){const e=r.params||{},t=e.platform,o=e.field,a=e.value;if(!t||!o){v.warn("[updateShoppingIntegration] Missing required params");return}const n=window.G7Core;if(!n?.state){v.warn("[updateShoppingIntegration] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=s.form?.shopping_integrations??{naver:{enabled:!1,sync_interval:"manual"},daum:{enabled:!1,sync_interval:"manual"},facebook:{enabled:!1,sync_interval:"manual"},google:{enabled:!1,sync_interval:"manual"}};n.state.setLocal({form:{...s.form,shopping_integrations:{...i,[t]:{...i[t],[o]:a}}},hasChanges:!0}),v.log(`[updateShoppingIntegration] Updated ${t}.${o} to ${a}`)}function kr(r,c){const t=(r.params||{}).type;if(!t){v.warn("[updateShippingType] Missing type param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateShippingType] G7Core.state API is not available");return}const a=o.state.getLocal()||{};let n=0,s=0;t==="fixed_paid"?n=3e3:t==="threshold_free"&&(s=3e4),o.state.setLocal({form:{...a.form,shipping_type:t,shipping_fee:t==="fixed_free"?0:a.form?.shipping_fee??n,free_shipping_threshold:t==="threshold_free"?a.form?.free_shipping_threshold??s:0},hasChanges:!0}),v.log(`[updateShippingType] Updated shipping type to ${t}`)}function Or(r,c){const e=r.params||{},t=e.field,o=e.value;if(!t){v.warn("[updateIdentificationCode] Missing field param");return}const a=window.G7Core;if(!a?.state){v.warn("[updateIdentificationCode] G7Core.state API is not available");return}const n=a.state.getLocal()||{};a.state.setLocal({form:{...n.form,[t]:o},hasChanges:!0})}function Gr(r,c){const e=r.params||{},t=e.labelId,o=e.labelName;if(!t){v.warn("[openLabelPeriodModal] Missing labelId param");return}const a=window.G7Core;if(!a?.state){v.warn("[openLabelPeriodModal] G7Core.state API is not available");return}const n=a.state.getLocal()||{},i=(n.form?.labels??[]).find(l=>l.id===t);a.state.setLocal({ui:{...n.ui,showLabelPeriodModal:!0,labelPeriodModalData:{labelId:t,labelName:o,startDate:i?.start_date??null,endDate:i?.end_date??null}}})}function Er(r,c){const e=r.params||{},t=e.labelId,o=e.startDate,a=e.endDate;if(!t){v.warn("[saveLabelPeriod] Missing labelId param");return}const n=window.G7Core;if(!n?.state){v.warn("[saveLabelPeriod] G7Core.state API is not available");return}const s=n.state.getLocal()||{},i=[...s.form?.labels??[]],l=i.findIndex(u=>u.id===t);l>=0?i[l]={...i[l],start_date:o,end_date:a}:i.push({id:t,start_date:o,end_date:a}),n.state.setLocal({form:{...s.form,labels:i},ui:{...s.ui,showLabelPeriodModal:!1,labelPeriodModalData:null},hasChanges:!0}),n.toast?.success?.(n.t?.("sirsoft-ecommerce.admin.product.messages.label_period_saved")??"Label period has been saved."),v.log(`[saveLabelPeriod] Saved period for label ${t}`)}function Dr(r,c){const t=(r.params||{}).labelId;if(!t){v.warn("[removeLabelPeriod] Missing labelId param");return}const o=window.G7Core;if(!o?.state){v.warn("[removeLabelPeriod] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=[...a.form?.labels??[]],s=n.findIndex(i=>i.id===t);s>=0&&(n[s]={...n[s],start_date:null,end_date:null}),o.state.setLocal({form:{...a.form,labels:n},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.messages.label_period_removed")??"Label period has been removed."),v.log(`[removeLabelPeriod] Removed period for label ${t}`)}function Fr(r,c){const t=(r.params||{}).sort;if(!t){v.warn("[updateActivityLogSort] Missing sort param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateActivityLogSort] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,activityLogSort:t}}),o.datasources?.refresh?.("activity_logs",{params:{sort:t==="latest"?"desc":"asc",per_page:a.ui?.activityLogPerPage??10}}),v.log(`[updateActivityLogSort] Updated sort to ${t}`)}function $r(r,c){const t=(r.params||{}).perPage;if(!t){v.warn("[updateActivityLogPerPage] Missing perPage param");return}const o=window.G7Core;if(!o?.state){v.warn("[updateActivityLogPerPage] G7Core.state API is not available");return}const a=o.state.getLocal()||{};o.state.setLocal({ui:{...a.ui,activityLogPerPage:t}}),o.datasources?.refresh?.("activity_logs",{params:{sort:a.ui?.activityLogSort==="oldest"?"asc":"desc",per_page:t}}),v.log(`[updateActivityLogPerPage] Updated perPage to ${t}`)}function Nr(r,c){const e=window.G7Core;if(!e?.state){v.warn("[setDefaultShippingPolicy] G7Core.state API is not available");return}const t=e.state.getLocal()||{};if(t.form?.shipping_policy_id){v.log("[setDefaultShippingPolicy] Shipping policy already set, skipping");return}if(t.ui?.useDefaultShippingPolicy===!1){v.log("[setDefaultShippingPolicy] useDefaultShippingPolicy is OFF, skipping");return}const o=e.dataSource?.get?.("shipping_policies"),a=o?.data?.data??o?.data??[];if(a.length===0){v.log("[setDefaultShippingPolicy] No shipping policies found");return}const n=a.find(s=>s.is_default===!0);n?(e.state.setLocal({form:{...t.form,shipping_policy_id:n.id}}),v.log(`[setDefaultShippingPolicy] Set default shipping policy: ${n.id} (${n.name})`)):v.log("[setDefaultShippingPolicy] No default shipping policy found")}function Tr(r,c){const e=window.G7Core;if(!e?.state){v.warn("[toggleDefaultShippingPolicy] G7Core.state API is not available");return}const t=r.params?.checked??!1,o=e.state.getLocal()||{},a={...o.ui,useDefaultShippingPolicy:t};if(t){const n=e.dataSource?.get?.("shipping_policies"),i=(n?.data?.data??n?.data??[]).find(l=>l.is_default===!0);e.state.setLocal({ui:a,form:{...o.form,shipping_policy_id:i?.id??null}}),i?v.log(`[toggleDefaultShippingPolicy] ON - Selected default policy: ${i.id}`):v.log("[toggleDefaultShippingPolicy] ON - No default policy found")}else e.state.setLocal({ui:a}),v.log("[toggleDefaultShippingPolicy] OFF - Keeping current shipping policy")}function Rr(r,c){const e=window.G7Core;if(!e?.state){v.warn("[setLabelDatePreset] G7Core.state API is not available");return}const t=r.params?.preset;if(!t){v.warn("[setLabelDatePreset] Missing preset param");return}const o=new Date,a=i=>{const l=i.getFullYear(),u=String(i.getMonth()+1).padStart(2,"0"),d=String(i.getDate()).padStart(2,"0");return`${l}-${u}-${d}`},n=a(o);let s=null;switch(t){case"7d":{const i=new Date(o);i.setDate(i.getDate()+7),s=a(i);break}case"14d":{const i=new Date(o);i.setDate(i.getDate()+14),s=a(i);break}case"30d":{const i=new Date(o);i.setDate(i.getDate()+30),s=a(i);break}case"permanent":s=null;break;default:v.warn(`[setLabelDatePreset] Unknown preset: ${t}`);return}e.state.setGlobal({"labelFormData.start_date":n,"labelFormData.end_date":s}),v.log(`[setLabelDatePreset] Applied preset '${t}': ${n} ~ ${s??"permanent"}`)}const Mr=(r,c)=>{const e=window.G7Core,o=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],a=Number(r.params?.labelId);if(!a)return;const n=[...o];if(n.find(l=>l.label_id===a)){e?.state?.setLocal?.({"ui.lastClickedLabelId":a});return}const i=[...n,{label_id:a,started_at:null,ended_at:null}];e?.state?.setLocal?.({"form.label_assignments":i,"ui.lastClickedLabelId":a,hasChanges:!0})},Hr=(r,c)=>{const e=window.G7Core,o=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],a=Number(r.params?.labelId),n=r.params?.field,s=r.params?.value;if(!a||!n)return;const i=[...o],l=i.findIndex(u=>u.label_id===a);l<0||(i[l]={...i[l],[n]:s||null},e?.state?.setLocal?.({"form.label_assignments":i,hasChanges:!0}))},qr=(r,c)=>{const e=window.G7Core,t=e?.state?.getLocal?.()||{},o=t.form?.label_assignments??[],a=t.ui?.lastClickedLabelId,n=r.params?.preset;if(!a||!n)return;const s=[...o],i=s.findIndex(p=>p.label_id===a);if(i<0)return;const l=new Date,u=p=>p.toISOString().split("T")[0],d=u(l);let m=null;switch(n){case"7d":m=u(new Date(l.getTime()+7*24*60*60*1e3));break;case"14d":m=u(new Date(l.getTime()+14*24*60*60*1e3));break;case"30d":m=u(new Date(l.getTime()+30*24*60*60*1e3));break;case"permanent":m=null;break}s[i]={...s[i],started_at:d,ended_at:m},e?.state?.setLocal?.({"form.label_assignments":s,hasChanges:!0})},Ur=(r,c)=>{const e=window.G7Core,o=(e?.state?.get()||{}).labelToUncheckId;if(!o)return;const i=[...(e?.state?.getParent?.()?._local||{}).form?.label_assignments??[]].filter(l=>l.label_id!==o);e?.state?.setParentLocal?.({"form.label_assignments":i,hasChanges:!0}),e?.state?.setGlobal?.({labelToUncheckId:null}),e?.modal?.close?.()},Vr=async(r,c)=>{const e=window.G7Core,t=e?.state?.get()||{},a=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],n=t.labelToDeleteId;if(n){e?.state?.setGlobal?.({isDeletingLabel:!0});try{const s=await e.api.delete(`/api/modules/sirsoft-ecommerce/admin/product-labels/${n}`);if(!s?.success)throw new Error(s?.message||"Failed to delete label");const l=[...a].filter(u=>u.label_id!==n);e?.state?.setLocal?.({"form.label_assignments":l,"ui.lastClickedLabelId":null}),await e?.dataSource?.refetch?.("product_labels"),e?.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.product.labels.delete_success")??"Label deleted."),e?.modal?.close?.()}catch(s){e?.toast?.error?.(s?.response?.data?.message||s?.message||e.t?.("sirsoft-ecommerce.admin.product.labels.delete_error")||"Failed to delete label.")}finally{e?.state?.setGlobal?.({isDeletingLabel:!1,labelToDeleteId:null})}}},jr=async(r,c)=>{const e=window.G7Core,t=e?.state?.get()||{},a=(e?.state?.getLocal?.()||{}).form?.label_assignments??[],n=t.editingLabelId,s=t.labelFormData;if(s?.name?.ko){e?.state?.setGlobal?.({isSavingLabel:!0});try{if(n){const i=await e.api.put(`/api/modules/sirsoft-ecommerce/admin/product-labels/${n}`,{name:s.name,color:s.color});if(!i?.success)throw new Error(i?.message||"Failed to update label");const u=[...a].map(d=>d.label_id===n?{...d,started_at:s.started_at||null,ended_at:s.ended_at||null}:d);e?.state?.setLocal?.({"form.label_assignments":u,hasChanges:!0})}else{const i=await e.api.post("/api/modules/sirsoft-ecommerce/admin/product-labels",{name:s.name,color:s.color||"#6B7280"});if(!i?.success)throw new Error(i?.message||"Failed to create label");const l=Number(i?.data?.id);if(l){const d=[...[...a],{label_id:l,started_at:null,ended_at:null}];e?.state?.setLocal?.({"form.label_assignments":d,"ui.lastClickedLabelId":l,hasChanges:!0})}}await e?.dataSource?.refetch?.("product_labels"),e?.toast?.success?.(n?e.t?.("sirsoft-ecommerce.admin.product.labels.save_success")??"Label saved.":e.t?.("sirsoft-ecommerce.admin.product.labels.create_success")??"Label created."),e?.modal?.close?.()}catch(i){e?.toast?.error?.(i?.response?.data?.message||i?.message||e.t?.("sirsoft-ecommerce.admin.product.labels.save_error")||"Failed to save label.")}finally{e?.state?.setGlobal?.({isSavingLabel:!1})}}},ee=window.G7Core?.createLogger?.("Ecom:DescriptionLocale")??{log:(...r)=>console.log("[Ecom:DescriptionLocale]",...r),warn:(...r)=>console.warn("[Ecom:DescriptionLocale]",...r),error:(...r)=>console.error("[Ecom:DescriptionLocale]",...r)};function lt(){return window.G7Core?.config?.("app.supported_locales")??["ko","en"]}function Br(r,c){const t=(r.params||{}).locale;if(!t){ee.warn("[removeDescriptionLocale] Missing locale param");return}const o=window.G7Core;if(!o?.state){ee.warn("[removeDescriptionLocale] G7Core.state API is not available");return}const a=o?.state?.get?.("_global.settings.general.language")??o?.config?.("app.fallback_locale")??"ko";if(t===a){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.default_locale_cannot_delete")??"Default language cannot be deleted.");return}const n=o.state.getLocal()||{},s=lt(),i={...n.form?.description};delete i[t];const l={...n.form?.description_mobile};delete l[t];const u=(n.ui?.descriptionActiveLocales??s).filter(d=>d!==t);o.state.setLocal({form:{...n.form,description:i,description_mobile:l},ui:{...n.ui,descriptionActiveLocales:u,descriptionLocale:u[0]??"ko"},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_removed",{locale:t.toUpperCase()})??`${t.toUpperCase()} language has been removed.`),ee.log(`[removeDescriptionLocale] Removed locale: ${t}`)}function Kr(r,c){const e=window.G7Core;if(!e?.state){ee.warn("[showAddLocaleModal] G7Core.state API is not available");return}const t=e.state.getLocal()||{};e.state.setLocal({ui:{...t.ui,showAddLocaleModal:!0}})}function Qr(r,c){const t=(r.params||{}).locale;if(!t){ee.warn("[addDescriptionLocale] Missing locale param");return}const o=window.G7Core;if(!o?.state){ee.warn("[addDescriptionLocale] G7Core.state API is not available");return}const a=o.state.getLocal()||{},n=lt(),s=a.ui?.descriptionActiveLocales??["ko"];if(s.includes(t)){o.toast?.warning?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_already_added")??"This language is already added.");return}if(!n.includes(t)){o.toast?.error?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_unsupported")??"Unsupported language.");return}o.state.setLocal({form:{...a.form,description:{...a.form?.description,[t]:""},description_mobile:{...a.form?.description_mobile,[t]:""}},ui:{...a.ui,descriptionActiveLocales:[...s,t],descriptionLocale:t,showAddLocaleModal:!1},hasChanges:!0}),o.toast?.success?.(o.t?.("sirsoft-ecommerce.admin.product.description_editor.messages.locale_added",{locale:t.toUpperCase()})??`${t.toUpperCase()} language has been added.`),ee.log(`[addDescriptionLocale] Added locale: ${t}`)}const Y=window.G7Core?.createLogger?.("Ecom:BulkUpdate")??{log:(...r)=>console.log("[Ecom:BulkUpdate]",...r),warn:(...r)=>console.warn("[Ecom:BulkUpdate]",...r),error:(...r)=>console.error("[Ecom:BulkUpdate]",...r)};function dt(r,c,e){const t=c?.response?.data??{},o=t?.message||c?.message||"",a=t?.errors,n=a&&typeof a=="object"?Object.values(a).map(i=>Array.isArray(i)?i[0]:String(i)):[],s=o||r.t(e);r.toast.error(s),r.state.set({bulkUpdateErrors:n.length>0?n:s?[s]:[]})}function W(r){return typeof r=="string"?r:r&&typeof r=="object"?r.ko||r.en||JSON.stringify(r):String(r??"")}function ve(r,c,e){return e?r.t(`sirsoft-ecommerce.enums.${c}.${e}`)||e:String(e??"")}function ut(){const r=window.G7Core;return r||(Y.error("[bulkUpdate] G7Core is not available"),null)}function zr(r,c){const e=ut();if(!e)return;const t=e.state.getLocal()||{},o=e.state.get()||{},a=o.bulkSelectedItems||t.selectedItems||[],n=o.bulkSelectedOptionIds||t.selectedOptionIds||[];Y.log("[bulkUpdate] globalState.bulkSelectedItems:",o.bulkSelectedItems),Y.log("[bulkUpdate] localState.selectedItems:",t.selectedItems),Y.log("[bulkUpdate] Final selectedItems:",a),a.length>0?Yr(e,o,t,a):n.length>0?Wr(e,o,t,n):e.toast.warning(e.t("sirsoft-ecommerce.admin.product.bulk.no_selection"))}function Yr(r,c,e,t){const a=r.dataSource.get("products")?.data?.data||[],n={};c.bulkSalesStatus&&(n.sales_status=c.bulkSalesStatus),c.bulkDisplayStatus&&(n.display_status=c.bulkDisplayStatus);const s={};if(c.bulkPriceMethod&&c.bulkPriceValue!==void 0&&c.bulkPriceValue!==null&&c.bulkPriceValue!==""){const g={fixed:"set",increase:"add",decrease:"subtract"};c.bulkPriceUnit==="percent"?s.price_adjustment={method:"percent",value:c.bulkPriceMethod==="decrease"?-(Number(c.bulkPriceValue)||0):Number(c.bulkPriceValue)||0}:s.price_adjustment={method:g[c.bulkPriceMethod]||"add",value:Number(c.bulkPriceValue)||0}}if(c.bulkStockMethod&&c.bulkStockValue!==void 0&&c.bulkStockValue!==null&&c.bulkStockValue!==""){const g={set:"set",increase:"add",decrease:"subtract"};s.stock_quantity={method:g[c.bulkStockMethod]||"add",value:Number(c.bulkStockValue)||0}}const i=e.modifiedProductIds||[],l=e.modifiedOptionIds||[],u=e.modifiedProductFields||{},d=e.modifiedOptionFields||{},m=c.bulkSelectedOptionIds||e.selectedOptionIds||[],p=new Set(t),f=[],y=[];function w(g,b,C){if(b._modified||l.includes(C)){const P={product_id:g.id,option_id:b.id},S=d[C]||[];!s.price_adjustment&&S.includes("price_adjustment")&&(P.price_adjustment=b.price_adjustment),!s.price_adjustment&&S.includes("selling_price")&&(P.price_adjustment=b.price_adjustment),!s.stock_quantity&&S.includes("stock_quantity")&&(P.stock_quantity=b.stock_quantity),S.includes("option_name")&&(P.option_name=b.option_name),S.includes("sku")&&(P.sku=b.sku),S.includes("list_price")&&(P.list_price=b.list_price),S.includes("safe_stock_quantity")&&(P.safe_stock_quantity=b.safe_stock_quantity),S.includes("is_default")&&(P.is_default=b.is_default),S.includes("is_active")&&(P.is_active=b.is_active),Object.keys(P).length>2&&y.push(P)}}a.filter(g=>t.includes(g.id)).forEach(g=>{if(g._modified||i.includes(g.id)){const C={id:g.id},x=String(g.id),P=u[x]||[];!n.sales_status&&P.includes("sales_status")&&(C.sales_status=g.sales_status),!n.display_status&&P.includes("display_status")&&(C.display_status=g.display_status),P.includes("name")&&(C.name=g.name),P.includes("list_price")&&(C.list_price=g.list_price),P.includes("selling_price")&&(C.selling_price=g.selling_price),Object.keys(C).length>1&&f.push(C)}(g.options||[]).forEach(C=>{const x=`${g.id}-${C.id}`;w(g,C,x)})}),m.length>0&&a.forEach(g=>{p.has(g.id)||(g.options||[]).forEach(b=>{const C=`${g.id}-${b.id}`;m.includes(C)&&w(g,b,C)})});const _={ids:t};Object.keys(n).length>0&&(_.bulk_changes=n),f.length>0&&(_.items=f),Object.keys(s).length>0&&(_.option_bulk_changes=s),y.length>0&&(_.option_items=y),r.state.set({isProcessing:!0,bulkUpdateErrors:[]}),r.api.patch("/api/modules/sirsoft-ecommerce/admin/products/bulk-update",_).then(g=>{r.toast.success(r.t("sirsoft-ecommerce.admin.product.bulk.update_success",{products:g?.data?.products_updated||0,options:g?.data?.options_updated||0})),pt(r),mt(r),r.modal.close()}).catch(g=>{Y.error("[bulkUpdateProducts] Error:",g),dt(r,g,"sirsoft-ecommerce.admin.product.bulk.update_error")}).finally(()=>{r.state.set({isProcessing:!1})})}function Wr(r,c,e,t){const a=r.dataSource.get("products")?.data?.data||[],n={};if(c.bulkPriceMethod&&c.bulkPriceValue!==void 0&&c.bulkPriceValue!==null&&c.bulkPriceValue!==""){const d={fixed:"set",increase:"add",decrease:"subtract"};c.bulkPriceUnit==="percent"?n.price_adjustment={method:"percent",value:c.bulkPriceMethod==="decrease"?-(Number(c.bulkPriceValue)||0):Number(c.bulkPriceValue)||0}:n.price_adjustment={method:d[c.bulkPriceMethod]||"add",value:Number(c.bulkPriceValue)||0}}if(c.bulkStockMethod&&c.bulkStockValue!==void 0&&c.bulkStockValue!==null&&c.bulkStockValue!==""){const d={set:"set",increase:"add",decrease:"subtract"};n.stock_quantity={method:d[c.bulkStockMethod]||"add",value:Number(c.bulkStockValue)||0}}const s=e.modifiedOptionFields||{},i=e.modifiedOptionIds||[],l=[];a.forEach(d=>{(d.options||[]).forEach(m=>{const p=`${d.id}-${m.id}`,f=m._modified||i.includes(p);if(t.includes(p)&&f){const y={product_id:d.id,option_id:m.id},w=s[p]||[];!n.price_adjustment&&w.includes("price_adjustment")&&(y.price_adjustment=m.price_adjustment),!n.price_adjustment&&w.includes("selling_price")&&(y.price_adjustment=m.price_adjustment),!n.stock_quantity&&w.includes("stock_quantity")&&(y.stock_quantity=m.stock_quantity),w.includes("option_name")&&(y.option_name=m.option_name),w.includes("sku")&&(y.sku=m.sku),w.includes("list_price")&&(y.list_price=m.list_price),w.includes("safe_stock_quantity")&&(y.safe_stock_quantity=m.safe_stock_quantity),w.includes("is_default")&&(y.is_default=m.is_default),w.includes("is_active")&&(y.is_active=m.is_active),Object.keys(y).length>2&&l.push(y)}})});const u={ids:t};Object.keys(n).length>0&&(u.bulk_changes=n),l.length>0&&(u.items=l),r.state.set({isProcessing:!0,bulkUpdateErrors:[]}),r.api.patch("/api/modules/sirsoft-ecommerce/admin/options/bulk-update",u).then(d=>{r.toast.success(r.t("sirsoft-ecommerce.admin.product.bulk.option_update_success",{count:d?.data?.options_updated||0})),pt(r),mt(r),r.modal.close()}).catch(d=>{Y.error("[bulkUpdateOptions] Error:",d),dt(r,d,"sirsoft-ecommerce.admin.product.bulk.option_update_error")}).finally(()=>{r.state.set({isProcessing:!1})})}function mt(r){Promise.resolve(r.dataSource.refetch("products")).then(()=>{lo(r,{force:!0})}).catch(c=>{Y.error("[bulkUpdate] Failed to refetch products:",c)})}function pt(r){r.state.setLocal({modifiedProductIds:[],modifiedOptionIds:[],modifiedProductFields:{},modifiedOptionFields:{},selectedItems:[],selectedOptionIds:[]}),r.state.set({bulkSalesStatus:null,bulkDisplayStatus:null,bulkPriceCondition:null,bulkStockCondition:null,bulkConfirmData:null,bulkUpdateErrors:[]})}function Jr(r,c){const e=ut();if(!e)return;const t=e.state.getLocal()||{},o=e.state.get()||{},n=e.dataSource.get("products")?.data?.data||[],s=o.bulkSelectedItems||t.selectedItems||[],i=o.bulkSelectedOptionIds||t.selectedOptionIds||[],l={products:[],options:[],summary:{productCount:0,optionCount:0,hasBulkChanges:!1,hasInlineChanges:!1}},u=!!(o.bulkSalesStatus||o.bulkDisplayStatus||o.bulkPriceCondition||o.bulkStockCondition);l.summary.hasBulkChanges=u;const d=t.modifiedProductIds||[],m=t.modifiedOptionIds||[],p=t.modifiedProductFields||{},f=t.modifiedOptionFields||{},y=new Set(s);function w(_,g,b){const C=[],x=f[g]||[];if(o.bulkPriceCondition&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_price_adjustment_inline",{value:o.bulkPriceCondition})||`Price adjustment: ${o.bulkPriceCondition}`),o.bulkStockCondition&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_stock_inline",{value:o.bulkStockCondition})||`Stock: ${o.bulkStockCondition}`),b){if(!o.bulkPriceCondition&&x.includes("price_adjustment")){const P=_.price_adjustment?.toLocaleString?.()??_.price_adjustment;C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_price_adjustment_inline",{value:P})||`Price adjustment: ${P}`)}!o.bulkPriceCondition&&x.includes("selling_price")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selling_price",{price:_.selling_price?.toLocaleString?.()??_.selling_price})||`Selling price: ${_.selling_price}`),!o.bulkStockCondition&&x.includes("stock_quantity")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_stock_inline",{value:_.stock_quantity})||`Stock: ${_.stock_quantity}`),x.includes("option_name")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_option_name",{name:W(_.option_name)})||`Option name: ${W(_.option_name)}`),x.includes("sku")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sku",{sku:_.sku})||`SKU: ${_.sku}`),x.includes("list_price")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_list_price",{price:ue(Number(_.list_price??0))})||`List price: ${_.list_price}`),x.includes("safe_stock_quantity")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_safe_stock",{value:_.safe_stock_quantity})||`Safe stock: ${_.safe_stock_quantity}`),x.includes("is_active")&&C.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_is_active",{value:_.is_active?"ON":"OFF"})||`Active: ${_.is_active?"ON":"OFF"}`),l.summary.hasInlineChanges=!0}return C}s.length>0&&n.filter(_=>s.includes(_.id)).forEach(_=>{const g=[],b=_._modified||d.includes(_.id),C=String(_.id),x=p[C]||[];if(o.bulkSalesStatus){const S=ve(e,"sales_status",o.bulkSalesStatus);g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sales_status",{status:S})||`Sales status: ${S}`)}if(o.bulkDisplayStatus){const S=ve(e,"display_status",o.bulkDisplayStatus);g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_display_status",{status:S})||`Display status: ${S}`)}if(b){if(!o.bulkSalesStatus&&x.includes("sales_status")){const S=ve(e,"sales_status",_.sales_status);g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_sales_status",{status:S})||`Sales status: ${S}`)}if(!o.bulkDisplayStatus&&x.includes("display_status")){const S=ve(e,"display_status",_.display_status);g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_display_status",{status:S})||`Display status: ${S}`)}x.includes("name")&&g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_name_changed")||"Name changed"),x.includes("list_price")&&g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_list_price",{price:ue(Number(_.list_price??0))})||`List price: ${_.list_price?.toLocaleString?.()||_.list_price}`),x.includes("selling_price")&&g.push(e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selling_price",{price:ue(Number(_.selling_price??0))})||`Selling price: ${_.selling_price?.toLocaleString?.()||_.selling_price}`),l.summary.hasInlineChanges=!0}(g.length>0||u||b)&&l.products.push({id:_.id,name:W(_.name),changes:g.length>0?g.join(", "):b?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"});const P=u&&!!(o.bulkPriceCondition||o.bulkStockCondition);if(Array.isArray(_.options))_.options.forEach(S=>{const N=`${_.id}-${S.id}`,L=S._modified||m.includes(N),O=w(S,N,L);(O.length>0||P||L)&&l.options.push({productId:_.id,optionId:S.id,productName:W(_.name),optionName:S.option_name_localized||W(S.option_name)||S.name||"",changes:O.length>0?O.join(", "):L?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"})});else if(P){const S=Number(_.options_total_count)||0;S>0&&l.options.push({aggregate:!0,productId:_.id,optionId:0,optionCount:S,productName:W(_.name),optionName:"",changes:w({},`${_.id}-*`,!1).join(", ")||e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_bulk_applied")||"Bulk changes applied"})}}),i.length>0&&n.forEach(_=>{y.has(_.id)||(_.options||[]).forEach(g=>{const b=`${_.id}-${g.id}`;if(i.includes(b)){const C=g._modified||m.includes(b),x=w(g,b,C);l.options.push({productId:_.id,optionId:g.id,productName:W(_.name),optionName:g.option_name_localized||W(g.option_name)||g.name||"",changes:x.length>0?x.join(", "):C?e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_inline_modified")||"Inline modified":e.t("sirsoft-ecommerce.admin.product.messages.bulk_summary_selected")||"Selected"})}})}),l.summary.productCount=l.products.length,l.summary.optionCount=l.options.reduce((_,g)=>_+(g.aggregate?g.optionCount||0:1),0),e.state.set({bulkConfirmData:l}),Y.log("[buildConfirmData] Built confirm data:",l)}const M=window.G7Core?.createLogger?.("Ecom:Order")??{log:(...r)=>console.log("[Ecom:Order]",...r),warn:(...r)=>console.warn("[Ecom:Order]",...r),error:(...r)=>console.error("[Ecom:Order]",...r)};function Xr(r){const{currencies:c=[]}=r.params||{},e=[{field:"no",header:"NO",width:"60px",sortable:!1,required:!0},{field:"ordered_at",header:"$t:sirsoft-ecommerce.admin.order.column.order_date",width:"140px",sortable:!0,required:!0},{field:"order_number",header:"$t:sirsoft-ecommerce.admin.order.column.order_number",width:"140px",sortable:!0,required:!0},{field:"product",header:"$t:sirsoft-ecommerce.admin.order.column.product",width:"auto",sortable:!1,required:!0},{field:"order_amount",header:"$t:sirsoft-ecommerce.admin.order.column.order_amount",width:"120px",sortable:!0,required:!1},{field:"order_status",header:"$t:sirsoft-ecommerce.admin.order.column.order_status",width:"100px",sortable:!1,required:!1},{field:"orderer",header:"$t:sirsoft-ecommerce.admin.order.column.orderer",width:"120px",sortable:!1,required:!1},{field:"payment_method",header:"$t:sirsoft-ecommerce.admin.order.column.payment_method",width:"100px",sortable:!1,required:!1},{field:"delivery_method",header:"$t:sirsoft-ecommerce.admin.order.column.delivery_method",width:"100px",sortable:!1,required:!1},{field:"country",header:"$t:sirsoft-ecommerce.admin.order.column.country",width:"80px",sortable:!1,required:!1}],t=c.filter(o=>!o.is_default).map(o=>({field:`order_amount_${o.code.toLowerCase()}`,header:`${o.name} (${o.symbol})`,width:"100px",sortable:!0,required:!1}));return[...e,...t]}function Zr(r,c){const e=r.params||{},{target:t="_local",path:o,value:a}=e,n=window.G7Core;if(!n?.state)return M.warn("[toggleArrayValue] G7Core.state를 사용할 수 없습니다."),{success:!1,newValue:[]};const s=t==="_global"?n.state.getGlobal():n.state.getLocal(),i=o.split(".");let l=s;for(const d of i)l=l?.[d];Array.isArray(l)||(l=[]);let u;return l.includes(a)?u=l.filter(d=>d!==a):u=[...l,a],t==="_global"?n.state.setGlobal({[o]:u}):n.state.setLocal({[o]:u}),{success:!0,newValue:u}}function en(r,c){const e=r.params||{},{filterKey:t,checked:o}=e,a=window.G7Core;if(!a?.state)return M.warn("[toggleVisibleFilter] G7Core.state를 사용할 수 없습니다."),{success:!1,newValue:[]};const s=a.state.getLocal().visibleFilters||[];let i;o?i=s.includes(t)?s:[...s,t]:i=s.filter(l=>l!==t),a.state.setLocal({visibleFilters:i});try{localStorage.setItem("g7_order_filter_visible_filters",JSON.stringify(i))}catch(l){M.warn("[toggleVisibleFilter] LocalStorage 저장 실패:",l)}return{success:!0,newValue:i}}function tn(r,c){const e=r.params||{},{selectedIds:t=[]}=e,o=window.G7Core;return o?.state?(o.state.setLocal({selectedItems:t,selectAll:!1}),{success:!0}):(M.warn("[syncOrderSelection] G7Core.state를 사용할 수 없습니다."),{success:!1})}function on(r,c){const e=r.params||{},{actionId:t,row:o}=e,a=window.G7Core;if(!t||!o?.order_number)return M.warn("[handleOrderRowAction] actionId 또는 row.order_number가 없습니다."),{success:!1};switch(t){case"view":a?.navigate?.(`/admin/ecommerce/orders/${o.order_number}`);break;case"edit":a?.navigate?.(`/admin/ecommerce/orders/${o.order_number}`);break;case"status":case"shipping":case"memo":case"delete":a?.state?.setGlobal({orderList:{currentActionOrderId:o.id,currentActionType:t}});break;case"print":window.open(`/admin/ecommerce/orders/${o.order_number}/shipping-label`,"_blank");break;default:return console.warn(`[handleOrderRowAction] 알 수 없는 액션: ${t}`),{success:!1}}return{success:!0,action:t,targetId:o.id}}async function rn(r,c){const e=r.params||{},{actionType:t,orderIds:o,status:a,carrierId:n,trackingNumber:s}=e,i=window.G7Core;if(!o||o.length===0)throw new Error("선택된 주문이 없습니다.");let l="";const u={ids:o};switch(t){case"status":if(!a)throw new Error("변경할 상태를 선택해주세요.");l="/api/modules/sirsoft-ecommerce/admin/orders/bulk",u.order_status=a;break;case"shipping":if(!s&&!n)throw new Error("택배사 또는 운송장번호를 입력해주세요.");l="/api/modules/sirsoft-ecommerce/admin/orders/bulk",n&&(u.carrier_id=n),s&&(u.tracking_number=s);break;default:throw new Error(`알 수 없는 액션 타입: ${t}`)}const d=await i?.api?.call(l,{method:"PATCH",body:u});if(!d?.success)throw new Error(d?.message||"일괄 처리에 실패했습니다.");return{success:!0,successCount:d.data?.updated_count??o.length,failCount:d.data?.fail_count??0,errors:d.data?.errors??[]}}async function nn(r,c){const e=r.params||{},{downloadType:t,orderIds:o,filters:a}=e,n=window.G7Core,s="/api/modules/sirsoft-ecommerce/admin/orders/export",i={};if(t==="selected"){if(!o||o.length===0)throw new Error("선택된 주문이 없습니다.");i.ids=o}else i.filters=a||{};const l=n?.auth?.getToken(),u=await fetch(s,{method:"POST",headers:{"Content-Type":"application/json",...l?{Authorization:`Bearer ${l}`}:{}},body:JSON.stringify(i)});if(!u.ok){const w=await u.json().catch(()=>null);throw new Error(w?.message||"엑셀 다운로드에 실패했습니다.")}const d=await u.blob(),m=window.URL.createObjectURL(d),p=document.createElement("a");p.href=m;const f=new Date,y=`${f.getFullYear()}-${String(f.getMonth()+1).padStart(2,"0")}-${String(f.getDate()).padStart(2,"0")}`;return p.download=`orders_${y}.xlsx`,document.body.appendChild(p),p.click(),document.body.removeChild(p),window.URL.revokeObjectURL(m),{success:!0}}function an(r,c){const e=r.params||{},{columns:t=[]}=e;try{localStorage.setItem("g7_order_visible_columns",JSON.stringify(t))}catch(o){return M.warn("[saveVisibleColumns] LocalStorage 저장 실패:",o),{success:!1}}return{success:!0}}function sn(r,c){const t=(r.params||{}).defaultColumns||["order_date","order_number","order_status","order_amount","product","orderer","payment_method","delivery_method","country"];try{const o=localStorage.getItem("g7_order_visible_columns");if(o){const a=JSON.parse(o);if(Array.isArray(a))return a}}catch(o){M.warn("[loadVisibleColumns] LocalStorage 로드 실패:",o)}return t}function cn(r,c){const e=r.params||{},{id:t,defaultFilters:o=[]}=e,a=window.G7Core;if(!t)return M.warn("[loadVisibleFilters] id 파라미터가 필요합니다."),o;let n;try{const i=a?.AuthManager?.getInstance();i&&(n=i.getUser()?.uuid)}catch{}const s=`g7_filters_${t}${n!==void 0?`_${n}`:""}`;try{const i=localStorage.getItem(s);if(i){const l=JSON.parse(i);if(Array.isArray(l)&&l.length>0)return M.log(`[loadVisibleFilters] localStorage에서 로드: ${s}`,l),l}}catch(i){M.warn("[loadVisibleFilters] LocalStorage 로드 실패:",i)}return M.log(`[loadVisibleFilters] 기본값 사용: ${s}`,o),o}function ln(r,c){const e=window.G7Core;if(!e)return;const t=e.state.get()||{},o=t.bulkSelectedItems||[],a=t.bulkOrderStatus||"",n=t.bulkCourier||"",s=t.bulkTrackingNumber||"";if(!a){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_status_selected")||"변경할 주문상태를 선택해주세요.");return}if(["shipping_ready","shipping"].includes(a)&&(!n||!s)){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.carrier_required")||"해당 상태로 변경하려면 택배사와 송장번호를 모두 입력해주세요.");return}e.state.set({bulkConfirmData:{selectedItems:o,orderStatus:a,courierId:n,trackingNumber:s}}),e.modal?.open?.("modal_bulk_confirm")}async function dn(r,c){const e=window.G7Core;if(!e)return;const o=(e.state.get()||{}).bulkConfirmData;if(!o||!o.selectedItems?.length){e.toast?.error?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.error")||"일괄 처리에 실패했습니다.");return}const a={ids:o.selectedItems,order_status:o.orderStatus};o.courierId&&(a.carrier_id=o.courierId),o.trackingNumber&&(a.tracking_number=o.trackingNumber);try{await e.api.patch("/api/modules/sirsoft-ecommerce/admin/orders/bulk",a),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.success")||"일괄 변경이 완료되었습니다."),e.state.setLocal({selectedItems:[],selectAll:!1,bulkOrderStatus:"",bulkCourier:"",bulkTrackingNumber:""}),e.state.set({bulkConfirmData:null}),e.dataSource?.refetch?.("orders"),e.modal?.close?.()}catch(n){M.error("[executeOrderBulkAction] Error:",n);const i=(n?.response?.data||n?.data||{})?.message||n?.message;e.toast?.error?.(i||e.t?.("sirsoft-ecommerce.admin.order.bulk.error")||"일괄 처리에 실패했습니다.")}}const Ue=window.G7Core?.createLogger?.("Ecom:ProductList")??{log:(...r)=>console.log("[Ecom:ProductList]",...r),warn:(...r)=>console.warn("[Ecom:ProductList]",...r),error:(...r)=>console.error("[Ecom:ProductList]",...r)};async function un(r,c){const e=r.params||{},{actionId:t,row:o}=e,a=window.G7Core;if(!t||!o?.id)return Ue.warn("[handleProductRowAction] actionId 또는 row.id가 없습니다."),{success:!1};switch(t){case"edit":a?.navigate?.(`/admin/ecommerce/products/${o.product_code}/edit`);break;case"copy":a?.state?.setLocal({productList:{copyTargetProduct:o},ui:{showCopyModal:!0}});break;case"delete":try{const s=(await a?.api?.get(`/api/modules/sirsoft-ecommerce/admin/products/${o.id}/can-delete`))?.data;a?.state?.setLocal({productList:{deleteTargetProduct:o,canDelete:s?.canDelete??!0,deleteBlockReason:s?.reason??null,relatedData:s?.relatedData??null},ui:{showDeleteModal:!0}})}catch(n){Ue.error("[handleProductRowAction] 삭제 가능 여부 확인 실패:",n),a?.state?.setLocal({productList:{deleteTargetProduct:o,canDelete:!1,deleteBlockReason:"삭제 가능 여부를 확인할 수 없습니다.",relatedData:null},ui:{showDeleteModal:!0}})}break;default:return Ue.warn(`[handleProductRowAction] 알 수 없는 액션: ${t}`),{success:!1}}return{success:!0,action:t,targetId:o.id}}const U=window.G7Core?.createLogger?.("Ecom:OrderDetail")??{log:(...r)=>console.log("[Ecom:OrderDetail]",...r),warn:(...r)=>console.warn("[Ecom:OrderDetail]",...r),error:(...r)=>console.error("[Ecom:OrderDetail]",...r)};function ft(){return typeof document>"u"?"":document.querySelector('input[name="batchTrackingNumber"]')?.value?.trim()??""}function mn(r,c){const e=window.G7Core;if(!e?.state){U.warn("[initOrderDetailForm] G7Core.state를 사용할 수 없습니다.");return}e.state.getLocal();const o=c?.data?.data??e.dataSource?.get?.("order")?.data;if(!o){U.warn("[initOrderDetailForm] order 데이터가 없습니다.");return}e.state.setLocal({form:{recipient_name:o.recipient_name||"",recipient_phone:o.recipient_phone||"",recipient_tel:o.recipient_tel||"",recipient_zipcode:o.recipient_zipcode||"",recipient_address:o.recipient_address||"",recipient_detail_address:o.recipient_detail_address||"",delivery_memo:o.delivery_memo||"",admin_memo:o.admin_memo||""}}),U.log("[initOrderDetailForm] 폼 초기화 완료")}function pn(r,c){const e=window.G7Core,{optionId:t}=r.params||{};if(!e?.state||!t)return;const a=e.state.getLocal().selectedProducts||[];let n;a.includes(t)?n=a.filter(u=>u!==t):n=[...a,t];const i=(e.dataSource?.get?.("order")?.data?.options||[]).length,l=n.length===i&&i>0;e.state.setLocal({selectedProducts:n,selectAll:l})}function fn(r,c){const e=window.G7Core;if(!e?.state)return;const o=e.state.getLocal().selectAll,n=e.dataSource?.get?.("order")?.data?.options||[];if(o)e.state.setLocal({selectedProducts:[],selectAll:!1});else{const s=n.map(i=>i.id);e.state.setLocal({selectedProducts:s,selectAll:!0})}}function gn(r,c){const e=window.G7Core;if(!e)return;const t=r.params||{},o=t.selectedProducts||[],a=t.batchOrderStatus||"",n=t.batchCarrierId||"",s=t.batchTrackingNumber||ft();if(!a){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_status_selected")||"변경할 주문상태를 선택해주세요.");return}if(o.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_items_selected")||"변경할 상품을 선택해주세요.");return}if(a==="cancelled"){const p=t.cancelPg??!0,f=e.dataSource?.get?.("order")?.data,y=f?.options||[],w=f?.currency_snapshot?.base_currency??null,_=o.map(g=>{const b=y.find(x=>x.id===g);if(!b||b.option_status==="cancelled")return null;const C=b.quantity??1;return{id:b.id,product_name:b.product_name,product_option_name:b.product_option_name,thumbnail_url:b.product_snapshot?.thumbnail_url||"",unit_price:b.unit_price,unit_price_formatted:b.unit_price_formatted??ue(Number(b.unit_price??0),w),quantity:C,subtotal_formatted:ue(Number(b.unit_price??0)*C,w),cancel_quantity:C,option_status:b.option_status,option_status_label:b.option_status_label||b.option_status}}).filter(Boolean);if(_.length===0){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.no_cancellable_items")||"취소할 수 있는 상품이 없습니다. 이미 취소된 상품은 제외됩니다.");return}if(p){e.state.setLocal({cancelItems:_,cancelReason:"",cancelReasonDetail:"",cancelPg:!0,refundPriority:"pg_first",refundEstimate:null,refundLoading:!1,isCancelling:!1,cancelError:null,cancelValidationErrors:null}),e.modal?.open?.("modal_cancel_order");const g=f?.order_number||"";g&&e.dispatch?.({handler:"sirsoft-ecommerce.estimateRefundAmount",params:{orderId:g,cancelItems:_}})}else{const g={};for(const b of _)g[b.id]=b.quantity;e.state.setLocal({bulkConfirmItems:_,changeQuantities:g,batchOrderStatus:a}),e.modal?.open?.("modal_batch_change_confirm")}return}if(["shipping_ready","shipping","delivered"].includes(a)&&(!n||!s)){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.order.bulk.carrier_required")||"해당 상태로 변경하려면 택배사와 송장번호를 모두 입력해주세요.");return}const u=e.dataSource?.get?.("order")?.data?.options||[],d=[],m={};for(const p of o){const f=u.find(y=>y.id===p);f&&(d.push({id:f.id,product_name:f.product_name,product_option_name:f.product_option_name,sku:f.sku,thumbnail_url:f.product_snapshot?.thumbnail_url||"",unit_price:f.unit_price,original_price:f.product_snapshot?.original_price||f.unit_price,quantity:f.quantity,option_status:f.option_status}),m[f.id]=f.quantity)}e.state.setLocal({bulkConfirmItems:d,changeQuantities:m,batchOrderStatus:a,batchCarrierId:n,batchTrackingNumber:s}),e.modal?.open?.("modal_batch_change_confirm")}async function _n(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t){U.warn("[processOrderDetailBulkChange] orderId가 없습니다.");return}const o=e.state.getLocal?.()??{},a=r.params?.selectedProducts??o.selectedProducts,n=r.params?.batchOrderStatus||o.batchOrderStatus,s=r.params?.batchCarrierId||o.batchCarrierId,i=r.params?.batchTrackingNumber||o.batchTrackingNumber||ft(),l=r.params?.changeQuantities??o.changeQuantities;if(!a||a.length===0){U.warn("[processOrderDetailBulkChange] 선택된 상품이 없습니다.");return}if(!n){U.warn("[processOrderDetailBulkChange] 변경할 상태가 없습니다.");return}const d=e.dataSource?.get?.("order")?.data?.options||[],p={items:a.map(f=>{const y=d.find(w=>w.id===f);return{option_id:f,quantity:l?.[f]??y?.quantity??1}}),status:n};s&&(p.carrier_id=s),i&&(p.tracking_number=i);try{const f=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/options/bulk-status`,p);if(f?.success){const y=e.t,w=f.data?.changed_count??a.length;e.toast?.success?.(y?.("sirsoft-ecommerce.admin.order.detail.handler.bulk_change_success",{count:w})??`${w}개 옵션의 상태가 변경되었습니다.`),e.modal?.close?.("modal_batch_change_confirm"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}}),e.state.setLocal({selectedProducts:[],selectAll:!1,batchOrderStatus:"",batchCarrierId:"",batchTrackingNumber:"",changeQuantities:{}})}else throw new Error(f?.message||"상태 변경에 실패했습니다.")}catch(f){U.error("[processOrderDetailBulkChange] API 호출 실패:",f);const w=(f?.response?.data||f?.data||{})?.message||f?.message;e.toast?.error?.(w??e.t?.("sirsoft-ecommerce.admin.order.detail.handler.bulk_change_failed")??"상태 변경에 실패했습니다.")}}async function yn(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t)return;const a=e.state.getLocal().form?.admin_memo??"";try{const n=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}`,{admin_memo:a});if(n?.success)e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.handler.memo_save_success")??"관리자 메모가 저장되었습니다.");else throw new Error(n?.message)}catch(n){U.error("[saveAdminMemo] 저장 실패:",n),e.toast?.error?.(n?.message??"메모 저장에 실패했습니다.")}}function wn(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o}=r.params||{};if(!t||!o)return;const a=r.params?.value??r.$event?.target?.value,n=parseInt(String(a),10),s=Math.max(1,Math.min(isNaN(n)?o:n,o)),l={...e.state.getLocal().changeQuantities||{}};l[t]=s,e.state.setLocal({changeQuantities:l})}function hn(r,c){const e=window.G7Core;if(!e?.state){U.warn("[openConfirmDepositModal] G7Core.state 를 사용할 수 없습니다.");return}const t=e.dataSource?.get?.("order")?.data,o=t?.depositor_name??"",a=t?.total_due_charge_amount??t?.total_due_amount??0;e.state.setLocal({depositorName:o,depositAmount:a,depositConfirmErrors:null,isConfirmingDeposit:!1,markOrderComplete:!1}),e.modal?.open?.("modal_confirm_deposit")}async function Cn(r,c){const e=window.G7Core,{orderId:t}=r.params||{};if(!e?.state||!t){U.warn("[confirmDeposit] orderId 가 없습니다.");return}const o=e.state.getLocal(),a=r.params?.amount??o.depositAmount,n=r.params?.depositorName??o.depositorName??"",s=r.params?.markOrderComplete??o.markOrderComplete??!1,i=s===!0||s==="true";e.state.setLocal({isConfirmingDeposit:!0,depositConfirmErrors:null});try{const l=await e.api.patch(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/confirm-deposit`,{amount:a,depositor_name:n,mark_order_complete:i});if(l?.success)e.state.setLocal({isConfirmingDeposit:!1,depositConfirmErrors:null}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.modal.confirm_deposit.confirm_success")??"입금이 확인되어 결제완료 처리되었습니다."),e.modal?.close?.("modal_confirm_deposit"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}});else throw new Error(l?.message||"Deposit confirm failed")}catch(l){U.error("[confirmDeposit] 실패:",l);const u=l?.response?.data||l?.data||{},d=u?.errors?.detail||u?.message||l?.message||"",m=e.t?.("sirsoft-ecommerce.admin.order.detail.modal.confirm_deposit.confirm_failed")??"입금 확인에 실패했습니다.";e.state.setLocal({isConfirmingDeposit:!1,depositConfirmErrors:{amount:[d||m]}}),e.toast?.error?.(d||m)}}const Ve=window.G7Core?.createLogger?.("Ecom:CancelOrder")??{log:(...r)=>console.log("[Ecom:CancelOrder]",...r),warn:(...r)=>console.warn("[Ecom:CancelOrder]",...r),error:(...r)=>console.error("[Ecom:CancelOrder]",...r)};let K=null;function bn(){K&&(clearTimeout(K),K=null,Ve.log("[clearCancelOrderTimers] debounce 타이머 정리 완료"))}function In(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o,orderId:a}=r.params||{};if(!t||!o||!a)return;const n=r.params?.value??r.$event?.target?.value,s=parseInt(String(n),10),i=Math.max(1,Math.min(isNaN(s)?o:s,o)),u=(e.state.getLocal().cancelItems||[]).map(m=>m.id===t?{...m,cancel_quantity:i}:m);e.state.setLocal({cancelItems:u});const d=window.__g7ActionContext;K&&clearTimeout(K),K=setTimeout(()=>{const m=window.__g7ActionContext;window.__g7ActionContext=d;try{je(e,a)}finally{window.__g7ActionContext=m}},500)}async function Sn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,cancelItems:o}=r.params||{};t&&await je(e,t,o)}async function je(r,c,e){const t=r.state.getLocal(),o=e||t.cancelItems||[],a=t.refundPriority||"pg_first";if(o.length===0){r.state.setLocal({refundEstimate:null});return}const n=o.map(s=>({order_option_id:s.id,cancel_quantity:s.cancel_quantity}));r.state.setLocal({refundLoading:!0});try{const s=await r.api.post(`/api/modules/sirsoft-ecommerce/admin/orders/${c}/estimate-refund`,{items:n,refund_priority:a});if(s?.success)r.state.setLocal({refundEstimate:s.data,refundLoading:!1,cancelError:null});else throw new Error(s?.message||"Estimate failed")}catch(s){Ve.error("[estimateRefundAmount] 실패:",s),r.state.setLocal({refundLoading:!1,refundEstimate:null}),r.toast?.error?.(r.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.estimate_failed")??"환불 예상금액 계산에 실패했습니다.")}}function vn(r,c){const e=window.G7Core;if(!e?.state)return;const{priority:t,orderId:o}=r.params||{};if(!t||!o)return;e.state.setLocal({refundPriority:t});const a=window.__g7ActionContext;K&&clearTimeout(K),K=setTimeout(()=>{const n=window.__g7ActionContext;window.__g7ActionContext=a;try{je(e,o)}finally{window.__g7ActionContext=n}},300)}async function Ln(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};if(!t)return;const o=e.state.getLocal(),a=r.params?.cancelItems||o.cancelItems||[],n=r.params?.cancelReason||o.cancelReason||"",s=r.params?.cancelReasonDetail||o.cancelReasonDetail||"",i=r.params?.cancelPg??o.cancelPg??!0,l=r.params?.refundPriority||o.refundPriority||"pg_first",u={type:"partial",cancel_pg:i,refund_priority:l,items:a.map(m=>({order_option_id:m.id,cancel_quantity:m.cancel_quantity}))};n&&(u.reason=n),s&&(u.reason_detail=s);const d={bank_code:o.refundBankCode||"",account_number:o.refundBankAccount||"",holder:o.refundBankHolder||""};(d.bank_code||d.account_number||d.holder)&&(u.refund_bank=d),e.state.setLocal({isCancelling:!0,cancelError:null,cancelValidationErrors:null});try{const m=await e.api.post(`/api/modules/sirsoft-ecommerce/admin/orders/${t}/cancel`,u);if(m?.success)e.state.setLocal({isCancelling:!1}),e.toast?.success?.(e.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_success")??"주문이 취소되었습니다."),e.modal?.close?.("modal_cancel_order"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order_logs"}}),e.state.setLocal({selectedProducts:[],selectAll:!1,batchOrderStatus:""});else throw new Error(m?.message||"Cancel failed")}catch(m){Ve.error("[executeCancelOrder] 실패:",m);const p=m?.response?.data||m?.data,y=(m?.response?.status||m?.status)===422,w=p?.errors?.detail,_=p?.errors??null,g=w||p?.message||m?.message||"",b=e.t?.("sirsoft-ecommerce.admin.order.detail.modal.cancel.cancel_failed")??"주문 취소에 실패했습니다.";y?(e.state.setLocal({isCancelling:!1,cancelError:null,cancelValidationErrors:_}),e.toast?.error?.(g||b)):(e.state.setLocal({isCancelling:!1,cancelError:g||b,cancelValidationErrors:null}),e.toast?.error?.(g||b))}}const gt=window.G7Core?.createLogger?.("Ecom:UserCancelOrder")??{log:(...r)=>console.log("[Ecom:UserCancelOrder]",...r),warn:(...r)=>console.warn("[Ecom:UserCancelOrder]",...r),error:(...r)=>console.error("[Ecom:UserCancelOrder]",...r)};let R=null;function Le(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}function xn(){R&&(clearTimeout(R),R=null)}function Pn(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t}=r.params||{};if(!t)return;const a=[...e.state.getLocal().selectedItemIds||[]],n=a.indexOf(t);n>=0?a.splice(n,1):a.push(t);const i=(e.dataSource?.get?.("order")?.data?.options||[]).filter(u=>u.option_status!=="cancelled").map(u=>u.id),l=i.length>0&&i.every(u=>a.includes(u));e.state.setLocal({selectedItemIds:a,selectAllItems:l})}function An(r,c){const e=window.G7Core;if(!e?.state)return;const t=e.state.getLocal(),a=(e.dataSource?.get?.("order")?.data?.options||[]).filter(s=>s.option_status!=="cancelled").map(s=>s.id),n=!t.selectAllItems;e.state.setLocal({selectedItemIds:n?a:[],selectAllItems:n})}function kn(r,c){const e=window.G7Core;if(!e?.state)return;const t=e.dataSource?.get?.("order")?.data;if(!t)return;const a=e.state.getLocal().selectedItemIds||[];if(a.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_selected_items")??"취소할 상품을 선택해주세요.");return}const s=(t.options||[]).filter(l=>a.includes(l.id)&&l.option_status!=="cancelled").map(l=>({id:l.id,product_name:l.product_name??"",product_option_name:l.product_option_name??"",thumbnail_url:l.product_snapshot?.thumbnail_url||"",unit_price:l.unit_price??0,quantity:l.quantity??1,cancel_quantity:l.quantity??1,option_status:l.option_status??"",selected:!0}));if(s.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_cancellable_items")??"취소 가능한 상품이 없습니다.");return}e.state.setLocal({cancelItems:s,cancelSelectAll:!0,cancelReason:"",refundEstimate:null,refundLoading:!1,isCancelling:!1,cancelError:null}),e.modal?.open?.("modal_cancel_order");const i=r.params?.orderId||t.id;(i||t.order_number)&&le(e,i)}function On(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,orderId:o}=r.params||{};if(!t)return;const n=(e.state.getLocal().cancelItems||[]).map(i=>i.id===t?{...i,selected:!i.selected}:i),s=n.every(i=>i.selected);e.state.setLocal({cancelItems:n,cancelSelectAll:s}),R&&clearTimeout(R),R=setTimeout(()=>{le(e,o)},500)}function Gn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{},o=e.state.getLocal(),a=!o.cancelSelectAll,n=(o.cancelItems||[]).map(s=>({...s,selected:a}));e.state.setLocal({cancelItems:n,cancelSelectAll:a}),R&&clearTimeout(R),R=setTimeout(()=>{le(e,t)},500)}function En(r,c){const e=window.G7Core;if(!e?.state)return;const{optionId:t,maxQuantity:o,orderId:a}=r.params||{};if(!t||!o||!a&&!Le(e))return;const n=r.params?.value??r.$event?.target?.value,s=parseInt(String(n),10),i=Math.max(1,Math.min(isNaN(s)?o:s,o)),u=(e.state.getLocal().cancelItems||[]).map(m=>m.id===t?{...m,cancel_quantity:i}:m);e.state.setLocal({cancelItems:u});const d=window.__g7ActionContext;R&&clearTimeout(R),R=setTimeout(()=>{const m=window.__g7ActionContext;window.__g7ActionContext=d;try{le(e,a)}finally{window.__g7ActionContext=m}},500)}async function Dn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};!t&&!Le(e)||await le(e,t)}async function le(r,c){const e=r.state.getLocal(),t=e.cancelItems||[],o=e.refundPriority||"pg_first",a=t.filter(s=>s.selected);if(a.length===0){r.state.setLocal({refundEstimate:null,refundLoading:!1});return}const n=a.map(s=>({order_option_id:s.id,cancel_quantity:s.cancel_quantity}));r.state.setLocal({refundLoading:!0});try{const s=r.state.get?.("_global")?.guestOrderToken,i=r.dataSource?.get?.("order")?.data?.order_number,l=!!(s&&i),u=l?`/api/modules/sirsoft-ecommerce/guest/orders/${i}/estimate-refund`:`/api/modules/sirsoft-ecommerce/user/orders/${c}/estimate-refund`,d=l?{headers:{"X-Guest-Order-Token":s}}:void 0,m=await r.api.post(u,{items:n,refund_priority:o},d);if(m?.success)r.state.setLocal({refundEstimate:m.data,refundLoading:!1,cancelError:null});else throw new Error(m?.message||"Estimate failed")}catch(s){gt.error("[estimateUserRefund] 실패:",s),r.state.setLocal({refundLoading:!1,refundEstimate:null}),r.toast?.error?.(r.t?.("mypage.order_detail.cancel_modal.estimate_failed")??"환불 예상금액 계산에 실패했습니다.")}}function Fn(r,c){const e=window.G7Core;if(!e?.state)return;const{priority:t,orderId:o}=r.params||{};if(!t||!o&&!Le(e))return;e.state.setLocal({refundPriority:t});const a=window.__g7ActionContext;R&&clearTimeout(R),R=setTimeout(()=>{const n=window.__g7ActionContext;window.__g7ActionContext=a;try{le(e,o)}finally{window.__g7ActionContext=n}},300)}async function $n(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{};if(!t&&!Le(e))return;const o=e.state.getLocal(),n=(o.cancelItems||[]).filter(d=>d.selected),s=r.params?.cancelReason||o.cancelReason||"",i=r.params?.cancelReasonDetail??o.cancelReasonDetail??"",l=r.params?.refundPriority||o.refundPriority||"pg_first";if(n.length===0){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.no_selected_items")??"취소할 상품을 선택해주세요.");return}if(!s){e.toast?.warning?.(e.t?.("mypage.order_detail.cancel_modal.reason_required")??"취소 사유를 선택해주세요.");return}const u={reason:s,refund_priority:l,items:n.map(d=>({order_option_id:d.id,cancel_quantity:d.cancel_quantity}))};i&&(u.reason_detail=i),e.state.setLocal({isCancelling:!0,cancelError:null,cancelValidationErrors:null});try{const d=e.state.get?.("_global")?.guestOrderToken,m=e.dataSource?.get?.("order")?.data?.order_number,p=!!(d&&m),f=p?`/api/modules/sirsoft-ecommerce/guest/orders/${m}/cancel`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/cancel`,y=p?{headers:{"X-Guest-Order-Token":d}}:void 0,w=await e.api.post(f,u,y);if(w?.success)e.state.setLocal({isCancelling:!1}),e.toast?.success?.(e.t?.("mypage.order_detail.cancel_success")??"주문이 취소되었습니다."),e.modal?.close?.("modal_cancel_order"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}});else throw new Error(w?.message||"Cancel failed")}catch(d){gt.error("[executeUserCancelOrder] 실패:",d);const m=d?.response?.data||d?.data,p=m?.message||d?.message||"",f=m?.errors??null,y=e.t?.("mypage.order_detail.cancel_modal.cancel_failed")??"주문 취소에 실패했습니다.";e.state.setLocal({isCancelling:!1,cancelError:p||y,cancelValidationErrors:f}),e.toast?.error?.(p||y)}}const _t=window.G7Core?.createLogger?.("Ecom:ConfirmOrder")??{log:(...r)=>console.log("[Ecom:ConfirmOrder]",...r),warn:(...r)=>console.warn("[Ecom:ConfirmOrder]",...r),error:(...r)=>console.error("[Ecom:ConfirmOrder]",...r)};function Nn(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}async function Tn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,optionId:o}=r.params||{};if(!o||!t&&!Nn(e)){_t.error("[confirmOrderOption] optionId 또는 식별자 누락");return}e.state.setLocal({isConfirming:!0});try{const a=e.state.get?.("_global")?.guestOrderToken,n=e.dataSource?.get?.("order")?.data?.order_number,s=!!(a&&n),i=s?`/api/modules/sirsoft-ecommerce/guest/orders/${n}/options/${o}/confirm`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/options/${o}/confirm`,l=s?{headers:{"X-Guest-Order-Token":a}}:void 0,u=await e.api.post(i,void 0,l);if(u?.success)e.state.setLocal({isConfirming:!1,confirmTarget:null}),e.modal?.close?.("modal_confirm_purchase"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(u.message??e.t?.("sirsoft-ecommerce::messages.order.confirmed")??"구매확정이 완료되었습니다.");else throw new Error(u?.message||"Confirm failed")}catch(a){_t.error("[confirmOrderOption] 실패:",a);const n=a?.response?.data?.message||a?.data?.message||a?.message||e.t?.("sirsoft-ecommerce::messages.order.cannot_confirm")||"구매확정에 실패했습니다.";e.state.setLocal({isConfirming:!1}),e.toast?.error?.(n)}}const yt=window.G7Core?.createLogger?.("Ecom:ChangeShippingAddress")??{log:(...r)=>console.log("[Ecom:ChangeShippingAddress]",...r),warn:(...r)=>console.warn("[Ecom:ChangeShippingAddress]",...r),error:(...r)=>console.error("[Ecom:ChangeShippingAddress]",...r)};function Rn(r){const c=r?.state?.get?.("_global")?.guestOrderToken,e=r?.dataSource?.get?.("order")?.data?.order_number;return!!(c&&e)}async function Mn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t}=r.params||{},o=Rn(e);if(!t&&!o){yt.error("[changeShippingAddress] orderId 또는 비회원 식별자 누락");return}const a=e.state.get?.("_global")??{},s=(o?"manual":a.changeAddressMode??"saved")==="saved"?{address_id:a.selectedAddressId}:a.editingShippingAddress;e.state.setLocal({isSubmittingAddress:!0});try{const i=a.guestOrderToken,l=e.dataSource?.get?.("order")?.data?.order_number,u=o?`/api/modules/sirsoft-ecommerce/guest/orders/${l}/shipping-address`:`/api/modules/sirsoft-ecommerce/user/orders/${t}/shipping-address`,d=o?{headers:{"X-Guest-Order-Token":i}}:void 0,m=await e.api.put(u,s,d);if(m?.success)e.state.setLocal({isSubmittingAddress:!1}),e.modal?.close?.("changeAddressModal"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(m.message??e.t?.("mypage.order_detail.shipping_address_changed")??"배송지가 변경되었습니다.");else throw new Error(m?.message||"Change shipping address failed")}catch(i){yt.error("[changeShippingAddress] 실패:",i),e.state.setLocal({isSubmittingAddress:!1});const l=i?.response?.data?.message||i?.data?.message||i?.message||"배송지 변경에 실패했습니다.";e.toast?.error?.(l)}}const Be=window.G7Core?.createLogger?.("Ecom:Review")??{log:(...r)=>console.log("[Ecom:Review]",...r),warn:(...r)=>console.warn("[Ecom:Review]",...r),error:(...r)=>console.error("[Ecom:Review]",...r)};async function Hn(r,c){const e=window.G7Core;if(!e?.state)return;const{orderId:t,optionId:o,productId:a}=r.params||{};if(!t||!o||!a){Be.error("[submitReview] orderId, optionId 또는 productId 누락");return}const n=e.state.getLocal(),s=r.params?.rating??n.reviewRating??5,i=(r.params?.content??n.reviewContent??"").trim(),u=(r.params?.images??n.reviewImages??[]).map(d=>d instanceof File?d:d?.file instanceof File||d?.file instanceof Blob||d?.file&&typeof d.file.name=="string"&&d.file.size>0?d.file:d instanceof Blob?d:null).filter(d=>d!==null);if(!s||s<1||s>5){e.toast?.error?.(e.t?.("sirsoft-ecommerce.user.review.validation.rating_required")??"별점을 선택해 주세요.");return}if(i.length<10){e.state.setLocal({reviewError:e.t?.("sirsoft-ecommerce.user.review.validation.content_min")??"리뷰 내용을 최소 10자 이상 입력해 주세요."});return}e.state.setLocal({isSubmittingReview:!0,reviewError:null});try{const d=await e.api.post("/api/modules/sirsoft-ecommerce/user/reviews",{order_option_id:o,product_id:a,rating:s,content:i});if(!d?.success)throw new Error(d?.message||"Review creation failed");const m=d.data?.id;if(m&&u.length>0)for(let p=0;p<u.length;p++){const f=new FormData;f.append("image",u[p]),f.append("sort_order",String(p));try{await e.api.post(`/api/modules/sirsoft-ecommerce/user/reviews/${m}/images`,f,{headers:{"Content-Type":"multipart/form-data"}})}catch(y){Be.warn(`[submitReview] 이미지 ${p+1} 업로드 실패:`,y),e.toast?.warning?.(e.t?.("sirsoft-ecommerce.user.review.image_upload_partial_fail")??`이미지 ${p+1}번 업로드에 실패했습니다.`)}}e.state.setLocal({isSubmittingReview:!1,reviewTarget:null,reviewRating:5,reviewContent:"",reviewImages:[],reviewError:null}),e.modal?.close?.("modal_write_review"),e.dispatch?.({handler:"refetchDataSource",params:{dataSourceId:"order"}}),e.toast?.success?.(d.message??e.t?.("sirsoft-ecommerce.user.review.submit_success")??"리뷰가 등록되었습니다.")}catch(d){Be.error("[submitReview] 실패:",d);const m=d?.response?.data||d?.data,p=d?.response?.status||d?.status,f=m?.message||d?.message||e.t?.("sirsoft-ecommerce.user.review.submit_failed")||"리뷰 등록에 실패했습니다.";if(p===422){const y=m?.errors??{},w=Object.values(y).flat()[0];e.state.setLocal({isSubmittingReview:!1,reviewError:w||f})}else e.state.setLocal({isSubmittingReview:!1});e.toast?.error?.(f)}}const I=window.G7Core?.createLogger?.("Ecom:ShippingPolicyForm")??{log:(...r)=>console.log("[Ecom:ShippingPolicyForm]",...r),warn:(...r)=>console.warn("[Ecom:ShippingPolicyForm]",...r),error:(...r)=>console.error("[Ecom:ShippingPolicyForm]",...r)},qn=["fixed","conditional_free","per_quantity","per_weight","per_volume","per_volume_weight","per_amount"],wt=["conditional_free"],ht=["range_amount","range_quantity","range_weight","range_volume","range_volume_weight"],Un=["api"],Ct=["per_quantity","per_weight","per_volume","per_volume_weight","per_amount"],Vn=["range_quantity"],jn=.001;function bt(r){return Vn.includes(r)}function Bn(r,c){return r==null||Number.isNaN(r)?null:bt(c)?r+1:r}function xe(r,c){return r.map((e,t)=>({...e,min:t===0?0:Bn(r[t-1]?.max,c)}))}const Kn={shipping_method:"parcel",custom_shipping_name:null,carrier:null,currency_code:"KRW",charge_policy:"fixed",base_fee:0,free_threshold:null,ranges:null,api_endpoint:null,api_request_fields:null,api_response_fee_field:null,api_config:null,extra_fee_enabled:!1,extra_fee_settings:[],extra_fee_multiply:!1,is_active:!0};function te(r){return{showBaseFee:qn.includes(r),showFreeThreshold:wt.includes(r),showRanges:ht.includes(r),showApiSettings:Un.includes(r),showUnitValue:Ct.includes(r)}}function G(){const r=window.G7Core;return r?.state?r:(I.warn("G7Core.state not available"),null)}function E(r,c){r.setLocalState?r.setLocalState(c):G()?.state?.setLocal(c)}function D(r){return[...(r.state.getLocal?.()??{}).form?.country_settings??[]]}function Qn(r,c){const e=r.params||{},t=e.policy,o=!!e.isEdit,a=!!e.isCopy;if(e.availableCountries,!G())return;const s={activeCountryTab:0,rangeErrors:{}};if((o||a)&&t){const i=t.country_settings??[],u=i[0]?.charge_policy??"fixed",d=te(u);Object.assign(s,d);const m=i.map(p=>{const f=p?.ranges?.tiers;return!Array.isArray(f)||f.length===0?p:{...p,ranges:{...p.ranges,tiers:xe(f,p.charge_policy)}}});s["form.country_settings"]=m,I.log("[initShippingPolicyForm]",o?"Edit":"Copy","mode, countries:",i.length,"first charge_policy:",u)}else{const i=te("fixed");Object.assign(s,i),I.log("[initShippingPolicyForm] Create mode, default flags")}E(c,s)}function zn(r,c){const t=(r.params||{}).country_code??"";if(!t){I.warn("[addCountrySetting] country_code is empty");return}const o=G();if(!o)return;const a=D(o);if(a.some(u=>u.country_code===t)){I.warn("[addCountrySetting] Duplicate country_code:",t);return}const n={country_code:t,...Kn};a.push(n);const s=a.length-1,i=te(n.charge_policy),l={"form.country_settings":a,activeCountryTab:s,...i};E(c,l),I.log("[addCountrySetting] Added country:",t,"index:",s)}function Yn(r,c){const e=r.params||{},t=Number(e.index);if(isNaN(t)||t<0){I.warn("[removeCountrySetting] Invalid index:",e.index);return}const o=G();if(!o)return;const a=D(o),n=o.state.getLocal?.()??{};if(t>=a.length){I.warn("[removeCountrySetting] Index out of bounds:",t);return}const s=a[t].country_code;a.splice(t,1);const i=n.activeCountryTab??0;let l=i;a.length===0?l=0:i>=a.length?l=a.length-1:i>t&&(l=i-1);const u={...n.rangeErrors??{}};u[s]=[];const d=a[l],m=te(d?d.charge_policy:"fixed"),p={"form.country_settings":a,activeCountryTab:l,rangeErrors:u,...m};E(c,p),I.log("[removeCountrySetting] Removed index:",t,"country:",s,"newTab:",l)}function Wn(r,c){const e=r.params||{},t=Number(e.index);if(isNaN(t)||t<0){I.warn("[switchCountryTab] Invalid index:",e.index);return}const o=G();if(!o)return;const a=D(o);if(t>=a.length){I.warn("[switchCountryTab] Index out of bounds:",t);return}const n=a[t],s=te(n.charge_policy),i={activeCountryTab:t,...s};E(c,i),I.log("[switchCountryTab] Switched to tab:",t,"country:",n.country_code,"charge_policy:",n.charge_policy)}function Jn(r,c){const e=r.params||{},t=e.field??"",o=e.value;if(!t){I.warn("[updateCountryField] field is empty");return}const a=G();if(!a)return;const n=a.state.getLocal?.()??{},s=Number(n.activeCountryTab??0),i=D(a);if(s>=i.length){I.warn("[updateCountryField] Index out of bounds:",s);return}i[s]={...i[s],[t]:o},E(c,{"form.country_settings":i}),t==="free_threshold"&&Ke(a,c,s,i[s]),I.log("[updateCountryField] Updated",t,"=",o,"at index:",s)}function Xn(r,c){const e=r.params||{},t=e.value??"",o=Number(e.index??0);if(!t){I.warn("[onChargePolicyChange] charge_policy value is empty");return}const a=G();if(!a)return;const n=te(t);I.log("[onChargePolicyChange]",t,n,"countryIndex:",o);const s=D(a),i=s[o];if(!i){I.warn("[onChargePolicyChange] No country setting at index:",o);return}const l={...i,charge_policy:t};if(n.showBaseFee||(l.base_fee=0),n.showFreeThreshold||(l.free_threshold=null),!n.showRanges&&!n.showUnitValue&&(l.ranges=null),n.showApiSettings||(l.api_endpoint=null,l.api_request_fields=null,l.api_response_fee_field=null,l.api_config=null),n.showRanges&&(!l.ranges||!l.ranges.tiers)){const d=t.replace("range_","");l.ranges={type:d,tiers:[{min:0,max:null,fee:null}]}}n.showUnitValue&&(!l.ranges||!l.ranges.unit_value)&&(l.ranges={type:t,unit_value:1}),s[o]=l;const u={"form.country_settings":s,...n};E(c,u),Ke(a,c,o,l)}function Zn(r,c){const e=r.params||{},t=Number(e.index??0),o=G();if(!o)return;const a=D(o),n=a[t];if(!n){I.warn("[addRangeTier] No country setting at index:",t);return}const s=[...n.ranges?.tiers??[],{min:null,max:null,fee:null}],i=xe(s,n.charge_policy);a[t]={...n,ranges:{...n.ranges??{},tiers:i}},E(c,{"form.country_settings":a}),Pe(o,c,t,i,n.country_code,n.charge_policy),I.log("[addRangeTier] Added tier, total:",i.length)}function ea(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.tierIndex);if(isNaN(o)||o<0){I.warn("[removeRangeTier] Invalid tierIndex:",e.tierIndex);return}const a=G();if(!a)return;const n=D(a),s=n[t];if(!s){I.warn("[removeRangeTier] No country setting at index:",t);return}const i=[...s.ranges?.tiers??[]];if(o>=i.length){I.warn("[removeRangeTier] tierIndex out of bounds:",o);return}i.splice(o,1);const l=xe(i,s.charge_policy);n[t]={...s,ranges:{...s.ranges??{},tiers:l}},E(c,{"form.country_settings":n}),Pe(a,c,t,l,s.country_code,s.charge_policy),I.log("[removeRangeTier] Removed tier:",o,"remaining:",l.length)}function ta(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.tierIndex),a=e.field,n=e.value;if(isNaN(o)||o<0){I.warn("[updateRangeTierField] Invalid tierIndex:",e.tierIndex);return}if(!["min","max","fee"].includes(a)){I.warn("[updateRangeTierField] Invalid field:",a);return}const s=G();if(!s)return;const i=D(s),l=i[t];if(!l){I.warn("[updateRangeTierField] No country setting at index:",t);return}const u=[...l.ranges?.tiers??[]];if(o>=u.length){I.warn("[updateRangeTierField] tierIndex out of bounds:",o);return}const m=n===""||n===null||n===void 0?null:Number(n);u[o]={...u[o],[a]:m};const p=a==="max"?xe(u,l.charge_policy):u;i[t]={...l,ranges:{...l.ranges??{},tiers:p}},E(c,{"form.country_settings":i}),Pe(s,c,t,p,l.country_code,l.charge_policy)}function oa(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=G();if(!o)return;const n=D(o)[t];if(!n)return;const s=n.ranges?.tiers??[];Pe(o,c,t,s,n.country_code,n.charge_policy)}function Pe(r,c,e,t,o,a){const s={...(r.state.getLocal?.()??{}).rangeErrors??{}},i=r.t;if(t.length===0){s[o]=ht.includes(a)?[{max:i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_min_one")??"구간별 배송비 정책은 구간을 1개 이상 등록해야 합니다."}]:[],E(c,{rangeErrors:s});return}const l=new Array(t.length).fill(null).map(()=>({}));let u=!1;const d=bt(a),m=p=>p==null;for(let p=0;p<t.length;p++){const f=t[p],y=p===t.length-1;if(p===0&&Number(f.min??0)!==0&&(l[p].min=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_first_min")??"첫 구간의 시작값은 0이어야 합니다.",u=!0),y&&!m(f.max)){l[p].max=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_last_max")??"마지막 구간의 종료값은 비워야 합니다.",u=!0;continue}if(y){f.fee===null||f.fee===void 0?(l[p].fee=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee_required")??"구간 배송비를 입력해주세요.",u=!0):f.fee<0&&(l[p].fee=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee")??"배송비는 0 이상이어야 합니다.",u=!0);continue}if(m(f.max))l[p].max=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_middle_max")??"마지막 구간을 제외한 구간에는 종료값을 입력해야 합니다.",u=!0;else{const w=Number(f.max);d&&!Number.isInteger(w)&&(l[p].max=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_integer")??"수량 구간의 시작값과 종료값은 정수여야 합니다.",u=!0),Number(f.min??0)>=w&&(l[p].min=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_min_max")??"시작값이 종료값보다 작아야 합니다.",u=!0);const _=d?w+1:w,g=Number(t[p+1]?.min??0);Math.abs(_-g)>=jn&&(l[p].max=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_continuity")??"구간이 연속적이지 않습니다.",u=!0)}f.fee===null||f.fee===void 0?(l[p].fee=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee_required")??"구간 배송비를 입력해주세요.",u=!0):f.fee<0&&(l[p].fee=i?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee")??"배송비는 0 이상이어야 합니다.",u=!0)}s[o]=u?l:[],E(c,{rangeErrors:s})}function It(r,c){return`country_settings.${r}.${c}`}function Ke(r,c,e,t){const a={...(r.state.getLocal?.()??{}).errors??{}},n=r.t,s=t.ranges?.unit_value,i=Ct.includes(t.charge_policy)&&(s==null||Number(s)<=0);a[It(e,"ranges.unit_value")]=i?[n?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_unit_value_required")??"단위당 배송비 정책은 단위값을 입력해야 합니다."]:null;const l=t.free_threshold,u=wt.includes(t.charge_policy)&&(l==null||String(l)==="");a[It(e,"free_threshold")]=u?[n?.("sirsoft-ecommerce.admin.shipping_policy.form.range_error_free_threshold_required")??"조건부 무료배송 정책은 무료배송 기준금액을 입력해야 합니다."]:null,E(c,{errors:a})}function ra(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=G();if(!o)return;const a=D(o),n=a[t];if(!n){I.warn("[addExtraFeeRow] No country setting at index:",t);return}if(n.country_code!=="KR"){I.warn("[addExtraFeeRow] Extra fee only available for KR, current:",n.country_code);return}const s=[...n.extra_fee_settings??[]];s.push({zipcode:"",fee:0,region:""}),a[t]={...n,extra_fee_settings:s},E(c,{"form.country_settings":a}),I.log("[addExtraFeeRow] Added row, total:",s.length)}function na(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=Number(e.feeIndex);if(isNaN(o)||o<0){I.warn("[removeExtraFeeRow] Invalid feeIndex:",e.feeIndex);return}const a=G();if(!a)return;const n=D(a),s=n[t];if(!s){I.warn("[removeExtraFeeRow] No country setting at index:",t);return}const i=[...s.extra_fee_settings??[]];if(o>=i.length){I.warn("[removeExtraFeeRow] feeIndex out of bounds:",o);return}i.splice(o,1),n[t]={...s,extra_fee_settings:i},E(c,{"form.country_settings":n}),I.log("[removeExtraFeeRow] Removed index:",o,"remaining:",i.length)}function aa(r,c){const e=r.params||{},t=Number(e.countryIndex??0),o=e.settings;if(!o||!Array.isArray(o)){I.warn("[applyExtraFeeTemplate] Invalid settings:",o);return}const a=G();if(!a)return;const n=D(a),s=n[t];if(!s){I.warn("[applyExtraFeeTemplate] No country setting at index:",t);return}n[t]={...s,extra_fee_settings:o},E(c,{"form.country_settings":n});const i=a.t,l=i?.("sirsoft-ecommerce.admin.shipping_policy.form.template_applied")??"템플릿이 적용되었습니다.";a.toast?.success?.(l),I.log("[applyExtraFeeTemplate] Applied template with",o.length,"rows")}function sa(r,c){const t=(r.params||{}).value,o=t===""||t===null||t===void 0,a=parseFloat(t),n=o||Number.isNaN(a)?null:a,s=G();if(!s)return;const i=s.state.getLocal?.()??{},l=Number(i.activeCountryTab??0),u=D(s),d=u[l];d&&(u[l]={...d,ranges:{...d.ranges??{},type:d.charge_policy,unit_value:n}},E(c,{"form.country_settings":u}),Ke(s,c,l,u[l]),I.log("[updateUnitValue] Updated unit_value:",n))}function ia(r,c){const e=G();if(!e)return;const t=e.state.getLocal?.()??{},o=Number(t.activeCountryTab??0),a=D(e),n=a[o];if(!n)return;const s=[...n.api_request_fields??[]];s.push(""),a[o]={...n,api_request_fields:s},E(c,{"form.country_settings":a}),I.log("[addApiRequestField] Added field, total:",s.length)}function ca(r,c){const e=r.params||{},t=Number(e.fieldIndex),o=e.value??"";if(isNaN(t)||t<0){I.warn("[updateApiRequestField] Invalid fieldIndex:",e.fieldIndex);return}const a=G();if(!a)return;const n=a.state.getLocal?.()??{},s=Number(n.activeCountryTab??0),i=D(a),l=i[s];if(!l)return;const u=[...l.api_request_fields??[]];t>=u.length||(u[t]=o,i[s]={...l,api_request_fields:u},E(c,{"form.country_settings":i}),I.log("[updateApiRequestField] Updated index:",t,"value:",o))}function la(r,c){const e=r.params||{},t=Number(e.fieldIndex);if(isNaN(t)||t<0){I.warn("[removeApiRequestField] Invalid fieldIndex:",e.fieldIndex);return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=D(o),i=s[n];if(!i)return;const l=[...i.api_request_fields??[]];t>=l.length||(l.splice(t,1),s[n]={...i,api_request_fields:l},E(c,{"form.country_settings":s}),I.log("[removeApiRequestField] Removed index:",t,"remaining:",l.length))}function da(r,c){const t=(r.params||{}).field??"";if(!t){I.warn("[toggleApiRequestField] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=D(o),i=s[n];if(!i)return;const l=[...i.api_request_fields??[]],u=l.indexOf(t);u>=0?l.splice(u,1):l.push(t);const d=l.length>0?l:null;s[n]={...i,api_request_fields:d},E(c,{"form.country_settings":s}),I.log("[toggleApiRequestField] Toggled:",t,"selected:",d)}function ua(r,c){const e=r.params||{},t=e.field??"";if(!t){I.warn("[updateApiConfigField] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=D(o),i=s[n];if(!i)return;const l={...i.api_config??{},[t]:e.value};s[n]={...i,api_config:l},E(c,{"form.country_settings":s}),I.log("[updateApiConfigField] Updated",t,"at index:",n)}function ma(r,c){const e=r.params||{},t=e.field??"";if(!t){I.warn("[updateApiFieldMap] Missing field param");return}const o=G();if(!o)return;const a=o.state.getLocal?.()??{},n=Number(a.activeCountryTab??0),s=D(o),i=s[n];if(!i)return;const l=e.value??"",u={...i.api_config?.field_map??{}};l.trim()===""?delete u[t]:u[t]=l;const d={...i.api_config??{},field_map:Object.keys(u).length>0?u:null};s[n]={...i,api_config:d},E(c,{"form.country_settings":s}),I.log("[updateApiFieldMap] Mapped",t,"→",l||"(removed)")}function pa(r,c){const e=G();if(!e)return;const t=e.state.getLocal?.()??{},o=Number(t.activeCountryTab??0),n=D(e)[o];if(n){if(!n.api_endpoint){e.toast?.warning?.(e.t?.("sirsoft-ecommerce.admin.shipping_policy.form.api_test_endpoint_required")??"Endpoint required");return}e.state.set?.({apiTestLoading:!0,apiTestResult:null}),e.dispatch({handler:"apiCall",target:"/api/modules/sirsoft-ecommerce/admin/shipping-policies/test-api-call",auth_required:!0,params:{method:"POST",body:{endpoint:n.api_endpoint,request_fields:n.api_request_fields,config:n.api_config??{},sample:{country_code:n.country_code}}},onSuccess:{handler:"setState",params:{target:"global",apiTestLoading:!1,apiTestResult:"{{response.data}}"}},onError:{handler:"setState",params:{target:"global",apiTestLoading:!1,apiTestResult:"{{error.errors ?? error}}"}}})}}function fa(r,c){const e=r.params||{},t=Number(e.feeIndex),o=e.field,a=e.value;if(isNaN(t)||t<0){I.warn("[updateExtraFeeField] Invalid feeIndex:",e.feeIndex);return}if(!["zipcode","fee","region"].includes(o)){I.warn("[updateExtraFeeField] Invalid field:",o);return}const n=G();if(!n)return;const s=n.state.getLocal?.()??{},i=Number(s.activeCountryTab??0),l=D(n),u=l[i];if(!u)return;const d=[...u.extra_fee_settings??[]];if(t>=d.length)return;const m=o==="fee"?parseFloat(a)||0:a;d[t]={...d[t],[o]:m},l[i]={...u,extra_fee_settings:d},E(c,{"form.country_settings":l}),I.log("[updateExtraFeeField] Updated index:",t,o,"=",m)}const fe=window.G7Core?.createLogger?.("Handler:InitCategory")??{log:(...r)=>console.log("[Handler:InitCategory]",...r),warn:(...r)=>console.warn("[Handler:InitCategory]",...r),error:(...r)=>console.error("[Handler:InitCategory]",...r)};function St(r,c){for(const e of r){if(e.id===c)return e;if(e.children&&e.children.length>0){const t=St(e.children,c);if(t)return t}}return null}function vt(r){return new URLSearchParams(window.location.search).get(r)}function ga(r){return window.G7Core?.state?.getDataSource?.(r)}async function _a(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=ga(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function ya(r,c){const e=window.G7Core,t=vt("id"),o=vt("mode");if(!t){fe.log("[initCategoryFromUrl] No id parameter in URL");return}if(!e?.state?.set){fe.warn("[initCategoryFromUrl] G7Core.state.set not available");return}const a=await _a("categories",l=>l?.data?.data);if(!a){fe.warn("[initCategoryFromUrl] categories data not available after waiting");return}const n=parseInt(t,10),s=St(a,n);if(!s){fe.warn("[initCategoryFromUrl] Category not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedCategoryId:s.id,selectedCategory:s,panelMode:i}),fe.log("[initCategoryFromUrl] Category initialized from URL:",{id:n,mode:i})}const ge=window.G7Core?.createLogger?.("Handler:InitBrand")??{log:(...r)=>console.log("[Handler:InitBrand]",...r),warn:(...r)=>console.warn("[Handler:InitBrand]",...r),error:(...r)=>console.error("[Handler:InitBrand]",...r)};function Lt(r){return new URLSearchParams(window.location.search).get(r)}function wa(r){return window.G7Core?.state?.getDataSource?.(r)}async function ha(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=wa(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function Ca(r,c){const e=window.G7Core,t=Lt("id"),o=Lt("mode");if(!t){ge.log("[initBrandFromUrl] No id parameter in URL");return}if(!e?.state?.set){ge.warn("[initBrandFromUrl] G7Core.state.set not available");return}const a=await ha("brands",l=>l?.data?.data);if(!a){ge.warn("[initBrandFromUrl] brands data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){ge.warn("[initBrandFromUrl] Brand not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedBrandId:s.id,selectedBrand:s,panelMode:i}),ge.log("[initBrandFromUrl] Brand initialized from URL:",{id:n,mode:i})}const _e=window.G7Core?.createLogger?.("Handler:InitCommonInfo")??{log:(...r)=>console.log("[Handler:InitCommonInfo]",...r),warn:(...r)=>console.warn("[Handler:InitCommonInfo]",...r),error:(...r)=>console.error("[Handler:InitCommonInfo]",...r)};function xt(r){return new URLSearchParams(window.location.search).get(r)}function ba(r){return window.G7Core?.state?.getDataSource?.(r)}async function Ia(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=ba(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function Sa(r,c){const e=window.G7Core,t=xt("id"),o=xt("mode");if(!t){_e.log("[initCommonInfoFromUrl] No id parameter in URL");return}if(!e?.state?.set){_e.warn("[initCommonInfoFromUrl] G7Core.state.set not available");return}const a=await Ia("commonInfos",l=>l?.data);if(!a){_e.warn("[initCommonInfoFromUrl] commonInfos data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){_e.warn("[initCommonInfoFromUrl] Common info not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedId:s.id,selectedItem:s,panelMode:i}),_e.log("[initCommonInfoFromUrl] Common info initialized from URL:",{id:n,mode:i})}const ye=window.G7Core?.createLogger?.("Handler:InitNotice")??{log:(...r)=>console.log("[Handler:InitNotice]",...r),warn:(...r)=>console.warn("[Handler:InitNotice]",...r),error:(...r)=>console.error("[Handler:InitNotice]",...r)};function Pt(r){return new URLSearchParams(window.location.search).get(r)}function va(r){return window.G7Core?.state?.getDataSource?.(r)}async function La(r,c,e=30,t=100){for(let o=0;o<e;o++){const a=va(r),n=c(a);if(Array.isArray(n)&&n.length>0)return n;await new Promise(s=>setTimeout(s,t))}return null}async function xa(r,c){const e=window.G7Core,t=Pt("id"),o=Pt("mode");if(!t){ye.log("[initNoticeFromUrl] No id parameter in URL");return}if(!e?.state?.set){ye.warn("[initNoticeFromUrl] G7Core.state.set not available");return}const a=await La("templates",l=>l?.data);if(!a){ye.warn("[initNoticeFromUrl] templates data not available after waiting");return}const n=parseInt(t,10),s=a.find(l=>l.id===n);if(!s){ye.warn("[initNoticeFromUrl] Template not found with id:",n);return}const i=o==="edit"?"edit":"view";e.state.set({selectedTemplateId:s.id,selectedTemplate:s,panelMode:i}),ye.log("[initNoticeFromUrl] Template initialized from URL:",{id:n,mode:i})}const oe={updateProductField:Rt,updateOptionField:Mt,calculateCurrencyPrices:se,initPreferredCurrency:Kt,initPreferredShippingCountry:Wt,setDateRange:Xt,setDefaultOption:Zt,toggleOption:eo,toggleProductOptions:et,toggleAllOptionsInRow:to,getProductOptionStates:oo,syncProductSelection:ro,loadExpandedOptions:$e,retryExpandedOptions:uo,generateCopyProductCode:no,copyProduct:ao,selectCategory:po,selectCategoryMobile:fo,addCategoryToSelection:go,removeCategoryFromSelection:_o,getCategoryBreadcrumb:yo,validateCategoryPath:ho,initCategoryInfosFromProduct:wo,getBrandName:Co,getBrandDescription:bo,updatePrice:Lo,calculateTotalOptionStock:xo,validatePriceRelation:Po,addOptionInput:Ho,removeOptionInput:qo,updateOptionInput:Uo,generateOptions:Vo,deleteOption:jo,applyOptionAddTool:Bo,addRequiredItem:Ko,updateRequiredItem:Qo,removeRequiredItem:zo,reorderRequiredItems:Yo,addAdditionalOption:Wo,updateAdditionalOption:Jo,removeAdditionalOption:Xo,reorderAdditionalOptions:Zo,clearAdditionalOptions:er,addAdditionalOptionValue:tr,updateAdditionalOptionValue:or,removeAdditionalOptionValue:rr,uploadImages:ir,setThumbnail:cr,reorderImages:lr,updateDescription:dr,confirmSelectNoticeTemplate:ur,selectNoticeTemplate:ct,updateNoticeItem:mr,removeNoticeItem:pr,reorderNoticeItems:fr,fillNoticeWithValue:gr,switchNoticeMode:_r,updateNewTemplateName:yr,addNoticeItem:wr,updateNoticeItemName:hr,saveAsNoticeTemplate:Cr,confirmSaveNoticeTemplate:Sr,fillTemplateFieldsWithDetailReference:br,fillNoticeItemsWithDetailReference:Ir,toggleLabel:vr,generateProductCode:Lr,getShippingPolicyInfo:xr,getCommonInfoContent:Pr,updateShoppingIntegration:Ar,updateShippingType:kr,updateIdentificationCode:Or,openLabelPeriodModal:Gr,saveLabelPeriod:Er,removeLabelPeriod:Dr,updateActivityLogSort:Fr,updateActivityLogPerPage:$r,setDefaultShippingPolicy:Nr,setLabelDatePreset:Rr,toggleDefaultShippingPolicy:Tr,toggleLabelAssignment:Mr,saveLabelSettings:jr,deleteLabel:Vr,updateLabelPeriodInline:Hr,setLabelDatePresetInline:qr,confirmUncheckLabel:Ur,removeDescriptionLocale:Br,showAddLocaleModal:Kr,addDescriptionLocale:Qr,setDefaultOptionFromGrid:Go,addOptionRow:Do,updateFormOptionField:Eo,recalculateOptionPriceAdjustments:Fo,bulkUpdate:zr,buildConfirmData:Jr,buildOrderColumns:Xr,toggleArrayValue:Zr,toggleVisibleFilter:en,syncOrderSelection:tn,handleOrderRowAction:on,processOrderBulkAction:rn,buildOrderBulkConfirmData:ln,executeOrderBulkAction:dn,downloadOrderExcel:nn,saveVisibleColumns:an,loadVisibleColumns:sn,loadVisibleFilters:cn,handleProductRowAction:un,initOrderDetailForm:mn,toggleProductSelection:pn,toggleAllProducts:fn,buildOrderDetailBulkConfirmData:gn,processOrderDetailBulkChange:_n,saveAdminMemo:yn,updateChangeQuantity:wn,openConfirmDepositModal:hn,confirmDeposit:Cn,initShippingPolicyForm:Qn,addCountrySetting:zn,removeCountrySetting:Yn,switchCountryTab:Wn,updateCountryField:Jn,onChargePolicyChange:Xn,addRangeTier:Zn,removeRangeTier:ea,updateRangeTierField:ta,validateRangeTiers:oa,addExtraFeeRow:ra,removeExtraFeeRow:na,applyExtraFeeTemplate:aa,updateUnitValue:sa,addApiRequestField:ia,updateApiRequestField:ca,removeApiRequestField:la,toggleApiRequestField:da,updateApiConfigField:ua,updateApiFieldMap:ma,testShippingApi:pa,updateExtraFeeField:fa,updateCancelQuantity:In,estimateRefundAmount:Sn,changeRefundPriority:vn,executeCancelOrder:Ln,clearCancelOrderTimers:bn,toggleItemSelection:Pn,toggleSelectAllItems:An,initUserCancelItems:kn,toggleUserCancelItem:On,toggleUserCancelSelectAll:Gn,updateUserCancelQuantity:En,estimateUserRefund:Dn,changeUserRefundPriority:Fn,executeUserCancelOrder:$n,clearUserCancelOrderTimers:xn,confirmOrderOption:Tn,changeShippingAddress:Mn,submitReview:Hn,initCategoryFromUrl:ya,initBrandFromUrl:Ca,initCommonInfoFromUrl:Sa,initNoticeFromUrl:xa},V="sirsoft-ecommerce",we=window.G7Core?.createLogger?.(`Module:${V}`)??{log:(...r)=>console.log(`[Module:${V}]`,...r),warn:(...r)=>console.warn(`[Module:${V}]`,...r),error:(...r)=>console.error(`[Module:${V}]`,...r)};function At(r=!1){const c=window.G7Core?.getActionDispatcher?.();if(c)Object.entries(oe).forEach(([e,t])=>{const o=`${V}.${e}`;c.registerHandler(o,t,{category:"module",source:V})}),we.log(`${Object.keys(oe).length} handler(s) registered:`,Object.keys(oe).map(e=>`${V}.${e}`));else if(r){let e=0;const t=50,o=()=>{const a=window.G7Core?.getActionDispatcher?.();a?(Object.entries(oe).forEach(([n,s])=>{const i=`${V}.${n}`;a.registerHandler(i,s,{category:"module",source:V})}),we.log(`${Object.keys(oe).length} handler(s) registered:`,Object.keys(oe).map(n=>`${V}.${n}`))):(e++,e<=t?(we.warn(`ActionDispatcher not found, retrying... (${e}/${t})`),setTimeout(o,100)):we.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};o()}else we.warn("ActionDispatcher not found, handlers not registered")}function Qe(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>At(!0));else{const r=!!window.G7Core?.getActionDispatcher?.();At(!r)}}return Qe(),typeof window<"u"&&(window.__SirsoftEcommerce={identifier:V,handlers:Object.keys(oe),initModule:Qe}),ke.initModule=Qe,Object.defineProperty(ke,Symbol.toStringTag,{value:"Module"}),ke}({}); diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md index 91c9fa62..60445626 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/addresses.md @@ -199,7 +199,7 @@ HTTP/1.1 201 <!-- @generated:end --> -**설명** 로그인한 회원 본인의 새 배송지를 등록합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::createAddress()`가 검증된 요청에 현재 사용자 ID를 결합해 배송지를 생성하고 성공 시 `201`로 반환합니다. 국내(우편번호/도로명·지번)·해외(intl_* 필드) 주소를 모두 지원하고 `is_default`로 기본 배송지 지정이 가능합니다. 같은 이름의 배송지가 있으면 `409`(중복 ID 포함)를, `force_overwrite`로 덮어쓰기를 허용할 수 있으며, 최대 배송지 개수를 초과하면 `422`를 반환합니다. +**설명** 로그인한 회원 본인의 새 배송지를 등록합니다. `auth:sanctum` 인증이 필요하며, `UserAddressService::createAddress()`가 검증된 요청에 현재 사용자 ID를 결합해 배송지를 생성하고 성공 시 `201`로 반환합니다. 국내(우편번호/도로명·지번)·해외(intl_* 필드) 주소를 모두 지원하고 `is_default`로 기본 배송지 지정이 가능합니다. 같은 이름의 배송지가 있으면 `409`(중복 ID 포함)를 반환하며, `force_overwrite`로 덮어쓰기를 허용할 수 있습니다. 그 밖의 예외는 서버 결함으로 보아 `500` 으로 응답합니다 — 개수 상한은 현재 도입되어 있지 않습니다. ### DELETE /api/modules/sirsoft-ecommerce/user/addresses/{id} @@ -229,7 +229,17 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지가 삭제되었습니다.", + "data": null +} +``` **에러 응답** @@ -266,11 +276,57 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| address | object | `{"id":99,"user_id":"a26219fc-94a0-4f63-9404-04c2a6ac99e4"…` | 조회한 배송지 단건 (`UserAddressResource`) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지를 조회했습니다.", + "data": { + "address": { + "id": 99, + "user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", + "name": "실측 예시값", + "recipient_name": "실측 예시값", + "recipient_phone": "실측 예시값", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "실측 예시값", + "address": "실측 예시값", + "address_detail": "실측 예시값", + "address_line_1": null, + "address_line_2": null, + "city": "실측 예시값", + "state": null, + "postal_code": null, + "is_default": true, + "is_domestic": true, + "is_international": false, + "full_address": "실측 예시값 실측 예시값", + "created_at": "2026-08-05 09:46:56", + "updated_at": "2026-08-05 09:46:56", + "abilities": { + "can_update": true, + "can_delete": false, + "can_set_default": false + } + } + } +} +``` **에러 응답** @@ -345,11 +401,57 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| address | object | `{"id":99,"user_id":"a26219fc-94a0-4f63-9404-04c2a6ac99e4"…` | 수정된 배송지 (`UserAddressResource`) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송지가 수정되었습니다.", + "data": { + "address": { + "id": 99, + "user_id": "a26219fc-94a0-4f63-9404-04c2a6ac99e4", + "name": "실측 예시값", + "recipient_name": "실측 예시값", + "recipient_phone": "실측 예시값", + "country_code": "KR", + "country_name": { + "ko": "한국", + "en": "South Korea" + }, + "zipcode": "실측 예시값", + "address": "실측 예시값", + "address_detail": "실측 예시값", + "address_line_1": null, + "address_line_2": null, + "city": "실측 예시값", + "state": null, + "postal_code": null, + "is_default": true, + "is_domestic": true, + "is_international": false, + "full_address": "실측 예시값 실측 예시값", + "created_at": "2026-08-05 09:46:56", + "updated_at": "2026-08-05 09:46:56", + "abilities": { + "can_update": true, + "can_delete": false, + "can_set_default": false + } + } + } +} +``` **에러 응답** @@ -391,7 +493,17 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "기본 배송지가 설정되었습니다.", + "data": null +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md index e168d6d8..92d0cfeb 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/brands.md @@ -283,23 +283,42 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`BrandService::deleteBrand()` 가 반환한 배열 — Resource 로 감싸지 않는다)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| brand_id | integer | `1` | 삭제된 브랜드의 ID | +| products_count | integer | `0` | 삭제 시점의 연결 상품 수. **항상 `0`** 이다 — 연결 상품이 하나라도 있으면 삭제 자체가 차단되므로 성공 응답에서는 0 외의 값이 나올 수 없다 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "브랜드가 삭제되었습니다.", + "data": { + "brand_id": 1, + "products_count": 0 + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.delete`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.delete`)이 없거나, 대상 브랜드가 요청자의 **스코프 밖**인 경우 (`auth.scope_denied`) | +| 400 | Bad Request | 해당 `id` 의 브랜드가 없는 경우(`exceptions.brand_not_found`), 연결된 상품이 있어 삭제가 차단된 경우(`exceptions.brand_has_products` — 상품 수 포함), 또는 삭제 처리 중 도메인 규칙 위반 | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | <!-- @generated:end --> -**설명** 브랜드 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.delete` 권한이 필요하며, `BrandService::deleteBrand()`가 삭제 전 연결된 상품 수를 확인합니다. 연결된 상품이 1개 이상이면 예외가 발생해 삭제가 차단되고 400 오류가 반환되므로, 해당 브랜드의 상품을 먼저 정리하거나 다른 브랜드로 이전해야 삭제할 수 있습니다. 대상이 없으면 404를 반환합니다. +**설명** 브랜드 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.delete` 권한이 필요하며, `BrandService::deleteBrand()`가 삭제 전 연결된 상품 수를 확인합니다. 연결된 상품이 1개 이상이면 예외가 발생해 삭제가 차단되고 400 오류가 반환되므로, 해당 브랜드의 상품을 먼저 정리하거나 다른 브랜드로 이전해야 삭제할 수 있습니다. 존재하지 않는 ID 는 400(`brand_not_found`)을 반환하고, 존재하지만 요청자의 스코프 밖인 대상은 403(`auth.scope_denied`)을 반환합니다. ### PUT /api/modules/sirsoft-ecommerce/admin/brands/{brand} @@ -403,13 +422,13 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없거나, 대상 브랜드가 요청자의 **스코프 밖**인 경우 (`auth.scope_denied`) | +| 400 | Bad Request | 해당 `brand` 의 브랜드가 없는 경우(`exceptions.brand_not_found`) 또는 수정 처리 중 도메인 규칙 위반 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | <!-- @generated:end --> -**설명** 기존 브랜드(path의 `brand`)를 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.update` 권한이 필요하며, `BrandService::updateBrand()`가 검증된 데이터로 갱신한 뒤 `BrandResource`를 반환합니다. `name`은 필수이고 `slug`·`website`·`sort_order`·`is_active`는 선택입니다. 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환하며, `sirsoft-ecommerce.brand.update_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. +**설명** 기존 브랜드(path의 `brand`)를 수정합니다. `auth:sanctum` + `sirsoft-ecommerce.brands.update` 권한이 필요하며, `BrandService::updateBrand()`가 검증된 데이터로 갱신한 뒤 `BrandResource`를 반환합니다. `name`은 필수이고 `slug`·`website`·`sort_order`·`is_active`는 선택입니다. 존재하지 않는 ID 는 400(`brand_not_found`), 스코프 밖 대상은 403(`auth.scope_denied`)을 반환하며, `sirsoft-ecommerce.brand.update_validation_rules` 필터로 확장이 검증 규칙을 추가할 수 있습니다. ### GET /api/modules/sirsoft-ecommerce/admin/brands/{id} @@ -435,11 +454,62 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 가 브랜드 하나 (`BrandResource`). 필드 구성은 목록 응답의 `data.data[]` 항목과 동일하되, 목록 전용 순번(`number`)은 없다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `20` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"ASUS","en":"ASUS"}` | 다국어 원본 이름 (로케일 키 → 값) | +| localized_name | string | `ASUS` | `name` 의 현재 로케일 해석 값 | +| slug | string | `asus` | URL 친화 식별자 | +| url | string | `asus` | SortableMenuItem 표시용 URL (slug 값을 그대로 노출) | +| website | string \| null | `https://www.asus.com` | 브랜드 공식 웹사이트 URL | +| sort_order | integer | `20` | 표시 정렬 순서 값 (작을수록 우선) | +| is_active | boolean | `true` | 사용 여부 | +| icon | string | `tag` | 아이콘 식별자 (Resource 가 고정값으로 부여) | +| created_at | string | `2026-07-30 23:35:46` | 생성 일시 | +| updated_at | string | `2026-07-30 23:35:46` | 최종 수정 일시 | +| creator | object \| array | `[]` | 생성자 정보 (`id`/`name`) — 관계가 로드된 경우에만 포함 | +| updater | object \| array | `[]` | 수정자 정보 (`id`/`name`) — 관계가 로드된 경우에만 포함 | +| products_count | integer | `1` | 이 브랜드에 속한 상품 수 (집계) | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "브랜드를 조회했습니다.", + "data": { + "id": 20, + "name": { + "ko": "ASUS", + "en": "ASUS" + }, + "localized_name": "ASUS", + "slug": "asus", + "url": "asus", + "website": "https://www.asus.com", + "sort_order": 20, + "is_active": true, + "icon": "tag", + "created_at": "2026-07-30 23:35:46", + "updated_at": "2026-07-30 23:35:46", + "creator": [], + "updater": [], + "products_count": 1, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -447,7 +517,7 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `id` 의 브랜드가 없는 경우 (`messages.brands.not_found`) | <!-- @generated:end --> @@ -477,19 +547,64 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 가 토글된 브랜드 (`BrandResource`). 필드 구성은 상세 조회와 동일하며, 실질적으로 달라지는 것은 `is_active` 와 `updated_at` 이다._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `20` | 브랜드 기본키 | +| is_active | boolean | `false` | **토글 후** 의 사용 여부. 화면은 이 값으로 스위치 상태를 갱신한다 | +| updated_at | string | `2026-08-16 01:30:00` | 토글 시각으로 갱신된 수정 일시 | +| (그 외) | — | — | `name`·`localized_name`·`slug`·`url`·`website`·`sort_order`·`icon`·`created_at`·`creator`·`updater`·`products_count`·`abilities` — 상세 조회 응답과 동일 | + +`message` 는 활성/비활성 구분 없이 `messages.brands.status_changed` 하나다. 현재 상태는 `data.is_active` 로 판정한다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +비활성으로 토글된 경우: + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "브랜드 상태가 변경되었습니다.", + "data": { + "id": 20, + "name": { + "ko": "ASUS", + "en": "ASUS" + }, + "localized_name": "ASUS", + "slug": "asus", + "url": "asus", + "website": "https://www.asus.com", + "sort_order": 20, + "is_active": false, + "icon": "tag", + "created_at": "2026-07-30 23:35:46", + "updated_at": "2026-08-16 01:30:00", + "creator": [], + "updater": [], + "products_count": 1, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.brands.update`)이 없거나, 대상 브랜드가 요청자의 **스코프 밖**인 경우 (`auth.scope_denied`) | +| 400 | Bad Request | 해당 `id` 의 브랜드가 없는 경우(`exceptions.brand_not_found`) 또는 토글 처리 중 도메인 규칙 위반 | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md index a0e2d144..11707de4 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/categories.md @@ -402,10 +402,11 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 파일 저장 중 예외 발생 (`exceptions.operation_failed`) | +| 400 | Bad Request | 파일 저장 중 도메인 규칙 위반 (대상 카테고리 부재 등 — `exceptions.category_not_found` 같은 예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `file` 은 필수이며 `jpeg,png,jpg,gif,svg,webp` 이미지만 허용, 최대 10MB | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -465,10 +466,11 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 순서 갱신 처리 중 예외 발생 (`exceptions.operation_failed`) | +| 400 | Bad Request | 순서 갱신 처리 중 도메인 규칙 위반 (`exceptions.category_not_found` 같은 예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `order` 는 1건 이상 필수이며 각 항목에 정수 `id` 와 0 이상의 정수 `order` 가 필요 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -519,10 +521,11 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 이미지 삭제 처리 중 예외 발생 (`exceptions.operation_failed`) | +| 400 | Bad Request | 이미지 삭제 처리 중 도메인 규칙 위반 (`exceptions.category_not_found` 같은 예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 404 | Not Found | path 의 이미지 `id` 에 해당하는 카테고리 이미지가 없는 경우 (`messages.category_images.not_found`) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -586,10 +589,11 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 순서 갱신 트랜잭션 중 예외 발생 (`exceptions.operation_failed`) | +| 400 | Bad Request | 순서 갱신 트랜잭션 중 도메인 규칙 위반 (순환 참조·대상 부재 등 — 예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `parent_menus` / `child_menus` 중 최소 하나가 필요하며, 각 항목의 `id` 는 실제 존재하는 카테고리여야 하고 `order` 는 0 이상의 정수 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -812,11 +816,12 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 파일 저장 중 예외 발생 (`exceptions.operation_failed`) | +| 400 | Bad Request | 파일 저장 중 도메인 규칙 위반 (대상 카테고리 부재 등 — `exceptions.category_not_found` 같은 예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `file` 은 필수이며 `jpeg,png,jpg,gif,svg,webp` 이미지만 허용, 최대 10MB | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -846,11 +851,27 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| category_id | integer | `12` | 삭제된 카테고리의 기본 키 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리가 삭제되었습니다.", + "data": { + "category_id": 12 + } +} +``` **에러 응답** @@ -858,11 +879,13 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.delete`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 400 | Bad Request | 해당 카테고리가 없거나(`exceptions.category_not_found`), 하위 카테고리(`exceptions.category_has_children`)·연결 상품(`exceptions.category_has_products`)이 있어 삭제가 차단된 경우 | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> -**설명** 카테고리 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.delete` 권한이 필요하며, `CategoryService::deleteCategory()`가 삭제 전 안전 검사를 수행합니다. 하위 카테고리가 있거나 연결된 상품이 존재하면 예외가 발생해 삭제가 차단되고 400 오류가 반환되므로, 자식과 상품을 먼저 정리해야 삭제할 수 있습니다. 대상이 없으면 404를 반환합니다. +**설명** 카테고리 1건을 삭제합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.delete` 권한이 필요하며, `CategoryService::deleteCategory()`가 삭제 전 안전 검사를 수행합니다. 하위 카테고리가 있거나 연결된 상품이 존재하면 예외가 발생해 삭제가 차단되고 400 오류(사유별 구체 키)가 반환되므로, 자식과 상품을 먼저 정리해야 삭제할 수 있습니다. 대상이 없으면 400(`category_not_found`)을 반환합니다. ### PUT /api/modules/sirsoft-ecommerce/admin/categories/{category} @@ -985,11 +1008,12 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 400 | Bad Request | 대상 카테고리가 없거나 수정 트랜잭션 중 예외 발생 (컨트롤러가 예외를 잡아 `exceptions.operation_failed` 로 응답) | +| 400 | Bad Request | 대상 카테고리가 없거나(`exceptions.category_not_found`) 수정 트랜잭션 중 도메인 규칙 위반 (예외별 구체 키로 응답) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1019,11 +1043,49 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`CategoryResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/categories/{id} (카테고리 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "1", + "depth": 0, + "sort_order": 0, + "is_active": true, + "slug": "apidoc-sample-category", + "url": "apidoc-sample-category", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-07-08 01:44:49", + "updated_at": "2026-07-08 06:00:17", + "images": [], + "products_count": 0, + "children_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -1061,11 +1123,49 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`CategoryResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/categories/{id} (카테고리 수정)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 상태가 변경되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "1", + "depth": 0, + "sort_order": 0, + "is_active": true, + "slug": "apidoc-sample-category", + "url": "apidoc-sample-category", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-07-08 01:44:49", + "updated_at": "2026-07-08 06:00:17", + "images": [], + "products_count": 0, + "children_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -1073,11 +1173,13 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.categories.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 400 | Bad Request | 해당 카테고리가 없는 경우(`exceptions.category_not_found`) 또는 토글 처리 중 도메인 규칙 위반 | +| 404 | Not Found | path 파라미터 형식이 라우트에 매칭되지 않는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> -**설명** 카테고리의 활성 상태를 토글합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, path의 `id`로 `CategoryService::toggleStatus()`를 호출해 현재 `is_active` 값을 반전시킨 뒤 갱신된 `CategoryResource`를 반환합니다. 관리자 목록에서 노출/비노출을 빠르게 전환할 때 사용하며, 대상이 없거나 처리 중 예외가 발생하면 404 또는 400을 반환합니다. +**설명** 카테고리의 활성 상태를 토글합니다. `auth:sanctum` + `sirsoft-ecommerce.categories.update` 권한이 필요하며, path의 `id`로 `CategoryService::toggleStatus()`를 호출해 현재 `is_active` 값을 반전시킨 뒤 갱신된 `CategoryResource`를 반환합니다. 관리자 목록에서 노출/비노출을 빠르게 전환할 때 사용하며, 대상이 없으면 400(`category_not_found`), 도메인 규칙 위반 시 사유별 구체 키의 400 을 반환합니다. ### GET /api/modules/sirsoft-ecommerce/categories @@ -1215,11 +1317,49 @@ Accept: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`CategoryResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/categories/{id} (카테고리 수정)** 응답 필드 표와 동일합니다._ 공개 API 는 비활성 카테고리를 노출하지 않습니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "카테고리 목록을 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 카테고리", + "en": "API Doc Sample Category" + }, + "description": null, + "localized_name": "API 문서 샘플 카테고리", + "parent_id": null, + "path": "1", + "depth": 0, + "sort_order": 0, + "is_active": true, + "slug": "apidoc-sample-category", + "url": "apidoc-sample-category", + "icon": "folder", + "meta_title": null, + "meta_description": null, + "created_at": "2026-07-08 01:44:49", + "updated_at": "2026-07-08 06:00:17", + "images": [], + "products_count": 0, + "children_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md index 9a6b948d..f418e995 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/claim-reasons.md @@ -28,7 +28,7 @@ | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | type | query | string | 아니오 | — | 유형 필터 (해당 유형의 항목만 조회) | -| fault_type | query | string | 아니오 | — | <!-- TODO: 용도 --> | +| fault_type | query | string | 아니오 | `customer`, `seller`, `carrier` | 귀책 구분 필터 (ClaimReasonFaultTypeEnum — 고객/판매자/배송사 귀책). 미전달 시 전체 | | is_active | query | boolean | 아니오 | — | 활성 여부 (true 활성 / false 비활성) | | search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | @@ -313,7 +313,7 @@ HTTP/1.1 200 <!-- @generated:end --> -**설명** 관리자가 클래임 사유 1건을 삭제합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ClaimReasonService::deleteReason()`이 삭제합니다. 이미 사용 중인 사유 등 삭제 불가 상황에서는 서비스가 던진 예외 메시지를 그대로 사용해 400 으로 응답하므로, 관리자에게 삭제 실패 사유가 노출됩니다. +**설명** 관리자가 클래임 사유 1건을 삭제합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ClaimReasonService::deleteReason()`이 삭제합니다. 이미 사용 중인 사유 등 삭제 불가 상황에서는 그 사유에 대응하는 안내 문구를 400 으로 응답합니다 (예외 메시지 원문이 아니라 전용 다국어 키를 사용). 그 밖의 서버 내부 오류는 500 으로 구분됩니다. ### GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id} @@ -510,6 +510,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 같은 `type` 내 다른 사유가 동일한 `code` 를 이미 사용 중이면 `code` 필드 검증 실패 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -599,6 +600,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -756,11 +758,27 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| reason_id | integer | `8` | 삭제된 클레임 사유의 기본 키 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클레임 사유가 삭제되었습니다.", + "data": { + "reason_id": 8 + } +} +``` **에러 응답** @@ -769,10 +787,11 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> -**설명** 관리자가 클레임 사유 1건을 삭제합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ClaimReasonService::deleteReason()`이 삭제합니다. 이미 사용 중인 사유 등 삭제 불가 상황에서는 서비스가 던진 예외 메시지를 그대로 사용해 400 으로 응답하므로, 관리자에게 삭제 실패 사유가 노출됩니다. +**설명** 관리자가 클레임 사유 1건을 삭제합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, 대상이 없으면 404 를 반환하고 존재하면 `ClaimReasonService::deleteReason()`이 삭제합니다. 이미 사용 중인 사유 등 삭제 불가 상황에서는 그 사유에 대응하는 안내 문구를 400 으로 응답합니다 (예외 메시지 원문이 아니라 전용 다국어 키를 사용). 그 밖의 서버 내부 오류는 500 으로 구분됩니다. ### GET /api/modules/sirsoft-ecommerce/admin/claim-reasons/{id} @@ -798,11 +817,42 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ClaimReasonResource`). 필드 구성은 이 문서의 **POST /api/modules/sirsoft-ecommerce/admin/claim-reasons (클레임 사유 생성)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클레임 사유 정보를 조회했습니다.", + "data": { + "id": 8, + "type": "refund", + "code": "apidoc_sample", + "name": { + "ko": "API 문서 샘플 사유", + "en": "API Doc Sample Reason" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -860,11 +910,42 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ClaimReasonResource`). 필드 구성은 이 문서의 **POST /api/modules/sirsoft-ecommerce/admin/claim-reasons (클레임 사유 생성)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클레임 사유가 수정되었습니다.", + "data": { + "id": 8, + "type": "refund", + "code": "apidoc_sample", + "name": { + "ko": "API 문서 샘플 사유", + "en": "API Doc Sample Reason" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -874,6 +955,7 @@ Content-Type: application/json | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -903,11 +985,42 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ClaimReasonResource`). 필드 구성은 이 문서의 **POST /api/modules/sirsoft-ecommerce/admin/claim-reasons (클레임 사유 생성)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "클레임 사유 상태가 변경되었습니다.", + "data": { + "id": 8, + "type": "refund", + "code": "apidoc_sample", + "name": { + "ko": "API 문서 샘플 사유", + "en": "API Doc Sample Reason" + }, + "localized_name": "API 문서 샘플 사유", + "fault_type": "customer", + "fault_type_label": "고객 귀책", + "is_user_selectable": true, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -916,6 +1029,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md index e6d58135..74d0f505 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/coupons.md @@ -580,12 +580,14 @@ HTTP/1.1 201 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 400 | 쿠폰 다운로드 제한 횟수를 초과했습니다. | 회원 1인당 발급 제한(`per_user_limit`) 을 이미 채운 경우 | +| 400 | Bad Request | 발급 불가 사유(한도 초과·발급기간 종료·중복 발급 등). 사유 식별자에 대응하는 `messages.coupon.*` 문구가 `message` 에 담긴다 | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | `coupon_id` 가 정수가 아니거나 존재하지 않는 쿠폰인 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> -**설명** 로그인한 회원이 다운로드형 쿠폰 1건을 실제로 발급받습니다. `auth:sanctum` 인증이 필요하고 대상 쿠폰은 `couponId`(경로)로 지정하며, `UserCouponService::downloadCoupon()`이 발급기간·수량·회원당 한도·중복 발급 여부를 검증한 뒤 발급 내역을 생성해 201로 반환합니다. 한도 초과·발급기간 종료 등 발급 불가 사유는 서비스 예외의 메시지와 코드가 그대로 오류 응답에 전달됩니다. +**설명** 로그인한 회원이 다운로드형 쿠폰 1건을 실제로 발급받습니다. `auth:sanctum` 인증이 필요하고 대상 쿠폰은 `couponId`(경로)로 지정하며, `UserCouponService::downloadCoupon()`이 발급기간·수량·회원당 한도·중복 발급 여부를 검증한 뒤 발급 내역을 생성해 201로 반환합니다. 한도 초과·발급기간 종료 등 발급 불가 사유는 사유 식별자에 대응하는 다국어 키(`messages.coupon.*`)로 해석되어 `400` 으로 응답합니다. diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md index d114266a..32787321 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/extra-fee-templates.md @@ -259,6 +259,7 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `zipcode` 는 숫자 또는 `숫자-숫자` 범위 형식이어야 하며 이미 등록된 우편번호면 중복 오류 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -554,6 +555,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `ids` 는 1건 이상이어야 하며 각 값은 존재하는 템플릿 ID 여야 함 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -622,6 +624,7 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 항목별로 `items.*.zipcode` (필수, 숫자 또는 `숫자-숫자` 범위, 최대 20자) 와 `items.*.fee` (필수, 0 이상) 가 검증됨 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -692,6 +695,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `ids` 는 1건 이상이며 각 값은 존재하는 템플릿 ID, `is_active` 는 필수 boolean | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -725,7 +729,17 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿이 삭제되었습니다.", + "data": true +} +``` **에러 응답** @@ -734,6 +748,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -763,11 +778,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ExtraFeeTemplateResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id} (템플릿 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿 정보를 조회했습니다.", + "data": { + "id": 1, + "zipcode": "06234", + "fee": 5000, + "fee_formatted": "5,000원", + "region": "경기 안산 풍도동", + "description": "도서산간 지역", + "is_active": true, + "created_by": null, + "updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:20", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -876,6 +918,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). `zipcode` 는 숫자 또는 `숫자-숫자` 범위 형식이어야 하며 자신을 제외한 다른 템플릿과 중복될 수 없음 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -905,11 +948,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ExtraFeeTemplateResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{id} (템플릿 수정)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "추가배송비 템플릿 사용여부가 변경되었습니다.", + "data": { + "id": 1, + "zipcode": "06234", + "fee": 5000, + "fee_formatted": "5,000원", + "region": "경기 안산 풍도동", + "description": "도서산간 지역", + "is_active": true, + "created_by": null, + "updated_by": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:20", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -918,6 +988,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md index 7c7b7d0d..82ca21d1 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/guest.md @@ -261,7 +261,8 @@ _단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 취소 후 갱 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 (토큰이 지목한 주문 외에는 접근 불가) | -| 422 | Unprocessable Entity | 요청 파라미터 검증 위반 (`error.errors` 에 필드별 메시지), 또는 취소 처리 실패 (`주문 취소에 실패했습니다.`) | +| 422 | Unprocessable Entity | 요청 파라미터 검증 위반 (`error.errors` 에 필드별 메시지), 또는 취소 도메인 규칙 위반 (`주문 취소 처리에 실패했습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`작업 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> @@ -560,7 +561,7 @@ _단건 응답: `data` 객체의 필드 (`AdjustmentResult::toPreviewArray()` | --- | --- | --- | | 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | -| 500 | Internal Server Error | 환불 예상금액 계산 실패 (`환불 예상금액 조회에 실패했습니다.`) | +| 500 | Internal Server Error | 환불 예상금액 계산 실패 (`환불 예상금액 계산에 실패했습니다.`) | <!-- @generated:end --> @@ -634,11 +635,11 @@ _단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 구매확정 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치, 또는 `optionId` 가 해당 주문에 속하지 않는 경우 | -| 422 | Unprocessable Entity | 구매확정 불가 상태 (배송 미완료 등 — `현재 상태에서는 구매확정할 수 없습니다.`) | +| 500 | Internal Server Error | 구매확정 처리 실패 (`작업 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> -**설명** 비회원이 배송 완료된 주문의 개별 옵션(`optionId`)을 구매확정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, `OrderController@confirmOption`이 토큰으로 검증된 주문에 실제 속한 옵션인지 다시 확인한 뒤 `OrderService::confirmOption()`을 호출합니다. 주문에 속하지 않은 옵션 ID면 404, 확정 불가 상태(배송 미완료 등)면 422를 반환합니다. 구매확정 시 적립 포인트 확정 등 후속 처리가 서비스 계층에서 이어집니다. +**설명** 비회원이 배송 완료된 주문의 개별 옵션(`optionId`)을 구매확정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, `OrderController@confirmOption`이 토큰으로 검증된 주문에 실제 속한 옵션인지 다시 확인한 뒤 `OrderService::confirmOption()`을 호출합니다. 주문에 속하지 않은 옵션 ID면 404를 반환합니다. `OrderService::confirmOption()` 은 도메인 예외를 던지지 않으므로 그 밖의 실패는 모두 서버 내부 오류(500)로 구분됩니다. 구매확정 시 적립 포인트 확정 등 후속 처리가 서비스 계층에서 이어집니다. ### PUT /api/modules/sirsoft-ecommerce/guest/orders/{orderNumber}/shipping-address @@ -741,9 +742,21 @@ _단건 응답: `data` 객체의 필드 (`GuestOrderResource` — 배송지 수 | --- | --- | --- | | 404 | Not Found | `X-Guest-Order-Token` 누락·만료·주문번호 불일치 | | 422 | Unprocessable Entity | 요청 파라미터 검증 위반 (`error.errors` 에 필드별 메시지), 또는 배송이 이미 시작되어 수정 불가 (`배송 전 상태에서만 배송지를 변경할 수 있습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`배송지 변경 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> -**설명** 비회원이 배송 전 상태의 주문 배송지를 수정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, 비회원은 저장된 회원 주소(`address_id`)를 쓸 수 없으므로 수취인·연락처·주소 필드를 직접 입력받아 `OrderController@updateShippingAddress`가 회원과 동일한 `OrderService::updateShippingAddress()`로 처리합니다. 국내(`zipcode`/`address`)와 해외(`address_line_1`·`intl_city` 등) 주소 필드를 함께 지원합니다. 이미 배송이 시작된 주문 등 수정 불가 상태면 422를 반환합니다. +**설명** 비회원이 배송 전 상태의 주문 배송지를 수정합니다. 조회 토큰(`X-Guest-Order-Token`)으로 주문 소유권이 검증되며, 비회원은 저장된 회원 주소(`address_id`)를 쓸 수 없으므로 수취인·연락처·주소 필드를 직접 입력받아 `OrderController@updateShippingAddress`가 회원과 동일한 `OrderService::updateShippingAddress()`로 처리합니다. + +**국내/해외 주소 조합** `country_code` 가 국내/해외를 가르는 기준축입니다. + +| `country_code` | 필수 필드 | 비활성 측 처리 | +| --- | --- | --- | +| 미전송 또는 `KR` | `zipcode`, `address` | 해외 컬럼(`address_line_1`·`intl_*`)을 `null` 로 초기화 | +| 그 외 (예: `US`) | `address_line_1`, `intl_city`, `intl_postal_code` | 국내 컬럼(`zipcode`·`address`)을 빈 문자열로 초기화 | + +필수 필드가 비면 422 검증 오류를 반환합니다. 국내↔해외를 전환하면 반대편 컬럼이 초기화되므로 구 주소가 남지 않습니다. + +**상태코드 구분** 배송 전 상태가 아닌 등 도메인 규칙 위반은 422, 그 밖의 서버 내부 오류는 500 입니다. 500 응답에는 예외 원문이 실리지 않습니다. diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md index 4e7810e8..8641a124 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/inquiries.md @@ -65,12 +65,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteInquiry()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteInquiry()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 삭제 처리 중 예기치 못한 예외 (`문의 삭제에 실패했습니다.`) | <!-- @generated:end --> -**설명** 관리자가 상품 1:1 문의 1건을 삭제합니다. `sirsoft-ecommerce.inquiries.delete` 권한이 필요하며, `ProductInquiryService::deleteInquiry()` 가 해당 문의를 제거하고 `{deleted: true}` 를 반환합니다. 문의가 존재하지 않는 등 삭제 불가 상황에서는 서비스가 `RuntimeException` 을 던져 422 로 응답합니다. 관리자 문의 관리 화면에서 부적절하거나 중복된 문의를 정리할 때 사용합니다. +**설명** 관리자가 상품 1:1 문의 1건을 삭제합니다. `sirsoft-ecommerce.inquiries.delete` 권한이 필요하며, `ProductInquiryService::deleteInquiry()` 가 게시판 훅으로 질문 게시글을 소프트 삭제한 뒤 문의 피벗(`product_inquiries`)도 **소프트 삭제**하고 `{deleted: true}` 를 반환합니다. 피벗이 소프트 삭제로 유지되므로, 이후 게시판 관리 화면에서 질문 게시글을 복원하면 문의 피벗도 함께 복원되어 문의 목록에 다시 나타납니다. 문의가 존재하지 않는 등 삭제 불가 상황에서는 서비스가 `ProductInquiryOperationException` 을 던지고, 컨트롤러가 그 예외의 다국어 키로 422 응답을 만듭니다. 관리자 문의 관리 화면에서 부적절하거나 중복된 문의를 정리할 때 사용합니다. ### DELETE /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply @@ -121,12 +121,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 삭제 처리 중 예기치 못한 예외 (`답변 삭제에 실패했습니다.`) | <!-- @generated:end --> -**설명** 관리자가 문의에 등록된 답변을 삭제합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::deleteReply()` 가 답변을 제거하고 문의를 미답변 상태로 되돌린 뒤 `{deleted: true}` 를 반환합니다. 문의 자체는 유지되며, 잘못 등록한 답변을 회수할 때 사용합니다. +**설명** 관리자가 문의에 등록된 답변을 삭제합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::deleteReply()` 가 답변을 제거한 뒤 필터 훅 `sirsoft-ecommerce.inquiry.count_replies`(carry: int, 초기값 `0` / 인자: 부모 문의 Post ID / 반환: 살아있는 답변 수)로 잔여 답변 수를 재확인하여, **살아있는 답변이 0건일 때만** 문의를 미답변 상태(`is_answered=false`)로 되돌리고 `{deleted: true}` 를 반환합니다 — 과거 결함으로 답변이 여러 건 쌓인 문의에서 일부만 삭제해도 답변완료 표기가 유지됩니다. 문의 자체는 유지되며, 잘못 등록한 답변을 회수할 때 사용합니다. ### POST /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply @@ -185,12 +185,21 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.` — `board_not_configured`) / 이미 답변이 등록된 문의에 재등록을 시도한 경우(`이미 등록된 답변이 있습니다. 기존 답변을 수정하거나 삭제한 후 다시 등록해주세요.` — `reply_already_exists`) / 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 등록 처리 중 예기치 못한 예외 (`답변 등록에 실패했습니다.`) | +**오류 응답 예시** (이미 답변된 문의에 재등록 시 — 422) + +```json +{ + "success": false, + "message": "이미 등록된 답변이 있습니다. 기존 답변을 수정하거나 삭제한 후 다시 등록해주세요." +} +``` + <!-- @generated:end --> -**설명** 관리자가 상품 문의에 답변을 등록합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료 상태로 전환한 뒤 `{id, is_answered}` 를 201 로 반환합니다. 이미 답변이 있는 등 등록 불가 상황에서는 `RuntimeException` 이 던져져 422 로 응답합니다. 관리자가 고객 문의에 응대할 때 사용합니다. +**설명** 관리자가 상품 문의에 답변을 등록합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료 상태로 전환한 뒤 `{id, is_answered}` 를 201 로 반환합니다. 문의당 답변은 1건만 유지되는 단일 답변 정책이라, 이미 답변완료(`is_answered=true`)인 문의에 재등록을 시도하면 `reply_already_exists` 사유의 422 로 거절됩니다 — 기존 답변을 수정(PUT)하거나 삭제(DELETE) 후 다시 등록해야 합니다. 문의 게시판이 미설정인 경우에도 `board_not_configured` 사유의 422 가 반환됩니다. 관리자가 고객 문의에 응대할 때 사용합니다. ### PUT /api/modules/sirsoft-ecommerce/admin/inquiries/{inquiryId}/reply @@ -247,12 +256,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 수정 처리 중 예기치 못한 예외 (`답변 수정에 실패했습니다.`) | <!-- @generated:end --> -**설명** 관리자가 문의에 이미 등록된 답변 내용을 수정합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::updateReply()` 가 `content`(1~5000자)로 기존 답변을 갱신하고 `{id}` 를 반환합니다. 답변이 없는 문의 등 수정 불가 상황에서는 `RuntimeException` 이 던져져 422 로 응답합니다. 오탈자 정정 등 답변 내용을 고칠 때 사용합니다. +**설명** 관리자가 문의에 이미 등록된 답변 내용을 수정합니다. `sirsoft-ecommerce.inquiries.update` 권한이 필요하며, `ProductInquiryService::updateReply()` 가 `content`(1~5000자)로 기존 답변을 갱신하고 `{id}` 를 반환합니다. 답변이 없는 문의 등 수정 불가 상황에서는 `ProductInquiryOperationException` 이 던져지고, 컨트롤러가 그 예외의 다국어 키로 422 응답을 만듭니다. 오탈자 정정 등 답변 내용을 고칠 때 사용합니다. ### GET /api/modules/sirsoft-ecommerce/user/inquiries @@ -453,11 +462,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 문의 작성자(`user_id`)가 로그인 사용자와 다른 경우 (`해당 문의에 대한 권한이 없습니다.`) | | 404 | Not Found | 해당 ID 의 문의가 존재하지 않는 경우 (`문의를 찾을 수 없습니다.`) | -| 500 | Internal Server Error | 삭제 처리 중 예기치 못한 예외 (`문의 삭제에 실패했습니다.`) — 문의 게시판 미설정 등 서비스 `RuntimeException` 포함 | +| 422 | Unprocessable Entity | 삭제할 수 없는 도메인 사유 — 문의 게시판 미설정 등. 응답 `message` 에 사유가 그대로 실린다 (`문의 게시판이 설정되지 않았습니다.`) | +| 500 | Internal Server Error | 삭제 처리 중 예기치 못한 서버 오류 (`문의 삭제에 실패했습니다.`) — 예외 원문은 응답에 포함되지 않는다 | <!-- @generated:end --> -**설명** 로그인 회원이 자신이 작성한 문의를 삭제합니다. `auth:sanctum` 인증이 필요하며, 컨트롤러가 문의를 조회해 없으면 404, `inquiry->user_id` 가 로그인 사용자와 다르면 403 을 반환한 뒤 `ProductInquiryService::deleteInquiry()` 로 삭제하고 `{deleted: true}` 를 반환합니다. 마이페이지에서 본인 문의를 취소/삭제할 때 사용합니다. +**설명** 로그인 회원이 자신이 작성한 문의를 삭제합니다. `auth:sanctum` 인증이 필요하며, 컨트롤러가 문의를 조회해 없으면 404, `inquiry->user_id` 가 로그인 사용자와 다르면 403 을 반환한 뒤 `ProductInquiryService::deleteInquiry()` 로 삭제하고 `{deleted: true}` 를 반환합니다. 관리자 삭제 경로와 마찬가지로 문의 피벗은 **소프트 삭제**되며, 게시판에서 질문 게시글이 복원되면 문의도 함께 복원됩니다. 마이페이지에서 본인 문의를 취소/삭제할 때 사용합니다. ### PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId} @@ -522,7 +532,7 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 문의 작성자(`user_id`)가 로그인 사용자와 다른 경우 (`해당 문의에 대한 권한이 없습니다.`) | | 404 | Not Found | 해당 ID 의 문의가 존재하지 않는 경우 (`문의를 찾을 수 없습니다.`) | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateInquiry()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateInquiry()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 수정 처리 중 예기치 못한 예외 (`문의 수정에 실패했습니다.`) | <!-- @generated:end --> @@ -578,12 +588,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::deleteReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 삭제 처리 중 예기치 못한 예외 (`답변 삭제에 실패했습니다.`) | <!-- @generated:end --> -**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의 답변을 삭제합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::deleteReply()` 로 답변을 제거하고 `{deleted: true}` 를 반환합니다. 답변 권한을 위임받은 사용자(예: 상담원 역할)가 사용자 화면에서 답변을 회수할 때 사용합니다. +**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의 답변을 삭제합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::deleteReply()` 로 답변을 제거하고 `{deleted: true}` 를 반환합니다. 관리자 경로와 동일하게 필터 훅 `sirsoft-ecommerce.inquiry.count_replies`(carry: int, 초기값 `0` / 인자: 부모 문의 Post ID / 반환: 살아있는 답변 수)로 잔여 답변 수를 재확인해 0건일 때만 미답변 상태로 되돌립니다. 답변 권한을 위임받은 사용자(예: 상담원 역할)가 사용자 화면에서 답변을 회수할 때 사용합니다. ### POST /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply @@ -642,12 +652,12 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.` — `board_not_configured`) / 이미 답변이 등록된 문의에 재등록을 시도한 경우(`이미 등록된 답변이 있습니다. 기존 답변을 수정하거나 삭제한 후 다시 등록해주세요.` — `reply_already_exists`) / 답변 게시글 생성에 실패한 경우(`답변 등록에 실패했습니다.`) — `ProductInquiryService::createReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 등록 처리 중 예기치 못한 예외 (`답변 등록에 실패했습니다.`) | <!-- @generated:end --> -**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의에 답변을 등록합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료로 전환해 `{id, is_answered}` 를 201 로 반환합니다. 관리자 화면이 아닌 사용자 프론트에서 답변을 처리하는 상담원 역할에 사용합니다. +**설명** 사용자 표면에서 문의 답변 권한을 가진 회원이 문의에 답변을 등록합니다. `auth:sanctum` 인증에 더해 컨트롤러가 `PermissionHelper::check('sirsoft-ecommerce.inquiries.update')` 로 답변 권한을 확인(없으면 403)한 뒤 `ProductInquiryService::createReply()` 가 `content`(1~5000자)로 답변을 저장하고 문의를 답변완료로 전환해 `{id, is_answered}` 를 201 로 반환합니다. 관리자 경로와 동일한 단일 답변 정책이 적용되어, 이미 답변완료인 문의에 재등록하면 `reply_already_exists` 사유의 422, 문의 게시판 미설정이면 `board_not_configured` 사유의 422 로 거절됩니다. 관리자 화면이 아닌 사용자 프론트에서 답변을 처리하는 상담원 역할에 사용합니다. ### PUT /api/modules/sirsoft-ecommerce/user/inquiries/{inquiryId}/reply @@ -704,7 +714,7 @@ _단건 응답: `data` 객체의 필드._ | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 답변 권한(`sirsoft-ecommerce.inquiries.update`)이 없는 경우 (`해당 문의에 대한 권한이 없습니다.`) | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `RuntimeException` | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) / 문의를 찾을 수 없거나(`문의를 찾을 수 없습니다.`) 문의 게시판이 설정되지 않은 경우(`문의 게시판이 설정되지 않았습니다.`) — `ProductInquiryService::updateReply()` 의 `ProductInquiryOperationException` | | 500 | Internal Server Error | 답변 수정 처리 중 예기치 못한 예외 (`답변 수정에 실패했습니다.`) | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md index f8236706..14db7a29 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/mileage-transactions.md @@ -637,7 +637,65 @@ _목록 응답: `data.data[]` 배열 항목의 필드. 페이지네이션 없는 **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "연결 거래를 조회했습니다.", + "data": { + "data": [ + { + "number": 1, + "id": 531, + "user_id": 166, + "currency": "KRW", + "type": "order_use", + "type_label": "주문 사용", + "admin_badge_group": "blue", + "user_display_category": "use", + "amount": -500, + "amount_formatted": "-500원", + "remaining_amount": 0, + "remaining_amount_formatted": "0원", + "balance_after": 500, + "order_id": 436, + "order_option_id": 824, + "order_cancel_id": null, + "source_transaction_id": 528, + "granted_by": null, + "description": "주문 마일리지 사용", + "memo": null, + "expires_at": null, + "expires_at_formatted": null, + "expires_at_date": null, + "expired_at": null, + "expired_at_formatted": null, + "created_at": "2026-07-07T05:47:31+00:00", + "created_at_formatted": "2026-07-07 14:47:31", + "created_at_date": "2026-07-07", + "is_earning": false, + "can_edit_expiry": false, + "expired_amount": 0, + "expired_amount_formatted": "0원", + "expiry_state": "active", + "abilities": { + "can_manage": true, + "can_edit": false + } + } + ], + "abilities": { + "can_manage": true + }, + "currencies": [] + } +} +``` + +> 연결 거래가 없으면 `data.data` 가 빈 배열이다 (404 가 아니다 — 404 는 `{id}` 거래 자체가 없을 때뿐이다). 페이지네이션이 없는 컬렉션이라 `data.pagination` 키는 존재하지 않으며, `data.currencies` 는 이 엔드포인트가 주입하지 않아 항상 `[]` 다. **에러 응답** @@ -645,7 +703,7 @@ _목록 응답: `data.data[]` 배열 항목의 필드. 페이지네이션 없는 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.mileage.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | `{id}` 에 해당하는 마일리지 거래가 없는 경우 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md index 0ca1fa31..459a7b78 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/orders.md @@ -841,7 +841,8 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 취소 처리 실패 — 취소 불가 상태, PG 취소 실패, 가상계좌 환불계좌 정보 누락 등 (`주문 취소에 실패했습니다.` + `errors.detail` 에 예외 메시지) | +| 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 취소 도메인 규칙 위반 — 취소 불가 상태, PG 취소 실패, 가상계좌 환불계좌 정보 누락 등 (`주문 취소에 실패했습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`작업 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> @@ -1219,6 +1220,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 입금액 불일치(`입금액이 결제예정금액과 일치하지 않습니다.` + `errors.detail`), 또는 입금확인 처리 실패 — 무통장 결제가 아닌 주문·이미 결제완료된 주문 등 (`입금확인 처리에 실패했습니다.` + `errors.detail`) | +| 500 | Internal Server Error | 서버 내부 오류 (`작업 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> @@ -1349,7 +1351,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | -| 500 | Server Error | 환불 예상금액 계산 중 오류 (`환불 예상금액 조회에 실패했습니다.`) | +| 500 | Server Error | 환불 예상금액 계산 중 오류 (`환불 예상금액 계산에 실패했습니다.`) | <!-- @generated:end --> @@ -2439,7 +2441,8 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.cancel`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터 검증 실패(본인 주문 아님·취소 불가 상태 포함), 또는 취소 처리 실패 (`exceptions.order_cancel_failed`) | +| 422 | Unprocessable Entity | 요청 파라미터 검증 실패(본인 주문 아님·취소 불가 상태 포함), 또는 취소 도메인 규칙 위반 (`exceptions.order_cancel_failed`) | +| 500 | Internal Server Error | 서버 내부 오류 (`exceptions.operation_failed`) | <!-- @generated:end --> @@ -2815,7 +2818,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-orders.confirm`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 구매확정 불가 — 본인 주문이 아니거나 배송완료 전 등 확정 불가 상태 (`exceptions.order_option_cannot_confirm`) | +| 500 | Internal Server Error | 구매확정 처리 실패 (`exceptions.operation_failed`) | <!-- @generated:end --> @@ -2884,7 +2887,8 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 재주문 실패 — 주문이 없거나 본인 주문이 아닌 경우 (`재주문에 실패했습니다.`) | +| 422 | Unprocessable Entity | 재주문 도메인 규칙 위반 — 장바구니 담기 불가·수량 한도 초과 등 (`재주문에 실패했습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`exceptions.operation_failed`) | <!-- @generated:end --> @@ -2989,6 +2993,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 404 | Not Found | 주문이 없거나 본인 주문이 아닌 경우 (`주문을 찾을 수 없습니다.`) | | 422 | Unprocessable Entity | 요청 파라미터 검증 실패, 또는 배송지 변경 불가 상태 — 이미 배송이 시작된 주문 등 (`배송 전 상태에서만 배송지를 변경할 수 있습니다.`) | +| 500 | Internal Server Error | 서버 내부 오류 (`배송지 변경 처리 중 오류가 발생했습니다.`) | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md index 862c31d8..4aaaa7ce 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/presets.md @@ -21,7 +21,7 @@ <!-- @generated:start:api.modules.sirsoft-ecommerce.admin.presets.index --> - **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.index` - **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@index` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-ecommerce.products.read` **요청 파라미터** @@ -103,7 +103,7 @@ HTTP/1.1 200 <!-- @generated:start:api.modules.sirsoft-ecommerce.admin.presets.store --> - **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.store` - **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@store` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-ecommerce.products.update` **요청 파라미터** @@ -192,7 +192,7 @@ HTTP/1.1 201 <!-- @generated:start:api.modules.sirsoft-ecommerce.admin.presets.destroy --> - **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.destroy` - **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@destroy` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-ecommerce.products.update` **요청 파라미터** @@ -250,7 +250,7 @@ HTTP/1.1 200 <!-- @generated:start:api.modules.sirsoft-ecommerce.admin.presets.update --> - **라우트명**: `api.modules.sirsoft-ecommerce.admin.presets.update` - **컨트롤러**: `Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\SearchPresetController@update` -- **인증/권한**: `auth:sanctum` +- **인증/권한**: `auth:sanctum`, `admin`, `permission:admin,sirsoft-ecommerce.products.update` **요청 파라미터** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md index 620d67d5..bf302721 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-common-infos.md @@ -28,8 +28,8 @@ | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | search | query | string | 아니오 | max 255 | 검색어 (지정한 검색 대상 필드에서 부분 일치) | -| active_only | query | boolean | 아니오 | — | <!-- TODO: 용도 --> | -| default_only | query | boolean | 아니오 | — | <!-- TODO: 용도 --> | +| active_only | query | boolean | 아니오 | `true`, `false` | 활성 공통정보만 조회 (true 일 때만 필터 적용, false·미전달은 전체). 쿼리 문자열 `"true"`/`"false"` 도 인식 | +| default_only | query | boolean | 아니오 | `true`, `false` | 기본 공통정보만 조회 (true 일 때만 필터 적용). 쿼리 문자열 `"true"`/`"false"` 도 인식 | | per_page | query | string | 아니오 | — | 페이지당 항목 수 | **요청 예시** @@ -270,6 +270,7 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -299,11 +300,29 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| common_info_id | integer | `5` | 삭제된 공통정보의 기본 키 | +| products_count | integer | `2` | 이 공통정보를 참조하던 상품 수 (삭제 시 해당 상품의 연결이 해제됨) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "공통정보가 삭제되었습니다.", + "data": { + "common_info_id": 5, + "products_count": 2 + } +} +``` **에러 응답** @@ -312,6 +331,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -341,11 +361,46 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductCommonInfoResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id} (공통정보 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "공통정보 목록을 불러왔습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 공통정보(수정)", + "en": "API Doc Sample Common Info (updated)" + }, + "localized_name": "API 문서 샘플 공통정보(수정)", + "content": { + "ko": "배송은 결제 후 1~3일 이내 출고됩니다." + }, + "localized_content": "배송은 결제 후 1~3일 이내 출고됩니다.", + "content_mode": "text", + "is_default": false, + "is_active": true, + "sort_order": 0, + "icon": "info-circle", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:26", + "products_count": 0, + "language_count": 1, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -472,6 +527,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -501,11 +557,46 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductCommonInfoResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-common-infos/{id} (공통정보 수정)** 응답 필드 표와 동일합니다._ 비활성으로 전환하면 `공통정보가 비활성화되었습니다.` 가 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "공통정보가 활성화되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "API 문서 샘플 공통정보(수정)", + "en": "API Doc Sample Common Info (updated)" + }, + "localized_name": "API 문서 샘플 공통정보(수정)", + "content": { + "ko": "배송은 결제 후 1~3일 이내 출고됩니다." + }, + "localized_content": "배송은 결제 후 1~3일 이내 출고됩니다.", + "content_mode": "text", + "is_default": false, + "is_active": true, + "sort_order": 0, + "icon": "info-circle", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:26", + "products_count": 0, + "language_count": 1, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -514,6 +605,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-common-infos.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md index efb8732b..43d67ba8 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-labels.md @@ -247,11 +247,29 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| label_id | integer | `7` | 삭제된 라벨의 기본 키 | +| products_count | integer | `0` | 삭제 시점에 이 라벨을 쓰던 상품 수 (연결 상품이 있으면 삭제가 거부되므로 언제나 0) | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨이 삭제되었습니다.", + "data": { + "label_id": 7, + "products_count": 0 + } +} +``` **에러 응답** @@ -260,6 +278,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -289,11 +308,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductLabelResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-labels/{id} (라벨 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨 정보를 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "베스트", + "en": "Best" + }, + "color": "#4F46E5", + "is_active": true, + "sort_order": 1, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:27", + "assignments_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -398,6 +444,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -427,11 +474,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductLabelResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-labels/{id} (라벨 수정)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "라벨 상태가 변경되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "베스트", + "en": "Best" + }, + "color": "#4F46E5", + "is_active": true, + "sort_order": 1, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:27", + "assignments_count": 0, + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -440,6 +514,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-labels.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md index 9b81ea14..4c7ed566 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/product-notice-templates.md @@ -336,6 +336,7 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.create`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -365,11 +366,27 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| template_id | integer | `3` | 삭제된 템플릿의 기본 키 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 삭제되었습니다.", + "data": { + "template_id": 3 + } +} +``` **에러 응답** @@ -378,6 +395,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -407,11 +425,46 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductNoticeTemplateResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} (템플릿 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시 목록을 불러왔습니다.", + "data": { + "id": 1, + "name": { + "ko": "의류 고시(수정)", + "en": "Clothing Notice (Updated)" + }, + "localized_name": "의류 고시(수정)", + "category": "clothing", + "fields": [ + { + "label": "품명", + "value": "샘플" + } + ], + "fields_count": 1, + "is_active": true, + "sort_order": 0, + "icon": "file-alt", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:27", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -534,6 +587,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -563,11 +617,46 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductNoticeTemplateResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} (템플릿 수정)** 응답 필드 표와 동일합니다._ 원본과 동일한 항목 구성으로 새 기본 키가 발급됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 복사되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "의류 고시(수정)", + "en": "Clothing Notice (Updated)" + }, + "localized_name": "의류 고시(수정)", + "category": "clothing", + "fields": [ + { + "label": "품명", + "value": "샘플" + } + ], + "fields_count": 1, + "is_active": true, + "sort_order": 0, + "icon": "file-alt", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:27", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -576,6 +665,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.create`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -605,11 +695,46 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ProductNoticeTemplateResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/product-notice-templates/{id} (템플릿 수정)** 응답 필드 표와 동일합니다._ 비활성으로 전환하면 `상품정보제공고시가 비활성화되었습니다.` 가 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품정보제공고시가 활성화되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "의류 고시(수정)", + "en": "Clothing Notice (Updated)" + }, + "localized_name": "의류 고시(수정)", + "category": "clothing", + "fields": [ + { + "label": "품명", + "value": "샘플" + } + ], + "fields_count": 1, + "is_active": true, + "sort_order": 0, + "icon": "file-alt", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:00:27", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -618,6 +743,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.product-notice-templates.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md index a8f591cb..58f95aa8 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/products.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/products.md @@ -16,6 +16,27 @@ --- +## 옵션의 무게·부피 (`options[].weight` / `options[].volume`) + +옵션 리소스(`ProductOptionResource`)는 배송비 계산에 쓰이는 물성 두 개를 함께 내려보냅니다. + +| 필드 | 타입 | 단위 | 설명 | +| --- | --- | --- | --- | +| weight | number\|null | g (그램) | 옵션 1개의 무게. 미입력이면 `null` | +| volume | number\|null | cm³ (세제곱센티미터) | 옵션 1개의 부피. 미입력이면 `null` | + +단위는 저장 단위 그대로입니다. 배송정책의 구간·단위값은 kg / L 로 입력받으며, 환산은 +배송비를 계산하는 시점에 한 번만 수행됩니다 (`docs/api/shipping-policies.md` 참고). + +`null` 과 `0` 은 구분됩니다 — `null` 은 미입력, `0` 은 "무게 없음"입니다. 수정 요청은 +전달된 필드만 반영하므로, 편집 화면이 이 값을 받지 못한 채 저장하면 기존 값이 그대로 +유지되지만 화면에 `0` 으로 그려 놓고 저장하면 실측값이 0 으로 덮어써집니다. + +주문 생성 시 이 값은 주문 옵션(`unit_weight`/`unit_volume`)에 g / cm³ 그대로 복사되고, +주문의 `total_weight`/`total_volume` 은 옵션 소계의 합입니다. + +--- + ## 목록 응답의 총 건수와 페이지 이동 상품 목록(관리자·공개 양쪽)은 총 건수를 상한까지만 셉니다. 상한을 넘으면 응답의 @@ -1321,6 +1342,7 @@ HTTP/1.1 201 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 상품 이미지 개수 상한 초과 (`ProductImageUploadLimitException`) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1383,6 +1405,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1436,6 +1459,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | | 404 | Not Found | `id` 에 해당하는 이미지가 없는 경우 (`messages.product_images.not_found`) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1694,6 +1718,7 @@ HTTP/1.1 201 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 상품 이미지 개수 상한 초과 (`ProductImageUploadLimitException`) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1748,6 +1773,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.products.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -3392,7 +3418,7 @@ HTTP/1.1 200 | product | path | string | 예 | — | 대상 product의 식별자 | | page | query | integer | 아니오 | min 1 | 조회할 페이지 번호 (1부터 시작) | | per_page | query | integer | 아니오 | min 1, max 100 | 페이지당 항목 수 | -| exclude_secret | query | boolean | 아니오 | — | 비밀글 제외 여부 (기본 `false` — 포함). `true` 면 비밀 문의를 목록에서 제외합니다. 쿼리 문자열 `"true"`/`"false"` 도 해석되며, 해석할 수 없는 값은 그대로 검증되어 422 가 됩니다 | +| exclude_secret | query | boolean | 아니오 | — | 비밀글 제외 여부 (기본 `false` — 포함). `true` 면 비밀 문의를 목록에서 제외합니다. 쿼리 문자열 `"true"`/`"false"` 도 해석되며, 해석할 수 없는 값은 그대로 검증되어 422 가 됩니다. **보안 경계가 아니라 단순 표시 필터입니다** — 비밀 문의의 노출/마스킹은 이 값과 무관하게 서버가 요청자 신원으로 결정합니다 | **요청 예시** @@ -3495,7 +3521,7 @@ HTTP/1.1 200 <!-- @generated:end --> -**설명** 상품의 1:1 문의 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductInquiryService::getProductInquiries()`가 게시판 모듈과 연동된 문의 글을 페이지네이션해 `items`와 `board_settings`(비밀글 모드·카테고리 등) 메타를 반환합니다. `per_page`/`page`/`exclude_secret` 쿼리로 조회 범위를 조정하며, 비밀 문의는 설정과 열람 권한에 따라 마스킹됩니다. 상품 상세의 문의 탭에 사용됩니다. +**설명** 상품의 1:1 문의 목록을 조회합니다. `optional.sanctum`(회원/비회원 모두 접근) + `sirsoft-ecommerce.user-products.read` 권한이 적용되며, `ProductInquiryService::getProductInquiries()`가 게시판 모듈과 연동된 문의 글을 페이지네이션해 `items`와 `board_settings`(비밀글 모드·카테고리 등) 메타를 반환합니다. `per_page`/`page`/`exclude_secret` 쿼리로 조회 범위를 조정합니다. 비밀 문의는 요청자 신원(작성자 본인·게시판 비밀글 열람 권한)에 따라 서버가 마스킹하며, 열람 권한이 없으면 `title`은 "비밀글" 플레이스홀더로 치환되고 `content`·`reply`는 `null`, `attachments`는 빈 배열로 내려갑니다(`is_secret`·작성자·답변 여부 등 메타는 유지). 이 마스킹은 게시판 모듈이 실어 보내는 권위 플래그(`can_view_secret`)를 신뢰하며, 플래그가 없으면 fail-closed 로 마스킹합니다. `exclude_secret` 쿼리는 표시 필터일 뿐 이 판정에 관여하지 않습니다. 상품 상세의 문의 탭에 사용됩니다. ### POST /api/modules/sirsoft-ecommerce/products/{product}/inquiries @@ -3565,11 +3591,11 @@ HTTP/1.1 201 | --- | --- | --- | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-products.read`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 도메인 규칙 위반(비밀글 비허용 등, `RuntimeException` → `messages.inquiries.create_failed`) | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) 또는 도메인 규칙 위반(문의 게시판 미설정·게시판 모듈 불가 등, `ProductInquiryOperationException` → `messages.inquiries.operation_failed_reason` 에 사유가 담김) | <!-- @generated:end --> -**설명** 상품에 1:1 문의를 작성합니다. `optional.sanctum` + `sirsoft-ecommerce.user-products.read` 권한이 적용되며(선택적 인증 표면이지만 실제 작성은 인증 사용자를 전제), `ProductInquiryService::createInquiry()`가 게시판 모듈과 연동해 문의 글을 생성하고 성공 시 201과 생성된 `id`를 반환합니다. `content`는 필수, `title`/`category`/`is_secret`은 선택이며, 첨부는 사전 업로드한 `temp_key`로 연결됩니다. 도메인 규칙 위반(비밀글 비허용 등)은 `RuntimeException`으로 422를 반환합니다. +**설명** 상품에 1:1 문의를 작성합니다. `optional.sanctum` + `sirsoft-ecommerce.user-products.read` 권한이 적용되며(선택적 인증 표면이지만 실제 작성은 인증 사용자를 전제), `ProductInquiryService::createInquiry()`가 게시판 모듈과 연동해 문의 글을 생성하고 성공 시 201과 생성된 `id`를 반환합니다. `content`는 필수, `title`/`category`/`is_secret`은 선택이며, 첨부는 사전 업로드한 `temp_key`로 연결됩니다. 도메인 규칙 위반(문의 게시판 미설정·게시판 모듈 불가 등)은 `ProductInquiryOperationException` 으로 422 를 반환하며, 예외가 들고 있는 다국어 키로 해석한 사유가 응답 문구에 포함됩니다. 그 밖의 예외는 서버 결함으로 보아 500 입니다. ### GET /api/modules/sirsoft-ecommerce/products/{product}/reviews diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md index 9ae14fea..fcb0a535 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/promotion-coupons.md @@ -215,7 +215,7 @@ HTTP/1.1 200 | valid_to | body | date | 아니오 | — | 유효기간 종료일 (valid_type=period 시 필수, valid_from 이후) | | issue_from | body | date | 아니오 | — | 발급기간 시작 일시 (미입력 시 상시발급) | | issue_to | body | date | 아니오 | — | 발급기간 종료 일시 (issue_from 이후) | -| is_combinable | body | boolean | 아니오 | — | combinable 여부 | +| is_combinable | body | boolean | 아니오 | — | 다른 쿠폰과 중복 사용 가능 여부. 문자열 `"true"`/`"false"`/`"1"`/`"0"` 도 수용해 boolean 으로 정규화한다 (해석 불가값은 422) | | target_scope | body | string | 아니오 | `all`, `products`, `categories` | 적용 범위: all(전체 상품), products(특정 상품), categories(특정 카테고리) | | products | body | array | 아니오 | — | 적용 상품 목록 (`target_scope=products`), 항목별 `{id, type: include\|exclude}` | | categories | body | array | 아니오 | — | 적용 카테고리 목록 (`target_scope=categories`), 항목별 `{id, type: include\|exclude}` | @@ -515,11 +515,25 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| coupon_id | integer | `52` | 삭제된 쿠폰(정의)의 ID | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰이 삭제되었습니다.", + "data": { + "coupon_id": 52 + } +} +``` **에러 응답** @@ -528,6 +542,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -557,11 +572,36 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`CouponResource`) — 생성(`POST /admin/promotion-coupons`) 201 응답의 `data` 와 동일 스키마이므로 필드 표는 그 절을 참조합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 정보를 조회했습니다.", + "data": { + "id": 2, + "name": "신규가입 쿠폰", + "discount_type": "fixed", + "discount_value": 5000, + "issue_status": "issuing", + "is_combinable": false, + "target_scope": "all", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + +_예시는 대표 필드만 발췌한 축약본입니다. 전체 필드는 생성 201 응답 예시와 동일합니다._ **에러 응답** @@ -605,7 +645,7 @@ Authorization: Bearer {YOUR_TOKEN} | valid_to | body | date | 아니오 | — | 유효기간 종료일 (valid_type=period 시 필수, valid_from 이후) | | issue_from | body | date | 아니오 | — | 발급기간 시작 일시 (미입력 시 상시발급) | | issue_to | body | date | 아니오 | — | 발급기간 종료 일시 (issue_from 이후) | -| is_combinable | body | boolean | 아니오 | — | combinable 여부 | +| is_combinable | body | boolean | 아니오 | — | 다른 쿠폰과 중복 사용 가능 여부. 문자열 `"true"`/`"false"`/`"1"`/`"0"` 도 수용해 boolean 으로 정규화한다 (해석 불가값은 422) | | target_scope | body | string | 아니오 | `all`, `products`, `categories` | 적용 범위: all(전체 상품), products(특정 상품), categories(특정 카테고리) | | products | body | array | 아니오 | — | 적용 상품 목록 (`target_scope=products`), 항목별 `{id, type: include\|exclude}` | | categories | body | array | 아니오 | — | 적용 카테고리 목록 (`target_scope=categories`), 항목별 `{id, type: include\|exclude}` | @@ -811,6 +851,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -890,6 +931,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -951,16 +993,59 @@ _목록 응답: `data.data[]` 배열 항목의 필드 (`CouponIssueResource`) + **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "쿠폰 발급 내역을 조회했습니다.", + "data": { + "data": [ + { + "id": 1, + "coupon_id": 1, + "user_id": "a1e0a91a-fba6-491c-a53e-7285a5686857", + "coupon_code": "WELCOME2026", + "status": "available", + "status_label": "사용가능", + "status_badge_color": "blue", + "issued_at": "2026-07-08 10:50:11", + "expired_at": "2026-07-31 23:59:59", + "used_at": null, + "order_id": null, + "order_number": null, + "discount_amount": null, + "is_used": false, + "is_expired": false, + "is_usable": true, + "is_cancellable": true, + "user_name": "홍길동" + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 1, + "from": 1, + "to": 1, + "has_more_pages": false + } + } +} +``` **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | +| 400 | Bad Request | `{id}` 에 해당하는 쿠폰이 없는 경우 (`sirsoft-ecommerce::exceptions.coupon_not_found`) | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -1053,6 +1138,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.promotion-coupon.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md index 33091217..c5d8fc7f 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/review-image.md @@ -73,10 +73,14 @@ Content-Disposition: attachment; filename="review.jpg" | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 404 | Not Found | 해시에 해당하는 리뷰 이미지가 없거나(`findByHash()` → null), 레코드는 있으나 스토리지에 실제 파일이 없는 경우 (`messages.reviews.image_not_found`) | +| 404 | Not Found | 해시에 해당하는 리뷰 이미지가 없거나(`findByHash()` → null), 레코드는 있으나 스토리지에 실제 파일이 없는 경우, **또는 이미지가 속한 리뷰가 노출(VISIBLE) 상태가 아니면서 유효한 서명도 없는 경우**(숨김·블라인드·대기 리뷰의 이미지는 존재를 감추기 위해 동일하게 404) (`messages.reviews.image_not_found`) | <!-- @generated:end --> -**설명** 리뷰에 첨부된 이미지를 해시(12자) 기반으로 공개 서빙합니다. 인증이 필요 없으며, `ReviewImageController@download` 가 `ProductReviewImageService::download()` 로 해시에 해당하는 이미지를 찾아 스트림(`StreamedResponse`)으로 반환합니다. 해시에 해당하는 이미지가 없으면 404 를 반환합니다. `<img src>` 등에서 리뷰 이미지 원본을 표시할 때 사용하며, 실제 파일 경로를 노출하지 않고 해시로만 접근하게 합니다. +**설명** 리뷰에 첨부된 이미지를 해시(12자) 기반으로 서빙합니다. `ReviewImageController@download` 가 `ProductReviewImageService::download()` 로 해시에 해당하는 이미지를 찾아 스트림(`StreamedResponse`)으로 반환합니다. `<img src>` 등에서 리뷰 이미지 원본을 표시할 때 사용하며, 실제 파일 경로를 노출하지 않고 해시로만 접근하게 합니다. + +이미지 서빙은 **부모 리뷰가 노출(VISIBLE) 상태일 때로 한정**됩니다(KVE-2026-1914). 숨김·블라인드·대기 상태 리뷰의 이미지는 해시가 유효해도 서빙되지 않고 404 를 반환합니다 — 본문이 감춰진 리뷰의 이미지가 해시만으로 노출되는 것을 막고, 존재 자체를 드러내지 않기 위해 "이미지 없음"과 동일한 404 로 응답합니다. 노출 상태 리뷰의 이미지는 별도 인증 없이 공개 접근할 수 있습니다. + +브라우저 `<img src>` 는 Authorization 헤더를 실을 수 없으므로, 숨김 리뷰의 이미지를 싣는 관리자 응답(리뷰 목록·상세)의 `download_url` 에는 `expires`·`signature` 쿼리가 붙은 **한시 서명 URL** 이 직렬화되며, 이 엔드포인트는 유효한 서명을 상태 게이트와 동등한 자격으로 허용합니다. 서명이 변조·만료된 요청은 종전과 동일하게 404 로 차단됩니다. 노출(VISIBLE) 리뷰의 `download_url` 은 무서명 공개 URL(직접 URL/CDN 또는 이 API 경로)을 유지합니다. diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md index c5ba3335..f086d054 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/reviews.md @@ -259,11 +259,29 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 성공 플래그만 반환한다 (Resource 로 감싸지 않는다)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | boolean | `true` | 삭제 성공 여부. 성공 응답에서는 항상 `true` 이며, 실패는 `500` 으로 갈린다 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰가 삭제되었습니다.", + "data": { + "deleted": true + } +} +``` + +> 삭제 전 컨트롤러가 `images` 관계를 로드한다 — 첨부 이미지 파일까지 함께 정리하기 위해서다. DB CASCADE 에 맡기지 않고 Service 가 명시적으로 삭제하므로 훅 발화와 파일 정리가 보장된다. **에러 응답** @@ -271,7 +289,8 @@ Authorization: Bearer {YOUR_TOKEN} | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.delete`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `review` 의 리뷰가 없는 경우 (라우트 모델 바인딩이 해석에 실패) | +| 500 | Internal Server Error | 삭제 처리 중 예외 발생 (`messages.reviews.delete_failed`) | <!-- @generated:end --> @@ -337,7 +356,64 @@ _단건 응답: `data` 객체의 필드._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰를 조회했습니다.", + "data": { + "id": 99, + "product_id": 320, + "order_option_id": 859, + "user_id": "a231747f-e82e-4cf2-9ae1-a261849dce40", + "user": { + "uuid": "a231747f-e82e-4cf2-9ae1-a261849dce40", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "product": { + "id": 320, + "name": "API 문서 샘플 상품", + "thumbnail_url": null + }, + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", + "rating": 5, + "content": "Molestiae repellendus accusantium omnis.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "images": [], + "image_count": 0, + "orderOption": { + "id": 859, + "order_id": 455, + "order_number": "ORD-20260707-000123", + "quantity": 1, + "ordered_at": "2026-07-07 14:40:00" + }, + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "reply_content_mode": "text", + "reply_admin_uuid": null, + "reply_admin": null, + "replied_at": null, + "reply_updated_at": null, + "created_at": "2026-07-07 14:47:31", + "updated_at": "2026-07-07 14:47:31", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -345,7 +421,8 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.read`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `review` 의 리뷰가 없는 경우 (라우트 모델 바인딩이 해석에 실패) | +| 500 | Internal Server Error | 조회 중 예외 발생 (`messages.reviews.fetch_failed`) | <!-- @generated:end --> @@ -405,7 +482,50 @@ _단건 응답: `data` 객체의 필드._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "판매자 답변이 삭제되었습니다.", + "data": { + "id": 1, + "product_id": 1, + "order_option_id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", + "rating": 5, + "content": "Alias quas iusto dolorem eum eveniet.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "has_reply": false, + "has_reply_label": "미답변", + "has_reply_badge_color": "gray", + "reply_content": null, + "reply_content_mode": "text", + "replied_at": null, + "reply_updated_at": null, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-08-16 01:30:00", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` + +> 답변이 지워진 결과가 응답에 그대로 반영된다 — `has_reply` 는 `false`, `reply_content`·`replied_at`·`reply_updated_at` 은 `null` 로 비워진다. 리뷰 본문(`content`·`rating`·`status`)은 그대로 유지된다. **에러 응답** @@ -413,7 +533,8 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 404 | Not Found | 해당 `review` 의 리뷰가 없는 경우 (라우트 모델 바인딩이 해석에 실패) | +| 500 | Internal Server Error | 답변 삭제 처리 중 예외 발생 (`messages.reviews.reply_delete_failed`) | <!-- @generated:end --> @@ -483,7 +604,50 @@ _단건 응답: `data` 객체의 필드._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "판매자 답변이 저장되었습니다.", + "data": { + "id": 1, + "product_id": 1, + "order_option_id": 1, + "user_id": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "user": { + "uuid": "a234c2b1-cde8-437f-b28b-23323be2b98d", + "name": "API 문서 샘플 사용자", + "email": "apidoc-sample-user@example.com" + }, + "option_snapshot": "{\"id\":104,\"option_code\":\"FWACBAVCBKCD\"}", + "option_snapshot_label": "", + "rating": 5, + "content": "Alias quas iusto dolorem eum eveniet.", + "content_mode": "text", + "status": "visible", + "status_label": "전시중", + "status_badge_color": "blue", + "has_reply": true, + "has_reply_label": "답변완료", + "has_reply_badge_color": "green", + "reply_content": "소중한 후기 감사합니다. 앞으로도 좋은 상품으로 보답하겠습니다.", + "reply_content_mode": "text", + "replied_at": "2026-08-16 01:30:00", + "reply_updated_at": "2026-08-16 01:30:00", + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-08-16 01:30:00", + "abilities": { + "can_update": true, + "can_delete": true + } + } +} +``` + +> 답변 저장 결과가 응답에 반영된다 — `has_reply` 는 `true`, `has_reply_badge_color` 는 `green` 으로 바뀌고 `replied_at`·`reply_updated_at` 이 채워진다. 이미 답변이 있던 리뷰를 다시 호출하면 내용이 **갱신**되며(중복 답변이 생기지 않는다) `reply_updated_at` 만 새로 갱신된다. 답변 작성자는 요청한 관리자(`Auth::id()`)로 기록된다. **에러 응답** @@ -491,8 +655,9 @@ _단건 응답: `data` 객체의 필드._ | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.reviews.update`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 404 | Not Found | 해당 `review` 의 리뷰가 없는 경우 (라우트 모델 바인딩이 해석에 실패) | +| 422 | Unprocessable Entity | `reply_content` 가 비었거나 1~2000자 범위를 벗어난 경우, `reply_content_mode` 가 `text`/`html` 이 아닌 경우 | +| 500 | Internal Server Error | 답변 저장 중 예외 발생 (`messages.reviews.reply_save_failed`) | <!-- @generated:end --> @@ -760,18 +925,63 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 작성 자격 판정 결과 (`ProductReviewService::canWrite()` 가 반환한 배열 — Resource 로 감싸지 않는다)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| can_write | boolean | `true` | 이 주문 옵션에 리뷰를 쓸 수 있는지. 화면은 이 값으로 작성 버튼 노출을 결정한다 | +| reason | string \| null | `null` | 쓸 수 없는 사유 키. `can_write` 가 `true` 면 `null` | + +`reason` 이 가질 수 있는 값: + +| 값 | 의미 | +| --- | --- | +| `order_option_not_found` | 해당 주문 옵션이 존재하지 않음 | +| `not_own_order` | 본인 주문이 아님 | +| `not_confirmed` | 구매확정(`CONFIRMED`) 상태가 아님 | +| `deadline_passed` | 작성 가능 기간이 지남 (판정 규칙은 `ReviewWritePolicy` 단일 SSoT) | +| `already_written` | 이 주문 옵션으로 이미 리뷰를 작성함 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +작성 가능한 경우: + +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰 작성 가능 여부를 확인했습니다.", + "data": { + "can_write": true, + "reason": null + } +} +``` + +작성 불가한 경우 (이미 작성함): + +```json +{ + "success": true, + "message": "리뷰 작성 가능 여부를 확인했습니다.", + "data": { + "can_write": false, + "reason": "already_written" + } +} +``` + +> 작성 불가는 **에러가 아니라 정상 응답**이다. 존재하지 않는 주문 옵션이나 남의 주문도 `404`/`403` 이 아니라 `200` + `can_write: false` 로 응답하므로, 이 엔드포인트로 주문 존재 여부를 탐색할 수 없다. **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 판정 중 예외 발생 (`messages.reviews.can_write_check_failed`) | <!-- @generated:end --> @@ -801,19 +1011,38 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 성공 플래그만 반환한다 (관리자 삭제 엔드포인트와 같은 형태)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | boolean | `true` | 삭제 성공 여부. 성공 응답에서는 항상 `true` | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "리뷰가 삭제되었습니다.", + "data": { + "deleted": true + } +} +``` + +> 삭제 전 `images` 관계를 로드해 첨부 이미지 파일까지 함께 정리한다. **에러 응답** | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우 | -| 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.user-reviews.write`)이 없는 경우, 또는 **본인이 작성한 리뷰가 아닌 경우** (`messages.reviews.forbidden`) | +| 404 | Not Found | 해당 `review` 의 리뷰가 없는 경우 (라우트 모델 바인딩이 해석에 실패) | +| 500 | Internal Server Error | 삭제 처리 중 예외 발생 (`messages.reviews.delete_failed`) | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md index 16fa1143..940e9f07 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/settings.md @@ -43,7 +43,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | | basic_info | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 쇼핑몰 기본 정보 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처·이메일 등) | -| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙) | +| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙). `removed_default_currencies` 는 관리자가 삭제한 기본 제공 통화 코드 목록으로, 서버가 저장 시점에 도출해 기록한다 (관리자 응답 전용 — 공개 설정에는 노출되지 않음) | | order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 주문/결제 설정 (기본 PG·병합된 결제수단·은행/무통장 계좌·자동취소·장바구니 만료·현금영수증 발급 제공자·자진발급·배송비 과세 방식 등) | | shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 배송 설정 (기본 국가·배송 가능 국가·무료배송·DB 관리 배송사(carriers)·배송유형(types)·계산 API 후보 필드 포함) | | seo | object | `{"meta_category_title":"{commerce_name} - {category_name}…` | SEO 메타 설정 (카테고리·검색·상품·쇼핑몰 인덱스별 메타 타이틀/설명 및 SEO 활성 토글) | @@ -54,6 +54,7 @@ _단건 응답: `data` 객체의 필드._ | claim | object | `{"refund_reasons":[{"id":1,"type":"refund","code":"order_…` | 클레임 설정 (DB 관리 대상인 환불 사유 목록: 코드·다국어명·귀책 유형·노출/활성 여부) | | 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 — 미등록 시 빈 배열이며 신청 폼이 노출되지 않음) | +| available_public_asset_disks | array | `[{"id":"none","label":{"ko":"사용 안 함 (스트리밍)","en":"Disabl…` | 공개 자산 직접 URL 서빙 디스크 선택지 (코어 DriverRegistryService 카탈로그 — none/public/s3 + 플러그인 훅 등록분). 기본정보 탭의 공개 자산 디스크 Select 옵션 소스 | | abilities | object | `{"can_update":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 (can_update, can_delete 등 — 권한 맵 기반) | | _meta | object | `{"limits":{"auto_cancel_days_min":1,"auto_cancel_days_max…` | 설정 화면 전용 메타. `limits` 는 숫자 입력의 경계값 맵(`config('sirsoft-ecommerce.limits')`)으로, 저장 규칙(FormRequest)과 같은 출처다 — 화면이 리터럴 경계를 들면 규칙 변경을 따라가지 못해 "화면은 받는데 저장은 422" 가 되므로 입력 min/max 는 이 값을 바인딩한다 | @@ -449,9 +450,11 @@ HTTP/1.1 200 | basic_info.privacy_officer_email | body | email | 아니오 | max 255 | 개인정보 보호책임자 E-mail | | basic_info.mail_order_number | body | string | 아니오 | max 100 | 통신판매업 신고번호 | | basic_info.telecom_number | body | string | 아니오 | max 100 | 부가통신 사업자번호 | +| basic_info.public_asset_disk | body | string | 아니오 | max 100 | 공개 자산 디스크 오버라이드 (빈값=코어 설정 따름, none=강제 스트리밍, 그 외=카탈로그 디스크 id — 존재하지 않는 디스크는 스트리밍으로 안전 폴백) | | language_currency | body | array | 아니오 | — | 통화 설정 섹션 (기본 통화·통화 목록: 코드·다국어명·환율·반올림 규칙·통화별 로케일) | | language_currency.default_currency | body | string | 아니오 | max 10 | 쇼핑몰 기본(base) 통화 코드. 상품/주문이 1건이라도 생성된 뒤에는 변경 불가 | | language_currency.currencies | body | array | 아니오 | — | 등록 통화 목록. 항목별 `code`(ISO 4217 3자리 대문자, 필수)·`name`(다국어 배열, 필수)·`symbol`·`exchange_rate`·`base_unit`·`rounding_unit`·`rounding_method`(`floor`\|`round`\|`ceil`)·`decimal_places`·`is_default`·`locales` | +| language_currency.removed_default_currencies | body | array | — | — | 서버 관리 필드. 요청에 실어 보내도 무시되며, 제출된 `currencies` 와 기본 제공 통화 목록의 차집합으로 서버가 재계산한다. `currencies` 를 보내지 않은 저장은 기존 값을 그대로 이월한다 | | seo | body | array | 아니오 | — | SEO 메타 설정 섹션 (페이지 유형별 메타 타이틀/설명·SEO 활성 토글) | | seo.meta_category_title | body | string | 아니오 | max 500 | 카테고리 페이지 메타 Title (`{commerce_name}`·`{category_name}` 등 변수 사용 가능) | | seo.meta_category_description | body | string | 아니오 | max 1000 | 카테고리 페이지 메타 Description | @@ -655,7 +658,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | | basic_info | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 쇼핑몰 기본 정보 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처·이메일 등) | -| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙) | +| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙). `removed_default_currencies` 는 관리자가 삭제한 기본 제공 통화 코드 목록으로, 서버가 저장 시점에 도출해 기록한다 (관리자 응답 전용 — 공개 설정에는 노출되지 않음) | | order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 주문/결제 설정 (기본 PG·병합된 결제수단·은행/무통장 계좌·자동취소·장바구니 만료·현금영수증 발급 제공자·자진발급·배송비 과세 방식 등) | | shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 배송 설정 (기본 국가·배송 가능 국가·무료배송·DB 관리 배송사(carriers)·배송유형(types)·계산 API 후보 필드 포함) | | seo | object | `{"meta_category_title":"{commerce_name} - {category_name}…` | SEO 메타 설정 (카테고리·검색·상품·쇼핑몰 인덱스별 메타 타이틀/설명 및 SEO 활성 토글) | @@ -666,6 +669,7 @@ _단건 응답: `data` 객체의 필드._ | claim | object | `{"refund_reasons":[{"id":1,"type":"refund","code":"order_…` | 클레임 설정 (DB 관리 대상인 환불 사유 목록: 코드·다국어명·귀책 유형·노출/활성 여부) | | 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 — 미등록 시 빈 배열이며 신청 폼이 노출되지 않음) | +| available_public_asset_disks | array | `[{"id":"none","label":{"ko":"사용 안 함 (스트리밍)","en":"Disabl…` | 공개 자산 직접 URL 서빙 디스크 선택지 (코어 DriverRegistryService 카탈로그 — none/public/s3 + 플러그인 훅 등록분). 기본정보 탭의 공개 자산 디스크 Select 옵션 소스 | **응답 예시** @@ -1009,12 +1013,16 @@ HTTP/1.1 200 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | +| 400 | Bad Request | 설정 저장은 통과했으나 DB 동기화가 도메인 규칙에 걸린 경우 — 주문에서 사용 중인 배송유형·배송사를 payload 에서 빼 삭제하려 한 경우가 이에 해당하며, `message` 에 대상 이름과 사용 건수가 담긴다 | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> +**삭제 차단 사유의 구분** `shipping.types`·`shipping.carriers` 는 payload 에서 항목을 빼는 방식으로 삭제합니다. 그 항목이 주문에서 사용 중이면 삭제가 거부되는데, 이때는 **400** 과 함께 사유(대상 이름·사용 건수)가 `message` 로 전달됩니다. 저장 자체가 서버 결함으로 실패한 경우의 500(일반 저장 오류 문구)과 구분되므로, 운영자는 화면 문구만으로 "내가 고칠 수 있는 일인지"를 판별할 수 있습니다. + **설명** 관리자가 이커머스 환경설정을 저장합니다. `permission:sirsoft-ecommerce.settings.update` 권한이 필요하며, `_tab` 으로 저장할 카테고리를 지정하고 각 섹션(`basic_info`·`shipping`·`claim` 등)을 배열로 전달합니다. `EcommerceSettingsService::saveSettings()`가 JSON 설정을 저장하되, DB 관리 대상인 `shipping.carriers`·`shipping.types`·`claim.refund_reasons` 는 분리해 각 Service 의 sync 메서드로 동기화합니다. 저장 성공 시 `sirsoft-ecommerce.settings.after_save` 훅을 발화하고, 관리자 UI 상태 갱신을 위해 병합된 전체 설정을 다시 반환합니다. **숫자 필드 정규화** 검증 규칙에 `integer` / `numeric` 이 선언된 모든 필드는 검증 직전에 숫자 타입으로 캐스트되어 저장됩니다(중첩 배열의 와일드카드 경로 포함 — 예: `mileage.currency_rules.*.use_unit`). HTML `number` 입력의 값은 문자열(`"5"`)로 전송되고 Laravel 의 `integer` 규칙은 숫자 문자열을 통과시키되 캐스트하지 않으므로, 정규화가 없으면 문자열이 그대로 설정 파일에 영속되어 이후 날짜/수치 연산에서 타입 오류를 유발합니다. @@ -1071,7 +1079,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | | basic_info | object | `{"shop_name":"","route_path":"shop","no_route":false,"com…` | 쇼핑몰 기본 정보 (쇼핑몰명·라우트 경로·상호·사업자번호·주소·연락처·이메일 등) | -| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙) | +| language_currency | object | `{"default_currency":"KRW","currencies":[{"code":"KRW","na…` | 통화 설정 (기본 통화 + 등록 통화 목록: 코드·다국어명·환율·기호·국기·반올림 규칙). `removed_default_currencies` 는 관리자가 삭제한 기본 제공 통화 코드 목록으로, 서버가 저장 시점에 도출해 기록한다 (관리자 응답 전용 — 공개 설정에는 노출되지 않음) | | order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 주문/결제 설정 (기본 PG·병합된 결제수단·은행/무통장 계좌·자동취소·장바구니 만료·현금영수증 발급 제공자·자진발급·배송비 과세 방식 등) | | shipping | object | `{"default_country":"KR","available_countries":[{"code":"K…` | 배송 설정 (기본 국가·배송 가능 국가·무료배송·DB 관리 배송사(carriers)·배송유형(types)·계산 API 후보 필드 포함) | | seo | object | `{"meta_category_title":"{commerce_name} - {category_name}…` | SEO 메타 설정 (카테고리·검색·상품·쇼핑몰 인덱스별 메타 타이틀/설명 및 SEO 활성 토글) | @@ -1652,7 +1660,7 @@ _단건 응답: `data` 객체의 필드._ | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 공개 가능한 결제 설정 (활성 결제수단·무통장 은행명 매핑 포함, 민감 정보 제외). `payment_methods` 는 현재 제공 가능한 결제수단만 포함하며, 공급 확장이 더 이상 제공하지 않는 결제수단(관리자 화면의 고아 항목)은 `is_active` 가 참이어도 제외된다 | +| order_settings | object | `{"default_pg_provider":null,"cash_receipt_provider":"toss…` | 공개 가능한 결제 설정 (활성 결제수단·무통장 은행명 매핑 포함, 민감 정보 제외). `payment_methods` 는 현재 제공 가능한 결제수단만 포함하며, 공급 확장이 더 이상 제공하지 않는 결제수단(관리자 화면의 고아 항목)은 `is_active` 가 참이어도 제외된다. **지정된 PG 사가 현재 등록되어 있지 않은 결제수단도 같은 이유로 제외된다** — 수단 자체는 카탈로그에 남아 있지만 주문 시 PG 라우팅이 매칭에 실패해 결제창 없이 주문이 완료되기 때문이다. 유효 PG 판정은 결제수단의 `pg_provider` 가 비어 있을 때만 `default_pg_provider` 로 폴백하며(런타임 라우팅과 동일), 양쪽 모두 미설정이면 PG 비경유 수단으로 종전대로 노출된다. `default_pg_provider` 와 `cash_receipt_provider` 도 등록되지 않은 값이면 `null` 로 정규화된다. 관리자 응답(`GET admin/settings`)은 이 필터를 적용하지 않고 `_orphaned` / `_orphaned_pg` 플래그를 그대로 실어 운영자가 확인·수정할 수 있게 한다 | **응답 예시** diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md index 85b0b7b8..471d1d79 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-carriers.md @@ -420,11 +420,27 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 삭제 결과 요약 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| carrier_id | integer | `4` | 삭제된 배송사의 기본 키 | **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사가 삭제되었습니다.", + "data": { + "carrier_id": 4 + } +} +``` **에러 응답** @@ -433,6 +449,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -462,11 +479,41 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ShippingCarrierResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id} (배송사 수정)** 응답 필드 표와 동일합니다._ **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사 정보를 조회했습니다.", + "data": { + "id": 13, + "code": "apidoc", + "name": { + "ko": "API 문서 샘플 배송사(수정)", + "en": "API Doc Sample Carrier" + }, + "localized_name": "API 문서 샘플 배송사(수정)", + "type": "domestic", + "type_label": "국내", + "tracking_url": null, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:12:07", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -585,6 +632,7 @@ HTTP/1.1 200 | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -614,11 +662,41 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +_단건 응답: `data` 객체의 필드 (`ShippingCarrierResource`). 필드 구성은 이 문서의 **PUT /api/modules/sirsoft-ecommerce/admin/shipping-carriers/{id} (배송사 수정)** 응답 필드 표와 동일합니다._ `is_active` 가 반전된 상태로 반환됩니다. **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송사 상태가 변경되었습니다.", + "data": { + "id": 13, + "code": "apidoc", + "name": { + "ko": "API 문서 샘플 배송사(수정)", + "en": "API Doc Sample Carrier" + }, + "localized_name": "API 문서 샘플 배송사(수정)", + "type": "domestic", + "type_label": "국내", + "tracking_url": null, + "is_active": true, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 15:12:07", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` **에러 응답** @@ -627,6 +705,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.settings.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md index 849ff4d1..669b5824 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/shipping-policies.md @@ -16,6 +16,91 @@ --- +## 국가별 설정(`country_settings[]`)의 검증 계약 + +생성(`POST`)·수정(`PUT`) 요청은 같은 규칙을 공유합니다. 이 규칙들은 저장 시점에 422 로 차단되며, +목적은 "설정은 저장됐는데 계산 결과가 조용히 0원(무료배송)이 되는" 상태를 만들지 않는 것입니다. + +**부과정책별 필수값** + +| 부과정책 | 필수 항목 | 누락 시 | +| --- | --- | --- | +| `range_*` (금액/수량/무게/부피/부피무게) | `ranges.tiers` 최소 1개 | 422 | +| `per_*` (수량당/무게당/부피당/부피무게당/금액당) | `ranges.unit_value` | 422 | +| `conditional_free` | `free_threshold` | 422 | +| `fixed`, `conditional_free`, `per_*`, `api` | `base_fee` > 0 | 422 | +| `api` | `api_endpoint` | 422 | + +`api` 정책의 `base_fee` 는 외부 API 호출이 실패했을 때 사용할 폴백 배송비입니다. 0 을 허용하면 +외부 API 장애가 그대로 무료배송이 되므로 0 을 거부합니다. + +**구간(`ranges.tiers[]`) 규칙** + +구간의 종료값(`max`)은 **포함**입니다 — `max: 5` 인 구간은 값 5까지 그 구간입니다. +마지막 구간의 `max` 는 `null`(무제한)이어야 하고, 그 외 구간은 `max` 가 있어야 합니다. + +구간을 잇는 방식은 값의 성질에 따라 다릅니다. + +| 부과정책 | 다음 구간의 `min` | 예시 | +| --- | --- | --- | +| `range_quantity` (이산값) | 직전 `max` + 1 | `0~5` → `6~` | +| `range_amount` · `range_weight` · `range_volume` · `range_volume_weight` (연속값) | 직전 `max` | `0~2` → `2~` | + +연속형은 소수 경계(`2.5`)를 허용하며, 비교에는 0.001 의 허용 오차를 둡니다. +`range_quantity` 의 `min`/`max` 는 정수여야 합니다. +첫 구간의 `min` 은 0 이어야 합니다. + +`min` 은 화면 표시용이며 배송비 계산에는 사용되지 않습니다. 계산은 구간을 종료값 오름차순으로 +정렬한 뒤 값이 종료값 이하인 첫 구간의 배송비를 채택합니다. + +**구간·단위값의 단위** + +상품 옵션의 무게·부피는 **g / cm³** 로 저장되고, 배송정책의 구간 경계와 `unit_value` 는 +**kg / L** 로 입력합니다. 환산은 배송비 계산 시점에 한 번만 수행됩니다. + +| 부과정책 | 구간/단위값 단위 | +| --- | --- | +| `range_quantity` · `per_quantity` | 개 | +| `range_weight` · `range_volume_weight` · `per_weight` · `per_volume_weight` | kg | +| `range_volume` · `per_volume` | L | +| `range_amount` · `per_amount` | 기본 통화 금액 | + +부피무게는 `부피(cm³) ÷ ranges.volume_weight_divisor`(기본 6000)로 kg 을 얻고, 실무게(kg)와 +비교해 큰 값을 사용합니다. + +**계산 API 요청 페이로드의 단위** + +`api` 정책이 외부 서버로 보내는 요청은 **저장 단위(g / cm³)를 그대로** 싣습니다. 위 kg/L 환산은 +쇼핑몰이 자체 계산할 때만 적용되며, 외부 API 로는 환산 전 값이 나갑니다. 값에 단위를 나타내는 +접미사나 별도 필드는 붙지 않으므로, 받는 쪽에서 g/cm³ 로 해석해야 합니다. + +| 필드 | 단위 | 비고 | +| --- | --- | --- | +| `items[].weight` | g | 상품 옵션에 저장된 무게 | +| `items[].volume` | cm³ | 상품 옵션에 저장된 부피 | +| `items[].quantity` | 개 | | +| `items[].subtotal` · `group_total` | 기본 통화 금액 | | +| `total_quantity` | 개 | 배송 그룹 합계 수량 | + +`api_config.field_map` 은 키 이름만 바꿉니다 — 값의 단위는 변환하지 않습니다. 외부 API 가 kg 을 +요구한다면 그 변환은 받는 쪽에서 수행해야 합니다. + +**도서산간 우편번호(`extra_fee_settings[].zipcode`) 형식** + +다음 3가지 형식만 허용합니다. 그 외 형식은 어떤 우편번호에도 매칭되지 않아 추가배송비가 +조용히 누락되므로 422 로 거부합니다. + +| 형식 | 예 | 의미 | +| --- | --- | --- | +| `\d+` | `63000` | 정확일치 | +| `\d+-\d+` | `63000-63644` | 범위(양끝 포함) | +| `\d+*` | `63*` | 접두 일치 | + +`extra_fee_multiply` 를 켜면 `per_*` 정책에서 추가배송비가 **기본 배송비와 같은 배송 단위 수**만큼 +곱해집니다(주문 수량이 아닙니다). + +--- + ### GET /api/modules/sirsoft-ecommerce/admin/shipping-policies <!-- @generated:start:api.modules.sirsoft-ecommerce.admin.shipping-policies.index --> @@ -498,6 +583,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -570,6 +656,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -728,7 +815,18 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책이 삭제되었습니다.", + "data": null +} +``` + **에러 응답** @@ -738,6 +836,7 @@ _이 엔드포인트는 `data` 를 반환하지 않습니다 (성공 메시지 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.delete`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -767,11 +866,55 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"기본 배송정책","en":"Default Shipping Policy"}` | 정책명 (로케일별 값 객체) | +| name_localized | string | `기본 배송정책` | `name` 의 현재 로케일 해석 값 | +| country_settings | array | `[]` | 국가별 배송 설정 목록 (`countrySettings` 관계가 로드된 경우에만 포함) | +| fee_summary | string | `KR: 배송비: 3,000원` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (활성 설정 없으면 빈 문자열) | +| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분은 `+N` 축약) | +| is_active | boolean | `true` | 활성 여부 | +| is_default | boolean | `false` | 기본 배송정책 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 | + **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책을 조회했습니다.", + "data": { + "id": 1, + "name": { + "ko": "기본 배송정책", + "en": "Default Shipping Policy" + }, + "name_localized": "기본 배송정책", + "fee_summary": "KR: 배송비: 3,000원", + "countries_display": "🇰🇷", + "is_active": true, + "is_default": false, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** @@ -917,11 +1060,55 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"기본 배송정책","en":"Default Shipping Policy"}` | 정책명 (로케일별 값 객체) | +| name_localized | string | `기본 배송정책` | `name` 의 현재 로케일 해석 값 | +| country_settings | array | `[]` | 국가별 배송 설정 목록 (`countrySettings` 관계가 로드된 경우에만 포함) | +| fee_summary | string | `KR: 배송비: 3,000원` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (활성 설정 없으면 빈 문자열) | +| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분은 `+N` 축약) | +| is_active | boolean | `true` | 활성 여부 | +| is_default | boolean | `false` | 기본 배송정책 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 | + **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "기본 배송정책으로 설정되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "기본 배송정책", + "en": "Default Shipping Policy" + }, + "name_localized": "기본 배송정책", + "fee_summary": "KR: 배송비: 3,000원", + "countries_display": "🇰🇷", + "is_active": true, + "is_default": false, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** @@ -930,6 +1117,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -959,11 +1147,55 @@ Authorization: Bearer {YOUR_TOKEN} **응답 필드** (`data` 내부) -<!-- 실측 제외: unresolved-path-param — 응답 필드는 사람이 작성하세요. --> +| 필드 | 타입 | 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| id | integer | `1` | 기본 키 (내부 식별자) | +| name | object | `{"ko":"기본 배송정책","en":"Default Shipping Policy"}` | 정책명 (로케일별 값 객체) | +| name_localized | string | `기본 배송정책` | `name` 의 현재 로케일 해석 값 | +| country_settings | array | `[]` | 국가별 배송 설정 목록 (`countrySettings` 관계가 로드된 경우에만 포함) | +| fee_summary | string | `KR: 배송비: 3,000원` | 활성 국가별 설정을 종합한 배송비 요약 텍스트 (활성 설정 없으면 빈 문자열) | +| countries_display | string | `🇰🇷` | 활성 배송 국가를 국기 이모지로 표시한 문자열 (최대 3개, 초과분은 `+N` 축약) | +| is_active | boolean | `true` | 활성 여부 | +| is_default | boolean | `false` | 기본 배송정책 여부 | +| sort_order | integer | `0` | 표시 정렬 순서 값 (작을수록 우선) | +| created_at | string | `2026-07-08 10:44:49` | 생성 일시 | +| updated_at | string | `2026-07-08 10:44:49` | 최종 수정 일시 | +| abilities | object | `{"can_create":true,"can_update":true,"can_delete":true}` | 현재 사용자가 이 리소스에 수행 가능한 작업 불리언 맵 | + **응답 예시** -<!-- 실측 제외: unresolved-path-param — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "배송정책 상태가 변경되었습니다.", + "data": { + "id": 1, + "name": { + "ko": "기본 배송정책", + "en": "Default Shipping Policy" + }, + "name_localized": "기본 배송정책", + "fee_summary": "KR: 배송비: 3,000원", + "countries_display": "🇰🇷", + "is_active": true, + "is_default": false, + "sort_order": 0, + "created_at": "2026-07-08 10:44:49", + "updated_at": "2026-07-08 10:44:49", + "abilities": { + "can_create": true, + "can_update": true, + "can_delete": true + } + } +} +``` + **에러 응답** @@ -972,6 +1204,7 @@ Authorization: Bearer {YOUR_TOKEN} | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.shipping-policies.update`)이 없는 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md index bfd0fd17..49e72b9a 100644 --- a/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md +++ b/modules/_bundled/sirsoft-ecommerce/docs/api/wishlist.md @@ -169,6 +169,7 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 422 | Unprocessable Entity | `page` < 1, `per_page` 가 1~100 범위를 벗어나거나 정수가 아닌 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -212,11 +213,29 @@ Content-Type: application/json **응답 필드** (`data` 내부) -<!-- 실측 제외: http-422 — 응답 필드는 사람이 작성하세요. --> +_단건 응답: 토글 결과 (`data` 객체)._ + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| added | boolean | `true` | 토글 후 찜 상태 — `true` 면 이번 호출로 추가됨, `false` 면 제거됨. 하트 버튼의 표시 상태를 이 값으로 갱신합니다 | **응답 예시** -<!-- 실측 제외: http-422 — 응답 예시는 사람이 작성하세요. --> +```http +HTTP/1.1 200 +``` + +```json +{ + "success": true, + "message": "상품이 찜 목록에 추가되었습니다.", + "data": { + "added": true + } +} +``` + +> 제거된 경우 `message` 는 `상품이 찜 목록에서 제거되었습니다.`, `data.added` 는 `false` 입니다. **에러 응답** @@ -224,6 +243,7 @@ Content-Type: application/json | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지) | +| 500 | Internal Server Error | 서버 내부 오류 (`찜 처리에 실패했습니다.`) — 도메인 규칙 위반이 아닌 예외는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> @@ -275,6 +295,7 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 404 | Not Found | path 파라미터에 해당하는 리소스가 없는 경우 | +| 500 | Internal Server Error | 서버 내부 오류 — 도메인 규칙 위반이 아닌 예외(인프라 장애·코드 결함)는 4xx 로 뭉개지 않고 500 으로 구분한다 | <!-- @generated:end --> diff --git a/modules/_bundled/sirsoft-ecommerce/module.json b/modules/_bundled/sirsoft-ecommerce/module.json index e7b68a12..697c7767 100644 --- a/modules/_bundled/sirsoft-ecommerce/module.json +++ b/modules/_bundled/sirsoft-ecommerce/module.json @@ -5,13 +5,13 @@ "ko": "이커머스", "en": "Ecommerce" }, - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "description": { "ko": "그누보드7 이커머스 모듈 - 상품, 주문, 결제 관리", "en": "Gnuboard7 Ecommerce Module - Product, Order, Payment Management" }, - "g7_version": ">=7.0.6", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} diff --git a/modules/_bundled/sirsoft-ecommerce/module.php b/modules/_bundled/sirsoft-ecommerce/module.php index e7cb30b4..d40f81f2 100644 --- a/modules/_bundled/sirsoft-ecommerce/module.php +++ b/modules/_bundled/sirsoft-ecommerce/module.php @@ -19,6 +19,7 @@ use Modules\Sirsoft\Ecommerce\Listeners\AssignDefaultCurrencyOnRegisterListener; use Modules\Sirsoft\Ecommerce\Listeners\AssignDefaultShippingCountryOnRegisterListener; use Modules\Sirsoft\Ecommerce\Listeners\CategoryActivityLogListener; use Modules\Sirsoft\Ecommerce\Listeners\CategoryTreeCacheListener; +use Modules\Sirsoft\Ecommerce\Listeners\Ckeditor5ReferenceSourcesListener; use Modules\Sirsoft\Ecommerce\Listeners\CouponActivityLogListener; use Modules\Sirsoft\Ecommerce\Listeners\CouponRestoreListener; use Modules\Sirsoft\Ecommerce\Listeners\CouponUseListener; @@ -39,6 +40,7 @@ use Modules\Sirsoft\Ecommerce\Listeners\SeoCategoryCacheListener; use Modules\Sirsoft\Ecommerce\Listeners\SeoProductCacheListener; use Modules\Sirsoft\Ecommerce\Listeners\SeoSettingsCacheListener; use Modules\Sirsoft\Ecommerce\Listeners\ShippingPolicyActivityLogListener; +use Modules\Sirsoft\Ecommerce\Listeners\ShippingPolicyCacheListener; use Modules\Sirsoft\Ecommerce\Listeners\SyncOptionGroupsListener; use Modules\Sirsoft\Ecommerce\Listeners\SyncProductFromOptionListener; use Modules\Sirsoft\Ecommerce\Listeners\UserCurrencyInfoListener; @@ -1056,6 +1058,43 @@ class Module extends AbstractModule ]; } + /** + * 완전 공개 자산 스토리지 카테고리 목록 + * + * 이 카테고리들만 공개 자산 디스크 설정을 따른다. 권한 검사가 걸린 자산 + * (회원 전용/비밀 자료 등)은 직접 URL 이 권한을 우회하므로 포함하지 않는다. + * 새 공개 자산 카테고리가 생기면 여기에만 추가하면 된다. + * + * @var list<string> + */ + private const PUBLIC_ASSET_CATEGORIES = ['images']; + + /** + * 카테고리별 스토리지 디스크 이름 반환 + * + * 완전 공개 자산 카테고리만 공개 자산 디스크(모듈 설정 basic_info.public_asset_disk > + * 코어 전역 core.storage.public_asset_disk)를 따르고, 설정/캐시 등 나머지 + * 카테고리는 기본 디스크를 유지합니다. 미설정/고아 디스크는 기본 디스크로 + * 폴백해 기존 스트리밍 동작을 보존합니다. + * + * 모듈 설정 조회는 공개 자산 카테고리에서만 수행합니다 — 'settings' 카테고리에서 + * 조회하면 설정 로드와 재귀 고리가 생깁니다 (AbstractModule 주석 참조). + * + * @param string $category 카테고리 + * @return string 디스크 이름 + */ + public function getStorageDiskFor(string $category): string + { + if (! in_array($category, self::PUBLIC_ASSET_CATEGORIES, true)) { + return $this->getStorageDisk(); + } + + $override = module_setting('sirsoft-ecommerce', 'basic_info.public_asset_disk', ''); + + return $this->resolvePublicAssetDisk(is_string($override) ? $override : '') + ?? $this->getStorageDisk(); + } + /** * 모듈 설치 시 실행할 시더 목록 반환 * @@ -2092,6 +2131,7 @@ class Module extends AbstractModule SeoCategoryCacheListener::class, CategoryTreeCacheListener::class, SeoSettingsCacheListener::class, + ShippingPolicyCacheListener::class, MileageTransactionListener::class, UserMileageInfoListener::class, UserCurrencyInfoListener::class, @@ -2104,6 +2144,7 @@ class Module extends AbstractModule InjectAppConfigDeviceListener::class, IssueCashReceiptOnDepositListener::class, PurgeCashReceiptIdentifierListener::class, + Ckeditor5ReferenceSourcesListener::class, ]; } @@ -2139,6 +2180,21 @@ class Module extends AbstractModule // cart_expiry_days < 1 시 커맨드 내부 self-guard 로 비활성 (별도 토글 없음) 'enabled_config' => null, ], + [ + 'command' => 'sirsoft-ecommerce:prune-temp-product-images', + 'schedule' => 'daily', + 'description' => '미연결 임시 상품 이미지 자동 삭제', + // temp_key 가 남아 있으면 끝내 연결되지 않은 폼 세션 부산물이라 오탐 여지가 없다. + // 운영 데이터가 아니므로 별도 토글 없이 상시 동작한다 (보존기간은 커맨드 옵션). + 'enabled_config' => null, + ], + [ + 'command' => 'sirsoft-ecommerce:prune-temp-orders', + 'schedule' => 'hourly', + 'description' => '만료 임시 주문 자동 삭제', + // 만료 판정은 임시 주문 자체의 TTL 이 정하므로 별도 토글 없음 + 'enabled_config' => null, + ], [ 'command' => 'sirsoft-ecommerce:earn-mileage', 'schedule' => 'hourly', diff --git a/modules/_bundled/sirsoft-ecommerce/package-lock.json b/modules/_bundled/sirsoft-ecommerce/package-lock.json index bb1e54fb..a1e1cb71 100644 --- a/modules/_bundled/sirsoft-ecommerce/package-lock.json +++ b/modules/_bundled/sirsoft-ecommerce/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-ecommerce", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-ecommerce", - "version": "1.1.0", + "version": "1.1.1", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/modules/_bundled/sirsoft-ecommerce/package.json b/modules/_bundled/sirsoft-ecommerce/package.json index 7da219b5..4513558f 100644 --- a/modules/_bundled/sirsoft-ecommerce/package.json +++ b/modules/_bundled/sirsoft-ecommerce/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-ecommerce", - "version": "1.1.0", + "version": "1.1.1", "description": "그누보드7 이커머스 모듈 프론트엔드 에셋", "private": true, "type": "module", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json b/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json index ae3c827d..40f44866 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/extensions/mypage_order_cash_receipt.json @@ -253,10 +253,10 @@ "components": [ { "id": "ext_mypage_cash_receipt_card", - "comment": "상태 ① — 무통장이 아니거나 프로바이더 미설정이면 카드 자체를 렌더하지 않는다", + "comment": "상태 ① — 무통장이 아니면 카드 자체를 렌더하지 않는다. 프로바이더가 미설정(또는 제공 확장 제거)이어도 이미 발급/시도된 이력이 있으면 계속 표시한다 — 영수증 URL 에 도달할 방법이 사라지면 안 된다(A3)", "type": "basic", "name": "Div", - "if": "{{order.data.payment?.payment_method === 'dbank' && !!order.data.payment?.cash_receipt_provider}}", + "if": "{{order.data.payment?.payment_method === 'dbank' && (!!order.data.payment?.cash_receipt_provider || !!order.data.cash_receipt || (order.data.cash_receipts ?? []).length > 0)}}", "props": { "className": "mt-4 pt-4 border-t border-gray-200 dark:border-gray-700" }, "responsive": { "portable": { "props": { "className": "mt-3 pt-3 border-t border-gray-200 dark:border-gray-700" } } @@ -305,10 +305,10 @@ }, { "id": "ext_mypage_cash_receipt_issuable", - "comment": "상태 ③ 입금완료 + 미발급 — 발급 버튼. 재발급 실패 이력이 있으면 상태 ⑤(경고)가 담당하므로 여기서 제외한다(두 상태는 배타적이어야 한다).", + "comment": "상태 ③ 입금완료 + 미발급 — 발급 버튼. 재발급 실패 이력이 있으면 상태 ⑤(경고)가 담당하므로 여기서 제외한다(두 상태는 배타적이어야 한다). 발급 프로바이더가 해석되지 않으면(제공 확장 제거 등) 버튼을 내리지 않는다 — 눌러도 구독자 없는 훅을 호출해 실패로만 기록된다(A3).", "type": "basic", "name": "Div", - "if": "{{order.data.payment?.payment_status === 'paid' && !order.data.cash_receipt && String((order.data.cash_receipts ?? [])[0]?.issue_status ?? '').toLowerCase() !== 'failed'}}", + "if": "{{!!order.data.payment?.cash_receipt_provider && order.data.payment?.payment_status === 'paid' && !order.data.cash_receipt && String((order.data.cash_receipts ?? [])[0]?.issue_status ?? '').toLowerCase() !== 'failed'}}", "props": { "className": "flex flex-row items-center justify-between gap-2" }, "responsive": { "portable": { "props": { "className": "flex flex-col items-start gap-2 w-full" } } diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormHandlers.test.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormHandlers.test.ts index 71490a66..5aeed037 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormHandlers.test.ts +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormHandlers.test.ts @@ -25,6 +25,7 @@ import { addCountrySettingHandler, removeCountrySettingHandler, switchCountryTabHandler, + updateCountryFieldHandler, onChargePolicyChangeHandler, addRangeTierHandler, removeRangeTierHandler, @@ -36,6 +37,7 @@ import { toggleApiRequestFieldHandler, updateApiConfigFieldHandler, updateApiFieldMapHandler, + updateUnitValueHandler, } from '../../handlers/shippingPolicyFormHandlers'; // ===== 헬퍼: 중첩 경로에 값 설정 ===== @@ -498,7 +500,7 @@ describe('shippingPolicyFormHandlers', () => { expect(updates.showBaseFee).toBe(false); expect(updates['form.country_settings'][0].ranges).toEqual({ type: 'weight', - tiers: [{ min: 0, max: null, fee: 0 }], + tiers: [{ min: 0, max: null, fee: null }], }); }); @@ -606,12 +608,13 @@ describe('shippingPolicyFormHandlers', () => { // ===== addRangeTierHandler ===== describe('addRangeTierHandler', () => { - it('country_settings[index].ranges.tiers에 추가', () => { + it('country_settings[index].ranges.tiers에 추가 (연속형: 시작값 = 직전 종료값)', () => { mockLocalState = { form: { country_settings: [ { ...createDefaultCountrySetting('KR'), + charge_policy: 'range_amount', ranges: { type: 'amount', tiers: [{ min: 0, max: 10000, fee: 3000 }], @@ -630,17 +633,19 @@ describe('shippingPolicyFormHandlers', () => { const updates = mockContext.setLocalState.mock.calls[0][0]; const ranges = updates['form.country_settings'][0].ranges; expect(ranges.tiers).toHaveLength(2); - expect(ranges.tiers[1]).toEqual({ min: 10001, max: null, fee: 0 }); + // 배송비는 빈 값으로 시작해 미입력이 required 검증에 걸리게 한다 + expect(ranges.tiers[1]).toEqual({ min: 10000, max: null, fee: null }); }); - it('새 tier의 min = 이전 tier의 max + 1 (포함 범위)', () => { + it('이산형(수량) 구간의 새 시작값 = 직전 종료값 + 1', () => { mockLocalState = { form: { country_settings: [ { ...createDefaultCountrySetting('KR'), + charge_policy: 'range_quantity', ranges: { - type: 'weight', + type: 'quantity', tiers: [ { min: 0, max: 5, fee: 3000 }, { min: 6, max: 20, fee: 5000 }, @@ -663,6 +668,37 @@ describe('shippingPolicyFormHandlers', () => { expect(ranges.tiers[2].min).toBe(21); }); + it('연속형(무게) 구간의 새 시작값 = 직전 종료값', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'range_weight', + ranges: { + type: 'weight', + tiers: [ + { min: 0, max: 5, fee: 3000 }, + { min: 5, max: 20, fee: 5000 }, + ], + }, + }, + ], + }, + rangeErrors: {}, + }; + + addRangeTierHandler( + createAction({ index: 0 }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const ranges = updates['form.country_settings'][0].ranges; + expect(ranges.tiers).toHaveLength(3); + expect(ranges.tiers[2].min).toBe(20); + }); + it('빈 tiers에 첫 추가', () => { mockLocalState = { form: { @@ -684,7 +720,42 @@ describe('shippingPolicyFormHandlers', () => { const updates = mockContext.setLocalState.mock.calls[0][0]; const ranges = updates['form.country_settings'][0].ranges; expect(ranges.tiers).toHaveLength(1); - expect(ranges.tiers[0]).toEqual({ min: 0, max: null, fee: 0 }); + expect(ranges.tiers[0]).toEqual({ min: 0, max: null, fee: null }); + }); + + it('직전 구간 종료값이 비어 있으면 새 시작값은 비고 종료값 누락 오류가 뜬다', () => { + // 마지막 구간(무제한)은 종료값이 비어 있다 — 그 뒤에 구간을 추가하면 직전 구간이 + // 중간 구간이 되므로 종료값을 입력해야 하고, 시작값은 파생할 근거가 없어 빈 값이 된다 + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'range_weight', + ranges: { + type: 'weight', + tiers: [{ min: 0, max: null, fee: 3000 }], + }, + }, + ], + }, + rangeErrors: {}, + }; + + addRangeTierHandler( + createAction({ index: 0 }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const ranges = updates['form.country_settings'][0].ranges; + expect(ranges.tiers).toHaveLength(2); + expect(ranges.tiers[1].min).toBeNull(); + + const rangeErrors = mockLocalState.rangeErrors; + expect(rangeErrors.KR[0].max).toBe( + 'sirsoft-ecommerce.admin.shipping_policy.form.range_error_middle_max' + ); }); }); @@ -933,11 +1004,12 @@ describe('shippingPolicyFormHandlers', () => { country_settings: [ { ...createDefaultCountrySetting('KR'), + charge_policy: 'range_amount', ranges: { type: 'amount', tiers: [ - { min: 0, max: 9999, fee: 3000 }, - { min: 10000, max: 29999, fee: 2000 }, + { min: 0, max: 10000, fee: 3000 }, + { min: 10000, max: 30000, fee: 2000 }, { min: 30000, max: null, fee: 0 }, ], }, @@ -1443,4 +1515,419 @@ describe('shippingPolicyFormHandlers', () => { expect(updates['form.country_settings'][0].api_config.field_map).toBeNull(); }); }); + + // ===== 구간 경계·필수값 계약 (공개 #94) ===== + + describe('구간 경계·필수값 계약 (#94)', () => { + /** + * 구간 정책의 국가별 설정을 만듭니다. + * + * @param chargePolicy 부과정책 + * @param tiers 구간 배열 + */ + function stateWithTiers(chargePolicy: string, tiers: any[]) { + return { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: chargePolicy, + ranges: { type: chargePolicy.replace('range_', ''), tiers }, + }, + ], + }, + activeCountryTab: 0, + rangeErrors: {}, + }; + } + + /** + * 마지막 setLocalState 호출의 rangeErrors 를 반환합니다. + */ + function lastRangeErrors(): any[] { + const calls = mockContext.setLocalState.mock.calls; + const last = calls[calls.length - 1][0]; + + return last.rangeErrors.KR; + } + + it('종료값 입력 시 다음 구간의 시작값이 자동 파생된다 (연속형)', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: null, fee: 3000 }, + { min: null, max: null, fee: 5000 }, + ]); + + updateRangeTierFieldHandler( + createAction({ countryIndex: 0, tierIndex: 0, field: 'max', value: '2.5' }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const tiers = updates['form.country_settings'][0].ranges.tiers; + expect(tiers[0].max).toBe(2.5); + expect(tiers[1].min).toBe(2.5); + }); + + it('종료값 입력 시 다음 구간의 시작값이 자동 파생된다 (이산형)', () => { + mockLocalState = stateWithTiers('range_quantity', [ + { min: 0, max: null, fee: 3000 }, + { min: null, max: null, fee: 5000 }, + ]); + + updateRangeTierFieldHandler( + createAction({ countryIndex: 0, tierIndex: 0, field: 'max', value: '5' }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const tiers = updates['form.country_settings'][0].ranges.tiers; + expect(tiers[1].min).toBe(6); + }); + + it('종료값을 비우면 다음 구간의 시작값도 비워진다', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: 2, fee: 3000 }, + { min: 2, max: null, fee: 5000 }, + ]); + + updateRangeTierFieldHandler( + createAction({ countryIndex: 0, tierIndex: 0, field: 'max', value: '' }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const tiers = updates['form.country_settings'][0].ranges.tiers; + expect(tiers[0].max).toBeNull(); + expect(tiers[1].min).toBeNull(); + }); + + it('구간이 하나도 없으면 오류를 표시한다', () => { + mockLocalState = stateWithTiers('range_quantity', []); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + const errors = lastRangeErrors(); + expect(errors).toHaveLength(1); + expect(errors[0].max).toContain('range_error_min_one'); + }); + + it('배송비 미입력은 오류다 (0 으로 강제하지 않는다)', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: 2, fee: null }, + { min: 2, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + const errors = lastRangeErrors(); + expect(errors[0].fee).toContain('range_error_fee_required'); + }); + + it('중간 구간의 종료값 누락은 오류다', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: null, fee: 3000 }, + { min: 2, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + const errors = lastRangeErrors(); + expect(errors[0].max).toContain('range_error_middle_max'); + }); + + it('연속형은 다음 시작값이 직전 종료값이어야 한다', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: 2, fee: 3000 }, + { min: 3, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + expect(lastRangeErrors()[0].max).toContain('range_error_continuity'); + }); + + it('연속형은 소수 경계를 허용한다', () => { + mockLocalState = stateWithTiers('range_weight', [ + { min: 0, max: 2.5, fee: 3000 }, + { min: 2.5, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + expect(lastRangeErrors()).toEqual([]); + }); + + it('이산형은 다음 시작값이 직전 종료값 + 1 이어야 한다', () => { + mockLocalState = stateWithTiers('range_quantity', [ + { min: 0, max: 5, fee: 3000 }, + { min: 6, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + expect(lastRangeErrors()).toEqual([]); + }); + + it('이산형의 소수 경계는 오류다', () => { + mockLocalState = stateWithTiers('range_quantity', [ + { min: 0, max: 5.5, fee: 3000 }, + { min: 6.5, max: null, fee: 5000 }, + ]); + + validateRangeTiersHandler(createAction({ countryIndex: 0 }), mockContext); + + expect(lastRangeErrors()[0].max).toContain('range_error_integer'); + }); + + it('수정 모드 진입 시 저장된 구간의 시작값을 재파생한다', () => { + // 기존 데이터는 "1~5개 / 6개~" 처럼 첫 시작값이 1 인 형태로 저장돼 있다. + // 시작값이 읽기전용이 된 이상 화면이 정규화하지 않으면 운영자가 고칠 방법이 없고, + // 그대로 저장하면 "첫 구간의 시작값은 0이어야 합니다" 로 422 가 된다. + mockLocalState = { form: { country_settings: [] } }; + + initShippingPolicyFormHandler( + createAction({ + isEdit: true, + policy: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'range_quantity', + ranges: { + type: 'quantity', + tiers: [ + { min: 1, max: 5, fee: 3000 }, + { min: 6, max: null, fee: 5000 }, + ], + }, + }, + ], + }, + }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const tiers = updates['form.country_settings'][0].ranges.tiers; + expect(tiers[0].min).toBe(0); + expect(tiers[1].min).toBe(6); + // 종료값·배송비는 손대지 않는다 + expect(tiers[0].max).toBe(5); + expect(tiers[1].fee).toBe(5000); + }); + + it('수정 모드 진입 시 연속형 구간의 시작값도 재파생한다', () => { + mockLocalState = { form: { country_settings: [] } }; + + initShippingPolicyFormHandler( + createAction({ + isEdit: true, + policy: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'range_weight', + ranges: { + type: 'weight', + tiers: [ + { min: 0, max: 2, fee: 3000 }, + { min: 3, max: null, fee: 5000 }, + ], + }, + }, + ], + }, + }), + mockContext + ); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + const tiers = updates['form.country_settings'][0].ranges.tiers; + expect(tiers[1].min).toBe(2); + }); + + it('단위값을 비우면 null 로 보존한다 (1 로 강제하지 않는다)', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'per_weight', + ranges: { type: 'per_weight', unit_value: 2 }, + }, + ], + }, + activeCountryTab: 0, + }; + + updateUnitValueHandler(createAction({ value: '' }), mockContext); + + const updates = mockContext.setLocalState.mock.calls[0][0]; + expect(updates['form.country_settings'][0].ranges.unit_value).toBeNull(); + }); + }); + + // ===== 폼 레벨 필수값 선제 검증 (단위값 / 무료배송 기준금액) ===== + + describe('폼 레벨 필수값 선제 검증', () => { + const UNIT_VALUE_KEY = 'country_settings.0.ranges.unit_value'; + const FREE_THRESHOLD_KEY = 'country_settings.0.free_threshold'; + const UNIT_VALUE_MESSAGE = + 'sirsoft-ecommerce.admin.shipping_policy.form.range_error_unit_value_required'; + const FREE_THRESHOLD_MESSAGE = + 'sirsoft-ecommerce.admin.shipping_policy.form.range_error_free_threshold_required'; + + function seedPerUnitPolicy(unitValue: number | null) { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'per_weight', + ranges: { type: 'per_weight', unit_value: unitValue }, + }, + ], + }, + activeCountryTab: 0, + errors: null, + }; + } + + it('단위값을 비우면 저장 전에 오류가 표시된다', () => { + seedPerUnitPolicy(2); + + updateUnitValueHandler(createAction({ value: '' }), mockContext); + + expect(mockLocalState.errors[UNIT_VALUE_KEY]).toEqual([UNIT_VALUE_MESSAGE]); + }); + + it('단위값이 0 이하이면 오류가 표시된다', () => { + seedPerUnitPolicy(2); + + updateUnitValueHandler(createAction({ value: '0' }), mockContext); + + expect(mockLocalState.errors[UNIT_VALUE_KEY]).toEqual([UNIT_VALUE_MESSAGE]); + }); + + it('단위값을 다시 채우면 오류가 해제된다', () => { + seedPerUnitPolicy(null); + mockLocalState.errors = { [UNIT_VALUE_KEY]: [UNIT_VALUE_MESSAGE] }; + + updateUnitValueHandler(createAction({ value: '0.5' }), mockContext); + + expect(mockLocalState.errors[UNIT_VALUE_KEY]).toBeNull(); + }); + + it('단위값을 요구하지 않는 정책에서는 오류가 없다', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'fixed', + ranges: { type: 'fixed', unit_value: null }, + }, + ], + }, + activeCountryTab: 0, + errors: null, + }; + + updateUnitValueHandler(createAction({ value: '' }), mockContext); + + expect(mockLocalState.errors[UNIT_VALUE_KEY]).toBeNull(); + }); + + it('조건부 무료배송에서 기준금액을 비우면 저장 전에 오류가 표시된다', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'conditional_free', + free_threshold: 50000, + }, + ], + }, + activeCountryTab: 0, + errors: null, + }; + + updateCountryFieldHandler( + createAction({ field: 'free_threshold', value: null }), + mockContext + ); + + expect(mockLocalState.errors[FREE_THRESHOLD_KEY]).toEqual([FREE_THRESHOLD_MESSAGE]); + }); + + it('기준금액을 다시 채우면 오류가 해제된다', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'conditional_free', + free_threshold: null, + }, + ], + }, + activeCountryTab: 0, + errors: { [FREE_THRESHOLD_KEY]: [FREE_THRESHOLD_MESSAGE] }, + }; + + updateCountryFieldHandler( + createAction({ field: 'free_threshold', value: 30000 }), + mockContext + ); + + expect(mockLocalState.errors[FREE_THRESHOLD_KEY]).toBeNull(); + }); + + it('정책을 바꾸면 직전 정책에서 남은 필수값 오류가 사라진다', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'per_weight', + ranges: { type: 'per_weight', unit_value: null }, + }, + ], + }, + activeCountryTab: 0, + errors: { [UNIT_VALUE_KEY]: [UNIT_VALUE_MESSAGE] }, + }; + + onChargePolicyChangeHandler( + createAction({ value: 'fixed', index: 0 }), + mockContext + ); + + expect(mockLocalState.errors[UNIT_VALUE_KEY]).toBeNull(); + }); + + it('조건부 무료배송으로 바꾸면 비어 있는 기준금액이 즉시 오류로 드러난다', () => { + mockLocalState = { + form: { + country_settings: [ + { + ...createDefaultCountrySetting('KR'), + charge_policy: 'fixed', + free_threshold: null, + }, + ], + }, + activeCountryTab: 0, + errors: null, + }; + + onChargePolicyChangeHandler( + createAction({ value: 'conditional_free', index: 0 }), + mockContext + ); + + expect(mockLocalState.errors[FREE_THRESHOLD_KEY]).toEqual([FREE_THRESHOLD_MESSAGE]); + }); + }); }); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormI18nKeys.test.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormI18nKeys.test.ts new file mode 100644 index 00000000..2cae3be0 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormI18nKeys.test.ts @@ -0,0 +1,74 @@ +/** + * 배송정책 폼 핸들러가 요청하는 다국어 키가 실제로 존재하는지 검사한다. + * + * `G7Core.t()` 는 키를 찾지 못하면 키 문자열을 그대로 돌려준다. 핸들러의 + * `t?.(key) ?? '기본 문구'` 는 이 경우 방어가 되지 않는다 — null 이 아니라 키가 + * 반환되므로 `??` 가 발동하지 않고, 화면에는 + * "sirsoft-ecommerce.validation.shipping_policy.ranges.continuity" 같은 원문 키가 뜬다. + * + * 오류도 경고도 나지 않고 문구만 깨지므로, 키 존재 여부는 이렇게 구조적으로 잠근다. + * + * @vitest-environment node + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const MODULE_ROOT = resolve(__dirname, '../../../..'); +const HANDLER_PATH = resolve(MODULE_ROOT, 'resources/js/handlers/shippingPolicyFormHandlers.ts'); + +/** 핸들러 소스에서 요청하는 다국어 키를 전수 추출한다. */ +function extractRequestedKeys(): string[] { + const source = readFileSync(HANDLER_PATH, 'utf-8'); + const keys = new Set<string>(); + const re = /t\?\.\(\s*'([^']+)'\s*\)/g; + let match; + + while ((match = re.exec(source)) !== null) { + keys.add(match[1]); + } + + return [...keys]; +} + +/** + * `sirsoft-ecommerce.admin.shipping_policy.form.x` 형태의 키를 파일/경로로 해석한다. + * + * 프론트 다국어는 `resources/lang/partial/{locale}/admin/shipping_policy.json` 이 + * `admin/shipping_policy` 네임스페이스를 담당한다. + */ +function resolveKey(locale: string, key: string): string | undefined { + const prefix = 'sirsoft-ecommerce.admin.shipping_policy.'; + + if (!key.startsWith(prefix)) { + return undefined; + } + + const json = JSON.parse( + readFileSync(resolve(MODULE_ROOT, `resources/lang/partial/${locale}/admin/shipping_policy.json`), 'utf-8') + ); + + return key + .slice(prefix.length) + .split('.') + .reduce<any>((acc, segment) => (acc == null ? undefined : acc[segment]), json); +} + +describe('shippingPolicyFormHandlers 다국어 키', () => { + const requested = extractRequestedKeys(); + + it('핸들러가 다국어 키를 실제로 요청한다 (추출이 비면 이 테스트는 아무것도 검증하지 못한다)', () => { + expect(requested.length).toBeGreaterThan(5); + }); + + it('모든 요청 키가 프론트엔드 네임스페이스를 사용한다', () => { + // 백엔드 lang 경로(sirsoft-ecommerce.validation.*)는 프론트 번들에 실리지 않는다 + const backendKeys = requested.filter(k => !k.startsWith('sirsoft-ecommerce.admin.shipping_policy.')); + expect(backendKeys).toEqual([]); + }); + + it.each(['ko', 'en'])('%s 로케일에 모든 요청 키가 정의되어 있다', (locale) => { + const missing = requested.filter(key => typeof resolveKey(locale, key) !== 'string'); + expect(missing).toEqual([]); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminCategoryImageRemoveExpression.test.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminCategoryImageRemoveExpression.test.ts new file mode 100644 index 00000000..a40e87c0 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminCategoryImageRemoveExpression.test.ts @@ -0,0 +1,135 @@ +/** + * @file adminCategoryImageRemoveExpression.test.ts + * @description 카테고리 이미지 제거 시 form 에서 실제로 빠지는지 — 레이아웃 표현식 자체를 평가한다. + * + * 배경(브라우저 실측): FileUploader 는 `onRemove` 에 **hash 우선**(`item.hash || item.id`)으로 + * 식별자를 넘긴다. 레이아웃이 `img.id !== $args[0]` 로만 걸러내면 저장된 이미지(항상 hash 보유)는 + * 어떤 항목도 제거되지 않아, 이미지는 서버에서 즉시 삭제됐는데 폼에는 그대로 남는다. + * + * 코어 사이트 로고(`admin-settings-logo-remove-expression`)와 같은 근본 원인이며, 같은 방식으로 + * **실제 레이아웃 파일**에서 식을 꺼내 평가한다 — 액션을 테스트 안에 복사해 두면 표현식 자체는 + * 한 번도 평가되지 않아 회귀를 잡지 못한다. + * + * @scenario image_state=temp_unlinked, age=within_retention + * + * @effects category_image_remove_filters_by_hash, category_image_remove_falls_back_to_id + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * 모듈 루트(module.json 기준)를 위로 훑어 찾는다. + * + * @returns 모듈 루트 절대경로 + */ +function moduleRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'module.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('module.json 을 가진 모듈 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리 전체를 훑어 조건을 만족하는 첫 노드를 찾는다. + * + * 슬롯·partial 등 children 이외의 키에도 노드가 매달리므로 모든 값을 훑는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: unknown, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + + if (!Array.isArray(node) && predicate(node)) return node; + + for (const value of Object.values(node)) { + const found = findNode(value, predicate); + if (found) return found; + } + + return null; +} + +const panelForm = JSON.parse( + fs.readFileSync( + path.join( + moduleRoot(), + 'resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_form.json', + ), + 'utf-8', + ), +); + +const uploader = findNode( + panelForm, + (n: any) => + n.name === 'FileUploader' + && Array.isArray(n.actions) + && n.actions.some((a: any) => a.event === 'onRemove'), +); + +const removeExpression: string = uploader?.actions + .filter((a: any) => a.event === 'onRemove') + .map((a: any) => a.params?.['form.images']) + .find((v: unknown) => typeof v === 'string'); + +/** + * `{{...}}` 바인딩 식을 주어진 컨텍스트로 평가한다. + * + * @param expression 레이아웃 바인딩 식 + * @param context 평가 컨텍스트 (_local, $args) + * @returns 평가 결과 + */ +function evaluateBinding(expression: string, context: { _local: any; $args: unknown[] }): unknown { + const body = expression.trim().replace(/^\{\{/, '').replace(/\}\}$/, ''); + + return new Function('_local', '$args', `return (${body});`)(context._local, context.$args); +} + +const SAVED_IMAGE = { id: 101, hash: 'abc123', original_filename: 'category1.png' }; +const OTHER_IMAGE = { id: 202, hash: 'def456', original_filename: 'category2.png' }; + +describe('카테고리 이미지 제거 — 레이아웃 표현식 평가', () => { + it('onRemove 식이 레이아웃에 선언되어 있다', () => { + expect(typeof removeExpression).toBe('string'); + }); + + it('hash 로 통지된 항목이 form 에서 제거된다', () => { + const result = evaluateBinding(removeExpression, { + _local: { form: { images: [SAVED_IMAGE, OTHER_IMAGE] } }, + $args: [SAVED_IMAGE.hash], + }) as any[]; + + expect(result.map((item) => item.id)).toEqual([OTHER_IMAGE.id]); + }); + + it('hash 가 없는 항목은 id 로도 제거된다', () => { + const idOnly = { id: 303, original_filename: 'no-hash.png' }; + + const result = evaluateBinding(removeExpression, { + _local: { form: { images: [idOnly, OTHER_IMAGE] } }, + $args: [idOnly.id], + }) as any[]; + + expect(result.map((item) => item.id)).toEqual([OTHER_IMAGE.id]); + }); + + it('대상이 아닌 식별자면 아무것도 제거하지 않는다', () => { + const result = evaluateBinding(removeExpression, { + _local: { form: { images: [SAVED_IMAGE, OTHER_IMAGE] } }, + $args: ['nonexistent-hash'], + }) as any[]; + + expect(result).toHaveLength(2); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx index 649bc59f..979b6b4b 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx @@ -9,8 +9,12 @@ * - 폼 바인딩 및 핸들러 검증 * - 다국어 키 검증 * - * @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 + * 축 요약(마커 아님 — 평문): extension_payment_method 의 method_kind=extension, + * capability_declared=declared, capability=pg_locked. 요약을 시나리오 축 마커로 적을 때 + * 구분자를 `×` 로 쓰면 파서가 쉼표로만 축을 분리하므로 세 축이 한 문자열로 뭉쳐 + * 실재하지 않는 조합 1건이 되어 어떤 칸도 커버하지 못한다 — 요약은 평문으로 둔다. + * + * 효과 요약(마커 아님 — 평문): admin_shows_pg_locked_badge, admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked. * * @vitest-environment node */ @@ -382,6 +386,59 @@ describe('결제수단 Sortable 리스트 구조 검증 (_payment_methods_list.j ); }); + // 지정 PG 가 레지스트리에서 사라진 상태(A2). 수단 자체는 카탈로그에 남아 있어 + // _orphaned 로는 잡히지 않으므로 별도 배지가 필요하다. + it('죽은 PG 배지가 _orphaned_pg 조건에서만 표시되어야 한다', () => { + const badge = findFirst(tpl, (n: any) => + typeof n?.props?.['data-testid'] === 'string' + && n.props['data-testid'].includes('orphaned-pg-badge-'), + ); + expect(badge).not.toBeNull(); + expect(badge.if).toBe('{{$method._orphaned_pg}}'); + expect(badge.text).toBe( + '$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_pg_badge', + ); + }); + + // 브라우저 실측에서 드러난 결함(A2 매트릭스 T1): 배지가 줄바꿈 가능·축소 가능이라 + // 좁은 폭에서 글자 단위로 접히고, 그 압력이 이름 열까지 밀어 이름도 세로로 무너졌다. + it('상태 배지는 줄바꿈·축소되지 않아야 한다 (이름 열 붕괴 차단)', () => { + const badges = [ + findFirst(tpl, (n: any) => n?.if === '{{$method._orphaned}}' && n?.name === 'Span'), + findFirst(tpl, (n: any) => n?.if === '{{$method._orphaned_pg}}' && n?.name === 'Span'), + ]; + for (const badge of badges) { + expect(badge).not.toBeNull(); + expect(badge.props.className).toContain('whitespace-nowrap'); + expect(badge.props.className).toContain('shrink-0'); + } + }); + + it('이름 줄은 축소 가능해야 하고 이름은 말줄임 처리되어야 한다', () => { + const nameSpan = findFirst(tpl, (n: any) => + typeof n?.text === 'string' && n.text.includes('_cached_name') && n?.name === 'Span', + ); + expect(nameSpan).not.toBeNull(); + // truncate 가 없으면 좁은 폭에서 글자 단위 줄바꿈으로 무너진다 + expect(nameSpan.props.className).toContain('truncate'); + + const nameRow = findFirst(tpl, (n: any) => + Array.isArray(n?.children) && n.children.includes(nameSpan), + ); + // flex 항목 기본 min-width:auto 때문에 min-w-0 없이는 축소 자체가 안 된다 + expect(nameRow.props.className).toContain('min-w-0'); + }); + + // _orphaned 와 달리 행 편집 컨트롤은 막지 않는다 — 살아있는 PG 로 바꿔 복구해야 하므로 + it('죽은 PG 상태는 PG 선택 셀렉트를 감추지 않아야 한다', () => { + const select = findFirst(tpl, (n: any) => + typeof n?.props?.['data-testid'] === 'string' + && n.props['data-testid'].includes('pg-select-'), + ); + expect(select).not.toBeNull(); + expect(select.if).not.toContain('_orphaned_pg'); + }); + it('재고차감시점 Select가 3개 옵션(order_placed/payment_complete/none)을 가져야 한다', () => { // 2개 옵션 → 3개 (none 추가: 차감 안함) const select = findFirst(tpl, (n: any) => @@ -527,6 +584,8 @@ describe('결제수단 Sortable 리스트 구조 검증 (_payment_methods_list.j && n.props['data-testid'].includes(needle), ); + /** @effects admin_shows_pg_locked_badge */ + /** @effects admin_shows_pg_locked_badge */ it('PG 고정 배지가 $method.pg_locked 조건으로 렌더된다', () => { const badge = findByTestidExpr('pg-locked-badge-'); expect(badge).not.toBeNull(); @@ -537,6 +596,7 @@ describe('결제수단 Sortable 리스트 구조 검증 (_payment_methods_list.j expect(badge.text).toContain('pg_provider'); }); + /** @effects admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked */ it('PG 선택 셀렉트가 !pg_locked && needs_pg && 제공자>0 조건으로만 렌더된다', () => { const select = findByTestidExpr('pg-select-'); expect(select).not.toBeNull(); @@ -611,6 +671,7 @@ describe('결제수단 모바일 카드 구조 검증 (_payment_methods_cards.js expect(badge.if).toBe('{{$method.pg_locked}}'); }); + /** @effects admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked */ it('PG 선택 셀렉트가 !pg_locked && needs_pg 조건으로만 렌더된다', () => { const select = findByTestidExpr('pg-select-'); expect(select).not.toBeNull(); @@ -630,6 +691,16 @@ describe('결제수단 모바일 카드 구조 검증 (_payment_methods_cards.js expect(findByTestidExpr(needle)).not.toBeNull(); } }); + + // A2 — 죽은 PG 배지는 PC/모바일 양쪽에 같은 조건·같은 키로 있어야 한다 + it('죽은 PG 배지가 _orphaned_pg 조건에서만 표시되어야 한다', () => { + const badge = findByTestidExpr('orphaned-pg-badge-'); + expect(badge).not.toBeNull(); + expect(badge.if).toBe('{{$method._orphaned_pg}}'); + expect(badge.text).toBe( + '$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_pg_badge', + ); + }); }); }); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsPublicAssetDisk.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsPublicAssetDisk.test.tsx new file mode 100644 index 00000000..d05758f1 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsPublicAssetDisk.test.tsx @@ -0,0 +1,249 @@ +/** + * 이커머스 설정 기본정보 탭 — 공개 자산 디스크 오버라이드 필드 테스트 (공개#100) + * + * @description + * 모듈 오버라이드 필드는 저장 키(basic_info.public_asset_disk)와 설정 응답의 + * 카탈로그(available_public_asset_disks)를 바인딩해야 한다. 세 지점(defaults.json · + * StoreEcommerceSettingsRequest · 레이아웃 name)이 같은 키를 가리키지 않으면 + * 저장값이 조용히 버려지거나(whitelist 탈락) 화면만 있고 저장이 없는 상태가 된다. + * + * 계약 단언에 더해, 실제 카드 서브트리를 createLayoutTest 로 렌더링해 + * "코어 설정 따름" 선두 옵션 + 카탈로그 $localized 매핑 계산까지 검증한다. + * + * @effects settings_catalog_includes_plugin_registered_disks + */ + +import React from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +import tabBasicInfo from '../../../layouts/admin/partials/admin_ecommerce_settings/_tab_basic_info.json'; + +// ─── 테스트용 컴포넌트 ─── + +const TestDiv: React.FC<{ className?: string; children?: React.ReactNode; 'data-testid'?: string }> = + ({ className, children, 'data-testid': testId }) => ( + <div className={className} data-testid={testId}>{children}</div> + ); + +const TestH3: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <h3 className={className}>{children || text}</h3>; + +const TestLabel: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <label className={className}>{children || text}</label>; + +const TestSpan: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <span className={className}>{children || text}</span>; + +const TestSelect: React.FC<{ + name?: string; value?: string; className?: string; options?: any[]; disabled?: boolean; + error?: string; onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void; +}> = ({ name, value, className, options, disabled, onChange }) => ( + <select data-name={name} value={value} className={className} disabled={disabled} onChange={onChange}> + {options?.map((o) => <option key={String(o.value)} value={o.value}>{o.label}</option>)} + </select> +); + +// 렌더러가 컴포넌트 목록을 Fragment 로 감싸므로 반드시 등록해야 한다. +const TestFragment: React.FC<{ children?: React.ReactNode }> = ({ children }) => <>{children}</>; + +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + H3: { component: TestH3, metadata: { name: 'H3', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + return registry; +} + +// ─── 다국어 (기본정보 탭 공개 자산 카드가 쓰는 키) ─── + +const translations = { + 'sirsoft-ecommerce': { + admin: { + settings: { + basic_info: { + public_asset_storage: { + title: '공개 자산 스토리지', + description: '이 모듈의 이미지 서빙 디스크를 오버라이드합니다.', + disk: '공개 자산 디스크', + follow_core: '코어 설정 따름', + help: '미설정 시 코어 전역 설정을 따릅니다.', + }, + }, + }, + }, + }, +}; + +/** + * 모듈 루트(모듈 manifest 기준)를 위로 훑어 찾는다. + * + * @returns 모듈 루트 절대경로 + */ +function moduleRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'module.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('module.json 을 가진 모듈 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + for (const child of node.children ?? []) { + const found = findNode(child, predicate); + if (found) return found; + } + + return null; +} + +// 실제 카드 서브트리를 그대로 렌더 대상으로 쓴다 — 사본 표현식이 아닌 배포 JSON 자체가 SSoT. +const publicAssetCard = findNode( + tabBasicInfo, + (n) => n.id === 'public_asset_storage_card', +); + +const CATALOG = [ + { id: 'none', label: { ko: '사용 안 함 (스트리밍)', en: 'None (streaming)' } }, + { id: 'fake_cdn', label: { ko: '가짜 CDN', en: 'Fake CDN' } }, +]; + +/** + * 카드 렌더링용 테스트 유틸을 생성한다. + * + * @returns createLayoutTest 유틸 + */ +function makeUtils() { + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_ecommerce_public_asset_card', + components: [publicAssetCard], + } as any, + { + auth: { + isAuthenticated: true, + user: { id: 1, name: 'Admin', role: 'super_admin' }, + authType: 'admin', + }, + translations, + locale: 'ko', + initialState: { + _local: { + form: { + available_public_asset_disks: CATALOG, + basic_info: { public_asset_disk: '' }, + }, + errors: null, + }, + }, + }, + ); +} + +describe('이커머스 공개 자산 디스크 오버라이드 필드 — 계약', () => { + it('Select 가 저장 키와 카탈로그를 바인딩하고 코어 따름 기본 옵션을 갖는다', () => { + const select = findNode( + publicAssetCard, + (n) => n.name === 'Select' && n.props?.name === 'basic_info.public_asset_disk', + ); + + expect(select).not.toBeNull(); + expect(select.props.options).toContain('available_public_asset_disks'); + expect(select.props.options).toContain('follow_core'); + }); + + it('defaults.json 과 저장 규칙이 같은 키를 선언한다', () => { + const root = moduleRoot(); + const defaults = JSON.parse( + fs.readFileSync(path.join(root, 'config/settings/defaults.json'), 'utf-8'), + ); + const requestSource = fs.readFileSync( + path.join(root, 'src/Http/Requests/Admin/StoreEcommerceSettingsRequest.php'), + 'utf-8', + ); + + expect(defaults.defaults.basic_info).toHaveProperty('public_asset_disk'); + expect(requestSource).toContain("'basic_info.public_asset_disk'"); + }); + + it('module.php 오버라이드가 같은 설정 키를 조회한다', () => { + const moduleSource = fs.readFileSync(path.join(moduleRoot(), 'module.php'), 'utf-8'); + + expect(moduleSource).toContain("'basic_info.public_asset_disk'"); + expect(moduleSource).toContain('PUBLIC_ASSET_CATEGORIES'); + }); +}); + +describe('이커머스 공개 자산 디스크 오버라이드 필드 — 렌더링', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest> | null = null; + + beforeEach(() => { + registry = setupTestRegistry(); + // 표현식 내 $t() 는 컨텍스트에 $templateId 가 없으면 __templateApp.getConfig() + // 로 회수한다(engine-v1.38.2 폴백). layoutTestUtils 는 이를 세팅하지 않으므로 + // 테스트에서 동일 폴백 경로를 제공해야 옵션 계산식의 $t() 가 해석된다. + (window as any).__templateApp = { + getConfig: () => ({ templateId: 'test-template', locale: 'ko' }), + }; + }); + + afterEach(() => { + testUtils?.cleanup(); + testUtils = null; + (registry as any).registry = {}; + delete (window as any).__templateApp; + }); + + it('카드 제목/도움말이 다국어 해석되어 렌더된다', async () => { + testUtils = makeUtils(); + const { container } = await testUtils.render(); + + expect(container.querySelector('h3.card-title')?.textContent ?? '').toContain( + '공개 자산 스토리지', + ); + expect(container.textContent ?? '').toContain('미설정 시 코어 전역 설정을 따릅니다.'); + expect(() => testUtils!.assertNoValidationErrors()).not.toThrow(); + }); + + it('Select 옵션이 코어 따름 선두 + 카탈로그 $localized 매핑으로 계산된다', async () => { + testUtils = makeUtils(); + const { container } = await testUtils.render(); + + const select = container.querySelector('select[data-name="basic_info.public_asset_disk"]'); + expect(select).not.toBeNull(); + + const options = Array.from(select!.querySelectorAll('option')); + expect(options.map((o) => o.getAttribute('value'))).toEqual(['', 'none', 'fake_cdn']); + expect(options.map((o) => o.textContent)).toEqual([ + '코어 설정 따름', + '사용 안 함 (스트리밍)', + '가짜 CDN', + ]); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsSavePerTab.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsSavePerTab.test.tsx index d08a103a..d4102400 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsSavePerTab.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsSavePerTab.test.tsx @@ -1,5 +1,7 @@ /** * 환경설정 탭별 개별 저장 패턴 검증 테스트 + * e2e:allow 테스트 설명 문구만 변경(공개 릴리즈 사전점검 — 내부 호칭 정리). + * 런타임 코드/레이아웃 무변경이라 브라우저 동작 차이가 없다. * * @description * - 저장 버튼의 apiCall body가 활성 탭 데이터만 전송하는지 검증 @@ -93,6 +95,20 @@ describe('환경설정 탭별 개별 저장 패턴 검증', () => { expect(body).toContain("'basic_info'"); }); + it('저장 실패 토스트가 서버 오류 메시지를 노출해야 한다 (회귀 — 운영 실측 제보)', () => { + // 고정 문구만 쓰면 검증 실패 사유(예: 필드별 422 메시지)가 사용자에게 + // 도달하지 못하고 "설정 저장에 실패했습니다" 로 뭉개진다. + const apiCallAction = findActionByHandler(saveButton.actions, 'apiCall'); + const errorToast = (apiCallAction.onError ?? []).find( + (a: any) => a.handler === 'toast', + ); + + expect(errorToast).toBeDefined(); + expect(errorToast.params.message).toContain('error.message'); + // 서버 메시지 부재 시 기존 고정 문구로 폴백 + expect(errorToast.params.message).toContain('save_error'); + }); + it('body 에 _tab 메타 필드가 포함되어야 한다', () => { const apiCallAction = findActionByHandler(saveButton.actions, 'apiCall'); const body = apiCallAction.params.body; diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminMobileViewportOverflow.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminMobileViewportOverflow.test.tsx index 9d0ff405..41375328 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminMobileViewportOverflow.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminMobileViewportOverflow.test.tsx @@ -97,11 +97,17 @@ describe('B2 — 주문상세 모달 상품표 카드형 전환', () => { const cancel = load('partials/admin_ecommerce_order_detail/_modal_cancel_order.json'); const batch = load('partials/admin_ecommerce_order_detail/_modal_batch_change_confirm.json'); - const unitPrice = collect( + // 금액 노드는 서버가 통화까지 포함해 서식한 `*_formatted` 를 그대로 출력한다. + // (한때 `원` 리터럴로 이 노드를 찾았으나 통화 하드코딩 제거 후 선택자가 아무것도 못 찾아 + // `cls(undefined)` 를 단언하는 빈 가드가 되어 있었다 — 선택자를 서식 필드 기준으로 고정한다) + const unitPrices = collect( cancel, - (n) => typeof n.text === 'string' && n.text.includes('unit_price') && n.text.includes('원'), - )[0]; - expect(cls(unitPrice)).toContain('whitespace-nowrap'); + (n) => typeof n.text === 'string' && n.text.includes('unit_price_formatted'), + ); + expect(unitPrices.length, '단가 표시 노드를 찾지 못했다').toBeGreaterThan(0); + for (const node of unitPrices) { + expect(cls(node)).toContain('whitespace-nowrap'); + } const batchAmounts = collect( batch, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/cashReceiptUi.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/cashReceiptUi.test.tsx index bd2f5bbe..e9197d78 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/cashReceiptUi.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/cashReceiptUi.test.tsx @@ -163,10 +163,30 @@ describe('W-2 유저 주문상세 현금영수증 카드 — 상태머신 5종', ...over, }); - it('① 무통장이 아니거나 프로바이더 미설정이면 카드 자체를 렌더하지 않는다', () => { + it('① 무통장이 아니면 카드 자체를 렌더하지 않는다', () => { expect(evalIf(card.if, { order: { data: { payment: payment() } } })).toBe(true); expect(evalIf(card.if, { order: { data: { payment: payment({ payment_method: 'card' }) } } })).toBe(false); - expect(evalIf(card.if, { order: { data: { payment: payment({ cash_receipt_provider: null }) } } })).toBe(false); + }); + + // A3 — 제공 확장이 사라지면 프로바이더는 미설정으로 해석된다. 그때 이력까지 감추면 + // 이미 발급된 영수증 URL 에 도달할 방법이 없어진다. + it('① 프로바이더 미설정 + 이력 없음이면 카드를 렌더하지 않는다', () => { + const ctx = { order: { data: { payment: payment({ cash_receipt_provider: null }), cash_receipt: null, cash_receipts: [] } } }; + expect(evalIf(card.if, ctx)).toBe(false); + }); + + it('① 프로바이더 미설정이어도 발급/시도 이력이 있으면 카드를 유지한다', () => { + const issued = { order: { data: { payment: payment({ cash_receipt_provider: null }), cash_receipt: { id: 1 }, cash_receipts: [{ id: 1 }] } } }; + expect(evalIf(card.if, issued)).toBe(true); + + const failedOnly = { order: { data: { payment: payment({ cash_receipt_provider: null }), cash_receipt: null, cash_receipts: [{ issue_status: 'FAILED' }] } } }; + expect(evalIf(card.if, failedOnly)).toBe(true); + }); + + it('③ 발급 버튼은 프로바이더가 해석될 때만 렌더한다 (죽은 provider 로 신청 불가)', () => { + const node = byId(mypageExt, 'ext_mypage_cash_receipt_issuable'); + const dead = { order: { data: { payment: payment({ cash_receipt_provider: null }), cash_receipt: null, cash_receipts: [] } } }; + expect(evalIf(node.if, dead)).toBe(false); }); it('② 입금 전에는 안내만 노출한다 — 무통장은 ready, 가상계좌는 waiting_deposit', () => { @@ -267,10 +287,23 @@ describe('W-2 유저 주문상세 현금영수증 카드 — 상태머신 5종', describe('W-3 관리자 주문상세 현금영수증 카드', () => { const card = byRowId(paymentInfo, 'payment_cash_receipt_card'); - it('결제카드 반복(payment) 컨텍스트에서 무통장 + 프로바이더 설정 시에만 렌더된다', () => { + it('결제카드 반복(payment) 컨텍스트에서 무통장일 때만 렌더된다', () => { expect(evalIf(card.if, { payment: { payment_method: 'dbank', cash_receipt_provider: 'toss' } })).toBe(true); expect(evalIf(card.if, { payment: { payment_method: 'vbank', cash_receipt_provider: 'toss' } })).toBe(false); - expect(evalIf(card.if, { payment: { payment_method: 'dbank', cash_receipt_provider: '' } })).toBe(false); + }); + + // A3 — 유저 화면과 같은 규칙: 프로바이더가 사라져도 이력이 있으면 관리자도 계속 봐야 한다 + it('프로바이더 미설정 + 이력 없음이면 카드를 렌더하지 않는다', () => { + const ctx = { payment: { payment_method: 'dbank', cash_receipt_provider: '' }, order: { data: { cash_receipt: null, cash_receipts: [] } } }; + expect(evalIf(card.if, ctx)).toBe(false); + }); + + it('프로바이더 미설정이어도 발급/시도 이력이 있으면 카드를 유지한다', () => { + const issued = { payment: { payment_method: 'dbank', cash_receipt_provider: '' }, order: { data: { cash_receipt: { id: 1 }, cash_receipts: [{ id: 1 }] } } }; + expect(evalIf(card.if, issued)).toBe(true); + + const failedOnly = { payment: { payment_method: 'dbank', cash_receipt_provider: null }, order: { data: { cash_receipt: null, cash_receipts: [{ issue_status: 'FAILED' }] } } }; + expect(evalIf(card.if, failedOnly)).toBe(true); }); it('입금 전 안내는 무통장(ready)과 가상계좌(waiting_deposit) 모두에서 노출된다', () => { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponCombinableWritePath.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponCombinableWritePath.test.tsx new file mode 100644 index 00000000..dd5b9cc2 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponCombinableWritePath.test.tsx @@ -0,0 +1,217 @@ +/** + * @file couponCombinableWritePath.test.tsx + * @description 쿠폰 is_combinable 라디오 쓰기 경로 boolean 회귀 — 공개 이슈 #97 + * + * 결함: RadioGroup 이 `name` 자동바인딩만으로 폼에 묶이면 클릭 시 + * `e.target.value`(DOM 문자열 "true"/"false")가 그대로 폼 상태에 저장되어 + * 서버 `boolean` 규칙에서 422 가 된다. 표시 계층은 느슨 비교(String===String)로 + * 정상이라 저장 시점에야 드러난다. + * + * 정정: 레이아웃이 `autoBinding: false` + `value: String(...)` 표시 바인딩 + + * `change` 액션의 `=== 'true'` 캐스팅으로 쓰기 경로를 boolean 으로 고정한다. + * + * 실제 partial(_partial_usage_conditions.json)의 field_is_combinable 노드를 + * 실제 메인 레이아웃과 동일한 폼 컨텍스트(dataKey: "form" + debounce: 300)로 + * 렌더하고, 폼 상태의 타입은 레이아웃 바인딩 프로브(엄격 === 비교 삼항식)로 + * 관측한다 — 렌더러 내부 폼 상태는 테스트 하네스 getState() 로는 보이지 않는다. + * (수정 전에는 클릭 후 프로브가 string-false 라 RED) + * + * @vitest-environment jsdom + */ + +import React, { useState, useEffect } from 'react'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { + createLayoutTest, + createMockComponentRegistryWithBasics, + fireEvent, + waitFor, + screen, + type MockComponentRegistry, +} from '@core/template-engine/__tests__/utils/layoutTestUtils'; + +import usageConditionsPartial from '../../../layouts/admin/partials/admin_ecommerce_promotion_coupon_form/_partial_usage_conditions.json'; + +/** JSON 트리에서 id로 노드를 재귀 탐색 */ +function findById(node: any, id: string): any { + if (!node) return null; + if (node.id === id) return node; + if (Array.isArray(node.children)) { + for (const child of node.children) { + const found = findById(child, id); + if (found) return found; + } + } + return null; +} + +/** + * FormField 더블 — 라벨 + children 렌더 (구조 통과용) + */ +const TestFormField: React.FC<{ + label?: string; + error?: string; + children?: React.ReactNode; +}> = ({ label, error, children }) => ( + <div data-testid="form-field"> + {label && <span>{label}</span>} + {children} + {error && <span role="alert">{error}</span>} + </div> +); + +/** + * RadioGroup 더블 — 실제 sirsoft-admin_basic RadioGroup 의 핵심 동작 복제: + * - 내부 상태 + prop 동기화 + * - 표시: String(option.value) === String(value) 느슨 비교 (표시 계층 구제 동작 그대로) + * - 클릭: DOM ChangeEvent 를 onChange 로 그대로 전달 (e.target.value 는 문자열) + */ +const TestRadioGroup: React.FC<{ + name: string; + value?: string; + options: Array<{ value: string; label: string }>; + onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; + inline?: boolean; +}> = ({ name, value: valueProp, options, onChange }) => { + const [value, setValue] = useState(() => valueProp ?? ''); + + useEffect(() => { + setValue(valueProp ?? ''); + }, [valueProp]); + + const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setValue(e.target.value); + if (onChange) onChange(e); + }; + + return ( + <div data-testid={`radio-group-${name}`}> + {options.map((option) => ( + <label key={String(option.value)}> + <input + type="radio" + name={name} + value={option.value} + checked={String(option.value) === String(value)} + onChange={handleChange} + /> + <span>{option.label}</span> + </label> + ))} + </div> + ); +}; + +/** + * 폼 상태 타입 프로브 — 엄격 === 비교로 boolean 과 문자열을 구분해 텍스트로 노출. + * 자동바인딩/액션의 쓰기 결과는 렌더러 내부 _local 에 반영되므로 + * 레이아웃 바인딩으로만 정확한 타입을 관측할 수 있다. + */ +const TYPE_PROBE_EXPR = + "{{_local.form?.is_combinable === true ? 'bool-true' : (_local.form?.is_combinable === false ? 'bool-false' : (_local.form?.is_combinable === 'true' ? 'string-true' : (_local.form?.is_combinable === 'false' ? 'string-false' : 'other')))}}"; + +/** + * 실제 partial 의 field_is_combinable 노드만 잘라 프로브 레이아웃 구성. + * 폼 컨텍스트는 실제 메인 레이아웃(admin_ecommerce_promotion_coupon_form.json + * #form_sections)과 동일하게 dataKey: "form" + debounce: 300 을 재현한다 — + * 자동바인딩 쓰기 경로는 이 컨텍스트가 있어야 발화한다. + */ +function buildProbeLayout() { + const fieldNode = findById(usageConditionsPartial, 'field_is_combinable'); + if (!fieldNode) throw new Error('field_is_combinable 노드를 찾지 못함'); + + return { + version: '1.0.0', + layout_name: 'test/coupon-combinable-write-path', + state: { + form: { + is_combinable: true, + }, + }, + components: [ + { + type: 'basic', + name: 'Div', + dataKey: 'form', + debounce: 300, + children: [ + fieldNode, + { + type: 'basic', + name: 'Span', + props: { 'data-testid': 'combinable-type-probe' }, + text: TYPE_PROBE_EXPR, + }, + ], + }, + ], + }; +} + +describe('쿠폰 is_combinable 쓰기 경로 (공개 #97)', () => { + let registry: MockComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest>; + + beforeEach(() => { + registry = createMockComponentRegistryWithBasics(); + registry.register('composite', 'FormField', TestFormField); + registry.register('composite', 'RadioGroup', TestRadioGroup); + + testUtils = createLayoutTest(buildProbeLayout(), { + componentRegistry: registry as any, + locale: 'ko', + }); + }); + + afterEach(() => { + testUtils.cleanup(); + vi.clearAllMocks(); + }); + + it('초기 상태: is_combinable 은 boolean true, "가능" 라디오 checked', async () => { + await testUtils.render(); + + expect(screen.getByTestId('combinable-type-probe').textContent).toBe('bool-true'); + + const radios = document.querySelectorAll<HTMLInputElement>('input[name=is_combinable]'); + expect(radios).toHaveLength(2); + expect(radios[0].checked).toBe(true); // value "true" (가능) + expect(radios[1].checked).toBe(false); // value "false" (불가능) + }); + + it('"불가능" 라디오 클릭 → 폼 상태가 boolean false 로 저장된다 (문자열 "false" 금지)', async () => { + await testUtils.render(); + + const radios = document.querySelectorAll<HTMLInputElement>('input[name=is_combinable]'); + fireEvent.click(radios[1]); + + // debounce 300ms 를 넘겨 최종 저장값의 타입을 판정한다 + await waitFor( + () => { + expect(screen.getByTestId('combinable-type-probe').textContent).toBe('bool-false'); + }, + { timeout: 2000 } + ); + }); + + it('"불가능" → "가능" 재클릭 → boolean true 복원', async () => { + await testUtils.render(); + + const radios = document.querySelectorAll<HTMLInputElement>('input[name=is_combinable]'); + fireEvent.click(radios[1]); + await waitFor( + () => { + expect(screen.getByTestId('combinable-type-probe').textContent).toBe('bool-false'); + }, + { timeout: 2000 } + ); + + fireEvent.click(radios[0]); + await waitFor( + () => { + expect(screen.getByTestId('combinable-type-probe').textContent).toBe('bool-true'); + }, + { timeout: 2000 } + ); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponFormLayouts.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponFormLayouts.test.tsx index a03622fc..962f9028 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponFormLayouts.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponFormLayouts.test.tsx @@ -564,6 +564,27 @@ describe('couponFormLayouts', () => { expect(radioGroup.props.options[1].value).toBe('false'); }); + it('쿠폰 중복 사용 RadioGroup: 쓰기 경로 boolean 캐스팅 (공개 #97)', () => { + const field = findById(usageConditionsPartial, 'field_is_combinable'); + const radioGroup = findByName(field, 'RadioGroup'); + + // 자동바인딩 value 경로는 e.target.value 문자열을 그대로 저장하므로 차단 + expect(radioGroup.props.autoBinding).toBe(false); + + // 표시 바인딩: 저장된 boolean 을 문자열로 변환해 옵션 값과 비교 + expect(radioGroup.props.value).toContain('String('); + expect(radioGroup.props.value).toContain('is_combinable'); + + // 쓰기 경로: change 액션이 === 'true' 캐스팅으로 boolean 저장 + const changeAction = (radioGroup.actions ?? []).find( + (a: any) => a.type === 'change' && a.handler === 'setState' + ); + expect(changeAction).toBeDefined(); + expect(changeAction.params['form.is_combinable']).toBe( + "{{$event.target.value === 'true'}}" + ); + }); + it('적용 대상 RadioGroup: all/products/categories', () => { const field = findById(usageConditionsPartial, 'field_target_scope'); expect(field).toBeDefined(); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/excel-download-history-modal-fallback.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/excel-download-history-modal-fallback.test.tsx new file mode 100644 index 00000000..e53b838c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/excel-download-history-modal-fallback.test.tsx @@ -0,0 +1,131 @@ +/** + * @file excel-download-history-modal-fallback.test.tsx + * @description 엑셀 다운로드 이력 모달의 미선택 상태 표시 가드 (공개 이슈 #92 동일 결함 클래스) + * + * 결함 클래스: 부모 레이아웃 state 에 `null` 로만 선언된 키를 옵셔널 체이닝 없이 하위 접근하면, + * 그 키를 채우는 유일한 지점(행 클릭 setState)이 아직 실행되지 않은 구간에서 표시가 비거나 + * 조건식이 예외로 떨어진다. `admin_ecommerce_excel_download_index.json` 의 `selectedItem` 이 + * 그 형태이고, 이 모달에는 selectedItem 을 검사하는 `if` 가드가 없다. + * + * 정정: 모달의 모든 `selectedItem` 하위 접근에 `?.` + 표시식 폴백(`?? ''`, count 인자는 `?? 0`)을 부여한다. + * 값이 채워진 뒤의 출력은 이전과 바이트 동일하므로 기능 변화는 없다. + * + * @vitest-environment jsdom + */ + +import { describe, it, expect } from 'vitest'; + +import downloadHistoryModal from '../../../layouts/admin/partials/admin_ecommerce_excel_download_index/_modal_download_history.json'; +import excelDownloadIndex from '../../../layouts/admin/admin_ecommerce_excel_download_index.json'; + +/** + * JSON 트리에서 특정 문자열을 포함하는 문자열 값을 모두 수집합니다. + * + * @param node 순회 시작 노드 + * @param needle 찾을 부분 문자열 + * @returns 매칭된 문자열 값 목록 + */ +function collectStringsContaining(node: unknown, needle: string): string[] { + const out: string[] = []; + const walk = (n: unknown): void => { + if (Array.isArray(n)) { + n.forEach(walk); + return; + } + if (!n || typeof n !== 'object') return; + const obj = n as Record<string, unknown>; + for (const v of Object.values(obj)) { + if (typeof v === 'string' && v.includes(needle)) out.push(v); + } + Object.values(obj).forEach(walk); + }; + walk(node); + return out; +} + +const refs = collectStringsContaining(downloadHistoryModal, 'selectedItem'); + +describe('_modal_download_history.json — selectedItem 표시 가드', () => { + /** + * @scenario selection_state=unselected + * @effects history_modal_display_guarded + */ + it('부모 레이아웃이 selectedItem 을 null 로만 시드한다 (전제 확인)', () => { + const state = (excelDownloadIndex as any).state ?? {}; + expect( + Object.prototype.hasOwnProperty.call(state, 'selectedItem'), + 'selectedItem 시드가 사라졌다면 이 테스트의 전제를 재검토할 것' + ).toBe(true); + expect(state.selectedItem).toBeNull(); + }); + + /** + * @scenario selection_state=unselected + * @effects history_modal_display_guarded + */ + it('selectedItem 하위 접근은 전부 옵셔널 체이닝을 쓴다', () => { + expect(refs.length, 'selectedItem 참조를 찾지 못했다').toBeGreaterThan(0); + const unguarded = refs.filter((r) => /selectedItem\.[A-Za-z_]/.test(r)); + expect( + unguarded, + `selectedItem 뒤에 '?.' 없는 역참조: ${unguarded.join(' | ')}` + ).toEqual([]); + }); + + /** + * 다운로드 기록 목록이 문자열 형태 `"iteration": "{{...}}"` 로 선언되어 있어 + * 엔진이 `iteration.source` 를 읽지 못하고 매 렌더마다 TypeError 로 떨어졌다 + * (목록이 통째로 비어 보임 — 브라우저 실측에서 발견). + * + * @scenario selection_state=selected + * @effects history_list_renders + */ + it('다운로드 기록 iteration 이 source/item_var/index_var 객체 형태다', () => { + const iterations: unknown[] = []; + const walk = (n: unknown): void => { + if (Array.isArray(n)) { n.forEach(walk); return; } + if (!n || typeof n !== 'object') return; + const obj = n as Record<string, unknown>; + if ('iteration' in obj) iterations.push(obj.iteration); + Object.values(obj).forEach(walk); + }; + walk(downloadHistoryModal); + + expect(iterations.length, 'iteration 선언을 찾지 못했다').toBeGreaterThan(0); + for (const it of iterations) { + expect(typeof it, `문자열 형태 iteration 은 엔진이 해석하지 못한다: ${String(it)}`).toBe('object'); + const cfg = it as Record<string, unknown>; + expect(typeof cfg.source).toBe('string'); + expect(cfg.item_var).toBeTruthy(); + } + }); + + /** + * @scenario selection_state=selected + * @effects history_modal_display_guarded + */ + it('표시식(text)은 값 부재 시 폴백을 갖는다', () => { + const textRefs: string[] = []; + const walk = (n: unknown): void => { + if (Array.isArray(n)) { + n.forEach(walk); + return; + } + if (!n || typeof n !== 'object') return; + const obj = n as Record<string, unknown>; + if (typeof obj.text === 'string' && obj.text.includes('selectedItem')) { + textRefs.push(obj.text); + } + Object.values(obj).forEach(walk); + }; + walk(downloadHistoryModal); + + // 개수를 고정한다 — `> 0` 만 두면 표시식이 삭제돼도 남은 것들이 폴백을 갖는 한 통과한다. + // 현재 6건: file_content / category_name / created_date / expire_date / download_count / + // download_history.length (마지막 1건은 원래부터 가드가 있어 #92 수정 대상 5건에 포함되지 않는다) + expect(textRefs.length, `selectedItem 표시식 개수 변동: ${textRefs.join(' | ')}`).toBe(6); + for (const t of textRefs) { + expect(t, `폴백 없는 표시식: ${t}`).toMatch(/\?\?\s*('{2}|0)/); + } + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/orderDetailLayouts.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/orderDetailLayouts.test.tsx index 147468e6..6aa325d1 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/orderDetailLayouts.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/orderDetailLayouts.test.tsx @@ -698,6 +698,13 @@ describe('Partial 레이아웃 구조 검증', () => { // ========== 합계행 백엔드 필드 마이그레이션 검증 ========== + /** + * 합계행(footerCells)이 order.data 백엔드 필드에 바인딩되어 있는지의 구조 검증. + * 공개 #98(stale dist) 시나리오 매니페스트 admin-order-detail-footer-totals.yaml 의 + * 레이아웃 계층 커버를 담당한다 — 서빙 번들 실검은 Playwright spec 이 맡는다. + * + * @effects order_detail_footer_totals_fields_bound + */ it('합계행(footer)이 computed 대신 order.data 백엔드 필드를 사용한다', () => { const json = JSON.stringify(orderInfoPartial); // computed 참조가 없어야 함 diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicyFormLayouts.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicyFormLayouts.test.tsx index caa7edab..58d2b203 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicyFormLayouts.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicyFormLayouts.test.tsx @@ -563,8 +563,10 @@ describe('shippingPolicyFormLayouts', () => { expect(rangesField.if).toBe('{{_local.showRanges}}'); // tier 행 iteration - const tierRow = findById(chargeSettingsPartial, 'range_tier_row'); - expect(tierRow).toBeDefined(); + // id 는 반복 인덱스를 포함한 템플릿 형태다 — 접두어만으로 찾으면 null 이 돌아오고, + // toBeDefined() 는 null 도 통과시키므로 존재 확인은 not.toBeNull() 로 한다. + const tierRow = findById(chargeSettingsPartial, 'range_tier_row_{{tierIdx}}'); + expect(tierRow).not.toBeNull(); expect(tierRow.iteration).toBeDefined(); expect(tierRow.iteration.source).toBe('{{_computed.activeCountrySetting?.ranges?.tiers ?? []}}'); expect(tierRow.iteration.item_var).toBe('tier'); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicySettingsTab.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicySettingsTab.test.tsx index 9085713f..fcc7b828 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicySettingsTab.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicySettingsTab.test.tsx @@ -6,6 +6,9 @@ * - _tab_shipping.json (배송설정 기본 + 배송가능국가 + 도서산간) * - _shipping_country_table.json (국가 테이블) */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; // 레이아웃 JSON 임포트 @@ -16,6 +19,40 @@ import countryCards from '../../../layouts/admin/partials/admin_ecommerce_settin import disableIntlModal from '../../../layouts/admin/partials/admin_ecommerce_settings/_disable_international_shipping_modal.json'; // ── 헬퍼 함수 ────────────────────────────────────────── + +/** + * 이 화면을 렌더하는 admin 템플릿의 매니페스트가 선언한 컴포넌트 종류를 읽는다. + * + * 레이아웃 노드의 `type` 은 렌더러가 props 전달 범위를 고르는 기준이라 + * 매니페스트 선언과 어긋나면 `editorAttrs` 가 제거돼 레이아웃 편집기에서 표식을 잃는다. + * 바인딩 방식(자동/수동)과는 무관한 값이다. + * + * @param name 컴포넌트 이름 + * @returns 매니페스트가 선언한 type + */ +function manifestType(name: string): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'artisan'))) break; + current = path.dirname(current); + } + + const manifest = JSON.parse( + fs.readFileSync( + path.join(current, 'templates/_bundled/sirsoft-admin_basic/components.json'), + 'utf-8', + ), + ); + + for (const [kind, entries] of Object.entries<any>(manifest.components ?? {})) { + if ((entries as any[]).some((entry) => entry.name === name)) { + return kind; + } + } + + throw new Error(`매니페스트에 ${name} 선언이 없습니다.`); +} function flattenAll(node: any): any[] { const result: any[] = []; if (!node) return result; @@ -123,14 +160,15 @@ describe('배송설정 탭 콘텐츠 (_tab_shipping.json)', () => { expect(select.props?.options).toContain('is_active'); }); - it('해외배송 Toggle이 수동 바인딩(basic + checked + actions)으로 구성되어 있다', () => { + it('해외배송 Toggle이 수동 바인딩(checked + actions)으로 구성되어 있다', () => { const intlField = findById(allNodes, 'international_shipping_field'); expect(intlField).toBeDefined(); const toggles = flattenAll(intlField).filter((n: any) => n.name === 'Toggle'); expect(toggles.length).toBeGreaterThan(0); const toggle = toggles[0]; - // 수동 바인딩: type=basic, checked prop, actions 있음 - expect(toggle.type).toBe('basic'); + // 수동 바인딩의 실체는 checked prop + actions 다. 노드 type 은 바인딩 방식과 무관하며 + // 매니페스트 선언을 따라야 한다(어긋나면 레이아웃 편집기에서 표식을 잃는다). + expect(toggle.type).toBe(manifestType('Toggle')); expect(toggle.props?.checked).toContain('international_shipping_enabled'); expect(toggle.actions).toBeDefined(); expect(toggle.actions.length).toBeGreaterThanOrEqual(2); @@ -359,7 +397,7 @@ describe('배송가능국가 테이블 (_shipping_country_table.json)', () => { }); it('Tbody에 iteration이 있다', () => { - const iterNode = findById(allNodes, 'country_row'); + const iterNode = findById(allNodes, 'country_row_{{countryIndex}}'); expect(iterNode).toBeDefined(); expect(iterNode.iteration).toBeDefined(); expect(iterNode.iteration.source).toContain('available_countries'); @@ -496,7 +534,7 @@ describe('바인딩 패턴 일관성 검증', () => { it('해외배송 Toggle이 수동 바인딩이며 ON/OFF 모두 hasChanges를 설정한다', () => { const intlField = findById(allNodes, 'international_shipping_field'); const toggle = flattenAll(intlField).find((n: any) => n.name === 'Toggle'); - expect(toggle.type).toBe('basic'); + expect(toggle.type).toBe(manifestType('Toggle')); expect(toggle.props?.checked).toContain('international_shipping_enabled'); expect(toggle.actions).toBeDefined(); // ON action에 hasChanges: true @@ -758,7 +796,7 @@ describe('배송가능국가 모바일 카드 — 루트 구조', () => { it('카드가 excel-card 클래스로 반복 렌더링된다', () => { const allNodes = flattenAll(countryCards); - const card = findById(allNodes, 'country_card'); + const card = findById(allNodes, 'country_card_{{countryIndex}}'); expect(card).toBeDefined(); expect(card.props?.className).toContain('excel-card'); expect(card.iteration).toBeDefined(); @@ -769,7 +807,7 @@ describe('배송가능국가 모바일 카드 — 루트 구조', () => { it('iteration source에 _idx 인덱스 주입이 포함된다', () => { const allNodes = flattenAll(countryCards); - const card = findById(allNodes, 'country_card'); + const card = findById(allNodes, 'country_card_{{countryIndex}}'); expect(card.iteration.source).toContain('_idx'); expect(card.iteration.source).toContain('.map('); }); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingTypeSection.test.tsx b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingTypeSection.test.tsx index 04c5325e..0995cf28 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingTypeSection.test.tsx +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingTypeSection.test.tsx @@ -277,7 +277,7 @@ describe('배송유형 카드뷰 구조', () => { const allNodes = flattenAll(typeCards); it('iteration으로 _local.form.shipping.types를 순회한다', () => { - const card = allNodes.find((n: any) => n.id === 'shipping_type_card'); + const card = allNodes.find((n: any) => n.id === 'shipping_type_card_{{tIdx}}'); expect(card).toBeDefined(); expect(card.iteration.source).toContain('_local.form?.shipping?.types'); expect(card.iteration.item_var).toBe('type'); diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/confirmSaveNoticeTemplate.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/confirmSaveNoticeTemplate.ts index 19f7da7b..580c5736 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/confirmSaveNoticeTemplate.ts +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/confirmSaveNoticeTemplate.ts @@ -1,3 +1,4 @@ +// audit:allow extension-dist-source-literal-sync 고아 중복 구현 — handlers/index.ts 는 noticeHandlers.ts 의 동명 핸들러를 사용하며 이 파일은 어디서도 import 되지 않아 번들에 포함되지 않는다 (공유 리터럴이 있어 번들 포함 게이트로는 걸러지지 않음) /** * 고시정보 템플릿 저장 확인 핸들러 * diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/expandedOptionsHandlers.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/expandedOptionsHandlers.ts index 7294597c..2424054a 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/expandedOptionsHandlers.ts +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/expandedOptionsHandlers.ts @@ -1,3 +1,4 @@ +// audit:allow extension-dist-source-literal-sync 수신 함수가 읽지 않는 handler 필드를 rollup 최적화가 소거해 해당 리터럴만 번들에서 빠진다 (fresh 빌드로 실측 — stale 아님) /** * 상품 목록 확장 행 옵션 지연 로딩 핸들러 * diff --git a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/shippingPolicyFormHandlers.ts b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/shippingPolicyFormHandlers.ts index e2ada8ab..63f9073c 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/shippingPolicyFormHandlers.ts +++ b/modules/_bundled/sirsoft-ecommerce/resources/js/handlers/shippingPolicyFormHandlers.ts @@ -29,7 +29,7 @@ interface CountrySetting { charge_policy: string; base_fee: number; free_threshold: number | null; - ranges: { type?: string; tiers?: RangeTier[]; unit_value?: number } | null; + ranges: { type?: string; tiers?: RangeTier[]; unit_value?: number | null } | null; api_endpoint: string | null; api_request_fields: string[] | null; api_response_fee_field: string | null; @@ -41,9 +41,10 @@ interface CountrySetting { } interface RangeTier { - min: number; + /** 시작값 — 직전 구간의 종료값에서 자동 파생되며 화면에서는 읽기전용 표시 */ + min: number | null; max: number | null; - fee: number; + fee: number | null; } interface ApiConfig { @@ -87,6 +88,59 @@ const REQUIRES_UNIT_VALUE = [ 'per_quantity', 'per_weight', 'per_volume', 'per_volume_weight', 'per_amount', ]; +/** + * 구간 경계값이 이산형(정수)인 정책. + * + * 수량은 "5개 다음이 6개"(max + 1)로 이어지고, 금액·무게·부피는 연속값이라 + * "5kg 다음은 5kg 초과"(다음 min = max)로 이어진다. 서버 검증(ChargePolicyEnum:: + * hasDiscreteRangeValues)과 같은 판정을 사용해야 화면과 저장 결과가 어긋나지 않는다. + */ +const DISCRETE_RANGE_POLICIES = ['range_quantity']; + +/** 연속형 구간 경계 비교 허용 오차 (서버 검증과 동일) */ +const CONTINUITY_EPSILON = 0.001; + +/** + * 부과정책이 이산형 구간을 쓰는지 판정합니다. + * + * @param chargePolicy 부과정책 값 + * @returns 이산형이면 true + */ +function isDiscreteRangePolicy(chargePolicy: string): boolean { + return DISCRETE_RANGE_POLICIES.includes(chargePolicy); +} + +/** + * 직전 구간의 종료값으로부터 다음 구간의 시작값을 파생합니다. + * + * @param previousMax 직전 구간 종료값 + * @param chargePolicy 부과정책 값 + * @returns 파생된 시작값 (종료값 미입력 시 null) + */ +function deriveNextMin(previousMax: number | null | undefined, chargePolicy: string): number | null { + if (previousMax === null || previousMax === undefined || Number.isNaN(previousMax)) { + return null; + } + + return isDiscreteRangePolicy(chargePolicy) ? previousMax + 1 : previousMax; +} + +/** + * 구간 배열의 시작값을 직전 구간 종료값 기준으로 전부 재파생합니다. + * + * 첫 구간은 항상 0 이며, 이후 구간은 직전 종료값에서 파생됩니다. + * + * @param tiers 구간 배열 + * @param chargePolicy 부과정책 값 + * @returns 시작값이 재파생된 새 구간 배열 + */ +function rederiveTierMins(tiers: RangeTier[], chargePolicy: string): RangeTier[] { + return tiers.map((tier, index) => ({ + ...tier, + min: index === 0 ? 0 : deriveNextMin(tiers[index - 1]?.max, chargePolicy), + })); +} + // ===== 국가별 기본 설정값 ===== const DEFAULT_COUNTRY_SETTING: Omit<CountrySetting, 'country_code'> = { @@ -197,6 +251,26 @@ export function initShippingPolicyFormHandler( const chargePolicy = firstSetting?.charge_policy ?? 'fixed'; const flags = getVisibilityFlags(chargePolicy); Object.assign(stateUpdates, flags); + + // 저장된 구간의 시작값을 현재 계약으로 재파생한다. + // 기존 데이터는 "1~5개 / 6개~" 처럼 첫 시작값이 1 인 형태가 있는데, 시작값이 + // 읽기전용이 된 이상 화면이 정규화하지 않으면 운영자가 고칠 수단이 없고 + // 그대로 저장하면 "첫 구간의 시작값은 0이어야 합니다" 로 거부된다. + const normalized = countrySettings.map((cs: any) => { + const tiers = cs?.ranges?.tiers; + + if (!Array.isArray(tiers) || tiers.length === 0) { + return cs; + } + + return { + ...cs, + ranges: { ...cs.ranges, tiers: rederiveTierMins(tiers, cs.charge_policy) }, + }; + }); + + stateUpdates['form.country_settings'] = normalized; + logger.log('[initShippingPolicyForm]', isEdit ? 'Edit' : 'Copy', 'mode, countries:', countrySettings.length, 'first charge_policy:', chargePolicy); } else { // 등록 모드: 기본 가시성 (fixed 기준) @@ -399,6 +473,12 @@ export function updateCountryFieldHandler( updateLocalState(context, { 'form.country_settings': countrySettings, }); + + // 무료배송 기준금액은 비면 배송비가 계산되지 않으므로 저장 전에 화면에서 먼저 알린다 + if (field === 'free_threshold') { + validateChargeSettingRequirements(G7Core, context, index, countrySettings[index]); + } + logger.log('[updateCountryField] Updated', field, '=', value, 'at index:', index); } @@ -457,12 +537,14 @@ export function onChargePolicyChangeHandler( } // ranges 초기화 (구간 정책 선택 시 기본 구조 제공) + // 배송비는 빈 값으로 두어 미입력 상태가 required 검증에 걸리도록 한다 + // (0 으로 채우면 운영자가 입력하지 않은 구간이 무료배송으로 저장된다). if (flags.showRanges) { if (!updatedCS.ranges || !updatedCS.ranges.tiers) { const rangeType = chargePolicy.replace('range_', ''); updatedCS.ranges = { type: rangeType, - tiers: [{ min: 0, max: null, fee: 0 }], + tiers: [{ min: 0, max: null, fee: null }], }; } } @@ -485,6 +567,9 @@ export function onChargePolicyChangeHandler( }; updateLocalState(context, stateUpdates); + + // 정책이 바뀌면 요구되는 필수값도 바뀐다 — 직전 정책에서 남은 오류를 지우고 새 요구사항을 반영 + validateChargeSettingRequirements(G7Core, context, countryIndex, updatedCS); } /** @@ -510,19 +595,18 @@ export function addRangeTierHandler( return; } - const currentTiers = [...(cs.ranges?.tiers ?? [])]; - - // 마지막 tier의 max + 1 값을 새 tier의 min으로 사용 (포함 범위 기준) - const lastTier = currentTiers[currentTiers.length - 1]; - const newMin = lastTier?.max != null ? lastTier.max + 1 : 0; - - const newTier: RangeTier = { min: newMin, max: null, fee: 0 }; - currentTiers.push(newTier); + // 직전까지 마지막이던 구간은 이제 종료값이 필요하다 (무제한은 새 구간이 이어받는다). + // 종료값은 비운 상태로 두어 운영자 입력을 기다리고, 새 구간의 시작값은 그 입력 시점에 파생된다. + const currentTiers = [ + ...(cs.ranges?.tiers ?? []), + { min: null, max: null, fee: null } as RangeTier, + ]; + const nextTiers = rederiveTierMins(currentTiers, cs.charge_policy); // 새 객체 생성 (원본 mutation 방지 — deepMerge 변경 감지를 위해 필수) countrySettings[countryIndex] = { ...cs, - ranges: { ...(cs.ranges ?? {}), tiers: currentTiers }, + ranges: { ...(cs.ranges ?? {}), tiers: nextTiers }, }; updateLocalState(context, { @@ -530,8 +614,8 @@ export function addRangeTierHandler( }); // 구간 검증 실행 - validateRangeTiersInternal(G7Core, context, countryIndex, currentTiers, cs.country_code); - logger.log('[addRangeTier] Added tier, total:', currentTiers.length); + validateRangeTiersInternal(G7Core, context, countryIndex, nextTiers, cs.country_code, cs.charge_policy); + logger.log('[addRangeTier] Added tier, total:', nextTiers.length); } /** @@ -572,10 +656,13 @@ export function removeRangeTierHandler( currentTiers.splice(tierIndex, 1); + // 삭제로 앞뒤 구간이 이어졌으므로 시작값을 다시 파생한다 + const nextTiers = rederiveTierMins(currentTiers, cs.charge_policy); + // 새 객체 생성 (원본 mutation 방지 — deepMerge 변경 감지를 위해 필수) countrySettings[countryIndex] = { ...cs, - ranges: { ...(cs.ranges ?? {}), tiers: currentTiers }, + ranges: { ...(cs.ranges ?? {}), tiers: nextTiers }, }; updateLocalState(context, { @@ -583,8 +670,8 @@ export function removeRangeTierHandler( }); // 구간 검증 실행 - validateRangeTiersInternal(G7Core, context, countryIndex, currentTiers, cs.country_code); - logger.log('[removeRangeTier] Removed tier:', tierIndex, 'remaining:', currentTiers.length); + validateRangeTiersInternal(G7Core, context, countryIndex, nextTiers, cs.country_code, cs.charge_policy); + logger.log('[removeRangeTier] Removed tier:', tierIndex, 'remaining:', nextTiers.length); } /** @@ -630,20 +717,21 @@ export function updateRangeTierFieldHandler( } // min/max/fee는 숫자로 변환 (DOM input의 $event.target.value는 string) - // max는 빈 값일 경우 null (무제한) - let parsedValue: number | null; - if (field === 'max') { - parsedValue = value === '' || value === null || value === undefined ? null : Number(value); - } else { - parsedValue = Number(value); - } + // 빈 값은 null 로 보존한다 — 0 으로 강제하면 미입력 구간이 무료배송으로 저장된다. + const isEmpty = value === '' || value === null || value === undefined; + const parsedValue: number | null = isEmpty ? null : Number(value); currentTiers[tierIndex] = { ...currentTiers[tierIndex], [field]: parsedValue }; + // 종료값이 바뀌면 뒤따르는 구간의 시작값을 자동 재파생한다 (시작값은 입력 대상이 아니다) + const nextTiers = field === 'max' + ? rederiveTierMins(currentTiers, cs.charge_policy) + : currentTiers; + // 새 객체 생성 (원본 mutation 방지 — deepMerge 변경 감지를 위해 필수) countrySettings[countryIndex] = { ...cs, - ranges: { ...(cs.ranges ?? {}), tiers: currentTiers }, + ranges: { ...(cs.ranges ?? {}), tiers: nextTiers }, }; updateLocalState(context, { @@ -651,7 +739,7 @@ export function updateRangeTierFieldHandler( }); // 구간 검증 실행 - validateRangeTiersInternal(G7Core, context, countryIndex, currentTiers, cs.country_code); + validateRangeTiersInternal(G7Core, context, countryIndex, nextTiers, cs.country_code, cs.charge_policy); } /** @@ -675,73 +763,116 @@ export function validateRangeTiersHandler( if (!cs) return; const tiers = cs.ranges?.tiers ?? []; - validateRangeTiersInternal(G7Core, context, countryIndex, tiers, cs.country_code); + validateRangeTiersInternal(G7Core, context, countryIndex, tiers, cs.country_code, cs.charge_policy); } /** * 구간 검증 내부 로직 + * + * 서버(StoreShippingPolicyRequest::validateRangeTiersContinuity)와 동일한 규칙을 적용합니다. + * 한쪽만 바뀌면 화면에서 통과한 설정이 저장 시 422 로 거부되거나, 그 반대가 됩니다. */ function validateRangeTiersInternal( G7Core: any, context: ActionContext, _countryIndex: number, tiers: RangeTier[], - countryCode: string + countryCode: string, + chargePolicy: string ): void { const localState = G7Core.state.getLocal?.() ?? {}; const rangeErrors: Record<string, RangeTierError[]> = { ...(localState.rangeErrors ?? {}) }; + const t = G7Core.t; if (tiers.length === 0) { - // deepMerge에서 delete로 키를 제거할 수 없으므로 빈 배열로 대체 - rangeErrors[countryCode] = []; + // 구간별 정책인데 구간이 하나도 없으면 모든 주문이 0원(무료배송)이 된다 + rangeErrors[countryCode] = REQUIRES_RANGES.includes(chargePolicy) + ? [{ + max: t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_min_one') + ?? '구간별 배송비 정책은 구간을 1개 이상 등록해야 합니다.', + }] + : []; updateLocalState(context, { rangeErrors }); return; } const tierErrors: RangeTierError[] = new Array(tiers.length).fill(null).map(() => ({})); let hasError = false; - - const t = G7Core.t; + const isDiscrete = isDiscreteRangePolicy(chargePolicy); + const isUnlimited = (max: number | null | undefined) => max === null || max === undefined; for (let i = 0; i < tiers.length; i++) { const tier = tiers[i]; + const isLast = i === tiers.length - 1; // 첫 구간 min은 0이어야 함 - if (i === 0 && tier.min !== 0) { - tierErrors[i].min = t?.('sirsoft-ecommerce.validation.shipping_policy.ranges.first_min_zero') + if (i === 0 && Number(tier.min ?? 0) !== 0) { + tierErrors[i].min = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_first_min') ?? '첫 구간의 시작값은 0이어야 합니다.'; hasError = true; } - // 마지막 구간 max는 null이어야 함 - if (i === tiers.length - 1 && tier.max !== null && tier.max !== undefined) { - tierErrors[i].max = t?.('sirsoft-ecommerce.validation.shipping_policy.ranges.last_max_unlimited') + // 마지막 구간 max는 비어 있어야 함 (무제한) + if (isLast && !isUnlimited(tier.max)) { + tierErrors[i].max = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_last_max') ?? '마지막 구간의 종료값은 비워야 합니다.'; hasError = true; + continue; } - // min < max (마지막 구간 제외) - if (i < tiers.length - 1 && tier.max !== null && tier.max !== undefined) { - if (tier.min >= tier.max) { - tierErrors[i].min = t?.('sirsoft-ecommerce.validation.shipping_policy.ranges.min_less_than_max') + if (isLast) { + // 마지막 구간은 아래 종료값 기반 검사 대상이 아니다 + if (tier.fee === null || tier.fee === undefined) { + tierErrors[i].fee = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee_required') + ?? '구간 배송비를 입력해주세요.'; + hasError = true; + } else if (tier.fee < 0) { + tierErrors[i].fee = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee') + ?? '배송비는 0 이상이어야 합니다.'; + hasError = true; + } + continue; + } + + // 중간 구간의 종료값 누락 금지 (뒤 구간이 영구히 도달 불가해진다) + if (isUnlimited(tier.max)) { + tierErrors[i].max = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_middle_max') + ?? '마지막 구간을 제외한 구간에는 종료값을 입력해야 합니다.'; + hasError = true; + } else { + const max = Number(tier.max); + + // 수량 구간의 경계값은 정수 + if (isDiscrete && !Number.isInteger(max)) { + tierErrors[i].max = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_integer') + ?? '수량 구간의 시작값과 종료값은 정수여야 합니다.'; + hasError = true; + } + + // min < max + if (Number(tier.min ?? 0) >= max) { + tierErrors[i].min = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_min_max') ?? '시작값이 종료값보다 작아야 합니다.'; hasError = true; } - } - // 구간 연속성: 현재 max + 1 === 다음 min (포함 범위 기준) - if (i < tiers.length - 1) { - const nextTier = tiers[i + 1]; - if (tier.max !== null && tier.max !== undefined && (tier.max + 1) !== nextTier.min) { - tierErrors[i].max = t?.('sirsoft-ecommerce.validation.shipping_policy.ranges.continuity') + // 구간 연속성 (이산형은 max + 1, 연속형은 max 가 다음 min) + const expectedNextMin = isDiscrete ? max + 1 : max; + const actualNextMin = Number(tiers[i + 1]?.min ?? 0); + if (Math.abs(expectedNextMin - actualNextMin) >= CONTINUITY_EPSILON) { + tierErrors[i].max = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_continuity') ?? '구간이 연속적이지 않습니다.'; hasError = true; } } - // fee >= 0 - if (tier.fee < 0) { - tierErrors[i].fee = t?.('sirsoft-ecommerce.validation.shipping_policy.ranges.fee_non_negative') + // 배송비 필수 + 0 이상 + if (tier.fee === null || tier.fee === undefined) { + tierErrors[i].fee = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee_required') + ?? '구간 배송비를 입력해주세요.'; + hasError = true; + } else if (tier.fee < 0) { + tierErrors[i].fee = t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_fee') ?? '배송비는 0 이상이어야 합니다.'; hasError = true; } @@ -753,6 +884,66 @@ function validateRangeTiersInternal( updateLocalState(context, { rangeErrors }); } +/** + * 폼 레벨 오류 키를 만듭니다. + * + * 서버 422 응답의 키 형태(`country_settings.{index}.{field}`)와 동일해야 레이아웃의 기존 + * 오류 표시가 클라이언트 검증 결과에도 그대로 쓰인다. + * + * @param countryIndex 국가 탭 인덱스 + * @param field 필드 경로 + * @returns 오류 맵 키 + */ +function chargeSettingErrorKey(countryIndex: number, field: string): string { + return `country_settings.${countryIndex}.${field}`; +} + +/** + * 부과정책이 요구하는 폼 레벨 필수값(단위값 / 무료배송 기준금액)을 선제 검증합니다. + * + * 서버(StoreShippingPolicyRequest::validatePolicyRequiredSettings)와 같은 규칙을 화면에서 + * 먼저 적용해, 저장을 눌러 422 를 받기 전에 빈 값이 드러나게 한다. 이 두 값이 비면 배송비가 + * 계산되지 않아 조용히 0원이 되므로 서버도 저장을 거부한다. + * + * 오류 없음은 `null` 로 기록한다 — deepMerge 로는 키를 제거할 수 없고, 빈 배열은 레이아웃의 + * `if` 조건에서 truthy 라 빈 오류 문구가 남는다. + * + * @param G7Core G7Core 전역 객체 + * @param context 액션 컨텍스트 + * @param countryIndex 국가 탭 인덱스 + * @param cs 검증 대상 국가별 설정 + */ +function validateChargeSettingRequirements( + G7Core: any, + context: ActionContext, + countryIndex: number, + cs: CountrySetting +): void { + const localState = G7Core.state.getLocal?.() ?? {}; + const errors: Record<string, any> = { ...(localState.errors ?? {}) }; + const t = G7Core.t; + + const unitValue = cs.ranges?.unit_value; + const unitValueMissing = REQUIRES_UNIT_VALUE.includes(cs.charge_policy) + && (unitValue === null || unitValue === undefined || Number(unitValue) <= 0); + + errors[chargeSettingErrorKey(countryIndex, 'ranges.unit_value')] = unitValueMissing + ? [t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_unit_value_required') + ?? '단위당 배송비 정책은 단위값을 입력해야 합니다.'] + : null; + + const freeThreshold = cs.free_threshold; + const freeThresholdMissing = REQUIRES_FREE_THRESHOLD.includes(cs.charge_policy) + && (freeThreshold === null || freeThreshold === undefined || String(freeThreshold) === ''); + + errors[chargeSettingErrorKey(countryIndex, 'free_threshold')] = freeThresholdMissing + ? [t?.('sirsoft-ecommerce.admin.shipping_policy.form.range_error_free_threshold_required') + ?? '조건부 무료배송 정책은 무료배송 기준금액을 입력해야 합니다.'] + : null; + + updateLocalState(context, { errors }); +} + /** * 도서산간 추가배송비 행을 추가합니다. * @@ -896,7 +1087,11 @@ export function updateUnitValueHandler( context: ActionContext ): void { const params = action.params || {}; - const value = parseFloat(params.value as string) || 1; + const raw = params.value; + // 빈 값은 null 로 보존한다 — 1 로 강제하면 미입력이 조용히 유효값이 되어 서버 필수 검증을 우회한다. + const isEmpty = raw === '' || raw === null || raw === undefined; + const parsed = parseFloat(raw as string); + const value: number | null = isEmpty || Number.isNaN(parsed) ? null : parsed; const G7Core = getG7Core(); if (!G7Core) return; @@ -916,6 +1111,10 @@ export function updateUnitValueHandler( updateLocalState(context, { 'form.country_settings': countrySettings, }); + + // 단위값이 비거나 0 이하이면 배송비가 계산되지 않으므로 저장 전에 화면에서 먼저 알린다 + validateChargeSettingRequirements(G7Core, context, countryIndex, countrySettings[countryIndex]); + logger.log('[updateUnitValue] Updated unit_value:', value); } diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json index d7d44b99..6ce90069 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/settings.json @@ -59,6 +59,13 @@ "telecom_number_placeholder": "Telecom business number", "postcode_plugin_required": "Daum Postcode plugin is not installed or inactive. Please activate it in Admin > Plugins." }, + "public_asset_storage": { + "title": "Public Asset Storage", + "description": "Configure the storage disk for this module's fully public assets such as product, category, and review images.", + "disk": "Public Asset Disk", + "follow_core": "Follow core setting", + "help": "Select to use a different disk for this module only. Leave empty to follow the public asset storage setting in core system settings." + }, "inquiry_settings": { "title": "1:1 Inquiry Board Settings", "description": "Configure the product 1:1 inquiry feature.", @@ -145,6 +152,7 @@ "min_order_amount": "Min. Order Amount", "is_active": "Active", "orphaned_badge": "Plugin Not Installed", + "orphaned_pg_badge": "PG removed", "remove_orphaned": "Remove", "pg_provider": "PG Provider", "pg_use_default": "Default PG", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/shipping_policy.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/shipping_policy.json index f8a49885..cd1d29b7 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/shipping_policy.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/en/admin/shipping_policy.json @@ -121,12 +121,6 @@ "free": "Free Shipping", "fixed": "Shipping: {{fee}}", "conditional": "Under {{threshold}}: {{fee}} / {{threshold}}+: Free", - "range_prefix": { - "amount": "", - "quantity": "pcs", - "weight": "kg", - "volume": "cm³" - }, "api": "External API (Real-time calculation)", "per_quantity": "{{fee}} per {{unit}} items", "per_weight": "{{fee}} per {{unit}}kg", @@ -213,7 +207,11 @@ "free_threshold_hint": "Free shipping for orders at or above this amount", "range_tiers": "Range-based Fees", "range_min": "Min", + "range_min_auto": "Auto", "range_max": "Max", + "unit_quantity": "items", + "unit_weight": "kg", + "unit_volume": "L", "range_fee": "Fee", "range_unlimited": "Unlimited", "range_add": "Add Range", @@ -223,6 +221,11 @@ "range_error_min_max": "Min value must be less than max value.", "range_error_fee": "Fee must be 0 or greater.", "range_error_min_one": "At least one range is required.", + "range_error_middle_max": "Every range except the last one must have an end value.", + "range_error_fee_required": "Please enter the shipping fee for this range.", + "range_error_integer": "Quantity range start and end values must be integers.", + "range_error_unit_value_required": "A per-unit charge policy requires a unit value.", + "range_error_free_threshold_required": "A conditional free shipping policy requires a free shipping threshold.", "per_unit_value": "Unit Value", "per_unit_hint": "The shipping fee above will be charged per unit", "api_endpoint": "API Endpoint", @@ -252,8 +255,8 @@ "api_test_response": "Response (status :status):", "api_test_endpoint_required": "Enter the API endpoint first to run a test.", "extra_fee_enabled": "Enable Remote Area Fee", - "extra_fee_multiply": "Multiply Per Unit", - "extra_fee_multiply_hint": "Apply remote area fee per unit when using per-unit shipping", + "extra_fee_multiply": "Multiply Per Shipping Unit", + "extra_fee_multiply_hint": "Charge the remote area fee by the same number of shipping units used for the base fee", "extra_fee_zipcode": "Zipcode", "extra_fee_zipcode_placeholder": "63000 or 63000-63644", "extra_fee_amount": "Extra Fee", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json index e792c594..1f2a64f7 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/settings.json @@ -59,6 +59,13 @@ "telecom_number_placeholder": "부가통신 사업자번호", "postcode_plugin_required": "다음 우편번호 플러그인이 설치되지 않았거나 비활성화 상태입니다. 관리자 > 플러그인에서 활성화해주세요." }, + "public_asset_storage": { + "title": "공개 자산 스토리지", + "description": "상품·카테고리·리뷰 이미지 등 이 모듈의 완전 공개 자산 저장 디스크를 설정합니다.", + "disk": "공개 자산 디스크", + "follow_core": "코어 설정 따름", + "help": "이 모듈만 다른 디스크를 쓰려면 선택합니다. 비우면 코어 환경설정의 공개 자산 스토리지 설정을 따릅니다." + }, "inquiry_settings": { "title": "1:1 문의게시판 설정", "description": "상품 1:1 문의 기능을 설정합니다.", @@ -145,6 +152,7 @@ "min_order_amount": "최소 주문금액", "is_active": "사용여부", "orphaned_badge": "플러그인 미설치", + "orphaned_pg_badge": "지정 PG 삭제됨", "remove_orphaned": "삭제", "pg_provider": "PG사", "pg_use_default": "기본 PG", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/shipping_policy.json b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/shipping_policy.json index 5c340003..f0958b71 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/shipping_policy.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/lang/partial/ko/admin/shipping_policy.json @@ -121,12 +121,6 @@ "free": "무료배송", "fixed": "배송비: {{fee}}", "conditional": "{{threshold}} 미만 {{fee}} / {{threshold}} 이상 무료", - "range_prefix": { - "amount": "", - "quantity": "개", - "weight": "kg", - "volume": "cm³" - }, "api": "외부 API 연동 (실시간 계산)", "per_quantity": "{{unit}}개당 {{fee}}", "per_weight": "{{unit}}kg당 {{fee}}", @@ -213,7 +207,11 @@ "free_threshold_hint": "이 금액 이상 구매 시 배송비 무료", "range_tiers": "구간별 배송비", "range_min": "시작값", + "range_min_auto": "자동", "range_max": "종료값", + "unit_quantity": "개", + "unit_weight": "kg", + "unit_volume": "L", "range_fee": "배송비", "range_unlimited": "무제한", "range_add": "구간 추가", @@ -223,6 +221,11 @@ "range_error_min_max": "시작값은 종료값보다 작아야 합니다.", "range_error_fee": "배송비는 0 이상이어야 합니다.", "range_error_min_one": "최소 1개 구간이 필요합니다.", + "range_error_middle_max": "마지막 구간을 제외한 구간에는 종료값을 입력해야 합니다.", + "range_error_fee_required": "구간 배송비를 입력해주세요.", + "range_error_integer": "수량 구간의 시작값과 종료값은 정수여야 합니다.", + "range_error_unit_value_required": "단위당 배송비 정책은 단위값을 입력해야 합니다.", + "range_error_free_threshold_required": "조건부 무료배송 정책은 무료배송 기준금액을 입력해야 합니다.", "per_unit_value": "단위 기준값", "per_unit_hint": "설정한 단위마다 위 배송비가 부과됩니다", "api_endpoint": "API 엔드포인트", @@ -252,8 +255,8 @@ "api_test_response": "응답 (상태 :status):", "api_test_endpoint_required": "테스트하려면 먼저 API 엔드포인트를 입력하세요.", "extra_fee_enabled": "도서산간 추가배송비 사용", - "extra_fee_multiply": "건당 중복 부과", - "extra_fee_multiply_hint": "단위당 배송비 적용 시 도서산간 추가배송비를 건당 중복 부과합니다", + "extra_fee_multiply": "배송 단위당 중복 부과", + "extra_fee_multiply_hint": "단위당 배송비 적용 시 도서산간 추가배송비를 기본 배송비와 같은 배송 단위 수만큼 중복 부과합니다", "extra_fee_zipcode": "우편번호", "extra_fee_zipcode_placeholder": "63000 또는 63000-63644", "extra_fee_amount": "추가배송비", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_product_review_index.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_product_review_index.json index fd68d536..93f671fd 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_product_review_index.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_product_review_index.json @@ -1590,6 +1590,7 @@ "data": "{{reviews.data?.data ?? []}}", "idField": "id", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_local.selectedReviews ?? []}}", "expandable": true, "expandedRowIds": "{{_local.expandedRows ?? []}}", @@ -2482,7 +2483,7 @@ "text": "$t:sirsoft-ecommerce.admin.product_review.detail.seller_reply.title" }, { - "type": "basic", + "type": "composite", "name": "Badge", "props": { "color": "{{row.has_reply_badge_color}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_settings.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_settings.json index 1617bdec..3a01b657 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_settings.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_settings.json @@ -590,7 +590,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.settings.save_error" + "message": "{{error.message ?? $t('sirsoft-ecommerce.admin.settings.save_error')}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_shipping_policy_form.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_shipping_policy_form.json index 9a30286f..5869804d 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_shipping_policy_form.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/admin_ecommerce_shipping_policy_form.json @@ -22,6 +22,8 @@ "apiAuthTypeOptions": "{{ecommerce_settings?.data?.shipping?.api_auth_types ?? []}}", "apiResponseTypeOptions": "{{ecommerce_settings?.data?.shipping?.api_response_types ?? []}}", "activeCountrySetting": "{{(_local.form?.country_settings ?? [])[_local.activeCountryTab ?? 0]}}", + "rangeUnitLabel": "{{(() => { const p = (_local.form?.country_settings ?? [])[_local.activeCountryTab ?? 0]?.charge_policy; if (p === 'range_quantity') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_quantity'); if (p === 'range_weight' || p === 'range_volume_weight') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_weight'); if (p === 'range_volume') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_volume'); if (p === 'range_amount') return ecommerce_settings?.data?.language_currency?.default_currency ?? ''; return ''; })()}}", + "perUnitLabel": "{{(() => { const p = (_local.form?.country_settings ?? [])[_local.activeCountryTab ?? 0]?.charge_policy; if (p === 'per_quantity') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_quantity'); if (p === 'per_weight' || p === 'per_volume_weight') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_weight'); if (p === 'per_volume') return $t('sirsoft-ecommerce.admin.shipping_policy.form.unit_volume'); if (p === 'per_amount') return ecommerce_settings?.data?.language_currency?.default_currency ?? ''; return ''; })()}}", "activeCountryCode": "{{(_local.form?.country_settings ?? [])[_local.activeCountryTab ?? 0]?.country_code ?? ''}}", "isReadOnly": "{{!!route.id && policy?.data?.abilities?.can_update !== true}}" }, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_category_list.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_category_list.json index c81e35c9..9f553715 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_category_list.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_category_list.json @@ -30,7 +30,7 @@ "name": "Icon", "props": { "name": "folder", - "className": "w-5 h-5 text-gray-700 dark:text-gray-300" + "className": "text-xl text-gray-700 dark:text-gray-300" } }, { @@ -139,7 +139,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.category.messages.order_update_failed" + "message": "{{error.message ?? '$t:sirsoft-ecommerce.admin.category.messages.order_update_failed'}}" } }, { @@ -220,7 +220,7 @@ "name": "Icon", "props": { "name": "folder", - "className": "w-5 h-5 text-gray-400 dark:text-gray-500" + "className": "text-xl text-gray-400 dark:text-gray-500" } }, { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_form.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_form.json index bc330b86..5cb56598 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_form.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_category_index/_panel_form.json @@ -90,7 +90,7 @@ "if": "{{!_local.isSaving}}", "props": { "name": "save", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -141,7 +141,7 @@ "name": "Icon", "props": { "name": "triangle-exclamation", - "className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" } }, { @@ -567,7 +567,7 @@ "handler": "setState", "params": { "target": "local", - "form.images": "{{(_local.form?.images ?? []).filter(img => img.id !== $args[0])}}" + "form.images": "{{(_local.form?.images ?? []).filter(img => (img.hash || img.id) !== $args[0])}}" } } ] @@ -627,7 +627,7 @@ "props": { "name": "parent_id", "placeholder": "$t:sirsoft-ecommerce.admin.category.form.parent_category_placeholder", - "options": "{{(() => { const result = []; const flatten = (items, path = [], depth = 0) => { if (!items || !Array.isArray(items) || items.length === 0) return; for (const item of items) { if (!item || !item.id) continue; const currentPath = [...path, $localized(item.name)]; result.push({ value: item.id, label: currentPath.join(' > '), id: item.id }); if (item.children && Array.isArray(item.children) && item.children.length > 0 && depth < 2) { flatten(item.children, currentPath, depth + 1); } } }; flatten(categories?.data?.data); const filtered = result.filter(c => c && c.id && (_global.panelMode === 'create' || c.id !== _global.selectedCategoryId)); return [{ value: '', label: $t('sirsoft-ecommerce.admin.category.form.parent_top_level') }, ...filtered]; })()}}", + "options": "{{(() => { const result = []; const flatten = (items, path = [], depth = 0) => { if (!items || !Array.isArray(items) || items.length === 0) return; for (const item of items) { if (!item || !item.id) continue; const currentPath = [...path, $localized(item.name)]; result.push({ value: item.id, label: currentPath.join(' > '), id: item.id }); if (item.children && Array.isArray(item.children) && item.children.length > 0 && depth < 2) { flatten(item.children, currentPath, depth + 1); } } }; flatten(categories?.data?.data); const filtered = result.filter(c => c && c.id && (_global.panelMode === 'create' || c.id !== _global.selectedCategoryId)); return [{ value: '', label: $t('sirsoft-ecommerce.admin.category.form.parent_top_level') }, ...filtered]; })() ?? []}}", "isMulti": false, "isClearable": false, "isSearchable": true, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_excel_download_index/_modal_download_history.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_excel_download_index/_modal_download_history.json index 6586994e..262ae060 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_excel_download_index/_modal_download_history.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_excel_download_index/_modal_download_history.json @@ -41,7 +41,7 @@ "props": { "className": "text-primary text-right" }, - "text": "{{_local.selectedItem.file_content}}" + "text": "{{_local.selectedItem?.file_content ?? ''}}" }, { "type": "basic", @@ -57,7 +57,7 @@ "props": { "className": "text-primary text-right" }, - "text": "{{_local.selectedItem.category_name}}" + "text": "{{_local.selectedItem?.category_name ?? ''}}" }, { "type": "basic", @@ -77,7 +77,7 @@ { "type": "basic", "name": "Span", - "condition": "{{_local.selectedItem.status === 'completed'}}", + "condition": "{{_local.selectedItem?.status === 'completed'}}", "props": { "className": "badge-status badge-status-success" }, @@ -87,7 +87,7 @@ "name": "Icon", "props": { "name": "check-circle", - "className": "w-3 h-3" + "className": "text-xs" } }, { @@ -100,7 +100,7 @@ { "type": "basic", "name": "Span", - "condition": "{{_local.selectedItem.status === 'processing'}}", + "condition": "{{_local.selectedItem?.status === 'processing'}}", "props": { "className": "badge-status badge-status-processing" }, @@ -109,8 +109,8 @@ "type": "basic", "name": "Icon", "props": { - "name": "loader", - "className": "w-3 h-3 animate-spin" + "name": "spinner", + "className": "text-xs animate-spin" } }, { @@ -123,7 +123,7 @@ { "type": "basic", "name": "Span", - "condition": "{{_local.selectedItem.status === 'failed'}}", + "condition": "{{_local.selectedItem?.status === 'failed'}}", "props": { "className": "badge-status badge-status-error" }, @@ -132,8 +132,8 @@ "type": "basic", "name": "Icon", "props": { - "name": "alert-circle", - "className": "w-3 h-3" + "name": "circle-exclamation", + "className": "text-xs" } }, { @@ -146,7 +146,7 @@ { "type": "basic", "name": "Span", - "condition": "{{_local.selectedItem.status === 'expired'}}", + "condition": "{{_local.selectedItem?.status === 'expired'}}", "props": { "className": "badge-status badge-status-expired" }, @@ -155,8 +155,8 @@ "type": "basic", "name": "Icon", "props": { - "name": "file-text", - "className": "w-3 h-3" + "name": "file-lines", + "className": "text-xs" } }, { @@ -182,7 +182,7 @@ "props": { "className": "text-primary text-right" }, - "text": "{{_local.selectedItem.created_date}}" + "text": "{{_local.selectedItem?.created_date ?? ''}}" }, { "type": "basic", @@ -198,7 +198,7 @@ "props": { "className": "text-primary text-right" }, - "text": "{{_local.selectedItem.expire_date}}" + "text": "{{_local.selectedItem?.expire_date ?? ''}}" }, { "type": "basic", @@ -214,7 +214,7 @@ "props": { "className": "text-primary text-right font-medium" }, - "text": "$t:sirsoft-ecommerce.admin.excel_download.modal.download_history.download_count|count={{_local.selectedItem.download_count}}" + "text": "$t:sirsoft-ecommerce.admin.excel_download.modal.download_history.download_count|count={{_local.selectedItem?.download_count ?? 0}}" } ] } @@ -259,10 +259,14 @@ }, "children": [ { - "id": "history_item", + "id": "history_item_{{index}}", "type": "basic", "name": "Div", - "iteration": "{{_local.selectedItem?.download_history ?? []}}", + "iteration": { + "source": "{{_local.selectedItem?.download_history ?? []}}", + "item_var": "item", + "index_var": "index" + }, "props": { "className": "admin-card mb-3" }, @@ -286,7 +290,7 @@ "name": "Icon", "props": { "name": "user", - "className": "w-4 h-4" + "className": "text-base" } } ] @@ -329,7 +333,7 @@ "name": "Icon", "props": { "name": "calendar", - "className": "w-3 h-3" + "className": "text-xs" } }, { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_mileage_transaction_index/_transactions_table.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_mileage_transaction_index/_transactions_table.json index c1d797fd..12ede5e0 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_mileage_transaction_index/_transactions_table.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_mileage_transaction_index/_transactions_table.json @@ -18,6 +18,7 @@ "data": "{{transactions?.data?.data || []}}", "loading": "{{transactions?.loading}}", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_local.selectedItems || []}}", "expandable": true, "expandedRowIds": "{{_local.expandedRows || []}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json index a2ca5bd1..d77c3af4 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_order_info.json @@ -271,6 +271,7 @@ "data": "{{order.data?.options ?? []}}", "idField": "id", "selectable": true, + "selectionScope": "free", "selectedIds": "{{_local.selectedProducts || []}}", "pagination": false, "showColumnSelector": false, @@ -2283,7 +2284,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.order.detail.order_info.save_error" + "message": "{{error.message ?? '$t:sirsoft-ecommerce.admin.order.detail.order_info.save_error'}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_payment_info.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_payment_info.json index f676cfdf..9a7d2577 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_payment_info.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_detail/_partial_payment_info.json @@ -743,10 +743,10 @@ }, { "id": "payment_cash_receipt_card_{{payIdx}}", - "comment": "현금영수증 카드 (W-3 / #454). 무통장이 아니거나 프로바이더 미설정이면 카드 자체를 렌더하지 않는다. 카드 바깥의 기존 결제정보 본문은 .grid-2col-responsive 등 CSS 유틸을 쓰지만, 신설 카드는 responsive.portable 로 작성한다(액션 버튼의 마크업 구조가 바뀌므로 Tailwind breakpoint 로 처리 불가 — 편집기 디바이스 미리보기가 overrideWidth 를 무시한다).", + "comment": "현금영수증 카드 (W-3 / #454). 무통장이 아니면 카드 자체를 렌더하지 않는다. 카드 바깥의 기존 결제정보 본문은 .grid-2col-responsive 등 CSS 유틸을 쓰지만, 신설 카드는 responsive.portable 로 작성한다(액션 버튼의 마크업 구조가 바뀌므로 Tailwind breakpoint 로 처리 불가 — 편집기 디바이스 미리보기가 overrideWidth 를 무시한다). 프로바이더 미설정이어도 이미 발급/시도된 이력이 있으면 계속 표시한다 — 제공 확장이 사라졌다고 과거 발급 내역까지 화면에서 사라지면 안 된다(A3).", "type": "basic", "name": "Div", - "if": "{{payment?.payment_method === 'dbank' && !!payment?.cash_receipt_provider}}", + "if": "{{payment?.payment_method === 'dbank' && (!!payment?.cash_receipt_provider || !!order.data?.cash_receipt || (order.data?.cash_receipts ?? []).length > 0)}}", "props": { "className": "mt-4 pt-4 border-t border-gray-200 dark:border-gray-700" }, @@ -1077,13 +1077,13 @@ "if": "{{payment?.payment_status === 'paid' && !order.data?.cash_receipt && String((order.data?.cash_receipts ?? [])[0]?.issue_status ?? '').toLowerCase() !== 'failed'}}", "props": { "type": "button", - "className": "px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white" + "className": "px-3 py-1.5 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white" }, "responsive": { "portable": { "props": { "type": "button", - "className": "w-full px-3 py-2 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 text-white" + "className": "w-full px-3 py-2 text-sm rounded-lg bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white" } } }, @@ -1118,13 +1118,13 @@ "if": "{{!order.data?.cash_receipt && (order.data?.cash_receipts ?? []).length > 0 && String((order.data?.cash_receipts ?? [])[0]?.issue_status ?? '').toLowerCase() === 'failed'}}", "props": { "type": "button", - "className": "px-3 py-1.5 text-sm rounded-lg bg-amber-600 hover:bg-amber-700 text-white" + "className": "px-3 py-1.5 text-sm rounded-lg bg-amber-600 hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600 text-white" }, "responsive": { "portable": { "props": { "type": "button", - "className": "w-full px-3 py-2 text-sm rounded-lg bg-amber-600 hover:bg-amber-700 text-white" + "className": "w-full px-3 py-2 text-sm rounded-lg bg-amber-600 hover:bg-amber-700 dark:bg-amber-500 dark:hover:bg-amber-600 text-white" } } }, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_list/_partial_order_datagrid.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_list/_partial_order_datagrid.json index b6b07b15..0bedef17 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_list/_partial_order_datagrid.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_order_list/_partial_order_datagrid.json @@ -730,6 +730,7 @@ "data": "{{orders?.data?.data || []}}", "loading": "{{orders?.loading}}", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_local.selectedItems || []}}", "showColumnSelector": false, "selectedCountText": "", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_product_list/_partial_product_datagrid.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_product_list/_partial_product_datagrid.json index 0e692e63..f67ec8ce 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_product_list/_partial_product_datagrid.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_product_list/_partial_product_datagrid.json @@ -551,6 +551,7 @@ "idField": "id", "sortable": true, "selectable": true, + "selectionScope": "page", "selectedIds": "{{_local.selectedItems || []}}", "expandable": true, "expandedRowIds": "{{_local.expandedRows || []}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_form/_partial_usage_conditions.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_form/_partial_usage_conditions.json index 87e58a8a..687ae3df 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_form/_partial_usage_conditions.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_form/_partial_usage_conditions.json @@ -32,11 +32,14 @@ }, "children": [ { + "comment": "boolean 필드는 자동바인딩 금지 — e.target.value 문자열이 그대로 저장되어 서버 boolean 규칙에서 422 (공개 #97). 표시는 String() 변환, 쓰기는 change 액션의 === 'true' 캐스팅", "type": "composite", "name": "RadioGroup", "props": { "name": "is_combinable", "inline": true, + "autoBinding": false, + "value": "{{String(_local.form?.is_combinable ?? true)}}", "options": [ { "label": "$t:sirsoft-ecommerce.admin.promotion_coupon.form.option.combinable_yes", @@ -48,6 +51,16 @@ } ] }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "form.is_combinable": "{{$event.target.value === 'true'}}" + } + } + ], "responsive": { "mobile": { "props": { @@ -369,6 +382,7 @@ } ], "selectable": true, + "selectionScope": "free", "selectedIds": "{{_local.productSearchSelected ?? []}}", "emptyMessage": "$t:sirsoft-ecommerce.admin.promotion_coupon.form.search.no_results" }, @@ -519,6 +533,7 @@ } ], "selectable": true, + "selectionScope": "free", "selectedIds": "{{_local.selectedProductIds ?? []}}", "emptyMessage": "$t:sirsoft-ecommerce.admin.promotion_coupon.form.search.no_selected" }, @@ -778,6 +793,7 @@ } ], "selectable": true, + "selectionScope": "free", "selectedIds": "{{_local.selectedCategoryIds ?? []}}", "emptyMessage": "$t:sirsoft-ecommerce.admin.promotion_coupon.form.search.no_selected" }, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_direct_issue.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_direct_issue.json index 6ae1e8ea..4e35b4d5 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_direct_issue.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_direct_issue.json @@ -76,7 +76,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.promotion_coupon.modal.direct_issue.search_failed" + "message": "{{error.message ?? '$t:sirsoft-ecommerce.admin.promotion_coupon.modal.direct_issue.search_failed'}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_status_change_confirm.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_status_change_confirm.json index e684af54..8c2b5e4a 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_status_change_confirm.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_modal_status_change_confirm.json @@ -61,7 +61,7 @@ "type": "basic", "name": "Button", "props": { - "className": "flex-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", + "className": "flex-center gap-2 px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed", "disabled": "{{_global.isChangingStatus}}" }, "actions": [ @@ -126,7 +126,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.common.error.general" + "message": "{{error.message ?? '$t:sirsoft-ecommerce.common.error.general'}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_partial_coupon_datagrid.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_partial_coupon_datagrid.json index 44296856..a0f8c962 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_partial_coupon_datagrid.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_promotion_coupon_list/_partial_coupon_datagrid.json @@ -237,6 +237,7 @@ "pageSize": "{{+(query.per_page || 10)}}", "alwaysShowPagination": true, "selectable": true, + "selectionScope": "page", "selectedIds": "{{_local.selectedItems || []}}", "visibleColumns": "{{_local.visibleColumns}}", "selectedCountText": "", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_bank_management_modal.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_bank_management_modal.json index 0b2bd420..a7a20edd 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_bank_management_modal.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_bank_management_modal.json @@ -460,7 +460,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.settings.save_error" + "message": "{{error.message ?? $t('sirsoft-ecommerce.admin.settings.save_error')}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_clear_seo_cache.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_clear_seo_cache.json index 58a95550..1c0ec368 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_clear_seo_cache.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_clear_seo_cache.json @@ -172,7 +172,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-ecommerce.admin.settings.seo.seo_friendly.clear_cache_failed" + "message": "{{error.message ?? $t('sirsoft-ecommerce.admin.settings.seo.seo_friendly.clear_cache_failed')}}" } } ] diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_mail_template_edit.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_mail_template_edit.json index aed2666e..cab4f6a5 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_mail_template_edit.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_mail_template_edit.json @@ -626,7 +626,7 @@ { "handler": "toast", "params": { - "message": "$t:sirsoft-ecommerce.admin.settings.mail_templates.save_error", + "message": "{{error.message ?? $t('sirsoft-ecommerce.admin.settings.mail_templates.save_error')}}", "type": "error" } } diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_notification_template_edit.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_notification_template_edit.json index c8e84953..bab7fc2f 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_notification_template_edit.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_modal_notification_template_edit.json @@ -969,7 +969,7 @@ { "handler": "toast", "params": { - "message": "$t:sirsoft-ecommerce.admin.settings.notification_definitions.save_error", + "message": "{{error.message ?? $t('sirsoft-ecommerce.admin.settings.notification_definitions.save_error')}}", "type": "error" } } diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_cards.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_cards.json index 51465cd8..d0964753 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_cards.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_cards.json @@ -94,9 +94,19 @@ "name": "Span", "if": "{{$method._orphaned}}", "props": { - "className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" + "className": "inline-flex items-center whitespace-nowrap shrink-0 px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" }, "text": "$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_badge" + }, + { + "type": "basic", + "name": "Span", + "if": "{{$method._orphaned_pg}}", + "props": { + "data-testid": "orphaned-pg-badge-{{$method.id}}", + "className": "inline-flex items-center whitespace-nowrap shrink-0 px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_pg_badge" } ] }, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_list.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_list.json index 308a7764..1e03d40a 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_list.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_payment_methods_list.json @@ -76,15 +76,16 @@ { "type": "basic", "name": "Div", + "comment": "이름 + 상태 배지 줄. 배지는 shrink-0 이라 좁아지면 이름이 줄어야 하는데, min-w-0 이 없으면 flex 항목의 기본 min-width:auto 때문에 줄지 못하고 글자 단위로 접힌다.", "props": { - "className": "flex-center gap-2" + "className": "flex-center gap-2 min-w-0" }, "children": [ { "type": "basic", "name": "Span", "props": { - "className": "font-medium text-gray-900 dark:text-gray-100" + "className": "font-medium text-gray-900 dark:text-gray-100 truncate" }, "text": "{{$localized($method._cached_name, 'sirsoft-ecommerce::settings.payment_methods.' + ($method.id ?? '') + '.name') || $method.id}}" }, @@ -93,9 +94,20 @@ "name": "Span", "if": "{{$method._orphaned}}", "props": { - "className": "inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" + "className": "inline-flex items-center whitespace-nowrap shrink-0 px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400" }, "text": "$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_badge" + }, + { + "comment": "지정 PG 가 레지스트리에서 사라진 상태. _orphaned 와 달리 행 편집 컨트롤은 그대로 둔다 — 살아있는 PG 로 바꿔 복구하는 경로를 막지 않기 위함.", + "type": "basic", + "name": "Span", + "if": "{{$method._orphaned_pg}}", + "props": { + "data-testid": "orphaned-pg-badge-{{$method.id}}", + "className": "inline-flex items-center whitespace-nowrap shrink-0 px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.order_settings.payment_methods.orphaned_pg_badge" } ] }, diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_cards.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_cards.json index 38822682..a9de4b1d 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_cards.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_cards.json @@ -10,7 +10,7 @@ }, "children": [ { - "id": "refund_reason_card", + "id": "refund_reason_card_{{rIdx}}", "type": "basic", "name": "Div", "iteration": { @@ -63,7 +63,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{reason.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_section.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_section.json index 7407de52..428d605a 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_section.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_refund_reason_section.json @@ -604,7 +604,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{reason.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_cards.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_cards.json index ab5b859a..0438acf9 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_cards.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_cards.json @@ -8,7 +8,7 @@ "name": "Div", "children": [ { - "id": "carrier_card", + "id": "carrier_card_{{$cIdx}}", "type": "basic", "name": "Div", "iteration": { @@ -69,7 +69,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{$carrier.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_section.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_section.json index 7683d4aa..d1da0235 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_section.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_carrier_section.json @@ -677,7 +677,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{$carrier.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_cards.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_cards.json index 5e15a721..57b46826 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_cards.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_cards.json @@ -8,7 +8,7 @@ "name": "Div", "children": [ { - "id": "country_card", + "id": "country_card_{{countryIndex}}", "type": "basic", "name": "Div", "iteration": { @@ -76,7 +76,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{country.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_table.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_table.json index aae81b89..e9326691 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_table.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_country_table.json @@ -108,7 +108,7 @@ "name": "Tbody", "children": [ { - "id": "country_row", + "id": "country_row_{{countryIndex}}", "type": "basic", "name": "Tr", "iteration": { @@ -166,7 +166,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{country.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_cards.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_cards.json index 4ba078bf..664708f9 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_cards.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_cards.json @@ -10,7 +10,7 @@ }, "children": [ { - "id": "shipping_type_card", + "id": "shipping_type_card_{{tIdx}}", "type": "basic", "name": "Div", "iteration": { @@ -63,7 +63,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{type.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_section.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_section.json index 81562972..53b9469e 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_section.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_shipping_type_section.json @@ -567,7 +567,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{$type.is_active ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_basic_info.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_basic_info.json index e2c39841..f9057863 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_basic_info.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_basic_info.json @@ -1055,6 +1055,89 @@ } ] }, + { + "id": "public_asset_storage_card", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card" + }, + "children": [ + { + "id": "public_asset_storage_title", + "type": "basic", + "name": "H3", + "props": { + "className": "card-title" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.basic_info.public_asset_storage.title" + }, + { + "id": "public_asset_storage_description", + "type": "basic", + "name": "Div", + "props": { + "className": "card-description" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.basic_info.public_asset_storage.description" + }, + { + "id": "field_public_asset_disk", + "type": "basic", + "name": "Div", + "props": { + "className": "form-group" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.basic_info.public_asset_storage.disk" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "w-full md:w-64" + }, + "children": [ + { + "type": "composite", + "name": "Select", + "props": { + "name": "basic_info.public_asset_disk", + "disabled": "{{_computed.isReadOnly}}", + "className": "w-full", + "error": "{{_local.errors?.['basic_info.public_asset_disk']?.[0] ?? ''}}", + "options": "{{[{value: '', label: $t('sirsoft-ecommerce.admin.settings.basic_info.public_asset_storage.follow_core')}].concat((_local.form?.available_public_asset_disks ?? []).map(d => ({value: d.id, label: $localized(d.label)})))}}" + } + }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.['basic_info.public_asset_disk']}}", + "props": { + "className": "form-error" + }, + "text": "{{_local.errors?.['basic_info.public_asset_disk']?.[0] ?? ''}}" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "text-sm text-gray-500 dark:text-gray-400 mt-2" + }, + "text": "$t:sirsoft-ecommerce.admin.settings.basic_info.public_asset_storage.help" + } + ] + } + ] + } + ] + }, { "id": "inquiry_settings_card", "type": "basic", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_identity_policies.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_identity_policies.json index f82bfecc..83439a80 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_identity_policies.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_identity_policies.json @@ -571,7 +571,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? $t('admin.identity.policies.toggle_failed')}}" } }, { @@ -908,7 +908,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? '$t:admin.identity.policies.toggle_failed'}}" } }, { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_shipping.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_shipping.json index 8a6cfc3b..be62378f 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_shipping.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_settings/_tab_shipping.json @@ -96,7 +96,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{_local.form?.shipping?.international_shipping_enabled ?? false}}", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_charge_settings.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_charge_settings.json index 19355439..1686ed7b 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_charge_settings.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_charge_settings.json @@ -259,26 +259,53 @@ }, { "type": "basic", - "name": "Input", + "name": "Div", "props": { - "type": "number", - "className": "input", - "value": "{{_computed.activeCountrySetting?.ranges?.unit_value ?? 1}}", - "min": 0.01, - "step": 0.01, - "disabled": "{{_computed.isReadOnly}}" + "className": "flex items-center gap-2" }, - "actions": [ + "children": [ { - "type": "change", - "handler": "sirsoft-ecommerce.updateUnitValue", - "debounce": 300, - "params": { - "value": "{{$event.target.value}}" - } + "type": "basic", + "name": "Input", + "props": { + "type": "number", + "className": "{{_local.errors?.['country_settings.' + (_local.activeCountryTab ?? 0) + '.ranges.unit_value'] ? 'input input-error' : 'input'}}", + "value": "{{_computed.activeCountrySetting?.ranges?.unit_value ?? ''}}", + "min": 0.01, + "step": 0.01, + "disabled": "{{_computed.isReadOnly}}" + }, + "actions": [ + { + "type": "change", + "handler": "sirsoft-ecommerce.updateUnitValue", + "debounce": 300, + "params": { + "value": "{{$event.target.value}}" + } + } + ] + }, + { + "type": "basic", + "name": "Span", + "if": "{{!!_computed.perUnitLabel}}", + "props": { + "className": "text-tertiary whitespace-nowrap" + }, + "text": "{{_computed.perUnitLabel ?? ''}}" } ] }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.['country_settings.' + (_local.activeCountryTab ?? 0) + '.ranges.unit_value']}}", + "props": { + "className": "form-error-xs" + }, + "text": "{{(_local.errors?.['country_settings.' + (_local.activeCountryTab ?? 0) + '.ranges.unit_value'] ?? [])[0] ?? ''}}" + }, { "type": "basic", "name": "Span", @@ -374,12 +401,12 @@ { "type": "basic", "name": "Th", - "text": "$t:sirsoft-ecommerce.admin.shipping_policy.form.range_min" + "text": "{{$t('sirsoft-ecommerce.admin.shipping_policy.form.range_min') + (_computed.rangeUnitLabel ? ' (' + _computed.rangeUnitLabel + ')' : '')}}" }, { "type": "basic", "name": "Th", - "text": "$t:sirsoft-ecommerce.admin.shipping_policy.form.range_max" + "text": "{{$t('sirsoft-ecommerce.admin.shipping_policy.form.range_max') + (_computed.rangeUnitLabel ? ' (' + _computed.rangeUnitLabel + ')' : '')}}" }, { "type": "basic", @@ -420,25 +447,13 @@ "name": "Input", "props": { "type": "number", - "className": "{{_local.rangeErrors?.[_computed.activeCountryCode]?.[tierIdx]?.min ? 'input input-error' : 'input'}}", - "value": "{{tier.min ?? 0}}", - "min": 0, - "placeholder": "0", + "className": "{{_local.rangeErrors?.[_computed.activeCountryCode]?.[tierIdx]?.min ? 'input input-error bg-gray-100 dark:bg-gray-700' : 'input bg-gray-100 dark:bg-gray-700'}}", + "value": "{{tier.min ?? ''}}", + "readOnly": true, + "tabIndex": -1, + "placeholder": "$t:sirsoft-ecommerce.admin.shipping_policy.form.range_min_auto", "disabled": "{{_computed.isReadOnly}}" - }, - "actions": [ - { - "type": "change", - "handler": "sirsoft-ecommerce.updateRangeTierField", - "debounce": 300, - "params": { - "countryIndex": "{{_local.activeCountryTab ?? 0}}", - "tierIndex": "{{tierIdx}}", - "field": "min", - "value": "{{parseFloat($event.target.value) || 0}}" - } - } - ] + } }, { "type": "basic", @@ -475,7 +490,7 @@ "countryIndex": "{{_local.activeCountryTab ?? 0}}", "tierIndex": "{{tierIdx}}", "field": "max", - "value": "{{$event.target.value ? parseFloat($event.target.value) : null}}" + "value": "{{$event.target.value}}" } } ] @@ -501,7 +516,7 @@ "props": { "type": "number", "className": "{{_local.rangeErrors?.[_computed.activeCountryCode]?.[tierIdx]?.fee ? 'input input-error' : 'input'}}", - "value": "{{tier.fee ?? 0}}", + "value": "{{tier.fee ?? ''}}", "min": 0, "step": 100, "disabled": "{{_computed.isReadOnly}}" @@ -515,7 +530,7 @@ "countryIndex": "{{_local.activeCountryTab ?? 0}}", "tierIndex": "{{tierIdx}}", "field": "fee", - "value": "{{parseFloat($event.target.value) || 0}}" + "value": "{{$event.target.value}}" } } ] @@ -586,6 +601,15 @@ "type": "basic", "name": "Span", "text": "$t:sirsoft-ecommerce.admin.shipping_policy.form.no_ranges" + }, + { + "type": "basic", + "name": "Span", + "if": "{{!!_local.rangeErrors?.[_computed.activeCountryCode]?.[0]?.max}}", + "props": { + "className": "form-error-xs block" + }, + "text": "{{_local.rangeErrors?.[_computed.activeCountryCode]?.[0]?.max ?? ''}}" } ] } diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_country_basic_fields.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_country_basic_fields.json index b64d7904..6889176d 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_country_basic_fields.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_country_basic_fields.json @@ -207,7 +207,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "label": "$t:sirsoft-ecommerce.admin.shipping_policy.form.country_active", diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_extra_fee.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_extra_fee.json index c5a161e2..555c7dd2 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_extra_fee.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_form/_partial_extra_fee.json @@ -47,7 +47,7 @@ "name": "Div", "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "label": "$t:sirsoft-ecommerce.admin.shipping_policy.form.extra_fee_enabled", @@ -90,7 +90,7 @@ "name": "Div", "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "label": "$t:sirsoft-ecommerce.admin.shipping_policy.form.extra_fee_multiply", @@ -435,7 +435,7 @@ "portable": { "children": [ { - "id": "extra_fee_cards", + "id": "extra_fee_cards_{{feeIdx}}", "type": "basic", "name": "Div", "iteration": { diff --git a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json index e9ca5a27..182a1b82 100644 --- a/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json +++ b/modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json @@ -10,6 +10,7 @@ "data": "{{shipping_policies?.data?.data || []}}", "idField": "id", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_global.bulkSelectedItems || []}}", "selectedCountText": "", "loading": "{{shipping_policies?.loading}}", diff --git a/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneExpiredTempOrdersCommand.php b/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneExpiredTempOrdersCommand.php new file mode 100644 index 00000000..31d8148a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneExpiredTempOrdersCommand.php @@ -0,0 +1,46 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Console\Commands; + +use Illuminate\Console\Command; +use Modules\Sirsoft\Ecommerce\Services\TempOrderService; + +/** + * 만료 임시 주문 자동 정리 커맨드 + * + * 임시 주문은 체크아웃 진입마다 생성되고 30분 TTL 로 만료되지만, 만료된 행을 + * 실제로 삭제하는 주체가 없어 무한히 누적됩니다. 스케줄러에서 주기적으로 실행합니다. + * + * @example php artisan sirsoft-ecommerce:prune-temp-orders + */ +class PruneExpiredTempOrdersCommand extends Command +{ + /** + * 커맨드 이름 및 시그니처 + * + * @var string + */ + protected $signature = 'sirsoft-ecommerce:prune-temp-orders'; + + /** + * 커맨드 설명 + * + * @var string + */ + protected $description = '만료된 임시 주문을 삭제합니다.'; + + /** + * 커맨드 실행 + * + * @param TempOrderService $tempOrderService 임시 주문 서비스 + * @return int 종료 코드 + */ + public function handle(TempOrderService $tempOrderService): int + { + $deleted = $tempOrderService->cleanupExpiredTempOrders(); + + $this->info("만료된 임시 주문 {$deleted}건이 삭제되었습니다."); + + return Command::SUCCESS; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneTempProductImagesCommand.php b/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneTempProductImagesCommand.php new file mode 100644 index 00000000..167e1b3a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Console/Commands/PruneTempProductImagesCommand.php @@ -0,0 +1,92 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Services\ProductImageService; + +/** + * 미연결 임시 상품 이미지 정리 커맨드 + * + * 상품 등록 폼에서 업로드만 하고 저장 없이 이탈해 남은 임시 이미지(파일 + 기록)를 정리합니다. + * 연결 시점에 본경로로 옮겨지므로 `temp_key` 가 남아 있다는 것은 "끝내 연결되지 않았다" 는 + * 뜻이고, 그래서 별도 운영자 토글 없이 상시 동작합니다 (운영 데이터가 아닌 폼 세션 부산물). + * + * @example php artisan sirsoft-ecommerce:prune-temp-product-images --dry-run + * @example php artisan sirsoft-ecommerce:prune-temp-product-images --days=1 + */ +class PruneTempProductImagesCommand extends Command +{ + /** + * 커맨드 이름 및 시그니처 + * + * @var string + */ + protected $signature = 'sirsoft-ecommerce:prune-temp-product-images + {--dry-run : 실제 삭제 없이 대상 건수만 확인} + {--limit=500 : 한 번에 처리할 최대 건수} + {--days=2 : 임시 이미지 보존기간(일)}'; + + /** + * 커맨드 설명 + * + * @var string + */ + protected $description = '상품에 연결되지 않은 채 방치된 임시 상품 이미지를 정리합니다.'; + + /** + * @param ProductImageService $productImageService 상품 이미지 서비스 + */ + public function __construct( + protected ProductImageService $productImageService + ) { + parent::__construct(); + } + + /** + * 커맨드 실행 + * + * @return int 종료 코드 + */ + public function handle(): int + { + $days = (int) $this->option('days'); + + // 보존기간 0 이하 = 방금 올린 임시 이미지까지 지우게 되므로 차단한다. + if ($days < 1) { + $this->info('임시 이미지 보존기간이 1일 미만이어서 정리를 수행하지 않았습니다.'); + + return Command::SUCCESS; + } + + $limit = max(1, (int) $this->option('limit')); + $isDryRun = (bool) $this->option('dry-run'); + + $result = $this->productImageService->pruneTempUploads($days, $limit, $isDryRun); + + if ($isDryRun) { + $this->info("[DRY RUN] 보존기간({$days}일) 경과 미연결 임시 상품 이미지: {$result['scanned']}건"); + + return Command::SUCCESS; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 임시 상품 이미지 %d건 중 %d건을 삭제했습니다. (실패 %d건)', + $days, + $result['scanned'], + $result['deleted'], + $result['failed'], + )); + + Log::info('PruneTempProductImagesCommand: 임시 상품 이미지 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + 'scanned' => $result['scanned'], + 'deleted' => $result['deleted'], + 'failed' => $result['failed'], + ]); + + return Command::SUCCESS; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Enums/ChargePolicyEnum.php b/modules/_bundled/sirsoft-ecommerce/src/Enums/ChargePolicyEnum.php index bd5359d0..eae84538 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Enums/ChargePolicyEnum.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Enums/ChargePolicyEnum.php @@ -75,6 +75,9 @@ enum ChargePolicyEnum: string return in_array($this, [ self::FIXED, self::CONDITIONAL_FREE, + // API 정책의 base_fee 는 외부 API 장애 시의 폴백 배송비다. + // 0 으로 강제되면 장애가 곧 무음 무료배송이 된다. + self::API, self::PER_QUANTITY, self::PER_WEIGHT, self::PER_VOLUME, @@ -83,6 +86,19 @@ enum ChargePolicyEnum: string ]); } + /** + * 구간 경계값이 이산형(정수)인 정책인지 확인합니다. + * + * 수량은 정수 단위라 "5개 다음은 6개"(max + 1)로 이어지고, 금액·무게·부피는 + * 연속값이라 "5kg 다음은 5kg 초과"(다음 min = max)로 이어집니다. + * + * @return bool 이산형 여부 + */ + public function hasDiscreteRangeValues(): bool + { + return $this === self::RANGE_QUANTITY; + } + /** * 무료 기준금액 필드가 필요한 정책인지 확인합니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/BrandOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/BrandOperationException.php new file mode 100644 index 00000000..ae0a4a47 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/BrandOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 브랜드 조작 실패 예외 + * + * 브랜드를 찾을 수 없거나, 연결된 상품이 있어 삭제할 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class BrandOperationException extends RuntimeException +{ + /** + * BrandOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CategoryOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CategoryOperationException.php index f38e761e..2501f5f2 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CategoryOperationException.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CategoryOperationException.php @@ -18,8 +18,32 @@ class CategoryOperationException extends RuntimeException * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) * @param array<string, mixed> $replace 메시지 치환 파라미터 */ - public function __construct(string $messageKey, array $replace = []) - { + public function __construct( + private string $messageKey, + private array $replace = [] + ) { parent::__construct(__($messageKey, $replace)); } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ClaimReasonException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ClaimReasonException.php index 0bb2bb5b..55471a56 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ClaimReasonException.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ClaimReasonException.php @@ -12,6 +12,17 @@ use Exception; */ class ClaimReasonException extends Exception { + /** + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + private function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + /** * 대상 클레임 사유를 찾을 수 없을 때 * @@ -19,7 +30,7 @@ class ClaimReasonException extends Exception */ public static function notFound(): self { - return new self(__('sirsoft-ecommerce::exceptions.claim_reason_not_found')); + return new self('sirsoft-ecommerce::exceptions.claim_reason_not_found'); } /** @@ -30,8 +41,30 @@ class ClaimReasonException extends Exception */ public static function inUse(int $usageCount): self { - return new self(__('sirsoft-ecommerce::exceptions.claim_reason_in_use', [ + return new self('sirsoft-ecommerce::exceptions.claim_reason_in_use', [ 'count' => $usageCount, - ])); + ]); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CouponOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CouponOperationException.php new file mode 100644 index 00000000..fbae9c22 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/CouponOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 쿠폰 조작 실패 예외 + * + * 쿠폰을 찾을 수 없거나, 발급 내역을 취소할 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class CouponOperationException extends RuntimeException +{ + /** + * CouponOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/OrderOptionNotConfirmableException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/OrderOptionNotConfirmableException.php new file mode 100644 index 00000000..db7cab42 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/OrderOptionNotConfirmableException.php @@ -0,0 +1,41 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 주문 옵션 구매확정 불가 예외 + * + * 이미 구매확정된 옵션을 다시 확정하려 하거나, 구매확정 가능 상태 + * (order_settings.confirmable_statuses) 가 아닌 옵션을 확정하려 할 때 발생합니다. + * OrderService::confirmOption 이 호출자(회원/비회원)와 무관하게 던지며, 컨트롤러는 + * 사유 식별자로 422 응답을 만듭니다. + * + * 사유 식별자는 `exceptions.*` 의 키와 같은 이름을 사용합니다: + * - order_option_already_confirmed + * - order_option_not_confirmable + */ +class OrderOptionNotConfirmableException extends RuntimeException +{ + /** + * @param string $reason 확정 불가 사유 식별자 (exceptions.* 키와 동일) + */ + public function __construct( + private string $reason + ) { + parent::__construct( + __("sirsoft-ecommerce::exceptions.{$reason}") + ); + } + + /** + * 확정 불가 사유 식별자 반환 + * + * @return string 사유 식별자 + */ + public function getReason(): string + { + return $this->reason; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductCommonInfoOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductCommonInfoOperationException.php new file mode 100644 index 00000000..5d392179 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductCommonInfoOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 상품 공통정보 조작 실패 예외 + * + * 상품 공통정보를 찾을 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class ProductCommonInfoOperationException extends RuntimeException +{ + /** + * ProductCommonInfoOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductInquiryOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductInquiryOperationException.php new file mode 100644 index 00000000..417d86cf --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductInquiryOperationException.php @@ -0,0 +1,54 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 상품 문의 조작 실패 예외 + * + * 문의 게시판이 설정되지 않았거나, 대상 문의·답변을 찾을 수 없거나, + * 답변 등록·수정·삭제가 도메인 규칙에 어긋날 때 사용합니다. + * + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 422 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. 종전에는 컨트롤러가 + * 예외 메시지 원문을 응답 메시지 키 자리에 넘겨, 번역되지 않은 문자열이 + * 그대로 노출될 수 있었습니다. + */ +class ProductInquiryOperationException extends RuntimeException +{ + /** + * ProductInquiryOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::messages.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductLabelOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductLabelOperationException.php new file mode 100644 index 00000000..5be0de4a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ProductLabelOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 상품 라벨 조작 실패 예외 + * + * 연결된 상품이 있어 라벨을 삭제할 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class ProductLabelOperationException extends RuntimeException +{ + /** + * ProductLabelOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewImageUploadLimitException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewImageUploadLimitException.php index 5cd992f7..5f6a0497 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewImageUploadLimitException.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewImageUploadLimitException.php @@ -15,8 +15,9 @@ class ReviewImageUploadLimitException extends RuntimeException /** * @param int $maxImages 허용된 최대 이미지 수 */ - public function __construct(int $maxImages) - { + public function __construct( + public readonly int $maxImages + ) { parent::__construct( __('sirsoft-ecommerce::review.image_upload_limit_exceeded', ['max' => $maxImages]) ); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewNotWritableException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewNotWritableException.php index 0c0f8848..b9bce66b 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewNotWritableException.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ReviewNotWritableException.php @@ -32,4 +32,21 @@ class ReviewNotWritableException extends RuntimeException { return $this->reason; } + + /** + * 작성 불가 사유의 번역 라벨을 반환합니다. + * + * 사유 식별자를 그대로 :reason 치환자에 실으면 최종 사용자 화면에 원시 + * 식별자(already_written 등)가 노출된다. 라벨 키가 없는 미지의 사유는 + * 식별자를 그대로 반환한다 (라벨 미정의가 표시 자체를 깨면 안 된다). + * + * @return string 번역된 사유 라벨 + */ + public function getReasonLabel(): string + { + $key = 'sirsoft-ecommerce::messages.reviews.reasons.'.$this->reason; + $label = __($key); + + return $label === $key ? $this->reason : $label; + } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingCarrierOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingCarrierOperationException.php new file mode 100644 index 00000000..2aecf1ab --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingCarrierOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 택배사 조작 실패 예외 + * + * 택배사를 찾을 수 없거나, 사용 중이어서 삭제·비활성화할 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 기존 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class ShippingCarrierOperationException extends RuntimeException +{ + /** + * ShippingCarrierOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingTypeOperationException.php b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingTypeOperationException.php new file mode 100644 index 00000000..978e6b89 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Exceptions/ShippingTypeOperationException.php @@ -0,0 +1,50 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Exceptions; + +use RuntimeException; + +/** + * 배송유형 조작 실패 예외 + * + * 배송유형을 찾을 수 없거나, 주문에서 사용 중이어서 삭제할 수 없는 경우에 사용합니다. + * 컨트롤러는 이 예외를 도메인 실패로 구분해 사유가 담긴 400 응답으로 처리하고, + * 그 외 예외는 서버 결함으로 보아 500 을 반환합니다. + */ +class ShippingTypeOperationException extends RuntimeException +{ + /** + * ShippingTypeOperationException 생성자 + * + * @param string $messageKey 다국어 메시지 키 (sirsoft-ecommerce::exceptions.*) + * @param array<string, mixed> $replace 메시지 치환 파라미터 + */ + public function __construct( + private string $messageKey, + private array $replace = [] + ) { + parent::__construct(__($messageKey, $replace)); + } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 컨트롤러가 예외 메시지 원문 대신 이 키로 응답을 구성하기 위한 접근자입니다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return $this->messageKey; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return $this->replace; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/BrandController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/BrandController.php index 45bd9546..571b1db6 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/BrandController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/BrandController.php @@ -5,12 +5,14 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\BrandOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BrandListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreBrandRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateBrandRequest; use Modules\Sirsoft\Ecommerce\Http\Resources\BrandCollection; use Modules\Sirsoft\Ecommerce\Http\Resources\BrandResource; use Modules\Sirsoft\Ecommerce\Services\BrandService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 브랜드 관리 컨트롤러 @@ -24,7 +26,7 @@ class BrandController extends AdminBaseController /** * 브랜드 목록 조회 * - * @param BrandListRequest $request + * @param BrandListRequest $request * @return JsonResponse */ public function index(BrandListRequest $request): JsonResponse @@ -41,7 +43,7 @@ class BrandController extends AdminBaseController /** * 브랜드 생성 * - * @param StoreBrandRequest $request + * @param StoreBrandRequest $request * @return JsonResponse */ public function store(StoreBrandRequest $request): JsonResponse @@ -59,7 +61,7 @@ class BrandController extends AdminBaseController /** * 브랜드 상세 조회 * - * @param int $id + * @param int $id * @return JsonResponse */ public function show(int $id): JsonResponse @@ -84,8 +86,8 @@ class BrandController extends AdminBaseController /** * 브랜드 수정 * - * @param UpdateBrandRequest $request - * @param int $brand + * @param UpdateBrandRequest $request + * @param int $brand * @return JsonResponse */ public function update(UpdateBrandRequest $request, int $brand): JsonResponse @@ -98,11 +100,26 @@ class BrandController extends AdminBaseController 'messages.brands.updated', new BrandResource($updatedBrand) ); + } catch (BrandOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/상품 연결)를 전용 + // 키로 안내한다 — generic 키로 접으면 운영자가 원인을 알 수 없다 + // (형제 ClaimReason/ShippingCarrier/ProductLabel destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -110,7 +127,7 @@ class BrandController extends AdminBaseController /** * 브랜드 상태 토글 * - * @param int $brand + * @param int $brand * @return JsonResponse */ public function toggleStatus(int $brand): JsonResponse @@ -123,11 +140,26 @@ class BrandController extends AdminBaseController 'messages.brands.status_changed', new BrandResource($updatedBrand) ); + } catch (BrandOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/상품 연결)를 전용 + // 키로 안내한다 — generic 키로 접으면 운영자가 원인을 알 수 없다 + // (형제 ClaimReason/ShippingCarrier/ProductLabel destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -135,7 +167,7 @@ class BrandController extends AdminBaseController /** * 브랜드 삭제 * - * @param int $brand + * @param int $brand * @return JsonResponse */ public function destroy(int $brand): JsonResponse @@ -148,11 +180,26 @@ class BrandController extends AdminBaseController 'messages.brands.deleted', $result ); + } catch (BrandOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/상품 연결)를 전용 + // 키로 안내한다 — generic 키로 접으면 운영자가 원인을 알 수 없다 + // (형제 ClaimReason/ShippingCarrier/ProductLabel destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CategoryController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CategoryController.php index 51602e09..974d05a5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CategoryController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CategoryController.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\CategoryOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\CategoryListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\CreateCategoryRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ReorderCategoriesRequest; @@ -125,11 +126,23 @@ class CategoryController extends AdminBaseController 'messages.categories.updated', new CategoryResource($updatedCategory) ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -150,11 +163,23 @@ class CategoryController extends AdminBaseController 'messages.categories.deleted', $result ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -199,11 +224,23 @@ class CategoryController extends AdminBaseController ], 201 ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -231,11 +268,23 @@ class CategoryController extends AdminBaseController 'sirsoft-ecommerce', 'messages.category_images.deleted' ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -257,11 +306,23 @@ class CategoryController extends AdminBaseController 'sirsoft-ecommerce', 'messages.category_images.reordered' ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -313,11 +374,23 @@ class CategoryController extends AdminBaseController 'messages.categories.status_changed', new CategoryResource($category) ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -390,11 +463,23 @@ class CategoryController extends AdminBaseController 'sirsoft-ecommerce', 'messages.categories.order_updated' ); + } catch (CategoryOperationException $e) { + // 도메인 규칙 위반 — 기존 400 유지. 구체 사유(미존재/하위·상품 연결)를 + // 전용 키로 안내한다 (형제 Brand/ClaimReason destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ClaimReasonController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ClaimReasonController.php index d2cd712a..68145fa6 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ClaimReasonController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ClaimReasonController.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ClaimReasonException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ActiveClaimReasonRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\IndexClaimReasonRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreClaimReasonRequest; @@ -119,12 +120,20 @@ class ClaimReasonController extends AdminBaseController 'messages.claim_reasons.updated', new ClaimReasonResource($reason) ); - } catch (\Exception $e) { + } catch (ClaimReasonException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -154,11 +163,23 @@ class ClaimReasonController extends AdminBaseController 'messages.claim_reasons.deleted', $result ); + } catch (ClaimReasonException $e) { + // 삭제 불가 사유(미존재/사용 중)를 전용 키로 안내 — 예외 메시지 원문을 + // 키 자리에 넘기면(종전 동작) 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', - $e->getMessage(), - 400 + 'exceptions.operation_failed', + 500 ); } } @@ -179,12 +200,20 @@ class ClaimReasonController extends AdminBaseController 'messages.claim_reasons.toggled', new ClaimReasonResource($reason) ); - } catch (\Exception $e) { + } catch (ClaimReasonException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CouponController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CouponController.php index ad9d0f27..8d7872c5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CouponController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CouponController.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\CouponOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BulkUpdateCouponStatusRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\CouponIssuesListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\CouponListRequest; @@ -16,6 +17,7 @@ use Modules\Sirsoft\Ecommerce\Http\Resources\CouponIssueCollection; use Modules\Sirsoft\Ecommerce\Http\Resources\CouponIssueResource; use Modules\Sirsoft\Ecommerce\Http\Resources\CouponResource; use Modules\Sirsoft\Ecommerce\Services\CouponService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 쿠폰 관리 컨트롤러 @@ -106,12 +108,22 @@ class CouponController extends AdminBaseController 'messages.coupons.updated', new CouponResource($coupon) ); - } catch (\Exception $e) { + } catch (CouponOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -131,12 +143,22 @@ class CouponController extends AdminBaseController 'messages.coupons.deleted', $result ); - } catch (\Exception $e) { + } catch (CouponOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -188,12 +210,22 @@ class CouponController extends AdminBaseController 200, ['issued' => $result['issued'], 'skipped' => count($result['skipped'])] ); - } catch (\Exception $e) { + } catch (CouponOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -214,13 +246,25 @@ class CouponController extends AdminBaseController 'messages.coupons.issue_cancelled', new CouponIssueResource($issue) ); + } catch (CouponOperationException $e) { + // 취소 불가 사유(미존재/미사용 아님)를 전용 키로 안내 — 예외 메시지 원문 대신 + // 키로 응답을 구성한다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { - // 취소 불가 사유(미사용 아님 등)를 관리자에게 그대로 노출 + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400, - ['detail' => $e->getMessage()] + 500 ); } } @@ -249,11 +293,24 @@ class CouponController extends AdminBaseController 'messages.coupons.issues_retrieved', new CouponIssueCollection($issues) ); + } catch (CouponOperationException $e) { + // 쿠폰 미존재 등 도메인 사유 — 운영자에게 안내 가능하므로 기존 400 유지 + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/EcommerceSettingsController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/EcommerceSettingsController.php index c3ca608d..c304aeb5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/EcommerceSettingsController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/EcommerceSettingsController.php @@ -7,14 +7,20 @@ use App\Helpers\PermissionHelper; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use App\Seo\Contracts\SeoCacheManagerInterface; +use App\Services\DriverRegistryService; use App\Services\NotificationChannelService; use App\Services\NotificationDefinitionService; use Exception; use Illuminate\Http\JsonResponse; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiAuthType; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiHttpMethod; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiRequestField; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiResponseType; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingCarrierOperationException; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingTypeOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\GetSettingRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreBanksRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreEcommerceSettingsRequest; @@ -56,6 +62,9 @@ class EcommerceSettingsController extends AdminBaseController // 현금영수증 발급 프로바이더 후보 — 결제 플러그인이 훅으로 자신을 등록한다. // 발급 PG 와 결제 PG 는 독립 선택이므로 목록도 별도로 내린다 (KG 결제 + 토스 발급 등). $settings['available_cash_receipt_providers'] = $this->settingsService->getRegisteredCashReceiptProviders(); + // 공개 자산 디스크 선택지 — 카탈로그 SSoT 는 코어 DriverRegistryService 게터 한 곳 + $settings['available_public_asset_disks'] = app(DriverRegistryService::class) + ->getAvailableDrivers('public_asset'); $settings['abilities'] = [ 'can_update' => PermissionHelper::check('sirsoft-ecommerce.settings.update', request()->user()), ]; @@ -162,6 +171,12 @@ class EcommerceSettingsController extends AdminBaseController // 설정 저장 활동로그 (저장된 카테고리 목록 전달) HookManager::doAction('sirsoft-ecommerce.settings.after_save', array_keys($settings)); + // 코어 모듈 설정 저장 훅 — SEO 캐시 무효화 등 코어/타 확장 리스너가 구독한다. + // 발화 지점을 서비스가 아니라 관리자 컨트롤러에 두는 이유: 훅 의미가 + // "관리자가 설정을 저장했다" 이고, 서비스에 두면 내부 저장 호출 전부가 + // 활동로그·캐시 무효화를 유발한다. + HookManager::doAction('core.module_settings.after_save', 'sirsoft-ecommerce', $settings, $result); + // 저장 후 전체 설정 반환 (관리자 UI 상태 업데이트용) $updatedSettings = $this->settingsService->getAllSettings(); $updatedSettings = $this->appendCarriersToSettings($updatedSettings); @@ -171,6 +186,9 @@ class EcommerceSettingsController extends AdminBaseController $updatedSettings = $this->appendMileageNotificationChannelsToSettings($updatedSettings); $updatedSettings['available_pg_providers'] = $this->settingsService->getRegisteredPgProviders(); $updatedSettings['available_cash_receipt_providers'] = $this->settingsService->getRegisteredCashReceiptProviders(); + // 공개 자산 디스크 선택지 — 저장 응답에도 재부착 (관리자 UI 상태 갱신용) + $updatedSettings['available_public_asset_disks'] = app(DriverRegistryService::class) + ->getAvailableDrivers('public_asset'); return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', @@ -184,7 +202,25 @@ class EcommerceSettingsController extends AdminBaseController 400 ); } + } catch (ShippingTypeOperationException|ShippingCarrierOperationException $e) { + // 도메인 실패(사용 중이라 삭제 불가 등)는 운영자가 조치할 수 있는 사유다. + // generic 500 으로 뭉개면 "왜" 저장이 실패했는지 화면에서 알 수 없다. + // 키가 이미 `sirsoft-ecommerce::` 로 정규화돼 있어 moduleError 가 아닌 error 를 쓴다 + // (ShippingCarrierController::destroy 와 동형). + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (Exception $e) { + Log::error('이커머스 설정 저장 실패', [ + 'user_id' => Auth::id(), + 'error' => $e->getMessage(), + ]); + return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.settings.save_error', @@ -207,6 +243,13 @@ class EcommerceSettingsController extends AdminBaseController $result = $this->settingsService->saveBanks($banks); if ($result) { + HookManager::doAction( + 'core.module_settings.after_save', + 'sirsoft-ecommerce', + ['order_settings' => ['banks' => $banks]], + $result + ); + $updatedSettings = $this->settingsService->getAllSettings(); return ResponseHelper::moduleSuccess( @@ -275,6 +318,11 @@ class EcommerceSettingsController extends AdminBaseController $result = $this->settingsService->setSetting($key, $value); if ($result) { + // dot-key 를 카테고리 하위 구조로 되돌려 벌크 저장과 같은 payload 형태로 만든다 + $payload = []; + Arr::set($payload, $key, $value); + HookManager::doAction('core.module_settings.after_save', 'sirsoft-ecommerce', $payload, $result); + return ResponseHelper::moduleSuccess( 'sirsoft-ecommerce', 'messages.settings.update_success', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ExtraFeeTemplateController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ExtraFeeTemplateController.php index 346d6c2e..8c162db8 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ExtraFeeTemplateController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ExtraFeeTemplateController.php @@ -5,7 +5,6 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ExtraFeeTemplateBulkCreateRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ExtraFeeTemplateBulkDeleteRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ExtraFeeTemplateBulkToggleActiveRequest; @@ -15,6 +14,7 @@ use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ExtraFeeTemplateUpdateRequest; use Modules\Sirsoft\Ecommerce\Http\Resources\ExtraFeeTemplateCollection; use Modules\Sirsoft\Ecommerce\Http\Resources\ExtraFeeTemplateResource; use Modules\Sirsoft\Ecommerce\Services\ExtraFeeTemplateService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 추가배송비 템플릿 관리 컨트롤러 @@ -28,7 +28,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 목록 조회 * - * @param ExtraFeeTemplateListRequest $request + * @param ExtraFeeTemplateListRequest $request * @return JsonResponse */ public function index(ExtraFeeTemplateListRequest $request): JsonResponse @@ -48,7 +48,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 상세 조회 * - * @param int $id + * @param int $id * @return JsonResponse */ public function show(int $id): JsonResponse @@ -73,7 +73,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 생성 * - * @param ExtraFeeTemplateStoreRequest $request + * @param ExtraFeeTemplateStoreRequest $request * @return JsonResponse */ public function store(ExtraFeeTemplateStoreRequest $request): JsonResponse @@ -87,11 +87,14 @@ class ExtraFeeTemplateController extends AdminBaseController new ExtraFeeTemplateResource($template), 201 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -99,8 +102,8 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 수정 * - * @param ExtraFeeTemplateUpdateRequest $request - * @param int $id + * @param ExtraFeeTemplateUpdateRequest $request + * @param int $id * @return JsonResponse */ public function update(ExtraFeeTemplateUpdateRequest $request, int $id): JsonResponse @@ -123,11 +126,14 @@ class ExtraFeeTemplateController extends AdminBaseController 'messages.extra_fee_template.updated', new ExtraFeeTemplateResource($updatedTemplate) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -135,7 +141,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 삭제 * - * @param int $id + * @param int $id * @return JsonResponse */ public function destroy(int $id): JsonResponse @@ -157,11 +163,14 @@ class ExtraFeeTemplateController extends AdminBaseController 'sirsoft-ecommerce', 'messages.extra_fee_template.deleted' ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -169,7 +178,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 사용여부 토글 * - * @param int $id + * @param int $id * @return JsonResponse */ public function toggleActive(int $id): JsonResponse @@ -192,11 +201,14 @@ class ExtraFeeTemplateController extends AdminBaseController 'messages.extra_fee_template.toggled', new ExtraFeeTemplateResource($updatedTemplate) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -204,7 +216,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 일괄 삭제 * - * @param ExtraFeeTemplateBulkDeleteRequest $request + * @param ExtraFeeTemplateBulkDeleteRequest $request * @return JsonResponse */ public function bulkDestroy(ExtraFeeTemplateBulkDeleteRequest $request): JsonResponse @@ -217,11 +229,14 @@ class ExtraFeeTemplateController extends AdminBaseController 'messages.extra_fee_template.bulk_deleted', ['deleted_count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -229,7 +244,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 추가배송비 템플릿 일괄 사용여부 변경 * - * @param ExtraFeeTemplateBulkToggleActiveRequest $request + * @param ExtraFeeTemplateBulkToggleActiveRequest $request * @return JsonResponse */ public function bulkToggleActive(ExtraFeeTemplateBulkToggleActiveRequest $request): JsonResponse @@ -246,11 +261,14 @@ class ExtraFeeTemplateController extends AdminBaseController 'messages.extra_fee_template.bulk_toggled', ['updated_count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -258,7 +276,7 @@ class ExtraFeeTemplateController extends AdminBaseController /** * 일괄 등록 (CSV/엑셀 업로드용) * - * @param ExtraFeeTemplateBulkCreateRequest $request + * @param ExtraFeeTemplateBulkCreateRequest $request * @return JsonResponse */ public function bulkStore(ExtraFeeTemplateBulkCreateRequest $request): JsonResponse @@ -272,11 +290,14 @@ class ExtraFeeTemplateController extends AdminBaseController ['created_count' => $count], 201 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/OrderController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/OrderController.php index d57f8371..dab08c4f 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/OrderController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/OrderController.php @@ -12,6 +12,9 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderAmountChangedException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderCancellationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderProcessingException; use Modules\Sirsoft\Ecommerce\Exceptions\PaymentAmountMismatchException; use Modules\Sirsoft\Ecommerce\Exceptions\ResourceScopeMismatchException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BulkChangeOrderOptionStatusRequest; @@ -31,6 +34,7 @@ use Modules\Sirsoft\Ecommerce\Services\OrderCancellationService; use Modules\Sirsoft\Ecommerce\Services\OrderOptionService; use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; use Modules\Sirsoft\Ecommerce\Services\OrderService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 주문 관리 컨트롤러 @@ -67,6 +71,8 @@ class OrderController extends AdminBaseController 'messages.orders.fetch_success', $collection->withStatistics($statistics) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -100,6 +106,8 @@ class OrderController extends AdminBaseController 'messages.orders.fetch_success', new OrderResource($order) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -132,6 +140,8 @@ class OrderController extends AdminBaseController 'messages.orders.update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -164,6 +174,8 @@ class OrderController extends AdminBaseController 'messages.orders.bulk_update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -190,6 +202,8 @@ class OrderController extends AdminBaseController 'messages.orders.deleted', ['deleted' => true] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -247,6 +261,8 @@ class OrderController extends AdminBaseController 'messages.orders.option_status_change_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -273,6 +289,8 @@ class OrderController extends AdminBaseController 'sirsoft-ecommerce', 'messages.orders.email_sent' ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -304,6 +322,8 @@ class OrderController extends AdminBaseController 'messages.orders.estimate_refund_success', $result->toPreviewArray() ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('환불 예상금액 계산 실패', [ 'order_id' => $order->id, @@ -361,6 +381,18 @@ class OrderController extends AdminBaseController 'messages.orders.cancelled', new OrderResource($updatedOrder) ); + } catch (OrderCancellationException $e) { + // 취소 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 422. + // detail 은 PG 환불 거절 사유 등 조치 근거다 — 관리자 전용 면의 errors + // 페이로드는 진단 정보로 허용된다(감추면 서버 로그 없이는 조치 불가). + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.orders.cancel_failed', + 422, + ['detail' => $e->getMessage()] + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('주문 취소 실패 (관리자)', [ 'order_id' => $order->id, @@ -370,9 +402,8 @@ class OrderController extends AdminBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'messages.orders.cancel_failed', - 422, - ['detail' => $e->getMessage()] + 'exceptions.operation_failed', + 500 ); } } @@ -412,6 +443,18 @@ class OrderController extends AdminBaseController 422, ['detail' => $e->getMessage()] ); + } catch (OrderProcessingException|OrderAmountChangedException $e) { + // 입금확인 도메인 규칙 위반(전이 불가 상태/결제예정금액 변동) — 422. + // detail 은 같은 메서드의 PaymentAmountMismatchException 과 동일한 관리자 + // 진단 통로다 — 한 메서드 안에서 노출 수준이 갈리면 안 된다. + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.orders.deposit_confirm_failed', + 422, + ['detail' => $e->getMessage()] + ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('무통장 입금확인 실패 (관리자)', [ 'order_id' => $order->id, @@ -420,9 +463,8 @@ class OrderController extends AdminBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'messages.orders.deposit_confirm_failed', - 422, - ['detail' => $e->getMessage()] + 'exceptions.operation_failed', + 500 ); } } @@ -458,6 +500,8 @@ class OrderController extends AdminBaseController 'sirsoft-ecommerce', 'messages.orders.guest_password_reset_success' ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('비회원 조회 비밀번호 재설정 실패', [ 'order_id' => $order->id, @@ -490,6 +534,8 @@ class OrderController extends AdminBaseController 'messages.orders.logs_fetch_success', ActivityLogResource::collection($logs)->response()->getData(true) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductCommonInfoController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductCommonInfoController.php index 821b81fe..276bbd07 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductCommonInfoController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductCommonInfoController.php @@ -6,6 +6,7 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Exception; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductCommonInfoOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\IndexProductCommonInfoRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreProductCommonInfoRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateProductCommonInfoRequest; @@ -97,12 +98,20 @@ class ProductCommonInfoController extends AdminBaseController new ProductCommonInfoResource($commonInfo), 201 ); - } catch (Exception $e) { + } catch (ProductCommonInfoOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -123,12 +132,20 @@ class ProductCommonInfoController extends AdminBaseController 'messages.common_infos.updated', new ProductCommonInfoResource($commonInfo) ); - } catch (Exception $e) { + } catch (ProductCommonInfoOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -148,12 +165,20 @@ class ProductCommonInfoController extends AdminBaseController 'messages.common_infos.deleted', $result ); - } catch (Exception $e) { + } catch (ProductCommonInfoOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -175,12 +200,20 @@ class ProductCommonInfoController extends AdminBaseController : 'messages.common_infos.deactivated', new ProductCommonInfoResource($commonInfo) ); - } catch (Exception $e) { + } catch (ProductCommonInfoOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductController.php index 84ca4a18..d261b3e4 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductController.php @@ -12,6 +12,7 @@ use Illuminate\Validation\ValidationException; use Modules\Sirsoft\Ecommerce\Exceptions\ProductHasOrderHistoryException; use Modules\Sirsoft\Ecommerce\Exceptions\ProductImageUploadLimitException; use Modules\Sirsoft\Ecommerce\Exceptions\ProductPriceRelationException; +use Modules\Sirsoft\Ecommerce\Exceptions\ResourceScopeMismatchException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BulkUpdatePriceRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BulkUpdateProductsRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\BulkUpdateStatusRequest; @@ -30,6 +31,7 @@ use Modules\Sirsoft\Ecommerce\Http\Resources\ProductResource; use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Services\ProductImageService; use Modules\Sirsoft\Ecommerce\Services\ProductService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 상품 관리 컨트롤러 @@ -63,6 +65,8 @@ class ProductController extends AdminBaseController 'messages.products.fetch_success', $collection->withStatistics($statistics) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -110,6 +114,8 @@ class ProductController extends AdminBaseController 'product_ids' => $result['product_ids'], ] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('상품 옵션 배치 조회 실패', ['message' => $e->getMessage()]); @@ -151,6 +157,8 @@ class ProductController extends AdminBaseController 'messages.products.fetch_success', new ProductResource($product) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -186,6 +194,8 @@ class ProductController extends AdminBaseController 422, $e->errors() ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('상품 등록 실패', [ 'message' => $e->getMessage(), @@ -227,6 +237,8 @@ class ProductController extends AdminBaseController 422, $e->errors() ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { Log::error('상품 수정 실패', [ 'product_id' => $product->id, @@ -261,6 +273,8 @@ class ProductController extends AdminBaseController 'messages.products.can_delete_checked', $result ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -311,6 +325,8 @@ class ProductController extends AdminBaseController null, ['count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -349,6 +365,8 @@ class ProductController extends AdminBaseController 'messages.products.bulk_update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -388,6 +406,8 @@ class ProductController extends AdminBaseController 'messages.products.bulk_price_update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -426,6 +446,8 @@ class ProductController extends AdminBaseController 'messages.products.bulk_stock_update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -472,6 +494,8 @@ class ProductController extends AdminBaseController 'messages.products.bulk_update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -496,6 +520,8 @@ class ProductController extends AdminBaseController 'messages.products.code_generated', ['product_code' => $code] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -529,6 +555,8 @@ class ProductController extends AdminBaseController 'messages.products.fetch_success', new ProductResource($product) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -571,6 +599,8 @@ class ProductController extends AdminBaseController 'messages.products.update_failed', 422 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -604,6 +634,8 @@ class ProductController extends AdminBaseController 'messages.products.fetch_success', $formData ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -671,6 +703,8 @@ class ProductController extends AdminBaseController 'messages.products.fetch_success', $copyData ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -730,7 +764,10 @@ class ProductController extends AdminBaseController ['max' => $e->maxImages], 'sirsoft-ecommerce' ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { + // 개수 상한 초과는 위에서 typed 로 분기된다 — 여기 도달하면 서버 결함/인프라 장애다 Log::error('상품 이미지 업로드 실패', [ 'product_id' => $productId, 'error' => $e->getMessage(), @@ -740,7 +777,7 @@ class ProductController extends AdminBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -768,12 +805,22 @@ class ProductController extends AdminBaseController 'sirsoft-ecommerce', 'messages.product_images.deleted' ); - } catch (Exception $e) { + } catch (ResourceScopeMismatchException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -793,12 +840,22 @@ class ProductController extends AdminBaseController 'sirsoft-ecommerce', 'messages.product_images.reordered' ); - } catch (Exception $e) { + } catch (ResourceScopeMismatchException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -818,12 +875,22 @@ class ProductController extends AdminBaseController 'sirsoft-ecommerce', 'messages.product_images.thumbnail_set' ); - } catch (Exception $e) { + } catch (ResourceScopeMismatchException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -862,6 +929,8 @@ class ProductController extends AdminBaseController 'messages.products.logs_fetch_success', ActivityLogResource::collection($logs)->response()->getData(true) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductInquiryController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductInquiryController.php index 8775daae..1c60cb3d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductInquiryController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductInquiryController.php @@ -6,6 +6,7 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Exception; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductInquiryOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreInquiryReplyRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateInquiryReplyRequest; use Modules\Sirsoft\Ecommerce\Services\ProductInquiryService; @@ -39,8 +40,12 @@ class ProductInquiryController extends AdminBaseController ['id' => $inquiry->id, 'is_answered' => $inquiry->is_answered], 201 ); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ProductInquiryOperationException $e) { + // 도메인 규칙 위반 — 예외가 보관한 메시지 키로 응답을 구성한다. + // 종전처럼 번역된 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error($messageKey, 422, null, $e->getMessageParams()); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_failed', 500); } @@ -58,8 +63,12 @@ class ProductInquiryController extends AdminBaseController $this->inquiryService->deleteInquiry($inquiryId); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.deleted', ['deleted' => true]); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ProductInquiryOperationException $e) { + // 도메인 규칙 위반 — 예외가 보관한 메시지 키로 응답을 구성한다. + // 종전처럼 번역된 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error($messageKey, 422, null, $e->getMessageParams()); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.delete_failed', 500); } @@ -78,8 +87,12 @@ class ProductInquiryController extends AdminBaseController $this->inquiryService->updateReply($inquiryId, $request->validated()); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.reply_updated', ['id' => $inquiryId]); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ProductInquiryOperationException $e) { + // 도메인 규칙 위반 — 예외가 보관한 메시지 키로 응답을 구성한다. + // 종전처럼 번역된 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error($messageKey, 422, null, $e->getMessageParams()); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_update_failed', 500); } @@ -97,8 +110,12 @@ class ProductInquiryController extends AdminBaseController $this->inquiryService->deleteReply($inquiryId); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.reply_deleted', ['deleted' => true]); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ProductInquiryOperationException $e) { + // 도메인 규칙 위반 — 예외가 보관한 메시지 키로 응답을 구성한다. + // 종전처럼 번역된 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error($messageKey, 422, null, $e->getMessageParams()); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_delete_failed', 500); } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductLabelController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductLabelController.php index d981ac4d..87bb9aff 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductLabelController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductLabelController.php @@ -6,6 +6,7 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductLabelOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ProductLabelListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ProductLabelStoreRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ProductLabelUpdateRequest; @@ -25,7 +26,7 @@ class ProductLabelController extends AdminBaseController /** * 라벨 목록 조회 * - * @param ProductLabelListRequest $request + * @param ProductLabelListRequest $request * @return JsonResponse */ public function index(ProductLabelListRequest $request): JsonResponse @@ -33,7 +34,7 @@ class ProductLabelController extends AdminBaseController $filters = $request->validated(); // active_only 필터 처리 (기존 호환성 유지) - if (!empty($filters['active_only'])) { + if (! empty($filters['active_only'])) { $filters['is_active'] = true; } @@ -49,14 +50,14 @@ class ProductLabelController extends AdminBaseController /** * 라벨 상세 조회 * - * @param int $id + * @param int $id * @return JsonResponse */ public function show(int $id): JsonResponse { $label = $this->service->getLabel($id); - if (!$label) { + if (! $label) { return ResponseHelper::notFound( 'messages.labels.not_found', [], @@ -74,7 +75,7 @@ class ProductLabelController extends AdminBaseController /** * 라벨 생성 * - * @param ProductLabelStoreRequest $request + * @param ProductLabelStoreRequest $request * @return JsonResponse */ public function store(ProductLabelStoreRequest $request): JsonResponse @@ -92,8 +93,8 @@ class ProductLabelController extends AdminBaseController /** * 라벨 수정 * - * @param ProductLabelUpdateRequest $request - * @param int $id + * @param ProductLabelUpdateRequest $request + * @param int $id * @return JsonResponse */ public function update(ProductLabelUpdateRequest $request, int $id): JsonResponse @@ -113,10 +114,11 @@ class ProductLabelController extends AdminBaseController 'sirsoft-ecommerce' ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.labels.update_failed', - 400 + 500 ); } } @@ -124,7 +126,7 @@ class ProductLabelController extends AdminBaseController /** * 라벨 상태 토글 * - * @param int $id + * @param int $id * @return JsonResponse */ public function toggleStatus(int $id): JsonResponse @@ -144,10 +146,11 @@ class ProductLabelController extends AdminBaseController 'sirsoft-ecommerce' ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.labels.update_failed', - 400 + 500 ); } } @@ -155,7 +158,7 @@ class ProductLabelController extends AdminBaseController /** * 라벨 삭제 * - * @param int $id + * @param int $id * @return JsonResponse */ public function destroy(int $id): JsonResponse @@ -174,11 +177,22 @@ class ProductLabelController extends AdminBaseController [], 'sirsoft-ecommerce' ); + } catch (ProductLabelOperationException $e) { + // 연결된 상품이 있어 삭제 불가 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.labels.delete_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductNoticeTemplateController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductNoticeTemplateController.php index 8380eb38..aba8b15c 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductNoticeTemplateController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductNoticeTemplateController.php @@ -6,6 +6,7 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Exception; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductNoticeTemplateNotFoundException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ProductNoticeTemplateListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreProductNoticeTemplateRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateProductNoticeTemplateRequest; @@ -106,12 +107,20 @@ class ProductNoticeTemplateController extends AdminBaseController new ProductNoticeTemplateResource($template), 201 ); - } catch (Exception $e) { + } catch (ProductNoticeTemplateNotFoundException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -132,12 +141,20 @@ class ProductNoticeTemplateController extends AdminBaseController 'messages.notice_templates.updated', new ProductNoticeTemplateResource($template) ); - } catch (Exception $e) { + } catch (ProductNoticeTemplateNotFoundException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -157,12 +174,20 @@ class ProductNoticeTemplateController extends AdminBaseController 'messages.notice_templates.deleted', $result ); - } catch (Exception $e) { + } catch (ProductNoticeTemplateNotFoundException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -183,12 +208,20 @@ class ProductNoticeTemplateController extends AdminBaseController new ProductNoticeTemplateResource($template), 201 ); - } catch (Exception $e) { + } catch (ProductNoticeTemplateNotFoundException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -210,12 +243,20 @@ class ProductNoticeTemplateController extends AdminBaseController : 'messages.notice_templates.deactivated', new ProductNoticeTemplateResource($template) ); - } catch (Exception $e) { + } catch (ProductNoticeTemplateNotFoundException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductReviewController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductReviewController.php index 78dbc469..e7c78067 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductReviewController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ProductReviewController.php @@ -14,6 +14,7 @@ use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateReviewStatusRequest; use Modules\Sirsoft\Ecommerce\Http\Resources\ProductReviewResource; use Modules\Sirsoft\Ecommerce\Models\ProductReview; use Modules\Sirsoft\Ecommerce\Services\ProductReviewService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 상품 리뷰 관리 컨트롤러 (관리자) @@ -44,6 +45,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.fetch_success', ProductReviewResource::collection($reviews)->response()->getData(true) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -69,6 +72,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.fetch_success', new ProductReviewResource($review) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -96,6 +101,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.updated', new ProductReviewResource($updated) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -123,6 +130,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.reply_saved', new ProductReviewResource($updated) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -148,6 +157,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.reply_deleted', new ProductReviewResource($updated) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -175,6 +186,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.deleted', ['deleted' => true] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -215,6 +228,8 @@ class ProductReviewController extends AdminBaseController 'messages.reviews.bulk_updated', ['updated_count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingCarrierController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingCarrierController.php index 0eae9b7e..71d1cfad 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingCarrierController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingCarrierController.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Http\Controllers\Admin; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingCarrierOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\ActiveShippingCarrierRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\IndexShippingCarrierRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreShippingCarrierRequest; @@ -126,12 +127,20 @@ class ShippingCarrierController extends AdminBaseController 'messages.shipping_carriers.updated', new ShippingCarrierResource($updatedCarrier) ); - } catch (\Exception $e) { + } catch (ShippingCarrierOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } @@ -161,11 +170,23 @@ class ShippingCarrierController extends AdminBaseController 'messages.shipping_carriers.deleted', $result ); + } catch (ShippingCarrierOperationException $e) { + // 삭제 불가 사유(미존재/사용 중)를 전용 키로 안내 — 예외 메시지 원문을 + // 키 자리에 넘기면(종전 동작) 키 해석에 실패해 원문이 그대로 노출된다. + $messageKey = $e->getMessageKey(); + + return ResponseHelper::error( + $messageKey, + 400, + null, + $e->getMessageParams() + ); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', - $e->getMessage(), - 400 + 'exceptions.operation_failed', + 500 ); } } @@ -186,12 +207,20 @@ class ShippingCarrierController extends AdminBaseController 'messages.shipping_carriers.status_changed', new ShippingCarrierResource($updatedCarrier) ); - } catch (\Exception $e) { + } catch (ShippingCarrierOperationException $e) { + // 도메인 규칙 위반 — 운영자에게 안내 가능한 상황이므로 기존 400 유지 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', 400 ); + } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 + ); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingPolicyController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingPolicyController.php index 00eda019..7861eb3b 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingPolicyController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/ShippingPolicyController.php @@ -15,6 +15,7 @@ use Modules\Sirsoft\Ecommerce\Http\Resources\ShippingPolicyCollection; use Modules\Sirsoft\Ecommerce\Http\Resources\ShippingPolicyResource; use Modules\Sirsoft\Ecommerce\Services\OrderCalculationService; use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyService; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 배송정책 관리 컨트롤러 @@ -29,7 +30,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 목록 조회 * - * @param ShippingPolicyListRequest $request + * @param ShippingPolicyListRequest $request * @return JsonResponse */ public function index(ShippingPolicyListRequest $request): JsonResponse @@ -49,7 +50,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 생성 * - * @param StoreShippingPolicyRequest $request + * @param StoreShippingPolicyRequest $request * @return JsonResponse */ public function store(StoreShippingPolicyRequest $request): JsonResponse @@ -63,6 +64,8 @@ class ShippingPolicyController extends AdminBaseController new ShippingPolicyResource($shippingPolicy), 201 ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -102,8 +105,8 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 수정 * - * @param UpdateShippingPolicyRequest $request - * @param int $id + * @param UpdateShippingPolicyRequest $request + * @param int $id * @return JsonResponse */ public function update(UpdateShippingPolicyRequest $request, int $id): JsonResponse @@ -126,6 +129,8 @@ class ShippingPolicyController extends AdminBaseController 'messages.shipping_policy.updated', new ShippingPolicyResource($updated) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -138,7 +143,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 상세 조회 * - * @param int $id + * @param int $id * @return JsonResponse */ public function show(int $id): JsonResponse @@ -163,7 +168,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 삭제 * - * @param int $id + * @param int $id * @return JsonResponse */ public function destroy(int $id): JsonResponse @@ -185,11 +190,14 @@ class ShippingPolicyController extends AdminBaseController 'sirsoft-ecommerce', 'messages.shipping_policy.deleted' ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -197,7 +205,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 사용여부 토글 * - * @param int $id + * @param int $id * @return JsonResponse */ public function toggleActive(int $id): JsonResponse @@ -220,11 +228,14 @@ class ShippingPolicyController extends AdminBaseController 'messages.shipping_policy.toggled', new ShippingPolicyResource($updatedPolicy) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -232,7 +243,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 일괄 삭제 * - * @param ShippingPolicyBulkDeleteRequest $request + * @param ShippingPolicyBulkDeleteRequest $request * @return JsonResponse */ public function bulkDestroy(ShippingPolicyBulkDeleteRequest $request): JsonResponse @@ -245,11 +256,14 @@ class ShippingPolicyController extends AdminBaseController 'messages.shipping_policy.bulk_deleted', ['deleted_count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -257,7 +271,7 @@ class ShippingPolicyController extends AdminBaseController /** * 배송정책 일괄 사용여부 변경 * - * @param ShippingPolicyBulkToggleActiveRequest $request + * @param ShippingPolicyBulkToggleActiveRequest $request * @return JsonResponse */ public function bulkToggleActive(ShippingPolicyBulkToggleActiveRequest $request): JsonResponse @@ -274,11 +288,14 @@ class ShippingPolicyController extends AdminBaseController 'messages.shipping_policy.bulk_toggled', ['updated_count' => $count] ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } @@ -310,7 +327,7 @@ class ShippingPolicyController extends AdminBaseController /** * 기본 배송정책 설정 * - * @param int $id + * @param int $id * @return JsonResponse */ public function setDefault(int $id): JsonResponse @@ -333,11 +350,14 @@ class ShippingPolicyController extends AdminBaseController 'messages.shipping_policy.set_default_success', new ShippingPolicyResource($updatedPolicy) ); + } catch (AccessDeniedHttpException $e) { + return ResponseHelper::forbidden('auth.scope_denied'); } catch (\Exception $e) { + // 서버 결함/인프라 장애 — 4xx 로 뭉개면 장애가 입력 오류로 위장된다 return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'exceptions.operation_failed', - 400 + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/OrderController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/OrderController.php index e26f2e03..8eb3ecb4 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/OrderController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/OrderController.php @@ -8,6 +8,9 @@ use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderCancellationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderModificationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderOptionNotConfirmableException; use Modules\Sirsoft\Ecommerce\Http\Controllers\Traits\HandlesOrderCreation; use Modules\Sirsoft\Ecommerce\Http\Requests\Public\CreateOrderRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Public\GuestCancelOrderRequest; @@ -310,6 +313,12 @@ class OrderController extends PublicBaseController 'sirsoft-ecommerce::messages.order.cancelled', new GuestOrderResource($updatedOrder) ); + } catch (OrderCancellationException $e) { + // 취소 도메인 규칙 위반 — 사용자에게 안내 가능한 상황이므로 422 + return ResponseHelper::error( + 'sirsoft-ecommerce::exceptions.order_cancel_failed', + 422 + ); } catch (Exception $e) { Log::error('비회원 주문 취소 실패', [ 'order_number' => $request->getOrder()->order_number, @@ -317,8 +326,8 @@ class OrderController extends PublicBaseController ]); return ResponseHelper::error( - 'sirsoft-ecommerce::exceptions.order_cancel_failed', - 422 + 'sirsoft-ecommerce::exceptions.operation_failed', + 500 ); } } @@ -376,7 +385,15 @@ class OrderController extends PublicBaseController 'sirsoft-ecommerce::messages.orders.shipping_address_updated', new GuestOrderResource($updatedOrder) ); + } catch (OrderModificationException $e) { + // 도메인 규칙 위반(배송 전 상태 아님 등) — 사용자가 고칠 수 있는 상황이므로 422 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.orders.cannot_modify_address', + 422 + ); } catch (Exception $e) { + // 그 외 예외는 서버 결함/인프라 장애다. 422 로 뭉개면 장애가 입력 오류로 위장된다. Log::error('비회원 배송지 수정 실패', [ 'order_number' => $request->getOrder()->order_number, 'error' => $e->getMessage(), @@ -384,8 +401,8 @@ class OrderController extends PublicBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'messages.orders.cannot_modify_address', - 422 + 'exceptions.order_shipping_address_update_failed', + 500 ); } } @@ -398,6 +415,9 @@ class OrderController extends PublicBaseController * @param int $optionId 주문 옵션 ID (라우트 파라미터) * @return JsonResponse 구매확정 후 주문 상세 */ + // route-param:unused $orderNumber 는 VerifyGuestOrderToken 미들웨어가 토큰↔주문 일치 검증에 + // 사용하고, 본문은 미들웨어가 확정한 주문($request->getOrder())만 쓴다. 본문에서 재사용하면 + // 검증되지 않은 라우트 값으로 주문을 다시 찾는 우회로가 생긴다. public function confirmOption(GuestOrderTokenRequest $request, string $orderNumber, int $optionId): JsonResponse { $order = $request->getOrder(); @@ -417,7 +437,15 @@ class OrderController extends PublicBaseController 'sirsoft-ecommerce::messages.order.confirmed', new GuestOrderResource($updatedOrder) ); + } catch (OrderOptionNotConfirmableException $e) { + // 상태 게이트 위반(이미 확정 / 확정 불가 상태) — 도메인 사유이므로 422. + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.'.$e->getReason(), + 422 + ); } catch (Exception $e) { + // 위 typed 예외를 제외한 나머지는 전부 서버 결함/인프라 장애다. Log::error('비회원 구매확정 실패', [ 'order_number' => $order->order_number, 'option_id' => $optionId, @@ -426,8 +454,8 @@ class OrderController extends PublicBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'exceptions.order_option_cannot_confirm', - 422 + 'exceptions.operation_failed', + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/ProductInquiryController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/ProductInquiryController.php index 48395305..5805127a 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/ProductInquiryController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/ProductInquiryController.php @@ -6,6 +6,7 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\PublicBaseController; use Exception; use Illuminate\Http\JsonResponse; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductInquiryOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\Public\ProductInquiryListRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\Public\StoreInquiryRequest; use Modules\Sirsoft\Ecommerce\Models\Product; @@ -74,16 +75,19 @@ class ProductInquiryController extends PublicBaseController ['id' => $inquiry->id], 201 ); - } catch (\RuntimeException $e) { + } catch (ProductInquiryOperationException $e) { // 실패 사유(문의 게시판 미설정·게시판 모듈 불가 등)를 그대로 보여준다. // 일반 문구만 남기면 서버 기록을 봐야만 원인을 알 수 있다 — 같은 기능의 // 수정·답변 경로는 이미 사유를 노출하고 있어 안내 수준을 맞춘다. + // + // 종전에는 `\RuntimeException` 을 잡아서, 도메인 사유가 아닌 인프라 예외까지 + // 422 로 뭉개고 그 원문을 사유 자리에 실어 보냈다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.inquiries.operation_failed_reason', 422, null, - ['reason' => $e->getMessage()] + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] ); } catch (Exception $e) { return ResponseHelper::moduleError( diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/WishlistController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/WishlistController.php index 94d103a5..6d3895eb 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/WishlistController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/WishlistController.php @@ -47,7 +47,9 @@ class WishlistController extends PublicBaseController 'added' => $result['added'], ]); } catch (Exception $e) { - return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.toggle_failed'); + // 상태코드를 생략하면 ResponseHelper 기본값 400 이 되어 서버 오류가 + // "입력 오류"로 위장된다 — 찜 토글에 도메인 예외는 없다. + return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.toggle_failed', 500); } } @@ -73,7 +75,9 @@ class WishlistController extends PublicBaseController new WishlistCollection($wishlists) ); } catch (Exception $e) { - return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.retrieve_failed'); + // 상태코드를 생략하면 ResponseHelper 기본값 400 이 되어 조회 실패가 + // "입력 오류"로 위장된다 — 목록 조회에 도메인 예외는 없다. + return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.retrieve_failed', 500); } } @@ -97,7 +101,8 @@ class WishlistController extends PublicBaseController return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.wishlist.removed'); } catch (Exception $e) { - return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.delete_failed'); + // 상태코드 생략 시 기본값 400 — 삭제 실패는 서버 오류다 (미존재는 위에서 404 로 분기) + return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.wishlist.delete_failed', 500); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/OrderController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/OrderController.php index 837a4510..134f712e 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/OrderController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/OrderController.php @@ -8,6 +8,12 @@ use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Exceptions\CartOperationException; +use Modules\Sirsoft\Ecommerce\Exceptions\CartQuantityLimitException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderCancellationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderModificationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderOptionNotConfirmableException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderProcessingException; use Modules\Sirsoft\Ecommerce\Http\Requests\User\CancelOrderRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\ConfirmOrderOptionRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\EstimateRefundRequest; @@ -136,6 +142,12 @@ class OrderController extends AuthBaseController 'sirsoft-ecommerce::messages.order.cancelled', new OrderResource($updatedOrder) ); + } catch (OrderCancellationException $e) { + // 취소 도메인 규칙 위반 — 사용자에게 안내 가능한 상황이므로 422 + return ResponseHelper::error( + 'sirsoft-ecommerce::exceptions.order_cancel_failed', + 422 + ); } catch (Exception $e) { Log::error('주문 취소 실패', [ 'order_id' => $request->getOrder()->id, @@ -143,8 +155,8 @@ class OrderController extends AuthBaseController ]); return ResponseHelper::error( - 'sirsoft-ecommerce::exceptions.order_cancel_failed', - 422 + 'sirsoft-ecommerce::exceptions.operation_failed', + 500 ); } } @@ -211,7 +223,15 @@ class OrderController extends AuthBaseController return ResponseHelper::success('sirsoft-ecommerce::messages.orders.shipping_address_updated', [ 'order' => new OrderResource($order), ]); + } catch (OrderModificationException $e) { + // 도메인 규칙 위반(배송 전 상태 아님, 타인 주소 지정 등) — 422 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.orders.cannot_modify_address', + 422 + ); } catch (Exception $e) { + // 그 외 예외는 서버 결함/인프라 장애다. 422 로 뭉개면 장애가 입력 오류로 위장된다. Log::error('Order shipping address update failed', [ 'message' => $e->getMessage(), 'order_id' => $id, @@ -220,8 +240,8 @@ class OrderController extends AuthBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'messages.orders.cannot_modify_address', - 422 + 'exceptions.order_shipping_address_update_failed', + 500 ); } } @@ -249,7 +269,15 @@ class OrderController extends AuthBaseController 'sirsoft-ecommerce::messages.order.confirmed', ['order' => new OrderResource($updatedOrder)] ); + } catch (OrderOptionNotConfirmableException $e) { + // 상태 게이트 위반(이미 확정 / 확정 불가 상태) — 도메인 사유이므로 422. + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.'.$e->getReason(), + 422 + ); } catch (Exception $e) { + // 위 typed 예외를 제외한 나머지는 전부 서버 결함/인프라 장애다. Log::error('Order option confirm failed', [ 'message' => $e->getMessage(), 'order_id' => $request->route('id'), @@ -259,8 +287,8 @@ class OrderController extends AuthBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'exceptions.order_option_cannot_confirm', - 422 + 'exceptions.operation_failed', + 500 ); } } @@ -286,6 +314,13 @@ class OrderController extends AuthBaseController 'sirsoft-ecommerce::messages.cart.reorder_added', $result ); + } catch (OrderProcessingException|CartOperationException|CartQuantityLimitException $e) { + // 재주문 도메인 규칙 위반(장바구니 담기 불가/수량 한도 등) — 422 + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.cart.reorder_failed', + 422 + ); } catch (Exception $e) { Log::error('Order reorder failed', [ 'message' => $e->getMessage(), @@ -295,8 +330,8 @@ class OrderController extends AuthBaseController return ResponseHelper::moduleError( 'sirsoft-ecommerce', - 'messages.cart.reorder_failed', - 422 + 'exceptions.operation_failed', + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductInquiryController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductInquiryController.php index 72ac9ab5..901f1140 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductInquiryController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductInquiryController.php @@ -8,6 +8,7 @@ use App\Http\Controllers\Api\Base\AuthBaseController; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductInquiryOperationException; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UpdateInquiryReplyRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UpdateInquiryRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UserInquiryListRequest; @@ -76,15 +77,19 @@ class ProductInquiryController extends AuthBaseController $this->inquiryService->updateInquiry($inquiryId, $request->validated()); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.updated', ['id' => $inquiryId]); - } catch (\RuntimeException $e) { - // 서비스가 던지는 RuntimeException 은 생성 시점에 __() 로 번역된 사용자 문구다. - // 사유를 파라미터로 넘겨 실패 원인이 화면에 그대로 남게 한다. + } catch (ProductInquiryOperationException $e) { + // 실패 사유(문의 게시판 미설정 등)를 그대로 보여준다 — 일반 문구만 남기면 + // 서버 기록을 봐야만 원인을 알 수 있다. + // + // 종전에는 `\RuntimeException` 을 잡았다. 서비스가 도메인 실패를 typed 로 + // 승격한 뒤로 그 catch 에 남는 것은 인프라 예외뿐이었고, 그것까지 422 + + // 예외 원문으로 뭉갰다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.inquiries.operation_failed_reason', 422, null, - ['reason' => $e->getMessage()] + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] ); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.update_failed', 500); @@ -114,6 +119,17 @@ class ProductInquiryController extends AuthBaseController $this->inquiryService->deleteInquiry($inquiryId); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.deleted', ['deleted' => true]); + } catch (ProductInquiryOperationException $e) { + // `deleteInquiry` 도 도메인 실패를 typed 로 던지는데 여기에는 그것을 구분하는 + // catch 가 없어, 운영자가 고칠 수 있는 사유(문의 게시판 미설정)가 일반 500 + // 문구로 나갔다. 형제 메서드(수정·답변)와 같은 폭으로 맞춘다. + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'messages.inquiries.operation_failed_reason', + 422, + null, + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] + ); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.delete_failed', 500); } @@ -144,15 +160,19 @@ class ProductInquiryController extends AuthBaseController ['id' => $inquiry->id, 'is_answered' => $inquiry->is_answered], 201 ); - } catch (\RuntimeException $e) { - // 서비스가 던지는 RuntimeException 은 생성 시점에 __() 로 번역된 사용자 문구다. - // 사유를 파라미터로 넘겨 실패 원인이 화면에 그대로 남게 한다. + } catch (ProductInquiryOperationException $e) { + // 실패 사유(문의 게시판 미설정 등)를 그대로 보여준다 — 일반 문구만 남기면 + // 서버 기록을 봐야만 원인을 알 수 있다. + // + // 종전에는 `\RuntimeException` 을 잡았다. 서비스가 도메인 실패를 typed 로 + // 승격한 뒤로 그 catch 에 남는 것은 인프라 예외뿐이었고, 그것까지 422 + + // 예외 원문으로 뭉갰다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.inquiries.operation_failed_reason', 422, null, - ['reason' => $e->getMessage()] + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] ); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_failed', 500); @@ -179,15 +199,19 @@ class ProductInquiryController extends AuthBaseController $this->inquiryService->updateReply($inquiryId, $request->validated()); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.reply_updated', ['id' => $inquiryId]); - } catch (\RuntimeException $e) { - // 서비스가 던지는 RuntimeException 은 생성 시점에 __() 로 번역된 사용자 문구다. - // 사유를 파라미터로 넘겨 실패 원인이 화면에 그대로 남게 한다. + } catch (ProductInquiryOperationException $e) { + // 실패 사유(문의 게시판 미설정 등)를 그대로 보여준다 — 일반 문구만 남기면 + // 서버 기록을 봐야만 원인을 알 수 있다. + // + // 종전에는 `\RuntimeException` 을 잡았다. 서비스가 도메인 실패를 typed 로 + // 승격한 뒤로 그 catch 에 남는 것은 인프라 예외뿐이었고, 그것까지 422 + + // 예외 원문으로 뭉갰다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.inquiries.operation_failed_reason', 422, null, - ['reason' => $e->getMessage()] + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] ); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_update_failed', 500); @@ -213,15 +237,19 @@ class ProductInquiryController extends AuthBaseController $this->inquiryService->deleteReply($inquiryId); return ResponseHelper::moduleSuccess('sirsoft-ecommerce', 'messages.inquiries.reply_deleted', ['deleted' => true]); - } catch (\RuntimeException $e) { - // 서비스가 던지는 RuntimeException 은 생성 시점에 __() 로 번역된 사용자 문구다. - // 사유를 파라미터로 넘겨 실패 원인이 화면에 그대로 남게 한다. + } catch (ProductInquiryOperationException $e) { + // 실패 사유(문의 게시판 미설정 등)를 그대로 보여준다 — 일반 문구만 남기면 + // 서버 기록을 봐야만 원인을 알 수 있다. + // + // 종전에는 `\RuntimeException` 을 잡았다. 서비스가 도메인 실패를 typed 로 + // 승격한 뒤로 그 catch 에 남는 것은 인프라 예외뿐이었고, 그것까지 422 + + // 예외 원문으로 뭉갰다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.inquiries.operation_failed_reason', 422, null, - ['reason' => $e->getMessage()] + ['reason' => __($e->getMessageKey(), $e->getMessageParams())] ); } catch (Exception $e) { return ResponseHelper::moduleError('sirsoft-ecommerce', 'messages.inquiries.reply_delete_failed', 500); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductReviewController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductReviewController.php index 5c8944dd..16380889 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductReviewController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ProductReviewController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Api\Base\AuthBaseController; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; +use Modules\Sirsoft\Ecommerce\Exceptions\ReviewNotWritableException; use Modules\Sirsoft\Ecommerce\Http\Requests\User\StoreReviewRequest; use Modules\Sirsoft\Ecommerce\Http\Resources\ProductReviewResource; use Modules\Sirsoft\Ecommerce\Models\ProductReview; @@ -69,8 +70,17 @@ class ProductReviewController extends AuthBaseController new ProductReviewResource($review), 201 ); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ReviewNotWritableException $e) { + // 작성 자격 미충족은 도메인 검증 실패 → 422 + 전용 메시지 (메시지 키 + 사유 파라미터). + // 예외 메시지 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + // 사유는 원시 식별자(already_written)가 아니라 번역 라벨로 싣는다. + return ResponseHelper::error( + 'messages.reviews.cannot_write', + 422, + null, + ['reason' => $e->getReasonLabel()], + 'sirsoft-ecommerce' + ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ReviewImageController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ReviewImageController.php index 97bcd2af..70e120c7 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ReviewImageController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/ReviewImageController.php @@ -6,7 +6,9 @@ use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AuthBaseController; use Exception; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Modules\Sirsoft\Ecommerce\Exceptions\ReviewImageUploadLimitException; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UploadReviewImageRequest; use Modules\Sirsoft\Ecommerce\Http\Resources\ProductReviewImageResource; use Modules\Sirsoft\Ecommerce\Models\ProductReview; @@ -54,8 +56,16 @@ class ReviewImageController extends AuthBaseController new ProductReviewImageResource($image), 201 ); - } catch (\RuntimeException $e) { - return ResponseHelper::error($e->getMessage(), 422); + } catch (ReviewImageUploadLimitException $e) { + // 개수 상한 초과는 도메인 검증 실패 → 422 + 전용 메시지 (메시지 키 + 파라미터). + // 예외 메시지 원문을 키 자리에 넘기면 키 해석에 실패해 원문이 그대로 노출된다. + return ResponseHelper::error( + 'review.image_upload_limit_exceeded', + 422, + null, + ['max' => $e->maxImages], + 'sirsoft-ecommerce' + ); } catch (Exception $e) { return ResponseHelper::moduleError( 'sirsoft-ecommerce', @@ -113,12 +123,21 @@ class ReviewImageController extends AuthBaseController /** * 리뷰 이미지 다운로드 (해시 기반 공개 서빙) * + * 숨김(HIDDEN) 리뷰의 이미지는 서비스의 상태 게이트가 차단하되, 게이트를 통과한 + * 응답 직렬화(ProductReviewResource)가 발급한 한시 서명 URL 은 동등한 자격으로 + * 허용한다 — <img> 는 Authorization 헤더를 실을 수 없는 렌더 경로이기 때문이다. + * + * @param Request $request HTTP 요청 객체 (한시 서명 검증용) * @param string $hash 이미지 해시 (12자) * @return StreamedResponse|JsonResponse 이미지 스트림 또는 404 응답 */ - public function download(string $hash): StreamedResponse|JsonResponse + // audit:allow controller-base-request-injection reason: GET 이미지 서빙. 요청 URL 의 한시 서명 검증(hasValidSignature)만 수행. 검증할 body 없음(hash 는 라우트 파라미터) + public function download(Request $request, string $hash): StreamedResponse|JsonResponse { - $response = $this->imageService->download($hash); + $response = $this->imageService->download( + $hash, + signatureVerified: $request->hasValidSignature(absolute: false) + ); if (! $response) { return ResponseHelper::moduleError( diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserAddressController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserAddressController.php index d3b4daaf..420930f2 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserAddressController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserAddressController.php @@ -34,7 +34,7 @@ class UserAddressController extends AuthBaseController public function index(): JsonResponse { try { - + $userId = Auth::id(); $addresses = $this->userAddressService->getUserAddresses($userId); @@ -58,13 +58,13 @@ class UserAddressController extends AuthBaseController /** * 사용자 배송지 상세 조회 * - * @param int $id 배송지 ID + * @param int $id 배송지 ID * @return JsonResponse 배송지 정보를 포함한 JSON 응답 */ public function show(int $id): JsonResponse { try { - + $userId = Auth::id(); $address = $this->userAddressService->getAddress($userId, $id); @@ -97,13 +97,13 @@ class UserAddressController extends AuthBaseController /** * 사용자 배송지 등록 * - * @param StoreUserAddressRequest $request 검증된 요청 데이터 + * @param StoreUserAddressRequest $request 검증된 요청 데이터 * @return JsonResponse 생성된 배송지 정보를 포함한 JSON 응답 */ public function store(StoreUserAddressRequest $request): JsonResponse { try { - + $userId = Auth::id(); $data = $request->validated(); $data['user_id'] = $userId; @@ -126,15 +126,9 @@ class UserAddressController extends AuthBaseController 'user_id' => Auth::id(), ]); - // 최대 배송지 개수 초과 예외 처리 - if (str_contains($e->getMessage(), 'max_addresses_exceeded')) { - return ResponseHelper::moduleError( - 'sirsoft-ecommerce', - 'exceptions.max_addresses_exceeded', - 422 - ); - } - + // 종전에는 예외 메시지 문자열을 검사해 422 로 분기했으나, 그 문자열을 던지는 + // 지점도 대응 다국어 키도 존재하지 않아 도달 불가능한 분기였다. 개수 상한이 + // 도입되면 전용 예외 타입으로 분기한다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', 'messages.address.create_failed', @@ -146,14 +140,14 @@ class UserAddressController extends AuthBaseController /** * 사용자 배송지 수정 * - * @param UpdateUserAddressRequest $request 검증된 요청 데이터 - * @param int $id 배송지 ID + * @param UpdateUserAddressRequest $request 검증된 요청 데이터 + * @param int $id 배송지 ID * @return JsonResponse 수정된 배송지 정보를 포함한 JSON 응답 */ public function update(UpdateUserAddressRequest $request, int $id): JsonResponse { try { - + $userId = Auth::id(); $data = $request->validated(); @@ -188,13 +182,13 @@ class UserAddressController extends AuthBaseController /** * 사용자 배송지 삭제 * - * @param int $id 배송지 ID + * @param int $id 배송지 ID * @return JsonResponse 삭제 결과를 포함한 JSON 응답 */ public function destroy(int $id): JsonResponse { try { - + $userId = Auth::id(); $deleted = $this->userAddressService->deleteAddress($userId, $id); @@ -225,13 +219,13 @@ class UserAddressController extends AuthBaseController /** * 기본 배송지 설정 * - * @param int $id 배송지 ID + * @param int $id 배송지 ID * @return JsonResponse 설정 결과를 포함한 JSON 응답 */ public function setDefault(int $id): JsonResponse { try { - + $userId = Auth::id(); $result = $this->userAddressService->setDefaultAddress($userId, $id); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserCouponController.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserCouponController.php index fe145c1e..cb23f6c6 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserCouponController.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/User/UserCouponController.php @@ -7,6 +7,8 @@ use App\Http\Controllers\Api\Base\AuthBaseController; use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Exceptions\CouponNotIssuableException; use Modules\Sirsoft\Ecommerce\Http\Requests\User\DownloadCouponRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UserCouponAvailableRequest; use Modules\Sirsoft\Ecommerce\Http\Requests\User\UserCouponDownloadableRequest; @@ -28,7 +30,7 @@ class UserCouponController extends AuthBaseController /** * 사용자 쿠폰함 목록 조회 * - * @param Request $request 요청 데이터 + * @param Request $request 요청 데이터 * @return JsonResponse 쿠폰 목록을 포함한 JSON 응답 */ public function index(UserCouponListRequest $request): JsonResponse @@ -58,7 +60,7 @@ class UserCouponController extends AuthBaseController /** * 체크아웃에서 사용 가능한 쿠폰 목록 조회 * - * @param Request $request 요청 데이터 + * @param Request $request 요청 데이터 * @return JsonResponse 사용 가능한 쿠폰 목록을 포함한 JSON 응답 */ public function available(UserCouponAvailableRequest $request): JsonResponse @@ -86,7 +88,7 @@ class UserCouponController extends AuthBaseController /** * 다운로드 가능한 쿠폰 목록 조회 * - * @param Request $request 요청 데이터 + * @param Request $request 요청 데이터 * @return JsonResponse 다운로드 가능 쿠폰 목록 */ public function downloadable(UserCouponDownloadableRequest $request): JsonResponse @@ -115,8 +117,8 @@ class UserCouponController extends AuthBaseController /** * 쿠폰 다운로드 (발급) * - * @param DownloadCouponRequest $request 검증된 요청 - * @param int $couponId 쿠폰 ID + * @param DownloadCouponRequest $request 검증된 요청 + * @param int $couponId 쿠폰 ID * @return JsonResponse 생성된 발급 정보 */ public function download(DownloadCouponRequest $request, int $couponId): JsonResponse @@ -133,11 +135,25 @@ class UserCouponController extends AuthBaseController $couponIssue, 201 ); - } catch (Exception $e) { + } catch (CouponNotIssuableException $e) { + // 발급 불가 사유는 예외가 식별자로 들고 있다 — 번역된 메시지를 다시 키로 + // 넘기면(종전 동작) 키 해석에 실패해 원문이 그대로 노출된다. return ResponseHelper::moduleError( 'sirsoft-ecommerce', - $e->getMessage(), - $e->getCode() ?: 400 + 'messages.coupon.'.$e->getReason(), + 400 + ); + } catch (Exception $e) { + Log::error('쿠폰 다운로드 실패', [ + 'coupon_id' => $couponId, + 'user_id' => Auth::id(), + 'error' => $e->getMessage(), + ]); + + return ResponseHelper::moduleError( + 'sirsoft-ecommerce', + 'exceptions.operation_failed', + 500 ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreCouponRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreCouponRequest.php index 48d41ddb..9f81611c 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreCouponRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreCouponRequest.php @@ -72,6 +72,14 @@ class StoreCouponRequest extends FormRequest $merge['min_order_amount'] = 0; } + // 라디오가 문자열 "true"/"false" 를 보낼 수 있어 boolean 으로 정규화합니다. (해석 불가값은 유지 → boolean 규칙이 422 처리) + if ($this->has('is_combinable')) { + $normalized = filter_var($this->input('is_combinable'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($normalized !== null) { + $merge['is_combinable'] = $normalized; + } + } + if ($merge) { $this->merge($merge); } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreEcommerceSettingsRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreEcommerceSettingsRequest.php index 2342321c..99f8602e 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreEcommerceSettingsRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreEcommerceSettingsRequest.php @@ -3,9 +3,9 @@ namespace Modules\Sirsoft\Ecommerce\Http\Requests\Admin; use App\Rules\LocaleRequiredTranslatable; +use App\Services\DriverRegistryService; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Validator; -use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\ShippingFeeTaxPolicy; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductRepositoryInterface; @@ -206,6 +206,13 @@ class StoreEcommerceSettingsRequest extends FormRequest 'basic_info.privacy_officer_email' => ['nullable', 'email', 'max:255'], 'basic_info.mail_order_number' => ['nullable', 'string', 'max:100'], 'basic_info.telecom_number' => ['nullable', 'string', 'max:100'], + // 공개 자산 디스크 — 빈 값은 코어 공개 자산 디스크 설정을 따른다. + // 선택지가 코어 카탈로그 + 플러그인 훅 등록 디스크로 동적이라 정적 in 불가 — + // 코어 SaveSettingsRequest 와 동일하게 카탈로그 조회 closure 로 검증한다. + // (런타임 resolvePublicAssetDisk() 의 스트리밍 폴백은 최후 안전망이지, 저장 + // 시점의 오타를 성공 응답으로 통과시켜도 된다는 뜻이 아니다 — 운영자에게는 + // "저장됐는데 CDN 이 안 붙는" 무증상 상태로만 보인다.) + 'basic_info.public_asset_disk' => ['nullable', 'string', 'max:100', $this->publicAssetDiskRule()], 'language_currency' => ['sometimes', 'array'], // default_language 제거 (A1-⑤, D-LANG): 사이트 언어는 코어 일반설정으로 일원화. 모듈 orphan 필드. @@ -364,6 +371,28 @@ class StoreEcommerceSettingsRequest extends FormRequest ]; } + /** + * 공개 자산 디스크 존재 검증 규칙을 만듭니다. + * + * 선택지는 코어 3종 + 플러그인이 훅으로 등록한 디스크라 런타임에만 확정되므로 + * 정적 `Rule::in` 을 쓸 수 없습니다. 코어 `SaveSettingsRequest` 와 동일하게 + * 카탈로그 게터(SSoT)를 조회하는 closure 로 검증해 두 표면의 강도를 맞춥니다. + * + * @return \Closure 검증 closure + */ + protected function publicAssetDiskRule(): \Closure + { + return function (string $attribute, mixed $value, \Closure $fail): void { + if ($value === null || $value === '') { + return; + } + + if (! app(DriverRegistryService::class)->isDriverAvailable('public_asset', $value)) { + $fail(__('sirsoft-ecommerce::validation.public_asset_disk_invalid')); + } + }; + } + /** * 검증 속성명 다국어 처리 * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreShippingPolicyRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreShippingPolicyRequest.php index 2d95e452..3d0bf0b0 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreShippingPolicyRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/StoreShippingPolicyRequest.php @@ -13,12 +13,11 @@ use Modules\Sirsoft\Ecommerce\Enums\ShippingApiAuthType; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiHttpMethod; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiRequestField; use Modules\Sirsoft\Ecommerce\Enums\ShippingApiResponseType; -use Modules\Sirsoft\Ecommerce\Models\ShippingType; +use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingTypeRepositoryInterface; /** * 배송정책 생성 요청 */ -// audit:allow api-doc-coverage reason: 이번 변경은 messages() 의 다국어 키 경로 정정뿐이다. 요청 파라미터·응답 구조·검증 규칙이 그대로라 docs/api/shipping-policies.md 에 갱신할 내용이 없다 (422 에러표는 개별 메시지 문구를 기재하지 않음). class StoreShippingPolicyRequest extends FormRequest { /** @@ -48,7 +47,12 @@ class StoreShippingPolicyRequest extends FormRequest // 국가별 설정 (최소 1개 필수) 'country_settings' => ['required', 'array', 'min:1'], 'country_settings.*.country_code' => ['required', 'string', 'max:10', 'distinct'], - 'country_settings.*.shipping_method' => ['required', 'string', Rule::in(ShippingType::pluck('code')->toArray())], + 'country_settings.*.shipping_method' => [ + 'required', + 'string', + // audit:allow query-unbounded-get reason: 배송유형은 운영자 등록 설정성 테이블 — 허용 어휘 구성용 전량 조회이며 행 수가 데이터 증가에 비례하지 않는다 + Rule::in(app(ShippingTypeRepositoryInterface::class)->getAll()->pluck('code')->toArray()), + ], 'country_settings.*.custom_shipping_name' => ['nullable', 'array'], 'country_settings.*.custom_shipping_name.*' => ['nullable', 'string', 'max:100'], // 통화는 서버가 상점 기본 통화로 강제하므로 클라이언트 입력에 의존하지 않는다 (읽기전용 표시). @@ -141,6 +145,9 @@ class StoreShippingPolicyRequest extends FormRequest 'country_settings.*.ranges.tiers.*.min.min' => __('sirsoft-ecommerce::validation.shipping_policy.ranges.tier_min_non_negative'), 'country_settings.*.ranges.tiers.*.max.min' => __('sirsoft-ecommerce::validation.shipping_policy.ranges.tier_max_non_negative'), 'country_settings.*.ranges.unit_value.min' => __('sirsoft-ecommerce::validation.shipping_policy.ranges.unit_value_min'), + 'country_settings.*.ranges.tiers.min' => __('sirsoft-ecommerce::validation.shipping_policy.ranges.tiers_required'), + 'country_settings.*.ranges.tiers.array' => __('sirsoft-ecommerce::validation.shipping_policy.ranges.tiers_required'), + 'country_settings.*.extra_fee_settings.*.zipcode.required' => __('sirsoft-ecommerce::validation.shipping_policy.extra_fee.zipcode_format'), 'country_settings.*.extra_fee_settings.*.fee.min' => __('sirsoft-ecommerce::validation.extra_fee_template.fee_min'), 'is_active.required' => __('sirsoft-ecommerce::validation.shipping_policy.is_active_required'), ]; @@ -158,14 +165,125 @@ class StoreShippingPolicyRequest extends FormRequest public function withValidator(Validator $validator): void { $validator->after(function (Validator $validator) { + $this->validatePolicyRequiredSettings($validator); $this->validateRangeTiersContinuity($validator); $this->validateNonFreeBaseFee($validator); $this->validateCustomShippingName($validator); $this->validateApiEndpointRequired($validator); $this->validateApiConfig($validator); + $this->validateExtraFeeZipcodeFormat($validator); }); } + /** + * 부과정책이 요구하는 설정값의 존재를 검증합니다 (cross-field). + * + * 이 값들이 비어 있으면 계산 결과가 조용히 0원(무료배송)이 되므로 저장 시점에 차단합니다. + * + * - 구간별 정책: ranges.tiers 최소 1개 + * - 단위당 정책: ranges.unit_value + * - 조건부 무료: free_threshold + * + * @param Validator $validator Laravel validator 인스턴스 + */ + private function validatePolicyRequiredSettings(Validator $validator): void + { + $countrySettings = $this->input('country_settings', []); + + if (! is_array($countrySettings)) { + return; + } + + foreach ($countrySettings as $i => $cs) { + $policy = ChargePolicyEnum::tryFrom($cs['charge_policy'] ?? ''); + + if (! $policy) { + continue; + } + + if ($policy->requiresRanges()) { + $tiers = $cs['ranges']['tiers'] ?? null; + + if (! is_array($tiers) || count($tiers) === 0) { + $validator->errors()->add( + "country_settings.{$i}.ranges.tiers", + __('sirsoft-ecommerce::validation.shipping_policy.ranges.tiers_required') + ); + } + } + + if ($policy->requiresUnitValue()) { + $unitValue = $cs['ranges']['unit_value'] ?? null; + + if ($unitValue === null || $unitValue === '' || ! is_numeric($unitValue)) { + $validator->errors()->add( + "country_settings.{$i}.ranges.unit_value", + __('sirsoft-ecommerce::validation.shipping_policy.ranges.unit_value_required') + ); + } + } + + if ($policy->requiresFreeThreshold()) { + $freeThreshold = $cs['free_threshold'] ?? null; + + if ($freeThreshold === null || $freeThreshold === '' || ! is_numeric($freeThreshold)) { + $validator->errors()->add( + "country_settings.{$i}.free_threshold", + __('sirsoft-ecommerce::validation.shipping_policy.free_threshold_required') + ); + } + } + } + } + + /** + * 도서산간 우편번호 패턴의 형식을 검증합니다. + * + * 지원 형식은 정확일치("63000"), 범위("63000-63999"), 접두 와일드카드("63*") 3종입니다. + * 그 외 형식은 어떤 우편번호에도 매칭되지 않아 추가배송비가 조용히 누락됩니다. + * + * @param Validator $validator Laravel validator 인스턴스 + */ + private function validateExtraFeeZipcodeFormat(Validator $validator): void + { + $countrySettings = $this->input('country_settings', []); + + if (! is_array($countrySettings)) { + return; + } + + foreach ($countrySettings as $i => $cs) { + if (empty($cs['extra_fee_enabled'])) { + continue; + } + + $settings = $cs['extra_fee_settings'] ?? []; + + if (! is_array($settings)) { + continue; + } + + foreach ($settings as $j => $setting) { + $zipcode = trim((string) ($setting['zipcode'] ?? '')); + + if ($zipcode === '') { + continue; + } + + $isValid = preg_match('/^\d+-\d+$/', $zipcode) + || preg_match('/^\d+\*$/', $zipcode) + || preg_match('/^\d+$/', $zipcode); + + if (! $isValid) { + $validator->errors()->add( + "country_settings.{$i}.extra_fee_settings.{$j}.zipcode", + __('sirsoft-ecommerce::validation.shipping_policy.extra_fee.zipcode_format') + ); + } + } + } + } + /** * 계산 API 정책(api) 선택 시 api_endpoint 필수 검증 (cross-field). * @@ -234,8 +352,11 @@ class StoreShippingPolicyRequest extends FormRequest * * - 첫 구간의 시작값은 0이어야 합니다. * - 마지막 구간의 종료값은 null(무제한)이어야 합니다. + * - 마지막을 제외한 구간에는 종료값이 있어야 합니다 (중간 무제한 구간 금지). * - 시작값이 종료값보다 작아야 합니다. - * - 구간이 연속적이어야 합니다 (현재 max + 1 === 다음 min, 포함 범위 기준). + * - 구간이 연속적이어야 합니다. 연결 규칙은 값의 성질에 따라 다릅니다. + * - 이산형(수량): 다음 min === 현재 max + 1 + * - 연속형(금액/무게/부피): 다음 min === 현재 max (부동소수 오차 허용) * - 배송비는 0 이상이어야 합니다. */ private function validateRangeTiersContinuity(Validator $validator): void @@ -246,6 +367,9 @@ class StoreShippingPolicyRequest extends FormRequest return; } + // 연속형 구간의 소수 입력(2.5kg)에서 부동소수 오차로 정상 설정이 거부되지 않도록 허용 오차를 둔다. + $epsilon = 0.001; + foreach ($countrySettings as $i => $cs) { $tiers = $cs['ranges']['tiers'] ?? null; @@ -253,8 +377,11 @@ class StoreShippingPolicyRequest extends FormRequest continue; } - // 첫 구간 min은 0이어야 함 - if (($tiers[0]['min'] ?? null) != 0) { + $policy = ChargePolicyEnum::tryFrom($cs['charge_policy'] ?? ''); + $isDiscrete = $policy?->hasDiscreteRangeValues() ?? false; + + // 첫 구간 min은 0이어야 함 (문자열 "0" 도 허용) + if ((float) ($tiers[0]['min'] ?? null) != 0.0) { $validator->errors()->add( "country_settings.{$i}.ranges.tiers.0.min", __('sirsoft-ecommerce::validation.shipping_policy.ranges.first_min_zero') @@ -263,7 +390,7 @@ class StoreShippingPolicyRequest extends FormRequest // 마지막 구간 max는 null이어야 함 $lastIdx = count($tiers) - 1; - if (isset($tiers[$lastIdx]['max']) && $tiers[$lastIdx]['max'] !== null) { + if (! $this->isUnlimitedMax($tiers[$lastIdx]['max'] ?? null)) { $validator->errors()->add( "country_settings.{$i}.ranges.tiers.{$lastIdx}.max", __('sirsoft-ecommerce::validation.shipping_policy.ranges.last_max_unlimited') @@ -272,9 +399,35 @@ class StoreShippingPolicyRequest extends FormRequest for ($j = 0; $j < count($tiers); $j++) { $tier = $tiers[$j]; + $hasMax = ! $this->isUnlimitedMax($tier['max'] ?? null); + + // 중간 구간의 종료값 누락 금지 (뒤 구간이 영구히 도달 불가해진다) + if ($j < $lastIdx && ! $hasMax) { + $validator->errors()->add( + "country_settings.{$i}.ranges.tiers.{$j}.max", + __('sirsoft-ecommerce::validation.shipping_policy.ranges.middle_max_required') + ); + + continue; + } + + // 수량 구간의 경계값은 정수 + if ($isDiscrete) { + $minValue = $tier['min'] ?? 0; + $maxValue = $tier['max'] ?? null; + $hasFractionalBoundary = (is_numeric($minValue) && floor((float) $minValue) != (float) $minValue) + || ($hasMax && is_numeric($maxValue) && floor((float) $maxValue) != (float) $maxValue); + + if ($hasFractionalBoundary) { + $validator->errors()->add( + "country_settings.{$i}.ranges.tiers.{$j}.max", + __('sirsoft-ecommerce::validation.shipping_policy.ranges.tier_value_integer') + ); + } + } // min < max (마지막 구간 제외) - if ($j < $lastIdx && isset($tier['max']) && $tier['max'] !== null) { + if ($j < $lastIdx && $hasMax) { if ((float) ($tier['min'] ?? 0) >= (float) $tier['max']) { $validator->errors()->add( "country_settings.{$i}.ranges.tiers.{$j}.min", @@ -283,11 +436,12 @@ class StoreShippingPolicyRequest extends FormRequest } } - // 구간 연속성: 현재 max + 1 === 다음 min (포함 범위 기준) - if ($j < $lastIdx) { - $nextTier = $tiers[$j + 1]; - if (isset($tier['max']) && $tier['max'] !== null - && (float) $tier['max'] + 1 !== (float) ($nextTier['min'] ?? 0)) { + // 구간 연속성 (이산형은 max + 1, 연속형은 max 가 다음 min) + if ($j < $lastIdx && $hasMax) { + $expectedNextMin = (float) $tier['max'] + ($isDiscrete ? 1 : 0); + $actualNextMin = (float) ($tiers[$j + 1]['min'] ?? 0); + + if (abs($expectedNextMin - $actualNextMin) >= $epsilon) { $validator->errors()->add( "country_settings.{$i}.ranges.tiers.{$j}.max", __('sirsoft-ecommerce::validation.shipping_policy.ranges.continuity') @@ -306,6 +460,17 @@ class StoreShippingPolicyRequest extends FormRequest } } + /** + * 구간의 종료값이 무제한(비어 있음)인지 판정합니다. + * + * @param mixed $max 종료값 + * @return bool 무제한이면 true + */ + private function isUnlimitedMax(mixed $max): bool + { + return $max === null || $max === ''; + } + /** * 무료배송이 아닌 정책에서 배송비 0원을 금지합니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/UpdateCouponRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/UpdateCouponRequest.php index e786735f..a4dbaecb 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/UpdateCouponRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Admin/UpdateCouponRequest.php @@ -72,6 +72,14 @@ class UpdateCouponRequest extends FormRequest $merge['min_order_amount'] = 0; } + // 라디오가 문자열 "true"/"false" 를 보낼 수 있어 boolean 으로 정규화합니다. (해석 불가값은 유지 → boolean 규칙이 422 처리) + if ($this->has('is_combinable')) { + $normalized = filter_var($this->input('is_combinable'), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + if ($normalized !== null) { + $merge['is_combinable'] = $normalized; + } + } + if ($merge) { $this->merge($merge); } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Concerns/ValidatesCancelItems.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Concerns/ValidatesCancelItems.php new file mode 100644 index 00000000..0f60ed5c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Concerns/ValidatesCancelItems.php @@ -0,0 +1,65 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Http\Requests\Concerns; + +use Illuminate\Contracts\Validation\Validator; +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\Order; + +/** + * 취소/환불 대상 항목(items) per-item 검증 공통 로직 + * + * 회원(EstimateRefundRequest / CancelOrderRequest)과 비회원 + * (GuestEstimateRefundRequest / GuestCancelOrderRequest) 경로가 동일한 강도로 + * 항목을 검증하도록 단일 지점에서 판정합니다. 각 항목이 대상 주문에 속하는지, + * 이미 취소된 옵션인지, 취소 수량이 보유 수량을 넘지 않는지 확인합니다. + */ +trait ValidatesCancelItems +{ + /** + * 취소/환불 대상 항목이 주문에 속하고 취소 가능한지 검증합니다. + * + * @param Validator $validator 검증기 + * @param Order $order 대상 주문(소유권은 상위에서 이미 검증됨) + * @param array<int, array{order_option_id?: int, cancel_quantity?: int}> $items 검증 대상 항목 배열 + * @return void + */ + protected function validateCancelItemsAgainstOrder(Validator $validator, Order $order, array $items): void + { + $order->loadMissing('options'); + + foreach ($items as $index => $item) { + $optionId = $item['order_option_id'] ?? null; + $option = $order->options->firstWhere('id', $optionId); + + if (! $option) { + $validator->errors()->add( + "items.{$index}.order_option_id", + __('sirsoft-ecommerce::exceptions.order_option_not_found') + ); + + continue; + } + + // 이미 취소된 옵션은 제외 + if ($option->option_status === OrderStatusEnum::CANCELLED) { + $validator->errors()->add( + "items.{$index}.order_option_id", + __('sirsoft-ecommerce::exceptions.order_option_already_cancelled') + ); + + continue; + } + + // 취소 수량이 현재 수량을 초과하는지 검증 + if (($item['cancel_quantity'] ?? 0) > $option->quantity) { + $validator->errors()->add( + "items.{$index}.cancel_quantity", + __('sirsoft-ecommerce::exceptions.cancel_quantity_exceeds', [ + 'max' => $option->quantity, + ]) + ); + } + } + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php index f3ad66f9..799695d2 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/CreateOrderRequest.php @@ -68,6 +68,16 @@ class CreateOrderRequest extends FormRequest 'required', 'string', Rule::in(app(PaymentMethodResolver::class)->allValidIds()), + // 지금 주문을 받을 수 있는 수단인지 별도 판정 (A2 / 공개 #111 서버 대칭). + // 공급 확장이 사라진 수단, 지정 PG 가 사라진 수단은 화이트리스트를 통과하지만 + // 주문하면 PG 라우팅이 매칭에 실패해 결제창 없이 주문완료로 넘어간다. + // 화이트리스트(allValidIds) 자체는 조이지 않는다 — 과거 주문 목록 필터가 + // 같은 목록을 쓰므로, 조이면 예전 수단으로 결제한 주문을 조회할 수 없게 된다. + function (string $attribute, mixed $value, callable $fail) { + if (is_string($value) && ! app(PaymentMethodResolver::class)->isOrderable($value)) { + $fail(__('sirsoft-ecommerce::validation.order.payment_method_unavailable')); + } + }, ], 'expected_total_amount' => 'required|numeric|min:0', diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestCancelOrderRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestCancelOrderRequest.php index 5b479e4d..475c602e 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestCancelOrderRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestCancelOrderRequest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Modules\Sirsoft\Ecommerce\Enums\RefundPriorityEnum; +use Modules\Sirsoft\Ecommerce\Http\Requests\Concerns\ValidatesCancelItems; use Modules\Sirsoft\Ecommerce\Models\ClaimReason; use Modules\Sirsoft\Ecommerce\Models\Order; @@ -19,6 +20,8 @@ use Modules\Sirsoft\Ecommerce\Models\Order; */ class GuestCancelOrderRequest extends FormRequest { + use ValidatesCancelItems; + /** * 사용자가 이 요청을 수행할 권한이 있는지 확인 * @@ -65,6 +68,13 @@ class GuestCancelOrderRequest extends FormRequest 'order_status', $order->getCancelDeniedReason($cancellableStatuses) ); + + return; + } + + // 부분취소 items 검증 (회원판 CancelOrderRequest 와 동일 강도) + if ($this->has('items') && is_array($this->input('items'))) { + $this->validateCancelItemsAgainstOrder($validator, $order, $this->input('items', [])); } }); } @@ -102,7 +112,7 @@ class GuestCancelOrderRequest extends FormRequest */ public function getCancelItems(): array { - return $this->input('items', []); + return $this->validated('items') ?? []; } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestEstimateRefundRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestEstimateRefundRequest.php index 6c23aecb..a2b6b82d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestEstimateRefundRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestEstimateRefundRequest.php @@ -3,8 +3,10 @@ namespace Modules\Sirsoft\Ecommerce\Http\Requests\Public; use App\Helpers\ResponseHelper; +use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Modules\Sirsoft\Ecommerce\Enums\RefundPriorityEnum; +use Modules\Sirsoft\Ecommerce\Http\Requests\Concerns\ValidatesCancelItems; use Modules\Sirsoft\Ecommerce\Models\Order; /** @@ -15,6 +17,8 @@ use Modules\Sirsoft\Ecommerce\Models\Order; */ class GuestEstimateRefundRequest extends FormRequest { + use ValidatesCancelItems; + /** * 사용자가 이 요청을 수행할 권한이 있는지 확인 * @@ -40,6 +44,26 @@ class GuestEstimateRefundRequest extends FormRequest ]; } + /** + * 추가 검증 로직 — 각 항목이 대상 주문에 속하고 취소 가능한지 확인합니다. + * + * 회원판 EstimateRefundRequest 와 동일 강도. 대상 주문은 미들웨어가 전달한 + * guest_order attribute 를 사용한다. + * + * @param Validator $validator 검증기 + * @return void + */ + protected function withValidator(Validator $validator): void + { + $validator->after(function (Validator $validator) { + if ($validator->errors()->any()) { + return; + } + + $this->validateCancelItemsAgainstOrder($validator, $this->getOrder(), $this->input('items', [])); + }); + } + /** * 미들웨어가 검증한 대상 주문을 반환합니다. * @@ -63,7 +87,7 @@ class GuestEstimateRefundRequest extends FormRequest */ public function getCancelItems(): array { - return $this->input('items', []); + return $this->validated('items') ?? []; } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestUpdateShippingAddressRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestUpdateShippingAddressRequest.php index 9df3a40d..3426cd78 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestUpdateShippingAddressRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/Public/GuestUpdateShippingAddressRequest.php @@ -31,6 +31,12 @@ class GuestUpdateShippingAddressRequest extends FormRequest */ public function rules(): array { + // country_code 가 국내/해외를 가르는 기준축이다. 종전에는 zipcode/address 를 + // address_line_1 유무로만 강제해, 해외 필드만 채우고 country_code 를 생략하면 + // 검증을 통과한 뒤 서비스가 국내 분기로 떨어져 NOT NULL 컬럼에 null 을 쓰려다 + // SQL 무결성 위반이 났다. 서비스의 폴백(?? 'KR')과 동일한 기준으로 판정한다. + $isDomestic = $this->isDomesticShipping(); + return [ 'recipient_name' => ['required', 'string', 'max:50'], 'recipient_phone' => ['required', 'string', 'max:20'], @@ -38,21 +44,35 @@ class GuestUpdateShippingAddressRequest extends FormRequest 'country_code' => ['nullable', 'string', 'size:2'], // 국내 배송 주소 - 'zipcode' => ['required_without:address_line_1', 'nullable', 'string', 'max:10'], - 'address' => ['required_without:address_line_1', 'nullable', 'string', 'max:255'], + 'zipcode' => [$isDomestic ? 'required' : 'nullable', 'string', 'max:10'], + 'address' => [$isDomestic ? 'required' : 'nullable', 'string', 'max:255'], 'address_detail' => ['nullable', 'string', 'max:255'], // 해외 배송 주소 - 'address_line_1' => ['required_without:address', 'nullable', 'string', 'max:255'], + 'address_line_1' => [$isDomestic ? 'nullable' : 'required', 'string', 'max:255'], 'address_line_2' => ['nullable', 'string', 'max:255'], - 'intl_city' => ['required_with:address_line_1', 'nullable', 'string', 'max:100'], + 'intl_city' => [$isDomestic ? 'nullable' : 'required', 'string', 'max:100'], 'intl_state' => ['nullable', 'string', 'max:100'], - 'intl_postal_code' => ['required_with:address_line_1', 'nullable', 'string', 'max:20'], + 'intl_postal_code' => [$isDomestic ? 'nullable' : 'required', 'string', 'max:20'], 'delivery_memo' => ['nullable', 'string', 'max:255'], ]; } + /** + * 국내 배송 여부를 판정합니다. + * + * country_code 미전송/빈값은 국내로 간주한다 (OrderService 의 'KR' 폴백과 동일 기준). + * + * @return bool 국내 배송이면 true + */ + private function isDomesticShipping(): bool + { + $country = strtoupper(trim((string) $this->input('country_code', ''))); + + return $country === '' || $country === 'KR'; + } + /** * 검증 오류 메시지 커스터마이징 * @@ -63,10 +83,11 @@ class GuestUpdateShippingAddressRequest extends FormRequest return [ 'recipient_name.required' => __('sirsoft-ecommerce::validation.user_address.recipient_name_required'), 'recipient_phone.required' => __('sirsoft-ecommerce::validation.user_address.recipient_phone_required'), - 'zipcode.required_without' => __('sirsoft-ecommerce::validation.user_address.zipcode_required'), - 'address.required_without' => __('sirsoft-ecommerce::validation.user_address.address_required'), - 'address_line_1.required_without' => __('sirsoft-ecommerce::validation.user_address.address_line_1_required'), - 'intl_city.required_with' => __('sirsoft-ecommerce::validation.user_address.intl_city_required'), + 'zipcode.required' => __('sirsoft-ecommerce::validation.user_address.zipcode_required'), + 'address.required' => __('sirsoft-ecommerce::validation.user_address.address_required'), + 'address_line_1.required' => __('sirsoft-ecommerce::validation.user_address.address_line_1_required'), + 'intl_city.required' => __('sirsoft-ecommerce::validation.user_address.intl_city_required'), + 'intl_postal_code.required' => __('sirsoft-ecommerce::validation.user_address.intl_postal_code_required'), ]; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/CancelOrderRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/CancelOrderRequest.php index a56198ec..839eae2f 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/CancelOrderRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/CancelOrderRequest.php @@ -8,10 +8,11 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; -use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; -use Modules\Sirsoft\Ecommerce\Models\ClaimReason; use Modules\Sirsoft\Ecommerce\Enums\RefundPriorityEnum; +use Modules\Sirsoft\Ecommerce\Http\Requests\Concerns\ValidatesCancelItems; +use Modules\Sirsoft\Ecommerce\Models\ClaimReason; use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderRepositoryInterface; /** * 주문 취소 요청 @@ -20,6 +21,8 @@ use Modules\Sirsoft\Ecommerce\Models\Order; */ class CancelOrderRequest extends FormRequest { + use ValidatesCancelItems; + protected ?Order $order = null; /** @@ -65,14 +68,14 @@ class CancelOrderRequest extends FormRequest /** * 추가 검증 로직 * - * @param Validator $validator + * @param Validator $validator * @return void */ protected function withValidator(Validator $validator): void { $validator->after(function (Validator $validator) { $orderId = $this->route('id'); - $this->order = Order::find($orderId); + $this->order = app(OrderRepositoryInterface::class)->find((int) $orderId); if (! $this->order) { abort(ResponseHelper::moduleError( @@ -109,60 +112,15 @@ class CancelOrderRequest extends FormRequest // 부분취소 items 검증 if ($this->has('items') && is_array($this->input('items'))) { - $this->validateCancelItems($validator); + $this->validateCancelItemsAgainstOrder($validator, $this->order, $this->input('items', [])); } }); } - /** - * 취소 아이템 목록을 검증합니다. - * - * @param Validator $validator - * @return void - */ - protected function validateCancelItems(Validator $validator): void - { - $this->order->loadMissing('options'); - $items = $this->input('items', []); - - foreach ($items as $index => $item) { - $option = $this->order->options->firstWhere('id', $item['order_option_id']); - - if (! $option) { - $validator->errors()->add( - "items.{$index}.order_option_id", - __('sirsoft-ecommerce::exceptions.order_option_not_found') - ); - - continue; - } - - // 이미 취소된 옵션은 제외 - if ($option->option_status === OrderStatusEnum::CANCELLED) { - $validator->errors()->add( - "items.{$index}.order_option_id", - __('sirsoft-ecommerce::exceptions.order_option_already_cancelled') - ); - - continue; - } - - // 취소 수량이 현재 수량을 초과하는지 검증 - if ($item['cancel_quantity'] > $option->quantity) { - $validator->errors()->add( - "items.{$index}.cancel_quantity", - __('sirsoft-ecommerce::exceptions.cancel_quantity_exceeds', [ - 'max' => $option->quantity, - ]) - ); - } - } - } - /** * 검증 실패 시 응답 커스터마이징 * - * @param Validator $validator + * @param Validator $validator * @return void * * @throws ValidationException diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/EstimateRefundRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/EstimateRefundRequest.php index 619601eb..59402d97 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/EstimateRefundRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/EstimateRefundRequest.php @@ -7,9 +7,10 @@ use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; -use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\RefundPriorityEnum; +use Modules\Sirsoft\Ecommerce\Http\Requests\Concerns\ValidatesCancelItems; use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderRepositoryInterface; /** * 환불 예상금액 조회 요청 (사용자) @@ -18,6 +19,8 @@ use Modules\Sirsoft\Ecommerce\Models\Order; */ class EstimateRefundRequest extends FormRequest { + use ValidatesCancelItems; + protected ?Order $order = null; /** @@ -59,7 +62,7 @@ class EstimateRefundRequest extends FormRequest } $orderId = $this->route('id'); - $this->order = Order::find($orderId); + $this->order = app(OrderRepositoryInterface::class)->find((int) $orderId); if (! $this->order) { abort(ResponseHelper::moduleError( @@ -78,41 +81,7 @@ class EstimateRefundRequest extends FormRequest )); } - $this->order->loadMissing('options'); - $items = $this->input('items', []); - - foreach ($items as $index => $item) { - $option = $this->order->options->firstWhere('id', $item['order_option_id']); - - if (! $option) { - $validator->errors()->add( - "items.{$index}.order_option_id", - __('sirsoft-ecommerce::exceptions.order_option_not_found') - ); - - continue; - } - - // 이미 취소된 옵션은 제외 - if ($option->option_status === OrderStatusEnum::CANCELLED) { - $validator->errors()->add( - "items.{$index}.order_option_id", - __('sirsoft-ecommerce::exceptions.order_option_already_cancelled') - ); - - continue; - } - - // 취소 수량이 현재 수량을 초과하는지 검증 - if ($item['cancel_quantity'] > $option->quantity) { - $validator->errors()->add( - "items.{$index}.cancel_quantity", - __('sirsoft-ecommerce::exceptions.cancel_quantity_exceeds', [ - 'max' => $option->quantity, - ]) - ); - } - } + $this->validateCancelItemsAgainstOrder($validator, $this->order, $this->input('items', [])); }); } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UpdateOrderShippingAddressRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UpdateOrderShippingAddressRequest.php index 039dfae8..186d7079 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UpdateOrderShippingAddressRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UpdateOrderShippingAddressRequest.php @@ -23,8 +23,24 @@ class UpdateOrderShippingAddressRequest extends FormRequest return true; } + /** + * 요청에 적용할 검증 규칙 + * + * @return array<string, mixed> 검증 규칙 배열 + */ public function rules(): array { + // country_code 가 국내/해외를 가르는 기준축이다. 종전에는 zipcode/address 를 + // address_line_1 유무로만 강제해, 해외 필드만 채우고 country_code 를 생략하면 + // 검증을 통과한 뒤 서비스가 국내 분기로 떨어져 NOT NULL 컬럼에 null 을 쓰려다 + // SQL 무결성 위반이 났다. 서비스의 폴백(?? 'KR')과 동일한 기준으로 판정한다. + // 저장된 배송지(address_id) 선택 시에는 직접 입력 필드를 요구하지 않는다. + $hasAddressId = $this->filled('address_id'); + $isDomestic = $this->isDomesticShipping(); + + $domesticRule = (! $hasAddressId && $isDomestic) ? 'required' : 'nullable'; + $intlRule = (! $hasAddressId && ! $isDomestic) ? 'required' : 'nullable'; + $rules = [ // 저장된 배송지 선택 (address_id가 있으면 다른 필드 불필요) 'address_id' => ['nullable', 'integer', Rule::exists(UserAddress::class, 'id')->where('user_id', Auth::id())], @@ -37,16 +53,16 @@ class UpdateOrderShippingAddressRequest extends FormRequest 'country_code' => 'nullable|string|size:2', // 국내 배송 주소 - 'zipcode' => 'required_without_all:address_id,address_line_1|nullable|string|max:10', - 'address' => 'required_without_all:address_id,address_line_1|nullable|string|max:255', + 'zipcode' => $domesticRule.'|string|max:10', + 'address' => $domesticRule.'|string|max:255', 'address_detail' => 'nullable|string|max:255', // 해외 배송 주소 - 'address_line_1' => 'required_without_all:address_id,address|nullable|string|max:255', + 'address_line_1' => $intlRule.'|string|max:255', 'address_line_2' => 'nullable|string|max:255', - 'intl_city' => 'required_with:address_line_1|nullable|string|max:100', + 'intl_city' => $intlRule.'|string|max:100', 'intl_state' => 'nullable|string|max:100', - 'intl_postal_code' => 'required_with:address_line_1|nullable|string|max:20', + 'intl_postal_code' => $intlRule.'|string|max:20', // 배송 메모 'delivery_memo' => 'nullable|string|max:255', @@ -55,6 +71,20 @@ class UpdateOrderShippingAddressRequest extends FormRequest return HookManager::applyFilters('sirsoft-ecommerce.order.shipping_address_validation_rules', $rules, $this); } + /** + * 국내 배송 여부를 판정합니다. + * + * country_code 미전송/빈값은 국내로 간주한다 (OrderService 의 'KR' 폴백과 동일 기준). + * + * @return bool 국내 배송이면 true + */ + private function isDomesticShipping(): bool + { + $country = strtoupper(trim((string) $this->input('country_code', ''))); + + return $country === '' || $country === 'KR'; + } + /** * 검증 오류 메시지 커스터마이징 * @@ -65,11 +95,11 @@ class UpdateOrderShippingAddressRequest extends FormRequest return [ 'recipient_name.required' => __('sirsoft-ecommerce::validation.user_address.recipient_name_required'), 'recipient_phone.required' => __('sirsoft-ecommerce::validation.user_address.recipient_phone_required'), - 'zipcode.required_without' => __('sirsoft-ecommerce::validation.user_address.zipcode_required'), - 'address.required_without' => __('sirsoft-ecommerce::validation.user_address.address_required'), - 'address_line_1.required_without' => __('sirsoft-ecommerce::validation.user_address.address_line_1_required'), - 'intl_city.required_with' => __('sirsoft-ecommerce::validation.user_address.intl_city_required'), - 'intl_postal_code.required_with' => __('sirsoft-ecommerce::validation.user_address.intl_postal_code_required'), + 'zipcode.required' => __('sirsoft-ecommerce::validation.user_address.zipcode_required'), + 'address.required' => __('sirsoft-ecommerce::validation.user_address.address_required'), + 'address_line_1.required' => __('sirsoft-ecommerce::validation.user_address.address_line_1_required'), + 'intl_city.required' => __('sirsoft-ecommerce::validation.user_address.intl_city_required'), + 'intl_postal_code.required' => __('sirsoft-ecommerce::validation.user_address.intl_postal_code_required'), ]; } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UploadReviewImageRequest.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UploadReviewImageRequest.php index 15799225..b29c0fc5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UploadReviewImageRequest.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Requests/User/UploadReviewImageRequest.php @@ -67,7 +67,8 @@ class UploadReviewImageRequest extends FormRequest return (int) module_setting( 'sirsoft-ecommerce', 'review_settings.max_image_size_mb', - config('ecommerce.review.max_image_size_mb', 10) + // 모듈 config 는 'sirsoft-ecommerce' 네임스페이스로 로드된다 (ModuleManager::loadModuleConfig) + config('sirsoft-ecommerce.review.max_image_size_mb', 10) ); } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ExtraFeeTemplateResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ExtraFeeTemplateResource.php index e1d3a5a2..9f187eb8 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ExtraFeeTemplateResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ExtraFeeTemplateResource.php @@ -55,10 +55,15 @@ class ExtraFeeTemplateResource extends BaseApiResource */ protected function abilityMap(): array { + // 추가배송비 템플릿의 write 라우트는 모두 shipping-policies.{create,update,delete} 로 + // 게이팅된다(라우트 SSoT). 능력 플래그도 그 리소스의 권한과 일치해야 한다 — 형제 + // ExtraFeeTemplateCollection 과 동일. 과거 settings.update(타 리소스)로 게이팅해 + // shipping-policies 권한만 가진 액터가 상세 화면에서 편집 능력이 false 로 보이던 + // 결함을 정정한다. return [ - 'can_create' => 'sirsoft-ecommerce.settings.update', - 'can_update' => 'sirsoft-ecommerce.settings.update', - 'can_delete' => 'sirsoft-ecommerce.settings.update', + 'can_create' => 'sirsoft-ecommerce.shipping-policies.create', + 'can_update' => 'sirsoft-ecommerce.shipping-policies.update', + 'can_delete' => 'sirsoft-ecommerce.shipping-policies.delete', ]; } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/GuestOrderResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/GuestOrderResource.php index a2ee1599..321c8867 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/GuestOrderResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/GuestOrderResource.php @@ -39,6 +39,9 @@ class GuestOrderResource extends BaseApiResource { // 주문 시점 기준 통화 — 과거 주문의 *_formatted 는 설정 변경과 무관하게 이 통화로 고정 표기한다. $orderCurrency = $this->resolveOrderBaseCurrencyCode($this->resource); + // 주문 시점 통화 스냅샷 — 소수 자릿수를 현재 설정이 아닌 주문 시점 값으로 고정 (공개 #91 후속). + $currencySnapshot = $this->resource->currency_snapshot ?? null; + $this->withCurrencySnapshot($currencySnapshot); // 결제 통화(order_currency) — 현금영수증 금액 표기 기준(구매자가 실제 낸 통화). $paymentCurrency = $this->currency @@ -118,14 +121,14 @@ class GuestOrderResource extends BaseApiResource // 주문 옵션 (품목) — 주문 시점 통화를 자식에 전파 'options' => $this->whenLoaded('options', fn () => OrderOptionResource::collection($this->options)->each( - fn ($r) => $r->withOrderCurrency($orderCurrency) + fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot) )), // 배송지/결제/배송 정보 'shipping_address' => new OrderAddressResource($this->whenLoaded('shippingAddress')), - 'payment' => $this->whenLoaded('payment', fn () => (new OrderPaymentResource($this->payment))->withOrderCurrency($orderCurrency)), + 'payment' => $this->whenLoaded('payment', fn () => (new OrderPaymentResource($this->payment))->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)), 'shippings' => $this->whenLoaded('shippings', fn () => OrderShippingResource::collection($this->shippings)->each( - fn ($r) => $r->withOrderCurrency($orderCurrency) + fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot) )), // 취소 이력 (취소 사유·상세 사유·취소일시 표시용) — 최근 취소가 먼저 오도록 정렬 @@ -137,17 +140,20 @@ class GuestOrderResource extends BaseApiResource // "미발급" 으로 남고 영수증 URL 에 도달할 방법이 사라진다(오류·경고 없음). // CashReceiptResource 는 식별번호를 마스킹 값으로만 내보내고 프로바이더 원응답을 // 노출하지 않으므로 비회원에게 내려도 안전하다. - 'cash_receipt' => $this->whenLoaded('cashReceipts', function () use ($orderCurrency, $paymentCurrency) { + // 통화 스냅샷은 명시 캡처가 필요하다 — 형제 항목들이 쓰는 화살표 함수와 달리 + // 이 클로저는 자동 캡처가 없어, use 목록에서 빠지면 undefined 로 떨어져 주문 시점 + // 통화(자릿수·절사 규칙)가 전파되지 않는다. + 'cash_receipt' => $this->whenLoaded('cashReceipts', function () use ($orderCurrency, $paymentCurrency, $currencySnapshot) { $active = OrderCashReceipt::filterActive($this->cashReceipts)[0] ?? null; return $active - ? (new CashReceiptResource($active))->withOrderCurrency($orderCurrency)->withReceiptCurrency($paymentCurrency) + ? (new CashReceiptResource($active))->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)->withReceiptCurrency($paymentCurrency) : null; }), // 발급/취소 전체 이력 — 카드의 "직전 발급이 실패했는가" 분기가 cash_receipts[0] 를 본다. 'cash_receipts' => $this->whenLoaded('cashReceipts', fn () => CashReceiptResource::collection( $this->cashReceipts->sortByDesc('id')->values() - )->each(fn ($r) => $r->withOrderCurrency($orderCurrency)->withReceiptCurrency($paymentCurrency))), + )->each(fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)->withReceiptCurrency($paymentCurrency))), // 배송정책 — **표시용 필드만**. 비회원 주문 상세 화면은 회원 마이페이지와 같은 // partial 을 써서 상품별 정책명·개별 배송비를 그리는데, 이 필드를 통째로 빼면 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderListResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderListResource.php index ec22752b..78a710cc 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderListResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderListResource.php @@ -30,6 +30,9 @@ class OrderListResource extends BaseApiResource { // 주문 시점 기준 통화 — 과거 주문의 *_formatted 는 설정 변경과 무관하게 이 통화로 고정 표기한다. $orderCurrency = $this->resolveOrderBaseCurrencyCode($this->resource); + // 주문 시점 통화 스냅샷 — 소수 자릿수를 현재 설정이 아닌 주문 시점 값으로 고정 (공개 #91 후속). + $currencySnapshot = $this->resource->currency_snapshot ?? null; + $this->withCurrencySnapshot($currencySnapshot); // 결제 통화(order_currency) — 유저가 선택·결제한 통화. base 통화와 다를 때 함께 표기. $paymentCurrency = $this->currency diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderResource.php index 347698f5..5fd07424 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/OrderResource.php @@ -27,6 +27,9 @@ class OrderResource extends BaseApiResource { // 주문 시점 기준 통화 — 과거 주문의 *_formatted 는 설정 변경과 무관하게 이 통화로 고정 표기한다. $orderCurrency = $this->resolveOrderBaseCurrencyCode($this->resource); + // 주문 시점 통화 스냅샷 — 소수 자릿수를 현재 설정이 아닌 주문 시점 값으로 고정 (공개 #91 후속). + $currencySnapshot = $this->resource->currency_snapshot ?? null; + $this->withCurrencySnapshot($currencySnapshot); // 결제 통화(order_currency) — 유저가 선택·결제한 통화. base 통화와 다를 때 화면에 함께 표기한다. $paymentCurrency = $this->currency @@ -177,7 +180,7 @@ class OrderResource extends BaseApiResource // 주문 옵션 (품목) — 주문 시점 통화를 자식에 전파 'options' => $this->whenLoaded('options', fn () => OrderOptionResource::collection($this->options)->each( - fn ($r) => $r->withOrderCurrency($orderCurrency) + fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot) )), // 배송지 정보 @@ -185,27 +188,30 @@ class OrderResource extends BaseApiResource 'billing_address' => new OrderAddressResource($this->whenLoaded('billingAddress')), // 결제 정보 - 'payment' => $this->whenLoaded('payment', fn () => (new OrderPaymentResource($this->payment))->withOrderCurrency($orderCurrency)), + 'payment' => $this->whenLoaded('payment', fn () => (new OrderPaymentResource($this->payment))->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)), 'payments' => $this->whenLoaded('payments', fn () => OrderPaymentResource::collection($this->payments)->each( - fn ($r) => $r->withOrderCurrency($orderCurrency) + fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot) )), // 현금영수증 — 현재 활성 영수증 1건(없으면 null). 발급 카드의 "발급완료" 상태 근거. - 'cash_receipt' => $this->whenLoaded('cashReceipts', function () use ($orderCurrency, $paymentCurrency) { + // 통화 스냅샷은 명시 캡처가 필요하다 — 형제 항목들이 쓰는 화살표 함수와 달리 이 + // 클로저는 자동 캡처가 없어, use 목록에서 빠지면 undefined 로 떨어져 주문 시점 + // 통화(자릿수·절사 규칙)가 전파되지 않는다. + 'cash_receipt' => $this->whenLoaded('cashReceipts', function () use ($orderCurrency, $paymentCurrency, $currencySnapshot) { $active = OrderCashReceipt::filterActive($this->cashReceipts)[0] ?? null; return $active - ? (new CashReceiptResource($active))->withOrderCurrency($orderCurrency)->withReceiptCurrency($paymentCurrency) + ? (new CashReceiptResource($active))->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)->withReceiptCurrency($paymentCurrency) : null; }), // 발급/취소 전체 이력 — 관리자 화면의 "취소 성공 + 재발급 실패" 경고 배지 판정 근거. 'cash_receipts' => $this->whenLoaded('cashReceipts', fn () => CashReceiptResource::collection( $this->cashReceipts->sortByDesc('id')->values() - )->each(fn ($r) => $r->withOrderCurrency($orderCurrency)->withReceiptCurrency($paymentCurrency))), + )->each(fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot)->withReceiptCurrency($paymentCurrency))), // 배송 정보 'shippings' => $this->whenLoaded('shippings', fn () => OrderShippingResource::collection($this->shippings)->each( - fn ($r) => $r->withOrderCurrency($orderCurrency) + fn ($r) => $r->withOrderCurrency($orderCurrency)->withCurrencySnapshot($currencySnapshot) )), // 취소 이력 (취소 사유·상세 사유·취소일시 표시용) — 최근 취소가 먼저 오도록 정렬 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductOptionResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductOptionResource.php index 24fb8ac3..cef38f86 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductOptionResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductOptionResource.php @@ -61,6 +61,11 @@ class ProductOptionResource extends BaseApiResource // 품절 여부 — 재고 0 이하이거나 비활성 옵션 (프론트 드롭다운 비활성/라벨용, MP07 §2-b) 'is_sold_out' => ($this->stock_quantity ?? 0) <= 0 || ! $this->is_active, + // 배송 계산용 물성 (g / cm³ — 배송정책의 kg/L 환산은 배송비 계산 시점에 수행) + // 직렬화하지 않으면 관리자 수정 화면이 0 으로 표시하고, 그대로 저장하면 실측값이 덮어써진다. + 'weight' => $this->weight !== null ? (float) $this->weight : null, + 'volume' => $this->volume !== null ? (float) $this->volume : null, + // 상태 'is_default' => $this->is_default, 'is_active' => $this->is_active, diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewImageResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewImageResource.php index ed95ea53..7024af27 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewImageResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewImageResource.php @@ -4,12 +4,42 @@ namespace Modules\Sirsoft\Ecommerce\Http\Resources; use App\Http\Resources\BaseApiResource; use Illuminate\Http\Request; +use Illuminate\Support\Collection; +use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; /** * 리뷰 이미지 리소스 */ class ProductReviewImageResource extends BaseApiResource { + /** + * 숨김 리뷰 이미지의 <img> 렌더용 한시 서명 download URL 발급 여부. + * + * 게이트(관리자 권한 미들웨어)를 통과한 응답의 직렬화 시점에만 참이 된다 — + * 전시중(VISIBLE) 리뷰는 무서명 공개 URL 을 유지한다 (CDN·캐시 안정성). + */ + private bool $signedDownload = false; + + /** + * 리뷰 이미지 목록을 리소스 배열로 변환합니다. + * + * @param iterable<int, ProductReviewImage>|null $images 리뷰 이미지 목록 + * @param bool $signedDownload 한시 서명 download URL 발급 여부 (숨김 리뷰 전용) + * @return array<int, self> + */ + public static function collectionFor($images, bool $signedDownload = false): array + { + return Collection::make($images ?? []) + ->map(function ($image) use ($signedDownload) { + $resource = new self($image); + $resource->signedDownload = $signedDownload; + + return $resource; + }) + ->values() + ->all(); + } + /** * 리소스를 배열로 변환 * @@ -23,7 +53,9 @@ class ProductReviewImageResource extends BaseApiResource 'review_id' => $this->review_id, 'hash' => $this->hash, 'original_filename' => $this->original_filename, - 'download_url' => $this->download_url, + 'download_url' => $this->signedDownload + ? $this->resource->signedDownloadUrl() + : $this->download_url, 'mime_type' => $this->mime_type, 'file_size' => $this->file_size, 'width' => $this->width, diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewResource.php index 7e1186ee..e3af2dc6 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/ProductReviewResource.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Ecommerce\Http\Resources; use App\Http\Resources\BaseApiResource; use Illuminate\Http\Request; +use Modules\Sirsoft\Ecommerce\Enums\ReviewStatus; /** * 상품 리뷰 리소스 @@ -55,8 +56,14 @@ class ProductReviewResource extends BaseApiResource 'status_label' => $this->status->label(), 'status_badge_color' => $this->status->badgeColor(), - // 이미지 - 'images' => ProductReviewImageResource::collection($this->whenLoaded('images')), + // 이미지 — 숨김(HIDDEN) 리뷰의 이미지는 한시 서명 download URL 로 직렬화한다. + // 숨김 리뷰가 이미지와 함께 직렬화되는 경로는 관리자(권한 미들웨어) 응답뿐이므로 + // (공개 목록은 VISIBLE 필터, 작성 직후는 이미지 미로드) 서명 발급이 곧 + // 게이트 통과 자격의 위임이다. 전시중 리뷰는 무서명 공개 URL 유지. + 'images' => $this->whenLoaded('images', fn () => ProductReviewImageResource::collectionFor( + $this->images, + signedDownload: $this->status === ReviewStatus::HIDDEN + )), // 첨부 개수는 목록 쿼리의 DB 집계(`withCount('images as image_count')`)를 우선 쓴다. // 관계 기반 계산만 두면, 이미지를 로드하지 않는 목록에서 개수까지 함께 사라진다. 'image_count' => $this->image_count !== null diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/Traits/HasMultiCurrencyPrices.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/Traits/HasMultiCurrencyPrices.php index f0eb37be..a2daa69d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/Traits/HasMultiCurrencyPrices.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/Traits/HasMultiCurrencyPrices.php @@ -20,6 +20,14 @@ trait HasMultiCurrencyPrices */ protected ?string $orderCurrencyCode = null; + /** + * 부모 주문 리소스가 주입한 주문 시점 통화 스냅샷 (공개 #91 후속). + * + * 소수 자릿수를 현재 설정이 아니라 주문 시점 값으로 고정하기 위해 전파한다. + * null 이면 현재 설정으로 폴백 — 상품 등 카탈로그 경로는 그것이 정답이다. + */ + protected ?array $currencySnapshot = null; + /** * 주문 시점 기준 통화 코드를 주입합니다 (부모 → 자식 리소스 전파). * @@ -33,6 +41,23 @@ trait HasMultiCurrencyPrices return $this; } + /** + * 주문 시점 통화 스냅샷을 주입합니다 (부모 → 자식 리소스 전파). + * + * 관리자가 통화를 삭제하면 현재 설정에서 사라져 자릿수가 폴백 2자리로 바뀐다. + * 그러면 0자리 통화로 결제된 과거 주문의 표기가 `¥14,835` → `¥14,835.00` 이 되고, + * 3자리 이상 통화는 표시 금액이 절사된다. 스냅샷이 있으면 그것을 SSoT 로 삼는다. + * + * @param array|null $snapshot 주문의 `currency_snapshot` + * @return $this 메서드 체이닝을 위한 자기 자신 + */ + public function withCurrencySnapshot(?array $snapshot): static + { + $this->currencySnapshot = $snapshot; + + return $this; + } + /** * 다중 통화 가격 정보를 생성합니다. * @@ -302,11 +327,20 @@ trait HasMultiCurrencyPrices /** * 통화의 소수 자릿수를 반환합니다. * + * 주문 시점 스냅샷이 주입돼 있고 그 통화의 자릿수를 박제해 두었으면 그것이 SSoT 다 + * (공개 #91 후속 — 삭제된 통화가 현재 설정에 없어 폴백 2자리로 바뀌는 것을 막는다). + * 스냅샷이 없으면(상품 등 카탈로그 경로) 현재 설정을 그대로 따른다. + * * @param string $code 통화 코드 * @return int 소수 자릿수 (기본값: 2) */ protected function getDecimalPlacesForCurrency(string $code): int { + $snapshotPlaces = $this->currencySnapshot['exchange_rates'][$code]['decimal_places'] ?? null; + if ($snapshotPlaces !== null) { + return (int) $snapshotPlaces; + } + $currencies = $this->getCurrencySettings(); foreach ($currencies as $currency) { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/UserOrderListResource.php b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/UserOrderListResource.php index ee289165..e1cf9c25 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/UserOrderListResource.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Http/Resources/UserOrderListResource.php @@ -5,9 +5,11 @@ namespace Modules\Sirsoft\Ecommerce\Http\Resources; use App\Helpers\PermissionHelper; use App\Http\Resources\BaseApiResource; use Illuminate\Http\Request; +use Illuminate\Http\Resources\MissingValue; use Modules\Sirsoft\Ecommerce\Http\Resources\Concerns\LocalizesCountryName; use Modules\Sirsoft\Ecommerce\Http\Resources\Traits\HasMultiCurrencyPrices; use Modules\Sirsoft\Ecommerce\Http\Resources\Traits\SummarizesAdditionalOptions; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; /** * 사용자 주문 목록 리소스 @@ -31,6 +33,9 @@ class UserOrderListResource extends BaseApiResource { // 주문 시점 기준 통화 — 과거 주문의 *_formatted 는 설정 변경과 무관하게 이 통화로 고정 표기한다. $orderCurrency = $this->resolveOrderBaseCurrencyCode($this->resource); + // 주문 시점 통화 스냅샷 — 소수 자릿수를 현재 설정이 아닌 주문 시점 값으로 고정 (공개 #91 후속). + $currencySnapshot = $this->resource->currency_snapshot ?? null; + $this->withCurrencySnapshot($currencySnapshot); // 컬렉션(UserOrderCollection)이 이 배열을 그대로 응답에 실어 Laravel 의 MissingValue // 제거 단계를 거치지 않는다 — 미충족 조건부 필드를 직접 걸러낸다. 걸러내지 않으면 @@ -98,7 +103,7 @@ class UserOrderListResource extends BaseApiResource * 주문" 이라는 사실 아닌 단언이 된다. * * @param string|null $orderCurrency 주문 시점 기준 통화 - * @return array<int, array<string, mixed>>|\Illuminate\Http\Resources\MissingValue 아이템 배열 + * @return array<int, array<string, mixed>>|MissingValue 아이템 배열 */ private function resolveItems(?string $orderCurrency) { @@ -117,13 +122,13 @@ class UserOrderListResource extends BaseApiResource : []; } - return new \Illuminate\Http\Resources\MissingValue; + return new MissingValue; } /** * 주문 아이템 1건을 표시용 배열로 변환합니다. * - * @param \Modules\Sirsoft\Ecommerce\Models\OrderOption $option 주문 옵션 + * @param OrderOption $option 주문 옵션 * @param string|null $orderCurrency 주문 시점 기준 통화 * @return array<string, mixed> 표시용 아이템 배열 */ @@ -157,7 +162,7 @@ class UserOrderListResource extends BaseApiResource * 유무는 값이 아니라 속성 키로 판정한다 — 0건의 `0` 과 "집계 안 함" 은 값으로 구분되지 * 않는다. * - * @return int|\Illuminate\Http\Resources\MissingValue 아이템 수 + * @return int|MissingValue 아이템 수 */ private function resolveItemCount() { @@ -169,7 +174,7 @@ class UserOrderListResource extends BaseApiResource return $this->resource->options->count(); } - return new \Illuminate\Http\Resources\MissingValue; + return new MissingValue; } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Listeners/Ckeditor5ReferenceSourcesListener.php b/modules/_bundled/sirsoft-ecommerce/src/Listeners/Ckeditor5ReferenceSourcesListener.php new file mode 100644 index 00000000..a85269db --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Listeners/Ckeditor5ReferenceSourcesListener.php @@ -0,0 +1,57 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Listeners; + +use App\Contracts\Extension\HookListenerInterface; + +/** + * 상품 설명·공통 안내를 CKEditor5 이미지 참조 스캔 대상으로 등록하는 리스너 + * + * `sirsoft-ckeditor5.image.filter_reference_sources` 필터 훅에 상품 상세 설명과 + * 상품 공통 정보 본문 컬럼을 덧붙입니다. 등록하지 않으면 상품 설명에만 쓰이는 + * 이미지가 "미참조" 로 판정돼 자동 정리 대상이 됩니다. + * + * 테이블명 문자열만 덧붙이며 DB 에 접근하지 않습니다. + */ +class Ckeditor5ReferenceSourcesListener implements HookListenerInterface +{ + /** + * 구독할 훅 목록 반환 + * + * @return array<string, mixed> + */ + public static function getSubscribedHooks(): array + { + return [ + 'sirsoft-ckeditor5.image.filter_reference_sources' => [ + 'method' => 'addEcommerceSources', + 'priority' => 10, + 'type' => 'filter', + ], + ]; + } + + /** + * 훅 이벤트를 처리합니다. + * + * Filter 훅은 getSubscribedHooks 에서 지정한 메서드를 직접 호출하므로 + * 이 메서드는 인터페이스 요구사항 충족을 위해서만 존재합니다. + * + * @param mixed ...$args 훅에서 전달된 인수들 + */ + public function handle(...$args): void {} + + /** + * 이커머스 참조 소스를 추가합니다. + * + * @param array $sources 기존 참조 소스 목록 + * @return array 이커머스 소스가 추가된 목록 + */ + public function addEcommerceSources(array $sources): array + { + $sources[] = ['table' => 'ecommerce_products', 'columns' => ['description']]; + $sources[] = ['table' => 'ecommerce_product_common_infos', 'columns' => ['content']]; + + return $sources; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Listeners/ProductInquiryBoardListener.php b/modules/_bundled/sirsoft-ecommerce/src/Listeners/ProductInquiryBoardListener.php index 8db9c3f5..6be12427 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Listeners/ProductInquiryBoardListener.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Listeners/ProductInquiryBoardListener.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Ecommerce\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Extension\HookManager; +use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductInquiryRepositoryInterface; use Modules\Sirsoft\Ecommerce\Services\ProductInquiryService; @@ -11,8 +12,10 @@ use Modules\Sirsoft\Ecommerce\Services\ProductInquiryService; /** * 게시판 이벤트 훅 리스너 (이커머스 문의 연동) * - * 게시판 Post 생성/삭제 이벤트를 수신하여 문의 피벗 데이터를 관리합니다. - * - sirsoft-board.post.after_delete → 게시판 Post 삭제 시 피벗 삭제 + * 게시판 Post 삭제/복원 이벤트를 수신하여 문의 피벗 데이터를 관리합니다. + * - sirsoft-board.post.after_delete → 질문 글 삭제 시 피벗 소프트 삭제, 답변 글 삭제 시 답변완료 재계산 + * - sirsoft-board.post.after_restore → 질문 글 복원 시 피벗 복원, 답변 글 복원 시 답변완료 재마킹 + * - sirsoft-board.board.posts.before_force_delete → 게시판 삭제 시 삭제될 글의 피벗 일괄 정리 * - sirsoft-ecommerce.inquiry.store_validation_rules → 게시판 설정 기반 동적 검증 규칙 주입 * - sirsoft-ecommerce.inquiry.update_validation_rules → 게시판 설정 기반 동적 검증 규칙 주입 */ @@ -37,10 +40,25 @@ class ProductInquiryBoardListener implements HookListenerInterface public static function getSubscribedHooks(): array { return [ - // 게시판 Post 삭제 후 피벗 삭제 (Action 훅) + // 게시판 Post 삭제 후 피벗 정리 (Action 훅) + // sync: 큐 워커 미가동 환경에서 피벗 정리가 누락되지 않도록 동기 실행 (N3) 'sirsoft-board.post.after_delete' => [ 'method' => 'handlePostDeleted', 'priority' => 20, + 'sync' => true, + ], + // 게시판 Post 복원 후 피벗 복원/재마킹 (Action 훅) + 'sirsoft-board.post.after_restore' => [ + 'method' => 'handlePostRestored', + 'priority' => 20, + 'sync' => true, + ], + // 게시판 삭제 시 삭제될 글 ID 벌크 통지 → 피벗 정리 (Action 훅, board 1.0.4+) + // 트랜잭션 내부에서 chunk 당 1회 다회 발화 — 멱등 처리 의무 + 'sirsoft-board.board.posts.before_force_delete' => [ + 'method' => 'handleBoardPostsForceDeleting', + 'priority' => 20, + 'sync' => true, ], // 문의 작성 폼 검증 규칙에 게시판 설정 기반 동적 규칙 주입 (Filter 훅) 'sirsoft-ecommerce.inquiry.store_validation_rules' => [ @@ -60,7 +78,7 @@ class ProductInquiryBoardListener implements HookListenerInterface /** * 기본 훅 핸들러 (HookListenerInterface 필수 메서드) * - * @param mixed ...$args 훅 인자 + * @param mixed ...$args 훅 인자 * @return void */ public function handle(...$args): void @@ -69,11 +87,15 @@ class ProductInquiryBoardListener implements HookListenerInterface } /** - * 게시판 Post 삭제 후 문의 피벗 삭제 + * 게시판 Post 삭제 후 문의 피벗 정리 * - * 게시판에서 Post가 삭제될 때 연결된 문의 피벗 데이터를 정리합니다. - * 이커머스 경로에서는 ProductInquiryService::deleteInquiry()가 피벗을 직접 삭제하므로 - * 이 리스너가 실행되더라도 이미 삭제된 피벗이면 deleteByInquirableIds가 0건 처리합니다. + * 질문 글 삭제: 연결된 문의 피벗을 소프트 삭제합니다. 이커머스 경로에서는 + * ProductInquiryService::deleteInquiry()가 피벗을 직접 삭제하므로 이 리스너가 + * 실행되더라도 이미 삭제된 피벗이면 deleteByInquirableIds가 0건 처리합니다(멱등). + * + * 답변 글 삭제(N4): 게시판 관리자가 답변 글을 직접 지운 경우 — 부모(질문) 피벗의 + * `is_answered` 를 잔여 답변 수 기준으로 재계산합니다. 종전에는 이 분기가 없어 + * "답변완료" 인데 답변이 없는 상태가 남았습니다. * * @param object $post 삭제된 Post 객체 * @param string $slug 게시판 슬러그 @@ -87,6 +109,38 @@ class ProductInquiryBoardListener implements HookListenerInterface } try { + // 답변 글(자식 Post) 분기: 부모 문의의 답변완료 상태 재계산 + if (! empty($post->parent_id)) { + $pivot = $this->repository->findByInquirable(get_class($post), (int) $post->parent_id); + + if ($pivot && $pivot->is_answered) { + // 기본값을 null(판정 불가)로 두고, 공급자(board 모듈 리스너)가 실제 + // 집계값을 실어야만 해제를 진행한다. 기본값 0 이면 공급자 부재 + // (board 비활성/구버전)가 "답변 없음" 으로 읽혀 답변완료가 오해제된다. + $remaining = HookManager::applyFilters( + 'sirsoft-ecommerce.inquiry.count_replies', + null, + (int) $post->parent_id + ); + + if ($remaining === null || ! is_numeric($remaining)) { + return; // 공급자 부재 — 판정 불가 시 상태를 건드리지 않는다 + } + + if ((int) $remaining === 0) { + $this->repository->unmarkAnswered($pivot); + + Log::debug('ProductInquiryBoardListener: 답변 글 삭제 → 답변완료 해제', [ + 'reply_post_id' => $post->id, + 'parent_post_id' => $post->parent_id, + ]); + } + } + + return; + } + + // 질문 글 분기: 피벗 소프트 삭제 (SoftDeletes 도입으로 복원 대칭 확보) $this->repository->deleteByInquirableIds( get_class($post), [$post->id] @@ -96,13 +150,104 @@ class ProductInquiryBoardListener implements HookListenerInterface 'post_id' => $post->id, ]); } catch (\Exception $e) { - Log::error('ProductInquiryBoardListener: 문의 피벗 삭제 실패', [ + Log::error('ProductInquiryBoardListener: 문의 피벗 정리 실패', [ 'post_id' => $post->id ?? null, 'error' => $e->getMessage(), ]); } } + /** + * 게시판 Post 복원 후 문의 피벗 복원/재마킹 + * + * 질문 글 복원: 소프트 삭제됐던 피벗을 복원해 문의 목록에 다시 나타나게 합니다. + * 답변 글 복원: 부모 문의 피벗을 답변완료로 재마킹합니다 (answered_at 은 + * markAsAnswered 가 기존 값을 보존). + * + * @param object $post 복원된 Post 객체 + * @param string $slug 게시판 슬러그 + * @return void + */ + public function handlePostRestored(?object $post, string $slug): void + { + if ($post === null) { + return; + } + + try { + // 답변 글 복원: 부모 문의를 답변완료로 재마킹 + if (! empty($post->parent_id)) { + $pivot = $this->repository->findByInquirable(get_class($post), (int) $post->parent_id); + + if ($pivot && ! $pivot->is_answered) { + $this->repository->markAsAnswered($pivot); + + Log::debug('ProductInquiryBoardListener: 답변 글 복원 → 답변완료 재마킹', [ + 'reply_post_id' => $post->id, + 'parent_post_id' => $post->parent_id, + ]); + } + + return; + } + + // 질문 글 복원: 피벗 복원 + $restored = $this->repository->restoreByInquirable(get_class($post), (int) $post->id); + + if ($restored > 0) { + Log::debug('ProductInquiryBoardListener: 문의 피벗 복원 완료', [ + 'post_id' => $post->id, + 'restored' => $restored, + ]); + } + } catch (\Exception $e) { + Log::error('ProductInquiryBoardListener: 문의 피벗 복원 실패', [ + 'post_id' => $post->id ?? null, + 'error' => $e->getMessage(), + ]); + } + } + + /** + * 게시판 삭제 시 삭제될 글 ID 벌크 수신 → 문의 피벗 소프트 삭제 (N2) + * + * 게시판 삭제는 글별 after_delete 훅 없이 하드 삭제되므로, 이 벌크 훅이 없으면 + * 피벗이 존재하지 않는 Post 를 가리킨 채 잔존합니다(역방향 고아). chunk 당 1회 + * 다회 발화되며 이미 삭제된 피벗은 0건 처리라 멱등합니다. + * + * inquirable_type 은 게시판 Post FQCN 리터럴을 사용합니다 — 이커머스는 board + * 모듈 클래스에 컴파일 타임 의존하지 않습니다(훅 전용 연동). + * + * @param object $board 삭제 중인 게시판 모델 + * @param array<int> $postIds 삭제될 Post ID 배열 (withTrashed 포함) + * @return void + */ + public function handleBoardPostsForceDeleting(?object $board, array $postIds = []): void + { + if ($postIds === []) { + return; + } + + try { + $deleted = $this->repository->deleteByInquirableIds( + 'Modules\\Sirsoft\\Board\\Models\\Post', + $postIds + ); + + if ($deleted > 0) { + Log::debug('ProductInquiryBoardListener: 게시판 삭제 → 문의 피벗 정리', [ + 'board_id' => $board->id ?? null, + 'deleted' => $deleted, + ]); + } + } catch (\Exception $e) { + Log::error('ProductInquiryBoardListener: 게시판 삭제 피벗 정리 실패', [ + 'board_id' => $board->id ?? null, + 'error' => $e->getMessage(), + ]); + } + } + /** * 게시판 설정 기반 동적 검증 규칙 주입 * @@ -110,7 +255,7 @@ class ProductInquiryBoardListener implements HookListenerInterface * StoreInquiryRequest의 검증 규칙에 동적으로 주입합니다. * * @param array $rules 기존 검증 규칙 - * @param \Illuminate\Foundation\Http\FormRequest $request 요청 객체 + * @param FormRequest $request 요청 객체 * @return array 게시판 설정이 반영된 검증 규칙 */ public function injectBoardValidationRules(array $rules, $request): array @@ -158,5 +303,4 @@ class ProductInquiryBoardListener implements HookListenerInterface return $rules; } - } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Listeners/SearchProductsListener.php b/modules/_bundled/sirsoft-ecommerce/src/Listeners/SearchProductsListener.php index fcea15f8..e42c673a 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Listeners/SearchProductsListener.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Listeners/SearchProductsListener.php @@ -6,6 +6,7 @@ use App\Contracts\Extension\HookListenerInterface; use App\Enums\TotalRelation; use App\Helpers\PermissionHelper; use App\Search\SearchCategoryPayload; +use App\Search\SearchHighlighter; use App\Support\Query\BoundedCount; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Http\Resources\Traits\HasMultiCurrencyPrices; @@ -171,10 +172,14 @@ class SearchProductsListener implements HookListenerInterface $results['products'] = SearchCategoryPayload::fromBounded($searchPage, $format($searchPage->items())); } catch (\Exception $e) { + // 실패를 카테고리 키 미설정으로 삼키면 화면이 "검색 결과 없음" 을 그린다 — + // failed 페이로드로 표면화하고, 원인 추적을 위해 스택을 함께 남긴다 (#103). Log::error('Search products error', [ 'message' => $e->getMessage(), 'q' => $q, + 'exception' => $e, ]); + $results['products'] = SearchCategoryPayload::failed(); } return $results; @@ -287,13 +292,7 @@ class SearchProductsListener implements HookListenerInterface */ private function highlightKeyword(?string $text, string $keyword): string { - if (empty($text) || empty($keyword)) { - return $text ?? ''; - } - - $escapedKeyword = preg_quote($keyword, '/'); - - return preg_replace('/('.$escapedKeyword.')/iu', '<mark>$1</mark>', $text); + return SearchHighlighter::highlight($text, $keyword); } /** @@ -310,7 +309,7 @@ class SearchProductsListener implements HookListenerInterface return ''; } - $plainText = trim(preg_replace('/\s+/', ' ', html_entity_decode(strip_tags($content)))); + $plainText = SearchHighlighter::toPlainText($content); $position = mb_stripos($plainText, $keyword); if ($position !== false) { diff --git a/modules/_bundled/sirsoft-ecommerce/src/Listeners/ShippingPolicyCacheListener.php b/modules/_bundled/sirsoft-ecommerce/src/Listeners/ShippingPolicyCacheListener.php new file mode 100644 index 00000000..a094fec9 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Listeners/ShippingPolicyCacheListener.php @@ -0,0 +1,73 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Listeners; + +use App\Contracts\Extension\HookListenerInterface; +use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyResolver; + +/** + * 배송정책 변경 시 해석기 캐시 무효화 리스너 + * + * `ShippingPolicyResolver` 는 싱글톤이라 기본 배송정책을 요청 내 1회만 조회한다. + * 그래서 한 요청이 배송정책을 바꾼 뒤에도 그 요청 안에서는 변경 전 정책이 계속 쓰인다. + * + * 무효화 호출을 서비스 메서드마다 흩어 놓지 않고 이 리스너 한 곳에 모은다 — 배송정책을 + * 바꾸는 지점은 이미 전부 `after_*` 훅을 발화하므로, 새 변경 경로가 생겨도 같은 훅만 + * 발화하면 무효화가 자동으로 따라온다. + */ +class ShippingPolicyCacheListener implements HookListenerInterface +{ + /** + * 구독할 훅 목록 반환 + * + * @return array<string, array{method: string, priority: int}> 훅 이름 → 메서드/우선순위 매핑 + */ + public static function getSubscribedHooks(): array + { + $hooks = []; + + foreach ([ + 'after_create', + 'after_update', + 'after_delete', + 'after_bulk_delete', + 'after_toggle_active', + 'after_bulk_toggle_active', + 'after_set_default', + ] as $event) { + $hooks['sirsoft-ecommerce.shipping_policy.'.$event] = [ + 'method' => 'onShippingPolicyChanged', + 'priority' => 5, + ]; + } + + return $hooks; + } + + /** + * 기본 훅 핸들러 (HookListenerInterface 필수 메서드) + * + * @param mixed ...$args 훅 인자 + */ + public function handle(...$args): void + { + $this->onShippingPolicyChanged(...$args); + } + + /** + * 배송정책이 바뀌면 해석기의 기본 배송정책 캐시를 비웁니다. + * + * 아직 해석기를 해석하지 않은 요청에서는 새로 만들 필요가 없다 — 처음 조회할 때 + * 이미 변경 후 값을 읽는다. + * + * @param mixed ...$args 훅 인자 (배송정책 또는 ID 목록 — 무효화는 대상과 무관) + */ + public function onShippingPolicyChanged(...$args): void + { + if (! app()->resolved(ShippingPolicyResolver::class)) { + return; + } + + app(ShippingPolicyResolver::class)->flushCache(); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/CategoryImage.php b/modules/_bundled/sirsoft-ecommerce/src/Models/CategoryImage.php index 24d88cd2..e8eeb966 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Models/CategoryImage.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/CategoryImage.php @@ -6,12 +6,14 @@ use App\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Modules\Sirsoft\Ecommerce\Models\Concerns\HasDirectAssetUrl; /** * 카테고리 이미지 모델 */ class CategoryImage extends Model { + use HasDirectAssetUrl; use SoftDeletes; protected $table = 'ecommerce_category_images'; @@ -108,11 +110,11 @@ class CategoryImage extends Model } /** - * `download_url` 가상 attribute — hash 기반 카테고리 이미지 서빙 API URL. + * hash 기반 카테고리 이미지 서빙 API URL (직접 URL 불가 시 폴백). * * @return string `/api/modules/sirsoft-ecommerce/category-image/{hash}` 형식 URL */ - public function getDownloadUrlAttribute(): string + protected function apiDownloadUrl(): string { return '/api/modules/sirsoft-ecommerce/category-image/'.$this->hash; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/Concerns/HasDirectAssetUrl.php b/modules/_bundled/sirsoft-ecommerce/src/Models/Concerns/HasDirectAssetUrl.php new file mode 100644 index 00000000..4e3a06f8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/Concerns/HasDirectAssetUrl.php @@ -0,0 +1,65 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Models\Concerns; + +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; + +/** + * 공개 자산 모델 공용 download_url 해석 trait + * + * 행에 기록된 disk 로 직접 URL(CDN) 생성을 시도하고, 불가하면(로컬 디스크, + * url 미설정 디스크, 훅 차단 등) 기존 API 서빙 경로로 폴백합니다. + * 디스크 전환 이전에 업로드된 로컬 행과 이후의 원격 행이 혼재해도 + * 각 행이 자기 disk 기준으로 올바른 URL 을 얻습니다. + * + * 스토리지 카테고리는 directUrlCategory() 오버라이드로 바꿀 수 있어(기본 'images') + * 이미지 외의 완전 공개 자산 모델도 재사용할 수 있습니다. 단, 권한 검사가 걸린 + * 첨부파일(비밀글/회원 전용 게시판 등)에는 배선하지 않습니다 — 직접 URL 은 + * 서버 스트리밍 경로의 권한 검사를 우회합니다. + */ +trait HasDirectAssetUrl +{ + /** + * `download_url` 가상 attribute — 직접 URL 우선, 불가 시 API 서빙 URL. + * + * @return string 자산 URL + */ + public function getDownloadUrlAttribute(): string + { + return $this->resolveDirectUrl() ?? $this->apiDownloadUrl(); + } + + /** + * 행 disk 기준 직접 URL 해석을 시도합니다. + * + * @return string|null 직접 URL (불가하면 null) + */ + protected function resolveDirectUrl(): ?string + { + $disk = (string) ($this->disk ?? ''); + $path = (string) ($this->path ?? ''); + + if ($disk === '' || $path === '') { + return null; + } + + return AssetStorage::forDisk($disk)->url($this->directUrlCategory(), $path); + } + + /** + * 직접 URL 해석에 쓸 스토리지 카테고리를 반환합니다. + * + * @return string 스토리지 카테고리 (기본 'images') + */ + protected function directUrlCategory(): string + { + return 'images'; + } + + /** + * 기존 API 서빙 경로를 반환합니다 (직접 URL 불가 시 폴백). + * + * @return string API 서빙 URL + */ + abstract protected function apiDownloadUrl(): string; +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductImage.php b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductImage.php index 108e8afa..b9b32042 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductImage.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductImage.php @@ -3,16 +3,19 @@ namespace Modules\Sirsoft\Ecommerce\Models; use App\Models\User; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; use Modules\Sirsoft\Ecommerce\Enums\ProductImageCollection; +use Modules\Sirsoft\Ecommerce\Models\Concerns\HasDirectAssetUrl; /** * 상품 이미지 모델 */ class ProductImage extends Model { + use HasDirectAssetUrl; use SoftDeletes; protected $table = 'ecommerce_product_images'; @@ -116,9 +119,9 @@ class ProductImage extends Model /** * 특정 컬렉션(main/gallery/detail 등) 의 이미지만 필터링하는 쿼리 스코프. * - * @param \Illuminate\Database\Eloquent\Builder $query + * @param Builder $query * @param ProductImageCollection $collection 필터링할 이미지 컬렉션 enum - * @return \Illuminate\Database\Eloquent\Builder + * @return Builder */ public function scopeByCollection($query, ProductImageCollection $collection) { @@ -128,8 +131,8 @@ class ProductImage extends Model /** * 대표 이미지만 조회 * - * @param \Illuminate\Database\Eloquent\Builder $query - * @return \Illuminate\Database\Eloquent\Builder + * @param Builder $query + * @return Builder */ public function scopeThumbnails($query) { @@ -137,11 +140,11 @@ class ProductImage extends Model } /** - * `download_url` 가상 attribute — hash 기반 상품 이미지 서빙 API URL. + * hash 기반 상품 이미지 서빙 API URL (직접 URL 불가 시 폴백). * * @return string `/api/modules/sirsoft-ecommerce/product-image/{hash}` 형식 URL */ - public function getDownloadUrlAttribute(): string + protected function apiDownloadUrl(): string { return '/api/modules/sirsoft-ecommerce/product-image/'.$this->hash; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductInquiry.php b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductInquiry.php index e6fe459f..26146ee3 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductInquiry.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductInquiry.php @@ -3,28 +3,36 @@ namespace Modules\Sirsoft\Ecommerce\Models; use App\Models\User; +use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\SoftDeletes; +use Modules\Sirsoft\Ecommerce\Database\Factories\ProductInquiryFactory; /** * 상품 1:1 문의 피벗 모델 * * 이커머스 상품과 외부 컨텐츠(게시판 게시글 등)를 다형성 관계로 연결합니다. + * + * SoftDeletes: 게시판에서 질문 글을 삭제→복원하는 경로에서 피벗도 함께 + * 복원할 수 있어야 한다 — 하드 삭제는 복원 대칭이 불가능했다(#107 후속). + * 상품 자체가 forceDelete 되는 경로만 피벗도 forceDelete 한다. */ class ProductInquiry extends Model { use HasFactory; + use SoftDeletes; /** * Factory 클래스 경로 지정 * * @return string */ - protected static function newFactory(): \Illuminate\Database\Eloquent\Factories\Factory + protected static function newFactory(): Factory { - return \Modules\Sirsoft\Ecommerce\Database\Factories\ProductInquiryFactory::new(); + return ProductInquiryFactory::new(); } protected $table = 'ecommerce_product_inquiries'; diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductReviewImage.php b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductReviewImage.php index 9f3338e0..ec29656a 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Models/ProductReviewImage.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/ProductReviewImage.php @@ -7,13 +7,16 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\URL; use Modules\Sirsoft\Ecommerce\Database\Factories\ProductReviewImageFactory; +use Modules\Sirsoft\Ecommerce\Models\Concerns\HasDirectAssetUrl; /** * 상품 리뷰 이미지 모델 */ class ProductReviewImage extends Model { + use HasDirectAssetUrl; use HasFactory, SoftDeletes; protected $table = 'ecommerce_product_review_images'; @@ -103,12 +106,45 @@ class ProductReviewImage extends Model } /** - * 다운로드 URL 반환 (API 서빙 URL) + * hash 기반 리뷰 이미지 서빙 API URL (직접 URL 불가 시 폴백). * * @return string */ - public function getDownloadUrlAttribute(): string + protected function apiDownloadUrl(): string { return '/api/modules/sirsoft-ecommerce/review-image/'.$this->hash; } + + /** + * 숨김(HIDDEN) 리뷰 이미지의 <img> 렌더용 한시 서명 다운로드 URL 을 반환합니다. + * + * 브라우저 <img src> 는 Authorization 헤더를 실을 수 없으므로, 숨김 리뷰의 + * 이미지를 권한자 화면(관리자 리뷰 목록·상세)에 표시할 때는 한시 서명 URL 을 + * 발급한다. 서명 URL 은 게이트(관리자 권한 미들웨어)를 통과한 응답 직렬화 + * 시점에만 발급되고(ProductReviewResource), 서빙 엔드포인트가 유효 서명을 + * 무인증 허용한다 — 무서명 요청의 상태 게이트는 종전과 동일하다. + * + * 직접 URL(CDN)과 무관하게 항상 API 서빙 경로에 서명한다 — 게이트가 있는 + * 경로는 API 서빙뿐이고, 직접 URL 가용 여부로 분기하면 저장 모드에 따라 + * 관리자 화면 동작이 갈린다. + * + * @return string 한시 서명 다운로드 URL (상대경로) + */ + public function signedDownloadUrl(): string + { + return URL::temporarySignedRoute( + 'api.modules.sirsoft-ecommerce.review-image.download', + now()->addMinutes(self::SIGNED_DOWNLOAD_TTL_MINUTES), + ['hash' => $this->hash], + absolute: false + ); + } + + /** + * 서명 다운로드 URL 의 유효 시간(분). + * + * 관리자 리뷰 화면이 열려 있는 동안 썸네일이 유지될 만큼 길고, + * URL 유출 시 노출 창을 좁힐 만큼 짧은 값. + */ + public const SIGNED_DOWNLOAD_TTL_MINUTES = 30; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Models/ShippingPolicyCountrySetting.php b/modules/_bundled/sirsoft-ecommerce/src/Models/ShippingPolicyCountrySetting.php index 9688539b..95470a7c 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Models/ShippingPolicyCountrySetting.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Models/ShippingPolicyCountrySetting.php @@ -124,29 +124,88 @@ class ShippingPolicyCountrySetting extends Model return ''; } - $tiers = $this->ranges['tiers']; - $unit = $tiers[0]['unit'] ?? ''; $parts = []; - foreach ($tiers as $tier) { - $min = $tier['min'] ?? 0; - $max = $tier['max'] ?? null; + foreach ($this->ranges['tiers'] as $tier) { $fee = ecommerce_format_price($tier['fee'] ?? 0); - - if ($min === 0) { - $range = "~{$max}{$unit}"; - } elseif ($max === null) { - $range = "{$min}{$unit}~"; - } else { - $range = "{$min}~{$max}{$unit}"; - } - - $parts[] = "{$range}: {$fee}"; + $parts[] = $this->formatTierRangeLabel($tier).": {$fee}"; } return implode(' / ', $parts); } + /** + * 구간의 범위 라벨을 포맷합니다 (예: "~5개", "6개~", "2~5kg"). + * + * 단위는 부과정책에서 파생합니다. tier 의 `unit` 키는 시더만 채우던 값이라 + * 관리자 화면에서 저장한 정책에는 존재하지 않아 단위가 사라졌습니다. + * + * @param array $tier 구간 정의 + * @return string 범위 라벨 + */ + protected function formatTierRangeLabel(array $tier): string + { + $min = $tier['min'] ?? 0; + $max = $tier['max'] ?? null; + $isAmount = $this->charge_policy === ChargePolicyEnum::RANGE_AMOUNT; + + $format = fn ($value) => $isAmount + ? ecommerce_format_price($value ?? 0) + : $this->formatRangeNumber($value).$this->resolveRangeUnitLabel(); + + // 첫 구간(시작 0)은 상한만, 마지막 구간(상한 없음)은 시작값만 표기 + if ((float) $min == 0.0) { + return $max === null || $max === '' ? '~' : '~'.$format($max); + } + + if ($max === null || $max === '') { + return $format($min).'~'; + } + + return $isAmount + ? $format($min).'~'.$format($max) + : $this->formatRangeNumber($min).'~'.$format($max); + } + + /** + * 부과정책에서 구간 단위 라벨을 파생합니다. + * + * @return string 단위 라벨 (해당 없으면 빈 문자열) + */ + protected function resolveRangeUnitLabel(): string + { + $key = match ($this->charge_policy) { + ChargePolicyEnum::RANGE_QUANTITY => 'quantity', + ChargePolicyEnum::RANGE_WEIGHT, ChargePolicyEnum::RANGE_VOLUME_WEIGHT => 'weight', + ChargePolicyEnum::RANGE_VOLUME => 'volume', + default => null, + }; + + return $key === null + ? '' + : __('sirsoft-ecommerce::messages.shipping_policy.fee_summary.range_unit.'.$key); + } + + /** + * 구간 경계값을 소수 손실 없이 포맷합니다. + * + * number_format 은 기본 소수 0자리라 0.5kg 이 "1" 로 표시됩니다. + * + * @param int|float|string|null $value 경계값 + * @return string 포맷된 숫자 + */ + protected function formatRangeNumber(int|float|string|null $value): string + { + $number = (float) ($value ?? 0); + + if (floor($number) == $number) { + return number_format($number); + } + + // 불필요한 뒤쪽 0 제거 (2.50 → 2.5) + return rtrim(rtrim(number_format($number, 3), '0'), '.'); + } + /** * 단위당 배송비 요약 포맷 * @@ -156,8 +215,13 @@ class ShippingPolicyCountrySetting extends Model { $unitValue = $this->ranges['unit_value'] ?? 1; + // 금액당 정책의 단위값은 금액이므로 통화 표기를 붙인다 (그 외는 개/kg/L 로 문구가 단위를 갖는다) + $unit = $this->charge_policy === ChargePolicyEnum::PER_AMOUNT + ? ecommerce_format_price($unitValue) + : $this->formatRangeNumber($unitValue); + return __('sirsoft-ecommerce::messages.shipping_policy.fee_summary.'.$this->charge_policy->value, [ - 'unit' => number_format($unitValue), + 'unit' => $unit, 'fee' => ecommerce_format_price($this->base_fee ?? 0), ]); } @@ -190,26 +254,12 @@ class ShippingPolicyCountrySetting extends Model return null; } - $tiers = $this->ranges['tiers']; - $unit = $tiers[0]['unit'] ?? ''; $result = []; - foreach ($tiers as $tier) { - $min = $tier['min'] ?? 0; - $max = $tier['max'] ?? null; - $fee = $tier['fee'] ?? 0; - - if ($min === 0) { - $range = '~'.number_format($max).$unit; - } elseif ($max === null) { - $range = number_format($min).$unit.'~'; - } else { - $range = number_format($min).'~'.number_format($max).$unit; - } - + foreach ($this->ranges['tiers'] as $tier) { $result[] = [ - 'range' => $range, - 'fee' => ecommerce_format_price($fee), + 'range' => $this->formatTierRangeLabel($tier), + 'fee' => ecommerce_format_price($tier['fee'] ?? 0), ]; } @@ -219,7 +269,7 @@ class ShippingPolicyCountrySetting extends Model /** * 우편번호가 도서산간 지역인지 확인하고 추가배송비를 반환합니다. * - * @param string|null $zipcode 우편번호 + * @param string|null $zipcode 우편번호 * @return int 추가배송비 (도서산간 아닌 경우 0) */ public function getExtraFeeForZipcode(?string $zipcode): int @@ -245,10 +295,12 @@ class ShippingPolicyCountrySetting extends Model } // 범위 지원: "63000-63999" + // 자릿수가 다른 우편번호에서 문자열 비교가 오판정하므로 숫자로 비교한다. if (preg_match('/^(\d+)-(\d+)$/', $pattern, $matches)) { - $start = $matches[1]; - $end = $matches[2]; - if ($normalizedZipcode >= $start && $normalizedZipcode <= $end) { + $start = (int) $matches[1]; + $end = (int) $matches[2]; + $numericZipcode = (int) $normalizedZipcode; + if ($numericZipcode >= $start && $numericZipcode <= $end) { return $fee; } @@ -277,7 +329,7 @@ class ShippingPolicyCountrySetting extends Model /** * 우편번호가 도서산간 지역인지 확인합니다. * - * @param string|null $zipcode 우편번호 + * @param string|null $zipcode 우편번호 * @return bool 도서산간 지역 여부 */ public function isRemoteArea(?string $zipcode): bool diff --git a/modules/_bundled/sirsoft-ecommerce/src/Providers/EcommerceServiceProvider.php b/modules/_bundled/sirsoft-ecommerce/src/Providers/EcommerceServiceProvider.php index 6bcc6f11..681180c0 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Providers/EcommerceServiceProvider.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Providers/EcommerceServiceProvider.php @@ -10,6 +10,8 @@ use Modules\Sirsoft\Ecommerce\Console\Commands\EarnMileageCommand; use Modules\Sirsoft\Ecommerce\Console\Commands\ExpireMileageCommand; use Modules\Sirsoft\Ecommerce\Console\Commands\NotifyExpiringMileageCommand; use Modules\Sirsoft\Ecommerce\Console\Commands\PruneExpiredCartsCommand; +use Modules\Sirsoft\Ecommerce\Console\Commands\PruneExpiredTempOrdersCommand; +use Modules\Sirsoft\Ecommerce\Console\Commands\PruneTempProductImagesCommand; use Modules\Sirsoft\Ecommerce\Console\Commands\ReconcileMileageBalanceCommand; use Modules\Sirsoft\Ecommerce\Http\Middleware\DetectDevice; use Modules\Sirsoft\Ecommerce\Repositories\BrandRepository; @@ -95,6 +97,7 @@ use Modules\Sirsoft\Ecommerce\Repositories\UserAddressRepository; use Modules\Sirsoft\Ecommerce\Seo\EcommerceSitemapContributor; use Modules\Sirsoft\Ecommerce\Services\CategoryImageService; use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; use Modules\Sirsoft\Ecommerce\Services\PaymentMethodResolver; use Modules\Sirsoft\Ecommerce\Services\ProductImageService; use Modules\Sirsoft\Ecommerce\Services\ProductReviewImageService; @@ -119,10 +122,21 @@ class EcommerceServiceProvider extends BaseModuleServiceProvider * @var array<int, class-string> */ protected array $storageServices = [ - CategoryImageService::class, - ProductImageService::class, ProductReviewService::class, - ProductReviewImageService::class, + ]; + + /** + * 카테고리별 StorageInterface가 필요한 서비스 매핑 (클래스 ⇒ 카테고리) + * + * 이미지 서비스는 getStorageDiskFor('images') 가 결정한 디스크(공개 자산 디스크 + * 설정 반영)를 주입받아, put/getDisk() 행 기록이 자동으로 카테고리 디스크를 따릅니다. + * + * @var array<class-string, string> + */ + protected array $storageCategoryServices = [ + CategoryImageService::class => 'images', + ProductImageService::class => 'images', + ProductReviewImageService::class => 'images', ]; /** @@ -185,6 +199,8 @@ class EcommerceServiceProvider extends BaseModuleServiceProvider ExpireMileageCommand::class, NotifyExpiringMileageCommand::class, PruneExpiredCartsCommand::class, + PruneExpiredTempOrdersCommand::class, + PruneTempProductImagesCommand::class, ReconcileMileageBalanceCommand::class, ]; @@ -203,6 +219,12 @@ class EcommerceServiceProvider extends BaseModuleServiceProvider // PaymentMethodResolver를 싱글톤으로 등록 (요청 내 결제수단 카탈로그 조회 1회 캐시) $this->app->singleton(PaymentMethodResolver::class); + + // EcommerceSettingsService를 싱글톤으로 등록 (공개 #116) + // 위 3종과 달리 미등록이라 주입 지점마다 별개 인스턴스가 만들어졌고, 각자 자기 설정 + // 캐시를 들고 있었다. 싱글톤 리졸버가 비-싱글톤 설정 서비스를 captive 로 보유하는 + // 비대칭도 함께 생긴다. 쓰기 메서드가 모두 자기 캐시를 무효화하므로 공유해도 안전하다. + $this->app->singleton(EcommerceSettingsService::class); } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/CategoryImageRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/CategoryImageRepository.php index b250f70b..1e581ef5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/CategoryImageRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/CategoryImageRepository.php @@ -90,6 +90,14 @@ class CategoryImageRepository implements CategoryImageRepositoryInterface return $image?->delete() ?? false; } + /** + * {@inheritDoc} + */ + public function deleteByCategoryId(int $categoryId): int + { + return $this->model->where('category_id', $categoryId)->delete(); + } + /** * {@inheritDoc} */ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/CategoryImageRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/CategoryImageRepositoryInterface.php index 792629ed..ea2f2b6b 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/CategoryImageRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/CategoryImageRepositoryInterface.php @@ -13,7 +13,7 @@ interface CategoryImageRepositoryInterface /** * ID로 이미지 조회 * - * @param int $id 이미지 ID + * @param int $id 이미지 ID * @return CategoryImage|null */ public function findById(int $id): ?CategoryImage; @@ -21,7 +21,7 @@ interface CategoryImageRepositoryInterface /** * 해시로 이미지 조회 * - * @param string $hash 이미지 해시 + * @param string $hash 이미지 해시 * @return CategoryImage|null */ public function findByHash(string $hash): ?CategoryImage; @@ -29,8 +29,8 @@ interface CategoryImageRepositoryInterface /** * 카테고리의 모든 이미지 조회 * - * @param int $categoryId 카테고리 ID - * @param string|null $collection 컬렉션 필터 + * @param int $categoryId 카테고리 ID + * @param string|null $collection 컬렉션 필터 * @return Collection */ public function getByCategoryId(int $categoryId, ?string $collection = null): Collection; @@ -38,8 +38,8 @@ interface CategoryImageRepositoryInterface /** * temp_key로 임시 이미지 조회 * - * @param string $tempKey 임시 키 - * @param string|null $collection 컬렉션 필터 + * @param string $tempKey 임시 키 + * @param string|null $collection 컬렉션 필터 * @return Collection */ public function getByTempKey(string $tempKey, ?string $collection = null): Collection; @@ -47,7 +47,7 @@ interface CategoryImageRepositoryInterface /** * 카테고리 이미지 생성 * - * @param array $data 이미지 데이터 + * @param array $data 이미지 데이터 * @return CategoryImage */ public function create(array $data): CategoryImage; @@ -55,8 +55,8 @@ interface CategoryImageRepositoryInterface /** * 이미지 수정 * - * @param int $id 이미지 ID - * @param array $data 수정할 데이터 + * @param int $id 이미지 ID + * @param array $data 수정할 데이터 * @return CategoryImage */ public function update(int $id, array $data): CategoryImage; @@ -64,16 +64,26 @@ interface CategoryImageRepositoryInterface /** * 이미지 삭제 * - * @param int $id 이미지 ID + * @param int $id 이미지 ID * @return bool */ public function delete(int $id): bool; + /** + * 카테고리에 속한 이미지 행을 일괄 삭제 + * + * 물리 파일 삭제는 호출한 Service 가 담당합니다 (행마다 disk 가 다를 수 있음). + * + * @param int $categoryId 카테고리 ID + * @return int 삭제된 행 수 + */ + public function deleteByCategoryId(int $categoryId): int; + /** * 임시 이미지를 카테고리에 연결 * - * @param string $tempKey 임시 키 - * @param int $categoryId 카테고리 ID + * @param string $tempKey 임시 키 + * @param int $categoryId 카테고리 ID * @return int 연결된 이미지 수 */ public function linkTempImages(string $tempKey, int $categoryId): int; @@ -81,8 +91,8 @@ interface CategoryImageRepositoryInterface /** * 카테고리의 최대 sort_order 조회 * - * @param int $categoryId 카테고리 ID - * @param string $collection 컬렉션명 + * @param int $categoryId 카테고리 ID + * @param string $collection 컬렉션명 * @return int */ public function getMaxSortOrder(int $categoryId, string $collection): int; @@ -90,8 +100,8 @@ interface CategoryImageRepositoryInterface /** * temp_key의 최대 sort_order 조회 * - * @param string $tempKey 임시 키 - * @param string $collection 컬렉션명 + * @param string $tempKey 임시 키 + * @param string $collection 컬렉션명 * @return int */ public function getMaxSortOrderByTempKey(string $tempKey, string $collection): int; @@ -99,7 +109,7 @@ interface CategoryImageRepositoryInterface /** * 순서 변경 * - * @param array<int, int> $orders 이미지 ID => sort_order 매핑 + * @param array<int, int> $orders 이미지 ID => sort_order 매핑 * @return bool */ public function reorder(array $orders): bool; diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ExtraFeeTemplateRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ExtraFeeTemplateRepositoryInterface.php index be944968..1bb7f8dd 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ExtraFeeTemplateRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ExtraFeeTemplateRepositoryInterface.php @@ -14,7 +14,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * ID로 템플릿 조회 * - * @param int $id 템플릿 ID + * @param int $id 템플릿 ID * @return ExtraFeeTemplate|null */ public function find(int $id): ?ExtraFeeTemplate; @@ -22,16 +22,24 @@ interface ExtraFeeTemplateRepositoryInterface /** * 우편번호로 템플릿 조회 * - * @param string $zipcode 우편번호 + * @param string $zipcode 우편번호 * @return ExtraFeeTemplate|null */ public function findByZipcode(string $zipcode): ?ExtraFeeTemplate; + /** + * 여러 우편번호로 템플릿을 한 번에 조회 + * + * @param array $zipcodes 우편번호 목록 + * @return Collection + */ + public function findByZipcodes(array $zipcodes): Collection; + /** * 필터링된 템플릿 목록 조회 (페이지네이션) * - * @param array $filters 필터 조건 - * @param int $perPage 페이지당 개수 + * @param array $filters 필터 조건 + * @param int $perPage 페이지당 개수 * @return LengthAwarePaginator */ public function getListWithFilters(array $filters, int $perPage = 20): LengthAwarePaginator; @@ -39,7 +47,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 생성 * - * @param array $data 템플릿 데이터 + * @param array $data 템플릿 데이터 * @return ExtraFeeTemplate */ public function create(array $data): ExtraFeeTemplate; @@ -47,8 +55,8 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 수정 * - * @param ExtraFeeTemplate $template 템플릿 모델 - * @param array $data 수정 데이터 + * @param ExtraFeeTemplate $template 템플릿 모델 + * @param array $data 수정 데이터 * @return ExtraFeeTemplate */ public function update(ExtraFeeTemplate $template, array $data): ExtraFeeTemplate; @@ -56,7 +64,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 삭제 * - * @param ExtraFeeTemplate $template 템플릿 모델 + * @param ExtraFeeTemplate $template 템플릿 모델 * @return bool */ public function delete(ExtraFeeTemplate $template): bool; @@ -64,7 +72,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 사용여부 토글 * - * @param ExtraFeeTemplate $template 템플릿 모델 + * @param ExtraFeeTemplate $template 템플릿 모델 * @return ExtraFeeTemplate */ public function toggleActive(ExtraFeeTemplate $template): ExtraFeeTemplate; @@ -72,7 +80,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 일괄 삭제 * - * @param array $ids 템플릿 ID 배열 + * @param array $ids 템플릿 ID 배열 * @return int 삭제된 개수 */ public function bulkDelete(array $ids): int; @@ -80,8 +88,8 @@ interface ExtraFeeTemplateRepositoryInterface /** * 템플릿 일괄 사용여부 변경 * - * @param array $ids 템플릿 ID 배열 - * @param bool $isActive 사용여부 + * @param array $ids 템플릿 ID 배열 + * @param bool $isActive 사용여부 * @return int 변경된 개수 */ public function bulkToggleActive(array $ids, bool $isActive): int; @@ -103,7 +111,7 @@ interface ExtraFeeTemplateRepositoryInterface /** * 일괄 등록 (CSV 또는 엑셀 업로드용) * - * @param array $items 템플릿 데이터 배열 [{zipcode, fee, region?, description?}] + * @param array $items 템플릿 데이터 배열 [{zipcode, fee, region?, description?}] * @return int 등록된 개수 */ public function bulkCreate(array $items): int; @@ -119,7 +127,7 @@ interface ExtraFeeTemplateRepositoryInterface * ID 목록으로 추가비용 템플릿을 조회하고 ID 키 맵으로 반환합니다 (bulk activity log lookup). * * @param array<int, int> $ids 템플릿 ID 목록 - * @return \Illuminate\Database\Eloquent\Collection + * @return Collection */ - public function findByIdsKeyed(array $ids): \Illuminate\Database\Eloquent\Collection; + public function findByIdsKeyed(array $ids): Collection; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductImageRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductImageRepositoryInterface.php index 82e16160..a839f867 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductImageRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductImageRepositoryInterface.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Repositories\Contracts; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Modules\Sirsoft\Ecommerce\Models\ProductImage; /** @@ -69,6 +70,18 @@ interface ProductImageRepositoryInterface */ public function delete(int $id): bool; + /** + * 상품에 연결되지 않은 채 방치된 임시 이미지를 오래된 순으로 조회 + * + * 상품 등록 폼에서 업로드만 하고 저장 없이 이탈하면 `temp_key` 가 남은 채 + * `product_id` 가 비어 있는 행과 그 파일이 영구 잔존합니다. 그 회수 대상을 찾습니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 업로드가 대상) + * @param int $limit 최대 조회 건수 + * @return Collection<int, ProductImage> 임시 이미지 목록 (created_at 오름차순) + */ + public function findStaleTempImages(Carbon $threshold, int $limit): Collection; + /** * 임시 이미지를 상품에 연결 * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductInquiryRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductInquiryRepositoryInterface.php index 47a1f7a0..087d88cf 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductInquiryRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ProductInquiryRepositoryInterface.php @@ -32,9 +32,10 @@ interface ProductInquiryRepositoryInterface * * @param int $productId 상품 ID * @param int $perPage 페이지당 개수 + * @param int|null $page 페이지 번호 (null 이면 요청 파라미터에서 해석) * @return LengthAwarePaginator */ - public function paginateByProductId(int $productId, int $perPage = 10): LengthAwarePaginator; + public function paginateByProductId(int $productId, int $perPage = 10, ?int $page = null): LengthAwarePaginator; /** * inquirable_id로 문의 조회 (단일) @@ -105,6 +106,37 @@ interface ProductInquiryRepositoryInterface */ public function deleteByInquirableIds(string $inquirableType, array $inquirableIds): int; + /** + * 상품 ID 로 피벗 전건 조회 (소프트 삭제 포함) + * + * 상품 삭제 시 문의 스레드 정리 경로에서 사용합니다. + * + * @param int $productId 상품 ID + * @return Collection<int, ProductInquiry> 피벗 컬렉션 + */ + public function findByProductIdWithTrashed(int $productId): Collection; + + /** + * 상품 ID 로 피벗 전건 영구 삭제 (소프트 삭제 포함) + * + * 상품이 forceDelete 되는 경로 전용 — 그 외 삭제는 소프트 삭제를 유지합니다. + * + * @param int $productId 상품 ID + * @return int 삭제된 피벗 수 + */ + public function forceDeleteByProductId(int $productId): int; + + /** + * 대상(inquirable) 기준으로 소프트 삭제된 피벗을 복원합니다. + * + * 게시판에서 질문 글이 삭제→복원될 때 피벗을 함께 되살리는 경로입니다. + * + * @param string $inquirableType 대상 모델 클래스명 + * @param int $inquirableId 대상 ID + * @return int 복원된 피벗 수 + */ + public function restoreByInquirable(string $inquirableType, int $inquirableId): int; + /** * 전체 상품의 최신 미답변 문의를 조회합니다 (대시보드 미답변 문의). * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingCarrierRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingCarrierRepositoryInterface.php index 9f49b9b5..55ee99f6 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingCarrierRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingCarrierRepositoryInterface.php @@ -13,8 +13,8 @@ interface ShippingCarrierRepositoryInterface /** * 배송사 목록 조회 * - * @param array $filters 필터 조건 - * @param array $with Eager loading 관계 + * @param array $filters 필터 조건 + * @param array $with Eager loading 관계 * @return Collection */ public function getAll(array $filters = [], array $with = []): Collection; @@ -22,8 +22,8 @@ interface ShippingCarrierRepositoryInterface /** * ID로 배송사 조회 * - * @param int $id 배송사 ID - * @param array $with Eager loading 관계 + * @param int $id 배송사 ID + * @param array $with Eager loading 관계 * @return ShippingCarrier|null */ public function findById(int $id, array $with = []): ?ShippingCarrier; @@ -31,7 +31,7 @@ interface ShippingCarrierRepositoryInterface /** * 배송사 생성 * - * @param array $data 배송사 데이터 + * @param array $data 배송사 데이터 * @return ShippingCarrier */ public function create(array $data): ShippingCarrier; @@ -39,8 +39,8 @@ interface ShippingCarrierRepositoryInterface /** * 배송사 수정 * - * @param int $id 배송사 ID - * @param array $data 수정 데이터 + * @param int $id 배송사 ID + * @param array $data 수정 데이터 * @return ShippingCarrier */ public function update(int $id, array $data): ShippingCarrier; @@ -48,7 +48,7 @@ interface ShippingCarrierRepositoryInterface /** * 배송사 삭제 * - * @param int $id 배송사 ID + * @param int $id 배송사 ID * @return bool */ public function delete(int $id): bool; @@ -56,8 +56,8 @@ interface ShippingCarrierRepositoryInterface /** * 코드 중복 확인 * - * @param string $code 배송사 코드 - * @param int|null $excludeId 제외할 배송사 ID + * @param string $code 배송사 코드 + * @param int|null $excludeId 제외할 배송사 ID * @return bool */ public function existsByCode(string $code, ?int $excludeId = null): bool; @@ -65,8 +65,18 @@ interface ShippingCarrierRepositoryInterface /** * 활성 배송사 목록 조회 (Select 옵션용) * - * @param string|null $type 배송사 유형 필터 (domestic, international, null=전체) + * @param string|null $type 배송사 유형 필터 (domestic, international, null=전체) * @return Collection */ public function getActiveCarriers(?string $type = null): Collection; + + /** + * 전체 배송사 ID 목록 조회 + * + * 일괄 동기화(syncCarriers)에서 payload 에 없는 기존 항목을 가려내는 데 사용합니다. + * 모델 전체를 적재하지 않고 기본키만 조회합니다. + * + * @return array<int, int> 배송사 ID 배열 + */ + public function pluckIds(): array; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingTypeRepositoryInterface.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingTypeRepositoryInterface.php index 8f096076..9affa071 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingTypeRepositoryInterface.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/Contracts/ShippingTypeRepositoryInterface.php @@ -84,4 +84,14 @@ interface ShippingTypeRepositoryInterface * @return string|null 배송유형 코드 (없으면 null) */ public function getFirstActiveCodeByCategory(string $category): ?string; + + /** + * 전체 배송유형의 기본키만 조회합니다. + * + * 일괄 동기화(syncShippingTypes)에서 payload 에 없는 기존 항목을 가려내는 데 사용합니다. + * 모델 전체를 적재하지 않고 기본키만 조회합니다. + * + * @return array<int, int> 배송유형 ID 배열 + */ + public function pluckIds(): array; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ExtraFeeTemplateRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ExtraFeeTemplateRepository.php index 177d9d27..74f956b1 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ExtraFeeTemplateRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ExtraFeeTemplateRepository.php @@ -40,6 +40,21 @@ class ExtraFeeTemplateRepository implements ExtraFeeTemplateRepositoryInterface return $this->model->where('zipcode', $zipcode)->first(); } + /** + * 여러 우편번호로 템플릿을 한 번에 조회 + * + * @param array $zipcodes 우편번호 목록 + * @return Collection + */ + public function findByZipcodes(array $zipcodes): Collection + { + if (empty($zipcodes)) { + return $this->model->newCollection(); + } + + return $this->model->whereIn('zipcode', $zipcodes)->get(); + } + /** * {@inheritDoc} */ @@ -145,6 +160,7 @@ class ExtraFeeTemplateRepository implements ExtraFeeTemplateRepositoryInterface */ public function getActiveList(): Collection { + // audit:allow query-unbounded-get reason: 추가배송비 템플릿은 운영자 등록 설정성 테이블 — 행 수가 운영자 행위(도서산간 우편번호 등록)에 묶여 데이터 증가에 비례하지 않는다 return $this->model ->active() ->orderBy('zipcode') @@ -156,6 +172,7 @@ class ExtraFeeTemplateRepository implements ExtraFeeTemplateRepositoryInterface */ public function getAllAsExtraFeeSettings(): array { + // audit:allow query-unbounded-get reason: 추가배송비 템플릿은 운영자 등록 설정성 테이블 — 행 수가 운영자 행위(도서산간 우편번호 등록)에 묶여 데이터 증가에 비례하지 않는다 return $this->model ->active() ->orderBy('zipcode') diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductImageRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductImageRepository.php index 383f0a13..4d081c19 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductImageRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductImageRepository.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Repositories; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Modules\Sirsoft\Ecommerce\Models\ProductImage; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductImageRepositoryInterface; @@ -90,6 +91,21 @@ class ProductImageRepository implements ProductImageRepositoryInterface return $image?->forceDelete() ?? false; } + /** + * {@inheritDoc} + */ + public function findStaleTempImages(Carbon $threshold, int $limit): Collection + { + return $this->model->newQuery() + ->whereNotNull('temp_key') + ->whereNull('product_id') + ->where('created_at', '<', $threshold) + ->orderBy('created_at') + ->orderBy('id') + ->limit($limit) + ->get(['id', 'temp_key', 'disk', 'path', 'created_at']); + } + /** * {@inheritDoc} */ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductInquiryRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductInquiryRepository.php index 37bd4fd0..443a576d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductInquiryRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ProductInquiryRepository.php @@ -48,7 +48,7 @@ class ProductInquiryRepository implements ProductInquiryRepositoryInterface /** * {@inheritDoc} */ - public function paginateByProductId(int $productId, int $perPage = 10): LengthAwarePaginator + public function paginateByProductId(int $productId, int $perPage = 10, ?int $page = null): LengthAwarePaginator { return $this->model->newQuery() ->where('product_id', $productId) @@ -57,7 +57,8 @@ class ProductInquiryRepository implements ProductInquiryRepositoryInterface ->orderBy('id', 'desc') // audit:allow repository-paginate-column-pruning reason: 상품 1건에 종속된 문의 목록 — // where(product_id) 로 이미 좁혀져 OFFSET 이 깊어질 수 없고, 목록이 본문을 그대로 쓴다 - ->paginate($perPage); + // page 명시 하달 — HTTP `page` 파라미터 암묵 해석에 기대지 않는다 (#102 동형 예방) + ->paginate($perPage, ['*'], 'page', $page); } /** @@ -167,7 +168,8 @@ class ProductInquiryRepository implements ProductInquiryRepositoryInterface { $inquiry->update([ 'is_answered' => true, - 'answered_at' => now(), + // 최초 답변 시각 보존 — 답변 수정/재마킹이 시각을 덮어쓰면 안 된다 (선례: ProductReviewService) + 'answered_at' => $inquiry->answered_at ?? now(), ]); return $inquiry->fresh(); @@ -205,6 +207,41 @@ class ProductInquiryRepository implements ProductInquiryRepositoryInterface ->delete(); } + /** + * {@inheritDoc} + */ + public function findByProductIdWithTrashed(int $productId): Collection + { + // audit:allow query-unbounded-get reason: 상품 삭제 정리 경로 — 단일 상품에 종속된 피벗만 조회 + return $this->model->newQuery() + ->withTrashed() + ->where('product_id', $productId) + ->get(); + } + + /** + * {@inheritDoc} + */ + public function forceDeleteByProductId(int $productId): int + { + return $this->model->newQuery() + ->withTrashed() + ->where('product_id', $productId) + ->forceDelete(); + } + + /** + * {@inheritDoc} + */ + public function restoreByInquirable(string $inquirableType, int $inquirableId): int + { + return $this->model->newQuery() + ->onlyTrashed() + ->where('inquirable_type', $inquirableType) + ->where('inquirable_id', $inquirableId) + ->restore(); + } + /** * {@inheritDoc} */ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingCarrierRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingCarrierRepository.php index 86342e99..729ae281 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingCarrierRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingCarrierRepository.php @@ -127,4 +127,12 @@ class ShippingCarrierRepository implements ShippingCarrierRepositoryInterface return $query->get(); } + + /** + * {@inheritDoc} + */ + public function pluckIds(): array + { + return $this->model->newQuery()->pluck('id')->all(); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingTypeRepository.php b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingTypeRepository.php index 97f9283a..1a13b3e5 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingTypeRepository.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Repositories/ShippingTypeRepository.php @@ -149,4 +149,12 @@ class ShippingTypeRepository implements ShippingTypeRepositoryInterface ->orderBy('sort_order') ->value('code'); } + + /** + * {@inheritDoc} + */ + public function pluckIds(): array + { + return $this->model->newQuery()->pluck('id')->all(); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php index 6b32fde6..f6935c54 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/BrandService.php @@ -6,14 +6,18 @@ use App\Extension\HookManager; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Exceptions\BrandOperationException; use Modules\Sirsoft\Ecommerce\Models\Brand; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\BrandRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 브랜드 서비스 */ class BrandService { + use ReappliesPermissionScope; + public function __construct( protected BrandRepositoryInterface $repository ) {} @@ -21,7 +25,7 @@ class BrandService /** * 브랜드 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return Collection */ public function getAllBrands(array $filters = []): Collection @@ -46,7 +50,7 @@ class BrandService /** * 브랜드 상세 조회 * - * @param int $id 브랜드 ID + * @param int $id 브랜드 ID * @return Brand|null */ public function getBrand(int $id): ?Brand @@ -70,7 +74,7 @@ class BrandService /** * 브랜드 생성 * - * @param array $data 브랜드 데이터 + * @param array $data 브랜드 데이터 * @return Brand */ public function createBrand(array $data): Brand @@ -101,18 +105,20 @@ class BrandService /** * 브랜드 수정 * - * @param int $id 브랜드 ID - * @param array $data 수정할 데이터 + * @param int $id 브랜드 ID + * @param array $data 수정할 데이터 * @return Brand */ public function updateBrand(int $id, array $data): Brand { $brand = $this->repository->findById($id); - if (!$brand) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.brand_not_found')); + if (! $brand) { + throw new BrandOperationException('sirsoft-ecommerce::exceptions.brand_not_found'); } + $this->assertWithinScope($brand, 'sirsoft-ecommerce.brands.update'); + // Before 훅 HookManager::doAction('sirsoft-ecommerce.brand.before_update', $id, $data); @@ -141,24 +147,27 @@ class BrandService /** * 브랜드 상태 토글 * - * @param int $id 브랜드 ID + * @param int $id 브랜드 ID * @return Brand + * * @throws \Exception */ public function toggleStatus(int $id): Brand { $brand = $this->repository->findById($id); - if (!$brand) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.brand_not_found')); + if (! $brand) { + throw new BrandOperationException('sirsoft-ecommerce::exceptions.brand_not_found'); } + $this->assertWithinScope($brand, 'sirsoft-ecommerce.brands.update'); + // Before 훅 HookManager::doAction('sirsoft-ecommerce.brand.before_toggle_status', $brand); $brand = DB::transaction(function () use ($brand) { return $this->repository->update($brand->id, [ - 'is_active' => !$brand->is_active, + 'is_active' => ! $brand->is_active, ]); }); @@ -171,26 +180,29 @@ class BrandService /** * 브랜드 삭제 * - * @param int $id 브랜드 ID + * @param int $id 브랜드 ID * @return array 삭제 결과 정보 + * * @throws \Exception */ public function deleteBrand(int $id): array { $brand = $this->repository->findById($id); - if (!$brand) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.brand_not_found')); + if (! $brand) { + throw new BrandOperationException('sirsoft-ecommerce::exceptions.brand_not_found'); } + $this->assertWithinScope($brand, 'sirsoft-ecommerce.brands.delete'); + // 연결된 상품 수 확인 $productsCount = $this->repository->getProductCount($id); // 연결된 상품이 있으면 삭제 차단 if ($productsCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.brand_has_products', [ + throw new BrandOperationException('sirsoft-ecommerce::exceptions.brand_has_products', [ 'count' => $productsCount, - ])); + ]); } // Before 훅 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryImageService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryImageService.php index 6a09cb8a..44893d77 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryImageService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryImageService.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Modules\Sirsoft\Ecommerce\Models\CategoryImage; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\CategoryImageRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Services\Concerns\ResolvesRowStorage; use Symfony\Component\HttpFoundation\StreamedResponse; /** @@ -21,6 +22,8 @@ use Symfony\Component\HttpFoundation\StreamedResponse; */ class CategoryImageService { + use ResolvesRowStorage; + /** * CategoryImageService 생성자 * @@ -174,7 +177,7 @@ class CategoryImageService return null; } - $response = $this->storage->response( + $response = $this->storageForRow($image->disk)->response( 'images', $image->path, $image->original_filename, @@ -188,7 +191,7 @@ class CategoryImageService Log::error('카테고리 이미지 스토리지에 없음', [ 'category_image_id' => $image->id, 'path' => $image->path, - 'disk' => $this->storage->getDisk(), + 'disk' => $image->disk ?: $this->storage->getDisk(), ]); return null; @@ -218,11 +221,12 @@ class CategoryImageService // Before 훅 HookManager::doAction('sirsoft-ecommerce.category-image.before_delete', $image); - // 스토리지에서 파일 삭제 + // 스토리지에서 파일 삭제 — 행 disk 기준 // DB에 저장된 경로: category/{date}/{filename} // storage->exists/delete에 category를 전달하면 자동으로 images/ 추가됨 - if ($this->storage->exists('images', $image->path)) { - $this->storage->delete('images', $image->path); + $rowStorage = $this->storageForRow($image->disk); + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); } // DB에서 삭제 @@ -244,6 +248,49 @@ class CategoryImageService return $result; } + /** + * 카테고리에 속한 이미지를 파일까지 함께 삭제합니다. + * + * 카테고리 삭제 흐름에서 호출합니다. 단건 삭제(delete)와 달리 남은 이미지 재정렬이 + * 필요 없으므로(카테고리 자체가 사라짐) 재정렬 없이 파일 → 행 순으로 정리합니다. + * + * 행마다 disk 가 다를 수 있으므로 파일 삭제는 각 행의 disk 를 향해 수행합니다. + * + * @param int $categoryId 카테고리 ID + * @return int 삭제된 이미지 행 수 + */ + public function deleteByCategoryId(int $categoryId): int + { + $images = $this->repository->getByCategoryId($categoryId); + + if ($images->isEmpty()) { + return 0; + } + + foreach ($images as $image) { + HookManager::doAction('sirsoft-ecommerce.category-image.before_delete', $image); + + $rowStorage = $this->storageForRow($image->disk); + + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); + } + } + + $deleted = $this->repository->deleteByCategoryId($categoryId); + + Log::info('카테고리 이미지 일괄 삭제 완료', [ + 'category_id' => $categoryId, + 'deleted' => $deleted, + ]); + + foreach ($images as $image) { + HookManager::doAction('sirsoft-ecommerce.category-image.after_delete', $image); + } + + return $deleted; + } + /** * 순서 변경 * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryService.php index 3fa89184..c413ebba 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/CategoryService.php @@ -18,7 +18,8 @@ class CategoryService { public function __construct( protected CategoryRepositoryInterface $repository, - protected CategoryImageRepositoryInterface $imageRepository + protected CategoryImageRepositoryInterface $imageRepository, + protected CategoryImageService $categoryImageService ) {} /** @@ -274,7 +275,9 @@ class CategoryService DB::transaction(function () use ($category) { // 카테고리 이미지 삭제 (명시적 삭제 - CASCADE 의존 금지) - $category->images()->delete(); + // 물리 파일 → 행 순으로 정리한다. 행만 지우면 파일이 디스크에 영구 잔존한다 + // (상품 삭제와 동일한 순서 — ProductService::delete 선례). + $this->categoryImageService->deleteByCategoryId($category->id); // 카테고리 삭제 $this->repository->delete($category->id); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/Concerns/ResolvesRowStorage.php b/modules/_bundled/sirsoft-ecommerce/src/Services/Concerns/ResolvesRowStorage.php new file mode 100644 index 00000000..c9bdfdcc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/Concerns/ResolvesRowStorage.php @@ -0,0 +1,39 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Services\Concerns; + +use App\Contracts\Extension\StorageInterface; + +/** + * 이미지 행 disk 기준 스토리지 해석 trait (서비스 공용) + * + * 디스크 전환 이후에도 전환 이전 행(구 disk)의 서빙/삭제/이동이 그 행의 + * 실제 저장 위치를 향하도록, 행에 기록된 disk 의 스토리지를 돌려줍니다. + * 사용 서비스는 `protected StorageInterface $storage` 주입을 전제로 합니다. + */ +trait ResolvesRowStorage +{ + /** + * 이미지 행에 기록된 disk 기준 스토리지를 반환합니다. + * + * @param string|null $disk 이미지 행의 disk 컬럼 값 (비어 있으면 주입 스토리지) + * @return StorageInterface 행 disk 의 스토리지 + */ + protected function storageForRow(?string $disk): StorageInterface + { + if ($disk === null || $disk === '' || $disk === $this->storage->getDisk()) { + return $this->storage; + } + + // 고아 disk 방어 — 디스크를 제공하던 플러그인이 비활성화되면 그 disk 가 + // config 에서 사라지는데, 미등록 disk 로 withDisk 를 만들면 이후 exists/response/ + // delete 가 InvalidArgumentException 을 던져 서빙·삭제가 500 이 된다. + // AbstractModule::resolvePublicAssetDisk() 와 동일한 존재 검증으로 주입 스토리지에 + // 폴백한다 (파일은 도달 불가이므로 404 가 정상 degradation). + if (config("filesystems.disks.{$disk}") === null) { + return $this->storage; + } + + return $this->storage->withDisk($disk); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php index 4b06c189..fd17a848 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/CouponService.php @@ -7,16 +7,20 @@ use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Ecommerce\Enums\CouponIssueRecordStatus; +use Modules\Sirsoft\Ecommerce\Exceptions\CouponOperationException; use Modules\Sirsoft\Ecommerce\Models\Coupon; use Modules\Sirsoft\Ecommerce\Models\CouponIssue; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\CouponIssueRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\CouponRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 쿠폰 서비스 */ class CouponService { + use ReappliesPermissionScope; + public function __construct( protected CouponRepositoryInterface $repository, protected UserCouponService $userCouponService, @@ -142,9 +146,11 @@ class CouponService $coupon = $this->repository->findById($id); if (! $coupon) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_not_found')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_not_found'); } + $this->assertWithinScope($coupon, 'sirsoft-ecommerce.promotion-coupon.update'); + // Before 훅 HookManager::doAction('sirsoft-ecommerce.coupon.before_update', $id, $data); @@ -199,9 +205,11 @@ class CouponService $coupon = $this->repository->findById($id); if (! $coupon) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_not_found')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_not_found'); } + $this->assertWithinScope($coupon, 'sirsoft-ecommerce.promotion-coupon.delete'); + // Before 훅 HookManager::doAction('sirsoft-ecommerce.coupon.before_delete', $coupon); @@ -231,11 +239,16 @@ class CouponService */ public function bulkUpdateIssueStatus(array $ids, string $issueStatus): int { + // 스코프 검사와 스냅샷이 같은 조회를 공유한다 (Model 직접 호출 제거 겸용). + $targets = $this->repository->findByIdsKeyed($ids); + + $this->assertAllWithinScope($targets, 'sirsoft-ecommerce.promotion-coupon.update'); + // Before 훅 HookManager::doAction('sirsoft-ecommerce.coupon.before_bulk_status', $ids, $issueStatus); // 수정 전 스냅샷 캡처 - $snapshots = Coupon::whereIn('id', $ids)->get()->keyBy('id')->map->toArray()->all(); + $snapshots = $targets->keyBy('id')->map->toArray()->all(); $count = DB::transaction(function () use ($ids, $issueStatus) { return $this->repository->bulkUpdateIssueStatus($ids, $issueStatus); @@ -261,13 +274,21 @@ class CouponService */ public function issueDirectly(int $couponId, array $userIds): array { + $coupon = $this->repository->findById($couponId); + + if (! $coupon) { + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_not_found'); + } + + $this->assertWithinScope($coupon, 'sirsoft-ecommerce.promotion-coupon.update'); + HookManager::doAction('sirsoft-ecommerce.coupon.before_direct_issue', $couponId, $userIds); $result = DB::transaction(function () use ($couponId, $userIds) { $coupon = $this->repository->findByIdForUpdate($couponId); if (! $coupon) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_not_found')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_not_found'); } // 쿠폰 자체 발급 가능 여부는 일괄 검증(불가 시 전원 차단) @@ -316,12 +337,12 @@ class CouponService $issue = $this->issueRepository->findById($issueId); if (! $issue || $issue->coupon_id !== $couponId) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_issue_not_found')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_issue_not_found'); } // 미사용(available) 건만 취소 가능 — 사용/만료/취소된 건은 차단 if ($issue->status !== CouponIssueRecordStatus::AVAILABLE) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_issue_not_cancellable')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_issue_not_cancellable'); } HookManager::doAction('sirsoft-ecommerce.coupon.before_issue_cancel', $issue); @@ -355,7 +376,7 @@ class CouponService $coupon = $this->repository->findById($couponId); if (! $coupon) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.coupon_not_found')); + throw new CouponOperationException('sirsoft-ecommerce::exceptions.coupon_not_found'); } // Before 훅 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/CurrencyConversionService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/CurrencyConversionService.php index 9e34c501..255f60ef 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/CurrencyConversionService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/CurrencyConversionService.php @@ -82,11 +82,23 @@ class CurrencyConversionService /** * 통화의 소수 자릿수를 반환합니다. * + * 주문 시점 스냅샷이 주어지고 그 통화의 자릿수가 박제돼 있으면 그것을 우선합니다. + * * @param string $code 통화 코드 + * @param array|null $currencySnapshot 주문 시점 통화 스냅샷 (없으면 현재 설정 사용) * @return int 소수 자릿수 (기본값: 2) */ - public function getDecimalPlaces(string $code): int + public function getDecimalPlaces(string $code, ?array $currencySnapshot = null): int { + // 주문 시점 스냅샷이 자릿수를 박제해 두었으면 그것이 SSoT (공개 #91 후속). + // 관리자가 그 통화를 삭제하면 현재 설정에서 사라져 폴백 2자리가 적용되는데, + // 그러면 0자리 통화의 과거 주문 표기가 `¥18,860` → `¥18,860.00` 으로 바뀌고 + // 3자리 이상 통화는 표시 금액이 절사된다. + $snapshotPlaces = $this->snapshotDecimalPlaces($currencySnapshot, $code); + if ($snapshotPlaces !== null) { + return $snapshotPlaces; + } + $currencies = $this->getCurrencySettings(); foreach ($currencies as $currency) { @@ -99,6 +111,27 @@ class CurrencyConversionService return 2; } + /** + * 통화 스냅샷에 박제된 소수 자릿수를 반환합니다. (공개 #91 후속) + * + * 자릿수를 박제하지 않은 구형 스냅샷(환율이 단순 숫자)은 null 을 돌려주어 + * 호출부가 현재 설정 폴백을 그대로 타게 합니다. + * + * @param array|null $currencySnapshot 주문 시점 통화 스냅샷 + * @param string $code 통화 코드 + * @return int|null 박제된 자릿수 (없으면 null) + */ + private function snapshotDecimalPlaces(?array $currencySnapshot, string $code): ?int + { + $rateData = $currencySnapshot['exchange_rates'][$code] ?? null; + + if (! is_array($rateData) || ! isset($rateData['decimal_places'])) { + return null; + } + + return (int) $rateData['decimal_places']; + } + /** * 통화의 base_unit(기본 통화일 때 환율 분모가 되는 1단위 금액)을 반환합니다. * @@ -342,12 +375,12 @@ class CurrencyConversionService $snapshotRate = (float) $rateData; $roundingUnit = '0.01'; $roundingMethod = 'round'; - $decimalPlaces = $this->getDecimalPlaces($orderCurrency); + $decimalPlaces = $this->getDecimalPlaces($orderCurrency, $currencySnapshot); } else { $snapshotRate = (float) ($rateData['rate'] ?? 0); $roundingUnit = $rateData['rounding_unit'] ?? '0.01'; $roundingMethod = $rateData['rounding_method'] ?? 'round'; - $decimalPlaces = (int) ($rateData['decimal_places'] ?? $this->getDecimalPlaces($orderCurrency)); + $decimalPlaces = (int) ($rateData['decimal_places'] ?? $this->getDecimalPlaces($orderCurrency, $currencySnapshot)); } $isBase = ($orderCurrency === $baseCurrency); @@ -355,7 +388,7 @@ class CurrencyConversionService if ($isBase) { // base 통화 결제: 환산 없이 그대로. base 의 decimal_places 로 정수화. $convertedAmount = (float) $baseAmount; - $decimalPlaces = (int) ($rateData['decimal_places'] ?? $this->getDecimalPlaces($orderCurrency)); + $decimalPlaces = (int) ($rateData['decimal_places'] ?? $this->getDecimalPlaces($orderCurrency, $currencySnapshot)); $snapshotRate = 1.0; } else { if ($snapshotRate <= 0) { @@ -433,11 +466,15 @@ class CurrencyConversionService /** * 통화별 가격을 포맷팅합니다. * + * 주문 시점 스냅샷을 넘기면 소수 자릿수를 그 시점 값으로 고정합니다 — 통화가 삭제되어도 + * 과거 주문의 표기가 바뀌지 않습니다. 상품 등 카탈로그 표시는 스냅샷 없이 호출합니다. + * * @param float|int $price 가격 * @param string $code 통화 코드 + * @param array|null $currencySnapshot 주문 시점 통화 스냅샷 (없으면 현재 설정 사용) * @return string 포맷팅된 가격 */ - public function formatPrice(float|int $price, string $code): string + public function formatPrice(float|int $price, string $code, ?array $currencySnapshot = null): string { $prefix = __('sirsoft-ecommerce::messages.currency.prefix.'.$code, [], app()->getLocale()); $suffix = __('sirsoft-ecommerce::messages.currency.suffix.'.$code, [], app()->getLocale()); @@ -450,7 +487,7 @@ class CurrencyConversionService $suffix = ''; } - $decimalPlaces = $this->getDecimalPlaces($code); + $decimalPlaces = $this->getDecimalPlaces($code, $currencySnapshot); $formattedNumber = number_format($price, $decimalPlaces); // prefix나 suffix가 없으면 기본 포맷 @@ -507,7 +544,7 @@ class CurrencyConversionService foreach ($amounts as $field => $baseAmount) { if ($isDefault) { $currencyAmounts[$field] = $baseAmount; - $currencyAmounts[$field.'_formatted'] = $this->formatPrice($baseAmount, $code); + $currencyAmounts[$field.'_formatted'] = $this->formatPrice($baseAmount, $code, $currencySnapshot); } else { if ($snapshotRate > 0) { $convertedPrice = ($baseAmount / $baseUnit) * $snapshotRate; @@ -517,7 +554,7 @@ class CurrencyConversionService $roundingMethod ); $currencyAmounts[$field] = $convertedAmount; - $currencyAmounts[$field.'_formatted'] = $this->formatPrice($convertedAmount, $code); + $currencyAmounts[$field.'_formatted'] = $this->formatPrice($convertedAmount, $code, $currencySnapshot); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/EcommerceSettingsService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/EcommerceSettingsService.php index 246fd7d5..03990113 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/EcommerceSettingsService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/EcommerceSettingsService.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\File; use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\RefundMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\ShippingFeeTaxPolicy; +use Modules\Sirsoft\Ecommerce\Support\CurrencySettingsCache; /** * 이커머스 모듈 환경설정 서비스 @@ -28,6 +29,11 @@ class EcommerceSettingsService implements ModuleSettingsInterface */ private const MODULE_IDENTIFIER = 'sirsoft-ecommerce'; + /** + * 관리자가 삭제한 기본 제공 통화 코드 목록의 저장 키 (공개 #91) + */ + private const REMOVED_CURRENCIES_KEY = 'removed_default_currencies'; + /** * 설정 기본값 (캐시) */ @@ -67,25 +73,41 @@ class EcommerceSettingsService implements ModuleSettingsInterface /** * 설정값 저장 * - * @param string $key 설정 키 + * 벌크 저장(saveSettings)과 동일한 정규화 파이프라인을 경유한다 (공개 #114). + * 예전에는 `Arr::set` 결과를 카테고리 파일에 통째로 덮어써서 분리 입력 필드 병합· + * 기본 통화 동기화·defaults 스키마 정규화·결제수단 메타데이터 스냅샷·삭제 통화 기록· + * 통화 캐시 무효화를 전부 건너뛰었고, 저장 파일이 서로 어긋난 상태로 남았다 + * (예: default_currency 는 USD 인데 통화 목록의 is_default 는 KRW). + * + * 위임 payload 의 기저는 **저장본**(loadCategorySettings)이다. 조회 결과 + * (getAllSettings)를 기저로 삼으면 읽기 시점 보강분(통화 symbol/flag, 결제수단 + * 병합 메타)이 영속화되고, 삭제 통화 기록(공개 #91)이 재계산되며 지워져 관리자가 + * 삭제한 통화가 부활한다. + * + * @param string $key 설정 키 (예: 'basic_info.shop_name', 카테고리 통째 지정도 허용) * @param mixed $value 저장할 값 * @return bool 성공 여부 */ public function setSetting(string $key, mixed $value): bool { - $settings = $this->getAllSettings(); - Arr::set($settings, $key, $value); - - // 카테고리 추출 $parts = explode('.', $key); - $category = $parts[0]; + $category = array_shift($parts); - $result = $this->saveCategorySettings($category, $settings[$category] ?? []); + if ($parts === []) { + // 카테고리 통째 저장 — 배열이 아니면 저장할 카테고리 데이터가 없다 + // (기존 경로도 배열 아닌 값은 저장 대상이 되지 못했다) + if (! is_array($value)) { + return false; + } - // 파일 저장 후 캐시 초기화 (다음 조회 시 재계산) - $this->settings = null; + $categoryData = $value; + } else { + $categoryData = $this->loadCategorySettings($category); + Arr::set($categoryData, implode('.', $parts), $value); + } - return $result; + // saveSettings 는 제공된 카테고리만 저장하므로 다른 카테고리 파일은 건드리지 않는다 + return $this->saveSettings([$category => $categoryData]); } /** @@ -119,10 +141,14 @@ class EcommerceSettingsService implements ModuleSettingsInterface // language_currency.currencies 는 정수키 리스트라 array_merge 가 통째 교체 → // defaults 에 있고 저장본에 없는 통화는 code 기준으로 보충(환율은 저장본 우선 보존). // 관리자가 의도적으로 삭제한 게 아니라 array_merge 부작용으로 소실되던 영속성 공백 수정(U11-A). + // 단, 관리자가 실제로 삭제한 통화는 저장 시점에 기록되므로 보충 대상에서 뺀다(공개 #91). if (isset($defaultValues['language_currency']['currencies'])) { + $removedCodes = $settings['language_currency'][self::REMOVED_CURRENCIES_KEY] ?? []; + $settings['language_currency']['currencies'] = $this->mergeCurrenciesByCode( $defaultValues['language_currency']['currencies'], - $settings['language_currency']['currencies'] ?? [] + $settings['language_currency']['currencies'] ?? [], + is_array($removedCodes) ? $removedCodes : [] ); } @@ -135,7 +161,8 @@ class EcommerceSettingsService implements ModuleSettingsInterface // 결제수단 병합 (기본 + 플러그인 필터 + 사용자 저장 설정) if (isset($settings['order_settings'])) { $settings['order_settings']['payment_methods'] = $this->getMergedPaymentMethods( - $settings['order_settings']['payment_methods'] ?? [] + $settings['order_settings']['payment_methods'] ?? [], + $settings['order_settings']['default_pg_provider'] ?? null ); } @@ -150,7 +177,8 @@ class EcommerceSettingsService implements ModuleSettingsInterface ); } // 셀렉터(_currency_selector.json)가 참조하는 symbol/flag 보강 - // (settings 스키마에 없는 표시 메타 — 저장 시 normalize 가 떨궈 round-trip 오염 없음) + // (flag 는 저장 규칙에 없는 표시 메타 — 폼이 되돌려 보내도 FormRequest 검증에서 + // 탈락해 round-trip 오염이 없다) if (! empty($currency['code'])) { $meta = $this->currencyDisplayMeta($currency['code']); // 관리자가 직접 지정한 기호는 보존, 없을 때만 표준 매핑으로 보충 @@ -175,7 +203,16 @@ class EcommerceSettingsService implements ModuleSettingsInterface } } - $this->settings = $settings; + // 부팅이 끝나기 전 결과는 캐시하지 않는다. + // + // 이 결과에는 훅 카탈로그와 병합된 값이 섞여 있다 — 확장이 등록한 결제수단과 그 수단·PG 의 + // 생사 판정이다. 그런데 코어는 부팅 중(CoreServiceProvider::boot)에 config 미러를 채우려고 + // 설정을 한 번 읽고, 그 시점은 플러그인이 자기 훅을 등록하기 전이라 카탈로그가 비어 있다. + // 서비스가 공유 인스턴스이므로(공개 #116) 그 빈 카탈로그 기준 판정을 캐시하면 요청 내내 + // 남아, 살아 있는 PG 를 지정한 결제수단이 주문서에서 통째로 사라진다. + if (app()->isBooted()) { + $this->settings = $settings; + } return $settings; } @@ -201,6 +238,11 @@ class EcommerceSettingsService implements ModuleSettingsInterface * is_active 는 그대로 남아 있으므로 걸러내지 않으면 체크아웃이 선택 가능한 * 결제수단으로 계속 노출한다(관리자 화면은 _orphaned 를 읽어 이미 차단). * + * 수단은 살아 있는데 그 수단이 지정한 **PG 가 사라진** 경우도 같은 결함이다(A2). + * 이쪽은 카탈로그에 남아 있어 `_orphaned` 로 걸리지 않지만, 주문 시 PG 라우팅이 + * 매칭에 실패해 결제창 없이 주문완료로 넘어간다 — `_orphaned_pg` 로 함께 차단한다. + * 관리자 응답은 두 플래그를 그대로 유지한다(운영자가 확인하고 고쳐야 할 대상). + * * @return array 고아 항목이 제거되고 은행명이 포함된 결제 설정 */ public function getPublicPaymentSettings(): array @@ -208,12 +250,27 @@ class EcommerceSettingsService implements ModuleSettingsInterface $orderSettings = $this->getSettings('order_settings'); if (isset($orderSettings['payment_methods']) && is_array($orderSettings['payment_methods'])) { + // 비연속 키가 JSON 객체로 직렬화되지 않도록 array_values 로 재정렬 $orderSettings['payment_methods'] = array_values(array_filter( $orderSettings['payment_methods'], - fn ($method) => ! ($method['_orphaned'] ?? false) + fn ($method) => ! ($method['_orphaned'] ?? false) && ! ($method['_orphaned_pg'] ?? false) )); } + // 죽은 기본 PG 는 공개 응답에서 미설정으로 정규화한다 (프론트가 그 값을 그대로 쓰지 않도록) + $defaultPg = $orderSettings['default_pg_provider'] ?? null; + if (is_string($defaultPg) && $defaultPg !== '' + && ! in_array($defaultPg, $this->registeredPgProviderIds(), true)) { + $orderSettings['default_pg_provider'] = null; + } + + // 현금영수증 프로바이더도 같은 정규화를 거친다 (A3). + // 체크아웃 신청 폼은 이 카테고리 raw 값을 truthy 로 읽으므로, 여기서 정규화하지 않으면 + // 제공 확장이 사라진 뒤에도 신청 폼이 계속 렌더된다. + if (array_key_exists('cash_receipt_provider', $orderSettings)) { + $orderSettings['cash_receipt_provider'] = $this->getCashReceiptProvider(); + } + if (isset($orderSettings['bank_accounts'], $orderSettings['banks'])) { $banks = collect($orderSettings['banks']); $orderSettings['bank_accounts'] = array_map(function ($account) use ($banks) { @@ -263,6 +320,11 @@ class EcommerceSettingsService implements ModuleSettingsInterface ); } + // language_currency: 관리자가 삭제한 기본 제공 통화를 서버가 도출해 기록 (공개 #91) + if ($category === 'language_currency') { + $processedSettings = $this->applyRemovedDefaultCurrencies($processedSettings); + } + if (! $this->saveCategorySettings($category, $processedSettings)) { $success = false; } @@ -271,14 +333,118 @@ class EcommerceSettingsService implements ModuleSettingsInterface // 캐시 초기화 $this->settings = null; + // 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109) + g7_refresh_module_settings_config('sirsoft-ecommerce'); + + // 통화 목록이 바뀌었을 수 있으므로 요청 단위 통화 캐시도 함께 비운다. + // (리소스 계층이 이 캐시로 다통화 금액을 만들기 때문에, 비우지 않으면 같은 요청 안에서 + // 저장 전 통화 구성으로 금액이 계산된다) + CurrencySettingsCache::clear(); + + $this->flushResolvedCaches(); + return $success; } + /** + * 이미 해석된 싱글톤 서비스들의 요청 단위 캐시를 비웁니다. (공개 #116) + * + * 싱글톤 리졸버들은 자기 캐시를 따로 들고 있어서, 설정 서비스의 캐시만 비우면 같은 요청 + * 안에서 이미 해석된 리졸버가 저장 전 카탈로그를 계속 답한다. 저장 경로가 이 한 지점으로 + * 모여 있으므로(단건 저장도 saveSettings 에 위임) 여기서 함께 무효화한다. + * + * 생성자 상호 주입은 순환이라 lazy 해석하며, `resolved()` 가드로 아직 필요하지 않은 + * 서비스를 저장이 강제로 인스턴스화하지 않게 한다. + */ + private function flushResolvedCaches(): void + { + if (app()->resolved(PaymentMethodResolver::class)) { + app(PaymentMethodResolver::class)->flushCache(); + } + + if (app()->resolved(CurrencyConversionService::class)) { + app(CurrencyConversionService::class)->clearCache(); + } + } + + /** + * 관리자가 삭제한 기본 제공 통화를 저장본에 기록합니다. (공개 #91) + * + * 조회 병합(mergeCurrenciesByCode)은 저장본에 없는 defaults 통화를 무조건 보충하는데, + * 그 보충은 "array_merge 통째 교체로 인한 의도치 않은 소실"(U11-A)을 되돌리기 위한 것이다. + * 관리자의 의도적 삭제와 구분할 장치가 없으면 삭제까지 되돌아가므로, 저장 시점에 + * 삭제 의도를 도출해 남긴다. + * + * - 클라이언트가 보낸 같은 키는 신뢰하지 않고 항상 서버가 재계산한다. + * - `currencies` 미제출 저장(기본 통화만 변경 등)은 기존 통화 목록과 기록을 함께 이월한다. + * 저장이 파일 통째 교체이므로 이월하지 않으면 저장본에서 두 키가 모두 사라지고, 조회 + * 병합이 defaults 를 그대로 들여와 삭제가 전부 부활한다. + * - 기본 통화 코드는 기록 대상에서 제외한다 (기본 통화는 항상 생존해야 한다). + * + * @param array $processed 정규화까지 끝난 language_currency 저장값 + * @return array 삭제 기록이 반영된 저장값 + */ + private function applyRemovedDefaultCurrencies(array $processed): array + { + // 클라이언트 주입 방어 — 검증에서 떨어지지만 프로그램 직접 호출 경로도 막는다 + unset($processed[self::REMOVED_CURRENCIES_KEY]); + + $saved = $this->loadCategorySettings('language_currency'); + $defaultValues = $this->getDefaults()['defaults']['language_currency'] ?? []; + + // currencies 미제출 → 기존 통화 목록과 삭제 기록을 함께 이월 + if (! isset($processed['currencies']) || ! is_array($processed['currencies'])) { + $carried = $saved[self::REMOVED_CURRENCIES_KEY] ?? []; + $processed[self::REMOVED_CURRENCIES_KEY] = array_values(array_filter( + is_array($carried) ? $carried : [], + 'is_string' + )); + + if (isset($saved['currencies']) && is_array($saved['currencies'])) { + $processed['currencies'] = array_values($saved['currencies']); + // 같은 저장에서 기본 통화가 바뀌었을 수 있으므로 is_default 를 다시 맞춘다 + $processed = $this->syncCurrencyDefaults($processed); + } + + return $processed; + } + + // 제출된 통화 코드 집합 + $submitted = []; + foreach ($processed['currencies'] as $currency) { + $code = is_array($currency) ? ($currency['code'] ?? null) : null; + if (is_string($code) && $code !== '') { + $submitted[$code] = true; + } + } + + // 유효 기본 통화: 제출값 → 기존 저장본 → defaults 순 + $baseCode = $processed['default_currency'] + ?? ($saved['default_currency'] ?? ($defaultValues['default_currency'] ?? null)); + + $removed = []; + foreach ($defaultValues['currencies'] ?? [] as $defaultCurrency) { + $code = $defaultCurrency['code'] ?? null; + if (! is_string($code) || $code === '' || isset($submitted[$code]) || $code === $baseCode) { + continue; + } + $removed[] = $code; + } + + // 비연속 키가 JSON 객체로 직렬화되지 않도록 재정렬 + $processed[self::REMOVED_CURRENCIES_KEY] = array_values($removed); + + return $processed; + } + /** * 은행 목록만 저장합니다. * * 기존 order_settings의 다른 설정은 유지하고 banks만 교체합니다. * + * 저장은 벌크 저장(saveSettings)에 위임해 정규화 파이프라인을 함께 경유한다 (공개 #114) — + * 은행명 다국어 정규화와 결제수단 메타데이터 스냅샷이 이 경로에서도 적용된다. + * * @param array $banks 은행 목록 배열 * @return bool 성공 여부 */ @@ -287,12 +453,7 @@ class EcommerceSettingsService implements ModuleSettingsInterface $currentSettings = $this->loadCategorySettings('order_settings'); $currentSettings['banks'] = $banks; - $result = $this->saveCategorySettings('order_settings', $currentSettings); - - // 캐시 초기화 - $this->settings = null; - - return $result; + return $this->saveSettings(['order_settings' => $currentSettings]); } /** @@ -725,13 +886,27 @@ class EcommerceSettingsService implements ModuleSettingsInterface /** * 현재 선택된 현금영수증 발급 프로바이더 ID를 반환합니다. * - * @return string|null 프로바이더 ID (미설정 시 null) + * 저장값이 남아 있어도 그 프로바이더를 제공하는 확장이 없으면 **미설정으로 해석**한다(A3). + * 플러그인을 제거해도 설정 문자열은 그대로 남는데, 그 값을 신뢰하면 체크아웃의 신청 폼과 + * 마이페이지 발급 버튼이 계속 렌더되고, 신청하면 구독자 없는 훅을 호출해 발급 실패로만 + * 조용히 기록된다. + * + * @return string|null 프로바이더 ID (미설정이거나 제공 확장 부재 시 null) */ public function getCashReceiptProvider(): ?string { $provider = $this->getSetting('order_settings.cash_receipt_provider'); - return is_string($provider) && $provider !== '' ? $provider : null; + if (! is_string($provider) || $provider === '') { + return null; + } + + $registeredIds = array_map( + fn ($entry) => $entry['id'] ?? null, + $this->getRegisteredCashReceiptProviders() + ); + + return in_array($provider, $registeredIds, true) ? $provider : null; } /** @@ -846,13 +1021,65 @@ class EcommerceSettingsService implements ModuleSettingsInterface * 기본/플러그인 정의와 사용자 저장 설정을 병합합니다. * * @param array $savedMethods 사용자 저장 결제수단 배열 + * @param string|null $defaultPgProvider 기본 PG 제공자 (죽은 PG 판정용, 미전달 시 판정 생략) * @return array 병합된 결제수단 배열 */ - public function getMergedPaymentMethods(array $savedMethods = []): array + public function getMergedPaymentMethods(array $savedMethods = [], ?string $defaultPgProvider = null): array { $available = $this->getAvailablePaymentMethods(); - return $this->mergePaymentMethodSettings($available, $savedMethods); + return $this->mergePaymentMethodSettings($available, $savedMethods, $defaultPgProvider); + } + + /** + * 결제수단에 지정된 PG 가 현재 레지스트리에 없는지 판정합니다. (A2) + * + * 고아 판정(`_orphaned`)은 결제수단 ID 만 본다. builtin 수단에 특정 PG 를 지정한 뒤 그 + * PG 플러그인을 제거하면 수단 자체는 카탈로그에 남아 있어 그 필터를 통과하고, 체크아웃에 + * 선택 가능한 수단으로 노출된다. 주문하면 PG 라우팅이 매칭에 실패해 결제창 없이 + * 주문완료로 넘어간다. + * + * 유효 PG 의 폴백 규칙은 런타임(`OrderProcessingService::determinePgProvider()`)과 + * 동일하게 맞춘다 — 수단의 지정값이 **null 일 때만** 기본 PG 로 내려간다. 지정값이 죽은 + * 문자열이면 기본 PG 가 살아 있어도 폴백하지 않으므로, 그 경우도 차단 대상이다. + * + * @param bool $needsPg PG 결제창이 필요한 수단인지 + * @param string|null $ownProvider 수단에 지정된 PG + * @param string|null $defaultPgProvider 기본 PG + * @param array<int, string> $registeredIds 현재 등록된 PG provider ID 목록 + * @return bool 죽은 PG 지정 여부 + */ + private function hasOrphanedPgProvider( + bool $needsPg, + ?string $ownProvider, + ?string $defaultPgProvider, + array $registeredIds + ): bool { + if (! $needsPg) { + return false; + } + + $effective = $ownProvider ?? $defaultPgProvider; + + // 양쪽 미설정은 기존 계약('none' → non-PG 강하) — 고아가 아니다 + if (! is_string($effective) || $effective === '') { + return false; + } + + return ! in_array($effective, $registeredIds, true); + } + + /** + * 현재 등록된 PG provider ID 목록을 반환합니다. + * + * @return array<int, string> provider ID 목록 + */ + private function registeredPgProviderIds(): array + { + return array_values(array_filter( + array_map(fn ($provider) => $provider['id'] ?? null, $this->getRegisteredPgProviders()), + fn ($id) => is_string($id) && $id !== '' + )); } /** @@ -860,13 +1087,17 @@ class EcommerceSettingsService implements ModuleSettingsInterface * * @param array $available 사용 가능한 결제수단 정의 배열 * @param array $saved 사용자 저장 설정 배열 + * @param string|null $defaultPgProvider 기본 PG 제공자 (죽은 PG 판정용, null 이면 판정 생략) * @return array 병합된 결제수단 배열 */ - private function mergePaymentMethodSettings(array $available, array $saved): array + private function mergePaymentMethodSettings(array $available, array $saved, ?string $defaultPgProvider = null): array { $availableById = collect($available)->keyBy('id'); $savedById = collect($saved)->keyBy('id'); + // 레지스트리는 루프 밖에서 1회만 조회한다 (수단 수만큼 훅이 돌지 않도록) + $registeredPgIds = $this->registeredPgProviderIds(); + $merged = []; // 1. 사용 가능한 결제수단: 저장된 설정과 병합 @@ -937,6 +1168,12 @@ class EcommerceSettingsService implements ModuleSettingsInterface $entry['core_payment_method'] = $definition['defaults']['core_payment_method']; } + // 지정된 PG 가 현재 레지스트리에 없으면 런타임 전용 플래그를 단다 (A2). + // 관리자 화면은 이 플래그로 배지를 띄우고, 공개 응답은 이 항목을 제거한다. + if ($this->hasOrphanedPgProvider($needsPg, $entry['pg_provider'] ?? null, $defaultPgProvider, $registeredPgIds)) { + $entry['_orphaned_pg'] = true; + } + $merged[] = $entry; } @@ -994,8 +1231,8 @@ class EcommerceSettingsService implements ModuleSettingsInterface } // 고아 항목은 기존 _cached_* 유지 - // _orphaned 플래그는 저장하지 않음 (런타임 전용) - unset($savedMethods[$index]['_orphaned']); + // _orphaned / _orphaned_pg 플래그는 저장하지 않음 (런타임 전용) + unset($savedMethods[$index]['_orphaned'], $savedMethods[$index]['_orphaned_pg']); } return $savedMethods; @@ -1068,6 +1305,9 @@ class EcommerceSettingsService implements ModuleSettingsInterface { $this->defaults = null; $this->settings = null; + + // 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109) + g7_refresh_module_settings_config('sirsoft-ecommerce'); } /** @@ -1105,12 +1345,17 @@ class EcommerceSettingsService implements ModuleSettingsInterface * - 저장본에 있는 통화: 저장본(환율 포함) 채택 (관리자 편집 보존) * - 저장본에 없는 defaults 통화: defaults 항목 보충 (소실 방지) * - 저장본에만 있는(관리자 신규 추가) 통화: 그대로 보존 + * - 관리자가 삭제한 defaults 통화($removedCodes): 보충하지 않음 (공개 #91) + * + * 삭제 기록에 있어도 저장본에 그 통화가 다시 들어 있으면 저장본을 우선해 되살린다 — + * 재추가 저장의 결과가 즉시 반영되어야 하기 때문이다(기록은 다음 저장에서 재계산된다). * * @param array $defaults defaults.json 의 통화 목록 * @param array $saved 저장본 통화 목록 + * @param array $removedCodes 관리자가 삭제한 기본 제공 통화 코드 목록 * @return array code 기준으로 병합된 통화 목록 */ - private function mergeCurrenciesByCode(array $defaults, array $saved): array + private function mergeCurrenciesByCode(array $defaults, array $saved, array $removedCodes = []): array { // defaults 를 code 인덱스로 매핑 (필드 보충용) $defaultsByCode = []; @@ -1139,6 +1384,14 @@ class EcommerceSettingsService implements ModuleSettingsInterface if ($code === null) { continue; } + + // 관리자가 삭제한 통화는 보충하지 않는다 (저장본에 있으면 재추가된 것이므로 채택) + if (! isset($savedByCode[$code]) && in_array($code, $removedCodes, true)) { + $usedCodes[$code] = true; + + continue; + } + $merged[] = $this->backfillBaseUnit($savedByCode[$code] ?? $defaultCurrency, $defaultsByCode[$code] ?? []); $usedCodes[$code] = true; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ExtraFeeTemplateService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ExtraFeeTemplateService.php index ca08dac6..53f79a56 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ExtraFeeTemplateService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ExtraFeeTemplateService.php @@ -8,12 +8,15 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; use Modules\Sirsoft\Ecommerce\Models\ExtraFeeTemplate; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ExtraFeeTemplateRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 추가배송비 템플릿 서비스 */ class ExtraFeeTemplateService { + use ReappliesPermissionScope; + public function __construct( protected ExtraFeeTemplateRepositoryInterface $repository ) {} @@ -21,7 +24,7 @@ class ExtraFeeTemplateService /** * 템플릿 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return LengthAwarePaginator */ public function getList(array $filters): LengthAwarePaginator @@ -47,7 +50,7 @@ class ExtraFeeTemplateService /** * 템플릿 상세 조회 * - * @param int $id 템플릿 ID + * @param int $id 템플릿 ID * @return ExtraFeeTemplate|null */ public function getDetail(int $id): ?ExtraFeeTemplate @@ -64,7 +67,7 @@ class ExtraFeeTemplateService /** * 우편번호로 템플릿 조회 * - * @param string $zipcode 우편번호 + * @param string $zipcode 우편번호 * @return ExtraFeeTemplate|null */ public function findByZipcode(string $zipcode): ?ExtraFeeTemplate @@ -75,7 +78,7 @@ class ExtraFeeTemplateService /** * 템플릿 생성 * - * @param array $data 템플릿 데이터 + * @param array $data 템플릿 데이터 * @return ExtraFeeTemplate */ public function create(array $data): ExtraFeeTemplate @@ -101,12 +104,14 @@ class ExtraFeeTemplateService /** * 템플릿 수정 * - * @param ExtraFeeTemplate $template 템플릿 모델 - * @param array $data 수정 데이터 + * @param ExtraFeeTemplate $template 템플릿 모델 + * @param array $data 수정 데이터 * @return ExtraFeeTemplate */ public function update(ExtraFeeTemplate $template, array $data): ExtraFeeTemplate { + $this->assertWithinScope($template, 'sirsoft-ecommerce.shipping-policies.update'); + // 수정 전 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.before_update', $template, $data); @@ -130,11 +135,13 @@ class ExtraFeeTemplateService /** * 템플릿 삭제 * - * @param ExtraFeeTemplate $template 템플릿 모델 + * @param ExtraFeeTemplate $template 템플릿 모델 * @return bool */ public function delete(ExtraFeeTemplate $template): bool { + $this->assertWithinScope($template, 'sirsoft-ecommerce.shipping-policies.delete'); + // 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.before_delete', $template); @@ -149,11 +156,13 @@ class ExtraFeeTemplateService /** * 템플릿 사용여부 토글 * - * @param ExtraFeeTemplate $template 템플릿 모델 + * @param ExtraFeeTemplate $template 템플릿 모델 * @return ExtraFeeTemplate */ public function toggleActive(ExtraFeeTemplate $template): ExtraFeeTemplate { + $this->assertWithinScope($template, 'sirsoft-ecommerce.shipping-policies.update'); + // 토글 전 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.before_toggle_active', $template); @@ -168,13 +177,18 @@ class ExtraFeeTemplateService /** * 템플릿 일괄 삭제 * - * @param array $ids 템플릿 ID 배열 + * @param array $ids 템플릿 ID 배열 * @return int 삭제된 개수 */ public function bulkDelete(array $ids): int { + // 스코프 검사와 스냅샷이 같은 조회를 공유한다 (Model 직접 호출 제거 겸용). + $targets = $this->repository->findByIdsKeyed($ids); + + $this->assertAllWithinScope($targets, 'sirsoft-ecommerce.shipping-policies.delete'); + // 삭제 전 스냅샷 캡처 (after_bulk_delete 훅에 전달) - $snapshots = ExtraFeeTemplate::whereIn('id', $ids)->get()->keyBy('id')->map->toArray()->all(); + $snapshots = $targets->keyBy('id')->map->toArray()->all(); // 일괄 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.before_bulk_delete', $ids); @@ -190,14 +204,19 @@ class ExtraFeeTemplateService /** * 템플릿 일괄 사용여부 변경 * - * @param array $ids 템플릿 ID 배열 - * @param bool $isActive 사용여부 + * @param array $ids 템플릿 ID 배열 + * @param bool $isActive 사용여부 * @return int 변경된 개수 */ public function bulkToggleActive(array $ids, bool $isActive): int { + // 스코프 검사와 스냅샷이 같은 조회를 공유한다 (Model 직접 호출 제거 겸용). + $targets = $this->repository->findByIdsKeyed($ids); + + $this->assertAllWithinScope($targets, 'sirsoft-ecommerce.shipping-policies.update'); + // 변경 전 스냅샷 캡처 (after_bulk_toggle_active 훅에 전달) - $snapshots = ExtraFeeTemplate::whereIn('id', $ids)->get()->keyBy('id')->map->toArray()->all(); + $snapshots = $targets->keyBy('id')->map->toArray()->all(); // 일괄 변경 전 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.before_bulk_toggle_active', $ids, $isActive); @@ -233,7 +252,7 @@ class ExtraFeeTemplateService /** * 일괄 등록 (CSV 또는 엑셀 업로드용) * - * @param array $items 템플릿 데이터 배열 [{zipcode, fee, region?, description?}] + * @param array $items 템플릿 데이터 배열 [{zipcode, fee, region?, description?}] * @return int 등록된 개수 */ public function bulkCreate(array $items): int @@ -248,7 +267,7 @@ class ExtraFeeTemplateService // 생성/업데이트된 레코드 조회 (per-item 로깅용) $zipcodes = array_column($items, 'zipcode'); - $createdTemplates = ExtraFeeTemplate::whereIn('zipcode', $zipcodes)->get(); + $createdTemplates = $this->repository->findByZipcodes($zipcodes); // 일괄 등록 후 훅 HookManager::doAction('sirsoft-ecommerce.extra_fee_template.after_bulk_create', $items, $count, $createdTemplates); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderAdjustmentService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderAdjustmentService.php index 0bebd050..6cba09a1 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderAdjustmentService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderAdjustmentService.php @@ -10,7 +10,6 @@ use Modules\Sirsoft\Ecommerce\DTO\ItemCalculation; use Modules\Sirsoft\Ecommerce\DTO\OrderAdjustment; use Modules\Sirsoft\Ecommerce\DTO\OrderCalculationResult; use Modules\Sirsoft\Ecommerce\DTO\ShippingAddress; -use Modules\Sirsoft\Ecommerce\Enums\CouponIssueRecordStatus; use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\RefundPriorityEnum; @@ -72,19 +71,14 @@ class OrderAdjustmentService return $this->buildFullCancelResult($order, $originalSnapshot, $originalPaidAmount, $originalPointsUsed, $excludedItems, $refundPriority); } - // 4. 쿠폰 used_at 일시 리셋 (재계산 시 검증 통과를 위해) - $couponIssueIds = $recalcInput->couponIssueIds; - $originalCouponStates = $this->temporarilyResetCouponUsage($couponIssueIds); + // 4. 재계산 실행 + // + // buildRecalcInput() 이 항상 snapshot_mode 를 켜므로 재계산은 쿠폰의 사용 상태 + // (status/used_at)를 읽지 않는다. 따라서 재계산을 위해 쿠폰을 일시적으로 미사용으로 + // 되돌릴 필요가 없다 — 조회만 수행한다. + $recalcResult = $this->calculationService->calculate($recalcInput); - // 5. 재계산 실행 - try { - $recalcResult = $this->calculationService->calculate($recalcInput); - } finally { - // 쿠폰 상태 복원 (예외 발생 시에도 반드시 복원) - $this->restoreCouponUsage($originalCouponStates); - } - - // 6. 환불금액 계산 + // 5. 환불금액 계산 $recalculatedPaidAmount = (float) $recalcResult->summary->finalAmount; $recalculatedPointsUsed = (float) $recalcResult->summary->pointsUsed; $additionalCharges = $adjustment->getAdditionalCharges(); @@ -106,7 +100,7 @@ class OrderAdjustmentService $refundPointsAmount = min($totalRefundable - $refundAmount, max(0, $remainingPoints)); } - // 7. 배송비/할인 차이 + // 6. 배송비/할인 차이 $originalShipping = (float) $order->total_shipping_amount; $recalculatedShipping = (float) $recalcResult->summary->totalShipping; $shippingDifference = $originalShipping - $recalculatedShipping; @@ -115,28 +109,28 @@ class OrderAdjustmentService $recalculatedDiscount = (float) $recalcResult->summary->totalDiscount; $discountDifference = $originalDiscount - $recalculatedDiscount; - // 8. 재계산 스냅샷 + // 7. 재계산 스냅샷 $recalculatedSnapshot = $this->captureRecalcSnapshot($recalcResult); $recalculatedSnapshot['total_list_price_amount'] = $this->calculateListPriceTotal($order, $excludedMap); - // 9. 옵션별 업데이트 정보 생성 + // 8. 옵션별 업데이트 정보 생성 $optionUpdates = $this->buildOptionUpdates($order, $excludedMap, $recalcResult); $shippingUpdates = $this->buildShippingUpdates($order, $recalcResult); $orderUpdates = $this->buildOrderUpdates($order, $recalcResult); - // 10. 취소 대상 아이템 정보 + // 9. 취소 대상 아이템 정보 $adjustedItems = $this->buildAdjustedItems($order, $excludedMap); - // 11. 쿠폰 복원 대상 확인 + // 10. 쿠폰 복원 대상 확인 $restoredCouponIssueIds = $this->detectRestoredCoupons($order, $recalcResult); - // 12. 복원 쿠폰 상세 정보 구성 + // 11. 복원 쿠폰 상세 정보 구성 $restoredCoupons = $this->buildRestoredCouponsInfo($restoredCouponIssueIds, $order->currency_snapshot); - // 13. 배송비 정책별 상세 + // 12. 배송비 정책별 상세 $shippingDetails = $this->buildShippingDetails($order, $recalcResult); - // 14. mc_* 다통화 환불금 변환 + // 13. mc_* 다통화 환불금 변환 $currencySnapshot = $order->currency_snapshot; $mcRefundAmount = null; $mcRefundPointsAmount = null; @@ -164,23 +158,23 @@ class OrderAdjustmentService } } - // 15. 환불 후 잔여 잔액 + // 14. 환불 후 잔여 잔액 $remainingPgBalance = max(0, $remainingPg - $refundAmount); $remainingPointsBalance = max(0, $remainingPoints - $refundPointsAmount); - // 16. 다통화 스냅샷 (총 정가금액 + 실결제금액) + // 15. 다통화 스냅샷 (총 정가금액 + 실결제금액) $mcOriginalSnapshot = $this->buildMcOriginalSnapshot($order); $mcRecalculatedSnapshot = $this->buildMcRecalcSnapshot($recalculatedSnapshot, $order->currency_snapshot); - // 17. 쿠폰 상세 (전/후) + // 16. 쿠폰 상세 (전/후) $originalCoupons = $this->extractCouponDetails($order->promotions_applied_snapshot ?? [], $order->currency_snapshot); $recalculatedCoupons = $this->extractCouponDetails($recalcResult->promotions?->toArray() ?? [], $order->currency_snapshot); - // 18. 스냅샷 각 줄을 base 통화로 포맷(취소 모달 primary 표기 = base 통화 기호 고정) + // 17. 스냅샷 각 줄을 base 통화로 포맷(취소 모달 primary 표기 = base 통화 기호 고정) $originalSnapshot = $this->enrichSnapshotWithBaseFormat($originalSnapshot, $order->currency_snapshot); $recalculatedSnapshot = $this->enrichSnapshotWithBaseFormat($recalculatedSnapshot, $order->currency_snapshot); - // 19. 환불 총액/잔액 base 포맷 + 결제 통화 병기 + // 18. 환불 총액/잔액 base 포맷 + 결제 통화 병기 $refundFormatted = $this->buildRefundFormatted([ 'refund_total' => max(0, $refundAmount) + $refundPointsAmount, 'refund_amount' => max(0, $refundAmount), @@ -524,15 +518,15 @@ class OrderAdjustmentService if ($mcOriginalSnapshot) { $zeroFormattedSubtotal = []; foreach ($mcOriginalSnapshot['mc_subtotal_amount'] ?? [] as $code => $amount) { - $zeroFormattedSubtotal[$code] = $this->currencyService->formatPrice(0, $code); + $zeroFormattedSubtotal[$code] = $this->currencyService->formatPrice(0, $code, $currencySnapshot); } $zeroFormattedTotalPaid = []; foreach ($mcOriginalSnapshot['mc_total_paid_amount'] ?? [] as $code => $amount) { - $zeroFormattedTotalPaid[$code] = $this->currencyService->formatPrice(0, $code); + $zeroFormattedTotalPaid[$code] = $this->currencyService->formatPrice(0, $code, $currencySnapshot); } $zeroFormattedListPrice = []; foreach ($mcOriginalSnapshot['mc_total_list_price_amount'] ?? [] as $code => $amount) { - $zeroFormattedListPrice[$code] = $this->currencyService->formatPrice(0, $code); + $zeroFormattedListPrice[$code] = $this->currencyService->formatPrice(0, $code, $currencySnapshot); } $zeroMcSnapshot = [ @@ -866,59 +860,6 @@ class OrderAdjustmentService return $items; } - /** - * 쿠폰 사용 상태를 일시적으로 리셋합니다. - * - * 재계산 시 OrderCalculationService의 validateCoupon()이 - * used_at !== null인 쿠폰을 'alreadyUsed'로 거부하므로, - * 원 주문에서 사용된 쿠폰을 일시적으로 미사용 상태로 변경합니다. - * - * @param int[] $couponIssueIds 쿠폰 발급 ID 배열 - * @return array 원래 상태 [{id, status, used_at}] - */ - private function temporarilyResetCouponUsage(array $couponIssueIds): array - { - if (empty($couponIssueIds)) { - return []; - } - - $originalStates = []; - $couponIssues = $this->couponIssueRepository->findByIds($couponIssueIds); - - foreach ($couponIssues as $couponIssue) { - $originalStates[] = [ - 'id' => $couponIssue->id, - 'status' => $couponIssue->status, - 'used_at' => $couponIssue->used_at, - 'order_id' => $couponIssue->order_id, - ]; - - $this->couponIssueRepository->update($couponIssue->id, [ - 'status' => CouponIssueRecordStatus::AVAILABLE, - 'used_at' => null, - 'order_id' => null, - ]); - } - - return $originalStates; - } - - /** - * 쿠폰 사용 상태를 원래대로 복원합니다. - * - * @param array $originalStates 원래 상태 배열 - */ - private function restoreCouponUsage(array $originalStates): void - { - foreach ($originalStates as $state) { - $this->couponIssueRepository->update($state['id'], [ - 'status' => $state['status'], - 'used_at' => $state['used_at'], - 'order_id' => $state['order_id'], - ]); - } - } - /** * 복원 대상 쿠폰을 감지합니다. * @@ -1014,7 +955,7 @@ class OrderAdjustmentService $restoredCoupons[] = [ 'coupon_name' => $issue->coupon?->getLocalizedName() ?? '', 'discount_amount' => $discountAmount, - 'discount_amount_formatted' => $this->currencyService->formatPrice($discountAmount, $baseCurrency), + 'discount_amount_formatted' => $this->currencyService->formatPrice($discountAmount, $baseCurrency, $currencySnapshot), ]; } @@ -1093,7 +1034,7 @@ class OrderAdjustmentService 'name' => $coupon['name'] ?? '', 'target_type' => $coupon['target_type'] ?? '', 'discount_amount' => $discountAmount, - 'discount_amount_formatted' => $this->currencyService->formatPrice($discountAmount, $baseCurrency), + 'discount_amount_formatted' => $this->currencyService->formatPrice($discountAmount, $baseCurrency, $currencySnapshot), ]; } } @@ -1119,7 +1060,7 @@ class OrderAdjustmentService $formatted = []; foreach ($snapshot as $key => $value) { if (is_numeric($value)) { - $formatted[$key] = $this->currencyService->formatPrice((float) $value, $baseCurrency); + $formatted[$key] = $this->currencyService->formatPrice((float) $value, $baseCurrency, $currencySnapshot); } } @@ -1145,7 +1086,7 @@ class OrderAdjustmentService $base = []; foreach ($amounts as $field => $value) { - $base[$field] = $this->currencyService->formatPrice((float) $value, $baseCurrency); + $base[$field] = $this->currencyService->formatPrice((float) $value, $baseCurrency, $currencySnapshot); } $mc = []; @@ -1184,12 +1125,12 @@ class OrderAdjustmentService $formattedSubtotal = []; foreach ($order->mc_subtotal_amount ?? [] as $code => $amount) { - $formattedSubtotal[$code] = $this->currencyService->formatPrice($amount, $code); + $formattedSubtotal[$code] = $this->currencyService->formatPrice($amount, $code, $order->currency_snapshot); } $formattedTotalPaid = []; foreach ($order->mc_total_paid_amount ?? [] as $code => $amount) { - $formattedTotalPaid[$code] = $this->currencyService->formatPrice($amount, $code); + $formattedTotalPaid[$code] = $this->currencyService->formatPrice($amount, $code, $order->currency_snapshot); } // 정가 합계 다통화 변환 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderCalculationService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderCalculationService.php index 6bfa65b2..7f6a6385 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderCalculationService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderCalculationService.php @@ -1105,6 +1105,11 @@ class OrderCalculationService // 배송정책이 삭제되었거나 조회 불가한 경우 배송비 0으로 처리 if (! $policy) { + Log::warning('배송정책을 조회할 수 없어 배송비를 0원으로 처리했습니다', [ + 'policy_id' => $policyId, + 'country_code' => $countryCode, + ]); + foreach ($groupItems as $item) { $shippingResults[$item['product_option_id']] = new AppliedShippingPolicy( policyId: $policyId, @@ -1124,6 +1129,11 @@ class OrderCalculationService if (! $countrySetting) { // 해당 국가 설정이 없으면 배송비 0으로 처리 + Log::warning('국가별 배송 설정이 없어 배송비를 0원으로 처리했습니다', [ + 'policy_id' => $policyId, + 'country_code' => $countryCode, + ]); + foreach ($groupItems as $item) { $shippingResults[$item['product_option_id']] = new AppliedShippingPolicy( policyId: $policyId, @@ -1142,7 +1152,7 @@ class OrderCalculationService $baseShippingTotal += $shippingFee; // 추가 배송비(도서산간) 계산 - KR 전용 - $extraShippingFee = $this->calculateExtraShippingFee($countrySetting, $countryCode, $zipcode, $group['total_quantity']); + $extraShippingFee = $this->calculateExtraShippingFee($countrySetting, $countryCode, $zipcode, $group); $extraShippingTotal += $extraShippingFee; // 배송비를 그룹 내 아이템들에게 안분 @@ -1170,7 +1180,7 @@ class OrderCalculationService 'custom_shipping_name' => $countrySetting->custom_shipping_name, 'currency_code' => $countrySetting->currency_code, 'charge_policy' => $countrySetting->charge_policy->value, - 'base_fee' => (int) $countrySetting->base_fee, + 'base_fee' => (int) round((float) $countrySetting->base_fee), 'free_threshold' => $countrySetting->free_threshold ? (int) $countrySetting->free_threshold : null, 'ranges' => $countrySetting->ranges, 'extra_fee_enabled' => $countrySetting->extra_fee_enabled, @@ -1198,10 +1208,10 @@ class OrderCalculationService * @param ShippingPolicyCountrySetting $countrySetting 국가별 설정 * @param string $countryCode 수신자 국가코드 * @param string|null $zipcode 우편번호 - * @param int $quantity 그룹 합계 수량 + * @param array $group 그룹 정보 (total_amount, total_quantity, total_weight, total_volume) * @return int 추가 배송비 */ - protected function calculateExtraShippingFee(ShippingPolicyCountrySetting $countrySetting, string $countryCode, ?string $zipcode, int $quantity): int + protected function calculateExtraShippingFee(ShippingPolicyCountrySetting $countrySetting, string $countryCode, ?string $zipcode, array $group): int { // 도서산간 추가배송비는 KR 전용 if ($countryCode !== 'KR') { @@ -1219,15 +1229,10 @@ class OrderCalculationService return 0; } - // per_* 정책에서 extra_fee_multiply가 true면 건수만큼 중복 부과 + // per_* 정책에서 extra_fee_multiply가 true면 배송 단위 수만큼 중복 부과 + // (기본 배송비와 동일한 단위 수를 사용해야 "단위당" 의미가 어긋나지 않는다) if ($countrySetting->extra_fee_multiply && $this->isPerUnitPolicy($countrySetting)) { - $unitValue = $countrySetting->ranges['unit_value'] ?? 1; - if ($unitValue <= 0) { - $unitValue = 1; - } - $units = (int) ceil($quantity / $unitValue); - - return $extraFee * $units; + return $extraFee * $this->resolveChargeUnits($countrySetting, $group); } // 기본: 1회만 부과 @@ -1242,13 +1247,7 @@ class OrderCalculationService */ protected function isPerUnitPolicy(ShippingPolicyCountrySetting $countrySetting): bool { - return in_array($countrySetting->charge_policy, [ - ChargePolicyEnum::PER_QUANTITY, - ChargePolicyEnum::PER_WEIGHT, - ChargePolicyEnum::PER_VOLUME, - ChargePolicyEnum::PER_VOLUME_WEIGHT, - ChargePolicyEnum::PER_AMOUNT, - ]); + return in_array($countrySetting->charge_policy, ChargePolicyEnum::perUnitPolicies(), true); } /** @@ -1291,7 +1290,7 @@ class OrderCalculationService $baseShippingFee = $this->calculateCountryShippingFee($countrySetting, $standaloneGroup); // 추가 배송비 계산 (도서산간 - KR 전용) - $extraShippingFee = $this->calculateExtraShippingFee($countrySetting, $countryCode, $zipcode, $targetItem['quantity']); + $extraShippingFee = $this->calculateExtraShippingFee($countrySetting, $countryCode, $zipcode, $standaloneGroup); return $baseShippingFee + $extraShippingFee; } @@ -2143,70 +2142,182 @@ class OrderCalculationService $groupWeight = $group['total_weight'] ?? 0.0; $groupVolume = $group['total_volume'] ?? 0.0; - // 부피무게 계산 (부피 / 부피무게 계수, 기본값 6000) + // 단위 환산은 이 지점 한 곳에서만 수행한다. + // 상품 옵션은 g / cm³ 로 저장되고, 배송정책 구간·단위값은 kg / L 로 입력받는다. $volumeWeightDivisor = $countrySetting->ranges['volume_weight_divisor'] ?? 6000; - $volumeWeight = $volumeWeightDivisor > 0 ? $groupVolume / $volumeWeightDivisor : 0.0; - $chargeableWeight = max($groupWeight, $volumeWeight); + $weightKg = $groupWeight / 1000; + $volumeLiter = $groupVolume / 1000; + // 부피무게(kg) = 부피(cm³) / 부피무게 계수 (기본 6000) + $volumetricWeightKg = $volumeWeightDivisor > 0 ? $groupVolume / $volumeWeightDivisor : 0.0; + $chargeableWeightKg = max($weightKg, $volumetricWeightKg); return match ($countrySetting->charge_policy) { ChargePolicyEnum::FREE => 0, - ChargePolicyEnum::FIXED => (int) $countrySetting->base_fee, - ChargePolicyEnum::CONDITIONAL_FREE => $groupTotal >= $countrySetting->free_threshold ? 0 : (int) $countrySetting->base_fee, - ChargePolicyEnum::RANGE_AMOUNT => $this->calculateRangeFee($countrySetting->ranges, $groupTotal), - ChargePolicyEnum::RANGE_QUANTITY => $this->calculateRangeFee($countrySetting->ranges, $groupQuantity), - ChargePolicyEnum::RANGE_WEIGHT => $this->calculateRangeFee($countrySetting->ranges, (int) ($groupWeight * 1000)), // kg → g 변환 - ChargePolicyEnum::RANGE_VOLUME => $this->calculateRangeFee($countrySetting->ranges, (int) $groupVolume), - ChargePolicyEnum::RANGE_VOLUME_WEIGHT => $this->calculateRangeFee($countrySetting->ranges, (int) ($chargeableWeight * 1000)), + ChargePolicyEnum::FIXED => (int) round((float) $countrySetting->base_fee), + ChargePolicyEnum::CONDITIONAL_FREE => $groupTotal >= $countrySetting->free_threshold ? 0 : (int) round((float) $countrySetting->base_fee), + ChargePolicyEnum::RANGE_AMOUNT => $this->calculateRangeFee($countrySetting->ranges, $groupTotal, $countrySetting), + ChargePolicyEnum::RANGE_QUANTITY => $this->calculateRangeFee($countrySetting->ranges, $groupQuantity, $countrySetting), + ChargePolicyEnum::RANGE_WEIGHT => $this->calculateRangeFee($countrySetting->ranges, $weightKg, $countrySetting), + ChargePolicyEnum::RANGE_VOLUME => $this->calculateRangeFee($countrySetting->ranges, $volumeLiter, $countrySetting), + ChargePolicyEnum::RANGE_VOLUME_WEIGHT => $this->calculateRangeFee($countrySetting->ranges, $chargeableWeightKg, $countrySetting), ChargePolicyEnum::PER_QUANTITY => $this->calculatePerUnitFee($countrySetting->base_fee, $groupQuantity, $countrySetting->ranges['unit_value'] ?? 1), - ChargePolicyEnum::PER_WEIGHT => $this->calculatePerUnitFee($countrySetting->base_fee, $groupWeight, $countrySetting->ranges['unit_value'] ?? 0.5), - ChargePolicyEnum::PER_VOLUME => $this->calculatePerUnitFee($countrySetting->base_fee, $groupVolume, $countrySetting->ranges['unit_value'] ?? 1000), - ChargePolicyEnum::PER_VOLUME_WEIGHT => $this->calculatePerUnitFee($countrySetting->base_fee, $chargeableWeight, $countrySetting->ranges['unit_value'] ?? 0.5), + ChargePolicyEnum::PER_WEIGHT => $this->calculatePerUnitFee($countrySetting->base_fee, $weightKg, $countrySetting->ranges['unit_value'] ?? 0.5), + ChargePolicyEnum::PER_VOLUME => $this->calculatePerUnitFee($countrySetting->base_fee, $volumeLiter, $countrySetting->ranges['unit_value'] ?? 1), + ChargePolicyEnum::PER_VOLUME_WEIGHT => $this->calculatePerUnitFee($countrySetting->base_fee, $chargeableWeightKg, $countrySetting->ranges['unit_value'] ?? 0.5), ChargePolicyEnum::PER_AMOUNT => $this->calculatePerUnitFee($countrySetting->base_fee, $groupTotal, $countrySetting->ranges['unit_value'] ?? 10000), ChargePolicyEnum::API => $this->calculateApiShippingFee($countrySetting, $group), - default => (int) $countrySetting->base_fee, + default => (int) round((float) $countrySetting->base_fee), }; } /** - * 구간별 배송비를 계산합니다. + * 구간별 배송비를 계산합니다 (상한 포함 사다리 매칭). + * + * 구간을 상한 오름차순(무제한 구간을 마지막)으로 정렬한 뒤, 값이 상한 이하인 첫 구간의 + * 배송비를 반환합니다. 시작값(min)은 화면 표시용이며 매칭에 사용하지 않습니다. 이 방식은 + * 상한 포함형(max+1=다음 min)과 반개형(max=다음 min) 어느 형태로 저장된 데이터에서도 + * 간극 없이 동작합니다. * * @param array|null $ranges 구간 설정 - * @param int $value 비교 값 (금액 또는 수량) + * @param int|float $value 비교 값 (금액·수량·kg·L) + * @param ShippingPolicyCountrySetting|null $countrySetting 로그 맥락용 국가별 설정 * @return int 배송비 */ - protected function calculateRangeFee(?array $ranges, int $value): int + protected function calculateRangeFee(?array $ranges, int|float $value, ?ShippingPolicyCountrySetting $countrySetting = null): int { - if (empty($ranges) || empty($ranges['tiers'])) { + $tiers = is_array($ranges) ? ($ranges['tiers'] ?? []) : []; + + if (! is_array($tiers) || count($tiers) === 0) { + $this->logZeroShippingFallback( + '구간별 배송비 구간(tiers)이 비어 있어 배송비를 0원으로 처리했습니다', + $countrySetting, + ['value' => $value] + ); + return 0; } - foreach ($ranges['tiers'] as $tier) { - $min = $tier['min'] ?? 0; - $max = $tier['max'] ?? PHP_INT_MAX; + // 상한 오름차순 정렬 (무제한 구간은 항상 마지막) + $sortedTiers = array_values($tiers); + usort($sortedTiers, function ($a, $b) { + $aMax = $this->resolveTierMax($a); + $bMax = $this->resolveTierMax($b); - if ($value >= $min && ($max === null || $value < $max)) { - return (int) ($tier['fee'] ?? 0); + if ($aMax === null && $bMax === null) { + return 0; + } + if ($aMax === null) { + return 1; + } + if ($bMax === null) { + return -1; + } + + return $aMax <=> $bMax; + }); + + foreach ($sortedTiers as $tier) { + $max = $this->resolveTierMax($tier); + + if ($max === null || (float) $value <= $max) { + return (int) round((float) ($tier['fee'] ?? 0)); } } + // 마지막 구간의 max 가 null 이면 도달하지 않는 경로 (저장 시점 검증이 1차 방어) + $this->logZeroShippingFallback( + '구간별 배송비에서 매칭되는 구간을 찾지 못해 0원으로 처리했습니다', + $countrySetting, + ['value' => $value] + ); + return 0; } + /** + * 구간의 상한값을 정규화합니다 (무제한이면 null). + * + * @param array $tier 구간 정의 + * @return float|null 상한값 (무제한이면 null) + */ + protected function resolveTierMax(array $tier): ?float + { + $max = $tier['max'] ?? null; + + if ($max === null || $max === '') { + return null; + } + + return (float) $max; + } + + /** + * 배송비 0원 폴백을 경고 로그로 남깁니다. + * + * 설정 오류로 인한 무음 0원(무료배송)은 화면상 정상 주문과 구분되지 않으므로 + * 운영자가 사후에 원인을 추적할 수 있도록 맥락을 남깁니다. + * + * @param string $message 로그 메시지 + * @param ShippingPolicyCountrySetting|null $countrySetting 국가별 설정 + * @param array $context 추가 맥락 + */ + protected function logZeroShippingFallback(string $message, ?ShippingPolicyCountrySetting $countrySetting, array $context = []): void + { + Log::warning($message, array_merge([ + 'policy_id' => $countrySetting?->shipping_policy_id, + 'country_code' => $countrySetting?->country_code, + 'charge_policy' => $countrySetting?->charge_policy?->value, + ], $context)); + } + /** * 단위당 배송비를 계산합니다. * - * @param int|float $baseFee 기본 배송비 - * @param int|float $value 계산 기준 값 (수량, 무게, 부피, 금액 등) + * @param int|float|string $baseFee 기본 배송비 (decimal 캐스트로 문자열이 올 수 있음) + * @param int|float $value 계산 기준 값 (수량, kg, L, 금액 등) * @param int|float $unitValue 단위 값 * @return int 배송비 */ - protected function calculatePerUnitFee(int|float $baseFee, int|float $value, int|float $unitValue): int + protected function calculatePerUnitFee(int|float|string $baseFee, int|float $value, int|float $unitValue): int { if ($unitValue <= 0) { $unitValue = 1; } - return (int) ceil($value / $unitValue) * (int) $baseFee; + return (int) round(ceil($value / $unitValue) * (float) $baseFee); + } + + /** + * per_* 정책에서 부과 단위 수를 계산합니다 (기본 배송비와 도서산간 곱의 공통 기준). + * + * @param ShippingPolicyCountrySetting $countrySetting 국가별 설정 + * @param array $group 그룹 정보 (total_amount, total_quantity, total_weight, total_volume) + * @return int 부과 단위 수 + */ + protected function resolveChargeUnits(ShippingPolicyCountrySetting $countrySetting, array $group): int + { + $groupWeight = (float) ($group['total_weight'] ?? 0.0); + $groupVolume = (float) ($group['total_volume'] ?? 0.0); + $volumeWeightDivisor = $countrySetting->ranges['volume_weight_divisor'] ?? 6000; + + $weightKg = $groupWeight / 1000; + $volumeLiter = $groupVolume / 1000; + $volumetricWeightKg = $volumeWeightDivisor > 0 ? $groupVolume / $volumeWeightDivisor : 0.0; + + [$value, $defaultUnitValue] = match ($countrySetting->charge_policy) { + ChargePolicyEnum::PER_WEIGHT => [$weightKg, 0.5], + ChargePolicyEnum::PER_VOLUME => [$volumeLiter, 1], + ChargePolicyEnum::PER_VOLUME_WEIGHT => [max($weightKg, $volumetricWeightKg), 0.5], + ChargePolicyEnum::PER_AMOUNT => [(float) ($group['total_amount'] ?? 0), 10000], + default => [(float) ($group['total_quantity'] ?? 0), 1], + }; + + $unitValue = $countrySetting->ranges['unit_value'] ?? $defaultUnitValue; + if ($unitValue <= 0) { + $unitValue = 1; + } + + return max(0, (int) ceil($value / $unitValue)); } /** @@ -2222,7 +2333,7 @@ class OrderCalculationService // 엔드포인트 미설정 시 기본 배송비 폴백 if (empty($apiEndpoint)) { - return (int) $countrySetting->base_fee; + return (int) round((float) $countrySetting->base_fee); } try { @@ -2232,12 +2343,12 @@ class OrderCalculationService $response = $this->dispatchApiRequest($apiEndpoint, $requestData, $config); if ($response === null || ! $response->successful()) { - return (int) $countrySetting->base_fee; + return (int) round((float) $countrySetting->base_fee); } $fee = $this->extractFeeFromApiResponse($response, $countrySetting, $config); - return $fee ?? (int) $countrySetting->base_fee; + return $fee ?? (int) round((float) $countrySetting->base_fee); } catch (\Throwable $e) { // 예외 발생 시 기본 배송비 반환 (토큰 등 민감값은 로그에 남기지 않음) Log::warning('API 배송비 계산 실패', [ @@ -2248,7 +2359,7 @@ class OrderCalculationService 'error' => $e->getMessage(), ]); - return (int) $countrySetting->base_fee; + return (int) round((float) $countrySetting->base_fee); } } @@ -2385,7 +2496,7 @@ class OrderCalculationService // 텍스트 응답: 숫자/소수점/부호만 남겨 추출 ("₩3,000" → 3000) $cleaned = preg_replace('/[^0-9.\-]/', '', $response->body()); - return is_numeric($cleaned) ? (int) $cleaned : null; + return is_numeric($cleaned) ? $this->normalizeApiFee((int) $cleaned, $countrySetting) : null; } // JSON 응답: response_path 점표기 중첩 경로 추출 (없으면 기존 api_response_fee_field 폴백) @@ -2395,7 +2506,32 @@ class OrderCalculationService $value = data_get($response->json(), $path); - return is_numeric($value) ? (int) $value : null; + return is_numeric($value) ? $this->normalizeApiFee((int) $value, $countrySetting) : null; + } + + /** + * 외부 API 가 돌려준 배송비를 검증합니다. + * + * 음수 배송비는 주문 총액을 깎아 금전 손실로 이어지므로 추출 실패로 처리해 + * 기본 배송비 폴백 경로에 태웁니다. + * + * @param int $fee 추출된 배송비 + * @param ShippingPolicyCountrySetting $countrySetting 국가별 설정 + * @return int|null 검증을 통과한 배송비 (음수면 null) + */ + protected function normalizeApiFee(int $fee, ShippingPolicyCountrySetting $countrySetting): ?int + { + if ($fee < 0) { + Log::warning('배송비 계산 API 가 음수 배송비를 반환해 기본 배송비로 폴백합니다', [ + 'policy_id' => $countrySetting->shipping_policy_id, + 'country_code' => $countrySetting->country_code, + 'fee' => $fee, + ]); + + return null; + } + + return $fee; } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderProcessingService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderProcessingService.php index 8c068bf1..44633f9c 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderProcessingService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderProcessingService.php @@ -386,6 +386,11 @@ class OrderProcessingService * * 게스트 체크아웃(비로그인)은 유저 컨텍스트 부재 → 헤더/base 로 안전 폴백한다. * + * 해석 결과가 현재 등록된 통화가 아니면 base 로 폴백한다 (공개 #91). 관리자가 통화를 + * 삭제해도 유저의 영속 선호 통화와 클라이언트 헤더에는 그 코드가 남아 있는데, 그대로 + * 채택하면 스냅샷 환율에 코드가 없어 체크아웃이 차단된다. 저장된 선호 값 자체는 지우지 + * 않는다 — 그 통화가 다시 등록되면 자동으로 복원되는 것이 올바른 동작이다. + * * @param string $baseCurrency 기본 통화(폴백) * @return string 결정된 통화 코드 */ @@ -393,12 +398,15 @@ class OrderProcessingService { // 1순위: 로그인 유저의 영속 통화 (§A3 user-profile) $persisted = $this->resolvePersistedUserCurrency(); - if ($persisted !== null && $persisted !== '') { - return $persisted; - } // 2순위: X-Currency 헤더 (비로그인/세션 표시) → 3순위: base 폴백 - return request()->header('X-Currency', $baseCurrency) ?: $baseCurrency; + $resolved = ($persisted !== null && $persisted !== '') + ? $persisted + : (request()->header('X-Currency', $baseCurrency) ?: $baseCurrency); + + return $this->currencyConversionService->isSupportedCurrency($resolved) + ? $resolved + : $baseCurrency; } /** @@ -795,6 +803,20 @@ class OrderProcessingService $createdOptions[$item->productOptionId] = $orderOption; } + // 주문 합계 무게/부피는 옵션 소계의 합이다. + // 주문 생성 시점에는 옵션이 없어 0 으로 두었으므로 여기서 실값으로 채운다 + // (배송사 연동·운임 정산이 이 값을 읽는다). + $order->update([ + 'total_weight' => array_sum(array_map( + fn (OrderOption $option) => (float) ($option->subtotal_weight ?? 0), + $createdOptions + )), + 'total_volume' => array_sum(array_map( + fn (OrderOption $option) => (float) ($option->subtotal_volume ?? 0), + $createdOptions + )), + ]); + return $createdOptions; } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php index c6ae25d7..1297c528 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/OrderService.php @@ -14,17 +14,21 @@ use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Exceptions\InsufficientStockException; use Modules\Sirsoft\Ecommerce\Exceptions\OrderModificationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderOptionNotConfirmableException; use Modules\Sirsoft\Ecommerce\Exceptions\OrderProcessingException; use Modules\Sirsoft\Ecommerce\Models\Order; use Modules\Sirsoft\Ecommerce\Models\OrderOption; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\UserAddressRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 주문 서비스 */ class OrderService { + use ReappliesPermissionScope; + public function __construct( protected OrderRepositoryInterface $repository, protected UserAddressRepositoryInterface $userAddressRepository, @@ -161,6 +165,8 @@ class OrderService */ public function update(Order $order, array $data): Order { + $this->assertWithinScope($order, 'sirsoft-ecommerce.orders.update'); + $oldStatus = $order->order_status?->value; // 수정 전 훅 @@ -350,6 +356,8 @@ class OrderService */ public function delete(Order $order): bool { + $this->assertWithinScope($order, 'sirsoft-ecommerce.orders.delete'); + // 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.order.before_delete', $order); @@ -390,6 +398,8 @@ class OrderService */ public function bulkUpdate(array $data): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($data['ids'] ?? []), 'sirsoft-ecommerce.orders.update'); + $ids = $data['ids'] ?? []; $orderStatus = $data['order_status'] ?? null; $carrierId = $data['carrier_id'] ?? null; @@ -506,6 +516,8 @@ class OrderService */ public function bulkUpdateStatus(array $ids, string $status): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($ids), 'sirsoft-ecommerce.orders.update'); + // 스냅샷 캡처 (ChangeDetector용 + 전이 감지용 이전 order_status) $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -548,6 +560,8 @@ class OrderService */ public function bulkUpdateShipping(array $ids, ?int $carrierId, ?string $trackingNumber): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($ids), 'sirsoft-ecommerce.orders.update'); + // 스냅샷 캡처 (ChangeDetector용) $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -628,6 +642,8 @@ class OrderService */ public function updateShippingAddress(Order $order, array $data): Order { + $this->assertWithinScope($order, 'sirsoft-ecommerce.orders.update'); + $status = $order->order_status; if (! $status->isBeforeShipping()) { @@ -643,6 +659,15 @@ class OrderService throw new OrderModificationException(__('sirsoft-ecommerce::messages.address.not_found')); } + // 저장된 배송지의 국내·해외 필드를 모두 싣는다. 지역 판정은 아래 + // $isDomestic 한 곳에서만 하고 그 분기가 필요한 쪽만 골라 쓴다. + // 국내 6필드만 싣던 종전 형태에서는 해외 배송지를 고르면 해외 컬럼이 + // 전부 null 로 덮여 주소가 통째로 사라졌다. + // 배송 메모는 저장 주소가 아니라 이번 요청의 값이므로 승계한다. + $memo = array_key_exists('delivery_memo', $data) + ? ['delivery_memo' => $data['delivery_memo']] + : []; + $data = [ 'recipient_name' => $savedAddress->recipient_name, 'recipient_phone' => $savedAddress->recipient_phone, @@ -650,7 +675,12 @@ class OrderService 'zipcode' => $savedAddress->zipcode, 'address' => $savedAddress->address, 'address_detail' => $savedAddress->address_detail, - ]; + 'address_line_1' => $savedAddress->address_line_1, + 'address_line_2' => $savedAddress->address_line_2, + 'intl_city' => $savedAddress->city, + 'intl_state' => $savedAddress->state, + 'intl_postal_code' => $savedAddress->postal_code, + ] + $memo; } // 변경 전 훅 @@ -665,24 +695,43 @@ class OrderService $addressSnapshot = $shippingAddress ? $shippingAddress->toArray() : null; if ($shippingAddress) { + // 국가 코드 폴백은 UpdateOrderShippingAddressRequest::isDomesticShipping() + // 과 반드시 같은 기준이어야 한다 — 게이트가 "미전송 = 국내" 로 보고 + // zipcode/address 를 필수 검증했는데 서비스가 기존 국가를 승계해 해외 + // 분기로 떨어지면, 방금 검증한 국내 입력을 버리고 빈 문자열을 쓴다. + // 직접 입력 폼은 국내 필드만 제공해 country_code 를 보내지 않으므로, + // 해외 주문의 배송지를 국내로 바꾸는 경로가 통째로 사라진다(응답은 200). + $country = $data['country_code'] ?? 'KR'; + $isDomestic = strtoupper(trim((string) $country)) === 'KR'; + $addressData = [ 'recipient_name' => $data['recipient_name'], 'recipient_phone' => $data['recipient_phone'], - 'recipient_country_code' => $data['country_code'] ?? 'KR', + 'recipient_country_code' => $country, ]; - $isDomestic = ($data['country_code'] ?? 'KR') === 'KR'; - + // 주의: zipcode/address 컬럼은 NOT NULL 이므로 비활성 측은 null 이 아닌 빈 문자열로 초기화. + // 반대편 컬럼을 비우지 않으면 국내↔해외 전환 시 구 주소가 잔존해 배송지가 두 벌이 된다. if ($isDomestic) { - $addressData['zipcode'] = $data['zipcode'] ?? null; - $addressData['address'] = $data['address'] ?? null; + $addressData['zipcode'] = $data['zipcode'] ?? ''; + $addressData['address'] = $data['address'] ?? ''; $addressData['address_detail'] = $data['address_detail'] ?? null; + // 해외 컬럼 초기화 (nullable) + $addressData['address_line_1'] = null; + $addressData['address_line_2'] = null; + $addressData['intl_city'] = null; + $addressData['intl_state'] = null; + $addressData['intl_postal_code'] = null; } else { $addressData['address_line_1'] = $data['address_line_1'] ?? null; $addressData['address_line_2'] = $data['address_line_2'] ?? null; $addressData['intl_city'] = $data['intl_city'] ?? null; $addressData['intl_state'] = $data['intl_state'] ?? null; $addressData['intl_postal_code'] = $data['intl_postal_code'] ?? null; + // 국내 컬럼 초기화 (zipcode/address 는 NOT NULL → 빈 문자열) + $addressData['zipcode'] = ''; + $addressData['address'] = ''; + $addressData['address_detail'] = null; } // 배송 메모 (항상 업데이트) @@ -713,6 +762,23 @@ class OrderService */ public function confirmOption(Order $order, OrderOption $option): OrderOption { + // 상태 게이트를 서비스 자체에 내장한다 — 호출자(회원 FormRequest / 비회원 컨트롤러 / 훅)와 + // 무관하게 동일 강도로 강제한다. FormRequest 검사는 UI 조기 피드백용으로 유지되지만, + // 그 경로를 우회하는 호출이 이미 확정된/확정 불가 상태의 옵션을 재확정하는 것을 막는다. + if ($option->option_status === OrderStatusEnum::CONFIRMED) { + throw new OrderOptionNotConfirmableException('order_option_already_confirmed'); + } + + $confirmableStatuses = module_setting( + 'sirsoft-ecommerce', + 'order_settings.confirmable_statuses', + ['shipping', 'delivered'] + ); + + if (! in_array($option->option_status->value, $confirmableStatuses, true)) { + throw new OrderOptionNotConfirmableException('order_option_not_confirmable'); + } + $previousStatus = $order->order_status?->value; $purchaseConfirmed = false; diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/PaymentMethodResolver.php b/modules/_bundled/sirsoft-ecommerce/src/Services/PaymentMethodResolver.php index 7ea66e98..0c10f60c 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/PaymentMethodResolver.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/PaymentMethodResolver.php @@ -168,6 +168,31 @@ class PaymentMethodResolver ))); } + /** + * 지금 이 결제수단으로 주문을 받을 수 있는지 판정합니다. (A2 / 공개 #111 서버 대칭) + * + * 두 가지를 막는다: + * - `_orphaned` — 저장값은 남았지만 공급 확장이 그 수단 제공을 중단한 상태 + * - `_orphaned_pg` — 수단은 살아 있으나 지정된 PG 가 레지스트리에서 사라진 상태 + * + * 카탈로그에 아예 없는 ID 는 **막지 않는다**. 능력 질의 전반의 폴백 계약(카탈로그 → + * enum → 기본값)과 같은 방향이며, 여기서 조이면 카탈로그 구성 시점에 따라 정상 주문이 + * 거부될 수 있다. 화이트리스트 자체는 `allValidIds()` 가 이미 담당한다. + * + * @param string $methodId 결제수단 ID + * @return bool 주문 가능 여부 + */ + public function isOrderable(string $methodId): bool + { + $entry = $this->catalog()[$methodId] ?? null; + + if ($entry === null) { + return true; + } + + return ! ($entry['_orphaned'] ?? false) && ! ($entry['_orphaned_pg'] ?? false); + } + /** * 카탈로그에서 특정 결제수단의 특정 키 값을 조회합니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductCommonInfoService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductCommonInfoService.php index aa850838..f7b84cde 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductCommonInfoService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductCommonInfoService.php @@ -6,6 +6,7 @@ use App\Extension\HookManager; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductCommonInfoOperationException; use Modules\Sirsoft\Ecommerce\Models\ProductCommonInfo; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductCommonInfoRepositoryInterface; @@ -21,7 +22,7 @@ class ProductCommonInfoService /** * 공통정보 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return Collection */ public function getAllCommonInfos(array $filters = []): Collection @@ -46,8 +47,8 @@ class ProductCommonInfoService /** * 공통정보 목록 페이지네이션 조회 * - * @param array $filters 필터 조건 - * @param int $perPage 페이지당 항목 수 + * @param array $filters 필터 조건 + * @param int $perPage 페이지당 항목 수 * @return LengthAwarePaginator */ public function getPaginatedCommonInfos(array $filters = [], int $perPage = 20): LengthAwarePaginator @@ -69,7 +70,7 @@ class ProductCommonInfoService /** * 공통정보 상세 조회 * - * @param int $id 공통정보 ID + * @param int $id 공통정보 ID * @return ProductCommonInfo|null */ public function getCommonInfo(int $id): ?ProductCommonInfo @@ -93,7 +94,7 @@ class ProductCommonInfoService /** * 공통정보 생성 * - * @param array $data 공통정보 데이터 + * @param array $data 공통정보 데이터 * @return ProductCommonInfo */ public function createCommonInfo(array $data): ProductCommonInfo @@ -129,8 +130,8 @@ class ProductCommonInfoService /** * 공통정보 수정 * - * @param int $id 공통정보 ID - * @param array $data 수정할 데이터 + * @param int $id 공통정보 ID + * @param array $data 수정할 데이터 * @return ProductCommonInfo * * @throws \Exception @@ -140,7 +141,7 @@ class ProductCommonInfoService $commonInfo = $this->repository->findById($id); if (! $commonInfo) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.product_common_info_not_found')); + throw new ProductCommonInfoOperationException('sirsoft-ecommerce::exceptions.product_common_info_not_found'); } // Before 훅 @@ -172,7 +173,7 @@ class ProductCommonInfoService /** * 공통정보 삭제 * - * @param int $id 공통정보 ID + * @param int $id 공통정보 ID * @return array 삭제 결과 정보 * * @throws \Exception @@ -182,7 +183,7 @@ class ProductCommonInfoService $commonInfo = $this->repository->findById($id); if (! $commonInfo) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.product_common_info_not_found')); + throw new ProductCommonInfoOperationException('sirsoft-ecommerce::exceptions.product_common_info_not_found'); } // 연결된 상품 수 확인 @@ -211,7 +212,7 @@ class ProductCommonInfoService /** * 사용 여부 토글 * - * @param int $id 공통정보 ID + * @param int $id 공통정보 ID * @return ProductCommonInfo * * @throws \Exception @@ -221,7 +222,7 @@ class ProductCommonInfoService $commonInfo = $this->repository->findById($id); if (! $commonInfo) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.product_common_info_not_found')); + throw new ProductCommonInfoOperationException('sirsoft-ecommerce::exceptions.product_common_info_not_found'); } return $this->updateCommonInfo($id, [ diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductImageService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductImageService.php index 1ff0589f..8ec5d373 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductImageService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductImageService.php @@ -7,6 +7,7 @@ use App\Extension\HookManager; use App\Support\ImageResizer; use Illuminate\Database\Eloquent\Collection; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; @@ -16,6 +17,7 @@ use Modules\Sirsoft\Ecommerce\Exceptions\ResourceScopeMismatchException; use Modules\Sirsoft\Ecommerce\Models\ProductImage; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductImageRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Services\Concerns\ResolvesRowStorage; use Symfony\Component\HttpFoundation\StreamedResponse; /** @@ -25,6 +27,8 @@ use Symfony\Component\HttpFoundation\StreamedResponse; */ class ProductImageService { + use ResolvesRowStorage; + /** * ProductImageService 생성자 * @@ -193,11 +197,12 @@ class ProductImageService // 새 경로 생성: products/{productCode}/{filename} $newPath = "products/{$productCode}/{$image->stored_filename}"; - // 파일 이동 (get + put + delete) - $content = $this->storage->get('images', $image->path); + // 파일 이동 (get + put + delete) — 행 disk 기준 (이동 후에도 행 disk 불변) + $rowStorage = $this->storageForRow($image->disk); + $content = $rowStorage->get('images', $image->path); if ($content) { - $this->storage->put('images', $newPath, $content); - $this->storage->delete('images', $image->path); + $rowStorage->put('images', $newPath, $content); + $rowStorage->delete('images', $image->path); } // DB 업데이트: product_id 설정, temp_key 제거, path 변경, sort_order 재배치, is_thumbnail 해제 @@ -212,7 +217,10 @@ class ProductImageService $linkedCount++; } - // 빈 임시 디렉토리 정리 + // 빈 임시 디렉토리 정리 — 임시 행이 여러 disk 에 걸칠 수 있어 disk 별로 정리 + foreach ($tempImages->pluck('disk')->filter()->unique() as $tempDisk) { + $this->storageForRow($tempDisk)->deleteDirectory('images', "products/temp/{$tempKey}"); + } $this->storage->deleteDirectory('images', "products/temp/{$tempKey}"); return $linkedCount; @@ -262,9 +270,10 @@ class ProductImageService // Before 훅 HookManager::doAction('sirsoft-ecommerce.product-image.before_delete', $image); - // 스토리지에서 파일 삭제 - if ($this->storage->exists('images', $image->path)) { - $this->storage->delete('images', $image->path); + // 스토리지에서 파일 삭제 — 행 disk 기준 + $rowStorage = $this->storageForRow($image->disk); + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); } // DB에서 삭제 @@ -350,7 +359,7 @@ class ProductImageService return null; } - $response = $this->storage->response( + $response = $this->storageForRow($image->disk)->response( 'images', $image->path, $image->original_filename, @@ -364,7 +373,7 @@ class ProductImageService Log::error('상품 이미지 스토리지에 없음', [ 'product_image_id' => $image->id, 'path' => $image->path, - 'disk' => $this->storage->getDisk(), + 'disk' => $image->disk ?: $this->storage->getDisk(), ]); return null; @@ -406,10 +415,11 @@ class ProductImageService $newStoredFilename = Str::uuid().'.'.$extension; $newPath = "products/{$targetProductCode}/{$newStoredFilename}"; - // 파일 복사 - $content = $this->storage->get('images', $source->path); + // 파일 복사 — 원본 행 disk 기준 (신규 행도 원본과 같은 disk 로 기록됨) + $sourceStorage = $this->storageForRow($source->disk); + $content = $sourceStorage->get('images', $source->path); if ($content) { - $this->storage->put('images', $newPath, $content); + $sourceStorage->put('images', $newPath, $content); } else { Log::warning('상품 이미지 복사 실패: 원본 파일 없음', [ 'hash' => $sourceHash, @@ -445,18 +455,128 @@ class ProductImageService * 실제 경로: storage/app/modules/sirsoft-ecommerce/images/products/{productCode}/ * * @param int $productId 상품 ID - * @return bool 삭제 성공 여부 + * @return bool 삭제 성공 여부 (대상 디스크 전체 기준) */ public function deleteByProductId(int $productId): bool { $product = $this->productRepository->find($productId); - if ($product) { - return $this->storage->deleteDirectory('images', "products/{$product->product_code}"); + // 행이 여러 disk 에 걸칠 수 있어(디스크 전환 후 혼재) disk 별로 디렉토리 삭제. + // 빈 disk(디스크 컬럼 도입 전 구 데이터)는 주입 스토리지 소속으로 정규화한다. + $rowDisks = $this->repository->getByProductId($productId) + ->pluck('disk') + ->map(fn ($disk) => ($disk === null || $disk === '') ? $this->storage->getDisk() : $disk) + ->unique(); + + $directory = $product + ? "products/{$product->product_code}" + // 폴백: product_id 기반 (기존 데이터 호환) + : "products/{$productId}"; + + if ($rowDisks->isEmpty()) { + // 행이 없으면 주입 스토리지의 잔여 디렉토리만 정리 (기존 동작 보존) + return $this->storage->deleteDirectory('images', $directory); } - // 폴백: product_id 기반 (기존 데이터 호환) - return $this->storage->deleteDirectory('images', "products/{$productId}"); + $results = []; + foreach ($rowDisks as $rowDisk) { + $results[] = $this->storageForRow($rowDisk)->deleteDirectory('images', $directory); + } + + // 행이 실리지 않은 주입 스토리지의 잔여 파일도 정리 시도하되 반환값에는 + // 반영하지 않는다 — 행 기준으로는 지울 것이 없는 디스크의 실패가 혼재 행 + // 삭제 성공을 false 로 오염시키면 안 된다 (반환값 = 행이 실린 디스크 전체 기준) + if (! $rowDisks->contains($this->storage->getDisk())) { + $this->storage->deleteDirectory('images', $directory); + } + + return ! in_array(false, $results, true); + } + + /** + * 상품에 연결되지 않은 채 방치된 임시 이미지를 정리합니다. + * + * 상품 등록 폼에서 이미지를 올린 뒤 저장하지 않고 이탈하면 `temp_key` 만 남은 행과 + * 그 파일이 남습니다. 연결 시점에 본경로로 옮겨지므로 `temp_key` 가 남아 있다는 것은 + * "끝내 연결되지 않았다" 는 뜻이고, 그래서 오탐 여지가 없습니다. + * + * 파일은 행마다 기록된 disk 를 향해 지웁니다 (디스크 전환 이후 혼재 대응). + * 파일 → 행 순서를 지켜, 행이 먼저 사라져 파일을 못 찾는 상태를 만들지 않습니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 처리할 최대 건수 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + public function pruneTempUploads(int $days, int $limit, bool $dryRun = false): array + { + $threshold = Carbon::now()->subDays($days); + $images = $this->repository->findStaleTempImages($threshold, $limit); + + $result = ['scanned' => $images->count(), 'deleted' => 0, 'failed' => 0]; + + if ($dryRun) { + return $result; + } + + foreach ($images as $image) { + $rowStorage = $this->storageForRow($image->disk); + + if ($rowStorage->exists('images', $image->path) && ! $rowStorage->delete('images', $image->path)) { + Log::warning('임시 상품 이미지 파일 삭제 실패 — 기록 보존', [ + 'image_id' => $image->id, + 'disk' => $image->disk, + 'path' => $image->path, + ]); + + $result['failed']++; + + continue; + } + + $this->repository->delete($image->id); + $result['deleted']++; + } + + $this->removeEmptyTempDirectories($images); + + return $result; + } + + /** + * 파일을 모두 지운 temp_key 디렉토리를 정리합니다. + * + * 파일만 지우고 디렉토리를 남기면 폼 세션마다 빈 디렉토리가 쌓여, 정리를 돌려도 + * 저장소에는 흔적이 계속 늘어납니다. + * + * 디렉토리에 파일이 남아 있으면(같은 temp_key 의 다른 이미지가 limit 에 걸려 이번 회차에서 + * 빠졌거나 파일 삭제에 실패한 경우 등) 삭제하지 않습니다. + * + * @param Collection $images 이번 회차에 처리한 이미지 목록 + */ + private function removeEmptyTempDirectories(Collection $images): void + { + $directories = []; + + foreach ($images as $image) { + $directory = dirname((string) $image->path); + + if ($directory === '' || $directory === '.') { + continue; + } + + $directories[$image->disk.'|'.$directory] = [$image->disk, $directory]; + } + + foreach ($directories as [$disk, $directory]) { + $storage = $this->storageForRow($disk); + + if ($storage->files('images', $directory) !== []) { + continue; + } + + $storage->deleteDirectory('images', $directory); + } } /** diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php index 922db4c8..ba457184 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductInquiryService.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductInquiryOperationException; use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductInquiryRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductRepositoryInterface; @@ -85,39 +86,39 @@ class ProductInquiryService $boardSlug ); - // 피벗 기준 전체 목록 조회 (페이지네이션 전 — 비밀글 필터 적용 위해) - $pivots = $this->repository->findByProductId($productId); - $currentUserId = Auth::id(); - // inquirable_id 목록으로 Post 데이터 일괄 조회 - $ids = $pivots->pluck('inquirable_id')->all(); - $posts = []; - if (! empty($ids)) { - $rawPosts = HookManager::applyFilters( - 'sirsoft-ecommerce.inquiry.get_by_ids', - [], - ['ids' => $ids, 'slug' => $boardSlug] - ); - foreach ($rawPosts as $post) { - $postId = $post['id'] ?? null; - if ($postId) { - $posts[$postId] = $post; - } - } - } - - // 비밀글 제외 필터 적용 (Post의 is_secret 기준) + // 비밀글 원문 마스킹은 게시판 훅(getByIds)이 요청자 신원 기준으로 서버측에서 + // 이미 수행한다(KVE-2026-1914, SecretContentGate SSoT). 아래 exclude_secret 은 + // 보안 판정이 아니라 단순 "비밀글 행 숨김" 표시 필터일 뿐이며, 노출 여부는 + // 클라이언트 파라미터와 무관하게 서버가 결정한다. if ($excludeSecret) { + // 비밀글 판정(is_secret)은 게시판 모듈의 게시글 데이터에만 있어 SQL 로 거를 수 + // 없다 — 이 경로만 전량 조회 후 PHP 필터가 구조적으로 필요하다. 기본 화면 + // 경로(exclude_secret=false)는 아래 else 의 쿼리 레벨 페이지네이션을 쓴다. + $pivots = $this->repository->findByProductId($productId); + $posts = $this->fetchPostsByIds($pivots->pluck('inquirable_id')->all(), $boardSlug); + + // 비밀글 제외 필터 적용 (Post의 is_secret 기준) $pivots = $pivots->filter(function ($pivot) use ($posts) { $post = $posts[$pivot->inquirable_id] ?? null; return empty($post['is_secret']); })->values(); - } - $total = $pivots->count(); - $pagePivots = $pivots->forPage($page, $perPage); + $total = $pivots->count(); + $pagePivots = $pivots->forPage($page, $perPage); + $lastPage = (int) ceil($total / $perPage); + } else { + // 화면 목록은 쿼리 레벨 페이지네이션 — 전량 적재 후 PHP 잘라내기(#102 동형)를 + // 하지 않는다. 게시글 데이터도 이 페이지 분량만 일괄 조회한다. + $paginator = $this->repository->paginateByProductId($productId, $perPage, $page); + $pagePivots = collect($paginator->items()); + $posts = $this->fetchPostsByIds($pagePivots->pluck('inquirable_id')->all(), $boardSlug); + + $total = $paginator->total(); + $lastPage = $paginator->lastPage(); + } // user_id 일괄 조회 (N+1 방지) $userIds = $pagePivots->map(fn ($pivot) => $posts[$pivot->inquirable_id]['user_id'] ?? null) @@ -131,26 +132,40 @@ class ProductInquiryService $userId = $post['user_id'] ?? null; $name = $userId ? ($userMap[$userId] ?? $post['author_name'] ?? null) : ($post['author_name'] ?? null); + // 비밀글 이중 방어(KVE-2026-1914): 게시판 훅(getByIds)이 이미 요청자 신원으로 + // 원문을 마스킹하지만, 훅이 신원 판정만 하고 특정 필드 null 처리를 누락하는 회귀에 + // 대비해 훅이 실어 보낸 권위 플래그(can_view_secret)로 payload 를 재확정한다. + // 자기 권한을 재계산하지 않으므로(플래그만 신뢰) 게이트 강도가 훅과 갈리지 않는다. + // + // fail-closed: 비밀글인데 권위 플래그가 없으면(훅 미치환·타 확장 치환으로 누락) + // 열람 가능으로 가정하지 않고 마스킹한다. 플래그 부재 = 권위 미상 = 안전 측(감춤). + // 비밀글이 아니면($isSecret=false) 어느 경우에도 마스킹되지 않으므로 영향 없다. + $isSecret = $post['is_secret'] ?? false; + $secretMasked = $isSecret && $post !== null && ($post['can_view_secret'] ?? false) === false; + return [ 'id' => $pivot->id, 'post_id' => $pivot->inquirable_id, 'user_id' => $userId, 'author_name' => $this->maskAuthorName($name), - 'title' => $post['title'] ?? null, + // 비밀글이면 title 도 fail-closed 로 재마스킹한다(KVE-2026-1914 A2b). + // 훅이 신원 판정만 하고 title 치환을 누락하는 회귀에 대비 — 플레이스홀더 텍스트 + // 자체는 게시판 lang 키(post.secret_post_title)가 SSoT 로, 훅의 마스킹 값과 동일하다. + 'title' => $secretMasked + ? __('sirsoft-board::messages.post.secret_post_title') + : ($post['title'] ?? null), 'category' => $post['category'] ?? null, - 'content' => $post['content'] ?? null, - 'is_secret' => $post['is_secret'] ?? false, + 'content' => $secretMasked ? null : ($post['content'] ?? null), + 'is_secret' => $isSecret, 'is_owner' => $isOwner, 'is_answered' => $pivot->is_answered ?? false, 'answered_at' => $pivot->answered_at?->toIso8601String(), 'created_at' => $pivot->created_at?->toIso8601String(), - 'reply' => $post['reply'] ?? null, - 'attachments' => $post['attachments'] ?? [], + 'reply' => $secretMasked ? null : ($post['reply'] ?? null), + 'attachments' => $secretMasked ? [] : ($post['attachments'] ?? []), ]; })->values()->all(); - $lastPage = (int) ceil($total / $perPage); - return [ 'items' => $items, 'meta' => [ @@ -168,6 +183,36 @@ class ProductInquiryService ]; } + /** + * inquirable_id 목록으로 게시글 데이터를 일괄 조회합니다 (N+1 방지). + * + * @param array<int, int> $ids 게시글 ID 목록 + * @param string $boardSlug 문의 게시판 슬러그 + * @return array<int, array<string, mixed>> 게시글 ID => 게시글 데이터 + */ + private function fetchPostsByIds(array $ids, string $boardSlug): array + { + if (empty($ids)) { + return []; + } + + $rawPosts = HookManager::applyFilters( + 'sirsoft-ecommerce.inquiry.get_by_ids', + [], + ['ids' => $ids, 'slug' => $boardSlug] + ); + + $posts = []; + foreach ($rawPosts as $post) { + $postId = $post['id'] ?? null; + if ($postId) { + $posts[$postId] = $post; + } + } + + return $posts; + } + /** * 기본 게시판 설정값 반환 * @@ -205,9 +250,7 @@ class ProductInquiryService $boardSlug = $this->getInquiryBoardSlug(); if (! $boardSlug) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_not_configured') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); } $product = $this->productRepository->find($productId); @@ -223,6 +266,12 @@ class ProductInquiryService $data['user_id'] = Auth::id(); } + // 클라이언트 IP 를 요청 경계(Service)에서 캡처해 게시판 훅 payload 로 전달한다. + // 게시판 Listener 가 request() 를 직접 참조하지 않도록 소유 서비스가 주입한다. + if (empty($data['ip_address'])) { + $data['ip_address'] = request()->ip() ?? '0.0.0.0'; + } + $inquiry = DB::transaction(function () use ($productId, $product, $boardSlug, $data) { // 게시판 훅으로 Post 생성 $postResult = HookManager::applyFilters( @@ -233,9 +282,7 @@ class ProductInquiryService ); if (! $postResult || empty($postResult['post_id'])) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_unavailable') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_unavailable'); } // 상품명 스냅샷 (다국어) — name은 array cast @@ -376,17 +423,13 @@ class ProductInquiryService $inquiry = $this->repository->findById($inquiryId); if (! $inquiry) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.not_found') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.not_found'); } $boardSlug = $this->getInquiryBoardSlug(); if (! $boardSlug) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_not_configured') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); } HookManager::applyFilters( @@ -421,17 +464,13 @@ class ProductInquiryService $inquiry = $this->repository->findById($inquiryId); if (! $inquiry) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.not_found') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.not_found'); } $boardSlug = $this->getInquiryBoardSlug(); if (! $boardSlug) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_not_configured') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); } // ① Post 삭제 — after_delete Action 훅 포함, 트랜잭션 외부에서 실행 @@ -467,17 +506,13 @@ class ProductInquiryService $inquiry = $this->repository->findById($inquiryId); if (! $inquiry) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.not_found') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.not_found'); } $boardSlug = $this->getInquiryBoardSlug(); if (! $boardSlug) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_not_configured') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); } HookManager::applyFilters( @@ -511,17 +546,13 @@ class ProductInquiryService $inquiry = $this->repository->findById($inquiryId); if (! $inquiry) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.not_found') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.not_found'); } $boardSlug = $this->getInquiryBoardSlug(); if (! $boardSlug) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.board_not_configured') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); } // ① Reply Post 삭제 — after_delete Action 훅 포함, 트랜잭션 외부에서 실행 @@ -532,11 +563,24 @@ class ProductInquiryService $inquiry->inquirable_id ); - // ② 피벗 is_answered=false 업데이트 - $this->repository->unmarkAnswered($inquiry); + // ② 잔여 답변이 없을 때만 피벗 is_answered=false 업데이트. + // 기설치본에는 과거 결함(#106)으로 답변이 여러 건 쌓인 문의가 있을 수 있다 — + // 첫 답변만 지웠는데 무조건 해제하면 "답변이 남았는데 미답변" 표기가 된다. + // 기본값 null = 판정 불가: 공급자(board 리스너) 부재 시 0 으로 접혀 + // 답변완료가 오해제되는 fail-open 을 막는다. + $remaining = HookManager::applyFilters( + 'sirsoft-ecommerce.inquiry.count_replies', + null, + $inquiry->inquirable_id + ); + + if (is_numeric($remaining) && (int) $remaining === 0) { + $this->repository->unmarkAnswered($inquiry); + } Log::info('상품 문의 답변 삭제 완료', [ 'inquiry_id' => $inquiryId, + 'remaining_replies' => $remaining, ]); } @@ -551,6 +595,54 @@ class ProductInquiryService return $this->repository->findById($inquiryId); } + /** + * 상품 삭제 시 그 상품의 문의 스레드(질문+답변 Post)와 피벗을 정리합니다. + * + * 상품이 forceDelete 되면 피벗은 FK 캐스케이드로 하드 소멸하지만, 게시판의 + * 질문·답변 Post 는 published 로 잔존했다(#107 확대판). 애플리케이션이 먼저 + * 훅으로 Post 를 소프트 삭제(cascade_replies 로 답변 포함)한 뒤 피벗을 + * forceDelete 한다 — FK 캐스케이드는 백스톱으로만 유지. + * + * 문의 게시판이 미설정이면 Post 정리는 건너뛰고 피벗만 정리한다(안전 열화). + * 개별 훅 실패는 warning 후 계속 진행한다 — 한 건의 실패가 상품 삭제 전체를 + * 막으면 안 되고, 남은 고아는 업그레이드 스텝 백필이 재정리한다. + * + * @param int $productId 상품 ID + * @return int 정리된 피벗 수 + */ + public function deleteInquiriesForProduct(int $productId): int + { + $boardSlug = $this->getInquiryBoardSlug(); + + if ($boardSlug) { + // withTrashed 포함 — 소프트 삭제된 문의의 Post 잔존물도 함께 정리 + $inquiries = $this->repository->findByProductIdWithTrashed($productId); + + foreach ($inquiries as $inquiry) { + try { + HookManager::applyFilters( + 'sirsoft-ecommerce.inquiry.delete', + null, + $boardSlug, + $inquiry->inquirable_id + ); + } catch (\Exception $e) { + Log::warning('상품 삭제 시 문의 Post 정리 실패 — 계속 진행', [ + 'product_id' => $productId, + 'inquiry_id' => $inquiry->id, + 'error' => $e->getMessage(), + ]); + } + } + } else { + Log::info('문의 게시판 미설정 — 상품 삭제 시 문의 Post 정리 생략', [ + 'product_id' => $productId, + ]); + } + + return $this->repository->forceDeleteByProductId($productId); + } + /** * 작성자 이름 마스킹 처리 * @@ -614,13 +706,28 @@ class ProductInquiryService $inquiry = $this->repository->findById($inquiryId); if (! $inquiry) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.not_found') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.not_found'); } $boardSlug = $this->getInquiryBoardSlug(); + // update/delete 경로와 동일한 미설정 가드 — 없으면 훅 무응답이 reply_failed 로 위장된다 + if (! $boardSlug) { + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); + } + + // 1차 방어(피벗 플래그): 이미 답변된 문의에는 재등록 거부 — 단일 답변 정책. + // UI 는 `!item.reply` 게이팅으로 이미 차단하므로, 이 가드는 API 직접 호출/동시 + // 클릭 경로를 막는다. 게시판 실데이터 기준 2차 방어는 리스너(createAndReturn)가 담당. + if ($inquiry->is_answered) { + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.reply_already_exists'); + } + + // 클라이언트 IP 를 요청 경계(Service)에서 캡처해 게시판 훅 payload 로 전달한다. + if (empty($data['ip_address'])) { + $data['ip_address'] = request()->ip() ?? '0.0.0.0'; + } + $updated = DB::transaction(function () use ($inquiry, $boardSlug, $data) { // 게시판 훅으로 Reply Post 생성 (title은 리스너에서 Re: 부모글제목 형식으로 설정) $replyData = array_merge($data, [ @@ -633,10 +740,15 @@ class ProductInquiryService $replyData ); + // 2차 방어(리스너, 게시판 실데이터)가 중복을 감지하면 중복 마커를 돌려준다. + // 경합 경로(피벗 is_answered 가 아직 false)에서도 사유를 보존해 422 로 안내 + // — null 과 합치면 "답변 등록에 실패했습니다" 로 위장된다 (운영 실측 제보). + if (is_array($postResult) && ! empty($postResult['duplicate'])) { + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.reply_already_exists'); + } + if (! $postResult || empty($postResult['post_id'])) { - throw new \RuntimeException( - __('sirsoft-ecommerce::messages.inquiries.reply_failed') - ); + throw new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.reply_failed'); } // 피벗 is_answered 업데이트 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductLabelService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductLabelService.php index 59e90d4a..e298b9db 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductLabelService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductLabelService.php @@ -6,6 +6,7 @@ use App\Extension\HookManager; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductLabelOperationException; use Modules\Sirsoft\Ecommerce\Models\ProductLabel; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductLabelRepositoryInterface; @@ -21,7 +22,7 @@ class ProductLabelService /** * 라벨 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return Collection */ public function getAllLabels(array $filters = []): Collection @@ -46,7 +47,7 @@ class ProductLabelService /** * 라벨 상세 조회 * - * @param int $id 라벨 ID + * @param int $id 라벨 ID * @return ProductLabel|null */ public function getLabel(int $id): ?ProductLabel @@ -80,7 +81,7 @@ class ProductLabelService /** * 라벨 생성 * - * @param array $data 라벨 데이터 + * @param array $data 라벨 데이터 * @return ProductLabel */ public function createLabel(array $data): ProductLabel @@ -107,16 +108,17 @@ class ProductLabelService /** * 라벨 수정 * - * @param int $id 라벨 ID - * @param array $data 수정할 데이터 + * @param int $id 라벨 ID + * @param array $data 수정할 데이터 * @return ProductLabel + * * @throws \Exception */ public function updateLabel(int $id, array $data): ProductLabel { $label = $this->repository->findById($id); - if (!$label) { + if (! $label) { throw (new ModelNotFoundException)->setModel(ProductLabel::class, $id); } @@ -145,15 +147,16 @@ class ProductLabelService /** * 라벨 상태 토글 * - * @param int $id 라벨 ID + * @param int $id 라벨 ID * @return ProductLabel + * * @throws \Exception */ public function toggleStatus(int $id): ProductLabel { $label = $this->repository->findById($id); - if (!$label) { + if (! $label) { throw (new ModelNotFoundException)->setModel(ProductLabel::class, $id); } @@ -162,7 +165,7 @@ class ProductLabelService $label = DB::transaction(function () use ($label) { return $this->repository->update($label->id, [ - 'is_active' => !$label->is_active, + 'is_active' => ! $label->is_active, ]); }); @@ -175,15 +178,16 @@ class ProductLabelService /** * 라벨 삭제 * - * @param int $id 라벨 ID + * @param int $id 라벨 ID * @return array 삭제 결과 정보 + * * @throws \Exception */ public function deleteLabel(int $id): array { $label = $this->repository->findById($id); - if (!$label) { + if (! $label) { throw (new ModelNotFoundException)->setModel(ProductLabel::class, $id); } @@ -192,9 +196,9 @@ class ProductLabelService // 연결된 상품이 있으면 삭제 차단 if ($productsCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.label_has_products', [ + throw new ProductLabelOperationException('sirsoft-ecommerce::exceptions.label_has_products', [ 'count' => $productsCount, - ])); + ]); } // Before 훅 diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php index ef4b70bc..b9e986f7 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewImageService.php @@ -9,10 +9,12 @@ use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; +use Modules\Sirsoft\Ecommerce\Enums\ReviewStatus; use Modules\Sirsoft\Ecommerce\Exceptions\ReviewImageUploadLimitException; use Modules\Sirsoft\Ecommerce\Models\ProductReview; use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductReviewImageRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Services\Concerns\ResolvesRowStorage; use Symfony\Component\HttpFoundation\StreamedResponse; /** @@ -22,6 +24,8 @@ use Symfony\Component\HttpFoundation\StreamedResponse; */ class ProductReviewImageService { + use ResolvesRowStorage; + /** * ProductReviewImageService 생성자 * @@ -48,7 +52,8 @@ class ProductReviewImageService { $maxImages = (int) $this->settingsService->getSetting( 'review_settings.max_images', - config('ecommerce.review.max_images', 5) + // 모듈 config 는 'sirsoft-ecommerce' 네임스페이스로 로드된다 (ModuleManager::loadModuleConfig) + config('sirsoft-ecommerce.review.max_images', 5) ); $currentCount = $review->images()->count(); @@ -58,6 +63,9 @@ class ProductReviewImageService HookManager::doAction('sirsoft-ecommerce.review-image.before_upload', $file, $review); + // 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트) + $file = HookManager::applyFilters('sirsoft-ecommerce.review-image.filter_upload_file', $file); + $storedFilename = Str::uuid().'.'.$file->getClientOriginalExtension(); $path = "reviews/{$review->id}/{$storedFilename}"; @@ -118,8 +126,10 @@ class ProductReviewImageService { HookManager::doAction('sirsoft-ecommerce.review-image.before_delete', $image); - if ($this->storage->exists('images', $image->path)) { - $this->storage->delete('images', $image->path); + // 스토리지에서 파일 삭제 — 행 disk 기준 + $rowStorage = $this->storageForRow($image->disk); + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); } $result = $this->repository->delete($image); @@ -145,10 +155,17 @@ class ProductReviewImageService /** * 이미지 다운로드 응답 생성 * + * $signatureVerified 는 요청 URL 의 한시 서명이 검증된 경우다 — 서명은 + * 숨김 리뷰 게이트(관리자 권한 미들웨어)를 통과한 응답 직렬화 + * (ProductReviewResource)만 발급하므로, 검증된 서명은 그 게이트 통과 자격의 + * 위임으로 보고 상태 게이트를 재적용하지 않는다 (<img> 는 인증 헤더를 실을 수 + * 없는 렌더 경로). + * * @param string $hash 이미지 해시 (12자) + * @param bool $signatureVerified 한시 서명 검증 통과 여부 * @return StreamedResponse|null 다운로드 응답 (없으면 null) */ - public function download(string $hash): ?StreamedResponse + public function download(string $hash, bool $signatureVerified = false): ?StreamedResponse { $image = $this->findByHash($hash); @@ -156,7 +173,19 @@ class ProductReviewImageService return null; } - $response = $this->storage->response( + // 숨김(HIDDEN) 리뷰의 이미지는 서빙하지 않는다(KVE-2026-1914 S-2). + // 관리자가 숨긴 리뷰의 이미지가 해시만으로 공개 서빙되던 불일치를 정합화한다. + // + // 부모 리뷰를 못 읽으면 **가린다**(fail-closed). ProductReview 는 소프트 삭제되므로 + // 관계 조회가 null 을 돌려줄 수 있는데, 그때 게이트가 성립하지 않아 통과하면 + // "부모가 사라진 이미지는 무조건 공개" 가 된다 — 첨부 게이트가 채택한 방향과 반대다. + // 현재는 리뷰 삭제 시 이미지도 함께 삭제돼 여기까지 오지 않지만, 그 두 서비스가 + // 계속 동기화된다는 암묵 불변식에 보안을 걸지 않는다. + if (! $signatureVerified && (! $image->review || $image->review->status !== ReviewStatus::VISIBLE)) { + return null; + } + + $response = $this->storageForRow($image->disk)->response( 'images', $image->path, $image->original_filename, diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php index 2de49193..f29a5130 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductReviewService.php @@ -15,7 +15,9 @@ use Modules\Sirsoft\Ecommerce\Models\ProductReview; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderOptionRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductReviewImageRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductReviewRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Services\Concerns\ResolvesRowStorage; use Modules\Sirsoft\Ecommerce\Support\ReviewWritePolicy; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 상품 리뷰 서비스 @@ -24,6 +26,9 @@ use Modules\Sirsoft\Ecommerce\Support\ReviewWritePolicy; */ class ProductReviewService { + use ReappliesPermissionScope; + use ResolvesRowStorage; + /** * ProductReviewService 생성자 * @@ -132,10 +137,16 @@ class ProductReviewService $orderOption = $this->orderOptionRepository->findOrFail($data['order_option_id']); $optionSnapshot = $orderOption->option_snapshot ?? []; + // product_id 는 클라이언트 payload 가 아니라 확정된 주문 옵션에서 도출한다(SSoT). + // canWrite 가 옵션 소유권/확정만 검증하므로, payload 의 product_id 를 그대로 신뢰하면 + // 소유하지 않은 상품에 리뷰를 붙일 수 있다. 도출값이 유일한 진실이므로 payload 값은 + // 옵션과 일치하든 아니든 무시한다. + $resolvedProductId = (int) $orderOption->product_id; + HookManager::doAction('sirsoft-ecommerce.product-review.before_create', $data); $review = $this->repository->create([ - 'product_id' => $data['product_id'], + 'product_id' => $resolvedProductId, 'order_option_id' => $data['order_option_id'], 'user_id' => $userId, 'rating' => $data['rating'], @@ -165,6 +176,8 @@ class ProductReviewService */ public function updateStatus(ProductReview $review, string $status): ProductReview { + $this->assertWithinScope($review, 'sirsoft-ecommerce.reviews.update'); + return $this->repository->update($review, ['status' => $status]); } @@ -197,6 +210,8 @@ class ProductReviewService */ public function deleteReply(ProductReview $review): ProductReview { + $this->assertWithinScope($review, 'sirsoft-ecommerce.reviews.update'); + return $this->repository->update($review, [ 'reply_content' => null, 'reply_content_mode' => 'text', @@ -214,13 +229,16 @@ class ProductReviewService */ public function deleteReview(ProductReview $review): bool { + $this->assertWithinScope($review, 'sirsoft-ecommerce.reviews.delete'); + HookManager::doAction('sirsoft-ecommerce.product-review.before_delete', $review); return DB::transaction(function () use ($review) { - // 이미지 파일 삭제 (StorageInterface 사용) + // 이미지 파일 삭제 (StorageInterface 사용) — 행 disk 기준 foreach ($review->images as $image) { - if ($this->storage->exists('images', $image->path)) { - $this->storage->delete('images', $image->path); + $rowStorage = $this->storageForRow($image->disk); + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); } } @@ -247,6 +265,8 @@ class ProductReviewService */ public function bulkUpdateStatus(array $ids, string $status): int { + $this->assertAllWithinScope($this->repository->getByIdsWithImages($ids), 'sirsoft-ecommerce.reviews.update'); + return $this->repository->bulkUpdateStatus($ids, $status); } @@ -260,17 +280,20 @@ class ProductReviewService { $reviews = $this->repository->getByIdsWithImages($ids); + $this->assertAllWithinScope($reviews, 'sirsoft-ecommerce.reviews.delete'); + // 삭제 전 스냅샷 캡처 (after_bulk_delete 훅에 전달) $snapshots = $reviews->keyBy('id')->map->toArray()->all(); HookManager::doAction('sirsoft-ecommerce.product-review.before_bulk_delete', $ids, $reviews); return DB::transaction(function () use ($reviews, $ids, $snapshots) { - // 이미지 파일 일괄 삭제 + // 이미지 파일 일괄 삭제 — 행 disk 기준 foreach ($reviews as $review) { foreach ($review->images as $image) { - if ($this->storage->exists('images', $image->path)) { - $this->storage->delete('images', $image->path); + $rowStorage = $this->storageForRow($image->disk); + if ($rowStorage->exists('images', $image->path)) { + $rowStorage->delete('images', $image->path); } } } diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php index d41566f6..981b9ba1 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ProductService.php @@ -22,12 +22,15 @@ use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderOptionRepositoryInterf use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductAdditionalOptionValueRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductLabelRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 상품 서비스 */ class ProductService { + use ReappliesPermissionScope; + /** * 검색 정렬 이름 → [실제 컬럼, 방향] 선언 * @@ -67,7 +70,8 @@ class ProductService protected SequenceService $sequenceService, protected OrderOptionRepositoryInterface $orderOptionRepository, protected ProductLabelRepositoryInterface $productLabelRepository, - protected ProductAdditionalOptionValueRepositoryInterface $additionalOptionValueRepository + protected ProductAdditionalOptionValueRepositoryInterface $additionalOptionValueRepository, + protected ProductInquiryService $inquiryService ) {} /** @@ -301,6 +305,8 @@ class ProductService */ public function update(Product $product, array $data): Product { + $this->assertWithinScope($product, 'sirsoft-ecommerce.products.update'); + // 수정 전 훅 HookManager::doAction('sirsoft-ecommerce.product.before_update', $product, $data); @@ -405,6 +411,8 @@ class ProductService */ public function delete(Product $product): bool { + $this->assertWithinScope($product, 'sirsoft-ecommerce.products.delete'); + // 도메인 가드: 주문 이력이 있는 상품은 삭제 불가 (컨트롤러 우회·bulk 경로 방어) // DB FK restrictOnDelete 가 거부하기 전에 사유가 명확한 예외로 차단한다. $ordersCount = $this->orderOptionRepository->countByProductId($product->id); @@ -415,6 +423,11 @@ class ProductService // 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.product.before_delete', $product); + // 상품 문의 스레드 정리 — 트랜잭션 진입 전 실행 (게시판 훅이 자체 트랜잭션과 + // after_delete Action 훅을 내부에서 발행하므로 바깥 트랜잭션에 묶지 않는다). + // 종전에는 피벗이 FK 캐스케이드로만 소멸해 질문·답변 Post 가 공개 상태로 잔존했다. + $this->inquiryService->deleteInquiriesForProduct($product->id); + return DB::transaction(function () use ($product) { // 1. 이미지 파일 물리적 삭제 (Storage) $this->deleteProductImageFiles($product); @@ -451,9 +464,6 @@ class ProductService // TODO: 리뷰 삭제 (테이블: ecommerce_reviews) // Review::where('product_id', $product->id)->delete(); - // TODO: 상품문의 삭제 (테이블: ecommerce_product_inquiries) - // ProductInquiry::where('product_id', $product->id)->delete(); - // 8. 상품 레코드 완전 삭제 (SoftDeletes 무시) // 모든 연관 데이터가 완전 삭제되었으므로 상품도 완전 삭제합니다. $result = $this->repository->forceDelete($product); @@ -489,6 +499,8 @@ class ProductService */ public function bulkUpdateStatus(array $ids, string $field, string $value): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($ids), 'sirsoft-ecommerce.products.update'); + // 스냅샷 캡처 (활동 로그 변경 감지용) $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -522,6 +534,8 @@ class ProductService */ public function bulkUpdatePrice(array $ids, string $method, float $value, string $unit): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($ids), 'sirsoft-ecommerce.products.update'); + // 스냅샷 캡처 (활동 로그 변경 감지용) $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -554,6 +568,8 @@ class ProductService */ public function bulkUpdateStock(array $ids, string $method, int $value): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($ids), 'sirsoft-ecommerce.products.update'); + // 스냅샷 캡처 (활동 로그 변경 감지용) $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -584,6 +600,8 @@ class ProductService */ public function bulkUpdate(array $data): array { + $this->assertAllWithinScope($this->repository->findByIdsKeyed($data['ids'] ?? []), 'sirsoft-ecommerce.products.update'); + // 스냅샷 캡처 (활동 로그 변경 감지용) $ids = $data['ids'] ?? []; $snapshots = $this->repository->getSnapshotsByIds($ids); @@ -1616,7 +1634,7 @@ class ProductService 'hash' => $img->hash, 'url' => $img->url, 'original_filename' => $img->original_filename, - 'download_url' => '/api/modules/sirsoft-ecommerce/product-image/'.$img->hash, + 'download_url' => $img->download_url, 'file_size' => $img->file_size, 'size' => $img->file_size, 'size_formatted' => $this->formatFileSize($img->file_size), diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingCarrierService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingCarrierService.php index 5a7accfe..278ef2f9 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingCarrierService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingCarrierService.php @@ -6,6 +6,7 @@ use App\Extension\HookManager; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingCarrierOperationException; use Modules\Sirsoft\Ecommerce\Models\ShippingCarrier; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderShippingRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingCarrierRepositoryInterface; @@ -27,7 +28,7 @@ class ShippingCarrierService /** * 배송사 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return Collection */ public function getAllCarriers(array $filters = []): Collection @@ -48,7 +49,7 @@ class ShippingCarrierService /** * 배송사 상세 조회 * - * @param int $id 배송사 ID + * @param int $id 배송사 ID * @return ShippingCarrier|null */ public function getCarrier(int $id): ?ShippingCarrier @@ -68,7 +69,7 @@ class ShippingCarrierService /** * 활성 배송사 목록 조회 (Select 옵션용) * - * @param string|null $type 배송사 유형 필터 + * @param string|null $type 배송사 유형 필터 * @return Collection */ public function getActiveCarriers(?string $type = null): Collection @@ -79,7 +80,7 @@ class ShippingCarrierService /** * 배송사 생성 * - * @param array $data 배송사 데이터 + * @param array $data 배송사 데이터 * @return ShippingCarrier */ public function createCarrier(array $data): ShippingCarrier @@ -105,9 +106,10 @@ class ShippingCarrierService /** * 배송사 수정 * - * @param int $id 배송사 ID - * @param array $data 수정할 데이터 + * @param int $id 배송사 ID + * @param array $data 수정할 데이터 * @return ShippingCarrier + * * @throws \Exception */ public function updateCarrier(int $id, array $data): ShippingCarrier @@ -115,7 +117,7 @@ class ShippingCarrierService $carrier = $this->repository->findById($id); if (! $carrier) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_carrier_not_found')); + throw new ShippingCarrierOperationException('sirsoft-ecommerce::exceptions.shipping_carrier_not_found'); } HookManager::doAction('sirsoft-ecommerce.shipping_carrier.before_update', $id, $data); @@ -141,8 +143,9 @@ class ShippingCarrierService /** * 배송사 상태 토글 * - * @param int $id 배송사 ID + * @param int $id 배송사 ID * @return ShippingCarrier + * * @throws \Exception */ public function toggleStatus(int $id): ShippingCarrier @@ -150,7 +153,7 @@ class ShippingCarrierService $carrier = $this->repository->findById($id); if (! $carrier) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_carrier_not_found')); + throw new ShippingCarrierOperationException('sirsoft-ecommerce::exceptions.shipping_carrier_not_found'); } HookManager::doAction('sirsoft-ecommerce.shipping_carrier.before_toggle_status', $carrier); @@ -174,14 +177,15 @@ class ShippingCarrierService * - id 없음 → 새 carrier 생성 * - DB에 있지만 payload에 없음 → 삭제 (주문에서 사용 중이면 예외) * - * @param array $carriersData carriers 배열 + * @param array $carriersData carriers 배열 * @return void + * * @throws \Exception 사용 중인 배송사 삭제 시도 시 */ public function syncCarriers(array $carriersData): void { DB::transaction(function () use ($carriersData) { - $existingIds = ShippingCarrier::pluck('id')->toArray(); + $existingIds = $this->repository->pluckIds(); $incomingIds = array_filter(array_column($carriersData, 'id')); // 삭제: DB에 있지만 payload에 없는 항목 @@ -189,11 +193,11 @@ class ShippingCarrierService foreach ($toDeleteIds as $id) { $usageCount = $this->orderShippingRepository->countByCarrierId($id); if ($usageCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_carrier_in_use', [ + throw new ShippingCarrierOperationException('sirsoft-ecommerce::exceptions.shipping_carrier_in_use', [ 'count' => $usageCount, - ])); + ]); } - ShippingCarrier::destroy($id); + $this->repository->delete($id); } // 생성/수정 @@ -209,10 +213,10 @@ class ShippingCarrierService ]; if (! empty($data['id']) && in_array($data['id'], $existingIds)) { - ShippingCarrier::where('id', $data['id'])->update($carrierData); + $this->repository->update((int) $data['id'], $carrierData); } else { $carrierData['created_by'] = Auth::id(); - ShippingCarrier::create($carrierData); + $this->repository->create($carrierData); } } }); @@ -221,8 +225,9 @@ class ShippingCarrierService /** * 배송사 삭제 * - * @param int $id 배송사 ID + * @param int $id 배송사 ID * @return array 삭제 결과 정보 + * * @throws \Exception */ public function deleteCarrier(int $id): array @@ -230,16 +235,16 @@ class ShippingCarrierService $carrier = $this->repository->findById($id); if (! $carrier) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_carrier_not_found')); + throw new ShippingCarrierOperationException('sirsoft-ecommerce::exceptions.shipping_carrier_not_found'); } // 주문에서 사용 중인지 확인 $usageCount = $this->orderShippingRepository->countByCarrierId($id); if ($usageCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_carrier_in_use', [ + throw new ShippingCarrierOperationException('sirsoft-ecommerce::exceptions.shipping_carrier_in_use', [ 'count' => $usageCount, - ])); + ]); } HookManager::doAction('sirsoft-ecommerce.shipping_carrier.before_delete', $carrier); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyResolver.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyResolver.php index 0f2b2991..72fee26b 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyResolver.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyResolver.php @@ -44,6 +44,18 @@ class ShippingPolicyResolver return $this->defaultPolicyCache; } + /** + * 캐시된 기본 배송정책을 비웁니다. + * + * 이 해석기는 싱글톤이라 요청 하나가 배송정책을 바꾼 뒤에도 같은 요청 안에서는 + * 변경 전 정책을 계속 돌려준다. 배송정책을 바꾸는 모든 지점(생성/수정/삭제/기본지정/ + * 사용여부)이 이 메서드를 거쳐 캐시를 비운다. + */ + public function flushCache(): void + { + $this->defaultPolicyCache = false; + } + /** * 상품에 실제로 적용될 배송정책을 해석합니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyService.php index eaf745d0..e6d700cd 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingPolicyService.php @@ -9,12 +9,15 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingPolicyRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Traits\ReappliesPermissionScope; /** * 배송정책 서비스 */ class ShippingPolicyService { + use ReappliesPermissionScope; + public function __construct( protected ShippingPolicyRepositoryInterface $repository ) {} @@ -118,6 +121,8 @@ class ShippingPolicyService */ public function update(ShippingPolicy $shippingPolicy, array $data): ShippingPolicy { + $this->assertWithinScope($shippingPolicy, 'sirsoft-ecommerce.shipping-policies.update'); + // 수정 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_update', $shippingPolicy, $data); @@ -239,6 +244,9 @@ class ShippingPolicyService */ public function delete(ShippingPolicy $shippingPolicy): bool { + // 라우트가 강제하는 권한(SSoT)과 같은 식별자를 쓴다 — 삭제 경로는 `.delete` 다. + $this->assertWithinScope($shippingPolicy, 'sirsoft-ecommerce.shipping-policies.delete'); + // 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_delete', $shippingPolicy); @@ -264,6 +272,8 @@ class ShippingPolicyService */ public function toggleActive(ShippingPolicy $shippingPolicy): ShippingPolicy { + $this->assertWithinScope($shippingPolicy, 'sirsoft-ecommerce.shipping-policies.update'); + // 토글 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_toggle_active', $shippingPolicy); @@ -283,8 +293,14 @@ class ShippingPolicyService */ public function bulkDelete(array $ids): int { + // 스코프 검사와 스냅샷이 같은 조회를 공유한다 — 두 번 부르면 쿼리가 늘고, + // 그 사이에 대상이 바뀌면 검사한 것과 기록하는 것이 달라진다. + $targets = $this->repository->findByIdsKeyed($ids); + + $this->assertAllWithinScope($targets, 'sirsoft-ecommerce.shipping-policies.delete'); + // 삭제 전 스냅샷 캡처 (after_bulk_delete 훅에 전달) - $snapshots = $this->repository->findByIdsKeyed($ids)->map->toArray()->all(); + $snapshots = $targets->map->toArray()->all(); // 일괄 삭제 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_bulk_delete', $ids); @@ -309,8 +325,13 @@ class ShippingPolicyService */ public function bulkToggleActive(array $ids, bool $isActive): int { + // 스코프 검사와 스냅샷이 같은 조회를 공유한다(위 bulkDelete 와 동일 이유). + $targets = $this->repository->findByIdsKeyed($ids); + + $this->assertAllWithinScope($targets, 'sirsoft-ecommerce.shipping-policies.update'); + // 변경 전 스냅샷 캡처 (after_bulk_toggle_active 훅에 전달) - $snapshots = $this->repository->findByIdsKeyed($ids)->map->toArray()->all(); + $snapshots = $targets->map->toArray()->all(); // 일괄 변경 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_bulk_toggle_active', $ids, $isActive); @@ -341,6 +362,8 @@ class ShippingPolicyService */ public function setDefault(ShippingPolicy $shippingPolicy): ShippingPolicy { + $this->assertWithinScope($shippingPolicy, 'sirsoft-ecommerce.shipping-policies.update'); + // 기본값 설정 전 훅 HookManager::doAction('sirsoft-ecommerce.shipping_policy.before_set_default', $shippingPolicy); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingTypeService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingTypeService.php index c409cb05..fbaa3432 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingTypeService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/ShippingTypeService.php @@ -6,6 +6,7 @@ use App\Extension\HookManager; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingTypeOperationException; use Modules\Sirsoft\Ecommerce\Models\ShippingType; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderShippingRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingTypeRepositoryInterface; @@ -16,8 +17,8 @@ use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ShippingTypeRepositoryInter class ShippingTypeService { /** - * @param ShippingTypeRepositoryInterface $repository 배송유형 Repository - * @param OrderShippingRepositoryInterface $orderShippingRepository 주문 배송 Repository + * @param ShippingTypeRepositoryInterface $repository 배송유형 Repository + * @param OrderShippingRepositoryInterface $orderShippingRepository 주문 배송 Repository */ public function __construct( protected ShippingTypeRepositoryInterface $repository, @@ -27,7 +28,7 @@ class ShippingTypeService /** * 배송유형 목록 조회 * - * @param array $filters 필터 조건 + * @param array $filters 필터 조건 * @return Collection */ public function getAllTypes(array $filters = []): Collection @@ -48,7 +49,7 @@ class ShippingTypeService /** * 배송유형 상세 조회 * - * @param int $id 배송유형 ID + * @param int $id 배송유형 ID * @return ShippingType|null */ public function getType(int $id): ?ShippingType @@ -68,7 +69,7 @@ class ShippingTypeService /** * 활성 배송유형 목록 조회 * - * @param string|null $category 카테고리 필터 + * @param string|null $category 카테고리 필터 * @return Collection */ public function getActiveTypes(?string $category = null): Collection @@ -98,7 +99,7 @@ class ShippingTypeService /** * 배송유형 생성 * - * @param array $data 배송유형 데이터 + * @param array $data 배송유형 데이터 * @return ShippingType */ public function createType(array $data): ShippingType @@ -127,17 +128,18 @@ class ShippingTypeService /** * 배송유형 수정 * - * @param int $id 배송유형 ID - * @param array $data 수정할 데이터 + * @param int $id 배송유형 ID + * @param array $data 수정할 데이터 * @return ShippingType - * @throws \Exception + * + * @throws ShippingTypeOperationException 대상 배송유형이 없을 때 */ public function updateType(int $id, array $data): ShippingType { $type = $this->repository->findById($id); if (! $type) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_type_not_found')); + throw new ShippingTypeOperationException('sirsoft-ecommerce::exceptions.shipping_type_not_found'); } HookManager::doAction('sirsoft-ecommerce.shipping_type.before_update', $id, $data); @@ -170,29 +172,30 @@ class ShippingTypeService * - id 없음 → 새 type 생성 * - DB에 있지만 payload에 없음 → 삭제 (주문에서 사용 중이면 예외) * - * @param array $typesData types 배열 + * @param array $typesData types 배열 * @return void - * @throws \Exception 사용 중인 배송유형 삭제 시도 시 + * + * @throws ShippingTypeOperationException 사용 중인 배송유형 삭제 시도 시 */ public function syncShippingTypes(array $typesData): void { DB::transaction(function () use ($typesData) { - $existingIds = ShippingType::pluck('id')->toArray(); + $existingIds = $this->repository->pluckIds(); $incomingIds = array_filter(array_column($typesData, 'id')); // 삭제: DB에 있지만 payload에 없는 항목 $toDeleteIds = array_diff($existingIds, $incomingIds); foreach ($toDeleteIds as $id) { - $type = ShippingType::find($id); + $type = $this->repository->findById((int) $id); if ($type) { $usageCount = $this->orderShippingRepository->countByShippingType($type->code); if ($usageCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_type_in_use', [ + throw new ShippingTypeOperationException('sirsoft-ecommerce::exceptions.shipping_type_in_use', [ 'name' => $type->getLocalizedName(), 'count' => $usageCount, - ])); + ]); } - $type->delete(); + $this->repository->delete((int) $type->id); } } @@ -208,10 +211,10 @@ class ShippingTypeService ]; if (! empty($data['id']) && in_array($data['id'], $existingIds)) { - ShippingType::where('id', $data['id'])->update($typeData); + $this->repository->update((int) $data['id'], $typeData); } else { $typeData['created_by'] = Auth::id(); - ShippingType::create($typeData); + $this->repository->create($typeData); } } }); @@ -223,26 +226,27 @@ class ShippingTypeService /** * 배송유형 삭제 * - * @param int $id 배송유형 ID + * @param int $id 배송유형 ID * @return array 삭제 결과 정보 - * @throws \Exception + * + * @throws ShippingTypeOperationException 대상이 없거나 주문에서 사용 중일 때 */ public function deleteType(int $id): array { $type = $this->repository->findById($id); if (! $type) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_type_not_found')); + throw new ShippingTypeOperationException('sirsoft-ecommerce::exceptions.shipping_type_not_found'); } // 주문에서 사용 중인지 확인 $usageCount = $this->orderShippingRepository->countByShippingType($type->code); if ($usageCount > 0) { - throw new \Exception(__('sirsoft-ecommerce::exceptions.shipping_type_in_use', [ + throw new ShippingTypeOperationException('sirsoft-ecommerce::exceptions.shipping_type_in_use', [ 'name' => $type->getLocalizedName(), 'count' => $usageCount, - ])); + ]); } HookManager::doAction('sirsoft-ecommerce.shipping_type.before_delete', $type); diff --git a/modules/_bundled/sirsoft-ecommerce/src/Services/UserCouponService.php b/modules/_bundled/sirsoft-ecommerce/src/Services/UserCouponService.php index a672d4ea..197ac80e 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/Services/UserCouponService.php +++ b/modules/_bundled/sirsoft-ecommerce/src/Services/UserCouponService.php @@ -6,6 +6,8 @@ use App\Extension\HookManager; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueCondition; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueMethod; use Modules\Sirsoft\Ecommerce\Enums\CouponIssueRecordStatus; use Modules\Sirsoft\Ecommerce\Enums\CouponTargetType; use Modules\Sirsoft\Ecommerce\Exceptions\CouponNotIssuableException; @@ -322,6 +324,10 @@ class UserCouponService throw new CouponNotIssuableException('not_downloadable'); } + // 다운로드 경로 전용 게이트(발급 방법/조건/유효기간) — getDownloadableCoupons 목록 조건과 대칭. + // assertIssuable 은 관리자 직접발급(CouponService::issueDirectly)과 공유하므로 여기에 두지 않는다. + $this->assertDownloadable($coupon); + // 발급 가능 조건 + per_user_limit 검증 (위반 시 사유별 예외) $this->assertIssuable($coupon); $this->assertWithinUserLimit($coupon, $userId); @@ -335,6 +341,32 @@ class UserCouponService }); } + /** + * 쿠폰이 사용자 다운로드 경로로 발급 가능한지 검증합니다. 위반 시 사유별 예외를 던집니다. + * + * 발급 방법(다운로드)·발급 조건(수동)·유효기간(valid_to) 만료 여부를 검사하며, + * `CouponRepository::getDownloadableCoupons` 의 목록 조건과 대칭입니다. 이 게이트가 + * 없으면 자동/관리자 발급 전용 쿠폰이나 만료 쿠폰의 id 를 직접 지정해 다운로드할 수 있습니다. + * + * @param Coupon $coupon 다운로드 대상 쿠폰 + * + * @throws CouponNotIssuableException 다운로드 불가(발급 방법/조건 불일치) 또는 만료 시 + */ + public function assertDownloadable(Coupon $coupon): void + { + if ($coupon->issue_method !== CouponIssueMethod::DOWNLOAD) { + throw new CouponNotIssuableException('not_downloadable'); + } + + if ($coupon->issue_condition !== CouponIssueCondition::MANUAL) { + throw new CouponNotIssuableException('not_downloadable'); + } + + if ($coupon->valid_to !== null && $coupon->valid_to->isPast()) { + throw new CouponNotIssuableException('expired'); + } + } + /** * 쿠폰의 발급 가능 상태(발급중/재고/기간)를 검증합니다. 위반 시 사유별 예외를 던집니다. * diff --git a/modules/_bundled/sirsoft-ecommerce/src/Support/AssetStorage.php b/modules/_bundled/sirsoft-ecommerce/src/Support/AssetStorage.php new file mode 100644 index 00000000..111704c8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Support/AssetStorage.php @@ -0,0 +1,43 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Support; + +use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\ModuleStorageDriver; + +/** + * 자산 행 disk 기준 스토리지 드라이버 팩토리 + * + * 자산 행마다 기록된 disk 가 다를 수 있어(디스크 전환 후 혼재 운용), + * 행 disk 별 ModuleStorageDriver 인스턴스를 memoize 하여 제공합니다. + * 모델 accessor(HasDirectAssetUrl)가 직렬화 루프에서 반복 호출하므로 + * 드라이버 생성 비용을 요청 단위 1회로 고정합니다. + */ +class AssetStorage +{ + /** + * 디스크명 키 드라이버 캐시 + * + * @var array<string, StorageInterface> + */ + private static array $drivers = []; + + /** + * 지정 디스크의 ecommerce 모듈 스토리지 드라이버를 반환합니다. + * + * @param string $disk 디스크 이름 (자산 행의 disk 컬럼 값) + * @return StorageInterface 해당 디스크의 스토리지 드라이버 + */ + public static function forDisk(string $disk): StorageInterface + { + return self::$drivers[$disk] ??= new ModuleStorageDriver('sirsoft-ecommerce', $disk); + } + + /** + * memoize 캐시를 비웁니다 (테스트 격리용). + */ + public static function flush(): void + { + self::$drivers = []; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/Traits/ReappliesPermissionScope.php b/modules/_bundled/sirsoft-ecommerce/src/Traits/ReappliesPermissionScope.php new file mode 100644 index 00000000..b09e7047 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/src/Traits/ReappliesPermissionScope.php @@ -0,0 +1,59 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Traits; + +use App\Helpers\PermissionHelper; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + +/** + * 서비스 계층 스코프 게이트 재적용 + * + * `PermissionMiddleware` 는 라우트 파라미터가 **Model 로 resolve 될 때만** 스코프를 검사하고, + * 아니면 "목록 엔드포인트" 로 보아 건너뛴다. 이 모듈은 두 형태가 모두 존재한다: + * + * 1. **정적 경로** — `bulk-*`, `reorder` 처럼 리소스 파라미터 자체가 없는 라우트 + * 2. **파라미터명 불일치** — 권한은 `resource_route_key` 로 `shippingPolicy`/`coupon` 을 + * 가리키는데 라우트는 `{id}` 로 선언돼 바인딩이 일어나지 않는 경우. 이쪽은 **상세 경로까지** + * 무가드가 된다. + * + * 두 경우 모두 미들웨어가 아무 것도 하지 않으므로 서비스가 같은 판정을 재적용한다. 판정은 + * `PermissionHelper::checkScopeAccess` 단일 SSoT 에 위임한다 — 여기서 재구현하면 스코프 + * 유형 하나가 빠지는 식으로 경로별 강도가 갈린다. + */ +trait ReappliesPermissionScope +{ + /** + * 단일 대상이 액터의 스코프 안에 있는지 검사합니다. + * + * @param Model $model 대상 모델 + * @param string $permission 스코프 대상 권한 식별자 + * + * @throws AccessDeniedHttpException 스코프 밖인 경우 + */ + protected function assertWithinScope(Model $model, string $permission): void + { + if (! PermissionHelper::checkScopeAccess($model, $permission)) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + } + + /** + * 일괄 대상 전체가 액터의 스코프 안에 있는지 검사합니다. + * + * 걸러내지 않고 **전량 거부**한다. 일괄 작업은 호출자가 지정한 집합에 대한 한 번의 + * 의사표시라, 일부만 조용히 빠지면 호출자는 전부 반영됐다고 믿는다(응답의 처리 건수만 + * 다를 뿐 어느 것이 빠졌는지 알 수 없다). + * + * @param iterable<Model> $models 대상 모델들 + * @param string $permission 스코프 대상 권한 식별자 + * + * @throws AccessDeniedHttpException 스코프 밖 대상이 하나라도 있는 경우 + */ + protected function assertAllWithinScope(iterable $models, string $permission): void + { + foreach ($models as $model) { + $this->assertWithinScope($model, $permission); + } + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/en/exceptions.php b/modules/_bundled/sirsoft-ecommerce/src/lang/en/exceptions.php index fa15010d..4e1aa6ba 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/en/exceptions.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/en/exceptions.php @@ -65,6 +65,7 @@ return [ 'order_option_already_cancelled' => 'This order option has already been cancelled.', 'order_option_already_confirmed' => 'This order option has already been confirmed.', 'order_option_cannot_confirm' => 'Cannot confirm purchase in current status.', + 'order_option_not_confirmable' => 'This order option cannot be confirmed in its current status.', 'cancel_quantity_exceeds' => 'Cancel quantity exceeds current quantity (:max).', // Order payment related @@ -85,6 +86,7 @@ return [ 'order_cancel_failed' => 'Failed to cancel the order.', 'order_estimate_refund_failed' => 'Failed to calculate the estimated refund amount.', 'order_create_failed' => 'Failed to create the order.', + 'order_shipping_address_update_failed' => 'Failed to update the shipping address.', // Currency related 'unknown_currency' => 'Unsupported currency: :currency', diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/en/messages.php b/modules/_bundled/sirsoft-ecommerce/src/lang/en/messages.php index f418aeea..e90da989 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/en/messages.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/en/messages.php @@ -488,6 +488,12 @@ return [ 'per_volume' => ':fee per :unit L', 'per_volume_weight' => ':fee per :unit kg', 'per_amount' => ':fee per :unit', + // Tier boundary units for range policies (derived from the charge policy, not stored data) + 'range_unit' => [ + 'quantity' => ' items', + 'weight' => ' kg', + 'volume' => ' L', + ], ], 'fee_details' => [ 'base_fee' => 'Shipping Fee', @@ -1151,6 +1157,14 @@ return [ 'can_write_checked' => 'Review eligibility checked.', 'can_write_check_failed' => 'Failed to check review eligibility.', 'cannot_write' => 'Cannot write a review. (Reason: :reason)', + // 작성 불가 사유 라벨 — :reason 치환자에 원시 식별자 대신 번역문을 싣는다 + 'reasons' => [ + 'order_option_not_found' => 'order item not found', + 'not_own_order' => 'not your order', + 'not_confirmed' => 'purchase not confirmed yet', + 'deadline_passed' => 'review period has ended', + 'already_written' => 'review already written', + ], 'forbidden' => 'You do not have permission.', 'deleted' => 'Review has been deleted.', 'delete_failed' => 'Failed to delete review.', @@ -1211,6 +1225,7 @@ return [ 'board_changed' => 'The board configuration has changed and the post could not be found. Please contact the administrator.', 'reply_created' => 'Reply has been submitted.', 'reply_failed' => 'Failed to submit reply.', + 'reply_already_exists' => 'A reply has already been registered. Please edit or delete the existing reply before submitting a new one.', 'reply_updated' => 'Reply has been updated.', 'reply_update_failed' => 'Failed to update reply.', 'reply_deleted' => 'Reply has been deleted.', diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/en/validation.php b/modules/_bundled/sirsoft-ecommerce/src/lang/en/validation.php index 85c581b2..85571590 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/en/validation.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/en/validation.php @@ -862,6 +862,7 @@ return [ // Order validation messages (backward compatibility - order.* format) 'order' => [ + 'payment_method_unavailable' => 'This payment method is currently unavailable. Please choose another one.', 'ids' => [ 'required' => 'Please select orders to update.', 'array' => 'Order IDs must be an array.', @@ -1033,6 +1034,14 @@ return [ 'tier_min_non_negative' => 'The tier start value must be 0 or greater.', 'tier_max_non_negative' => 'The tier end value must be 0 or greater.', 'unit_value_min' => 'The tier unit value must be greater than 0.', + 'tiers_required' => 'A range-based charge policy requires at least one tier.', + 'middle_max_required' => 'Every tier except the last one must have an end value.', + 'tier_value_integer' => 'Quantity tier start and end values must be integers.', + 'unit_value_required' => 'A per-unit charge policy requires a unit value.', + ], + 'free_threshold_required' => 'A conditional free shipping policy requires a free shipping threshold.', + 'extra_fee' => [ + 'zipcode_format' => 'The postal code must be in one of these formats: "63000", "63000-63999", "63*".', ], 'country_settings' => [ 'required' => 'Please add at least one country shipping setting.', @@ -1319,8 +1328,14 @@ return [ ], ], + // Public asset disk (validated against the core catalog) + 'public_asset_disk_invalid' => 'Please select a valid public asset disk.', + // Field name translations (Laravel standard) 'attributes' => [ + // Public asset storage + 'basic_info.public_asset_disk' => 'Public asset disk', + // Shipping policy country settings 'country_settings' => 'Country settings', 'country_settings.*.country_code' => 'Country', diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/exceptions.php b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/exceptions.php index bb467cd1..08e94a55 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/exceptions.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/exceptions.php @@ -65,6 +65,7 @@ return [ 'order_option_already_cancelled' => '이미 취소된 주문 옵션입니다.', 'order_option_already_confirmed' => '이미 구매확정된 주문 옵션입니다.', 'order_option_cannot_confirm' => '현재 상태에서는 구매확정할 수 없습니다.', + 'order_option_not_confirmable' => '현재 상태에서는 구매확정할 수 없는 주문 옵션입니다.', 'cancel_quantity_exceeds' => '취소 수량이 현재 수량(:max개)을 초과합니다.', // 주문 결제 관련 @@ -85,6 +86,7 @@ return [ 'order_cancel_failed' => '주문 취소 처리에 실패했습니다.', 'order_estimate_refund_failed' => '환불 예상금액 계산에 실패했습니다.', 'order_create_failed' => '주문 생성에 실패했습니다.', + 'order_shipping_address_update_failed' => '배송지 변경 처리 중 오류가 발생했습니다.', // 통화 관련 'unknown_currency' => '지원하지 않는 통화입니다: :currency', diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/messages.php b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/messages.php index 27730908..362a029f 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/messages.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/messages.php @@ -492,6 +492,12 @@ return [ 'per_volume' => ':unitL당 :fee', 'per_volume_weight' => ':unitkg당 :fee', 'per_amount' => ':unit당 :fee', + // 구간별 정책의 경계값 단위 (부과정책에서 파생 — 저장 데이터에 의존하지 않음) + 'range_unit' => [ + 'quantity' => '개', + 'weight' => 'kg', + 'volume' => 'L', + ], ], 'fee_details' => [ 'base_fee' => '배송비', @@ -1140,6 +1146,14 @@ return [ 'can_write_checked' => '리뷰 작성 가능 여부를 확인했습니다.', 'can_write_check_failed' => '리뷰 작성 가능 여부 확인에 실패했습니다.', 'cannot_write' => '리뷰를 작성할 수 없습니다. (사유: :reason)', + // 작성 불가 사유 라벨 — :reason 치환자에 원시 식별자 대신 번역문을 싣는다 + 'reasons' => [ + 'order_option_not_found' => '주문 내역을 찾을 수 없음', + 'not_own_order' => '본인 주문이 아님', + 'not_confirmed' => '구매확정 전', + 'deadline_passed' => '작성 기한 경과', + 'already_written' => '이미 작성한 리뷰', + ], 'forbidden' => '권한이 없습니다.', 'deleted' => '리뷰가 삭제되었습니다.', 'delete_failed' => '리뷰 삭제에 실패했습니다.', @@ -1200,6 +1214,7 @@ return [ 'board_changed' => '게시판 설정이 변경되어 해당 게시글을 찾을 수 없습니다. 관리자에게 문의하세요.', 'reply_created' => '답변이 등록되었습니다.', 'reply_failed' => '답변 등록에 실패했습니다.', + 'reply_already_exists' => '이미 등록된 답변이 있습니다. 기존 답변을 수정하거나 삭제한 후 다시 등록해주세요.', 'reply_updated' => '답변이 수정되었습니다.', 'reply_update_failed' => '답변 수정에 실패했습니다.', 'reply_deleted' => '답변이 삭제되었습니다.', diff --git a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/validation.php b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/validation.php index f15a1c7e..e18042c8 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/lang/ko/validation.php +++ b/modules/_bundled/sirsoft-ecommerce/src/lang/ko/validation.php @@ -862,6 +862,7 @@ return [ // 주문 검증 메시지 (하위 호환성 - order.* 형식) 'order' => [ + 'payment_method_unavailable' => '현재 사용할 수 없는 결제수단입니다. 다른 결제수단을 선택해주세요.', 'ids' => [ 'required' => '변경할 주문을 선택해주세요.', 'array' => '주문 ID는 배열 형태여야 합니다.', @@ -1033,6 +1034,14 @@ return [ 'tier_min_non_negative' => '구간 시작값은 0 이상이어야 합니다.', 'tier_max_non_negative' => '구간 종료값은 0 이상이어야 합니다.', 'unit_value_min' => '구간 단위값은 0보다 커야 합니다.', + 'tiers_required' => '구간별 배송비 정책은 구간을 1개 이상 등록해야 합니다.', + 'middle_max_required' => '마지막 구간을 제외한 구간에는 종료값을 입력해야 합니다.', + 'tier_value_integer' => '수량 구간의 시작값과 종료값은 정수여야 합니다.', + 'unit_value_required' => '단위당 배송비 정책은 단위값을 입력해야 합니다.', + ], + 'free_threshold_required' => '조건부 무료배송 정책은 무료배송 기준금액을 입력해야 합니다.', + 'extra_fee' => [ + 'zipcode_format' => '우편번호는 "63000", "63000-63999", "63*" 형식 중 하나여야 합니다.', ], 'country_settings' => [ 'required' => '국가별 배송 설정을 1개 이상 추가해주세요.', @@ -1319,8 +1328,14 @@ return [ ], ], + // 공개 자산 디스크 (코어 카탈로그 조회 검증) + 'public_asset_disk_invalid' => '올바른 공개 자산 디스크를 선택해주세요.', + // 필드명 한국어 변환 (Laravel 표준) 'attributes' => [ + // 공개 자산 스토리지 + 'basic_info.public_asset_disk' => '공개 자산 디스크', + // 배송정책 국가별 설정 'country_settings' => '국가별 설정', 'country_settings.*.country_code' => '국가', diff --git a/modules/_bundled/sirsoft-ecommerce/src/routes/api.php b/modules/_bundled/sirsoft-ecommerce/src/routes/api.php index fdd08e15..bc3d508d 100644 --- a/modules/_bundled/sirsoft-ecommerce/src/routes/api.php +++ b/modules/_bundled/sirsoft-ecommerce/src/routes/api.php @@ -261,7 +261,8 @@ Route::post('user/orders', [PublicOrderController::class, 'store']) // 비회원 주문 조회 인증 API (주문번호+전화번호+비밀번호 → 조회 토큰 발급) // POST /api/modules/sirsoft-ecommerce/guest/orders/verify -// 실패 잠금은 GuestOrderAuthService 가 담당하고, throttle 로 요청 빈도도 제한한다. +// 무차별 대입 방어는 라우트 throttle(분당 20회, IP 기준) + 조회 비밀번호 정책(영문+숫자 8자 이상)이 +// 담당한다. 업계 표준·로그인/IDV 일관성을 고려해 별도 N회 잠금은 두지 않는다(#55 결정). Route::post('guest/orders/verify', [PublicOrderController::class, 'verify']) ->middleware('throttle:20,1') ->name('guest.orders.verify'); @@ -1136,16 +1137,20 @@ Route::prefix('admin')->middleware(['auth:sanctum', 'admin'])->group(function () // DELETE /api/modules/sirsoft-ecommerce/admin/presets/{preset} - 프리셋 삭제 Route::prefix('presets')->group(function () { Route::get('/', [SearchPresetController::class, 'index']) + ->middleware('permission:admin,sirsoft-ecommerce.products.read') ->name('admin.presets.index'); Route::post('/', [SearchPresetController::class, 'store']) + ->middleware('permission:admin,sirsoft-ecommerce.products.update') ->name('admin.presets.store'); Route::put('/{preset}', [SearchPresetController::class, 'update']) + ->middleware('permission:admin,sirsoft-ecommerce.products.update') ->whereNumber('preset') ->name('admin.presets.update'); Route::delete('/{preset}', [SearchPresetController::class, 'destroy']) + ->middleware('permission:admin,sirsoft-ecommerce.products.update') ->whereNumber('preset') ->name('admin.presets.destroy'); }); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Concerns/RegistersTestCashReceiptProvider.php b/modules/_bundled/sirsoft-ecommerce/tests/Concerns/RegistersTestCashReceiptProvider.php new file mode 100644 index 00000000..772025a5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Concerns/RegistersTestCashReceiptProvider.php @@ -0,0 +1,37 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Concerns; + +use App\Extension\HookManager; + +/** + * 테스트용 현금영수증 발급 프로바이더 등록 헬퍼 + * + * 현금영수증 프로바이더 선택값은 저장만으로 유효해지지 않는다 — 그 프로바이더를 실제로 + * 제공하는 확장이 `sirsoft-ecommerce.cash_receipt.registered_providers` 훅에 자신을 + * 등록해야 한다(A3). 그래서 설정값만 넣고 확장을 등록하지 않으면 "제공자 없음" 상태가 되며, + * 이는 운영에서 플러그인을 제거한 상태와 같다. + * + * 프로바이더가 살아 있는 상황을 검증하려는 테스트는 이 트레이트를 사용해 레지스트리에도 + * 등록한다. 등록은 `HookManager` 정적 상태에 남지만 `ModuleTestCase` 가 테스트마다 + * 스냅샷/복원하므로 누수되지 않는다. + */ +trait RegistersTestCashReceiptProvider +{ + /** + * 현금영수증 발급 프로바이더를 레지스트리에 등록합니다. + * + * @param string $providerId 프로바이더 ID + */ + protected function registerCashReceiptProvider(string $providerId): void + { + HookManager::addFilter( + 'sirsoft-ecommerce.cash_receipt.registered_providers', + function (array $providers) use ($providerId) { + $providers[] = ['id' => $providerId, 'name' => strtoupper($providerId)]; + + return $providers; + } + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php index c0d15d17..fc6acb3b 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php @@ -334,4 +334,228 @@ class ShippingPolicyValidationMessageTest extends ModuleTestCase $response->assertStatus(422); } + + // ───────── #94: 구간 필수값·경계 규칙 ───────── + + #[Test] + public function test_range_policy_without_tiers_is_rejected(): void + { + // 구간이 없으면 모든 주문이 조용히 0원(무료배송)이 된다 + $cs = $this->krSetting([ + 'charge_policy' => 'range_quantity', + 'base_fee' => 0, + 'ranges' => ['type' => 'quantity', 'tiers' => []], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + $this->assertStringContainsString('구간을 1개 이상', implode(' ', $this->allMessages($response))); + } + + #[Test] + public function test_per_unit_policy_without_unit_value_is_rejected(): void + { + $cs = $this->krSetting([ + 'charge_policy' => 'per_weight', + 'base_fee' => 1000, + 'ranges' => ['type' => 'per_weight'], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + $this->assertStringContainsString('단위값', implode(' ', $this->allMessages($response))); + } + + #[Test] + public function test_conditional_free_without_threshold_is_rejected(): void + { + $cs = $this->krSetting([ + 'charge_policy' => 'conditional_free', + 'base_fee' => 3000, + 'free_threshold' => null, + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + $this->assertStringContainsString('무료배송 기준금액', implode(' ', $this->allMessages($response))); + } + + #[Test] + public function test_api_policy_with_zero_base_fee_is_rejected(): void + { + // API 폴백 배송비가 0 이면 외부 API 장애가 곧 무음 무료배송이 된다 + $cs = $this->krSetting([ + 'charge_policy' => 'api', + 'base_fee' => 0, + 'api_endpoint' => 'https://example.com/calc', + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + } + + #[Test] + public function test_middle_tier_without_max_is_rejected(): void + { + // 중간 구간의 종료값이 비면 뒤 구간이 영구히 도달 불가해진다 + $cs = $this->krSetting([ + 'charge_policy' => 'range_weight', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'weight', + 'tiers' => [ + ['min' => 0, 'max' => null, 'fee' => 3000], + ['min' => 2, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + $this->assertStringContainsString('종료값을 입력', implode(' ', $this->allMessages($response))); + } + + #[Test] + public function test_continuous_range_accepts_next_min_equal_to_previous_max(): void + { + $cs = $this->krSetting([ + 'charge_policy' => 'range_weight', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'weight', + 'tiers' => [ + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 2, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $this->store($this->payload([$cs]))->assertStatus(201); + } + + #[Test] + public function test_continuous_range_rejects_legacy_max_plus_one_gap(): void + { + // 구 규칙(max + 1)은 2 초과 3 미만 구간에 간극을 만든다 + $cs = $this->krSetting([ + 'charge_policy' => 'range_weight', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'weight', + 'tiers' => [ + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 3, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertStringContainsString('연속', implode(' ', $this->allMessages($response))); + } + + #[Test] + public function test_continuous_range_accepts_decimal_boundary(): void + { + // 소수 구간(2.5kg)은 정상 설정이다 — 구 max+1 규칙에서는 저장 자체가 불가했다 + $cs = $this->krSetting([ + 'charge_policy' => 'range_weight', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'weight', + 'tiers' => [ + ['min' => 0, 'max' => 2.5, 'fee' => 3000], + ['min' => 2.5, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $this->store($this->payload([$cs]))->assertStatus(201); + } + + #[Test] + public function test_discrete_quantity_range_accepts_max_plus_one(): void + { + // 수량은 이산형이라 "5개 다음은 6개"가 맞다 (공개 #94 본문 설정) + $cs = $this->krSetting([ + 'charge_policy' => 'range_quantity', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'quantity', + 'tiers' => [ + ['min' => 0, 'max' => 5, 'fee' => 3000], + ['min' => 6, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $this->store($this->payload([$cs]))->assertStatus(201); + } + + #[Test] + public function test_discrete_quantity_range_rejects_fractional_boundary(): void + { + $cs = $this->krSetting([ + 'charge_policy' => 'range_quantity', + 'base_fee' => 0, + 'ranges' => [ + 'type' => 'quantity', + 'tiers' => [ + ['min' => 0, 'max' => 5.5, 'fee' => 3000], + ['min' => 6.5, 'max' => null, 'fee' => 5000], + ], + ], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertStringContainsString('정수', implode(' ', $this->allMessages($response))); + } + + // ───────── #94: 도서산간 우편번호 형식 ───────── + + #[Test] + public function test_extra_fee_accepts_three_supported_zipcode_formats(): void + { + $cs = $this->krSetting([ + 'extra_fee_enabled' => true, + 'extra_fee_settings' => [ + ['zipcode' => '63000', 'fee' => 3000, 'region' => '정확일치'], + ['zipcode' => '63000-63644', 'fee' => 3000, 'region' => '범위'], + ['zipcode' => '63*', 'fee' => 3000, 'region' => '접두 와일드카드'], + ], + ]); + + $this->store($this->payload([$cs]))->assertStatus(201); + } + + #[Test] + public function test_extra_fee_rejects_unsupported_zipcode_format(): void + { + // 어떤 우편번호에도 매칭되지 않아 추가배송비가 조용히 누락된다 + $cs = $this->krSetting([ + 'extra_fee_enabled' => true, + 'extra_fee_settings' => [ + ['zipcode' => '제주 63*', 'fee' => 3000, 'region' => '오형식'], + ], + ]); + + $response = $this->store($this->payload([$cs])); + + $response->assertStatus(422); + $this->assertNoRawKeys($response); + $this->assertStringContainsString('우편번호', implode(' ', $this->allMessages($response))); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneExpiredTempOrdersCommandTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneExpiredTempOrdersCommandTest.php new file mode 100644 index 00000000..30ebc297 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneExpiredTempOrdersCommandTest.php @@ -0,0 +1,60 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Console; + +use Illuminate\Support\Carbon; +use Modules\Sirsoft\Ecommerce\Models\TempOrder; +use Modules\Sirsoft\Ecommerce\Module; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 만료 임시 주문 자동 정리 커맨드 테스트 (공개이슈 #110 전수 해소분, B5) + * + * 임시 주문은 체크아웃 진입마다 생성되고 TTL 로 만료되지만, 이 커맨드가 없으면 + * 만료 행을 실제로 지우는 주체가 없어 무한히 누적됩니다. + * + * @scenario target=ecommerce_temp_orders, row_age=beyond_retention + * + * @effects rows_beyond_retention_are_deleted, rows_within_retention_are_kept, all_gc_commands_are_registered_in_schedule + */ +class PruneExpiredTempOrdersCommandTest extends ModuleTestCase +{ + /** + * 만료된 임시 주문만 삭제되고 유효한 임시 주문은 보존된다. + */ + public function test_prunes_only_expired_temp_orders(): void + { + $expired = TempOrder::create([ + 'cart_key' => 'expired-key', + 'items' => [], + 'calculation_input' => [], + 'calculation_result' => [], + 'expires_at' => Carbon::now()->subMinutes(10), + ]); + + $alive = TempOrder::create([ + 'cart_key' => 'alive-key', + 'items' => [], + 'calculation_input' => [], + 'calculation_result' => [], + 'expires_at' => Carbon::now()->addMinutes(10), + ]); + + $this->artisan('sirsoft-ecommerce:prune-temp-orders')->assertSuccessful(); + + $this->assertDatabaseMissing('ecommerce_temp_orders', ['id' => $expired->id]); + $this->assertDatabaseHas('ecommerce_temp_orders', ['id' => $alive->id]); + } + + /** + * 모듈이 이 커맨드를 스케줄로 등록한다 (등록 누락 회귀 차단). + */ + public function test_command_is_registered_in_module_schedules(): void + { + $entry = collect((new Module)->getSchedules()) + ->firstWhere('command', 'sirsoft-ecommerce:prune-temp-orders'); + + $this->assertNotNull($entry, '만료 임시 주문 정리 스케줄이 등록되어야 합니다.'); + $this->assertSame('hourly', $entry['schedule']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneTempProductImagesCommandTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneTempProductImagesCommandTest.php new file mode 100644 index 00000000..43adc641 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneTempProductImagesCommandTest.php @@ -0,0 +1,186 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Console; + +use App\Extension\Storage\ModuleStorageDriver; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductImage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 미연결 임시 상품 이미지 정리 커맨드 테스트 + * + * `temp_key` 가 남고 `product_id` 가 비어 있는 행만 대상이어야 합니다 — 연결된 이미지가 + * 섞이면 판매 중인 상품의 이미지가 사라집니다. + */ +class PruneTempProductImagesCommandTest extends ModuleTestCase +{ + /** + * 임시 상품 이미지 파일과 기록을 함께 만듭니다. + * + * @param array<string, mixed> $attributes 덮어쓸 속성 + * @return ProductImage 생성된 이미지 + */ + private function makeImage(array $attributes = []): ProductImage + { + $filename = uniqid('temp-product-').'.png'; + $path = $attributes['path'] ?? "products/temp/tempkey/{$filename}"; + + (new ModuleStorageDriver('sirsoft-ecommerce', 'modules'))->put('images', $path, 'bytes'); + + return ProductImage::create(array_merge([ + 'product_id' => null, + 'temp_key' => 'tempkey', + 'original_filename' => $filename, + 'stored_filename' => $filename, + 'disk' => 'modules', + 'path' => $path, + 'mime_type' => 'image/png', + 'file_size' => 5, + 'collection' => 'main', + 'sort_order' => 1, + ], $attributes)); + } + + /** + * @scenario age=past_retention, image_state=temp_unlinked + * + * @effects ecommerce_temp_prune_deletes_file_and_record + */ + #[Test] + public function stale_temp_images_lose_both_file_and_record(): void + { + $image = $this->makeImage(); + $image->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images')->assertSuccessful(); + + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-ecommerce', 'modules'))->exists('images', $image->path) + ); + $this->assertDatabaseMissing('ecommerce_product_images', ['id' => $image->id]); + } + + /** + * @scenario age=within_retention, image_state=temp_unlinked + * + * @effects ecommerce_temp_prune_keeps_within_retention + */ + #[Test] + public function temp_image_within_retention_is_kept(): void + { + $image = $this->makeImage(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images')->assertSuccessful(); + + $this->assertDatabaseHas('ecommerce_product_images', ['id' => $image->id]); + } + + /** + * @scenario age=past_retention, image_state=linked_to_product + * + * @effects ecommerce_temp_prune_skips_linked + */ + #[Test] + public function image_linked_to_a_product_is_never_pruned(): void + { + $product = Product::factory()->create(); + + $image = $this->makeImage([ + 'product_id' => $product->id, + 'temp_key' => null, + 'path' => "products/{$product->product_code}/".uniqid('linked-').'.png', + ]); + $image->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images')->assertSuccessful(); + + $this->assertDatabaseHas('ecommerce_product_images', ['id' => $image->id]); + } + + /** + * @effects ecommerce_temp_prune_dry_run + */ + #[Test] + public function dry_run_reports_targets_without_deleting(): void + { + $image = $this->makeImage(); + $image->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images --dry-run') + ->expectsOutputToContain('[DRY RUN]') + ->assertSuccessful(); + + $this->assertDatabaseHas('ecommerce_product_images', ['id' => $image->id]); + } + + /** + * @effects ecommerce_temp_prune_days_guard + */ + #[Test] + public function retention_below_one_day_performs_no_cleanup(): void + { + $image = $this->makeImage(); + $image->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images --days=0')->assertSuccessful(); + + $this->assertDatabaseHas('ecommerce_product_images', ['id' => $image->id]); + } + + /** + * 파일을 비운 temp_key 디렉토리는 남기지 않는다 (회귀) + * + * 파일만 지우고 디렉토리를 남기면 상품 등록 폼 세션마다 빈 디렉토리가 쌓여, 정리를 + * 돌려도 저장소 흔적은 계속 늘어난다. + * + * @scenario age=past_retention, image_state=temp_unlinked + * + * @effects ecommerce_temp_prune_removes_empty_directory + */ + #[Test] + public function emptied_temp_directory_is_removed(): void + { + $image = $this->makeImage(['path' => 'products/temp/tempkey-empty-dir/'.uniqid('t-').'.png']); + $image->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images')->assertSuccessful(); + + $storage = new ModuleStorageDriver('sirsoft-ecommerce', 'modules'); + + $this->assertSame( + [], + $storage->files('images', 'products/temp/tempkey-empty-dir'), + '디렉토리 안의 파일은 모두 삭제되어야 한다.' + ); + $this->assertDirectoryDoesNotExist( + rtrim($storage->getBasePath('images'), '/\\').'/products/temp/tempkey-empty-dir', + '비워진 temp 디렉토리는 남기지 않는다.' + ); + } + + /** + * 같은 temp_key 에 처리되지 않은 파일이 남아 있으면 디렉토리를 지우지 않는다. + * + * @scenario age=past_retention, image_state=temp_unlinked + * + * @effects ecommerce_temp_prune_keeps_directory_with_remaining_files + */ + #[Test] + public function temp_directory_with_remaining_files_is_kept(): void + { + $directory = 'products/temp/tempkey-partial'; + + $processed = $this->makeImage(['path' => $directory.'/'.uniqid('done-').'.png']); + $processed->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + // 같은 디렉토리에 이번 회차 대상이 아닌 파일이 남아 있는 상태 + $storage = new ModuleStorageDriver('sirsoft-ecommerce', 'modules'); + $storage->put('images', $directory.'/keep.png', 'bytes'); + + $this->artisan('sirsoft-ecommerce:prune-temp-product-images')->assertSuccessful(); + + $this->assertTrue($storage->exists('images', $directory.'/keep.png')); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Coupon/CouponDownloadGuardTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Coupon/CouponDownloadGuardTest.php new file mode 100644 index 00000000..a5244a14 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Coupon/CouponDownloadGuardTest.php @@ -0,0 +1,149 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Coupon; + +use Carbon\Carbon; +use Modules\Sirsoft\Ecommerce\Enums\CouponDiscountType; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueCondition; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueMethod; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueStatus; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetScope; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetType; +use Modules\Sirsoft\Ecommerce\Models\Coupon; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 쿠폰 다운로드 게이트 회귀 테스트 (④⑤) + * + * downloadCoupon 경로는 getDownloadableCoupons 목록 조건과 대칭인 게이트를 거쳐야 한다. + * 발급 방법(다운로드)/발급 조건(수동)이 아니거나 유효기간(valid_to)이 만료된 쿠폰의 id 를 + * 직접 지정해 다운로드하는 것을 차단하며, 거부 시 발급 카운터가 소모되지 않아야 한다. + */ +class CouponDownloadGuardTest extends ModuleTestCase +{ + /** + * 다운로드 가능 쿠폰 기본형을 생성합니다. + * + * @param array<string, mixed> $overrides 오버라이드 속성 + * @return Coupon + */ + private function makeCoupon(array $overrides = []): Coupon + { + return Coupon::create(array_merge([ + 'name' => ['ko' => '테스트 쿠폰', 'en' => 'Test Coupon'], + 'description' => ['ko' => '설명', 'en' => 'Desc'], + 'target_type' => CouponTargetType::PRODUCT_AMOUNT, + 'discount_type' => CouponDiscountType::FIXED, + 'discount_value' => 1000, + 'min_order_amount' => 10000, + 'target_scope' => CouponTargetScope::ALL, + 'issue_method' => CouponIssueMethod::DOWNLOAD, + 'issue_condition' => CouponIssueCondition::MANUAL, + 'issue_status' => CouponIssueStatus::ISSUING, + 'issue_from' => Carbon::now()->subDay(), + 'issue_to' => Carbon::now()->addMonth(), + 'per_user_limit' => 1, + 'total_quantity' => 100, + 'issued_count' => 0, + 'valid_type' => 'period', + 'valid_from' => Carbon::now()->subDay(), + 'valid_to' => Carbon::now()->addMonth(), + ], $overrides)); + } + + /** + * 자동발급 전용 쿠폰(issue_method=AUTO)은 id 직접 다운로드가 거부되고 카운터가 소모되지 않는다. + * + * @scenario case=coupon_method_gate + * + * @effects non_downloadable_coupon_rejected + */ + public function test_auto_issue_coupon_cannot_be_downloaded_by_id(): void + { + $user = $this->createUser(); + $coupon = $this->makeCoupon(['issue_method' => CouponIssueMethod::AUTO]); + + $response = $this->actingAs($user, 'sanctum') + ->postJson("/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download"); + + $response->assertStatus(400); + + // per_user_limit / issued_count 미소모 + 발급 레코드 미생성 + $this->assertSame(0, $coupon->fresh()->issued_count); + $this->assertDatabaseMissing('ecommerce_promotion_coupon_issues', [ + 'coupon_id' => $coupon->id, + 'user_id' => $user->id, + ]); + } + + /** + * 관리자 직접발급 조건(issue_condition != MANUAL)인 쿠폰도 다운로드가 거부된다. + * + * @scenario case=coupon_condition_gate + * + * @effects non_downloadable_coupon_rejected + */ + public function test_non_manual_condition_coupon_cannot_be_downloaded(): void + { + $user = $this->createUser(); + $coupon = $this->makeCoupon(['issue_condition' => CouponIssueCondition::SIGNUP]); + + $response = $this->actingAs($user, 'sanctum') + ->postJson("/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download"); + + $response->assertStatus(400); + + $this->assertSame(0, $coupon->fresh()->issued_count); + $this->assertDatabaseMissing('ecommerce_promotion_coupon_issues', [ + 'coupon_id' => $coupon->id, + 'user_id' => $user->id, + ]); + } + + /** + * 유효기간(valid_to)이 만료된 쿠폰은 다운로드가 거부되고 카운터가 소모되지 않는다. + * + * @scenario case=coupon_expired_gate + * + * @effects expired_coupon_rejected + */ + public function test_expired_valid_to_coupon_cannot_be_downloaded(): void + { + $user = $this->createUser(); + $coupon = $this->makeCoupon([ + 'valid_from' => Carbon::now()->subMonth(), + 'valid_to' => Carbon::now()->subDay(), // 유효기간 만료 + ]); + + $response = $this->actingAs($user, 'sanctum') + ->postJson("/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download"); + + $response->assertStatus(400); + + // 만료 거부 시에도 발급 카운터 미소모 + $this->assertSame(0, $coupon->fresh()->issued_count); + $this->assertDatabaseMissing('ecommerce_promotion_coupon_issues', [ + 'coupon_id' => $coupon->id, + 'user_id' => $user->id, + ]); + } + + /** + * 정상적인 다운로드 전용 쿠폰은 여전히 다운로드된다 (게이트 과잉 차단 회귀 방지). + * + * @scenario case=coupon_valid_download + * + * @effects valid_manual_download_coupon_succeeds + */ + public function test_valid_downloadable_coupon_still_succeeds(): void + { + $user = $this->createUser(); + $coupon = $this->makeCoupon(); + + $response = $this->actingAs($user, 'sanctum') + ->postJson("/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download"); + + $response->assertStatus(201); + $this->assertSame(1, $coupon->fresh()->issued_count); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/AdminProductInquiryControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/AdminProductInquiryControllerTest.php index f3f1438a..1be374fc 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/AdminProductInquiryControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/AdminProductInquiryControllerTest.php @@ -53,6 +53,7 @@ class AdminProductInquiryControllerTest extends ModuleTestCase 'sirsoft-ecommerce.inquiry.update_reply', 'sirsoft-ecommerce.inquiry.delete_reply', 'sirsoft-ecommerce.inquiry.create', + 'sirsoft-ecommerce.inquiry.count_replies', 'sirsoft-ecommerce.inquiry.get_settings', 'sirsoft-ecommerce.inquiry.store_validation_rules', 'sirsoft-ecommerce.inquiry.update_validation_rules', @@ -85,9 +86,16 @@ class AdminProductInquiryControllerTest extends ModuleTestCase priority: 1 ); + // 잔여 답변 수 훅 모킹 — 기본값 0 (답변 삭제 시 답변완료 해제 경로) + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.count_replies', + fn () => 0, + priority: 1 + ); + $this->inquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->inquiryUser->id, - 'product_id' => $this->product->id, + 'user_id' => $this->inquiryUser->id, + 'product_id' => $this->product->id, 'is_answered' => false, ]); } @@ -111,7 +119,8 @@ class AdminProductInquiryControllerTest extends ModuleTestCase ->deleteJson("{$this->apiBase}/{$this->inquiry->id}"); $response->assertOk(); - $this->assertDatabaseMissing('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); + // SoftDeletes 도입(#107 후속) — 게시판 복원 경로와의 대칭을 위해 피벗은 소프트 삭제된다 + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); } // ======================================== @@ -140,7 +149,7 @@ class AdminProductInquiryControllerTest extends ModuleTestCase $response->assertStatus(201); $this->assertDatabaseHas('ecommerce_product_inquiries', [ - 'id' => $this->inquiry->id, + 'id' => $this->inquiry->id, 'is_answered' => true, ]); } @@ -157,6 +166,75 @@ class AdminProductInquiryControllerTest extends ModuleTestCase $response->assertStatus(422); } + /** + * 단일 답변 정책 — 이미 답변완료인 문의에는 재등록이 거부되고, 거부 시 + * 게시판 create 훅은 호출 자체가 없어야 한다 (거부됐는데 Post 만 생기는 + * 고아 답변 방지). + */ + #[Test] + public function 이미_답변된_문의에_답변_등록_시_422를_반환한다(): void + { + $answeredInquiry = ProductInquiry::factory()->answered()->create([ + 'product_id' => $this->product->id, + ]); + + // setUp 의 기본 모킹을 걷어내고 호출 횟수를 계수하는 훅으로 교체 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.create'); + $createCalls = 0; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.create', + function () use (&$createCalls) { + $createCalls++; + + return ['post_id' => 999, 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post']; + }, + priority: 1 + ); + + $response = $this->actingAs($this->adminUser) + ->postJson( + "{$this->apiBase}/{$answeredInquiry->id}/reply", + ['content' => '중복 답변 등록 시도 내용입니다.'] + ); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-ecommerce::messages.inquiries.reply_already_exists'), + $response->json('message') + ); + $this->assertSame(0, $createCalls, '재등록 거부 시 게시판 create 훅은 호출되지 않아야 합니다.'); + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $answeredInquiry->id, + 'is_answered' => true, + ]); + } + + /** + * 게시판 미설정 가드 — update/delete 경로와 동일한 board_not_configured 422. + * 없으면 훅 무응답이 reply_failed(500)로 위장된다. + */ + #[Test] + public function 문의_게시판_미설정_시_답변_등록은_422를_반환한다(): void + { + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', null); + + $response = $this->actingAs($this->adminUser) + ->postJson( + "{$this->apiBase}/{$this->inquiry->id}/reply", + ['content' => '게시판 미설정 상태의 답변 등록 시도입니다.'] + ); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-ecommerce::messages.inquiries.board_not_configured'), + $response->json('message') + ); + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $this->inquiry->id, + 'is_answered' => false, + ]); + } + // ======================================== // updateReply() — 답변 수정 // ======================================== @@ -165,7 +243,7 @@ class AdminProductInquiryControllerTest extends ModuleTestCase public function 관리자는_답변을_수정할_수_있다(): void { $answeredInquiry = ProductInquiry::factory()->create([ - 'product_id' => $this->product->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -183,7 +261,7 @@ class AdminProductInquiryControllerTest extends ModuleTestCase { $normalUser = $this->createUser(); $answeredInquiry = ProductInquiry::factory()->create([ - 'product_id' => $this->product->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -204,26 +282,56 @@ class AdminProductInquiryControllerTest extends ModuleTestCase public function 관리자는_답변을_삭제할_수_있다(): void { $answeredInquiry = ProductInquiry::factory()->create([ - 'product_id' => $this->product->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); $response = $this->actingAs($this->adminUser) ->deleteJson("{$this->apiBase}/{$answeredInquiry->id}/reply"); + // setUp 의 count_replies 모킹(0)에 따라 잔여 답변이 없으므로 답변완료가 해제된다 $response->assertOk(); $this->assertDatabaseHas('ecommerce_product_inquiries', [ - 'id' => $answeredInquiry->id, + 'id' => $answeredInquiry->id, 'is_answered' => false, ]); } + /** + * 과거 결함(#106)으로 답변이 여러 건 쌓인 문의에서 하나만 삭제한 경우 — + * 잔여 답변이 있으면(count_replies > 0) 답변완료 표기를 유지해야 한다. + */ + #[Test] + public function 잔여_답변이_있으면_답변_삭제_후에도_답변완료가_유지된다(): void + { + $answeredInquiry = ProductInquiry::factory()->answered()->create([ + 'product_id' => $this->product->id, + ]); + + // setUp 의 기본 모킹(0)을 걷어내고 잔여 답변 1건으로 교체 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.count_replies'); + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.count_replies', + fn () => 1, + priority: 1 + ); + + $response = $this->actingAs($this->adminUser) + ->deleteJson("{$this->apiBase}/{$answeredInquiry->id}/reply"); + + $response->assertOk(); + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $answeredInquiry->id, + 'is_answered' => true, + ]); + } + #[Test] public function 비관리자는_답변을_삭제할_수_없다(): void { $normalUser = $this->createUser(); $answeredInquiry = ProductInquiry::factory()->create([ - 'product_id' => $this->product->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponCombinableBooleanTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponCombinableBooleanTest.php new file mode 100644 index 00000000..d598ec24 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponCombinableBooleanTest.php @@ -0,0 +1,210 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Enums\CouponDiscountType; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueCondition; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueMethod; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueStatus; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetScope; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetType; +use Modules\Sirsoft\Ecommerce\Models\Coupon; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 쿠폰 is_combinable boolean 정규화 — 공개 이슈 #97 회귀 + * + * 관리자 쿠폰 폼의 라디오가 문자열 "true"/"false" 를 전송하면 `boolean` 규칙에서 + * 422 로 저장이 실패했다. Store/UpdateCouponRequest 의 prepareForValidation 이 + * 문자열 표기를 boolean 으로 정규화("false" → false 가 핵심 — truthy 오변환 방지) + * 하고, 해석 불가값은 건드리지 않아 boolean 규칙이 422 로 처리함을 고정한다. + */ +class CouponCombinableBooleanTest extends ModuleTestCase +{ + protected User $adminUser; + + protected function setUp(): void + { + parent::setUp(); + + app()->setLocale('ko'); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-ecommerce.promotion-coupon.read', + 'sirsoft-ecommerce.promotion-coupon.create', + 'sirsoft-ecommerce.promotion-coupon.update', + ]); + } + + /** + * 쿠폰 생성 시 필요한 기본 데이터 + * + * @param array $overrides 오버라이드할 속성 + */ + private function validCouponData(array $overrides = []): array + { + return array_merge([ + 'name' => ['ko' => '중복사용 테스트 쿠폰', 'en' => 'Combinable Test Coupon'], + 'description' => ['ko' => '테스트 설명', 'en' => 'Test description'], + 'target_type' => CouponTargetType::PRODUCT_AMOUNT->value, + 'discount_type' => CouponDiscountType::FIXED->value, + 'discount_value' => 1000, + 'min_order_amount' => 5000, + 'issue_method' => CouponIssueMethod::DIRECT->value, + 'issue_condition' => CouponIssueCondition::MANUAL->value, + 'issue_status' => CouponIssueStatus::ISSUING->value, + 'per_user_limit' => 0, + 'valid_type' => 'period', + 'valid_from' => now()->format('Y-m-d'), + 'valid_to' => now()->addMonth()->format('Y-m-d'), + 'is_combinable' => true, + 'target_scope' => CouponTargetScope::ALL->value, + ], $overrides); + } + + /** + * 수용해야 하는 boolean 표기 6종과 기대 저장값 + * + * @return array<string, array{mixed, bool}> [입력값, 기대 boolean] + */ + private function acceptedNotations(): array + { + return [ + "문자열 'true'" => ['true', true], + "문자열 'false'" => ['false', false], + "문자열 '1'" => ['1', true], + "문자열 '0'" => ['0', false], + 'boolean true' => [true, true], + 'boolean false' => [false, false], + ]; + } + + /** + * 생성: boolean 표기 6종 모두 201 + DB boolean 저장 + */ + public function test_store_accepts_boolean_notations_and_persists_boolean(): void + { + foreach ($this->acceptedNotations() as $label => [$input, $expected]) { + $response = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $this->validCouponData([ + 'is_combinable' => $input, + ])); + + $response->assertStatus(201); + + $coupon = Coupon::find($response->json('data.id')); + $this->assertSame( + $expected, + $coupon->is_combinable, + "생성 {$label} 입력이 boolean {$this->boolLabel($expected)} 로 저장되어야 합니다." + ); + } + } + + /** + * 수정: boolean 표기 6종 모두 200 + DB boolean 저장 + */ + public function test_update_accepts_boolean_notations_and_persists_boolean(): void + { + $createResponse = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $this->validCouponData()); + $createResponse->assertStatus(201); + $couponId = $createResponse->json('data.id'); + + foreach ($this->acceptedNotations() as $label => [$input, $expected]) { + $response = $this->actingAs($this->adminUser) + ->putJson("/api/modules/sirsoft-ecommerce/admin/promotion-coupons/{$couponId}", [ + 'is_combinable' => $input, + 'per_user_limit' => 0, + ]); + + $response->assertStatus(200); + $this->assertSame( + $expected, + Coupon::find($couponId)->is_combinable, + "수정 {$label} 입력이 boolean {$this->boolLabel($expected)} 로 저장되어야 합니다." + ); + } + } + + /** + * 생성: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리 + */ + public function test_store_rejects_unparsable_value_with_422(): void + { + $response = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $this->validCouponData([ + 'is_combinable' => 'abc', + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['is_combinable']); + } + + /** + * 수정: 해석 불가값('abc')은 정규화하지 않고 boolean 규칙이 422 처리 + */ + public function test_update_rejects_unparsable_value_with_422(): void + { + $createResponse = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $this->validCouponData()); + $createResponse->assertStatus(201); + $couponId = $createResponse->json('data.id'); + + $response = $this->actingAs($this->adminUser) + ->putJson("/api/modules/sirsoft-ecommerce/admin/promotion-coupons/{$couponId}", [ + 'is_combinable' => 'abc', + 'per_user_limit' => 0, + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['is_combinable']); + + // 실패한 요청이 저장값을 건드리지 않아야 한다 + $this->assertTrue(Coupon::find($couponId)->is_combinable); + } + + /** + * 생성: is_combinable 미전송이면 정규화가 개입하지 않고 그대로 통과 (has 가드) + */ + public function test_store_without_is_combinable_passes(): void + { + $data = $this->validCouponData(); + unset($data['is_combinable']); + + $response = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $data); + + $response->assertStatus(201); + } + + /** + * 수정: is_combinable 미전송이면 기존 저장값이 유지된다 (has 가드) + */ + public function test_update_without_is_combinable_preserves_stored_value(): void + { + $createResponse = $this->actingAs($this->adminUser) + ->postJson('/api/modules/sirsoft-ecommerce/admin/promotion-coupons', $this->validCouponData([ + 'is_combinable' => false, + ])); + $createResponse->assertStatus(201); + $couponId = $createResponse->json('data.id'); + + $response = $this->actingAs($this->adminUser) + ->putJson("/api/modules/sirsoft-ecommerce/admin/promotion-coupons/{$couponId}", [ + 'per_user_limit' => 0, + ]); + + $response->assertStatus(200); + $this->assertFalse(Coupon::find($couponId)->is_combinable); + } + + /** + * 단언 메시지용 boolean 표기 + */ + private function boolLabel(bool $value): string + { + return $value ? 'true' : 'false'; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponDirectIssueTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponDirectIssueTest.php index 042a1d40..e5f4f832 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponDirectIssueTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponDirectIssueTest.php @@ -138,10 +138,20 @@ class CouponDirectIssueTest extends ModuleTestCase /** * #4 코어 회귀: downloadCoupon() 추출 후에도 DL- 코드·동일 동작 + * + * @scenario case=coupon_valid_download + * + * @effects valid_manual_download_coupon_succeeds */ public function test_download_coupon_still_uses_dl_prefix(): void { - $coupon = $this->createIssuableCoupon(['per_user_limit' => 0]); + // 다운로드 발급은 issue_method=DOWNLOAD + issue_condition=MANUAL 인 쿠폰만 대상이다(④⑤ 게이트). + // 종전 픽스처는 DIRECT(관리자 발급) 쿠폰을 다운로드했는데, 이는 게이트 부재 시절의 동작이었다. + $coupon = $this->createIssuableCoupon([ + 'per_user_limit' => 0, + 'issue_method' => CouponIssueMethod::DOWNLOAD->value, + 'issue_condition' => CouponIssueCondition::MANUAL->value, + ]); $user = User::factory()->create(); $issue = app(UserCouponService::class)->downloadCoupon($user->id, $coupon->id); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCashReceiptTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCashReceiptTest.php index 43680e96..c5894e4b 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCashReceiptTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCashReceiptTest.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; use App\Models\User; use Modules\Sirsoft\Ecommerce\Enums\ShippingFeeTaxPolicy; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; /** @@ -23,6 +24,8 @@ use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; */ class EcommerceSettingsCashReceiptTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private string $apiBase = '/api/modules/sirsoft-ecommerce/admin/settings'; private User $adminUser; @@ -46,6 +49,9 @@ class EcommerceSettingsCashReceiptTest extends ModuleTestCase public function test_현금영수증_프로바이더가_저장된다(): void { + // 저장값 해석은 레지스트리 대조를 거치므로 제공 확장을 함께 등록한다 (A3) + $this->registerCashReceiptProvider('tosspayments'); + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ '_tab' => 'order_settings', 'order_settings' => ['cash_receipt_provider' => 'tosspayments'], @@ -124,4 +130,78 @@ class EcommerceSettingsCashReceiptTest extends ModuleTestCase ->assertOk() ->assertJsonStructure(['data' => ['available_cash_receipt_providers']]); } + + /** + * 저장값이 남아 있어도 그 프로바이더를 제공하는 확장이 없으면 미설정으로 본다. (A3, 실패-먼저) + * + * 플러그인을 제거해도 `order_settings.cash_receipt_provider` 문자열은 그대로 남는다. + * 그 값을 그대로 신뢰하면 체크아웃의 현금영수증 신청 폼과 마이페이지 발급 버튼이 계속 + * 렌더되고, 신청하면 구독자 없는 훅을 호출해 발급 실패로만 기록된다. + * + * @scenario provider_state=dead + * + * @effects dead_cash_receipt_provider_treated_as_unset + */ + public function test_등록되지_않은_프로바이더는_미설정으로_취급된다(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'order_settings', + 'order_settings' => ['cash_receipt_provider' => 'ghost_provider'], + ])->assertOk(); + + // 저장값 자체는 남는다 (관리자가 확인하고 고칠 수 있어야 하므로) + $this->assertSame('ghost_provider', $this->settings()->getSetting('order_settings.cash_receipt_provider')); + + // 해석 결과는 미설정 + $this->assertNull( + $this->settings()->getCashReceiptProvider(), + '제공 확장이 없는 프로바이더가 유효한 것으로 해석되었습니다.' + ); + } + + /** + * 공개 결제 설정에서도 죽은 프로바이더가 미설정으로 정규화된다. (실패-먼저) + * + * 체크아웃 신청 폼은 카테고리 raw 값을 그대로 읽으므로, 공개 응답에서 정규화하지 않으면 + * 폼이 계속 렌더된다. + * + * @scenario provider_state=dead + * + * @effects dead_cash_receipt_provider_normalized_in_public + */ + public function test_공개_결제설정에서_죽은_프로바이더가_정규화된다(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'order_settings', + 'order_settings' => ['cash_receipt_provider' => 'ghost_provider'], + ])->assertOk(); + + $public = $this->settings()->getPublicPaymentSettings(); + + $this->assertArrayHasKey('cash_receipt_provider', $public); + $this->assertNull( + $public['cash_receipt_provider'], + '죽은 현금영수증 프로바이더가 공개 응답에 그대로 노출되었습니다.' + ); + } + + /** + * 등록된 프로바이더는 그대로 해석된다. (비회귀 pin) + * + * @scenario provider_state=live + * + * @effects live_cash_receipt_provider_resolved + */ + public function test_등록된_프로바이더는_그대로_해석된다(): void + { + $this->registerCashReceiptProvider('tosspayments'); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'order_settings', + 'order_settings' => ['cash_receipt_provider' => 'tosspayments'], + ])->assertOk(); + + $this->assertSame('tosspayments', $this->settings()->getCashReceiptProvider()); + $this->assertSame('tosspayments', $this->settings()->getPublicPaymentSettings()['cash_receipt_provider'] ?? null); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCurrencyDeletionTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCurrencyDeletionTest.php new file mode 100644 index 00000000..7efc6d91 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCurrencyDeletionTest.php @@ -0,0 +1,81 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 통화 삭제 저장 왕복 테스트 (공개 #91) + * + * 제보된 증상은 "저장 응답에 이미 삭제한 통화가 되살아나 있다" 였다. 서비스 단위 테스트만으로는 + * 저장 응답이 재조회 결과를 그대로 싣는 이 층을 잡지 못하므로, PUT 응답과 재-GET 양쪽을 검증한다. + */ +class EcommerceSettingsCurrencyDeletionTest extends ModuleTestCase +{ + private string $apiBase = '/api/modules/sirsoft-ecommerce/admin/settings'; + + private User $adminUser; + + protected function setUp(): void + { + parent::setUp(); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-ecommerce.settings.read', + 'sirsoft-ecommerce.settings.update', + ]); + } + + /** + * 응답 payload 에서 통화 코드 목록을 추출합니다. + * + * @param array|null $currencies 통화 배열 + * @return array<int, string> 통화 코드 목록 + */ + private function codes(?array $currencies): array + { + return array_values(array_map(fn ($c) => $c['code'] ?? null, $currencies ?? [])); + } + + /** + * 기본 제공 통화를 삭제해 저장하면 저장 응답과 재조회 양쪽에서 사라진다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects intentionally_removed_default_currency_stays_deleted + */ + public function test_put_response_reflects_currency_deletion(): void + { + $response = $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'language_currency', + 'language_currency' => [ + 'default_currency' => 'KRW', + 'currencies' => [ + [ + 'code' => 'KRW', + 'name' => ['ko' => 'KRW (원)', 'en' => 'KRW (Won)'], + 'exchange_rate' => null, + 'base_unit' => 1000, + 'rounding_unit' => '1', + 'rounding_method' => 'floor', + 'decimal_places' => 0, + 'is_default' => true, + ], + ], + ], + ]); + + $response->assertOk(); + + // 저장 응답 자체가 삭제를 반영해야 한다 (제보된 증상: 응답에 5종 부활) + $saved = $this->codes($response->json('data.language_currency.currencies')); + $this->assertSame(['KRW'], $saved, '저장 응답에 삭제한 통화가 되살아났습니다.'); + + // 재조회에서도 유지 (새로고침 시 부활 여부) + $fetched = $this->actingAs($this->adminUser)->getJson($this->apiBase); + $fetched->assertOk(); + + $this->assertSame(['KRW'], $this->codes($fetched->json('data.language_currency.currencies')), '재조회에서 삭제한 통화가 되살아났습니다.'); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsInUseDeletionTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsInUseDeletionTest.php new file mode 100644 index 00000000..0aaaf84c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsInUseDeletionTest.php @@ -0,0 +1,215 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Models\User; +use Mockery; +use Modules\Sirsoft\Ecommerce\Models\ShippingCarrier; +use Modules\Sirsoft\Ecommerce\Models\ShippingType; +use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderShippingRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 사용 중인 배송유형·택배사 삭제 시도의 응답 계약 + * + * 관리자 화면은 배송설정 저장 payload 에서 항목을 빼는 방식으로 삭제한다. 그 항목이 + * 주문에서 사용 중이면 서비스가 도메인 실패를 던지는데, 컨트롤러가 그것을 generic + * catch 로 받아 500 "설정 저장 중 오류" 로 뭉개면 운영자는 **왜** 못 지우는지 알 수 없다. + * 인프라 장애와 구분되지 않으므로 조치 방법도 없다. + * + * 도메인 실패는 사유가 담긴 400 으로, 그 외 예외만 500 으로 나가야 한다. + */ +class EcommerceSettingsInUseDeletionTest extends ModuleTestCase +{ + private string $apiBase = '/api/modules/sirsoft-ecommerce/admin/settings'; + + private User $adminUser; + + protected function setUp(): void + { + parent::setUp(); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-ecommerce.settings.read', + 'sirsoft-ecommerce.settings.update', + ]); + } + + protected function tearDown(): void + { + Mockery::close(); + + parent::tearDown(); + } + + /** + * 주문에서 사용 중인 것으로 보이도록 주문배송 Repository 를 대체합니다. + * + * 실제 주문 그래프를 세우지 않고 "사용 중" 조건만 만든다 — 이 테스트가 고정하려는 + * 것은 사용량 집계가 아니라 그 결과가 어떤 응답이 되는가이기 때문이다. + * + * @param int $count 사용 건수 + */ + private function pretendInUse(int $count): void + { + $repository = Mockery::mock(OrderShippingRepositoryInterface::class); + $repository->shouldReceive('countByShippingType')->andReturn($count); + $repository->shouldReceive('countByCarrierId')->andReturn($count); + + $this->app->instance(OrderShippingRepositoryInterface::class, $repository); + } + + /** + * 사용 중인 배송유형을 payload 에서 빼 저장하면 사유가 담긴 400 이 돌아온다. + * + * @scenario resource=shipping_type, error_class=domain + * + * @effects in_use_shipping_type_deletion_returns_400_with_reason + */ + public function test_deleting_in_use_shipping_type_returns_400_with_reason(): void + { + $type = ShippingType::create([ + 'code' => 'inuse_type', + 'name' => ['ko' => '사용중배송유형', 'en' => 'In-use type'], + 'category' => 'delivery', + 'is_active' => true, + 'sort_order' => 0, + ]); + + $this->pretendInUse(3); + + // payload 에서 이 유형을 빼면 syncShippingTypes 가 삭제 대상으로 판정한다. + $response = $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'shipping', + 'shipping' => [ + 'types' => [], + ], + ]); + + $response->assertStatus(400); + $response->assertJsonPath( + 'message', + __('sirsoft-ecommerce::exceptions.shipping_type_in_use', [ + 'name' => $type->getLocalizedName(), + 'count' => 3, + ]) + ); + + // 사유가 사라지지 않았는지 — 일반 저장 실패 문구로 대체되면 안 된다. + $this->assertNotSame( + __('sirsoft-ecommerce::messages.settings.save_error'), + $response->json('message') + ); + + // 삭제는 롤백되어야 한다. + $this->assertDatabaseHas($type->getTable(), ['id' => $type->id]); + } + + /** + * 사용 중인 택배사도 같은 계약을 따른다 (같은 저장 경로의 형제 결함). + * + * @scenario resource=shipping_carrier, error_class=domain + * + * @effects in_use_shipping_carrier_deletion_returns_400_with_reason + */ + public function test_deleting_in_use_shipping_carrier_returns_400_with_reason(): void + { + $carrier = ShippingCarrier::create([ + 'code' => 'inuse_carrier', + 'name' => ['ko' => '사용중택배사', 'en' => 'In-use carrier'], + 'type' => 'domestic', + 'is_active' => true, + 'sort_order' => 0, + ]); + + $this->pretendInUse(2); + + $response = $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'shipping', + 'shipping' => [ + 'carriers' => [], + ], + ]); + + $response->assertStatus(400); + $response->assertJsonPath( + 'message', + __('sirsoft-ecommerce::exceptions.shipping_carrier_in_use', ['count' => 2]) + ); + $this->assertNotSame( + __('sirsoft-ecommerce::messages.settings.save_error'), + $response->json('message') + ); + + $this->assertDatabaseHas($carrier->getTable(), ['id' => $carrier->id]); + } + + /** + * 도메인 실패가 아닌 예외는 그대로 500 이어야 한다 (구분의 반대편). + * + * @scenario resource=shipping_type, error_class=infrastructure + * + * @effects settings_save_infrastructure_exception_returns_500 + */ + public function test_infrastructure_exception_on_settings_save_returns_500(): void + { + $repository = Mockery::mock(OrderShippingRepositoryInterface::class); + $repository->shouldReceive('countByShippingType') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: connection lost')); + $this->app->instance(OrderShippingRepositoryInterface::class, $repository); + + ShippingType::create([ + 'code' => 'infra_type', + 'name' => ['ko' => '인프라', 'en' => 'Infra'], + 'category' => 'delivery', + 'is_active' => true, + 'sort_order' => 0, + ]); + + $response = $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'shipping', + 'shipping' => [ + 'types' => [], + ], + ]); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', (string) $response->json('message')); + } + + /** + * 택배사 경로의 인프라 예외도 같은 구분을 따른다. + * + * 배송유형과 택배사는 사용량을 각각 `countByShippingType`·`countByCarrierId` 로 세므로 + * 호출 지점이 다르다. 한쪽만 단언하면 나머지 경로의 뭉개기가 그대로 남는다. + * + * @scenario resource=shipping_carrier, error_class=infrastructure + * + * @effects settings_save_carrier_infrastructure_exception_returns_500 + */ + public function test_infrastructure_exception_on_carrier_settings_save_returns_500(): void + { + $repository = Mockery::mock(OrderShippingRepositoryInterface::class); + $repository->shouldReceive('countByCarrierId') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: connection lost')); + $this->app->instance(OrderShippingRepositoryInterface::class, $repository); + + ShippingCarrier::create([ + 'code' => 'infra_carrier', + 'name' => ['ko' => '인프라택배사', 'en' => 'Infra carrier'], + 'type' => 'domestic', + 'is_active' => true, + 'sort_order' => 0, + ]); + + $response = $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'shipping', + 'shipping' => [ + 'carriers' => [], + ], + ]); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', (string) $response->json('message')); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsSeoCacheInvalidationTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsSeoCacheInvalidationTest.php new file mode 100644 index 00000000..b202eb21 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsSeoCacheInvalidationTest.php @@ -0,0 +1,305 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Extension\HookManager; +use App\Listeners\CoreActivityLogListener; +use App\Models\User; +use App\Seo\Contracts\SeoCacheManagerInterface; +use Illuminate\Support\Facades\DB; +use Mockery; +use Modules\Sirsoft\Ecommerce\Http\Controllers\Admin\EcommerceSettingsController; +use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\UpdateSettingRequest; +use Modules\Sirsoft\Ecommerce\Listeners\SeoSettingsCacheListener; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 모듈 설정 저장 시 `core.module_settings.after_save` 발화 (B-5) + * + * 이 훅을 구독하는 리스너가 셋(이커머스 SEO 캐시 / 게시판 SEO 캐시 / 코어 활동로그) 있는데, + * 정작 발화 지점인 `ModuleSettingsService::save()` 는 프로덕션에서 호출되지 않는다. + * 모듈 설정은 각 모듈의 관리자 컨트롤러가 자기 SettingsService 로 직접 저장하기 때문이다. + * 그 결과 모듈 환경설정에서 SEO 메타 템플릿을 바꿔도 SEO 캐시가 무효화되지 않았다. + * + * 발화 지점을 관리자 컨트롤러로 둔다 — 서비스에 두면 테스트 fixture 의 모든 저장이 + * 활동로그·SEO 무효화를 유발하고, 훅 의미도 "관리자가 설정을 저장했다" 이다. + */ +class EcommerceSettingsSeoCacheInvalidationTest extends ModuleTestCase +{ + private string $apiBase = '/api/modules/sirsoft-ecommerce/admin/settings'; + + private User $adminUser; + + /** + * 훅 수신 기록 [[identifier, settings, result], ...] + * + * @var array<int, array> + */ + private array $received = []; + + protected function setUp(): void + { + parent::setUp(); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-ecommerce.settings.read', + 'sirsoft-ecommerce.settings.update', + ]); + + $this->received = []; + HookManager::addAction('core.module_settings.after_save', function (...$args) { + $this->received[] = $args; + }, 5); + } + + /** + * 설정 저장 API 가 모듈 설정 저장 훅을 발화한다. (실패-먼저) + * + * @scenario actor=admin, save_path=bulk + * + * @effects module_settings_after_save_hook_fired + */ + public function test_settings_save_fires_module_settings_after_save_hook(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'seo', + 'seo' => ['meta_product_title' => '{product_name} | shop'], + ])->assertOk(); + + $this->assertCount(1, $this->received, '모듈 설정 저장 훅이 발화되지 않았습니다.'); + [$identifier, $settings, $result] = $this->received[0]; + + $this->assertSame('sirsoft-ecommerce', $identifier); + $this->assertArrayHasKey('seo', $settings, '훅 payload 가 저장된 카테고리를 담지 않았습니다.'); + $this->assertTrue($result); + } + + /** + * 은행 목록 저장도 같은 훅을 발화한다. (실패-먼저) + * + * @scenario actor=admin, save_path=save_banks + * + * @effects module_settings_after_save_hook_fired + */ + public function test_bank_save_fires_module_settings_after_save_hook(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase.'/banks', [ + 'banks' => [['code' => '004', 'name' => ['ko' => '국민은행', 'en' => 'Kookmin Bank']]], + ])->assertOk(); + + $this->assertCount(1, $this->received, '은행 저장이 모듈 설정 저장 훅을 발화하지 않았습니다.'); + [$identifier, $settings] = $this->received[0]; + + $this->assertSame('sirsoft-ecommerce', $identifier); + $this->assertArrayHasKey('order_settings', $settings); + } + + /** + * 저장 실패(검증 탈락) 시에는 훅이 발화되지 않는다. + * + * @scenario actor=admin, save_path=bulk, outcome=validation_failed + * + * @effects module_settings_after_save_hook_not_fired_on_failure + */ + public function test_hook_is_not_fired_when_validation_fails(): void + { + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'order_settings', + 'order_settings' => ['shipping_fee_tax_policy' => 'no_such_policy'], + ])->assertStatus(422); + + $this->assertSame([], $this->received, '검증 실패인데 저장 훅이 발화되었습니다.'); + } + + /** + * 이커머스 SEO 캐시 리스너가 이 훅으로 실제 무효화를 수행한다. + * + * @scenario actor=admin, save_path=bulk, listener=seo_cache + * + * @effects seo_cache_invalidated_on_module_settings_save + */ + public function test_seo_cache_listener_receives_the_hook(): void + { + $listener = new SeoSettingsCacheListener; + $hooks = $listener::getSubscribedHooks(); + + $this->assertArrayHasKey('core.module_settings.after_save', $hooks); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'seo', + 'seo' => ['meta_product_title' => '{product_name}'], + ])->assertOk(); + + $this->assertNotEmpty($this->received); + // 리스너가 SEO 키 존재로 무효화를 판정하므로 payload 에 seo 카테고리가 실려야 한다 + $this->assertArrayHasKey('meta_product_title', $this->received[0][1]['seo'] ?? []); + } + + // ─── 실제 무효화 수행 ────────────────────────────────────── + + /** + * SEO 탭 저장이 상품 상세 레이아웃 캐시를 실제로 지운다. (훅 발화가 아닌 결과 검증) + * + * 훅 발화만 고정하면 리스너가 조용히 아무것도 하지 않게 되어도 green 이다. + * 캐시 매니저를 mock 해 호출 자체를 단언한다. + * + * @scenario actor=admin, save_path=bulk, listener=seo_cache + * + * @effects seo_cache_invalidated_on_module_settings_save + */ + public function test_seo_tab_save_actually_invalidates_layout_cache(): void + { + $cache = Mockery::mock(SeoCacheManagerInterface::class); + $cache->shouldReceive('invalidateByLayout')->with('shop/show')->atLeast()->once(); + $cache->shouldReceive('invalidateByLayout')->andReturnNull(); + $this->app->instance(SeoCacheManagerInterface::class, $cache); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'seo', + 'seo' => ['meta_product_title' => '{product_name} | shop'], + ])->assertOk(); + + $this->addToAssertionCount(1); + } + + /** + * SEO 와 무관한 탭 저장은 SEO 캐시를 건드리지 않는다. + * + * @scenario actor=admin, save_path=bulk, listener=seo_cache, tab=non_seo + * + * @effects seo_cache_untouched_for_non_seo_tab + */ + public function test_non_seo_tab_save_does_not_invalidate_layout_cache(): void + { + $cache = Mockery::mock(SeoCacheManagerInterface::class); + $cache->shouldNotReceive('invalidateByLayout'); + $this->app->instance(SeoCacheManagerInterface::class, $cache); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'order_settings', + 'order_settings' => ['auto_cancel_days' => 5], + ])->assertOk(); + + $this->addToAssertionCount(1); + } + + /** + * 관리자 단건 저장(`updateSetting`) 경로도 같은 훅을 발화한다. + * + * 훅 발화는 서비스가 아니라 관리자 컨트롤러가 한다 — 서비스에 두면 테스트 fixture 나 + * 내부 호출까지 활동로그·SEO 무효화를 유발하기 때문이다. 그래서 이 경로도 컨트롤러가 + * 직접 발화해야 하며, 그것을 여기서 고정한다. + * + * 이 메서드에는 아직 라우트가 없어(설정 화면은 벌크 저장만 사용) HTTP 로 도달할 수 + * 없다. 그래서 컨트롤러 메서드를 직접 호출한다 — 라우트가 붙는 시점에 발화 계약이 + * 이미 고정되어 있게 한다. + * + * @scenario actor=admin, save_path=single_key + * + * @effects module_settings_after_save_hook_fired + */ + public function test_admin_single_key_save_fires_module_settings_after_save_hook(): void + { + $this->actingAs($this->adminUser); + + $request = UpdateSettingRequest::create('/', 'PUT', [ + 'key' => 'seo.meta_product_title', + 'value' => '{product_name}', + ]); + $request->setContainer($this->app)->setRedirector($this->app['redirect']); + $request->validateResolved(); + + app(EcommerceSettingsController::class)->updateSetting($request)->getData(); + + $this->assertCount(1, $this->received, '관리자 단건 저장이 모듈 설정 저장 훅을 발화하지 않았습니다.'); + [$identifier, $settings] = $this->received[0]; + + $this->assertSame('sirsoft-ecommerce', $identifier); + $this->assertArrayHasKey('seo', $settings, '단건 저장 payload 가 벌크와 같은 카테고리 형태가 아닙니다.'); + $this->assertArrayHasKey('meta_product_title', $settings['seo']); + } + + /** + * 서비스 직접 호출은 훅을 발화하지 않는다. (설계 고정) + * + * 발화 지점을 서비스로 내리면 내부 호출·시더·테스트 fixture 의 모든 저장이 활동로그와 + * SEO 무효화를 유발한다. 그래서 서비스는 조용해야 한다 — 이 비발화를 명시 고정한다. + * + * @scenario actor=system, save_path=single_key + * + * @effects service_level_save_stays_silent + */ + public function test_service_level_save_does_not_fire_the_hook(): void + { + app(EcommerceSettingsService::class)->setSetting('seo.meta_product_title', '{product_name}'); + + $this->assertSame([], $this->received, '서비스 직접 저장이 관리자 저장 훅을 발화했습니다.'); + } + + /** + * 서비스의 벌크 저장도 훅을 발화하지 않는다. (설계 고정) + * + * 단건만 조용하고 벌크는 발화하면, 시더·업그레이드 스텝의 대량 저장이 운영자 감사 + * 기록을 오염시킨다. 침묵은 저장 경로 전부에 걸린 계약이다. + * + * @scenario actor=system, save_path=bulk + * + * @effects service_level_save_stays_silent + */ + public function test_service_level_bulk_save_does_not_fire_the_hook(): void + { + app(EcommerceSettingsService::class)->saveSettings([ + 'seo' => ['meta_product_title' => '{product_name} | shop'], + ]); + + $this->assertSame([], $this->received, '서비스 벌크 저장이 관리자 저장 훅을 발화했습니다.'); + } + + /** + * 서비스의 은행 목록 저장도 훅을 발화하지 않는다. (설계 고정) + * + * @scenario actor=system, save_path=save_banks + * + * @effects service_level_save_stays_silent + */ + public function test_service_level_bank_save_does_not_fire_the_hook(): void + { + app(EcommerceSettingsService::class)->saveBanks([ + ['code' => '004', 'name' => ['ko' => '국민은행', 'en' => 'Kookmin Bank']], + ]); + + $this->assertSame([], $this->received, '서비스 은행 저장이 관리자 저장 훅을 발화했습니다.'); + } + + /** + * 저장이 코어 활동로그에 기록된다. + * + * 코어 활동로그 리스너가 같은 훅을 구독하므로, 훅이 죽으면 SEO 캐시와 함께 + * 설정 변경 감사 기록도 조용히 사라진다. + * + * @scenario actor=admin, save_path=bulk, listener=activity_log + * + * @effects module_settings_save_recorded_in_activity_log + */ + public function test_settings_save_is_recorded_in_activity_log(): void + { + $hooks = CoreActivityLogListener::getSubscribedHooks(); + + $this->assertArrayHasKey('core.module_settings.after_save', $hooks); + $this->assertSame('handleModuleSettingsAfterSave', $hooks['core.module_settings.after_save']['method']); + + $before = DB::table('activity_logs')->where('action', 'module_settings.save')->count(); + + $this->actingAs($this->adminUser)->putJson($this->apiBase, [ + '_tab' => 'seo', + 'seo' => ['meta_product_title' => '{product_name}'], + ])->assertOk(); + + $this->assertGreaterThan( + $before, + DB::table('activity_logs')->where('action', 'module_settings.save')->count(), + '설정 저장이 활동로그에 기록되지 않았습니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExceptionStatusMappingTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExceptionStatusMappingTest.php new file mode 100644 index 00000000..195a48a3 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExceptionStatusMappingTest.php @@ -0,0 +1,164 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Exceptions\BrandOperationException; +use Modules\Sirsoft\Ecommerce\Exceptions\CouponOperationException; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductCommonInfoOperationException; +use Modules\Sirsoft\Ecommerce\Exceptions\ShippingCarrierOperationException; +use Modules\Sirsoft\Ecommerce\Services\BrandService; +use Modules\Sirsoft\Ecommerce\Services\CouponService; +use Modules\Sirsoft\Ecommerce\Services\ProductCommonInfoService; +use Modules\Sirsoft\Ecommerce\Services\ShippingCarrierService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +/** + * 관리자 CRUD 엔드포인트의 예외 → 상태코드 매핑 검증 (#104) + * + * 코어 GenericCatchStatusCodeContractTest 가 소스 전수로 "광역 catch 는 5xx" 계약을 고정한다면, + * 이 테스트는 그 계약이 실제 HTTP 응답까지 도달하는지를 대표 엔드포인트로 확인한다. + * + * 축: + * - 도메인 예외(typed) → 기존 4xx 유지 (운영자 안내) + * - 그 외 예외(인프라/코드 결함) → 500 (장애가 입력 오류로 위장되지 않음) + */ +class ExceptionStatusMappingTest extends ModuleTestCase +{ + protected User $adminUser; + + protected function setUp(): void + { + parent::setUp(); + + $this->adminUser = $this->createAdminUser([ + 'sirsoft-ecommerce.brands.read', + 'sirsoft-ecommerce.brands.update', + 'sirsoft-ecommerce.brands.delete', + 'sirsoft-ecommerce.promotion-coupon.read', + 'sirsoft-ecommerce.promotion-coupon.update', + 'sirsoft-ecommerce.promotion-coupon.delete', + 'sirsoft-ecommerce.settings.read', + 'sirsoft-ecommerce.settings.update', + 'sirsoft-ecommerce.product-common-infos.read', + 'sirsoft-ecommerce.product-common-infos.update', + 'sirsoft-ecommerce.product-common-infos.delete', + ]); + } + + /** + * 대표 엔드포인트 정의. + * + * [라벨, HTTP 메서드, URI, 서비스 클래스, 서비스 메서드, typed 예외 클래스|null, typed 기대 상태코드] + * + * @return array<string, array{0: string, 1: string, 2: class-string, 3: string, 4: class-string|null, 5: int|null}> + */ + public static function endpointProvider(): array + { + $base = '/api/modules/sirsoft-ecommerce/admin'; + + return [ + '브랜드 삭제' => [ + 'delete', $base.'/brands/1', + BrandService::class, 'deleteBrand', + BrandOperationException::class, 400, + ], + '브랜드 상태토글' => [ + 'patch', $base.'/brands/1/toggle-status', + BrandService::class, 'toggleStatus', + BrandOperationException::class, 400, + ], + '쿠폰 삭제' => [ + 'delete', $base.'/promotion-coupons/1', + CouponService::class, 'deleteCoupon', + CouponOperationException::class, 400, + ], + '배송사 상태토글' => [ + 'patch', $base.'/shipping-carriers/1/toggle-status', + ShippingCarrierService::class, 'toggleStatus', + ShippingCarrierOperationException::class, 400, + ], + '상품 공통정보 삭제' => [ + 'delete', $base.'/product-common-infos/1', + ProductCommonInfoService::class, 'deleteCommonInfo', + ProductCommonInfoOperationException::class, 400, + ], + '상품 공통정보 사용토글' => [ + 'patch', $base.'/product-common-infos/1/toggle-active', + ProductCommonInfoService::class, 'toggleActive', + ProductCommonInfoOperationException::class, 400, + ], + ]; + } + + /** + * 도메인 예외가 아닌 예외는 500 으로 구분되어야 한다. + * + * @scenario error_class=infrastructure + * + * @effects admin_brand_delete_infrastructure_exception_returns_500, + * admin_brand_toggle_status_infrastructure_exception_returns_500, + * admin_coupon_delete_infrastructure_exception_returns_500, + * admin_shipping_carrier_toggle_status_infrastructure_exception_returns_500, + * admin_product_common_info_delete_infrastructure_exception_returns_500, + * admin_product_common_info_toggle_active_infrastructure_exception_returns_500, + * admin_crud_infrastructure_response_omits_exception_text + */ + #[DataProvider('endpointProvider')] + public function test_infrastructure_exception_returns_500( + string $verb, + string $uri, + string $serviceClass, + string $serviceMethod, + ?string $typedException, + ?int $typedStatus + ): void { + $this->mock($serviceClass, function ($mock) use ($serviceMethod) { + $mock->shouldReceive($serviceMethod) + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->adminUser)->{$verb.'Json'}($uri, []); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + /** + * 도메인 예외는 기존 상태코드를 유지해야 한다 (계약 변경 없음). + * + * @scenario error_class=domain + * + * @effects admin_brand_delete_domain_exception_keeps_400, + * admin_brand_toggle_status_domain_exception_keeps_400, + * admin_coupon_delete_domain_exception_keeps_400, + * admin_shipping_carrier_toggle_status_domain_exception_keeps_400, + * admin_product_common_info_delete_domain_exception_keeps_400, + * admin_product_common_info_toggle_active_domain_exception_keeps_400 + */ + #[DataProvider('endpointProvider')] + public function test_domain_exception_keeps_original_status( + string $verb, + string $uri, + string $serviceClass, + string $serviceMethod, + ?string $typedException, + ?int $typedStatus + ): void { + if ($typedException === null) { + $this->markTestSkipped('typed 도메인 예외가 없는 엔드포인트'); + } + + $this->mock($serviceClass, function ($mock) use ($serviceMethod, $typedException) { + $mock->shouldReceive($serviceMethod) + ->andThrow(new $typedException('sirsoft-ecommerce::exceptions.operation_failed')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->adminUser)->{$verb.'Json'}($uri, []); + + $response->assertStatus($typedStatus); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExtraFeeTemplateControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExtraFeeTemplateControllerTest.php index aff96418..ea51ca0c 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExtraFeeTemplateControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExtraFeeTemplateControllerTest.php @@ -484,4 +484,52 @@ class ExtraFeeTemplateControllerTest extends ModuleTestCase $response = $this->getJson('/api/modules/sirsoft-ecommerce/admin/extra-fee-templates'); $response->assertStatus(401); } + + // ──────────────────────────────────────────────────────── + // 능력(abilities) 게이트 정합성 — 라우트 SSoT(shipping-policies.*) 일치 + // ──────────────────────────────────────────────────────── + + /** + * 상세 리소스의 can_* 능력은 라우트 SSoT(shipping-policies.*)로 게이팅된다. + * + * 회귀 방지: 과거 상세 리소스만 abilityMap 을 settings.update(타 리소스)로 게이팅해, + * shipping-policies.* 만 보유한(=라우트상 편집 권한이 있는) 액터가 상세 화면에서 can_update + * 가 false 로 보이던 결함. 이제 상세도 형제 Collection·라우트와 동일한 권한으로 게이팅한다. + */ + public function test_show_abilities_follow_shipping_policies_permission(): void + { + $template = $this->createTemplate(); + + // adminUser 는 shipping-policies.* 만 보유하고 settings.update 는 없다(setUp). + $response = $this->actingAs($this->adminUser) + ->getJson("/api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{$template->id}"); + + $response->assertOk(); + $this->assertTrue($response->json('data.abilities.can_update'), 'can_update 는 shipping-policies.update 로 게이팅되어야 합니다'); + $this->assertTrue($response->json('data.abilities.can_create'), 'can_create 는 shipping-policies.create 로 게이팅되어야 합니다'); + $this->assertTrue($response->json('data.abilities.can_delete'), 'can_delete 는 shipping-policies.delete 로 게이팅되어야 합니다'); + } + + /** + * settings.update 만 있고 shipping-policies 권한이 없으면 상세 능력은 모두 false 여야 한다. + * + * (게이트가 실제로 shipping-policies 로 옮겨졌음을 반대 방향으로 고정 — settings.update 로는 + * 더 이상 편집 능력이 켜지지 않는다) + */ + public function test_show_abilities_false_with_only_settings_permission(): void + { + $settingsOnlyUser = $this->createAdminUser([ + 'sirsoft-ecommerce.shipping-policies.read', // 상세 조회용 + 'sirsoft-ecommerce.settings.update', + ]); + $template = $this->createTemplate(); + + $response = $this->actingAs($settingsOnlyUser) + ->getJson("/api/modules/sirsoft-ecommerce/admin/extra-fee-templates/{$template->id}"); + + $response->assertOk(); + $this->assertFalse($response->json('data.abilities.can_update'), 'settings.update 로는 편집 능력이 켜지면 안 됩니다'); + $this->assertFalse($response->json('data.abilities.can_create')); + $this->assertFalse($response->json('data.abilities.can_delete')); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/PaymentSettingsOrphanedPgProviderTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/PaymentSettingsOrphanedPgProviderTest.php new file mode 100644 index 00000000..22fde59d --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/PaymentSettingsOrphanedPgProviderTest.php @@ -0,0 +1,385 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Admin; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 죽은 PG 를 지정한 결제수단의 공개 노출 차단 (A2, 공개 #111 동종) + * + * 고아 판정(`_orphaned`)은 결제수단 ID 에만 계산된다. 그래서 builtin 수단(card 등)에 + * 특정 PG 를 지정한 뒤 그 PG 플러그인을 제거하면, 수단 자체는 카탈로그에 남아 있으므로 + * 고아 필터를 그대로 통과한다. 결과적으로 체크아웃에 선택 가능한 결제수단으로 노출되고, + * 주문 시 PG 라우팅이 매칭에 실패해 **결제창 없이 주문완료로 넘어간다**. + * + * 판정식은 런타임 폴백과 일치시킨다: + * effective = method.pg_provider ?? default_pg_provider // null 일 때만 폴백 + * _orphaned_pg = needs_pg && effective 가 비지 않았고 && 레지스트리에 없음 + * + * own 이 죽었고 default 가 살아 있어도 폴백하지 않는다 — `determinePgProvider()` 의 실제 + * 동작이 그렇다(죽은 own 을 그대로 반환). 레지스트리 인식 폴백으로 바꾸는 것은 결제 라우팅 + * 계약 변경이므로 하지 않고, 차단만 한다. + */ +class PaymentSettingsOrphanedPgProviderTest 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 = app(EcommerceSettingsService::class); + $this->service->clearCache(); + } + + protected function tearDown(): void + { + if (File::isDirectory($this->storagePath)) { + File::cleanDirectory($this->storagePath); + } + + parent::tearDown(); + } + + /** + * 살아 있는 PG provider 를 레지스트리에 등록합니다. + * + * @param array<int, string> $ids provider ID 목록 + */ + private function registerPgProviders(array $ids): void + { + HookManager::addFilter( + 'sirsoft-ecommerce.payment.registered_pg_providers', + function (array $providers) use ($ids) { + foreach ($ids as $id) { + $providers[] = ['id' => $id, 'name' => strtoupper($id)]; + } + + return $providers; + }, + 10 + ); + } + + /** + * PG 고정(`pg_locked`) 확장 결제수단을 카탈로그에 등록합니다. + * + * @param string $pgProvider 이 수단이 고정으로 쓰는 PG + */ + private function registerLockedExtensionMethod(string $pgProvider): void + { + HookManager::addFilter( + 'sirsoft-ecommerce.settings.filter_available_payment_methods', + fn (array $methods) => array_merge($methods, [[ + 'id' => 'locked_easypay', + 'name' => ['ko' => '고정PG 간편결제', 'en' => 'Locked Easy Pay'], + 'description' => ['ko' => '', 'en' => ''], + 'icon' => 'credit-card', + 'source' => 'plugin:test-locked', + 'defaults' => [ + 'pg_provider' => $pgProvider, + 'pg_locked' => true, + 'needs_pg' => true, + 'refund_method' => 'pg', + 'is_active' => true, + 'min_order_amount' => 0, + ], + ]]), + 10 + ); + } + + /** + * order_settings 저장 파일을 직접 구성합니다. + * + * @param string|null $cardPgProvider card 수단에 지정할 PG + * @param string|null $defaultPgProvider 기본 PG + */ + private function seedOrderSettings(?string $cardPgProvider, ?string $defaultPgProvider): void + { + File::ensureDirectoryExists($this->storagePath); + File::put($this->storagePath.'/order_settings.json', json_encode([ + 'default_pg_provider' => $defaultPgProvider, + 'payment_methods' => [ + ['id' => 'card', 'is_active' => true, 'sort_order' => 1, 'pg_provider' => $cardPgProvider], + ['id' => 'dbank', 'is_active' => true, 'sort_order' => 2], + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + $this->service->clearCache(); + } + + /** + * 관리자 응답에서 특정 결제수단 항목을 찾습니다. + * + * @param string $id 결제수단 ID + * @return array|null 항목 + */ + private function adminMethod(string $id): ?array + { + $methods = $this->service->getSettings('order_settings')['payment_methods'] ?? []; + + return collect($methods)->firstWhere('id', $id); + } + + /** + * 공개 응답의 결제수단 ID 목록을 반환합니다. + * + * @return array<int, string> 결제수단 ID 목록 + */ + private function publicMethodIds(): array + { + $methods = $this->service->getPublicPaymentSettings()['payment_methods'] ?? []; + + return array_values(array_map(fn ($m) => $m['id'] ?? null, $methods)); + } + + /** + * 수단에 지정된 PG 가 레지스트리에 없으면 공개 응답에서 제거된다. (실패-먼저) + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_method_hidden_from_checkout, dead_pg_method_flagged_for_admin + */ + public function test_dead_own_pg_provider_is_hidden_from_public(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: 'ghost_pg', defaultPgProvider: 'kginicis'); + + $this->assertTrue( + (bool) ($this->adminMethod('card')['_orphaned_pg'] ?? false), + '관리자 응답에 죽은 PG 표시가 없습니다.' + ); + $this->assertNotContains('card', $this->publicMethodIds(), '죽은 PG 를 지정한 수단이 공개 응답에 남았습니다.'); + $this->assertContains('dbank', $this->publicMethodIds(), '정상 수단까지 제거되었습니다.'); + } + + /** + * own 이 죽었으면 default 가 살아 있어도 차단한다. (런타임 폴백과 동일 판정) + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_method_hidden_from_checkout + */ + public function test_live_default_does_not_rescue_dead_own_provider(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: 'ghost_pg', defaultPgProvider: 'kginicis'); + + $this->assertNotContains('card', $this->publicMethodIds()); + } + + /** + * 수단에 PG 지정이 없으면 기본 PG 로 폴백하며, 그 기본 PG 가 죽었으면 차단한다. (실패-먼저) + * + * @scenario pg_provider_state=dead_default + * + * @effects dead_pg_method_hidden_from_checkout, dead_default_pg_normalized_in_public + */ + public function test_dead_default_pg_provider_blocks_method_without_own(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: null, defaultPgProvider: 'ghost_pg'); + + $this->assertTrue((bool) ($this->adminMethod('card')['_orphaned_pg'] ?? false)); + $this->assertNotContains('card', $this->publicMethodIds()); + + // `??` 는 null 을 부재로 접으므로 키 존재 여부와 값을 나눠 확인한다 + $public = $this->service->getPublicPaymentSettings(); + $this->assertArrayHasKey('default_pg_provider', $public); + $this->assertNull( + $public['default_pg_provider'], + '죽은 기본 PG 가 공개 응답에 그대로 노출되었습니다.' + ); + } + + /** + * 양쪽 모두 미설정이면 기존 계약(non-PG 강하)을 유지한다. (비회귀 pin) + * + * @scenario pg_provider_state=none + * + * @effects unconfigured_pg_keeps_legacy_contract + */ + public function test_unset_pg_provider_keeps_method_visible(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: null, defaultPgProvider: null); + + $this->assertFalse( + (bool) ($this->adminMethod('card')['_orphaned_pg'] ?? false), + 'PG 미설정 상태가 고아로 판정되었습니다 (기존 계약 축소).' + ); + $this->assertContains('card', $this->publicMethodIds()); + } + + /** + * 살아 있는 PG 지정은 그대로 노출된다. (비회귀 pin) + * + * @scenario pg_provider_state=live + * + * @effects live_pg_method_remains_visible + */ + public function test_live_pg_provider_stays_visible(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: 'kginicis', defaultPgProvider: 'kginicis'); + + $this->assertFalse((bool) ($this->adminMethod('card')['_orphaned_pg'] ?? false)); + $this->assertContains('card', $this->publicMethodIds()); + $this->assertSame('kginicis', $this->service->getPublicPaymentSettings()['default_pg_provider'] ?? null); + } + + /** + * PG 불필요 수단(dbank)은 PG 레지스트리와 무관하게 노출된다. + * + * @scenario pg_provider_state=dead_default + * + * @effects non_pg_method_unaffected_by_registry + */ + public function test_non_pg_method_is_not_affected(): void + { + $this->registerPgProviders([]); + $this->seedOrderSettings(cardPgProvider: null, defaultPgProvider: 'ghost_pg'); + + $this->assertFalse((bool) ($this->adminMethod('dbank')['_orphaned_pg'] ?? false)); + $this->assertContains('dbank', $this->publicMethodIds()); + } + + /** + * PG 고정(`pg_locked`) 수단도 그 PG 가 사라지면 똑같이 차단된다. (비회귀 pin) + * + * 판정식에 `pg_locked` 특례를 두면 안 된다 — 고정이라는 선언은 "운영자가 PG 를 바꿀 수 + * 없다" 는 뜻일 뿐, 그 PG 가 살아 있다는 보증이 아니다. 특례를 두는 순간 PG 고정 수단만 + * 죽은 PG 를 달고 주문서에 남아, 결제창 없이 주문완료로 넘어가는 원래 결함이 되살아난다. + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_method_hidden_from_checkout, dead_pg_method_flagged_for_admin + */ + public function test_pg_locked_method_has_no_orphan_exemption(): void + { + // 수단은 등록하되 그 수단이 고정으로 쓰는 PG 는 레지스트리에 없다 (PG 플러그인 삭제) + $this->registerLockedExtensionMethod('ghost_pg'); + $this->registerPgProviders(['kginicis']); + + File::ensureDirectoryExists($this->storagePath); + File::put($this->storagePath.'/order_settings.json', json_encode([ + 'default_pg_provider' => 'kginicis', + 'payment_methods' => [ + ['id' => 'locked_easypay', 'is_active' => true, 'sort_order' => 1], + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + $this->service->clearCache(); + + $method = $this->adminMethod('locked_easypay'); + + $this->assertNotNull($method, '고정 PG 수단이 카탈로그에서 사라졌습니다.'); + $this->assertTrue((bool) ($method['pg_locked'] ?? false), '테스트 전제(pg_locked)가 깨졌습니다.'); + $this->assertTrue( + (bool) ($method['_orphaned_pg'] ?? false), + 'pg_locked 수단이 고아 판정에서 면제되었습니다.' + ); + $this->assertNotContains('locked_easypay', $this->publicMethodIds()); + } + + /** + * PG 고정 수단의 PG 가 살아 있으면 그대로 노출된다. (비회귀 pin) + * + * @scenario pg_provider_state=live + * + * @effects live_pg_method_remains_visible + */ + public function test_pg_locked_method_with_live_provider_stays_visible(): void + { + $this->registerLockedExtensionMethod('kginicis'); + $this->registerPgProviders(['kginicis']); + + File::ensureDirectoryExists($this->storagePath); + File::put($this->storagePath.'/order_settings.json', json_encode([ + 'default_pg_provider' => 'kginicis', + 'payment_methods' => [ + ['id' => 'locked_easypay', 'is_active' => true, 'sort_order' => 1], + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + $this->service->clearCache(); + + $this->assertFalse((bool) ($this->adminMethod('locked_easypay')['_orphaned_pg'] ?? false)); + $this->assertContains('locked_easypay', $this->publicMethodIds()); + } + + /** + * 런타임 전용 플래그는 저장 파일에 박제되지 않는다. + * + * @scenario pg_provider_state=dead_own + * + * @effects runtime_only_flag_not_persisted + */ + public function test_orphaned_pg_flag_is_not_persisted(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: 'ghost_pg', defaultPgProvider: 'kginicis'); + + // 관리자 화면이 읽은 값을 그대로 되저장하는 상황 + $methods = $this->service->getSettings('order_settings')['payment_methods']; + $this->service->saveSettings(['order_settings' => ['payment_methods' => $methods]]); + + $saved = json_decode(File::get($this->storagePath.'/order_settings.json'), true); + foreach ($saved['payment_methods'] as $method) { + $this->assertArrayNotHasKey('_orphaned_pg', $method, '런타임 전용 플래그가 저장되었습니다.'); + } + } + + /** + * 체크아웃이 실제로 호출하는 엔드포인트도 같은 목록을 내려준다. + * + * 위 테스트들은 서비스 메서드를 직접 부른다 — 필터가 응답 조립 경로에 실제로 걸려 + * 있는지는 말해 주지 않는다. 컨트롤러가 raw `getSettings()` 를 쓰면 서비스는 정상이고 + * 그 엔드포인트만 조용히 뚫린다. + * + * 항목 제거 후 인덱스 재정렬(`array_values`)도 여기서 함께 본다 — 비연속 키는 JSON 에서 + * 배열이 아니라 객체로 직렬화되어, 화면의 반복 렌더가 아무것도 그리지 않는다. + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_method_hidden_from_checkout, dead_default_pg_normalized_in_public + */ + public function test_checkout_endpoint_returns_the_same_filtered_list(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings(cardPgProvider: 'ghost_pg', defaultPgProvider: 'kginicis'); + + // 같은 데이터를 내보내는 공개 엔드포인트가 둘이다 — 한쪽이 raw 게터를 쓰면 그 경로만 뚫린다. + foreach (['checkout', 'payment'] as $endpoint) { + $methods = $this->getJson('/api/modules/sirsoft-ecommerce/settings/'.$endpoint) + ->assertOk() + ->json('data.order_settings.payment_methods'); + + $this->assertIsArray($methods, "[{$endpoint}] 결제수단이 배열로 직렬화되지 않았습니다."); + $this->assertSame( + range(0, count($methods) - 1), + array_keys($methods), + "[{$endpoint}] 결제수단 배열의 키가 비연속입니다 — JSON 객체로 직렬화되어 화면 반복이 깨집니다." + ); + + $ids = array_column($methods, 'id'); + $this->assertNotContains('card', $ids, "[{$endpoint}] 죽은 PG 수단이 공개 응답에 남았습니다."); + $this->assertContains('dbank', $ids, "[{$endpoint}] 정상 수단까지 응답에서 사라졌습니다."); + $this->assertSame( + $this->publicMethodIds(), + $ids, + "[{$endpoint}] 공개 게터와 다른 목록을 내려줍니다." + ); + } + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductReviewControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductReviewControllerTest.php index 970a16aa..4c530794 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductReviewControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductReviewControllerTest.php @@ -7,6 +7,7 @@ use Modules\Sirsoft\Ecommerce\Enums\ReviewStatus; use Modules\Sirsoft\Ecommerce\Models\OrderOption; use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Models\ProductReview; +use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -574,4 +575,61 @@ class ProductReviewControllerTest extends ModuleTestCase // Then $response->assertForbidden(); } + + // ======================================== + // 이미지 download URL 직렬화 (숨김 리뷰 <img> 썸네일 렌더 계약) + // ======================================== + + /** + * 숨김(HIDDEN) 리뷰의 관리자 상세 응답은 이미지에 한시 서명 download URL 을 + * 직렬화한다 — <img> 는 인증 헤더를 실을 수 없어, 무서명 URL 이면 관리자 + * 화면의 썸네일이 서빙 게이트(404)에 깨진다. + * + * @scenario resource=review_image, parent_state=restricted + * + * @effects admin_review_serializes_signed_download_url_for_hidden_review + */ + #[Test] + public function test_admin_detail_serializes_signed_download_url_for_hidden_review(): void + { + // Given: 숨김 리뷰 + 이미지 + $review = $this->createReview(['status' => ReviewStatus::HIDDEN->value]); + ProductReviewImage::factory()->create(['review_id' => $review->id]); + + // When + $downloadUrl = $this->actingAs($this->adminUser) + ->getJson("{$this->apiBase}/{$review->id}") + ->assertOk() + ->json('data.images.0.download_url'); + + // Then: 서명 쿼리가 실린 URL + $this->assertIsString($downloadUrl); + $this->assertStringContainsString('signature=', $downloadUrl, '숨김 리뷰 이미지는 서명 download URL 로 직렬화되어야 합니다'); + } + + /** + * 전시중(VISIBLE) 리뷰의 이미지 download URL 은 종전과 동일한 무서명 URL 이다 + * (공개 콘텐츠에 만료성 URL 이 섞이는 회귀 방지 — CDN·캐시 안정성). + * + * @scenario resource=review_image, parent_state=public + * + * @effects visible_review_serializes_plain_download_url + */ + #[Test] + public function test_admin_detail_serializes_plain_download_url_for_visible_review(): void + { + // Given: 전시중 리뷰 + 이미지 + $review = $this->createReview(); + $image = ProductReviewImage::factory()->create(['review_id' => $review->id]); + + // When + $downloadUrl = $this->actingAs($this->adminUser) + ->getJson("{$this->apiBase}/{$review->id}") + ->assertOk() + ->json('data.images.0.download_url'); + + // Then: 기존 무서명 URL 그대로 (직접 URL 또는 API 폴백) + $this->assertSame($image->download_url, $downloadUrl); + $this->assertStringNotContainsString('signature=', (string) $downloadUrl); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ShippingPolicyControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ShippingPolicyControllerTest.php index 7a76fa6d..9bad627a 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ShippingPolicyControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ShippingPolicyControllerTest.php @@ -552,9 +552,9 @@ class ShippingPolicyControllerTest extends ModuleTestCase $ranges = [ 'type' => 'amount', 'tiers' => [ - ['min' => 0, 'max' => 29999, 'fee' => 3000, 'unit' => '원'], - ['min' => 30000, 'max' => 49999, 'fee' => 2000, 'unit' => '원'], - ['min' => 50000, 'max' => null, 'fee' => 0, 'unit' => '원'], + ['min' => 0, 'max' => 30000, 'fee' => 3000], + ['min' => 30000, 'max' => 50000, 'fee' => 2000], + ['min' => 50000, 'max' => null, 'fee' => 0], ], ]; @@ -586,8 +586,8 @@ class ShippingPolicyControllerTest extends ModuleTestCase $ranges = [ 'type' => 'quantity', 'tiers' => [ - ['min' => 0, 'max' => 4, 'fee' => 3000, 'unit' => '개'], - ['min' => 5, 'max' => null, 'fee' => 5000, 'unit' => '개'], + ['min' => 0, 'max' => 4, 'fee' => 3000], + ['min' => 5, 'max' => null, 'fee' => 5000], ], ]; @@ -614,8 +614,8 @@ class ShippingPolicyControllerTest extends ModuleTestCase $ranges = [ 'type' => 'weight', 'tiers' => [ - ['min' => 0, 'max' => 2, 'fee' => 3000, 'unit' => 'kg'], - ['min' => 3, 'max' => null, 'fee' => 5000, 'unit' => 'kg'], + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 2, 'max' => null, 'fee' => 5000], ], ]; @@ -642,8 +642,8 @@ class ShippingPolicyControllerTest extends ModuleTestCase $ranges = [ 'type' => 'volume', 'tiers' => [ - ['min' => 0, 'max' => 99, 'fee' => 5000, 'unit' => 'cm³'], - ['min' => 100, 'max' => null, 'fee' => 10000, 'unit' => 'cm³'], + ['min' => 0, 'max' => 100, 'fee' => 5000], + ['min' => 100, 'max' => null, 'fee' => 10000], ], ]; @@ -670,8 +670,8 @@ class ShippingPolicyControllerTest extends ModuleTestCase $ranges = [ 'type' => 'volume_weight', 'tiers' => [ - ['min' => 0, 'max' => 4, 'fee' => 5000, 'unit' => 'kg'], - ['min' => 5, 'max' => null, 'fee' => 10000, 'unit' => 'kg'], + ['min' => 0, 'max' => 5, 'fee' => 5000], + ['min' => 5, 'max' => null, 'fee' => 10000], ], ]; @@ -698,7 +698,8 @@ class ShippingPolicyControllerTest extends ModuleTestCase $payload = $this->makeStorePayload([], [ $this->makeKrCountrySetting([ 'charge_policy' => 'api', - 'base_fee' => 0, + // API 정책의 base_fee 는 외부 API 장애 시 폴백 배송비 (0 금지) + 'base_fee' => 30000, 'api_endpoint' => 'https://api.example.com/shipping/calculate', // 후보 5종(ShippingApiRequestField) SSoT 내 값만 허용 (W3 Rule::in) 'api_request_fields' => ['items', 'group_total', 'total_quantity'], @@ -1034,7 +1035,7 @@ class ShippingPolicyControllerTest extends ModuleTestCase 'type' => 'amount', 'tiers' => [ ['min' => 0, 'max' => 50000, 'fee' => 3000], - ['min' => 50001, 'max' => null, 'fee' => 0], + ['min' => 50000, 'max' => null, 'fee' => 0], ], ], ]), @@ -1378,6 +1379,7 @@ class ShippingPolicyControllerTest extends ModuleTestCase * 배송정책 상세 조회 - country_settings 포함 * * @scenario endpoint=detail,opt_in=default + * * @effects detail_still_provides_full_country_settings */ #[Test] diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/CashReceiptControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/CashReceiptControllerTest.php index 38ca45b9..f92a1977 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/CashReceiptControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/CashReceiptControllerTest.php @@ -16,6 +16,7 @@ use Modules\Sirsoft\Ecommerce\Models\OrderCashReceipt; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -27,6 +28,8 @@ use PHPUnit\Framework\Attributes\Test; */ class CashReceiptControllerTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -47,6 +50,7 @@ class CashReceiptControllerTest extends ModuleTestCase $this->receiptSequence = 0; $this->adminUser = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/InquiryDomainExceptionStatusPairTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/InquiryDomainExceptionStatusPairTest.php new file mode 100644 index 00000000..c6fa4856 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/InquiryDomainExceptionStatusPairTest.php @@ -0,0 +1,327 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers; + +use App\Extension\HookManager; +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductInquiryOperationException; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Services\ProductInquiryService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 문의 도메인 액션의 예외 → 상태코드 페어 검증 + * + * `ProductInquiryService` 가 도메인 실패를 `ProductInquiryOperationException` 으로 + * 승격한 뒤에도 사용자 컨트롤러는 `catch (\RuntimeException)` 을 유지하고 있었다. + * 그 catch 는 승격된 도메인 예외보다 넓어서, 남는 것은 인프라 RuntimeException 뿐인데 + * 그것까지 422 + 예외 원문으로 뭉갰다 — #104 가 없애려던 바로 그 형태다. + * + * 반대 방향의 짝도 있었다. `destroy` 는 `deleteInquiry` 가 던지는 도메인 예외를 + * 구분하는 catch 가 없어, 운영자가 고칠 수 있는 사유(문의 게시판 미설정)가 500 으로 + * 나갔다. + * + * 두 축을 실제 HTTP 응답으로 고정한다. + * + * - 도메인 예외(typed) → 422 + 해석된 사유 문구 + * - 그 외 예외(인프라/코드 결함) → 500, 응답 본문에 예외 원문 미포함 + * + * 소스 전수 판정(`GenericCatchStatusCodeContractTest`)은 `\Exception`/`\Throwable` 만 + * generic 으로 보므로 `\RuntimeException` 형태의 광역 catch 를 잡지 못한다. 이 + * 테스트가 그 사각을 응답 단에서 메운다. + */ +class InquiryDomainExceptionStatusPairTest extends ModuleTestCase +{ + private User $user; + + private User $manager; + + private ProductInquiry $inquiry; + + protected function setUp(): void + { + parent::setUp(); + + $this->user = $this->createUser(); + $this->manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); + + $product = Product::factory()->create(); + $this->inquiry = ProductInquiry::factory()->create([ + 'user_id' => $this->user->id, + 'product_id' => $product->id, + 'is_answered' => false, + ]); + + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-inquiry-board'); + + // 다른 모듈이 등록한 inquiry.* 필터가 스냅샷으로 잔존해 서비스 mock 과 충돌하는 + // cross-module contamination 차단 (UserProductInquiryControllerTest 와 동형). + foreach ([ + 'sirsoft-ecommerce.inquiry.update', + 'sirsoft-ecommerce.inquiry.delete', + 'sirsoft-ecommerce.inquiry.create', + 'sirsoft-ecommerce.inquiry.update_reply', + 'sirsoft-ecommerce.inquiry.delete_reply', + 'sirsoft-ecommerce.inquiry.update_validation_rules', + 'sirsoft-board.post.get_by_ids', + ] as $hook) { + HookManager::clearFilter($hook); + } + } + + /** + * 도메인 실패를 던지는 서비스 mock (본인 문의 조회는 성공). + */ + private function mockServiceThrowing(string $method, \Throwable $e): void + { + $inquiry = $this->inquiry; + + $this->mock(ProductInquiryService::class, function ($mock) use ($method, $e, $inquiry) { + $mock->shouldReceive('findById')->andReturn($inquiry); + $mock->shouldReceive($method)->andThrow($e); + $mock->shouldReceive()->andReturnNull(); + }); + } + + /** + * 도메인 예외 — 게시판 미설정. + */ + private function domainException(): ProductInquiryOperationException + { + return new ProductInquiryOperationException('sirsoft-ecommerce::messages.inquiries.board_not_configured'); + } + + /** + * 인프라 예외 — DB 장애. `\RuntimeException` 이라 종전 광역 catch 에 걸렸다. + */ + private function infraException(): \RuntimeException + { + return new \RuntimeException('SQLSTATE[HY000]: General error: 2006 MySQL server has gone away'); + } + + /** + * 응답 메시지가 "해석된 도메인 사유" 인지 단언한다. + * + * 응답 로케일은 요청 처리 중에 결정되어 테스트 프로세스의 로케일과 다를 수 있으므로 + * 특정 언어로 못박지 않고 지원 로케일의 해석 결과 집합과 대조한다. 고정하려는 것은 + * ① 메시지 키 원문이 새지 않을 것 ② 일반 실패 문구로 뭉개지지 않을 것 두 가지다. + * + * @param string|null $message 응답 message 필드 + * @param string $context 실패 시 표시할 맥락 설명 + */ + private function assertResolvedReason(?string $message, string $context = '도메인 실패 사유가 해석된 다국어 문구로 나가야 합니다.'): void + { + $this->assertNotNull($message, $context); + $this->assertStringNotContainsString( + 'sirsoft-ecommerce::', + $message, + '메시지 키 원문이 응답에 노출되면 안 됩니다.' + ); + + $key = 'sirsoft-ecommerce::messages.inquiries.board_not_configured'; + $resolved = array_map( + fn (string $locale) => trans($key, [], $locale), + config('app.supported_locales') + ); + + $this->assertContains($message, $resolved, $context); + } + + // ------------------------------------------------------------------ + // 문의 수정 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=update_inquiry, error_class=domain + * + * @effects inquiry_update_domain_exception_returns_422 + */ + public function test_update_domain_exception_returns_422_with_resolved_reason(): void + { + $this->mockServiceThrowing('updateInquiry', $this->domainException()); + + $response = $this->actingAs($this->user)->putJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}", + ['content' => '수정된 문의 내용입니다.'] + ); + + $response->assertStatus(422); + $this->assertResolvedReason($response->json('message')); + } + + /** + * @scenario endpoint=update_inquiry, error_class=infrastructure + * + * @effects inquiry_update_infrastructure_exception_returns_500_not_422 + */ + public function test_update_infrastructure_exception_returns_500_not_422(): void + { + $this->mockServiceThrowing('updateInquiry', $this->infraException()); + + $response = $this->actingAs($this->user)->putJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}", + ['content' => '수정된 문의 내용입니다.'] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 문의 삭제 (도메인 예외가 500 으로 나가던 역방향 짝) + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=destroy_inquiry, error_class=domain + * + * @effects inquiry_destroy_domain_exception_returns_422 + */ + public function test_destroy_domain_exception_returns_422_not_500(): void + { + $this->mockServiceThrowing('deleteInquiry', $this->domainException()); + + $response = $this->actingAs($this->user)->deleteJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}" + ); + + $response->assertStatus(422); + $this->assertResolvedReason($response->json('message'), '삭제 실패 사유가 운영자에게 도달해야 합니다 (종전에는 500 일반 문구).'); + } + + /** + * @scenario endpoint=destroy_inquiry, error_class=infrastructure + * + * @effects inquiry_destroy_infrastructure_exception_returns_500_not_422 + */ + public function test_destroy_infrastructure_exception_returns_500(): void + { + $this->mockServiceThrowing('deleteInquiry', $this->infraException()); + + $response = $this->actingAs($this->user)->deleteJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}" + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 답변 등록 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=create_reply, error_class=domain + * + * @effects inquiry_reply_domain_exception_returns_422 + */ + public function test_reply_domain_exception_returns_422(): void + { + $this->mockServiceThrowing('createReply', $this->domainException()); + + $response = $this->actingAs($this->manager)->postJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '답변 내용입니다.'] + ); + + $response->assertStatus(422); + $this->assertResolvedReason($response->json('message')); + } + + /** + * @scenario endpoint=create_reply, error_class=infrastructure + * + * @effects inquiry_reply_infrastructure_exception_returns_500_not_422 + */ + public function test_reply_infrastructure_exception_returns_500_not_422(): void + { + $this->mockServiceThrowing('createReply', $this->infraException()); + + $response = $this->actingAs($this->manager)->postJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '답변 내용입니다.'] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 답변 수정 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=update_reply, error_class=domain + * + * @effects inquiry_update_reply_domain_exception_returns_422 + */ + public function test_update_reply_domain_exception_returns_422(): void + { + $this->mockServiceThrowing('updateReply', $this->domainException()); + + $response = $this->actingAs($this->manager)->putJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '수정된 답변 내용입니다.'] + ); + + $response->assertStatus(422); + $this->assertResolvedReason($response->json('message')); + } + + /** + * @scenario endpoint=update_reply, error_class=infrastructure + * + * @effects inquiry_update_reply_infrastructure_exception_returns_500_not_422 + */ + public function test_update_reply_infrastructure_exception_returns_500_not_422(): void + { + $this->mockServiceThrowing('updateReply', $this->infraException()); + + $response = $this->actingAs($this->manager)->putJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '수정된 답변 내용입니다.'] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 답변 삭제 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=destroy_reply, error_class=domain + * + * @effects inquiry_destroy_reply_domain_exception_returns_422 + */ + public function test_destroy_reply_domain_exception_returns_422(): void + { + $this->mockServiceThrowing('deleteReply', $this->domainException()); + + $response = $this->actingAs($this->manager)->deleteJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply" + ); + + $response->assertStatus(422); + $this->assertResolvedReason($response->json('message')); + } + + /** + * @scenario endpoint=destroy_reply, error_class=infrastructure + * + * @effects inquiry_destroy_reply_infrastructure_exception_returns_500_not_422 + */ + public function test_destroy_reply_infrastructure_exception_returns_500_not_422(): void + { + $this->mockServiceThrowing('deleteReply', $this->infraException()); + + $response = $this->actingAs($this->manager)->deleteJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply" + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/OrderDomainExceptionStatusPairTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/OrderDomainExceptionStatusPairTest.php new file mode 100644 index 00000000..fe7bc38f --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/OrderDomainExceptionStatusPairTest.php @@ -0,0 +1,398 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers; + +use App\Extension\HookManager; +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Enums\CouponDiscountType; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueCondition; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueMethod; +use Modules\Sirsoft\Ecommerce\Enums\CouponIssueStatus; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetScope; +use Modules\Sirsoft\Ecommerce\Enums\CouponTargetType; +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; +use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum; +use Modules\Sirsoft\Ecommerce\Exceptions\CouponNotIssuableException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderCancellationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderProcessingException; +use Modules\Sirsoft\Ecommerce\Models\ClaimReason; +use Modules\Sirsoft\Ecommerce\Models\Coupon; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; +use Modules\Sirsoft\Ecommerce\Services\CartService; +use Modules\Sirsoft\Ecommerce\Services\OrderCancellationService; +use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; +use Modules\Sirsoft\Ecommerce\Services\OrderService; +use Modules\Sirsoft\Ecommerce\Services\UserCouponService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 주문 도메인 액션의 예외 → 상태코드 페어 검증 (#104) + * + * 배송지 변경(본체)에서 확인된 "인프라 예외가 422 로 위장되는" 결함은 같은 도메인의 + * 다른 액션에도 같은 형태로 존재했다. 여기서는 취소·구매확정·재주문·입금확인·쿠폰 + * 다운로드 각각에 대해 두 축을 실제 HTTP 응답으로 고정한다. + * + * - 도메인 예외(typed) → 기존 4xx 유지 (사용자/운영자 안내) + * - 그 외 예외(인프라/코드 결함) → 500, 응답 본문에 예외 원문 미포함 + * + * 코어 `GenericCatchStatusCodeContractTest` 는 소스 전수로 계약을 고정하지만 "그 계약이 응답까지 + * 도달하는지" 는 증명하지 못한다 — catch 블록에 500 이라고 적혀 있어도 그 앞의 + * FormRequest·미들웨어가 다른 응답을 만들 수 있다. 이 테스트가 그 간극을 메운다. + */ +class OrderDomainExceptionStatusPairTest extends ModuleTestCase +{ + protected User $user; + + protected function setUp(): void + { + parent::setUp(); + + $this->user = $this->createUser(); + + // 사용자 취소는 PG 환불 훅을 타므로 성공 응답으로 고정 (본 테스트의 관심사가 아님) + HookManager::addFilter('sirsoft-ecommerce.payment.refund', fn () => [ + 'success' => true, + 'transaction_id' => 'TEST_PAIR_TXN', + 'error_code' => null, + 'error_message' => null, + ], 10); + } + + /** + * 취소 가능한 결제완료 주문(옵션 1개)을 만든다. + */ + private function makeCancellableOrder(): Order + { + $order = Order::factory()->create([ + 'user_id' => $this->user->id, + 'order_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'subtotal_amount' => 20000, + 'total_amount' => 20000, + 'total_paid_amount' => 20000, + 'total_cancelled_amount' => 0, + 'cancellation_count' => 0, + 'promotions_applied_snapshot' => [], + 'shipping_policy_applied_snapshot' => [], + ]); + + OrderOption::factory()->forOrder($order)->create([ + 'quantity' => 1, + 'unit_price' => 20000, + 'subtotal_price' => 20000, + 'subtotal_paid_amount' => 20000, + 'option_status' => OrderStatusEnum::PAYMENT_COMPLETE, + ]); + + return $order->fresh(['options']); + } + + /** + * 사용자 선택 가능한 환불 사유 코드를 반환한다. + */ + private function refundReasonCode(): string + { + $code = ClaimReason::where('type', 'refund') + ->where('is_active', true) + ->where('is_user_selectable', true) + ->value('code'); + + if ($code === null) { + $this->markTestSkipped('사용자 선택 가능한 환불 사유(ClaimReason) 시드가 없다'); + } + + return $code; + } + + // ------------------------------------------------------------------ + // 회원 주문 취소 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=cancel, error_class=domain + * + * @effects order_cancel_domain_exception_returns_422 + */ + public function test_user_cancel_domain_exception_returns_422(): void + { + $order = $this->makeCancellableOrder(); + + $this->mock(OrderCancellationService::class, function ($mock) { + $mock->shouldReceive('cancelOrder') + ->andThrow(new OrderCancellationException('취소 불가')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/orders/{$order->id}/cancel", + ['reason' => $this->refundReasonCode()] + ); + + $response->assertStatus(422); + } + + /** + * @scenario endpoint=cancel, error_class=infrastructure + * + * @effects order_cancel_infrastructure_exception_returns_500_not_422 + */ + public function test_user_cancel_infrastructure_exception_returns_500(): void + { + $order = $this->makeCancellableOrder(); + + $this->mock(OrderCancellationService::class, function ($mock) { + $mock->shouldReceive('cancelOrder') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error: 2006 MySQL server has gone away')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/orders/{$order->id}/cancel", + ['reason' => $this->refundReasonCode()] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 회원 구매확정 (typed 예외 없음 — 전부 인프라 축) + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=confirm_option, error_class=infrastructure + * + * @effects order_confirm_option_infrastructure_exception_returns_500_not_422 + */ + public function test_user_confirm_option_infrastructure_exception_returns_500(): void + { + $order = $this->makeCancellableOrder(); + $option = $order->options->first(); + // 구매확정 가능 상태(기본값 shipping/delivered)여야 FormRequest 를 통과해 + // 컨트롤러의 catch 까지 도달한다. + $option->update(['option_status' => OrderStatusEnum::DELIVERED]); + + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('confirmOption') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/orders/{$order->id}/options/{$option->id}/confirm" + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 회원 재주문 + // ------------------------------------------------------------------ + + /** + * @scenario endpoint=reorder, error_class=domain + * + * @effects order_reorder_domain_exception_returns_422 + */ + public function test_user_reorder_domain_exception_returns_422(): void + { + $order = $this->makeCancellableOrder(); + + $this->mock(CartService::class, function ($mock) { + $mock->shouldReceive('reorderFromOrder') + ->andThrow(new OrderProcessingException('장바구니 담기 불가')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/orders/{$order->id}/reorder" + ); + + $response->assertStatus(422); + } + + /** + * @scenario endpoint=reorder, error_class=infrastructure + * + * @effects order_reorder_infrastructure_exception_returns_500_not_422 + */ + public function test_user_reorder_infrastructure_exception_returns_500(): void + { + $order = $this->makeCancellableOrder(); + + $this->mock(CartService::class, function ($mock) { + $mock->shouldReceive('reorderFromOrder') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/orders/{$order->id}/reorder" + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 회원 쿠폰 다운로드 + // ------------------------------------------------------------------ + + /** + * 다운로드 대상 쿠폰 1건을 만든다 (발급 로직은 mock 이 대체하므로 존재만 하면 된다). + */ + private function makeDownloadableCoupon(): Coupon + { + return Coupon::create([ + 'name' => ['ko' => '다운로드 쿠폰', 'en' => 'Downloadable Coupon'], + 'target_type' => CouponTargetType::PRODUCT_AMOUNT->value, + 'discount_type' => CouponDiscountType::FIXED->value, + 'discount_value' => 1000, + 'min_order_amount' => 0, + 'issue_method' => CouponIssueMethod::DOWNLOAD->value, + 'issue_condition' => CouponIssueCondition::MANUAL->value, + 'issue_status' => CouponIssueStatus::ISSUING->value, + 'per_user_limit' => 1, + 'valid_type' => 'days_from_issue', + 'valid_days' => 30, + 'is_combinable' => true, + 'target_scope' => CouponTargetScope::ALL->value, + 'created_by' => $this->user->id, + ]); + } + + /** + * 발급 불가 사유는 400 을 유지하고, 사유 식별자로 만든 키가 응답 메시지가 된다. + * + * @scenario endpoint=coupon_download, error_class=domain + * + * @effects coupon_download_domain_exception_returns_400_with_reason_key + */ + public function test_user_coupon_download_domain_exception_returns_400(): void + { + $coupon = $this->makeDownloadableCoupon(); + + $this->mock(UserCouponService::class, function ($mock) { + $mock->shouldReceive('downloadCoupon') + ->andThrow(new CouponNotIssuableException('already_downloaded')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download" + ); + + $response->assertStatus(400); + // 번역된 예외 원문이 아니라 사유 키로 해석된 문구가 나가야 한다 + $this->assertSame( + __('sirsoft-ecommerce::messages.coupon.already_downloaded'), + $response->json('message') + ); + } + + /** + * @scenario endpoint=coupon_download, error_class=infrastructure + * + * @effects coupon_download_infrastructure_exception_returns_500_not_400 + */ + public function test_user_coupon_download_infrastructure_exception_returns_500(): void + { + $coupon = $this->makeDownloadableCoupon(); + + $this->mock(UserCouponService::class, function ($mock) { + $mock->shouldReceive('downloadCoupon') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($this->user)->postJson( + "/api/modules/sirsoft-ecommerce/user/coupons/{$coupon->id}/download" + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ------------------------------------------------------------------ + // 관리자 무통장 입금확인 + // ------------------------------------------------------------------ + + /** + * 입금확인 대상(무통장·미결제) 주문을 만든다. + */ + private function makeDepositPendingOrder(): Order + { + $order = Order::factory()->create([ + 'user_id' => $this->user->id, + 'order_status' => OrderStatusEnum::PENDING_PAYMENT, + 'subtotal_amount' => 20000, + 'total_amount' => 20000, + 'total_paid_amount' => 0, + 'total_cancelled_amount' => 0, + 'cancellation_count' => 0, + 'promotions_applied_snapshot' => [], + 'shipping_policy_applied_snapshot' => [], + ]); + + OrderPayment::factory()->forOrder($order)->create([ + 'payment_method' => PaymentMethodEnum::DBANK, + 'payment_status' => PaymentStatusEnum::WAITING_DEPOSIT, + 'paid_amount_local' => 20000, + 'paid_amount_base' => 20000, + ]); + + return $order->fresh(['payment']); + } + + /** + * @scenario endpoint=confirm_deposit, error_class=domain + * + * @effects order_confirm_deposit_domain_exception_returns_422 + */ + public function test_admin_confirm_deposit_domain_exception_returns_422(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->makeDepositPendingOrder(); + + $this->mock(OrderProcessingService::class, function ($mock) { + $mock->shouldReceive('confirmManualDeposit') + ->andThrow(new OrderProcessingException('전이 불가 상태')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($admin)->patchJson( + "/api/modules/sirsoft-ecommerce/admin/orders/{$order->id}/confirm-deposit", + ['amount' => 20000] + ); + + $response->assertStatus(422); + } + + /** + * @scenario endpoint=confirm_deposit, error_class=infrastructure + * + * @effects order_confirm_deposit_infrastructure_exception_returns_500_not_422 + */ + public function test_admin_confirm_deposit_infrastructure_exception_returns_500(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->makeDepositPendingOrder(); + + $this->mock(OrderProcessingService::class, function ($mock) { + $mock->shouldReceive('confirmManualDeposit') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->actingAs($admin)->patchJson( + "/api/modules/sirsoft-ecommerce/admin/orders/{$order->id}/confirm-deposit", + ['amount' => 20000] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/OrderControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/OrderControllerTest.php index 6ebe0f52..34512dd1 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/OrderControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/OrderControllerTest.php @@ -10,6 +10,8 @@ use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\ProductDisplayStatus; use Modules\Sirsoft\Ecommerce\Enums\ProductSalesStatus; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderCancellationException; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderModificationException; use Modules\Sirsoft\Ecommerce\Models\ClaimReason; use Modules\Sirsoft\Ecommerce\Models\MileageTransaction; use Modules\Sirsoft\Ecommerce\Models\Order; @@ -17,6 +19,8 @@ use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Models\ProductOption; use Modules\Sirsoft\Ecommerce\Models\TempOrder; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Services\OrderCancellationService; +use Modules\Sirsoft\Ecommerce\Services\OrderService; use Modules\Sirsoft\Ecommerce\Services\PaymentMethodResolver; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\DataProvider; @@ -1286,6 +1290,10 @@ class OrderControllerTest extends ModuleTestCase * * 비회원은 저장된 주소(address_id) 없이 직접 입력한 배송지 필드를 전송한다. * 회원과 동일한 OrderService::updateShippingAddress 로 처리되며, 응답은 GuestOrderResource. + * + * @scenario actor=guest, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects guest_update_shipping_address_succeeds_with_valid_token */ public function test_비회원_배송지_변경_성공(): void { @@ -1483,4 +1491,361 @@ class OrderControllerTest extends ModuleTestCase $this->postJson('/api/modules/sirsoft-ecommerce/guest/orders/verify', $payload) ->assertStatus(429); } + + // ================================================================ + // 비회원 배송지 국내/해외 조합 정합성 (F3) + // ================================================================ + + /** + * 비회원 배송지: country_code 없이 해외 주소 조합만 보내면 검증으로 차단된다. + * + * 수정 전에는 검증을 통과해 서비스가 국내 분기로 떨어졌고, NOT NULL 인 zipcode 에 + * null 을 쓰다 SQL 23000 이 발생해 "배송 전에만 변경 가능" 메시지로 위장됐다. + * + * @scenario actor=guest, change_mode=manual, address_region=intl_without_country_code, e2e_browser=chromium + * + * @effects guest_shipping_address_intl_combination_without_country_code_returns_422_validation_errors + */ + public function test_비회원_배송지_해외조합_country_code_누락시_검증차단(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + Order::where('order_number', $orderNumber) + ->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_postal_code' => '94043', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['zipcode', 'address']); + } + + /** + * 비회원 배송지: country_code 를 해외로 명시하면 해외 컬럼이 기록되고 국내 컬럼은 초기화된다. + * + * @scenario actor=guest, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects guest_shipping_address_switching_to_intl_clears_domestic_columns + */ + public function test_비회원_배송지_해외전환시_국내컬럼_초기화(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_state' => 'CA', + 'intl_postal_code' => '94043', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(200); + + $shipping = $order->refresh()->load('addresses') + ->addresses->firstWhere('address_type', 'shipping'); + + $this->assertSame('US', $shipping->recipient_country_code); + $this->assertSame('1600 Amphitheatre Parkway', $shipping->address_line_1); + $this->assertSame('Mountain View', $shipping->intl_city); + $this->assertSame('', $shipping->zipcode); + $this->assertSame('', $shipping->address); + } + + /** + * 비회원 배송지: country_code 를 국내로 명시하면 국내 필수값이 강제된다. + * + * 회원 경로에만 있던 축이다 — F3 의 원 재현 경로가 비회원이었으므로 같은 폭으로 고정한다. + * + * @scenario actor=guest, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects guest_shipping_address_domestic_country_code_requires_zipcode_and_address + */ + public function test_비회원_배송지_국내_country_code_는_zipcode_와_address_를_요구한다(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '김국내', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'KR', + 'address_line_1' => '1600 Amphitheatre Parkway', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['zipcode', 'address']); + } + + /** + * 비회원 배송지: country_code 가 해외인데 해외 필수값이 비면 검증으로 차단된다. + * + * 국내 방향(위)만 고정하면 게이트가 한쪽으로만 서 있는지 드러나지 않는다. + * 회원 경로에는 이 역방향 짝이 있었고 비회원 경로에만 없었다. + * + * @scenario actor=guest, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects guest_shipping_address_foreign_country_code_requires_intl_fields + */ + public function test_비회원_배송지_해외_country_code_는_해외_필수값을_요구한다(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['address_line_1', 'intl_city', 'intl_postal_code']); + } + + /** + * 비회원 배송지: 국내 → 해외 → 국내 왕복 시 반대편 컬럼이 매번 초기화된다. + * + * 한 방향만 검사하면 되돌아온 뒤 해외 컬럼이 잔존해 배송지가 두 벌로 남는 결함을 놓친다. + * + * @scenario actor=guest, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects guest_shipping_address_round_trip_between_domestic_and_intl_clears_opposite_columns + */ + public function test_비회원_배송지_국내_해외_왕복시_반대편_컬럼_초기화(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $url = "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address"; + $headers = ['X-Guest-Order-Token' => $token]; + + // ① 해외로 전환 + $this->putJson($url, [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_state' => 'CA', + 'intl_postal_code' => '94043', + ], $headers)->assertStatus(200); + + // ② 다시 국내로 전환 + $this->putJson($url, [ + 'recipient_name' => '김국내', + 'recipient_phone' => '010-1111-2222', + 'country_code' => 'KR', + 'zipcode' => '06134', + 'address' => '서울특별시 강남구 테헤란로 123', + 'address_detail' => '4층', + ], $headers)->assertStatus(200); + + $shipping = $order->refresh()->load('addresses') + ->addresses->firstWhere('address_type', 'shipping'); + + $this->assertSame('KR', $shipping->recipient_country_code); + $this->assertSame('06134', $shipping->zipcode); + $this->assertSame('서울특별시 강남구 테헤란로 123', $shipping->address); + + $this->assertNull($shipping->address_line_1, '국내로 돌아오면 해외 주소가 남으면 안 됩니다.'); + $this->assertNull($shipping->intl_city); + $this->assertNull($shipping->intl_state); + $this->assertNull($shipping->intl_postal_code); + } + + /** + * 비회원 배송지 변경: 도메인 예외는 기존 422 + 안내 문구를 유지한다. + * + * @scenario actor=guest, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects guest_update_shipping_address_domain_exception_returns_422_infra_returns_500 + */ + public function test_비회원_배송지_도메인예외_422(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('updateShippingAddress') + ->andThrow(new OrderModificationException('배송 전 상태에서만 변경 가능')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '변경수령인', + 'recipient_phone' => '010-9999-8888', + 'zipcode' => '06236', + 'address' => '서울 강남구 테헤란로 1', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-ecommerce::messages.orders.cannot_modify_address'), + $response->json('message') + ); + } + + /** + * 비회원 배송지 변경: 인프라 예외는 500 이며 예외 원문을 노출하지 않는다. + * + * 회원 경로(User\OrderController)와 쌍둥이 코드라 한쪽만 고치면 비회원 화면에서만 + * 장애가 "배송 전에만 변경 가능" 이라는 엉뚱한 입력 오류로 계속 위장된다. + * + * @scenario actor=guest, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects guest_update_shipping_address_domain_exception_returns_422_infra_returns_500, guest_action_5xx_responses_exclude_exception_message + */ + public function test_비회원_배송지_인프라예외_500(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('updateShippingAddress') + ->andThrow(new \RuntimeException('SQLSTATE[23000]: Integrity constraint violation')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->putJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/shipping-address", + [ + 'recipient_name' => '변경수령인', + 'recipient_phone' => '010-9999-8888', + 'zipcode' => '06236', + 'address' => '서울 강남구 테헤란로 1', + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + /** + * 사용자 선택 가능한 환불 사유 코드를 반환한다. + */ + private function userSelectableRefundReason(): string + { + $code = ClaimReason::where('type', 'refund') + ->where('is_active', true) + ->where('is_user_selectable', true) + ->value('code'); + + if ($code === null) { + $this->markTestSkipped('사용자 선택 가능한 환불 사유(ClaimReason) 시드가 없다'); + } + + return $code; + } + + /** + * 비회원 주문 취소: 도메인 예외는 기존 422 를 유지한다. + * + * @effects guest_cancel_domain_exception_returns_422_infra_returns_500 + */ + public function test_비회원_주문취소_도메인예외_422(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $this->mock(OrderCancellationService::class, function ($mock) { + $mock->shouldReceive('cancelOrder') + ->andThrow(new OrderCancellationException('취소 불가')); + $mock->shouldReceive()->andReturnNull(); + }); + + $this->postJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/cancel", + ['reason' => $this->userSelectableRefundReason()], + ['X-Guest-Order-Token' => $token] + )->assertStatus(422); + } + + /** + * 비회원 주문 취소: 인프라 예외는 500 이며 예외 원문을 노출하지 않는다. + * + * @effects guest_cancel_domain_exception_returns_422_infra_returns_500, guest_action_5xx_responses_exclude_exception_message + */ + public function test_비회원_주문취소_인프라예외_500(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + + $this->mock(OrderCancellationService::class, function ($mock) { + $mock->shouldReceive('cancelOrder') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->postJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/cancel", + ['reason' => $this->userSelectableRefundReason()], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + /** + * 비회원 구매확정: typed 도메인 예외가 없으므로 모든 예외가 500 이어야 한다. + * + * @effects guest_confirm_option_has_no_domain_exception_so_generic_catch_returns_500, guest_action_5xx_responses_exclude_exception_message + */ + public function test_비회원_구매확정_인프라예외_500(): void + { + [$orderNumber, $token] = $this->placeGuestOrderAndToken(); + $order = Order::where('order_number', $orderNumber)->first(); + $order->update(['order_status' => OrderStatusEnum::PAYMENT_COMPLETE]); + $option = $order->options()->first(); + + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('confirmOption') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: General error')); + $mock->shouldReceive()->andReturnNull(); + }); + + $response = $this->postJson( + "/api/modules/sirsoft-ecommerce/guest/orders/{$orderNumber}/options/{$option->id}/confirm", + [], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php index 4efa496f..9d45050d 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\Public; use App\Extension\HookManager; +use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; @@ -128,6 +129,262 @@ class PublicProductInquiryControllerTest extends ModuleTestCase $this->assertSame('홍*동', $items[0]['author_name']); } + /** + * 게시판 훅이 신원 판정(can_view_secret=false)만 하고 원문 필드 null 처리를 누락하더라도 + * 서비스가 그 권위 플래그로 payload 를 재확정한다 (KVE-2026-1914 A-2 이중 방어). + * + * 훅(get_by_ids)이 마스킹을 누락한 채 content/reply/attachments 를 실어 보내는 회귀를 + * 모사한다 — 서비스가 can_view_secret=false 를 신뢰해 content/reply/attachments 를 + * 다시 마스킹해야 원문이 새지 않는다. 자기 권한을 재계산하지 않으므로(플래그만 신뢰) + * 게이트 강도가 훅과 갈리지 않는다. + * + * @scenario layer=service, viewer=non_viewer + * + * @effects service_remasks_when_hook_omits_masking, service_remasks_title_with_shared_placeholder + */ + #[Test] + public function 훅이_마스킹을_누락해도_서비스가_can_view_secret_플래그로_원문을_재차단한다(): void + { + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-board'); + + $owner = $this->createUser(); // 조회자(비회원)와 다른 작성자 + $pivot = ProductInquiry::create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'board_post', + 'inquirable_id' => 555, + 'user_id' => $owner->id, + ]); + + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_settings', + fn ($defaults) => $defaults, + priority: 1 + ); + // 훅이 신원 판정만 하고 필드 마스킹을 누락한 상태(회귀)를 모사 — content/reply/attachments 가 원문 그대로 실려온다. + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_by_ids', + fn () => [[ + 'id' => $pivot->inquirable_id, + 'user_id' => $owner->id, + 'author_name' => '작성자', + 'title' => '비밀 문의 제목', + 'is_secret' => true, + 'can_view_secret' => false, + 'content' => '유출되면 안 되는 비밀 내용', + 'reply' => '유출되면 안 되는 답변', + 'attachments' => [['id' => 1, 'original_filename' => 'secret.pdf']], + ]], + priority: 1 + ); + + $response = $this->getJson( + "/api/modules/sirsoft-ecommerce/products/{$this->product->id}/inquiries" + ); + + $response->assertOk(); + + $items = $response->json('data.items'); + $this->assertCount(1, $items); + $this->assertTrue($items[0]['is_secret']); + $this->assertNull($items[0]['content'], '훅 누락 시에도 서비스가 content 를 재마스킹해야 합니다'); + $this->assertNull($items[0]['reply'], '훅 누락 시에도 서비스가 reply 를 재마스킹해야 합니다'); + $this->assertSame([], $items[0]['attachments'], '훅 누락 시에도 서비스가 attachments 를 비워야 합니다'); + // A2b: title 도 재마스킹 대상 — 훅이 실어 보낸 원문 제목이 유출되면 안 된다. + $this->assertNotSame('비밀 문의 제목', $items[0]['title'], '훅 누락 시에도 서비스가 title 원문을 유출하면 안 됩니다'); + $this->assertSame( + __('sirsoft-board::messages.post.secret_post_title'), + $items[0]['title'], + '훅 누락 시에도 서비스가 title 을 게시판 비밀글 플레이스홀더로 재마스킹해야 합니다' + ); + } + + /** + * 훅 payload 에 can_view_secret 키가 **아예 없으면** 서비스는 마스킹 쪽으로 닫힌다. + * + * `ProductInquiryService` 의 판정은 `($post['can_view_secret'] ?? false) === false` 라 + * 플래그 부재 시 fail-closed 다. 그런데 이 분기를 밟는 테스트가 없으면 `?? false` 를 + * `?? true`(fail-open)로 바꿔도 전 스위트가 green 이다 — 게시판 훅을 대체 구현한 + * 확장이 플래그를 실어 보내지 않는 순간 비밀 문의 원문이 그대로 나간다. + * + * 플래그를 명시 전달하는 위 테스트와 달리, 여기서는 키 자체를 생략한다. + * + * @scenario layer=service, viewer=non_viewer + * + * @effects service_fails_closed_when_flag_absent + */ + #[Test] + public function can_view_secret_키가_없으면_서비스가_마스킹_쪽으로_닫힌다(): void + { + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-board'); + + $owner = $this->createUser(); + $pivot = ProductInquiry::create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'board_post', + 'inquirable_id' => 556, + 'user_id' => $owner->id, + ]); + + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_settings', + fn ($defaults) => $defaults, + priority: 1 + ); + // can_view_secret 키를 의도적으로 생략 — 권위 플래그를 실어 보내지 않는 훅 구현을 모사한다. + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_by_ids', + fn () => [[ + 'id' => $pivot->inquirable_id, + 'user_id' => $owner->id, + 'author_name' => '작성자', + 'title' => '비밀 문의 제목', + 'is_secret' => true, + 'content' => '유출되면 안 되는 비밀 내용', + 'reply' => '유출되면 안 되는 답변', + 'attachments' => [['id' => 1, 'original_filename' => 'secret.pdf']], + ]], + priority: 1 + ); + + $response = $this->getJson( + "/api/modules/sirsoft-ecommerce/products/{$this->product->id}/inquiries" + ); + + $response->assertOk(); + + $items = $response->json('data.items'); + $this->assertCount(1, $items); + $this->assertTrue($items[0]['is_secret']); + $this->assertNull($items[0]['content'], '플래그 부재 시 content 는 마스킹되어야 합니다(fail-closed)'); + $this->assertNull($items[0]['reply'], '플래그 부재 시 reply 는 마스킹되어야 합니다(fail-closed)'); + $this->assertSame([], $items[0]['attachments'], '플래그 부재 시 attachments 는 비워져야 합니다'); + $this->assertSame( + __('sirsoft-board::messages.post.secret_post_title'), + $items[0]['title'], + '플래그 부재 시 title 도 플레이스홀더로 마스킹되어야 합니다' + ); + } + + /** + * 열람 권한이 있으면(can_view_secret=true) 비밀글 원문이 그대로 노출된다 (과잉 차단 회귀 방지). + * + * @scenario layer=service, viewer=owner + * + * @effects service_exposes_original_when_flag_true + */ + #[Test] + public function can_view_secret_true면_비밀글_원문이_노출된다(): void + { + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-board'); + + $owner = $this->createUser(); + $pivot = ProductInquiry::create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'board_post', + 'inquirable_id' => 556, + 'user_id' => $owner->id, + ]); + + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_settings', + fn ($defaults) => $defaults, + priority: 1 + ); + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_by_ids', + fn () => [[ + 'id' => $pivot->inquirable_id, + 'user_id' => $owner->id, + 'author_name' => '작성자', + 'title' => '비밀 문의 제목', + 'is_secret' => true, + 'can_view_secret' => true, + 'content' => '열람 가능한 비밀 내용', + 'reply' => '열람 가능한 답변', + 'attachments' => [['id' => 1, 'original_filename' => 'ok.pdf']], + ]], + priority: 1 + ); + + $items = $this->getJson( + "/api/modules/sirsoft-ecommerce/products/{$this->product->id}/inquiries" + )->assertOk()->json('data.items'); + + $this->assertCount(1, $items); + $this->assertSame('열람 가능한 비밀 내용', $items[0]['content']); + $this->assertSame('열람 가능한 답변', $items[0]['reply']); + $this->assertCount(1, $items[0]['attachments']); + // 과잉 차단 회귀 방지: 열람 권한이 있으면 title 도 원문 그대로여야 한다(A2b 재마스킹이 과하지 않음). + $this->assertSame('비밀 문의 제목', $items[0]['title']); + } + + /** + * 화면 목록은 저장소 쿼리 레벨 페이지네이션을 써야 한다 (#102 동형 결함 해소). + * + * 종전에는 전량 get() 후 PHP forPage() 잘라내기라, 문의가 늘수록 한 페이지를 여는 + * 것만으로 전 행이 적재됐다. 페이지 경계 정확성과 LIMIT 사용을 함께 고정한다. + */ + #[Test] + public function 문의_목록은_쿼리_레벨_페이지네이션을_사용하고_페이지_경계가_정확하다(): void + { + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-board'); + + for ($i = 1; $i <= 15; $i++) { + ProductInquiry::create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'board_post', + 'inquirable_id' => 1000 + $i, + 'user_id' => null, + ]); + } + + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_settings', + fn ($defaults) => $defaults, + priority: 1 + ); + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.get_by_ids', + fn ($_, $context) => array_map( + fn ($id) => ['id' => $id, 'title' => '문의 '.$id, 'is_secret' => false], + $context['ids'] ?? [] + ), + priority: 1 + ); + + $queries = []; + DB::listen(function ($query) use (&$queries): void { + $queries[] = $query->sql; + }); + + $response = $this->getJson( + "/api/modules/sirsoft-ecommerce/products/{$this->product->id}/inquiries?page=2&per_page=10" + ); + + $response->assertOk(); + + // 페이지 경계: 15건 중 두 번째 묶음(5건) + $items = $response->json('data.items'); + $this->assertCount(5, $items); + $this->assertSame(15, $response->json('data.meta.total')); + $this->assertSame(2, $response->json('data.meta.current_page')); + $this->assertSame(2, $response->json('data.meta.last_page')); + // id 내림차순 전순서 — 2페이지 첫 항목은 1005 (1015..1006 이 1페이지) + $this->assertSame(1005, $items[0]['post_id']); + + // 쿼리 레벨 페이지네이션 — 피벗 조회 SELECT 에 LIMIT 이 있어야 한다 + $limitedPivotSelects = array_filter( + $queries, + fn (string $sql): bool => str_contains($sql, 'product_inquiries') + && stripos(ltrim($sql), 'select') === 0 + && stripos($sql, 'limit') !== false + ); + $this->assertNotEmpty( + $limitedPivotSelects, + '문의 피벗 조회에 LIMIT 이 없습니다 — 전량 적재 후 PHP 잘라내기(#102 동형)입니다.' + ); + } + #[Test] public function board_slug_미설정_시_빈_목록과_inquiry_available_false를_반환한다(): void { @@ -183,6 +440,49 @@ class PublicProductInquiryControllerTest extends ModuleTestCase $response->assertUnauthorized(); } + /** + * 서비스가 클라이언트 IP 를 게시판 훅 payload 에 주입한다 (문의 생성 경로). + * + * 게시판 Listener 는 `request()->ip()` 를 참조하지 않고 payload 의 ip_address 만 + * 쓰도록 경계가 잡혀 있다(Listener 측 회귀 테스트 별도 존재). 그 경계가 성립하려면 + * **요청 경계인 이 서비스가 IP 를 실어 보내야** 하는데, 주입 측을 단언하는 테스트가 + * 없으면 주입 코드를 지워도 스위트가 green 이고 문의 IP 가 조용히 0.0.0.0 이 된다. + * + * @effects service_injects_client_ip_into_hook_payload + */ + #[Test] + public function 서비스가_문의_생성_훅_payload_에_클라이언트_ip_를_주입한다(): void + { + $user = $this->createUser(); + + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-board'); + + $capturedIp = null; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.create', + function ($result, $slug, $data) use (&$capturedIp) { + $capturedIp = $data['ip_address'] ?? null; + + return ['post_id' => 999, 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post']; + }, + priority: 1 + ); + + $this->actingAs($user) + ->withServerVariables(['REMOTE_ADDR' => '203.0.113.55']) + ->postJson( + "/api/modules/sirsoft-ecommerce/products/{$this->product->id}/inquiries", + ['content' => '문의 내용입니다 자세하게'] + ) + ->assertStatus(201); + + $this->assertSame( + '203.0.113.55', + $capturedIp, + '서비스가 요청 IP 를 게시판 훅 payload 로 주입해야 합니다' + ); + } + #[Test] public function 로그인_사용자는_문의를_작성할_수_있다(): void { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderShippingAddressTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderShippingAddressTest.php index 01142839..9c693a8a 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderShippingAddressTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderShippingAddressTest.php @@ -3,9 +3,11 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\User; use App\Models\User; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderModificationException; use Modules\Sirsoft\Ecommerce\Models\Order; use Modules\Sirsoft\Ecommerce\Models\OrderAddress; use Modules\Sirsoft\Ecommerce\Models\UserAddress; +use Modules\Sirsoft\Ecommerce\Services\OrderService; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; /** @@ -31,7 +33,7 @@ class OrderShippingAddressTest extends ModuleTestCase /** * 배송지 URL 생성 헬퍼 * - * @param int $orderId 주문 ID + * @param int $orderId 주문 ID * @return string */ private function url(int $orderId): string @@ -39,6 +41,11 @@ class OrderShippingAddressTest extends ModuleTestCase return "/api/modules/sirsoft-ecommerce/user/orders/{$orderId}/shipping-address"; } + /** + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects change_address_handler_uses_user_endpoint_without_token_header_for_member + */ public function test_update_shipping_address_with_manual_input(): void { $data = [ @@ -62,6 +69,11 @@ class OrderShippingAddressTest extends ModuleTestCase $this->assertEquals('서울시 서초구 서초대로 123', $this->shippingAddress->address); } + /** + * @scenario actor=member, change_mode=saved, address_region=domestic, e2e_browser=chromium + * + * @effects change_address_handler_saved_mode_sends_address_id_manual_sends_full_object + */ public function test_update_shipping_address_with_saved_address(): void { $savedAddress = UserAddress::factory()->create([ @@ -86,6 +98,90 @@ class OrderShippingAddressTest extends ModuleTestCase $this->assertEquals('부산시 해운대구', $this->shippingAddress->address); } + /** + * 저장된 해외 배송지를 선택해도 해외 주소가 보존되어야 한다. + * + * 종전에는 address_id 분기가 요청 데이터를 국내 6필드로만 재구성해, 해외 주소를 + * 고르면 해외 컬럼이 전부 null 로 덮이고 국내 컬럼도 빈 문자열이라 배송지가 + * 통째로 사라졌다. 응답은 200 이고 오류도 남지 않는다. + * + * @scenario actor=member, change_mode=saved, address_region=intl, e2e_browser=chromium + * + * @effects shipping_address_saved_intl_address_preserves_intl_columns + */ + public function test_update_shipping_address_with_saved_intl_address_preserves_intl_columns(): void + { + $savedAddress = UserAddress::factory()->create([ + 'user_id' => $this->user->id, + 'recipient_name' => 'John Doe', + 'recipient_phone' => '+1-202-555-0143', + 'country_code' => 'US', + 'zipcode' => null, + 'address' => null, + 'address_detail' => null, + 'address_line_1' => '1600 Pennsylvania Ave NW', + 'address_line_2' => 'Suite 200', + 'city' => 'Washington', + 'state' => 'DC', + 'postal_code' => '20500', + ]); + + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'address_id' => $savedAddress->id, + ]); + + $response->assertOk() + ->assertJsonPath('success', true); + + $this->shippingAddress->refresh(); + $this->assertEquals('US', $this->shippingAddress->recipient_country_code); + $this->assertEquals('1600 Pennsylvania Ave NW', $this->shippingAddress->address_line_1); + $this->assertEquals('Suite 200', $this->shippingAddress->address_line_2); + $this->assertEquals('Washington', $this->shippingAddress->intl_city); + $this->assertEquals('DC', $this->shippingAddress->intl_state); + $this->assertEquals('20500', $this->shippingAddress->intl_postal_code); + // 국내 컬럼은 비활성 측이므로 초기화된다 (NOT NULL → 빈 문자열) + $this->assertSame('', $this->shippingAddress->zipcode); + $this->assertSame('', $this->shippingAddress->address); + } + + /** + * 저장된 배송지를 선택하면서 함께 보낸 배송 메모가 반영되어야 한다. + * + * address_id 분기가 요청 데이터를 통째로 갈아치우면서 delivery_memo 가 조용히 + * 버려졌다 — 검증은 통과하고 응답도 200 이라 입력이 사라진 것이 드러나지 않는다. + * + * @scenario actor=member, change_mode=saved, address_region=domestic, e2e_browser=chromium + * + * @effects shipping_address_saved_mode_preserves_delivery_memo_from_request + */ + public function test_update_shipping_address_with_saved_address_keeps_delivery_memo(): void + { + $savedAddress = UserAddress::factory()->create([ + 'user_id' => $this->user->id, + 'recipient_name' => '박영희', + 'recipient_phone' => '010-1111-2222', + 'country_code' => 'KR', + 'zipcode' => '11111', + 'address' => '부산시 해운대구', + 'address_detail' => '마린시티', + ]); + + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'address_id' => $savedAddress->id, + 'delivery_memo' => '부재 시 문 앞에 놓아주세요', + ]); + + $response->assertOk() + ->assertJsonPath('success', true); + + $this->shippingAddress->refresh(); + $this->assertEquals('부산시 해운대구', $this->shippingAddress->address); + $this->assertEquals('부재 시 문 앞에 놓아주세요', $this->shippingAddress->delivery_memo); + } + public function test_update_shipping_address_fails_after_shipping(): void { $order = Order::factory()->shipping()->forUser($this->user)->create(); @@ -238,4 +334,281 @@ class OrderShippingAddressTest extends ModuleTestCase 'data' => ['order'], ]); } + + // ================================================================ + // 예외 → 상태코드 분리 (#104) + // + // 종전에는 모든 예외를 422 + "배송 전에만 변경 가능" 으로 뭉갰다. 인프라 장애까지 + // 입력 오류로 위장되어 장애 인지가 늦어진다. + // ================================================================ + + /** + * 도메인 예외(배송 전 상태 아님)는 422 + 기존 안내 메시지를 유지한다. + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects update_shipping_address_domain_exception_returns_422_with_cannot_modify_address + */ + public function test_domain_exception_returns_422_with_cannot_modify_address(): void + { + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('getDetail')->andReturn($this->order); + $mock->shouldReceive('updateShippingAddress') + ->andThrow(new OrderModificationException('배송 전 상태에서만 변경할 수 있습니다.')); + }); + + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김철수', + 'recipient_phone' => '010-9999-8888', + 'zipcode' => '54321', + 'address' => '서울시 서초구 서초대로 123', + ]); + + $response->assertStatus(422) + ->assertJsonPath('message', __('sirsoft-ecommerce::messages.orders.cannot_modify_address')); + } + + /** + * 도메인 예외가 아닌 예외(인프라/코드 결함)는 500 으로 구분되어야 한다. + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects update_shipping_address_infrastructure_exception_returns_500_not_422, update_shipping_address_500_response_excludes_exception_message + */ + public function test_infrastructure_exception_returns_500_not_422(): void + { + $this->mock(OrderService::class, function ($mock) { + $mock->shouldReceive('getDetail')->andReturn($this->order); + $mock->shouldReceive('updateShippingAddress') + ->andThrow(new \RuntimeException('SQLSTATE[23000]: Integrity constraint violation')); + }); + + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김철수', + 'recipient_phone' => '010-9999-8888', + 'zipcode' => '54321', + 'address' => '서울시 서초구 서초대로 123', + ]); + + $response->assertStatus(500); + + // 예외 원문이 응답에 실려서는 안 된다 + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } + + // ================================================================ + // 국내/해외 주소 조합 정합성 (F3) + // + // country_code 가 국내/해외를 가르는 기준축인데 nullable 이라, 해외 필드만 채우고 + // country_code 를 생략하면 검증을 통과한 뒤 서비스가 국내 분기로 떨어져 + // NOT NULL 컬럼(zipcode/address)에 null 을 쓰려다 SQL 무결성 위반이 났다. + // ================================================================ + + /** + * country_code 없이 해외 주소 조합만 보내면 국내 필수값 검증으로 차단되어야 한다. + * + * 수정 전에는 검증을 통과해 SQL 23000(zipcode cannot be null)이 발생했고, + * 그 인프라 예외가 "배송 전에만 변경 가능" 이라는 엉뚱한 도메인 메시지로 위장됐다. + * + * @scenario actor=member, change_mode=manual, address_region=intl_without_country_code, e2e_browser=chromium + * + * @effects shipping_address_country_code_is_the_axis_for_domestic_vs_intl_validation, shipping_address_intl_combination_without_country_code_returns_422_validation_errors + */ + public function test_intl_address_without_country_code_is_blocked_by_validation(): void + { + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_postal_code' => '94043', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['zipcode', 'address']); + } + + /** + * country_code=KR 을 명시하면서 zipcode 를 생략하면 검증으로 차단되어야 한다. + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects shipping_address_domestic_country_code_requires_zipcode_and_address + */ + public function test_domestic_country_code_requires_zipcode_and_address(): void + { + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김국내', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'KR', + 'address_line_1' => '1600 Amphitheatre Parkway', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['zipcode', 'address']); + } + + /** + * country_code 가 해외인데 해외 필수값이 비면 검증으로 차단되어야 한다. + * + * @scenario actor=member, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects shipping_address_foreign_country_code_requires_address_line_1_intl_city_intl_postal_code + */ + public function test_foreign_country_code_requires_intl_fields(): void + { + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['address_line_1', 'intl_city', 'intl_postal_code']); + } + + /** + * 해외 주소로 전환하면 해외 컬럼이 기록되고 국내 컬럼은 초기화되어야 한다. + * + * 초기화하지 않으면 구 국내 주소가 그대로 남아 배송지가 두 벌이 된다. + * + * @scenario actor=member, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects shipping_address_switching_to_intl_clears_domestic_columns_with_empty_string + */ + public function test_switching_to_intl_address_clears_domestic_columns(): void + { + $this->shippingAddress->update([ + 'recipient_country_code' => 'KR', + 'zipcode' => '54321', + 'address' => '서울시 서초구 서초대로 123', + 'address_detail' => '456동 789호', + ]); + + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_state' => 'CA', + 'intl_postal_code' => '94043', + ]); + + $response->assertOk(); + + $this->shippingAddress->refresh(); + $this->assertSame('US', $this->shippingAddress->recipient_country_code); + $this->assertSame('1600 Amphitheatre Parkway', $this->shippingAddress->address_line_1); + $this->assertSame('Mountain View', $this->shippingAddress->intl_city); + $this->assertSame('94043', $this->shippingAddress->intl_postal_code); + + // 국내 컬럼은 NOT NULL 이므로 빈 문자열로 초기화 + $this->assertSame('', $this->shippingAddress->zipcode); + $this->assertSame('', $this->shippingAddress->address); + $this->assertNull($this->shippingAddress->address_detail); + } + + /** + * 국내 → 해외 → 국내 왕복 시 매번 반대편 컬럼이 초기화되어야 한다. + * + * @scenario actor=member, change_mode=manual, address_region=intl, e2e_browser=chromium + * + * @effects shipping_address_switching_to_domestic_clears_intl_columns_with_null + */ + public function test_round_trip_between_domestic_and_intl_clears_opposite_columns(): void + { + // 1) 해외로 전환 + $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김해외', + 'recipient_phone' => '010-9999-8888', + 'country_code' => 'US', + 'address_line_1' => '1600 Amphitheatre Parkway', + 'intl_city' => 'Mountain View', + 'intl_postal_code' => '94043', + ])->assertOk(); + + // 2) 다시 국내로 전환 + $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김국내', + 'recipient_phone' => '010-1111-2222', + 'country_code' => 'KR', + 'zipcode' => '06236', + 'address' => '서울 강남구 테헤란로 1', + 'address_detail' => '10층', + ])->assertOk(); + + $this->shippingAddress->refresh(); + $this->assertSame('KR', $this->shippingAddress->recipient_country_code); + $this->assertSame('06236', $this->shippingAddress->zipcode); + $this->assertSame('서울 강남구 테헤란로 1', $this->shippingAddress->address); + + // 해외 컬럼은 nullable 이므로 null 로 초기화 + $this->assertNull($this->shippingAddress->address_line_1); + $this->assertNull($this->shippingAddress->intl_city); + $this->assertNull($this->shippingAddress->intl_state); + $this->assertNull($this->shippingAddress->intl_postal_code); + } + + /** + * 해외 배송지 주문에서 국내 주소를 직접 입력하면 그 주소가 저장되어야 한다. + * + * 배송지 변경 모달의 직접 입력 폼은 국내 필드만 제공하므로 country_code 를 보내지 + * 않는다. FormRequest 게이트는 "국가 미전송 = 국내" 로 판정해 zipcode/address 를 + * 필수로 검증하는데, 서비스가 기존 주소의 국가(US)를 승계해 해외 분기로 떨어지면 + * 방금 검증한 국내 입력을 통째로 버리고 빈 문자열을 쓴다 — 응답은 200 이고 + * 토스트도 성공이라 주소가 사라진 사실이 화면 재조회 전까지 드러나지 않는다. + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects shipping_address_manual_domestic_input_on_intl_order_is_saved_not_discarded + */ + public function test_manual_domestic_input_on_intl_order_saves_domestic_address(): void + { + // 주문 배송지를 해외 상태로 만든다 + $this->shippingAddress->update([ + 'recipient_country_code' => 'US', + 'zipcode' => '', + 'address' => '', + 'address_detail' => null, + 'address_line_1' => '1600 Amphitheatre Parkway', + 'address_line_2' => 'Building 42', + 'intl_city' => 'Mountain View', + 'intl_state' => 'CA', + 'intl_postal_code' => '94043', + ]); + + // 직접 입력 폼과 동일한 페이로드 — country_code 를 보내지 않는다 + $response = $this->actingAs($this->user) + ->putJson($this->url($this->order->id), [ + 'recipient_name' => '김국내', + 'recipient_phone' => '010-1111-2222', + 'zipcode' => '06134', + 'address' => '서울특별시 강남구 테헤란로 152', + 'address_detail' => '10층', + ]); + + $response->assertOk(); + + $this->shippingAddress->refresh(); + $this->assertSame('KR', $this->shippingAddress->recipient_country_code); + $this->assertSame('06134', $this->shippingAddress->zipcode); + $this->assertSame('서울특별시 강남구 테헤란로 152', $this->shippingAddress->address); + $this->assertSame('10층', $this->shippingAddress->address_detail); + + // 해외 컬럼은 초기화된다 + $this->assertNull($this->shippingAddress->address_line_1); + $this->assertNull($this->shippingAddress->address_line_2); + $this->assertNull($this->shippingAddress->intl_city); + $this->assertNull($this->shippingAddress->intl_state); + $this->assertNull($this->shippingAddress->intl_postal_code); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php index 9378da95..ce7791df 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php @@ -8,7 +8,9 @@ use App\Models\Permission; use App\Models\Role; use App\Models\User; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\URL; use Mockery; +use Mockery\MockInterface; use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\ReviewStatus; use Modules\Sirsoft\Ecommerce\Models\Order; @@ -19,6 +21,7 @@ use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; use Modules\Sirsoft\Ecommerce\Services\ProductReviewImageService; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; /** * 사용자 리뷰 이미지 API Feature 테스트 @@ -36,7 +39,7 @@ class ReviewImageControllerTest extends ModuleTestCase private ProductReview $review; - /** @var \Mockery\MockInterface&StorageInterface */ + /** @var MockInterface&StorageInterface */ private $storageMock; protected function setUp(): void @@ -351,4 +354,149 @@ class ReviewImageControllerTest extends ModuleTestCase // Then $response->assertUnauthorized(); } + + // ======================================== + // download() — 해시 기반 공개 서빙 (KVE-2026-1914 S-2) + // ======================================== + + /** + * @scenario resource=review_image, parent_state=restricted + * + * @effects hidden_review_image_download_blocked + */ + #[Test] + public function test_download_blocks_hidden_review_image(): void + { + // Given: 관리자가 숨긴(HIDDEN) 리뷰의 이미지 + $hiddenReview = ProductReview::factory()->create([ + 'product_id' => $this->product->id, + 'order_option_id' => $this->orderOption->id, + 'user_id' => $this->user->id, + 'status' => ReviewStatus::HIDDEN->value, + ]); + $image = ProductReviewImage::factory()->create([ + 'review_id' => $hiddenReview->id, + ]); + + // When: 해시로 이미지 다운로드 시도 + $response = $this->get( + "/api/modules/sirsoft-ecommerce/review-image/{$image->hash}" + ); + + // Then: 숨김 리뷰 이미지는 서빙 차단(404) + $response->assertNotFound(); + } + + /** + * @scenario resource=review_image, parent_state=public + * + * @effects visible_review_image_download_still_served + */ + #[Test] + public function test_download_serves_visible_review_image(): void + { + // Given: 전시중(VISIBLE) 리뷰의 이미지 + storage response mock + $image = ProductReviewImage::factory()->create([ + 'review_id' => $this->review->id, + ]); + $this->storageMock->allows('response')->andReturn( + new StreamedResponse(fn () => null, 200) + ); + + // When + $response = $this->get( + "/api/modules/sirsoft-ecommerce/review-image/{$image->hash}" + ); + + // Then: VISIBLE 리뷰 이미지는 상태 게이트를 통과(404 아님) + $this->assertNotSame(404, $response->getStatusCode(), 'VISIBLE 리뷰 이미지는 서빙되어야 합니다'); + } + + // ========================================== + // 서명 download URL (숨김 리뷰 <img> 썸네일 렌더 경로) + // + // 브라우저 <img src> 는 Authorization 헤더를 실을 수 없어, 숨김 게이트 도입 후 + // 관리자 리뷰 화면의 숨김 리뷰 이미지 썸네일이 무인증 요청 → 404 로 깨졌다. + // 게이트를 통과한 응답 직렬화(ProductReviewResource)가 한시 서명 URL 을 발급하고, + // 서빙 엔드포인트가 유효 서명을 허용한다 — 무서명 게이트는 종전과 동일하다. + // (게시판·페이지 첨부 preview 와 동일 설계) + // ========================================== + + /** + * 숨김 리뷰 이미지를 만들고 서명 download URL 을 돌려주는 헬퍼 + * + * @param int $minutes 유효 시간(분, 음수면 만료된 URL) + * @return string 상대경로 서명 URL + */ + private function hiddenImageSignedUrl(int $minutes = 30): string + { + $hiddenReview = ProductReview::factory()->create([ + 'product_id' => $this->product->id, + 'order_option_id' => $this->orderOption->id, + 'user_id' => $this->user->id, + 'status' => ReviewStatus::HIDDEN->value, + ]); + $image = ProductReviewImage::factory()->create([ + 'review_id' => $hiddenReview->id, + ]); + + return URL::temporarySignedRoute( + 'api.modules.sirsoft-ecommerce.review-image.download', + now()->addMinutes($minutes), + ['hash' => $image->hash], + absolute: false + ); + } + + /** + * 유효한 한시 서명 download URL 은 무인증(<img>) 요청도 숨김 게이트를 통과한다. + * + * @scenario resource=review_image, parent_state=restricted + * + * @effects hidden_review_image_download_allowed_with_valid_signature + */ + #[Test] + public function test_download_allows_hidden_review_image_with_valid_signature(): void + { + $this->storageMock->allows('response')->andReturn( + new StreamedResponse(fn () => null, 200) + ); + + $response = $this->get($this->hiddenImageSignedUrl()); + + $this->assertNotSame(404, $response->getStatusCode(), '유효 서명 URL 은 숨김 게이트를 통과해야 합니다'); + } + + /** + * 변조된 서명 download URL 은 종전과 동일하게 숨김 게이트에 차단된다 (404). + * + * @scenario resource=review_image, parent_state=restricted + * + * @effects hidden_review_image_download_blocked_with_tampered_signature + */ + #[Test] + public function test_download_blocks_hidden_review_image_with_tampered_signature(): void + { + // signature 쿼리 값의 마지막 8자를 뒤집어 변조한다 + $tampered = preg_replace_callback( + '/(signature=)([0-9a-f]+)/', + fn ($m) => $m[1].substr($m[2], 0, -8).strrev(substr($m[2], -8)), + $this->hiddenImageSignedUrl() + ); + + $this->get($tampered)->assertNotFound(); + } + + /** + * 만료된 서명 download URL 은 숨김 게이트에 차단된다 (404, 한시성 보장). + * + * @scenario resource=review_image, parent_state=restricted + * + * @effects hidden_review_image_download_blocked_with_expired_signature + */ + #[Test] + public function test_download_blocks_hidden_review_image_with_expired_signature(): void + { + $this->get($this->hiddenImageSignedUrl(minutes: -1))->assertNotFound(); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php index 240d190f..018962ce 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Controllers\User; use App\Extension\HookManager; +use App\Models\User; use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; @@ -20,7 +21,7 @@ use PHPUnit\Framework\Attributes\Test; */ class UserProductInquiryControllerTest extends ModuleTestCase { - private \App\Models\User $user; + private User $user; private Product $product; @@ -44,6 +45,7 @@ class UserProductInquiryControllerTest extends ModuleTestCase 'sirsoft-ecommerce.inquiry.update_reply', 'sirsoft-ecommerce.inquiry.delete_reply', 'sirsoft-ecommerce.inquiry.create', + 'sirsoft-ecommerce.inquiry.count_replies', 'sirsoft-ecommerce.inquiry.update', 'sirsoft-ecommerce.inquiry.get_settings', 'sirsoft-ecommerce.inquiry.get_by_ids', @@ -68,8 +70,15 @@ class UserProductInquiryControllerTest extends ModuleTestCase priority: 1 ); + // 잔여 답변 수 훅 모킹 — 기본값 0 (답변 삭제 시 답변완료 해제 경로) + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.count_replies', + fn () => 0, + priority: 1 + ); + $this->inquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, + 'user_id' => $this->user->id, 'product_id' => $this->product->id, 'is_answered' => false, ]); @@ -191,7 +200,34 @@ class UserProductInquiryControllerTest extends ModuleTestCase ); $response->assertOk(); - $this->assertDatabaseMissing('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); + // SoftDeletes 도입(#107 후속) — 게시판 복원 경로와의 대칭을 위해 피벗은 소프트 삭제된다 + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); + } + + /** + * SoftDeletes 계약 명시 — 삭제된 피벗 행은 DB 에 남되(deleted_at 세팅) + * 기본 스코프 조회에서는 제외되어야 한다. 게시판에서 질문 글을 복원하는 + * 경로가 피벗을 되살릴 수 있어야 하므로 하드 삭제가 아니다. + */ + #[Test] + public function 문의_삭제_시_피벗이_소프트_삭제된다(): void + { + $response = $this->actingAs($this->user) + ->deleteJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}" + ); + + $response->assertOk(); + + // 행 자체는 잔존 + deleted_at 세팅 + $this->assertDatabaseHas('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $this->inquiry->id]); + + // 기본 스코프에서는 보이지 않고, withTrashed 로만 조회된다 + $this->assertNull(ProductInquiry::find($this->inquiry->id)); + $trashed = ProductInquiry::withTrashed()->find($this->inquiry->id); + $this->assertNotNull($trashed); + $this->assertNotNull($trashed->deleted_at); } #[Test] @@ -237,11 +273,140 @@ class UserProductInquiryControllerTest extends ModuleTestCase $response->assertStatus(201); $this->assertDatabaseHas('ecommerce_product_inquiries', [ - 'id' => $this->inquiry->id, + 'id' => $this->inquiry->id, 'is_answered' => true, ]); } + /** + * 단일 답변 정책 (user 경로) — 이미 답변완료인 문의에는 재등록이 422 로 + * 거부되고, 거부 시 게시판 create 훅은 호출 자체가 없어야 한다. + * admin 경로와 동일 가드가 user 경로에도 대칭 적용되는지 고정한다. + */ + #[Test] + public function 이미_답변된_문의에_답변_재등록_시_422를_반환한다(): void + { + $manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); + $answeredInquiry = ProductInquiry::factory()->answered()->create([ + 'user_id' => $this->user->id, + 'product_id' => $this->product->id, + ]); + + // setUp 의 기본 모킹을 걷어내고 호출 횟수를 계수하는 훅으로 교체 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.create'); + $createCalls = 0; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.create', + function () use (&$createCalls) { + $createCalls++; + + return ['post_id' => 999, 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post']; + }, + priority: 1 + ); + + $response = $this->actingAs($manager) + ->postJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$answeredInquiry->id}/reply", + ['content' => '중복 답변 재등록 시도 내용입니다.'] + ); + + // user 컨트롤러는 operation_failed_reason(':reason') 래핑으로 사유 원문을 그대로 노출한다 + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-ecommerce::messages.inquiries.reply_already_exists'), + $response->json('message') + ); + $this->assertSame(0, $createCalls, '재등록 거부 시 게시판 create 훅은 호출되지 않아야 합니다.'); + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $answeredInquiry->id, + 'is_answered' => true, + ]); + } + + /** + * 경합 경로의 중복도 사유가 보존된다 (회귀 — 운영 실측 제보). + * + * 동시 등록 경합에서는 피벗 is_answered 가 아직 false 라 1차 방어를 통과하고, + * 게시판 리스너의 2차 방어(실데이터 기준)가 중복을 감지한다. 종전에는 리스너가 + * null 을 돌려줘 서비스가 훅 무응답과 구분하지 못했고, "답변 등록에 실패했습니다" + * (reply_failed, 500) 로 위장됐다. 리스너는 중복 마커(['duplicate' => true])를 + * 돌려주고 서비스는 이를 reply_already_exists(422) 로 변환해야 한다. + */ + #[Test] + public function 경합_중복_감지_시에도_이미_답변됨_사유가_노출된다(): void + { + $manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); + + // 피벗은 미답변(1차 방어 통과) — 게시판 실데이터에만 답변이 존재하는 경합 상태 재현 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.create'); + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.create', + fn () => ['duplicate' => true], + priority: 1 + ); + + $response = $this->actingAs($manager) + ->postJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '경합 중복 답변 등록 시도 내용입니다.'] + ); + + $response->assertStatus(422); + $this->assertSame( + __('sirsoft-ecommerce::messages.inquiries.reply_already_exists'), + $response->json('message'), + '경합 중복도 일반 실패가 아니라 "이미 등록된 답변" 사유로 안내되어야 합니다.' + ); + } + + /** + * 서비스가 클라이언트 IP 를 게시판 훅 payload 에 주입한다 (답변 경로). + * + * 게시판 Listener 는 `request()->ip()` 를 참조하지 않고 payload 의 ip_address 만 쓴다. + * 그 경계는 **요청 경계인 서비스가 IP 를 실어 보낼 때만** 성립하는데, 생성 경로 + * (`createInquiry`)와 답변 경로(`createReply`)는 서로 다른 메서드라 주입 코드도 각각 + * 있다. 생성 경로만 단언하면 답변 경로의 주입을 지워도 스위트가 green 이고 답변 IP 가 + * 조용히 0.0.0.0 으로 기록된다. + * + * @effects service_injects_client_ip_into_reply_hook_payload + */ + #[Test] + public function 서비스가_답변_훅_payload_에_클라이언트_ip_를_주입한다(): void + { + $manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); + + // setUp 의 기본 모킹을 걷어내고 payload 를 캡처하는 훅으로 교체한다. + HookManager::clearFilter('sirsoft-ecommerce.inquiry.create'); + + $capturedIp = null; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.create', + function ($result, $slug, $data) use (&$capturedIp) { + $capturedIp = $data['ip_address'] ?? null; + + return ['post_id' => 999, 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post']; + }, + priority: 1 + ); + + // 요청 IP 를 비-0.0.0.0 으로 세팅한다 — 기본 request 로는 폴백값과 구분되지 않아 + // 주입 코드를 지워도 단언이 통과한다(무증상 green). + $this->actingAs($manager) + ->withServerVariables(['REMOTE_ADDR' => '203.0.113.77']) + ->postJson( + "/api/modules/sirsoft-ecommerce/user/inquiries/{$this->inquiry->id}/reply", + ['content' => '답변 내용입니다 친절하게 작성'] + ) + ->assertStatus(201); + + $this->assertSame( + '203.0.113.77', + $capturedIp, + '서비스가 답변 작성 시에도 요청 IP 를 게시판 훅 payload 로 주입해야 합니다' + ); + } + // ======================================== // updateReply() — 답변 수정 (관리자 권한 필요) // ======================================== @@ -250,8 +415,8 @@ class UserProductInquiryControllerTest extends ModuleTestCase public function 권한_없는_사용자는_답변을_수정할_수_없다(): void { $answeredInquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, - 'product_id' => $this->product->id, + 'user_id' => $this->user->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -269,8 +434,8 @@ class UserProductInquiryControllerTest extends ModuleTestCase { $manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); $answeredInquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, - 'product_id' => $this->product->id, + 'user_id' => $this->user->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -291,8 +456,8 @@ class UserProductInquiryControllerTest extends ModuleTestCase public function 권한_없는_사용자는_답변을_삭제할_수_없다(): void { $answeredInquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, - 'product_id' => $this->product->id, + 'user_id' => $this->user->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -309,8 +474,8 @@ class UserProductInquiryControllerTest extends ModuleTestCase { $manager = $this->createAdminUser(['sirsoft-ecommerce.inquiries.update']); $answeredInquiry = ProductInquiry::factory()->create([ - 'user_id' => $this->user->id, - 'product_id' => $this->product->id, + 'user_id' => $this->user->id, + 'product_id' => $this->product->id, 'is_answered' => true, ]); @@ -321,7 +486,7 @@ class UserProductInquiryControllerTest extends ModuleTestCase $response->assertOk(); $this->assertDatabaseHas('ecommerce_product_inquiries', [ - 'id' => $answeredInquiry->id, + 'id' => $answeredInquiry->id, 'is_answered' => false, ]); } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Requests/CreateOrderDeadPgGuardTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Requests/CreateOrderDeadPgGuardTest.php new file mode 100644 index 00000000..d4e23386 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Requests/CreateOrderDeadPgGuardTest.php @@ -0,0 +1,274 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Http\Requests; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Validator; +use Modules\Sirsoft\Ecommerce\Http\Requests\Public\CreateOrderRequest; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Services\PaymentMethodResolver; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 죽은 PG 를 지정한 결제수단의 주문 제출 차단 (A2 서버 대칭 가드) + * + * 공개 응답에서 감추는 것만으로는 부족하다 — 프론트를 우회해 직접 제출하면 그대로 통과해 + * 결제창 없는 주문이 만들어진다. 검증 계층에서도 같은 판정을 적용한다. + * + * 이 가드는 `_orphaned`(수단 자체 고아)도 함께 막는다. `allValidIds()` 는 카탈로그 키를 + * 그대로 쓰므로 고아 수단이 통과하는 공백이 있었다 — 공개 #111 의 서버측 대칭이 여기서 완성된다. + * `allValidIds()` 자체는 건드리지 않는다(과거 주문 목록 필터가 같은 목록을 쓴다 — 조이면 + * 예전 수단으로 결제한 주문을 조회할 수 없게 된다). + */ +class CreateOrderDeadPgGuardTest extends ModuleTestCase +{ + 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); + } + } + + protected function tearDown(): void + { + if (File::isDirectory($this->storagePath)) { + File::cleanDirectory($this->storagePath); + } + + parent::tearDown(); + } + + /** + * 살아 있는 PG provider 를 레지스트리에 등록합니다. + * + * @param array<int, string> $ids provider ID 목록 + */ + private function registerPgProviders(array $ids): void + { + HookManager::addFilter( + 'sirsoft-ecommerce.payment.registered_pg_providers', + function (array $providers) use ($ids) { + foreach ($ids as $id) { + $providers[] = ['id' => $id, 'name' => strtoupper($id)]; + } + + return $providers; + } + ); + } + + /** + * order_settings 저장 파일을 구성합니다. + * + * @param array $paymentMethods 결제수단 배열 + * @param string|null $defaultPgProvider 기본 PG + */ + private function seedOrderSettings(array $paymentMethods, ?string $defaultPgProvider = null): void + { + File::ensureDirectoryExists($this->storagePath); + File::put($this->storagePath.'/order_settings.json', json_encode([ + 'default_pg_provider' => $defaultPgProvider, + 'payment_methods' => $paymentMethods, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + app(EcommerceSettingsService::class)->clearCache(); + app(PaymentMethodResolver::class)->flushCache(); + } + + /** + * payment_method 규칙만 떼어 검증합니다. + * + * @param string $methodId 결제수단 ID + * @return \Illuminate\Contracts\Validation\Validator 검증기 + */ + private function validatePaymentMethod(string $methodId) + { + $rules = (new CreateOrderRequest)->rules(); + + return Validator::make( + ['payment_method' => $methodId], + ['payment_method' => $rules['payment_method']] + ); + } + + /** + * 죽은 PG 를 지정한 수단의 주문 제출은 거부된다. (실패-먼저) + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_order_submission_rejected_422 + */ + public function test_dead_pg_method_submission_is_rejected(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'card', 'is_active' => true, 'pg_provider' => 'ghost_pg'], + ], 'kginicis'); + + $validator = $this->validatePaymentMethod('card'); + + $this->assertTrue($validator->fails(), '죽은 PG 를 지정한 결제수단이 검증을 통과했습니다.'); + $message = $validator->errors()->first('payment_method'); + $this->assertNotSame( + 'sirsoft-ecommerce::validation.order.payment_method_unavailable', + $message, + '다국어 키가 원문 그대로 노출되었습니다.' + ); + $this->assertNotEmpty($message); + } + + /** + * 카탈로그에서 사라진 고아 수단의 제출도 거부된다. (공개 #111 서버 대칭) + * + * @scenario pg_provider_state=live + * + * @effects orphaned_method_order_submission_rejected_422 + */ + public function test_orphaned_method_submission_is_rejected(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'zombie_pay', 'is_active' => true, 'pg_provider' => 'kginicis'], + ], 'kginicis'); + + $this->assertTrue( + $this->validatePaymentMethod('zombie_pay')->fails(), + '카탈로그에 없는 고아 결제수단이 검증을 통과했습니다.' + ); + } + + /** + * 살아 있는 PG 를 지정한 수단은 그대로 통과한다. (비회귀 pin) + * + * @scenario pg_provider_state=live + * + * @effects live_pg_order_submission_accepted + */ + public function test_live_pg_method_submission_passes(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'card', 'is_active' => true, 'pg_provider' => 'kginicis'], + ], 'kginicis'); + + $this->assertFalse( + $this->validatePaymentMethod('card')->fails(), + '정상 결제수단이 거부되었습니다.' + ); + } + + /** + * PG 미설정 수단(non-PG)은 그대로 통과한다. (비회귀 pin) + * + * @scenario pg_provider_state=none + * + * @effects live_pg_order_submission_accepted + */ + public function test_non_pg_method_submission_passes(): void + { + $this->registerPgProviders([]); + $this->seedOrderSettings([ + ['id' => 'dbank', 'is_active' => true], + ]); + + $this->assertFalse($this->validatePaymentMethod('dbank')->fails()); + } + + /** + * 죽은 PG 수단은 실제 주문 생성 엔드포인트에서도 422 로 거부된다. + * + * 위 테스트들은 `rules()` 배열을 떼어 검증한다 — 규칙 자체는 고정되지만, 그 규칙이 + * 라우트에 실제로 걸려 있는지는 말해 주지 않는다. FormRequest 를 컨트롤러가 타입힌트하지 + * 않으면 규칙은 살아 있고 요청만 그대로 통과한다. 그 연결을 여기서 고정한다. + * + * @scenario pg_provider_state=dead_own + * + * @effects dead_pg_order_submission_rejected_422 + */ + public function test_dead_pg_method_submission_is_rejected_over_http(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'card', 'is_active' => true, 'pg_provider' => 'ghost_pg'], + ], 'kginicis'); + + $this->postJson('/api/modules/sirsoft-ecommerce/user/orders', $this->orderPayload('card')) + ->assertStatus(422) + ->assertJsonValidationErrors(['payment_method']); + } + + /** + * 살아 있는 PG 수단은 결제수단 검증에서 걸리지 않는다. (거짓 양성 차단) + * + * 다른 필드(주소·상품)로 422 가 날 수는 있으나 `payment_method` 는 그 목록에 없어야 한다 — + * 그래야 위 테스트의 422 가 결제수단 때문임이 증명된다. + * + * @scenario pg_provider_state=live + * + * @effects live_pg_order_submission_accepted + */ + public function test_live_pg_method_is_not_flagged_over_http(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'card', 'is_active' => true, 'pg_provider' => 'kginicis'], + ], 'kginicis'); + + $response = $this->postJson('/api/modules/sirsoft-ecommerce/user/orders', $this->orderPayload('card')); + + $this->assertArrayNotHasKey( + 'payment_method', + (array) ($response->json('errors') ?? []), + '정상 PG 를 지정한 결제수단이 HTTP 검증에서 거부되었습니다.' + ); + } + + /** + * 주문 생성 요청 본문을 만듭니다. + * + * 결제수단 축만 보므로 나머지 필드는 형식만 갖춘다 — 다른 필드의 검증 실패는 + * `payment_method` 오류의 유무 판정에 영향을 주지 않는다. + * + * @param string $methodId 결제수단 ID + * @return array<string, mixed> 요청 본문 + */ + private function orderPayload(string $methodId): array + { + return [ + 'payment_method' => $methodId, + 'items' => [['product_id' => 1, 'quantity' => 1]], + 'receiver_name' => '홍길동', + 'receiver_phone' => '01012345678', + 'address' => '서울시 강남구', + 'address_detail' => '101호', + 'postcode' => '06234', + ]; + } + + /** + * 리졸버의 주문 가능 판정은 카탈로그 밖 ID 를 막지 않는다. (기존 계약 유지) + * + * @scenario pg_provider_state=live + * + * @effects unknown_method_id_keeps_legacy_contract + */ + public function test_resolver_allows_ids_outside_catalog(): void + { + $this->registerPgProviders(['kginicis']); + $this->seedOrderSettings([ + ['id' => 'card', 'is_active' => true, 'pg_provider' => 'kginicis'], + ], 'kginicis'); + + $this->assertTrue( + app(PaymentMethodResolver::class)->isOrderable('some_unknown_id'), + '카탈로그 밖 ID 판정이 기존 계약보다 좁아졌습니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestCancelItemValidationTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestCancelItemValidationTest.php new file mode 100644 index 00000000..7a0cede1 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestCancelItemValidationTest.php @@ -0,0 +1,124 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Order; + +use Illuminate\Support\Facades\Hash; +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\ClaimReason; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 비회원 주문취소 per-item 검증 회귀 테스트 (N-1) + * + * 비회원 부분취소도 회원판 CancelOrderRequest 와 동일 강도로 각 항목이 대상 주문에 + * 속하는지, 취소 수량이 보유 수량을 넘지 않는지 검증해야 한다. + */ +class GuestCancelItemValidationTest extends ModuleTestCase +{ + private const PASSWORD = 'guest12'; + + private const PHONE = '010-1234-5678'; + + /** + * 취소 가능한 비회원 주문 + 유효 토큰을 만들어 [주문, 토큰]을 반환합니다. + * + * @return array{0: Order, 1: string} + */ + private function makeGuestOrderWithToken(string $orderNumber): array + { + $order = Order::factory()->forGuest()->create([ + 'order_number' => $orderNumber, + 'order_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'guest_lookup_password_hash' => Hash::make(self::PASSWORD), + ]); + + OrderAddress::factory()->shipping()->forOrder($order)->create([ + 'orderer_phone' => self::PHONE, + ]); + + /** @var GuestOrderAuthService $service */ + $service = app(GuestOrderAuthService::class); + $token = $service->authenticate($order->order_number, self::PHONE, self::PASSWORD, '10.0.0.2')['token']; + + return [$order, $token]; + } + + /** + * 사용자가 선택 가능한 refund 사유 코드를 반환합니다. + */ + private function refundReasonCode(): string + { + return (string) ClaimReason::where('type', 'refund') + ->where('is_active', true) + ->where('is_user_selectable', true) + ->value('code'); + } + + private function cancelUrl(Order $order): string + { + return "/api/modules/sirsoft-ecommerce/guest/orders/{$order->order_number}/cancel"; + } + + /** + * 다른 주문에 속한 옵션 id 로 부분취소를 시도하면 422. + * + * @scenario case=guest_cancel_validation + * + * @effects guest_cancel_items_validated_per_item + */ + public function test_foreign_option_id_is_rejected_422(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-GC-1'); + + $otherOrder = Order::factory()->forGuest()->create(['order_number' => 'ORD-GC-OTHER']); + $foreignOption = OrderOption::factory()->forOrder($otherOrder)->create([ + 'option_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'quantity' => 3, + ]); + + $response = $this->postJson( + $this->cancelUrl($order), + [ + 'reason' => $this->refundReasonCode(), + 'items' => [['order_option_id' => $foreignOption->id, 'cancel_quantity' => 1]], + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['items.0.order_option_id']); + } + + /** + * 보유 수량을 초과하는 부분취소 수량은 422. + * + * @scenario case=guest_cancel_validation + * + * @effects guest_cancel_items_validated_per_item + */ + public function test_over_quantity_is_rejected_422(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-GC-2'); + + $option = OrderOption::factory()->forOrder($order)->create([ + 'option_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'quantity' => 2, + ]); + + $response = $this->postJson( + $this->cancelUrl($order), + [ + 'reason' => $this->refundReasonCode(), + 'items' => [['order_option_id' => $option->id, 'cancel_quantity' => 5]], + ], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['items.0.cancel_quantity']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestConfirmOptionGateTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestConfirmOptionGateTest.php new file mode 100644 index 00000000..c9af13c5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestConfirmOptionGateTest.php @@ -0,0 +1,237 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Order; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\Hash; +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Exceptions\OrderOptionNotConfirmableException; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; +use Modules\Sirsoft\Ecommerce\Services\OrderService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 구매확정 상태 게이트 회귀 테스트 (㉙) + * + * OrderService::confirmOption 자체에 상태 가드가 내장되어 호출자(회원/비회원/훅)와 무관하게 + * 강제된다: 이미 확정된 옵션 재확정 차단, 확정 가능 상태(confirmable_statuses) 미포함 차단. + * 비회원 경로는 이 예외를 422 로 응답한다. + */ +class GuestConfirmOptionGateTest extends ModuleTestCase +{ + private const PASSWORD = 'guest12'; + + private const PHONE = '010-1234-5678'; + + /** + * 테스트용 주문을 생성합니다 (금액 필드 포함 — 확정 후속 훅 안정성). + * + * @param array<string, mixed> $overrides 오버라이드 속성 + */ + private function makeOrder(array $overrides = []): Order + { + return Order::factory()->create(array_merge([ + 'order_status' => OrderStatusEnum::DELIVERED, + 'subtotal_amount' => 30000, + 'total_amount' => 30000, + 'total_paid_amount' => 30000, + 'total_cancelled_amount' => 0, + 'cancellation_count' => 0, + 'promotions_applied_snapshot' => [], + 'shipping_policy_applied_snapshot' => [], + ], $overrides)); + } + + /** + * 테스트용 주문 옵션을 생성합니다. + */ + private function makeOption(Order $order, OrderStatusEnum $status): OrderOption + { + return OrderOption::factory()->forOrder($order)->create([ + 'quantity' => 1, + 'unit_price' => 15000, + 'subtotal_price' => 15000, + 'subtotal_paid_amount' => 15000, + 'option_status' => $status, + ]); + } + + /** + * 비회원 주문 + 유효 토큰을 만들어 [주문, 토큰]을 반환합니다. + * + * @return array{0: Order, 1: string} + */ + private function makeGuestOrderWithToken(string $orderNumber, OrderStatusEnum $status = OrderStatusEnum::DELIVERED): array + { + $order = $this->makeOrder([ + 'order_number' => $orderNumber, + 'order_status' => $status, + 'user_id' => null, + 'guest_lookup_password_hash' => Hash::make(self::PASSWORD), + 'is_first_order' => false, + ]); + + OrderAddress::factory()->shipping()->forOrder($order)->create([ + 'orderer_phone' => self::PHONE, + ]); + + /** @var GuestOrderAuthService $service */ + $service = app(GuestOrderAuthService::class); + $token = $service->authenticate($order->order_number, self::PHONE, self::PASSWORD, '10.0.0.3')['token']; + + return [$order, $token]; + } + + private function guestConfirmUrl(Order $order, int $optionId): string + { + return "/api/modules/sirsoft-ecommerce/guest/orders/{$order->order_number}/options/{$optionId}/confirm"; + } + + private function memberConfirmUrl(int $orderId, int $optionId): string + { + return "/api/modules/sirsoft-ecommerce/user/orders/{$orderId}/options/{$optionId}/confirm"; + } + + // ============ 서비스 계층 가드 (호출자 무관) ============ + + /** + * 이미 확정된 옵션을 다시 확정하면 도메인 예외를 던진다. + * + * @scenario case=confirm_service_gate + * + * @effects confirm_rejected_unless_confirmable_status, double_confirm_rejected + */ + public function test_service_rejects_already_confirmed_option(): void + { + $order = $this->makeOrder(['order_status' => OrderStatusEnum::CONFIRMED]); + $option = $this->makeOption($order, OrderStatusEnum::CONFIRMED); + + try { + app(OrderService::class)->confirmOption($order, $option); + $this->fail('OrderOptionNotConfirmableException 이 발생해야 한다'); + } catch (OrderOptionNotConfirmableException $e) { + $this->assertSame('order_option_already_confirmed', $e->getReason()); + } + } + + /** + * 확정 가능 상태(shipping/delivered)가 아닌 옵션(preparing)은 확정이 거부된다. + * + * @scenario case=confirm_service_gate + * + * @effects confirm_rejected_unless_confirmable_status + */ + public function test_service_rejects_non_confirmable_status(): void + { + $order = $this->makeOrder(['order_status' => OrderStatusEnum::PREPARING]); + $option = $this->makeOption($order, OrderStatusEnum::PREPARING); + + try { + app(OrderService::class)->confirmOption($order, $option); + $this->fail('OrderOptionNotConfirmableException 이 발생해야 한다'); + } catch (OrderOptionNotConfirmableException $e) { + $this->assertSame('order_option_not_confirmable', $e->getReason()); + } + } + + /** + * 확정 가능 상태(delivered)는 확정되고 after_confirm 훅은 1회만 발화한다. + * 이후 재확정은 already_confirmed 로 거부되어 훅이 다시 발화하지 않는다. + * + * @scenario case=confirm_service_gate + * + * @effects confirm_hook_fires_once + */ + public function test_service_confirms_delivered_and_fires_after_confirm_hook_once(): void + { + $hookCount = 0; + HookManager::addAction( + 'sirsoft-ecommerce.order-option.after_confirm', + function () use (&$hookCount) { + $hookCount++; + } + ); + + $order = $this->makeOrder(['order_status' => OrderStatusEnum::DELIVERED]); + $option = $this->makeOption($order, OrderStatusEnum::DELIVERED); + + $service = app(OrderService::class); + + $service->confirmOption($order, $option); + $this->assertSame(1, $hookCount); + + // 재확정 시도 → 예외 + 훅 미발화 + $option->refresh(); + try { + $service->confirmOption($order->fresh(), $option); + $this->fail('재확정은 거부되어야 한다'); + } catch (OrderOptionNotConfirmableException $e) { + $this->assertSame('order_option_already_confirmed', $e->getReason()); + } + + $this->assertSame(1, $hookCount); + } + + // ============ 비회원 HTTP 경로 ============ + + /** + * 비회원: 배송 전(preparing) 옵션 확정 시 422. + * + * @scenario case=confirm_guest_http + * + * @effects confirm_rejected_unless_confirmable_status + */ + public function test_guest_confirm_preparing_returns_422(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-CF-1', OrderStatusEnum::PREPARING); + $option = $this->makeOption($order, OrderStatusEnum::PREPARING); + + $this->postJson($this->guestConfirmUrl($order, $option->id), [], ['X-Guest-Order-Token' => $token]) + ->assertStatus(422); + } + + /** + * 비회원: 확정 가능 옵션의 이중 확정 시 두 번째 요청은 422. + * + * @scenario case=confirm_guest_http + * + * @effects double_confirm_rejected + */ + public function test_guest_double_confirm_returns_422_on_second(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-CF-2', OrderStatusEnum::DELIVERED); + $option = $this->makeOption($order, OrderStatusEnum::DELIVERED); + + $this->postJson($this->guestConfirmUrl($order, $option->id), [], ['X-Guest-Order-Token' => $token]) + ->assertStatus(200); + + $this->postJson($this->guestConfirmUrl($order, $option->id), [], ['X-Guest-Order-Token' => $token]) + ->assertStatus(422); + } + + // ============ 회원 HTTP 경로 ============ + + /** + * 회원: 배송 전(preparing) 옵션 확정 시 422. + * + * @scenario case=confirm_member_http + * + * @effects confirm_rejected_unless_confirmable_status + */ + public function test_member_confirm_preparing_returns_422(): void + { + $user = $this->createUser(); + $order = $this->makeOrder([ + 'user_id' => $user->id, + 'order_status' => OrderStatusEnum::PREPARING, + ]); + $option = $this->makeOption($order, OrderStatusEnum::PREPARING); + + $this->actingAs($user) + ->postJson($this->memberConfirmUrl($order->id, $option->id)) + ->assertStatus(422); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Payment/ExtensionPaymentMethodRegressionTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Payment/ExtensionPaymentMethodRegressionTest.php index faa1628d..3d02eac5 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Payment/ExtensionPaymentMethodRegressionTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Payment/ExtensionPaymentMethodRegressionTest.php @@ -551,4 +551,99 @@ class ExtensionPaymentMethodRegressionTest extends ModuleTestCase $this->assertTrue($payment->needsPgProvider()); $this->assertFalse($payment->isCardPayment()); } + + // ───────────────────────────────────────────────────────────── + // 죽은 PG 차단(A2) 과의 비회귀 — 살아있는 확장 수단은 걸리지 않아야 한다 + // ───────────────────────────────────────────────────────────── + + /** + * 살아있는 확장 결제수단은 죽은-PG 판정에 걸리지 않는다. (비회귀 pin) + * + * 죽은 PG 를 가리키는 결제수단을 주문서에서 걷어내는 가드가 들어왔다. 그 판정이 + * 헐거우면 정상 등록된 간편결제 수단까지 함께 사라지는데, 이때 오류도 경고도 + * 남지 않는다 — 결제수단 목록에서 조용히 빠질 뿐이다. 이 파일이 지키는 + * "확장 결제수단 1급 시민" 계약이 그 방향으로 깨지는 것을 막는다. + * + * @scenario method_kind=extension, pg_provider_state=live + * + * @effects live_pg_method_remains_visible, extension_method_survives_dead_pg_guard + */ + #[Test] + public function live_extension_payment_method_is_not_flagged_as_dead_pg(): void + { + $settingsService = app(EcommerceSettingsService::class); + + $methods = $settingsService->getAllSettings()['order_settings']['payment_methods'] ?? []; + $extensionMethod = collect($methods)->firstWhere('id', self::EXT_METHOD); + + $this->assertNotNull($extensionMethod, '확장 결제수단이 병합 결과에 없습니다.'); + $this->assertArrayNotHasKey( + '_orphaned_pg', + $extensionMethod, + '살아있는 PG 를 가리키는 확장 결제수단이 죽은-PG 로 표시됐습니다.' + ); + + $publicMethods = $settingsService->getPublicPaymentSettings()['payment_methods'] ?? []; + $this->assertContains( + self::EXT_METHOD, + array_column($publicMethods, 'id'), + '살아있는 확장 결제수단이 공개 응답에서 사라졌습니다 — 주문서에서 결제수단이 통째로 빠집니다.' + ); + } + + /** + * `pg_locked` 수단도 죽은-PG 판정에 특례가 없다. (비회귀 pin) + * + * `pg_locked` 는 저장값이 카탈로그 선언을 덮지 못한다는 뜻이지, 그 선언이 가리키는 + * PG 가 사라져도 결제 가능하다는 뜻이 아니다. 특례를 두면 그 수단은 결제창 없이 + * 주문완료로 넘어간다. + * + * @scenario method_kind=extension, pg_provider_state=dead_own + * + * @effects dead_pg_method_flagged_for_admin, dead_pg_method_hidden_from_checkout, pg_locked_method_has_no_dead_pg_exemption + */ + #[Test] + public function pg_locked_extension_method_has_no_dead_pg_exemption(): void + { + // 수단 카탈로그는 남아 있으나 그 수단이 지목한 PG 만 레지스트리에서 사라진 상태 + HookManager::resetAll(); + HookManager::addFilter( + 'sirsoft-ecommerce.settings.filter_available_payment_methods', + fn (array $methods) => array_merge($methods, [[ + 'id' => self::EXT_METHOD, + 'name' => ['ko' => '네이버페이', 'en' => 'Naver Pay'], + 'description' => ['ko' => '', 'en' => ''], + 'icon' => 'credit-card', + 'source' => 'plugin:sirsoft-pay_nhnkcp', + 'defaults' => [ + 'pg_provider' => self::EXT_PG, + 'pg_locked' => true, + 'needs_pg' => true, + 'refund_method' => 'pg', + 'is_active' => true, + 'min_order_amount' => 0, + 'stock_deduction_timing' => 'payment_complete', + 'mileage_deduction_timing' => 'payment_complete', + ], + ]]) + ); + + $settingsService = app(EcommerceSettingsService::class); + + $methods = $settingsService->getAllSettings()['order_settings']['payment_methods'] ?? []; + $extensionMethod = collect($methods)->firstWhere('id', self::EXT_METHOD); + + $this->assertNotNull($extensionMethod, '확장 결제수단이 병합 결과에 없습니다.'); + $this->assertTrue( + $extensionMethod['_orphaned_pg'] ?? false, + 'pg_locked 수단이 죽은 PG 를 가리키는데 관리자 화면에 표시되지 않습니다.' + ); + + $publicMethods = $settingsService->getPublicPaymentSettings()['payment_methods'] ?? []; + $this->assertNotContains( + self::EXT_METHOD, + array_column($publicMethods, 'id'), + 'pg_locked 수단이 죽은 PG 를 가리키는데 주문서에 남았습니다 — 결제창 없이 주문완료로 넘어갑니다.' + ); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Refund/GuestRefundEstimateValidationTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Refund/GuestRefundEstimateValidationTest.php new file mode 100644 index 00000000..7e0fe561 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Refund/GuestRefundEstimateValidationTest.php @@ -0,0 +1,107 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Refund; + +use Illuminate\Support\Facades\Hash; +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 비회원 환불예상 per-item 검증 회귀 테스트 (㉒) + * + * 비회원 환불예상(estimate-refund)도 회원판과 동일 강도로 각 항목이 대상 주문에 + * 속하는지, 취소 수량이 보유 수량을 넘지 않는지 검증해야 한다. + */ +class GuestRefundEstimateValidationTest extends ModuleTestCase +{ + private const PASSWORD = 'guest12'; + + private const PHONE = '010-1234-5678'; + + /** + * 비회원 주문과 유효한 조회 토큰을 만들어 [주문, 토큰]을 반환합니다. + * + * @return array{0: Order, 1: string} + */ + private function makeGuestOrderWithToken(string $orderNumber): array + { + $order = Order::factory()->forGuest()->create([ + 'order_number' => $orderNumber, + 'order_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'guest_lookup_password_hash' => Hash::make(self::PASSWORD), + ]); + + OrderAddress::factory()->shipping()->forOrder($order)->create([ + 'orderer_phone' => self::PHONE, + ]); + + /** @var GuestOrderAuthService $service */ + $service = app(GuestOrderAuthService::class); + $token = $service->authenticate($order->order_number, self::PHONE, self::PASSWORD, '10.0.0.1')['token']; + + return [$order, $token]; + } + + private function estimateUrl(Order $order): string + { + return "/api/modules/sirsoft-ecommerce/guest/orders/{$order->order_number}/estimate-refund"; + } + + /** + * 다른 주문에 속한 옵션 id 를 넘기면 422 (스코프 위반). + * + * @scenario case=guest_refund_estimate_validation + * + * @effects guest_refund_items_validated_per_item + */ + public function test_foreign_option_id_is_rejected_422(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-EST-1'); + + // 다른 주문의 옵션 + $otherOrder = Order::factory()->forGuest()->create(['order_number' => 'ORD-EST-OTHER']); + $foreignOption = OrderOption::factory()->forOrder($otherOrder)->create([ + 'option_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'quantity' => 3, + ]); + + $response = $this->postJson( + $this->estimateUrl($order), + ['items' => [['order_option_id' => $foreignOption->id, 'cancel_quantity' => 1]]], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['items.0.order_option_id']); + } + + /** + * 보유 수량을 초과하는 취소 수량은 422. + * + * @scenario case=guest_refund_estimate_validation + * + * @effects guest_refund_items_validated_per_item + */ + public function test_over_quantity_is_rejected_422(): void + { + [$order, $token] = $this->makeGuestOrderWithToken('ORD-EST-2'); + + $option = OrderOption::factory()->forOrder($order)->create([ + 'option_status' => OrderStatusEnum::PAYMENT_COMPLETE, + 'quantity' => 2, + ]); + + $response = $this->postJson( + $this->estimateUrl($order), + ['items' => [['order_option_id' => $option->id, 'cancel_quantity' => 5]]], + ['X-Guest-Order-Token' => $token] + ); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['items.0.cancel_quantity']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Review/ProductReviewProductIdScopeTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Review/ProductReviewProductIdScopeTest.php new file mode 100644 index 00000000..f6385d06 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Review/ProductReviewProductIdScopeTest.php @@ -0,0 +1,65 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Review; + +use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Services\ProductReviewService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 리뷰 product_id 스코프 회귀 테스트 (③) + * + * 리뷰 저장 시 product_id 는 클라이언트 payload 가 아니라 확정된 주문 옵션에서 + * 도출되어야 한다. payload 의 product_id 를 신뢰하면 소유하지 않은 상품에 리뷰를 + * 붙일 수 있다. + */ +class ProductReviewProductIdScopeTest extends ModuleTestCase +{ + /** + * 상품 A 의 확정 옵션으로 product_id=B 를 지정해도 리뷰는 옵션의 상품(A)로 저장된다. + * + * @scenario case=review_product_scope + * + * @effects review_product_id_derived_from_order_option + */ + public function test_review_product_id_is_derived_from_order_option_not_payload(): void + { + $user = $this->createUser(); + + $productA = Product::factory()->create(); + $productB = Product::factory()->create(); + + $order = Order::factory()->create(['user_id' => $user->id]); + $option = OrderOption::factory()->forOrder($order)->create([ + 'product_id' => $productA->id, + 'option_status' => OrderStatusEnum::CONFIRMED, + 'confirmed_at' => now(), + 'quantity' => 1, + ]); + + /** @var ProductReviewService $service */ + $service = app(ProductReviewService::class); + + $review = $service->createReview($user->id, [ + 'order_option_id' => $option->id, + // 클라이언트가 다른 상품(B)의 id 를 주입 — 무시되어야 한다 + 'product_id' => $productB->id, + 'rating' => 5, + 'content' => str_repeat('좋은 상품입니다 ', 3), + 'content_mode' => 'text', + ]); + + // 저장된 product_id 는 옵션의 상품(A)여야 한다 (payload 의 B 가 아니라) + $this->assertSame($productA->id, $review->product_id); + $this->assertNotSame($productB->id, $review->product_id); + + $this->assertDatabaseHas('ecommerce_product_reviews', [ + 'id' => $review->id, + 'order_option_id' => $option->id, + 'product_id' => $productA->id, + ]); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/SearchPreset/SearchPresetPermissionTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/SearchPreset/SearchPresetPermissionTest.php new file mode 100644 index 00000000..3983627c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/SearchPreset/SearchPresetPermissionTest.php @@ -0,0 +1,89 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\SearchPreset; + +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 검색 프리셋 라우트 권한 회귀 테스트 (N-6) + * + * presets 라우트 4종은 형제 그룹(brands/product-labels) 규약대로 products 권한 + * 미들웨어로 보호되어야 한다. index=products.read, store/update/destroy=products.update. + */ +class SearchPresetPermissionTest extends ModuleTestCase +{ + private const INDEX_URL = '/api/modules/sirsoft-ecommerce/admin/presets'; + + /** + * products.read 권한이 없는 관리자는 프리셋 목록 조회가 403. + * + * @scenario case=presets_permission + * + * @effects presets_require_product_permission + */ + public function test_index_forbidden_without_products_read(): void + { + $admin = $this->createAdminUser(); + + $this->actingAs($admin, 'sanctum') + ->getJson(self::INDEX_URL) + ->assertStatus(403); + } + + /** + * products.read 권한이 있는 관리자는 프리셋 목록 조회가 200. + * + * @scenario case=presets_permission + * + * @effects presets_require_product_permission + */ + public function test_index_allowed_with_products_read(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.products.read']); + + $this->actingAs($admin, 'sanctum') + ->getJson(self::INDEX_URL) + ->assertStatus(200); + } + + /** + * products.update 권한이 없는 관리자는 프리셋 생성이 403. + * + * @scenario case=presets_permission + * + * @effects presets_require_product_permission + */ + public function test_store_forbidden_without_products_update(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.products.read']); + + $this->actingAs($admin, 'sanctum') + ->postJson(self::INDEX_URL, [ + 'name' => '내 프리셋', + 'conditions' => ['status' => 'active'], + ]) + ->assertStatus(403); + } + + /** + * products.update 권한이 있는 관리자는 프리셋 생성이 허용된다. + * + * @scenario case=presets_permission + * + * @effects presets_require_product_permission + */ + public function test_store_allowed_with_products_update(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.products.update']); + + $response = $this->actingAs($admin, 'sanctum') + ->postJson(self::INDEX_URL, [ + 'name' => '내 프리셋', + 'conditions' => ['status' => 'active'], + ]); + + // 권한 게이트를 통과했으므로 403 이 아니어야 하며, 정상 생성(201)된다. + $this->assertNotSame(403, $response->status()); + $response->assertStatus(201); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php new file mode 100644 index 00000000..2a297334 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php @@ -0,0 +1,258 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Security; + +use App\Enums\PermissionType; +use App\Helpers\PermissionHelper; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Modules\Sirsoft\Ecommerce\Models\ExtraFeeTemplate; +use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy; +use Modules\Sirsoft\Ecommerce\Services\ExtraFeeTemplateService; +use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + +/** + * 서비스 계층 스코프 게이트 재적용 (KVE-2026-1919 형제) + * + * `PermissionMiddleware` 는 라우트 파라미터가 Model 로 resolve 될 때만 스코프를 검사하고, + * 아니면 "목록 엔드포인트" 로 보아 건너뛴다. 이 모듈에는 두 형태가 모두 있었다: + * + * 1. 정적 경로 — `bulk-*` 처럼 리소스 파라미터 자체가 없음 + * 2. 파라미터명 불일치 — 권한은 `shippingPolicy`/`coupon` 을 가리키는데 라우트는 `{id}` + * 라 바인딩이 안 일어남. 이쪽은 **상세 경로까지** 무가드였다. + * + * 조사 시점 이 모듈 전체의 `checkScopeAccess`/`filterByScope` 호출은 **0건**이었다 — + * 미들웨어가 스킵되면 어떤 2차 방어도 없었다는 뜻이다. + * + * 대표 경로(상세 1 + 일괄 1)를 고정한다. 나머지 서비스도 같은 trait 을 쓴다. + */ +class ServiceScopeReapplyTest extends ModuleTestCase +{ + /** + * 지정한 스코프로 권한을 보유한 관리자를 만듭니다. + * + * @param string $identifier 권한 식별자 + * @param string|null $scope 스코프 유형 (self / role / null=전체) + * @return User 생성된 관리자 + */ + private function adminWithScope(string $identifier, ?string $scope): User + { + $user = User::factory()->create(); + + $role = Role::create([ + 'identifier' => 'scoped-'.$user->id.'-'.uniqid(), + 'name' => ['ko' => '스코프 관리자', 'en' => 'Scoped Admin'], + ]); + $user->roles()->attach($role->id); + + foreach (['admin.access', $identifier] as $key) { + $permission = Permission::firstOrCreate( + ['identifier' => $key], + ['name' => ['ko' => $key, 'en' => $key], 'type' => PermissionType::Admin] + ); + + if ($key === $identifier) { + // 스코프 대상 권한임을 명시 — 미들웨어가 상세 경로에서 쓰는 것과 같은 메타데이터 + $permission->update(['resource_route_key' => 'shippingPolicy', 'owner_key' => 'created_by']); + } + + $role->permissions()->syncWithoutDetaching([ + $permission->id => ['scope_type' => $key === $identifier ? $scope : null], + ]); + } + + // PermissionHelper 정적 캐시 초기화 (다른 액터의 판정이 남지 않도록) + $reflection = new \ReflectionClass(PermissionHelper::class); + $prop = $reflection->getProperty('permissionCache'); + $prop->setAccessible(true); + $prop->setValue(null, []); + + return $user->fresh(); + } + + /** + * @scenario route_shape=param_name_mismatch + * + * @effects detail_route_reapplies_scope_gate_when_binding_absent + */ + public function test_self_scoped_admin_cannot_update_others_shipping_policy(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.shipping-policies.update', 'self'); + $owner = User::factory()->create(); + $this->actingAs($actor); + + $policy = $this->makePolicy($owner->id); + + $this->expectException(AccessDeniedHttpException::class); + + app(ShippingPolicyService::class)->update($policy, ['name' => ['ko' => '변경', 'en' => 'Changed']]); + } + + /** + * (과차단 회귀) 자기 소유 대상은 그대로 수정할 수 있어야 한다. + * + * @scenario route_shape=param_name_mismatch + * + * @effects detail_route_allows_in_scope_target + */ + public function test_self_scoped_admin_can_update_own_shipping_policy(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.shipping-policies.update', 'self'); + $this->actingAs($actor); + + $policy = $this->makePolicy($actor->id); + + $updated = app(ShippingPolicyService::class)->update($policy, [ + 'name' => ['ko' => '변경', 'en' => 'Changed'], + ]); + + $this->assertNotNull($updated, '자기 소유 정책은 수정할 수 있어야 합니다'); + } + + /** + * @scenario route_shape=static_bulk + * + * @effects static_bulk_route_reapplies_scope_gate + */ + public function test_self_scoped_admin_cannot_bulk_toggle_others_extra_fee_templates(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.shipping-policies.update', 'self'); + $owner = User::factory()->create(); + $this->actingAs($actor); + + $mine = $this->makeTemplate($actor->id, '63001'); + $theirs = $this->makeTemplate($owner->id, '63002'); + + try { + app(ExtraFeeTemplateService::class)->bulkToggleActive([$mine->id, $theirs->id], false); + $this->fail('스코프 밖 대상이 섞이면 거부되어야 합니다'); + } catch (AccessDeniedHttpException) { + // 기대 경로 + } + + // 전량 거부 — 내 것도 바뀌지 않아야 한다. + $this->assertTrue((bool) $mine->fresh()->is_active, '거부된 일괄 요청은 아무것도 바꾸지 않아야 합니다'); + $this->assertTrue((bool) $theirs->fresh()->is_active); + } + + /** + * (과차단 회귀) 글로벌 스코프 액터의 일괄 작업은 종전대로 동작해야 한다. + * + * @scenario route_shape=static_bulk + * + * @effects static_bulk_route_allows_global_scoped_actor + */ + public function test_global_scoped_admin_can_bulk_toggle_any_extra_fee_template(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.shipping-policies.update', null); + $owner = User::factory()->create(); + $this->actingAs($actor); + + $theirs = $this->makeTemplate($owner->id, '63003'); + + $count = app(ExtraFeeTemplateService::class)->bulkToggleActive([$theirs->id], false); + + $this->assertSame(1, $count); + $this->assertFalse((bool) $theirs->fresh()->is_active); + } + + /** + * (계약 표면) 스코프 거부는 HTTP 403 으로 표면화되어야 한다. + * + * 서비스가 던지는 AccessDeniedHttpException 이 컨트롤러 generic catch 에 삼켜지면 + * 권한 거부가 500 서버 장애로 위장된다 — 서비스 층 단언만으로는 이 회귀를 못 본다. + * + * @scenario route_shape=static_bulk + * + * @effects scope_denial_surfaces_as_403_over_http + */ + public function test_scope_denied_bulk_toggle_surfaces_as_403_over_http(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.shipping-policies.update', 'self'); + $owner = User::factory()->create(); + + $mine = $this->makeTemplate($actor->id, '63004'); + $theirs = $this->makeTemplate($owner->id, '63005'); + + $response = $this->actingAs($actor)->patchJson( + '/api/modules/sirsoft-ecommerce/admin/extra-fee-templates/bulk-toggle-active', + ['ids' => [$mine->id, $theirs->id], 'is_active' => false] + ); + + $response->assertStatus(403); + + // 전량 거부 — 상태 불변까지 단언한다. + $this->assertTrue((bool) $mine->fresh()->is_active); + $this->assertTrue((bool) $theirs->fresh()->is_active); + } + + /** + * (계약 복원) 존재하지 않는 대상은 스코프 거부가 아니라 not_found 400 으로 응답한다. + * + * 스코프 게이트 도입이 "리소스 부재 = 스코프 거부(→ 500 위장)" 로 기존 400 계약을 + * 바꾸면 안 된다. + * + * @scenario route_shape=param_name_mismatch + * + * @effects missing_target_keeps_not_found_contract + */ + public function test_missing_brand_returns_not_found_not_scope_denied(): void + { + $actor = $this->adminWithScope('sirsoft-ecommerce.brands.delete', null); + + $response = $this->actingAs($actor)->deleteJson( + '/api/modules/sirsoft-ecommerce/admin/brands/999999' + ); + + $response->assertStatus(400); + } + + /** + * 소유자를 지정해 배송정책을 만듭니다 (팩토리 부재 — 기존 테스트와 같은 직접 생성). + * + * @param int $ownerId 소유자 ID + * @return ShippingPolicy 생성된 배송정책 + */ + private function makePolicy(int $ownerId): ShippingPolicy + { + $policy = ShippingPolicy::create([ + 'name' => ['ko' => '정책', 'en' => 'Policy'], + 'is_active' => true, + 'is_default' => false, + 'sort_order' => 1, + 'created_by' => $ownerId, + ]); + + $policy->countrySettings()->create([ + 'country_code' => 'KR', + 'shipping_method' => 'parcel', + 'currency_code' => 'KRW', + 'charge_policy' => 'fixed', + 'base_fee' => 3000, + ]); + + return $policy; + } + + /** + * 소유자를 지정해 추가배송비 템플릿을 만듭니다. + * + * @param int $ownerId 소유자 ID + * @param string $zipcode 우편번호 (유니크 충돌 회피용) + * @return ExtraFeeTemplate 생성된 템플릿 + */ + private function makeTemplate(int $ownerId, string $zipcode): ExtraFeeTemplate + { + return ExtraFeeTemplate::create([ + 'zipcode' => $zipcode, + 'fee' => 3000.00, + 'region' => '제주도', + 'description' => '제주도 추가배송비', + 'is_active' => true, + 'created_by' => $ownerId, + ]); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Seo/EcommerceSitemapIndexTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Seo/EcommerceSitemapIndexTest.php index 2b6ed0e3..40350ffe 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Seo/EcommerceSitemapIndexTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Seo/EcommerceSitemapIndexTest.php @@ -4,13 +4,12 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Seo; use App\Jobs\GenerateSitemapJob; use App\Models\SitemapUrl; -use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Config; use Modules\Sirsoft\Ecommerce\Enums\ProductDisplayStatus; use Modules\Sirsoft\Ecommerce\Listeners\SeoCategoryCacheListener; use Modules\Sirsoft\Ecommerce\Listeners\SeoProductCacheListener; -use Tests\TestCase; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; /** * Ecommerce 리스너 사이트맵 증분 색인 테스트 (S4 ⑲) @@ -20,10 +19,8 @@ use Tests\TestCase; * - 활성 카테고리 변경 → 색인, 비활성/삭제 → 색인 제거 * - 리스너가 사이트맵 재생성 잡을 디스패치 */ -class EcommerceSitemapIndexTest extends TestCase +class EcommerceSitemapIndexTest extends ModuleTestCase { - use RefreshDatabase; - protected function setUp(): void { parent::setUp(); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/CategoryDeleteImageFileTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/CategoryDeleteImageFileTest.php new file mode 100644 index 00000000..9c105ecc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/CategoryDeleteImageFileTest.php @@ -0,0 +1,119 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Services; + +use App\Extension\Storage\ModuleStorageDriver; +use Modules\Sirsoft\Ecommerce\Models\Category; +use Modules\Sirsoft\Ecommerce\Models\CategoryImage; +use Modules\Sirsoft\Ecommerce\Services\CategoryService; +use Modules\Sirsoft\Ecommerce\Services\ProductService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 카테고리 삭제 시 이미지 물리 파일까지 삭제되는지 검증 (회귀) + * + * 종전에는 `$category->images()->delete()` 로 DB 행만 지워 파일이 디스크에 영구 잔존했다. + * 같은 모듈의 상품 삭제는 파일까지 지우므로 두 경로가 비대칭이었다. + * + * @see ProductService::delete() 대칭 선례 + */ +class CategoryDeleteImageFileTest extends ModuleTestCase +{ + /** + * 지정 disk 에 파일과 카테고리 이미지 행을 함께 생성합니다. + * + * @param int $categoryId 카테고리 ID + * @param string $disk 행 disk + * @param string $path 이미지 경로 (images/ 하위 상대 경로) + * @return CategoryImage 생성된 행 + */ + private function makeImage(int $categoryId, string $disk, string $path): CategoryImage + { + (new ModuleStorageDriver('sirsoft-ecommerce', $disk))->put('images', $path, 'image-bytes'); + + return CategoryImage::create([ + 'category_id' => $categoryId, + 'original_filename' => 'category.jpg', + 'stored_filename' => basename($path), + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'sort_order' => 0, + ]); + } + + /** + * @effects category_delete_removes_image_files, category_delete_removes_image_rows + */ + #[Test] + public function deleting_a_category_removes_its_image_files_and_rows(): void + { + $category = Category::create([ + 'name' => ['ko' => '삭제 대상 카테고리', 'en' => 'Category To Delete'], + 'slug' => 'category-delete-image-file-test', + 'is_active' => true, + 'depth' => 0, + 'path' => '', + 'sort_order' => 0, + ]); + + $image = $this->makeImage($category->id, 'modules', 'category/delete-test/main.jpg'); + + $storage = new ModuleStorageDriver('sirsoft-ecommerce', 'modules'); + $this->assertTrue( + $storage->exists('images', $image->path), + '사전 조건: 삭제 전에는 이미지 파일이 존재해야 한다.' + ); + + app(CategoryService::class)->deleteCategory($category->id); + + $this->assertFalse( + $storage->exists('images', $image->path), + '카테고리를 삭제하면 이미지 물리 파일도 함께 삭제되어야 한다.' + ); + + $this->assertDatabaseMissing('ecommerce_category_images', ['id' => $image->id]); + $this->assertDatabaseMissing('ecommerce_categories', ['id' => $category->id]); + } + + /** + * 행마다 disk 가 다른 혼재 상태에서도 각 행의 실제 저장 위치를 향해 삭제해야 한다. + * + * @effects category_delete_removes_image_files + */ + #[Test] + public function deleting_a_category_removes_image_files_across_mixed_disks(): void + { + config()->set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + + $category = Category::create([ + 'name' => ['ko' => '혼재 디스크 카테고리', 'en' => 'Mixed Disk Category'], + 'slug' => 'category-delete-mixed-disk-test', + 'is_active' => true, + 'depth' => 0, + 'path' => '', + 'sort_order' => 0, + ]); + + $local = $this->makeImage($category->id, 'modules', 'category/delete-mixed/local.jpg'); + $cdn = $this->makeImage($category->id, 'fake_cdn', 'category/delete-mixed/cdn.jpg'); + + app(CategoryService::class)->deleteCategory($category->id); + + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-ecommerce', 'modules'))->exists('images', $local->path), + '기본 디스크 행의 파일이 삭제되어야 한다.' + ); + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'))->exists('images', $cdn->path), + '다른 디스크 행의 파일도 그 행의 disk 를 향해 삭제되어야 한다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/EcommerceSettingsMirrorTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/EcommerceSettingsMirrorTest.php new file mode 100644 index 00000000..b6f3b757 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/EcommerceSettingsMirrorTest.php @@ -0,0 +1,56 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Services; + +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 이커머스 설정 미러 재채움 테스트 (공개이슈 #109, A3) + * + * 모듈 설정 저장에는 코어 공통 지점이 없어 각 모듈 서비스가 직접 저장한다. + * 그 지점에서 미러를 갱신하지 않으면 상주 프로세스(큐 워커 등)가 옛 값을 계속 읽는다. + * + * @scenario scope=module, trigger=save, trigger=cache_clear + * + * @effects module_mirror_refreshed_after_save, module_mirror_refreshed_after_cache_clear + */ +class EcommerceSettingsMirrorTest extends ModuleTestCase +{ + /** + * 설정 저장 후 같은 프로세스의 미러가 신값을 읽는다. + */ + public function test_module_settings_mirror_is_refreshed_after_save(): void + { + $service = app(EcommerceSettingsService::class); + + $basic = $service->getSettings('basic_info'); + $newName = '미러 확인 상점 '.uniqid(); + + $service->saveSettings(['basic_info' => array_merge($basic, ['shop_name' => $newName])]); + + $this->assertSame( + $newName, + g7_module_settings('sirsoft-ecommerce', 'basic_info.shop_name'), + '저장 후에도 미러가 옛 값을 유지합니다' + ); + } + + /** + * 캐시 초기화도 미러를 함께 갱신한다. + */ + public function test_clear_cache_also_refreshes_mirror(): void + { + $service = app(EcommerceSettingsService::class); + + config(['g7_settings.modules.sirsoft-ecommerce' => null]); + + $service->clearCache(); + + $this->assertIsArray( + g7_module_settings('sirsoft-ecommerce'), + '캐시 초기화 후 미러가 다시 채워지지 않았습니다' + ); + $this->assertNotEmpty(g7_module_settings('sirsoft-ecommerce')); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/ShippingPolicyResolverCacheTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/ShippingPolicyResolverCacheTest.php new file mode 100644 index 00000000..01901a0f --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/ShippingPolicyResolverCacheTest.php @@ -0,0 +1,199 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Services; + +use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum; +use Modules\Sirsoft\Ecommerce\Listeners\ShippingPolicyCacheListener; +use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy; +use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyResolver; +use Modules\Sirsoft\Ecommerce\Services\ShippingPolicyService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 배송정책 변경 후 해석기 캐시 무효화 검증 + * + * `ShippingPolicyResolver` 는 싱글톤이라 기본 배송정책을 요청 내 1회만 조회한다. + * 무효화 경로가 없으면 배송정책을 바꾼 요청이 같은 요청 안에서 변경 전 정책을 계속 쓴다 + * — 예외도 오류도 남지 않고 배송비만 이전 값으로 계산된다. + */ +class ShippingPolicyResolverCacheTest extends ModuleTestCase +{ + /** + * 배송정책을 생성합니다. + * + * @param bool $isDefault 기본 배송정책 여부 + * @param int $baseFee 기본 배송비 + * @return ShippingPolicy 생성된 배송정책 + */ + private function makePolicy(bool $isDefault, int $baseFee = 600): ShippingPolicy + { + $policy = ShippingPolicy::create([ + 'name' => ['ko' => '배송정책 '.$baseFee, 'en' => 'Policy '.$baseFee], + 'is_default' => $isDefault, + 'is_active' => true, + ]); + + $policy->countrySettings()->create([ + 'country_code' => 'KR', + 'shipping_method' => 'parcel', + 'currency_code' => 'KRW', + 'charge_policy' => ChargePolicyEnum::FIXED, + 'base_fee' => $baseFee, + 'is_active' => true, + ]); + + return $policy->load('countrySettings'); + } + + /** + * 기본 배송정책은 요청 내 1회만 조회됩니다 (캐시가 살아 있는지 확인). + * + * @scenario mutation=none + * + * @effects default_policy_cached_within_request + */ + public function test_default_policy_is_cached_within_request(): void + { + $policy = $this->makePolicy(isDefault: true); + + $resolver = app(ShippingPolicyResolver::class); + + $first = $resolver->getDefaultPolicy(); + $this->assertNotNull($first); + $this->assertSame($policy->id, $first->id); + + // 캐시가 없으면 이 단언이 새 기본정책을 집어 캐시 부재를 드러낸다 + $this->makePolicy(isDefault: false); + + $this->assertSame($first, $resolver->getDefaultPolicy(), '요청 내 재조회가 발생했습니다.'); + } + + /** + * `flushCache()` 후에는 저장소를 다시 조회합니다. (실패-먼저) + * + * @scenario mutation=manual_flush + * + * @effects resolver_cache_flushed + */ + public function test_flush_cache_forces_refetch(): void + { + $old = $this->makePolicy(isDefault: true, baseFee: 600); + + $resolver = app(ShippingPolicyResolver::class); + $this->assertSame($old->id, $resolver->getDefaultPolicy()?->id); + + $old->update(['is_default' => false]); + $new = $this->makePolicy(isDefault: true, baseFee: 900); + + $resolver->flushCache(); + + $this->assertSame($new->id, $resolver->getDefaultPolicy()?->id, 'flushCache() 후에도 이전 정책이 반환됩니다.'); + } + + /** + * 기본 배송정책 지정을 바꾸면 같은 요청 안에서 해석기가 새 정책을 반환합니다. (실패-먼저) + * + * @scenario mutation=set_default + * + * @effects resolver_cache_flushed + */ + public function test_set_default_invalidates_resolver_cache(): void + { + $old = $this->makePolicy(isDefault: true, baseFee: 600); + $candidate = $this->makePolicy(isDefault: false, baseFee: 900); + + $resolver = app(ShippingPolicyResolver::class); + $this->assertSame($old->id, $resolver->getDefaultPolicy()?->id); + + app(ShippingPolicyService::class)->setDefault($candidate); + + $this->assertSame( + $candidate->id, + $resolver->getDefaultPolicy()?->id, + '기본 배송정책을 바꿨는데 해석기가 이전 정책을 반환합니다.' + ); + } + + /** + * 기본 배송정책 수정도 같은 요청 안에서 반영됩니다. (실패-먼저) + * + * @scenario mutation=update + * + * @effects resolver_cache_flushed + */ + public function test_update_invalidates_resolver_cache(): void + { + $policy = $this->makePolicy(isDefault: true, baseFee: 600); + + $resolver = app(ShippingPolicyResolver::class); + $this->assertSame( + 600, + (int) $resolver->getDefaultPolicy()?->getCountrySetting('KR')?->base_fee + ); + + app(ShippingPolicyService::class)->update($policy, [ + 'name' => ['ko' => '배송정책', 'en' => 'Policy'], + 'is_default' => true, + 'is_active' => true, + 'country_settings' => [[ + 'country_code' => 'KR', + 'shipping_method' => 'parcel', + 'currency_code' => 'KRW', + 'charge_policy' => ChargePolicyEnum::FIXED->value, + 'base_fee' => 1500, + 'is_active' => true, + ]], + ]); + + $this->assertSame( + 1500, + (int) $resolver->getDefaultPolicy()?->getCountrySetting('KR')?->base_fee, + '배송정책을 수정했는데 해석기가 이전 배송비를 반환합니다.' + ); + } + + /** + * 기본 배송정책 삭제도 같은 요청 안에서 반영됩니다. (실패-먼저) + * + * @scenario mutation=delete + * + * @effects resolver_cache_flushed + */ + public function test_delete_invalidates_resolver_cache(): void + { + $policy = $this->makePolicy(isDefault: true); + + $resolver = app(ShippingPolicyResolver::class); + $this->assertNotNull($resolver->getDefaultPolicy()); + + app(ShippingPolicyService::class)->delete($policy); + + $this->assertNull($resolver->getDefaultPolicy(), '삭제된 배송정책이 계속 반환됩니다.'); + } + + /** + * 리스너가 배송정책 변경 훅 전부를 구독합니다. + * + * 변경 경로가 늘어나도 같은 훅만 발화하면 무효화가 따라오도록, 구독 목록을 고정한다. + * + * @scenario mutation=hook_subscription + * + * @effects resolver_cache_flushed + */ + public function test_listener_subscribes_every_mutation_hook(): void + { + $hooks = array_keys(ShippingPolicyCacheListener::getSubscribedHooks()); + + foreach ([ + 'after_create', + 'after_update', + 'after_delete', + 'after_bulk_delete', + 'after_toggle_active', + 'after_bulk_toggle_active', + 'after_set_default', + ] as $event) { + $this->assertContains('sirsoft-ecommerce.shipping_policy.'.$event, $hooks); + } + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Feature/Upgrade/ShippingTierAndOrderTotalsBackfillTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Upgrade/ShippingTierAndOrderTotalsBackfillTest.php new file mode 100644 index 00000000..3ad422e5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Feature/Upgrade/ShippingTierAndOrderTotalsBackfillTest.php @@ -0,0 +1,210 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Feature\Upgrade; + +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory; +use Modules\Sirsoft\Ecommerce\Database\Factories\OrderOptionFactory; +use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum; +use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use Modules\Sirsoft\Ecommerce\Upgrades\Upgrade_1_1_1; + +/** + * 1.1.1 업그레이드 스텝 테스트 (공개 #94) + * + * - 연속형 구간 정책의 시작값을 직전 종료값으로 정규화 (표시 정합) + * - 총 무게/부피가 0 으로 고정 기록된 주문을 옵션 소계 합으로 백필 + * + * 두 스텝 모두 멱등이어야 합니다 — 업그레이드는 재실행될 수 있고, 두 번째 실행이 + * 값을 다시 움직이면 운영자가 보정한 값이 조용히 덮어써집니다. + * + * @scenario charge_policy=range_weight, boundary=at_max, unit_source=g + * + * @effects continuous_tier_min_normalized, order_totals_backfilled, upgrade_is_idempotent + */ +class ShippingTierAndOrderTotalsBackfillTest extends ModuleTestCase +{ + /** + * 1.1.1 업그레이드 스텝을 실행합니다. + */ + private function runUpgrade(): void + { + (new Upgrade_1_1_1)->run(new UpgradeContext('1.1.0', '1.1.1', '1.1.1', 'extension-upgrade')); + } + + /** + * 구간 설정을 가진 배송정책 국가별 설정을 생성합니다. + * + * @param ChargePolicyEnum $chargePolicy 부과정책 + * @param array $tiers 구간 배열 + * @return int 생성된 국가별 설정 ID + */ + private function makeCountrySetting(ChargePolicyEnum $chargePolicy, array $tiers): int + { + $policy = ShippingPolicy::create([ + 'name' => ['ko' => '업그레이드 테스트 정책', 'en' => 'Upgrade Test Policy'], + 'is_default' => false, + 'is_active' => true, + ]); + + $setting = $policy->countrySettings()->create([ + 'country_code' => 'KR', + 'shipping_method' => 'parcel', + 'currency_code' => 'KRW', + 'charge_policy' => $chargePolicy, + 'base_fee' => 0, + 'ranges' => ['tiers' => $tiers], + 'extra_fee_enabled' => false, + 'is_active' => true, + ]); + + return $setting->id; + } + + /** + * 국가별 설정의 구간 배열을 반환합니다. + * + * @param int $settingId 국가별 설정 ID + * @return array 구간 배열 + */ + private function tiersOf(int $settingId): array + { + $row = DB::table('ecommerce_shipping_policy_country_settings')->where('id', $settingId)->first(); + + return json_decode((string) $row->ranges, true)['tiers'] ?? []; + } + + public function test_it_normalizes_continuous_tier_min_to_previous_max(): void + { + // 구 규칙(max + 1)으로 저장된 무게 구간 + $settingId = $this->makeCountrySetting(ChargePolicyEnum::RANGE_WEIGHT, [ + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 3, 'max' => 5, 'fee' => 5000], + ['min' => 6, 'max' => null, 'fee' => 8000], + ]); + + $this->runUpgrade(); + + $tiers = $this->tiersOf($settingId); + $this->assertEquals(0, $tiers[0]['min']); + $this->assertEquals(2, $tiers[1]['min']); + $this->assertEquals(5, $tiers[2]['min']); + // 종료값과 배송비는 손대지 않는다 + $this->assertEquals(2, $tiers[0]['max']); + $this->assertEquals(5, $tiers[1]['max']); + $this->assertNull($tiers[2]['max']); + $this->assertEquals(8000, $tiers[2]['fee']); + } + + public function test_it_keeps_max_plus_one_for_quantity_tiers(): void + { + // 수량은 이산형이라 max + 1 이 정상 — 그 형태는 그대로 유지된다 + $settingId = $this->makeCountrySetting(ChargePolicyEnum::RANGE_QUANTITY, [ + ['min' => 0, 'max' => 5, 'fee' => 3000], + ['min' => 6, 'max' => null, 'fee' => 5000], + ]); + + $this->runUpgrade(); + + $tiers = $this->tiersOf($settingId); + $this->assertEquals(0, $tiers[0]['min']); + $this->assertEquals(6, $tiers[1]['min']); + } + + public function test_it_normalizes_first_quantity_tier_min_to_zero(): void + { + // 샘플 데이터는 "1~5개 / 6개~" 로 저장돼 있다. 시작값이 읽기전용이 된 이상 + // 이 형태가 남아 있으면 운영자가 그 정책을 다시 저장할 수 없다. + $settingId = $this->makeCountrySetting(ChargePolicyEnum::RANGE_QUANTITY, [ + ['min' => 1, 'max' => 5, 'fee' => 3000], + ['min' => 6, 'max' => null, 'fee' => 5000], + ]); + + $this->runUpgrade(); + + $tiers = $this->tiersOf($settingId); + $this->assertEquals(0, $tiers[0]['min']); + $this->assertEquals(6, $tiers[1]['min']); + // 종료값·배송비는 불변 (계산 결과가 바뀌면 안 된다) + $this->assertEquals(5, $tiers[0]['max']); + $this->assertEquals(3000, $tiers[0]['fee']); + } + + public function test_it_backfills_order_totals_from_option_subtotals(): void + { + $order = OrderFactory::new()->create(); + DB::table('ecommerce_orders')->where('id', $order->id)->update([ + 'total_weight' => 0, + 'total_volume' => 0, + ]); + + foreach ([[500.0, 1000.0], [250.0, 400.0]] as [$weight, $volume]) { + $orderOption = OrderOptionFactory::new()->forOrder($order)->create(['parent_option_id' => null]); + DB::table('ecommerce_order_options')->where('id', $orderOption->id)->update([ + 'subtotal_weight' => $weight, + 'subtotal_volume' => $volume, + ]); + } + + $this->runUpgrade(); + + $row = DB::table('ecommerce_orders')->where('id', $order->id)->first(); + $this->assertEquals(750.0, (float) $row->total_weight); + $this->assertEquals(1400.0, (float) $row->total_volume); + } + + public function test_it_preserves_orders_that_already_have_totals(): void + { + $order = OrderFactory::new()->create(); + DB::table('ecommerce_orders')->where('id', $order->id)->update([ + 'total_weight' => 999.0, + 'total_volume' => 888.0, + ]); + + $orderOption = OrderOptionFactory::new()->forOrder($order)->create(['parent_option_id' => null]); + DB::table('ecommerce_order_options')->where('id', $orderOption->id)->update([ + 'subtotal_weight' => 1.0, + 'subtotal_volume' => 2.0, + ]); + + $this->runUpgrade(); + + $row = DB::table('ecommerce_orders')->where('id', $order->id)->first(); + $this->assertEquals(999.0, (float) $row->total_weight); + $this->assertEquals(888.0, (float) $row->total_volume); + } + + public function test_it_is_idempotent(): void + { + $settingId = $this->makeCountrySetting(ChargePolicyEnum::RANGE_VOLUME, [ + ['min' => 0, 'max' => 50, 'fee' => 5000], + ['min' => 51, 'max' => null, 'fee' => 9000], + ]); + + $order = OrderFactory::new()->create(); + DB::table('ecommerce_orders')->where('id', $order->id)->update([ + 'total_weight' => 0, + 'total_volume' => 0, + ]); + $orderOption = OrderOptionFactory::new()->forOrder($order)->create(['parent_option_id' => null]); + DB::table('ecommerce_order_options')->where('id', $orderOption->id)->update([ + 'subtotal_weight' => 500.0, + 'subtotal_volume' => 1000.0, + ]); + + $this->runUpgrade(); + $firstTiers = $this->tiersOf($settingId); + $firstOrder = DB::table('ecommerce_orders')->where('id', $order->id)->first(); + + $this->runUpgrade(); + $secondTiers = $this->tiersOf($settingId); + $secondOrder = DB::table('ecommerce_orders')->where('id', $order->id)->first(); + + $this->assertSame($firstTiers, $secondTiers); + $this->assertEquals((float) $firstOrder->total_weight, (float) $secondOrder->total_weight); + $this->assertEquals((float) $firstOrder->total_volume, (float) $secondOrder->total_volume); + $this->assertEquals(50, $secondTiers[1]['min']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/ModuleTestCase.php b/modules/_bundled/sirsoft-ecommerce/tests/ModuleTestCase.php index 871ca28d..a1e19d38 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/ModuleTestCase.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/ModuleTestCase.php @@ -380,8 +380,12 @@ abstract class ModuleTestCase extends TestCase $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts index 1b31be82..ee654ad9 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/fixtures/ecommerce-auth.ts @@ -43,6 +43,10 @@ type EcommerceAuthFixtures = { orderManageToken: string; /** 배송정책 조회+수정 권한 보유 토큰 (배송정책 목록 표시 검증용) */ shippingPolicyToken: string; + /** 프로모션 쿠폰 조회+등록+수정+삭제 권한 보유 토큰 (쿠폰 폼 검증용) */ + couponManageToken: string; + /** 배송정책 + 상품 + 카테고리 권한 토큰 (구간 경계 E2E 픽스처 구성용) */ + shippingBoundaryToken: string; /** 일반 쇼핑 사용자 토큰 (관리자 권한 없음 — 유저 화면 검증용) */ userToken: string; /** 구매 고객 토큰 (장바구니/추가옵션 등 쇼핑 플로우 검증용) */ @@ -103,6 +107,32 @@ export const test = base.extend<EcommerceAuthFixtures>({ ), ); }, + couponManageToken: async ({}, use) => { + await use( + issueToken( + 'sirsoft-ecommerce.promotion-coupon.read', + 'sirsoft-ecommerce.promotion-coupon.create', + 'sirsoft-ecommerce.promotion-coupon.update', + 'sirsoft-ecommerce.promotion-coupon.delete', + ), + ); + }, + shippingBoundaryToken: async ({}, use) => { + // 구간 경계 검증은 정책·상품·카테고리를 한 세션에서 만들고 지우므로 권한을 함께 발급한다. + await use( + issueToken( + 'sirsoft-ecommerce.shipping-policies.read', + 'sirsoft-ecommerce.shipping-policies.create', + 'sirsoft-ecommerce.shipping-policies.update', + 'sirsoft-ecommerce.shipping-policies.delete', + 'sirsoft-ecommerce.products.read', + 'sirsoft-ecommerce.products.create', + 'sirsoft-ecommerce.products.update', + 'sirsoft-ecommerce.products.delete', + 'sirsoft-ecommerce.categories.read', + ), + ); + }, // userToken / customerToken 은 현재 둘 다 "관리자 권한 없는 인증 사용자" 로 동일하다. // 유저 화면(마이페이지/헤더)과 구매 플로우(장바구니/주문서)는 요구 권한이 갈릴 수 있어 // 호출부의 의도를 이름으로 남긴다 — 한쪽에 권한이 붙어도 다른 쪽이 영향받지 않는다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-footer-totals.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-footer-totals.spec.ts new file mode 100644 index 00000000..d5593659 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-footer-totals.spec.ts @@ -0,0 +1,80 @@ +/** + * 주문 상세 — 합계행(tfoot) 텍스트 바인딩 해석 회귀 (공개 #98). + * + * 배경: admin_basic 1.0.4 릴리즈에 stale dist 가 실려, `components.json` 의 + * `skipBindingKeys`(footerCells 선평가 중단) 선언과 구형 JS(renderFooterCellText 부재)가 + * 어긋나 합계행 3셀(`수량`·`적립예정`·`배송비`)에 `{{order.data?.total_quantity ?? 0}}개` + * 같은 표현식 원문이 그대로 노출됐다. 소스·단위 테스트는 정상(green)이었고 **서빙 산출물만** + * 구형이었으므로, 서빙 번들을 실제로 로드하는 이 E2E 만이 해당 회귀를 잡는다. + * + * 측정 규율 (이 spec 을 고칠 사람에게): + * - 값 하드코딩 금지 — 주문 데이터는 변동한다. 형태(정규식)로 단언한다. + * - 헤더/라벨의 한글 문자열 단언 금지 — E2E 세션 로케일이 en 이면 `$t:` 라벨이 영문으로 + * 렌더된다(order-detail-numeric-alignment.spec.ts 실측 선례). 셀 식별은 레이아웃 + * JSON 이 부여한 className 훅으로 한다: 수량=`text-center`(비 blue), 적립=`text-center`+ + * `text-blue-600`, 배송비=`text-label`. `개`/`P` 접미사는 레이아웃에 리터럴로 박힌 + * 로케일 무관 문자라 단언에 써도 된다. + * - tfoot 의 선행 빈 셀(expandable/selectable) 유무에 좌우되지 않도록 인덱스 단언 금지. + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; +import type { Page } from '@playwright/test'; + +/** + * 첫 주문의 상세로 진입한다 (주문번호 하드코딩 회피). + * + * 목록 화면은 좁은 뷰포트에서 카드로 렌더되므로 진입은 항상 데스크톱 폭에서 한다. + * 상세 진입은 주문번호 `<span>` 클릭(DataGrid onRowClick 계열). + */ +async function gotoFirstOrderDetail(page: Page): Promise<void> { + await page.setViewportSize({ width: 1280, height: 900 }); + await page.goto('/admin/ecommerce/orders'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const orderNumber = page.locator('table tbody tr span.cursor-pointer').first(); + await orderNumber.waitFor({ state: 'visible', timeout: 30_000 }); + await orderNumber.click(); + + await page.waitForURL(/\/admin\/ecommerce\/orders\/[\w-]+$/, { timeout: 30_000 }); +} + +test.describe('@sirsoft-ecommerce 주문 상세 합계행 텍스트 바인딩', () => { + // @scenario footer_cell=quantity, viewport=desktop + // @effects footer_text_bindings_resolved_no_raw_expression, footer_quantity_cell_shows_count_with_unit + // @scenario footer_cell=points, viewport=desktop + // @effects footer_points_cell_shows_formatted_number + // @scenario footer_cell=delivery, viewport=desktop + // @effects footer_delivery_cell_shows_shipping_fee_label + test('합계행 수량·적립·배송비 셀이 표현식 원문 없이 값으로 렌더된다', async ({ page, ordersReadToken }) => { + await authenticatePage(page, ordersReadToken); + await gotoFirstOrderDetail(page); + + // 상품 목록 표의 합계행이 뜰 때까지 대기 (footerCells 는 데이터 로드 후에만 렌더) + const tfoot = page.locator('table tfoot').first(); + await tfoot.waitFor({ state: 'visible', timeout: 30_000 }); + + // ① 페이지 전체에 미해석 표현식 원문이 없다 — stale dist 회귀의 1차 증상. + // (poll: 데이터 소스 응답 직후 재렌더 타이밍 흡수) + await expect + .poll(async () => (await page.evaluate(() => document.body.innerText)).includes('{{'), { + timeout: 20_000, + }) + .toBe(false); + + // ② 수량 셀 — 레이아웃 className 훅: text-center 이면서 blue(적립) 가 아닌 셀. + // `개` 는 레이아웃 리터럴(로케일 무관). 회귀 시 `{{...}}개` 라 정규식이 깨진다. + const quantityCell = tfoot.locator('td.text-center:not(.text-blue-600)').first(); + await expect(quantityCell).toHaveText(/^[\d,]+개$/); + + // ③ 적립예정 셀 — text-center + text-blue-600. `| number` 파이프 해석 결과라 + // 숫자(천단위 콤마 허용) + `P` 리터럴 형태여야 한다. + const pointsCell = tfoot.locator('td.text-center.text-blue-600').first(); + await expect(pointsCell).toHaveText(/^[\d,]+P$/); + + // ④ 배송비 셀 — text-label. 라벨은 `$t:` 로 로케일 의존이므로 라벨 문자열 대신 + // "라벨: 값" 구분자 존재 + 표현식 원문 부재를 단언한다. + const deliveryCell = tfoot.locator('td.text-label').first(); + const deliveryText = (await deliveryCell.innerText()).trim(); + expect(deliveryText).not.toContain('{{'); + expect(deliveryText).toMatch(/:/); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-orphaned-pg-badge.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-orphaned-pg-badge.spec.ts new file mode 100644 index 00000000..09410853 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-orphaned-pg-badge.spec.ts @@ -0,0 +1,180 @@ +/** + * 주문설정 결제수단 — 죽은 PG 지정 표시·차단 (A2). + * + * 배경: 고아 판정(`_orphaned`)은 결제수단 ID 에만 계산된다. builtin 수단(카드 등)에 특정 PG 를 + * 지정한 뒤 그 PG 플러그인을 제거하면, 수단 자체는 카탈로그에 남아 있어 고아 필터를 그대로 + * 통과한다. 체크아웃에 선택 가능한 결제수단으로 노출되고, 주문하면 PG 라우팅이 매칭에 실패해 + * **결제창 없이 주문완료로 넘어간다**. + * + * 측정 규율 (이 spec 을 고칠 사람에게): + * - 결제수단 ID·PG명을 하드코딩하지 않는다. 검증 대상은 카탈로그에서 `needs_pg && !pg_locked` + * 조건으로 런타임 선별한다. + * - 죽은 PG 상태는 환경에 자연 발생하지 않으므로 **관리자 API 로 주입**하고 `finally` 에서 + * 반드시 원복한다. 주입 없이 skip 하면 그 skip 이 거짓 통과가 된다. + * - `_orphaned` 와 달리 행 편집 컨트롤은 막지 않는다 — 살아있는 PG 로 바꿔 복구하는 경로가 + * 남아야 하므로 PG 선택 셀렉트는 계속 보여야 한다. + * + * @scenario pg_provider_state=dead_own + * @effects dead_pg_method_flagged_for_admin, + * dead_pg_method_hidden_from_checkout, + * dead_pg_method_keeps_recovery_control + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; +import type { APIRequestContext, Page } from '@playwright/test'; + +const ORDER_SETTINGS_URL = '/admin/ecommerce/settings?tab=order_settings'; +const ADMIN_SETTINGS_API = '/api/modules/sirsoft-ecommerce/admin/settings'; +const PUBLIC_PAYMENT_API = '/api/modules/sirsoft-ecommerce/settings/payment'; +const DEAD_PG = 'ghost_pg_e2e'; + +interface CatalogMethod { + id: string; + pg_provider: string | null; + pg_locked?: boolean; + needs_pg?: boolean; + _orphaned?: boolean; + _orphaned_pg?: boolean; +} + +/** 폼에 시드된 결제수단 카탈로그 (화면이 실제로 그리는 원본). */ +async function catalogMethods(page: Page): Promise<CatalogMethod[]> { + return page.evaluate(() => { + const form = (window as any).G7Core?.state?.getLocal?.()?.form; + return (form?.order_settings?.payment_methods ?? []) as CatalogMethod[]; + }); +} + +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 catalogMethods(page)).length, { timeout: 30_000 }).toBeGreaterThan(0); +} + +/** 관리자 API 로 저장된 결제수단 배열을 읽는다 (런타임 전용 플래그 포함). */ +async function readSavedMethods(request: APIRequestContext, token: string): Promise<CatalogMethod[]> { + const response = await request.get(ADMIN_SETTINGS_API, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + expect(response.ok(), '관리자 설정 조회 실패').toBeTruthy(); + const body = await response.json(); + return body?.data?.order_settings?.payment_methods ?? []; +} + +/** 결제수단 배열을 저장한다 (런타임 전용 플래그는 서버가 제거한다). */ +async function saveMethods( + request: APIRequestContext, + token: string, + methods: CatalogMethod[], +): Promise<void> { + const response = await request.put(ADMIN_SETTINGS_API, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + data: { _tab: 'order_settings', order_settings: { payment_methods: methods } }, + }); + expect(response.ok(), `결제수단 저장 실패 (${response.status()})`).toBeTruthy(); +} + +test.describe('@sirsoft-ecommerce 주문설정 결제수단 — 죽은 PG 표시·차단', () => { + // 이 그룹은 상점 설정(전역 상태)을 주입·원복한다. 병렬로 돌리면 다른 테스트가 주입 구간의 + // 상태를 읽어 거짓 실패를 낸다. + test.describe.configure({ mode: 'serial' }); + + // @scenario pg_provider_state=dead_own + // @effects dead_pg_method_flagged_for_admin, dead_pg_method_hidden_from_checkout, dead_pg_method_keeps_recovery_control + test('지정 PG 가 사라진 결제수단은 관리자에 배지가 뜨고 공개 응답에서는 제거된다', async ({ + page, + request, + settingsToken, + }) => { + const original = await readSavedMethods(request, settingsToken); + expect(original.length, '결제수단 카탈로그가 비어 있어 판별 불가').toBeGreaterThan(0); + + const target = original.find((m) => m.needs_pg === true && !m.pg_locked); + test.skip(!target, 'PG 를 관리자가 지정할 수 있는 결제수단이 없는 환경 — 판별 불가'); + + const targetId = target!.id; + + // 주입 전에 이미 죽은 PG 가 지정돼 있으면 원복 기준(original)이 오염된 상태다. + // 그대로 진행하면 "원복" 이 오염 상태를 되돌려 놓으므로 여기서 멈추고 드러낸다. + expect( + target!._orphaned_pg ?? false, + `${targetId} 에 이미 죽은 PG 가 지정돼 있다 — 운영자 조치가 필요하거나 이전 실행이 원복에 실패했다`, + ).toBeFalsy(); + + try { + // 죽은 PG 주입 — 플러그인 제거 후 저장값만 남은 상태를 등가 재현한다 + await saveMethods( + request, + settingsToken, + original.map((m) => (m.id === targetId ? { ...m, pg_provider: DEAD_PG, is_active: true } : m)), + ); + + // ① 관리자 응답: 플래그가 서고 배지가 뜬다 + const flagged = (await readSavedMethods(request, settingsToken)).find((m) => m.id === targetId); + expect( + flagged?._orphaned_pg, + `${targetId} 에 죽은 PG 가 지정됐으므로 관리자 응답에 _orphaned_pg 가 서야 한다`, + ).toBe(true); + + await gotoOrderSettings(page, settingsToken); + + const badge = page.getByTestId(`orphaned-pg-badge-${targetId}`); + await expect(badge, `${targetId} 에 죽은 PG 배지가 떠야 한다`).toBeVisible({ timeout: 15_000 }); + + // ② 복구 경로 유지 — PG 선택 셀렉트를 막지 않는다 + await expect( + page.getByTestId(`pg-select-${targetId}`), + `${targetId} 는 살아있는 PG 로 바꿔 복구할 수 있어야 하므로 PG 선택 셀렉트가 남아야 한다`, + ).toBeVisible(); + + // ③ 공개 응답: 해당 수단이 제거된다 (체크아웃에 노출되면 결제창 없는 주문이 만들어진다) + const publicResponse = await request.get(PUBLIC_PAYMENT_API, { headers: { Accept: 'application/json' } }); + expect(publicResponse.ok(), '공개 결제설정 조회 실패').toBeTruthy(); + const publicBody = await publicResponse.json(); + const publicIds = (publicBody?.data?.payment_methods ?? []).map((m: CatalogMethod) => m.id); + + expect( + publicIds, + `${targetId} 는 죽은 PG 를 지정했으므로 공개 응답에서 제거돼야 한다`, + ).not.toContain(targetId); + + // 런타임 전용 플래그는 공개 응답에도 남지 않는다 + for (const method of publicBody?.data?.payment_methods ?? []) { + expect(method._orphaned_pg ?? false, '공개 응답에 런타임 플래그가 남았다').toBeFalsy(); + } + } finally { + // 원복 — 실패하더라도 환경을 저장 전 상태로 되돌린다 + await saveMethods(request, settingsToken, original); + } + + // 원복 확인: 배지가 사라지고 공개 응답에 다시 실린다 + const restored = (await readSavedMethods(request, settingsToken)).find((m) => m.id === targetId); + expect(restored?._orphaned_pg ?? false, '원복 후에도 죽은 PG 플래그가 남았다').toBeFalsy(); + expect(restored?.pg_provider ?? null, '원복 후 PG 지정이 주입값 그대로다').not.toBe(DEAD_PG); + }); + + // 살아 있는 PG 만 있는 상태를 확인하는 테스트다 — dead_own 축이 아니다. + // @scenario pg_provider_state=live + // @effects live_pg_method_remains_visible + test('정상 환경에서는 죽은 PG 배지가 어디에도 뜨지 않는다 (거짓 양성 차단)', async ({ + page, + settingsToken, + }) => { + await gotoOrderSettings(page, settingsToken); + + const methods = await catalogMethods(page); + + // 부재를 단언하기 전에 카탈로그가 실제로 렌더됐는지부터 확정한다 + expect(methods.length, '결제수단 카탈로그가 비어 있다 — 부재 단언이 무의미해진다').toBeGreaterThan(0); + + const flagged = methods.filter((m) => m._orphaned_pg === true); + + expect( + flagged.map((m) => m.id), + '죽은 PG 를 지정한 결제수단이 남아 있다 (운영자 조치 필요) — 또는 판정식이 정상 PG 를 오판한다', + ).toEqual([]); + + await expect(page.locator('[data-testid^="orphaned-pg-badge-"]')).toHaveCount(0); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts index f0afb958..a0355ae1 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-pg-locked.spec.ts @@ -22,11 +22,12 @@ * 태그로 찾지 말고 `data-testid` (`pg-select-{id}` / `pg-locked-badge-{id}` / `pg-not-required-{id}`) * 로 분기 결과를 조회한다 — 컴포넌트 내부 구현이 바뀌어도 이 spec 은 유지된다. * - * @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, - * saved_null_pg_provider_self_healed + * 축 요약(마커 아님 — 평문): method_kind=extension, capability_declared=declared, + * capability=pg_locked. 요약을 시나리오 축 마커로 적으면 마커 파서가 + * 쉼표로만 축을 분리하므로 `×` 표기는 첫 축만, 그것도 오염된 형태로 집계된다 — 실재하지 않는 + * 조합이 커버된 것처럼 쌓이는 방향이라 요약은 평문으로 둔다. 실제 커버는 각 test 의 라인 마커가 담당한다. + * + * 효과 요약(마커 아님 — 평문): admin_shows_pg_locked_badge, admin_hides_pg_select_for_locked, admin_shows_pg_select_for_unlocked, saved_null_pg_provider_self_healed. */ import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; import type { Page } from '@playwright/test'; diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/promotion-coupon-combinable.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/promotion-coupon-combinable.spec.ts new file mode 100644 index 00000000..0015bcb3 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/promotion-coupon-combinable.spec.ts @@ -0,0 +1,134 @@ +/** + * 쿠폰 「타 쿠폰과 중복 사용」 라디오 boolean 저장 — 공개 이슈 #97 회귀. + * + * 결함: 라디오 클릭 시 DOM 문자열 "true"/"false" 가 폼 상태에 그대로 저장되어 + * 저장 요청이 서버 boolean 규칙에서 422 (`is combinable 필드는 true 또는 + * false여야 합니다.`) 로 실패했다. 표시 계층은 느슨 비교로 정상이라 저장 + * 시점에야 드러난다. 라디오를 클릭하지 않으면 초기 boolean 이 전송되어 + * 저장이 되는 것이 제보의 "임시 우회" 였다. + * + * 검증: 등록 화면에서 "불가능" 을 실제 클릭해 저장(POST body 의 is_combinable 이 + * boolean false)하고, 수정 화면에서 저장값 반영 + "가능" 재선택 저장(PUT 200) + * 까지 왕복한다. 생성한 쿠폰은 API 로 정리한다. + * + * @scenario issue97-coupon-combinable-boolean + * @axes request=store,update surface=browser_form + * @effects radio_click_saves_boolean_state, edit_screen_reflects_saved_value + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; + +const CREATE_URL = '/admin/ecommerce/promotion-coupon-create'; +const LIST_URL_PATTERN = /\/admin\/ecommerce\/promotion-coupons(\?|$)/; +const API_BASE = '/api/modules/sirsoft-ecommerce/admin/promotion-coupons'; + +/** 폼 자동바인딩 debounce(300ms) 유실 방지 — 필드별 순차 입력 + 대기 */ +async function fillDebounced( + page: import('@playwright/test').Page, + selector: string, + value: string, +): Promise<void> { + await page.locator(selector).fill(value); + await page.waitForTimeout(500); +} + +test.describe('쿠폰 중복 사용 라디오 boolean 저장 (#97)', () => { + // @scenario request=store surface=browser_form + // @effects radio_click_saves_boolean_state + test('등록: "불가능" 클릭 후 저장하면 boolean false 로 201 저장되고, 수정 화면 반영 + "가능" 재저장까지 왕복한다', async ({ + page, + couponManageToken, + }) => { + await authenticatePage(page, couponManageToken); + await page.goto(CREATE_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect(page.locator('input[name="is_combinable"][value="false"]')).toBeVisible({ + timeout: 30_000, + }); + + // 필수값 입력 — debounce 유실 방지를 위해 필드별 순차 입력 + const today = new Date(); + const nextMonth = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); + const fmt = (d: Date) => d.toISOString().slice(0, 10); + + await fillDebounced(page, 'input[name="name[ko]"]', 'E2E 중복사용 라디오 검증 쿠폰'); + await fillDebounced(page, 'input[name="discount_value"]', '1000'); + await fillDebounced(page, 'input[name="valid_from"]', fmt(today)); + await fillDebounced(page, 'input[name="valid_to"]', fmt(nextMonth)); + + // 핵심 조작: "불가능" 라디오를 실제로 클릭한다 (수정 전에는 이 클릭이 422 를 만들었다) + await page.locator('input[name="is_combinable"][value="false"]').check(); + await page.waitForTimeout(500); + + // 저장 → POST 요청 본문의 is_combinable 이 boolean false 인지 채증 + const [request, response] = await Promise.all([ + page.waitForRequest( + (req) => req.url().includes(API_BASE) && req.method() === 'POST', + { timeout: 30_000 }, + ), + page.waitForResponse( + (res) => res.url().includes(API_BASE) && res.request().method() === 'POST', + { timeout: 30_000 }, + ), + page.locator('#save_button_top').click(), + ]); + + const body = request.postDataJSON() as Record<string, unknown>; + expect(body.is_combinable).toBe(false); // boolean false — 문자열 "false" 금지 + expect(response.status()).toBe(201); + + const created = (await response.json()) as { data?: { id?: number } }; + const couponId = created.data?.id; + expect(couponId, '생성 응답에서 쿠폰 id 를 얻지 못했습니다.').toBeTruthy(); + + try { + // 저장 성공 → 목록 복귀 + await page.waitForURL(LIST_URL_PATTERN, { timeout: 30_000 }); + + // 수정 화면: 저장된 false 가 "불가능" checked 로 반영되는지 + await page.goto(`/admin/ecommerce/promotion-coupons/${couponId}/edit`); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + const noRadio = page.locator('input[name="is_combinable"][value="false"]'); + await expect(noRadio).toBeVisible({ timeout: 30_000 }); + await expect(noRadio).toBeChecked({ timeout: 30_000 }); + + // "가능" 재선택 후 저장 → PUT 200 + body boolean true + await page.locator('input[name="is_combinable"][value="true"]').check(); + await page.waitForTimeout(500); + + const [putRequest, putResponse] = await Promise.all([ + page.waitForRequest( + (req) => req.url().includes(`${API_BASE}/${couponId}`) && req.method() === 'PUT', + { timeout: 30_000 }, + ), + page.waitForResponse( + (res) => + res.url().includes(`${API_BASE}/${couponId}`) && res.request().method() === 'PUT', + { timeout: 30_000 }, + ), + page.locator('#save_button_top').click(), + ]); + + const putBody = putRequest.postDataJSON() as Record<string, unknown>; + expect(putBody.is_combinable).toBe(true); + expect(putResponse.status()).toBe(200); + + await page.waitForURL(LIST_URL_PATTERN, { timeout: 30_000 }); + } finally { + // 정리: 생성한 쿠폰 삭제 (페이지 컨텍스트의 토큰 재사용) + const deleteStatus = await page.evaluate( + async ({ apiBase, id }) => { + const res = await fetch(`${apiBase}/${id}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + Accept: 'application/json', + }, + }); + return res.status; + }, + { apiBase: API_BASE, id: couponId }, + ); + expect([200, 204]).toContain(deleteStatus); + } + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/public-asset-disk-override.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/public-asset-disk-override.spec.ts new file mode 100644 index 00000000..69b47c5e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/public-asset-disk-override.spec.ts @@ -0,0 +1,136 @@ +/** + * E2E: 이커머스 공개 자산 디스크 오버라이드 필드 (공개#100) + * + * 시나리오 매니페스트: `tests/scenarios/public-asset-cdn.yaml` + * (케이스 마킹은 각 test 의 라인 주석에 있다 — 축 구분자는 쉼표) + * + * 기본정보 탭의 "공개 자산 디스크" Select 가 [코어 설정 따름('')] + 코어 카탈로그를 + * 그리는지, 그리고 선택→저장→재진입 왕복에서 값이 유지되는지 검증한다. + * 코어 드라이버 탭 카드는 admin_basic 템플릿 spec 이 담당한다 — 여기서는 모듈이 + * 소유한 오버라이드 표면만 본다. + * + * composite Select 는 네이티브 select 가 아니라 커스텀 드롭다운(root div 에 name 속성, + * Button 토글, portal listbox role=option)이다 — 상호작용은 그 구조를 따른다. + * 저장 후에는 반드시 원값으로 되돌려 사이트 설정을 바꾸지 않는다. + * + * @effects settings_catalog_includes_plugin_registered_disks, + * none_override_forces_streaming_over_global + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; +import type { Page } from '@playwright/test'; + +const ECOMMERCE_BASIC_TAB = '/admin/ecommerce/settings?tab=basic_info'; +const MODULE_SELECT_ROOT = '[name="basic_info.public_asset_disk"]'; + +/** + * 기본정보 탭 진입 — 카탈로그 적재를 엔진 상태로 확인한다. + * + * DOM attach 만 기다리면 바인딩 전 빈 Select 를 카탈로그 누락으로 오인한다. + * + * @param page Playwright 페이지 + */ +async function gotoBasicInfoTab(page: Page): Promise<void> { + await page.goto(ECOMMERCE_BASIC_TAB); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect + .poll( + async () => + await page.evaluate(() => { + const local = (window as any).G7Core?.state?.getLocal?.(); + return Array.isArray(local?.form?.available_public_asset_disks); + }), + { timeout: 20_000 }, + ) + .toBe(true); +} + +/** + * 커스텀 드롭다운을 열고 옵션 라벨 목록을 읽는다 (읽은 뒤 드롭다운은 닫지 않음). + * + * @param page Playwright 페이지 + * @returns 옵션 라벨 배열 + */ +async function openAndReadOptions(page: Page): Promise<string[]> { + await page.locator(`${MODULE_SELECT_ROOT} button`).first().click(); + const listbox = page.locator('[role="listbox"]'); + await expect(listbox).toBeVisible({ timeout: 10_000 }); + + return listbox.locator('[role="option"]').allInnerTexts(); +} + +/** + * 엔진 로컬 상태에서 오버라이드 필드 값을 읽는다. + * + * @param page Playwright 페이지 + * @returns 현재 폼 값 (미설정이면 '') + */ +async function readOverrideValue(page: Page): Promise<unknown> { + return page.evaluate(() => { + const form = (window as any).G7Core?.state?.getLocal?.()?.form; + return form?.basic_info?.public_asset_disk ?? ''; + }); +} + +/** + * 드롭다운에서 라벨로 옵션을 고른다. + * + * @param page Playwright 페이지 + * @param label 고를 옵션 라벨(부분 일치) + */ +async function selectOptionByLabel(page: Page, label: string): Promise<void> { + await openAndReadOptions(page); + await page.locator('[role="listbox"] [role="option"]', { hasText: label }).first().click(); +} + +/** 저장 버튼을 눌러 성공 토스트를 기다린다. */ +async function save(page: Page): Promise<void> { + await page.locator('#save_button button, button#save_button, [data-testid="settings-save-button"]').first().click(); + await expect(page.getByText('설정이 저장되었습니다')).toBeVisible({ timeout: 20_000 }); +} + +test.describe('이커머스 공개 자산 디스크 오버라이드', () => { + // @scenario consumer=product, disk_setting=none, e2e=ecommerce_settings_field, hook=unregistered, override=follow_core, row_state=legacy_local_row + // @effects settings_catalog_includes_plugin_registered_disks + test('코어 따름 기본 옵션 + 코어 카탈로그를 그린다', async ({ page, settingsToken }) => { + await authenticatePage(page, settingsToken); + await gotoBasicInfoTab(page); + + await expect(page.locator(MODULE_SELECT_ROOT)).toBeVisible({ timeout: 20_000 }); + + const labels = await openAndReadOptions(page); + // 첫 옵션 = 코어 설정 따름('') + 카탈로그(none 포함 — 강제 스트리밍 오버라이드) + expect(labels[0]).toContain('코어 설정 따름'); + expect(labels.join('|')).toContain('사용 안 함'); + expect(labels.join('|')).toContain('Amazon S3'); + + await page.keyboard.press('Escape'); + }); + + // @scenario consumer=product, disk_setting=public, e2e=save_roundtrip, hook=unregistered, override=module_override, row_state=new_remote_row + // @effects none_override_forces_streaming_over_global + test('선택-저장-재진입 왕복에서 오버라이드 값이 유지되고 원복된다', async ({ page, settingsToken }) => { + test.setTimeout(150_000); + await authenticatePage(page, settingsToken); + await gotoBasicInfoTab(page); + + const original = (await readOverrideValue(page)) as string; + + // 모듈 개별 오버라이드로 Public 디스크 지정 → 저장 + await selectOptionByLabel(page, 'Public'); + await expect.poll(() => readOverrideValue(page)).toBe('public'); + await save(page); + + // 재진입 — 서버 저장값이 폼으로 복원되는지 (저장 응답의 카탈로그 재부착 포함) + await gotoBasicInfoTab(page); + await expect.poll(() => readOverrideValue(page), { timeout: 20_000 }).toBe('public'); + + // 원복 (사이트 설정 보존) + const restoreLabel = + original === 'public' ? 'Public' : original === 's3' ? 'Amazon S3' : original === 'none' ? '사용 안 함' : '코어 설정 따름'; + await selectOptionByLabel(page, restoreLabel); + await save(page); + + await gotoBasicInfoTab(page); + await expect.poll(() => readOverrideValue(page), { timeout: 20_000 }).toBe(original || ''); + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-api-config.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-api-config.spec.ts index 9ac9a146..ab298e8e 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-api-config.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-api-config.spec.ts @@ -1,12 +1,11 @@ /** * 관리자 배송정책 — 계산 API 연동 고급 설정 (skeleton, placeholder). * - * @scenario http_method GET POST × auth_type none bearer custom_header × response_type json text - * @effects method_get_sends_query_string, bearer_attaches_authorization_header, - * custom_header_requires_header_name, field_map_renames_request_keys, - * json_nested_path_extracted, text_currency_stripped_to_number, - * auth_token_masked_in_response, test_call_returns_preview_and_fee, - * options_from_backend_enum_ssot + * 축 요약(마커 아님 — 평문): http_method=GET|POST, auth_type=none|bearer|custom_header, + * response_type=json|text. 요약을 시나리오 축 마커로 적으면 파서가 `=` 없는 토큰을 버려 빈 조합 + * `{}` 이 되고, 축이 0개인 매니페스트가 생기면 어떤 케이스든 커버된 것으로 오집계될 수 있다. + * + * 효과 요약(마커 아님 — 평문): method_get_sends_query_string, bearer_attaches_authorization_header, custom_header_requires_header_name, field_map_renames_request_keys, json_nested_path_extracted, text_currency_stripped_to_number, auth_token_masked_in_response, test_call_returns_preview_and_fee, options_from_backend_enum_ssot. * * 배경(MP12): 배송정책 부과정책 "외부 API 연동" 선택 시 HTTP 메서드/인증/필드매핑/응답형식을 * 설정하고, "테스트 호출" 버튼으로 실제 API 응답·추출 배송비를 미리볼 수 있다. 인증 토큰은 @@ -34,6 +33,7 @@ import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; const NEW_POLICY_URL = '/admin/ecommerce/shipping-policies/create'; test.describe.skip('관리자 배송정책 — 계산 API 연동 고급 설정 (placeholder — data-testid 보강 후 활성화)', () => { + /** @effects custom_header_requires_header_name */ test('인증 custom_header 선택 시 헤더명 입력란이 노출된다', async ({ page, settingsToken }) => { await authenticatePage(page, settingsToken); await page.goto(NEW_POLICY_URL); @@ -45,6 +45,7 @@ test.describe.skip('관리자 배송정책 — 계산 API 연동 고급 설정 ( await expect(page.getByTestId('api-auth-token')).toBeVisible(); }); + /** @effects options_from_backend_enum_ssot */ test('응답 형식 text 선택 시 응답 경로 입력란이 숨겨진다', async ({ page, settingsToken }) => { await authenticatePage(page, settingsToken); await page.goto(NEW_POLICY_URL); @@ -55,6 +56,7 @@ test.describe.skip('관리자 배송정책 — 계산 API 연동 고급 설정 ( await expect(page.getByTestId('api-response-path')).not.toBeVisible(); }); + /** @effects method_get_sends_query_string, test_call_returns_preview_and_fee */ test('테스트 호출 → 요청 메서드 + 응답 상태 + 추출 배송비가 표시된다', async ({ page, settingsToken }) => { await authenticatePage(page, settingsToken); await page.goto(NEW_POLICY_URL); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-country-i18n.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-country-i18n.spec.ts index c6ffa6aa..0ca9905f 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-country-i18n.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-country-i18n.spec.ts @@ -19,11 +19,11 @@ * 폴백)만 골라 보면 통과해 버린다. **ko 값과 다른 국가가 하나라도 있는지** 먼저 확인한 * 뒤, 그 국가에서 화면값 === en 값을 단언한다. * - * @scenario shipping_country_i18n locale=en|ja × surface=settings_list|add_form - * @effects country_names_localized_by_active_locale, - * country_names_not_pinned_to_ko, - * add_form_inputs_follow_installed_locales, - * add_button_enabled_by_any_locale_name + * 축 요약(마커 아님 — 평문): locale=en|ja, surface=settings_list|add_form. 요약을 시나리오 축 마커 + * 로 적으면 파서가 쉼표로만 축을 분리하므로 `×` 표기는 첫 축만, 그것도 나머지 전부를 값으로 + * 삼킨 형태로 집계된다. 실제 커버는 각 test 의 라인 마커가 담당한다. + * + * 효과 요약(마커 아님 — 평문): country_names_localized_by_active_locale, country_names_not_pinned_to_ko, add_form_inputs_follow_installed_locales, add_button_enabled_by_any_locale_name. */ import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; import type { Page } from '@playwright/test'; diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-policy-tier-boundary.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-policy-tier-boundary.spec.ts new file mode 100644 index 00000000..52334d0c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-policy-tier-boundary.spec.ts @@ -0,0 +1,450 @@ +/** + * 배송정책 구간 경계·단위 환산 (공개 이슈 #94). + * + * 관리자 화면이 "1~5개: 3,000원 / 6개~: 5,000원" 이라고 약속한 정책에서 수량 5가 어느 구간에도 + * 매칭되지 않아 배송비가 0원(무료배송)이 됐다. 상품 옵션은 g/cm³ 로 저장되는데 배송정책은 + * kg/L 로 설정되므로, 500g 상품에 "1kg당 1,000원" 을 걸면 500,000원이 청구됐다. + * + * 두 결함 모두 저장된 값은 정상이고 화면도 오류를 내지 않는다 — 금액만 틀린다. 그래서 이 + * spec 은 관리자 저장(검증 계약)과 상점 장바구니(계산 결과)를 한 흐름에서 확인한다. + * + * @scenario charge_policy=range_quantity, charge_policy=range_weight, charge_policy=per_weight, + * boundary=at_max, boundary=decimal, unit_source=g, tier_shape=inclusive + * @effects fee_matches_admin_promise, + * boundary_value_charged_not_free, + * unit_converted_kg_l, + * invalid_config_rejected_422, + * tier_min_derived_from_previous_max, + * range_unit_label_displayed + */ +import { test, expect, authenticatePage } from '../../fixtures/ecommerce-auth'; + +const POLICY_API = '/api/modules/sirsoft-ecommerce/admin/shipping-policies'; +const PRODUCT_API = '/api/modules/sirsoft-ecommerce/admin/products'; +const CATEGORY_API = '/api/modules/sirsoft-ecommerce/admin/categories'; +const CART_API = '/api/modules/sirsoft-ecommerce/cart'; +const POLICY_FORM_URL = '/admin/ecommerce/shipping-policies/create'; +const POLICY_EDIT_URL = (id: number) => `/admin/ecommerce/shipping-policies/${id}/edit`; + +type ApiResult = { status: number; body: any }; + +/** + * 로그인된 페이지 컨텍스트에서 API 를 호출합니다. + * + * @param page Playwright 페이지 + * @param method HTTP 메서드 + * @param url 요청 URL + * @param body 요청 본문 (없으면 생략) + * @returns 상태코드와 파싱된 응답 + */ +async function api(page: any, method: string, url: string, body?: any): Promise<ApiResult> { + return page.evaluate( + async ({ method, url, body }: { method: string; url: string; body?: any }) => { + const res = await fetch(url, { + method, + headers: { + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + + let parsed: any = null; + try { + parsed = await res.json(); + } catch { + parsed = null; + } + + return { status: res.status, body: parsed }; + }, + { method, url, body }, + ); +} + +/** + * 국가별 설정 1건을 가진 배송정책 페이로드를 만듭니다. + * + * @param name 정책명 + * @param countrySetting 국가별 설정 오버라이드 + */ +function policyPayload(name: string, countrySetting: Record<string, any>) { + return { + name: { ko: name }, + is_active: true, + is_default: false, + sort_order: 0, + country_settings: [ + { + country_code: 'KR', + shipping_method: 'parcel', + charge_policy: 'fixed', + base_fee: 3000, + extra_fee_enabled: false, + is_active: true, + ...countrySetting, + }, + ], + }; +} + +test.describe('배송정책 구간 경계·단위 환산 (#94)', () => { + test('저장 검증: 무음 0원을 만드는 설정은 422 로 차단된다', async ({ page, shippingBoundaryToken }) => { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const suffix = `${Date.now()}`.slice(-9); + const createdIds: number[] = []; + + /** + * 저장 요청 결과를 확인하고, 만들어진 정책이 있으면 정리 대상에 등록합니다. + * + * 422 를 기대한 요청이 통과해 버리면 그 정책이 사이트에 그대로 남는다 — 실패한 + * 단언보다 잔여 데이터가 더 오래 남으므로 생성 여부와 무관하게 수집한다. + */ + const track = (result: ApiResult): ApiResult => { + if (result.body?.data?.id) { + createdIds.push(result.body.data.id); + } + + return result; + }; + + try { + // ① 구간별 정책인데 구간이 없음 → 모든 주문이 0원이 된다 + const noTiers = track(await api(page, 'POST', POLICY_API, policyPayload(`E2E 구간없음 ${suffix}`, { + charge_policy: 'range_quantity', + base_fee: 0, + ranges: { type: 'quantity', tiers: [] }, + }))); + expect(noTiers.status).toBe(422); + + // ② 중간 구간의 종료값 공란 → 뒤 구간이 영구히 도달 불가 + const middleMax = track(await api(page, 'POST', POLICY_API, policyPayload(`E2E 중간무제한 ${suffix}`, { + charge_policy: 'range_weight', + base_fee: 0, + ranges: { + type: 'weight', + tiers: [ + { min: 0, max: null, fee: 3000 }, + { min: 2, max: null, fee: 5000 }, + ], + }, + }))); + expect(middleMax.status).toBe(422); + + // ③ 조건부 무료인데 기준금액 공란 + const noThreshold = track(await api(page, 'POST', POLICY_API, policyPayload(`E2E 기준없음 ${suffix}`, { + charge_policy: 'conditional_free', + base_fee: 3000, + free_threshold: null, + }))); + expect(noThreshold.status).toBe(422); + + // ④ 단위당 정책인데 단위값 공란 + const noUnit = track(await api(page, 'POST', POLICY_API, policyPayload(`E2E 단위없음 ${suffix}`, { + charge_policy: 'per_weight', + base_fee: 1000, + ranges: { type: 'per_weight' }, + }))); + expect(noUnit.status).toBe(422); + + // ⑤ 연속형 구간의 소수 경계는 정상 설정이다 (구 규칙에서는 저장조차 불가했다) + const decimal = track(await api(page, 'POST', POLICY_API, policyPayload(`E2E 소수구간 ${suffix}`, { + charge_policy: 'range_weight', + base_fee: 0, + ranges: { + type: 'weight', + tiers: [ + { min: 0, max: 2.5, fee: 3000 }, + { min: 2.5, max: null, fee: 5000 }, + ], + }, + }))); + expect(decimal.status).toBe(201); + } finally { + for (const id of createdIds) { + await api(page, 'DELETE', `${POLICY_API}/${id}`); + } + } + }); + + test('구간 폼 UI: 시작값 자동 파생·읽기전용, 단위 라벨, 단위값 선제 검증', async ({ + page, + shippingBoundaryToken, + }) => { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const suffix = `${Date.now()}`.slice(-9); + let policyId: number | null = null; + + try { + // 소수 경계를 가진 무게 구간 — 시작값이 직전 종료값과 같은 연속형이다 + const policy = await api(page, 'POST', POLICY_API, policyPayload(`E2E 폼UI ${suffix}`, { + charge_policy: 'range_weight', + base_fee: 0, + ranges: { + type: 'weight', + tiers: [ + { min: 0, max: 2.5, fee: 3000 }, + { min: 2.5, max: null, fee: 5000 }, + ], + }, + })); + expect(policy.status, JSON.stringify(policy.body?.errors ?? {})).toBe(201); + policyId = policy.body.data.id; + + await page.goto(POLICY_EDIT_URL(policyId as number)); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const firstRow = page.locator('#range_tier_row_0'); + await expect(firstRow).toBeVisible({ timeout: 30_000 }); + + // 시작값 칸은 파생값이라 운영자가 직접 고칠 수 없다 + await expect(firstRow.locator('input').first()).toHaveJSProperty('readOnly', true); + + // 구간 표 머리글의 단위는 부과정책(무게)에서 파생된다 + const headers = await page.locator('#range_tiers_table thead th').allInnerTexts(); + expect(headers[0], '시작값 머리글 단위').toContain('kg'); + expect(headers[1], '종료값 머리글 단위').toContain('kg'); + + // 종료값을 고치면 다음 구간 시작값이 따라온다 (연속형 = 직전 종료값) + const secondRowMin = page.locator('#range_tier_row_1').locator('input').first(); + await expect(secondRowMin).toHaveValue('2.5'); + + await firstRow.locator('input').nth(1).fill('3.5'); + await expect(secondRowMin).toHaveValue('3.5', { timeout: 10_000 }); + + // 부과정책을 단위당으로 바꾸면 단위 표기가 그 정책을 따라간다 + await page.locator('#field_charge_policy').getByRole('button').first().click(); + const listbox = page.getByRole('listbox'); + await expect(listbox).toBeVisible({ timeout: 10_000 }); + await listbox.getByRole('option', { name: '무게당 배송비', exact: true }).click(); + + const unitField = page.locator('#field_unit_value'); + await expect(unitField).toBeVisible({ timeout: 10_000 }); + await expect(unitField).toContainText('kg'); + + // 단위값을 비우면 저장 요청 없이 화면에서 먼저 오류가 뜬다 + const requestsBefore = page.waitForRequest( + (req) => req.url().includes('/admin/shipping-policies') && req.method() !== 'GET', + { timeout: 3_000 }, + ).catch(() => null); + + await unitField.locator('input').first().fill(''); + await expect(unitField).toContainText('단위값', { timeout: 10_000 }); + expect(await requestsBefore, '선제 검증은 저장 요청 없이 이뤄진다').toBeNull(); + } finally { + if (policyId) { + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await api(page, 'DELETE', `${POLICY_API}/${policyId}`); + } + } + }); + + test('수량 구간의 상한은 포함이다 — 장바구니 4/5/6개 배송비', async ({ + page, + shippingBoundaryToken, + customerToken, + }) => { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const suffix = `${Date.now()}`.slice(-9); + let policyId: number | null = null; + let productId: number | null = null; + let optionId: number | null = null; + + try { + // 공개 이슈 #94 본문과 동일한 설정: 1~5개 3,000원 / 6개~ 5,000원 + const policy = await api(page, 'POST', POLICY_API, policyPayload(`E2E 수량구간 ${suffix}`, { + charge_policy: 'range_quantity', + base_fee: 0, + ranges: { + type: 'quantity', + tiers: [ + { min: 0, max: 5, fee: 3000 }, + { min: 6, max: null, fee: 5000 }, + ], + }, + })); + expect(policy.status).toBe(201); + policyId = policy.body.data.id; + + // 목록 요약이 관리자에게 약속하는 문장 — 단위(개)가 붙어야 한다 + expect(String(policy.body.data.fee_summary)).toContain('개'); + + const categories = await api(page, 'GET', `${CATEGORY_API}?per_page=1`); + const categoryId = categories.body?.data?.data?.[0]?.id ?? categories.body?.data?.[0]?.id; + expect(categoryId, '카테고리가 1건 이상 있어야 상품을 만들 수 있다').toBeTruthy(); + + const product = await api(page, 'POST', PRODUCT_API, { + name: { ko: `E2E 구간경계 상품 ${suffix}` }, + product_code: `E2E-BOUND-${suffix}`, + category_ids: [categoryId], + list_price: 10000, + selling_price: 10000, + stock_quantity: 999, + sales_status: 'on_sale', + display_status: 'visible', + tax_status: 'taxable', + shipping_policy_id: policyId, + options: [ + { + option_code: `E2E-BOUND-${suffix}-001`, + option_name: { ko: '기본' }, + option_values: [{ key: { ko: '구성' }, value: { ko: '기본' } }], + list_price: 10000, + selling_price: 10000, + stock_quantity: 999, + is_default: true, + is_active: true, + }, + ], + }); + expect(product.status, JSON.stringify(product.body?.errors ?? {})).toBe(201); + productId = product.body.data.id; + optionId = product.body.data.options?.[0]?.id; + expect(optionId).toBeTruthy(); + + // 구매자로 전환해 장바구니에서 실제 계산 결과를 본다 + await authenticatePage(page, customerToken); + await page.goto('/shop/cart'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + // product_id 는 최상위, 옵션·수량은 items[] (BulkAddToCartRequest 계약) + const added = await api(page, 'POST', CART_API, { + product_id: productId, + items: [{ product_option_id: optionId, quantity: 4 }], + }); + expect(added.status, JSON.stringify(added.body ?? {})).toBe(201); + const cartItemId = added.body?.data?.items?.[0]?.id; + expect(cartItemId).toBeTruthy(); + + const expectations: Array<[number, number]> = [ + [4, 3000], + [5, 3000], // 경계값 — 기존에는 어느 구간에도 걸리지 않아 0원이었다 + [6, 5000], + ]; + + for (const [quantity, expectedFee] of expectations) { + await api(page, 'PATCH', `${CART_API}/${cartItemId}/quantity`, { quantity }); + + const cart = await api(page, 'POST', `${CART_API}/query`, { selected_ids: [cartItemId] }); + expect(cart.status).toBe(200); + + const totalShipping = cart.body?.data?.calculation?.summary?.total_shipping; + expect(totalShipping, `수량 ${quantity} 의 배송비`).toBe(expectedFee); + } + + await api(page, 'DELETE', `${CART_API}/${cartItemId}`); + } finally { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + if (productId) { + await api(page, 'DELETE', `${PRODUCT_API}/${productId}`); + } + if (policyId) { + await api(page, 'DELETE', `${POLICY_API}/${policyId}`); + } + } + }); + + test('무게당 정책은 g 저장값을 kg 으로 환산한다 — 500g 상품 1개', async ({ + page, + shippingBoundaryToken, + customerToken, + }) => { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const suffix = `${Date.now()}`.slice(-9); + let policyId: number | null = null; + let productId: number | null = null; + let optionId: number | null = null; + + try { + const policy = await api(page, 'POST', POLICY_API, policyPayload(`E2E 무게당 ${suffix}`, { + charge_policy: 'per_weight', + base_fee: 1000, + ranges: { type: 'per_weight', unit_value: 1 }, + })); + expect(policy.status).toBe(201); + policyId = policy.body.data.id; + + const categories = await api(page, 'GET', `${CATEGORY_API}?per_page=1`); + const categoryId = categories.body?.data?.data?.[0]?.id ?? categories.body?.data?.[0]?.id; + expect(categoryId).toBeTruthy(); + + const product = await api(page, 'POST', PRODUCT_API, { + name: { ko: `E2E 무게환산 상품 ${suffix}` }, + product_code: `E2E-WEIGHT-${suffix}`, + category_ids: [categoryId], + list_price: 10000, + selling_price: 10000, + stock_quantity: 999, + sales_status: 'on_sale', + display_status: 'visible', + tax_status: 'taxable', + shipping_policy_id: policyId, + options: [ + { + option_code: `E2E-WEIGHT-${suffix}-001`, + option_name: { ko: '기본' }, + option_values: [{ key: { ko: '구성' }, value: { ko: '기본' } }], + list_price: 10000, + selling_price: 10000, + stock_quantity: 999, + // 상품 옵션의 저장 단위는 g — 정책의 1kg 과 단위가 다르다 + weight: 500, + is_default: true, + is_active: true, + }, + ], + }); + expect(product.status, JSON.stringify(product.body?.errors ?? {})).toBe(201); + productId = product.body.data.id; + optionId = product.body.data.options?.[0]?.id; + + // 저장한 무게가 응답에 실려야 관리자 수정 화면이 0 으로 덮어쓰지 않는다 + expect(Number(product.body.data.options?.[0]?.weight)).toBe(500); + + await authenticatePage(page, customerToken); + await page.goto('/shop/cart'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const added = await api(page, 'POST', CART_API, { + product_id: productId, + items: [{ product_option_id: optionId, quantity: 1 }], + }); + expect(added.status, JSON.stringify(added.body ?? {})).toBe(201); + const cartItemId = added.body?.data?.items?.[0]?.id; + + const cart = await api(page, 'POST', `${CART_API}/query`, { selected_ids: [cartItemId] }); + // 500g = 0.5kg → ceil(0.5 / 1) = 1단위 × 1,000원. 기존 버그값은 500,000원이었다. + expect(cart.body?.data?.calculation?.summary?.total_shipping).toBe(1000); + + await api(page, 'DELETE', `${CART_API}/${cartItemId}`); + } finally { + await authenticatePage(page, shippingBoundaryToken); + await page.goto(POLICY_FORM_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + if (productId) { + await api(page, 'DELETE', `${PRODUCT_API}/${productId}`); + } + if (policyId) { + await api(page, 'DELETE', `${POLICY_API}/${policyId}`); + } + } + }); +}); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/cash-receipt-lifecycle.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/cash-receipt-lifecycle.spec.ts index 87601a15..7fb91097 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/cash-receipt-lifecycle.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/cash-receipt-lifecycle.spec.ts @@ -679,4 +679,50 @@ test.describe('현금영수증 전주기 (신청 → 입금확인 → 자동발 '거부된 조합인데 영수증이 발급되었다' ).toBeNull(); }); + + // 발급사를 제공하는 확장이 없는 상태(미설정 또는 플러그인 제거)에서의 계약. + // 위 테스트들과 정확히 반대 조건이라 서로 배타적으로 skip 된다. + // @scenario provider_state=dead + // @effects issue_blocked_when_provider_unregistered, dead_provider_nulled_in_resource + test('발급사가 없으면 신규 발급이 차단되고 발급사 필드가 null 로 내려간다', async ({ + page, + orderManageToken, + }) => { + await authenticatePage(page, orderManageToken); + await page.addInitScript((l) => localStorage.setItem('g7_locale', l), CART_LOCALE); + await page.goto('/shop'); + await page.waitForLoadState('domcontentloaded'); + + const provider = await cashReceiptProvider(page); + test.skip(provider !== null, '현금영수증 발급사가 설정된 환경 — 이 계약은 미설정 상태 전용'); + + const orderNumber = await placeDbankOrderWithCashReceipt(page, null); + const orderId = await resolveOrderId(page, orderNumber); + + // 주문 상세가 발급사를 null 로 내려야 화면이 발급 버튼을 켜지 않는다 + const detail = await api(page, `/api/modules/sirsoft-ecommerce/admin/orders/${orderId}`); + expect(detail.status, `주문 상세 조회 실패: ${detail.body}`).toBe(200); + + const payment = JSON.parse(detail.body)?.data?.payment ?? {}; + expect( + payment, + '주문 상세 응답에 결제 정보가 없다 — 이후 단언이 무의미해진다' + ).toHaveProperty('cash_receipt_provider'); + expect( + payment.cash_receipt_provider ?? null, + '발급사가 없는데 발급사 필드가 값을 갖고 있다' + ).toBeNull(); + + // 발급 시도는 거부되고 발급 이력도 남지 않아야 한다 + const attempt = await api(page, `/api/modules/sirsoft-ecommerce/admin/orders/${orderId}/cash-receipt`, { + method: 'POST', + body: { receipt_type: 'income', identifier_type: 'phone', identifier: '01012345678' }, + }); + + expect(attempt.status, `발급사가 없는데 발급이 수락됐다: ${attempt.body}`).not.toBe(200); + expect( + (await readReceiptState(page, orderId)).active, + '발급사가 없는데 활성 영수증이 생겼다' + ).toBeNull(); + }); }); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts index aa007335..eb92aa42 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts +++ b/modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/checkout-cash-receipt.spec.ts @@ -255,6 +255,39 @@ test.describe('체크아웃 현금영수증 신청 폼 (무통장 슬롯 주입) await expect(page.locator(SLOT)).toHaveCount(0); }); + // A3 — 저장값은 남아 있는데 그 프로바이더를 제공하는 확장이 없으면 미설정과 동일하게 다뤄야 한다. + // 공개 응답이 raw 값을 그대로 내보내면 신청 폼이 계속 렌더되고, 신청하면 구독자 없는 훅을 + // 호출해 발급 실패로만 조용히 기록된다. + // @scenario cash-receipt-ui-and-refund-bank provider_state=dead + // @effects dead_cash_receipt_provider_normalized_in_public, checkout_slot_hidden_when_provider_unset + test('공개 결제설정의 현금영수증 프로바이더는 등록된 확장이 있을 때만 실린다', async ({ page }) => { + const settings = await page.evaluate(async () => { + const response = await fetch('/api/modules/sirsoft-ecommerce/settings/payment', { + headers: { Accept: 'application/json' }, + }); + return response.json(); + }); + + const provider = settings?.data?.cash_receipt_provider ?? null; + + // 화면 렌더와 공개 응답이 같은 판정을 공유해야 한다 — 한쪽만 살아 있으면 폼은 뜨는데 + // 신청은 실패하는 상태가 된다. + // + // selectDbank() 헬퍼는 슬롯이 보이는 것을 전제하므로 여기서는 쓰지 않는다 — + // 이 테스트는 "슬롯이 없어야 하는 경우" 도 판정 대상이다. + await gotoCheckout(page); + await paymentMethod(page, 'dbank').click(); + + await expect(page.locator(SLOT)).toHaveCount(provider ? 1 : 0); + + if (provider === null) { + test.info().annotations.push({ + type: 'coverage-note', + description: '현금영수증 프로바이더 미설정 환경 — 폼 미렌더 방향만 검증', + }); + } + }); + test('신청 체크 전에는 입력 필드가 없고, 체크하면 마운트된다', async ({ page }) => { await gotoCheckout(page); await selectDbank(page); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Database/Seeders/ShippingPolicySeederTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Database/Seeders/ShippingPolicySeederTest.php index 384f9fa8..2402408f 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Database/Seeders/ShippingPolicySeederTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Database/Seeders/ShippingPolicySeederTest.php @@ -2,8 +2,12 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Database\Seeders; +use Illuminate\Routing\Redirector; +use Illuminate\Validation\ValidationException; use Modules\Sirsoft\Ecommerce\Database\Seeders\Sample\ShippingPolicySeeder; +use Modules\Sirsoft\Ecommerce\Database\Seeders\ShippingTypeSeeder; use Modules\Sirsoft\Ecommerce\Enums\ChargePolicyEnum; +use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreShippingPolicyRequest; use Modules\Sirsoft\Ecommerce\Models\ShippingPolicy; use Modules\Sirsoft\Ecommerce\Models\ShippingPolicyCountrySetting; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; @@ -142,10 +146,83 @@ class ShippingPolicySeederTest extends ModuleTestCase $this->assertArrayHasKey('min', $tier); $this->assertArrayHasKey('max', $tier); - $this->assertArrayHasKey('unit', $tier); $this->assertArrayHasKey('fee', $tier); } + /** + * 시더가 만든 모든 정책이 관리자 저장 검증(StoreShippingPolicyRequest)을 통과해야 한다. + * + * 시더는 Model::create 로 검증을 우회하므로, 키 존재만 확인하면 화면에서는 저장조차 + * 불가한 설정이 샘플 데이터로 배포된다 (구간 연속성·필수값 규칙 위반). + */ + public function test_seeded_policies_pass_admin_store_validation(): void + { + $this->seed(ShippingTypeSeeder::class); + $this->runSeeder(); + + $policies = ShippingPolicy::with('countrySettings')->get(); + $this->assertTrue($policies->isNotEmpty()); + + foreach ($policies as $policy) { + $payload = [ + 'name' => $policy->name, + 'is_active' => $policy->is_active, + 'is_default' => $policy->is_default, + 'sort_order' => $policy->sort_order, + 'country_settings' => $policy->countrySettings + ->map(fn (ShippingPolicyCountrySetting $cs) => [ + 'country_code' => $cs->country_code, + 'shipping_method' => $cs->shipping_method, + 'custom_shipping_name' => $cs->custom_shipping_name, + 'currency_code' => $cs->currency_code, + 'charge_policy' => $cs->charge_policy->value, + 'base_fee' => (float) $cs->base_fee, + 'free_threshold' => $cs->free_threshold !== null ? (float) $cs->free_threshold : null, + 'ranges' => $cs->ranges, + 'api_endpoint' => $cs->api_endpoint, + 'api_request_fields' => $cs->api_request_fields, + 'api_response_fee_field' => $cs->api_response_fee_field, + 'api_config' => $cs->api_config, + 'extra_fee_enabled' => (bool) $cs->extra_fee_enabled, + 'extra_fee_settings' => $cs->extra_fee_settings, + 'extra_fee_multiply' => (bool) $cs->extra_fee_multiply, + 'is_active' => (bool) $cs->is_active, + ]) + ->values() + ->all(), + ]; + + $errors = $this->collectStoreValidationErrors($payload); + + $this->assertSame( + [], + $errors, + "정책 '{$policy->getLocalizedName()}' 이 관리자 저장 검증을 통과하지 못했습니다: " + .json_encode($errors, JSON_UNESCAPED_UNICODE) + ); + } + } + + /** + * 배송정책 저장 요청 검증을 수행하고 에러 메시지 배열을 반환합니다. + * + * @param array $payload 저장 요청 페이로드 + * @return array<string, array<int, string>> 필드별 에러 메시지 (통과 시 빈 배열) + */ + private function collectStoreValidationErrors(array $payload): array + { + $request = StoreShippingPolicyRequest::create('/', 'POST', $payload); + $request->setContainer(app())->setRedirector(app(Redirector::class)); + + try { + $request->validateResolved(); + } catch (ValidationException $e) { + return $e->errors(); + } + + return []; + } + // ======================================== // 단위당(per_*) 정책 테스트 (countrySettings 기반) // ======================================== diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Enums/ShippingPolicyEnumsTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Enums/ShippingPolicyEnumsTest.php index 7604ad81..2356f565 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Enums/ShippingPolicyEnumsTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Enums/ShippingPolicyEnumsTest.php @@ -73,9 +73,23 @@ class ShippingPolicyEnumsTest extends ModuleTestCase $this->assertTrue(ChargePolicyEnum::PER_VOLUME->requiresBaseFee()); $this->assertTrue(ChargePolicyEnum::PER_VOLUME_WEIGHT->requiresBaseFee()); $this->assertTrue(ChargePolicyEnum::PER_AMOUNT->requiresBaseFee()); + // API 정책의 base_fee 는 외부 API 장애 시 폴백 배송비 — 0 이면 장애가 곧 무음 무료배송이 된다 + $this->assertTrue(ChargePolicyEnum::API->requiresBaseFee()); $this->assertFalse(ChargePolicyEnum::FREE->requiresBaseFee()); $this->assertFalse(ChargePolicyEnum::RANGE_AMOUNT->requiresBaseFee()); - $this->assertFalse(ChargePolicyEnum::API->requiresBaseFee()); + } + + public function test_charge_policy_has_discrete_range_values(): void + { + // 수량만 이산형 — "5개 다음은 6개"(max + 1)로 이어진다 + $this->assertTrue(ChargePolicyEnum::RANGE_QUANTITY->hasDiscreteRangeValues()); + + // 금액·무게·부피는 연속값 — "5kg 다음은 5kg 초과"(다음 min = max) + $this->assertFalse(ChargePolicyEnum::RANGE_AMOUNT->hasDiscreteRangeValues()); + $this->assertFalse(ChargePolicyEnum::RANGE_WEIGHT->hasDiscreteRangeValues()); + $this->assertFalse(ChargePolicyEnum::RANGE_VOLUME->hasDiscreteRangeValues()); + $this->assertFalse(ChargePolicyEnum::RANGE_VOLUME_WEIGHT->hasDiscreteRangeValues()); + $this->assertFalse(ChargePolicyEnum::PER_QUANTITY->hasDiscreteRangeValues()); } public function test_charge_policy_requires_free_threshold(): void diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Requests/StoreEcommerceSettingsRequestPublicAssetDiskTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Requests/StoreEcommerceSettingsRequestPublicAssetDiskTest.php new file mode 100644 index 00000000..880f2183 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Requests/StoreEcommerceSettingsRequestPublicAssetDiskTest.php @@ -0,0 +1,113 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Http\Requests; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\Config; +use Illuminate\Validation\ValidationException; +use Modules\Sirsoft\Ecommerce\Http\Requests\Admin\StoreEcommerceSettingsRequest; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 이커머스 공개 자산 디스크 저장 검증 테스트 (공개#100) + * + * 모듈 오버라이드 필드도 코어 환경설정(SaveSettingsRequest)과 같은 강도로 검증한다. + * 이 게이트가 없으면 오타 디스크명이 200 으로 저장된 뒤 런타임 폴백만 남아, + * 운영자에게는 "저장은 됐는데 CDN 이 안 붙는" 무증상 상태로만 보인다. + * + * 선택지는 코어 3종 + 플러그인 훅 등록 디스크라 정적 in 으로 고정할 수 없으므로, + * 카탈로그 게터(SSoT)를 조회하는 closure 가 실제로 걸려 있는지를 고정한다. + * + * @effects invalid_disk_rejected_with_422, settings_catalog_includes_plugin_registered_disks + */ +class StoreEcommerceSettingsRequestPublicAssetDiskTest extends ModuleTestCase +{ + protected function tearDown(): void + { + HookManager::resetAll(); + + parent::tearDown(); + } + + /** + * basic_info 탭 저장 요청을 해석합니다. + * + * @param string $disk public_asset_disk 값 + */ + private function resolve(string $disk): StoreEcommerceSettingsRequest + { + $request = StoreEcommerceSettingsRequest::create('/', 'POST', [ + '_tab' => 'basic_info', + 'basic_info' => [ + // 같은 탭의 필수 형제 필드 — 이 축과 무관하지만 없으면 탭 전체가 422 라 + // 디스크 규칙만 분리해 보기 위해 채운다. + 'shop_name' => '테스트 상점', + 'route_path' => 'shop', + 'public_asset_disk' => $disk, + ], + ]); + $request->setContainer($this->app)->setRedirector($this->app['redirect']); + $request->validateResolved(); + + return $request; + } + + public function test_core_catalog_disk_is_accepted(): void + { + $request = $this->resolve('public'); + + $this->assertSame('public', $request->validated()['basic_info']['public_asset_disk']); + } + + public function test_empty_value_is_accepted_as_follow_core(): void + { + $request = $this->resolve(''); + + // 빈 값 = "코어 설정 따름" 이므로 검증을 통과해야 한다. + $this->assertSame('', $request->validated()['basic_info']['public_asset_disk'] ?? ''); + } + + public function test_unknown_disk_is_rejected(): void + { + $this->expectException(ValidationException::class); + + try { + $this->resolve('nonexistent_disk'); + } catch (ValidationException $e) { + $this->assertArrayHasKey( + 'basic_info.public_asset_disk', + $e->validator->errors()->toArray(), + '카탈로그에 없는 디스크는 basic_info.public_asset_disk 오류를 내야 한다' + ); + + throw $e; + } + } + + public function test_plugin_registered_disk_is_accepted(): void + { + // 플러그인이 훅으로 등록한 디스크도 통과해야 한다 — 정적 in 이었다면 여기서 깨진다. + Config::set('filesystems.disks.plugin_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/plugin_cdn'), + 'url' => 'https://plugin-cdn.test/assets', + ]); + + HookManager::addFilter( + 'core.settings.available_public_asset_drivers', + function (array $drivers): array { + $drivers[] = [ + 'id' => 'plugin_cdn', + 'label' => ['ko' => '플러그인 CDN', 'en' => 'Plugin CDN'], + 'provider' => 'test-plugin', + ]; + + return $drivers; + } + ); + + $request = $this->resolve('plugin_cdn'); + + $this->assertSame('plugin_cdn', $request->validated()['basic_info']['public_asset_disk']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/ProductOptionResourceDimensionsTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/ProductOptionResourceDimensionsTest.php new file mode 100644 index 00000000..bc9a0f38 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/ProductOptionResourceDimensionsTest.php @@ -0,0 +1,67 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Http\Resources; + +use Illuminate\Http\Request; +use Modules\Sirsoft\Ecommerce\Http\Resources\ProductOptionResource; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductOption; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 상품 옵션 응답의 무게/부피 직렬화 테스트 (공개 #94 / N8) + * + * 저장된 무게·부피가 응답에 실리지 않으면 관리자 수정 화면이 0 을 표시하고, + * 그 상태로 저장하면 실측값이 덮어써진다. 배송비 계산이 이 값을 읽으므로 + * 왕복(read → edit → save)에서 값이 살아남는지가 이 필드의 계약이다. + * + * @scenario charge_policy=range_weight, unit_source=g + * + * @effects product_option_dimensions_round_trip + */ +class ProductOptionResourceDimensionsTest extends ModuleTestCase +{ + /** + * 옵션을 리소스 배열로 변환합니다. + * + * @param array $attributes 옵션 속성 오버라이드 + * @return array 리소스 배열 + */ + private function toResourceArray(array $attributes): array + { + $product = Product::factory()->create(); + $option = ProductOption::factory()->create(array_merge([ + 'product_id' => $product->id, + ], $attributes)); + + return (new ProductOptionResource($option))->toArray(Request::create('/')); + } + + public function test_it_serializes_weight_and_volume_in_storage_units(): void + { + // 상품 옵션의 저장 단위는 g / cm³ (배송정책의 kg/L 환산은 배송비 계산 시점에만) + $data = $this->toResourceArray(['weight' => 500, 'volume' => 1200]); + + $this->assertArrayHasKey('weight', $data); + $this->assertArrayHasKey('volume', $data); + $this->assertEquals(500.0, $data['weight']); + $this->assertEquals(1200.0, $data['volume']); + } + + public function test_it_preserves_null_instead_of_coercing_to_zero(): void + { + // null 을 0 으로 내리면 "미입력"과 "무게 0" 이 구분되지 않는다 + $data = $this->toResourceArray(['weight' => null, 'volume' => null]); + + $this->assertNull($data['weight']); + $this->assertNull($data['volume']); + } + + public function test_it_preserves_decimal_precision(): void + { + $data = $this->toResourceArray(['weight' => 12.5, 'volume' => 0.75]); + + $this->assertEquals(12.5, $data['weight']); + $this->assertEquals(0.75, $data['volume']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php new file mode 100644 index 00000000..6a3b7b11 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php @@ -0,0 +1,59 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Listeners; + +use Modules\Sirsoft\Ecommerce\Listeners\Ckeditor5ReferenceSourcesListener; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 이커머스 참조 소스 등록 리스너 테스트 + * + * 이 리스너가 빠지면 상품 설명에만 쓰이는 이미지가 "미참조" 로 판정돼 자동 정리 대상이 + * 되므로, 구독 형식(filter)과 append 계약을 회귀로 고정합니다. + */ +class Ckeditor5ReferenceSourcesListenerTest extends ModuleTestCase +{ + /** + * @effects reference_source_listener_subscribes_as_filter + */ + #[Test] + public function it_subscribes_to_the_reference_source_filter_hook(): void + { + $hooks = Ckeditor5ReferenceSourcesListener::getSubscribedHooks(); + + $this->assertArrayHasKey('sirsoft-ckeditor5.image.filter_reference_sources', $hooks); + $this->assertSame('filter', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['type']); + $this->assertSame('addEcommerceSources', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['method']); + } + + /** + * @effects reference_source_listener_appends_without_dropping + */ + #[Test] + public function it_appends_ecommerce_sources_while_preserving_existing_ones(): void + { + $existing = [['table' => 'mail_templates', 'columns' => ['body']]]; + + $result = (new Ckeditor5ReferenceSourcesListener)->addEcommerceSources($existing); + + $this->assertContains($existing[0], $result); + $this->assertContains(['table' => 'ecommerce_products', 'columns' => ['description']], $result); + $this->assertContains(['table' => 'ecommerce_product_common_infos', 'columns' => ['content']], $result); + } + + /** + * @effects reference_source_listener_schema_contract + */ + #[Test] + public function appended_sources_follow_the_declared_schema(): void + { + foreach ((new Ckeditor5ReferenceSourcesListener)->addEcommerceSources([]) as $source) { + $this->assertArrayHasKey('table', $source); + $this->assertArrayHasKey('columns', $source); + $this->assertIsString($source['table']); + $this->assertIsArray($source['columns']); + $this->assertNotEmpty($source['columns']); + } + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/IssueCashReceiptOnDepositListenerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/IssueCashReceiptOnDepositListenerTest.php index d8b27f98..18fc13f5 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/IssueCashReceiptOnDepositListenerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/IssueCashReceiptOnDepositListenerTest.php @@ -18,6 +18,7 @@ use Modules\Sirsoft\Ecommerce\Models\OrderCashReceipt; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -29,6 +30,8 @@ use PHPUnit\Framework\Attributes\Test; */ class IssueCashReceiptOnDepositListenerTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -48,6 +51,7 @@ class IssueCashReceiptOnDepositListenerTest extends ModuleTestCase $this->receiptSequence = 0; $this->issueCalls = []; + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/ProductInquiryBoardListenerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/ProductInquiryBoardListenerTest.php new file mode 100644 index 00000000..6996d258 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/ProductInquiryBoardListenerTest.php @@ -0,0 +1,259 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Listeners; + +use App\Extension\HookManager; +use Illuminate\Support\Carbon; +use Modules\Sirsoft\Ecommerce\Listeners\ProductInquiryBoardListener; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * ProductInquiryBoardListener 단위 테스트 + * + * 게시판 Post 삭제/복원/게시판 삭제 이벤트에 대한 문의 피벗 정합 동작을 검증합니다. + * - after_delete: 질문 글 → 피벗 소프트 삭제, 답변 글 → 잔여 답변 기준 답변완료 재계산 + * - after_restore: 질문 글 → 피벗 복원, 답변 글 → 답변완료 재마킹 (answered_at 보존) + * - board.posts.before_force_delete: 삭제될 글 ID 벌크 수신 → 피벗 일괄 소프트 삭제 (멱등) + * + * Post 객체는 리스너가 get_class($post) 로 inquirable_type 을 매칭하므로, + * stdClass 스텁을 쓰는 테스트는 피벗의 inquirable_type 도 'stdClass' 로 맞춘다. + * (벌크 훅은 FQCN 리터럴 'Modules\Sirsoft\Board\Models\Post' 고정 — 스텁 무관) + */ +class ProductInquiryBoardListenerTest extends ModuleTestCase +{ + private const POST_MODEL = 'Modules\\Sirsoft\\Board\\Models\\Post'; + + protected ProductInquiryBoardListener $listener; + + protected Product $product; + + protected function setUp(): void + { + parent::setUp(); + + $this->listener = app(ProductInquiryBoardListener::class); + $this->product = Product::factory()->create(); + + // 다른 모듈이 등록한 count_replies 필터 잔존 차단 (cross-module contamination) + HookManager::clearFilter('sirsoft-ecommerce.inquiry.count_replies'); + } + + /** + * stdClass 기반 Post 스텁을 생성합니다. + * + * @param int $id Post ID + * @param int|null $parentId 부모 Post ID (답변 글이면 지정) + * @return object Post 스텁 + */ + private function makePostStub(int $id, ?int $parentId = null): object + { + return (object) [ + 'id' => $id, + 'parent_id' => $parentId, + ]; + } + + /** + * getSubscribedHooks 는 3개 액션 훅을 sync=true 로 선언한다. + * + * sync 미선언 시 큐 워커 미가동 환경에서 피벗 정리가 조용히 누락된다(N3). + */ + public function test_subscribed_hooks_declare_three_sync_action_hooks(): void + { + $hooks = ProductInquiryBoardListener::getSubscribedHooks(); + + // 액션 훅 3종 — method + sync=true + $this->assertSame('handlePostDeleted', $hooks['sirsoft-board.post.after_delete']['method']); + $this->assertTrue($hooks['sirsoft-board.post.after_delete']['sync']); + + $this->assertSame('handlePostRestored', $hooks['sirsoft-board.post.after_restore']['method']); + $this->assertTrue($hooks['sirsoft-board.post.after_restore']['sync']); + + $this->assertSame( + 'handleBoardPostsForceDeleting', + $hooks['sirsoft-board.board.posts.before_force_delete']['method'] + ); + $this->assertTrue($hooks['sirsoft-board.board.posts.before_force_delete']['sync']); + + // 필터 훅 2종 — type: 'filter' 명시 (미지정 시 반환값 무시 회귀) + $this->assertSame('filter', $hooks['sirsoft-ecommerce.inquiry.store_validation_rules']['type']); + $this->assertSame('filter', $hooks['sirsoft-ecommerce.inquiry.update_validation_rules']['type']); + } + + /** + * 질문 글 삭제 → 연결된 피벗이 소프트 삭제된다 (복원 대칭 확보). + */ + public function test_handle_post_deleted_soft_deletes_pivot_for_question_post(): void + { + // Given: 질문 Post(101)를 가리키는 피벗 + $pivot = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'stdClass', + 'inquirable_id' => 101, + ]); + + // When: 질문 글(parent_id 없음) 삭제 이벤트 + $this->listener->handlePostDeleted($this->makePostStub(101), 'test-inquiry-board'); + + // Then: 피벗 소프트 삭제 — 행은 잔존, deleted_at 세팅 + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivot->id]); + $this->assertNotNull(ProductInquiry::withTrashed()->find($pivot->id)->deleted_at); + } + + /** + * 답변 글 삭제 + 잔여 답변 0 → 부모 피벗의 답변완료가 해제된다 (피벗은 삭제하지 않음). + */ + public function test_handle_post_deleted_unmarks_answered_when_no_replies_remain(): void + { + // Given: 질문 Post(201)를 가리키는 답변완료 피벗 + $pivot = ProductInquiry::factory()->answered()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'stdClass', + 'inquirable_id' => 201, + ]); + + // And: 잔여 답변 0건 모킹 + HookManager::addFilter('sirsoft-ecommerce.inquiry.count_replies', fn () => 0, priority: 1); + + // When: 답변 글(301, parent_id=201) 삭제 이벤트 + $this->listener->handlePostDeleted($this->makePostStub(301, 201), 'test-inquiry-board'); + + // Then: 답변완료 해제 + answered_at 초기화 + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $pivot->id, + 'is_answered' => false, + 'answered_at' => null, + ]); + + // And: 피벗 자체는 삭제되지 않는다 (질문 분기와 구분 — 답변 분기는 return 으로 종료) + $this->assertNull(ProductInquiry::withTrashed()->find($pivot->id)->deleted_at); + } + + /** + * 답변 글 삭제 + 잔여 답변 1 → 답변완료 표기가 유지된다 (#106 다중 답변 잔재 대응). + */ + public function test_handle_post_deleted_keeps_answered_when_replies_remain(): void + { + // Given: 질문 Post(202)를 가리키는 답변완료 피벗 + $pivot = ProductInquiry::factory()->answered()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'stdClass', + 'inquirable_id' => 202, + ]); + + // And: 잔여 답변 1건 모킹 + HookManager::addFilter('sirsoft-ecommerce.inquiry.count_replies', fn () => 1, priority: 1); + + // When: 답변 글(302, parent_id=202) 삭제 이벤트 + $this->listener->handlePostDeleted($this->makePostStub(302, 202), 'test-inquiry-board'); + + // Then: 답변완료 유지 + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $pivot->id, + 'is_answered' => true, + ]); + $this->assertNotNull(ProductInquiry::find($pivot->id)->answered_at); + } + + /** + * 질문 글 복원 → 소프트 삭제됐던 피벗이 복원된다. + */ + public function test_handle_post_restored_restores_pivot_for_question_post(): void + { + // Given: 질문 Post(401)를 가리키는 소프트 삭제된 피벗 + $pivot = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'stdClass', + 'inquirable_id' => 401, + ]); + $pivot->delete(); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivot->id]); + + // When: 질문 글 복원 이벤트 + $this->listener->handlePostRestored($this->makePostStub(401), 'test-inquiry-board'); + + // Then: 피벗 복원 — 기본 스코프에서 다시 조회 가능 + $this->assertNotNull(ProductInquiry::find($pivot->id)); + $this->assertNull(ProductInquiry::withTrashed()->find($pivot->id)->deleted_at); + } + + /** + * 답변 글 복원 → 부모 피벗이 답변완료로 재마킹되고 answered_at 은 기존값이 보존된다. + */ + public function test_handle_post_restored_remarks_answered_preserving_answered_at(): void + { + // Given: 최초 답변 시각이 남아 있는 미답변 피벗 (질문 Post 402) + $firstAnsweredAt = Carbon::parse('2026-02-02 09:00:00'); + $pivot = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'stdClass', + 'inquirable_id' => 402, + 'is_answered' => false, + 'answered_at' => $firstAnsweredAt, + ]); + + // When: 답변 글(502, parent_id=402) 복원 이벤트 + $this->listener->handlePostRestored($this->makePostStub(502, 402), 'test-inquiry-board'); + + // Then: 답변완료 재마킹 + 최초 답변 시각 보존 (markAsAnswered 의 보존 계약) + $fresh = ProductInquiry::find($pivot->id); + $this->assertTrue($fresh->is_answered); + $this->assertSame( + $firstAnsweredAt->format('Y-m-d H:i:s'), + $fresh->answered_at->format('Y-m-d H:i:s'), + '재마킹이 최초 답변 시각을 덮어쓰면 안 됩니다.' + ); + } + + /** + * 게시판 삭제 벌크 훅 → 삭제될 글 ID 의 피벗을 일괄 소프트 삭제하며, 재호출은 멱등이다. + * + * chunk 당 1회 다회 발화되는 훅이므로 이미 삭제된 피벗은 0건 처리여야 한다. + */ + public function test_handle_board_posts_force_deleting_bulk_soft_deletes_and_is_idempotent(): void + { + // Given: 게시판 Post FQCN 을 가리키는 피벗 2건 + 대상 밖 피벗 1건 + $pivot1 = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => self::POST_MODEL, + 'inquirable_id' => 601, + ]); + $pivot2 = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => self::POST_MODEL, + 'inquirable_id' => 602, + ]); + $untouched = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => self::POST_MODEL, + 'inquirable_id' => 699, + ]); + + $board = (object) ['id' => 7]; + + // When: 벌크 삭제 통지 + $this->listener->handleBoardPostsForceDeleting($board, [601, 602]); + + // Then: 대상 피벗만 소프트 삭제 + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivot1->id]); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivot2->id]); + $this->assertNull(ProductInquiry::withTrashed()->find($untouched->id)->deleted_at); + + // When: 동일 payload 재호출 (chunk 다회 발화 시뮬레이션) — 예외 없이 0건 처리 + $firstDeletedAt = ProductInquiry::withTrashed()->find($pivot1->id)->deleted_at; + $this->listener->handleBoardPostsForceDeleting($board, [601, 602]); + + // Then: 멱등 — deleted_at 이 갱신되지 않고 그대로 유지된다 + $this->assertSame( + $firstDeletedAt->format('Y-m-d H:i:s'), + ProductInquiry::withTrashed()->find($pivot1->id)->deleted_at->format('Y-m-d H:i:s') + ); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivot2->id]); + + // And: 빈 배열 호출은 no-op + $this->listener->handleBoardPostsForceDeleting($board, []); + $this->assertNull(ProductInquiry::withTrashed()->find($untouched->id)->deleted_at); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/PurgeCashReceiptIdentifierListenerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/PurgeCashReceiptIdentifierListenerTest.php index 0b17afbf..4f0e91dd 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/PurgeCashReceiptIdentifierListenerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/PurgeCashReceiptIdentifierListenerTest.php @@ -13,6 +13,7 @@ use Modules\Sirsoft\Ecommerce\Models\OrderCashReceipt; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -23,6 +24,8 @@ use PHPUnit\Framework\Attributes\Test; */ class PurgeCashReceiptIdentifierListenerTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -31,6 +34,7 @@ class PurgeCashReceiptIdentifierListenerTest extends ModuleTestCase { parent::setUp(); + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php index e8f6ee80..934166dd 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php @@ -9,6 +9,7 @@ use App\Models\User; use App\Support\GuestRoleResolver; use App\Support\Query\BoundedCount; use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Listeners\SearchProductsListener; use Modules\Sirsoft\Ecommerce\Services\ProductService; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; @@ -59,6 +60,42 @@ class SearchProductsListenerTest extends ModuleTestCase GuestRoleResolver::flush(); } + /** + * 검색 실패가 "결과 0건" 으로 위장되지 않고 failed 페이로드로 표면화되는지 확인 (#103) + * + * @effects failed_flag_in_response, exception_stack_logged + */ + public function test_search_failure_surfaces_failed_payload_and_logs_exception(): void + { + Log::spy(); + + $exception = new \RuntimeException('DB 오류 재현'); + $this->productService + ->method('searchByKeywordWithCursor') + ->willThrowException($exception); + + $context = [ + 'type' => 'products', + 'q' => '문의', + 'sort' => 'relevance', + 'page' => 1, + 'per_page' => 10, + 'user' => null, + 'request' => null, + ]; + + $result = $this->listener->searchProducts([], $context); + + $this->assertArrayHasKey('products', $result); + $this->assertTrue($result['products']['failed'] ?? false, '실패 카테고리에는 failed 플래그가 실려야 합니다.'); + $this->assertFalse($result['products']['total_is_exact'], '실패한 0건을 "정확한 0건" 으로 말하면 안 됩니다.'); + $this->assertSame([], $result['products']['items']); + + Log::shouldHaveReceived('error') + ->withArgs(fn (string $message, array $ctx = []) => ($ctx['exception'] ?? null) === $exception) + ->once(); + } + /** * getSubscribedHooks()가 올바른 훅 목록을 반환하는지 확인 */ @@ -325,4 +362,41 @@ class SearchProductsListenerTest extends ModuleTestCase $this->assertEquals(0.0, $result['products'][1]['rating_avg']); $this->assertEquals(0, $result['products'][1]['review_count']); } + + /** + * 하이라이트 필드가 원문 태그를 이스케이프하는지 확인 (⑧) + * + * @scenario case=search_highlight_escape + * + * @effects highlighted_fields_escaped + */ + public function test_highlight_keyword_escapes_markup(): void + { + $method = new \ReflectionMethod($this->listener, 'highlightKeyword'); + $method->setAccessible(true); + + $result = $method->invoke($this->listener, '<img src=x onerror=alert(1)> 상품', '상품'); + + $this->assertStringNotContainsString('<img', $result); + $this->assertStringContainsString('<img', $result); + $this->assertStringContainsString('<mark>상품</mark>', $result); + } + + /** + * 본문 프리뷰가 엔티티 인코딩된 태그를 부활시키지 않는지 확인 (N-8) + * + * @scenario case=preview_entity_no_resurrect + * + * @effects preview_does_not_resurrect_entities + */ + public function test_extract_content_preview_does_not_resurrect_entity_encoded_tags(): void + { + $method = new \ReflectionMethod($this->listener, 'extractContentPreview'); + $method->setAccessible(true); + + $result = $method->invoke($this->listener, '<script>alert(1)</script> 상품 설명입니다.', '상품', 150); + + $this->assertStringNotContainsString('<script>', $result); + $this->assertStringContainsString('상품', $result); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/HasDirectAssetUrlTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/HasDirectAssetUrlTest.php new file mode 100644 index 00000000..9372b0d9 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/HasDirectAssetUrlTest.php @@ -0,0 +1,152 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Models; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\Config; +use Modules\Sirsoft\Ecommerce\Models\CategoryImage; +use Modules\Sirsoft\Ecommerce\Models\ProductImage; +use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * HasDirectAssetUrl(download_url 직접 URL/폴백) 단위 테스트 + * + * 행 disk 가 직접 URL 지원 디스크(fake_cdn)면 CDN URL, 아니면 기존 API 경로 폴백을 + * 3개 이미지 모델(상품/카테고리/리뷰)에서 검증합니다. 혼재 운용(구 로컬 행 + 신규 + * 원격 행)과 훅 차단 폴백도 함께 고정합니다. + */ +class HasDirectAssetUrlTest extends ModuleTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + AssetStorage::flush(); + } + + protected function tearDown(): void + { + AssetStorage::flush(); + HookManager::resetAll(); + + parent::tearDown(); + } + + #[Test] + public function product_image_falls_back_to_api_url_for_local_disk_row(): void + { + $image = new ProductImage([ + 'hash' => 'aaaabbbbcccc', + 'disk' => 'modules', + 'path' => 'products/P001/a.jpg', + ]); + + $this->assertSame( + '/api/modules/sirsoft-ecommerce/product-image/aaaabbbbcccc', + $image->download_url + ); + } + + #[Test] + public function product_image_returns_direct_url_for_cdn_disk_row(): void + { + $image = new ProductImage([ + 'hash' => 'aaaabbbbcccc', + 'disk' => 'fake_cdn', + 'path' => 'products/P001/a.jpg', + ]); + + $this->assertSame( + 'https://cdn.test/assets/sirsoft-ecommerce/images/products/P001/a.jpg', + $image->download_url + ); + } + + /** + * @scenario consumer=category, disk_setting=public, e2e=ecommerce_settings_field, hook=supply, override=module_override, row_state=new_remote_row + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable + */ + #[Test] + public function category_and_review_images_resolve_direct_url_and_fallback(): void + { + $cdnCategory = new CategoryImage([ + 'hash' => 'ccc111222333', + 'disk' => 'fake_cdn', + 'path' => 'category/2026/01/01/c.jpg', + ]); + $localReview = new ProductReviewImage([ + 'hash' => 'rrr111222333', + 'disk' => 'modules', + 'path' => 'reviews/1/r.jpg', + ]); + + $this->assertSame( + 'https://cdn.test/assets/sirsoft-ecommerce/images/category/2026/01/01/c.jpg', + $cdnCategory->download_url + ); + $this->assertSame( + '/api/modules/sirsoft-ecommerce/review-image/rrr111222333', + $localReview->download_url + ); + } + + /** + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function mixed_rows_resolve_urls_per_row_disk(): void + { + // 혼재 운용 — 디스크 전환 이전 로컬 행과 이후 원격 행이 각자 disk 기준으로 해석 + $localRow = new ProductImage(['hash' => 'llllllllllll', 'disk' => 'modules', 'path' => 'products/P001/old.jpg']); + $remoteRow = new ProductImage(['hash' => 'rrrrrrrrrrrr', 'disk' => 'fake_cdn', 'path' => 'products/P001/new.jpg']); + + $this->assertStringStartsWith('/api/modules/sirsoft-ecommerce/product-image/', $localRow->download_url); + $this->assertStringStartsWith('https://cdn.test/assets/', $remoteRow->download_url); + } + + /** + * @scenario consumer=review, disk_setting=s3_without_url, e2e=ecommerce_settings_field, hook=unregistered, override=follow_core, row_state=new_remote_row + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable + */ + #[Test] + public function blank_disk_or_path_falls_back_to_api_url(): void + { + $noDisk = new ProductImage(['hash' => 'aaaabbbbcccc', 'disk' => '', 'path' => 'products/P001/a.jpg']); + $noPath = new ProductImage(['hash' => 'aaaabbbbcccc', 'disk' => 'fake_cdn', 'path' => '']); + + $this->assertSame('/api/modules/sirsoft-ecommerce/product-image/aaaabbbbcccc', $noDisk->download_url); + $this->assertSame('/api/modules/sirsoft-ecommerce/product-image/aaaabbbbcccc', $noPath->download_url); + } + + /** + * @scenario consumer=category, disk_setting=none, e2e=ecommerce_settings_field, hook=block, override=follow_core, row_state=legacy_local_row + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable + */ + #[Test] + public function hook_block_falls_back_to_api_url(): void + { + HookManager::addFilter('core.storage.filter_url', fn ($url) => ''); + + $image = new ProductImage([ + 'hash' => 'aaaabbbbcccc', + 'disk' => 'fake_cdn', + 'path' => 'products/P001/a.jpg', + ]); + + $this->assertSame( + '/api/modules/sirsoft-ecommerce/product-image/aaaabbbbcccc', + $image->download_url + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php index 7cae0c18..05c8b30a 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderPaymentTest.php @@ -13,8 +13,11 @@ 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 + * 축 요약(마커 아님 — 평문): method_kind=extension, capability_declared=declared, + * capability=refund_method. 요약을 시나리오 축 마커로 적으면 파서가 쉼표로만 축을 분리하므로 `×` + * 표기는 실재하지 않는 조합 1건으로 집계된다(커버리지를 부풀리는 방향). + * + * 효과 요약(마커 아님 — 평문): extension_id_persisted_as_is, builtin_capability_unchanged, refund_method_is_pg. */ class OrderPaymentTest extends ModuleTestCase { @@ -42,6 +45,9 @@ class OrderPaymentTest extends ModuleTestCase $this->assertEquals($order->id, $payment->order->id); } + /** + * @effects builtin_capability_unchanged + */ public function test_order_payment_method_is_stored_as_plain_string(): void { // payment_method 는 enum 캐스트를 두지 않는다 (#475) — PG 플러그인이 등록하는 @@ -54,6 +60,9 @@ class OrderPaymentTest extends ModuleTestCase $this->assertTrue($payment->isCardPayment()); } + /** + * @effects extension_id_persisted_as_is + */ public function test_order_payment_accepts_extension_payment_method_id(): void { // 확장 결제수단 ID 를 1급 시민으로 저장할 수 있어야 한다 (캐스트가 있으면 ValueError). diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Repositories/ProductInquiryRepositoryTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Repositories/ProductInquiryRepositoryTest.php new file mode 100644 index 00000000..aad4ad94 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Repositories/ProductInquiryRepositoryTest.php @@ -0,0 +1,165 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Repositories; + +use Illuminate\Support\Carbon; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; +use Modules\Sirsoft\Ecommerce\Repositories\Contracts\ProductInquiryRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * ProductInquiryRepository 단위 테스트 + * + * SoftDeletes 도입(#107 후속)에 따른 신규 메서드와 answered_at 보존 계약을 검증합니다. + * - markAsAnswered: 최초 답변 시각 보존 (재마킹이 시각을 덮어쓰면 안 됨) + * - restoreByInquirable: 소프트 삭제 피벗 복원 (게시판 질문 글 복원 대칭) + * - forceDeleteByProductId: 상품 삭제 경로 — trashed 포함 영구 삭제 + * - findByProductIdWithTrashed: trashed 포함 조회 (기본 조회는 trashed 제외) + */ +class ProductInquiryRepositoryTest extends ModuleTestCase +{ + protected ProductInquiryRepositoryInterface $repository; + + protected Product $product; + + protected function setUp(): void + { + parent::setUp(); + + $this->repository = app(ProductInquiryRepositoryInterface::class); + $this->product = Product::factory()->create(); + } + + /** + * markAsAnswered 는 기존 answered_at 을 보존한다. + * + * 답변 수정/복원 재마킹 경로에서 최초 답변 시각이 덮어써지면 안 된다 + * (선례: ProductReviewService). 값이 없을 때만 now() 로 채운다. + */ + public function test_mark_as_answered_preserves_existing_answered_at(): void + { + // Given: 최초 답변 시각이 기록된 피벗 (답변완료 해제 없이 재마킹되는 상황) + $firstAnsweredAt = Carbon::parse('2026-01-01 12:34:56'); + $inquiry = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'is_answered' => false, + 'answered_at' => $firstAnsweredAt, + ]); + + // When: 재마킹 + $updated = $this->repository->markAsAnswered($inquiry); + + // Then: is_answered 는 true, answered_at 은 최초 시각 그대로 + $this->assertTrue($updated->is_answered); + $this->assertSame( + $firstAnsweredAt->format('Y-m-d H:i:s'), + $updated->answered_at->format('Y-m-d H:i:s'), + '재마킹이 최초 답변 시각을 덮어쓰면 안 됩니다.' + ); + + // And: answered_at 이 없는 피벗은 now() 로 채워진다 (기존 동작 유지) + $fresh = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'is_answered' => false, + 'answered_at' => null, + ]); + $freshUpdated = $this->repository->markAsAnswered($fresh); + $this->assertTrue($freshUpdated->is_answered); + $this->assertNotNull($freshUpdated->answered_at); + } + + /** + * restoreByInquirable 은 소프트 삭제된 피벗만 복원한다 (onlyTrashed → restore). + */ + public function test_restore_by_inquirable_restores_soft_deleted_pivot(): void + { + // Given: 소프트 삭제된 피벗 + $trashed = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post', + 'inquirable_id' => 501, + ]); + $trashed->delete(); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $trashed->id]); + + // And: 살아있는 피벗 (onlyTrashed 대상 아님) + $alive = ProductInquiry::factory()->create([ + 'product_id' => $this->product->id, + 'inquirable_type' => 'Modules\\Sirsoft\\Board\\Models\\Post', + 'inquirable_id' => 502, + ]); + + // When: 복원 + $restored = $this->repository->restoreByInquirable( + 'Modules\\Sirsoft\\Board\\Models\\Post', + 501 + ); + + // Then: 1건 복원 + 기본 스코프에서 다시 조회 가능 + $this->assertSame(1, $restored); + $this->assertNull(ProductInquiry::withTrashed()->find($trashed->id)->deleted_at); + $this->assertNotNull(ProductInquiry::find($trashed->id)); + + // And: 살아있는 피벗을 대상으로 하면 0건 (onlyTrashed 스코프) + $this->assertSame( + 0, + $this->repository->restoreByInquirable('Modules\\Sirsoft\\Board\\Models\\Post', 502) + ); + $this->assertNotNull(ProductInquiry::find($alive->id)); + } + + /** + * forceDeleteByProductId 는 trashed 피벗까지 포함해 영구 삭제한다 (상품 삭제 경로). + */ + public function test_force_delete_by_product_id_removes_trashed_too(): void + { + // Given: 살아있는 피벗 1건 + 소프트 삭제된 피벗 1건 + $alive = ProductInquiry::factory()->create(['product_id' => $this->product->id]); + $trashed = ProductInquiry::factory()->create(['product_id' => $this->product->id]); + $trashed->delete(); + + // And: 다른 상품의 피벗 (삭제 대상 아님) + $otherProduct = Product::factory()->create(); + $other = ProductInquiry::factory()->create(['product_id' => $otherProduct->id]); + + // When: 상품 기준 영구 삭제 + $deleted = $this->repository->forceDeleteByProductId($this->product->id); + + // Then: trashed 포함 2건 영구 삭제 — 행 자체가 사라진다 + $this->assertSame(2, $deleted); + $this->assertDatabaseMissing('ecommerce_product_inquiries', ['id' => $alive->id]); + $this->assertDatabaseMissing('ecommerce_product_inquiries', ['id' => $trashed->id]); + + // And: 다른 상품의 피벗은 보존 + $this->assertDatabaseHas('ecommerce_product_inquiries', ['id' => $other->id]); + } + + /** + * findByProductIdWithTrashed 는 trashed 를 포함하고, 기본 findByProductId 는 제외한다. + */ + public function test_find_by_product_id_with_trashed_includes_trashed(): void + { + // Given: 살아있는 피벗 2건 + 소프트 삭제된 피벗 1건 + $alive1 = ProductInquiry::factory()->create(['product_id' => $this->product->id]); + $alive2 = ProductInquiry::factory()->create(['product_id' => $this->product->id]); + $trashed = ProductInquiry::factory()->create(['product_id' => $this->product->id]); + $trashed->delete(); + + // When / Then: withTrashed 조회는 3건 전부 + $withTrashed = $this->repository->findByProductIdWithTrashed($this->product->id); + $this->assertCount(3, $withTrashed); + $this->assertEqualsCanonicalizing( + [$alive1->id, $alive2->id, $trashed->id], + $withTrashed->pluck('id')->all() + ); + + // And: 기본 조회는 trashed 제외 2건 + $default = $this->repository->findByProductId($this->product->id); + $this->assertCount(2, $default); + $this->assertEqualsCanonicalizing( + [$alive1->id, $alive2->id], + $default->pluck('id')->all() + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Resources/CashReceiptResourceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Resources/CashReceiptResourceTest.php index e75b2d8d..d75168a0 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Resources/CashReceiptResourceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Resources/CashReceiptResourceTest.php @@ -16,6 +16,7 @@ use Modules\Sirsoft\Ecommerce\Models\Order; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -28,6 +29,8 @@ use PHPUnit\Framework\Attributes\Test; */ class CashReceiptResourceTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -43,6 +46,7 @@ class CashReceiptResourceTest extends ModuleTestCase $this->issueShouldFail = false; $this->receiptSequence = 0; + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } @@ -284,6 +288,61 @@ class CashReceiptResourceTest extends ModuleTestCase $this->assertNotEmpty($failed['occurred_at_formatted'], '실패 이력도 발생 시각을 표시할 수 있어야 한다'); } + /** + * 발급사 플러그인이 제거되면 발급사 필드가 null 로 내려간다. (비회귀 pin) + * + * 화면은 이 필드로 신규 발급 버튼을 켠다. 죽은 발급사 ID 를 그대로 내려보내면 + * 버튼이 살아 있고, 눌러도 실패 이력만 쌓인다. + * + * @scenario provider_state=dead + * + * @effects dead_provider_nulled_in_resource + */ + #[Test] + public function 발급사가_제거되면_발급사_필드가_null_이_된다(): void + { + $this->registerProvider(); + $order = $this->makeOrder(); + + // 살아 있는 동안은 그대로 내려간다 (존재를 먼저 확정) + $this->assertSame(self::PROVIDER, $this->resourceArray($order)['payment']['cash_receipt_provider'] ?? null); + + app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', 'ghost_provider'); + + $payment = $this->resourceArray($order->fresh())['payment']; + + $this->assertArrayHasKey('cash_receipt_provider', $payment); + $this->assertNull($payment['cash_receipt_provider'], '레지스트리에 없는 발급사가 그대로 노출되었습니다.'); + } + + /** + * 발급사가 제거돼도 이미 발급한 이력은 계속 노출된다. (비회귀 pin) + * + * 발급 이력과 영수증 링크는 구매자·운영자의 증빙이다. 발급사 유무로 감추면 + * 이미 발급된 영수증의 확인 경로가 사라진다. + * + * @scenario provider_state=dead + * + * @effects existing_receipt_history_survives_dead_provider + */ + #[Test] + public function 발급사가_제거돼도_기존_발급_이력은_계속_노출된다(): void + { + $this->registerProvider(); + $order = $this->makeOrder(); + app(CashReceiptService::class)->issue( + $order, CashReceiptType::INCOME, self::IDENTIFIER, CashReceiptIdentifierType::PHONE, + ); + + app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', 'ghost_provider'); + + $array = $this->resourceArray($order->fresh()); + + $this->assertNotEmpty($array['cash_receipts'], '발급사 제거로 발급 이력이 사라졌습니다.'); + $this->assertNotNull($array['cash_receipt'], '활성 영수증이 사라졌습니다.'); + $this->assertSame(self::PROVIDER, $array['cash_receipts'][0]['provider'], '이력의 발급사 스냅샷이 바뀌었습니다.'); + } + #[Test] public function 리소스는_프로바이더_원응답을_노출하지_않는다(): void { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptCancellationSyncTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptCancellationSyncTest.php index 57ed78b2..d3628200 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptCancellationSyncTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptCancellationSyncTest.php @@ -19,6 +19,7 @@ use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; use Modules\Sirsoft\Ecommerce\Services\OrderCancellationService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -31,6 +32,8 @@ use PHPUnit\Framework\Attributes\Test; */ class CashReceiptCancellationSyncTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -46,6 +49,7 @@ class CashReceiptCancellationSyncTest extends ModuleTestCase $this->issueShouldFail = false; $this->receiptSequence = 0; + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptServiceTest.php index 47a2ba4f..8b20110a 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptServiceTest.php @@ -19,6 +19,7 @@ use Modules\Sirsoft\Ecommerce\Repositories\Contracts\OrderCashReceiptRepositoryI use Modules\Sirsoft\Ecommerce\Services\CashReceiptService; use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; use Modules\Sirsoft\Ecommerce\Services\OrderService; +use Modules\Sirsoft\Ecommerce\Tests\Concerns\RegistersTestCashReceiptProvider; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use PHPUnit\Framework\Attributes\Test; @@ -30,6 +31,8 @@ use PHPUnit\Framework\Attributes\Test; */ class CashReceiptServiceTest extends ModuleTestCase { + use RegistersTestCashReceiptProvider; + private const PROVIDER = 'tosspayments'; private const IDENTIFIER = '01012345678'; @@ -54,6 +57,7 @@ class CashReceiptServiceTest extends ModuleTestCase $this->receiptSequence = 0; $this->issueShouldFail = false; + $this->registerCashReceiptProvider(self::PROVIDER); app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', self::PROVIDER); } @@ -716,6 +720,70 @@ class CashReceiptServiceTest extends ModuleTestCase $this->assertSame(2, $this->issueCalls[1]['payload']['issue_sequence']); } + // ───────────────────────────────────────────────────────────── + // 발급사 플러그인 제거 후 동작 (A3) + // ───────────────────────────────────────────────────────────── + + /** + * 발급사 플러그인이 제거되면 신규 발급이 차단된다. (비회귀 pin) + * + * 저장값은 남아 있어도 그 발급사를 제공하는 확장이 없으면 발급 요청이 어디에도 + * 도달하지 못한다. 그 상태에서 발급을 허용하면 구매자에게는 성공처럼 보이고 + * 실제로는 실패 이력만 쌓인다. + * + * @scenario provider_state=dead + * + * @effects issue_blocked_when_provider_unregistered + */ + #[Test] + public function 발급사_플러그인이_제거되면_신규_발급이_차단된다(): void + { + $this->registerProvider(); + $order = $this->makeOrder(11000, 11000, 0); + + // 레지스트리에 없는 발급사로 저장값만 바꾼다 (= 플러그인 제거 상태) + app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', 'ghost_provider'); + + $receipt = $this->service()->issue($order, CashReceiptType::INCOME, self::IDENTIFIER); + + $this->assertSame(CashReceiptIssueStatus::FAILED, $receipt->issue_status); + $this->assertSame('PROVIDER_NOT_CONFIGURED', $receipt->error_code); + $this->assertSame([], $this->issueCalls, '발급사가 없는데 발급 훅이 호출되었습니다.'); + } + + /** + * 이미 발급된 영수증의 취소는 영수증에 박제된 발급사로 수행된다. (비회귀 pin) + * + * 취소를 현재 설정값으로 판정하면, 발급사 플러그인을 제거한 뒤에는 과거 영수증을 + * 취소할 수 없게 된다. 구매자는 환불받았는데 현금영수증만 살아 있는 상태가 남는다. + * + * @scenario provider_state=dead + * + * @effects cancel_uses_receipt_snapshot_provider + */ + #[Test] + public function 발급사_플러그인이_제거돼도_기존_영수증_취소는_스냅샷_발급사로_수행된다(): void + { + $this->registerProvider(); + $order = $this->makeOrder(11000, 11000, 0); + $this->service()->issue($order, CashReceiptType::INCOME, self::IDENTIFIER); + + $this->assertNotNull($this->activeReceipt($order->fresh()), '취소 대상 영수증이 없습니다.'); + + app(EcommerceSettingsService::class)->setSetting('order_settings.cash_receipt_provider', 'ghost_provider'); + + $result = $this->service()->cancelAll($order->fresh(), '전액취소'); + + $this->assertTrue($result, '발급사 제거 후 기존 영수증 취소가 실패했습니다.'); + $this->assertNotSame([], $this->cancelCalls, '취소 훅이 호출되지 않았습니다.'); + $this->assertSame( + self::PROVIDER, + $this->cancelCalls[0]['provider'], + '취소가 영수증 스냅샷이 아닌 현재 설정값을 사용했습니다.' + ); + $this->assertNull($this->activeReceipt($order->fresh()), '취소 후에도 활성 영수증이 남았습니다.'); + } + #[Test] public function 주문_삭제_시_현금영수증_이력도_명시적으로_삭제된다(): void { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryImageServiceRowDiskTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryImageServiceRowDiskTest.php new file mode 100644 index 00000000..e881149e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryImageServiceRowDiskTest.php @@ -0,0 +1,189 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use App\Extension\Storage\ModuleStorageDriver; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Storage; +use Modules\Sirsoft\Ecommerce\Models\CategoryImage; +use Modules\Sirsoft\Ecommerce\Services\CategoryImageService; +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * CategoryImageService 행 disk 기준 서빙/삭제 테스트 + * + * 공개 자산 디스크 전환 이후에도 전환 이전 행(구 disk)의 서빙이 404 가 되지 않고, + * 삭제가 그 행의 실제 저장 위치를 향하는지(혼재 운용 정합) 검증합니다. + * + * CategoryImageServiceTest 는 StorageInterface 를 Mock 으로 대체하므로 + * 실제 디스크 해석(withDisk) 회귀를 검출할 수 없어 별도 파일로 둡니다. + */ +class CategoryImageServiceRowDiskTest extends ModuleTestCase +{ + private CategoryImageService $service; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + AssetStorage::flush(); + + $this->service = app(CategoryImageService::class); + } + + protected function tearDown(): void + { + AssetStorage::flush(); + + parent::tearDown(); + } + + /** + * 지정 disk 에 파일과 카테고리 이미지 행을 함께 생성합니다. + * + * @param string $disk 행 disk + * @param string $path 이미지 경로 (images/ 하위 상대 경로) + * @return CategoryImage 생성된 행 + */ + private function makeRowOnDisk(string $disk, string $path): CategoryImage + { + (new ModuleStorageDriver('sirsoft-ecommerce', $disk))->put('images', $path, 'image-bytes'); + + return CategoryImage::create([ + 'temp_key' => 'category-row-disk-test', + 'original_filename' => 'category.jpg', + 'stored_filename' => basename($path), + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'sort_order' => 0, + ]); + } + + /** + * @effects serve_and_delete_follow_row_disk, mixed_rows_resolve_per_row_disk + */ + #[Test] + public function download_serves_row_from_its_recorded_disk(): void + { + // 혼재 운용 — 주입 스토리지 disk 와 다른 disk 의 행도 서빙 가능해야 한다 + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'category/row-disk-test/cdn.jpg'); + $localRow = $this->makeRowOnDisk('modules', 'category/row-disk-test/local.jpg'); + + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($cdnRow->hash)); + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($localRow->hash)); + } + + /** + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function delete_removes_file_on_row_disk(): void + { + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'category/row-disk-test/del.jpg'); + $cdnDriver = new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'); + $this->assertTrue($cdnDriver->exists('images', $cdnRow->path)); + + $this->assertTrue($this->service->delete($cdnRow->id)); + + $this->assertFalse($cdnDriver->exists('images', $cdnRow->path)); + } + + /** + * 행 disk 를 제공하던 플러그인이 비활성화되어 config 에서 사라진 상태를 만듭니다. + * + * @param string $disk 사라지게 할 디스크명 + */ + private function vanishDisk(string $disk): void + { + Config::set("filesystems.disks.{$disk}", null); + Storage::forgetDisk($disk); + AssetStorage::flush(); + } + + /** + * 고아 disk 행의 서빙은 500 이 아니라 스트리밍 폴백이어야 한다. + * + * 디스크를 제공하던 플러그인을 비활성화하면 행 disk 가 config 에서 사라진다. + * 이때 withDisk 로 미등록 디스크를 만들면 Laravel 이 InvalidArgumentException 을 + * 던져 서빙이 500 이 된다 — 접근자는 정상적으로 API 경로를 내주므로 사용자에게는 + * 이미지 링크가 살아 있는데 열면 500 인 상태가 된다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function download_falls_back_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'category/row-disk-test/orphan.jpg'); + $this->vanishDisk('fake_cdn'); + + // 예외 없이 폴백 — 파일은 도달 불가이므로 null(404) 이 정상 degradation + $this->assertNull($this->service->download($row->hash)); + } + + /** + * 고아 disk 행의 삭제도 예외 없이 완료되어야 한다(카테고리 삭제 자체가 막히면 안 됨). + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function delete_succeeds_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'category/row-disk-test/orphan-del.jpg'); + $this->vanishDisk('fake_cdn'); + + $this->assertTrue($this->service->delete($row->id)); + $this->assertNull(CategoryImage::find($row->id)); + } + + /** + * put 종단 — SP 카테고리 배선을 실제로 경유해 공개 자산 디스크 설정값이 + * 신규 행의 disk 컬럼에 기록되는지 단언합니다(배선 회귀 검출). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_configured_public_asset_disk_on_row(): void + { + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + + $service = app(CategoryImageService::class); + $image = $service->upload(UploadedFile::fake()->image('cdn-category.jpg', 40, 40)); + + $this->assertSame('fake_cdn', $image->disk); + $this->assertTrue( + (new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'))->exists('images', $image->path) + ); + + $this->assertTrue($service->delete($image->id)); + } + + /** + * put 종단 — 공개 자산 디스크 미설정이면 신규 행이 기본 디스크로 기록(현행 보존). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_base_disk_when_public_asset_disk_unset(): void + { + Config::set('core.storage.public_asset_disk', ''); + + $service = app(CategoryImageService::class); + $image = $service->upload(UploadedFile::fake()->image('local-category.jpg', 40, 40)); + + $this->assertSame('modules', $image->disk); + + $this->assertTrue($service->delete($image->id)); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryServiceTest.php index 3e551499..2c913027 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryServiceTest.php @@ -7,6 +7,7 @@ use Mockery; use Modules\Sirsoft\Ecommerce\Models\Category; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\CategoryImageRepositoryInterface; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\CategoryRepositoryInterface; +use Modules\Sirsoft\Ecommerce\Services\CategoryImageService; use Modules\Sirsoft\Ecommerce\Services\CategoryService; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; @@ -23,13 +24,20 @@ class CategoryServiceTest extends ModuleTestCase protected $mockImageRepository; + protected $mockImageService; + protected function setUp(): void { parent::setUp(); $this->mockRepository = Mockery::mock(CategoryRepositoryInterface::class); $this->mockImageRepository = Mockery::mock(CategoryImageRepositoryInterface::class); - $this->service = new CategoryService($this->mockRepository, $this->mockImageRepository); + $this->mockImageService = Mockery::mock(CategoryImageService::class); + $this->service = new CategoryService( + $this->mockRepository, + $this->mockImageRepository, + $this->mockImageService, + ); } protected function tearDown(): void @@ -43,7 +51,10 @@ class CategoryServiceTest extends ModuleTestCase // ======================================== /** - * 카테고리 삭제 시 이미지가 명시적으로 삭제되는지 확인 + * 카테고리 삭제 시 이미지가 파일까지 명시적으로 삭제되는지 확인 + * + * 행만 지우면 물리 파일이 디스크에 영구 잔존하므로, 삭제는 파일 삭제를 포함하는 + * CategoryImageService 경로를 거쳐야 한다. */ public function test_delete_category_deletes_images(): void { @@ -51,9 +62,11 @@ class CategoryServiceTest extends ModuleTestCase $category = Mockery::mock(Category::class)->makePartial(); $category->shouldReceive('getAttribute')->with('id')->andReturn(1); - $mockImages = Mockery::mock(HasMany::class); - $mockImages->shouldReceive('delete')->once(); - $category->shouldReceive('images')->once()->andReturn($mockImages); + $this->mockImageService + ->shouldReceive('deleteByCategoryId') + ->with(1) + ->once() + ->andReturn(1); $this->mockRepository ->shouldReceive('findById') diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/DeletedCurrencyOrderIntegrityTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/DeletedCurrencyOrderIntegrityTest.php new file mode 100644 index 00000000..f49eb546 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/DeletedCurrencyOrderIntegrityTest.php @@ -0,0 +1,287 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Http\Resources\Traits\HasMultiCurrencyPrices; +use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService; +use Modules\Sirsoft\Ecommerce\Support\CurrencySettingsCache; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 통화 삭제 후 기존 주문의 금액·표기 불변성 테스트 (공개 #91 후속) + * + * 관리자가 통화를 삭제해도 그 통화로 결제된 과거 주문은 환불 재계산·표기 모두 주문 시점 + * 스냅샷을 따라야 한다. 환산 금액은 스냅샷 환율·절사규칙을 쓰므로 이미 불변이지만, + * 소수 자릿수만은 현재 설정에서 조회해 왔다 — 삭제된 통화는 설정에서 사라지므로 + * 폴백 2자리가 적용되어 0자리 통화(JPY 등)의 표기가 `¥18,860` → `¥18,860.00` 으로 바뀌고, + * 3자리 이상으로 설정된 통화는 표시 금액이 절사된다. + */ +class DeletedCurrencyOrderIntegrityTest extends ModuleTestCase +{ + private string $storagePath; + + /** + * 주문 시점 스냅샷 — JPY(0자리)·XAU(3자리)가 살아 있던 시점. + */ + private const SNAPSHOT = [ + 'base_currency' => 'KRW', + 'order_currency' => 'JPY', + 'exchange_rate' => 115.0, + 'base_unit' => 1000, + 'exchange_rates' => [ + 'KRW' => ['rate' => 1.0, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0, 'base_unit' => 1000], + 'JPY' => ['rate' => 115.0, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0, 'base_unit' => 100], + 'XAU' => ['rate' => 0.002, 'rounding_unit' => '0.001', 'rounding_method' => 'round', 'decimal_places' => 3, 'base_unit' => 1], + ], + ]; + + protected function setUp(): void + { + parent::setUp(); + + $this->storagePath = storage_path('framework/testing/modules/sirsoft-ecommerce/settings'); + + // 현재 설정 = 관리자가 JPY·XAU 를 삭제한 뒤 (KRW 만 남음) + File::ensureDirectoryExists($this->storagePath); + File::put( + $this->storagePath.'/language_currency.json', + json_encode([ + 'default_currency' => 'KRW', + 'currencies' => [ + [ + 'code' => 'KRW', + 'name' => ['ko' => 'KRW (원)', 'en' => 'KRW (Won)'], + 'exchange_rate' => null, + 'base_unit' => 1000, + 'rounding_unit' => '1', + 'rounding_method' => 'floor', + 'decimal_places' => 0, + 'is_default' => true, + ], + ], + 'removed_default_currencies' => ['USD', 'JPY', 'CNY', 'EUR'], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + + config(['g7_settings.modules.sirsoft-ecommerce.language_currency' => [ + 'default_currency' => 'KRW', + 'currencies' => [ + ['code' => 'KRW', 'name' => ['ko' => 'KRW', 'en' => 'KRW'], 'exchange_rate' => null, 'base_unit' => 1000, 'rounding_unit' => '1', 'rounding_method' => 'floor', 'decimal_places' => 0, 'is_default' => true], + ], + ]]); + + CurrencySettingsCache::clear(); + } + + protected function tearDown(): void + { + if (File::isDirectory($this->storagePath)) { + File::cleanDirectory($this->storagePath); + } + CurrencySettingsCache::clear(); + + parent::tearDown(); + } + + /** + * 리소스 계층(트레이트)을 단독으로 쓰기 위한 익명 클래스를 만듭니다. + */ + private function resourceStub(): object + { + return new class + { + use HasMultiCurrencyPrices; + + public function formatStored(?array $amounts): array + { + return $this->formatStoredMultiCurrency($amounts); + } + + public function roundTo(float|int|null $price, string $code): float|int + { + return $this->roundToCurrency($price, $code); + } + }; + } + + // ────────────────────────────────────────────── + // 환불 재계산 금액 (스냅샷 고정 — 회귀 방지) + // ────────────────────────────────────────────── + + /** + * 삭제된 통화로 결제된 주문의 부분환불 금액이 스냅샷 환율로 계산된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_order_refund_uses_snapshot_rate + */ + public function test_partial_refund_of_deleted_currency_order_uses_snapshot_rate(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + // 부분환불 10,000원 → JPY 환산 = (10000/1000) * 115 = 1,150 + $result = $svc->convertMultipleAmountsWithSnapshot(['refund_amount' => 10000], self::SNAPSHOT); + + $this->assertArrayHasKey('JPY', $result, '삭제된 통화가 스냅샷 환산 결과에서 사라졌습니다.'); + $this->assertEquals(1150, $result['JPY']['refund_amount']); + $this->assertEquals(10000, $result['KRW']['refund_amount']); + } + + /** + * 삭제된 통화의 결제 청구액도 스냅샷 환율·자릿수로 산출된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_order_refund_uses_snapshot_rate + */ + public function test_payment_charge_of_deleted_currency_uses_snapshot(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + $charge = $svc->resolveSnapshotPaymentCharge(10000, self::SNAPSHOT); + + $this->assertSame('JPY', $charge['currency']); + $this->assertEquals(1150, $charge['amount']); + } + + // ────────────────────────────────────────────── + // 자릿수 — 스냅샷 우선 (이번 수정 대상) + // ────────────────────────────────────────────── + + /** + * 삭제된 0자리 통화의 환산 표기가 스냅샷 자릿수(0)를 유지한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_display_keeps_snapshot_decimal_places + */ + public function test_converted_amount_formatting_keeps_snapshot_decimal_places(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + $result = $svc->convertMultipleAmountsWithSnapshot(['refund_amount' => 10000], self::SNAPSHOT); + + $this->assertStringNotContainsString( + '.00', + $result['JPY']['refund_amount_formatted'], + '삭제된 0자리 통화가 폴백 2자리로 표기되었습니다.' + ); + } + + /** + * 삭제된 3자리 통화의 표기가 절사되지 않는다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_display_keeps_snapshot_decimal_places + */ + public function test_high_precision_deleted_currency_is_not_truncated(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + // (10000/1000) * 0.002 = 0.02 → 절사단위 0.001 → 0.02, 3자리 표기 '0.020' + $result = $svc->convertMultipleAmountsWithSnapshot(['refund_amount' => 10000], self::SNAPSHOT); + + $this->assertStringContainsString( + '0.020', + $result['XAU']['refund_amount_formatted'], + '삭제된 3자리 통화가 폴백 2자리로 절사되었습니다.' + ); + } + + /** + * 서비스의 단일 금액 포맷도 스냅샷 자릿수를 따른다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_display_keeps_snapshot_decimal_places + */ + public function test_format_price_accepts_snapshot(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + $this->assertSame('¥18,860', $svc->formatPrice(18860, 'JPY', self::SNAPSHOT)); + // 스냅샷 없이 호출하면 현행대로 현재 설정 폴백(2자리) + $this->assertSame('¥18,860.00', $svc->formatPrice(18860, 'JPY')); + } + + /** + * 주문의 저장된 mc_* 금액 표기가 스냅샷 자릿수를 따른다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_display_keeps_snapshot_decimal_places + */ + public function test_stored_multi_currency_formatting_keeps_snapshot_decimal_places(): void + { + $resource = $this->resourceStub()->withCurrencySnapshot(self::SNAPSHOT); + + $formatted = $resource->formatStored(['KRW' => 129000, 'JPY' => 14835]); + + $this->assertSame('129,000원', $formatted['KRW']['formatted']); + $this->assertSame('¥14,835', $formatted['JPY']['formatted'], '삭제된 0자리 통화가 폴백 2자리로 표기되었습니다.'); + } + + /** + * raw 금액 라운딩도 스냅샷 자릿수를 따른다 (0자리 통화는 int 유지). + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects deleted_currency_display_keeps_snapshot_decimal_places + */ + public function test_round_to_currency_keeps_snapshot_decimal_places(): void + { + $resource = $this->resourceStub()->withCurrencySnapshot(self::SNAPSHOT); + + $this->assertSame(14835, $resource->roundTo(14835.0, 'JPY'), '삭제된 0자리 통화가 float 로 반환되었습니다.'); + } + + // ────────────────────────────────────────────── + // 폴백 (레거시 스냅샷 / 스냅샷 부재) + // ────────────────────────────────────────────── + + /** + * 자릿수가 없는 구형 스냅샷은 현재 설정 폴백을 그대로 유지한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=none_legacy + * + * @effects legacy_snapshot_without_decimal_places_falls_back_to_settings + */ + public function test_legacy_snapshot_without_decimal_places_falls_back(): void + { + /** @var CurrencyConversionService $svc */ + $svc = app(CurrencyConversionService::class); + + // 구형 스냅샷: 환율이 단순 숫자 (절사규칙·자릿수 미박제) + $legacy = [ + 'base_currency' => 'KRW', + 'order_currency' => 'JPY', + 'exchange_rates' => ['JPY' => 115.0], + ]; + + $this->assertSame('¥18,860.00', $svc->formatPrice(18860, 'JPY', $legacy)); + } + + /** + * 스냅샷을 주지 않은 리소스는 현재 설정으로 동작한다 (상품 등 카탈로그 경로 회귀 방지). + * + * @scenario saved_currency_set=all_five, deletion_tombstone=none_legacy + * + * @effects legacy_snapshot_without_decimal_places_falls_back_to_settings + */ + public function test_resource_without_snapshot_uses_live_settings(): void + { + $resource = $this->resourceStub(); + + // KRW 는 현재 설정에 살아 있고 0자리 + $formatted = $resource->formatStored(['KRW' => 129000]); + + $this->assertSame('129,000원', $formatted['KRW']['formatted']); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsOrderSettingsTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsOrderSettingsTest.php index b729ab92..a202e0bb 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsOrderSettingsTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsOrderSettingsTest.php @@ -569,8 +569,105 @@ class EcommerceSettingsOrderSettingsTest extends ModuleTestCase $this->assertEquals('mobile', $kakaopay['_cached_icon']); $this->assertEquals('plugin:sirsoft-kakaopay', $kakaopay['_cached_source']); - // _orphaned 플래그는 저장되지 않아야 함 + // 런타임 전용 플래그는 저장되지 않아야 함 $this->assertArrayNotHasKey('_orphaned', $kakaopay); + $this->assertArrayNotHasKey('_orphaned_pg', $kakaopay); + + // 저장 파일 전체에 런타임 플래그가 하나도 없어야 한다 (항목별 누락 방지) + foreach ($saved['payment_methods'] as $method) { + $this->assertArrayNotHasKey('_orphaned', $method); + $this->assertArrayNotHasKey('_orphaned_pg', $method); + } + } + + // ────────────────────────────────────────────── + // 병합 4분면 — (카탈로그 등록) × (저장값 존재) + // ────────────────────────────────────────────── + + /** + * 카탈로그 등록 여부와 저장값 존재 여부의 네 조합을 한 번에 고정합니다. + * + * 이 네 칸이 각각 다른 결과를 내야 한다 — 하나라도 뒤섞이면 삭제한 플러그인의 + * 결제수단이 정상 수단처럼 노출되거나, 반대로 살아 있는 수단이 사라진다. + * + * @scenario catalog_state=registered, saved_state=present + * + * @effects merge_quadrant_pinned + */ + public function test_merge_quadrants_of_catalog_and_saved_state(): void + { + // 카탈로그에는 kakaopay 만 등록한다 (ghostpay 는 미등록 = 플러그인 삭제 상태) + $this->addPaymentMethodFilter(function (array $methods) { + $methods[] = [ + 'id' => 'kakaopay', + 'name' => ['ko' => '카카오페이', 'en' => 'Kakao Pay'], + 'description' => ['ko' => '', 'en' => ''], + 'icon' => 'mobile', + 'source' => 'plugin:sirsoft-kakaopay', + 'defaults' => ['is_active' => true, 'min_order_amount' => 0, 'stock_deduction_timing' => 'payment_complete'], + ]; + + return $methods; + }); + + // 저장값에는 kakaopay(등록 O) 와 ghostpay(등록 X) 만 둔다. + // builtin card 는 저장값 없이 카탈로그에만 있는 칸(등록 O + 저장 X)을 담당한다. + $this->saveOrderSettings([ + 'payment_methods' => [ + ['id' => 'kakaopay', 'sort_order' => 1, 'is_active' => true], + ['id' => 'ghostpay', 'sort_order' => 2, 'is_active' => true, '_cached_name' => ['ko' => '유령페이', 'en' => 'Ghost Pay']], + ], + ]); + $this->service->clearCache(); + + $methods = collect($this->service->getSettings('order_settings')['payment_methods'] ?? [])->keyBy('id'); + + // ① 등록 O + 저장 O → 정상 병합 + $this->assertTrue($methods->has('kakaopay'), '등록·저장된 수단이 사라졌습니다.'); + $this->assertFalse((bool) ($methods['kakaopay']['_orphaned'] ?? false)); + + // ② 등록 O + 저장 X → 카탈로그 기본값으로 포함 (builtin) + $this->assertTrue($methods->has('card'), '저장값 없는 카탈로그 수단이 누락되었습니다.'); + $this->assertFalse((bool) ($methods['card']['_orphaned'] ?? false)); + + // ③ 등록 X + 저장 O → 고아로 표시하되 관리자 응답에는 남긴다 + $this->assertTrue($methods->has('ghostpay'), '고아 수단이 관리자 응답에서 사라졌습니다.'); + $this->assertTrue( + (bool) ($methods['ghostpay']['_orphaned'] ?? false), + '공급 플러그인이 없는 수단이 고아로 표시되지 않았습니다.' + ); + + // ④ 등록 X + 저장 X → 애초에 존재하지 않는다 + $this->assertFalse($methods->has('nowherepay')); + } + + /** + * 고아 수단은 공개 응답에서만 제거되고 관리자 응답에는 남습니다. + * + * 관리자는 그 항목을 보고 지워야 하므로 양쪽 응답의 처리가 달라야 한다. + * + * @scenario catalog_state=unregistered, saved_state=present + * + * @effects merge_quadrant_pinned + */ + public function test_orphaned_method_is_admin_only(): void + { + $this->saveOrderSettings([ + 'payment_methods' => [ + ['id' => 'card', 'sort_order' => 1, 'is_active' => true], + ['id' => 'ghostpay', 'sort_order' => 2, 'is_active' => true], + ], + ]); + $this->service->clearCache(); + + $adminIds = collect($this->service->getSettings('order_settings')['payment_methods'] ?? []) + ->pluck('id')->all(); + $publicIds = collect($this->service->getPublicPaymentSettings()['payment_methods'] ?? []) + ->pluck('id')->all(); + + $this->assertContains('ghostpay', $adminIds); + $this->assertNotContains('ghostpay', $publicIds); + $this->assertContains('card', $publicIds, '정상 수단까지 제거되었습니다.'); } // ────────────────────────────────────────────── diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php new file mode 100644 index 00000000..07308ab9 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php @@ -0,0 +1,365 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 기본 제공 통화의 의도적 삭제 영속화 테스트 (공개 #91) + * + * U11-A 는 array_merge 통째 교체로 defaults 통화가 소실되던 문제를 code 기준 보충으로 막았으나, + * "부작용 소실" 과 "관리자 의도 삭제" 를 구분할 장치가 없어 의도 삭제까지 되돌렸다. + * 저장 시점에 삭제 의도를 서버가 도출해 `removed_default_currencies` 로 기록하고, + * 조회 병합이 그 기록을 존중하는지 검증한다. + */ +class EcommerceSettingsServiceCurrencyTombstoneTest 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(); + } + + // ────────────────────────────────────────────── + // 헬퍼 + // ────────────────────────────────────────────── + + /** + * 통화 코드 목록으로 저장 payload 의 currencies 배열을 만듭니다. + * + * @param array<int, string> $codes 통화 코드 목록 + * @param string $defaultCode 기본 통화 코드 + * @return array 통화 항목 배열 + */ + private function currencies(array $codes, string $defaultCode = 'KRW'): array + { + $meta = [ + 'KRW' => ['rate' => null, 'unit' => '1', 'method' => 'floor', 'dp' => 0, 'base_unit' => 1000], + 'USD' => ['rate' => 0.85, 'unit' => '0.01', 'method' => 'round', 'dp' => 2, 'base_unit' => 1], + 'JPY' => ['rate' => 115, 'unit' => '1', 'method' => 'floor', 'dp' => 0, 'base_unit' => 100], + 'CNY' => ['rate' => 5.8, 'unit' => '0.01', 'method' => 'round', 'dp' => 2, 'base_unit' => 1], + 'EUR' => ['rate' => 0.78, 'unit' => '0.01', 'method' => 'round', 'dp' => 2, 'base_unit' => 1], + 'GBP' => ['rate' => 0.6, 'unit' => '0.01', 'method' => 'round', 'dp' => 2, 'base_unit' => 1], + ]; + + return array_map(function (string $code) use ($meta, $defaultCode) { + $m = $meta[$code] ?? ['rate' => 1.0, 'unit' => '0.01', 'method' => 'round', 'dp' => 2, 'base_unit' => 1]; + + return [ + 'code' => $code, + 'name' => ['ko' => $code, 'en' => $code], + 'exchange_rate' => $code === $defaultCode ? null : $m['rate'], + 'base_unit' => $m['base_unit'], + 'rounding_unit' => $m['unit'], + 'rounding_method' => $m['method'], + 'decimal_places' => $m['dp'], + 'is_default' => $code === $defaultCode, + ]; + }, $codes); + } + + /** + * 관리자 저장 경로(saveSettings)로 language_currency 를 저장합니다. + * + * @param array $languageCurrency language_currency 카테고리 payload + */ + private function save(array $languageCurrency): void + { + $this->service->saveSettings(['language_currency' => $languageCurrency]); + $this->service->clearCache(); + } + + /** + * 저장된 language_currency.json 을 그대로 읽습니다. + * + * @return array 저장 파일의 디코드 결과 + */ + private function savedFile(): array + { + $path = $this->storagePath.'/language_currency.json'; + + if (! File::exists($path)) { + return []; + } + + return json_decode(File::get($path), true) ?? []; + } + + /** + * 현재 조회 결과의 통화 코드 목록을 반환합니다. + * + * @return array<int, string> 통화 코드 목록 + */ + private function resolvedCodes(): array + { + $lc = $this->service->getSettings('language_currency'); + + return array_values(array_map(fn ($c) => $c['code'] ?? null, $lc['currencies'] ?? [])); + } + + // ────────────────────────────────────────────── + // 삭제 영속화 (핵심) + // ────────────────────────────────────────────── + + /** + * 관리자가 삭제한 기본 제공 통화가 저장 후에도 되살아나지 않는다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects intentionally_removed_default_currency_stays_deleted + */ + public function test_deleted_default_currency_stays_deleted_after_save(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ]); + + $codes = $this->resolvedCodes(); + + $this->assertContains('KRW', $codes); + $this->assertContains('USD', $codes); + $this->assertNotContains('JPY', $codes, '관리자가 삭제한 JPY 가 defaults 에서 되살아났습니다.'); + $this->assertNotContains('CNY', $codes, '관리자가 삭제한 CNY 가 defaults 에서 되살아났습니다.'); + $this->assertNotContains('EUR', $codes, '관리자가 삭제한 EUR 가 defaults 에서 되살아났습니다.'); + } + + /** + * 삭제 의도가 저장 파일에 tombstone 으로 기록된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects tombstone_recomputed_server_side_on_save + */ + public function test_tombstone_written_to_saved_file(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ]); + + $saved = $this->savedFile(); + + $this->assertArrayHasKey('removed_default_currencies', $saved, '삭제 기록이 저장되지 않았습니다.'); + $this->assertSame(['JPY', 'CNY', 'EUR'], $saved['removed_default_currencies']); + // 비연속 키가 JSON 객체로 직렬화되지 않도록 array_values 재정렬 (코딩 규약) + $this->assertSame([0, 1, 2], array_keys($saved['removed_default_currencies'])); + } + + /** + * 기본 통화 코드는 제출에서 빠져도 tombstone 되지 않는다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects base_currency_never_tombstoned + */ + public function test_default_currency_code_never_tombstoned(): void + { + // API 직접 제출로 기본 통화(KRW)가 빠진 목록을 보낸 상황 + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['USD'], 'USD'), + ]); + + $saved = $this->savedFile(); + $this->assertNotContains('KRW', $saved['removed_default_currencies'] ?? [], '기본 통화가 tombstone 되었습니다.'); + + $codes = $this->resolvedCodes(); + $this->assertContains('KRW', $codes, '기본 통화가 조회 결과에서 사라졌습니다.'); + + $lc = $this->service->getSettings('language_currency'); + $krw = collect($lc['currencies'])->firstWhere('code', 'KRW'); + $this->assertTrue($krw['is_default'], '기본 통화의 is_default 가 유지되어야 합니다.'); + } + + /** + * 삭제한 통화를 다시 추가하면 tombstone 이 해제되고 복원된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=readded + * + * @effects intentionally_removed_default_currency_stays_deleted, tombstone_recomputed_server_side_on_save + */ + public function test_readding_currency_clears_tombstone_and_resurrects(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW']), + ]); + $this->assertSame(['USD', 'JPY', 'CNY', 'EUR'], $this->savedFile()['removed_default_currencies'] ?? []); + + // USD 재등록 + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ]); + + $codes = $this->resolvedCodes(); + $this->assertContains('USD', $codes, '재추가한 통화가 복원되지 않았습니다.'); + $this->assertSame(['JPY', 'CNY', 'EUR'], $this->savedFile()['removed_default_currencies'] ?? []); + } + + /** + * 관리자가 추가한 통화(defaults 밖)의 삭제는 종전대로 유지된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects intentionally_removed_default_currency_stays_deleted + */ + public function test_admin_added_currency_still_deletable(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'GBP']), + ]); + $this->assertContains('GBP', $this->resolvedCodes()); + + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW']), + ]); + + $this->assertNotContains('GBP', $this->resolvedCodes(), '관리자 추가 통화의 삭제가 유지되지 않았습니다.'); + // defaults 밖 통화는 tombstone 대상이 아니다 (보충 병합이 되살리지 않으므로 기록 불필요) + $this->assertNotContains('GBP', $this->savedFile()['removed_default_currencies'] ?? []); + } + + /** + * 다른 탭 저장은 language_currency 의 삭제 상태를 되돌리지 않는다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects other_category_save_preserves_tombstone + */ + public function test_other_category_save_preserves_tombstone(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW']), + ]); + $before = $this->savedFile(); + + // 기본정보 탭만 저장 + $this->service->saveSettings(['basic_info' => ['shop_name' => ['ko' => '테스트샵', 'en' => 'Test Shop']]]); + $this->service->clearCache(); + + $this->assertSame($before, $this->savedFile(), '타 카테고리 저장이 language_currency 파일을 변경했습니다.'); + $this->assertSame(['KRW'], $this->resolvedCodes()); + } + + /** + * currencies 키 없이 language_currency 를 저장해도 tombstone 이 이월된다. + * + * saveCategorySettings 는 파일 통째 교체라 이월하지 않으면 삭제가 전부 부활한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects other_category_save_preserves_tombstone + */ + public function test_language_currency_save_without_currencies_preserves_tombstone(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW']), + ]); + + // default_currency 만 담긴 저장 (currencies 키 부재) + $this->save(['default_currency' => 'KRW']); + + $this->assertSame(['USD', 'JPY', 'CNY', 'EUR'], $this->savedFile()['removed_default_currencies'] ?? []); + $this->assertSame(['KRW'], $this->resolvedCodes(), 'currencies 미제출 저장이 삭제 통화를 부활시켰습니다.'); + } + + /** + * tombstone 키가 없는 구 저장본은 U11-A 보충 동작을 그대로 유지한다. + * + * @scenario saved_currency_set=jpy_missing, deletion_tombstone=none_legacy + * + * @effects legacy_file_without_tombstone_keeps_supplement + */ + public function test_legacy_saved_file_without_tombstone_supplements_defaults(): void + { + File::ensureDirectoryExists($this->storagePath); + File::put( + $this->storagePath.'/language_currency.json', + json_encode([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + + $this->service->clearCache(); + $codes = $this->resolvedCodes(); + + $this->assertContains('JPY', $codes, '구 저장본의 defaults 보충(U11-A)이 깨졌습니다.'); + $this->assertContains('CNY', $codes); + $this->assertContains('EUR', $codes); + } + + /** + * 클라이언트가 보낸 tombstone 값은 무시되고 서버가 재계산한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects tombstone_recomputed_server_side_on_save + */ + public function test_client_supplied_tombstone_is_recomputed(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD', 'JPY', 'CNY', 'EUR']), + 'removed_default_currencies' => ['KRW', 'USD'], + ]); + + $this->assertSame([], $this->savedFile()['removed_default_currencies'] ?? null, '클라이언트 값이 그대로 저장되었습니다.'); + + $codes = $this->resolvedCodes(); + $this->assertContains('KRW', $codes); + $this->assertContains('USD', $codes); + } + + /** + * tombstone 은 프론트엔드 공개 설정에 노출되지 않는다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects tombstone_recomputed_server_side_on_save + */ + public function test_tombstone_not_exposed_in_frontend_settings(): void + { + $this->save([ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW']), + ]); + + $frontend = $this->service->getFrontendSettings(); + + $this->assertArrayHasKey('language_currency', $frontend); + $this->assertArrayNotHasKey( + 'removed_default_currencies', + $frontend['language_currency'], + '내부 삭제 기록이 공개 설정에 노출되었습니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsSetSettingPipelineTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsSetSettingPipelineTest.php new file mode 100644 index 00000000..28a75c2c --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsSetSettingPipelineTest.php @@ -0,0 +1,311 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Support\CurrencySettingsCache; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 단건 설정 저장(setSetting)·은행 저장(saveBanks)의 정규화 파이프라인 경유 테스트 (공개 #114) + * + * 두 경로는 `Arr::set` 후 카테고리 파일을 통째로 덮어써서 벌크 저장(saveSettings)의 + * 정규화 단계(분리 입력 필드 병합 / 기본 통화 동기화 / defaults 스키마 정규화 / + * 결제수단 메타데이터 스냅샷 / 삭제 통화 기록 / 통화 캐시 무효화)를 전부 건너뛰었다. + * 저장 파일이 서로 어긋난 상태로 남고(예: default_currency 는 USD 인데 통화 목록의 + * is_default 는 KRW), 읽기 경로의 재동기화가 그 어긋남을 가려 왔다. + * + * 위임 payload 는 반드시 **저장본**(loadCategorySettings) 기준이어야 한다. 읽기 결과 + * (getAllSettings)를 넘기면 조회 시점 보강분(통화 symbol/flag, 결제수단 병합 메타)이 + * 영속화되고, 삭제 통화 기록(tombstone, 공개 #91)이 재계산으로 지워져 관리자가 삭제한 + * 통화가 부활한다. + */ +class EcommerceSettingsSetSettingPipelineTest 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(); + } + + // ────────────────────────────────────────────── + // 헬퍼 + // ────────────────────────────────────────────── + + /** + * 카테고리 저장 파일을 직접 기록합니다. (저장 이전 상태 구성용) + * + * @param string $category 카테고리명 + * @param array $data 파일에 기록할 데이터 + */ + private function seedFile(string $category, array $data): void + { + File::ensureDirectoryExists($this->storagePath); + File::put( + $this->storagePath.'/'.$category.'.json', + json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + ); + $this->service->clearCache(); + } + + /** + * 카테고리 저장 파일을 그대로 읽습니다. + * + * @param string $category 카테고리명 + * @return array 저장 파일의 디코드 결과 (파일 부재 시 빈 배열) + */ + private function savedFile(string $category): array + { + $path = $this->storagePath.'/'.$category.'.json'; + + if (! File::exists($path)) { + return []; + } + + return json_decode(File::get($path), true) ?? []; + } + + /** + * 통화 항목 배열을 만듭니다. + * + * @param array<int, string> $codes 통화 코드 목록 + * @param string $defaultCode 기본 통화 코드 + * @return array 통화 항목 배열 + */ + private function currencies(array $codes, string $defaultCode = 'KRW'): array + { + return array_map(fn (string $code) => [ + 'code' => $code, + 'name' => ['ko' => $code, 'en' => $code], + 'exchange_rate' => $code === $defaultCode ? null : 1.5, + 'base_unit' => 1, + 'rounding_unit' => '0.01', + 'rounding_method' => 'round', + 'decimal_places' => 2, + 'is_default' => $code === $defaultCode, + ], $codes); + } + + // ────────────────────────────────────────────── + // ① 저장본 기준 위임 + // ────────────────────────────────────────────── + + /** + * 단건 저장은 조회 시점 보강분을 영속화하지 않고 타 카테고리 파일도 건드리지 않는다. + * + * @scenario pipeline_stage=stored_file_basis + * + * @effects read_time_enrichment_not_persisted, other_category_file_untouched + */ + public function test_single_key_save_persists_stored_basis_only(): void + { + $this->seedFile('language_currency', [ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ]); + + // 조회 보강(symbol/flag 주입)이 일어난 뒤에 저장해도 그 보강분이 파일에 남으면 안 된다 + $this->service->getAllSettings(); + $this->service->setSetting('language_currency.default_currency', 'USD'); + + $saved = $this->savedFile('language_currency'); + + $this->assertCount(2, $saved['currencies'] ?? [], '조회 병합으로 보충된 통화가 저장본에 영속화되었습니다.'); + foreach ($saved['currencies'] as $currency) { + $this->assertArrayNotHasKey('flag', $currency, '표시 전용 메타(flag)가 저장본에 영속화되었습니다.'); + } + + $this->assertSame([], $this->savedFile('basic_info'), '단건 저장이 다른 카테고리 파일을 생성/변경했습니다.'); + $this->assertSame([], $this->savedFile('order_settings'), '단건 저장이 다른 카테고리 파일을 생성/변경했습니다.'); + } + + /** + * 단건 저장이 관리자의 통화 삭제 기록(tombstone)을 지우지 않는다. + * + * @scenario pipeline_stage=stored_file_basis + * + * @effects removed_currency_tombstone_preserved + */ + public function test_single_key_save_preserves_removed_currency_tombstone(): void + { + $this->seedFile('language_currency', [ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + 'removed_default_currencies' => ['JPY', 'CNY', 'EUR'], + ]); + + $this->service->setSetting('language_currency.default_currency', 'USD'); + + $saved = $this->savedFile('language_currency'); + $this->assertSame( + ['JPY', 'CNY', 'EUR'], + $saved['removed_default_currencies'] ?? [], + '단건 저장이 삭제 통화 기록을 훼손했습니다.' + ); + + $this->service->clearCache(); + $codes = array_map(fn ($c) => $c['code'] ?? null, $this->service->getSettings('language_currency')['currencies'] ?? []); + $this->assertNotContains('JPY', $codes, '관리자가 삭제한 통화가 단건 저장으로 부활했습니다.'); + } + + // ────────────────────────────────────────────── + // ② 정규화 파이프라인 경유 (실패-먼저) + // ────────────────────────────────────────────── + + /** + * 기본 통화를 단건으로 바꾸면 통화 목록의 is_default 도 함께 동기화된다. (실패-먼저) + * + * @scenario pipeline_stage=currency_default_sync + * + * @effects saved_file_default_currency_matches_is_default + */ + public function test_single_key_save_syncs_currency_is_default(): void + { + $this->seedFile('language_currency', [ + 'default_currency' => 'KRW', + 'currencies' => $this->currencies(['KRW', 'USD']), + ]); + + $this->service->setSetting('language_currency.default_currency', 'USD'); + + $saved = $this->savedFile('language_currency'); + $byCode = collect($saved['currencies'] ?? [])->keyBy('code'); + + $this->assertSame('USD', $saved['default_currency'] ?? null); + $this->assertTrue( + (bool) ($byCode['USD']['is_default'] ?? false), + '저장 파일의 default_currency 와 is_default 가 어긋났습니다 (정규화 파이프라인 미경유).' + ); + $this->assertFalse( + (bool) ($byCode['KRW']['is_default'] ?? false), + '이전 기본 통화의 is_default 가 해제되지 않았습니다.' + ); + } + + /** + * 카테고리 통째 단건 저장도 분리 입력 필드 병합을 거친다. (실패-먼저) + * + * @scenario pipeline_stage=split_fields + * + * @effects split_input_fields_merged_on_single_key_save + */ + public function test_single_key_save_merges_split_fields(): void + { + $this->service->setSetting('basic_info', [ + 'phone_1' => '02', + 'phone_2' => '1234', + 'phone_3' => '5678', + ]); + + $saved = $this->savedFile('basic_info'); + + $this->assertSame('02-1234-5678', $saved['phone'] ?? null, '분리 입력 필드가 병합되지 않았습니다.'); + $this->assertArrayNotHasKey('phone_1', $saved, '분리 입력 필드 원본이 저장본에 남았습니다.'); + } + + /** + * 결제수단 단건 저장도 _cached_* 스냅샷을 남기고 런타임 전용 플래그를 제거한다. (실패-먼저) + * + * @scenario pipeline_stage=payment_method_snapshot + * + * @effects payment_method_metadata_snapshotted, runtime_only_flag_not_persisted + */ + public function test_single_key_save_snapshots_payment_method_metadata(): void + { + $this->service->setSetting('order_settings.payment_methods', [ + ['id' => 'card', 'is_active' => true, 'sort_order' => 1], + ['id' => 'zombie_pay', 'is_active' => true, 'sort_order' => 2, '_orphaned' => true], + ]); + + $saved = $this->savedFile('order_settings'); + $byId = collect($saved['payment_methods'] ?? [])->keyBy('id'); + + $this->assertArrayHasKey( + '_cached_name', + $byId['card'] ?? [], + '결제수단 메타데이터 스냅샷이 남지 않았습니다.' + ); + $this->assertArrayNotHasKey( + '_orphaned', + $byId['zombie_pay'] ?? ['_orphaned' => true], + '런타임 전용 플래그(_orphaned)가 저장본에 박제되었습니다.' + ); + } + + /** + * 단건 저장 후 요청 단위 통화 캐시가 비워져 같은 요청에서 신값이 읽힌다. (실패-먼저) + * + * @scenario pipeline_stage=currency_cache_flush + * + * @effects currency_request_cache_cleared_on_single_key_save + */ + public function test_single_key_save_clears_currency_request_cache(): void + { + // 캐시 선점 (저장 전 통화 구성) + $before = array_map(fn ($c) => $c['code'] ?? null, CurrencySettingsCache::currencies()); + $this->assertNotEmpty($before); + + $this->service->setSetting('language_currency.currencies', $this->currencies(['KRW'])); + + $after = array_map(fn ($c) => $c['code'] ?? null, CurrencySettingsCache::currencies()); + + $this->assertSame( + ['KRW'], + array_values($after), + '단건 저장 후에도 요청 단위 통화 캐시가 저장 전 구성을 유지했습니다.' + ); + } + + // ────────────────────────────────────────────── + // ③ saveBanks + // ────────────────────────────────────────────── + + /** + * 은행 목록 저장도 정규화를 거치고 order_settings 의 다른 키를 보존한다. (실패-먼저) + * + * @scenario pipeline_stage=normalize_category_data + * + * @effects bank_name_normalized_to_multilingual, sibling_keys_preserved_on_bank_save + */ + public function test_save_banks_normalizes_and_preserves_siblings(): void + { + $this->seedFile('order_settings', [ + 'cash_receipt_provider' => 'keep_me', + 'banks' => [['code' => '004', 'name' => ['ko' => '국민은행', 'en' => 'Kookmin Bank']]], + ]); + + $this->service->saveBanks([ + ['code' => '999', 'name' => '테스트은행'], + ]); + + $saved = $this->savedFile('order_settings'); + + $this->assertSame('keep_me', $saved['cash_receipt_provider'] ?? null, '은행 저장이 형제 키를 지웠습니다.'); + $this->assertIsArray( + $saved['banks'][0]['name'] ?? null, + '은행명이 다국어 배열로 정규화되지 않았습니다 (정규화 파이프라인 미경유).' + ); + $this->assertSame('테스트은행', $saved['banks'][0]['name']['ko'] ?? null); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php index a68cf0fc..14063df2 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php @@ -1873,12 +1873,12 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 500, 'fee' => 2500], - ['min' => 500, 'max' => null, 'fee' => 4000], + ['min' => 0, 'max' => 0.5, 'fee' => 2500], + ['min' => 0.5, 'max' => null, 'fee' => 4000], ], ], ); - [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 0.3); + [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 300); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 1), @@ -1903,8 +1903,8 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_VOLUME, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 1000, 'fee' => 3000], - ['min' => 1000, 'max' => null, 'fee' => 5000], + ['min' => 0, 'max' => 1, 'fee' => 3000], + ['min' => 1, 'max' => null, 'fee' => 5000], ], ], ); @@ -1934,13 +1934,13 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_VOLUME_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 500, 'fee' => 3000], - ['min' => 500, 'max' => null, 'fee' => 5000], + ['min' => 0, 'max' => 0.5, 'fee' => 3000], + ['min' => 0.5, 'max' => null, 'fee' => 5000], ], 'volume_weight_divisor' => 6000, ], ); - [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 0.5, volume: 4200); + [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 500, volume: 4200); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 1), @@ -1965,7 +1965,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase baseFee: 2000, ranges: ['unit_value' => 0.5], ); - [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 1.2); + [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 1200); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 1), @@ -1988,7 +1988,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase $sp = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::PER_VOLUME, baseFee: 1500, - ranges: ['unit_value' => 1000], + ranges: ['unit_value' => 1], ); [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, volume: 2500); @@ -2017,7 +2017,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase baseFee: 2000, ranges: ['unit_value' => 0.5, 'volume_weight_divisor' => 6000], ); - [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 0.3, volume: 4200); + [$p, $o] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 300, volume: 4200); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 1), @@ -2360,13 +2360,13 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 500, 'fee' => 2500], - ['min' => 500, 'max' => null, 'fee' => 4000], + ['min' => 0, 'max' => 0.5, 'fee' => 2500], + ['min' => 0.5, 'max' => null, 'fee' => 4000], ], ], ); // 0.35kg × 2개 = 0.7kg = 700g → 4000원 구간 - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.35); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 350); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 2), @@ -2396,8 +2396,8 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_VOLUME, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 1000, 'fee' => 3000], - ['min' => 1000, 'max' => null, 'fee' => 5000], + ['min' => 0, 'max' => 1, 'fee' => 3000], + ['min' => 1, 'max' => null, 'fee' => 5000], ], ], ); @@ -2437,8 +2437,8 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_VOLUME_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 500, 'fee' => 3000], - ['min' => 500, 'max' => null, 'fee' => 5000], + ['min' => 0, 'max' => 0.5, 'fee' => 3000], + ['min' => 0.5, 'max' => null, 'fee' => 5000], ], 'volume_weight_divisor' => 6000, ], @@ -2446,7 +2446,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase // 0.2kg, 1500cm³ per item // 3개: 무게0.6kg, 부피4500→부피무게0.75, max(0.6,0.75)=750g → 5000원 // 1개: 무게0.2kg, 부피1500→부피무게0.25, max(0.2,0.25)=250g → 3000원 - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.2, volume: 1500); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 200, volume: 1500); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 3), @@ -2642,7 +2642,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase ranges: ['unit_value' => 0.5], ); // 0.4kg × 3개 = 1.2kg - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.4); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 400); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 3), @@ -2675,7 +2675,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase ranges: ['unit_value' => 0.5], ); // 0.4kg × 3개 = 1.2kg - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.4); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 400); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 3), @@ -2704,7 +2704,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase $sp = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::PER_VOLUME, baseFee: 1500, - ranges: ['unit_value' => 1000], + ranges: ['unit_value' => 1], ); // 500cm³ × 5개 = 2500cm³ [$p, $o] = $this->createProductWithOption(price: 5000, shippingPolicy: $sp, volume: 500); @@ -2809,7 +2809,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase baseFee: 2000, ranges: ['unit_value' => 0.5, 'volume_weight_divisor' => 6000], ); - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.2, volume: 1500); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 200, volume: 1500); $input = new CalculationInput(items: [ new CalculationItem(productId: $p->id, productOptionId: $o->id, quantity: 4), @@ -2987,9 +2987,9 @@ class OrderAdjustmentServiceTest extends ModuleTestCase * A-7-4: PER_WEIGHT + multiply extra fee → 수량 감소 시 도서산간도 비례 감소 * * unit=0.5kg, fee=2000, extra=1500×multiply. - * 도서산간 multiply는 quantity/unit_value 기반 (PER_WEIGHT에서도 수량 사용) - * 3개: 배송 ceil(1.2kg/0.5)×2K=6K, 도서산간 ceil(3/0.5)×1.5K=9K - * 1개: 배송 ceil(0.4kg/0.5)×2K=2K, 도서산간 ceil(1/0.5)×1.5K=3K + * 도서산간 multiply 는 기본 배송비와 같은 배송 단위 수 기준 (수량이 아니다) + * 3개(1.2kg): 배송 ceil(1.2/0.5)=3단위 × 2K=6K, 도서산간 3단위 × 1.5K=4.5K + * 1개(0.4kg): 배송 ceil(0.4/0.5)=1단위 × 2K=2K, 도서산간 1단위 × 1.5K=1.5K */ public function test_extra_fee_multiply_per_weight(): void { @@ -3004,7 +3004,7 @@ class OrderAdjustmentServiceTest extends ModuleTestCase extraFeeMultiply: true, ); // 0.4kg × 3개 = 1.2kg - [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 0.4); + [$p, $o] = $this->createProductWithOption(price: 10000, shippingPolicy: $sp, weight: 400); $address = new ShippingAddress(countryCode: 'KR', zipcode: '63400'); $input = new CalculationInput( @@ -3020,10 +3020,10 @@ class OrderAdjustmentServiceTest extends ModuleTestCase ['order_option_id' => $option->id, 'cancel_quantity' => 2], ])); - // 원래: 30K + 6K(배송) + 9K(도서산간) = 45K - // 잔여: 10K + 2K(배송) + 3K(도서산간) = 15K - // refund = 45K - 15K = 30,000 - $this->assertEquals(30000, $result->refundAmount); + // 원래: 30K + 6K(배송) + 4.5K(도서산간) = 40.5K + // 잔여: 10K + 2K(배송) + 1.5K(도서산간) = 13.5K + // refund = 40.5K - 13.5K = 27,000 + $this->assertEquals(27000, $result->refundAmount); } /** @@ -4228,15 +4228,15 @@ class OrderAdjustmentServiceTest extends ModuleTestCase chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 500, 'fee' => 2500], - ['min' => 500, 'max' => null, 'fee' => 4000], + ['min' => 0, 'max' => 0.5, 'fee' => 2500], + ['min' => 0.5, 'max' => null, 'fee' => 4000], ], ], ); // 0.3kg × 2개 - [$pA, $oA] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 0.3); - [$pB, $oB] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 0.3); + [$pA, $oA] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 300); + [$pB, $oB] = $this->createProductWithOption(price: 20000, shippingPolicy: $sp, weight: 300); $input = new CalculationInput( items: [ @@ -4953,6 +4953,12 @@ class OrderAdjustmentServiceTest extends ModuleTestCase * * 50,000×1 + 주문쿠폰(FIXED 5000, minOrder=30000) * 전체취소 → 잔여 0 < 30000 → 쿠폰 복원 + * + * 재계산 전 쿠폰을 일시적으로 미사용으로 되돌리던 왕복 UPDATE 를 제거한 뒤에도(#108) + * 이 복원 판정이 그대로여야 한다 — 판정은 스냅샷의 min_order_amount 로만 이뤄지고 + * 쿠폰의 사용 상태(status/used_at)를 읽지 않기 때문이다. + * + * @effects coupon_restore_on_min_amount_shortfall_unchanged_after_reset_removal */ public function test_coupon_restore_detected_when_min_order_not_met_after_partial(): void { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCalculationServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCalculationServiceTest.php index 062f4958..1c9c593b 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCalculationServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCalculationServiceTest.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; use App\Models\User; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Database\Factories\ProductFactory; use Modules\Sirsoft\Ecommerce\Database\Factories\ProductOptionFactory; use Modules\Sirsoft\Ecommerce\DTO\CalculationInput; @@ -744,8 +745,8 @@ class OrderCalculationServiceTest extends ModuleTestCase * * @param int $price 상품 가격 * @param ShippingPolicy $shippingPolicy 배송정책 - * @param float $weight 무게 (kg) - * @param float $volume 부피 (cm³) + * @param float $weight 무게 (g — 상품 옵션의 저장 단위) + * @param float $volume 부피 (cm³ — 상품 옵션의 저장 단위) * @return array [Product, ProductOption] */ protected function createProductWithDimensions( @@ -1018,19 +1019,19 @@ class OrderCalculationServiceTest extends ModuleTestCase */ public function test_it_calculates_range_weight_shipping_policy(): void { - // Given: RANGE_WEIGHT 정책 (g 단위로 설정) + // Given: RANGE_WEIGHT 정책 (배송정책 구간은 kg 단위) $shippingPolicy = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 1000, 'fee' => 3000], // ~1kg - ['min' => 1000, 'max' => 3000, 'fee' => 5000], // ~3kg - ['min' => 3000, 'max' => null, 'fee' => 8000], // 3kg~ + ['min' => 0, 'max' => 1, 'fee' => 3000], // ~1kg + ['min' => 1, 'max' => 3, 'fee' => 5000], // ~3kg + ['min' => 3, 'max' => null, 'fee' => 8000], // 3kg~ ], ] ); - // 2.5kg = 2500g → ~3000g 구간 → 5000원 - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2.5, 1000); + // 상품 옵션 무게는 g 저장 → 2,500g = 2.5kg → ~3kg 구간 → 5,000원 + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2500, 1000); $input = new CalculationInput( items: [ @@ -1052,23 +1053,23 @@ class OrderCalculationServiceTest extends ModuleTestCase /** * 테스트 17: RANGE_VOLUME 정책 - 부피 구간별 배송비 * - * 입력: RANGE_VOLUME, tiers: [~5000cm³:3천, ~10000cm³:5천, ~∞:8천], 부피 7000cm³ + * 입력: RANGE_VOLUME, tiers: [~5L:3천, ~10L:5천, ~∞:8천], 부피 7000cm³(=7L) * 기대: 배송비 5,000원 */ public function test_it_calculates_range_volume_shipping_policy(): void { - // Given: RANGE_VOLUME 정책 + // Given: RANGE_VOLUME 정책 (배송정책 구간은 L 단위) $shippingPolicy = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::RANGE_VOLUME, ranges: [ 'tiers' => [ - ['min' => 0, 'max' => 5000, 'fee' => 3000], - ['min' => 5000, 'max' => 10000, 'fee' => 5000], - ['min' => 10000, 'max' => null, 'fee' => 8000], + ['min' => 0, 'max' => 5, 'fee' => 3000], + ['min' => 5, 'max' => 10, 'fee' => 5000], + ['min' => 10, 'max' => null, 'fee' => 8000], ], ] ); - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1.0, 7000); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1000, 7000); $input = new CalculationInput( items: [ @@ -1083,7 +1084,7 @@ class OrderCalculationServiceTest extends ModuleTestCase // When $result = $this->service->calculate($input); - // Then: 7000cm³ → ~10000cm³ 구간 → 5,000원 + // Then: 7000cm³ = 7L → ~10L 구간 → 5,000원 $this->assertEquals(5000, $result->summary->totalShipping); } @@ -1096,20 +1097,20 @@ class OrderCalculationServiceTest extends ModuleTestCase */ public function test_it_calculates_range_volume_weight_shipping_policy(): void { - // Given: RANGE_VOLUME_WEIGHT 정책 (g 단위로 설정) + // Given: RANGE_VOLUME_WEIGHT 정책 (배송정책 구간은 kg 단위) $shippingPolicy = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::RANGE_VOLUME_WEIGHT, ranges: [ 'volume_weight_divisor' => 6000, 'tiers' => [ - ['min' => 0, 'max' => 2000, 'fee' => 3000], // ~2kg - ['min' => 2000, 'max' => 5000, 'fee' => 5000], // ~5kg - ['min' => 5000, 'max' => null, 'fee' => 8000], // 5kg~ + ['min' => 0, 'max' => 2, 'fee' => 3000], // ~2kg + ['min' => 2, 'max' => 5, 'fee' => 5000], // ~5kg + ['min' => 5, 'max' => null, 'fee' => 8000], // 5kg~ ], ] ); - // 부피 18000cm³ / 6000 = 부피무게 3kg = 3000g → ~5000g 구간 → 5000원 - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1.0, 18000); + // 실무게 1,000g(=1kg) vs 부피무게 18000cm³ / 6000 = 3kg → 3kg 채택 → ~5kg 구간 → 5,000원 + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1000, 18000); $input = new CalculationInput( items: [ @@ -1142,7 +1143,8 @@ class OrderCalculationServiceTest extends ModuleTestCase baseFee: 1000, ranges: ['unit_value' => 0.5] ); - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2.0, 1000); + // 2,000g = 2kg + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2000, 1000); $input = new CalculationInput( items: [ @@ -1164,18 +1166,18 @@ class OrderCalculationServiceTest extends ModuleTestCase /** * 테스트 21: PER_VOLUME 정책 - 부피당 배송비 * - * 입력: PER_VOLUME, base_fee: 500원, unit_value: 1000cm³, 부피 3500cm³ - * 기대: ceil(3500/1000) × 500 = 4 × 500 = 2,000원 + * 입력: PER_VOLUME, base_fee: 500원, unit_value: 1L, 부피 3500cm³(=3.5L) + * 기대: ceil(3.5/1) × 500 = 4 × 500 = 2,000원 */ public function test_it_calculates_per_volume_shipping_policy(): void { - // Given: PER_VOLUME (1000cm³당 500원) + // Given: PER_VOLUME (1L당 500원 — 배송정책 단위값은 L) $shippingPolicy = $this->createShippingPolicy( chargePolicy: ChargePolicyEnum::PER_VOLUME, baseFee: 500, - ranges: ['unit_value' => 1000] + ranges: ['unit_value' => 1] ); - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1.0, 3500); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1000, 3500); $input = new CalculationInput( items: [ @@ -1190,7 +1192,7 @@ class OrderCalculationServiceTest extends ModuleTestCase // When $result = $this->service->calculate($input); - // Then: ceil(3500/1000) × 500 = 4 × 500 = 2,000원 + // Then: ceil(3.5/1) × 500 = 4 × 500 = 2,000원 $this->assertEquals(2000, $result->summary->totalShipping); } @@ -1209,8 +1211,8 @@ class OrderCalculationServiceTest extends ModuleTestCase baseFee: 2000, ranges: ['volume_weight_divisor' => 5000, 'unit_value' => 1] ); - // 부피 12000cm³ / 5000 = 부피무게 2.4kg - [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1.0, 12000); + // 실무게 1,000g(=1kg) vs 부피 12000cm³ / 5000 = 부피무게 2.4kg → 2.4kg 채택 + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 1000, 12000); $input = new CalculationInput( items: [ @@ -2681,7 +2683,7 @@ class OrderCalculationServiceTest extends ModuleTestCase /** * 테스트 36-11e: RANGE_WEIGHT 정책 + 도서산간 * - * 무게 2.5kg(2500g) → 구간 매칭 → 기본 5,000원 + 추가 3,000원 + * 무게 2,500g(=2.5kg) → 구간 매칭 → 기본 5,000원 + 추가 3,000원 */ public function test_it_applies_extra_fee_with_range_weight(): void { @@ -2690,9 +2692,9 @@ class OrderCalculationServiceTest extends ModuleTestCase ranges: [ 'type' => 'weight', 'tiers' => [ - ['min' => 0, 'max' => 1000, 'fee' => 3000], - ['min' => 1001, 'max' => 3000, 'fee' => 5000], - ['min' => 3001, 'max' => null, 'fee' => 8000], + ['min' => 0, 'max' => 1, 'fee' => 3000], + ['min' => 1, 'max' => 3, 'fee' => 5000], + ['min' => 3, 'max' => null, 'fee' => 8000], ], ], extraFeeEnabled: true, @@ -2700,7 +2702,7 @@ class OrderCalculationServiceTest extends ModuleTestCase ['zipcode' => '63000-63644', 'fee' => 3000], ] ); - [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 2.5, volume: 0); + [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 2500, volume: 0); $shippingAddress = new ShippingAddress(zipcode: '63100'); @@ -2717,7 +2719,7 @@ class OrderCalculationServiceTest extends ModuleTestCase $result = $this->service->calculate($input); - // Then: 기본 5,000원 (1001~3000g 구간) + 추가 3,000원 = 8,000원 + // Then: 기본 5,000원 (1~3kg 구간) + 추가 3,000원 = 8,000원 $this->assertEquals(8000, $result->summary->totalShipping); $this->assertEquals(5000, $result->items[0]->appliedShippingPolicy->shippingAmount); $this->assertEquals(3000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); @@ -2726,7 +2728,7 @@ class OrderCalculationServiceTest extends ModuleTestCase /** * 테스트 36-11f: RANGE_VOLUME 정책 + 도서산간 (이번 버그 시나리오) * - * 부피 30cm³ → 구간 매칭 → 기본 5,000원 + 추가 3,000원 + * 부피 30,000cm³ × 2개 = 60L → 구간 매칭 → 기본 10,000원 + 추가 3,000원 */ public function test_it_applies_extra_fee_with_range_volume(): void { @@ -2736,8 +2738,8 @@ class OrderCalculationServiceTest extends ModuleTestCase 'type' => 'volume', 'tiers' => [ ['min' => 0, 'max' => 50, 'fee' => 5000], - ['min' => 51, 'max' => 100, 'fee' => 10000], - ['min' => 101, 'max' => null, 'fee' => 20000], + ['min' => 50, 'max' => 100, 'fee' => 10000], + ['min' => 100, 'max' => null, 'fee' => 20000], ], ], extraFeeEnabled: true, @@ -2745,7 +2747,7 @@ class OrderCalculationServiceTest extends ModuleTestCase ['zipcode' => '63000-63644', 'fee' => 3000], ] ); - [$product, $option] = $this->createProductWithDimensions(27000, $policy, weight: 0.5, volume: 30); + [$product, $option] = $this->createProductWithDimensions(27000, $policy, weight: 500, volume: 30000); $shippingAddress = new ShippingAddress(zipcode: '63100'); @@ -2762,7 +2764,7 @@ class OrderCalculationServiceTest extends ModuleTestCase $result = $this->service->calculate($input); - // Then: 부피 60cm³(30×2) → 51~100 구간 → 기본 10,000원 + 추가 3,000원 = 13,000원 + // Then: 부피 60,000cm³(30,000×2) = 60L → ~100L 구간 → 기본 10,000원 + 추가 3,000원 = 13,000원 $this->assertEquals(13000, $result->summary->totalShipping); $this->assertEquals(10000, $result->items[0]->appliedShippingPolicy->shippingAmount); $this->assertEquals(3000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); @@ -2771,9 +2773,9 @@ class OrderCalculationServiceTest extends ModuleTestCase /** * 테스트 36-11g: PER_WEIGHT 정책 + 도서산간 + multiply=true * - * 무게 1.5kg, 수량 4개, 단위 2개당 2,000원 - * 기본: ceil(4/2)×2,000=4,000원 - * 추가: multiply는 total_quantity/unit_value 기준 → ceil(4/2)×3,000=6,000원 + * 무게 1,500g(=1.5kg) × 4개 = 6kg, 단위 2kg당 2,000원 + * 기본: ceil(6/2)×2,000=6,000원 + * 추가: multiply 는 기본 배송비와 같은 배송 단위 수(3) 기준 → 3×3,000=9,000원 */ public function test_it_applies_extra_fee_with_per_weight_multiply(): void { @@ -2787,7 +2789,7 @@ class OrderCalculationServiceTest extends ModuleTestCase ], extraFeeMultiply: true ); - [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 1.5, volume: 0); + [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 1500, volume: 0); $shippingAddress = new ShippingAddress(zipcode: '63200'); @@ -2804,16 +2806,16 @@ class OrderCalculationServiceTest extends ModuleTestCase $result = $this->service->calculate($input); - // Then: 기본 ceil(6kg/2)×2,000=6,000원 + 추가 ceil(4/2)×3,000=6,000원 = 12,000원 - $this->assertEquals(12000, $result->summary->totalShipping); + // Then: 기본 ceil(6kg/2kg)=3단위 × 2,000 = 6,000원 + 추가 3단위 × 3,000 = 9,000원 = 15,000원 + $this->assertEquals(15000, $result->summary->totalShipping); $this->assertEquals(6000, $result->items[0]->appliedShippingPolicy->shippingAmount); - $this->assertEquals(6000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); + $this->assertEquals(9000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); } /** * 테스트 36-11h: PER_VOLUME 정책 + 도서산간 * - * 부피 50cm³, 단위 20cm³당 1,500원, 추가배송비 3,000원(1회) + * 부피 50,000cm³(=50L), 단위 20L당 1,500원, 추가배송비 3,000원(1회) */ public function test_it_applies_extra_fee_with_per_volume(): void { @@ -2826,7 +2828,7 @@ class OrderCalculationServiceTest extends ModuleTestCase ['zipcode' => '63000-63644', 'fee' => 3000], ] ); - [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 0, volume: 50); + [$product, $option] = $this->createProductWithDimensions(10000, $policy, weight: 0, volume: 50000); $shippingAddress = new ShippingAddress(zipcode: '63500'); @@ -2843,7 +2845,7 @@ class OrderCalculationServiceTest extends ModuleTestCase $result = $this->service->calculate($input); - // Then: 기본 ceil(50/20)×1,500=4,500원 + 추가 3,000원(1회) = 7,500원 + // Then: 기본 ceil(50L/20L)×1,500=4,500원 + 추가 3,000원(1회) = 7,500원 $this->assertEquals(7500, $result->summary->totalShipping); $this->assertEquals(4500, $result->items[0]->appliedShippingPolicy->shippingAmount); $this->assertEquals(3000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); @@ -5112,10 +5114,10 @@ class OrderCalculationServiceTest extends ModuleTestCase // Then $this->assertEquals(35000, $result->summary->subtotal); $this->assertEquals(5000, $result->summary->productCouponDiscount); - // 배송비 구간: 35000원 → 30001-50000 구간 → 3000원 - $this->assertEquals(3000, $result->summary->totalShipping); - // finalAmount = 35000 - 5000 + 3000 = 33000 - $this->assertEquals(33000, $result->summary->finalAmount); + // 배송비는 쿠폰 할인 후 금액 기준: 35,000 - 5,000 = 30,000원 → "~30,000원" 구간(상한 포함) → 5,000원 + $this->assertEquals(5000, $result->summary->totalShipping); + // finalAmount = 35000 - 5000 + 5000 = 35000 + $this->assertEquals(35000, $result->summary->finalAmount); } /** @@ -7897,4 +7899,486 @@ class OrderCalculationServiceTest extends ModuleTestCase $this->assertEquals(0, $result->summary->totalShipping); $this->assertEquals(30000, $result->summary->subtotal); } + + // ======================================== + // Section 7.15: 구간 경계 · 단위 환산 회귀 (공개 #94) + // + // 계약 ① 구간 매칭 = 상한 포함 + 사다리(ladder). min 은 표시 전용이며 매칭에 쓰지 않는다. + // 계약 ② 단위 = 상품 옵션은 g/cm³ 저장, 배송정책 입력은 kg/L, 환산은 엔진 한 곳에서. + // 계약 ③ 무음 0원 금지 = 잔여 미매칭 경로는 0원을 유지하되 경고 로그를 남긴다. + // ======================================== + + /** + * 수량 구간의 상한은 포함이다 (공개 #94 본문 재현). + * + * 관리자 화면·요약 문자열이 "1~5개: 3,000 / 6개~: 5,000" 으로 약속한 대로 + * 경계값 5 가 첫 구간에 매칭되어야 한다. 기존 반개(半開) 매칭에서는 5 가 + * 어느 구간에도 걸리지 않아 무음 0원(무료배송)이 되었다. + */ + public function test_it_matches_quantity_range_with_inclusive_upper_bound(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_QUANTITY, + ranges: [ + 'tiers' => [ + ['min' => 1, 'max' => 5, 'fee' => 3000], + ['min' => 6, 'max' => null, 'fee' => 5000], + ], + ] + ); + [$product, $option] = $this->createProductWithShippingPolicy(10000, $shippingPolicy); + + $expectations = [1 => 3000, 4 => 3000, 5 => 3000, 6 => 5000, 100 => 5000]; + + foreach ($expectations as $quantity => $expectedFee) { + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem( + productId: $product->id, + productOptionId: $option->id, + quantity: $quantity + ), + ] + )); + + $this->assertEquals( + $expectedFee, + $result->summary->totalShipping, + "수량 {$quantity} 의 배송비가 기대와 다릅니다." + ); + } + } + + /** + * 반개(半開) 형태로 저장된 기존 데이터에서도 사다리 매칭이 정확해야 한다. + * + * 다음 구간의 min 이 현재 구간의 max 와 같은 형태(0~2 / 2~5 / 5~)에서 + * 경계값 2.0kg 은 "~2kg" 구간에 속한다 (상한 포함). + */ + public function test_it_matches_half_open_legacy_tiers_by_upper_bound(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, + ranges: [ + 'tiers' => [ + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 2, 'max' => 5, 'fee' => 5000], + ['min' => 5, 'max' => null, 'fee' => 8000], + ], + ] + ); + // 2,000g = 2.0kg (경계값) + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2000, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(3000, $result->summary->totalShipping); + } + + /** + * 소수 경계(2.5kg)를 가진 연속형 구간도 정확히 매칭되어야 한다. + */ + public function test_it_matches_decimal_tier_boundary(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, + ranges: [ + 'tiers' => [ + ['min' => 0, 'max' => 2.5, 'fee' => 3000], + ['min' => 2.5, 'max' => null, 'fee' => 5000], + ], + ] + ); + // 2,500g = 2.5kg (소수 경계값) + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 2500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(3000, $result->summary->totalShipping); + } + + /** + * tiers 가 비어 있으면 0원을 유지하되 경고 로그를 남긴다 (계약 ③). + */ + public function test_it_logs_warning_when_range_tiers_are_empty(): void + { + Log::spy(); + + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_QUANTITY, + ranges: ['tiers' => []] + ); + [$product, $option] = $this->createProductWithShippingPolicy(10000, $shippingPolicy); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(0, $result->summary->totalShipping); + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains((string) $message, '구간별 배송비')) + ->atLeast()->once(); + } + + /** + * 배송정책이 삭제된 경우 0원을 유지하되 경고 로그를 남긴다 (계약 ③). + */ + public function test_it_logs_warning_when_shipping_policy_is_missing(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::FIXED, + baseFee: 3000 + ); + [$product, $option] = $this->createProductWithShippingPolicy(30000, $shippingPolicy); + + $shippingPolicy->countrySettings()->delete(); + $shippingPolicy->delete(); + + Log::spy(); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(0, $result->summary->totalShipping); + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains((string) $message, '배송정책')) + ->atLeast()->once(); + } + + /** + * 수신자 국가의 설정이 없으면 0원을 유지하되 경고 로그를 남긴다 (계약 ③). + */ + public function test_it_logs_warning_when_country_setting_is_missing(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::FIXED, + baseFee: 3000, + countryCode: 'KR' + ); + [$product, $option] = $this->createProductWithShippingPolicy(30000, $shippingPolicy); + + Log::spy(); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ], + shippingAddress: new ShippingAddress(countryCode: 'US') + )); + + $this->assertEquals(0, $result->summary->totalShipping); + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains((string) $message, '국가별 배송 설정')) + ->atLeast()->once(); + } + + /** + * 무게 구간 정책은 상품의 g 값을 kg 으로 환산해 비교한다 (계약 ②). + * + * 500g 상품 → 0.5kg → "~2kg" 구간. 기존 코드는 ×1000 이중 변환으로 + * 500,000 이 되어 최상위 구간에 오적중했다. + */ + public function test_it_converts_grams_to_kilograms_for_range_weight(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_WEIGHT, + ranges: [ + 'tiers' => [ + ['min' => 0, 'max' => 2, 'fee' => 3000], + ['min' => 2, 'max' => null, 'fee' => 7000], + ], + ] + ); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(3000, $result->summary->totalShipping); + } + + /** + * 부피 구간 정책은 상품의 cm³ 값을 L 로 환산해 비교한다 (계약 ②). + * + * 1,000cm³ = 1L → "~50L" 구간. + */ + public function test_it_converts_cubic_centimeters_to_liters_for_range_volume(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_VOLUME, + ranges: [ + 'tiers' => [ + ['min' => 0, 'max' => 50, 'fee' => 5000], + ['min' => 50, 'max' => null, 'fee' => 9000], + ], + ] + ); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 0, 1000); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(5000, $result->summary->totalShipping); + } + + /** + * 무게당 정책은 kg 단위로 계산한다 (계약 ②). + * + * 500g = 0.5kg, 1kg당 1,000원 → ceil(0.5/1) × 1,000 = 1,000원. + * 기존 코드는 500 ÷ 1 = 500단위 → 500,000원이었다 (공개 #94 재현값). + */ + public function test_it_charges_per_kilogram_for_per_weight_policy(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_WEIGHT, + baseFee: 1000, + ranges: ['unit_value' => 1] + ); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(1000, $result->summary->totalShipping); + } + + /** + * 부피당 정책은 L 단위로 계산한다 (계약 ②). + * + * 1,000cm³ = 1L, 10L당 2,000원 → ceil(1/10) × 2,000 = 2,000원. + */ + public function test_it_charges_per_liter_for_per_volume_policy(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_VOLUME, + baseFee: 2000, + ranges: ['unit_value' => 10] + ); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 0, 1000); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(2000, $result->summary->totalShipping); + } + + /** + * 부피무게 구간은 실무게(kg)와 부피무게(kg)를 같은 단위로 비교한다 (계약 ②). + * + * 500g(0.5kg) + 6,000cm³ ÷ 6,000 = 1kg → 부피무게 1kg 채택 → "~5kg" 구간. + */ + public function test_it_compares_actual_and_volumetric_weight_in_kilograms(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::RANGE_VOLUME_WEIGHT, + ranges: [ + 'volume_weight_divisor' => 6000, + 'tiers' => [ + ['min' => 0, 'max' => 5, 'fee' => 3500], + ['min' => 5, 'max' => null, 'fee' => 9000], + ], + ] + ); + [$product, $option] = $this->createProductWithDimensions(20000, $shippingPolicy, 500, 6000); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(3500, $result->summary->totalShipping); + } + + /** + * 소수 기본배송비는 절삭이 아니라 반올림한다 (검토 메모 12). + * + * base_fee 999.50 × 1단위 → 1,000원 (기존 (int) 절삭은 999원). + */ + public function test_it_rounds_decimal_base_fee_instead_of_truncating(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_QUANTITY, + baseFee: 1000, + ranges: ['unit_value' => 1] + ); + $shippingPolicy->countrySettings()->first()->update(['base_fee' => 999.5]); + [$product, $option] = $this->createProductWithShippingPolicy(10000, $shippingPolicy); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(1000, $result->summary->totalShipping); + } + + /** + * 도서산간 중복 부과는 수량이 아니라 배송 단위 수를 기준으로 한다 (검토 메모 14). + * + * PER_WEIGHT 1kg당 2,000원, 500g 상품 3개 → 1.5kg → 단위 수 2 (수량 3이 아님). + * 기본 2 × 2,000 = 4,000원, 도서산간 2 × 3,000 = 6,000원. + */ + public function test_it_multiplies_extra_fee_by_charge_units_not_quantity(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_WEIGHT, + baseFee: 2000, + ranges: ['unit_value' => 1], + extraFeeEnabled: true, + extraFeeSettings: [ + ['zipcode' => '63*', 'fee' => 3000], + ], + extraFeeMultiply: true + ); + [$product, $option] = $this->createProductWithDimensions(10000, $shippingPolicy, 500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 3), + ], + shippingAddress: new ShippingAddress(zipcode: '63123') + )); + + $this->assertEquals(4000, $result->items[0]->appliedShippingPolicy->shippingAmount); + $this->assertEquals(6000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); + $this->assertEquals(10000, $result->summary->totalShipping); + } + + /** + * 저장된 단위값이 0 이어도 0 으로 나누지 않고 1 단위로 계산한다. + * + * 신규 저장은 FormRequest 의 min:0.01 이 막지만, 그 규칙이 생기기 전에 저장된 정책과 + * 시더·업그레이드처럼 FormRequest 를 거치지 않는 경로는 0 을 그대로 들고 있을 수 있다. + * 이 값이 나눗셈에 닿으면 계산 전체가 DivisionByZeroError 로 죽는다. + * + * PER_WEIGHT base 2,000원 · unit_value 0 → 1 로 클램프 → 1.5kg → ceil(1.5/1)=2 → 4,000원. + */ + public function test_it_clamps_zero_unit_value_instead_of_dividing_by_zero(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_WEIGHT, + baseFee: 2000, + ranges: ['unit_value' => 0] + ); + [$product, $option] = $this->createProductWithDimensions(10000, $shippingPolicy, 1500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(4000, $result->summary->totalShipping); + } + + /** + * 저장된 단위값이 음수여도 1 단위로 클램프한다. + * + * 음수는 나눗셈이 성립해 예외가 나지 않는 대신 단위 수가 음수가 되어, 배송비가 음수이거나 + * 0 원으로 뒤집힌다 — 예외 없이 금액만 틀리는 무음 결함이라 0 과 함께 고정한다. + */ + public function test_it_clamps_negative_unit_value_instead_of_inverting_the_fee(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_WEIGHT, + baseFee: 2000, + ranges: ['unit_value' => -1] + ); + [$product, $option] = $this->createProductWithDimensions(10000, $shippingPolicy, 1500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(4000, $result->summary->totalShipping); + } + + /** + * 도서산간 곱의 단위 수 계산도 같은 클램프를 적용한다. + * + * 단위 수는 기본 배송비와 도서산간 곱 두 곳에서 각각 나눗셈을 수행하므로, 한쪽만 막으면 + * 도서산간을 켠 정책에서만 계산이 죽는다. + */ + public function test_it_clamps_zero_unit_value_in_extra_fee_charge_units(): void + { + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::PER_WEIGHT, + baseFee: 2000, + ranges: ['unit_value' => 0], + extraFeeEnabled: true, + extraFeeSettings: [ + ['zipcode' => '63*', 'fee' => 3000], + ], + extraFeeMultiply: true + ); + [$product, $option] = $this->createProductWithDimensions(10000, $shippingPolicy, 1500, 0); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ], + shippingAddress: new ShippingAddress(zipcode: '63123') + )); + + $this->assertEquals(4000, $result->items[0]->appliedShippingPolicy->shippingAmount); + $this->assertEquals(6000, $result->items[0]->appliedShippingPolicy->extraShippingAmount); + } + + /** + * 외부 API 가 음수 배송비를 반환하면 기본 배송비로 폴백하고 경고를 남긴다 (검토 메모 11). + */ + public function test_it_falls_back_to_base_fee_when_api_returns_negative_fee(): void + { + Http::fake([ + 'https://shipping-api.example.com/calculate' => Http::response(['shipping_fee' => -500], 200), + ]); + Log::spy(); + + $shippingPolicy = $this->createShippingPolicy( + chargePolicy: ChargePolicyEnum::API, + baseFee: 3000, + apiEndpoint: 'https://shipping-api.example.com/calculate' + ); + [$product, $option] = $this->createProductWithShippingPolicy(30000, $shippingPolicy); + + $result = $this->service->calculate(new CalculationInput( + items: [ + new CalculationItem(productId: $product->id, productOptionId: $option->id, quantity: 1), + ] + )); + + $this->assertEquals(3000, $result->summary->totalShipping); + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => str_contains((string) $message, '음수')) + ->atLeast()->once(); + } } diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCancellationServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCancellationServiceTest.php index 4683d5fc..3611a21b 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCancellationServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCancellationServiceTest.php @@ -4,6 +4,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; use App\Extension\HookManager; use App\Models\User; +use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Ecommerce\Database\Factories\ProductFactory; use Modules\Sirsoft\Ecommerce\DTO\CalculationInput; use Modules\Sirsoft\Ecommerce\DTO\CalculationItem; @@ -1238,6 +1239,77 @@ class OrderCancellationServiceTest extends ModuleTestCase $this->assertEquals(0, OrderRefund::where('order_id', $order->id)->count()); } + /** + * C-1-6: 부분취소 미리보기가 쿠폰 발급 행을 일절 변이하지 않는지 검증 (#108) + * + * C-1-5 는 단일 옵션 전량 취소라 buildFullCancelResult 조기 반환 경로를 타서 + * 재계산 자체가 일어나지 않는다. 잔여 옵션이 남는 부분취소여야 재계산 경로에 + * 진입하므로, 여기서는 옵션 2개 중 1개만 취소한다. + * + * 전후 status/used_at 비교만으로는 "변경했다 되돌리는" 왕복을 잡지 못하므로 + * (1) 실행 중 발생한 UPDATE 쿼리 자체를 세고 (2) updated_at 불변을 함께 단언한다. + * + * @effects preview_refund_issues_zero_write_queries_on_coupon_issues, preview_refund_keeps_coupon_issue_updated_at_unchanged, preview_refund_keeps_coupon_status_used_at_order_id_unchanged, partial_cancel_preview_reaches_recalculation_path_not_full_cancel_shortcut, recalculation_never_reads_coupon_usage_state_in_snapshot_mode + */ + public function test_partial_cancel_preview_issues_no_write_query_on_coupon_issues(): void + { + $this->createShippingPolicy(); + + $couponIssue = $this->createCouponWithIssue( + targetType: CouponTargetType::ORDER_AMOUNT, + discountType: CouponDiscountType::FIXED, + discountValue: 5000, + ); + + [$pA, $oA] = $this->createProductWithOption(price: 30000); + [$pB, $oB] = $this->createProductWithOption(price: 20000); + + $input = new CalculationInput( + items: [ + new CalculationItem(productId: $pA->id, productOptionId: $oA->id, quantity: 1), + new CalculationItem(productId: $pB->id, productOptionId: $oB->id, quantity: 1), + ], + couponIssueIds: [$couponIssue->id], + ); + $order = $this->createOrderFromCalculation($input); + + $couponTable = (new CouponIssue)->getTable(); + $before = CouponIssue::find($couponIssue->id); + + // 미리보기 실행 중 쿠폰 발급 테이블에 발생한 쓰기 쿼리를 수집 + $writeQueries = []; + DB::listen(function ($query) use ($couponTable, &$writeQueries) { + $sql = strtolower($query->sql); + if (str_contains($sql, $couponTable) && ! str_starts_with(ltrim($sql), 'select')) { + $writeQueries[] = $query->sql; + } + }); + + // 옵션 2개 중 1개만 취소 → 잔여 옵션이 있으므로 재계산 경로 진입 + $cancelOption = $order->options->firstWhere('product_option_id', $oA->id); + $this->cancellationService->previewRefund($order, [ + ['order_option_id' => $cancelOption->id, 'cancel_quantity' => 1], + ]); + + $this->assertSame( + [], + $writeQueries, + '환불 예상 미리보기는 조회만 수행해야 하는데 쿠폰 발급 테이블에 쓰기 쿼리가 발생했습니다: ' + .implode(' | ', $writeQueries) + ); + + // 왕복 UPDATE 는 값을 되돌려도 updated_at 을 오염시킨다 + $after = CouponIssue::find($couponIssue->id); + $this->assertSame( + $before->updated_at?->toDateTimeString(), + $after->updated_at?->toDateTimeString(), + '미리보기 후 쿠폰 발급 행의 updated_at 이 변경되었습니다.' + ); + $this->assertSame($before->status, $after->status); + $this->assertSame($before->used_at?->toDateTimeString(), $after->used_at?->toDateTimeString()); + $this->assertSame($before->order_id, $after->order_id); + } + // ================================================================ // C-2. 환불 처리 검증 (5건) // ================================================================ diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingCurrencySnapshotTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingCurrencySnapshotTest.php index 7851b255..85bd6d9e 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingCurrencySnapshotTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingCurrencySnapshotTest.php @@ -4,9 +4,11 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; use App\Models\User; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; use Modules\Sirsoft\Ecommerce\Repositories\Contracts\EcommerceUserProfileRepositoryInterface; use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService; use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; +use Modules\Sirsoft\Ecommerce\Support\CurrencySettingsCache; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; use ReflectionMethod; @@ -96,6 +98,80 @@ class OrderProcessingCurrencySnapshotTest extends ModuleTestCase $this->assertSame(1, $snapshot['exchange_rates']['USD']['base_unit']); } + // ────────────────────────────────────────────── + // 미등록 통화 방어 (공개 #91) — 삭제 영속화로 처음 열리는 경로 + // ────────────────────────────────────────────── + + /** + * 등록 통화를 KRW 단독으로 축소합니다. (관리자가 나머지를 삭제한 상태) + */ + private function registerOnlyBaseCurrency(): void + { + Config::set('g7_settings.modules.sirsoft-ecommerce.language_currency', [ + 'default_currency' => 'KRW', + 'currencies' => [ + [ + 'code' => 'KRW', + 'name' => ['ko' => 'KRW', 'en' => 'KRW'], + 'exchange_rate' => null, + 'base_unit' => 1000, + 'rounding_unit' => '1', + 'rounding_method' => 'floor', + 'decimal_places' => 0, + 'is_default' => true, + ], + ], + ]); + + CurrencySettingsCache::clear(); + $this->app->forgetInstance(CurrencyConversionService::class); + $this->app->forgetInstance(OrderProcessingService::class); + } + + /** + * 사용 중지된 통화를 선호 통화로 가진 유저는 기본 통화로 폴백한다. + * + * 폴백이 없으면 스냅샷 exchange_rates 에 그 코드가 없어 체크아웃이 차단된다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects unregistered_active_currency_falls_back_to_base + */ + public function test_unregistered_persisted_currency_falls_back_to_base(): void + { + $user = User::factory()->create(); + Auth::login($user); + app(EcommerceUserProfileRepositoryInterface::class)->setPreferredCurrency($user->id, 'EUR'); + request()->headers->remove('X-Currency'); + + $this->registerOnlyBaseCurrency(); + + $snapshot = $this->invokeBuildSnapshot(); + + $this->assertSame('KRW', $snapshot['order_currency'], '미등록 선호 통화가 결제 통화로 그대로 채택되었습니다.'); + $this->assertArrayHasKey('KRW', $snapshot['exchange_rates']); + Auth::logout(); + } + + /** + * 사용 중지된 통화가 X-Currency 헤더로 들어와도 기본 통화로 폴백한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects unregistered_active_currency_falls_back_to_base + */ + public function test_unregistered_x_currency_header_falls_back_to_base(): void + { + request()->headers->set('X-Currency', 'JPY'); + + $this->registerOnlyBaseCurrency(); + + $snapshot = $this->invokeBuildSnapshot(); + + $this->assertSame('KRW', $snapshot['order_currency'], '미등록 헤더 통화가 결제 통화로 그대로 채택되었습니다.'); + request()->headers->remove('X-Currency'); + } + // ────────────────────────────────────────────── // 환불 불변식 (A2 D-BASE-3) — 핵심 // ────────────────────────────────────────────── diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php index c2d337b8..fb392257 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php @@ -644,6 +644,79 @@ class OrderProcessingServiceTest extends ModuleTestCase $this->assertStringContainsString('"items":[', json_encode($snapshot)); } + /** + * 주문의 총 무게/부피는 주문 옵션 소계의 합으로 기록된다 (공개 #94 / N7). + * + * 기존에는 0 으로 고정 기록되어, 배송사 연동·운임 정산이 이 값을 읽으면 + * 무게 없는 주문으로 취급됐다. 값의 단위는 상품 옵션과 같은 g / cm³ 다. + */ + public function test_create_from_temp_order_records_total_weight_and_volume(): void + { + $user = User::factory()->create(); + + $product = Product::factory()->create(); + $productOption = ProductOption::factory()->create([ + 'product_id' => $product->id, + 'weight' => 500, + 'volume' => 1200, + ]); + + $tempOrder = TempOrderFactory::new() + ->forUser($user) + ->withItems([ + [ + 'cart_id' => 1, + 'product_id' => $product->id, + 'product_option_id' => $productOption->id, + 'quantity' => 3, + ], + ]) + ->withCalculationResult([ + 'summary' => ['final_amount' => 103000], + 'items' => [], + 'promotions' => [ + 'product_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []], + 'order_promotions' => ['coupons' => [], 'discount_codes' => [], 'events' => []], + ], + 'validation_errors' => [], + ]) + ->create(); + + $item = new ItemCalculation( + productId: $product->id, + productOptionId: $productOption->id, + quantity: 3, + unitPrice: 100000, + subtotal: 300000, + finalAmount: 300000, + ); + + $this->mockCalculationService($this->makeCalculationResult(103000, [ + 'items' => [$item], + ])); + + $order = $this->service->createFromTempOrder( + $tempOrder, + ['name' => 'Test', 'phone' => '010-0000-0000', 'email' => 'test@test.com'], + ['recipient_name' => 'Test', 'recipient_phone' => '010-0000-0000', 'zipcode' => '00000', 'address' => 'Test', 'address_detail' => 'Test'], + 'card', + 103000 + ); + + $order->refresh(); + + // 단위 무게는 상품 옵션의 g 값을 그대로 복사한다 (kg 환산은 배송비 계산 시점에만) + $orderOption = $order->options()->first(); + $this->assertEquals(500.0, (float) $orderOption->unit_weight); + $this->assertEquals(1200.0, (float) $orderOption->unit_volume); + $this->assertEquals(1500.0, (float) $orderOption->subtotal_weight); + $this->assertEquals(3600.0, (float) $orderOption->subtotal_volume); + + // 주문 합계는 옵션 소계의 합 + $this->assertEquals(1500.0, (float) $order->total_weight); + $this->assertEquals(3600.0, (float) $order->total_volume); + } + public function test_create_from_temp_order_saves_order_meta_with_calculation_input(): void { $this->allowAnyMileageUsage(); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderServiceTest.php index 877a461a..33ad449f 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderServiceTest.php @@ -408,11 +408,13 @@ class OrderServiceTest extends ModuleTestCase ->once() ->andReturn(5); - // 전이 알림 발화용 — 스냅샷이 비어(이전 상태 null) 전 주문이 전이 대상 → fresh 조회 + // 전이 알림 발화용(fresh 조회) + 서비스 진입부의 스코프 재적용 조회 두 곳에서 호출된다. + // 일괄 라우트는 라우트 모델이 없어 미들웨어 스코프 검사가 스킵되므로 서비스가 재적용한다. + // 상한을 없애지 않고 2회로 좁힌다 — 상한을 지우면 중복 조회 회귀를 못 잡는다. $this->mockRepository ->shouldReceive('findByIdsKeyed') ->with($ids) - ->once() + ->twice() ->andReturn(new Collection); // When: bulkUpdate 호출 @@ -446,6 +448,13 @@ class OrderServiceTest extends ModuleTestCase ->once() ->andReturn(2); + // 서비스 진입부의 스코프 재적용 조회 — 일괄 라우트는 라우트 모델이 없어 + // PermissionMiddleware 의 스코프 검사가 스킵되므로 서비스가 재적용한다. + $this->mockRepository + ->shouldReceive('findByIdsKeyed') + ->once() + ->andReturn(new Collection); + // When: bulkUpdate 호출 $result = $this->service->bulkUpdate($data); @@ -633,7 +642,8 @@ class OrderServiceTest extends ModuleTestCase $this->mockRepository->shouldReceive('getSnapshotsByIds')->with($ids)->once()->andReturn([]); $this->mockRepository->shouldReceive('bulkUpdateStatus')->once()->andReturn(3); $this->mockRepository->shouldReceive('bulkUpdateOptionStatus')->once()->andReturn(3); - $this->mockRepository->shouldReceive('findByIdsKeyed')->once()->andReturn(new Collection); + // 스코프 재적용 조회 + 전이 알림 fresh 조회 = 2회 (상한 유지) + $this->mockRepository->shouldReceive('findByIdsKeyed')->twice()->andReturn(new Collection); $approveCount = 0; $cb = function () use (&$approveCount) { diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceRowDiskTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceRowDiskTest.php new file mode 100644 index 00000000..eac2e2de --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceRowDiskTest.php @@ -0,0 +1,285 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use App\Extension\Storage\ModuleStorageDriver; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductImage; +use Modules\Sirsoft\Ecommerce\Services\ProductImageService; +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * ProductImageService 행 disk 기준 서빙/삭제 테스트 + * + * 디스크 전환 이후에도 전환 이전 행(구 disk)의 서빙이 404 가 되지 않고, + * 삭제가 그 행의 실제 저장 위치를 향하는지(혼재 운용 정합) 검증합니다. + */ +class ProductImageServiceRowDiskTest extends ModuleTestCase +{ + private ProductImageService $service; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + AssetStorage::flush(); + + $this->service = app(ProductImageService::class); + } + + protected function tearDown(): void + { + AssetStorage::flush(); + + parent::tearDown(); + } + + /** + * 지정 disk 에 파일과 이미지 행을 함께 생성합니다. + * + * @param string $disk 행 disk + * @param string $path 이미지 경로 + * @return ProductImage 생성된 행 + */ + private function makeRowOnDisk(string $disk, string $path): ProductImage + { + (new ModuleStorageDriver('sirsoft-ecommerce', $disk))->put('images', $path, 'image-bytes'); + + return ProductImage::create([ + 'temp_key' => 'row-disk-test', + 'original_filename' => 'a.jpg', + 'stored_filename' => basename($path), + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'is_thumbnail' => false, + 'sort_order' => 0, + ]); + } + + /** + * @effects serve_and_delete_follow_row_disk, mixed_rows_resolve_per_row_disk + */ + #[Test] + public function download_serves_row_from_its_recorded_disk(): void + { + // 혼재 운용 — 주입 스토리지 disk 와 다른 disk 의 행도 서빙 가능해야 한다 + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'products/temp/row-disk-test/cdn.jpg'); + $localRow = $this->makeRowOnDisk('modules', 'products/temp/row-disk-test/local.jpg'); + + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($cdnRow->hash)); + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($localRow->hash)); + } + + /** + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function delete_removes_file_on_row_disk(): void + { + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'products/temp/row-disk-test/del.jpg'); + $cdnDriver = new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'); + $this->assertTrue($cdnDriver->exists('images', $cdnRow->path)); + + $this->assertTrue($this->service->delete($cdnRow->id)); + + $this->assertFalse($cdnDriver->exists('images', $cdnRow->path)); + } + + /** + * deleteByProductId 반환값 — 행이 CDN 디스크에만 있어도 행 disk 삭제 결과를 + * 반환해야 한다 (기본 디스크의 빈 디렉토리 결과로 오염 금지). + * + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function delete_by_product_id_returns_row_disk_result_for_cdn_only_rows(): void + { + $product = Product::create([ + 'product_code' => 'ROWDEL-'.Str::random(6), + 'name' => ['ko' => '행 disk 삭제 테스트'], + 'selling_price' => 1000, + 'stock_quantity' => 1, + 'sales_status' => 'on_sale', + 'display_status' => 'visible', + ]); + + $path = "products/{$product->product_code}/cdn-only.jpg"; + $cdnDriver = new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'); + $cdnDriver->put('images', $path, 'image-bytes'); + ProductImage::create([ + 'product_id' => $product->id, + 'original_filename' => 'cdn-only.jpg', + 'stored_filename' => 'cdn-only.jpg', + 'disk' => 'fake_cdn', + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'is_thumbnail' => false, + 'sort_order' => 0, + ]); + + $this->assertTrue($this->service->deleteByProductId($product->id)); + $this->assertFalse($cdnDriver->exists('images', $path)); + } + + /** + * deleteByProductId — disk 컬럼이 빈 구 데이터 행은 주입 스토리지 소속으로 + * 정규화되어 함께 삭제된다. + * + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function delete_by_product_id_normalizes_blank_disk_rows_to_base_storage(): void + { + $product = Product::create([ + 'product_code' => 'ROWDEL-'.Str::random(6), + 'name' => ['ko' => '구 데이터 삭제 테스트'], + 'selling_price' => 1000, + 'stock_quantity' => 1, + 'sales_status' => 'on_sale', + 'display_status' => 'visible', + ]); + + $path = "products/{$product->product_code}/legacy.jpg"; + $baseDriver = new ModuleStorageDriver('sirsoft-ecommerce', 'modules'); + $baseDriver->put('images', $path, 'image-bytes'); + ProductImage::create([ + 'product_id' => $product->id, + 'original_filename' => 'legacy.jpg', + 'stored_filename' => 'legacy.jpg', + 'disk' => '', + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'is_thumbnail' => false, + 'sort_order' => 0, + ]); + + $this->assertTrue($this->service->deleteByProductId($product->id)); + $this->assertFalse($baseDriver->exists('images', $path)); + } + + /** + * put 종단 — SP 카테고리 배선(storageCategoryServices)을 실제로 경유해 + * 공개 자산 디스크 설정값이 신규 행의 disk 컬럼에 기록되는지 단언합니다. + * (픽스처 disk 직접 주입이 아니라 실제 업로드 경로 — 배선 회귀 검출) + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_configured_public_asset_disk_on_row(): void + { + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + + // 컨테이너에서 새로 해석 — contextual binding 이 getStorageFor('images') 를 평가 + $service = app(ProductImageService::class); + + $image = $service->upload( + UploadedFile::fake()->image('cdn-upload.jpg', 40, 40) + ); + + $this->assertSame('fake_cdn', $image->disk); + $this->assertTrue( + (new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'))->exists('images', $image->path) + ); + + // 뒷정리 — 업로드 파일 제거 (행 disk 기준 삭제) + $this->assertTrue($service->delete($image->id)); + } + + /** + * put 종단 — 공개 자산 디스크 미설정이면 신규 행이 기본 디스크로 기록 (현행 보존) + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_base_disk_when_public_asset_disk_unset(): void + { + Config::set('core.storage.public_asset_disk', ''); + + $service = app(ProductImageService::class); + + $image = $service->upload( + UploadedFile::fake()->image('local-upload.jpg', 40, 40) + ); + + $this->assertSame('modules', $image->disk); + + $this->assertTrue($service->delete($image->id)); + } + + /** + * 행 disk 를 제공하던 플러그인이 비활성화되어 config 에서 사라진 상태를 만듭니다. + * + * @param string $disk 사라지게 할 디스크명 + */ + private function vanishDisk(string $disk): void + { + Config::set("filesystems.disks.{$disk}", null); + Storage::forgetDisk($disk); + AssetStorage::flush(); + } + + /** + * 고아 disk 행의 서빙은 500 이 아니라 스트리밍 폴백이어야 한다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function download_falls_back_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'products/row-disk-test/orphan.jpg'); + $this->vanishDisk('fake_cdn'); + + $this->assertNull($this->service->download($row->hash)); + } + + /** + * 고아 disk 행의 삭제도 예외 없이 완료되어야 한다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function delete_succeeds_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'products/row-disk-test/orphan-del.jpg'); + $this->vanishDisk('fake_cdn'); + + $this->assertTrue($this->service->delete($row->id)); + $this->assertNull(ProductImage::find($row->id)); + } + + /** + * 상품 삭제(이미지 일괄 삭제)도 고아 disk 행 때문에 막히면 안 된다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function delete_by_product_id_succeeds_when_row_disk_is_orphaned(): void + { + $product = Product::factory()->create(); + $row = $this->makeRowOnDisk('fake_cdn', 'products/row-disk-test/orphan-bulk.jpg'); + $row->update(['product_id' => $product->id, 'temp_key' => null]); + + $this->vanishDisk('fake_cdn'); + + $this->assertTrue($this->service->deleteByProductId($product->id)); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceTest.php index 080f86d7..bb6c132c 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceTest.php @@ -434,6 +434,11 @@ class ProductImageServiceTest extends ModuleTestCase ->with($sourceHash) ->andReturn($sourceImage); + // 복사는 원본 행 disk 기준으로 스토리지를 해석한다 (혼재 운용, 공개#100) + $this->storage + ->shouldReceive('getDisk') + ->andReturn('local'); + $this->storage ->shouldReceive('get') ->once() diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceRowDiskTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceRowDiskTest.php new file mode 100644 index 00000000..72757201 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceRowDiskTest.php @@ -0,0 +1,209 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use App\Extension\Storage\ModuleStorageDriver; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Storage; +use Modules\Sirsoft\Ecommerce\Enums\ReviewStatus; +use Modules\Sirsoft\Ecommerce\Models\OrderOption; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductReview; +use Modules\Sirsoft\Ecommerce\Models\ProductReviewImage; +use Modules\Sirsoft\Ecommerce\Services\ProductReviewImageService; +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * ProductReviewImageService 행 disk 기준 서빙/삭제 테스트 + * + * 공개 자산 디스크 전환 이후에도 전환 이전 행(구 disk)의 서빙이 404 가 되지 않고, + * 삭제가 그 행의 실제 저장 위치를 향하는지(혼재 운용 정합) 검증합니다. + * + * ProductReviewImageServiceTest 는 StorageInterface 를 Mock 으로 대체하므로 + * 실제 디스크 해석(withDisk) 회귀를 검출할 수 없어 별도 파일로 둡니다. + */ +class ProductReviewImageServiceRowDiskTest extends ModuleTestCase +{ + private ProductReviewImageService $service; + + private ProductReview $review; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + AssetStorage::flush(); + + Auth::login($this->createUser()); + + $product = Product::factory()->onSale()->create(); + $orderOption = OrderOption::factory()->create(['product_id' => $product->id]); + + $this->review = ProductReview::factory()->create([ + 'product_id' => $product->id, + 'order_option_id' => $orderOption->id, + 'user_id' => Auth::id(), + 'status' => ReviewStatus::VISIBLE->value, + ]); + + $this->service = app(ProductReviewImageService::class); + } + + protected function tearDown(): void + { + AssetStorage::flush(); + + parent::tearDown(); + } + + /** + * 지정 disk 에 파일과 리뷰 이미지 행을 함께 생성합니다. + * + * @param string $disk 행 disk + * @param string $path 이미지 경로 (images/ 하위 상대 경로) + * @return ProductReviewImage 생성된 행 + */ + private function makeRowOnDisk(string $disk, string $path): ProductReviewImage + { + (new ModuleStorageDriver('sirsoft-ecommerce', $disk))->put('images', $path, 'image-bytes'); + + return ProductReviewImage::create([ + 'review_id' => $this->review->id, + 'original_filename' => 'review.jpg', + 'stored_filename' => basename($path), + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/jpeg', + 'file_size' => 11, + 'collection' => 'main', + 'is_thumbnail' => false, + 'sort_order' => 0, + ]); + } + + /** + * @effects serve_and_delete_follow_row_disk, mixed_rows_resolve_per_row_disk + */ + #[Test] + public function download_serves_row_from_its_recorded_disk(): void + { + // 혼재 운용 — 주입 스토리지 disk 와 다른 disk 의 행도 서빙 가능해야 한다 + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'reviews/row-disk-test/cdn.jpg'); + $localRow = $this->makeRowOnDisk('modules', 'reviews/row-disk-test/local.jpg'); + + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($cdnRow->hash)); + $this->assertInstanceOf(StreamedResponse::class, $this->service->download($localRow->hash)); + } + + /** + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function delete_removes_file_on_row_disk(): void + { + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'reviews/row-disk-test/del.jpg'); + $cdnDriver = new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'); + $this->assertTrue($cdnDriver->exists('images', $cdnRow->path)); + + $this->assertTrue($this->service->delete($cdnRow)); + + $this->assertFalse($cdnDriver->exists('images', $cdnRow->path)); + } + + /** + * 행 disk 를 제공하던 플러그인이 비활성화되어 config 에서 사라진 상태를 만듭니다. + * + * @param string $disk 사라지게 할 디스크명 + */ + private function vanishDisk(string $disk): void + { + Config::set("filesystems.disks.{$disk}", null); + Storage::forgetDisk($disk); + AssetStorage::flush(); + } + + /** + * 고아 disk 행의 서빙은 500 이 아니라 스트리밍 폴백이어야 한다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function download_falls_back_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'reviews/row-disk-test/orphan.jpg'); + $this->vanishDisk('fake_cdn'); + + $this->assertNull($this->service->download($row->hash)); + } + + /** + * 고아 disk 행의 삭제도 예외 없이 완료되어야 한다(리뷰 삭제 자체가 막히면 안 됨). + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function delete_succeeds_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'reviews/row-disk-test/orphan-del.jpg'); + $this->vanishDisk('fake_cdn'); + + $this->assertTrue($this->service->delete($row)); + $this->assertNull(ProductReviewImage::find($row->id)); + } + + /** + * put 종단 — SP 카테고리 배선을 실제로 경유해 공개 자산 디스크 설정값이 + * 신규 행의 disk 컬럼에 기록되는지 단언합니다(배선 회귀 검출). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_configured_public_asset_disk_on_row(): void + { + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + + $service = app(ProductReviewImageService::class); + $image = $service->upload( + UploadedFile::fake()->image('cdn-review.jpg', 40, 40), + $this->review + ); + + $this->assertSame('fake_cdn', $image->disk); + $this->assertTrue( + (new ModuleStorageDriver('sirsoft-ecommerce', 'fake_cdn'))->exists('images', $image->path) + ); + + $this->assertTrue($service->delete($image)); + } + + /** + * put 종단 — 공개 자산 디스크 미설정이면 신규 행이 기본 디스크로 기록(현행 보존). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_base_disk_when_public_asset_disk_unset(): void + { + Config::set('core.storage.public_asset_disk', ''); + + $service = app(ProductReviewImageService::class); + $image = $service->upload( + UploadedFile::fake()->image('local-review.jpg', 40, 40), + $this->review + ); + + $this->assertSame('modules', $image->disk); + + $this->assertTrue($service->delete($image)); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php index 271a5c27..95cdcd4d 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; use App\Contracts\Extension\StorageInterface; +use App\Extension\HookManager; use App\Models\User; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Auth; @@ -177,6 +178,51 @@ class ProductReviewImageServiceTest extends ModuleTestCase $this->assertFalse($image->is_thumbnail); } + #[Test] + public function test_upload_fires_hooks(): void + { + // Arrange + $beforeUploadFired = false; + $afterUploadFired = false; + $filterApplied = false; + + HookManager::addAction('sirsoft-ecommerce.review-image.before_upload', function () use (&$beforeUploadFired) { + $beforeUploadFired = true; + }); + + HookManager::addFilter('sirsoft-ecommerce.review-image.filter_upload_file', function ($file) use (&$filterApplied) { + $filterApplied = true; + + // 반환 파일이 실제로 소비되는지 검증하기 위해 다른 이름/확장자의 파일을 반환 + return UploadedFile::fake()->image('filtered.webp'); + }); + + HookManager::addAction('sirsoft-ecommerce.review-image.after_upload', function () use (&$afterUploadFired) { + $afterUploadFired = true; + }); + + $file = UploadedFile::fake()->image('original.jpg'); + + $this->settingsService->shouldReceive('getSetting')->andReturn(5); + $this->storage->shouldReceive('put')->andReturn(true); + $this->storage->shouldReceive('getDisk')->andReturn('local'); + + // Act + $image = $this->service->upload($file, $this->review); + + // Assert + $this->assertTrue($beforeUploadFired, 'before_upload hook should be fired'); + $this->assertTrue($filterApplied, 'filter_upload_file hook should be applied'); + $this->assertTrue($afterUploadFired, 'after_upload hook should be fired'); + // 필터가 반환한 파일이 저장 파일명의 근거가 되어야 한다 (반환값 소비 증명) + $this->assertStringEndsWith('.webp', $image->stored_filename, 'filter return value should be consumed'); + + // Cleanup hooks + HookManager::clearAction('sirsoft-ecommerce.review-image.before_upload'); + HookManager::clearFilter('sirsoft-ecommerce.review-image.filter_upload_file'); + HookManager::clearAction('sirsoft-ecommerce.review-image.after_upload'); + } + // ======================================== // delete() 테스트 // ======================================== @@ -189,6 +235,11 @@ class ProductReviewImageServiceTest extends ModuleTestCase 'path' => 'reviews/1/test.jpg', ]); + // 삭제는 행 disk 기준으로 스토리지를 해석한다 (혼재 운용, 공개#100) + $this->storage + ->shouldReceive('getDisk') + ->andReturn('local'); + $this->storage ->shouldReceive('exists') ->with('images', 'reviews/1/test.jpg') @@ -215,6 +266,11 @@ class ProductReviewImageServiceTest extends ModuleTestCase 'path' => 'reviews/1/missing.jpg', ]); + // 삭제는 행 disk 기준으로 스토리지를 해석한다 (혼재 운용, 공개#100) + $this->storage + ->shouldReceive('getDisk') + ->andReturn('local'); + $this->storage ->shouldReceive('exists') ->with('images', 'reviews/1/missing.jpg') @@ -276,6 +332,11 @@ class ProductReviewImageServiceTest extends ModuleTestCase 'original_filename' => 'test.jpg', ]); + // 서빙은 행 disk 기준으로 스토리지를 해석한다 (혼재 운용, 공개#100) + $this->storage + ->shouldReceive('getDisk') + ->andReturn('local'); + $this->storage ->shouldReceive('response') ->once() diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewServiceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewServiceTest.php index c97fda06..b9b3b6db 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewServiceTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewServiceTest.php @@ -3,6 +3,7 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; use App\Contracts\Extension\StorageInterface; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Queue; @@ -432,6 +433,14 @@ class ProductReviewServiceTest extends ModuleTestCase #[Test] public function test_bulk_update_status_delegates_to_repository(): void { + // 일괄 라우트는 라우트 모델이 없어 PermissionMiddleware 의 스코프 검사가 스킵되므로 + // 서비스가 대상을 조회해 스코프를 재적용한다 — 순수 위임이 아니다. + $this->repository + ->shouldReceive('getByIdsWithImages') + ->with([1, 2, 3]) + ->once() + ->andReturn(new EloquentCollection); + $this->repository ->shouldReceive('bulkUpdateStatus') ->with([1, 2, 3], 'hidden') diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDeleteTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDeleteTest.php index b5e933f2..8769f360 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDeleteTest.php +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDeleteTest.php @@ -2,18 +2,22 @@ namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; +use App\Extension\HookManager; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Mockery; +use Modules\Sirsoft\Ecommerce\Exceptions\ProductHasOrderHistoryException; use Modules\Sirsoft\Ecommerce\Models\Category; use Modules\Sirsoft\Ecommerce\Models\OrderOption; use Modules\Sirsoft\Ecommerce\Models\Product; use Modules\Sirsoft\Ecommerce\Models\ProductAdditionalOption; use Modules\Sirsoft\Ecommerce\Models\ProductImage; +use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; use Modules\Sirsoft\Ecommerce\Models\ProductLabel; use Modules\Sirsoft\Ecommerce\Models\ProductLabelAssignment; use Modules\Sirsoft\Ecommerce\Models\ProductOption; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; use Modules\Sirsoft\Ecommerce\Services\ProductService; use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; @@ -324,7 +328,7 @@ class ProductServiceDeleteTest extends ModuleTestCase OrderOption::factory()->create(['product_id' => $product->id]); // Then: 도메인 예외 throw + 상품 보존 - $this->expectException(\Modules\Sirsoft\Ecommerce\Exceptions\ProductHasOrderHistoryException::class); + $this->expectException(ProductHasOrderHistoryException::class); try { $this->service->delete($product); @@ -333,6 +337,91 @@ class ProductServiceDeleteTest extends ModuleTestCase } } + // ======================================== + // delete() — 문의 스레드 정리 (#107 확대판) + // ======================================== + + /** + * 상품 삭제 시 문의 게시판이 설정돼 있으면 피벗 전건(소프트 삭제 포함)에 대해 + * inquiry.delete 훅이 호출되고, 피벗은 trashed 포함 영구 삭제된다. + */ + public function test_delete_fires_inquiry_delete_hook_per_pivot_including_trashed(): void + { + // Given: 문의 게시판 설정 + 상품에 딸린 피벗 (alive 2건 + trashed 1건) + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', 'test-inquiry-board'); + + $product = Product::factory()->create(); + $productId = $product->id; + + ProductInquiry::factory()->create(['product_id' => $productId, 'inquirable_id' => 701]); + ProductInquiry::factory()->create(['product_id' => $productId, 'inquirable_id' => 702]); + $trashed = ProductInquiry::factory()->create(['product_id' => $productId, 'inquirable_id' => 703]); + $trashed->delete(); + + // And: 게시판 삭제 훅을 걷어내고 호출된 Post ID 를 수집하는 훅으로 교체 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.delete'); + $deletedPostIds = []; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.delete', + function ($result, $slug, $postId) use (&$deletedPostIds) { + $deletedPostIds[] = (int) $postId; + + return true; + }, + priority: 1 + ); + + // When: 상품 삭제 + $result = $this->service->delete($product); + + // Then: 피벗 수만큼(withTrashed 포함 3건) 훅 호출 + $this->assertTrue($result); + $this->assertCount(3, $deletedPostIds); + $this->assertEqualsCanonicalizing([701, 702, 703], $deletedPostIds); + + // And: 피벗은 trashed 포함 0건 (forceDelete — 행 자체 소멸) + $this->assertDatabaseMissing('ecommerce_product_inquiries', ['product_id' => $productId]); + $this->assertDatabaseMissing('ecommerce_products', ['id' => $productId]); + } + + /** + * 문의 게시판 미설정이면 훅 없이 피벗만 정리되고 상품 삭제는 성공한다 (안전 열화). + */ + public function test_delete_without_board_slug_skips_hook_but_purges_pivots(): void + { + // Given: 문의 게시판 미설정 (명시적 null) + app(EcommerceSettingsService::class)->setSetting('inquiry.board_slug', null); + + $product = Product::factory()->create(); + $productId = $product->id; + + ProductInquiry::factory()->create(['product_id' => $productId, 'inquirable_id' => 801]); + $trashed = ProductInquiry::factory()->create(['product_id' => $productId, 'inquirable_id' => 802]); + $trashed->delete(); + + // And: 훅 호출 계수기 + HookManager::clearFilter('sirsoft-ecommerce.inquiry.delete'); + $hookCalls = 0; + HookManager::addFilter( + 'sirsoft-ecommerce.inquiry.delete', + function () use (&$hookCalls) { + $hookCalls++; + + return true; + }, + priority: 1 + ); + + // When: 상품 삭제 + $result = $this->service->delete($product); + + // Then: 훅 미호출 + 피벗 전건 영구 삭제 + 상품 삭제 성공 + $this->assertTrue($result); + $this->assertSame(0, $hookCalls, '게시판 미설정 시 inquiry.delete 훅은 호출되지 않아야 합니다.'); + $this->assertDatabaseMissing('ecommerce_product_inquiries', ['product_id' => $productId]); + $this->assertDatabaseMissing('ecommerce_products', ['id' => $productId]); + } + protected function tearDown(): void { Mockery::close(); diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDetailImageUrlTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDetailImageUrlTest.php new file mode 100644 index 00000000..16a90c25 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDetailImageUrlTest.php @@ -0,0 +1,97 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Str; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductImage; +use Modules\Sirsoft\Ecommerce\Services\ProductService; +use Modules\Sirsoft\Ecommerce\Support\AssetStorage; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 상품 상세 폼 직렬화 이미지 URL 호출측 회귀 테스트 (#100 URL 조립 단일화) + * + * accessor(download_url) 자체가 아니라 **호출측**(ProductService getDetailForForm)이 + * accessor 에 위임해 직렬화하는지 고정합니다 — 손조립 문자열로 회귀하면 행 disk + * 기준 직접 URL(혼재 운용)이 화면에서 사라집니다. + */ +class ProductServiceDetailImageUrlTest extends ModuleTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + AssetStorage::flush(); + } + + protected function tearDown(): void + { + AssetStorage::flush(); + + parent::tearDown(); + } + + /** + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function detail_form_serializes_download_url_per_row_disk(): void + { + $product = Product::create([ + 'product_code' => 'URLDELEG-'.Str::random(6), + 'name' => ['ko' => 'URL 위임 테스트 상품'], + 'selling_price' => 1000, + 'stock_quantity' => 1, + 'sales_status' => 'on_sale', + 'display_status' => 'visible', + ]); + + $localImage = ProductImage::create([ + 'product_id' => $product->id, + 'original_filename' => 'old.jpg', + 'stored_filename' => 'old.jpg', + 'disk' => 'modules', + 'path' => 'products/'.$product->id.'/old.jpg', + 'mime_type' => 'image/jpeg', + 'file_size' => 10, + 'collection' => 'main', + 'is_thumbnail' => true, + 'sort_order' => 0, + ]); + $cdnImage = ProductImage::create([ + 'product_id' => $product->id, + 'original_filename' => 'new.jpg', + 'stored_filename' => 'new.jpg', + 'disk' => 'fake_cdn', + 'path' => 'products/'.$product->id.'/new.jpg', + 'mime_type' => 'image/jpeg', + 'file_size' => 10, + 'collection' => 'main', + 'is_thumbnail' => false, + 'sort_order' => 1, + ]); + + $detail = app(ProductService::class)->getDetailForForm($product->id); + + $this->assertNotNull($detail); + $byHash = collect($detail['images'])->keyBy('hash'); + + // 혼재 운용 — 구 로컬 행은 API 폴백, 신규 원격 행은 직접 URL 로 직렬화 + $this->assertSame( + '/api/modules/sirsoft-ecommerce/product-image/'.$localImage->hash, + $byHash[$localImage->hash]['download_url'], + ); + $this->assertSame( + 'https://cdn.test/assets/sirsoft-ecommerce/images/products/'.$product->id.'/new.jpg', + $byHash[$cdnImage->hash]['download_url'], + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsCacheBootOrderTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsCacheBootOrderTest.php new file mode 100644 index 00000000..044d609e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsCacheBootOrderTest.php @@ -0,0 +1,161 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 부팅 중 만들어진 병합 결과를 요청 전체가 물고 가지 않는다 (공개 #116 파생) + * + * 설정 조회 결과에는 훅 카탈로그와 병합된 값이 섞여 있다 — 확장이 등록한 결제수단, 그리고 + * 그 수단·PG 의 생사 판정(`_orphaned` / `_orphaned_pg`)이다. + * + * 그런데 코어는 부팅 중(`CoreServiceProvider::boot`)에 config 미러를 채우려고 이 설정을 한 번 + * 읽는다. 그 시점은 플러그인이 자기 훅을 등록하기 **전**이라 카탈로그가 비어 있다. 서비스가 + * 비-싱글톤이던 때는 요청 처리 단계에서 새 인스턴스가 다시 읽어 문제가 드러나지 않았지만, + * 공유 인스턴스가 되면 그 빈 카탈로그 기준 판정이 요청 내내 남는다 — 살아 있는 PG 를 지정한 + * 결제수단이 "PG 없음" 으로 판정되어 주문서에서 통째로 사라진다. + */ +class SettingsCacheBootOrderTest extends ModuleTestCase +{ + 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); + } + } + + protected function tearDown(): void + { + $this->setBooted(true); + + if (File::isDirectory($this->storagePath)) { + File::cleanDirectory($this->storagePath); + } + + parent::tearDown(); + } + + /** + * 컨테이너의 부팅 완료 플래그를 조작합니다. (부팅 순서 재현용) + * + * @param bool $booted 부팅 완료 여부 + */ + private function setBooted(bool $booted): void + { + $prop = (new \ReflectionClass($this->app))->getProperty('booted'); + $prop->setAccessible(true); + $prop->setValue($this->app, $booted); + } + + /** + * 결제수단 카탈로그의 ID 목록을 반환합니다. + * + * @param EcommerceSettingsService $service 설정 서비스 + * @return array<int, string> 결제수단 ID 목록 + */ + private function methodIds(EcommerceSettingsService $service): array + { + $methods = $service->getSettings('order_settings')['payment_methods'] ?? []; + + return array_values(array_map(fn ($m) => $m['id'] ?? null, $methods)); + } + + /** + * 부팅 중 읽은 결과가 캐시로 굳지 않는다. (실패-먼저) + * + * @scenario pg_provider_state=live + * + * @effects boot_time_read_not_cached + */ + public function test_settings_read_during_boot_is_not_cached(): void + { + $service = app(EcommerceSettingsService::class); + + // 부팅 중 — 플러그인 훅 등록 이전 상태에서 코어가 config 미러를 채우려고 한 번 읽는다 + $this->setBooted(false); + $this->assertNotContains('plugin_pay', $this->methodIds($service)); + + // 부팅 완료 — 이제 플러그인이 자기 결제수단을 등록한 상태다 + $this->setBooted(true); + HookManager::addFilter( + 'sirsoft-ecommerce.settings.filter_available_payment_methods', + function (array $methods) { + $methods[] = [ + 'id' => 'plugin_pay', + 'name' => ['ko' => '플러그인 결제', 'en' => 'Plugin Pay'], + 'description' => ['ko' => '', 'en' => ''], + 'icon' => 'credit-card', + 'source' => 'plugin', + 'defaults' => ['needs_pg' => true, 'pg_provider' => 'plugin_pg', 'is_active' => true], + ]; + + return $methods; + } + ); + + $this->assertContains( + 'plugin_pay', + $this->methodIds($service), + '부팅 중 만들어진 카탈로그가 캐시로 굳어 확장 결제수단이 요청 내내 사라졌습니다.' + ); + } + + /** + * 살아 있는 PG 를 지정한 결제수단이 부팅 순서 때문에 고아로 오판되지 않는다. (실패-먼저) + * + * @scenario pg_provider_state=live + * + * @effects live_pg_method_remains_visible + */ + public function test_live_pg_method_is_not_flagged_due_to_boot_order(): void + { + File::ensureDirectoryExists($this->storagePath); + File::put($this->storagePath.'/order_settings.json', json_encode([ + 'payment_methods' => [ + ['id' => 'card', 'is_active' => true, 'sort_order' => 1, 'pg_provider' => 'late_pg'], + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + $service = app(EcommerceSettingsService::class); + + // 부팅 중 조회 — PG 레지스트리가 아직 비어 있다. + // 캐시 비우기도 부팅 구간 안에서 수행한다(부팅 완료 상태에서 비우면 그 시점의 + // "훅 미등록" 상태가 정상적으로 캐시되어 재현 대상이 아닌 상태가 된다). + $this->setBooted(false); + $service->clearCache(); + $service->getSettings('order_settings'); + + // 부팅 완료 후 PG 플러그인이 자기 PG 를 등록한다 + $this->setBooted(true); + HookManager::addFilter( + 'sirsoft-ecommerce.payment.registered_pg_providers', + function (array $providers) { + $providers[] = ['id' => 'late_pg', 'name' => 'Late PG']; + + return $providers; + } + ); + + $card = collect($service->getSettings('order_settings')['payment_methods'] ?? [])->firstWhere('id', 'card'); + + $this->assertFalse( + (bool) ($card['_orphaned_pg'] ?? false), + '살아 있는 PG 인데 부팅 순서 때문에 죽은 PG 로 판정되었습니다.' + ); + $this->assertContains( + 'card', + array_map(fn ($m) => $m['id'], $service->getPublicPaymentSettings()['payment_methods'] ?? []), + '정상 결제수단이 공개 응답에서 제거되었습니다.' + ); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsListDeletionPersistenceTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsListDeletionPersistenceTest.php new file mode 100644 index 00000000..fb1b3f85 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsListDeletionPersistenceTest.php @@ -0,0 +1,152 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 목록형 설정의 삭제 영속성 실측 (공개 #91 전수 조사) + * + * defaults.json 이 항목을 제공하고 관리자 화면이 그 항목의 삭제를 제공하는 목록 설정 전부에 대해, + * 삭제 저장 후 재조회에서 항목이 되살아나지 않는지 확인한다. 통화(currencies)만 code 기준 + * 보충 병합을 거치고 나머지는 통째 교체이므로 삭제가 그대로 유지되어야 하는데, 이 구분은 + * 코드를 읽어야만 드러나므로 실측으로 고정한다. + */ +class SettingsListDeletionPersistenceTest 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(); + } + + /** + * 카테고리의 목록 값을 저장하고 재조회 결과를 돌려줍니다. + * + * @param string $category 설정 카테고리 + * @param string $key 목록 키 + * @param array $list 저장할 목록 + * @return array 재조회된 목록 + */ + private function saveAndReload(string $category, string $key, array $list): array + { + $current = $this->service->getSettings($category); + $current[$key] = $list; + + $this->service->saveSettings([$category => $current]); + $this->service->clearCache(); + + return $this->service->getSettings($category)[$key] ?? []; + } + + /** + * 은행 목록에서 항목을 삭제하면 유지된다. (통째 교체) + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects non_supplemented_list_settings_keep_deletion + */ + public function test_deleted_bank_stays_deleted(): void + { + $banks = $this->service->getSettings('order_settings')['banks'] ?? []; + $this->assertGreaterThan(1, count($banks), '기본 은행 목록이 비어 있어 삭제를 측정할 수 없습니다.'); + + $kept = array_values(array_slice($banks, 0, 2)); + $reloaded = $this->saveAndReload('order_settings', 'banks', $kept); + + $this->assertCount(2, $reloaded, '삭제한 은행이 defaults 에서 되살아났습니다.'); + } + + /** + * 입금 계좌 목록에서 항목을 삭제하면 유지된다. (통째 교체) + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects non_supplemented_list_settings_keep_deletion + */ + public function test_deleted_bank_account_stays_deleted(): void + { + $accounts = $this->service->getSettings('order_settings')['bank_accounts'] ?? []; + $this->assertNotEmpty($accounts, '기본 입금 계좌가 없어 삭제를 측정할 수 없습니다.'); + + $reloaded = $this->saveAndReload('order_settings', 'bank_accounts', []); + + $this->assertSame([], $reloaded, '삭제한 입금 계좌가 defaults 에서 되살아났습니다.'); + } + + /** + * 배송 가능 국가에서 항목을 삭제하면 유지된다. (통째 교체 + 라벨 보강만) + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects non_supplemented_list_settings_keep_deletion + */ + public function test_deleted_shipping_country_stays_deleted(): void + { + $countries = $this->service->getSettings('shipping')['available_countries'] ?? []; + $this->assertGreaterThan(1, count($countries), '기본 배송 국가 목록이 비어 있어 삭제를 측정할 수 없습니다.'); + + $kept = array_values(array_slice($countries, 0, 1)); + $reloaded = $this->saveAndReload('shipping', 'available_countries', $kept); + + $this->assertCount(1, $reloaded, '삭제한 배송 국가가 defaults 에서 되살아났습니다.'); + } + + /** + * 마일리지 통화 규칙에서 항목을 삭제하면 유지된다. (통째 교체) + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects non_supplemented_list_settings_keep_deletion + */ + public function test_deleted_mileage_currency_rule_stays_deleted(): void + { + $rules = $this->service->getSettings('mileage')['currency_rules'] ?? []; + $this->assertNotEmpty($rules, '기본 마일리지 통화 규칙이 없어 삭제를 측정할 수 없습니다.'); + + $reloaded = $this->saveAndReload('mileage', 'currency_rules', []); + + $this->assertSame([], $reloaded, '삭제한 마일리지 통화 규칙이 defaults 에서 되살아났습니다.'); + } + + /** + * 결제수단은 카탈로그 병합 모델이라 저장본에서 빠져도 카탈로그에서 다시 채워진다. + * + * 삭제 UX 가 아니라 is_active 토글 모델이므로 #91 과 다른 구조다 — 그 사실을 고정한다. + * + * @scenario saved_currency_set=default_removed_by_admin, deletion_tombstone=has_removed_codes + * + * @effects catalog_merged_list_settings_refill_by_design + */ + public function test_payment_methods_are_catalog_merged_not_deletable(): void + { + $methods = $this->service->getSettings('order_settings')['payment_methods'] ?? []; + $this->assertNotEmpty($methods, '기본 결제수단 카탈로그가 비어 있습니다.'); + + $reloaded = $this->saveAndReload('order_settings', 'payment_methods', []); + + $this->assertNotEmpty($reloaded, '결제수단은 카탈로그에서 다시 채워져야 합니다(토글 모델).'); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsSaveFlushesResolversTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsSaveFlushesResolversTest.php new file mode 100644 index 00000000..6d73bf8b --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsSaveFlushesResolversTest.php @@ -0,0 +1,259 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Services; + +use Illuminate\Support\Facades\File; +use Modules\Sirsoft\Ecommerce\Services\CurrencyConversionService; +use Modules\Sirsoft\Ecommerce\Services\EcommerceSettingsService; +use Modules\Sirsoft\Ecommerce\Services\PaymentMethodResolver; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 설정 서비스 공유 인스턴스화 + 저장 시 리졸버 캐시 무효화 (공개 #116) + * + * 형제 서비스 3종(CurrencyConversionService / ShippingPolicyResolver / PaymentMethodResolver)은 + * 싱글톤으로 등록되어 있는데 `EcommerceSettingsService` 만 미등록이라, 주입 지점마다 별개 + * 인스턴스가 만들어지고 각자 자기 설정 캐시를 들고 있었다. 싱글톤 리졸버가 비-싱글톤 설정 + * 서비스를 captive 로 보유하는 비대칭도 함께 생긴다. + * + * 싱글톤화만으로는 부족하다 — 리졸버들은 자기 캐시를 따로 들고 있어서, 같은 요청 안에서 + * 설정을 저장해도 이미 해석된 리졸버는 저장 전 카탈로그를 계속 답한다. 저장 지점에서 + * (이미 해석된 경우에만) 무효화를 연동한다. + */ +class SettingsSaveFlushesResolversTest extends ModuleTestCase +{ + 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); + } + } + + protected function tearDown(): void + { + if (File::isDirectory($this->storagePath)) { + File::cleanDirectory($this->storagePath); + } + + parent::tearDown(); + } + + /** + * 카드 결제수단을 지정 상태로 만드는 저장 payload 를 만듭니다. + * + * @param bool $cardActive card 결제수단 활성 여부 + * @return array order_settings.payment_methods payload + */ + private function paymentMethods(bool $cardActive): array + { + return [ + ['id' => 'card', 'is_active' => $cardActive, 'sort_order' => 1], + ['id' => 'dbank', 'is_active' => true, 'sort_order' => 2], + ]; + } + + /** + * 설정 서비스는 컨테이너에서 공유 인스턴스로 해석된다. (실패-먼저) + * + * @scenario resolution=container + * + * @effects settings_service_shared_instance + */ + public function test_settings_service_is_resolved_as_shared_instance(): void + { + $this->assertTrue( + $this->app->bound(EcommerceSettingsService::class), + '설정 서비스가 컨테이너에 등록되어 있지 않습니다 (형제 3종은 싱글톤 등록).' + ); + + $first = $this->app->make(EcommerceSettingsService::class); + $second = $this->app->make(EcommerceSettingsService::class); + + $this->assertSame($first, $second, '설정 서비스가 해석할 때마다 새 인스턴스로 만들어집니다.'); + } + + /** + * 싱글톤 리졸버가 붙든 설정 서비스도 같은 공유 인스턴스다. + * + * @scenario resolution=captive_dependency + * + * @effects settings_service_shared_instance + */ + public function test_resolver_holds_the_same_shared_settings_instance(): void + { + $resolver = $this->app->make(PaymentMethodResolver::class); + + $reflection = new \ReflectionClass($resolver); + $prop = $reflection->getProperty('settingsService'); + $prop->setAccessible(true); + + $this->assertSame( + $this->app->make(EcommerceSettingsService::class), + $prop->getValue($resolver), + '싱글톤 리졸버가 별개의 설정 서비스 인스턴스를 붙들고 있습니다.' + ); + } + + /** + * 저장 직후 같은 요청에서 결제수단 리졸버가 신값을 답한다. (실패-먼저) + * + * @scenario resolution=container + * + * @effects payment_resolver_reflects_saved_catalog + */ + public function test_bulk_save_flushes_payment_method_resolver(): void + { + $settings = $this->app->make(EcommerceSettingsService::class); + $settings->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(false)]]); + + // 저장 전 카탈로그를 리졸버가 선점하도록 강제 + $resolver = $this->app->make(PaymentMethodResolver::class); + $this->assertFalse($this->isActive($resolver, 'card')); + + $settings->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(true)]]); + + $this->assertTrue( + $this->isActive($resolver, 'card'), + '저장 후에도 리졸버가 저장 전 카탈로그를 답했습니다 (캐시 무효화 미연동).' + ); + } + + /** + * 단건 저장(setSetting) 경로도 같은 무효화 지점을 지난다. (실패-먼저) + * + * @scenario resolution=container + * + * @effects payment_resolver_reflects_saved_catalog + */ + public function test_single_key_save_flushes_payment_method_resolver(): void + { + $settings = $this->app->make(EcommerceSettingsService::class); + $settings->setSetting('order_settings.payment_methods', $this->paymentMethods(false)); + + $resolver = $this->app->make(PaymentMethodResolver::class); + $this->assertFalse($this->isActive($resolver, 'card')); + + $settings->setSetting('order_settings.payment_methods', $this->paymentMethods(true)); + + $this->assertTrue( + $this->isActive($resolver, 'card'), + '단건 저장 후에도 리졸버가 저장 전 카탈로그를 답했습니다.' + ); + } + + /** + * 저장 직후 같은 요청에서 통화 서비스도 신값을 답한다. (실패-먼저) + * + * 결제수단 리졸버와 함께 무효화 목록에 있으나, 종전에는 "해석 가드가 있는가" 만 + * 확인하고 실제로 신값이 나오는지는 검증하지 않았다. + * + * @scenario resolution=container + * + * @effects currency_service_reflects_saved_settings + */ + public function test_bulk_save_flushes_currency_conversion_service(): void + { + $settings = $this->app->make(EcommerceSettingsService::class); + $settings->saveSettings(['language_currency' => $this->currencySettings('KRW')]); + + // 저장 전 설정을 통화 서비스가 선점하도록 강제 + $currency = $this->app->make(CurrencyConversionService::class); + $this->assertSame('KRW', $currency->getDefaultCurrency()); + + $settings->saveSettings(['language_currency' => $this->currencySettings('USD')]); + + $this->assertSame( + 'USD', + $currency->getDefaultCurrency(), + '저장 후에도 통화 서비스가 저장 전 기본 통화를 답했습니다 (캐시 무효화 미연동).' + ); + } + + /** + * 단건 저장 경로도 통화 서비스 캐시를 비운다. (실패-먼저) + * + * @scenario resolution=container + * + * @effects currency_service_reflects_saved_settings + */ + public function test_single_key_save_flushes_currency_conversion_service(): void + { + $settings = $this->app->make(EcommerceSettingsService::class); + $settings->saveSettings(['language_currency' => $this->currencySettings('KRW')]); + + $currency = $this->app->make(CurrencyConversionService::class); + $this->assertSame('KRW', $currency->getDefaultCurrency()); + + $settings->setSetting('language_currency.default_currency', 'USD'); + + $this->assertSame( + 'USD', + $currency->getDefaultCurrency(), + '단건 저장 후에도 통화 서비스가 저장 전 기본 통화를 답했습니다.' + ); + } + + /** + * 지정한 기본 통화로 language_currency payload 를 만듭니다. + * + * @param string $defaultCurrency 기본 통화 코드 + * @return array language_currency payload + */ + private function currencySettings(string $defaultCurrency): array + { + return [ + 'default_currency' => $defaultCurrency, + 'currencies' => [ + ['code' => 'KRW', 'symbol' => '₩', 'decimal_places' => 0, 'exchange_rate' => 1, 'is_active' => true, 'is_default' => $defaultCurrency === 'KRW'], + ['code' => 'USD', 'symbol' => '$', 'decimal_places' => 2, 'exchange_rate' => 0.00075, 'is_active' => true, 'is_default' => $defaultCurrency === 'USD'], + ], + ]; + } + + /** + * 아직 해석되지 않은 리졸버는 저장이 강제로 인스턴스화하지 않는다. + * + * @scenario resolution=not_resolved + * + * @effects unresolved_services_not_instantiated_on_save + */ + public function test_save_does_not_instantiate_unresolved_resolvers(): void + { + $this->app->forgetInstance(PaymentMethodResolver::class); + $this->app->forgetInstance(CurrencyConversionService::class); + + $this->app->make(EcommerceSettingsService::class) + ->saveSettings(['order_settings' => ['payment_methods' => $this->paymentMethods(true)]]); + + $this->assertFalse( + $this->app->resolved(PaymentMethodResolver::class), + '저장이 미해석 리졸버를 강제로 인스턴스화했습니다.' + ); + $this->assertFalse( + $this->app->resolved(CurrencyConversionService::class), + '저장이 미해석 통화 서비스를 강제로 인스턴스화했습니다.' + ); + } + + /** + * 리졸버가 보는 카탈로그에서 결제수단의 활성 여부를 읽습니다. + * + * @param PaymentMethodResolver $resolver 결제수단 리졸버 + * @param string $methodId 결제수단 ID + * @return bool 활성 여부 + */ + private function isActive(PaymentMethodResolver $resolver, string $methodId): bool + { + $reflection = new \ReflectionClass($resolver); + $method = $reflection->getMethod('catalogValue'); + $method->setAccessible(true); + + return (bool) $method->invoke($resolver, $methodId, 'is_active'); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/StorageDiskRecursionTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/StorageDiskRecursionTest.php new file mode 100644 index 00000000..2c89671b --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/StorageDiskRecursionTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit; + +use App\Extension\ModuleManager; +use App\Services\ModuleSettingsService; +use Illuminate\Support\Facades\Config; +use Modules\Sirsoft\Ecommerce\Module; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * getStorageDiskFor 설정 로드 순환 참조 회귀 테스트 (실제 설정 로드 경로) + * + * 실제 모듈 인스턴스의 getStorageDiskFor 가 module_setting() 을 경유할 때, + * 설정 로드 자신이 스토리지('settings' 카테고리)를 다시 요구하는 상황을 + * 컨테이너 스파이로 재현합니다. PUBLIC_ASSET_CATEGORIES 게이트가 제거되면 + * 'settings' 해석이 설정 서비스를 재호출해 호출 횟수 단언이 깨집니다. + */ +class StorageDiskRecursionTest extends ModuleTestCase +{ + private Module $module; + + /** + * 설정 로드가 스토리지를 경유하는 경로를 재현하는 스파이 + */ + private object $settingsSpy; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + Config::set('core.storage.public_asset_disk', ''); + + $this->module = app(ModuleManager::class)->getModule('sirsoft-ecommerce') + ?? new Module(base_path('modules/sirsoft-ecommerce')); + + // 실제 설정 로드처럼 get() 내부에서 모듈 스토리지('settings')를 재요구하는 스파이. + // 재진입(= getStorageDiskFor('settings') 가 다시 설정을 조회하는 회귀) 발생 시 + // reentered 플래그 + 호출 횟수 증가로 검출됩니다. + $this->settingsSpy = new class extends ModuleSettingsService + { + public ?Module $module = null; + + public int $calls = 0; + + public bool $reentered = false; + + public ?string $nestedDisk = null; + + private bool $inGet = false; + + public function __construct() {} + + public function get(string $identifier, ?string $key = null, mixed $default = null): mixed + { + $this->calls++; + + if ($this->inGet) { + // 설정 로드 중 설정 재조회 = 순환 고리. 무한 재귀 대신 폭로. + $this->reentered = true; + + return $default; + } + + $this->inGet = true; + + try { + // 설정 로드가 스토리지를 경유하는 실제 경로 재현 — + // 이 호출이 getStorageDiskFor('settings') 를 타고 되돌아온다. + $this->nestedDisk = $this->module->getStorageFor('settings')->getDisk(); + + if ($key === 'basic_info.public_asset_disk') { + return 'fake_cdn'; + } + + return $default; + } finally { + $this->inGet = false; + } + } + }; + $this->settingsSpy->module = $this->module; + $this->app->instance(ModuleSettingsService::class, $this->settingsSpy); + } + + protected function tearDown(): void + { + $this->app->forgetInstance(ModuleSettingsService::class); + + parent::tearDown(); + } + + /** + * @effects settings_load_recursion_free + */ + #[Test] + public function settings_category_never_consults_module_settings(): void + { + // 'settings' 카테고리 해석은 설정 서비스를 한 번도 거치지 않아야 한다 — + // 거치는 순간 설정 로드 ↔ 디스크 해석의 순환 고리가 열린다. + $this->assertSame($this->module->getStorageDisk(), $this->module->getStorageDiskFor('settings')); + $this->assertSame(0, $this->settingsSpy->calls); + } + + /** + * @effects settings_load_recursion_free + */ + #[Test] + public function images_resolution_survives_settings_load_that_uses_storage(): void + { + // 공개 자산 카테고리 해석 → 설정 조회 → (설정 로드가 'settings' 스토리지 재요구) + // 전체 사슬이 유한 종료하고, 설정 조회는 정확히 1회만 발생해야 한다. + $this->assertSame('fake_cdn', $this->module->getStorageDiskFor('images')); + + $this->assertSame(1, $this->settingsSpy->calls); + $this->assertFalse($this->settingsSpy->reentered, '설정 로드 중 설정 재조회(순환) 발생'); + // 설정 로드가 요구한 'settings' 스토리지는 공개 자산 디스크가 아닌 기본 디스크 + $this->assertSame($this->module->getStorageDisk(), $this->settingsSpy->nestedDisk); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/Unit/Upgrades/InquiryOrphanReplyBackfillTest.php b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Upgrades/InquiryOrphanReplyBackfillTest.php new file mode 100644 index 00000000..9469e19b --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/Unit/Upgrades/InquiryOrphanReplyBackfillTest.php @@ -0,0 +1,253 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Tests\Unit\Upgrades; + +use App\Extension\UpgradeContext; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Product; +use Modules\Sirsoft\Ecommerce\Models\ProductInquiry; +use Modules\Sirsoft\Ecommerce\Tests\ModuleTestCase; + +/** + * 1.1.1 문의 정합화 데이터 마이그레이션(04/05/06) 회귀 테스트 + * + * - 04 SoftDeleteOrphanInquiryReplyPosts: 부모(질문)가 부재/삭제된 고아 답변 글 소프트 삭제 + * - 05 SoftDeleteInquiryPivotsForTrashedQuestionPosts: 질문 Post 가 부재/삭제인데 살아있는 피벗 소프트 삭제 + * - 06 UnmarkAnsweredInquiriesWithoutLiveReplies: 살아있는 답변이 없는 「답변완료」 피벗 정정 + * + * 문의 게시판 판별(04)은 설정 파일 축이 아닌 「피벗 join 실데이터」 축으로 검증한다 — + * 테스트 DB 의 boards 테이블에는 설정 slug 에 대응하는 행이 없으므로 설정 축은 자연 무해하다. + */ +class InquiryOrphanReplyBackfillTest extends ModuleTestCase +{ + private const POST_MODEL = 'Modules\\Sirsoft\\Board\\Models\\Post'; + + private const BOARD_ID = 7; + + private object $orphanReplyStep; + + private object $trashedQuestionPivotStep; + + private object $unmarkAnsweredStep; + + private UpgradeContext $context; + + private Product $product; + + protected function setUp(): void + { + parent::setUp(); + + $migrationsDir = dirname(__DIR__, 3).'/upgrades/data/1.1.1/migrations'; + + require_once $migrationsDir.'/04_SoftDeleteOrphanInquiryReplyPosts.php'; + require_once $migrationsDir.'/05_SoftDeleteInquiryPivotsForTrashedQuestionPosts.php'; + require_once $migrationsDir.'/06_UnmarkAnsweredInquiriesWithoutLiveReplies.php'; + + $namespace = 'App\\Upgrades\\Data\\Ext\\Modules\\SirsoftEcommerce\\V1_1_1\\Migrations\\'; + $orphanClass = $namespace.'SoftDeleteOrphanInquiryReplyPosts'; + $pivotClass = $namespace.'SoftDeleteInquiryPivotsForTrashedQuestionPosts'; + $unmarkClass = $namespace.'UnmarkAnsweredInquiriesWithoutLiveReplies'; + + $this->orphanReplyStep = new $orphanClass; + $this->trashedQuestionPivotStep = new $pivotClass; + $this->unmarkAnsweredStep = new $unmarkClass; + + $this->context = new UpgradeContext( + fromVersion: '1.1.0', + toVersion: '1.1.1', + currentStep: '1.1.1', + logChannel: 'extension-upgrade', + ); + + $this->product = Product::factory()->create(); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); + } + + /** + * 게시글 행을 생성합니다 (board_posts 직접 시드 — V-1 안전 스텝과 동일하게 raw DB). + * + * @param array $overrides 컬럼 오버라이드 + * @return int 생성된 게시글 ID + */ + private function makeBoardPost(array $overrides = []): int + { + return (int) DB::table('board_posts')->insertGetId(array_merge([ + 'board_id' => self::BOARD_ID, + 'title' => '상품 문의 글', + 'content' => '상품 문의 내용입니다.', + 'ip_address' => '127.0.0.1', + 'status' => 'published', + 'created_at' => now(), + 'updated_at' => now(), + ], $overrides)); + } + + /** + * 문의 피벗 행을 생성합니다 (inquirable_type 은 게시판 Post FQCN 고정). + * + * @param int $postId 질문 Post ID + * @param array $overrides 컬럼 오버라이드 + * @return ProductInquiry 생성된 피벗 + */ + private function makePivot(int $postId, array $overrides = []): ProductInquiry + { + return ProductInquiry::factory()->create(array_merge([ + 'product_id' => $this->product->id, + 'inquirable_type' => self::POST_MODEL, + 'inquirable_id' => $postId, + ], $overrides)); + } + + /** + * 04 — 부모가 삭제/부재인 답변 글만 소프트 삭제하고, 부모가 살아있는 답변은 건드리지 않는다. + */ + public function test_04_soft_deletes_only_orphan_reply_posts(): void + { + // Given: 살아있는 질문 + 그 답변 (오탐 검증 대상) + $aliveQuestion = $this->makeBoardPost(); + $aliveReply = $this->makeBoardPost(['parent_id' => $aliveQuestion, 'depth' => 1]); + + // And: 소프트 삭제된 질문 + 잔존 답변 (고아 ①) + $trashedQuestion = $this->makeBoardPost([ + 'status' => 'deleted', + 'deleted_at' => now()->subDay(), + ]); + $orphanByTrashedParent = $this->makeBoardPost(['parent_id' => $trashedQuestion, 'depth' => 1]); + + // And: 부모가 하드 삭제로 부재한 답변 (고아 ②) + $orphanByMissingParent = $this->makeBoardPost(['parent_id' => 999999, 'depth' => 1]); + + // And: 문의 게시판 판별용 피벗 (피벗 join posts.board_id 축) + $this->makePivot($aliveQuestion); + + // When: 04 실행 + $this->orphanReplyStep->run($this->context); + + // Then: 고아 답변 2건만 status=deleted + deleted_at 마킹 + foreach ([$orphanByTrashedParent, $orphanByMissingParent] as $orphanId) { + $row = DB::table('board_posts')->where('id', $orphanId)->first(); + $this->assertSame('deleted', $row->status, "고아 답변({$orphanId})은 deleted 상태여야 합니다."); + $this->assertNotNull($row->deleted_at, "고아 답변({$orphanId})은 소프트 삭제되어야 합니다."); + } + + // And: 부모가 살아있는 답변과 질문 글은 오탐 없이 그대로 + foreach ([$aliveQuestion, $aliveReply] as $aliveId) { + $row = DB::table('board_posts')->where('id', $aliveId)->first(); + $this->assertSame('published', $row->status, "살아있는 글({$aliveId})은 건드리면 안 됩니다."); + $this->assertNull($row->deleted_at); + } + } + + /** + * 04 — 재실행 멱등: 이미 마킹된 고아의 deleted_at 이 갱신되지 않는다. + */ + public function test_04_is_idempotent_on_rerun(): void + { + // Given: 고아 답변 1건 (부모 부재) + 판별용 피벗 + $question = $this->makeBoardPost(); + $orphan = $this->makeBoardPost(['parent_id' => 999999, 'depth' => 1]); + $this->makePivot($question); + + // When: 1차 실행 (T1 시각) + $firstRunAt = Carbon::parse('2026-08-17 10:00:00'); + Carbon::setTestNow($firstRunAt); + $this->orphanReplyStep->run($this->context); + + $afterFirst = DB::table('board_posts')->where('id', $orphan)->value('deleted_at'); + $this->assertNotNull($afterFirst); + + // When: 시간이 흐른 뒤 2차 실행 (T2 시각) + Carbon::setTestNow($firstRunAt->copy()->addHour()); + $this->orphanReplyStep->run($this->context); + + // Then: deleted_at IS NULL 필터로 재실행이 0건 — 1차 마킹 시각 그대로 + $this->assertSame( + $afterFirst, + DB::table('board_posts')->where('id', $orphan)->value('deleted_at'), + '재실행이 기존 소프트 삭제 시각을 덮어쓰면 안 됩니다 (멱등).' + ); + } + + /** + * 05 — 질문 Post 가 삭제/부재인 살아있는 피벗만 소프트 삭제한다. + */ + public function test_05_soft_deletes_pivots_whose_question_post_is_gone(): void + { + // Given: 살아있는 질문 + 그 피벗 (생존 대상) + $aliveQuestion = $this->makeBoardPost(); + $alivePivot = $this->makePivot($aliveQuestion); + + // And: 소프트 삭제된 질문 + 살아있는 피벗 (정리 대상 ①) + $trashedQuestion = $this->makeBoardPost([ + 'status' => 'deleted', + 'deleted_at' => now()->subDay(), + ]); + $pivotForTrashed = $this->makePivot($trashedQuestion); + + // And: 하드 삭제로 부재한 질문을 가리키는 피벗 (정리 대상 ②) + $pivotForMissing = $this->makePivot(888888); + + // When: 05 실행 + $this->trashedQuestionPivotStep->run($this->context); + + // Then: 질문이 사라진 피벗 2건만 소프트 삭제 + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivotForTrashed->id]); + $this->assertSoftDeleted('ecommerce_product_inquiries', ['id' => $pivotForMissing->id]); + + // And: 질문이 살아있는 피벗은 그대로 + $this->assertNull( + ProductInquiry::withTrashed()->find($alivePivot->id)->deleted_at, + '질문 Post 가 살아있는 피벗은 건드리면 안 됩니다.' + ); + } + + /** + * 06 — 살아있는 답변이 없는 「답변완료」 피벗만 미답변으로 정정한다. + */ + public function test_06_unmarks_answered_inquiries_without_live_replies(): void + { + // Given: 답변이 소프트 삭제된 질문 + 답변완료 피벗 (정정 대상) + $questionWithoutReply = $this->makeBoardPost(); + $this->makeBoardPost([ + 'parent_id' => $questionWithoutReply, + 'depth' => 1, + 'status' => 'deleted', + 'deleted_at' => now()->subDay(), + ]); + $staleAnswered = $this->makePivot($questionWithoutReply, [ + 'is_answered' => true, + 'answered_at' => now()->subDays(2), + ]); + + // And: 살아있는 답변이 있는 질문 + 답변완료 피벗 (유지 대상) + $questionWithReply = $this->makeBoardPost(); + $this->makeBoardPost(['parent_id' => $questionWithReply, 'depth' => 1]); + $validAnswered = $this->makePivot($questionWithReply, [ + 'is_answered' => true, + 'answered_at' => now()->subDays(2), + ]); + + // When: 06 실행 + $this->unmarkAnsweredStep->run($this->context); + + // Then: 답변 없는 답변완료만 해제 (is_answered=0 + answered_at=null) + $this->assertDatabaseHas('ecommerce_product_inquiries', [ + 'id' => $staleAnswered->id, + 'is_answered' => false, + 'answered_at' => null, + ]); + + // And: 살아있는 답변이 있는 피벗은 답변완료 유지 + $kept = ProductInquiry::find($validAnswered->id); + $this->assertTrue($kept->is_answered, '살아있는 답변이 있으면 답변완료를 유지해야 합니다.'); + $this->assertNotNull($kept->answered_at); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-crud-exception-status.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-crud-exception-status.yaml new file mode 100644 index 00000000..7c737dff --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-crud-exception-status.yaml @@ -0,0 +1,84 @@ +feature: 관리자 CRUD 의 예외 → 상태코드 매핑 (Admin CRUD Exception Status) + +description: | + 브랜드·쿠폰·배송사·상품 공통정보의 관리자 CRUD 가 실패했을 때, 그 원인이 + "운영자가 고칠 수 있는 도메인 사유" 인지 "서버 결함·인프라 장애" 인지를 + 응답 상태코드로 구분한다. + + 배경: + 이 확장의 관리자 CRUD 는 `ResponseHelper::moduleError` 의 기본 상태코드가 400 이라, + 광역 catch 에서 상태코드 인자를 생략하거나 400 을 그대로 두면 인프라 장애가 + "입력 오류" 로 위장된다. 운영자는 고칠 수 없는 안내를 보고, 장애는 늦게 발견된다. + + 핵심 설계: + - typed 도메인 예외 → **기존 400 유지** (예외 승격이 사용자 계약을 함께 바꾸면 그것이 회귀다) + - 그 외 예외 → 500, 응답 본문에 예외 원문(SQLSTATE 등) 미포함 + + 소스 전수 계약(`tests/Feature/Http/GenericCatchStatusCodeContractTest.php`)은 + "광역 catch 에 500 이라고 적혀 있다" 까지만 보증한다. 그 앞의 FormRequest·미들웨어가 + 다른 응답을 만들 수 있으므로, 이 매니페스트가 담당하는 축은 **계약이 실제 HTTP 응답까지 + 도달하는가** 이고 대표 엔드포인트의 런타임 매핑으로 확인한다. + + 인프라 500 축은 서비스 mock 으로만 유발할 수 있어 PHPUnit 이 SSoT 다. + 브라우저에서는 도메인 사유나 검증이 먼저 걸려 재현되지 않는다. + +axes: + error_class: [domain, infrastructure] + +exclusions: [] + +effects: + # 도메인 예외 — 기존 400 을 유지한다 + - admin_brand_delete_domain_exception_keeps_400 + - admin_brand_toggle_status_domain_exception_keeps_400 + - admin_coupon_delete_domain_exception_keeps_400 + - admin_shipping_carrier_toggle_status_domain_exception_keeps_400 + - admin_product_common_info_delete_domain_exception_keeps_400 + - admin_product_common_info_toggle_active_domain_exception_keeps_400 + # 인프라 예외 — 500 으로 구분된다 + - admin_brand_delete_infrastructure_exception_returns_500 + - admin_brand_toggle_status_infrastructure_exception_returns_500 + - admin_coupon_delete_infrastructure_exception_returns_500 + - admin_shipping_carrier_toggle_status_infrastructure_exception_returns_500 + - admin_product_common_info_delete_infrastructure_exception_returns_500 + - admin_product_common_info_toggle_active_infrastructure_exception_returns_500 + # 공통 — 응답 본문에 예외 원문이 실리지 않는다 + - admin_crud_infrastructure_response_omits_exception_text + +scenarios: + - id: admin_crud_domain_failure_keeps_original_status + description: 도메인 사유로 실패하면 그 엔드포인트가 원래 쓰던 400 이 그대로 전달된다 (예외 typed 승격이 계약을 바꾸지 않았음) + effects: + - admin_brand_delete_domain_exception_keeps_400 + - admin_brand_toggle_status_domain_exception_keeps_400 + - admin_coupon_delete_domain_exception_keeps_400 + - admin_shipping_carrier_toggle_status_domain_exception_keeps_400 + - admin_product_common_info_delete_domain_exception_keeps_400 + - admin_product_common_info_toggle_active_domain_exception_keeps_400 + + - id: admin_crud_infrastructure_failure_is_not_disguised_as_input_error + description: DB 장애 등 인프라 예외는 500 으로 구분되고 응답 본문에 예외 원문(SQLSTATE 등)이 포함되지 않는다 + effects: + - admin_brand_delete_infrastructure_exception_returns_500 + - admin_brand_toggle_status_infrastructure_exception_returns_500 + - admin_coupon_delete_infrastructure_exception_returns_500 + - admin_shipping_carrier_toggle_status_infrastructure_exception_returns_500 + - admin_product_common_info_delete_infrastructure_exception_returns_500 + - admin_product_common_info_toggle_active_infrastructure_exception_returns_500 + - admin_crud_infrastructure_response_omits_exception_text + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ExceptionStatusMappingTest.php + +notes: | + **엔드포인트를 axis 로 두지 않은 이유**: 대상 테스트는 DataProvider 로 6개 엔드포인트를 + 한 메서드가 쓸어 담는다. `@scenario` 마커는 도크블록당 1개이고 축마다 값 1개만 받으므로, + 엔드포인트를 axis 로 세우면 cross product 6×2=12 칸 중 2칸만 마킹되어 나머지 10칸이 + 영구 미커버로 남는다. 실제로 판정을 가르는 축은 `error_class` 하나이고, + 엔드포인트별 커버리지는 effects 12건이 1:1로 담는다. + + 이 매니페스트는 5차 감사에서 신설됐다. `OrderDomainExceptionStatusPairTest` 와 + `InquiryDomainExceptionStatusPairTest` 는 3차 감사에서 각각 전용 매니페스트를 받았는데, + 같은 라운드에 만들어진 이 파일만 `@scenario`/`@effects` 가 하나도 없고 어느 매니페스트 + `test_files` 에도 등록되지 않은 채였다. effect 선언이 아예 없으면 test-scenario-coverage + 의 두 방향 검사(선언→테스트, 테스트→선언) 어느 쪽에도 걸리지 않아 조용히 통과한다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-order-detail-footer-totals.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-order-detail-footer-totals.yaml new file mode 100644 index 00000000..a8390666 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/admin-order-detail-footer-totals.yaml @@ -0,0 +1,37 @@ +feature: 관리자 주문상세 — 합계행(tfoot) 텍스트 바인딩 해석 (공개 #98 stale dist 회귀) + +description: | + 관리자 주문 상세(`admin/ecommerce/orders/:orderNumber`) 상품 목록 표의 합계행은 + DataGrid `footerCells` 로 렌더된다. `components.json` 의 `skipBindingKeys` 선언으로 + 엔진 선평가가 중단된 뒤에는 DataGrid `renderFooterCellText` 가 `text` 바인딩을 + 해석해야 한다. admin_basic 1.0.4 는 stale dist(구형 JS) 가 릴리즈에 실려 + 수량·적립예정·배송비 셀에 `{{order.data?.total_quantity ?? 0}}개` 같은 표현식 + 원문이 노출됐다(공개 #98). 소스와 단위 테스트는 green 인 채 서빙 산출물만 구형인 + 결함이므로, 서빙 번들을 실제 로드하는 브라우저 E2E 를 회귀 방벽으로 둔다. + + axes 는 audit fallback YAML 파서 호환을 위해 flat 인라인 배열로만 선언한다. + 모바일 카드 푸터는 `footerCardChildren`(컴포넌트 children 경로) 라 이 결함의 + 영향 밖이며(실측), Chrome MCP 검수 매트릭스 T8 축이 별도로 확인한다. + +axes: + footer_cell: [quantity, points, delivery] + viewport: [desktop] + +effects: + # 브라우저 E2E (Playwright — 서빙 번들 실검) + - footer_text_bindings_resolved_no_raw_expression # 페이지 전체 `{{` 0건 + - footer_quantity_cell_shows_count_with_unit # 수량 셀 `[\d,]+개` 형태 + - footer_points_cell_shows_formatted_number # 적립 셀 `[\d,]+P` 형태 (`| number` 파이프) + - footer_delivery_cell_shows_shipping_fee_label # 배송비 셀 라벨: 값 형태 + 원문 부재 + # 컴포넌트 단위 (Vitest — admin_basic DataGrid) + - datagrid_footer_cell_text_binding_resolved # footerCells[].text 바인딩/파이프 해석 + # 레이아웃 구조 (Vitest — 이커머스 주문상세 레이아웃 JSON) + - order_detail_footer_totals_fields_bound # 합계행이 order.data 백엔드 필드 사용 + +test_files: + - templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/orderDetailLayouts.test.tsx + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-detail-footer-totals.spec.ts + +validation: + audit_rule: frontend-change-requires-e2e diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-dead-provider.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-dead-provider.yaml new file mode 100644 index 00000000..dfb39ab3 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-dead-provider.yaml @@ -0,0 +1,40 @@ +feature: 죽은 현금영수증 발급사 차단 (A3) + +description: | + 발급사(`order_settings.cash_receipt_provider`)는 플러그인이 훅으로 등록한 카탈로그에서 고른다. + 운영자가 고른 뒤 그 플러그인을 제거하면 저장값만 남는다 — 카탈로그에는 없는 문자열이다. + + 그 상태에서 발급을 허용하면 요청이 어디에도 도달하지 못한다. 구매자 화면에는 신청 폼이 + 그대로 뜨고, 발급을 눌러도 실패 이력만 쌓인다. 예외는 나지 않는다. + + 판정은 하나다 — 저장값이 현재 레지스트리에 없으면 **미설정과 동일하게** 취급한다. + 단, 저장값 자체는 지우지 않는다. 관리자 응답에는 남겨 운영자가 무엇을 골랐었는지 보고 + 고칠 수 있게 하고, 공개 응답·리소스에서만 null 로 정규화한다. + + 이미 발급된 영수증은 별개다. 취소는 현재 설정값이 아니라 **영수증에 박제된 발급사**로 + 수행한다 — 그러지 않으면 플러그인을 제거한 뒤 과거 영수증을 취소할 수 없게 되어, + 구매자는 환불받았는데 현금영수증만 살아 있는 상태가 남는다. + +axis_notes: + provider_state: | + surface(관리자 설정 / 공개 응답 / 주문 리소스 / 발급 서비스)는 축이 아니라 effects 로 + 구분한다 — 네 표면이 같은 판정을 공유하는 것이 이 기능의 요구사항이기 때문이다. + 죽은 PG 를 다루는 형제 매니페스트(payment-method-dead-pg.yaml)와 같은 구조다. + +axes: + provider_state: [live, dead] + +effects: + - live_cash_receipt_provider_resolved # 등록된 발급사는 그대로 해석 (거짓 양성 차단) + - dead_cash_receipt_provider_treated_as_unset # 해석 결과가 null (저장값은 관리자 확인용으로 보존) + - dead_cash_receipt_provider_normalized_in_public # 공개 응답에서 null → 체크아웃 신청 폼 미렌더 + - dead_provider_nulled_in_resource # 주문 리소스의 발급사 필드도 null → 발급 버튼 미노출 + - existing_receipt_history_survives_dead_provider # 이미 발급/시도된 이력은 계속 표시 (영수증 URL 도달 경로 보존) + - issue_blocked_when_provider_unregistered # 신규 발급은 훅 호출 없이 차단 (실패 이력만 쌓이는 것 방지) + - cancel_uses_receipt_snapshot_provider # 취소는 영수증에 박제된 발급사로 수행 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCashReceiptTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Resources/CashReceiptResourceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CashReceiptServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/shop/cash-receipt-lifecycle.spec.ts diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml index f9b09fa8..487cc61a 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/cash-receipt-ui-and-refund-bank.yaml @@ -57,7 +57,8 @@ axes: - free provider_state: # 발급 프로바이더 설정 상태 - - configured # 슬롯/카드 렌더 + - configured # 설정값이 있고 그 프로바이더를 제공하는 확장도 살아 있음 → 슬롯/카드 렌더 + - dead # 설정값은 남았으나 제공 확장이 사라짐 → 미설정과 동일 취급 (A3) - not_configured # 슬롯/카드 전체 미렌더 (DOM 카운트 0) receipt_type: @@ -157,6 +158,10 @@ effects: - checkout_refund_bank_empty_allowed - checkout_refund_bank_name_resolved_from_code # 은행코드 → 은행명 조회 저장 + # 죽은 프로바이더(A3)의 판정·응답 계약은 cash-receipt-dead-provider.yaml 이 소유한다. + # 이 매니페스트는 cross product 상한 초과로 effects 검사가 면제되어 있어, 그곳에 두면 + # 검증되지 않는다. provider_state=dead 축은 UI 렌더 조건으로서 여기 남는다. + # 유저 주문상세 카드 (W-2) - mypage_card_hidden_when_not_dbank_or_no_provider - mypage_card_state_machine_five_states diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-resolver-flush.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-resolver-flush.yaml new file mode 100644 index 00000000..67539c87 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-resolver-flush.yaml @@ -0,0 +1,35 @@ +feature: 설정 저장이 리졸버 캐시에 도달하는 경로 (B-6) + +description: | + 설정을 저장한 뒤 같은 요청 안에서 그 값을 읽는 코드가 있다. 그런데 SettingsService 가 + 컨테이너에 singleton 으로 묶여 있지 않으면, 저장한 인스턴스와 읽는 인스턴스가 서로 다른 + 객체가 된다 — 저장은 성공하고, 바로 뒤의 조회만 옛 값을 돌려준다. + + 더 조용한 형태가 captive dependency 다. 생성자에서 SettingsService 를 받아 둔 서비스는 + 컨테이너를 다시 거치지 않으므로, singleton 으로 바꾼 뒤에도 그 서비스만 자기 사본을 + 붙들고 있을 수 있다. 그래서 "컨테이너에서 두 번 꺼내 같은 객체인가" 와 "생성자로 받아 둔 + 객체도 같은가" 를 따로 본다. + + 반대 방향의 위험도 있다. 저장 시점에 리졸버를 무조건 깨우면(`app(X)->flush()`), 그 요청에서 + 쓰지도 않을 서비스가 인스턴스화되어 부팅 비용이 늘고 부작용이 생긴다. 아직 해석되지 않은 + 서비스는 건드리지 않아야 한다. + +axis_notes: + resolution: | + 저장 시점에 대상 서비스가 어떤 상태였는가. container = 컨테이너에서 해석된 상태. + captive_dependency = 다른 서비스의 생성자에 붙들린 사본. not_resolved = 아직 해석되지 않음 + (건드리지 않아야 하는 케이스). + 저장 경로(벌크/단건)와 대상 서비스(결제수단/통화)는 조합이 아니라 같은 계약의 반복이라 + effects 로 판정한다 — 축으로 두면 전개만 늘고 검증 내용은 같아진다. + +axes: + resolution: [container, captive_dependency, not_resolved] + +effects: + - settings_service_shared_instance # 컨테이너 해석분과 주입된 사본이 동일 객체 + - payment_resolver_reflects_saved_catalog # 저장 직후 결제수단 리졸버가 새 카탈로그를 본다 + - currency_service_reflects_saved_settings # 저장 직후 통화 서비스가 새 설정을 본다 + - unresolved_services_not_instantiated_on_save # 해석 안 된 서비스는 저장이 깨우지 않는다 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsSaveFlushesResolversTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-save-hook.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-save-hook.yaml new file mode 100644 index 00000000..3765bfbc --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-save-hook.yaml @@ -0,0 +1,37 @@ +feature: 모듈 설정 저장 훅의 발화 지점 (B-5) + +description: | + `core.module_settings.after_save` 를 구독하는 리스너가 셋(이커머스 SEO 캐시 / 게시판 SEO + 캐시 / 코어 활동로그) 있는데, 정작 발화 지점인 `ModuleSettingsService::save()` 는 + 프로덕션에서 호출되지 않았다. 모듈 설정은 각 모듈의 관리자 컨트롤러가 자기 + SettingsService 로 직접 저장하기 때문이다. + + 결과: 모듈 환경설정에서 SEO 메타 템플릿을 바꿔도 SEO 캐시가 무효화되지 않고, 설정 변경이 + 감사 기록에도 남지 않았다. 예외도 경고도 없이 구독자만 굶었다. + + 발화 지점은 관리자 컨트롤러다. 서비스에 두면 시더·업그레이드 스텝·테스트 픽스처의 모든 + 저장이 활동로그와 SEO 무효화를 유발하고, 훅의 의미도 "관리자가 설정을 저장했다" 이다. + +axis_notes: + actor: | + admin = 관리자 HTTP 요청(컨트롤러 경유). system = 서비스 직접 호출(시더·내부 로직). + 이 축이 곧 발화 여부를 가르는 설계 결정이라, 두 값 모두에 대해 저장 경로 전부를 본다 — + 한쪽 경로만 조용하면 나머지 경로로 감사 기록이 오염된다. + save_path: | + single_key 는 아직 라우트가 없다(설정 화면은 벌크 저장만 사용). 컨트롤러 메서드를 직접 + 호출해 계약을 고정한다 — 라우트가 붙는 시점에 발화 계약이 이미 서 있게 한다. + +axes: + actor: [admin, system] + save_path: [bulk, single_key, save_banks] + +effects: + - module_settings_after_save_hook_fired # 관리자 저장 → 발화 (저장 카테고리 payload 동반) + - module_settings_after_save_hook_not_fired_on_failure # 검증 탈락 시 미발화 + - service_level_save_stays_silent # 서비스 직접 호출은 전 경로에서 침묵 + - seo_cache_invalidated_on_module_settings_save # 구독자 ① SEO 프리렌더 캐시 + - seo_cache_untouched_for_non_seo_tab # SEO 무관 탭 저장은 캐시를 건드리지 않는다 + - module_settings_save_recorded_in_activity_log # 구독자 ② 코어 활동로그 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsSeoCacheInvalidationTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-single-key-pipeline.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-single-key-pipeline.yaml new file mode 100644 index 00000000..6d13e144 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/ecommerce-settings-single-key-pipeline.yaml @@ -0,0 +1,42 @@ +feature: 이커머스 단건 설정 저장의 파이프라인 통과 (B-1) + +description: | + 이커머스 설정은 저장 전에 여러 단계를 거친다 — 분할 입력 필드 병합, 통화 기본값 동기화, + 결제수단 메타데이터 스냅샷, 은행명 다국어 정규화. 벌크 저장(`saveSettings`)은 그 파이프라인을 + 전부 통과하는데, 단건 저장(`setSetting`)은 값 하나를 파일에 바로 써 넣었다. + + 그래서 같은 값을 어느 화면에서 저장했느냐에 따라 결과가 달라졌다. 오류는 나지 않는다 — + 저장은 성공하고, 파생 값만 갱신되지 않은 채 남는다. + + 단건 저장이 딛는 기준(payload_basis)도 문제였다. 조회 결과에는 읽기 시점 보강값(카탈로그 + 라벨 등)이 섞여 있고, 삭제된 통화의 tombstone 은 조회에서 걸러진다. 조회 결과를 기준으로 + 덮어쓰면 보강값이 파일에 눌어붙고 tombstone 이 사라진다 — 저장된 파일이 기준이어야 한다. + +axis_notes: + pipeline_stage: | + 벌크 경로가 이미 수행하던 단계를 단건 경로에도 통과시킨다. 각 단계는 서로 다른 입력을 + 보므로 조합이 아니라 독립 축이다 — 한 단계를 통과시켜도 다른 단계가 빠지면 그 축만 + 조용히 어긋난다. + +axes: + pipeline_stage: + - stored_file_basis # 저장 파일 기준 병합 (조회 결과 기준 금지) + - currency_default_sync # is_default 통화 ↔ default_currency 일치 + - split_fields # 화면의 분할 입력 필드를 한 키로 병합 + - payment_method_snapshot # 결제수단 카탈로그 메타데이터 박제 + - currency_cache_flush # 요청 캐시 무효화 + - normalize_category_data # 카테고리 단위 정규화 (은행명 다국어) + +effects: + - read_time_enrichment_not_persisted # 읽기 시점 보강값이 파일에 눌어붙지 않는다 + - other_category_file_untouched # 단건 저장이 다른 카테고리 파일을 건드리지 않는다 + - removed_currency_tombstone_preserved # 삭제 통화 tombstone 보존 + - saved_file_default_currency_matches_is_default + - split_input_fields_merged_on_single_key_save + - payment_method_metadata_snapshotted + - currency_request_cache_cleared_on_single_key_save + - bank_name_normalized_to_multilingual + - sibling_keys_preserved_on_bank_save # 은행 저장이 같은 카테고리의 형제 키를 지우지 않는다 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsSetSettingPipelineTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/excel-download-history-modal-display.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/excel-download-history-modal-display.yaml new file mode 100644 index 00000000..1d90e1e4 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/excel-download-history-modal-display.yaml @@ -0,0 +1,36 @@ +feature: 엑셀 다운로드 이력 모달의 선택 항목 표시 가드 + +description: | + 관리자 "엑셀 다운로드 관리" 화면의 다운로드 이력 모달은 목록에서 고른 항목(`_local.selectedItem`)의 + 파일 내용·분류·생성일·만료일·다운로드 횟수를 표시한다. 이 키는 부모 레이아웃이 `null` 로만 시드하고 + 행 클릭 시에만 채워지므로, 아직 아무 행도 고르지 않은 구간이 항상 존재한다. + + 수정 이력 (공개 이슈 #92 와 동일한 결함 클래스): + - 모든 표시식이 `{{_local.selectedItem.<field>}}` 형태로 옵셔널 체이닝이 없었고, + 모달에 selectedItem 을 검사하는 `if` 가드도 없었다. 상태 배지의 조건식 + (`selectedItem.status === '...'`)은 값 부재 구간에서 조건 평가가 실패해 + 경고 로그를 남기며 false 로 떨어지고 있었다. + - 모든 하위 접근에 `?.` 를, 표시식에 폴백(`?? ''`, 횟수는 `?? 0`)을 부여했다. + 값이 채워진 뒤의 출력은 이전과 동일하므로 기능 변화는 없다. + - 브라우저 실측에서 별개 결함이 함께 드러났다 — "다운로드 기록" 목록의 iteration 이 + 문자열 형태로 선언되어 엔진이 source 를 읽지 못하고 매 렌더마다 예외로 떨어졌다. + 목록이 통째로 비어 보이던 문제이며, 객체 형태(source/item_var/index_var)로 정정했다. + - 상태 배지 3종의 아이콘 이름이 Font Awesome Free 에 없는 값이라 빈 글리프로 렌더됐다. + +axes: + selection_state: [unselected, selected] # 이력 행 미선택 / 선택 + +effects: + - history_modal_display_guarded # 값 부재 구간에서 조건 평가 실패·표시 공백이 발생하지 않음 + - history_list_renders # 다운로드 기록 목록이 실제로 렌더된다 (iteration 선언 형태) + +test_files: + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/excel-download-history-modal-fallback.test.tsx + +validation: + audit_rule: frontend-change-requires-e2e + browser_check: Chrome MCP # 관리자 화면 실측(정밀 점검 매트릭스 T9)으로 브라우저 검증 대체 + note: | + 레이아웃 JSON 은 헤더 주석을 가질 수 없어 `e2e:allow` 를 붙일 수 없다. 표시식의 옵셔널/폴백 + 유무는 정적 구조로 판정 가능하므로 레이아웃 정적 가드 테스트가 두 selection_state 를 모두 + 떠받치고, 실제 모달 열기 전/후 화면은 Chrome MCP 정밀 점검이 확인한다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml index 240b6cc1..e41a1143 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/extension-payment-method.yaml @@ -34,6 +34,8 @@ axes: capability_declared: [declared, undeclared] # 능력별 판정 표면 capability: [needs_pg, label, refund_method, pg_locked, brand_mark] + # 지정 PG 의 생사(pg_provider_state)는 능력 해석과 직교하는 별개 관심사다 — + # payment-method-dead-pg.yaml 이 그 축을 소유한다. exclusions: - method_kind: builtin diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/inquiry-single-reply-lifecycle.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/inquiry-single-reply-lifecycle.yaml new file mode 100644 index 00000000..d3d9226a --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/inquiry-single-reply-lifecycle.yaml @@ -0,0 +1,58 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경에 js-yaml 부재로 nested axes 를 읽지 못하는 +# 한계로 검출되지 않는다(동일 한계로 다른 번들 시나리오도 audit:allow). 본 변경은 +# ProductInquiryService/ProductInquiryBoardListener/PostService cascade 를 +# PHPUnit Unit/Feature(양 모듈) + 업그레이드 백필 테스트가 커버하며 green. +feature: 상품 문의 단일 답변 수명주기 (#106 · #107) + +description: | + 문의 답변은 단일 정책 — 이미 답변된 문의에 재등록하면 422(reply_already_exists)로 + 거절하고(서비스 1차: 피벗 is_answered / 리스너 2차: 게시판 실데이터), 문의·상품·게시판 + 삭제 시 질문+답변 Post 와 피벗이 함께 정리된다. 피벗은 SoftDeletes 로 전환되어 + 게시판에서 질문 글을 삭제→복원하면 문의 목록에도 복원된다. 답변 글만 직접 삭제되면 + 잔여 답변 수(count_replies 훅) 기준으로 답변완료가 재계산된다. 업그레이드 스텝이 + 기설치본의 고아 답변·고아 피벗·거짓 답변완료를 백필 정리한다. + +axes: + action: + - create_reply + - delete_reply + - delete_inquiry + - delete_product + - board_delete_question + - board_delete_reply + - board_restore_question + - board_restore_reply + - upgrade_backfill + state: + - unanswered + - answered + - legacy_multi_reply # 과거 결함으로 답변 다건 누적된 기설치본 + - orphan_reply # 부모 없는/삭제된 부모의 답변 글 + path: + - user_api + - admin_api + - board_direct # 게시판 관리 화면 직권 삭제/복원 + - product_delete + +effects: + - duplicate_reply_rejected_422 # 서비스 1차 가드 + reply_already_exists 메시지 + - listener_blocks_duplicate_child # 리스너 2차 가드 — 게시판 실데이터 기준 duplicate 마커 반환 (서비스가 reply_already_exists 422 로 변환) + - inquiry_delete_removes_reply_posts # 문의 삭제 시 답변 Post 까지 cascade 정리 + - product_delete_cleans_thread # 상품 삭제 시 질문+답변 Post 소프트 삭제 + 피벗 정리 + - pivot_cleanup_runs_sync # after_delete 리스너 sync 실행 (큐 워커 무관) + - reply_delete_recomputes_answered # 잔여 답변 0 일 때만 답변완료 해제 + - answered_at_preserved # 최초 답변 시각 보존 (markAsAnswered 재마킹 포함) + - pivot_soft_delete_and_restore # 질문 글 삭제→복원 시 피벗 소프트 삭제→복원 + - orphan_replies_backfilled_idempotent # 업그레이드 스텝 04~06 정리 + 재실행 멱등 + - board_not_configured_safe # 게시판 미설정 시 422/안전 열화 (500 없음) + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/AdminProductInquiryControllerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Repositories/ProductInquiryRepositoryTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/ProductInquiryBoardListenerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDeleteTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Upgrades/InquiryOrphanReplyBackfillTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Repositories/PostRepositoryReplyLookupTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue94-shipping-tier-boundary-units.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue94-shipping-tier-boundary-units.yaml new file mode 100644 index 00000000..6eaced31 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue94-shipping-tier-boundary-units.yaml @@ -0,0 +1,84 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경에 js-yaml 부재로 nested axes 를 읽지 못하는 +# 한계로 검출되지 않는다(동일 한계로 다른 번들 시나리오도 audit:allow). 본 변경은 +# OrderCalculationServiceTest 가 charge_policy × boundary × unit_source 전 조합을, +# ShippingPolicyValidationMessageTest 가 저장 검증 매트릭스를 커버하며 green 이고, +# Playwright spec 이 관리자 저장 → 상점 장바구니 계산 경로를 종단으로 고정한다. +feature: 배송비 구간 경계 포함 + 무게/부피 단위 환산 (공개 #94) + +description: | + 구간별 배송비의 종료값은 포함(inclusive)이며, 매칭은 종료값 오름차순 사다리로 수행한다 + (시작값은 표시 전용). 상품 옵션은 g/cm³ 로 저장되고 배송정책은 kg/L 로 설정되므로, + 환산은 배송비 계산 지점 한 곳에서만 수행한다. 설정 오류로 계산이 조용히 0원이 되는 + 경우는 저장 시점에 422 로 차단하고, 런타임 잔여 경로는 경고 로그를 남긴다. + +axes: + charge_policy: + - range_quantity + - range_amount + - range_weight + - range_volume + - range_volume_weight + - per_weight + - per_volume + - per_volume_weight + - api + - conditional_free + boundary: + - below # 구간 상한 미만 + - at_max # 구간 상한 정확히 (공개 #94 재현 지점) + - above # 구간 상한 초과 + - decimal # 소수 경계 (2.5kg) + - empty_tiers # 구간 없음 + unit_source: + - g # 상품 옵션 무게 저장 단위 + - cm3 # 상품 옵션 부피 저장 단위 + - quantity # 수량 (환산 없음) + - amount # 금액 (환산 없음) + tier_shape: + - inclusive # 다음 min = max + 1 (관리자 화면 저장 형태) + - half_open # 다음 min = max (기존 저장 데이터 형태) + config_validity: + - valid + - missing_tiers + - missing_unit_value + - missing_free_threshold + - middle_max_unlimited + - legacy_max_plus_one_on_continuous + - fractional_quantity_boundary + - zero_base_fee_on_api + - invalid_zipcode_format + - zero_unit_value # 저장 규칙 신설 이전 데이터 / 시더 경로 + - negative_unit_value + +effects: + - fee_matches_admin_promise # 요약 문자열이 약속한 금액과 계산값 일치 + - boundary_value_charged_not_free # 상한 경계값이 무료배송으로 새지 않음 + - unit_converted_kg_l # g→kg, cm³→L 환산이 1회만 수행됨 + - volumetric_weight_compared_in_kg # 실무게와 부피무게를 같은 단위로 비교 + - decimal_base_fee_rounded # 소수 기본배송비 절삭 대신 반올림 + - extra_fee_multiplied_by_charge_units # 도서산간 곱이 수량이 아닌 배송 단위 수 기준 + - api_negative_fee_falls_back # 외부 API 음수 배송비 → 기본 배송비 폴백 + - invalid_config_rejected_422 # 무음 0원을 만드는 설정은 저장 차단 + - silent_zero_logged # 잔여 0원 경로는 경고 로그 + - tier_min_derived_from_previous_max # 화면에서 시작값 자동 파생 (읽기전용) + - range_unit_label_displayed # 구간 입력에 정책별 단위(개/kg/L) 표기 + - continuous_tier_min_normalized # 업그레이드 스텝의 표시 정합 보정 + - order_totals_backfilled # 주문 총 무게/부피 백필 + - unit_value_clamped_not_divided_by_zero # 0/음수 단위값이 계산을 죽이거나 금액을 뒤집지 않음 + - upgrade_is_idempotent + - product_option_dimensions_round_trip # 옵션 무게/부피 응답 왕복 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCalculationServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Enums/ShippingPolicyEnumsTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Resources/ProductOptionResourceDimensionsTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Database/Seeders/ShippingPolicySeederTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ShippingPolicyControllerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Upgrade/ShippingTierAndOrderTotalsBackfillTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingServiceTest.php + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/shippingPolicyFormHandlers.test.ts + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/shippingPolicyFormLayouts.test.tsx + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/shipping-policy-tier-boundary.spec.ts diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue97-coupon-combinable-boolean.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue97-coupon-combinable-boolean.yaml new file mode 100644 index 00000000..7ce9b5bf --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/issue97-coupon-combinable-boolean.yaml @@ -0,0 +1,47 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경에 js-yaml 이 없어 fallback YAML 파서가 +# nested axes 배열을 읽지 못하는 한계로 검출되지 않는다(동일 한계로 다른 번들 시나리오도 +# audit:allow 처리됨). 입력 표기 axis 는 CouponCombinableBooleanTest 가 store/update +# 양면에서 전수(6종 수용 + 해석불가 + 미전송) 커버하고, 브라우저 축은 +# promotion-coupon-combinable.spec.ts 가 라디오 클릭 → 저장 → 수정 반영 왕복을 실측한다. +# effects ↔ 테스트 매핑은 본 매니페스트 effects 블록과 각 테스트가 1:1 대응한다. +feature: 쿠폰 중복사용 boolean 정규화 (공개 #97) + +description: | + 관리자 쿠폰 폼의 「타 쿠폰과 중복 사용」 라디오를 클릭하면 DOM 문자열 "true"/"false" 가 + 폼 상태에 저장되어 서버 `boolean` 규칙에서 422 로 저장이 실패했다 (공개 이슈 #97). + 표시 계층은 느슨 비교로 정상이라 저장 시점에야 드러난다. + + 정정은 양면이다 — ① 레이아웃이 autoBinding: false + change 액션 `=== 'true'` 캐스팅으로 + 쓰기 경로를 boolean 으로 고정하고, ② Store/UpdateCouponRequest 가 문자열 표기를 + boolean 으로 정규화해 캐시된 구 레이아웃도 방어한다. 해석 불가값은 건드리지 않아 + boolean 규칙이 422 로 처리한다. 같은 잠재 위험이 있던 게시판 is_secret 도 서버 + 정규화로 함께 방어한다 (sirsoft-board Store/UpdatePostRequest). + +axes: + # 서버가 수용/거부해야 하는 is_combinable 입력 표기 + is_combinable_input: [true, false, "true", "false", "1", "0", "abc", absent] + # 저장 경로 + request: [store, update] + # 검증 표면 + surface: [api, browser_form] + +effects: + - string_boolean_accepted_and_persisted # 문자열 표기가 boolean 으로 정규화되어 DB 에 boolean 저장 + - unparsable_value_rejected_422 # 해석 불가값은 정규화하지 않고 boolean 규칙이 422 처리 + - radio_click_saves_boolean_state # 라디오 클릭 시 폼 상태가 boolean (문자열 아님) + - edit_screen_reflects_saved_value # 수정 화면 진입 시 저장된 boolean 이 라디오 선택 상태로 반영 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponCombinableBooleanTest.php + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponCombinableWritePath.test.tsx + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/couponFormLayouts.test.tsx + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/promotion-coupon-combinable.spec.ts + +validation: + note: | + 입력 표기 × store/update 조합은 PHPUnit 이 전수 커버한다 (browser_form 표면은 + 라디오 UI 가 만들 수 있는 값이 "true"/"false" 두 가지뿐이라 E2E 는 클릭 왕복만 + 실측한다). 쓰기 경로의 boolean 타입은 레이아웃 렌더링 테스트가 실제 partial 노드로 + 고정하고, 재발 차단은 audit 룰 `layout-boolean-option-autobinding` (error) 가 담당한다. + 게시판 is_secret 방어는 sirsoft-board 의 PostSecretBooleanNormalizationTest 가 커버한다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp02-order-cancellation.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp02-order-cancellation.yaml index 5c8c833e..1c00228c 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp02-order-cancellation.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp02-order-cancellation.yaml @@ -89,9 +89,19 @@ effects: - refund_priority_radio_conditional # 환불 우선순위 라디오 조건부 노출 - pg_checkbox_conditional # PG 함께취소 체크박스 조건부 노출 - resource_exposes_requires_pg_cancellation # OrderPaymentResource requires_pg_cancellation + # 공개 #108 — 환불 예상(preview)은 조회만 수행한다 + - preview_refund_issues_zero_write_queries_on_coupon_issues # 미리보기 중 coupon_issues 대상 UPDATE 0건 + - preview_refund_keeps_coupon_issue_updated_at_unchanged # 왕복 UPDATE 로 인한 updated_at 오염 없음 + - preview_refund_keeps_coupon_status_used_at_order_id_unchanged # status/used_at/order_id 불변 + - partial_cancel_preview_reaches_recalculation_path_not_full_cancel_shortcut # 잔여 옵션이 있어야 재계산 경로 진입(전량취소 조기반환 아님) + - recalculation_never_reads_coupon_usage_state_in_snapshot_mode # 스냅샷 모드 재계산은 쿠폰 사용 상태를 읽지 않음 + - coupon_restore_on_min_amount_shortfall_unchanged_after_reset_removal # 최소주문금액 미달 쿠폰 복원 동작 불변(회귀 가드) test_files: - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderCancellationServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderAdjustmentServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/OrderCancellationControllerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderCancellationControllerTest.php - modules/_bundled/sirsoft-ecommerce/tests/Unit/DTO/AdjustmentResultTest.php - modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/OrderTest.php - modules/_bundled/sirsoft-ecommerce/tests/Unit/Repositories/OrderRepositoryTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp08-currency-persistence.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp08-currency-persistence.yaml index e7fedeed..f8fa0387 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp08-currency-persistence.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp08-currency-persistence.yaml @@ -1,23 +1,34 @@ # audit:allow test-scenario-coverage reason: | # cross product 자동 전개는 audit 실행 환경에 js-yaml 부재로 검출되지 않는다(다른 번들 동일). # 환율 영속성 axis 는 PHPUnit(EcommerceSettingsServiceTest) 가 전수 커버하며 green 이다. -feature: 통화 환율 영속성 — code 기준 병합 (U11-A) +feature: 통화 목록 영속성 — code 기준 병합(U11-A) + 의도적 삭제 기록(공개 #91) description: | language_currency.currencies 는 정수키 리스트라 array_merge 가 통째 교체되어, 관리자가 일부 통화를 빼고 저장하면 defaults 통화가 영구 소실되던 영속성 공백을 수정한다. getAllSettings() 에서 code 기준 병합(mergeCurrenciesByCode)으로 defaults 보충(환율은 저장본 우선). + 그 보충은 "array_merge 부작용으로 인한 소실" 을 되돌리기 위한 것이었으나 관리자의 의도적 + 삭제와 구분할 장치가 없어, 기본 제공 통화를 삭제하면 저장 응답에서 이미 되살아났다(공개 #91). + 저장 시점에 삭제 의도를 서버가 도출해 저장본에 기록하고, 조회 병합이 그 기록을 존중한다. + 삭제가 실제로 영속되면 처음 도달 가능해지는 후속 경로(사용 중지된 통화를 표시 통화로 가진 + 구매자의 체크아웃 차단)도 같은 축에서 방어한다. + axes: saved_currency_set: - - all_five # 전 5종 저장 - - jpy_missing # JPY 빼고 저장 - - usd_missing # USD 빼고 저장 - - new_added # 관리자 신규 통화 추가 + - all_five # 전 5종 저장 + - jpy_missing # JPY 빼고 저장 + - usd_missing # USD 빼고 저장 + - new_added # 관리자 신규 통화 추가 + - default_removed_by_admin # 관리자 의도 삭제 (공개 #91) get_settings_result: - - defaults_supplemented # 누락 통화 defaults 보충됨 + - defaults_supplemented # 누락 통화 defaults 보충됨 exchange_rate_preservation: - - saved_value_kept # 저장본 exchange_rate 보존 + - saved_value_kept # 저장본 exchange_rate 보존 + deletion_tombstone: # 공개 #91 — 의도적 삭제 영속화 + - none_legacy # 구 저장본(키 없음) → U11-A 보충 유지 + - has_removed_codes # 삭제 기록 보유 → defaults 보충 스킵 + - readded # 재추가 저장 → 기록 해제·부활 effects: - missing_currency_supplemented_from_defaults # array_merge 통째교체로 통화 소실 안 됨 @@ -25,6 +36,64 @@ effects: - new_added_currency_preserved # 저장본에만 있는 신규 통화 보존 - default_currency_exchange_rate_null # 기본통화 exchange_rate null 유지(syncCurrencyDefaults) - currency_conversion_uses_persisted_rate # 환율 109 설정 시 JPY 정확 환산, null 시 제외 + # 공개 #91 + - intentionally_removed_default_currency_stays_deleted # 삭제한 기본 제공 통화 미부활 + - tombstone_recomputed_server_side_on_save # 삭제 기록은 서버가 재계산(클라이언트 값 불신) + - base_currency_never_tombstoned # 기본 통화는 기록 대상 제외 → 항상 생존 + - other_category_save_preserves_tombstone # 타 탭/통화 미제출 저장이 삭제를 되돌리지 않음 + - legacy_file_without_tombstone_keeps_supplement # 구 저장본은 U11-A 보충 그대로 + - unregistered_active_currency_falls_back_to_base # 미등록 표시/선호 통화 → 기본 통화 폴백 + - non_supplemented_list_settings_keep_deletion # 보충 병합을 거치지 않는 목록 설정은 삭제 유지 + - catalog_merged_list_settings_refill_by_design # 카탈로그 병합 목록(결제수단)은 토글 모델이라 재충전 + # 공개 #91 후속 — 삭제된 통화로 결제된 과거 주문 + - deleted_currency_order_refund_uses_snapshot_rate # 환불 재계산이 스냅샷 환율을 사용 + - deleted_currency_display_keeps_snapshot_decimal_places # 표기 자릿수도 주문 시점 값 유지 + - legacy_snapshot_without_decimal_places_falls_back_to_settings # 구형 스냅샷은 현재 설정 폴백 + +sub_flows: + - id: intentional_deletion_round_trip + description: | + 삭제 → 저장 → 재조회 왕복. 서비스 계층뿐 아니라 저장 응답(PUT) 자체가 삭제를 + 반영해야 한다 — 제보된 증상이 "저장 응답에 이미 5종이 부활" 이었기 때문이다. + effects: + - intentionally_removed_default_currency_stays_deleted + - tombstone_recomputed_server_side_on_save + - base_currency_never_tombstoned + - id: deletion_side_effects + description: 삭제가 영속되면 처음 열리는 후속 경로(미등록 통화가 결제 통화로 채택되는 경우) 방어 + effects: + - unregistered_active_currency_falls_back_to_base + - id: deleted_currency_existing_orders + description: | + 통화를 삭제해도 그 통화로 결제된 과거 주문은 환불 재계산·표기가 불변이어야 한다. + 금액은 스냅샷 환율·절사규칙을 쓰므로 원래 불변이었으나, 소수 자릿수만 현재 설정에서 + 조회해 삭제 시 폴백 2자리가 적용됐다(0자리 통화 표기 변화 / 3자리 이상 통화 절사). + effects: + - deleted_currency_order_refund_uses_snapshot_rate + - deleted_currency_display_keeps_snapshot_decimal_places + - legacy_snapshot_without_decimal_places_falls_back_to_settings + - id: list_settings_deletion_sweep + description: | + defaults 가 항목을 제공하고 화면이 삭제를 제공하는 다른 목록 설정(은행·입금계좌·배송 국가· + 마일리지 통화 규칙)의 삭제 영속성 실측. 통화만 항목 단위 보충을 거치고 나머지는 통째 + 교체라 삭제가 유지되는데, 이 구분은 코드를 읽어야만 드러나므로 실측으로 고정한다. + effects: + - non_supplemented_list_settings_keep_deletion + - catalog_merged_list_settings_refill_by_design test_files: - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyMergeTest.php + # 공개 #91 — 삭제 영속화 (서비스 계층) + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsServiceCurrencyTombstoneTest.php + # 공개 #91 — 저장 응답 왕복 (제보된 증상 층) + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsCurrencyDeletionTest.php + # 공개 #91 — 미등록 통화 폴백 (후속 경로 방어) + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderProcessingCurrencySnapshotTest.php + # 공개 #91 전수 조사 — 다른 목록형 설정의 삭제 영속성 실측 + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsListDeletionPersistenceTest.php + # 공개 #91 후속 — 통화 삭제 후 기존 주문의 금액·표기 불변성 + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/DeletedCurrencyOrderIntegrityTest.php + +notes: | + E2E 미해당: 변경 범위가 백엔드 전용(Service)이며 레이아웃 JSON·컴포넌트·엔진 코드 변경이 없다. + 브라우저 검증은 Chrome MCP 매트릭스가 담당한다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-review-image-policy.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-review-image-policy.yaml index 186064da..a91369c1 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-review-image-policy.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-review-image-policy.yaml @@ -41,6 +41,7 @@ effects: - front_props_reflect_settings # maxFiles/maxSize 동적 바인딩 - attribute_label_not_raw_key # 음수 입력 시 한글/영문 라벨 - review_settings_save_logs_activity # 리뷰설정 저장 활동로그 + - review_image_filter_upload_file_hook_applied # 업로드 경로 filter_upload_file 발화 (공개 #96, upload-filter-hook-parity.yaml) # 설정 화면 JSON ↔ 저장 규칙 경계 정합 (#493 E10) - non_integer_image_size_rejected_422 # step:0.5 가 오류 — 규칙은 integer - settings_screen_bounds_match_rules # 화면 min/max/step 이 서버 경계와 동일 diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-shipping-policy-validation.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-shipping-policy-validation.yaml index 5861c39e..53382875 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-shipping-policy-validation.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/mp11-shipping-policy-validation.yaml @@ -2,9 +2,10 @@ # cross product 자동 전개는 audit 실행 환경에 js-yaml 부재로 nested axes 를 읽지 못하는 # 한계로 검출되지 않는다(동일 한계로 다른 번들 시나리오도 audit:allow). 본 변경은 # StoreShippingPolicyRequest 의 attributes()/messages() 보강(A12) + api_endpoint 필수화(W1) -# + i18n 키 보강(W2) + api_request_fields Rule::in 후보 검증(W3)을 PHPUnit Feature 가 -# 대상필드 × 입력 × 로케일 × Store/Update cross product 로 커버하며 green. -feature: 배송정책 구간/계산 API 검증 메시지 친화화 (A12 + A13) +# + i18n 키 보강(W2) + api_request_fields Rule::in 후보 검증(W3) + 구간 필수/연속성 규칙 +# 신설(공개 #94)을 PHPUnit Feature 가 대상필드 × 입력 × 로케일 × Store/Update cross product +# 로 커버하며 green. +feature: 배송정책 구간/계산 API 검증 메시지 친화화 (A12 + A13) + 구간 설정 필수 규칙 (#94) description: | 구간별 배송비(tier fee/min/max/unit_value)·추가배송비 음수/비숫자/required 누락 시 사용자 @@ -12,13 +13,22 @@ description: | 계산 API 정책 선택 시 api_endpoint 필수화(cross-field), 잘못된 URL 시 i18n 키 보강, api_request_fields 는 후보 5종 SSoT enum 의 Rule::in 으로 검증(후보 외 거부). + 공개 #94 로 저장 시점 차단 규칙이 확장된다 — 계산이 조용히 0원이 되는 설정(구간 없음 / + 단위값 없음 / 무료 기준금액 없음 / 중간 구간 상한 무제한 / 계산 API 폴백 배송비 0)은 422 로 + 거부하고, 연속성 규칙은 정책 타입별로 갈린다(이산형 수량 = 다음 시작값이 직전 종료값 + 1, + 연속형 금액·무게·부피 = 다음 시작값이 직전 종료값과 동일, 소수 경계 허용). 추가배송비 + 우편번호는 지원 3형식만 통과한다. + axes: target_field: - tier_fee - tier_min - tier_max + - tiers # 구간 배열 자체 (RANGE_* 필수) - unit_value + - free_threshold # conditional_free 필수 - extra_fee + - extra_fee_zipcode - base_fee - api_endpoint - api_request_fields @@ -30,6 +40,16 @@ axes: - api_empty_endpoint # api 정책 + 빈 endpoint → W1 422 - field_in_candidates # api_request_fields 후보 내 → 통과 - field_out_candidates # api_request_fields 후보 외 → Rule::in 422 + - empty_tiers # RANGE_* 인데 구간 0개 → 422 (#94) + - middle_max_unlimited # 중간 구간 상한 무제한 → 422 (#94) + - continuous_next_min_equals_prev_max # 연속형 정상 → 통과 (#94) + - continuous_legacy_max_plus_one # 연속형 구 규칙 → 422 간극 (#94) + - continuous_decimal_boundary # 연속형 소수 경계 → 통과 (#94) + - discrete_next_min_equals_prev_max_plus_one # 이산형 정상 → 통과 (#94) + - discrete_fractional_boundary # 이산형 소수 경계 → 422 (#94) + - zero_base_fee_on_api # 계산 API 폴백 0원 → 422 (#94) + - zipcode_supported_format # 3형식 → 통과 (#94) + - zipcode_unsupported_format # 그 외 → 422 (#94) locale: - ko - en @@ -46,6 +66,14 @@ effects: - request_field_out_of_candidates_rejected # W3 Rule::in - request_field_in_candidates_accepted # W3 통과 - update_inherits_messages_attributes # Update 상속 + - range_policy_requires_tiers # #94 구간 없는 구간별 정책 차단 + - per_unit_policy_requires_unit_value # #94 단위값 없는 단위당 정책 차단 + - conditional_free_requires_threshold # #94 무료 기준금액 없는 조건부 무료 차단 + - middle_tier_unlimited_max_rejected # #94 중간 구간 상한 무제한 차단 + - continuity_rule_matches_policy_type # #94 이산형 max+1 / 연속형 max + - decimal_tier_boundary_accepted # #94 소수 구간(2.5kg) 저장 허용 + - api_policy_requires_positive_base_fee # #94 폴백 0원 차단 + - zipcode_format_validated # #94 지원 3형식 외 거부 test_files: - modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ShippingPolicyValidationMessageTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-action-exception-status.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-action-exception-status.yaml new file mode 100644 index 00000000..31f305b7 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-action-exception-status.yaml @@ -0,0 +1,78 @@ +feature: 주문 액션의 예외 → 상태코드 매핑 (Order Action Exception Status) + +description: | + 주문 취소·구매확정·재주문·무통장 입금확인과 쿠폰 다운로드가 실패했을 때, 그 원인이 + "사용자·운영자가 고칠 수 있는 도메인 사유" 인지 "서버 결함·인프라 장애" 인지를 + 응답 상태코드로 구분한다. + + 배경: + 공개 #104 는 비회원 배송지 변경에서 모든 예외가 422 로 뭉개지는 것을 지적했지만, + 같은 형태가 주문 도메인의 다른 액션에도 그대로 있었다. 인프라 장애가 "배송 전 + 상태에서만 변경 가능" 같은 도메인 안내로 위장되면 사용자는 고칠 수 없는 안내를 + 보고, 운영자는 장애를 늦게 안다. + + 핵심 설계: + - typed 도메인 예외 → **기존 상태코드 유지** (계약 변경 최소화) + · 취소/재주문/입금확인 = 422 + · 쿠폰 다운로드 = 400 (원래 400이었고, 예외 승격이 사용자 계약을 함께 + 바꾸면 그것이 회귀다) + - 그 외 예외 → 500, 응답 본문에 예외 원문 미포함 + - 사유는 예외가 들고 다니는 키로 조립한다. 번역문을 메시지 키 자리에 넘기면 + 해석에 실패해 원문이 그대로 노출된다. + + 인프라 500 축은 서비스 mock 으로만 유발할 수 있어 PHPUnit 이 SSoT 다. + 브라우저에서는 도메인 사유가 먼저 걸려 재현되지 않는다. + +axes: + endpoint: [cancel, confirm_option, reorder, coupon_download, confirm_deposit] + error_class: [domain, infrastructure] + +exclusions: + - { endpoint: confirm_option, error_class: domain, reason: "OrderService::confirmOption 은 throw 지점이 0건이라 typed 도메인 예외가 존재하지 않는다. 도메인 축을 만들려면 없는 예외를 새로 만들어야 하므로 조합이 성립하지 않는다 (계획 §3 B그룹 — typed 예외가 아예 없어 4xx 에 걸리는 건 100% 인프라/코드버그)" } + +effects: + # 주문 취소 + - order_cancel_domain_exception_returns_422 + - order_cancel_infrastructure_exception_returns_500_not_422 + # 구매확정 (도메인 축 없음 — exclusions 참조) + - order_confirm_option_infrastructure_exception_returns_500_not_422 + # 재주문 + - order_reorder_domain_exception_returns_422 + - order_reorder_infrastructure_exception_returns_500_not_422 + # 쿠폰 다운로드 — typed 라도 기존 400 을 유지한다 + - coupon_download_domain_exception_returns_400_with_reason_key + - coupon_download_infrastructure_exception_returns_500_not_400 + # 관리자 무통장 입금확인 + - order_confirm_deposit_domain_exception_returns_422 + - order_confirm_deposit_infrastructure_exception_returns_500_not_422 + +scenarios: + - id: order_domain_failure_keeps_its_original_status + description: 도메인 사유로 실패하면 그 액션이 원래 쓰던 상태코드(취소·재주문·입금확인 422 / 쿠폰 다운로드 400)와 해석된 다국어 문구가 그대로 전달된다 + effects: + - order_cancel_domain_exception_returns_422 + - order_reorder_domain_exception_returns_422 + - coupon_download_domain_exception_returns_400_with_reason_key + - order_confirm_deposit_domain_exception_returns_422 + + - id: order_infrastructure_failure_is_not_disguised_as_input_error + description: DB 장애 등 인프라 예외는 500 으로 구분되고 응답 본문에 예외 원문(SQLSTATE 등)이 포함되지 않는다 + effects: + - order_cancel_infrastructure_exception_returns_500_not_422 + - order_confirm_option_infrastructure_exception_returns_500_not_422 + - order_reorder_infrastructure_exception_returns_500_not_422 + - coupon_download_infrastructure_exception_returns_500_not_400 + - order_confirm_deposit_infrastructure_exception_returns_500_not_422 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/OrderDomainExceptionStatusPairTest.php + +notes: | + 이 매니페스트는 감사에서 뒤늦게 신설됐다. 테스트에는 `@effects` 9건이 처음부터 + 마킹돼 있었지만 어느 매니페스트에도 선언돼 있지 않았고, 그 상태로 검사를 통과했다 — + test-scenario-coverage 는 "선언된 effect 가 어느 테스트에든 언급되는가" 한 방향만 + 보기 때문에, 선언이 아예 없는 effect 는 셀 대상 자체가 아니었다. + + 파일은 `member-order-change-address.yaml` 의 test_files 에 등록돼 있었으나 그 + 매니페스트가 다루는 것은 배송지 변경이고, 취소·재주문·입금확인·쿠폰 다운로드는 + 그 시나리오의 축이 아니다. 등록을 이쪽으로 옮겼다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-option-status-transition.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-option-status-transition.yaml index b8580ad3..8d6dcd6e 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-option-status-transition.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-option-status-transition.yaml @@ -97,7 +97,7 @@ test_files: - modules/_bundled/sirsoft-ecommerce/tests/Feature/Admin/ConfirmDepositControllerTest.php - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminOrderConfirmDeposit.test.tsx - templates/_bundled/sirsoft-basic/__tests__/layouts/orderReviewButton.test.tsx - - tests/Playwright/specs/order-option-status-review.spec.ts + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/order-option-status-review.spec.ts validation: audit_rule: frontend-change-requires-e2e diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-status-notification.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-status-notification.yaml index 5b034cea..d32f7d47 100644 --- a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-status-notification.yaml +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-status-notification.yaml @@ -90,7 +90,7 @@ test_files: - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderServiceTest.php - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/OrderOptionServiceTest.php - modules/_bundled/sirsoft-ecommerce/tests/Feature/Notifications/OrderStatusNotificationTest.php - - tests/Playwright/specs/admin/order-status-notification.spec.ts + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/order-status-notification.spec.ts validation: audit_rule: test-scenario-coverage diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/payment-method-dead-pg.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/payment-method-dead-pg.yaml new file mode 100644 index 00000000..221271e8 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/payment-method-dead-pg.yaml @@ -0,0 +1,50 @@ +feature: 죽은 PG 를 지정한 결제수단 차단 (A2) + +description: | + 고아 판정(`_orphaned`)은 결제수단 ID 에만 계산된다. 그래서 builtin 수단(카드 등)에 특정 PG 를 + 지정한 뒤 그 PG 플러그인을 제거하면, 수단 자체는 카탈로그에 남아 있어 고아 필터를 그대로 + 통과한다. 체크아웃에 선택 가능한 결제수단으로 노출되고, 주문하면 PG 라우팅이 매칭에 실패해 + **결제창 없이 주문완료로 넘어간다**. + + 판정식은 런타임 폴백(`OrderProcessingService::determinePgProvider()`)과 일치시킨다. + 수단의 지정값이 **null 일 때만** 기본 PG 로 내려가므로, 지정값이 죽은 문자열이면 기본 PG 가 + 살아 있어도 폴백하지 않는다 — 그 경우도 차단 대상이다. 레지스트리를 인식하는 폴백으로 + 바꾸는 것은 결제 라우팅 계약 변경이라 하지 않는다. + + 관리자 응답은 필터하지 않고 `_orphaned_pg` 플래그를 그대로 실어 운영자가 고칠 수 있게 한다. + `_orphaned` 와 달리 행 편집 컨트롤(PG 선택)도 막지 않는다 — 살아 있는 PG 로 바꾸는 복구 + 경로가 남아야 한다. + +axes: + # 유효 PG 의 상태. surface(관리자/공개/주문제출)는 축이 아니라 effects 로 구분한다 — + # 세 표면이 같은 판정을 공유하는 것이 이 기능의 요구사항이기 때문이다. + pg_provider_state: [live, dead_own, dead_default, none] + +effects: + - dead_pg_method_flagged_for_admin # 관리자 응답·화면에 _orphaned_pg 플래그와 배지 + - dead_pg_method_hidden_from_checkout # 공개 응답에서 제거 — 결제창 없는 주문완료 차단 + - dead_pg_method_keeps_recovery_control # 행 편집(PG 선택)은 막지 않음 — 복구 경로 유지 + - dead_default_pg_normalized_in_public # 죽은 기본 PG 는 공개 응답에서 null 로 정규화 + - dead_pg_order_submission_rejected_422 # 프론트 우회 직접 제출도 검증에서 거부 + - orphaned_method_order_submission_rejected_422 # 고아 수단 제출도 함께 차단 (공개 #111 서버 대칭) + - unconfigured_pg_keeps_legacy_contract # 양쪽 미설정은 기존 계약(non-PG 강하) 유지 + - live_pg_method_remains_visible # 살아있는 PG 지정은 그대로 노출 (거짓 양성 차단) + - live_pg_order_submission_accepted # 정상 수단 제출은 통과 + - non_pg_method_unaffected_by_registry # PG 불필요 수단은 레지스트리와 무관 + - unknown_method_id_keeps_legacy_contract # 카탈로그 밖 ID 판정은 종전대로 통과 + - runtime_only_flag_not_persisted # _orphaned_pg 는 저장 파일에 박제되지 않음 + - boot_time_read_not_cached # 부팅 중(훅 등록 전) 판정이 요청 전체에 남지 않음 + - extension_method_survives_dead_pg_guard # 플러그인 등록 수단이 가드에 휩쓸리지 않음 (#475 계약 보존) + - pg_locked_method_has_no_dead_pg_exemption # pg_locked 는 저장값 고정일 뿐 PG 사멸 면제가 아님 + - merge_quadrant_pinned # 카탈로그 등재 × 저장값 존재 4분면 — 고아 판정의 토대 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/PaymentSettingsOrphanedPgProviderTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Requests/CreateOrderDeadPgGuardTest.php + # 확장 결제수단 1급화(#475) 스위트에 심은 죽은-PG 비회귀 pin — 그 스위트만 돌려도 걸리도록 + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Payment/ExtensionPaymentMethodRegressionTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/SettingsCacheBootOrderTest.php + # 카탈로그 × 저장값 4분면 (고아 판정이 딛는 병합 규칙) + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/EcommerceSettingsOrderSettingsTest.php + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsOrder.test.tsx + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/payment-method-orphaned-pg-badge.spec.ts diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/product-inquiry-exception-status.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/product-inquiry-exception-status.yaml new file mode 100644 index 00000000..efd66935 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/product-inquiry-exception-status.yaml @@ -0,0 +1,81 @@ +feature: 상품 문의 액션의 예외 → 상태코드 매핑 (Product Inquiry Exception Status) + +description: | + 마이페이지의 문의 수정·삭제와 답변 등록·수정·삭제가 실패했을 때, 그 원인이 + "사용자·운영자가 고칠 수 있는 도메인 사유" 인지 "서버 결함·인프라 장애" 인지를 + 응답 상태코드로 구분한다. + + 배경: + ProductInquiryService 의 도메인 실패 12곳을 ProductInquiryOperationException 으로 + 승격하면서 Public/ProductInquiryController::store 만 typed catch 로 좁혔고, + User/ProductInquiryController 4곳은 `catch (\RuntimeException)` 을 유지했다. + 그 예외는 승격된 도메인 예외의 **부모**라서, 승격 후 그 catch 에 남는 것은 + 인프라 예외뿐인데 그것까지 422 + 예외 원문으로 나갔다 — 공개 #104 가 없애려던 + 형태 그 자체다. + + 반대 방향의 짝도 있었다. destroy 는 deleteInquiry 가 던지는 도메인 예외를 + 구분하는 catch 가 없어, 운영자가 고칠 수 있는 사유(문의 게시판 미설정)가 + 일반 500 문구로 나갔다. + + 핵심 설계: + - typed(ProductInquiryOperationException) → 422 + 해석된 다국어 사유 + - 그 외 예외 → 500, 응답 본문에 예외 원문 미포함 + - 사유는 예외가 들고 다니는 키(getMessageKey/getMessageParams)로 조립한다. + 번역문을 메시지 키 자리에 넘기면 해석에 실패해 원문이 그대로 노출된다. + + 인프라 500 축은 서비스 mock 으로만 유발할 수 있어 PHPUnit 이 SSoT 다. + 브라우저로는 재현할 수 없다(도메인 사유가 먼저 걸린다). + +axes: + endpoint: [update_inquiry, destroy_inquiry, create_reply, update_reply, destroy_reply] + error_class: [domain, infrastructure] + +exclusions: [] + +effects: + # 문의 수정 + - inquiry_update_domain_exception_returns_422 + - inquiry_update_infrastructure_exception_returns_500_not_422 + # 문의 삭제 — 도메인 사유가 500 으로 나가던 역방향 짝 + - inquiry_destroy_domain_exception_returns_422 + - inquiry_destroy_infrastructure_exception_returns_500_not_422 + # 답변 등록 + - inquiry_reply_domain_exception_returns_422 + - inquiry_reply_infrastructure_exception_returns_500_not_422 + # 답변 수정 + - inquiry_update_reply_domain_exception_returns_422 + - inquiry_update_reply_infrastructure_exception_returns_500_not_422 + # 답변 삭제 + - inquiry_destroy_reply_domain_exception_returns_422 + - inquiry_destroy_reply_infrastructure_exception_returns_500_not_422 + +scenarios: + - id: inquiry_domain_failure_reaches_operator + description: 문의 게시판 미설정 상태에서 문의 수정·삭제와 답변 관리를 시도하면, 그 사유가 422 로 해석된 다국어 문구와 함께 전달된다 (키 원문 미노출) + effects: + - inquiry_update_domain_exception_returns_422 + - inquiry_destroy_domain_exception_returns_422 + - inquiry_reply_domain_exception_returns_422 + - inquiry_update_reply_domain_exception_returns_422 + - inquiry_destroy_reply_domain_exception_returns_422 + + - id: inquiry_infrastructure_failure_is_not_disguised_as_input_error + description: DB 장애 등 인프라 예외는 500 으로 구분되고 응답 본문에 예외 원문(SQLSTATE 등)이 포함되지 않는다 + effects: + - inquiry_update_infrastructure_exception_returns_500_not_422 + - inquiry_destroy_infrastructure_exception_returns_500_not_422 + - inquiry_reply_infrastructure_exception_returns_500_not_422 + - inquiry_update_reply_infrastructure_exception_returns_500_not_422 + - inquiry_destroy_reply_infrastructure_exception_returns_500_not_422 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/InquiryDomainExceptionStatusPairTest.php + +notes: | + 소스 전수 판정(tests/Feature/Http/GenericCatchStatusCodeContractTest.php)은 광역 catch 가 + 4xx 를 반환하지 않는 것까지만 보증한다. 그 계약이 실제 응답까지 도달하는지는 + FormRequest·미들웨어를 통과해야 알 수 있으므로 이 매니페스트의 런타임 페어가 담당한다. + + 그 판정기는 이 결함을 놓치고 있었다 — 광역 catch 를 \Exception/\Throwable 로만 정의해 + \RuntimeException 을 통과시켰고, 상태코드 추출 정규식이 error($e->getMessage(), 422) 처럼 + 첫 인자에 함수 호출이 있는 형태를 괄호 배제 때문에 읽지 못했다. 둘 다 수정했다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/security-fixes-2026-08.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/security-fixes-2026-08.yaml new file mode 100644 index 00000000..e2309625 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/security-fixes-2026-08.yaml @@ -0,0 +1,44 @@ +feature: 이커머스 보안 수정 묶음 (2026-08 — 리뷰 스코프 / 쿠폰 발급 게이트 / 비회원 요청 검증 패리티 / 구매확정 게이트 / 프리셋 권한 / 하이라이트 이스케이프) + +description: | + 2026-08 비공개 보안 제보 대응으로 sirsoft-ecommerce 에 반영된 수정의 회귀 고정. + + - ③: 리뷰 저장 시 클라이언트 product_id 를 신뢰하던 문제 — 주문 옵션에서 도출(SSoT). + - ④/⑤: 쿠폰 다운로드 실행 게이트가 목록 게이트보다 넓던 문제 — + issue_method=DOWNLOAD + issue_condition=MANUAL + 미만료(valid_to)를 실행 시점에 검사. + - ㉒/N-1: 비회원 환불예상·주문취소 FormRequest 가 회원판 per-item 검증(주문 소속 / + 취소완료 거부 / 수량 상한)을 누락하던 문제 — ValidatesCancelItems trait 로 4개 + Request 가 동일 검증을 공유. + - ㉙: 구매확정에 상태 게이트가 없어 배송 전/이중 확정이 가능하던 문제 — 서비스 내장 + 게이트(OrderOptionNotConfirmableException) + 비회원/회원 컨트롤러 typed catch 422. + - N-6: 관리자 검색 프리셋 라우트의 permission 게이트 부재 — products.read/update 부착. + - ⑧/N-8: 검색 하이라이트 이스케이프 + 프리뷰 decode→strip 순서(코어 SearchHighlighter 위임). + +axes: + case: [review_product_scope, coupon_method_gate, coupon_condition_gate, coupon_expired_gate, coupon_valid_download, guest_refund_estimate_validation, guest_cancel_validation, confirm_service_gate, confirm_guest_http, confirm_member_http, presets_permission, search_highlight_escape, preview_entity_no_resurrect] + +exclusions: [] + +effects: + - review_product_id_derived_from_order_option + - non_downloadable_coupon_rejected + - expired_coupon_rejected + - valid_manual_download_coupon_succeeds + - guest_refund_items_validated_per_item + - guest_cancel_items_validated_per_item + - confirm_rejected_unless_confirmable_status + - double_confirm_rejected + - confirm_hook_fires_once + - presets_require_product_permission + - highlighted_fields_escaped + - preview_does_not_resurrect_entities + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Review/ProductReviewProductIdScopeTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Coupon/CouponDownloadGuardTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/CouponDirectIssueTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Refund/GuestRefundEstimateValidationTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestCancelItemValidationTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Order/GuestConfirmOptionGateTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/SearchPreset/SearchPresetPermissionTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/service-scope-reapply.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/service-scope-reapply.yaml new file mode 100644 index 00000000..6923755d --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/service-scope-reapply.yaml @@ -0,0 +1,38 @@ +feature: 서비스 계층 스코프 게이트 재적용 (KVE-2026-1919 형제) + +description: | + `PermissionMiddleware` 는 라우트 파라미터가 Model 로 resolve 될 때만 스코프를 검사하고, + 아니면 "목록 엔드포인트" 로 보아 건너뛴다. 이 모듈에는 그 스킵이 성립하는 두 형태가 + 모두 있었고, 조사 시점 모듈 전체의 `checkScopeAccess`/`filterByScope` 호출은 0건이었다 — + 미들웨어가 스킵되면 어떤 2차 방어도 없었다는 뜻이다. + + 축은 그 **우회 메커니즘**이다. 두 형태는 노출 범위가 다르므로 한쪽만 검증하면 나머지가 + 무보호로 남는다: + + - static_bulk — `bulk-*` 처럼 리소스 파라미터 자체가 없는 정적 경로. 일괄 경로만 뚫린다. + - param_name_mismatch — 권한의 `resource_route_key` 는 `shippingPolicy`/`coupon` 인데 + 라우트는 `{id}` 라 바인딩이 일어나지 않는다. 이쪽은 **상세 경로까지** 무가드다. + + 공용 trait `ReappliesPermissionScope` 가 두 형태 모두에서 코어 SSoT + (`PermissionHelper::checkScopeAccess` / `filterByScope`)에 판정을 위임한다. 대표 경로 + (상세 1 + 일괄 1)를 고정하며, 나머지 서비스도 같은 trait 을 쓴다. + + 각 형태마다 **차단 축과 과차단 회귀 축을 짝으로** 둔다 — 차단만 단언하면 가드가 정상 + 대상까지 막는 회귀가 green 으로 통과한다. + +axes: + route_shape: [static_bulk, param_name_mismatch] + +effects: + # param_name_mismatch — 상세 경로 + - detail_route_reapplies_scope_gate_when_binding_absent + - detail_route_allows_in_scope_target + # static_bulk — 일괄 경로 + - static_bulk_route_reapplies_scope_gate + - static_bulk_route_allows_global_scoped_actor + # HTTP 표면 — 컨트롤러 typed catch (거부가 500 으로 위장되지 않는다) + - scope_denial_surfaces_as_403_over_http + - missing_target_keeps_not_found_contract + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Security/ServiceScopeReapplyTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/settings-in-use-deletion-exception-status.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/settings-in-use-deletion-exception-status.yaml new file mode 100644 index 00000000..5f507ab7 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/settings-in-use-deletion-exception-status.yaml @@ -0,0 +1,60 @@ +feature: 사용 중인 배송유형·택배사 삭제의 예외 → 상태코드 매핑 (Settings In-Use Deletion Exception Status) + +description: | + 관리자 배송설정 화면은 항목을 지우는 전용 엔드포인트를 갖지 않는다 — 저장 payload + 에서 그 항목을 빼면 서비스가 삭제 대상으로 판정한다. 그런데 그 항목이 주문에서 + 사용 중이면 서비스가 도메인 실패를 던지고, 컨트롤러가 그것을 광역 catch 로 받아 + 500 "설정 저장 중 오류" 로 뭉개면 운영자는 **왜** 못 지우는지 알 수 없다. + + 핵심 설계: + - 사용 중 사유 → **400 + 사유 문구**(사용 중인 이름·건수). 일반 저장 실패 문구로 + 대체되지 않아야 하고, 삭제는 롤백되어야 한다. + - 그 외 예외 → 500, 응답 본문에 예외 원문(SQLSTATE 등) 미포함. + + 배송유형과 택배사는 사용량 집계 호출 지점이 각각 `countByShippingType`·`countByCarrierId` + 로 다르다. 한쪽만 단언하면 나머지 경로의 뭉개기가 그대로 남으므로 `resource` 를 축으로 + 세워 네 칸을 모두 덮는다. + + 인프라 500 축은 Repository mock 으로만 유발할 수 있어 PHPUnit 이 SSoT 다. + 브라우저에서는 사용 중 사유가 먼저 걸려 재현되지 않는다. + +axes: + resource: [shipping_type, shipping_carrier] + error_class: [domain, infrastructure] + +exclusions: [] + +effects: + # 도메인 사유 — 사유가 담긴 400, 롤백 + - in_use_shipping_type_deletion_returns_400_with_reason + - in_use_shipping_carrier_deletion_returns_400_with_reason + # 인프라 예외 — 500 으로 구분, 원문 미노출 + - settings_save_infrastructure_exception_returns_500 + - settings_save_carrier_infrastructure_exception_returns_500 + +scenarios: + - id: in_use_deletion_tells_the_operator_why + description: 사용 중인 항목을 지우려 하면 사용 중이라는 사유가 담긴 400 이 돌아오고, 일반 저장 실패 문구로 대체되지 않으며 삭제는 롤백된다 + effects: + - in_use_shipping_type_deletion_returns_400_with_reason + - in_use_shipping_carrier_deletion_returns_400_with_reason + + - id: settings_save_infrastructure_failure_is_not_disguised_as_input_error + description: 사용량 집계 중 인프라 예외가 나면 두 자원 경로 모두 500 으로 구분되고 응답 본문에 예외 원문이 실리지 않는다 + effects: + - settings_save_infrastructure_exception_returns_500 + - settings_save_carrier_infrastructure_exception_returns_500 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/EcommerceSettingsInUseDeletionTest.php + +notes: | + 이 매니페스트는 5차 감사에서 신설됐다. 대상 테스트는 브랜치가 추가하면서 `@scenario`· + `@effects` 마커까지 붙였는데, 그 effect 이름을 선언한 매니페스트가 어디에도 없었다. + 선언이 아예 없는 effect 는 test-scenario-coverage 의 셀 대상이 되지 않아 조용히 + 통과한다 — 3차 감사가 `OrderDomainExceptionStatusPairTest` 에서 지적한 것과 같은 + 사각이 같은 브랜치 안에 하나 더 남아 있었다. + + 같은 라운드에 `resource=shipping_carrier, error_class=infrastructure` 칸의 테스트도 + 함께 추가했다. 그 칸을 exclusion 으로 처리하지 않은 것은, 두 자원의 집계 호출 지점이 + 실제로 다르기 때문이다. 여기서 제외 사유를 적었다면 그 사유가 곧 거짓이 된다. diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/shipping-policy-resolver-cache.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/shipping-policy-resolver-cache.yaml new file mode 100644 index 00000000..75baffff --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/shipping-policy-resolver-cache.yaml @@ -0,0 +1,29 @@ +feature: 기본 배송정책 리졸버의 요청 내 캐시 무효화 + +description: | + `ShippingPolicyResolver` 는 기본 배송정책(`is_default = true`) 행을 요청 안에서 한 번만 + 조회해 들고 있다. 그 캐시를 비우는 지점이 없으면, 같은 요청에서 기본 정책을 바꾼 뒤의 + 계산이 옛 정책으로 수행된다 — 배송비만 틀린 주문이 조용히 생긴다. + + 무효화 트리거는 **설정 저장이 아니라 배송정책 CRUD** 다. 이 리졸버가 캐시하는 것은 설정 + 값이 아니라 DB 행이기 때문이다. 설정 저장에 걸어 두면 정작 정책을 바꿨을 때 캐시가 남고, + 설정만 바꿨을 때는 쓸데없이 비워진다. + + 무효화는 화면·서비스마다 복제하지 않고 리스너 하나가 관련 훅 전부를 구독해 수행한다. + 구독 목록에서 훅 하나가 빠지면 그 경로에서만 옛 정책이 살아남는다. + +axis_notes: + mutation: | + 기본 정책이 바뀔 수 있는 경로 전부. hook_subscription 은 개별 경로가 아니라 + "리스너가 구독한 훅 집합이 정책 변경 훅 전부를 덮는가" 를 본다 — 경로마다 테스트를 + 복제하는 대신 구독 목록 자체를 고정한다. + +axes: + mutation: [none, manual_flush, set_default, update, delete, hook_subscription] + +effects: + - default_policy_cached_within_request # 변경이 없으면 요청 내 재조회 없음 + - resolver_cache_flushed # 정책이 바뀌면 다음 조회가 새 행을 본다 + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/ShippingPolicyResolverCacheTest.php diff --git a/modules/_bundled/sirsoft-ecommerce/tests/scenarios/temp-image-cleanup.yaml b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/temp-image-cleanup.yaml new file mode 100644 index 00000000..8297a7bf --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/tests/scenarios/temp-image-cleanup.yaml @@ -0,0 +1,66 @@ +feature: 상품 이미지 잔존물 회수 (Temp Product Image Cleanup + Category Delete) + +description: | + 이커머스의 이미지 잔존물 두 갈래를 회수한다. 공개 이슈 #115 동류 결함. + + 1. 미연결 임시 상품 이미지 — 상품 등록 폼에서 올린 뒤 저장 없이 이탈해 `temp_key` 만 + 남고 `product_id` 가 비어 있는 행. 연결 시점에 본경로로 옮겨지므로 오탐 여지가 없다. + 운영 데이터가 아닌 폼 세션 부산물이라 토글 없이 상시 실행된다. + 2. 카테고리 삭제 시 이미지 파일 잔존 (버그) — `$category->images()->delete()` 로 DB 행만 + 지워 파일이 디스크에 영구 잔존했다. 같은 모듈의 상품 삭제는 파일까지 지우므로 두 경로가 + 비대칭이었다. 파일 → 행 순서로 정리하도록 교정했다(ProductService 선례와 동일 순서). + +axes: + image_state: [temp_unlinked, linked_to_product] + age: [within_retention, past_retention] + +exclusions: + - { image_state: linked_to_product, age: within_retention, reason: "연결된 이미지는 나이와 무관하게 대상이 아니다 — 경과분 1건으로 충분" } + +sub_flows: + - id: dry_run + description: 판정만 수행하고 파일·기록을 건드리지 않는 미리보기 경로 + effects: + - ecommerce_temp_prune_dry_run + - id: retention_guard + description: 보존기간 1일 미만 자기 차단 (방금 올린 임시 이미지까지 지우는 사고 방지) + effects: + - ecommerce_temp_prune_days_guard + - id: temp_directory_hygiene + description: | + 파일을 지운 뒤 남는 temp 디렉토리 처리. 비워졌을 때만 지우고, 다른 폼 세션의 파일이 + 남아 있으면 그대로 둔다 (디렉토리째 지우면 아직 작성 중인 상품의 이미지가 사라진다). + effects: + - ecommerce_temp_prune_removes_empty_directory + - ecommerce_temp_prune_keeps_directory_with_remaining_files + - id: category_delete + description: | + 카테고리 삭제가 이미지 파일까지 지우는지 (회귀). 행마다 disk 가 다른 혼재 상태에서도 + 각 행의 실제 저장 위치를 향해 지워야 한다. + effects: + - category_delete_removes_image_files + - category_delete_removes_image_rows + - id: category_image_remove_ui + description: | + 카테고리 폼에서 이미지를 뺐을 때 폼 상태에서도 빠지는지. FileUploader 는 hash 우선으로 + 식별자를 넘기므로 id 로만 걸러내면 서버에서는 지워졌는데 폼에는 남는다. + effects: + - category_image_remove_filters_by_hash + - category_image_remove_falls_back_to_id + - id: reference_source_registry + description: 상품 설명·공통 안내를 에디터 이미지 참조 소스로 등록하는 리스너 + effects: + - reference_source_listener_subscribes_as_filter + - reference_source_listener_appends_without_dropping + - reference_source_listener_schema_contract + +effects: + - ecommerce_temp_prune_deletes_file_and_record + - ecommerce_temp_prune_keeps_within_retention + - ecommerce_temp_prune_skips_linked + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneTempProductImagesCommandTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/CategoryDeleteImageFileTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminCategoryImageRemoveExpression.test.ts diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/Upgrade_1_1_1.php b/modules/_bundled/sirsoft-ecommerce/upgrades/Upgrade_1_1_1.php new file mode 100644 index 00000000..f7680db1 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/Upgrade_1_1_1.php @@ -0,0 +1,27 @@ +<?php + +namespace Modules\Sirsoft\Ecommerce\Upgrades; + +use App\Extension\AbstractUpgradeStep; + +/** + * Ecommerce 모듈 1.1.1 업그레이드 스텝 + * + * 공개 #94 (배송비 구간 경계·단위) 후속 데이터 정리. + * + * 1. 연속형 구간(금액/무게/부피/부피무게)의 시작값을 직전 구간 종료값으로 정규화한다. + * 구(舊) 규칙(다음 min = max + 1)으로 저장된 정책은 계산에는 영향이 없지만 + * (사다리 매칭은 시작값을 쓰지 않는다) 관리자 화면 요약이 "10001~30000" 처럼 표시된다. + * 2. 총 무게/부피가 0 으로 고정 기록된 기존 주문을 주문 옵션 소계 합으로 백필한다. + * + * 공개 #106/#107 (상품 문의 답변 사슬) 후속 데이터 정리: + * + * 3. 과거 문의 삭제로 홀로 남은 답변 글(고아 답변)을 소프트 삭제한다. + * 4. 질문 글이 삭제됐는데 살아남은 문의 피벗을 소프트 삭제한다 (큐 비동기 유실 잔재). + * 5. 살아있는 답변이 없는데 「답변완료」로 남은 문의를 미답변으로 정정한다. + * + * 모든 비즈니스 로직은 data/1.1.1/migrations/ 로 격리(AbstractUpgradeStep 규약). + * + * @upgrade-path B + */ +class Upgrade_1_1_1 extends AbstractUpgradeStep {} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/01_NormalizeRangeTierMin.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/01_NormalizeRangeTierMin.php new file mode 100644 index 00000000..a6ae9c18 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/01_NormalizeRangeTierMin.php @@ -0,0 +1,136 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +/** + * 구간별 배송비의 구간 시작값을 현재 계약으로 정규화합니다. + * + * 시작값은 표시 전용이며 직전 구간의 종료값에서 파생됩니다 — 첫 구간은 0, 이후 구간은 + * 수량이면 `직전 max + 1`, 금액·무게·부피면 `직전 max` 입니다. 배송비 계산은 종료값 + * 기준 사다리 매칭이라 시작값과 무관하지만, 화면에서 시작값이 읽기전용이 된 이상 + * 저장 데이터가 규칙과 어긋나 있으면 운영자가 그 정책을 다시 저장할 수 없습니다 + * (첫 구간 시작값이 1 인 기존 데이터가 그대로 거부됨). + * + * 종료값과 배송비는 손대지 않습니다 — 계산 결과가 바뀌지 않습니다. + * + * idempotent: 이미 정규화된 정책은 변경 대상이 없어 no-op. + * V-1 안전: Illuminate\Support\Facades\* 만 사용 — 모듈 Enum/Model 을 참조하지 않는다. + */ +class NormalizeRangeTierMin implements DataMigration +{ + private const TABLE = 'ecommerce_shipping_policy_country_settings'; + + /** + * 이산형(다음 시작값 = 직전 종료값 + 1) 부과정책. + * + * @var array<int, string> + */ + private const DISCRETE_POLICIES = ['range_quantity']; + + /** + * 연속형(다음 시작값 = 직전 종료값) 부과정책. + * + * @var array<int, string> + */ + private const CONTINUOUS_POLICIES = [ + 'range_amount', + 'range_weight', + 'range_volume', + 'range_volume_weight', + ]; + + public function name(): string + { + return 'NormalizeRangeTierMin'; + } + + public function run(UpgradeContext $context): void + { + if (! Schema::hasTable(self::TABLE) || ! Schema::hasColumn(self::TABLE, 'ranges')) { + $context->logger->warning('[ecommerce:1.1.1] '.self::TABLE.'.ranges 미존재 — 스킵'); + + return; + } + + $normalized = 0; + + DB::table(self::TABLE) + ->whereIn('charge_policy', array_merge(self::DISCRETE_POLICIES, self::CONTINUOUS_POLICIES)) + ->whereNotNull('ranges') + ->orderBy('id') + ->chunkById(100, function ($rows) use (&$normalized, $context) { + foreach ($rows as $row) { + $ranges = json_decode((string) $row->ranges, true); + + if (! is_array($ranges) || ! isset($ranges['tiers']) || ! is_array($ranges['tiers'])) { + continue; + } + + $isDiscrete = in_array($row->charge_policy, self::DISCRETE_POLICIES, true); + $tiers = array_values($ranges['tiers']); + $changed = false; + + foreach ($tiers as $index => $tier) { + if (! is_array($tier)) { + continue; + } + + if ($index === 0) { + $expectedMin = 0; + } else { + $previousMax = $this->resolveMax($tiers[$index - 1] ?? []); + + if ($previousMax === null) { + // 직전 구간의 종료값이 없으면 파생 근거가 없다 — 손대지 않는다 + continue; + } + + $expectedMin = $previousMax + ($isDiscrete ? 1 : 0); + } + + if ((float) ($tier['min'] ?? 0) !== (float) $expectedMin) { + $tiers[$index]['min'] = $expectedMin; + $changed = true; + } + } + + if (! $changed) { + continue; + } + + $ranges['tiers'] = $tiers; + + DB::table(self::TABLE) + ->where('id', $row->id) + ->update(['ranges' => json_encode($ranges, JSON_UNESCAPED_UNICODE)]); + + $normalized++; + $context->logger->info("[ecommerce:1.1.1] 국가별 설정 #{$row->id} 구간 시작값 정규화"); + } + }); + + $context->logger->info("[ecommerce:1.1.1] 구간 시작값 정규화 완료: {$normalized} 건"); + } + + /** + * 구간의 종료값을 반환합니다 (무제한이면 null). + * + * @param array $tier 구간 정의 + * @return float|null 종료값 + */ + private function resolveMax(array $tier): ?float + { + $max = $tier['max'] ?? null; + + if ($max === null || $max === '') { + return null; + } + + return (float) $max; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/02_BackfillOrderTotalWeightVolume.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/02_BackfillOrderTotalWeightVolume.php new file mode 100644 index 00000000..a84566e5 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/02_BackfillOrderTotalWeightVolume.php @@ -0,0 +1,88 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +/** + * 주문의 총 무게/부피를 주문 옵션 소계 합으로 백필합니다. + * + * 주문 생성 시 `total_weight`/`total_volume` 이 0 으로 고정 기록되어, + * 배송사 연동·운임 정산이 이 값을 읽으면 무게 없는 주문으로 취급됐다. + * 옵션에는 주문 시점 소계(subtotal_weight/subtotal_volume)가 남아 있으므로 + * 그 합으로 복원한다. + * + * 0 인 행만 대상으로 하며(운영자가 이후 보정한 값은 보존), 두 값이 모두 0 인 + * 주문(무게 미입력 상품만 담긴 주문)은 갱신 결과도 0 이라 재실행해도 동일하다. + * + * V-1 안전: Illuminate\Support\Facades\* 만 사용 — 모듈 Model 을 참조하지 않는다. + */ +class BackfillOrderTotalWeightVolume implements DataMigration +{ + private const ORDERS_TABLE = 'ecommerce_orders'; + + private const OPTIONS_TABLE = 'ecommerce_order_options'; + + public function name(): string + { + return 'BackfillOrderTotalWeightVolume'; + } + + public function run(UpgradeContext $context): void + { + if (! Schema::hasTable(self::ORDERS_TABLE) || ! Schema::hasTable(self::OPTIONS_TABLE)) { + $context->logger->warning('[ecommerce:1.1.1] 주문/주문옵션 테이블 미존재 — 스킵'); + + return; + } + + foreach (['total_weight', 'total_volume'] as $column) { + if (! Schema::hasColumn(self::ORDERS_TABLE, $column)) { + $context->logger->warning('[ecommerce:1.1.1] '.self::ORDERS_TABLE.".{$column} 미존재 — 스킵"); + + return; + } + } + + $backfilled = 0; + + DB::table(self::ORDERS_TABLE) + ->where(function ($query) { + $query->where('total_weight', 0)->orWhereNull('total_weight'); + }) + ->where(function ($query) { + $query->where('total_volume', 0)->orWhereNull('total_volume'); + }) + ->orderBy('id') + ->select('id') + ->chunkById(200, function ($orders) use (&$backfilled) { + foreach ($orders as $order) { + $totals = DB::table(self::OPTIONS_TABLE) + ->where('order_id', $order->id) + ->selectRaw('COALESCE(SUM(subtotal_weight), 0) as weight_sum, COALESCE(SUM(subtotal_volume), 0) as volume_sum') + ->first(); + + $weight = (float) ($totals->weight_sum ?? 0); + $volume = (float) ($totals->volume_sum ?? 0); + + if ($weight <= 0 && $volume <= 0) { + continue; + } + + DB::table(self::ORDERS_TABLE) + ->where('id', $order->id) + ->update([ + 'total_weight' => $weight, + 'total_volume' => $volume, + ]); + + $backfilled++; + } + }); + + $context->logger->info("[ecommerce:1.1.1] 주문 총 무게/부피 백필 완료: {$backfilled} 건"); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/03_BackfillOrderForeignKeyColumnComments.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/03_BackfillOrderForeignKeyColumnComments.php new file mode 100644 index 00000000..01a149f1 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/03_BackfillOrderForeignKeyColumnComments.php @@ -0,0 +1,178 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; + +/** + * 주문/주문옵션 외래키 컬럼의 누락된 한국어 comment 를 채웁니다. + * + * 1.0.5 의 같은 보정에서 빠져 있던 컬럼들입니다. 마이그레이션 소스는 교정했지만, + * 이미 설치를 마친 사이트는 create 마이그레이션을 다시 실행하지 않으므로 + * 설명이 비어 있는 채로 남습니다. + * + * 멱등: 설명이 이미 있는 컬럼은 건드리지 않습니다. 재실행해도 결과가 같습니다. + * + * 안전: + * - 설명이 **비어 있을 때만** 채웁니다 — 운영자가 직접 넣어 둔 설명을 덮어쓰지 않습니다. + * - 자료형·NULL 허용·기본값·자동증가를 현재 스키마에서 읽어 그대로 재적용하므로 설명 외에는 + * 아무 것도 바뀌지 않습니다. + * - MySQL 계열에서만 동작합니다(다른 DB 는 건너뜁니다). + * + * 실패 정책: 컬럼 단위로 실패를 흡수합니다 — 한 컬럼이 실패해도 나머지를 계속 처리합니다. + * + * V-1 안전: `Illuminate\Support\Facades\DB` 와 로컬 private 헬퍼만 사용하고, 대상 목록을 + * 본 클래스에 동결 상수로 둡니다(마이그레이션 파일·모델 미참조). + */ +class BackfillOrderForeignKeyColumnComments implements DataMigration +{ + /** + * 보정 대상 (1.1.1 시점 동결) — 테이블 => [컬럼 => 설명]. + */ + private const TARGETS = [ + 'ecommerce_orders' => [ + 'user_id' => '주문자 회원 ID (비회원 주문은 null)', + ], + 'ecommerce_order_options' => [ + 'order_id' => '소속 주문 ID', + 'product_id' => '주문 시점 상품 ID', + 'product_option_id' => '주문 시점 상품 옵션 ID', + ], + ]; + + /** + * 마이그레이션 식별자 (로그용). + * + * @return string 식별자 + */ + public function name(): string + { + return 'BackfillOrderForeignKeyColumnComments'; + } + + /** + * 비어 있는 외래키 컬럼 설명을 채웁니다. + * + * @param UpgradeContext $context 업그레이드 컨텍스트 + */ + public function run(UpgradeContext $context): void + { + if (! in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) { + $context->logger->info('[ecommerce:1.1.1] 컬럼 설명 보정 — MySQL 계열이 아니어서 건너뜁니다'); + + return; + } + + $filled = 0; + $skipped = 0; + $failed = 0; + + foreach (self::TARGETS as $table => $columns) { + $prefixed = $context->table($table); + + foreach ($columns as $column => $comment) { + try { + $meta = $this->columnMeta($prefixed, $column); + + if ($meta === null || trim((string) $meta->COLUMN_COMMENT) !== '') { + $skipped++; + + continue; + } + + DB::statement($this->buildModifyStatement($prefixed, $column, $meta, $comment)); + $filled++; + } catch (\Throwable $e) { + $failed++; + $context->logger->warning(sprintf( + '[ecommerce:1.1.1] 컬럼 설명 보정 실패 (계속 진행): %s.%s — %s', + $prefixed, + $column, + $e->getMessage(), + )); + } + } + } + + $context->logger->info(sprintf( + '[ecommerce:1.1.1] 주문 외래키 컬럼 설명 보정 — 채움 %d건 / 대상 아님 %d건 / 실패 %d건', + $filled, + $skipped, + $failed, + )); + } + + /** + * 컬럼의 현재 스키마 메타데이터를 조회합니다. + * + * @param string $table 프리픽스가 적용된 테이블명 + * @param string $column 컬럼명 + * @return object|null 컬럼 메타 (테이블/컬럼 부재 시 null) + */ + private function columnMeta(string $table, string $column): ?object + { + $rows = DB::select( + 'SELECT COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA, COLUMN_COMMENT + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?', + [$table, $column], + ); + + return $rows[0] ?? null; + } + + /** + * 설명만 덧붙이는 MODIFY COLUMN 문을 조립합니다. + * + * @param string $table 프리픽스가 적용된 테이블명 + * @param string $column 컬럼명 + * @param object $meta columnMeta() 결과 + * @param string $comment 넣을 설명 + * @return string 실행할 SQL + */ + private function buildModifyStatement(string $table, string $column, object $meta, string $comment): string + { + $sql = sprintf( + 'ALTER TABLE %s MODIFY COLUMN %s %s', + $this->quoteIdentifier($table), + $this->quoteIdentifier($column), + $meta->COLUMN_TYPE, + ); + + $sql .= $meta->IS_NULLABLE === 'YES' ? ' NULL' : ' NOT NULL'; + + if ($meta->COLUMN_DEFAULT !== null) { + $sql .= ' DEFAULT '.$this->quoteValue((string) $meta->COLUMN_DEFAULT); + } + + if (stripos(trim((string) $meta->EXTRA), 'auto_increment') !== false) { + $sql .= ' AUTO_INCREMENT'; + } + + return $sql.' COMMENT '.$this->quoteValue($comment); + } + + /** + * 식별자를 백틱으로 감쌉니다. + * + * @param string $identifier 테이블/컬럼명 + * @return string 이스케이프된 식별자 + */ + private function quoteIdentifier(string $identifier): string + { + return '`'.str_replace('`', '``', $identifier).'`'; + } + + /** + * 문자열 값을 SQL 리터럴로 인용합니다. + * + * @param string $value 값 + * @return string 인용된 리터럴 + */ + private function quoteValue(string $value): string + { + return DB::connection()->getPdo()->quote($value); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/04_SoftDeleteOrphanInquiryReplyPosts.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/04_SoftDeleteOrphanInquiryReplyPosts.php new file mode 100644 index 00000000..0e559f0b --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/04_SoftDeleteOrphanInquiryReplyPosts.php @@ -0,0 +1,134 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Schema; + +/** + * 과거 문의 삭제로 홀로 남은 답변 글(고아 답변)을 소프트 삭제합니다. + * + * 종전 결함(#107): 문의 삭제 시 질문 글만 소프트 삭제되고 답변 글은 published 로 + * 잔존해, 직접 URL 로 비회원도 열람 가능한 고아 답변이 쌓였다. 대상은 문의 게시판의 + * `parent_id NOT NULL AND deleted_at IS NULL` 이면서 부모가 없거나(하드 삭제) 부모가 + * 삭제된 답변 글이다. + * + * 문의 게시판 판별은 두 축의 합집합이다 — 설정 파일의 board_slug 해석 ∪ 문의 피벗이 + * 실제로 가리키는 Post 의 board_id (설정 변경/유실 사이트 보강). + * + * trigger_type 은 변경하지 않는다 — 백필된 고아는 부모 복원 시 되살리지 않는 + * 보수적 선택이다(cascade 마킹이 없으면 복원 경로가 건드리지 않는다). + * + * 멱등: deleted_at IS NULL 필터로 재실행 시 0건. + * V-1 안전: DB/Schema/File Facade 만 사용 — 모듈 클래스를 참조하지 않는다. + */ +class SoftDeleteOrphanInquiryReplyPosts implements DataMigration +{ + private const POSTS_TABLE = 'board_posts'; + + private const BOARDS_TABLE = 'boards'; + + private const PIVOT_TABLE = 'ecommerce_product_inquiries'; + + private const POST_MODEL = 'Modules\\Sirsoft\\Board\\Models\\Post'; + + public function name(): string + { + return 'SoftDeleteOrphanInquiryReplyPosts'; + } + + public function run(UpgradeContext $context): void + { + if (! Schema::hasTable(self::POSTS_TABLE) || ! Schema::hasTable(self::PIVOT_TABLE)) { + $context->logger->warning('[ecommerce:1.1.1] 게시글/문의 피벗 테이블 미존재 — 스킵'); + + return; + } + + $boardIds = $this->resolveInquiryBoardIds($context); + + if ($boardIds === []) { + $context->logger->info('[ecommerce:1.1.1] 문의 게시판을 판별할 수 없음 — 고아 답변 정리 스킵'); + + return; + } + + $now = now(); + $cleaned = 0; + + DB::table(self::POSTS_TABLE.' as replies') + ->whereIn('replies.board_id', $boardIds) + ->whereNotNull('replies.parent_id') + ->whereNull('replies.deleted_at') + ->where(function ($query) { + // 부모가 없거나(하드 삭제) 부모가 소프트 삭제된 답변만 대상 + $query->whereNotExists(function ($sub) { + $sub->selectRaw('1') + ->from(self::POSTS_TABLE.' as parents') + ->whereColumn('parents.id', 'replies.parent_id') + ->whereNull('parents.deleted_at'); + }); + }) + ->orderBy('replies.id') + ->select('replies.id') + ->chunkById(500, function ($rows) use (&$cleaned, $now) { + $ids = $rows->pluck('id')->all(); + + $cleaned += DB::table(self::POSTS_TABLE) + ->whereIn('id', $ids) + ->whereNull('deleted_at') + ->update([ + 'status' => 'deleted', + 'deleted_at' => $now, + ]); + }, 'replies.id', 'id'); + + $context->logger->info("[ecommerce:1.1.1] 고아 답변 글 {$cleaned}건 소프트 삭제 완료", [ + 'board_ids' => $boardIds, + ]); + } + + /** + * 문의 게시판 ID 집합을 판별합니다 (설정 board_slug ∪ 피벗 실데이터). + * + * @param UpgradeContext $context 업그레이드 컨텍스트 + * @return array<int> 게시판 ID 배열 + */ + private function resolveInquiryBoardIds(UpgradeContext $context): array + { + $boardIds = []; + + // ① 설정 파일의 board_slug → boards.id 해석 + $settingsPath = storage_path('app/modules/sirsoft-ecommerce/settings/inquiry.json'); + + if (File::exists($settingsPath) && Schema::hasTable(self::BOARDS_TABLE)) { + $settings = json_decode(File::get($settingsPath), true); + $slug = is_array($settings) ? ($settings['board_slug'] ?? null) : null; + + if (is_string($slug) && $slug !== '') { + $id = DB::table(self::BOARDS_TABLE)->where('slug', $slug)->value('id'); + + if ($id !== null) { + $boardIds[] = (int) $id; + } + } + } + + // ② 피벗이 실제로 가리키는 Post 의 board_id (설정 변경/유실 사이트 보강) + $pivotBoardIds = DB::table(self::PIVOT_TABLE.' as pivots') + ->join(self::POSTS_TABLE.' as posts', 'posts.id', '=', 'pivots.inquirable_id') + ->where('pivots.inquirable_type', self::POST_MODEL) + ->distinct() + ->pluck('posts.board_id') + ->all(); + + $boardIds = array_values(array_unique(array_merge($boardIds, array_map('intval', $pivotBoardIds)))); + + $context->logger->info('[ecommerce:1.1.1] 문의 게시판 판별 결과', ['board_ids' => $boardIds]); + + return $boardIds; + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/05_SoftDeleteInquiryPivotsForTrashedQuestionPosts.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/05_SoftDeleteInquiryPivotsForTrashedQuestionPosts.php new file mode 100644 index 00000000..bdc4244e --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/05_SoftDeleteInquiryPivotsForTrashedQuestionPosts.php @@ -0,0 +1,76 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +/** + * 질문 글이 삭제됐는데 살아남은 문의 피벗을 소프트 삭제합니다. + * + * 종전 결함(N3): 게시판 직권 삭제 시 피벗 정리 리스너가 큐 비동기로 등록되어, + * 큐 워커 미가동 환경에서는 피벗 정리가 조용히 누락됐다. 그 잔재 — 질문 Post 는 + * 소프트 삭제(또는 하드 삭제로 부재)인데 피벗은 살아있는 행 — 를 정리한다. + * + * 04 스텝(고아 답변 정리)이 먼저 실행된 뒤 수행되어야 한다 (실행 순서 = 파일명 prefix). + * + * 멱등: 피벗 deleted_at IS NULL 필터로 재실행 시 0건. + * V-1 안전: DB/Schema Facade 만 사용 — 모듈 클래스를 참조하지 않는다. + */ +class SoftDeleteInquiryPivotsForTrashedQuestionPosts implements DataMigration +{ + private const POSTS_TABLE = 'board_posts'; + + private const PIVOT_TABLE = 'ecommerce_product_inquiries'; + + private const POST_MODEL = 'Modules\\Sirsoft\\Board\\Models\\Post'; + + public function name(): string + { + return 'SoftDeleteInquiryPivotsForTrashedQuestionPosts'; + } + + public function run(UpgradeContext $context): void + { + if (! Schema::hasTable(self::POSTS_TABLE) || ! Schema::hasTable(self::PIVOT_TABLE)) { + $context->logger->warning('[ecommerce:1.1.1] 게시글/문의 피벗 테이블 미존재 — 스킵'); + + return; + } + + if (! Schema::hasColumn(self::PIVOT_TABLE, 'deleted_at')) { + // 피벗 SoftDeletes 컬럼은 정규 마이그레이션이 추가한다 — 아직 없으면 스킵 + $context->logger->warning('[ecommerce:1.1.1] 피벗 deleted_at 컬럼 미존재 — 스킵 (마이그레이션 선행 필요)'); + + return; + } + + $now = now(); + $cleaned = 0; + + DB::table(self::PIVOT_TABLE.' as pivots') + ->where('pivots.inquirable_type', self::POST_MODEL) + ->whereNull('pivots.deleted_at') + ->whereNotExists(function ($sub) { + // 질문 Post 가 살아있는 피벗만 생존 허용 — 부재(하드 삭제)/소프트 삭제 모두 정리 대상 + $sub->selectRaw('1') + ->from(self::POSTS_TABLE.' as posts') + ->whereColumn('posts.id', 'pivots.inquirable_id') + ->whereNull('posts.deleted_at'); + }) + ->orderBy('pivots.id') + ->select('pivots.id') + ->chunkById(500, function ($rows) use (&$cleaned, $now) { + $ids = $rows->pluck('id')->all(); + + $cleaned += DB::table(self::PIVOT_TABLE) + ->whereIn('id', $ids) + ->whereNull('deleted_at') + ->update(['deleted_at' => $now]); + }, 'pivots.id', 'id'); + + $context->logger->info("[ecommerce:1.1.1] 질문 글 부재 문의 피벗 {$cleaned}건 소프트 삭제 완료"); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/06_UnmarkAnsweredInquiriesWithoutLiveReplies.php b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/06_UnmarkAnsweredInquiriesWithoutLiveReplies.php new file mode 100644 index 00000000..7868e5b1 --- /dev/null +++ b/modules/_bundled/sirsoft-ecommerce/upgrades/data/1.1.1/migrations/06_UnmarkAnsweredInquiriesWithoutLiveReplies.php @@ -0,0 +1,78 @@ +<?php + +namespace App\Upgrades\Data\Ext\Modules\SirsoftEcommerce\V1_1_1\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +/** + * 살아있는 답변이 없는데 「답변완료」로 남은 문의를 미답변으로 정정합니다. + * + * 종전 결함(N4): 게시판 관리자가 답변 글을 직접 삭제해도 문의 피벗의 + * `is_answered` 가 해제되지 않아, 답변이 없는데 「답변완료」로 표기됐다. + * 04 스텝의 고아 답변 정리로 답변이 사라진 문의도 이 스텝이 정합화한다 + * (실행 순서 = 파일명 prefix — 04 가 답변을 정리한 뒤 실행되어야 한다). + * + * 살아있는 피벗만 대상으로 한다 — 05 에서 소프트 삭제된 피벗은 화면에 + * 노출되지 않으므로 답변완료 플래그를 건드릴 필요가 없다. + * + * 멱등: is_answered=1 필터로 재실행 시 0건. + * V-1 안전: DB/Schema Facade 만 사용 — 모듈 클래스를 참조하지 않는다. + */ +class UnmarkAnsweredInquiriesWithoutLiveReplies implements DataMigration +{ + private const POSTS_TABLE = 'board_posts'; + + private const PIVOT_TABLE = 'ecommerce_product_inquiries'; + + private const POST_MODEL = 'Modules\\Sirsoft\\Board\\Models\\Post'; + + public function name(): string + { + return 'UnmarkAnsweredInquiriesWithoutLiveReplies'; + } + + public function run(UpgradeContext $context): void + { + if (! Schema::hasTable(self::POSTS_TABLE) || ! Schema::hasTable(self::PIVOT_TABLE)) { + $context->logger->warning('[ecommerce:1.1.1] 게시글/문의 피벗 테이블 미존재 — 스킵'); + + return; + } + + $query = DB::table(self::PIVOT_TABLE.' as pivots') + ->where('pivots.inquirable_type', self::POST_MODEL) + ->where('pivots.is_answered', 1) + ->whereNotExists(function ($sub) { + // 살아있는 답변(자식 Post)이 하나라도 있으면 답변완료 유지 + $sub->selectRaw('1') + ->from(self::POSTS_TABLE.' as replies') + ->whereColumn('replies.parent_id', 'pivots.inquirable_id') + ->whereNull('replies.deleted_at'); + }); + + if (Schema::hasColumn(self::PIVOT_TABLE, 'deleted_at')) { + $query->whereNull('pivots.deleted_at'); + } + + $fixed = 0; + + $query->orderBy('pivots.id') + ->select('pivots.id') + ->chunkById(500, function ($rows) use (&$fixed) { + $ids = $rows->pluck('id')->all(); + + $fixed += DB::table(self::PIVOT_TABLE) + ->whereIn('id', $ids) + ->where('is_answered', 1) + ->update([ + 'is_answered' => 0, + 'answered_at' => null, + ]); + }, 'pivots.id', 'id'); + + $context->logger->info("[ecommerce:1.1.1] 답변 없는 답변완료 문의 {$fixed}건 정정 완료"); + } +} diff --git a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json index 0d6fc6d0..96fd99b2 100644 --- a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json +++ b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.json @@ -1,11 +1,11 @@ { "schema_version": "1.0", - "generated_at": "2026-08-06T03:32:02+00:00", + "generated_at": "2026-08-12T14:41:52+00:00", "generator": "g7 vendor-bundle:build", "target": "module:sirsoft-ecommerce", - "composer_json_sha256": "cf5460aed4b90364c33889e3bd479eeb87c05b8f4b1ee94886575e14dcf441d1", + "composer_json_sha256": "c1a7e6cb8fcf68b62af5b7817509e2a2df6cb19348b9951ec8ea30634f3030d3", "composer_lock_sha256": "876ca9c2273a33baff878d25050a567018a946412db053930a7f548c4add595d", - "zip_sha256": "a475756e4c92ad685d324cacb83beab2c8f94e5d29d8b33bf64dd63b12180f67", + "zip_sha256": "afda2275ddb2a403be87062539f2ecc4b2d9136ffa5f85533128cc3eaa2724d9", "zip_size": 435548, "package_count": 1, "php_requirement": "^8.2", diff --git a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip index aec8fc5f..b280f36c 100644 Binary files a/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip and b/modules/_bundled/sirsoft-ecommerce/vendor-bundle.zip differ diff --git a/modules/_bundled/sirsoft-page/CHANGELOG.md b/modules/_bundled/sirsoft-page/CHANGELOG.md index 20baecf8..aef17cfa 100644 --- a/modules/_bundled/sirsoft-page/CHANGELOG.md +++ b/modules/_bundled/sirsoft-page/CHANGELOG.md @@ -4,6 +4,31 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.3] - 2026-08-19 + +### Security + +- 미발행(초안) 페이지에 붙은 이미지 첨부의 미리보기가 주소만 알면 누구에게나 열리던 문제를 수정했습니다. 다운로드는 이미 발행 상태를 확인하고 있었지만 미리보기는 확인하지 않아, 발행 전 콘텐츠의 이미지가 새어 나갈 수 있었습니다. 이제 미리보기도 다운로드와 같은 기준을 적용해, 발행된 페이지의 첨부는 누구나, 미발행 페이지의 첨부는 페이지 조회 권한이 있는 관리자만 미리볼 수 있습니다. 권한이 있는 관리자 화면(편집·상세·미발행 미리보기)의 초안 첨부 썸네일은 한시적으로만 유효한 서명 주소로 전달되어, 보호를 유지한 채 종전처럼 정상 표시됩니다. (KISA 측에서 제보해주셨습니다 — KVE-2026-1914) +- 페이지 일괄 발행/발행취소와 첨부파일 삭제·순서 변경에 담당 범위 제한을 적용했습니다. 페이지를 하나씩 여는 화면에는 이 확인이 있었지만 목록에서 여러 건을 한 번에 처리하는 경로와 첨부 관리 경로에는 없어, "본인이 만든 페이지만" 으로 범위를 좁혀 위임받은 관리자가 담당 밖 페이지까지 발행 상태를 바꾸거나 첨부를 지울 수 있었습니다. 이제 대상마다 같은 기준으로 확인하며, 범위 밖 대상이 섞이면 요청 전체를 거부하고 아무것도 변경하지 않습니다. 범위 제한 없이 위임받은 관리자의 작업은 종전처럼 정상 동작합니다. (KVE-2026-1919) +- 페이지 통합 검색 결과의 제목·본문 미리보기에서 검색어 강조 처리가 원문을 그대로 실어 보내던 것을, 안전하게 변환한 뒤 강조하도록 바로잡았습니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) + +### Added + +- 페이지 작성 중 파일만 올리고 저장하지 않은 채 나간 임시 첨부를 매일 자동 정리합니다. 파일을 지운 뒤 남는 빈 폴더까지 함께 정리하므로 저장소에 흔적이 남지 않습니다. 종전에는 이런 파일이 서버에 계속 쌓여도 회수할 방법이 없었습니다. + +### Changed + +- 첨부 개수 상한 초과 안내 문구를 만드는 시점이 응답을 만드는 시점과 어긋나 있던 것을 맞췄습니다. 표시되는 문구와 상태는 종전과 동일합니다. +- 코어 최소 요구 버전을 7.0.7 로 상향했습니다. + +### Fixed + +- 페이지를 발행하거나 미발행으로 바꿔도(발행 버튼·일괄 발행/미발행) 검색엔진용 미리 생성 캐시와 사이트맵이 갱신되지 않던 문제를 수정했습니다. 편집 화면에서 저장하는 경우에만 갱신이 걸려 있어, 발행 상태만 바꾸면 검색엔진에 이전 상태(미발행 안내 또는 이전 콘텐츠)가 계속 노출되고 사이트맵에도 반영되지 않았습니다. 이제 모든 발행 경로에서 동일하게 갱신됩니다. +- 통합검색에서 페이지 검색이 실패하면 "검색 결과 없음" 이 아니라 오류 안내로 구분해 표시되도록 실패 정보를 응답에 싣습니다. 실패 원인은 서버 기록에 상세히 남습니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 페이지 첨부 파일의 최대 개수·용량·허용 형식 설정이 저장해도 반영되지 않던 문제를 수정했습니다. 설정값을 읽어 오는 경로 자체가 없어 항상 내장 기본값이 쓰였습니다. (#109 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 페이지 목록에서 항목을 선택한 뒤 검색하거나 페이지를 넘기면, 화면에서 사라진 항목이 선택된 채로 남아 일괄 발행·발행 취소 대상에 포함되던 문제를 수정했습니다. 이제 일괄 처리 대상은 언제나 화면에 보이면서 체크된 항목뿐입니다. +- 관리자 페이지 편집 화면에서 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.2] - 2026-08-10 ### Security diff --git a/modules/_bundled/sirsoft-page/composer.json b/modules/_bundled/sirsoft-page/composer.json index 49e93a38..0f070e06 100644 --- a/modules/_bundled/sirsoft-page/composer.json +++ b/modules/_bundled/sirsoft-page/composer.json @@ -2,7 +2,7 @@ "name": "modules/sirsoft-page", "description": "Page module for Gnuboard7", "type": "library", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "autoload": { "psr-4": { diff --git a/modules/_bundled/sirsoft-page/docs/api/attachments.md b/modules/_bundled/sirsoft-page/docs/api/attachments.md index 683e0cd7..df216b2e 100644 --- a/modules/_bundled/sirsoft-page/docs/api/attachments.md +++ b/modules/_bundled/sirsoft-page/docs/api/attachments.md @@ -116,7 +116,7 @@ _단건 응답: FileUploader 컴포넌트 규약에 맞춰 `data.data` 객체에 **설명** 페이지 첨부파일을 업로드합니다. `file`(필수)과 함께 이미 저장된 페이지에 귀속시키려면 `page_id`, 아직 저장 전이면 `temp_key`(임시 귀속 후 페이지 저장 시 `store`/`update` 의 `temp_key` 로 확정)를 보냅니다. `collection` 으로 첨부 그룹을 구분합니다. 응답은 FileUploader 컴포넌트 규약(`data.data`)에 맞춰 `PageAttachmentResource` 를 201 로 반환합니다. 권한은 `pages.create` 를 요구합니다. -업로드 정책(최대 개수 · 최대 용량 · 허용 형식)은 모듈 환경설정 `attachment.*` 가 SSoT 입니다. 용량은 `attachment.max_size_mb`(MB), 형식은 `attachment.allowed_types`(MIME 목록)로 판정하며, 형식 목록을 비우면 형식을 제한하지 않습니다(설정 키 자체가 없으면 모듈 기본 목록으로 폴백). 개수는 `attachment.max_count` 를 기준으로 **직접 업로드 · 임시 업로드(`temp_key`) 연결 · 이미 연결된 첨부** 를 합산해 판정하며, 초과 시 `422`(`errors.code = attachment_limit_exceeded`)를 반환합니다. 이 판정은 업로드 엔드포인트뿐 아니라 페이지 생성·수정 저장 시점에도 동일하게 적용됩니다. `max_count` 가 0 이면 개수를 제한하지 않습니다. +업로드 정책(최대 개수 · 최대 용량 · 허용 형식)은 모듈 환경설정 `attachment.*` 가 SSoT 입니다. 용량은 `attachment.max_size_mb`(MB), 형식은 `attachment.allowed_types`(MIME 목록)로 판정하며, 형식 목록을 비우면 형식을 제한하지 않습니다(설정 키 자체가 없으면 모듈 기본 목록으로 폴백). 개수는 `attachment.max_count` 를 기준으로 **직접 업로드 · 임시 업로드(`temp_key`) 연결 · 이미 연결된 첨부** 를 합산해 판정하며, 초과 시 `422`(`errors.code = attachment_limit_exceeded`)를 반환하며, 안내 문구(`message`)는 허용 개수와 시도한 개수를 담아 응답 시점의 언어로 만들어집니다. 이 판정은 업로드 엔드포인트뿐 아니라 페이지 생성·수정 저장 시점에도 동일하게 적용됩니다. `max_count` 가 0 이면 개수를 제한하지 않습니다. ### PATCH /api/modules/sirsoft-page/admin/attachments/reorder diff --git a/modules/_bundled/sirsoft-page/docs/api/pages.md b/modules/_bundled/sirsoft-page/docs/api/pages.md index b82bacc2..d8dcd6ad 100644 --- a/modules/_bundled/sirsoft-page/docs/api/pages.md +++ b/modules/_bundled/sirsoft-page/docs/api/pages.md @@ -270,7 +270,7 @@ HTTP/1.1 201 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-page.pages.create`)이 없는 경우 | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `slug` 중복/형식 위반(소문자·숫자·하이픈만), `title` 누락 또는 기본 로케일 값 누락, `content_mode` 가 html/text 이외 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `slug` 중복/형식 위반(소문자·숫자·하이픈만), `title` 누락 또는 기본 로케일 값 누락, `content_mode` 가 html/text 이외. 첨부 개수 상한 초과 시에도 422 이며 `errors.code = attachment_limit_exceeded` 와 함께 허용/시도 개수를 담은 안내 문구가 응답 시점의 언어로 내려갑니다 | | 500 | Internal Server Error | 생성 처리 중 예외 (`페이지 생성에 실패했습니다.`) | <!-- @generated:end --> @@ -715,7 +715,7 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-page.pages.update`)이 없거나, 스코프 밖 리소스인 경우 (`auth.scope_denied`) | | 404 | Not Found | `{page}` ID 에 해당하는 페이지가 없는 경우 (`페이지를 찾을 수 없습니다.`) | -| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `title` 누락, `slug` 중복(자기 자신은 제외)/형식 위반, `content_mode` 가 html/text 이외 | +| 422 | Unprocessable Entity | 요청 파라미터가 검증 규칙을 위반한 경우 (`error.errors` 에 필드별 메시지). 대표 케이스: `title` 누락, `slug` 중복(자기 자신은 제외)/형식 위반, `content_mode` 가 html/text 이외. 첨부 개수 상한 초과 시에도 422 이며 `errors.code = attachment_limit_exceeded` 와 함께 허용/시도 개수를 담은 안내 문구가 응답 시점의 언어로 내려갑니다 | | 500 | Internal Server Error | 수정 처리 중 예외 (`페이지 수정에 실패했습니다.`) | <!-- @generated:end --> @@ -1230,11 +1230,13 @@ Content-Disposition: inline | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | -| 404 | Not Found | 해시에 해당하는 첨부가 없는 경우(`첨부파일을 찾을 수 없습니다.`), 첨부가 이미지가 아니거나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`). 미리보기는 발행/미발행과 무관하게 공개 서빙되므로 권한에 의한 404 는 없습니다 | +| 404 | Not Found | 해시에 해당하는 첨부가 없는 경우(`첨부파일을 찾을 수 없습니다.`), 미발행 페이지의 첨부를 `sirsoft-page.pages.read` 권한·유효 서명 없이 요청한 경우(존재를 숨기기 위해 동일하게 404), 첨부가 이미지가 아니거나 스토리지에 실제 파일이 없는 경우(`첨부파일이 스토리지에 존재하지 않습니다.`) | <!-- @generated:end --> -**설명** 이미지 첨부 썸네일을 인라인으로 미리봅니다(해시 기반, 12자). 썸네일 `<img>` 는 토큰을 실을 수 없으므로 발행/미발행과 무관하게 공개 서빙합니다. 미발행 콘텐츠의 썸네일은 해시를 보유해야만 조회 가능(비추측성)하며, 실제 파일 다운로드는 `download` 의 권한 게이트로 보호됩니다. 이미지 스트리밍 응답이므로 실측 대상이 아닙니다. +**설명** 이미지 첨부 썸네일을 인라인으로 미리봅니다(해시 기반, 12자). `download` 와 동일한 발행상태 게이트를 적용합니다 — 발행된 페이지의 썸네일은 누구나, 미발행 페이지의 썸네일은 `sirsoft-page.pages.read` 권한 관리자만 미리볼 수 있습니다. 무인가 사용자에게는 미발행 썸네일이 차단됩니다. + +브라우저 `<img src>` 는 Authorization 헤더를 실을 수 없으므로, 미발행 페이지의 관리자 응답(관리자 상세·미발행 미리보기)에는 `expires`·`signature` 쿼리가 붙은 **한시 서명 URL** 이 `preview_url` 로 직렬화되며, 이 엔드포인트는 유효한 서명을 권한과 동등한 자격으로 허용합니다. 서명이 변조·만료된 요청은 404 로 차단됩니다. 발행 페이지의 `preview_url` 은 종전과 동일한 무서명 공개 경로입니다. 이미지 스트리밍 응답이므로 실측 대상이 아닙니다. ### GET /api/modules/sirsoft-page/pages/{slug} diff --git a/modules/_bundled/sirsoft-page/module.json b/modules/_bundled/sirsoft-page/module.json index f3e4bc5d..76440da8 100644 --- a/modules/_bundled/sirsoft-page/module.json +++ b/modules/_bundled/sirsoft-page/module.json @@ -5,13 +5,13 @@ "ko": "페이지", "en": "Page" }, - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "description": { "ko": "정적 페이지(정보/정책/안내) 관리 모듈", "en": "Static page (info/policy/guide) management module" }, - "g7_version": ">=7.0.6", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} diff --git a/modules/_bundled/sirsoft-page/module.php b/modules/_bundled/sirsoft-page/module.php index cf10e164..9b8e7855 100644 --- a/modules/_bundled/sirsoft-page/module.php +++ b/modules/_bundled/sirsoft-page/module.php @@ -6,6 +6,7 @@ use App\Extension\AbstractModule; use Illuminate\Database\Seeder; use Modules\Sirsoft\Page\Database\Seeders\PageSeeder; use Modules\Sirsoft\Page\Listeners\ActivityLogDescriptionResolver; +use Modules\Sirsoft\Page\Listeners\Ckeditor5ReferenceSourcesListener; use Modules\Sirsoft\Page\Listeners\PageActivityLogListener; use Modules\Sirsoft\Page\Listeners\SearchPagesListener; use Modules\Sirsoft\Page\Listeners\SeoPageCacheListener; @@ -132,6 +133,28 @@ class Module extends AbstractModule PageActivityLogListener::class, ActivityLogDescriptionResolver::class, SeoPageCacheListener::class, + Ckeditor5ReferenceSourcesListener::class, + ]; + } + + /** + * 모듈 스케줄 목록 반환 + * + * 페이지 작성 폼에서 올린 뒤 저장되지 않은 임시 첨부를 매일 정리합니다. + * `temp_key` 가 남아 있으면 끝내 연결되지 않은 폼 세션 부산물이라 오탐 여지가 없어 + * 별도 운영자 토글 없이 상시 동작합니다 (보존기간은 커맨드 옵션). + * + * @return array<int, array<string, mixed>> 스케줄 정의 목록 + */ + public function getSchedules(): array + { + return [ + [ + 'command' => 'sirsoft-page:prune-temp-attachments', + 'schedule' => 'daily', + 'description' => '미연결 임시 페이지 첨부 자동 삭제', + 'enabled_config' => null, + ], ]; } diff --git a/modules/_bundled/sirsoft-page/package-lock.json b/modules/_bundled/sirsoft-page/package-lock.json index f4ab96db..d29325a4 100644 --- a/modules/_bundled/sirsoft-page/package-lock.json +++ b/modules/_bundled/sirsoft-page/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-page", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-page", - "version": "1.0.2", + "version": "1.0.3", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/modules/_bundled/sirsoft-page/package.json b/modules/_bundled/sirsoft-page/package.json index 6de095d7..bdc4a069 100644 --- a/modules/_bundled/sirsoft-page/package.json +++ b/modules/_bundled/sirsoft-page/package.json @@ -1,13 +1,10 @@ { "name": "@g7/sirsoft-page", - "version": "1.0.2", + "version": "1.0.3", "description": "그누보드7 페이지 모듈 프론트엔드 에셋", "private": true, "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", "test": "vitest", "test:run": "vitest run", "test:e2e": "playwright test --config=tests/Playwright/playwright.config.ts", diff --git a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json index 582a89b7..f6b14488 100644 --- a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json +++ b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json @@ -1081,7 +1081,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-page.admin.page.form.validation_error" + "message": "{{error.message ?? '$t:sirsoft-page.admin.page.form.validation_error'}}" } } ] diff --git a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json index 669df1e4..ab72c8b4 100644 --- a/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json +++ b/modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json @@ -769,6 +769,7 @@ "serverTotalPages": "{{pages?.data?.meta?.last_page ?? null}}", "serverHasMorePages": "{{pages?.data?.meta?.has_more_pages ?? false}}", "selectable": true, + "selectionScope": "page", "selectedCountText": "", "selectedIds": "{{_global.selectedIds_admin_pages || []}}", "idField": "id", diff --git a/modules/_bundled/sirsoft-page/src/Console/Commands/PruneTempAttachmentsCommand.php b/modules/_bundled/sirsoft-page/src/Console/Commands/PruneTempAttachmentsCommand.php new file mode 100644 index 00000000..997c2a95 --- /dev/null +++ b/modules/_bundled/sirsoft-page/src/Console/Commands/PruneTempAttachmentsCommand.php @@ -0,0 +1,92 @@ +<?php + +namespace Modules\Sirsoft\Page\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Page\Services\PageAttachmentService; + +/** + * 미연결 임시 페이지 첨부 정리 커맨드 + * + * 페이지 작성 폼에서 업로드만 하고 저장 없이 이탈해 남은 임시 첨부(파일 + 기록)를 정리합니다. + * 연결 시점에 본경로로 옮겨지므로 `temp_key` 가 남아 있다는 것은 "끝내 연결되지 않았다" 는 + * 뜻이고, 그래서 별도 운영자 토글 없이 상시 동작합니다 (운영 데이터가 아닌 폼 세션 부산물). + * + * @example php artisan sirsoft-page:prune-temp-attachments --dry-run + * @example php artisan sirsoft-page:prune-temp-attachments --days=1 + */ +class PruneTempAttachmentsCommand extends Command +{ + /** + * 커맨드 이름 및 시그니처 + * + * @var string + */ + protected $signature = 'sirsoft-page:prune-temp-attachments + {--dry-run : 실제 삭제 없이 대상 건수만 확인} + {--limit=500 : 한 번에 처리할 최대 건수} + {--days=2 : 임시 첨부 보존기간(일)}'; + + /** + * 커맨드 설명 + * + * @var string + */ + protected $description = '페이지에 연결되지 않은 채 방치된 임시 첨부파일을 정리합니다.'; + + /** + * @param PageAttachmentService $attachmentService 페이지 첨부 서비스 + */ + public function __construct( + protected PageAttachmentService $attachmentService + ) { + parent::__construct(); + } + + /** + * 커맨드 실행 + * + * @return int 종료 코드 + */ + public function handle(): int + { + $days = (int) $this->option('days'); + + // 보존기간 0 이하 = 방금 올린 임시 첨부까지 지우게 되므로 차단한다. + if ($days < 1) { + $this->info('임시 첨부 보존기간이 1일 미만이어서 정리를 수행하지 않았습니다.'); + + return Command::SUCCESS; + } + + $limit = max(1, (int) $this->option('limit')); + $isDryRun = (bool) $this->option('dry-run'); + + $result = $this->attachmentService->pruneTempUploads($days, $limit, $isDryRun); + + if ($isDryRun) { + $this->info("[DRY RUN] 보존기간({$days}일) 경과 미연결 임시 페이지 첨부: {$result['scanned']}건"); + + return Command::SUCCESS; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 임시 페이지 첨부 %d건 중 %d건을 삭제했습니다. (실패 %d건)', + $days, + $result['scanned'], + $result['deleted'], + $result['failed'], + )); + + Log::info('PruneTempAttachmentsCommand: 임시 페이지 첨부 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + 'scanned' => $result['scanned'], + 'deleted' => $result['deleted'], + 'failed' => $result['failed'], + ]); + + return Command::SUCCESS; + } +} diff --git a/modules/_bundled/sirsoft-page/src/Exceptions/AttachmentLimitExceededException.php b/modules/_bundled/sirsoft-page/src/Exceptions/AttachmentLimitExceededException.php index b8e99363..385f2def 100644 --- a/modules/_bundled/sirsoft-page/src/Exceptions/AttachmentLimitExceededException.php +++ b/modules/_bundled/sirsoft-page/src/Exceptions/AttachmentLimitExceededException.php @@ -46,4 +46,27 @@ class AttachmentLimitExceededException extends Exception { return $this->attempted; } + + /** + * 다국어 메시지 키를 반환합니다. + * + * 응답을 만들 때 이미 번역된 `getMessage()` 대신 이 키를 넘긴다 — 번역문을 키로 + * 해석하려 하면 실패해 원문이 그대로 노출된다. + * + * @return string 다국어 메시지 키 + */ + public function getMessageKey(): string + { + return 'sirsoft-page::messages.errors.attachment_limit_exceeded'; + } + + /** + * 메시지 치환 파라미터를 반환합니다. + * + * @return array<string, mixed> 치환 파라미터 + */ + public function getMessageParams(): array + { + return ['limit' => $this->limit, 'attempted' => $this->attempted]; + } } diff --git a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageAttachmentController.php b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageAttachmentController.php index 7278af46..b1677264 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageAttachmentController.php +++ b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageAttachmentController.php @@ -53,7 +53,12 @@ class PageAttachmentController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (\Exception $e) { return $this->error('sirsoft-page::messages.attachment.upload_failed', 500, $e->getMessage()); } diff --git a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php index 1fe8879f..1cde65e8 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php +++ b/modules/_bundled/sirsoft-page/src/Http/Controllers/Admin/PageController.php @@ -80,7 +80,12 @@ class PageController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (\Exception $e) { return $this->error('sirsoft-page::messages.page.create_failed', 500, $e->getMessage()); } @@ -131,7 +136,12 @@ class PageController extends AdminBaseController ); } catch (AttachmentLimitExceededException $e) { // 첨부 개수 상한 초과 — generic 500 이 아닌 422 명시 차단 - return $this->error($e->getMessage(), 422, ['code' => 'attachment_limit_exceeded']); + return $this->error( + $e->getMessageKey(), + 422, + ['code' => 'attachment_limit_exceeded'], + $e->getMessageParams() + ); } catch (ModelNotFoundException) { return $this->notFound('sirsoft-page::messages.page.not_found'); } catch (AccessDeniedHttpException) { diff --git a/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageAttachmentController.php b/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageAttachmentController.php index 15970557..22c2c969 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageAttachmentController.php +++ b/modules/_bundled/sirsoft-page/src/Http/Controllers/User/PublicPageAttachmentController.php @@ -14,8 +14,11 @@ use Symfony\Component\HttpFoundation\StreamedResponse; * * 첨부파일 서빙(다운로드/미리보기)을 처리합니다. 썸네일 <img>·다운로드는 브라우저 직접 * GET 이라 토큰을 실을 수 없으므로 공개 hash 라우트로 단일화합니다. - * - 미리보기(썸네일): 공개 서빙 (미발행 콘텐츠도 hash 보유 시 조회 가능 — 트레이드오프 수용) + * - 미리보기(썸네일): 발행 첨부는 누구나, 미발행 첨부는 pages.read 관리자만 (download 와 동일 게이트) * - 다운로드: 발행 첨부는 누구나, 미발행 첨부는 pages.read 관리자만 (파일 보호) + * + * preview 와 download 는 동일한 발행상태 게이트를 공유한다 — 한쪽만 열려 있으면 + * 미발행 콘텐츠가 무인가로 새는 경로가 남는다(preview↔download 정합). */ class PublicPageAttachmentController extends PublicBaseController { @@ -60,20 +63,38 @@ class PublicPageAttachmentController extends PublicBaseController /** * 이미지 첨부파일을 미리봅니다 (해시 기반, inline). * - * 썸네일 <img> 는 토큰을 실을 수 없으므로 발행/미발행 무관 공개 서빙합니다. - * 미발행 콘텐츠의 썸네일은 hash 를 보유해야만 조회 가능하며(비추측성), 파일 - * 다운로드는 download() 의 권한 게이트로 보호됩니다. + * 발행된 페이지의 썸네일은 누구나, 미발행 페이지의 썸네일은 페이지 조회 권한 + * (sirsoft-page.pages.read) 관리자만 미리볼 수 있습니다(download 와 동일 게이트). + * 편집 중인 초안(미발행)의 <img> 썸네일은 편집 권한을 가진 관리자가 보므로 + * 정상 노출되며, 무인가 사용자에게만 미발행 썸네일이 차단됩니다. * + * @param Request $request HTTP 요청 객체 (미리보기 권한 판정용) * @param string $hash 첨부파일 해시 (12자) * @return StreamedResponse|JsonResponse 파일 스트리밍 응답 또는 오류 */ - public function preview(string $hash): StreamedResponse|JsonResponse + // audit:allow controller-base-request-injection reason: GET 이미지 서빙. 인증 사용자만 read-only 참조($request->user())해 미발행 첨부 미리보기 권한을 판정. 검증할 body 없음(hash 는 라우트 파라미터) + public function preview(Request $request, string $hash): StreamedResponse|JsonResponse { $attachment = $this->attachmentService->getByHash($hash); if (! $attachment) { return $this->notFound('sirsoft-page::messages.attachment.not_found'); } + // 미발행 페이지의 첨부는 페이지 조회 권한 관리자만 미리보기 가능 (download 와 동일 게이트). + // 브라우저 <img> 는 Authorization 헤더를 실을 수 없으므로, 게이트를 통과한 응답 + // (관리자 상세 직렬화)이 발급한 한시 서명 URL 도 동등한 자격으로 허용한다 — + // 서명은 발급 시점에 게이트를 통과한 요청에만 실리므로 무서명 게이트는 약화되지 않는다. + $published = $attachment->page && $attachment->page->published; + $canReadUnpublished = $request->user()?->hasPermission( + 'sirsoft-page.pages.read', + PermissionType::Admin + ) ?? false; + $hasValidSignature = $request->hasValidSignature(absolute: false); + + if (! $published && ! $canReadUnpublished && ! $hasValidSignature) { + return $this->notFound('sirsoft-page::messages.attachment.not_found'); + } + $response = $this->attachmentService->preview($attachment); return $response ?: $this->error('sirsoft-page::messages.attachment.file_not_found', 404); diff --git a/modules/_bundled/sirsoft-page/src/Http/Resources/PageAttachmentResource.php b/modules/_bundled/sirsoft-page/src/Http/Resources/PageAttachmentResource.php index 69005827..af8fb72c 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Resources/PageAttachmentResource.php +++ b/modules/_bundled/sirsoft-page/src/Http/Resources/PageAttachmentResource.php @@ -12,16 +12,30 @@ use Modules\Sirsoft\Page\Models\PageAttachment; */ class PageAttachmentResource extends BaseApiResource { + /** + * 미발행 첨부의 <img> 렌더용 한시 서명 preview URL 발급 여부. + * + * 게이트를 통과한 응답(관리자 상세 등)의 직렬화 시점에만 참이 된다 — + * 발행 페이지는 무서명 공개 URL 을 유지한다 (SEO·캐시 안정성). + */ + private bool $signedPreview = false; + /** * 첨부파일 목록을 리소스 배열로 변환합니다. * * @param iterable<int, PageAttachment>|null $attachments 첨부파일 목록 + * @param bool $signedPreview 한시 서명 preview URL 발급 여부 (미발행 페이지 전용) * @return array<int, self> */ - public static function collectionFor($attachments): array + public static function collectionFor($attachments, bool $signedPreview = false): array { return Collection::make($attachments ?? []) - ->map(fn ($attachment) => new self($attachment)) + ->map(function ($attachment) use ($signedPreview) { + $resource = new self($attachment); + $resource->signedPreview = $signedPreview; + + return $resource; + }) ->all(); } @@ -43,7 +57,7 @@ class PageAttachmentResource extends BaseApiResource 'order' => $this->order, 'is_image' => $this->isImage(), 'download_url' => $this->resource->downloadUrl(), - 'preview_url' => $this->resource->previewUrl(), + 'preview_url' => $this->resource->previewUrl(signed: $this->signedPreview), 'created_at' => $this->created_at ? $this->formatDateTimeStringForUser($this->created_at) : null, diff --git a/modules/_bundled/sirsoft-page/src/Http/Resources/PageResource.php b/modules/_bundled/sirsoft-page/src/Http/Resources/PageResource.php index fa24f200..f87c1c20 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Resources/PageResource.php +++ b/modules/_bundled/sirsoft-page/src/Http/Resources/PageResource.php @@ -38,9 +38,15 @@ class PageResource extends BaseApiResource 'uuid' => $this->updater->uuid, 'name' => $this->updater->name, ]), + // 미발행 페이지 첨부는 한시 서명 preview URL 로 직렬화한다. + // 이 리소스는 게이트(pages.read 권한 미들웨어)를 통과한 관리자 응답에만 쓰이므로 + // 서명 발급이 곧 게이트 통과 자격의 위임이다. 발행 페이지는 무서명 공개 URL 유지. 'attachments' => $this->whenLoaded( 'attachments', - fn () => PageAttachmentResource::collectionFor($this->attachments) + fn () => PageAttachmentResource::collectionFor( + $this->attachments, + signedPreview: ! $this->published + ) ), ...$this->formatTimestamps(), ...$this->resourceMeta($request), diff --git a/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php b/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php index d6320dad..be2e611c 100644 --- a/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php +++ b/modules/_bundled/sirsoft-page/src/Http/Resources/PublicPageResource.php @@ -59,9 +59,15 @@ class PublicPageResource extends BaseApiResource : null, 'seo_meta' => $this->seo_meta, 'current_version' => $this->current_version, + // 미발행 페이지는 pages.read 관리자의 미리보기(is_preview)로만 이 리소스에 + // 도달하므로(공개 게이트가 게스트를 404 차단), 그 화면의 <img> 썸네일용으로 + // 한시 서명 preview URL 을 직렬화한다. 발행 페이지는 무서명 공개 URL 유지. 'attachments' => $this->whenLoaded( 'attachments', - fn () => PageAttachmentResource::collectionFor($this->attachments) + fn () => PageAttachmentResource::collectionFor( + $this->attachments, + signedPreview: ! $this->published + ) ), 'created_at' => $this->created_at ? $this->formatDateTimeStringForUser($this->created_at) diff --git a/modules/_bundled/sirsoft-page/src/Listeners/Ckeditor5ReferenceSourcesListener.php b/modules/_bundled/sirsoft-page/src/Listeners/Ckeditor5ReferenceSourcesListener.php new file mode 100644 index 00000000..611208c5 --- /dev/null +++ b/modules/_bundled/sirsoft-page/src/Listeners/Ckeditor5ReferenceSourcesListener.php @@ -0,0 +1,57 @@ +<?php + +namespace Modules\Sirsoft\Page\Listeners; + +use App\Contracts\Extension\HookListenerInterface; + +/** + * 페이지 본문을 CKEditor5 이미지 참조 스캔 대상으로 등록하는 리스너 + * + * `sirsoft-ckeditor5.image.filter_reference_sources` 필터 훅에 페이지 본문과 버전 + * 스냅샷 컬럼을 덧붙입니다. 버전 스냅샷을 포함하는 이유는 과거 버전으로 되돌릴 때 + * 그 시점 이미지가 살아 있어야 하기 때문입니다. + * + * 테이블명 문자열만 덧붙이며 DB 에 접근하지 않습니다. + */ +class Ckeditor5ReferenceSourcesListener implements HookListenerInterface +{ + /** + * 구독할 훅 목록 반환 + * + * @return array<string, mixed> + */ + public static function getSubscribedHooks(): array + { + return [ + 'sirsoft-ckeditor5.image.filter_reference_sources' => [ + 'method' => 'addPageSources', + 'priority' => 10, + 'type' => 'filter', + ], + ]; + } + + /** + * 훅 이벤트를 처리합니다. + * + * Filter 훅은 getSubscribedHooks 에서 지정한 메서드를 직접 호출하므로 + * 이 메서드는 인터페이스 요구사항 충족을 위해서만 존재합니다. + * + * @param mixed ...$args 훅에서 전달된 인수들 + */ + public function handle(...$args): void {} + + /** + * 페이지 참조 소스를 추가합니다. + * + * @param array $sources 기존 참조 소스 목록 + * @return array 페이지 소스가 추가된 목록 + */ + public function addPageSources(array $sources): array + { + $sources[] = ['table' => 'pages', 'columns' => ['content']]; + $sources[] = ['table' => 'page_versions', 'columns' => ['content']]; + + return $sources; + } +} diff --git a/modules/_bundled/sirsoft-page/src/Listeners/SearchPagesListener.php b/modules/_bundled/sirsoft-page/src/Listeners/SearchPagesListener.php index 8a1e632b..2e2001b0 100644 --- a/modules/_bundled/sirsoft-page/src/Listeners/SearchPagesListener.php +++ b/modules/_bundled/sirsoft-page/src/Listeners/SearchPagesListener.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Page\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Enums\TotalRelation; use App\Search\SearchCategoryPayload; +use App\Search\SearchHighlighter; use App\Support\Query\BoundedCount; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Page\Services\PageService; @@ -150,7 +151,10 @@ class SearchPagesListener implements HookListenerInterface $results['pages'] = SearchCategoryPayload::fromBounded($searchPage, $format($searchPage->items())); } catch (\Exception $e) { - Log::error('Search pages error', ['message' => $e->getMessage(), 'q' => $q]); + // 실패를 카테고리 키 미설정으로 삼키면 화면이 "검색 결과 없음" 을 그린다 — + // failed 페이로드로 표면화하고, 원인 추적을 위해 스택을 함께 남긴다 (#103). + Log::error('Search pages error', ['message' => $e->getMessage(), 'q' => $q, 'exception' => $e]); + $results['pages'] = SearchCategoryPayload::failed(); } return $results; @@ -256,13 +260,7 @@ class SearchPagesListener implements HookListenerInterface */ private function highlightKeyword(?string $text, string $keyword): string { - if (empty($text) || empty($keyword)) { - return $text ?? ''; - } - - $escapedKeyword = preg_quote($keyword, '/'); - - return preg_replace('/('.$escapedKeyword.')/iu', '<mark>$1</mark>', $text); + return SearchHighlighter::highlight($text, $keyword); } /** @@ -295,8 +293,8 @@ class SearchPagesListener implements HookListenerInterface return ''; } - // HTML 태그 제거 후 공백 정규화 - $plainText = trim(preg_replace('/\s+/', ' ', html_entity_decode(strip_tags((string) $text)))); + // HTML 태그 제거 후 공백 정규화 (엔티티 디코드를 태그 제거보다 먼저 수행) + $plainText = SearchHighlighter::toPlainText((string) $text); $position = mb_stripos($plainText, $keyword); if ($position !== false) { diff --git a/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php b/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php index c333e40d..570248b6 100644 --- a/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php +++ b/modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php @@ -43,6 +43,14 @@ class SeoPageCacheListener implements HookListenerInterface 'method' => 'onPageChange', 'priority' => 20, ], + // 발행/미발행 전환(단건 setPublished·일괄 bulk-publish)은 after_update 를 거치지 + // 않고 after_publish 만 발화한다 — 미구독 시 발행 상태를 바꿔도 봇 캐시가 이전 + // 상태(미발행 soft-404 / 발행 콘텐츠)로 남고 사이트맵 증분 색인도 빠진다. + // onPageChange 가 모델의 published 최신 상태로 색인(upsert)/해제(remove)를 판정한다. + 'sirsoft-page.page.after_publish' => [ + 'method' => 'onPageChange', + 'priority' => 20, + ], ]; } diff --git a/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php b/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php index 7f7cb1b7..b730ba44 100644 --- a/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php +++ b/modules/_bundled/sirsoft-page/src/Models/PageAttachment.php @@ -6,6 +6,7 @@ use App\Models\User; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Facades\URL; use Modules\Sirsoft\Page\Database\Factories\PageAttachmentFactory; class PageAttachment extends Model @@ -152,20 +153,51 @@ class PageAttachment extends Model */ public function downloadUrl(): string { - return '/api/modules/sirsoft-page/pages/attachment/'.$this->hash; + return route( + 'api.modules.sirsoft-page.pages.attachment.download', + ['hash' => $this->hash], + absolute: false + ); } /** * 이미지 미리보기 URL을 반환합니다 (공개 hash 라우트). * + * 브라우저 <img src> 는 Authorization 헤더를 실을 수 없으므로, 미발행 페이지의 + * 첨부를 권한자 화면(관리자 상세 등)에 표시할 때는 한시 서명 URL 을 발급한다. + * 서명 URL 은 게이트를 통과한 응답 직렬화 시점에만 발급되고(PageResource), + * 서빙 엔드포인트가 유효 서명을 무인증 허용한다 — 무서명 게이트는 종전과 동일. + * + * @param bool $signed 한시 서명 URL 발급 여부 (미발행 첨부의 <img> 렌더용) * @return string|null 미리보기 URL (이미지가 아니면 null) */ - public function previewUrl(): ?string + public function previewUrl(bool $signed = false): ?string { if (! $this->isImage()) { return null; } - return '/api/modules/sirsoft-page/pages/attachment/'.$this->hash.'/preview'; + if ($signed) { + return URL::temporarySignedRoute( + 'api.modules.sirsoft-page.pages.attachment.preview', + now()->addMinutes(self::SIGNED_PREVIEW_TTL_MINUTES), + ['hash' => $this->hash], + absolute: false + ); + } + + return route( + 'api.modules.sirsoft-page.pages.attachment.preview', + ['hash' => $this->hash], + absolute: false + ); } + + /** + * 서명 preview URL 의 유효 시간(분). + * + * 관리자 상세 화면이 열려 있는 동안 썸네일이 유지될 만큼 길고, + * URL 유출 시 노출 창을 좁힐 만큼 짧은 값. + */ + public const SIGNED_PREVIEW_TTL_MINUTES = 30; } diff --git a/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php b/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php index 8f16dda5..2207a73f 100644 --- a/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php +++ b/modules/_bundled/sirsoft-page/src/Providers/PageServiceProvider.php @@ -5,6 +5,7 @@ namespace Modules\Sirsoft\Page\Providers; use App\Extension\BaseModuleServiceProvider; use App\Seo\SitemapGenerator; use Modules\Sirsoft\Page\Console\Commands\PlaywrightSeedPage; +use Modules\Sirsoft\Page\Console\Commands\PruneTempAttachmentsCommand; use Modules\Sirsoft\Page\Repositories\Contracts\PageAttachmentRepositoryInterface; use Modules\Sirsoft\Page\Repositories\Contracts\PageRepositoryInterface; use Modules\Sirsoft\Page\Repositories\Contracts\PageVersionRepositoryInterface; @@ -57,6 +58,7 @@ class PageServiceProvider extends BaseModuleServiceProvider if ($this->app->runningInConsole()) { $this->commands([ PlaywrightSeedPage::class, + PruneTempAttachmentsCommand::class, ]); } diff --git a/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageAttachmentRepositoryInterface.php b/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageAttachmentRepositoryInterface.php index e7b935b7..1cefb469 100644 --- a/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageAttachmentRepositoryInterface.php +++ b/modules/_bundled/sirsoft-page/src/Repositories/Contracts/PageAttachmentRepositoryInterface.php @@ -3,6 +3,8 @@ namespace Modules\Sirsoft\Page\Repositories\Contracts; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Support\Carbon; use Modules\Sirsoft\Page\Models\PageAttachment; /** @@ -65,7 +67,7 @@ interface PageAttachmentRepositoryInterface * @param int $id 첨부파일 ID * @return PageAttachment 첨부파일 모델 * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public function findOrFail(int $id): PageAttachment; @@ -77,6 +79,18 @@ interface PageAttachmentRepositoryInterface */ public function delete(PageAttachment $attachment): bool; + /** + * 페이지에 연결되지 않은 채 방치된 임시 첨부를 오래된 순으로 조회합니다. + * + * 페이지 작성 폼에서 업로드만 하고 저장 없이 이탈하면 `temp_key` 가 남은 채 + * `page_id` 가 비어 있는 행과 그 파일이 영구 잔존합니다. 그 회수 대상을 찾습니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 업로드가 대상) + * @param int $limit 최대 조회 건수 + * @return Collection<int, PageAttachment> 임시 첨부 목록 (created_at 오름차순) + */ + public function findStaleTempAttachments(Carbon $threshold, int $limit): Collection; + /** * 첨부파일을 수정합니다. * diff --git a/modules/_bundled/sirsoft-page/src/Repositories/PageAttachmentRepository.php b/modules/_bundled/sirsoft-page/src/Repositories/PageAttachmentRepository.php index 6b7ee7a9..a2e92f7f 100644 --- a/modules/_bundled/sirsoft-page/src/Repositories/PageAttachmentRepository.php +++ b/modules/_bundled/sirsoft-page/src/Repositories/PageAttachmentRepository.php @@ -3,6 +3,8 @@ namespace Modules\Sirsoft\Page\Repositories; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Page\Models\PageAttachment; use Modules\Sirsoft\Page\Repositories\Contracts\PageAttachmentRepositoryInterface; @@ -97,7 +99,7 @@ class PageAttachmentRepository implements PageAttachmentRepositoryInterface * @param int $id 첨부파일 ID * @return PageAttachment 첨부파일 모델 * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws ModelNotFoundException */ public function findOrFail(int $id): PageAttachment { @@ -115,6 +117,21 @@ class PageAttachmentRepository implements PageAttachmentRepositoryInterface return (bool) $attachment->delete(); } + /** + * {@inheritDoc} + */ + public function findStaleTempAttachments(Carbon $threshold, int $limit): Collection + { + return PageAttachment::query() + ->whereNotNull('temp_key') + ->whereNull('page_id') + ->where('created_at', '<', $threshold) + ->orderBy('created_at') + ->orderBy('id') + ->limit($limit) + ->get(['id', 'temp_key', 'disk', 'path', 'created_at']); + } + /** * 첨부파일을 수정합니다. * diff --git a/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php b/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php index 0b3edee5..c9cbd4f8 100644 --- a/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php +++ b/modules/_bundled/sirsoft-page/src/Services/PageAttachmentService.php @@ -4,16 +4,20 @@ namespace Modules\Sirsoft\Page\Services; use App\Contracts\Extension\StorageInterface; use App\Extension\HookManager; +use App\Helpers\PermissionHelper; use App\Support\ImageResizer; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Modules\Sirsoft\Page\Exceptions\AttachmentLimitExceededException; use Modules\Sirsoft\Page\Models\PageAttachment; use Modules\Sirsoft\Page\Repositories\Contracts\PageAttachmentRepositoryInterface; use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; /** * 페이지 첨부파일 서비스 @@ -191,6 +195,8 @@ class PageAttachmentService */ public function deleteAttachment(PageAttachment $attachment): bool { + $this->assertPageScope($attachment); + HookManager::doAction('sirsoft-page.attachment.before_delete', $attachment); // 물리 파일 삭제 @@ -204,6 +210,114 @@ class PageAttachmentService return $result; } + /** + * 페이지에 연결되지 않은 채 방치된 임시 첨부를 정리합니다. + * + * 페이지 작성 폼에서 첨부만 올리고 저장하지 않고 이탈하면 `temp_key` 만 남은 행과 + * 그 파일이 남습니다. 연결 시점에 본경로로 옮겨지므로 `temp_key` 가 남아 있다는 것은 + * "끝내 연결되지 않았다" 는 뜻이고, 그래서 오탐 여지가 없습니다. + * + * 파일 → 행 순서를 지켜, 행이 먼저 사라져 파일을 못 찾는 상태를 만들지 않습니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 처리할 최대 건수 + * @param bool $dryRun true 면 대상만 세고 삭제하지 않음 + * @return array{scanned: int, deleted: int, failed: int} 처리 결과 + */ + public function pruneTempUploads(int $days, int $limit, bool $dryRun = false): array + { + $threshold = Carbon::now()->subDays($days); + $attachments = $this->attachmentRepository->findStaleTempAttachments($threshold, $limit); + + $result = ['scanned' => $attachments->count(), 'deleted' => 0, 'failed' => 0]; + + if ($dryRun) { + return $result; + } + + foreach ($attachments as $attachment) { + $storage = $this->storageForRow($attachment->disk); + + if ($storage->exists('attachments', $attachment->path) && ! $storage->delete('attachments', $attachment->path)) { + Log::warning('임시 페이지 첨부 파일 삭제 실패 — 기록 보존', [ + 'attachment_id' => $attachment->id, + 'disk' => $attachment->disk, + 'path' => $attachment->path, + ]); + + $result['failed']++; + + continue; + } + + $this->attachmentRepository->delete($attachment); + $result['deleted']++; + } + + $this->removeEmptyTempDirectories($attachments); + + return $result; + } + + /** + * 파일을 모두 지운 temp_key 디렉토리를 정리합니다. + * + * 파일만 지우고 디렉토리를 남기면 폼 세션마다 빈 디렉토리가 쌓여, 정리를 돌려도 + * 저장소에는 흔적이 계속 늘어납니다. + * + * 디렉토리에 파일이 남아 있으면(같은 temp_key 의 다른 첨부가 limit 에 걸려 이번 회차에서 + * 빠졌거나 파일 삭제에 실패한 경우 등) 삭제하지 않습니다. + * + * @param Collection $attachments 이번 회차에 처리한 첨부 목록 + */ + private function removeEmptyTempDirectories(Collection $attachments): void + { + $directories = []; + + foreach ($attachments as $attachment) { + $directory = dirname((string) $attachment->path); + + if ($directory === '' || $directory === '.') { + continue; + } + + $directories[$attachment->disk.'|'.$directory] = [$attachment->disk, $directory]; + } + + foreach ($directories as [$disk, $directory]) { + $storage = $this->storageForRow($disk); + + if ($storage->files('attachments', $directory) !== []) { + continue; + } + + $storage->deleteDirectory('attachments', $directory); + } + } + + /** + * 첨부 행에 기록된 disk 기준 스토리지를 반환합니다. + * + * 디스크를 전환한 뒤에도 전환 이전 행의 파일을 그 행의 실제 저장 위치에서 지우기 위한 + * 해석입니다. 미등록 disk(그 디스크를 제공하던 확장이 비활성화된 경우)는 주입 스토리지로 + * 폴백합니다 — withDisk 로 미등록 disk 인스턴스를 만들면 이후 호출이 예외가 됩니다. + * + * @param string|null $disk 행의 disk 컬럼 값 + * @return StorageInterface 행 disk 의 스토리지 + */ + private function storageForRow(?string $disk): StorageInterface + { + if ($disk === null || $disk === '' || $disk === $this->storage->getDisk()) { + return $this->storage; + } + + if (config("filesystems.disks.{$disk}") === null) { + return $this->storage; + } + + return $this->storage->withDisk($disk); + } + /** * 첨부파일 순서를 변경합니다. * @@ -212,6 +326,8 @@ class PageAttachmentService */ public function reorder(array $orders): bool { + $this->assertReorderScope($orders); + HookManager::doAction('sirsoft-page.attachment.before_reorder', $orders); $result = $this->attachmentRepository->reorder($orders); @@ -221,6 +337,52 @@ class PageAttachmentService return $result; } + /** + * 첨부의 부모 페이지가 액터의 스코프 안에 있는지 검사합니다. + * + * 첨부 라우트는 `{id}`(정수)로 선언돼 있어 라우트 모델 바인딩이 일어나지 않고, + * 순서 변경은 아예 정적 경로다. 두 경우 모두 PermissionMiddleware 의 스코프 검사가 + * 스킵되므로(모델이 resolve 되지 않으면 목록 엔드포인트로 간주) 서비스가 재적용한다. + * 스코프 대상은 첨부가 아니라 **부모 페이지**다 — `sirsoft-page.pages.update` 의 + * owner_key 가 페이지의 `created_by` 이기 때문이다. + * + * @param PageAttachment $attachment 대상 첨부 + * + * @throws AccessDeniedHttpException 부모 페이지가 스코프 밖인 경우 + */ + private function assertPageScope(PageAttachment $attachment): void + { + $page = $attachment->page; + + // 부모를 못 읽으면 막는다(fail-closed) — 첨부에 page_id 가 있는데 페이지를 못 찾는 + // 것은 정상 상태가 아니고, 통과시키면 게이트가 있어야 할 자리가 비어 버린다. + if (! $page || ! PermissionHelper::checkScopeAccess($page, 'sirsoft-page.pages.update')) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + } + + /** + * 순서 변경 대상 첨부 전체가 액터의 스코프 안에 있는지 검사합니다. + * + * 순서는 집합 전체에 대한 하나의 배열이라 일부만 반영하면 나머지와 어긋난다 — + * 걸러내지 않고 전량 거부한다(코어 첨부/메뉴 순서 변경과 같은 의미론). + * + * @param array<int, array{id: int, order: int}> $orders 순서 데이터 + * + * @throws AccessDeniedHttpException 스코프 밖 첨부가 하나라도 포함된 경우 + */ + private function assertReorderScope(array $orders): void + { + $ids = array_values(array_unique(array_filter( + array_map(static fn ($item): int => (int) ($item['id'] ?? 0), $orders) + ))); + + foreach ($ids as $id) { + // 존재하지 않는 id 는 findOrFail 이 404 로 끊는다 — 통과시키면 안 된다. + $this->assertPageScope($this->attachmentRepository->findOrFail($id)); + } + } + /** * 해시로 첨부파일을 조회합니다. * diff --git a/modules/_bundled/sirsoft-page/src/Services/PageService.php b/modules/_bundled/sirsoft-page/src/Services/PageService.php index 89d75cbe..615e37ac 100644 --- a/modules/_bundled/sirsoft-page/src/Services/PageService.php +++ b/modules/_bundled/sirsoft-page/src/Services/PageService.php @@ -246,6 +246,15 @@ class PageService // 존재하지 않는 페이지가 섞이면 예외 → 트랜잭션 전체 롤백 (all-or-nothing) $page = $this->pageRepository->findOrFail((int) $id); + // 일괄 라우트(`PATCH admin/pages/bulk-publish`)에는 `{page}` 파라미터가 없어 + // PermissionMiddleware 의 스코프 검사가 통째로 스킵된다. 단건 경로 + // (updatePage/deletePage 등)는 전부 이 검사를 하는데 일괄만 비어 있으면 + // 그 경로가 우회로다 — 같은 판정을 여기서 재적용한다. + // 예외는 위 findOrFail 과 같은 all-or-nothing 롤백을 탄다. + if (! PermissionHelper::checkScopeAccess($page, 'sirsoft-page.pages.update')) { + throw new AccessDeniedHttpException(__('auth.scope_denied')); + } + HookManager::doAction('sirsoft-page.page.before_publish', $page, $published); $updateData = [ diff --git a/modules/_bundled/sirsoft-page/src/Services/PageSettingsService.php b/modules/_bundled/sirsoft-page/src/Services/PageSettingsService.php new file mode 100644 index 00000000..a483ea83 --- /dev/null +++ b/modules/_bundled/sirsoft-page/src/Services/PageSettingsService.php @@ -0,0 +1,269 @@ +<?php + +namespace Modules\Sirsoft\Page\Services; + +use App\Contracts\Extension\ModuleSettingsInterface; +use App\Traits\NormalizesSettingsData; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\File; + +/** + * 페이지 모듈 환경설정 서비스 + * + * 이 서비스가 없던 동안 `g7_settings.modules.sirsoft-page` 미러는 **영구 미존재**였고, + * 소비자(`PageAttachmentService`, `UploadPageAttachmentRequest`)는 항상 인자 기본값으로 + * 폴백했습니다 — 운영자가 설정을 바꿔도 첨부 상한이 반영되지 않았습니다 (공개이슈 #109). + * + * defaults.json 은 카테고리 맵(`{"attachment": {...}}`) 한 단계 구조입니다. + */ +class PageSettingsService implements ModuleSettingsInterface +{ + use NormalizesSettingsData; + + /** + * 모듈 식별자 + */ + private const MODULE_IDENTIFIER = 'sirsoft-page'; + + /** + * 설정 기본값 (캐시) + */ + private ?array $defaults = null; + + /** + * 현재 설정값 (캐시) + */ + private ?array $settings = null; + + /** + * 모듈 설정 기본값 파일 경로 반환 + * + * @return string|null defaults.json 파일의 절대 경로, 없으면 null + */ + public function getSettingsDefaultsPath(): ?string + { + $path = base_path('modules/'.self::MODULE_IDENTIFIER).'/config/settings/defaults.json'; + + return file_exists($path) ? $path : null; + } + + /** + * 설정값 조회 + * + * @param string $key 설정 키 (예: 'attachment.max_count') + * @param mixed $default 기본값 + * @return mixed 설정값 + */ + public function getSetting(string $key, mixed $default = null): mixed + { + return Arr::get($this->getAllSettings(), $key, $default); + } + + /** + * 설정값 저장 + * + * @param string $key 설정 키 + * @param mixed $value 저장할 값 + * @return bool 성공 여부 + */ + public function setSetting(string $key, mixed $value): bool + { + $settings = $this->getAllSettings(); + Arr::set($settings, $key, $value); + + $category = explode('.', $key)[0]; + + return $this->saveSettings([$category => $settings[$category] ?? []]); + } + + /** + * 전체 설정 조회 + * + * @return array 모든 카테고리의 설정값 + */ + public function getAllSettings(): array + { + if ($this->settings !== null) { + return $this->settings; + } + + $defaultValues = $this->getDefaults(); + $settings = []; + + foreach ($defaultValues as $category => $categoryDefaults) { + if (! is_array($categoryDefaults)) { + continue; + } + + $settings[$category] = array_merge($categoryDefaults, $this->loadCategorySettings($category)); + } + + $settings = $this->normalizeSettingsData($settings, $defaultValues); + + $this->settings = $settings; + + return $settings; + } + + /** + * 카테고리별 설정 조회 + * + * @param string $category 카테고리명 + * @return array 카테고리의 설정값 + */ + public function getSettings(string $category): array + { + return $this->getAllSettings()[$category] ?? []; + } + + /** + * 설정 저장 + * + * @param array $settings 저장할 설정 배열 + * @return bool 성공 여부 + */ + public function saveSettings(array $settings): bool + { + $success = true; + $defaultValues = $this->getDefaults(); + + foreach ($settings as $category => $categorySettings) { + if (str_starts_with((string) $category, '_') || ! is_array($categorySettings)) { + continue; + } + + $categoryDefaults = $defaultValues[$category] ?? []; + + // 토글이 꺼진 채 전송되지 않는 boolean 필드는 false 로 채운다 + foreach ($categoryDefaults as $key => $defaultValue) { + if (is_bool($defaultValue) && ! array_key_exists($key, $categorySettings)) { + $categorySettings[$key] = false; + } + } + + $processed = $this->normalizeCategoryData($categorySettings, $categoryDefaults); + + if (! $this->saveCategorySettings($category, $processed)) { + $success = false; + } + } + + $this->clearCache(); + + return $success; + } + + /** + * 프론트엔드용 설정 조회 (민감정보 제외) + * + * defaults.json 에 frontend_schema 가 없으므로 노출 대상이 없습니다. + * + * @return array 프론트엔드에 노출 가능한 설정값 + */ + public function getFrontendSettings(): array + { + return []; + } + + /** + * 캐시 초기화 + */ + public function clearCache(): void + { + $this->defaults = null; + $this->settings = null; + + // 상주 프로세스의 config 미러도 함께 갱신한다 (공개이슈 #109) + g7_refresh_module_settings_config(self::MODULE_IDENTIFIER); + } + + /** + * 기본값 조회 + * + * @return array defaults.json 의 카테고리 맵 + */ + private function getDefaults(): array + { + if ($this->defaults !== null) { + return $this->defaults; + } + + $path = $this->getSettingsDefaultsPath(); + + if ($path === null) { + return []; + } + + $decoded = json_decode(File::get($path), true) ?? []; + + // `_meta`/`defaults` 래퍼 형식(다른 모듈 관례)도 함께 받아 준다 + $this->defaults = is_array($decoded['defaults'] ?? null) ? $decoded['defaults'] : $decoded; + + return $this->defaults; + } + + /** + * 카테고리 설정 로드 + * + * @param string $category 카테고리명 + * @return array 저장된 설정값 + */ + private function loadCategorySettings(string $category): array + { + $path = $this->getCategoryFilePath($category); + + if (! File::exists($path)) { + return []; + } + + return json_decode(File::get($path), true) ?? []; + } + + /** + * 카테고리 설정 저장 + * + * @param string $category 카테고리명 + * @param array $settings 설정값 + * @return bool 성공 여부 + */ + private function saveCategorySettings(string $category, array $settings): bool + { + $storagePath = $this->getStoragePath(); + + if (! File::isDirectory($storagePath)) { + File::makeDirectory($storagePath, 0755, true); + } + + $content = json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + + return File::put($this->getCategoryFilePath($category), $content) !== false; + } + + /** + * 카테고리 설정 파일 경로 반환 + * + * @param string $category 카테고리명 + * @return string 설정 파일 경로 + */ + private function getCategoryFilePath(string $category): string + { + return $this->getStoragePath().'/'.$category.'.json'; + } + + /** + * 설정 저장 경로 반환 + * + * testing 환경에서는 운영 설정(storage/app/modules/.../settings)을 보호하기 위해 + * 별도 경로를 사용합니다 — 테스트가 개발/운영 설정을 덮어쓰지 않게 합니다. + * + * @return string 설정 파일 저장 디렉토리 경로 + */ + private function getStoragePath(): string + { + if (app()->runningUnitTests()) { + return storage_path('framework/testing/modules/'.self::MODULE_IDENTIFIER.'/settings'); + } + + return storage_path('app/modules/'.self::MODULE_IDENTIFIER.'/settings'); + } +} diff --git a/modules/_bundled/sirsoft-page/src/routes/api.php b/modules/_bundled/sirsoft-page/src/routes/api.php index 789afd0e..3ecef26d 100644 --- a/modules/_bundled/sirsoft-page/src/routes/api.php +++ b/modules/_bundled/sirsoft-page/src/routes/api.php @@ -127,7 +127,7 @@ Route::prefix('pages')->middleware(['optional.sanctum', 'throttle:600,1'])->name ->where('hash', '[a-zA-Z0-9]{12}') ->name('attachment.download'); - // 첨부 이미지 미리보기 (공개 - 토큰 없는 <img> 직접 GET 을 위해 권한 체크 없이 이미지만 제공) + // 첨부 이미지 미리보기 (해시 기반, 발행 상태에 따라 컨트롤러에서 접근 제어 — download 와 동일 게이트) Route::get('/attachment/{hash}/preview', [PublicPageAttachmentController::class, 'preview']) ->where('hash', '[a-zA-Z0-9]{12}') ->name('attachment.preview'); diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php index 21ed966a..5dd14b78 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Admin/PageControllerTest.php @@ -421,7 +421,9 @@ class PageControllerTest extends FeatureTestCase * * 썸네일 <img>·다운로드는 토큰을 실을 수 없어 인증 라우트에 물리면 401 로 깨진다. * 게시판·이커머스 표준과 동일하게 공개 hash 라우트로 단일화하고, 미발행 콘텐츠 - * 다운로드 차단은 공개 라우트 내부의 권한 게이트가 담당한다. + * 차단은 공개 라우트 내부의 발행상태 게이트가 담당한다(KVE-2026-1914). + * 미발행 페이지의 preview URL 은 게이트를 통과할 수 있도록 한시 서명이 실린다 + * — 무인증 <img> 요청으로도 렌더 가능해야 하기 때문이다. */ public function test_admin_show_returns_public_attachment_urls(): void { @@ -442,10 +444,13 @@ class PageControllerTest extends FeatureTestCase $response->assertStatus(200); $att = $response->json('data.attachments.0'); - $this->assertSame( - "/api/modules/sirsoft-page/pages/attachment/{$attachment->hash}/preview", + // 미발행 preview: 공개 hash 라우트 + 한시 서명 (무인증 <img> 렌더 경로) + $this->assertStringStartsWith( + "/api/modules/sirsoft-page/pages/attachment/{$attachment->hash}/preview?", $att['preview_url'] ); + $this->assertStringContainsString('signature=', $att['preview_url']); + // download 는 인증 fetch(blob) 경로이므로 무서명 공개 hash 라우트 유지 $this->assertSame( "/api/modules/sirsoft-page/pages/attachment/{$attachment->hash}", $att['download_url'] diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Console/PruneTempAttachmentsCommandTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Console/PruneTempAttachmentsCommandTest.php new file mode 100644 index 00000000..886b9a11 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Feature/Console/PruneTempAttachmentsCommandTest.php @@ -0,0 +1,191 @@ +<?php + +namespace Modules\Sirsoft\Page\Tests\Feature\Console; + +use App\Extension\Storage\ModuleStorageDriver; +use Modules\Sirsoft\Page\Models\Page; +use Modules\Sirsoft\Page\Models\PageAttachment; +use Modules\Sirsoft\Page\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 미연결 임시 페이지 첨부 정리 커맨드 테스트 + * + * `temp_key` 가 남고 `page_id` 가 비어 있는 행만 대상이어야 합니다 — 연결된 첨부가 + * 섞이면 공개 중인 페이지의 첨부가 사라집니다. + */ +class PruneTempAttachmentsCommandTest extends ModuleTestCase +{ + /** + * 임시 첨부 파일과 기록을 함께 만듭니다. + * + * @param array<string, mixed> $attributes 덮어쓸 속성 + * @return PageAttachment 생성된 첨부 + */ + private function makeAttachment(array $attributes = []): PageAttachment + { + $filename = uniqid('temp-page-').'.png'; + $path = $attributes['path'] ?? "temp/tempkey/{$filename}"; + + (new ModuleStorageDriver('sirsoft-page', 'modules'))->put('attachments', $path, 'bytes'); + + return PageAttachment::create(array_merge([ + 'page_id' => null, + 'temp_key' => 'tempkey', + 'original_filename' => $filename, + 'stored_filename' => $filename, + 'disk' => 'modules', + 'path' => $path, + 'mime_type' => 'image/png', + 'size' => 5, + 'collection' => 'attachments', + 'order' => 1, + ], $attributes)); + } + + /** + * @scenario age=past_retention, attachment_state=temp_unlinked + * + * @effects page_temp_prune_deletes_file_and_record + */ + #[Test] + public function stale_temp_attachments_lose_both_file_and_record(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-page:prune-temp-attachments')->assertSuccessful(); + + $this->assertFalse( + (new ModuleStorageDriver('sirsoft-page', 'modules'))->exists('attachments', $attachment->path) + ); + $this->assertDatabaseMissing('page_attachments', ['id' => $attachment->id]); + } + + /** + * @scenario age=within_retention, attachment_state=temp_unlinked + * + * @effects page_temp_prune_keeps_within_retention + */ + #[Test] + public function temp_attachment_within_retention_is_kept(): void + { + $attachment = $this->makeAttachment(); + + $this->artisan('sirsoft-page:prune-temp-attachments')->assertSuccessful(); + + $this->assertDatabaseHas('page_attachments', ['id' => $attachment->id]); + } + + /** + * @scenario age=past_retention, attachment_state=linked_to_page + * + * @effects page_temp_prune_skips_linked + */ + #[Test] + public function attachment_linked_to_a_page_is_never_pruned(): void + { + $page = Page::create([ + 'slug' => 'prune-temp-attachment-test-'.uniqid(), + 'title' => ['ko' => '연결 첨부 테스트', 'en' => 'Linked Attachment Test'], + 'content' => ['ko' => '', 'en' => ''], + 'published' => true, + ]); + + $attachment = $this->makeAttachment([ + 'page_id' => $page->id, + 'temp_key' => null, + 'path' => '2026/08/14/'.uniqid('linked-').'.png', + ]); + $attachment->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-page:prune-temp-attachments')->assertSuccessful(); + + $this->assertDatabaseHas('page_attachments', ['id' => $attachment->id]); + } + + /** + * @effects page_temp_prune_dry_run + */ + #[Test] + public function dry_run_reports_targets_without_deleting(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-page:prune-temp-attachments --dry-run') + ->expectsOutputToContain('[DRY RUN]') + ->assertSuccessful(); + + $this->assertDatabaseHas('page_attachments', ['id' => $attachment->id]); + } + + /** + * @effects page_temp_prune_days_guard + */ + #[Test] + public function retention_below_one_day_performs_no_cleanup(): void + { + $attachment = $this->makeAttachment(); + $attachment->forceFill(['created_at' => now()->subDays(90)])->saveQuietly(); + + $this->artisan('sirsoft-page:prune-temp-attachments --days=0')->assertSuccessful(); + + $this->assertDatabaseHas('page_attachments', ['id' => $attachment->id]); + } + + /** + * 파일을 비운 temp_key 디렉토리는 남기지 않는다 (회귀) + * + * 파일만 지우고 디렉토리를 남기면 페이지 작성 폼 세션마다 빈 디렉토리가 쌓여, 정리를 + * 돌려도 저장소 흔적은 계속 늘어난다. + * + * @scenario age=past_retention, attachment_state=temp_unlinked + * + * @effects page_temp_prune_removes_empty_directory + */ + #[Test] + public function emptied_temp_directory_is_removed(): void + { + $attachment = $this->makeAttachment(['path' => 'temp/tempkey-empty-dir/'.uniqid('t-').'.png']); + $attachment->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('sirsoft-page:prune-temp-attachments')->assertSuccessful(); + + $storage = new ModuleStorageDriver('sirsoft-page', 'modules'); + + $this->assertSame( + [], + $storage->files('attachments', 'temp/tempkey-empty-dir'), + '디렉토리 안의 파일은 모두 삭제되어야 한다.' + ); + $this->assertDirectoryDoesNotExist( + rtrim($storage->getBasePath('attachments'), '/\\').'/temp/tempkey-empty-dir', + '비워진 temp 디렉토리는 남기지 않는다.' + ); + } + + /** + * 같은 temp_key 에 처리되지 않은 파일이 남아 있으면 디렉토리를 지우지 않는다. + * + * @scenario age=past_retention, attachment_state=temp_unlinked + * + * @effects page_temp_prune_keeps_directory_with_remaining_files + */ + #[Test] + public function temp_directory_with_remaining_files_is_kept(): void + { + $directory = 'temp/tempkey-partial'; + + $processed = $this->makeAttachment(['path' => $directory.'/'.uniqid('done-').'.png']); + $processed->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + // 같은 디렉토리에 이번 회차 대상이 아닌 파일이 남아 있는 상태 + $storage = new ModuleStorageDriver('sirsoft-page', 'modules'); + $storage->put('attachments', $directory.'/keep.png', 'bytes'); + + $this->artisan('sirsoft-page:prune-temp-attachments')->assertSuccessful(); + + $this->assertTrue($storage->exists('attachments', $directory.'/keep.png')); + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Feature/PageAttachmentPolicyTest.php b/modules/_bundled/sirsoft-page/tests/Feature/PageAttachmentPolicyTest.php index 3e31a8a0..d83f1538 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/PageAttachmentPolicyTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/PageAttachmentPolicyTest.php @@ -7,6 +7,7 @@ use Modules\Sirsoft\Page\Http\Requests\UploadPageAttachmentRequest; use Modules\Sirsoft\Page\Models\PageAttachment; use Modules\Sirsoft\Page\Module; use Modules\Sirsoft\Page\Services\PageAttachmentService; +use Modules\Sirsoft\Page\Services\PageSettingsService; use Modules\Sirsoft\Page\Tests\ModuleTestCase; /** @@ -22,20 +23,25 @@ use Modules\Sirsoft\Page\Tests\ModuleTestCase; class PageAttachmentPolicyTest extends ModuleTestCase { /** - * 첨부 설정을 주입합니다. + * 첨부 설정을 저장합니다. * - * 모듈 설정은 SettingsServiceProvider 가 `g7_settings.modules.*` config 로 올려 두고 - * `g7_module_settings()` 로 조회한다 (board 관례). + * config 를 직접 주입하지 않고 **설정 서비스를 경유**한다 — 직접 주입하면 + * 설정이 미러에 도달하는 경로가 통째로 우회되어, 서비스가 없어 미러가 영구 + * 미존재였던 결함(공개이슈 #109)이 테스트에서 드러나지 않는다. * * @param array $overrides 덮어쓸 설정 값 */ private function writeAttachmentSettings(array $overrides = []): void { - config(['g7_settings.modules.sirsoft-page.attachment' => array_merge([ - 'max_count' => 5, - 'max_size_mb' => 10, - 'allowed_types' => ['image/jpeg', 'image/png', 'application/pdf'], - ], $overrides)]); + config(['g7_settings.modules.sirsoft-page' => null]); + + app(PageSettingsService::class)->saveSettings([ + 'attachment' => array_merge([ + 'max_count' => 5, + 'max_size_mb' => 10, + 'allowed_types' => ['image/jpeg', 'image/png', 'application/pdf'], + ], $overrides), + ]); } /** diff --git a/modules/_bundled/sirsoft-page/tests/Feature/PageSettingsMirrorTest.php b/modules/_bundled/sirsoft-page/tests/Feature/PageSettingsMirrorTest.php new file mode 100644 index 00000000..310bb43c --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Feature/PageSettingsMirrorTest.php @@ -0,0 +1,77 @@ +<?php + +namespace Modules\Sirsoft\Page\Tests\Feature; + +use App\Support\ExtensionSettingsMirror; +use Illuminate\Support\Facades\Config; +use Modules\Sirsoft\Page\Services\PageSettingsService; +use Modules\Sirsoft\Page\Tests\ModuleTestCase; + +/** + * 페이지 모듈 설정 미러 테스트 (공개이슈 #109) + * + * 이 모듈에는 `ModuleSettingsInterface` 구현체가 없어 `g7_settings.modules.sirsoft-page` + * 미러가 영구히 존재하지 않았고, 소비자는 항상 인자 기본값으로 폴백했습니다. + * 기존 정책 테스트는 config 를 직접 주입해 우회하고 있어 이 결함이 검출되지 않았습니다 — + * 여기서는 **주입 없이** 설정 서비스 경유로 값이 도달하는지를 본다. + * + * @scenario scope=module, trigger=save, trigger=cache_clear, value_origin=defaults_only + * + * @effects page_module_mirror_exists, page_module_consumers_read_configured_values, module_mirror_refreshed_after_save + */ +class PageSettingsMirrorTest extends ModuleTestCase +{ + /** + * 설정 서비스가 defaults.json 을 읽어 전체 설정을 만든다. + */ + public function test_settings_service_reads_defaults(): void + { + $settings = app(PageSettingsService::class)->getAllSettings(); + + $this->assertArrayHasKey('attachment', $settings); + $this->assertArrayHasKey('max_count', $settings['attachment']); + $this->assertIsInt($settings['attachment']['max_count']); + } + + /** + * 미러가 채워지고, 소비자가 config 직접 주입 없이 그 값을 읽는다. + */ + public function test_mirror_is_populated_without_direct_config_injection(): void + { + // 미러가 비어 있는 상태에서 출발한다 + Config::set('g7_settings.modules.sirsoft-page', null); + $this->assertSame(0, (int) g7_module_settings('sirsoft-page', 'attachment.max_count', 0)); + + app(ExtensionSettingsMirror::class)->refreshModule('sirsoft-page'); + + $expected = (int) app(PageSettingsService::class)->getSetting('attachment.max_count'); + + $this->assertGreaterThan(0, $expected, 'defaults.json 의 max_count 가 0 입니다'); + $this->assertSame( + $expected, + (int) g7_module_settings('sirsoft-page', 'attachment.max_count', 0), + '미러가 채워지지 않아 소비자가 기본값 폴백을 탑니다' + ); + } + + /** + * 설정 저장이 같은 프로세스의 미러를 즉시 갱신한다. + */ + public function test_save_refreshes_the_mirror_in_the_same_process(): void + { + $service = app(PageSettingsService::class); + $current = (int) $service->getSetting('attachment.max_count'); + $next = $current + 3; + + $service->saveSettings(['attachment' => array_merge( + $service->getSettings('attachment'), + ['max_count' => $next] + )]); + + $this->assertSame( + $next, + (int) g7_module_settings('sirsoft-page', 'attachment.max_count', 0), + '저장 후에도 미러가 옛 값을 유지합니다' + ); + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php b/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php index 850dbf09..1e6fc0b8 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/Repositories/PageRepositorySearchTest.php @@ -19,6 +19,9 @@ use Tests\TestCase; * 따라서 이 스위트만 매 테스트 fresh DB + commit 되는 INSERT 로 FULLTEXT 경로를 * 실제 검증한다. (ModuleTestCase 의 DatabaseTransactions 와 상호배타.) */ +// audit:allow test-extension-base-class reason: InnoDB FULLTEXT 는 커밋된 행만 색인하므로 +// ModuleTestCase 의 DatabaseTransactions 와 상호배타 — RefreshDatabase + 수동 정리로 격리하고 +// 모듈 부팅(오토로드/Provider/마이그레이션)은 setUp 이 동일 로직으로 수행한다 class PageRepositorySearchTest extends TestCase { use RefreshDatabase; @@ -104,8 +107,12 @@ class PageRepositorySearchTest extends TestCase $file = $moduleBasePath.'/src/'.str_replace('\\', '/', $relativeClass).'.php'; } - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php new file mode 100644 index 00000000..8d3e313d --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php @@ -0,0 +1,325 @@ +<?php + +namespace Modules\Sirsoft\Page\Tests\Feature\User; + +// FeatureTestCase를 수동으로 require (autoload 전에 로드 필요) +require_once __DIR__.'/../../FeatureTestCase.php'; + +use App\Models\User; +use Illuminate\Support\Facades\URL; +use Mockery; +use Modules\Sirsoft\Page\Models\Page; +use Modules\Sirsoft\Page\Models\PageAttachment; +use Modules\Sirsoft\Page\Services\PageAttachmentService; +use Modules\Sirsoft\Page\Tests\FeatureTestCase; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * 공개 페이지 첨부 미리보기/다운로드 접근 게이트 테스트 (KVE-2026-1914 A-6 S-1) + * + * preview() 는 과거 발행상태 무관 공개 서빙이라, 미발행(초안) 페이지의 첨부 썸네일을 + * hash 만 있으면 무인가로 볼 수 있었다(preview↔download 게이트 불일치). 이 클래스는 + * preview 가 download 와 동일하게 "발행 첨부는 공개, 미발행 첨부는 pages.read 관리자만" + * 게이트를 적용하는지 실제 HTTP 요청으로 고정한다. + * + * 게이트 통과/차단을 응답코드로 명확히 구분하기 위해 Service 를 모킹한다 — + * 게이트 통과 시 200(스트리밍), 게이트 차단 시 컨트롤러가 Service 호출 전에 404 를 낸다. + */ +class PublicPageAttachmentAccessTest extends FeatureTestCase +{ + private const BASE = '/api/modules/sirsoft-page/pages/attachment'; + + protected function tearDown(): void + { + Page::where('slug', 'like', 'attach-access-%')->forceDelete(); + Mockery::close(); + parent::tearDown(); + } + + /** + * Service 를 모킹해 게이트 통과 시 200 을 반환하도록 만든다. + * + * @param PageAttachment $attachment getByHash 가 반환할 첨부 + */ + private function mockServicePassThrough(PageAttachment $attachment): void + { + $svc = Mockery::mock(PageAttachmentService::class); + $svc->shouldReceive('getByHash')->andReturn($attachment); + $svc->shouldReceive('preview')->andReturn(new StreamedResponse(fn () => null, 200)); + $svc->shouldReceive('download')->andReturn(new StreamedResponse(fn () => null, 200)); + $this->app->instance(PageAttachmentService::class, $svc); + } + + /** + * 첨부를 만든다. + * + * @param bool $published 부모 페이지 발행 여부 + * @param string $slug 페이지 슬러그 + * @return PageAttachment 생성된 첨부 + */ + private function makeAttachment(bool $published, string $slug): PageAttachment + { + $pageFactory = Page::factory()->state(['slug' => $slug]); + if ($published) { + $pageFactory = $pageFactory->published(); + } + $page = $pageFactory->create(); + + return PageAttachment::factory()->image()->create(['page_id' => $page->id]); + } + + // ── preview 게이트 (S-1 핵심) ────────────────────────── + + /** + * 미발행 페이지 첨부 미리보기는 비인가 사용자에게 404 로 차단된다 (S-1 수정 핵심) + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_blocked_for_guest + */ + public function test_unpublished_preview_blocked_for_anonymous(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-draft'); + $this->mockServicePassThrough($attachment); + + $this->getJson(self::BASE.'/'.$attachment->hash.'/preview') + ->assertStatus(404); + } + + /** + * 미발행 페이지 첨부 미리보기는 권한 없는 로그인 사용자에게도 404 로 차단된다 + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_blocked_without_permission + */ + public function test_unpublished_preview_blocked_for_user_without_permission(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-draft-user'); + $this->mockServicePassThrough($attachment); + + $user = User::factory()->create(); + + $this->actingAs($user) + ->getJson(self::BASE.'/'.$attachment->hash.'/preview') + ->assertStatus(404); + } + + /** + * 미발행 페이지 첨부 미리보기는 pages.read 관리자에게 허용된다 (초안 편집 썸네일 회귀 방지) + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_allowed_with_pages_read + */ + public function test_unpublished_preview_allowed_for_admin_with_pages_read(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-draft-admin'); + $this->mockServicePassThrough($attachment); + + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $this->actingAs($admin) + ->getJson(self::BASE.'/'.$attachment->hash.'/preview') + ->assertStatus(200); + } + + /** + * 발행된 페이지 첨부 미리보기는 비인가 사용자에게도 공개된다 (정상 흐름 회귀 방지) + * + * @scenario resource=page_attachment, parent_state=public + * + * @effects published_page_attachment_preview_still_public + */ + public function test_published_preview_allowed_for_anonymous(): void + { + $attachment = $this->makeAttachment(true, 'attach-access-live'); + $this->mockServicePassThrough($attachment); + + $this->getJson(self::BASE.'/'.$attachment->hash.'/preview') + ->assertStatus(200); + } + + // ── download 게이트 (기존 동작 회귀 대조) ────────────── + + /** + * 미발행 페이지 첨부 다운로드는 비인가 사용자에게 404 로 차단된다 (기존 게이트 회귀 대조) + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_download_blocked_for_guest + */ + public function test_unpublished_download_blocked_for_anonymous(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-dl-draft'); + $this->mockServicePassThrough($attachment); + + $this->getJson(self::BASE.'/'.$attachment->hash) + ->assertStatus(404); + } + + /** + * 미발행 페이지 첨부 다운로드는 pages.read 관리자에게 허용된다. + * + * preview 축은 4건(게스트/무권한 회원/관리자/발행글)인데 download 축은 차단 1건뿐이라 + * 두 경로의 커버리지 강도가 갈려 있었다. 게이트를 조일 때 download 만 과차단으로 + * 회귀해도 스위트가 green 이 되므로 허용 축을 함께 고정한다. + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_download_allowed_with_pages_read + */ + public function test_unpublished_download_allowed_for_admin_with_pages_read(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-dl-draft-admin'); + $this->mockServicePassThrough($attachment); + + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $this->actingAs($admin) + ->getJson(self::BASE.'/'.$attachment->hash) + ->assertStatus(200); + } + + /** + * 발행된 페이지 첨부 다운로드는 비인가 사용자에게도 공개된다 (정상 흐름 회귀 방지). + * + * @scenario resource=page_attachment, parent_state=public + * + * @effects published_page_attachment_download_still_public + */ + public function test_published_download_allowed_for_anonymous(): void + { + $attachment = $this->makeAttachment(true, 'attach-access-dl-live'); + $this->mockServicePassThrough($attachment); + + $this->getJson(self::BASE.'/'.$attachment->hash) + ->assertStatus(200); + } + + // ── 서명 preview URL (미발행 썸네일 <img> 렌더 경로) ────────────── + // + // 브라우저 <img src> 는 Authorization 헤더를 실을 수 없어, 게이트가 조여진 뒤 + // 관리자 상세 화면의 미발행 첨부 썸네일이 무인증 요청 → 404 로 깨졌다. + // 게이트를 통과한 응답(관리자 상세)이 한시 서명 URL 을 발급하고, 서빙 엔드포인트가 + // 유효 서명을 허용하는 방식으로 복구한다 — 무서명 요청 게이트는 종전과 동일하다. + + /** + * 유효한 한시 서명 preview URL 은 무인증 요청(<img>)에도 미발행 첨부를 서빙한다. + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_allowed_with_valid_signature + */ + public function test_unpublished_preview_allowed_with_valid_signature_without_auth(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-signed'); + $this->mockServicePassThrough($attachment); + + $signedUrl = URL::temporarySignedRoute( + 'api.modules.sirsoft-page.pages.attachment.preview', + now()->addMinutes(30), + ['hash' => $attachment->hash], + absolute: false + ); + + $this->getJson($signedUrl)->assertStatus(200); + } + + /** + * 서명이 변조된 preview URL 은 미발행 첨부를 서빙하지 않는다 (404 존재 은닉). + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_blocked_with_tampered_signature + */ + public function test_unpublished_preview_blocked_with_tampered_signature(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-tampered'); + $this->mockServicePassThrough($attachment); + + $signedUrl = URL::temporarySignedRoute( + 'api.modules.sirsoft-page.pages.attachment.preview', + now()->addMinutes(30), + ['hash' => $attachment->hash], + absolute: false + ); + // signature 쿼리 값의 마지막 8자를 뒤집어 변조한다 + $tampered = preg_replace_callback( + '/(signature=)([0-9a-f]+)/', + fn ($m) => $m[1].substr($m[2], 0, -8).strrev(substr($m[2], -8)), + $signedUrl + ); + + $this->getJson($tampered)->assertStatus(404); + } + + /** + * 만료된 서명 preview URL 은 미발행 첨부를 서빙하지 않는다 (한시성 보장). + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects unpublished_page_attachment_preview_blocked_with_expired_signature + */ + public function test_unpublished_preview_blocked_with_expired_signature(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-expired'); + $this->mockServicePassThrough($attachment); + + $signedUrl = URL::temporarySignedRoute( + 'api.modules.sirsoft-page.pages.attachment.preview', + now()->subMinute(), + ['hash' => $attachment->hash], + absolute: false + ); + + $this->getJson($signedUrl)->assertStatus(404); + } + + /** + * 관리자 상세 응답은 미발행 페이지 첨부에 서명 preview URL 을 직렬화하고, + * 그 URL 은 무인증 <img> 요청으로도 열린다 (렌더 계약의 양끝 검증). + * + * @scenario resource=page_attachment, parent_state=restricted + * + * @effects admin_response_serializes_signed_preview_url_for_unpublished_page + */ + public function test_admin_detail_serializes_signed_preview_url_for_unpublished_page(): void + { + $attachment = $this->makeAttachment(false, 'attach-access-serialize'); + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $previewUrl = $this->actingAs($admin) + ->getJson('/api/modules/sirsoft-page/admin/pages/'.$attachment->page_id) + ->assertStatus(200) + ->json('data.attachments.0.preview_url'); + + $this->assertIsString($previewUrl); + $this->assertStringContainsString('signature=', $previewUrl); + + // 발급된 서명 URL 은 인증 헤더 없이도 서빙된다 (<img> 경로) + $this->mockServicePassThrough($attachment); + $this->getJson($previewUrl)->assertStatus(200); + } + + /** + * 발행 페이지 첨부의 preview URL 은 종전과 동일한 무서명 공개 hash 경로다 + * (SEO·캐시 안정성 — 발행 콘텐츠에 만료성 URL 이 섞이는 회귀 방지). + * + * @scenario resource=page_attachment, parent_state=public + * + * @effects published_page_serializes_plain_preview_url + */ + public function test_published_page_serializes_plain_preview_url(): void + { + $attachment = $this->makeAttachment(true, 'attach-access-plain'); + $admin = $this->createAdminUser(['sirsoft-page.pages.read']); + + $previewUrl = $this->actingAs($admin) + ->getJson('/api/modules/sirsoft-page/admin/pages/'.$attachment->page_id) + ->assertStatus(200) + ->json('data.attachments.0.preview_url'); + + $this->assertSame(self::BASE.'/'.$attachment->hash.'/preview', $previewUrl); + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php index 0fd947eb..f5980169 100644 --- a/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageControllerTest.php @@ -358,15 +358,17 @@ class PublicPageControllerTest extends FeatureTestCase $response->assertStatus(404); } - // ─── 썸네일(preview) 공개 서빙 매트릭스 (별건: 관리자 상세 썸네일 401 회귀) ─── + // ─── 썸네일(preview) 서빙 매트릭스 (KVE-2026-1914 이후: 미발행은 게이트) ─── /** - * 미발행 페이지의 이미지 첨부 미리보기(썸네일)는 비로그인이어도 200을 반환한다. + * 미발행 페이지의 이미지 첨부 미리보기(썸네일)는 비로그인에게 404 로 차단된다. * - * 썸네일 <img> 는 토큰을 실을 수 없으므로 공개 hash 서빙으로 둔다. - * 보안은 hash 비추측성 + 파일 다운로드 권한 게이트가 담당한다(트레이드오프 수용). + * KVE-2026-1914 이전에는 공개 hash 서빙 트레이드오프로 200 이었으나, 이후 + * preview 도 download 와 동일한 발행상태 게이트를 적용한다. 권한자 화면의 + * <img> 썸네일은 응답 직렬화 시점에 발급되는 한시 서명 URL 이 담당한다 + * (PublicPageAttachmentAccessTest 서명 축 참조). */ - public function test_public_preview_unpublished_image_returns_200_for_guest(): void + public function test_public_preview_unpublished_image_blocked_for_guest(): void { $admin = $this->createAdminUser([]); @@ -392,7 +394,7 @@ class PublicPageControllerTest extends FeatureTestCase $response = $this->getJson("/api/modules/sirsoft-page/pages/attachment/{$attachment->hash}/preview"); - $response->assertStatus(200); + $response->assertStatus(404); } /** diff --git a/modules/_bundled/sirsoft-page/tests/ModuleTestCase.php b/modules/_bundled/sirsoft-page/tests/ModuleTestCase.php index 807f3353..46a00035 100644 --- a/modules/_bundled/sirsoft-page/tests/ModuleTestCase.php +++ b/modules/_bundled/sirsoft-page/tests/ModuleTestCase.php @@ -241,8 +241,12 @@ abstract class ModuleTestCase extends TestCase $file = $moduleBasePath.'/src/'.str_replace('\\', '/', $relativeClass).'.php'; } - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php new file mode 100644 index 00000000..d5dbe441 --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php @@ -0,0 +1,61 @@ +<?php + +namespace Modules\Sirsoft\Page\Tests\Unit\Listeners; + +use Modules\Sirsoft\Page\Listeners\Ckeditor5ReferenceSourcesListener; +use Modules\Sirsoft\Page\Tests\ModuleTestCase; +use PHPUnit\Framework\Attributes\Test; + +/** + * 페이지 참조 소스 등록 리스너 테스트 + * + * 이 리스너가 빠지면 페이지 본문에만 쓰이는 이미지가 "미참조" 로 판정돼 자동 정리 대상이 + * 되므로, 구독 형식(filter)과 append 계약을 회귀로 고정합니다. + */ +class Ckeditor5ReferenceSourcesListenerTest extends ModuleTestCase +{ + /** + * @effects reference_source_listener_subscribes_as_filter + */ + #[Test] + public function it_subscribes_to_the_reference_source_filter_hook(): void + { + $hooks = Ckeditor5ReferenceSourcesListener::getSubscribedHooks(); + + $this->assertArrayHasKey('sirsoft-ckeditor5.image.filter_reference_sources', $hooks); + $this->assertSame('filter', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['type']); + $this->assertSame('addPageSources', $hooks['sirsoft-ckeditor5.image.filter_reference_sources']['method']); + } + + /** + * 버전 스냅샷도 포함해야 과거 버전으로 되돌릴 때 그 시점 이미지가 남아 있다. + * + * @effects reference_source_listener_appends_without_dropping + */ + #[Test] + public function it_appends_page_and_version_sources_while_preserving_existing_ones(): void + { + $existing = [['table' => 'mail_templates', 'columns' => ['body']]]; + + $result = (new Ckeditor5ReferenceSourcesListener)->addPageSources($existing); + + $this->assertContains($existing[0], $result); + $this->assertContains(['table' => 'pages', 'columns' => ['content']], $result); + $this->assertContains(['table' => 'page_versions', 'columns' => ['content']], $result); + } + + /** + * @effects reference_source_listener_schema_contract + */ + #[Test] + public function appended_sources_follow_the_declared_schema(): void + { + foreach ((new Ckeditor5ReferenceSourcesListener)->addPageSources([]) as $source) { + $this->assertArrayHasKey('table', $source); + $this->assertArrayHasKey('columns', $source); + $this->assertIsString($source['table']); + $this->assertIsArray($source['columns']); + $this->assertNotEmpty($source['columns']); + } + } +} diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php index 9ab8c001..1a59e480 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php @@ -7,6 +7,7 @@ use App\Support\Query\BoundedCount; use App\Support\Query\BoundedPage; use Carbon\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; use Mockery; use Modules\Sirsoft\Page\Listeners\SearchPagesListener; use Modules\Sirsoft\Page\Services\PageService; @@ -49,6 +50,42 @@ class SearchPagesListenerTest extends ModuleTestCase parent::tearDown(); } + /** + * 검색 실패가 "결과 0건" 으로 위장되지 않고 failed 페이로드로 표면화되는지 확인 (#103) + * + * @effects failed_flag_in_response, exception_stack_logged + */ + public function test_search_failure_surfaces_failed_payload_and_logs_exception(): void + { + Log::spy(); + + $exception = new \RuntimeException('DB 오류 재현'); + $this->pageServiceMock + ->shouldReceive('searchByKeywordWithCursor') + ->andThrow($exception); + + $context = [ + 'type' => 'pages', + 'q' => '문의', + 'sort' => 'relevance', + 'page' => 1, + 'per_page' => 10, + 'user' => null, + 'request' => null, + ]; + + $result = $this->listener->searchPages([], $context); + + $this->assertArrayHasKey('pages', $result); + $this->assertTrue($result['pages']['failed'] ?? false, '실패 카테고리에는 failed 플래그가 실려야 합니다.'); + $this->assertFalse($result['pages']['total_is_exact'], '실패한 0건을 "정확한 0건" 으로 말하면 안 됩니다.'); + $this->assertSame([], $result['pages']['items']); + + Log::shouldHaveReceived('error') + ->withArgs(fn (string $message, array $ctx = []) => ($ctx['exception'] ?? null) === $exception) + ->once(); + } + // ─── 훅 구독 등록 ─────────────────────────────────── /** @@ -310,7 +347,64 @@ class SearchPagesListenerTest extends ModuleTestCase } /** - * PageService 예외 발생 시 결과가 변경되지 않는지 확인 + * 제목에 삽입된 태그가 하이라이트 필드에서 이스케이프되는지 확인 (⑧) + * + * @scenario case=search_highlight_escape + * + * @effects highlighted_fields_escaped + */ + public function test_search_pages_escapes_markup_in_highlighted_title(): void + { + $fakePage = $this->makeFakePage('<img src=x onerror=alert(1)> 이용약관', 'terms', ''); + + $this->pageServiceMock + ->shouldReceive('searchByKeyword') + ->once() + ->andReturn($this->boundedPage(new Collection([$fakePage]), 1)); + + $result = $this->listener->searchPages([], [ + 'q' => '이용', + 'type' => 'all', + 'all_tab_limit' => 5, + ]); + + $item = $result['pages']['items'][0]; + $this->assertStringNotContainsString('<img', $item['title_highlighted']); + $this->assertStringContainsString('<img', $item['title_highlighted']); + $this->assertStringContainsString('<mark>이용</mark>', $item['title_highlighted']); + } + + /** + * 엔티티로 인코딩된 태그가 본문 프리뷰에서 부활하지 않는지 확인 (N-8) + * + * @scenario case=preview_entity_no_resurrect + * + * @effects preview_does_not_resurrect_entities + */ + public function test_search_pages_does_not_resurrect_entity_encoded_tags_in_preview(): void + { + $fakePage = $this->makeFakePage('약관', 'terms', '<script>alert(1)</script> 약관 본문입니다.'); + + $this->pageServiceMock + ->shouldReceive('searchByKeyword') + ->once() + ->andReturn($this->boundedPage(new Collection([$fakePage]), 1)); + + $result = $this->listener->searchPages([], [ + 'q' => '약관', + 'type' => 'all', + 'all_tab_limit' => 5, + ]); + + $item = $result['pages']['items'][0]; + $this->assertStringNotContainsString('<script>', $item['content_preview']); + $this->assertStringNotContainsString('<script>', $item['content_preview_highlighted']); + } + + /** + * PageService 예외 발생 시 다른 카테고리는 보존하고 pages 는 failed 로 표면화하는지 확인 + * + * 종전에는 키 미설정으로 삼켜 화면이 "검색 결과 없음" 을 그렸다 (#103). */ public function test_search_pages_handles_service_exception_gracefully(): void { @@ -325,8 +419,11 @@ class SearchPagesListenerTest extends ModuleTestCase 'type' => 'all', ]); - // 예외 발생 시 기존 results 그대로 반환 - $this->assertEquals($original, $result); + // 예외가 전파되지 않고, 다른 카테고리 결과는 그대로 보존된다 + $this->assertSame($original['posts'], $result['posts']); + // pages 는 실패 페이로드로 표면화된다 + $this->assertTrue($result['pages']['failed'] ?? false); + $this->assertFalse($result['pages']['total_is_exact']); } // ─── buildPagesResponse() ─────────────────────────── diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php index 3db014c0..6a1c64df 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Listeners/SeoPageCacheListenerTest.php @@ -57,17 +57,24 @@ class SeoPageCacheListenerTest extends ModuleTestCase $this->assertArrayHasKey('sirsoft-page.page.after_delete', $hooks); // 버전 복원도 SEO 캐시 무효화가 필요하다 (복원 후 봇에 복원 전 버전 잔존 회귀 방지) $this->assertArrayHasKey('sirsoft-page.page.after_restore', $hooks); + // 발행/미발행 전환(단건 setPublished·일괄 bulk-publish)은 after_publish 만 발화한다. + // 미구독 시 발행 상태를 바꿔도 SEO 캐시가 이전 상태(soft-404/이전 콘텐츠)로 남고 + // 사이트맵 증분 색인도 수행되지 않는다 (7.0.7 사전점검 브라우저+curl 실측 발견). + $this->assertArrayHasKey('sirsoft-page.page.after_publish', $hooks); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_create']['method']); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_update']['method']); // delete 는 모델 상태와 무관하게 색인을 제거해야 하므로 전용 메서드로 분리 $this->assertEquals('onPageDelete', $hooks['sirsoft-page.page.after_delete']['method']); $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_restore']['method']); + // publish 는 모델의 published 최신 상태로 onPageChange 가 색인/해제를 판정한다 + $this->assertEquals('onPageChange', $hooks['sirsoft-page.page.after_publish']['method']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_create']['priority']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_update']['priority']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_delete']['priority']); $this->assertEquals(20, $hooks['sirsoft-page.page.after_restore']['priority']); + $this->assertEquals(20, $hooks['sirsoft-page.page.after_publish']['priority']); } // ─── onPageChange: 캐시 무효화 상태 검증 ────────────── diff --git a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php index 6ee8ca29..64812d5c 100644 --- a/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php +++ b/modules/_bundled/sirsoft-page/tests/Unit/Services/PageServiceTest.php @@ -561,6 +561,67 @@ class PageServiceTest extends ModuleTestCase $this->service->restoreVersion($page, $version->id); } + /** + * 일괄 발행 경로도 단건 경로와 같은 스코프 게이트를 적용하는지 확인 + * + * `PATCH admin/pages/bulk-publish` 에는 `{page}` 파라미터가 없어 PermissionMiddleware 의 + * 스코프 검사가 통째로 스킵된다. 위 단건 경로 5곳은 전부 검사하는데 일괄만 비어 있으면 + * 그 경로가 우회로가 된다. + */ + public function test_bulk_change_publish_status_throws_exception_for_scope_denied(): void + { + $scopeUser = $this->setupScopeUser(ScopeType::Self); + Auth::setUser($scopeUser); + $this->actingAs($scopeUser); + + $mine = Page::factory()->create([ + 'slug' => 'test-svc-bulk-mine', + 'published' => false, + 'created_by' => $scopeUser->id, + 'updated_by' => $scopeUser->id, + ]); + + $theirs = Page::factory()->create([ + 'slug' => 'test-svc-bulk-theirs', + 'published' => false, + 'created_by' => $this->adminUser->id, + 'updated_by' => $this->adminUser->id, + ]); + + try { + $this->service->bulkChangePublishStatus([$mine->id, $theirs->id], true); + $this->fail('스코프 밖 페이지가 섞이면 거부되어야 합니다'); + } catch (AccessDeniedHttpException) { + // 기대 경로 + } + + // all-or-nothing — 내 페이지도 발행되지 않아야 한다(트랜잭션 롤백). + $this->assertFalse($mine->fresh()->published, '거부된 일괄 요청은 아무것도 바꾸지 않아야 합니다'); + $this->assertFalse($theirs->fresh()->published); + } + + /** + * (과차단 회귀) 자기 소유 페이지만 넘기면 일괄 발행이 정상 동작한다 + */ + public function test_bulk_change_publish_status_allows_own_pages(): void + { + $scopeUser = $this->setupScopeUser(ScopeType::Self); + Auth::setUser($scopeUser); + $this->actingAs($scopeUser); + + $mine = Page::factory()->create([ + 'slug' => 'test-svc-bulk-own', + 'published' => false, + 'created_by' => $scopeUser->id, + 'updated_by' => $scopeUser->id, + ]); + + $count = $this->service->bulkChangePublishStatus([$mine->id], true); + + $this->assertSame(1, $count); + $this->assertTrue($mine->fresh()->published); + } + // ─── 헬퍼 ──────────────────────────────────────── /** diff --git a/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-serving.yaml b/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-serving.yaml index 643cb96e..b9fc6cfa 100644 --- a/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-serving.yaml +++ b/modules/_bundled/sirsoft-page/tests/scenarios/page-attachment-serving.yaml @@ -9,6 +9,12 @@ feature: 페이지 첨부 서빙 (썸네일 공개 hash 서빙 + 다운로드 권한 게이트) description: | + [2026-08-18 후속] 아래 "preview 는 모든 조합 공개(200)" 정책은 KVE-2026-1914(A-6 S-1)로 + 대체되었다 — preview 도 download 와 동일한 발행상태 게이트를 적용하며, 권한자 화면의 + <img> 썸네일은 한시 서명 URL 로 서빙한다. 현행 계약과 회귀 가드는 + tests/scenarios/file-serving-parent-state-gate.yaml(코어) 이 SSoT 다. + 본 문서는 당시 결정 기록으로 보존한다. + 별건(관리자 상세 첨부 썸네일·다운로드 미노출): 커밋 d48c684 가 관리자 화면 첨부 URL 을 auth:sanctum 보호 admin 라우트로 바꿔, 토큰을 실을 수 없는 <img>/<a> 브라우저 직접 GET 이 발행/미발행 무관 401 로 깨지던 회귀를 수정한다. 게시판·이커머스 표준으로 정렬. diff --git a/modules/_bundled/sirsoft-page/tests/scenarios/security-fixes-2026-08.yaml b/modules/_bundled/sirsoft-page/tests/scenarios/security-fixes-2026-08.yaml new file mode 100644 index 00000000..aa88ff1b --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/scenarios/security-fixes-2026-08.yaml @@ -0,0 +1,19 @@ +feature: 페이지 검색 하이라이트 이스케이프 (2026-08 보안 수정) + +description: | + 2026-08 비공개 보안 제보(⑧/N-8) 대응 — SearchPagesListener 가 검색 하이라이트에서 + 원문 마크업을 이스케이프하지 않고, 프리뷰가 strip_tags → html_entity_decode 순서라 + 엔티티 인코딩된 태그가 부활하던 문제. 코어 app/Search/SearchHighlighter(이스케이프 후 + <mark> 삽입, decode → strip 순서) 위임으로 board/ecommerce 와 동일 SSoT 를 공유한다. + +axes: + case: [search_highlight_escape, preview_entity_no_resurrect] + +exclusions: [] + +effects: + - highlighted_fields_escaped + - preview_does_not_resurrect_entities + +test_files: + - modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php diff --git a/modules/_bundled/sirsoft-page/tests/scenarios/temp-attachment-cleanup.yaml b/modules/_bundled/sirsoft-page/tests/scenarios/temp-attachment-cleanup.yaml new file mode 100644 index 00000000..8c56582d --- /dev/null +++ b/modules/_bundled/sirsoft-page/tests/scenarios/temp-attachment-cleanup.yaml @@ -0,0 +1,50 @@ +feature: 미연결 임시 페이지 첨부 회수 (Temp Page Attachment Cleanup) + +description: | + 페이지 작성 폼에서 첨부만 올리고 저장 없이 이탈하면 `temp_key` 가 남고 `page_id` 가 비어 + 있는 행과 그 파일이 영구 잔존한다. 공개 이슈 #115 동류 결함. + + 연결 시점에 본경로로 옮겨지므로 `temp_key` 잔존 = 끝내 연결되지 않음이 확정이라 오탐 + 여지가 구조적으로 없다. 운영 데이터가 아닌 폼 세션 부산물이므로 토글 없이 상시 실행되며, + 보존기간만 커맨드 옵션으로 조정한다. + + 삭제 순서는 파일 먼저, 기록 나중. 행마다 disk 가 다를 수 있어 그 행의 disk 를 향해 지운다. + +axes: + attachment_state: [temp_unlinked, linked_to_page] + age: [within_retention, past_retention] + +exclusions: + - { attachment_state: linked_to_page, age: within_retention, reason: "연결된 첨부는 나이와 무관하게 대상이 아니다 — 경과분 1건으로 충분" } + +sub_flows: + - id: dry_run + description: 판정만 수행하고 파일·기록을 건드리지 않는 미리보기 경로 + effects: + - page_temp_prune_dry_run + - id: retention_guard + description: 보존기간 1일 미만 자기 차단 (방금 올린 임시 첨부까지 지우는 사고 방지) + effects: + - page_temp_prune_days_guard + - id: temp_directory_hygiene + description: | + 파일을 지운 뒤 남는 temp 디렉토리 처리. 비워졌을 때만 지우고, 다른 폼 세션의 파일이 + 남아 있으면 그대로 둔다 (디렉토리째 지우면 아직 작성 중인 페이지의 첨부가 사라진다). + effects: + - page_temp_prune_removes_empty_directory + - page_temp_prune_keeps_directory_with_remaining_files + - id: reference_source_registry + description: 페이지 본문·버전 스냅샷을 에디터 이미지 참조 소스로 등록하는 리스너 + effects: + - reference_source_listener_subscribes_as_filter + - reference_source_listener_appends_without_dropping + - reference_source_listener_schema_contract + +effects: + - page_temp_prune_deletes_file_and_record + - page_temp_prune_keeps_within_retention + - page_temp_prune_skips_linked + +test_files: + - modules/_bundled/sirsoft-page/tests/Feature/Console/PruneTempAttachmentsCommandTest.php + - modules/_bundled/sirsoft-page/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php diff --git a/plugins/_bundled/gnuboard7-hello_plugin/CHANGELOG.md b/plugins/_bundled/gnuboard7-hello_plugin/CHANGELOG.md index 0812e80f..d22ec620 100644 --- a/plugins/_bundled/gnuboard7-hello_plugin/CHANGELOG.md +++ b/plugins/_bundled/gnuboard7-hello_plugin/CHANGELOG.md @@ -4,11 +4,12 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. -## [0.1.1] - 2026-08-10 +## [0.1.1] - 2026-08-17 ### Fixed - 오류 안내가 뜨기는 하지만 내용이 비어 있던 문제를 수정했습니다. 설정 저장에 실패하면 서버가 알려 준 사유가 그대로 표시됩니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. ## [0.1.0] - 2026-07-01 diff --git a/plugins/_bundled/gnuboard7-hello_plugin/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/gnuboard7-hello_plugin/resources/layouts/admin/plugin_settings.json index 3d506894..5d9ce987 100644 --- a/plugins/_bundled/gnuboard7-hello_plugin/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/gnuboard7-hello_plugin/resources/layouts/admin/plugin_settings.json @@ -205,7 +205,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/gnuboard7-hello_plugin/tests/PluginTestCase.php b/plugins/_bundled/gnuboard7-hello_plugin/tests/PluginTestCase.php index cdb06623..d62baee2 100644 --- a/plugins/_bundled/gnuboard7-hello_plugin/tests/PluginTestCase.php +++ b/plugins/_bundled/gnuboard7-hello_plugin/tests/PluginTestCase.php @@ -5,6 +5,7 @@ namespace Plugins\Gnuboard7\HelloPlugin\Tests; use App\Extension\HookManager; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Schema; +use Plugins\Gnuboard7\HelloPlugin\Providers\HelloPluginServiceProvider; use Tests\TestCase; /** @@ -52,7 +53,7 @@ abstract class PluginTestCase extends TestCase $this->registerPluginAutoload(); - $this->app->register(\Plugins\Gnuboard7\HelloPlugin\Providers\HelloPluginServiceProvider::class); + $this->app->register(HelloPluginServiceProvider::class); $this->runCoreMigrationIfNeeded(); @@ -153,8 +154,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $pluginBasePath.'/src/'.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } @@ -188,7 +193,7 @@ abstract class PluginTestCase extends TestCase $priority = is_array($config) ? ($config['priority'] ?? 10) : 10; $type = is_array($config) ? ($config['type'] ?? 'action') : 'action'; - $listenerInstance = new $listenerClass(); + $listenerInstance = new $listenerClass; $callback = [$listenerInstance, $method]; if ($type === 'filter') { diff --git a/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md b/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md index b60a6a74..6cd71ec7 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-ckeditor5/CHANGELOG.md @@ -4,6 +4,34 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Security + +- 에디터 이미지 업로드의 권한 검사가 클라이언트가 보낸 값에 좌우되어, 그 값이 비어 있으면 검사가 통째로 건너뛰어지던 구조를 제거했습니다. 업로드는 종전처럼 관리자만 할 수 있으며, 이제 그 판정은 전적으로 서버가 합니다. (sir.kr 커뮤니티의 Xbuilder 님께서 제보해주셨습니다.) + +### Added + +- 에디터 이미지 업로드에 파일 가공 필터 훅 제공 — 확장에서 업로드 이미지를 저장 전에 변환할 수 있습니다. +- 에디터에 올린 이미지를 코어의 공개 자산 스토리지(S3+CDN 등) 디스크에 저장하고 본문에는 CDN 직접 주소로 서빙할 수 있습니다. 에디터 설정의 「공개 자산 디스크」에서 이 플러그인만 다른 디스크를 쓰도록 개별 설정할 수도 있습니다(비우면 코어 설정 따름). 디스크를 바꿔도 기존 이미지는 원래 위치에서 계속 정상 표시됩니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) + +#### 미사용 이미지 정리 + +- 에디터에 올렸지만 어떤 콘텐츠에서도 더 이상 사용되지 않는 이미지를 자동으로 정리할 수 있습니다. 플러그인 설정에서 자동 정리를 켜면(기본 꺼짐) 설정한 보존기간(기본 30일)이 지난 미사용 이미지를 파일과 기록까지 매일 삭제합니다. (#115 @Tuwasduliebst 님께서 건의해주셨습니다.) +- 「에디터 업로드 이미지」 관리 화면이 추가되었습니다. 업로드한 이미지를 미리보기·업로더·용량과 함께 확인하고, 사용 중인지 여부를 배지로 구분해 필요한 것만 골라 삭제할 수 있습니다(개별·선택 삭제). +- 사용 여부는 게시글·페이지·상품 설명 등 등록된 콘텐츠를 훑어 판정합니다. 자체 콘텐츠를 가진 확장은 판정 대상에 자기 콘텐츠를 등록할 수 있어, 그 확장에서만 쓰이는 이미지도 「사용 중」으로 올바르게 인식됩니다. 비활성 상태의 모듈이 있으면 그 콘텐츠를 확인할 수 없으므로 자동 정리는 안전하게 건너뛰고, 관리 화면에도 판정이 불완전할 수 있음을 알립니다. +- 무엇이 지워질지 먼저 확인하려면 서버에서 `php artisan sirsoft-ckeditor5:prune-unused-images --dry-run` 을 실행하면 됩니다. + +### Changed + +- 코어 7.0.7의 외부 스크립트 보안 정책 강화에 맞춰, 에디터가 사용하는 CKEditor CDN 주소(`cdn.ckeditor.com`)를 이 플러그인의 신뢰 출처로 선언했습니다. 정책이 강화된 이후에도 위지윅 에디터가 정상적으로 로드됩니다. +- 코어 최소 요구 버전을 7.0.7 로 상향했습니다. + +### Fixed + +- 플러그인 설정 화면의 「에디터 이미지 업로드」 스위치가 레이아웃 편집기에서 편집 대상으로 선택되지 않던 문제를 수정했습니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-10 ### Changed diff --git a/plugins/_bundled/sirsoft-ckeditor5/components.json b/plugins/_bundled/sirsoft-ckeditor5/components.json index e96e25d7..8d2e4e45 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/components.json +++ b/plugins/_bundled/sirsoft-ckeditor5/components.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "identifier": "sirsoft-ckeditor5", - "version": "1.0.1", + "version": "1.0.2", "components": { "basic": [], "composite": [], diff --git a/plugins/_bundled/sirsoft-ckeditor5/composer.json b/plugins/_bundled/sirsoft-ckeditor5/composer.json index dd6a92bd..1c74f925 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/composer.json +++ b/plugins/_bundled/sirsoft-ckeditor5/composer.json @@ -2,7 +2,7 @@ "name": "plugins/sirsoft-ckeditor5", "description": "CKEditor 5 WYSIWYG Editor Plugin for Gnuboard7 platform", "type": "library", - "version": "1.0.1", + "version": "1.0.2", "authors": [ { "name": "sirsoft", diff --git a/plugins/_bundled/sirsoft-ckeditor5/config/settings/defaults.json b/plugins/_bundled/sirsoft-ckeditor5/config/settings/defaults.json index 658366f4..b475374c 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/config/settings/defaults.json +++ b/plugins/_bundled/sirsoft-ckeditor5/config/settings/defaults.json @@ -7,12 +7,18 @@ "imageUpload": true, "imageMaxSizeMb": 2, "editorHeight": 400, - "toolbar": "standard" + "toolbar": "standard", + "public_asset_disk": "", + "unusedImageCleanup": false, + "unusedImageRetentionDays": 30 }, "frontend_schema": { "imageUpload": { "expose": true }, "imageMaxSizeMb": { "expose": true }, "editorHeight": { "expose": true }, - "toolbar": { "expose": true } + "toolbar": { "expose": true }, + "public_asset_disk": { "expose": false }, + "unusedImageCleanup": { "expose": false }, + "unusedImageRetentionDays": { "expose": false } } -} \ No newline at end of file +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/database/migrations/2026_08_14_000001_add_created_at_index_to_ckeditor5_image_uploads.php b/plugins/_bundled/sirsoft-ckeditor5/database/migrations/2026_08_14_000001_add_created_at_index_to_ckeditor5_image_uploads.php new file mode 100644 index 00000000..ccc8d7f9 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/database/migrations/2026_08_14_000001_add_created_at_index_to_ckeditor5_image_uploads.php @@ -0,0 +1,39 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * 마이그레이션 실행 + * + * 미참조 이미지 정리는 "보존기간이 지난 오래된 업로드" 를 created_at 오름차순으로 + * 훑으므로 해당 컬럼에 인덱스를 둔다. 관리 화면의 기본 정렬(최신순)도 같은 인덱스를 쓴다. + */ + public function up(): void + { + if (! Schema::hasTable('ckeditor5_image_uploads')) { + return; + } + + Schema::table('ckeditor5_image_uploads', function (Blueprint $table) { + $table->index('created_at', 'ckeditor5_image_uploads_created_at_index'); + }); + } + + /** + * 마이그레이션 롤백 + */ + public function down(): void + { + if (! Schema::hasTable('ckeditor5_image_uploads')) { + return; + } + + Schema::table('ckeditor5_image_uploads', function (Blueprint $table) { + $table->dropIndex('ckeditor5_image_uploads_created_at_index'); + }); + } +}; diff --git a/plugins/_bundled/sirsoft-ckeditor5/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-ckeditor5/dist/js/plugin.iife.js index 61d09b94..474882bc 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-ckeditor5/dist/js/plugin.iife.js @@ -332,7 +332,7 @@ var SirsoftCkeditor5=function(G){"use strict";const E=new Map,Y="https://cdn.cke .ck.ck-editor__editable li::marker { color: #d1d5db; } - `,document.head.appendChild(o)}let x=null;function ot(){x||(x=new MutationObserver(()=>{q()}),x.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]}))}function nt(){x&&(x.disconnect(),x=null)}const rt={ko:"한국어",en:"English",ja:"日本語",zh:"中文",fr:"Français",de:"Deutsch",es:"Español"};function ct(t){return rt[t]??t.toUpperCase()}function lt(){const t=window.G7Core;return t?.locale?.supported?t.locale.supported():[localStorage.getItem("g7_locale")||"ko"]}function I(){return window.G7Core?.locale?.current?.()??localStorage.getItem("g7_locale")??"ko"}function at(t,o,e){const r=[t.Essentials,t.Bold,t.Italic,t.Underline,t.Strikethrough,t.Link,t.Paragraph,t.Heading,t.BlockQuote,t.List,t.Alignment,t.Table,t.TableToolbar,t.TableProperties,t.TableCellProperties,t.Indent,t.IndentBlock,t.PasteFromOffice,t.GeneralHtmlSupport,t.Image,t.ImageResize,t.ImageStyle,t.ImageCaption,t.ImageToolbar];return o==="full"&&r.push(t.MediaEmbed,t.HorizontalLine,t.CodeBlock,t.SourceEditing,t.FontSize,t.FontColor,t.FontBackgroundColor),e&&r.push(t.ImageUpload,t.SimpleUploadAdapter),r}function it(t){const o="/api/plugins/sirsoft-ckeditor5/upload";return t?`${o}?permission=${encodeURIComponent(t)}`:o}function dt(){const t=localStorage.getItem("auth_token");return t?{Authorization:`Bearer ${t}`}:{}}async function F(t,o){const e=window.CKEDITOR;if(!e)throw new Error("[sirsoft-ckeditor5] CKEDITOR 전역 객체를 찾을 수 없습니다. CDN 스크립트가 로드되었는지 확인하세요.");const r=O[o.toolbar]||O.standard,c=o.imageUpload?r:r.filter(n=>n!=="uploadImage"),l={initialData:o.initialContent,plugins:at(e,o.toolbar,o.imageUpload),toolbar:{items:c},table:{contentToolbar:["tableColumn","tableRow","mergeTableCells","|","tableProperties","tableCellProperties"],tableProperties:{defaultProperties:{borderStyle:"solid",borderColor:"#d1d5db",borderWidth:"1px",alignment:"left"}},tableCellProperties:{defaultProperties:{borderStyle:"solid",borderColor:"#d1d5db",borderWidth:"1px",padding:"8px"}}},image:{toolbar:["imageStyle:inline","imageStyle:alignLeft","imageStyle:alignCenter","imageStyle:alignRight","|","toggleImageCaption","|","resizeImage"],resizeOptions:[{name:"resizeImage:original",label:"Original",value:null},{name:"resizeImage:25",label:"25%",value:"25"},{name:"resizeImage:50",label:"50%",value:"50"},{name:"resizeImage:75",label:"75%",value:"75"}],resizeUnit:"%"},indentBlock:{offset:40,unit:"px"},link:{defaultProtocol:"https://",decorators:{openInNewTab:{mode:"automatic",callback:n=>n.startsWith("http://")||n.startsWith("https://"),attributes:{target:"_blank",rel:"noopener noreferrer"}}}},htmlSupport:{allow:[{name:/.*/,attributes:!0,classes:!0,styles:!0}]},placeholder:o.placeholder,language:I()};o.imageUpload&&(l.simpleUpload={uploadUrl:it(o.uploadPermission),headers:dt()});const i=await e.ClassicEditor.create(t,l),s=`ckeditor5-height-style-${o.containerId}`,b=document.getElementById(s);if(b)b.textContent=`#${o.containerId} .ck-editor__editable { min-height: ${o.height}px !important; }`;else{const n=document.createElement("style");n.id=s,n.textContent=`#${o.containerId} .ck-editor__editable { min-height: ${o.height}px !important; }`,document.head.appendChild(n)}return o.readOnly&&i.enableReadOnlyMode("read-only"),i}function st(t,o,e,r,c){const l=document.createElement("div");l.className="ckeditor5-locale-tabs";const i=new Map;o.forEach(n=>{const a=document.createElement("div");a.dataset.locale=n,n!==e&&(a.style.cssText="display:none;");const C=document.createElement("div");a.appendChild(C),i.set(n,C)});const s=()=>{c&&l.querySelectorAll("button").forEach(n=>{const a=n.dataset.locale;if(!a||n.classList.contains("is-active")){const h=n.querySelector(".ck5-check");h&&h.remove();return}const g=!!c(a)?.trim(),p=n.querySelector(".ck5-check");if(g&&!p){const h=document.createElement("i");h.className="fas fa-check ck5-check",n.appendChild(h)}else!g&&p&&p.remove()})},b=o[0];return o.forEach(n=>{const a=n===b,C=n===e,g=document.createElement("button");g.type="button",g.dataset.locale=n,g.className=[C?"is-active":"",a?"is-default":""].filter(Boolean).join(" "),g.innerHTML=`<i class="fas fa-globe ck5-lang-icon"></i><span>${ct(n)}</span>${a?'<span class="ck5-required">*</span>':""}`,g.addEventListener("click",async()=>{l.querySelectorAll("button").forEach(p=>{const L=p.dataset.locale===n;p.classList.toggle("is-active",L)}),i.forEach((p,h)=>{const L=p.parentElement;L&&(L.style.display=h===n?"block":"none")}),r&&await r(n),s()}),l.appendChild(g)}),t.appendChild(l),i.forEach(n=>{n.parentElement&&t.appendChild(n.parentElement)}),{tabsEl:l,contentEls:i,updateCheckIcons:s}}function T(t,o,e,r){const c=window.G7Core;if(!c?.state?.setLocal)return;const l={[`form.${t}_mode`]:"html",hasChanges:!0};r?l[`form.${t}.${o}`]=e:l[`form.${t}`]=e,c.state.setLocal(l,{debounce:300,debounceKey:`ckeditor-sync-${t}`,render:!1,selfManaged:!0})}async function ut(t,o){const e=t.params??{},c=`ckeditor5-${e.name??"content"}`,l=E.get(c);if(l&&l.size>0){const y=[];l.forEach(u=>{u&&typeof u.destroy=="function"&&y.push(u.destroy().catch(()=>{}))}),await Promise.allSettled(y),E.delete(c)}const i=document.getElementById(c);if(!i){console.warn(`[sirsoft-ckeditor5] 컨테이너 엘리먼트를 찾을 수 없습니다: #${c}`);return}K(),tt(),q(),ot(),await R(I());const s=window.G7Core,b=s?.state?.get()||{},n=b._global?.plugins?.["sirsoft-ckeditor5"]??b.plugins?.["sirsoft-ckeditor5"]??{},a=e.name??"content",C=e.multilingual===!0||e.multilingual==="true",g=e.readOnly===!0||e.readOnly==="true",p=e.imageUpload!==void 0?e.imageUpload===!0||e.imageUpload==="true":n.imageUpload===!0||n.imageUpload==="true",h=e.uploadPermission??"",L=e.placeholder??"",bt=e.height!==void 0?Number(e.height)||400:Number(n.editorHeight)||400,gt=(e.toolbar!==void 0?e.toolbar:n.toolbar)??"standard";let _={};if(C){try{typeof e.content=="string"&&e.content.startsWith("{")&&!e.content.startsWith("{{")?_=JSON.parse(e.content):typeof e.content=="object"&&e.content!==null&&(_=e.content)}catch{_={}}if(Object.keys(_).length===0&&a){const u=(s?.state?.getLocal?.()??{})?.form?.[a];if(u&&typeof u=="object"&&!Array.isArray(u)?_=u:typeof u=="string"&&u&&(_={[I()]:u}),Object.keys(_).length===0){const k=s?.state?.get?.()??{},d=(k?._global?.selectedItem??k?.selectedItem)?.[a];d&&typeof d=="object"&&!Array.isArray(d)&&(_=d)}}}const P=new Map;E.set(c,P);const J={placeholder:L,readOnly:g,imageUpload:p,uploadPermission:h,height:bt,toolbar:gt,containerId:c};if(C){const y=lt(),u=I(),k={current:new Map},$={current:()=>{}},d=async f=>{if(P.has(f))return;const U=k.current.get(f);if(!U)return;const pt=_[f]??"";try{const A=await F(U,{...J,initialContent:pt});P.set(f,A);let X=!0;if(A.model.document.on("change:data",()=>{if(X)return;const M=A.getData();T(a,f,M,!0),$.current()}),X=!1,a){const M=window.G7Core;(M?.state?.getLocal?.()??{}).form?.[`${a}_mode`]!=="html"&&M.state.setLocal({[`form.${a}_mode`]:"html"})}}catch(A){console.error(`[sirsoft-ckeditor5] 에디터 초기화 오류 (locale: ${f}):`,A)}},S=f=>{const U=P.get(f);return U?U.getData?.()??null:null},{contentEls:B,updateCheckIcons:w}=st(i,y,u,d,S);k.current=B,$.current=w,await d(u),setTimeout(()=>{y.filter(f=>f!==u).forEach(f=>{d(f)})},2e3)}else{const y=typeof e.content=="string"?e.content:"";let k=y.startsWith("{{")&&y.endsWith("}}")?"":y;if(!k&&a){const S=(s?.state?.getLocal?.()??{})?.form?.[a];typeof S=="string"&&S&&(k=S)}const $=document.createElement("div");i.appendChild($);try{const d=await F($,{...J,initialContent:k}),S=I();P.set(S,d);let B=!0;if(d.model.document.on("change:data",()=>{if(B)return;const w=d.getData();T(a,S,w,!1)}),B=!1,k){const w=d.getData();(!w||w.trim()==="")&&d.setData(k)}if(a){const w=window.G7Core;(w?.state?.getLocal?.()??{}).form?.[`${a}_mode`]!=="html"&&w.state.setLocal({[`form.${a}_mode`]:"html"})}}catch(d){console.error("[sirsoft-ckeditor5] 에디터 초기화 오류:",d),E.delete(c)}}}async function ft(t,o){const r=`ckeditor5-${t.params?.name??"content"}`,c=E.get(r);if(!c||c.size===0)return;const l=[];c.forEach(s=>{s&&typeof s.destroy=="function"&&l.push(s.destroy().catch(b=>{console.warn("[sirsoft-ckeditor5] destroyEditor error:",b)}))}),await Promise.allSettled(l),E.delete(r);const i=document.getElementById(`ckeditor5-height-style-${r}`);i&&i.remove(),E.size===0&&nt()}const m="sirsoft-ckeditor5",W="ckeditor5-content-styles",V="ckeditor5-content-styles-override",kt="https://cdn.ckeditor.com/ckeditor5/43.3.1/ckeditor5.css";function mt(){if(!document.getElementById(W)){const t=document.createElement("link");t.id=W,t.rel="stylesheet",t.href=kt,document.head.appendChild(t)}if(!document.getElementById(V)){const t=document.createElement("style");t.id=V,t.textContent=` + `,document.head.appendChild(o)}let x=null;function ot(){x||(x=new MutationObserver(()=>{q()}),x.observe(document.documentElement,{attributes:!0,attributeFilter:["class"]}))}function nt(){x&&(x.disconnect(),x=null)}const rt={ko:"한국어",en:"English",ja:"日本語",zh:"中文",fr:"Français",de:"Deutsch",es:"Español"};function ct(t){return rt[t]??t.toUpperCase()}function lt(){const t=window.G7Core;return t?.locale?.supported?t.locale.supported():[localStorage.getItem("g7_locale")||"ko"]}function I(){return window.G7Core?.locale?.current?.()??localStorage.getItem("g7_locale")??"ko"}function at(t,o,e){const r=[t.Essentials,t.Bold,t.Italic,t.Underline,t.Strikethrough,t.Link,t.Paragraph,t.Heading,t.BlockQuote,t.List,t.Alignment,t.Table,t.TableToolbar,t.TableProperties,t.TableCellProperties,t.Indent,t.IndentBlock,t.PasteFromOffice,t.GeneralHtmlSupport,t.Image,t.ImageResize,t.ImageStyle,t.ImageCaption,t.ImageToolbar];return o==="full"&&r.push(t.MediaEmbed,t.HorizontalLine,t.CodeBlock,t.SourceEditing,t.FontSize,t.FontColor,t.FontBackgroundColor),e&&r.push(t.ImageUpload,t.SimpleUploadAdapter),r}function it(){return"/api/plugins/sirsoft-ckeditor5/upload"}function dt(){const t=localStorage.getItem("auth_token");return t?{Authorization:`Bearer ${t}`}:{}}async function F(t,o){const e=window.CKEDITOR;if(!e)throw new Error("[sirsoft-ckeditor5] CKEDITOR 전역 객체를 찾을 수 없습니다. CDN 스크립트가 로드되었는지 확인하세요.");const r=O[o.toolbar]||O.standard,c=o.imageUpload?r:r.filter(n=>n!=="uploadImage"),l={initialData:o.initialContent,plugins:at(e,o.toolbar,o.imageUpload),toolbar:{items:c},table:{contentToolbar:["tableColumn","tableRow","mergeTableCells","|","tableProperties","tableCellProperties"],tableProperties:{defaultProperties:{borderStyle:"solid",borderColor:"#d1d5db",borderWidth:"1px",alignment:"left"}},tableCellProperties:{defaultProperties:{borderStyle:"solid",borderColor:"#d1d5db",borderWidth:"1px",padding:"8px"}}},image:{toolbar:["imageStyle:inline","imageStyle:alignLeft","imageStyle:alignCenter","imageStyle:alignRight","|","toggleImageCaption","|","resizeImage"],resizeOptions:[{name:"resizeImage:original",label:"Original",value:null},{name:"resizeImage:25",label:"25%",value:"25"},{name:"resizeImage:50",label:"50%",value:"50"},{name:"resizeImage:75",label:"75%",value:"75"}],resizeUnit:"%"},indentBlock:{offset:40,unit:"px"},link:{defaultProtocol:"https://",decorators:{openInNewTab:{mode:"automatic",callback:n=>n.startsWith("http://")||n.startsWith("https://"),attributes:{target:"_blank",rel:"noopener noreferrer"}}}},htmlSupport:{allow:[{name:/.*/,attributes:!0,classes:!0,styles:!0}]},placeholder:o.placeholder,language:I()};o.imageUpload&&(l.simpleUpload={uploadUrl:it(),headers:dt()});const i=await e.ClassicEditor.create(t,l),s=`ckeditor5-height-style-${o.containerId}`,m=document.getElementById(s);if(m)m.textContent=`#${o.containerId} .ck-editor__editable { min-height: ${o.height}px !important; }`;else{const n=document.createElement("style");n.id=s,n.textContent=`#${o.containerId} .ck-editor__editable { min-height: ${o.height}px !important; }`,document.head.appendChild(n)}return o.readOnly&&i.enableReadOnlyMode("read-only"),i}function st(t,o,e,r,c){const l=document.createElement("div");l.className="ckeditor5-locale-tabs";const i=new Map;o.forEach(n=>{const a=document.createElement("div");a.dataset.locale=n,n!==e&&(a.style.cssText="display:none;");const C=document.createElement("div");a.appendChild(C),i.set(n,C)});const s=()=>{c&&l.querySelectorAll("button").forEach(n=>{const a=n.dataset.locale;if(!a||n.classList.contains("is-active")){const h=n.querySelector(".ck5-check");h&&h.remove();return}const g=!!c(a)?.trim(),p=n.querySelector(".ck5-check");if(g&&!p){const h=document.createElement("i");h.className="fas fa-check ck5-check",n.appendChild(h)}else!g&&p&&p.remove()})},m=o[0];return o.forEach(n=>{const a=n===m,C=n===e,g=document.createElement("button");g.type="button",g.dataset.locale=n,g.className=[C?"is-active":"",a?"is-default":""].filter(Boolean).join(" "),g.innerHTML=`<i class="fas fa-globe ck5-lang-icon"></i><span>${ct(n)}</span>${a?'<span class="ck5-required">*</span>':""}`,g.addEventListener("click",async()=>{l.querySelectorAll("button").forEach(p=>{const L=p.dataset.locale===n;p.classList.toggle("is-active",L)}),i.forEach((p,h)=>{const L=p.parentElement;L&&(L.style.display=h===n?"block":"none")}),r&&await r(n),s()}),l.appendChild(g)}),t.appendChild(l),i.forEach(n=>{n.parentElement&&t.appendChild(n.parentElement)}),{tabsEl:l,contentEls:i,updateCheckIcons:s}}function T(t,o,e,r){const c=window.G7Core;if(!c?.state?.setLocal)return;const l={[`form.${t}_mode`]:"html",hasChanges:!0};r?l[`form.${t}.${o}`]=e:l[`form.${t}`]=e,c.state.setLocal(l,{debounce:300,debounceKey:`ckeditor-sync-${t}`,render:!1,selfManaged:!0})}async function ut(t,o){const e=t.params??{},c=`ckeditor5-${e.name??"content"}`,l=E.get(c);if(l&&l.size>0){const y=[];l.forEach(u=>{u&&typeof u.destroy=="function"&&y.push(u.destroy().catch(()=>{}))}),await Promise.allSettled(y),E.delete(c)}const i=document.getElementById(c);if(!i){console.warn(`[sirsoft-ckeditor5] 컨테이너 엘리먼트를 찾을 수 없습니다: #${c}`);return}K(),tt(),q(),ot(),await R(I());const s=window.G7Core,m=s?.state?.get()||{},n=m._global?.plugins?.["sirsoft-ckeditor5"]??m.plugins?.["sirsoft-ckeditor5"]??{},a=e.name??"content",C=e.multilingual===!0||e.multilingual==="true",g=e.readOnly===!0||e.readOnly==="true",p=e.imageUpload!==void 0?e.imageUpload===!0||e.imageUpload==="true":n.imageUpload===!0||n.imageUpload==="true",h=e.placeholder??"",L=e.height!==void 0?Number(e.height)||400:Number(n.editorHeight)||400,mt=(e.toolbar!==void 0?e.toolbar:n.toolbar)??"standard";let _={};if(C){try{typeof e.content=="string"&&e.content.startsWith("{")&&!e.content.startsWith("{{")?_=JSON.parse(e.content):typeof e.content=="object"&&e.content!==null&&(_=e.content)}catch{_={}}if(Object.keys(_).length===0&&a){const u=(s?.state?.getLocal?.()??{})?.form?.[a];if(u&&typeof u=="object"&&!Array.isArray(u)?_=u:typeof u=="string"&&u&&(_={[I()]:u}),Object.keys(_).length===0){const k=s?.state?.get?.()??{},d=(k?._global?.selectedItem??k?.selectedItem)?.[a];d&&typeof d=="object"&&!Array.isArray(d)&&(_=d)}}}const P=new Map;E.set(c,P);const J={placeholder:h,readOnly:g,imageUpload:p,height:L,toolbar:mt,containerId:c};if(C){const y=lt(),u=I(),k={current:new Map},$={current:()=>{}},d=async f=>{if(P.has(f))return;const U=k.current.get(f);if(!U)return;const gt=_[f]??"";try{const A=await F(U,{...J,initialContent:gt});P.set(f,A);let X=!0;if(A.model.document.on("change:data",()=>{if(X)return;const M=A.getData();T(a,f,M,!0),$.current()}),X=!1,a){const M=window.G7Core;(M?.state?.getLocal?.()??{}).form?.[`${a}_mode`]!=="html"&&M.state.setLocal({[`form.${a}_mode`]:"html"})}}catch(A){console.error(`[sirsoft-ckeditor5] 에디터 초기화 오류 (locale: ${f}):`,A)}},S=f=>{const U=P.get(f);return U?U.getData?.()??null:null},{contentEls:B,updateCheckIcons:w}=st(i,y,u,d,S);k.current=B,$.current=w,await d(u),setTimeout(()=>{y.filter(f=>f!==u).forEach(f=>{d(f)})},2e3)}else{const y=typeof e.content=="string"?e.content:"";let k=y.startsWith("{{")&&y.endsWith("}}")?"":y;if(!k&&a){const S=(s?.state?.getLocal?.()??{})?.form?.[a];typeof S=="string"&&S&&(k=S)}const $=document.createElement("div");i.appendChild($);try{const d=await F($,{...J,initialContent:k}),S=I();P.set(S,d);let B=!0;if(d.model.document.on("change:data",()=>{if(B)return;const w=d.getData();T(a,S,w,!1)}),B=!1,k){const w=d.getData();(!w||w.trim()==="")&&d.setData(k)}if(a){const w=window.G7Core;(w?.state?.getLocal?.()??{}).form?.[`${a}_mode`]!=="html"&&w.state.setLocal({[`form.${a}_mode`]:"html"})}}catch(d){console.error("[sirsoft-ckeditor5] 에디터 초기화 오류:",d),E.delete(c)}}}async function ft(t,o){const r=`ckeditor5-${t.params?.name??"content"}`,c=E.get(r);if(!c||c.size===0)return;const l=[];c.forEach(s=>{s&&typeof s.destroy=="function"&&l.push(s.destroy().catch(m=>{console.warn("[sirsoft-ckeditor5] destroyEditor error:",m)}))}),await Promise.allSettled(l),E.delete(r);const i=document.getElementById(`ckeditor5-height-style-${r}`);i&&i.remove(),E.size===0&&nt()}const b="sirsoft-ckeditor5",W="ckeditor5-content-styles",V="ckeditor5-content-styles-override",kt="https://cdn.ckeditor.com/ckeditor5/43.3.1/ckeditor5.css";function bt(){if(!document.getElementById(W)){const t=document.createElement("link");t.id=W,t.rel="stylesheet",t.href=kt,document.head.appendChild(t)}if(!document.getElementById(V)){const t=document.createElement("style");t.id=V,t.textContent=` /* 이미지 캡션 - 다크모드 */ html.dark .ck-content .image > figcaption { color: hsl(0, 0%, 80%); background-color: hsl(0, 0%, 15%); } @@ -369,4 +369,4 @@ var SirsoftCkeditor5=function(G){"use strict";const E=new Map,Y="https://cdn.cke .ck-content ul ul ul { list-style-type: square; padding-left: 2em; } .ck-content ol ol { list-style-type: lower-alpha; padding-left: 2em; } .ck-content ol ol ol { list-style-type: lower-roman; padding-left: 2em; } - `,document.head.appendChild(t)}}const v={initEditor:ut,destroyEditor:ft,injectContentCss:mt},z=window.G7Core?.createLogger?.(`Plugin:${m}`)??{log:(...t)=>console.log(`[Plugin:${m}]`,...t),warn:(...t)=>console.warn(`[Plugin:${m}]`,...t),error:(...t)=>console.error(`[Plugin:${m}]`,...t)};function Q(t=!1){const o=window.G7Core?.getActionDispatcher?.();if(o)Object.entries(v).forEach(([e,r])=>{const c=`${m}.${e}`;o.registerHandler(c,r,{category:"plugin",source:m})}),z.log(`${Object.keys(v).length} handler(s) registered:`,Object.keys(v).map(e=>`${m}.${e}`));else if(t){let e=0;const r=50,c=()=>{const l=window.G7Core?.getActionDispatcher?.();l?(Object.entries(v).forEach(([i,s])=>{const b=`${m}.${i}`;l.registerHandler(b,s,{category:"plugin",source:m})}),z.log(`${Object.keys(v).length} handler(s) registered:`,Object.keys(v).map(i=>`${m}.${i}`))):(e++,e<=r?(z.warn(`ActionDispatcher not found, retrying... (${e}/${r})`),setTimeout(c,100)):z.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};c()}else z.warn("ActionDispatcher not found, handlers not registered")}function j(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>Q(!0));else{const t=!!window.G7Core?.getActionDispatcher?.();Q(!t)}}return j(),typeof window<"u"&&(window.__SirsoftCkeditor5={identifier:m,handlers:Object.keys(v),initPlugin:j}),G.initPlugin=j,Object.defineProperty(G,Symbol.toStringTag,{value:"Module"}),G}({}); + `,document.head.appendChild(t)}}const v={initEditor:ut,destroyEditor:ft,injectContentCss:bt},z=window.G7Core?.createLogger?.(`Plugin:${b}`)??{log:(...t)=>console.log(`[Plugin:${b}]`,...t),warn:(...t)=>console.warn(`[Plugin:${b}]`,...t),error:(...t)=>console.error(`[Plugin:${b}]`,...t)};function Q(t=!1){const o=window.G7Core?.getActionDispatcher?.();if(o)Object.entries(v).forEach(([e,r])=>{const c=`${b}.${e}`;o.registerHandler(c,r,{category:"plugin",source:b})}),z.log(`${Object.keys(v).length} handler(s) registered:`,Object.keys(v).map(e=>`${b}.${e}`));else if(t){let e=0;const r=50,c=()=>{const l=window.G7Core?.getActionDispatcher?.();l?(Object.entries(v).forEach(([i,s])=>{const m=`${b}.${i}`;l.registerHandler(m,s,{category:"plugin",source:b})}),z.log(`${Object.keys(v).length} handler(s) registered:`,Object.keys(v).map(i=>`${b}.${i}`))):(e++,e<=r?(z.warn(`ActionDispatcher not found, retrying... (${e}/${r})`),setTimeout(c,100)):z.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};c()}else z.warn("ActionDispatcher not found, handlers not registered")}function j(){if(document.readyState==="loading")document.addEventListener("DOMContentLoaded",()=>Q(!0));else{const t=!!window.G7Core?.getActionDispatcher?.();Q(!t)}}return j(),typeof window<"u"&&(window.__SirsoftCkeditor5={identifier:b,handlers:Object.keys(v),initPlugin:j}),G.initPlugin=j,Object.defineProperty(G,Symbol.toStringTag,{value:"Module"}),G}({}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md index 19f7e45f..f48760e8 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/README.md @@ -4,12 +4,13 @@ > 아래 표는 자동 생성됩니다. 각 문서를 열면 엔드포인트별 파라미터·응답·예시를 볼 수 있습니다. <!-- @generated:start:api-readme-index --> -- **문서 수**: 2 · **엔드포인트 수**: 2 +- **문서 수**: 3 · **엔드포인트 수**: 5 | 문서 | 도메인 | 엔드포인트 | | --- | --- | --- | | [images.md](images.md) | `images` | 1 | | [upload.md](upload.md) | `upload` | 1 | +| [uploads.md](uploads.md) | `uploads` | 3 | <!-- @generated:end --> diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md index 519eb643..ff3e6201 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/upload.md @@ -52,7 +52,7 @@ _이 엔드포인트는 표준 envelope(`success`/`message`/`data`)를 사용하 | 필드 | 타입 | 실측 예시값 | 용도/설명 | | --- | --- | --- | --- | -| url | string | `/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6` | 업로드된 이미지의 서빙 URL. 모델 접근자 `download_url` 이 생성하며 `/api/plugins/sirsoft-ckeditor5/images/{hash}` (hash = 12자리 hex) 형식이다. CKEditor 가 이 값을 그대로 `<img src>` 에 삽입한다. | +| url | string | `/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6` | 업로드된 이미지의 서빙 URL. 모델 접근자 `download_url` 이 생성한다. 기본(스트리밍)은 `/api/plugins/sirsoft-ckeditor5/images/{hash}` (hash = 12자리 hex) 형식이고, 공개 자산 디스크(플러그인 설정 `public_asset_disk` 또는 코어 전역 설정)가 URL 지원 디스크로 설정되어 있으면 그 디스크의 직접 URL(예: `https://cdn.example.com/sirsoft-ckeditor5/images/...`)이 반환된다. CKEditor 가 이 값을 그대로 `<img src>` 에 삽입한다. | 실패 시에는 최상위 `error.message` 만 반환합니다 (아래 에러 응답 표 참조). @@ -73,7 +73,7 @@ _이 엔드포인트는 표준 envelope(`success`/`message`/`data`)를 사용하 | 상태코드 | 의미 | 발생 조건 | | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | -| 403 | Forbidden | query 파라미터 `permission` 이 지정됐고 현재 사용자가 해당 권한을 갖지 못한 경우 — `{"error":{"message":"이미지 업로드 권한이 없습니다."}}` | +| 403 | Forbidden | 관리자(admin) 권한이 없는 경우 (라우트의 `admin` 미들웨어) | | 422 | Unprocessable Entity | 검증 규칙 위반 (`upload` 누락 / 파일 아님 / 이미지 아님 / 허용 MIME 아님 / 용량 초과). 응답은 Laravel 기본 `errors` 형식이 아니라 CKEditor 규격 `{"error":{"message":"<첫 번째 오류 메시지>"}}` | | 500 | Internal Server Error | 스토리지 저장 또는 업로드 기록 생성 중 예외 발생 — `{"error":{"message":"이미지 업로드에 실패했습니다."}}` | @@ -86,8 +86,8 @@ CKEditor5 의 SimpleUploadAdapter 가 에디터에 드롭/붙여넣은 이미지 - **응답 형식이 표준 envelope 가 아니다.** SimpleUploadAdapter 규격상 성공 시 HTTP 201 + 최상위 `{"url": "..."}`, 실패 시 4xx/5xx + `{"error": {"message": "..."}}` 를 반환한다. `ResponseHelper` 를 쓰지 않으므로 `data`/`success` 필드가 없다. 이 규격은 우리 코드가 정한 것이 아니다 — 응답을 파싱하는 주체가 CDN 으로 로드되는 CKEditor5 43.3.1 의 `SimpleUploadAdapter`(`resources/js/handlers/initEditor.ts` 의 `editorConfig.simpleUpload`)이므로 파싱 규약을 바꿀 수 없다. 컨트롤러의 각 응답 지점에는 이 사유로 `audit:allow response-helper-bypass` 를 명시해 두었으니, envelope 로 감싸는 "수정" 을 하면 에디터의 이미지 업로드가 조용히 실패한다. - **요청 파라미터**: multipart body 의 `upload` 필드(이미지 파일 1개). 허용 MIME 은 `jpeg,jpg,png,gif,webp`, 최대 크기는 플러그인 설정 `imageMaxSizeMb`(기본 2MB) 로 동적 결정된다. 검증 실패도 CKEditor 규격(`{"error":{"message":...}}`, HTTP 422)으로 응답한다. -- **선택 권한 게이트**: query 파라미터 `permission` 이 주어지면, 현재 사용자가 해당 권한을 갖지 못한 경우 403 `{"error":{"message":...}}`. 에디터를 임베드하는 화면이 업로드 권한을 세분화할 때 사용한다. -- 업로드 성공 시 반환하는 `url` 은 공개 서빙 엔드포인트(`GET /images/{hash}`)의 절대 URL 이다. +- **인가**: 업로드 권한은 서버측 라우트 게이트(`auth:sanctum` + `admin`)만으로 판정한다. 클라이언트가 검사 대상 권한명을 지정하던 방식(query 파라미터 `permission`)은 제거되었다. +- 업로드 성공 시 반환하는 `url` 은 기본적으로 공개 서빙 엔드포인트(`GET /images/{hash}`)의 절대 URL 이다. 단, 공개 자산 디스크(플러그인 설정 `public_asset_disk` > 코어 전역 `drivers.public_asset_disk`)가 직접 URL 을 지원하는 디스크(public / URL 이 설정된 s3 등)로 지정되어 있으면 스트리밍 경로 대신 그 디스크의 직접 URL(CDN)이 반환된다. 디스크 미설정('none' 포함)·URL 미지원 디스크·`core.storage.filter_url` 훅 차단 시에는 기존 스트리밍 경로로 폴백한다. **응답 예시** (성공 — CKEditor 규격, envelope 아님) diff --git a/plugins/_bundled/sirsoft-ckeditor5/docs/api/uploads.md b/plugins/_bundled/sirsoft-ckeditor5/docs/api/uploads.md new file mode 100644 index 00000000..7402a54a --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/docs/api/uploads.md @@ -0,0 +1,255 @@ +# Uploads API 레퍼런스 + +> **소유**: plugin `sirsoft-ckeditor5` · **생성**: `php artisan api:docgen` (실측 기반). @generated 블록은 재생성 시 갱신되며, 사람이 작성한 설명은 보존됩니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 이 문서는 에디터 업로드 이미지 관리(조회·삭제) 엔드포인트 레퍼런스입니다 +2. 각 엔드포인트: 메서드/URI/권한 + 요청 파라미터 표 + 요청 예시(raw HTTP) + 응답 필드 표 + 응답 예시(envelope) +3. 조회(uploads.read)와 삭제(uploads.delete)는 별도 권한입니다 — 삭제는 파일을 실제로 파기합니다 +4. 참조 상태(referenced)는 저장 컬럼이 아니라 조회 시점에 판정한 값입니다 +5. 갱신: 코드 변경 후 php artisan api:docgen 재실행 +``` + +--- + +### GET /api/plugins/sirsoft-ckeditor5/admin/uploads + +<!-- @generated:start:api.plugins.sirsoft-ckeditor5.admin.uploads.index --> + +- **라우트명**: `api.plugins.sirsoft-ckeditor5.admin.uploads.index` +- **컨트롤러**: `Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin\ImageUploadAdminController@index` +- **인증/권한**: `auth:sanctum`, `permission:admin,sirsoft-ckeditor5.uploads.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| page | query | integer | 아니오 | min 1 | 페이지 번호 (기본 1) | +| per_page | query | integer | 아니오 | 1 ~ 100 | 페이지 크기 (기본 20). 참조 판정을 페이지 단위로 수행하므로 이 값이 곧 판정 비용의 상한이다. | +| search | query | string | 아니오 | max 255 | 원본 파일명 또는 해시 부분 일치 | +| referenced | query | string | 아니오 | `all`, `referenced`, `unreferenced` | 참조 상태 필터 (기본 `all`) | +| date_from | query | date | 아니오 | — | 업로드 시작일 (그날 00:00:00 포함) | +| date_to | query | date | 아니오 | — | 업로드 종료일 (그날 23:59:59 포함) | +| sort_by | query | string | 아니오 | `created_at`, `file_size`, `original_name` | 정렬 컬럼 (기본 `created_at`). 목록 밖 컬럼은 422 | +| sort_order | query | string | 아니오 | `asc`, `desc` | 정렬 방향 (기본 `desc`) | + +**요청 예시** + +```http +GET /api/plugins/sirsoft-ckeditor5/admin/uploads?referenced=unreferenced&per_page=20 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| data[].id | integer | `12` | 업로드 기록 ID (삭제 요청의 대상 식별자) | +| data[].hash | string | `a1b2c3d4e5f6` | URL용 고유 해시 12자리 hex. 본문에 API 폴백형 URL 로 삽입될 때 이 값이 들어간다 | +| data[].original_name | string | `banner.png` | 업로드 당시 원본 파일명 | +| data[].file_size | integer | `78310` | 파일 크기 (bytes) | +| data[].mime_type | string | `image/png` | MIME 타입 | +| data[].uploaded_by | integer\|null | `1` | 업로드한 사용자 ID (탈퇴·미상이면 null) | +| data[].uploader_name | string\|null | `관리자` | 업로더 표시명. 관계가 로드된 경우에만 채워진다 | +| data[].created_at | string | `2026-08-14 19:20:11` | 업로드 일시 (요청자 시간대 기준 포맷) | +| data[].download_url | string | `/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6` | 이미지 서빙 URL. 공개 자산 디스크가 직접 URL 을 지원하면 CDN 주소가 반환된다 | +| data[].referenced | boolean | `false` | **조회 시점 판정값**(저장 컬럼 아님). 등록된 참조 소스 본문에 hash 또는 저장 파일명이 등장하면 true. 판정 불가 행은 안전 방향인 true 로 본다 | +| pagination.current_page | integer | `1` | 현재 페이지 | +| pagination.last_page | integer | `1` | 마지막 페이지 | +| pagination.per_page | integer | `20` | 페이지 크기 | +| pagination.total | integer | `3` | 총 건수 | +| pagination.from | integer\|null | `1` | 현재 페이지 첫 항목 순번 (비어 있으면 null) | +| pagination.to | integer\|null | `3` | 현재 페이지 마지막 항목 순번 (비어 있으면 null) | +| meta.scan_limited | boolean | `false` | 참조 상태 필터가 스캔 윈도우 상한에 걸렸는지. true 면 그보다 오래된 이미지는 목록에 포함되지 않는다 | +| meta.scan_window | integer | `500` | 참조 상태 필터가 훑는 최신 업로드 건수 상한 | +| meta.reference_sources_incomplete | boolean | `false` | 설치돼 있으나 비활성인 모듈이 있어 참조 판정이 불완전할 수 있는지. true 면 그 모듈 콘텐츠에서만 쓰이는 이미지가 「미참조」로 오판될 수 있으므로 화면은 경고를 표시해야 한다 | + +**응답 예시** + +```json +{ + "success": true, + "message": "요청이 성공적으로 처리되었습니다.", + "data": { + "data": [ + { + "id": 12, + "hash": "a1b2c3d4e5f6", + "original_name": "banner.png", + "file_size": 78310, + "mime_type": "image/png", + "uploaded_by": 1, + "uploader_name": "관리자", + "created_at": "2026-08-14 19:20:11", + "download_url": "/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6", + "referenced": false + } + ], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 1, + "from": 1, + "to": 1 + }, + "meta": { + "scan_limited": false, + "scan_window": 500, + "reference_sources_incomplete": false + } + } +} +``` + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | `sirsoft-ckeditor5.uploads.read` 권한이 없는 경우 | +| 422 | Unprocessable Entity | 허용 목록 밖 `sort_by`/`sort_order`/`referenced`, `per_page` 범위 초과, 날짜 형식 오류 | + +<!-- @generated:end --> + +**설명** + +에디터로 업로드된 이미지 목록을 관리 화면에 제공하는 엔드포인트다. + +- **참조 상태는 저장하지 않는다.** 참조 관계는 콘텐츠가 편집될 때마다 바뀌므로 스냅샷을 저장하면 "저장 당시엔 미참조였던" 이미지를 이후에 오판 삭제할 수 있다. 대신 판정 비용에 상한을 둔다 — 기본 목록은 현재 페이지(≤100행)만 판정하고, 참조 상태 필터를 쓸 때만 최신 `meta.scan_window` 건을 훑는다. +- **`meta.scan_limited` 가 true 면 목록이 전수가 아니다.** 화면은 이 값을 근거로 "최근 N건 기준" 안내를 표시해야 한다. 이 값을 무시하면 운영자가 "미참조 이미지가 이게 전부" 로 오해한다. +- 판정 토큰은 `hash` 와 저장 파일명 둘 다이다. 본문에 박히는 URL 이 API 폴백형(hash 포함)과 디스크 직접 URL(저장 파일명 포함) 두 형태이기 때문이며, 한쪽만 검사하면 다른 형태로 저장된 이미지를 미참조로 오판한다. + +--- + +### DELETE /api/plugins/sirsoft-ckeditor5/admin/uploads/{id} + +<!-- @generated:start:api.plugins.sirsoft-ckeditor5.admin.uploads.destroy --> + +- **라우트명**: `api.plugins.sirsoft-ckeditor5.admin.uploads.destroy` +- **컨트롤러**: `Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin\ImageUploadAdminController@destroy` +- **인증/권한**: `auth:sanctum`, `permission:admin,sirsoft-ckeditor5.uploads.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| id | path | integer | 예 | 숫자만 | 삭제할 업로드 기록 ID | + +**요청 예시** + +```http +DELETE /api/plugins/sirsoft-ckeditor5/admin/uploads/12 HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +``` + +**응답 필드** (`data` 내부) + +성공 시 `data` 는 `null` 이며, 결과는 `message` 로만 전달된다. + +**응답 예시** + +```json +{ + "success": true, + "message": "이미지를 삭제했습니다.", + "data": null +} +``` + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | `sirsoft-ckeditor5.uploads.delete` 권한이 없는 경우 (조회 권한만으로는 삭제 불가) | +| 404 | Not Found | 해당 ID 의 업로드 기록이 없는 경우 | +| 500 | Internal Server Error | 물리 파일이 존재하는데 삭제에 실패한 경우 — 기록은 보존되며 다음 시도에서 재시도할 수 있다 | + +<!-- @generated:end --> + +**설명** + +업로드 이미지 1건을 파일과 기록까지 삭제한다. + +- **삭제 순서는 파일 먼저, 기록 나중이다.** 반대로 하면 기록이 사라진 파일은 어떤 경로로도 다시 찾을 수 없어 영구 고아가 된다. +- 파일이 이미 없는 행은 고아 기록으로 보고 기록만 지운다. 그러지 않으면 매 회차 같은 행을 재시도하는 루프가 된다. +- 행마다 `storage_disk` 가 다를 수 있으므로 삭제는 그 행에 기록된 디스크를 향해 수행된다(디스크 전환 이후 혼재 대응). +- 이 경로는 정리 커맨드(`sirsoft-ckeditor5:prune-unused-images`)와 **같은 삭제 구현을 공유**한다 — 화면 삭제와 자동 정리의 동작이 갈리지 않게 하기 위함이다. + +--- + +### POST /api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete + +<!-- @generated:start:api.plugins.sirsoft-ckeditor5.admin.uploads.bulk-delete --> + +- **라우트명**: `api.plugins.sirsoft-ckeditor5.admin.uploads.bulk-delete` +- **컨트롤러**: `Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin\ImageUploadAdminController@bulkDestroy` +- **인증/권한**: `auth:sanctum`, `permission:admin,sirsoft-ckeditor5.uploads.delete` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| ids | body | array | 예 | 1 ~ 100개 | 삭제할 업로드 기록 ID 목록 | +| ids.* | body | integer | 예 | 존재하는 업로드 ID | 각 항목은 실재하는 기록이어야 한다 (아니면 422) | + +**요청 예시** + +```http +POST /api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "ids": [12, 13, 14] +} +``` + +**응답 필드** (`data` 내부) + +| 필드 | 타입 | 실측 예시값 | 용도/설명 | +| --- | --- | --- | --- | +| deleted | integer | `3` | 파일·기록이 함께 삭제된 건수 | +| failed | integer | `0` | 물리 파일 삭제 실패로 기록을 보존한 건수 (다음 시도에서 재시도 가능) | + +**응답 예시** + +```json +{ + "success": true, + "message": "선택한 이미지 3건을 삭제했습니다.", + "data": { + "deleted": 3, + "failed": 0 + } +} +``` + +**에러 응답** + +| 상태코드 | 의미 | 발생 조건 | +| --- | --- | --- | +| 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | +| 403 | Forbidden | `sirsoft-ckeditor5.uploads.delete` 권한이 없는 경우 | +| 422 | Unprocessable Entity | `ids` 누락/빈 배열/100개 초과, 또는 존재하지 않는 ID 포함 | +| 500 | Internal Server Error | 요청한 전부가 파일 삭제에 실패한 경우 (일부만 실패하면 200 + `failed` 로 보고) | + +<!-- @generated:end --> + +**설명** + +관리 화면에서 체크한 이미지를 한 번에 삭제한다. + +- 선택 범위는 **화면에 보이는 페이지**로 한정된다(화면 계약). 검색·필터·페이지 이동으로 목록에서 빠진 행의 선택이 남아 대상에 실리면, 운영자가 보지도 체크하지도 않은 파일이 지워지기 때문이다. +- 일부만 실패한 경우 요청 자체는 성공(200)으로 응답하고 `failed` 로 보고하며, 응답 메시지도 "N건 삭제, M건 실패" 형태(`messages.uploads.bulk_partially_deleted`)로 실패 사실을 함께 안내한다. 전부 실패한 경우에만 500 이다 — 부분 성공을 실패로 되돌릴 방법이 없기 때문이다. diff --git a/plugins/_bundled/sirsoft-ckeditor5/lang/en/messages.php b/plugins/_bundled/sirsoft-ckeditor5/lang/en/messages.php index 9dbaa1d4..b66e83e3 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-ckeditor5/lang/en/messages.php @@ -5,12 +5,25 @@ return [ 'not_found' => 'Image not found.', ], 'upload' => [ - 'required' => 'An image file is required.', + 'required' => 'An image file is required.', 'invalid_file' => 'Invalid file.', - 'not_image' => 'Only image files can be uploaded.', + 'not_image' => 'Only image files can be uploaded.', 'invalid_mime' => 'Only jpeg, jpg, png, gif, webp formats are supported.', - 'too_large' => 'The file size exceeds the allowed limit (:max MB).', - 'forbidden' => 'You do not have permission to upload images.', - 'failed' => 'Image upload failed.', + 'too_large' => 'The file size exceeds the allowed limit (:max MB).', + 'forbidden' => 'You do not have permission to upload images.', + 'failed' => 'Image upload failed.', + ], + 'uploads' => [ + 'not_found' => 'Upload not found.', + 'file_delete_failed' => 'Failed to delete the image file. Please try again shortly.', + 'ids_required' => 'Please select the images to delete.', + 'ids_invalid' => 'Some of the selected images no longer exist. Refresh the list and select again.', + 'deleted' => 'The image has been deleted.', + 'bulk_deleted' => 'Deleted :deleted selected image(s).', + 'bulk_partially_deleted' => 'Deleted :deleted of the selected images; :failed could not be deleted and remain in the list.', + ], + 'cleanup' => [ + 'retention_disabled' => 'The retention period is less than 1 day, so no cleanup was performed.', + 'sources_incomplete' => 'Unreferenced-image cleanup was skipped because an inactive module may hide content references. Activate or remove the module, then run again.', ], ]; diff --git a/plugins/_bundled/sirsoft-ckeditor5/lang/ko/messages.php b/plugins/_bundled/sirsoft-ckeditor5/lang/ko/messages.php index 750d897e..dba11b3e 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-ckeditor5/lang/ko/messages.php @@ -5,12 +5,25 @@ return [ 'not_found' => '이미지를 찾을 수 없습니다.', ], 'upload' => [ - 'required' => '업로드할 이미지 파일이 필요합니다.', + 'required' => '업로드할 이미지 파일이 필요합니다.', 'invalid_file' => '유효하지 않은 파일입니다.', - 'not_image' => '이미지 파일만 업로드할 수 있습니다.', + 'not_image' => '이미지 파일만 업로드할 수 있습니다.', 'invalid_mime' => 'jpeg, jpg, png, gif, webp 형식만 지원합니다.', - 'too_large' => '파일 크기가 허용 용량(:max MB)을 초과합니다.', - 'forbidden' => '이미지 업로드 권한이 없습니다.', - 'failed' => '이미지 업로드에 실패했습니다.', + 'too_large' => '파일 크기가 허용 용량(:max MB)을 초과합니다.', + 'forbidden' => '이미지 업로드 권한이 없습니다.', + 'failed' => '이미지 업로드에 실패했습니다.', + ], + 'uploads' => [ + 'not_found' => '업로드 이미지를 찾을 수 없습니다.', + 'file_delete_failed' => '이미지 파일 삭제에 실패했습니다. 잠시 후 다시 시도해 주세요.', + 'ids_required' => '삭제할 이미지를 선택해 주세요.', + 'ids_invalid' => '선택한 이미지 중 더 이상 존재하지 않는 항목이 있습니다. 목록을 새로고침한 뒤 다시 선택해 주세요.', + 'deleted' => '이미지를 삭제했습니다.', + 'bulk_deleted' => '선택한 이미지 :deleted건을 삭제했습니다.', + 'bulk_partially_deleted' => '선택한 이미지 중 :deleted건을 삭제했고 :failed건은 삭제하지 못했습니다. 실패한 항목은 목록에 남아 있습니다.', + ], + 'cleanup' => [ + 'retention_disabled' => '보존기간이 1일 미만으로 설정되어 정리를 수행하지 않았습니다.', + 'sources_incomplete' => '비활성 상태의 모듈이 있어 참조 판정이 불완전할 수 있으므로 미참조 이미지 정리를 건너뛰었습니다. 모듈을 활성화하거나 삭제한 뒤 다시 실행해 주세요.', ], ]; diff --git a/plugins/_bundled/sirsoft-ckeditor5/package-lock.json b/plugins/_bundled/sirsoft-ckeditor5/package-lock.json index 63e481a5..019f5aa6 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/package-lock.json +++ b/plugins/_bundled/sirsoft-ckeditor5/package-lock.json @@ -1,15 +1,199 @@ { "name": "@g7/sirsoft-ckeditor5", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-ckeditor5", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { + "jsdom": "^27.4.0", "typescript": "^5.3.3", - "vite": "^5.4.14" + "vite": "^5.4.14", + "vitest": "^2.1.8" + } + }, + "node_modules/@acemir/cssom": { + "version": "0.9.31", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", + "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.2.tgz", + "integrity": "sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.0.0", + "@csstools/css-color-parser": "^4.0.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.5" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", + "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -403,6 +587,31 @@ "node": ">=12" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -760,6 +969,295 @@ "dev": true, "license": "MIT" }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/cssstyle": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.7.tgz", + "integrity": "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.1.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.21", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.1.tgz", + "integrity": "sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^15.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -799,6 +1297,26 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -814,6 +1332,135 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "27.4.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.4.0.tgz", + "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@acemir/cssom": "^0.9.28", + "@asamuzakjp/dom-selector": "^6.7.6", + "@exodus/bytes": "^1.6.0", + "cssstyle": "^5.3.4", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -833,6 +1480,36 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -869,6 +1546,26 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", @@ -914,6 +1611,26 @@ "fsevents": "~2.3.2" } }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -924,6 +1641,117 @@ "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -997,6 +1825,198 @@ "optional": true } } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" } } } diff --git a/plugins/_bundled/sirsoft-ckeditor5/package.json b/plugins/_bundled/sirsoft-ckeditor5/package.json index 831ae22d..1d32594a 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/package.json +++ b/plugins/_bundled/sirsoft-ckeditor5/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-ckeditor5", - "version": "1.0.1", + "version": "1.0.2", "description": "그누보드7 CKEditor 5 WYSIWYG 에디터 플러그인 프론트엔드 에셋", "private": true, "type": "module", @@ -9,7 +9,9 @@ "build": "vite build", "preview": "vite preview", "test": "vitest", - "test:run": "vitest run" + "test:run": "vitest run", + "test:e2e": "playwright test --config=tests/Playwright/playwright.config.ts", + "test:e2e:ui": "playwright test --config=tests/Playwright/playwright.config.ts --ui" }, "devDependencies": { "jsdom": "^27.4.0", diff --git a/plugins/_bundled/sirsoft-ckeditor5/plugin.json b/plugins/_bundled/sirsoft-ckeditor5/plugin.json index a54c17e1..a2c61edf 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/plugin.json +++ b/plugins/_bundled/sirsoft-ckeditor5/plugin.json @@ -5,19 +5,22 @@ "ko": "CKEditor 5 WYSIWYG 에디터", "en": "CKEditor 5 WYSIWYG Editor" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "description": { "ko": "CKEditor 5를 이용한 WYSIWYG 에디터 플러그인입니다. 플러그인 설치만으로 기존 HtmlEditor가 교체됩니다.", "en": "WYSIWYG editor plugin using CKEditor 5. Installing this plugin replaces the default HtmlEditor." }, - "g7_version": ">=7.0.6", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} }, "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-ckeditor5/blob/main/CHANGELOG.md", + "trusted_script_hosts": [ + "cdn.ckeditor.com" + ], "assets": { "js": { "entry": "resources/js/index.ts", diff --git a/plugins/_bundled/sirsoft-ckeditor5/plugin.php b/plugins/_bundled/sirsoft-ckeditor5/plugin.php index 71c4f2ae..c48fdc79 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/plugin.php +++ b/plugins/_bundled/sirsoft-ckeditor5/plugin.php @@ -2,7 +2,9 @@ namespace Plugins\Sirsoft\Ckeditor5; +use App\Enums\ExtensionOwnerType; use App\Extension\AbstractPlugin; +use App\Extension\Helpers\ExtensionMenuSyncHelper; /** * CKEditor 5 WYSIWYG 에디터 플러그인 @@ -73,9 +75,291 @@ class Plugin extends AbstractPlugin ], 'required' => false, ], + // 선택지가 코어 카탈로그 + 플러그인 훅 등록 디스크로 동적이라 enum 불가 — string. + // 존재하지 않는 디스크 값은 resolvePublicAssetDisk() 가 스트리밍으로 안전 폴백한다. + 'public_asset_disk' => [ + 'type' => 'string', + 'max' => 100, + 'default' => '', + 'label' => [ + 'ko' => '공개 자산 디스크', + 'en' => 'Public Asset Disk', + ], + 'hint' => [ + 'ko' => '이 플러그인만 다른 디스크를 쓰려면 선택합니다. 비우면 코어 공개 자산 디스크 설정을 따릅니다.', + 'en' => 'Select to use a different disk for this plugin only. Leave empty to follow the core public asset disk setting.', + ], + 'required' => false, + ], + 'unusedImageCleanup' => [ + 'type' => 'boolean', + 'default' => false, + 'label' => [ + 'ko' => '미사용 이미지 자동 정리', + 'en' => 'Auto Cleanup Unused Images', + ], + 'hint' => [ + 'ko' => '기본 꺼짐 — 운영자가 직접 켜야 동작합니다. 켜면 보존기간이 지난 미사용 이미지를 매일 삭제합니다.', + 'en' => 'Off by default — the operator must turn it on. When on, unused images past the retention period are deleted daily.', + ], + 'required' => false, + ], + 'unusedImageRetentionDays' => [ + 'type' => 'integer', + 'min' => 1, + 'max' => 3650, + 'default' => 30, + 'label' => [ + 'ko' => '미사용 이미지 보존기간 (일)', + 'en' => 'Unused Image Retention (days)', + ], + 'hint' => [ + 'ko' => '업로드 후 이 기간이 지난 미사용 이미지만 정리 대상이 됩니다. (1 ~ 3650 일)', + 'en' => 'Only unused images older than this period are eligible for cleanup. (1 ~ 3650 days)', + ], + 'required' => false, + ], ]; } + /** + * 플러그인이 제공하는 훅 정보 반환 + * + * @return array 훅 정의 배열 (action 2 + filter 2) + */ + public function getHooks(): array + { + return [ + [ + 'name' => 'sirsoft-ckeditor5.image.before_upload', + 'type' => 'action', + 'description' => [ + 'ko' => '에디터 이미지 업로드 직전 발화 (본인인증·쿼터 등 확장 지점)', + 'en' => 'Fired right before an editor image upload (identity verification, quota, etc.)', + ], + 'parameters' => [ + 'file' => 'UploadedFile - 업로드된 파일', + 'uploadedBy' => 'int|null - 업로드 사용자 ID', + ], + ], + [ + 'name' => 'sirsoft-ckeditor5.image.after_upload', + 'type' => 'action', + 'description' => [ + 'ko' => '에디터 이미지 업로드 기록 생성 후 발화', + 'en' => 'Fired after the editor image upload record is created', + ], + 'parameters' => [ + 'record' => 'Model - Ckeditor5ImageUpload', + ], + ], + [ + 'name' => 'sirsoft-ckeditor5.image.filter_upload_file', + 'type' => 'filter', + 'description' => [ + 'ko' => '업로드 파일 변형 지점 (압축·리사이즈 등)', + 'en' => 'Transform the uploaded file (compression, resizing, etc.)', + ], + 'parameters' => [ + 'file' => 'UploadedFile - 업로드된 파일', + ], + ], + [ + 'name' => 'sirsoft-ckeditor5.image.filter_reference_sources', + 'type' => 'filter', + 'description' => [ + 'ko' => '에디터 이미지 참조 스캔 대상 테이블/컬럼 목록에 확장 콘텐츠를 추가', + 'en' => 'Append extension content tables/columns to the editor image reference scan sources', + ], + 'parameters' => [ + 'sources' => 'array - list<array{table: string, columns: list<string>}>', + ], + ], + ]; + } + + /** + * 플러그인 권한 목록 반환 (계층 구조) + * + * 업로드 이미지 관리 화면(조회/삭제)의 접근을 분리한다 — 조회는 감사·현황 파악, + * 삭제는 파일을 실제로 파기하므로 별도 권한으로 둔다. + * + * @return array 권한 정의 배열 + */ + public function getPermissions(): array + { + return [ + 'name' => [ + 'ko' => 'CKEditor 5 WYSIWYG 에디터', + 'en' => 'CKEditor 5 WYSIWYG Editor', + ], + 'description' => [ + 'ko' => 'CKEditor5 플러그인이 제공하는 권한', + 'en' => 'Permissions provided by the CKEditor5 plugin', + ], + 'categories' => [ + [ + 'identifier' => 'uploads', + 'name' => ['ko' => '에디터 업로드 이미지', 'en' => 'Editor Uploads'], + 'description' => [ + 'ko' => '에디터로 업로드된 이미지의 조회·삭제 권한', + 'en' => 'View and delete permissions for images uploaded via the editor', + ], + 'permissions' => [ + [ + 'action' => 'read', + 'name' => ['ko' => '업로드 이미지 조회', 'en' => 'View Uploads'], + 'description' => [ + 'ko' => '에디터 업로드 이미지 목록·참조 상태 조회', + 'en' => 'View the editor upload list and reference status', + ], + 'type' => 'admin', + 'roles' => ['admin'], + ], + [ + 'action' => 'delete', + 'name' => ['ko' => '업로드 이미지 삭제', 'en' => 'Delete Uploads'], + 'description' => [ + 'ko' => '에디터 업로드 이미지의 파일·기록 삭제', + 'en' => 'Delete editor upload files and records', + ], + 'type' => 'admin', + 'roles' => ['admin'], + ], + ], + ], + ], + ]; + } + + /** + * 관리자 메뉴 정의 + * + * 코어 PluginManager 가 설치·업데이트 공통 경로에서 이 선언을 동기화한다. + * activate() 의 직접 동기화는 그 경로를 타지 않는 활성화 단독 호출을 위한 것이며, + * 동기화는 upsert 라 두 경로가 겹쳐도 메뉴가 중복 생성되지 않는다. + * + * @return array<int, array<string, mixed>> + */ + public function getAdminMenus(): array + { + return [ + [ + 'name' => ['ko' => '에디터 업로드 이미지', 'en' => 'Editor Uploads'], + 'slug' => 'sirsoft-ckeditor5-uploads', + 'url' => '/admin/plugins/sirsoft-ckeditor5/uploads', + 'icon' => 'fas fa-images', + 'order' => 50, + ], + ]; + } + + /** + * 플러그인 활성화 — 관리자 메뉴 자동 등록. + * + * @return bool 활성화 성공 여부 + */ + public function activate(): bool + { + $helper = app(ExtensionMenuSyncHelper::class); + + foreach ($this->getAdminMenus() as $menuData) { + $helper->syncMenuRecursive( + $menuData, + ExtensionOwnerType::Plugin, + $this->getIdentifier(), + ); + } + + return true; + } + + /** + * 플러그인 비활성화 — 관리자 메뉴 일괄 제거. + * + * @return bool 비활성화 성공 여부 + */ + public function deactivate(): bool + { + app(ExtensionMenuSyncHelper::class)->cleanupStaleMenus( + ExtensionOwnerType::Plugin, + $this->getIdentifier(), + currentSlugs: [], + ); + + return true; + } + + /** + * 플러그인 제거 — 메뉴 잔존 안전망 (정상 흐름은 deactivate 가 먼저 처리). + * + * @return bool 제거 성공 여부 + */ + public function uninstall(): bool + { + $this->deactivate(); + + return true; + } + + /** + * 플러그인 스케줄 목록 반환 + * + * 미참조 이미지 정리는 사용자 파일을 지우므로 기본 꺼짐(옵트인)이다. + * enabled_config 게이트는 설정 조회 실패 시 true 로 폴백하므로, 커맨드가 + * `--scheduled` 에서 같은 설정을 false 폴백으로 재확인해 자동 삭제를 차단한다. + * + * @return array<int, array<string, string>> 스케줄 정의 목록 + */ + public function getSchedules(): array + { + return [ + [ + 'command' => 'sirsoft-ckeditor5:prune-unused-images --scheduled', + 'schedule' => 'daily', + 'description' => '미참조 에디터 업로드 이미지 정리', + 'enabled_config' => 'sirsoft-ckeditor5.unusedImageCleanup', + ], + ]; + } + + /** + * 완전 공개 자산 스토리지 카테고리 목록 + * + * 이 카테고리들만 공개 자산 디스크 설정을 따른다. 권한 검사가 걸린 자산은 + * 직접 URL 이 권한을 우회하므로 포함하지 않는다. 에디터가 이미지 외의 + * 공개 자산 업로드를 지원하게 되면 여기에만 카테고리를 추가하면 된다. + * + * @var list<string> + */ + private const PUBLIC_ASSET_CATEGORIES = ['images']; + + /** + * 카테고리별 스토리지 디스크 이름 반환 + * + * 완전 공개 자산 카테고리만 공개 자산 디스크(플러그인 설정 public_asset_disk > + * 코어 전역 core.storage.public_asset_disk)를 따르고, 설정 등 나머지 카테고리는 + * 기본 디스크를 유지합니다. 미설정/고아 디스크는 기본 디스크로 폴백해 기존 + * 스트리밍 동작을 보존합니다. + * + * 플러그인 설정 조회는 공개 자산 카테고리에서만 수행합니다 — 'settings' 카테고리에서 + * 조회하면 설정 로드와 재귀 고리가 생깁니다 (AbstractPlugin 주석 참조). + * + * @param string $category 카테고리 + * @return string 디스크 이름 + */ + public function getStorageDiskFor(string $category): string + { + if (! in_array($category, self::PUBLIC_ASSET_CATEGORIES, true)) { + return $this->getStorageDisk(); + } + + $override = plugin_setting('sirsoft-ckeditor5', 'public_asset_disk', ''); + + return $this->resolvePublicAssetDisk(is_string($override) ? $override : '') + ?? $this->getStorageDisk(); + } + /** * 플러그인이 관리하는 동적 테이블 목록 반환 * diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/extensions/html-editor.json b/plugins/_bundled/sirsoft-ckeditor5/resources/extensions/html-editor.json index dfe02dc9..0cb61dda 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/extensions/html-editor.json +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/extensions/html-editor.json @@ -27,7 +27,6 @@ "placeholder": "{{extensionPointProps.placeholder ?? ''}}", "readOnly": "{{_computed.isReadOnly ?? extensionPointProps.disabled ?? extensionPointProps.readOnly ?? false}}", "imageUpload": "{{_global.plugins['sirsoft-ckeditor5']?.imageUpload ?? false}}", - "uploadPermission": "{{extensionPointProps.uploadPermission ?? ''}}", "height": "{{_global.plugins['sirsoft-ckeditor5']?.editorHeight ?? 400}}", "toolbar": "{{_global.plugins['sirsoft-ckeditor5']?.toolbar ?? 'standard'}}" } diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/ckeditor5Uploads.test.tsx b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/ckeditor5Uploads.test.tsx new file mode 100644 index 00000000..393bee93 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/ckeditor5Uploads.test.tsx @@ -0,0 +1,358 @@ +/** + * CKEditor5 업로드 관리 화면 — 계약 + 렌더링 테스트 (공개 #115) + * + * @description + * 이 화면은 사용자 파일을 실제로 파기하므로, 화면이 지키기로 한 계약을 코드에 고정한다. + * + * - 일괄 삭제가 달린 목록이므로 선택 범위는 반드시 `page` (화면 밖 선택이 대상에 실리면 + * 운영자가 보지도 체크하지도 않은 파일이 지워진다) + * - 목록 클러스터 안의 이동은 목록 상태를 승계(mergeQuery)하고, 의도적 리셋만 예외 + * - 정렬 선택지는 서버 게이트(IndexUploadsRequest)의 부분집합이어야 한다 + * + * @effects admin_ui_selection_scope_page, admin_ui_merge_query, admin_ui_sort_subset, + * admin_ui_reference_badge + */ + +import React from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +import uploadsLayout from '../../../layouts/admin/ckeditor5_uploads.json'; +import routesJson from '../../../routes.json'; + +// ─── 헬퍼 ─── + +/** + * 플러그인 루트(plugin.json 기준)를 위로 훑어 찾는다. + * + * @returns 플러그인 루트 절대경로 + */ +function pluginRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'plugin.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('plugin.json 을 가진 플러그인 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다 (slots/modals 포함). + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + const groups: any[] = [ + node.children ?? [], + node.modals ?? [], + ...(node.slots ? Object.values(node.slots) : []), + ]; + + for (const group of groups) { + for (const child of group as any[]) { + const found = findNode(child, predicate); + if (found) return found; + } + } + + return null; +} + +/** + * 레이아웃 트리의 모든 navigate 액션을 수집한다. + * + * @param node 탐색 시작 노드 + * @param collected 누적 배열 + * @returns navigate 액션 목록 + */ +function collectNavigateActions(node: any, collected: any[] = []): any[] { + if (!node || typeof node !== 'object') return collected; + + if (Array.isArray(node)) { + node.forEach((child) => collectNavigateActions(child, collected)); + return collected; + } + + if (node.handler === 'navigate') { + collected.push(node); + } + + Object.values(node).forEach((value) => { + if (value && typeof value === 'object') { + collectNavigateActions(value, collected); + } + }); + + return collected; +} + +const dataGrid = findNode(uploadsLayout, (n) => n.id === 'ckeditor5_uploads_datagrid'); + +describe('업로드 관리 화면 — 목록 계약', () => { + it('일괄 삭제 목록이므로 선택 범위가 page 로 고정돼 있다', () => { + expect(dataGrid).not.toBeNull(); + expect(dataGrid.props.selectionScope).toBe('page'); + }); + + it('선택 상태 변경이 전용 전역 키에 저장되고 일괄 삭제가 같은 키를 읽는다', () => { + const selectionAction = (dataGrid.actions ?? []).find( + (a: any) => a.event === 'onSelectionChange', + ); + + expect(selectionAction?.params?.selectedIds_ckeditor5_uploads).toBe('{{$args[0]}}'); + + const bulkModal = findNode(uploadsLayout, (n) => n.id === 'bulk_delete_confirm_modal'); + expect(JSON.stringify(bulkModal)).toContain('_global.selectedIds_ckeditor5_uploads'); + }); + + it('삭제 API 가 플러그인 네임스페이스 경로를 호출한다', () => { + const payload = JSON.stringify(uploadsLayout); + + expect(payload).toContain('/api/plugins/sirsoft-ckeditor5/admin/uploads/{{_global.modal_data.id}}'); + expect(payload).toContain('/api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete'); + }); + + it('참조 상태 배지가 참조/미참조 두 갈래로 렌더된다', () => { + const column = dataGrid.props.columns.find((c: any) => c.field === 'referenced'); + + expect(column).toBeDefined(); + const conditions = column.cellChildren.map((c: any) => c.if); + expect(conditions).toContain('{{row.referenced === true}}'); + expect(conditions).toContain('{{row.referenced !== true}}'); + }); +}); + +describe('업로드 관리 화면 — 목록 컨텍스트 왕복', () => { + it('클러스터 안 이동은 mergeQuery 로 목록 상태를 승계한다', () => { + const navigates = collectNavigateActions(uploadsLayout).filter( + (a) => (a.params?.path ?? '').includes('/admin/plugins/sirsoft-ckeditor5/uploads'), + ); + + expect(navigates.length).toBeGreaterThan(0); + + navigates.forEach((action) => { + const isIntentionalReset = JSON.stringify(action).includes('audit:allow') + || Object.keys(action.params?.query ?? {}).length === 0; + + if (!isIntentionalReset) { + expect(action.params.mergeQuery).toBe(true); + } + }); + }); + + it('검색은 페이지를 되돌리면서 바뀐 값만 직접 넘긴다', () => { + const search = (uploadsLayout as any).named_actions.searchUploads; + + expect(search.params.mergeQuery).toBe(true); + expect(search.params.query.page).toBe(''); + expect(search.params.query.search).toContain('_local.filter.search'); + }); + + it('필터 초기화는 병합하지 않으며 그 의도를 코드에 남긴다', () => { + const clear = (uploadsLayout as any).named_actions.clearFilters; + const navigate = clear.params.actions.find((a: any) => a.handler === 'navigate'); + + expect(navigate.params.mergeQuery).toBeUndefined(); + expect(navigate.comment).toContain('audit:allow layout-list-context-navigate-merge-query'); + }); +}); + +describe('업로드 관리 화면 — 서버 계약 정합', () => { + it('라우트가 플러그인 네임스페이스 안에 선언되고 조회 권한을 요구한다', () => { + const route = (routesJson as any).routes.find( + (r: any) => r.layout === 'ckeditor5_uploads', + ); + + expect(route.path).toContain('/admin/plugins/sirsoft-ckeditor5/uploads'); + expect(route.meta.permission).toBe('sirsoft-ckeditor5.uploads.read'); + expect(route.auth_required).toBe(true); + }); + + it('화면 정렬 선택지가 서버 게이트의 부분집합이다', () => { + const gate = fs.readFileSync( + path.join(pluginRoot(), 'src/Http/Requests/IndexUploadsRequest.php'), + 'utf-8', + ); + const allowed = /public const SORTABLE = \[([^\]]+)\]/ + .exec(gate)![1] + .split(',') + .map((s) => s.trim().replace(/'/g, '')) + .filter(Boolean); + + const screenSortable = dataGrid.props.columns + .filter((c: any) => c.sortable === true) + .map((c: any) => c.field); + + expect(screenSortable.length).toBeGreaterThan(0); + screenSortable.forEach((field: string) => expect(allowed).toContain(field)); + }); + + it('레이아웃이 요구하는 권한이 플러그인 권한 선언에 존재한다', () => { + const pluginSource = fs.readFileSync(path.join(pluginRoot(), 'plugin.php'), 'utf-8'); + + expect(pluginSource).toContain("'identifier' => 'uploads'"); + expect(pluginSource).toContain("'action' => 'read'"); + expect(pluginSource).toContain("'action' => 'delete'"); + }); +}); + +// ─── 렌더링 ─── + +const TestDiv: React.FC<any> = ({ className, children }) => <div className={className}>{children}</div>; +const TestSpan: React.FC<any> = ({ className, children, text }) => ( + <span className={className}>{children || text}</span> +); +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; + +describe('업로드 관리 화면 — 안내 배너 렌더링', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest> | null = null; + + beforeEach(() => { + registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + P: { component: TestSpan, metadata: { name: 'P', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + }); + + afterEach(() => { + testUtils?.cleanup(); + testUtils = null; + (registry as any).registry = {}; + }); + + /** + * 스캔 상한 안내 배너를 지정 상태로 렌더한다. + * + * @param scanLimited 상한 도달 여부 + * @returns createLayoutTest 유틸 + */ + function makeUtils() { + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_ckeditor5_uploads_notice', + components: [findNode(uploadsLayout, (n) => n.id === 'scan_limited_notice')], + // 실제 화면의 데이터소스 선언을 그대로 쓴다 — 배너 조건이 이 응답을 읽는다. + data_sources: (uploadsLayout as any).data_sources, + } as any, + { + auth: { + isAuthenticated: true, + user: { id: 1, name: 'Admin', role: 'super_admin' }, + authType: 'admin', + }, + translations: { + 'sirsoft-ckeditor5': { + admin: { + uploads: { + scan_limited_notice: '참조 상태 필터는 최근 :count건을 기준으로 판정합니다.', + }, + }, + }, + }, + locale: 'ko', + }, + ); + } + + it('스캔 상한에 걸리면 안내 배너가 노출된다', async () => { + testUtils = makeUtils(); + testUtils.mockApi('ckeditor5Uploads', { + // 화면은 `ckeditor5Uploads?.data?.meta` 를 읽으므로 응답 봉투의 data 계층을 유지한다. + response: { + data: { + data: [], + pagination: { current_page: 1, last_page: 1, per_page: 20, total: 0 }, + meta: { scan_limited: true, scan_window: 500 }, + }, + }, + }); + + const { container } = await testUtils.render(); + + expect(container.textContent ?? '').toContain('참조 상태 필터는'); + }); + + it('배너 문구가 스캔 상한 값을 파라미터로 바인딩한다', () => { + const notice = findNode(uploadsLayout, (n) => n.id === 'scan_limited_notice'); + const text = findNode(notice, (n) => typeof n.text === 'string' && n.text.includes('scan_limited_notice')).text; + + expect(text).toContain('|count='); + expect(text).toContain('meta?.scan_window'); + }); + + it('상한에 걸리지 않으면 배너가 렌더되지 않는다', async () => { + testUtils = makeUtils(); + testUtils.mockApi('ckeditor5Uploads', { + response: { + data: { + data: [], + pagination: { current_page: 1, last_page: 1, per_page: 20, total: 0 }, + meta: { scan_limited: false, scan_window: 500 }, + }, + }, + }); + + const { container } = await testUtils.render(); + + expect(container.textContent ?? '').not.toContain('참조 상태 필터는'); + }); +}); + +describe('업로드 관리 화면 — 파라미터 치환 문구', () => { + /** + * 프론트 다국어 파라미터는 `{{name}}` 규약이다. Laravel 스타일 `:name` 을 쓰면 치환되지 + * 않아 운영자에게 원시 placeholder 가 그대로 보인다 — 파괴적 삭제 확인 모달에서도 마찬가지다 + * (브라우저 실측 — "선택한 :count건의 파일과 기록이 함께 삭제됩니다"). + */ + it.each(['ko', 'en'])('%s 문구가 :param 대신 {{param}} 을 쓴다', (locale) => { + const raw = fs.readFileSync(path.join(pluginRoot(), `resources/lang/${locale}.json`), 'utf-8'); + const messages = JSON.parse(raw); + const offenders: string[] = []; + + const walk = (node: unknown, keyPath: string): void => { + if (typeof node === 'string') { + // `:count` 처럼 낱말이 이어지는 형태만 파라미터로 본다 (URL·명령어의 콜론은 제외). + if (/(^|\s):[a-z_]+/.test(node)) offenders.push(`${keyPath} = ${node}`); + return; + } + if (node && typeof node === 'object') { + for (const [key, value] of Object.entries(node as Record<string, unknown>)) { + walk(value, keyPath ? `${keyPath}.${key}` : key); + } + } + }; + + walk(messages.admin?.uploads ?? {}, 'admin.uploads'); + walk(messages.settings?.cleanup ?? {}, 'settings.cleanup'); + + expect(offenders).toEqual([]); + }); + + it('레이아웃이 넘기는 파라미터 이름이 문구의 placeholder 와 일치한다', () => { + const messages = JSON.parse( + fs.readFileSync(path.join(pluginRoot(), 'resources/lang/ko.json'), 'utf-8'), + ); + + expect(messages.admin.uploads.bulk_delete.selected).toContain('{{count}}'); + expect(messages.admin.uploads.bulk_delete.confirm_message).toContain('{{count}}'); + expect(messages.admin.uploads.scan_limited_notice).toContain('{{count}}'); + }); +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsCleanup.test.tsx b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsCleanup.test.tsx new file mode 100644 index 00000000..dd3a0e1e --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsCleanup.test.tsx @@ -0,0 +1,249 @@ +/** + * CKEditor5 설정 화면 — 미사용 이미지 정리 섹션 테스트 (공개 #115) + * + * @description + * 자동 정리는 사용자 파일을 지우므로 기본 꺼짐이다. 화면이 그 계약을 지키는지 + * (저장 키 · 기본값 · 스키마 세 지점 일치)와, 라벨/힌트가 실제로 렌더되는지를 고정한다. + * + * @effects settings_cleanup_toggle_default_off, settings_cleanup_keys_declared + */ + +import React from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +import pluginSettings from '../../../layouts/admin/plugin_settings.json'; + +/** + * 플러그인 루트(plugin.json 기준)를 위로 훑어 찾는다. + * + * @returns 플러그인 루트 절대경로 + */ +function pluginRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'plugin.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('plugin.json 을 가진 플러그인 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다 (slots 포함). + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + const groups = [node.children ?? [], ...(node.slots ? Object.values(node.slots) : [])]; + for (const group of groups) { + for (const child of group as any[]) { + const found = findNode(child, predicate); + if (found) return found; + } + } + + return null; +} + +/** + * 이 화면을 렌더하는 admin 템플릿의 매니페스트가 선언한 컴포넌트 종류를 읽는다. + * + * 레이아웃 노드의 `type` 은 렌더러가 DOM 안전 필터링을 고르는 기준이라 + * 매니페스트 선언과 어긋나면 `editorAttrs` 가 제거된다(DynamicRenderer). + * + * @param name 컴포넌트 이름 + * @returns 매니페스트가 선언한 type + */ +function manifestType(name: string): string { + // 플러그인 루트(plugins/_bundled/sirsoft-ckeditor5) 에서 저장소 루트로 두 단계 올라간다. + const repoRoot = path.dirname(path.dirname(path.dirname(pluginRoot()))); + const manifest = JSON.parse( + fs.readFileSync( + path.join(repoRoot, 'templates/_bundled/sirsoft-admin_basic/components.json'), + 'utf-8', + ), + ); + + for (const [kind, entries] of Object.entries<any>(manifest.components ?? {})) { + if ((entries as any[]).some((entry) => entry.name === name)) { + return kind; + } + } + + throw new Error(`매니페스트에 ${name} 선언이 없습니다.`); +} + +const cleanupCard = findNode(pluginSettings, (n) => n.id === 'section_cleanup_card'); + +describe('미사용 이미지 정리 섹션 — 계약', () => { + it('설정 섹션이 존재한다', () => { + expect(cleanupCard).not.toBeNull(); + }); + + it('설정 화면의 모든 Toggle 노드 type 이 매니페스트 선언과 같다', () => { + const collected: any[] = []; + const collect = (node: any): void => { + if (!node || typeof node !== 'object') return; + if (node.name === 'Toggle') collected.push(node); + + const groups = [node.children ?? [], ...(node.slots ? Object.values(node.slots) : [])]; + for (const group of groups) { + for (const child of group as any[]) collect(child); + } + }; + collect(pluginSettings); + + // 특정 노드만 짚으면 다음에 추가되는 토글이 같은 실수를 반복해도 잡히지 않는다. + expect(collected.length).toBeGreaterThan(0); + for (const toggle of collected) { + expect(toggle.type).toBe(manifestType('Toggle')); + } + }); + + it('토글과 보존기간 입력이 저장 키를 그대로 바인딩한다', () => { + const toggle = findNode(cleanupCard, (n) => n.name === 'Toggle'); + const input = findNode(cleanupCard, (n) => n.name === 'Input'); + + expect(toggle.props.name).toBe('unusedImageCleanup'); + expect(input.props.name).toBe('unusedImageRetentionDays'); + expect(input.props.min).toBe(1); + }); + + it('기본값이 꺼짐이고 보존기간 기본이 30일이다 (defaults.json)', () => { + const defaults = JSON.parse( + fs.readFileSync(path.join(pluginRoot(), 'config/settings/defaults.json'), 'utf-8'), + ); + + expect(defaults.defaults.unusedImageCleanup).toBe(false); + expect(defaults.defaults.unusedImageRetentionDays).toBe(30); + }); + + it('두 키가 프론트엔드에 노출되지 않는다 (운영자 전용 설정)', () => { + const defaults = JSON.parse( + fs.readFileSync(path.join(pluginRoot(), 'config/settings/defaults.json'), 'utf-8'), + ); + + expect(defaults.frontend_schema.unusedImageCleanup.expose).toBe(false); + expect(defaults.frontend_schema.unusedImageRetentionDays.expose).toBe(false); + }); + + it('설정 스키마가 같은 키를 선언한다 (저장 검증 whitelist)', () => { + const pluginSource = fs.readFileSync(path.join(pluginRoot(), 'plugin.php'), 'utf-8'); + + expect(pluginSource).toContain("'unusedImageCleanup' => ["); + expect(pluginSource).toContain("'unusedImageRetentionDays' => ["); + expect(pluginSource).toContain("'default' => false"); + }); + + it('스케줄 게이트가 같은 설정 키를 가리킨다', () => { + const pluginSource = fs.readFileSync(path.join(pluginRoot(), 'plugin.php'), 'utf-8'); + + expect(pluginSource).toContain("'enabled_config' => 'sirsoft-ckeditor5.unusedImageCleanup'"); + expect(pluginSource).toContain('sirsoft-ckeditor5:prune-unused-images --scheduled'); + }); + + it('업로드 관리 화면으로 가는 링크가 있다', () => { + const button = findNode(cleanupCard, (n) => n.id === 'open_uploads_button'); + + expect(button.actions[0].params.path).toBe('/admin/plugins/sirsoft-ckeditor5/uploads'); + }); +}); + +// ─── 렌더링 ─── + +const TestDiv: React.FC<any> = ({ className, children }) => <div className={className}>{children}</div>; +const TestLabel: React.FC<any> = ({ className, children, text }) => ( + <label className={className}>{children || text}</label> +); +const TestP: React.FC<any> = ({ className, children, text }) => <p className={className}>{children || text}</p>; +const TestH3: React.FC<any> = ({ className, children, text }) => <h3 className={className}>{children || text}</h3>; +const TestToggle: React.FC<any> = ({ name }) => <input type="checkbox" data-name={name} readOnly />; +const TestInput: React.FC<any> = ({ name, type }) => <input type={type} data-name={name} readOnly />; +const TestButton: React.FC<any> = ({ className, children }) => <button type="button" className={className}>{children}</button>; +const TestSpan: React.FC<any> = ({ className, children, text }) => <span className={className}>{children || text}</span>; +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; + +describe('미사용 이미지 정리 섹션 — 렌더링', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest> | null = null; + + beforeEach(() => { + registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + H3: { component: TestH3, metadata: { name: 'H3', type: 'basic' } }, + Toggle: { component: TestToggle, metadata: { name: 'Toggle', type: 'composite' } }, + Input: { component: TestInput, metadata: { name: 'Input', type: 'basic' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + }); + + afterEach(() => { + testUtils?.cleanup(); + testUtils = null; + (registry as any).registry = {}; + }); + + it('라벨과 힌트가 다국어 해석되어 렌더된다', async () => { + testUtils = createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_ckeditor5_cleanup_section', + components: [cleanupCard], + } as any, + { + auth: { + isAuthenticated: true, + user: { id: 1, name: 'Admin', role: 'super_admin' }, + authType: 'admin', + }, + translations: { + 'sirsoft-ckeditor5': { + settings: { + section_cleanup: '미사용 이미지 정리', + cleanup: { + toggle_label: '미사용 이미지 자동 정리', + toggle_hint: '기본 꺼짐 — 운영자가 직접 켜야 동작합니다.', + retention_label: '보존기간 (일)', + retention_hint: '보존기간이 지난 미사용 이미지만 정리 대상입니다.', + manual_hint: 'php artisan sirsoft-ckeditor5:prune-unused-images --dry-run', + open_uploads: '업로드 이미지 관리 열기', + }, + }, + }, + }, + locale: 'ko', + initialState: { _local: { form: { unusedImageCleanup: false, unusedImageRetentionDays: 30 } } }, + }, + ); + + const { container } = await testUtils.render(); + const text = container.textContent ?? ''; + + expect(text).toContain('미사용 이미지 정리'); + expect(text).toContain('기본 꺼짐'); + expect(text).toContain('보존기간 (일)'); + expect(text).toContain('업로드 이미지 관리 열기'); + + expect(container.querySelector('input[data-name="unusedImageCleanup"]')).not.toBeNull(); + expect(container.querySelector('input[data-name="unusedImageRetentionDays"]')).not.toBeNull(); + expect(() => testUtils!.assertNoValidationErrors()).not.toThrow(); + }); +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsPublicAssetDisk.test.tsx b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsPublicAssetDisk.test.tsx new file mode 100644 index 00000000..495eac63 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsPublicAssetDisk.test.tsx @@ -0,0 +1,248 @@ +/** + * CKEditor5 설정 화면 — 공개 자산 디스크 필드 테스트 (공개#100) + * + * @description + * 플러그인 오버라이드 필드는 저장 키(public_asset_disk)와 **플러그인 설정 응답에 + * 서버 부착되는** 카탈로그(available_public_asset_disks)를 바인딩해야 한다. + * 별도 코어 설정 API(/api/admin/settings, core.settings.read)를 교차 조회하면 + * 화면 권한(core.plugins.read)과 표면이 갈려 커스텀 역할에서 카탈로그가 조용히 + * 빈다 — 단일 표면 계약을 고정한다. 설정 스키마(getSettingsSchema)에 같은 키가 + * 없으면 저장 검증이 규칙을 만들지 않아 값이 조용히 버려진다 — 세 지점을 대조한다. + * + * 계약 단언에 더해, 실제 필드 서브트리를 createLayoutTest 로 렌더링해 + * 옵션 계산식($t 선두 + $localized 매핑)까지 검증한다. + * + * @effects settings_catalog_includes_plugin_registered_disks + */ + +import React from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +import pluginSettings from '../../../layouts/admin/plugin_settings.json'; + +// ─── 테스트용 컴포넌트 ─── + +const TestDiv: React.FC<{ className?: string; children?: React.ReactNode; 'data-testid'?: string }> = + ({ className, children, 'data-testid': testId }) => ( + <div className={className} data-testid={testId}>{children}</div> + ); + +const TestLabel: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <label className={className}>{children || text}</label>; + +const TestP: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <p className={className}>{children || text}</p>; + +const TestSelect: React.FC<{ + name?: string; value?: string; className?: string; options?: any[]; + onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void; +}> = ({ name, value, className, options, onChange }) => ( + <select data-name={name} value={value} className={className} onChange={onChange}> + {options?.map((o) => <option key={String(o.value)} value={o.value}>{o.label}</option>)} + </select> +); + +// 렌더러가 컴포넌트 목록을 Fragment 로 감싸므로 반드시 등록해야 한다. +const TestFragment: React.FC<{ children?: React.ReactNode }> = ({ children }) => <>{children}</>; + +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + return registry; +} + +// ─── 다국어 (공개 자산 디스크 필드가 쓰는 키) ─── + +const translations = { + 'sirsoft-ckeditor5': { + settings: { + fields: { + public_asset_disk: { + label: '공개 자산 디스크', + hint: '에디터 이미지 서빙 디스크를 오버라이드합니다.', + follow_core: '코어 설정 따름', + }, + }, + }, + }, +}; + +/** + * 플러그인 루트(plugin.json 기준)를 위로 훑어 찾는다. + * + * @returns 플러그인 루트 절대경로 + */ +function pluginRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'plugin.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('plugin.json 을 가진 플러그인 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다 (slots 포함). + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + const childGroups = [node.children ?? [], ...(node.slots ? Object.values(node.slots) : [])]; + for (const group of childGroups) { + for (const child of group as any[]) { + const found = findNode(child, predicate); + if (found) return found; + } + } + + return null; +} + +// 실제 필드 서브트리를 그대로 렌더 대상으로 쓴다 — 사본 표현식이 아닌 배포 JSON 자체가 SSoT. +const publicAssetField = findNode( + pluginSettings, + (n) => n.id === 'field_public_asset_disk', +); + +const CATALOG = [ + { id: 'none', label: { ko: '사용 안 함 (스트리밍)', en: 'None (streaming)' } }, + { id: 'fake_cdn', label: { ko: '가짜 CDN', en: 'Fake CDN' } }, +]; + +describe('CKEditor5 공개 자산 디스크 필드 — 계약', () => { + it('코어 설정 API 교차 조회 데이터소스가 없다 (권한 표면 단일화)', () => { + const dataSources = (pluginSettings as any).data_sources; + + // 존재 앵커 먼저 — data_sources 가 비어 있으면 아래 부재 단언이 공허하게 통과한다 + expect(dataSources.find((d: any) => d.id === 'settings')).toBeDefined(); + + // 카탈로그는 플러그인 설정 응답에 서버 부착 — 화면 권한(core.plugins.read)과 + // 다른 권한(core.settings.read)을 요구하는 교차 조회가 남아 있으면 안 된다 + expect(dataSources.find((d: any) => d.endpoint === '/api/admin/settings')).toBeUndefined(); + }); + + it('Select 가 저장 키와 설정 응답 부착 카탈로그를 바인딩한다', () => { + const select = findNode( + publicAssetField, + (n) => n.name === 'Select' && n.props?.name === 'public_asset_disk', + ); + + expect(select).not.toBeNull(); + expect(select.props.options).toContain('_local.form?.available_public_asset_disks'); + expect(select.props.options).toContain('follow_core'); + }); + + it('설정 스키마(getSettingsSchema)가 같은 키를 선언한다 (저장 whitelist)', () => { + const pluginSource = fs.readFileSync(path.join(pluginRoot(), 'plugin.php'), 'utf-8'); + const defaults = JSON.parse( + fs.readFileSync(path.join(pluginRoot(), 'config/settings/defaults.json'), 'utf-8'), + ); + + expect(pluginSource).toContain("'public_asset_disk' => ["); + expect(defaults.defaults).toHaveProperty('public_asset_disk'); + }); +}); + +describe('CKEditor5 공개 자산 디스크 필드 — 렌더링', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest> | null = null; + + beforeEach(() => { + registry = setupTestRegistry(); + // 표현식 내 $t() 는 컨텍스트에 $templateId 가 없으면 __templateApp.getConfig() + // 로 회수한다(engine-v1.38.2 폴백). layoutTestUtils 는 이를 세팅하지 않으므로 + // 테스트에서 동일 폴백 경로를 제공해야 옵션 계산식의 $t() 가 해석된다. + (window as any).__templateApp = { + getConfig: () => ({ templateId: 'test-template', locale: 'ko' }), + }; + }); + + afterEach(() => { + testUtils?.cleanup(); + testUtils = null; + (registry as any).registry = {}; + delete (window as any).__templateApp; + }); + + /** + * 필드 렌더링용 테스트 유틸을 생성한다. + * + * @returns createLayoutTest 유틸 + */ + function makeUtils() { + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_ckeditor5_public_asset_field', + components: [publicAssetField], + } as any, + { + auth: { + isAuthenticated: true, + user: { id: 1, name: 'Admin', role: 'super_admin' }, + authType: 'admin', + }, + translations, + locale: 'ko', + initialState: { + _local: { + // 플러그인 설정 GET 응답(initLocal: form)에 서버 부착되는 형태 + form: { + public_asset_disk: '', + available_public_asset_disks: CATALOG, + }, + }, + }, + }, + ); + } + + it('라벨/힌트가 다국어 해석되어 렌더된다', async () => { + testUtils = makeUtils(); + const { container } = await testUtils.render(); + + expect(container.querySelector('label.form-label')?.textContent ?? '').toContain( + '공개 자산 디스크', + ); + expect(container.querySelector('p.form-hint')?.textContent ?? '').toContain( + '에디터 이미지 서빙 디스크를 오버라이드합니다.', + ); + expect(() => testUtils!.assertNoValidationErrors()).not.toThrow(); + }); + + it('설정 응답 부착 카탈로그가 옵션으로 계산된다 (코어 따름 선두 + $localized 매핑)', async () => { + testUtils = makeUtils(); + const { container } = await testUtils.render(); + + const select = container.querySelector('select[data-name="public_asset_disk"]'); + expect(select).not.toBeNull(); + + const options = Array.from(select!.querySelectorAll('option')); + expect(options.map((o) => o.getAttribute('value'))).toEqual(['', 'none', 'fake_cdn']); + expect(options.map((o) => o.textContent)).toEqual([ + '코어 설정 따름', + '사용 안 함 (스트리밍)', + '가짜 CDN', + ]); + }); +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx index a97b7175..b9037eb0 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx @@ -7,9 +7,33 @@ * 적용되어 있는지 검증한다. */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import pluginSettingsLayout from '../../../layouts/admin/plugin_settings.json'; +/** 관리자 템플릿의 컴파일된 컴포넌트 CSS 경로 (합성 유틸리티 클래스의 실제 선언 위치). */ +const ADMIN_COMPONENTS_CSS = 'templates/_bundled/sirsoft-admin_basic/dist/css/components.css'; + +/** + * 저장소 루트(artisan 기준)를 위로 훑어 찾는다. + * + * @returns 저장소 루트 절대경로 + */ +function repoRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'artisan'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('artisan 을 가진 저장소 루트를 찾지 못했습니다.'); +} + /** 레이아웃 트리에서 주어진 id 의 노드를 찾는다. */ function findById(node: unknown, id: string): Record<string, unknown> | undefined { if (!node || typeof node !== 'object') { @@ -34,13 +58,24 @@ function classNameOf(node: Record<string, unknown> | undefined): string { } describe('plugin_settings 하단 버튼 sticky 고정', () => { - it('footer_buttons 에 sticky bottom 고정 클래스가 존재해야 한다', () => { + it('footer_buttons 가 sticky 고정 유틸리티 클래스를 사용해야 한다', () => { const footer = findById(pluginSettingsLayout, 'footer_buttons'); expect(footer).toBeDefined(); - const className = classNameOf(footer); - expect(className).toContain('sticky'); - expect(className).toContain('bottom-0'); - expect(className).toContain('z-10'); + // 레이아웃은 raw Tailwind 원자(sticky bottom-0 z-10) 대신 합성 유틸리티 한 개를 쓴다. + // 원자 이름을 직접 단언하면 합성 클래스로 옮긴 뒤 고정이 멀쩡한데도 실패한다. + expect(classNameOf(footer)).toContain('sticky-footer-buttons'); + }); + + it('그 유틸리티 클래스가 실제로 하단 고정 속성을 선언해야 한다', () => { + // 클래스 이름만 대조하면 동어반복이라, 컴파일된 CSS 에서 실제 선언을 확인한다 — + // 클래스명이 남아 있어도 선언이 사라지면 화면에서 고정이 풀리기 때문이다. + const css = fs.readFileSync(path.join(repoRoot(), ADMIN_COMPONENTS_CSS), 'utf-8'); + const rule = /\.sticky-footer-buttons\{([^}]*)\}/.exec(css); + + expect(rule).not.toBeNull(); + expect(rule![1]).toContain('position:sticky'); + expect(rule![1]).toContain('bottom:'); + expect(rule![1]).toContain('z-index:10'); }); }); diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/js/handlers/initEditor.ts b/plugins/_bundled/sirsoft-ckeditor5/resources/js/handlers/initEditor.ts index 2cdeb1da..849cc7c1 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/js/handlers/initEditor.ts +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/js/handlers/initEditor.ts @@ -1,3 +1,4 @@ +// e2e:allow ⑫⑬ 보안 수정 — 클라이언트 지정 업로드 권한 쿼리(죽은 배선) 제거만 수행. 업로드 동작·UI 불변(서버 admin 게이트 유지), 사용자 시나리오 변화 없음. /** * CKEditor5 초기화 핸들러 * @@ -65,7 +66,6 @@ interface InitEditorParams { placeholder?: string; readOnly?: boolean | string; imageUpload?: boolean | string; - uploadPermission?: string; height?: number | string; toolbar?: string; } @@ -641,14 +641,10 @@ function getPlugins(CKEDITOR: CKEditorGlobal, toolbarType: string, withImageUplo /** * 이미지 업로드 URL을 생성합니다. - * uploadPermission이 있으면 쿼리스트링으로 추가합니다. + * 인가는 서버 라우트 게이트(auth:sanctum + admin)가 전담하므로 쿼리 파라미터는 붙이지 않습니다. */ -function buildUploadUrl(uploadPermission: string): string { - const base = '/api/plugins/sirsoft-ckeditor5/upload'; - if (uploadPermission) { - return `${base}?permission=${encodeURIComponent(uploadPermission)}`; - } - return base; +function buildUploadUrl(): string { + return '/api/plugins/sirsoft-ckeditor5/upload'; } /** @@ -673,7 +669,6 @@ async function createEditorInstance( placeholder: string; readOnly: boolean; imageUpload: boolean; - uploadPermission: string; height: number; toolbar: string; containerId: string; @@ -764,7 +759,7 @@ async function createEditorInstance( if (options.imageUpload) { editorConfig.simpleUpload = { - uploadUrl: buildUploadUrl(options.uploadPermission), + uploadUrl: buildUploadUrl(), headers: getUploadHeaders(), }; } @@ -1005,7 +1000,6 @@ export async function initEditorHandler( const withImageUpload = params.imageUpload !== undefined ? (params.imageUpload === true || params.imageUpload === 'true') : (pluginSettings.imageUpload === true || pluginSettings.imageUpload === 'true'); - const uploadPermission = params.uploadPermission ?? ''; const placeholder = params.placeholder ?? ''; const height = params.height !== undefined ? (Number(params.height) || 400) : (Number(pluginSettings.editorHeight) || 400); const toolbarType = (params.toolbar !== undefined ? (params.toolbar as string) : (pluginSettings.toolbar as string)) ?? 'standard'; @@ -1055,7 +1049,6 @@ export async function initEditorHandler( placeholder, readOnly: isReadOnly, imageUpload: withImageUpload, - uploadPermission, height, toolbar: toolbarType, containerId, diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/lang/en.json b/plugins/_bundled/sirsoft-ckeditor5/resources/lang/en.json index d7baae0a..b8f06e64 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/lang/en.json +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/lang/en.json @@ -1,7 +1,54 @@ { "editor": { "data_source": { - "settings": "Editor Settings" + "settings": "Editor Settings", + "ckeditor5Uploads": "Uploaded Image List" + } + }, + "admin": { + "uploads": { + "title": "Editor Uploads", + "description": "Review images uploaded through the editor and clean up the ones that are no longer used.", + "refresh": "Refresh", + "open_settings": "Open cleanup settings", + "reference_notice": "Reference status is determined by scanning registered content such as posts, pages, and product descriptions. An image used only by an external extension that has not registered a reference source may appear as \"Unreferenced\", so verify before deleting.", + "scan_limited_notice": "The reference status filter evaluates the most recent {{count}} uploads. Older images are not shown in this list.", + "empty": "No uploaded images.", + "col": { + "thumbnail": "Preview", + "original_name": "Original name", + "file_size": "Size", + "uploader": "Uploaded by", + "created_at": "Uploaded at", + "referenced": "Reference" + }, + "badge": { + "referenced": "In use", + "unreferenced": "Unused" + }, + "filter": { + "search_placeholder": "Search by original name or hash", + "search_button": "Search", + "referenced_all": "All statuses", + "referenced_only": "In use only", + "unreferenced_only": "Unused only", + "date_from": "From", + "date_to": "To", + "clear": "Clear filters" + }, + "delete": { + "button": "Delete", + "confirm_title": "Delete this image?", + "confirm_message": "The image file and its record will both be deleted. This cannot be undone.", + "success": "The image has been deleted.", + "failed": "Failed to delete the image." + }, + "bulk_delete": { + "button": "Delete selected", + "selected": "{{count}} selected", + "confirm_title": "Delete the selected images?", + "confirm_message": "The files and records of the {{count}} selected images will both be deleted. This cannot be undone." + } } }, "settings": { @@ -10,8 +57,17 @@ "notice": "CKEditor5 is loaded via CDN. An internet connection is required. It is free to use without a separate license.", "section_toolbar": "Editor Settings", "section_image": "Image Settings", + "section_cleanup": "Unused Image Cleanup", "section_preview": "Editor Test", "section_preview_hint": "Preview CKEditor5 with the current settings applied.", + "cleanup": { + "toggle_label": "Auto cleanup unused images", + "toggle_hint": "Off by default — the operator must turn it on. When on, files and records of unused images past the retention period are deleted once a day.", + "retention_label": "Retention period (days)", + "retention_hint": "Only unused images older than this period are eligible for cleanup. (1 ~ 3650 days)", + "manual_hint": "To see what would be removed first, run this command on the server: php artisan sirsoft-ckeditor5:prune-unused-images --dry-run", + "open_uploads": "Open editor uploads" + }, "preview_placeholder": "Type here to test the editor.", "fields": { "toolbar": { @@ -29,6 +85,11 @@ "label": "Max Image Size (MB)", "hint": "Maximum allowed size for uploaded images. (1 ~ 10 MB)" }, + "public_asset_disk": { + "label": "Public Asset Disk", + "hint": "Select to use a different disk for this plugin only. Leave empty to follow the core public asset disk setting.", + "follow_core": "Follow core setting" + }, "editor_height": { "label": "Editor Height (px)", "hint": "Set the minimum height of the editor in pixels. (200 ~ 2000 px)" diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/lang/ko.json b/plugins/_bundled/sirsoft-ckeditor5/resources/lang/ko.json index 8f463bee..0dcee6a1 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/lang/ko.json +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/lang/ko.json @@ -1,7 +1,54 @@ { "editor": { "data_source": { - "settings": "에디터 설정" + "settings": "에디터 설정", + "ckeditor5Uploads": "업로드 이미지 목록" + } + }, + "admin": { + "uploads": { + "title": "에디터 업로드 이미지", + "description": "에디터로 업로드된 이미지를 확인하고, 더 이상 사용되지 않는 이미지를 정리합니다.", + "refresh": "새로고침", + "open_settings": "정리 설정 열기", + "reference_notice": "참조 상태는 게시글·페이지·상품 설명 등 등록된 콘텐츠를 훑어 판정합니다. 참조 소스를 등록하지 않은 외부 확장의 콘텐츠에만 쓰인 이미지는 「미참조」로 보일 수 있으므로 삭제 전에 확인하세요.", + "scan_limited_notice": "참조 상태 필터는 최근 {{count}}건을 기준으로 판정합니다. 그보다 오래된 이미지는 이 목록에 표시되지 않습니다.", + "empty": "업로드된 이미지가 없습니다.", + "col": { + "thumbnail": "미리보기", + "original_name": "원본 파일명", + "file_size": "크기", + "uploader": "업로더", + "created_at": "업로드 일시", + "referenced": "참조 상태" + }, + "badge": { + "referenced": "참조됨", + "unreferenced": "미참조" + }, + "filter": { + "search_placeholder": "원본 파일명 또는 해시로 검색", + "search_button": "검색", + "referenced_all": "전체 상태", + "referenced_only": "참조됨만", + "unreferenced_only": "미참조만", + "date_from": "시작일", + "date_to": "종료일", + "clear": "필터 초기화" + }, + "delete": { + "button": "삭제", + "confirm_title": "이미지를 삭제할까요?", + "confirm_message": "선택한 이미지의 파일과 기록이 함께 삭제됩니다. 이 작업은 되돌릴 수 없습니다.", + "success": "이미지를 삭제했습니다.", + "failed": "이미지 삭제에 실패했습니다." + }, + "bulk_delete": { + "button": "선택 삭제", + "selected": "{{count}}건 선택됨", + "confirm_title": "선택한 이미지를 삭제할까요?", + "confirm_message": "선택한 {{count}}건의 파일과 기록이 함께 삭제됩니다. 이 작업은 되돌릴 수 없습니다." + } } }, "settings": { @@ -10,8 +57,17 @@ "notice": "CKEditor5는 CDN 방식으로 로드됩니다. 인터넷 연결이 필요하며, 별도의 라이선스 없이 무료로 사용할 수 있습니다.", "section_toolbar": "에디터 설정", "section_image": "이미지 설정", + "section_cleanup": "미사용 이미지 정리", "section_preview": "에디터 테스트", "section_preview_hint": "현재 설정이 적용된 CKEditor5 에디터를 미리 확인할 수 있습니다.", + "cleanup": { + "toggle_label": "미사용 이미지 자동 정리", + "toggle_hint": "기본 꺼짐 — 운영자가 직접 켜야 동작합니다. 켜면 매일 한 번, 보존기간이 지난 미사용 이미지의 파일과 기록을 함께 삭제합니다.", + "retention_label": "보존기간 (일)", + "retention_hint": "업로드 후 이 기간이 지난 미사용 이미지만 정리 대상이 됩니다. (1 ~ 3650 일)", + "manual_hint": "지금 무엇이 지워질지 먼저 확인하려면 서버에서 다음 명령을 실행하세요: php artisan sirsoft-ckeditor5:prune-unused-images --dry-run", + "open_uploads": "업로드 이미지 관리 열기" + }, "preview_placeholder": "여기에 내용을 입력하여 에디터를 테스트해 보세요.", "fields": { "toolbar": { @@ -29,6 +85,11 @@ "label": "최대 이미지 크기 (MB)", "hint": "업로드 가능한 이미지의 최대 크기입니다. (1 ~ 10 MB)" }, + "public_asset_disk": { + "label": "공개 자산 디스크", + "hint": "이 플러그인만 다른 디스크를 쓰려면 선택합니다. 비우면 코어 공개 자산 디스크 설정을 따릅니다.", + "follow_core": "코어 설정 따름" + }, "editor_height": { "label": "에디터 높이 (px)", "hint": "에디터의 최소 높이를 픽셀 단위로 설정합니다. (200 ~ 2000 px)" diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/ckeditor5_uploads.json b/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/ckeditor5_uploads.json new file mode 100644 index 00000000..eeccf509 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/ckeditor5_uploads.json @@ -0,0 +1,1068 @@ +{ + "version": "1.0.0", + "layout_name": "ckeditor5_uploads", + "permissions": [ + "sirsoft-ckeditor5.uploads.read" + ], + "extends": "_admin_base", + "meta": { + "title": "$t:sirsoft-ckeditor5.admin.uploads.title", + "description": "$t:sirsoft-ckeditor5.admin.uploads.description" + }, + "state": { + "filter": { + "search": "", + "referenced": "all", + "dateFrom": "", + "dateTo": "" + } + }, + "transition_overlay": { + "wait_for": [ + "ckeditor5Uploads" + ] + }, + "data_sources": [ + { + "id": "ckeditor5Uploads", + "label_key": "$t:sirsoft-ckeditor5.editor.data_source.ckeditor5Uploads", + "type": "api", + "endpoint": "/api/plugins/sirsoft-ckeditor5/admin/uploads", + "method": "GET", + "auto_fetch": true, + "auth_required": true, + "loading_strategy": "progressive", + "params": { + "page": "{{query.page || 1}}", + "per_page": "{{query.per_page || 20}}", + "search": "{{query.search}}", + "referenced": "{{query.referenced}}", + "date_from": "{{query.date_from}}", + "date_to": "{{query.date_to}}", + "sort_by": "{{query.sort_by}}", + "sort_order": "{{query.sort_order}}" + }, + "errorHandling": { + "403": { + "handler": "showErrorPage", + "params": { + "target": "content" + } + } + }, + "fallback": { + "data": { + "data": [], + "pagination": { + "current_page": 1, + "last_page": 1, + "per_page": 20, + "total": 0, + "from": null, + "to": null + }, + "meta": { + "scan_limited": false, + "scan_window": 500 + } + } + } + } + ], + "init_actions": [ + { + "handler": "setState", + "params": { + "target": "local", + "filter": { + "search": "{{query.search || ''}}", + "referenced": "{{query.referenced || 'all'}}", + "dateFrom": "{{query.date_from || ''}}", + "dateTo": "{{query.date_to || ''}}" + } + } + } + ], + "named_actions": { + "searchUploads": { + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/uploads", + "replace": true, + "mergeQuery": true, + "query": { + "page": "", + "search": "{{_local.filter.search || ''}}", + "referenced": "{{_local.filter.referenced || 'all'}}", + "date_from": "{{_local.filter.dateFrom || ''}}", + "date_to": "{{_local.filter.dateTo || ''}}" + }, + "transition_overlay_target": "ckeditor5_uploads_datagrid__body" + } + }, + "clearFilters": { + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "local", + "filter": { + "search": "", + "referenced": "all", + "dateFrom": "", + "dateTo": "" + } + } + }, + { + "comment": "audit:allow layout-list-context-navigate-merge-query 필터 초기화 — 검색·참조상태·기간과 페이지를 모두 비우는 의도적 리셋이므로 병합하지 않는다", + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/uploads", + "replace": true, + "query": {} + } + } + ] + } + } + }, + "slots": { + "content": [ + { + "id": "ckeditor5_uploads_content", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-page-content" + }, + "children": [ + { + "id": "page_header", + "type": "basic", + "name": "Div", + "props": { + "className": "flex-between mb-6" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "H1", + "props": { + "className": "page-title" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.title" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-label-subtle mt-1" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.description" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "flex-center gap-2" + }, + "children": [ + { + "id": "open_settings_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary" + }, + "actions": [ + { + "comment": "audit:allow layout-list-context-navigate-merge-query 플러그인 설정 화면으로 나가는 이동 — 목록 클러스터 밖이라 목록 상태를 승계하지 않는다", + "type": "click", + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/settings" + } + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-ckeditor5.admin.uploads.open_settings" + } + ] + }, + { + "id": "refresh_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn-icon", + "title": "$t:sirsoft-ckeditor5.admin.uploads.refresh" + }, + "actions": [ + { + "type": "click", + "handler": "refetchDataSource", + "params": { + "dataSourceId": "ckeditor5Uploads" + } + } + ], + "children": [ + { + "type": "basic", + "name": "Icon", + "props": { + "name": "fa-solid fa-arrows-rotate", + "className": "text-xl" + } + } + ] + } + ] + } + ] + }, + { + "id": "reference_notice", + "type": "basic", + "name": "Div", + "props": { + "className": "mb-6 p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { + "className": "text-sm text-blue-800 dark:text-blue-200" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.reference_notice" + } + ] + }, + { + "id": "scan_limited_notice", + "type": "basic", + "name": "Div", + "if": "{{ckeditor5Uploads?.data?.meta?.scan_limited === true}}", + "props": { + "className": "mb-6 p-4 rounded-xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { + "className": "text-sm text-amber-800 dark:text-amber-200" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.scan_limited_notice|count={{ckeditor5Uploads?.data?.meta?.scan_window ?? 500}}" + } + ] + }, + { + "id": "filter_section", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card mb-6" + }, + "children": [ + { + "id": "search_row", + "type": "basic", + "name": "Div", + "props": { + "className": "flex-center flex-wrap gap-2" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "relative flex-1" + }, + "children": [ + { + "type": "basic", + "name": "Icon", + "props": { + "name": "fa-solid fa-search", + "className": "absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 dark:text-gray-500 text-sm pointer-events-none" + } + }, + { + "type": "basic", + "name": "Input", + "props": { + "name": "search", + "type": "text", + "placeholder": "$t:sirsoft-ckeditor5.admin.uploads.filter.search_placeholder", + "className": "input pl-10", + "value": "{{_local.filter.search || ''}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "filter.search": "{{$event.target.value}}" + } + }, + { + "type": "keydown", + "key": "Enter", + "actionRef": "searchUploads" + } + ] + } + ] + }, + { + "type": "composite", + "name": "Select", + "props": { + "name": "referenced", + "value": "{{_local.filter.referenced || 'all'}}", + "options": [ + { + "value": "all", + "label": "$t:sirsoft-ckeditor5.admin.uploads.filter.referenced_all" + }, + { + "value": "referenced", + "label": "$t:sirsoft-ckeditor5.admin.uploads.filter.referenced_only" + }, + { + "value": "unreferenced", + "label": "$t:sirsoft-ckeditor5.admin.uploads.filter.unreferenced_only" + } + ] + }, + "actions": [ + { + "type": "change", + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "local", + "filter.referenced": "{{$event.target.value}}" + } + }, + { + "actionRef": "searchUploads" + } + ] + } + } + ] + }, + { + "type": "basic", + "name": "Input", + "props": { + "name": "date_from", + "type": "date", + "className": "input w-40", + "title": "$t:sirsoft-ckeditor5.admin.uploads.filter.date_from", + "value": "{{_local.filter.dateFrom || ''}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "filter.dateFrom": "{{$event.target.value}}" + } + } + ] + }, + { + "type": "basic", + "name": "Input", + "props": { + "name": "date_to", + "type": "date", + "className": "input w-40", + "title": "$t:sirsoft-ckeditor5.admin.uploads.filter.date_to", + "value": "{{_local.filter.dateTo || ''}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "filter.dateTo": "{{$event.target.value}}" + } + } + ] + }, + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-primary" + }, + "actions": [ + { + "type": "click", + "actionRef": "searchUploads" + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-ckeditor5.admin.uploads.filter.search_button" + } + ] + } + ] + }, + { + "id": "clear_filters_row", + "type": "basic", + "name": "Div", + "if": "{{!!(query.search || query.referenced || query.date_from || query.date_to)}}", + "props": { + "className": "flex justify-end mt-2" + }, + "children": [ + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "text-tertiary hover:text-gray-700 dark:hover:text-gray-200 underline" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.filter.clear", + "actions": [ + { + "type": "click", + "actionRef": "clearFilters" + } + ] + } + ] + } + ] + }, + { + "id": "bulk_action_bar", + "type": "basic", + "name": "Div", + "if": "{{(_global.selectedIds_ckeditor5_uploads?.length ?? 0) > 0}}", + "props": { + "className": "flex-between mb-4 p-3 rounded-xl bg-gray-100 dark:bg-gray-800" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "text-body" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.bulk_delete.selected|count={{_global.selectedIds_ckeditor5_uploads?.length ?? 0}}" + }, + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "flex-center gap-2 px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800" + }, + "actions": [ + { + "type": "click", + "handler": "openModal", + "target": "bulk_delete_confirm_modal" + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-ckeditor5.admin.uploads.bulk_delete.button" + } + ] + } + ] + }, + { + "id": "ckeditor5_uploads_datagrid", + "type": "composite", + "name": "DataGrid", + "props": { + "data": "{{ckeditor5Uploads?.data?.data ?? []}}", + "pagination": true, + "serverSidePagination": true, + "serverCurrentPage": "{{ckeditor5Uploads?.data?.pagination?.current_page ?? 1}}", + "serverTotalPages": "{{ckeditor5Uploads?.data?.pagination?.last_page ?? null}}", + "pageSize": "{{+(query.per_page || 20)}}", + "alwaysShowPagination": true, + "sortable": true, + "sortField": "{{query.sort_by || 'created_at'}}", + "sortDirection": "{{query.sort_order || 'desc'}}", + "idField": "id", + "selectable": true, + "selectionScope": "page", + "selectedIds": "{{_global.selectedIds_ckeditor5_uploads || []}}", + "loading": "{{ckeditor5Uploads?.loading}}", + "emptyText": "$t:sirsoft-ckeditor5.admin.uploads.empty", + "actionsColumnHeader": "$t:common.actions", + "rowActions": [ + { + "id": "delete", + "label": "$t:sirsoft-ckeditor5.admin.uploads.delete.button", + "iconName": "trash", + "variant": "danger" + } + ], + "columns": [ + { + "field": "thumbnail", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.thumbnail", + "width": "90px", + "cellChildren": [ + { + "type": "basic", + "name": "Img", + "props": { + "src": "{{row.download_url}}", + "alt": "{{row.original_name ?? ''}}", + "loading": "lazy", + "className": "w-16 h-16 object-cover rounded-lg border border-gray-200 dark:border-gray-700" + } + } + ] + }, + { + "field": "original_name", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.original_name", + "sortable": true, + "width": "260px", + "cellChildren": [ + { + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "block text-heading line-clamp-1" + }, + "text": "{{row.original_name ?? '-'}}" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "block text-tertiary font-mono" + }, + "text": "{{row.hash ?? ''}}" + } + ] + } + ] + }, + { + "field": "file_size", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.file_size", + "sortable": true, + "width": "110px", + "cellChildren": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "text-label whitespace-nowrap" + }, + "text": "{{Math.round((row.file_size ?? 0) / 1024).toLocaleString()}} KB" + } + ] + }, + { + "field": "uploader_name", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.uploader", + "width": "140px", + "cellChildren": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "text-body" + }, + "text": "{{row.uploader_name ?? '-'}}" + } + ] + }, + { + "field": "created_at", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.created_at", + "sortable": true, + "width": "170px", + "cellChildren": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "text-label whitespace-nowrap" + }, + "text": "{{row.created_at ?? '-'}}" + } + ] + }, + { + "field": "referenced", + "header": "$t:sirsoft-ckeditor5.admin.uploads.col.referenced", + "width": "110px", + "cellChildren": [ + { + "type": "basic", + "name": "Span", + "if": "{{row.referenced === true}}", + "props": { + "className": "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.badge.referenced" + }, + { + "type": "basic", + "name": "Span", + "if": "{{row.referenced !== true}}", + "props": { + "className": "inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.badge.unreferenced" + } + ] + } + ] + }, + "actions": [ + { + "event": "onPageChange", + "type": "change", + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/uploads", + "replace": true, + "mergeQuery": true, + "query": { + "page": "{{$args[0]}}" + }, + "transition_overlay_target": "ckeditor5_uploads_datagrid__body" + } + }, + { + "event": "onSortChange", + "type": "change", + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/uploads", + "replace": true, + "mergeQuery": true, + "query": { + "page": "", + "sort_by": "{{$args[0]}}", + "sort_order": "{{$args[1]}}" + }, + "transition_overlay_target": "ckeditor5_uploads_datagrid__body" + } + }, + { + "event": "onSelectionChange", + "type": "action", + "handler": "setState", + "params": { + "target": "global", + "selectedIds_ckeditor5_uploads": "{{$args[0]}}" + } + }, + { + "event": "onRowAction", + "type": "action", + "handler": "switch", + "cases": { + "delete": { + "handler": "setState", + "params": { + "target": "global", + "modal_data": "{{$args[1]}}" + }, + "onSuccess": [ + { + "handler": "openModal", + "target": "delete_confirm_modal" + } + ] + } + } + } + ] + } + ] + } + ] + }, + "modals": [ + { + "meta": { + "editor_label": "$t:sirsoft-ckeditor5.admin.uploads.delete.confirm_title" + }, + "id": "delete_confirm_modal", + "type": "composite", + "name": "Modal", + "props": { + "title": "$t:sirsoft-ckeditor5.admin.uploads.delete.confirm_title", + "size": "sm" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "py-2" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { + "className": "text-gray-700 dark:text-gray-300" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.delete.confirm_message" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "modal-footer-buttons" + }, + "children": [ + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary", + "disabled": "{{_global.isDeletingUpload}}" + }, + "actions": [ + { + "type": "click", + "handler": "closeModal" + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:common.cancel" + } + ] + }, + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "flex-center gap-2 px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 disabled:opacity-50 disabled:cursor-not-allowed", + "disabled": "{{_global.isDeletingUpload}}" + }, + "actions": [ + { + "type": "click", + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": true + } + }, + { + "handler": "apiCall", + "auth_required": true, + "target": "/api/plugins/sirsoft-ckeditor5/admin/uploads/{{_global.modal_data.id}}", + "params": { + "method": "DELETE" + }, + "onSuccess": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false + } + }, + { + "handler": "refetchDataSource", + "params": { + "dataSourceId": "ckeditor5Uploads" + } + }, + { + "handler": "toast", + "params": { + "type": "success", + "message": "$t:sirsoft-ckeditor5.admin.uploads.delete.success" + } + }, + { + "handler": "closeModal" + } + ], + "onError": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "{{error.message ?? '$t:sirsoft-ckeditor5.admin.uploads.delete.failed'}}" + } + } + ], + "errorHandling": { + "403": { + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "$t:auth.permission_denied" + } + } + ] + } + } + } + } + ] + } + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:common.delete" + } + ] + } + ] + } + ] + }, + { + "meta": { + "editor_label": "$t:sirsoft-ckeditor5.admin.uploads.bulk_delete.confirm_title" + }, + "id": "bulk_delete_confirm_modal", + "type": "composite", + "name": "Modal", + "props": { + "title": "$t:sirsoft-ckeditor5.admin.uploads.bulk_delete.confirm_title", + "size": "sm" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "py-2" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { + "className": "text-gray-700 dark:text-gray-300" + }, + "text": "$t:sirsoft-ckeditor5.admin.uploads.bulk_delete.confirm_message|count={{_global.selectedIds_ckeditor5_uploads?.length ?? 0}}" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "modal-footer-buttons" + }, + "children": [ + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary", + "disabled": "{{_global.isDeletingUpload}}" + }, + "actions": [ + { + "type": "click", + "handler": "closeModal" + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:common.cancel" + } + ] + }, + { + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "flex-center gap-2 px-4 py-2 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 disabled:opacity-50 disabled:cursor-not-allowed", + "disabled": "{{_global.isDeletingUpload}}" + }, + "actions": [ + { + "type": "click", + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": true + } + }, + { + "handler": "apiCall", + "auth_required": true, + "target": "/api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete", + "params": { + "method": "POST", + "body": { + "ids": "{{_global.selectedIds_ckeditor5_uploads ?? []}}" + } + }, + "onSuccess": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false, + "selectedIds_ckeditor5_uploads": [] + } + }, + { + "handler": "refetchDataSource", + "params": { + "dataSourceId": "ckeditor5Uploads" + } + }, + { + "handler": "toast", + "params": { + "type": "success", + "message": "{{response.message}}" + } + }, + { + "handler": "closeModal" + } + ], + "onError": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "{{error.message ?? '$t:sirsoft-ckeditor5.admin.uploads.delete.failed'}}" + } + } + ], + "errorHandling": { + "403": { + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isDeletingUpload": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "$t:auth.permission_denied" + } + } + ] + } + } + } + } + ] + } + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:common.delete" + } + ] + } + ] + } + ] + } + ] +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/plugin_settings.json index ca0c1831..2aefc4ad 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/plugin_settings.json @@ -330,7 +330,7 @@ "props": { "className": "lg:col-span-2" }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "name": "imageUpload" } } @@ -380,6 +380,205 @@ ] } ] + }, + { + "id": "field_public_asset_disk", + "type": "basic", + "name": "Div", + "props": { "className": "section-heading-md" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-1" }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { "className": "form-label" }, + "text": "$t:sirsoft-ckeditor5.settings.fields.public_asset_disk.label" + }, + { + "type": "basic", + "name": "P", + "props": { "className": "form-hint" }, + "text": "$t:sirsoft-ckeditor5.settings.fields.public_asset_disk.hint" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-2" }, + "children": [ + { + "type": "composite", + "name": "Select", + "props": { + "name": "public_asset_disk", + "className": "w-full", + "options": "{{[{value: '', label: $t('sirsoft-ckeditor5.settings.fields.public_asset_disk.follow_core')}].concat((_local.form?.available_public_asset_disks ?? []).map(d => ({value: d.id, label: $localized(d.label)})))}}" + } + } + ] + } + ] + } + ] + } + ] + }, + { + "id": "section_cleanup_card", + "type": "basic", + "name": "Div", + "props": { + "className": "mt-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700" + }, + "children": [ + { + "id": "section_cleanup_header", + "type": "basic", + "name": "Div", + "props": { "className": "panel-header-row" }, + "children": [ + { + "type": "basic", + "name": "H3", + "props": { "className": "text-error-strong" }, + "text": "$t:sirsoft-ckeditor5.settings.section_cleanup" + } + ] + }, + { + "id": "section_cleanup_fields", + "type": "basic", + "name": "Div", + "props": { "className": "p-6 space-y-6" }, + "children": [ + { + "id": "field_unused_image_cleanup", + "type": "basic", + "name": "Div", + "props": { "className": "section-heading-md" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-1" }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { "className": "form-label" }, + "text": "$t:sirsoft-ckeditor5.settings.cleanup.toggle_label" + }, + { + "type": "basic", + "name": "P", + "props": { "className": "form-hint" }, + "text": "$t:sirsoft-ckeditor5.settings.cleanup.toggle_hint" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-2" }, + "children": [ + { + "type": "composite", + "name": "Toggle", + "props": { "name": "unusedImageCleanup" } + } + ] + } + ] + }, + { + "id": "field_unused_image_retention_days", + "type": "basic", + "name": "Div", + "props": { "className": "section-heading-md" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-1" }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { "className": "form-label" }, + "text": "$t:sirsoft-ckeditor5.settings.cleanup.retention_label" + }, + { + "type": "basic", + "name": "P", + "props": { "className": "form-hint" }, + "text": "$t:sirsoft-ckeditor5.settings.cleanup.retention_hint" + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { "className": "lg:col-span-2" }, + "children": [ + { + "type": "basic", + "name": "Input", + "props": { + "type": "number", + "name": "unusedImageRetentionDays", + "min": 1, + "className": "focus:border-transparent" + } + } + ] + } + ] + }, + { + "id": "cleanup_manual_hint", + "type": "basic", + "name": "Div", + "props": { + "className": "p-4 rounded-lg bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 space-y-3" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { "className": "text-sm text-gray-600 dark:text-gray-300 break-all" }, + "text": "$t:sirsoft-ckeditor5.settings.cleanup.manual_hint" + }, + { + "id": "open_uploads_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary" + }, + "actions": [ + { + "type": "click", + "handler": "navigate", + "params": { + "path": "/admin/plugins/sirsoft-ckeditor5/uploads" + } + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:sirsoft-ckeditor5.settings.cleanup.open_uploads" + } + ] + } + ] } ] } @@ -631,7 +830,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-ckeditor5/resources/routes.json b/plugins/_bundled/sirsoft-ckeditor5/resources/routes.json index 46ad1446..deba863f 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/resources/routes.json +++ b/plugins/_bundled/sirsoft-ckeditor5/resources/routes.json @@ -1,4 +1,14 @@ { "version": "1.0.0", - "routes": [] + "routes": [ + { + "path": "*/admin/plugins/sirsoft-ckeditor5/uploads", + "layout": "ckeditor5_uploads", + "auth_required": true, + "meta": { + "title": "$t:sirsoft-ckeditor5.admin.uploads.title", + "permission": "sirsoft-ckeditor5.uploads.read" + } + } + ] } diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Console/Commands/PruneUnusedImagesCommand.php b/plugins/_bundled/sirsoft-ckeditor5/src/Console/Commands/PruneUnusedImagesCommand.php new file mode 100644 index 00000000..4f545bfd --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Console/Commands/PruneUnusedImagesCommand.php @@ -0,0 +1,172 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Support\Facades\Log; +use Plugins\Sirsoft\Ckeditor5\Services\ImageCleanupService; + +/** + * 미참조 CKEditor5 업로드 이미지 정리 커맨드 + * + * 에디터로 올렸지만 어떤 콘텐츠에서도 쓰이지 않는 이미지를 파일·기록 함께 정리합니다. + * + * 자동 정리는 사용자 파일을 지우므로 기본 꺼짐(운영자 옵트인)입니다. 스케줄러 호출 + * (`--scheduled`)은 커맨드 안에서 설정을 **false 폴백으로** 재확인합니다 — 스케줄 등록부의 + * `enabled_config` 게이트는 설정 조회가 실패하면 true 로 폴백하므로, 그것만으로는 + * "설정을 만지지 않은 사이트에서 자동 삭제 0" 을 보장할 수 없습니다. + * + * @example php artisan sirsoft-ckeditor5:prune-unused-images --dry-run + * @example php artisan sirsoft-ckeditor5:prune-unused-images --days=7 --limit=50 + */ +class PruneUnusedImagesCommand extends Command +{ + /** + * 커맨드 이름 및 시그니처 + * + * @var string + */ + protected $signature = 'sirsoft-ckeditor5:prune-unused-images + {--dry-run : 실제 삭제 없이 판정 결과만 확인} + {--limit=200 : 한 번에 스캔할 최대 후보 수} + {--days= : 보존기간(일) 재정의 — 미지정 시 플러그인 설정값} + {--scheduled : 스케줄러 호출 표시 — 자동 정리 토글이 꺼져 있으면 조기 종료}'; + + /** + * 커맨드 설명 + * + * @var string + */ + protected $description = '어떤 콘텐츠에서도 사용되지 않는 에디터 업로드 이미지를 정리합니다.'; + + /** + * @param ImageCleanupService $cleanupService 이미지 정리 서비스 + */ + public function __construct( + protected ImageCleanupService $cleanupService + ) { + parent::__construct(); + } + + /** + * 커맨드 실행 + * + * @return int 종료 코드 + */ + public function handle(): int + { + if ($this->option('scheduled') && ! $this->isAutoCleanupEnabled()) { + $this->info('미사용 이미지 자동 정리가 꺼져 있어 실행하지 않았습니다. (unusedImageCleanup = false)'); + + return Command::SUCCESS; + } + + $days = $this->resolveRetentionDays(); + + if ($days < 1) { + $this->info(__('sirsoft-ckeditor5::messages.cleanup.retention_disabled')); + + return Command::SUCCESS; + } + + $limit = max(1, (int) $this->option('limit')); + $isDryRun = (bool) $this->option('dry-run'); + + $result = $this->cleanupService->pruneUnused($days, $limit, $isDryRun); + + // 판정 불가로 건너뛴 회차 — "정리 완료(0건)" 로 보이면 운영자가 원인을 모른다. + if (($result['skipped_reason'] ?? null) === 'sources_incomplete') { + $this->warn(__('sirsoft-ckeditor5::messages.cleanup.sources_incomplete')); + + return Command::SUCCESS; + } + + if ($isDryRun) { + $this->renderDryRun($result, $days); + + return Command::SUCCESS; + } + + $this->info(sprintf( + '보존기간(%d일) 경과 후보 %d건 중 참조됨 %d건, 삭제 %d건, 실패 %d건.', + $days, + $result['scanned'], + $result['referenced'], + $result['deleted'], + $result['failed'], + )); + + if ($result['failed'] > 0) { + $this->warn(sprintf('%d건은 파일 삭제에 실패해 기록을 보존했습니다. 다음 회차에 재시도합니다.', $result['failed'])); + } + + Log::info('PruneUnusedImagesCommand: 미참조 에디터 이미지 정리 완료', [ + 'days' => $days, + 'limit' => $limit, + 'scanned' => $result['scanned'], + 'referenced' => $result['referenced'], + 'deleted' => $result['deleted'], + 'failed' => $result['failed'], + ]); + + return Command::SUCCESS; + } + + /** + * 자동 정리 토글을 false 폴백으로 조회합니다. + * + * @return bool 자동 정리 활성 여부 + */ + private function isAutoCleanupEnabled(): bool + { + return (bool) plugin_setting('sirsoft-ckeditor5', 'unusedImageCleanup', false); + } + + /** + * 보존기간을 해석합니다 (옵션 > 플러그인 설정 > 기본 30일). + * + * @return int 보존기간(일) + */ + private function resolveRetentionDays(): int + { + $option = $this->option('days'); + + if ($option !== null && $option !== '') { + return (int) $option; + } + + return (int) plugin_setting('sirsoft-ckeditor5', 'unusedImageRetentionDays', 30); + } + + /** + * dry-run 결과를 표로 출력합니다. + * + * @param array{scanned: int, referenced: int, deleted: int, failed: int, items: array<int, array<string, mixed>>} $result 판정 결과 + * @param int $days 보존기간(일) + */ + private function renderDryRun(array $result, int $days): void + { + $this->info(sprintf( + '[DRY RUN] 보존기간(%d일) 경과 후보 %d건 중 참조됨 %d건, 삭제 대상 %d건.', + $days, + $result['scanned'], + $result['referenced'], + count($result['items']), + )); + + if ($result['items'] === []) { + return; + } + + $this->table( + ['ID', 'HASH', '원본명', '크기(bytes)', '업로드 시각'], + array_map(fn (array $item) => [ + $item['id'], + $item['hash'], + $item['original_name'], + $item['file_size'], + $item['created_at'], + ], $result['items']) + ); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/Admin/ImageUploadAdminController.php b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/Admin/ImageUploadAdminController.php new file mode 100644 index 00000000..2d71a187 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/Admin/ImageUploadAdminController.php @@ -0,0 +1,111 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin; + +use App\Helpers\ResponseHelper; +use App\Http\Controllers\Api\Base\AdminBaseController; +use Illuminate\Http\JsonResponse; +use Plugins\Sirsoft\Ckeditor5\Http\Requests\BulkDeleteUploadsRequest; +use Plugins\Sirsoft\Ckeditor5\Http\Requests\IndexUploadsRequest; +use Plugins\Sirsoft\Ckeditor5\Http\Resources\ImageUploadResource; +use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadAdminService; + +/** + * CKEditor5 업로드 이미지 관리 컨트롤러 + * + * GET /api/plugins/sirsoft-ckeditor5/admin/uploads + * DELETE /api/plugins/sirsoft-ckeditor5/admin/uploads/{id} + * POST /api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete + * + * 권한은 라우트 미들웨어(`sirsoft-ckeditor5.uploads.read` / `.delete`)가 검증합니다. + */ +class ImageUploadAdminController extends AdminBaseController +{ + /** + * @param ImageUploadAdminService $adminService 업로드 관리 서비스 + */ + public function __construct( + private readonly ImageUploadAdminService $adminService, + ) { + parent::__construct(); + } + + /** + * 업로드 이미지 목록을 반환합니다. + * + * @param IndexUploadsRequest $request HTTP 요청 + * @return JsonResponse + */ + public function index(IndexUploadsRequest $request): JsonResponse + { + $result = $this->adminService->paginate( + $request->filters(), + $request->perPage(), + $request->pageNumber(), + ); + + return ResponseHelper::success('common.success', [ + 'data' => ImageUploadResource::collection($result['items']), + 'pagination' => $result['pagination'], + 'meta' => [ + 'scan_limited' => $result['scan_limited'], + 'scan_window' => ImageUploadAdminService::SCAN_WINDOW, + 'reference_sources_incomplete' => $result['reference_sources_incomplete'] ?? false, + ], + ]); + } + + /** + * 업로드 이미지 1건을 삭제합니다. + * + * @param int $id 업로드 ID + * @return JsonResponse + */ + public function destroy(int $id): JsonResponse + { + $upload = $this->adminService->find($id); + + if ($upload === null) { + return ResponseHelper::notFound('messages.uploads.not_found', domain: 'sirsoft-ckeditor5'); + } + + if (! $this->adminService->delete($upload)) { + return ResponseHelper::error('messages.uploads.file_delete_failed', 500, domain: 'sirsoft-ckeditor5'); + } + + return ResponseHelper::success('messages.uploads.deleted', domain: 'sirsoft-ckeditor5'); + } + + /** + * 업로드 이미지를 일괄 삭제합니다. + * + * @param BulkDeleteUploadsRequest $request HTTP 요청 + * @return JsonResponse + */ + public function bulkDestroy(BulkDeleteUploadsRequest $request): JsonResponse + { + $result = $this->adminService->bulkDelete($request->ids()); + + if ($result['deleted'] === 0 && $result['failed'] > 0) { + return ResponseHelper::error('messages.uploads.file_delete_failed', 500, $result, domain: 'sirsoft-ckeditor5'); + } + + // 부분 실패를 성공 문구("N건 삭제")로 접으면 운영자가 남은 파일을 모른다 — + // 실패 건수를 문구에 함께 싣는다 (실패 상세는 data 페이로드). + if ($result['failed'] > 0) { + return ResponseHelper::success( + 'messages.uploads.bulk_partially_deleted', + $result, + messageParams: ['deleted' => $result['deleted'], 'failed' => $result['failed']], + domain: 'sirsoft-ckeditor5', + ); + } + + return ResponseHelper::success( + 'messages.uploads.bulk_deleted', + $result, + messageParams: ['deleted' => $result['deleted']], + domain: 'sirsoft-ckeditor5', + ); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/ImageUploadController.php b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/ImageUploadController.php index cf7ad713..f0bb063b 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/ImageUploadController.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Controllers/ImageUploadController.php @@ -39,15 +39,9 @@ class ImageUploadController extends AdminBaseController */ public function upload(ImageUploadRequest $request): JsonResponse { - $uploadPermission = $request->query('permission', ''); - + // 인가는 라우트 게이트(AdminBaseController: auth:sanctum + admin)가 전담한다. + // 클라이언트가 검사 권한명을 지정하던 쿼리 분기는 죽은 인가라 제거했다. $user = $this->getCurrentUser(); - if ($uploadPermission && $user && ! $user->hasPermission($uploadPermission)) { - // audit:allow response-helper-bypass reason: CDN 상위 SimpleUploadAdapter 가 최상위 error.message 를 읽는다 (봉투 불가) - return response()->json([ - 'error' => ['message' => __('sirsoft-ckeditor5::messages.upload.forbidden')], - ], 403); - } try { $image = $this->imageUploadService->upload( diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/BulkDeleteUploadsRequest.php b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/BulkDeleteUploadsRequest.php new file mode 100644 index 00000000..f7122c99 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/BulkDeleteUploadsRequest.php @@ -0,0 +1,67 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; + +/** + * 관리자 업로드 이미지 일괄 삭제 요청 검증 + */ +class BulkDeleteUploadsRequest extends FormRequest +{ + /** 한 번에 삭제할 수 있는 최대 건수 */ + public const MAX_IDS = 100; + + /** + * 요청 권한 — 라우트 permission 미들웨어가 담당하므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'ids' => ['required', 'array', 'min:1', 'max:'.self::MAX_IDS], + 'ids.*' => ['integer', Rule::exists(Ckeditor5ImageUpload::class, 'id')], + ]; + } + + /** + * 검증 메시지를 반환합니다. + * + * @return array<string, string> 메시지 배열 + */ + public function messages(): array + { + return [ + 'ids.required' => __('sirsoft-ckeditor5::messages.uploads.ids_required'), + 'ids.min' => __('sirsoft-ckeditor5::messages.uploads.ids_required'), + 'ids.*.exists' => __('sirsoft-ckeditor5::messages.uploads.ids_invalid'), + 'ids.*.integer' => __('sirsoft-ckeditor5::messages.uploads.ids_invalid'), + ]; + } + + /** + * 삭제 대상 ID 목록을 반환합니다. + * + * @return array<int, int> ID 목록 + */ + public function ids(): array + { + /** @var array<int, int|string> $ids */ + $ids = $this->validated('ids') ?? []; + + return array_values(array_unique(array_map('intval', $ids))); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/IndexUploadsRequest.php b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/IndexUploadsRequest.php new file mode 100644 index 00000000..8e50e9c2 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Requests/IndexUploadsRequest.php @@ -0,0 +1,123 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; + +/** + * 관리자 업로드 이미지 목록 조회 요청 검증 + * + * 정렬 허용 어휘(`sort_by`)는 Repository 화이트리스트의 부분집합이어야 합니다 — + * 게이트가 더 넓으면 저장소가 조용히 기본 정렬로 되돌리고, 화면 옵션이 더 넓으면 + * 그 옵션을 고를 때마다 목록 전체가 422 가 됩니다. + */ +class IndexUploadsRequest extends FormRequest +{ + /** 기본 페이지 크기 */ + public const DEFAULT_PER_PAGE = 20; + + /** 페이지 크기 상한 */ + public const MAX_PER_PAGE = 100; + + /** + * 정렬 허용 컬럼 (Repository 화이트리스트와 동일 어휘) + * + * @var list<string> + */ + public const SORTABLE = ['created_at', 'file_size', 'original_name']; + + /** + * 참조 상태 필터 허용 어휘 + * + * @var list<string> + */ + public const REFERENCED_STATES = ['all', 'referenced', 'unreferenced']; + + /** + * 요청 권한 — 라우트 permission 미들웨어가 담당하므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 빈 문자열로 도착한 필터를 검증 전에 제거합니다. + * + * 화면은 값을 비울 때 `?referenced=` 처럼 빈 문자열을 보냅니다 — 그대로 두면 + * `Rule::in` 에 걸려 목록 전체가 422 가 됩니다. + */ + protected function prepareForValidation(): void + { + $cleared = []; + + foreach (['search', 'referenced', 'date_from', 'date_to', 'sort_by', 'sort_order', 'per_page', 'page'] as $key) { + if ($this->has($key) && $this->input($key) === '') { + $cleared[$key] = null; + } + } + + if ($cleared !== []) { + $this->merge($cleared); + } + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'search' => ['nullable', 'string', 'max:255'], + 'referenced' => ['nullable', 'string', Rule::in(self::REFERENCED_STATES)], + 'date_from' => ['nullable', 'date'], + 'date_to' => ['nullable', 'date'], + 'sort_by' => ['nullable', 'string', Rule::in(self::SORTABLE)], + 'sort_order' => ['nullable', 'string', Rule::in(['asc', 'desc'])], + 'page' => ['nullable', 'integer', 'min:1'], + 'per_page' => ['nullable', 'integer', 'min:1', 'max:'.self::MAX_PER_PAGE], + ]; + } + + /** + * 목록 필터를 반환합니다. + * + * @return array<string, mixed> 필터 배열 + */ + public function filters(): array + { + return [ + 'search' => $this->validated('search'), + 'referenced' => $this->validated('referenced') ?? 'all', + 'date_from' => $this->validated('date_from'), + 'date_to' => $this->validated('date_to'), + 'sort_by' => $this->validated('sort_by'), + 'sort_order' => $this->validated('sort_order'), + ]; + } + + /** + * 페이지 크기를 반환합니다. + * + * @return int 페이지 크기 + */ + public function perPage(): int + { + return (int) ($this->validated('per_page') ?? self::DEFAULT_PER_PAGE); + } + + /** + * 페이지 번호를 반환합니다. + * + * @return int 페이지 번호 + */ + public function pageNumber(): int + { + return (int) ($this->validated('page') ?? 1); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Http/Resources/ImageUploadResource.php b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Resources/ImageUploadResource.php new file mode 100644 index 00000000..95f2dd79 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Http/Resources/ImageUploadResource.php @@ -0,0 +1,37 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Http\Resources; + +use App\Http\Resources\BaseApiResource; +use Illuminate\Http\Request; + +/** + * CKEditor5 업로드 이미지 관리 화면 API 리소스 + * + * `referenced` 는 저장 컬럼이 아니라 조회 시 판정해 붙인 응답 전용 값입니다 + * (ImageUploadAdminService 참조). 판정하지 못한 행은 안전 방향인 "참조됨" 으로 봅니다. + */ +class ImageUploadResource extends BaseApiResource +{ + /** + * 리소스를 배열로 변환합니다. + * + * @param Request $request HTTP 요청 + * @return array<string, mixed> + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'hash' => $this->hash, + 'original_name' => $this->original_name, + 'file_size' => (int) $this->file_size, + 'mime_type' => $this->mime_type, + 'uploaded_by' => $this->uploaded_by, + 'uploader_name' => $this->relationLoaded('uploader') ? $this->uploader?->name : null, + 'created_at' => $this->formatDateTimeStringForUser($this->created_at), + 'download_url' => $this->download_url, + 'referenced' => (bool) ($this->referenced ?? true), + ]; + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Models/Ckeditor5ImageUpload.php b/plugins/_bundled/sirsoft-ckeditor5/src/Models/Ckeditor5ImageUpload.php index f4e7d4c0..0ddb3a90 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Models/Ckeditor5ImageUpload.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Models/Ckeditor5ImageUpload.php @@ -2,9 +2,12 @@ namespace Plugins\Sirsoft\Ckeditor5\Models; +use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\PluginStorageDriver; +use App\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use App\Models\User; +use Illuminate\Support\Carbon; /** * CKEditor5 이미지 업로드 기록 모델 @@ -17,8 +20,8 @@ use App\Models\User; * @property int $file_size 파일 크기 (bytes) * @property string $mime_type MIME 타입 * @property int|null $uploaded_by 업로드 사용자 ID - * @property \Illuminate\Support\Carbon $created_at - * @property \Illuminate\Support\Carbon $updated_at + * @property Carbon $created_at + * @property Carbon $updated_at * @property-read string $download_url 이미지 서빙 URL */ class Ckeditor5ImageUpload extends Model @@ -74,13 +77,59 @@ class Ckeditor5ImageUpload extends Model } /** - * 이미지 서빙 URL 반환 (API 라우트 경유) + * 행 disk 별 스토리지 드라이버 캐시 (직렬화 루프 대응 memoize) * - * @return string 이미지 서빙 URL + * @var array<string, StorageInterface> + */ + private static array $storageByDisk = []; + + /** + * 이미지 서빙 URL 반환 — 직접 URL(CDN) 우선, 불가 시 API 라우트 폴백 + * + * 행에 기록된 storage_disk 로 직접 URL 생성을 시도하고, 불가하면(로컬 디스크, + * url 미설정 디스크, 훅 차단 등) 기존 API 서빙 경로로 폴백합니다. + * + * @return string 이미지 URL */ public function getDownloadUrlAttribute(): string { - return '/api/plugins/sirsoft-ckeditor5/images/'.$this->hash; + return $this->resolveDirectUrl() ?? '/api/plugins/sirsoft-ckeditor5/images/'.$this->hash; + } + + /** + * 행 storage_disk 기준 직접 URL 해석을 시도합니다. + * + * file_path 는 카테고리 prefix 를 포함하므로("images/..." 또는 "ckeditor5/..." 등 + * 외부 생성 행 포함) 첫 세그먼트를 카테고리로 분해하는 일반형으로 처리합니다. + * + * @return string|null 직접 URL (불가하면 null) + */ + protected function resolveDirectUrl(): ?string + { + $disk = (string) ($this->storage_disk ?? ''); + $filePath = (string) ($this->file_path ?? ''); + + if ($disk === '' || $filePath === '' || ! str_contains($filePath, '/')) { + return null; + } + + [$category, $relativePath] = explode('/', $filePath, 2); + + if ($category === '' || $relativePath === '') { + return null; + } + + $storage = self::$storageByDisk[$disk] ??= new PluginStorageDriver('sirsoft-ckeditor5', $disk); + + return $storage->url($category, $relativePath); + } + + /** + * 행 disk 드라이버 memoize 캐시를 비웁니다 (테스트 격리용). + */ + public static function flushStorageCache(): void + { + self::$storageByDisk = []; } /** diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Providers/Ckeditor5ServiceProvider.php b/plugins/_bundled/sirsoft-ckeditor5/src/Providers/Ckeditor5ServiceProvider.php index e0bf62fb..4d12e8da 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Providers/Ckeditor5ServiceProvider.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Providers/Ckeditor5ServiceProvider.php @@ -3,8 +3,12 @@ namespace Plugins\Sirsoft\Ckeditor5\Providers; use App\Extension\BasePluginServiceProvider; +use Plugins\Sirsoft\Ckeditor5\Console\Commands\PruneUnusedImagesCommand; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageReferenceSourceRepositoryInterface; use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; +use Plugins\Sirsoft\Ckeditor5\Repositories\ImageReferenceSourceRepository; use Plugins\Sirsoft\Ckeditor5\Repositories\ImageUploadRepository; +use Plugins\Sirsoft\Ckeditor5\Services\ImageCleanupService; use Plugins\Sirsoft\Ckeditor5\Services\ImageServeService; use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadService; @@ -20,10 +24,38 @@ class Ckeditor5ServiceProvider extends BasePluginServiceProvider protected array $repositories = [ ImageUploadRepositoryInterface::class => ImageUploadRepository::class, + ImageReferenceSourceRepositoryInterface::class => ImageReferenceSourceRepository::class, ]; protected array $storageServices = [ - ImageUploadService::class, ImageServeService::class, + ImageCleanupService::class, ]; + + /** + * 카테고리별 StorageInterface가 필요한 서비스 매핑 (클래스 ⇒ 카테고리) + * + * 업로드 서비스는 getStorageDiskFor('images') 가 결정한 디스크(공개 자산 디스크 + * 설정 반영)를 주입받아, put/getDisk() 행 기록이 자동으로 카테고리 디스크를 따릅니다. + * 서빙 서비스는 행 storage_disk 기준 withDisk() 를 쓰므로 기본 주입을 유지합니다. + * + * @var array<class-string, string> + */ + protected array $storageCategoryServices = [ + ImageUploadService::class => 'images', + ]; + + /** + * 플러그인 부팅 — 콘솔 실행 시 정리 커맨드를 등록합니다. + */ + public function boot(): void + { + parent::boot(); + + if ($this->app->runningInConsole()) { + $this->commands([ + PruneUnusedImagesCommand::class, + ]); + } + } } diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageReferenceSourceRepositoryInterface.php b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageReferenceSourceRepositoryInterface.php new file mode 100644 index 00000000..7a6c6f92 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageReferenceSourceRepositoryInterface.php @@ -0,0 +1,45 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Repositories\Contracts; + +/** + * 에디터 이미지 참조 소스 조회 Repository 인터페이스 + * + * 참조 소스는 확장이 훅으로 등록하는 "테이블명 + 컬럼명" 이라 전용 모델이 없습니다. + * 그래서 이 Repository 는 특정 엔티티가 아니라 **선언된 소스에 대한 조회**를 소유합니다 — + * Service 는 어느 소스를 볼지와 무엇을 찾을지만 정하고, 실제 데이터 접근은 여기로 모읍니다. + */ +interface ImageReferenceSourceRepositoryInterface +{ + /** + * 소스 선언이 실제 스키마에 존재하는지 검증하고, 존재하는 컬럼만 남깁니다. + * + * @param string $table 테이블명 (프리픽스 제외) + * @param array<int, string> $columns 컬럼명 목록 + * @return array<int, string> 실재하는 컬럼 목록 (테이블이 없으면 빈 배열) + */ + public function resolveExistingColumns(string $table, array $columns): array; + + /** + * 지정 소스의 컬럼 중 하나라도 토큰을 포함하는 행이 있는지 확인합니다. + * + * @param string $table 테이블명 (프리픽스 제외) + * @param array<int, string> $columns 검사할 컬럼 목록 + * @param array<int, string> $tokens 판정 토큰 (LIKE 이스케이프는 구현이 담당) + * @return bool 하나라도 등장하면 true + */ + public function containsAnyToken(string $table, array $columns, array $tokens): bool; + + /** + * 지정 소스에 등장하는 토큰들을 한 번의 순회로 찾아 반환합니다. + * + * 토큰별로 LIKE 전체 스캔을 반복하면 비용이 (토큰 수 × 테이블 크기) 로 커진다 — + * 일괄 판정(관리 화면 미참조 필터·prune)은 이 메서드로 소스당 1회 순회만 수행한다. + * + * @param string $table 테이블명 (프리픽스 제외) + * @param array<int, string> $columns 검사할 컬럼 목록 + * @param array<int, string> $tokens 판정 토큰 + * @return array<int, string> 소스에 등장하는 토큰 목록 (부분집합) + */ + public function findTokensInSource(string $table, array $columns, array $tokens): array; +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageUploadRepositoryInterface.php b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageUploadRepositoryInterface.php index 2cb4c5e4..c55535d9 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageUploadRepositoryInterface.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/Contracts/ImageUploadRepositoryInterface.php @@ -2,6 +2,9 @@ namespace Plugins\Sirsoft\Ckeditor5\Repositories\Contracts; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; /** @@ -24,4 +27,68 @@ interface ImageUploadRepositoryInterface * @return Ckeditor5ImageUpload */ public function create(array $data): Ckeditor5ImageUpload; + + /** + * 기준 시각보다 오래된 업로드를 오래된 순으로 조회합니다. + * + * 정리 후보 조회 전용이라 판정·삭제에 필요한 컬럼만 읽습니다. + * + * @param Carbon $threshold 기준 시각 (이 시각 이전 업로드가 대상) + * @param int $limit 최대 조회 건수 + * @return Collection<int, Ckeditor5ImageUpload> 업로드 목록 (created_at 오름차순) + */ + public function findOlderThan(Carbon $threshold, int $limit): Collection; + + /** + * 기준 시각보다 오래된 업로드 건수를 반환합니다. + * + * @param Carbon $threshold 기준 시각 + * @return int 건수 + */ + public function countOlderThan(Carbon $threshold): int; + + /** + * 관리 화면용 업로드 목록을 페이지네이션 조회합니다. + * + * @param array $filters 필터 (search / date_from / date_to / sort_by / sort_order) + * @param int $perPage 페이지 크기 + * @return LengthAwarePaginator 페이지네이터 + */ + public function paginateForAdmin(array $filters, int $perPage): LengthAwarePaginator; + + /** + * 참조 판정 스캔 윈도우를 조회합니다 (최신순 상한). + * + * 참조 여부는 저장 컬럼이 아니라 매 조회 시 판정하므로, 참조 상태 필터를 쓸 때는 + * 전수가 아니라 최신 N 건만 훑습니다. 상한을 넘긴 사실은 응답 메타로 알립니다. + * + * @param array $filters 필터 (search / date_from / date_to) + * @param int $limit 스캔 윈도우 상한 + * @return Collection<int, Ckeditor5ImageUpload> 업로드 목록 (created_at 내림차순) + */ + public function findScanWindow(array $filters, int $limit): Collection; + + /** + * ID 목록으로 업로드를 조회합니다. + * + * @param array<int, int> $ids 업로드 ID 목록 + * @return Collection<int, Ckeditor5ImageUpload> 업로드 목록 + */ + public function findManyByIds(array $ids): Collection; + + /** + * ID 로 업로드 단건을 조회합니다. + * + * @param int $id 업로드 ID + * @return Ckeditor5ImageUpload|null + */ + public function findById(int $id): ?Ckeditor5ImageUpload; + + /** + * 업로드 기록을 삭제합니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @return bool 삭제 성공 여부 + */ + public function delete(Ckeditor5ImageUpload $upload): bool; } diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageReferenceSourceRepository.php b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageReferenceSourceRepository.php new file mode 100644 index 00000000..4b7d5dc4 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageReferenceSourceRepository.php @@ -0,0 +1,115 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Repositories; + +use Illuminate\Database\Query\Builder; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageReferenceSourceRepositoryInterface; + +/** + * 에디터 이미지 참조 소스 조회 Repository 구현체 + * + * 테이블명은 확장이 훅으로 선언한 값이라 런타임에 정해집니다. raw SQL 대신 쿼리빌더를 + * 쓰는 이유는 테이블 프리픽스를 빌더가 붙이고 값 바인딩도 빌더가 처리하기 때문입니다. + */ +class ImageReferenceSourceRepository implements ImageReferenceSourceRepositoryInterface +{ + /** + * {@inheritDoc} + */ + public function resolveExistingColumns(string $table, array $columns): array + { + if ($table === '' || $columns === [] || ! Schema::hasTable($table)) { + return []; + } + + return array_values(array_filter( + $columns, + fn (string $column) => Schema::hasColumn($table, $column) + )); + } + + /** + * {@inheritDoc} + */ + public function containsAnyToken(string $table, array $columns, array $tokens): bool + { + if ($columns === [] || $tokens === []) { + return false; + } + + return DB::table($table) + ->where(function (Builder $query) use ($columns, $tokens) { + foreach ($columns as $column) { + foreach ($tokens as $token) { + $query->orWhere($column, 'like', '%'.$this->escapeLike($token).'%'); + } + } + }) + ->exists(); + } + + /** + * {@inheritDoc} + */ + public function findTokensInSource(string $table, array $columns, array $tokens): array + { + if ($columns === [] || $tokens === []) { + return []; + } + + // id 키셋 순회가 불가능한 테이블은 토큰별 존재 확인으로 폴백한다. + if (! Schema::hasColumn($table, 'id')) { + return array_values(array_filter( + $tokens, + fn (string $token) => $this->containsAnyToken($table, $columns, [$token]) + )); + } + + $pending = array_values(array_unique($tokens)); + $found = []; + + // LIKE 는 인덱스를 못 쓰므로 토큰별 쿼리는 (토큰 수 × 전체 스캔) 이 된다. + // 대신 소스를 키셋 청크로 1회만 읽고 PHP strpos 로 대조한다 — 비용이 + // (테이블 크기 1회 + 행×잔여토큰 strpos) 로 고정되고, 토큰이 모두 발견되면 + // 조기 종료한다. + DB::table($table) + ->select(array_merge(['id'], $columns)) + ->lazyById(200) + ->each(function ($row) use ($columns, &$pending, &$found) { + foreach ($columns as $column) { + $content = (string) ($row->{$column} ?? ''); + + if ($content === '') { + continue; + } + + foreach ($pending as $index => $token) { + if (str_contains($content, $token)) { + $found[] = $token; + unset($pending[$index]); + } + } + } + + return $pending !== []; // 전 토큰 발견 시 순회 중단 + }); + + return $found; + } + + /** + * LIKE 와일드카드를 이스케이프합니다. + * + * 원본명·해시에 `%`/`_` 가 포함되면 임의 문자로 해석돼 무관한 본문이 매칭됩니다 + * (참조됨 오판 → 정리되지 않고 영구 잔존). + * + * @param string $value 원본 값 + * @return string 이스케이프된 값 + */ + private function escapeLike(string $value): string + { + return addcslashes($value, '\\%_'); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageUploadRepository.php b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageUploadRepository.php index 979a42f1..23a3c5e6 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageUploadRepository.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Repositories/ImageUploadRepository.php @@ -2,6 +2,12 @@ namespace Plugins\Sirsoft\Ckeditor5\Repositories; +use App\Repositories\Concerns\FiltersByDateRange; +use App\Repositories\Concerns\ResolvesSortSpec; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Support\Carbon; use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; @@ -10,6 +16,41 @@ use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterf */ class ImageUploadRepository implements ImageUploadRepositoryInterface { + use FiltersByDateRange; + use ResolvesSortSpec; + + /** + * 목록·정리 조회에서 읽는 컬럼 목록 + * + * 참조 판정(hash·file_path)·삭제(file_path·storage_disk)·화면 표시에 쓰이는 컬럼만 + * 읽습니다. 이 테이블에는 본문 성격의 넓은 컬럼이 없으나, 컬럼을 명시해 두면 이후 + * 컬럼이 늘어도 목록이 자동으로 넓어지지 않습니다. + * + * @var list<string> + */ + private const LIST_COLUMNS = [ + 'id', + 'hash', + 'original_name', + 'file_path', + 'storage_disk', + 'file_size', + 'mime_type', + 'uploaded_by', + 'created_at', + ]; + + /** + * 관리 화면 정렬 허용 컬럼 화이트리스트 + * + * @var list<string> + */ + private const SORTABLE_COLUMNS = [ + 'created_at', + 'file_size', + 'original_name', + ]; + public function __construct( protected Ckeditor5ImageUpload $model ) {} @@ -29,4 +70,125 @@ class ImageUploadRepository implements ImageUploadRepositoryInterface { return $this->model->create($data); } + + /** + * {@inheritDoc} + */ + public function findOlderThan(Carbon $threshold, int $limit): Collection + { + return $this->model->newQuery() + ->where('created_at', '<', $threshold) + ->orderBy('created_at') + ->orderBy('id') + ->limit($limit) + ->get(self::LIST_COLUMNS); + } + + /** + * {@inheritDoc} + */ + public function countOlderThan(Carbon $threshold): int + { + return $this->model->newQuery() + ->where('created_at', '<', $threshold) + ->count(); + } + + /** + * {@inheritDoc} + */ + public function paginateForAdmin(array $filters, int $perPage): LengthAwarePaginator + { + $sort = $this->resolveSortSpec( + $filters, + self::SORTABLE_COLUMNS, + defaultColumn: 'created_at', + defaultDirection: 'desc', + )[0]; + + $query = $this->applyFilters($this->model->newQuery(), $filters) + ->with('uploader:id,name') + ->orderBy($sort['column'], $sort['direction']); + + // 정렬 마지막에 기본키를 덧붙여 전순서를 보장한다 — 동률 구간에서 인접 페이지가 + // 같은 행을 중복 노출하거나 다른 행을 누락하는 것을 막는다. + if ($sort['column'] !== 'id') { + $query->orderBy('id', $sort['direction']); + } + + return $query->paginate($perPage, self::LIST_COLUMNS); + } + + /** + * {@inheritDoc} + */ + public function findScanWindow(array $filters, int $limit): Collection + { + return $this->applyFilters($this->model->newQuery(), $filters) + ->with('uploader:id,name') + ->orderByDesc('created_at') + ->orderByDesc('id') + ->limit($limit) + ->get(self::LIST_COLUMNS); + } + + /** + * {@inheritDoc} + */ + public function findManyByIds(array $ids): Collection + { + if ($ids === []) { + return new Collection; + } + + return $this->model->newQuery() + ->whereIn('id', $ids) + ->get(self::LIST_COLUMNS); + } + + /** + * {@inheritDoc} + */ + public function findById(int $id): ?Ckeditor5ImageUpload + { + return $this->model->newQuery()->find($id, self::LIST_COLUMNS); + } + + /** + * {@inheritDoc} + */ + public function delete(Ckeditor5ImageUpload $upload): bool + { + return (bool) $upload->delete(); + } + + /** + * 목록 필터를 쿼리에 적용합니다. + * + * @param Builder<Ckeditor5ImageUpload> $query 대상 쿼리 + * @param array $filters 필터 배열 + * @return Builder<Ckeditor5ImageUpload> 필터가 적용된 쿼리 + */ + private function applyFilters(Builder $query, array $filters): Builder + { + $search = trim((string) ($filters['search'] ?? '')); + + if ($search !== '') { + $escaped = addcslashes($search, '\\%_'); + + $query->where(function (Builder $inner) use ($escaped) { + $inner->where('original_name', 'like', '%'.$escaped.'%') + ->orWhere('hash', 'like', '%'.$escaped.'%'); + }); + } + + $this->applyDateRangeFilter( + $query, + 'created_at', + $filters['date_from'] ?? null, + $filters['date_to'] ?? null, + ); + + return $query; + } } diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageCleanupService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageCleanupService.php new file mode 100644 index 00000000..92dfce6b --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageCleanupService.php @@ -0,0 +1,198 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Services; + +use App\Contracts\Extension\StorageInterface; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Log; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; + +/** + * CKEditor5 미참조 업로드 이미지 정리 서비스 + * + * 관리 화면의 수동 삭제와 정리 커맨드가 같은 삭제 경로(deleteUpload)를 공유합니다. + * + * 삭제 순서는 "물리 파일 먼저, 레코드 나중" 입니다. 반대로 하면 레코드가 사라진 파일은 + * 어떤 경로로도 다시 찾을 수 없어 영구 고아가 됩니다. 파일이 이미 없는 행은 고아 레코드로 + * 보고 레코드만 지웁니다 — 그러지 않으면 매 회차 같은 행을 재시도하는 루프가 됩니다. + */ +class ImageCleanupService +{ + /** + * @param ImageUploadRepositoryInterface $repository 업로드 리포지토리 + * @param ImageReferenceScanService $scanner 참조 판정 서비스 + * @param StorageInterface $storage 플러그인 스토리지 드라이버 + */ + public function __construct( + protected ImageUploadRepositoryInterface $repository, + protected ImageReferenceScanService $scanner, + protected StorageInterface $storage, + ) {} + + /** + * 보존기간이 지난 미참조 이미지를 정리합니다. + * + * @param int $days 보존기간(일) + * @param int $limit 한 회차에 스캔할 최대 후보 수 + * @param bool $dryRun true 면 판정만 하고 삭제하지 않음 + * @return array{scanned: int, referenced: int, deleted: int, failed: int, items: array<int, array<string, mixed>>, skipped_reason?: string} + */ + public function pruneUnused(int $days, int $limit, bool $dryRun = false): array + { + // 비활성 모듈이 있으면 그 모듈이 등록했을 참조 소스가 이번 판정에 빠져 있을 수 + // 있다 — "미참조" 판정을 신뢰할 수 없으므로 실삭제 회차를 통째로 건너뛴다. + // (dry-run 은 삭제가 없으므로 판정 미리보기 용도로 계속 허용한다.) + if (! $dryRun && $this->scanner->hasPotentiallyMissingSources()) { + Log::warning('CKEditor5 미참조 정리 건너뜀 — 비활성 모듈이 있어 참조 소스가 불완전할 수 있습니다.'); + + return [ + 'scanned' => 0, + 'referenced' => 0, + 'deleted' => 0, + 'failed' => 0, + 'items' => [], + 'skipped_reason' => 'sources_incomplete', + ]; + } + + $threshold = Carbon::now()->subDays($days); + $candidates = $this->repository->findOlderThan($threshold, $limit); + + $result = [ + 'scanned' => $candidates->count(), + 'referenced' => 0, + 'deleted' => 0, + 'failed' => 0, + 'items' => [], + ]; + + // 행별 isReferenced 반복 대신 일괄 판정 — 소스당 1회 순회로 비용을 고정한다. + $referencedMap = $this->scanner->mapReferenced($candidates); + + foreach ($candidates as $upload) { + // 판정 누락 행은 안전 방향(참조됨)으로 본다. + if ($referencedMap[(int) $upload->id] ?? true) { + $result['referenced']++; + + continue; + } + + if ($dryRun) { + $result['items'][] = $this->describe($upload, 'would_delete'); + + continue; + } + + if ($this->deleteUpload($upload)) { + $result['deleted']++; + $result['items'][] = $this->describe($upload, 'deleted'); + + continue; + } + + $result['failed']++; + $result['items'][] = $this->describe($upload, 'failed'); + } + + return $result; + } + + /** + * 업로드 이미지 1건의 파일과 레코드를 삭제합니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @return bool 레코드까지 삭제되면 true, 파일 삭제 실패로 보류되면 false + */ + public function deleteUpload(Ckeditor5ImageUpload $upload): bool + { + [$category, $relativePath] = $this->splitFilePath((string) ($upload->file_path ?? '')); + + if ($category === null || $relativePath === null) { + // 경로를 분해할 수 없는 행은 가리키는 파일이 없다 — 레코드만 정리한다. + Log::warning('CKEditor5 업로드 경로를 분해할 수 없어 레코드만 삭제합니다.', [ + 'id' => $upload->id, + 'file_path' => $upload->file_path, + ]); + + return $this->repository->delete($upload); + } + + // 행 disk 가 더 이상 등록돼 있지 않으면(공급 플러그인 비활성 등) withDisk 가 + // InvalidArgumentException 을 던져 삭제/일괄삭제/prune 루프 전체가 500 으로 + // 중단된다 — ImageServeService::serve 와 동일하게 존재 검증 후 폴백한다. + $disk = (string) ($upload->storage_disk ?? ''); + $useRowDisk = $disk !== '' + && $disk !== $this->storage->getDisk() + && config("filesystems.disks.{$disk}") !== null; + + if ($disk !== '' && ! $useRowDisk && $disk !== $this->storage->getDisk()) { + Log::warning('CKEditor5 업로드의 저장 disk 가 미등록 상태 — 기본 스토리지로 폴백해 정리를 시도합니다.', [ + 'id' => $upload->id, + 'disk' => $disk, + ]); + } + + $storage = $useRowDisk ? $this->storage->withDisk($disk) : $this->storage; + + if (! $storage->exists($category, $relativePath)) { + // 파일이 이미 없는 고아 레코드 — 재시도 루프를 만들지 않도록 레코드를 정리한다. + return $this->repository->delete($upload); + } + + if (! $storage->delete($category, $relativePath)) { + Log::warning('CKEditor5 업로드 파일 삭제에 실패해 레코드를 보존합니다.', [ + 'id' => $upload->id, + 'disk' => $disk, + 'file_path' => $upload->file_path, + ]); + + return false; + } + + return $this->repository->delete($upload); + } + + /** + * 파일 경로를 카테고리와 상대 경로로 분해합니다. + * + * `file_path` 는 카테고리 prefix 를 포함합니다(`images/Y/m/d/{uuid}.{ext}`) — + * 모델의 `resolveDirectUrl()` 과 동일한 분해 규칙을 씁니다. + * + * @param string $filePath 저장 파일 경로 + * @return array{0: string|null, 1: string|null} [카테고리, 상대 경로] + */ + private function splitFilePath(string $filePath): array + { + if ($filePath === '' || ! str_contains($filePath, '/')) { + return [null, null]; + } + + [$category, $relativePath] = explode('/', $filePath, 2); + + if ($category === '' || $relativePath === '') { + return [null, null]; + } + + return [$category, $relativePath]; + } + + /** + * 커맨드 출력·로그용 항목 요약을 만듭니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @param string $status 처리 상태 (would_delete|deleted|failed) + * @return array<string, mixed> 요약 배열 + */ + private function describe(Ckeditor5ImageUpload $upload, string $status): array + { + return [ + 'id' => (int) $upload->id, + 'hash' => (string) $upload->hash, + 'original_name' => (string) $upload->original_name, + 'file_size' => (int) $upload->file_size, + 'created_at' => $upload->created_at?->toDateTimeString(), + 'status' => $status, + ]; + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageReferenceScanService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageReferenceScanService.php new file mode 100644 index 00000000..c6b8298c --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageReferenceScanService.php @@ -0,0 +1,285 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Services; + +use App\Contracts\Repositories\ModuleRepositoryInterface; +use App\Enums\ExtensionStatus; +use App\Extension\HookManager; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Log; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageReferenceSourceRepositoryInterface; + +/** + * CKEditor5 업로드 이미지의 참조 여부 판정 서비스 + * + * 에디터로 올린 이미지가 어떤 콘텐츠에서도 더 이상 쓰이지 않는지를 판정합니다. + * + * 판정 토큰이 둘인 이유: 본문에 박히는 URL 이 두 형태이기 때문입니다. + * - API 폴백형 `/api/plugins/sirsoft-ckeditor5/images/{12자hex}` → `hash` 가 들어간다 + * - 디스크 직접 URL(`url` 설정 디스크/CDN) → 저장 파일명(uuid.ext)이 들어간다 + * 한쪽만 검사하면 다른 형태로 저장된 이미지를 "미참조" 로 오판해 삭제하게 됩니다. + * `core.storage.filter_url` 훅으로 URL 이 변형될 수 있으므로 두 토큰을 OR 로 모두 검사합니다. + * + * 로그 사본 테이블(`notification_logs.body`, `mail_send_logs.body`, + * `boards_report_logs.snapshot`)과 미리보기(`template_layout_previews`)는 참조 소스에서 + * 제외합니다. 이들은 자체 보존기간으로 삭제되는 사본이라 소스로 삼으면 "로그가 지워지는 + * 순간 이미지가 고아가 되는" 역전이 생깁니다. + */ +class ImageReferenceScanService +{ + /** + * 참조 소스 확장 훅 이름 + */ + public const FILTER_REFERENCE_SOURCES = 'sirsoft-ckeditor5.image.filter_reference_sources'; + + /** + * 코어가 소유한 참조 소스 (테이블명은 프리픽스 제외 원시 이름) + * + * 모듈 콘텐츠(게시글·페이지·상품 설명 등)는 각 모듈이 훅으로 등록합니다. + * + * @var list<array{table: string, columns: list<string>}> + */ + private const CORE_SOURCES = [ + ['table' => 'notification_templates', 'columns' => ['body']], + ['table' => 'mail_templates', 'columns' => ['body']], + ['table' => 'identity_message_templates', 'columns' => ['body']], + ['table' => 'template_layouts', 'columns' => ['content']], + ['table' => 'template_layout_extensions', 'columns' => ['content']], + ['table' => 'template_layout_versions', 'columns' => ['content']], + ]; + + /** + * 요청 수명 memoize 된 검증 완료 소스 목록 + * + * @var list<array{table: string, columns: list<string>}>|null + */ + private ?array $resolvedSources = null; + + /** + * @param ImageReferenceSourceRepositoryInterface $sourceRepository 참조 소스 조회 리포지토리 + * @param ModuleRepositoryInterface $moduleRepository 모듈 상태 조회 리포지토리 (소스 불완전 감지) + */ + public function __construct( + protected ImageReferenceSourceRepositoryInterface $sourceRepository, + protected ModuleRepositoryInterface $moduleRepository + ) {} + + /** + * 참조 소스가 불완전할 수 있는 상태인지 판정합니다. + * + * 참조 소스는 **활성** 모듈이 훅으로 등록한다 — 설치돼 있으나 비활성인 모듈이 있으면 + * 그 모듈 콘텐츠(게시글 본문 등)가 이번 판정에 빠져, 실제로 쓰이는 이미지가 + * "미참조" 로 오판된다(fail-open). 삭제(uninstalled)는 콘텐츠도 함께 정리되는 + * 상태이므로 대상이 아니다. + * + * @return bool 비활성 설치 모듈이 하나라도 있으면 true + */ + public function hasPotentiallyMissingSources(): bool + { + // audit:allow query-unbounded-get reason: modules 는 설치된 확장 수에 묶인 설정성 테이블 — 행 수가 데이터 증가에 비례하지 않는다 + return $this->moduleRepository->getAll() + ->contains(fn ($module) => ! $module->isActive() + && $module->status !== ExtensionStatus::Uninstalled->value); + } + + /** + * 참조 스캔 대상 소스 목록을 반환합니다. + * + * 확장이 훅으로 추가한 선언은 테이블·컬럼 실재 여부를 검증하고, 불량 선언은 + * 경고만 남기고 건너뜁니다 — 한 확장의 잘못된 선언이 판정 전체를 멈추지 않게 합니다. + * + * @return list<array{table: string, columns: list<string>}> 검증된 소스 목록 + */ + public function getReferenceSources(): array + { + if ($this->resolvedSources !== null) { + return $this->resolvedSources; + } + + $declared = HookManager::applyFilters(self::FILTER_REFERENCE_SOURCES, self::CORE_SOURCES); + + if (! is_array($declared)) { + Log::warning('CKEditor5 참조 소스 훅이 배열이 아닌 값을 반환해 코어 기본값을 사용합니다.'); + + $declared = self::CORE_SOURCES; + } + + return $this->resolvedSources = $this->validateSources($declared); + } + + /** + * 업로드 이미지가 어떤 콘텐츠에서든 참조되는지 판정합니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @return bool 참조되면 true + */ + public function isReferenced(Ckeditor5ImageUpload $upload): bool + { + $tokens = $this->buildTokens($upload); + + if ($tokens === []) { + // 판정 토큰을 만들 수 없는 행은 안전 방향(참조됨)으로 본다. + return true; + } + + foreach ($this->getReferenceSources() as $source) { + if ($this->sourceContains($source, $tokens)) { + return true; + } + } + + return false; + } + + /** + * 업로드 목록의 참조 여부를 일괄 판정합니다. + * + * 행별로 isReferenced 를 반복하면 비용이 (행 수 × 소스 수 × LIKE 전체 스캔) 으로 + * 커진다 — 여기서는 전 행의 토큰을 모아 소스당 1회 순회로 등장 토큰을 수집한다 + * (비용 상한: 소스 크기 합 1회 + 조기 종료). + * + * @param Collection<int, Ckeditor5ImageUpload>|iterable<Ckeditor5ImageUpload> $uploads 업로드 목록 + * @return array<int, bool> [업로드 ID => 참조 여부] + */ + public function mapReferenced(iterable $uploads): array + { + $map = []; + $tokenOwners = []; + + foreach ($uploads as $upload) { + $id = (int) $upload->id; + $tokens = $this->buildTokens($upload); + + if ($tokens === []) { + // 판정 토큰을 만들 수 없는 행은 안전 방향(참조됨)으로 본다. + $map[$id] = true; + + continue; + } + + $map[$id] = false; + + foreach ($tokens as $token) { + $tokenOwners[$token][] = $id; + } + } + + if ($tokenOwners === []) { + return $map; + } + + $pending = $tokenOwners; + + foreach ($this->getReferenceSources() as $source) { + if ($pending === []) { + break; + } + + // PHP 배열 키는 순수 숫자 토큰을 int 로 접으므로 문자열로 복원해 넘긴다. + $foundTokens = $this->sourceRepository->findTokensInSource( + $source['table'], + $source['columns'], + array_map('strval', array_keys($pending)) + ); + + foreach ($foundTokens as $token) { + foreach ($pending[$token] ?? [] as $id) { + $map[$id] = true; + } + + unset($pending[$token]); + } + } + + return $map; + } + + /** + * 판정 토큰(해시 + 저장 파일명)을 만듭니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @return list<string> 토큰 목록 + */ + private function buildTokens(Ckeditor5ImageUpload $upload): array + { + $tokens = []; + + $hash = trim((string) ($upload->hash ?? '')); + if ($hash !== '') { + $tokens[] = $hash; + } + + $filePath = trim((string) ($upload->file_path ?? '')); + if ($filePath !== '') { + $basename = basename($filePath); + + if ($basename !== '' && ! in_array($basename, $tokens, true)) { + $tokens[] = $basename; + } + } + + return $tokens; + } + + /** + * 한 소스에 토큰이 하나라도 등장하는지 확인합니다. + * + * @param array{table: string, columns: list<string>} $source 참조 소스 + * @param list<string> $tokens 판정 토큰 + * @return bool 등장하면 true + */ + private function sourceContains(array $source, array $tokens): bool + { + return $this->sourceRepository->containsAnyToken($source['table'], $source['columns'], $tokens); + } + + /** + * 훅으로 수집한 소스 선언을 검증합니다. + * + * @param array $declared 선언 목록 + * @return list<array{table: string, columns: list<string>}> 검증 통과 소스 + */ + private function validateSources(array $declared): array + { + $valid = []; + + foreach ($declared as $source) { + if (! is_array($source) || ! isset($source['table'], $source['columns'])) { + Log::warning('CKEditor5 참조 소스 선언 형식이 올바르지 않아 건너뜁니다.', [ + 'source' => $source, + ]); + + continue; + } + + $table = (string) $source['table']; + $columns = is_array($source['columns']) ? array_values(array_map('strval', $source['columns'])) : []; + + if ($table === '' || $columns === []) { + Log::warning('CKEditor5 참조 소스 선언에 테이블 또는 컬럼이 비어 있어 건너뜁니다.', [ + 'table' => $table, + ]); + + continue; + } + + $existingColumns = $this->sourceRepository->resolveExistingColumns($table, $columns); + + if ($existingColumns === []) { + // 테이블 자체가 없거나(확장 미설치) 선언한 컬럼이 하나도 없는 경우다. + // 한 확장의 잘못된 선언이 판정 전체를 멈추지 않도록 경고만 남기고 건너뛴다. + Log::warning('CKEditor5 참조 소스에 존재하는 테이블/컬럼이 없어 건너뜁니다.', [ + 'table' => $table, + 'columns' => $columns, + ]); + + continue; + } + + $valid[] = ['table' => $table, 'columns' => $existingColumns]; + } + + return $valid; + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageServeService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageServeService.php index c8c0252f..37e4287c 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageServeService.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageServeService.php @@ -43,27 +43,41 @@ class ImageServeService */ public function serve(Ckeditor5ImageUpload $image): ?StreamedResponse { - // file_path 형태: "images/2026/04/06/{uuid}.jpg" - // PluginStorageDriver::response(category, path) 에서 category = 'images' - $category = 'images'; - $relativePath = substr($image->file_path, strlen($category) + 1); + // file_path 형태: "images/2026/04/06/{uuid}.jpg" (외부 생성 행은 "ckeditor5/..." 등) + // 첫 세그먼트 = 카테고리 일반형으로 분해 — PluginStorageDriver::response(category, path) + [$category, $relativePath] = array_pad(explode('/', $image->file_path, 2), 2, ''); - $response = $this->storage->response( + if ($category === '' || $relativePath === '') { + return null; + } + + // 행에 기록된 disk 기준 서빙 — 디스크 전환 이전 행도 실제 저장 위치를 향한다. + // 단 고아 disk(디스크를 제공하던 플러그인 비활성화로 config 에서 사라진 경우)는 + // 미등록 disk 로 withDisk 를 만들면 response 가 InvalidArgumentException 을 던져 + // 서빙이 500 이 되므로, 존재 검증 후 주입 스토리지로 폴백한다. + $rowDisk = (string) ($image->storage_disk ?? ''); + $useRowDisk = $rowDisk !== '' + && $rowDisk !== $this->storage->getDisk() + && config("filesystems.disks.{$rowDisk}") !== null; + + $storage = $useRowDisk ? $this->storage->withDisk($rowDisk) : $this->storage; + + $response = $storage->response( $category, $relativePath, $image->original_name, [ - 'Content-Type' => $image->mime_type, + 'Content-Type' => $image->mime_type, 'Cache-Control' => 'public, max-age=31536000', ] ); if (! $response) { Log::error('CKEditor5 이미지 스토리지에 없음', [ - 'image_id' => $image->id, - 'hash' => $image->hash, + 'image_id' => $image->id, + 'hash' => $image->hash, 'file_path' => $image->file_path, - 'disk' => $image->storage_disk, + 'disk' => $image->storage_disk, ]); return null; diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadAdminService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadAdminService.php new file mode 100644 index 00000000..68dd35e8 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadAdminService.php @@ -0,0 +1,184 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Services; + +use Illuminate\Support\Collection; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; + +/** + * CKEditor5 업로드 이미지 관리 화면 서비스 + * + * 참조 여부는 컬럼에 저장하지 않고 조회할 때마다 판정합니다. 참조 관계는 콘텐츠가 + * 편집될 때마다 바뀌므로 스냅샷을 저장하면 "저장 당시엔 미참조였던" 이미지를 이후에 + * 오판 삭제할 수 있기 때문입니다. 대신 판정 비용에 상한을 둡니다. + * + * - 기본 목록: 현재 페이지(≤100행)만 판정 — 페이지 크기가 곧 비용 상한 + * - 참조 상태 필터: 최신순 스캔 윈도우(SCAN_WINDOW) 안에서만 판정하고, 상한에 걸린 + * 사실을 응답 메타(`scan_limited`)로 알려 화면이 "최근 N건 기준" 임을 표시하게 한다 + */ +class ImageUploadAdminService +{ + /** + * 참조 상태 필터를 적용할 때 훑는 최신 업로드 건수 상한 + */ + public const SCAN_WINDOW = 500; + + /** + * @param ImageUploadRepositoryInterface $repository 업로드 리포지토리 + * @param ImageReferenceScanService $scanner 참조 판정 서비스 + * @param ImageCleanupService $cleanupService 삭제 서비스 (커맨드와 동일 경로 공유) + */ + public function __construct( + protected ImageUploadRepositoryInterface $repository, + protected ImageReferenceScanService $scanner, + protected ImageCleanupService $cleanupService, + ) {} + + /** + * 관리 화면 목록을 조회합니다. + * + * @param array $filters 필터 (search / referenced / date_from / date_to / sort_by / sort_order) + * @param int $perPage 페이지 크기 + * @param int $page 페이지 번호 + * @return array{items: Collection<int, Ckeditor5ImageUpload>, pagination: array<string, mixed>, scan_limited: bool} + */ + public function paginate(array $filters, int $perPage, int $page = 1): array + { + $referencedFilter = (string) ($filters['referenced'] ?? 'all'); + + if ($referencedFilter === 'referenced' || $referencedFilter === 'unreferenced') { + return $this->paginateByReferenceState($filters, $perPage, $page, $referencedFilter === 'referenced'); + } + + $paginator = $this->repository->paginateForAdmin($filters, $perPage); + + /** @var Collection<int, Ckeditor5ImageUpload> $items */ + $items = collect($paginator->items()); + $this->attachReferenceState($items); + + return [ + 'items' => $items, + 'pagination' => [ + 'current_page' => $paginator->currentPage(), + 'last_page' => $paginator->lastPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + 'from' => $paginator->firstItem(), + 'to' => $paginator->lastItem(), + ], + 'scan_limited' => false, + 'reference_sources_incomplete' => false, + ]; + } + + /** + * 업로드 이미지 1건을 삭제합니다. + * + * @param Ckeditor5ImageUpload $upload 업로드 기록 + * @return bool 삭제 성공 여부 (파일 삭제 실패 시 false) + */ + public function delete(Ckeditor5ImageUpload $upload): bool + { + return $this->cleanupService->deleteUpload($upload); + } + + /** + * 업로드 이미지를 일괄 삭제합니다. + * + * @param array<int, int> $ids 업로드 ID 목록 + * @return array{deleted: int, failed: int} 처리 결과 + */ + public function bulkDelete(array $ids): array + { + $uploads = $this->repository->findManyByIds($ids); + + $deleted = 0; + $failed = 0; + + foreach ($uploads as $upload) { + $this->cleanupService->deleteUpload($upload) ? $deleted++ : $failed++; + } + + return ['deleted' => $deleted, 'failed' => $failed]; + } + + /** + * ID 로 업로드를 조회합니다. + * + * @param int $id 업로드 ID + * @return Ckeditor5ImageUpload|null + */ + public function find(int $id): ?Ckeditor5ImageUpload + { + return $this->repository->findById($id); + } + + /** + * 참조 상태 필터가 걸린 목록을 스캔 윈도우 안에서 페이지네이션합니다. + * + * @param array $filters 필터 배열 + * @param int $perPage 페이지 크기 + * @param int $page 페이지 번호 + * @param bool $wantReferenced true 면 참조됨, false 면 미참조만 + * @return array{items: Collection<int, Ckeditor5ImageUpload>, pagination: array<string, mixed>, scan_limited: bool} + */ + private function paginateByReferenceState(array $filters, int $perPage, int $page, bool $wantReferenced): array + { + $window = $this->repository->findScanWindow($filters, self::SCAN_WINDOW); + $scanLimited = $window->count() >= self::SCAN_WINDOW; + + $this->attachReferenceState($window); + + $matched = $window + ->filter(fn (Ckeditor5ImageUpload $upload) => (bool) $upload->referenced === $wantReferenced) + ->values(); + + // 스캔 윈도우는 최신순 고정으로 확보하지만, 화면이 요청한 정렬(검증 완료 값)은 + // 윈도우 안에서 그대로 적용한다 — 무시하면 "파일 크기순 + 미참조" 조합이 + // 오류 없이 최신순으로만 나와 정렬이 동작하는 것처럼 보인다. + $sortBy = (string) ($filters['sort_by'] ?? 'created_at'); + $sortDesc = strtolower((string) ($filters['sort_order'] ?? 'desc')) !== 'asc'; + $matched = $matched + ->sortBy([[$sortBy, $sortDesc ? 'desc' : 'asc'], ['id', 'desc']]) + ->values(); + + $total = $matched->count(); + $lastPage = max(1, (int) ceil($total / max(1, $perPage))); + $currentPage = max(1, min($page, $lastPage)); + $items = $matched->slice(($currentPage - 1) * $perPage, $perPage)->values(); + + return [ + 'items' => $items, + 'pagination' => [ + 'current_page' => $currentPage, + 'last_page' => $lastPage, + 'per_page' => $perPage, + 'total' => $total, + 'from' => $items->isEmpty() ? null : ($currentPage - 1) * $perPage + 1, + 'to' => $items->isEmpty() ? null : ($currentPage - 1) * $perPage + $items->count(), + ], + 'scan_limited' => $scanLimited, + // 비활성 모듈이 있으면 그 모듈의 콘텐츠가 판정에서 빠져 "미참조" 가 + // 신뢰 불가다 — 화면이 경고를 표시할 수 있도록 메타로 알린다. + 'reference_sources_incomplete' => $this->scanner->hasPotentiallyMissingSources(), + ]; + } + + /** + * 업로드 목록에 참조 여부를 임시 속성으로 부착합니다. + * + * 저장 컬럼이 아니라 응답 전용 값이라 모델에 setAttribute 로만 실어 보냅니다 + * (저장 호출 없음). + * + * @param Collection<int, Ckeditor5ImageUpload> $uploads 업로드 목록 + */ + private function attachReferenceState(Collection $uploads): void + { + $map = $this->scanner->mapReferenced($uploads); + + foreach ($uploads as $upload) { + $upload->setAttribute('referenced', $map[(int) $upload->id] ?? true); + } + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadService.php b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadService.php index cb81871a..5713a598 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadService.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/Services/ImageUploadService.php @@ -6,6 +6,7 @@ use App\Contracts\Extension\StorageInterface; use App\Extension\HookManager; use App\Support\ImageResizer; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; @@ -38,6 +39,18 @@ class ImageUploadService // 훅: 이미지 업로드 전 (본인인증 등 확장 지점) HookManager::doAction('sirsoft-ckeditor5.image.before_upload', $file, $uploadedBy); + // 필터 훅 - 파일 데이터 변형 (압축, 리사이즈 등 확장 포인트). + // 잘못 작성된 리스너가 UploadedFile 이 아닌 값을 반환하면 원본으로 폴백한다 — + // 확장 결함이 코어 업로드 전면 장애(TypeError → 500)로 위장되면 안 된다. + $filtered = HookManager::applyFilters('sirsoft-ckeditor5.image.filter_upload_file', $file); + if ($filtered instanceof UploadedFile) { + $file = $filtered; + } else { + Log::warning('sirsoft-ckeditor5.image.filter_upload_file 필터가 UploadedFile 이 아닌 값을 반환해 원본을 사용합니다.', [ + 'returned_type' => get_debug_type($filtered), + ]); + } + $extension = $file->guessExtension() ?? $file->getClientOriginalExtension(); $storedFilename = Str::uuid().'.'.$extension; $path = date('Y/m/d').'/'.$storedFilename; diff --git a/plugins/_bundled/sirsoft-ckeditor5/src/routes/api.php b/plugins/_bundled/sirsoft-ckeditor5/src/routes/api.php index 436741a9..8fea4839 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/src/routes/api.php +++ b/plugins/_bundled/sirsoft-ckeditor5/src/routes/api.php @@ -1,6 +1,7 @@ <?php use Illuminate\Support\Facades\Route; +use Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin\ImageUploadAdminController; use Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageServeController; use Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageUploadController; @@ -21,3 +22,23 @@ Route::post('upload', [ImageUploadController::class, 'upload']) Route::get('images/{hash}', [ImageServeController::class, 'serve']) ->where('hash', '[a-f0-9]{12}') ->name('api.sirsoft-ckeditor5.images.serve'); + +/* +| 업로드 이미지 관리 (관리자) +| +| 조회와 삭제를 별도 권한으로 분리한다 — 삭제는 파일을 실제로 파기하기 때문이다. +*/ +Route::prefix('admin')->name('admin.')->middleware('auth:sanctum')->group(function () { + Route::get('uploads', [ImageUploadAdminController::class, 'index']) + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.read') + ->name('uploads.index'); + + Route::post('uploads/bulk-delete', [ImageUploadAdminController::class, 'bulkDestroy']) + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.delete') + ->name('uploads.bulk-delete'); + + Route::delete('uploads/{id}', [ImageUploadAdminController::class, 'destroy']) + ->whereNumber('id') + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.delete') + ->name('uploads.destroy'); +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Console/PruneUnusedImagesCommandTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Console/PruneUnusedImagesCommandTest.php new file mode 100644 index 00000000..52cdd67e --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Console/PruneUnusedImagesCommandTest.php @@ -0,0 +1,225 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Feature\Console; + +use App\Extension\Storage\PluginStorageDriver; +use App\Services\PluginSettingsService; +use Illuminate\Support\Facades\Log; +use Mockery; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 미참조 이미지 정리 커맨드 테스트 + * + * 인수 기준은 "설정을 만지지 않은 사이트에서 자동 삭제 0" 입니다. 스케줄 게이트가 + * 설정 조회 실패 시 true 로 폴백하므로, 커맨드가 `--scheduled` 에서 false 폴백으로 + * 재확인하는지를 회귀로 고정합니다. + */ +class PruneUnusedImagesCommandTest extends PluginTestCase +{ + /** + * 플러그인 설정 조회를 지정 값으로 고정합니다. + * + * 테스트 환경에는 플러그인이 설치돼 있지 않아 PluginSettingsService 가 기본값을 + * 그대로 돌려주므로, 설정 분기를 밟으려면 조회 지점을 대체해야 합니다. + * + * @param array<string, mixed> $values 설정 키 => 값 + */ + private function fakePluginSettings(array $values): void + { + $service = Mockery::mock(PluginSettingsService::class); + + $service->shouldReceive('get') + ->andReturnUsing(function (string $identifier, ?string $key = null, mixed $default = null) use ($values) { + return array_key_exists($key, $values) ? $values[$key] : $default; + }); + + $this->app->instance(PluginSettingsService::class, $service); + } + + /** + * 미참조 업로드 1건(파일 포함)을 만듭니다. + * + * @param int $ageDays 업로드 경과 일수 + * @return Ckeditor5ImageUpload 생성된 기록 + */ + private function makeStaleUpload(int $ageDays = 40): Ckeditor5ImageUpload + { + $filename = uniqid('cmd-').'.png'; + (new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'))->put('images', "2026/08/14/{$filename}", 'bytes'); + + $upload = Ckeditor5ImageUpload::create([ + 'original_name' => $filename, + 'file_path' => "images/2026/08/14/{$filename}", + 'storage_disk' => 'plugins', + 'file_size' => 5, + 'mime_type' => 'image/png', + ]); + + $upload->forceFill(['created_at' => now()->subDays($ageDays)])->saveQuietly(); + + return $upload->refresh(); + } + + /** + * 설정을 만지지 않은 사이트(설정 키 부재) 에서 스케줄 호출은 아무것도 지우지 않아야 한다. + * + * @effects schedule_gate_off_by_default + */ + #[Test] + public function scheduled_run_deletes_nothing_when_the_setting_was_never_touched(): void + { + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled') + ->assertSuccessful(); + + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects schedule_gate_off_explicit + */ + #[Test] + public function scheduled_run_stops_early_when_the_toggle_is_off(): void + { + $this->fakePluginSettings(['unusedImageCleanup' => false]); + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled') + ->assertSuccessful(); + + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects schedule_gate_on + */ + #[Test] + public function scheduled_run_prunes_when_the_toggle_is_on(): void + { + $this->fakePluginSettings([ + 'unusedImageCleanup' => true, + 'unusedImageRetentionDays' => 30, + ]); + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled') + ->assertSuccessful(); + + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects retention_disabled_guard + */ + #[Test] + public function retention_below_one_day_performs_no_cleanup(): void + { + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --days=0') + ->assertSuccessful(); + + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects days_option_overrides_setting + */ + #[Test] + public function days_option_overrides_the_configured_retention(): void + { + $this->fakePluginSettings(['unusedImageRetentionDays' => 365]); + $upload = $this->makeStaleUpload(ageDays: 40); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --days=7') + ->assertSuccessful(); + + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects manual_run_dry_run_no_delete + */ + #[Test] + public function dry_run_lists_targets_without_deleting(): void + { + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --dry-run') + ->expectsOutputToContain('[DRY RUN]') + ->assertSuccessful(); + + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * 수동 실행(스케줄 아님)은 토글과 무관하게 운영자 의사이므로 그대로 수행한다. + * + * @effects manual_run_ignores_toggle + */ + #[Test] + public function manual_run_prunes_regardless_of_the_schedule_toggle(): void + { + $this->fakePluginSettings(['unusedImageCleanup' => false, 'unusedImageRetentionDays' => 30]); + $upload = $this->makeStaleUpload(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images') + ->assertSuccessful(); + + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * 실행 결과는 로그에 남긴다. + * + * 스케줄 실행은 아무도 보고 있지 않으므로, 무엇이 몇 건 지워졌는지가 로그에 남지 않으면 + * 사후에 확인할 방법이 없다. + * + * @effects prune_logs_run_summary + */ + #[Test] + public function a_real_run_records_its_summary_in_the_log(): void + { + $this->fakePluginSettings(['unusedImageCleanup' => true, 'unusedImageRetentionDays' => 30]); + $this->makeStaleUpload(); + + Log::shouldReceive('info') + ->once() + ->with('PruneUnusedImagesCommand: 미참조 에디터 이미지 정리 완료', Mockery::on(function (array $context): bool { + return ($context['deleted'] ?? null) === 1 + && ($context['failed'] ?? null) === 0 + && ($context['days'] ?? null) === 30; + })); + + Log::shouldReceive('warning')->zeroOrMoreTimes(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --scheduled') + ->assertSuccessful(); + } + + /** + * dry-run 은 아무것도 지우지 않으므로 완료 로그도 남기지 않는다. + * + * @effects prune_dry_run_no_delete + */ + #[Test] + public function a_dry_run_does_not_record_a_completion_log(): void + { + $this->fakePluginSettings(['unusedImageRetentionDays' => 30]); + $this->makeStaleUpload(); + + Log::shouldReceive('info') + ->with('PruneUnusedImagesCommand: 미참조 에디터 이미지 정리 완료', Mockery::any()) + ->never(); + + Log::shouldReceive('info')->zeroOrMoreTimes(); + Log::shouldReceive('warning')->zeroOrMoreTimes(); + + $this->artisan('sirsoft-ckeditor5:prune-unused-images --dry-run') + ->assertSuccessful(); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/Admin/ImageUploadAdminControllerTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/Admin/ImageUploadAdminControllerTest.php new file mode 100644 index 00000000..4bcbdc2c --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/Admin/ImageUploadAdminControllerTest.php @@ -0,0 +1,335 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Feature\Http\Controllers\Admin; + +use App\Enums\PermissionType; +use App\Extension\Storage\PluginStorageDriver; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadAdminService; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 업로드 이미지 관리 API 테스트 + * + * 조회·삭제 권한이 분리돼 있으므로 권한 경계(403)와 게스트(401)를 함께 고정합니다. + */ +class ImageUploadAdminControllerTest extends PluginTestCase +{ + private const INDEX_URL = '/api/plugins/sirsoft-ckeditor5/admin/uploads'; + + /** + * 지정한 업로드 권한만 가진 사용자를 만듭니다. + * + * @param array<int, string> $actions 부여할 액션 (read|delete) + * @return User 생성된 사용자 + */ + private function userWithUploadPermissions(array $actions): User + { + $role = Role::firstOrCreate( + ['identifier' => 'ckeditor5-uploads-test-'.implode('-', $actions ?: ['none'])], + ['name' => ['ko' => '테스트 역할', 'en' => 'Test Role'], 'description' => ['ko' => '', 'en' => '']] + ); + + $adminAccess = Permission::firstOrCreate( + ['identifier' => 'admin.access'], + ['name' => ['ko' => '관리자 접근', 'en' => 'Admin Access'], 'type' => PermissionType::Admin] + ); + + $permissionIds = [$adminAccess->id]; + + foreach ($actions as $action) { + $permissionIds[] = Permission::firstOrCreate( + ['identifier' => "sirsoft-ckeditor5.uploads.{$action}"], + ['name' => ['ko' => $action, 'en' => $action], 'type' => PermissionType::Admin] + )->id; + } + + $role->permissions()->syncWithoutDetaching($permissionIds); + + $user = User::factory()->create(); + $user->roles()->attach($role->id); + + return $user; + } + + /** + * 파일과 기록을 함께 만듭니다. + * + * @param string|null $hash 이미지 해시 (null 이면 자동 생성) + * @return Ckeditor5ImageUpload 생성된 기록 + */ + private function makeUpload(?string $hash = null): Ckeditor5ImageUpload + { + $filename = uniqid('admin-').'.png'; + (new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'))->put('images', "2026/08/14/{$filename}", 'bytes'); + + return Ckeditor5ImageUpload::create(array_filter([ + 'hash' => $hash, + 'original_name' => $filename, + 'file_path' => "images/2026/08/14/{$filename}", + 'storage_disk' => 'plugins', + 'file_size' => 5, + 'mime_type' => 'image/png', + ])); + } + + /** + * @effects admin_index_lists_uploads + */ + #[Test] + public function index_returns_uploads_with_reference_state_and_pagination(): void + { + $upload = $this->makeUpload('aaaaaaaaaaaa'); + + $response = $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL); + + $response->assertOk() + ->assertJsonPath('data.pagination.total', 1) + ->assertJsonPath('data.meta.scan_limited', false) + ->assertJsonPath('data.data.0.id', $upload->id) + ->assertJsonPath('data.data.0.referenced', false); + } + + /** + * 목록 응답은 화면이 실제로 그리는 필드만 싣는다. + * + * 저장 경로(`file_path`)·디스크(`storage_disk`)는 화면이 쓰지 않는 내부 값이라 노출하지 + * 않는다. 조회도 같은 폭이어야 프루닝이 의미를 갖는다. + * + * @effects admin_index_column_pruning + */ + #[Test] + public function index_exposes_only_the_declared_columns(): void + { + $this->makeUpload('a1a1a1a1a1a1'); + + $response = $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL); + + $response->assertOk() + ->assertJsonStructure([ + 'data' => [ + 'data' => [ + ['id', 'hash', 'original_name', 'file_size', 'mime_type', 'created_at', 'download_url', 'referenced'], + ], + ], + ]); + + $row = $response->json('data.data.0'); + + $this->assertArrayNotHasKey('file_path', $row); + $this->assertArrayNotHasKey('storage_disk', $row); + } + + /** + * @effects admin_index_referenced_badge + */ + #[Test] + public function referenced_upload_is_reported_as_referenced(): void + { + $upload = $this->makeUpload('bbbbbbbbbbbb'); + + DB::table('mail_templates')->insert([ + 'type' => 'admin-list-test', + 'subject' => json_encode(['ko' => '목록 테스트']), + 'body' => '<img src="/api/plugins/sirsoft-ckeditor5/images/bbbbbbbbbbbb">', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL) + ->assertOk() + ->assertJsonPath('data.data.0.id', $upload->id) + ->assertJsonPath('data.data.0.referenced', true); + } + + /** + * @effects admin_index_unreferenced_filter + */ + #[Test] + public function unreferenced_filter_returns_only_unreferenced_rows(): void + { + $this->makeUpload('cccccccccccc'); + $referenced = $this->makeUpload('dddddddddddd'); + + DB::table('mail_templates')->insert([ + 'type' => 'admin-filter-test', + 'subject' => json_encode(['ko' => '필터 테스트']), + 'body' => '<img src="/api/plugins/sirsoft-ckeditor5/images/dddddddddddd">', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL.'?referenced=unreferenced'); + + $response->assertOk()->assertJsonPath('data.pagination.total', 1); + $this->assertSame('cccccccccccc', $response->json('data.data.0.hash')); + $this->assertNotSame($referenced->id, $response->json('data.data.0.id')); + } + + /** + * 참조 필터는 최신 N건만 훑는다. 그 상한에 걸리면 목록이 전수가 아니라는 사실을 + * 응답이 스스로 알려야 한다. + * + * `scan_limited` 가 false 로 굳으면 화면은 "최근 500건 기준" 안내를 띄우지 않고, + * 운영자는 윈도우 밖 이미지를 "이미 정리된 것" 으로 읽는다 — 오류도 경고도 남지 않는 + * 조용한 오해라 응답 메타가 유일한 통로다. + * + * @effects admin_index_scan_window_limited + */ + #[Test] + public function reference_filter_reports_scan_limited_when_the_window_is_full(): void + { + $rows = []; + + for ($i = 0; $i < ImageUploadAdminService::SCAN_WINDOW; $i++) { + $filename = sprintf('window-%04d.png', $i); + + $rows[] = [ + 'hash' => sprintf('%012d', $i), + 'original_name' => $filename, + 'file_path' => "images/2026/08/14/{$filename}", + 'storage_disk' => 'plugins', + 'file_size' => 5, + 'mime_type' => 'image/png', + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + // 파일 실체는 목록 판정에 관여하지 않는다 — 윈도우 상한만 재현하면 되므로 일괄 삽입한다. + DB::table((new Ckeditor5ImageUpload)->getTable())->insert($rows); + + $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL.'?referenced=unreferenced&per_page=1') + ->assertOk() + ->assertJsonPath('data.meta.scan_limited', true) + ->assertJsonPath('data.meta.scan_window', ImageUploadAdminService::SCAN_WINDOW); + } + + /** + * @effects admin_index_rejects_unknown_sort + */ + #[Test] + public function unknown_sort_column_is_rejected(): void + { + $this->actingAs($this->userWithUploadPermissions(['read'])) + ->getJson(self::INDEX_URL.'?sort_by=file_path') + ->assertStatus(422); + } + + /** + * @effects admin_index_requires_read_permission + */ + #[Test] + public function index_is_forbidden_without_the_read_permission(): void + { + $this->actingAs($this->userWithUploadPermissions([])) + ->getJson(self::INDEX_URL) + ->assertStatus(403); + } + + /** + * @effects admin_index_requires_authentication + */ + #[Test] + public function index_is_unauthorized_for_guests(): void + { + $this->getJson(self::INDEX_URL)->assertStatus(401); + } + + /** + * @effects admin_destroy_removes_file_and_record + */ + #[Test] + public function destroy_removes_both_the_file_and_the_record(): void + { + $upload = $this->makeUpload(); + $relativePath = substr($upload->file_path, strlen('images/')); + + $this->actingAs($this->userWithUploadPermissions(['read', 'delete'])) + ->deleteJson(self::INDEX_URL.'/'.$upload->id) + ->assertOk(); + + $this->assertFalse( + (new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'))->exists('images', $relativePath) + ); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects admin_destroy_not_found + */ + #[Test] + public function destroy_returns_not_found_for_a_missing_id(): void + { + $this->actingAs($this->userWithUploadPermissions(['read', 'delete'])) + ->deleteJson(self::INDEX_URL.'/999999') + ->assertStatus(404); + } + + /** + * @effects admin_destroy_requires_delete_permission + */ + #[Test] + public function destroy_is_forbidden_with_read_permission_only(): void + { + $upload = $this->makeUpload(); + + $this->actingAs($this->userWithUploadPermissions(['read'])) + ->deleteJson(self::INDEX_URL.'/'.$upload->id) + ->assertStatus(403); + + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects admin_bulk_delete_removes_selected + */ + #[Test] + public function bulk_delete_removes_every_selected_upload(): void + { + $first = $this->makeUpload(); + $second = $this->makeUpload(); + + $this->actingAs($this->userWithUploadPermissions(['read', 'delete'])) + ->postJson(self::INDEX_URL.'/bulk-delete', ['ids' => [$first->id, $second->id]]) + ->assertOk() + ->assertJsonPath('data.deleted', 2); + + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $first->id]); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $second->id]); + } + + /** + * @effects admin_bulk_delete_rejects_empty + */ + #[Test] + public function bulk_delete_rejects_an_empty_selection(): void + { + $this->actingAs($this->userWithUploadPermissions(['read', 'delete'])) + ->postJson(self::INDEX_URL.'/bulk-delete', ['ids' => []]) + ->assertStatus(422) + ->assertJsonValidationErrors('ids'); + } + + /** + * @effects admin_bulk_delete_rejects_unknown_id + */ + #[Test] + public function bulk_delete_rejects_ids_that_do_not_exist(): void + { + $this->actingAs($this->userWithUploadPermissions(['read', 'delete'])) + ->postJson(self::INDEX_URL.'/bulk-delete', ['ids' => [999999]]) + ->assertStatus(422) + ->assertJsonValidationErrors('ids.0'); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/ImageUploadAuthTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/ImageUploadAuthTest.php new file mode 100644 index 00000000..240d23b1 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/ImageUploadAuthTest.php @@ -0,0 +1,113 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Feature\Http\Controllers; + +use App\Models\User; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * CKEditor5 이미지 업로드 인가 회귀 테스트 (결함 ⑫⑬) + * + * 인가는 라우트 게이트(AdminBaseController: auth:sanctum + admin)가 전담한다. + * 클라이언트 쿼리스트링(`?permission=`)이 검사 권한명을 정하고 빈 값이면 검사를 + * 통째로 skip 하던 죽은 인가 분기가 제거되었음을 고정한다. + * + * POST /api/plugins/sirsoft-ckeditor5/upload + */ +class ImageUploadAuthTest extends PluginTestCase +{ + protected function setUp(): void + { + parent::setUp(); + Storage::fake('plugins'); + } + + /** + * 비로그인 요청은 admin 게이트에서 401 로 거부된다. + * + * @scenario case=guest_upload + * + * @effects upload_requires_admin + */ + public function test_upload_rejects_guest(): void + { + $file = UploadedFile::fake()->image('test.jpg'); + + $response = $this->postJson('/api/plugins/sirsoft-ckeditor5/upload', [ + 'upload' => $file, + ]); + + $response->assertUnauthorized(); + $this->assertDatabaseCount('ckeditor5_image_uploads', 0); + } + + /** + * 인증된 일반 회원(비관리자)은 admin 게이트에서 403 으로 거부된다. + * + * @scenario case=non_admin_upload + * + * @effects upload_requires_admin + */ + public function test_upload_rejects_authenticated_non_admin(): void + { + $member = User::factory()->create(); + $file = UploadedFile::fake()->image('test.jpg'); + + $response = $this->actingAs($member)->postJson( + '/api/plugins/sirsoft-ckeditor5/upload', + ['upload' => $file] + ); + + $response->assertForbidden(); + $this->assertDatabaseCount('ckeditor5_image_uploads', 0); + } + + /** + * 관리자는 정상적으로 업로드된다 (기본 동작 보존). + * + * @scenario case=admin_upload + * + * @effects upload_requires_admin + */ + public function test_upload_succeeds_for_admin(): void + { + $admin = $this->createAdminUser(); + $file = UploadedFile::fake()->image('photo.jpg', 100, 100)->size(500); + + $response = $this->actingAs($admin)->postJson( + '/api/plugins/sirsoft-ckeditor5/upload', + ['upload' => $file] + ); + + $response->assertStatus(201); + $response->assertJsonStructure(['url']); + $this->assertDatabaseCount('ckeditor5_image_uploads', 1); + } + + /** + * 클라이언트가 임의의 `?permission=` 쿼리를 붙여도 응답은 불변이다. + * + * 과거 죽은 인가 분기에서는 관리자가 보유하지 않은 권한명을 넘기면 403 이 되었으나, + * 이제 쿼리는 완전히 무시되고 admin 게이트만 판정하므로 201 이 유지된다. + * + * @scenario case=permission_param_ignored + * + * @effects client_permission_param_has_no_effect + */ + public function test_upload_ignores_client_supplied_permission_query(): void + { + $admin = $this->createAdminUser(); + $file = UploadedFile::fake()->image('photo.jpg', 100, 100)->size(500); + + $response = $this->actingAs($admin)->postJson( + '/api/plugins/sirsoft-ckeditor5/upload?permission=some.permission.the.admin.lacks', + ['upload' => $file] + ); + + $response->assertStatus(201); + $response->assertJsonStructure(['url']); + $this->assertDatabaseCount('ckeditor5_image_uploads', 1); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/fixtures/ckeditor5-auth.ts b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/fixtures/ckeditor5-auth.ts new file mode 100644 index 00000000..f80330de --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/fixtures/ckeditor5-auth.ts @@ -0,0 +1,34 @@ +/** + * CKEditor5 플러그인 권한 fixture. + * + * 코어 `tests/Playwright/fixtures/auth.ts` 의 헬퍼를 재사용하되, 플러그인 권한 토큰 fixture 를 + * 자체적으로 정의한다. 권한 식별자는 임의 string 이므로 코어 PlaywrightIssueToken 커맨드가 + * 그대로 동작한다 (Permission::firstOrCreate 가 자동 생성). + * + * 업로드 관리 화면은 조회와 삭제 권한이 분리돼 있으므로 두 토큰을 따로 발급해 + * 권한 경계(조회만 가진 계정의 삭제 403)를 실제로 밟는다. + */ +import { test as base } from '@playwright/test'; +// 6단계 상위 = 코어 루트의 fixtures/auth.ts +import { issueToken, issueScopedToken, authenticatePage } from '../../../../../../tests/Playwright/fixtures/auth'; + +type Ckeditor5AuthFixtures = { + /** 업로드 조회 + 삭제 권한 보유 토큰 */ + uploadsManageToken: string; + /** 업로드 조회만 가능한 토큰 (삭제 403 경계 검증용) */ + uploadsReadOnlyToken: string; +}; + +export const test = base.extend<Ckeditor5AuthFixtures>({ + uploadsManageToken: async ({}, use) => { + await use(issueToken('sirsoft-ckeditor5.uploads.read', 'sirsoft-ckeditor5.uploads.delete')); + }, + // 권한 경계를 실제로 밟으려면 admin 역할이 없어야 한다 — `issueToken` 은 admin 역할을 함께 + // 부여하고 admin 은 전체 권한을 보유하므로, 그 토큰으로는 삭제가 통과해 403 대신 404 가 온다. + uploadsReadOnlyToken: async ({}, use) => { + await use(issueScopedToken('admin.access', 'sirsoft-ckeditor5.uploads.read')); + }, +}); + +export { authenticatePage }; +export { expect } from '@playwright/test'; diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/playwright.config.ts b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/playwright.config.ts new file mode 100644 index 00000000..788b651c --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/playwright.config.ts @@ -0,0 +1,90 @@ +/** + * CKEditor5 플러그인 Playwright E2E 설정. + * + * 코어 `playwright.config.ts` 와 동일한 base URL 해석 우선순위를 따른다 — 플러그인도 활성 호스트가 + * 가변(개발자/CI/운영 환경별로 다른 도메인)이므로 하드코딩 회피. + * + * Base URL 해석: + * 1. PLAYWRIGHT_BASE_URL 환경변수 (CI/명시적 오버라이드) + * 2. .env (코어 루트) 의 APP_URL — 단 localhost 류는 fallback 부적합 + * 3. 그 외 — 명시 에러 + * + * 실행 예시: + * PowerShell — $env:PLAYWRIGHT_BASE_URL='https://g7.dev'; npm run test:e2e + * Bash — PLAYWRIGHT_BASE_URL=https://g7.dev npm run test:e2e + * + * 플러그인은 코어 fixture 의 `issueToken` / `authenticatePage` 헬퍼를 재사용 — 권한 식별자는 + * `sirsoft-ckeditor5.*` 등 임의 string. + */ +import { defineConfig, devices } from '@playwright/test'; +import { readFileSync, existsSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +// ESM 환경(package.json "type": "module")에서는 __dirname 이 정의되지 않으므로 +// import.meta.url 로 재구성한다. +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** + * 코어 루트 (artisan / .env / Playwright 산출물의 기준 경로). + * + * 확장 config 는 확장 디렉토리에서 실행되지만, 산출물을 그 안에 쓰면 Windows 에서 + * `plugin:update` 의 디렉토리 이동이 열린 핸들에 걸려 실패한다. + * 산출물은 코어 루트 아래로 모아 update 경로와 분리한다 (.gitignore 가 이미 덮는 위치). + */ +const CORE_ROOT = process.env.G7_ROOT || resolve(__dirname, '../../../../../'); + +/** 확장별 산출물 격리 — 확장끼리 리포트를 덮어쓰지 않도록 slug 로 네임스페이스. */ +const ARTIFACT_SLUG = 'plugins/sirsoft-ckeditor5'; + +function readEnvFile(filePath: string, key: string): string | null { + if (!existsSync(filePath)) return null; + const content = readFileSync(filePath, { encoding: 'utf-8' }); + const pattern = new RegExp(`^${key}=(.*)$`, 'm'); + const match = content.match(pattern); + if (!match) return null; + let value = match[1].trim(); + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { + value = value.slice(1, -1); + } + return value || null; +} + +function resolveBaseUrl(): string { + if (process.env.PLAYWRIGHT_BASE_URL) { + return process.env.PLAYWRIGHT_BASE_URL; + } + const appUrl = readEnvFile(resolve(CORE_ROOT, '.env'), 'APP_URL'); + if (appUrl && !/^https?:\/\/localhost(:\d+)?\/?$/i.test(appUrl)) { + return appUrl; + } + throw new Error( + 'CKEditor5 플러그인 E2E base URL 미설정. PLAYWRIGHT_BASE_URL 환경변수를 지정하거나 코어 .env 의 APP_URL 을 활성 호스트로 설정하세요.' + ); +} + +export default defineConfig({ + testDir: './specs', + outputDir: resolve(CORE_ROOT, 'test-results', ARTIFACT_SLUG), + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: resolve(CORE_ROOT, 'playwright-report', ARTIFACT_SLUG), open: 'never' }], + ['list'], + ], + 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, + }, + projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }], +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/admin/uploads-admin.spec.ts b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/admin/uploads-admin.spec.ts new file mode 100644 index 00000000..55eb742f --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/admin/uploads-admin.spec.ts @@ -0,0 +1,279 @@ +/** + * E2E: 에디터 업로드 이미지 관리 화면 (공개 #115) + * + * @scenario admin_ckeditor5_uploads_list_filter_and_permission_boundary + * @effects admin_index_lists_uploads, admin_ui_reference_badge, admin_ui_merge_query, + * admin_index_unreferenced_filter, admin_destroy_requires_delete_permission, + * settings_cleanup_toggle_default_off, admin_destroy_removes_file_and_record, + * admin_bulk_delete_removes_selected, settings_cleanup_retention_round_trip + * + * 배경: 에디터로 올렸지만 쓰이지 않는 이미지가 무기한 누적되던 결함(#115)의 회수 화면. + * 사용자 파일을 실제로 파기하는 화면이라, 브라우저에서 실제로 확인해야 하는 것은 + * "목록이 뜬다" 가 아니라 아래 여섯 가지다. + * + * 1. 목록·필터·배지가 i18n 원문 키 노출 없이 렌더된다 + * 2. 필터를 건 뒤 페이지를 옮겨도 목록 상태(검색·참조상태)가 URL 에 보존된다 + * (mergeQuery 누락 시 필터가 조용히 풀려 다른 대상이 화면에 실린다) + * 3. 삭제 권한이 없는 계정에서는 삭제가 403 으로 막힌다 + * 4. 설정 화면의 자동 정리 토글이 기본 꺼짐으로 보인다 + * 5. 실제로 올린 이미지가 단건·일괄 삭제로 목록에서 사라진다 (파괴 성공 경로) + * — 거절 경로(403/404/422)만 검증하면 "아무것도 못 지우는" 회귀를 잡지 못한다 + * 6. 설정 값을 바꿔 저장하면 다시 조회했을 때 그대로 남는다 (저장 왕복) + */ +import { test, expect, authenticatePage } from '../../fixtures/ckeditor5-auth'; + +const UPLOADS_URL = '/admin/plugins/sirsoft-ckeditor5/uploads'; +const SETTINGS_URL = '/admin/plugins/sirsoft-ckeditor5/settings'; + +const DATAGRID = '#ckeditor5_uploads_datagrid'; +const FILTER_SECTION = '#filter_section'; +const NOTICE = '#reference_notice'; + +/** 1x1 투명 PNG — 업로드 검증(image 규칙)을 통과하는 최소 이미지 */ +const PNG_1X1 = Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', + 'base64', +); + +/** + * 에디터 업로드 엔드포인트로 이미지를 올리고, 관리 목록에서 그 행의 id 를 찾아 돌려준다. + * + * 업로드 응답은 에디터용(URL) 이라 관리 id 를 담지 않으므로 원본명으로 되찾는다. + * + * @param page Playwright 페이지 + * @param token 업로드·조회 권한 토큰 + * @returns 업로드된 행의 id 와 원본 파일명 + */ +async function uploadImage( + page: import('@playwright/test').Page, + token: string, +): Promise<{ id: number; filename: string }> { + const filename = `e2e-issue115-${Date.now()}-${Math.floor(Math.random() * 100000)}.png`; + + const upload = await page.request.post('/api/plugins/sirsoft-ckeditor5/upload', { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + multipart: { upload: { name: filename, mimeType: 'image/png', buffer: PNG_1X1 } }, + }); + + expect(upload.ok(), `업로드가 성공해야 한다 (status ${upload.status()})`).toBeTruthy(); + + const list = await page.request.get( + `/api/plugins/sirsoft-ckeditor5/admin/uploads?search=${encodeURIComponent(filename)}&per_page=5`, + { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }, + ); + + expect(list.status()).toBe(200); + + const rows = (await list.json()).data.data as Array<{ id: number; original_name: string }>; + const row = rows.find((item) => item.original_name === filename); + + expect(row, '방금 올린 이미지가 관리 목록에서 조회되어야 한다').toBeTruthy(); + + return { id: row!.id, filename }; +} + +/** + * 원본명으로 관리 목록을 조회해 남아 있는 행 수를 센다. + * + * @param page Playwright 페이지 + * @param token 조회 권한 토큰 + * @param filename 원본 파일명 + * @returns 남아 있는 행 수 + */ +async function countByFilename( + page: import('@playwright/test').Page, + token: string, + filename: string, +): Promise<number> { + const response = await page.request.get( + `/api/plugins/sirsoft-ckeditor5/admin/uploads?search=${encodeURIComponent(filename)}&per_page=5`, + { headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' } }, + ); + + expect(response.status()).toBe(200); + + const rows = (await response.json()).data.data as Array<{ original_name: string }>; + + return rows.filter((item) => item.original_name === filename).length; +} + +/** + * 업로드 관리 화면으로 이동하고 목록이 붙을 때까지 기다린다. + * + * @param page Playwright 페이지 + */ +async function gotoUploads(page: import('@playwright/test').Page): Promise<void> { + await page.goto(UPLOADS_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect(page.locator(DATAGRID)).toBeAttached({ timeout: 20_000 }); +} + +// @scenario trigger=admin_ui, permitted=yes +// @effects admin_index_lists_uploads, admin_ui_reference_badge +test('#115 - 업로드 관리 화면이 목록·필터·안내와 함께 렌더된다', async ({ page, uploadsManageToken }) => { + const consoleErrors: string[] = []; + page.on('console', (message) => { + if (message.type() === 'error') consoleErrors.push(message.text()); + }); + + await authenticatePage(page, uploadsManageToken); + await gotoUploads(page); + + expect(page.url()).not.toMatch(/\/admin\/login/); + + await expect(page.locator(FILTER_SECTION)).toBeVisible(); + await expect(page.locator(NOTICE)).toBeVisible(); + + // i18n 원문 키가 그대로 노출되면 번역 누락이다. + const body = (await page.locator('body').innerText()) ?? ''; + expect(body).not.toContain('sirsoft-ckeditor5.admin.uploads'); + expect(body).not.toContain('$t:'); + + expect(consoleErrors).toEqual([]); +}); + +// @scenario trigger=admin_ui, filter=unreferenced +// @effects admin_index_unreferenced_filter, admin_ui_merge_query +test('#115 - 참조 상태 필터가 URL 에 실리고 페이지 이동에도 보존된다', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + await page.goto(`${UPLOADS_URL}?referenced=unreferenced&search=`); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect(page.locator(DATAGRID)).toBeAttached({ timeout: 20_000 }); + + expect(page.url()).toContain('referenced=unreferenced'); + + // 목록 조회가 필터를 그대로 서버에 전달하는지 — 응답이 200 이어야 화면이 유지된다. + const response = await page.request.get( + '/api/plugins/sirsoft-ckeditor5/admin/uploads?referenced=unreferenced&per_page=20', + { headers: { Authorization: `Bearer ${uploadsManageToken}`, Accept: 'application/json' } }, + ); + + expect(response.status()).toBe(200); + const payload = await response.json(); + expect(payload.data).toHaveProperty('pagination'); + expect(payload.data).toHaveProperty('meta.scan_limited'); +}); + +// @scenario trigger=admin_ui, permitted=read_only +// @effects admin_destroy_requires_delete_permission +test('#115 - 삭제 권한이 없으면 삭제 API 가 403 으로 막힌다', async ({ page, uploadsReadOnlyToken }) => { + await authenticatePage(page, uploadsReadOnlyToken); + await gotoUploads(page); + + // 조회는 가능해야 한다 (읽기 권한 보유) + const list = await page.request.get('/api/plugins/sirsoft-ckeditor5/admin/uploads?per_page=1', { + headers: { Authorization: `Bearer ${uploadsReadOnlyToken}`, Accept: 'application/json' }, + }); + expect(list.status()).toBe(200); + + // 삭제는 대상 존재 여부와 무관하게 권한 단계에서 먼저 막혀야 한다. + const destroy = await page.request.delete('/api/plugins/sirsoft-ckeditor5/admin/uploads/999999', { + headers: { Authorization: `Bearer ${uploadsReadOnlyToken}`, Accept: 'application/json' }, + }); + expect(destroy.status()).toBe(403); +}); + +// @scenario trigger=admin_ui, response=matrix +// @effects admin_bulk_delete_rejects_empty, admin_destroy_not_found +test('#115 - 응답 매트릭스: 빈 선택 422 / 없는 대상 404', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + + const headers = { Authorization: `Bearer ${uploadsManageToken}`, Accept: 'application/json' }; + + const empty = await page.request.post('/api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete', { + headers, + data: { ids: [] }, + }); + expect(empty.status()).toBe(422); + expect(await empty.json()).toHaveProperty('errors.ids'); + + const missing = await page.request.delete('/api/plugins/sirsoft-ckeditor5/admin/uploads/999999', { headers }); + expect(missing.status()).toBe(404); +}); + +// @scenario trigger=admin_ui_single, permitted=yes +// @effects admin_destroy_removes_file_and_record +test('#115 - 올린 이미지가 단건 삭제로 목록에서 사라진다', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + await gotoUploads(page); + + const { id, filename } = await uploadImage(page, uploadsManageToken); + + expect(await countByFilename(page, uploadsManageToken, filename)).toBe(1); + + const destroy = await page.request.delete(`/api/plugins/sirsoft-ckeditor5/admin/uploads/${id}`, { + headers: { Authorization: `Bearer ${uploadsManageToken}`, Accept: 'application/json' }, + }); + + expect(destroy.status()).toBe(200); + expect(await countByFilename(page, uploadsManageToken, filename)).toBe(0); +}); + +// @scenario trigger=admin_ui_bulk, permitted=yes +// @effects admin_bulk_delete_removes_selected +test('#115 - 선택한 이미지들이 일괄 삭제로 함께 사라진다', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + await gotoUploads(page); + + const first = await uploadImage(page, uploadsManageToken); + const second = await uploadImage(page, uploadsManageToken); + + const bulk = await page.request.post('/api/plugins/sirsoft-ckeditor5/admin/uploads/bulk-delete', { + headers: { Authorization: `Bearer ${uploadsManageToken}`, Accept: 'application/json' }, + data: { ids: [first.id, second.id] }, + }); + + expect(bulk.status()).toBe(200); + expect(await countByFilename(page, uploadsManageToken, first.filename)).toBe(0); + expect(await countByFilename(page, uploadsManageToken, second.filename)).toBe(0); +}); + +// @scenario trigger=settings_screen, toggle=default +// @effects settings_cleanup_retention_round_trip +test('#115 - 보존기간을 저장하면 다시 조회해도 유지된다', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + + const headers = { Authorization: `Bearer ${uploadsManageToken}`, Accept: 'application/json' }; + const SETTINGS_API = '/api/admin/plugins/sirsoft-ckeditor5/settings'; + + const before = await page.request.get(SETTINGS_API, { headers }); + expect(before.status()).toBe(200); + + const current = (await before.json()).data as Record<string, unknown>; + const originalDays = Number(current.unusedImageRetentionDays ?? 30); + const nextDays = originalDays === 45 ? 40 : 45; + + // 자동 정리 토글은 건드리지 않는다 — 켜 두면 검수 환경에서 실제 파기가 돌기 시작한다. + const saved = await page.request.put(SETTINGS_API, { + headers, + data: { ...current, unusedImageRetentionDays: nextDays }, + }); + expect(saved.status()).toBe(200); + + const after = await page.request.get(SETTINGS_API, { headers }); + expect(after.status()).toBe(200); + expect(Number((await after.json()).data.unusedImageRetentionDays)).toBe(nextDays); + + // 원래 값으로 되돌려 다음 회차·다른 spec 에 값을 남기지 않는다. + const restored = await page.request.put(SETTINGS_API, { + headers, + data: { ...current, unusedImageRetentionDays: originalDays }, + }); + expect(restored.status()).toBe(200); +}); + +// @scenario trigger=settings_screen, toggle=default +// @effects settings_cleanup_toggle_default_off +test('#115 - 플러그인 설정에 미사용 이미지 정리 섹션이 노출된다', async ({ page, uploadsManageToken }) => { + await authenticatePage(page, uploadsManageToken); + await page.goto(SETTINGS_URL); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const section = page.locator('#section_cleanup_card'); + await expect(section).toBeAttached({ timeout: 20_000 }); + await section.scrollIntoViewIfNeeded(); + + await expect(page.locator('#field_unused_image_cleanup')).toBeVisible(); + await expect(page.locator('#field_unused_image_retention_days')).toBeVisible(); + await expect(page.locator('#open_uploads_button')).toBeVisible(); +}); diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-ckeditor5/tests/PluginTestCase.php index 3c15b1a1..ba431c22 100644 --- a/plugins/_bundled/sirsoft-ckeditor5/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/PluginTestCase.php @@ -4,16 +4,21 @@ namespace Plugins\Sirsoft\Ckeditor5\Tests; use App\Contracts\Extension\StorageInterface; use App\Enums\PermissionType; +use App\Extension\HookManager; +use App\Extension\Storage\PluginStorageDriver; use App\Models\Permission; use App\Models\Role; use App\Models\User; -use App\Extension\Storage\PluginStorageDriver; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Route; +use Plugins\Sirsoft\Ckeditor5\Http\Controllers\Admin\ImageUploadAdminController; use Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageServeController; use Plugins\Sirsoft\Ckeditor5\Http\Controllers\ImageUploadController; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageReferenceSourceRepositoryInterface; use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; +use Plugins\Sirsoft\Ckeditor5\Repositories\ImageReferenceSourceRepository; use Plugins\Sirsoft\Ckeditor5\Repositories\ImageUploadRepository; +use Plugins\Sirsoft\Ckeditor5\Services\ImageCleanupService; use Plugins\Sirsoft\Ckeditor5\Services\ImageServeService; use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadService; use Tests\TestCase; @@ -43,8 +48,13 @@ abstract class PluginTestCase extends TestCase ImageUploadRepository::class ); + $this->app->bind( + ImageReferenceSourceRepositoryInterface::class, + ImageReferenceSourceRepository::class + ); + // StorageInterface 바인딩 - $this->app->when([ImageUploadService::class, ImageServeService::class]) + $this->app->when([ImageUploadService::class, ImageServeService::class, ImageCleanupService::class]) ->needs(StorageInterface::class) ->give(fn () => new PluginStorageDriver('sirsoft-ckeditor5', 'plugins')); @@ -60,6 +70,23 @@ abstract class PluginTestCase extends TestCase Route::get('images/{hash}', [ImageServeController::class, 'serve']) ->name('api.sirsoft-ckeditor5.images.serve'); + + // 업로드 관리 (관리자) — 실제 라우트 파일과 동일한 미들웨어 체인을 유지해 + // 권한 경계(403)까지 테스트가 실제로 밟게 한다. + Route::prefix('admin')->name('admin.')->middleware('auth:sanctum')->group(function () { + Route::get('uploads', [ImageUploadAdminController::class, 'index']) + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.read') + ->name('uploads.index'); + + Route::post('uploads/bulk-delete', [ImageUploadAdminController::class, 'bulkDestroy']) + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.delete') + ->name('uploads.bulk-delete'); + + Route::delete('uploads/{id}', [ImageUploadAdminController::class, 'destroy']) + ->whereNumber('id') + ->middleware('permission:admin,sirsoft-ckeditor5.uploads.delete') + ->name('uploads.destroy'); + }); }); } @@ -85,7 +112,7 @@ abstract class PluginTestCase extends TestCase */ private function snapshotHookManager(): void { - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $this->hookSnapshot = [ 'hooks' => $ref->getProperty('hooks')->getValue(), 'filters' => $ref->getProperty('filters')->getValue(), @@ -102,7 +129,7 @@ abstract class PluginTestCase extends TestCase return; } - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']); $ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']); $ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']); diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Ckeditor5ImageUploadUrlTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Ckeditor5ImageUploadUrlTest.php new file mode 100644 index 00000000..fa51d50b --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Ckeditor5ImageUploadUrlTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\Config; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * Ckeditor5ImageUpload download_url(직접 URL/폴백) 단위 테스트 + * + * file_path 의 첫 세그먼트=카테고리 일반형 분해와 행 storage_disk 기준 + * 직접 URL/스트리밍 폴백을 검증합니다. + */ +class Ckeditor5ImageUploadUrlTest extends PluginTestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + Ckeditor5ImageUpload::flushStorageCache(); + } + + protected function tearDown(): void + { + Ckeditor5ImageUpload::flushStorageCache(); + HookManager::resetAll(); + + parent::tearDown(); + } + + /** + * @scenario consumer=editor, disk_setting=public, e2e=ecommerce_settings_field, hook=unregistered, override=follow_core, row_state=legacy_local_row + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable + */ + #[Test] + public function it_falls_back_to_api_url_for_plugins_disk_row(): void + { + $image = new Ckeditor5ImageUpload([ + 'hash' => 'aaaabbbbcccc', + 'file_path' => 'images/2026/01/01/a.jpg', + 'storage_disk' => 'plugins', + ]); + + $this->assertSame( + '/api/plugins/sirsoft-ckeditor5/images/aaaabbbbcccc', + $image->download_url + ); + } + + #[Test] + public function it_returns_direct_url_for_cdn_disk_row(): void + { + $image = new Ckeditor5ImageUpload([ + 'hash' => 'aaaabbbbcccc', + 'file_path' => 'images/2026/01/01/a.jpg', + 'storage_disk' => 'fake_cdn', + ]); + + $this->assertSame( + 'https://cdn.test/assets/sirsoft-ckeditor5/images/2026/01/01/a.jpg', + $image->download_url + ); + } + + #[Test] + public function it_decomposes_non_images_prefix_generically(): void + { + // ApiDocSampleService 등 외부 생성 행 — 첫 세그먼트=카테고리 일반형 + $image = new Ckeditor5ImageUpload([ + 'hash' => 'aaaabbbbcccc', + 'file_path' => 'ckeditor5/apidoc-sample.png', + 'storage_disk' => 'fake_cdn', + ]); + + $this->assertSame( + 'https://cdn.test/assets/sirsoft-ckeditor5/ckeditor5/apidoc-sample.png', + $image->download_url + ); + } + + #[Test] + public function it_falls_back_when_file_path_has_no_prefix(): void + { + $image = new Ckeditor5ImageUpload([ + 'hash' => 'aaaabbbbcccc', + 'file_path' => 'orphan.png', + 'storage_disk' => 'fake_cdn', + ]); + + $this->assertSame( + '/api/plugins/sirsoft-ckeditor5/images/aaaabbbbcccc', + $image->download_url + ); + } + + /** + * @scenario consumer=editor, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=block, override=module_override, row_state=legacy_local_row + * + * @effects hook_supply_modify_block_respected + */ + #[Test] + public function hook_block_falls_back_to_api_url(): void + { + HookManager::addFilter('core.storage.filter_url', fn ($url) => ''); + + $image = new Ckeditor5ImageUpload([ + 'hash' => 'aaaabbbbcccc', + 'file_path' => 'images/2026/01/01/a.jpg', + 'storage_disk' => 'fake_cdn', + ]); + + $this->assertSame( + '/api/plugins/sirsoft-ckeditor5/images/aaaabbbbcccc', + $image->download_url + ); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/ImageServeServiceRowDiskTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/ImageServeServiceRowDiskTest.php new file mode 100644 index 00000000..94c28316 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/ImageServeServiceRowDiskTest.php @@ -0,0 +1,236 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit; + +use App\Extension\PluginManager; +use App\Extension\Storage\PluginStorageDriver; +use App\Services\PluginSettingsService; +use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Storage; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Plugin; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; +use Plugins\Sirsoft\Ckeditor5\Services\ImageServeService; +use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadService; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; +use Symfony\Component\HttpFoundation\StreamedResponse; + +/** + * ImageServeService 행 disk 기준 서빙 테스트 + * + * 공개 자산 디스크 전환 이후에도 전환 이전 행(구 disk)의 서빙이 404 가 되지 않고, + * CDN 디스크로 기록된 행의 스트리밍 요청도 그 행의 실제 저장 위치를 향하는지 + * (혼재 운용 정합) 검증합니다. + * + * ImageServeControllerTest 는 모든 행이 주입 디스크('plugins')라서 + * withDisk 분기가 실행되지 않으므로 별도 파일로 둡니다. + */ +class ImageServeServiceRowDiskTest extends PluginTestCase +{ + private ImageServeService $service; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + Ckeditor5ImageUpload::flushStorageCache(); + + $this->service = app(ImageServeService::class); + } + + protected function tearDown(): void + { + Ckeditor5ImageUpload::flushStorageCache(); + + parent::tearDown(); + } + + /** + * 지정 disk 에 파일과 업로드 행을 함께 생성합니다. + * + * @param string $disk 행 storage_disk + * @param string $filePath "카테고리/상대경로" 형태의 file_path + * @return Ckeditor5ImageUpload 생성된 행 + */ + private function makeRowOnDisk(string $disk, string $filePath): Ckeditor5ImageUpload + { + [$category, $relativePath] = explode('/', $filePath, 2); + + (new PluginStorageDriver('sirsoft-ckeditor5', $disk))->put($category, $relativePath, 'image-bytes'); + + return Ckeditor5ImageUpload::create([ + 'original_name' => basename($filePath), + 'file_path' => $filePath, + 'storage_disk' => $disk, + 'file_size' => 11, + 'mime_type' => 'image/jpeg', + 'uploaded_by' => null, + ]); + } + + /** + * @effects serve_and_delete_follow_row_disk, mixed_rows_resolve_per_row_disk + */ + #[Test] + public function serve_streams_row_from_its_recorded_disk(): void + { + // 혼재 운용 — 주입 스토리지 disk('plugins')와 다른 disk 의 행도 서빙 가능해야 한다 + $cdnRow = $this->makeRowOnDisk('fake_cdn', 'images/row-disk-test/cdn.jpg'); + $localRow = $this->makeRowOnDisk('plugins', 'images/row-disk-test/local.jpg'); + + $this->assertInstanceOf(StreamedResponse::class, $this->service->serve($cdnRow)); + $this->assertInstanceOf(StreamedResponse::class, $this->service->serve($localRow)); + } + + /** + * 외부 생성 행(첫 세그먼트가 'images' 가 아닌 경우)도 카테고리 일반형 분해로 + * 행 disk 에서 서빙된다 — ApiDoc 샘플(`ckeditor5/apidoc-sample.png`) 형태. + * + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function serve_resolves_non_images_prefix_on_row_disk(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'ckeditor5/row-disk-sample.png'); + + $this->assertInstanceOf(StreamedResponse::class, $this->service->serve($row)); + } + + /** + * 행 disk 에 파일이 없으면 주입 디스크로 몰래 폴백하지 않고 null 을 반환한다 + * (다른 디스크의 동명 파일을 잘못 서빙하는 회귀 차단). + * + * @effects serve_and_delete_follow_row_disk + */ + #[Test] + public function serve_returns_null_when_file_absent_on_row_disk(): void + { + // 파일은 주입 디스크에만 두고, 행은 CDN 디스크로 기록 + (new PluginStorageDriver('sirsoft-ckeditor5', 'plugins')) + ->put('images', 'row-disk-test/only-local.jpg', 'image-bytes'); + + $row = Ckeditor5ImageUpload::create([ + 'original_name' => 'only-local.jpg', + 'file_path' => 'images/row-disk-test/only-local.jpg', + 'storage_disk' => 'fake_cdn', + 'file_size' => 11, + 'mime_type' => 'image/jpeg', + 'uploaded_by' => null, + ]); + + $this->assertNull($this->service->serve($row)); + } + + /** + * 고아 disk 행의 서빙은 500 이 아니라 스트리밍 폴백이어야 한다. + * + * 디스크를 제공하던 플러그인이 비활성화되면 행 storage_disk 가 config 에서 사라진다. + * 이때 미등록 disk 로 withDisk 를 만들면 response 가 InvalidArgumentException 을 + * 던져 에디터 이미지 서빙이 500 이 된다. + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function serve_falls_back_when_row_disk_is_orphaned(): void + { + $row = $this->makeRowOnDisk('fake_cdn', 'images/row-disk-test/orphan.jpg'); + + Config::set('filesystems.disks.fake_cdn', null); + Storage::forgetDisk('fake_cdn'); + Ckeditor5ImageUpload::flushStorageCache(); + + // 예외 없이 폴백 — 파일은 도달 불가이므로 null(404) 이 정상 degradation + $this->assertNull($this->service->serve($row)); + } + + /** + * SP 배선(`$storageCategoryServices`)이 주입하는 것과 동일한 스토리지로 서비스를 만듭니다. + * + * PluginTestCase 는 ImageUploadService 의 StorageInterface 를 'plugins' 고정으로 + * 하드바인딩하므로, 컨테이너 해석으로는 카테고리 디스크 배선을 검증할 수 없습니다. + * 여기서는 실제 플러그인 인스턴스의 getStorageFor('images') 를 직접 주입해 + * 배선이 주는 것과 같은 조건을 재현합니다. + */ + private function makeUploadServiceWithCategoryStorage(): ImageUploadService + { + $plugin = app(PluginManager::class)->getPlugin('sirsoft-ckeditor5') + ?? new Plugin(base_path('plugins/sirsoft-ckeditor5')); + + return new ImageUploadService( + app(ImageUploadRepositoryInterface::class), + $plugin->getStorageFor('images') + ); + } + + /** + * 플러그인 개별 오버라이드를 지정 값으로 고정합니다. + * + * PluginTestCase 는 플러그인 설정을 격리하지 않아, 고정하지 않으면 + * `plugin_setting()` 이 개발 머신의 실제 저장값(storage/app/plugins/…)을 읽는다. + * 오버라이드는 코어 전역보다 우선하므로 그 값이 남아 있으면 전역 설정을 세우는 + * 이 테스트가 개발자 환경에 따라 통과/실패한다 — 두 입력을 모두 통제한다. + * + * @param string $override 플러그인 설정 public_asset_disk 값 ('' = 코어 따름) + */ + private function fixPluginOverride(string $override): void + { + $stub = new class($override) extends PluginSettingsService + { + public function __construct(private string $override) {} + + public function get(string $identifier, ?string $key = null, mixed $default = null): mixed + { + if ($key === 'public_asset_disk') { + return $this->override; + } + + return $default; + } + }; + + $this->app->instance(PluginSettingsService::class, $stub); + } + + /** + * put 종단 — 공개 자산 디스크 설정값이 신규 행의 storage_disk 에 기록되고, + * 그 행이 다시 자기 disk 로 서빙되는지 단언합니다(배선 회귀 검출). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_configured_public_asset_disk_on_row(): void + { + $this->fixPluginOverride(''); + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + + $record = $this->makeUploadServiceWithCategoryStorage() + ->upload(UploadedFile::fake()->image('cdn-editor.jpg', 40, 40), null); + + $this->assertSame('fake_cdn', $record->storage_disk); + $this->assertInstanceOf(StreamedResponse::class, $this->service->serve($record)); + } + + /** + * put 종단 — 공개 자산 디스크 미설정이면 신규 행이 기본 디스크로 기록(현행 보존). + * + * @effects mixed_rows_resolve_per_row_disk + */ + #[Test] + public function upload_records_base_disk_when_public_asset_disk_unset(): void + { + $this->fixPluginOverride(''); + Config::set('core.storage.public_asset_disk', ''); + + $record = $this->makeUploadServiceWithCategoryStorage() + ->upload(UploadedFile::fake()->image('local-editor.jpg', 40, 40), null); + + $this->assertSame('plugins', $record->storage_disk); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsCatalogAttachTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsCatalogAttachTest.php new file mode 100644 index 00000000..44c19219 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsCatalogAttachTest.php @@ -0,0 +1,67 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit; + +use App\Extension\AbstractPlugin; +use App\Extension\PluginManager; +use App\Http\Controllers\Api\Admin\PluginSettingsController; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 플러그인 설정 응답 공개 자산 카탈로그 부착 테스트 (공개#100) + * + * 설정 스키마가 public_asset_disk 를 선언한 플러그인(ckeditor5)의 설정 GET 응답에 + * available_public_asset_disks 카탈로그가 서버 부착되는지 검증합니다. 화면이 코어 + * 설정 API(core.settings.read)를 교차 조회하면 화면 권한(core.plugins.read)과 + * 표면이 갈려 커스텀 역할에서 카탈로그만 조용히 비므로, 단일 표면 계약을 고정합니다. + * + * @effects settings_catalog_includes_plugin_registered_disks + */ +class PluginSettingsCatalogAttachTest extends PluginTestCase +{ + #[Test] + public function settings_response_attaches_public_asset_catalog(): void + { + $response = app(PluginSettingsController::class)->show('sirsoft-ckeditor5'); + + $this->assertSame(200, $response->getStatusCode()); + + $data = $response->getData(true)['data'] ?? []; + $this->assertArrayHasKey('available_public_asset_disks', $data); + + $ids = array_column($data['available_public_asset_disks'], 'id'); + $this->assertContains('none', $ids); + $this->assertContains('public', $ids); + $this->assertContains('s3', $ids); + } + + #[Test] + public function catalog_is_not_attached_when_schema_lacks_public_asset_disk(): void + { + // 스키마에 public_asset_disk 가 없는 플러그인 시뮬레이션 — 게이트가 닫혀야 한다 + $plugin = $this->createMock(AbstractPlugin::class); + $plugin->method('getSettingsSchema')->willReturn([ + 'some_other_key' => ['type' => 'string'], + ]); + + $manager = $this->createMock(PluginManager::class); + $manager->method('getPlugin')->willReturn($plugin); + $this->app->instance(PluginManager::class, $manager); + + $response = app(PluginSettingsController::class)->show('sirsoft-ckeditor5'); + + $this->assertSame(200, $response->getStatusCode()); + $this->assertArrayNotHasKey( + 'available_public_asset_disks', + $response->getData(true)['data'] ?? [] + ); + } + + protected function tearDown(): void + { + $this->app->forgetInstance(PluginManager::class); + + parent::tearDown(); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsPublicAssetDiskValidationTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsPublicAssetDiskValidationTest.php new file mode 100644 index 00000000..c5274b95 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsPublicAssetDiskValidationTest.php @@ -0,0 +1,119 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit; + +use App\Extension\AbstractPlugin; +use App\Extension\PluginManager; +use App\Http\Requests\Admin\UpdatePluginSettingsRequest; +use Illuminate\Validation\ValidationException; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 플러그인 공개 자산 디스크 저장 검증 테스트 (공개#100) + * + * 코어가 카탈로그를 부착하는 게이트(스키마에 public_asset_disk 선언)와 동일한 + * 조건에서 값 검증도 걸린다. 선택지를 내려주는 표면과 값을 받는 표면의 강도가 + * 갈리면, 오타 디스크명이 200 으로 저장된 뒤 런타임 폴백만 남아 운영자에게는 + * "저장은 됐는데 CDN 이 안 붙는" 무증상 상태로만 보인다. + * + * 스키마 타입만으로는 검증할 수 없다 — 선택지가 코어 3종 + 플러그인 훅 등록 + * 디스크라 런타임에만 확정되기 때문이다. + * + * @effects invalid_disk_rejected_with_422 + */ +class PluginSettingsPublicAssetDiskValidationTest extends PluginTestCase +{ + /** + * 플러그인 설정 저장 요청을 해석합니다. + * + * @param string $disk public_asset_disk 값 + */ + private function resolve(string $disk): UpdatePluginSettingsRequest + { + $request = UpdatePluginSettingsRequest::create('/', 'PUT', [ + 'public_asset_disk' => $disk, + ]); + $request->setContainer($this->app)->setRedirector($this->app['redirect']); + $request->setRouteResolver(fn () => new class + { + public function parameter(string $key, $default = null): string + { + return 'sirsoft-ckeditor5'; + } + }); + $request->validateResolved(); + + return $request; + } + + #[Test] + public function core_catalog_disk_is_accepted(): void + { + $request = $this->resolve('public'); + + $this->assertSame('public', $request->validated()['public_asset_disk']); + } + + #[Test] + public function empty_value_is_accepted_as_follow_core(): void + { + $request = $this->resolve(''); + + $this->assertSame('', $request->validated()['public_asset_disk'] ?? ''); + } + + #[Test] + public function unknown_disk_is_rejected(): void + { + $this->expectException(ValidationException::class); + + try { + $this->resolve('nonexistent_disk'); + } catch (ValidationException $e) { + $this->assertArrayHasKey( + 'public_asset_disk', + $e->validator->errors()->toArray(), + '카탈로그에 없는 디스크는 public_asset_disk 오류를 내야 한다' + ); + + throw $e; + } + } + + #[Test] + public function validation_gate_is_closed_when_schema_lacks_the_key(): void + { + // 이 키를 선언하지 않은 플러그인에는 검증도 붙지 않아야 한다 — + // 부착 게이트(PluginSettingsController)와 검증 게이트가 같은 조건이어야 한다. + $plugin = $this->createMock(AbstractPlugin::class); + $plugin->method('getSettingsSchema')->willReturn([ + 'some_other_key' => ['type' => 'string'], + ]); + + $manager = $this->createMock(PluginManager::class); + $manager->method('getPlugin')->willReturn($plugin); + $this->app->instance(PluginManager::class, $manager); + + $request = UpdatePluginSettingsRequest::create('/', 'PUT', [ + 'some_other_key' => 'value', + ]); + $request->setContainer($this->app)->setRedirector($this->app['redirect']); + $request->setRouteResolver(fn () => new class + { + public function parameter(string $key, $default = null): string + { + return 'other-plugin'; + } + }); + + $this->assertArrayNotHasKey('public_asset_disk', $request->rules()); + } + + protected function tearDown(): void + { + $this->app->forgetInstance(PluginManager::class); + + parent::tearDown(); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageCleanupServiceTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageCleanupServiceTest.php new file mode 100644 index 00000000..ad43d7b0 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageCleanupServiceTest.php @@ -0,0 +1,294 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit\Services; + +use App\Contracts\Extension\StorageInterface; +use App\Extension\Storage\PluginStorageDriver; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; +use Plugins\Sirsoft\Ckeditor5\Services\ImageCleanupService; +use Plugins\Sirsoft\Ckeditor5\Services\ImageReferenceScanService; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 미참조 업로드 이미지 정리 서비스 테스트 + * + * "파일 먼저, 기록 나중" 순서와 행 disk 존중, 참조 보존(두 URL 변종), dry-run 무삭제를 + * 회귀로 고정합니다. + */ +class ImageCleanupServiceTest extends PluginTestCase +{ + private ImageCleanupService $service; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + + Ckeditor5ImageUpload::flushStorageCache(); + + $this->service = app(ImageCleanupService::class); + } + + protected function tearDown(): void + { + Ckeditor5ImageUpload::flushStorageCache(); + + parent::tearDown(); + } + + /** + * 파일과 기록을 함께 만듭니다. + * + * @param string $hash 이미지 해시 + * @param string $filename 저장 파일명 + * @param int $ageDays 업로드 경과 일수 + * @param string $disk 스토리지 디스크 + * @param bool $withFile 실제 파일 생성 여부 + * @return Ckeditor5ImageUpload 생성된 기록 + */ + private function makeUpload( + string $hash, + string $filename, + int $ageDays = 40, + string $disk = 'plugins', + bool $withFile = true, + ): Ckeditor5ImageUpload { + $path = "2026/08/14/{$filename}"; + + if ($withFile) { + (new PluginStorageDriver('sirsoft-ckeditor5', $disk))->put('images', $path, 'image-bytes'); + } + + $upload = Ckeditor5ImageUpload::create([ + 'hash' => $hash, + 'original_name' => $filename, + 'file_path' => "images/{$path}", + 'storage_disk' => $disk, + 'file_size' => 11, + 'mime_type' => 'image/png', + ]); + + $upload->forceFill(['created_at' => now()->subDays($ageDays)])->saveQuietly(); + + return $upload->refresh(); + } + + /** + * 코어 참조 소스에 본문을 심습니다. + * + * @param string $content 본문 + */ + private function seedReference(string $content): void + { + DB::table('mail_templates')->insert([ + 'type' => 'cleanup-test-'.uniqid(), + 'subject' => json_encode(['ko' => '정리 테스트']), + 'body' => $content, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * @scenario age=past_retention, disk_state=default_local, url_variant=absent + * + * @effects prune_deletes_file_and_record + */ + #[Test] + public function unreferenced_upload_past_retention_loses_both_file_and_record(): void + { + $upload = $this->makeUpload('aaaaaaaaaaaa', 'orphan.png'); + $storage = new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(1, $result['deleted']); + $this->assertFalse($storage->exists('images', '2026/08/14/orphan.png')); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @scenario age=past_retention, disk_state=default_local, url_variant=hash_api_fallback + * + * @effects prune_keeps_referenced_hash_form + */ + #[Test] + public function referenced_by_hash_url_is_kept(): void + { + $upload = $this->makeUpload('bbbbbbbbbbbb', 'used-hash.png'); + $this->seedReference('<img src="/api/plugins/sirsoft-ckeditor5/images/bbbbbbbbbbbb">'); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(0, $result['deleted']); + $this->assertSame(1, $result['referenced']); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @scenario age=past_retention, disk_state=default_local, url_variant=direct_url_uuid + * + * @effects prune_keeps_referenced_direct_url + */ + #[Test] + public function referenced_by_direct_disk_url_is_kept(): void + { + $upload = $this->makeUpload('cccccccccccc', 'used-direct.png'); + $this->seedReference('<img src="https://cdn.test/assets/ckeditor5/images/2026/08/14/used-direct.png">'); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(0, $result['deleted']); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @scenario age=within_retention, disk_state=default_local, url_variant=absent + * + * @effects prune_keeps_within_retention + */ + #[Test] + public function upload_within_retention_is_not_a_candidate(): void + { + $upload = $this->makeUpload('dddddddddddd', 'fresh.png', ageDays: 3); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(0, $result['scanned']); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects prune_dry_run_no_delete + */ + #[Test] + public function dry_run_reports_targets_without_deleting(): void + { + $upload = $this->makeUpload('eeeeeeeeeeee', 'dry.png'); + $storage = new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'); + + $result = $this->service->pruneUnused(30, 100, dryRun: true); + + $this->assertSame(0, $result['deleted']); + $this->assertCount(1, $result['items']); + $this->assertTrue($storage->exists('images', '2026/08/14/dry.png')); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * @effects prune_limit_oldest_first + */ + #[Test] + public function limit_processes_the_oldest_candidates_first(): void + { + $oldest = $this->makeUpload('111111111111', 'oldest.png', ageDays: 90); + $newer = $this->makeUpload('222222222222', 'newer.png', ageDays: 40); + + $result = $this->service->pruneUnused(30, 1); + + $this->assertSame(1, $result['scanned']); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $oldest->id]); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $newer->id]); + } + + /** + * 파일이 이미 없는 행은 고아 기록이므로 기록만 지운다 (재시도 루프 방지). + * + * @scenario age=past_retention, disk_state=file_missing, url_variant=absent + * + * @effects prune_missing_file_removes_record + */ + #[Test] + public function record_without_a_file_is_still_removed(): void + { + $upload = $this->makeUpload('333333333333', 'ghost.png', withFile: false); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(1, $result['deleted']); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * 행마다 disk 가 다른 혼재 상태에서도 그 행의 저장 위치를 향해 지워야 한다. + * + * @scenario age=past_retention, disk_state=secondary_disk, url_variant=absent + * + * @effects prune_respects_row_disk + */ + #[Test] + public function deletion_follows_the_disk_recorded_on_each_row(): void + { + $local = $this->makeUpload('444444444444', 'on-local.png'); + $cdn = $this->makeUpload('555555555555', 'on-cdn.png', disk: 'fake_cdn'); + + $result = $this->service->pruneUnused(30, 100); + + $this->assertSame(2, $result['deleted']); + $this->assertFalse( + (new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'))->exists('images', '2026/08/14/on-local.png') + ); + $this->assertFalse( + (new PluginStorageDriver('sirsoft-ckeditor5', 'fake_cdn'))->exists('images', '2026/08/14/on-cdn.png') + ); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $local->id]); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $cdn->id]); + } + + /** + * @effects delete_upload_shared_by_ui_and_command + */ + #[Test] + public function delete_upload_removes_file_and_record_for_a_single_row(): void + { + $upload = $this->makeUpload('666666666666', 'single.png', ageDays: 1); + $storage = new PluginStorageDriver('sirsoft-ckeditor5', 'plugins'); + + $this->assertTrue($this->service->deleteUpload($upload)); + $this->assertFalse($storage->exists('images', '2026/08/14/single.png')); + $this->assertDatabaseMissing('ckeditor5_image_uploads', ['id' => $upload->id]); + } + + /** + * 파일 삭제가 실패하면 기록을 남긴다. + * + * 파일이 남아 있는데 기록만 지우면 그 파일을 가리키는 것이 아무것도 없어져 다음 회차에도 + * 회수 대상이 되지 못한다. 실패는 failed 로 계상하고 다음 회차에 재시도한다. + * + * @scenario age=past_retention, disk_state=default_local, url_variant=absent + * + * @effects prune_delete_failure_keeps_record + */ + #[Test] + public function file_delete_failure_keeps_the_record_and_counts_as_failed(): void + { + $upload = $this->makeUpload('777777777777', 'locked.png'); + + $storage = \Mockery::mock(StorageInterface::class); + $storage->shouldReceive('getDisk')->andReturn('local'); + $storage->shouldReceive('withDisk')->andReturnSelf(); + $storage->shouldReceive('exists')->andReturnTrue(); + $storage->shouldReceive('delete')->andReturnFalse(); + + $service = new ImageCleanupService( + app(ImageUploadRepositoryInterface::class), + app(ImageReferenceScanService::class), + $storage, + ); + + $result = $service->pruneUnused(30, 100); + + $this->assertSame(1, $result['failed']); + $this->assertSame(0, $result['deleted']); + $this->assertDatabaseHas('ckeditor5_image_uploads', ['id' => $upload->id]); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageReferenceScanServiceTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageReferenceScanServiceTest.php new file mode 100644 index 00000000..60662e5d --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageReferenceScanServiceTest.php @@ -0,0 +1,214 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit\Services; + +use App\Extension\HookManager; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Services\ImageReferenceScanService; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * 업로드 이미지 참조 판정 테스트 + * + * 본문에 박히는 URL 이 두 형태(해시 API 폴백형 / 디스크 직접 URL)라 판정 토큰도 둘입니다. + * 한 형태만 검사하면 다른 형태로 저장된 이미지를 "미참조" 로 오판해 삭제하게 되므로, + * 두 변종 모두를 회귀로 고정합니다. + */ +class ImageReferenceScanServiceTest extends PluginTestCase +{ + private ImageReferenceScanService $service; + + protected function setUp(): void + { + parent::setUp(); + + $this->service = app(ImageReferenceScanService::class); + } + + /** + * 업로드 기록을 생성합니다. + * + * @param string $hash 이미지 해시 + * @param string $filename 저장 파일명 + * @return Ckeditor5ImageUpload 생성된 기록 + */ + private function makeUpload(string $hash = 'abcdef012345', string $filename = 'aaaaaaaa-bbbb.png'): Ckeditor5ImageUpload + { + return Ckeditor5ImageUpload::create([ + 'hash' => $hash, + 'original_name' => 'sample.png', + 'file_path' => "images/2026/08/14/{$filename}", + 'storage_disk' => 'plugins', + 'file_size' => 100, + 'mime_type' => 'image/png', + ]); + } + + /** + * 코어 소유 참조 소스(메일 템플릿 본문)에 임의 내용을 담은 행을 만듭니다. + * + * @param string $content 본문 + */ + private function seedCoreSourceContent(string $content): void + { + DB::table('mail_templates')->insert([ + 'type' => 'reference-scan-test-'.uniqid(), + 'subject' => json_encode(['ko' => '참조 스캔 테스트']), + 'body' => $content, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + + /** + * @effects reference_sources_default_core_only + */ + #[Test] + public function default_sources_are_core_only_when_no_extension_registers(): void + { + $tables = array_column($this->service->getReferenceSources(), 'table'); + + $this->assertContains('template_layouts', $tables); + $this->assertContains('notification_templates', $tables); + $this->assertNotContains('board_posts', $tables, '훅 미등록 상태에서는 모듈 소스가 포함되지 않아야 한다.'); + } + + /** + * @effects reference_sources_hook_append + */ + #[Test] + public function extensions_can_append_sources_through_the_filter_hook(): void + { + HookManager::addFilter( + ImageReferenceScanService::FILTER_REFERENCE_SOURCES, + fn (array $sources) => [...$sources, ['table' => 'pages', 'columns' => ['content']]], + ); + + $tables = array_column(app(ImageReferenceScanService::class)->getReferenceSources(), 'table'); + + $this->assertContains('pages', $tables); + } + + /** + * @effects reference_match_hash_api_url + */ + #[Test] + public function hash_form_url_in_content_counts_as_referenced(): void + { + $upload = $this->makeUpload(hash: 'a1b2c3d4e5f6'); + $this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/a1b2c3d4e5f6">'); + + $this->assertTrue($this->service->isReferenced($upload)); + } + + /** + * @effects reference_match_direct_url + */ + #[Test] + public function direct_disk_url_in_content_counts_as_referenced(): void + { + $upload = $this->makeUpload(hash: 'ffffffffffff', filename: '9f8e7d6c-1234.webp'); + $this->seedCoreSourceContent('<img src="https://cdn.test/assets/ckeditor5/images/2026/08/14/9f8e7d6c-1234.webp">'); + + $this->assertTrue( + $this->service->isReferenced($upload), + '직접 URL 은 해시를 포함하지 않으므로 저장 파일명으로 매칭되어야 한다.' + ); + } + + /** + * @effects reference_unreferenced + */ + #[Test] + public function content_without_the_image_leaves_it_unreferenced(): void + { + $upload = $this->makeUpload(hash: '0123456789ab', filename: 'unused-file.png'); + $this->seedCoreSourceContent('<p>이미지가 없는 본문</p>'); + + $this->assertFalse($this->service->isReferenced($upload)); + } + + /** + * LIKE 와일드카드가 들어간 파일명이 임의 문자로 해석되면 무관한 본문이 매칭된다. + * + * @effects reference_like_escape + */ + #[Test] + public function like_wildcards_in_the_filename_do_not_match_arbitrary_content(): void + { + $upload = $this->makeUpload(hash: 'cafebabe1234', filename: 'a%b_c.png'); + $this->seedCoreSourceContent('<img src="/files/aXXXbYc.png">'); + + $this->assertFalse( + $this->service->isReferenced($upload), + '% 와 _ 는 이스케이프되어 리터럴로 비교되어야 한다.' + ); + } + + /** + * 불량 선언은 건너뛰되 **반드시 흔적을 남긴다**. + * + * 조용히 건너뛰면 확장이 등록한 소스가 오타 하나로 빠져도 아무도 모른 채 그 확장의 + * 콘텐츠에 박힌 이미지가 "미참조" 로 분류된다 — 삭제 방향의 오판이라 경고가 유일한 + * 발견 통로다. skip 동작과 경고를 함께 고정한다. + * + * @effects reference_invalid_source_skipped, reference_invalid_source_warns + */ + #[Test] + public function invalid_source_declarations_are_skipped_without_breaking_the_scan(): void + { + Log::spy(); + + HookManager::addFilter( + ImageReferenceScanService::FILTER_REFERENCE_SOURCES, + fn (array $sources) => [ + ...$sources, + ['table' => 'no_such_table_here', 'columns' => ['content']], + ['table' => 'template_layouts', 'columns' => ['no_such_column']], + ['columns' => ['content']], + ], + ); + + $service = app(ImageReferenceScanService::class); + $tables = array_column($service->getReferenceSources(), 'table'); + + $this->assertNotContains('no_such_table_here', $tables); + $this->assertContains('template_layouts', $tables); + + // 형식 자체가 깨진 선언(table 키 없음) 1건 + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => is_string($message) && str_contains($message, '형식이 올바르지 않아')) + ->once(); + + // 실재하지 않는 테이블 1건 + 실재하지 않는 컬럼 1건 + Log::shouldHaveReceived('warning') + ->withArgs(fn ($message) => is_string($message) && str_contains($message, '존재하는 테이블/컬럼이 없어')) + ->twice(); + + // 불량 선언이 섞여도 판정 자체는 정상 동작해야 한다. + $upload = $this->makeUpload(hash: 'deadbeef0000', filename: 'skip-test.png'); + $this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/deadbeef0000">'); + + $this->assertTrue($service->isReferenced($upload)); + } + + /** + * @effects reference_map_batch + */ + #[Test] + public function map_referenced_reports_each_upload_separately(): void + { + $referenced = $this->makeUpload(hash: '111111111111', filename: 'used.png'); + $unreferenced = $this->makeUpload(hash: '222222222222', filename: 'unused.png'); + + $this->seedCoreSourceContent('<img src="/api/plugins/sirsoft-ckeditor5/images/111111111111">'); + + $map = $this->service->mapReferenced(collect([$referenced, $unreferenced])); + + $this->assertTrue($map[$referenced->id]); + $this->assertFalse($map[$unreferenced->id]); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageUploadServiceTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageUploadServiceTest.php new file mode 100644 index 00000000..d3f1ee11 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageUploadServiceTest.php @@ -0,0 +1,91 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit\Services; + +use App\Contracts\Extension\StorageInterface; +use App\Extension\HookManager; +use Illuminate\Http\UploadedFile; +use Mockery; +use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Models\Ckeditor5ImageUpload; +use Plugins\Sirsoft\Ckeditor5\Repositories\Contracts\ImageUploadRepositoryInterface; +use Plugins\Sirsoft\Ckeditor5\Services\ImageUploadService; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * ImageUploadService 단위 테스트 + * + * 업로드 훅 트라이어드(before_upload → filter_upload_file → after_upload) 발화를 검증합니다. + * upload 자체 동작(저장/기록)은 Feature ImageUploadControllerTest 가 커버합니다. + */ +class ImageUploadServiceTest extends PluginTestCase +{ + private ImageUploadService $service; + + /** @var MockInterface&ImageUploadRepositoryInterface */ + private $repository; + + /** @var MockInterface&StorageInterface */ + private $storage; + + protected function setUp(): void + { + parent::setUp(); + + $this->repository = Mockery::mock(ImageUploadRepositoryInterface::class); + $this->storage = Mockery::mock(StorageInterface::class); + + $this->service = new ImageUploadService($this->repository, $this->storage); + } + + protected function tearDown(): void + { + Mockery::close(); + parent::tearDown(); + } + + #[Test] + public function test_upload_fires_hooks(): void + { + // Arrange + $beforeUploadFired = false; + $afterUploadFired = false; + $filterApplied = false; + + HookManager::addAction('sirsoft-ckeditor5.image.before_upload', function () use (&$beforeUploadFired) { + $beforeUploadFired = true; + }); + + HookManager::addFilter('sirsoft-ckeditor5.image.filter_upload_file', function ($file) use (&$filterApplied) { + $filterApplied = true; + + return $file; + }); + + HookManager::addAction('sirsoft-ckeditor5.image.after_upload', function () use (&$afterUploadFired) { + $afterUploadFired = true; + }); + + $file = UploadedFile::fake()->image('test.jpg'); + + $this->storage->shouldReceive('put')->andReturn(true); + $this->storage->shouldReceive('getDisk')->andReturn('local'); + + $record = new Ckeditor5ImageUpload; + $this->repository->shouldReceive('create')->andReturn($record); + + // Act + $this->service->upload($file, null); + + // Assert + $this->assertTrue($beforeUploadFired, 'before_upload hook should be fired'); + $this->assertTrue($filterApplied, 'filter_upload_file hook should be applied'); + $this->assertTrue($afterUploadFired, 'after_upload hook should be fired'); + + // Cleanup hooks + HookManager::clearAction('sirsoft-ckeditor5.image.before_upload'); + HookManager::clearFilter('sirsoft-ckeditor5.image.filter_upload_file'); + HookManager::clearAction('sirsoft-ckeditor5.image.after_upload'); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/StorageDiskRecursionTest.php b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/StorageDiskRecursionTest.php new file mode 100644 index 00000000..04c5ff41 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/Unit/StorageDiskRecursionTest.php @@ -0,0 +1,126 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Tests\Unit; + +use App\Extension\PluginManager; +use App\Services\PluginSettingsService; +use Illuminate\Support\Facades\Config; +use PHPUnit\Framework\Attributes\Test; +use Plugins\Sirsoft\Ckeditor5\Plugin; +use Plugins\Sirsoft\Ckeditor5\Tests\PluginTestCase; + +/** + * getStorageDiskFor 설정 로드 순환 참조 회귀 테스트 (실제 설정 로드 경로) + * + * 실제 플러그인 인스턴스의 getStorageDiskFor 가 plugin_setting() 을 경유할 때, + * 설정 로드 자신이 스토리지('settings' 카테고리)를 다시 요구하는 상황을 + * 컨테이너 스파이로 재현합니다. PUBLIC_ASSET_CATEGORIES 게이트가 제거되면 + * 'settings' 해석이 설정 서비스를 재호출해 호출 횟수 단언이 깨집니다. + */ +class StorageDiskRecursionTest extends PluginTestCase +{ + private Plugin $plugin; + + /** + * 설정 로드가 스토리지를 경유하는 경로를 재현하는 스파이 + */ + private object $settingsSpy; + + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + Config::set('core.storage.public_asset_disk', ''); + + $this->plugin = app(PluginManager::class)->getPlugin('sirsoft-ckeditor5') + ?? new Plugin(base_path('plugins/sirsoft-ckeditor5')); + + // 실제 설정 로드처럼 get() 내부에서 플러그인 스토리지('settings')를 재요구하는 스파이. + // 재진입(= getStorageDiskFor('settings') 가 다시 설정을 조회하는 회귀) 발생 시 + // reentered 플래그 + 호출 횟수 증가로 검출됩니다. + $this->settingsSpy = new class extends PluginSettingsService + { + public ?Plugin $plugin = null; + + public int $calls = 0; + + public bool $reentered = false; + + public ?string $nestedDisk = null; + + private bool $inGet = false; + + public function __construct() {} + + public function get(string $identifier, ?string $key = null, mixed $default = null): mixed + { + $this->calls++; + + if ($this->inGet) { + // 설정 로드 중 설정 재조회 = 순환 고리. 무한 재귀 대신 폭로. + $this->reentered = true; + + return $default; + } + + $this->inGet = true; + + try { + // 설정 로드가 스토리지를 경유하는 실제 경로 재현 — + // 이 호출이 getStorageDiskFor('settings') 를 타고 되돌아온다. + $this->nestedDisk = $this->plugin->getStorageFor('settings')->getDisk(); + + if ($key === 'public_asset_disk') { + return 'fake_cdn'; + } + + return $default; + } finally { + $this->inGet = false; + } + } + }; + $this->settingsSpy->plugin = $this->plugin; + $this->app->instance(PluginSettingsService::class, $this->settingsSpy); + } + + protected function tearDown(): void + { + $this->app->forgetInstance(PluginSettingsService::class); + + parent::tearDown(); + } + + /** + * @effects settings_load_recursion_free + */ + #[Test] + public function settings_category_never_consults_plugin_settings(): void + { + // 'settings' 카테고리 해석은 설정 서비스를 한 번도 거치지 않아야 한다 — + // 거치는 순간 설정 로드 ↔ 디스크 해석의 순환 고리가 열린다. + $this->assertSame($this->plugin->getStorageDisk(), $this->plugin->getStorageDiskFor('settings')); + $this->assertSame(0, $this->settingsSpy->calls); + } + + /** + * @effects settings_load_recursion_free + */ + #[Test] + public function images_resolution_survives_settings_load_that_uses_storage(): void + { + // 공개 자산 카테고리 해석 → 설정 조회 → (설정 로드가 'settings' 스토리지 재요구) + // 전체 사슬이 유한 종료하고, 설정 조회는 정확히 1회만 발생해야 한다. + $this->assertSame('fake_cdn', $this->plugin->getStorageDiskFor('images')); + + $this->assertSame(1, $this->settingsSpy->calls); + $this->assertFalse($this->settingsSpy->reentered, '설정 로드 중 설정 재조회(순환) 발생'); + // 설정 로드가 요구한 'settings' 스토리지는 공개 자산 디스크가 아닌 기본 디스크 + $this->assertSame($this->plugin->getStorageDisk(), $this->settingsSpy->nestedDisk); + } +} diff --git a/plugins/_bundled/sirsoft-ckeditor5/tests/scenarios/security-fixes-2026-08.yaml b/plugins/_bundled/sirsoft-ckeditor5/tests/scenarios/security-fixes-2026-08.yaml new file mode 100644 index 00000000..927576ad --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/tests/scenarios/security-fixes-2026-08.yaml @@ -0,0 +1,20 @@ +feature: 에디터 이미지 업로드 인가 고정 (2026-08 보안 수정) + +description: | + 2026-08 비공개 보안 제보(⑫/⑬) 대응 — 업로드 엔드포인트가 클라이언트 쿼리스트링 + (?permission=)으로 인가 판정 대상 권한명을 받던 죽은 분기를 제거. 인가는 서버 라우트 + 게이트(auth:sanctum + admin, AdminBaseController)가 전담하고, 클라이언트가 보내는 + 권한 파라미터는 어떤 값이든 판정에 영향을 주지 않는다. 프론트(initEditor.ts / + html-editor.json)의 uploadPermission 전달·조립 코드도 제거(dist 재빌드 동반). + +axes: + case: [guest_upload, non_admin_upload, admin_upload, permission_param_ignored] + +exclusions: [] + +effects: + - upload_requires_admin + - client_permission_param_has_no_effect + +test_files: + - plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/ImageUploadAuthTest.php diff --git a/plugins/_bundled/sirsoft-ckeditor5/upgrades/Upgrade_1_0_2.php b/plugins/_bundled/sirsoft-ckeditor5/upgrades/Upgrade_1_0_2.php new file mode 100644 index 00000000..32ae52ee --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/upgrades/Upgrade_1_0_2.php @@ -0,0 +1,17 @@ +<?php + +namespace Plugins\Sirsoft\Ckeditor5\Upgrades; + +use App\Extension\AbstractUpgradeStep; + +/** + * sirsoft-ckeditor5 플러그인 1.0.2 업그레이드 스텝 + * + * 미사용 이미지 자동 정리 설정 두 키(`unusedImageCleanup`, `unusedImageRetentionDays`)를 + * 기설치본의 저장 설정 파일에 백필한다. defaults.json 은 설치 시점에만 저장 파일을 시드하므로 + * 이미 설치된 사이트에는 새 키가 생기지 않고, 그 상태에서는 스케줄 게이트가 설정을 찾지 못해 + * "설정을 만지지 않은 사이트에서 자동 삭제 0" 이 저장 파일 수준에서 보이지 않는다. + * + * 모든 비즈니스 로직은 data/1.0.2/migrations/ 로 격리(AbstractUpgradeStep 규약). + */ +class Upgrade_1_0_2 extends AbstractUpgradeStep {} diff --git a/plugins/_bundled/sirsoft-ckeditor5/upgrades/data/1.0.2/migrations/01_SeedUnusedImageCleanupSettings.php b/plugins/_bundled/sirsoft-ckeditor5/upgrades/data/1.0.2/migrations/01_SeedUnusedImageCleanupSettings.php new file mode 100644 index 00000000..91227791 --- /dev/null +++ b/plugins/_bundled/sirsoft-ckeditor5/upgrades/data/1.0.2/migrations/01_SeedUnusedImageCleanupSettings.php @@ -0,0 +1,105 @@ +<?php + +declare(strict_types=1); + +namespace App\Upgrades\Data\Ext\Plugins\SirsoftCkeditor5\V1_0_2\Migrations; + +use App\Extension\Helpers\FilePermissionHelper; +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\File; + +/** + * 기설치본의 플러그인 설정 파일에 미사용 이미지 정리 설정 두 키를 백필한다. + * + * 배경: + * + * `config/settings/defaults.json` 은 플러그인 설치 시점에 저장 설정 파일을 한 번 시드할 뿐, + * 이후 조회 경로(PluginSettingsService::get)의 폴백 체계에는 포함되지 않는다. 그래서 이미 + * 설치된 사이트에는 새 설정 키가 생기지 않는다. + * + * 자동 정리는 기본 꺼짐이어야 하므로 `unusedImageCleanup` 은 false 로 시드한다. 커맨드가 + * `--scheduled` 에서 false 폴백으로 다시 확인하므로 이 백필이 없어도 자동 삭제는 일어나지 + * 않지만, 관리자 설정 화면이 저장값을 그대로 보여주도록 파일에도 명시해 둔다. + * + * 멱등: 키가 이미 있으면 값을 덮어쓰지 않는다 (운영자가 켠 설정을 되돌리지 않기 위함). + * + * V-1 안전 격리 (docs/extension/upgrade-step-guide.md §13): + * - 파일 시스템 + FilePermissionHelper 만 사용 + * - Service / Manager / Repository 컨테이너 해석 없음 + */ +final class SeedUnusedImageCleanupSettings implements DataMigration +{ + /** + * 플러그인 설정 저장 경로. + */ + private const SETTINGS_PATH = 'app/plugins/sirsoft-ckeditor5/settings/setting.json'; + + /** + * 백필할 기본값 (키 => 기본값). + * + * @var array<string, mixed> + */ + private const DEFAULTS = [ + 'unusedImageCleanup' => false, + 'unusedImageRetentionDays' => 30, + ]; + + /** + * 마이그레이션 식별자 (로그용). + * + * @return string 사람이 읽을 수 있는 짧은 식별자 + */ + public function name(): string + { + return 'SeedUnusedImageCleanupSettings'; + } + + /** + * 설정 파일에 누락된 정리 설정 키를 추가한다. idempotent. + * + * @param UpgradeContext $context 업그레이드 컨텍스트 (로거 등) + */ + public function run(UpgradeContext $context): void + { + $path = storage_path(self::SETTINGS_PATH); + + if (! File::exists($path)) { + $context->logger->info('[sirsoft-ckeditor5] 설정 파일 없음 — 설치 시 defaults.json 이 시드하므로 skip'); + + return; + } + + $settings = json_decode(File::get($path), true); + + if (! is_array($settings)) { + $context->logger->warning('[sirsoft-ckeditor5] 설정 JSON 형식 비정상 — 정리 설정 백필 skip'); + + return; + } + + $added = []; + + foreach (self::DEFAULTS as $key => $default) { + if (array_key_exists($key, $settings)) { + continue; + } + + $settings[$key] = $default; + $added[] = $key; + } + + if ($added === []) { + $context->logger->info('[sirsoft-ckeditor5] 정리 설정 키가 이미 존재 — 변경 없음'); + + return; + } + + File::put($path, json_encode($settings, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)); + FilePermissionHelper::inheritOwnershipFromParent($path); + + $context->logger->info('[sirsoft-ckeditor5] 미사용 이미지 정리 설정 백필 완료', [ + 'added_keys' => $added, + ]); + } +} diff --git a/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md b/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md index e720dab6..c8c04dad 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-daum_postcode/CHANGELOG.md @@ -4,6 +4,17 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Changed + +- 코어 7.0.7의 외부 스크립트 보안 정책 강화에 맞춰, 주소 검색에 쓰는 Daum 우편번호 스크립트 주소(`t1.daumcdn.net`)를 이 플러그인의 신뢰 출처로 선언했습니다. 정책이 강화된 이후에도 주소 검색 창이 정상적으로 열립니다. +- 신뢰 출처 선언 기능이 코어 7.0.7에서 도입되었으므로, 설치에 필요한 최소 코어 버전을 7.0.7로 조정했습니다. + +### Fixed + +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-10 ### Fixed diff --git a/plugins/_bundled/sirsoft-daum_postcode/composer.json b/plugins/_bundled/sirsoft-daum_postcode/composer.json index 5b8f9e36..f7bb83d8 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/composer.json +++ b/plugins/_bundled/sirsoft-daum_postcode/composer.json @@ -2,7 +2,7 @@ "name": "plugins/sirsoft-daum_postcode", "description": "Daum Postcode Service Plugin for Gnuboard7 platform", "type": "library", - "version": "1.0.1", + "version": "1.0.2", "authors": [ { "name": "sirsoft", diff --git a/plugins/_bundled/sirsoft-daum_postcode/package-lock.json b/plugins/_bundled/sirsoft-daum_postcode/package-lock.json index b2eb5d18..fedaeb02 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/package-lock.json +++ b/plugins/_bundled/sirsoft-daum_postcode/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-daum_postcode", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-daum_postcode", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { "typescript": "^5.3.3", "vite": "^5.4.14" diff --git a/plugins/_bundled/sirsoft-daum_postcode/package.json b/plugins/_bundled/sirsoft-daum_postcode/package.json index 6167d0ad..e82333c6 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/package.json +++ b/plugins/_bundled/sirsoft-daum_postcode/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-daum_postcode", - "version": "1.0.1", + "version": "1.0.2", "description": "그누보드7 Daum 우편번호 플러그인 프론트엔드 에셋", "private": true, "type": "module", diff --git a/plugins/_bundled/sirsoft-daum_postcode/plugin.json b/plugins/_bundled/sirsoft-daum_postcode/plugin.json index 1728f3bd..557eceae 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/plugin.json +++ b/plugins/_bundled/sirsoft-daum_postcode/plugin.json @@ -5,19 +5,22 @@ "ko": "Daum 우편번호", "en": "Daum Postcode" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "description": { "ko": "Daum 우편번호 서비스를 통한 주소 검색 기능을 제공하는 플러그인입니다. API 키 없이 무료로 사용할 수 있습니다.", "en": "Plugin that provides address search functionality through Daum Postcode service. Free to use without API key." }, - "g7_version": ">=7.0.0", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} }, "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-daum_postcode/blob/main/CHANGELOG.md", + "trusted_script_hosts": [ + "t1.daumcdn.net" + ], "assets": { "js": { "entry": "resources/js/index.ts", diff --git a/plugins/_bundled/sirsoft-daum_postcode/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-daum_postcode/resources/layouts/admin/plugin_settings.json index f02ba8fa..a78d4765 100644 --- a/plugins/_bundled/sirsoft-daum_postcode/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-daum_postcode/resources/layouts/admin/plugin_settings.json @@ -542,7 +542,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-gdpr/CHANGELOG.md b/plugins/_bundled/sirsoft-gdpr/CHANGELOG.md index 5e06c66c..255bdaa4 100644 --- a/plugins/_bundled/sirsoft-gdpr/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-gdpr/CHANGELOG.md @@ -4,6 +4,14 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.3] - 2026-08-19 + +### Fixed + +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 회원 탈퇴·삭제 시 동의 이력 정리가 중간에 실패하면 일부 항목만 처리된 채 남던 문제를 수정했습니다 — 이제 전부 처리되거나 전부 취소됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 플러그인 설정 화면의 켜기/끄기 스위치가 레이아웃 편집기에서 편집 대상으로 선택되지 않던 문제를 수정했습니다. + ## [1.0.2] - 2026-08-10 ### Added diff --git a/plugins/_bundled/sirsoft-gdpr/composer.json b/plugins/_bundled/sirsoft-gdpr/composer.json index 8bb80961..d8fa5fea 100644 --- a/plugins/_bundled/sirsoft-gdpr/composer.json +++ b/plugins/_bundled/sirsoft-gdpr/composer.json @@ -2,7 +2,7 @@ "name": "plugins/sirsoft-gdpr", "description": "GDPR (General Data Protection Regulation) plugin for G7 platform by sirsoft", "type": "library", - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "authors": [ { diff --git a/plugins/_bundled/sirsoft-gdpr/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-gdpr/dist/js/plugin.iife.js index f4a59814..bf9f8bf5 100644 --- a/plugins/_bundled/sirsoft-gdpr/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-gdpr/dist/js/plugin.iife.js @@ -1,2 +1 @@ var SirsoftGdpr=(function(p){"use strict";function Z(e,t){const n=e.toLowerCase(),r=t.toLowerCase();if(r.startsWith("*.")){const o=r.slice(1);return n.endsWith(o)&&n!==o.slice(1)}return n===r}function R(e,t){if(typeof window>"u"||typeof URL>"u")return null;let n,r;try{const o=new URL(e,window.location.href);r=o.protocol,n=o.hostname}catch{return null}if(r!=="http:"&&r!=="https:"||n===""||n===window.location.hostname)return null;for(const[o,i]of Object.entries(t))if(o!=="necessary"){for(const s of i)if(Z(n,s))return o}return null}const b="data-gdpr-category",_="gdpr-blocked-embed",j="data-gdpr-original",k=[`script[${b}]`,`iframe[${b}]`,"script[src]","iframe[src]","img[src]","link[href]"].join(", ");let l=null,y={categories:{},needs_renewal:!1},N={};function P(e){if(e&&typeof e=="object"&&"categories"in e){const t=e;return{categories:{...t.categories},needs_renewal:t.needs_renewal===!0}}return{categories:{...e},needs_renewal:!1}}function H(e){y=P(e),l!==null&&m()}function ee(e){N={...e},l!==null&&m()}function te(){typeof document>"u"||l!==null||(m(),l=new MutationObserver(e=>{for(const t of e)t.addedNodes.forEach(n=>{n instanceof HTMLElement&&re(n)})}),l.observe(document.documentElement,{childList:!0,subtree:!0}),window.addEventListener("gdpr:consent-changed",ne))}function ne(e){const t=e.detail;t!==null&&typeof t=="object"&&(y=P(t)),m()}function re(e){ie(e)&&C(e),e.querySelectorAll(k).forEach(t=>C(t))}function m(){typeof document>"u"||(document.querySelectorAll(`.${_}`).forEach(e=>{const t=e.dataset.gdprBlockedCategory;t&&A(t)&&se(e)}),document.querySelectorAll(k).forEach(e=>C(e)))}function C(e){if(e.classList.contains(_)||e.hasAttribute("data-gdpr-blocked-category"))return;const t=e.getAttribute(b);if(t!==null&&t!==""){if(A(t))return;M(e,t);return}const n=oe(e);if(n===null)return;const r=R(n,N);r!==null&&(A(r)||M(e,r))}function oe(e){return e instanceof HTMLScriptElement||e instanceof HTMLIFrameElement||e instanceof HTMLImageElement?e.getAttribute("src"):e instanceof HTMLLinkElement?e.getAttribute("href"):null}function ie(e){if(typeof e.matches!="function")return!1;try{return e.matches(k)}catch{return!1}}function M(e,t){const n=document.createElement("div");n.className=_,n.dataset.gdprBlockedCategory=t,n.dataset.gdprOriginalTag=e.tagName.toLowerCase(),n.setAttribute(j,e.outerHTML),n.setAttribute("aria-hidden","true"),n.style.display="none",e.replaceWith(n)}function se(e){const t=e.getAttribute(j);if(!t){e.remove();return}const n=document.createElement("div");n.innerHTML=t;const r=n.firstElementChild;r!==null?e.replaceWith(r):e.remove()}function A(e){return e==="necessary"?!0:y.needs_renewal?!1:y.categories[e]===!0}const ce=500,ae=["click","keydown","change","submit","pointerdown","touchstart"];let B=!1,E=0;function le(e){e.isTrusted&&(E=Date.now())}function ue(){if(!B){B=!0;for(const e of ae){const t=le;window.addEventListener(e,t,{capture:!0,passive:!0})}}}function G(){const e=window.event;return e&&e.isTrusted?!0:E===0?!1:Date.now()-E<=ce}const u=["XSRF-TOKEN","laravel_session","laravel_maintenance","gdpr_session"];let T=!1,a=null,w={functionalConsented:!1,necessaryAllowlist:u};function fe(e){const t=e.indexOf("=");return t<=0?null:e.substring(0,t).trim()}function de(e){return w.necessaryAllowlist.includes(e)}function ge(e){const t=fe(e);return t===null?!1:!!(pe(e)||de(t)||w.functionalConsented||G())}function pe(e){const t=e.toLowerCase(),n=t.match(/max-age\s*=\s*(-?\d+)/);return!!(n&&parseInt(n[1],10)<=0||t.includes("expires=thu, 01 jan 1970")||t.includes("expires=thu, 01-jan-1970"))}function ye(e){if(T)return;if(T=!0,w=e,a=Object.getOwnPropertyDescriptor(Document.prototype,"cookie")??null,!a||!a.set||!a.get){T=!1;return}const t=a.set,n=a.get;Object.defineProperty(Document.prototype,"cookie",{configurable:!0,get(){return n.call(this)},set(r){ge(r)&&t.call(this,r)}})}function $(e){w=e}const f=[{key:"g7_locale",storage:"localStorage",matchType:"exact"},{key:"auth_token",storage:"localStorage",matchType:"exact"},{key:"g7_cache_version",storage:"localStorage",matchType:"exact"},{key:"g7_cart_key",storage:"localStorage",matchType:"exact"},{key:"g7-devtools-panel",storage:"localStorage",matchType:"exact"},{key:"g7_devtools_",matchType:"prefix"},{key:"g7_filters_",matchType:"prefix"},{key:"g7_columns_",matchType:"prefix"},{key:"g7_order_",matchType:"prefix"}];let U=!1,S=null,h={functionalConsented:!1,necessaryAllowlist:f};function me(e,t){for(const n of h.necessaryAllowlist){if(n.storage&&n.storage!==t)continue;const r=n.matchType??"exact";if(r==="exact"&&n.key===e||r==="prefix"&&e.startsWith(n.key))return!0}return!1}function we(e,t){return!!(me(e,t)||h.functionalConsented||G())}function he(e){if(U)return;U=!0,h=e;const t=Storage.prototype;S=t.setItem,t.setItem=function(n,r){const o=this===window.sessionStorage?"sessionStorage":"localStorage";we(n,o)&&S&&S.call(this,n,r)}}function W(e){h=e}function be(e,t,n){for(const r of n){if(r.storage&&r.storage!==t)continue;const o=r.matchType??"exact";if(o==="exact"&&r.key===e||o==="prefix"&&e.startsWith(r.key))return!0}return!1}function F(e,t,n){const r=[];for(let o=0;o<e.length;o++){const i=e.key(o);i!==null&&r.push(i)}for(const o of r)if(!be(o,t,n))try{e.removeItem(o)}catch{}}function _e(e){const t=document.cookie;if(!t)return;const n=t.split(";");for(const r of n){const o=r.indexOf("="),i=(o>-1?r.substring(0,o):r).trim();if(i&&!e.includes(i))try{document.cookie=`${i}=; Max-Age=0; Path=/`}catch{}}}function K(e={}){const t=e.storageAllowlist??f,n=e.cookieAllowlist??u;F(window.localStorage,"localStorage",t),F(window.sessionStorage,"sessionStorage",t),_e(n)}let L=null;function ke(){if(typeof window>"u")return null;const t=window.G7Core?.api;return t&&typeof t.get=="function"?t:null}async function q(e){const t=ke();if(t)try{return await t.get(e)}catch{return null}try{const n=await fetch("/api"+e,{method:"GET",headers:{Accept:"application/json"},credentials:"same-origin"});return n.ok?await n.json():null}catch{return null}}async function Ce(){if(L)return L;const t=(await q("/plugins/sirsoft-gdpr/settings"))?.data;return t?(L=t,t):null}async function Y(){const e=await q("/plugins/sirsoft-gdpr/consent/cookie/status"),t=e?.data?.consents;return t==null?null:{categories:t,needs_renewal:e?.data?.needs_renewal===!0,current_policy_version:String(e?.data?.current_policy_version??"")}}const D="data-gdpr-blocked-src",I="data-gdpr-blocked-category",Ae="data-gdpr-category";let z=!1,d={blockedDomains:{},consent:{necessary:!0},needs_renewal:!1};function O(e){return e==="necessary"?!0:d.needs_renewal===!0?!1:d.consent?.[e]===!0}function Ee(e,t){const n=e.getAttribute(Ae);if(n!==null&&n!=="")return O(n)?null:n;const r=R(t,d.blockedDomains);return r===null||O(r)?null:r}function Te(e){if(d={blockedDomains:e.blockedDomains??{},consent:e.consent??{necessary:!0},needs_renewal:e.needs_renewal===!0},z||typeof HTMLScriptElement>"u")return;const t=Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype,"src");t===void 0||typeof t.set!="function"||(Object.defineProperty(HTMLScriptElement.prototype,"src",{configurable:!0,enumerable:t.enumerable??!0,get(){return t.get?.call(this)??""},set(n){const r=Ee(this,n);if(r!==null){this.setAttribute(D,n),this.setAttribute(I,r);return}t.set.call(this,n)}}),z=!0)}function v(e){d={blockedDomains:e.blockedDomains??{},consent:e.consent??{necessary:!0},needs_renewal:e.needs_renewal===!0}}function J(e){if(!O(e)||typeof document>"u")return;document.querySelectorAll(`script[${I}="${e}"]`).forEach(n=>{const r=n.getAttribute(D);if(r===null||r==="")return;const o=document.createElement("script");for(const s of Array.from(n.attributes))s.name===D||s.name===I||s.name==="src"||o.setAttribute(s.name,s.value);const i=n.parentNode;i!==null&&(i.replaceChild(o,n),o.src=r)})}const g="sirsoft-gdpr";function X(){if(typeof window>"u")return{};const n=window.G7Config?.plugins?.[g]?.blocked_domains;if(n==null||typeof n!="object")return{};const r={};for(const[o,i]of Object.entries(n)){if(!Array.isArray(i))continue;const s=i.filter(c=>typeof c=="string"&&c!=="");s.length>0&&(r[o]=s)}return r}function Se(){return typeof window>"u"?!1:window.G7Config?.plugins?.[g]?.banner_enabled===!0}function x(){const e=window.G7Core?.getActionDispatcher?.();if(!e){setTimeout(x,100);return}e.registerHandler(`${g}.syncConsent`,async()=>{const t=await Y();if(t===null)return;const n=!t.needs_renewal&&t.categories.functional===!0;H({categories:t.categories,needs_renewal:t.needs_renewal});const r=X();if(v({blockedDomains:r,consent:t.categories,needs_renewal:t.needs_renewal}),W({functionalConsented:n,necessaryAllowlist:f}),$({functionalConsented:n,necessaryAllowlist:u}),n||K(),!t.needs_renewal)for(const[o,i]of Object.entries(t.categories))i===!0&&o!=="necessary"&&J(o)})}async function Q(){if(typeof window>"u")return;if(x(),Se()){const c=X();Te({blockedDomains:c,consent:{necessary:!0}}),ue(),he({functionalConsented:!1,necessaryAllowlist:f}),ye({functionalConsented:!1,necessaryAllowlist:u})}const t=await Ce();if(!t||!(t.banner_enabled===!0))return;const r=t.blocked_domains??{};ee(r),v({blockedDomains:r,consent:{necessary:!0}}),te();const i=await Y(),s=i!==null&&!i.needs_renewal&&i.categories.functional===!0;if(W({functionalConsented:s,necessaryAllowlist:f}),$({functionalConsented:s,necessaryAllowlist:u}),s||K(),i!==null&&(H({categories:i.categories,needs_renewal:i.needs_renewal}),v({blockedDomains:r,consent:i.categories,needs_renewal:i.needs_renewal}),!i.needs_renewal))for(const[c,Le]of Object.entries(i.categories))Le===!0&&c!=="necessary"&&J(c)}function V(){x()}return typeof window<"u"&&(window.__SirsoftGdpr={identifier:g,initPlugin:V}),typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{Q()}):Q()),p.PLUGIN_IDENTIFIER=g,p.initPlugin=V,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p})({}); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-gdpr/package.json b/plugins/_bundled/sirsoft-gdpr/package.json index 0ee7e653..bf745274 100644 --- a/plugins/_bundled/sirsoft-gdpr/package.json +++ b/plugins/_bundled/sirsoft-gdpr/package.json @@ -1,6 +1,6 @@ { "name": "@plugins/sirsoft-gdpr", - "version": "1.0.2", + "version": "1.0.3", "private": true, "type": "module", "scripts": { diff --git a/plugins/_bundled/sirsoft-gdpr/plugin.json b/plugins/_bundled/sirsoft-gdpr/plugin.json index 0c8f2fe3..5398adca 100644 --- a/plugins/_bundled/sirsoft-gdpr/plugin.json +++ b/plugins/_bundled/sirsoft-gdpr/plugin.json @@ -5,7 +5,7 @@ "ko": "GDPR (일반 데이터 보호 규정)", "en": "GDPR (General Data Protection Regulation)" }, - "version": "1.0.2", + "version": "1.0.3", "description": { "ko": "쿠키 동의 배너, 자동 차단, 동의 이력 저장, 마이페이지 동의 철회를 제공하는 GDPR 대응 플러그인입니다.", "en": "GDPR-ready plugin: cookie consent banner, auto-blocking, consent history, and mypage consent withdrawal." diff --git a/plugins/_bundled/sirsoft-gdpr/resources/js/blocker-domains.ts b/plugins/_bundled/sirsoft-gdpr/resources/js/blocker-domains.ts index 6c55797c..ad645358 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/js/blocker-domains.ts +++ b/plugins/_bundled/sirsoft-gdpr/resources/js/blocker-domains.ts @@ -1,3 +1,4 @@ +// audit:allow extension-dist-source-literal-sync DEFAULT_BLOCKED_DOMAINS 는 현재 소비처가 없어 tree-shake 로 번들에서 제외된다 (도메인 문자열 부재 = stale 아님) /** * GDPR 도메인 기반 차단 매칭 + 기본 카탈로그 * diff --git a/plugins/_bundled/sirsoft-gdpr/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-gdpr/resources/layouts/admin/plugin_settings.json index 570d1485..70f77ef3 100644 --- a/plugins/_bundled/sirsoft-gdpr/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-gdpr/resources/layouts/admin/plugin_settings.json @@ -588,7 +588,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "name": "banner_enabled" diff --git a/plugins/_bundled/sirsoft-gdpr/src/Services/GdprConsentService.php b/plugins/_bundled/sirsoft-gdpr/src/Services/GdprConsentService.php index 4b7a56e1..fbb9a7e4 100644 --- a/plugins/_bundled/sirsoft-gdpr/src/Services/GdprConsentService.php +++ b/plugins/_bundled/sirsoft-gdpr/src/Services/GdprConsentService.php @@ -295,9 +295,16 @@ class GdprConsentService { $activeConsents = $this->statusRepository->getActiveByUserId($userId); - foreach ($activeConsents as $consent) { - $this->updateConsent($userId, null, $consent->consent_key, false, 'withdraw'); - } + // 루프 중간에 실패하면 일부 항목만 철회된 채 남는다 — 전부 철회되거나 전부 취소되게 한다. + // updateConsent 내부의 consent.before/after_update 훅도 이 트랜잭션 안에서 + // 발화된다(의도): 리스너 예외 시 철회 전체가 롤백되는 것이 "전부 또는 전무" + // 계약이며, 외부 부수효과(알림 등)를 내는 리스너는 롤백 불가를 감안해 + // 멱등하게 작성해야 한다. + DB::transaction(function () use ($userId, $activeConsents) { + foreach ($activeConsents as $consent) { + $this->updateConsent($userId, null, $consent->consent_key, false, 'withdraw'); + } + }); } /** @@ -312,8 +319,11 @@ class GdprConsentService */ public function purgeOnUserDelete(int $userId): void { - $this->statusRepository->deleteByUserId($userId); - $this->historyRepository->anonymizeForUser($userId); + // 삭제만 되고 익명화가 실패하면 감사 이력에 회원 식별자가 그대로 남는다. + DB::transaction(function () use ($userId) { + $this->statusRepository->deleteByUserId($userId); + $this->historyRepository->anonymizeForUser($userId); + }); } /** diff --git a/plugins/_bundled/sirsoft-gdpr/tests/Feature/GdprConsentAtomicityTest.php b/plugins/_bundled/sirsoft-gdpr/tests/Feature/GdprConsentAtomicityTest.php new file mode 100644 index 00000000..31eaa93b --- /dev/null +++ b/plugins/_bundled/sirsoft-gdpr/tests/Feature/GdprConsentAtomicityTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Plugins\Sirsoft\Gdpr\Tests\Feature; + +use App\Models\User; +use Mockery; +use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentHistoryRepositoryInterface; +use Plugins\Sirsoft\Gdpr\Repositories\Contracts\GdprUserConsentRepositoryInterface; +use Plugins\Sirsoft\Gdpr\Services\GdprConsentService; +use Plugins\Sirsoft\Gdpr\Tests\PluginTestCase; +use RuntimeException; + +/** + * 동의 이력 정리 원자성 테스트 (공개이슈 #112) + * + * 탈퇴 시 항목별 철회는 루프이고, 삭제 시 status 삭제와 history 익명화는 2단계다. + * 중간에 실패하면 일부 항목만 철회되거나, 삭제만 되고 감사 이력에 식별자가 남는다. + * + * @scenario trigger=withdraw, trigger=delete, failure_injection=mid_loop, failure_injection=second_step + * + * @effects partial_revoke_rolled_back, delete_and_anonymize_are_atomic + */ +class GdprConsentAtomicityTest extends PluginTestCase +{ + /** + * 탈퇴 철회 루프가 중간에 실패하면 앞서 철회된 항목도 되돌아간다. + */ + public function test_revoke_all_on_withdraw_rolls_back_partial_loop(): void + { + $user = User::factory()->create(); + $service = app(GdprConsentService::class); + + // 동의 2건을 만든 뒤 철회 루프를 중간에서 실패시킨다 + $service->updateConsents($user->id, null, ['marketing' => true, 'analytics' => true], 'signup'); + + $activeBefore = app(GdprUserConsentRepositoryInterface::class) + ->getActiveByUserId($user->id) + ->count(); + + $this->assertGreaterThanOrEqual(2, $activeBefore, '철회 대상 동의가 2건 이상이어야 합니다'); + + // history 기록을 실패시켜 루프 중간에서 예외를 유발한다 + $failing = Mockery::mock(app(GdprUserConsentHistoryRepositoryInterface::class)); + $failing->shouldReceive('record')->andThrow(new RuntimeException('이력 기록 실패')); + $this->app->instance(GdprUserConsentHistoryRepositoryInterface::class, $failing); + + try { + app(GdprConsentService::class)->revokeAllOnWithdraw($user->id); + $this->fail('철회 루프 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('이력 기록 실패', $e->getMessage()); + } + + // 실패 시: 앞 항목만 철회되고 뒤 항목은 그대로인 반쪽 상태 + $this->app->forgetInstance(GdprUserConsentHistoryRepositoryInterface::class); + $activeAfter = app(GdprUserConsentRepositoryInterface::class) + ->getActiveByUserId($user->id) + ->count(); + + $this->assertSame($activeBefore, $activeAfter, '철회가 일부만 반영된 채 남았습니다'); + } + + /** + * 삭제 시 history 익명화가 실패하면 status 삭제도 되돌아간다. + */ + public function test_purge_on_user_delete_rolls_back_when_anonymize_fails(): void + { + $user = User::factory()->create(); + $service = app(GdprConsentService::class); + + $service->updateConsents($user->id, null, ['marketing' => true], 'signup'); + + $before = app(GdprUserConsentRepositoryInterface::class)->getActiveByUserId($user->id)->count(); + $this->assertGreaterThan(0, $before); + + $failing = Mockery::mock(app(GdprUserConsentHistoryRepositoryInterface::class)); + $failing->shouldReceive('anonymizeForUser')->andThrow(new RuntimeException('익명화 실패')); + $this->app->instance(GdprUserConsentHistoryRepositoryInterface::class, $failing); + + try { + app(GdprConsentService::class)->purgeOnUserDelete($user->id); + $this->fail('익명화 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('익명화 실패', $e->getMessage()); + } + + // 실패 시: 동의 status 는 지워졌는데 감사 이력에는 회원 식별자가 남는 상태 + $this->app->forgetInstance(GdprUserConsentHistoryRepositoryInterface::class); + $this->assertSame( + $before, + app(GdprUserConsentRepositoryInterface::class)->getActiveByUserId($user->id)->count() + ); + } +} diff --git a/plugins/_bundled/sirsoft-gdpr/tests/scenarios/preblocker.yaml b/plugins/_bundled/sirsoft-gdpr/tests/scenarios/preblocker.yaml index 9e40033a..c6253f60 100644 --- a/plugins/_bundled/sirsoft-gdpr/tests/scenarios/preblocker.yaml +++ b/plugins/_bundled/sirsoft-gdpr/tests/scenarios/preblocker.yaml @@ -59,10 +59,11 @@ effects: - restore_replaces_blocked_script_with_fresh_one_when_granted - restore_no_op_when_consent_still_denied +test_files: + - plugins/_bundled/sirsoft-gdpr/resources/js/__tests__/preblocker.test.ts + audit: status: codified - test_files: - - resources/js/__tests__/preblocker.test.ts rules: - id: preblocker-prototype-intercept kind: prototype_setter diff --git a/plugins/_bundled/sirsoft-marketing/CHANGELOG.md b/plugins/_bundled/sirsoft-marketing/CHANGELOG.md index f819998a..e40388c7 100644 --- a/plugins/_bundled/sirsoft-marketing/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-marketing/CHANGELOG.md @@ -4,6 +4,15 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Fixed + +- 플러그인 설정 화면의 켜기/끄기 스위치가 레이아웃 편집기에서 편집 대상으로 선택되지 않던 문제를 수정했습니다. +- 채널 목록의 각 행이 같은 HTML 식별자를 공유하던 문제를 수정했습니다. +- 다크 모드에서 설정 화면의 채널 추가 버튼 색이 밝은 화면과 동일하게 표시되던 문제를 수정했습니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-10 ### Fixed diff --git a/plugins/_bundled/sirsoft-marketing/composer.json b/plugins/_bundled/sirsoft-marketing/composer.json index 843aa4ee..2d45226c 100644 --- a/plugins/_bundled/sirsoft-marketing/composer.json +++ b/plugins/_bundled/sirsoft-marketing/composer.json @@ -2,7 +2,7 @@ "name": "plugins/sirsoft-marketing", "description": "Marketing consent and subscription management plugin for Gnuboard7 platform", "type": "library", - "version": "1.0.1", + "version": "1.0.2", "autoload": { "psr-4": { "Plugins\\Sirsoft\\Marketing\\": ["src/", "./"] diff --git a/plugins/_bundled/sirsoft-marketing/package-lock.json b/plugins/_bundled/sirsoft-marketing/package-lock.json index a0b5d390..a3670458 100644 --- a/plugins/_bundled/sirsoft-marketing/package-lock.json +++ b/plugins/_bundled/sirsoft-marketing/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-marketing", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-marketing", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-marketing/package.json b/plugins/_bundled/sirsoft-marketing/package.json index c8f1ac8d..26eba533 100644 --- a/plugins/_bundled/sirsoft-marketing/package.json +++ b/plugins/_bundled/sirsoft-marketing/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-marketing", - "version": "1.0.1", + "version": "1.0.2", "description": "G7 마케팅 동의 플러그인 프론트엔드 에셋", "private": true, "type": "module", diff --git a/plugins/_bundled/sirsoft-marketing/plugin.json b/plugins/_bundled/sirsoft-marketing/plugin.json index d44d9ce7..27da4071 100644 --- a/plugins/_bundled/sirsoft-marketing/plugin.json +++ b/plugins/_bundled/sirsoft-marketing/plugin.json @@ -5,7 +5,7 @@ "ko": "마케팅 동의", "en": "Marketing Consent" }, - "version": "1.0.1", + "version": "1.0.2", "description": { "ko": "이메일 구독, 마케팅 동의, 제3자 제공 동의 등을 관리하는 플러그인", "en": "Plugin for managing email subscriptions, marketing consent, and third-party data sharing consent" diff --git a/plugins/_bundled/sirsoft-marketing/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx b/plugins/_bundled/sirsoft-marketing/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx index e2bcf60e..5af36353 100644 --- a/plugins/_bundled/sirsoft-marketing/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx +++ b/plugins/_bundled/sirsoft-marketing/resources/js/__tests__/layouts/pluginSettingsSticky.test.tsx @@ -7,9 +7,33 @@ * 적용되어 있는지 검증한다. */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, it, expect } from 'vitest'; import pluginSettingsLayout from '../../../layouts/admin/plugin_settings.json'; +/** 관리자 템플릿의 컴파일된 컴포넌트 CSS 경로 (합성 유틸리티 클래스의 실제 선언 위치). */ +const ADMIN_COMPONENTS_CSS = 'templates/_bundled/sirsoft-admin_basic/dist/css/components.css'; + +/** + * 저장소 루트(artisan 기준)를 위로 훑어 찾는다. + * + * @returns 저장소 루트 절대경로 + */ +function repoRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'artisan'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('artisan 을 가진 저장소 루트를 찾지 못했습니다.'); +} + /** 레이아웃 트리에서 주어진 id 의 노드를 찾는다. */ function findById(node: unknown, id: string): Record<string, unknown> | undefined { if (!node || typeof node !== 'object') { @@ -34,13 +58,24 @@ function classNameOf(node: Record<string, unknown> | undefined): string { } describe('plugin_settings 하단 버튼 sticky 고정', () => { - it('footer_buttons 에 sticky bottom 고정 클래스가 존재해야 한다', () => { + it('footer_buttons 가 sticky 고정 유틸리티 클래스를 사용해야 한다', () => { const footer = findById(pluginSettingsLayout, 'footer_buttons'); expect(footer).toBeDefined(); - const className = classNameOf(footer); - expect(className).toContain('sticky'); - expect(className).toContain('bottom-0'); - expect(className).toContain('z-10'); + // 레이아웃은 raw Tailwind 원자(sticky bottom-0 z-10) 대신 합성 유틸리티 한 개를 쓴다. + // 원자 이름을 직접 단언하면 합성 클래스로 옮긴 뒤 고정이 멀쩡한데도 실패한다. + expect(classNameOf(footer)).toContain('sticky-footer-buttons'); + }); + + it('그 유틸리티 클래스가 실제로 하단 고정 속성을 선언해야 한다', () => { + // 클래스 이름만 대조하면 동어반복이라, 컴파일된 CSS 에서 실제 선언을 확인한다 — + // 클래스명이 남아 있어도 선언이 사라지면 화면에서 고정이 풀리기 때문이다. + const css = fs.readFileSync(path.join(repoRoot(), ADMIN_COMPONENTS_CSS), 'utf-8'); + const rule = /\.sticky-footer-buttons\{([^}]*)\}/.exec(css); + + expect(rule).not.toBeNull(); + expect(rule![1]).toContain('position:sticky'); + expect(rule![1]).toContain('bottom:'); + expect(rule![1]).toContain('z-index:10'); }); }); diff --git a/plugins/_bundled/sirsoft-marketing/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-marketing/resources/layouts/admin/plugin_settings.json index 15f51cdc..30f33b69 100644 --- a/plugins/_bundled/sirsoft-marketing/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-marketing/resources/layouts/admin/plugin_settings.json @@ -78,7 +78,7 @@ "text": "$t:sirsoft-marketing.settings.channel_modal.enabled" }, { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{_global.editingChannel.enabled}}", @@ -549,7 +549,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "name": "marketing_consent_enabled" @@ -641,7 +641,7 @@ "name": "Button", "props": { "type": "button", - "className": "flex-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors text-sm" + "className": "flex-center gap-1.5 px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 font-medium transition-colors text-sm" }, "actions": [ { @@ -747,7 +747,7 @@ ] }, { - "id": "channels_table_rows", + "id": "channels_table_rows_{{channelIdx}}", "type": "basic", "name": "Div", "props": { @@ -843,7 +843,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "checked": "{{channel.enabled}}", @@ -1049,7 +1049,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "name": "third_party_consent_enabled" @@ -1154,7 +1154,7 @@ }, "children": [ { - "type": "basic", + "type": "composite", "name": "Toggle", "props": { "name": "info_disclosure_enabled" @@ -1330,7 +1330,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md index be69a944..5b3a6acc 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/CHANGELOG.md @@ -4,6 +4,25 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Security + +- 에스크로 배송등록·구매거절확인·해외결제(CBT) 토큰/해시 요청에 입력 형식·길이 검증을 추가했습니다. + +### Changed + +- 에스크로 배송등록 오류 안내가 다국어로 제공됩니다. +- 결제창 닫힘 보고 사유의 길이 제한을 다른 결제 플러그인과 동일하게 160자로 통일했습니다. +- 해외결제(CBT) 해시 요청에서 체크아웃 토큰 누락 응답이 표준 검증 응답(422)으로 통일되었습니다. +- 주문·결제 정보 조회가 데이터베이스 표 이름을 직접 쓰지 않고 쇼핑몰 모듈의 정의를 따르도록 내부 정리했습니다. 표시되는 내용과 동작은 종전과 동일합니다. + +### Fixed + +- 플러그인 설정 화면의 안내·상태 아이콘이 의도한 크기와 다르게 보이던 문제와, 해외결제(CBT) 연결 상태 표시에 일부 스타일이 적용되지 않던 문제를 수정했습니다. +- 결제 실패 시 이동하는 페이지 주소에 서버 내부 오류 원문이 그대로 실려 나가던 문제를 수정했습니다. 이제 안내 문구만 전달되며, 원인 파악에 필요한 원문은 서버 로그에만 기록됩니다. +- 플러그인 설정 저장과 관리자 주문 결제 조회의 해외결제 대사 재시도·편의점 결제 처리가 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.1.0] - 2026-08-10 ### Added @@ -22,6 +41,7 @@ ### Fixed +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. - 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 로고가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다. - 결제 취소 기록의 환불 금액에 원화 단위가 고정 표기되던 문제를 수정했습니다. 해외 결제(엔화 등) 취소도 실제 결제 통화로 표시됩니다. @@ -163,7 +183,6 @@ - KG 이니시스 플러그인 설정 화면의 간편결제 토글은 이커머스 주문설정으로 통합 — 일반 결제수단과 동일한 인터페이스에서 활성화/정렬/최소 주문금액을 관리할 수 있습니다. - ### Added - 일본 결제(CBT) 설정에 JPPG 결제창 표시용 가맹점명/문의처 입력 항목을 추가했습니다. diff --git a/plugins/_bundled/sirsoft-pay_kginicis/composer.json b/plugins/_bundled/sirsoft-pay_kginicis/composer.json index 950d55fb..20bff2ba 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/composer.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-pay_kginicis", "description": "KG Inicis PG Plugin for G7 platform", - "version": "1.1.0", + "version": "1.1.1", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-pay_kginicis/dist/js/plugin.iife.js index 0419790f..f5678ff6 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-pay_kginicis/dist/js/plugin.iife.js @@ -88,4 +88,3 @@ </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",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.dataset.kginicisPaymentMethodRow="admin-order",c&&u&&(c.textContent="",c.style.display="none",c.dataset.kginicisPaymentInstallmentHidden="true"),n=!0}return n}function Bt(e,t,n){const o=Array.from(e.querySelectorAll("span")).filter(i=>i.textContent?.trim()===t).find(i=>{const s=typeof i.className=="string"?i.className:"";return s.includes("rounded-full")||s.includes("font-medium")});return o?(o.textContent=n,o.dataset.kginicisPaymentMethodPatched="true",o.dataset.kginicisPaymentMethodBadge="admin-order",!0):!1}function Ut(e,t){const n=t?._pay_method_label,r=t?._embedded_pg_provider_label,o=t?._base_pay_method_label;if(!n||!r)return!1;const i=Yt(e,n),s=o?Bt(e,o,r):!1;return i||s}async function Ht(e){const t=document.getElementById("section_payment_info")??document.getElementById("payment_info_card");if(!t)return!1;const n=await Gt(e);return n&&Ut(t,n)&&console.info(`[${j}] payment method display patched on admin order detail`),!0}function Jt(e){let t=0;const n=setInterval(()=>{t++,Ht(e).then(r=>{(r||t>=30)&&clearInterval(n)})},400)}function Ft(){const e=location.pathname.match(Dt);e&&Jt(e[1])}function qt(){if(typeof window>"u")return;const e=window;if(e[Ce])return;e[Ce]=!0,console.info(`[${j}] admin order payment display injector installed`);const t=(r=1500)=>setTimeout(Ft,r);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...r)=>{n(...r),t(600)},window.addEventListener("popstate",()=>t(500))}const Ee="sirsoft-pay_kginicis",Se="__kginicisOcReceiptInjectorInstalled",Pe="kginicis-oc-receipt-btn",Wt=/^\/shop\/orders\/([^/]+)\/complete$/;function Vt(){return localStorage.getItem("auth_token")}function zt(){try{const t=sessionStorage.getItem("g7_guest_order_token");if(t)return t}catch{}const e=window.G7Core?.state?.get?.("_global")?.guestOrderToken;return typeof e=="string"&&e!==""?e:null}async function Xt(e){const t=Vt(),n=zt();if(!t&&!n)return null;const r={Accept:"application/json"};t?r.Authorization=`Bearer ${t}`:n&&(r["X-Guest-Order-Token"]=n);try{const o=await fetch(`/api/modules/sirsoft-ecommerce/user/orders/${e}`,{headers:r});return o.ok?(await o.json())?.data?.payment??null:null}catch{return null}}function Zt(e){if(!e)return!1;const t=Array.from(document.querySelectorAll("div"));for(const n of t){const r=Array.from(n.children).filter(s=>s instanceof HTMLElement&&s.tagName==="SPAN");if(r.length<2)continue;const o=r[0].textContent?.trim();if(o!=="결제 방법"&&o!=="결제수단"&&o!=="결제 방식")continue;const i=r[r.length-1];return i.textContent?.trim()===e?(n.dataset.kginicisPaymentMethodRow="true",!0):(i.textContent=e,i.dataset.kginicisPaymentMethodPatched="true",n.dataset.kginicisPaymentMethodRow="true",!0)}return!1}async function Qt(e){if(document.getElementById(Pe))return;const t=await Xt(e);if(t&&(t.pg_provider!=="kginicis"||!t.transaction_id))return;const n=await v(e);if(!C(n))return;if(t){const a=t.payment_status==="paid",c=n.receipt_type==="cbt_confirmation";if(!a&&!c)return}Zt(n.payment_method_display_label);const r=Array.from(document.querySelectorAll('button[type="button"]')).find(a=>a.className.includes("bg-blue-600"));if(!r?.parentElement)return;const o=r.parentElement;o.dataset.kginicisReceiptContainer="order-complete";const i=document.createElement("button");i.id=Pe,i.type="button",i.dataset.kginicisReceiptButton="order-complete",i.className=r.className.replace(/bg-blue-\d+/g,"bg-green-600").replace(/hover:bg-blue-\d+/g,"hover:bg-green-700"),i.textContent=A(n),i.addEventListener("click",async()=>{i.disabled=!0,i.textContent="로딩 중...";const a=await v(e);i.disabled=!1,i.textContent=A(a??n),C(a)&&we(a)});const s=o.lastElementChild;o.insertBefore(i,s),console.info(`[${Ee}] receipt button injected on order complete page`)}function en(){const e=location.pathname.match(Wt);e&&Qt(e[1])}function tn(){if(typeof window>"u")return;const e=window;if(e[Se])return;e[Se]=!0,console.info(`[${Ee}] order complete receipt injector installed`);const t=(r=1200)=>setTimeout(en,r);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...r)=>{n(...r),t()},window.addEventListener("popstate",()=>t(500))}const Ie="sirsoft-pay_kginicis",ve="__kginicisVbankInfoInjectorInstalled",Y="kginicis-mp-vbank-info",nn=/^\/shop\/orders\/([^/]+)\/complete$/,rn=/^\/mypage\/orders\/([^/]+)$/,on=/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[+\-]\d{2}:\d{2}/,an={"03":"기업은행","04":"국민은행","05":"외환은행","07":"수협중앙회",11:"농협중앙회",20:"우리은행",23:"SC 제일은행",31:"대구은행",32:"부산은행",34:"광주은행",37:"전북은행",39:"경남은행",53:"한국씨티은행",71:"우체국",81:"하나은행",88:"신한은행",89:"케이뱅크",90:"카카오뱅크",92:"토스뱅크",D1:"동양종합금융증권",D2:"현대증권",D3:"미래에셋증권",D4:"한국투자증권",D5:"우리투자증권",D6:"하이투자증권",D7:"HMC 투자증권",D8:"SK 증권",D9:"대신증권",DA:"하나대투증권",DB:"굿모닝신한증권",DC:"동부증권",DD:"유진투자증권",DE:"메리츠증권",DF:"신영증권"};function sn(e){if(e.vbank_code)return String(e.vbank_code);const t=(e.vbank_name??"").trim();if(!t)return null;for(const[n,r]of Object.entries(an))if(r===t)return n;return null}function cn(){try{const t=window.G7Core?.getState;return t?.()?.currentDataContext?._global?.currentUser?.is_admin===!0}catch{return!1}}function Ae(e){try{return new Date(e).toLocaleString("ko-KR",{year:"numeric",month:"long",day:"numeric",hour:"2-digit",minute:"2-digit",timeZone:"Asia/Seoul"})}catch{return e}}function dn(){const e=document.createTreeWalker(document.body,NodeFilter.SHOW_TEXT),t=[];let n=e.nextNode();for(;n;){const r=n.textContent??"",o=r.match(on);o&&t.push({node:n,formatted:r.replace(o[0],Ae(o[0]))}),n=e.nextNode()}for(const{node:r,formatted:o}of t)r.textContent=o}function ln(e){try{const n=window.G7Core?.getState,i=n?.()?.currentDataContext?.order?.data;return!i||i.order_number!==e?null:i}catch{return null}}function un(){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=>r.textContent?.includes("결제 정보"));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 _(e,t,n=!1){const r=document.createElement("div");r.className="flex items-center justify-between";const o=document.createElement("span");o.className="text-gray-500 dark:text-gray-400 text-sm",o.textContent=e;const i=document.createElement("span");return i.className=n?"text-sm font-medium text-blue-600 dark:text-blue-400":"text-sm text-gray-900 dark:text-gray-100",i.textContent=t,r.appendChild(o),r.appendChild(i),r}function mn(e,t){const n=document.createElement("div");n.id=Y,n.className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-2";const r=document.createElement("p");if(r.className="text-sm font-semibold text-gray-700 dark:text-gray-300",r.textContent="가상계좌 입금 안내",n.appendChild(r),e.vbank_name&&n.appendChild(_("은행",e.vbank_name)),cn()){const o=sn(e);o&&n.appendChild(_("은행코드",o))}return e.vbank_number&&n.appendChild(_("계좌번호",e.vbank_number)),e.vbank_holder&&n.appendChild(_("예금주",e.vbank_holder)),t&&n.appendChild(_("입금 기한",t,!0)),n}function g(e,t){return e?.receipt_fields?.find(n=>n.label===t)?.value??""}function fn(e,t){if(t?.receipt_type!=="cbt_confirmation")return!1;if(g(t,"결제수단").includes("편의점"))return!0;const r=String(e.transaction_id??"");return String(e.currency??"").toUpperCase()==="JPY"&&(String(e.vbank_name??"").toUpperCase()==="CVS"||r.startsWith("INIJPGCVS"))}function pn(e,t,n){const r=document.createElement("div");r.id=Y,r.className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 space-y-2";const o=document.createElement("p");o.className="text-sm font-semibold text-gray-700 dark:text-gray-300",o.textContent="일본 편의점 입금 안내",r.appendChild(o);const i=g(t,"입금 상태"),s=g(t,"입금예정금액")||g(t,"결제금액"),a=g(t,"편의점 코드"),c=g(t,"편의점 확인번호")||String(e.vbank_number??""),u=g(t,"편의점 접수번호"),d=g(t,"입금 마감일시")||n;return i&&r.appendChild(_("상태",i)),s&&r.appendChild(_("입금예정금액",s,!0)),a&&r.appendChild(_("편의점 코드",a)),c&&r.appendChild(_("편의점 확인번호",c)),u&&r.appendChild(_("편의점 접수번호",u)),d&&r.appendChild(_("입금 마감일시",d,!0)),r}async function yn(e){const t=ln(e);if(!t)return!1;const{payment:n}=t;if(!n||n.pg_provider!=="kginicis"||n.payment_status!=="waiting_deposit"||!n.vbank_number||document.getElementById(Y))return!0;const r=un();if(!r)return!1;const o=await v(e),i=n.vbank_due_at?Ae(n.vbank_due_at):"";return r.appendChild(fn(n,o)?pn(n,o,i):mn(n,i)),console.info(`[${Ie}] vbank info injected on mypage`),!0}function _n(e){let t=0;const n=setInterval(()=>{t++,yn(e).then(r=>{(r||t>=30)&&clearInterval(n)})},400)}function hn(){if(location.pathname.match(nn)){setTimeout(dn,500);return}const t=location.pathname.match(rn);t&&_n(t[1])}function gn(){if(typeof window>"u")return;const e=window;if(e[ve])return;e[ve]=!0,console.info(`[${Ie}] vbank info injector installed`);const t=(r=800)=>setTimeout(hn,r);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...r)=>{n(...r),t(600)},window.addEventListener("popstate",()=>t(500))}const Te="sirsoft-pay_kginicis",xe="__sirsoftKginicisCheckoutJpyPaymentMethodRestrictorInstalled",B=/^\/shop\/checkout\/?$/,wn="/api/modules/sirsoft-ecommerce/checkout",bn="/api/modules/sirsoft-ecommerce/settings/payment",kn="/api/modules/sirsoft-ecommerce/payments/client-config/kginicis",Re="__sirsoftKginicisCheckoutCurrency",U=new Set(["card","kginicis_japan_paypay","kginicis_japan_cvs"]),H=new Set(["kginicis_japan_paypay","kginicis_japan_cvs"]),Me=new Set(["card","vbank","dbank","bank","phone","kginicis_samsung_pay","kginicis_naverpay","kginicis_lpay","kginicis_kakaopay","kginicis_japan_paypay","kginicis_japan_cvs"]),Cn=[{id:"card",labels:["신용카드","Credit Card","クレジットカード"]},{id:"vbank",labels:["가상계좌","Virtual Account","バーチャル口座"]},{id:"dbank",labels:["무통장입금","Bank Transfer","銀行振込"]},{id:"bank",labels:["계좌이체","Account Transfer","口座振替"]},{id:"phone",labels:["휴대폰결제","Mobile Payment","携帯電話決済"]},{id:"kginicis_naverpay",labels:["네이버페이 (KG이니시스)","Naver Pay (KG Inicis)"]},{id:"kginicis_samsung_pay",labels:["삼성페이 (KG이니시스)","Samsung Pay (KG Inicis)"]},{id:"kginicis_lpay",labels:["L.pay (KG이니시스)","L.pay (KG Inicis)"]},{id:"kginicis_kakaopay",labels:["카카오페이 (KG이니시스)","Kakao Pay (KG Inicis)"]},{id:"kginicis_japan_paypay",labels:["PayPay (일본 KG이니시스)","PayPay (KG Inicis Japan)"]},{id:"kginicis_japan_cvs",labels:["일본 편의점결제 (KG이니시스)","Japan Convenience Store (KG Inicis)"]}],J={info:(...e)=>console.info(`[${Te}]`,...e),warn:(...e)=>console.warn(`[${Te}]`,...e)};let S=null,F=null,b=null;function q(){return window}function En(e){return typeof e=="string"?e:e instanceof URL?e.toString():typeof Request<"u"&&e instanceof Request?e.url:String(e)}function Sn(e,t){return t?.method?t.method.toUpperCase():typeof Request<"u"&&e instanceof Request?e.method.toUpperCase():"GET"}function Pn(e){try{return new URL(e,window.location.origin).pathname}catch{return e.split("?")[0].split("#")[0]}}function Ne(e){if(typeof e!="string")return null;const t=e.trim().toUpperCase();return/^[A-Z]{3}$/.test(t)?t:null}function W(e,t){return t.split(".").reduce((n,r)=>{if(!(n===null||typeof n!="object"))return n[r]},e)}function In(e,t){for(const n of t){const r=Ne(W(e,n));if(r)return r}return null}function vn(e){return In(e,["data.calculation.summary.selected_payment_currency","data.calculation.metadata.payment_currency","data.calculation.metadata.currency","data.currency","data.currency_code"])}function An(e){const t=vn(e);t&&(q()[Re]=t)}function Le(){const e=q(),n=e.__templateApp?.globalState,r=[e[Re],n?.preferredCurrency];try{r.push(window.localStorage?.getItem("g7_preferred_currency"))}catch{}r.push(W(n,"modules.sirsoft-ecommerce.language_currency.default_currency"),W(n,"modules.sirsoft-ecommerce.language_currency.currencies.0.code"));for(const o of r){const i=Ne(o);if(i)return i}return null}function V(){return B.test(window.location.pathname)&&Le()==="JPY"}function z(){if(!B.test(window.location.pathname))return!1;const e=Le();return e!==null&&e!=="JPY"}async function Oe(e){return S!==null||(S=(async()=>{try{const t=await e(kn,{headers:{Accept:"application/json"}});return t.ok?(await t.json()).data?.japan_restrict_jpy_payment_methods===!0:!1}catch{return!1}})()),S}function Tn(e){const t=e.data?.order_settings?.payment_methods;return Array.isArray(t)?{...e,data:{...e.data,order_settings:{...e.data?.order_settings,payment_methods:t.map(n=>{const r=typeof n.id=="string"?n.id:"";return!Me.has(r)||U.has(r)?n:{...n,is_active:!1,_kginicis_restricted_for_jpy:!0}})}}}:e}function xn(e){const t=e.data?.order_settings?.payment_methods;return Array.isArray(t)?{...e,data:{...e.data,order_settings:{...e.data?.order_settings,payment_methods:t.map(n=>{const r=typeof n.id=="string"?n.id:"";return H.has(r)?{...n,is_active:!1,_kginicis_restricted_for_non_jpy:!0}:n})}}}:e}function Rn(e,t){const n=new Headers(e.headers);return n.set("Content-Type","application/json"),new Response(JSON.stringify(t),{status:e.status,statusText:e.statusText,headers:n})}function Mn(){const e=Array.from(document.querySelectorAll("h2")).find(n=>{const r=n.textContent??"";return r.includes("결제 수단")||r.includes("결제 방법")||r.includes("Payment Method")});if(!e)return null;let t=e.parentElement;for(;t&&t!==document.body;){if(t.tagName==="DIV"&&String(t.className).includes("rounded-lg"))return t;t=t.parentElement}return null}function Nn(e){const t=e.querySelector("p")?.textContent?.trim()??e.textContent?.trim()??"";for(const n of Cn)if(n.labels.some(r=>t.includes(r)))return n.id;return null}function X(){const e=V(),t=z();if(!e&&!t)return!1;const n=Mn();if(!n)return!1;let r=!1;if(n.querySelectorAll('button[type="button"]').forEach(o=>{const i=Nn(o);if(!i||!Me.has(i))return;if(e?U.has(i):!H.has(i)){o.style.removeProperty("display"),o.disabled=!1,o.removeAttribute("aria-hidden");return}o.style.display="none",o.disabled=!0,o.setAttribute("aria-hidden","true"),e?o.dataset.kginicisRestrictedForJpy="true":o.dataset.kginicisRestrictedForNonJpy="true",r=!0}),r)try{const i=window.G7Core?.state?.getLocal?.()?.paymentMethod;typeof i=="string"&&(e&&!U.has(i)||t&&H.has(i))&&window.G7Core?.state?.setLocal?.({paymentMethod:"card"})}catch{}return r}async function $e(e){if(!B.test(window.location.pathname))return;const t=V(),n=z();if(!t&&!n||t&&!await Oe(e))return;X(),F===null&&(F=new MutationObserver(()=>{X()}),F.observe(document.body,{childList:!0,subtree:!0})),b!==null&&clearInterval(b);let r=0;b=setInterval(()=>{r++,X(),r>=50&&b!==null&&(clearInterval(b),b=null)},200)}function De(e){S=null,$e(e)}function Ln(){if(typeof window>"u"||typeof window.fetch!="function")return;const e=q();if(e[xe])return;e[xe]=!0;const t=window.fetch.bind(window);window.fetch=async function(i,s){const a=En(i),c=Sn(i,s),u=Pn(a),d=await t(i,s);if(c==="GET"&&u.endsWith(wn)){try{const f=await d.clone().json();An(f)}catch{}return d}if(c!=="GET"||!u.endsWith(bn))return d;const l=V(),m=z();if(!l&&!m||l&&!await Oe(t))return d;try{const f=await d.clone().json(),p=l?Tn(f):xn(f);return J.info(l?"restricted checkout payment methods for JPY order":"hid Japan-only payment methods for non-JPY order"),Rn(d,p)}catch(f){return J.warn("failed to restrict checkout payment methods",f),d}},J.info("checkout JPY payment method restrictor installed");const n=()=>{$e(t)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",n):n();const r=history.pushState.bind(history);history.pushState=(...o)=>{r(...o),setTimeout(()=>De(t),200)},window.addEventListener("popstate",()=>setTimeout(()=>De(t),200))}const On="sirsoft-pay_kginicis",Ke="__kginicisAdminPaymentMethodBrandInjectorInstalled",Ge="__kginicisAdminPaymentMethodBrandSyncListenerAttached",$n=/^\/admin\/ecommerce\/settings\/?$/,je='[data-kginicis-admin-payment-brand-mark="true"]',Dn=200,Kn=120,Gn=[{id:"kginicis_naverpay",labels:["네이버페이 (KG이니시스)","Naver Pay (KG Inicis)"],shortLabels:[],markLines:["N","Pay"],markClassName:"bg-green-500 text-white"},{id:"kginicis_samsung_pay",labels:["삼성페이 (KG이니시스)","Samsung Pay (KG Inicis)"],shortLabels:[],markLines:["S","Pay"],markClassName:"bg-blue-700 text-white"},{id:"kginicis_lpay",labels:["L.pay (KG이니시스)","L.pay (KG Inicis)"],shortLabels:[],markLines:["L","pay"],markClassName:"bg-red-600 text-white"},{id:"kginicis_kakaopay",labels:["카카오페이 (KG이니시스)","Kakao Pay (KG Inicis)"],shortLabels:[],markLines:["Kakao","Pay"],markClassName:"bg-yellow-400 text-gray-950"},{id:"kginicis_japan_paypay",labels:["PayPay (일본 KG이니시스)","PayPay (KG Inicis Japan)"],shortLabels:[],markLines:["Pay","Pay"],markClassName:"bg-red-600 text-white"},{id:"kginicis_japan_cvs",labels:["일본 편의점결제 (KG이니시스)","Japan Convenience Store (KG Inicis)"],shortLabels:[],markLines:["CVS","JP"],markClassName:"bg-blue-600 text-white"}];let Ye=null,T=null,Z=!1;function jn(){return window}function Q(){if(!$n.test(window.location.pathname))return!1;const e=new URLSearchParams(window.location.search).get("tab");return e===null||e===""||e==="order_settings"}function Yn(e){return e instanceof HTMLElement?e.className:""}function Be(e){return(e??"").replace(/\u200B/g,"").replace(/\s+/g," ").trim()}function Bn(e){const t=Yn(e);return t.includes("excel-card")||t.includes("flex-center")&&t.includes("border")&&t.includes("gap-4")}function Ue(e){return e.querySelector(".font-medium")}function Un(e){const t=Be(Ue(e)?.textContent),n=Be(e.textContent);return Gn.find(r=>r.shortLabels.some(o=>t===o)||r.labels.some(o=>n.includes(o)))??null}function Hn(e){return Array.from(e.querySelectorAll("div")).filter(Bn)}function Jn(e){return Array.from(e.querySelectorAll("svg, [data-icon], i")).find(n=>!n.closest("[data-drag-handle]")&&!n.closest(je)&&!n.closest('.row-stack, .excel-card-body, button, select, [role="switch"]'))??null}function He(e,t){e.dataset.kginicisAdminPaymentBrandMark="true",e.dataset.kginicisAdminPaymentMethod=t.id,e.setAttribute("aria-hidden","true"),e.className=`inline-flex items-center justify-center rounded-lg font-bold ${t.markClassName}`,e.style.width="32px",e.style.height="32px",e.style.flex="0 0 32px",e.style.flexDirection="column",e.style.lineHeight="1",e.style.fontSize="9px";const n=t.markLines.join("");e.textContent!==n&&(e.replaceChildren(),t.markLines.forEach(r=>{const o=document.createElement("span");o.style.lineHeight="1",o.textContent=r,e.appendChild(o)}))}function Fn(e){const t=document.createElement("span");return He(t,e),t}function qn(e,t){const n=e.querySelector(je);if(n instanceof HTMLSpanElement)return e.dataset.kginicisAdminPaymentMethod=t.id,He(n,t),!0;const r=Fn(t),o=Jn(e);if(o&&o.parentElement)return e.dataset.kginicisAdminPaymentMethod=t.id,o.replaceWith(r),!0;const i=Ue(e);return i&&i.parentElement?(e.dataset.kginicisAdminPaymentMethod=t.id,i.parentElement.insertBefore(r,i),!0):!1}function ee(e=document){if(typeof window>"u"||typeof document>"u"||!Q())return!1;let t=!1;return Hn(e).forEach(n=>{const r=Un(n);r&&qn(n,r)&&(t=!0)}),t}function Wn(){Z||(Z=!0,window.setTimeout(()=>{Z=!1,ee()},0))}function te(){T!==null&&(window.clearInterval(T),T=null)}function ne(){if(!Q())return;te(),ee();let e=0;T=window.setInterval(()=>{e+=1,ee(),e>=Kn&&te()},Dn);const t=document.body;t[Ge]||(t[Ge]=!0,Ye=new MutationObserver(()=>Wn()),Ye.observe(document.body,{childList:!0,subtree:!0,characterData:!0}))}function re(){if(Q()){ne();return}te()}function Vn(){if(typeof window>"u"||typeof document>"u")return;const e=jn();if(e[Ke])return;e[Ke]=!0,console.info(`[${On}] admin payment method brand sync installed`),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>ne()):ne();const t=history.pushState.bind(history);history.pushState=(...r)=>{t(...r),window.setTimeout(()=>re(),200)};const n=history.replaceState.bind(history);history.replaceState=(...r)=>{n(...r),window.setTimeout(()=>re(),200)},window.addEventListener("popstate",()=>window.setTimeout(()=>re(),200))}const k="sirsoft-pay_kginicis",oe={info:(...e)=>console.info(`[${k}]`,...e),warn:(...e)=>console.warn(`[${k}]`,...e),error:(...e)=>console.error(`[${k}]`,...e)};function Je(){const e=window.G7Core;if(!e)return 0;const t=e.getActionDispatcher;if(typeof t!="function")return 0;const n=t();if(!n||typeof n.registerHandler!="function")return 0;let r=0;for(const[o,i]of Object.entries(_e)){const s=`${k}.${o}`;n.registerHandler(s,i,{category:"plugin",source:k}),r++}return r}function Fe(){const e=()=>{const t=Je();if(t>0){oe.info(`${t} handler(s) registered`);return}let n=0;const r=50,o=setInterval(()=>{n++;const i=Je();if(i>0){clearInterval(o),oe.info(`${i} handler(s) registered (after ${n} retries)`);return}n>=r&&(clearInterval(o),oe.warn("ActionDispatcher not available after timeout"))},100)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()}Et(),Ln(),$t(),qt(),Vn(),tn(),gn(),at(),Fe(),window.__SirsoftKginicis={identifier:k,handlers:Object.keys(_e),initPlugin:Fe}})(); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md index 19fe4554..2cf61b5d 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/orders.md @@ -820,6 +820,19 @@ HTTP/1.1 200 | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| invoice | body | string | 예 | max 40 | 운송장번호 (KG 이니시스 규격 40자) | +| ex_code | body | string | 예 | 공식 택배사 코드표 | 택배사 코드 — `formData` 응답의 `courier_codes`(컨트롤러 상수 `COURIER_CODES`)에 존재하는 코드만 허용 | +| report | body | string | 아니오 | `I`, `U` | 등록/수정 구분 (I 최초 등록 / U 수정). 미전송·허용값 밖이면 컨트롤러 기본값 `I` 로 흡수 | +| charge | body | string | 아니오 | `SH`, `BH` | 배송비 부담 구분 (SH 판매자 / BH 구매자). 미전송·허용값 밖이면 컨트롤러 기본값 `SH` 로 흡수 | +| recv_name | body | string | 아니오 | max 30 | 수령인명 (미전송 시 배송지 DB 값으로 채움) | +| recv_tel | body | string | 아니오 | max 20 | 수령인 연락처 (미전송 시 배송지 DB 값으로 채움) | +| recv_post | body | string | 아니오 | max 10 | 수령인 우편번호 (미전송 시 배송지 DB 값으로 채움) | +| recv_addr | body | string | 아니오 | max 200 | 수령인 주소 (미전송 시 배송지 DB 값으로 채움) | +| regist_name | body | string | 아니오 | max 30 | 등록자명 | +| send_name | body | string | 아니오 | max 30 | 발송인명 | +| send_tel | body | string | 아니오 | max 20 | 발송인 연락처 | +| send_post | body | string | 아니오 | max 10 | 발송인 우편번호 | +| send_addr | body | string | 아니오 | max 200 | 발송인 주소 | **요청 예시** @@ -828,6 +841,14 @@ POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-deliver Host: api.example.com Accept: application/json Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "invoice": "1234567890123", + "ex_code": "cjgls", + "report": "I", + "charge": "SH" +} ``` **응답 필드** (`data` 내부) @@ -867,14 +888,14 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | | 404 | Not Found | 해당 주문에 KG 이니시스 에스크로 결제(`is_escrow = true` + TID 존재)가 없는 경우 | -| 422 | Unprocessable | `invoice` 미입력 또는 `ex_code` 가 공식 택배사 코드표에 없는 경우 (errors: `invoice` / `ex_code`) | +| 422 | Unprocessable | 요청 파라미터가 `EscrowDeliveryRegisterRequest` 검증 규칙을 위반한 경우 (표준 validation 응답, `error.errors` 에 필드별 메시지) — `invoice` 미입력·40자 초과(`운송장번호를 입력해주세요.`), `ex_code` 미입력·공식 택배사 코드표에 없음(`택배사를 선택해주세요.` — 다국어 키 `sirsoft-pay_kginicis::messages.escrow.*`), `report`/`charge` 허용값 위반, `recv_*`/`send_*`/`regist_name` 길이 상한 초과 등 | | 500 | Server Error | 배송등록 호출 중 예외 발생 (errors.message = 예외 메시지) | | 502 | Bad Gateway | KG 이니시스 배송등록 응답의 `resultCode` 가 `00` 이 아닌 경우 (errors.message = PG `resultMsg`) | <!-- @generated:end --> **설명** -`AdminEscrowDeliveryController@register` 가 KG 이니시스 에스크로 결제의 배송정보를 INIAPI 에 등록한다. 운송장번호(`invoice`)와 택배사코드(`ex_code`, 공식 코드표에 존재해야 함)를 필수 검증하고, 수령인 정보는 요청값 우선·부재 시 배송지 DB 로 채운다. 에스크로 결제는 반드시 에스크로 자격증명(`useEscrowCredentials`)으로 `registerEscrowDelivery` 를 호출하며, 성공 시 정제된 PG 응답과 배송 이력을 `payment_meta.escrow_delivery` 에 저장한다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, 입력 검증 실패 422 / 에스크로 결제 미존재 404 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. +`AdminEscrowDeliveryController@register` 가 KG 이니시스 에스크로 결제의 배송정보를 INIAPI 에 등록한다. 형식·길이·택배사 코드 검증은 `EscrowDeliveryRegisterRequest` 가 담당한다 — 운송장번호(`invoice`, 필수·최대 40자)와 택배사코드(`ex_code`, 필수·공식 코드표 `COURIER_CODES` 내 값), 등록/수정 구분(`report`: I/U)·배송비 부담(`charge`: SH/BH, 미전송·비허용 값은 컨트롤러 기본값 I/SH 로 흡수), 수령인·발송인 필드 길이 상한(이름 30자/전화 20자/우편번호 10자/주소 200자)을 검증하며 위반 시 표준 validation 422(다국어 메시지)로 거부한다. 수령인 정보는 요청값 우선·부재 시 배송지 DB 로 채운다. 에스크로 결제는 반드시 에스크로 자격증명(`useEscrowCredentials`)으로 `registerEscrowDelivery` 를 호출하며, 성공 시 정제된 PG 응답과 배송 이력을 `payment_meta.escrow_delivery` 에 저장한다. `sirsoft-ecommerce.orders.update` 권한이 필요하고, 입력 검증 실패 422 / 에스크로 결제 미존재 404 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. ### POST /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/escrow-deny-confirm @@ -888,6 +909,7 @@ HTTP/1.1 200 | 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | | --- | --- | --- | --- | --- | --- | | orderNumber | path | string | 예 | — | 대상 order number의 식별자 | +| dcnf_name | body | string | 아니오 | max 20 | 구매거절 확인자명 (KG 이니시스 확인자명 규격 20자 — 초과·문자열 아님은 422, `EscrowDenyConfirmRequest` 검증). 미입력 시 다국어 기본값(`sirsoft-pay_kginicis::messages.escrow.default_confirmer`, ko: `관리자`)으로 채움 | **요청 예시** @@ -931,14 +953,14 @@ HTTP/1.1 200 | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.update`)이 없는 경우 | | 404 | Not Found | 해당 주문에 KG 이니시스 에스크로 결제(`is_escrow = true` + TID 존재)가 없는 경우 | -| 422 | Unprocessable | 이미 구매거절확인 이력(`payment_meta.escrow_deny_confirm`)이 있는 경우 (errors.message = "이미 구매거절확인이 완료되었습니다.") | +| 422 | Unprocessable | `dcnf_name` 이 형식 검증(문자열, 최대 20자)을 위반한 경우 (표준 validation 응답, `error.errors.dcnf_name`) / 이미 구매거절확인 이력(`payment_meta.escrow_deny_confirm`)이 있는 경우 (errors.message = "이미 구매거절확인이 완료되었습니다.") | | 500 | Server Error | 구매거절확인 호출 중 예외 발생 (errors.message = 예외 메시지) | | 502 | Bad Gateway | KG 이니시스 구매거절확인 응답의 `resultCode` 가 `00` 이 아닌 경우 (errors.message = PG `resultMsg`) | <!-- @generated:end --> **설명** -`AdminEscrowDenyConfirmController@confirm` 이 구매자가 구매거절을 선택한 에스크로 주문에 대해 판매자(관리자) 측 거절 확인을 처리한다(INIAPI v1 `type=Dncf`). 대상 에스크로 결제(`is_escrow = true`)를 찾아 이미 거절확인 이력(`payment_meta.escrow_deny_confirm`)이 있으면 중복 처리 없이 422 로 막고, 에스크로 자격증명으로 `denyConfirmEscrow` 를 호출한다. 성공 시 확인 시각과 담당자명(`dcnf_name`, 기본 "관리자"), 정제된 PG 응답을 메타에 기록한다. `sirsoft-ecommerce.orders.update` 권한이 필요하며, 에스크로 결제 미존재 404 / 이미 확인됨 422 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. +`AdminEscrowDenyConfirmController@confirm` 이 구매자가 구매거절을 선택한 에스크로 주문에 대해 판매자(관리자) 측 거절 확인을 처리한다(INIAPI v1 `type=Dncf`). 대상 에스크로 결제(`is_escrow = true`)를 찾아 이미 거절확인 이력(`payment_meta.escrow_deny_confirm`)이 있으면 중복 처리 없이 422 로 막고, 에스크로 자격증명으로 `denyConfirmEscrow` 를 호출한다. 성공 시 확인 시각과 담당자명(`dcnf_name`, 최대 20자 — `EscrowDenyConfirmRequest` 가 형식 검증하며 미입력 시 다국어 기본값 `escrow.default_confirmer`, ko: "관리자"), 정제된 PG 응답을 메타에 기록한다. `sirsoft-ecommerce.orders.update` 권한이 필요하며, 에스크로 결제 미존재 404 / 이미 확인됨 422 / PG 실패(resultCode≠`00`) 502 / 예외 500 으로 매핑된다. ### GET /api/plugins/sirsoft-pay_kginicis/admin/orders/{orderNumber}/transaction-status diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md index a75bb675..b7ab54f7 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/payment.md @@ -25,7 +25,12 @@ **요청 파라미터** -_요청 파라미터 없음._ +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| oid | body | string | 예 | max 40 | 주문번호 (`CbtCheckoutTokenRequest` 검증 — 40자 초과·문자열 아님은 422) | +| price | body | integer | 예 | min 1 | 결제 금액 (하한은 국내 결제와 동일한 `PaymentLimits::MIN_PRICE` SSoT — JPY 도 1 단위 결제 성립) | +| buyer_email | body | string | 아니오 | max 255 | 구매자 이메일 (주문 구매자 대조용) | +| buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호 (주문 구매자 대조용) | **요청 예시** @@ -33,6 +38,13 @@ _요청 파라미터 없음._ POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/checkout-token HTTP/1.1 Host: api.example.com Accept: application/json +Content-Type: application/json + +{ + "oid": "ORD20260714001", + "price": 10000, + "buyer_email": "buyer@example.com" +} ``` **응답 필드** (`data` 내부) @@ -62,14 +74,14 @@ _이 엔드포인트는 `ResponseHelper` 봉투를 쓰지 않고 `response()->js | --- | --- | --- | | 403 | Forbidden | 요청의 구매자 정보(`buyer_email` / `buyer_phone`)가 주문의 구매자와 일치하지 않는 경우 | | 404 | Not Found | `oid` 에 해당하는 주문이 없는 경우 | -| 422 | Unprocessable Entity | `oid` / `price` 누락, 일본 CBT 결제 비활성화·미설정, 주문이 결제 가능 상태가 아님, 주문 통화가 JPY 가 아님, 금액이 주문 청구액과 불일치 | +| 422 | Unprocessable Entity | 요청 파라미터가 `CbtCheckoutTokenRequest` 검증 규칙(oid 필수·max 40 / price 필수·정수·min 1 / buyer_email max 255 / buyer_phone max 30)을 위반한 경우 (표준 validation 응답, `error.errors` 에 필드별 메시지) / 일본 CBT 결제 비활성화·미설정, 주문이 결제 가능 상태가 아님, 주문 통화가 JPY 가 아님, 금액이 주문 청구액과 불일치 | | 429 | Too Many Requests | 동일 IP·`oid` 조합에서 분당 10회를 초과해 요청한 경우 | <!-- @generated:end --> **설명** -일본 CBT(국경 간) 결제 흐름의 첫 단계로, 프론트엔드 결제창이 후속 해시 생성 요청에 사용할 단기 체크아웃 토큰을 발급합니다. `CbtCheckoutTokenService::issue()` 가 주문번호·금액·구매자(이메일/전화)·요청 IP·User-Agent 를 HMAC-SHA256 으로 봉인한 서명 토큰을 만들어 반환하며, 이 토큰은 hash-data 단계에서 결제 컨텍스트가 위변조되지 않았는지 검증하는 데 쓰입니다. 인증은 필요 없고(결제창에서 직접 호출), 대신 `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정 여부, 주문 존재·결제 가능 상태·통화 JPY 여부·구매자 일치·금액 일치를 순차 검증합니다. 필수 파라미터(`oid`, `price`) 누락 시 422, 레이트리밋 초과 시 429, 주문 미존재 시 404, 구매자 검증 실패 시 403, 그 외 결제 불가 조건은 422 로 응답합니다. +일본 CBT(국경 간) 결제 흐름의 첫 단계로, 프론트엔드 결제창이 후속 해시 생성 요청에 사용할 단기 체크아웃 토큰을 발급합니다. `CbtCheckoutTokenService::issue()` 가 주문번호·금액·구매자(이메일/전화)·요청 IP·User-Agent 를 HMAC-SHA256 으로 봉인한 서명 토큰을 만들어 반환하며, 이 토큰은 hash-data 단계에서 결제 컨텍스트가 위변조되지 않았는지 검증하는 데 쓰입니다. 인증은 필요 없고(결제창에서 직접 호출), 대신 `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정 여부, 주문 존재·결제 가능 상태·통화 JPY 여부·구매자 일치·금액 일치를 순차 검증합니다. 요청 형식·길이는 `CbtCheckoutTokenRequest` 가 국내 짝(`SignatureRequest`)과 동일 강도로 검증하며(`oid` max 40, `price` 하한은 `PaymentLimits::MIN_PRICE`), 위반 시 표준 validation 422, 레이트리밋 초과 시 429, 주문 미존재 시 404, 구매자 검증 실패 시 403, 그 외 결제 불가 조건은 422 로 응답합니다. ### POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data @@ -80,7 +92,14 @@ _이 엔드포인트는 `ResponseHelper` 봉투를 쓰지 않고 `response()->js **요청 파라미터** -_요청 파라미터 없음._ +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| oid | body | string | 예 | max 40 | 주문번호 (`CbtHashDataRequest` 검증) | +| price | body | integer | 예 | min 1 | 결제 금액 (하한은 `PaymentLimits::MIN_PRICE` SSoT) | +| timestamp | body | string | 예 | max 20 | 결제창 타임스탬프 (해시 재료 — 신선도 검사 대상) | +| checkout_token | body | string | 예 | max 1024 | checkout-token 단계에서 발급받은 체크아웃 토큰. **필수 검증으로 승격** — 누락·빈 값은 422 (종전에는 토큰 검증 단계에서 403) | +| buyer_email | body | string | 아니오 | max 255 | 구매자 이메일 (주문 구매자 대조용) | +| buyer_phone | body | string | 아니오 | max 30 | 구매자 전화번호 (주문 구매자 대조용) | **요청 예시** @@ -88,6 +107,14 @@ _요청 파라미터 없음._ POST /api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data HTTP/1.1 Host: api.example.com Accept: application/json +Content-Type: application/json + +{ + "oid": "ORD20260714001", + "price": 10000, + "timestamp": "20260714120000123", + "checkout_token": "eyJvaWQiOiJPUkQyMDI2MDcxNDAwMSIsInByaWNlIjoxMDAwMH0.5f1c…" +} ``` **응답 필드** (`data` 내부) @@ -117,14 +144,14 @@ _이 엔드포인트는 `ResponseHelper` 봉투를 쓰지 않고 `response()->js | --- | --- | --- | | 403 | Forbidden | 구매자 정보가 주문과 불일치하거나, checkout-token 검증에 실패한 경우(토큰 위조·컨텍스트 불일치) | | 404 | Not Found | `oid` 에 해당하는 주문이 없는 경우 | -| 422 | Unprocessable Entity | `oid` / `price` / `timestamp` 누락, 타임스탬프 만료·형식 오류(재생 공격 방지), 일본 CBT 결제 비활성화·미설정, 주문이 결제 가능 상태가 아님, 주문 통화가 JPY 가 아님, 금액이 주문 청구액과 불일치 | +| 422 | Unprocessable Entity | 요청 파라미터가 `CbtHashDataRequest` 검증 규칙(oid 필수·max 40 / price 필수·정수·min 1 / timestamp 필수·max 20 / checkout_token 필수·max 1024)을 위반한 경우 (표준 validation 응답 — **계약 변경**: 종전에는 `checkout_token` 이 비어 있으면 토큰 검증 단계에서 403 이었으나 필수 검증 승격으로 422 가 된다) / 타임스탬프 만료·형식 오류(재생 공격 방지), 일본 CBT 결제 비활성화·미설정, 주문이 결제 가능 상태가 아님, 주문 통화가 JPY 가 아님, 금액이 주문 청구액과 불일치 | | 429 | Too Many Requests | 동일 IP·`oid` 조합에서 분당 10회를 초과해 요청한 경우 | <!-- @generated:end --> **설명** -일본 CBT 결제창이 실제로 KG 이니시스에 전송할 위변조 방지 해시(P_HASHDATA)를 생성해 반환합니다. `KgInicisApiService::generateCbtHashData()` 가 일본 가맹점 MID·타임스탬프·금액·주문번호로 해시를 만들며, 인증은 불필요하지만 checkout-token 단계에서 발급한 토큰을 `CbtCheckoutTokenService::verify()` 로 재검증해 동일한 결제 컨텍스트(주문·금액·구매자·IP·UA)에서 온 요청임을 보장합니다. 재생 공격을 막기 위해 타임스탬프 신선도(`isTimestampFresh`)를 확인하고, `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정·주문 상태·통화 JPY·구매자 일치·금액 일치를 검증합니다. 파라미터(`oid`, `price`, `timestamp`) 누락·타임스탬프 만료·금액 불일치 등은 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 또는 토큰 검증 실패는 403 으로 응답합니다. +일본 CBT 결제창이 실제로 KG 이니시스에 전송할 위변조 방지 해시(P_HASHDATA)를 생성해 반환합니다. `KgInicisApiService::generateCbtHashData()` 가 일본 가맹점 MID·타임스탬프·금액·주문번호로 해시를 만들며, 인증은 불필요하지만 checkout-token 단계에서 발급한 토큰을 `CbtCheckoutTokenService::verify()` 로 재검증해 동일한 결제 컨텍스트(주문·금액·구매자·IP·UA)에서 온 요청임을 보장합니다. 재생 공격을 막기 위해 타임스탬프 신선도(`isTimestampFresh`)를 확인하고, `oid` 기준 IP별 분당 10회 레이트리밋과 일본 결제 활성화·설정·주문 상태·통화 JPY·구매자 일치·금액 일치를 검증합니다. 요청 형식·길이는 `CbtHashDataRequest` 가 검증합니다 — 파라미터(`oid`, `price`, `timestamp`, `checkout_token`) 누락·길이 초과·타임스탬프 만료·금액 불일치 등은 422, 레이트리밋 초과 429, 주문 미존재 404, 구매자 검증 또는 (비어 있지 않은 토큰의) 토큰 검증 실패는 403 으로 응답합니다. 계약 변경: 종전에는 `checkout_token` 이 빈 값이면 토큰 검증 단계에서 403 이었으나, 필수 검증으로 승격되어 지금은 422 입니다 (클라이언트는 응답의 `.ok` 만 검사하므로 영향 없음). ### POST /api/plugins/sirsoft-pay_kginicis/payment/close-report @@ -1296,4 +1323,16 @@ PC 결제로 발급한 가상계좌의 입금통보를 KG 이니시스 서버로 예시 시나리오: 구매자가 PC 결제창에서 가상계좌를 선택 → 계좌 발급 후 주문은 입금대기 → 구매자가 계좌 이체 → KG 이니시스가 이 주소로 입금통보 → 검증 통과 시 결제완료 전환 후 `OK` 반환. +--- +## 결제 실패 리다이렉트 규약 (브라우저 콜백) + +결제창에서 돌아오는 브라우저 콜백은 JSON 응답이 아니라 상점 실패 페이지로 **리다이렉트**하며, 실패 사유를 쿼리스트링으로 전달합니다. + +| 쿼리 | 값 | 설명 | +| --- | --- | --- | +| `error` | 실패 코드 | 기계 판독용 고정 식별자 (`authorize_failed` · `approve_failed` 등). 화면 분기·문의 접수의 기준값 | +| `message` | 안내 문구 | 구매자에게 보여 줄 다국어 문구. 상점 실패 페이지가 그대로 출력합니다 | +| `orderId` | 주문번호 | 실패한 주문의 식별자 | + +`message` 에는 예외 원문(내부 오류 메시지·SQL 상태코드·클래스명·경로)을 싣지 않습니다. 이 값은 브라우저 주소창과 참조 로그에 남고 실패 페이지에 그대로 출력되므로, 내부 정보가 구매자와 중간 경유지에 노출됩니다. 원인 파악에 필요한 원문은 서버 로그(`Log::error`)에만 기록합니다. diff --git a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md index 6a6d9fde..0fa5559e 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md +++ b/plugins/_bundled/sirsoft-pay_kginicis/docs/api/transaction.md @@ -25,7 +25,9 @@ **요청 파라미터** -_요청 파라미터 없음._ +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| tid | body | string | 예 | max 100 | 조회할 KG 이니시스 거래번호(TID). 누락·문자열 아님·100자 초과는 422 (`TransactionQueryRequest` 검증 — 국내 30자·해외 CBT 40자 형식을 모두 수용, 상한은 저장 컬럼 varchar(100)) | **요청 예시** @@ -34,6 +36,11 @@ POST /api/plugins/sirsoft-pay_kginicis/admin/transaction/query HTTP/1.1 Host: api.example.com Accept: application/json Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "tid": "StdpayCARDSIRSOFT001202606191358556131" +} ``` **응답 필드** (`data` 내부) @@ -154,13 +161,13 @@ HTTP/1.1 200 | --- | --- | --- | | 401 | Unauthenticated | 유효한 Bearer 토큰이 없거나 만료된 경우 | | 403 | Forbidden | 요구 권한(`sirsoft-ecommerce.orders.read`)이 없는 경우 | -| 422 | Unprocessable Entity | `tid` 가 비어 있는 경우 (`errors.tid` = `TID를 입력하세요.`) | +| 422 | Unprocessable Entity | `tid` 가 검증 규칙(필수, 문자열, 최대 100자)을 위반한 경우 (`error.errors.tid` 에 표준 validation 메시지 — 배열 주입도 문자열 규칙이 차단) | | 502 | Bad Gateway | KG 이니시스 조회 API 호출 실패(HTTP 오류·타임아웃 등) 시 | <!-- @generated:end --> **설명** -거래번호(TID)를 직접 입력받아 KG 이니시스 거래 상태를 조회하고 화면 표시용으로 보강해 반환하는 관리자 엔드포인트입니다. 로컬 결제 레코드(`ecommerce_order_payments`)에서 결제 시점 MID·테스트 모드·에스크로 여부를 해석해 해당 자격증명으로 `KgInicisApiService::queryTransaction()` 을 호출하며, 응답을 카드/가상계좌/간편결제/취소 이력 등 상세 필드로 정규화하고 은행 코드→은행명·할부 개월·날짜 포맷을 변환합니다. 일본 CBT 거래(TID `INIJPG` prefix 또는 통화 JPY)는 한국 INIAPI 조회 대상이 아니므로 저장된 로컬 승인/입금 확인 정보로 결과를 구성합니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.orders.read` 권한이 필요하며, `tid` 미입력은 422, 토큰 누락·만료 401, 권한 부족 403, KG 이니시스 조회 실패 시 502 로 응답합니다. +거래번호(TID)를 직접 입력받아 KG 이니시스 거래 상태를 조회하고 화면 표시용으로 보강해 반환하는 관리자 엔드포인트입니다. 로컬 결제 레코드(`ecommerce_order_payments`)에서 결제 시점 MID·테스트 모드·에스크로 여부를 해석해 해당 자격증명으로 `KgInicisApiService::queryTransaction()` 을 호출하며, 응답을 카드/가상계좌/간편결제/취소 이력 등 상세 필드로 정규화하고 은행 코드→은행명·할부 개월·날짜 포맷을 변환합니다. 일본 CBT 거래(TID `INIJPG` prefix 또는 통화 JPY)는 한국 INIAPI 조회 대상이 아니므로 저장된 로컬 승인/입금 확인 정보로 결과를 구성합니다. 관리자 인증(`auth:sanctum`)과 `sirsoft-ecommerce.orders.read` 권한이 필요하며, `tid` 는 `TransactionQueryRequest` 가 필수·문자열·최대 100자(국내 30자·해외 CBT 40자 형식 수용, 저장 컬럼 기준)로 검증해 위반 시 표준 validation 422, 토큰 누락·만료 401, 권한 부족 403, KG 이니시스 조회 실패 시 502 로 응답합니다. diff --git a/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php b/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php index efbea8fe..f36b377c 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/lang/en/messages.php @@ -7,10 +7,16 @@ return [ 'cash_receipt_issue_failed' => 'Failed to issue cash receipt.', 'order_not_found' => 'Order not found.', 'cbt_failed' => 'Failed to process the cross-border payment.', + 'payment_failed' => 'Payment could not be completed. Please try again in a moment.', ], 'cash_receipt' => [ 'provider_name' => 'KG Inicis', ], + 'escrow' => [ + 'invoice_required' => 'Please enter the tracking number.', + 'courier_required' => 'Please select a courier.', + 'default_confirmer' => 'Administrator', + ], 'refund' => [ 'missing_tid' => 'Cannot process refund: transaction ID (TID) is missing.', 'default_reason' => 'Buyer refund request', diff --git a/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php b/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php index 16dd01e9..8e66f887 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/lang/ko/messages.php @@ -7,10 +7,16 @@ return [ 'cash_receipt_issue_failed' => '현금영수증 발행에 실패했습니다.', 'order_not_found' => '주문을 찾을 수 없습니다.', 'cbt_failed' => '해외결제 처리에 실패했습니다.', + 'payment_failed' => '결제 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.', ], 'cash_receipt' => [ 'provider_name' => 'KG이니시스', ], + 'escrow' => [ + 'invoice_required' => '운송장번호를 입력해주세요.', + 'courier_required' => '택배사를 선택해주세요.', + 'default_confirmer' => '관리자', + ], 'refund' => [ 'missing_tid' => '거래 ID(TID)가 없어 환불을 진행할 수 없습니다.', 'default_reason' => '구매자 환불 요청', diff --git a/plugins/_bundled/sirsoft-pay_kginicis/package-lock.json b/plugins/_bundled/sirsoft-pay_kginicis/package-lock.json index 32940ea4..25645d99 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/package-lock.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-pay_kginicis", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-pay_kginicis", - "version": "1.1.0", + "version": "1.1.1", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-pay_kginicis/package.json b/plugins/_bundled/sirsoft-pay_kginicis/package.json index 7458b87c..9823d096 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/package.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-pay_kginicis", - "version": "1.1.0", + "version": "1.1.1", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/plugin.json b/plugins/_bundled/sirsoft-pay_kginicis/plugin.json index 319a95e6..f0e400de 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/plugin.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/plugin.json @@ -5,7 +5,7 @@ "ko": "KG 이니시스", "en": "KG Inicis" }, - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_kginicis", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_kginicis/blob/main/CHANGELOG.md", diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/extensions/admin_order_payment_query.json b/plugins/_bundled/sirsoft-pay_kginicis/resources/extensions/admin_order_payment_query.json index 8d378392..98e79ed6 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/extensions/admin_order_payment_query.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/extensions/admin_order_payment_query.json @@ -324,7 +324,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-pay_kginicis.admin.cbt_reconciliation_retry_error" + "message": "{{error.message ?? '$t:sirsoft-pay_kginicis.admin.cbt_reconciliation_retry_error'}}" } }, { @@ -707,7 +707,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error" + "message": "{{error.message ?? '$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error'}}" } }, { @@ -829,7 +829,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error" + "message": "{{error.message ?? '$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error'}}" } }, { @@ -945,7 +945,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error" + "message": "{{error.message ?? '$t:sirsoft-pay_kginicis.admin.cbt_cvs_action_error'}}" } }, { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json index a4df03c1..79fd3b8e 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-pay_kginicis/resources/layouts/admin/plugin_settings.json @@ -80,7 +80,7 @@ "name": "Icon", "props": { "name": "triangle-exclamation", - "className": "w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" } }, { @@ -201,7 +201,7 @@ "name": "Icon", "props": { "name": "triangle-exclamation", - "className": "w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" } }, { @@ -430,7 +430,7 @@ "if": "{{!_local._show_live_sign_key}}", "props": { "name": "eye", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -439,7 +439,7 @@ "if": "{{_local._show_live_sign_key}}", "props": { "name": "eye-slash", - "className": "w-4 h-4" + "className": "text-base" } } ] @@ -531,7 +531,7 @@ "if": "{{!_local._show_live_iniapi_key}}", "props": { "name": "eye", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -540,7 +540,7 @@ "if": "{{_local._show_live_iniapi_key}}", "props": { "name": "eye-slash", - "className": "w-4 h-4" + "className": "text-base" } } ] @@ -632,7 +632,7 @@ "if": "{{!_local._show_live_iniapi_iv}}", "props": { "name": "eye", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -641,7 +641,7 @@ "if": "{{_local._show_live_iniapi_iv}}", "props": { "name": "eye-slash", - "className": "w-4 h-4" + "className": "text-base" } } ] @@ -733,7 +733,7 @@ "if": "{{!_local._show_live_mobile_hash_key}}", "props": { "name": "eye", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -742,7 +742,7 @@ "if": "{{_local._show_live_mobile_hash_key}}", "props": { "name": "eye-slash", - "className": "w-4 h-4" + "className": "text-base" } } ] @@ -1205,7 +1205,7 @@ "name": "Icon", "props": { "name": "flask-conical", - "className": "w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" } }, { @@ -1299,7 +1299,7 @@ "if": "{{_local.cbtTestProductCreating}}", "props": { "name": "spinner", - "className": "w-3 h-3 animate-spin" + "className": "text-xs animate-spin" } }, { @@ -1308,7 +1308,7 @@ "if": "{{!_local.cbtTestProductCreating}}", "props": { "name": "plus", - "className": "w-3 h-3" + "className": "text-xs" } }, { @@ -1333,7 +1333,7 @@ "name": "Icon", "props": { "name": "external-link-alt", - "className": "w-3 h-3" + "className": "text-xs" } }, { @@ -1426,7 +1426,7 @@ "name": "Icon", "props": { "name": "shield-alt", - "className": "w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" + "className": "text-xl text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" } }, { @@ -1520,7 +1520,7 @@ "if": "{{_local.cbtConnCheckRunning}}", "props": { "name": "spinner", - "className": "w-3 h-3 animate-spin" + "className": "text-xs animate-spin" } }, { @@ -1529,7 +1529,7 @@ "if": "{{!_local.cbtConnCheckRunning}}", "props": { "name": "satellite-dish", - "className": "w-3 h-3" + "className": "text-xs" } }, { @@ -1862,7 +1862,7 @@ "name": "Span", "if": "{{!host.tcp_443_reachable}}", "props": { - "className": "px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-900/40text-danger-soft font-medium" + "className": "px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-900/40 text-danger-soft font-medium" }, "text": "✗ {{host.tcp_443_error ?? '$t:sirsoft-pay_kginicis.settings.cbt_connectivity_unreachable'}}" } @@ -2494,7 +2494,7 @@ "name": "Icon", "props": { "name": "bell", - "className": "w-4 h-4 text-blue-600 dark:text-blue-400" + "className": "text-base text-blue-600 dark:text-blue-400" } }, { @@ -2578,7 +2578,7 @@ "name": "Icon", "props": { "name": "copy", - "className": "w-4 h-4" + "className": "text-base" } }, { @@ -2709,7 +2709,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminCbtCvsOperationsController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminCbtCvsOperationsController.php index 648a2483..066d1489 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminCbtCvsOperationsController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminCbtCvsOperationsController.php @@ -45,6 +45,7 @@ class AdminCbtCvsOperationsController extends AdminBaseController * @param string $orderNumber 주문번호 * @return JsonResponse 응답 */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — 감사 로그용 ip() 만 참조 (검증 대상 필드 없음) public function simulateNotify(Request $request, string $orderNumber): JsonResponse { $result = $this->operationsService->simulatePaidNotify($orderNumber, $request->ip()); diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDeliveryController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDeliveryController.php index 4cc89ca4..569a14f4 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDeliveryController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDeliveryController.php @@ -4,13 +4,18 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayKginicis\Concerns\SanitizesPgResponse; +use Plugins\Sirsoft\PayKginicis\Http\Requests\EscrowDeliveryRegisterRequest; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; /** @@ -23,22 +28,22 @@ class AdminEscrowDeliveryController extends AdminBaseController { use SanitizesPgResponse; - /** 택배사 코드 → 택배사명 매핑 (KG 이니시스 공식 코드표) */ - private const COURIER_CODES = [ - 'hanjin' => '한진택배', - 'cjgls' => 'CJ대한통운', - 'loge' => '롯데택배', - 'epost' => '우체국택배', - 'lotte' => '롯데글로벌로지스', - 'kdexp' => '경동택배', - 'cvs' => '편의점택배', - 'ilyang' => '일양로지스', - 'chunil' => '천일택배', - 'cvsnet' => 'CVSnet편의점', - 'daesin' => '대신택배', - 'kunyoung' => '건영택배', + /** 택배사 코드 → 택배사명 매핑 (KG 이니시스 공식 코드표 — EscrowDeliveryRegisterRequest 의 허용값 SSoT) */ + public const COURIER_CODES = [ + 'hanjin' => '한진택배', + 'cjgls' => 'CJ대한통운', + 'loge' => '롯데택배', + 'epost' => '우체국택배', + 'lotte' => '롯데글로벌로지스', + 'kdexp' => '경동택배', + 'cvs' => '편의점택배', + 'ilyang' => '일양로지스', + 'chunil' => '천일택배', + 'cvsnet' => 'CVSnet편의점', + 'daesin' => '대신택배', + 'kunyoung' => '건영택배', 'gsilogis' => 'GSI Express', - 'etc' => '기타', + 'etc' => '기타', ]; /** 에스크로 배송등록 PG 응답 저장 허용 필드 */ @@ -75,8 +80,9 @@ class AdminEscrowDeliveryController extends AdminBaseController return ResponseHelper::success('common.success', null); } - $address = DB::table('ecommerce_order_addresses as a') - ->join('ecommerce_orders as o', 'o.id', '=', 'a.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $address = DB::table((new OrderAddress)->getTable().' as a') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'a.order_id') ->where('o.order_number', $orderNumber) ->where('a.address_type', 'shipping') ->select([ @@ -89,47 +95,42 @@ class AdminEscrowDeliveryController extends AdminBaseController ->first(); // payment_meta에서 이력 추출 - $meta = $payment->payment_meta ? json_decode($payment->payment_meta, true) : []; + $meta = $payment->payment_meta ? json_decode($payment->payment_meta, true) : []; $escrowDelivery = $meta['escrow_delivery'] ?? null; - $escrowConfirm = $meta['escrow_confirm'] ?? null; - $denyConfirmed = isset($meta['escrow_deny_confirm']); + $escrowConfirm = $meta['escrow_confirm'] ?? null; + $denyConfirmed = isset($meta['escrow_deny_confirm']); return ResponseHelper::success('common.success', [ - 'has_escrow_payment' => true, - 'tid' => $payment->transaction_id, - 'price' => (int) round((float) $payment->paid_amount_local), - 'courier_codes' => self::COURIER_CODES, - 'prefill' => [ - 'recvName' => $address?->recipient_name ?? '', - 'recvTel' => $address?->recipient_phone ?? '', - 'recvPost' => $address?->zipcode ?? '', - 'recvAddr' => trim(($address?->address ?? '') . ' ' . ($address?->address_detail ?? '')), + 'has_escrow_payment' => true, + 'tid' => $payment->transaction_id, + 'price' => (int) round((float) $payment->paid_amount_local), + 'courier_codes' => self::COURIER_CODES, + 'prefill' => [ + 'recvName' => $address?->recipient_name ?? '', + 'recvTel' => $address?->recipient_phone ?? '', + 'recvPost' => $address?->zipcode ?? '', + 'recvAddr' => trim(($address?->address ?? '').' '.($address?->address_detail ?? '')), ], - 'registered_delivery' => $escrowDelivery, - 'escrow_confirm' => $escrowConfirm, - 'deny_confirmed' => $denyConfirmed, + 'registered_delivery' => $escrowDelivery, + 'escrow_confirm' => $escrowConfirm, + 'deny_confirmed' => $denyConfirmed, ]); } /** * register * - * @param Request $request - * @param string $orderNumber + * 형식·길이·택배사 코드 검증은 EscrowDeliveryRegisterRequest 가 담당한다 + * (종전 인라인 한글 하드코딩 메시지를 다국어 키로 이관). + * + * @param EscrowDeliveryRegisterRequest $request 배송 등록 폼 + * @param string $orderNumber 주문번호 * @return JsonResponse */ - public function register(Request $request, string $orderNumber): JsonResponse + public function register(EscrowDeliveryRegisterRequest $request, string $orderNumber): JsonResponse { - $invoice = trim((string) $request->input('invoice', '')); - $exCode = trim((string) $request->input('ex_code', '')); - - if ($invoice === '') { - return ResponseHelper::error('common.failed', 422, ['invoice' => ['운송장번호를 입력해주세요.']]); - } - - if ($exCode === '' || ! array_key_exists($exCode, self::COURIER_CODES)) { - return ResponseHelper::error('common.failed', 422, ['ex_code' => ['택배사를 선택해주세요.']]); - } + $invoice = trim((string) $request->validated('invoice')); + $exCode = trim((string) $request->validated('ex_code')); $payment = $this->findEscrowPayment($orderNumber); @@ -137,37 +138,38 @@ class AdminEscrowDeliveryController extends AdminBaseController return ResponseHelper::error('common.failed', 404, null); } - $report = in_array($request->input('report'), ['I', 'U'], true) ? $request->input('report') : 'I'; - $charge = in_array($request->input('charge'), ['SH', 'BH'], true) ? $request->input('charge') : 'SH'; - $price = (int) round((float) $payment->paid_amount_local); - $exName = self::COURIER_CODES[$exCode]; + $report = $request->validated('report') ?? 'I'; + $charge = $request->validated('charge') ?? 'SH'; + $price = (int) round((float) $payment->paid_amount_local); + $exName = self::COURIER_CODES[$exCode]; // 수신자 주소: 요청 우선, 없으면 DB 조회 - $recvName = trim((string) $request->input('recv_name', '')); - $recvTel = trim((string) $request->input('recv_tel', '')); - $recvPost = trim((string) $request->input('recv_post', '')); - $recvAddr = trim((string) $request->input('recv_addr', '')); + $recvName = trim((string) ($request->validated('recv_name') ?? '')); + $recvTel = trim((string) ($request->validated('recv_tel') ?? '')); + $recvPost = trim((string) ($request->validated('recv_post') ?? '')); + $recvAddr = trim((string) ($request->validated('recv_addr') ?? '')); if ($recvName === '' || $recvTel === '' || $recvAddr === '') { - $address = DB::table('ecommerce_order_addresses as a') - ->join('ecommerce_orders as o', 'o.id', '=', 'a.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $address = DB::table((new OrderAddress)->getTable().' as a') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'a.order_id') ->where('o.order_number', $orderNumber) ->where('a.address_type', 'shipping') ->select(['a.recipient_name', 'a.recipient_phone', 'a.zipcode', 'a.address', 'a.address_detail']) ->first(); $recvName = $recvName ?: ($address?->recipient_name ?? ''); - $recvTel = $recvTel ?: ($address?->recipient_phone ?? ''); + $recvTel = $recvTel ?: ($address?->recipient_phone ?? ''); $recvPost = $recvPost ?: ($address?->zipcode ?? ''); - $recvAddr = $recvAddr ?: trim(($address?->address ?? '') . ' ' . ($address?->address_detail ?? '')); + $recvAddr = $recvAddr ?: trim(($address?->address ?? '').' '.($address?->address_detail ?? '')); } Log::info('KG Inicis: escrow delivery register requested', [ 'order_number' => $orderNumber, - 'tid' => $payment->transaction_id, - 'invoice' => $invoice, - 'ex_code' => $exCode, - 'report' => $report, + 'tid' => $payment->transaction_id, + 'invoice' => $invoice, + 'ex_code' => $exCode, + 'report' => $report, ]); try { @@ -175,24 +177,24 @@ class AdminEscrowDeliveryController extends AdminBaseController $this->apiService->useEscrowCredentials(true); $pgResponse = $this->apiService->registerEscrowDelivery([ - 'tid' => $payment->transaction_id, - 'oid' => $orderNumber, - 'price' => $price, - 'report' => $report, - 'invoice' => $invoice, - 'registName' => trim((string) $request->input('regist_name', '')), - 'exCode' => $exCode, - 'exName' => $exName, - 'charge' => $charge, - 'invoiceDay' => date('Y-m-d H:i:s'), - 'sendName' => trim((string) $request->input('send_name', '')), - 'sendTel' => trim((string) $request->input('send_tel', '')), - 'sendPost' => trim((string) $request->input('send_post', '')), - 'sendAddr1' => trim((string) $request->input('send_addr', '')), - 'recvName' => $recvName, - 'recvTel' => $recvTel, - 'recvPost' => $recvPost, - 'recvAddr' => $recvAddr, + 'tid' => $payment->transaction_id, + 'oid' => $orderNumber, + 'price' => $price, + 'report' => $report, + 'invoice' => $invoice, + 'registName' => trim((string) ($request->validated('regist_name') ?? '')), + 'exCode' => $exCode, + 'exName' => $exName, + 'charge' => $charge, + 'invoiceDay' => date('Y-m-d H:i:s'), + 'sendName' => trim((string) ($request->validated('send_name') ?? '')), + 'sendTel' => trim((string) ($request->validated('send_tel') ?? '')), + 'sendPost' => trim((string) ($request->validated('send_post') ?? '')), + 'sendAddr1' => trim((string) ($request->validated('send_addr') ?? '')), + 'recvName' => $recvName, + 'recvTel' => $recvTel, + 'recvPost' => $recvPost, + 'recvAddr' => $recvAddr, ]); $resultCode = $pgResponse['resultCode'] ?? ''; @@ -201,9 +203,9 @@ class AdminEscrowDeliveryController extends AdminBaseController if ($resultCode !== '00') { Log::warning('KG Inicis: escrow delivery register failed', [ 'order_number' => $orderNumber, - 'result_code' => $resultCode, - 'result_msg' => $pgResponse['resultMsg'] ?? '', - 'pg_response' => $sanitizedPgResponse, + 'result_code' => $resultCode, + 'result_msg' => $pgResponse['resultMsg'] ?? '', + 'pg_response' => $sanitizedPgResponse, ]); return ResponseHelper::error('common.failed', 502, [ @@ -216,41 +218,42 @@ class AdminEscrowDeliveryController extends AdminBaseController $meta['pg_response_sanitized'] = true; $meta['escrow_delivery'] = [ 'registered_at' => now()->toDateTimeString(), - 'report' => $report, - 'invoice' => $invoice, - 'ex_code' => $exCode, - 'ex_name' => $exName, - 'charge' => $charge, - 'recv_name' => $recvName, - 'recv_addr' => $recvAddr, - 'pg_response' => $sanitizedPgResponse, + 'report' => $report, + 'invoice' => $invoice, + 'ex_code' => $exCode, + 'ex_name' => $exName, + 'charge' => $charge, + 'recv_name' => $recvName, + 'recv_addr' => $recvAddr, + 'pg_response' => $sanitizedPgResponse, ]; - DB::table('ecommerce_order_payments') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + DB::table((new OrderPayment)->getTable()) ->where('id', $payment->id) ->update([ 'payment_meta' => json_encode($meta, JSON_UNESCAPED_UNICODE), - 'updated_at' => now(), + 'updated_at' => now(), ]); Log::info('KG Inicis: escrow delivery registered', [ 'order_number' => $orderNumber, - 'tid' => $payment->transaction_id, - 'invoice' => $invoice, - 'ex_name' => $exName, + 'tid' => $payment->transaction_id, + 'invoice' => $invoice, + 'ex_name' => $exName, ]); return ResponseHelper::success('common.success', [ 'result_code' => $resultCode, - 'result_msg' => $pgResponse['resultMsg'] ?? 'OK', - 'invoice' => $invoice, - 'ex_name' => $exName, + 'result_msg' => $pgResponse['resultMsg'] ?? 'OK', + 'invoice' => $invoice, + 'ex_name' => $exName, ]); } catch (\Exception $e) { Log::error('KG Inicis: escrow delivery register exception', [ 'order_number' => $orderNumber, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); return ResponseHelper::error('common.failed', 500, [ @@ -261,8 +264,9 @@ class AdminEscrowDeliveryController extends AdminBaseController private function findEscrowPayment(string $orderNumber): ?object { - return DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + return DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'kginicis') ->where('p.is_escrow', true) diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDenyConfirmController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDenyConfirmController.php index 0b53e30f..0ad9ad5f 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDenyConfirmController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminEscrowDenyConfirmController.php @@ -4,13 +4,17 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayKginicis\Concerns\SanitizesPgResponse; +use Plugins\Sirsoft\PayKginicis\Http\Requests\EscrowDenyConfirmRequest; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; /** @@ -44,11 +48,13 @@ class AdminEscrowDenyConfirmController extends AdminBaseController /** * confirm * - * @param Request $request - * @param string $orderNumber + * 확인자명 형식 검증은 EscrowDenyConfirmRequest 가 담당한다. + * + * @param EscrowDenyConfirmRequest $request 구매거절확인 폼 + * @param string $orderNumber 주문번호 * @return JsonResponse */ - public function confirm(Request $request, string $orderNumber): JsonResponse + public function confirm(EscrowDenyConfirmRequest $request, string $orderNumber): JsonResponse { $payment = $this->findEscrowPayment($orderNumber); @@ -65,11 +71,11 @@ class AdminEscrowDenyConfirmController extends AdminBaseController ]); } - $dcnfName = trim((string) $request->input('dcnf_name', '관리자')); + $dcnfName = $request->confirmerName(); Log::info('KG Inicis: escrow deny confirm requested', [ 'order_number' => $orderNumber, - 'tid' => $payment->transaction_id, + 'tid' => $payment->transaction_id, ]); try { @@ -77,7 +83,7 @@ class AdminEscrowDenyConfirmController extends AdminBaseController $pgResponse = $this->apiService->denyConfirmEscrow([ 'originalTid' => $payment->transaction_id, - 'dcnfName' => $dcnfName, + 'dcnfName' => $dcnfName, ]); $resultCode = $pgResponse['resultCode'] ?? ''; @@ -86,9 +92,9 @@ class AdminEscrowDenyConfirmController extends AdminBaseController if ($resultCode !== '00') { Log::warning('KG Inicis: escrow deny confirm failed', [ 'order_number' => $orderNumber, - 'result_code' => $resultCode, - 'result_msg' => $pgResponse['resultMsg'] ?? '', - 'pg_response' => $sanitizedPgResponse, + 'result_code' => $resultCode, + 'result_msg' => $pgResponse['resultMsg'] ?? '', + 'pg_response' => $sanitizedPgResponse, ]); return ResponseHelper::error('common.failed', 502, [ @@ -100,31 +106,32 @@ class AdminEscrowDenyConfirmController extends AdminBaseController $meta['pg_response_sanitized'] = true; $meta['escrow_deny_confirm'] = [ 'confirmed_at' => now()->toDateTimeString(), - 'dcnf_name' => $dcnfName, - 'pg_response' => $sanitizedPgResponse, + 'dcnf_name' => $dcnfName, + 'pg_response' => $sanitizedPgResponse, ]; - DB::table('ecommerce_order_payments') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + DB::table((new OrderPayment)->getTable()) ->where('id', $payment->id) ->update([ 'payment_meta' => json_encode($meta, JSON_UNESCAPED_UNICODE), - 'updated_at' => now(), + 'updated_at' => now(), ]); Log::info('KG Inicis: escrow deny confirm completed', [ 'order_number' => $orderNumber, - 'tid' => $payment->transaction_id, + 'tid' => $payment->transaction_id, ]); return ResponseHelper::success('common.success', [ 'result_code' => $resultCode, - 'result_msg' => $pgResponse['resultMsg'] ?? 'OK', + 'result_msg' => $pgResponse['resultMsg'] ?? 'OK', ]); } catch (\Exception $e) { Log::error('KG Inicis: escrow deny confirm exception', [ 'order_number' => $orderNumber, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); return ResponseHelper::error('common.failed', 500, [ @@ -135,8 +142,9 @@ class AdminEscrowDenyConfirmController extends AdminBaseController private function findEscrowPayment(string $orderNumber): ?object { - return DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + return DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'kginicis') ->where('p.is_escrow', true) diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminOrderListController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminOrderListController.php index 31c238c9..5156de8c 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminOrderListController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminOrderListController.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; class AdminOrderListController extends AdminBaseController { @@ -31,8 +35,12 @@ class AdminOrderListController extends AdminBaseController */ public function testModeMap(): JsonResponse { - $rows = DB::table('ecommerce_orders as o') - ->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id') + $orders = (new Order)->getTable(); + $payments = (new OrderPayment)->getTable(); + + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $rows = DB::table($orders.' as o') + ->join($payments.' as p', 'p.order_id', '=', 'o.id') ->where('p.pg_provider', 'kginicis') ->where('p.created_at', '>=', now()->subMonths(6)) ->select(['o.order_number', 'p.transaction_id', 'p.payment_meta']) diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminTransactionController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminTransactionController.php index a45e372a..b4c253d8 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminTransactionController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/AdminTransactionController.php @@ -4,12 +4,17 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; +use Carbon\Carbon; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; +use Plugins\Sirsoft\PayKginicis\Http\Requests\TransactionQueryRequest; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; class AdminTransactionController extends AdminBaseController @@ -23,18 +28,12 @@ class AdminTransactionController extends AdminBaseController /** * TID 직접 조회 * - * @param Request $request + * @param TransactionQueryRequest $request tid 입력 폼 * @return JsonResponse */ - public function query(Request $request): JsonResponse + public function query(TransactionQueryRequest $request): JsonResponse { - $tid = trim((string) $request->input('tid', '')); - - if ($tid === '') { - return ResponseHelper::error('common.failed', 422, ['tid' => ['TID를 입력하세요.']]); - } - - return $this->queryByTid($tid); + return $this->queryByTid(trim((string) $request->validated('tid'))); } /** @@ -45,13 +44,17 @@ class AdminTransactionController extends AdminBaseController */ public function queryByOrder(string $orderNumber): JsonResponse { - $payment = DB::table('ecommerce_order_payments') - ->join('ecommerce_orders', 'ecommerce_orders.id', '=', 'ecommerce_order_payments.order_id') - ->where('ecommerce_orders.order_number', $orderNumber) - ->whereNotNull('ecommerce_order_payments.transaction_id') - ->where('ecommerce_order_payments.transaction_id', '!=', '') - ->where('ecommerce_order_payments.pg_provider', 'kginicis') - ->select(['ecommerce_order_payments.transaction_id', 'ecommerce_order_payments.payment_meta']) + $orders = (new Order)->getTable(); + $payments = (new OrderPayment)->getTable(); + + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $payment = DB::table($payments.' as p') + ->join($orders.' as o', 'o.id', '=', 'p.order_id') + ->where('o.order_number', $orderNumber) + ->whereNotNull('p.transaction_id') + ->where('p.transaction_id', '!=', '') + ->where('p.pg_provider', 'kginicis') + ->select(['p.transaction_id', 'p.payment_meta']) ->first(); if (! $payment) { @@ -64,35 +67,39 @@ class AdminTransactionController extends AdminBaseController private function queryByTid(string $tid): JsonResponse { try { - $localPayment = DB::table('ecommerce_order_payments') - ->leftJoin('ecommerce_orders', 'ecommerce_orders.id', '=', 'ecommerce_order_payments.order_id') - ->where('ecommerce_order_payments.transaction_id', $tid) + $orders = (new Order)->getTable(); + $payments = (new OrderPayment)->getTable(); + + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $localPayment = DB::table($payments.' as p') + ->leftJoin($orders.' as o', 'o.id', '=', 'p.order_id') + ->where('p.transaction_id', $tid) ->select([ - 'ecommerce_order_payments.transaction_id', - 'ecommerce_order_payments.payment_status', - 'ecommerce_order_payments.is_escrow', - 'ecommerce_order_payments.payment_meta', - 'ecommerce_order_payments.embedded_pg_provider', - 'ecommerce_order_payments.paid_amount_local', - 'ecommerce_order_payments.currency', - 'ecommerce_order_payments.card_name', - 'ecommerce_order_payments.card_number_masked', - 'ecommerce_order_payments.card_approval_number', - 'ecommerce_order_payments.card_installment_months', - 'ecommerce_order_payments.vbank_code', - 'ecommerce_order_payments.vbank_name', - 'ecommerce_order_payments.vbank_number', - 'ecommerce_order_payments.vbank_holder', - 'ecommerce_order_payments.vbank_due_at', - 'ecommerce_order_payments.buyer_name', - 'ecommerce_order_payments.buyer_email', - 'ecommerce_order_payments.buyer_phone', - 'ecommerce_order_payments.payment_name', - 'ecommerce_order_payments.paid_at', - 'ecommerce_orders.order_number', - 'ecommerce_orders.order_status', - 'ecommerce_orders.currency as order_currency', - 'ecommerce_orders.total_due_amount', + 'p.transaction_id', + 'p.payment_status', + 'p.is_escrow', + 'p.payment_meta', + 'p.embedded_pg_provider', + 'p.paid_amount_local', + 'p.currency', + 'p.card_name', + 'p.card_number_masked', + 'p.card_approval_number', + 'p.card_installment_months', + 'p.vbank_code', + 'p.vbank_name', + 'p.vbank_number', + 'p.vbank_holder', + 'p.vbank_due_at', + 'p.buyer_name', + 'p.buyer_email', + 'p.buyer_phone', + 'p.payment_name', + 'p.paid_at', + 'o.order_number', + 'o.order_status', + 'o.currency as order_currency', + 'o.total_due_amount', ]) ->first(); @@ -143,7 +150,7 @@ class AdminTransactionController extends AdminBaseController return ResponseHelper::success('common.success', $result); } catch (\Exception $e) { Log::error('KG Inicis queryTransaction failed', [ - 'tid' => $tid, + 'tid' => $tid, 'error' => $e->getMessage(), ]); @@ -297,7 +304,7 @@ class AdminTransactionController extends AdminBaseController '_embedded_pg_provider' => $embeddedPgProvider, '_embedded_pg_provider_label' => $embeddedPgProviderLabel, '_pay_method_label' => $embeddedPgProviderLabel - ? $embeddedPgProviderLabel . ' (' . $basePayMethodLabel . ')' + ? $embeddedPgProviderLabel.' ('.$basePayMethodLabel.')' : $basePayMethodLabel, '_auth_code' => $pick('approve', 'applNo', 'approvalNo', 'authCode', 'confNo', 'receiptNo'), '_auth_date' => $this->formatCbtDateTime( @@ -378,12 +385,12 @@ class AdminTransactionController extends AdminBaseController return null; } - return substr($value, 0, 4) . '-' - . substr($value, 4, 2) . '-' - . substr($value, 6, 2) . ' ' - . substr($value, 8, 2) . ':' - . substr($value, 10, 2) . ':' - . substr($value, 12, 2); + return substr($value, 0, 4).'-' + .substr($value, 4, 2).'-' + .substr($value, 6, 2).' ' + .substr($value, 8, 2).':' + .substr($value, 10, 2).':' + .substr($value, 12, 2); } private function formatTimestamp(string $value): ?string @@ -393,7 +400,7 @@ class AdminTransactionController extends AdminBaseController } try { - return \Carbon\Carbon::parse($value)->format('Y-m-d H:i:s'); + return Carbon::parse($value)->format('Y-m-d H:i:s'); } catch (\Throwable) { return null; } @@ -416,8 +423,7 @@ class AdminTransactionController extends AdminBaseController bool $isEscrow, ?string $localVbankDueAt = null, ?string $embeddedPgProvider = null, - ): array - { + ): array { $cardInfo = is_array($result['cardInfo'] ?? null) ? $result['cardInfo'] : []; $pick = function (string ...$keys) use ($result, $localRaw, $cardInfo): ?string { @@ -442,45 +448,45 @@ class AdminTransactionController extends AdminBaseController $basePayMethodLabel = $this->payMethodLabel($payMethod); $embeddedPgProviderLabel = $this->embeddedPgProviderLabel($embeddedPgProvider); - $result['_is_test_mode'] = $this->apiService->isTestMode(); - $result['_local_is_escrow'] = $isEscrow; - $result['_pay_method'] = $payMethod; + $result['_is_test_mode'] = $this->apiService->isTestMode(); + $result['_local_is_escrow'] = $isEscrow; + $result['_pay_method'] = $payMethod; $result['_base_pay_method_label'] = $basePayMethodLabel; $result['_embedded_pg_provider'] = $embeddedPgProvider; $result['_embedded_pg_provider_label'] = $embeddedPgProviderLabel; - $result['_pay_method_label'] = $embeddedPgProviderLabel + $result['_pay_method_label'] = $embeddedPgProviderLabel ? $embeddedPgProviderLabel.' ('.$basePayMethodLabel.')' : $basePayMethodLabel; - $result['_auth_code'] = $pick('applNum', 'approvedNumber', 'authCode', 'AuthCode'); - $result['_auth_date'] = $this->formatDateTime( + $result['_auth_code'] = $pick('applNum', 'approvedNumber', 'authCode', 'AuthCode'); + $result['_auth_date'] = $this->formatDateTime( $pick('applDate', 'approvedDate', 'AuthDate'), $pick('applTime', 'approvedTime', 'AuthTime'), ); - $result['_total_price'] = $pick('TotPrice', 'totalPrice', 'price', 'Amt', 'approvedAmount'); - $result['_currency'] = $pick('currency', 'Currency', 'currencyCode') ?? 'WON'; - $result['_moid'] = $pick('MOID', 'moid', 'Moid', 'oid'); - $result['_buyer_name'] = $pick('buyerName', 'BuyerName'); - $result['_buyer_email'] = $pick('buyerEmail', 'BuyerEmail', 'buyerMail'); - $result['_buyer_tel'] = $pick('buyerTel', 'BuyerTel'); - $result['_status'] = $pick('status', 'Status', 'transactionStatus'); + $result['_total_price'] = $pick('TotPrice', 'totalPrice', 'price', 'Amt', 'approvedAmount'); + $result['_currency'] = $pick('currency', 'Currency', 'currencyCode') ?? 'WON'; + $result['_moid'] = $pick('MOID', 'moid', 'Moid', 'oid'); + $result['_buyer_name'] = $pick('buyerName', 'BuyerName'); + $result['_buyer_email'] = $pick('buyerEmail', 'BuyerEmail', 'buyerMail'); + $result['_buyer_tel'] = $pick('buyerTel', 'BuyerTel'); + $result['_status'] = $pick('status', 'Status', 'transactionStatus'); // 취소이력 - $result['_cancel_price'] = $pick('cancelPrice', 'CancelPrice', 'cancelAmount'); - $result['_cancel_date'] = $this->formatDateTime($pick('cancelDate', 'CancelDate'), $pick('cancelTime', 'CancelTime')); + $result['_cancel_price'] = $pick('cancelPrice', 'CancelPrice', 'cancelAmount'); + $result['_cancel_date'] = $this->formatDateTime($pick('cancelDate', 'CancelDate'), $pick('cancelTime', 'CancelTime')); $partCancelRaw = $result['partCancelList'] ?? $localRaw['partCancelList'] ?? []; - $result['_part_cancel_list'] = $this->normalizePartCancelList(is_array($partCancelRaw) ? $partCancelRaw : []); + $result['_part_cancel_list'] = $this->normalizePartCancelList(is_array($partCancelRaw) ? $partCancelRaw : []); // 결제수단별 상세 (신구 응답 포맷 호환: 평탄 키 + cardInfo 중첩) - $result['_card_name'] = $pick('cardName', 'CardName', 'issuerName'); - $result['_card_num'] = $pick('cardNum', 'CardNum', 'CARD_Num', 'cardNumber'); - $result['_card_code'] = $pick('cardCode', 'CardCode'); - $result['_card_quota'] = $this->formatQuota($pick('cardQuota', 'CardQuota', 'quota')); - $result['_card_interest'] = $pick('cardInterest', 'CardInterest', 'isInterestFree'); + $result['_card_name'] = $pick('cardName', 'CardName', 'issuerName'); + $result['_card_num'] = $pick('cardNum', 'CardNum', 'CARD_Num', 'cardNumber'); + $result['_card_code'] = $pick('cardCode', 'CardCode'); + $result['_card_quota'] = $this->formatQuota($pick('cardQuota', 'CardQuota', 'quota')); + $result['_card_interest'] = $pick('cardInterest', 'CardInterest', 'isInterestFree'); - $result['_vbank_num'] = $pick('VACT_Num', 'vactNum', 'vbank_num'); - $result['_vbank_bank_code'] = $pick('VACT_BankCode', 'vactBankCode', 'vbank_bank_code'); - $result['_vbank_bank_name'] = $pick('VACT_BankName', 'vactBankName', 'vbank_bank_name') ?? $this->bankNameByCode($result['_vbank_bank_code'] ?? null); - $result['_vbank_holder'] = $pick('VACT_Name', 'vactName', 'vbank_holder'); + $result['_vbank_num'] = $pick('VACT_Num', 'vactNum', 'vbank_num'); + $result['_vbank_bank_code'] = $pick('VACT_BankCode', 'vactBankCode', 'vbank_bank_code'); + $result['_vbank_bank_name'] = $pick('VACT_BankName', 'vactBankName', 'vbank_bank_name') ?? $this->bankNameByCode($result['_vbank_bank_code'] ?? null); + $result['_vbank_holder'] = $pick('VACT_Name', 'vactName', 'vbank_holder'); // 가상계좌 입금기한: // 로컬 vbank_due_at 이 있으면 KST 로 변환해 사용 — 결제 발급 시 KG 이니시스가 보낸 // VACT_Date(=다음 영업일) + VACT_Time(=08:59:59) 으로 만든 정확한 cutoff timestamp. @@ -488,24 +494,24 @@ class AdminTransactionController extends AdminBaseController // 조회 응답의 vacctInfo.validDate 는 "마지막 입금 가능일" convention 이라 1일 일찍 표시되어 // 로컬 timestamp 가 더 정확. $result['_vbank_expire_date'] = $localVbankDueAt !== null - ? \Carbon\Carbon::parse($localVbankDueAt, 'UTC')->setTimezone('Asia/Seoul')->format('Y-m-d H:i:s') + ? Carbon::parse($localVbankDueAt, 'UTC')->setTimezone('Asia/Seoul')->format('Y-m-d H:i:s') : $this->formatDate($pick('VACT_Date', 'vactDate', 'vbank_expire_date', 'validDate')); - $vbankStatus = $pick('VACT_Status', 'vactStatus', 'vbank_status'); - $result['_vbank_status'] = $vbankStatus; - $result['_vbank_paid_at'] = $this->formatDateTime($pick('VACT_InputDate', 'VACT_InputTime') ? $pick('VACT_InputDate') : null, $pick('VACT_InputTime')); + $vbankStatus = $pick('VACT_Status', 'vactStatus', 'vbank_status'); + $result['_vbank_status'] = $vbankStatus; + $result['_vbank_paid_at'] = $this->formatDateTime($pick('VACT_InputDate', 'VACT_InputTime') ? $pick('VACT_InputDate') : null, $pick('VACT_InputTime')); - $result['_bank_code'] = $pick('acntBankCode', 'BankCode'); - $result['_bank_name'] = $pick('acntBankName', 'BankName') ?? $this->bankNameByCode($result['_bank_code'] ?? null); - $result['_bank_acnt_num'] = $pick('acntNum', 'AcntNum'); + $result['_bank_code'] = $pick('acntBankCode', 'BankCode'); + $result['_bank_name'] = $pick('acntBankName', 'BankName') ?? $this->bankNameByCode($result['_bank_code'] ?? null); + $result['_bank_acnt_num'] = $pick('acntNum', 'AcntNum'); - $result['_hpp_num'] = $pick('HPP_Num', 'hppNum', 'phoneNum'); - $result['_hpp_corp'] = $pick('HPP_Corp', 'hppCorp', 'mobileCarrier'); + $result['_hpp_num'] = $pick('HPP_Num', 'hppNum', 'phoneNum'); + $result['_hpp_corp'] = $pick('HPP_Corp', 'hppCorp', 'mobileCarrier'); - $result['_escrow_status'] = $pick('escrowStatus', 'EscrowStatus'); - $result['_escrow_confirm'] = $this->formatDateTime($pick('escrowConfirmDate'), $pick('escrowConfirmTime')); + $result['_escrow_status'] = $pick('escrowStatus', 'EscrowStatus'); + $result['_escrow_confirm'] = $this->formatDateTime($pick('escrowConfirmDate'), $pick('escrowConfirmTime')); // 환경 정보 - $result['_inquiry_at'] = date('Y-m-d H:i:s'); + $result['_inquiry_at'] = date('Y-m-d H:i:s'); return $result; } @@ -527,26 +533,26 @@ class AdminTransactionController extends AdminBaseController } return match (strtolower($code)) { - 'card' => '신용카드', - 'wcard' => '해외카드', - 'vbank' => '가상계좌', - 'directbank', 'inibank', 'banktransfer' => '계좌이체', - 'hpp', 'mobile' => '휴대폰', - 'easypay' => '간편결제', - 'point' => '포인트', - 'gift' => '상품권', - 'paybook' => '도서문화상품권', - 'billing', 'billingpay' => '정기결제', - 'samsungpay' => '삼성페이', - 'kakaopay' => '카카오페이', - 'lpay' => 'L.pay', - 'payco' => '페이코', - 'naverpay' => '네이버페이', - 'tosspay', 'toss' => '토스페이', - 'ssgpay' => 'SSG페이', - 'paypay' => 'PayPay', - 'cvs' => '일본 편의점결제', - default => $code, + 'card' => '신용카드', + 'wcard' => '해외카드', + 'vbank' => '가상계좌', + 'directbank', 'inibank', 'banktransfer' => '계좌이체', + 'hpp', 'mobile' => '휴대폰', + 'easypay' => '간편결제', + 'point' => '포인트', + 'gift' => '상품권', + 'paybook' => '도서문화상품권', + 'billing', 'billingpay' => '정기결제', + 'samsungpay' => '삼성페이', + 'kakaopay' => '카카오페이', + 'lpay' => 'L.pay', + 'payco' => '페이코', + 'naverpay' => '네이버페이', + 'tosspay', 'toss' => '토스페이', + 'ssgpay' => 'SSG페이', + 'paypay' => 'PayPay', + 'cvs' => '일본 편의점결제', + default => $code, }; } @@ -652,10 +658,10 @@ class AdminTransactionController extends AdminBaseController $timePart = ''; if ($time !== null && $time !== '' && strlen($time) >= 6) { - $timePart = ' ' . substr($time, 0, 2) . ':' . substr($time, 2, 2) . ':' . substr($time, 4, 2); + $timePart = ' '.substr($time, 0, 2).':'.substr($time, 2, 2).':'.substr($time, 4, 2); } - return $datePart . $timePart; + return $datePart.$timePart; } /** @@ -670,7 +676,7 @@ class AdminTransactionController extends AdminBaseController return $date; } - return substr($date, 0, 4) . '-' . substr($date, 4, 2) . '-' . substr($date, 6, 2); + return substr($date, 0, 4).'-'.substr($date, 4, 2).'-'.substr($date, 6, 2); } /** @@ -687,10 +693,10 @@ class AdminTransactionController extends AdminBaseController continue; } $normalized[] = [ - 'price' => $item['price'] ?? $item['cancelPrice'] ?? null, - 'date' => $this->formatDateTime($item['cancelDate'] ?? null, $item['cancelTime'] ?? null), - 'msg' => $item['cancelMsg'] ?? $item['msg'] ?? null, - 'tid' => $item['cancelTid'] ?? $item['tid'] ?? null, + 'price' => $item['price'] ?? $item['cancelPrice'] ?? null, + 'date' => $this->formatDateTime($item['cancelDate'] ?? null, $item['cancelTime'] ?? null), + 'msg' => $item['cancelMsg'] ?? $item['msg'] ?? null, + 'tid' => $item['cancelTid'] ?? $item['tid'] ?? null, ]; } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtCheckoutTokenController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtCheckoutTokenController.php index 74e666e7..172d1482 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtCheckoutTokenController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtCheckoutTokenController.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; +use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; use Plugins\Sirsoft\PayKginicis\Concerns\ValidatesCbtOrderContext; +use Plugins\Sirsoft\PayKginicis\Http\Requests\CbtCheckoutTokenRequest; use Plugins\Sirsoft\PayKginicis\Services\CbtCheckoutTokenService; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; -use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; class CbtCheckoutTokenController { @@ -22,19 +22,20 @@ class CbtCheckoutTokenController private readonly CbtCheckoutTokenService $checkoutTokenService, ) {} - public function issue(Request $request): JsonResponse + /** + * 해외결제(CBT) 체크아웃 토큰 발급 + * + * 형식·길이 검증은 CbtCheckoutTokenRequest 가 담당한다 (국내 SignatureRequest 와 동일 강도). + * + * @param CbtCheckoutTokenRequest $request oid/price/buyer 정보 + * @return JsonResponse checkout_token 또는 403/404/422 + */ + public function issue(CbtCheckoutTokenRequest $request): JsonResponse { - $oid = (string) $request->input('oid', ''); - $price = (int) $request->input('price', 0); + $oid = (string) $request->validated('oid'); + $price = (int) $request->validated('price'); - if ($oid === '' || $price <= 0) { - return response()->json([ - 'success' => false, - 'message' => 'Missing required parameters: oid, price', - ], 422); - } - - $rateLimitKey = 'sirsoft-pay_kginicis:cbt-token:' . sha1($request->ip() . '|' . $oid); + $rateLimitKey = 'sirsoft-pay_kginicis:cbt-token:'.sha1($request->ip().'|'.$oid); if (RateLimiter::tooManyAttempts($rateLimitKey, 10)) { return response()->json([ 'success' => false, diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtHashDataController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtHashDataController.php index 13d839a1..25a02f9d 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtHashDataController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/CbtHashDataController.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\RateLimiter; use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; use Plugins\Sirsoft\PayKginicis\Concerns\ValidatesCbtOrderContext; use Plugins\Sirsoft\PayKginicis\Concerns\ValidatesTimestampFreshness; +use Plugins\Sirsoft\PayKginicis\Http\Requests\CbtHashDataRequest; use Plugins\Sirsoft\PayKginicis\Services\CbtCheckoutTokenService; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; @@ -27,22 +28,18 @@ class CbtHashDataController /** * generate * - * @param Request $request - * @return JsonResponse + * 형식·길이 검증은 CbtHashDataRequest 가 담당한다 (국내 SignatureRequest 와 동일 강도). + * 계약 변경: 빈 checkout_token 은 종전 403(토큰 검증 실패) → 422(필수 검증). + * + * @param CbtHashDataRequest $request oid/price/timestamp/checkout_token/buyer 정보 + * @return JsonResponse hash_data 또는 403/404/422 */ - public function generate(Request $request): JsonResponse + public function generate(CbtHashDataRequest $request): JsonResponse { - $oid = (string) $request->input('oid', ''); - $price = (int) $request->input('price', 0); - $timestamp = (string) $request->input('timestamp', ''); - $checkoutToken = (string) $request->input('checkout_token', ''); - - if ($oid === '' || $price <= 0 || $timestamp === '') { - return response()->json([ - 'success' => false, - 'message' => 'Missing required parameters: oid, price, timestamp', - ], 422); - } + $oid = (string) $request->validated('oid'); + $price = (int) $request->validated('price'); + $timestamp = (string) $request->validated('timestamp'); + $checkoutToken = (string) $request->validated('checkout_token'); $rateLimitKey = $this->rateLimitKey($request, $oid); if (RateLimiter::tooManyAttempts($rateLimitKey, 10)) { @@ -138,6 +135,6 @@ class CbtHashDataController private function rateLimitKey(Request $request, string $oid): string { - return 'sirsoft-pay_kginicis:cbt-hash:' . sha1($request->ip() . '|' . $oid); + return 'sirsoft-pay_kginicis:cbt-hash:'.sha1($request->ip().'|'.$oid); } } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/MobileCallbackController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/MobileCallbackController.php index 91ec975f..75344eb8 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/MobileCallbackController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/MobileCallbackController.php @@ -319,7 +319,7 @@ class MobileCallbackController return redirect($this->resolveFailUrl([ 'error' => 'approve_failed', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-pay_kginicis::messages.errors.payment_failed'), 'orderId' => $moid, ])); } finally { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/PaymentCallbackController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/PaymentCallbackController.php index 8872d5ad..ed1db7da 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/PaymentCallbackController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/PaymentCallbackController.php @@ -371,7 +371,7 @@ class PaymentCallbackController return redirect($this->resolveFailUrl([ 'error' => 'authorize_failed', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-pay_kginicis::messages.errors.payment_failed'), 'orderId' => $moid, ])); } finally { diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserEscrowConfirmController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserEscrowConfirmController.php index 6a1cdfbf..a25842ca 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserEscrowConfirmController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserEscrowConfirmController.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -11,6 +13,8 @@ use Illuminate\Http\Response; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; /** @@ -21,9 +25,11 @@ use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; */ class UserEscrowConfirmController extends Controller { - private const PC_JS_URL_TEST = 'https://stgstdpay.inicis.com/stdjs/INIStdPay_escrow_conf.js'; - private const PC_JS_URL_LIVE = 'https://stdpay.inicis.com/stdjs/INIStdPay_escrow_conf.js'; - private const MOBILE_PAY_URL = 'https://mobile.inicis.com/smart/payment/'; + private const PC_JS_URL_TEST = 'https://stgstdpay.inicis.com/stdjs/INIStdPay_escrow_conf.js'; + + private const PC_JS_URL_LIVE = 'https://stdpay.inicis.com/stdjs/INIStdPay_escrow_conf.js'; + + private const MOBILE_PAY_URL = 'https://mobile.inicis.com/smart/payment/'; public function __construct( private readonly KgInicisApiService $apiService, @@ -36,6 +42,7 @@ class UserEscrowConfirmController extends Controller * @param string $orderNumber * @return Response */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — 에스크로 확인 HTML 화면 렌더 (검증 대상 필드 없음) public function show(Request $request, string $orderNumber): Response { $payment = $this->findEscrowPayment($orderNumber, (int) Auth::id()); @@ -59,24 +66,25 @@ class UserEscrowConfirmController extends Controller * @param Request $request * @return Response */ + // audit:allow controller-base-request-injection reason: PG(이니시스)가 POST 하는 콜백 표면 — 필드 구성이 PG 소유라 계약을 닫을 수 없다. 값은 문자열 캐스팅 후 tid 대조를 거쳐 payment_meta 에 영속된다(서명 검증 없는 무인증 콜백이라 기록 위조 여지가 있는 사전 상태 — 표시용 메타이며 금액·상태 전이에는 미사용) public function pcReturn(Request $request): Response { $resultCode = (string) $request->input('ResultCode', ''); - $tid = (string) $request->input('tid', ''); + $tid = (string) $request->input('tid', ''); Log::info('KG Inicis: escrow PC confirm return', [ 'result_code' => $resultCode, - 'tid' => $tid, + 'tid' => $tid, ]); $this->saveConfirmResult($tid, [ - 'type' => $resultCode === '00' ? 'confirm' : 'deny', + 'type' => $resultCode === '00' ? 'confirm' : 'deny', 'result_code' => $resultCode, - 'cnf_date' => (string) $request->input('CNF_Date', ''), - 'cnf_time' => (string) $request->input('CNF_Time', ''), - 'dny_date' => (string) $request->input('DNY_Date', ''), - 'dny_time' => (string) $request->input('DNY_Time', ''), - 'dny_msg' => (string) $request->input('DNY_DenyMsg', ''), + 'cnf_date' => (string) $request->input('CNF_Date', ''), + 'cnf_time' => (string) $request->input('CNF_Time', ''), + 'dny_date' => (string) $request->input('DNY_Date', ''), + 'dny_time' => (string) $request->input('DNY_Time', ''), + 'dny_msg' => (string) $request->input('DNY_DenyMsg', ''), ]); // 팝업 닫기 + 부모 창 새로고침 @@ -101,40 +109,41 @@ class UserEscrowConfirmController extends Controller * @param Request $request * @return RedirectResponse */ + // audit:allow controller-base-request-injection reason: PG(이니시스 모바일)가 POST 하는 콜백 표면 — 필드 구성이 PG 소유라 계약을 닫을 수 없다. 값은 문자열 캐스팅 후 tid 대조를 거쳐 payment_meta 에 영속된다(서명 검증 없는 무인증 콜백이라 기록 위조 여지가 있는 사전 상태 — 표시용 메타이며 금액·상태 전이에는 미사용) public function mobileReturn(Request $request): RedirectResponse { - $status = (string) $request->input('P_STATUS', ''); - $tid = (string) $request->input('P_ESCROW_TID', ''); + $status = (string) $request->input('P_STATUS', ''); + $tid = (string) $request->input('P_ESCROW_TID', ''); $clStatus = (string) $request->input('P_CL_STATUS', ''); - $rmesg = (string) $request->input('P_RMESG1', ''); + $rmesg = (string) $request->input('P_RMESG1', ''); Log::info('KG Inicis: escrow mobile confirm return', [ - 'status' => $status, - 'tid' => $tid, + 'status' => $status, + 'tid' => $tid, 'cl_status' => $clStatus, ]); $orderNumber = $this->saveConfirmResult($tid, [ - 'type' => $status === '00' ? 'confirm' : 'deny', + 'type' => $status === '00' ? 'confirm' : 'deny', 'result_code' => $status, - 'cl_status' => $clStatus, - 'result_msg' => $rmesg, + 'cl_status' => $clStatus, + 'result_msg' => $rmesg, ]); - return redirect($orderNumber ? '/mypage/orders/' . $orderNumber : '/mypage/orders'); + return redirect($orderNumber ? '/mypage/orders/'.$orderNumber : '/mypage/orders'); } // ──────────────────────────── Private ──────────────────────────── private function pcConfirmPage(string $tid): Response { - $isTest = $this->apiService->isTestMode(); - $mid = $this->apiService->getMid(); - $mKey = $this->apiService->getEscrowConfirmMKey(); + $isTest = $this->apiService->isTestMode(); + $mid = $this->apiService->getMid(); + $mKey = $this->apiService->getEscrowConfirmMKey(); $timestamp = (string) round(microtime(true) * 1000); - $jsUrl = $isTest ? self::PC_JS_URL_TEST : self::PC_JS_URL_LIVE; + $jsUrl = $isTest ? self::PC_JS_URL_TEST : self::PC_JS_URL_LIVE; $returnUrl = url('/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/pc/return'); - $closeUrl = url('/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/close'); + $closeUrl = url('/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/close'); $html = <<<HTML <!DOCTYPE html> @@ -173,9 +182,9 @@ class UserEscrowConfirmController extends Controller private function mobileConfirmPage(string $tid): Response { - $mid = $this->apiService->getMid(); + $mid = $this->apiService->getMid(); $nextUrl = url('/plugins/sirsoft-pay_kginicis/payment/escrow-confirm/mobile/return'); - $payUrl = self::MOBILE_PAY_URL; + $payUrl = self::MOBILE_PAY_URL; $html = <<<HTML <!DOCTYPE html> @@ -222,8 +231,9 @@ class UserEscrowConfirmController extends Controller return null; } - $row = DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $row = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('p.transaction_id', $tid) ->where('p.pg_provider', 'kginicis') ->where('p.is_escrow', true) @@ -232,6 +242,7 @@ class UserEscrowConfirmController extends Controller if (! $row) { Log::warning('KG Inicis: escrow confirm — payment not found', ['tid' => $tid]); + return null; } @@ -241,17 +252,18 @@ class UserEscrowConfirmController extends Controller $data, ); - DB::table('ecommerce_order_payments') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + DB::table((new OrderPayment)->getTable()) ->where('id', $row->id) ->update([ 'payment_meta' => json_encode($meta, JSON_UNESCAPED_UNICODE), - 'updated_at' => now(), + 'updated_at' => now(), ]); Log::info('KG Inicis: escrow confirm saved', [ 'order_number' => $row->order_number, - 'tid' => $tid, - 'type' => $data['type'] ?? '', + 'tid' => $tid, + 'type' => $data['type'] ?? '', ]); return $row->order_number; @@ -259,8 +271,9 @@ class UserEscrowConfirmController extends Controller private function findEscrowPayment(string $orderNumber, int $userId): ?object { - return DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + return DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('o.user_id', $userId) ->where('p.pg_provider', 'kginicis') diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserReceiptController.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserReceiptController.php index a0ffe146..efc73c43 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserReceiptController.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Controllers/UserReceiptController.php @@ -4,11 +4,15 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayKginicis\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Services\PluginSettingsService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; use Plugins\Sirsoft\PayKginicis\Concerns\IssuesReceiptCookie; use Plugins\Sirsoft\PayKginicis\Concerns\ResolvesEasyPaySelection; @@ -39,10 +43,12 @@ class UserReceiptController * @param string $orderNumber * @return JsonResponse */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — user()/비회원 영수증 쿠키만 참조 (검증 대상 필드 없음) public function show(Request $request, string $orderNumber): JsonResponse { - $query = DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $query = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'kginicis'); @@ -59,7 +65,7 @@ class UserReceiptController $cookieValue = $request->cookie(self::RECEIPT_COOKIE_NAME); if ($this->verifyReceiptCookie($cookieValue, $orderNumber)) { $query->where('o.id', function ($sub) use ($orderNumber) { - $sub->select('id')->from('ecommerce_orders')->where('order_number', $orderNumber); + $sub->select('id')->from((new Order)->getTable())->where('order_number', $orderNumber); }); } else { return response()->json(['error' => 'Not found'], 404); @@ -107,43 +113,43 @@ class UserReceiptController $receiptLabels = $this->cbtReceiptLabels($cbtPayMethod, (string) ($payment->payment_status ?? '')); return response()->json([ - 'receipt_type' => 'cbt_confirmation', - 'receipt_url' => null, - 'receipt_label' => $receiptLabels['label'], - 'receipt_view_label' => $receiptLabels['view_label'], - 'receipt_title' => $receiptLabels['title'], - 'receipt_notice' => $receiptLabels['notice'], - 'receipt_fields' => $this->buildCbtReceiptFields($payment, $paymentMeta), - 'is_test_mode' => $isTestMode, - 'payment_method_label' => $cbtPaymentMethodLabel, - 'payment_method_display_label' => $cbtPaymentMethodLabel, - 'cbt_pay_method' => $cbtPayMethod, - 'payment_status' => (string) ($payment->payment_status ?? ''), - 'selected_payment_method' => $paymentMeta['selected_payment_method'] ?? null, - 'embedded_pg_provider' => is_string($embeddedPgProvider) ? $embeddedPgProvider : null, - 'embedded_pg_provider_label' => is_string($embeddedPgProviderLabel) ? $embeddedPgProviderLabel : null, + 'receipt_type' => 'cbt_confirmation', + 'receipt_url' => null, + 'receipt_label' => $receiptLabels['label'], + 'receipt_view_label' => $receiptLabels['view_label'], + 'receipt_title' => $receiptLabels['title'], + 'receipt_notice' => $receiptLabels['notice'], + 'receipt_fields' => $this->buildCbtReceiptFields($payment, $paymentMeta), + 'is_test_mode' => $isTestMode, + 'payment_method_label' => $cbtPaymentMethodLabel, + 'payment_method_display_label' => $cbtPaymentMethodLabel, + 'cbt_pay_method' => $cbtPayMethod, + 'payment_status' => (string) ($payment->payment_status ?? ''), + 'selected_payment_method' => $paymentMeta['selected_payment_method'] ?? null, + 'embedded_pg_provider' => is_string($embeddedPgProvider) ? $embeddedPgProvider : null, + 'embedded_pg_provider_label' => is_string($embeddedPgProviderLabel) ? $embeddedPgProviderLabel : null, ]); } - $receiptUrl = self::RECEIPT_BASE_URL . '?' . http_build_query([ - 'noTid' => $payment->transaction_id, + $receiptUrl = self::RECEIPT_BASE_URL.'?'.http_build_query([ + 'noTid' => $payment->transaction_id, 'noMethod' => '1', ]); return response()->json([ - 'receipt_type' => 'inicis_receipt', - 'receipt_url' => $receiptUrl, - 'receipt_label' => '영수증', - 'receipt_view_label' => '영수증 조회', - 'is_test_mode' => $isTestMode, - 'payment_method_label' => $basePaymentMethodLabel, - 'payment_method_display_label' => $this->paymentMethodDisplayLabel( + 'receipt_type' => 'inicis_receipt', + 'receipt_url' => $receiptUrl, + 'receipt_label' => '영수증', + 'receipt_view_label' => '영수증 조회', + 'is_test_mode' => $isTestMode, + 'payment_method_label' => $basePaymentMethodLabel, + 'payment_method_display_label' => $this->paymentMethodDisplayLabel( $basePaymentMethodLabel, is_string($embeddedPgProviderLabel) ? $embeddedPgProviderLabel : null, ), - 'selected_payment_method' => $paymentMeta['selected_payment_method'] ?? null, - 'embedded_pg_provider' => is_string($embeddedPgProvider) ? $embeddedPgProvider : null, - 'embedded_pg_provider_label' => is_string($embeddedPgProviderLabel) ? $embeddedPgProviderLabel : null, + 'selected_payment_method' => $paymentMeta['selected_payment_method'] ?? null, + 'embedded_pg_provider' => is_string($embeddedPgProvider) ? $embeddedPgProvider : null, + 'embedded_pg_provider_label' => is_string($embeddedPgProviderLabel) ? $embeddedPgProviderLabel : null, ]); } @@ -282,7 +288,7 @@ class UserReceiptController return ''; } - return number_format((float) $amount) . ' ' . $currency; + return number_format((float) $amount).' '.$currency; } private function formatCbtDateTime(mixed $date, mixed $time): string @@ -324,7 +330,7 @@ class UserReceiptController $months = (int) $value; - return $months <= 0 ? '일시불' : $months . '개월'; + return $months <= 0 ? '일시불' : $months.'개월'; } private function decodePaymentMeta(mixed $paymentMeta): array @@ -387,6 +393,6 @@ class UserReceiptController return $embeddedLabel; } - return $embeddedLabel . ' (' . $baseLabel . ')'; + return $embeddedLabel.' ('.$baseLabel.')'; } } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtCheckoutTokenRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtCheckoutTokenRequest.php new file mode 100644 index 00000000..69e2be18 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtCheckoutTokenRequest.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayKginicis\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Plugins\Sirsoft\PayKginicis\Support\PaymentLimits; + +/** + * 해외결제(CBT/일본) 체크아웃 토큰 발급 요청 검증 + * + * 국내 짝(SignatureRequest)과 동일 강도 — 종전에는 CBT 경로만 raw 입력이라 + * 서명·토큰 발급 계약이 국내와 3단계 드리프트 상태였다. + * 주문·금액·구매자 대조는 컨트롤러의 도메인 검증(우리 DB 기준)이 담당한다. + */ +class CbtCheckoutTokenRequest extends FormRequest +{ + /** + * 요청 권한 — 공개 결제 표면이므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 체크아웃 토큰 발급 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'oid' => ['required', 'string', 'max:40'], + // 하한은 국내와 동일하게 PaymentLimits SSoT (JPY 도 1 단위 결제 성립) + 'price' => ['required', 'integer', 'min:'.PaymentLimits::MIN_PRICE], + 'buyer_email' => ['nullable', 'string', 'max:255'], + 'buyer_phone' => ['nullable', 'string', 'max:30'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtHashDataRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtHashDataRequest.php new file mode 100644 index 00000000..27e96c86 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/CbtHashDataRequest.php @@ -0,0 +1,50 @@ +<?php + +// audit:allow api-doc-coverage reason: docblock 서술 정정만 수행 — 검증 규칙/요청·응답 계약 무변경 + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayKginicis\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Plugins\Sirsoft\PayKginicis\Support\PaymentLimits; + +/** + * 해외결제(CBT/일본) 서명 해시 생성 요청 검증 + * + * 국내 짝(SignatureRequest)과 동일 강도 + timestamp/checkout_token. + * + * 계약 변경: 종전에는 checkout_token 이 비어 있으면 토큰 검증 단계에서 403 이었으나, + * 필수 검증으로 승격되어 422 가 된다. CBT 클라이언트(requestPayment.ts)는 + * `G7Core.api.post` 경유라 403/422 모두 실패 분기로 동일 취급되어 안전(실측 — + * raw fetch `.ok` 검사는 nicepayments signData 쪽 클라이언트의 방식). + */ +class CbtHashDataRequest extends FormRequest +{ + /** + * 요청 권한 — 공개 결제 표면이므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 서명 해시 생성 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'oid' => ['required', 'string', 'max:40'], + 'price' => ['required', 'integer', 'min:'.PaymentLimits::MIN_PRICE], + 'timestamp' => ['required', 'string', 'max:20'], + 'checkout_token' => ['required', 'string', 'max:1024'], + 'buyer_email' => ['nullable', 'string', 'max:255'], + 'buyer_phone' => ['nullable', 'string', 'max:30'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDeliveryRegisterRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDeliveryRegisterRequest.php new file mode 100644 index 00000000..f64baf9f --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDeliveryRegisterRequest.php @@ -0,0 +1,68 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayKginicis\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; +use Plugins\Sirsoft\PayKginicis\Controllers\AdminEscrowDeliveryController; + +/** + * 에스크로 배송 등록 요청 검증 + * + * 값은 KG 이니시스 에스크로 배송등록 API 로 전송되고 payment_meta 로 영속된다. + * 상한 근거(KG 이니시스 연동 스펙): 운송장 40자 / 이름 30자 / 전화 20자 / + * 우편번호 10자 / 주소 200자. 택배사 코드는 공식 코드표(컨트롤러 상수)가 SSoT. + * report/charge 는 미전송이면 컨트롤러 기본값(I/SH)으로 흡수하고, 비허용 값은 422 로 차단한다. + */ +class EscrowDeliveryRegisterRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 에스크로 배송 등록 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'invoice' => ['required', 'string', 'max:40'], + 'ex_code' => ['required', 'string', Rule::in(array_keys(AdminEscrowDeliveryController::COURIER_CODES))], + 'report' => ['nullable', 'string', 'in:I,U'], + 'charge' => ['nullable', 'string', 'in:SH,BH'], + 'recv_name' => ['nullable', 'string', 'max:30'], + 'recv_tel' => ['nullable', 'string', 'max:20'], + 'recv_post' => ['nullable', 'string', 'max:10'], + 'recv_addr' => ['nullable', 'string', 'max:200'], + 'regist_name' => ['nullable', 'string', 'max:30'], + 'send_name' => ['nullable', 'string', 'max:30'], + 'send_tel' => ['nullable', 'string', 'max:20'], + 'send_post' => ['nullable', 'string', 'max:10'], + 'send_addr' => ['nullable', 'string', 'max:200'], + ]; + } + + /** + * 검증 오류 메시지를 반환합니다 (종전 한글 하드코딩을 다국어 키로 이관). + * + * @return array<string, string> 규칙별 메시지 + */ + public function messages(): array + { + return [ + 'invoice.required' => __('sirsoft-pay_kginicis::messages.escrow.invoice_required'), + 'ex_code.required' => __('sirsoft-pay_kginicis::messages.escrow.courier_required'), + 'ex_code.in' => __('sirsoft-pay_kginicis::messages.escrow.courier_required'), + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDenyConfirmRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDenyConfirmRequest.php new file mode 100644 index 00000000..ccc012f6 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/EscrowDenyConfirmRequest.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayKginicis\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 에스크로 구매거절확인 요청 검증 + * + * dcnf_name(확인자명)은 KG 이니시스 구매거절확인 API 로 전송되고 payment_meta 로 + * 영속된다. 상한 근거: 이니시스 확인자명 20자. 미입력 시 기본값은 다국어 키 + * (escrow.default_confirmer)로 채운다 — 종전 한글 하드코딩('관리자') 이관. + */ +class EscrowDenyConfirmRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 구매거절확인 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'dcnf_name' => ['nullable', 'string', 'max:20'], + ]; + } + + /** + * 확인자명을 반환합니다 (미입력 시 다국어 기본값). + * + * @return string 확인자명 + */ + public function confirmerName(): string + { + $name = trim((string) ($this->validated('dcnf_name') ?? '')); + + return $name !== '' ? $name : __('sirsoft-pay_kginicis::messages.escrow.default_confirmer'); + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/PaymentCloseReportRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/PaymentCloseReportRequest.php index c228f6dc..fd456765 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/PaymentCloseReportRequest.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/PaymentCloseReportRequest.php @@ -32,7 +32,8 @@ class PaymentCloseReportRequest extends FormRequest 'buyer_email' => ['nullable', 'string', 'max:255'], 'buyer_phone' => ['nullable', 'string', 'max:30'], 'payment_method' => ['nullable', 'string', 'max:50'], - 'reason' => ['nullable', 'string', 'max:80'], + // 내부 텔레메트리 필드 (PG 미전송 실측) — 형제 플러그인(nhnkcp/nicepayments)과 160 으로 통일 + 'reason' => ['nullable', 'string', 'max:160'], ]; } } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/TransactionQueryRequest.php b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/TransactionQueryRequest.php new file mode 100644 index 00000000..362d8890 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_kginicis/src/Http/Requests/TransactionQueryRequest.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayKginicis\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * TID 단건 거래 조회 요청 검증 + * + * 상한 근거: 저장 컬럼 `ecommerce_order_payments.transaction_id` varchar(100) 이 SSoT. + * 국내 TID 는 30자지만 해외결제(CBT) TID 는 40자(관측)로 형식이 다르다 — 이 엔드포인트는 + * 두 형식을 모두 받으므로 특정 형식 길이(30)로 조이면 CBT 조회가 422 로 깨진다. + */ +class TransactionQueryRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 거래 조회 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'tid' => ['required', 'string', 'max:100'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminCbtTestProductControllerTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminCbtTestProductControllerTest.php index 22659e74..105a10f5 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminCbtTestProductControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminCbtTestProductControllerTest.php @@ -142,4 +142,20 @@ class AdminCbtTestProductControllerTest extends PluginTestCase Http::assertNothingSent(); } + + /** + * tid 게이트 상한은 저장 컬럼(transaction_id varchar(100))이 SSoT (회귀 고정). + * + * 국내 TID(30자) 길이로 조이면 40자 CBT TID 조회가 422 로 깨진다 — 위 테스트가 + * 그 회귀를 잡았다. 여기서는 반대편 경계(101자 초과 거부)를 고정한다. + */ + public function test_transaction_query_rejects_tid_over_storage_column_length(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.read']); + $this->actingAs($admin); + + $this->postJson('/api/plugins/sirsoft-pay_kginicis/admin/transaction/query', [ + 'tid' => str_repeat('T', 101), + ])->assertStatus(422); + } } diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminEscrowControllerTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminEscrowControllerTest.php index d8f4dc37..df45bc27 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminEscrowControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/AdminEscrowControllerTest.php @@ -110,6 +110,133 @@ class AdminEscrowControllerTest extends PluginTestCase $this->assertArrayNotHasKey('buyerTel', $storedPgResponse); } + /** + * 운송장번호 누락은 EscrowDeliveryRegisterRequest 가 표준 422 로 차단하고, + * 메시지는 다국어 키(escrow.invoice_required)의 ko 문구여야 한다 + * (종전 컨트롤러 인라인 한글 하드코딩 → 다국어 키 이관 회귀 방지). + */ + public function test_escrow_delivery_register_rejects_missing_invoice_with_localized_message(): void + { + + $this->createEscrowPayment('ORD-ESCROW-VAL-001'); + + $mock = $this->createMock(KgInicisApiService::class); + $mock->expects($this->never())->method('useEscrowCredentials'); + $mock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(KgInicisApiService::class, $mock); + + $response = $this->actingAs($this->adminUser) + ->withHeaders(['Accept-Language' => 'ko']) + ->postJson('/api/plugins/sirsoft-pay_kginicis/admin/orders/ORD-ESCROW-VAL-001/escrow-delivery', [ + 'ex_code' => 'hanjin', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['invoice']) + ->assertJsonPath('errors.invoice.0', '운송장번호를 입력해주세요.'); + } + + /** + * 코드표(COURIER_CODES) 밖의 택배사 코드는 Rule::in 이 422 로 차단해야 한다. + */ + public function test_escrow_delivery_register_rejects_unknown_courier_code(): void + { + + $this->createEscrowPayment('ORD-ESCROW-VAL-002'); + + $mock = $this->createMock(KgInicisApiService::class); + $mock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(KgInicisApiService::class, $mock); + + $response = $this->actingAs($this->adminUser) + ->withHeaders(['Accept-Language' => 'ko']) + ->postJson('/api/plugins/sirsoft-pay_kginicis/admin/orders/ORD-ESCROW-VAL-002/escrow-delivery', [ + 'invoice' => 'INV-001', + 'ex_code' => 'nope', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['ex_code']) + ->assertJsonPath('errors.ex_code.0', '택배사를 선택해주세요.'); + } + + /** + * 수령인 필드 상한 초과(이름 31자 / 전화 21자 / 우편 11자 / 주소 201자)는 + * KG 이니시스 연동 스펙 상한을 넘으므로 422 로 차단하고 PG 호출은 없어야 한다. + */ + public function test_escrow_delivery_register_rejects_overlong_recv_fields(): void + { + $this->createEscrowPayment('ORD-ESCROW-VAL-003'); + + $mock = $this->createMock(KgInicisApiService::class); + $mock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(KgInicisApiService::class, $mock); + + $response = $this->actingAs($this->adminUser) + ->postJson('/api/plugins/sirsoft-pay_kginicis/admin/orders/ORD-ESCROW-VAL-003/escrow-delivery', [ + 'invoice' => 'INV-001', + 'ex_code' => 'hanjin', + 'recv_name' => str_repeat('가', 31), + 'recv_tel' => str_repeat('0', 21), + 'recv_post' => str_repeat('1', 11), + 'recv_addr' => str_repeat('가', 201), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors([ + 'recv_name', + 'recv_tel', + 'recv_post', + 'recv_addr', + ]); + } + + /** + * 배열 주입(`dcnf_name[]=x`)은 EscrowDenyConfirmRequest 의 string 규칙이 422 로 + * 차단하고 PG 구매거절확인 API 는 호출되지 않아야 한다. + */ + public function test_escrow_deny_confirm_rejects_array_dcnf_name(): void + { + $this->createEscrowPayment('ORD-ESCROW-DNCF-VAL-001', [ + 'escrow_confirm' => ['type' => 'deny'], + ]); + + $mock = $this->createMock(KgInicisApiService::class); + $mock->expects($this->never())->method('useEscrowCredentials'); + $mock->expects($this->never())->method('denyConfirmEscrow'); + $this->app->instance(KgInicisApiService::class, $mock); + + $response = $this->actingAs($this->adminUser) + ->postJson('/api/plugins/sirsoft-pay_kginicis/admin/orders/ORD-ESCROW-DNCF-VAL-001/escrow-deny-confirm', [ + 'dcnf_name' => ['x'], + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['dcnf_name']); + } + + /** + * 확인자명 상한(20자) 초과는 422 로 차단해야 한다 (이니시스 확인자명 20자 스펙). + */ + public function test_escrow_deny_confirm_rejects_overlong_dcnf_name(): void + { + $this->createEscrowPayment('ORD-ESCROW-DNCF-VAL-002', [ + 'escrow_confirm' => ['type' => 'deny'], + ]); + + $mock = $this->createMock(KgInicisApiService::class); + $mock->expects($this->never())->method('denyConfirmEscrow'); + $this->app->instance(KgInicisApiService::class, $mock); + + $response = $this->actingAs($this->adminUser) + ->postJson('/api/plugins/sirsoft-pay_kginicis/admin/orders/ORD-ESCROW-DNCF-VAL-002/escrow-deny-confirm', [ + 'dcnf_name' => str_repeat('가', 21), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['dcnf_name']); + } + private function createEscrowPayment(string $orderNumber, array $paymentMeta = []): OrderPayment { $order = OrderFactory::new()->paid()->create([ diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/CbtPaymentControllerTest.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/CbtPaymentControllerTest.php index cb0d6a8e..ab736ea5 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/CbtPaymentControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/Feature/Controllers/CbtPaymentControllerTest.php @@ -2,20 +2,21 @@ namespace Plugins\Sirsoft\PayKginicis\Tests\Feature\Controllers; -use Mockery; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Mockery; use Modules\Sirsoft\Ecommerce\Database\Factories\OrderFactory; use Modules\Sirsoft\Ecommerce\Database\Factories\OrderPaymentFactory; use Modules\Sirsoft\Ecommerce\Enums\OrderStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\PaymentMethodEnum; use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum; use Modules\Sirsoft\Ecommerce\Models\Order; -use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\OrderProcessingService; use Plugins\Sirsoft\PayKginicis\Repositories\CbtCvsOperationsRepositoryInterface; -use Plugins\Sirsoft\PayKginicis\Services\CbtCvsOperationsService; use Plugins\Sirsoft\PayKginicis\Services\CbtCheckoutTokenService; +use Plugins\Sirsoft\PayKginicis\Services\CbtCvsOperationsService; use Plugins\Sirsoft\PayKginicis\Services\CbtReconciliationService; use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService; use Plugins\Sirsoft\PayKginicis\Tests\PluginTestCase; @@ -64,7 +65,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-001', 'sid' => 'SID001', 'resultCode' => 'OK', @@ -107,7 +108,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-CVS-001', 'sid' => 'SID-CVS-001', 'resultCode' => 'OK', @@ -139,7 +140,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-CANCEL-001', 'sid' => 'SID-CANCEL-001', 'resultCode' => 'FAIL', @@ -168,7 +169,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-PAYPAY-FAIL-001', 'sid' => 'SID-PAYPAY-FAIL-001', 'resultCode' => 'processing_failure', @@ -653,7 +654,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-003', 'sid' => 'SID003', 'resultCode' => 'OK', @@ -712,7 +713,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-CBT-POST-COMMIT', 'sid' => 'SID_POST_COMMIT', 'resultCode' => 'OK', @@ -763,7 +764,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-004', 'sid' => 'SID004', 'resultCode' => 'OK', @@ -810,7 +811,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-012', 'sid' => 'SID012', 'resultCode' => 'OK', @@ -862,7 +863,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-013', 'sid' => 'SID013', 'resultCode' => 'OK', @@ -923,7 +924,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-PAYPAY-OK', 'sid' => 'SID-PAYPAY-OK', 'resultCode' => 'OK', @@ -955,6 +956,9 @@ class CbtPaymentControllerTest extends PluginTestCase 'oid' => 'JP-ORDER-002', 'price' => 99, 'timestamp' => date('YmdHis'), + // 계약 변경(CbtHashDataRequest): checkout_token 이 필수로 승격되어 미전송 시 422 검증 + // 오류가 먼저 난다. 금액 대조는 토큰 검증보다 앞 단계이므로 임의 토큰으로 충분하다. + 'checkout_token' => 'token-not-reached', ]); $response->assertStatus(422) @@ -989,6 +993,9 @@ class CbtPaymentControllerTest extends PluginTestCase 'timestamp' => date('YmdHis'), 'buyer_email' => 'attacker@example.jp', 'buyer_phone' => '09012345678', + // 계약 변경(CbtHashDataRequest): checkout_token 필수 승격 — 구매자 대조(403)는 + // 토큰 검증보다 앞 단계이므로 임의 토큰으로 충분하다. + 'checkout_token' => 'token-not-reached', ]); $response->assertStatus(403) @@ -1020,7 +1027,7 @@ class CbtPaymentControllerTest extends PluginTestCase $checkoutTokenService = Mockery::mock(CbtCheckoutTokenService::class); $checkoutTokenService->shouldReceive('verify') ->once() - ->with('token-ok', 'JP-ORDER-006', 100, 'BUYER@example.jp', '09012345678', Mockery::type(\Illuminate\Http\Request::class)) + ->with('token-ok', 'JP-ORDER-006', 100, 'BUYER@example.jp', '09012345678', Mockery::type(Request::class)) ->andReturn(true); $this->app->instance(OrderProcessingService::class, $orderService); @@ -1107,6 +1114,66 @@ class CbtPaymentControllerTest extends PluginTestCase $this->assertStringContainsString('.', $response->json('data.checkout_token')); } + /** + * OID 상한(40자) 초과는 CbtCheckoutTokenRequest 가 표준 422 로 차단해야 한다 + * (국내 SignatureRequest 와 동일 강도 — 종전 CBT 경로만 raw 입력이던 드리프트 해소). + */ + public function test_cbt_checkout_token_rejects_overlong_oid(): void + { + $apiService = Mockery::mock(KgInicisApiService::class); + $apiService->shouldNotReceive('isJapanEnabled'); + $this->app->instance(KgInicisApiService::class, $apiService); + + $response = $this->postJson('/api/plugins/sirsoft-pay_kginicis/payment/cbt/checkout-token', [ + 'oid' => str_repeat('A', 41), + 'price' => 100, + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['oid']); + } + + /** + * checkout_token 누락은 표준 422 로 차단해야 한다. + * (계약 변경: 종전에는 빈 토큰이 토큰 검증 단계까지 흘러가 403 이었으나, + * CbtHashDataRequest 의 required 승격으로 422 가 된다 — 클라이언트는 `.ok` 만 검사) + */ + public function test_cbt_hash_data_rejects_missing_checkout_token(): void + { + $apiService = Mockery::mock(KgInicisApiService::class); + $apiService->shouldNotReceive('generateCbtHashData'); + $this->app->instance(KgInicisApiService::class, $apiService); + + $response = $this->postJson('/api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data', [ + 'oid' => 'JP-ORDER-TOKEN-MISSING', + 'price' => 100, + 'timestamp' => date('YmdHis'), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['checkout_token']); + } + + /** + * 배열 주입(`oid[]=x`)은 string 규칙이 표준 422 로 차단해야 한다. + */ + public function test_cbt_hash_data_rejects_array_injection(): void + { + $apiService = Mockery::mock(KgInicisApiService::class); + $apiService->shouldNotReceive('generateCbtHashData'); + $this->app->instance(KgInicisApiService::class, $apiService); + + $response = $this->postJson('/api/plugins/sirsoft-pay_kginicis/payment/cbt/hash-data', [ + 'oid' => ['x'], + 'price' => 100, + 'timestamp' => date('YmdHis'), + 'checkout_token' => 'token-not-reached', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['oid']); + } + public function test_cbt_callback_stores_sanitized_pg_response_only(): void { $order = $this->makePendingJpyOrder('JP-ORDER-009', 100); @@ -1149,7 +1216,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $response = $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-009', 'sid' => 'SID009', 'resultCode' => 'OK', @@ -1192,7 +1259,7 @@ class CbtPaymentControllerTest extends PluginTestCase $this->app->instance(KgInicisApiService::class, $apiService); $this->get('/plugins/sirsoft-pay_kginicis/payment/cbt/callback?' - . http_build_query([ + .http_build_query([ 'oid' => 'JP-ORDER-010', 'sid' => 'SID010', 'resultCode' => 'OK', @@ -1279,7 +1346,7 @@ class CbtPaymentControllerTest extends PluginTestCase private function makePendingJpyOrder(string $orderNumber, int $amount): Order { - $order = new Order(); + $order = new Order; $order->order_number = $orderNumber; $order->order_status = OrderStatusEnum::PENDING_ORDER; $order->currency = 'JPY'; @@ -1339,8 +1406,8 @@ class CbtPaymentControllerTest extends PluginTestCase private function createPersistedPendingCbtCvsOrder(string $orderNumber, int $amount, array $metaOverrides = []): Order { $order = $this->createPersistedPendingJpyOrder($orderNumber, $amount); - $sid = 'SID-' . $orderNumber; - $tid = 'CBT_CVS_TID_' . str_replace('-', '_', $orderNumber); + $sid = 'SID-'.$orderNumber; + $tid = 'CBT_CVS_TID_'.str_replace('-', '_', $orderNumber); $order->payment()->update([ 'payment_status' => PaymentStatusEnum::WAITING_DEPOSIT, diff --git a/plugins/_bundled/sirsoft-pay_kginicis/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-pay_kginicis/tests/PluginTestCase.php index 4a095fd2..783de01c 100644 --- a/plugins/_bundled/sirsoft-pay_kginicis/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-pay_kginicis/tests/PluginTestCase.php @@ -201,8 +201,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); @@ -231,8 +235,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $pluginBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }, true, true); diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md index cd7abc66..6d9be298 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/CHANGELOG.md @@ -4,6 +4,22 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Security + +- 에스크로 배송등록 요청의 운송장번호·택배사 코드에 형식·길이 검증을 추가했습니다. + +### Changed + +- 에스크로 배송등록 오류 안내가 다국어로 제공됩니다. +- 주문·결제 정보 조회가 데이터베이스 표 이름을 직접 쓰지 않고 쇼핑몰 모듈의 정의를 따르도록 내부 정리했습니다. 표시되는 내용과 동작은 종전과 동일합니다. + +### Fixed + +- 결제 실패 시 이동하는 페이지 주소에 서버 내부 오류 원문이 그대로 실려 나가던 문제를 수정했습니다. 이제 안내 문구만 전달되며, 원인 파악에 필요한 원문은 서버 로그에만 기록됩니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-10 ### Changed @@ -12,6 +28,7 @@ ### Fixed +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. - 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 색상 배지가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다. - 관리자 주문 상세의 거래 조회에서 취소·환불 금액에 원화 단위가 고정 표기되던 문제를 수정했습니다. 이제 주문 시점 기준 통화로 표시됩니다. - 결제 취소 기록의 환불 금액 표기도 결제 통화를 따르도록 수정했습니다. diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json b/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json index 375d28ae..81920c80 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-pay_nhnkcp", "description": "NHN KCP PG Plugin for G7 platform", - "version": "1.0.1", + "version": "1.0.2", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-pay_nhnkcp/dist/js/plugin.iife.js index aeb84cc3..0534dad6 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/dist/js/plugin.iife.js @@ -15,4 +15,3 @@ try { } <\/script> </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.receipt_url??void 0})}});a.id=de;const s=r.lastElementChild;r.insertBefore(a,s);const i="kcp-oc-cash-receipt-btn";if(!document.getElementById(i)&&(t.payment_method==="vbank"||t.payment_method==="bank_transfer")){const c=ue("현금영수증 조회",a.className.replace(/bg-green-\d+/g,"bg-teal-600").replace(/hover:bg-green-\d+/g,"hover:bg-teal-700"),async d=>{d.disabled=!0,d.textContent="로딩 중...";const l=await O(e);if(d.disabled=!1,d.textContent="현금영수증 조회",l){const u=window.KcpReceiptPopup;u&&new u({cash_url:l.cash_receipt_url??void 0})}});c.id=i,r.insertBefore(c,s)}console.info(`[${L}] receipt button injected on order complete page`)}function ft(){const t=location.pathname.match(lt);t&&mt(t[1])}function yt(){if(typeof window>"u")return;const e=window;if(e[ce])return;e[ce]=!0,console.info(`[${L}] order complete receipt injector installed`);const t=(o=1200)=>setTimeout(ft,o);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...o)=>{n(...o),t()},window.addEventListener("popstate",()=>t(500))}const _="sirsoft-pay_nhnkcp",pe="__kcpMpShowInjectorInstalled",K="kcp-mp-vbank-row",j="kcp-mp-receipt-row",v="kcp-mp-mock-deposit",ht=/^\/mypage\/orders\/([^/]+)$/;function _t(e){try{const n=window.G7Core?.getState,a=n?.()?.currentDataContext?.order?.data;return!a||a.order_number!==e?null:a}catch{return null}}function me(){return localStorage.getItem("auth_token")}async function fe(e){const t=me();if(!t)return null;try{const n=await fetch(`/api/plugins/${_}/user/orders/${e}/receipt`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}async function ye(e){const t=me();if(!t)return null;try{const n=await fetch(`/api/plugins/${_}/user/orders/${e}/vbank-mock-deposit-info`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}function he(e){const t=document.createElement("div");t.id=v,t.className="pt-4 mt-2 border-t border-dashed border-orange-300 dark:border-orange-700";const n=document.createElement("div");n.className="flex items-center gap-2 mb-3";const o=document.createElement("span");o.className="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300 border border-orange-300 dark:border-orange-700",o.textContent="TEST";const r=document.createElement("p");r.className="text-sm font-semibold text-orange-700 dark:text-orange-300",r.textContent="모의입금처리",n.appendChild(o),n.appendChild(r);const a=document.createElement("form");a.method="POST",a.action=e.mock_url??"",a.target="_blank",a.className="space-y-2";const s=[{label:"KCP 거래번호",name:"e_trade_no",value:e.trade_no??""},{label:"입금계좌",name:"deposit_no",value:e.account_no??""},{label:"입금자명",name:"req_name",value:"NHN KCP",editable:!0},{label:"입금통보 URL",name:"noti_url",value:e.notify_url??""}];for(const d of s){const l=document.createElement("div");l.className="flex items-center gap-2 text-sm";const u=document.createElement("span");u.className="w-28 flex-shrink-0 text-gray-500 dark:text-gray-400 text-xs",u.textContent=d.label;const p=document.createElement("input");p.type="text",p.name=d.name,p.value=d.value,p.readOnly=!d.editable,p.className=d.editable?"flex-1 px-2 py-1 text-xs border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono":"flex-1 px-2 py-1 text-xs border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 font-mono",l.appendChild(u),l.appendChild(p),a.appendChild(l)}const i=document.createElement("div");i.className="pt-1";const c=document.createElement("button");if(c.type="submit",c.className="w-full py-1.5 text-xs font-semibold rounded border border-orange-400 dark:border-orange-600 bg-orange-50 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 hover:bg-orange-100 dark:hover:bg-orange-900/50 transition-colors",c.textContent="입금통보 테스트",i.appendChild(c),a.appendChild(i),t.appendChild(n),t.appendChild(a),e.is_admin_view){const d=document.createElement("p");d.className="mt-2 text-xs text-center text-orange-500 dark:text-orange-400 font-medium",d.textContent="관리자에게만 출력되는 내용",t.appendChild(d)}return t}function _e(){const e=document.getElementById("order_payment_info_panel");if(e)return Array.from(e.children).find(a=>a.className?.includes("space-y"))??e;const t=Array.from(document.querySelectorAll("h3")).find(r=>r.textContent?.includes("결제 정보"));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 w(e,t,n="font-medium text-gray-900 dark:text-white"){const o=document.createElement("div");o.className="flex items-center justify-between text-sm";const r=document.createElement("span");r.className="text-gray-500 dark:text-gray-400",r.textContent=e;const a=document.createElement("span");return a.className=n,a.textContent=t,o.appendChild(r),o.appendChild(a),o}function bt(e,t){if(!t)return!1;const n=Array.from(e.querySelectorAll("div"));for(const o of n){const r=Array.from(o.children).filter(i=>i instanceof HTMLElement&&i.tagName==="SPAN");if(r.length<2)continue;const a=r[0].textContent?.trim();if(a!=="결제 방법"&&a!=="결제수단"&&a!=="Payment Method")continue;const s=r[r.length-1];return s.textContent?.trim()===t?(o.dataset.nhnkcpPaymentMethodRow="true",!0):(s.textContent=t,s.dataset.nhnkcpPaymentMethodPatched="true",o.dataset.nhnkcpPaymentMethodRow="true",!0)}return!1}function gt(e,t){const n=document.createElement("div");n.id=K,n.className="pt-4 mt-2 border-t border-gray-200 dark:border-gray-700 space-y-3";const o=document.createElement("p");o.className="text-sm font-semibold text-gray-800 dark:text-gray-200",o.textContent="가상계좌 입금 안내";const r=document.createElement("div");r.className="space-y-1.5",r.appendChild(w("은행",e.vbank_name??"")),r.appendChild(w("계좌번호",e.vbank_number??"","font-medium text-gray-900 dark:text-white font-mono tracking-wide")),r.appendChild(w("예금주",e.vbank_holder??"")),r.appendChild(w("입금 금액",t)),e.due_date_formatted&&r.appendChild(w("입금 기한",e.due_date_formatted));const a=document.createElement("p");return a.className="text-xs text-amber-600 dark:text-amber-400",a.textContent="입금 기한 내에 입금하지 않으시면 주문이 자동 취소됩니다.",n.appendChild(o),n.appendChild(r),n.appendChild(a),n}function wt(e){const t=document.createElement("div");t.id=j,t.className="pt-4 mt-2 border-t border-gray-200 dark:border-gray-700";const n=document.createElement("div");n.className="flex items-center justify-between";const o=document.createElement("span");o.className="text-gray-500 dark:text-gray-400 text-sm",o.textContent="영수증";const r=document.createElement("button");return r.type="button",r.className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-50",r.textContent="영수증 조회",r.addEventListener("click",async()=>{r.disabled=!0,r.textContent="로딩 중...";const a=await fe(e);if(r.disabled=!1,r.textContent="영수증 조회",a){const s=window.KcpReceiptPopup;s&&new s({url:a.receipt_url,cash_url:a.cash_receipt_url})}}),n.appendChild(o),n.appendChild(r),t.appendChild(n),t}async function kt(e){const t=_t(e);if(!t){if(!document.getElementById(v)){const p=_e();if(!p)return!1;const f=await ye(e);f?.available&&(p.appendChild(he(f)),console.info(`[${_}] mock deposit form injected on mypage order show`))}return!0}const{payment:n}=t;if(!n||n.pg_provider!=="nhnkcp")return!0;const o=n.payment_method==="vbank"&&!!n.vbank_name,r=!!n.transaction_id&&!!n.paid_at,a=n.payment_method==="vbank"&&!!n.vbank_number,s=!o||!!document.getElementById(K),i=!r||!!document.getElementById(j),c=!a||!!document.getElementById(v);if(s&&i&&c)return!0;const d=_e();if(!d)return!1;const l=r?await fe(e):null,u=bt(d,l?.payment_method_display_label);if(!document.getElementById(K)&&o)return d.appendChild(gt(n,t.total_amount_formatted??"")),console.info(`[${_}] vbank info injected on mypage order show`),!1;if(!document.getElementById(j)&&r)return d.appendChild(wt(e)),console.info(`[${_}] receipt button injected on mypage order show`),!1;if(u&&console.info(`[${_}] payment method display patched on mypage order show`),!document.getElementById(v)&&a){const p=await ye(e);if(p?.available)d.appendChild(he(p)),console.info(`[${_}] mock deposit form injected on mypage order show`);else return!0}return!0}function Ct(e){let t=0;const n=setInterval(()=>{t++,kt(e).then(o=>{(o||t>=30)&&clearInterval(n)})},400)}function xt(){const e=location.pathname.match(ht);e&&Ct(e[1])}function vt(){if(typeof window>"u")return;const e=window;if(e[pe])return;e[pe]=!0,console.info(`[${_}] mypage order show injector installed`);const t=(o=1500)=>setTimeout(xt,o);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...o)=>{n(...o),t(600)},window.addEventListener("popstate",()=>t(500))}const Pt="sirsoft-pay_nhnkcp",be="__kcpAdminApplePayNoticeInjectorInstalled",Et=/^\/admin\/ecommerce\/settings\/?$/,ge="__kcpAdminApplePayNoticeListenerAttached",Nt="애플페이는 IOS 기기에 모바일결제만 가능합니다.",At="Apple Pay is available only for mobile payments on iOS devices.";let we=null,P=null,B=!1;function It(){return window}function D(){if(!Et.test(window.location.pathname))return!1;const e=new URLSearchParams(window.location.search).get("tab");return e===null||e==="order_settings"}function ke(){return(document.documentElement.lang||navigator.language||"").toLowerCase().startsWith("ko")?Nt:At}function Ce(e){return(e??"").replace(/\s+/g," ").trim()}function St(e){const t=Ce(e.textContent);return t==="애플페이"||t==="Apple Pay"}function Mt(){const e=document.createElement("span");return e.dataset.nhnkcpAdminApplePayNotice="true",e.className="text-label-subtle",e.style.display="block",e.style.width="100%",e.style.flexBasis="100%",e.style.marginTop="2px",e.style.fontSize="12px",e.style.lineHeight="1rem",e.textContent=ke(),e}function H(e=document){if(typeof window>"u"||typeof document>"u"||!D()||Ce(document.body?.innerText??document.body?.textContent).includes(ke()))return!1;const n=Array.from(e.querySelectorAll("span, p, div")).find(St);return n?(n.insertAdjacentElement("afterend",Mt()),!0):!1}function Tt(){B||(B=!0,window.setTimeout(()=>{B=!1,H()},0))}function xe(){P!==null&&(window.clearInterval(P),P=null)}function F(){if(!D())return;xe(),H();let e=0;P=window.setInterval(()=>{e+=1,(H()||e>=50)&&xe()},200);const t=document.body;t[ge]||(t[ge]=!0,we=new MutationObserver(()=>Tt()),we.observe(document.body,{childList:!0,subtree:!0,characterData:!0}))}function ve(){D()&&F()}function Rt(){if(typeof window>"u"||typeof document>"u")return;const e=It();if(e[be])return;e[be]=!0,console.info(`[${Pt}] admin Apple Pay notice injector installed`),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>F()):F();const t=history.pushState.bind(history);history.pushState=(...n)=>{t(...n),window.setTimeout(()=>ve(),200)},window.addEventListener("popstate",()=>window.setTimeout(()=>ve(),200))}const $t="sirsoft-pay_nhnkcp",Pe="__nhnkcpAdminPaymentMethodBrandInjectorInstalled",Ee="__nhnkcpAdminPaymentMethodBrandSyncListenerAttached",Lt=/^\/admin\/ecommerce\/settings\/?$/,Ne='[data-nhnkcp-admin-payment-brand-mark="true"]',Ot=200,Kt=120,jt=[{id:"nhnkcp_payco",labels:["PAYCO (NHN KCP)","PAYCO로 결제 (NHN KCP)","Pay with PAYCO (NHN KCP)"],shortLabels:["PAYCO"],markLines:["P"],markClassName:"bg-red-500 text-white"},{id:"nhnkcp_naverpay",labels:["네이버페이 (NHN KCP)","네이버페이 신용카드로 결제 (NHN KCP)","Naver Pay (NHN KCP)","Pay by Naver Pay credit card (NHN KCP)"],shortLabels:["네이버페이 (카드)","Naver Pay (Card)"],markLines:["N"],markClassName:"bg-green-500 text-white"},{id:"nhnkcp_naverpay_point",labels:["네이버페이 포인트 (NHN KCP)","네이버페이 머니/포인트로 결제 (NHN KCP)","Naver Pay Point (NHN KCP)","Pay with Naver Pay Money/Points (NHN KCP)"],shortLabels:["네이버페이 (포인트)","Naver Pay (Point)"],markLines:["NP"],markClassName:"bg-green-600 text-white"},{id:"nhnkcp_kakaopay",labels:["카카오페이 (NHN KCP)","카카오페이로 결제 (NHN KCP)","Kakao Pay (NHN KCP)","Pay with Kakao Pay (NHN KCP)"],shortLabels:["카카오페이","Kakao Pay"],markLines:["K"],markClassName:"bg-yellow-400 text-gray-950"},{id:"nhnkcp_applepay",labels:["애플페이 (NHN KCP)","애플페이로 결제 (NHN KCP)","Apple Pay (NHN KCP)","Pay with Apple Pay (NHN KCP)"],shortLabels:["애플페이","Apple Pay"],markLines:["A"],markClassName:"bg-gray-900 text-white"}];let Ae=null,E=null,q=!1;function Bt(){return window}function U(){if(!Lt.test(window.location.pathname))return!1;const e=new URLSearchParams(window.location.search).get("tab");return e===null||e===""||e==="order_settings"}function Dt(e){return e instanceof HTMLElement?e.className:""}function Ie(e){return(e??"").replace(/\u200B/g,"").replace(/\s+/g," ").trim()}function Ht(e){const t=Dt(e);return t.includes("excel-card")||t.includes("flex-center")&&t.includes("border")&&t.includes("gap-4")}function Se(e){return e.querySelector(".font-medium")}function Ft(e){const t=Ie(Se(e)?.textContent),n=Ie(e.textContent);return jt.find(o=>o.shortLabels.some(r=>t===r)||o.labels.some(r=>n.includes(r)))??null}function qt(e){return Array.from(e.querySelectorAll("div")).filter(Ht)}function Ut(e){return Array.from(e.querySelectorAll("svg, [data-icon], i")).find(n=>!n.closest("[data-drag-handle]")&&!n.closest(Ne)&&!n.closest('.row-stack, .excel-card-body, button, select, [role="switch"]'))??null}function Me(e,t){e.dataset.nhnkcpAdminPaymentBrandMark="true",e.dataset.nhnkcpAdminPaymentMethod=t.id,e.setAttribute("aria-hidden","true"),e.className=`inline-flex items-center justify-center rounded-lg font-bold ${t.markClassName}`,e.style.width="32px",e.style.height="32px",e.style.flex="0 0 32px",e.style.lineHeight="1",e.style.fontSize=t.markLines.join("").length>2?"9px":"12px";const n=t.markLines.join("");e.textContent!==n&&(e.replaceChildren(),t.markLines.length>1?(e.style.flexDirection="column",t.markLines.forEach(o=>{const r=document.createElement("span");r.style.lineHeight="1",r.textContent=o,e.appendChild(r)})):(e.style.removeProperty("flex-direction"),e.textContent=t.markLines[0]??""))}function Yt(e){const t=document.createElement("span");return Me(t,e),t}function Gt(e,t){const n=e.querySelector(Ne);if(n instanceof HTMLSpanElement)return e.dataset.nhnkcpAdminPaymentMethod=t.id,Me(n,t),!0;const o=Yt(t),r=Ut(e);if(r&&r.parentElement)return e.dataset.nhnkcpAdminPaymentMethod=t.id,r.replaceWith(o),!0;const a=Se(e);return a&&a.parentElement?(e.dataset.nhnkcpAdminPaymentMethod=t.id,a.parentElement.insertBefore(o,a),!0):!1}function Y(e=document){if(typeof window>"u"||typeof document>"u"||!U())return!1;let t=!1;return qt(e).forEach(n=>{const o=Ft(n);o&&Gt(n,o)&&(t=!0)}),t}function Wt(){q||(q=!0,window.setTimeout(()=>{q=!1,Y()},0))}function G(){E!==null&&(window.clearInterval(E),E=null)}function W(){if(!U())return;G(),Y();let e=0;E=window.setInterval(()=>{e+=1,Y(),e>=Kt&&G()},Ot);const t=document.body;t[Ee]||(t[Ee]=!0,Ae=new MutationObserver(()=>Wt()),Ae.observe(document.body,{childList:!0,subtree:!0,characterData:!0}))}function z(){if(U()){W();return}G()}function zt(){if(typeof window>"u"||typeof document>"u")return;const e=Bt();if(e[Pe])return;e[Pe]=!0,console.info(`[${$t}] admin payment method brand sync installed`),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>W()):W();const t=history.pushState.bind(history);history.pushState=(...o)=>{t(...o),window.setTimeout(()=>z(),200)};const n=history.replaceState.bind(history);history.replaceState=(...o)=>{n(...o),window.setTimeout(()=>z(),200)},window.addEventListener("popstate",()=>window.setTimeout(()=>z(),200))}const J="sirsoft-pay_nhnkcp",Te="__nhnkcpAdminPaymentDisplayInjectorInstalled",Jt=/^\/admin\/ecommerce\/orders\/([^/]+)$/;function Vt(){return localStorage.getItem("auth_token")}async function Qt(e){const t=Vt();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 o=await n.json();return("data"in o?o.data:o)??null}catch{return null}}function Xt(e){return Array.from(e.children).filter(t=>t instanceof HTMLElement)}function Zt(e,t){let n=!1;const o=Array.from(e.querySelectorAll("div"));for(const r of o){const a=Xt(r),s=a.findIndex(u=>u.textContent?.trim()==="결제수단");if(s<0)continue;const i=a.slice(s+1).find(u=>u.textContent?.trim()&&u.textContent.trim()!=="결제수단");if(!i)continue;const c=a.slice(s+2).find(u=>/^\(.+\)$/.test(u.textContent?.trim()??"")),d=c?.textContent?.trim().replace(/^\(|\)$/g,"")??"",l=d&&t.endsWith(")")?t.replace(/\)$/,`, ${d})`):t;i.textContent?.trim()!==l&&(i.textContent=l,i.dataset.nhnkcpPaymentMethodPatched="true"),r.dataset.nhnkcpPaymentMethodRow="admin-order",c&&d&&(c.textContent="",c.style.display="none",c.dataset.nhnkcpPaymentInstallmentHidden="true"),n=!0}return n}function en(e,t,n){const r=Array.from(e.querySelectorAll("span")).filter(a=>a.textContent?.trim()===t).find(a=>{const s=typeof a.className=="string"?a.className:"";return s.includes("rounded-full")||s.includes("font-medium")});return r?(r.textContent=n,r.dataset.nhnkcpPaymentMethodPatched="true",r.dataset.nhnkcpPaymentMethodBadge="admin-order",!0):!1}function tn(e,t){const n=t?._pay_method_label,o=t?._embedded_pg_provider_label,r=t?._base_pay_method_label;if(!n||!o)return!1;const a=Zt(e,n),s=r?en(e,r,o):!1;return a||s}async function nn(e){const t=document.getElementById("section_payment_info")??document.getElementById("payment_info_card");if(!t)return!1;const n=await Qt(e);return n&&tn(t,n)&&console.info(`[${J}] payment method display patched on admin order detail`),!0}function on(e){let t=0;const n=setInterval(()=>{t++,nn(e).then(o=>{(o||t>=30)&&clearInterval(n)})},400)}function rn(){const e=location.pathname.match(Jt);e&&on(e[1])}function an(){if(typeof window>"u")return;const e=window;if(e[Te])return;e[Te]=!0,console.info(`[${J}] admin order payment display injector installed`);const t=(o=1500)=>setTimeout(rn,o);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...o)=>{n(...o),t(600)},window.addEventListener("popstate",()=>t(500))}class sn{constructor(t){const n="width=800,height=600,scrollbars=yes,resizable=yes,toolbar=no,menubar=no";t.url&&window.open(t.url,"kcp_receipt",n),t.cash_url&&window.open(t.cash_url,"kcp_cash_receipt",n)}}window.KcpReceiptPopup=sn;const b="sirsoft-pay_nhnkcp",V={info:(...e)=>console.info(`[${b}]`,...e),warn:(...e)=>console.warn(`[${b}]`,...e),error:(...e)=>console.error(`[${b}]`,...e)};function Re(){const e=window.G7Core;if(!e)return 0;const t=e.getActionDispatcher;if(typeof t!="function")return 0;const n=t();if(!n||typeof n.registerHandler!="function")return 0;let o=0;for(const[r,a]of Object.entries(ae)){const s=`${b}.${r}`;n.registerHandler(s,a,{category:"plugin",source:b}),o++}return o}function $e(){const e=()=>{const t=Re();if(t>0){V.info(`${t} handler(s) registered`);return}let n=0;const o=50,r=setInterval(()=>{n++;const a=Re();if(a>0){clearInterval(r),V.info(`${a} handler(s) registered (after ${n} retries)`);return}n>=o&&(clearInterval(r),V.warn("ActionDispatcher not available after timeout"))},100)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()}dt(),yt(),vt(),Rt(),zt(),an(),$e(),window.__SirsoftNhnkcp={identifier:b,handlers:Object.keys(ae),initPlugin:$e}})(); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/docs/api/vbank.md b/plugins/_bundled/sirsoft-pay_nhnkcp/docs/api/vbank.md index 9dd9011f..249c0599 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/docs/api/vbank.md +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/docs/api/vbank.md @@ -30,3 +30,17 @@ 이 IP 화이트리스트 검사는 코어의 확장 미들웨어 self-gate(`Plugin::getMiddleware()` 의 `targets` 로 위 두 통보 라우트명에만 정밀 타게팅)로 수행된다. 브라우저가 접속하는 결제 콜백(`payment.callback`)에는 적용되지 않는다 — 콜백은 정상 사용자의 브라우저에서 임의 IP 로 도달하므로 IP 로 제한하면 결제가 끊긴다. 검사 자체의 동작·허용 범위는 라우트 파일에서 직접 부착하던 이전 방식과 동일하다. 상세: [docs/backend/middleware.md "확장 미들웨어 선언 (self-gate)"](../../../../../docs/backend/middleware.md). + +--- + +## 결제 실패 리다이렉트 규약 (브라우저 콜백) + +결제창에서 돌아오는 브라우저 콜백은 JSON 응답이 아니라 상점 실패 페이지로 **리다이렉트**하며, 실패 사유를 쿼리스트링으로 전달합니다. + +| 쿼리 | 값 | 설명 | +| --- | --- | --- | +| `error` | 실패 코드 | 기계 판독용 고정 식별자 (`authorize_failed` · `approve_failed` 등). 화면 분기·문의 접수의 기준값 | +| `message` | 안내 문구 | 구매자에게 보여 줄 다국어 문구. 상점 실패 페이지가 그대로 출력합니다 | +| `orderId` | 주문번호 | 실패한 주문의 식별자 | + +`message` 에는 예외 원문(내부 오류 메시지·SQL 상태코드·클래스명·경로)을 싣지 않습니다. 이 값은 브라우저 주소창과 참조 로그에 남고 실패 페이지에 그대로 출력되므로, 내부 정보가 구매자와 중간 경유지에 노출됩니다. 원인 파악에 필요한 원문은 서버 로그(`Log::error`)에만 기록합니다. diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/lang/en/messages.php b/plugins/_bundled/sirsoft-pay_nhnkcp/lang/en/messages.php index 116e7b99..c2cea53b 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/lang/en/messages.php @@ -8,7 +8,12 @@ return [ 'default_reason' => 'Buyer refund request', 'in_progress' => 'NHN KCP refund is already in progress.', ], + 'escrow' => [ + 'invoice_required' => 'Please enter the tracking number.', + 'courier_required' => 'Please select a courier.', + ], 'errors' => [ + 'payment_failed' => 'Payment could not be completed. Please try again in a moment.', 'wsdl_missing' => 'KCP WSDL file not found: :file', 'approval_key_error' => 'KCP approval key error [:code]: :message', 'soap_error' => 'KCP SOAP integration error: :message', diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/lang/ko/messages.php b/plugins/_bundled/sirsoft-pay_nhnkcp/lang/ko/messages.php index 866a4515..8efa6cb3 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/lang/ko/messages.php @@ -8,7 +8,12 @@ return [ 'default_reason' => '구매자 환불 요청', 'in_progress' => 'NHN KCP 환불이 이미 처리 중입니다.', ], + 'escrow' => [ + 'invoice_required' => '운송장번호를 입력해주세요.', + 'courier_required' => '택배사를 선택해주세요.', + ], 'errors' => [ + 'payment_failed' => '결제 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.', 'wsdl_missing' => 'KCP WSDL 파일이 없습니다: :file', 'approval_key_error' => 'KCP 승인키 오류 [:code]: :message', 'soap_error' => 'KCP SOAP 연동 오류: :message', diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json b/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json index 53516502..c847ad60 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/package.json b/plugins/_bundled/sirsoft-pay_nhnkcp/package.json index 9b8a119a..a10bc4a2 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/package.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-pay_nhnkcp", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json b/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json index e84f7a9a..58774553 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/plugin.json @@ -5,7 +5,7 @@ "ko": "NHN KCP", "en": "NHN KCP" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nhnkcp/blob/main/CHANGELOG.md", diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-pay_nhnkcp/resources/layouts/admin/plugin_settings.json index 3e98ac50..9c1d3c38 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/resources/layouts/admin/plugin_settings.json @@ -1903,7 +1903,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminEscrowDeliveryController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminEscrowDeliveryController.php index 18b02039..71948caa 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminEscrowDeliveryController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminEscrowDeliveryController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 에스크로 배송등록 엔드포인트의 기존 문서 부재(사전 상태) — docgen 재생성은 기존 수기 문서(vbank/transaction-status)를 파괴해 신설 불가. 검증 규칙은 FormRequest·CHANGELOG 에 기록, 문서 신설은 후속 백로그 + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -7,10 +9,13 @@ namespace Plugins\Sirsoft\PayNhnkcp\Controllers; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderAddress; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayNhnkcp\Concerns\SanitizesPgResponse; +use Plugins\Sirsoft\PayNhnkcp\Http\Requests\EscrowDeliveryRegisterRequest; use Plugins\Sirsoft\PayNhnkcp\Services\NhnKcpApiService; /** @@ -31,8 +36,8 @@ class AdminEscrowDeliveryController extends AdminBaseController 'deli_corp', ]; - /** KCP 에스크로 택배사 코드표 */ - private const COURIER_CODES = [ + /** 택배사 코드 → 택배사명 매핑 (KCP 공식 코드표 — EscrowDeliveryRegisterRequest 의 허용값 SSoT) */ + public const COURIER_CODES = [ '04' => 'CJ대한통운', '05' => '한진택배', '06' => '로젠택배', @@ -72,8 +77,9 @@ class AdminEscrowDeliveryController extends AdminBaseController return ResponseHelper::success('common.success', null); } - $address = DB::table('ecommerce_order_addresses as a') - ->join('ecommerce_orders as o', 'o.id', '=', 'a.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $address = DB::table((new OrderAddress)->getTable().' as a') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'a.order_id') ->where('o.order_number', $orderNumber) ->where('a.address_type', 'shipping') ->select([ @@ -89,14 +95,14 @@ class AdminEscrowDeliveryController extends AdminBaseController $escrowDelivery = $meta['escrow_delivery'] ?? null; return ResponseHelper::success('common.success', [ - 'has_escrow_payment' => true, - 'tno' => $payment->transaction_id, - 'courier_codes' => self::COURIER_CODES, - 'prefill' => [ + 'has_escrow_payment' => true, + 'tno' => $payment->transaction_id, + 'courier_codes' => self::COURIER_CODES, + 'prefill' => [ 'recv_name' => $address?->recipient_name ?? '', - 'recv_tel' => $address?->recipient_phone ?? '', + 'recv_tel' => $address?->recipient_phone ?? '', 'recv_post' => $address?->zipcode ?? '', - 'recv_addr' => trim(($address?->address ?? '') . ' ' . ($address?->address_detail ?? '')), + 'recv_addr' => trim(($address?->address ?? '').' '.($address?->address_detail ?? '')), ], 'registered_delivery' => $escrowDelivery, ]); @@ -110,22 +116,17 @@ class AdminEscrowDeliveryController extends AdminBaseController * * KCP 에스크로 API 로 배송 정보(택배사/송장번호/수령인 등) 등록. 등록 완료 시 구매확정 안내 자동 발송. * - * @param Request $request 배송 정보 폼 + * 형식·길이·택배사 코드 검증은 EscrowDeliveryRegisterRequest 가 담당한다 + * (종전 인라인 한글 하드코딩 메시지를 다국어 키로 이관). + * + * @param EscrowDeliveryRegisterRequest $request 배송 정보 폼 * @param string $orderNumber 주문번호 * @return JsonResponse 등록 결과 */ - public function register(Request $request, string $orderNumber): JsonResponse + public function register(EscrowDeliveryRegisterRequest $request, string $orderNumber): JsonResponse { - $deliNumb = trim((string) $request->input('deli_numb', '')); - $deliCorp = trim((string) $request->input('deli_corp', '')); - - if ($deliNumb === '') { - return ResponseHelper::error('common.failed', 422, ['deli_numb' => ['운송장번호를 입력해주세요.']]); - } - - if ($deliCorp === '' || ! array_key_exists($deliCorp, self::COURIER_CODES)) { - return ResponseHelper::error('common.failed', 422, ['deli_corp' => ['택배사를 선택해주세요.']]); - } + $deliNumb = trim((string) $request->validated('deli_numb')); + $deliCorp = trim((string) $request->validated('deli_corp')); $payment = $this->findEscrowPayment($orderNumber); @@ -135,9 +136,9 @@ class AdminEscrowDeliveryController extends AdminBaseController Log::info('KCP: escrow delivery register requested', [ 'order_number' => $orderNumber, - 'tno' => $payment->transaction_id, - 'deli_numb' => $deliNumb, - 'deli_corp' => $deliCorp, + 'tno' => $payment->transaction_id, + 'deli_numb' => $deliNumb, + 'deli_corp' => $deliCorp, ]); try { @@ -152,37 +153,38 @@ class AdminEscrowDeliveryController extends AdminBaseController $meta = $payment->payment_meta ? json_decode($payment->payment_meta, true) : []; $meta['escrow_delivery'] = [ 'registered_at' => now()->toDateTimeString(), - 'deli_numb' => $deliNumb, - 'deli_corp' => $deliCorp, - 'courier_name' => $courierName, + 'deli_numb' => $deliNumb, + 'deli_corp' => $deliCorp, + 'courier_name' => $courierName, 'pg_response_sanitized' => true, - 'pg_response' => $this->sanitizePgResponse($pgResponse, self::ESCROW_DELIVERY_RESPONSE_KEYS), + 'pg_response' => $this->sanitizePgResponse($pgResponse, self::ESCROW_DELIVERY_RESPONSE_KEYS), ]; - DB::table('ecommerce_order_payments') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + DB::table((new OrderPayment)->getTable()) ->where('id', $payment->id) ->update([ 'payment_meta' => json_encode($meta, JSON_UNESCAPED_UNICODE), - 'updated_at' => now(), + 'updated_at' => now(), ]); Log::info('KCP: escrow delivery registered', [ 'order_number' => $orderNumber, - 'tno' => $payment->transaction_id, - 'deli_numb' => $deliNumb, + 'tno' => $payment->transaction_id, + 'deli_numb' => $deliNumb, 'courier_name' => $courierName, ]); return ResponseHelper::success('common.success', [ - 'res_cd' => $pgResponse['res_cd'] ?? '0000', - 'deli_numb' => $deliNumb, + 'res_cd' => $pgResponse['res_cd'] ?? '0000', + 'deli_numb' => $deliNumb, 'courier_name' => $courierName, ]); } catch (\Exception $e) { Log::error('KCP: escrow delivery register exception', [ 'order_number' => $orderNumber, - 'error' => $e->getMessage(), + 'error' => $e->getMessage(), ]); return ResponseHelper::error('common.failed', 500, [ @@ -193,8 +195,9 @@ class AdminEscrowDeliveryController extends AdminBaseController private function findEscrowPayment(string $orderNumber): ?object { - return DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + return DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'nhnkcp') ->where('p.is_escrow', true) diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminOrderListController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminOrderListController.php index b63d60ea..d05381bd 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminOrderListController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminOrderListController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 룰 면제 주석만 추가 — 요청/응답 계약 불변 (해당 엔드포인트 문서 부재는 사전 상태) + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -9,6 +11,8 @@ use App\Http\Controllers\Api\Base\AdminBaseController; use App\Services\PluginSettingsService; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay; class AdminOrderListController extends AdminBaseController @@ -36,8 +40,9 @@ class AdminOrderListController extends AdminBaseController */ public function testModeMap(): JsonResponse { - $query = DB::table('ecommerce_orders as o') - ->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $query = DB::table((new Order)->getTable().' as o') + ->join((new OrderPayment)->getTable().' as p', 'p.order_id', '=', 'o.id') ->where('p.pg_provider', 'nhnkcp') ->where('p.created_at', '>=', now()->subMonths(6)); @@ -72,8 +77,9 @@ class AdminOrderListController extends AdminBaseController */ public function easyPayDisplayMap(): JsonResponse { - $rows = DB::table('ecommerce_orders as o') - ->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $rows = DB::table((new Order)->getTable().' as o') + ->join((new OrderPayment)->getTable().' as p', 'p.order_id', '=', 'o.id') ->where('p.pg_provider', 'nhnkcp') ->where('p.created_at', '>=', now()->subMonths(6)) ->select([ diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminTransactionController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminTransactionController.php index 16101073..613529ec 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminTransactionController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/AdminTransactionController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 룰 면제 주석만 추가 — 요청/응답 계약 불변 (해당 엔드포인트 문서 부재는 사전 상태) + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -11,6 +13,9 @@ use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum; use Modules\Sirsoft\Ecommerce\Enums\RefundStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; +use Modules\Sirsoft\Ecommerce\Models\OrderRefund; use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay; /** @@ -45,8 +50,9 @@ class AdminTransactionController extends AdminBaseController */ public function queryByOrder(string $orderNumber): JsonResponse { - $payment = DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $payment = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'nhnkcp') ->whereNotNull('p.transaction_id') @@ -89,15 +95,15 @@ class AdminTransactionController extends AdminBaseController $refundAmount = $refund ? (float) ($refund->refund_amount ?? 0) : 0.0; return ResponseHelper::success('common.success', [ - 'tno' => $payment->transaction_id, - 'app_no' => $rawResponse['app_no'] ?? $meta['app_no'] ?? null, + 'tno' => $payment->transaction_id, + 'app_no' => $rawResponse['app_no'] ?? $meta['app_no'] ?? null, 'use_pay_method' => $meta['use_pay_method'] ?? $rawResponse['use_pay_method'] ?? null, - 'app_time' => $meta['app_time'] ?? $rawResponse['app_time'] ?? null, - 'res_cd' => $meta['res_cd'] ?? $rawResponse['res_cd'] ?? '0000', - 'card_name' => $rawResponse['card_name'] ?? $meta['card_name'] ?? $rawResponse['bank_name'] ?? $meta['bank_name'] ?? null, - 'account' => $meta['account'] ?? null, - 'bank_name' => $rawResponse['bank_name'] ?? $meta['bank_name'] ?? null, - '_is_test_mode' => $isTest, + 'app_time' => $meta['app_time'] ?? $rawResponse['app_time'] ?? null, + 'res_cd' => $meta['res_cd'] ?? $rawResponse['res_cd'] ?? '0000', + 'card_name' => $rawResponse['card_name'] ?? $meta['card_name'] ?? $rawResponse['bank_name'] ?? $meta['bank_name'] ?? null, + 'account' => $meta['account'] ?? null, + 'bank_name' => $rawResponse['bank_name'] ?? $meta['bank_name'] ?? null, + '_is_test_mode' => $isTest, 'payment_status' => $paymentStatus?->value ?? $payment->payment_status, 'payment_status_label' => $paymentStatus?->label(), 'payment_status_variant' => $paymentStatus?->variant(), @@ -123,7 +129,8 @@ class AdminTransactionController extends AdminBaseController private function latestRefundForOrder(int $orderId): ?object { - return DB::table('ecommerce_order_refunds') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + return DB::table((new OrderRefund)->getTable()) ->where('order_id', $orderId) ->orderByDesc('id') ->select([ diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/MobileApprovalController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/MobileApprovalController.php index 2d122af9..12ff1425 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/MobileApprovalController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/MobileApprovalController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 룰 면제 주석만 추가 — 요청/응답 계약 불변 (해당 엔드포인트 문서 부재는 사전 상태) + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -43,7 +45,7 @@ class MobileApprovalController /** 간편결제 direct 파라미터 기본값 (매 결제 시 초기화) */ private const EASY_PAY_DIRECT_DEFAULTS = [ - 'payco_direct' => '', + 'payco_direct' => '', 'naverpay_direct' => 'A', 'kakaopay_direct' => 'A', 'applepay_direct' => 'A', @@ -51,11 +53,11 @@ class MobileApprovalController /** 간편결제별 direct 파라미터 override (기본값 위에 덮어씀) */ private const EASY_PAY_DIRECT_FIELDS = [ - 'nhnkcp_payco' => ['payco_direct' => 'Y'], - 'nhnkcp_naverpay' => ['naverpay_direct' => 'Y'], + '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'], + 'nhnkcp_kakaopay' => ['kakaopay_direct' => 'Y'], + 'nhnkcp_applepay' => ['applepay_direct' => 'Y'], ]; private const PLUGIN_IDENTIFIER = 'sirsoft-pay_nhnkcp'; @@ -80,6 +82,7 @@ class MobileApprovalController * @param Request $request 주문/결제 메타 * @return JsonResponse approval_key + pay_url */ + // audit:allow controller-base-request-injection reason: 인라인 validate 로 전 필드 계약 보유 — FormRequest 승격은 후속 백로그 (계획 범위 제외 합의) public function getApprovalKey(Request $request): JsonResponse { $validated = $request->validate([ @@ -249,9 +252,9 @@ class MobileApprovalController $supplyAmt = $taxablePaymentAmt - $vatAmt; // 공급가액 (VAT 제외) return [ - 'tax_flag' => 'TG03', - 'comm_tax_mny' => (string) $supplyAmt, - 'comm_vat_mny' => (string) $vatAmt, + 'tax_flag' => 'TG03', + 'comm_tax_mny' => (string) $supplyAmt, + 'comm_vat_mny' => (string) $vatAmt, 'comm_free_mny' => (string) $taxFreeAmt, ]; } diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/PaymentCallbackController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/PaymentCallbackController.php index 7200253c..0bbf2771 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/PaymentCallbackController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/PaymentCallbackController.php @@ -429,7 +429,7 @@ class PaymentCallbackController return redirect($this->resolveFailUrl([ 'error' => 'confirm_failed', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-pay_nhnkcp::messages.errors.payment_failed'), 'orderId' => $ordrIdxx, ])); } finally { @@ -625,7 +625,7 @@ class PaymentCallbackController return redirect($this->resolveFailUrl([ 'error' => 'cli_exception', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-pay_nhnkcp::messages.errors.payment_failed'), 'orderId' => $ordrIdxx, ])); } @@ -745,7 +745,7 @@ class PaymentCallbackController return redirect($this->resolveFailUrl([ 'error' => 'vbank_save_failed', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-pay_nhnkcp::messages.errors.payment_failed'), 'orderId' => $ordrIdxx, ])); } diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserReceiptController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserReceiptController.php index 723e5336..e85204d5 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserReceiptController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserReceiptController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 룰 면제 주석만 추가 — 요청/응답 계약 불변 (해당 엔드포인트 문서 부재는 사전 상태) + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -8,6 +10,8 @@ use App\Services\PluginSettingsService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; use Plugins\Sirsoft\PayNhnkcp\Concerns\IssuesReceiptCookie; use Plugins\Sirsoft\PayNhnkcp\Concerns\ResolvesEasyPayDisplay; @@ -49,11 +53,13 @@ class UserReceiptController * @param string $orderNumber 주문번호 * @return JsonResponse receipt_url / cash_receipt_url 또는 404 */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — user()/비회원 영수증 쿠키만 참조 (검증 대상 필드 없음) public function show(Request $request, string $orderNumber): JsonResponse { $user = $request->user(); - $query = DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $query = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'nhnkcp') ->whereNotNull('p.transaction_id'); @@ -68,7 +74,7 @@ class UserReceiptController } elseif ($this->verifyReceiptCookie($request->cookie(self::RECEIPT_COOKIE_NAME), $orderNumber)) { $query->whereNull('o.user_id') ->where('o.id', function ($sub) use ($orderNumber) { - $sub->select('id')->from('ecommerce_orders')->where('order_number', $orderNumber); + $sub->select('id')->from((new Order)->getTable())->where('order_number', $orderNumber); }); } else { return response()->json(['error' => 'Not found'], 404); @@ -111,10 +117,10 @@ class UserReceiptController // 휴대폰결제 → mcash_bill, 그 외 (card / bank / vbank) → card_bill $billCmd = $isPhonePayment ? 'mcash_bill' : 'card_bill'; - $receiptUrl = $billBaseUrl . $billCmd - . '&tno=' . urlencode($tno) - . '&order_no=' . urlencode($orderNo) - . '&trade_mony=' . $tradeMony; + $receiptUrl = $billBaseUrl.$billCmd + .'&tno='.urlencode($tno) + .'&order_no='.urlencode($orderNo) + .'&trade_mony='.$tradeMony; // 현금영수증 URL (계좌이체 · 가상계좌만 해당) $cashReceiptUrl = null; @@ -124,16 +130,16 @@ class UserReceiptController $authNo = $pgRaw['app_no'] ?? $pgRaw['receipt_no'] ?? $tno; $siteCd = $this->apiService->getSiteCd(); - $cashReceiptUrl = $cashBaseUrl . $siteCd - . '&orderid=' . urlencode($orderNo) - . '&bill_yn=Y' - . '&authno=' . urlencode((string) $authNo); + $cashReceiptUrl = $cashBaseUrl.$siteCd + .'&orderid='.urlencode($orderNo) + .'&bill_yn=Y' + .'&authno='.urlencode((string) $authNo); } return response()->json([ - 'receipt_url' => $receiptUrl, + 'receipt_url' => $receiptUrl, 'cash_receipt_url' => $cashReceiptUrl, - 'is_test_mode' => $isTest, + 'is_test_mode' => $isTest, 'payment_method_label' => $display['payment_method_label'], 'payment_method_display_label' => $display['payment_method_display_label'], 'selected_payment_method' => $display['selected_payment_method'], diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserVbankMockDepositController.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserVbankMockDepositController.php index e855a4a7..e0e57bfd 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserVbankMockDepositController.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Controllers/UserVbankMockDepositController.php @@ -1,5 +1,7 @@ <?php +// audit:allow api-doc-coverage reason: 룰 면제 주석만 추가 — 요청/응답 계약 불변 (해당 엔드포인트 문서 부재는 사전 상태) + declare(strict_types=1); namespace Plugins\Sirsoft\PayNhnkcp\Controllers; @@ -36,6 +38,7 @@ class UserVbankMockDepositController * @param string $orderNumber 주문번호 * @return JsonResponse 가상계좌 폼 데이터 또는 404 */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — user() 만 참조 (검증 대상 필드 없음) public function show(Request $request, string $orderNumber): JsonResponse { $settings = $this->pluginSettingsService->get(self::PLUGIN_IDENTIFIER) ?? []; diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/src/Http/Requests/EscrowDeliveryRegisterRequest.php b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Http/Requests/EscrowDeliveryRegisterRequest.php new file mode 100644 index 00000000..ef516069 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/src/Http/Requests/EscrowDeliveryRegisterRequest.php @@ -0,0 +1,58 @@ +<?php + +// audit:allow api-doc-coverage reason: 에스크로 배송등록 엔드포인트의 기존 문서 부재(사전 상태) — 검증 규칙은 본 클래스 주석·CHANGELOG 에 기록, 문서 신설은 후속 백로그 + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayNhnkcp\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; +use Plugins\Sirsoft\PayNhnkcp\Controllers\AdminEscrowDeliveryController; + +/** + * 에스크로 배송 등록 요청 검증 + * + * 값은 NHN KCP 에스크로 배송등록 API(mod_type=STE1)로 전송되고 payment_meta 로 + * 영속된다. 상한 근거: KCP 운송장번호 30자. 택배사 코드는 공식 코드표(컨트롤러 상수)가 SSoT. + * 종전 인라인 한글 하드코딩 메시지를 다국어 키로 이관했다 (에스크로 배송등록 3형제 계약 대칭). + */ +class EscrowDeliveryRegisterRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 에스크로 배송 등록 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'deli_numb' => ['required', 'string', 'max:30'], + 'deli_corp' => ['required', 'string', Rule::in(array_keys(AdminEscrowDeliveryController::COURIER_CODES))], + ]; + } + + /** + * 검증 오류 메시지를 반환합니다 (종전 한글 하드코딩을 다국어 키로 이관). + * + * @return array<string, string> 규칙별 메시지 + */ + public function messages(): array + { + return [ + 'deli_numb.required' => __('sirsoft-pay_nhnkcp::messages.escrow.invoice_required'), + 'deli_corp.required' => __('sirsoft-pay_nhnkcp::messages.escrow.courier_required'), + 'deli_corp.in' => __('sirsoft-pay_nhnkcp::messages.escrow.courier_required'), + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/tests/Feature/Controllers/AdminEscrowDeliveryControllerTest.php b/plugins/_bundled/sirsoft-pay_nhnkcp/tests/Feature/Controllers/AdminEscrowDeliveryControllerTest.php index 82467961..8c01e3e3 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/tests/Feature/Controllers/AdminEscrowDeliveryControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/tests/Feature/Controllers/AdminEscrowDeliveryControllerTest.php @@ -72,6 +72,7 @@ class AdminEscrowDeliveryControllerTest extends PluginTestCase ]); $response = $this->actingAs($admin) + ->withHeaders(['Accept-Language' => 'ko']) ->postJson("/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{$order->order_number}/escrow-delivery", [ 'deli_numb' => '1234567890', 'deli_corp' => '04', @@ -92,4 +93,55 @@ class AdminEscrowDeliveryControllerTest extends PluginTestCase $this->assertArrayNotHasKey('recv_addr', $pgResponse); $this->assertArrayNotHasKey('unexpected_sensitive', $pgResponse); } + + /** + * 운송장번호 상한(30자) 초과는 EscrowDeliveryRegisterRequest 가 표준 422 로 차단하고 + * PG 배송등록 API 는 호출되지 않아야 한다 (KCP 운송장번호 30자 스펙). + */ + public function test_register_rejects_overlong_deli_numb(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->createEscrowOrder('KCP_ESCROW_TNO_VAL_001'); + + $mock = $this->createMock(NhnKcpApiService::class); + $mock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(NhnKcpApiService::class, $mock); + + $response = $this->actingAs($admin) + ->withHeaders(['Accept-Language' => 'ko']) + ->postJson("/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{$order->order_number}/escrow-delivery", [ + 'deli_numb' => str_repeat('1', 31), + 'deli_corp' => '04', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['deli_numb']); + } + + /** + * 코드표(COURIER_CODES) 밖의 택배사 코드('99')는 Rule::in 이 422 로 차단하고, + * 메시지는 다국어 키(escrow.courier_required)의 ko 문구여야 한다 + * (종전 컨트롤러 인라인 한글 하드코딩 → 다국어 키 이관 회귀 방지). + */ + public function test_register_rejects_unknown_deli_corp(): void + { + + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->createEscrowOrder('KCP_ESCROW_TNO_VAL_002'); + + $mock = $this->createMock(NhnKcpApiService::class); + $mock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(NhnKcpApiService::class, $mock); + + $response = $this->actingAs($admin) + ->withHeaders(['Accept-Language' => 'ko']) + ->postJson("/api/plugins/sirsoft-pay_nhnkcp/admin/orders/{$order->order_number}/escrow-delivery", [ + 'deli_numb' => '1234567890', + 'deli_corp' => '99', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['deli_corp']) + ->assertJsonPath('errors.deli_corp.0', '택배사를 선택해주세요.'); + } } diff --git a/plugins/_bundled/sirsoft-pay_nhnkcp/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-pay_nhnkcp/tests/PluginTestCase.php index 0880eef3..da869cb4 100644 --- a/plugins/_bundled/sirsoft-pay_nhnkcp/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-pay_nhnkcp/tests/PluginTestCase.php @@ -61,6 +61,8 @@ abstract class PluginTestCase extends TestCase $this->registerPluginAutoload(); $this->app->register(EcommerceServiceProvider::class); + // _bundled 테스트가 _bundled 의 lang 을 보도록 네임스페이스 등록 (kginicis PluginTestCase 선례) + $this->app['translator']->addNamespace('sirsoft-pay_nhnkcp', dirname(__DIR__).'/lang'); $this->registerModuleRoutes(); $this->registerPluginRoutes(); @@ -124,8 +126,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); @@ -150,8 +156,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $pluginBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/CHANGELOG.md b/plugins/_bundled/sirsoft-pay_nicepayments/CHANGELOG.md index c8f780dc..268d2890 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-pay_nicepayments/CHANGELOG.md @@ -4,6 +4,22 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Security + +- 가상계좌 환불·에스크로 배송등록·결제 서명 요청의 입력값에 형식·길이 검증을 추가했습니다. 잘못된 형식의 환불 계좌 정보(계좌번호·은행코드·예금주)는 PG 호출 전에 차단됩니다. + +### Changed + +- 결제 서명(SignData) 요청의 필수값 누락 응답이 표준 검증 응답(422)으로 통일되었습니다. +- 주문·결제 정보 조회가 데이터베이스 표 이름을 직접 쓰지 않고 쇼핑몰 모듈의 정의를 따르도록 내부 정리했습니다. 표시되는 내용과 동작은 종전과 동일합니다. + +### Fixed + +- 관리자 주문 상세에서 에스크로 배송 등록이 실패하면 내부 오류 원문이 그대로 안내 문구 자리에 표시되던 문제를 수정했습니다. 이제 상황에 맞는 안내가 표시되고, 원인은 서버 로그에만 기록됩니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-10 ### Changed @@ -12,6 +28,7 @@ ### Fixed +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. - 이커머스 관리자 주문설정에서 다른 탭에 갔다 돌아오면 간편결제 항목의 브랜드 색상 배지가 사라지고 회색 기본 아이콘으로 남던 문제를 수정했습니다. 화면을 새로고침해야만 다시 표시됐습니다. - 결제 취소 기록과 환불 금액 오류 안내에 원화 단위가 고정 표기되던 문제를 수정했습니다. 이제 실제 결제 통화로 표시됩니다. - 쇼핑몰 주소를 기본값이 아닌 값으로 바꾸거나 주소 없이 운영하는 상점에서, 결제를 마치거나 결제에 실패한 구매자가 존재하지 않는 페이지로 이동하던 문제를 수정했습니다. 이동 주소가 기본 주소로 고정돼 있었기 때문이며, 이제 상점 주소 설정을 따라갑니다. 이미 운영 중인 사이트도 업데이트 시 자동으로 반영되며, 이동 주소를 직접 입력해 두었다면 그 값은 그대로 유지됩니다. (#85 @hwaryeon1234 님께서 제보해주셨습니다.) diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/composer.json b/plugins/_bundled/sirsoft-pay_nicepayments/composer.json index 18a0835b..6e0090a6 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/composer.json +++ b/plugins/_bundled/sirsoft-pay_nicepayments/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-pay_nicepayments", "description": "NicePayments PG Plugin for G7 platform", - "version": "1.0.1", + "version": "1.0.2", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-pay_nicepayments/dist/js/plugin.iife.js index 3c4c7df1..748e8262 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-pay_nicepayments/dist/js/plugin.iife.js @@ -1,2 +1 @@ (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.amount),buyer_email:n.customer_email??"",buyer_phone:n.customer_phone??"",payment_method:o},P),window.removeEventListener("popstate",se),Array.from(document.body.children).forEach(h=>{ht.has(h)||h.remove()}),p.parentNode&&p.parentNode.removeChild(p),window.__sirsoftNicepayPaymentInProgress=!1,i?.state?.setLocal?.({isSubmittingOrder:!1,paymentMethod:o}),m&&i?.toast?.error?.(m)};window.nicepayClose=ie;const se=()=>ie("","");window.history.pushState({nicepayOpen:!0},""),window.addEventListener("popstate",se),window.goPay(p)}catch(s){console.error("[sirsoft-pay_nicepayments] requestPayment error",s),window.__sirsoftNicepayPaymentInProgress=!1,i?.state?.setLocal?.({isSubmittingOrder:!1,paymentMethod:o}),i?.toast?.error?.("결제 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.")}}const ge={nicepay_naverpay:"네이버페이",nicepay_kakaopay:"카카오페이",nicepay_samsungpay:"삼성페이",nicepay_applepay:"애플페이",nicepay_payco:"PAYCO",nicepay_skpay:"11pay",nicepay_ssgpay:"SSG페이",nicepay_lpay:"L.pay"},we="0 0 0 2px #ffffff, 0 0 0 5px rgba(0,0,0,0.55)";function D(){const e=document.getElementById("nicepay_checkout_payment_section");if(!e)return null;const t=e.querySelectorAll("p");for(const n of t)if(n.textContent?.includes("간편결제"))return n.nextElementSibling;return null}function R(){const e=D();e&&e.querySelectorAll("button").forEach(t=>{t.style.boxShadow="",t.style.outline=""})}function _e(e){const t=D();if(!t)return;const n=ge[e];t.querySelectorAll("button").forEach(a=>{a.textContent?.trim()===n?(a.style.boxShadow=we,a.style.outline="none"):(a.style.boxShadow="",a.style.outline="")})}function Pe(e){const t=e.params?.paymentMethod;if(!t)return;const n=typeof t=="string"&&t.indexOf("nicepay_")===0;window.G7Core?.state?.setLocal?.({paymentMethod:t}),n?_e(t):R()}function $(){const e=window.__templateApp;typeof e?.onGlobalStateChange=="function"&&e.onGlobalStateChange(t=>{const n=t?._local?.paymentMethod;typeof n=="string"&&(n.startsWith("nicepay_")||R())})}const T={requestPayment:be,setPaymentMethod:Pe},Ce="sirsoft-pay_nicepayments",Ee={info:(...e)=>console.info(`[${Ce}]`,...e)};function Se(){Ee.info("order interceptor is a no-op — payment entry is dispatched via pg_payment_handler")}const C="sirsoft-pay_nicepayments",O="__nicepayOcReceiptInjectorInstalled",B="nicepay-oc-receipt-btn",xe=/^\/shop\/orders\/([^/]+)\/complete$/;function j(){return localStorage.getItem("auth_token")}async function Ae(e){const t=j();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 G(e){const t=j();if(!t)return null;try{const n=await fetch(`/api/plugins/${C}/user/orders/${e}/receipt`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}function Ne(e){if(!e)return!1;const t=Array.from(document.querySelectorAll("div"));for(const n of t){const a=Array.from(n.children).filter(i=>i instanceof HTMLElement&&i.tagName==="SPAN");if(a.length<2)continue;const r=a[0].textContent?.trim();if(r!=="결제 방법"&&r!=="결제수단"&&r!=="결제 방식")continue;const o=a[a.length-1];return o.textContent?.trim()!==e&&(o.textContent=e,o.dataset.nicepayPaymentMethodPatched="true"),n.dataset.nicepayPaymentMethodRow="true",!0}return!1}async function ke(e){if(document.getElementById(B))return;const t=await Ae(e);if(!t||t.pg_provider!=="nicepayments"||!t.transaction_id||!t.paid_at)return;const n=Array.from(document.querySelectorAll('button[type="button"]')).find(s=>s.className.includes("bg-blue-600"));if(!n?.parentElement)return;const a=await G(e);Ne(a?.payment_method_display_label);const r=n.parentElement,o=document.createElement("button");o.id=B,o.type="button",o.className=n.className.replace(/bg-blue-\d+/g,"bg-green-600").replace(/hover:bg-blue-\d+/g,"hover:bg-green-700"),o.textContent="영수증 조회",o.addEventListener("click",async()=>{o.disabled=!0,o.textContent="로딩 중...";const s=await G(e);o.disabled=!1,o.textContent="영수증 조회";const l=s?.receipt_url??null;l&&window.open(l,"nicepay_receipt","width=800,height=600,scrollbars=yes,resizable=yes")});const i=r.lastElementChild;r.insertBefore(o,i),console.info(`[${C}] receipt button injected on order complete page`)}function ve(){const e=location.pathname.match(xe);e&&ke(e[1])}function Ie(){if(typeof window>"u")return;const e=window;if(e[O])return;e[O]=!0,console.info(`[${C}] order complete receipt injector installed`);const t=(a=1200)=>setTimeout(ve,a);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...a)=>{n(...a),t()},window.addEventListener("popstate",()=>t(500))}const b="sirsoft-pay_nicepayments",q="__nicepayOrderShowInjectorInstalled",H="nicepay-mp-receipt-row",F="nicepay-mp-vbank-info",Le=/^\/mypage\/orders\/([^/]+)$/;function Me(e){try{const n=window.G7Core?.getState,o=n?.()?.currentDataContext?.order?.data;return!o||o.order_number!==e?null:o}catch{return null}}function De(){return localStorage.getItem("auth_token")}async function U(e){const t=De();if(!t)return null;try{const n=await fetch(`/api/plugins/${b}/user/orders/${e}/receipt`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});return n.ok?await n.json():null}catch{return null}}function Re(e,t){if(!t)return!1;const n=Array.from(e.querySelectorAll("div"));for(const a of n){const r=Array.from(a.children).filter(s=>s instanceof HTMLElement&&s.tagName==="SPAN");if(r.length<2)continue;const o=r[0].textContent?.trim();if(o!=="결제 방법"&&o!=="결제수단"&&o!=="Payment Method")continue;const i=r[r.length-1];return i.textContent?.trim()!==t&&(i.textContent=t,i.dataset.nicepayPaymentMethodPatched="true"),a.dataset.nicepayPaymentMethodRow="true",!0}return!1}function $e(){const e=document.getElementById("order_payment_info_panel");if(e)return Array.from(e.children).find(a=>a.className?.includes("space-y"))??e;const t=Array.from(document.querySelectorAll("h3")).find(a=>a.textContent?.includes("결제 정보"));if(!t)return null;const n=t.parentElement?.parentElement;return n?Array.from(n.children).find(a=>a.className?.includes("space-y"))??n:null}function Te(e){const t=document.createElement("div");t.id=H,t.className="flex items-center justify-between";const n=document.createElement("span");n.className="text-gray-500 dark:text-gray-400 text-sm",n.textContent="영수증";const a=document.createElement("button");return a.type="button",a.className="inline-flex items-center gap-1 text-sm text-blue-600 dark:text-blue-400 hover:underline disabled:opacity-50",a.textContent="영수증 조회",a.addEventListener("click",async()=>{a.disabled=!0,a.textContent="로딩 중...";const r=await U(e);a.disabled=!1,a.textContent="영수증 조회";const o=r?.receipt_url??null;o&&window.open(o,"nicepay_receipt","width=800,height=600,scrollbars=yes,resizable=yes")}),t.appendChild(n),t.appendChild(a),t}function Oe(e){if(!e)return"";try{const t=new Date(e);if(Number.isNaN(t.getTime()))return e;const n=a=>String(a).padStart(2,"0");return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())} ${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`}catch{return e}}function g(e,t,n="font-medium text-gray-900 dark:text-white"){const a=document.createElement("div");a.className="flex justify-between text-sm";const r=document.createElement("span");r.className="text-gray-600 dark:text-gray-400",r.textContent=e;const o=document.createElement("span");return o.className=n,o.textContent=t,a.appendChild(r),a.appendChild(o),a}function Be(e){const t=e.payment??{},n=document.createElement("div");n.id=F,n.className="pt-4 mt-2 border-t border-gray-200 dark:border-gray-700";const a=document.createElement("div");a.className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-4";const r=document.createElement("h3");r.className="text-sm font-semibold text-blue-800 dark:text-blue-300 mb-3",r.textContent="가상계좌 입금 안내";const o=document.createElement("div");o.className="space-y-2",o.appendChild(g("은행",t.vbank_name??"")),o.appendChild(g("계좌번호",t.vbank_number??"","font-medium font-mono text-gray-900 dark:text-white")),o.appendChild(g("예금주",t.vbank_holder??"")),o.appendChild(g("입금 금액",e.total_amount_formatted??"","font-bold text-blue-600 dark:text-blue-400"));const i=t.vbank_due_at;if(i){const s=document.createElement("div");s.className="flex justify-between text-sm pt-2 border-t border-blue-200 dark:border-blue-700 mt-2";const l=document.createElement("span");l.className="text-gray-600 dark:text-gray-400",l.textContent="입금 기한";const d=document.createElement("span");d.className="font-medium text-red-600 dark:text-red-400",d.textContent=Oe(i),s.appendChild(l),s.appendChild(d),o.appendChild(s)}if(a.appendChild(r),a.appendChild(o),!t.paid_at){const s=document.createElement("p");s.className="text-xs text-blue-700 dark:text-blue-300 mt-3",s.textContent="입금 기한 내에 입금이 완료되지 않으면 주문이 자동 취소됩니다.",a.appendChild(s)}return n.appendChild(a),n}async function je(e){const t=Me(e);if(!t)return!1;const{payment:n}=t;if(!n||n.pg_provider!=="nicepayments")return!0;const a=$e();if(!a)return!1;const r=await U(e);return Re(a,r?.payment_method_display_label),n.payment_method==="vbank"&&n.vbank_number&&!document.getElementById(F)&&(a.appendChild(Be(t)),console.info(`[${b}] vbank info injected on mypage order show`)),!n.transaction_id||!n.paid_at||document.getElementById(H)||(a.appendChild(Te(e)),console.info(`[${b}] receipt button injected on mypage order show`)),!0}function Ge(e){let t=0;const n=setInterval(()=>{t++,je(e).then(a=>{(a||t>=30)&&clearInterval(n)})},400)}function qe(){const e=location.pathname.match(Le);e&&Ge(e[1])}function He(){if(typeof window>"u")return;const e=window;if(e[q])return;e[q]=!0,console.info(`[${b}] mypage order show injector installed`);const t=(a=1500)=>setTimeout(qe,a);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...a)=>{n(...a),t(600)},window.addEventListener("popstate",()=>t(500))}const Fe="sirsoft-pay_nicepayments",Y="__nicepayAdminPaymentMethodBrandInjectorInstalled",W="__nicepayAdminPaymentMethodBrandSyncListenerAttached",Ue=/^\/admin\/ecommerce\/settings\/?$/,K='[data-nicepay-admin-payment-brand-mark="true"]',Ye=200,We=120,Ke=[{id:"nicepay_naverpay",labels:["네이버페이 (나이스페이먼츠)","네이버페이로 결제 (나이스페이먼츠)","Naver Pay (NicePayments)","Pay with Naver Pay (NicePayments)"],shortLabels:[],markLines:["N"],markClassName:"bg-green-500 text-white"},{id:"nicepay_kakaopay",labels:["카카오페이 (나이스페이먼츠)","카카오페이로 결제 (나이스페이먼츠)","Kakao Pay (NicePayments)","Pay with Kakao Pay (NicePayments)"],shortLabels:[],markLines:["K"],markClassName:"bg-yellow-400 text-gray-950"},{id:"nicepay_samsungpay",labels:["삼성페이 (나이스페이먼츠)","삼성페이로 결제 (나이스페이먼츠)","Samsung Pay (NicePayments)","Pay with Samsung Pay (NicePayments)"],shortLabels:[],markLines:["S"],markClassName:"bg-blue-600 text-white"},{id:"nicepay_applepay",labels:["애플페이 (나이스페이먼츠)","애플페이로 결제 (나이스페이먼츠)","Apple Pay (NicePayments)","Pay with Apple Pay (NicePayments)"],shortLabels:[],markLines:["A"],markClassName:"bg-gray-900 text-white"},{id:"nicepay_payco",labels:["PAYCO (나이스페이먼츠)","PAYCO로 결제 (나이스페이먼츠)","PAYCO (NicePayments)","Pay with PAYCO (NicePayments)"],shortLabels:[],markLines:["P"],markClassName:"bg-red-500 text-white"},{id:"nicepay_skpay",labels:["11pay (나이스페이먼츠)","11pay로 결제 (나이스페이먼츠)","11pay (NicePayments)","Pay with 11pay (NicePayments)"],shortLabels:[],markLines:["11"],markClassName:"bg-orange-500 text-white"},{id:"nicepay_ssgpay",labels:["SSG페이 (나이스페이먼츠)","SSG페이로 결제 (나이스페이먼츠)","SSG Pay (NicePayments)","Pay with SSG Pay (NicePayments)"],shortLabels:[],markLines:["SSG"],markClassName:"bg-red-700 text-white"},{id:"nicepay_lpay",labels:["L.pay (나이스페이먼츠)","L.pay로 결제 (나이스페이먼츠)","L.pay (NicePayments)","Pay with L.pay (NicePayments)"],shortLabels:[],markLines:["L"],markClassName:"bg-purple-600 text-white"}];let z=null,w=null,E=!1;function ze(){return window}function S(){if(!Ue.test(window.location.pathname))return!1;const e=new URLSearchParams(window.location.search).get("tab");return e===null||e===""||e==="order_settings"}function Ve(e){return e instanceof HTMLElement?e.className:""}function V(e){return(e??"").replace(/\u200B/g,"").replace(/\s+/g," ").trim()}function Je(e){const t=Ve(e);return t.includes("excel-card")||t.includes("flex-center")&&t.includes("border")&&t.includes("gap-4")}function J(e){return e.querySelector(".font-medium")}function Xe(e){const t=V(J(e)?.textContent),n=V(e.textContent);return Ke.find(a=>a.shortLabels.some(r=>t===r)||a.labels.some(r=>n.includes(r)))??null}function Qe(e){return Array.from(e.querySelectorAll("div")).filter(Je)}function Ze(e){return Array.from(e.querySelectorAll("svg, [data-icon], i")).find(n=>!n.closest("[data-drag-handle]")&&!n.closest(K)&&!n.closest('.row-stack, .excel-card-body, button, select, [role="switch"]'))??null}function X(e,t){e.dataset.nicepayAdminPaymentBrandMark="true",e.dataset.nicepayAdminPaymentMethod=t.id,e.setAttribute("aria-hidden","true"),e.className=`inline-flex items-center justify-center rounded-lg font-bold ${t.markClassName}`,e.style.width="32px",e.style.height="32px",e.style.flex="0 0 32px",e.style.lineHeight="1",e.style.fontSize=t.markLines.join("").length>2?"9px":"12px";const n=t.markLines.join("");e.textContent!==n&&(e.textContent=t.markLines[0]??"")}function et(e){const t=document.createElement("span");return X(t,e),t}function tt(e,t){const n=e.querySelector(K);if(n instanceof HTMLSpanElement)return e.dataset.nicepayAdminPaymentMethod=t.id,X(n,t),!0;const a=et(t),r=Ze(e);if(r&&r.parentElement)return e.dataset.nicepayAdminPaymentMethod=t.id,r.replaceWith(a),!0;const o=J(e);return o&&o.parentElement?(e.dataset.nicepayAdminPaymentMethod=t.id,o.parentElement.insertBefore(a,o),!0):!1}function x(e=document){if(typeof window>"u"||typeof document>"u"||!S())return!1;let t=!1;return Qe(e).forEach(n=>{const a=Xe(n);a&&tt(n,a)&&(t=!0)}),t}function nt(){E||(E=!0,window.setTimeout(()=>{E=!1,x()},0))}function A(){w!==null&&(window.clearInterval(w),w=null)}function N(){if(!S())return;A(),x();let e=0;w=window.setInterval(()=>{e+=1,x(),e>=We&&A()},Ye);const t=document.body;t[W]||(t[W]=!0,z=new MutationObserver(()=>nt()),z.observe(document.body,{childList:!0,subtree:!0,characterData:!0}))}function k(){if(S()){N();return}A()}function at(){if(typeof window>"u"||typeof document>"u")return;const e=ze();if(e[Y])return;e[Y]=!0,console.info(`[${Fe}] admin payment method brand sync installed`),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>N()):N();const t=history.pushState.bind(history);history.pushState=(...a)=>{t(...a),window.setTimeout(()=>k(),200)};const n=history.replaceState.bind(history);history.replaceState=(...a)=>{n(...a),window.setTimeout(()=>k(),200)},window.addEventListener("popstate",()=>window.setTimeout(()=>k(),200))}const v="sirsoft-pay_nicepayments",Q="__nicepayAdminPaymentDisplayInjectorInstalled",ot=/^\/admin\/ecommerce\/orders\/([^/]+)$/;function rt(){return localStorage.getItem("auth_token")}async function it(e){const t=rt();if(!t)return null;try{const n=await fetch(`/api/plugins/${v}/admin/orders/${e}/transaction-status`,{headers:{Authorization:`Bearer ${t}`,Accept:"application/json"}});if(!n.ok)return null;const a=await n.json();return("data"in a?a.data:a)??null}catch{return null}}function st(e){return Array.from(e.children).filter(t=>t instanceof HTMLElement)}function ct(e,t){let n=!1;const a=Array.from(e.querySelectorAll("div"));for(const r of a){const o=st(r),i=o.findIndex(u=>u.textContent?.trim()==="결제수단");if(i<0)continue;const s=o.slice(i+1).find(u=>u.textContent?.trim()&&u.textContent.trim()!=="결제수단");if(!s)continue;const l=o.slice(i+2).find(u=>/^\(.+\)$/.test(u.textContent?.trim()??"")),d=l?.textContent?.trim().replace(/^\(|\)$/g,"")??"",_=d&&t.endsWith(")")?t.replace(/\)$/,`, ${d})`):t;s.textContent?.trim()!==_&&(s.textContent=_,s.dataset.nicepayPaymentMethodPatched="true"),r.dataset.nicepayPaymentMethodRow="admin-order",l&&d&&(l.textContent="",l.style.display="none",l.dataset.nicepayPaymentInstallmentHidden="true"),n=!0}return n}function lt(e,t,n){const r=Array.from(e.querySelectorAll("span")).filter(o=>o.textContent?.trim()===t).find(o=>{const i=typeof o.className=="string"?o.className:"";return i.includes("rounded-full")||i.includes("font-medium")});return r?(r.textContent=n,r.dataset.nicepayPaymentMethodPatched="true",r.dataset.nicepayPaymentMethodBadge="admin-order",!0):!1}function dt(e,t){const n=t?._pay_method_label,a=t?._embedded_pg_provider_label,r=t?._base_pay_method_label;if(!n||!a)return!1;const o=ct(e,n),i=r?lt(e,r,a):!1;return o||i}async function ut(e){const t=document.getElementById("section_payment_info")??document.getElementById("payment_info_card");if(!t)return!1;const n=await it(e);return n&&dt(t,n)&&console.info(`[${v}] payment method display patched on admin order detail`),!0}function pt(e){let t=0;const n=setInterval(()=>{t++,ut(e).then(a=>{(a||t>=30)&&clearInterval(n)})},400)}function mt(){const e=location.pathname.match(ot);e&&pt(e[1])}function yt(){if(typeof window>"u")return;const e=window;if(e[Q])return;e[Q]=!0,console.info(`[${v}] admin order payment display injector installed`);const t=(a=1500)=>setTimeout(mt,a);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>t()):t();const n=history.pushState.bind(history);history.pushState=(...a)=>{n(...a),t(600)},window.addEventListener("popstate",()=>t(500))}const y="sirsoft-pay_nicepayments",I={info:(...e)=>console.info(`[${y}]`,...e),warn:(...e)=>console.warn(`[${y}]`,...e),error:(...e)=>console.error(`[${y}]`,...e)};function Z(){const e=window.G7Core;if(!e)return 0;const t=e.getActionDispatcher;if(typeof t!="function")return 0;const n=t();if(!n||typeof n.registerHandler!="function")return 0;let a=0;for(const[r,o]of Object.entries(T)){const i=`${y}.${r}`;n.registerHandler(i,o,{category:"plugin",source:y}),a++}return a}function ee(){Se(),Ie(),He(),at(),yt();const e=()=>{const t=Z();if(t>0){I.info(`${t} handler(s) registered`),$();return}let n=0;const a=50,r=setInterval(()=>{n++;const o=Z();if(o>0){clearInterval(r),I.info(`${o} handler(s) registered (after ${n} retries)`),$();return}n>=a&&(clearInterval(r),I.warn("ActionDispatcher not available after timeout"))},100)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()}ee(),window.__SirsoftNicepayments={identifier:y,handlers:Object.keys(T),initPlugin:ee}})(); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/README.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/README.md index 10f6dbff..768f7ec8 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/README.md +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/README.md @@ -4,4 +4,9 @@ | 문서 | 도메인 | 설명 | | --- | --- | --- | -| [vbank.md](vbank.md) | `payment` | 가상계좌 입금통보 수신 경로와 발신 서버(IP) 확인 | +| [vbank.md](vbank.md) | `payment` | 가상계좌 입금통보 수신 경로와 발신 서버(IP) 확인 + 입금 완료 건 관리자 환불 (JSON API) | +| [escrow.md](escrow.md) | `order` | 관리자 에스크로 결제 조회·배송 등록 (JSON API) | +| [transaction.md](transaction.md) | `order` | 관리자 거래 조회 — TID 직접 / 주문번호 자동 매핑 (JSON API) | +| [payment.md](payment.md) | `payment` | 결제창 서명값(SignData) 발급 경로 (web, 결제창 SDK 계약) | +| [orders.md](orders.md) | `order` | 주문 보조 조회 — 입금통보 이력·테스트모드/간편결제 맵·사용자 영수증 (JSON API) | +| [settings.md](settings.md) | `setting` | 관리자 설정 상태 조회 — 테스트 모드·입금통보 URL (JSON API) | diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/escrow.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/escrow.md new file mode 100644 index 00000000..d11108df --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/escrow.md @@ -0,0 +1,119 @@ +# 에스크로 API 레퍼런스 + +> **소유**: 플러그인 `sirsoft-pay_nicepayments`. 관리자 주문 상세의 에스크로 배송 등록 화면이 쓰는 JSON API 표면이다. 결제대행사 서버가 직접 호출하는 통보 경로([vbank.md](vbank.md))와는 인증·접근 제어가 다르다. + +--- + +## TL;DR (5초 요약) + +```text +1. 에스크로(EscrowYN=Y) 결제 건의 TID/상태를 조회하고, 배송 정보를 나이스페이먼츠에 등록한다 +2. 두 경로 모두 Bearer 토큰 + admin 미들웨어 + 이커머스 주문 권한이 필요하다 +3. 배송 등록이 성공하면 구매자에게 구매확정 안내가 결제대행사를 통해 자동 발송된다 +4. 결제대행사 호출 실패는 502 로 구분된다 — 응답 본문에 예외 원문을 싣지 않는다 +5. 대상 TID 를 찾지 못하면 422 (등록 가능한 에스크로 결제가 아님) +``` + +--- + +## 공통 규약 + +| 항목 | 값 | +| --- | --- | +| 인증 | `Authorization: Bearer {token}` (`auth:sanctum` + `admin`) | +| 응답 봉투 | 코어 `ResponseHelper` 표준 (`success` / `message` / `data`) | +| 권한 | 조회 `sirsoft-ecommerce.orders.read` · 등록 `sirsoft-ecommerce.orders.update` | + +--- + +## 에스크로 결제 목록 조회 + +| 항목 | 값 | +| --- | --- | +| 라우트명 | `api.plugins.sirsoft-pay_nicepayments.admin.orders.escrow-payments` | +| 메서드/URI | `GET /api/plugins/sirsoft-pay_nicepayments/admin/orders/{orderNumber}/escrow-payments` | +| 권한 | `sirsoft-ecommerce.orders.read` | + +**경로 파라미터** + +| 이름 | 타입 | 필수 | 설명 | +| --- | --- | --- | --- | +| `orderNumber` | string | 예 | 주문번호 | + +**응답 필드** (`data.escrow_payments[]`) + +| 필드 | 타입 | 설명 | +| --- | --- | --- | +| `id` | integer | 결제 레코드 ID | +| `transaction_id` | string | 나이스페이먼츠 TID | +| `payment_method` | string | 결제수단 (가상계좌·카드 등) | +| `payment_status` | string | 결제 상태 | + +해당 주문에 에스크로 결제가 없으면 `escrow_payments` 는 빈 배열이다. + +**응답 예시** + +```json +{ + "success": true, + "message": "요청이 성공했습니다.", + "data": { + "escrow_payments": [ + { + "id": 812, + "transaction_id": "nicepay20260814012345", + "payment_method": "vbank", + "payment_status": "paid" + } + ] + } +} +``` + +--- + +## 에스크로 배송 등록 + +| 항목 | 값 | +| --- | --- | +| 라우트명 | `api.plugins.sirsoft-pay_nicepayments.admin.escrow.register-delivery` | +| 메서드/URI | `POST /api/plugins/sirsoft-pay_nicepayments/admin/escrow/register-delivery` | +| 권한 | `sirsoft-ecommerce.orders.update` | + +**요청 파라미터** + +| 이름 | 타입 | 필수 | 제약 | 설명 | +| --- | --- | --- | --- | --- | +| `tid` | string | 예 | 최대 30자 | 대상 에스크로 결제의 나이스페이먼츠 TID (나이스페이먼츠 TID 규격 30자 — 초과 시 422) | +| `delivery_name` | string | 예 | 최대 100자 | 택배사명 | +| `tracking_number` | string | 예 | 최대 100자 | 송장번호 | +| `buyer_address` | string | 예 | 최대 200자 | 구매자 배송지 | +| `register_name` | string | 예 | 최대 50자 | 등록자명 | + +**동작** + +나이스페이먼츠에 배송 정보를 등록한다. 등록이 완료되면 구매자에게 구매확정 안내가 결제대행사를 통해 자동 발송된다. + +**응답 예시** + +```json +{ + "success": true, + "message": "요청이 성공했습니다.", + "data": { + "ResultCode": "0000", + "ResultMsg": "정상처리" + } +} +``` + +**에러** + +| 상태코드 | 사유 | 응답 | +| --- | --- | --- | +| 401 | 토큰 누락·만료 | 코어 표준 | +| 403 | `sirsoft-ecommerce.orders.update` 권한 없음 | 코어 표준 | +| 422 | 요청 파라미터 검증 위반, 또는 해당 TID 로 등록 가능한 에스크로 결제를 찾지 못함 | `error.errors.message` 에 사유 | +| 502 | 결제대행사 호출 실패 (네트워크·응답 파싱·자격증명 오류 등) | `common.failed` | + +502 응답 본문에는 예외 원문을 싣지 않는다 — 원인은 서버 로그(`NicePayments: escrow delivery registration failed`)에만 기록된다. 이미 번역된 예외 메시지를 응답의 메시지 키 자리에 넘기면 키 해석에 실패해 내부 오류 원문이 그대로 관리자 화면에 노출되기 때문이다. 상세: [docs/backend/exceptions.md "예외 → 응답 매핑"](../../../../../docs/backend/exceptions.md). diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/orders.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/orders.md new file mode 100644 index 00000000..5ae0b060 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/orders.md @@ -0,0 +1,53 @@ +# Orders(주문 보조 조회) API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_nicepayments` · 사람이 서술한 레퍼런스. 관리자 주문 화면과 사용자 영수증 화면이 쓰는 보조 조회 경로를 다룬다. 에스크로 결제 목록은 [escrow.md](escrow.md), 거래 조회는 [transaction.md](transaction.md) 참조. + +--- + +## TL;DR (5초 요약) + +```text +1. 관리자 주문 목록 배지용 맵 2종(테스트모드 / 간편결제 표시)과 입금통보 이력, 사용자 영수증 조회 +2. 관리자 경로는 auth:sanctum + permission:sirsoft-ecommerce.orders.read +3. 맵 2종은 최근 6개월의 나이스페이 결제 건만 집계한다 (주문 목록 배지 표시용) +4. 영수증 조회는 회원이면 본인 주문만, 비회원이면 X-Guest-Order-Token 검증으로 접근 제어 +5. 응답 data 는 화면 표시용 경량 구조 — 주문 상세 데이터는 이커머스 API 가 담당 +``` + +--- + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/orders/{orderNumber}/vbank-notifications + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.orders.vbank-notifications` +- **컨트롤러**: `AdminVbankNotificationController@show` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.read` + +주문의 가상계좌 **입금통보 수신 이력**을 반환한다. 통보 수신 경로 자체는 [vbank.md](vbank.md) 참조. 주문이 없으면 실패 응답(`success: false`). + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/orders/test-mode-map + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.orders.test-mode-map` +- **컨트롤러**: `AdminOrderListController@testModeMap` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.read` + +최근 6개월 나이스페이 결제 건 중 **테스트 모드 자격증명으로 결제된 주문**의 맵을 반환한다. 주문 목록의 "테스트" 배지 표시용. + +```json +{ "success": true, "data": { "ORD-20260817-000001": true } } +``` + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/orders/easy-pay-display-map + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.orders.easy-pay-display-map` +- **컨트롤러**: `AdminOrderListController@easyPayDisplayMap` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.read` + +최근 6개월 나이스페이 결제 건 중 **간편결제(내장 PG) 표시 라벨**이 있는 주문의 맵을 반환한다. 주문 목록이 결제수단 열에 간편결제 명칭(네이버페이 등)을 표시하는 데 쓴다. + +### GET /api/plugins/sirsoft-pay_nicepayments/user/orders/{orderNumber}/receipt + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.user.orders.receipt` +- **컨트롤러**: `UserReceiptController@show` +- **인증/권한**: `optional.sanctum` — 회원이면 본인 주문만, 비회원이면 `X-Guest-Order-Token` 헤더 검증 + +주문의 나이스페이 **매출전표(영수증) URL** 을 반환한다. 소유권 검증에 실패하면 대상 없음으로 처리된다(다른 사용자의 주문번호로는 조회 불가). diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/payment.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/payment.md new file mode 100644 index 00000000..753fc979 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/payment.md @@ -0,0 +1,50 @@ +# Payment(결제창 서명) API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_nicepayments` · 사람이 서술한 레퍼런스. 이 경로는 결제창(브라우저) 흐름이 호출하는 web 라우트로, 표준 관리자 JSON API 표면이 아니다. + +--- + +## TL;DR (5초 요약) + +```text +1. 결제창 호출 직전 브라우저가 서명값(SignData)을 발급받는 경로다 +2. amt(정수)·moid(max 64) 형식 위반은 422 (SignDataRequest — 종전 400 에서 422 로 표준화) +3. 주문 실존·구매자 일치·나이스페이 결제 건 여부를 서버가 재검증한다 (금액 조작 방지) +4. 과도 호출은 429 (주문번호 기준 rate limit) +5. 응답은 envelope 없이 { ediDate, signData, mid } 원시 JSON (결제창 SDK 계약) +``` + +--- + +### POST /plugins/sirsoft-pay_nicepayments/payment/sign-data + +- **라우트명**: `web.plugins.sirsoft-pay_nicepayments.payment.sign-data` +- **컨트롤러**: `Plugins\Sirsoft\PayNicepayments\Controllers\PaymentCallbackController@signData` +- **인증/권한**: web 라우트 (세션/비회원 주문 검증은 구매자 대조로 수행) + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| amt | body | integer | 예 | 1 ~ 999999999999 | 결제 금액. 서버가 주문 금액과 대조하므로 임의 값은 서명 발급이 거절된다 | +| moid | body | string | 예 | max 64 | 주문번호(MOID) | + +**응답 (200)** + +```json +{ + "ediDate": "20260817143000", + "signData": "…SHA-256 서명…", + "mid": "nictest00m" +} +``` + +응답은 코어 envelope 를 쓰지 않는다 — 나이스페이 결제창 SDK 가 소비하는 원시 JSON 계약이다. 클라이언트는 `response.ok` 만 검사한다. + +**오류 응답** + +| 상태 | 조건 | +| --- | --- | +| 422 | `amt`/`moid` 형식 위반 (`SignDataRequest` — 종전 400 응답을 422 로 표준화) / 주문 없음 / 나이스페이 결제 건이 아님 | +| 403 | 구매자 대조 실패 (다른 사용자의 주문번호로 서명 요청) | +| 429 | 주문번호 기준 rate limit 초과 (60초 내 20회) | diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/settings.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/settings.md new file mode 100644 index 00000000..5f95fbaf --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/settings.md @@ -0,0 +1,41 @@ +# Settings(설정 상태) API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_nicepayments` · 사람이 서술한 레퍼런스. 관리자 설정 화면이 쓰는 상태 조회 경로를 다룬다. + +--- + +## TL;DR (5초 요약) + +```text +1. 관리자 설정 화면용 조회 2종 — 테스트 모드 상태 / 입금통보 URL 안내 +2. 두 경로 모두 auth:sanctum + permission:sirsoft-ecommerce.settings.read +3. 테스트 모드 상태는 미설정 시 true(테스트 모드)가 기본값이다 +4. 입금통보 URL 은 나이스페이먼츠 상점관리자에 등록할 값을 화면에 보여주기 위한 조회다 +5. 쓰기(설정 저장)는 코어 플러그인 설정 API 가 담당 — 이 문서 범위 아님 +``` + +--- + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/settings/test-mode-status + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.settings.test-mode-status` +- **컨트롤러**: `AdminSettingsStatusController@testMode` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.settings.read` + +플러그인의 테스트 모드 여부를 반환한다. 설정 미저장 상태의 기본값은 `true`(테스트 모드). + +```json +{ "success": true, "data": { "is_test_mode": true } } +``` + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/vbank-notify-url + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.vbank.notify.url` +- **컨트롤러**: 라우트 클로저 (`src/routes/api.php`) +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.settings.read` + +가상계좌 **입금통보 수신 URL** 을 반환한다. 운영자가 나이스페이먼츠 상점관리자에 등록할 주소를 설정 화면에 표시하는 용도다. 수신 경로의 동작·IP 제한은 [vbank.md](vbank.md) 참조. + +```json +{ "success": true, "data": { "url": "https://example.com/plugins/sirsoft-pay_nicepayments/payment/vbank-notify" } } +``` diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/transaction.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/transaction.md new file mode 100644 index 00000000..6f0ed10b --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/transaction.md @@ -0,0 +1,82 @@ +# Transaction API 레퍼런스 + +> **소유**: plugin `sirsoft-pay_nicepayments` · 사람이 서술한 레퍼런스 (docgen 스캐폴딩 불가 구간 — 응답이 PG 원본 응답 합성이라 실측 자리는 사람이 채운다). + +--- + +## TL;DR (5초 요약) + +```text +1. 관리자 주문 상세의 나이스페이 거래 조회 2종 (TID 직접 / 주문번호 자동 매핑) +2. 두 경로 모두 auth:sanctum + permission:sirsoft-ecommerce.orders.read +3. tid 는 필수·문자열·최대 30자 — 위반 시 PG 호출 없이 422 (TransactionQueryRequest) +4. 응답 data 는 NicePay 단건 조회 원본 키 + 로컬 보강 필드(_local_is_escrow, _is_test_mode 등) +5. 주문번호 경로는 나이스페이 결제 레코드가 없으면 data: null 로 성공 응답 +``` + +--- + +### POST /api/plugins/sirsoft-pay_nicepayments/admin/transaction/query + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.transaction.query` +- **컨트롤러**: `Plugins\Sirsoft\PayNicepayments\Controllers\AdminTransactionController@query` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| tid | body | string | 예 | max 30 | 조회할 나이스페이 거래번호(TID). 누락·문자열 아님·30자 초과는 PG 호출 전에 422 (`TransactionQueryRequest`) | + +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_nicepayments/admin/transaction/query HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "tid": "nictest00m01011104191651325596" +} +``` + +**응답 필드** (`data` 내부) + +`data` 는 NicePay 단건 거래 조회 응답(원본 키 그대로)에 로컬 보강 필드를 덧붙인 객체다. 원본 키는 결제수단·거래 상태에 따라 존재 여부가 달라진다. + +| 필드 | 타입 | 용도/설명 | +| --- | --- | --- | +| ResultCode | string | NicePay 조회 결과 코드 (원본 응답) | +| ResultMsg | string | NicePay 조회 결과 메시지 (원본 응답) | +| TID | string | 거래번호 | +| Amt | string | 거래 금액 (원본 응답) | +| \_local_is_escrow | boolean | 로컬 결제 레코드 기준 에스크로 거래 여부 (조회 응답에 EscrowYN 이 없을 때의 보강) | +| \_is_test_mode | boolean | 테스트 모드 자격증명으로 결제된 거래 여부 (로컬 레코드 기준) | + +**오류 응답** + +| 상태 | 조건 | +| --- | --- | +| 422 | `tid` 검증 실패 (누락 / 문자열 아님·배열 주입 / 30자 초과) — PG 미호출 | +| 401 / 403 | 미인증 / `sirsoft-ecommerce.orders.read` 권한 없음 | + +--- + +### GET /api/plugins/sirsoft-pay_nicepayments/admin/orders/{orderNumber}/transaction-status + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.orders.transaction-status` +- **컨트롤러**: `Plugins\Sirsoft\PayNicepayments\Controllers\AdminTransactionController@queryByOrder` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.read` + +**요청 파라미터** + +| 이름 | 위치 | 타입 | 필수 | 용도 | +| --- | --- | --- | --- | --- | +| orderNumber | path | string | 예 | 주문번호. `ecommerce_order_payments.transaction_id` 를 찾아 TID 조회로 위임 | + +**응답** + +- 나이스페이 결제 레코드(TID)가 있으면 위 `transaction/query` 와 동일한 `data`. +- 매핑되는 나이스페이 거래가 없으면 `data: null` 로 성공 응답 (오류 아님 — 화면이 "조회 대상 없음" 으로 표시). diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/vbank.md b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/vbank.md index 46a7144d..90f48492 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/vbank.md +++ b/plugins/_bundled/sirsoft-pay_nicepayments/docs/api/vbank.md @@ -29,3 +29,59 @@ 이 IP 화이트리스트 검사는 코어의 확장 미들웨어 self-gate(`Plugin::getMiddleware()` 의 `targets` 로 위 통보 라우트명에만 정밀 타게팅)로 수행된다. 브라우저가 접속하는 결제 콜백(`payment.callback`)에는 적용되지 않는다 — 콜백은 정상 사용자의 브라우저에서 임의 IP 로 도달하므로 IP 로 제한하면 결제가 끊긴다. 검사 자체의 동작·허용 범위는 라우트 파일에서 직접 부착하던 이전 방식과 동일하다. 상세: [docs/backend/middleware.md "확장 미들웨어 선언 (self-gate)"](../../../../../docs/backend/middleware.md). + +--- + +## 입금 완료 건 환불 (관리자 JSON API) + +### POST /api/plugins/sirsoft-pay_nicepayments/admin/vbank-refund + +- **라우트명**: `api.plugins.sirsoft-pay_nicepayments.vbank.refund` +- **컨트롤러**: `Plugins\Sirsoft\PayNicepayments\Controllers\AdminVbankRefundController@refund` +- **인증/권한**: `auth:sanctum` + `permission:admin,sirsoft-ecommerce.orders.update` + +입금이 완료된 가상계좌는 일반 환불 훅으로 처리할 수 없다 — 나이스페이먼츠가 환불받을 계좌 정보(계좌번호·은행코드·예금주)를 요구하기 때문이다. 이 경로가 해당 정보를 수집해 취소 API 를 호출하고 주문 취소·결제 상태 반영까지 수행한다. + +**요청 파라미터** (형식·길이 위반은 PG 호출 전에 422 — `VbankRefundRequest`) + +| 이름 | 위치 | 타입 | 필수 | 허용값 | 용도 | +| --- | --- | --- | --- | --- | --- | +| tid | body | string | 예 | max 30 | 거래번호(TID) | +| moid | body | string | 예 | max 64 | 주문번호(MOID) | +| cancel_amt | body | integer | 예 | 1 ~ 999999999999 | 취소 금액. 결제 금액과 불일치 시 422 | +| cancel_msg | body | string | 아니오 | max 100 | 취소 사유. 미입력 시 기본 문구 사용 | +| refund_acct_no | body | string | 예 | 숫자만, max 16 | 환불 계좌번호 | +| refund_bank_cd | body | string | 예 | 숫자 3자리 | 환불 은행코드 | +| refund_acct_nm | body | string | 예 | max 10 | 환불 예금주명 | + +**요청 예시** + +```http +POST /api/plugins/sirsoft-pay_nicepayments/admin/vbank-refund HTTP/1.1 +Host: api.example.com +Accept: application/json +Authorization: Bearer {YOUR_TOKEN} +Content-Type: application/json + +{ + "tid": "nictest00m02012104191651325596", + "moid": "ORD-20260817-000001", + "cancel_amt": 30000, + "cancel_msg": "구매자 요청 환불", + "refund_acct_no": "1234567890123", + "refund_bank_cd": "004", + "refund_acct_nm": "홍길동" +} +``` + +**응답** + +- 성공(200): `common.success` envelope, `data` 는 나이스페이 취소 API 원본 응답. +- 실패: + +| 상태 | 조건 | +| --- | --- | +| 422 | 입력 형식 위반 (`VbankRefundRequest` — PG 미호출) / 결제 상태가 환불 대상 아님 / 취소 금액 불일치 | +| 404 | 주문번호에 해당하는 주문 없음 | +| 409 | 동일 건 환불이 이미 처리 중 | +| 502 | PG 취소 API 실패 또는 취소 후 주문 반영 실패 (서버 로그에 원문 기록) | diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/lang/en/messages.php b/plugins/_bundled/sirsoft-pay_nicepayments/lang/en/messages.php index abbc4c5b..1d1c701f 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/lang/en/messages.php @@ -8,7 +8,6 @@ return [ 'order_not_found' => 'Order not found.', 'invalid_request' => 'Invalid request.', 'invalid_amount' => 'The requested amount is invalid.', - 'vbank_refund_required_fields' => 'Please provide all required fields: TID, order number, cancel amount, and refund account information (account number, bank code, account holder).', 'vbank_refund_order_not_found' => 'The order to refund could not be found.', 'vbank_refund_invalid_payment' => 'This is not a refundable completed NicePayments virtual-account payment.', 'vbank_refund_amount_mismatch' => 'The requested refund amount does not match the cancellable amount in the database.', @@ -23,4 +22,13 @@ return [ 'defaults' => [ 'vbank_refund_msg' => 'Virtual account refund', ], + 'fields' => [ + 'tid' => 'transaction ID (TID)', + 'moid' => 'order number', + 'cancel_amt' => 'cancel amount', + 'cancel_msg' => 'cancel reason', + 'refund_acct_no' => 'refund account number', + 'refund_bank_cd' => 'refund bank code', + 'refund_acct_nm' => 'refund account holder', + ], ]; diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/lang/ko/messages.php b/plugins/_bundled/sirsoft-pay_nicepayments/lang/ko/messages.php index 5720d041..ebd21a95 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/lang/ko/messages.php @@ -8,7 +8,6 @@ return [ 'order_not_found' => '주문을 찾을 수 없습니다.', 'invalid_request' => '잘못된 요청입니다.', 'invalid_amount' => '요청 금액이 유효하지 않습니다.', - 'vbank_refund_required_fields' => 'TID, 주문번호, 취소금액, 환불계좌 정보(계좌번호·은행코드·예금주)를 모두 입력해주세요.', 'vbank_refund_order_not_found' => '환불할 주문을 찾을 수 없습니다.', 'vbank_refund_invalid_payment' => '환불 가능한 나이스페이먼츠 가상계좌 입금완료 결제가 아닙니다.', 'vbank_refund_amount_mismatch' => '요청 환불 금액이 DB의 취소 가능 금액과 일치하지 않습니다.', @@ -23,4 +22,13 @@ return [ 'defaults' => [ 'vbank_refund_msg' => '가상계좌 환불', ], + 'fields' => [ + 'tid' => '거래 ID(TID)', + 'moid' => '주문번호', + 'cancel_amt' => '취소 금액', + 'cancel_msg' => '취소 사유', + 'refund_acct_no' => '환불 계좌번호', + 'refund_bank_cd' => '환불 은행코드', + 'refund_acct_nm' => '환불 예금주', + ], ]; diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/package-lock.json b/plugins/_bundled/sirsoft-pay_nicepayments/package-lock.json index 6d22766b..fee0d3b2 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/package-lock.json +++ b/plugins/_bundled/sirsoft-pay_nicepayments/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-pay_nicepayments", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-pay_nicepayments", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/package.json b/plugins/_bundled/sirsoft-pay_nicepayments/package.json index b62e7045..65d607bd 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/package.json +++ b/plugins/_bundled/sirsoft-pay_nicepayments/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-pay_nicepayments", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/plugin.json b/plugins/_bundled/sirsoft-pay_nicepayments/plugin.json index f5e1e019..e8ac9da2 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/plugin.json +++ b/plugins/_bundled/sirsoft-pay_nicepayments/plugin.json @@ -5,7 +5,7 @@ "ko": "나이스페이먼츠", "en": "NicePayments" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "github_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nicepayments", "github_changelog_url": "https://github.com/gnuboard/g7-plugin-sirsoft-pay_nicepayments/blob/main/CHANGELOG.md", diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-pay_nicepayments/resources/layouts/admin/plugin_settings.json index a477d2b7..88711c7a 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-pay_nicepayments/resources/layouts/admin/plugin_settings.json @@ -1122,7 +1122,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminEscrowController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminEscrowController.php index bb42dc48..925b9334 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminEscrowController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminEscrowController.php @@ -4,14 +4,17 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayNicepayments\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum; +use Modules\Sirsoft\Ecommerce\Models\Order; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; +use Plugins\Sirsoft\PayNicepayments\Http\Requests\EscrowDeliveryRegisterRequest; use Plugins\Sirsoft\PayNicepayments\Services\NicePaymentsApiService; class AdminEscrowController extends AdminBaseController @@ -38,12 +41,13 @@ class AdminEscrowController extends AdminBaseController */ public function getEscrowPayments(string $orderNumber): JsonResponse { - $payments = DB::table('ecommerce_order_payments') - ->join('ecommerce_orders', 'ecommerce_orders.id', '=', 'ecommerce_order_payments.order_id') - ->where('ecommerce_orders.order_number', $orderNumber) - ->where('ecommerce_order_payments.pg_provider', 'nicepayments') - ->where('ecommerce_order_payments.is_escrow', 1) - ->get(['ecommerce_order_payments.id', 'ecommerce_order_payments.transaction_id', 'ecommerce_order_payments.payment_method', 'ecommerce_order_payments.payment_status']); + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $payments = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') + ->where('o.order_number', $orderNumber) + ->where('p.pg_provider', 'nicepayments') + ->where('p.is_escrow', 1) + ->get(['p.id', 'p.transaction_id', 'p.payment_method', 'p.payment_status']); return ResponseHelper::success('common.success', [ 'escrow_payments' => $payments->map(fn ($p) => [ @@ -66,18 +70,12 @@ class AdminEscrowController extends AdminBaseController * NicePay escrow_process.jsp 호출하여 배송 정보(택배사/송장번호/수령인 등)를 * NicePay 측에 등록. 등록 완료 시 구매자에게 자동으로 구매확정 안내가 발송됨. * - * @param Request $request 배송 정보 폼 + * @param EscrowDeliveryRegisterRequest $request 배송 정보 폼 * @return JsonResponse 등록 결과 + ResultCode */ - public function registerDelivery(Request $request): JsonResponse + public function registerDelivery(EscrowDeliveryRegisterRequest $request): JsonResponse { - $validated = $request->validate([ - 'tid' => 'required|string', - 'delivery_name' => 'required|string|max:100', - 'tracking_number' => 'required|string|max:100', - 'buyer_address' => 'required|string|max:200', - 'register_name' => 'required|string|max:50', - ]); + $validated = $request->validated(); try { $payment = $this->findRegisterableEscrowPayment($validated['tid']); @@ -105,12 +103,23 @@ class AdminEscrowController extends AdminBaseController return ResponseHelper::success('common.success', $result); } catch (\Exception $e) { - return ResponseHelper::error($e->getMessage(), 502, null); + // 메시지 키 자리에는 원문 금지 — 키 해석에 실패해 원문이 그대로 나간다. + // 단 errors 페이로드는 관리자 전용 면의 진단 통로다(형제 KCP/이니시스 + // 에스크로와 동형) — 비우면 운영자가 서버 로그 없이는 실패 원인을 모른다. + Log::error('NicePayments: escrow delivery registration failed', [ + 'tid' => $validated['tid'], + 'error' => $e->getMessage(), + ]); + + return ResponseHelper::error('common.failed', 502, [ + 'message' => [$e->getMessage()], + ]); } } private function findRegisterableEscrowPayment(string $tid): ?OrderPayment { + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 return OrderPayment::query() ->where('pg_provider', 'nicepayments') ->where('is_escrow', true) diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminOrderListController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminOrderListController.php index 66aadda9..719afa89 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminOrderListController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminOrderListController.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayNicepayments\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayNicepayments\Concerns\ResolvesEasyPayDisplay; class AdminOrderListController extends AdminBaseController @@ -30,8 +34,9 @@ class AdminOrderListController extends AdminBaseController */ public function testModeMap(): JsonResponse { - $rows = DB::table('ecommerce_orders as o') - ->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $rows = DB::table((new Order)->getTable().' as o') + ->join((new OrderPayment)->getTable().' as p', 'p.order_id', '=', 'o.id') ->whereIn('p.pg_provider', ['nicepayments', 'nicepay']) ->whereNotNull('p.payment_meta') ->where('p.created_at', '>=', now()->subMonths(6)) @@ -59,8 +64,9 @@ class AdminOrderListController extends AdminBaseController */ public function easyPayDisplayMap(): JsonResponse { - $rows = DB::table('ecommerce_orders as o') - ->join('ecommerce_order_payments as p', 'p.order_id', '=', 'o.id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $rows = DB::table((new Order)->getTable().' as o') + ->join((new OrderPayment)->getTable().' as p', 'p.order_id', '=', 'o.id') ->whereIn('p.pg_provider', ['nicepayments', 'nicepay']) ->where('p.created_at', '>=', now()->subMonths(6)) ->select([ diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminTransactionController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminTransactionController.php index 09f099da..18b4d828 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminTransactionController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminTransactionController.php @@ -2,13 +2,17 @@ namespace Plugins\Sirsoft\PayNicepayments\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Plugins\Sirsoft\PayNicepayments\Concerns\ResolvesEasyPayDisplay; +use Plugins\Sirsoft\PayNicepayments\Http\Requests\TransactionQueryRequest; use Plugins\Sirsoft\PayNicepayments\Services\NicePaymentsApiService; class AdminTransactionController extends AdminBaseController @@ -27,18 +31,12 @@ class AdminTransactionController extends AdminBaseController * NicePay 단건 거래 조회 API 를 호출하고 로컬 DB 의 보조 정보(에스크로 여부, * 테스트 모드 플래그)를 합쳐 반환한다. * - * @param Request $request tid 입력 폼 + * @param TransactionQueryRequest $request tid 입력 폼 * @return JsonResponse 거래 정보 + _local_is_escrow / EscrowYN / _is_test_mode 보강 */ - public function query(Request $request): JsonResponse + public function query(TransactionQueryRequest $request): JsonResponse { - $tid = trim((string) $request->input('tid', '')); - - if ($tid === '') { - return ResponseHelper::error('common.failed', 422, ['tid' => [__('sirsoft-pay_nicepayments::messages.errors.tid_required')]]); - } - - return $this->queryByTid($tid); + return $this->queryByTid(trim((string) $request->validated('tid'))); } /** @@ -52,16 +50,17 @@ class AdminTransactionController extends AdminBaseController */ public function queryByOrder(string $orderNumber): JsonResponse { - $payment = DB::table('ecommerce_order_payments') - ->join('ecommerce_orders', 'ecommerce_orders.id', '=', 'ecommerce_order_payments.order_id') - ->where('ecommerce_orders.order_number', $orderNumber) - ->whereNotNull('ecommerce_order_payments.transaction_id') - ->where('ecommerce_order_payments.transaction_id', '!=', '') - ->whereIn('ecommerce_order_payments.pg_provider', ['nicepayments', 'nicepay']) - ->select(['ecommerce_order_payments.transaction_id']) + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $payment = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') + ->where('o.order_number', $orderNumber) + ->whereNotNull('p.transaction_id') + ->where('p.transaction_id', '!=', '') + ->whereIn('p.pg_provider', ['nicepayments', 'nicepay']) + ->select(['p.transaction_id']) ->first(); - if (!$payment) { + if (! $payment) { return ResponseHelper::success('common.success', null); } @@ -73,7 +72,8 @@ class AdminTransactionController extends AdminBaseController try { $result = $this->apiService->queryTransaction($tid); - $localPayment = DB::table('ecommerce_order_payments') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $localPayment = DB::table((new OrderPayment)->getTable()) ->where('transaction_id', $tid) ->select(['is_escrow', 'payment_meta', 'payment_method', 'embedded_pg_provider']) ->first(); @@ -104,6 +104,7 @@ class AdminTransactionController extends AdminBaseController 'tid' => $tid, 'error' => $e->getMessage(), ]); + return ResponseHelper::error('common.failed', 502, null); } } diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminVbankRefundController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminVbankRefundController.php index 830f43ee..f720ef0a 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminVbankRefundController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminVbankRefundController.php @@ -7,7 +7,6 @@ namespace Plugins\Sirsoft\PayNicepayments\Controllers; use App\Helpers\ResponseHelper; use App\Http\Controllers\Api\Base\AdminBaseController; use Illuminate\Http\JsonResponse; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -16,6 +15,7 @@ use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum; use Modules\Sirsoft\Ecommerce\Models\Order; use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\OrderCancellationService; +use Plugins\Sirsoft\PayNicepayments\Http\Requests\VbankRefundRequest; use Plugins\Sirsoft\PayNicepayments\Services\NicePaymentsApiService; /** @@ -42,26 +42,20 @@ class AdminVbankRefundController extends AdminBaseController * * 일반 환불 훅으로 처리할 수 없는 vbank 입금 완료 건의 환불을 직접 수행. * NicePay 가 요구하는 환불 계좌 정보(계좌번호·은행코드·예금주)를 직접 수집. + * 형식·길이 검증은 VbankRefundRequest 가 PG 호출 전에 차단한다. * - * @param Request $request TID/MOID/CancelAmt + 환불계좌 정보 - * @return JsonResponse 환불 결과 또는 422 (입력 누락) + * @param VbankRefundRequest $request TID/MOID/CancelAmt + 환불계좌 정보 + * @return JsonResponse 환불 결과 또는 422 (입력 형식 위반) */ - public function refund(Request $request): JsonResponse + public function refund(VbankRefundRequest $request): JsonResponse { - $tid = trim((string) $request->input('tid', '')); - $moid = trim((string) $request->input('moid', '')); - $cancelAmt = (int) $request->input('cancel_amt', 0); - $cancelMsg = trim((string) $request->input('cancel_msg', __('sirsoft-pay_nicepayments::messages.defaults.vbank_refund_msg'))); - $refundAcctNo = trim((string) $request->input('refund_acct_no', '')); - $refundBankCd = trim((string) $request->input('refund_bank_cd', '')); - $refundAcctNm = trim((string) $request->input('refund_acct_nm', '')); - - if ($tid === '' || $moid === '' || $cancelAmt <= 0 - || $refundAcctNo === '' || $refundBankCd === '' || $refundAcctNm === '') { - return ResponseHelper::error('common.failed', 422, [ - 'message' => __('sirsoft-pay_nicepayments::messages.errors.vbank_refund_required_fields'), - ]); - } + $tid = trim((string) $request->validated('tid')); + $moid = trim((string) $request->validated('moid')); + $cancelAmt = (int) $request->validated('cancel_amt'); + $cancelMsg = trim((string) ($request->validated('cancel_msg') ?? __('sirsoft-pay_nicepayments::messages.defaults.vbank_refund_msg'))); + $refundAcctNo = trim((string) $request->validated('refund_acct_no')); + $refundBankCd = trim((string) $request->validated('refund_bank_cd')); + $refundAcctNm = trim((string) $request->validated('refund_acct_nm')); $claimedPayment = null; $pgCancelled = false; @@ -130,6 +124,7 @@ class AdminVbankRefundController extends AdminBaseController private function claimRefund(string $moid, string $tid, int $cancelAmt): array { return DB::transaction(function () use ($moid, $tid, $cancelAmt): array { + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $order = Order::query() ->where('order_number', $moid) ->lockForUpdate() diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCallbackController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCallbackController.php index 92ffe36a..d4d76b9e 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCallbackController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCallbackController.php @@ -29,6 +29,7 @@ use Plugins\Sirsoft\PayNicepayments\Concerns\RecordsPaymentWindowClosure; use Plugins\Sirsoft\PayNicepayments\Concerns\ResolvesEasyPayDisplay; use Plugins\Sirsoft\PayNicepayments\Concerns\SanitizesPgResponse; use Plugins\Sirsoft\PayNicepayments\Http\Requests\AuthCallbackRequest; +use Plugins\Sirsoft\PayNicepayments\Http\Requests\SignDataRequest; use Plugins\Sirsoft\PayNicepayments\Http\Requests\VbankNotifyRequest; use Plugins\Sirsoft\PayNicepayments\Services\NicePaymentsApiService; use Plugins\Sirsoft\PayNicepayments\Support\ShopRedirectUrl; @@ -428,6 +429,7 @@ class PaymentCallbackController string $txTid, ): string { return DB::transaction(function () use ($moid, $pgResponse, $resultCode, $payMethod, $isEscrow, $txTid): string { + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $lockedOrder = Order::query() ->where('order_number', $moid) ->lockForUpdate() @@ -484,6 +486,7 @@ class PaymentCallbackController 'pg_response_sanitized' => true, 'pg_raw_response' => $this->sanitizePgResponse($pgResponse, self::VBANK_ISSUE_RESPONSE_KEYS), ]; + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $payment->save(); return 'recorded'; @@ -500,6 +503,7 @@ class PaymentCallbackController array $callbackPayload, ): string { return DB::transaction(function () use ($moid, $pgResponse, $txTid, $amt, $isEscrow, $request, $callbackPayload): string { + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $lockedOrder = Order::query() ->where('order_number', $moid) ->lockForUpdate() @@ -584,17 +588,13 @@ class PaymentCallbackController * MOID, 주문 상태, 나이스페이 결제 레코드, Amt 를 우리 DB 와 비교 검증하여 * 클라이언트 측 주문/금액 조작을 차단한다. * - * @param Request $request amt + moid - * @return JsonResponse ediDate / signData / mid 또는 400/422 + * @param SignDataRequest $request amt + moid (형식 검증은 FormRequest — 종전 수동 검사 400 → 표준 422) + * @return JsonResponse ediDate / signData / mid 또는 422 */ - public function signData(Request $request): JsonResponse + public function signData(SignDataRequest $request): JsonResponse { - $amt = (int) $request->input('amt', 0); - $moid = (string) $request->input('moid', ''); - - if ($amt <= 0 || $moid === '') { - return response()->json(['error' => __('sirsoft-pay_nicepayments::messages.errors.invalid_request')], 400); - } + $amt = (int) $request->validated('amt'); + $moid = (string) $request->validated('moid'); $rateLimitKey = $this->signDataRateLimitKey($request, $moid); if (RateLimiter::tooManyAttempts($rateLimitKey, 20)) { @@ -1083,6 +1083,7 @@ class PaymentCallbackController 'vbank_notifications' => $existing, 'vbank_notification_summary' => $this->buildNotificationSummary($existing), ]); + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $payment->save(); }); } catch (\Throwable $e) { diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCloseReportController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCloseReportController.php index ddbd0dc7..45a371d2 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCloseReportController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/PaymentCloseReportController.php @@ -118,6 +118,7 @@ class PaymentCloseReportController private function recordCloseReportWithLock(string $oid, int $price, string $closeReason): array { return DB::transaction(function () use ($oid, $price, $closeReason): array { + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 $lockedOrder = Order::query() ->where('order_number', $oid) ->lockForUpdate() diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/UserReceiptController.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/UserReceiptController.php index c4037c4d..88f68ab8 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/UserReceiptController.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/UserReceiptController.php @@ -4,9 +4,13 @@ declare(strict_types=1); namespace Plugins\Sirsoft\PayNicepayments\Controllers; +// audit:allow api-doc-coverage 요청 파라미터·응답 구조 무변경 — 테이블명 리터럴을 모델 파생으로 정리한 내부 리팩토링 (#571) + use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Modules\Sirsoft\Ecommerce\Models\Order; +use Modules\Sirsoft\Ecommerce\Models\OrderPayment; use Modules\Sirsoft\Ecommerce\Services\GuestOrderAuthService; use Plugins\Sirsoft\PayNicepayments\Concerns\IssuesReceiptCookie; use Plugins\Sirsoft\PayNicepayments\Concerns\ResolvesEasyPayDisplay; @@ -33,11 +37,13 @@ class UserReceiptController * @param string $orderNumber 주문번호 * @return JsonResponse receipt_url / cash_receipt_url / is_test_mode 또는 404 */ + // audit:allow controller-base-request-injection reason: 본문 입력을 읽지 않음 — user()/비회원 영수증 쿠키만 참조 (검증 대상 필드 없음) public function show(Request $request, string $orderNumber): JsonResponse { $user = $request->user(); - $query = DB::table('ecommerce_order_payments as p') - ->join('ecommerce_orders as o', 'o.id', '=', 'p.order_id') + // audit:allow controller-direct-data-access reason: PG 플러그인의 결제 레코드 직접 조회/기록 — ecommerce Repository 의존 시 모듈 버전 제약 연쇄(PaymentLimits 선례). Service/Repository 이관은 후속 백로그 + $query = DB::table((new OrderPayment)->getTable().' as p') + ->join((new Order)->getTable().' as o', 'o.id', '=', 'p.order_id') ->where('o.order_number', $orderNumber) ->where('p.pg_provider', 'nicepayments'); @@ -51,7 +57,7 @@ class UserReceiptController } elseif ($this->verifyReceiptCookie($request->cookie(self::RECEIPT_COOKIE_NAME), $orderNumber)) { $query->whereNull('o.user_id') ->where('o.id', function ($sub) use ($orderNumber) { - $sub->select('id')->from('ecommerce_orders')->where('order_number', $orderNumber); + $sub->select('id')->from((new Order)->getTable())->where('order_number', $orderNumber); }); } else { return response()->json(['error' => 'Not found'], 404); @@ -74,7 +80,7 @@ class UserReceiptController $receiptUrl = $payment->receipt_url; if (! $receiptUrl && $payment->transaction_id) { - $receiptUrl = self::RECEIPT_BASE_URL . '?type=0&TID=' . rawurlencode($payment->transaction_id); + $receiptUrl = self::RECEIPT_BASE_URL.'?type=0&TID='.rawurlencode($payment->transaction_id); } $cashReceiptUrl = null; @@ -84,7 +90,7 @@ class UserReceiptController $meta = json_decode($payment->payment_meta, true); $rcptTid = $meta['rcpt_tid'] ?? ($meta['pg_raw_response']['RcptTID'] ?? null); if ($rcptTid) { - $cashReceiptUrl = self::RECEIPT_BASE_URL . '?type=1&TID=' . rawurlencode($rcptTid); + $cashReceiptUrl = self::RECEIPT_BASE_URL.'?type=1&TID='.rawurlencode($rcptTid); } $isTestMode = (bool) ($meta['is_test_mode'] ?? false); } diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/EscrowDeliveryRegisterRequest.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/EscrowDeliveryRegisterRequest.php new file mode 100644 index 00000000..17c54e32 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/EscrowDeliveryRegisterRequest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayNicepayments\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 에스크로 배송 등록 요청 검증 + * + * 종전 컨트롤러 인라인 validate 를 승격했다 (에스크로 배송등록 3형제 계약 대칭). + * tid 상한 보강: 나이스페이먼츠 TID 는 30자다. + */ +class EscrowDeliveryRegisterRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 에스크로 배송 등록 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'tid' => ['required', 'string', 'max:30'], + 'delivery_name' => ['required', 'string', 'max:100'], + 'tracking_number' => ['required', 'string', 'max:100'], + 'buyer_address' => ['required', 'string', 'max:200'], + 'register_name' => ['required', 'string', 'max:50'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/SignDataRequest.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/SignDataRequest.php new file mode 100644 index 00000000..dddf7e5d --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/SignDataRequest.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayNicepayments\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 결제 요청 SignData 발급 요청 검증 + * + * 비회원 결제도 호출하는 공개 표면 — 형식·길이만 닫고, 주문·금액·구매자 대조는 + * 컨트롤러의 도메인 검증(우리 DB 기준)이 담당한다. + * + * 계약 변경: 종전 수동 검사(400) → 표준 422. 클라이언트는 `.ok` 만 검사하므로 안전(실측). + * 상한 근거: Amt 12자리(나이스페이먼츠 스펙), Moid 64자. + */ +class SignDataRequest extends FormRequest +{ + /** + * 요청 권한 — 공개 엔드포인트이므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * SignData 발급 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'amt' => ['required', 'integer', 'min:1', 'max:999999999999'], + 'moid' => ['required', 'string', 'max:64'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/TransactionQueryRequest.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/TransactionQueryRequest.php new file mode 100644 index 00000000..c5609c78 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/TransactionQueryRequest.php @@ -0,0 +1,37 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayNicepayments\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * TID 단건 거래 조회 요청 검증 + * + * 상한 근거: 나이스페이먼츠 TID 는 30자. + */ +class TransactionQueryRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 거래 조회 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'tid' => ['required', 'string', 'max:30'], + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/VbankRefundRequest.php b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/VbankRefundRequest.php new file mode 100644 index 00000000..895e8232 --- /dev/null +++ b/plugins/_bundled/sirsoft-pay_nicepayments/src/Http/Requests/VbankRefundRequest.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +namespace Plugins\Sirsoft\PayNicepayments\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * 가상계좌 입금 완료 건 환불 요청 검증 + * + * 환불 계좌 정보는 나이스페이먼츠 취소 API 로 전송되고 도메인 취소 사유로 + * 영속되는 금전 표면이다. 상한 근거(나이스페이먼츠 연동 스펙): + * TID 30자 / Moid 64자 / CancelAmt 12자리 / CancelMsg 100자(EUC-KR) / + * RefundAcctNo 16자리 숫자 / RefundBankCd 3자리 은행코드 / RefundAcctNm 10자. + */ +class VbankRefundRequest extends FormRequest +{ + /** + * 요청 권한 — 라우트 permission 미들웨어 체인이 담당. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 가상계좌 환불 요청 검증 규칙을 반환합니다. + * + * @return array<string, array<int, string>> 필드별 검증 규칙 + */ + public function rules(): array + { + return [ + 'tid' => ['required', 'string', 'max:30'], + 'moid' => ['required', 'string', 'max:64'], + 'cancel_amt' => ['required', 'integer', 'min:1', 'max:999999999999'], + 'cancel_msg' => ['nullable', 'string', 'max:100'], + 'refund_acct_no' => ['required', 'string', 'max:16', 'regex:/^\d+$/'], + 'refund_bank_cd' => ['required', 'string', 'regex:/^\d{3}$/'], + 'refund_acct_nm' => ['required', 'string', 'max:10'], + ]; + } + + /** + * 검증 오류 메시지에 쓰일 필드 표시명을 반환합니다. + * + * @return array<string, string> 필드별 표시명 + */ + public function attributes(): array + { + return [ + 'tid' => __('sirsoft-pay_nicepayments::messages.fields.tid'), + 'moid' => __('sirsoft-pay_nicepayments::messages.fields.moid'), + 'cancel_amt' => __('sirsoft-pay_nicepayments::messages.fields.cancel_amt'), + 'cancel_msg' => __('sirsoft-pay_nicepayments::messages.fields.cancel_msg'), + 'refund_acct_no' => __('sirsoft-pay_nicepayments::messages.fields.refund_acct_no'), + 'refund_bank_cd' => __('sirsoft-pay_nicepayments::messages.fields.refund_bank_cd'), + 'refund_acct_nm' => __('sirsoft-pay_nicepayments::messages.fields.refund_acct_nm'), + ]; + } +} diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminEscrowControllerTest.php b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminEscrowControllerTest.php index 5aed6b0a..f1a2fce5 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminEscrowControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminEscrowControllerTest.php @@ -75,6 +75,32 @@ class AdminEscrowControllerTest extends PluginTestCase ->assertJsonPath('success', false); } + /** + * TID 상한(30자) 초과는 EscrowDeliveryRegisterRequest 가 표준 422 로 차단하고 + * PG 배송등록 API 는 호출되지 않아야 한다 (나이스페이먼츠 TID 는 30자). + */ + public function test_register_delivery_rejects_overlong_tid(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $this->createPaidEscrowPayment(); + + $apiServiceMock = $this->createMock(NicePaymentsApiService::class); + $apiServiceMock->expects($this->never())->method('useStoredCredentials'); + $apiServiceMock->expects($this->never())->method('registerEscrowDelivery'); + $this->app->instance(NicePaymentsApiService::class, $apiServiceMock); + + $response = $this->actingAs($admin)->postJson('/api/plugins/sirsoft-pay_nicepayments/admin/escrow/register-delivery', [ + 'tid' => str_repeat('T', 31), + 'delivery_name' => 'CJ대한통운', + 'tracking_number' => 'TRACK123456', + 'buyer_address' => '서울시 테스트구 1', + 'register_name' => '운영자', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['tid']); + } + private function createPaidEscrowPayment(): void { $order = OrderFactory::new()->create([ diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminVbankRefundControllerTest.php b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminVbankRefundControllerTest.php index bc8b9f94..0502897a 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminVbankRefundControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/AdminVbankRefundControllerTest.php @@ -128,6 +128,69 @@ class AdminVbankRefundControllerTest extends PluginTestCase $response->assertForbidden(); } + /** + * 환불 계좌 형식 위반 4종(계좌번호 17자리 / 은행코드 비숫자 / 예금주 11자 / 취소사유 101자)은 + * VbankRefundRequest 가 표준 422 로 차단하고 PG 취소 API 는 호출되지 않아야 한다. + */ + public function test_admin_vbank_refund_rejects_malformed_refund_account_fields(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->createPaidVbankOrder(); + + $apiServiceMock = $this->createMock(NicePaymentsApiService::class); + $apiServiceMock->expects($this->never())->method('useStoredCredentials'); + $apiServiceMock->expects($this->never())->method('cancelPayment'); + $this->app->instance(NicePaymentsApiService::class, $apiServiceMock); + + $response = $this->actingAs($admin)->postJson('/api/plugins/sirsoft-pay_nicepayments/admin/vbank-refund', [ + 'tid' => self::TID, + 'moid' => $order->order_number, + 'cancel_amt' => 30000, + 'cancel_msg' => str_repeat('가', 101), + 'refund_acct_no' => str_repeat('1', 17), + 'refund_bank_cd' => '00A', + 'refund_acct_nm' => str_repeat('가', 11), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors([ + 'cancel_msg', + 'refund_acct_no', + 'refund_bank_cd', + 'refund_acct_nm', + ]); + + $order->refresh(); + $this->assertEquals(OrderStatusEnum::PAYMENT_COMPLETE, $order->order_status); + $this->assertEquals(0.0, (float) $order->payment->cancelled_amount); + } + + /** + * 배열 주입(`tid[]=x`)은 string 규칙이 422 로 차단해야 한다 — 종전 수동 검사는 + * 문자열 캐스팅 전제라 배열 입력이 형식 검증을 우회할 수 있었다. + */ + public function test_admin_vbank_refund_rejects_array_injection(): void + { + $admin = $this->createAdminUser(['sirsoft-ecommerce.orders.update']); + $order = $this->createPaidVbankOrder(); + + $apiServiceMock = $this->createMock(NicePaymentsApiService::class); + $apiServiceMock->expects($this->never())->method('cancelPayment'); + $this->app->instance(NicePaymentsApiService::class, $apiServiceMock); + + $response = $this->actingAs($admin)->postJson('/api/plugins/sirsoft-pay_nicepayments/admin/vbank-refund', [ + 'tid' => ['x'], + 'moid' => $order->order_number, + 'cancel_amt' => 30000, + 'refund_acct_no' => '1234567890', + 'refund_bank_cd' => '004', + 'refund_acct_nm' => '홍길동', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['tid']); + } + private function createPaidVbankOrder(): Order { $user = User::factory()->create(); diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/PaymentCallbackControllerTest.php b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/PaymentCallbackControllerTest.php index 7c96e0ab..299f6845 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/PaymentCallbackControllerTest.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/tests/Feature/Controllers/PaymentCallbackControllerTest.php @@ -428,6 +428,40 @@ class PaymentCallbackControllerTest extends PluginTestCase ->assertJsonPath('error', __('sirsoft-pay_nicepayments::messages.errors.invalid_request')); } + /** + * Moid 상한(64자) 초과는 SignDataRequest 가 표준 422 로 차단해야 한다. + * (계약 변경: 종전 수동 검사 400 → FormRequest 표준 422 — 클라이언트는 `.ok` 만 검사) + */ + public function test_sign_data_rejects_overlong_moid(): void + { + $this->mockPluginSettings(); + + $response = $this->postJson('/plugins/sirsoft-pay_nicepayments/payment/sign-data', [ + 'amt' => 50000, + 'moid' => str_repeat('A', 65), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['moid']); + } + + /** + * 비정수 amt('abc')는 SignDataRequest 의 integer 규칙이 표준 422 로 차단해야 한다. + * (계약 변경: 종전 수동 검사 400 → FormRequest 표준 422) + */ + public function test_sign_data_rejects_non_integer_amt(): void + { + $this->mockPluginSettings(); + + $response = $this->postJson('/plugins/sirsoft-pay_nicepayments/payment/sign-data', [ + 'amt' => 'abc', + 'moid' => 'ORD-TEST-AMT-TYPE', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['amt']); + } + public function test_sign_data_rejects_unknown_order(): void { $this->mockPluginSettings(); diff --git a/plugins/_bundled/sirsoft-pay_nicepayments/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-pay_nicepayments/tests/PluginTestCase.php index b82a5ecb..bc46e2df 100644 --- a/plugins/_bundled/sirsoft-pay_nicepayments/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-pay_nicepayments/tests/PluginTestCase.php @@ -167,8 +167,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); @@ -193,8 +197,12 @@ abstract class PluginTestCase extends TestCase $relativeClass = substr($class, $len); $file = $pluginBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md b/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md index a2c01751..26893dcb 100644 --- a/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-tosspayments/CHANGELOG.md @@ -4,6 +4,14 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.2] - 2026-08-19 + +### Fixed + +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 결제 실패 시 이동하는 페이지 주소에 서버 내부 오류 원문이 그대로 실려 나가던 문제를 수정했습니다. 이제 안내 문구만 전달되며, 원인 파악에 필요한 원문은 서버 로그에만 기록됩니다. +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.1] - 2026-08-11 ### Fixed @@ -51,7 +59,6 @@ ### Fixed - - 쇼핑몰 주소를 기본값이 아닌 값으로 바꾸거나 주소 없이 운영하는 상점에서, 결제를 마치거나 결제에 실패한 구매자가 존재하지 않는 페이지로 이동하던 문제를 수정했습니다. 이동 주소가 기본 주소로 고정돼 있었기 때문이며, 이제 상점 주소 설정을 따라갑니다. 이미 운영 중인 사이트도 업데이트 시 자동으로 반영되며, 이동 주소를 직접 입력해 두었다면 그 값은 그대로 유지됩니다. (#85 @hwaryeon1234 님께서 제보해주셨습니다.) - 전액 환불이 부분취소로 잘못 요청되던 문제를 수정했습니다. 이미 부분취소한 이력이 있는 주문의 잔액 취소도 정확한 금액으로 처리됩니다. - 면세 상품이 포함된 주문을 환불할 때 면세 금액이 함께 전달되지 않아 부가세가 잘못 계산되던 문제를 수정했습니다. diff --git a/plugins/_bundled/sirsoft-tosspayments/components.json b/plugins/_bundled/sirsoft-tosspayments/components.json index d1818243..5a68a743 100644 --- a/plugins/_bundled/sirsoft-tosspayments/components.json +++ b/plugins/_bundled/sirsoft-tosspayments/components.json @@ -1,7 +1,7 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", "identifier": "sirsoft-tosspayments", - "version": "1.0.0", + "version": "1.0.1", "components": { "basic": [], "composite": [], diff --git a/plugins/_bundled/sirsoft-tosspayments/composer.json b/plugins/_bundled/sirsoft-tosspayments/composer.json index ed455896..15e87a6b 100644 --- a/plugins/_bundled/sirsoft-tosspayments/composer.json +++ b/plugins/_bundled/sirsoft-tosspayments/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-tosspayments", "description": "TossPayments PG Plugin for G7 platform", - "version": "1.0.1", + "version": "1.0.2", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-tosspayments/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-tosspayments/dist/js/plugin.iife.js index 4f73c503..16486d0c 100644 --- a/plugins/_bundled/sirsoft-tosspayments/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-tosspayments/dist/js/plugin.iife.js @@ -1,2 +1 @@ (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(){return window}function f(){if(!B.test(window.location.pathname))return!1;const e=new URLSearchParams(window.location.search).get("tab");return e===null||e===""||e==="order_settings"}function j(e){return e instanceof HTMLElement?e.className:""}function C(e){return(e??"").replace(/​/g,"").replace(/\s+/g," ").trim()}function W(e){const t=j(e);return t.includes("excel-card")||t.includes("flex-center")&&t.includes("border")&&t.includes("gap-4")}function N(e){return e.querySelector(".font-medium")}function V(e){const t=C(N(e)?.textContent),s=C(e.textContent);return H.find(n=>n.shortLabels.some(o=>t===o)||n.labels.some(o=>s.includes(o)))??null}function z(e){return Array.from(e.querySelectorAll("div")).filter(W)}function J(e){return Array.from(e.querySelectorAll("svg, [data-icon], i")).find(s=>!s.closest("[data-drag-handle]")&&!s.closest(L)&&!s.closest('.row-stack, .excel-card-body, button, select, [role="switch"]'))??null}function R(e,t){e.dataset.tossAdminPaymentBrandMark="true",e.dataset.tossAdminPaymentMethod=t.id,e.setAttribute("aria-hidden","true"),e.className=`inline-flex items-center justify-center rounded-lg font-bold ${t.markClassName}`,e.style.width="32px",e.style.height="32px",e.style.flex="0 0 32px",e.style.lineHeight="1",e.style.fontSize=t.markLines.join("").length>2?"9px":"12px";const s=t.markLines.join("");e.textContent!==s&&(e.textContent=t.markLines[0]??"")}function Q(e){const t=document.createElement("span");return R(t,e),t}function X(e,t){const s=e.querySelector(L);if(s instanceof HTMLSpanElement)return e.dataset.tossAdminPaymentMethod=t.id,R(s,t),!0;const n=Q(t),o=J(e);if(o&&o.parentElement)return e.dataset.tossAdminPaymentMethod=t.id,o.replaceWith(n),!0;const r=N(e);return r&&r.parentElement?(e.dataset.tossAdminPaymentMethod=t.id,r.parentElement.insertBefore(n,r),!0):!1}function p(e=document){if(typeof window>"u"||typeof document>"u"||!f())return!1;let t=!1;return z(e).forEach(s=>{const n=V(s);n&&X(s,n)&&(t=!0)}),t}function Z(){y||(y=!0,window.setTimeout(()=>{y=!1,p()},0))}function h(){u!==null&&(window.clearInterval(u),u=null)}function w(){if(!f())return;h(),p();let e=0;u=window.setInterval(()=>{e+=1,p(),e>=G&&h()},K);const t=document.body;t[S]||(t[S]=!0,M=new MutationObserver(()=>Z()),M.observe(document.body,{childList:!0,subtree:!0,characterData:!0}))}function P(){if(f()){w();return}h()}function ee(){if(typeof window>"u"||typeof document>"u")return;const e=Y();if(e[E])return;e[E]=!0,console.info(`[${U}] admin payment method brand sync installed`),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>w()):w();const t=history.pushState.bind(history);history.pushState=(...n)=>{t(...n),window.setTimeout(()=>P(),200)};const s=history.replaceState.bind(history);history.replaceState=(...n)=>{s(...n),window.setTimeout(()=>P(),200)},window.addEventListener("popstate",()=>window.setTimeout(()=>P(),200))}const l="sirsoft-tosspayments",_={info:(...e)=>console.info(`[${l}]`,...e),warn:(...e)=>console.warn(`[${l}]`,...e),error:(...e)=>console.error(`[${l}]`,...e)};function I(){const e=window.G7Core;if(!e)return 0;const t=e.getActionDispatcher;if(typeof t!="function")return 0;const s=t();if(!s||typeof s.registerHandler!="function")return 0;let n=0;for(const[o,r]of Object.entries(v)){const a=`${l}.${o}`;s.registerHandler(a,r,{category:"plugin",source:l}),n++}return n}function k(){ee();const e=()=>{const t=I();if(t>0){_.info(`${t} handler(s) registered`);return}let s=0;const n=50,o=setInterval(()=>{s++;const r=I();if(r>0){clearInterval(o),_.info(`${r} handler(s) registered (after ${s} retries)`);return}s>=n&&(clearInterval(o),_.warn("ActionDispatcher not available after timeout"))},100)};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e):e()}k(),window.__SirsoftTosspayments={identifier:l,handlers:Object.keys(v),initPlugin:k}})(); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-tosspayments/lang/en/messages.php b/plugins/_bundled/sirsoft-tosspayments/lang/en/messages.php index 6a8fdb3d..1b924384 100644 --- a/plugins/_bundled/sirsoft-tosspayments/lang/en/messages.php +++ b/plugins/_bundled/sirsoft-tosspayments/lang/en/messages.php @@ -4,6 +4,9 @@ * TossPayments Plugin Messages (English) */ return [ + 'errors' => [ + 'payment_failed' => 'Payment could not be completed. Please try again in a moment.', + ], 'refund' => [ 'missing_payment_key' => 'Cannot process refund: TossPayments payment key not found.', 'default_reason' => 'Cancelled by customer request', diff --git a/plugins/_bundled/sirsoft-tosspayments/lang/ko/messages.php b/plugins/_bundled/sirsoft-tosspayments/lang/ko/messages.php index 2accaebf..420e1b52 100644 --- a/plugins/_bundled/sirsoft-tosspayments/lang/ko/messages.php +++ b/plugins/_bundled/sirsoft-tosspayments/lang/ko/messages.php @@ -4,6 +4,9 @@ * 토스페이먼츠 플러그인 메시지 (Korean) */ return [ + 'errors' => [ + 'payment_failed' => '결제 처리에 실패했습니다. 잠시 후 다시 시도해 주세요.', + ], 'refund' => [ 'missing_payment_key' => '토스페이먼츠 결제 키가 존재하지 않아 환불 처리할 수 없습니다.', 'default_reason' => '고객 요청에 의한 취소', diff --git a/plugins/_bundled/sirsoft-tosspayments/package-lock.json b/plugins/_bundled/sirsoft-tosspayments/package-lock.json index d841b69e..a509cf26 100644 --- a/plugins/_bundled/sirsoft-tosspayments/package-lock.json +++ b/plugins/_bundled/sirsoft-tosspayments/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-tosspayments", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-tosspayments", - "version": "1.0.1", + "version": "1.0.2", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-tosspayments/package.json b/plugins/_bundled/sirsoft-tosspayments/package.json index a502c56a..549cc877 100644 --- a/plugins/_bundled/sirsoft-tosspayments/package.json +++ b/plugins/_bundled/sirsoft-tosspayments/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-tosspayments", - "version": "1.0.1", + "version": "1.0.2", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-tosspayments/plugin.json b/plugins/_bundled/sirsoft-tosspayments/plugin.json index 9d5c7d9e..789e6b69 100644 --- a/plugins/_bundled/sirsoft-tosspayments/plugin.json +++ b/plugins/_bundled/sirsoft-tosspayments/plugin.json @@ -5,7 +5,7 @@ "ko": "토스페이먼츠", "en": "TossPayments" }, - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "description": { "ko": "토스페이먼츠 결제 게이트웨이 (통합결제창 연동)", diff --git a/plugins/_bundled/sirsoft-tosspayments/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-tosspayments/resources/layouts/admin/plugin_settings.json index 7c672c34..be666e68 100644 --- a/plugins/_bundled/sirsoft-tosspayments/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-tosspayments/resources/layouts/admin/plugin_settings.json @@ -1437,7 +1437,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.plugins.settings.save_error" + "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] diff --git a/plugins/_bundled/sirsoft-tosspayments/src/Controllers/PaymentCallbackController.php b/plugins/_bundled/sirsoft-tosspayments/src/Controllers/PaymentCallbackController.php index 7589540e..e41f44a0 100644 --- a/plugins/_bundled/sirsoft-tosspayments/src/Controllers/PaymentCallbackController.php +++ b/plugins/_bundled/sirsoft-tosspayments/src/Controllers/PaymentCallbackController.php @@ -148,7 +148,7 @@ class PaymentCallbackController return redirect($this->resolveFailUrl([ 'error' => 'confirm_failed', - 'message' => $e->getMessage(), + 'message' => __('sirsoft-tosspayments::messages.errors.payment_failed'), 'orderId' => $orderId, ])); } diff --git a/plugins/_bundled/sirsoft-tosspayments/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-tosspayments/tests/PluginTestCase.php index 04535bad..28cd46d3 100644 --- a/plugins/_bundled/sirsoft-tosspayments/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-tosspayments/tests/PluginTestCase.php @@ -2,7 +2,16 @@ namespace Plugins\Sirsoft\Tosspayments\Tests; +use App\Enums\PermissionType; +use App\Extension\HookManager; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Route; +use Modules\Sirsoft\Ecommerce\Database\Seeders\TestingSeeder; +use Modules\Sirsoft\Ecommerce\Providers\EcommerceServiceProvider; use Tests\TestCase; /** @@ -32,7 +41,7 @@ abstract class PluginTestCase extends TestCase */ protected function seeder(): string { - return \Modules\Sirsoft\Ecommerce\Database\Seeders\TestingSeeder::class; + return TestingSeeder::class; } /** @@ -82,7 +91,7 @@ abstract class PluginTestCase extends TestCase $this->registerPluginAutoload(); // 이커머스 모듈 ServiceProvider 등록 (Repository 바인딩) - $this->app->register(\Modules\Sirsoft\Ecommerce\Providers\EcommerceServiceProvider::class); + $this->app->register(EcommerceServiceProvider::class); // 모듈 라우트를 수동으로 등록 $this->registerModuleRoutes(); @@ -93,7 +102,7 @@ abstract class PluginTestCase extends TestCase // SettingsServiceProvider 가 storage/app/settings/general.json 의 site_url 로 // app.url 을 override 하면 Laravel 의 assertRedirect (APP_URL 기반) 와 mismatch. // 테스트 환경에서는 APP_URL 그대로 사용하도록 명시 리셋. - \Illuminate\Support\Facades\Config::set('app.url', env('APP_URL', 'http://localhost')); + Config::set('app.url', env('APP_URL', 'http://localhost')); // HookManager 상태 스냅샷 (tearDown 에서 복원) $this->snapshotHookManager(); @@ -114,7 +123,7 @@ abstract class PluginTestCase extends TestCase */ private function snapshotHookManager(): void { - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $this->hookSnapshot = [ 'hooks' => $ref->getProperty('hooks')->getValue(), 'filters' => $ref->getProperty('filters')->getValue(), @@ -131,7 +140,7 @@ abstract class PluginTestCase extends TestCase return; } - $ref = new \ReflectionClass(\App\Extension\HookManager::class); + $ref = new \ReflectionClass(HookManager::class); $ref->getProperty('hooks')->setValue(null, $this->hookSnapshot['hooks']); $ref->getProperty('filters')->setValue(null, $this->hookSnapshot['filters']); $ref->getProperty('dispatching')->setValue(null, $this->hookSnapshot['dispatching']); @@ -155,15 +164,19 @@ abstract class PluginTestCase extends TestCase } $relativeClass = substr($class, $len); - $file = $moduleBasePath . str_replace('\\', '/', $relativeClass) . '.php'; + $file = $moduleBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); // composer.json files 오토로드 (헬퍼 함수 등록) - $helpersFile = $moduleBasePath . 'Helpers/helpers.php'; + $helpersFile = $moduleBasePath.'Helpers/helpers.php'; if (file_exists($helpersFile)) { require_once $helpersFile; } @@ -176,7 +189,7 @@ abstract class PluginTestCase extends TestCase { // 활성 디렉토리(plugins/sirsoft-tosspayments)가 아닌 자기 자신 기준 경로 — // _bundled 에서 직접 실행할 때도 소스를 찾도록 한다. - $pluginBasePath = dirname(__DIR__) . '/src/'; + $pluginBasePath = dirname(__DIR__).'/src/'; spl_autoload_register(function ($class) use ($pluginBasePath) { $prefix = 'Plugins\\Sirsoft\\Tosspayments\\'; @@ -187,10 +200,14 @@ abstract class PluginTestCase extends TestCase } $relativeClass = substr($class, $len); - $file = $pluginBasePath . str_replace('\\', '/', $relativeClass) . '.php'; + $file = $pluginBasePath.str_replace('\\', '/', $relativeClass).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } @@ -203,7 +220,7 @@ abstract class PluginTestCase extends TestCase $apiRoutesFile = base_path('modules/sirsoft-ecommerce/src/routes/api.php'); if (file_exists($apiRoutesFile)) { - \Illuminate\Support\Facades\Route::prefix('api/modules/sirsoft-ecommerce') + Route::prefix('api/modules/sirsoft-ecommerce') ->name('api.modules.sirsoft-ecommerce.') ->middleware('api') ->group($apiRoutesFile); @@ -215,10 +232,10 @@ abstract class PluginTestCase extends TestCase */ protected function registerPluginRoutes(): void { - $webRoutesFile = dirname(__DIR__) . '/src/routes/web.php'; + $webRoutesFile = dirname(__DIR__).'/src/routes/web.php'; if (file_exists($webRoutesFile)) { - \Illuminate\Support\Facades\Route::prefix('plugins/sirsoft-tosspayments') + Route::prefix('plugins/sirsoft-tosspayments') ->name('plugins.sirsoft-tosspayments.') ->middleware('web') ->group($webRoutesFile); @@ -228,32 +245,32 @@ abstract class PluginTestCase extends TestCase /** * 관리자 사용자를 생성합니다. * - * @param array $permissions 추가 권한 목록 - * @return \App\Models\User + * @param array $permissions 추가 권한 목록 + * @return User */ - protected function createAdminUser(array $permissions = []): \App\Models\User + protected function createAdminUser(array $permissions = []): User { - $user = \App\Models\User::factory()->create(); + $user = User::factory()->create(); - $uniqueRoleIdentifier = 'admin-test-' . $user->id . '-' . time(); - $userRole = \App\Models\Role::create([ + $uniqueRoleIdentifier = 'admin-test-'.$user->id.'-'.time(); + $userRole = Role::create([ 'identifier' => $uniqueRoleIdentifier, 'name' => ['ko' => '테스트 관리자', 'en' => 'Test Admin'], ]); $user->roles()->attach($userRole->id); - $adminAccessPermission = \App\Models\Permission::firstOrCreate( + $adminAccessPermission = Permission::firstOrCreate( ['identifier' => 'admin.access'], [ 'name' => ['ko' => '관리자 접근', 'en' => 'Admin Access'], - 'type' => \App\Enums\PermissionType::Admin, + 'type' => PermissionType::Admin, ] ); $userRole->permissions()->attach($adminAccessPermission->id); if (! empty($permissions)) { foreach ($permissions as $permissionIdentifier) { - $permission = \App\Models\Permission::firstOrCreate( + $permission = Permission::firstOrCreate( ['identifier' => $permissionIdentifier], [ 'name' => ['ko' => $permissionIdentifier, 'en' => $permissionIdentifier], @@ -270,12 +287,12 @@ abstract class PluginTestCase extends TestCase /** * 일반 사용자를 생성합니다. * - * @return \App\Models\User + * @return User */ - protected function createUser(): \App\Models\User + protected function createUser(): User { - $userRole = \App\Models\Role::where('identifier', 'user')->first(); - $user = \App\Models\User::factory()->create(); + $userRole = Role::where('identifier', 'user')->first(); + $user = User::factory()->create(); if ($userRole) { $user->roles()->attach($userRole->id); diff --git a/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md b/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md index cdc6977a..6dc4c422 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-verification_kginicis/CHANGELOG.md @@ -4,6 +4,14 @@ 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.3] - 2026-08-19 + +### Fixed + +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 회원 탈퇴·삭제 시 본인확인 기록 파기와 이력 익명화 중 한쪽만 처리된 채 남을 수 있던 문제를 수정했습니다 — 이제 두 처리가 함께 완료됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.2] - 2026-08-10 ### Changed diff --git a/plugins/_bundled/sirsoft-verification_kginicis/composer.json b/plugins/_bundled/sirsoft-verification_kginicis/composer.json index 35ec03c0..d3f8bb8c 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/composer.json +++ b/plugins/_bundled/sirsoft-verification_kginicis/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-verification_kginicis", "description": "KG Inicis Identity Verification provider for G7", - "version": "1.0.2", + "version": "1.0.3", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-verification_kginicis/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-verification_kginicis/dist/js/plugin.iife.js index 663c14e9..4b1b80c5 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-verification_kginicis/dist/js/plugin.iife.js @@ -1,2 +1 @@ var SirsoftVerificationKginicis=(function(o){"use strict";const s="sirsoft-verification_kginicis",D="inicis",C="width=400,height=640,scrollbars=yes,resizable=yes",L="https://sa.inicis.com/auth",I="identity-challenge-modal",l={info:(...e)=>console.info(`[${s}]`,...e),warn:(...e)=>console.warn(`[${s}]`,...e),error:(...e)=>console.error(`[${s}]`,...e)};function a(){return window.G7Core??null}function v(){return`${window.location.origin}/plugins/${s}/plugin/inicis/callback`}function P(e,i){const t=document.createElement("form");t.setAttribute("name","saForm"),t.setAttribute("method","POST"),t.setAttribute("action",L),t.setAttribute("target",i),t.setAttribute("accept-charset","UTF-8"),t.style.display="none";const f={mid:e.mid,reqSvcCd:e.reqSvcCd,mTxId:e.mtxid,authHash:e.authHash,flgFixedUser:e.flgFixedUser,successUrl:v(),failUrl:v(),reservedMsg:e.reservedMsg};for(const[d,c]of Object.entries(f)){const r=document.createElement("input");r.type="hidden",r.name=d,r.value=c,t.appendChild(r)}document.body.appendChild(t),t.submit(),window.setTimeout(()=>{try{t.remove()}catch{}},0)}function _(e){const t=a()?.state?.set;typeof t=="function"&&t({identityChallenge:{providerInProgress:e}})}const S={NOT_ADULT:"$t:sirsoft-verification_kginicis.errors.not_adult",INVALID_AUTH_URL:"$t:sirsoft-verification_kginicis.errors.invalid_auth_url",DECRYPT_FAILED:"$t:sirsoft-verification_kginicis.errors.decrypt_failed",REMOTE_CALL_FAILED:"$t:sirsoft-verification_kginicis.errors.remote_call_failed",NOT_FOUND:"$t:sirsoft-verification_kginicis.errors.not_found",ALREADY_CONSUMED:"$t:sirsoft-verification_kginicis.errors.already_consumed",IDENTITY_BINDING_MISMATCH:"$t:sirsoft-verification_kginicis.errors.binding_mismatch",INCOMPLETE_IDENTITY:"$t:sirsoft-verification_kginicis.errors.incomplete_identity",STORAGE_FAILED:"$t:sirsoft-verification_kginicis.errors.storage_failed"},T="$t:sirsoft-verification_kginicis.errors.verify_failed",U=new Set(["NOT_ADULT"]);function O(e){return S[e]??T}function b(){const e=a()?.identity?.markDomainNoticeShown;if(typeof e=="function")try{e()}catch{}}async function g(e,i="error"){const t=a()?.dispatch;if(typeof t=="function")try{await t({handler:"toast",params:{type:i,message:e}})}catch{}}function N(e){const i=a()?.dispatch;return typeof i!="function"?Promise.resolve():i({handler:"sequence",params:{actions:[{handler:"resolveIdentityChallenge",params:e},{handler:"closeModal",target:I}]}}).catch(()=>{})}async function h(){const e=a(),i=e?.state?.getGlobal?.()?.identityChallenge??e?.state?.get?.()?.identityChallenge;if(!i){l.error("_global.identityChallenge 미설정 — 코어 모달 진입 흐름 확인 필요");return}const t=i.public_payload;if(!t?.mid||!t?.mtxid||!t?.authHash){l.error("public_payload 의 mid/mtxid/authHash 부재 — Provider.requestChallenge 응답 확인 필요",t),await g("본인인증 페이로드가 준비되지 않았습니다. 잠시 후 다시 시도해 주세요.");return}const f="sa_popup",d=window.open("",f,C);if(!d){l.error("window.open null — 팝업 차단됨"),await g("팝업이 차단되었습니다. 브라우저 팝업 허용 설정 후 다시 시도해 주세요.");return}_(!0),P(t,f);let c=!1,r=null,u=null;const p=()=>{u&&(window.removeEventListener("message",u),u=null),r!==null&&(window.clearInterval(r),r=null)};u=A=>{if(A.origin!==window.location.origin)return;const n=A.data;if(!n||n.type!=="identity_result"||c)return;c=!0,p();try{d.close()}catch{}_(!1);const R=n.verification_token?{result:"verified",token:n.verification_token}:n.identity_error?{result:"failed",failureCode:n.identity_error}:{result:"cancelled"};n.identity_error&&(U.has(n.identity_error)&&b(),g(O(n.identity_error),"error")),N(R)},window.addEventListener("message",u),r=window.setInterval(()=>{if(c){p();return}d.closed&&(c=!0,p(),_(!1))},500)}function y(){const i=a()?.getActionDispatcher;if(typeof i!="function")return!1;const t=i();return!t||typeof t.registerHandler!="function"?!1:(t.registerHandler(`${s}.startAuth`,h,{category:"plugin",source:s}),l.info("startAuth handler registered"),!0)}function E(){if(y())return;let e=0;const i=window.setInterval(()=>{e++,(y()||e>=50)&&(window.clearInterval(i),e>=50&&l.warn("G7Core ActionDispatcher 미준비 — handler 등록 실패"))},100)}function w(){E()}function m(){E()}return m(),window.__SirsoftVerificationKginicis={identifier:s,init:m,initPlugin:w,startAuthHandler:h},o.MODAL_ID=I,o.PLUGIN_IDENTIFIER=s,o.PROVIDER_ID=D,o.init=m,o.initPlugin=w,o.startAuthHandler=h,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"}),o})({}); -//# sourceMappingURL=plugin.iife.js.map diff --git a/plugins/_bundled/sirsoft-verification_kginicis/package-lock.json b/plugins/_bundled/sirsoft-verification_kginicis/package-lock.json index 7105f07d..a5dc8c3f 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/package-lock.json +++ b/plugins/_bundled/sirsoft-verification_kginicis/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-verification_kginicis", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-verification_kginicis", - "version": "1.0.2", + "version": "1.0.3", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-verification_kginicis/package.json b/plugins/_bundled/sirsoft-verification_kginicis/package.json index d4bd1d1f..65ee40f1 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/package.json +++ b/plugins/_bundled/sirsoft-verification_kginicis/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-verification_kginicis", - "version": "1.0.2", + "version": "1.0.3", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-verification_kginicis/plugin.json b/plugins/_bundled/sirsoft-verification_kginicis/plugin.json index 8f448c28..4a16bbf1 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/plugin.json +++ b/plugins/_bundled/sirsoft-verification_kginicis/plugin.json @@ -5,7 +5,7 @@ "ko": "KG이니시스 본인인증", "en": "KG Inicis Identity Verification" }, - "version": "1.0.2", + "version": "1.0.3", "license": "MIT", "description": { "ko": "KG이니시스 통합인증의 본인확인(reqSvcCd=03)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인", diff --git a/plugins/_bundled/sirsoft-verification_kginicis/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-verification_kginicis/resources/layouts/admin/plugin_settings.json index 204d08e5..27166134 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-verification_kginicis/resources/layouts/admin/plugin_settings.json @@ -668,7 +668,7 @@ }, { "handler": "toast", - "params": { "type": "error", "message": "$t:admin.plugins.settings.save_error" } + "params": { "type": "error", "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] } diff --git a/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserDelete.php b/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserDelete.php index b79b3b0f..3648978a 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserDelete.php +++ b/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserDelete.php @@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationKginicis\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Models\User; +use Illuminate\Support\Facades\DB; use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface; use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface; @@ -69,7 +70,11 @@ class CleanInicisRecordOnUserDelete implements HookListenerInterface return; } - $this->recordRepository->deleteByUserId($userId); - $this->logQueryRepository->anonymizeUserId($userId); + // record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 + // 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다. + DB::transaction(function () use ($userId) { + $this->recordRepository->deleteByUserId($userId); + $this->logQueryRepository->anonymizeUserId($userId); + }); } } diff --git a/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserWithdraw.php b/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserWithdraw.php index dfd149ec..3d22a2e1 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserWithdraw.php +++ b/plugins/_bundled/sirsoft-verification_kginicis/src/Listeners/CleanInicisRecordOnUserWithdraw.php @@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationKginicis\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Models\User; +use Illuminate\Support\Facades\DB; use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface; use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface; @@ -60,7 +61,11 @@ class CleanInicisRecordOnUserWithdraw implements HookListenerInterface $userId = (int) $user->id; - $this->recordRepository->deleteByUserId($userId); - $this->logQueryRepository->anonymizeUserId($userId); + // record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 + // 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다. + DB::transaction(function () use ($userId) { + $this->recordRepository->deleteByUserId($userId); + $this->logQueryRepository->anonymizeUserId($userId); + }); } } diff --git a/plugins/_bundled/sirsoft-verification_kginicis/tests/Feature/Listeners/CleanInicisRecordAtomicityTest.php b/plugins/_bundled/sirsoft-verification_kginicis/tests/Feature/Listeners/CleanInicisRecordAtomicityTest.php new file mode 100644 index 00000000..a49af6b3 --- /dev/null +++ b/plugins/_bundled/sirsoft-verification_kginicis/tests/Feature/Listeners/CleanInicisRecordAtomicityTest.php @@ -0,0 +1,153 @@ +<?php + +namespace Plugins\Sirsoft\VerificationKginicis\Tests\Feature\Listeners; + +use App\Enums\IdentityVerificationStatus; +use App\Extension\HookManager; +use App\Models\IdentityVerificationLog; +use App\Models\User; +use App\Services\IdentityLogService; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; +use Mockery; +use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityLogQueryRepositoryInterface; +use Plugins\Sirsoft\VerificationKginicis\Repositories\InicisIdentityRecordRepositoryInterface; +use Plugins\Sirsoft\VerificationKginicis\Tests\PluginTestCase; +use RuntimeException; + +/** + * 본인확인 정리 원자성 + 연쇄 삭제 테스트 (공개이슈 #112 / #110) + * + * record 파기와 로그 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 파기된 것으로 보이는데 + * 이력에 식별자가 남거나, 그 반대가 된다. + * + * 함께, 보관주기 파기(identity:prune-logs)가 challenge 매핑까지 연쇄 삭제하는지 고정한다. + * + * @scenario trigger=withdraw, trigger=delete, trigger=prune_logs, failure_injection=log_anonymize_fails + * + * @effects record_purge_rolled_back, half_done_state_prevented, prune_logs_cascades_to_challenge_mappings + */ +class CleanInicisRecordAtomicityTest extends PluginTestCase +{ + /** + * 로그 익명화가 실패하면 record 파기도 되돌아간다. + */ + public function test_record_purge_rolls_back_when_log_anonymize_fails(): void + { + $user = User::factory()->create(); + $this->seedRecordForUser($user); + + $this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]); + + // 두 번째 단계(로그 익명화)만 실패시킨다 + $failing = Mockery::mock(app(InicisIdentityLogQueryRepositoryInterface::class)); + $failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패')); + $this->app->instance(InicisIdentityLogQueryRepositoryInterface::class, $failing); + + try { + HookManager::doAction('core.user.after_withdraw', $user); + $this->fail('로그 익명화 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('익명화 실패', $e->getMessage()); + } + + // 선행 단계(record 파기)가 원상 복구되어야 한다 + // (실패 시: record 만 파기되고 이력 익명화는 실패한 반쪽 상태) + $this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]); + } + + /** + * 삭제 훅에서도 같은 원자성이 보장된다. + */ + public function test_delete_hook_rolls_back_when_log_anonymize_fails(): void + { + $user = User::factory()->create(); + $this->seedRecordForUser($user); + + $failing = Mockery::mock(app(InicisIdentityLogQueryRepositoryInterface::class)); + $failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패')); + $this->app->instance(InicisIdentityLogQueryRepositoryInterface::class, $failing); + + try { + HookManager::doAction('core.user.before_delete', $user); + $this->fail('로그 익명화 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('익명화 실패', $e->getMessage()); + } + + $this->assertDatabaseHas('inicis_identity_records', ['user_id' => $user->id]); + } + + /** + * B6 연쇄: 보관주기 파기가 challenge 매핑까지 함께 지운다. + */ + public function test_prune_logs_cascades_to_challenge_mappings(): void + { + $expiredLog = $this->seedChallengeLog(now()->subDays(200)); + $recentLog = $this->seedChallengeLog(now()->subDays(10)); + + $this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $expiredLog]); + $this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $recentLog]); + + app(IdentityLogService::class)->purge(180); + + $this->assertDatabaseMissing('identity_verification_logs', ['id' => $expiredLog]); + // 실패 시: 이력은 파기됐는데 challenge 매핑이 고아로 남은 상태 + $this->assertDatabaseMissing('inicis_challenge_mappings', ['challenge_id' => $expiredLog]); + + // 보관기간 이내 이력과 그 매핑은 보존된다 + $this->assertDatabaseHas('identity_verification_logs', ['id' => $recentLog]); + $this->assertDatabaseHas('inicis_challenge_mappings', ['challenge_id' => $recentLog]); + } + + /** + * 본인확인 record 를 시드합니다. + * + * @param User $user 대상 사용자 + */ + private function seedRecordForUser(User $user): void + { + app(InicisIdentityRecordRepositoryInterface::class)->upsertForUser((int) $user->id, [ + 'name_encrypted' => Crypt::encryptString('홍길동'), + 'phone_encrypted' => Crypt::encryptString('01012345678'), + 'birthday_encrypted' => Crypt::encryptString('19900101'), + 'di_encrypted' => Crypt::encryptString('DI-VAL-'.$user->id), + 'di_hash' => hash('sha256', 'DI-VAL-'.$user->id), + 'gender' => 'M', + 'is_foreigner' => false, + 'is_adult' => true, + 'provider_dev_cd' => 'SKT', + 'verified_at' => now(), + ]); + } + + /** + * challenge 로그 + mTxId 매핑을 시드합니다. + * + * @param Carbon $createdAt 생성 시각 + * @return string 생성된 challenge UUID + */ + private function seedChallengeLog($createdAt): string + { + $log = IdentityVerificationLog::create([ + 'id' => (string) Str::uuid(), + 'provider_id' => 'inicis', + 'purpose' => 'signup', + 'channel' => 'sms', + 'target_hash' => hash('sha256', Str::random()), + 'status' => IdentityVerificationStatus::Verified->value, + 'expires_at' => $createdAt, + ]); + $log->forceFill(['created_at' => $createdAt])->saveQuietly(); + + DB::table('inicis_challenge_mappings')->insert([ + 'mtxid' => Str::random(18), + 'challenge_id' => $log->id, + 'created_at' => $createdAt, + ]); + + return $log->id; + } +} diff --git a/plugins/_bundled/sirsoft-verification_kginicis/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-verification_kginicis/tests/PluginTestCase.php index 0c1db028..1e56f27a 100644 --- a/plugins/_bundled/sirsoft-verification_kginicis/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-verification_kginicis/tests/PluginTestCase.php @@ -195,8 +195,12 @@ abstract class PluginTestCase extends TestCase return; } $file = $base.str_replace('\\', '/', substr($class, $len)).'.php'; - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; } }); } diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/CHANGELOG.md b/plugins/_bundled/sirsoft-verification_nhnkcp/CHANGELOG.md index 10ec64ae..97007adc 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/CHANGELOG.md +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/CHANGELOG.md @@ -4,6 +4,18 @@ 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.1] - 2026-08-19 + +### Security + +- 인증 결과 브리지 페이지 진입값에 형식 검증을 추가해 비정상 형태(배열 주입 등)의 요청을 차단합니다. + +### Fixed + +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 회원 탈퇴·삭제 시 본인확인 기록 파기와 이력 익명화 중 한쪽만 처리된 채 남을 수 있던 문제를 수정했습니다 — 이제 두 처리가 함께 완료됩니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 플러그인 설정 저장이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.0] - 2026-08-10 ### Added diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/composer.json b/plugins/_bundled/sirsoft-verification_nhnkcp/composer.json index d30f3414..8d5d74e5 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/composer.json +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/composer.json @@ -1,7 +1,7 @@ { "name": "plugins/sirsoft-verification_nhnkcp", "description": "NHN KCP mobile identity verification provider for G7", - "version": "1.0.0", + "version": "1.0.1", "type": "library", "authors": [ { diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/dist/js/plugin.iife.js b/plugins/_bundled/sirsoft-verification_nhnkcp/dist/js/plugin.iife.js index d55e17fb..0b285e7b 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/dist/js/plugin.iife.js +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/dist/js/plugin.iife.js @@ -1,2 +1 @@ -var SirsoftVerificationNhnkcp=function(c){"use strict";const i="sirsoft-verification_nhnkcp",M="nhnkcp",k="width=410,height=500,scrollbars=yes,resizable=yes",E="kcp_cert_popup",S="identity-challenge-modal",A="g7.identity.redirectStash",h="sirsoft-verification_nhnkcp.formStash",l={info:(...e)=>console.info(`[${i}]`,...e),warn:(...e)=>console.warn(`[${i}]`,...e),error:(...e)=>console.error(`[${i}]`,...e)};function u(){return window.G7Core??null}const R={9999:`$t:${i}.errors.cancelled`,NOT_ADULT:`$t:${i}.errors.not_adult`,DUPLICATE:`$t:${i}.errors.duplicate`,REMOTE_CALL_FAILED:`$t:${i}.errors.remote_call_failed`,DECRYPT_FAILED:`$t:${i}.errors.decrypt_failed`,INCOMPLETE_IDENTITY:`$t:${i}.errors.incomplete_identity`,NOT_FOUND:`$t:${i}.errors.not_found`,ALREADY_CONSUMED:`$t:${i}.errors.already_consumed`,EXPIRED:`$t:${i}.errors.not_found`,IDENTITY_BINDING_MISMATCH:`$t:${i}.errors.binding_mismatch`,STORAGE_FAILED:`$t:${i}.errors.storage_failed`,PROVIDER_ERROR:`$t:${i}.errors.verify_failed`},v=`$t:${i}.errors.verify_failed`,T=new Set(["NOT_ADULT","DUPLICATE"]);function F(e){return R[e]??v}function g(e,t){const n=R[e];if(n)return n;const r=(t??"").trim();return r===""?v:e?`[${e}] ${r}`:r}const N=1023;function b(){if(typeof window>"u")return!1;const e=window.navigator?.userAgent??"";return/Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(e)?!0:typeof window.innerWidth=="number"&&window.innerWidth<=N}function p(e){const t=u()?.state?.set;typeof t=="function"&&t({identityChallenge:{providerInProgress:e}})}function I(){const e=u()?.identity?.markDomainNoticeShown;if(typeof e=="function")try{e()}catch{}}async function _(e,t="error"){const n=u()?.dispatch;if(typeof n=="function")try{await n({handler:"toast",params:{type:t,message:e}})}catch{}}function O(e,t){const n=document.createElement("form");n.setAttribute("name","kcpCertForm"),n.setAttribute("method","POST"),n.setAttribute("action",e.call_url),n.setAttribute("target",t),n.setAttribute("accept-charset","UTF-8"),n.style.display="none";const r={reg_cert_key:e.reg_cert_key,kcp_page_submit_yn:"Y"};for(const[o,s]of Object.entries(r)){const a=document.createElement("input");a.type="hidden",a.name=o,a.value=s,n.appendChild(a)}document.body.appendChild(n),n.submit(),window.setTimeout(()=>{try{n.remove()}catch{}},0)}function U(){try{window.sessionStorage.setItem(A,JSON.stringify({return_url:window.location.href,payload:{},stashed_at:Date.now()}))}catch(e){l.warn("복귀 주소 보관 실패 — 인증 후 처음 화면으로 돌아갈 수 있습니다.",e)}try{window.sessionStorage.setItem(h,JSON.stringify({fields:G(),stashed_at:Date.now()}))}catch(e){l.warn("입력값 보관 실패",e)}}function G(){const e={};return document.querySelectorAll("input[name], select[name], textarea[name]").forEach(n=>{const r=n.getAttribute("name")??"";if(!r)return;const o=(n.getAttribute("type")??"").toLowerCase();if(o==="password"||o==="file"||o==="hidden")return;if(o==="checkbox"||o==="radio"){n.checked&&(e[r]=String(n.value??"on"));return}const s=String(n.value??"");s!==""&&(e[r]=s)}),e}function H(e){const t=Object.keys(e);if(t.length===0)return;let n=0;const r=window.setInterval(()=>{n+=1;let o=0;t.forEach(s=>{const a=document.querySelector(`[name="${Y(s)}"]`);if(!a)return;const d=(a.getAttribute("type")??"").toLowerCase();if(d==="checkbox"||d==="radio"){const f=a;f.value===e[s]&&!f.checked&&f.click(),o+=1;return}j(a,e[s]),o+=1}),(o===t.length||n>=25)&&window.clearInterval(r)},200)}function Y(e){const t=globalThis.CSS;return t&&typeof t.escape=="function"?t.escape(e):e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function j(e,t){const n=Object.getPrototypeOf(e),r=Object.getOwnPropertyDescriptor(n,"value");r?.set?r.set.call(e,t):e.value=t,e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}function K(e){const t=u()?.dispatch;return typeof t!="function"?Promise.resolve():t({handler:"sequence",params:{actions:[{handler:"resolveIdentityChallenge",params:e},{handler:"closeModal",target:S}]}}).catch(()=>{})}const P=new Set;function $(e){e&&P.add(e)}function V(e){return!!e&&P.has(e)}async function B(e){if(!e)return;const t=u()?.api?.getToken?.()??null;try{await fetch(`/api/identity/challenges/${encodeURIComponent(e)}/cancel`,{method:"POST",credentials:"include",headers:{Accept:"application/json",...t?{Authorization:`Bearer ${t}`}:{}}})}catch(n){l.warn("이탈 거래 취소 통보 실패 (재시도 흐름에는 영향 없음)",n)}}async function q(e){const t=u(),n={purpose:e.purpose};e.provider_id&&(n.provider_id=e.provider_id),e.target&&(n.target=e.target);try{const r=t?.api?.getToken?.()??null,o=(()=>{try{return window.localStorage?.getItem("g7_locale")}catch{return null}})(),s=await fetch("/api/identity/challenges",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...o?{"Accept-Language":o}:{},...r?{Authorization:`Bearer ${r}`}:{}},body:JSON.stringify(n)}),a=await s.json().catch(()=>null);if(!s.ok||!a?.success)return l.error("challenge 재발급 실패",a),null;const d=a.data??a,f={...e,challenge_id:d.id,expires_at:d.expires_at,public_payload:d.public_payload??{},error:null};return t?.state?.set?.({identityChallenge:f}),f}catch(r){return l.error("challenge 재발급 중 오류",r),null}}async function y(){const e=u();let t=e?.state?.getGlobal?.()?.identityChallenge??e?.state?.get?.()?.identityChallenge;if(!t){l.error("_global.identityChallenge 미설정 — 코어 모달 진입 흐름 확인 필요");return}if(V(t.challenge_id)){const o=await q(t);if(!o){await _(`$t:${i}.modal.payload_not_ready`);return}t=o}const n=t.public_payload;if(!n?.call_url||!n?.reg_cert_key){l.error("public_payload 의 call_url/reg_cert_key 부재 — 거래등록 응답 확인 필요",n),await _(`$t:${i}.modal.payload_not_ready`);return}if(b()){U(),p(!0),O(n,"_self");return}const r=window.open("",E,k);if(!r){l.error("window.open null — 팝업 차단됨"),await _(`$t:${i}.modal.popup_blocked`);return}p(!0),O(n,E),J(r)}function J(e){let t=!1,n=null,r=null;const o=()=>{r&&(window.removeEventListener("message",r),r=null),n!==null&&(window.clearInterval(n),n=null)};r=s=>{if(s.origin!==window.location.origin)return;const a=s.data;if(!a||a.type!=="identity_result"||t)return;t=!0,o();try{e.close()}catch{}if(p(!1),a.identity_error){const f=a.challenge_id||u()?.state?.getGlobal?.()?.identityChallenge?.challenge_id;$(f),T.has(a.identity_error)&&I(),_(g(a.identity_error,a.identity_error_message),"error");return}const d=a.verification_token?{result:"verified",token:a.verification_token}:{result:"cancelled"};K(d)},window.addEventListener("message",r),n=window.setInterval(()=>{if(t){o();return}if(e.closed){t=!0,o(),p(!1);const s=u()?.state?.getGlobal?.()?.identityChallenge?.challenge_id;$(s),B(s)}},500)}function w(){if(typeof window>"u")return;const e=new URLSearchParams(window.location.search),t=e.get("verification_token")??"",n=e.get("identity_error")??"";if(t===""&&n==="")return;const r=W();if(r&&H(r.fields),t!==""){_(`$t:${i}.modal.mobile_return_success`,"success");return}T.has(n)&&I(),_(g(n,e.get("identity_error_message")??void 0),"error"),e.delete("identity_error"),e.delete("identity_error_message");const o=e.toString(),s=window.location.pathname+(o?`?${o}`:"")+window.location.hash;try{window.history.replaceState(null,"",s)}catch{}}function W(){let e=null;try{e=window.sessionStorage.getItem(h),window.sessionStorage.removeItem(h)}catch{return null}if(!e)return null;try{const t=JSON.parse(e);return!t?.fields||typeof t.stashed_at!="number"||Date.now()-t.stashed_at>6e5?null:t}catch{return null}}function C(){const t=u()?.getActionDispatcher;if(typeof t!="function")return!1;const n=t();return!n||typeof n.registerHandler!="function"?!1:(n.registerHandler(`${i}.startAuth`,y,{category:"plugin",source:i}),l.info("startAuth handler registered"),!0)}function D(){if(C())return;let e=0;const t=window.setInterval(()=>{e++,(C()||e>=50)&&(window.clearInterval(t),e>=50&&l.warn("G7Core ActionDispatcher 미준비 — handler 등록 실패"))},100)}function L(){D()}function m(){w(),D()}return m(),window.__SirsoftVerificationNhnkcp={identifier:i,init:m,initPlugin:L,startAuthHandler:y,handleMobileReturn:w},c.FORM_STASH_KEY=h,c.MODAL_ID=S,c.PLUGIN_IDENTIFIER=i,c.PROVIDER_ID=M,c.REDIRECT_STASH_KEY=A,c.handleMobileReturn=w,c.init=m,c.initPlugin=L,c.isMobileEnvironment=b,c.resolveFailureMessage=g,c.resolveFailureMessageKey=F,c.startAuthHandler=y,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}),c}({}); -//# sourceMappingURL=plugin.iife.js.map +var SirsoftVerificationNhnkcp=(function(c){"use strict";const i="sirsoft-verification_nhnkcp",M="nhnkcp",k="width=410,height=500,scrollbars=yes,resizable=yes",E="kcp_cert_popup",S="identity-challenge-modal",A="g7.identity.redirectStash",h="sirsoft-verification_nhnkcp.formStash",l={info:(...e)=>console.info(`[${i}]`,...e),warn:(...e)=>console.warn(`[${i}]`,...e),error:(...e)=>console.error(`[${i}]`,...e)};function u(){return window.G7Core??null}const R={9999:`$t:${i}.errors.cancelled`,NOT_ADULT:`$t:${i}.errors.not_adult`,DUPLICATE:`$t:${i}.errors.duplicate`,REMOTE_CALL_FAILED:`$t:${i}.errors.remote_call_failed`,DECRYPT_FAILED:`$t:${i}.errors.decrypt_failed`,INCOMPLETE_IDENTITY:`$t:${i}.errors.incomplete_identity`,NOT_FOUND:`$t:${i}.errors.not_found`,ALREADY_CONSUMED:`$t:${i}.errors.already_consumed`,EXPIRED:`$t:${i}.errors.not_found`,IDENTITY_BINDING_MISMATCH:`$t:${i}.errors.binding_mismatch`,STORAGE_FAILED:`$t:${i}.errors.storage_failed`,PROVIDER_ERROR:`$t:${i}.errors.verify_failed`},v=`$t:${i}.errors.verify_failed`,T=new Set(["NOT_ADULT","DUPLICATE"]);function F(e){return R[e]??v}function g(e,t){const n=R[e];if(n)return n;const r=(t??"").trim();return r===""?v:e?`[${e}] ${r}`:r}const N=1023;function b(){if(typeof window>"u")return!1;const e=window.navigator?.userAgent??"";return/Android|iPhone|iPad|iPod|IEMobile|Opera Mini/i.test(e)?!0:typeof window.innerWidth=="number"&&window.innerWidth<=N}function p(e){const t=u()?.state?.set;typeof t=="function"&&t({identityChallenge:{providerInProgress:e}})}function I(){const e=u()?.identity?.markDomainNoticeShown;if(typeof e=="function")try{e()}catch{}}async function _(e,t="error"){const n=u()?.dispatch;if(typeof n=="function")try{await n({handler:"toast",params:{type:t,message:e}})}catch{}}function O(e,t){const n=document.createElement("form");n.setAttribute("name","kcpCertForm"),n.setAttribute("method","POST"),n.setAttribute("action",e.call_url),n.setAttribute("target",t),n.setAttribute("accept-charset","UTF-8"),n.style.display="none";const r={reg_cert_key:e.reg_cert_key,kcp_page_submit_yn:"Y"};for(const[o,s]of Object.entries(r)){const a=document.createElement("input");a.type="hidden",a.name=o,a.value=s,n.appendChild(a)}document.body.appendChild(n),n.submit(),window.setTimeout(()=>{try{n.remove()}catch{}},0)}function U(){try{window.sessionStorage.setItem(A,JSON.stringify({return_url:window.location.href,payload:{},stashed_at:Date.now()}))}catch(e){l.warn("복귀 주소 보관 실패 — 인증 후 처음 화면으로 돌아갈 수 있습니다.",e)}try{window.sessionStorage.setItem(h,JSON.stringify({fields:G(),stashed_at:Date.now()}))}catch(e){l.warn("입력값 보관 실패",e)}}function G(){const e={};return document.querySelectorAll("input[name], select[name], textarea[name]").forEach(n=>{const r=n.getAttribute("name")??"";if(!r)return;const o=(n.getAttribute("type")??"").toLowerCase();if(o==="password"||o==="file"||o==="hidden")return;if(o==="checkbox"||o==="radio"){n.checked&&(e[r]=String(n.value??"on"));return}const s=String(n.value??"");s!==""&&(e[r]=s)}),e}function H(e){const t=Object.keys(e);if(t.length===0)return;let n=0;const r=window.setInterval(()=>{n+=1;let o=0;t.forEach(s=>{const a=document.querySelector(`[name="${Y(s)}"]`);if(!a)return;const d=(a.getAttribute("type")??"").toLowerCase();if(d==="checkbox"||d==="radio"){const f=a;f.value===e[s]&&!f.checked&&f.click(),o+=1;return}j(a,e[s]),o+=1}),(o===t.length||n>=25)&&window.clearInterval(r)},200)}function Y(e){const t=globalThis.CSS;return t&&typeof t.escape=="function"?t.escape(e):e.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}function j(e,t){const n=Object.getPrototypeOf(e),r=Object.getOwnPropertyDescriptor(n,"value");r?.set?r.set.call(e,t):e.value=t,e.dispatchEvent(new Event("input",{bubbles:!0})),e.dispatchEvent(new Event("change",{bubbles:!0}))}function K(e){const t=u()?.dispatch;return typeof t!="function"?Promise.resolve():t({handler:"sequence",params:{actions:[{handler:"resolveIdentityChallenge",params:e},{handler:"closeModal",target:S}]}}).catch(()=>{})}const P=new Set;function $(e){e&&P.add(e)}function V(e){return!!e&&P.has(e)}async function B(e){if(!e)return;const t=u()?.api?.getToken?.()??null;try{await fetch(`/api/identity/challenges/${encodeURIComponent(e)}/cancel`,{method:"POST",credentials:"include",headers:{Accept:"application/json",...t?{Authorization:`Bearer ${t}`}:{}}})}catch(n){l.warn("이탈 거래 취소 통보 실패 (재시도 흐름에는 영향 없음)",n)}}async function q(e){const t=u(),n={purpose:e.purpose};e.provider_id&&(n.provider_id=e.provider_id),e.target&&(n.target=e.target);try{const r=t?.api?.getToken?.()??null,o=(()=>{try{return window.localStorage?.getItem("g7_locale")}catch{return null}})(),s=await fetch("/api/identity/challenges",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...o?{"Accept-Language":o}:{},...r?{Authorization:`Bearer ${r}`}:{}},body:JSON.stringify(n)}),a=await s.json().catch(()=>null);if(!s.ok||!a?.success)return l.error("challenge 재발급 실패",a),null;const d=a.data??a,f={...e,challenge_id:d.id,expires_at:d.expires_at,public_payload:d.public_payload??{},error:null};return t?.state?.set?.({identityChallenge:f}),f}catch(r){return l.error("challenge 재발급 중 오류",r),null}}async function y(){const e=u();let t=e?.state?.getGlobal?.()?.identityChallenge??e?.state?.get?.()?.identityChallenge;if(!t){l.error("_global.identityChallenge 미설정 — 코어 모달 진입 흐름 확인 필요");return}if(V(t.challenge_id)){const o=await q(t);if(!o){await _(`$t:${i}.modal.payload_not_ready`);return}t=o}const n=t.public_payload;if(!n?.call_url||!n?.reg_cert_key){l.error("public_payload 의 call_url/reg_cert_key 부재 — 거래등록 응답 확인 필요",n),await _(`$t:${i}.modal.payload_not_ready`);return}if(b()){U(),p(!0),O(n,"_self");return}const r=window.open("",E,k);if(!r){l.error("window.open null — 팝업 차단됨"),await _(`$t:${i}.modal.popup_blocked`);return}p(!0),O(n,E),J(r)}function J(e){let t=!1,n=null,r=null;const o=()=>{r&&(window.removeEventListener("message",r),r=null),n!==null&&(window.clearInterval(n),n=null)};r=s=>{if(s.origin!==window.location.origin)return;const a=s.data;if(!a||a.type!=="identity_result"||t)return;t=!0,o();try{e.close()}catch{}if(p(!1),a.identity_error){const f=a.challenge_id||u()?.state?.getGlobal?.()?.identityChallenge?.challenge_id;$(f),T.has(a.identity_error)&&I(),_(g(a.identity_error,a.identity_error_message),"error");return}const d=a.verification_token?{result:"verified",token:a.verification_token}:{result:"cancelled"};K(d)},window.addEventListener("message",r),n=window.setInterval(()=>{if(t){o();return}if(e.closed){t=!0,o(),p(!1);const s=u()?.state?.getGlobal?.()?.identityChallenge?.challenge_id;$(s),B(s)}},500)}function w(){if(typeof window>"u")return;const e=new URLSearchParams(window.location.search),t=e.get("verification_token")??"",n=e.get("identity_error")??"";if(t===""&&n==="")return;const r=W();if(r&&H(r.fields),t!==""){_(`$t:${i}.modal.mobile_return_success`,"success");return}T.has(n)&&I(),_(g(n,e.get("identity_error_message")??void 0),"error"),e.delete("identity_error"),e.delete("identity_error_message");const o=e.toString(),s=window.location.pathname+(o?`?${o}`:"")+window.location.hash;try{window.history.replaceState(null,"",s)}catch{}}function W(){let e=null;try{e=window.sessionStorage.getItem(h),window.sessionStorage.removeItem(h)}catch{return null}if(!e)return null;try{const t=JSON.parse(e);return!t?.fields||typeof t.stashed_at!="number"||Date.now()-t.stashed_at>6e5?null:t}catch{return null}}function C(){const t=u()?.getActionDispatcher;if(typeof t!="function")return!1;const n=t();return!n||typeof n.registerHandler!="function"?!1:(n.registerHandler(`${i}.startAuth`,y,{category:"plugin",source:i}),l.info("startAuth handler registered"),!0)}function D(){if(C())return;let e=0;const t=window.setInterval(()=>{e++,(C()||e>=50)&&(window.clearInterval(t),e>=50&&l.warn("G7Core ActionDispatcher 미준비 — handler 등록 실패"))},100)}function L(){D()}function m(){w(),D()}return m(),window.__SirsoftVerificationNhnkcp={identifier:i,init:m,initPlugin:L,startAuthHandler:y,handleMobileReturn:w},c.FORM_STASH_KEY=h,c.MODAL_ID=S,c.PLUGIN_IDENTIFIER=i,c.PROVIDER_ID=M,c.REDIRECT_STASH_KEY=A,c.handleMobileReturn=w,c.init=m,c.initPlugin=L,c.isMobileEnvironment=b,c.resolveFailureMessage=g,c.resolveFailureMessageKey=F,c.startAuthHandler=y,Object.defineProperty(c,Symbol.toStringTag,{value:"Module"}),c})({}); diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/docs/api/identity.md b/plugins/_bundled/sirsoft-verification_nhnkcp/docs/api/identity.md index 544a06e1..17aa3f2f 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/docs/api/identity.md +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/docs/api/identity.md @@ -153,6 +153,12 @@ CI/DI 및 그 해시, 평문 이름·휴대폰·생년월일은 응답에 **포 | `verification_token` | string | 인증 성공 시 발급된 1회용 토큰 | | `challenge_id` | string | 대응하는 본인확인 요청 식별자 | | `identity_error` | string | 실패 시 사유 코드 (`9999`, `NOT_ADULT`, `DUPLICATE`, `REMOTE_CALL_FAILED` 등) | +| `identity_error_message` | string | 실패 시 사유 메시지 (콜백 Resolver 가 발신 — kginicis 브리지에는 없는 본 플러그인 고유 필드) | + +네 쿼리 파라미터는 모두 `KcpBridgeRequest` 가 string 형식으로 검증한다 — 배열 주입 +(`?verification_token[]=x`)은 422 로 차단된다. 종전에는 `(string)` 캐스팅이 배열을 `"Array"` +문자열로 바꿔 브리지 payload 에 유입시킬 수 있었다. 길이 상한은 두지 않는다 (토큰 길이가 +코어 발급 정책에 종속되기 때문). 응답은 HTML 이며 두 갈래로 동작한다. 부모창이 있으면(PC 팝업) 동일 origin 으로 결과를 전달하고 창을 닫는다. 부모창이 없으면(모바일 페이지 전환) 보관해 둔 원래 주소로 결과를 붙여 되돌아간다. diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/package-lock.json b/plugins/_bundled/sirsoft-verification_nhnkcp/package-lock.json index 9c2c734e..111cbc7d 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/package-lock.json +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@g7/sirsoft-verification_nhnkcp", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@g7/sirsoft-verification_nhnkcp", - "version": "1.0.0", + "version": "1.0.1", "devDependencies": { "jsdom": "^27.4.0", "typescript": "^5.3.3", diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/package.json b/plugins/_bundled/sirsoft-verification_nhnkcp/package.json index e9fa22ed..639638ef 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/package.json +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/package.json @@ -1,6 +1,6 @@ { "name": "@g7/sirsoft-verification_nhnkcp", - "version": "1.0.0", + "version": "1.0.1", "type": "module", "private": true, "scripts": { diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/plugin.json b/plugins/_bundled/sirsoft-verification_nhnkcp/plugin.json index 3b00e1f0..2dfa7be8 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/plugin.json +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/plugin.json @@ -5,7 +5,7 @@ "ko": "NHN KCP 휴대폰 본인확인", "en": "NHN KCP Mobile Identity Verification" }, - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "description": { "ko": "NHN KCP 휴대폰 본인확인(V2 REST)을 G7 코어 IDV 인프라에 Provider 로 등록하는 플러그인", diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/resources/layouts/admin/plugin_settings.json b/plugins/_bundled/sirsoft-verification_nhnkcp/resources/layouts/admin/plugin_settings.json index d819850f..75bbbdc7 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/resources/layouts/admin/plugin_settings.json +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/resources/layouts/admin/plugin_settings.json @@ -704,7 +704,7 @@ }, { "handler": "toast", - "params": { "type": "error", "message": "$t:admin.plugins.settings.save_error" } + "params": { "type": "error", "message": "{{error.message ?? '$t:admin.plugins.settings.save_error'}}" } } ] } diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpBridgeController.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpBridgeController.php index 1d227866..2d12a32f 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpBridgeController.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpBridgeController.php @@ -3,8 +3,8 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Http\Controllers; use App\Http\Controllers\Api\Base\PublicBaseController; -use Illuminate\Http\Request; use Illuminate\Http\Response; +use Plugins\Sirsoft\VerificationNhnkcp\Http\Requests\KcpBridgeRequest; /** * 인증 결과 전달 브리지 — 데스크톱 팝업 / 모바일 페이지 전환 분기. @@ -26,17 +26,18 @@ class KcpBridgeController extends PublicBaseController /** * 브리지 페이지를 렌더링한다. * - * @param Request $request 콜백 컨트롤러가 전달한 query + * 배열 주입(`?verification_token[]=x`)은 KcpBridgeRequest 의 string 규칙이 422 로 + * 차단한다 — 종전 `(string)` 캐스팅은 배열을 "Array" 문자열로 바꿔 payload 에 유입시켰다. + * + * @param KcpBridgeRequest $request 콜백 컨트롤러가 전달한 query * @return Response 부모창으로 결과를 전달하는 브리지 HTML */ - public function show(Request $request): Response + public function show(KcpBridgeRequest $request): Response { - $payloadJson = json_encode([ - 'verification_token' => (string) $request->query('verification_token', ''), - 'challenge_id' => (string) $request->query('challenge_id', ''), - 'identity_error' => (string) $request->query('identity_error', ''), - 'identity_error_message' => (string) $request->query('identity_error_message', ''), - ], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); + $payloadJson = json_encode( + $request->bridgePayload(), + JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE + ); return $this->htmlResponse($this->renderBridgeHtml((string) $payloadJson)); } diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpCallbackController.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpCallbackController.php index 0956dfa8..4136f590 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpCallbackController.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Controllers/KcpCallbackController.php @@ -4,7 +4,7 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Http\Controllers; use App\Http\Controllers\Api\Base\PublicBaseController; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; +use Plugins\Sirsoft\VerificationNhnkcp\Http\Requests\KcpCallbackRequest; use Plugins\Sirsoft\VerificationNhnkcp\Services\KcpCallbackResolver; /** @@ -14,8 +14,9 @@ use Plugins\Sirsoft\VerificationNhnkcp\Services\KcpCallbackResolver; * 판정 로직 전체를 KcpCallbackResolver 에 위임하고, 결과를 브리지 페이지로 302 redirect 한다. * PII 는 쿼리에 싣지 않는다 (토큰/실패코드만 전달). * - * FormRequest 미사용 사유: 외부 인증기관이 보내는 임의 필드를 strict validation 으로 차단하면 - * 정상 응답까지 거절된다. raw form POST 를 그대로 Resolver 에 전달한다. + * 콜백 본문은 KcpCallbackRequest("빈 규칙도 결정" 패턴)로 수신한다 — 외부 인증기관이 + * 보내는 임의 필드를 strict validation 으로 차단하면 정상 응답까지 거절되므로 규칙은 + * 비워 두고, 의미 검증은 Resolver 가 담당한다. * * @since 1.0.0 */ @@ -33,10 +34,10 @@ class KcpCallbackController extends PublicBaseController /** * KCP 콜백 수신 + Resolver 위임 + 브리지 페이지로 302 redirect. * - * @param Request $request KCP 표준창이 form POST 한 body + * @param KcpCallbackRequest $request KCP 표준창이 form POST 한 body * @return RedirectResponse 브리지 페이지로의 302 응답 */ - public function handle(Request $request): RedirectResponse + public function handle(KcpCallbackRequest $request): RedirectResponse { $outcome = $this->resolver->resolve( callbackInput: $request->all(), diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpBridgeRequest.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpBridgeRequest.php new file mode 100644 index 00000000..5f31a99e --- /dev/null +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpBridgeRequest.php @@ -0,0 +1,65 @@ +<?php + +// audit:allow api-doc-coverage reason: docblock 서술 정정만 수행 (web GET 브리지의 검증 실패 모드 서술) — 검증 규칙/요청·응답 계약 무변경. 이 브리지는 web 전용 HTML 내비게이션이라 API 문서 대상도 아니다 + +namespace Plugins\Sirsoft\VerificationNhnkcp\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * KCP 인증 결과 브리지 페이지 진입 쿼리를 검증합니다. + * + * 콜백 컨트롤러가 302 redirect 로 붙여 보내는 값만 받습니다. 배열 주입 + * (`?verification_token[]=x`)은 string 규칙이 차단합니다 — 이 라우트는 web GET + * HTML 내비게이션이라 검증 실패는 422 JSON 이 아니라 redirect-back 으로 끝나며, + * 어느 쪽이든 배열이 브리지 payload 에 유입되지 않는 것이 목적입니다. 종전에는 + * `(string)` 캐스팅이 배열을 "Array" 문자열로 바꿔 브리지 payload 에 유입됐습니다. + * 길이 상한을 두지 않는 이유는 토큰 길이가 코어 발급 정책에 종속되어 있기 때문입니다 + * (kginicis InicisPopupBridgeRequest 선례). + * + * 스키마 비대칭(의도): kginicis 브리지는 `identity_error_message` 를 발신하지 않으므로 + * (실측) 그쪽 계약에 이 필드를 신설하지 않는다 — 死계약 신설 금지. 본 플러그인의 + * 콜백 Resolver 는 이 필드를 발신하므로 여기에는 존재한다. + */ +class KcpBridgeRequest extends FormRequest +{ + /** + * 요청 권한 — PG 인증 후 사용자 브라우저가 진입하는 공개 페이지이므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 검증 규칙 + */ + public function rules(): array + { + return [ + 'verification_token' => ['nullable', 'string'], + 'challenge_id' => ['nullable', 'string'], + 'identity_error' => ['nullable', 'string'], + 'identity_error_message' => ['nullable', 'string'], + ]; + } + + /** + * bridge 스크립트에 전달할 payload 를 반환합니다. + * + * @return array<string, string> 네 값 (미지정은 빈 문자열) + */ + public function bridgePayload(): array + { + return [ + 'verification_token' => (string) ($this->validated('verification_token') ?? ''), + 'challenge_id' => (string) ($this->validated('challenge_id') ?? ''), + 'identity_error' => (string) ($this->validated('identity_error') ?? ''), + 'identity_error_message' => (string) ($this->validated('identity_error_message') ?? ''), + ]; + } +} diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpCallbackRequest.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpCallbackRequest.php new file mode 100644 index 00000000..a23b1db8 --- /dev/null +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Http/Requests/KcpCallbackRequest.php @@ -0,0 +1,39 @@ +<?php + +namespace Plugins\Sirsoft\VerificationNhnkcp\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +/** + * KCP 표준창 인증 종료 콜백(Ret_URL) 본문을 수신합니다. + * + * 규칙이 비어 있는 이유: KCP 는 매뉴얼에 명시되지 않은 필드를 함께 보내며 그 목록이 + * 가맹점 설정·인증 수단에 따라 달라집니다. 여기서 화이트리스트를 닫으면 정상 콜백이 + * 422 로 거부되어 인증이 끊깁니다. 콜백 본문의 의미 검증(서명·거래 대조)은 + * `KcpCallbackResolver` 가 단독으로 담당합니다. + * + * base `Request` 대신 이 클래스를 두는 것은, "검증하지 않는다" 가 누락이 아니라 + * 결정임을 코드에 남기기 위해서입니다 (kginicis InicisCallbackRequest 선례). + */ +class KcpCallbackRequest extends FormRequest +{ + /** + * 요청 권한 — 외부 PG 가 호출하는 공개 콜백이므로 true 고정. + * + * @return bool 항상 true + */ + public function authorize(): bool + { + return true; + } + + /** + * 검증 규칙을 반환합니다. + * + * @return array<string, array<int, mixed>> 빈 규칙 (사유는 클래스 설명 참조) + */ + public function rules(): array + { + return []; + } +} diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserDelete.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserDelete.php index f112c013..15c3829b 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserDelete.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserDelete.php @@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Models\User; +use Illuminate\Support\Facades\DB; use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface; use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface; @@ -61,7 +62,11 @@ class CleanKcpRecordOnUserDelete implements HookListenerInterface return; } - $this->recordRepository->deleteByUserId($userId); - $this->logQueryRepository->anonymizeUserId($userId); + // record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 + // 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다. + DB::transaction(function () use ($userId) { + $this->recordRepository->deleteByUserId($userId); + $this->logQueryRepository->anonymizeUserId($userId); + }); } } diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserWithdraw.php b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserWithdraw.php index 8d10a1bc..7782981a 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserWithdraw.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/src/Listeners/CleanKcpRecordOnUserWithdraw.php @@ -4,6 +4,7 @@ namespace Plugins\Sirsoft\VerificationNhnkcp\Listeners; use App\Contracts\Extension\HookListenerInterface; use App\Models\User; +use Illuminate\Support\Facades\DB; use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface; use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface; @@ -59,7 +60,11 @@ class CleanKcpRecordOnUserWithdraw implements HookListenerInterface return; } - $this->recordRepository->deleteByUserId($userId); - $this->logQueryRepository->anonymizeUserId($userId); + // record 파기와 log 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 + // 파기된 것으로 보이는데 이력에 식별자가 남거나, 그 반대가 된다. + DB::transaction(function () use ($userId) { + $this->recordRepository->deleteByUserId($userId); + $this->logQueryRepository->anonymizeUserId($userId); + }); } } diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Controllers/KcpCallbackControllerTest.php b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Controllers/KcpCallbackControllerTest.php index 23ec2c55..0f4e5b73 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Controllers/KcpCallbackControllerTest.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Controllers/KcpCallbackControllerTest.php @@ -127,6 +127,41 @@ class KcpCallbackControllerTest extends PluginTestCase $this->assertStringContainsString('window.location.origin', (string) $html); } + /** + * 배열 주입(`?verification_token[]=x`)은 KcpBridgeRequest 의 string 규칙이 422 로 + * 차단해야 한다 — 종전 `(string)` 캐스팅은 배열을 "Array" 문자열로 바꿔 브리지 + * payload 에 유입시켰다. + */ + public function test_bridge_rejects_array_verification_token_with_422(): void + { + $response = $this->getJson(self::BRIDGE_URL.'?verification_token[]=x'); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['verification_token']); + } + + /** + * 정상 쿼리의 identity_error / identity_error_message 는 브리지 HTML payload 에 + * 그대로 실려야 한다 (KcpBridgeRequest 주입 후 bridgePayload() 경유 회귀 방지). + */ + public function test_bridge_renders_identity_error_message_passthrough(): void + { + $response = $this->get(self::BRIDGE_URL.'?'.http_build_query([ + 'challenge_id' => 'chal-456', + 'identity_error' => '9999', + 'identity_error_message' => '사용자취소', + ])); + + $response->assertStatus(200); + $response->assertHeader('Content-Type', 'text/html; charset=UTF-8'); + + $html = (string) $response->getContent(); + $this->assertStringContainsString('identity_result', $html); + $this->assertStringContainsString('"identity_error":"9999"', $html); + $this->assertStringContainsString('사용자취소', $html); + $this->assertStringContainsString('"challenge_id":"chal-456"', $html); + } + /** * 결과조회 응답을 가로챈다. */ diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Listeners/CleanKcpRecordAtomicityTest.php b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Listeners/CleanKcpRecordAtomicityTest.php new file mode 100644 index 00000000..08fc82e3 --- /dev/null +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Feature/Listeners/CleanKcpRecordAtomicityTest.php @@ -0,0 +1,97 @@ +<?php + +namespace Plugins\Sirsoft\VerificationNhnkcp\Tests\Feature\Listeners; + +use App\Extension\HookManager; +use App\Models\User; +use Mockery; +use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityLogQueryRepositoryInterface; +use Plugins\Sirsoft\VerificationNhnkcp\Repositories\KcpIdentityRecordRepositoryInterface; +use Plugins\Sirsoft\VerificationNhnkcp\Tests\PluginTestCase; +use RuntimeException; + +/** + * 본인확인 정리 원자성 테스트 (공개이슈 #112) + * + * record 파기와 로그 익명화는 함께 성공해야 한다 — 한쪽만 끝나면 파기된 것으로 보이는데 + * 이력에 식별자가 남거나, 그 반대가 된다. + * + * @scenario trigger=withdraw, trigger=delete, failure_injection=log_anonymize_fails + * + * @effects record_purge_rolled_back, half_done_state_prevented + */ +class CleanKcpRecordAtomicityTest extends PluginTestCase +{ + /** + * 탈퇴 훅: 로그 익명화가 실패하면 record 파기도 되돌아간다. + */ + public function test_record_purge_rolls_back_when_log_anonymize_fails_on_withdraw(): void + { + $user = $this->seedUserWithRecord(); + + $this->failLogAnonymize(); + + try { + HookManager::doAction('core.user.after_withdraw', $user); + $this->fail('로그 익명화 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('익명화 실패', $e->getMessage()); + } + + // 실패 시: record 만 파기되고 이력에는 식별자가 남는 반쪽 상태 + $this->assertNotNull( + app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id) + ); + } + + /** + * 삭제 훅에서도 같은 원자성이 보장된다. + */ + public function test_record_purge_rolls_back_when_log_anonymize_fails_on_delete(): void + { + $user = $this->seedUserWithRecord(); + + $this->failLogAnonymize(); + + try { + HookManager::doAction('core.user.before_delete', $user); + $this->fail('로그 익명화 실패가 전파되어야 합니다.'); + } catch (\Throwable $e) { + $this->assertStringContainsString('익명화 실패', $e->getMessage()); + } + + $this->assertNotNull( + app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id) + ); + } + + /** + * record 를 가진 사용자를 만듭니다. + * + * @return User 생성된 사용자 + */ + private function seedUserWithRecord(): User + { + $user = User::factory()->create(); + + app(KcpIdentityRecordRepositoryInterface::class)->upsertForUser($user->id, [ + 'di_hash' => hash_hmac('sha256', 'DI-ATOMIC-'.$user->id, (string) config('app.key')), + 'verified_at' => now(), + ]); + + $this->assertNotNull(app(KcpIdentityRecordRepositoryInterface::class)->findByUserId($user->id)); + + return $user; + } + + /** + * 두 번째 단계(로그 익명화)만 실패시킵니다. + */ + private function failLogAnonymize(): void + { + $failing = Mockery::mock(app(KcpIdentityLogQueryRepositoryInterface::class)); + $failing->shouldReceive('anonymizeUserId')->andThrow(new RuntimeException('익명화 실패')); + + $this->app->instance(KcpIdentityLogQueryRepositoryInterface::class, $failing); + } +} diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwright/specs/admin/nhnkcp-verification-settings.spec.ts b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwright/specs/admin/nhnkcp-verification-settings.spec.ts index 0fd1f1d3..99eb39da 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwright/specs/admin/nhnkcp-verification-settings.spec.ts +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/Playwright/specs/admin/nhnkcp-verification-settings.spec.ts @@ -8,8 +8,11 @@ * KCP 표준창 내부(외부 도메인)는 자동화 대상이 아니다 — 인증창이 열리는 것까지만 확인 가능하며 * 통신사 인증 완주는 실 휴대폰이 필요해 사람이 검증한다. * - * @scenario mode=test,live_credentials=complete + mode=live,live_credentials=missing_site_cd - * @effects live_mode_requires_site_cd_and_enc_key + test_mode_saves_without_live_credentials + * 축 요약(마커 아님 — 평문): mode=test / live_credentials=complete 조합과 + * mode=live / live_credentials=missing_site_cd 조합. 두 조합은 각 test 의 마커가 잠그며, + * 여기에 마커로 다시 적으면 파서가 항목을 쉼표로만 나누어 두 조합이 한 문자열로 뭉친다. + * + * 효과 요약(마커 아님 — 평문): live_mode_requires_site_cd_and_enc_key, test_mode_saves_without_live_credentials. */ import { test, expect, authenticatePage } from '../../fixtures/nhnkcp-auth'; diff --git a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/PluginTestCase.php b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/PluginTestCase.php index 23b3d4e2..a5eb7d9a 100644 --- a/plugins/_bundled/sirsoft-verification_nhnkcp/tests/PluginTestCase.php +++ b/plugins/_bundled/sirsoft-verification_nhnkcp/tests/PluginTestCase.php @@ -198,8 +198,12 @@ abstract class PluginTestCase extends TestCase // composer.json 의 psr-4 는 src/ 와 플러그인 루트를 함께 매핑한다 (Plugin 클래스는 plugin.php). foreach ([$base.$relative.'.php', $root.strtolower($relative).'.php'] as $file) { - if (file_exists($file)) { - require $file; + if (file_exists($file) + && ! class_exists($class, false) && ! interface_exists($class, false) + && ! trait_exists($class, false) && ! enum_exists($class, false)) { + // 활성 디렉토리 사본이 이미 로드된 심볼을 다시 선언하면 fatal 이 된다 — + // 선언 여부를 자체 확인하고 require_once 로 이중 방어한다 + require_once $file; return; } diff --git a/public/build/core/devtools.min.js b/public/build/core/devtools.min.js index 66ccd681..4ba3df17 100644 --- a/public/build/core/devtools.min.js +++ b/public/build/core/devtools.min.js @@ -1558,4 +1558,3 @@ details[open] > .g7dt-summary::before { } `,ve=()=>{const d="g7-devtools-styles";if(document.getElementById(d))return;const t=document.createElement("style");t.id=d,t.textContent=be,document.head.appendChild(t)},le={state:"상태",actions:"액션",cache:"캐시",diagnose:"진단",performance:"성능",lifecycle:"라이프사이클",network:"네트워크",form:"Form",conditional:"조건부",expression:"표현식",handlers:"핸들러",events:"이벤트",datasources:"데이터소스",renders:"렌더링",styles:"스타일",auth:"인증",logs:"로그",layout:"레이아웃",changedetection:"변경감지",staleclosure:"Stale Closure",nestedcontext:"Context",modalscope:"모달스코프",namedactions:"Named Actions"},Ne=700,ue=500,re=400,ce=300,ne="g7-devtools-panel",te=d=>d===null||d===void 0?"g7dt-text-gray":typeof d=="string"?"g7dt-text-yellow":typeof d=="number"?"g7dt-text-cyan":typeof d=="boolean"?"g7dt-text-pink":Array.isArray(d)?"g7dt-text-orange":typeof d=="object"?"g7dt-text-blue":"g7dt-text-white",ye=d=>d===null?"null":d===void 0?"undefined":Array.isArray(d)?`Array(${d.length})`:typeof d=="object"?`Object(${Object.keys(d).length})`:typeof d,we=d=>d===null?"null":d===void 0?"undefined":typeof d=="string"?d.length>50?`"${d.substring(0,50)}..."`:`"${d}"`:typeof d=="boolean"?d?"true":"false":typeof d=="number"?String(d):"",oe=()=>{try{const d=localStorage.getItem(ne);if(d)return JSON.parse(d)}catch(d){console.warn("[G7DevTools] 설정 로드 실패:",d)}return{}},ie=d=>{try{const s={...oe(),...d};localStorage.setItem(ne,JSON.stringify(s))}catch(t){console.warn("[G7DevTools] 설정 저장 실패:",t)}},W=({keyName:d,value:t,path:s,expandedPaths:a,onToggle:g,searchTerm:r,depth:o=0})=>{const p=t!==null&&typeof t=="object",v=a.has(s),l=Array.isArray(t),f=r&&(d.toLowerCase().includes(r.toLowerCase())||typeof t=="string"&&t.toLowerCase().includes(r.toLowerCase())||typeof t=="number"&&String(t).includes(r)),c=y=>{y.stopPropagation(),p&&g(s)},h=p?Object.entries(t):[];return e.jsxs("div",{style:{marginLeft:o>0?"16px":"0"},children:[e.jsxs("div",{className:`g7dt-tree-node g7dt-flex g7dt-items-center g7dt-gap-1 ${f?"g7dt-ring-highlight":""}`,onClick:c,children:[p?e.jsx("span",{className:"g7dt-text-gray g7dt-w-4 g7dt-text-xs g7dt-select-none",style:{textAlign:"center"},children:v?"▼":"▶"}):e.jsx("span",{className:"g7dt-w-4"}),e.jsx("span",{className:"g7dt-text-purple g7dt-text-xs",children:d}),e.jsx("span",{className:"g7dt-text-gray-dark g7dt-text-xs",children:":"}),p?e.jsxs("span",{className:`g7dt-text-xs ${te(t)}`,children:[l?"[":"{",!v&&e.jsx("span",{className:"g7dt-text-gray",style:{margin:"0 4px"},children:ye(t)}),!v&&(l?"]":"}")]}):e.jsx("span",{className:`g7dt-text-xs ${te(t)}`,children:we(t)}),!p&&e.jsxs("span",{className:"g7dt-text-gray-dark g7dt-text-xs g7dt-ml-1",children:["(",typeof t,")"]})]}),p&&v&&e.jsxs("div",{className:"g7dt-border-l",style:{marginLeft:"8px"},children:[h.map(([y,n])=>e.jsx(W,{keyName:l?`[${y}]`:y,value:n,path:`${s}.${y}`,expandedPaths:a,onToggle:g,searchTerm:r,depth:o+1},`${s}.${y}`)),e.jsx("div",{className:"g7dt-ml-4 g7dt-text-xs",children:e.jsx("span",{className:te(t),children:l?"]":"}"})})]})]})},ke=({state:d,devTools:t})=>{const[s,a]=x.useState(new Set(["_global","_local","_computed","_isolated","$parent"])),[g,r]=x.useState(""),[o,p]=x.useState(!1),[v,l]=x.useState(!1),[f,c]=x.useState(null),h=x.useCallback(()=>{t&&c(t.getComputedDependencyTrackingInfo())},[t]);x.useEffect(()=>{if(v&&t){h();const w=setInterval(h,2e3);return()=>clearInterval(w)}},[v,t,h]);const y=w=>{switch(w){case"state-change":return"📊";case"prop-change":return"📥";case"dependency-change":return"🔗";case"manual":return"👆";case"initial":return"🚀";default:return"❓"}},n=x.useCallback(w=>{a(S=>{const z=new Set(S);return z.has(w)?z.delete(w):z.add(w),z})},[]),i=x.useCallback((w,S)=>{const z=[S];if(w!==null&&typeof w=="object")for(const[O,K]of Object.entries(w))z.push(...i(K,`${S}.${O}`));return z},[]),u=x.useCallback(()=>{if(o)a(new Set(["_global","_local","_computed","_isolated","$parent"]));else{const w=new Set;i(d._global,"_global").forEach(S=>w.add(S)),i(d._local,"_local").forEach(S=>w.add(S)),i(d._computed,"_computed").forEach(S=>w.add(S)),i(d._isolated,"_isolated").forEach(S=>w.add(S)),d.$parent&&i(d.$parent,"$parent").forEach(S=>w.add(S)),a(w)}p(!o)},[o,d,i]),m=Object.keys(d._global||{}).length,k=Object.keys(d._local||{}).length,b=Object.keys(d._computed||{}).length,N=Object.keys(d._isolated||{}).length,C=Object.keys(d.$parent||{}).length;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-items-center g7dt-flex-shrink-0",children:[e.jsx("input",{type:"text",placeholder:"키 또는 값 검색...",value:g,onChange:w=>r(w.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:u,className:"g7dt-btn g7dt-bg-gray-700",children:o?"모두 접기":"모두 펼치기"})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-3 g7dt-text-xs g7dt-text-gray g7dt-flex-wrap g7dt-flex-shrink-0",children:[e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-yellow",children:"●"})," string"]}),e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-cyan",children:"●"})," number"]}),e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-pink",children:"●"})," boolean"]}),e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"●"})," null"]}),e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-orange",children:"●"})," array"]}),e.jsxs("span",{children:[e.jsx("span",{className:"g7dt-text-blue",children:"●"})," object"]})]}),e.jsxs("div",{className:"g7dt-bg-gray-800 g7dt-rounded g7dt-p-2 g7dt-overflow-auto g7dt-border",style:{flex:1,minHeight:0},children:[e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-sm g7dt-font-bold g7dt-text-green g7dt-mb-1 g7dt-cursor-pointer",onClick:()=>n("_global"),children:[e.jsx("span",{children:s.has("_global")?"▼":"▶"}),e.jsx("span",{children:"_global"}),e.jsxs("span",{className:"g7dt-text-gray g7dt-text-xs",style:{fontWeight:"normal"},children:["(",m,"개 키)"]})]}),s.has("_global")&&e.jsx("div",{className:"g7dt-ml-2 g7dt-border-l",children:Object.entries(d._global||{}).map(([w,S])=>e.jsx(W,{keyName:w,value:S,path:`_global.${w}`,expandedPaths:s,onToggle:n,searchTerm:g},`_global.${w}`))})]}),e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-sm g7dt-font-bold g7dt-text-blue g7dt-mb-1 g7dt-cursor-pointer",onClick:()=>n("_local"),children:[e.jsx("span",{children:s.has("_local")?"▼":"▶"}),e.jsx("span",{children:"_local"}),e.jsxs("span",{className:"g7dt-text-gray g7dt-text-xs",style:{fontWeight:"normal"},children:["(",k,"개 키)"]})]}),s.has("_local")&&e.jsx("div",{className:"g7dt-ml-2 g7dt-border-l",children:Object.entries(d._local||{}).map(([w,S])=>e.jsx(W,{keyName:w,value:S,path:`_local.${w}`,expandedPaths:s,onToggle:n,searchTerm:g},`_local.${w}`))})]}),e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-sm g7dt-font-bold g7dt-text-purple g7dt-mb-1 g7dt-cursor-pointer",onClick:()=>n("_computed"),children:[e.jsx("span",{children:s.has("_computed")?"▼":"▶"}),e.jsx("span",{children:"_computed"}),e.jsxs("span",{className:"g7dt-text-gray g7dt-text-xs",style:{fontWeight:"normal"},children:["(",b,"개 키)"]})]}),s.has("_computed")&&e.jsx("div",{className:"g7dt-ml-2 g7dt-border-l",children:Object.entries(d._computed||{}).map(([w,S])=>e.jsx(W,{keyName:w,value:S,path:`_computed.${w}`,expandedPaths:s,onToggle:n,searchTerm:g},`_computed.${w}`))})]}),N>0&&e.jsxs("div",{children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-sm g7dt-font-bold g7dt-text-orange g7dt-mb-1 g7dt-cursor-pointer",onClick:()=>n("_isolated"),children:[e.jsx("span",{children:s.has("_isolated")?"▼":"▶"}),e.jsx("span",{children:"_isolated"}),e.jsxs("span",{className:"g7dt-text-gray g7dt-text-xs",style:{fontWeight:"normal"},children:["(",N,"개 스코프)"]})]}),s.has("_isolated")&&e.jsx("div",{className:"g7dt-ml-2 g7dt-border-l",children:Object.entries(d._isolated||{}).map(([w,S])=>e.jsx(W,{keyName:w,value:S,path:`_isolated.${w}`,expandedPaths:s,onToggle:n,searchTerm:g},`_isolated.${w}`))})]}),C>0&&e.jsxs("div",{children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-sm g7dt-font-bold g7dt-text-cyan g7dt-mb-1 g7dt-cursor-pointer",onClick:()=>n("$parent"),children:[e.jsx("span",{children:s.has("$parent")?"▼":"▶"}),e.jsx("span",{children:"$parent"}),e.jsxs("span",{className:"g7dt-text-gray g7dt-text-xs",style:{fontWeight:"normal"},children:["(",C,"개 키)"]})]}),s.has("$parent")&&e.jsx("div",{className:"g7dt-ml-2 g7dt-border-l",children:Object.entries(d.$parent||{}).map(([w,S])=>e.jsx(W,{keyName:w,value:S,path:`$parent.${w}`,expandedPaths:s,onToggle:n,searchTerm:g},`$parent.${w}`))})]})]}),t&&e.jsx("div",{className:"g7dt-flex g7dt-justify-end g7dt-mt-2",children:e.jsx("button",{onClick:()=>l(!v),className:`g7dt-tab-button ${v?"g7dt-tab-active":""}`,children:"🧮 Computed 추적"})}),v&&f&&e.jsxs("div",{className:"g7dt-card g7dt-mt-2",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:["Computed 의존성 추적 (",f.properties.length,"개)"]}),e.jsxs("div",{className:"g7dt-stats-grid g7dt-mb-3",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-purple",children:f.stats.totalComputed}),e.jsx("div",{className:"g7dt-stat-label",children:"Computed"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:f.stats.totalRecalculations}),e.jsx("div",{className:"g7dt-stat-label",children:"재계산"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-yellow",children:f.stats.unnecessaryRecalculations}),e.jsx("div",{className:"g7dt-stat-label",children:"불필요"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsxs("div",{className:"g7dt-stat-value g7dt-text-green",children:[f.stats.avgComputationTime.toFixed(1),"ms"]}),e.jsx("div",{className:"g7dt-stat-label",children:"평균시간"})]})]}),f.stats.cycleDetectionCount>0&&e.jsxs("div",{className:"g7dt-text-xs g7dt-text-red g7dt-mb-3 g7dt-p-2 g7dt-bg-red-900 g7dt-rounded",children:["⚠️ 순환 의존성 감지: ",f.stats.cycleDetectionCount,"건"]}),f.stats.topRecalculated.length>0&&e.jsxs("div",{className:"g7dt-mb-3",children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"자주 재계산되는 속성"}),e.jsx("div",{className:"g7dt-space-y-1",children:f.stats.topRecalculated.slice(0,3).map((w,S)=>e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-text-xs",children:[e.jsx("span",{className:"g7dt-text-purple g7dt-font-mono",children:w.name}),e.jsxs("span",{className:"g7dt-text-muted",children:[w.count,"회"]})]},S))})]}),f.recalcLogs.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"재계산 로그가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-40 g7dt-overflow-y-auto",children:f.recalcLogs.slice().reverse().slice(0,20).map(w=>e.jsxs("div",{className:`g7dt-list-item ${w.valueChanged?"":"g7dt-border-yellow"}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:y(w.trigger)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-purple g7dt-font-mono",children:w.computedName}),!w.valueChanged&&e.jsx("span",{className:"g7dt-badge g7dt-text-xs g7dt-text-yellow",children:"값 변경 없음"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:[w.computationTime.toFixed(1),"ms"]})]}),w.triggeredBy&&e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1",children:["트리거: ",e.jsx("span",{className:"g7dt-text-blue",children:w.triggeredBy.path})]})]},w.id))})]})]})},Ce=({actions:d,devTools:t})=>{const[s,a]=x.useState(""),[g,r]=x.useState("actions"),[o,p]=x.useState(null),[v,l]=x.useState(null),f=x.useCallback(()=>{t&&p(t.getSequenceTrackingInfo())},[t]);x.useEffect(()=>{if(g==="sequences"&&t){f();const i=setInterval(f,2e3);return()=>clearInterval(i)}},[g,t,f]);const c=s?d.filter(i=>i.type.toLowerCase().includes(s.toLowerCase())||i.status===s):d,h=i=>{switch(i){case"success":return"g7dt-status-success";case"error":return"g7dt-status-error";case"started":return"g7dt-status-started";default:return"g7dt-status-default"}},y=i=>{switch(i){case"success":return"g7dt-badge-success";case"error":return"g7dt-badge-error";default:return"g7dt-badge-warning"}},n=v&&o?o.executions.find(i=>i.id===v):null;return e.jsxs("div",{className:"g7dt-space-y-4",children:[t&&e.jsx("div",{className:"g7dt-card",children:e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsxs("button",{className:`g7dt-tab-button ${g==="actions"?"g7dt-tab-active":""}`,onClick:()=>r("actions"),children:["📋 액션 (",d.length,")"]}),e.jsxs("button",{className:`g7dt-tab-button ${g==="sequences"?"g7dt-tab-active":""}`,onClick:()=>r("sequences"),children:["🔗 Sequence (",o?.executions.length||0,")"]})]})}),g==="actions"&&e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-flex-shrink-0",children:[e.jsx("input",{type:"text",placeholder:"액션 타입 또는 상태로 필터...",value:s,onChange:i=>a(i.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:()=>a(""),className:"g7dt-btn-text",children:"초기화"})]}),g==="sequences"&&o&&e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Sequence 통계"}),e.jsxs("div",{className:"g7dt-stats-grid-3",children:[e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-blue",children:o.stats.totalSequences}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"총 실행"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-green",children:o.stats.successCount}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"성공"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-red",children:o.stats.errorCount}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"실패"})]})]})]}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Sequence 실행 목록"}),o.executions.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"실행된 Sequence가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-40 g7dt-overflow-y-auto",children:o.executions.slice().reverse().map(i=>e.jsx("div",{className:`g7dt-list-item g7dt-cursor-pointer ${v===i.id?"g7dt-selected":""} ${i.status==="error"?"g7dt-border-red":""}`,onClick:()=>l(v===i.id?null:i.id),children:e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:i.status==="completed"?"✅":i.status==="error"?"❌":"⏳"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-white",children:[i.actions.length,"개 액션"]}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:[i.totalDuration.toFixed(0),"ms"]})]})},i.id))})]}),n&&e.jsxs("div",{className:"g7dt-card g7dt-border-blue",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:["🔗 Sequence 상세 (",n.actions.length,"개 액션)"]}),e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:n.actions.map((i,u)=>e.jsxs("div",{className:`g7dt-list-item ${i.status==="error"?"g7dt-border-red":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted",children:["#",i.index+1]}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white g7dt-font-mono",children:i.handler}),e.jsx("span",{className:`g7dt-text-xs ${i.status==="success"?"g7dt-text-green":"g7dt-text-red"}`,children:i.status==="success"?"✓":"✕"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:[i.duration.toFixed(0),"ms"]})]}),i.stateChanges&&i.stateChanges.length>0&&e.jsxs("div",{className:"g7dt-mt-1",children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"상태 변경:"}),i.stateChanges.slice(0,3).map((m,k)=>e.jsx("div",{className:"g7dt-text-xs g7dt-text-yellow g7dt-ml-2",children:m.path},k)),i.stateChanges.length>3&&e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-2",children:["...외 ",i.stateChanges.length-3,"개"]})]})]},u))})]})]}),g==="actions"&&e.jsx("div",{className:"g7dt-section",children:e.jsx("div",{className:"g7dt-scroll-list g7dt-space-y-2-flat g7dt-action-list",children:c.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"액션 이력이 없습니다"}):c.slice(-30).reverse().map((i,u)=>e.jsxs("div",{className:`g7dt-action-item ${h(i.status)}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-start",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-font-mono g7dt-font-bold",children:i.type}),e.jsx("span",{className:`g7dt-status-badge ${y(i.status)}`,children:i.status})]}),e.jsx("span",{className:"g7dt-text-gray",children:i.duration?`${i.duration.toFixed(2)}ms`:"-"})]}),i.error&&e.jsx("div",{className:"g7dt-error-box",children:i.error.message}),i.params&&Object.keys(i.params).length>0&&e.jsxs("details",{className:"g7dt-details",children:[e.jsx("summary",{className:"g7dt-summary",children:"파라미터"}),e.jsx("pre",{className:"g7dt-pre",children:JSON.stringify(i.params,null,2)})]})]},`${i.id}-${u}`))})})]})},Se=({stats:d,devTools:t})=>{const[s,a]=x.useState(null),[g,r]=x.useState(!1),o=x.useCallback(()=>{a(t.getCacheDecisionTrackingInfo())},[t]);x.useEffect(()=>{if(g){o();const l=setInterval(o,2e3);return()=>clearInterval(l)}},[g,o]);const p=()=>{t.resetCacheStats(),console.log("[G7DevTools] 캐시 통계 초기화")},v=l=>{switch(l){case"use_cache":return"✅";case"skip_cache":return"⏭️";case"invalidate":return"🗑️";default:return"❓"}};return e.jsxs("div",{className:"g7dt-space-y-6",children:[e.jsxs("div",{className:"g7dt-stats-grid",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-green",children:d.hits}),e.jsx("div",{className:"g7dt-stat-label",children:"Cache Hits"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-red",children:d.misses}),e.jsx("div",{className:"g7dt-stat-label",children:"Cache Misses"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:d.entries}),e.jsx("div",{className:"g7dt-stat-label",children:"Entries"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsxs("div",{className:"g7dt-stat-value g7dt-text-purple",children:[(d.hitRate*100).toFixed(1),"%"]}),e.jsx("div",{className:"g7dt-stat-label",children:"Hit Rate"})]})]}),e.jsxs("div",{className:"g7dt-space-y-2",children:[e.jsx("div",{className:"g7dt-text-sm g7dt-text-gray",children:"캐시 히트율"}),e.jsx("div",{className:"g7dt-progress-bar",children:e.jsx("div",{className:"g7dt-progress-fill",style:{width:`${d.hitRate*100}%`}})})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsx("button",{onClick:p,className:"g7dt-btn-danger",children:"캐시 통계 초기화"}),e.jsx("button",{onClick:()=>r(!g),className:`g7dt-tab-button ${g?"g7dt-tab-active":""}`,children:"📋 Decision Log"})]}),g&&s&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:["캐시 결정 로그 (",s.decisions.length,"개)"]}),s.decisions.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"캐시 결정 기록이 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:s.decisions.slice().reverse().slice(0,50).map(l=>e.jsxs("div",{className:`g7dt-list-item ${l.decision==="skip_cache"?"g7dt-border-yellow":l.decision==="invalidate"?"g7dt-border-red":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:v(l.decision)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white g7dt-font-mono g7dt-truncate",style:{maxWidth:"200px"},children:l.expression}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:l.decision})]}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1",children:l.reason}),l.context&&e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-mt-1",children:[l.context.isInIteration&&e.jsx("span",{className:"g7dt-badge g7dt-text-xs",children:"🔄 iteration"}),l.context.isInAction&&e.jsx("span",{className:"g7dt-badge g7dt-text-xs",children:"⚡ action"})]})]},l.id))})]})]})},Ie=()=>{const[d,t]=x.useState([]),[s,a]=x.useState([]),[g,r]=x.useState(""),o=["이전 값이 전송됨","모든 행이 같은 값","상태가 업데이트 안됨","API 호출이 안됨","초기값이 없음","무한 로딩","Form 값이 저장 안됨","iteration 데이터 중복"],p=f=>{t(c=>c.includes(f)?c.filter(h=>h!==f):[...c,f])},v=()=>{g&&!d.includes(g)&&(t(f=>[...f,g]),r(""))},l=async()=>{const{DiagnosticEngine:f}=await Promise.resolve().then(()=>pe),h=new f().analyze(d);a(h)};return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"증상 선택"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:o.map(f=>e.jsx("button",{onClick:()=>p(f),className:d.includes(f)?"g7dt-symptom-btn g7dt-symptom-active":"g7dt-symptom-btn",children:f},f))})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsx("input",{type:"text",placeholder:"직접 증상 입력...",value:g,onChange:f=>r(f.target.value),onKeyDown:f=>f.key==="Enter"&&v(),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:v,className:"g7dt-btn-secondary",children:"추가"})]}),e.jsxs("button",{onClick:l,disabled:d.length===0,className:d.length===0?"g7dt-btn-primary g7dt-btn-disabled g7dt-w-full":"g7dt-btn-primary g7dt-w-full",children:["진단 실행 (",d.length,"개 증상)"]}),s.length>0&&e.jsxs("div",{className:"g7dt-space-y-3",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:"진단 결과"}),s.slice(0,5).map((f,c)=>e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("span",{className:"g7dt-font-bold g7dt-text-yellow",children:f.rule.name}),e.jsxs("span",{className:"g7dt-confidence-badge",children:[(f.confidence*100).toFixed(0),"%"]})]}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-lightgray",children:f.rule.solution}),f.rule.codeExample&&e.jsx("pre",{className:"g7dt-pre g7dt-mt-2",children:f.rule.codeExample})]},c))]})]})},Te=({devTools:d})=>{const t=d.getPerformanceInfo();return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-stats-grid-2",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:t.bindingEvalCount}),e.jsx("div",{className:"g7dt-stat-label",children:"바인딩 평가 횟수"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-purple",children:t.renderCounts.size}),e.jsx("div",{className:"g7dt-stat-label",children:"렌더링된 컴포넌트"})]})]}),t.memoryWarnings.length>0&&e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-red g7dt-mb-2",children:"메모리 경고"}),e.jsx("div",{className:"g7dt-space-y-2",children:t.memoryWarnings.map((s,a)=>e.jsxs("div",{className:"g7dt-warning-card",children:[e.jsx("div",{className:"g7dt-font-bold",children:s.type}),e.jsx("div",{className:"g7dt-text-lightgray",children:s.message}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-gray g7dt-mt-1",children:s.suggestion})]},a))})]}),t.renderCounts.size>0&&e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"컴포넌트별 렌더링"}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-scroll-list",children:Array.from(t.renderCounts.entries()).sort((s,a)=>a[1]-s[1]).slice(0,10).map(([s,a])=>e.jsxs("div",{className:"g7dt-list-item",children:[e.jsx("span",{className:"g7dt-text-lightgray g7dt-truncate",children:s}),e.jsxs("span",{className:a>50?"g7dt-text-red":"g7dt-text-gray",children:[a,"회"]})]},s))})]})]})},De=({devTools:d})=>{const t=d.getLifecycleInfo();return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-section",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-green g7dt-mb-2 g7dt-flex-shrink-0",children:["마운트된 컴포넌트 (",t.mountedComponents.length,")"]}),e.jsx("div",{className:"g7dt-scroll-list",children:t.mountedComponents.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"추적된 컴포넌트 없음"}):t.mountedComponents.map(s=>e.jsxs("div",{className:"g7dt-list-item g7dt-text-xs",children:[e.jsx("span",{className:"g7dt-text-lightgray",children:s.name}),e.jsx("span",{className:"g7dt-text-gray",children:s.id})]},s.id))})]}),t.orphanedListeners.length>0&&e.jsxs("div",{className:"g7dt-section",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-red g7dt-mb-2 g7dt-flex-shrink-0",children:["정리되지 않은 리스너 (",t.orphanedListeners.length,")"]}),e.jsx("div",{className:"g7dt-scroll-list",children:t.orphanedListeners.map((s,a)=>e.jsxs("div",{className:"g7dt-warning-card g7dt-text-xs",children:[e.jsx("span",{className:"g7dt-text-lightgray",children:s.type}),e.jsxs("span",{className:"g7dt-text-gray g7dt-ml-2",children:["on ",s.target]})]},a))})]})]})},Le=({devTools:d})=>{const t=d.getNetworkInfo(),s=o=>{switch(o){case"success":return"g7dt-text-green";case"error":return"g7dt-text-red";case"pending":return"g7dt-text-yellow";default:return"g7dt-text-gray"}},a=o=>o.queryParams&&Object.keys(o.queryParams).length>0,g=o=>Object.entries(o).map(([p,v])=>e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-text-xs",children:[e.jsxs("span",{className:"g7dt-text-purple",children:[p,":"]}),e.jsx("span",{className:"g7dt-text-lightgray",children:Array.isArray(v)?JSON.stringify(v):v})]},p)),r=(o,p=5e3)=>{try{const v=JSON.stringify(o,null,2);return v.length>p?v.substring(0,p)+` ... (truncated)`:v}catch{return"(직렬화 불가)"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[t.activeRequests.length>0&&e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-yellow g7dt-mb-2",children:["진행 중 (",t.activeRequests.length,")"]}),e.jsx("div",{className:"g7dt-space-y-1",children:t.activeRequests.map(o=>e.jsxs("details",{className:"g7dt-details",children:[e.jsxs("summary",{className:"g7dt-summary g7dt-pending-request",children:[e.jsx("span",{className:"g7dt-font-bold",children:o.method}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-lightgray g7dt-truncate",children:o.fullUrl||o.url})]}),e.jsxs("div",{className:"g7dt-card g7dt-mt-1 g7dt-text-xs",children:[a(o)&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-blue g7dt-font-bold g7dt-mb-1",children:"쿼리 파라미터"}),e.jsx("div",{className:"g7dt-pl-2 g7dt-border-l-2 g7dt-border-blue",children:g(o.queryParams)})]}),o.requestBody&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-orange g7dt-font-bold g7dt-mb-1",children:"요청 본문"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs",children:r(o.requestBody,2e3)})]})]})]},o.id))})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"요청 이력"}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-scroll-list-lg",children:t.requestHistory.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"요청 이력 없음"}):t.requestHistory.slice(-20).reverse().map(o=>e.jsxs("details",{className:"g7dt-details",children:[e.jsxs("summary",{className:"g7dt-summary g7dt-card g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-w-full",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-font-bold",children:o.method}),e.jsx("span",{className:`g7dt-ml-2 ${s(o.status)}`,children:o.statusCode||o.status})]}),e.jsxs("span",{className:"g7dt-text-gray",children:[o.duration,"ms"]})]}),e.jsx("div",{className:"g7dt-text-gray g7dt-truncate g7dt-mt-1",children:o.fullUrl||o.url})]}),e.jsxs("div",{className:"g7dt-card g7dt-mt-1 g7dt-text-xs g7dt-ml-2 g7dt-border-l-2 g7dt-border-gray",children:[e.jsx("div",{className:"g7dt-mb-2",children:e.jsxs("div",{className:"g7dt-text-xs g7dt-space-y-1",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"URL:"})," ",e.jsx("span",{className:"g7dt-text-lightgray",children:o.fullUrl||o.url})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"상태:"})," ",e.jsx("span",{className:s(o.status),children:o.statusCode||o.status})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"소요시간:"})," ",e.jsxs("span",{className:"g7dt-text-lightgray",children:[o.duration,"ms"]})]}),o.dataSourceId&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"데이터소스:"})," ",e.jsx("span",{className:"g7dt-text-blue",children:o.dataSourceId})]}),o.error&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"에러:"})," ",e.jsx("span",{className:"g7dt-text-red",children:o.error})]})]})}),a(o)&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-blue g7dt-font-bold g7dt-mb-1",children:"쿼리 파라미터"}),e.jsx("div",{className:"g7dt-pl-2 g7dt-border-l-2 g7dt-border-blue",children:g(o.queryParams)})]}),o.requestBody&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("div",{className:"g7dt-text-orange g7dt-font-bold g7dt-mb-1",children:"요청 본문"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs g7dt-max-h-40 g7dt-overflow-auto",children:r(o.requestBody,2e3)})]}),o.response!==void 0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-green g7dt-font-bold g7dt-mb-1",children:"응답 내용"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs g7dt-max-h-60 g7dt-overflow-auto",children:r(o.response,5e3)})]})]})]},o.id))})]}),t.pendingDataSources.length>0&&e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-blue g7dt-mb-2",children:["대기 중인 데이터소스 (",t.pendingDataSources.length,")"]}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:t.pendingDataSources.map(o=>e.jsx("span",{className:"g7dt-tag-blue",children:o},o))})]})]})},Ee=({devTools:d})=>{const t=d.getFormInfo(),[s,a]=x.useState(!1),[g,r]=x.useState(null),o=x.useCallback(()=>{r(d.getFormBindingValidationTrackingInfo())},[d]);x.useEffect(()=>{if(s){o();const l=setInterval(o,2e3);return()=>clearInterval(l)}},[s,o]);const p=l=>{switch(l){case"missing-datakey":return"🔑";case"missing-input-name":return"📝";case"context-not-propagated":return"🔗";case"sortable-context-break":return"📊";case"modal-context-isolation":return"🪟";case"duplicate-input-name":return"👯";case"value-type-mismatch":return"⚠️";case"binding-path-invalid":return"❌";default:return"❓"}},v=l=>{switch(l){case"error":return"g7dt-text-red";case"warning":return"g7dt-text-yellow";case"info":return"g7dt-text-blue";default:return"g7dt-text-gray"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[t.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-8",children:"추적된 Form이 없습니다"}):t.map(l=>e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("span",{className:"g7dt-font-bold g7dt-text-blue",children:l.dataKey||"(no dataKey)"}),!l.dataKey&&e.jsx("span",{className:"g7dt-text-red g7dt-text-xs",children:"dataKey 필요"})]}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-mb-2",children:l.inputs.map((f,c)=>e.jsxs("div",{className:"g7dt-text-xs g7dt-flex g7dt-justify-between",children:[e.jsx("span",{children:f.name||e.jsx("span",{className:"g7dt-text-red",children:"name 없음"})}),e.jsx("span",{className:"g7dt-text-gray",children:f.type})]},c))}),e.jsxs("details",{className:"g7dt-details",children:[e.jsx("summary",{className:"g7dt-summary",children:"현재 값 보기"}),e.jsx("pre",{className:"g7dt-pre",children:JSON.stringify(l,null,2)})]})]},l.id)),e.jsx("div",{className:"g7dt-flex g7dt-justify-end",children:e.jsx("button",{onClick:()=>a(!s),className:`g7dt-tab-button ${s?"g7dt-tab-active":""}`,children:"🔍 Auto-Binding Check"})}),s&&g&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Form 바인딩 검증 결과"}),e.jsxs("div",{className:"g7dt-stats-grid g7dt-mb-3",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:g.stats.totalFormsValidated}),e.jsx("div",{className:"g7dt-stat-label",children:"검증된 Form"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-green",children:g.stats.validForms}),e.jsx("div",{className:"g7dt-stat-label",children:"유효"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-red",children:g.stats.formsWithIssues}),e.jsx("div",{className:"g7dt-stat-label",children:"문제 있음"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-yellow",children:g.stats.totalIssues}),e.jsx("div",{className:"g7dt-stat-label",children:"총 이슈"})]})]}),g.stats.topIssues.length>0&&e.jsxs("div",{className:"g7dt-mb-3",children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"자주 발생하는 이슈"}),e.jsx("div",{className:"g7dt-space-y-1",children:g.stats.topIssues.slice(0,3).map((l,f)=>e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-xs",children:[e.jsx("span",{children:p(l.type)}),e.jsx("span",{className:"g7dt-text-white",children:l.description}),e.jsxs("span",{className:"g7dt-text-muted",children:["(",l.count,")"]})]},f))})]}),g.issues.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"바인딩 이슈가 없습니다 ✅"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:g.issues.slice().reverse().slice(0,30).map(l=>e.jsxs("div",{className:`g7dt-list-item ${l.severity==="error"?"g7dt-border-red":l.severity==="warning"?"g7dt-border-yellow":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:p(l.type)}),e.jsx("span",{className:`g7dt-text-xs g7dt-font-bold ${v(l.severity)}`,children:l.type}),l.formDataKey&&e.jsx("span",{className:"g7dt-text-xs g7dt-text-blue g7dt-font-mono",children:l.formDataKey})]}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-white g7dt-mt-1",children:l.description}),e.jsxs("div",{className:"g7dt-text-xs g7dt-text-green g7dt-mt-1",children:["💡 ",l.suggestion]}),l.contextInfo&&e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-mt-1",children:[l.contextInfo.isInsideModal&&e.jsx("span",{className:"g7dt-badge g7dt-text-xs",children:"🪟 modal"}),l.contextInfo.isInsideSortable&&e.jsx("span",{className:"g7dt-badge g7dt-text-xs",children:"📊 sortable"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted",children:["depth: ",l.contextInfo.depth]})]})]},l.id))})]})]})},$e=({devTools:d})=>{const t=d.getConditionalInfo();return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-purple g7dt-mb-2",children:["if 조건 (",t.ifConditions.length,")"]}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-scroll-list",children:t.ifConditions.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"추적된 if 조건 없음"}):t.ifConditions.map(s=>e.jsx("div",{className:`g7dt-condition-item ${s.evaluatedValue?"g7dt-condition-true":"g7dt-condition-false"}`,children:e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-lightgray g7dt-truncate-60",children:s.expression}),e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:s.evaluatedValue?"g7dt-badge-success":"g7dt-badge-error",children:s.evaluatedValue?"true":"false"}),e.jsxs("span",{className:"g7dt-text-gray",children:[s.evaluationCount,"회"]})]})]})},s.id))})]}),e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-cyan g7dt-mb-2",children:["iteration (",t.iterations.length,")"]}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-scroll-list",children:t.iterations.length===0?e.jsx("div",{className:"g7dt-empty-message",children:"추적된 iteration 없음"}):t.iterations.map(s=>e.jsxs("div",{className:"g7dt-card g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-1",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-lightgray g7dt-truncate-60",children:s.source}),e.jsxs("span",{className:s.sourceLength>0?"g7dt-badge-blue":"g7dt-badge-warning",children:[s.sourceLength,"개 항목"]})]}),e.jsxs("div",{className:"g7dt-text-gray",children:[e.jsxs("span",{className:"g7dt-mr-2",children:["item_var: ",s.itemVar]}),s.indexVar&&e.jsxs("span",{children:["index_var: ",s.indexVar]})]})]},s.id))})]}),(t.ifConditions.some(s=>s.evaluatedValue===!1&&s.evaluationCount>5)||t.iterations.some(s=>s.sourceLength===0))&&e.jsxs("div",{className:"g7dt-warning-box",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-yellow g7dt-mb-2",children:"잠재적 문제"}),e.jsxs("div",{className:"g7dt-space-y-1 g7dt-text-xs",children:[t.ifConditions.filter(s=>s.evaluatedValue===!1&&s.evaluationCount>5).map(s=>e.jsxs("div",{className:"g7dt-text-lightgray",children:["⚠️ if 조건 항상 false: ",s.expression]},s.id)),t.iterations.filter(s=>s.sourceLength===0).map(s=>e.jsxs("div",{className:"g7dt-text-lightgray",children:["⚠️ iteration 소스 비어있음: ",s.source]},s.id))]})]})]})},ze=({devTools:d})=>{const[t,s]=x.useState([]),[a,g]=x.useState({totalEvaluations:0,uniqueExpressions:0,warningCount:0,cacheHitRate:0,averageDuration:0,byType:{},byWarning:{}}),[r,o]=x.useState(""),[p,v]=x.useState(!1),[l,f]=x.useState(null),c=x.useCallback(()=>{const m=d.getExpressionInfo();s(m.expressions),g(m.stats)},[d]);x.useEffect(()=>{c();const m=d.watchExpressions(()=>{c()}),k=setInterval(c,2e3);return()=>{m(),clearInterval(k)}},[d,c]);const h=t.filter(m=>{if(p&&!m.warning)return!1;if(r){const k=r.toLowerCase();return m.expression.toLowerCase().includes(k)||m.componentName?.toLowerCase().includes(k)||m.propName?.toLowerCase().includes(k)||m.resultType.toLowerCase().includes(k)}return!0}),y=m=>{switch(m){case"string":return"g7dt-text-green";case"number":return"g7dt-text-blue";case"boolean":return"g7dt-text-purple";case"array":return"g7dt-text-cyan";case"object":return"g7dt-text-yellow";case"undefined":case"null":return"g7dt-text-red";default:return"g7dt-text-gray"}},n=m=>{switch(m){case"undefined-result":return"undefined 결과";case"null-result":return"null 결과";case"array-to-string":return"배열→문자열";case"object-to-string":return"객체→문자열";case"missing-optional-chain":return"?. 누락 의심";case"wrong-iteration-var":return"잘못된 반복 변수";case"type-mismatch":return"타입 불일치";case"cache-stale":return"캐시 오래됨";case"slow-evaluation":return"느린 평가";default:return m}},i=(m,k)=>k==="undefined"?"undefined":k==="null"?"null":k==="string"?`"${String(m).substring(0,50)}${String(m).length>50?"...":""}"`:k==="array"?`Array(${Array.isArray(m)?m.length:"?"})`:k==="object"?"Object{...}":String(m),u=()=>{d.clearExpressions(),s([]),f(null)};return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-stats-grid-5",children:[e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-blue",children:a.totalEvaluations}),e.jsx("div",{className:"g7dt-stat-label-xs",children:"총 평가"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-green",children:a.uniqueExpressions}),e.jsx("div",{className:"g7dt-stat-label-xs",children:"고유 표현식"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-yellow",children:a.warningCount}),e.jsx("div",{className:"g7dt-stat-label-xs",children:"경고"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsxs("div",{className:"g7dt-stat-value-lg g7dt-text-purple",children:[(a.cacheHitRate*100).toFixed(0),"%"]}),e.jsx("div",{className:"g7dt-stat-label-xs",children:"캐시 히트"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsxs("div",{className:"g7dt-stat-value-lg g7dt-text-cyan",children:[a.averageDuration.toFixed(1),"ms"]}),e.jsx("div",{className:"g7dt-stat-label-xs",children:"평균 시간"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-items-center",children:[e.jsx("input",{type:"text",placeholder:"표현식, 컴포넌트, prop 검색...",value:r,onChange:m=>o(m.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsxs("label",{className:"g7dt-checkbox-label",children:[e.jsx("input",{type:"checkbox",checked:p,onChange:m=>v(m.target.checked),className:"g7dt-checkbox"}),"경고만"]}),e.jsx("button",{onClick:u,className:"g7dt-btn-danger-sm",children:"초기화"})]}),Object.keys(a.byType).length>0&&e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1",children:Object.entries(a.byType).map(([m,k])=>e.jsxs("span",{className:`g7dt-type-tag ${y(m)}`,children:[m,": ",k]},m))}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-expr-list",children:h.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:r||p?"조건에 맞는 표현식이 없습니다":"추적된 표현식이 없습니다"}):h.slice(-50).reverse().map(m=>e.jsx("div",{onClick:()=>f(m),className:`g7dt-expr-item ${l?.id===m.id?"g7dt-expr-selected":m.warning?"g7dt-expr-warning":""}`,children:e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-start g7dt-gap-2",children:[e.jsxs("div",{className:"g7dt-flex-1 g7dt-min-w-0",children:[e.jsx("div",{className:"g7dt-font-mono g7dt-text-white g7dt-truncate",title:m.expression,children:m.expression}),e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-mt-1 g7dt-text-xxs",children:[m.componentName&&e.jsxs("span",{className:"g7dt-text-gray",children:[m.componentName,m.propName&&`.${m.propName}`]}),e.jsx("span",{className:y(m.resultType),children:i(m.result,m.resultType)})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-flex-col g7dt-items-end g7dt-gap-1",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-1",children:[m.fromCache&&e.jsx("span",{className:"g7dt-mini-badge g7dt-badge-success",children:"캐시"}),m.warning&&e.jsx("span",{className:"g7dt-mini-badge g7dt-badge-warning",children:n(m.warning.type)})]}),m.duration!==void 0&&e.jsxs("span",{className:"g7dt-text-xxs g7dt-text-gray",children:[m.duration.toFixed(2),"ms"]})]})]})},m.id))}),l&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:"표현식 상세"}),e.jsx("button",{onClick:()=>f(null),className:"g7dt-btn-close",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"표현식:"}),e.jsx("code",{className:"g7dt-code g7dt-text-green",children:l.expression})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"결과:"}),e.jsx("span",{className:`g7dt-ml-2 ${y(l.resultType)}`,children:JSON.stringify(l.result,null,2)?.substring(0,200)}),e.jsxs("span",{className:"g7dt-ml-2 g7dt-text-darkgray",children:["(",l.resultType,")"]})]}),l.componentName&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"컴포넌트:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-blue",children:l.componentName}),l.propName&&e.jsxs("span",{className:"g7dt-text-gray",children:[".",l.propName]})]}),l.method&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"메서드:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-purple",children:l.method})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"캐시:"}),e.jsxs("span",{className:`g7dt-ml-2 ${l.fromCache?"g7dt-text-green":"g7dt-text-gray"}`,children:[l.fromCache?"캐시에서 로드":"새로 평가",l.skipCache&&" (skipCache)"]})]}),l.duration!==void 0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"소요 시간:"}),e.jsxs("span",{className:`g7dt-ml-2 ${l.duration>10?"g7dt-text-red":"g7dt-text-lightgray"}`,children:[l.duration.toFixed(3),"ms"]})]}),l.warning&&e.jsxs("div",{className:"g7dt-warning-box-light",children:[e.jsxs("div",{className:"g7dt-text-yellow g7dt-font-bold",children:["⚠️ ",n(l.warning.type)]}),e.jsx("div",{className:"g7dt-text-lightgray g7dt-mt-1",children:l.warning.message}),l.warning.suggestion&&e.jsxs("div",{className:"g7dt-text-gray g7dt-mt-1 g7dt-text-xxs",children:["💡 ",l.warning.suggestion]})]}),l.steps&&l.steps.length>0&&e.jsxs("details",{className:"g7dt-details",children:[e.jsxs("summary",{className:"g7dt-summary",children:["평가 단계 (",l.steps.length,")"]}),e.jsx("div",{className:"g7dt-mt-1 g7dt-space-y-1 g7dt-pl-2 g7dt-border-l",children:l.steps.map((m,k)=>e.jsxs("div",{className:"g7dt-text-xxs",children:[e.jsx("span",{className:"g7dt-text-purple",children:m.step}),m.path&&e.jsx("span",{className:"g7dt-text-gray g7dt-ml-1",children:m.path}),m.fromCache&&e.jsx("span",{className:"g7dt-text-green g7dt-ml-1",children:"(cached)"}),m.error&&e.jsx("span",{className:"g7dt-text-red g7dt-ml-1",children:m.error})]},k))})]})]})]}),Object.keys(a.byWarning).length>0&&e.jsxs("div",{className:"g7dt-warning-box-light",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-text-yellow g7dt-mb-2",children:"경고 요약"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:Object.entries(a.byWarning).map(([m,k])=>e.jsxs("span",{className:"g7dt-warning-tag",children:[n(m),": ",k]},m))})]})]})},_e=({devTools:d})=>{const t=d.getHandlers(),[s,a]=x.useState(""),[g,r]=x.useState("all"),o={"built-in":t.filter(l=>l.category==="built-in"),custom:t.filter(l=>l.category==="custom"),module:t.filter(l=>l.category==="module"),plugin:t.filter(l=>l.category==="plugin")},p={all:t.length,"built-in":o["built-in"].length,custom:o.custom.length,module:o.module.length,plugin:o.plugin.length},v=t.filter(l=>g!=="all"&&l.category!==g?!1:s?l.name.toLowerCase().includes(s.toLowerCase())||l.description?.toLowerCase().includes(s.toLowerCase()):!0);return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsx("div",{className:"g7dt-flex g7dt-gap-2",children:["all","built-in","custom","module","plugin"].map(l=>e.jsxs("button",{onClick:()=>r(l),className:g===l?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:[l==="all"?"전체":l," (",p[l],")"]},l))}),e.jsx("input",{type:"text",placeholder:"핸들러 이름 검색...",value:s,onChange:l=>a(l.target.value),className:"g7dt-input g7dt-w-full"}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-handler-list",children:v.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:"등록된 핸들러가 없습니다"}):v.map(l=>e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-blue",children:l.name}),e.jsx("span",{className:l.category==="built-in"?"g7dt-mini-badge g7dt-badge-success":l.category==="module"?"g7dt-mini-badge g7dt-badge-purple":l.category==="plugin"?"g7dt-mini-badge g7dt-badge-info":"g7dt-mini-badge g7dt-badge-warning",children:l.category})]}),l.description&&e.jsx("div",{className:"g7dt-text-xs g7dt-text-gray g7dt-mt-1",children:l.description})]},l.name))}),e.jsxs("div",{className:"g7dt-text-xs g7dt-text-gray",children:["총 ",t.length,"개 핸들러 | 빌트인 ",p["built-in"]," | 커스텀 ",p.custom," | 모듈 ",p.module," | 플러그인 ",p.plugin]})]})},je=({devTools:d})=>{const[t,s]=x.useState(d.getComponentEventInfo()),[a,g]=x.useState(null),r=x.useCallback(()=>{s(d.getComponentEventInfo())},[d]);x.useEffect(()=>{r();const p=setInterval(r,2e3);return()=>clearInterval(p)},[r]);const o=a?t.emitHistory.filter(p=>p.eventName===a):t.emitHistory;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-stats-grid-3",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-blue",children:t.subscriptions.length}),e.jsx("div",{className:"g7dt-stat-label",children:"이벤트 타입"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-green",children:t.totalSubscribers}),e.jsx("div",{className:"g7dt-stat-label",children:"총 구독자"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value-lg g7dt-text-purple",children:t.totalEmits}),e.jsx("div",{className:"g7dt-stat-label",children:"총 발행"})]})]}),e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"구독된 이벤트"}),e.jsxs("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:[e.jsx("button",{onClick:()=>g(null),className:a===null?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:"전체"}),t.subscriptions.map(p=>e.jsxs("button",{onClick:()=>g(p.eventName),className:a===p.eventName?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:[p.eventName," (",p.subscriberCount,")"]},p.eventName))]})]}),e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:["발행 이력 ",a&&`(${a})`]}),e.jsx("div",{className:"g7dt-space-y-1 g7dt-event-list",children:o.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:"이벤트 발행 이력이 없습니다"}):o.slice(-30).reverse().map(p=>e.jsxs("div",{className:`g7dt-card g7dt-text-xs ${p.hasError?"g7dt-card-error":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-green",children:p.eventName}),e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsxs("span",{className:"g7dt-text-gray",children:[p.listenerCount," listeners"]}),p.hasError&&e.jsx("span",{className:"g7dt-text-red g7dt-text-xxs",children:"에러"})]})]}),p.data!==void 0&&e.jsxs("details",{className:"g7dt-details g7dt-mt-1",children:[e.jsx("summary",{className:"g7dt-summary",children:"데이터"}),e.jsx("pre",{className:"g7dt-pre",children:JSON.stringify(p.data,null,2)})]}),p.hasError&&p.errorMessage&&e.jsx("div",{className:"g7dt-error-inline",children:p.errorMessage})]},p.id))})]}),e.jsx("button",{onClick:r,className:"g7dt-btn-secondary",children:"새로고침"})]})},Ae=({devTools:d})=>{const t=d.getDataSources(),[s,a]=x.useState("all"),[g,r]=x.useState(null),[o,p]=x.useState(!1),[v,l]=x.useState(null),f=x.useCallback(()=>{l(d.getDataPathTransformTrackingInfo())},[d]);x.useEffect(()=>{if(o){f();const u=setInterval(f,2e3);return()=>clearInterval(u)}},[o,f]);const c=u=>{switch(u){case"api_response":return"📥";case"extract_data":return"📄";case"apply_path":return"🔗";case"init_global":return"🌐";case"init_local":return"📌";default:return"❓"}},h=t.filter(u=>s==="all"?!0:u.status===s),y={all:t.length,idle:t.filter(u=>u.status==="idle").length,loading:t.filter(u=>u.status==="loading").length,loaded:t.filter(u=>u.status==="loaded").length,error:t.filter(u=>u.status==="error").length},n=u=>{switch(u){case"loaded":return"g7dt-badge-success";case"loading":return"g7dt-badge-warning";case"error":return"g7dt-badge-error";default:return"g7dt-badge-gray"}},i=g?t.find(u=>u.id===g):null;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsx("div",{className:"g7dt-flex g7dt-gap-2",children:["all","idle","loading","loaded","error"].map(u=>e.jsxs("button",{onClick:()=>a(u),className:s===u?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:[u==="all"?"전체":u," (",y[u],")"]},u))}),e.jsx("div",{className:"g7dt-ds-grid",children:h.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-col-span-2 g7dt-py-4",children:"데이터소스가 없습니다"}):h.map(u=>e.jsxs("div",{onClick:()=>r(u.id),className:`g7dt-ds-item ${g===u.id?"g7dt-ds-selected":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-blue g7dt-truncate",children:u.id}),e.jsx("span",{className:`g7dt-mini-badge ${n(u.status)}`,children:u.status})]}),e.jsx("div",{className:"g7dt-text-gray g7dt-mt-1 g7dt-truncate",children:u.endpoint||"(endpoint 없음)"}),u.itemCount!==void 0&&e.jsxs("div",{className:"g7dt-text-lightgray g7dt-mt-1",children:[u.itemCount,"개 항목"]})]},u.id))}),i&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:i.id}),e.jsx("button",{onClick:()=>r(null),className:"g7dt-btn-close",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"엔드포인트:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-green",children:i.endpoint||"-"})]}),i.dataPath&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"데이터 경로:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-yellow",children:i.dataPath})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"상태:"}),e.jsx("span",{className:`g7dt-ml-2 g7dt-mini-badge ${n(i.status)}`,children:i.status})]}),i.itemCount!==void 0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"항목 수:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-blue",children:i.itemCount})]}),i.keys&&i.keys.length>0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"키:"}),e.jsxs("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1",children:[i.keys.slice(0,10).map(u=>e.jsx("span",{className:"g7dt-key-tag",children:u},u)),i.keys.length>10&&e.jsxs("span",{className:"g7dt-text-gray",children:["+",i.keys.length-10,"개"]})]})]}),i.dependencies&&i.dependencies.length>0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-gray",children:"의존성:"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1",children:i.dependencies.map(u=>e.jsx("span",{className:"g7dt-dep-tag",children:u},u))})]})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("div",{className:"g7dt-text-xs g7dt-text-gray",children:["총 ",t.length,"개 데이터소스 | 로드됨 ",y.loaded," | 로딩중 ",y.loading," | 에러 ",y.error]}),e.jsx("button",{onClick:()=>p(!o),className:`g7dt-tab-button ${o?"g7dt-tab-active":""}`,children:"🔀 Path Transform"})]}),o&&v&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:["데이터 경로 변환 추적 (",v.transforms.length,"개)"]}),e.jsxs("div",{className:"g7dt-stats-grid-3 g7dt-mb-3",children:[e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-xl g7dt-font-bold g7dt-text-blue",children:v.stats.totalTransforms}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"총 변환"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-xl g7dt-font-bold g7dt-text-purple",children:Object.keys(v.stats.byDataSource).length}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"데이터소스"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-xl g7dt-font-bold g7dt-text-yellow",children:v.stats.warningsCount}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"경고"})]})]}),v.transforms.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"변환 기록이 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:v.transforms.slice().reverse().slice(0,20).map((u,m)=>e.jsxs("div",{className:`g7dt-list-item ${u.warnings.length>0?"g7dt-border-yellow":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-blue g7dt-font-mono",children:u.dataSourceId}),u.warnings.length>0&&e.jsxs("span",{className:"g7dt-badge g7dt-text-xs g7dt-text-yellow",children:["⚠️ ",u.warnings.length]})]}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1",children:u.transformSteps.map((k,b)=>e.jsxs("span",{className:"g7dt-badge g7dt-text-xs",title:`${k.inputPath} → ${k.outputPath}`,children:[c(k.step)," ",k.step]},b))}),u.finalBinding&&e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1",children:["최종: ",e.jsx("span",{className:"g7dt-text-green",children:u.finalBinding.expression}),"→ ",e.jsx("span",{className:"g7dt-text-blue",children:u.finalBinding.resolvedPath})]}),u.warnings.length>0&&e.jsx("div",{className:"g7dt-text-xs g7dt-text-yellow g7dt-mt-1",children:u.warnings[0]})]},`${u.dataSourceId}-${m}`))}),v.stats.commonWarnings.length>0&&e.jsxs("div",{className:"g7dt-mt-3",children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-1",children:"자주 발생하는 경고"}),e.jsx("div",{className:"g7dt-space-y-1",children:v.stats.commonWarnings.slice(0,5).map((u,m)=>e.jsxs("div",{className:"g7dt-text-xs g7dt-text-yellow",children:[u.warning," (",u.count,"회)"]},m))})]})]})]})},Oe=({devTools:d})=>{const[t,s]=x.useState(d.getStateRenderingInfo()),[a,g]=x.useState(null),[r,o]=x.useState(!1),[p,v]=x.useState(""),l=x.useCallback(()=>{s(d.getStateRenderingInfo())},[d]);x.useEffect(()=>{l();const n=setInterval(l,2e3);return()=>clearInterval(n)},[l]);const f=t.logs.filter(n=>!(r&&n.renderedComponents.length>0||p&&!n.statePath.toLowerCase().includes(p.toLowerCase()))),c=a?t.logs.find(n=>n.setStateId===a):null,h=t.stats,y=t.logs.filter(n=>n.renderedComponents.length===0).length;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-stats-grid",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:h.totalStateChanges}),e.jsx("div",{className:"g7dt-stat-label",children:"상태 변경"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-green",children:h.totalRenders}),e.jsx("div",{className:"g7dt-stat-label",children:"총 렌더링"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsxs("div",{className:"g7dt-stat-value g7dt-text-purple",children:[h.avgRenderDuration.toFixed(1),"ms"]}),e.jsx("div",{className:"g7dt-stat-label",children:"평균 렌더 시간"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:`g7dt-stat-value ${y>0?"g7dt-text-yellow":"g7dt-text-green"}`,children:y}),e.jsx("div",{className:"g7dt-stat-label",children:"렌더링 없음"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsx("input",{type:"text",placeholder:"상태 경로 필터 (예: _global.user)",value:p,onChange:n=>v(n.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:()=>o(!r),className:r?"g7dt-filter-btn-active-yellow":"g7dt-filter-btn",children:"렌더링 없음만"})]}),e.jsx("div",{className:"g7dt-scroll-list",children:f.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"상태-렌더링 로그가 없습니다"}):f.slice(-30).reverse().map(n=>{const i=n.renderedComponents.length>0,u=i?"✅":"⚠️",m=new Date(n.timestamp).toLocaleTimeString();return e.jsxs("div",{onClick:()=>g(n.setStateId),className:`g7dt-render-log-item ${a===n.setStateId?"g7dt-selected":i?"":"g7dt-warning"}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("span",{className:"g7dt-mono g7dt-text-blue",children:[u," ",n.statePath]}),e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[i?e.jsxs("span",{className:"g7dt-text-green",children:[n.renderedComponents.length,"개 렌더"]}):e.jsx("span",{className:"g7dt-text-yellow",children:"렌더링 없음"}),e.jsx("span",{className:"g7dt-text-muted",children:m})]})]}),n.triggeredBy?.handlerType&&e.jsxs("div",{className:"g7dt-text-muted g7dt-mt-1",children:["트리거: ",n.triggeredBy.handlerType]})]},n.setStateId)})}),c&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:c.statePath}),e.jsx("button",{onClick:()=>g(null),className:"g7dt-close-btn",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"setStateId:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-purple g7dt-mono",children:c.setStateId})]}),c.triggeredBy&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"트리거:"}),e.jsxs("span",{className:"g7dt-ml-2 g7dt-text-yellow",children:[c.triggeredBy.handlerType,c.triggeredBy.actionId&&` (${c.triggeredBy.actionId})`]})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"이전 값:"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-red",children:JSON.stringify(c.oldValue,null,2)?.slice(0,200)})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"새 값:"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-green",children:JSON.stringify(c.newValue,null,2)?.slice(0,200)})]}),c.renderedComponents.length>0?e.jsxs("div",{children:[e.jsxs("span",{className:"g7dt-text-muted",children:["렌더링된 컴포넌트 (",c.renderedComponents.length,"개):"]}),e.jsxs("div",{className:"g7dt-mt-1 g7dt-space-y-1",children:[c.renderedComponents.slice(0,10).map((n,i)=>e.jsxs("div",{className:"g7dt-render-comp-item",children:[e.jsx("span",{className:"g7dt-text-blue",children:n.componentName}),e.jsxs("span",{className:"g7dt-text-muted",children:[n.renderDuration.toFixed(2),"ms"]})]},i)),c.renderedComponents.length>10&&e.jsxs("div",{className:"g7dt-text-muted g7dt-text-center",children:["... 외 ",c.renderedComponents.length-10,"개"]})]}),e.jsxs("div",{className:"g7dt-mt-2 g7dt-text-muted",children:["총 렌더 시간: ",c.totalRenderDuration.toFixed(2),"ms"]})]}):e.jsxs("div",{className:"g7dt-warning-box",children:[e.jsx("span",{className:"g7dt-text-yellow",children:"⚠️ 렌더링이 발생하지 않았습니다"}),e.jsxs("div",{className:"g7dt-text-muted g7dt-mt-1 g7dt-text-xxs",children:["가능한 원인:",e.jsxs("ul",{className:"g7dt-list g7dt-mt-1",children:[e.jsx("li",{children:"해당 상태를 바인딩하는 컴포넌트가 없음"}),e.jsx("li",{children:"컴포넌트가 마운트되지 않은 상태"}),e.jsx("li",{children:"동일한 값으로 상태 설정"})]})]})]})]})]}),h.topRenderedComponents.length>0&&e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"가장 많이 렌더링된 컴포넌트"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:h.topRenderedComponents.slice(0,5).map((n,i)=>e.jsxs("span",{className:"g7dt-tag",children:[n.name,": ",e.jsx("span",{className:"g7dt-text-blue",children:n.count})]},i))})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsx("button",{onClick:l,className:"g7dt-btn-secondary",children:"새로고침"}),e.jsx("button",{onClick:()=>{d.clearStateRenderingLogs(),l()},className:"g7dt-btn-danger",children:"로그 초기화"})]})]})},Me=({devTools:d})=>{const[t,s]=x.useState(null),[a,g]=x.useState(null),[r,o]=x.useState("all"),p=x.useCallback(()=>{s(d.getStyleValidationInfo())},[d]);if(x.useEffect(()=>{p();const c=setInterval(p,2e3);return()=>clearInterval(c)},[p]),!t)return e.jsx("div",{className:"g7dt-empty-state",children:"로딩 중..."});const v=t.issues.filter(c=>r==="all"?!0:c.type===r),l=a?t.issues.find(c=>c.componentId===a):null,f=t.stats;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-stats-grid",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value g7dt-text-blue",children:f.totalComponents}),e.jsx("div",{className:"g7dt-stat-label",children:"총 컴포넌트"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:`g7dt-stat-value ${f.invisibleCount>0?"g7dt-text-red":"g7dt-text-green"}`,children:f.invisibleCount}),e.jsx("div",{className:"g7dt-stat-label",children:"숨겨진 요소"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:`g7dt-stat-value ${f.tailwindIssueCount>0?"g7dt-text-yellow":"g7dt-text-green"}`,children:f.tailwindIssueCount}),e.jsx("div",{className:"g7dt-stat-label",children:"Tailwind 이슈"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:`g7dt-stat-value ${f.darkModeIssueCount>0?"g7dt-text-purple":"g7dt-text-green"}`,children:f.darkModeIssueCount}),e.jsx("div",{className:"g7dt-stat-label",children:"다크모드 이슈"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsxs("select",{value:r,onChange:c=>o(c.target.value),className:"g7dt-select",children:[e.jsx("option",{value:"all",children:"모든 이슈"}),e.jsx("option",{value:"invisible-element",children:"숨겨진 요소"}),e.jsx("option",{value:"tailwind-purging",children:"Tailwind 퍼징"}),e.jsx("option",{value:"dark-mode-missing",children:"다크모드 누락"})]}),e.jsx("button",{onClick:p,className:"g7dt-btn-secondary",children:"새로고침"})]}),e.jsx("div",{className:"g7dt-scroll-list",children:v.length===0?e.jsx("div",{className:"g7dt-empty-state",children:t.issues.length===0?"스타일 이슈가 없습니다 ✓":"필터에 맞는 이슈가 없습니다"}):v.map(c=>{const h=c.type==="invisible-element"?"👁️":c.type==="tailwind-purging"?"🎨":"🌙",y=c.severity==="error"?"g7dt-severity-error":c.severity==="warning"?"g7dt-severity-warning":"g7dt-severity-info";return e.jsxs("div",{onClick:()=>g(c.componentId),className:`g7dt-style-issue-item ${y} ${a===c.componentId?"g7dt-selected":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("span",{className:"g7dt-mono g7dt-text-blue",children:[h," ",c.componentName]}),e.jsx("span",{className:c.severity==="error"?"g7dt-badge-error":c.severity==="warning"?"g7dt-badge-warning":"g7dt-badge-info",children:c.type})]}),e.jsx("div",{className:"g7dt-text-muted g7dt-mt-1 g7dt-truncate",children:c.description})]},c.componentId)})}),l&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:l.componentName}),e.jsx("button",{onClick:()=>g(null),className:"g7dt-close-btn",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"이슈 타입:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-yellow",children:l.type})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"설명:"}),e.jsx("div",{className:"g7dt-mt-1 g7dt-text-white",children:l.description})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"CSS 속성:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-purple",children:l.property}),e.jsxs("span",{className:"g7dt-ml-2 g7dt-text-muted",children:["= ",l.currentValue]}),l.expectedValue&&e.jsxs("span",{className:"g7dt-ml-2 g7dt-text-green",children:["(예상: ",l.expectedValue,")"]})]}),l.suggestion&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"해결 방법:"}),e.jsx("div",{className:"g7dt-mt-1 g7dt-text-green",children:l.suggestion})]})]})]}),t.componentStyles.length>0&&e.jsxs("div",{children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:["컴포넌트 스타일 (",t.componentStyles.length,")"]}),e.jsx("div",{className:"g7dt-scroll-list-sm",children:t.componentStyles.slice(0,20).map((c,h)=>e.jsxs("div",{className:"g7dt-list-item",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-blue",children:c.componentName}),e.jsxs("span",{className:"g7dt-text-muted",children:[c.classes.length," classes"]})]}),c.classes.length>0&&e.jsxs("div",{className:"g7dt-text-muted g7dt-mt-1 g7dt-truncate",children:[c.classes.slice(0,5).join(" "),c.classes.length>5&&"..."]})]},h))})]})]})},Pe=({devTools:d})=>{const[t,s]=x.useState(null),[a,g]=x.useState(null),[r,o]=x.useState("all"),p=x.useCallback(()=>{s(d.getAuthDebugInfo())},[d]);if(x.useEffect(()=>{p();const h=setInterval(p,2e3);return()=>clearInterval(h)},[p]),!t)return e.jsx("div",{className:"g7dt-empty-state",children:"로딩 중..."});const v=t.events.filter(h=>r==="all"?!0:h.type===r),l=a?t.events.find(h=>h.id===a):null,f=t.stats,c=t.state;return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-justify-between g7dt-mb-3",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:"현재 인증 상태"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted",children:["소스: ",c.source||"unknown"]})]}),e.jsxs("div",{className:"g7dt-stats-grid-3 g7dt-mb-3",children:[e.jsx("div",{className:"g7dt-text-center",children:e.jsx("div",{className:`g7dt-text-lg g7dt-font-bold ${c.isAuthenticated?"g7dt-text-green":"g7dt-text-red"}`,children:c.isAuthenticated?"✓ 인증됨":"✕ 미인증"})}),e.jsx("div",{className:"g7dt-text-center",children:e.jsx("div",{className:`g7dt-text-lg g7dt-font-bold ${c.tokens.hasAccessToken?"g7dt-text-green":"g7dt-text-yellow"}`,children:c.tokens.hasAccessToken?"✓ 토큰 유효":"⚠ 토큰 없음"})}),e.jsx("div",{className:"g7dt-text-center",children:e.jsx("div",{className:"g7dt-text-lg g7dt-font-bold g7dt-text-blue",children:c.user?.name||c.user?.email||"익명"})})]}),c.user&&e.jsxs("div",{className:"g7dt-border-t g7dt-pt-3 g7dt-mt-3",children:[e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-2 g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"ID:"}),e.jsx("span",{className:"g7dt-text-white g7dt-mono",children:c.user.id})]}),e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"이메일:"}),e.jsx("span",{className:"g7dt-text-white g7dt-truncate g7dt-ml-2",title:c.user.email,children:c.user.email})]}),c.user.name&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"이름:"}),e.jsx("span",{className:"g7dt-text-white",children:c.user.name})]}),c.user.nickname&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"닉네임:"}),e.jsx("span",{className:"g7dt-text-white",children:c.user.nickname})]}),c.user.phone&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"전화번호:"}),e.jsx("span",{className:"g7dt-text-white",children:c.user.phone})]}),c.user.status&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"상태:"}),e.jsx("span",{className:c.user.status==="active"?"g7dt-text-green":"g7dt-text-yellow",children:c.user.status})]}),c.user.locale&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"언어:"}),e.jsx("span",{className:"g7dt-text-white",children:c.user.locale})]})]}),c.user.roles&&c.user.roles.length>0&&e.jsxs("div",{className:"g7dt-mt-2",children:[e.jsx("span",{className:"g7dt-text-muted g7dt-text-xs",children:"역할: "}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1",children:c.user.roles.map((h,y)=>e.jsx("span",{className:"g7dt-role-tag",title:h.guard_name?`guard: ${h.guard_name}`:void 0,children:typeof h=="string"?h:h.name},y))})]}),c.user.permissions&&c.user.permissions.length>0&&e.jsxs("div",{className:"g7dt-mt-2",children:[e.jsx("span",{className:"g7dt-text-muted g7dt-text-xs",children:"권한: "}),e.jsxs("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1 g7dt-max-h-16 g7dt-overflow-auto",children:[c.user.permissions.slice(0,10).map((h,y)=>e.jsx("span",{className:"g7dt-perm-tag",title:h.guard_name?`guard: ${h.guard_name}`:void 0,children:typeof h=="string"?h:h.name},y)),c.user.permissions.length>10&&e.jsxs("span",{className:"g7dt-text-muted g7dt-text-xs",children:["+",c.user.permissions.length-10,"개"]})]})]}),e.jsxs("div",{className:"g7dt-mt-2 g7dt-grid-2 g7dt-gap-2 g7dt-text-xs",children:[c.user.created_at&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"가입일:"}),e.jsx("span",{className:"g7dt-text-muted",children:new Date(c.user.created_at).toLocaleDateString()})]}),c.user.last_login_at&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"마지막 로그인:"}),e.jsx("span",{className:"g7dt-text-muted",children:new Date(c.user.last_login_at).toLocaleString()})]}),c.user.email_verified_at&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"이메일 인증:"}),e.jsx("span",{className:"g7dt-text-green",children:"✓ 인증됨"})]})]})]}),e.jsx("div",{className:"g7dt-border-t g7dt-pt-3 g7dt-mt-3",children:e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-2 g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"저장소:"}),e.jsx("span",{className:"g7dt-text-white",children:c.tokens.storage})]}),c.tokens.accessTokenPreview&&e.jsxs("div",{className:"g7dt-flex g7dt-justify-between",children:[e.jsx("span",{className:"g7dt-text-muted",children:"토큰:"}),e.jsx("span",{className:"g7dt-text-muted g7dt-mono",children:c.tokens.accessTokenPreview})]})]})}),c.tokens.accessTokenExpiresAt&&e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-2 g7dt-text-center",children:["토큰 만료: ",new Date(c.tokens.accessTokenExpiresAt).toLocaleString()]})]}),e.jsxs("div",{className:"g7dt-stats-grid-5",children:[e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-blue",children:f.loginAttempts}),e.jsx("div",{className:"g7dt-stat-label",children:"로그인 시도"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-green",children:f.successfulLogins}),e.jsx("div",{className:"g7dt-stat-label",children:"성공"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:`g7dt-stat-value-sm ${f.failedLogins>0?"g7dt-text-red":"g7dt-text-muted"}`,children:f.failedLogins}),e.jsx("div",{className:"g7dt-stat-label",children:"실패"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-purple",children:f.tokenRefreshes}),e.jsx("div",{className:"g7dt-stat-label",children:"토큰 갱신"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:`g7dt-stat-value-sm ${f.unauthorizedResponses>0?"g7dt-text-red":"g7dt-text-muted"}`,children:f.unauthorizedResponses}),e.jsx("div",{className:"g7dt-stat-label",children:"401 응답"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsxs("select",{value:r,onChange:h=>o(h.target.value),className:"g7dt-select",children:[e.jsx("option",{value:"all",children:"모든 이벤트"}),e.jsx("option",{value:"login",children:"로그인"}),e.jsx("option",{value:"logout",children:"로그아웃"}),e.jsx("option",{value:"token-refresh",children:"토큰 갱신"}),e.jsx("option",{value:"token-expired",children:"토큰 만료"}),e.jsx("option",{value:"api-unauthorized",children:"401 응답"})]}),e.jsx("button",{onClick:p,className:"g7dt-btn-secondary",children:"새로고침"})]}),e.jsx("div",{className:"g7dt-scroll-list-sm",children:v.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"인증 이벤트가 없습니다"}):v.slice(-20).reverse().map(h=>{const y=h.success?"✅":"❌",n=new Date(h.timestamp).toLocaleTimeString();return e.jsxs("div",{onClick:()=>g(h.id),className:`g7dt-auth-event-item ${a===h.id?"g7dt-selected":h.success?"":"g7dt-error"}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("span",{className:"g7dt-mono",children:[y," ",h.type]}),e.jsx("span",{className:"g7dt-text-muted",children:n})]}),h.error&&e.jsx("div",{className:"g7dt-text-red g7dt-mt-1 g7dt-truncate",children:h.error})]},h.id)})}),l&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:l.type}),e.jsx("button",{onClick:()=>g(null),className:"g7dt-close-btn",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"결과:"}),e.jsx("span",{className:`g7dt-ml-2 ${l.success?"g7dt-text-green":"g7dt-text-red"}`,children:l.success?"성공":"실패"})]}),l.error&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"에러:"}),e.jsx("div",{className:"g7dt-mt-1 g7dt-text-red",children:l.error})]}),l.details&&Object.keys(l.details).length>0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"상세:"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-green",children:JSON.stringify(l.details,null,2)})]})]})]}),t.headerAnalysis.length>0&&e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"최근 API 인증 헤더"}),e.jsx("div",{className:"g7dt-scroll-list-xs",children:t.headerAnalysis.slice(-10).reverse().map((h,y)=>e.jsxs("div",{className:"g7dt-header-item",children:[e.jsx("span",{className:"g7dt-text-blue g7dt-truncate g7dt-flex-1",children:h.url}),e.jsx("span",{className:`g7dt-ml-2 ${h.hasAuthHeader?"g7dt-text-green":"g7dt-text-yellow"}`,children:h.hasAuthHeader?`✓ ${h.headerType}`:"✕ 헤더 없음"}),h.responseStatus&&e.jsx("span",{className:`g7dt-ml-2 ${h.responseStatus<400?"g7dt-text-muted":"g7dt-text-red"}`,children:h.responseStatus})]},y))})]})]})},He=({devTools:d})=>{const[t,s]=x.useState([]),[a,g]=x.useState(null),[r,o]=x.useState(null),[p,v]=x.useState("all"),[l,f]=x.useState(""),[c,h]=x.useState(""),y=x.useCallback(()=>{const b=d.getLogInfo();s(b.entries),g(b.stats)},[d]);x.useEffect(()=>{y();const b=setInterval(y,1e3);return()=>clearInterval(b)},[y]);const n=t.filter(b=>!(p!=="all"&&b.level!==p||l&&!b.prefix.toLowerCase().includes(l.toLowerCase())||c&&!b.message.toLowerCase().includes(c.toLowerCase()))),i=r?t.find(b=>b.id===r):null,u=()=>{d.clearLogs(),y(),o(null)},m=b=>{switch(b){case"error":return"g7dt-text-red";case"warn":return"g7dt-text-yellow";case"info":return"g7dt-text-blue";case"debug":return"g7dt-text-muted";default:return"g7dt-text-white"}},k=b=>{switch(b){case"error":return"g7dt-log-error";case"warn":return"g7dt-log-warn";case"info":return"g7dt-log-info";case"debug":return"g7dt-log-debug";default:return"g7dt-log-default"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[a&&e.jsxs("div",{className:"g7dt-stats-grid-6",children:[e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-white",children:a.totalLogs}),e.jsx("div",{className:"g7dt-stat-label",children:"전체"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-red",children:a.byLevel.error}),e.jsx("div",{className:"g7dt-stat-label",children:"에러"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-yellow",children:a.byLevel.warn}),e.jsx("div",{className:"g7dt-stat-label",children:"경고"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:"g7dt-stat-value-sm g7dt-text-blue",children:a.byLevel.info}),e.jsx("div",{className:"g7dt-stat-label",children:"정보"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:`g7dt-stat-value-sm ${a.recentErrors>0?"g7dt-text-red":"g7dt-text-green"}`,children:a.recentErrors}),e.jsx("div",{className:"g7dt-stat-label",children:"최근 에러"})]}),e.jsxs("div",{className:"g7dt-stat-card-sm",children:[e.jsx("div",{className:`g7dt-stat-value-sm ${a.recentWarnings>0?"g7dt-text-yellow":"g7dt-text-green"}`,children:a.recentWarnings}),e.jsx("div",{className:"g7dt-stat-label",children:"최근 경고"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-flex-wrap",children:[e.jsxs("select",{value:p,onChange:b=>v(b.target.value),className:"g7dt-select",children:[e.jsx("option",{value:"all",children:"모든 레벨"}),e.jsx("option",{value:"error",children:"에러"}),e.jsx("option",{value:"warn",children:"경고"}),e.jsx("option",{value:"info",children:"정보"}),e.jsx("option",{value:"log",children:"로그"}),e.jsx("option",{value:"debug",children:"디버그"})]}),e.jsx("input",{type:"text",placeholder:"prefix 필터",value:l,onChange:b=>f(b.target.value),className:"g7dt-input g7dt-w-32"}),e.jsx("input",{type:"text",placeholder:"메시지 검색",value:c,onChange:b=>h(b.target.value),className:"g7dt-input g7dt-flex-1"}),e.jsx("button",{onClick:y,className:"g7dt-btn-secondary",children:"새로고침"}),e.jsx("button",{onClick:u,className:"g7dt-btn-danger",children:"초기화"})]}),e.jsx("div",{className:"g7dt-scroll-list-lg",children:n.length===0?e.jsx("div",{className:"g7dt-empty-state",children:t.length===0?"로그가 없습니다":"필터에 맞는 로그가 없습니다"}):n.slice(-50).reverse().map(b=>{const N=new Date(b.timestamp).toLocaleTimeString();return e.jsxs("div",{onClick:()=>o(b.id),className:`g7dt-log-item ${r===b.id?"g7dt-selected":k(b.level)}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:`g7dt-font-bold g7dt-uppercase ${m(b.level)}`,children:b.level}),e.jsxs("span",{className:"g7dt-text-purple",children:["[",b.prefix,"]"]})]}),e.jsx("span",{className:"g7dt-text-muted",children:N})]}),e.jsx("div",{className:"g7dt-text-white g7dt-mt-1 g7dt-truncate",children:b.message})]},b.id)})}),i&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold",children:[e.jsx("span",{className:m(i.level),children:i.level.toUpperCase()})," ",e.jsxs("span",{className:"g7dt-text-purple",children:["[",i.prefix,"]"]})]}),e.jsx("button",{onClick:()=>o(null),className:"g7dt-close-btn",children:"✕"})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"시간:"}),e.jsx("span",{className:"g7dt-ml-2 g7dt-text-white",children:new Date(i.timestamp).toLocaleString()})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"메시지:"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-white g7dt-whitespace-pre-wrap",children:i.message})]}),i.args.length>0&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"인수:"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-green",children:JSON.stringify(i.args,null,2)})]}),i.stack&&e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"스택 트레이스:"}),e.jsx("pre",{className:"g7dt-pre-stack g7dt-text-red",children:i.stack})]})]})]}),a&&Object.keys(a.byPrefix).length>0&&e.jsxs("div",{children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"Prefix별 로그 수"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-2",children:Object.entries(a.byPrefix).sort((b,N)=>N[1]-b[1]).slice(0,10).map(([b,N])=>e.jsxs("button",{onClick:()=>f(b),className:l===b?"g7dt-prefix-btn-active":"g7dt-prefix-btn",children:[b,": ",e.jsx("span",{className:"g7dt-text-blue",children:N})]},b))})]})]})},Fe=({devTools:d})=>{const[t,s]=x.useState(d.getLayoutDebugInfo()),[a,g]=x.useState(!1),[r,o]=x.useState(null),[p,v]=x.useState(!1),[l,f]=x.useState({});x.useEffect(()=>{const m=setInterval(()=>{s(d.getLayoutDebugInfo())},1e3);return()=>clearInterval(m)},[d]),x.useEffect(()=>{(async()=>{try{const k=await fetch("/api/admin/permissions",{headers:{Authorization:`Bearer ${localStorage.getItem("auth_token")}`,Accept:"application/json"}});if(k.ok){const b=await k.json(),N={},C=w=>{w.forEach(S=>{N[S.identifier]=S.name,S.children?.length&&C(S.children)})};Object.values(b.data?.permissions||{}).forEach(w=>{w.permissions&&C(w.permissions)}),f(N)}}catch(k){console.warn("[G7DevTools] 권한 목록 조회 실패:",k)}})()},[]);const c=t.current,h=c?.layoutJson,y=t.history,n=t.stats,i=h?[{key:"components",label:"컴포넌트",count:Array.isArray(h.components)?h.components.length:0},{key:"data_sources",label:"데이터소스",count:Array.isArray(h.data_sources)?h.data_sources.length:0},{key:"computed",label:"Computed",count:h.computed?Object.keys(h.computed).length:0},{key:"defines",label:"Defines",count:h.defines?Object.keys(h.defines).length:0},{key:"slots",label:"Slots",count:h.slots?Object.keys(h.slots).length:0},{key:"modals",label:"Modals",count:h.modals?Object.keys(h.modals).length:0},{key:"init_actions",label:"초기화 액션",count:Array.isArray(h.init_actions)?h.init_actions.length:0},{key:"scripts",label:"스크립트",count:Array.isArray(h.scripts)?h.scripts.length:0},{key:"meta",label:"메타데이터",count:h.meta?Object.keys(h.meta).length:0},{key:"errorHandling",label:"에러핸들링",count:h.errorHandling?Object.keys(h.errorHandling).length:0},{key:"permissions",label:"권한",count:Array.isArray(h.permissions)?h.permissions.length:0}]:[],u=m=>{navigator.clipboard.writeText(JSON.stringify(m,null,2))};return c?e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"📊 로드 통계"}),e.jsxs("div",{className:"g7dt-grid-3",children:[e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"총 로드:"}),e.jsx("span",{className:"g7dt-text-blue",children:n.totalLoads})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"캐시 히트:"}),e.jsx("span",{className:"g7dt-text-green",children:n.cacheHits})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"API 로드:"}),e.jsx("span",{className:"g7dt-text-yellow",children:n.apiLoads})]})]})]}),e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:"📋 현재 레이아웃"}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsx("button",{onClick:()=>u(h),className:"g7dt-btn-small",title:"전체 JSON 복사",children:"📋 복사"}),e.jsx("button",{onClick:()=>g(!a),className:"g7dt-btn-small",children:a?"요약 보기":"전체 보기"})]})]}),e.jsxs("div",{className:"g7dt-space-y-2 g7dt-text-xs",children:[e.jsxs("div",{className:"g7dt-flex g7dt-gap-4",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"경로:"}),e.jsx("span",{className:"g7dt-ml-1 g7dt-text-white g7dt-font-mono",children:c.layoutPath})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"템플릿:"}),e.jsx("span",{className:"g7dt-ml-1 g7dt-text-blue",children:c.templateId})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-4",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"버전:"}),e.jsx("span",{className:"g7dt-ml-1 g7dt-text-white",children:c.version||"-"})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"소스:"}),e.jsx("span",{className:`g7dt-ml-1 ${c.source==="cache"?"g7dt-text-green":"g7dt-text-yellow"}`,children:c.source==="cache"?"💾 캐시":"🌐 API"})]}),e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"로드 시간:"}),e.jsx("span",{className:"g7dt-ml-1 g7dt-text-white",children:c.loadedAt?new Date(c.loadedAt).toLocaleTimeString():"-"})]})]}),e.jsx("div",{className:"g7dt-flex g7dt-gap-4 g7dt-mt-2",children:e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-text-muted",children:"권한:"}),h.permissions&&h.permissions.length>0?e.jsxs("span",{className:"g7dt-ml-1 g7dt-text-yellow",children:["🔒 ",h.permissions.map(m=>l[m]||m).join(", ")]}):h.permissions&&h.permissions.length===0?e.jsx("span",{className:"g7dt-ml-1 g7dt-text-green",children:"🌐 공개"}):e.jsx("span",{className:"g7dt-ml-1 g7dt-text-muted",children:"미정의 (부모 상속)"})]})})]})]}),!a&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"🏗️ 레이아웃 구조"}),e.jsx("div",{className:"g7dt-grid-2",children:i.map(m=>e.jsxs("button",{onClick:()=>o(r===m.key?null:m.key),className:`g7dt-section-btn ${r===m.key?"g7dt-selected":""}`,children:[e.jsx("span",{children:m.label}),e.jsxs("span",{className:"g7dt-text-blue",children:[m.count,"개"]})]},m.key))})]}),(a||r)&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:a?"📄 전체 레이아웃 JSON":`📎 ${r} 섹션`}),e.jsx("button",{onClick:()=>{a?u(h):r&&h&&u(h[r])},className:"g7dt-btn-small",children:"복사"})]}),r==="permissions"&&!a?e.jsx("div",{className:"g7dt-text-xs",children:h.permissions&&h.permissions.length>0?e.jsxs("table",{className:"g7dt-table g7dt-w-full",children:[e.jsx("thead",{children:e.jsxs("tr",{className:"g7dt-border-b g7dt-border-gray-700",children:[e.jsx("th",{className:"g7dt-text-left g7dt-p-2 g7dt-text-muted",children:"권한 ID"}),e.jsx("th",{className:"g7dt-text-left g7dt-p-2 g7dt-text-muted",children:"명칭"})]})}),e.jsx("tbody",{children:h.permissions.map(m=>e.jsxs("tr",{className:"g7dt-border-b g7dt-border-gray-800",children:[e.jsx("td",{className:"g7dt-p-2 g7dt-font-mono g7dt-text-blue",children:m}),e.jsx("td",{className:"g7dt-p-2 g7dt-text-white",children:l[m]||e.jsx("span",{className:"g7dt-text-muted",children:"-"})})]},m))})]}):h.permissions&&h.permissions.length===0?e.jsx("p",{className:"g7dt-text-muted g7dt-p-2",children:"🌐 공개 레이아웃 (permissions: [])"}):e.jsx("p",{className:"g7dt-text-muted g7dt-p-2",children:"권한 미정의 (부모 레이아웃 상속)"})}):e.jsx("pre",{className:"g7dt-pre g7dt-text-green g7dt-max-h-96 g7dt-overflow-auto",children:JSON.stringify(a?h:r&&h?h[r]:null,null,2)})]}),y.length>0&&e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-cursor-pointer",onClick:()=>v(!p),children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold",children:["📜 로드 이력 (",y.length,")"]}),e.jsx("span",{className:"g7dt-text-muted",children:p?"▼":"▶"})]}),p&&e.jsx("div",{className:"g7dt-mt-2 g7dt-space-y-1",children:y.slice(-20).reverse().map(m=>e.jsxs("div",{className:"g7dt-history-item",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsx("span",{className:"g7dt-font-mono g7dt-text-xs",children:m.layoutPath}),e.jsx("span",{className:`g7dt-text-xs ${m.source==="cache"?"g7dt-text-green":"g7dt-text-yellow"}`,children:m.source==="cache"?"💾":"🌐"})]}),e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted",children:[m.templateId," · ",m.loadedAt?new Date(m.loadedAt).toLocaleTimeString():"-"]})]},m.id))})]}),e.jsxs("div",{className:"g7dt-card g7dt-text-xs g7dt-text-muted",children:["💡 레이아웃 캐시 문제가 의심되면:",e.jsxs("ul",{className:"g7dt-ml-4 g7dt-list-disc",children:[e.jsx("li",{children:'"소스" 항목이 💾 캐시인지 🌐 API인지 확인'}),e.jsx("li",{children:"캐시에서 로드된 경우, JSON 내용이 최신인지 확인"}),e.jsxs("li",{children:["문제가 있으면 ",e.jsx("code",{className:"g7dt-code",children:"php artisan template:refresh-layout"})," 실행"]})]})]})]}):e.jsx("div",{className:"g7dt-space-y-4",children:e.jsxs("div",{className:"g7dt-empty-state",children:["현재 로드된 레이아웃이 없습니다.",e.jsx("br",{}),"페이지를 새로고침하거나 라우트를 변경해보세요."]})})},We=({devTools:d})=>{const[t,s]=x.useState(d.getChangeDetectionInfo()),[a,g]=x.useState(null),[r,o]=x.useState(!1),[p,v]=x.useState(new Set);x.useEffect(()=>{const b=setInterval(()=>{s(d.getChangeDetectionInfo())},500);return()=>clearInterval(b)},[d]);const{executionDetails:l,alerts:f,stats:c}=t,h=r?l.filter(b=>b.alerts.length>0||b.stateChanges.length===0):l,y=a?l.find(b=>b.executionId===a):null,n=b=>{v(N=>{const C=new Set(N);return C.has(b)?C.delete(b):C.add(b),C})},i=b=>{switch(b){case"normal":return"✅";case"early-return-condition":return"⏎";case"early-return-validation":return"❌";case"error":return"💥";default:return"❓"}},u=b=>{switch(b){case"no-state-change":return"⚠️";case"no-datasource-change":return"📊";case"expected-not-fulfilled":return"❗";case"object-reference-same":return"🔄";case"early-return-detected":return"⏎";case"async-timing-issue":return"⏱️";default:return"📢"}},m=b=>{switch(b){case"error":return"g7dt-text-red";case"warning":return"g7dt-text-yellow";case"info":return"g7dt-text-blue";default:return"g7dt-text-white"}},k=b=>new Date(b).toLocaleTimeString("ko-KR",{hour:"2-digit",minute:"2-digit",second:"2-digit",fractionalSecondDigits:3});return e.jsxs("div",{className:"g7dt-space-y-4",children:[f.filter(b=>b.severity==="error"||b.severity==="warning").length>0&&e.jsx("div",{className:"g7dt-card",style:{backgroundColor:"#7f1d1d",borderColor:"#991b1b"},children:e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("span",{className:"g7dt-font-bold",children:["⚠️ 변경 감지 알림: ",f.filter(b=>b.severity==="error").length,"개 에러,"," ",f.filter(b=>b.severity==="warning").length,"개 경고"]}),e.jsx("button",{onClick:()=>d.clearChangeDetectionData(),className:"g7dt-btn-small",children:"초기화"})]})}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"📊 변경 감지 통계"}),e.jsxs("div",{className:"g7dt-grid-3",children:[e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"총 실행:"}),e.jsx("span",{className:"g7dt-text-blue",children:c.totalExecutions})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"상태 변경 있음:"}),e.jsx("span",{className:"g7dt-text-green",children:c.executionsWithStateChange})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"상태 변경 없음:"}),e.jsx("span",{className:c.executionsWithoutStateChange>0?"g7dt-text-yellow":"g7dt-text-gray",children:c.executionsWithoutStateChange})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"Early Return:"}),e.jsx("span",{className:"g7dt-text-orange",children:c.earlyReturnCount})]}),e.jsxs("div",{className:"g7dt-stat-item",children:[e.jsx("span",{className:"g7dt-text-muted",children:"알림:"}),e.jsx("span",{className:c.alertCount>0?"g7dt-text-red":"g7dt-text-gray",children:c.alertCount})]})]})]}),f.length>0&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"🔔 최근 알림"}),e.jsx("div",{className:"g7dt-space-y-2",style:{maxHeight:"200px",overflowY:"auto"},children:f.slice(-10).reverse().map(b=>e.jsxs("div",{className:"g7dt-list-item",onClick:()=>n(b.id),style:{cursor:"pointer"},children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-start",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:u(b.type)}),e.jsx("span",{className:`g7dt-text-xs ${m(b.severity)}`,children:b.message})]}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted",children:k(b.timestamp)})]}),p.has(b.id)&&e.jsxs("div",{className:"g7dt-mt-2 g7dt-text-xs g7dt-text-muted g7dt-pl-6",children:[e.jsx("p",{children:b.description}),b.statePath&&e.jsxs("p",{className:"g7dt-mt-1",children:[e.jsx("span",{className:"g7dt-text-cyan",children:"경로:"})," ",b.statePath]}),b.suggestion&&e.jsxs("p",{className:"g7dt-mt-1",children:[e.jsx("span",{className:"g7dt-text-green",children:"제안:"})," ",b.suggestion]}),b.docLink&&e.jsxs("p",{className:"g7dt-mt-1",children:[e.jsx("span",{className:"g7dt-text-blue",children:"문서:"})," ",e.jsx("code",{className:"g7dt-code",children:b.docLink})]})]})]},b.id))})]}),e.jsxs("div",{className:"g7dt-card",children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center g7dt-mb-2",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold",children:"🔧 핸들러 실행 이력"}),e.jsxs("label",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-xs",children:[e.jsx("input",{type:"checkbox",checked:r,onChange:b=>o(b.target.checked)}),e.jsx("span",{children:"경고만 표시"})]})]}),h.length===0?e.jsxs("div",{className:"g7dt-empty-state",children:["핸들러 실행 이력이 없습니다.",e.jsx("br",{}),"액션을 실행하면 여기에 표시됩니다."]}):e.jsx("div",{className:"g7dt-space-y-1",style:{maxHeight:"300px",overflowY:"auto"},children:h.slice(-20).reverse().map(b=>e.jsxs("div",{className:`g7dt-list-item ${a===b.executionId?"g7dt-selected":""}`,onClick:()=>g(a===b.executionId?null:b.executionId),style:{cursor:"pointer"},children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:i(b.exitReason)}),e.jsx("span",{className:"g7dt-text-white g7dt-font-mono g7dt-text-xs",children:b.handlerName}),b.alerts.length>0&&e.jsx("span",{className:"g7dt-badge g7dt-badge-warning",children:b.alerts.length})]}),e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-xs",children:[e.jsxs("span",{className:"g7dt-text-muted",children:["상태: ",b.stateChanges.length]}),e.jsx("span",{className:"g7dt-text-muted",children:b.duration?`${b.duration}ms`:"-"}),e.jsx("span",{className:"g7dt-text-muted",children:k(b.startTime)})]})]}),a===b.executionId&&y&&e.jsxs("div",{className:"g7dt-mt-3 g7dt-pt-3 g7dt-border-t g7dt-border-gray-700",children:[e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted",children:"종료 사유: "}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white",children:y.exitReason||"unknown"}),y.exitLocation&&e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-2",children:"위치: "}),e.jsx("code",{className:"g7dt-code g7dt-text-xs",children:y.exitLocation})]})]}),y.exitDescription&&e.jsx("div",{className:"g7dt-mb-2 g7dt-text-xs g7dt-text-muted",children:y.exitDescription}),y.stateChanges.length>0&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsxs("span",{className:"g7dt-text-xs g7dt-font-bold g7dt-text-green",children:["상태 변경 (",y.stateChanges.length,")"]}),e.jsx("div",{className:"g7dt-mt-1 g7dt-space-y-1",children:y.stateChanges.map(N=>e.jsxs("div",{className:"g7dt-text-xs g7dt-pl-2",children:[e.jsx("span",{className:"g7dt-text-cyan",children:N.path}),e.jsxs("span",{className:"g7dt-text-muted",children:[" (",N.changeType,")"]}),N.comparison.changedKeys&&e.jsxs("span",{className:"g7dt-text-muted g7dt-ml-1",children:["[변경: ",N.comparison.changedKeys.join(", "),"]"]})]},N.id))})]}),y.dataSourceChanges.length>0&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsxs("span",{className:"g7dt-text-xs g7dt-font-bold g7dt-text-blue",children:["데이터소스 변경 (",y.dataSourceChanges.length,")"]}),e.jsx("div",{className:"g7dt-mt-1 g7dt-space-y-1",children:y.dataSourceChanges.map((N,C)=>e.jsxs("div",{className:"g7dt-text-xs g7dt-pl-2",children:[e.jsx("span",{className:"g7dt-text-cyan",children:N.dataSourceId}),e.jsxs("span",{className:"g7dt-text-muted",children:[" ","(",N.changeType,": ",N.previousStatus," → ",N.newStatus,")"]})]},C))})]}),y.expectedChanges&&e.jsxs("div",{className:"g7dt-mb-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-font-bold g7dt-text-yellow",children:"기대 변경"}),e.jsxs("div",{className:"g7dt-mt-1 g7dt-text-xs g7dt-pl-2 g7dt-text-muted",children:["상태: ",y.expectedChanges.expectedStatePaths.join(", ")||"없음",e.jsx("br",{}),"데이터소스: ",y.expectedChanges.expectedDataSources.join(", ")||"없음"]})]}),y.alerts.length>0&&e.jsxs("div",{children:[e.jsxs("span",{className:"g7dt-text-xs g7dt-font-bold g7dt-text-red",children:["알림 (",y.alerts.length,")"]}),e.jsx("div",{className:"g7dt-mt-1 g7dt-space-y-1",children:y.alerts.map(N=>e.jsxs("div",{className:"g7dt-text-xs g7dt-pl-2",children:[e.jsx("span",{children:u(N.type)}),e.jsx("span",{className:`g7dt-ml-1 ${m(N.severity)}`,children:N.message})]},N.id))})]})]})]},b.executionId))})]}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-2",children:"💡 변경 감지 가이드"}),e.jsxs("ul",{className:"g7dt-help-list",children:[e.jsx("li",{children:"핸들러 실행 후 상태 변경이 없으면 ⚠️ 경고가 표시됩니다"}),e.jsx("li",{children:"early return으로 종료된 경우 ⏎ 아이콘으로 표시됩니다"}),e.jsx("li",{children:"객체 변경 시 참조가 동일하면 🔄 경고가 발생합니다"}),e.jsx("li",{children:"문제 해결: exitLocation 확인 → 조건문 검토 → 불변성 확인"})]})]})]})},Ve=({devTools:d})=>{const[t,s]=x.useState(null),[a,g]=x.useState(null),[r,o]=x.useState("all"),p=x.useCallback(()=>{s(d.getStaleClosureTrackingInfo())},[d]);if(x.useEffect(()=>{p();const n=setInterval(p,2e3);return()=>clearInterval(n)},[p]),!t)return e.jsx("div",{className:"g7dt-empty-state",children:"로딩 중..."});const{warnings:v,stats:l}=t,f=v.filter(n=>r==="all"?!0:n.severity===r),c=a?v.find(n=>n.id===a):null,h=n=>{switch(n){case"error":return"❌";case"warning":return"⚠️";default:return"❓"}},y=n=>{switch(n){case"state-changed-during-async":return"⏳";case"captured-in-callback":return"📸";case"stale-in-handler":return"🔄";default:return"❓"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Stale Closure 통계"}),e.jsxs("div",{className:"g7dt-stats-grid-3",children:[e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-yellow",children:l.totalWarnings||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"총 경고"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-red",children:l.warningsBySeverity?.error||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"에러"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-orange",children:l.warningsBySeverity?.warning||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"경고"})]})]})]}),l.warningsByType&&Object.keys(l.warningsByType).length>0&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"타입별 분포"}),e.jsx("div",{className:"g7dt-space-y-2",children:Object.entries(l.warningsByType).map(([n,i])=>e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-text-xs",children:[e.jsxs("span",{className:"g7dt-text-muted",children:[y(n)," ",n]}),e.jsx("span",{className:"g7dt-text-white g7dt-font-mono",children:i})]},n))})]}),e.jsx("div",{className:"g7dt-card",children:e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted",children:"심각도:"}),e.jsxs("select",{value:r,onChange:n=>o(n.target.value),className:"g7dt-select",children:[e.jsx("option",{value:"all",children:"전체"}),e.jsx("option",{value:"error",children:"에러"}),e.jsx("option",{value:"warning",children:"경고"})]}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:[f.length,"개 표시"]})]})}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Stale Closure 경고"}),f.length===0?e.jsx("div",{className:"g7dt-empty-state g7dt-text-green",children:"✅ Stale Closure 경고가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:f.slice().reverse().map(n=>e.jsxs("div",{className:`g7dt-list-item g7dt-cursor-pointer ${a===n.id?"g7dt-selected":""}`,onClick:()=>g(a===n.id?null:n.id),children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:h(n.severity)}),e.jsx("span",{children:y(n.type)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white g7dt-truncate",children:n.location}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:new Date(n.timestamp).toLocaleTimeString()})]}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1 g7dt-truncate",children:n.statePath})]},n.id))})]}),c&&e.jsxs("div",{className:"g7dt-card g7dt-border-yellow",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:[h(c.severity)," 경고 상세"]}),e.jsxs("div",{className:"g7dt-space-y-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"위치"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white g7dt-mono",children:c.location})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"타입"}),e.jsxs("div",{className:"g7dt-text-sm g7dt-text-white",children:[y(c.type)," ",c.type]})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"상태 경로"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-yellow g7dt-mono",children:c.statePath})]}),e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"캡처된 값"}),e.jsx("pre",{className:"g7dt-code-block g7dt-text-red",children:JSON.stringify(c.capturedValue,null,2)})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"현재 값"}),e.jsx("pre",{className:"g7dt-code-block g7dt-text-green",children:JSON.stringify(c.currentValue,null,2)})]})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"시간 차이"}),e.jsxs("div",{className:"g7dt-text-sm g7dt-text-white",children:[c.timeDiff,"ms"]})]}),c.suggestion&&e.jsxs("div",{className:"g7dt-bg-blue-dark g7dt-p-2 g7dt-rounded",children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-blue",children:"💡 제안"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white g7dt-mt-1",children:c.suggestion})]})]})]})]})},Be=({devTools:d})=>{const[t,s]=x.useState(null),[a,g]=x.useState(null),[r,o]=x.useState("all"),p=x.useCallback(()=>{s(d.getNestedContextTrackingInfo())},[d]);if(x.useEffect(()=>{p();const n=setInterval(p,2e3);return()=>clearInterval(n)},[p]),!t)return e.jsx("div",{className:"g7dt-empty-state",children:"로딩 중..."});const{contexts:v,stats:l}=t,f=v.filter(n=>r==="all"?!0:n.componentType===r),c=a?v.find(n=>n.id===a):null,h=n=>{switch(n){case"expandChildren":return"📂";case"cellChildren":return"📊";case"iteration":return"🔄";case"modal":return"🪟";case"slot":return"🔲";default:return"❓"}},y=n=>"│".repeat(Math.max(0,n-1))+(n>0?"├─":"");return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"Nested Context 통계"}),e.jsxs("div",{className:"g7dt-stats-grid-3",children:[e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-blue",children:l.totalContexts||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"총 컨텍스트"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-purple",children:l.maxDepth||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"최대 깊이"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-red",children:l.failedAccessCount||0}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"접근 오류"})]})]})]}),l.byType&&Object.keys(l.byType).length>0&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"타입별 분포"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-3",children:Object.entries(l.byType).map(([n,i])=>e.jsxs("div",{className:`g7dt-badge g7dt-cursor-pointer ${r===n?"g7dt-badge-selected":""}`,onClick:()=>o(r===n?"all":n),children:[h(n)," ",n,": ",i]},n))})]}),e.jsx("div",{className:"g7dt-card",children:e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted",children:"타입:"}),e.jsxs("select",{value:r,onChange:n=>o(n.target.value),className:"g7dt-select",children:[e.jsx("option",{value:"all",children:"전체"}),e.jsx("option",{value:"expandChildren",children:"expandChildren"}),e.jsx("option",{value:"cellChildren",children:"cellChildren"}),e.jsx("option",{value:"iteration",children:"iteration"}),e.jsx("option",{value:"modal",children:"modal"}),e.jsx("option",{value:"slot",children:"slot"})]}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:[f.length,"개 표시"]})]})}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"컨텍스트 목록"}),f.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"추적된 Nested Context가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:f.slice().reverse().map(n=>{const i=n.accessAttempts?.some(m=>!m.found),u=n.mergedContext?.all||[];return e.jsxs("div",{className:`g7dt-list-item g7dt-cursor-pointer ${a===n.id?"g7dt-selected":""} ${i?"g7dt-border-red":""}`,onClick:()=>g(a===n.id?null:n.id),children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:"g7dt-text-muted g7dt-mono g7dt-text-xs",children:y(n.depth)}),e.jsx("span",{children:h(n.componentType)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white",children:n.componentId||n.componentType}),i&&e.jsx("span",{className:"g7dt-text-red g7dt-text-xs",children:"⚠️ 오류"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:["깊이: ",n.depth]})]}),e.jsxs("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1",children:["변수: ",u.slice(0,5).join(", "),u.length>5&&"..."]})]},n.id)})})]}),c&&e.jsxs("div",{className:"g7dt-card g7dt-border-blue",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:[h(c.componentType)," 컨텍스트 상세"]}),e.jsxs("div",{className:"g7dt-space-y-3",children:[e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"타입"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white",children:c.componentType})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"깊이"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white",children:c.depth})]})]}),c.componentId&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"컴포넌트 ID"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-blue g7dt-mono",children:c.componentId})]}),c.parentId&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"부모 컨텍스트"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-purple g7dt-mono",children:c.parentId})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"사용 가능한 변수"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1",children:(c.mergedContext?.all||[]).map(n=>e.jsx("span",{className:"g7dt-badge g7dt-badge-blue g7dt-text-xs",children:n},n))})]}),c.ownContext?.added&&c.ownContext.added.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"추가된 변수 (이 컨텍스트에서)"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1",children:c.ownContext.added.map(n=>e.jsxs("span",{className:"g7dt-badge g7dt-badge-green g7dt-text-xs",children:["+ ",n]},n))})]}),c.accessAttempts&&c.accessAttempts.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"접근 시도"}),e.jsx("div",{className:"g7dt-space-y-1",children:c.accessAttempts.slice(0,10).map((n,i)=>e.jsxs("div",{className:`g7dt-flex g7dt-justify-between g7dt-text-xs ${n.found?"":"g7dt-text-red"}`,children:[e.jsx("span",{className:"g7dt-mono",children:n.path}),e.jsx("span",{children:n.found?"✓":"✕"})]},i))})]})]})]})]})},Ge=({devTools:d})=>{const[t,s]=x.useState(null),[a,g]=x.useState(null),[r,o]=x.useState(null),[p,v]=x.useState("modals"),l=x.useCallback(()=>{s(d.getModalStateScopeTrackingInfo())},[d]);if(x.useEffect(()=>{l();const N=setInterval(l,2e3);return()=>clearInterval(N)},[l]),!t)return e.jsx("div",{className:"g7dt-empty-state",children:"로딩 중..."});const{modals:f,issues:c,relations:h,changeLogs:y,stats:n}=t,i=a?f.find(N=>N.modalId===a):null,u=r?c.find(N=>N.id===r):null,m=N=>{switch(N){case"isolated":return"🔒";case"shared":return"🔓";case"inherited":return"📥";default:return"❓"}},k=N=>{switch(N){case"state-leakage":return"💧";case"isolation-violation":return"🔓";case"parent-mutation":return"👆";case"orphaned-state":return"👻";case"scope-mismatch":return"🔀";case"cleanup-failure":return"🧹";case"missing-definition":return"🚫";default:return"❓"}},b=N=>{switch(N){case"error":return"❌";case"warning":return"⚠️";default:return"❓"}};return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"모달 상태 스코프 통계"}),e.jsxs("div",{className:"g7dt-stats-grid-3",children:[e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-blue",children:n.totalModals}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"총 모달"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-green",children:n.openModals}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"열린 모달"})]}),e.jsxs("div",{className:"g7dt-text-center",children:[e.jsx("div",{className:"g7dt-text-2xl g7dt-font-bold g7dt-text-red",children:n.totalIssues}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"이슈"})]})]})]}),e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"스코프별 / 이슈별"}),e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-4",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"스코프 타입"}),e.jsx("div",{className:"g7dt-space-y-1",children:Object.entries(n.byScope).map(([N,C])=>e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-text-xs",children:[e.jsxs("span",{children:[m(N)," ",N]}),e.jsx("span",{className:"g7dt-font-mono",children:C})]},N))})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-2",children:"이슈 타입"}),e.jsxs("div",{className:"g7dt-space-y-1",children:[Object.entries(n.issuesByType).filter(([N,C])=>C>0).map(([N,C])=>e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-text-xs",children:[e.jsxs("span",{children:[k(N)," ",N]}),e.jsx("span",{className:"g7dt-font-mono g7dt-text-red",children:C})]},N)),Object.values(n.issuesByType).every(N=>N===0)&&e.jsx("div",{className:"g7dt-text-xs g7dt-text-green",children:"✅ 이슈 없음"})]})]})]})]}),e.jsx("div",{className:"g7dt-card",children:e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsxs("button",{className:`g7dt-tab-button ${p==="modals"?"g7dt-tab-active":""}`,onClick:()=>v("modals"),children:["🪟 모달 (",f.length,")"]}),e.jsxs("button",{className:`g7dt-tab-button ${p==="issues"?"g7dt-tab-active":""}`,onClick:()=>v("issues"),children:["⚠️ 이슈 (",c.length,")"]}),e.jsxs("button",{className:`g7dt-tab-button ${p==="logs"?"g7dt-tab-active":""}`,onClick:()=>v("logs"),children:["📝 로그 (",y.length,")"]})]})}),p==="modals"&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"모달 목록"}),f.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"추적된 모달이 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:f.map(N=>{const C=c.some(w=>w.modalId===N.modalId);return e.jsx("div",{className:`g7dt-list-item g7dt-cursor-pointer ${a===N.modalId?"g7dt-selected":""} ${C?"g7dt-border-red":""}`,onClick:()=>g(a===N.modalId?null:N.modalId),children:e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:N.closedAt===null?"📭":"📪"}),e.jsx("span",{children:m(N.scopeType)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white",children:N.modalName}),C&&e.jsx("span",{className:"g7dt-text-red",children:"⚠️"}),e.jsxs("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:["변경: ",N.stateChangeCount]})]})},N.modalId)})})]}),p==="issues"&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"이슈 목록"}),c.length===0?e.jsx("div",{className:"g7dt-empty-state g7dt-text-green",children:"✅ 모달 상태 이슈가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:c.slice().reverse().map(N=>e.jsxs("div",{className:`g7dt-list-item g7dt-cursor-pointer ${r===N.id?"g7dt-selected":""}`,onClick:()=>o(r===N.id?null:N.id),children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{children:b(N.severity)}),e.jsx("span",{children:k(N.type)}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-white",children:N.modalName}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:new Date(N.timestamp).toLocaleTimeString()})]}),e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mt-1 g7dt-truncate",children:N.description})]},N.id))})]}),p==="logs"&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"상태 변경 로그"}),y.length===0?e.jsx("div",{className:"g7dt-empty-state",children:"상태 변경 로그가 없습니다"}):e.jsx("div",{className:"g7dt-space-y-2 g7dt-max-h-60 g7dt-overflow-y-auto",children:y.slice().reverse().slice(0,50).map(N=>e.jsxs("div",{className:`g7dt-list-item ${N.violatesIsolation?"g7dt-border-red":""}`,children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2",children:[e.jsx("span",{className:"g7dt-text-xs g7dt-text-white",children:N.modalName}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-blue g7dt-mono",children:N.stateKey}),N.violatesIsolation&&e.jsx("span",{className:"g7dt-text-red",children:"⚠️ 격리 위반"}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted g7dt-ml-auto",children:new Date(N.timestamp).toLocaleTimeString()})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2 g7dt-text-xs g7dt-mt-1",children:[e.jsx("span",{className:"g7dt-text-red g7dt-truncate",style:{maxWidth:"45%"},children:JSON.stringify(N.previousValue)}),e.jsx("span",{className:"g7dt-text-muted",children:"→"}),e.jsx("span",{className:"g7dt-text-green g7dt-truncate",style:{maxWidth:"45%"},children:JSON.stringify(N.newValue)})]})]},N.id))})]}),i&&p==="modals"&&e.jsxs("div",{className:"g7dt-card g7dt-border-blue",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:[m(i.scopeType)," 모달 상세: ",i.modalName]}),e.jsxs("div",{className:"g7dt-space-y-3",children:[e.jsxs("div",{className:"g7dt-grid-2 g7dt-gap-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"스코프"}),e.jsxs("div",{className:"g7dt-text-sm g7dt-text-white",children:[m(i.scopeType)," ",i.scopeType]})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"상태"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white",children:i.closedAt===null?"📭 열림":"📪 닫힘"})]})]}),i.parentModalId&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"부모 모달"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-purple g7dt-mono",children:i.parentModalId})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"상태 변경 횟수"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white",children:i.stateChangeCount})]}),i.isolatedStateKeys.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-1",children:"격리된 상태 키"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1",children:i.isolatedStateKeys.map(N=>e.jsxs("span",{className:"g7dt-badge g7dt-badge-red g7dt-text-xs",children:["🔒 ",N]},N))})]}),i.sharedStateKeys.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted g7dt-mb-1",children:"공유된 상태 키"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1",children:i.sharedStateKeys.map(N=>e.jsxs("span",{className:"g7dt-badge g7dt-badge-blue g7dt-text-xs",children:["🔓 ",N]},N))})]})]})]}),u&&p==="issues"&&e.jsxs("div",{className:"g7dt-card g7dt-border-red",children:[e.jsxs("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:[b(u.severity)," 이슈 상세"]}),e.jsxs("div",{className:"g7dt-space-y-3",children:[e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"타입"}),e.jsxs("div",{className:"g7dt-text-sm g7dt-text-white",children:[k(u.type)," ",u.type]})]}),e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"설명"}),e.jsx("div",{className:"g7dt-text-sm g7dt-text-white",children:u.description})]}),u.affectedStateKeys.length>0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"영향 받는 상태 키"}),e.jsx("div",{className:"g7dt-flex g7dt-flex-wrap g7dt-gap-1 g7dt-mt-1",children:u.affectedStateKeys.map(N=>e.jsx("span",{className:"g7dt-badge g7dt-text-xs",children:N},N))})]}),u.leakedValue!==void 0&&e.jsxs("div",{children:[e.jsx("div",{className:"g7dt-text-xs g7dt-text-muted",children:"유출된 값"}),e.jsx("pre",{className:"g7dt-code-block g7dt-text-red",children:JSON.stringify(u.leakedValue,null,2)})]})]})]}),h.length>0&&e.jsxs("div",{className:"g7dt-card",children:[e.jsx("h3",{className:"g7dt-text-sm g7dt-font-bold g7dt-mb-3",children:"모달 관계"}),e.jsx("div",{className:"g7dt-space-y-2",children:h.map((N,C)=>e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-gap-2 g7dt-text-xs",children:[e.jsx("span",{className:"g7dt-text-purple",children:N.parentModalId}),e.jsx("span",{className:"g7dt-text-muted",children:"→"}),e.jsx("span",{className:"g7dt-text-blue",children:N.childModalId}),e.jsx("span",{className:"g7dt-badge g7dt-text-xs g7dt-ml-auto",children:N.relationType})]},C))})]})]})},Je=({devTools:d})=>{const[t,s]=x.useState("definitions"),[a,g]=x.useState(null),[r,o]=x.useState(""),p=x.useCallback(()=>{d&&g(d.getNamedActionTrackingInfo())},[d]);if(x.useEffect(()=>{p();const n=setInterval(p,2e3);return()=>clearInterval(n)},[p]),!a)return e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:"데이터 로딩 중..."});const{definitions:v,refLogs:l,stats:f}=a,c=Object.keys(v),h=c.filter(n=>!r||n.toLowerCase().includes(r.toLowerCase())||v[n]?.handler?.toLowerCase().includes(r.toLowerCase())),y=l.filter(n=>!r||n.actionRefName.toLowerCase().includes(r.toLowerCase())||n.resolvedHandler.toLowerCase().includes(r.toLowerCase()));return e.jsxs("div",{className:"g7dt-space-y-4",children:[e.jsxs("div",{className:"g7dt-grid g7dt-grid-cols-3 g7dt-gap-2",children:[e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value",children:f.totalDefinitions}),e.jsx("div",{className:"g7dt-stat-label",children:"정의"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value",children:f.totalRefs}),e.jsx("div",{className:"g7dt-stat-label",children:"참조"})]}),e.jsxs("div",{className:"g7dt-stat-card",children:[e.jsx("div",{className:"g7dt-stat-value",style:{color:f.unusedDefinitions.length>0?"#f59e0b":"#10b981"},children:f.unusedDefinitions.length}),e.jsx("div",{className:"g7dt-stat-label",children:"미사용"})]})]}),e.jsxs("div",{className:"g7dt-flex g7dt-gap-2",children:[e.jsxs("button",{onClick:()=>s("definitions"),className:t==="definitions"?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:["정의 목록 (",c.length,")"]}),e.jsxs("button",{onClick:()=>s("refs"),className:t==="refs"?"g7dt-filter-btn g7dt-filter-active":"g7dt-filter-btn",children:["참조 이력 (",l.length,")"]}),e.jsx("button",{onClick:p,className:"g7dt-filter-btn",title:"새로고침",children:"↻"})]}),e.jsx("input",{type:"text",placeholder:t==="definitions"?"액션 이름 검색...":"actionRef 검색...",value:r,onChange:n=>o(n.target.value),className:"g7dt-input g7dt-w-full"}),t==="definitions"&&e.jsx("div",{className:"g7dt-space-y-1",children:h.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:c.length===0?"정의된 named_actions가 없습니다":"검색 결과 없음"}):h.map(n=>{const i=v[n],u=f.refCountByName[n]||0,m=f.unusedDefinitions.includes(n);return e.jsxs("div",{className:"g7dt-card",style:{borderLeft:m?"3px solid #f59e0b":"3px solid #10b981"},children:[e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-font-bold",children:n}),e.jsxs("span",{className:"g7dt-text-muted g7dt-ml-2",children:["→ ",i.handler]})]}),e.jsxs("span",{className:"g7dt-badge",title:"참조 횟수",children:[u,"회"]})]}),i.params&&e.jsxs("details",{className:"g7dt-mt-1",children:[e.jsx("summary",{className:"g7dt-text-xs g7dt-text-muted g7dt-cursor-pointer",children:"params"}),e.jsx("pre",{className:"g7dt-pre g7dt-text-xs g7dt-mt-1",style:{maxHeight:"200px",overflow:"auto"},children:JSON.stringify(i.params,null,2)})]}),m&&e.jsx("div",{className:"g7dt-text-xs",style:{color:"#f59e0b",marginTop:"4px"},children:"⚠ 미사용 정의"})]},n)})}),t==="refs"&&e.jsx("div",{className:"g7dt-space-y-1",children:y.length===0?e.jsx("div",{className:"g7dt-empty-message g7dt-py-4",children:l.length===0?"actionRef 해석 이력이 없습니다":"검색 결과 없음"}):[...y].reverse().map(n=>e.jsx("div",{className:"g7dt-card",children:e.jsxs("div",{className:"g7dt-flex g7dt-justify-between g7dt-items-center",children:[e.jsxs("div",{children:[e.jsx("span",{className:"g7dt-font-bold",children:n.actionRefName}),e.jsxs("span",{className:"g7dt-text-muted g7dt-ml-2",children:["→ ",n.resolvedHandler]})]}),e.jsx("span",{className:"g7dt-text-xs g7dt-text-muted",children:new Date(n.timestamp).toLocaleTimeString()})]})},n.id))})]})},ge=()=>{x.useEffect(()=>{ve()},[]);const d=x.useRef(oe()),[t,s]=x.useState(d.current.isOpen??!1),[a,g]=x.useState(!1),[r,o]=x.useState("state"),[p,v]=x.useState({_global:{},_local:{},_computed:{}}),[l,f]=x.useState([]),[c,h]=x.useState({hits:0,misses:0,entries:0,hitRate:0}),[y,n]=x.useState(!1),[i,u]=x.useState(d.current.position??{x:-1,y:-1}),[m,k]=x.useState(!1),b=x.useRef(null),N=x.useRef(null),[C,w]=x.useState(d.current.size??{width:Ne,height:ue}),[S,z]=x.useState(!1),[O,K]=x.useState(null),L=x.useRef(null),[V,he]=x.useState(!1),q=x.useRef(null),I=G.getInstance(),se=R();x.useEffect(()=>{if(i.x===-1&&i.y===-1){const T=window.innerWidth-C.width-16;u({x:T,y:16})}},[i,C.width]),x.useEffect(()=>{if(i.x===-1&&i.y===-1||m||S)return;const T=setTimeout(()=>{ie({position:i,size:C})},300);return()=>clearTimeout(T)},[i,C,m,S]),x.useEffect(()=>{ie({isOpen:t})},[t]);const Ke=x.useCallback(T=>{T.target.tagName!=="BUTTON"&&(k(!0),b.current={x:T.clientX,y:T.clientY,posX:i.x,posY:i.y},T.preventDefault())},[i]);x.useEffect(()=>{if(!m)return;const T=_=>{if(!b.current)return;const Y=_.clientX-b.current.x,X=_.clientY-b.current.y;let j=b.current.posX+Y,A=b.current.posY+X;const H=window.innerWidth-C.width,F=window.innerHeight-C.height;j=Math.max(0,Math.min(j,H)),A=Math.max(0,Math.min(A,F)),u({x:j,y:A})},D=()=>{k(!1),b.current=null};return document.addEventListener("mousemove",T),document.addEventListener("mouseup",D),()=>{document.removeEventListener("mousemove",T),document.removeEventListener("mouseup",D)}},[m,C.width,C.height]);const M=x.useCallback(T=>D=>{D.preventDefault(),D.stopPropagation(),z(!0),K(T),L.current={x:D.clientX,y:D.clientY,width:C.width,height:C.height,posX:i.x,posY:i.y}},[C,i]);x.useEffect(()=>{if(!S||!O)return;const T=_=>{if(!L.current)return;const Y=_.clientX-L.current.x,X=_.clientY-L.current.y;let j=L.current.width,A=L.current.height,H=L.current.posX,F=L.current.posY;if(O.includes("e")&&(j=Math.max(re,L.current.width+Y)),O.includes("w")){const B=Math.min(Y,L.current.width-re);j=L.current.width-B,H=L.current.posX+B}if(O.includes("s")&&(A=Math.max(ce,L.current.height+X)),O.includes("n")){const B=Math.min(X,L.current.height-ce);A=L.current.height-B,F=L.current.posY+B}j=Math.min(j,window.innerWidth-H),A=Math.min(A,window.innerHeight-F),H=Math.max(0,H),F=Math.max(0,F),w({width:j,height:A}),u({x:H,y:F})},D=()=>{z(!1),K(null),L.current=null};return document.addEventListener("mousemove",T),document.addEventListener("mouseup",D),()=>{document.removeEventListener("mousemove",T),document.removeEventListener("mouseup",D)}},[S,O]);const qe=x.useCallback(()=>{V?(q.current&&(u(q.current.position),w(q.current.size)),he(!1)):(q.current={position:i,size:C},u({x:0,y:0}),w({width:window.innerWidth,height:window.innerHeight}),he(!0))},[V,i,C]),U=x.useCallback(()=>{I.isEnabled()&&(v(I.getState()),f(I.getActionHistory()),h(I.getCacheStats()),n(se.isConnected()))},[I,se]);x.useEffect(()=>{const T=D=>{D.ctrlKey&&D.shiftKey&&D.key==="G"&&(D.preventDefault(),s(_=>!_))};return window.addEventListener("keydown",T),()=>window.removeEventListener("keydown",T)},[]),x.useEffect(()=>{if(!t||!I.isEnabled())return;U();const T=I.watchState("*",()=>{v(I.getState())}),D=I.watchActions(()=>{f(I.getActionHistory()),h(I.getCacheStats())}),_=setInterval(U,5e3);return()=>{T(),D(),clearInterval(_)}},[t,I,U]);const Ue=async()=>{try{const T=await se.dumpState();console.log("[G7DevTools] 상태 덤프 완료:",T),n(!0)}catch(T){console.error("[G7DevTools] 상태 덤프 실패:",T),n(!1)}};return I.isEnabled()?t?a?e.jsx("div",{className:"g7dt-minimized",children:e.jsxs("div",{className:"g7dt-minimized-content",children:[e.jsx("span",{className:"g7dt-font-bold g7dt-text-sm",children:"G7 DevTools"}),e.jsx("button",{onClick:()=>g(!1),className:"g7dt-btn g7dt-text-xs",title:"확장",children:"▲"}),e.jsx("button",{onClick:()=>s(!1),className:"g7dt-btn g7dt-text-xs",children:"✕"})]})}):e.jsxs("div",{ref:N,className:`g7dt-panel ${V?"maximized":""} ${S?"g7dt-resizing":""}`,style:{left:`${i.x>=0?i.x:0}px`,top:`${i.y>=0?i.y:0}px`,width:`${C.width}px`,height:`${C.height}px`},children:[!V&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-corner g7dt-resize-nw",onMouseDown:M("nw")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-corner g7dt-resize-ne",onMouseDown:M("ne")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-corner g7dt-resize-sw",onMouseDown:M("sw")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-corner g7dt-resize-se",onMouseDown:M("se")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-edge-h g7dt-resize-n",onMouseDown:M("n")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-edge-h g7dt-resize-s",onMouseDown:M("s")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-edge-v g7dt-resize-w",onMouseDown:M("w")}),e.jsx("div",{className:"g7dt-resize-handle g7dt-resize-edge-v g7dt-resize-e",onMouseDown:M("e")})]}),e.jsxs("div",{className:`g7dt-header ${m?"dragging":""}`,onMouseDown:Ke,children:[e.jsxs("div",{className:"g7dt-header-left",children:[e.jsx("span",{className:"g7dt-header-title",children:"G7 DevTools"}),e.jsx("span",{className:`g7dt-header-badge ${y?"connected":""}`,title:y?"서버 덤프 성공":"서버에 덤프되지 않음 (덤프 버튼 클릭)",children:y?"덤프 완료":"로컬 모드"})]}),e.jsxs("div",{className:"g7dt-header-right",children:[e.jsx("button",{onClick:Ue,className:"g7dt-btn g7dt-btn-primary",title:"서버로 상태 덤프",children:"덤프"}),e.jsx("button",{onClick:U,className:"g7dt-btn",title:"새로고침",children:"↻"}),e.jsx("button",{onClick:qe,className:"g7dt-btn",title:V?"복원":"최대화",children:V?"❐":"□"}),e.jsx("button",{onClick:()=>g(!0),className:"g7dt-btn",title:"최소화",children:"▼"}),e.jsx("button",{onClick:()=>s(!1),className:"g7dt-btn",children:"✕"})]})]}),e.jsx("div",{className:"g7dt-tabs",children:Object.keys(le).map(T=>e.jsx("button",{onClick:()=>o(T),className:`g7dt-tab ${r===T?"active":""}`,children:le[T]},T))}),e.jsxs("div",{className:"g7dt-content",children:[r==="state"&&e.jsx(ke,{state:p,devTools:I}),r==="actions"&&e.jsx(Ce,{actions:l,devTools:I}),r==="cache"&&e.jsx(Se,{stats:c,devTools:I}),r==="diagnose"&&e.jsx(Ie,{}),r==="performance"&&e.jsx(Te,{devTools:I}),r==="lifecycle"&&e.jsx(De,{devTools:I}),r==="network"&&e.jsx(Le,{devTools:I}),r==="form"&&e.jsx(Ee,{devTools:I}),r==="conditional"&&e.jsx($e,{devTools:I}),r==="expression"&&e.jsx(ze,{devTools:I}),r==="handlers"&&e.jsx(_e,{devTools:I}),r==="events"&&e.jsx(je,{devTools:I}),r==="datasources"&&e.jsx(Ae,{devTools:I}),r==="renders"&&e.jsx(Oe,{devTools:I}),r==="styles"&&e.jsx(Me,{devTools:I}),r==="auth"&&e.jsx(Pe,{devTools:I}),r==="logs"&&e.jsx(He,{devTools:I}),r==="layout"&&e.jsx(Fe,{devTools:I}),r==="changedetection"&&e.jsx(We,{devTools:I}),r==="staleclosure"&&e.jsx(Ve,{devTools:I}),r==="nestedcontext"&&e.jsx(Be,{devTools:I}),r==="modalscope"&&e.jsx(Ge,{devTools:I}),r==="namedactions"&&e.jsx(Je,{devTools:I})]}),e.jsxs("div",{className:"g7dt-footer",children:[e.jsx("span",{children:"Ctrl+Shift+G 토글 | 헤더 드래그로 이동"}),e.jsxs("span",{children:["액션: ",l.length," | 캐시 히트율: ",(c.hitRate*100).toFixed(1),"%"]})]})]}):e.jsx("button",{onClick:()=>s(!0),className:"g7dt-floating-btn",title:"G7 DevTools (Ctrl+Shift+G)",children:e.jsxs("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",children:[e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"}),e.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M15 12a3 3 0 11-6 0 3 3 0 016 0z"})]})}):t?e.jsxs("div",{className:"g7dt-disabled-notice",children:[e.jsxs("div",{className:"g7dt-flex g7dt-items-center g7dt-justify-between g7dt-mb-2",children:[e.jsx("span",{className:"g7dt-font-bold g7dt-text-yellow",children:"G7 DevTools"}),e.jsx("button",{onClick:()=>s(!1),className:"g7dt-btn",children:"✕"})]}),e.jsx("p",{className:"g7dt-text-sm g7dt-text-gray",children:"DevTools가 비활성화되어 있습니다."}),e.jsx("p",{className:"g7dt-text-xs g7dt-text-gray-dark g7dt-mt-2",children:"환경설정 > 고급 설정 > 디버그 모드를 켜세요."})]}):null};if(typeof window<"u"){const d=window.G7Core||(window.G7Core={});if(d.__devtools={DiagnosticEngine:Z,getServerConnector:R,getStyleTracker:ae,DevToolsPanel:ge},typeof d.__onDevToolsReady=="function")try{d.__onDevToolsReady()}catch{}}$.DevToolsPanel=ge,$.DiagnosticEngine=Z,$.getServerConnector=R,$.getStyleTracker=ae,Object.defineProperty($,Symbol.toStringTag,{value:"Module"})})(this.G7DevToolsBundle=this.G7DevToolsBundle||{},ReactJSXRuntime,React); -//# sourceMappingURL=devtools.min.js.map diff --git a/public/build/core/layout-editor.min.js b/public/build/core/layout-editor.min.js index 56b70347..ed5fd8bb 100644 --- a/public/build/core/layout-editor.min.js +++ b/public/build/core/layout-editor.min.js @@ -1,14 +1,13 @@ -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.editMode,selectedRoute:{path:`__iteration__/${t.sourcePath}`,layoutName:t.hostLayout??e.selectedRoute?.layoutName??null}};case"EXIT_ITERATION_ITEM_EDIT":return{...e,editMode:e.returnEditMode??"route",selectedRoute:e.returnRoute,returnRoute:null,returnEditMode:null};case"SET_PREVIEW_DEVICE":return{...e,previewDevice:t.device};case"SET_PREVIEW_ZOOM":return{...e,previewZoom:t.zoom};case"SET_PREVIEW_CUSTOM_WIDTH":return{...e,previewCustomWidth:t.width};case"SET_PREVIEW_COLOR_SCHEME":return{...e,previewColorScheme:t.scheme};case"TOGGLE_PALETTE":return{...e,isPaletteOpen:!e.isPaletteOpen};case"SET_PALETTE_OPEN":return{...e,isPaletteOpen:t.open};case"SET_AVAILABLE_STATES":return{...e,availableStates:t.states,activeStateId:t.activeStateId};case"SET_ACTIVE_STATE":return{...e,activeStateId:t.activeStateId};case"SET_LAYOUT_VERSIONS":return{...e,layoutVersions:t.versions};case"SET_LAYOUT_VERSION":return{...e,layoutVersions:{...e.layoutVersions,[t.layoutName]:t.version}};case"SET_EXTENSION_VERSIONS":return{...e,extensionVersions:t.versions};case"SET_EXTENSION_VERSION":return{...e,extensionVersions:{...e.extensionVersions,[t.extensionId]:t.version}};default:return e}}function sy(e,t){const n=iy(e,t);return((n.selectedRoute?.path??null)!==(e.selectedRoute?.path??null)||n.editMode!==e.editMode)&&(n.availableStates.length>0||n.activeStateId!==null)?{...n,availableStates:[],activeStateId:null}:n}const Il=u.createContext(null);function ay(e,t){const n=sy(e,t);return Sl(n,t.type),n}function ly({templateIdentifier:e,initialLocale:t,children:n}){const[o,r]=u.useReducer(ay,void 0,()=>{const a=ry(e,t);return Sl(a,void 0),a});u.useEffect(()=>{oy(o.isRouteTreeCollapsed)},[o.isRouteTreeCollapsed]);const s=u.useMemo(()=>({state:o,dispatch:r}),[o]);return u.createElement(Il.Provider,{value:s},n)}function Je(){const e=u.useContext(Il);if(!e)throw new Error("useLayoutEditor must be used within LayoutEditorProvider");return e}const Al=u.createContext(null);function dy({value:e,children:t}){return u.createElement(Al.Provider,{value:e},t)}function Ft(){return u.useContext(Al)}const zi=u.createContext(null);let cy=1;function uy({children:e,t}){const[n,o]=u.useState([]),r=u.useRef([]),[s,a]=u.useState({}),l=u.useCallback(b=>{const x=b.id??`editor-modal-${cy++}`,k={id:x,content:b.content,ariaLabel:b.ariaLabel??"",closeOnBackdrop:b.closeOnBackdrop??!0,closeOnEscape:b.closeOnEscape??!0,width:b.width??"auto",height:b.height??"auto",maxHeightRatio:b.maxHeightRatio??.85,draggable:b.draggable??!1,avoidRect:b.avoidRect,onClose:b.onClose};return o(w=>{const C=[...w.filter(E=>E.id!==x),k];return r.current=C,C}),x},[]),d=u.useCallback(b=>{o(x=>{const k=x.find(_=>_.id===b),w=x.filter(_=>_.id!==b);return r.current=w,k?.onClose&&queueMicrotask(()=>k.onClose?.()),w})},[]),c=u.useCallback(()=>{const b=r.current[r.current.length-1];b&&d(b.id)},[d]),p=u.useCallback(()=>{o(b=>{for(let x=b.length-1;x>=0;x--){const k=b[x];k?.onClose&&queueMicrotask(()=>k.onClose?.())}return r.current=[],[]})},[]),f=u.useCallback((b,x)=>{a(k=>{const w=r.current.find(C=>C.id===b),_=x??w?.ariaLabel??"";return{...k,[b]:_}})},[]),y=u.useCallback(b=>{a(x=>{if(!(b in x))return x;const k={...x};return delete k[b],k})},[]),g=u.useCallback(b=>{y(b),d(b)},[y,d]),m=u.useCallback(()=>{a({}),p()},[p]),h=u.useMemo(()=>Object.keys(s),[s]),v=u.useMemo(()=>({open:l,close:g,closeTop:c,closeAll:m,stack:n,minimize:f,restore:y,minimizedStack:h}),[l,g,c,m,n,f,y,h]);return u.createElement(zi.Provider,{value:v},e,u.createElement(fy,{minimized:s,onRestore:y,t,key:"__min_bar"}))}function yo(){const e=u.useContext(zi);if(!e)throw new Error("useEditorModal must be used within EditorModalProvider");return e}function py(){const e=u.useContext(zi);if(!e)return null;const{stack:t,close:n,minimizedStack:o}=e,r=u.useMemo(()=>new Set(o),[o]);return u.useEffect(()=>{if(t.length===0)return;const s=a=>{if(a.key!=="Escape"&&a.key!=="Esc")return;const l=t.filter(c=>!r.has(c.id)),d=l[l.length-1];d?.closeOnEscape&&(a.preventDefault(),a.stopPropagation(),n(d.id))};return window.addEventListener("keydown",s,!0),()=>window.removeEventListener("keydown",s,!0)},[t,n,r]),t.length===0?null:i.jsx(i.Fragment,{children:t.map((s,a)=>i.jsx(hy,{entry:s,depth:a,hidden:r.has(s.id),onClose:()=>n(s.id)},s.id))})}function fy({minimized:e,onRestore:t,t:n}){const o=n??(s=>s),r=Object.keys(e);return r.length===0?null:i.jsx("div",{className:"g7le-minimized-bar","data-testid":"g7le-minimized-bar",style:{position:"fixed",left:16,bottom:16,display:"flex",gap:8,zIndex:9300},children:r.map(s=>i.jsxs("button",{type:"button","data-testid":`g7le-minimized-${s}`,onClick:()=>t(s),style:{display:"flex",alignItems:"center",gap:6,padding:"8px 12px",background:"#1e293b",color:"#fff",border:"none",borderRadius:8,boxShadow:"0 8px 24px rgba(15, 23, 42, 0.3)",cursor:"pointer",fontSize:13},children:[i.jsx("span",{"aria-hidden":"true",children:"🗗"}),i.jsx("span",{children:e[s]||o("layout_editor.modal.minimized_default")}),i.jsx("span",{style:{opacity:.7},children:o("layout_editor.modal.restore")})]},s))})}function gy(e,t,n){if(!e||typeof window>"u")return null;const o=window.innerWidth,r=window.innerHeight,s=16,a=e.left,l=o-(e.left+e.width),d=t+s;let c;if(l>=a&&l>=d)c=e.left+e.width+s;else if(a>=d)c=Math.max(s,e.left-s-t);else return null;c=Math.min(Math.max(s,c),Math.max(s,o-t-s));let p=e.top;return p=Math.min(Math.max(s,p),Math.max(s,r-n-s)),{left:c,top:p}}const Tl="clamp(16px, 6vh, 56px)";function hy({entry:e,depth:t,hidden:n,onClose:o}){const r=9100+t*10,s=e.maxHeightRatio>0?`${e.maxHeightRatio*100}vh`:void 0,a=e.draggable?s:`calc(100vh - (${Tl}) * 2)`,[l,d]=u.useState(null),c=u.useRef(null),p=u.useRef(null);u.useEffect(()=>{if(!e.draggable)return;const v=c.current;if(!v)return;const b=v.getBoundingClientRect(),x=gy(e.avoidRect,b.width,b.height);x&&d(x)},[e.id,e.draggable]);const f=u.useCallback(v=>{if(!e.draggable)return;const b=v.target;if(!b.closest("[data-modal-drag-handle]")||b.closest("button, a, input, textarea, select"))return;const k=c.current,w=k?.getBoundingClientRect(),_=l?.left??w?.left??0,C=l?.top??w?.top??0;p.current={startX:v.clientX,startY:v.clientY,baseLeft:_,baseTop:C};const E=T=>{const $=p.current;if(!$)return;const A=window.innerWidth,z=window.innerHeight,D=k?.offsetWidth??0,O=k?.offsetHeight??0;let M=$.baseLeft+(T.clientX-$.startX),B=$.baseTop+(T.clientY-$.startY);M=Math.min(Math.max(0,M),Math.max(0,A-D)),B=Math.min(Math.max(0,B),Math.max(0,z-O)),d({left:M,top:B})},S=()=>{p.current=null,window.removeEventListener("pointermove",E),window.removeEventListener("pointerup",S)};window.addEventListener("pointermove",E),window.addEventListener("pointerup",S)},[e.draggable,l]),y=v=>{e.closeOnBackdrop&&v.target===v.currentTarget&&o()},g=e.draggable&&l!==null,m=e.draggable?{position:"fixed",inset:0,background:"transparent",pointerEvents:"none",zIndex:r,...n?{display:"none"}:{}}:{position:"fixed",inset:0,background:"rgba(15, 23, 42, 0.45)",display:n?"none":"flex",alignItems:"flex-start",justifyContent:"center",paddingTop:Tl,boxSizing:"border-box",zIndex:r},h={background:"#fff",borderRadius:10,boxShadow:"0 20px 60px rgba(15, 23, 42, 0.25)",width:e.width,height:e.height,maxHeight:a,maxWidth:"92vw",overflow:"hidden",display:"flex",flexDirection:"column",...e.draggable?{position:"absolute",pointerEvents:"auto",left:g?l.left:"50%",top:g?l.top:"50%",transform:g?void 0:"translate(-50%, -50%)"}:{}};return i.jsx("div",{className:"g7le-modal-backdrop",role:"presentation","data-testid":`g7le-modal-backdrop-${e.id}`,"data-draggable":e.draggable?"true":"false",onClick:y,style:m,children:i.jsx("div",{ref:c,className:"g7le-modal",role:"dialog","aria-modal":"true","aria-label":e.ariaLabel||void 0,"data-testid":`g7le-modal-${e.id}`,"data-positioned":g?"true":"false",onClick:v=>v.stopPropagation(),onPointerDown:e.draggable?f:void 0,style:h,children:e.content})})}function wt({baseStyle:e,hoverStyle:t,disabled:n,style:o,children:r,...s}){const[a,l]=u.useState(!1),d={outline:"none",...e,...o,...a&&!n?t:{}};return i.jsx("button",{type:"button",disabled:n,onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),style:d,...s,children:r})}function yy({templateIdentifier:e,templateName:t,templateVersion:n,templateList:o,onSwitch:r}){const{t:s}=tt(),[a,l]=u.useState(!1),[d,c]=u.useState(!1),p=u.useRef(null);u.useEffect(()=>{if(!a)return;const v=x=>{p.current&&!p.current.contains(x.target)&&l(!1)},b=x=>{x.key==="Escape"&&l(!1)};return document.addEventListener("mousedown",v),document.addEventListener("keydown",b),()=>{document.removeEventListener("mousedown",v),document.removeEventListener("keydown",b)}},[a]);const f=o??[],y=f.some(v=>v.identifier!==e),g=!!r&&y,m=t||e,h=v=>{if(v===e){l(!1);return}l(!1),r?.(v)};return i.jsxs("div",{ref:p,className:"g7le-toolbar__template-switcher",style:{position:"relative",display:"inline-flex"},children:[i.jsxs("button",{type:"button",className:"g7le-toolbar__template","data-testid":"g7le-toolbar-template-label","data-can-switch":g?"true":"false","aria-haspopup":g?"listbox":void 0,"aria-expanded":g?a:void 0,disabled:!g,onClick:()=>g&&l(v=>!v),onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),title:g?s("layout_editor.chrome.toolbar.switch_template"):m,style:{display:"inline-flex",alignItems:"center",gap:6,padding:"4px 8px",fontSize:13,fontWeight:600,color:"#0f172a",letterSpacing:.1,borderRadius:6,borderWidth:1,borderStyle:"solid",borderColor:g&&(d||a)?"#cbd5e1":"transparent",backgroundColor:g&&(d||a)?"#f1f5f9":"transparent",cursor:g?"pointer":"default",outline:"none",transition:"background-color 120ms, border-color 120ms"},children:[i.jsx("span",{className:"g7le-toolbar__template-name","data-testid":"g7le-toolbar-template-name",children:m}),n&&i.jsxs("span",{className:"g7le-toolbar__template-version","data-testid":"g7le-toolbar-template-version",style:{fontSize:11,fontWeight:500,color:"#64748b",padding:"1px 6px",borderRadius:999,background:"#f1f5f9",border:"1px solid #e2e8f0"},children:["v",n]}),g&&i.jsx("span",{className:"g7le-toolbar__template-caret","aria-hidden":"true",style:{fontSize:9,color:"#94a3b8",marginLeft:1},children:"▼"})]}),g&&a&&i.jsx("div",{className:"g7le-toolbar__template-menu","data-testid":"g7le-toolbar-template-menu",role:"listbox",style:{position:"absolute",top:"calc(100% + 4px)",left:0,minWidth:240,maxWidth:360,maxHeight:360,overflowY:"auto",background:"#ffffff",border:"1px solid #e2e8f0",borderRadius:8,boxShadow:"0 8px 24px rgba(15, 23, 42, 0.14)",zIndex:30,padding:4},children:f.map(v=>{const b=v.identifier===e;return i.jsxs("button",{type:"button",role:"option","aria-selected":b,"aria-current":b?"true":void 0,className:"g7le-toolbar__template-menu-item","data-testid":"g7le-toolbar-template-menu-item","data-identifier":v.identifier,"data-current":b?"true":"false",onClick:()=>h(v.identifier),style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,width:"100%",padding:"8px 10px",fontSize:13,color:"#1e293b",background:b?"#eff6ff":"transparent",border:"none",borderRadius:6,cursor:b?"default":"pointer",textAlign:"left",outline:"none"},onMouseEnter:x=>{b||(x.currentTarget.style.background="#f1f5f9")},onMouseLeave:x=>{x.currentTarget.style.background=b?"#eff6ff":"transparent"},children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:8,minWidth:0},children:[i.jsx("span",{"aria-hidden":"true",style:{width:12,flex:"0 0 auto",color:"#2563eb",fontSize:12,textAlign:"center"},children:b?"✓":""}),i.jsxs("span",{style:{display:"flex",flexDirection:"column",gap:2,minWidth:0},children:[i.jsx("span",{style:{fontWeight:600,color:b?"#1d4ed8":"#1e293b",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:v.name||v.identifier}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:v.identifier})]})]}),i.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:6,flex:"0 0 auto"},children:[i.jsx("span",{style:{fontSize:10,fontWeight:600,color:v.type==="admin"?"#7c3aed":"#0891b2",textTransform:"uppercase",letterSpacing:.4},children:v.type}),v.version&&i.jsxs("span",{style:{fontSize:11,color:"#64748b"},children:["v",v.version]})]})]},v.identifier)})})]})}function by(e={}){const{onSave:t,isDirty:n=!1,onExit:o,onEditCode:r,onManageImages:s,onManageTranslations:a,onPageSettings:l,onReset:d,onShowShortcuts:c,onPreview:p,onShowVersions:f,templateName:y,templateVersion:g,templateList:m,onSwitchTemplate:h}=e,[v,b]=u.useState(!1),[x,k]=u.useState(!1),{t:w}=tt(),{state:_,dispatch:C}=Je(),[E,S]=u.useState(null);u.useEffect(()=>{const I=()=>{const F=window.__g7LayoutEditorHistory;S(F?{undo:F.undo,redo:F.redo,canUndo:!!F.canUndo,canRedo:!!F.canRedo}:null)};I();const P=window.setInterval(I,250);return()=>window.clearInterval(P)},[]);const T=()=>{C({type:"TOGGLE_ROUTE_TREE"})},$=()=>_.editMode==="base"?w("layout_editor.chrome.toolbar.mode_badge.base"):_.editMode==="modal"?w("layout_editor.chrome.toolbar.mode_badge.modal"):_.editMode==="extension"?w("layout_editor.chrome.toolbar.mode_badge.extension"):_.editMode==="iteration_item"?w("layout_editor.chrome.toolbar.mode_badge.iteration_item"):null,A=()=>{_.editMode==="base"?C({type:"EXIT_BASE_EDIT"}):_.editMode==="modal"?C({type:"EXIT_MODAL_EDIT"}):_.editMode==="extension"?C({type:"EXIT_EXTENSION_EDIT"}):_.editMode==="iteration_item"&&C({type:"EXIT_ITERATION_ITEM_EDIT"})},z=$(),D=_.editMode!=="route",O={display:"inline-flex",alignItems:"center",gap:6,padding:"6px 12px",fontSize:13,fontWeight:500,borderRadius:6,borderWidth:1,borderStyle:"solid",borderColor:"transparent",backgroundColor:"transparent",color:"#475569",cursor:"pointer",transition:"background-color 120ms, color 120ms, border-color 120ms"},M={...O,backgroundColor:"#2563eb",color:"#ffffff",borderColor:"#2563eb",fontWeight:600},B={...O,borderColor:"#e2e8f0",backgroundColor:"#ffffff",color:"#0f172a"},N={backgroundColor:"#f1f5f9",color:"#0f172a",borderColor:"#cbd5e1"},H={backgroundColor:"#1d4ed8",borderColor:"#1d4ed8"},L={backgroundColor:"#f8fafc",borderColor:"#cbd5e1"};return i.jsxs("div",{className:"g7le-toolbar","data-testid":"g7le-toolbar",role:"toolbar",style:{display:"flex",alignItems:"center",gap:6,padding:"8px 12px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",boxShadow:"0 1px 2px rgba(15, 23, 42, 0.04)"},children:[i.jsx("style",{children:"@keyframes g7le-spin { to { transform: rotate(360deg); } }.g7le-toolbar > * { flex-shrink: 0; white-space: nowrap; }"}),i.jsx("button",{type:"button",className:"g7le-toolbar__route-tree-toggle","data-testid":"g7le-toolbar-toggle-route-tree","aria-label":w("layout_editor.chrome.toolbar.toggle_route_tree"),title:w("layout_editor.chrome.toolbar.toggle_route_tree"),onClick:T,style:{...O,outline:"none",padding:"6px 10px",fontSize:16,lineHeight:1,color:"#64748b"},children:"☰"}),i.jsx(yy,{templateIdentifier:_.templateIdentifier,templateName:y,templateVersion:g,templateList:m,onSwitch:h}),z&&i.jsx("span",{className:"g7le-toolbar__mode-badge","data-testid":"g7le-toolbar-mode-badge","data-mode":_.editMode,style:{padding:"2px 8px",borderRadius:999,background:"#fef3c7",color:"#92400e",fontSize:11,fontWeight:600,border:"1px solid #fde68a"},children:z}),i.jsx("div",{style:{flex:1}}),i.jsxs(i.Fragment,{children:[i.jsx(wt,{"data-testid":"g7le-toolbar-undo","aria-label":w("layout_editor.chrome.toolbar.undo"),title:w("layout_editor.chrome.toolbar.undo"),disabled:!E?.canUndo,onClick:()=>E?.undo?.(),baseStyle:O,hoverStyle:N,style:{opacity:E?.canUndo?1:.4},children:"↶"}),i.jsx(wt,{"data-testid":"g7le-toolbar-redo","aria-label":w("layout_editor.chrome.toolbar.redo"),title:w("layout_editor.chrome.toolbar.redo"),disabled:!E?.canRedo,onClick:()=>E?.redo?.(),baseStyle:O,hoverStyle:N,style:{opacity:E?.canRedo?1:.4},children:"↷"}),i.jsxs(wt,{"data-testid":"g7le-toolbar-add-element","data-open":_.isPaletteOpen?"true":"false",disabled:!_.selectedRoute,onClick:()=>C({type:"TOGGLE_PALETTE"}),"aria-pressed":_.isPaletteOpen,baseStyle:O,hoverStyle:N,style:_.isPaletteOpen?{backgroundColor:"#eff6ff",color:"#1d4ed8",borderColor:"#93c5fd"}:void 0,children:["+ ",w("layout_editor.chrome.toolbar.add_element")]}),i.jsx(wt,{"data-testid":"g7le-toolbar-preview","data-previewing":x?"true":"false",disabled:!p||!_.selectedRoute?.layoutName||x,onClick:()=>{const I=p?.();I&&typeof I.then=="function"&&(k(!0),I.finally(()=>k(!1)))},baseStyle:O,hoverStyle:N,style:{opacity:x?.7:1},children:x?i.jsxs(i.Fragment,{children:[i.jsx("span",{"data-testid":"g7le-toolbar-preview-spinner","aria-hidden":"true",style:{display:"inline-block",width:12,height:12,border:"2px solid rgba(71,85,105,0.35)",borderTopColor:"#475569",borderRadius:"50%",animation:"g7le-spin 600ms linear infinite"}}),w("layout_editor.chrome.toolbar.previewing")]}):i.jsxs(i.Fragment,{children:["👁 ",w("layout_editor.chrome.toolbar.preview")]})}),i.jsxs(wt,{"data-testid":"g7le-toolbar-versions",disabled:!f||!(_.selectedRoute?.layoutName||_.selectedRoute?.path?.startsWith("__extension__/")),onClick:()=>f?.(),"aria-label":w("layout_editor.chrome.toolbar.version_history"),title:w("layout_editor.chrome.toolbar.version_history"),baseStyle:O,hoverStyle:N,children:["🕘 ",w("layout_editor.chrome.toolbar.version_history")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-edit-code",disabled:!r,onClick:()=>r?.(),baseStyle:O,hoverStyle:N,children:["⟨/⟩ ",w("layout_editor.chrome.toolbar.edit_code")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-images",disabled:!s||!_.selectedRoute,onClick:()=>s?.(),baseStyle:O,hoverStyle:N,children:["🖼 ",w("layout_editor.chrome.toolbar.images")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-translations",disabled:!a||!_.selectedRoute,onClick:()=>a?.(),baseStyle:O,hoverStyle:N,children:["🌐 ",w("layout_editor.chrome.toolbar.translations")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-page-settings",disabled:!l||!_.selectedRoute||_.editMode==="extension"||_.editMode==="iteration_item",onClick:()=>l?.(),baseStyle:O,hoverStyle:N,children:["⚙ ",w("layout_editor.chrome.toolbar.page_settings")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-reset",disabled:!d||!n,onClick:()=>d?.(),"aria-label":w("layout_editor.chrome.toolbar.reset"),title:w("layout_editor.chrome.toolbar.reset"),baseStyle:O,hoverStyle:N,style:{opacity:n?1:.4},children:["↺ ",w("layout_editor.chrome.toolbar.reset")]}),i.jsx(wt,{"data-testid":"g7le-toolbar-save","data-dirty":n?"true":"false","data-saving":v?"true":"false",disabled:!t||v,onClick:()=>{const I=t?.();I&&typeof I.then=="function"&&(b(!0),I.finally(()=>b(!1)))},baseStyle:M,hoverStyle:H,style:{opacity:v?.7:n?1:.7},children:v?i.jsxs(i.Fragment,{children:[i.jsx("span",{"data-testid":"g7le-toolbar-save-spinner","aria-hidden":"true",className:"g7le-toolbar__spinner",style:{display:"inline-block",width:12,height:12,border:"2px solid rgba(255,255,255,0.4)",borderTopColor:"#ffffff",borderRadius:"50%",animation:"g7le-spin 600ms linear infinite"}}),w("layout_editor.chrome.toolbar.saving")]}):i.jsxs(i.Fragment,{children:["💾 ",w("layout_editor.chrome.toolbar.save")]})}),i.jsx(wt,{"data-testid":"g7le-toolbar-shortcuts",disabled:!c,onClick:()=>c?.(),"aria-label":w("layout_editor.shortcuts.title"),title:w("layout_editor.shortcuts.title"),baseStyle:O,hoverStyle:N,children:"⌨"}),i.jsxs(wt,{"data-testid":"g7le-toolbar-exit",disabled:!o,onClick:()=>o?.(),baseStyle:B,hoverStyle:L,children:["✕ ",w("layout_editor.chrome.toolbar.exit")]})]}),D&&i.jsxs(wt,{"data-testid":"g7le-toolbar-exit-alt-mode",onClick:A,baseStyle:B,hoverStyle:L,children:["← ",_.editMode==="base"&&w("layout_editor.chrome.toolbar.exit_base_edit"),_.editMode==="modal"&&w("layout_editor.chrome.toolbar.exit_modal_edit"),_.editMode==="extension"&&w("layout_editor.chrome.toolbar.exit_extension_edit"),_.editMode==="iteration_item"&&w("layout_editor.chrome.toolbar.exit_iteration_item_edit")]})]})}function ar(){if(typeof window>"u")return null;try{return window.localStorage?.getItem("auth_token")??null}catch{return null}}function je(e){const t={Accept:"application/json",...e??{}},n=ar();return n&&(t.Authorization=`Bearer ${n}`),t}function Mi(e){return encodeURIComponent(e)}async function Dl(e,t){const n=`/api/admin/templates/${Mi(e)}/layout-attachments?layout_name=${encodeURIComponent(t)}`;try{const o=await fetch(n,{method:"GET",headers:je(),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||!r?.success?{ok:!1,status:o.status,message:Li(r,o.status)}:{ok:!0,data:Array.isArray(r?.data)?r.data:[]}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}async function Nl(e,t,n){const o=`/api/admin/templates/${Mi(e)}/layout-attachments`,r=new FormData;r.append("file",n),r.append("layout_name",t);try{const s=await fetch(o,{method:"POST",headers:je(),body:r,credentials:"same-origin"}),a=await s.json().catch(()=>null);return!s.ok||!a?.success||!a?.data?.url?{ok:!1,status:s.status,message:Li(a,s.status)}:{ok:!0,data:a.data}}catch(s){return{ok:!1,status:0,message:s instanceof Error?s.message:"network error"}}}async function zl(e){const t=`/api/admin/templates/layout-attachments/${Mi(String(e))}`;try{const n=await fetch(t,{method:"DELETE",headers:je(),credentials:"same-origin"}),o=await n.json().catch(()=>null);return!n.ok||o&&o.success===!1?{ok:!1,status:n.status,message:Li(o,n.status)}:{ok:!0,data:!0}}catch(n){return{ok:!1,status:0,message:n instanceof Error?n.message:"network error"}}}function Li(e,t){if(e&&typeof e=="object"&&"message"in e){const n=e.message;if(typeof n=="string"&&n.length>0)return n}return`HTTP ${t}`}function my(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function vy(e){if(!e)return"";const t=e.match(/^(\d{4})-(\d{2})-(\d{2})/);return t?`${t[2]}-${t[3]}`:""}function Ml({templateIdentifier:e,layoutName:t,t:n,onSelect:o,onChanged:r,onClose:s}){const[a,l]=u.useState([]),[d,c]=u.useState("idle"),[p,f]=u.useState(!1),[y,g]=u.useState(null),m=u.useRef(null),h=u.useCallback(async()=>{c("loading"),g(null);const x=await Dl(e,t);x.ok?(l(x.data),c("idle")):(g(x.message),c("error"))},[e,t]);u.useEffect(()=>{h()},[h]);const v=async x=>{const k=x.target.files?.[0];if(!k)return;f(!0),g(null);const w=await Nl(e,t,k);w.ok?(l(_=>[w.data,..._.filter(C=>C.id!==w.data.id)]),r?.()):g(w.message),f(!1),m.current&&(m.current.value="")},b=async x=>{if(!(typeof window<"u"&&typeof window.confirm=="function"?window.confirm(n("layout_editor.attachment_manager.delete_confirm")):!0))return;const w=await zl(x.id);w.ok?(l(_=>_.filter(C=>C.id!==x.id)),r?.()):g(w.message)};return i.jsxs("div",{className:"g7le-attachment-manager","data-testid":"g7le-attachment-manager",style:_y,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:xy,children:[i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-attachment-manager-title",children:n("layout_editor.attachment_manager.title")}),i.jsx("button",{type:"button","aria-label":"close",onClick:s,style:wy,"data-testid":"g7le-attachment-manager-close",children:"✕"})]}),i.jsx("div",{style:ky,children:i.jsxs("label",{style:Sy,"data-testid":"g7le-attachment-upload",children:[p?"…":`⬆ ${n("layout_editor.attachment_manager.upload")}`,i.jsx("input",{ref:m,type:"file",accept:"image/*",onChange:v,disabled:p,style:{display:"none"},"data-testid":"g7le-attachment-upload-input"})]})}),y&&i.jsx("div",{style:Cy,"data-testid":"g7le-attachment-manager-error",children:y}),i.jsx("div",{style:$y,children:d==="loading"?i.jsx("div",{style:Ll,"data-testid":"g7le-attachment-manager-loading",children:n("layout_editor.attachment_manager.loading")}):a.length===0&&d!=="error"?i.jsx("div",{style:Ll,"data-testid":"g7le-attachment-manager-empty",children:n("layout_editor.attachment_manager.empty")}):i.jsx("div",{style:Ey,"data-testid":"g7le-attachment-grid",children:a.map(x=>i.jsxs("div",{style:Iy,"data-testid":`g7le-attachment-card-${x.id}`,children:[i.jsx("div",{style:{...Ay,backgroundImage:`url(${x.url})`}}),i.jsx("div",{style:Ty,title:x.original_name,children:x.original_name}),i.jsxs("div",{style:Dy,children:[my(x.size),x.created_at?` · ${vy(x.created_at)}`:""]}),i.jsxs("div",{style:Ny,children:[o&&i.jsx("button",{type:"button",onClick:()=>o(x.url),style:zy,"data-testid":`g7le-attachment-use-${x.id}`,children:n("layout_editor.attachment_manager.use_as_bg")}),i.jsx("button",{type:"button",onClick:()=>b(x),style:My,"data-testid":`g7le-attachment-delete-${x.id}`,children:"🗑"})]})]},x.id))})}),i.jsx("div",{style:Ly,children:i.jsx("button",{type:"button",onClick:s,style:Py,"data-testid":"g7le-attachment-manager-footer-close",children:n("layout_editor.attachment_manager.close")})})]})}const _y={display:"flex",flexDirection:"column",minHeight:0,flex:1},xy={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0"},wy={border:"none",background:"transparent",fontSize:16,color:"#94a3b8",cursor:"pointer"},ky={padding:"10px 16px",borderBottom:"1px solid #f1f5f9",display:"flex",gap:8},Sy={padding:"6px 12px",fontSize:13,border:"1px solid #2563eb",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",display:"inline-flex",alignItems:"center"},Cy={padding:"8px 16px",fontSize:12,color:"#b91c1c",background:"#fef2f2"},$y={padding:16,overflow:"auto",flex:1,minHeight:120},Ll={fontSize:13,color:"#94a3b8",textAlign:"center",padding:"32px 0"},Ey={display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(140px, 1fr))",gap:12},Iy={display:"flex",flexDirection:"column",gap:4,border:"1px solid #e2e8f0",borderRadius:8,padding:8},Ay={width:"100%",height:88,borderRadius:6,border:"1px solid #cbd5e1",backgroundSize:"cover",backgroundPosition:"center",backgroundColor:"#f8fafc"},Ty={fontSize:12,fontWeight:600,color:"#0f172a",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},Dy={fontSize:11,color:"#64748b"},Ny={display:"flex",gap:6,marginTop:2},zy={flex:1,padding:"4px 6px",fontSize:11,border:"1px solid #2563eb",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},My={padding:"4px 8px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Ly={padding:"10px 16px",borderTop:"1px solid #e2e8f0",display:"flex",justifyContent:"flex-end"},Py={padding:"6px 14px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};function lr(e){return encodeURIComponent(e)}async function Oy(e,t,n){const o=new URLSearchParams;t&&o.set("layout_name",t);const r=o.toString(),s=`/api/admin/templates/${lr(e)}/custom-translations`+(r?`?${r}`:"");try{const a=await fetch(s,{method:"GET",headers:je(),credentials:"same-origin"}),l=await a.json().catch(()=>null);return!a.ok||!l?.success?{ok:!1,status:a.status,message:dr(l,a.status)}:{ok:!0,data:Array.isArray(l?.data)?l.data:[]}}catch(a){return{ok:!1,status:0,message:a instanceof Error?a.message:"network error"}}}async function By(e,t,n,o){const r=`/api/admin/templates/${lr(e)}/custom-translations/${t}`;try{const s=await fetch(r,{method:"PUT",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:n,expected_lock_version:o}),credentials:"same-origin"}),a=await s.json().catch(()=>null);return!s.ok||!a?.success||!a?.data?{ok:!1,status:s.status,message:dr(a,s.status)}:{ok:!0,data:a.data}}catch(s){return{ok:!1,status:0,message:s instanceof Error?s.message:"network error"}}}async function Wy(e,t){const n=`/api/admin/templates/${lr(e)}/custom-translations/${t}`;try{const o=await fetch(n,{method:"DELETE",headers:je(),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||r&&r.success===!1?{ok:!1,status:o.status,message:dr(r,o.status)}:{ok:!0,data:!0}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}async function jy(e,t){const n=`/api/admin/templates/${lr(e)}/custom-translations`;try{const o=await fetch(n,{method:"DELETE",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({ids:t}),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||!r?.success?{ok:!1,status:o.status,message:dr(r,o.status)}:{ok:!0,data:{deleted:typeof r?.data?.deleted=="number"?r.data.deleted:t.length}}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}const Pl=/\$t:(custom\.[A-Za-z0-9_\-]+\.\d+)/g;function Hy(e){const t=new Set,n=o=>{if(typeof o=="string"){if(o.includes("$t:custom.")){let r;for(Pl.lastIndex=0;(r=Pl.exec(o))!==null;)t.add(r[1])}return}if(Array.isArray(o)){for(const r of o)n(r);return}if(o&&typeof o=="object")for(const r of Object.values(o))n(r)};return n(e),t}function dr(e,t){if(e&&typeof e=="object"&&"message"in e){const n=e.message;if(typeof n=="string"&&n.length>0)return n}return`HTTP ${t}`}const Pt=Lt().TranslationEngine,bo=Lt().createLogger,Pi=bo("EditorSpecLoader"),Fy=["controls","componentCapabilities","actionRecipes","conditionRecipes","pageSettings","initActionRecipes","errorRecipes","computedRecipes","loadingComponents"],Ky=["actionRecipes","initActionRecipes","errorRecipes","computedRecipes","loadingComponents"];function Ol(e){if(typeof window>"u")return[];const n=window.G7Config?.[e];if(!Array.isArray(n))return[];const o=[];for(const r of n){const s=r?.identifier;typeof s=="string"&&s.length>0&&o.push(s)}return o}async function Vy(e,t,n,o){const s=`${n}/${e==="template"?"templates":e==="module"?"modules":"plugins"}/${encodeURIComponent(t)}/editor-spec`;try{const a=await o(s);if(!a.ok)return a.status!==404&&Pi.warn(`fetchOneSpec: HTTP ${a.status}`,s),null;const l=await a.json();return!l?.success||!l.data?null:l.data.spec??null}catch(a){return Pi.warn("fetchOneSpec: fetch failed",s,a),null}}function Uy(e){return Ky.includes(e)}function qy(e,t){return e==null||typeof e!="object"?e:{...e,__source:t}}function Gy(e,t,n,o){const r=t[n];if(!r||typeof r!="object")return;const s=e[n]??{};if(o&&Uy(n)){const a={};for(const[l,d]of Object.entries(r))a[l]=qy(d,o);e[n]={...s,...a};return}e[n]={...s,...r}}function Yy(e,t){if(!t.nesting)return;const n=e.nesting??{},o=new Set(n.draggable??[]);for(const r of t.nesting.draggable??[])o.add(r);n.draggable=Array.from(o),n.containers={...n.containers??{},...t.nesting.containers??{}},e.nesting=n}function Xy(e,t){if(!t.componentPalette)return;const n=e.componentPalette??{},o=[...n.groups??[],...t.componentPalette.groups??[]];n.groups=o,n.entries={...n.entries??{},...t.componentPalette.entries??{}},e.componentPalette=n}function Jy(e,t){if(!t.states)return;const n=e.states??{},o=[...n.groups??[],...t.states.groups??[]];n.groups=o,e.states=n}function Zy(e,t){if(!Array.isArray(t.stateLabels)||t.stateLabels.length===0)return;const n=[...e.stateLabels??[],...t.stateLabels],o=new Map;for(const r of n)!r||typeof r.key!="string"||typeof r.scope!="string"||o.set(`${r.scope}:${r.key}`,r);e.stateLabels=Array.from(o.values())}function Qy(e,t){if(!t.actionChipCandidates)return;const n=e.actionChipCandidates??{};for(const o of["response","error","payload"]){const r=t.actionChipCandidates[o];!Array.isArray(r)||r.length===0||(n[o]=[...n[o]??[],...r])}e.actionChipCandidates=n}function Ry(e,t){if(!t.sampleData)return;const n=e.sampleData??{};n.byDataSourceId={...n.byDataSourceId??{},...t.sampleData.byDataSourceId??{}},n.byEndpointPattern={...n.byEndpointPattern??{},...t.sampleData.byEndpointPattern??{}},e.sampleData=n}function Bl(e,t,n){for(const o of Fy)Gy(e,t,o,n);Yy(e,t),Xy(e,t),Jy(e,t),Zy(e,t),Qy(e,t),Ry(e,t),t.version&&(e.version=t.version)}async function eb(e){const t=e.apiBaseUrl??"/api",n=e.fetcher??(typeof fetch<"u"?fetch:void 0);if(!n)return Pi.warn("loadEditorSpecBundle: fetch unavailable, returning null spec"),{spec:null,sampleGlobalSources:[]};const o=e.activeModuleIdentifiers??Ol("activeModules"),r=e.activePluginIdentifiers??Ol("activePlugins"),s=[...o.map(p=>({kind:"module",id:p})),...r.map(p=>({kind:"plugin",id:p})),{kind:"template",id:e.templateIdentifier}],a=await Promise.all(s.map(p=>Vy(p.kind,p.id,t,n)));let l=null;const d=[],c={};if(e.coreSeed&&(l={},Bl(l,e.coreSeed,{kind:"core"})),s.forEach((p,f)=>{const y=a[f];if(!y)return;y.sampleGlobal&&typeof y.sampleGlobal=="object"&&d.push({id:p.id,kind:p.kind,sampleGlobal:y.sampleGlobal});const g=y.sampleData?.byDataSourceId;if(g&&typeof g=="object"){const m=p.kind==="template"?"template":`${p.kind}:${p.id}`;c[m]={...c[m]??{},...g}}l===null&&(l={}),Bl(l,y,{kind:p.kind,id:p.id})}),l!==null&&Object.keys(c).length>0){const p=l;p.sampleData={...p.sampleData??{},bySource:c}}return{spec:l,sampleGlobalSources:d}}function Oi(e,t){return e?.componentCapabilities?.[t]??null}function Ct(e,t){return e?.controls?.[t]??null}function tb(e){return e?.initActionRecipes??{}}function nb(e){return e?.computedRecipes??{}}function ob(e){return e?.errorRecipes??{}}function rb(e){const t=e?.loadingComponents;return!t||typeof t!="object"?[]:Object.values(t).filter(n=>n!=null&&typeof n=="object"&&typeof n.name=="string")}function ib(e,t,n){const o=e?.stateLabels;if(!Array.isArray(o))return null;for(const r of o)if(r?.scope===t&&r?.key===n)return typeof r.label_key=="string"?r.label_key:null;return null}function Bi(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function sb(e){return e===null?"null":typeof e=="string"?e.length>40?`${e.slice(0,40)}…`:e:typeof e=="number"||typeof e=="boolean"?String(e):""}function ab(e){const t=e.find(n=>Bi(n));if(Bi(t))return Object.keys(t).slice(0,8)}function lb(e){return Object.keys(e).slice(0,8)}function db(e){const t=Object.entries(e);return t.length===0?!1:t.some(([,n])=>n===null||typeof n!="object")}function Wi(e,t,n,o,r){if(Array.isArray(e)){r(t,"array",`[${e.length}]`,ab(e));return}if(Bi(e)){if(t&&db(e)){const s=lb(e);r(t,"object",`{${s.length}}`,s)}if(n>=o)return;for(const[s,a]of Object.entries(e)){const l=t?`${t}.${s}`:s;Wi(a,l,n+1,o,r)}return}r(t,"scalar",sb(e))}function cb(e,t,n){const o=[];let r=e;for(const s of t){if(!r||(r=(Array.isArray(r.children)?r.children:[])[s],!r))break;const l=r.iteration;if(!(!l||l.source===void 0)&&(typeof l.index_var=="string"&&l.index_var.length>0&&o.push({name:l.index_var,kind:"index"}),typeof l.item_var=="string"&&l.item_var.length>0)){const d=typeof l.source=="string"?l.source:"",c=d&&n?.get(Wl(d))||[];o.push({name:l.item_var,kind:"item",itemFields:c})}}return o}function Wl(e){return e.replace(/^\{\{|\}\}$/g,"").trim()}function ub(e){const t=new Map;for(const n of e)n.shape==="array"&&n.itemFields&&n.itemFields.length>0&&t.set(Wl(n.expression),n.itemFields);return t}function ji(e){const t=e.maxDepth??4,n=[];for(const o of e.dataSources??[])!o||typeof o.id!="string"||o.id.length===0||Wi(o.sample,"",0,t,(r,s,a,l)=>{const d=r?`{{${o.id}.${r}}}`:`{{${o.id}}}`;n.push({expression:d,source:"data_source",sourceId:o.id,path:r,shape:s,groupLabelKey:o.labelKey?.startsWith("$t:")?o.labelKey.slice(3):o.labelKey,preview:a,itemFields:l})});for(const o of e.states??[])!o||typeof o.scope!="string"||Wi(o.tree,"",0,t,(r,s,a,l)=>{if(!r)return;const d=ib(e.spec,o.scope,r)??void 0;n.push({expression:`{{${o.scope}.${r}}}`,source:o.scope,sourceId:o.scope,path:r,shape:s,labelKey:d?.startsWith("$t:")?d.slice(3):d,preview:a,itemFields:l})});for(const o of e.iterationVars??[])if(!(!o||typeof o.name!="string"||o.name.length===0)){if(o.kind==="index"){n.push({expression:`{{${o.name}}}`,source:"iteration",sourceId:o.name,path:"",shape:"scalar",preview:"0"});continue}for(const r of o.itemFields??[])n.push({expression:`{{${o.name}.${r}}}`,source:"iteration",sourceId:o.name,path:r,shape:"scalar",preview:""})}return n}const pb=["$localized"];function fb(e){const t=e.trim(),n=t.indexOf("(");if(n===-1||!t.endsWith(")"))return{fn:null,inner:t};const o=t.slice(0,n).trim();if(!pb.includes(o))return{fn:null,inner:t};const r=t.slice(n+1,-1);let s=0,a=null;for(let l=0;l<r.length;l++){const d=r[l];if(a){if(d==="\\"){l++;continue}d===a&&(a=null);continue}if(d==="'"||d==='"'||d==="`"){a=d;continue}if(d==="("||d==="[")s++;else if(d===")"||d==="]"){if(s--,s<0)return{fn:null,inner:t}}else if(s===0&&d===",")return{fn:null,inner:t}}return s!==0?{fn:null,inner:t}:{fn:o,inner:r.trim()}}const gb=["_global","_local","route","query","_computed"];function Hi(e){let t=e.trim();for(;t.startsWith("(")&&t.endsWith(")");){let n=0,o=!0;for(let r=0;r<t.length;r++)if(t[r]==="(")n++;else if(t[r]===")"&&(n--,n===0&&r<t.length-1)){o=!1;break}if(!o)break;t=t.slice(1,-1).trim()}return t}function Kt(e){if(typeof e!="string")return null;const t=e.trim(),n=/^\{\{\s*([^{}]+?)\s*\}\}$/.exec(t);if(!n)return null;let o=Hi(n[1].trim()),r;{let g=0;for(let m=0;m<o.length-1;m++){const h=o[m];if(h==="("||h==="[")g++;else if(h===")"||h==="]")g--;else if(g===0&&h==="?"&&o[m+1]==="?"){r=o.slice(m+2).trim(),o=o.slice(0,m).trim();break}}}o=Hi(o);const s=fb(o),a=s.fn??void 0;a&&(o=Hi(s.inner));const l=o.includes("?."),d=o.replace(/\?\./g,".");if(!/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[\d+\])*$/.test(d)||r!==void 0&&!/^(\[\s*\]|\{\s*\}|''|""|`+`|-?\d+(\.\d+)?|true|false|null)$/.test(r))return null;const c=d.indexOf("."),p=c===-1?d:d.slice(0,c),f=c===-1?"":d.slice(c+1);return{...gb.includes(p)?{source:p,sourceId:p,path:f}:{source:"data_source",sourceId:p,path:f},optional:l,fallback:r,localeFn:a}}function En(e,t,n,o){const r=t?t.split(".").filter(Boolean):[];return`{{${[e,...r].join("?.")} ?? ${n==="array"?"[]":n==="object"?"{}":"''"}}}`}function Fi(e,t){return e.filter(n=>n.shape===t)}function Ki(e,t){const n=t.trim().toLowerCase();return n===""?e:e.filter(o=>[o.resolvedLabel??"",o.sourceId,o.path,o.preview].join(" ").toLowerCase().includes(n))}function jl(e,t,n){const o={response:[{path:"data",labelKey:"layout_editor.action_chip.response_data",shape:"object"},{path:"message",labelKey:"layout_editor.action_chip.response_message",shape:"scalar"}],error:[{path:"status",labelKey:"layout_editor.action_chip.error_status",shape:"scalar"},{path:"message",labelKey:"layout_editor.action_chip.error_message",shape:"scalar"},{path:"errors",labelKey:"layout_editor.action_chip.error_errors",shape:"object"},{path:"data",labelKey:"layout_editor.action_chip.error_data",shape:"object"},{path:"statusText",labelKey:"layout_editor.action_chip.error_status_text",shape:"scalar"}],payload:[{path:"data",labelKey:"layout_editor.action_chip.payload_data",shape:"object"}]},r=e==="response"?"response":e==="error"?"error":"message",s=new Set,a=[];for(const d of o[e])s.has(d.path)||(s.add(d.path),a.push(d));const l=n?.[e];if(Array.isArray(l))for(const d of l)!d||typeof d.path!="string"||typeof d.labelKey!="string"||s.has(d.path)||(s.add(d.path),a.push({path:d.path,labelKey:d.labelKey,shape:d.shape??"scalar"}));return a.map(d=>({expression:`{{${r}.${d.path}}}`,source:"_local",sourceId:r,path:d.path,shape:d.shape,labelKey:d.labelKey,preview:t(d.labelKey)}))}const Vi=/\{\{(?:[^}]|\}(?!\}))*\}\}/g;function In(e){const t=[];if(typeof e!="string"||e.length===0)return t;let n=0,o=0;const r=new RegExp(Vi.source,"g");let s;for(;(s=r.exec(e))!==null;){const a=s.index,l=a+s[0].length;a>n&&t.push({kind:"literal",raw:e.slice(n,a),start:n,end:a}),t.push({kind:"binding",raw:s[0],start:a,end:l,parsed:Kt(s[0]),bindingIndex:o++}),n=l}return n<e.length&&t.push({kind:"literal",raw:e.slice(n),start:n,end:e.length}),t}function cr(e){return In(e).filter(t=>t.kind==="binding")}function Ui(e){return typeof e!="string"?!1:new RegExp(Vi.source).test(e)}function hb(e){const t=new Set(e.map(o=>o.trim()));let n=0;for(;t.has(`p${n}`);)n+=1;return`p${n}`}function Hl(e,t,n){const o=typeof e=="string"?e:"",r=Math.max(0,Math.min(t,o.length)),s=o.slice(0,r),a=o.slice(r),l=s.length>0&&!/\s$/.test(s)?" ":"",d=a.length>0&&!/^\s/.test(a)?" ":"";return`${s}${l}{${n}}${d}${a}`}function qi(e,t){const n=typeof e=="string"?e:"";return An(n).includes(t)?n:n.length===0?`{${t}}`:/\s$/.test(n)?`${n}{${t}}`:`${n} {${t}}`}function Fl(e){return cr(e).map(t=>({bindingIndex:t.bindingIndex??0,expression:t.raw,parsed:t.parsed??null,isComplex:(t.parsed??null)===null}))}function ur(e){return typeof e!="string"?"":e.replace(new RegExp(Vi.source,"g")," ").replace(/\s+/g," ").trim()}const yb=/\$t:([a-zA-Z0-9._-]+)/g,Kl="custom.";function Gi(e,t){return typeof e!="string"||e.length===0?typeof e=="string"?e:"":t?In(e).map(n=>n.kind!=="literal"?n.raw:n.raw.replace(yb,(o,r)=>{if(r.startsWith(Kl))return o;const s=t(r);return s&&s!==r?s:o})).join(""):e}const Kn="(?:(?!\\|[a-zA-Z0-9_]+=).)*",bb=new RegExp(`^\\s*\\$t:([a-zA-Z0-9._-]+)((?:\\|[a-zA-Z0-9_]+=${Kn})*)`),mb=new RegExp(`\\|([a-zA-Z0-9_]+)=(${Kn})`,"g");function Vt(e){if(typeof e!="string")return"";const t=Kt(e);if(t)return t.path||t.sourceId;const n=e.trim().replace(/^\{\{\s*/,"").replace(/\s*\}\}$/,"");let o=-1;for(let l=0;l<n.length;l++)if(n[l]==="|"&&n[l+1]!=="|"&&n[l-1]!=="|"){o=l;break}if(o>0){const l=n.slice(0,o).trim(),d=Kt(`{{${l}}}`);if(d)return d.path||d.sourceId;const c=l.replace(/\?\./g,".").replace(/\s*\?\?.*$/,"").trim();if(/^[A-Za-z_$][\w$.]*$/.test(c)){const p=c.split(".").filter(Boolean);return p.length>1&&p.slice(1).join(".")||c}}const r=n.replace(/\s+/g," ").trim(),s=r.replace(/\s*(\?\?|\|\|)\s*(\[\s*\]|\{\s*\}|'[^']*'|"[^"]*"|\d+|null|undefined)\s*$/,"").trim(),a=/\.([A-Za-z_$][\w$]*)\s*$/.exec(s);return a?a[1]:r}const Vl=/\$(?:core|module|plugin)_settings:[a-zA-Z0-9._:-]+/g;function Ul(e){return typeof e=="string"&&new RegExp(Vl.source).test(e)}function vb(e){return typeof e!="string"?"":e.replace(/^\$(?:core|module|plugin)_settings:/,"").split(/[.:]/).filter(Boolean).pop()||e}function ql(e){const t=[];for(const n of In(e)){if(n.kind==="binding"){t.push({kind:"chip",label:Vt(n.raw)});continue}let o=0;const r=n.raw,s=new RegExp(Vl.source,"g");let a;for(;(a=s.exec(r))!==null;)a.index>o&&t.push({kind:"text",label:r.slice(o,a.index)}),t.push({kind:"chip",label:vb(a[0])}),o=a.index+a[0].length;o<r.length&&t.push({kind:"text",label:r.slice(o)})}return t}function _b(e){return typeof e!="string"?!1:/\{\{/.test(e)?!0:Ul(e)}function Yi(e,t){const n={chipValue:"",bindings:[],paramLabels:{},keyifiable:!1};if(typeof e!="string"||e.length===0)return n;const o=[],r={},s=g=>{const m=`p${o.length}`;return o.push(g),r[m]=Vt(g),`{${m}}`};let a=e,l="";const d=bb.exec(e);if(d&&!d[1].startsWith(Kl)){const g=d[1],m=d[2]??"",h=e.slice(d[0].length),v=new Map,b=new RegExp(mb.source,"g");let x;for(;(x=b.exec(m))!==null;)x[2]&&x[2].includes("{{")&&v.set(x[1],x[2]);const k=t?t(g):"";if(k&&k!==g&&!k.startsWith("$t:"))l=k.replace(/\{\{?([a-zA-Z0-9_]+)\}?\}/g,(w,_)=>{const C=v.get(_);return C?s(C):w});else return n;a=h}const c=Gi(a,t),p=In(c).map(g=>g.kind==="binding"?s(g.raw):g.raw).join(""),f=`${l}${p}`;return/\$t:[a-zA-Z0-9._-]+/.test(f)||o.length===0?n:{chipValue:f,bindings:o,paramLabels:r,keyifiable:!0}}function Gl(e,t){return t.bindings.length?e+t.bindings.map((n,o)=>`|p${o}=${n}`).join(""):e}function Yl(e,t){const n=cr(t).map(r=>r.raw);if(n.length===0)return e;const o=n.map((r,s)=>`|p${s}=${r}`).join("");return`${e}${o}`}function Xl(e){if(typeof e!="string")return"";const t=In(e);let n=0;return t.map(o=>o.kind==="binding"?`{p${n++}}`:o.raw).join("")}const Jl=new RegExp(`^\\s*\\$t:([a-zA-Z0-9._-]+)((?:\\|[a-zA-Z0-9_]+=${Kn})+)\\s*$`),xb=new RegExp(`\\|([a-zA-Z0-9_]+)=(${Kn})`,"g");function mt(e){if(typeof e!="string")return null;const t=Jl.exec(e);if(!t)return null;const n=t[1],o=t[2],r=[],s=new RegExp(xb.source,"g");let a;for(;(a=s.exec(o))!==null;){const l=a[1],d=a[2];r.push({name:l,expression:d,parsed:Kt(d)})}return{key:n,params:r}}function Zl(e){return typeof e=="string"&&Jl.test(e)}function An(e){if(typeof e!="string")return[];const t=[],n=/\{\{?(p\d+)\}?\}/g;let o;for(;(o=n.exec(e))!==null;)t.push(o[1]);return t.sort()}function wb(e,t,n){if(typeof e!="string")return e;const o=new RegExp(`(\\|${t}=)${Kn}`);return o.test(e)?e.replace(o,`$1${n}`):e}function mo(e,t){if(typeof e!="string")return e;const n=new RegExp(`\\|${t}=${Kn}`);return e.replace(n,"")}function Ql(e,t){return typeof e!="string"?e:e.replace(new RegExp(`\\s*\\{\\{?${t}\\}?\\}\\s*`,"g")," ").replace(/\s+/g," ").trim()}function kb(e,t){const n=s=>cr(typeof s=="string"?s:"").map(a=>a.raw.replace(/\s+/g,"")).sort(),o=n(e),r=n(t);return o.length!==r.length?!1:o.every((s,a)=>s===r[a])}function Rl(e){return e.__source?.kind==="base"}function ed(e){return e.__source?.kind==="partial"}function Sb(e){return e.__source?.kind==="route"}function pr(e,t){return e.__source?.kind!=="extension"?!1:t==null?!0:e.__source.extensionId!==t}function td(e){return e.type==="extension_point"}function Xi(e=[]){for(const t of e)if(t.iteration&&t.iteration.source!==void 0)return!0;return!1}function Cb(e,t=[]){for(const n of t)if(n.iteration&&n.iteration.source!==void 0)return!0;return e.iteration&&e.iteration.source!==void 0?!0:nd(e)}function nd(e){if(typeof e.text=="string"&&od(e.text)&&!$b(e.text))return!0;if(e.props&&typeof e.props=="object"){for(const t of Object.values(e.props))if(typeof t=="string"&&od(t))return!0}return!1}function od(e){return/\{\{[\s\S]*?\}\}/.test(e)}function $b(e){const t=mt(e);return t!==null&&t.key.startsWith("custom.")}function Eb(e,t){return e??t??null}function Ji(e,t,n){if(td(e))return!0;switch(t){case"route":return Rl(e)||ed(e)||pr(e);case"base":return pr(e);case"modal":return pr(e);case"extension":return pr(e,n)?!0:Rl(e)||Sb(e)||ed(e);default:return!1}}function et(e){return typeof e=="object"&&e!==null&&typeof e.responsive=="string"}function fr(e,t){return et(e)?et(t)&&e.responsive===t.responsive:!et(t)&&e===t}function at(e){const t=[];let n=!1;for(const o of e){if(et(o)){t.push("responsive",o.responsive),n=!0;continue}n?t.push("children",String(o)):(t.push(String(o)),n=!0)}return t.join(".")}function Qt(e){return Array.isArray(e.children)?e.children:[]}function gr(e,t){const n=e.responsive?.[t];return n&&Array.isArray(n.children)?n.children:[]}function We(e,t){if(!e)return null;if(t.length===0)return e;let n=e,o=Qt(e);for(const r of t){if(et(r)){o=gr(n,r.responsive);continue}if(r<0||r>=o.length||(n=o[r],!n))return null;o=Qt(n)}return n}function Ib(e,t,n){if(!e||t.length===0)return t;let o=e,r=Qt(e),s;for(let a=0;a<t.length;a++){const l=t[a];if(et(l)){r=gr(o,l.responsive);continue}const d=r[l]??null;if(!d)return t;const c=d.__source?.kind,p=d.__source?.extensionId,f=c==="extension"&&n!=null&&p===n;if(c==="extension"&&s!=="extension"&&!f)return t.slice(0,a+1);s=c,o=d,r=Qt(d)}return t}function rd(e,t){if(t.length===0)return null;const n=t[t.length-1];if(et(n))return null;const o=n,r=t.length>=2?t[t.length-2]:void 0;if(r!==void 0&&et(r)){const a=We(e,t.slice(0,-2));return a?{owner:a,children:gr(a,r.responsive),branchKey:r.responsive,index:o}:null}const s=We(e,t.slice(0,-1));return s?{owner:s,children:Qt(s),index:o}:null}function Zi(e,t){if(e.branchKey!==void 0){const n={...e.owner.responsive??{}};n[e.branchKey]={...n[e.branchKey]??{},children:t},e.owner.responsive=n}else e.owner.children=t}function Rt(e){const t={...e};t.props&&typeof t.props=="object"&&(t.props={...t.props});const n=Qt(e);if(n.length>0&&(t.children=n.map(Rt)),e.responsive&&typeof e.responsive=="object"){const o={};for(const r of Object.keys(e.responsive)){const s=e.responsive[r];if(!s||typeof s!="object"){o[r]=s;continue}const a={...s};Array.isArray(s.children)&&(a.children=s.children.map(Rt)),a.props&&typeof a.props=="object"&&(a.props={...a.props}),o[r]=a}t.responsive=o}return t}function Qi(e,t,n,o){const r=Rt(e),s=t.length>0?t[t.length-1]:void 0;if(s!==void 0&&et(s)){const p=We(r,t.slice(0,-1));if(!p)return r;const f=gr(p,s.responsive),y=Math.max(0,Math.min(n,f.length)),g=[...f];return g.splice(y,0,o),Zi({owner:p,branchKey:s.responsive},g),r}const a=We(r,t);if(!a)return r;const l=Qt(a),d=Math.max(0,Math.min(n,l.length)),c=[...l];return c.splice(d,0,o),a.children=c,r}function Ri(e,t){if(t.length===0)return e;const n=Rt(e),o=rd(n,t);if(!o||o.index<0||o.index>=o.children.length)return n;const r=[...o.children];return r.splice(o.index,1),Zi(o,r),n}function Ab(e,t,n,o){if(t.length===0||!n||et(t[t.length-1]))return e;const r=Rt(e),s=We(r,t);if(!s)return r;const a=s.responsive&&typeof s.responsive=="object"?s.responsive[n]:void 0;if(a&&Array.isArray(a.children))return r;const l=o&&s.responsive&&typeof s.responsive=="object"?s.responsive[o]:void 0,c=(l&&Array.isArray(l.children)?l.children:Qt(s)).map(Rt),p={...s.responsive??{}};return p[n]={...p[n]??{},children:c},s.responsive=p,r}function Tb(e,t,n){if(t.length===0||!n||et(t[t.length-1]))return e;const o=Rt(e),r=We(o,t);if(!r||!r.responsive||typeof r.responsive!="object")return o;const s=r.responsive[n];if(!s||!Array.isArray(s.children))return o;const a={...r.responsive},l={...a[n]};return delete l.children,Object.keys(l).length===0?delete a[n]:a[n]=l,r.responsive=Object.keys(a).length>0?a:void 0,o}function Db(e,t,n,o){if(t.length===0)return e;const r=We(e,t);if(!r||Nb(t,n))return e;const s=Rt(r),a=Ri(e,t),l=id(n,t);let d=o;const c=t.slice(0,-1);if(sd(c,n)){const p=t[t.length-1];(typeof p=="number"?p:0)<o&&(d=Math.max(0,o-1))}return Qi(a,l,d,s)}function id(e,t){if(t.length===0)return e;const n=t.length-1;if(e.length<=n)return e;for(let s=0;s<n;s++)if(!fr(e[s],t[s]))return e;const o=t[n],r=e[n];if(et(o)||et(r))return e;if(r>o){const s=[...e];return s[n]=r-1,s}return e}function sd(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(!fr(e[n],t[n]))return!1;return!0}function Nb(e,t){if(t.length<e.length)return!1;for(let n=0;n<e.length;n++)if(!fr(t[n],e[n]))return!1;return t.length>e.length||sd(e,t)}function es(e){const t={...e};if(t.props&&typeof t.props=="object"&&(t.props={...t.props}),typeof t.id=="string"&&t.id.length>0){const o=typeof t.name=="string"?t.name:"node";t.id=`${o}_${zb()}`}const n=Qt(e);return n.length>0&&(t.children=n.map(es)),t}function Ot(e,t,n){if(t.length===0)return n(e);const o=Rt(e),r=We(o,t);if(!r)return o;const s=n(r),a=rd(o,t);if(!a||a.index<0||a.index>=a.children.length)return o;const l=[...a.children];return l[a.index]=s,Zi(a,l),o}function zb(){return Math.floor(Math.random()*2821109907456).toString(36).padStart(8,"0")}function Vn(e){if(!Array.isArray(e)||e.length===0)return[];const t=[];for(const n of e){const o=Mb(n);o.length>0&&t.push(...o)}return t}function Mb(e){if(!e||typeof e!="object")return[];const t=e.__source,n=e._fromBase===!0,o=t?.kind;if(o==="extension"||o==="partial")return[];if(o==="base"||n){const s=Array.isArray(e.children)?e.children:[];return Vn(s)}const r={...e};return delete r.__source,delete r._fromBase,delete r.__injectedProps,Array.isArray(r.children)&&(r.children=Vn(r.children)),[r]}function vo(e){if(!e||typeof e!="object")return e;const t=e.__editor,n=t&&typeof t=="object"&&!Array.isArray(t)?t.original:void 0,o=n&&typeof n=="object"&&!Array.isArray(n)?n:void 0,r=Array.isArray(e.components)?e.components:[],s=r.length>0,a=Vn(r),l=o?{...o}:{...e};if(typeof l.extends=="string"&&l.extends.length>0){delete l.components;const c=l.slots&&typeof l.slots=="object"&&!Array.isArray(l.slots)?l.slots:{},p=Object.keys(c);if(!s&&p.length>0){const f={};for(const[y,g]of Object.entries(c))f[y]=Array.isArray(g)?Vn(g):g;l.slots=f}else if(p.length===1)l.slots={[p[0]]:a};else if(p.length===0)l.slots={content:a};else{const f={};for(const[y,g]of Object.entries(c))f[y]=Array.isArray(g)?Vn(g):g;l.slots=f}}else l.components=a,delete l.slots;return delete l.lock_version,delete l.__editor,l}const ad="extension",Un="extensionless";function Lb(){const e=globalThis?.G7Config;if(globalThis?.__g7AssetUrlMode===Un)return Un;const n=e?.assetUrlMode;return n===Un||n===ad?n:e?.settings?.general?.asset_url_mode===Un?Un:ad}function Pb(){return Lb()===Un}function en(e,t,n,o){const r=e.replace(/\/+$/,""),s=t.replace(/^\.+/,""),a=Pb()?r:`${r}.${s}`,l=[];return o&&l.push(o.replace(/^[?&]+/,"")),n!=null&&n!==""&&l.push(`v=${n}`),l.length>0?`${a}?${l.join("&")}`:a}const ld=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,hr="custom.",Ob=/\{\{.*?\}\}/;function Bb(e){return!!(!e||e.iteration&&typeof e.iteration=="object")}function dd(e,t){const n=e?.text,o=typeof n=="string"?n:"";if(Bb(e)||typeof n!="string")return{sourceState:"plain_text",editable:!1,customKey:null,displayValue:o};const r=mt(o);if(r&&r.key.startsWith(hr)){const a=t?t(r.key):r.key;return{sourceState:"custom_key",editable:!0,customKey:r.key,displayValue:a,isParamKey:!0}}const s=ld.exec(o);if(s){const a=s[1];if(a.startsWith(hr)){const d=t?t(a):a;return{sourceState:"custom_key",editable:!0,customKey:a,displayValue:d}}const l=t?t(a):"";return{sourceState:"plain_text",editable:!0,customKey:null,displayValue:l&&l!==a?l:""}}if(Ob.test(o)){const a=ur(o),l=ld.exec(a);if(l&&l[1].startsWith(hr)){const f=l[1],y=t?t(f):f;return{sourceState:"custom_key",editable:!0,customKey:f,displayValue:y}}const d=Gi(o,t),c=ur(d),p=/\$t:[a-zA-Z0-9._-]+/.test(c);if(c.length>0){const f=Yi(o,t);return{sourceState:"plain_with_binding",editable:!0,customKey:null,displayValue:p?"":c,chipValue:f.keyifiable?f.chipValue:void 0,chipParamLabels:f.keyifiable?f.paramLabels:void 0}}return{sourceState:"binding_expression",editable:!1,customKey:null,displayValue:o}}return{sourceState:"plain_text",editable:!0,customKey:null,displayValue:o}}function Wb(){const{state:e}=Je(),t=Ft(),n=e.templateIdentifier,o=e.locale,r=e.selectedRoute?.layoutName??null,s=u.useCallback(p=>{try{const y=Pt.getInstance().translate(p,{templateId:n,locale:o});return y&&y!==p?y:""}catch{return""}},[n,o]),a=u.useCallback(p=>dd(p,s),[s]),l=u.useCallback(p=>{Et({op:"inline_edit_cancel",componentPath:p?at(p):null,timestamp:Date.now()})},[]),d=u.useCallback(async(p,f)=>{if(!t)return{kind:"error",message:"no document context"};const g={children:t.document?.raw?.components??[]},m=We(g,p),h=dd(m,s);if(!h.editable)return Et({op:"inline_edit_blocked_binding",sourceState:h.sourceState,componentPath:at(p),timestamp:Date.now()}),{kind:"blocked"};if(h.customKey){if(f===h.displayValue)return{kind:"noop",translationKey:h.customKey};_o(n,o,h.customKey,f);const E=await yr(n,h.customKey,o,f);return E.kind==="error"?{kind:"error",message:E.message}:(await tn(n,o),Et({op:"inline_edit_update_value",sourceState:"custom_key",componentPath:at(p),translationKey:h.customKey,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"updated",translationKey:h.customKey})}const v=typeof m?.text=="string"?m.text:"",b=h.sourceState==="plain_with_binding",x=Gi(v,s),k=b?Xl(x):f,w=E=>b?Yl(E,v):E;t.patchLayout(E=>Ot({children:E},p,$=>({...$,text:f})).children??[]);const _=await qn(n,r,o,k);if(_.kind==="error"||!_.resource)return{kind:"error",message:_.message};const C=_.resource.translation_key;return _o(n,o,C,k),t.patchLayout(E=>Ot({children:E},p,$=>({...$,text:w(`$t:${C}`)})).children??[]),await tn(n,o),Et({op:"inline_edit_create_key",sourceState:h.sourceState,componentPath:at(p),translationKey:C,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"created",translationKey:C,id:_.resource.id}},[t,n,o,r,s]),c=u.useCallback(async(p,f)=>{if(!t)return{kind:"error",message:"no document context"};const g={children:t.document?.raw?.components??[]},m=We(g,p),h=typeof m?.text=="string"?m.text:"",v=mt(h);if(v&&v.key.startsWith(hr))return br(n,v.key,o,f),_o(n,o,v.key,f),t.markDirty?.(),{kind:"updated",translationKey:v.key};const b=Yi(h,s),x=ur(f.replace(/\{\{?p\d+\}?\}/g," ")).replace(/\s+/g," ").trim(),k=await qn(n,r,o,x);if(k.kind==="error"||!k.resource)return{kind:"error",message:k.message};const w=k.resource.translation_key,_=An(f),C=k.resource.values??{},E={[o]:f};for(const S of Object.keys(C)){if(S===o)continue;let T=C[S]??"";for(const $ of _)T=qi(T,$);E[S]=T}return xo(n,w,E),_o(n,o,w,f),t.patchLayout(S=>Ot({children:S},p,A=>({...A,text:Gl(`$t:${w}`,b)})).children??[]),t.markDirty?.(),await tn(n,o),_o(n,o,w,f),Et({op:"inline_edit_create_key",sourceState:"plain_with_binding",componentPath:at(p),translationKey:w,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"created",translationKey:w}},[t,n,o,r]);return{classify:a,commit:d,trackCancel:l,keyifyChipValue:c}}async function qn(e,t,n,o){const r=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations`;try{const s=await fetch(r,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({layout_name:t,locale:n,value:o})}),a=await s.json().catch(()=>null);if(!s.ok)return{kind:"error",message:a?.message??`HTTP ${s.status}`};const l=a?.data;return!l||typeof l.translation_key!="string"?{kind:"error",message:"invalid response"}:{kind:"ok",resource:l}}catch(s){return{kind:"error",message:s instanceof Error?s.message:"network error"}}}async function yr(e,t,n,o){const r=await cn(e,t);if(!r)return{kind:"error",message:"custom key not found"};const s=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations/${r.id}`,a={...r.values??{},[n]:o};try{const l=await fetch(s,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:a,expected_lock_version:r.lock_version})}),d=await l.json().catch(()=>null);return l.status===409?{kind:"error",message:"concurrent_modification"}:l.ok?{kind:"ok"}:{kind:"error",message:d?.message??`HTTP ${l.status}`}}catch(l){return{kind:"error",message:l instanceof Error?l.message:"network error"}}}async function cn(e,t){const n=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations`;try{const o=await fetch(n,{method:"GET",credentials:"same-origin",headers:je()});if(!o.ok)return null;const s=(await o.json().catch(()=>null))?.data;return Array.isArray(s)?s.find(a=>a.translation_key===t)??null:null}catch{return null}}function _o(e,t,n,o){try{Pt.getInstance().setTranslationValue(e,t,n,o)}catch{}if(typeof window<"u")try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:t}}))}catch{}}const kt="g7le:translations-refreshed";async function tn(e,t){if(typeof window>"u")return;const n=t??window.__g7EditorContentLocale;if(n){try{const o=Pt.getInstance();try{const r=await fetch(en(`/api/templates/${encodeURIComponent(e)}/config`,"json"),{headers:{Accept:"application/json"},credentials:"same-origin"}).then(a=>a.ok?a.json():null),s=r?.data?.cache_version??r?.cache_version;if(typeof s=="number"&&s>0){o.setCacheVersion(s);const a=window;a.G7Config&&(a.G7Config.cache_version=s)}}catch{}await o.loadTranslations(e,n,"/api",!0)}catch{}try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:n}}))}catch{}}}const nn=new Map;function br(e,t,n,o){const r=nn.get(t)??{};r[n]=o,nn.set(t,r);try{Pt.getInstance().setTranslationValue(e,n,t,o)}catch{}if(typeof window<"u")try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:n}}))}catch{}}function xo(e,t,n){for(const[o,r]of Object.entries(n))br(e,t,o,r)}function It(e,t){return nn.get(e)?.[t]}function jb(e){const t=nn.get(e);return t?{...t}:void 0}function Hb(){return nn.size>0}function cd(e,t){for(const[n,o]of nn.entries())for(const[r,s]of Object.entries(o))try{e.setTranslationValue(t,r,n,s)}catch{}}async function Fb(e){if(nn.size===0)return{ok:0,failed:[]};const t=Array.from(nn.entries());let n=0;const o=[];for(const[r,s]of t)try{const a=await cn(e,r);if(!a){o.push(r);continue}const l={...a.values??{},...s},d=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations/${a.id}`;if(!(await fetch(d,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:l,expected_lock_version:a.lock_version})})).ok){o.push(r);continue}n+=1}catch{o.push(r)}for(const[r]of t)o.includes(r)||nn.delete(r);try{await tn(e)}catch{}return{ok:n,failed:o}}function Gn(e,t){const n=`layout_editor.locale.${e}`,o=t(n);return o&&o!==n?o:e.toUpperCase()}function ud(){if(typeof window>"u")return[];const t=window.G7Config?.appConfig?.supportedLocales;return Array.isArray(t)?t.filter(n=>typeof n=="string"&&n.length>0):[]}function Kb({locales:e}={}){const{t}=tt(),{state:n,dispatch:o}=Je();u.useEffect(()=>{window.__g7EditorContentLocale=n.locale},[n.locale]);const r=e??ud(),s=r.includes(n.locale)?r:[n.locale,...r];if(s.length<=1)return i.jsxs("span",{className:"g7le-locale-switcher g7le-locale-switcher--single","data-testid":"g7le-locale-switcher-single",style:{fontSize:11,color:"#475569",padding:"0 4px"},children:[t("layout_editor.chrome.toolbar.locale_switcher_label"),": ",Gn(n.locale,t)]});const a=l=>{if(l===n.locale)return;const d=n.locale;o({type:"SET_LOCALE",locale:l}),Et({op:"locale_switch",fromLocale:d,toLocale:l,timestamp:Date.now()})};return i.jsxs("div",{className:"g7le-locale-switcher","data-testid":"g7le-locale-switcher",role:"group","aria-label":t("layout_editor.chrome.toolbar.locale_switcher_label"),style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("span",{style:{fontSize:11,color:"#475569"},children:t("layout_editor.chrome.toolbar.locale_switcher_label")}),i.jsx("div",{style:{display:"inline-flex",gap:2},children:s.map(l=>{const d=l===n.locale;return i.jsx("button",{type:"button","data-testid":`g7le-locale-${l}`,"aria-pressed":d,onClick:()=>a(l),style:{padding:"2px 8px",fontSize:12,cursor:"pointer",background:d?"#0f172a":"#f8fafc",color:d?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:Gn(l,t)},l)})})]})}const Vb=/\{\{?(p\d+)\}?\}/g;function Ub(e){const t=[];if(typeof e!="string"||e.length===0)return t;const n=new RegExp(Vb.source,"g");let o=0,r;for(;(r=n.exec(e))!==null;){const s=r.index,a=s+r[0].length;s>o&&t.push({kind:"text",raw:e.slice(o,s),start:o,end:s}),t.push({kind:"chip",raw:r[0],paramName:r[1],start:s,end:a}),o=a}return o<e.length&&t.push({kind:"text",raw:e.slice(o),start:o,end:e.length}),t}function qb(e){const t=Ub(e),n=[];let o="",r=!1;const s=a=>{n.push({kind:"text",text:a}),r=!0};for(const a of t)a.kind==="text"?(o=a.raw,s(o),o=""):(r||s(""),n.push({kind:"chip",paramName:a.paramName}),r=!1);return r||s(""),n}function Gb(e,t,n,o){const r=[];return e.forEach((s,a)=>{if(s.kind==="chip"){if(s.paramName===t)return;r.push(`{${s.paramName}}`)}else{const l=s.text??"";if(a===n){const d=Math.max(0,Math.min(o,l.length));r.push(l.slice(0,d),`{${t}}`,l.slice(d))}else r.push(l)}}),r.join("")}function mr({value:e,onChange:t,t:n,paramLabels:o,onRequestInsert:r,onRemoveChip:s,testIdSuffix:a="pc",disabled:l=!1,hideInsertButton:d=!1,caretRef:c}){const p=u.useMemo(()=>qb(e),[e]),f=u.useRef(null),[y,g]=u.useState(null),[m,h]=u.useState(null),[v,b]=u.useState(null),x=u.useMemo(()=>p.map(B=>B.kind==="chip"?`c:${B.paramName}`:"t").join("|"),[p]),k=u.useRef(null),w=u.useRef([]),_=u.useRef(!1);u.useLayoutEffect(()=>{const B=_.current;_.current=!1,!(!B&&k.current===x)&&(k.current=x,p.forEach((N,H)=>{if(N.kind!=="text")return;const L=w.current[H];L&&L.textContent!==(N.text??"")&&(L.textContent=N.text??"")}))},[x,p,e]);const C=()=>{const B=f.current;if(!B)return e;const N=[];for(const H of Array.from(B.querySelectorAll("[data-seg]")))H.dataset.seg==="chip"?N.push(`{${H.dataset.param??""}}`):N.push(H.textContent??"");return N.join("")},E=()=>{l||t(C())},S=u.useRef(null),T=u.useRef(!1),$=(B,N)=>{if(!l){B.preventDefault(),B.stopPropagation(),S.current={param:N,pointerId:B.pointerId},T.current=!1,g(N);try{B.currentTarget.setPointerCapture(B.pointerId)}catch{}}},A=B=>{const N=S.current;if(!N||B.pointerId!==N.pointerId)return;T.current=!0;const H=O(B.clientX,B.clientY);H&&(h({slotIdx:H.slotIdx,offset:H.offset}),b(Xb(f.current,H.slotIdx,H.offset)))},z=(B,N)=>{if(B.stopPropagation(),T.current){T.current=!1;return}s?.(N)},D=B=>{const N=S.current;if(!N||B.pointerId!==N.pointerId)return;try{B.currentTarget.releasePointerCapture(B.pointerId)}catch{}S.current=null,T.current&&typeof setTimeout=="function"&&setTimeout(()=>{T.current=!1},0);const H=O(B.clientX,B.clientY);if(g(null),h(null),b(null),!H)return;const L=f.current;if(!L)return;const P=Array.from(L.querySelectorAll("[data-seg]")).map(G=>G.dataset.seg==="chip"?{kind:"chip",paramName:G.dataset.param??""}:{kind:"text",text:G.textContent??""}),F=Gb(P,N.param,H.slotIdx,H.offset);F!==e&&(_.current=!0,t(F))},O=(B,N)=>{const H=f.current;if(!H)return null;const L=Array.from(H.querySelectorAll("[data-seg]")),I=H.ownerDocument,P=I.elementFromPoint(B,N);let F=P?.closest('[data-seg="text"]');if(F)return{slotIdx:L.indexOf(F),offset:Yb(I,F,B,N)};const G=P?.closest('[data-seg="chip"]');if(G){const Y=L.indexOf(G),Z=G.getBoundingClientRect();if(B<Z.left+Z.width/2&&Y-1>=0){const se=L[Y-1];return{slotIdx:Y-1,offset:(se.textContent??"").length}}if(Y+1<L.length)return{slotIdx:Y+1,offset:0}}const V=L.map((Y,Z)=>({s:Y,i:Z})).filter(Y=>Y.s.dataset.seg==="text").pop();return V?{slotIdx:V.i,offset:(V.s.textContent??"").length}:null},M=()=>{l||!r||r(pd(f.current,e.length))};return u.useEffect(()=>{if(c)return c.current=()=>pd(f.current,e.length),()=>{c.current=null}},[c,e.length]),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsxs("div",{ref:f,"data-testid":`g7le-chip-input-${a}`,className:"g7le-chip-input",style:Jb,children:[p.map((B,N)=>B.kind==="chip"?i.jsxs("span",{"data-seg":"chip","data-param":B.paramName,"data-testid":`g7le-chip-${a}-${B.paramName}`,contentEditable:!1,onPointerDown:H=>$(H,B.paramName??""),onPointerMove:A,onPointerUp:D,title:n("layout_editor.translation.placeholder_locked"),style:{...em,...y===B.paramName?tm:null,touchAction:"none"},children:[i.jsx("span",{"aria-hidden":"true",style:nm,children:"⠿"}),i.jsx("span",{"aria-hidden":"true",style:{marginRight:3},children:"🔗"}),i.jsx("span",{style:om,children:o?.[B.paramName??""]??B.paramName}),s&&!l&&i.jsx("button",{type:"button","data-testid":`g7le-chip-remove-${a}-${B.paramName}`,"aria-label":n("layout_editor.inline_binding.clear"),title:n("layout_editor.inline_binding.clear"),onPointerDown:H=>{H.stopPropagation(),H.preventDefault()},onClick:H=>z(H,B.paramName??""),style:rm,children:"✕"})]},`chip-${B.paramName}`):i.jsx("span",{ref:H=>{w.current[N]=H},"data-seg":"text",contentEditable:!l,suppressContentEditableWarning:!0,onInput:E,"data-testid":`g7le-chip-text-${a}-${N}`,style:{...Zb,...y?Qb:null,...m?.slotIdx===N?Rb:null}},`text-slot-${N}`)),v&&i.jsx("span",{"aria-hidden":"true","data-testid":`g7le-chip-caret-${a}`,style:{position:"absolute",left:v.left,top:v.top,width:2,height:v.height,background:"#2563eb",borderRadius:1,pointerEvents:"none",boxShadow:"0 0 0 1px rgba(37,99,235,0.3)"}})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[r&&!d&&i.jsxs("button",{type:"button","data-testid":`g7le-chip-insert-${a}`,onClick:M,disabled:l,style:im,children:["+ ",n("layout_editor.inline_binding.insert_data")]}),i.jsx("span",{style:sm,children:n("layout_editor.translation.placeholder_locked")})]})]})}function Yb(e,t,n,o){const r=(t.textContent??"").length;try{const a=e;let l=null,d=0;if(typeof a.caretRangeFromPoint=="function"){const c=a.caretRangeFromPoint(n,o);c&&(l=c.startContainer,d=c.startOffset)}else if(typeof a.caretPositionFromPoint=="function"){const c=a.caretPositionFromPoint(n,o);c&&(l=c.offsetNode,d=c.offset)}if(l&&t.contains(l))return Math.max(0,Math.min(d,r))}catch{}const s=t.getBoundingClientRect();return n>s.left+s.width/2?r:0}function Xb(e,t,n){if(!e)return null;const r=Array.from(e.querySelectorAll("[data-seg]"))[t];if(!r)return null;const s=e.getBoundingClientRect();try{const d=r.firstChild,c=e.ownerDocument;if(d&&d.nodeType===Node.TEXT_NODE&&(d.textContent??"").length>0){const p=c.createRange(),f=Math.max(0,Math.min(n,(d.textContent??"").length));p.setStart(d,f),p.setEnd(d,f);const y=p.getClientRects(),g=y.length>0?y[0]:p.getBoundingClientRect();if(g&&(g.height>0||g.width>=0))return{left:g.left-s.left,top:g.top-s.top,height:g.height||r.getBoundingClientRect().height}}}catch{}const a=r.getBoundingClientRect();return{left:(n>0?a.right:a.left)-s.left,top:a.top-s.top,height:a.height||16}}function pd(e,t){if(!e)return t;try{const n=e.ownerDocument.defaultView?.getSelection();if(!n||n.rangeCount===0)return t;const o=n.getRangeAt(0);let r=o.startContainer,s=null;for(;r&&r!==e;){if(r instanceof HTMLElement&&r.dataset.seg==="text"){s=r;break}r=r.parentNode}if(!s||!e.contains(s))return t;const a=Array.from(e.querySelectorAll("[data-seg]"));let l=0;for(const d of a){if(d===s)break;l+=d.dataset.seg==="chip"?`{${d.dataset.param??""}}`.length:(d.textContent??"").length}return l+o.startOffset}catch{return t}}const Jb={position:"relative",display:"flex",flexWrap:"wrap",alignItems:"center",gap:2,minHeight:30,padding:"4px 8px",border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",fontSize:13,lineHeight:1.6},Zb={outline:"none",minWidth:8,minHeight:"1.2em",whiteSpace:"pre-wrap",color:"#0f172a",display:"inline-block"},Qb={boxShadow:"inset 0 -2px 0 #93c5fd",background:"#eff6ff",borderRadius:2},Rb={background:"#dbeafe",boxShadow:"inset 0 -2px 0 #2563eb"},em={display:"inline-flex",alignItems:"center",gap:1,background:"linear-gradient(180deg,#eff6ff,#dbeafe)",color:"#1d4ed8",border:"1px dashed #60a5fa",borderRadius:9999,padding:"1px 8px 1px 4px",margin:"0 1px",fontSize:12,fontWeight:600,cursor:"grab",userSelect:"none",whiteSpace:"nowrap",boxShadow:"0 1px 2px rgba(37,99,235,0.18)",verticalAlign:"middle"},tm={opacity:.5,borderStyle:"solid",cursor:"grabbing"},nm={color:"#2563eb",opacity:.7,fontSize:11,marginRight:1,cursor:"grab"},om={textDecoration:"none"},rm={marginLeft:4,width:14,height:14,lineHeight:"12px",padding:0,border:"none",borderRadius:9999,background:"rgba(29,78,216,0.12)",color:"#1d4ed8",fontSize:9,fontWeight:700,cursor:"pointer",display:"inline-flex",alignItems:"center",justifyContent:"center",flex:"0 0 auto"},im={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#334155",padding:"3px 10px",cursor:"pointer"},sm={fontSize:10,color:"#94a3b8"};function fd(e){e&&(e.style.height="auto",e.style.height=`${Math.max(e.scrollHeight,28)}px`)}function am(e){fd(e)}function lm(e){const t=new Set;for(const n of e)for(const o of Object.keys(n.values??{}))t.add(o);return Array.from(t).sort()}function dm({templateIdentifier:e,layoutName:t,t:n,referencedKeys:o,onClose:r}){const[s,a]=u.useState([]),[l,d]=u.useState("idle"),[c,p]=u.useState(null),[f,y]=u.useState("all"),[g,m]=u.useState(new Set),[h,v]=u.useState({}),[b,x]=u.useState(null),[k,w]=u.useState({}),_=u.useCallback(async()=>{d("loading"),p(null);const N=await Oy(e,t);N.ok?(a(N.data),v({}),m(new Set),w({}),d("idle")):(p(N.message),d("error"))},[e,t]);u.useEffect(()=>{_()},[_]);const C=u.useMemo(()=>lm(s),[s]),E=u.useMemo(()=>s.filter(N=>f==="all"?!0:N.status===f).slice().sort((N,H)=>{const L=N.updated_at?Date.parse(N.updated_at):0,I=H.updated_at?Date.parse(H.updated_at):0;return I!==L?I-L:H.id-N.id}),[s,f]),S=u.useMemo(()=>s.filter(N=>N.status==="orphaned"),[s]),T=u.useCallback(N=>{const H={...N.values??{}};for(const L of Object.keys(H)){const I=It(N.translation_key,L);I!==void 0&&(H[L]=I)}return H},[]),$=u.useCallback(N=>h[N.id]??T(N),[h,T]),A=u.useCallback((N,H,L)=>{v(I=>{const P=I[N]??s.find(F=>F.id===N)?.values??{};return{...I,[N]:{...P,[H]:L}}})},[s]),z=u.useCallback(async N=>{x(N.id),w(I=>({...I,[N.id]:""}));const H=$(N),L=await By(e,N.id,H,N.lock_version);if(L.ok)a(I=>I.map(P=>P.id===N.id?L.data:P)),v(I=>{const P={...I};return delete P[N.id],P});else{const I=L.status===409?n("layout_editor.translation_manager.conflict"):L.message;w(P=>({...P,[N.id]:I}))}x(null)},[e,$,n]),D=u.useCallback(N=>typeof window<"u"&&typeof window.confirm=="function"?window.confirm(n("layout_editor.translation_manager.delete_confirm",{count:N})):!0,[n]),O=u.useCallback(async N=>{if(!D(1))return;const H=await Wy(e,N.id);H.ok?(a(L=>L.filter(I=>I.id!==N.id)),m(L=>{const I=new Set(L);return I.delete(N.id),I})):p(H.message)},[e,D]),M=u.useCallback(async N=>{if(N.length===0||!D(N.length))return;const H=await jy(e,N);if(H.ok){const L=new Set(N);a(I=>I.filter(P=>!L.has(P.id))),m(new Set)}else p(H.message)},[e,D]),B=u.useCallback(N=>{m(H=>{const L=new Set(H);return L.has(N)?L.delete(N):L.add(N),L})},[]);return i.jsxs("div",{className:"g7le-translation-manager","data-testid":"g7le-translation-manager",style:cm,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:um,children:[i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-translation-manager-title",children:n("layout_editor.translation_manager.title")}),i.jsx("button",{type:"button","aria-label":"close",onClick:r,style:pm,"data-testid":"g7le-translation-manager-close",children:"✕"})]}),i.jsxs("div",{style:fm,children:[i.jsx("div",{style:{display:"inline-flex",gap:4},role:"tablist","aria-label":n("layout_editor.translation_manager.filter_label"),children:["all","active","orphaned"].map(N=>i.jsx("button",{type:"button",role:"tab","aria-selected":f===N,"data-active":f===N?"true":"false",onClick:()=>y(N),style:f===N?gm:gd,"data-testid":`g7le-translation-filter-${N}`,children:n(`layout_editor.translation_manager.filter_${N}`)},N))}),i.jsx("div",{style:{flex:1}}),g.size>0&&i.jsxs("button",{type:"button",onClick:()=>void M(Array.from(g)),style:hd,"data-testid":"g7le-translation-bulk-delete",children:["🗑 ",n("layout_editor.translation_manager.delete_selected",{count:g.size})]}),S.length>0&&i.jsxs("button",{type:"button",onClick:()=>void M(S.map(N=>N.id)),style:hd,"data-testid":"g7le-translation-purge-orphaned",children:["🧹 ",n("layout_editor.translation_manager.purge_orphaned",{count:S.length})]})]}),c&&i.jsx("div",{style:hm,"data-testid":"g7le-translation-manager-error",children:c}),i.jsx("div",{style:ym,children:l==="loading"?i.jsx("div",{style:yd,"data-testid":"g7le-translation-manager-loading",children:n("layout_editor.translation_manager.loading")}):E.length===0&&l!=="error"?i.jsx("div",{style:yd,"data-testid":"g7le-translation-manager-empty",children:n("layout_editor.translation_manager.empty")}):i.jsx("div",{"data-testid":"g7le-translation-list",style:bm,children:E.map(N=>{const H=$(N),L=N.status==="orphaned",I=o?o.has(N.translation_key):null;return i.jsxs("div",{style:mm,"data-testid":`g7le-translation-row-${N.id}`,children:[i.jsxs("div",{style:vm,children:[i.jsx("input",{type:"checkbox",checked:g.has(N.id),onChange:()=>B(N.id),"aria-label":n("layout_editor.translation_manager.select_aria",{key:N.translation_key}),"data-testid":`g7le-translation-select-${N.id}`}),i.jsx("code",{style:_m,children:N.translation_key}),L&&i.jsx("span",{style:xm,"data-testid":`g7le-translation-badge-orphaned-${N.id}`,children:n("layout_editor.translation_manager.badge_orphaned")}),I===!0&&i.jsx("span",{style:wm,"data-testid":`g7le-translation-badge-live-inuse-${N.id}`,children:n("layout_editor.translation_manager.badge_live_in_use")}),I===!1&&i.jsx("span",{style:km,"data-testid":`g7le-translation-badge-live-unused-${N.id}`,children:n("layout_editor.translation_manager.badge_live_unused")}),i.jsx("div",{style:{flex:1}}),i.jsx("button",{type:"button",onClick:()=>void O(N),style:Tm,"data-testid":`g7le-translation-delete-${N.id}`,children:"🗑"})]}),i.jsxs("div",{style:Sm,children:[C.map(P=>{const F=H[P]??"",G=An(F).length>0;return i.jsxs("div",{style:Cm,"data-testid":`g7le-translation-field-${N.id}-${P}`,children:[i.jsxs("label",{style:$m,title:P,children:[Gn(P,n),i.jsxs("span",{style:Em,children:["(",P,")"]})]}),G?i.jsx("div",{style:{flex:1},"data-testid":`g7le-translation-chip-${N.id}-${P}`,children:i.jsx(mr,{value:F,onChange:V=>A(N.id,P,V),t:n,testIdSuffix:`${N.id}-${P}`})}):i.jsx("textarea",{value:F,rows:1,ref:am,onChange:V=>{A(N.id,P,V.currentTarget.value),fd(V.currentTarget)},style:Im,"data-testid":`g7le-translation-input-${N.id}-${P}`})]},P)}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[i.jsx("button",{type:"button",disabled:b===N.id,onClick:()=>void z(N),style:Am,"data-testid":`g7le-translation-save-${N.id}`,children:b===N.id?n("layout_editor.translation_manager.saving"):n("layout_editor.translation_manager.save")}),k[N.id]?i.jsx("span",{style:{fontSize:11,color:"#dc2626"},"data-testid":`g7le-translation-row-error-${N.id}`,children:k[N.id]}):null]})]})]},N.id)})})}),i.jsx("div",{style:Dm,children:i.jsx("button",{type:"button",onClick:r,style:Nm,"data-testid":"g7le-translation-manager-footer-close",children:n("layout_editor.translation_manager.close")})})]})}const cm={display:"flex",flexDirection:"column",minHeight:0,flex:1},um={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0"},pm={border:"none",background:"transparent",fontSize:16,color:"#94a3b8",cursor:"pointer"},fm={padding:"10px 16px",borderBottom:"1px solid #f1f5f9",display:"flex",gap:8,alignItems:"center"},gd={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},gm={...gd,borderColor:"#2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},hd={padding:"4px 10px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},hm={padding:"8px 16px",fontSize:12,color:"#b91c1c",background:"#fef2f2"},ym={padding:16,overflow:"auto",flex:1,minHeight:120,display:"flex",flexDirection:"column",gap:12},yd={fontSize:13,color:"#94a3b8",textAlign:"center",padding:"32px 0"},bm={display:"flex",flexDirection:"column",gap:20},mm={border:"1px solid #e2e8f0",borderRadius:8,padding:10,display:"flex",flexDirection:"column",gap:6},vm={display:"flex",alignItems:"center",gap:8},_m={fontSize:11,background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a"},xm={fontSize:10,color:"#b45309",background:"#fef3c7",padding:"1px 6px",borderRadius:4,fontWeight:600},wm={fontSize:10,color:"#15803d",background:"#dcfce7",padding:"1px 6px",borderRadius:4,fontWeight:600},km={fontSize:10,color:"#9f1239",background:"#ffe4e6",padding:"1px 6px",borderRadius:4,fontWeight:600},Sm={display:"flex",flexDirection:"column",gap:6},Cm={display:"flex",alignItems:"flex-start",gap:8},$m={fontSize:12,color:"#475569",minWidth:88,fontWeight:600,paddingTop:4,display:"flex",flexDirection:"column",lineHeight:1.2},Em={fontSize:10,color:"#94a3b8",fontWeight:400},Im={flex:1,padding:"4px 8px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:4,color:"#0f172a",resize:"vertical",overflow:"hidden",minHeight:28,lineHeight:1.4,fontFamily:"inherit",boxSizing:"border-box"},Am={padding:"5px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},Tm={padding:"4px 8px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Dm={padding:"10px 16px",borderTop:"1px solid #e2e8f0",display:"flex",justifyContent:"flex-end"},Nm={padding:"6px 14px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};function zm(){const e=Ft(),{state:t}=Je(),n=e?.document?.raw??null,o=t.templateIdentifier??"",r=t.selectedRoute?.layoutName??"",s=u.useCallback((c,p)=>{const f=n?.[c];return f===void 0?p:f},[n]),a=u.useCallback((c,p,...f)=>{e&&(f.length>=1?e.patchDocumentRaw(c,p,f[0]):e.patchDocumentRaw(c,p))},[e]),l=u.useCallback(async(c,p)=>!o||!r?null:qn(o,r,c,p),[o,r]),d=u.useCallback(async(c,p,f)=>o?yr(o,c,p,f):null,[o]);return{raw:n,getValue:s,patch:a,createI18nKey:l,updateI18nKeyValue:d}}function Mm(e){const t=e.__source;return t&&(t.kind==="module"||t.kind==="plugin")&&t.identifier?`${t.kind}:${t.identifier}`:"template"}function bd(e,t){if(!t)return null;const n=Mm(e);if(n&&t.bySource){const a=t.bySource[n]?.[e.id];if(a!==void 0)return{value:a,match:{dataSourceId:e.id,resolvedBy:"spec_byId",sourceKey:n}}}const o=t.byDataSourceId?.[e.id];if(o!==void 0)return{value:o,match:{dataSourceId:e.id,resolvedBy:"spec_byId"}};const r=e.endpoint??"";if(t.byEndpointPattern&&typeof r=="string"&&r.length>0){for(const[s,a]of Object.entries(t.byEndpointPattern))if(Lm(r,s))return{value:a,match:{dataSourceId:e.id,resolvedBy:"spec_byEndpoint"}}}return null}function ts(e,t){const n=bd(e,t);if(n)return n;const o=e.fallback?.data;return o!==void 0?{value:{data:Array.isArray(o)?o:[o]},match:{dataSourceId:e.id,resolvedBy:"fallback"}}:{value:{},match:{dataSourceId:e.id,resolvedBy:"inferred"}}}function Lm(e,t){const n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${n}$`).test(e)}function Pm(e={}){const{spec:t,overrideSpec:n,onMatch:o}=e;return{has(r){return!0},resolve(r){const s=n?bd(r,n):null,{value:a,match:l}=s??ts(r,t);if(o)try{o(l)}catch{}return a}}}const md=Lt(),vd=md.DataBindingEngine,_d=md.dataBindingEngine;bo("PipeRegistry");function Om(e){let t=null,n=0;for(let o=0;o<e.length;o++){const r=e[o],s=o>0?e[o-1]:"",a=o<e.length-1?e[o+1]:"";if(s!=="\\"){if((r==='"'||r==="'")&&!t){t=r;continue}if(r===t){t=null;continue}if(!t){if(r==="("||r==="["||r==="{"){n++;continue}if(r===")"||r==="]"||r==="}"){n--;continue}if(r==="|"&&a!=="|"&&s!=="|"&&n===0)return!0}}}return!1}const Bm=new Set(["comment"]);function xd(e){if(!e||typeof e!="object")return[];const t=[];for(const[n,o]of Object.entries(e)){if(Bm.has(n)||!o||typeof o!="object")continue;const r=o;typeof r.expr!="string"||r.expr.length===0||t.push({id:n,label:typeof r.label=="string"?r.label:void 0,params:Array.isArray(r.params)?r.params:[],expr:r.expr,group:typeof r.group=="string"?r.group:void 0,source:r.__source})}return t}function Wm(e,t){let n=e.replace(/\{(\w+)\*\}(\s*\?\?\s*)?/g,(o,r,s)=>{const a=t[r],l=a==null?[]:String(a).split(",").map(d=>d.trim()).filter(Boolean);return l.length===0?"":l.join(" ?? ")+(s?" ?? ":"")});return n=n.replace(/\{(\w+)\}/g,(o,r)=>{const s=t[r];return s===void 0?o:s}),n}function vr(e){const t=e.trim(),n=/^\{\{([\s\S]*)\}\}$/.exec(t);return n?n[1].trim():t}function wd(e){const t=e.trim();return/^\{\{[\s\S]*\}\}$/.test(t)?t:`{{ ${t} }}`}function kd(e,t){const n={};for(const[r,s]of Object.entries(t))n[r]=typeof s=="string"?vr(s):s;const o=Wm(e.expr,n);return wd(o)}function jm(e,t){if(typeof e!="string")return null;const n=Sd(vr(e));for(const o of t){const r=Hm(n,o.expr,o.params);if(r)return{recipeId:o.id,params:r}}return null}function Sd(e){return e.replace(/\s+/g," ").trim()}function Cd(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Hm(e,t,n){const o=[],r=new Set;let s="";const a=/\{(\w+)(\*?)\}(\s*\?\?\s*)?/g;let l=0,d;const c=Sd(t);for(;(d=a.exec(c))!==null;){const g=c.slice(l,d.index);s+=Cd(g).replace(/\\ /g,"\\s*");const m=d[2]==="*",h=!!d[3];m?(r.add(d[1]),s+="(.*?)",h&&(s+="(?:\\s*\\?\\?\\s*)?")):(s+="(.+?)",h&&(s+="\\s*\\?\\?\\s*")),o.push(d[1]),l=a.lastIndex}s+=Cd(c.slice(l)).replace(/\\ /g,"\\s*");let p;try{p=new RegExp(`^${s}$`)}catch{return null}const f=p.exec(e);if(!f)return null;const y={};if(o.forEach((g,m)=>{let h=(f[m+1]??"").trim();r.has(g)&&(h=h.split(/\s*\?\?\s*/).map(v=>v.trim()).filter(Boolean).join(", ")),y[g]=h}),n.length>0){const g=new Set(n.map(m=>m.key));for(const m of Object.keys(y))g.has(m)||delete y[m]}return y}function Fm(e){const t=`x.${e.field}`,n=Km(e.value);switch(e.cmp){case"=":return`${t} === ${n}`;case"!=":return`${t} !== ${n}`;case">":return`${t} > ${n}`;case"<":return`${t} < ${n}`;case">=":return`${t} >= ${n}`;case"<=":return`${t} <= ${n}`;case"includes":return`(${t} ?? []).includes(${n})`;default:return`${t} === ${n}`}}function Km(e){const t=e.trim();return t==="true"||t==="false"||t!==""&&!Number.isNaN(Number(t))||/^['"].*['"]$/.test(t)||/[.([{]/.test(t)?t:`'${t.replace(/'/g,"\\'")}'`}function ns(e){return!e||e.length===0?"true":e.map(Fm).join(" && ")}function Vm(e){const n=`(${(e.source??"").trim()||"[]"} ?? [])`;let o;switch(e.op){case"count":o=`${n}.filter(x => ${ns(e.conditions)}).length`;break;case"sum":{const r=(e.sumField??"").trim()||"value";o=`${n}.filter(x => ${ns(e.conditions)}).reduce((s, x) => s + (x.${r} ?? 0), 0)`;break}case"filter":o=`${n}.filter(x => ${ns(e.conditions)})`;break;case"toOptions":{const r=(e.valueField??"").trim()||"value",s=(e.labelField??"").trim()||"label";o=`${n}.map(x => ({ value: x.${r}, label: x.${s} }))`;break}case"nth":{const r=(e.index??"").trim()||"0",s=(e.prop??"").trim();o=`${n}[${r}]${s?`?.${s}`:""}`;break}case"firstOf":{const r=(e.candidates??[]).map(l=>l.trim()).filter(Boolean),s=r.length>0?r.join(" ?? "):"''",a=(e.fallback??"").trim();o=a?`${s} ?? '${a.replace(/'/g,"\\'")}'`:s;break}case"literal":{const r=e.literalKind??"string",s=(e.literalValue??"").trim();r==="number"?o=s===""?"0":s:r==="boolean"?o=s==="true"?"true":"false":o=`'${s.replace(/'/g,"\\'")}'`;break}default:o="[]"}return wd(o)}function Um(e){if(typeof e!="string")return null;const t=vr(e).trim();let n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)\.length$/.exec(t);return n?{key:"",op:"count",source:n[1].trim(),conditions:os(n[2])}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)\.reduce\(\(s, x\) => s \+ \(x\.(\w+(?:\.\w+)*) \?\? 0\), 0\)$/.exec(t),n?{key:"",op:"sum",source:n[1].trim(),conditions:os(n[2]),sumField:n[3]}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)$/.exec(t),n?{key:"",op:"filter",source:n[1].trim(),conditions:os(n[2])}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.map\(x => \(\{ value: x\.(\w+(?:\.\w+)*), label: x\.(\w+(?:\.\w+)*) \}\)\)$/.exec(t),n?{key:"",op:"toOptions",source:n[1].trim(),valueField:n[2],labelField:n[3]}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\[(.+?)\](?:\?\.(\w+(?:\.\w+)*))?$/.exec(t),n?{key:"",op:"nth",source:n[1].trim(),index:n[2].trim(),prop:n[3]}:null))))}function os(e){const t=e.trim();if(t==="true")return[];const n=[];for(const o of qm(t)){const r=Gm(o.trim());r&&n.push(r)}return n}function qm(e){const t=[];let n=0,o=0;for(let r=0;r<e.length;r++){const s=e[r];s==="("||s==="["||s==="{"?n++:s===")"||s==="]"||s==="}"?n--:n===0&&s==="&"&&e[r+1]==="&"&&(t.push(e.slice(o,r)),r++,o=r+1)}return t.push(e.slice(o)),t}function Gm(e){let t=/^\(x\.(\w+(?:\.\w+)*) \?\? \[\]\)\.includes\((.+)\)$/.exec(e);if(t)return{field:t[1],cmp:"includes",value:$d(t[2])};if(t=/^x\.(\w+(?:\.\w+)*)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(e),t){const n={"===":"=","!==":"!=",">":">","<":"<",">=":">=","<=":"<="};return{field:t[1],cmp:n[t[2]]??"=",value:$d(t[3])}}return null}function $d(e){const t=e.trim(),n=/^'(.*)'$/.exec(t)||/^"(.*)"$/.exec(t);return n?n[1]:t}function Ed(e,t){const n=jm(e,t);if(n)return{kind:"preset",recipeId:n.recipeId,params:n.params};const o=Um(e);return o?{kind:"custom",model:o}:{kind:"advanced"}}function Ym(e){if(e==null)return"null";if(Array.isArray(e))return"list";switch(typeof e){case"number":return"number";case"boolean":return"boolean";case"string":return"string";default:return"object"}}function Id(e,t){if(typeof e!="string"||e.trim()==="")return{ok:!1,reason:"empty"};const n=vr(e);try{const o=Om(n)?_d.evaluatePipeExpression(n,t??{},{skipCache:!0}):_d.evaluateExpression(n,t??{},{skipCache:!0});return{ok:!0,value:o,type:Ym(o)}}catch(o){return{ok:!1,reason:o instanceof Error?o.message:String(o)}}}function Ad(e,t){const n=Array.isArray(e?.data_sources)?e.data_sources:[],o=[];for(const r of n){const s=r?.id;if(typeof s!="string"||s.length===0)continue;const{value:a}=ts(r,t?.sampleData),l=typeof r.label_key=="string"?r.label_key:void 0;o.push({id:s,sample:a,labelKey:l})}return o}function Td(e,t){const n=[];t?.sampleGlobal&&typeof t.sampleGlobal=="object"&&n.push({scope:"_global",tree:t.sampleGlobal});const o={},r={},s={},a=(l,d)=>{if(!(!d||typeof d!="object"||Array.isArray(d)))for(const[c,p]of Object.entries(d))if(p&&typeof p=="object"&&!Array.isArray(p)){const f=l[c]&&typeof l[c]=="object"&&!Array.isArray(l[c])?l[c]:l[c]={};a(f,p)}else c in l||(l[c]=p)};for(const l of t?.states?.groups??[])for(const d of l.items??[]){const c=d.initialState;c?.local&&a(o,c.local),c?.query&&a(r,c.query),c?.route&&a(s,c.route)}return Object.keys(o).length&&n.push({scope:"_local",tree:o}),Object.keys(r).length&&n.push({scope:"query",tree:r}),Object.keys(s).length&&n.push({scope:"route",tree:s}),e?.computed&&typeof e.computed=="object"&&!Array.isArray(e.computed)&&n.push({scope:"_computed",tree:e.computed}),n}function Dd(e,t){const n={};for(const r of t)r.scope!=="_computed"&&(n[r.scope]=r.tree);const o={};for(const[r,s]of Object.entries(e)){if(typeof s!="string"){o[r]=s;continue}const a=Id(s,n);o[r]=a.ok?a.value:s}return o}function Xm(e,t){const n=Ad(e,t),o=Td(e,t),r={};for(const s of n)s.sample!==void 0&&(r[s.id]=s.sample);for(const s of o)s.scope==="_computed"?r[s.scope]=Dd(s.tree,o):r[s.scope]=s.tree;return r}function Nd(e){const{raw:t,spec:n,evaluateComputed:o}=e;return u.useMemo(()=>{const r=Ad(t,n),s=Td(t,n);if(o){const a=s.findIndex(l=>l.scope==="_computed");if(a>=0){const l=Dd(s[a].tree,s);s[a]={scope:"_computed",tree:l}}}return ji({dataSources:r,states:s,spec:n??void 0})},[t,n,o])}function Jm(e){const{raw:t,spec:n,pageType:o}=e;return u.useMemo(()=>{const r=Array.isArray(t?.data_sources)?t.data_sources:[],s=[];for(const d of r){const c=d?.id;if(typeof c!="string"||c.length===0)continue;const{value:p}=ts(d,n?.sampleData),f=typeof d.label_key=="string"?d.label_key:void 0;s.push({id:c,sample:p,labelKey:f})}const a=[];n?.sampleGlobal&&typeof n.sampleGlobal=="object"&&a.push({scope:"_global",tree:n.sampleGlobal}),t?.computed&&typeof t.computed=="object"&&!Array.isArray(t.computed)&&a.push({scope:"_computed",tree:t.computed});const l=ji({dataSources:s,states:a,spec:n??void 0});if(typeof o=="string"&&o!=="")for(const d of["title","description"])l.push({expression:`{{_seo.${o}.${d}}}`,source:"_seo",sourceId:"_seo",path:`${o}.${d}`,shape:"scalar",preview:""});return l},[t,n,o])}function zd(e,t){const n=[],o=new Set,r=s=>{for(const a of s??[]){if(a.kind==="route"&&!a.isRedirect&&typeof a.path=="string"&&a.path.startsWith("/")&&!o.has(a.path)){o.add(a.path);const l=a.label.startsWith("$t:")?a.label.slice(3):a.label,d=a.label.startsWith("$t:")?t(l):l,c=d&&d!==l&&!d.startsWith("$t:")?d:"";n.push({value:a.path,label:c?`${c} (${a.path})`:a.path})}r(a.children)}};return r(e),n}function Md(e){const t=Array.isArray(e?.data_sources)?e.data_sources:[],n=[];for(const o of t){const r=o?.id;typeof r=="string"&&r.length>0&&n.push({value:r,label:r})}return n}function rs(e,t){const n=typeof e.id=="string"?e.id:"",o=typeof e.label_key=="string"?e.label_key:"";if(!o)return null;const r=o.startsWith("$t:")?o.slice(3):o,s=t(r);return s&&s!==r&&!s.startsWith("$t:")&&s!==n?s:null}function is(e,t){const n=e.__source;if(!n||typeof n!="object"||Array.isArray(n))return null;const o=n;if(o.kind!=="extension")return null;const r=o.extensionSourceType==="plugin"?t("layout_editor.data_sources.source.plugin"):t("layout_editor.data_sources.source.module"),s=typeof o.extensionIdentifier=="string"?o.extensionIdentifier:"",a=typeof o.extensionName=="string"?o.extensionName:"",l=a&&a!==s?`${a} (${s})`:s;return l?`${r}: ${l}`:r}function Zm(e,t,n){const o=Array.isArray(e?.data_sources)?e.data_sources:[],r=[],s=new Set;for(const a of o){const l=a?.id;typeof l!="string"||l.length===0||s.has(l)||(s.add(l),r.push({id:l,friendly:rs(a,t),source:is(a,n)}))}return r}function Ld(e){const t=e?.states?.groups??[],n=new Set;for(const o of t)for(const r of o.items??[]){for(const s of Object.keys(r.initialState?.local??{}))n.add(s);for(const s of Object.keys(r.initialState?.global??{}))n.add(s)}return Array.from(n).map(o=>({value:o,label:o}))}function Qm(e,t){const n=Array.isArray(e?.modals)?e.modals:[],o=[],r=new Set;for(const s of n){const a=s?.id;typeof a!="string"||a.length===0||r.has(a)||(r.add(a),o.push({value:a,label:Rm(s,a,t)}))}return o}function Rm(e,t,n){const r=e?.meta?.editor_label;if(typeof r=="string"&&r.length>0)return Pd(r,n);const s=e?.title;return typeof s=="string"&&s.length>0?Pd(s,n):t}function Pd(e,t){if(e.startsWith("$t:")){const n=e.slice(3),o=t?t(n):n;return o&&o!==n?o:n}return e}const Od=bo("ActionRecipeEngine"),ev={api:"apiCall",nav:"navigate",showToast:"toast",setLocalState:"setState"},tv=new Set(["comment"]);function wo(e){if(!e||typeof e!="object")return[];const t=[];for(const[n,o]of Object.entries(e)){if(tv.has(n)||!o||typeof o!="object")continue;const r=o,s=r.build&&typeof r.build=="object"?r.build:typeof r.handler=="string"?{handler:r.handler}:void 0;!s||typeof s.handler!="string"||s.handler.length===0||t.push({id:n,label:typeof r.label=="string"?r.label:void 0,params:Array.isArray(r.params)?r.params:[],build:s})}return t}function on(e){const t=/^\{\{\s*([\w.]+)\s*\}\}$/.exec(e);return t?t[1]:null}const _r="...";function nv(e,t){return e.replace(/\{\{\s*([\w.]+)\s*\}\}/g,(n,o)=>{const r=t[o];return r==null?"":String(r)})}function ss(e,t){if(typeof e=="string"){const n=on(e);return n!==null?n in t?t[n]:void 0:e.includes("{{")?nv(e,t):e}if(Array.isArray(e))return e.map(n=>ss(n,t)).filter(n=>n!==void 0);if(e&&typeof e=="object"){const n={};for(const[o,r]of Object.entries(e)){if(o===_r&&typeof r=="string"){const d=on(r),c=d!==null?t[d]:void 0;if(c&&typeof c=="object"&&!Array.isArray(c))for(const[p,f]of Object.entries(c))f===void 0||p===""||(n[p]=f);continue}const s=ss(r,t);if(s===void 0||Array.isArray(s)&&s.length===0&&(o==="onSuccess"||o==="onError"))continue;const a=on(o),l=a!==null?a in t?String(t[a]):null:o;l===null||l===""||s&&typeof s=="object"&&!Array.isArray(s)&&Object.keys(s).length===0||(n[l]=s)}return n}return e}function ov(e){const t=e.handler;if(typeof t!="string")return e;const n=ev[t];if(n)return Od.warn(`action recipe used forbidden handler "${t}" — corrected to "${n}" (core handler rules)`),{...e,handler:n};if(t==="apiCall"||t==="navigate"){const o=e.params;if(o&&typeof o=="object")for(const r of["target","onSuccess","onError"])r in o&&Od.warn(`action recipe build placed "${r}" inside params for handler "${t}" — should be top-level (core handler rules)`)}return e}function xr(e,t){const n=ss(e.build,t);if(typeof n.handler!="string"&&e.build.handler.includes("{{")&&(n.handler=e.build.handler),e.build.handler.includes("{{")){const o=new Set(e.params.filter(r=>r.required).map(r=>r.key));o.size>0&&Bd(e.build,n,o)}return ov(n)}function Bd(e,t,n){if(!e||typeof e!="object"||Array.isArray(e)||!t||typeof t!="object"||Array.isArray(t))return;const o=e,r=t;for(const[s,a]of Object.entries(o))if(s!=="handler"){if(typeof a=="string"){const l=on(a);l!==null&&n.has(l)&&!(s in r)&&(r[s]=a)}else if(a&&typeof a=="object"){if(!(s in r)||typeof r[s]!="object"||r[s]===null)if(JSON.stringify(a).includes("{{"))r[s]={};else continue;Bd(a,r[s],n)}}}function rv(e,t){if(!e||typeof e!="object")return null;const n=e.handler;if(typeof n!="string")return null;let o=null,r=-1;for(const s of t){const a=s.build.handler.includes("{{");if(!a&&s.build.handler!==n||a&&!iv(s.build,e))continue;const l=sv(s.build,e);if(l===null)continue;const d=Object.keys(l).length;d>r&&(r=d,o={recipeId:s.id,values:l})}return o}function iv(e,t){if(typeof t.handler=="string"&&t.handler===e.handler)return!0;const n=e.params;if(!n||typeof n!="object")return!1;const o=t.params;if(!o||typeof o!="object")return!1;const r=o,s=Object.entries(n).filter(([,a])=>typeof a=="string"&&on(a)!==null);return s.length===0?!1:s.every(([a])=>a in r)}function sv(e,t){const n={},o=(r,s)=>{if(typeof r=="string"){const a=on(r);return a!==null?(n[a]=s,!0):r.includes("{{")?typeof s=="string":r===s}if(Array.isArray(r))return Array.isArray(s);if(r&&typeof r=="object"){if(!s||typeof s!="object"||Array.isArray(s))return!1;const a=s,l=r[_r],d=typeof l=="string"?on(l):null,c=new Set(Object.keys(r).filter(p=>p!==_r));for(const[p,f]of Object.entries(r))if(p!==_r){if(typeof f=="string"&&on(f)!==null){const y=on(f);p in a&&(n[y]=a[p]);continue}if(!(p in a)){if(p==="onSuccess"||p==="onError"||p==="params"||p==="identity_target")continue;return!1}if(!o(f,a[p]))return!1}if(d!==null){const p={};for(const[f,y]of Object.entries(a))c.has(f)||(p[f]=y);Object.keys(p).length>0&&(n[d]=p)}return!0}return r===s};return o(e,t)?n:null}function Yn(e,t){const n=typeof e?.handler=="string"?e.handler:"",o=rv(e,t);return o?{kind:"preset",recipeId:o.recipeId,values:o.values,handler:n}:{kind:"advanced",handler:n}}function Wd(e,t,n){const o=e?.label,r=o?n?n(o):o:"",s=[],a=e?.params??[];for(const l of a){if(l.advanced||l.widget==="action-list")continue;const d=t[l.key];if(d==null||d==="")continue;const c=av(d);if(c!==""&&(s.push(c),s.length>=2))break}return s.length===0?r:`${r} (${s.join(", ")})`}function av(e){return typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"?String(e):Array.isArray(e)?`[${e.length}]`:e&&typeof e=="object"?"{…}":""}function jd(e,t){const n={...e};return typeof t=="string"&&t.trim().length>0?n.if=t.trim():delete n.if,n}function as(e){const t=e?.if;return typeof t=="string"&&t.trim().length>0?t:null}const lv=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/,dv=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,cv=/\{\{.*?\}\}/;function Hd(e,t){const n=typeof e=="string"?e:"",o=lv.exec(n);if(o){const a=o[1],l=t?t(a):"";return{customKey:a,binding:!1,paramKey:!1,displayValue:l}}const r=mt(n);if(r&&r.key.startsWith("custom.")){const a=t?t(r.key):"";return{customKey:r.key,binding:!1,paramKey:!0,displayValue:a}}if(cv.test(n))return{customKey:null,binding:!0,paramKey:!1,displayValue:n};const s=dv.exec(n);if(s){const a=s[1],l=t?t(a):"";return{customKey:null,binding:!1,paramKey:!1,displayValue:l&&l!==a?l:""}}return{customKey:null,binding:!1,paramKey:!1,displayValue:n}}function uv(){const{state:e}=Je(),t=e.templateIdentifier,n=e.locale,o=e.selectedRoute?.layoutName??null,r=u.useCallback(l=>{try{const c=Pt.getInstance().translate(l,{templateId:t,locale:n});return c&&c!==l?c:""}catch{return""}},[t,n]),s=u.useCallback(l=>Hd(l,r),[r]),a=u.useCallback(async(l,d)=>{const c=Hd(l,r);if(c.binding)return{kind:"blocked"};if(c.customKey){if(d===c.displayValue)return{kind:"noop",customKey:c.customKey};const y=await yr(t,c.customKey,n,d);return y.kind==="error"?{kind:"error",message:y.message}:(await tn(t,n),{kind:"updated",customKey:c.customKey})}const p=await qn(t,o,n,d);if(p.kind==="error"||!p.resource)return{kind:"error",message:p.message};const f=p.resource.translation_key;return await tn(t,n),{kind:"created",customKey:f,token:`$t:${f}`}},[t,n,o,r]);return{locale:n,templateIdentifier:t,translate:r,classify:s,commitText:a}}async function ls(e,t,n,o,r,s,a,l,d,c){const p=c?Yi(o,c):null,f=!!(p&&p.keyifiable),y=d?d(o):o,g=f?p.chipValue:Xl(y),h=`p${f?p.bindings.length:cr(y).length}`,v=Math.max(0,Math.min(r,g.length)),b=Hl(g,v,h),x=ur(b).replace(/\{\{?p\d+\}?\}/g," ").replace(/\s+/g," ").trim(),k=x.length>0?x:b,w=await qn(e,t,n,k);if(w.kind==="error"||!w.resource)return{kind:"error",message:w.message};const _=w.resource.translation_key,C=An(b),E=w.resource.values??{},S={[n]:b};for(const z of Object.keys(E)){if(z===n)continue;let D=E[z]??"";for(const O of C)D=qi(D,O);S[z]=D}xo(e,_,S);const T=En(s,a,l);return{kind:"ok",text:`${f?Gl(`$t:${_}`,p):Yl(`$t:${_}`,y)}|${h}=${T}`,key:_}}async function wr(e,t,n,o,r,s,a){let l=mt(n);if(!l&&typeof n=="string"){const h=/^\s*\$t:(custom\.[A-Za-z0-9._-]+)\s*$/.exec(n);h&&(l={key:h[1],params:[]})}if(!l)return{kind:"error",message:"not a param key"};if(!l.key.startsWith("custom."))return{kind:"error",message:"not a custom param key"};const d=hb(l.params.map(h=>h.name)),c=En(r,s,a),p=`${n.trim()}|${d}=${c}`,f=await cn(e,l.key),y=jb(l.key)??{},g=new Set([...Object.keys(f?.values??{}),...Object.keys(y),t]);if(g.size===0)return{kind:"error",message:"custom key not found"};const m={};for(const h of g){const v=It(l.key,h)??f?.values?.[h]??"";m[h]=h===t?Hl(v,o,d):qi(v,d)}return xo(e,l.key,m),{kind:"ok",text:p,paramName:d}}async function ko(e,t,n,o){return br(e,t,n,o),{kind:"ok"}}async function pv(e,t,n){const o=await cn(e,t);if(!o)return{kind:"error",message:"custom key not found"};const r={};for(const[s,a]of Object.entries(o.values??{})){const l=It(t,s)??a;r[s]=n(l)}xo(e,t,r);try{const s=Pt.getInstance();for(const[a,l]of Object.entries(r))s.setTranslationValue(e,a,t,l)}catch{}return{kind:"ok"}}function Fd(e,t,n){return pv(e,t,o=>Ql(o,n))}function Kd(e,t){if(!(typeof window>"u"))try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:t}}))}catch{}}async function kr(e,t,n,o){const r=await Fd(e,t,n);return Kd(e,o),r}const fv=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/;function gv(e,t){const n=s=>{const a=[],l=/\{\{?(p\d+)\}?\}/g;let d;for(;(d=l.exec(s))!==null;)a.push(d[1]);return a},o=n(e),r=n(t);return o.length!==r.length?!0:o.some((s,a)=>s!==r[a])}function Vd(e){const t=e.text;if(typeof t!="string")return null;const n=fv.exec(t);if(n)return n[1];const o=mt(t);return o&&o.key.startsWith("custom.")?o.key:null}function ds(e){const t={},n=mt(typeof e=="string"?e:"");for(const o of n?.params??[])t[o.name]=Vt(o.expression);return t}function hv(e){return ds(typeof e.text=="string"?e.text:"")}function Ud({customKey:e,templateIdentifier:t,t:n,paramLabels:o,locales:r,onRemoveParam:s}){const a=r??ud(),l=Ft(),d=o??{},[c,p]=u.useState({status:"idle",values:{}}),[f,y]=u.useState(!1),[g,m]=u.useState(!1),[h,v]=u.useState(null),b=u.useRef({});u.useEffect(()=>{let $=!1;if(!e){p({status:"not_a_key",values:{}});return}return p({status:"loading",values:{}}),(async()=>{const A=await cn(t,e);if($)return;if(!A){p({status:"error",values:{}});return}const z={...A.values??{}};for(const D of Object.keys(z)){const O=It(e,D);O!==void 0&&(z[D]=O)}for(const D of a){const O=It(e,D);O!==void 0&&(z[D]=O)}b.current={...z},p({status:"loaded",id:A.id,lockVersion:A.lock_version,values:z})})(),()=>{$=!0}},[e,t]),u.useEffect(()=>{if(typeof window>"u"||!e)return;const $=A=>{const z=A.detail;z?.templateIdentifier&&z.templateIdentifier!==t||p(D=>{if(D.status!=="loaded")return D;let O=!1;const M={...D.values};for(const B of Object.keys(M)){const N=It(e,B);N!==void 0&&N!==M[B]&&(M[B]=N,O=!0)}return O?(b.current={...b.current,...M},{...D,values:M}):D})};return window.addEventListener(kt,$),()=>window.removeEventListener(kt,$)},[e,t]);const x=u.useCallback(($,A)=>{p(z=>{const D=z.values[$]??"",O={...z,values:{...z.values,[$]:A}};return e&&gv(D,A)&&(br(t,e,$,A),b.current={...b.current,[$]:A},l?.markDirty?.(),Kd(t,$)),O})},[e,t,l]),k=u.useCallback($=>{p(A=>{const z={};for(const[D,O]of Object.entries(A.values))z[D]=Ql(O,$);return b.current={...z},{...A,values:z}}),s?.($)},[s]),w=u.useMemo(()=>[...Object.values(c.values),...Object.values(b.current)].some(A=>An(A).length>0),[c.values]),_=u.useRef(null),C=u.useCallback(()=>{m(!0),_.current&&clearTimeout(_.current),_.current=setTimeout(()=>m(!1),1500)},[]);u.useEffect(()=>()=>{_.current&&clearTimeout(_.current)},[]);const E=u.useCallback(async()=>{if(m(!1),c.status!=="loaded"||c.id===void 0)return;const $=b.current;for(const[z,D]of Object.entries(c.values)){const O=$[z]??"";if(w){const M=An(O);if(M.length===0)continue;const B=An(D);if(!(M.length===B.length&&M.every((H,L)=>H===B[L]))){v(n("layout_editor.translation.placeholder_mismatch"));return}continue}if(Ui(O)&&!kb(O,D)){v(n("layout_editor.translation.token_mismatch"));return}}if(y(!0),v(null),w){xo(t,e??"",c.values),b.current={...c.values},l?.markDirty?.(),Et({op:"translation_field_update",translationKey:e,changedLocales:Object.keys(c.values),timestamp:Date.now()}),await new Promise(z=>setTimeout(z,0)),y(!1),C();return}const A=`/api/admin/templates/${encodeURIComponent(t)}/custom-translations/${c.id}`;try{const z=await fetch(A,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:c.values,expected_lock_version:c.lockVersion??0})}),D=await z.json().catch(()=>null);if(z.status===409){v(n("layout_editor.translation.conflict"));return}if(!z.ok){v(D?.message??n("layout_editor.translation.save_failed"));return}const O=typeof D?.data?.lock_version=="number"?D.data.lock_version:(c.lockVersion??0)+1;p(M=>({...M,lockVersion:O})),b.current={...c.values},await tn(t),Et({op:"translation_field_update",translationKey:e,changedLocales:Object.keys(c.values),timestamp:Date.now()}),C()}catch(z){v(z instanceof Error?z.message:n("layout_editor.translation.save_failed"))}finally{y(!1)}},[c,t,e,n]);if(c.status==="not_a_key"||!e)return i.jsx("div",{"data-testid":"g7le-translation-not-a-key",style:cs,children:n("layout_editor.translation.not_a_key")});if(c.status==="loading"||c.status==="idle")return i.jsx("div",{"data-testid":"g7le-translation-loading",style:cs,children:n("layout_editor.translation.loading")});if(c.status==="error")return i.jsx("div",{"data-testid":"g7le-translation-error",style:cs,children:n("layout_editor.translation.load_failed")});const S=new Set([...a,...Object.keys(c.values)]),T=Array.from(S);return i.jsxs("div",{"data-testid":"g7le-translation-field",style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsxs("div",{"data-testid":"g7le-translation-key",style:yv,children:[n("layout_editor.translation.key_label"),": ",i.jsx("code",{style:bv,children:e})]}),T.map($=>{const A=c.values[$]??"",z=A.trim().length===0;return i.jsxs("div",{className:"g7le-translation-row","data-testid":`g7le-translation-row-${$}`,style:mv,children:[i.jsxs("label",{htmlFor:`g7le-translation-input-${$}`,style:{fontSize:12,color:"#475569",minWidth:72,display:"inline-flex",alignItems:"center",gap:4},children:[Gn($,n),z&&i.jsx("span",{"data-testid":`g7le-translation-missing-${$}`,style:vv,children:n("layout_editor.translation.needs_translation")})]}),w?i.jsx("div",{style:{flex:1},"data-testid":`g7le-translation-chip-${$}`,children:i.jsx(mr,{value:A,onChange:D=>x($,D),t:n,paramLabels:d,testIdSuffix:$,onRemoveChip:s?k:void 0})}):i.jsx("input",{id:`g7le-translation-input-${$}`,type:"text","data-testid":`g7le-translation-input-${$}`,value:A,placeholder:z?n("layout_editor.translation.needs_translation"):"",onChange:D=>x($,D.currentTarget.value),onBlur:E,style:{flex:1,padding:"4px 8px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:4,color:z?"#94a3b8":"#0f172a"}})]},$)}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[i.jsx("button",{type:"button","data-testid":"g7le-translation-save",disabled:f,onClick:E,style:_v,children:n(f?"layout_editor.translation.saving":"layout_editor.translation.save")}),g&&!f&&i.jsxs("span",{"data-testid":"g7le-translation-saved",style:{fontSize:11,color:"#16a34a",display:"inline-flex",alignItems:"center",gap:4},children:["✓ ",n("layout_editor.translation.saved")]}),h&&i.jsx("span",{"data-testid":"g7le-translation-save-error",style:{fontSize:11,color:"#dc2626"},children:h})]})]})}const cs={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},yv={fontSize:12,color:"#475569"},bv={fontSize:11,background:"#f1f5f9",padding:"1px 4px",borderRadius:3,color:"#0f172a"},mv={display:"flex",alignItems:"center",gap:8},vv={fontSize:10,color:"#b45309",background:"#fef3c7",padding:"0 4px",borderRadius:3},_v={padding:"6px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"};function Xn({anchorRef:e,open:t,onClose:n,children:o,placement:r="bottom",minWidth:s=240,maxWidth:a=360,margin:l=8,testid:d}){const c=u.useRef(null),[p,f]=u.useState(null),y=u.useCallback(()=>{const m=e.current,h=c.current;if(!m||!h)return;const v=m.getBoundingClientRect(),b=window.innerWidth,x=window.innerHeight,k=h.getBoundingClientRect(),w=Math.min(Math.max(k.width,s),a),_=k.height;let C=v.left;C+w+l>b&&(C=v.right-w),C=Math.min(Math.max(C,l),b-w-l);const E=x-v.bottom-l,S=v.top-l;let T,$;const A=r==="bottom",z=E>=Math.min(_,160),D=S>=Math.min(_,160);A&&z||A&&!D?(T=v.bottom+4,$=E):!A&&D||D?(T=v.top-4-Math.min(_,S),$=S):E>=S?(T=v.bottom+4,$=E):(T=l,$=S),f({left:C,top:T,maxHeight:Math.max($,120)})},[e,r,s,a,l]);if(u.useLayoutEffect(()=>{if(!t){f(null);return}y()},[t,y]),u.useEffect(()=>{if(!t)return;const m=()=>y();window.addEventListener("resize",m),window.addEventListener("scroll",m,!0);const h=b=>{const x=c.current,k=e.current,w=b.target;x?.contains(w)||k?.contains(w)||n()},v=b=>{b.key==="Escape"&&n()};return document.addEventListener("pointerdown",h,!0),document.addEventListener("keydown",v),()=>{window.removeEventListener("resize",m),window.removeEventListener("scroll",m,!0),document.removeEventListener("pointerdown",h,!0),document.removeEventListener("keydown",v)}},[t,y,n,e]),!t)return null;const g={...xv,minWidth:s,maxWidth:`min(${a}px, calc(100vw - ${l*2}px))`,...p?{left:p.left,top:p.top,maxHeight:p.maxHeight,visibility:"visible"}:{left:-9999,top:-9999,visibility:"hidden"}};return i.jsx("div",{ref:c,"data-testid":d,style:g,role:"dialog",children:o})}const xv={position:"fixed",zIndex:1e3,background:"#fff",border:"1px solid #cbd5e1",borderRadius:8,boxShadow:"0 4px 16px rgba(0,0,0,0.14)",padding:6,overflowY:"auto",boxSizing:"border-box"};function qd(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function $t({candidates:e,t,onSelect:n,testIdSuffix:o,defaultOpen:r=!1,floating:s=!0}){const[a,l]=u.useState(r),[d,c]=u.useState(""),p=u.useRef(null),f=u.useMemo(()=>Fi(e,"scalar").map(h=>({...h,resolvedLabel:qd(t,h.labelKey,h.path||h.sourceId)})),[e,t]),y=u.useMemo(()=>Ki(f,d),[f,d]),g=i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":`g7le-inline-binding-search-input-${o}`,value:d,placeholder:t("layout_editor.inline_binding.search_placeholder"),onChange:m=>c(m.target.value),style:Cv}),i.jsx("div",{style:$v,children:y.length===0?i.jsx("div",{style:Ev,children:t("layout_editor.inline_binding.no_results")}):y.map(m=>i.jsxs("button",{type:"button","data-testid":`g7le-inline-binding-candidate-${m.expression}`,onClick:()=>{n(m),l(!1),c("")},style:Iv,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:m.resolvedLabel}),i.jsxs("span",{style:Av,children:[qd(t,m.groupLabelKey,m.sourceId)," · ",m.path||m.sourceId]})]}),i.jsx("span",{style:Tv,children:m.preview})]},m.expression))})]});return i.jsxs("div",{style:s?Sv:{marginTop:4},children:[i.jsx("button",{ref:p,type:"button","data-testid":`g7le-inline-binding-search-toggle-${o}`,onClick:()=>l(m=>!m),title:t("layout_editor.inline_binding.search"),"aria-label":t("layout_editor.inline_binding.search"),"aria-expanded":a,style:wv,children:"🔍"}),s?i.jsx(Xn,{anchorRef:p,open:a,onClose:()=>l(!1),testid:`g7le-inline-binding-picker-${o}`,children:g}):a&&i.jsx("div",{"data-testid":`g7le-inline-binding-picker-${o}`,style:kv,children:g})]})}const wv={fontSize:13,lineHeight:1,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 8px",cursor:"pointer",textAlign:"center",whiteSpace:"nowrap"},kv={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},Sv={display:"inline-block"},Cv={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},$v={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},Ev={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},Iv={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},Av={fontSize:10,color:"#94a3b8"},Tv={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},Dv=["$localized"];function Jn(e){const t=e.trim();if(!t.startsWith("{{")||!t.endsWith("}}")||t.length<4)return null;let n=0;for(let o=0;o<t.length;o++){if(t[o]==="{"&&t[o+1]==="{"){n++,o++;continue}if(t[o]==="}"&&t[o+1]==="}"&&(n--,o++,n===0))return o===t.length-1?t.slice(2,-2).trim():null}return null}function Gd(e){if(typeof e!="string"||e.trim()===""||Jn(e)!==null)return!1;const t=e.match(/\{\{(?:[^}]|\}(?!\}))*\}\}/g);return!t||t.length===0?!1:t.some(n=>un(n).decomposed)}function So(e,t,n,o){switch(e.kind){case"leaf":Yd(e.text,t,n,o);return;case"conditional":So(e.then,t,n,o),o.push({kind:"ellipsis",text:"⋯"});return;case"fallback":So(e.primary,t,n,o);return;case"concat":e.parts.forEach(r=>So(r,t,n,o));return;case"raw":default:o.push({kind:"text",text:e.source});return}}function Yd(e,t,n,o){const r=e.trim();if(/^\{\{([\s\S]*)\}\}$/.exec(r)&&!r.slice(2,-2).includes("}}")){o.push({kind:"chip",text:n(r)});return}if(r.startsWith("$t:")){const a=r.slice(3).split("|")[0];o.push({kind:"text",text:t(a)||a});return}for(const a of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))a!==""&&(/^\{\{[\s\S]*\}\}$/.test(a)?o.push({kind:"chip",text:n(a)}):o.push({kind:"text",text:a}))}function us(e,t,n){const o=[];if(typeof e!="string"||e==="")return o;if(Jn(e)===null&&/\{\{/.test(e)){for(const a of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))if(!(a===void 0||a===""))if(/^\{\{[\s\S]*\}\}$/.test(a)){const l=un(a);So(l.node,t,n,o)}else Yd(a,t,n,o);return o}const s=un(e);return So(s.node,t,n,o),o}function Nv(e){if(typeof e!="string"||e.trim()==="")return"empty";const t=e.trim();if(Jn(t)===null)return"simple";const n=un(e);return n.node.kind==="leaf"?"simple":n.decomposed?"expression":"raw"}function un(e){const t=typeof e=="string"?e:"",n=t.trim();if(n==="")return{node:{kind:"leaf",text:t},decomposed:!1,original:t};const o=Jn(n);if(o===null)return{node:{kind:"leaf",text:t},decomposed:!1,original:t};let r;try{r=Hv(o)}catch{r=null}if(r===null)return{node:{kind:"raw",source:o},decomposed:!1,original:t};if(r.kind==="leaf"||r.kind==="raw")return{node:r,decomposed:!1,original:t};if(r.kind==="fallback"&&r.op==="??"&&zv(r.primary)&&Mv(r.fallback))return{node:{kind:"leaf",text:t},decomposed:!1,original:t};const s=Zn(r,!0);return qv(o,s)?{node:r,decomposed:!0,original:t}:{node:{kind:"raw",source:o},decomposed:!1,original:t}}function Zn(e,t=!1){const n=Jd(e);return t?n:e.kind==="leaf"?e.text:`{{${n}}}`}function Co(e){return Zn({kind:"conditional",condition:{kind:"simple",left:"",op:"truthy",right:""},then:{kind:"leaf",text:typeof e=="string"?e:""},else:{kind:"leaf",text:""}},!1)}function ps(e){return Zn({kind:"fallback",op:"??",primary:{kind:"leaf",text:typeof e=="string"?e:""},fallback:{kind:"leaf",text:""}},!1)}function $o(e){switch(e.kind){case"leaf":return e.text;case"conditional":return $o(e.then);case"fallback":return $o(e.primary);case"concat":return e.parts.map($o).join("");case"raw":default:return e.kind==="raw"?`{{${e.source}}}`:""}}function uM(e){return e}function Sr(e){if(typeof e!="string"||e.trim()==="")return"";if(Jn(e)===null&&/\{\{/.test(e)){for(const n of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))if(!(n===void 0||n===""))return/^\{\{[\s\S]*\}\}$/.test(n)?$o(un(n).node):n;return""}return $o(un(e).node)}function zv(e){if(e.kind!=="leaf")return!1;const t=/^\{\{([\s\S]*)\}\}$/.exec(e.text.trim());return t?Qn(t[1].trim()):!1}function Mv(e){if(e.kind!=="leaf")return!1;const t=e.text.trim();return!(/\{\{/.test(t)||t.startsWith("$t:"))}function Cr(e){return`'${e.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`}function Lv(e){const t=[];let n=0,o=0;const r=e.length;for(;o<r;){if(e[o]==="{"&&e[o+1]==="{"){o>n&&t.push(Cr(e.slice(n,o)));let s=o+2;for(;s<r&&!(e[s]==="}"&&e[s+1]==="}");)s++;if(s>=r)break;const a=e.slice(o+2,s).trim();t.push(a),o=s+2,n=o;continue}o++}return n<r&&t.push(Cr(e.slice(n))),t.length===0?Cr(e):t.join(" + ")}function Pv(e){const t=e.trim(),n=/^\{\{([\s\S]*)\}\}$/.exec(t);return n?n[1].trim():!t.startsWith("$t:")&&/\{\{/.test(t)?Lv(e):Cr(e)}const Xd="false";function Ov(e){if(e.kind==="raw")return e.source;if(e.left.trim()==="")return Xd;if(e.op==="truthy")return e.left;if(e.op==="falsy")return`!${Bv(e.left)?`(${e.left})`:e.left}`;const t=e.right.trim()===""?"''":e.right;return`${e.left} ${e.op} ${t}`}function Bv(e){return/[\s()?:|&+]/.test(e.trim())&&!/^[A-Za-z_$][\w$.?[\]]*$/.test(e.trim())}function Jd(e){switch(e.kind){case"leaf":return Pv(e.text);case"raw":return e.source;case"concat":return e.parts.map(t=>Eo(t,"concat")).join(" + ");case"fallback":return`${Eo(e.primary,"fallback")} ${e.op} ${Eo(e.fallback,"fallback")}`;case"conditional":return`${Ov(e.condition)} ? ${Eo(e.then,"conditional")} : ${Eo(e.else,"conditional")}`;default:return""}}function Eo(e,t){const n=Jd(e);return e.kind==="leaf"||e.kind==="raw"?n:e.kind==="conditional"?t==="conditional"?n:`(${n})`:e.kind==="fallback"?t==="concat"?`(${n})`:n:(e.kind==="concat",n)}const Wv=["===","!==","??","||","&&","==","!=",">=","<="];function Zd(e){const t=[];let n=0;const o=e.length;for(;n<o;){const r=e[n];if(/\s/.test(r)){n++;continue}if(r==="'"||r==='"'||r==="`"){const a=r;let l=n+1,d=a;for(;l<o;){if(e[l]==="\\"){d+=e[l]+(e[l+1]??""),l+=2;continue}if(e[l]==="{"&&e[l+1]==="{"){let c=l+2;for(;c<o&&!(e[c]==="}"&&e[c+1]==="}");)c++;if(c>=o)throw new Error("unterminated-binding-in-string");d+=e.slice(l,c+2),l=c+2;continue}if(e[l]===a){d+=a,l++;break}if(a==="`"&&e[l]==="$"&&e[l+1]==="{")throw new Error("template-literal-interpolation-unsupported");d+=e[l],l++}if(l>o||d[d.length-1]!==a)throw new Error("unterminated-string");t.push({kind:"string",value:d,start:n,end:l}),n=l;continue}if(r==="{"&&e[n+1]==="{"){let a=n+2;for(;a<o;){if(e[a]==="}"&&e[a+1]==="}"){a+=2;break}a++}if(e[a-1]!=="}"||e[a-2]!=="}")throw new Error("unterminated-binding");t.push({kind:"binding",value:e.slice(n,a),start:n,end:a}),n=a;continue}if(r==="$"&&e.startsWith("$t:",n)){let a=n+3;for(;a<o&&/[a-zA-Z0-9._-]/.test(e[a]);)a++;t.push({kind:"tkey",value:e.slice(n,a),start:n,end:a}),n=a;continue}let s="";for(const a of Wv)if(e.startsWith(a,n)){s=a;break}if(s){t.push({kind:"op",value:s,start:n,end:n+s.length}),n+=s.length;continue}if(r==="?"){t.push({kind:"op",value:"?",start:n,end:n+1}),n++;continue}if(r===":"){t.push({kind:"op",value:":",start:n,end:n+1}),n++;continue}if(r==="+"){t.push({kind:"op",value:"+",start:n,end:n+1}),n++;continue}if(r===">"||r==="<"){t.push({kind:"op",value:r,start:n,end:n+1}),n++;continue}if(r==="!"){t.push({kind:"op",value:"!",start:n,end:n+1}),n++;continue}if(r==="("){t.push({kind:"lparen",value:"(",start:n,end:n+1}),n++;continue}if(r===")"){t.push({kind:"rparen",value:")",start:n,end:n+1}),n++;continue}if(r==="["){t.push({kind:"lbracket",value:"[",start:n,end:n+1}),n++;continue}if(r==="]"){t.push({kind:"rbracket",value:"]",start:n,end:n+1}),n++;continue}if(r==="."){t.push({kind:"dot",value:".",start:n,end:n+1}),n++;continue}if(/[0-9]/.test(r)){let a=n;for(;a<o&&/[0-9.]/.test(e[a]);)a++;t.push({kind:"number",value:e.slice(n,a),start:n,end:a}),n=a;continue}if(/[A-Za-z_$]/.test(r)){let a=n;for(;a<o&&/[A-Za-z0-9_$]/.test(e[a]);)a++;t.push({kind:"ident",value:e.slice(n,a),start:n,end:a}),n=a;continue}t.push({kind:"other",value:r,start:n,end:n+1}),n++}return t}class jv{constructor(t){Ti(this,"toks");Ti(this,"pos",0);Ti(this,"src");this.src=t,this.toks=Zd(t)}parse(){const t=this.parseTernary();if(this.pos<this.toks.length)throw new Error("trailing-tokens");return t}peek(){return this.toks[this.pos]}next(){return this.toks[this.pos++]}parseTernary(){const t=this.pos,n=this.findTopLevelQuestion(t);if(n===-1)return this.parseFallback();const o=this.sliceSource(t,n);this.pos=n+1;const r=this.parseTernary(),s=this.next();if(!s||s.kind!=="op"||s.value!==":")throw new Error("expected-colon");const a=this.parseTernary();return{kind:"conditional",condition:Kv(o.trim()),then:r,else:a}}findTopLevelQuestion(t){let n=0;for(let o=t;o<this.toks.length;o++){const r=this.toks[o];if(r.kind==="lparen"||r.kind==="lbracket")n++;else if(r.kind==="rparen"||r.kind==="rbracket"){if(n===0)return-1;n--}else if(n===0&&r.kind==="op"&&r.value==="?"){if(this.toks[o+1]?.kind==="dot")continue;return o}else if(n===0&&r.kind==="op"&&r.value===":")return-1}return-1}sliceSource(t,n){if(t>=n)return"";const o=this.toks[t].start,r=this.toks[n-1].end;return this.src.slice(o,r)}parseFallback(){let t=this.parseConcat(),n=this.peek();for(;n&&n.kind==="op"&&(n.value==="??"||n.value==="||");){const o=n.value;this.next();const r=this.parseConcat();t={kind:"fallback",op:o,primary:t,fallback:r},n=this.peek()}return t}parseConcat(){const t=this.parseUnaryOrPrimary(),n=[t];let o=this.peek();for(;o&&o.kind==="op"&&o.value==="+";)this.next(),n.push(this.parseUnaryOrPrimary()),o=this.peek();return n.length===1?t:{kind:"concat",parts:n}}parseUnaryOrPrimary(){return this.parsePrimary()}parsePrimary(){const t=this.peek();if(!t)throw new Error("unexpected-end");if(t.kind==="lparen"){this.next();const n=this.parseTernary(),o=this.next();if(!o||o.kind!=="rparen")throw new Error("expected-rparen");return n}if(t.kind==="string")return this.next(),{kind:"leaf",text:Rd(t.value)};if(t.kind==="tkey")return this.next(),{kind:"leaf",text:t.value};if(t.kind==="binding")return this.next(),{kind:"leaf",text:t.value};if(t.kind==="ident"&&Dv.includes(t.value)&&this.toks[this.pos+1]?.kind==="lparen"){const n=this.tryParsePathWrapperCall(t.value);if(n!==null)return n;throw new Error("path-wrapper-non-simple-arg")}if(t.kind==="ident")return{kind:"leaf",text:`{{${this.parsePath()}}}`};if(t.kind==="number")return this.next(),{kind:"leaf",text:t.value};throw new Error(`unparseable-primary:${t.kind}`)}parsePath(){let t="";const n=this.next();if(!n||n.kind!=="ident")throw new Error("expected-ident");for(t+=n.value;;){const o=this.peek();if(!o)break;if(o.kind==="op"&&o.value==="?"&&this.toks[this.pos+1]?.kind==="dot"){this.next(),this.next();const r=this.next();if(!r||r.kind!=="ident")throw new Error("expected-ident-after-optional");t+="?."+r.value;continue}if(o.kind==="dot"){this.next();const r=this.next();if(!r||r.kind!=="ident")throw new Error("expected-ident-after-dot");t+="."+r.value;continue}if(o.kind==="lbracket"){this.next();const r=this.next(),s=this.next();if(!s||s.kind!=="rbracket")throw new Error("expected-rbracket");if(r&&(r.kind==="number"||r.kind==="string")){t+=`[${r.value}]`;continue}throw new Error("non-literal-index")}if(o.kind==="lparen")throw new Error("call-expression");break}return t}tryParsePathWrapperCall(t){const n=this.pos;this.next();const o=this.next();if(!o||o.kind!=="lparen")return this.pos=n,null;let r;try{if(this.peek()?.kind!=="ident")return this.pos=n,null;r=this.parsePath()}catch{return this.pos=n,null}const s=this.next();return!s||s.kind!=="rparen"?(this.pos=n,null):{kind:"leaf",text:`{{${t}(${r})}}`}}}function Hv(e){return new jv(e).parse()}const Fv=["===","!==","==","!=",">=","<=",">","<"];function Kv(e,t){const n=e.trim();if(n==="")return{kind:"raw",source:n};if(n===Xd)return{kind:"simple",left:"",op:"truthy",right:""};if(n.startsWith("!")){const s=n.slice(1).trim(),a=Qd(s);return Qn(a)?{kind:"simple",left:a,op:"falsy",right:""}:{kind:"raw",source:n}}const o=Vv(n);if(o){const{left:s,op:a,right:l}=o,d=s.trim(),c=l.trim();return Qn(d)&&(c==="''"||c==='""')?{kind:"simple",left:d,op:a,right:""}:Qn(d)&&Uv(c)?{kind:"simple",left:d,op:a,right:c}:{kind:"raw",source:n}}const r=Qd(n);return Qn(r)?{kind:"simple",left:r,op:"truthy",right:""}:{kind:"raw",source:n}}function Vv(e){let t=0,n=null;for(let o=0;o<e.length;o++){const r=e[o];if(n){if(r==="\\"){o++;continue}r===n&&(n=null);continue}if(r==="'"||r==='"'||r==="`"){n=r;continue}if(r==="("||r==="["){t++;continue}if(r===")"||r==="]"){t--;continue}if(t===0){if(r==="&"&&e[o+1]==="&"||r==="|"&&e[o+1]==="|"||r==="?"&&e[o+1]==="?")return null;for(const s of Fv)if(e.startsWith(s,o))return{left:e.slice(0,o),op:s,right:e.slice(o+s.length)}}}return null}function Qd(e){let t=e.trim();for(;t.startsWith("(")&&t.endsWith(")");){let n=0,o=!0;for(let r=0;r<t.length;r++)if(t[r]==="(")n++;else if(t[r]===")"&&(n--,n===0&&r<t.length-1)){o=!1;break}if(!o)break;t=t.slice(1,-1).trim()}return t}function Qn(e){const t=e.trim();if(t==="")return!1;let n=0;const o=t.length;if(!/[A-Za-z_$]/.test(t[0]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;for(;n<o;){if(t[n]==="?"&&t[n+1]==="."){if(n+=2,n>=o||!/[A-Za-z_$]/.test(t[n]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;continue}if(t[n]==="."){if(n++,n>=o||!/[A-Za-z_$]/.test(t[n]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;continue}if(t[n]==="["){const r=t.indexOf("]",n);if(r===-1)return!1;const s=t.slice(n+1,r).trim();if(!/^\d+$/.test(s)&&!/^'[^']*'$/.test(s)&&!/^"[^"]*"$/.test(s))return!1;n=r+1;continue}return!1}return!0}function Uv(e){const t=e.trim();return/^'[^']*'$/.test(t)||/^"[^"]*"$/.test(t)||/^-?\d+(\.\d+)?$/.test(t)||/^(true|false|null|undefined)$/.test(t)?!0:Qn(t)}function Rd(e){const t=e.trim();return t.length>=2&&(t[0]==="'"||t[0]==='"'||t[0]==="`")?t.slice(1,-1).replace(/\\(['"`\\])/g,"$1"):t}function qv(e,t){try{return $r(e)===$r(t)}catch{return!1}}function $r(e){const t=Jn(e);return t!==null?$r(t):Zd(e).map(o=>{if(o.kind==="string")return`S:${Rd(o.value)}`;if(o.kind==="binding"){const r=/^\{\{([\s\S]*)\}\}$/.exec(o.value);return`B:${r?$r(r[1].trim()):o.value.replace(/\s+/g,"")}`}return o.kind==="tkey"?`S:${o.value}`:o.kind==="op"?`O:${o.value}`:o.kind==="ident"||o.kind==="number"?`I:${o.value}`:o.kind==="dot"?".":o.kind==="lbracket"?"[":o.kind==="rbracket"?"]":o.kind==="lparen"||o.kind==="rparen"?"":`X:${o.value}`}).join("")}function Rn({active:e,testid:t}){return i.jsx("div",{"data-testid":t,"data-active":e?"true":"false","aria-hidden":"true",style:e?Yv:Gv})}const Gv={height:0,margin:0,transition:"height 80ms"},Yv={height:3,margin:"1px 0",borderRadius:2,background:"#2563eb",boxShadow:"0 0 0 1px #93c5fd"};function fs(e){const{length:t,onMove:n,canDrag:o,canDropAt:r}=e,[s,a]=u.useState(null),[l,d]=u.useState(null),c=u.useCallback(m=>{o&&!o(m)||(a(m),d(m))},[o]),p=u.useCallback(()=>{a(null),d(null)},[]),f=u.useCallback((m,h)=>{if(s===null)return;const v=h==="before"?m:m+1;if(r&&!r(s,v)){d(null);return}d(v)},[s,r]),y=u.useCallback(()=>{if(s!==null&&l!==null){const m=l>s?l-1:l;m!==s&&(!r||r(s,l))&&n(s,m)}a(null),d(null)},[s,l,n,r]),g=u.useCallback(m=>s!==null&&l===m&&m<=t,[s,l,t]);return{dragIndex:s,dropIndex:l,dragging:s!==null,onDragStart:c,onDragEnd:p,onDragOverItem:f,onDrop:y,isDropTarget:g}}const Xv={text:"",expression:"{{false ? '' : ''}}",fallback:"{{'' ?? ''}}",data:""};function ec(e,t){const n=In(e);let o=t;const r=n.map(s=>({id:o++,kind:s.kind==="literal"?"text":"expression",value:s.raw}));return r.length===0&&r.push({id:o++,kind:"text",value:""}),{segs:r,nextSeq:o}}function gs({value:e,onChange:t,t:n,candidates:o,testidPrefix:r="g7le-seg-value",renderLeafInput:s}){const a=u.useRef(0),l=u.useRef(e),d=u.useMemo(()=>{const k=ec(e,a.current);return a.current=k.nextSeq,k.segs},[]),[c,p]=u.useState(d),[f,y]=u.useState(!1);if(e!==l.current){l.current=e;const k=c.map(w=>w.value).join("");if(e!==k){const w=ec(e,a.current);a.current=w.nextSeq,p(w.segs)}}const g=u.useCallback(k=>{p(k);const w=k.map(_=>_.value).join("");l.current=w,t(w)},[t]),m=u.useCallback((k,w)=>{g(c.map(_=>_.id===k?{..._,value:w??""}:_))},[c,g]),h=u.useCallback(k=>{const w=c.filter(_=>_.id!==k);g(w.length>0?w:[{id:a.current++,kind:"text",value:""}])},[c,g]),v=u.useCallback(k=>{g([...c,{id:a.current++,kind:k,value:Xv[k]}])},[c,g]),b=u.useCallback((k,w)=>{if(k===w||k<0||w<0||k>=c.length||w>=c.length)return;const _=c.slice(),[C]=_.splice(k,1);_.splice(w,0,C),g(_)},[c,g]),x=fs({length:c.length,onMove:b});return i.jsxs("div",{"data-testid":r,style:Qv,children:[i.jsx("div",{style:Rv,children:n("layout_editor.value_tree.segmented_hint")}),c.map((k,w)=>i.jsxs(u.Fragment,{children:[i.jsx(Rn,{active:x.isDropTarget(w),testid:`${r}-dropline-${w}`}),i.jsx(Zv,{seg:k,index:w,t:n,candidates:o,testidPrefix:r,renderLeafInput:s,onValueChange:_=>m(k.id,_),onRemove:()=>h(k.id),dragging:x.dragIndex===w,onDragStartCard:()=>x.onDragStart(w),onDragEndCard:x.onDragEnd,onDragOverCard:_=>x.onDragOverItem(w,_),onDropCard:x.onDrop})]},k.id)),i.jsx(Rn,{active:x.isDropTarget(c.length),testid:`${r}-dropline-end`}),i.jsxs("div",{style:i_,"data-testid":`${r}-add-row`,children:[i.jsxs("button",{type:"button","data-testid":`${r}-add-text`,onClick:()=>v("text"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.text")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-expression`,onClick:()=>v("expression"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.expression")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-fallback`,onClick:()=>v("fallback"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.fallback")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-data`,onClick:()=>v("data"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.data")]})]}),i.jsxs("div",{style:l_,children:[i.jsxs("button",{type:"button","data-testid":`${r}-source-toggle`,"aria-expanded":f,onClick:()=>y(k=>!k),style:d_,children:["</>"," ",n("layout_editor.value_tree.show_source")," ",f?"▴":"▾"]}),f&&i.jsx("code",{"data-testid":`${r}-source-code`,style:c_,children:c.map(k=>k.value).join("")})]})]})}function Jv({t:e,candidates:t,onPick:n,testidPrefix:o}){return!t||t.length===0?i.jsx("div",{"data-testid":`${o}-data-empty`,style:a_,children:e("layout_editor.value_tree.segment.data_no_candidates")}):i.jsxs("div",{"data-testid":`${o}-data-picker`,style:{minWidth:0},children:[i.jsx("div",{style:s_,children:e("layout_editor.value_tree.segment.data_pick_hint")}),i.jsx($t,{candidates:t,t:e,onSelect:r=>{n(En(r.sourceId,r.path,"scalar"))},testIdSuffix:`${o}-data`,defaultOpen:!0,floating:!0})]})}function Zv({seg:e,index:t,t:n,candidates:o,testidPrefix:r,renderLeafInput:s,onValueChange:a,onRemove:l,dragging:d,onDragStartCard:c,onDragEndCard:p,onDragOverCard:f,onDropCard:y}){const g=e.kind==="text"?"layout_editor.value_tree.segment.text":e.kind==="data"?"layout_editor.value_tree.segment.data":e.kind==="fallback"?"layout_editor.value_tree.segment.fallback":"layout_editor.value_tree.segment.expression";return i.jsxs("div",{"data-testid":`${r}-card-${t}`,"data-seg-kind":e.kind,style:d?{...tc,opacity:.5}:tc,onDragOver:m=>{m.preventDefault();const h=m.currentTarget.getBoundingClientRect();f(m.clientY<h.top+h.height/2?"before":"after")},onDrop:m=>{m.preventDefault(),y()},children:[i.jsxs("div",{style:e_,children:[i.jsx("span",{"data-testid":`${r}-drag-${t}`,style:o_,draggable:!0,onDragStart:c,onDragEnd:p,title:n("layout_editor.value_tree.drag_reorder"),"aria-label":n("layout_editor.value_tree.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:t_,children:n(g)}),i.jsx("button",{type:"button","data-testid":`${r}-remove-${t}`,onClick:l,title:n("layout_editor.value_tree.segment.remove"),"aria-label":n("layout_editor.value_tree.segment.remove"),style:r_,children:"✕"})]}),i.jsx("div",{style:n_,children:e.kind==="data"&&e.value.trim()===""?i.jsx(Jv,{t:n,candidates:o,onPick:a,testidPrefix:`${r}-field-${t}`}):s?s({value:e.value,onChange:a,t:n,candidates:o,testidPrefix:`${r}-field-${t}`}):i.jsx(vt,{value:e.value,onChange:a,t:n,candidates:o,testidPrefix:`${r}-field-${t}`,enableExpressionTree:!0,expressionSourceToggle:!1})})]})}const Qv={display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},Rv={fontSize:11,color:"#64748b"},tc={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #e2e8f0",borderLeft:"3px solid #6366f1",borderRadius:6,background:"#f8fafc",minWidth:0},e_={display:"flex",alignItems:"center",gap:6,minWidth:0},t_={fontSize:10,fontWeight:600,color:"#6366f1",flex:1,minWidth:0},n_={minWidth:0},o_={color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},r_={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12,padding:2,flexShrink:0},i_={display:"flex",flexWrap:"wrap",gap:6,marginTop:2},s_={fontSize:11,color:"#64748b",marginBottom:2},a_={fontSize:11,color:"#94a3b8",padding:"4px 0"},Er={padding:"4px 10px",fontSize:11,border:"1px dashed #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},l_={display:"flex",flexDirection:"column",gap:4,marginTop:2},d_={alignSelf:"flex-start",padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontFamily:"monospace"},c_={fontSize:11,background:"#0f172a",color:"#e2e8f0",padding:"6px 8px",borderRadius:4,wordBreak:"break-all",fontFamily:"monospace"},u_=[{value:"truthy",labelKey:"layout_editor.value_tree.cond.op_truthy"},{value:"falsy",labelKey:"layout_editor.value_tree.cond.op_falsy"},{value:"===",labelKey:"layout_editor.value_tree.cond.op_eq"},{value:"!==",labelKey:"layout_editor.value_tree.cond.op_neq"},{value:">",labelKey:"layout_editor.value_tree.cond.op_gt"},{value:"<",labelKey:"layout_editor.value_tree.cond.op_lt"},{value:">=",labelKey:"layout_editor.value_tree.cond.op_gte"},{value:"<=",labelKey:"layout_editor.value_tree.cond.op_lte"}];function nc({node:e,onChange:t,t:n,candidates:o,testidPrefix:r="g7le-value-tree",depth:s=0,showSourceToggle:a=!0,renderLeafInput:l}){const[d,c]=u.useState(!1),p=s===0,f=i.jsx(eo,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:l});return p?a?i.jsxs("div",{"data-testid":r,style:rc,children:[f,i.jsxs("div",{style:D_,children:[i.jsxs("button",{type:"button","data-testid":`${r}-source-toggle`,"aria-expanded":d,onClick:()=>c(y=>!y),style:N_,children:["</>"," ",n("layout_editor.value_tree.show_source")," ",d?"▴":"▾"]}),d&&i.jsx("code",{"data-testid":`${r}-source-code`,style:z_,children:Zn(e,!1)})]})]}):i.jsx("div",{"data-testid":r,style:rc,children:f}):f}function eo({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){switch(e.kind){case"leaf":return i.jsx(gs,{value:e.text,onChange:l=>t({kind:"leaf",text:l}),t:n,candidates:o,testidPrefix:`${r}-leaf`,renderLeafInput:a});case"conditional":return i.jsx(p_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});case"fallback":return i.jsx(f_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});case"concat":return i.jsx(h_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});case"raw":default:return i.jsxs("div",{"data-testid":`${r}-raw`,style:M_,children:[i.jsx("code",{style:ac,children:e.source??""}),i.jsxs("span",{style:lc,children:["🔒 ",n("layout_editor.value_tree.raw_code_only")]})]})}}function p_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback(p=>t({...e,condition:p}),[e,t]),d=u.useCallback(p=>t({...e,then:p}),[e,t]),c=u.useCallback(p=>t({...e,else:p}),[e,t]);return i.jsxs("div",{"data-testid":`${r}-conditional`,style:hs(s),children:[i.jsx(y_,{condition:e.condition,onChange:l,t:n,candidates:o,testidPrefix:r}),i.jsx(Ir,{label:n("layout_editor.value_tree.cond.when_true"),testid:`${r}-then`,children:i.jsx(eo,{node:e.then,onChange:d,t:n,candidates:o,testidPrefix:`${r}-then`,depth:s+1,renderLeafInput:a})}),i.jsx(Ir,{label:n("layout_editor.value_tree.cond.when_false"),testid:`${r}-else`,children:i.jsx(eo,{node:e.else,onChange:c,t:n,candidates:o,testidPrefix:`${r}-else`,depth:s+1,renderLeafInput:a})})]})}function f_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback(c=>t({...e,primary:c}),[e,t]),d=u.useCallback(c=>t({...e,fallback:c}),[e,t]);return i.jsxs("div",{"data-testid":`${r}-fallback`,style:hs(s),children:[i.jsx(Ir,{label:n("layout_editor.value_tree.fallback.primary"),testid:`${r}-primary`,children:i.jsx(eo,{node:e.primary,onChange:l,t:n,candidates:o,testidPrefix:`${r}-primary`,depth:s+1,renderLeafInput:a})}),i.jsx(Ir,{label:n(e.op==="??"?"layout_editor.value_tree.fallback.when_empty":"layout_editor.value_tree.fallback.when_falsy"),testid:`${r}-fallback-branch`,children:i.jsx(eo,{node:e.fallback,onChange:d,t:n,candidates:o,testidPrefix:`${r}-fallbackb`,depth:s+1,renderLeafInput:a})})]})}const g_={text:()=>({kind:"leaf",text:""}),data:()=>({kind:"leaf",text:""}),expression:()=>({kind:"conditional",condition:{kind:"simple",left:"",op:"truthy",right:""},then:{kind:"leaf",text:""},else:{kind:"leaf",text:""}}),fallback:()=>({kind:"fallback",op:"??",primary:{kind:"leaf",text:""},fallback:{kind:"leaf",text:""}})};function h_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback((g,m)=>{const h=e.parts.slice();h[g]=m,t({...e,parts:h})},[e,t]),d=u.useCallback(g=>{g.length===0?t({kind:"leaf",text:""}):g.length===1?t(g[0]):t({...e,parts:g})},[e,t]),c=u.useCallback(g=>d(e.parts.filter((m,h)=>h!==g)),[e.parts,d]),p=u.useCallback(g=>d([...e.parts,g_[g]()]),[e.parts,d]),f=u.useCallback((g,m)=>{if(g===m||g<0||m<0||g>=e.parts.length||m>=e.parts.length)return;const h=e.parts.slice(),[v]=h.splice(g,1);h.splice(m,0,v),d(h)},[e.parts,d]),y=fs({length:e.parts.length,onMove:f});return i.jsxs("div",{"data-testid":`${r}-concat`,style:hs(s),children:[i.jsx("div",{style:C_,children:n("layout_editor.value_tree.concat.hint")}),e.parts.map((g,m)=>i.jsxs(u.Fragment,{children:[i.jsx(Rn,{active:y.isDropTarget(m),testid:`${r}-part-dropline-${m}`}),i.jsxs("div",{"data-testid":`${r}-part-card-${m}`,style:y.dragIndex===m?{...sc,opacity:.5}:sc,onDragOver:h=>{h.preventDefault();const v=h.currentTarget.getBoundingClientRect();y.onDragOverItem(m,h.clientY<v.top+v.height/2?"before":"after")},onDrop:h=>{h.preventDefault(),y.onDrop()},children:[i.jsxs("div",{style:$_,children:[i.jsx("span",{"data-testid":`${r}-part-handle-${m}`,style:A_,draggable:!0,onDragStart:()=>y.onDragStart(m),onDragEnd:y.onDragEnd,title:n("layout_editor.value_tree.drag_reorder"),"aria-label":n("layout_editor.value_tree.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:E_,children:n("layout_editor.value_tree.concat.part",{n:m+1})}),i.jsx("button",{type:"button","data-testid":`${r}-part-remove-${m}`,onClick:()=>c(m),title:n("layout_editor.value_tree.segment.remove"),"aria-label":n("layout_editor.value_tree.segment.remove"),style:T_,children:"✕"})]}),i.jsx("div",{style:{minWidth:0},children:i.jsx(eo,{node:g,onChange:h=>l(m,h),t:n,candidates:o,testidPrefix:`${r}-part-${m}`,depth:s+1,renderLeafInput:a})})]})]},m)),i.jsx(Rn,{active:y.isDropTarget(e.parts.length),testid:`${r}-part-dropline-end`}),i.jsxs("div",{style:I_,"data-testid":`${r}-part-add-row`,children:[i.jsxs("button",{type:"button","data-testid":`${r}-part-add-text`,onClick:()=>p("text"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.text")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-expression`,onClick:()=>p("expression"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.expression")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-fallback`,onClick:()=>p("fallback"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.fallback")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-data`,onClick:()=>p("data"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.data")]})]})]})}function y_({condition:e,onChange:t,t:n,candidates:o,testidPrefix:r}){if(e.kind==="raw")return i.jsxs("div",{"data-testid":`${r}-cond-raw`,style:x_,children:[i.jsx("span",{style:ic,children:n("layout_editor.value_tree.cond.label")}),i.jsx("code",{style:ac,children:e.source}),i.jsxs("span",{style:lc,children:["🔒 ",n("layout_editor.value_tree.cond.code_only")]})]});const s=e,a=s.op!=="truthy"&&s.op!=="falsy";return i.jsxs("div",{"data-testid":`${r}-cond`,style:__,children:[i.jsx("span",{style:ic,children:n("layout_editor.value_tree.cond.label")}),i.jsx(oc,{testid:`${r}-cond-left`,value:s.left,onCommit:l=>t({...s,left:l}),placeholder:n("layout_editor.value_tree.cond.field_placeholder"),candidates:o,t:n}),i.jsx("select",{"data-testid":`${r}-cond-op`,value:s.op,onChange:l=>t({...s,op:l.target.value}),style:S_,children:u_.map(l=>i.jsx("option",{value:l.value,children:n(l.labelKey)},l.value))}),a&&i.jsx(oc,{testid:`${r}-cond-right`,value:s.right,onCommit:l=>t({...s,right:l}),placeholder:n("layout_editor.value_tree.cond.value_placeholder"),candidates:o,t:n,quoteDataPick:!0})]})}function oc({testid:e,value:t,onCommit:n,placeholder:o,candidates:r,t:s,quoteDataPick:a=!1}){const[l,d]=u.useState(null),c=l!==null?l:t,p=()=>{l!==null&&(l!==t&&n(l),d(null))},f=(y,g)=>{const m=En(y,g,"scalar"),h=/^\{\{\s*([\s\S]*?)\s*\}\}$/.exec(m.trim());let v=h?h[1]:m;v=v.replace(/\s*\?\?\s*''$/,"").trim(),n(a?`'${v}'`:v),d(null)};return i.jsxs("span",{style:w_,children:[i.jsx("input",{type:"text","data-testid":e,value:c,onChange:y=>d(y.target.value),onBlur:p,onKeyDown:y=>{y.key==="Enter"&&(y.preventDefault(),p())},placeholder:o,style:k_}),r&&r.length>0&&s&&i.jsx($t,{candidates:r,t:s,onSelect:y=>f(y.sourceId,y.path),testIdSuffix:`${e}-data`,floating:!0})]})}function Ir({label:e,testid:t,children:n}){return i.jsxs("div",{"data-testid":t,style:b_,children:[i.jsx("span",{style:m_,children:e}),i.jsx("div",{style:v_,children:n})]})}const rc={display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},hs=e=>({display:"flex",flexDirection:"column",gap:6,padding:"8px",border:"1px solid #e2e8f0",borderLeft:`3px solid ${e%2===0?"#6366f1":"#0ea5e9"}`,borderRadius:6,background:e%2===0?"#f8fafc":"#fefefe",minWidth:0}),b_={display:"flex",flexDirection:"column",gap:3,minWidth:0},m_={fontSize:11,fontWeight:600,color:"#475569"},v_={paddingLeft:8,minWidth:0},__={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center",minWidth:0},x_={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center",minWidth:0},ic={fontSize:11,fontWeight:600,color:"#0f172a",whiteSpace:"nowrap"},w_={display:"inline-flex",alignItems:"flex-start",gap:2,flex:"1 1 80px",minWidth:60},k_={flex:"1 1 80px",minWidth:60,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},S_={padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff"},C_={fontSize:11,color:"#94a3b8"},sc={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #e2e8f0",borderLeft:"3px solid #6366f1",borderRadius:6,background:"#fff",minWidth:0},$_={display:"flex",alignItems:"center",gap:6,minWidth:0},E_={fontSize:10,fontWeight:600,color:"#6366f1",flex:1,minWidth:0},I_={display:"flex",flexWrap:"wrap",gap:6,marginTop:2},A_={color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},T_={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12,padding:2,flexShrink:0},Ar={padding:"4px 10px",fontSize:11,border:"1px dashed #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},D_={display:"flex",flexDirection:"column",gap:4,marginTop:2},N_={alignSelf:"flex-start",padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontFamily:"monospace"},z_={fontSize:11,background:"#0f172a",color:"#e2e8f0",padding:"6px 8px",borderRadius:4,wordBreak:"break-all",fontFamily:"monospace"},M_={display:"flex",flexDirection:"column",gap:2,minWidth:0},ac={fontSize:11,background:"#f1f5f9",padding:"3px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all",fontFamily:"monospace"},lc={fontSize:10,color:"#64748b"};function vt({value:e,onChange:t,t:n,placeholder:o,testidPrefix:r="g7le-i18n-text-field",locales:s,candidates:a,enableExpressionTree:l=!1,expressionTreeCollapsible:d=!1,expressionSourceToggle:c=!0}){const{locale:p,templateIdentifier:f,commitText:y,classify:g,translate:m}=uv(),[h,v]=u.useState(null),[b,x]=u.useState(!1),[k,w]=u.useState(!1),[_,C]=u.useState(!1),[E,S]=u.useState(!1),[T,$]=u.useState(!1),[,A]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const N=H=>{const L=H.detail;L?.templateIdentifier&&L.templateIdentifier!==f||A(I=>I+1)};return window.addEventListener(kt,N),()=>window.removeEventListener(kt,N)},[f]);const z=g(e);u.useEffect(()=>{v(null),$(!1)},[p,e]);const D=h!==null?h:z.displayValue,O=u.useCallback(async()=>{if(h!==null){if(h===z.displayValue){v(null);return}x(!0);try{const N=await y(e,h);N.kind==="created"&&N.token?(t(N.token),Et({op:"prop_i18n_create_key",sourceState:"plain_text",translationKey:N.customKey,toLocale:p,valueLength:h.length,timestamp:Date.now()})):N.kind==="updated"&&Et({op:"prop_i18n_update_value",sourceState:"custom_key",translationKey:N.customKey,toLocale:p,valueLength:h.length,timestamp:Date.now()})}finally{x(!1),v(null)}}},[h,z.displayValue,y,e,t,p]),M=u.useCallback(N=>{typeof e=="string"&&(t(mo(e,N)),z.customKey&&kr(f,z.customKey,N,p))},[e,t,z.customKey,f,p]);if(z.binding){if(l&&typeof e=="string"){const N=Gd(e),H=un(e),L=!N&&Kt(e)!==null,I=(N||H.decomposed)&&!L;if(I&&d&&!_)return i.jsx(O_,{value:e,t:n,translate:m,onEdit:()=>C(!0),testidPrefix:`${r}-preview`});const F=I&&(d||N)?i.jsx(gs,{value:e,onChange:G=>t(G),t:n,candidates:a,testidPrefix:`${r}-seg`}):H.decomposed?i.jsx(nc,{node:H.node,onChange:G=>t(Zn(G,!1)),t:n,candidates:a,testidPrefix:`${r}-tree`,showSourceToggle:c}):null;if(F){if(!d)return F;const G=Sr(e),V=G?us(G,Y=>{const Z=m(Y);return Z&&Z!==Y&&!Z.startsWith("$t:")?Z:""},Y=>Vt(Y)):[];return i.jsxs("div",{"data-testid":`${r}-builder`,style:{display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:6},children:[i.jsxs("button",{type:"button","data-testid":`${r}-to-plain`,onClick:()=>S(!0),style:V_,title:n("layout_editor.value_tree.to_plain"),"aria-label":n("layout_editor.value_tree.to_plain"),children:["↩ ",n("layout_editor.value_tree.to_plain")]}),i.jsxs("button",{type:"button","data-testid":`${r}-builder-collapse`,onClick:()=>C(!1),style:uc,title:n("layout_editor.value_tree.collapse"),"aria-label":n("layout_editor.value_tree.collapse"),children:["▴ ",n("layout_editor.value_tree.collapse")]})]}),E&&i.jsxs("div",{"data-testid":`${r}-to-plain-confirm`,style:U_,children:[i.jsx("p",{style:q_,children:n("layout_editor.value_tree.to_plain_lead")}),i.jsx("div",{style:G_,children:V.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:n("layout_editor.prop_i18n.placeholder")}):V.map((Y,Z)=>Y.kind==="chip"?i.jsxs("span",{style:dc,children:["🔗 ",Y.text]},Z):i.jsx("span",{style:Y.kind==="ellipsis"?cc:void 0,children:Y.text},Z))}),i.jsxs("p",{style:Y_,children:["⚠ ",n("layout_editor.value_tree.to_plain_warn")]}),i.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:6},children:[i.jsx("button",{type:"button","data-testid":`${r}-to-plain-confirm-cancel`,onClick:()=>S(!1),style:uc,children:n("layout_editor.value_tree.to_plain_cancel")}),i.jsx("button",{type:"button","data-testid":`${r}-to-plain-confirm-ok`,onClick:()=>{S(!1),C(!1),t(Sr(e)||void 0)},style:X_,children:n("layout_editor.value_tree.to_plain_ok")})]})]}),F]})}}return l&&typeof e=="string"&&a&&a.length>0&&Kt(e)!==null?i.jsx(B_,{value:e,onChange:t,t:n,candidates:a,testidPrefix:`${r}-data`}):i.jsxs("div",{"data-testid":`${r}-binding`,style:tx,children:[i.jsx("code",{style:nx,children:typeof e=="string"?e:""}),i.jsxs("span",{style:ox,children:["🔗 ",n("layout_editor.prop_i18n.bound_code_only")]})]})}if(!T&&typeof e=="string"&&Ul(e)){const N=ql(e);return i.jsxs("div",{"data-testid":`${r}-settings-ref`,style:Q_,children:[i.jsx("div",{"data-testid":`${r}-settings-ref-chips`,style:R_,children:N.map((H,L)=>H.kind==="chip"?i.jsxs("span",{style:ex,children:["🔗 ",H.label]},L):i.jsx("span",{children:H.label},L))}),i.jsxs("button",{type:"button","data-testid":`${r}-settings-ref-edit`,onClick:()=>$(!0),style:pc,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:["✎ ",n("layout_editor.value_tree.edit")]})]})}const B=i.jsxs("button",{type:"button","data-testid":`${r}-toggle`,title:`${n("layout_editor.prop_i18n.edit_languages")} (${Gn(p,n)})`,"aria-label":`${n("layout_editor.prop_i18n.edit_languages")} (${Gn(p,n)})`,"aria-expanded":k,onClick:()=>w(N=>!N),style:pc,children:["🌐 ",z.customKey?k?"⌃":"⌄":"+"]});return i.jsxs("div",{"data-testid":r,style:{display:"flex",flexDirection:"column",gap:4,width:"100%"},children:[i.jsx("div",{style:J_,children:z.paramKey&&z.customKey?i.jsx(L_,{customKey:z.customKey,value:typeof e=="string"?e:"",onChange:t,t:n,candidates:a,testidPrefix:r,showExprButton:l&&d,onToExpression:()=>{C(!0),t(Co(typeof e=="string"?e:""))},langToggle:B}):i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text","data-testid":`${r}-preview`,value:D,placeholder:o??n("layout_editor.prop_i18n.placeholder"),onChange:N=>v(N.target.value),onBlur:()=>{O(),T&&$(!1)},disabled:b,style:Z_}),a&&a.length>0&&i.jsx(P_,{value:typeof e=="string"?e:"",customKey:z.customKey,onChange:t,t:n,candidates:a,testidPrefix:r}),l&&d&&i.jsx("button",{type:"button","data-testid":`${r}-to-expr`,onClick:()=>{C(!0),t(Co(typeof e=="string"?e:""))},title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),style:Io,children:"ƒx"}),B]})}),k&&(z.customKey?i.jsx(Ud,{customKey:z.customKey,templateIdentifier:f,t:n,paramLabels:ds(e),locales:s,onRemoveParam:M},z.customKey):i.jsx("div",{"data-testid":`${r}-expand-hint`,style:rx,children:n("layout_editor.prop_i18n.create_first")}))]})}function L_({customKey:e,value:t,onChange:n,t:o,candidates:r,testidPrefix:s,showExprButton:a=!1,onToExpression:l,langToggle:d}){const{state:c}=Je(),p=c.templateIdentifier,f=c.locale,y=Ft(),[g,m]=u.useState(null),h=u.useRef(""),v=u.useRef(null),b=u.useRef(null),x=u.useRef(null),[k,w]=u.useState(null),[_,C]=u.useState(!1),E=ds(t),S=u.useCallback(async()=>{const D=It(e,f);if(D!==void 0){h.current=D,m(D);return}const M=(await cn(p,e))?.values?.[f]??"";h.current=M,m(M)},[e,p,f]);u.useEffect(()=>{let D=!1;return(async()=>{const O=It(e,f);if(O!==void 0){D||(h.current=O,m(O));return}const M=await cn(p,e);if(D)return;const B=M?.values?.[f]??"";h.current=B,m(B)})(),()=>{D=!0}},[e,p,f]),u.useEffect(()=>{if(typeof window>"u")return;const D=O=>{const M=O.detail;if(M?.templateIdentifier&&M.templateIdentifier!==p)return;const B=It(e,f);B!==void 0&&B!==g&&(h.current=B,m(B))};return window.addEventListener(kt,D),()=>window.removeEventListener(kt,D)},[e,p,f,g]);const T=u.useCallback(D=>{m(D),ko(p,e,f,D).then(()=>{y?.markDirty?.()})},[p,e,f,y]),$=u.useCallback(D=>{n(mo(t,D)),kr(p,e,D,f).then(()=>{S(),y?.markDirty?.()})},[t,n,p,e,f,S,y]),A=u.useCallback(async D=>{if(!(k===null||_)){C(!0);try{g!==null&&g!==h.current&&(await ko(p,e,f,g),h.current=g);const O=await wr(p,f,t,k,D.sourceId,D.path,"scalar");O.kind==="ok"&&(n(O.text),await S(),y?.markDirty?.())}finally{C(!1),w(null)}}},[k,_,g,t,p,e,f,n,S,y]),z=!!r&&r.length>0;return i.jsxs("div",{"data-testid":`${s}-chipfield-wrap`,style:{flex:1,minWidth:0,display:"flex",alignItems:"center",gap:4},children:[i.jsx("div",{ref:v,"data-testid":`${s}-chipfield`,style:{flex:1,minWidth:0},children:g===null?i.jsx("div",{"data-testid":`${s}-chipfield-loading`,style:{fontSize:11,color:"#94a3b8",padding:"4px 0"},children:o("layout_editor.translation.loading")}):i.jsx(mr,{value:g,onChange:T,t:o,paramLabels:E,testIdSuffix:`${s}-field`,onRequestInsert:z?D=>w(D):void 0,onRemoveChip:$,hideInsertButton:!0,caretRef:x})}),z&&i.jsx("button",{ref:b,type:"button","data-testid":`${s}-plus-data-btn`,onClick:()=>w(x.current?x.current():g?.length??0),title:o("layout_editor.inline_binding.insert_data"),"aria-label":o("layout_editor.inline_binding.insert_data"),style:Io,children:"+"}),a&&l&&i.jsx("button",{type:"button","data-testid":`${s}-to-expr`,onClick:l,title:o("layout_editor.value_tree.to_expression"),"aria-label":o("layout_editor.value_tree.to_expression"),style:Io,children:"ƒx"}),d,z&&i.jsx(Xn,{anchorRef:b,open:k!==null,onClose:()=>w(null),children:i.jsx("div",{"data-testid":`${s}-chipfield-picker`,children:i.jsx($t,{candidates:r,t:o,onSelect:D=>void A(D),testIdSuffix:`${s}-field-insert`,defaultOpen:!0,floating:!1})})})]})}function P_({value:e,customKey:t,onChange:n,t:o,candidates:r,testidPrefix:s}){const{state:a}=Je(),l=a.templateIdentifier,d=a.selectedRoute?.layoutName??null,c=a.locale,p=Ft(),[f,y]=u.useState(!1),[g,m]=u.useState(!1),h=u.useRef(null),v=u.useCallback(k=>{const w=o(k);return w&&w!==k&&!w.startsWith("$t:")?w:""},[o]),b=u.useCallback(k=>k.replace(/\$t:[a-zA-Z0-9._-]+/g,w=>v(w.slice(3))).replace(/\s+/g," ").trim(),[v]),x=u.useCallback(async k=>{if(!g){m(!0);try{if(t){const w=await wr(l,c,e,Number.MAX_SAFE_INTEGER,k.sourceId,k.path,"scalar");w.kind==="ok"&&(n(w.text),p?.markDirty?.())}else{const w=await ls(l,d,c,e,e.length,k.sourceId,k.path,"scalar",b,_=>o(_));w.kind==="ok"&&(n(w.text),p?.markDirty?.())}}finally{m(!1),y(!1)}}},[g,t,l,d,c,e,n,p,o,b]);return i.jsxs("div",{"data-testid":`${s}-plus-data`,style:{display:"inline-block"},children:[i.jsx("button",{ref:h,type:"button","data-testid":`${s}-plus-data-btn`,disabled:g,onClick:()=>y(k=>!k),title:o("layout_editor.inline_binding.insert_data"),"aria-label":o("layout_editor.inline_binding.insert_data"),style:Io,children:"+"}),i.jsx(Xn,{anchorRef:h,open:f,onClose:()=>y(!1),children:i.jsx($t,{candidates:r,t:o,onSelect:k=>void x(k),testIdSuffix:`${s}-plus-insert`,defaultOpen:!0,floating:!1})})]})}function O_({value:e,t,translate:n,onEdit:o,testidPrefix:r}){const s=us(e,a=>{const l=n(a);return l&&l!==a&&!l.startsWith("$t:")?l:""},a=>Vt(a));return i.jsxs("div",{"data-testid":r,style:H_,children:[i.jsx("div",{"data-testid":`${r}-text`,style:F_,children:s.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:t("layout_editor.prop_i18n.placeholder")}):s.map((a,l)=>a.kind==="chip"?i.jsxs("span",{style:dc,children:["🔗 ",a.text]},l):i.jsx("span",{style:a.kind==="ellipsis"?cc:void 0,children:a.text},l))}),i.jsxs("button",{type:"button","data-testid":`${r}-edit`,onClick:o,style:K_,title:t("layout_editor.value_tree.edit"),"aria-label":t("layout_editor.value_tree.edit"),children:["✎ ",t("layout_editor.value_tree.edit")]})]})}function B_({value:e,onChange:t,t:n,candidates:o,testidPrefix:r}){const[s,a]=u.useState(!1),l=u.useRef(null),d=Vt(e);return i.jsxs("div",{"data-testid":r,style:{display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0},children:[i.jsxs("div",{style:W_,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:j_,children:["🔗 ",d]}),i.jsx("button",{ref:l,type:"button","data-testid":`${r}-change`,onClick:()=>a(c=>!c),style:Io,title:n("layout_editor.value_tree.change_data"),"aria-label":n("layout_editor.value_tree.change_data"),children:n("layout_editor.value_tree.change_data")})]}),i.jsx(Xn,{anchorRef:l,open:s,onClose:()=>a(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:c=>{const p=c.path?c.path.split(".").filter(Boolean):[],f=[c.sourceId,...p].join("?.");t(`{{${f}}}`),a(!1)},testIdSuffix:`${r}-pick`,defaultOpen:!0,floating:!1})})]})}const W_={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},j_={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"3px 8px",borderRadius:12,wordBreak:"break-all"},H_={display:"flex",alignItems:"center",gap:8,width:"100%",minWidth:0},F_={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,padding:"5px 8px",fontSize:12,color:"#334155",border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc",overflow:"hidden"},dc={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"1px 6px",borderRadius:10,whiteSpace:"nowrap"},cc={color:"#94a3b8"},K_={padding:"4px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",whiteSpace:"nowrap",flexShrink:0},uc={padding:"3px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},V_={padding:"3px 10px",fontSize:11,border:"1px solid #fca5a5",borderRadius:6,background:"#fef2f2",color:"#b91c1c",cursor:"pointer",whiteSpace:"nowrap"},U_={display:"flex",flexDirection:"column",gap:6,padding:10,border:"1px solid #fca5a5",borderRadius:8,background:"#fffbeb"},q_={margin:0,fontSize:12,color:"#334155"},G_={padding:"5px 8px",fontSize:12,color:"#0f172a",background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,wordBreak:"break-all"},Y_={margin:0,fontSize:11,color:"#b45309"},X_={padding:"3px 12px",fontSize:11,border:"1px solid #dc2626",borderRadius:6,background:"#dc2626",color:"#fff",cursor:"pointer",whiteSpace:"nowrap"},J_={display:"flex",gap:4,alignItems:"center",width:"100%",minWidth:0},Z_={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%"},pc={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Io={padding:"4px 8px",fontSize:13,lineHeight:1,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Q_={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},R_={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,fontSize:12,color:"#334155"},ex={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"2px 8px",borderRadius:12,whiteSpace:"nowrap"},tx={display:"flex",flexDirection:"column",gap:2,width:"100%"},nx={fontSize:11,background:"#f1f5f9",padding:"3px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},ox={fontSize:10,color:"#64748b"},rx={fontSize:11,color:"#94a3b8",padding:"6px 8px",border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc"};function ys({value:e,onChange:t,candidates:n,t:o}){const r=Array.isArray(e)?e:[],s=n??[],[a,l]=u.useState(!1),d=g=>s.find(m=>m.value===g)?.label??g,c=g=>d(g)!==g,p=g=>{const m=r.filter(h=>h!==g);t(m.length===0?void 0:m)},f=g=>{r.includes(g)||(t([...r,g]),l(!1))},y=s.filter(g=>!r.includes(g.value));return i.jsxs("div",{className:"g7le-widget g7le-widget--tag-input","data-testid":"g7le-widget-tag-input",style:ix,children:[i.jsxs("div",{style:sx,children:[r.map(g=>i.jsxs("span",{"data-testid":`g7le-tag-chip-${g}`,style:ax,children:[i.jsx("span",{style:lx,children:d(g)}),c(g)&&i.jsx("code",{"data-testid":`g7le-tag-chip-id-${g}`,style:dx,children:g}),i.jsx("button",{type:"button","aria-label":`remove ${g}`,"data-testid":`g7le-tag-remove-${g}`,onClick:()=>p(g),style:cx,children:"✕"})]},g)),i.jsx("button",{type:"button","data-testid":"g7le-tag-add",onClick:()=>l(g=>!g),disabled:y.length===0,style:ux,children:o("layout_editor.control.tag_add")})]}),a&&y.length>0&&i.jsx("div",{"data-testid":"g7le-tag-candidates",style:px,children:y.map(g=>{const m=g.value!==g.label;return i.jsxs("button",{type:"button","data-testid":`g7le-tag-candidate-${g.value}`,onClick:()=>f(g.value),style:fx,children:[i.jsx("span",{style:gx,children:g.label}),m&&i.jsx("code",{"data-testid":`g7le-tag-candidate-id-${g.value}`,style:hx,children:g.value})]},g.value)})})]})}const ix={display:"flex",flexDirection:"column",gap:6},sx={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"},ax={display:"inline-flex",alignItems:"center",gap:4,padding:"2px 6px",fontSize:12,background:"#e0e7ff",color:"#1d4ed8",borderRadius:12},lx={fontSize:12},dx={fontSize:10,color:"#4f46e5",background:"#c7d2fe",borderRadius:4,padding:"0 4px",fontFamily:"monospace"},cx={border:"none",background:"transparent",color:"#1d4ed8",cursor:"pointer",fontSize:10,padding:0,lineHeight:1},ux={padding:"2px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:12,background:"#fff",cursor:"pointer",color:"#475569"},px={display:"flex",flexDirection:"column",maxHeight:180,overflowY:"auto",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff"},fx={display:"flex",flexDirection:"column",gap:1,textAlign:"left",padding:"6px 10px",fontSize:12,border:"none",borderBottom:"1px solid #f1f5f9",background:"transparent",cursor:"pointer",color:"#0f172a"},gx={fontSize:12,color:"#0f172a"},hx={fontSize:10,color:"#64748b",fontFamily:"monospace"},fc=new Map;function Xe(e,t){fc.set(e,t)}function Tn(e){return e?fc.get(e)??null:null}function Tr(e,t){const o=e("meta",{})?.[t];return typeof o=="string"?o:""}function yx({getValue:e,patch:t,t:n,fieldT:o,spec:r,permissionCandidates:s,bindingCandidates:a}){const l=o??n,d=Tr(e,"title"),c=Tr(e,"description"),p=Tr(e,"icon"),f=Tr(e,"editor_label"),y=e("permissions",[]),g=u.useCallback((b,x)=>{const k={...e("meta",{})??{}};x===void 0||x===""?delete k[b]:k[b]=x,t("meta",k)},[e,t]),m=u.useCallback(b=>{const x=Array.isArray(b)?b:[];x.length===0?t("permissions",void 0):t("permissions",x)},[t]),h=Tn("icon-picker"),v=Ct(r,"icon-picker")??Ct(r,"icon")??(r?.controls?Object.values(r.controls).find(b=>b&&typeof b=="object"&&b.widget==="icon-picker")??null:null);return i.jsxs("div",{className:"g7le-meta-form","data-testid":"g7le-meta-form",style:mx,children:[i.jsx(Ao,{label:n("layout_editor.page_settings.meta.title"),children:i.jsx(vt,{value:d,onChange:b=>g("title",b),t:l,testidPrefix:"g7le-meta-title",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.description"),children:i.jsx(vt,{value:c,onChange:b=>g("description",b),t:l,testidPrefix:"g7le-meta-description",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.icon"),children:h?i.jsx(bx,{icon:p,t:n,onChange:b=>g("icon",typeof b=="string"?b:void 0),IconWidget:h,iconControl:v??{widget:"icon-picker"}}):i.jsx("input",{type:"text","data-testid":"g7le-meta-icon",value:p,placeholder:n("layout_editor.page_settings.meta.icon_placeholder"),onChange:b=>g("icon",b.target.value),style:wx})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.editor_label"),hint:n("layout_editor.page_settings.meta.editor_label_hint"),children:i.jsx(vt,{value:f,onChange:b=>g("editor_label",b),t:l,testidPrefix:"g7le-meta-editor-label",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.permissions"),hint:n("layout_editor.page_settings.meta.permissions_hint"),children:i.jsx("div",{"data-testid":"g7le-meta-permissions",children:i.jsx(ys,{control:{widget:"tag-input"},value:y,onChange:m,t:n,candidates:s})})})]})}function Ao({label:e,hint:t,children:n}){return i.jsxs("div",{className:"g7le-meta-field",style:vx,children:[i.jsx("label",{style:_x,children:e}),n,t?i.jsxs("p",{style:xx,children:["ⓘ ",t]}):null]})}function bx({icon:e,onChange:t,t:n,IconWidget:o,iconControl:r}){const[s,a]=u.useState(!1),l=e?/\bfa-/.test(e)?i.jsx("i",{className:e,"aria-hidden":"true"}):e:"📄";return i.jsxs("div",{"data-testid":"g7le-meta-icon",style:{display:"flex",flexDirection:"column",gap:6,minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":"g7le-meta-icon-toggle","aria-expanded":s,onClick:()=>a(d=>!d),style:{display:"flex",alignItems:"center",gap:8,padding:"5px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",fontSize:12,color:"#475569",width:"fit-content"},children:[i.jsx("span",{style:{fontSize:16},"aria-hidden":"true",children:l}),i.jsx("span",{children:e||n("layout_editor.page_settings.meta.icon_placeholder")}),i.jsx("span",{"aria-hidden":"true",children:s?"▴":"▾"})]}),s?i.jsx(o,{control:r,value:e,onChange:d=>{t(d),a(!1)},t:n}):null]})}const mx={display:"flex",flexDirection:"column",gap:16,minWidth:0},vx={display:"flex",flexDirection:"column",gap:6,minWidth:0},_x={fontSize:13,fontWeight:600,color:"#0f172a"},xx={margin:0,fontSize:11,color:"#94a3b8"},wx={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%"};function Dr({checked:e,onChange:t,label:n,testid:o,ariaLabel:r,disabled:s=!1}){return i.jsxs("label",{style:{...Ex,...s?{cursor:"not-allowed"}:{}},children:[i.jsx("button",{type:"button","data-testid":o,role:"switch","aria-checked":e,"aria-label":r,disabled:s,onClick:()=>t(!e),style:Ix(e,s),children:i.jsx("span",{style:Ax(e)})}),n!=null?i.jsx("span",{style:Tx(s),children:n}):null]})}function Nr({disabled:e,children:t,testid:n,style:o}){return i.jsx("div",{"data-testid":n,"data-disabled":e?"true":"false","aria-disabled":e,style:{...o,...e?{opacity:.5,pointerEvents:"none",userSelect:"none"}:{}},children:t})}function kx({filled:e,sourceLabel:t,onRevert:n,revertLabel:o,hint:r,children:s,testid:a}){const l=e&&typeof n=="function",d=!e&&t!=null&&t!=="";return i.jsxs("div",{"data-testid":a,style:Sx,children:[s,l?i.jsxs("button",{type:"button","data-testid":a?`${a}-revert`:void 0,onClick:n,title:r??o,"aria-label":o,style:$x,children:["↩ ",o]}):d?i.jsx("span",{"data-testid":a?`${a}-source`:void 0,title:r,style:Cx,children:t}):null]})}const Sx={position:"relative",minWidth:0,width:"100%"},Cx={position:"absolute",right:8,top:"50%",transform:"translateY(-50%)",fontSize:10,color:"#94a3b8",background:"#f1f5f9",borderRadius:4,padding:"1px 6px",pointerEvents:"none",maxWidth:"45%",overflow:"hidden",whiteSpace:"nowrap",textOverflow:"ellipsis"},$x={position:"absolute",right:6,top:"50%",transform:"translateY(-50%)",fontSize:10,color:"#0369a1",background:"#e0f2fe",border:"1px solid #bae6fd",borderRadius:4,padding:"1px 6px",cursor:"pointer",whiteSpace:"nowrap"},Ex={display:"inline-flex",alignItems:"center",gap:8,cursor:"pointer"},Ix=(e,t)=>({position:"relative",width:38,height:22,flexShrink:0,borderRadius:999,border:"none",padding:0,background:t?"#e2e8f0":e?"#2563eb":"#cbd5e1",cursor:t?"not-allowed":"pointer",transition:"background 0.15s ease"}),Ax=e=>({position:"absolute",top:2,left:e?18:2,width:18,height:18,borderRadius:"50%",background:"#fff",boxShadow:"0 1px 2px rgba(0,0,0,0.2)",transition:"left 0.15s ease"}),Tx=e=>({fontSize:13,fontWeight:600,color:e?"#94a3b8":"#0f172a"}),Dx=/\$(?:core|module|plugin)_settings:[a-zA-Z0-9._:-]+/;function Nx(e){const t=[];let n=!1;const o=s=>{t.push({kind:"text",text:s}),n=!0},r=(s,a,l)=>{n||o(""),t.push({kind:"chip",raw:s,text:a,removable:l}),n=!1};for(const s of zx(e))s.kind==="text"?o(s.raw):r(s.raw,s.label,s.removable);return n||o(""),t}function zx(e){const t=[];if(typeof e!="string"||e.length===0)return t;const n=e.length;let o=0,r=0;const s=a=>{a>r&&t.push({kind:"text",raw:e.slice(r,a),label:"",removable:!1})};for(;o<n;){if(e[o]==="{"&&e[o+1]==="{"){let a=o+2;for(;a<n&&!(e[a]==="}"&&e[a+1]==="}");)a++;if(a>=n){o++;continue}s(o);const l=e.slice(o,a+2);t.push({kind:"chip",raw:l,label:Vt(l),removable:!0}),o=a+2,r=o;continue}if(e[o]==="$"){const a=Dx.exec(e.slice(o));if(a&&a.index===0){s(o);const l=a[0],d=l.split(":").pop()||l;t.push({kind:"chip",raw:l,label:d,removable:!1}),o+=l.length,r=o;continue}}o++}return s(n),t}function Mx(e,t,n,o){const r=[];let s=!1;return e.forEach((a,l)=>{if(a.kind==="chip"){if(!s&&a.raw===t){s=!0;return}r.push(a.raw??"")}else{const d=a.text??"";if(l===n){const c=Math.max(0,Math.min(o,d.length));r.push(d.slice(0,c),t,d.slice(c))}else r.push(d)}}),r.join("")}function Lx(e,t,n){const o=Math.max(0,Math.min(t,e.length));return e.slice(0,o)+n+e.slice(o)}function Px(e,t){const n=e.indexOf(t);return n<0?e:e.slice(0,n)+e.slice(n+t.length)}function bs({value:e,onChange:t,t:n,candidates:o,onDone:r,testidPrefix:s="g7le-binding-chip-text",disabled:a=!1}){const l=u.useMemo(()=>Nx(e),[e]),d=u.useRef(null),c=!!o&&o.length>0,[p,f]=u.useState(null),[y,g]=u.useState(null),[m,h]=u.useState(null),[v,b]=u.useState(!1),x=u.useRef(0),k=u.useRef(null),w=u.useMemo(()=>l.map(L=>L.kind==="chip"?`c:${L.raw}`:"t").join("|"),[l]),_=u.useRef(null),C=u.useRef([]),E=u.useRef(!1);u.useLayoutEffect(()=>{const L=E.current;E.current=!1,!(!L&&_.current===w)&&(_.current=w,l.forEach((I,P)=>{if(I.kind!=="text")return;const F=C.current[P];F&&F.textContent!==(I.text??"")&&(F.textContent=I.text??"")}))},[w,l,e]);const S=u.useCallback(()=>{const L=d.current;if(!L)return e;const I=[];for(const P of Array.from(L.querySelectorAll("[data-seg]")))P.dataset.seg==="chip"?I.push(P.dataset.raw??""):I.push(P.textContent??"");return I.join("")},[e]),T=u.useCallback(()=>{a||t(S())},[a,t,S]),$=u.useRef(null),A=u.useRef(!1),z=(L,I)=>{if(!a){L.preventDefault(),L.stopPropagation(),$.current={raw:I,pointerId:L.pointerId},A.current=!1,f(I);try{L.currentTarget.setPointerCapture(L.pointerId)}catch{}}},D=L=>{const I=$.current;if(!I||L.pointerId!==I.pointerId)return;A.current=!0;const P=B(L.clientX,L.clientY);P&&(g({slotIdx:P.slotIdx,offset:P.offset}),h(Bx(d.current,P.slotIdx,P.offset)))},O=(L,I)=>{if(L.stopPropagation(),A.current){A.current=!1;return}E.current=!0,t(Px(e,I))},M=L=>{const I=$.current;if(!I||L.pointerId!==I.pointerId)return;try{L.currentTarget.releasePointerCapture(L.pointerId)}catch{}$.current=null,A.current&&typeof setTimeout=="function"&&setTimeout(()=>{A.current=!1},0);const P=B(L.clientX,L.clientY);if(f(null),g(null),h(null),!P)return;const F=d.current;if(!F)return;const V=Array.from(F.querySelectorAll("[data-seg]")).map(Z=>Z.dataset.seg==="chip"?{kind:"chip",raw:Z.dataset.raw??""}:{kind:"text",text:Z.textContent??""}),Y=Mx(V,I.raw,P.slotIdx,P.offset);Y!==e&&(E.current=!0,t(Y))},B=(L,I)=>{const P=d.current;if(!P)return null;const F=Array.from(P.querySelectorAll("[data-seg]")),G=P.ownerDocument,V=G.elementFromPoint(L,I),Y=V?.closest('[data-seg="text"]');if(Y)return{slotIdx:F.indexOf(Y),offset:Ox(G,Y,L,I)};const Z=V?.closest('[data-seg="chip"]');if(Z){const re=F.indexOf(Z),U=Z.getBoundingClientRect();if(L<U.left+U.width/2&&re-1>=0){const ee=F[re-1];return{slotIdx:re-1,offset:(ee.textContent??"").length}}if(re+1<F.length)return{slotIdx:re+1,offset:0}}const se=F.map((re,U)=>({s:re,i:U})).filter(re=>re.s.dataset.seg==="text").pop();return se?{slotIdx:se.i,offset:(se.s.textContent??"").length}:null},N=()=>{a||!c||(x.current=Wx(d.current,e.length),b(L=>!L))},H=L=>{const I=L.path?L.path.split(".").filter(Boolean):[],P=[L.sourceId,...I].join("?.");E.current=!0,t(Lx(e,x.current,`{{${P}}}`)),b(!1)};return i.jsxs("div",{"data-testid":s,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsxs("div",{ref:d,"data-testid":`${s}-box`,className:"g7le-binding-chip-input",style:jx,children:[l.map((L,I)=>L.kind==="chip"?i.jsxs("span",{"data-seg":"chip","data-raw":L.raw,"data-testid":`${s}-chip-${I}`,contentEditable:!1,onPointerDown:P=>z(P,L.raw??""),onPointerMove:D,onPointerUp:M,title:L.removable?n("layout_editor.value_tree.change_data"):n("layout_editor.prop_i18n.bound_code_only"),style:{...Vx,...p===L.raw?Ux:null,touchAction:"none"},children:[i.jsx("span",{"aria-hidden":"true",style:qx,children:"⠿"}),i.jsx("span",{"aria-hidden":"true",style:{marginRight:3},children:"🔗"}),i.jsx("span",{children:L.text}),L.removable&&!a&&i.jsx("button",{type:"button","data-testid":`${s}-chip-remove-${I}`,"aria-label":n("layout_editor.inline_binding.clear"),title:n("layout_editor.inline_binding.clear"),onPointerDown:P=>{P.stopPropagation(),P.preventDefault()},onClick:P=>O(P,L.raw??""),style:Gx,children:"✕"})]},`chip-${L.raw}-${I}`):i.jsx("span",{ref:P=>{C.current[I]=P},"data-seg":"text",contentEditable:!a,suppressContentEditableWarning:!0,onInput:T,"data-testid":`${s}-text-${I}`,style:{...Hx,...p?Fx:null,...y?.slotIdx===I?Kx:null}},`text-slot-${I}`)),m&&i.jsx("span",{"aria-hidden":"true","data-testid":`${s}-caret`,style:{position:"absolute",left:m.left,top:m.top,width:2,height:m.height,background:"#2563eb",borderRadius:1,pointerEvents:"none",boxShadow:"0 0 0 1px rgba(37,99,235,0.3)"}})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"},children:[r&&i.jsxs("button",{type:"button","data-testid":`${s}-done`,onClick:r,style:gc,title:n("layout_editor.value_tree.done"),"aria-label":n("layout_editor.value_tree.done"),children:["✓ ",n("layout_editor.value_tree.done")]}),c&&i.jsxs("div",{style:{position:"relative"},children:[i.jsxs("button",{ref:k,type:"button","data-testid":`${s}-insert`,onClick:N,disabled:a,style:gc,title:n("layout_editor.inline_binding.insert_data"),"aria-label":n("layout_editor.inline_binding.insert_data"),children:["+ ",n("layout_editor.inline_binding.insert_data")]}),i.jsx(Xn,{anchorRef:k,open:v,onClose:()=>b(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:H,testIdSuffix:`${s}-insert`,defaultOpen:!0,floating:!1})})]}),i.jsx("span",{style:Yx,children:n("layout_editor.translation.placeholder_locked")})]})]})}function Ox(e,t,n,o){const r=(t.textContent??"").length;try{const a=e;let l=null,d=0;if(typeof a.caretRangeFromPoint=="function"){const c=a.caretRangeFromPoint(n,o);c&&(l=c.startContainer,d=c.startOffset)}else if(typeof a.caretPositionFromPoint=="function"){const c=a.caretPositionFromPoint(n,o);c&&(l=c.offsetNode,d=c.offset)}if(l&&t.contains(l))return Math.max(0,Math.min(d,r))}catch{}const s=t.getBoundingClientRect();return n>s.left+s.width/2?r:0}function Bx(e,t,n){if(!e)return null;const r=Array.from(e.querySelectorAll("[data-seg]"))[t];if(!r)return null;const s=e.getBoundingClientRect();try{const d=r.firstChild,c=e.ownerDocument;if(d&&d.nodeType===Node.TEXT_NODE&&(d.textContent??"").length>0){const p=c.createRange(),f=Math.max(0,Math.min(n,(d.textContent??"").length));p.setStart(d,f),p.setEnd(d,f);const y=p.getClientRects(),g=y.length>0?y[0]:p.getBoundingClientRect();if(g&&(g.height>0||g.width>=0))return{left:g.left-s.left,top:g.top-s.top,height:g.height||r.getBoundingClientRect().height}}}catch{}const a=r.getBoundingClientRect();return{left:(n>0?a.right:a.left)-s.left,top:a.top-s.top,height:a.height||16}}function Wx(e,t){if(!e)return t;try{const n=e.ownerDocument.defaultView?.getSelection();if(!n||n.rangeCount===0)return t;const o=n.getRangeAt(0);let r=o.startContainer,s=null;for(;r&&r!==e;){if(r instanceof HTMLElement&&r.dataset.seg==="text"){s=r;break}r=r.parentNode}if(!s||!e.contains(s))return t;const a=Array.from(e.querySelectorAll("[data-seg]"));let l=0;for(const d of a){if(d===s)break;l+=d.dataset.seg==="chip"?(d.dataset.raw??"").length:(d.textContent??"").length}return l+o.startOffset}catch{return t}}const jx={position:"relative",display:"flex",flexWrap:"wrap",alignItems:"center",gap:2,minHeight:30,padding:"4px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",fontSize:12,lineHeight:1.6,minWidth:0,width:"100%",boxSizing:"border-box"},Hx={outline:"none",minWidth:8,minHeight:"1.2em",whiteSpace:"pre-wrap",color:"#0f172a",display:"inline-block",wordBreak:"break-all"},Fx={boxShadow:"inset 0 -2px 0 #93c5fd",background:"#eff6ff",borderRadius:2},Kx={background:"#dbeafe",boxShadow:"inset 0 -2px 0 #2563eb"},Vx={display:"inline-flex",alignItems:"center",gap:1,background:"linear-gradient(180deg,#eff6ff,#dbeafe)",color:"#1d4ed8",border:"1px dashed #60a5fa",borderRadius:9999,padding:"1px 8px 1px 4px",margin:"0 1px",fontSize:11,fontWeight:600,cursor:"grab",userSelect:"none",whiteSpace:"nowrap",boxShadow:"0 1px 2px rgba(37,99,235,0.18)",verticalAlign:"middle"},Ux={opacity:.5,borderStyle:"solid",cursor:"grabbing"},qx={color:"#2563eb",opacity:.7,fontSize:11,marginRight:1,cursor:"grab"},Gx={marginLeft:4,width:14,height:14,lineHeight:"12px",padding:0,border:"none",borderRadius:9999,background:"rgba(29,78,216,0.12)",color:"#1d4ed8",fontSize:9,fontWeight:700,cursor:"pointer",display:"inline-flex",alignItems:"center",justifyContent:"center",flex:"0 0 auto"},gc={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Yx={fontSize:10,color:"#94a3b8"},Xx=ql;function ms(e){return _b(e)}function Ve({value:e,onChange:t,t:n,candidates:o,placeholder:r,testidPrefix:s="g7le-data-chip-value",readOnly:a=!1,enableExpressionTree:l=!0}){const d=typeof e=="string"?e:"",c=!!o&&o.length>0,[p,f]=u.useState(!1),[y,g]=u.useState(!1),[m,h]=u.useState(!1),v=u.useCallback(E=>i.jsx(Ve,{value:E.value,onChange:S=>E.onChange(S),t:E.t,candidates:E.candidates,testidPrefix:E.testidPrefix,enableExpressionTree:!0}),[]),b=()=>i.jsx("div",{style:{display:"flex",gap:4,alignItems:"center",width:"100%",minWidth:0},children:i.jsx("input",{type:"text","data-testid":`${s}-input`,value:d,placeholder:r??n("layout_editor.page_settings.seo.value_placeholder"),readOnly:a,onChange:E=>t(E.target.value),onFocus:()=>f(!0),onBlur:()=>f(!1),style:{flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%",background:a?"#f1f5f9":"#fff",color:a?"#64748b":"#0f172a"}})}),x=u.useCallback(E=>{const S=d?`${d} ${E.expression}`:E.expression;t(S)},[d,t]);if(!l||a)return i.jsxs("div",{"data-testid":s,style:pn,children:[b(),!a&&c&&i.jsx($t,{candidates:o,t:n,onSelect:x,testIdSuffix:`${s}-pick`})]});const k=Nv(d),w=Gd(d),_=Kt(d),C=E=>{const S=Sr(d),T=S?us(S,()=>"",$=>Vt($)):[];return i.jsxs(i.Fragment,{children:[i.jsx("div",{style:{display:"flex",justifyContent:"flex-start"},children:i.jsxs("button",{type:"button","data-testid":`${E}-to-plain`,onClick:()=>h(!0),style:Rx,title:n("layout_editor.value_tree.to_plain"),"aria-label":n("layout_editor.value_tree.to_plain"),children:["↩ ",n("layout_editor.value_tree.to_plain")]})}),m&&i.jsxs("div",{"data-testid":`${E}-to-plain-confirm`,style:e0,children:[i.jsx("p",{style:t0,children:n("layout_editor.value_tree.to_plain_lead")}),i.jsx("div",{style:n0,children:T.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:n("layout_editor.page_settings.seo.value_placeholder")}):T.map(($,A)=>$.kind==="chip"?i.jsxs("span",{style:vs,children:["🔗 ",$.text]},A):i.jsx("span",{style:$.kind==="ellipsis"?{color:"#94a3b8"}:void 0,children:$.text},A))}),i.jsxs("p",{style:o0,children:["⚠ ",n("layout_editor.value_tree.to_plain_warn")]}),i.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:6},children:[i.jsx("button",{type:"button","data-testid":`${E}-to-plain-confirm-cancel`,onClick:()=>h(!1),style:Ut,children:n("layout_editor.value_tree.to_plain_cancel")}),i.jsx("button",{type:"button","data-testid":`${E}-to-plain-confirm-ok`,onClick:()=>{h(!1),t(Sr(d)||"")},style:r0,children:n("layout_editor.value_tree.to_plain_ok")})]})]})]})};if(!p&&(k==="expression"||w)){if(w)return i.jsxs("div",{"data-testid":s,style:pn,children:[!a&&C(`${s}-seg`),i.jsx(gs,{value:d,onChange:S=>t(S),t:n,candidates:o,testidPrefix:`${s}-seg`,renderLeafInput:v})]});const E=un(d);return i.jsxs("div",{"data-testid":s,style:pn,children:[!a&&C(`${s}-tree`),i.jsx(nc,{node:E.node,onChange:S=>t(Zn(S,!1)),t:n,candidates:o,testidPrefix:`${s}-tree`,renderLeafInput:v})]})}if(_&&!p&&!y)return i.jsx(Jx,{value:d,onChange:t,t:n,candidates:o,testidPrefix:s,onEditRaw:()=>g(!0)});if(!p&&!y&&ms(d)){const E=Xx(d);return i.jsx("div",{"data-testid":s,style:pn,children:i.jsxs("div",{style:hc,children:[i.jsx("div",{"data-testid":`${s}-chips`,style:Zx,children:E.map((S,T)=>S.kind==="chip"?i.jsxs("span",{style:vs,children:["🔗 ",S.label]},T):i.jsx("span",{children:S.label},T))}),i.jsxs("button",{type:"button","data-testid":`${s}-edit-raw`,onClick:()=>g(!0),style:Ut,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:["✎ ",n("layout_editor.value_tree.edit")]})]})})}return y&&ms(d)?i.jsxs("div",{"data-testid":s,style:pn,children:[i.jsx(bs,{value:d,onChange:t,t:n,candidates:o,disabled:a,onDone:()=>g(!1),testidPrefix:`${s}-chipedit`}),!a&&k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]}),k==="raw"&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]}):!p&&ms(d)?i.jsxs("div",{"data-testid":s,style:pn,children:[i.jsx(bs,{value:d,onChange:t,t:n,candidates:o,disabled:a,onDone:()=>{g(!1),f(!1)},testidPrefix:`${s}-chipedit`}),!a&&k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]}),k==="raw"&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]}):i.jsxs("div",{"data-testid":s,style:pn,children:[b(),i.jsxs("div",{style:Qx,children:[c&&i.jsx($t,{candidates:o,t:n,onSelect:x,testIdSuffix:`${s}-pick`}),k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]})]}),k==="raw"&&!p&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]})}function Jx({value:e,onChange:t,t:n,candidates:o,testidPrefix:r,onEditRaw:s}){const[a,l]=u.useState(!1),d=u.useRef(null),c=Vt(e),p=!!o&&o.length>0,f=Kt(e)?.localeFn;return i.jsxs("div",{"data-testid":r,style:pn,children:[i.jsxs("div",{style:hc,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:vs,children:["🔗 ",c]}),p&&i.jsx("button",{ref:d,type:"button","data-testid":`${r}-change`,onClick:()=>l(y=>!y),style:Ut,title:n("layout_editor.value_tree.change_data"),"aria-label":n("layout_editor.value_tree.change_data"),children:n("layout_editor.value_tree.change_data")}),i.jsx("button",{type:"button","data-testid":`${r}-edit-raw`,onClick:s,style:Ut,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:"✎"})]}),p&&i.jsx(Xn,{anchorRef:d,open:a,onClose:()=>l(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:y=>{if(f){const h=[y.sourceId,...y.path?y.path.split(".").filter(Boolean):[]].join(".");t(`{{${f}(${h})}}`),l(!1);return}const g=y.path?y.path.split(".").filter(Boolean):[],m=[y.sourceId,...g].join("?.");t(`{{${m}}}`),l(!1)},testIdSuffix:`${r}-pick`,defaultOpen:!0,floating:!1})})]})}const pn={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0},hc={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},Zx={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,padding:"5px 8px",fontSize:12,color:"#334155",border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc"},vs={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"2px 8px",borderRadius:12,whiteSpace:"nowrap"},Ut={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Qx={display:"flex",flexWrap:"wrap",alignItems:"center",gap:6},_s={margin:"2px 0 0",fontSize:11,color:"#94a3b8",lineHeight:1.4},Rx={padding:"3px 10px",fontSize:11,border:"1px solid #fca5a5",borderRadius:6,background:"#fef2f2",color:"#b91c1c",cursor:"pointer",whiteSpace:"nowrap"},e0={display:"flex",flexDirection:"column",gap:6,padding:10,border:"1px solid #fca5a5",borderRadius:8,background:"#fffbeb"},t0={margin:0,fontSize:12,color:"#334155"},n0={padding:"5px 8px",fontSize:12,color:"#0f172a",background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,wordBreak:"break-all",display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"},o0={margin:0,fontSize:11,color:"#b45309"},r0={padding:"3px 12px",fontSize:11,border:"1px solid #dc2626",borderRadius:6,background:"#dc2626",color:"#fff",cursor:"pointer",whiteSpace:"nowrap"};let yc=0;function zr({value:e,onChange:t,keyField:n,valueField:o="content",t:r,candidates:s,testidPrefix:a="g7le-kv-chip-editor",keyPlaceholder:l,valuePlaceholder:d,addLabel:c,renderKeyInput:p}){const f=u.useMemo(()=>(Array.isArray(e)?e:[]).map(k=>({id:yc++,k:typeof k[n]=="string"?k[n]:"",v:typeof k[o]=="string"?k[o]:""})),[]),[y,g]=u.useState(f),m=u.useCallback(k=>{g(k);const w=k.filter(_=>_.k.trim()!=="").map(_=>({[n]:_.k,[o]:_.v}));t(w)},[n,o,t]),h=u.useCallback(()=>{m([...y,{id:yc++,k:"",v:""}])},[y,m]),v=u.useCallback(k=>{m(y.filter(w=>w.id!==k))},[y,m]),b=u.useCallback((k,w)=>{m(y.map(_=>_.id===k?{..._,k:w}:_))},[y,m]),x=u.useCallback((k,w)=>{m(y.map(_=>_.id===k?{..._,v:w}:_))},[y,m]);return i.jsxs("div",{"data-testid":a,style:{display:"flex",flexDirection:"column",gap:6},children:[y.map(k=>i.jsxs("div",{"data-testid":`${a}-row-${k.id}`,style:{display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},children:[i.jsxs("div",{style:{flex:1,minWidth:0,display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2,minWidth:0},children:p?i.jsx("div",{style:{minWidth:0},children:p({value:k.k,onChange:w=>b(k.id,w),testid:`${a}-key-${k.id}`,placeholder:l??r(`layout_editor.page_settings.seo.extra_${n}`)})}):i.jsx("input",{type:"text","data-testid":`${a}-key-${k.id}`,value:k.k,placeholder:l??r(`layout_editor.page_settings.seo.extra_${n}`),onChange:w=>b(k.id,w.target.value),style:{width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,boxSizing:"border-box"}})}),i.jsx("div",{style:{minWidth:0},children:i.jsx(Ve,{value:k.v,onChange:w=>x(k.id,w),t:r,candidates:s,placeholder:d??r("layout_editor.page_settings.seo.extra_content"),testidPrefix:`${a}-value-${k.id}`})})]}),i.jsx("button",{type:"button","data-testid":`${a}-remove-${k.id}`,onClick:()=>v(k.id),title:r("layout_editor.page_settings.seo.remove"),"aria-label":r("layout_editor.page_settings.seo.remove"),style:{flex:"0 0 auto",padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]},k.id)),i.jsxs("button",{type:"button","data-testid":`${a}-add`,onClick:h,style:{alignSelf:"flex-start",padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},children:["+ ",c??r("layout_editor.page_settings.seo.add_extra")]})]})}const i0=["website","article","product","profile"],s0=["summary","summary_large_image","app","player"];function a0(e){switch(e){case"core":return"layout_editor.page_settings.seo.og_source_core";case"inherited":return"layout_editor.page_settings.seo.og_source_inherited";case"filter":return"layout_editor.page_settings.seo.og_source_filter";case"layout":return null;default:return e?"layout_editor.page_settings.seo.og_source_module":null}}function bc(e){const t={};for(const n of e??[])t[n.key]=n;return t}function mc(e,t){return!!e&&t&&typeof e.sourceExpr=="string"&&e.sourceExpr!==""&&!e.overriddenByLayout&&!e.lockedByFilter}function vc({label:e,sourceExpr:t,onReplace:n,t:o,testid:r}){return i.jsxs("div",{"data-testid":r,style:b0,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:m0,title:t,children:["🔗 ",e||t]}),i.jsx("span",{"data-testid":`${r}-badge`,style:v0,children:o("layout_editor.page_settings.seo.auto_chip_source")}),i.jsx("button",{type:"button","data-testid":`${r}-replace`,onClick:n,style:_0,children:o("layout_editor.page_settings.seo.auto_chip_replace")})]})}function l0({og:e,twitter:t,onChangeOg:n,onChangeTwitter:o,ogPreview:r,twitterPreview:s,defaultsAvailable:a=!1,candidates:l,t:d,testidPrefix:c="g7le-seo-og"}){const p=e&&typeof e=="object"?e:{},f=t&&typeof t=="object"?t:{},[y,g]=u.useState(p.image_height!=null||p.image_type!=null||p.image_secure_url!=null),m=bc(r),h=bc(s),v=u.useCallback((T,$)=>{const A={...p};$===void 0||$===""||$===null?delete A[T]:A[T]=$,n(A)},[p,n]),b=u.useCallback((T,$)=>{const A={...f};$===void 0||$===""||$===null?delete A[T]:A[T]=$,o(A)},[f,o]),x=(T,$,A)=>!A||!a||!A.lockedByFilter?null:i.jsx("div",{style:{display:"flex",gap:6,alignItems:"center",marginTop:2},children:i.jsxs("span",{"data-testid":`${T}-locked-${$}`,style:g0,children:["🔒 ",d("layout_editor.page_settings.seo.og_locked")]})}),k=T=>{if(!T||!a||T.lockedByFilter||T.overriddenByLayout)return;const $=a0(T.source);return $?d($):void 0},w=(T,$)=>T&&a?T.overriddenByLayout===!0:$!=="",_=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),B?i.jsx("div",{"data-testid":`${T}-locked-value-${$}`,style:f0,children:String(M?.effectiveValue??"")}):mc(M,a)?i.jsx(vc,{label:M.label??"",sourceExpr:M.sourceExpr,onReplace:()=>D($,M.sourceExpr),t:d,testid:`${T}-${$}-auto`}):i.jsx(vt,{value:N,onChange:H=>D($,H??""),t:d,candidates:l,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-field`,enableExpressionTree:!0,expressionTreeCollapsible:!0}),x(T,$,M)]})},C=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),mc(M,a)?i.jsx(vc,{label:M.label??"",sourceExpr:M.sourceExpr,onReplace:()=>D($,M.sourceExpr),t:d,testid:`${T}-${$}-auto`}):i.jsx(Ve,{value:N,onChange:H=>D($,H),t:d,candidates:l,readOnly:B,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-field`}),x(T,$,M)]})},E=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"",H=w(M,N),L=H?N:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),i.jsx(kx,{filled:H,sourceLabel:k(M),onRevert:B?void 0:()=>D($,void 0),revertLabel:d("layout_editor.page_settings.seo.revert_default"),testid:`${T}-${$}-overlay`,children:i.jsx("div",{"data-testid":`${T}-${$}-input`,style:{minWidth:0},children:i.jsx(Ve,{value:L,onChange:I=>D($,I),t:d,candidates:l,readOnly:B,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-chip`})})}),x(T,$,M)]})},S=(T,$,A,z,D,O,M)=>{const B=M[$],N=typeof D[$]=="string"?D[$]:"",H=w(B,N),L=k(B),I=H?N:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsxs("div",{style:c0,children:[i.jsx("label",{style:to,children:d(A)}),L?i.jsxs("span",{"data-testid":`${T}-${$}-source-inline`,style:u0,children:["· ",L]}):null]}),i.jsxs("select",{"data-testid":`${T}-${$}-select`,value:I,onChange:P=>O($,P.target.value),style:p0,children:[i.jsx("option",{value:"",children:a&&B?.effectiveValue!=null&&B.effectiveValue!==""?`${d("layout_editor.page_settings.seo.use_default")} (${String(B.effectiveValue)})`:d("layout_editor.page_settings.seo.use_default")}),z.map(P=>i.jsx("option",{value:P,children:P},P))]}),x(T,$,B)]})};return i.jsxs("div",{"data-testid":c,style:{display:"flex",flexDirection:"column",gap:12},children:[i.jsxs("div",{"data-testid":`${c}-og-section`,style:_c,children:[i.jsx("div",{style:xc,children:d("layout_editor.page_settings.seo.og_section")}),i.jsxs("p",{"data-testid":`${c}-intro`,style:d0,children:["ⓘ ",d("layout_editor.page_settings.seo.og_intro")]}),S(c,"type","layout_editor.page_settings.seo.og_type",i0,p,v,m),_(c,"title","layout_editor.page_settings.seo.og_title",p,v,m),_(c,"description","layout_editor.page_settings.seo.og_description",p,v,m),C(c,"image","layout_editor.page_settings.seo.og_image",p,v,m),_(c,"image_alt","layout_editor.page_settings.seo.og_image_alt",p,v,m),i.jsxs("div",{style:Dn,children:[i.jsxs("button",{type:"button","data-testid":`${c}-image-adv-toggle`,"aria-expanded":y,onClick:()=>g(T=>!T),style:h0,children:[y?"▾":"▸"," ",d("layout_editor.page_settings.seo.og_image_adv")]}),y?i.jsxs("div",{"data-testid":`${c}-image-adv`,style:y0,children:[E(c,"image_height","layout_editor.page_settings.seo.og_image_height",p,v,m),E(c,"image_type","layout_editor.page_settings.seo.og_image_type",p,v,m),E(c,"image_secure_url","layout_editor.page_settings.seo.og_image_secure_url",p,v,m)]}):null]}),E(c,"site_name","layout_editor.page_settings.seo.og_site_name",p,v,m),E(c,"locale","layout_editor.page_settings.seo.og_locale",p,v,m),i.jsxs("div",{"data-testid":`${c}-extra`,style:Dn,children:[i.jsx("label",{style:to,children:d("layout_editor.page_settings.seo.og_extra")}),i.jsx(zr,{value:Array.isArray(p.extra)?p.extra:[],onChange:T=>v("extra",T.length>0?T:void 0),keyField:"property",t:d,candidates:l,testidPrefix:`${c}-extra-editor`})]})]}),i.jsxs("div",{"data-testid":`${c}-tw-section`,style:_c,children:[i.jsx("div",{style:xc,children:d("layout_editor.page_settings.seo.tw_section")}),i.jsx("p",{style:{margin:"0 0 6px",fontSize:11,color:"#94a3b8"},children:d("layout_editor.page_settings.seo.tw_fallback_note")}),S("g7le-seo-tw","card","layout_editor.page_settings.seo.tw_card",s0,f,b,h),E("g7le-seo-tw","site","layout_editor.page_settings.seo.tw_site",f,b,h),E("g7le-seo-tw","creator","layout_editor.page_settings.seo.tw_creator",f,b,h),_("g7le-seo-tw","title","layout_editor.page_settings.seo.tw_title",f,b,h),_("g7le-seo-tw","description","layout_editor.page_settings.seo.tw_description",f,b,h),C("g7le-seo-tw","image","layout_editor.page_settings.seo.tw_image",f,b,h),_("g7le-seo-tw","image_alt","layout_editor.page_settings.seo.tw_image_alt",f,b,h),i.jsxs("div",{"data-testid":"g7le-seo-tw-extra",style:Dn,children:[i.jsx("label",{style:to,children:d("layout_editor.page_settings.seo.tw_extra")}),i.jsx(zr,{value:Array.isArray(f.extra)?f.extra:[],onChange:T=>b("extra",T.length>0?T:void 0),keyField:"name",t:d,candidates:l,testidPrefix:"g7le-seo-tw-extra-editor"})]})]})]})}const _c={border:"1px solid #e2e8f0",borderRadius:8,padding:12,display:"flex",flexDirection:"column",gap:10},xc={fontSize:13,fontWeight:600,color:"#0f172a"},d0={margin:0,fontSize:11,color:"#64748b",background:"#f8fafc",borderRadius:6,padding:"6px 8px"},Dn={display:"flex",flexDirection:"column",gap:4},to={fontSize:12,color:"#475569"},c0={display:"flex",alignItems:"center",gap:6},u0={fontSize:11,color:"#94a3b8"},p0={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0,boxSizing:"border-box"},f0={padding:"5px 8px",fontSize:12,border:"1px solid #e2e8f0",borderRadius:6,background:"#f1f5f9",color:"#64748b"},g0={fontSize:11,color:"#7c3aed"},h0={alignSelf:"flex-start",padding:"2px 0",fontSize:12,fontWeight:600,color:"#475569",background:"transparent",border:"none",cursor:"pointer"},y0={display:"flex",flexDirection:"column",gap:8,paddingLeft:14,marginTop:4,minWidth:0},b0={display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",minWidth:0},m0={display:"inline-flex",alignItems:"center",maxWidth:"100%",padding:"3px 8px",fontSize:12,color:"#1d4ed8",background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:6,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},v0={fontSize:11,color:"#94a3b8"},_0={padding:"2px 8px",fontSize:11,color:"#475569",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,cursor:"pointer"},x0=["WebPage","WebSite","ItemList","BreadcrumbList","Organization","Person","CollectionPage","AboutPage","ContactPage","SearchAction"];function Mr(e){const t=[],n=(o,r)=>{for(const[s,a]of Object.entries(o)){if(s==="@type"||s==="@context")continue;const l=r?`${r}.${s}`:s;a!==null&&typeof a=="object"&&!Array.isArray(a)?n(a,l):t.push({key:l,value:a==null?"":String(a)})}};return n(e,""),t}function wc(e,t){const n={"@type":e};for(const{key:o,value:r}of t){if(o.trim()==="")continue;const s=o.split(".");let a=n;for(let l=0;l<s.length-1;l++){const d=s[l];(typeof a[d]!="object"||a[d]===null||Array.isArray(a[d]))&&(a[d]={}),a=a[d]}a[s[s.length-1]]=r}return n}let xs=0;function w0({value:e,onChange:t,autoBlock:n,autoMeta:o,lockedByFilter:r=!1,filteredBlock:s,pageType:a,candidates:l,t:d,testidPrefix:c="g7le-seo-sd"}){const p=!!e&&typeof e=="object",[f,y]=u.useState(p),g=p&&typeof e["@type"]=="string"?e["@type"]:"WebPage",[m,h]=u.useState(g),[v,b]=u.useState(()=>p?Mr(e).map(D=>({id:xs++,...D})):[]),x=u.useCallback((D,O)=>{h(D),b(O),t(wc(D,O.map(M=>({key:M.key,value:M.value}))))},[t]),k=u.useCallback(()=>{const D=n,O=typeof D["@type"]=="string"?D["@type"]:m,M=Mr(D).map(B=>({id:xs++,key:B.key,value:o?.[B.key]?.expr??B.value}));return{type:O,rows:M}},[n,o,m]),w=u.useCallback(D=>{if(y(D),D){if(v.length===0&&n&&typeof n=="object"){const O=k();x(O.type,O.rows);return}t(wc(m,v.map(O=>({key:O.key,value:O.value}))))}else t(null)},[m,v,n,k,x,t]),_=u.useCallback(()=>{if(!n)return;const D=k();x(D.type,D.rows)},[n,k,x]),C=u.useCallback(()=>{x(m,[...v,{id:xs++,key:"",value:""}])},[m,v,x]),E=u.useCallback(D=>{x(m,v.filter(O=>O.id!==D))},[m,v,x]),S=u.useCallback((D,O)=>{x(m,v.map(M=>M.id===D?{...M,key:O}:M))},[m,v,x]),T=u.useCallback((D,O)=>{x(m,v.map(M=>M.id===D?{...M,value:O}:M))},[m,v,x]),$=u.useMemo(()=>n?Mr(n):[],[n]),A=u.useMemo(()=>s?Mr(s):[],[s]),z=v.some(D=>D.key.trim()!==""&&D.value.trim()==="");if(r){const D=s&&typeof s["@type"]=="string"?s["@type"]:"";return i.jsxs("div",{"data-testid":c,style:kc,children:[i.jsxs("div",{style:Sc,children:[i.jsx("span",{style:k0,children:"🔒"}),i.jsx("span",{style:{fontSize:13,fontWeight:600},children:d("layout_editor.page_settings.seo.sd_mode")}),i.jsx("span",{"data-testid":`${c}-locked-badge`,style:S0,children:d("layout_editor.page_settings.seo.sd_filter_locked")})]}),i.jsxs("div",{"data-testid":`${c}-locked`,style:C0,children:[i.jsxs("p",{style:{margin:"0 0 6px",fontSize:12,color:"#475569"},children:["ⓘ ",d("layout_editor.page_settings.seo.sd_filter_locked_note")]}),i.jsxs("div",{"data-testid":`${c}-filtered-block`,children:[D?i.jsxs("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:["@type: ",D]}):null,A.map(O=>i.jsxs("div",{"data-testid":`${c}-filtered-row-${O.key}`,style:Cc,children:[i.jsx("code",{style:$c,children:O.key}),i.jsx("span",{style:Ec,children:O.value})]},O.key))]})]})]})}return i.jsxs("div",{"data-testid":c,style:kc,children:[i.jsx("div",{style:Sc,children:i.jsx(Dr,{checked:f,onChange:w,testid:`${c}-mode`,label:d("layout_editor.page_settings.seo.sd_manual_toggle")})}),!f&&i.jsxs("div",{"data-testid":`${c}-auto`,style:{marginTop:8},children:[i.jsxs("div",{"data-testid":`${c}-auto-preview`,style:$0,children:[i.jsxs("div",{style:{fontSize:12,fontWeight:600,marginBottom:6},children:[d("layout_editor.page_settings.seo.sd_auto_title"),n&&typeof n["@type"]=="string"?`: ${n["@type"]}`:"",a?i.jsxs("span",{style:{marginLeft:6,color:"#94a3b8",fontWeight:400},children:["(",a,")"]}):null]}),$.length===0?i.jsx("p",{style:{margin:0,fontSize:12,color:"#94a3b8"},children:d("layout_editor.page_settings.seo.sd_auto_empty")}):$.map(D=>{const O=o?.[D.key];return i.jsxs("div",{"data-testid":`${c}-auto-row-${D.key}`,style:Cc,children:[i.jsx("code",{style:$c,children:D.key}),O?i.jsxs("span",{"data-testid":`${c}-auto-chip-${D.key}`,style:E0,title:O.expr,children:["🔗 ",O.label||O.expr]}):i.jsx("span",{style:Ec,children:D.value})]},D.key)})]}),i.jsxs("p",{"data-testid":`${c}-replace-warn`,style:I0,children:["ⓘ ",d("layout_editor.page_settings.seo.sd_replace_pre_warn")]})]}),f&&i.jsxs("div",{"data-testid":`${c}-manual`,style:{marginTop:8},children:[i.jsxs("p",{"data-testid":`${c}-replace-warn`,style:A0,children:["⚠ ",d("layout_editor.page_settings.seo.sd_replace_warn")]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("label",{style:{fontSize:12,color:"#475569"},children:d("layout_editor.page_settings.seo.sd_type")}),i.jsx("input",{type:"text","data-testid":`${c}-type`,list:`${c}-type-options`,value:m,onChange:D=>x(D.target.value,v),style:T0}),i.jsx("datalist",{id:`${c}-type-options`,children:x0.map(D=>i.jsx("option",{value:D},D))})]}),i.jsx("div",{style:{marginTop:8,display:"flex",flexDirection:"column",gap:6},children:v.map(D=>i.jsxs("div",{"data-testid":`${c}-prop-${D.id}`,style:D0,children:[i.jsx("input",{type:"text","data-testid":`${c}-prop-key-${D.id}`,value:D.key,placeholder:d("layout_editor.page_settings.seo.sd_prop_key"),onChange:O=>S(D.id,O.target.value),style:{width:160,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0}}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:D.value,onChange:O=>T(D.id,O),t:d,candidates:l,placeholder:d("layout_editor.page_settings.seo.sd_prop_value"),testidPrefix:`${c}-prop-value-${D.id}`})}),i.jsx("button",{type:"button","data-testid":`${c}-prop-remove-${D.id}`,onClick:()=>E(D.id),title:d("layout_editor.page_settings.seo.remove"),"aria-label":d("layout_editor.page_settings.seo.remove"),style:N0,children:"✕"})]},D.id))}),i.jsxs("div",{style:{marginTop:8,display:"flex",gap:8},children:[i.jsxs("button",{type:"button","data-testid":`${c}-add`,onClick:C,style:Ic,children:["+ ",d("layout_editor.page_settings.seo.sd_add_prop")]}),n?i.jsxs("button",{type:"button","data-testid":`${c}-seed-from-module`,onClick:_,style:Ic,children:["↩ ",d("layout_editor.page_settings.seo.sd_seed_from_module")]}):null]}),i.jsxs("p",{"data-testid":`${c}-empty-note`,style:z0,children:["ⓘ ",d("layout_editor.page_settings.seo.sd_empty_note"),z?i.jsxs("span",{style:{color:"#b45309"},children:[" ",d("layout_editor.page_settings.seo.sd_has_empty")]}):null]})]})]})}const kc={display:"flex",flexDirection:"column"},Sc={display:"flex",alignItems:"center",gap:8},k0={fontSize:14},S0={fontSize:11,padding:"2px 6px",borderRadius:4,background:"#f1f5f9",color:"#64748b"},C0={marginTop:8,border:"1px solid #fcd34d",borderRadius:8,padding:10,background:"#fffbeb"},$0={border:"1px solid #e2e8f0",borderRadius:8,padding:10,background:"#f8fafc"},Cc={display:"flex",gap:8,alignItems:"center",padding:"2px 0"},$c={fontSize:11,color:"#475569",background:"#fff",padding:"2px 6px",borderRadius:4,minWidth:120},Ec={fontSize:11,color:"#0f172a",fontFamily:"monospace",wordBreak:"break-all"},E0={display:"inline-flex",alignItems:"center",padding:"2px 8px",fontSize:11,color:"#1d4ed8",background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:6},I0={margin:"6px 0 0",fontSize:12,color:"#64748b"},A0={margin:"0 0 8px",fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"6px 8px"},T0={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0},D0={display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},N0={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},Ic={padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},z0={margin:"8px 0 0",fontSize:11,color:"#94a3b8"},Ac=new Set(["core_setting","setting","query","route"]);function M0({vars:e,onChange:t,varCandidates:n,gatingMet:o=!1,candidates:r,t:s,testidPrefix:a="g7le-seo-vars"}){const l=u.useMemo(()=>e&&typeof e=="object"?{...e}:{},[e]),d=u.useMemo(()=>Array.isArray(n)?n:[],[n]),c=u.useMemo(()=>o?d.filter(_=>Ac.has(_.source)):[],[d,o]),p=u.useMemo(()=>o?d.filter(_=>_.source==="data"):[],[d,o]);u.useMemo(()=>new Set(c.map(_=>_.name)),[c]);const f=u.useMemo(()=>new Set(p.map(_=>_.name)),[p]),y=u.useMemo(()=>{const _=new Set;for(const C of d)Ac.has(C.source)&&_.add(C.name);return _},[d]),g=u.useMemo(()=>Object.keys(l).filter(_=>!f.has(_)),[l,f]),m=u.useCallback((_,C)=>{const E={...l};C===void 0?delete E[_]:E[_]=C,t(E)},[l,t]),[h,v]=u.useState(""),b=h.trim()!==""&&y.has(h.trim()),x=h.trim()!==""&&Object.prototype.hasOwnProperty.call(l,h.trim()),k=h.trim()!==""&&!b&&!x&&!f.has(h.trim()),w=u.useCallback(()=>{const _=h.trim();_===""||y.has(_)||Object.prototype.hasOwnProperty.call(l,_)||(t({...l,[_]:""}),v(""))},[h,y,l,t]);return i.jsxs("div",{"data-testid":a,style:{display:"flex",flexDirection:"column",gap:10},children:[i.jsx("span",{"data-testid":"g7le-seo-var-list",hidden:!0,"data-gating":o?"1":"0"}),!o&&i.jsxs("div",{"data-testid":`${a}-precondition`,style:L0,children:["⚠ ",s("layout_editor.page_settings.seo.vars_precondition")]}),c.length>0&&i.jsxs("div",{"data-testid":`${a}-auto-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_auto_group")}),c.map(_=>i.jsxs("div",{"data-testid":`g7le-seo-var-auto-${_.name}`,style:P0,children:[i.jsx("span",{style:O0,children:"🔒"}),i.jsx("code",{style:Ss,children:_.name}),i.jsxs("span",{style:B0,children:["〔",s("layout_editor.page_settings.seo.vars_auto"),"·",_.source,"〕"]}),_.owner?.name?i.jsx("span",{style:W0,children:_.owner.name}):null]},_.name))]}),p.length>0&&i.jsxs("div",{"data-testid":`${a}-data-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_data_group")}),p.map(_=>{const C=l[_.name]??"",E=C.trim()==="";return i.jsxs("div",{"data-testid":`g7le-seo-var-data-${_.name}`,style:Tc,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:4},children:[i.jsx("code",{style:Ss,children:_.name}),_.required?i.jsx("span",{"data-testid":`g7le-seo-var-required-${_.name}`,style:j0,children:"*"}):null]}),i.jsx(Ve,{value:C,onChange:S=>m(_.name,S??""),t:s,candidates:r,testidPrefix:`g7le-seo-var-data-field-${_.name}`}),_.required&&E?i.jsxs("p",{"data-testid":`g7le-seo-var-empty-warn-${_.name}`,style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_required_empty")]}):null]},_.name)})]}),i.jsxs("div",{"data-testid":`${a}-custom-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_custom_group")}),g.map(_=>i.jsxs("div",{"data-testid":`g7le-seo-var-custom-${_}`,style:Tc,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:4},children:[i.jsx("code",{style:Ss,children:_}),i.jsx("button",{type:"button","data-testid":`g7le-seo-var-custom-remove-${_}`,onClick:()=>m(_,void 0),title:s("layout_editor.page_settings.seo.remove"),"aria-label":s("layout_editor.page_settings.seo.remove"),style:{marginLeft:"auto",padding:"2px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]}),i.jsx(Ve,{value:l[_]??"",onChange:C=>m(_,C??""),t:s,candidates:r,testidPrefix:`g7le-seo-var-custom-field-${_}`})]},_)),i.jsxs("div",{"data-testid":"g7le-seo-var-add",style:{display:"flex",flexDirection:"column",gap:4,marginTop:6},children:[i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-seo-var-add-name",value:h,placeholder:s("layout_editor.page_settings.seo.vars_add_name"),onChange:_=>v(_.target.value),style:{flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0}}),i.jsxs("button",{type:"button","data-testid":"g7le-seo-var-add-confirm",onClick:w,disabled:h.trim()===""||b||x,style:H0,children:["+ ",s("layout_editor.page_settings.seo.vars_add")]})]}),b?i.jsxs("p",{"data-testid":"g7le-seo-var-add-reserved-warn",style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_name_reserved",{name:h.trim()})]}):null,x&&!b?i.jsxs("p",{"data-testid":"g7le-seo-var-add-duplicate-warn",style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_name_duplicate",{name:h.trim()})]}):null,k?i.jsxs("p",{"data-testid":"g7le-seo-var-add-not-provided-hint",style:{margin:0,fontSize:11,color:"#94a3b8"},children:["ⓘ ",s("layout_editor.page_settings.seo.vars_name_not_provided",{name:h.trim()})]}):null]})]}),i.jsxs("p",{style:{margin:0,fontSize:11,color:"#94a3b8"},children:["ⓘ ",s("layout_editor.page_settings.seo.vars_value_hint")]})]})}const L0={fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"6px 8px"},ws={border:"1px solid #e2e8f0",borderRadius:8,padding:10,display:"flex",flexDirection:"column",gap:6},ks={fontSize:12,fontWeight:600,color:"#475569"},P0={display:"flex",alignItems:"center",gap:6,padding:"2px 0"},Tc={display:"flex",flexDirection:"column",border:"1px solid #f1f5f9",borderRadius:6,padding:8},O0={fontSize:13},Ss={fontSize:12,color:"#0f172a",background:"#f1f5f9",padding:"2px 6px",borderRadius:4},B0={fontSize:11,color:"#64748b"},W0={fontSize:11,color:"#94a3b8"},j0={color:"#dc2626",fontWeight:700},Cs={margin:"4px 0 0",fontSize:11,color:"#b45309"},H0={padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"};function F0({templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s,settingsSignature:a,t:l,debounceMs:d=400,fetchImpl:c,testidPrefix:p="g7le-seo-bot-preview"}){const[f,y]=u.useState(!0),[g,m]=u.useState(null),[h,v]=u.useState(!1),[b,x]=u.useState(null),k=u.useRef(null),w=u.useRef({templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s});w.current={templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s};const _=u.useCallback(async()=>{const T=c??(typeof fetch<"u"?fetch:void 0);if(!T)return;const{templateIdentifier:$,layout:A,url:z,locale:D,seedContext:O,routeParams:M}=w.current;v(!0),x(null);try{const B=`/api/admin/templates/${encodeURIComponent($)}/editor/seo-bot-preview`,N=await T(B,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({layout:A,url:z,locale:D,seed_context:O??{},route_params:M??{}})}),H=await N.json().catch(()=>null);if(!N.ok){x(H?.message??`HTTP ${N.status}`);return}const L=H?.data??null;m(L)}catch(B){x(B instanceof Error?B.message:String(B))}finally{v(!1)}},[c]);u.useEffect(()=>{if(f)return k.current&&clearTimeout(k.current),k.current=setTimeout(()=>{_()},d),()=>{k.current&&clearTimeout(k.current)}},[a,f,d,_]);const C=u.useCallback(()=>{k.current&&clearTimeout(k.current),_()},[_]),E=g?.html??null,S=g?.enabled??!0;return i.jsxs("div",{"data-testid":p,style:K0,children:[i.jsxs("div",{style:V0,children:[i.jsxs("button",{type:"button","data-testid":`${p}-toggle`,onClick:()=>y(T=>!T),"aria-expanded":f,style:U0,children:[f?"▾":"▸"," ",l("layout_editor.page_settings.seo.bot_preview_title")]}),f?i.jsxs("button",{type:"button","data-testid":`${p}-refresh`,onClick:C,disabled:h,style:q0,children:["⟳ ",l("layout_editor.page_settings.seo.bot_preview_refresh")]}):null]}),f&&i.jsxs("div",{style:{marginTop:8},children:[g&&!S?i.jsxs("p",{"data-testid":`${p}-disabled`,style:G0,children:["ⓘ ",l("layout_editor.page_settings.seo.bot_preview_disabled")]}):i.jsx("div",{"data-testid":`${p}-body`,style:Y0,children:h&&!E?i.jsx("p",{style:$s,children:l("layout_editor.translation.loading")}):b?i.jsx("p",{"data-testid":`${p}-error`,style:{...$s,color:"#dc2626"},children:b}):E==null?i.jsx("p",{style:$s,children:l("layout_editor.page_settings.seo.bot_preview_empty")}):i.jsx("pre",{"data-testid":`${p}-code`,style:X0,children:E})}),i.jsxs("p",{"data-testid":`${p}-sample-note`,style:J0,children:["ⓘ ",l("layout_editor.page_settings.seo.bot_preview_sample_note")]})]})]})}const K0={borderTop:"1px solid #e2e8f0",paddingTop:10,marginTop:12},V0={display:"flex",alignItems:"center",gap:8},U0={background:"none",border:"none",cursor:"pointer",fontSize:13,fontWeight:600,color:"#0f172a",padding:0},q0={marginLeft:"auto",padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},G0={margin:0,fontSize:12,color:"#64748b",background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:6,padding:"8px 10px"},Y0={border:"1px solid #e2e8f0",borderRadius:8,background:"#0b1020",padding:0,minHeight:80,overflow:"hidden"},X0={margin:0,padding:12,fontSize:11,lineHeight:1.5,color:"#d1fae5",fontFamily:"monospace",whiteSpace:"pre-wrap",wordBreak:"break-word",maxHeight:360,overflow:"auto"},$s={margin:0,padding:12,fontSize:12,color:"#94a3b8"},J0={margin:"8px 0 0",fontSize:11,color:"#94a3b8"};function Dc({option:e,testIdPrefix:t}){const{id:n,friendly:o,source:r}=e;return i.jsxs("span",{style:Z0,children:[i.jsx("span",{"data-testid":t?`${t}-title`:void 0,style:Q0,children:o??n}),o?i.jsx("span",{"data-testid":t?`${t}-id`:void 0,style:R0,children:n}):null,r?i.jsx("span",{"data-testid":t?`${t}-source`:void 0,style:ew,children:r}):null]})}const Z0={display:"inline-flex",alignItems:"baseline",gap:4,flexWrap:"wrap",minWidth:0},Q0={fontSize:12,color:"#0f172a"},R0={fontSize:10,color:"#94a3b8",fontFamily:"ui-monospace, monospace"},ew={fontSize:9,fontWeight:600,color:"#7c3aed",background:"#f5f3ff",border:"1px solid #ddd6fe",borderRadius:4,padding:"0 5px",whiteSpace:"nowrap"},tw=["always","hourly","daily","weekly","monthly","yearly","never"];function nw({getValue:e,patch:t,t:n,templateIdentifier:o,candidates:r,availableExtensions:s=[],dataSourceOptions:a=[],previewUrl:l="/",previewLocale:d="ko",seedContext:c,candidatesFetcher:p,ogPreviewFetcher:f,botFetchImpl:y,testidPrefix:g="g7le-seo"}){const m=e("meta",{}).seo,h=m&&typeof m=="object"?m:{},v=u.useCallback((K,ne)=>{const me={...e("meta",{})},Ce={...me.seo??{}};ne==null||ne===""?delete Ce[K]:Ce[K]=ne,me.seo=Ce,t("meta",me)},[e,t]),b=h.enabled===!0,x=typeof h.page_type=="string"?h.page_type:"",k=Array.isArray(h.extensions)?h.extensions:[],w=Array.isArray(h.data_sources)?h.data_sources:[],_=k.length>0&&x!=="",[C,E]=u.useState(null),[S,T]=u.useState(null),$=u.useCallback(async(K,ne)=>{if(p)return p(K,ne);if(typeof fetch>"u")return null;const me=new URLSearchParams;K&&me.set("page_type",K),Array.isArray(ne)&&ne.length>0&&me.set("extensions",JSON.stringify(ne.map(qe=>({type:qe.type,id:qe.id}))));const Ce=me.toString()?`?${me.toString()}`:"",ze=await fetch(en(`/api/admin/templates/${encodeURIComponent(o)}/editor/seo-candidates`,"json",null,Ce.replace(/^[?&]+/,"")),{credentials:"same-origin",headers:je()}),Se=await ze.json().catch(()=>null);return ze.ok?Se?.data??null:null},[p,o]),A=u.useMemo(()=>{const me=(e("__editor",{})??{}).original?.meta?.seo;return me&&typeof me=="object"?me:void 0},[e]),z=u.useRef(A);z.current=A;const D=u.useCallback(async K=>{if(f)return f(K);if(typeof fetch>"u")return null;const ne=await fetch(`/api/admin/templates/${encodeURIComponent(o)}/editor/seo-og-preview`,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({seo:K,own_seo:z.current??null,seed_context:c??{},route_params:{}})}),me=await ne.json().catch(()=>null);return ne.ok?me?.data??null:null},[f,o,c]),O=u.useMemo(()=>JSON.stringify(k),[k]),M=u.useRef(h);M.current=h,u.useEffect(()=>{let K=!1;return(async()=>{const ne=await $(x,k);K||E(ne)})(),()=>{K=!0}},[x,O]);const B=u.useMemo(()=>JSON.stringify({og:h.og??null,twitter:h.twitter??null,structured_data:h.structured_data??null}),[h.og,h.twitter,h.structured_data]),N=u.useRef(!1);u.useEffect(()=>{let K=!1;const ne=()=>{(async()=>{const Ce=await D(M.current);K||T(Ce)})()};if(!N.current)return N.current=!0,ne(),()=>{K=!0};const me=setTimeout(ne,180);return()=>{K=!0,clearTimeout(me)}},[x,O,B]);const H=C?.page_types??[],L=C?.toggle_settings??[],I=C?.vars??[],P=u.useMemo(()=>JSON.stringify(h),[h]),F=u.useMemo(()=>e("meta")?{meta:e("meta"),components:e("components",[])}:{meta:{},components:[]},[e]),G=u.useCallback(K=>{k.some(ne=>ne.type===K.type&&ne.id===K.id)||v("extensions",[...k,{type:K.type,id:K.id}])},[k,v]),V=u.useCallback(K=>{const ne=k.filter(me=>!(me.type===K.type&&me.id===K.id));v("extensions",ne.length>0?ne:void 0)},[k,v]),[Y,Z]=u.useState(!1),[se,re]=u.useState(""),U=u.useMemo(()=>{const K=se.trim().toLowerCase();return K?L.filter(ne=>`${ne.ref} ${ne.label??""}`.toLowerCase().includes(K)):L},[L,se]),ee=typeof h.toggle_setting=="string"?h.toggle_setting:"",fe=u.useCallback(K=>{const me=w.includes(K)?w.filter(Ce=>Ce!==K):[...w,K];v("data_sources",me.length>0?me:void 0)},[w,v]);return i.jsxs("div",{"data-testid":g,style:{display:"flex",flexDirection:"column",gap:14},children:[i.jsxs("div",{style:Bt,children:[i.jsx(Dr,{checked:b,onChange:K=>v("enabled",K),testid:`${g}-enabled`,label:n("layout_editor.page_settings.seo.enabled")}),b?null:i.jsxs("p",{"data-testid":`${g}-disabled-note`,style:no,children:["ⓘ ",n("layout_editor.page_settings.seo.disabled_note")]})]}),i.jsx(Nr,{disabled:!b,testid:`${g}-body`,style:{display:"flex",flexDirection:"column",gap:14},children:i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.page_type")}),H.length>0?i.jsxs("select",{"data-testid":`${g}-page-type`,value:x,onChange:K=>v("page_type",K.target.value),style:Lr,children:[i.jsx("option",{value:"",children:n("layout_editor.page_settings.seo.page_type_none")}),H.map(K=>i.jsxs("option",{value:K.value,children:[K.label??K.value,K.owner?.name?` (${K.owner.name})`:""]},K.value))]}):i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text","data-testid":`${g}-page-type-free`,value:x,placeholder:n("layout_editor.page_settings.seo.page_type_free_placeholder"),onChange:K=>v("page_type",K.target.value),style:Lr}),i.jsxs("p",{"data-testid":`${g}-page-type-free-note`,style:no,children:["ⓘ ",n("layout_editor.page_settings.seo.page_type_free_note")]})]})]}),i.jsxs("div",{style:Bt,children:[i.jsxs("label",{style:qt,children:["★ ",n("layout_editor.page_settings.seo.extensions")]}),i.jsxs("div",{"data-testid":`${g}-extensions`,style:{display:"flex",flexWrap:"wrap",gap:6},children:[k.map(K=>i.jsxs("span",{"data-testid":`${g}-extension-${K.type}-${K.id}`,style:Nc,children:[K.name??`${K.type}:${K.id}`,i.jsx("button",{type:"button","data-testid":`${g}-extension-remove-${K.type}-${K.id}`,onClick:()=>V(K),style:zc,children:"✕"})]},`${K.type}:${K.id}`)),i.jsxs("button",{type:"button","data-testid":`${g}-extension-add`,onClick:()=>Z(K=>!K),style:iw,children:["+ ",n("layout_editor.page_settings.seo.add_extension")]})]}),Y&&i.jsx("div",{"data-testid":`${g}-extension-picker`,style:Mc,children:s.length===0?i.jsx("p",{style:no,children:n("layout_editor.page_settings.seo.no_extension_candidate")}):s.map(K=>i.jsx("button",{type:"button","data-testid":`${g}-extension-option-${K.type}-${K.id}`,onClick:()=>{G(K),Z(!1)},style:Lc,children:K.name??`${K.type}:${K.id}`},`${K.type}:${K.id}`))})]}),!_&&i.jsxs("div",{"data-testid":`${g}-defaults-precondition`,style:rw,children:["⚠ ",n("layout_editor.page_settings.seo.defaults_precondition"),i.jsxs("div",{"data-testid":`${g}-defaults-missing`,style:{fontSize:11,marginTop:4},children:[k.length===0?n("layout_editor.page_settings.seo.missing_extensions"):"",x===""?` ${n("layout_editor.page_settings.seo.missing_page_type")}`:""]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.toggle_setting")}),i.jsxs("div",{"data-testid":`${g}-toggle-setting`,children:[ee?i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center",marginBottom:4},children:[i.jsx("code",{style:Nc,children:ee}),i.jsx("button",{type:"button","data-testid":`${g}-toggle-setting-clear`,onClick:()=>v("toggle_setting",void 0),style:zc,children:"✕"})]}):null,i.jsx("input",{type:"text","data-testid":`${g}-toggle-setting-search`,value:se,placeholder:n("layout_editor.page_settings.seo.toggle_setting_search"),onChange:K=>re(K.target.value),style:Lr}),se.trim()!==""&&i.jsx("div",{"data-testid":`${g}-toggle-setting-results`,style:Mc,children:U.length===0?i.jsx("p",{style:no,children:n("layout_editor.inline_binding.no_results")}):U.map(K=>i.jsx("button",{type:"button","data-testid":`${g}-toggle-setting-option-${K.ref}`,onClick:()=>{v("toggle_setting",K.ref),re("")},style:Lc,children:K.label??K.ref},K.ref))})]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.data_sources")}),i.jsx("p",{style:no,children:n("layout_editor.page_settings.seo.data_sources_hint")}),i.jsx("div",{"data-testid":`${g}-data-sources`,style:{display:"flex",flexDirection:"column",gap:6},children:a.length===0?i.jsx("span",{style:no,children:n("layout_editor.page_settings.seo.data_sources_empty")}):a.map(K=>{const ne=w.includes(K.id);return i.jsxs("label",{"data-testid":`${g}-data-source-${K.id}`,style:ow(ne),children:[i.jsx("input",{type:"checkbox","data-testid":`${g}-data-source-check-${K.id}`,checked:ne,onChange:()=>fe(K.id)}),i.jsx(Dc,{option:K,testIdPrefix:`${g}-data-source-${K.id}`})]},K.id)})})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.priority")}),i.jsx("input",{type:"range","data-testid":`${g}-priority`,min:0,max:1,step:.1,value:typeof h.priority=="number"?h.priority:.5,onChange:K=>v("priority",Number(K.target.value))}),i.jsx("span",{"data-testid":`${g}-priority-value`,style:{fontSize:12,color:"#475569"},children:typeof h.priority=="number"?h.priority:.5})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.changefreq")}),i.jsxs("select",{"data-testid":`${g}-changefreq`,value:typeof h.changefreq=="string"?h.changefreq:"",onChange:K=>v("changefreq",K.target.value),style:Lr,children:[i.jsx("option",{value:"",children:n("layout_editor.page_settings.seo.use_default")}),tw.map(K=>i.jsx("option",{value:K,children:K},K))]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.title")}),i.jsx(vt,{value:typeof h.title=="string"?h.title:"",onChange:K=>v("title",K??""),t:n,candidates:r,testidPrefix:`${g}-title`,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.description")}),i.jsx(vt,{value:typeof h.description=="string"?h.description:"",onChange:K=>v("description",K??""),t:n,candidates:r,testidPrefix:`${g}-description`,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),i.jsx(l0,{og:h.og,twitter:h.twitter,onChangeOg:K=>v("og",Object.keys(K).length>0?K:void 0),onChangeTwitter:K=>v("twitter",Object.keys(K).length>0?K:void 0),ogPreview:S?.og,twitterPreview:S?.twitter,defaultsAvailable:S?.defaultsAvailable??!1,candidates:r,t:n}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.structured_data")}),i.jsx(w0,{value:h.structured_data,onChange:K=>v("structured_data",K??void 0),autoBlock:S?.structured?.autoBlock,autoMeta:S?.structured?.autoMeta,lockedByFilter:S?.structured?.lockedByFilter,filteredBlock:S?.structured?.filteredBlock,pageType:x,candidates:r,t:n})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.vars")}),i.jsx(M0,{vars:h.vars,onChange:K=>v("vars",Object.keys(K).length>0?K:void 0),varCandidates:I,gatingMet:_,candidates:r,t:n})]}),i.jsx(F0,{templateIdentifier:o,layout:F,url:l,locale:d,seedContext:c,settingsSignature:P,t:n,fetchImpl:y})]})})]})}const Bt={display:"flex",flexDirection:"column",gap:4},qt={fontSize:12,fontWeight:600,color:"#334155"},Lr={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0,boxSizing:"border-box"},no={margin:"4px 0 0",fontSize:11,color:"#94a3b8"},ow=e=>({display:"flex",alignItems:"center",gap:8,padding:"6px 10px",fontSize:12,border:`1px solid ${e?"#3b82f6":"#e2e8f0"}`,borderRadius:8,background:e?"#eff6ff":"#fff",cursor:"pointer",minWidth:0}),rw={fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"8px 10px"},Nc={display:"inline-flex",alignItems:"center",gap:4,fontSize:11,padding:"3px 8px",borderRadius:999,background:"#eef2ff",color:"#3730a3"},zc={border:"none",background:"transparent",cursor:"pointer",color:"#6366f1",fontSize:11,padding:0},iw={fontSize:11,padding:"3px 10px",borderRadius:999,border:"1px dashed #cbd5e1",background:"#f8fafc",color:"#475569",cursor:"pointer"},Mc={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6,display:"flex",flexDirection:"column",gap:2},Lc={textAlign:"left",border:"none",background:"transparent",cursor:"pointer",padding:"5px 8px",borderRadius:4,fontSize:12,color:"#0f172a"},sw={setState:"state",loadFromLocalStorage:"state",saveToLocalStorage:"state",navigate:"nav",openWindow:"nav",navigateBack:"nav",navigateForward:"nav",replaceUrl:"nav",toast:"notify",openModal:"notify",closeModal:"notify",showAlert:"notify",setError:"notify",refetchDataSource:"data",appendDataSource:"data",updateDataSource:"data",apiCall:"data",scrollIntoView:"etc",login:"etc",logout:"etc",setLocale:"locale",emitEvent:"etc",conditions:"flow",sequence:"flow",parallel:"flow",switch:"flow",suppress:"flow",showErrorPage:"error"},aw={state:"layout_editor.action.group.state",nav:"layout_editor.action.group.nav",notify:"layout_editor.action.group.notify",data:"layout_editor.action.group.data",locale:"layout_editor.action.group.locale",etc:"layout_editor.action.group.etc",flow:"layout_editor.action.group.flow",error:"layout_editor.action.group.error",extension:"layout_editor.action.group.extension"},lw={init:["state","data","notify","nav","locale","etc","flow","error","extension"],component:["nav","notify","data","state","locale","etc","flow","error","extension"]};function dw(e,t){const n=e?.[t];if(n&&typeof n=="object"){const o=n.__source;if(o&&typeof o=="object")return o}return null}function cw(e,t){return!e||e.kind==="core"?t("layout_editor.action.source_core"):e.id??t(`layout_editor.action.source_${e.kind}`)}function Pc(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function uw({recipes:e,t,onAdd:n,context:o="init",testIdPrefix:r="g7le-action-add"}){const[s,a]=u.useState(!1),[l,d]=u.useState(""),c=u.useMemo(()=>wo(e),[e]),p=u.useMemo(()=>{const f=lw[o],y=new Map;for(const m of c){const h=dw(e,m.id),b=h!=null&&h.kind!=="core"?"extension":sw[m.id]??"etc",x=y.get(b)??[];x.push({recipe:m,source:h}),y.set(b,x)}const g=l.trim().toLowerCase();return f.map(m=>{let h=y.get(m)??[];return g&&(h=h.filter(({recipe:v})=>Pc(v.label,t,v.id).toLowerCase().includes(g)||v.id.toLowerCase().includes(g)||v.build.handler.toLowerCase().includes(g))),{groupKey:m,items:h}}).filter(m=>m.items.length>0)},[c,e,o,l,t]);return c.length===0?i.jsx("div",{"data-testid":`${r}-empty`,style:xw,children:t("layout_editor.action.no_recipes")}):i.jsxs("div",{className:r,"data-testid":r,style:{minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":`${r}-toggle`,onClick:()=>a(f=>!f),style:pw,children:[t("layout_editor.action.add_action")," ▾"]}),s?i.jsxs("div",{"data-testid":`${r}-list`,style:fw,children:[i.jsx("input",{type:"text","data-testid":`${r}-search`,value:l,placeholder:t("layout_editor.action.add_search"),onChange:f=>d(f.target.value),style:gw}),p.map(({groupKey:f,items:y})=>i.jsxs("div",{"data-testid":`${r}-group-${f}`,children:[i.jsxs("div",{style:hw,children:["━━ ",t(aw[f]??f)," ━━"]}),y.map(({recipe:g,source:m})=>i.jsxs("button",{type:"button","data-testid":`g7le-init-action-spec-${g.id}`,onClick:()=>{n(xr(g,{})),a(!1),d("")},style:yw,children:[i.jsx("span",{style:bw,children:Pc(g.label,t,g.id)}),i.jsxs("span",{"data-testid":`g7le-init-action-spec-source-${g.id}`,style:mw,children:["〔",cw(m,t),"〕"]}),i.jsx("code",{style:vw,children:g.build.handler})]},g.id))]},f)),p.length===0?i.jsx("div",{"data-testid":`${r}-no-match`,style:_w,children:t("layout_editor.action.add_no_match")}):null]}):null]})}const pw={padding:"6px 12px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},fw={marginTop:6,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff",maxHeight:320,overflowY:"auto",padding:8},gw={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:6,boxSizing:"border-box"},hw={fontSize:11,fontWeight:700,color:"#94a3b8",padding:"6px 4px 2px"},yw={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"6px 8px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:6},bw={flex:1,minWidth:0,color:"#0f172a"},mw={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},vw={fontSize:10,color:"#cbd5e1",fontFamily:"monospace",whiteSpace:"nowrap"},_w={fontSize:12,color:"#94a3b8",padding:"8px 4px"},xw={fontSize:12,color:"#94a3b8",padding:"8px 0"};function ww(e,t){return t.startsWith("$t:")?e(t.slice(3)):t}function kw({action:e,recipe:t,values:n={},t:o,missingRequired:r=!1,showCodeButton:s=!0,testIdPrefix:a="g7le-action-preview"}){const[l,d]=u.useState(!1),c=t?Wd(t,n,p=>ww(o,p)):typeof e.handler=="string"?e.handler:o("layout_editor.action.advanced_action");return i.jsxs("div",{className:a,"data-testid":a,style:Sw,children:[r?i.jsxs("span",{"data-testid":`${a}-missing`,style:$w,children:["⚠ ",o("layout_editor.action.summary_missing_required")]}):i.jsx("span",{"data-testid":`${a}-summary`,style:Cw,children:c}),s?i.jsx("button",{type:"button","data-testid":`${a}-code-toggle`,onClick:()=>d(p=>!p),style:Ew,"aria-expanded":l,"aria-label":o("layout_editor.action.view_code"),children:"</>"}):null,s&&l?i.jsx("pre",{"data-testid":`${a}-code`,style:Iw,children:JSON.stringify(e,null,2)}):null]})}const Sw={display:"flex",flexWrap:"wrap",alignItems:"center",gap:6,minWidth:0},Cw={flex:1,minWidth:0,fontSize:12,color:"#475569"},$w={flex:1,minWidth:0,fontSize:12,color:"#b45309"},Ew={border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px",whiteSpace:"nowrap"},Iw={flexBasis:"100%",margin:"6px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:11,overflow:"auto"},Oc="__g7le_custom__";function Pr({value:e,onChange:t,candidates:n,placeholder:o,testid:r,t:s,bindingCandidates:a}){const l=e==null?"":String(e),d=Array.isArray(n)&&n.length>0,c=d&&n.some(y=>y.value===l),[p,f]=u.useState(!d||l!==""&&!c);return u.useEffect(()=>{l!==""&&d&&!c&&f(!0)},[l,d,c]),d&&!p?i.jsxs("div",{style:Hc,children:[i.jsxs("select",{className:"g7le-widget g7le-widget--picker","data-testid":r,value:c?l:"",onChange:y=>{const g=y.target.value;if(g===Oc){f(!0);return}t(g===""?void 0:g)},style:{...Wc,flex:1,minWidth:0},children:[i.jsx("option",{value:"",children:s("layout_editor.control.default")}),n.map(y=>i.jsx("option",{value:y.value,children:y.label},y.value)),i.jsx("option",{value:Oc,children:s("layout_editor.recipe.custom_input")})]}),i.jsx("button",{type:"button","data-testid":`${r}-to-custom`,title:s("layout_editor.recipe.custom_input"),"aria-label":s("layout_editor.recipe.custom_input"),onClick:()=>f(!0),style:Fc,children:"✎"})]}):i.jsxs("div",{style:Hc,children:[i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:l,onChange:y=>t(y===""?void 0:y),t:s,candidates:a,placeholder:o,testidPrefix:`${r}-input`})}),d&&i.jsx("button",{type:"button","data-testid":`${r}-pick`,title:s("layout_editor.recipe.pick_from_list"),onClick:()=>{f(!1),t(void 0)},style:Fc,children:"≡"})]})}function Aw({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.page_placeholder"),testid:"g7le-widget-page-picker",t:o,bindingCandidates:r})}function Bc({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.datasource_placeholder"),testid:"g7le-widget-datasource-picker",t:o,bindingCandidates:r})}function Es({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.state_key_placeholder"),testid:"g7le-widget-state-key-picker",t:o,bindingCandidates:r})}function Tw({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.modal_placeholder"),testid:"g7le-widget-modal-picker",t:o,bindingCandidates:r})}function Dw({value:e,onChange:t,t:n,bindingCandidates:o}){const r=typeof e=="string"?e:e==null?"":String(e);return i.jsx("div",{className:"g7le-widget g7le-widget--i18n-text","data-testid":"g7le-widget-i18n-text",style:{flex:1,minWidth:100,width:"100%"},children:i.jsx(vt,{value:r,onChange:s=>t(s===""?void 0:s),t:n,placeholder:n("layout_editor.recipe.i18n_text_placeholder"),testidPrefix:"g7le-widget-i18n-text-field",candidates:o,enableExpressionTree:!0,expressionTreeCollapsible:!0})})}function Nw({value:e,onChange:t,candidates:n,t:o}){const r=e==null?"":String(e),s=r.split(",").map(d=>d.trim()).filter(Boolean),a=Array.isArray(n)&&n.length>0,l=d=>{if(!d)return;const c=[...s,d];t(c.join(", "))};return i.jsxs("div",{className:"g7le-widget g7le-widget--binding-list","data-testid":"g7le-widget-binding-list",style:{display:"flex",flexDirection:"column",gap:4,width:"100%"},children:[i.jsx("input",{type:"text","data-testid":"g7le-widget-binding-list-input",value:r,placeholder:o("layout_editor.recipe.datasource_placeholder"),onChange:d=>t(d.target.value===""?void 0:d.target.value),style:jc}),a?i.jsxs("select",{className:"g7le-widget g7le-widget--picker","data-testid":"g7le-widget-binding-list-add",value:"",onChange:d=>{l(d.target.value),d.target.value=""},style:{...Wc,minWidth:0},children:[i.jsx("option",{value:"",children:o("layout_editor.recipe.pick_from_list")}),n.map(d=>i.jsx("option",{value:d.value,children:d.label},d.value))]}):null]})}function zw({value:e,onChange:t}){const n=e==null?"":String(e);return i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--text","data-testid":"g7le-widget-text",value:n,onChange:o=>t(o.target.value===""?void 0:o.target.value),style:jc})}const Wc={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",minWidth:140,width:"100%"},jc={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:100,width:"100%"},Hc={display:"flex",gap:4,alignItems:"flex-start",width:"100%"},Fc={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"};function Mw(e){if(e==null)return"null";if(Array.isArray(e))return"list";switch(typeof e){case"number":return"number";case"boolean":return"boolean";case"string":return"string";default:return"object"}}function Kc(e){switch(e){case"string":return"";case"number":return 0;case"boolean":return!1;case"null":return null;case"list":return[];case"object":return{};default:return null}}function Is(e){return/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e)}function Lw(e,t,n){return t.includes(n)?"self":e.includes(n)?"inherited":"self"}function Pw(e){const t=e.state&&typeof e.state=="object"&&Object.keys(e.state).length>0,n=e.initLocal&&typeof e.initLocal=="object"?e.initLocal:void 0;if(!t)return{initLocal:n??{},migrated:!1};const o={...e.state};if(n)for(const[r,s]of Object.entries(n))o[r]=s;return{initLocal:o,migrated:!0}}const Vc=[{kind:"string",labelKey:"layout_editor.initstate.kind_string"},{kind:"number",labelKey:"layout_editor.initstate.kind_number"},{kind:"boolean",labelKey:"layout_editor.initstate.kind_boolean"},{kind:"null",labelKey:"layout_editor.initstate.kind_null"},{kind:"list",labelKey:"layout_editor.initstate.kind_list"},{kind:"object",labelKey:"layout_editor.initstate.kind_object"}];function Nn({value:e,onChange:t,t:n,path:o,scope:r,depth:s=0,renderStringLeaf:a}){const l=Mw(e),d=c=>{t(Kc(c))};return i.jsxs("div",{className:"g7le-initstate-value",style:{paddingLeft:s>0?12:0,minWidth:0},children:[i.jsxs("div",{style:Bw,children:[i.jsx("select",{"data-testid":`g7le-initstate-type-${o}`,value:l,onChange:c=>d(c.target.value),style:Uc,children:Vc.map(({kind:c,labelKey:p})=>i.jsx("option",{value:c,children:n(p)},c))}),l==="string"?a?i.jsx("div",{style:{flex:1,minWidth:0},children:a({value:String(e??""),onChange:c=>t(c),testidPrefix:`g7le-initstate-value-${o}`})}):i.jsx("input",{type:"text","data-testid":`g7le-initstate-value-${o}`,value:String(e??""),onChange:c=>t(c.target.value),style:qc}):l==="number"?i.jsx("input",{type:"number","data-testid":`g7le-initstate-value-${o}`,value:typeof e=="number"?e:"",onChange:c=>{const p=parseFloat(c.target.value);t(Number.isNaN(p)?0:p)},style:qc}):l==="boolean"?i.jsx(Dr,{checked:!!e,onChange:t,testid:`g7le-initstate-value-${o}`,label:n(e?"layout_editor.overlay.on":"layout_editor.overlay.off")}):l==="null"?i.jsx("span",{"data-testid":`g7le-initstate-value-${o}`,style:Ww,children:"null"}):null]}),l==="list"?i.jsxs("div",{style:Gc,children:[e.map((c,p)=>i.jsxs("div",{style:jw,children:[i.jsxs("span",{style:Hw,children:["[",p,"]"]}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Nn,{value:c,onChange:f=>{const y=e.slice();y[p]=f,t(y)},t:n,path:`${o}.${p}`,scope:r,depth:s+1,renderStringLeaf:a})}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-item-remove-${o}.${p}`,onClick:()=>t(e.filter((f,y)=>y!==p)),style:Yc,children:"✕"})]},p)),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-item-add-${o}`,onClick:()=>t([...e,""]),style:Xc,children:["+ ",n("layout_editor.initstate.add_item")]})]}):null,l==="object"?i.jsx(Ow,{value:e,onChange:t,t:n,path:o,scope:r,depth:s+1,renderStringLeaf:a}):null]})}function Ow({value:e,onChange:t,t:n,path:o,scope:r,depth:s,renderStringLeaf:a}){const[l,d]=u.useState(""),[c,p]=u.useState("string"),[f,y]=u.useState(null),g=Object.entries(e),m=()=>{const h=l.trim();if(!h){y(n("layout_editor.initstate.name_required"));return}if(!Is(h)){y(n("layout_editor.initstate.name_invalid"));return}if(h in e){y(n("layout_editor.initstate.name_duplicate"));return}t({...e,[h]:Kc(c)}),d(""),y(null)};return i.jsxs("div",{style:Gc,children:[g.map(([h,v])=>i.jsxs("div",{style:Fw,children:[i.jsx("code",{style:Kw,children:h}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Nn,{value:v,onChange:b=>t({...e,[h]:b}),t:n,path:`${o}.${h}`,scope:r,depth:s,renderStringLeaf:a})}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-subremove-${o}.${h}`,onClick:()=>{const b={...e};delete b[h],t(b)},style:Yc,children:"✕"})]},h)),i.jsxs("div",{style:Vw,children:[i.jsx("input",{type:"text","data-testid":`g7le-initstate-subkey-input-${o}`,value:l,placeholder:n("layout_editor.initstate.subkey_name"),onChange:h=>{d(h.target.value),f&&y(null)},onKeyDown:h=>{h.key==="Enter"&&(h.preventDefault(),m())},"aria-invalid":f?!0:void 0,style:{...Uw,...f?qw:null}}),i.jsx("select",{"data-testid":`g7le-initstate-subkey-kind-${o}`,value:c,onChange:h=>p(h.target.value),"aria-label":n("layout_editor.initstate.value_kind"),style:Uc,children:Vc.map(({kind:h,labelKey:v})=>i.jsx("option",{value:h,children:n(v)},h))}),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-subkey-add-${o}`,onClick:m,style:Xc,children:["+ ",n("layout_editor.initstate.add_subkey")]})]}),f?i.jsx("p",{"data-testid":`g7le-initstate-subkey-error-${o}`,style:Gw,children:f}):null]})}const Bw={display:"flex",gap:6,alignItems:"center",minWidth:0},Uc={padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,color:"#64748b"},qc={flex:1,minWidth:0,padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},Ww={fontSize:12,color:"#94a3b8",fontFamily:"monospace"},Gc={display:"flex",flexDirection:"column",gap:6,marginTop:6,paddingLeft:8,borderLeft:"2px solid #e2e8f0"},jw={display:"flex",gap:6,alignItems:"flex-start",minWidth:0},Hw={fontSize:11,color:"#94a3b8",minWidth:28,paddingTop:4},Fw={display:"flex",gap:6,alignItems:"flex-start",minWidth:0},Kw={fontSize:11,color:"#475569",fontFamily:"monospace",minWidth:72,paddingTop:4},Yc={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12},Xc={alignSelf:"flex-start",padding:"2px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Vw={display:"flex",gap:6,alignItems:"center"},Uw={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6},qw={border:"1px solid #dc2626"},Gw={margin:"2px 0 0",fontSize:11,color:"#dc2626"},Yw=new Set(["text","data-chip","number"]),Xw=new Set(["select","toggle","page-picker","datasource-picker","state-key-picker","modal-picker","component-target-picker","tag-input","locale-picker","i18n-text","segmented","slider","color","image"]);function Jw(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function Zw(e,t){if(e==="page-picker")return t.pageCandidates;if(e==="datasource-picker")return t.dataSourceCandidates;if(e==="state-key-picker")return t.stateKeyCandidates;if(e==="modal-picker")return t.modalCandidates}function Qw({param:e,value:t,t:n,pools:o,onChange:r,testIdPrefix:s,renderActionList:a,siblingValues:l}){const d=Jw(e.label,n,e.key);if(e.advanced)return i.jsxs("div",{"data-testid":`${s}-locked-${e.key}`,style:lk,children:[i.jsx("span",{style:dk,children:d}),i.jsxs("span",{style:ck,children:["[",n("layout_editor.action.advanced"),"] ",n("layout_editor.init_actions.advanced_param_hint")]})]});if(e.widget==="action-list"&&a){const f=Array.isArray(t)?t.filter(y=>!!y&&typeof y=="object"):[];return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),a(f,y=>r(y.length===0?void 0:y),`${s}-${e.key}`,e.key)]})}if(e.widget==="branch-list"&&a){const f=Array.isArray(t)?t.filter(y=>!!y&&typeof y=="object"):[];return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(Rw,{branches:f,onChange:y=>r(y.length===0?void 0:y),t:n,pools:o,renderActionList:a,testIdPrefix:`${s}-${e.key}`})]})}if(e.widget==="key-value")return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(rk,{value:t,onChange:r,t:n,candidates:o.bindingCandidates,testIdPrefix:`${s}-kv-${e.key}`})]});if(e.widget==="state-key-value")return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(ik,{value:t,onChange:r,t:n,candidates:o.bindingCandidates,stateKeyCandidates:o.stateKeyCandidates,param:e,siblingTarget:l?.target,testIdPrefix:`${s}-skv-${e.key}`})]});if(Yw.has(e.widget??"")||!Xw.has(e.widget??""))return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:Zc,children:[i.jsx("span",{style:oo,children:d}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:typeof t=="string"?t:t==null?"":String(t),onChange:f=>r(f===""?void 0:f),t:n,candidates:o.bindingCandidates,placeholder:e.widget==="number"?n("layout_editor.action_param.number_placeholder"):void 0,testidPrefix:`${s}-chip-${e.key}`})})]});const p=Tn(e.widget);return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:Zc,children:[i.jsx("span",{style:oo,children:d}),i.jsx("div",{style:{flex:1,minWidth:0},children:p?i.jsx(p,{control:{...e},value:t,onChange:r,t:n,candidates:Zw(e.widget,o),bindingCandidates:o.bindingCandidates}):i.jsx(Ve,{value:typeof t=="string"?t:t==null?"":String(t),onChange:f=>r(f===""?void 0:f),t:n,candidates:o.bindingCandidates,testidPrefix:`${s}-chip-${e.key}`})})]})}function Rw({branches:e,onChange:t,t:n,pools:o,renderActionList:r,testIdPrefix:s}){const a=y=>Array.isArray(y)?y.filter(g=>!!g&&typeof g=="object"):y&&typeof y=="object"?[y]:[],l=y=>{if(y.length!==0)return y.length===1?y[0]:y},d=(y,g)=>{const m=e.slice();m[y]=g,t(m)},c=y=>{t(e.filter((g,m)=>m!==y))},p=(y,g)=>{if(g<0||g>=e.length)return;const m=e.slice(),[h]=m.splice(y,1);m.splice(g,0,h),t(m)},f=()=>{t([...e,{if:"",then:void 0}])};return i.jsxs("div",{"data-testid":`${s}-branches`,style:{display:"flex",flexDirection:"column",gap:8},children:[e.map((y,g)=>{const m=typeof y.if=="string"?y.if:"",h=a(y.then);return i.jsxs("div",{"data-testid":`${s}-branch-${g}`,style:ek,children:[i.jsxs("div",{style:tk,children:[i.jsxs("span",{style:nk,children:[n(m?"layout_editor.action_recipe.conditions.branch_when":"layout_editor.action_recipe.conditions.branch_else")," ",g+1]}),i.jsxs("span",{style:{display:"flex",gap:4},children:[i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-up`,onClick:()=>p(g,g-1),disabled:g===0,style:As,title:n("layout_editor.action_recipe.conditions.move_up"),children:"↑"}),i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-down`,onClick:()=>p(g,g+1),disabled:g===e.length-1,style:As,title:n("layout_editor.action_recipe.conditions.move_down"),children:"↓"}),i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-remove`,onClick:()=>c(g),style:As,title:n("layout_editor.action_recipe.conditions.remove_branch"),children:"✕"})]})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:3},children:[i.jsx("span",{style:Jc,children:n("layout_editor.action_recipe.conditions.param_if")}),i.jsx(Ve,{value:m,onChange:v=>d(g,{...y,if:v}),t:n,candidates:o.bindingCandidates,testidPrefix:`${s}-branch-${g}-if`})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:3},children:[i.jsx("span",{style:Jc,children:n("layout_editor.action_recipe.conditions.param_then")}),r(h,v=>d(g,{...y,then:l(v)}),`${s}-branch-${g}-then`)]})]},g)}),i.jsxs("button",{type:"button","data-testid":`${s}-add-branch`,onClick:f,style:ok,children:["+ ",n("layout_editor.action_recipe.conditions.add_branch")]})]})}const ek={border:"1px solid #e2e8f0",borderRadius:6,padding:8,display:"flex",flexDirection:"column",gap:6,background:"#f8fafc"},tk={display:"flex",justifyContent:"space-between",alignItems:"center"},nk={fontSize:12,fontWeight:700,color:"#475569"},Jc={fontSize:11,color:"#64748b"},As={border:"1px solid #cbd5e1",background:"#fff",borderRadius:4,width:22,height:22,cursor:"pointer",fontSize:12,lineHeight:"1",color:"#475569"},ok={border:"1px dashed #94a3b8",background:"#fff",borderRadius:6,padding:"6px 10px",cursor:"pointer",fontSize:12,color:"#475569",alignSelf:"flex-start"};function rk({value:e,onChange:t,t:n,candidates:o,testIdPrefix:r,renderKeyInput:s}){const a=e&&typeof e=="object"&&!Array.isArray(e)?Object.entries(e).map(([l,d])=>({key:l,value:typeof d=="string"?d:d==null?"":String(d)})):[];return i.jsx(zr,{value:a,onChange:l=>{const d={};for(const c of l)c.key.trim()!==""&&(d[c.key]=c.value??"");t(Object.keys(d).length===0?void 0:d)},keyField:"key",valueField:"value",t:n,candidates:o,testidPrefix:r,keyPlaceholder:n("layout_editor.action_param.key_placeholder"),valuePlaceholder:n("layout_editor.action_param.value_placeholder"),addLabel:n("layout_editor.action_param.add_pair"),renderKeyInput:s})}function ik({value:e,onChange:t,t:n,candidates:o,stateKeyCandidates:r,param:s,siblingTarget:a,testIdPrefix:l}){const d=e&&typeof e=="object"&&!Array.isArray(e)?Object.entries(e):[],c=typeof a=="string"?a:"";if((c.startsWith("_local.")||c.startsWith("global.")||c.startsWith("$parent._local.")||c.startsWith("$root._local."))&&d.length===1&&d[0][0]==="value"){const b=d[0][1];return i.jsx("div",{"data-testid":`${l}-path-value`,style:{display:"flex",flexDirection:"column",gap:4},children:i.jsx(Nn,{value:b,onChange:x=>t(x===void 0?void 0:{value:x}),t:n,path:"value",scope:`${l}-path-value-val`,renderStringLeaf:x=>i.jsx(Ve,{value:x.value,onChange:x.onChange,t:n,candidates:o,testidPrefix:x.testidPrefix})})})}const y=(b,x)=>{const k={};for(const[w,_]of d)k[w]=w===b?x:_;t(Object.keys(k).length===0?void 0:k)},g=b=>{const x={};for(const[k,w]of d)k!==b&&(x[k]=w);t(Object.keys(x).length===0?void 0:x)},[m,h]=u.useState(""),v=()=>{const b=m.trim();if(b===""||d.some(([k])=>k===b))return;const x={};for(const[k,w]of d)x[k]=w;x[b]="",t(x),h("")};return i.jsxs("div",{"data-testid":l,style:{display:"flex",flexDirection:"column",gap:6},children:[d.map(([b,x])=>i.jsxs("div",{"data-testid":`${l}-row-${b}`,style:{display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},children:[i.jsxs("div",{style:{flex:1,minWidth:0,display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:{minWidth:0},children:i.jsx(Es,{control:{...s},value:b,onChange:k=>{const w=k==null?"":String(k);if(w===""||w===b||d.some(([C])=>C===w))return;const _={};for(const[C,E]of d)_[C===b?w:C]=E;t(_)},t:n,candidates:r})}),i.jsx("div",{style:{minWidth:0},children:i.jsx(Nn,{value:x,onChange:k=>y(b,k),t:n,path:b,scope:`${l}-val`,renderStringLeaf:k=>i.jsx(Ve,{value:k.value,onChange:k.onChange,t:n,candidates:o,testidPrefix:k.testidPrefix})})})]}),i.jsx("button",{type:"button","data-testid":`${l}-remove-${b}`,onClick:()=>g(b),title:n("layout_editor.page_settings.seo.remove"),"aria-label":n("layout_editor.page_settings.seo.remove"),style:{flex:"0 0 auto",padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]},b)),i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Es,{control:{...s},value:m,onChange:b=>h(b==null?"":String(b)),t:n,candidates:r})}),i.jsxs("button",{type:"button","data-testid":`${l}-add`,onClick:v,style:{flex:"0 0 auto",padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},children:["+ ",n("layout_editor.action_param.add_pair")]})]})]})}function Ts({raw:e,recipe:t,values:n,t:o,pools:r,onChange:s,testIdPrefix:a,renderActionList:l}){const d=(c,p)=>{const f={...n,[c]:p},y=xr(t,f),g=as(e);s(g?jd(y,g):y)};return i.jsx(i.Fragment,{children:t.params.filter(c=>sk(c,n)).map(c=>i.jsx(Qw,{param:c,value:n[c.key],t:o,pools:r,onChange:p=>d(c.key,p),testIdPrefix:a,renderActionList:l,siblingValues:n},c.key))})}function sk(e,t){const n=e.dependsOn;return!n||typeof n!="object"||typeof n.param!="string"?!0:t[n.param]===n.equals}function ak({raw:e,t,onChange:n,testIdPrefix:o,candidates:r}){const[s,a]=u.useState(!1);return i.jsxs("div",{"data-testid":`${o}-if`,style:uk,children:[i.jsxs("button",{type:"button",onClick:()=>a(l=>!l),style:pk,children:["▸ ",t("layout_editor.init_actions.if_label")]}),s?i.jsx("div",{"data-testid":`${o}-if-input`,style:{minWidth:0},children:i.jsx(Ve,{value:as(e)??"",onChange:l=>n(jd(e,l)),t,candidates:r,placeholder:t("layout_editor.init_actions.if_placeholder"),testidPrefix:`${o}-if-chip`})}):null]})}const Zc={display:"flex",alignItems:"center",gap:8,minWidth:0},oo={fontSize:11,color:"#64748b",minWidth:80},lk={display:"flex",alignItems:"center",gap:8,padding:"4px 0"},dk={fontSize:11,color:"#64748b",minWidth:80},ck={fontSize:11,color:"#92400e"},uk={display:"flex",flexDirection:"column",gap:4},pk={textAlign:"left",border:"none",background:"transparent",color:"#475569",cursor:"pointer",fontSize:11,padding:0};function fk(e,t,n){if(!t)return n("layout_editor.action.source_core");const o=e?.[t],r=o&&typeof o=="object"&&o.__source?o.__source:null;return!r||r.kind==="core"?n("layout_editor.action.source_core"):r.id??n(`layout_editor.action.source_${r.kind}`)}function gk(e,t,n){const o=Yn(e,t);if(o.kind==="preset"){const r=t.find(s=>s.id===o.recipeId);return Wd(r??null,o.values,s=>n(s.startsWith("$t:")?s.slice(3):s))}return typeof e.handler=="string"?e.handler:n("layout_editor.action_list.unknown")}function Gt({actions:e,onChange:t,t:n,recipes:o,chipContext:r,actionChipCandidates:s,addContext:a="init",renderAddPicker:l,renderEditor:d,candidatePools:c,showSourceBadge:p=!1,canDrag:f,canDropAt:y,isLocked:g,hideAddPicker:m=!1,testIdPrefix:h="g7le-action-list"}){const v=u.useMemo(()=>wo(o),[o]),b=u.useMemo(()=>{if(r==null||r==="init")return c;const O=jl(r,n,s??null);if(O.length===0)return c;const M=c??{},B=new Set((M.bindingCandidates??[]).map(H=>H.expression)),N=O.filter(H=>!B.has(H.expression));return N.length===0?c:{...M,bindingCandidates:[...N,...M.bindingCandidates??[]]}},[r,s,c,n]),[x,k]=u.useState(null),[w,_]=u.useState(null),C=u.useCallback((O,M)=>{if(O===M||O<0||M<0||O>=e.length||M>=e.length)return;const B=e.slice(),[N]=B.splice(O,1);B.splice(M,0,N),t(B)},[e,t]),E=fs({length:e.length,onMove:C,canDrag:f,canDropAt:y}),S=u.useCallback(O=>{t(e.filter((M,B)=>B!==O))},[e,t]),T=u.useCallback((O,M)=>{const B=e.slice();B[O]=M,t(B)},[e,t]),$=u.useCallback(O=>{t([...e,O])},[e,t]),A=u.useCallback((O,M)=>{const B=Yn(O,v),N=B.kind==="preset"?v.find(L=>L.id===B.recipeId)??null:null;if(!N)return null;const H=B.kind==="preset"?B.values:{};return i.jsxs("div",{style:mk,children:[i.jsx(Ts,{raw:O,recipe:N,values:H,t:n,pools:b??{},onChange:M,testIdPrefix:`${h}-edit`,renderActionList:(L,I,P,F)=>{const G=F==="onSuccess"?"response":F==="onError"?"error":void 0;return i.jsx(Gt,{actions:L,onChange:I,t:n,recipes:o,chipContext:G,actionChipCandidates:G?s:void 0,candidatePools:G?c:b,addContext:a,showSourceBadge:p,testIdPrefix:P})}}),i.jsx(ak,{raw:O,t:n,onChange:M,testIdPrefix:`${h}-edit`,candidates:b?.bindingCandidates})]})},[v,n,b,c,s,h,o,a,p]),z=d??(o?A:void 0),D=m?null:l?l($):o?i.jsx(uw,{recipes:o,t:n,onAdd:$,context:a,testIdPrefix:`${h}-add-picker`}):null;return i.jsxs("div",{className:h,"data-testid":h,children:[i.jsxs("ul",{style:{listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:6},children:[e.map((O,M)=>{const B=Yn(O,v),N=B.kind==="preset"?B.recipeId:null,H=N?v.find(G=>G.id===N)??null:null,L=H===null,I=g?.(M)??!1,P=as(O)!==null,F=z?z(O,G=>T(M,G)):null;return i.jsxs(u.Fragment,{children:[I?null:i.jsx("li",{"aria-hidden":"true",style:{listStyle:"none",margin:0,padding:0},children:i.jsx(Rn,{active:E.isDropTarget(M),testid:`${h}-dropline-${M}`})}),i.jsxs("li",{"data-testid":`${h}-item-${M}`,"data-drag-source":E.dragIndex===M?"true":void 0,style:{border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",...E.dragIndex===M?{opacity:.5}:{}},onDragOver:E.dragging&&!I?G=>{G.preventDefault();const V=G.currentTarget.getBoundingClientRect();E.onDragOverItem(M,G.clientY<V.top+V.height/2?"before":"after")}:void 0,onDrop:E.dragging&&!I?G=>{G.preventDefault(),E.onDrop()}:void 0,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[I?i.jsx("span",{style:{fontSize:12},children:"🔒"}):i.jsx("span",{"data-testid":`${h}-drag-${M}`,style:{color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},draggable:!0,onDragStart:()=>E.onDragStart(M),onDragEnd:E.onDragEnd,"aria-label":n("layout_editor.action_list.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:{flex:1,minWidth:0,fontSize:13},"data-testid":`${h}-summary-${M}`,children:gk(O,v,n)}),p?i.jsxs("span",{"data-testid":`${h}-source-${M}`,style:hk,children:["〔",fk(o,N,n),"〕"]}):null,P?i.jsxs("span",{"data-testid":`${h}-cond-badge-${M}`,style:yk,children:["〔",n("layout_editor.init_actions.cond_badge"),"〕"]}):null,L?i.jsxs("span",{"data-testid":`${h}-advanced-${M}`,style:bk,children:["[",n("layout_editor.action.advanced"),"]"]}):null,!I&&F?i.jsxs("button",{type:"button","data-testid":`${h}-edit-${M}`,onClick:()=>_(w===M?null:M),"aria-label":n("layout_editor.init_actions.edit"),title:n("layout_editor.init_actions.edit"),style:w===M?vk:Qc,children:["✎ ",n("layout_editor.init_actions.edit")]}):null,i.jsx("button",{type:"button","data-testid":`${h}-code-${M}`,onClick:()=>k(x===M?null:M),"aria-label":n("layout_editor.action_list.view_code"),title:n("layout_editor.action_list.view_code"),style:x===M?_k:Ds,children:"</>"}),I?null:i.jsx("button",{type:"button","data-testid":`${h}-remove-${M}`,onClick:()=>S(M),"aria-label":n("layout_editor.action_list.remove"),title:n("layout_editor.action_list.remove"),style:xk,children:"✕"})]}),H?i.jsx(kw,{action:O,recipe:H,values:B.kind==="preset"?B.values:{},t:n,showCodeButton:!1,testIdPrefix:`${h}-preview-${M}`}):null,w===M&&!I&&F?i.jsx("div",{"data-testid":`${h}-edit-body-${M}`,children:F}):null,d&&w!==M?d(O,G=>T(M,G)):null,x===M?i.jsx("pre",{"data-testid":`${h}-code-view-${M}`,style:{margin:"8px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:12,overflow:"auto"},children:JSON.stringify(O,null,2)}):null]})]},M)}),i.jsx("li",{"aria-hidden":"true",style:{listStyle:"none",margin:0,padding:0},children:i.jsx(Rn,{active:E.isDropTarget(e.length),testid:`${h}-dropline-end`})})]}),D?i.jsx("div",{style:{marginTop:8},children:D}):null]})}const hk={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},yk={fontSize:10,color:"#7c3aed",whiteSpace:"nowrap"},bk={fontSize:10,color:"#92400e",whiteSpace:"nowrap"},mk={display:"flex",flexDirection:"column",gap:6,paddingTop:6,borderTop:"1px dashed #e2e8f0",marginTop:6},Qc={flex:"0 0 auto",padding:"3px 8px",fontSize:11,fontWeight:500,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap",lineHeight:1.4},vk={...Qc,background:"#eff6ff",borderColor:"#bfdbfe",color:"#2563eb"},Ds={flex:"0 0 auto",width:26,height:24,display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:11,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",fontFamily:"ui-monospace, monospace"},_k={...Ds,background:"#0f172a",borderColor:"#0f172a",color:"#e2e8f0"},xk={...Ds,color:"#dc2626",borderColor:"#fecaca",fontFamily:"inherit"};function Rc(e){const t=e.__source;return t&&typeof t=="object"?t:null}function wk({actions:e,onChange:t,recipes:n,t:o,bindingCandidates:r,pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,modalCandidates:d}){const c=u.useMemo(()=>({pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,modalCandidates:d,bindingCandidates:r}),[s,a,l,d,r]),p=u.useMemo(()=>e.filter(h=>Rc(h)?.kind==="base"),[e]),f=u.useMemo(()=>e.filter(h=>Rc(h)?.kind!=="base"),[e]),y=p.length>0,g=u.useCallback(h=>{t(y?[...p,...h]:h)},[t,y,p]),m=u.useCallback(()=>{},[]);return i.jsxs("div",{className:"g7le-init-actions-form","data-testid":"g7le-init-actions-form",style:kk,children:[i.jsx("p",{style:Sk,children:o("layout_editor.page_settings.init_actions.heading")}),y?i.jsxs(i.Fragment,{children:[i.jsxs("div",{"data-testid":"g7le-init-action-group-base",style:eu,children:[i.jsx("div",{style:tu,children:o("layout_editor.init_actions.group_base")}),i.jsx(Gt,{actions:p,onChange:m,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,isLocked:()=>!0,hideAddPicker:!0,testIdPrefix:"g7le-init-action-base"}),i.jsxs("p",{"data-testid":"g7le-init-action-base-hint",style:Ck,children:["ⓘ ",o("layout_editor.init_actions.base_readonly_hint")]})]}),i.jsxs("div",{"data-testid":"g7le-init-action-group-self",style:eu,children:[i.jsx("div",{style:tu,children:o("layout_editor.init_actions.group_self")}),f.length===0?i.jsx("p",{style:nu,children:o("layout_editor.init_actions.empty_self")}):null,i.jsx(Gt,{actions:f,onChange:g,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,testIdPrefix:"g7le-init-action-self"})]}),i.jsxs("p",{"data-testid":"g7le-init-action-merge-order",style:$k,children:["ⓘ ",o("layout_editor.init_actions.merge_order")]})]}):i.jsxs(i.Fragment,{children:[e.length===0?i.jsx("p",{"data-testid":"g7le-init-action-empty",style:nu,children:o("layout_editor.init_actions.empty")}):null,i.jsx(Gt,{actions:e,onChange:t,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,testIdPrefix:"g7le-init-action"})]})]})}const kk={display:"flex",flexDirection:"column",gap:12,minWidth:0},Sk={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},eu={border:"1px solid #e2e8f0",borderRadius:10,padding:10,display:"flex",flexDirection:"column",gap:8},tu={fontSize:12,fontWeight:700,color:"#475569"},Ck={margin:0,fontSize:11,color:"#64748b"},nu={margin:0,fontSize:12,color:"#94a3b8"},$k={margin:0,fontSize:11,color:"#64748b"};function Ek(e){return e==="spinner"?new Set(["spinner","page"]):new Set(["skeleton"])}function ou(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function ru({candidates:e,value:t,onSelect:n,styleKind:o,t:r,testIdPrefix:s="g7le-loading-component-picker"}){const a=yo(),l=()=>{const d=a.open({ariaLabel:r("layout_editor.overlay.loading_picker.select"),width:520,maxHeightRatio:.7,content:i.jsx(Ik,{candidates:e,value:t,styleKind:o,t:r,testIdPrefix:s,onPick:c=>{n(c),a.close(d)},onClose:()=>a.close(d)})})};return i.jsx("div",{className:s,"data-testid":s,style:{minWidth:0},children:i.jsxs("div",{style:Ak,children:[i.jsx("span",{"data-testid":`${s}-current`,style:Tk,children:t&&t.length>0?t:r("layout_editor.overlay.loading_picker.none")}),i.jsxs("button",{type:"button","data-testid":`${s}-open`,onClick:l,style:Dk,children:["🔍 ",r("layout_editor.overlay.loading_picker.select")]})]})})}function Ik({candidates:e,value:t,styleKind:n,t:o,testIdPrefix:r,onPick:s,onClose:a}){const[l,d]=u.useState(""),c=Ek(n),p=u.useMemo(()=>{const f=l.trim().toLowerCase();return e.filter(y=>c.has(y.role)).filter(y=>f?ou(y.label,o,y.name).toLowerCase().includes(f)||y.name.toLowerCase().includes(f):!0)},[e,n,l,o]);return i.jsxs("div",{"data-testid":`${r}-modal`,style:Nk,children:[i.jsxs("div",{style:zk,children:[i.jsxs("span",{style:Mk,children:["🔍 ",o("layout_editor.overlay.loading_picker.select")]}),i.jsx("button",{type:"button","data-testid":`${r}-close`,onClick:a,"aria-label":o("layout_editor.overlay.loading_picker.select"),style:Lk,children:"✕"})]}),i.jsx("input",{type:"text","data-testid":`${r}-search`,value:l,placeholder:o("layout_editor.overlay.loading_picker.search"),onChange:f=>d(f.target.value),style:Pk,autoFocus:!0}),p.length===0?i.jsx("p",{"data-testid":`${r}-empty`,style:Kk,children:o("layout_editor.overlay.loading_picker.empty")}):i.jsx("ul",{style:Ok,children:p.map(f=>i.jsx("li",{children:i.jsxs("button",{type:"button","data-testid":`${r}-item-${f.name}`,onClick:()=>s(f.name),style:{...Bk,...t===f.name?Wk:{}},children:[i.jsx("span",{style:jk,children:ou(f.label,o,f.name)}),i.jsx("code",{style:Hk,children:f.name}),i.jsx("span",{style:Fk,children:f.role})]})},f.name))})]})}const Ak={display:"flex",alignItems:"center",gap:6,minWidth:0},Tk={flex:1,minWidth:0,fontSize:12,color:"#0f172a",fontFamily:"monospace"},Dk={padding:"4px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Nk={display:"flex",flexDirection:"column",minHeight:0,padding:16,boxSizing:"border-box",fontFamily:"system-ui, -apple-system, sans-serif"},zk={display:"flex",alignItems:"center",gap:8,paddingBottom:8,borderBottom:"1px solid #e2e8f0",marginBottom:10},Mk={flex:1,fontSize:14,fontWeight:700,color:"#0f172a",minWidth:0},Lk={border:"none",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:15,lineHeight:1,padding:4},Pk={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:8,boxSizing:"border-box"},Ok={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:2,overflowY:"auto",minHeight:0},Bk={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"8px 10px",fontSize:12,border:"1px solid transparent",background:"transparent",cursor:"pointer",borderRadius:6},Wk={border:"1px solid #3b82f6",background:"#eff6ff"},jk={flex:1,minWidth:0,color:"#0f172a"},Hk={fontSize:10,color:"#94a3b8",fontFamily:"monospace"},Fk={fontSize:10,color:"#64748b",background:"#f1f5f9",borderRadius:4,padding:"0 4px"},Kk={margin:0,fontSize:12,color:"#94a3b8",padding:"8px 4px"};function iu(e){return e===!0?{enabled:!0}:e===!1||e===void 0?{}:e}const Vk=["opaque","blur","fade","skeleton","spinner"];function Uk({value:e,patch:t,t:n,baseValue:o,ownValue:r,loadingComponents:s=[],progressiveDataSources:a=[],bindingCandidates:l}){const d=iu(e),c=d.enabled??!1,p=!!o&&Object.keys(o).length>0,f=Tn("component-target-picker"),[y,g]=u.useState((d.wait_for??[]).length>0),m=u.useCallback(w=>{const _={...iu(e)};w(_),t(_)},[e,t]),h=r&&typeof r=="object"?r:e&&typeof e=="object"?e:{},v=u.useCallback(w=>p&&o!==void 0&&w in o&&!(w in h),[p,o,h]),b=u.useCallback(w=>{o&&m(_=>{_[w]=o[w]})},[o,m]),x=u.useCallback(w=>m(_=>{_.enabled=w}),[m]),k=d.target!==void 0?"region":"full";return i.jsxs("div",{className:"g7le-overlay-form","data-testid":"g7le-overlay-form",style:qk,children:[p?i.jsxs("p",{"data-testid":"g7le-overlay-inherit-banner",style:Gk,children:["ⓘ ",n("layout_editor.overlay.inherit_banner")]}):null,i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.enabled_label"),inherited:v("enabled"),hasInheritance:p,onOverride:()=>b("enabled"),testidKey:"enabled",t:n}),i.jsx(Nr,{disabled:v("enabled"),children:i.jsx(Dr,{checked:c,onChange:x,testid:"g7le-overlay-enabled",ariaLabel:n("layout_editor.overlay.enabled_label"),label:n(c?"layout_editor.overlay.on":"layout_editor.overlay.off")})}),i.jsxs("p",{style:Wr,children:["ⓘ ",n("layout_editor.overlay.enabled_hint")]})]}),i.jsx(Nr,{disabled:!c,testid:"g7le-overlay-body",style:{display:"flex",flexDirection:"column",gap:16,minWidth:0},children:i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.scope_label"),inherited:v("target"),hasInheritance:p,onOverride:()=>b("target"),testidKey:"target",t:n}),i.jsxs("div",{"data-testid":"g7le-overlay-scope",style:zs,children:[i.jsx(Or,{active:k==="full",onClick:()=>m(w=>{delete w.target,delete w.fallback_target}),testid:"g7le-overlay-scope-full",children:n("layout_editor.overlay.scope_full")}),i.jsx(Or,{active:k==="region",onClick:()=>m(w=>{w.target=w.target??""}),testid:"g7le-overlay-scope-region",children:n("layout_editor.overlay.scope_region")})]}),i.jsx(Nr,{disabled:k!=="region",testid:"g7le-overlay-region-box",style:Zk,children:i.jsxs(i.Fragment,{children:[i.jsx(zn,{label:n("layout_editor.overlay.target_label"),children:i.jsx("div",{"data-testid":"g7le-overlay-target-picker",children:f?i.jsx(f,{control:{label:n("layout_editor.overlay.target_label")},value:d.target??"",onChange:w=>m(_=>{_.target=typeof w=="string"?w:""}),t:n}):i.jsx("input",{type:"text","data-testid":"g7le-overlay-target-input",value:d.target??"",onChange:w=>m(_=>{_.target=w.target.value}),style:e1})})}),i.jsx(zn,{label:n("layout_editor.overlay.fallback_label"),children:i.jsx("div",{"data-testid":"g7le-overlay-fallback-picker",children:f?i.jsx(f,{control:{label:n("layout_editor.overlay.fallback_label")},value:d.fallback_target??"",onChange:w=>m(_=>{const C=typeof w=="string"?w:"";C===""?delete _.fallback_target:_.fallback_target=C}),t:n}):null})}),i.jsxs("p",{style:Wr,children:["ⓘ ",n("layout_editor.overlay.fallback_hint")]})]})})]}),i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.style_label"),inherited:v("style"),hasInheritance:p,onOverride:()=>b("style"),testidKey:"style",t:n}),i.jsx("div",{"data-testid":"g7le-overlay-style",style:zs,children:Vk.map(w=>i.jsx(Or,{active:(d.style??"opaque")===w,onClick:()=>m(_=>{_.style=w}),testid:`g7le-overlay-style-${w}`,children:n(`layout_editor.overlay.style_${w}`)},w))}),d.style==="skeleton"?i.jsxs("div",{style:su,children:[i.jsx(zn,{label:n("layout_editor.overlay.skeleton_component"),children:i.jsx(ru,{candidates:s,value:d.skeleton?.component,styleKind:"skeleton",onSelect:w=>m(_=>{_.skeleton={..._.skeleton,component:w}}),t:n,testIdPrefix:"g7le-overlay-skeleton-component"})}),i.jsx(zn,{label:n("layout_editor.overlay.skeleton_animation"),children:i.jsx("div",{"data-testid":"g7le-overlay-skeleton-anim",style:zs,children:["wave","pulse","none"].map(w=>i.jsx(Or,{active:(d.skeleton?.animation??"wave")===w,onClick:()=>m(_=>{_.skeleton={..._.skeleton,animation:w}}),testid:`g7le-overlay-skeleton-anim-${w}`,children:n(`layout_editor.overlay.anim_${w}`)},w))})}),i.jsx(zn,{label:n("layout_editor.overlay.skeleton_count"),children:i.jsx("input",{type:"number","data-testid":"g7le-overlay-skeleton-count",value:d.skeleton?.iteration_count??"",onChange:w=>m(_=>{const C=parseInt(w.target.value,10);_.skeleton={..._.skeleton,iteration_count:Number.isNaN(C)?void 0:C}}),style:t1})})]}):null,d.style==="spinner"?i.jsxs("div",{style:su,children:[i.jsx(zn,{label:n("layout_editor.overlay.spinner_component"),children:i.jsx(ru,{candidates:s,value:d.spinner?.component,styleKind:"spinner",onSelect:w=>m(_=>{_.spinner={..._.spinner,component:w}}),t:n,testIdPrefix:"g7le-overlay-spinner-component"})}),i.jsx(zn,{label:n("layout_editor.overlay.spinner_text"),children:i.jsx("div",{"data-testid":"g7le-overlay-spinner-text",style:{minWidth:0},children:i.jsx(vt,{value:d.spinner?.text??"",onChange:w=>m(_=>{_.spinner={..._.spinner,text:w===void 0||w===""?void 0:w}}),t:n,candidates:l,enableExpressionTree:!0,expressionTreeCollapsible:!0,testidPrefix:"g7le-overlay-spinner-text-field"})})})]}):null]}),i.jsxs("div",{style:Br,children:[i.jsxs("button",{type:"button","data-testid":"g7le-overlay-waitfor-toggle","aria-expanded":y,onClick:()=>g(w=>!w),style:r1,children:[y?"▾":"▸"," ",n("layout_editor.overlay.waitfor_label")]}),y?i.jsxs("div",{"data-testid":"g7le-overlay-waitfor-body",style:i1,children:[i.jsxs("p",{"data-testid":"g7le-overlay-waitfor-hint",style:Wr,children:["ⓘ ",n("layout_editor.overlay.waitfor_hint")]}),a.length===0?i.jsx("p",{"data-testid":"g7le-overlay-waitfor-empty",style:Wr,children:n("layout_editor.overlay.waitfor_empty")}):i.jsx("div",{"data-testid":"g7le-overlay-waitfor",style:{display:"flex",flexWrap:"wrap",gap:6},children:a.map(w=>{const _=(d.wait_for??[]).includes(w.id);return i.jsxs("label",{"data-testid":`g7le-overlay-waitfor-${w.id}`,style:n1(_),children:[i.jsx("input",{type:"checkbox",checked:_,onChange:()=>m(C=>{const E=new Set(C.wait_for??[]);E.has(w.id)?E.delete(w.id):E.add(w.id);const S=Array.from(E);S.length===0?delete C.wait_for:C.wait_for=S})}),i.jsx(Dc,{option:w,testIdPrefix:`g7le-overlay-waitfor-${w.id}`})]},w.id)})})]}):null]}),p?i.jsxs("button",{type:"button","data-testid":"g7le-overlay-reset-inherit",onClick:()=>t(void 0),style:o1,children:["⟲ ",n("layout_editor.overlay.reset_inherit")]}):null]})})]})}function Or({active:e,onClick:t,children:n,testid:o}){return i.jsx("button",{type:"button","data-testid":o,"aria-pressed":e,onClick:t,style:{...Xk,...e?Jk:{}},children:n})}function Ns({label:e,inherited:t,hasInheritance:n,onOverride:o,testidKey:r,t:s}){return i.jsxs("div",{style:s1,children:[i.jsx("label",{style:Yk,children:e}),n?t?i.jsxs(i.Fragment,{children:[i.jsxs("span",{"data-testid":`g7le-overlay-inherited-${r}`,style:a1,children:["〔",s("layout_editor.overlay.inherited_badge"),"〕"]}),i.jsx("button",{type:"button","data-testid":`g7le-overlay-override-${r}`,onClick:o,style:d1,children:s("layout_editor.overlay.override_field")})]}):i.jsxs("span",{"data-testid":`g7le-overlay-overridden-${r}`,style:l1,children:["〔",s("layout_editor.overlay.overridden_badge"),"〕"]}):null]})}function zn({label:e,children:t}){return i.jsxs("div",{style:Qk,children:[i.jsx("span",{style:Rk,children:e}),i.jsx("div",{style:{flex:1,minWidth:0},children:t})]})}const qk={display:"flex",flexDirection:"column",gap:16,minWidth:0},Gk={margin:0,fontSize:12,color:"#1d4ed8",background:"#eff6ff",padding:"8px 10px",borderRadius:6},Br={display:"flex",flexDirection:"column",gap:6,minWidth:0},Yk={fontSize:13,fontWeight:600,color:"#0f172a"},Wr={margin:0,fontSize:11,color:"#94a3b8"},zs={display:"flex",gap:4,flexWrap:"wrap"},Xk={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Jk={border:"1px solid #3b82f6",background:"#eff6ff",color:"#1d4ed8"},Zk={display:"flex",flexDirection:"column",gap:8,padding:8,border:"1px dashed #e2e8f0",borderRadius:8},su={display:"flex",flexDirection:"column",gap:8,padding:8,border:"1px dashed #e2e8f0",borderRadius:8},Qk={display:"flex",alignItems:"center",gap:8,minWidth:0},Rk={fontSize:11,color:"#64748b",minWidth:90},e1={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},t1={width:80,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},n1=e=>({display:"inline-flex",alignItems:"center",gap:4,padding:"4px 8px",fontSize:12,border:`1px solid ${e?"#3b82f6":"#cbd5e1"}`,borderRadius:12,background:e?"#eff6ff":"#fff",cursor:"pointer"}),o1={alignSelf:"flex-start",padding:"6px 12px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},r1={alignSelf:"flex-start",padding:"4px 0",fontSize:13,fontWeight:600,color:"#0f172a",background:"transparent",border:"none",cursor:"pointer"},i1={display:"flex",flexDirection:"column",gap:6,paddingLeft:14,minWidth:0},s1={display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",minWidth:0},a1={fontSize:11,color:"#64748b",background:"#f1f5f9",borderRadius:4,padding:"1px 6px"},l1={fontSize:11,color:"#0369a1",background:"#e0f2fe",borderRadius:4,padding:"1px 6px"},d1={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},c1="g7le:computed-effect-preview",u1={number:"layout_editor.computed.type_number",string:"layout_editor.computed.type_string",boolean:"layout_editor.computed.type_boolean",list:"layout_editor.computed.type_list",object:"layout_editor.computed.type_object",null:"layout_editor.computed.type_null"};function p1(e,t,n){if(t==="list"){const o=e,r=o.slice(0,2).map(s=>JSON.stringify(s)).join(", ");return o.length<=2?`[${r}]`:`[${r}, ${n("layout_editor.computed.list_more",{count:o.length})}]`}return t==="object"?JSON.stringify(e):t==="boolean"?e?"true":"false":t==="null"?n("layout_editor.computed.value_none"):String(e)}function au({expr:e,computedKey:t,sampleContext:n,t:o,isEffect:r=!1,testIdPrefix:s="g7le-computed-preview"}){const a=Id(e,n),l=()=>{typeof window>"u"||!t||window.dispatchEvent(new CustomEvent(c1,{detail:{key:t}}))};return a.ok?i.jsxs("div",{className:s,"data-testid":s,style:f1,children:[i.jsx("span",{style:g1,children:o("layout_editor.computed.preview_label")}),i.jsx("span",{"data-testid":`${s}-value`,style:h1,children:p1(a.value,a.type,o)}),i.jsxs("span",{"data-testid":`${s}-type`,style:y1,children:["(",o(u1[a.type]),")"]}),r&&a.type==="boolean"?i.jsxs("button",{type:"button","data-testid":`${s}-effect`,onClick:l,style:b1,children:["▷ ",o("layout_editor.computed.effect_preview")]}):null]}):i.jsxs("div",{"data-testid":`${s}-error`,style:m1,children:["⚠ ",o("layout_editor.computed.preview_error")]})}const f1={display:"flex",alignItems:"center",flexWrap:"wrap",gap:6,minWidth:0},g1={fontSize:11,color:"#94a3b8"},h1={fontSize:12,color:"#0f172a",fontFamily:"monospace",wordBreak:"break-all"},y1={fontSize:10,color:"#64748b"},b1={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},m1={fontSize:12,color:"#b45309"},v1=[{op:"count",labelKey:"layout_editor.computed.op_count"},{op:"sum",labelKey:"layout_editor.computed.op_sum"},{op:"filter",labelKey:"layout_editor.computed.op_filter"},{op:"toOptions",labelKey:"layout_editor.computed.op_to_options"},{op:"nth",labelKey:"layout_editor.computed.op_nth"},{op:"firstOf",labelKey:"layout_editor.computed.op_first_of"},{op:"literal",labelKey:"layout_editor.computed.op_literal"}],_1=["=","!=",">","<",">=","<=","includes"],x1=new Set(["count","sum","filter"]);function lu({model:e,onChange:t,t:n,dataSourceCandidates:o,bindingCandidates:r,existingKeys:s=[]}){const a=u.useCallback(g=>{const m={...e};g(m),t(m)},[e,t]),l=e.conditions??[],d=Tn("datasource-picker"),c=e.key!==""&&s.includes(e.key),p=()=>a(g=>{g.conditions=[...g.conditions??[],{field:"",cmp:"=",value:""}]}),f=(g,m)=>a(h=>{const v=(h.conditions??[]).slice(),b={...v[g]};m(b),v[g]=b,h.conditions=v}),y=g=>a(m=>{m.conditions=(m.conditions??[]).filter((h,v)=>v!==g)});return i.jsxs("div",{className:"g7le-computed-custom","data-testid":"g7le-computed-custom",style:w1,children:[i.jsx(Ms,{label:`① ${n("layout_editor.computed.custom_step1")}`,children:i.jsx("div",{"data-testid":"g7le-computed-custom-source",children:d?i.jsx(d,{control:{},value:e.source,onChange:g=>a(m=>{m.source=typeof g=="string"?g:void 0}),t:n,candidates:o}):i.jsx("input",{type:"text",value:e.source??"",onChange:g=>a(m=>{m.source=g.target.value}),style:ro})})}),i.jsxs(Ms,{label:`② ${n("layout_editor.computed.custom_step2")}`,children:[i.jsx("select",{"data-testid":"g7le-computed-custom-op",value:e.op,onChange:g=>a(m=>{m.op=g.target.value}),style:ro,children:v1.map(({op:g,labelKey:m})=>i.jsx("option",{value:g,children:n(m)},g))}),e.op==="sum"?i.jsx("div",{"data-testid":"g7le-computed-custom-sumfield",style:{minWidth:0},children:i.jsx(Ve,{value:e.sumField??"",onChange:g=>a(m=>{m.sumField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_sum_field"),testidPrefix:"g7le-computed-custom-sumfield-chip"})}):null,e.op==="toOptions"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("div",{"data-testid":"g7le-computed-custom-valuefield",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.valueField??"",onChange:g=>a(m=>{m.valueField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_value_field"),testidPrefix:"g7le-computed-custom-valuefield-chip"})}),i.jsx("div",{"data-testid":"g7le-computed-custom-labelfield",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.labelField??"",onChange:g=>a(m=>{m.labelField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_label_field"),testidPrefix:"g7le-computed-custom-labelfield-chip"})})]}):null,e.op==="nth"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-index",value:e.index??"",placeholder:"0",onChange:g=>a(m=>{m.index=g.target.value}),style:ro}),i.jsx("div",{"data-testid":"g7le-computed-custom-prop",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.prop??"",onChange:g=>a(m=>{m.prop=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_prop"),testidPrefix:"g7le-computed-custom-prop-chip"})})]}):null,e.op==="literal"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsxs("select",{"data-testid":"g7le-computed-custom-literalkind",value:e.literalKind??"string",onChange:g=>a(m=>{m.literalKind=g.target.value}),style:ro,children:[i.jsx("option",{value:"string",children:n("layout_editor.computed.kind_string")}),i.jsx("option",{value:"number",children:n("layout_editor.computed.kind_number")}),i.jsx("option",{value:"boolean",children:n("layout_editor.computed.kind_boolean")})]}),i.jsx("div",{"data-testid":"g7le-computed-custom-literalvalue",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.literalValue??"",onChange:g=>a(m=>{m.literalValue=g}),t:n,candidates:r,testidPrefix:"g7le-computed-custom-literalvalue-chip"})})]}):null,e.op==="firstOf"?i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-candidates",value:(e.candidates??[]).join(", "),placeholder:n("layout_editor.computed.custom_candidates"),onChange:g=>a(m=>{m.candidates=g.target.value.split(",").map(h=>h.trim()).filter(Boolean)}),style:ro}),i.jsx("div",{"data-testid":"g7le-computed-custom-fallback",style:{minWidth:0},children:i.jsx(Ve,{value:e.fallback??"",onChange:g=>a(m=>{m.fallback=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_fallback"),testidPrefix:"g7le-computed-custom-fallback-chip"})})]}):null,x1.has(e.op)?i.jsxs("div",{style:$1,children:[l.map((g,m)=>i.jsxs("div",{"data-testid":`g7le-computed-custom-cond-${m}`,style:E1,children:[i.jsx("div",{"data-testid":`g7le-computed-custom-cond-field-${m}`,style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:g.field,onChange:h=>f(m,v=>{v.field=h}),t:n,candidates:r,placeholder:n("layout_editor.computed.cond_field"),testidPrefix:`g7le-computed-custom-cond-field-${m}-chip`})}),i.jsx("select",{value:g.cmp,onChange:h=>f(m,v=>{v.cmp=h.target.value}),style:I1,children:_1.map(h=>i.jsx("option",{value:h,children:h},h))}),i.jsx("div",{"data-testid":`g7le-computed-custom-cond-value-${m}`,style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:g.value,onChange:h=>f(m,v=>{v.value=h}),t:n,candidates:r,placeholder:n("layout_editor.computed.cond_value"),testidPrefix:`g7le-computed-custom-cond-value-${m}-chip`})}),i.jsx("button",{type:"button","data-testid":`g7le-computed-custom-cond-remove-${m}`,onClick:()=>y(m),style:A1,children:"✕"})]},m)),i.jsxs("button",{type:"button","data-testid":"g7le-computed-custom-cond-add",onClick:p,style:T1,children:["+ ",n("layout_editor.computed.cond_add")]})]}):null]}),i.jsxs(Ms,{label:`③ ${n("layout_editor.computed.custom_step3")}`,children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-key",value:e.key,onChange:g=>a(m=>{m.key=g.target.value}),style:{...ro,...c?C1:{}}}),c?i.jsxs("p",{"data-testid":"g7le-computed-custom-key-dup",style:D1,children:["⚠ ",n("layout_editor.computed.key_duplicate")]}):null]})]})}function Ms({label:e,children:t}){return i.jsxs("div",{style:k1,children:[i.jsx("div",{style:S1,children:e}),i.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6,minWidth:0},children:t})]})}function Ls(e){return Vm(e)}const w1={display:"flex",flexDirection:"column",gap:12,padding:10,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff"},k1={display:"flex",flexDirection:"column",gap:4,minWidth:0},S1={fontSize:12,fontWeight:600,color:"#475569"},ro={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%",boxSizing:"border-box"},C1={border:"1px solid #ef4444"},$1={display:"flex",flexDirection:"column",gap:6,marginTop:4},E1={display:"flex",gap:4,alignItems:"center",minWidth:0},I1={padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},A1={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12},T1={alignSelf:"flex-start",padding:"2px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},D1={margin:0,fontSize:11,color:"#b45309"};function Ps(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function N1(e){if(e&&typeof e=="object"){const t=e.__source;if(t&&typeof t=="object")return t}return null}function z1({computed:e,onChange:t,recipes:n,t:o,sampleContext:r,dataSourceCandidates:s,bindingCandidates:a,computedSource:l={},effectKeys:d=[],onRevert:c}){const p=u.useMemo(()=>xd(n),[n]),[f,y]=u.useState(!1),[g,m]=u.useState(null),h=Object.keys(e),v=u.useCallback((_,C)=>{t({...e,[_]:C})},[e,t]),b=u.useCallback(_=>{const C={...e};delete C[_],t(C)},[e,t]),x=u.useCallback((_,C,E)=>{const S={...e};_&&_!==C&&delete S[_],S[C]=E,t(S)},[e,t]),k=u.useCallback(_=>{const C=kd(_,{});let E=_.id,S=1;for(;E in e;)E=`${_.id}${++S}`;t({...e,[E]:C}),y(!1)},[e,t]),w=u.useCallback(()=>{!g||!g.key||(t({...e,[g.key]:Ls(g)}),m(null),y(!1))},[g,e,t]);return i.jsxs("div",{className:"g7le-computed-form","data-testid":"g7le-computed-form",style:O1,children:[i.jsx("p",{style:B1,children:o("layout_editor.page_settings.computed.heading")}),h.length>0?h.map(_=>i.jsx(M1,{computedKey:_,expr:e[_],recipes:p,rawRecipes:n,t:o,sampleContext:r,dataSourceCandidates:s,bindingCandidates:a,source:l[_],isEffect:d.includes(_),existingKeys:h,onPatchExpr:C=>v(_,C),onRename:(C,E)=>x(_,C,E),onRemove:()=>b(_),onRevert:c?()=>c(_):void 0},_)):i.jsx("p",{"data-testid":"g7le-computed-empty",style:aS,children:o("layout_editor.computed.empty")}),i.jsxs("div",{style:{minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":"g7le-computed-add",onClick:()=>y(_=>!_),style:R1,children:["+ ",o("layout_editor.computed.add")," ▾"]}),f?i.jsxs("div",{"data-testid":"g7le-computed-add-list",style:eS,children:[["common","more"].map(_=>{const C=p.filter(E=>(E.group??"more")===_);return C.length===0?null:i.jsxs("div",{children:[i.jsxs("div",{style:tS,children:["━━ ",o(`layout_editor.computed.group_${_}`)," ━━"]}),C.map(E=>{const S=N1(n?.[E.id]);return i.jsxs("button",{type:"button","data-testid":`g7le-computed-preset-${E.id}`,onClick:()=>k(E),style:nS,children:[i.jsx("span",{style:oS,children:Ps(E.label,o,E.id)}),S&&S.kind!=="core"?i.jsxs("span",{"data-testid":`g7le-computed-preset-source-${E.id}`,style:rS,children:["〔",S.id??S.kind,"〕"]}):null]},E.id)})]},_)}),i.jsxs("div",{style:{borderTop:"1px solid #e2e8f0",marginTop:6,paddingTop:6},children:[i.jsxs("button",{type:"button","data-testid":"g7le-computed-custom-open",onClick:()=>m(g?null:{key:"",op:"count",source:"",conditions:[]}),style:iS,children:["⚙ ",o("layout_editor.computed.custom_open")," ▾"]}),g?i.jsxs("div",{style:{marginTop:6},children:[i.jsx(lu,{model:g,onChange:m,t:o,dataSourceCandidates:s,bindingCandidates:a,existingKeys:h}),i.jsx("div",{"data-testid":"g7le-computed-custom-preview",style:{marginTop:6},children:i.jsx(au,{expr:Ls(g),sampleContext:r,t:o,testIdPrefix:"g7le-computed-custom-preview-eval"})}),i.jsx("button",{type:"button","data-testid":"g7le-computed-custom-commit",disabled:!g.key||h.includes(g.key),onClick:w,style:sS,children:o("layout_editor.computed.custom_add")})]}):null]})]}):null]})]})}function M1({computedKey:e,expr:t,recipes:n,rawRecipes:o,t:r,sampleContext:s,dataSourceCandidates:a,bindingCandidates:l,source:d,isEffect:c,existingKeys:p,onPatchExpr:f,onRename:y,onRemove:g,onRevert:m}){const[h,v]=u.useState(!1),[b,x]=u.useState(!1),k=u.useMemo(()=>Ed(t,n),[t,n]),w=d==="base",_=d==="route-override",C=k.kind==="advanced",E=k.kind==="preset"?n.find($=>$.id===k.recipeId)??null:null,S=E?Ps(E.label,r,e):e,T=($,A)=>{if(k.kind!=="preset"||!E)return;const z={...k.params,[$]:A};f(kd(E,z))};return i.jsxs("div",{"data-testid":`g7le-computed-item-${e}`,style:W1,children:[i.jsxs("div",{style:j1,children:[i.jsx("span",{style:H1,children:S}),i.jsx("code",{style:F1,children:e}),w?i.jsxs("span",{"data-testid":`g7le-computed-source-${e}`,style:K1,children:["〔",r("layout_editor.computed.source_base"),"〕"]}):null,_?i.jsxs("span",{"data-testid":`g7le-computed-overridden-${e}`,style:V1,children:["〔",r("layout_editor.computed.source_overridden"),"〕"]}):null,C?i.jsxs("span",{"data-testid":"g7le-computed-advanced",style:U1,children:["[",r("layout_editor.action.advanced"),"]"]}):null,i.jsx("span",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-computed-code-${e}`,onClick:()=>x($=>!$),style:Os,"aria-label":r("layout_editor.value_tree.show_source"),title:r("layout_editor.value_tree.show_source"),children:"</>"}),C?null:i.jsx("button",{type:"button","data-testid":`g7le-computed-edit-${e}`,onClick:()=>v($=>!$),style:Os,children:r("layout_editor.init_actions.edit")}),i.jsx("button",{type:"button","data-testid":`g7le-computed-remove-${e}`,onClick:g,style:Os,"aria-label":r("layout_editor.init_actions.remove"),children:"✕"})]}),b?i.jsx("pre",{"data-testid":`g7le-computed-code-block-${e}`,style:lS,children:t}):null,i.jsx("div",{"data-testid":`g7le-computed-preview-${e}`,children:i.jsx(au,{expr:t,computedKey:e,sampleContext:s,t:r,isEffect:c,testIdPrefix:`g7le-computed-preview-${e}-eval`})}),_&&m?i.jsxs("div",{style:q1,children:[i.jsxs("span",{style:G1,children:["ⓘ ",r("layout_editor.computed.overridden_hint")]}),i.jsx("button",{type:"button","data-testid":`g7le-computed-revert-${e}`,onClick:m,style:Y1,children:r("layout_editor.computed.revert")})]}):null,h&&!C?i.jsxs("div",{style:X1,children:[w?i.jsxs("p",{"data-testid":`g7le-computed-override-notice-${e}`,style:J1,children:["⚠ ",r("layout_editor.computed.override_notice")]}):null,k.kind==="preset"&&E?i.jsx(i.Fragment,{children:E.params.map($=>i.jsx(P1,{param:$,value:k.params[$.key]??"",t:r,dataSourceCandidates:a,bindingCandidates:l,onChange:A=>T($.key,A)},$.key))}):k.kind==="custom"?i.jsx(lu,{model:{...k.model,key:e},onChange:$=>y($.key,Ls($)),t:r,dataSourceCandidates:a,bindingCandidates:l,existingKeys:p.filter($=>$!==e)}):null]}):null]})}const L1=new Set(["datasource-picker","binding-picker","select","toggle","state-key-picker","page-picker"]);function P1({param:e,value:t,t:n,dataSourceCandidates:o,bindingCandidates:r,onChange:s}){const a=Ps(e.label,n,e.key),l=e.widget??"",c=L1.has(l)?Tn(e.widget):null,p=l==="datasource-picker"||l==="binding-picker"?o:void 0;return i.jsxs("div",{"data-testid":`g7le-computed-param-${e.key}`,style:Z1,children:[i.jsx("span",{style:Q1,children:a}),i.jsx("div",{style:{flex:1,minWidth:0},children:c?i.jsx(c,{control:{...e},value:t,onChange:f=>s(typeof f=="string"?f:""),t:n,candidates:p}):i.jsx(Ve,{value:t,onChange:f=>s(f),t:n,candidates:r,testidPrefix:`g7le-computed-param-${e.key}-chip`})})]})}const O1={display:"flex",flexDirection:"column",gap:12,minWidth:0},B1={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},W1={border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",display:"flex",flexDirection:"column",gap:6,background:"#f8fafc"},j1={display:"flex",alignItems:"center",gap:6},H1={fontSize:13,fontWeight:600,color:"#0f172a"},F1={fontSize:10,color:"#64748b",fontFamily:"monospace"},K1={fontSize:10,color:"#0891b2",whiteSpace:"nowrap"},V1={fontSize:10,color:"#7c3aed",whiteSpace:"nowrap"},U1={fontSize:10,color:"#92400e",whiteSpace:"nowrap"},q1={display:"flex",alignItems:"center",gap:8,marginTop:4,minWidth:0},G1={fontSize:11,color:"#7c3aed",flex:1,minWidth:0},Y1={border:"1px solid #c4b5fd",borderRadius:6,background:"#f5f3ff",color:"#6d28d9",cursor:"pointer",fontSize:11,padding:"2px 8px",whiteSpace:"nowrap"},Os={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px"},X1={display:"flex",flexDirection:"column",gap:6,paddingTop:6,borderTop:"1px dashed #e2e8f0"},J1={margin:0,fontSize:11,color:"#b45309",background:"#fef3c7",padding:"6px 8px",borderRadius:6},Z1={display:"flex",alignItems:"center",gap:8,minWidth:0},Q1={fontSize:11,color:"#64748b",minWidth:90},R1={padding:"6px 12px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},eS={marginTop:6,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff",padding:8,maxHeight:380,overflowY:"auto"},tS={fontSize:11,fontWeight:700,color:"#94a3b8",padding:"6px 4px 2px"},nS={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"6px 8px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:6},oS={flex:1,minWidth:0,color:"#0f172a"},rS={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},iS={padding:"6px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},sS={marginTop:6,padding:"6px 12px",fontSize:12,border:"1px solid #3b82f6",borderRadius:6,background:"#3b82f6",color:"#fff",cursor:"pointer"},aS={margin:0,fontSize:12,color:"#94a3b8"},lS={margin:"2px 0 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"};function dS(e,t){return t==="any"?!0:e===null||typeof e!="object"?!1:t==="object"?!Array.isArray(e):!0}function Bs({value:e,onChange:t,onValidityChange:n,t:o,shape:r="any",emptyValue:s=void 0,placeholder:a,minHeight:l=80,testidPrefix:d="g7le-json-block",shapeErrorKey:c,invalidErrorKey:p,ariaLabel:f,id:y,validate:g}){const[m,h]=u.useState(()=>jr(e)),[v,b]=u.useState(null),x=u.useRef(!1),k=u.useRef(e);u.useEffect(()=>{if(!x.current&&!Object.is(e,k.current)){if(jr(e)===jr(k.current)){k.current=e;return}h(jr(e)),b(null),k.current=e,n?.(!0)}},[e]);const w=_=>{if(x.current=!0,h(_),_.trim()===""){b(null),k.current=s,n?.(!0),t(s);return}let C;try{C=JSON.parse(_)}catch{b(o(p??"layout_editor.json_block.invalid")),n?.(!1);return}if(!dS(C,r)){b(o(c??"layout_editor.json_block.invalid")),n?.(!1);return}const E=g?.(C)??null;if(E){b(E),n?.(!1);return}b(null),k.current=C,n?.(!0),t(C)};return i.jsxs("div",{className:"g7le-json-block",style:cS,children:[i.jsx("textarea",{id:y,"data-testid":d,style:{...uS,minHeight:l},value:m,spellCheck:!1,"aria-label":f,"aria-invalid":v?!0:void 0,placeholder:a,onChange:_=>w(_.target.value),onBlur:()=>{x.current=!1}}),v?i.jsx("div",{"data-testid":`${d}-error`,style:pS,children:v}):null]})}function jr(e){if(e===void 0)return"";try{return JSON.stringify(e,null,2)??""}catch{return""}}const cS={display:"flex",flexDirection:"column",gap:4,minWidth:0},uS={width:"100%",boxSizing:"border-box",fontFamily:"ui-monospace, SFMono-Regular, Menlo, monospace",fontSize:12,lineHeight:1.5,color:"#0f172a",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,padding:"6px 8px",resize:"vertical"},pS={fontSize:11,color:"#dc2626"},fS=[{kind:"string",labelKey:"layout_editor.initstate.kind_string"},{kind:"number",labelKey:"layout_editor.initstate.kind_number"},{kind:"boolean",labelKey:"layout_editor.initstate.kind_boolean"},{kind:"null",labelKey:"layout_editor.initstate.kind_null"},{kind:"list",labelKey:"layout_editor.initstate.kind_list"},{kind:"object",labelKey:"layout_editor.initstate.kind_object"}],Ws={local:"initLocal",global:"initGlobal",isolated:"initIsolated"};function gS({raw:e,own:t,patch:n,t:o,isolatedOrphan:r={}}){const s=u.useMemo(()=>{const f=t?.state&&typeof t.state=="object"?t.state:void 0,y=e?.state&&typeof e.state=="object"?e.state:void 0;if(!(!f&&!y))return{...y??{},...f??{}}},[t,e]),{initLocal:a,migrated:l}=u.useMemo(()=>Pw({initLocal:e?.initLocal,state:s}),[e,s]),d=f=>{if(f==="local")return a;const y=e?.[Ws[f]];return y&&typeof y=="object"?y:{}},c=f=>{if(f==="local"){const y=t?.initLocal??{},g=s?Object.keys(s):[];return Array.from(new Set([...Object.keys(y),...g]))}return Object.keys(t?.[Ws[f]]??{})},p=u.useCallback((f,y)=>{const g=Ws[f];f==="local"&&l&&n("state",void 0),n(g,Object.keys(y).length===0?void 0:y)},[n,l]);return i.jsxs("div",{className:"g7le-initstate-form","data-testid":"g7le-initstate-form",style:yS,children:[i.jsx("p",{style:bS,children:o("layout_editor.page_settings.initstate.heading")}),l?i.jsxs("p",{"data-testid":"g7le-initstate-legacy-state",style:mS,children:["ⓘ ",o("layout_editor.initstate.legacy_state")]}):null,i.jsx(js,{scope:"local",label:o("layout_editor.initstate.section_local"),value:d("local"),ownKeys:c("local"),patchSection:p,t:o}),i.jsx(js,{scope:"global",label:o("layout_editor.initstate.section_global"),value:d("global"),ownKeys:c("global"),patchSection:p,t:o}),i.jsx(js,{scope:"isolated",label:o("layout_editor.initstate.section_isolated"),description:o("layout_editor.initstate.section_isolated_desc"),value:d("isolated"),ownKeys:c("isolated"),patchSection:p,t:o,orphan:r}),i.jsxs("p",{style:PS,children:["ⓘ ",o("layout_editor.initstate.isolated_dynamic_hint")]})]})}function js({scope:e,label:t,description:n,value:o,ownKeys:r,patchSection:s,t:a,orphan:l={}}){const[d,c]=u.useState(""),[p,f]=u.useState("string"),[y,g]=u.useState(null),[m,h]=u.useState(!1),v=Object.keys(o),b=v,x=(_,C)=>{s(e,{...o,[_]:C})},k=_=>{const C={...o};delete C[_],s(e,C)},w=()=>{const _=d.trim();if(!_){g(a("layout_editor.initstate.name_required"));return}if(!Is(_)){g(a("layout_editor.initstate.name_invalid"));return}if(_ in o){g(a("layout_editor.initstate.name_duplicate"));return}const C={string:"",number:0,boolean:!1,null:null,list:[],object:{}};s(e,{...o,[_]:C[p]}),c(""),g(null)};return i.jsxs("div",{"data-testid":`g7le-initstate-section-${e}`,style:vS,children:[i.jsxs("div",{style:_S,children:[i.jsx("div",{style:xS,children:t}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-${e}-mode-toggle`,onClick:()=>h(_=>!_),style:kS,children:a(m?"layout_editor.json_block.mode_block":"layout_editor.json_block.mode_code")})]}),n?i.jsxs("p",{style:wS,children:["ⓘ ",n]}):null,m?i.jsx(Bs,{value:o,shape:"object",emptyValue:{},onChange:_=>s(e,_??{}),t:a,testidPrefix:`g7le-initstate-${e}-code`,placeholder:`{ +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.editMode,selectedRoute:{path:`__iteration__/${t.sourcePath}`,layoutName:t.hostLayout??e.selectedRoute?.layoutName??null}};case"EXIT_ITERATION_ITEM_EDIT":return{...e,editMode:e.returnEditMode??"route",selectedRoute:e.returnRoute,returnRoute:null,returnEditMode:null};case"SET_PREVIEW_DEVICE":return{...e,previewDevice:t.device};case"SET_PREVIEW_ZOOM":return{...e,previewZoom:t.zoom};case"SET_PREVIEW_CUSTOM_WIDTH":return{...e,previewCustomWidth:t.width};case"SET_PREVIEW_COLOR_SCHEME":return{...e,previewColorScheme:t.scheme};case"TOGGLE_PALETTE":return{...e,isPaletteOpen:!e.isPaletteOpen};case"SET_PALETTE_OPEN":return{...e,isPaletteOpen:t.open};case"SET_AVAILABLE_STATES":return{...e,availableStates:t.states,activeStateId:t.activeStateId};case"SET_ACTIVE_STATE":return{...e,activeStateId:t.activeStateId};case"SET_LAYOUT_VERSIONS":return{...e,layoutVersions:t.versions};case"SET_LAYOUT_VERSION":return{...e,layoutVersions:{...e.layoutVersions,[t.layoutName]:t.version}};case"SET_EXTENSION_VERSIONS":return{...e,extensionVersions:t.versions};case"SET_EXTENSION_VERSION":return{...e,extensionVersions:{...e.extensionVersions,[t.extensionId]:t.version}};default:return e}}function sy(e,t){const n=iy(e,t);return((n.selectedRoute?.path??null)!==(e.selectedRoute?.path??null)||n.editMode!==e.editMode)&&(n.availableStates.length>0||n.activeStateId!==null)?{...n,availableStates:[],activeStateId:null}:n}const Il=u.createContext(null);function ay(e,t){const n=sy(e,t);return Sl(n,t.type),n}function ly({templateIdentifier:e,initialLocale:t,children:n}){const[o,r]=u.useReducer(ay,void 0,()=>{const a=ry(e,t);return Sl(a,void 0),a});u.useEffect(()=>{oy(o.isRouteTreeCollapsed)},[o.isRouteTreeCollapsed]);const s=u.useMemo(()=>({state:o,dispatch:r}),[o]);return u.createElement(Il.Provider,{value:s},n)}function Je(){const e=u.useContext(Il);if(!e)throw new Error("useLayoutEditor must be used within LayoutEditorProvider");return e}const Al=u.createContext(null);function dy({value:e,children:t}){return u.createElement(Al.Provider,{value:e},t)}function Ft(){return u.useContext(Al)}const zi=u.createContext(null);let cy=1;function uy({children:e,t}){const[n,o]=u.useState([]),r=u.useRef([]),[s,a]=u.useState({}),l=u.useCallback(b=>{const x=b.id??`editor-modal-${cy++}`,k={id:x,content:b.content,ariaLabel:b.ariaLabel??"",closeOnBackdrop:b.closeOnBackdrop??!0,closeOnEscape:b.closeOnEscape??!0,width:b.width??"auto",height:b.height??"auto",maxHeightRatio:b.maxHeightRatio??.85,draggable:b.draggable??!1,avoidRect:b.avoidRect,onClose:b.onClose};return o(w=>{const C=[...w.filter(E=>E.id!==x),k];return r.current=C,C}),x},[]),d=u.useCallback(b=>{o(x=>{const k=x.find(_=>_.id===b),w=x.filter(_=>_.id!==b);return r.current=w,k?.onClose&&queueMicrotask(()=>k.onClose?.()),w})},[]),c=u.useCallback(()=>{const b=r.current[r.current.length-1];b&&d(b.id)},[d]),p=u.useCallback(()=>{o(b=>{for(let x=b.length-1;x>=0;x--){const k=b[x];k?.onClose&&queueMicrotask(()=>k.onClose?.())}return r.current=[],[]})},[]),f=u.useCallback((b,x)=>{a(k=>{const w=r.current.find(C=>C.id===b),_=x??w?.ariaLabel??"";return{...k,[b]:_}})},[]),y=u.useCallback(b=>{a(x=>{if(!(b in x))return x;const k={...x};return delete k[b],k})},[]),g=u.useCallback(b=>{y(b),d(b)},[y,d]),m=u.useCallback(()=>{a({}),p()},[p]),h=u.useMemo(()=>Object.keys(s),[s]),v=u.useMemo(()=>({open:l,close:g,closeTop:c,closeAll:m,stack:n,minimize:f,restore:y,minimizedStack:h}),[l,g,c,m,n,f,y,h]);return u.createElement(zi.Provider,{value:v},e,u.createElement(fy,{minimized:s,onRestore:y,t,key:"__min_bar"}))}function yo(){const e=u.useContext(zi);if(!e)throw new Error("useEditorModal must be used within EditorModalProvider");return e}function py(){const e=u.useContext(zi);if(!e)return null;const{stack:t,close:n,minimizedStack:o}=e,r=u.useMemo(()=>new Set(o),[o]);return u.useEffect(()=>{if(t.length===0)return;const s=a=>{if(a.key!=="Escape"&&a.key!=="Esc")return;const l=t.filter(c=>!r.has(c.id)),d=l[l.length-1];d?.closeOnEscape&&(a.preventDefault(),a.stopPropagation(),n(d.id))};return window.addEventListener("keydown",s,!0),()=>window.removeEventListener("keydown",s,!0)},[t,n,r]),t.length===0?null:i.jsx(i.Fragment,{children:t.map((s,a)=>i.jsx(hy,{entry:s,depth:a,hidden:r.has(s.id),onClose:()=>n(s.id)},s.id))})}function fy({minimized:e,onRestore:t,t:n}){const o=n??(s=>s),r=Object.keys(e);return r.length===0?null:i.jsx("div",{className:"g7le-minimized-bar","data-testid":"g7le-minimized-bar",style:{position:"fixed",left:16,bottom:16,display:"flex",gap:8,zIndex:9300},children:r.map(s=>i.jsxs("button",{type:"button","data-testid":`g7le-minimized-${s}`,onClick:()=>t(s),style:{display:"flex",alignItems:"center",gap:6,padding:"8px 12px",background:"#1e293b",color:"#fff",border:"none",borderRadius:8,boxShadow:"0 8px 24px rgba(15, 23, 42, 0.3)",cursor:"pointer",fontSize:13},children:[i.jsx("span",{"aria-hidden":"true",children:"🗗"}),i.jsx("span",{children:e[s]||o("layout_editor.modal.minimized_default")}),i.jsx("span",{style:{opacity:.7},children:o("layout_editor.modal.restore")})]},s))})}function gy(e,t,n){if(!e||typeof window>"u")return null;const o=window.innerWidth,r=window.innerHeight,s=16,a=e.left,l=o-(e.left+e.width),d=t+s;let c;if(l>=a&&l>=d)c=e.left+e.width+s;else if(a>=d)c=Math.max(s,e.left-s-t);else return null;c=Math.min(Math.max(s,c),Math.max(s,o-t-s));let p=e.top;return p=Math.min(Math.max(s,p),Math.max(s,r-n-s)),{left:c,top:p}}const Tl="clamp(16px, 6vh, 56px)";function hy({entry:e,depth:t,hidden:n,onClose:o}){const r=9100+t*10,s=e.maxHeightRatio>0?`${e.maxHeightRatio*100}vh`:void 0,a=e.draggable?s:`calc(100vh - (${Tl}) * 2)`,[l,d]=u.useState(null),c=u.useRef(null),p=u.useRef(null);u.useEffect(()=>{if(!e.draggable)return;const v=c.current;if(!v)return;const b=v.getBoundingClientRect(),x=gy(e.avoidRect,b.width,b.height);x&&d(x)},[e.id,e.draggable]);const f=u.useCallback(v=>{if(!e.draggable)return;const b=v.target;if(!b.closest("[data-modal-drag-handle]")||b.closest("button, a, input, textarea, select"))return;const k=c.current,w=k?.getBoundingClientRect(),_=l?.left??w?.left??0,C=l?.top??w?.top??0;p.current={startX:v.clientX,startY:v.clientY,baseLeft:_,baseTop:C};const E=T=>{const $=p.current;if(!$)return;const A=window.innerWidth,z=window.innerHeight,D=k?.offsetWidth??0,O=k?.offsetHeight??0;let M=$.baseLeft+(T.clientX-$.startX),B=$.baseTop+(T.clientY-$.startY);M=Math.min(Math.max(0,M),Math.max(0,A-D)),B=Math.min(Math.max(0,B),Math.max(0,z-O)),d({left:M,top:B})},S=()=>{p.current=null,window.removeEventListener("pointermove",E),window.removeEventListener("pointerup",S)};window.addEventListener("pointermove",E),window.addEventListener("pointerup",S)},[e.draggable,l]),y=v=>{e.closeOnBackdrop&&v.target===v.currentTarget&&o()},g=e.draggable&&l!==null,m=e.draggable?{position:"fixed",inset:0,background:"transparent",pointerEvents:"none",zIndex:r,...n?{display:"none"}:{}}:{position:"fixed",inset:0,background:"rgba(15, 23, 42, 0.45)",display:n?"none":"flex",alignItems:"flex-start",justifyContent:"center",paddingTop:Tl,boxSizing:"border-box",zIndex:r},h={background:"#fff",borderRadius:10,boxShadow:"0 20px 60px rgba(15, 23, 42, 0.25)",width:e.width,height:e.height,maxHeight:a,maxWidth:"92vw",overflow:"hidden",display:"flex",flexDirection:"column",...e.draggable?{position:"absolute",pointerEvents:"auto",left:g?l.left:"50%",top:g?l.top:"50%",transform:g?void 0:"translate(-50%, -50%)"}:{}};return i.jsx("div",{className:"g7le-modal-backdrop",role:"presentation","data-testid":`g7le-modal-backdrop-${e.id}`,"data-draggable":e.draggable?"true":"false",onClick:y,style:m,children:i.jsx("div",{ref:c,className:"g7le-modal",role:"dialog","aria-modal":"true","aria-label":e.ariaLabel||void 0,"data-testid":`g7le-modal-${e.id}`,"data-positioned":g?"true":"false",onClick:v=>v.stopPropagation(),onPointerDown:e.draggable?f:void 0,style:h,children:e.content})})}function wt({baseStyle:e,hoverStyle:t,disabled:n,style:o,children:r,...s}){const[a,l]=u.useState(!1),d={outline:"none",...e,...o,...a&&!n?t:{}};return i.jsx("button",{type:"button",disabled:n,onMouseEnter:()=>l(!0),onMouseLeave:()=>l(!1),style:d,...s,children:r})}function yy({templateIdentifier:e,templateName:t,templateVersion:n,templateList:o,onSwitch:r}){const{t:s}=tt(),[a,l]=u.useState(!1),[d,c]=u.useState(!1),p=u.useRef(null);u.useEffect(()=>{if(!a)return;const v=x=>{p.current&&!p.current.contains(x.target)&&l(!1)},b=x=>{x.key==="Escape"&&l(!1)};return document.addEventListener("mousedown",v),document.addEventListener("keydown",b),()=>{document.removeEventListener("mousedown",v),document.removeEventListener("keydown",b)}},[a]);const f=o??[],y=f.some(v=>v.identifier!==e),g=!!r&&y,m=t||e,h=v=>{if(v===e){l(!1);return}l(!1),r?.(v)};return i.jsxs("div",{ref:p,className:"g7le-toolbar__template-switcher",style:{position:"relative",display:"inline-flex"},children:[i.jsxs("button",{type:"button",className:"g7le-toolbar__template","data-testid":"g7le-toolbar-template-label","data-can-switch":g?"true":"false","aria-haspopup":g?"listbox":void 0,"aria-expanded":g?a:void 0,disabled:!g,onClick:()=>g&&l(v=>!v),onMouseEnter:()=>c(!0),onMouseLeave:()=>c(!1),title:g?s("layout_editor.chrome.toolbar.switch_template"):m,style:{display:"inline-flex",alignItems:"center",gap:6,padding:"4px 8px",fontSize:13,fontWeight:600,color:"#0f172a",letterSpacing:.1,borderRadius:6,borderWidth:1,borderStyle:"solid",borderColor:g&&(d||a)?"#cbd5e1":"transparent",backgroundColor:g&&(d||a)?"#f1f5f9":"transparent",cursor:g?"pointer":"default",outline:"none",transition:"background-color 120ms, border-color 120ms"},children:[i.jsx("span",{className:"g7le-toolbar__template-name","data-testid":"g7le-toolbar-template-name",children:m}),n&&i.jsxs("span",{className:"g7le-toolbar__template-version","data-testid":"g7le-toolbar-template-version",style:{fontSize:11,fontWeight:500,color:"#64748b",padding:"1px 6px",borderRadius:999,background:"#f1f5f9",border:"1px solid #e2e8f0"},children:["v",n]}),g&&i.jsx("span",{className:"g7le-toolbar__template-caret","aria-hidden":"true",style:{fontSize:9,color:"#94a3b8",marginLeft:1},children:"▼"})]}),g&&a&&i.jsx("div",{className:"g7le-toolbar__template-menu","data-testid":"g7le-toolbar-template-menu",role:"listbox",style:{position:"absolute",top:"calc(100% + 4px)",left:0,minWidth:240,maxWidth:360,maxHeight:360,overflowY:"auto",background:"#ffffff",border:"1px solid #e2e8f0",borderRadius:8,boxShadow:"0 8px 24px rgba(15, 23, 42, 0.14)",zIndex:30,padding:4},children:f.map(v=>{const b=v.identifier===e;return i.jsxs("button",{type:"button",role:"option","aria-selected":b,"aria-current":b?"true":void 0,className:"g7le-toolbar__template-menu-item","data-testid":"g7le-toolbar-template-menu-item","data-identifier":v.identifier,"data-current":b?"true":"false",onClick:()=>h(v.identifier),style:{display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,width:"100%",padding:"8px 10px",fontSize:13,color:"#1e293b",background:b?"#eff6ff":"transparent",border:"none",borderRadius:6,cursor:b?"default":"pointer",textAlign:"left",outline:"none"},onMouseEnter:x=>{b||(x.currentTarget.style.background="#f1f5f9")},onMouseLeave:x=>{x.currentTarget.style.background=b?"#eff6ff":"transparent"},children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:8,minWidth:0},children:[i.jsx("span",{"aria-hidden":"true",style:{width:12,flex:"0 0 auto",color:"#2563eb",fontSize:12,textAlign:"center"},children:b?"✓":""}),i.jsxs("span",{style:{display:"flex",flexDirection:"column",gap:2,minWidth:0},children:[i.jsx("span",{style:{fontWeight:600,color:b?"#1d4ed8":"#1e293b",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:v.name||v.identifier}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:v.identifier})]})]}),i.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:6,flex:"0 0 auto"},children:[i.jsx("span",{style:{fontSize:10,fontWeight:600,color:v.type==="admin"?"#7c3aed":"#0891b2",textTransform:"uppercase",letterSpacing:.4},children:v.type}),v.version&&i.jsxs("span",{style:{fontSize:11,color:"#64748b"},children:["v",v.version]})]})]},v.identifier)})})]})}function by(e={}){const{onSave:t,isDirty:n=!1,onExit:o,onEditCode:r,onManageImages:s,onManageTranslations:a,onPageSettings:l,onReset:d,onShowShortcuts:c,onPreview:p,onShowVersions:f,templateName:y,templateVersion:g,templateList:m,onSwitchTemplate:h}=e,[v,b]=u.useState(!1),[x,k]=u.useState(!1),{t:w}=tt(),{state:_,dispatch:C}=Je(),[E,S]=u.useState(null);u.useEffect(()=>{const I=()=>{const F=window.__g7LayoutEditorHistory;S(F?{undo:F.undo,redo:F.redo,canUndo:!!F.canUndo,canRedo:!!F.canRedo}:null)};I();const P=window.setInterval(I,250);return()=>window.clearInterval(P)},[]);const T=()=>{C({type:"TOGGLE_ROUTE_TREE"})},$=()=>_.editMode==="base"?w("layout_editor.chrome.toolbar.mode_badge.base"):_.editMode==="modal"?w("layout_editor.chrome.toolbar.mode_badge.modal"):_.editMode==="extension"?w("layout_editor.chrome.toolbar.mode_badge.extension"):_.editMode==="iteration_item"?w("layout_editor.chrome.toolbar.mode_badge.iteration_item"):null,A=()=>{_.editMode==="base"?C({type:"EXIT_BASE_EDIT"}):_.editMode==="modal"?C({type:"EXIT_MODAL_EDIT"}):_.editMode==="extension"?C({type:"EXIT_EXTENSION_EDIT"}):_.editMode==="iteration_item"&&C({type:"EXIT_ITERATION_ITEM_EDIT"})},z=$(),D=_.editMode!=="route",O={display:"inline-flex",alignItems:"center",gap:6,padding:"6px 12px",fontSize:13,fontWeight:500,borderRadius:6,borderWidth:1,borderStyle:"solid",borderColor:"transparent",backgroundColor:"transparent",color:"#475569",cursor:"pointer",transition:"background-color 120ms, color 120ms, border-color 120ms"},M={...O,backgroundColor:"#2563eb",color:"#ffffff",borderColor:"#2563eb",fontWeight:600},B={...O,borderColor:"#e2e8f0",backgroundColor:"#ffffff",color:"#0f172a"},N={backgroundColor:"#f1f5f9",color:"#0f172a",borderColor:"#cbd5e1"},H={backgroundColor:"#1d4ed8",borderColor:"#1d4ed8"},L={backgroundColor:"#f8fafc",borderColor:"#cbd5e1"};return i.jsxs("div",{className:"g7le-toolbar","data-testid":"g7le-toolbar",role:"toolbar",style:{display:"flex",alignItems:"center",gap:6,padding:"8px 12px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",boxShadow:"0 1px 2px rgba(15, 23, 42, 0.04)"},children:[i.jsx("style",{children:"@keyframes g7le-spin { to { transform: rotate(360deg); } }.g7le-toolbar > * { flex-shrink: 0; white-space: nowrap; }"}),i.jsx("button",{type:"button",className:"g7le-toolbar__route-tree-toggle","data-testid":"g7le-toolbar-toggle-route-tree","aria-label":w("layout_editor.chrome.toolbar.toggle_route_tree"),title:w("layout_editor.chrome.toolbar.toggle_route_tree"),onClick:T,style:{...O,outline:"none",padding:"6px 10px",fontSize:16,lineHeight:1,color:"#64748b"},children:"☰"}),i.jsx(yy,{templateIdentifier:_.templateIdentifier,templateName:y,templateVersion:g,templateList:m,onSwitch:h}),z&&i.jsx("span",{className:"g7le-toolbar__mode-badge","data-testid":"g7le-toolbar-mode-badge","data-mode":_.editMode,style:{padding:"2px 8px",borderRadius:999,background:"#fef3c7",color:"#92400e",fontSize:11,fontWeight:600,border:"1px solid #fde68a"},children:z}),i.jsx("div",{style:{flex:1}}),i.jsxs(i.Fragment,{children:[i.jsx(wt,{"data-testid":"g7le-toolbar-undo","aria-label":w("layout_editor.chrome.toolbar.undo"),title:w("layout_editor.chrome.toolbar.undo"),disabled:!E?.canUndo,onClick:()=>E?.undo?.(),baseStyle:O,hoverStyle:N,style:{opacity:E?.canUndo?1:.4},children:"↶"}),i.jsx(wt,{"data-testid":"g7le-toolbar-redo","aria-label":w("layout_editor.chrome.toolbar.redo"),title:w("layout_editor.chrome.toolbar.redo"),disabled:!E?.canRedo,onClick:()=>E?.redo?.(),baseStyle:O,hoverStyle:N,style:{opacity:E?.canRedo?1:.4},children:"↷"}),i.jsxs(wt,{"data-testid":"g7le-toolbar-add-element","data-open":_.isPaletteOpen?"true":"false",disabled:!_.selectedRoute,onClick:()=>C({type:"TOGGLE_PALETTE"}),"aria-pressed":_.isPaletteOpen,baseStyle:O,hoverStyle:N,style:_.isPaletteOpen?{backgroundColor:"#eff6ff",color:"#1d4ed8",borderColor:"#93c5fd"}:void 0,children:["+ ",w("layout_editor.chrome.toolbar.add_element")]}),i.jsx(wt,{"data-testid":"g7le-toolbar-preview","data-previewing":x?"true":"false",disabled:!p||!_.selectedRoute?.layoutName||x,onClick:()=>{const I=p?.();I&&typeof I.then=="function"&&(k(!0),I.finally(()=>k(!1)))},baseStyle:O,hoverStyle:N,style:{opacity:x?.7:1},children:x?i.jsxs(i.Fragment,{children:[i.jsx("span",{"data-testid":"g7le-toolbar-preview-spinner","aria-hidden":"true",style:{display:"inline-block",width:12,height:12,border:"2px solid rgba(71,85,105,0.35)",borderTopColor:"#475569",borderRadius:"50%",animation:"g7le-spin 600ms linear infinite"}}),w("layout_editor.chrome.toolbar.previewing")]}):i.jsxs(i.Fragment,{children:["👁 ",w("layout_editor.chrome.toolbar.preview")]})}),i.jsxs(wt,{"data-testid":"g7le-toolbar-versions",disabled:!f||!(_.selectedRoute?.layoutName||_.selectedRoute?.path?.startsWith("__extension__/")),onClick:()=>f?.(),"aria-label":w("layout_editor.chrome.toolbar.version_history"),title:w("layout_editor.chrome.toolbar.version_history"),baseStyle:O,hoverStyle:N,children:["🕘 ",w("layout_editor.chrome.toolbar.version_history")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-edit-code",disabled:!r,onClick:()=>r?.(),baseStyle:O,hoverStyle:N,children:["⟨/⟩ ",w("layout_editor.chrome.toolbar.edit_code")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-images",disabled:!s||!_.selectedRoute,onClick:()=>s?.(),baseStyle:O,hoverStyle:N,children:["🖼 ",w("layout_editor.chrome.toolbar.images")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-translations",disabled:!a||!_.selectedRoute,onClick:()=>a?.(),baseStyle:O,hoverStyle:N,children:["🌐 ",w("layout_editor.chrome.toolbar.translations")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-page-settings",disabled:!l||!_.selectedRoute||_.editMode==="extension"||_.editMode==="iteration_item",onClick:()=>l?.(),baseStyle:O,hoverStyle:N,children:["⚙ ",w("layout_editor.chrome.toolbar.page_settings")]}),i.jsxs(wt,{"data-testid":"g7le-toolbar-reset",disabled:!d||!n,onClick:()=>d?.(),"aria-label":w("layout_editor.chrome.toolbar.reset"),title:w("layout_editor.chrome.toolbar.reset"),baseStyle:O,hoverStyle:N,style:{opacity:n?1:.4},children:["↺ ",w("layout_editor.chrome.toolbar.reset")]}),i.jsx(wt,{"data-testid":"g7le-toolbar-save","data-dirty":n?"true":"false","data-saving":v?"true":"false",disabled:!t||v,onClick:()=>{const I=t?.();I&&typeof I.then=="function"&&(b(!0),I.finally(()=>b(!1)))},baseStyle:M,hoverStyle:H,style:{opacity:v?.7:n?1:.7},children:v?i.jsxs(i.Fragment,{children:[i.jsx("span",{"data-testid":"g7le-toolbar-save-spinner","aria-hidden":"true",className:"g7le-toolbar__spinner",style:{display:"inline-block",width:12,height:12,border:"2px solid rgba(255,255,255,0.4)",borderTopColor:"#ffffff",borderRadius:"50%",animation:"g7le-spin 600ms linear infinite"}}),w("layout_editor.chrome.toolbar.saving")]}):i.jsxs(i.Fragment,{children:["💾 ",w("layout_editor.chrome.toolbar.save")]})}),i.jsx(wt,{"data-testid":"g7le-toolbar-shortcuts",disabled:!c,onClick:()=>c?.(),"aria-label":w("layout_editor.shortcuts.title"),title:w("layout_editor.shortcuts.title"),baseStyle:O,hoverStyle:N,children:"⌨"}),i.jsxs(wt,{"data-testid":"g7le-toolbar-exit",disabled:!o,onClick:()=>o?.(),baseStyle:B,hoverStyle:L,children:["✕ ",w("layout_editor.chrome.toolbar.exit")]})]}),D&&i.jsxs(wt,{"data-testid":"g7le-toolbar-exit-alt-mode",onClick:A,baseStyle:B,hoverStyle:L,children:["← ",_.editMode==="base"&&w("layout_editor.chrome.toolbar.exit_base_edit"),_.editMode==="modal"&&w("layout_editor.chrome.toolbar.exit_modal_edit"),_.editMode==="extension"&&w("layout_editor.chrome.toolbar.exit_extension_edit"),_.editMode==="iteration_item"&&w("layout_editor.chrome.toolbar.exit_iteration_item_edit")]})]})}function ar(){if(typeof window>"u")return null;try{return window.localStorage?.getItem("auth_token")??null}catch{return null}}function je(e){const t={Accept:"application/json",...e??{}},n=ar();return n&&(t.Authorization=`Bearer ${n}`),t}function Mi(e){return encodeURIComponent(e)}async function Dl(e,t){const n=`/api/admin/templates/${Mi(e)}/layout-attachments?layout_name=${encodeURIComponent(t)}`;try{const o=await fetch(n,{method:"GET",headers:je(),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||!r?.success?{ok:!1,status:o.status,message:Li(r,o.status)}:{ok:!0,data:Array.isArray(r?.data)?r.data:[]}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}async function Nl(e,t,n){const o=`/api/admin/templates/${Mi(e)}/layout-attachments`,r=new FormData;r.append("file",n),r.append("layout_name",t);try{const s=await fetch(o,{method:"POST",headers:je(),body:r,credentials:"same-origin"}),a=await s.json().catch(()=>null);return!s.ok||!a?.success||!a?.data?.url?{ok:!1,status:s.status,message:Li(a,s.status)}:{ok:!0,data:a.data}}catch(s){return{ok:!1,status:0,message:s instanceof Error?s.message:"network error"}}}async function zl(e){const t=`/api/admin/templates/layout-attachments/${Mi(String(e))}`;try{const n=await fetch(t,{method:"DELETE",headers:je(),credentials:"same-origin"}),o=await n.json().catch(()=>null);return!n.ok||o&&o.success===!1?{ok:!1,status:n.status,message:Li(o,n.status)}:{ok:!0,data:!0}}catch(n){return{ok:!1,status:0,message:n instanceof Error?n.message:"network error"}}}function Li(e,t){if(e&&typeof e=="object"&&"message"in e){const n=e.message;if(typeof n=="string"&&n.length>0)return n}return`HTTP ${t}`}function my(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} KB`:`${(e/(1024*1024)).toFixed(1)} MB`}function vy(e){if(!e)return"";const t=e.match(/^(\d{4})-(\d{2})-(\d{2})/);return t?`${t[2]}-${t[3]}`:""}function Ml({templateIdentifier:e,layoutName:t,t:n,onSelect:o,onChanged:r,onClose:s}){const[a,l]=u.useState([]),[d,c]=u.useState("idle"),[p,f]=u.useState(!1),[y,g]=u.useState(null),m=u.useRef(null),h=u.useCallback(async()=>{c("loading"),g(null);const x=await Dl(e,t);x.ok?(l(x.data),c("idle")):(g(x.message),c("error"))},[e,t]);u.useEffect(()=>{h()},[h]);const v=async x=>{const k=x.target.files?.[0];if(!k)return;f(!0),g(null);const w=await Nl(e,t,k);w.ok?(l(_=>[w.data,..._.filter(C=>C.id!==w.data.id)]),r?.()):g(w.message),f(!1),m.current&&(m.current.value="")},b=async x=>{if(!(typeof window<"u"&&typeof window.confirm=="function"?window.confirm(n("layout_editor.attachment_manager.delete_confirm")):!0))return;const w=await zl(x.id);w.ok?(l(_=>_.filter(C=>C.id!==x.id)),r?.()):g(w.message)};return i.jsxs("div",{className:"g7le-attachment-manager","data-testid":"g7le-attachment-manager",style:_y,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:xy,children:[i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-attachment-manager-title",children:n("layout_editor.attachment_manager.title")}),i.jsx("button",{type:"button","aria-label":"close",onClick:s,style:wy,"data-testid":"g7le-attachment-manager-close",children:"✕"})]}),i.jsx("div",{style:ky,children:i.jsxs("label",{style:Sy,"data-testid":"g7le-attachment-upload",children:[p?"…":`⬆ ${n("layout_editor.attachment_manager.upload")}`,i.jsx("input",{ref:m,type:"file",accept:"image/*",onChange:v,disabled:p,style:{display:"none"},"data-testid":"g7le-attachment-upload-input"})]})}),y&&i.jsx("div",{style:Cy,"data-testid":"g7le-attachment-manager-error",children:y}),i.jsx("div",{style:$y,children:d==="loading"?i.jsx("div",{style:Ll,"data-testid":"g7le-attachment-manager-loading",children:n("layout_editor.attachment_manager.loading")}):a.length===0&&d!=="error"?i.jsx("div",{style:Ll,"data-testid":"g7le-attachment-manager-empty",children:n("layout_editor.attachment_manager.empty")}):i.jsx("div",{style:Ey,"data-testid":"g7le-attachment-grid",children:a.map(x=>i.jsxs("div",{style:Iy,"data-testid":`g7le-attachment-card-${x.id}`,children:[i.jsx("div",{style:{...Ay,backgroundImage:`url(${x.url})`}}),i.jsx("div",{style:Ty,title:x.original_name,children:x.original_name}),i.jsxs("div",{style:Dy,children:[my(x.size),x.created_at?` · ${vy(x.created_at)}`:""]}),i.jsxs("div",{style:Ny,children:[o&&i.jsx("button",{type:"button",onClick:()=>o(x.url),style:zy,"data-testid":`g7le-attachment-use-${x.id}`,children:n("layout_editor.attachment_manager.use_as_bg")}),i.jsx("button",{type:"button",onClick:()=>b(x),style:My,"data-testid":`g7le-attachment-delete-${x.id}`,children:"🗑"})]})]},x.id))})}),i.jsx("div",{style:Ly,children:i.jsx("button",{type:"button",onClick:s,style:Py,"data-testid":"g7le-attachment-manager-footer-close",children:n("layout_editor.attachment_manager.close")})})]})}const _y={display:"flex",flexDirection:"column",minHeight:0,flex:1},xy={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0"},wy={border:"none",background:"transparent",fontSize:16,color:"#94a3b8",cursor:"pointer"},ky={padding:"10px 16px",borderBottom:"1px solid #f1f5f9",display:"flex",gap:8},Sy={padding:"6px 12px",fontSize:13,border:"1px solid #2563eb",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",display:"inline-flex",alignItems:"center"},Cy={padding:"8px 16px",fontSize:12,color:"#b91c1c",background:"#fef2f2"},$y={padding:16,overflow:"auto",flex:1,minHeight:120},Ll={fontSize:13,color:"#94a3b8",textAlign:"center",padding:"32px 0"},Ey={display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(140px, 1fr))",gap:12},Iy={display:"flex",flexDirection:"column",gap:4,border:"1px solid #e2e8f0",borderRadius:8,padding:8},Ay={width:"100%",height:88,borderRadius:6,border:"1px solid #cbd5e1",backgroundSize:"cover",backgroundPosition:"center",backgroundColor:"#f8fafc"},Ty={fontSize:12,fontWeight:600,color:"#0f172a",whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis"},Dy={fontSize:11,color:"#64748b"},Ny={display:"flex",gap:6,marginTop:2},zy={flex:1,padding:"4px 6px",fontSize:11,border:"1px solid #2563eb",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},My={padding:"4px 8px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Ly={padding:"10px 16px",borderTop:"1px solid #e2e8f0",display:"flex",justifyContent:"flex-end"},Py={padding:"6px 14px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};function lr(e){return encodeURIComponent(e)}async function Oy(e,t,n){const o=new URLSearchParams;t&&o.set("layout_name",t);const r=o.toString(),s=`/api/admin/templates/${lr(e)}/custom-translations`+(r?`?${r}`:"");try{const a=await fetch(s,{method:"GET",headers:je(),credentials:"same-origin"}),l=await a.json().catch(()=>null);return!a.ok||!l?.success?{ok:!1,status:a.status,message:dr(l,a.status)}:{ok:!0,data:Array.isArray(l?.data)?l.data:[]}}catch(a){return{ok:!1,status:0,message:a instanceof Error?a.message:"network error"}}}async function By(e,t,n,o){const r=`/api/admin/templates/${lr(e)}/custom-translations/${t}`;try{const s=await fetch(r,{method:"PUT",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:n,expected_lock_version:o}),credentials:"same-origin"}),a=await s.json().catch(()=>null);return!s.ok||!a?.success||!a?.data?{ok:!1,status:s.status,message:dr(a,s.status)}:{ok:!0,data:a.data}}catch(s){return{ok:!1,status:0,message:s instanceof Error?s.message:"network error"}}}async function Wy(e,t){const n=`/api/admin/templates/${lr(e)}/custom-translations/${t}`;try{const o=await fetch(n,{method:"DELETE",headers:je(),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||r&&r.success===!1?{ok:!1,status:o.status,message:dr(r,o.status)}:{ok:!0,data:!0}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}async function jy(e,t){const n=`/api/admin/templates/${lr(e)}/custom-translations`;try{const o=await fetch(n,{method:"DELETE",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({ids:t}),credentials:"same-origin"}),r=await o.json().catch(()=>null);return!o.ok||!r?.success?{ok:!1,status:o.status,message:dr(r,o.status)}:{ok:!0,data:{deleted:typeof r?.data?.deleted=="number"?r.data.deleted:t.length}}}catch(o){return{ok:!1,status:0,message:o instanceof Error?o.message:"network error"}}}const Pl=/\$t:(custom\.[A-Za-z0-9_\-]+\.\d+)/g;function Hy(e){const t=new Set,n=o=>{if(typeof o=="string"){if(o.includes("$t:custom.")){let r;for(Pl.lastIndex=0;(r=Pl.exec(o))!==null;)t.add(r[1])}return}if(Array.isArray(o)){for(const r of o)n(r);return}if(o&&typeof o=="object")for(const r of Object.values(o))n(r)};return n(e),t}function dr(e,t){if(e&&typeof e=="object"&&"message"in e){const n=e.message;if(typeof n=="string"&&n.length>0)return n}return`HTTP ${t}`}const Pt=Lt().TranslationEngine,bo=Lt().createLogger,Pi=bo("EditorSpecLoader"),Fy=["controls","componentCapabilities","actionRecipes","conditionRecipes","pageSettings","initActionRecipes","errorRecipes","computedRecipes","loadingComponents"],Ky=["actionRecipes","initActionRecipes","errorRecipes","computedRecipes","loadingComponents"];function Ol(e){if(typeof window>"u")return[];const n=window.G7Config?.[e];if(!Array.isArray(n))return[];const o=[];for(const r of n){const s=r?.identifier;typeof s=="string"&&s.length>0&&o.push(s)}return o}async function Vy(e,t,n,o){const s=`${n}/${e==="template"?"templates":e==="module"?"modules":"plugins"}/${encodeURIComponent(t)}/editor-spec`;try{const a=await o(s);if(!a.ok)return a.status!==404&&Pi.warn(`fetchOneSpec: HTTP ${a.status}`,s),null;const l=await a.json();return!l?.success||!l.data?null:l.data.spec??null}catch(a){return Pi.warn("fetchOneSpec: fetch failed",s,a),null}}function Uy(e){return Ky.includes(e)}function qy(e,t){return e==null||typeof e!="object"?e:{...e,__source:t}}function Gy(e,t,n,o){const r=t[n];if(!r||typeof r!="object")return;const s=e[n]??{};if(o&&Uy(n)){const a={};for(const[l,d]of Object.entries(r))a[l]=qy(d,o);e[n]={...s,...a};return}e[n]={...s,...r}}function Yy(e,t){if(!t.nesting)return;const n=e.nesting??{},o=new Set(n.draggable??[]);for(const r of t.nesting.draggable??[])o.add(r);n.draggable=Array.from(o),n.containers={...n.containers??{},...t.nesting.containers??{}},e.nesting=n}function Xy(e,t){if(!t.componentPalette)return;const n=e.componentPalette??{},o=[...n.groups??[],...t.componentPalette.groups??[]];n.groups=o,n.entries={...n.entries??{},...t.componentPalette.entries??{}},e.componentPalette=n}function Jy(e,t){if(!t.states)return;const n=e.states??{},o=[...n.groups??[],...t.states.groups??[]];n.groups=o,e.states=n}function Zy(e,t){if(!Array.isArray(t.stateLabels)||t.stateLabels.length===0)return;const n=[...e.stateLabels??[],...t.stateLabels],o=new Map;for(const r of n)!r||typeof r.key!="string"||typeof r.scope!="string"||o.set(`${r.scope}:${r.key}`,r);e.stateLabels=Array.from(o.values())}function Qy(e,t){if(!t.actionChipCandidates)return;const n=e.actionChipCandidates??{};for(const o of["response","error","payload"]){const r=t.actionChipCandidates[o];!Array.isArray(r)||r.length===0||(n[o]=[...n[o]??[],...r])}e.actionChipCandidates=n}function Ry(e,t){if(!t.sampleData)return;const n=e.sampleData??{};n.byDataSourceId={...n.byDataSourceId??{},...t.sampleData.byDataSourceId??{}},n.byEndpointPattern={...n.byEndpointPattern??{},...t.sampleData.byEndpointPattern??{}},e.sampleData=n}function Bl(e,t,n){for(const o of Fy)Gy(e,t,o,n);Yy(e,t),Xy(e,t),Jy(e,t),Zy(e,t),Qy(e,t),Ry(e,t),t.version&&(e.version=t.version)}async function eb(e){const t=e.apiBaseUrl??"/api",n=e.fetcher??(typeof fetch<"u"?fetch:void 0);if(!n)return Pi.warn("loadEditorSpecBundle: fetch unavailable, returning null spec"),{spec:null,sampleGlobalSources:[]};const o=e.activeModuleIdentifiers??Ol("activeModules"),r=e.activePluginIdentifiers??Ol("activePlugins"),s=[...o.map(p=>({kind:"module",id:p})),...r.map(p=>({kind:"plugin",id:p})),{kind:"template",id:e.templateIdentifier}],a=await Promise.all(s.map(p=>Vy(p.kind,p.id,t,n)));let l=null;const d=[],c={};if(e.coreSeed&&(l={},Bl(l,e.coreSeed,{kind:"core"})),s.forEach((p,f)=>{const y=a[f];if(!y)return;y.sampleGlobal&&typeof y.sampleGlobal=="object"&&d.push({id:p.id,kind:p.kind,sampleGlobal:y.sampleGlobal});const g=y.sampleData?.byDataSourceId;if(g&&typeof g=="object"){const m=p.kind==="template"?"template":`${p.kind}:${p.id}`;c[m]={...c[m]??{},...g}}l===null&&(l={}),Bl(l,y,{kind:p.kind,id:p.id})}),l!==null&&Object.keys(c).length>0){const p=l;p.sampleData={...p.sampleData??{},bySource:c}}return{spec:l,sampleGlobalSources:d}}function Oi(e,t){return e?.componentCapabilities?.[t]??null}function Ct(e,t){return e?.controls?.[t]??null}function tb(e){return e?.initActionRecipes??{}}function nb(e){return e?.computedRecipes??{}}function ob(e){return e?.errorRecipes??{}}function rb(e){const t=e?.loadingComponents;return!t||typeof t!="object"?[]:Object.values(t).filter(n=>n!=null&&typeof n=="object"&&typeof n.name=="string")}function ib(e,t,n){const o=e?.stateLabels;if(!Array.isArray(o))return null;for(const r of o)if(r?.scope===t&&r?.key===n)return typeof r.label_key=="string"?r.label_key:null;return null}function Bi(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)}function sb(e){return e===null?"null":typeof e=="string"?e.length>40?`${e.slice(0,40)}…`:e:typeof e=="number"||typeof e=="boolean"?String(e):""}function ab(e){const t=e.find(n=>Bi(n));if(Bi(t))return Object.keys(t).slice(0,8)}function lb(e){return Object.keys(e).slice(0,8)}function db(e){const t=Object.entries(e);return t.length===0?!1:t.some(([,n])=>n===null||typeof n!="object")}function Wi(e,t,n,o,r){if(Array.isArray(e)){r(t,"array",`[${e.length}]`,ab(e));return}if(Bi(e)){if(t&&db(e)){const s=lb(e);r(t,"object",`{${s.length}}`,s)}if(n>=o)return;for(const[s,a]of Object.entries(e)){const l=t?`${t}.${s}`:s;Wi(a,l,n+1,o,r)}return}r(t,"scalar",sb(e))}function cb(e,t,n){const o=[];let r=e;for(const s of t){if(!r||(r=(Array.isArray(r.children)?r.children:[])[s],!r))break;const l=r.iteration;if(!(!l||l.source===void 0)&&(typeof l.index_var=="string"&&l.index_var.length>0&&o.push({name:l.index_var,kind:"index"}),typeof l.item_var=="string"&&l.item_var.length>0)){const d=typeof l.source=="string"?l.source:"",c=d&&n?.get(Wl(d))||[];o.push({name:l.item_var,kind:"item",itemFields:c})}}return o}function Wl(e){return e.replace(/^\{\{|\}\}$/g,"").trim()}function ub(e){const t=new Map;for(const n of e)n.shape==="array"&&n.itemFields&&n.itemFields.length>0&&t.set(Wl(n.expression),n.itemFields);return t}function ji(e){const t=e.maxDepth??4,n=[];for(const o of e.dataSources??[])!o||typeof o.id!="string"||o.id.length===0||Wi(o.sample,"",0,t,(r,s,a,l)=>{const d=r?`{{${o.id}.${r}}}`:`{{${o.id}}}`;n.push({expression:d,source:"data_source",sourceId:o.id,path:r,shape:s,groupLabelKey:o.labelKey?.startsWith("$t:")?o.labelKey.slice(3):o.labelKey,preview:a,itemFields:l})});for(const o of e.states??[])!o||typeof o.scope!="string"||Wi(o.tree,"",0,t,(r,s,a,l)=>{if(!r)return;const d=ib(e.spec,o.scope,r)??void 0;n.push({expression:`{{${o.scope}.${r}}}`,source:o.scope,sourceId:o.scope,path:r,shape:s,labelKey:d?.startsWith("$t:")?d.slice(3):d,preview:a,itemFields:l})});for(const o of e.iterationVars??[])if(!(!o||typeof o.name!="string"||o.name.length===0)){if(o.kind==="index"){n.push({expression:`{{${o.name}}}`,source:"iteration",sourceId:o.name,path:"",shape:"scalar",preview:"0"});continue}for(const r of o.itemFields??[])n.push({expression:`{{${o.name}.${r}}}`,source:"iteration",sourceId:o.name,path:r,shape:"scalar",preview:""})}return n}const pb=["$localized"];function fb(e){const t=e.trim(),n=t.indexOf("(");if(n===-1||!t.endsWith(")"))return{fn:null,inner:t};const o=t.slice(0,n).trim();if(!pb.includes(o))return{fn:null,inner:t};const r=t.slice(n+1,-1);let s=0,a=null;for(let l=0;l<r.length;l++){const d=r[l];if(a){if(d==="\\"){l++;continue}d===a&&(a=null);continue}if(d==="'"||d==='"'||d==="`"){a=d;continue}if(d==="("||d==="[")s++;else if(d===")"||d==="]"){if(s--,s<0)return{fn:null,inner:t}}else if(s===0&&d===",")return{fn:null,inner:t}}return s!==0?{fn:null,inner:t}:{fn:o,inner:r.trim()}}const gb=["_global","_local","route","query","_computed"];function Hi(e){let t=e.trim();for(;t.startsWith("(")&&t.endsWith(")");){let n=0,o=!0;for(let r=0;r<t.length;r++)if(t[r]==="(")n++;else if(t[r]===")"&&(n--,n===0&&r<t.length-1)){o=!1;break}if(!o)break;t=t.slice(1,-1).trim()}return t}function Kt(e){if(typeof e!="string")return null;const t=e.trim(),n=/^\{\{\s*([^{}]+?)\s*\}\}$/.exec(t);if(!n)return null;let o=Hi(n[1].trim()),r;{let g=0;for(let m=0;m<o.length-1;m++){const h=o[m];if(h==="("||h==="[")g++;else if(h===")"||h==="]")g--;else if(g===0&&h==="?"&&o[m+1]==="?"){r=o.slice(m+2).trim(),o=o.slice(0,m).trim();break}}}o=Hi(o);const s=fb(o),a=s.fn??void 0;a&&(o=Hi(s.inner));const l=o.includes("?."),d=o.replace(/\?\./g,".");if(!/^[A-Za-z_$][\w$]*(\.[A-Za-z_$][\w$]*|\[\d+\])*$/.test(d)||r!==void 0&&!/^(\[\s*\]|\{\s*\}|''|""|`+`|-?\d+(\.\d+)?|true|false|null)$/.test(r))return null;const c=d.indexOf("."),p=c===-1?d:d.slice(0,c),f=c===-1?"":d.slice(c+1);return{...gb.includes(p)?{source:p,sourceId:p,path:f}:{source:"data_source",sourceId:p,path:f},optional:l,fallback:r,localeFn:a}}function En(e,t,n,o){const r=t?t.split(".").filter(Boolean):[];return`{{${[e,...r].join("?.")} ?? ${n==="array"?"[]":n==="object"?"{}":"''"}}}`}function Fi(e,t){return e.filter(n=>n.shape===t)}function Ki(e,t){const n=t.trim().toLowerCase();return n===""?e:e.filter(o=>[o.resolvedLabel??"",o.sourceId,o.path,o.preview].join(" ").toLowerCase().includes(n))}function jl(e,t,n){const o={response:[{path:"data",labelKey:"layout_editor.action_chip.response_data",shape:"object"},{path:"message",labelKey:"layout_editor.action_chip.response_message",shape:"scalar"}],error:[{path:"status",labelKey:"layout_editor.action_chip.error_status",shape:"scalar"},{path:"message",labelKey:"layout_editor.action_chip.error_message",shape:"scalar"},{path:"errors",labelKey:"layout_editor.action_chip.error_errors",shape:"object"},{path:"data",labelKey:"layout_editor.action_chip.error_data",shape:"object"},{path:"statusText",labelKey:"layout_editor.action_chip.error_status_text",shape:"scalar"}],payload:[{path:"data",labelKey:"layout_editor.action_chip.payload_data",shape:"object"}]},r=e==="response"?"response":e==="error"?"error":"message",s=new Set,a=[];for(const d of o[e])s.has(d.path)||(s.add(d.path),a.push(d));const l=n?.[e];if(Array.isArray(l))for(const d of l)!d||typeof d.path!="string"||typeof d.labelKey!="string"||s.has(d.path)||(s.add(d.path),a.push({path:d.path,labelKey:d.labelKey,shape:d.shape??"scalar"}));return a.map(d=>({expression:`{{${r}.${d.path}}}`,source:"_local",sourceId:r,path:d.path,shape:d.shape,labelKey:d.labelKey,preview:t(d.labelKey)}))}const Vi=/\{\{(?:[^}]|\}(?!\}))*\}\}/g;function In(e){const t=[];if(typeof e!="string"||e.length===0)return t;let n=0,o=0;const r=new RegExp(Vi.source,"g");let s;for(;(s=r.exec(e))!==null;){const a=s.index,l=a+s[0].length;a>n&&t.push({kind:"literal",raw:e.slice(n,a),start:n,end:a}),t.push({kind:"binding",raw:s[0],start:a,end:l,parsed:Kt(s[0]),bindingIndex:o++}),n=l}return n<e.length&&t.push({kind:"literal",raw:e.slice(n),start:n,end:e.length}),t}function cr(e){return In(e).filter(t=>t.kind==="binding")}function Ui(e){return typeof e!="string"?!1:new RegExp(Vi.source).test(e)}function hb(e){const t=new Set(e.map(o=>o.trim()));let n=0;for(;t.has(`p${n}`);)n+=1;return`p${n}`}function Hl(e,t,n){const o=typeof e=="string"?e:"",r=Math.max(0,Math.min(t,o.length)),s=o.slice(0,r),a=o.slice(r),l=s.length>0&&!/\s$/.test(s)?" ":"",d=a.length>0&&!/^\s/.test(a)?" ":"";return`${s}${l}{${n}}${d}${a}`}function qi(e,t){const n=typeof e=="string"?e:"";return An(n).includes(t)?n:n.length===0?`{${t}}`:/\s$/.test(n)?`${n}{${t}}`:`${n} {${t}}`}function Fl(e){return cr(e).map(t=>({bindingIndex:t.bindingIndex??0,expression:t.raw,parsed:t.parsed??null,isComplex:(t.parsed??null)===null}))}function ur(e){return typeof e!="string"?"":e.replace(new RegExp(Vi.source,"g")," ").replace(/\s+/g," ").trim()}const yb=/\$t:([a-zA-Z0-9._-]+)/g,Kl="custom.";function Gi(e,t){return typeof e!="string"||e.length===0?typeof e=="string"?e:"":t?In(e).map(n=>n.kind!=="literal"?n.raw:n.raw.replace(yb,(o,r)=>{if(r.startsWith(Kl))return o;const s=t(r);return s&&s!==r?s:o})).join(""):e}const Kn="(?:(?!\\|[a-zA-Z0-9_]+=).)*",bb=new RegExp(`^\\s*\\$t:([a-zA-Z0-9._-]+)((?:\\|[a-zA-Z0-9_]+=${Kn})*)`),mb=new RegExp(`\\|([a-zA-Z0-9_]+)=(${Kn})`,"g");function Vt(e){if(typeof e!="string")return"";const t=Kt(e);if(t)return t.path||t.sourceId;const n=e.trim().replace(/^\{\{\s*/,"").replace(/\s*\}\}$/,"");let o=-1;for(let l=0;l<n.length;l++)if(n[l]==="|"&&n[l+1]!=="|"&&n[l-1]!=="|"){o=l;break}if(o>0){const l=n.slice(0,o).trim(),d=Kt(`{{${l}}}`);if(d)return d.path||d.sourceId;const c=l.replace(/\?\./g,".").replace(/\s*\?\?.*$/,"").trim();if(/^[A-Za-z_$][\w$.]*$/.test(c)){const p=c.split(".").filter(Boolean);return p.length>1&&p.slice(1).join(".")||c}}const r=n.replace(/\s+/g," ").trim(),s=r.replace(/\s*(\?\?|\|\|)\s*(\[\s*\]|\{\s*\}|'[^']*'|"[^"]*"|\d+|null|undefined)\s*$/,"").trim(),a=/\.([A-Za-z_$][\w$]*)\s*$/.exec(s);return a?a[1]:r}const Vl=/\$(?:core|module|plugin)_settings:[a-zA-Z0-9._:-]+/g;function Ul(e){return typeof e=="string"&&new RegExp(Vl.source).test(e)}function vb(e){return typeof e!="string"?"":e.replace(/^\$(?:core|module|plugin)_settings:/,"").split(/[.:]/).filter(Boolean).pop()||e}function ql(e){const t=[];for(const n of In(e)){if(n.kind==="binding"){t.push({kind:"chip",label:Vt(n.raw)});continue}let o=0;const r=n.raw,s=new RegExp(Vl.source,"g");let a;for(;(a=s.exec(r))!==null;)a.index>o&&t.push({kind:"text",label:r.slice(o,a.index)}),t.push({kind:"chip",label:vb(a[0])}),o=a.index+a[0].length;o<r.length&&t.push({kind:"text",label:r.slice(o)})}return t}function _b(e){return typeof e!="string"?!1:/\{\{/.test(e)?!0:Ul(e)}function Yi(e,t){const n={chipValue:"",bindings:[],paramLabels:{},keyifiable:!1};if(typeof e!="string"||e.length===0)return n;const o=[],r={},s=g=>{const m=`p${o.length}`;return o.push(g),r[m]=Vt(g),`{${m}}`};let a=e,l="";const d=bb.exec(e);if(d&&!d[1].startsWith(Kl)){const g=d[1],m=d[2]??"",h=e.slice(d[0].length),v=new Map,b=new RegExp(mb.source,"g");let x;for(;(x=b.exec(m))!==null;)x[2]&&x[2].includes("{{")&&v.set(x[1],x[2]);const k=t?t(g):"";if(k&&k!==g&&!k.startsWith("$t:"))l=k.replace(/\{\{?([a-zA-Z0-9_]+)\}?\}/g,(w,_)=>{const C=v.get(_);return C?s(C):w});else return n;a=h}const c=Gi(a,t),p=In(c).map(g=>g.kind==="binding"?s(g.raw):g.raw).join(""),f=`${l}${p}`;return/\$t:[a-zA-Z0-9._-]+/.test(f)||o.length===0?n:{chipValue:f,bindings:o,paramLabels:r,keyifiable:!0}}function Gl(e,t){return t.bindings.length?e+t.bindings.map((n,o)=>`|p${o}=${n}`).join(""):e}function Yl(e,t){const n=cr(t).map(r=>r.raw);if(n.length===0)return e;const o=n.map((r,s)=>`|p${s}=${r}`).join("");return`${e}${o}`}function Xl(e){if(typeof e!="string")return"";const t=In(e);let n=0;return t.map(o=>o.kind==="binding"?`{p${n++}}`:o.raw).join("")}const Jl=new RegExp(`^\\s*\\$t:([a-zA-Z0-9._-]+)((?:\\|[a-zA-Z0-9_]+=${Kn})+)\\s*$`),xb=new RegExp(`\\|([a-zA-Z0-9_]+)=(${Kn})`,"g");function mt(e){if(typeof e!="string")return null;const t=Jl.exec(e);if(!t)return null;const n=t[1],o=t[2],r=[],s=new RegExp(xb.source,"g");let a;for(;(a=s.exec(o))!==null;){const l=a[1],d=a[2];r.push({name:l,expression:d,parsed:Kt(d)})}return{key:n,params:r}}function Zl(e){return typeof e=="string"&&Jl.test(e)}function An(e){if(typeof e!="string")return[];const t=[],n=/\{\{?(p\d+)\}?\}/g;let o;for(;(o=n.exec(e))!==null;)t.push(o[1]);return t.sort()}function wb(e,t,n){if(typeof e!="string")return e;const o=new RegExp(`(\\|${t}=)${Kn}`);return o.test(e)?e.replace(o,`$1${n}`):e}function mo(e,t){if(typeof e!="string")return e;const n=new RegExp(`\\|${t}=${Kn}`);return e.replace(n,"")}function Ql(e,t){return typeof e!="string"?e:e.replace(new RegExp(`\\s*\\{\\{?${t}\\}?\\}\\s*`,"g")," ").replace(/\s+/g," ").trim()}function kb(e,t){const n=s=>cr(typeof s=="string"?s:"").map(a=>a.raw.replace(/\s+/g,"")).sort(),o=n(e),r=n(t);return o.length!==r.length?!1:o.every((s,a)=>s===r[a])}function Rl(e){return e.__source?.kind==="base"}function ed(e){return e.__source?.kind==="partial"}function Sb(e){return e.__source?.kind==="route"}function pr(e,t){return e.__source?.kind!=="extension"?!1:t==null?!0:e.__source.extensionId!==t}function td(e){return e.type==="extension_point"}function Xi(e=[]){for(const t of e)if(t.iteration&&t.iteration.source!==void 0)return!0;return!1}function Cb(e,t=[]){for(const n of t)if(n.iteration&&n.iteration.source!==void 0)return!0;return e.iteration&&e.iteration.source!==void 0?!0:nd(e)}function nd(e){if(typeof e.text=="string"&&od(e.text)&&!$b(e.text))return!0;if(e.props&&typeof e.props=="object"){for(const t of Object.values(e.props))if(typeof t=="string"&&od(t))return!0}return!1}function od(e){return/\{\{[\s\S]*?\}\}/.test(e)}function $b(e){const t=mt(e);return t!==null&&t.key.startsWith("custom.")}function Eb(e,t){return e??t??null}function Ji(e,t,n){if(td(e))return!0;switch(t){case"route":return Rl(e)||ed(e)||pr(e);case"base":return pr(e);case"modal":return pr(e);case"extension":return pr(e,n)?!0:Rl(e)||Sb(e)||ed(e);default:return!1}}function et(e){return typeof e=="object"&&e!==null&&typeof e.responsive=="string"}function fr(e,t){return et(e)?et(t)&&e.responsive===t.responsive:!et(t)&&e===t}function at(e){const t=[];let n=!1;for(const o of e){if(et(o)){t.push("responsive",o.responsive),n=!0;continue}n?t.push("children",String(o)):(t.push(String(o)),n=!0)}return t.join(".")}function Qt(e){return Array.isArray(e.children)?e.children:[]}function gr(e,t){const n=e.responsive?.[t];return n&&Array.isArray(n.children)?n.children:[]}function We(e,t){if(!e)return null;if(t.length===0)return e;let n=e,o=Qt(e);for(const r of t){if(et(r)){o=gr(n,r.responsive);continue}if(r<0||r>=o.length||(n=o[r],!n))return null;o=Qt(n)}return n}function Ib(e,t,n){if(!e||t.length===0)return t;let o=e,r=Qt(e),s;for(let a=0;a<t.length;a++){const l=t[a];if(et(l)){r=gr(o,l.responsive);continue}const d=r[l]??null;if(!d)return t;const c=d.__source?.kind,p=d.__source?.extensionId,f=c==="extension"&&n!=null&&p===n;if(c==="extension"&&s!=="extension"&&!f)return t.slice(0,a+1);s=c,o=d,r=Qt(d)}return t}function rd(e,t){if(t.length===0)return null;const n=t[t.length-1];if(et(n))return null;const o=n,r=t.length>=2?t[t.length-2]:void 0;if(r!==void 0&&et(r)){const a=We(e,t.slice(0,-2));return a?{owner:a,children:gr(a,r.responsive),branchKey:r.responsive,index:o}:null}const s=We(e,t.slice(0,-1));return s?{owner:s,children:Qt(s),index:o}:null}function Zi(e,t){if(e.branchKey!==void 0){const n={...e.owner.responsive??{}};n[e.branchKey]={...n[e.branchKey]??{},children:t},e.owner.responsive=n}else e.owner.children=t}function Rt(e){const t={...e};t.props&&typeof t.props=="object"&&(t.props={...t.props});const n=Qt(e);if(n.length>0&&(t.children=n.map(Rt)),e.responsive&&typeof e.responsive=="object"){const o={};for(const r of Object.keys(e.responsive)){const s=e.responsive[r];if(!s||typeof s!="object"){o[r]=s;continue}const a={...s};Array.isArray(s.children)&&(a.children=s.children.map(Rt)),a.props&&typeof a.props=="object"&&(a.props={...a.props}),o[r]=a}t.responsive=o}return t}function Qi(e,t,n,o){const r=Rt(e),s=t.length>0?t[t.length-1]:void 0;if(s!==void 0&&et(s)){const p=We(r,t.slice(0,-1));if(!p)return r;const f=gr(p,s.responsive),y=Math.max(0,Math.min(n,f.length)),g=[...f];return g.splice(y,0,o),Zi({owner:p,branchKey:s.responsive},g),r}const a=We(r,t);if(!a)return r;const l=Qt(a),d=Math.max(0,Math.min(n,l.length)),c=[...l];return c.splice(d,0,o),a.children=c,r}function Ri(e,t){if(t.length===0)return e;const n=Rt(e),o=rd(n,t);if(!o||o.index<0||o.index>=o.children.length)return n;const r=[...o.children];return r.splice(o.index,1),Zi(o,r),n}function Ab(e,t,n,o){if(t.length===0||!n||et(t[t.length-1]))return e;const r=Rt(e),s=We(r,t);if(!s)return r;const a=s.responsive&&typeof s.responsive=="object"?s.responsive[n]:void 0;if(a&&Array.isArray(a.children))return r;const l=o&&s.responsive&&typeof s.responsive=="object"?s.responsive[o]:void 0,c=(l&&Array.isArray(l.children)?l.children:Qt(s)).map(Rt),p={...s.responsive??{}};return p[n]={...p[n]??{},children:c},s.responsive=p,r}function Tb(e,t,n){if(t.length===0||!n||et(t[t.length-1]))return e;const o=Rt(e),r=We(o,t);if(!r||!r.responsive||typeof r.responsive!="object")return o;const s=r.responsive[n];if(!s||!Array.isArray(s.children))return o;const a={...r.responsive},l={...a[n]};return delete l.children,Object.keys(l).length===0?delete a[n]:a[n]=l,r.responsive=Object.keys(a).length>0?a:void 0,o}function Db(e,t,n,o){if(t.length===0)return e;const r=We(e,t);if(!r||Nb(t,n))return e;const s=Rt(r),a=Ri(e,t),l=id(n,t);let d=o;const c=t.slice(0,-1);if(sd(c,n)){const p=t[t.length-1];(typeof p=="number"?p:0)<o&&(d=Math.max(0,o-1))}return Qi(a,l,d,s)}function id(e,t){if(t.length===0)return e;const n=t.length-1;if(e.length<=n)return e;for(let s=0;s<n;s++)if(!fr(e[s],t[s]))return e;const o=t[n],r=e[n];if(et(o)||et(r))return e;if(r>o){const s=[...e];return s[n]=r-1,s}return e}function sd(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(!fr(e[n],t[n]))return!1;return!0}function Nb(e,t){if(t.length<e.length)return!1;for(let n=0;n<e.length;n++)if(!fr(t[n],e[n]))return!1;return t.length>e.length||sd(e,t)}function es(e){const t={...e};if(t.props&&typeof t.props=="object"&&(t.props={...t.props}),typeof t.id=="string"&&t.id.length>0){const o=typeof t.name=="string"?t.name:"node";t.id=`${o}_${zb()}`}const n=Qt(e);return n.length>0&&(t.children=n.map(es)),t}function Ot(e,t,n){if(t.length===0)return n(e);const o=Rt(e),r=We(o,t);if(!r)return o;const s=n(r),a=rd(o,t);if(!a||a.index<0||a.index>=a.children.length)return o;const l=[...a.children];return l[a.index]=s,Zi(a,l),o}function zb(){return Math.floor(Math.random()*2821109907456).toString(36).padStart(8,"0")}function Vn(e){if(!Array.isArray(e)||e.length===0)return[];const t=[];for(const n of e){const o=Mb(n);o.length>0&&t.push(...o)}return t}function Mb(e){if(!e||typeof e!="object")return[];const t=e.__source,n=e._fromBase===!0,o=t?.kind;if(o==="extension"||o==="partial")return[];if(o==="base"||n){const s=Array.isArray(e.children)?e.children:[];return Vn(s)}const r={...e};return delete r.__source,delete r._fromBase,delete r.__injectedProps,Array.isArray(r.children)&&(r.children=Vn(r.children)),[r]}function vo(e){if(!e||typeof e!="object")return e;const t=e.__editor,n=t&&typeof t=="object"&&!Array.isArray(t)?t.original:void 0,o=n&&typeof n=="object"&&!Array.isArray(n)?n:void 0,r=Array.isArray(e.components)?e.components:[],s=r.length>0,a=Vn(r),l=o?{...o}:{...e};if(typeof l.extends=="string"&&l.extends.length>0){delete l.components;const c=l.slots&&typeof l.slots=="object"&&!Array.isArray(l.slots)?l.slots:{},p=Object.keys(c);if(!s&&p.length>0){const f={};for(const[y,g]of Object.entries(c))f[y]=Array.isArray(g)?Vn(g):g;l.slots=f}else if(p.length===1)l.slots={[p[0]]:a};else if(p.length===0)l.slots={content:a};else{const f={};for(const[y,g]of Object.entries(c))f[y]=Array.isArray(g)?Vn(g):g;l.slots=f}}else l.components=a,delete l.slots;return delete l.lock_version,delete l.__editor,l}const ad="extension",Un="extensionless";function Lb(){const e=globalThis?.G7Config;if(globalThis?.__g7AssetUrlMode===Un)return Un;const n=e?.assetUrlMode;return n===Un||n===ad?n:e?.settings?.general?.asset_url_mode===Un?Un:ad}function Pb(){return Lb()===Un}function en(e,t,n,o){const r=e.replace(/\/+$/,""),s=t.replace(/^\.+/,""),a=Pb()?r:`${r}.${s}`,l=[];return o&&l.push(o.replace(/^[?&]+/,"")),n!=null&&n!==""&&l.push(`v=${n}`),l.length>0?`${a}?${l.join("&")}`:a}const ld=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,hr="custom.",Ob=/\{\{.*?\}\}/;function Bb(e){return!!(!e||e.iteration&&typeof e.iteration=="object")}function dd(e,t){const n=e?.text,o=typeof n=="string"?n:"";if(Bb(e)||typeof n!="string")return{sourceState:"plain_text",editable:!1,customKey:null,displayValue:o};const r=mt(o);if(r&&r.key.startsWith(hr)){const a=t?t(r.key):r.key;return{sourceState:"custom_key",editable:!0,customKey:r.key,displayValue:a,isParamKey:!0}}const s=ld.exec(o);if(s){const a=s[1];if(a.startsWith(hr)){const d=t?t(a):a;return{sourceState:"custom_key",editable:!0,customKey:a,displayValue:d}}const l=t?t(a):"";return{sourceState:"plain_text",editable:!0,customKey:null,displayValue:l&&l!==a?l:""}}if(Ob.test(o)){const a=ur(o),l=ld.exec(a);if(l&&l[1].startsWith(hr)){const f=l[1],y=t?t(f):f;return{sourceState:"custom_key",editable:!0,customKey:f,displayValue:y}}const d=Gi(o,t),c=ur(d),p=/\$t:[a-zA-Z0-9._-]+/.test(c);if(c.length>0){const f=Yi(o,t);return{sourceState:"plain_with_binding",editable:!0,customKey:null,displayValue:p?"":c,chipValue:f.keyifiable?f.chipValue:void 0,chipParamLabels:f.keyifiable?f.paramLabels:void 0}}return{sourceState:"binding_expression",editable:!1,customKey:null,displayValue:o}}return{sourceState:"plain_text",editable:!0,customKey:null,displayValue:o}}function Wb(){const{state:e}=Je(),t=Ft(),n=e.templateIdentifier,o=e.locale,r=e.selectedRoute?.layoutName??null,s=u.useCallback(p=>{try{const y=Pt.getInstance().translate(p,{templateId:n,locale:o});return y&&y!==p?y:""}catch{return""}},[n,o]),a=u.useCallback(p=>dd(p,s),[s]),l=u.useCallback(p=>{Et({op:"inline_edit_cancel",componentPath:p?at(p):null,timestamp:Date.now()})},[]),d=u.useCallback(async(p,f)=>{if(!t)return{kind:"error",message:"no document context"};const g={children:t.document?.raw?.components??[]},m=We(g,p),h=dd(m,s);if(!h.editable)return Et({op:"inline_edit_blocked_binding",sourceState:h.sourceState,componentPath:at(p),timestamp:Date.now()}),{kind:"blocked"};if(h.customKey){if(f===h.displayValue)return{kind:"noop",translationKey:h.customKey};_o(n,o,h.customKey,f);const E=await yr(n,h.customKey,o,f);return E.kind==="error"?{kind:"error",message:E.message}:(await tn(n,o),Et({op:"inline_edit_update_value",sourceState:"custom_key",componentPath:at(p),translationKey:h.customKey,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"updated",translationKey:h.customKey})}const v=typeof m?.text=="string"?m.text:"",b=h.sourceState==="plain_with_binding",x=Gi(v,s),k=b?Xl(x):f,w=E=>b?Yl(E,v):E;t.patchLayout(E=>Ot({children:E},p,$=>({...$,text:f})).children??[]);const _=await qn(n,r,o,k);if(_.kind==="error"||!_.resource)return{kind:"error",message:_.message};const C=_.resource.translation_key;return _o(n,o,C,k),t.patchLayout(E=>Ot({children:E},p,$=>({...$,text:w(`$t:${C}`)})).children??[]),await tn(n,o),Et({op:"inline_edit_create_key",sourceState:h.sourceState,componentPath:at(p),translationKey:C,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"created",translationKey:C,id:_.resource.id}},[t,n,o,r,s]),c=u.useCallback(async(p,f)=>{if(!t)return{kind:"error",message:"no document context"};const g={children:t.document?.raw?.components??[]},m=We(g,p),h=typeof m?.text=="string"?m.text:"",v=mt(h);if(v&&v.key.startsWith(hr))return br(n,v.key,o,f),_o(n,o,v.key,f),t.markDirty?.(),{kind:"updated",translationKey:v.key};const b=Yi(h,s),x=ur(f.replace(/\{\{?p\d+\}?\}/g," ")).replace(/\s+/g," ").trim(),k=await qn(n,r,o,x);if(k.kind==="error"||!k.resource)return{kind:"error",message:k.message};const w=k.resource.translation_key,_=An(f),C=k.resource.values??{},E={[o]:f};for(const S of Object.keys(C)){if(S===o)continue;let T=C[S]??"";for(const $ of _)T=qi(T,$);E[S]=T}return xo(n,w,E),_o(n,o,w,f),t.patchLayout(S=>Ot({children:S},p,A=>({...A,text:Gl(`$t:${w}`,b)})).children??[]),t.markDirty?.(),await tn(n,o),_o(n,o,w,f),Et({op:"inline_edit_create_key",sourceState:"plain_with_binding",componentPath:at(p),translationKey:w,toLocale:o,valueLength:f.length,valueHash:Ni(f),timestamp:Date.now()}),{kind:"created",translationKey:w}},[t,n,o,r]);return{classify:a,commit:d,trackCancel:l,keyifyChipValue:c}}async function qn(e,t,n,o){const r=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations`;try{const s=await fetch(r,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({layout_name:t,locale:n,value:o})}),a=await s.json().catch(()=>null);if(!s.ok)return{kind:"error",message:a?.message??`HTTP ${s.status}`};const l=a?.data;return!l||typeof l.translation_key!="string"?{kind:"error",message:"invalid response"}:{kind:"ok",resource:l}}catch(s){return{kind:"error",message:s instanceof Error?s.message:"network error"}}}async function yr(e,t,n,o){const r=await cn(e,t);if(!r)return{kind:"error",message:"custom key not found"};const s=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations/${r.id}`,a={...r.values??{},[n]:o};try{const l=await fetch(s,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:a,expected_lock_version:r.lock_version})}),d=await l.json().catch(()=>null);return l.status===409?{kind:"error",message:"concurrent_modification"}:l.ok?{kind:"ok"}:{kind:"error",message:d?.message??`HTTP ${l.status}`}}catch(l){return{kind:"error",message:l instanceof Error?l.message:"network error"}}}async function cn(e,t){const n=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations`;try{const o=await fetch(n,{method:"GET",credentials:"same-origin",headers:je()});if(!o.ok)return null;const s=(await o.json().catch(()=>null))?.data;return Array.isArray(s)?s.find(a=>a.translation_key===t)??null:null}catch{return null}}function _o(e,t,n,o){try{Pt.getInstance().setTranslationValue(e,t,n,o)}catch{}if(typeof window<"u")try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:t}}))}catch{}}const kt="g7le:translations-refreshed";async function tn(e,t){if(typeof window>"u")return;const n=t??window.__g7EditorContentLocale;if(n){try{const o=Pt.getInstance();try{const r=await fetch(en(`/api/templates/${encodeURIComponent(e)}/config`,"json"),{headers:{Accept:"application/json"},credentials:"same-origin"}).then(a=>a.ok?a.json():null),s=r?.data?.cache_version??r?.cache_version;if(typeof s=="number"&&s>0){o.setCacheVersion(s);const a=window;a.G7Config&&(a.G7Config.cache_version=s)}}catch{}await o.loadTranslations(e,n,"/api",!0)}catch{}try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:n}}))}catch{}}}const nn=new Map;function br(e,t,n,o){const r=nn.get(t)??{};r[n]=o,nn.set(t,r);try{Pt.getInstance().setTranslationValue(e,n,t,o)}catch{}if(typeof window<"u")try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:n}}))}catch{}}function xo(e,t,n){for(const[o,r]of Object.entries(n))br(e,t,o,r)}function It(e,t){return nn.get(e)?.[t]}function jb(e){const t=nn.get(e);return t?{...t}:void 0}function Hb(){return nn.size>0}function cd(e,t){for(const[n,o]of nn.entries())for(const[r,s]of Object.entries(o))try{e.setTranslationValue(t,r,n,s)}catch{}}async function Fb(e){if(nn.size===0)return{ok:0,failed:[]};const t=Array.from(nn.entries());let n=0;const o=[];for(const[r,s]of t)try{const a=await cn(e,r);if(!a){o.push(r);continue}const l={...a.values??{},...s},d=`/api/admin/templates/${encodeURIComponent(e)}/custom-translations/${a.id}`;if(!(await fetch(d,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:l,expected_lock_version:a.lock_version})})).ok){o.push(r);continue}n+=1}catch{o.push(r)}for(const[r]of t)o.includes(r)||nn.delete(r);try{await tn(e)}catch{}return{ok:n,failed:o}}function Gn(e,t){const n=`layout_editor.locale.${e}`,o=t(n);return o&&o!==n?o:e.toUpperCase()}function ud(){if(typeof window>"u")return[];const t=window.G7Config?.appConfig?.supportedLocales;return Array.isArray(t)?t.filter(n=>typeof n=="string"&&n.length>0):[]}function Kb({locales:e}={}){const{t}=tt(),{state:n,dispatch:o}=Je();u.useEffect(()=>{window.__g7EditorContentLocale=n.locale},[n.locale]);const r=e??ud(),s=r.includes(n.locale)?r:[n.locale,...r];if(s.length<=1)return i.jsxs("span",{className:"g7le-locale-switcher g7le-locale-switcher--single","data-testid":"g7le-locale-switcher-single",style:{fontSize:11,color:"#475569",padding:"0 4px"},children:[t("layout_editor.chrome.toolbar.locale_switcher_label"),": ",Gn(n.locale,t)]});const a=l=>{if(l===n.locale)return;const d=n.locale;o({type:"SET_LOCALE",locale:l}),Et({op:"locale_switch",fromLocale:d,toLocale:l,timestamp:Date.now()})};return i.jsxs("div",{className:"g7le-locale-switcher","data-testid":"g7le-locale-switcher",role:"group","aria-label":t("layout_editor.chrome.toolbar.locale_switcher_label"),style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("span",{style:{fontSize:11,color:"#475569"},children:t("layout_editor.chrome.toolbar.locale_switcher_label")}),i.jsx("div",{style:{display:"inline-flex",gap:2},children:s.map(l=>{const d=l===n.locale;return i.jsx("button",{type:"button","data-testid":`g7le-locale-${l}`,"aria-pressed":d,onClick:()=>a(l),style:{padding:"2px 8px",fontSize:12,cursor:"pointer",background:d?"#0f172a":"#f8fafc",color:d?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:Gn(l,t)},l)})})]})}const Vb=/\{\{?(p\d+)\}?\}/g;function Ub(e){const t=[];if(typeof e!="string"||e.length===0)return t;const n=new RegExp(Vb.source,"g");let o=0,r;for(;(r=n.exec(e))!==null;){const s=r.index,a=s+r[0].length;s>o&&t.push({kind:"text",raw:e.slice(o,s),start:o,end:s}),t.push({kind:"chip",raw:r[0],paramName:r[1],start:s,end:a}),o=a}return o<e.length&&t.push({kind:"text",raw:e.slice(o),start:o,end:e.length}),t}function qb(e){const t=Ub(e),n=[];let o="",r=!1;const s=a=>{n.push({kind:"text",text:a}),r=!0};for(const a of t)a.kind==="text"?(o=a.raw,s(o),o=""):(r||s(""),n.push({kind:"chip",paramName:a.paramName}),r=!1);return r||s(""),n}function Gb(e,t,n,o){const r=[];return e.forEach((s,a)=>{if(s.kind==="chip"){if(s.paramName===t)return;r.push(`{${s.paramName}}`)}else{const l=s.text??"";if(a===n){const d=Math.max(0,Math.min(o,l.length));r.push(l.slice(0,d),`{${t}}`,l.slice(d))}else r.push(l)}}),r.join("")}function mr({value:e,onChange:t,t:n,paramLabels:o,onRequestInsert:r,onRemoveChip:s,testIdSuffix:a="pc",disabled:l=!1,hideInsertButton:d=!1,caretRef:c}){const p=u.useMemo(()=>qb(e),[e]),f=u.useRef(null),[y,g]=u.useState(null),[m,h]=u.useState(null),[v,b]=u.useState(null),x=u.useMemo(()=>p.map(B=>B.kind==="chip"?`c:${B.paramName}`:"t").join("|"),[p]),k=u.useRef(null),w=u.useRef([]),_=u.useRef(!1);u.useLayoutEffect(()=>{const B=_.current;_.current=!1,!(!B&&k.current===x)&&(k.current=x,p.forEach((N,H)=>{if(N.kind!=="text")return;const L=w.current[H];L&&L.textContent!==(N.text??"")&&(L.textContent=N.text??"")}))},[x,p,e]);const C=()=>{const B=f.current;if(!B)return e;const N=[];for(const H of Array.from(B.querySelectorAll("[data-seg]")))H.dataset.seg==="chip"?N.push(`{${H.dataset.param??""}}`):N.push(H.textContent??"");return N.join("")},E=()=>{l||t(C())},S=u.useRef(null),T=u.useRef(!1),$=(B,N)=>{if(!l){B.preventDefault(),B.stopPropagation(),S.current={param:N,pointerId:B.pointerId},T.current=!1,g(N);try{B.currentTarget.setPointerCapture(B.pointerId)}catch{}}},A=B=>{const N=S.current;if(!N||B.pointerId!==N.pointerId)return;T.current=!0;const H=O(B.clientX,B.clientY);H&&(h({slotIdx:H.slotIdx,offset:H.offset}),b(Xb(f.current,H.slotIdx,H.offset)))},z=(B,N)=>{if(B.stopPropagation(),T.current){T.current=!1;return}s?.(N)},D=B=>{const N=S.current;if(!N||B.pointerId!==N.pointerId)return;try{B.currentTarget.releasePointerCapture(B.pointerId)}catch{}S.current=null,T.current&&typeof setTimeout=="function"&&setTimeout(()=>{T.current=!1},0);const H=O(B.clientX,B.clientY);if(g(null),h(null),b(null),!H)return;const L=f.current;if(!L)return;const P=Array.from(L.querySelectorAll("[data-seg]")).map(G=>G.dataset.seg==="chip"?{kind:"chip",paramName:G.dataset.param??""}:{kind:"text",text:G.textContent??""}),F=Gb(P,N.param,H.slotIdx,H.offset);F!==e&&(_.current=!0,t(F))},O=(B,N)=>{const H=f.current;if(!H)return null;const L=Array.from(H.querySelectorAll("[data-seg]")),I=H.ownerDocument,P=I.elementFromPoint(B,N);let F=P?.closest('[data-seg="text"]');if(F)return{slotIdx:L.indexOf(F),offset:Yb(I,F,B,N)};const G=P?.closest('[data-seg="chip"]');if(G){const Y=L.indexOf(G),Z=G.getBoundingClientRect();if(B<Z.left+Z.width/2&&Y-1>=0){const se=L[Y-1];return{slotIdx:Y-1,offset:(se.textContent??"").length}}if(Y+1<L.length)return{slotIdx:Y+1,offset:0}}const V=L.map((Y,Z)=>({s:Y,i:Z})).filter(Y=>Y.s.dataset.seg==="text").pop();return V?{slotIdx:V.i,offset:(V.s.textContent??"").length}:null},M=()=>{l||!r||r(pd(f.current,e.length))};return u.useEffect(()=>{if(c)return c.current=()=>pd(f.current,e.length),()=>{c.current=null}},[c,e.length]),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsxs("div",{ref:f,"data-testid":`g7le-chip-input-${a}`,className:"g7le-chip-input",style:Jb,children:[p.map((B,N)=>B.kind==="chip"?i.jsxs("span",{"data-seg":"chip","data-param":B.paramName,"data-testid":`g7le-chip-${a}-${B.paramName}`,contentEditable:!1,onPointerDown:H=>$(H,B.paramName??""),onPointerMove:A,onPointerUp:D,title:n("layout_editor.translation.placeholder_locked"),style:{...em,...y===B.paramName?tm:null,touchAction:"none"},children:[i.jsx("span",{"aria-hidden":"true",style:nm,children:"⠿"}),i.jsx("span",{"aria-hidden":"true",style:{marginRight:3},children:"🔗"}),i.jsx("span",{style:om,children:o?.[B.paramName??""]??B.paramName}),s&&!l&&i.jsx("button",{type:"button","data-testid":`g7le-chip-remove-${a}-${B.paramName}`,"aria-label":n("layout_editor.inline_binding.clear"),title:n("layout_editor.inline_binding.clear"),onPointerDown:H=>{H.stopPropagation(),H.preventDefault()},onClick:H=>z(H,B.paramName??""),style:rm,children:"✕"})]},`chip-${B.paramName}`):i.jsx("span",{ref:H=>{w.current[N]=H},"data-seg":"text",contentEditable:!l,suppressContentEditableWarning:!0,onInput:E,"data-testid":`g7le-chip-text-${a}-${N}`,style:{...Zb,...y?Qb:null,...m?.slotIdx===N?Rb:null}},`text-slot-${N}`)),v&&i.jsx("span",{"aria-hidden":"true","data-testid":`g7le-chip-caret-${a}`,style:{position:"absolute",left:v.left,top:v.top,width:2,height:v.height,background:"#2563eb",borderRadius:1,pointerEvents:"none",boxShadow:"0 0 0 1px rgba(37,99,235,0.3)"}})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[r&&!d&&i.jsxs("button",{type:"button","data-testid":`g7le-chip-insert-${a}`,onClick:M,disabled:l,style:im,children:["+ ",n("layout_editor.inline_binding.insert_data")]}),i.jsx("span",{style:sm,children:n("layout_editor.translation.placeholder_locked")})]})]})}function Yb(e,t,n,o){const r=(t.textContent??"").length;try{const a=e;let l=null,d=0;if(typeof a.caretRangeFromPoint=="function"){const c=a.caretRangeFromPoint(n,o);c&&(l=c.startContainer,d=c.startOffset)}else if(typeof a.caretPositionFromPoint=="function"){const c=a.caretPositionFromPoint(n,o);c&&(l=c.offsetNode,d=c.offset)}if(l&&t.contains(l))return Math.max(0,Math.min(d,r))}catch{}const s=t.getBoundingClientRect();return n>s.left+s.width/2?r:0}function Xb(e,t,n){if(!e)return null;const r=Array.from(e.querySelectorAll("[data-seg]"))[t];if(!r)return null;const s=e.getBoundingClientRect();try{const d=r.firstChild,c=e.ownerDocument;if(d&&d.nodeType===Node.TEXT_NODE&&(d.textContent??"").length>0){const p=c.createRange(),f=Math.max(0,Math.min(n,(d.textContent??"").length));p.setStart(d,f),p.setEnd(d,f);const y=p.getClientRects(),g=y.length>0?y[0]:p.getBoundingClientRect();if(g&&(g.height>0||g.width>=0))return{left:g.left-s.left,top:g.top-s.top,height:g.height||r.getBoundingClientRect().height}}}catch{}const a=r.getBoundingClientRect();return{left:(n>0?a.right:a.left)-s.left,top:a.top-s.top,height:a.height||16}}function pd(e,t){if(!e)return t;try{const n=e.ownerDocument.defaultView?.getSelection();if(!n||n.rangeCount===0)return t;const o=n.getRangeAt(0);let r=o.startContainer,s=null;for(;r&&r!==e;){if(r instanceof HTMLElement&&r.dataset.seg==="text"){s=r;break}r=r.parentNode}if(!s||!e.contains(s))return t;const a=Array.from(e.querySelectorAll("[data-seg]"));let l=0;for(const d of a){if(d===s)break;l+=d.dataset.seg==="chip"?`{${d.dataset.param??""}}`.length:(d.textContent??"").length}return l+o.startOffset}catch{return t}}const Jb={position:"relative",display:"flex",flexWrap:"wrap",alignItems:"center",gap:2,minHeight:30,padding:"4px 8px",border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",fontSize:13,lineHeight:1.6},Zb={outline:"none",minWidth:8,minHeight:"1.2em",whiteSpace:"pre-wrap",color:"#0f172a",display:"inline-block"},Qb={boxShadow:"inset 0 -2px 0 #93c5fd",background:"#eff6ff",borderRadius:2},Rb={background:"#dbeafe",boxShadow:"inset 0 -2px 0 #2563eb"},em={display:"inline-flex",alignItems:"center",gap:1,background:"linear-gradient(180deg,#eff6ff,#dbeafe)",color:"#1d4ed8",border:"1px dashed #60a5fa",borderRadius:9999,padding:"1px 8px 1px 4px",margin:"0 1px",fontSize:12,fontWeight:600,cursor:"grab",userSelect:"none",whiteSpace:"nowrap",boxShadow:"0 1px 2px rgba(37,99,235,0.18)",verticalAlign:"middle"},tm={opacity:.5,borderStyle:"solid",cursor:"grabbing"},nm={color:"#2563eb",opacity:.7,fontSize:11,marginRight:1,cursor:"grab"},om={textDecoration:"none"},rm={marginLeft:4,width:14,height:14,lineHeight:"12px",padding:0,border:"none",borderRadius:9999,background:"rgba(29,78,216,0.12)",color:"#1d4ed8",fontSize:9,fontWeight:700,cursor:"pointer",display:"inline-flex",alignItems:"center",justifyContent:"center",flex:"0 0 auto"},im={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#334155",padding:"3px 10px",cursor:"pointer"},sm={fontSize:10,color:"#94a3b8"};function fd(e){e&&(e.style.height="auto",e.style.height=`${Math.max(e.scrollHeight,28)}px`)}function am(e){fd(e)}function lm(e){const t=new Set;for(const n of e)for(const o of Object.keys(n.values??{}))t.add(o);return Array.from(t).sort()}function dm({templateIdentifier:e,layoutName:t,t:n,referencedKeys:o,onClose:r}){const[s,a]=u.useState([]),[l,d]=u.useState("idle"),[c,p]=u.useState(null),[f,y]=u.useState("all"),[g,m]=u.useState(new Set),[h,v]=u.useState({}),[b,x]=u.useState(null),[k,w]=u.useState({}),_=u.useCallback(async()=>{d("loading"),p(null);const N=await Oy(e,t);N.ok?(a(N.data),v({}),m(new Set),w({}),d("idle")):(p(N.message),d("error"))},[e,t]);u.useEffect(()=>{_()},[_]);const C=u.useMemo(()=>lm(s),[s]),E=u.useMemo(()=>s.filter(N=>f==="all"?!0:N.status===f).slice().sort((N,H)=>{const L=N.updated_at?Date.parse(N.updated_at):0,I=H.updated_at?Date.parse(H.updated_at):0;return I!==L?I-L:H.id-N.id}),[s,f]),S=u.useMemo(()=>s.filter(N=>N.status==="orphaned"),[s]),T=u.useCallback(N=>{const H={...N.values??{}};for(const L of Object.keys(H)){const I=It(N.translation_key,L);I!==void 0&&(H[L]=I)}return H},[]),$=u.useCallback(N=>h[N.id]??T(N),[h,T]),A=u.useCallback((N,H,L)=>{v(I=>{const P=I[N]??s.find(F=>F.id===N)?.values??{};return{...I,[N]:{...P,[H]:L}}})},[s]),z=u.useCallback(async N=>{x(N.id),w(I=>({...I,[N.id]:""}));const H=$(N),L=await By(e,N.id,H,N.lock_version);if(L.ok)a(I=>I.map(P=>P.id===N.id?L.data:P)),v(I=>{const P={...I};return delete P[N.id],P});else{const I=L.status===409?n("layout_editor.translation_manager.conflict"):L.message;w(P=>({...P,[N.id]:I}))}x(null)},[e,$,n]),D=u.useCallback(N=>typeof window<"u"&&typeof window.confirm=="function"?window.confirm(n("layout_editor.translation_manager.delete_confirm",{count:N})):!0,[n]),O=u.useCallback(async N=>{if(!D(1))return;const H=await Wy(e,N.id);H.ok?(a(L=>L.filter(I=>I.id!==N.id)),m(L=>{const I=new Set(L);return I.delete(N.id),I})):p(H.message)},[e,D]),M=u.useCallback(async N=>{if(N.length===0||!D(N.length))return;const H=await jy(e,N);if(H.ok){const L=new Set(N);a(I=>I.filter(P=>!L.has(P.id))),m(new Set)}else p(H.message)},[e,D]),B=u.useCallback(N=>{m(H=>{const L=new Set(H);return L.has(N)?L.delete(N):L.add(N),L})},[]);return i.jsxs("div",{className:"g7le-translation-manager","data-testid":"g7le-translation-manager",style:cm,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:um,children:[i.jsx("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-translation-manager-title",children:n("layout_editor.translation_manager.title")}),i.jsx("button",{type:"button","aria-label":"close",onClick:r,style:pm,"data-testid":"g7le-translation-manager-close",children:"✕"})]}),i.jsxs("div",{style:fm,children:[i.jsx("div",{style:{display:"inline-flex",gap:4},role:"tablist","aria-label":n("layout_editor.translation_manager.filter_label"),children:["all","active","orphaned"].map(N=>i.jsx("button",{type:"button",role:"tab","aria-selected":f===N,"data-active":f===N?"true":"false",onClick:()=>y(N),style:f===N?gm:gd,"data-testid":`g7le-translation-filter-${N}`,children:n(`layout_editor.translation_manager.filter_${N}`)},N))}),i.jsx("div",{style:{flex:1}}),g.size>0&&i.jsxs("button",{type:"button",onClick:()=>{M(Array.from(g))},style:hd,"data-testid":"g7le-translation-bulk-delete",children:["🗑 ",n("layout_editor.translation_manager.delete_selected",{count:g.size})]}),S.length>0&&i.jsxs("button",{type:"button",onClick:()=>{M(S.map(N=>N.id))},style:hd,"data-testid":"g7le-translation-purge-orphaned",children:["🧹 ",n("layout_editor.translation_manager.purge_orphaned",{count:S.length})]})]}),c&&i.jsx("div",{style:hm,"data-testid":"g7le-translation-manager-error",children:c}),i.jsx("div",{style:ym,children:l==="loading"?i.jsx("div",{style:yd,"data-testid":"g7le-translation-manager-loading",children:n("layout_editor.translation_manager.loading")}):E.length===0&&l!=="error"?i.jsx("div",{style:yd,"data-testid":"g7le-translation-manager-empty",children:n("layout_editor.translation_manager.empty")}):i.jsx("div",{"data-testid":"g7le-translation-list",style:bm,children:E.map(N=>{const H=$(N),L=N.status==="orphaned",I=o?o.has(N.translation_key):null;return i.jsxs("div",{style:mm,"data-testid":`g7le-translation-row-${N.id}`,children:[i.jsxs("div",{style:vm,children:[i.jsx("input",{type:"checkbox",checked:g.has(N.id),onChange:()=>B(N.id),"aria-label":n("layout_editor.translation_manager.select_aria",{key:N.translation_key}),"data-testid":`g7le-translation-select-${N.id}`}),i.jsx("code",{style:_m,children:N.translation_key}),L&&i.jsx("span",{style:xm,"data-testid":`g7le-translation-badge-orphaned-${N.id}`,children:n("layout_editor.translation_manager.badge_orphaned")}),I===!0&&i.jsx("span",{style:wm,"data-testid":`g7le-translation-badge-live-inuse-${N.id}`,children:n("layout_editor.translation_manager.badge_live_in_use")}),I===!1&&i.jsx("span",{style:km,"data-testid":`g7le-translation-badge-live-unused-${N.id}`,children:n("layout_editor.translation_manager.badge_live_unused")}),i.jsx("div",{style:{flex:1}}),i.jsx("button",{type:"button",onClick:()=>{O(N)},style:Tm,"data-testid":`g7le-translation-delete-${N.id}`,children:"🗑"})]}),i.jsxs("div",{style:Sm,children:[C.map(P=>{const F=H[P]??"",G=An(F).length>0;return i.jsxs("div",{style:Cm,"data-testid":`g7le-translation-field-${N.id}-${P}`,children:[i.jsxs("label",{style:$m,title:P,children:[Gn(P,n),i.jsxs("span",{style:Em,children:["(",P,")"]})]}),G?i.jsx("div",{style:{flex:1},"data-testid":`g7le-translation-chip-${N.id}-${P}`,children:i.jsx(mr,{value:F,onChange:V=>A(N.id,P,V),t:n,testIdSuffix:`${N.id}-${P}`})}):i.jsx("textarea",{value:F,rows:1,ref:am,onChange:V=>{A(N.id,P,V.currentTarget.value),fd(V.currentTarget)},style:Im,"data-testid":`g7le-translation-input-${N.id}-${P}`})]},P)}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[i.jsx("button",{type:"button",disabled:b===N.id,onClick:()=>{z(N)},style:Am,"data-testid":`g7le-translation-save-${N.id}`,children:b===N.id?n("layout_editor.translation_manager.saving"):n("layout_editor.translation_manager.save")}),k[N.id]?i.jsx("span",{style:{fontSize:11,color:"#dc2626"},"data-testid":`g7le-translation-row-error-${N.id}`,children:k[N.id]}):null]})]})]},N.id)})})}),i.jsx("div",{style:Dm,children:i.jsx("button",{type:"button",onClick:r,style:Nm,"data-testid":"g7le-translation-manager-footer-close",children:n("layout_editor.translation_manager.close")})})]})}const cm={display:"flex",flexDirection:"column",minHeight:0,flex:1},um={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0"},pm={border:"none",background:"transparent",fontSize:16,color:"#94a3b8",cursor:"pointer"},fm={padding:"10px 16px",borderBottom:"1px solid #f1f5f9",display:"flex",gap:8,alignItems:"center"},gd={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},gm={...gd,borderColor:"#2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},hd={padding:"4px 10px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},hm={padding:"8px 16px",fontSize:12,color:"#b91c1c",background:"#fef2f2"},ym={padding:16,overflow:"auto",flex:1,minHeight:120,display:"flex",flexDirection:"column",gap:12},yd={fontSize:13,color:"#94a3b8",textAlign:"center",padding:"32px 0"},bm={display:"flex",flexDirection:"column",gap:20},mm={border:"1px solid #e2e8f0",borderRadius:8,padding:10,display:"flex",flexDirection:"column",gap:6},vm={display:"flex",alignItems:"center",gap:8},_m={fontSize:11,background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a"},xm={fontSize:10,color:"#b45309",background:"#fef3c7",padding:"1px 6px",borderRadius:4,fontWeight:600},wm={fontSize:10,color:"#15803d",background:"#dcfce7",padding:"1px 6px",borderRadius:4,fontWeight:600},km={fontSize:10,color:"#9f1239",background:"#ffe4e6",padding:"1px 6px",borderRadius:4,fontWeight:600},Sm={display:"flex",flexDirection:"column",gap:6},Cm={display:"flex",alignItems:"flex-start",gap:8},$m={fontSize:12,color:"#475569",minWidth:88,fontWeight:600,paddingTop:4,display:"flex",flexDirection:"column",lineHeight:1.2},Em={fontSize:10,color:"#94a3b8",fontWeight:400},Im={flex:1,padding:"4px 8px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:4,color:"#0f172a",resize:"vertical",overflow:"hidden",minHeight:28,lineHeight:1.4,fontFamily:"inherit",boxSizing:"border-box"},Am={padding:"5px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},Tm={padding:"4px 8px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Dm={padding:"10px 16px",borderTop:"1px solid #e2e8f0",display:"flex",justifyContent:"flex-end"},Nm={padding:"6px 14px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};function zm(){const e=Ft(),{state:t}=Je(),n=e?.document?.raw??null,o=t.templateIdentifier??"",r=t.selectedRoute?.layoutName??"",s=u.useCallback((c,p)=>{const f=n?.[c];return f===void 0?p:f},[n]),a=u.useCallback((c,p,...f)=>{e&&(f.length>=1?e.patchDocumentRaw(c,p,f[0]):e.patchDocumentRaw(c,p))},[e]),l=u.useCallback(async(c,p)=>!o||!r?null:qn(o,r,c,p),[o,r]),d=u.useCallback(async(c,p,f)=>o?yr(o,c,p,f):null,[o]);return{raw:n,getValue:s,patch:a,createI18nKey:l,updateI18nKeyValue:d}}function Mm(e){const t=e.__source;return t&&(t.kind==="module"||t.kind==="plugin")&&t.identifier?`${t.kind}:${t.identifier}`:"template"}function bd(e,t){if(!t)return null;const n=Mm(e);if(n&&t.bySource){const a=t.bySource[n]?.[e.id];if(a!==void 0)return{value:a,match:{dataSourceId:e.id,resolvedBy:"spec_byId",sourceKey:n}}}const o=t.byDataSourceId?.[e.id];if(o!==void 0)return{value:o,match:{dataSourceId:e.id,resolvedBy:"spec_byId"}};const r=e.endpoint??"";if(t.byEndpointPattern&&typeof r=="string"&&r.length>0){for(const[s,a]of Object.entries(t.byEndpointPattern))if(Lm(r,s))return{value:a,match:{dataSourceId:e.id,resolvedBy:"spec_byEndpoint"}}}return null}function ts(e,t){const n=bd(e,t);if(n)return n;const o=e.fallback?.data;return o!==void 0?{value:{data:Array.isArray(o)?o:[o]},match:{dataSourceId:e.id,resolvedBy:"fallback"}}:{value:{},match:{dataSourceId:e.id,resolvedBy:"inferred"}}}function Lm(e,t){const n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${n}$`).test(e)}function Pm(e={}){const{spec:t,overrideSpec:n,onMatch:o}=e;return{has(r){return!0},resolve(r){const s=n?bd(r,n):null,{value:a,match:l}=s??ts(r,t);if(o)try{o(l)}catch{}return a}}}const md=Lt(),vd=md.DataBindingEngine,_d=md.dataBindingEngine;bo("PipeRegistry");function Om(e){let t=null,n=0;for(let o=0;o<e.length;o++){const r=e[o],s=o>0?e[o-1]:"",a=o<e.length-1?e[o+1]:"";if(s!=="\\"){if((r==='"'||r==="'")&&!t){t=r;continue}if(r===t){t=null;continue}if(!t){if(r==="("||r==="["||r==="{"){n++;continue}if(r===")"||r==="]"||r==="}"){n--;continue}if(r==="|"&&a!=="|"&&s!=="|"&&n===0)return!0}}}return!1}const Bm=new Set(["comment"]);function xd(e){if(!e||typeof e!="object")return[];const t=[];for(const[n,o]of Object.entries(e)){if(Bm.has(n)||!o||typeof o!="object")continue;const r=o;typeof r.expr!="string"||r.expr.length===0||t.push({id:n,label:typeof r.label=="string"?r.label:void 0,params:Array.isArray(r.params)?r.params:[],expr:r.expr,group:typeof r.group=="string"?r.group:void 0,source:r.__source})}return t}function Wm(e,t){let n=e.replace(/\{(\w+)\*\}(\s*\?\?\s*)?/g,(o,r,s)=>{const a=t[r],l=a==null?[]:String(a).split(",").map(d=>d.trim()).filter(Boolean);return l.length===0?"":l.join(" ?? ")+(s?" ?? ":"")});return n=n.replace(/\{(\w+)\}/g,(o,r)=>{const s=t[r];return s===void 0?o:s}),n}function vr(e){const t=e.trim(),n=/^\{\{([\s\S]*)\}\}$/.exec(t);return n?n[1].trim():t}function wd(e){const t=e.trim();return/^\{\{[\s\S]*\}\}$/.test(t)?t:`{{ ${t} }}`}function kd(e,t){const n={};for(const[r,s]of Object.entries(t))n[r]=typeof s=="string"?vr(s):s;const o=Wm(e.expr,n);return wd(o)}function jm(e,t){if(typeof e!="string")return null;const n=Sd(vr(e));for(const o of t){const r=Hm(n,o.expr,o.params);if(r)return{recipeId:o.id,params:r}}return null}function Sd(e){return e.replace(/\s+/g," ").trim()}function Cd(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Hm(e,t,n){const o=[],r=new Set;let s="";const a=/\{(\w+)(\*?)\}(\s*\?\?\s*)?/g;let l=0,d;const c=Sd(t);for(;(d=a.exec(c))!==null;){const g=c.slice(l,d.index);s+=Cd(g).replace(/\\ /g,"\\s*");const m=d[2]==="*",h=!!d[3];m?(r.add(d[1]),s+="(.*?)",h&&(s+="(?:\\s*\\?\\?\\s*)?")):(s+="(.+?)",h&&(s+="\\s*\\?\\?\\s*")),o.push(d[1]),l=a.lastIndex}s+=Cd(c.slice(l)).replace(/\\ /g,"\\s*");let p;try{p=new RegExp(`^${s}$`)}catch{return null}const f=p.exec(e);if(!f)return null;const y={};if(o.forEach((g,m)=>{let h=(f[m+1]??"").trim();r.has(g)&&(h=h.split(/\s*\?\?\s*/).map(v=>v.trim()).filter(Boolean).join(", ")),y[g]=h}),n.length>0){const g=new Set(n.map(m=>m.key));for(const m of Object.keys(y))g.has(m)||delete y[m]}return y}function Fm(e){const t=`x.${e.field}`,n=Km(e.value);switch(e.cmp){case"=":return`${t} === ${n}`;case"!=":return`${t} !== ${n}`;case">":return`${t} > ${n}`;case"<":return`${t} < ${n}`;case">=":return`${t} >= ${n}`;case"<=":return`${t} <= ${n}`;case"includes":return`(${t} ?? []).includes(${n})`;default:return`${t} === ${n}`}}function Km(e){const t=e.trim();return t==="true"||t==="false"||t!==""&&!Number.isNaN(Number(t))||/^['"].*['"]$/.test(t)||/[.([{]/.test(t)?t:`'${t.replace(/'/g,"\\'")}'`}function ns(e){return!e||e.length===0?"true":e.map(Fm).join(" && ")}function Vm(e){const n=`(${(e.source??"").trim()||"[]"} ?? [])`;let o;switch(e.op){case"count":o=`${n}.filter(x => ${ns(e.conditions)}).length`;break;case"sum":{const r=(e.sumField??"").trim()||"value";o=`${n}.filter(x => ${ns(e.conditions)}).reduce((s, x) => s + (x.${r} ?? 0), 0)`;break}case"filter":o=`${n}.filter(x => ${ns(e.conditions)})`;break;case"toOptions":{const r=(e.valueField??"").trim()||"value",s=(e.labelField??"").trim()||"label";o=`${n}.map(x => ({ value: x.${r}, label: x.${s} }))`;break}case"nth":{const r=(e.index??"").trim()||"0",s=(e.prop??"").trim();o=`${n}[${r}]${s?`?.${s}`:""}`;break}case"firstOf":{const r=(e.candidates??[]).map(l=>l.trim()).filter(Boolean),s=r.length>0?r.join(" ?? "):"''",a=(e.fallback??"").trim();o=a?`${s} ?? '${a.replace(/'/g,"\\'")}'`:s;break}case"literal":{const r=e.literalKind??"string",s=(e.literalValue??"").trim();r==="number"?o=s===""?"0":s:r==="boolean"?o=s==="true"?"true":"false":o=`'${s.replace(/'/g,"\\'")}'`;break}default:o="[]"}return wd(o)}function Um(e){if(typeof e!="string")return null;const t=vr(e).trim();let n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)\.length$/.exec(t);return n?{key:"",op:"count",source:n[1].trim(),conditions:os(n[2])}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)\.reduce\(\(s, x\) => s \+ \(x\.(\w+(?:\.\w+)*) \?\? 0\), 0\)$/.exec(t),n?{key:"",op:"sum",source:n[1].trim(),conditions:os(n[2]),sumField:n[3]}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.filter\(x => (.+)\)$/.exec(t),n?{key:"",op:"filter",source:n[1].trim(),conditions:os(n[2])}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\.map\(x => \(\{ value: x\.(\w+(?:\.\w+)*), label: x\.(\w+(?:\.\w+)*) \}\)\)$/.exec(t),n?{key:"",op:"toOptions",source:n[1].trim(),valueField:n[2],labelField:n[3]}:(n=/^\((.+?)\s*\?\?\s*\[\]\)\[(.+?)\](?:\?\.(\w+(?:\.\w+)*))?$/.exec(t),n?{key:"",op:"nth",source:n[1].trim(),index:n[2].trim(),prop:n[3]}:null))))}function os(e){const t=e.trim();if(t==="true")return[];const n=[];for(const o of qm(t)){const r=Gm(o.trim());r&&n.push(r)}return n}function qm(e){const t=[];let n=0,o=0;for(let r=0;r<e.length;r++){const s=e[r];s==="("||s==="["||s==="{"?n++:s===")"||s==="]"||s==="}"?n--:n===0&&s==="&"&&e[r+1]==="&"&&(t.push(e.slice(o,r)),r++,o=r+1)}return t.push(e.slice(o)),t}function Gm(e){let t=/^\(x\.(\w+(?:\.\w+)*) \?\? \[\]\)\.includes\((.+)\)$/.exec(e);if(t)return{field:t[1],cmp:"includes",value:$d(t[2])};if(t=/^x\.(\w+(?:\.\w+)*)\s*(===|!==|>=|<=|>|<)\s*(.+)$/.exec(e),t){const n={"===":"=","!==":"!=",">":">","<":"<",">=":">=","<=":"<="};return{field:t[1],cmp:n[t[2]]??"=",value:$d(t[3])}}return null}function $d(e){const t=e.trim(),n=/^'(.*)'$/.exec(t)||/^"(.*)"$/.exec(t);return n?n[1]:t}function Ed(e,t){const n=jm(e,t);if(n)return{kind:"preset",recipeId:n.recipeId,params:n.params};const o=Um(e);return o?{kind:"custom",model:o}:{kind:"advanced"}}function Ym(e){if(e==null)return"null";if(Array.isArray(e))return"list";switch(typeof e){case"number":return"number";case"boolean":return"boolean";case"string":return"string";default:return"object"}}function Id(e,t){if(typeof e!="string"||e.trim()==="")return{ok:!1,reason:"empty"};const n=vr(e);try{const o=Om(n)?_d.evaluatePipeExpression(n,t??{},{skipCache:!0}):_d.evaluateExpression(n,t??{},{skipCache:!0});return{ok:!0,value:o,type:Ym(o)}}catch(o){return{ok:!1,reason:o instanceof Error?o.message:String(o)}}}function Ad(e,t){const n=Array.isArray(e?.data_sources)?e.data_sources:[],o=[];for(const r of n){const s=r?.id;if(typeof s!="string"||s.length===0)continue;const{value:a}=ts(r,t?.sampleData),l=typeof r.label_key=="string"?r.label_key:void 0;o.push({id:s,sample:a,labelKey:l})}return o}function Td(e,t){const n=[];t?.sampleGlobal&&typeof t.sampleGlobal=="object"&&n.push({scope:"_global",tree:t.sampleGlobal});const o={},r={},s={},a=(l,d)=>{if(!(!d||typeof d!="object"||Array.isArray(d)))for(const[c,p]of Object.entries(d))if(p&&typeof p=="object"&&!Array.isArray(p)){const f=l[c]&&typeof l[c]=="object"&&!Array.isArray(l[c])?l[c]:l[c]={};a(f,p)}else c in l||(l[c]=p)};for(const l of t?.states?.groups??[])for(const d of l.items??[]){const c=d.initialState;c?.local&&a(o,c.local),c?.query&&a(r,c.query),c?.route&&a(s,c.route)}return Object.keys(o).length&&n.push({scope:"_local",tree:o}),Object.keys(r).length&&n.push({scope:"query",tree:r}),Object.keys(s).length&&n.push({scope:"route",tree:s}),e?.computed&&typeof e.computed=="object"&&!Array.isArray(e.computed)&&n.push({scope:"_computed",tree:e.computed}),n}function Dd(e,t){const n={};for(const r of t)r.scope!=="_computed"&&(n[r.scope]=r.tree);const o={};for(const[r,s]of Object.entries(e)){if(typeof s!="string"){o[r]=s;continue}const a=Id(s,n);o[r]=a.ok?a.value:s}return o}function Xm(e,t){const n=Ad(e,t),o=Td(e,t),r={};for(const s of n)s.sample!==void 0&&(r[s.id]=s.sample);for(const s of o)s.scope==="_computed"?r[s.scope]=Dd(s.tree,o):r[s.scope]=s.tree;return r}function Nd(e){const{raw:t,spec:n,evaluateComputed:o}=e;return u.useMemo(()=>{const r=Ad(t,n),s=Td(t,n);if(o){const a=s.findIndex(l=>l.scope==="_computed");if(a>=0){const l=Dd(s[a].tree,s);s[a]={scope:"_computed",tree:l}}}return ji({dataSources:r,states:s,spec:n??void 0})},[t,n,o])}function Jm(e){const{raw:t,spec:n,pageType:o}=e;return u.useMemo(()=>{const r=Array.isArray(t?.data_sources)?t.data_sources:[],s=[];for(const d of r){const c=d?.id;if(typeof c!="string"||c.length===0)continue;const{value:p}=ts(d,n?.sampleData),f=typeof d.label_key=="string"?d.label_key:void 0;s.push({id:c,sample:p,labelKey:f})}const a=[];n?.sampleGlobal&&typeof n.sampleGlobal=="object"&&a.push({scope:"_global",tree:n.sampleGlobal}),t?.computed&&typeof t.computed=="object"&&!Array.isArray(t.computed)&&a.push({scope:"_computed",tree:t.computed});const l=ji({dataSources:s,states:a,spec:n??void 0});if(typeof o=="string"&&o!=="")for(const d of["title","description"])l.push({expression:`{{_seo.${o}.${d}}}`,source:"_seo",sourceId:"_seo",path:`${o}.${d}`,shape:"scalar",preview:""});return l},[t,n,o])}function zd(e,t){const n=[],o=new Set,r=s=>{for(const a of s??[]){if(a.kind==="route"&&!a.isRedirect&&typeof a.path=="string"&&a.path.startsWith("/")&&!o.has(a.path)){o.add(a.path);const l=a.label.startsWith("$t:")?a.label.slice(3):a.label,d=a.label.startsWith("$t:")?t(l):l,c=d&&d!==l&&!d.startsWith("$t:")?d:"";n.push({value:a.path,label:c?`${c} (${a.path})`:a.path})}r(a.children)}};return r(e),n}function Md(e){const t=Array.isArray(e?.data_sources)?e.data_sources:[],n=[];for(const o of t){const r=o?.id;typeof r=="string"&&r.length>0&&n.push({value:r,label:r})}return n}function rs(e,t){const n=typeof e.id=="string"?e.id:"",o=typeof e.label_key=="string"?e.label_key:"";if(!o)return null;const r=o.startsWith("$t:")?o.slice(3):o,s=t(r);return s&&s!==r&&!s.startsWith("$t:")&&s!==n?s:null}function is(e,t){const n=e.__source;if(!n||typeof n!="object"||Array.isArray(n))return null;const o=n;if(o.kind!=="extension")return null;const r=o.extensionSourceType==="plugin"?t("layout_editor.data_sources.source.plugin"):t("layout_editor.data_sources.source.module"),s=typeof o.extensionIdentifier=="string"?o.extensionIdentifier:"",a=typeof o.extensionName=="string"?o.extensionName:"",l=a&&a!==s?`${a} (${s})`:s;return l?`${r}: ${l}`:r}function Zm(e,t,n){const o=Array.isArray(e?.data_sources)?e.data_sources:[],r=[],s=new Set;for(const a of o){const l=a?.id;typeof l!="string"||l.length===0||s.has(l)||(s.add(l),r.push({id:l,friendly:rs(a,t),source:is(a,n)}))}return r}function Ld(e){const t=e?.states?.groups??[],n=new Set;for(const o of t)for(const r of o.items??[]){for(const s of Object.keys(r.initialState?.local??{}))n.add(s);for(const s of Object.keys(r.initialState?.global??{}))n.add(s)}return Array.from(n).map(o=>({value:o,label:o}))}function Qm(e,t){const n=Array.isArray(e?.modals)?e.modals:[],o=[],r=new Set;for(const s of n){const a=s?.id;typeof a!="string"||a.length===0||r.has(a)||(r.add(a),o.push({value:a,label:Rm(s,a,t)}))}return o}function Rm(e,t,n){const r=e?.meta?.editor_label;if(typeof r=="string"&&r.length>0)return Pd(r,n);const s=e?.title;return typeof s=="string"&&s.length>0?Pd(s,n):t}function Pd(e,t){if(e.startsWith("$t:")){const n=e.slice(3),o=t?t(n):n;return o&&o!==n?o:n}return e}const Od=bo("ActionRecipeEngine"),ev={api:"apiCall",nav:"navigate",showToast:"toast",setLocalState:"setState"},tv=new Set(["comment"]);function wo(e){if(!e||typeof e!="object")return[];const t=[];for(const[n,o]of Object.entries(e)){if(tv.has(n)||!o||typeof o!="object")continue;const r=o,s=r.build&&typeof r.build=="object"?r.build:typeof r.handler=="string"?{handler:r.handler}:void 0;!s||typeof s.handler!="string"||s.handler.length===0||t.push({id:n,label:typeof r.label=="string"?r.label:void 0,params:Array.isArray(r.params)?r.params:[],build:s})}return t}function on(e){const t=/^\{\{\s*([\w.]+)\s*\}\}$/.exec(e);return t?t[1]:null}const _r="...";function nv(e,t){return e.replace(/\{\{\s*([\w.]+)\s*\}\}/g,(n,o)=>{const r=t[o];return r==null?"":String(r)})}function ss(e,t){if(typeof e=="string"){const n=on(e);return n!==null?n in t?t[n]:void 0:e.includes("{{")?nv(e,t):e}if(Array.isArray(e))return e.map(n=>ss(n,t)).filter(n=>n!==void 0);if(e&&typeof e=="object"){const n={};for(const[o,r]of Object.entries(e)){if(o===_r&&typeof r=="string"){const d=on(r),c=d!==null?t[d]:void 0;if(c&&typeof c=="object"&&!Array.isArray(c))for(const[p,f]of Object.entries(c))f===void 0||p===""||(n[p]=f);continue}const s=ss(r,t);if(s===void 0||Array.isArray(s)&&s.length===0&&(o==="onSuccess"||o==="onError"))continue;const a=on(o),l=a!==null?a in t?String(t[a]):null:o;l===null||l===""||s&&typeof s=="object"&&!Array.isArray(s)&&Object.keys(s).length===0||(n[l]=s)}return n}return e}function ov(e){const t=e.handler;if(typeof t!="string")return e;const n=ev[t];if(n)return Od.warn(`action recipe used forbidden handler "${t}" — corrected to "${n}" (core handler rules)`),{...e,handler:n};if(t==="apiCall"||t==="navigate"){const o=e.params;if(o&&typeof o=="object")for(const r of["target","onSuccess","onError"])r in o&&Od.warn(`action recipe build placed "${r}" inside params for handler "${t}" — should be top-level (core handler rules)`)}return e}function xr(e,t){const n=ss(e.build,t);if(typeof n.handler!="string"&&e.build.handler.includes("{{")&&(n.handler=e.build.handler),e.build.handler.includes("{{")){const o=new Set(e.params.filter(r=>r.required).map(r=>r.key));o.size>0&&Bd(e.build,n,o)}return ov(n)}function Bd(e,t,n){if(!e||typeof e!="object"||Array.isArray(e)||!t||typeof t!="object"||Array.isArray(t))return;const o=e,r=t;for(const[s,a]of Object.entries(o))if(s!=="handler"){if(typeof a=="string"){const l=on(a);l!==null&&n.has(l)&&!(s in r)&&(r[s]=a)}else if(a&&typeof a=="object"){if(!(s in r)||typeof r[s]!="object"||r[s]===null)if(JSON.stringify(a).includes("{{"))r[s]={};else continue;Bd(a,r[s],n)}}}function rv(e,t){if(!e||typeof e!="object")return null;const n=e.handler;if(typeof n!="string")return null;let o=null,r=-1;for(const s of t){const a=s.build.handler.includes("{{");if(!a&&s.build.handler!==n||a&&!iv(s.build,e))continue;const l=sv(s.build,e);if(l===null)continue;const d=Object.keys(l).length;d>r&&(r=d,o={recipeId:s.id,values:l})}return o}function iv(e,t){if(typeof t.handler=="string"&&t.handler===e.handler)return!0;const n=e.params;if(!n||typeof n!="object")return!1;const o=t.params;if(!o||typeof o!="object")return!1;const r=o,s=Object.entries(n).filter(([,a])=>typeof a=="string"&&on(a)!==null);return s.length===0?!1:s.every(([a])=>a in r)}function sv(e,t){const n={},o=(r,s)=>{if(typeof r=="string"){const a=on(r);return a!==null?(n[a]=s,!0):r.includes("{{")?typeof s=="string":r===s}if(Array.isArray(r))return Array.isArray(s);if(r&&typeof r=="object"){if(!s||typeof s!="object"||Array.isArray(s))return!1;const a=s,l=r[_r],d=typeof l=="string"?on(l):null,c=new Set(Object.keys(r).filter(p=>p!==_r));for(const[p,f]of Object.entries(r))if(p!==_r){if(typeof f=="string"&&on(f)!==null){const y=on(f);p in a&&(n[y]=a[p]);continue}if(!(p in a)){if(p==="onSuccess"||p==="onError"||p==="params"||p==="identity_target")continue;return!1}if(!o(f,a[p]))return!1}if(d!==null){const p={};for(const[f,y]of Object.entries(a))c.has(f)||(p[f]=y);Object.keys(p).length>0&&(n[d]=p)}return!0}return r===s};return o(e,t)?n:null}function Yn(e,t){const n=typeof e?.handler=="string"?e.handler:"",o=rv(e,t);return o?{kind:"preset",recipeId:o.recipeId,values:o.values,handler:n}:{kind:"advanced",handler:n}}function Wd(e,t,n){const o=e?.label,r=o?n?n(o):o:"",s=[],a=e?.params??[];for(const l of a){if(l.advanced||l.widget==="action-list")continue;const d=t[l.key];if(d==null||d==="")continue;const c=av(d);if(c!==""&&(s.push(c),s.length>=2))break}return s.length===0?r:`${r} (${s.join(", ")})`}function av(e){return typeof e=="string"?e:typeof e=="number"||typeof e=="boolean"?String(e):Array.isArray(e)?`[${e.length}]`:e&&typeof e=="object"?"{…}":""}function jd(e,t){const n={...e};return typeof t=="string"&&t.trim().length>0?n.if=t.trim():delete n.if,n}function as(e){const t=e?.if;return typeof t=="string"&&t.trim().length>0?t:null}const lv=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/,dv=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,cv=/\{\{.*?\}\}/;function Hd(e,t){const n=typeof e=="string"?e:"",o=lv.exec(n);if(o){const a=o[1],l=t?t(a):"";return{customKey:a,binding:!1,paramKey:!1,displayValue:l}}const r=mt(n);if(r&&r.key.startsWith("custom.")){const a=t?t(r.key):"";return{customKey:r.key,binding:!1,paramKey:!0,displayValue:a}}if(cv.test(n))return{customKey:null,binding:!0,paramKey:!1,displayValue:n};const s=dv.exec(n);if(s){const a=s[1],l=t?t(a):"";return{customKey:null,binding:!1,paramKey:!1,displayValue:l&&l!==a?l:""}}return{customKey:null,binding:!1,paramKey:!1,displayValue:n}}function uv(){const{state:e}=Je(),t=e.templateIdentifier,n=e.locale,o=e.selectedRoute?.layoutName??null,r=u.useCallback(l=>{try{const c=Pt.getInstance().translate(l,{templateId:t,locale:n});return c&&c!==l?c:""}catch{return""}},[t,n]),s=u.useCallback(l=>Hd(l,r),[r]),a=u.useCallback(async(l,d)=>{const c=Hd(l,r);if(c.binding)return{kind:"blocked"};if(c.customKey){if(d===c.displayValue)return{kind:"noop",customKey:c.customKey};const y=await yr(t,c.customKey,n,d);return y.kind==="error"?{kind:"error",message:y.message}:(await tn(t,n),{kind:"updated",customKey:c.customKey})}const p=await qn(t,o,n,d);if(p.kind==="error"||!p.resource)return{kind:"error",message:p.message};const f=p.resource.translation_key;return await tn(t,n),{kind:"created",customKey:f,token:`$t:${f}`}},[t,n,o,r]);return{locale:n,templateIdentifier:t,translate:r,classify:s,commitText:a}}async function ls(e,t,n,o,r,s,a,l,d,c){const p=c?Yi(o,c):null,f=!!(p&&p.keyifiable),y=d?d(o):o,g=f?p.chipValue:Xl(y),h=`p${f?p.bindings.length:cr(y).length}`,v=Math.max(0,Math.min(r,g.length)),b=Hl(g,v,h),x=ur(b).replace(/\{\{?p\d+\}?\}/g," ").replace(/\s+/g," ").trim(),k=x.length>0?x:b,w=await qn(e,t,n,k);if(w.kind==="error"||!w.resource)return{kind:"error",message:w.message};const _=w.resource.translation_key,C=An(b),E=w.resource.values??{},S={[n]:b};for(const z of Object.keys(E)){if(z===n)continue;let D=E[z]??"";for(const O of C)D=qi(D,O);S[z]=D}xo(e,_,S);const T=En(s,a,l);return{kind:"ok",text:`${f?Gl(`$t:${_}`,p):Yl(`$t:${_}`,y)}|${h}=${T}`,key:_}}async function wr(e,t,n,o,r,s,a){let l=mt(n);if(!l&&typeof n=="string"){const h=/^\s*\$t:(custom\.[A-Za-z0-9._-]+)\s*$/.exec(n);h&&(l={key:h[1],params:[]})}if(!l)return{kind:"error",message:"not a param key"};if(!l.key.startsWith("custom."))return{kind:"error",message:"not a custom param key"};const d=hb(l.params.map(h=>h.name)),c=En(r,s,a),p=`${n.trim()}|${d}=${c}`,f=await cn(e,l.key),y=jb(l.key)??{},g=new Set([...Object.keys(f?.values??{}),...Object.keys(y),t]);if(g.size===0)return{kind:"error",message:"custom key not found"};const m={};for(const h of g){const v=It(l.key,h)??f?.values?.[h]??"";m[h]=h===t?Hl(v,o,d):qi(v,d)}return xo(e,l.key,m),{kind:"ok",text:p,paramName:d}}async function ko(e,t,n,o){return br(e,t,n,o),{kind:"ok"}}async function pv(e,t,n){const o=await cn(e,t);if(!o)return{kind:"error",message:"custom key not found"};const r={};for(const[s,a]of Object.entries(o.values??{})){const l=It(t,s)??a;r[s]=n(l)}xo(e,t,r);try{const s=Pt.getInstance();for(const[a,l]of Object.entries(r))s.setTranslationValue(e,a,t,l)}catch{}return{kind:"ok"}}function Fd(e,t,n){return pv(e,t,o=>Ql(o,n))}function Kd(e,t){if(!(typeof window>"u"))try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:e,locale:t}}))}catch{}}async function kr(e,t,n,o){const r=await Fd(e,t,n);return Kd(e,o),r}const fv=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/;function gv(e,t){const n=s=>{const a=[],l=/\{\{?(p\d+)\}?\}/g;let d;for(;(d=l.exec(s))!==null;)a.push(d[1]);return a},o=n(e),r=n(t);return o.length!==r.length?!0:o.some((s,a)=>s!==r[a])}function Vd(e){const t=e.text;if(typeof t!="string")return null;const n=fv.exec(t);if(n)return n[1];const o=mt(t);return o&&o.key.startsWith("custom.")?o.key:null}function ds(e){const t={},n=mt(typeof e=="string"?e:"");for(const o of n?.params??[])t[o.name]=Vt(o.expression);return t}function hv(e){return ds(typeof e.text=="string"?e.text:"")}function Ud({customKey:e,templateIdentifier:t,t:n,paramLabels:o,locales:r,onRemoveParam:s}){const a=r??ud(),l=Ft(),d=o??{},[c,p]=u.useState({status:"idle",values:{}}),[f,y]=u.useState(!1),[g,m]=u.useState(!1),[h,v]=u.useState(null),b=u.useRef({});u.useEffect(()=>{let $=!1;if(!e){p({status:"not_a_key",values:{}});return}return p({status:"loading",values:{}}),(async()=>{const A=await cn(t,e);if($)return;if(!A){p({status:"error",values:{}});return}const z={...A.values??{}};for(const D of Object.keys(z)){const O=It(e,D);O!==void 0&&(z[D]=O)}for(const D of a){const O=It(e,D);O!==void 0&&(z[D]=O)}b.current={...z},p({status:"loaded",id:A.id,lockVersion:A.lock_version,values:z})})(),()=>{$=!0}},[e,t]),u.useEffect(()=>{if(typeof window>"u"||!e)return;const $=A=>{const z=A.detail;z?.templateIdentifier&&z.templateIdentifier!==t||p(D=>{if(D.status!=="loaded")return D;let O=!1;const M={...D.values};for(const B of Object.keys(M)){const N=It(e,B);N!==void 0&&N!==M[B]&&(M[B]=N,O=!0)}return O?(b.current={...b.current,...M},{...D,values:M}):D})};return window.addEventListener(kt,$),()=>window.removeEventListener(kt,$)},[e,t]);const x=u.useCallback(($,A)=>{p(z=>{const D=z.values[$]??"",O={...z,values:{...z.values,[$]:A}};return e&&gv(D,A)&&(br(t,e,$,A),b.current={...b.current,[$]:A},l?.markDirty?.(),Kd(t,$)),O})},[e,t,l]),k=u.useCallback($=>{p(A=>{const z={};for(const[D,O]of Object.entries(A.values))z[D]=Ql(O,$);return b.current={...z},{...A,values:z}}),s?.($)},[s]),w=u.useMemo(()=>[...Object.values(c.values),...Object.values(b.current)].some(A=>An(A).length>0),[c.values]),_=u.useRef(null),C=u.useCallback(()=>{m(!0),_.current&&clearTimeout(_.current),_.current=setTimeout(()=>m(!1),1500)},[]);u.useEffect(()=>()=>{_.current&&clearTimeout(_.current)},[]);const E=u.useCallback(async()=>{if(m(!1),c.status!=="loaded"||c.id===void 0)return;const $=b.current;for(const[z,D]of Object.entries(c.values)){const O=$[z]??"";if(w){const M=An(O);if(M.length===0)continue;const B=An(D);if(!(M.length===B.length&&M.every((H,L)=>H===B[L]))){v(n("layout_editor.translation.placeholder_mismatch"));return}continue}if(Ui(O)&&!kb(O,D)){v(n("layout_editor.translation.token_mismatch"));return}}if(y(!0),v(null),w){xo(t,e??"",c.values),b.current={...c.values},l?.markDirty?.(),Et({op:"translation_field_update",translationKey:e,changedLocales:Object.keys(c.values),timestamp:Date.now()}),await new Promise(z=>setTimeout(z,0)),y(!1),C();return}const A=`/api/admin/templates/${encodeURIComponent(t)}/custom-translations/${c.id}`;try{const z=await fetch(A,{method:"PUT",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({values:c.values,expected_lock_version:c.lockVersion??0})}),D=await z.json().catch(()=>null);if(z.status===409){v(n("layout_editor.translation.conflict"));return}if(!z.ok){v(D?.message??n("layout_editor.translation.save_failed"));return}const O=typeof D?.data?.lock_version=="number"?D.data.lock_version:(c.lockVersion??0)+1;p(M=>({...M,lockVersion:O})),b.current={...c.values},await tn(t),Et({op:"translation_field_update",translationKey:e,changedLocales:Object.keys(c.values),timestamp:Date.now()}),C()}catch(z){v(z instanceof Error?z.message:n("layout_editor.translation.save_failed"))}finally{y(!1)}},[c,t,e,n]);if(c.status==="not_a_key"||!e)return i.jsx("div",{"data-testid":"g7le-translation-not-a-key",style:cs,children:n("layout_editor.translation.not_a_key")});if(c.status==="loading"||c.status==="idle")return i.jsx("div",{"data-testid":"g7le-translation-loading",style:cs,children:n("layout_editor.translation.loading")});if(c.status==="error")return i.jsx("div",{"data-testid":"g7le-translation-error",style:cs,children:n("layout_editor.translation.load_failed")});const S=new Set([...a,...Object.keys(c.values)]),T=Array.from(S);return i.jsxs("div",{"data-testid":"g7le-translation-field",style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsxs("div",{"data-testid":"g7le-translation-key",style:yv,children:[n("layout_editor.translation.key_label"),": ",i.jsx("code",{style:bv,children:e})]}),T.map($=>{const A=c.values[$]??"",z=A.trim().length===0;return i.jsxs("div",{className:"g7le-translation-row","data-testid":`g7le-translation-row-${$}`,style:mv,children:[i.jsxs("label",{htmlFor:`g7le-translation-input-${$}`,style:{fontSize:12,color:"#475569",minWidth:72,display:"inline-flex",alignItems:"center",gap:4},children:[Gn($,n),z&&i.jsx("span",{"data-testid":`g7le-translation-missing-${$}`,style:vv,children:n("layout_editor.translation.needs_translation")})]}),w?i.jsx("div",{style:{flex:1},"data-testid":`g7le-translation-chip-${$}`,children:i.jsx(mr,{value:A,onChange:D=>x($,D),t:n,paramLabels:d,testIdSuffix:$,onRemoveChip:s?k:void 0})}):i.jsx("input",{id:`g7le-translation-input-${$}`,type:"text","data-testid":`g7le-translation-input-${$}`,value:A,placeholder:z?n("layout_editor.translation.needs_translation"):"",onChange:D=>x($,D.currentTarget.value),onBlur:E,style:{flex:1,padding:"4px 8px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:4,color:z?"#94a3b8":"#0f172a"}})]},$)}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[i.jsx("button",{type:"button","data-testid":"g7le-translation-save",disabled:f,onClick:E,style:_v,children:n(f?"layout_editor.translation.saving":"layout_editor.translation.save")}),g&&!f&&i.jsxs("span",{"data-testid":"g7le-translation-saved",style:{fontSize:11,color:"#16a34a",display:"inline-flex",alignItems:"center",gap:4},children:["✓ ",n("layout_editor.translation.saved")]}),h&&i.jsx("span",{"data-testid":"g7le-translation-save-error",style:{fontSize:11,color:"#dc2626"},children:h})]})]})}const cs={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},yv={fontSize:12,color:"#475569"},bv={fontSize:11,background:"#f1f5f9",padding:"1px 4px",borderRadius:3,color:"#0f172a"},mv={display:"flex",alignItems:"center",gap:8},vv={fontSize:10,color:"#b45309",background:"#fef3c7",padding:"0 4px",borderRadius:3},_v={padding:"6px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"};function Xn({anchorRef:e,open:t,onClose:n,children:o,placement:r="bottom",minWidth:s=240,maxWidth:a=360,margin:l=8,testid:d}){const c=u.useRef(null),[p,f]=u.useState(null),y=u.useCallback(()=>{const m=e.current,h=c.current;if(!m||!h)return;const v=m.getBoundingClientRect(),b=window.innerWidth,x=window.innerHeight,k=h.getBoundingClientRect(),w=Math.min(Math.max(k.width,s),a),_=k.height;let C=v.left;C+w+l>b&&(C=v.right-w),C=Math.min(Math.max(C,l),b-w-l);const E=x-v.bottom-l,S=v.top-l;let T,$;const A=r==="bottom",z=E>=Math.min(_,160),D=S>=Math.min(_,160);A&&z||A&&!D?(T=v.bottom+4,$=E):!A&&D||D?(T=v.top-4-Math.min(_,S),$=S):E>=S?(T=v.bottom+4,$=E):(T=l,$=S),f({left:C,top:T,maxHeight:Math.max($,120)})},[e,r,s,a,l]);if(u.useLayoutEffect(()=>{if(!t){f(null);return}y()},[t,y]),u.useEffect(()=>{if(!t)return;const m=()=>y();window.addEventListener("resize",m),window.addEventListener("scroll",m,!0);const h=b=>{const x=c.current,k=e.current,w=b.target;x?.contains(w)||k?.contains(w)||n()},v=b=>{b.key==="Escape"&&n()};return document.addEventListener("pointerdown",h,!0),document.addEventListener("keydown",v),()=>{window.removeEventListener("resize",m),window.removeEventListener("scroll",m,!0),document.removeEventListener("pointerdown",h,!0),document.removeEventListener("keydown",v)}},[t,y,n,e]),!t)return null;const g={...xv,minWidth:s,maxWidth:`min(${a}px, calc(100vw - ${l*2}px))`,...p?{left:p.left,top:p.top,maxHeight:p.maxHeight,visibility:"visible"}:{left:-9999,top:-9999,visibility:"hidden"}};return i.jsx("div",{ref:c,"data-testid":d,style:g,role:"dialog",children:o})}const xv={position:"fixed",zIndex:1e3,background:"#fff",border:"1px solid #cbd5e1",borderRadius:8,boxShadow:"0 4px 16px rgba(0,0,0,0.14)",padding:6,overflowY:"auto",boxSizing:"border-box"};function qd(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function $t({candidates:e,t,onSelect:n,testIdSuffix:o,defaultOpen:r=!1,floating:s=!0}){const[a,l]=u.useState(r),[d,c]=u.useState(""),p=u.useRef(null),f=u.useMemo(()=>Fi(e,"scalar").map(h=>({...h,resolvedLabel:qd(t,h.labelKey,h.path||h.sourceId)})),[e,t]),y=u.useMemo(()=>Ki(f,d),[f,d]),g=i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":`g7le-inline-binding-search-input-${o}`,value:d,placeholder:t("layout_editor.inline_binding.search_placeholder"),onChange:m=>c(m.target.value),style:Cv}),i.jsx("div",{style:$v,children:y.length===0?i.jsx("div",{style:Ev,children:t("layout_editor.inline_binding.no_results")}):y.map(m=>i.jsxs("button",{type:"button","data-testid":`g7le-inline-binding-candidate-${m.expression}`,onClick:()=>{n(m),l(!1),c("")},style:Iv,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:m.resolvedLabel}),i.jsxs("span",{style:Av,children:[qd(t,m.groupLabelKey,m.sourceId)," · ",m.path||m.sourceId]})]}),i.jsx("span",{style:Tv,children:m.preview})]},m.expression))})]});return i.jsxs("div",{style:s?Sv:{marginTop:4},children:[i.jsx("button",{ref:p,type:"button","data-testid":`g7le-inline-binding-search-toggle-${o}`,onClick:()=>l(m=>!m),title:t("layout_editor.inline_binding.search"),"aria-label":t("layout_editor.inline_binding.search"),"aria-expanded":a,style:wv,children:"🔍"}),s?i.jsx(Xn,{anchorRef:p,open:a,onClose:()=>l(!1),testid:`g7le-inline-binding-picker-${o}`,children:g}):a&&i.jsx("div",{"data-testid":`g7le-inline-binding-picker-${o}`,style:kv,children:g})]})}const wv={fontSize:13,lineHeight:1,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 8px",cursor:"pointer",textAlign:"center",whiteSpace:"nowrap"},kv={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},Sv={display:"inline-block"},Cv={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},$v={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},Ev={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},Iv={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},Av={fontSize:10,color:"#94a3b8"},Tv={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},Dv=["$localized"];function Jn(e){const t=e.trim();if(!t.startsWith("{{")||!t.endsWith("}}")||t.length<4)return null;let n=0;for(let o=0;o<t.length;o++){if(t[o]==="{"&&t[o+1]==="{"){n++,o++;continue}if(t[o]==="}"&&t[o+1]==="}"&&(n--,o++,n===0))return o===t.length-1?t.slice(2,-2).trim():null}return null}function Gd(e){if(typeof e!="string"||e.trim()===""||Jn(e)!==null)return!1;const t=e.match(/\{\{(?:[^}]|\}(?!\}))*\}\}/g);return!t||t.length===0?!1:t.some(n=>un(n).decomposed)}function So(e,t,n,o){switch(e.kind){case"leaf":Yd(e.text,t,n,o);return;case"conditional":So(e.then,t,n,o),o.push({kind:"ellipsis",text:"⋯"});return;case"fallback":So(e.primary,t,n,o);return;case"concat":e.parts.forEach(r=>So(r,t,n,o));return;default:o.push({kind:"text",text:e.source});return}}function Yd(e,t,n,o){const r=e.trim();if(/^\{\{([\s\S]*)\}\}$/.exec(r)&&!r.slice(2,-2).includes("}}")){o.push({kind:"chip",text:n(r)});return}if(r.startsWith("$t:")){const a=r.slice(3).split("|")[0];o.push({kind:"text",text:t(a)||a});return}for(const a of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))a!==""&&(/^\{\{[\s\S]*\}\}$/.test(a)?o.push({kind:"chip",text:n(a)}):o.push({kind:"text",text:a}))}function us(e,t,n){const o=[];if(typeof e!="string"||e==="")return o;if(Jn(e)===null&&/\{\{/.test(e)){for(const a of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))if(!(a===void 0||a===""))if(/^\{\{[\s\S]*\}\}$/.test(a)){const l=un(a);So(l.node,t,n,o)}else Yd(a,t,n,o);return o}const s=un(e);return So(s.node,t,n,o),o}function Nv(e){if(typeof e!="string"||e.trim()==="")return"empty";const t=e.trim();if(Jn(t)===null)return"simple";const n=un(e);return n.node.kind==="leaf"?"simple":n.decomposed?"expression":"raw"}function un(e){const t=typeof e=="string"?e:"",n=t.trim();if(n==="")return{node:{kind:"leaf",text:t},decomposed:!1,original:t};const o=Jn(n);if(o===null)return{node:{kind:"leaf",text:t},decomposed:!1,original:t};let r;try{r=Hv(o)}catch{r=null}if(r===null)return{node:{kind:"raw",source:o},decomposed:!1,original:t};if(r.kind==="leaf"||r.kind==="raw")return{node:r,decomposed:!1,original:t};if(r.kind==="fallback"&&r.op==="??"&&zv(r.primary)&&Mv(r.fallback))return{node:{kind:"leaf",text:t},decomposed:!1,original:t};const s=Zn(r,!0);return qv(o,s)?{node:r,decomposed:!0,original:t}:{node:{kind:"raw",source:o},decomposed:!1,original:t}}function Zn(e,t=!1){const n=Jd(e);return t?n:e.kind==="leaf"?e.text:`{{${n}}}`}function Co(e){return Zn({kind:"conditional",condition:{kind:"simple",left:"",op:"truthy",right:""},then:{kind:"leaf",text:typeof e=="string"?e:""},else:{kind:"leaf",text:""}},!1)}function ps(e){return Zn({kind:"fallback",op:"??",primary:{kind:"leaf",text:typeof e=="string"?e:""},fallback:{kind:"leaf",text:""}},!1)}function $o(e){switch(e.kind){case"leaf":return e.text;case"conditional":return $o(e.then);case"fallback":return $o(e.primary);case"concat":return e.parts.map($o).join("");default:return e.kind==="raw"?`{{${e.source}}}`:""}}function uM(e){return e}function Sr(e){if(typeof e!="string"||e.trim()==="")return"";if(Jn(e)===null&&/\{\{/.test(e)){for(const n of e.split(/(\{\{(?:[^}]|\}(?!\}))*\}\})/g))if(!(n===void 0||n===""))return/^\{\{[\s\S]*\}\}$/.test(n)?$o(un(n).node):n;return""}return $o(un(e).node)}function zv(e){if(e.kind!=="leaf")return!1;const t=/^\{\{([\s\S]*)\}\}$/.exec(e.text.trim());return t?Qn(t[1].trim()):!1}function Mv(e){if(e.kind!=="leaf")return!1;const t=e.text.trim();return!(/\{\{/.test(t)||t.startsWith("$t:"))}function Cr(e){return`'${e.replace(/\\/g,"\\\\").replace(/'/g,"\\'")}'`}function Lv(e){const t=[];let n=0,o=0;const r=e.length;for(;o<r;){if(e[o]==="{"&&e[o+1]==="{"){o>n&&t.push(Cr(e.slice(n,o)));let s=o+2;for(;s<r&&!(e[s]==="}"&&e[s+1]==="}");)s++;if(s>=r)break;const a=e.slice(o+2,s).trim();t.push(a),o=s+2,n=o;continue}o++}return n<r&&t.push(Cr(e.slice(n))),t.length===0?Cr(e):t.join(" + ")}function Pv(e){const t=e.trim(),n=/^\{\{([\s\S]*)\}\}$/.exec(t);return n?n[1].trim():!t.startsWith("$t:")&&/\{\{/.test(t)?Lv(e):Cr(e)}const Xd="false";function Ov(e){if(e.kind==="raw")return e.source;if(e.left.trim()==="")return Xd;if(e.op==="truthy")return e.left;if(e.op==="falsy")return`!${Bv(e.left)?`(${e.left})`:e.left}`;const t=e.right.trim()===""?"''":e.right;return`${e.left} ${e.op} ${t}`}function Bv(e){return/[\s()?:|&+]/.test(e.trim())&&!/^[A-Za-z_$][\w$.?[\]]*$/.test(e.trim())}function Jd(e){switch(e.kind){case"leaf":return Pv(e.text);case"raw":return e.source;case"concat":return e.parts.map(t=>Eo(t,"concat")).join(" + ");case"fallback":return`${Eo(e.primary,"fallback")} ${e.op} ${Eo(e.fallback,"fallback")}`;case"conditional":return`${Ov(e.condition)} ? ${Eo(e.then,"conditional")} : ${Eo(e.else,"conditional")}`;default:return""}}function Eo(e,t){const n=Jd(e);return e.kind==="leaf"||e.kind==="raw"?n:e.kind==="conditional"?t==="conditional"?n:`(${n})`:e.kind==="fallback"?t==="concat"?`(${n})`:n:(e.kind==="concat",n)}const Wv=["===","!==","??","||","&&","==","!=",">=","<="];function Zd(e){const t=[];let n=0;const o=e.length;for(;n<o;){const r=e[n];if(/\s/.test(r)){n++;continue}if(r==="'"||r==='"'||r==="`"){const a=r;let l=n+1,d=a;for(;l<o;){if(e[l]==="\\"){d+=e[l]+(e[l+1]??""),l+=2;continue}if(e[l]==="{"&&e[l+1]==="{"){let c=l+2;for(;c<o&&!(e[c]==="}"&&e[c+1]==="}");)c++;if(c>=o)throw new Error("unterminated-binding-in-string");d+=e.slice(l,c+2),l=c+2;continue}if(e[l]===a){d+=a,l++;break}if(a==="`"&&e[l]==="$"&&e[l+1]==="{")throw new Error("template-literal-interpolation-unsupported");d+=e[l],l++}if(l>o||d[d.length-1]!==a)throw new Error("unterminated-string");t.push({kind:"string",value:d,start:n,end:l}),n=l;continue}if(r==="{"&&e[n+1]==="{"){let a=n+2;for(;a<o;){if(e[a]==="}"&&e[a+1]==="}"){a+=2;break}a++}if(e[a-1]!=="}"||e[a-2]!=="}")throw new Error("unterminated-binding");t.push({kind:"binding",value:e.slice(n,a),start:n,end:a}),n=a;continue}if(r==="$"&&e.startsWith("$t:",n)){let a=n+3;for(;a<o&&/[a-zA-Z0-9._-]/.test(e[a]);)a++;t.push({kind:"tkey",value:e.slice(n,a),start:n,end:a}),n=a;continue}let s="";for(const a of Wv)if(e.startsWith(a,n)){s=a;break}if(s){t.push({kind:"op",value:s,start:n,end:n+s.length}),n+=s.length;continue}if(r==="?"){t.push({kind:"op",value:"?",start:n,end:n+1}),n++;continue}if(r===":"){t.push({kind:"op",value:":",start:n,end:n+1}),n++;continue}if(r==="+"){t.push({kind:"op",value:"+",start:n,end:n+1}),n++;continue}if(r===">"||r==="<"){t.push({kind:"op",value:r,start:n,end:n+1}),n++;continue}if(r==="!"){t.push({kind:"op",value:"!",start:n,end:n+1}),n++;continue}if(r==="("){t.push({kind:"lparen",value:"(",start:n,end:n+1}),n++;continue}if(r===")"){t.push({kind:"rparen",value:")",start:n,end:n+1}),n++;continue}if(r==="["){t.push({kind:"lbracket",value:"[",start:n,end:n+1}),n++;continue}if(r==="]"){t.push({kind:"rbracket",value:"]",start:n,end:n+1}),n++;continue}if(r==="."){t.push({kind:"dot",value:".",start:n,end:n+1}),n++;continue}if(/[0-9]/.test(r)){let a=n;for(;a<o&&/[0-9.]/.test(e[a]);)a++;t.push({kind:"number",value:e.slice(n,a),start:n,end:a}),n=a;continue}if(/[A-Za-z_$]/.test(r)){let a=n;for(;a<o&&/[A-Za-z0-9_$]/.test(e[a]);)a++;t.push({kind:"ident",value:e.slice(n,a),start:n,end:a}),n=a;continue}t.push({kind:"other",value:r,start:n,end:n+1}),n++}return t}class jv{constructor(t){Ti(this,"toks");Ti(this,"pos",0);Ti(this,"src");this.src=t,this.toks=Zd(t)}parse(){const t=this.parseTernary();if(this.pos<this.toks.length)throw new Error("trailing-tokens");return t}peek(){return this.toks[this.pos]}next(){return this.toks[this.pos++]}parseTernary(){const t=this.pos,n=this.findTopLevelQuestion(t);if(n===-1)return this.parseFallback();const o=this.sliceSource(t,n);this.pos=n+1;const r=this.parseTernary(),s=this.next();if(!s||s.kind!=="op"||s.value!==":")throw new Error("expected-colon");const a=this.parseTernary();return{kind:"conditional",condition:Kv(o.trim()),then:r,else:a}}findTopLevelQuestion(t){let n=0;for(let o=t;o<this.toks.length;o++){const r=this.toks[o];if(r.kind==="lparen"||r.kind==="lbracket")n++;else if(r.kind==="rparen"||r.kind==="rbracket"){if(n===0)return-1;n--}else if(n===0&&r.kind==="op"&&r.value==="?"){if(this.toks[o+1]?.kind==="dot")continue;return o}else if(n===0&&r.kind==="op"&&r.value===":")return-1}return-1}sliceSource(t,n){if(t>=n)return"";const o=this.toks[t].start,r=this.toks[n-1].end;return this.src.slice(o,r)}parseFallback(){let t=this.parseConcat(),n=this.peek();for(;n&&n.kind==="op"&&(n.value==="??"||n.value==="||");){const o=n.value;this.next();const r=this.parseConcat();t={kind:"fallback",op:o,primary:t,fallback:r},n=this.peek()}return t}parseConcat(){const t=this.parseUnaryOrPrimary(),n=[t];let o=this.peek();for(;o&&o.kind==="op"&&o.value==="+";)this.next(),n.push(this.parseUnaryOrPrimary()),o=this.peek();return n.length===1?t:{kind:"concat",parts:n}}parseUnaryOrPrimary(){return this.parsePrimary()}parsePrimary(){const t=this.peek();if(!t)throw new Error("unexpected-end");if(t.kind==="lparen"){this.next();const n=this.parseTernary(),o=this.next();if(!o||o.kind!=="rparen")throw new Error("expected-rparen");return n}if(t.kind==="string")return this.next(),{kind:"leaf",text:Rd(t.value)};if(t.kind==="tkey")return this.next(),{kind:"leaf",text:t.value};if(t.kind==="binding")return this.next(),{kind:"leaf",text:t.value};if(t.kind==="ident"&&Dv.includes(t.value)&&this.toks[this.pos+1]?.kind==="lparen"){const n=this.tryParsePathWrapperCall(t.value);if(n!==null)return n;throw new Error("path-wrapper-non-simple-arg")}if(t.kind==="ident")return{kind:"leaf",text:`{{${this.parsePath()}}}`};if(t.kind==="number")return this.next(),{kind:"leaf",text:t.value};throw new Error(`unparseable-primary:${t.kind}`)}parsePath(){let t="";const n=this.next();if(!n||n.kind!=="ident")throw new Error("expected-ident");for(t+=n.value;;){const o=this.peek();if(!o)break;if(o.kind==="op"&&o.value==="?"&&this.toks[this.pos+1]?.kind==="dot"){this.next(),this.next();const r=this.next();if(!r||r.kind!=="ident")throw new Error("expected-ident-after-optional");t+="?."+r.value;continue}if(o.kind==="dot"){this.next();const r=this.next();if(!r||r.kind!=="ident")throw new Error("expected-ident-after-dot");t+="."+r.value;continue}if(o.kind==="lbracket"){this.next();const r=this.next(),s=this.next();if(!s||s.kind!=="rbracket")throw new Error("expected-rbracket");if(r&&(r.kind==="number"||r.kind==="string")){t+=`[${r.value}]`;continue}throw new Error("non-literal-index")}if(o.kind==="lparen")throw new Error("call-expression");break}return t}tryParsePathWrapperCall(t){const n=this.pos;this.next();const o=this.next();if(!o||o.kind!=="lparen")return this.pos=n,null;let r;try{if(this.peek()?.kind!=="ident")return this.pos=n,null;r=this.parsePath()}catch{return this.pos=n,null}const s=this.next();return!s||s.kind!=="rparen"?(this.pos=n,null):{kind:"leaf",text:`{{${t}(${r})}}`}}}function Hv(e){return new jv(e).parse()}const Fv=["===","!==","==","!=",">=","<=",">","<"];function Kv(e,t){const n=e.trim();if(n==="")return{kind:"raw",source:n};if(n===Xd)return{kind:"simple",left:"",op:"truthy",right:""};if(n.startsWith("!")){const s=n.slice(1).trim(),a=Qd(s);return Qn(a)?{kind:"simple",left:a,op:"falsy",right:""}:{kind:"raw",source:n}}const o=Vv(n);if(o){const{left:s,op:a,right:l}=o,d=s.trim(),c=l.trim();return Qn(d)&&(c==="''"||c==='""')?{kind:"simple",left:d,op:a,right:""}:Qn(d)&&Uv(c)?{kind:"simple",left:d,op:a,right:c}:{kind:"raw",source:n}}const r=Qd(n);return Qn(r)?{kind:"simple",left:r,op:"truthy",right:""}:{kind:"raw",source:n}}function Vv(e){let t=0,n=null;for(let o=0;o<e.length;o++){const r=e[o];if(n){if(r==="\\"){o++;continue}r===n&&(n=null);continue}if(r==="'"||r==='"'||r==="`"){n=r;continue}if(r==="("||r==="["){t++;continue}if(r===")"||r==="]"){t--;continue}if(t===0){if(r==="&"&&e[o+1]==="&"||r==="|"&&e[o+1]==="|"||r==="?"&&e[o+1]==="?")return null;for(const s of Fv)if(e.startsWith(s,o))return{left:e.slice(0,o),op:s,right:e.slice(o+s.length)}}}return null}function Qd(e){let t=e.trim();for(;t.startsWith("(")&&t.endsWith(")");){let n=0,o=!0;for(let r=0;r<t.length;r++)if(t[r]==="(")n++;else if(t[r]===")"&&(n--,n===0&&r<t.length-1)){o=!1;break}if(!o)break;t=t.slice(1,-1).trim()}return t}function Qn(e){const t=e.trim();if(t==="")return!1;let n=0;const o=t.length;if(!/[A-Za-z_$]/.test(t[0]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;for(;n<o;){if(t[n]==="?"&&t[n+1]==="."){if(n+=2,n>=o||!/[A-Za-z_$]/.test(t[n]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;continue}if(t[n]==="."){if(n++,n>=o||!/[A-Za-z_$]/.test(t[n]))return!1;for(n++;n<o&&/[\w$]/.test(t[n]);)n++;continue}if(t[n]==="["){const r=t.indexOf("]",n);if(r===-1)return!1;const s=t.slice(n+1,r).trim();if(!/^\d+$/.test(s)&&!/^'[^']*'$/.test(s)&&!/^"[^"]*"$/.test(s))return!1;n=r+1;continue}return!1}return!0}function Uv(e){const t=e.trim();return/^'[^']*'$/.test(t)||/^"[^"]*"$/.test(t)||/^-?\d+(\.\d+)?$/.test(t)||/^(true|false|null|undefined)$/.test(t)?!0:Qn(t)}function Rd(e){const t=e.trim();return t.length>=2&&(t[0]==="'"||t[0]==='"'||t[0]==="`")?t.slice(1,-1).replace(/\\(['"`\\])/g,"$1"):t}function qv(e,t){try{return $r(e)===$r(t)}catch{return!1}}function $r(e){const t=Jn(e);return t!==null?$r(t):Zd(e).map(o=>{if(o.kind==="string")return`S:${Rd(o.value)}`;if(o.kind==="binding"){const r=/^\{\{([\s\S]*)\}\}$/.exec(o.value);return`B:${r?$r(r[1].trim()):o.value.replace(/\s+/g,"")}`}return o.kind==="tkey"?`S:${o.value}`:o.kind==="op"?`O:${o.value}`:o.kind==="ident"||o.kind==="number"?`I:${o.value}`:o.kind==="dot"?".":o.kind==="lbracket"?"[":o.kind==="rbracket"?"]":o.kind==="lparen"||o.kind==="rparen"?"":`X:${o.value}`}).join("")}function Rn({active:e,testid:t}){return i.jsx("div",{"data-testid":t,"data-active":e?"true":"false","aria-hidden":"true",style:e?Yv:Gv})}const Gv={height:0,margin:0,transition:"height 80ms"},Yv={height:3,margin:"1px 0",borderRadius:2,background:"#2563eb",boxShadow:"0 0 0 1px #93c5fd"};function fs(e){const{length:t,onMove:n,canDrag:o,canDropAt:r}=e,[s,a]=u.useState(null),[l,d]=u.useState(null),c=u.useCallback(m=>{o&&!o(m)||(a(m),d(m))},[o]),p=u.useCallback(()=>{a(null),d(null)},[]),f=u.useCallback((m,h)=>{if(s===null)return;const v=h==="before"?m:m+1;if(r&&!r(s,v)){d(null);return}d(v)},[s,r]),y=u.useCallback(()=>{if(s!==null&&l!==null){const m=l>s?l-1:l;m!==s&&(!r||r(s,l))&&n(s,m)}a(null),d(null)},[s,l,n,r]),g=u.useCallback(m=>s!==null&&l===m&&m<=t,[s,l,t]);return{dragIndex:s,dropIndex:l,dragging:s!==null,onDragStart:c,onDragEnd:p,onDragOverItem:f,onDrop:y,isDropTarget:g}}const Xv={text:"",expression:"{{false ? '' : ''}}",fallback:"{{'' ?? ''}}",data:""};function ec(e,t){const n=In(e);let o=t;const r=n.map(s=>({id:o++,kind:s.kind==="literal"?"text":"expression",value:s.raw}));return r.length===0&&r.push({id:o++,kind:"text",value:""}),{segs:r,nextSeq:o}}function gs({value:e,onChange:t,t:n,candidates:o,testidPrefix:r="g7le-seg-value",renderLeafInput:s}){const a=u.useRef(0),l=u.useRef(e),d=u.useMemo(()=>{const k=ec(e,a.current);return a.current=k.nextSeq,k.segs},[]),[c,p]=u.useState(d),[f,y]=u.useState(!1);if(e!==l.current){l.current=e;const k=c.map(w=>w.value).join("");if(e!==k){const w=ec(e,a.current);a.current=w.nextSeq,p(w.segs)}}const g=u.useCallback(k=>{p(k);const w=k.map(_=>_.value).join("");l.current=w,t(w)},[t]),m=u.useCallback((k,w)=>{g(c.map(_=>_.id===k?{..._,value:w??""}:_))},[c,g]),h=u.useCallback(k=>{const w=c.filter(_=>_.id!==k);g(w.length>0?w:[{id:a.current++,kind:"text",value:""}])},[c,g]),v=u.useCallback(k=>{g([...c,{id:a.current++,kind:k,value:Xv[k]}])},[c,g]),b=u.useCallback((k,w)=>{if(k===w||k<0||w<0||k>=c.length||w>=c.length)return;const _=c.slice(),[C]=_.splice(k,1);_.splice(w,0,C),g(_)},[c,g]),x=fs({length:c.length,onMove:b});return i.jsxs("div",{"data-testid":r,style:Qv,children:[i.jsx("div",{style:Rv,children:n("layout_editor.value_tree.segmented_hint")}),c.map((k,w)=>i.jsxs(u.Fragment,{children:[i.jsx(Rn,{active:x.isDropTarget(w),testid:`${r}-dropline-${w}`}),i.jsx(Zv,{seg:k,index:w,t:n,candidates:o,testidPrefix:r,renderLeafInput:s,onValueChange:_=>m(k.id,_),onRemove:()=>h(k.id),dragging:x.dragIndex===w,onDragStartCard:()=>x.onDragStart(w),onDragEndCard:x.onDragEnd,onDragOverCard:_=>x.onDragOverItem(w,_),onDropCard:x.onDrop})]},k.id)),i.jsx(Rn,{active:x.isDropTarget(c.length),testid:`${r}-dropline-end`}),i.jsxs("div",{style:i_,"data-testid":`${r}-add-row`,children:[i.jsxs("button",{type:"button","data-testid":`${r}-add-text`,onClick:()=>v("text"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.text")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-expression`,onClick:()=>v("expression"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.expression")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-fallback`,onClick:()=>v("fallback"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.fallback")]}),i.jsxs("button",{type:"button","data-testid":`${r}-add-data`,onClick:()=>v("data"),style:Er,children:["+ ",n("layout_editor.value_tree.segment.data")]})]}),i.jsxs("div",{style:l_,children:[i.jsxs("button",{type:"button","data-testid":`${r}-source-toggle`,"aria-expanded":f,onClick:()=>y(k=>!k),style:d_,children:["</>"," ",n("layout_editor.value_tree.show_source")," ",f?"▴":"▾"]}),f&&i.jsx("code",{"data-testid":`${r}-source-code`,style:c_,children:c.map(k=>k.value).join("")})]})]})}function Jv({t:e,candidates:t,onPick:n,testidPrefix:o}){return!t||t.length===0?i.jsx("div",{"data-testid":`${o}-data-empty`,style:a_,children:e("layout_editor.value_tree.segment.data_no_candidates")}):i.jsxs("div",{"data-testid":`${o}-data-picker`,style:{minWidth:0},children:[i.jsx("div",{style:s_,children:e("layout_editor.value_tree.segment.data_pick_hint")}),i.jsx($t,{candidates:t,t:e,onSelect:r=>{n(En(r.sourceId,r.path,"scalar"))},testIdSuffix:`${o}-data`,defaultOpen:!0,floating:!0})]})}function Zv({seg:e,index:t,t:n,candidates:o,testidPrefix:r,renderLeafInput:s,onValueChange:a,onRemove:l,dragging:d,onDragStartCard:c,onDragEndCard:p,onDragOverCard:f,onDropCard:y}){const g=e.kind==="text"?"layout_editor.value_tree.segment.text":e.kind==="data"?"layout_editor.value_tree.segment.data":e.kind==="fallback"?"layout_editor.value_tree.segment.fallback":"layout_editor.value_tree.segment.expression";return i.jsxs("div",{"data-testid":`${r}-card-${t}`,"data-seg-kind":e.kind,style:d?{...tc,opacity:.5}:tc,onDragOver:m=>{m.preventDefault();const h=m.currentTarget.getBoundingClientRect();f(m.clientY<h.top+h.height/2?"before":"after")},onDrop:m=>{m.preventDefault(),y()},children:[i.jsxs("div",{style:e_,children:[i.jsx("span",{"data-testid":`${r}-drag-${t}`,style:o_,draggable:!0,onDragStart:c,onDragEnd:p,title:n("layout_editor.value_tree.drag_reorder"),"aria-label":n("layout_editor.value_tree.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:t_,children:n(g)}),i.jsx("button",{type:"button","data-testid":`${r}-remove-${t}`,onClick:l,title:n("layout_editor.value_tree.segment.remove"),"aria-label":n("layout_editor.value_tree.segment.remove"),style:r_,children:"✕"})]}),i.jsx("div",{style:n_,children:e.kind==="data"&&e.value.trim()===""?i.jsx(Jv,{t:n,candidates:o,onPick:a,testidPrefix:`${r}-field-${t}`}):s?s({value:e.value,onChange:a,t:n,candidates:o,testidPrefix:`${r}-field-${t}`}):i.jsx(vt,{value:e.value,onChange:a,t:n,candidates:o,testidPrefix:`${r}-field-${t}`,enableExpressionTree:!0,expressionSourceToggle:!1})})]})}const Qv={display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},Rv={fontSize:11,color:"#64748b"},tc={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #e2e8f0",borderLeft:"3px solid #6366f1",borderRadius:6,background:"#f8fafc",minWidth:0},e_={display:"flex",alignItems:"center",gap:6,minWidth:0},t_={fontSize:10,fontWeight:600,color:"#6366f1",flex:1,minWidth:0},n_={minWidth:0},o_={color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},r_={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12,padding:2,flexShrink:0},i_={display:"flex",flexWrap:"wrap",gap:6,marginTop:2},s_={fontSize:11,color:"#64748b",marginBottom:2},a_={fontSize:11,color:"#94a3b8",padding:"4px 0"},Er={padding:"4px 10px",fontSize:11,border:"1px dashed #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},l_={display:"flex",flexDirection:"column",gap:4,marginTop:2},d_={alignSelf:"flex-start",padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontFamily:"monospace"},c_={fontSize:11,background:"#0f172a",color:"#e2e8f0",padding:"6px 8px",borderRadius:4,wordBreak:"break-all",fontFamily:"monospace"},u_=[{value:"truthy",labelKey:"layout_editor.value_tree.cond.op_truthy"},{value:"falsy",labelKey:"layout_editor.value_tree.cond.op_falsy"},{value:"===",labelKey:"layout_editor.value_tree.cond.op_eq"},{value:"!==",labelKey:"layout_editor.value_tree.cond.op_neq"},{value:">",labelKey:"layout_editor.value_tree.cond.op_gt"},{value:"<",labelKey:"layout_editor.value_tree.cond.op_lt"},{value:">=",labelKey:"layout_editor.value_tree.cond.op_gte"},{value:"<=",labelKey:"layout_editor.value_tree.cond.op_lte"}];function nc({node:e,onChange:t,t:n,candidates:o,testidPrefix:r="g7le-value-tree",depth:s=0,showSourceToggle:a=!0,renderLeafInput:l}){const[d,c]=u.useState(!1),p=s===0,f=i.jsx(eo,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:l});return p?a?i.jsxs("div",{"data-testid":r,style:rc,children:[f,i.jsxs("div",{style:D_,children:[i.jsxs("button",{type:"button","data-testid":`${r}-source-toggle`,"aria-expanded":d,onClick:()=>c(y=>!y),style:N_,children:["</>"," ",n("layout_editor.value_tree.show_source")," ",d?"▴":"▾"]}),d&&i.jsx("code",{"data-testid":`${r}-source-code`,style:z_,children:Zn(e,!1)})]})]}):i.jsx("div",{"data-testid":r,style:rc,children:f}):f}function eo({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){switch(e.kind){case"leaf":return i.jsx(gs,{value:e.text,onChange:l=>t({kind:"leaf",text:l}),t:n,candidates:o,testidPrefix:`${r}-leaf`,renderLeafInput:a});case"conditional":return i.jsx(p_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});case"fallback":return i.jsx(f_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});case"concat":return i.jsx(h_,{node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a});default:return i.jsxs("div",{"data-testid":`${r}-raw`,style:M_,children:[i.jsx("code",{style:ac,children:e.source??""}),i.jsxs("span",{style:lc,children:["🔒 ",n("layout_editor.value_tree.raw_code_only")]})]})}}function p_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback(p=>t({...e,condition:p}),[e,t]),d=u.useCallback(p=>t({...e,then:p}),[e,t]),c=u.useCallback(p=>t({...e,else:p}),[e,t]);return i.jsxs("div",{"data-testid":`${r}-conditional`,style:hs(s),children:[i.jsx(y_,{condition:e.condition,onChange:l,t:n,candidates:o,testidPrefix:r}),i.jsx(Ir,{label:n("layout_editor.value_tree.cond.when_true"),testid:`${r}-then`,children:i.jsx(eo,{node:e.then,onChange:d,t:n,candidates:o,testidPrefix:`${r}-then`,depth:s+1,renderLeafInput:a})}),i.jsx(Ir,{label:n("layout_editor.value_tree.cond.when_false"),testid:`${r}-else`,children:i.jsx(eo,{node:e.else,onChange:c,t:n,candidates:o,testidPrefix:`${r}-else`,depth:s+1,renderLeafInput:a})})]})}function f_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback(c=>t({...e,primary:c}),[e,t]),d=u.useCallback(c=>t({...e,fallback:c}),[e,t]);return i.jsxs("div",{"data-testid":`${r}-fallback`,style:hs(s),children:[i.jsx(Ir,{label:n("layout_editor.value_tree.fallback.primary"),testid:`${r}-primary`,children:i.jsx(eo,{node:e.primary,onChange:l,t:n,candidates:o,testidPrefix:`${r}-primary`,depth:s+1,renderLeafInput:a})}),i.jsx(Ir,{label:n(e.op==="??"?"layout_editor.value_tree.fallback.when_empty":"layout_editor.value_tree.fallback.when_falsy"),testid:`${r}-fallback-branch`,children:i.jsx(eo,{node:e.fallback,onChange:d,t:n,candidates:o,testidPrefix:`${r}-fallbackb`,depth:s+1,renderLeafInput:a})})]})}const g_={text:()=>({kind:"leaf",text:""}),data:()=>({kind:"leaf",text:""}),expression:()=>({kind:"conditional",condition:{kind:"simple",left:"",op:"truthy",right:""},then:{kind:"leaf",text:""},else:{kind:"leaf",text:""}}),fallback:()=>({kind:"fallback",op:"??",primary:{kind:"leaf",text:""},fallback:{kind:"leaf",text:""}})};function h_({node:e,onChange:t,t:n,candidates:o,testidPrefix:r,depth:s,renderLeafInput:a}){const l=u.useCallback((g,m)=>{const h=e.parts.slice();h[g]=m,t({...e,parts:h})},[e,t]),d=u.useCallback(g=>{g.length===0?t({kind:"leaf",text:""}):g.length===1?t(g[0]):t({...e,parts:g})},[e,t]),c=u.useCallback(g=>d(e.parts.filter((m,h)=>h!==g)),[e.parts,d]),p=u.useCallback(g=>d([...e.parts,g_[g]()]),[e.parts,d]),f=u.useCallback((g,m)=>{if(g===m||g<0||m<0||g>=e.parts.length||m>=e.parts.length)return;const h=e.parts.slice(),[v]=h.splice(g,1);h.splice(m,0,v),d(h)},[e.parts,d]),y=fs({length:e.parts.length,onMove:f});return i.jsxs("div",{"data-testid":`${r}-concat`,style:hs(s),children:[i.jsx("div",{style:C_,children:n("layout_editor.value_tree.concat.hint")}),e.parts.map((g,m)=>i.jsxs(u.Fragment,{children:[i.jsx(Rn,{active:y.isDropTarget(m),testid:`${r}-part-dropline-${m}`}),i.jsxs("div",{"data-testid":`${r}-part-card-${m}`,style:y.dragIndex===m?{...sc,opacity:.5}:sc,onDragOver:h=>{h.preventDefault();const v=h.currentTarget.getBoundingClientRect();y.onDragOverItem(m,h.clientY<v.top+v.height/2?"before":"after")},onDrop:h=>{h.preventDefault(),y.onDrop()},children:[i.jsxs("div",{style:$_,children:[i.jsx("span",{"data-testid":`${r}-part-handle-${m}`,style:A_,draggable:!0,onDragStart:()=>y.onDragStart(m),onDragEnd:y.onDragEnd,title:n("layout_editor.value_tree.drag_reorder"),"aria-label":n("layout_editor.value_tree.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:E_,children:n("layout_editor.value_tree.concat.part",{n:m+1})}),i.jsx("button",{type:"button","data-testid":`${r}-part-remove-${m}`,onClick:()=>c(m),title:n("layout_editor.value_tree.segment.remove"),"aria-label":n("layout_editor.value_tree.segment.remove"),style:T_,children:"✕"})]}),i.jsx("div",{style:{minWidth:0},children:i.jsx(eo,{node:g,onChange:h=>l(m,h),t:n,candidates:o,testidPrefix:`${r}-part-${m}`,depth:s+1,renderLeafInput:a})})]})]},m)),i.jsx(Rn,{active:y.isDropTarget(e.parts.length),testid:`${r}-part-dropline-end`}),i.jsxs("div",{style:I_,"data-testid":`${r}-part-add-row`,children:[i.jsxs("button",{type:"button","data-testid":`${r}-part-add-text`,onClick:()=>p("text"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.text")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-expression`,onClick:()=>p("expression"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.expression")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-fallback`,onClick:()=>p("fallback"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.fallback")]}),i.jsxs("button",{type:"button","data-testid":`${r}-part-add-data`,onClick:()=>p("data"),style:Ar,children:["+ ",n("layout_editor.value_tree.segment.data")]})]})]})}function y_({condition:e,onChange:t,t:n,candidates:o,testidPrefix:r}){if(e.kind==="raw")return i.jsxs("div",{"data-testid":`${r}-cond-raw`,style:x_,children:[i.jsx("span",{style:ic,children:n("layout_editor.value_tree.cond.label")}),i.jsx("code",{style:ac,children:e.source}),i.jsxs("span",{style:lc,children:["🔒 ",n("layout_editor.value_tree.cond.code_only")]})]});const s=e,a=s.op!=="truthy"&&s.op!=="falsy";return i.jsxs("div",{"data-testid":`${r}-cond`,style:__,children:[i.jsx("span",{style:ic,children:n("layout_editor.value_tree.cond.label")}),i.jsx(oc,{testid:`${r}-cond-left`,value:s.left,onCommit:l=>t({...s,left:l}),placeholder:n("layout_editor.value_tree.cond.field_placeholder"),candidates:o,t:n}),i.jsx("select",{"data-testid":`${r}-cond-op`,value:s.op,onChange:l=>t({...s,op:l.target.value}),style:S_,children:u_.map(l=>i.jsx("option",{value:l.value,children:n(l.labelKey)},l.value))}),a&&i.jsx(oc,{testid:`${r}-cond-right`,value:s.right,onCommit:l=>t({...s,right:l}),placeholder:n("layout_editor.value_tree.cond.value_placeholder"),candidates:o,t:n,quoteDataPick:!0})]})}function oc({testid:e,value:t,onCommit:n,placeholder:o,candidates:r,t:s,quoteDataPick:a=!1}){const[l,d]=u.useState(null),c=l!==null?l:t,p=()=>{l!==null&&(l!==t&&n(l),d(null))},f=(y,g)=>{const m=En(y,g,"scalar"),h=/^\{\{\s*([\s\S]*?)\s*\}\}$/.exec(m.trim());let v=h?h[1]:m;v=v.replace(/\s*\?\?\s*''$/,"").trim(),n(a?`'${v}'`:v),d(null)};return i.jsxs("span",{style:w_,children:[i.jsx("input",{type:"text","data-testid":e,value:c,onChange:y=>d(y.target.value),onBlur:p,onKeyDown:y=>{y.key==="Enter"&&(y.preventDefault(),p())},placeholder:o,style:k_}),r&&r.length>0&&s&&i.jsx($t,{candidates:r,t:s,onSelect:y=>f(y.sourceId,y.path),testIdSuffix:`${e}-data`,floating:!0})]})}function Ir({label:e,testid:t,children:n}){return i.jsxs("div",{"data-testid":t,style:b_,children:[i.jsx("span",{style:m_,children:e}),i.jsx("div",{style:v_,children:n})]})}const rc={display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},hs=e=>({display:"flex",flexDirection:"column",gap:6,padding:"8px",border:"1px solid #e2e8f0",borderLeft:`3px solid ${e%2===0?"#6366f1":"#0ea5e9"}`,borderRadius:6,background:e%2===0?"#f8fafc":"#fefefe",minWidth:0}),b_={display:"flex",flexDirection:"column",gap:3,minWidth:0},m_={fontSize:11,fontWeight:600,color:"#475569"},v_={paddingLeft:8,minWidth:0},__={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center",minWidth:0},x_={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center",minWidth:0},ic={fontSize:11,fontWeight:600,color:"#0f172a",whiteSpace:"nowrap"},w_={display:"inline-flex",alignItems:"flex-start",gap:2,flex:"1 1 80px",minWidth:60},k_={flex:"1 1 80px",minWidth:60,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},S_={padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff"},C_={fontSize:11,color:"#94a3b8"},sc={display:"flex",flexDirection:"column",gap:4,padding:"6px 8px",border:"1px solid #e2e8f0",borderLeft:"3px solid #6366f1",borderRadius:6,background:"#fff",minWidth:0},$_={display:"flex",alignItems:"center",gap:6,minWidth:0},E_={fontSize:10,fontWeight:600,color:"#6366f1",flex:1,minWidth:0},I_={display:"flex",flexWrap:"wrap",gap:6,marginTop:2},A_={color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},T_={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12,padding:2,flexShrink:0},Ar={padding:"4px 10px",fontSize:11,border:"1px dashed #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},D_={display:"flex",flexDirection:"column",gap:4,marginTop:2},N_={alignSelf:"flex-start",padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontFamily:"monospace"},z_={fontSize:11,background:"#0f172a",color:"#e2e8f0",padding:"6px 8px",borderRadius:4,wordBreak:"break-all",fontFamily:"monospace"},M_={display:"flex",flexDirection:"column",gap:2,minWidth:0},ac={fontSize:11,background:"#f1f5f9",padding:"3px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all",fontFamily:"monospace"},lc={fontSize:10,color:"#64748b"};function vt({value:e,onChange:t,t:n,placeholder:o,testidPrefix:r="g7le-i18n-text-field",locales:s,candidates:a,enableExpressionTree:l=!1,expressionTreeCollapsible:d=!1,expressionSourceToggle:c=!0}){const{locale:p,templateIdentifier:f,commitText:y,classify:g,translate:m}=uv(),[h,v]=u.useState(null),[b,x]=u.useState(!1),[k,w]=u.useState(!1),[_,C]=u.useState(!1),[E,S]=u.useState(!1),[T,$]=u.useState(!1),[,A]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const N=H=>{const L=H.detail;L?.templateIdentifier&&L.templateIdentifier!==f||A(I=>I+1)};return window.addEventListener(kt,N),()=>window.removeEventListener(kt,N)},[f]);const z=g(e);u.useEffect(()=>{v(null),$(!1)},[p,e]);const D=h!==null?h:z.displayValue,O=u.useCallback(async()=>{if(h!==null){if(h===z.displayValue){v(null);return}x(!0);try{const N=await y(e,h);N.kind==="created"&&N.token?(t(N.token),Et({op:"prop_i18n_create_key",sourceState:"plain_text",translationKey:N.customKey,toLocale:p,valueLength:h.length,timestamp:Date.now()})):N.kind==="updated"&&Et({op:"prop_i18n_update_value",sourceState:"custom_key",translationKey:N.customKey,toLocale:p,valueLength:h.length,timestamp:Date.now()})}finally{x(!1),v(null)}}},[h,z.displayValue,y,e,t,p]),M=u.useCallback(N=>{typeof e=="string"&&(t(mo(e,N)),z.customKey&&kr(f,z.customKey,N,p))},[e,t,z.customKey,f,p]);if(z.binding){if(l&&typeof e=="string"){const N=Gd(e),H=un(e),L=!N&&Kt(e)!==null,I=(N||H.decomposed)&&!L;if(I&&d&&!_)return i.jsx(O_,{value:e,t:n,translate:m,onEdit:()=>C(!0),testidPrefix:`${r}-preview`});const F=I&&(d||N)?i.jsx(gs,{value:e,onChange:G=>t(G),t:n,candidates:a,testidPrefix:`${r}-seg`}):H.decomposed?i.jsx(nc,{node:H.node,onChange:G=>t(Zn(G,!1)),t:n,candidates:a,testidPrefix:`${r}-tree`,showSourceToggle:c}):null;if(F){if(!d)return F;const G=Sr(e),V=G?us(G,Y=>{const Z=m(Y);return Z&&Z!==Y&&!Z.startsWith("$t:")?Z:""},Y=>Vt(Y)):[];return i.jsxs("div",{"data-testid":`${r}-builder`,style:{display:"flex",flexDirection:"column",gap:6,width:"100%",minWidth:0},children:[i.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",gap:6},children:[i.jsxs("button",{type:"button","data-testid":`${r}-to-plain`,onClick:()=>S(!0),style:V_,title:n("layout_editor.value_tree.to_plain"),"aria-label":n("layout_editor.value_tree.to_plain"),children:["↩ ",n("layout_editor.value_tree.to_plain")]}),i.jsxs("button",{type:"button","data-testid":`${r}-builder-collapse`,onClick:()=>C(!1),style:uc,title:n("layout_editor.value_tree.collapse"),"aria-label":n("layout_editor.value_tree.collapse"),children:["▴ ",n("layout_editor.value_tree.collapse")]})]}),E&&i.jsxs("div",{"data-testid":`${r}-to-plain-confirm`,style:U_,children:[i.jsx("p",{style:q_,children:n("layout_editor.value_tree.to_plain_lead")}),i.jsx("div",{style:G_,children:V.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:n("layout_editor.prop_i18n.placeholder")}):V.map((Y,Z)=>Y.kind==="chip"?i.jsxs("span",{style:dc,children:["🔗 ",Y.text]},Z):i.jsx("span",{style:Y.kind==="ellipsis"?cc:void 0,children:Y.text},Z))}),i.jsxs("p",{style:Y_,children:["⚠ ",n("layout_editor.value_tree.to_plain_warn")]}),i.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:6},children:[i.jsx("button",{type:"button","data-testid":`${r}-to-plain-confirm-cancel`,onClick:()=>S(!1),style:uc,children:n("layout_editor.value_tree.to_plain_cancel")}),i.jsx("button",{type:"button","data-testid":`${r}-to-plain-confirm-ok`,onClick:()=>{S(!1),C(!1),t(Sr(e)||void 0)},style:X_,children:n("layout_editor.value_tree.to_plain_ok")})]})]}),F]})}}return l&&typeof e=="string"&&a&&a.length>0&&Kt(e)!==null?i.jsx(B_,{value:e,onChange:t,t:n,candidates:a,testidPrefix:`${r}-data`}):i.jsxs("div",{"data-testid":`${r}-binding`,style:tx,children:[i.jsx("code",{style:nx,children:typeof e=="string"?e:""}),i.jsxs("span",{style:ox,children:["🔗 ",n("layout_editor.prop_i18n.bound_code_only")]})]})}if(!T&&typeof e=="string"&&Ul(e)){const N=ql(e);return i.jsxs("div",{"data-testid":`${r}-settings-ref`,style:Q_,children:[i.jsx("div",{"data-testid":`${r}-settings-ref-chips`,style:R_,children:N.map((H,L)=>H.kind==="chip"?i.jsxs("span",{style:ex,children:["🔗 ",H.label]},L):i.jsx("span",{children:H.label},L))}),i.jsxs("button",{type:"button","data-testid":`${r}-settings-ref-edit`,onClick:()=>$(!0),style:pc,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:["✎ ",n("layout_editor.value_tree.edit")]})]})}const B=i.jsxs("button",{type:"button","data-testid":`${r}-toggle`,title:`${n("layout_editor.prop_i18n.edit_languages")} (${Gn(p,n)})`,"aria-label":`${n("layout_editor.prop_i18n.edit_languages")} (${Gn(p,n)})`,"aria-expanded":k,onClick:()=>w(N=>!N),style:pc,children:["🌐 ",z.customKey?k?"⌃":"⌄":"+"]});return i.jsxs("div",{"data-testid":r,style:{display:"flex",flexDirection:"column",gap:4,width:"100%"},children:[i.jsx("div",{style:J_,children:z.paramKey&&z.customKey?i.jsx(L_,{customKey:z.customKey,value:typeof e=="string"?e:"",onChange:t,t:n,candidates:a,testidPrefix:r,showExprButton:l&&d,onToExpression:()=>{C(!0),t(Co(typeof e=="string"?e:""))},langToggle:B}):i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text","data-testid":`${r}-preview`,value:D,placeholder:o??n("layout_editor.prop_i18n.placeholder"),onChange:N=>v(N.target.value),onBlur:()=>{O(),T&&$(!1)},disabled:b,style:Z_}),a&&a.length>0&&i.jsx(P_,{value:typeof e=="string"?e:"",customKey:z.customKey,onChange:t,t:n,candidates:a,testidPrefix:r}),l&&d&&i.jsx("button",{type:"button","data-testid":`${r}-to-expr`,onClick:()=>{C(!0),t(Co(typeof e=="string"?e:""))},title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),style:Io,children:"ƒx"}),B]})}),k&&(z.customKey?i.jsx(Ud,{customKey:z.customKey,templateIdentifier:f,t:n,paramLabels:ds(e),locales:s,onRemoveParam:M},z.customKey):i.jsx("div",{"data-testid":`${r}-expand-hint`,style:rx,children:n("layout_editor.prop_i18n.create_first")}))]})}function L_({customKey:e,value:t,onChange:n,t:o,candidates:r,testidPrefix:s,showExprButton:a=!1,onToExpression:l,langToggle:d}){const{state:c}=Je(),p=c.templateIdentifier,f=c.locale,y=Ft(),[g,m]=u.useState(null),h=u.useRef(""),v=u.useRef(null),b=u.useRef(null),x=u.useRef(null),[k,w]=u.useState(null),[_,C]=u.useState(!1),E=ds(t),S=u.useCallback(async()=>{const D=It(e,f);if(D!==void 0){h.current=D,m(D);return}const M=(await cn(p,e))?.values?.[f]??"";h.current=M,m(M)},[e,p,f]);u.useEffect(()=>{let D=!1;return(async()=>{const O=It(e,f);if(O!==void 0){D||(h.current=O,m(O));return}const M=await cn(p,e);if(D)return;const B=M?.values?.[f]??"";h.current=B,m(B)})(),()=>{D=!0}},[e,p,f]),u.useEffect(()=>{if(typeof window>"u")return;const D=O=>{const M=O.detail;if(M?.templateIdentifier&&M.templateIdentifier!==p)return;const B=It(e,f);B!==void 0&&B!==g&&(h.current=B,m(B))};return window.addEventListener(kt,D),()=>window.removeEventListener(kt,D)},[e,p,f,g]);const T=u.useCallback(D=>{m(D),ko(p,e,f,D).then(()=>{y?.markDirty?.()})},[p,e,f,y]),$=u.useCallback(D=>{n(mo(t,D)),kr(p,e,D,f).then(()=>{S(),y?.markDirty?.()})},[t,n,p,e,f,S,y]),A=u.useCallback(async D=>{if(!(k===null||_)){C(!0);try{g!==null&&g!==h.current&&(await ko(p,e,f,g),h.current=g);const O=await wr(p,f,t,k,D.sourceId,D.path,"scalar");O.kind==="ok"&&(n(O.text),await S(),y?.markDirty?.())}finally{C(!1),w(null)}}},[k,_,g,t,p,e,f,n,S,y]),z=!!r&&r.length>0;return i.jsxs("div",{"data-testid":`${s}-chipfield-wrap`,style:{flex:1,minWidth:0,display:"flex",alignItems:"center",gap:4},children:[i.jsx("div",{ref:v,"data-testid":`${s}-chipfield`,style:{flex:1,minWidth:0},children:g===null?i.jsx("div",{"data-testid":`${s}-chipfield-loading`,style:{fontSize:11,color:"#94a3b8",padding:"4px 0"},children:o("layout_editor.translation.loading")}):i.jsx(mr,{value:g,onChange:T,t:o,paramLabels:E,testIdSuffix:`${s}-field`,onRequestInsert:z?D=>w(D):void 0,onRemoveChip:$,hideInsertButton:!0,caretRef:x})}),z&&i.jsx("button",{ref:b,type:"button","data-testid":`${s}-plus-data-btn`,onClick:()=>w(x.current?x.current():g?.length??0),title:o("layout_editor.inline_binding.insert_data"),"aria-label":o("layout_editor.inline_binding.insert_data"),style:Io,children:"+"}),a&&l&&i.jsx("button",{type:"button","data-testid":`${s}-to-expr`,onClick:l,title:o("layout_editor.value_tree.to_expression"),"aria-label":o("layout_editor.value_tree.to_expression"),style:Io,children:"ƒx"}),d,z&&i.jsx(Xn,{anchorRef:b,open:k!==null,onClose:()=>w(null),children:i.jsx("div",{"data-testid":`${s}-chipfield-picker`,children:i.jsx($t,{candidates:r,t:o,onSelect:D=>{A(D)},testIdSuffix:`${s}-field-insert`,defaultOpen:!0,floating:!1})})})]})}function P_({value:e,customKey:t,onChange:n,t:o,candidates:r,testidPrefix:s}){const{state:a}=Je(),l=a.templateIdentifier,d=a.selectedRoute?.layoutName??null,c=a.locale,p=Ft(),[f,y]=u.useState(!1),[g,m]=u.useState(!1),h=u.useRef(null),v=u.useCallback(k=>{const w=o(k);return w&&w!==k&&!w.startsWith("$t:")?w:""},[o]),b=u.useCallback(k=>k.replace(/\$t:[a-zA-Z0-9._-]+/g,w=>v(w.slice(3))).replace(/\s+/g," ").trim(),[v]),x=u.useCallback(async k=>{if(!g){m(!0);try{if(t){const w=await wr(l,c,e,Number.MAX_SAFE_INTEGER,k.sourceId,k.path,"scalar");w.kind==="ok"&&(n(w.text),p?.markDirty?.())}else{const w=await ls(l,d,c,e,e.length,k.sourceId,k.path,"scalar",b,_=>o(_));w.kind==="ok"&&(n(w.text),p?.markDirty?.())}}finally{m(!1),y(!1)}}},[g,t,l,d,c,e,n,p,o,b]);return i.jsxs("div",{"data-testid":`${s}-plus-data`,style:{display:"inline-block"},children:[i.jsx("button",{ref:h,type:"button","data-testid":`${s}-plus-data-btn`,disabled:g,onClick:()=>y(k=>!k),title:o("layout_editor.inline_binding.insert_data"),"aria-label":o("layout_editor.inline_binding.insert_data"),style:Io,children:"+"}),i.jsx(Xn,{anchorRef:h,open:f,onClose:()=>y(!1),children:i.jsx($t,{candidates:r,t:o,onSelect:k=>{x(k)},testIdSuffix:`${s}-plus-insert`,defaultOpen:!0,floating:!1})})]})}function O_({value:e,t,translate:n,onEdit:o,testidPrefix:r}){const s=us(e,a=>{const l=n(a);return l&&l!==a&&!l.startsWith("$t:")?l:""},a=>Vt(a));return i.jsxs("div",{"data-testid":r,style:H_,children:[i.jsx("div",{"data-testid":`${r}-text`,style:F_,children:s.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:t("layout_editor.prop_i18n.placeholder")}):s.map((a,l)=>a.kind==="chip"?i.jsxs("span",{style:dc,children:["🔗 ",a.text]},l):i.jsx("span",{style:a.kind==="ellipsis"?cc:void 0,children:a.text},l))}),i.jsxs("button",{type:"button","data-testid":`${r}-edit`,onClick:o,style:K_,title:t("layout_editor.value_tree.edit"),"aria-label":t("layout_editor.value_tree.edit"),children:["✎ ",t("layout_editor.value_tree.edit")]})]})}function B_({value:e,onChange:t,t:n,candidates:o,testidPrefix:r}){const[s,a]=u.useState(!1),l=u.useRef(null),d=Vt(e);return i.jsxs("div",{"data-testid":r,style:{display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0},children:[i.jsxs("div",{style:W_,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:j_,children:["🔗 ",d]}),i.jsx("button",{ref:l,type:"button","data-testid":`${r}-change`,onClick:()=>a(c=>!c),style:Io,title:n("layout_editor.value_tree.change_data"),"aria-label":n("layout_editor.value_tree.change_data"),children:n("layout_editor.value_tree.change_data")})]}),i.jsx(Xn,{anchorRef:l,open:s,onClose:()=>a(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:c=>{const p=c.path?c.path.split(".").filter(Boolean):[],f=[c.sourceId,...p].join("?.");t(`{{${f}}}`),a(!1)},testIdSuffix:`${r}-pick`,defaultOpen:!0,floating:!1})})]})}const W_={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},j_={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"3px 8px",borderRadius:12,wordBreak:"break-all"},H_={display:"flex",alignItems:"center",gap:8,width:"100%",minWidth:0},F_={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,padding:"5px 8px",fontSize:12,color:"#334155",border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc",overflow:"hidden"},dc={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"1px 6px",borderRadius:10,whiteSpace:"nowrap"},cc={color:"#94a3b8"},K_={padding:"4px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",whiteSpace:"nowrap",flexShrink:0},uc={padding:"3px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},V_={padding:"3px 10px",fontSize:11,border:"1px solid #fca5a5",borderRadius:6,background:"#fef2f2",color:"#b91c1c",cursor:"pointer",whiteSpace:"nowrap"},U_={display:"flex",flexDirection:"column",gap:6,padding:10,border:"1px solid #fca5a5",borderRadius:8,background:"#fffbeb"},q_={margin:0,fontSize:12,color:"#334155"},G_={padding:"5px 8px",fontSize:12,color:"#0f172a",background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,wordBreak:"break-all"},Y_={margin:0,fontSize:11,color:"#b45309"},X_={padding:"3px 12px",fontSize:11,border:"1px solid #dc2626",borderRadius:6,background:"#dc2626",color:"#fff",cursor:"pointer",whiteSpace:"nowrap"},J_={display:"flex",gap:4,alignItems:"center",width:"100%",minWidth:0},Z_={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%"},pc={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Io={padding:"4px 8px",fontSize:13,lineHeight:1,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Q_={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},R_={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,fontSize:12,color:"#334155"},ex={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"2px 8px",borderRadius:12,whiteSpace:"nowrap"},tx={display:"flex",flexDirection:"column",gap:2,width:"100%"},nx={fontSize:11,background:"#f1f5f9",padding:"3px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},ox={fontSize:10,color:"#64748b"},rx={fontSize:11,color:"#94a3b8",padding:"6px 8px",border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc"};function ys({value:e,onChange:t,candidates:n,t:o}){const r=Array.isArray(e)?e:[],s=n??[],[a,l]=u.useState(!1),d=g=>s.find(m=>m.value===g)?.label??g,c=g=>d(g)!==g,p=g=>{const m=r.filter(h=>h!==g);t(m.length===0?void 0:m)},f=g=>{r.includes(g)||(t([...r,g]),l(!1))},y=s.filter(g=>!r.includes(g.value));return i.jsxs("div",{className:"g7le-widget g7le-widget--tag-input","data-testid":"g7le-widget-tag-input",style:ix,children:[i.jsxs("div",{style:sx,children:[r.map(g=>i.jsxs("span",{"data-testid":`g7le-tag-chip-${g}`,style:ax,children:[i.jsx("span",{style:lx,children:d(g)}),c(g)&&i.jsx("code",{"data-testid":`g7le-tag-chip-id-${g}`,style:dx,children:g}),i.jsx("button",{type:"button","aria-label":`remove ${g}`,"data-testid":`g7le-tag-remove-${g}`,onClick:()=>p(g),style:cx,children:"✕"})]},g)),i.jsx("button",{type:"button","data-testid":"g7le-tag-add",onClick:()=>l(g=>!g),disabled:y.length===0,style:ux,children:o("layout_editor.control.tag_add")})]}),a&&y.length>0&&i.jsx("div",{"data-testid":"g7le-tag-candidates",style:px,children:y.map(g=>{const m=g.value!==g.label;return i.jsxs("button",{type:"button","data-testid":`g7le-tag-candidate-${g.value}`,onClick:()=>f(g.value),style:fx,children:[i.jsx("span",{style:gx,children:g.label}),m&&i.jsx("code",{"data-testid":`g7le-tag-candidate-id-${g.value}`,style:hx,children:g.value})]},g.value)})})]})}const ix={display:"flex",flexDirection:"column",gap:6},sx={display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"},ax={display:"inline-flex",alignItems:"center",gap:4,padding:"2px 6px",fontSize:12,background:"#e0e7ff",color:"#1d4ed8",borderRadius:12},lx={fontSize:12},dx={fontSize:10,color:"#4f46e5",background:"#c7d2fe",borderRadius:4,padding:"0 4px",fontFamily:"monospace"},cx={border:"none",background:"transparent",color:"#1d4ed8",cursor:"pointer",fontSize:10,padding:0,lineHeight:1},ux={padding:"2px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:12,background:"#fff",cursor:"pointer",color:"#475569"},px={display:"flex",flexDirection:"column",maxHeight:180,overflowY:"auto",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff"},fx={display:"flex",flexDirection:"column",gap:1,textAlign:"left",padding:"6px 10px",fontSize:12,border:"none",borderBottom:"1px solid #f1f5f9",background:"transparent",cursor:"pointer",color:"#0f172a"},gx={fontSize:12,color:"#0f172a"},hx={fontSize:10,color:"#64748b",fontFamily:"monospace"},fc=new Map;function Xe(e,t){fc.set(e,t)}function Tn(e){return e?fc.get(e)??null:null}function Tr(e,t){const o=e("meta",{})?.[t];return typeof o=="string"?o:""}function yx({getValue:e,patch:t,t:n,fieldT:o,spec:r,permissionCandidates:s,bindingCandidates:a}){const l=o??n,d=Tr(e,"title"),c=Tr(e,"description"),p=Tr(e,"icon"),f=Tr(e,"editor_label"),y=e("permissions",[]),g=u.useCallback((b,x)=>{const k={...e("meta",{})??{}};x===void 0||x===""?delete k[b]:k[b]=x,t("meta",k)},[e,t]),m=u.useCallback(b=>{const x=Array.isArray(b)?b:[];x.length===0?t("permissions",void 0):t("permissions",x)},[t]),h=Tn("icon-picker"),v=Ct(r,"icon-picker")??Ct(r,"icon")??(r?.controls?Object.values(r.controls).find(b=>b&&typeof b=="object"&&b.widget==="icon-picker")??null:null);return i.jsxs("div",{className:"g7le-meta-form","data-testid":"g7le-meta-form",style:mx,children:[i.jsx(Ao,{label:n("layout_editor.page_settings.meta.title"),children:i.jsx(vt,{value:d,onChange:b=>g("title",b),t:l,testidPrefix:"g7le-meta-title",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.description"),children:i.jsx(vt,{value:c,onChange:b=>g("description",b),t:l,testidPrefix:"g7le-meta-description",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.icon"),children:h?i.jsx(bx,{icon:p,t:n,onChange:b=>g("icon",typeof b=="string"?b:void 0),IconWidget:h,iconControl:v??{widget:"icon-picker"}}):i.jsx("input",{type:"text","data-testid":"g7le-meta-icon",value:p,placeholder:n("layout_editor.page_settings.meta.icon_placeholder"),onChange:b=>g("icon",b.target.value),style:wx})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.editor_label"),hint:n("layout_editor.page_settings.meta.editor_label_hint"),children:i.jsx(vt,{value:f,onChange:b=>g("editor_label",b),t:l,testidPrefix:"g7le-meta-editor-label",candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0})}),i.jsx(Ao,{label:n("layout_editor.page_settings.meta.permissions"),hint:n("layout_editor.page_settings.meta.permissions_hint"),children:i.jsx("div",{"data-testid":"g7le-meta-permissions",children:i.jsx(ys,{control:{widget:"tag-input"},value:y,onChange:m,t:n,candidates:s})})})]})}function Ao({label:e,hint:t,children:n}){return i.jsxs("div",{className:"g7le-meta-field",style:vx,children:[i.jsx("label",{style:_x,children:e}),n,t?i.jsxs("p",{style:xx,children:["ⓘ ",t]}):null]})}function bx({icon:e,onChange:t,t:n,IconWidget:o,iconControl:r}){const[s,a]=u.useState(!1),l=e?/\bfa-/.test(e)?i.jsx("i",{className:e,"aria-hidden":"true"}):e:"📄";return i.jsxs("div",{"data-testid":"g7le-meta-icon",style:{display:"flex",flexDirection:"column",gap:6,minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":"g7le-meta-icon-toggle","aria-expanded":s,onClick:()=>a(d=>!d),style:{display:"flex",alignItems:"center",gap:8,padding:"5px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",fontSize:12,color:"#475569",width:"fit-content"},children:[i.jsx("span",{style:{fontSize:16},"aria-hidden":"true",children:l}),i.jsx("span",{children:e||n("layout_editor.page_settings.meta.icon_placeholder")}),i.jsx("span",{"aria-hidden":"true",children:s?"▴":"▾"})]}),s?i.jsx(o,{control:r,value:e,onChange:d=>{t(d),a(!1)},t:n}):null]})}const mx={display:"flex",flexDirection:"column",gap:16,minWidth:0},vx={display:"flex",flexDirection:"column",gap:6,minWidth:0},_x={fontSize:13,fontWeight:600,color:"#0f172a"},xx={margin:0,fontSize:11,color:"#94a3b8"},wx={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%"};function Dr({checked:e,onChange:t,label:n,testid:o,ariaLabel:r,disabled:s=!1}){return i.jsxs("label",{style:{...Ex,...s?{cursor:"not-allowed"}:{}},children:[i.jsx("button",{type:"button","data-testid":o,role:"switch","aria-checked":e,"aria-label":r,disabled:s,onClick:()=>t(!e),style:Ix(e,s),children:i.jsx("span",{style:Ax(e)})}),n!=null?i.jsx("span",{style:Tx(s),children:n}):null]})}function Nr({disabled:e,children:t,testid:n,style:o}){return i.jsx("div",{"data-testid":n,"data-disabled":e?"true":"false","aria-disabled":e,style:{...o,...e?{opacity:.5,pointerEvents:"none",userSelect:"none"}:{}},children:t})}function kx({filled:e,sourceLabel:t,onRevert:n,revertLabel:o,hint:r,children:s,testid:a}){const l=e&&typeof n=="function",d=!e&&t!=null&&t!=="";return i.jsxs("div",{"data-testid":a,style:Sx,children:[s,l?i.jsxs("button",{type:"button","data-testid":a?`${a}-revert`:void 0,onClick:n,title:r??o,"aria-label":o,style:$x,children:["↩ ",o]}):d?i.jsx("span",{"data-testid":a?`${a}-source`:void 0,title:r,style:Cx,children:t}):null]})}const Sx={position:"relative",minWidth:0,width:"100%"},Cx={position:"absolute",right:8,top:"50%",transform:"translateY(-50%)",fontSize:10,color:"#94a3b8",background:"#f1f5f9",borderRadius:4,padding:"1px 6px",pointerEvents:"none",maxWidth:"45%",overflow:"hidden",whiteSpace:"nowrap",textOverflow:"ellipsis"},$x={position:"absolute",right:6,top:"50%",transform:"translateY(-50%)",fontSize:10,color:"#0369a1",background:"#e0f2fe",border:"1px solid #bae6fd",borderRadius:4,padding:"1px 6px",cursor:"pointer",whiteSpace:"nowrap"},Ex={display:"inline-flex",alignItems:"center",gap:8,cursor:"pointer"},Ix=(e,t)=>({position:"relative",width:38,height:22,flexShrink:0,borderRadius:999,border:"none",padding:0,background:t?"#e2e8f0":e?"#2563eb":"#cbd5e1",cursor:t?"not-allowed":"pointer",transition:"background 0.15s ease"}),Ax=e=>({position:"absolute",top:2,left:e?18:2,width:18,height:18,borderRadius:"50%",background:"#fff",boxShadow:"0 1px 2px rgba(0,0,0,0.2)",transition:"left 0.15s ease"}),Tx=e=>({fontSize:13,fontWeight:600,color:e?"#94a3b8":"#0f172a"}),Dx=/\$(?:core|module|plugin)_settings:[a-zA-Z0-9._:-]+/;function Nx(e){const t=[];let n=!1;const o=s=>{t.push({kind:"text",text:s}),n=!0},r=(s,a,l)=>{n||o(""),t.push({kind:"chip",raw:s,text:a,removable:l}),n=!1};for(const s of zx(e))s.kind==="text"?o(s.raw):r(s.raw,s.label,s.removable);return n||o(""),t}function zx(e){const t=[];if(typeof e!="string"||e.length===0)return t;const n=e.length;let o=0,r=0;const s=a=>{a>r&&t.push({kind:"text",raw:e.slice(r,a),label:"",removable:!1})};for(;o<n;){if(e[o]==="{"&&e[o+1]==="{"){let a=o+2;for(;a<n&&!(e[a]==="}"&&e[a+1]==="}");)a++;if(a>=n){o++;continue}s(o);const l=e.slice(o,a+2);t.push({kind:"chip",raw:l,label:Vt(l),removable:!0}),o=a+2,r=o;continue}if(e[o]==="$"){const a=Dx.exec(e.slice(o));if(a&&a.index===0){s(o);const l=a[0],d=l.split(":").pop()||l;t.push({kind:"chip",raw:l,label:d,removable:!1}),o+=l.length,r=o;continue}}o++}return s(n),t}function Mx(e,t,n,o){const r=[];let s=!1;return e.forEach((a,l)=>{if(a.kind==="chip"){if(!s&&a.raw===t){s=!0;return}r.push(a.raw??"")}else{const d=a.text??"";if(l===n){const c=Math.max(0,Math.min(o,d.length));r.push(d.slice(0,c),t,d.slice(c))}else r.push(d)}}),r.join("")}function Lx(e,t,n){const o=Math.max(0,Math.min(t,e.length));return e.slice(0,o)+n+e.slice(o)}function Px(e,t){const n=e.indexOf(t);return n<0?e:e.slice(0,n)+e.slice(n+t.length)}function bs({value:e,onChange:t,t:n,candidates:o,onDone:r,testidPrefix:s="g7le-binding-chip-text",disabled:a=!1}){const l=u.useMemo(()=>Nx(e),[e]),d=u.useRef(null),c=!!o&&o.length>0,[p,f]=u.useState(null),[y,g]=u.useState(null),[m,h]=u.useState(null),[v,b]=u.useState(!1),x=u.useRef(0),k=u.useRef(null),w=u.useMemo(()=>l.map(L=>L.kind==="chip"?`c:${L.raw}`:"t").join("|"),[l]),_=u.useRef(null),C=u.useRef([]),E=u.useRef(!1);u.useLayoutEffect(()=>{const L=E.current;E.current=!1,!(!L&&_.current===w)&&(_.current=w,l.forEach((I,P)=>{if(I.kind!=="text")return;const F=C.current[P];F&&F.textContent!==(I.text??"")&&(F.textContent=I.text??"")}))},[w,l,e]);const S=u.useCallback(()=>{const L=d.current;if(!L)return e;const I=[];for(const P of Array.from(L.querySelectorAll("[data-seg]")))P.dataset.seg==="chip"?I.push(P.dataset.raw??""):I.push(P.textContent??"");return I.join("")},[e]),T=u.useCallback(()=>{a||t(S())},[a,t,S]),$=u.useRef(null),A=u.useRef(!1),z=(L,I)=>{if(!a){L.preventDefault(),L.stopPropagation(),$.current={raw:I,pointerId:L.pointerId},A.current=!1,f(I);try{L.currentTarget.setPointerCapture(L.pointerId)}catch{}}},D=L=>{const I=$.current;if(!I||L.pointerId!==I.pointerId)return;A.current=!0;const P=B(L.clientX,L.clientY);P&&(g({slotIdx:P.slotIdx,offset:P.offset}),h(Bx(d.current,P.slotIdx,P.offset)))},O=(L,I)=>{if(L.stopPropagation(),A.current){A.current=!1;return}E.current=!0,t(Px(e,I))},M=L=>{const I=$.current;if(!I||L.pointerId!==I.pointerId)return;try{L.currentTarget.releasePointerCapture(L.pointerId)}catch{}$.current=null,A.current&&typeof setTimeout=="function"&&setTimeout(()=>{A.current=!1},0);const P=B(L.clientX,L.clientY);if(f(null),g(null),h(null),!P)return;const F=d.current;if(!F)return;const V=Array.from(F.querySelectorAll("[data-seg]")).map(Z=>Z.dataset.seg==="chip"?{kind:"chip",raw:Z.dataset.raw??""}:{kind:"text",text:Z.textContent??""}),Y=Mx(V,I.raw,P.slotIdx,P.offset);Y!==e&&(E.current=!0,t(Y))},B=(L,I)=>{const P=d.current;if(!P)return null;const F=Array.from(P.querySelectorAll("[data-seg]")),G=P.ownerDocument,V=G.elementFromPoint(L,I),Y=V?.closest('[data-seg="text"]');if(Y)return{slotIdx:F.indexOf(Y),offset:Ox(G,Y,L,I)};const Z=V?.closest('[data-seg="chip"]');if(Z){const re=F.indexOf(Z),U=Z.getBoundingClientRect();if(L<U.left+U.width/2&&re-1>=0){const ee=F[re-1];return{slotIdx:re-1,offset:(ee.textContent??"").length}}if(re+1<F.length)return{slotIdx:re+1,offset:0}}const se=F.map((re,U)=>({s:re,i:U})).filter(re=>re.s.dataset.seg==="text").pop();return se?{slotIdx:se.i,offset:(se.s.textContent??"").length}:null},N=()=>{a||!c||(x.current=Wx(d.current,e.length),b(L=>!L))},H=L=>{const I=L.path?L.path.split(".").filter(Boolean):[],P=[L.sourceId,...I].join("?.");E.current=!0,t(Lx(e,x.current,`{{${P}}}`)),b(!1)};return i.jsxs("div",{"data-testid":s,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsxs("div",{ref:d,"data-testid":`${s}-box`,className:"g7le-binding-chip-input",style:jx,children:[l.map((L,I)=>L.kind==="chip"?i.jsxs("span",{"data-seg":"chip","data-raw":L.raw,"data-testid":`${s}-chip-${I}`,contentEditable:!1,onPointerDown:P=>z(P,L.raw??""),onPointerMove:D,onPointerUp:M,title:L.removable?n("layout_editor.value_tree.change_data"):n("layout_editor.prop_i18n.bound_code_only"),style:{...Vx,...p===L.raw?Ux:null,touchAction:"none"},children:[i.jsx("span",{"aria-hidden":"true",style:qx,children:"⠿"}),i.jsx("span",{"aria-hidden":"true",style:{marginRight:3},children:"🔗"}),i.jsx("span",{children:L.text}),L.removable&&!a&&i.jsx("button",{type:"button","data-testid":`${s}-chip-remove-${I}`,"aria-label":n("layout_editor.inline_binding.clear"),title:n("layout_editor.inline_binding.clear"),onPointerDown:P=>{P.stopPropagation(),P.preventDefault()},onClick:P=>O(P,L.raw??""),style:Gx,children:"✕"})]},`chip-${L.raw}-${I}`):i.jsx("span",{ref:P=>{C.current[I]=P},"data-seg":"text",contentEditable:!a,suppressContentEditableWarning:!0,onInput:T,"data-testid":`${s}-text-${I}`,style:{...Hx,...p?Fx:null,...y?.slotIdx===I?Kx:null}},`text-slot-${I}`)),m&&i.jsx("span",{"aria-hidden":"true","data-testid":`${s}-caret`,style:{position:"absolute",left:m.left,top:m.top,width:2,height:m.height,background:"#2563eb",borderRadius:1,pointerEvents:"none",boxShadow:"0 0 0 1px rgba(37,99,235,0.3)"}})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,flexWrap:"wrap"},children:[r&&i.jsxs("button",{type:"button","data-testid":`${s}-done`,onClick:r,style:gc,title:n("layout_editor.value_tree.done"),"aria-label":n("layout_editor.value_tree.done"),children:["✓ ",n("layout_editor.value_tree.done")]}),c&&i.jsxs("div",{style:{position:"relative"},children:[i.jsxs("button",{ref:k,type:"button","data-testid":`${s}-insert`,onClick:N,disabled:a,style:gc,title:n("layout_editor.inline_binding.insert_data"),"aria-label":n("layout_editor.inline_binding.insert_data"),children:["+ ",n("layout_editor.inline_binding.insert_data")]}),i.jsx(Xn,{anchorRef:k,open:v,onClose:()=>b(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:H,testIdSuffix:`${s}-insert`,defaultOpen:!0,floating:!1})})]}),i.jsx("span",{style:Yx,children:n("layout_editor.translation.placeholder_locked")})]})]})}function Ox(e,t,n,o){const r=(t.textContent??"").length;try{const a=e;let l=null,d=0;if(typeof a.caretRangeFromPoint=="function"){const c=a.caretRangeFromPoint(n,o);c&&(l=c.startContainer,d=c.startOffset)}else if(typeof a.caretPositionFromPoint=="function"){const c=a.caretPositionFromPoint(n,o);c&&(l=c.offsetNode,d=c.offset)}if(l&&t.contains(l))return Math.max(0,Math.min(d,r))}catch{}const s=t.getBoundingClientRect();return n>s.left+s.width/2?r:0}function Bx(e,t,n){if(!e)return null;const r=Array.from(e.querySelectorAll("[data-seg]"))[t];if(!r)return null;const s=e.getBoundingClientRect();try{const d=r.firstChild,c=e.ownerDocument;if(d&&d.nodeType===Node.TEXT_NODE&&(d.textContent??"").length>0){const p=c.createRange(),f=Math.max(0,Math.min(n,(d.textContent??"").length));p.setStart(d,f),p.setEnd(d,f);const y=p.getClientRects(),g=y.length>0?y[0]:p.getBoundingClientRect();if(g&&(g.height>0||g.width>=0))return{left:g.left-s.left,top:g.top-s.top,height:g.height||r.getBoundingClientRect().height}}}catch{}const a=r.getBoundingClientRect();return{left:(n>0?a.right:a.left)-s.left,top:a.top-s.top,height:a.height||16}}function Wx(e,t){if(!e)return t;try{const n=e.ownerDocument.defaultView?.getSelection();if(!n||n.rangeCount===0)return t;const o=n.getRangeAt(0);let r=o.startContainer,s=null;for(;r&&r!==e;){if(r instanceof HTMLElement&&r.dataset.seg==="text"){s=r;break}r=r.parentNode}if(!s||!e.contains(s))return t;const a=Array.from(e.querySelectorAll("[data-seg]"));let l=0;for(const d of a){if(d===s)break;l+=d.dataset.seg==="chip"?(d.dataset.raw??"").length:(d.textContent??"").length}return l+o.startOffset}catch{return t}}const jx={position:"relative",display:"flex",flexWrap:"wrap",alignItems:"center",gap:2,minHeight:30,padding:"4px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",fontSize:12,lineHeight:1.6,minWidth:0,width:"100%",boxSizing:"border-box"},Hx={outline:"none",minWidth:8,minHeight:"1.2em",whiteSpace:"pre-wrap",color:"#0f172a",display:"inline-block",wordBreak:"break-all"},Fx={boxShadow:"inset 0 -2px 0 #93c5fd",background:"#eff6ff",borderRadius:2},Kx={background:"#dbeafe",boxShadow:"inset 0 -2px 0 #2563eb"},Vx={display:"inline-flex",alignItems:"center",gap:1,background:"linear-gradient(180deg,#eff6ff,#dbeafe)",color:"#1d4ed8",border:"1px dashed #60a5fa",borderRadius:9999,padding:"1px 8px 1px 4px",margin:"0 1px",fontSize:11,fontWeight:600,cursor:"grab",userSelect:"none",whiteSpace:"nowrap",boxShadow:"0 1px 2px rgba(37,99,235,0.18)",verticalAlign:"middle"},Ux={opacity:.5,borderStyle:"solid",cursor:"grabbing"},qx={color:"#2563eb",opacity:.7,fontSize:11,marginRight:1,cursor:"grab"},Gx={marginLeft:4,width:14,height:14,lineHeight:"12px",padding:0,border:"none",borderRadius:9999,background:"rgba(29,78,216,0.12)",color:"#1d4ed8",fontSize:9,fontWeight:700,cursor:"pointer",display:"inline-flex",alignItems:"center",justifyContent:"center",flex:"0 0 auto"},gc={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Yx={fontSize:10,color:"#94a3b8"},Xx=ql;function ms(e){return _b(e)}function Ve({value:e,onChange:t,t:n,candidates:o,placeholder:r,testidPrefix:s="g7le-data-chip-value",readOnly:a=!1,enableExpressionTree:l=!0}){const d=typeof e=="string"?e:"",c=!!o&&o.length>0,[p,f]=u.useState(!1),[y,g]=u.useState(!1),[m,h]=u.useState(!1),v=u.useCallback(E=>i.jsx(Ve,{value:E.value,onChange:S=>E.onChange(S),t:E.t,candidates:E.candidates,testidPrefix:E.testidPrefix,enableExpressionTree:!0}),[]),b=()=>i.jsx("div",{style:{display:"flex",gap:4,alignItems:"center",width:"100%",minWidth:0},children:i.jsx("input",{type:"text","data-testid":`${s}-input`,value:d,placeholder:r??n("layout_editor.page_settings.seo.value_placeholder"),readOnly:a,onChange:E=>t(E.target.value),onFocus:()=>f(!0),onBlur:()=>f(!1),style:{flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%",background:a?"#f1f5f9":"#fff",color:a?"#64748b":"#0f172a"}})}),x=u.useCallback(E=>{const S=d?`${d} ${E.expression}`:E.expression;t(S)},[d,t]);if(!l||a)return i.jsxs("div",{"data-testid":s,style:pn,children:[b(),!a&&c&&i.jsx($t,{candidates:o,t:n,onSelect:x,testIdSuffix:`${s}-pick`})]});const k=Nv(d),w=Gd(d),_=Kt(d),C=E=>{const S=Sr(d),T=S?us(S,()=>"",$=>Vt($)):[];return i.jsxs(i.Fragment,{children:[i.jsx("div",{style:{display:"flex",justifyContent:"flex-start"},children:i.jsxs("button",{type:"button","data-testid":`${E}-to-plain`,onClick:()=>h(!0),style:Rx,title:n("layout_editor.value_tree.to_plain"),"aria-label":n("layout_editor.value_tree.to_plain"),children:["↩ ",n("layout_editor.value_tree.to_plain")]})}),m&&i.jsxs("div",{"data-testid":`${E}-to-plain-confirm`,style:e0,children:[i.jsx("p",{style:t0,children:n("layout_editor.value_tree.to_plain_lead")}),i.jsx("div",{style:n0,children:T.length===0?i.jsx("span",{style:{color:"#94a3b8"},children:n("layout_editor.page_settings.seo.value_placeholder")}):T.map(($,A)=>$.kind==="chip"?i.jsxs("span",{style:vs,children:["🔗 ",$.text]},A):i.jsx("span",{style:$.kind==="ellipsis"?{color:"#94a3b8"}:void 0,children:$.text},A))}),i.jsxs("p",{style:o0,children:["⚠ ",n("layout_editor.value_tree.to_plain_warn")]}),i.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:6},children:[i.jsx("button",{type:"button","data-testid":`${E}-to-plain-confirm-cancel`,onClick:()=>h(!1),style:Ut,children:n("layout_editor.value_tree.to_plain_cancel")}),i.jsx("button",{type:"button","data-testid":`${E}-to-plain-confirm-ok`,onClick:()=>{h(!1),t(Sr(d)||"")},style:r0,children:n("layout_editor.value_tree.to_plain_ok")})]})]})]})};if(!p&&(k==="expression"||w)){if(w)return i.jsxs("div",{"data-testid":s,style:pn,children:[!a&&C(`${s}-seg`),i.jsx(gs,{value:d,onChange:S=>t(S),t:n,candidates:o,testidPrefix:`${s}-seg`,renderLeafInput:v})]});const E=un(d);return i.jsxs("div",{"data-testid":s,style:pn,children:[!a&&C(`${s}-tree`),i.jsx(nc,{node:E.node,onChange:S=>t(Zn(S,!1)),t:n,candidates:o,testidPrefix:`${s}-tree`,renderLeafInput:v})]})}if(_&&!p&&!y)return i.jsx(Jx,{value:d,onChange:t,t:n,candidates:o,testidPrefix:s,onEditRaw:()=>g(!0)});if(!p&&!y&&ms(d)){const E=Xx(d);return i.jsx("div",{"data-testid":s,style:pn,children:i.jsxs("div",{style:hc,children:[i.jsx("div",{"data-testid":`${s}-chips`,style:Zx,children:E.map((S,T)=>S.kind==="chip"?i.jsxs("span",{style:vs,children:["🔗 ",S.label]},T):i.jsx("span",{children:S.label},T))}),i.jsxs("button",{type:"button","data-testid":`${s}-edit-raw`,onClick:()=>g(!0),style:Ut,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:["✎ ",n("layout_editor.value_tree.edit")]})]})})}return y&&ms(d)?i.jsxs("div",{"data-testid":s,style:pn,children:[i.jsx(bs,{value:d,onChange:t,t:n,candidates:o,disabled:a,onDone:()=>g(!1),testidPrefix:`${s}-chipedit`}),!a&&k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]}),k==="raw"&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]}):!p&&ms(d)?i.jsxs("div",{"data-testid":s,style:pn,children:[i.jsx(bs,{value:d,onChange:t,t:n,candidates:o,disabled:a,onDone:()=>{g(!1),f(!1)},testidPrefix:`${s}-chipedit`}),!a&&k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]}),k==="raw"&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]}):i.jsxs("div",{"data-testid":s,style:pn,children:[b(),i.jsxs("div",{style:Qx,children:[c&&i.jsx($t,{candidates:o,t:n,onSelect:x,testIdSuffix:`${s}-pick`}),k!=="raw"&&i.jsxs("div",{style:{display:"flex",gap:4},children:[i.jsxs("button",{type:"button","data-testid":`${s}-to-expr`,onClick:()=>t(Co(d)),style:Ut,title:n("layout_editor.value_tree.to_expression"),"aria-label":n("layout_editor.value_tree.to_expression"),children:["ƒx ",n("layout_editor.value_tree.to_expression")]}),i.jsxs("button",{type:"button","data-testid":`${s}-to-fallback`,onClick:()=>t(ps(d)),style:Ut,title:n("layout_editor.value_tree.segment.fallback"),"aria-label":n("layout_editor.value_tree.segment.fallback"),children:["?? ",n("layout_editor.value_tree.segment.fallback")]})]})]}),k==="raw"&&!p&&i.jsxs("p",{"data-testid":`${s}-raw-hint`,style:_s,children:["ⓘ ",n("layout_editor.value_tree.raw_expression_hint")]})]})}function Jx({value:e,onChange:t,t:n,candidates:o,testidPrefix:r,onEditRaw:s}){const[a,l]=u.useState(!1),d=u.useRef(null),c=Vt(e),p=!!o&&o.length>0,f=Kt(e)?.localeFn;return i.jsxs("div",{"data-testid":r,style:pn,children:[i.jsxs("div",{style:hc,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:vs,children:["🔗 ",c]}),p&&i.jsx("button",{ref:d,type:"button","data-testid":`${r}-change`,onClick:()=>l(y=>!y),style:Ut,title:n("layout_editor.value_tree.change_data"),"aria-label":n("layout_editor.value_tree.change_data"),children:n("layout_editor.value_tree.change_data")}),i.jsx("button",{type:"button","data-testid":`${r}-edit-raw`,onClick:s,style:Ut,title:n("layout_editor.value_tree.edit"),"aria-label":n("layout_editor.value_tree.edit"),children:"✎"})]}),p&&i.jsx(Xn,{anchorRef:d,open:a,onClose:()=>l(!1),children:i.jsx($t,{candidates:o,t:n,onSelect:y=>{if(f){const h=[y.sourceId,...y.path?y.path.split(".").filter(Boolean):[]].join(".");t(`{{${f}(${h})}}`),l(!1);return}const g=y.path?y.path.split(".").filter(Boolean):[],m=[y.sourceId,...g].join("?.");t(`{{${m}}}`),l(!1)},testIdSuffix:`${r}-pick`,defaultOpen:!0,floating:!1})})]})}const pn={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0},hc={display:"flex",alignItems:"center",gap:6,width:"100%",minWidth:0,flexWrap:"wrap"},Zx={flex:1,minWidth:0,display:"flex",flexWrap:"wrap",alignItems:"center",gap:4,padding:"5px 8px",fontSize:12,color:"#334155",border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc"},vs={fontSize:11,background:"#eef2ff",color:"#4338ca",padding:"2px 8px",borderRadius:12,whiteSpace:"nowrap"},Ut={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Qx={display:"flex",flexWrap:"wrap",alignItems:"center",gap:6},_s={margin:"2px 0 0",fontSize:11,color:"#94a3b8",lineHeight:1.4},Rx={padding:"3px 10px",fontSize:11,border:"1px solid #fca5a5",borderRadius:6,background:"#fef2f2",color:"#b91c1c",cursor:"pointer",whiteSpace:"nowrap"},e0={display:"flex",flexDirection:"column",gap:6,padding:10,border:"1px solid #fca5a5",borderRadius:8,background:"#fffbeb"},t0={margin:0,fontSize:12,color:"#334155"},n0={padding:"5px 8px",fontSize:12,color:"#0f172a",background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,wordBreak:"break-all",display:"flex",flexWrap:"wrap",gap:4,alignItems:"center"},o0={margin:0,fontSize:11,color:"#b45309"},r0={padding:"3px 12px",fontSize:11,border:"1px solid #dc2626",borderRadius:6,background:"#dc2626",color:"#fff",cursor:"pointer",whiteSpace:"nowrap"};let yc=0;function zr({value:e,onChange:t,keyField:n,valueField:o="content",t:r,candidates:s,testidPrefix:a="g7le-kv-chip-editor",keyPlaceholder:l,valuePlaceholder:d,addLabel:c,renderKeyInput:p}){const f=u.useMemo(()=>(Array.isArray(e)?e:[]).map(k=>({id:yc++,k:typeof k[n]=="string"?k[n]:"",v:typeof k[o]=="string"?k[o]:""})),[]),[y,g]=u.useState(f),m=u.useCallback(k=>{g(k);const w=k.filter(_=>_.k.trim()!=="").map(_=>({[n]:_.k,[o]:_.v}));t(w)},[n,o,t]),h=u.useCallback(()=>{m([...y,{id:yc++,k:"",v:""}])},[y,m]),v=u.useCallback(k=>{m(y.filter(w=>w.id!==k))},[y,m]),b=u.useCallback((k,w)=>{m(y.map(_=>_.id===k?{..._,k:w}:_))},[y,m]),x=u.useCallback((k,w)=>{m(y.map(_=>_.id===k?{..._,v:w}:_))},[y,m]);return i.jsxs("div",{"data-testid":a,style:{display:"flex",flexDirection:"column",gap:6},children:[y.map(k=>i.jsxs("div",{"data-testid":`${a}-row-${k.id}`,style:{display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},children:[i.jsxs("div",{style:{flex:1,minWidth:0,display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:{display:"flex",flexDirection:"column",gap:2,minWidth:0},children:p?i.jsx("div",{style:{minWidth:0},children:p({value:k.k,onChange:w=>b(k.id,w),testid:`${a}-key-${k.id}`,placeholder:l??r(`layout_editor.page_settings.seo.extra_${n}`)})}):i.jsx("input",{type:"text","data-testid":`${a}-key-${k.id}`,value:k.k,placeholder:l??r(`layout_editor.page_settings.seo.extra_${n}`),onChange:w=>b(k.id,w.target.value),style:{width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,boxSizing:"border-box"}})}),i.jsx("div",{style:{minWidth:0},children:i.jsx(Ve,{value:k.v,onChange:w=>x(k.id,w),t:r,candidates:s,placeholder:d??r("layout_editor.page_settings.seo.extra_content"),testidPrefix:`${a}-value-${k.id}`})})]}),i.jsx("button",{type:"button","data-testid":`${a}-remove-${k.id}`,onClick:()=>v(k.id),title:r("layout_editor.page_settings.seo.remove"),"aria-label":r("layout_editor.page_settings.seo.remove"),style:{flex:"0 0 auto",padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]},k.id)),i.jsxs("button",{type:"button","data-testid":`${a}-add`,onClick:h,style:{alignSelf:"flex-start",padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},children:["+ ",c??r("layout_editor.page_settings.seo.add_extra")]})]})}const i0=["website","article","product","profile"],s0=["summary","summary_large_image","app","player"];function a0(e){switch(e){case"core":return"layout_editor.page_settings.seo.og_source_core";case"inherited":return"layout_editor.page_settings.seo.og_source_inherited";case"filter":return"layout_editor.page_settings.seo.og_source_filter";case"layout":return null;default:return e?"layout_editor.page_settings.seo.og_source_module":null}}function bc(e){const t={};for(const n of e??[])t[n.key]=n;return t}function mc(e,t){return!!e&&t&&typeof e.sourceExpr=="string"&&e.sourceExpr!==""&&!e.overriddenByLayout&&!e.lockedByFilter}function vc({label:e,sourceExpr:t,onReplace:n,t:o,testid:r}){return i.jsxs("div",{"data-testid":r,style:b0,children:[i.jsxs("span",{"data-testid":`${r}-chip`,style:m0,title:t,children:["🔗 ",e||t]}),i.jsx("span",{"data-testid":`${r}-badge`,style:v0,children:o("layout_editor.page_settings.seo.auto_chip_source")}),i.jsx("button",{type:"button","data-testid":`${r}-replace`,onClick:n,style:_0,children:o("layout_editor.page_settings.seo.auto_chip_replace")})]})}function l0({og:e,twitter:t,onChangeOg:n,onChangeTwitter:o,ogPreview:r,twitterPreview:s,defaultsAvailable:a=!1,candidates:l,t:d,testidPrefix:c="g7le-seo-og"}){const p=e&&typeof e=="object"?e:{},f=t&&typeof t=="object"?t:{},[y,g]=u.useState(p.image_height!=null||p.image_type!=null||p.image_secure_url!=null),m=bc(r),h=bc(s),v=u.useCallback((T,$)=>{const A={...p};$===void 0||$===""||$===null?delete A[T]:A[T]=$,n(A)},[p,n]),b=u.useCallback((T,$)=>{const A={...f};$===void 0||$===""||$===null?delete A[T]:A[T]=$,o(A)},[f,o]),x=(T,$,A)=>!A||!a||!A.lockedByFilter?null:i.jsx("div",{style:{display:"flex",gap:6,alignItems:"center",marginTop:2},children:i.jsxs("span",{"data-testid":`${T}-locked-${$}`,style:g0,children:["🔒 ",d("layout_editor.page_settings.seo.og_locked")]})}),k=T=>{if(!T||!a||T.lockedByFilter||T.overriddenByLayout)return;const $=a0(T.source);return $?d($):void 0},w=(T,$)=>T&&a?T.overriddenByLayout===!0:$!=="",_=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),B?i.jsx("div",{"data-testid":`${T}-locked-value-${$}`,style:f0,children:String(M?.effectiveValue??"")}):mc(M,a)?i.jsx(vc,{label:M.label??"",sourceExpr:M.sourceExpr,onReplace:()=>D($,M.sourceExpr),t:d,testid:`${T}-${$}-auto`}):i.jsx(vt,{value:N,onChange:H=>D($,H??""),t:d,candidates:l,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-field`,enableExpressionTree:!0,expressionTreeCollapsible:!0}),x(T,$,M)]})},C=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),mc(M,a)?i.jsx(vc,{label:M.label??"",sourceExpr:M.sourceExpr,onReplace:()=>D($,M.sourceExpr),t:d,testid:`${T}-${$}-auto`}):i.jsx(Ve,{value:N,onChange:H=>D($,H),t:d,candidates:l,readOnly:B,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-field`}),x(T,$,M)]})},E=(T,$,A,z,D,O)=>{const M=O[$],B=!!M?.lockedByFilter,N=typeof z[$]=="string"?z[$]:"",H=w(M,N),L=H?N:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsx("label",{style:to,children:d(A)}),i.jsx(kx,{filled:H,sourceLabel:k(M),onRevert:B?void 0:()=>D($,void 0),revertLabel:d("layout_editor.page_settings.seo.revert_default"),testid:`${T}-${$}-overlay`,children:i.jsx("div",{"data-testid":`${T}-${$}-input`,style:{minWidth:0},children:i.jsx(Ve,{value:L,onChange:I=>D($,I),t:d,candidates:l,readOnly:B,placeholder:a&&M?.effectiveValue!=null?String(M.effectiveValue):void 0,testidPrefix:`${T}-${$}-chip`})})}),x(T,$,M)]})},S=(T,$,A,z,D,O,M)=>{const B=M[$],N=typeof D[$]=="string"?D[$]:"",H=w(B,N),L=k(B),I=H?N:"";return i.jsxs("div",{"data-testid":`${T}-${$}`,style:Dn,children:[i.jsxs("div",{style:c0,children:[i.jsx("label",{style:to,children:d(A)}),L?i.jsxs("span",{"data-testid":`${T}-${$}-source-inline`,style:u0,children:["· ",L]}):null]}),i.jsxs("select",{"data-testid":`${T}-${$}-select`,value:I,onChange:P=>O($,P.target.value),style:p0,children:[i.jsx("option",{value:"",children:a&&B?.effectiveValue!=null&&B.effectiveValue!==""?`${d("layout_editor.page_settings.seo.use_default")} (${String(B.effectiveValue)})`:d("layout_editor.page_settings.seo.use_default")}),z.map(P=>i.jsx("option",{value:P,children:P},P))]}),x(T,$,B)]})};return i.jsxs("div",{"data-testid":c,style:{display:"flex",flexDirection:"column",gap:12},children:[i.jsxs("div",{"data-testid":`${c}-og-section`,style:_c,children:[i.jsx("div",{style:xc,children:d("layout_editor.page_settings.seo.og_section")}),i.jsxs("p",{"data-testid":`${c}-intro`,style:d0,children:["ⓘ ",d("layout_editor.page_settings.seo.og_intro")]}),S(c,"type","layout_editor.page_settings.seo.og_type",i0,p,v,m),_(c,"title","layout_editor.page_settings.seo.og_title",p,v,m),_(c,"description","layout_editor.page_settings.seo.og_description",p,v,m),C(c,"image","layout_editor.page_settings.seo.og_image",p,v,m),_(c,"image_alt","layout_editor.page_settings.seo.og_image_alt",p,v,m),i.jsxs("div",{style:Dn,children:[i.jsxs("button",{type:"button","data-testid":`${c}-image-adv-toggle`,"aria-expanded":y,onClick:()=>g(T=>!T),style:h0,children:[y?"▾":"▸"," ",d("layout_editor.page_settings.seo.og_image_adv")]}),y?i.jsxs("div",{"data-testid":`${c}-image-adv`,style:y0,children:[E(c,"image_height","layout_editor.page_settings.seo.og_image_height",p,v,m),E(c,"image_type","layout_editor.page_settings.seo.og_image_type",p,v,m),E(c,"image_secure_url","layout_editor.page_settings.seo.og_image_secure_url",p,v,m)]}):null]}),E(c,"site_name","layout_editor.page_settings.seo.og_site_name",p,v,m),E(c,"locale","layout_editor.page_settings.seo.og_locale",p,v,m),i.jsxs("div",{"data-testid":`${c}-extra`,style:Dn,children:[i.jsx("label",{style:to,children:d("layout_editor.page_settings.seo.og_extra")}),i.jsx(zr,{value:Array.isArray(p.extra)?p.extra:[],onChange:T=>v("extra",T.length>0?T:void 0),keyField:"property",t:d,candidates:l,testidPrefix:`${c}-extra-editor`})]})]}),i.jsxs("div",{"data-testid":`${c}-tw-section`,style:_c,children:[i.jsx("div",{style:xc,children:d("layout_editor.page_settings.seo.tw_section")}),i.jsx("p",{style:{margin:"0 0 6px",fontSize:11,color:"#94a3b8"},children:d("layout_editor.page_settings.seo.tw_fallback_note")}),S("g7le-seo-tw","card","layout_editor.page_settings.seo.tw_card",s0,f,b,h),E("g7le-seo-tw","site","layout_editor.page_settings.seo.tw_site",f,b,h),E("g7le-seo-tw","creator","layout_editor.page_settings.seo.tw_creator",f,b,h),_("g7le-seo-tw","title","layout_editor.page_settings.seo.tw_title",f,b,h),_("g7le-seo-tw","description","layout_editor.page_settings.seo.tw_description",f,b,h),C("g7le-seo-tw","image","layout_editor.page_settings.seo.tw_image",f,b,h),_("g7le-seo-tw","image_alt","layout_editor.page_settings.seo.tw_image_alt",f,b,h),i.jsxs("div",{"data-testid":"g7le-seo-tw-extra",style:Dn,children:[i.jsx("label",{style:to,children:d("layout_editor.page_settings.seo.tw_extra")}),i.jsx(zr,{value:Array.isArray(f.extra)?f.extra:[],onChange:T=>b("extra",T.length>0?T:void 0),keyField:"name",t:d,candidates:l,testidPrefix:"g7le-seo-tw-extra-editor"})]})]})]})}const _c={border:"1px solid #e2e8f0",borderRadius:8,padding:12,display:"flex",flexDirection:"column",gap:10},xc={fontSize:13,fontWeight:600,color:"#0f172a"},d0={margin:0,fontSize:11,color:"#64748b",background:"#f8fafc",borderRadius:6,padding:"6px 8px"},Dn={display:"flex",flexDirection:"column",gap:4},to={fontSize:12,color:"#475569"},c0={display:"flex",alignItems:"center",gap:6},u0={fontSize:11,color:"#94a3b8"},p0={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0,boxSizing:"border-box"},f0={padding:"5px 8px",fontSize:12,border:"1px solid #e2e8f0",borderRadius:6,background:"#f1f5f9",color:"#64748b"},g0={fontSize:11,color:"#7c3aed"},h0={alignSelf:"flex-start",padding:"2px 0",fontSize:12,fontWeight:600,color:"#475569",background:"transparent",border:"none",cursor:"pointer"},y0={display:"flex",flexDirection:"column",gap:8,paddingLeft:14,marginTop:4,minWidth:0},b0={display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",minWidth:0},m0={display:"inline-flex",alignItems:"center",maxWidth:"100%",padding:"3px 8px",fontSize:12,color:"#1d4ed8",background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:6,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},v0={fontSize:11,color:"#94a3b8"},_0={padding:"2px 8px",fontSize:11,color:"#475569",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,cursor:"pointer"},x0=["WebPage","WebSite","ItemList","BreadcrumbList","Organization","Person","CollectionPage","AboutPage","ContactPage","SearchAction"];function Mr(e){const t=[],n=(o,r)=>{for(const[s,a]of Object.entries(o)){if(s==="@type"||s==="@context")continue;const l=r?`${r}.${s}`:s;a!==null&&typeof a=="object"&&!Array.isArray(a)?n(a,l):t.push({key:l,value:a==null?"":String(a)})}};return n(e,""),t}function wc(e,t){const n={"@type":e};for(const{key:o,value:r}of t){if(o.trim()==="")continue;const s=o.split(".");let a=n;for(let l=0;l<s.length-1;l++){const d=s[l];(typeof a[d]!="object"||a[d]===null||Array.isArray(a[d]))&&(a[d]={}),a=a[d]}a[s[s.length-1]]=r}return n}let xs=0;function w0({value:e,onChange:t,autoBlock:n,autoMeta:o,lockedByFilter:r=!1,filteredBlock:s,pageType:a,candidates:l,t:d,testidPrefix:c="g7le-seo-sd"}){const p=!!e&&typeof e=="object",[f,y]=u.useState(p),g=p&&typeof e["@type"]=="string"?e["@type"]:"WebPage",[m,h]=u.useState(g),[v,b]=u.useState(()=>p?Mr(e).map(D=>({id:xs++,...D})):[]),x=u.useCallback((D,O)=>{h(D),b(O),t(wc(D,O.map(M=>({key:M.key,value:M.value}))))},[t]),k=u.useCallback(()=>{const D=n,O=typeof D["@type"]=="string"?D["@type"]:m,M=Mr(D).map(B=>({id:xs++,key:B.key,value:o?.[B.key]?.expr??B.value}));return{type:O,rows:M}},[n,o,m]),w=u.useCallback(D=>{if(y(D),D){if(v.length===0&&n&&typeof n=="object"){const O=k();x(O.type,O.rows);return}t(wc(m,v.map(O=>({key:O.key,value:O.value}))))}else t(null)},[m,v,n,k,x,t]),_=u.useCallback(()=>{if(!n)return;const D=k();x(D.type,D.rows)},[n,k,x]),C=u.useCallback(()=>{x(m,[...v,{id:xs++,key:"",value:""}])},[m,v,x]),E=u.useCallback(D=>{x(m,v.filter(O=>O.id!==D))},[m,v,x]),S=u.useCallback((D,O)=>{x(m,v.map(M=>M.id===D?{...M,key:O}:M))},[m,v,x]),T=u.useCallback((D,O)=>{x(m,v.map(M=>M.id===D?{...M,value:O}:M))},[m,v,x]),$=u.useMemo(()=>n?Mr(n):[],[n]),A=u.useMemo(()=>s?Mr(s):[],[s]),z=v.some(D=>D.key.trim()!==""&&D.value.trim()==="");if(r){const D=s&&typeof s["@type"]=="string"?s["@type"]:"";return i.jsxs("div",{"data-testid":c,style:kc,children:[i.jsxs("div",{style:Sc,children:[i.jsx("span",{style:k0,children:"🔒"}),i.jsx("span",{style:{fontSize:13,fontWeight:600},children:d("layout_editor.page_settings.seo.sd_mode")}),i.jsx("span",{"data-testid":`${c}-locked-badge`,style:S0,children:d("layout_editor.page_settings.seo.sd_filter_locked")})]}),i.jsxs("div",{"data-testid":`${c}-locked`,style:C0,children:[i.jsxs("p",{style:{margin:"0 0 6px",fontSize:12,color:"#475569"},children:["ⓘ ",d("layout_editor.page_settings.seo.sd_filter_locked_note")]}),i.jsxs("div",{"data-testid":`${c}-filtered-block`,children:[D?i.jsxs("div",{style:{fontSize:12,fontWeight:600,marginBottom:4},children:["@type: ",D]}):null,A.map(O=>i.jsxs("div",{"data-testid":`${c}-filtered-row-${O.key}`,style:Cc,children:[i.jsx("code",{style:$c,children:O.key}),i.jsx("span",{style:Ec,children:O.value})]},O.key))]})]})]})}return i.jsxs("div",{"data-testid":c,style:kc,children:[i.jsx("div",{style:Sc,children:i.jsx(Dr,{checked:f,onChange:w,testid:`${c}-mode`,label:d("layout_editor.page_settings.seo.sd_manual_toggle")})}),!f&&i.jsxs("div",{"data-testid":`${c}-auto`,style:{marginTop:8},children:[i.jsxs("div",{"data-testid":`${c}-auto-preview`,style:$0,children:[i.jsxs("div",{style:{fontSize:12,fontWeight:600,marginBottom:6},children:[d("layout_editor.page_settings.seo.sd_auto_title"),n&&typeof n["@type"]=="string"?`: ${n["@type"]}`:"",a?i.jsxs("span",{style:{marginLeft:6,color:"#94a3b8",fontWeight:400},children:["(",a,")"]}):null]}),$.length===0?i.jsx("p",{style:{margin:0,fontSize:12,color:"#94a3b8"},children:d("layout_editor.page_settings.seo.sd_auto_empty")}):$.map(D=>{const O=o?.[D.key];return i.jsxs("div",{"data-testid":`${c}-auto-row-${D.key}`,style:Cc,children:[i.jsx("code",{style:$c,children:D.key}),O?i.jsxs("span",{"data-testid":`${c}-auto-chip-${D.key}`,style:E0,title:O.expr,children:["🔗 ",O.label||O.expr]}):i.jsx("span",{style:Ec,children:D.value})]},D.key)})]}),i.jsxs("p",{"data-testid":`${c}-replace-warn`,style:I0,children:["ⓘ ",d("layout_editor.page_settings.seo.sd_replace_pre_warn")]})]}),f&&i.jsxs("div",{"data-testid":`${c}-manual`,style:{marginTop:8},children:[i.jsxs("p",{"data-testid":`${c}-replace-warn`,style:A0,children:["⚠ ",d("layout_editor.page_settings.seo.sd_replace_warn")]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("label",{style:{fontSize:12,color:"#475569"},children:d("layout_editor.page_settings.seo.sd_type")}),i.jsx("input",{type:"text","data-testid":`${c}-type`,list:`${c}-type-options`,value:m,onChange:D=>x(D.target.value,v),style:T0}),i.jsx("datalist",{id:`${c}-type-options`,children:x0.map(D=>i.jsx("option",{value:D},D))})]}),i.jsx("div",{style:{marginTop:8,display:"flex",flexDirection:"column",gap:6},children:v.map(D=>i.jsxs("div",{"data-testid":`${c}-prop-${D.id}`,style:D0,children:[i.jsx("input",{type:"text","data-testid":`${c}-prop-key-${D.id}`,value:D.key,placeholder:d("layout_editor.page_settings.seo.sd_prop_key"),onChange:O=>S(D.id,O.target.value),style:{width:160,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0}}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:D.value,onChange:O=>T(D.id,O),t:d,candidates:l,placeholder:d("layout_editor.page_settings.seo.sd_prop_value"),testidPrefix:`${c}-prop-value-${D.id}`})}),i.jsx("button",{type:"button","data-testid":`${c}-prop-remove-${D.id}`,onClick:()=>E(D.id),title:d("layout_editor.page_settings.seo.remove"),"aria-label":d("layout_editor.page_settings.seo.remove"),style:N0,children:"✕"})]},D.id))}),i.jsxs("div",{style:{marginTop:8,display:"flex",gap:8},children:[i.jsxs("button",{type:"button","data-testid":`${c}-add`,onClick:C,style:Ic,children:["+ ",d("layout_editor.page_settings.seo.sd_add_prop")]}),n?i.jsxs("button",{type:"button","data-testid":`${c}-seed-from-module`,onClick:_,style:Ic,children:["↩ ",d("layout_editor.page_settings.seo.sd_seed_from_module")]}):null]}),i.jsxs("p",{"data-testid":`${c}-empty-note`,style:z0,children:["ⓘ ",d("layout_editor.page_settings.seo.sd_empty_note"),z?i.jsxs("span",{style:{color:"#b45309"},children:[" ",d("layout_editor.page_settings.seo.sd_has_empty")]}):null]})]})]})}const kc={display:"flex",flexDirection:"column"},Sc={display:"flex",alignItems:"center",gap:8},k0={fontSize:14},S0={fontSize:11,padding:"2px 6px",borderRadius:4,background:"#f1f5f9",color:"#64748b"},C0={marginTop:8,border:"1px solid #fcd34d",borderRadius:8,padding:10,background:"#fffbeb"},$0={border:"1px solid #e2e8f0",borderRadius:8,padding:10,background:"#f8fafc"},Cc={display:"flex",gap:8,alignItems:"center",padding:"2px 0"},$c={fontSize:11,color:"#475569",background:"#fff",padding:"2px 6px",borderRadius:4,minWidth:120},Ec={fontSize:11,color:"#0f172a",fontFamily:"monospace",wordBreak:"break-all"},E0={display:"inline-flex",alignItems:"center",padding:"2px 8px",fontSize:11,color:"#1d4ed8",background:"#eff6ff",border:"1px solid #bfdbfe",borderRadius:6},I0={margin:"6px 0 0",fontSize:12,color:"#64748b"},A0={margin:"0 0 8px",fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"6px 8px"},T0={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0},D0={display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},N0={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},Ic={padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},z0={margin:"8px 0 0",fontSize:11,color:"#94a3b8"},Ac=new Set(["core_setting","setting","query","route"]);function M0({vars:e,onChange:t,varCandidates:n,gatingMet:o=!1,candidates:r,t:s,testidPrefix:a="g7le-seo-vars"}){const l=u.useMemo(()=>e&&typeof e=="object"?{...e}:{},[e]),d=u.useMemo(()=>Array.isArray(n)?n:[],[n]),c=u.useMemo(()=>o?d.filter(_=>Ac.has(_.source)):[],[d,o]),p=u.useMemo(()=>o?d.filter(_=>_.source==="data"):[],[d,o]);u.useMemo(()=>new Set(c.map(_=>_.name)),[c]);const f=u.useMemo(()=>new Set(p.map(_=>_.name)),[p]),y=u.useMemo(()=>{const _=new Set;for(const C of d)Ac.has(C.source)&&_.add(C.name);return _},[d]),g=u.useMemo(()=>Object.keys(l).filter(_=>!f.has(_)),[l,f]),m=u.useCallback((_,C)=>{const E={...l};C===void 0?delete E[_]:E[_]=C,t(E)},[l,t]),[h,v]=u.useState(""),b=h.trim()!==""&&y.has(h.trim()),x=h.trim()!==""&&Object.prototype.hasOwnProperty.call(l,h.trim()),k=h.trim()!==""&&!b&&!x&&!f.has(h.trim()),w=u.useCallback(()=>{const _=h.trim();_===""||y.has(_)||Object.prototype.hasOwnProperty.call(l,_)||(t({...l,[_]:""}),v(""))},[h,y,l,t]);return i.jsxs("div",{"data-testid":a,style:{display:"flex",flexDirection:"column",gap:10},children:[i.jsx("span",{"data-testid":"g7le-seo-var-list",hidden:!0,"data-gating":o?"1":"0"}),!o&&i.jsxs("div",{"data-testid":`${a}-precondition`,style:L0,children:["⚠ ",s("layout_editor.page_settings.seo.vars_precondition")]}),c.length>0&&i.jsxs("div",{"data-testid":`${a}-auto-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_auto_group")}),c.map(_=>i.jsxs("div",{"data-testid":`g7le-seo-var-auto-${_.name}`,style:P0,children:[i.jsx("span",{style:O0,children:"🔒"}),i.jsx("code",{style:Ss,children:_.name}),i.jsxs("span",{style:B0,children:["〔",s("layout_editor.page_settings.seo.vars_auto"),"·",_.source,"〕"]}),_.owner?.name?i.jsx("span",{style:W0,children:_.owner.name}):null]},_.name))]}),p.length>0&&i.jsxs("div",{"data-testid":`${a}-data-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_data_group")}),p.map(_=>{const C=l[_.name]??"",E=C.trim()==="";return i.jsxs("div",{"data-testid":`g7le-seo-var-data-${_.name}`,style:Tc,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:4},children:[i.jsx("code",{style:Ss,children:_.name}),_.required?i.jsx("span",{"data-testid":`g7le-seo-var-required-${_.name}`,style:j0,children:"*"}):null]}),i.jsx(Ve,{value:C,onChange:S=>m(_.name,S??""),t:s,candidates:r,testidPrefix:`g7le-seo-var-data-field-${_.name}`}),_.required&&E?i.jsxs("p",{"data-testid":`g7le-seo-var-empty-warn-${_.name}`,style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_required_empty")]}):null]},_.name)})]}),i.jsxs("div",{"data-testid":`${a}-custom-group`,style:ws,children:[i.jsx("div",{style:ks,children:s("layout_editor.page_settings.seo.vars_custom_group")}),g.map(_=>i.jsxs("div",{"data-testid":`g7le-seo-var-custom-${_}`,style:Tc,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginBottom:4},children:[i.jsx("code",{style:Ss,children:_}),i.jsx("button",{type:"button","data-testid":`g7le-seo-var-custom-remove-${_}`,onClick:()=>m(_,void 0),title:s("layout_editor.page_settings.seo.remove"),"aria-label":s("layout_editor.page_settings.seo.remove"),style:{marginLeft:"auto",padding:"2px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]}),i.jsx(Ve,{value:l[_]??"",onChange:C=>m(_,C??""),t:s,candidates:r,testidPrefix:`g7le-seo-var-custom-field-${_}`})]},_)),i.jsxs("div",{"data-testid":"g7le-seo-var-add",style:{display:"flex",flexDirection:"column",gap:4,marginTop:6},children:[i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-seo-var-add-name",value:h,placeholder:s("layout_editor.page_settings.seo.vars_add_name"),onChange:_=>v(_.target.value),style:{flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0}}),i.jsxs("button",{type:"button","data-testid":"g7le-seo-var-add-confirm",onClick:w,disabled:h.trim()===""||b||x,style:H0,children:["+ ",s("layout_editor.page_settings.seo.vars_add")]})]}),b?i.jsxs("p",{"data-testid":"g7le-seo-var-add-reserved-warn",style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_name_reserved",{name:h.trim()})]}):null,x&&!b?i.jsxs("p",{"data-testid":"g7le-seo-var-add-duplicate-warn",style:Cs,children:["⚠ ",s("layout_editor.page_settings.seo.vars_name_duplicate",{name:h.trim()})]}):null,k?i.jsxs("p",{"data-testid":"g7le-seo-var-add-not-provided-hint",style:{margin:0,fontSize:11,color:"#94a3b8"},children:["ⓘ ",s("layout_editor.page_settings.seo.vars_name_not_provided",{name:h.trim()})]}):null]})]}),i.jsxs("p",{style:{margin:0,fontSize:11,color:"#94a3b8"},children:["ⓘ ",s("layout_editor.page_settings.seo.vars_value_hint")]})]})}const L0={fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"6px 8px"},ws={border:"1px solid #e2e8f0",borderRadius:8,padding:10,display:"flex",flexDirection:"column",gap:6},ks={fontSize:12,fontWeight:600,color:"#475569"},P0={display:"flex",alignItems:"center",gap:6,padding:"2px 0"},Tc={display:"flex",flexDirection:"column",border:"1px solid #f1f5f9",borderRadius:6,padding:8},O0={fontSize:13},Ss={fontSize:12,color:"#0f172a",background:"#f1f5f9",padding:"2px 6px",borderRadius:4},B0={fontSize:11,color:"#64748b"},W0={fontSize:11,color:"#94a3b8"},j0={color:"#dc2626",fontWeight:700},Cs={margin:"4px 0 0",fontSize:11,color:"#b45309"},H0={padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"};function F0({templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s,settingsSignature:a,t:l,debounceMs:d=400,fetchImpl:c,testidPrefix:p="g7le-seo-bot-preview"}){const[f,y]=u.useState(!0),[g,m]=u.useState(null),[h,v]=u.useState(!1),[b,x]=u.useState(null),k=u.useRef(null),w=u.useRef({templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s});w.current={templateIdentifier:e,layout:t,url:n,locale:o,seedContext:r,routeParams:s};const _=u.useCallback(async()=>{const T=c??(typeof fetch<"u"?fetch:void 0);if(!T)return;const{templateIdentifier:$,layout:A,url:z,locale:D,seedContext:O,routeParams:M}=w.current;v(!0),x(null);try{const B=`/api/admin/templates/${encodeURIComponent($)}/editor/seo-bot-preview`,N=await T(B,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({layout:A,url:z,locale:D,seed_context:O??{},route_params:M??{}})}),H=await N.json().catch(()=>null);if(!N.ok){x(H?.message??`HTTP ${N.status}`);return}const L=H?.data??null;m(L)}catch(B){x(B instanceof Error?B.message:String(B))}finally{v(!1)}},[c]);u.useEffect(()=>{if(f)return k.current&&clearTimeout(k.current),k.current=setTimeout(()=>{_()},d),()=>{k.current&&clearTimeout(k.current)}},[a,f,d,_]);const C=u.useCallback(()=>{k.current&&clearTimeout(k.current),_()},[_]),E=g?.html??null,S=g?.enabled??!0;return i.jsxs("div",{"data-testid":p,style:K0,children:[i.jsxs("div",{style:V0,children:[i.jsxs("button",{type:"button","data-testid":`${p}-toggle`,onClick:()=>y(T=>!T),"aria-expanded":f,style:U0,children:[f?"▾":"▸"," ",l("layout_editor.page_settings.seo.bot_preview_title")]}),f?i.jsxs("button",{type:"button","data-testid":`${p}-refresh`,onClick:C,disabled:h,style:q0,children:["⟳ ",l("layout_editor.page_settings.seo.bot_preview_refresh")]}):null]}),f&&i.jsxs("div",{style:{marginTop:8},children:[g&&!S?i.jsxs("p",{"data-testid":`${p}-disabled`,style:G0,children:["ⓘ ",l("layout_editor.page_settings.seo.bot_preview_disabled")]}):i.jsx("div",{"data-testid":`${p}-body`,style:Y0,children:h&&!E?i.jsx("p",{style:$s,children:l("layout_editor.translation.loading")}):b?i.jsx("p",{"data-testid":`${p}-error`,style:{...$s,color:"#dc2626"},children:b}):E==null?i.jsx("p",{style:$s,children:l("layout_editor.page_settings.seo.bot_preview_empty")}):i.jsx("pre",{"data-testid":`${p}-code`,style:X0,children:E})}),i.jsxs("p",{"data-testid":`${p}-sample-note`,style:J0,children:["ⓘ ",l("layout_editor.page_settings.seo.bot_preview_sample_note")]})]})]})}const K0={borderTop:"1px solid #e2e8f0",paddingTop:10,marginTop:12},V0={display:"flex",alignItems:"center",gap:8},U0={background:"none",border:"none",cursor:"pointer",fontSize:13,fontWeight:600,color:"#0f172a",padding:0},q0={marginLeft:"auto",padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},G0={margin:0,fontSize:12,color:"#64748b",background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:6,padding:"8px 10px"},Y0={border:"1px solid #e2e8f0",borderRadius:8,background:"#0b1020",padding:0,minHeight:80,overflow:"hidden"},X0={margin:0,padding:12,fontSize:11,lineHeight:1.5,color:"#d1fae5",fontFamily:"monospace",whiteSpace:"pre-wrap",wordBreak:"break-word",maxHeight:360,overflow:"auto"},$s={margin:0,padding:12,fontSize:12,color:"#94a3b8"},J0={margin:"8px 0 0",fontSize:11,color:"#94a3b8"};function Dc({option:e,testIdPrefix:t}){const{id:n,friendly:o,source:r}=e;return i.jsxs("span",{style:Z0,children:[i.jsx("span",{"data-testid":t?`${t}-title`:void 0,style:Q0,children:o??n}),o?i.jsx("span",{"data-testid":t?`${t}-id`:void 0,style:R0,children:n}):null,r?i.jsx("span",{"data-testid":t?`${t}-source`:void 0,style:ew,children:r}):null]})}const Z0={display:"inline-flex",alignItems:"baseline",gap:4,flexWrap:"wrap",minWidth:0},Q0={fontSize:12,color:"#0f172a"},R0={fontSize:10,color:"#94a3b8",fontFamily:"ui-monospace, monospace"},ew={fontSize:9,fontWeight:600,color:"#7c3aed",background:"#f5f3ff",border:"1px solid #ddd6fe",borderRadius:4,padding:"0 5px",whiteSpace:"nowrap"},tw=["always","hourly","daily","weekly","monthly","yearly","never"];function nw({getValue:e,patch:t,t:n,templateIdentifier:o,candidates:r,availableExtensions:s=[],dataSourceOptions:a=[],previewUrl:l="/",previewLocale:d="ko",seedContext:c,candidatesFetcher:p,ogPreviewFetcher:f,botFetchImpl:y,testidPrefix:g="g7le-seo"}){const m=e("meta",{}).seo,h=m&&typeof m=="object"?m:{},v=u.useCallback((K,ne)=>{const me={...e("meta",{})},Ce={...me.seo??{}};ne==null||ne===""?delete Ce[K]:Ce[K]=ne,me.seo=Ce,t("meta",me)},[e,t]),b=h.enabled===!0,x=typeof h.page_type=="string"?h.page_type:"",k=Array.isArray(h.extensions)?h.extensions:[],w=Array.isArray(h.data_sources)?h.data_sources:[],_=k.length>0&&x!=="",[C,E]=u.useState(null),[S,T]=u.useState(null),$=u.useCallback(async(K,ne)=>{if(p)return p(K,ne);if(typeof fetch>"u")return null;const me=new URLSearchParams;K&&me.set("page_type",K),Array.isArray(ne)&&ne.length>0&&me.set("extensions",JSON.stringify(ne.map(qe=>({type:qe.type,id:qe.id}))));const Ce=me.toString()?`?${me.toString()}`:"",ze=await fetch(en(`/api/admin/templates/${encodeURIComponent(o)}/editor/seo-candidates`,"json",null,Ce.replace(/^[?&]+/,"")),{credentials:"same-origin",headers:je()}),Se=await ze.json().catch(()=>null);return ze.ok?Se?.data??null:null},[p,o]),A=u.useMemo(()=>{const me=(e("__editor",{})??{}).original?.meta?.seo;return me&&typeof me=="object"?me:void 0},[e]),z=u.useRef(A);z.current=A;const D=u.useCallback(async K=>{if(f)return f(K);if(typeof fetch>"u")return null;const ne=await fetch(`/api/admin/templates/${encodeURIComponent(o)}/editor/seo-og-preview`,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({seo:K,own_seo:z.current??null,seed_context:c??{},route_params:{}})}),me=await ne.json().catch(()=>null);return ne.ok?me?.data??null:null},[f,o,c]),O=u.useMemo(()=>JSON.stringify(k),[k]),M=u.useRef(h);M.current=h,u.useEffect(()=>{let K=!1;return(async()=>{const ne=await $(x,k);K||E(ne)})(),()=>{K=!0}},[x,O]);const B=u.useMemo(()=>JSON.stringify({og:h.og??null,twitter:h.twitter??null,structured_data:h.structured_data??null}),[h.og,h.twitter,h.structured_data]),N=u.useRef(!1);u.useEffect(()=>{let K=!1;const ne=()=>{(async()=>{const Ce=await D(M.current);K||T(Ce)})()};if(!N.current)return N.current=!0,ne(),()=>{K=!0};const me=setTimeout(ne,180);return()=>{K=!0,clearTimeout(me)}},[x,O,B]);const H=C?.page_types??[],L=C?.toggle_settings??[],I=C?.vars??[],P=u.useMemo(()=>JSON.stringify(h),[h]),F=u.useMemo(()=>e("meta")?{meta:e("meta"),components:e("components",[])}:{meta:{},components:[]},[e]),G=u.useCallback(K=>{k.some(ne=>ne.type===K.type&&ne.id===K.id)||v("extensions",[...k,{type:K.type,id:K.id}])},[k,v]),V=u.useCallback(K=>{const ne=k.filter(me=>!(me.type===K.type&&me.id===K.id));v("extensions",ne.length>0?ne:void 0)},[k,v]),[Y,Z]=u.useState(!1),[se,re]=u.useState(""),U=u.useMemo(()=>{const K=se.trim().toLowerCase();return K?L.filter(ne=>`${ne.ref} ${ne.label??""}`.toLowerCase().includes(K)):L},[L,se]),ee=typeof h.toggle_setting=="string"?h.toggle_setting:"",fe=u.useCallback(K=>{const me=w.includes(K)?w.filter(Ce=>Ce!==K):[...w,K];v("data_sources",me.length>0?me:void 0)},[w,v]);return i.jsxs("div",{"data-testid":g,style:{display:"flex",flexDirection:"column",gap:14},children:[i.jsxs("div",{style:Bt,children:[i.jsx(Dr,{checked:b,onChange:K=>v("enabled",K),testid:`${g}-enabled`,label:n("layout_editor.page_settings.seo.enabled")}),b?null:i.jsxs("p",{"data-testid":`${g}-disabled-note`,style:no,children:["ⓘ ",n("layout_editor.page_settings.seo.disabled_note")]})]}),i.jsx(Nr,{disabled:!b,testid:`${g}-body`,style:{display:"flex",flexDirection:"column",gap:14},children:i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.page_type")}),H.length>0?i.jsxs("select",{"data-testid":`${g}-page-type`,value:x,onChange:K=>v("page_type",K.target.value),style:Lr,children:[i.jsx("option",{value:"",children:n("layout_editor.page_settings.seo.page_type_none")}),H.map(K=>i.jsxs("option",{value:K.value,children:[K.label??K.value,K.owner?.name?` (${K.owner.name})`:""]},K.value))]}):i.jsxs(i.Fragment,{children:[i.jsx("input",{type:"text","data-testid":`${g}-page-type-free`,value:x,placeholder:n("layout_editor.page_settings.seo.page_type_free_placeholder"),onChange:K=>v("page_type",K.target.value),style:Lr}),i.jsxs("p",{"data-testid":`${g}-page-type-free-note`,style:no,children:["ⓘ ",n("layout_editor.page_settings.seo.page_type_free_note")]})]})]}),i.jsxs("div",{style:Bt,children:[i.jsxs("label",{style:qt,children:["★ ",n("layout_editor.page_settings.seo.extensions")]}),i.jsxs("div",{"data-testid":`${g}-extensions`,style:{display:"flex",flexWrap:"wrap",gap:6},children:[k.map(K=>i.jsxs("span",{"data-testid":`${g}-extension-${K.type}-${K.id}`,style:Nc,children:[K.name??`${K.type}:${K.id}`,i.jsx("button",{type:"button","data-testid":`${g}-extension-remove-${K.type}-${K.id}`,onClick:()=>V(K),style:zc,children:"✕"})]},`${K.type}:${K.id}`)),i.jsxs("button",{type:"button","data-testid":`${g}-extension-add`,onClick:()=>Z(K=>!K),style:iw,children:["+ ",n("layout_editor.page_settings.seo.add_extension")]})]}),Y&&i.jsx("div",{"data-testid":`${g}-extension-picker`,style:Mc,children:s.length===0?i.jsx("p",{style:no,children:n("layout_editor.page_settings.seo.no_extension_candidate")}):s.map(K=>i.jsx("button",{type:"button","data-testid":`${g}-extension-option-${K.type}-${K.id}`,onClick:()=>{G(K),Z(!1)},style:Lc,children:K.name??`${K.type}:${K.id}`},`${K.type}:${K.id}`))})]}),!_&&i.jsxs("div",{"data-testid":`${g}-defaults-precondition`,style:rw,children:["⚠ ",n("layout_editor.page_settings.seo.defaults_precondition"),i.jsxs("div",{"data-testid":`${g}-defaults-missing`,style:{fontSize:11,marginTop:4},children:[k.length===0?n("layout_editor.page_settings.seo.missing_extensions"):"",x===""?` ${n("layout_editor.page_settings.seo.missing_page_type")}`:""]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.toggle_setting")}),i.jsxs("div",{"data-testid":`${g}-toggle-setting`,children:[ee?i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center",marginBottom:4},children:[i.jsx("code",{style:Nc,children:ee}),i.jsx("button",{type:"button","data-testid":`${g}-toggle-setting-clear`,onClick:()=>v("toggle_setting",void 0),style:zc,children:"✕"})]}):null,i.jsx("input",{type:"text","data-testid":`${g}-toggle-setting-search`,value:se,placeholder:n("layout_editor.page_settings.seo.toggle_setting_search"),onChange:K=>re(K.target.value),style:Lr}),se.trim()!==""&&i.jsx("div",{"data-testid":`${g}-toggle-setting-results`,style:Mc,children:U.length===0?i.jsx("p",{style:no,children:n("layout_editor.inline_binding.no_results")}):U.map(K=>i.jsx("button",{type:"button","data-testid":`${g}-toggle-setting-option-${K.ref}`,onClick:()=>{v("toggle_setting",K.ref),re("")},style:Lc,children:K.label??K.ref},K.ref))})]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.data_sources")}),i.jsx("p",{style:no,children:n("layout_editor.page_settings.seo.data_sources_hint")}),i.jsx("div",{"data-testid":`${g}-data-sources`,style:{display:"flex",flexDirection:"column",gap:6},children:a.length===0?i.jsx("span",{style:no,children:n("layout_editor.page_settings.seo.data_sources_empty")}):a.map(K=>{const ne=w.includes(K.id);return i.jsxs("label",{"data-testid":`${g}-data-source-${K.id}`,style:ow(ne),children:[i.jsx("input",{type:"checkbox","data-testid":`${g}-data-source-check-${K.id}`,checked:ne,onChange:()=>fe(K.id)}),i.jsx(Dc,{option:K,testIdPrefix:`${g}-data-source-${K.id}`})]},K.id)})})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.priority")}),i.jsx("input",{type:"range","data-testid":`${g}-priority`,min:0,max:1,step:.1,value:typeof h.priority=="number"?h.priority:.5,onChange:K=>v("priority",Number(K.target.value))}),i.jsx("span",{"data-testid":`${g}-priority-value`,style:{fontSize:12,color:"#475569"},children:typeof h.priority=="number"?h.priority:.5})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.changefreq")}),i.jsxs("select",{"data-testid":`${g}-changefreq`,value:typeof h.changefreq=="string"?h.changefreq:"",onChange:K=>v("changefreq",K.target.value),style:Lr,children:[i.jsx("option",{value:"",children:n("layout_editor.page_settings.seo.use_default")}),tw.map(K=>i.jsx("option",{value:K,children:K},K))]})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.title")}),i.jsx(vt,{value:typeof h.title=="string"?h.title:"",onChange:K=>v("title",K??""),t:n,candidates:r,testidPrefix:`${g}-title`,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.description")}),i.jsx(vt,{value:typeof h.description=="string"?h.description:"",onChange:K=>v("description",K??""),t:n,candidates:r,testidPrefix:`${g}-description`,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),i.jsx(l0,{og:h.og,twitter:h.twitter,onChangeOg:K=>v("og",Object.keys(K).length>0?K:void 0),onChangeTwitter:K=>v("twitter",Object.keys(K).length>0?K:void 0),ogPreview:S?.og,twitterPreview:S?.twitter,defaultsAvailable:S?.defaultsAvailable??!1,candidates:r,t:n}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.structured_data")}),i.jsx(w0,{value:h.structured_data,onChange:K=>v("structured_data",K??void 0),autoBlock:S?.structured?.autoBlock,autoMeta:S?.structured?.autoMeta,lockedByFilter:S?.structured?.lockedByFilter,filteredBlock:S?.structured?.filteredBlock,pageType:x,candidates:r,t:n})]}),i.jsxs("div",{style:Bt,children:[i.jsx("label",{style:qt,children:n("layout_editor.page_settings.seo.vars")}),i.jsx(M0,{vars:h.vars,onChange:K=>v("vars",Object.keys(K).length>0?K:void 0),varCandidates:I,gatingMet:_,candidates:r,t:n})]}),i.jsx(F0,{templateIdentifier:o,layout:F,url:l,locale:d,seedContext:c,settingsSignature:P,t:n,fetchImpl:y})]})})]})}const Bt={display:"flex",flexDirection:"column",gap:4},qt={fontSize:12,fontWeight:600,color:"#334155"},Lr={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",minWidth:0,boxSizing:"border-box"},no={margin:"4px 0 0",fontSize:11,color:"#94a3b8"},ow=e=>({display:"flex",alignItems:"center",gap:8,padding:"6px 10px",fontSize:12,border:`1px solid ${e?"#3b82f6":"#e2e8f0"}`,borderRadius:8,background:e?"#eff6ff":"#fff",cursor:"pointer",minWidth:0}),rw={fontSize:12,color:"#b45309",background:"#fffbeb",border:"1px solid #fcd34d",borderRadius:6,padding:"8px 10px"},Nc={display:"inline-flex",alignItems:"center",gap:4,fontSize:11,padding:"3px 8px",borderRadius:999,background:"#eef2ff",color:"#3730a3"},zc={border:"none",background:"transparent",cursor:"pointer",color:"#6366f1",fontSize:11,padding:0},iw={fontSize:11,padding:"3px 10px",borderRadius:999,border:"1px dashed #cbd5e1",background:"#f8fafc",color:"#475569",cursor:"pointer"},Mc={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6,display:"flex",flexDirection:"column",gap:2},Lc={textAlign:"left",border:"none",background:"transparent",cursor:"pointer",padding:"5px 8px",borderRadius:4,fontSize:12,color:"#0f172a"},sw={setState:"state",loadFromLocalStorage:"state",saveToLocalStorage:"state",navigate:"nav",openWindow:"nav",navigateBack:"nav",navigateForward:"nav",replaceUrl:"nav",toast:"notify",openModal:"notify",closeModal:"notify",showAlert:"notify",setError:"notify",refetchDataSource:"data",appendDataSource:"data",updateDataSource:"data",apiCall:"data",scrollIntoView:"etc",login:"etc",logout:"etc",setLocale:"locale",emitEvent:"etc",conditions:"flow",sequence:"flow",parallel:"flow",switch:"flow",suppress:"flow",showErrorPage:"error"},aw={state:"layout_editor.action.group.state",nav:"layout_editor.action.group.nav",notify:"layout_editor.action.group.notify",data:"layout_editor.action.group.data",locale:"layout_editor.action.group.locale",etc:"layout_editor.action.group.etc",flow:"layout_editor.action.group.flow",error:"layout_editor.action.group.error",extension:"layout_editor.action.group.extension"},lw={init:["state","data","notify","nav","locale","etc","flow","error","extension"],component:["nav","notify","data","state","locale","etc","flow","error","extension"]};function dw(e,t){const n=e?.[t];if(n&&typeof n=="object"){const o=n.__source;if(o&&typeof o=="object")return o}return null}function cw(e,t){return!e||e.kind==="core"?t("layout_editor.action.source_core"):e.id??t(`layout_editor.action.source_${e.kind}`)}function Pc(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function uw({recipes:e,t,onAdd:n,context:o="init",testIdPrefix:r="g7le-action-add"}){const[s,a]=u.useState(!1),[l,d]=u.useState(""),c=u.useMemo(()=>wo(e),[e]),p=u.useMemo(()=>{const f=lw[o],y=new Map;for(const m of c){const h=dw(e,m.id),b=h!=null&&h.kind!=="core"?"extension":sw[m.id]??"etc",x=y.get(b)??[];x.push({recipe:m,source:h}),y.set(b,x)}const g=l.trim().toLowerCase();return f.map(m=>{let h=y.get(m)??[];return g&&(h=h.filter(({recipe:v})=>Pc(v.label,t,v.id).toLowerCase().includes(g)||v.id.toLowerCase().includes(g)||v.build.handler.toLowerCase().includes(g))),{groupKey:m,items:h}}).filter(m=>m.items.length>0)},[c,e,o,l,t]);return c.length===0?i.jsx("div",{"data-testid":`${r}-empty`,style:xw,children:t("layout_editor.action.no_recipes")}):i.jsxs("div",{className:r,"data-testid":r,style:{minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":`${r}-toggle`,onClick:()=>a(f=>!f),style:pw,children:[t("layout_editor.action.add_action")," ▾"]}),s?i.jsxs("div",{"data-testid":`${r}-list`,style:fw,children:[i.jsx("input",{type:"text","data-testid":`${r}-search`,value:l,placeholder:t("layout_editor.action.add_search"),onChange:f=>d(f.target.value),style:gw}),p.map(({groupKey:f,items:y})=>i.jsxs("div",{"data-testid":`${r}-group-${f}`,children:[i.jsxs("div",{style:hw,children:["━━ ",t(aw[f]??f)," ━━"]}),y.map(({recipe:g,source:m})=>i.jsxs("button",{type:"button","data-testid":`g7le-init-action-spec-${g.id}`,onClick:()=>{n(xr(g,{})),a(!1),d("")},style:yw,children:[i.jsx("span",{style:bw,children:Pc(g.label,t,g.id)}),i.jsxs("span",{"data-testid":`g7le-init-action-spec-source-${g.id}`,style:mw,children:["〔",cw(m,t),"〕"]}),i.jsx("code",{style:vw,children:g.build.handler})]},g.id))]},f)),p.length===0?i.jsx("div",{"data-testid":`${r}-no-match`,style:_w,children:t("layout_editor.action.add_no_match")}):null]}):null]})}const pw={padding:"6px 12px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},fw={marginTop:6,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff",maxHeight:320,overflowY:"auto",padding:8},gw={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:6,boxSizing:"border-box"},hw={fontSize:11,fontWeight:700,color:"#94a3b8",padding:"6px 4px 2px"},yw={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"6px 8px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:6},bw={flex:1,minWidth:0,color:"#0f172a"},mw={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},vw={fontSize:10,color:"#cbd5e1",fontFamily:"monospace",whiteSpace:"nowrap"},_w={fontSize:12,color:"#94a3b8",padding:"8px 4px"},xw={fontSize:12,color:"#94a3b8",padding:"8px 0"};function ww(e,t){return t.startsWith("$t:")?e(t.slice(3)):t}function kw({action:e,recipe:t,values:n={},t:o,missingRequired:r=!1,showCodeButton:s=!0,testIdPrefix:a="g7le-action-preview"}){const[l,d]=u.useState(!1),c=t?Wd(t,n,p=>ww(o,p)):typeof e.handler=="string"?e.handler:o("layout_editor.action.advanced_action");return i.jsxs("div",{className:a,"data-testid":a,style:Sw,children:[r?i.jsxs("span",{"data-testid":`${a}-missing`,style:$w,children:["⚠ ",o("layout_editor.action.summary_missing_required")]}):i.jsx("span",{"data-testid":`${a}-summary`,style:Cw,children:c}),s?i.jsx("button",{type:"button","data-testid":`${a}-code-toggle`,onClick:()=>d(p=>!p),style:Ew,"aria-expanded":l,"aria-label":o("layout_editor.action.view_code"),children:"</>"}):null,s&&l?i.jsx("pre",{"data-testid":`${a}-code`,style:Iw,children:JSON.stringify(e,null,2)}):null]})}const Sw={display:"flex",flexWrap:"wrap",alignItems:"center",gap:6,minWidth:0},Cw={flex:1,minWidth:0,fontSize:12,color:"#475569"},$w={flex:1,minWidth:0,fontSize:12,color:"#b45309"},Ew={border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px",whiteSpace:"nowrap"},Iw={flexBasis:"100%",margin:"6px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:11,overflow:"auto"},Oc="__g7le_custom__";function Pr({value:e,onChange:t,candidates:n,placeholder:o,testid:r,t:s,bindingCandidates:a}){const l=e==null?"":String(e),d=Array.isArray(n)&&n.length>0,c=d&&n.some(y=>y.value===l),[p,f]=u.useState(!d||l!==""&&!c);return u.useEffect(()=>{l!==""&&d&&!c&&f(!0)},[l,d,c]),d&&!p?i.jsxs("div",{style:Hc,children:[i.jsxs("select",{className:"g7le-widget g7le-widget--picker","data-testid":r,value:c?l:"",onChange:y=>{const g=y.target.value;if(g===Oc){f(!0);return}t(g===""?void 0:g)},style:{...Wc,flex:1,minWidth:0},children:[i.jsx("option",{value:"",children:s("layout_editor.control.default")}),n.map(y=>i.jsx("option",{value:y.value,children:y.label},y.value)),i.jsx("option",{value:Oc,children:s("layout_editor.recipe.custom_input")})]}),i.jsx("button",{type:"button","data-testid":`${r}-to-custom`,title:s("layout_editor.recipe.custom_input"),"aria-label":s("layout_editor.recipe.custom_input"),onClick:()=>f(!0),style:Fc,children:"✎"})]}):i.jsxs("div",{style:Hc,children:[i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:l,onChange:y=>t(y===""?void 0:y),t:s,candidates:a,placeholder:o,testidPrefix:`${r}-input`})}),d&&i.jsx("button",{type:"button","data-testid":`${r}-pick`,title:s("layout_editor.recipe.pick_from_list"),onClick:()=>{f(!1),t(void 0)},style:Fc,children:"≡"})]})}function Aw({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.page_placeholder"),testid:"g7le-widget-page-picker",t:o,bindingCandidates:r})}function Bc({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.datasource_placeholder"),testid:"g7le-widget-datasource-picker",t:o,bindingCandidates:r})}function Es({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.state_key_placeholder"),testid:"g7le-widget-state-key-picker",t:o,bindingCandidates:r})}function Tw({value:e,onChange:t,candidates:n,t:o,bindingCandidates:r}){return i.jsx(Pr,{value:e,onChange:t,candidates:n,placeholder:o("layout_editor.recipe.modal_placeholder"),testid:"g7le-widget-modal-picker",t:o,bindingCandidates:r})}function Dw({value:e,onChange:t,t:n,bindingCandidates:o}){const r=typeof e=="string"?e:e==null?"":String(e);return i.jsx("div",{className:"g7le-widget g7le-widget--i18n-text","data-testid":"g7le-widget-i18n-text",style:{flex:1,minWidth:100,width:"100%"},children:i.jsx(vt,{value:r,onChange:s=>t(s===""?void 0:s),t:n,placeholder:n("layout_editor.recipe.i18n_text_placeholder"),testidPrefix:"g7le-widget-i18n-text-field",candidates:o,enableExpressionTree:!0,expressionTreeCollapsible:!0})})}function Nw({value:e,onChange:t,candidates:n,t:o}){const r=e==null?"":String(e),s=r.split(",").map(d=>d.trim()).filter(Boolean),a=Array.isArray(n)&&n.length>0,l=d=>{if(!d)return;const c=[...s,d];t(c.join(", "))};return i.jsxs("div",{className:"g7le-widget g7le-widget--binding-list","data-testid":"g7le-widget-binding-list",style:{display:"flex",flexDirection:"column",gap:4,width:"100%"},children:[i.jsx("input",{type:"text","data-testid":"g7le-widget-binding-list-input",value:r,placeholder:o("layout_editor.recipe.datasource_placeholder"),onChange:d=>t(d.target.value===""?void 0:d.target.value),style:jc}),a?i.jsxs("select",{className:"g7le-widget g7le-widget--picker","data-testid":"g7le-widget-binding-list-add",value:"",onChange:d=>{l(d.target.value),d.target.value=""},style:{...Wc,minWidth:0},children:[i.jsx("option",{value:"",children:o("layout_editor.recipe.pick_from_list")}),n.map(d=>i.jsx("option",{value:d.value,children:d.label},d.value))]}):null]})}function zw({value:e,onChange:t}){const n=e==null?"":String(e);return i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--text","data-testid":"g7le-widget-text",value:n,onChange:o=>t(o.target.value===""?void 0:o.target.value),style:jc})}const Wc={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",minWidth:140,width:"100%"},jc={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:100,width:"100%"},Hc={display:"flex",gap:4,alignItems:"flex-start",width:"100%"},Fc={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"};function Mw(e){if(e==null)return"null";if(Array.isArray(e))return"list";switch(typeof e){case"number":return"number";case"boolean":return"boolean";case"string":return"string";default:return"object"}}function Kc(e){switch(e){case"string":return"";case"number":return 0;case"boolean":return!1;case"null":return null;case"list":return[];case"object":return{};default:return null}}function Is(e){return/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(e)}function Lw(e,t,n){return t.includes(n)?"self":e.includes(n)?"inherited":"self"}function Pw(e){const t=e.state&&typeof e.state=="object"&&Object.keys(e.state).length>0,n=e.initLocal&&typeof e.initLocal=="object"?e.initLocal:void 0;if(!t)return{initLocal:n??{},migrated:!1};const o={...e.state};if(n)for(const[r,s]of Object.entries(n))o[r]=s;return{initLocal:o,migrated:!0}}const Vc=[{kind:"string",labelKey:"layout_editor.initstate.kind_string"},{kind:"number",labelKey:"layout_editor.initstate.kind_number"},{kind:"boolean",labelKey:"layout_editor.initstate.kind_boolean"},{kind:"null",labelKey:"layout_editor.initstate.kind_null"},{kind:"list",labelKey:"layout_editor.initstate.kind_list"},{kind:"object",labelKey:"layout_editor.initstate.kind_object"}];function Nn({value:e,onChange:t,t:n,path:o,scope:r,depth:s=0,renderStringLeaf:a}){const l=Mw(e),d=c=>{t(Kc(c))};return i.jsxs("div",{className:"g7le-initstate-value",style:{paddingLeft:s>0?12:0,minWidth:0},children:[i.jsxs("div",{style:Bw,children:[i.jsx("select",{"data-testid":`g7le-initstate-type-${o}`,value:l,onChange:c=>d(c.target.value),style:Uc,children:Vc.map(({kind:c,labelKey:p})=>i.jsx("option",{value:c,children:n(p)},c))}),l==="string"?a?i.jsx("div",{style:{flex:1,minWidth:0},children:a({value:String(e??""),onChange:c=>t(c),testidPrefix:`g7le-initstate-value-${o}`})}):i.jsx("input",{type:"text","data-testid":`g7le-initstate-value-${o}`,value:String(e??""),onChange:c=>t(c.target.value),style:qc}):l==="number"?i.jsx("input",{type:"number","data-testid":`g7le-initstate-value-${o}`,value:typeof e=="number"?e:"",onChange:c=>{const p=parseFloat(c.target.value);t(Number.isNaN(p)?0:p)},style:qc}):l==="boolean"?i.jsx(Dr,{checked:!!e,onChange:t,testid:`g7le-initstate-value-${o}`,label:n(e?"layout_editor.overlay.on":"layout_editor.overlay.off")}):l==="null"?i.jsx("span",{"data-testid":`g7le-initstate-value-${o}`,style:Ww,children:"null"}):null]}),l==="list"?i.jsxs("div",{style:Gc,children:[e.map((c,p)=>i.jsxs("div",{style:jw,children:[i.jsxs("span",{style:Hw,children:["[",p,"]"]}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Nn,{value:c,onChange:f=>{const y=e.slice();y[p]=f,t(y)},t:n,path:`${o}.${p}`,scope:r,depth:s+1,renderStringLeaf:a})}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-item-remove-${o}.${p}`,onClick:()=>t(e.filter((f,y)=>y!==p)),style:Yc,children:"✕"})]},p)),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-item-add-${o}`,onClick:()=>t([...e,""]),style:Xc,children:["+ ",n("layout_editor.initstate.add_item")]})]}):null,l==="object"?i.jsx(Ow,{value:e,onChange:t,t:n,path:o,scope:r,depth:s+1,renderStringLeaf:a}):null]})}function Ow({value:e,onChange:t,t:n,path:o,scope:r,depth:s,renderStringLeaf:a}){const[l,d]=u.useState(""),[c,p]=u.useState("string"),[f,y]=u.useState(null),g=Object.entries(e),m=()=>{const h=l.trim();if(!h){y(n("layout_editor.initstate.name_required"));return}if(!Is(h)){y(n("layout_editor.initstate.name_invalid"));return}if(h in e){y(n("layout_editor.initstate.name_duplicate"));return}t({...e,[h]:Kc(c)}),d(""),y(null)};return i.jsxs("div",{style:Gc,children:[g.map(([h,v])=>i.jsxs("div",{style:Fw,children:[i.jsx("code",{style:Kw,children:h}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Nn,{value:v,onChange:b=>t({...e,[h]:b}),t:n,path:`${o}.${h}`,scope:r,depth:s,renderStringLeaf:a})}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-subremove-${o}.${h}`,onClick:()=>{const b={...e};delete b[h],t(b)},style:Yc,children:"✕"})]},h)),i.jsxs("div",{style:Vw,children:[i.jsx("input",{type:"text","data-testid":`g7le-initstate-subkey-input-${o}`,value:l,placeholder:n("layout_editor.initstate.subkey_name"),onChange:h=>{d(h.target.value),f&&y(null)},onKeyDown:h=>{h.key==="Enter"&&(h.preventDefault(),m())},"aria-invalid":f?!0:void 0,style:{...Uw,...f?qw:null}}),i.jsx("select",{"data-testid":`g7le-initstate-subkey-kind-${o}`,value:c,onChange:h=>p(h.target.value),"aria-label":n("layout_editor.initstate.value_kind"),style:Uc,children:Vc.map(({kind:h,labelKey:v})=>i.jsx("option",{value:h,children:n(v)},h))}),i.jsxs("button",{type:"button","data-testid":`g7le-initstate-subkey-add-${o}`,onClick:m,style:Xc,children:["+ ",n("layout_editor.initstate.add_subkey")]})]}),f?i.jsx("p",{"data-testid":`g7le-initstate-subkey-error-${o}`,style:Gw,children:f}):null]})}const Bw={display:"flex",gap:6,alignItems:"center",minWidth:0},Uc={padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,color:"#64748b"},qc={flex:1,minWidth:0,padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},Ww={fontSize:12,color:"#94a3b8",fontFamily:"monospace"},Gc={display:"flex",flexDirection:"column",gap:6,marginTop:6,paddingLeft:8,borderLeft:"2px solid #e2e8f0"},jw={display:"flex",gap:6,alignItems:"flex-start",minWidth:0},Hw={fontSize:11,color:"#94a3b8",minWidth:28,paddingTop:4},Fw={display:"flex",gap:6,alignItems:"flex-start",minWidth:0},Kw={fontSize:11,color:"#475569",fontFamily:"monospace",minWidth:72,paddingTop:4},Yc={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12},Xc={alignSelf:"flex-start",padding:"2px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Vw={display:"flex",gap:6,alignItems:"center"},Uw={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6},qw={border:"1px solid #dc2626"},Gw={margin:"2px 0 0",fontSize:11,color:"#dc2626"},Yw=new Set(["text","data-chip","number"]),Xw=new Set(["select","toggle","page-picker","datasource-picker","state-key-picker","modal-picker","component-target-picker","tag-input","locale-picker","i18n-text","segmented","slider","color","image"]);function Jw(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function Zw(e,t){if(e==="page-picker")return t.pageCandidates;if(e==="datasource-picker")return t.dataSourceCandidates;if(e==="state-key-picker")return t.stateKeyCandidates;if(e==="modal-picker")return t.modalCandidates}function Qw({param:e,value:t,t:n,pools:o,onChange:r,testIdPrefix:s,renderActionList:a,siblingValues:l}){const d=Jw(e.label,n,e.key);if(e.advanced)return i.jsxs("div",{"data-testid":`${s}-locked-${e.key}`,style:lk,children:[i.jsx("span",{style:dk,children:d}),i.jsxs("span",{style:ck,children:["[",n("layout_editor.action.advanced"),"] ",n("layout_editor.init_actions.advanced_param_hint")]})]});if(e.widget==="action-list"&&a){const f=Array.isArray(t)?t.filter(y=>!!y&&typeof y=="object"):[];return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),a(f,y=>r(y.length===0?void 0:y),`${s}-${e.key}`,e.key)]})}if(e.widget==="branch-list"&&a){const f=Array.isArray(t)?t.filter(y=>!!y&&typeof y=="object"):[];return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(Rw,{branches:f,onChange:y=>r(y.length===0?void 0:y),t:n,pools:o,renderActionList:a,testIdPrefix:`${s}-${e.key}`})]})}if(e.widget==="key-value")return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(rk,{value:t,onChange:r,t:n,candidates:o.bindingCandidates,testIdPrefix:`${s}-kv-${e.key}`})]});if(e.widget==="state-key-value")return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:{display:"flex",flexDirection:"column",gap:4},children:[i.jsx("span",{style:oo,children:d}),i.jsx(ik,{value:t,onChange:r,t:n,candidates:o.bindingCandidates,stateKeyCandidates:o.stateKeyCandidates,param:e,siblingTarget:l?.target,testIdPrefix:`${s}-skv-${e.key}`})]});if(Yw.has(e.widget??"")||!Xw.has(e.widget??""))return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:Zc,children:[i.jsx("span",{style:oo,children:d}),i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:typeof t=="string"?t:t==null?"":String(t),onChange:f=>r(f===""?void 0:f),t:n,candidates:o.bindingCandidates,placeholder:e.widget==="number"?n("layout_editor.action_param.number_placeholder"):void 0,testidPrefix:`${s}-chip-${e.key}`})})]});const p=Tn(e.widget);return i.jsxs("div",{"data-testid":`${s}-param-${e.key}`,style:Zc,children:[i.jsx("span",{style:oo,children:d}),i.jsx("div",{style:{flex:1,minWidth:0},children:p?i.jsx(p,{control:{...e},value:t,onChange:r,t:n,candidates:Zw(e.widget,o),bindingCandidates:o.bindingCandidates}):i.jsx(Ve,{value:typeof t=="string"?t:t==null?"":String(t),onChange:f=>r(f===""?void 0:f),t:n,candidates:o.bindingCandidates,testidPrefix:`${s}-chip-${e.key}`})})]})}function Rw({branches:e,onChange:t,t:n,pools:o,renderActionList:r,testIdPrefix:s}){const a=y=>Array.isArray(y)?y.filter(g=>!!g&&typeof g=="object"):y&&typeof y=="object"?[y]:[],l=y=>{if(y.length!==0)return y.length===1?y[0]:y},d=(y,g)=>{const m=e.slice();m[y]=g,t(m)},c=y=>{t(e.filter((g,m)=>m!==y))},p=(y,g)=>{if(g<0||g>=e.length)return;const m=e.slice(),[h]=m.splice(y,1);m.splice(g,0,h),t(m)},f=()=>{t([...e,{if:"",then:void 0}])};return i.jsxs("div",{"data-testid":`${s}-branches`,style:{display:"flex",flexDirection:"column",gap:8},children:[e.map((y,g)=>{const m=typeof y.if=="string"?y.if:"",h=a(y.then);return i.jsxs("div",{"data-testid":`${s}-branch-${g}`,style:ek,children:[i.jsxs("div",{style:tk,children:[i.jsxs("span",{style:nk,children:[n(m?"layout_editor.action_recipe.conditions.branch_when":"layout_editor.action_recipe.conditions.branch_else")," ",g+1]}),i.jsxs("span",{style:{display:"flex",gap:4},children:[i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-up`,onClick:()=>p(g,g-1),disabled:g===0,style:As,title:n("layout_editor.action_recipe.conditions.move_up"),children:"↑"}),i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-down`,onClick:()=>p(g,g+1),disabled:g===e.length-1,style:As,title:n("layout_editor.action_recipe.conditions.move_down"),children:"↓"}),i.jsx("button",{type:"button","data-testid":`${s}-branch-${g}-remove`,onClick:()=>c(g),style:As,title:n("layout_editor.action_recipe.conditions.remove_branch"),children:"✕"})]})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:3},children:[i.jsx("span",{style:Jc,children:n("layout_editor.action_recipe.conditions.param_if")}),i.jsx(Ve,{value:m,onChange:v=>d(g,{...y,if:v}),t:n,candidates:o.bindingCandidates,testidPrefix:`${s}-branch-${g}-if`})]}),i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:3},children:[i.jsx("span",{style:Jc,children:n("layout_editor.action_recipe.conditions.param_then")}),r(h,v=>d(g,{...y,then:l(v)}),`${s}-branch-${g}-then`)]})]},g)}),i.jsxs("button",{type:"button","data-testid":`${s}-add-branch`,onClick:f,style:ok,children:["+ ",n("layout_editor.action_recipe.conditions.add_branch")]})]})}const ek={border:"1px solid #e2e8f0",borderRadius:6,padding:8,display:"flex",flexDirection:"column",gap:6,background:"#f8fafc"},tk={display:"flex",justifyContent:"space-between",alignItems:"center"},nk={fontSize:12,fontWeight:700,color:"#475569"},Jc={fontSize:11,color:"#64748b"},As={border:"1px solid #cbd5e1",background:"#fff",borderRadius:4,width:22,height:22,cursor:"pointer",fontSize:12,lineHeight:"1",color:"#475569"},ok={border:"1px dashed #94a3b8",background:"#fff",borderRadius:6,padding:"6px 10px",cursor:"pointer",fontSize:12,color:"#475569",alignSelf:"flex-start"};function rk({value:e,onChange:t,t:n,candidates:o,testIdPrefix:r,renderKeyInput:s}){const a=e&&typeof e=="object"&&!Array.isArray(e)?Object.entries(e).map(([l,d])=>({key:l,value:typeof d=="string"?d:d==null?"":String(d)})):[];return i.jsx(zr,{value:a,onChange:l=>{const d={};for(const c of l)c.key.trim()!==""&&(d[c.key]=c.value??"");t(Object.keys(d).length===0?void 0:d)},keyField:"key",valueField:"value",t:n,candidates:o,testidPrefix:r,keyPlaceholder:n("layout_editor.action_param.key_placeholder"),valuePlaceholder:n("layout_editor.action_param.value_placeholder"),addLabel:n("layout_editor.action_param.add_pair"),renderKeyInput:s})}function ik({value:e,onChange:t,t:n,candidates:o,stateKeyCandidates:r,param:s,siblingTarget:a,testIdPrefix:l}){const d=e&&typeof e=="object"&&!Array.isArray(e)?Object.entries(e):[],c=typeof a=="string"?a:"";if((c.startsWith("_local.")||c.startsWith("global.")||c.startsWith("$parent._local.")||c.startsWith("$root._local."))&&d.length===1&&d[0][0]==="value"){const b=d[0][1];return i.jsx("div",{"data-testid":`${l}-path-value`,style:{display:"flex",flexDirection:"column",gap:4},children:i.jsx(Nn,{value:b,onChange:x=>t(x===void 0?void 0:{value:x}),t:n,path:"value",scope:`${l}-path-value-val`,renderStringLeaf:x=>i.jsx(Ve,{value:x.value,onChange:x.onChange,t:n,candidates:o,testidPrefix:x.testidPrefix})})})}const y=(b,x)=>{const k={};for(const[w,_]of d)k[w]=w===b?x:_;t(Object.keys(k).length===0?void 0:k)},g=b=>{const x={};for(const[k,w]of d)k!==b&&(x[k]=w);t(Object.keys(x).length===0?void 0:x)},[m,h]=u.useState(""),v=()=>{const b=m.trim();if(b===""||d.some(([k])=>k===b))return;const x={};for(const[k,w]of d)x[k]=w;x[b]="",t(x),h("")};return i.jsxs("div",{"data-testid":l,style:{display:"flex",flexDirection:"column",gap:6},children:[d.map(([b,x])=>i.jsxs("div",{"data-testid":`${l}-row-${b}`,style:{display:"flex",gap:6,alignItems:"flex-start",border:"1px solid #e2e8f0",borderRadius:8,padding:8},children:[i.jsxs("div",{style:{flex:1,minWidth:0,display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:{minWidth:0},children:i.jsx(Es,{control:{...s},value:b,onChange:k=>{const w=k==null?"":String(k);if(w===""||w===b||d.some(([C])=>C===w))return;const _={};for(const[C,E]of d)_[C===b?w:C]=E;t(_)},t:n,candidates:r})}),i.jsx("div",{style:{minWidth:0},children:i.jsx(Nn,{value:x,onChange:k=>y(b,k),t:n,path:b,scope:`${l}-val`,renderStringLeaf:k=>i.jsx(Ve,{value:k.value,onChange:k.onChange,t:n,candidates:o,testidPrefix:k.testidPrefix})})})]}),i.jsx("button",{type:"button","data-testid":`${l}-remove-${b}`,onClick:()=>g(b),title:n("layout_editor.page_settings.seo.remove"),"aria-label":n("layout_editor.page_settings.seo.remove"),style:{flex:"0 0 auto",padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},children:"✕"})]},b)),i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[i.jsx("div",{style:{flex:1,minWidth:0},children:i.jsx(Es,{control:{...s},value:m,onChange:b=>h(b==null?"":String(b)),t:n,candidates:r})}),i.jsxs("button",{type:"button","data-testid":`${l}-add`,onClick:v,style:{flex:"0 0 auto",padding:"5px 10px",fontSize:12,border:"1px dashed #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},children:["+ ",n("layout_editor.action_param.add_pair")]})]})]})}function Ts({raw:e,recipe:t,values:n,t:o,pools:r,onChange:s,testIdPrefix:a,renderActionList:l}){const d=(c,p)=>{const f={...n,[c]:p},y=xr(t,f),g=as(e);s(g?jd(y,g):y)};return i.jsx(i.Fragment,{children:t.params.filter(c=>sk(c,n)).map(c=>i.jsx(Qw,{param:c,value:n[c.key],t:o,pools:r,onChange:p=>d(c.key,p),testIdPrefix:a,renderActionList:l,siblingValues:n},c.key))})}function sk(e,t){const n=e.dependsOn;return!n||typeof n!="object"||typeof n.param!="string"?!0:t[n.param]===n.equals}function ak({raw:e,t,onChange:n,testIdPrefix:o,candidates:r}){const[s,a]=u.useState(!1);return i.jsxs("div",{"data-testid":`${o}-if`,style:uk,children:[i.jsxs("button",{type:"button",onClick:()=>a(l=>!l),style:pk,children:["▸ ",t("layout_editor.init_actions.if_label")]}),s?i.jsx("div",{"data-testid":`${o}-if-input`,style:{minWidth:0},children:i.jsx(Ve,{value:as(e)??"",onChange:l=>n(jd(e,l)),t,candidates:r,placeholder:t("layout_editor.init_actions.if_placeholder"),testidPrefix:`${o}-if-chip`})}):null]})}const Zc={display:"flex",alignItems:"center",gap:8,minWidth:0},oo={fontSize:11,color:"#64748b",minWidth:80},lk={display:"flex",alignItems:"center",gap:8,padding:"4px 0"},dk={fontSize:11,color:"#64748b",minWidth:80},ck={fontSize:11,color:"#92400e"},uk={display:"flex",flexDirection:"column",gap:4},pk={textAlign:"left",border:"none",background:"transparent",color:"#475569",cursor:"pointer",fontSize:11,padding:0};function fk(e,t,n){if(!t)return n("layout_editor.action.source_core");const o=e?.[t],r=o&&typeof o=="object"&&o.__source?o.__source:null;return!r||r.kind==="core"?n("layout_editor.action.source_core"):r.id??n(`layout_editor.action.source_${r.kind}`)}function gk(e,t,n){const o=Yn(e,t);if(o.kind==="preset"){const r=t.find(s=>s.id===o.recipeId);return Wd(r??null,o.values,s=>n(s.startsWith("$t:")?s.slice(3):s))}return typeof e.handler=="string"?e.handler:n("layout_editor.action_list.unknown")}function Gt({actions:e,onChange:t,t:n,recipes:o,chipContext:r,actionChipCandidates:s,addContext:a="init",renderAddPicker:l,renderEditor:d,candidatePools:c,showSourceBadge:p=!1,canDrag:f,canDropAt:y,isLocked:g,hideAddPicker:m=!1,testIdPrefix:h="g7le-action-list"}){const v=u.useMemo(()=>wo(o),[o]),b=u.useMemo(()=>{if(r==null||r==="init")return c;const O=jl(r,n,s??null);if(O.length===0)return c;const M=c??{},B=new Set((M.bindingCandidates??[]).map(H=>H.expression)),N=O.filter(H=>!B.has(H.expression));return N.length===0?c:{...M,bindingCandidates:[...N,...M.bindingCandidates??[]]}},[r,s,c,n]),[x,k]=u.useState(null),[w,_]=u.useState(null),C=u.useCallback((O,M)=>{if(O===M||O<0||M<0||O>=e.length||M>=e.length)return;const B=e.slice(),[N]=B.splice(O,1);B.splice(M,0,N),t(B)},[e,t]),E=fs({length:e.length,onMove:C,canDrag:f,canDropAt:y}),S=u.useCallback(O=>{t(e.filter((M,B)=>B!==O))},[e,t]),T=u.useCallback((O,M)=>{const B=e.slice();B[O]=M,t(B)},[e,t]),$=u.useCallback(O=>{t([...e,O])},[e,t]),A=u.useCallback((O,M)=>{const B=Yn(O,v),N=B.kind==="preset"?v.find(L=>L.id===B.recipeId)??null:null;if(!N)return null;const H=B.kind==="preset"?B.values:{};return i.jsxs("div",{style:mk,children:[i.jsx(Ts,{raw:O,recipe:N,values:H,t:n,pools:b??{},onChange:M,testIdPrefix:`${h}-edit`,renderActionList:(L,I,P,F)=>{const G=F==="onSuccess"?"response":F==="onError"?"error":void 0;return i.jsx(Gt,{actions:L,onChange:I,t:n,recipes:o,chipContext:G,actionChipCandidates:G?s:void 0,candidatePools:G?c:b,addContext:a,showSourceBadge:p,testIdPrefix:P})}}),i.jsx(ak,{raw:O,t:n,onChange:M,testIdPrefix:`${h}-edit`,candidates:b?.bindingCandidates})]})},[v,n,b,c,s,h,o,a,p]),z=d??(o?A:void 0),D=m?null:l?l($):o?i.jsx(uw,{recipes:o,t:n,onAdd:$,context:a,testIdPrefix:`${h}-add-picker`}):null;return i.jsxs("div",{className:h,"data-testid":h,children:[i.jsxs("ul",{style:{listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:6},children:[e.map((O,M)=>{const B=Yn(O,v),N=B.kind==="preset"?B.recipeId:null,H=N?v.find(G=>G.id===N)??null:null,L=H===null,I=g?.(M)??!1,P=as(O)!==null,F=z?z(O,G=>T(M,G)):null;return i.jsxs(u.Fragment,{children:[I?null:i.jsx("li",{"aria-hidden":"true",style:{listStyle:"none",margin:0,padding:0},children:i.jsx(Rn,{active:E.isDropTarget(M),testid:`${h}-dropline-${M}`})}),i.jsxs("li",{"data-testid":`${h}-item-${M}`,"data-drag-source":E.dragIndex===M?"true":void 0,style:{border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",...E.dragIndex===M?{opacity:.5}:{}},onDragOver:E.dragging&&!I?G=>{G.preventDefault();const V=G.currentTarget.getBoundingClientRect();E.onDragOverItem(M,G.clientY<V.top+V.height/2?"before":"after")}:void 0,onDrop:E.dragging&&!I?G=>{G.preventDefault(),E.onDrop()}:void 0,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[I?i.jsx("span",{style:{fontSize:12},children:"🔒"}):i.jsx("span",{"data-testid":`${h}-drag-${M}`,style:{color:"#94a3b8",cursor:"grab",fontSize:14,userSelect:"none"},draggable:!0,onDragStart:()=>E.onDragStart(M),onDragEnd:E.onDragEnd,"aria-label":n("layout_editor.action_list.drag_reorder"),role:"button",children:"⠿"}),i.jsx("span",{style:{flex:1,minWidth:0,fontSize:13},"data-testid":`${h}-summary-${M}`,children:gk(O,v,n)}),p?i.jsxs("span",{"data-testid":`${h}-source-${M}`,style:hk,children:["〔",fk(o,N,n),"〕"]}):null,P?i.jsxs("span",{"data-testid":`${h}-cond-badge-${M}`,style:yk,children:["〔",n("layout_editor.init_actions.cond_badge"),"〕"]}):null,L?i.jsxs("span",{"data-testid":`${h}-advanced-${M}`,style:bk,children:["[",n("layout_editor.action.advanced"),"]"]}):null,!I&&F?i.jsxs("button",{type:"button","data-testid":`${h}-edit-${M}`,onClick:()=>_(w===M?null:M),"aria-label":n("layout_editor.init_actions.edit"),title:n("layout_editor.init_actions.edit"),style:w===M?vk:Qc,children:["✎ ",n("layout_editor.init_actions.edit")]}):null,i.jsx("button",{type:"button","data-testid":`${h}-code-${M}`,onClick:()=>k(x===M?null:M),"aria-label":n("layout_editor.action_list.view_code"),title:n("layout_editor.action_list.view_code"),style:x===M?_k:Ds,children:"</>"}),I?null:i.jsx("button",{type:"button","data-testid":`${h}-remove-${M}`,onClick:()=>S(M),"aria-label":n("layout_editor.action_list.remove"),title:n("layout_editor.action_list.remove"),style:xk,children:"✕"})]}),H?i.jsx(kw,{action:O,recipe:H,values:B.kind==="preset"?B.values:{},t:n,showCodeButton:!1,testIdPrefix:`${h}-preview-${M}`}):null,w===M&&!I&&F?i.jsx("div",{"data-testid":`${h}-edit-body-${M}`,children:F}):null,d&&w!==M?d(O,G=>T(M,G)):null,x===M?i.jsx("pre",{"data-testid":`${h}-code-view-${M}`,style:{margin:"8px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:12,overflow:"auto"},children:JSON.stringify(O,null,2)}):null]})]},M)}),i.jsx("li",{"aria-hidden":"true",style:{listStyle:"none",margin:0,padding:0},children:i.jsx(Rn,{active:E.isDropTarget(e.length),testid:`${h}-dropline-end`})})]}),D?i.jsx("div",{style:{marginTop:8},children:D}):null]})}const hk={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},yk={fontSize:10,color:"#7c3aed",whiteSpace:"nowrap"},bk={fontSize:10,color:"#92400e",whiteSpace:"nowrap"},mk={display:"flex",flexDirection:"column",gap:6,paddingTop:6,borderTop:"1px dashed #e2e8f0",marginTop:6},Qc={flex:"0 0 auto",padding:"3px 8px",fontSize:11,fontWeight:500,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap",lineHeight:1.4},vk={...Qc,background:"#eff6ff",borderColor:"#bfdbfe",color:"#2563eb"},Ds={flex:"0 0 auto",width:26,height:24,display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:11,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",fontFamily:"ui-monospace, monospace"},_k={...Ds,background:"#0f172a",borderColor:"#0f172a",color:"#e2e8f0"},xk={...Ds,color:"#dc2626",borderColor:"#fecaca",fontFamily:"inherit"};function Rc(e){const t=e.__source;return t&&typeof t=="object"?t:null}function wk({actions:e,onChange:t,recipes:n,t:o,bindingCandidates:r,pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,modalCandidates:d}){const c=u.useMemo(()=>({pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,modalCandidates:d,bindingCandidates:r}),[s,a,l,d,r]),p=u.useMemo(()=>e.filter(h=>Rc(h)?.kind==="base"),[e]),f=u.useMemo(()=>e.filter(h=>Rc(h)?.kind!=="base"),[e]),y=p.length>0,g=u.useCallback(h=>{t(y?[...p,...h]:h)},[t,y,p]),m=u.useCallback(()=>{},[]);return i.jsxs("div",{className:"g7le-init-actions-form","data-testid":"g7le-init-actions-form",style:kk,children:[i.jsx("p",{style:Sk,children:o("layout_editor.page_settings.init_actions.heading")}),y?i.jsxs(i.Fragment,{children:[i.jsxs("div",{"data-testid":"g7le-init-action-group-base",style:eu,children:[i.jsx("div",{style:tu,children:o("layout_editor.init_actions.group_base")}),i.jsx(Gt,{actions:p,onChange:m,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,isLocked:()=>!0,hideAddPicker:!0,testIdPrefix:"g7le-init-action-base"}),i.jsxs("p",{"data-testid":"g7le-init-action-base-hint",style:Ck,children:["ⓘ ",o("layout_editor.init_actions.base_readonly_hint")]})]}),i.jsxs("div",{"data-testid":"g7le-init-action-group-self",style:eu,children:[i.jsx("div",{style:tu,children:o("layout_editor.init_actions.group_self")}),f.length===0?i.jsx("p",{style:nu,children:o("layout_editor.init_actions.empty_self")}):null,i.jsx(Gt,{actions:f,onChange:g,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,testIdPrefix:"g7le-init-action-self"})]}),i.jsxs("p",{"data-testid":"g7le-init-action-merge-order",style:$k,children:["ⓘ ",o("layout_editor.init_actions.merge_order")]})]}):i.jsxs(i.Fragment,{children:[e.length===0?i.jsx("p",{"data-testid":"g7le-init-action-empty",style:nu,children:o("layout_editor.init_actions.empty")}):null,i.jsx(Gt,{actions:e,onChange:t,t:o,recipes:n,candidatePools:c,addContext:"init",showSourceBadge:!0,testIdPrefix:"g7le-init-action"})]})]})}const kk={display:"flex",flexDirection:"column",gap:12,minWidth:0},Sk={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},eu={border:"1px solid #e2e8f0",borderRadius:10,padding:10,display:"flex",flexDirection:"column",gap:8},tu={fontSize:12,fontWeight:700,color:"#475569"},Ck={margin:0,fontSize:11,color:"#64748b"},nu={margin:0,fontSize:12,color:"#94a3b8"},$k={margin:0,fontSize:11,color:"#64748b"};function Ek(e){return e==="spinner"?new Set(["spinner","page"]):new Set(["skeleton"])}function ou(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function ru({candidates:e,value:t,onSelect:n,styleKind:o,t:r,testIdPrefix:s="g7le-loading-component-picker"}){const a=yo(),l=()=>{const d=a.open({ariaLabel:r("layout_editor.overlay.loading_picker.select"),width:520,maxHeightRatio:.7,content:i.jsx(Ik,{candidates:e,value:t,styleKind:o,t:r,testIdPrefix:s,onPick:c=>{n(c),a.close(d)},onClose:()=>a.close(d)})})};return i.jsx("div",{className:s,"data-testid":s,style:{minWidth:0},children:i.jsxs("div",{style:Ak,children:[i.jsx("span",{"data-testid":`${s}-current`,style:Tk,children:t&&t.length>0?t:r("layout_editor.overlay.loading_picker.none")}),i.jsxs("button",{type:"button","data-testid":`${s}-open`,onClick:l,style:Dk,children:["🔍 ",r("layout_editor.overlay.loading_picker.select")]})]})})}function Ik({candidates:e,value:t,styleKind:n,t:o,testIdPrefix:r,onPick:s,onClose:a}){const[l,d]=u.useState(""),c=Ek(n),p=u.useMemo(()=>{const f=l.trim().toLowerCase();return e.filter(y=>c.has(y.role)).filter(y=>f?ou(y.label,o,y.name).toLowerCase().includes(f)||y.name.toLowerCase().includes(f):!0)},[e,n,l,o]);return i.jsxs("div",{"data-testid":`${r}-modal`,style:Nk,children:[i.jsxs("div",{style:zk,children:[i.jsxs("span",{style:Mk,children:["🔍 ",o("layout_editor.overlay.loading_picker.select")]}),i.jsx("button",{type:"button","data-testid":`${r}-close`,onClick:a,"aria-label":o("layout_editor.overlay.loading_picker.select"),style:Lk,children:"✕"})]}),i.jsx("input",{type:"text","data-testid":`${r}-search`,value:l,placeholder:o("layout_editor.overlay.loading_picker.search"),onChange:f=>d(f.target.value),style:Pk,autoFocus:!0}),p.length===0?i.jsx("p",{"data-testid":`${r}-empty`,style:Kk,children:o("layout_editor.overlay.loading_picker.empty")}):i.jsx("ul",{style:Ok,children:p.map(f=>i.jsx("li",{children:i.jsxs("button",{type:"button","data-testid":`${r}-item-${f.name}`,onClick:()=>s(f.name),style:{...Bk,...t===f.name?Wk:{}},children:[i.jsx("span",{style:jk,children:ou(f.label,o,f.name)}),i.jsx("code",{style:Hk,children:f.name}),i.jsx("span",{style:Fk,children:f.role})]})},f.name))})]})}const Ak={display:"flex",alignItems:"center",gap:6,minWidth:0},Tk={flex:1,minWidth:0,fontSize:12,color:"#0f172a",fontFamily:"monospace"},Dk={padding:"4px 10px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},Nk={display:"flex",flexDirection:"column",minHeight:0,padding:16,boxSizing:"border-box",fontFamily:"system-ui, -apple-system, sans-serif"},zk={display:"flex",alignItems:"center",gap:8,paddingBottom:8,borderBottom:"1px solid #e2e8f0",marginBottom:10},Mk={flex:1,fontSize:14,fontWeight:700,color:"#0f172a",minWidth:0},Lk={border:"none",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:15,lineHeight:1,padding:4},Pk={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:8,boxSizing:"border-box"},Ok={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:2,overflowY:"auto",minHeight:0},Bk={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"8px 10px",fontSize:12,border:"1px solid transparent",background:"transparent",cursor:"pointer",borderRadius:6},Wk={border:"1px solid #3b82f6",background:"#eff6ff"},jk={flex:1,minWidth:0,color:"#0f172a"},Hk={fontSize:10,color:"#94a3b8",fontFamily:"monospace"},Fk={fontSize:10,color:"#64748b",background:"#f1f5f9",borderRadius:4,padding:"0 4px"},Kk={margin:0,fontSize:12,color:"#94a3b8",padding:"8px 4px"};function iu(e){return e===!0?{enabled:!0}:e===!1||e===void 0?{}:e}const Vk=["opaque","blur","fade","skeleton","spinner"];function Uk({value:e,patch:t,t:n,baseValue:o,ownValue:r,loadingComponents:s=[],progressiveDataSources:a=[],bindingCandidates:l}){const d=iu(e),c=d.enabled??!1,p=!!o&&Object.keys(o).length>0,f=Tn("component-target-picker"),[y,g]=u.useState((d.wait_for??[]).length>0),m=u.useCallback(w=>{const _={...iu(e)};w(_),t(_)},[e,t]),h=r&&typeof r=="object"?r:e&&typeof e=="object"?e:{},v=u.useCallback(w=>p&&o!==void 0&&w in o&&!(w in h),[p,o,h]),b=u.useCallback(w=>{o&&m(_=>{_[w]=o[w]})},[o,m]),x=u.useCallback(w=>m(_=>{_.enabled=w}),[m]),k=d.target!==void 0?"region":"full";return i.jsxs("div",{className:"g7le-overlay-form","data-testid":"g7le-overlay-form",style:qk,children:[p?i.jsxs("p",{"data-testid":"g7le-overlay-inherit-banner",style:Gk,children:["ⓘ ",n("layout_editor.overlay.inherit_banner")]}):null,i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.enabled_label"),inherited:v("enabled"),hasInheritance:p,onOverride:()=>b("enabled"),testidKey:"enabled",t:n}),i.jsx(Nr,{disabled:v("enabled"),children:i.jsx(Dr,{checked:c,onChange:x,testid:"g7le-overlay-enabled",ariaLabel:n("layout_editor.overlay.enabled_label"),label:n(c?"layout_editor.overlay.on":"layout_editor.overlay.off")})}),i.jsxs("p",{style:Wr,children:["ⓘ ",n("layout_editor.overlay.enabled_hint")]})]}),i.jsx(Nr,{disabled:!c,testid:"g7le-overlay-body",style:{display:"flex",flexDirection:"column",gap:16,minWidth:0},children:i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.scope_label"),inherited:v("target"),hasInheritance:p,onOverride:()=>b("target"),testidKey:"target",t:n}),i.jsxs("div",{"data-testid":"g7le-overlay-scope",style:zs,children:[i.jsx(Or,{active:k==="full",onClick:()=>m(w=>{delete w.target,delete w.fallback_target}),testid:"g7le-overlay-scope-full",children:n("layout_editor.overlay.scope_full")}),i.jsx(Or,{active:k==="region",onClick:()=>m(w=>{w.target=w.target??""}),testid:"g7le-overlay-scope-region",children:n("layout_editor.overlay.scope_region")})]}),i.jsx(Nr,{disabled:k!=="region",testid:"g7le-overlay-region-box",style:Zk,children:i.jsxs(i.Fragment,{children:[i.jsx(zn,{label:n("layout_editor.overlay.target_label"),children:i.jsx("div",{"data-testid":"g7le-overlay-target-picker",children:f?i.jsx(f,{control:{label:n("layout_editor.overlay.target_label")},value:d.target??"",onChange:w=>m(_=>{_.target=typeof w=="string"?w:""}),t:n}):i.jsx("input",{type:"text","data-testid":"g7le-overlay-target-input",value:d.target??"",onChange:w=>m(_=>{_.target=w.target.value}),style:e1})})}),i.jsx(zn,{label:n("layout_editor.overlay.fallback_label"),children:i.jsx("div",{"data-testid":"g7le-overlay-fallback-picker",children:f?i.jsx(f,{control:{label:n("layout_editor.overlay.fallback_label")},value:d.fallback_target??"",onChange:w=>m(_=>{const C=typeof w=="string"?w:"";C===""?delete _.fallback_target:_.fallback_target=C}),t:n}):null})}),i.jsxs("p",{style:Wr,children:["ⓘ ",n("layout_editor.overlay.fallback_hint")]})]})})]}),i.jsxs("div",{style:Br,children:[i.jsx(Ns,{label:n("layout_editor.overlay.style_label"),inherited:v("style"),hasInheritance:p,onOverride:()=>b("style"),testidKey:"style",t:n}),i.jsx("div",{"data-testid":"g7le-overlay-style",style:zs,children:Vk.map(w=>i.jsx(Or,{active:(d.style??"opaque")===w,onClick:()=>m(_=>{_.style=w}),testid:`g7le-overlay-style-${w}`,children:n(`layout_editor.overlay.style_${w}`)},w))}),d.style==="skeleton"?i.jsxs("div",{style:su,children:[i.jsx(zn,{label:n("layout_editor.overlay.skeleton_component"),children:i.jsx(ru,{candidates:s,value:d.skeleton?.component,styleKind:"skeleton",onSelect:w=>m(_=>{_.skeleton={..._.skeleton,component:w}}),t:n,testIdPrefix:"g7le-overlay-skeleton-component"})}),i.jsx(zn,{label:n("layout_editor.overlay.skeleton_animation"),children:i.jsx("div",{"data-testid":"g7le-overlay-skeleton-anim",style:zs,children:["wave","pulse","none"].map(w=>i.jsx(Or,{active:(d.skeleton?.animation??"wave")===w,onClick:()=>m(_=>{_.skeleton={..._.skeleton,animation:w}}),testid:`g7le-overlay-skeleton-anim-${w}`,children:n(`layout_editor.overlay.anim_${w}`)},w))})}),i.jsx(zn,{label:n("layout_editor.overlay.skeleton_count"),children:i.jsx("input",{type:"number","data-testid":"g7le-overlay-skeleton-count",value:d.skeleton?.iteration_count??"",onChange:w=>m(_=>{const C=parseInt(w.target.value,10);_.skeleton={..._.skeleton,iteration_count:Number.isNaN(C)?void 0:C}}),style:t1})})]}):null,d.style==="spinner"?i.jsxs("div",{style:su,children:[i.jsx(zn,{label:n("layout_editor.overlay.spinner_component"),children:i.jsx(ru,{candidates:s,value:d.spinner?.component,styleKind:"spinner",onSelect:w=>m(_=>{_.spinner={..._.spinner,component:w}}),t:n,testIdPrefix:"g7le-overlay-spinner-component"})}),i.jsx(zn,{label:n("layout_editor.overlay.spinner_text"),children:i.jsx("div",{"data-testid":"g7le-overlay-spinner-text",style:{minWidth:0},children:i.jsx(vt,{value:d.spinner?.text??"",onChange:w=>m(_=>{_.spinner={..._.spinner,text:w===void 0||w===""?void 0:w}}),t:n,candidates:l,enableExpressionTree:!0,expressionTreeCollapsible:!0,testidPrefix:"g7le-overlay-spinner-text-field"})})})]}):null]}),i.jsxs("div",{style:Br,children:[i.jsxs("button",{type:"button","data-testid":"g7le-overlay-waitfor-toggle","aria-expanded":y,onClick:()=>g(w=>!w),style:r1,children:[y?"▾":"▸"," ",n("layout_editor.overlay.waitfor_label")]}),y?i.jsxs("div",{"data-testid":"g7le-overlay-waitfor-body",style:i1,children:[i.jsxs("p",{"data-testid":"g7le-overlay-waitfor-hint",style:Wr,children:["ⓘ ",n("layout_editor.overlay.waitfor_hint")]}),a.length===0?i.jsx("p",{"data-testid":"g7le-overlay-waitfor-empty",style:Wr,children:n("layout_editor.overlay.waitfor_empty")}):i.jsx("div",{"data-testid":"g7le-overlay-waitfor",style:{display:"flex",flexWrap:"wrap",gap:6},children:a.map(w=>{const _=(d.wait_for??[]).includes(w.id);return i.jsxs("label",{"data-testid":`g7le-overlay-waitfor-${w.id}`,style:n1(_),children:[i.jsx("input",{type:"checkbox",checked:_,onChange:()=>m(C=>{const E=new Set(C.wait_for??[]);E.has(w.id)?E.delete(w.id):E.add(w.id);const S=Array.from(E);S.length===0?delete C.wait_for:C.wait_for=S})}),i.jsx(Dc,{option:w,testIdPrefix:`g7le-overlay-waitfor-${w.id}`})]},w.id)})})]}):null]}),p?i.jsxs("button",{type:"button","data-testid":"g7le-overlay-reset-inherit",onClick:()=>t(void 0),style:o1,children:["⟲ ",n("layout_editor.overlay.reset_inherit")]}):null]})})]})}function Or({active:e,onClick:t,children:n,testid:o}){return i.jsx("button",{type:"button","data-testid":o,"aria-pressed":e,onClick:t,style:{...Xk,...e?Jk:{}},children:n})}function Ns({label:e,inherited:t,hasInheritance:n,onOverride:o,testidKey:r,t:s}){return i.jsxs("div",{style:s1,children:[i.jsx("label",{style:Yk,children:e}),n?t?i.jsxs(i.Fragment,{children:[i.jsxs("span",{"data-testid":`g7le-overlay-inherited-${r}`,style:a1,children:["〔",s("layout_editor.overlay.inherited_badge"),"〕"]}),i.jsx("button",{type:"button","data-testid":`g7le-overlay-override-${r}`,onClick:o,style:d1,children:s("layout_editor.overlay.override_field")})]}):i.jsxs("span",{"data-testid":`g7le-overlay-overridden-${r}`,style:l1,children:["〔",s("layout_editor.overlay.overridden_badge"),"〕"]}):null]})}function zn({label:e,children:t}){return i.jsxs("div",{style:Qk,children:[i.jsx("span",{style:Rk,children:e}),i.jsx("div",{style:{flex:1,minWidth:0},children:t})]})}const qk={display:"flex",flexDirection:"column",gap:16,minWidth:0},Gk={margin:0,fontSize:12,color:"#1d4ed8",background:"#eff6ff",padding:"8px 10px",borderRadius:6},Br={display:"flex",flexDirection:"column",gap:6,minWidth:0},Yk={fontSize:13,fontWeight:600,color:"#0f172a"},Wr={margin:0,fontSize:11,color:"#94a3b8"},zs={display:"flex",gap:4,flexWrap:"wrap"},Xk={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Jk={border:"1px solid #3b82f6",background:"#eff6ff",color:"#1d4ed8"},Zk={display:"flex",flexDirection:"column",gap:8,padding:8,border:"1px dashed #e2e8f0",borderRadius:8},su={display:"flex",flexDirection:"column",gap:8,padding:8,border:"1px dashed #e2e8f0",borderRadius:8},Qk={display:"flex",alignItems:"center",gap:8,minWidth:0},Rk={fontSize:11,color:"#64748b",minWidth:90},e1={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},t1={width:80,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},n1=e=>({display:"inline-flex",alignItems:"center",gap:4,padding:"4px 8px",fontSize:12,border:`1px solid ${e?"#3b82f6":"#cbd5e1"}`,borderRadius:12,background:e?"#eff6ff":"#fff",cursor:"pointer"}),o1={alignSelf:"flex-start",padding:"6px 12px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},r1={alignSelf:"flex-start",padding:"4px 0",fontSize:13,fontWeight:600,color:"#0f172a",background:"transparent",border:"none",cursor:"pointer"},i1={display:"flex",flexDirection:"column",gap:6,paddingLeft:14,minWidth:0},s1={display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",minWidth:0},a1={fontSize:11,color:"#64748b",background:"#f1f5f9",borderRadius:4,padding:"1px 6px"},l1={fontSize:11,color:"#0369a1",background:"#e0f2fe",borderRadius:4,padding:"1px 6px"},d1={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},c1="g7le:computed-effect-preview",u1={number:"layout_editor.computed.type_number",string:"layout_editor.computed.type_string",boolean:"layout_editor.computed.type_boolean",list:"layout_editor.computed.type_list",object:"layout_editor.computed.type_object",null:"layout_editor.computed.type_null"};function p1(e,t,n){if(t==="list"){const o=e,r=o.slice(0,2).map(s=>JSON.stringify(s)).join(", ");return o.length<=2?`[${r}]`:`[${r}, ${n("layout_editor.computed.list_more",{count:o.length})}]`}return t==="object"?JSON.stringify(e):t==="boolean"?e?"true":"false":t==="null"?n("layout_editor.computed.value_none"):String(e)}function au({expr:e,computedKey:t,sampleContext:n,t:o,isEffect:r=!1,testIdPrefix:s="g7le-computed-preview"}){const a=Id(e,n),l=()=>{typeof window>"u"||!t||window.dispatchEvent(new CustomEvent(c1,{detail:{key:t}}))};return a.ok?i.jsxs("div",{className:s,"data-testid":s,style:f1,children:[i.jsx("span",{style:g1,children:o("layout_editor.computed.preview_label")}),i.jsx("span",{"data-testid":`${s}-value`,style:h1,children:p1(a.value,a.type,o)}),i.jsxs("span",{"data-testid":`${s}-type`,style:y1,children:["(",o(u1[a.type]),")"]}),r&&a.type==="boolean"?i.jsxs("button",{type:"button","data-testid":`${s}-effect`,onClick:l,style:b1,children:["▷ ",o("layout_editor.computed.effect_preview")]}):null]}):i.jsxs("div",{"data-testid":`${s}-error`,style:m1,children:["⚠ ",o("layout_editor.computed.preview_error")]})}const f1={display:"flex",alignItems:"center",flexWrap:"wrap",gap:6,minWidth:0},g1={fontSize:11,color:"#94a3b8"},h1={fontSize:12,color:"#0f172a",fontFamily:"monospace",wordBreak:"break-all"},y1={fontSize:10,color:"#64748b"},b1={padding:"2px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},m1={fontSize:12,color:"#b45309"},v1=[{op:"count",labelKey:"layout_editor.computed.op_count"},{op:"sum",labelKey:"layout_editor.computed.op_sum"},{op:"filter",labelKey:"layout_editor.computed.op_filter"},{op:"toOptions",labelKey:"layout_editor.computed.op_to_options"},{op:"nth",labelKey:"layout_editor.computed.op_nth"},{op:"firstOf",labelKey:"layout_editor.computed.op_first_of"},{op:"literal",labelKey:"layout_editor.computed.op_literal"}],_1=["=","!=",">","<",">=","<=","includes"],x1=new Set(["count","sum","filter"]);function lu({model:e,onChange:t,t:n,dataSourceCandidates:o,bindingCandidates:r,existingKeys:s=[]}){const a=u.useCallback(g=>{const m={...e};g(m),t(m)},[e,t]),l=e.conditions??[],d=Tn("datasource-picker"),c=e.key!==""&&s.includes(e.key),p=()=>a(g=>{g.conditions=[...g.conditions??[],{field:"",cmp:"=",value:""}]}),f=(g,m)=>a(h=>{const v=(h.conditions??[]).slice(),b={...v[g]};m(b),v[g]=b,h.conditions=v}),y=g=>a(m=>{m.conditions=(m.conditions??[]).filter((h,v)=>v!==g)});return i.jsxs("div",{className:"g7le-computed-custom","data-testid":"g7le-computed-custom",style:w1,children:[i.jsx(Ms,{label:`① ${n("layout_editor.computed.custom_step1")}`,children:i.jsx("div",{"data-testid":"g7le-computed-custom-source",children:d?i.jsx(d,{control:{},value:e.source,onChange:g=>a(m=>{m.source=typeof g=="string"?g:void 0}),t:n,candidates:o}):i.jsx("input",{type:"text",value:e.source??"",onChange:g=>a(m=>{m.source=g.target.value}),style:ro})})}),i.jsxs(Ms,{label:`② ${n("layout_editor.computed.custom_step2")}`,children:[i.jsx("select",{"data-testid":"g7le-computed-custom-op",value:e.op,onChange:g=>a(m=>{m.op=g.target.value}),style:ro,children:v1.map(({op:g,labelKey:m})=>i.jsx("option",{value:g,children:n(m)},g))}),e.op==="sum"?i.jsx("div",{"data-testid":"g7le-computed-custom-sumfield",style:{minWidth:0},children:i.jsx(Ve,{value:e.sumField??"",onChange:g=>a(m=>{m.sumField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_sum_field"),testidPrefix:"g7le-computed-custom-sumfield-chip"})}):null,e.op==="toOptions"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("div",{"data-testid":"g7le-computed-custom-valuefield",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.valueField??"",onChange:g=>a(m=>{m.valueField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_value_field"),testidPrefix:"g7le-computed-custom-valuefield-chip"})}),i.jsx("div",{"data-testid":"g7le-computed-custom-labelfield",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.labelField??"",onChange:g=>a(m=>{m.labelField=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_label_field"),testidPrefix:"g7le-computed-custom-labelfield-chip"})})]}):null,e.op==="nth"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-index",value:e.index??"",placeholder:"0",onChange:g=>a(m=>{m.index=g.target.value}),style:ro}),i.jsx("div",{"data-testid":"g7le-computed-custom-prop",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.prop??"",onChange:g=>a(m=>{m.prop=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_prop"),testidPrefix:"g7le-computed-custom-prop-chip"})})]}):null,e.op==="literal"?i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsxs("select",{"data-testid":"g7le-computed-custom-literalkind",value:e.literalKind??"string",onChange:g=>a(m=>{m.literalKind=g.target.value}),style:ro,children:[i.jsx("option",{value:"string",children:n("layout_editor.computed.kind_string")}),i.jsx("option",{value:"number",children:n("layout_editor.computed.kind_number")}),i.jsx("option",{value:"boolean",children:n("layout_editor.computed.kind_boolean")})]}),i.jsx("div",{"data-testid":"g7le-computed-custom-literalvalue",style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:e.literalValue??"",onChange:g=>a(m=>{m.literalValue=g}),t:n,candidates:r,testidPrefix:"g7le-computed-custom-literalvalue-chip"})})]}):null,e.op==="firstOf"?i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-candidates",value:(e.candidates??[]).join(", "),placeholder:n("layout_editor.computed.custom_candidates"),onChange:g=>a(m=>{m.candidates=g.target.value.split(",").map(h=>h.trim()).filter(Boolean)}),style:ro}),i.jsx("div",{"data-testid":"g7le-computed-custom-fallback",style:{minWidth:0},children:i.jsx(Ve,{value:e.fallback??"",onChange:g=>a(m=>{m.fallback=g}),t:n,candidates:r,placeholder:n("layout_editor.computed.custom_fallback"),testidPrefix:"g7le-computed-custom-fallback-chip"})})]}):null,x1.has(e.op)?i.jsxs("div",{style:$1,children:[l.map((g,m)=>i.jsxs("div",{"data-testid":`g7le-computed-custom-cond-${m}`,style:E1,children:[i.jsx("div",{"data-testid":`g7le-computed-custom-cond-field-${m}`,style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:g.field,onChange:h=>f(m,v=>{v.field=h}),t:n,candidates:r,placeholder:n("layout_editor.computed.cond_field"),testidPrefix:`g7le-computed-custom-cond-field-${m}-chip`})}),i.jsx("select",{value:g.cmp,onChange:h=>f(m,v=>{v.cmp=h.target.value}),style:I1,children:_1.map(h=>i.jsx("option",{value:h,children:h},h))}),i.jsx("div",{"data-testid":`g7le-computed-custom-cond-value-${m}`,style:{flex:1,minWidth:0},children:i.jsx(Ve,{value:g.value,onChange:h=>f(m,v=>{v.value=h}),t:n,candidates:r,placeholder:n("layout_editor.computed.cond_value"),testidPrefix:`g7le-computed-custom-cond-value-${m}-chip`})}),i.jsx("button",{type:"button","data-testid":`g7le-computed-custom-cond-remove-${m}`,onClick:()=>y(m),style:A1,children:"✕"})]},m)),i.jsxs("button",{type:"button","data-testid":"g7le-computed-custom-cond-add",onClick:p,style:T1,children:["+ ",n("layout_editor.computed.cond_add")]})]}):null]}),i.jsxs(Ms,{label:`③ ${n("layout_editor.computed.custom_step3")}`,children:[i.jsx("input",{type:"text","data-testid":"g7le-computed-custom-key",value:e.key,onChange:g=>a(m=>{m.key=g.target.value}),style:{...ro,...c?C1:{}}}),c?i.jsxs("p",{"data-testid":"g7le-computed-custom-key-dup",style:D1,children:["⚠ ",n("layout_editor.computed.key_duplicate")]}):null]})]})}function Ms({label:e,children:t}){return i.jsxs("div",{style:k1,children:[i.jsx("div",{style:S1,children:e}),i.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6,minWidth:0},children:t})]})}function Ls(e){return Vm(e)}const w1={display:"flex",flexDirection:"column",gap:12,padding:10,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff"},k1={display:"flex",flexDirection:"column",gap:4,minWidth:0},S1={fontSize:12,fontWeight:600,color:"#475569"},ro={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:0,width:"100%",boxSizing:"border-box"},C1={border:"1px solid #ef4444"},$1={display:"flex",flexDirection:"column",gap:6,marginTop:4},E1={display:"flex",gap:4,alignItems:"center",minWidth:0},I1={padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},A1={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12},T1={alignSelf:"flex-start",padding:"2px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},D1={margin:0,fontSize:11,color:"#b45309"};function Ps(e,t,n){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function N1(e){if(e&&typeof e=="object"){const t=e.__source;if(t&&typeof t=="object")return t}return null}function z1({computed:e,onChange:t,recipes:n,t:o,sampleContext:r,dataSourceCandidates:s,bindingCandidates:a,computedSource:l={},effectKeys:d=[],onRevert:c}){const p=u.useMemo(()=>xd(n),[n]),[f,y]=u.useState(!1),[g,m]=u.useState(null),h=Object.keys(e),v=u.useCallback((_,C)=>{t({...e,[_]:C})},[e,t]),b=u.useCallback(_=>{const C={...e};delete C[_],t(C)},[e,t]),x=u.useCallback((_,C,E)=>{const S={...e};_&&_!==C&&delete S[_],S[C]=E,t(S)},[e,t]),k=u.useCallback(_=>{const C=kd(_,{});let E=_.id,S=1;for(;E in e;)E=`${_.id}${++S}`;t({...e,[E]:C}),y(!1)},[e,t]),w=u.useCallback(()=>{!g||!g.key||(t({...e,[g.key]:Ls(g)}),m(null),y(!1))},[g,e,t]);return i.jsxs("div",{className:"g7le-computed-form","data-testid":"g7le-computed-form",style:O1,children:[i.jsx("p",{style:B1,children:o("layout_editor.page_settings.computed.heading")}),h.length>0?h.map(_=>i.jsx(M1,{computedKey:_,expr:e[_],recipes:p,rawRecipes:n,t:o,sampleContext:r,dataSourceCandidates:s,bindingCandidates:a,source:l[_],isEffect:d.includes(_),existingKeys:h,onPatchExpr:C=>v(_,C),onRename:(C,E)=>x(_,C,E),onRemove:()=>b(_),onRevert:c?()=>c(_):void 0},_)):i.jsx("p",{"data-testid":"g7le-computed-empty",style:aS,children:o("layout_editor.computed.empty")}),i.jsxs("div",{style:{minWidth:0},children:[i.jsxs("button",{type:"button","data-testid":"g7le-computed-add",onClick:()=>y(_=>!_),style:R1,children:["+ ",o("layout_editor.computed.add")," ▾"]}),f?i.jsxs("div",{"data-testid":"g7le-computed-add-list",style:eS,children:[["common","more"].map(_=>{const C=p.filter(E=>(E.group??"more")===_);return C.length===0?null:i.jsxs("div",{children:[i.jsxs("div",{style:tS,children:["━━ ",o(`layout_editor.computed.group_${_}`)," ━━"]}),C.map(E=>{const S=N1(n?.[E.id]);return i.jsxs("button",{type:"button","data-testid":`g7le-computed-preset-${E.id}`,onClick:()=>k(E),style:nS,children:[i.jsx("span",{style:oS,children:Ps(E.label,o,E.id)}),S&&S.kind!=="core"?i.jsxs("span",{"data-testid":`g7le-computed-preset-source-${E.id}`,style:rS,children:["〔",S.id??S.kind,"〕"]}):null]},E.id)})]},_)}),i.jsxs("div",{style:{borderTop:"1px solid #e2e8f0",marginTop:6,paddingTop:6},children:[i.jsxs("button",{type:"button","data-testid":"g7le-computed-custom-open",onClick:()=>m(g?null:{key:"",op:"count",source:"",conditions:[]}),style:iS,children:["⚙ ",o("layout_editor.computed.custom_open")," ▾"]}),g?i.jsxs("div",{style:{marginTop:6},children:[i.jsx(lu,{model:g,onChange:m,t:o,dataSourceCandidates:s,bindingCandidates:a,existingKeys:h}),i.jsx("div",{"data-testid":"g7le-computed-custom-preview",style:{marginTop:6},children:i.jsx(au,{expr:Ls(g),sampleContext:r,t:o,testIdPrefix:"g7le-computed-custom-preview-eval"})}),i.jsx("button",{type:"button","data-testid":"g7le-computed-custom-commit",disabled:!g.key||h.includes(g.key),onClick:w,style:sS,children:o("layout_editor.computed.custom_add")})]}):null]})]}):null]})]})}function M1({computedKey:e,expr:t,recipes:n,rawRecipes:o,t:r,sampleContext:s,dataSourceCandidates:a,bindingCandidates:l,source:d,isEffect:c,existingKeys:p,onPatchExpr:f,onRename:y,onRemove:g,onRevert:m}){const[h,v]=u.useState(!1),[b,x]=u.useState(!1),k=u.useMemo(()=>Ed(t,n),[t,n]),w=d==="base",_=d==="route-override",C=k.kind==="advanced",E=k.kind==="preset"?n.find($=>$.id===k.recipeId)??null:null,S=E?Ps(E.label,r,e):e,T=($,A)=>{if(k.kind!=="preset"||!E)return;const z={...k.params,[$]:A};f(kd(E,z))};return i.jsxs("div",{"data-testid":`g7le-computed-item-${e}`,style:W1,children:[i.jsxs("div",{style:j1,children:[i.jsx("span",{style:H1,children:S}),i.jsx("code",{style:F1,children:e}),w?i.jsxs("span",{"data-testid":`g7le-computed-source-${e}`,style:K1,children:["〔",r("layout_editor.computed.source_base"),"〕"]}):null,_?i.jsxs("span",{"data-testid":`g7le-computed-overridden-${e}`,style:V1,children:["〔",r("layout_editor.computed.source_overridden"),"〕"]}):null,C?i.jsxs("span",{"data-testid":"g7le-computed-advanced",style:U1,children:["[",r("layout_editor.action.advanced"),"]"]}):null,i.jsx("span",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-computed-code-${e}`,onClick:()=>x($=>!$),style:Os,"aria-label":r("layout_editor.value_tree.show_source"),title:r("layout_editor.value_tree.show_source"),children:"</>"}),C?null:i.jsx("button",{type:"button","data-testid":`g7le-computed-edit-${e}`,onClick:()=>v($=>!$),style:Os,children:r("layout_editor.init_actions.edit")}),i.jsx("button",{type:"button","data-testid":`g7le-computed-remove-${e}`,onClick:g,style:Os,"aria-label":r("layout_editor.init_actions.remove"),children:"✕"})]}),b?i.jsx("pre",{"data-testid":`g7le-computed-code-block-${e}`,style:lS,children:t}):null,i.jsx("div",{"data-testid":`g7le-computed-preview-${e}`,children:i.jsx(au,{expr:t,computedKey:e,sampleContext:s,t:r,isEffect:c,testIdPrefix:`g7le-computed-preview-${e}-eval`})}),_&&m?i.jsxs("div",{style:q1,children:[i.jsxs("span",{style:G1,children:["ⓘ ",r("layout_editor.computed.overridden_hint")]}),i.jsx("button",{type:"button","data-testid":`g7le-computed-revert-${e}`,onClick:m,style:Y1,children:r("layout_editor.computed.revert")})]}):null,h&&!C?i.jsxs("div",{style:X1,children:[w?i.jsxs("p",{"data-testid":`g7le-computed-override-notice-${e}`,style:J1,children:["⚠ ",r("layout_editor.computed.override_notice")]}):null,k.kind==="preset"&&E?i.jsx(i.Fragment,{children:E.params.map($=>i.jsx(P1,{param:$,value:k.params[$.key]??"",t:r,dataSourceCandidates:a,bindingCandidates:l,onChange:A=>T($.key,A)},$.key))}):k.kind==="custom"?i.jsx(lu,{model:{...k.model,key:e},onChange:$=>y($.key,Ls($)),t:r,dataSourceCandidates:a,bindingCandidates:l,existingKeys:p.filter($=>$!==e)}):null]}):null]})}const L1=new Set(["datasource-picker","binding-picker","select","toggle","state-key-picker","page-picker"]);function P1({param:e,value:t,t:n,dataSourceCandidates:o,bindingCandidates:r,onChange:s}){const a=Ps(e.label,n,e.key),l=e.widget??"",c=L1.has(l)?Tn(e.widget):null,p=l==="datasource-picker"||l==="binding-picker"?o:void 0;return i.jsxs("div",{"data-testid":`g7le-computed-param-${e.key}`,style:Z1,children:[i.jsx("span",{style:Q1,children:a}),i.jsx("div",{style:{flex:1,minWidth:0},children:c?i.jsx(c,{control:{...e},value:t,onChange:f=>s(typeof f=="string"?f:""),t:n,candidates:p}):i.jsx(Ve,{value:t,onChange:f=>s(f),t:n,candidates:r,testidPrefix:`g7le-computed-param-${e.key}-chip`})})]})}const O1={display:"flex",flexDirection:"column",gap:12,minWidth:0},B1={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},W1={border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",display:"flex",flexDirection:"column",gap:6,background:"#f8fafc"},j1={display:"flex",alignItems:"center",gap:6},H1={fontSize:13,fontWeight:600,color:"#0f172a"},F1={fontSize:10,color:"#64748b",fontFamily:"monospace"},K1={fontSize:10,color:"#0891b2",whiteSpace:"nowrap"},V1={fontSize:10,color:"#7c3aed",whiteSpace:"nowrap"},U1={fontSize:10,color:"#92400e",whiteSpace:"nowrap"},q1={display:"flex",alignItems:"center",gap:8,marginTop:4,minWidth:0},G1={fontSize:11,color:"#7c3aed",flex:1,minWidth:0},Y1={border:"1px solid #c4b5fd",borderRadius:6,background:"#f5f3ff",color:"#6d28d9",cursor:"pointer",fontSize:11,padding:"2px 8px",whiteSpace:"nowrap"},Os={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:11,padding:"2px 6px"},X1={display:"flex",flexDirection:"column",gap:6,paddingTop:6,borderTop:"1px dashed #e2e8f0"},J1={margin:0,fontSize:11,color:"#b45309",background:"#fef3c7",padding:"6px 8px",borderRadius:6},Z1={display:"flex",alignItems:"center",gap:8,minWidth:0},Q1={fontSize:11,color:"#64748b",minWidth:90},R1={padding:"6px 12px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},eS={marginTop:6,border:"1px solid #cbd5e1",borderRadius:8,background:"#fff",padding:8,maxHeight:380,overflowY:"auto"},tS={fontSize:11,fontWeight:700,color:"#94a3b8",padding:"6px 4px 2px"},nS={display:"flex",alignItems:"center",gap:6,width:"100%",textAlign:"left",padding:"6px 8px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:6},oS={flex:1,minWidth:0,color:"#0f172a"},rS={fontSize:10,color:"#64748b",whiteSpace:"nowrap"},iS={padding:"6px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},sS={marginTop:6,padding:"6px 12px",fontSize:12,border:"1px solid #3b82f6",borderRadius:6,background:"#3b82f6",color:"#fff",cursor:"pointer"},aS={margin:0,fontSize:12,color:"#94a3b8"},lS={margin:"2px 0 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"};function dS(e,t){return t==="any"?!0:e===null||typeof e!="object"?!1:t==="object"?!Array.isArray(e):!0}function Bs({value:e,onChange:t,onValidityChange:n,t:o,shape:r="any",emptyValue:s=void 0,placeholder:a,minHeight:l=80,testidPrefix:d="g7le-json-block",shapeErrorKey:c,invalidErrorKey:p,ariaLabel:f,id:y,validate:g}){const[m,h]=u.useState(()=>jr(e)),[v,b]=u.useState(null),x=u.useRef(!1),k=u.useRef(e);u.useEffect(()=>{if(!x.current&&!Object.is(e,k.current)){if(jr(e)===jr(k.current)){k.current=e;return}h(jr(e)),b(null),k.current=e,n?.(!0)}},[e]);const w=_=>{if(x.current=!0,h(_),_.trim()===""){b(null),k.current=s,n?.(!0),t(s);return}let C;try{C=JSON.parse(_)}catch{b(o(p??"layout_editor.json_block.invalid")),n?.(!1);return}if(!dS(C,r)){b(o(c??"layout_editor.json_block.invalid")),n?.(!1);return}const E=g?.(C)??null;if(E){b(E),n?.(!1);return}b(null),k.current=C,n?.(!0),t(C)};return i.jsxs("div",{className:"g7le-json-block",style:cS,children:[i.jsx("textarea",{id:y,"data-testid":d,style:{...uS,minHeight:l},value:m,spellCheck:!1,"aria-label":f,"aria-invalid":v?!0:void 0,placeholder:a,onChange:_=>w(_.target.value),onBlur:()=>{x.current=!1}}),v?i.jsx("div",{"data-testid":`${d}-error`,style:pS,children:v}):null]})}function jr(e){if(e===void 0)return"";try{return JSON.stringify(e,null,2)??""}catch{return""}}const cS={display:"flex",flexDirection:"column",gap:4,minWidth:0},uS={width:"100%",boxSizing:"border-box",fontFamily:"ui-monospace, SFMono-Regular, Menlo, monospace",fontSize:12,lineHeight:1.5,color:"#0f172a",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,padding:"6px 8px",resize:"vertical"},pS={fontSize:11,color:"#dc2626"},fS=[{kind:"string",labelKey:"layout_editor.initstate.kind_string"},{kind:"number",labelKey:"layout_editor.initstate.kind_number"},{kind:"boolean",labelKey:"layout_editor.initstate.kind_boolean"},{kind:"null",labelKey:"layout_editor.initstate.kind_null"},{kind:"list",labelKey:"layout_editor.initstate.kind_list"},{kind:"object",labelKey:"layout_editor.initstate.kind_object"}],Ws={local:"initLocal",global:"initGlobal",isolated:"initIsolated"};function gS({raw:e,own:t,patch:n,t:o,isolatedOrphan:r={}}){const s=u.useMemo(()=>{const f=t?.state&&typeof t.state=="object"?t.state:void 0,y=e?.state&&typeof e.state=="object"?e.state:void 0;if(!(!f&&!y))return{...y??{},...f??{}}},[t,e]),{initLocal:a,migrated:l}=u.useMemo(()=>Pw({initLocal:e?.initLocal,state:s}),[e,s]),d=f=>{if(f==="local")return a;const y=e?.[Ws[f]];return y&&typeof y=="object"?y:{}},c=f=>{if(f==="local"){const y=t?.initLocal??{},g=s?Object.keys(s):[];return Array.from(new Set([...Object.keys(y),...g]))}return Object.keys(t?.[Ws[f]]??{})},p=u.useCallback((f,y)=>{const g=Ws[f];f==="local"&&l&&n("state",void 0),n(g,Object.keys(y).length===0?void 0:y)},[n,l]);return i.jsxs("div",{className:"g7le-initstate-form","data-testid":"g7le-initstate-form",style:yS,children:[i.jsx("p",{style:bS,children:o("layout_editor.page_settings.initstate.heading")}),l?i.jsxs("p",{"data-testid":"g7le-initstate-legacy-state",style:mS,children:["ⓘ ",o("layout_editor.initstate.legacy_state")]}):null,i.jsx(js,{scope:"local",label:o("layout_editor.initstate.section_local"),value:d("local"),ownKeys:c("local"),patchSection:p,t:o}),i.jsx(js,{scope:"global",label:o("layout_editor.initstate.section_global"),value:d("global"),ownKeys:c("global"),patchSection:p,t:o}),i.jsx(js,{scope:"isolated",label:o("layout_editor.initstate.section_isolated"),description:o("layout_editor.initstate.section_isolated_desc"),value:d("isolated"),ownKeys:c("isolated"),patchSection:p,t:o,orphan:r}),i.jsxs("p",{style:PS,children:["ⓘ ",o("layout_editor.initstate.isolated_dynamic_hint")]})]})}function js({scope:e,label:t,description:n,value:o,ownKeys:r,patchSection:s,t:a,orphan:l={}}){const[d,c]=u.useState(""),[p,f]=u.useState("string"),[y,g]=u.useState(null),[m,h]=u.useState(!1),v=Object.keys(o),b=v,x=(_,C)=>{s(e,{...o,[_]:C})},k=_=>{const C={...o};delete C[_],s(e,C)},w=()=>{const _=d.trim();if(!_){g(a("layout_editor.initstate.name_required"));return}if(!Is(_)){g(a("layout_editor.initstate.name_invalid"));return}if(_ in o){g(a("layout_editor.initstate.name_duplicate"));return}const C={string:"",number:0,boolean:!1,null:null,list:[],object:{}};s(e,{...o,[_]:C[p]}),c(""),g(null)};return i.jsxs("div",{"data-testid":`g7le-initstate-section-${e}`,style:vS,children:[i.jsxs("div",{style:_S,children:[i.jsx("div",{style:xS,children:t}),i.jsx("button",{type:"button","data-testid":`g7le-initstate-${e}-mode-toggle`,onClick:()=>h(_=>!_),style:kS,children:a(m?"layout_editor.json_block.mode_block":"layout_editor.json_block.mode_code")})]}),n?i.jsxs("p",{style:wS,children:["ⓘ ",n]}):null,m?i.jsx(Bs,{value:o,shape:"object",emptyValue:{},onChange:_=>s(e,_??{}),t:a,testidPrefix:`g7le-initstate-${e}-code`,placeholder:`{ "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=y!==void 0,m=r==="badge"&&s?s(f):null;return i.jsxs("div",{"data-testid":`${l}-row-${f}`,style:{border:"1px solid #e2e8f0",borderRadius:8,padding:"8px 10px",marginBottom:6},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[i.jsx("strong",{"data-testid":`${l}-code-${f}`,style:{fontSize:13},children:f==="default"?n("layout_editor.error_rows.default"):f}),m?i.jsxs("span",{"data-testid":`${l}-source-${f}`,style:{fontSize:11,padding:"2px 6px",borderRadius:4,background:"#f1f5f9",color:"#475569"},children:["〔",n(BS[m]),"〕"]}):null,g?i.jsx("button",{type:"button","data-testid":`${l}-clear-${f}`,onClick:()=>p(f),style:{marginLeft:"auto",fontSize:12},children:n("layout_editor.error_rows.clear")}):i.jsx("span",{style:{marginLeft:"auto",fontSize:12,color:"#94a3b8"},children:n("layout_editor.error_rows.no_action")})]}),m==="inherited"||m==="template"?i.jsxs("div",{"data-testid":`${l}-override-${f}`,style:{marginTop:6,display:"flex",alignItems:"flex-start",gap:8},children:[i.jsxs("p",{style:{margin:0,flex:1,fontSize:12,color:"#64748b"},children:["ⓘ ",n(m==="inherited"?"layout_editor.error_rows.override_inherited":"layout_editor.error_rows.override_template")]}),g?i.jsx("button",{type:"button","data-testid":`${l}-revert-${f}`,onClick:()=>p(f),style:{fontSize:12,padding:"2px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},children:n("layout_editor.error_rows.revert")}):i.jsx("button",{type:"button","data-testid":`${l}-override-here-${f}`,onClick:()=>c(f,{}),style:{fontSize:12,padding:"2px 8px",border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",whiteSpace:"nowrap"},children:n("layout_editor.error_rows.override_here")})]}):null,f==="default"?i.jsxs("p",{"data-testid":`${l}-default-warn-${f}`,style:{margin:"6px 0 0",fontSize:12,color:"#b45309"},children:["⚠ ",n("layout_editor.error_rows.default_warning")]}):null,i.jsx("div",{style:{marginTop:6},children:a?a(f,y,h=>c(f,h)):i.jsx("pre",{"data-testid":`${l}-json-${f}`,style:{margin:0,fontSize:12,color:"#475569"},children:g?JSON.stringify(y):""})})]},f)})})}function WS(e){return[...e.filter(n=>n!=="maintenance"),"default"]}function jS({value:e,onChange:t,t:n,errorConfigCodes:o=[],ownCodes:r=[],templateCodes:s=[],recipes:a,pageCandidates:l,modalCandidates:d,dataSourceCandidates:c,stateKeyCandidates:p}){const f=u.useMemo(()=>wo(a),[a]),y=u.useMemo(()=>WS(o),[o]),g=f.length>0,m=u.useMemo(()=>jl("error",n),[n]),h=u.useMemo(()=>({pageCandidates:l,dataSourceCandidates:c,stateKeyCandidates:p,modalCandidates:d,bindingCandidates:m}),[l,c,p,d,m]),v=u.useCallback(b=>r.includes(b)?"self":b in e?"inherited":s.includes(b)?"template":"none",[r,e,s]);return g?i.jsxs("div",{className:"g7le-error-form","data-testid":"g7le-error-form",style:du,children:[i.jsx("p",{style:cu,children:n("layout_editor.page_settings.error.heading")}),i.jsxs("p",{style:FS,children:["ⓘ ",n("layout_editor.error.intro")]}),i.jsx(Fs,{value:e,onChange:t,t:n,codes:y,mode:"badge",sourceOf:v,testIdPrefix:"g7le-error-rows",renderActionList:(b,x,k)=>i.jsx(HS,{code:b,action:x,recipes:a,normalized:f,t:n,pools:h,onPatch:k})})]}):i.jsxs("div",{className:"g7le-error-form","data-testid":"g7le-error-form",style:du,children:[i.jsx("p",{style:cu,children:n("layout_editor.page_settings.error.heading")}),i.jsxs("p",{"data-testid":"g7le-error-degrade",style:KS,children:["ⓘ ",n("layout_editor.error.degrade")]}),i.jsx(Fs,{value:e,onChange:t,t:n,codes:y,mode:"badge",sourceOf:v,testIdPrefix:"g7le-error-rows"})]})}function HS({code:e,action:t,recipes:n,normalized:o,t:r,pools:s,onPatch:a}){const[l,d]=u.useState(!1),c=Array.isArray(t)?t[0]:t,p=typeof c?.handler=="string"?c.handler:"",f=o.find(x=>x.id===p)??null,y=c?Yn(c,o):null,g=y?.kind==="preset"?y.values:{},m=p==="sequence"||p==="parallel",h=x=>{if(x===""){a({});return}const k=o.find(w=>w.id===x);k&&a(xr(k,{}))},v=["","showErrorPage","navigate","openModal","toast","setState","sequence","parallel"],b=m&&c?Array.isArray(c.params?.actions)?c.params.actions:[]:[];return i.jsxs("div",{"data-testid":`g7le-error-action-${e}`,style:VS,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[i.jsx("select",{"data-testid":`g7le-error-handler-${e}`,value:p,onChange:x=>h(x.target.value),style:{...US,flex:1},children:v.map(x=>i.jsx("option",{value:x,children:r(x===""?"layout_editor.error.handler_none":`layout_editor.error.handler_${x}`)},x))}),p!==""?i.jsx("button",{type:"button","data-testid":`g7le-error-code-${e}`,onClick:()=>d(x=>!x),"aria-label":r("layout_editor.action_list.view_code"),title:r("layout_editor.action_list.view_code"),style:l?qS:uu,children:"</>"}):null]}),m?i.jsx("div",{"data-testid":`g7le-error-actions-${e}`,children:i.jsx(Gt,{actions:b,onChange:x=>a({handler:p,params:{actions:x}}),t:r,recipes:n,candidatePools:s,chipContext:"error",testIdPrefix:`g7le-error-seq-${e}`})}):f&&c?i.jsx(Ts,{raw:c,recipe:f,values:g,t:r,pools:s,onChange:x=>a(x),testIdPrefix:`g7le-error-action-${e}-edit`}):null,l&&c?i.jsx("pre",{"data-testid":`g7le-error-code-view-${e}`,style:GS,children:JSON.stringify(t,null,2)}):null]})}const du={display:"flex",flexDirection:"column",gap:12,minWidth:0},cu={margin:0,fontSize:13,fontWeight:700,color:"#0f172a"},FS={margin:0,fontSize:11,color:"#94a3b8"},KS={margin:0,fontSize:12,color:"#b45309",background:"#fef3c7",padding:"8px 10px",borderRadius:6},VS={display:"flex",flexDirection:"column",gap:6,minWidth:0},US={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},uu={flex:"0 0 auto",width:26,height:26,display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:11,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",fontFamily:"ui-monospace, monospace"},qS={...uu,background:"#0f172a",borderColor:"#0f172a",color:"#e2e8f0"},GS={margin:"4px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:12,overflow:"auto",fontFamily:"ui-monospace, monospace"};function Hr(e){const t=e.trim(),n=/^\{\{([\s\S]*)\}\}$/.exec(t);return n?n[1].trim():t}function YS(e){let t=e;for(;t.length>=2&&t[0]==="("&&t[t.length-1]===")";){let n=0,o=!0;for(let r=0;r<t.length;r++)if(t[r]==="(")n++;else if(t[r]===")"&&(n--,n===0&&r<t.length-1)){o=!1;break}if(!o)break;t=t.slice(1,-1)}return t}function Fr(e){let t=e.replace(/\s+/g,"");for(t=t.replace(/\?\./g,".");t.startsWith("!!");)t=t.slice(2);return t=YS(t),t}function XS(e){if(!e||typeof e!="object")return[];const t=e.operators;if(Array.isArray(t))return t.filter(o=>!!o&&typeof o.value=="string"&&typeof o.expr=="string").map(o=>({value:o.value,label:typeof o.label=="string"?o.label:void 0,params:Array.isArray(o.params)?o.params:[],expr:Hr(o.expr),aliases:pu(o.aliases)}));const n=[];for(const[o,r]of Object.entries(e)){if(o==="comment"||o==="operators"||!r||typeof r!="object")continue;const s=r;typeof s.expr=="string"&&n.push({value:o,label:typeof s.label=="string"?s.label:void 0,params:Array.isArray(s.params)?s.params:[],expr:Hr(s.expr),aliases:pu(s.aliases)})}return n}function pu(e){if(!Array.isArray(e))return[];const t=[];for(const n of e){if(typeof n!="string")continue;const o=Hr(n);o.trim().length>0&&t.push(o)}return t}function JS(e){const t=e.params.length*1e3;return Fr(e.expr).replace(/\{\w+\}/g,"").length-t}function ZS(e){return e.map((t,n)=>({op:t,idx:n,score:JS(t)})).sort((t,n)=>n.score-t.score||t.idx-n.idx).map(t=>t.op)}function QS(e,t){return e.replace(/\{(\w+)\}/g,(n,o)=>{const r=t[o];return r===void 0?n:r})}function RS(e,t){const n=t.find(o=>o.value===e.operator);return n?QS(n.expr,e.params??{}):null}function fu(e,t,n){const o=e.map(a=>RS(a,n)).filter(a=>typeof a=="string"&&a.trim().length>0);if(o.length===0)return"";if(o.length===1)return`{{ ${o[0]} }}`;const r=t==="or"?" || ":" && ";return`{{ ${o.map(a=>`(${a})`).join(r)} }}`}function gu(e,t){const n=Fr(e);for(const o of ZS(t)){if(o.params.length===0){for(const r of[o.expr,...o.aliases])if(Fr(r)===n)return{operator:o.value,params:{}};continue}for(const r of[o.expr,...o.aliases]){const s=eC(o,r,n);if(s)return s}}return null}function eC(e,t,n){const o=t.trim(),r=[];let s="",a=0;for(;a<o.length;){const f=/^\{(\w+)\}/.exec(o.slice(a));if(f)r.push(f[1]),s+="(.+?)",a+=f[0].length;else{let y="";for(;a<o.length&&!/^\{(\w+)\}/.test(o.slice(a));)y+=o[a],a+=1;const g=Fr(y);for(const m of g)s+=m.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}}const d=new RegExp("^"+s+"$").exec(n);if(!d)return null;const c={};for(let f=0;f<r.length;f++){const y=r[f],g=d[f+1];if(y in c&&c[y]!==g)return null;c[y]=g}const p=tC(o);for(const[f,y]of Object.entries(c))if(/&&|\|\|/.test(y)||!p.has(f)&&/===|!==|==|!=|>=|<=|>|<|\?|:|\(|\)|!/.test(y))return null;return{operator:e.value,params:c}}function tC(e){const t=new Set,n=/(['"])\{(\w+)\}\1/g;let o;for(;(o=n.exec(e))!==null;)t.add(o[2]);return t}function nC(e,t){if(typeof e!="string"||e.trim()==="")return null;const n=Hr(e),o=gu(n,t);if(o)return{clauses:[o],combinator:"and"};const r=n.includes(") || (")?"or":(n.includes(") && ("),"and"),s=r==="or"?") || (":") && (";if(!n.startsWith("(")||!n.endsWith(")")||!n.includes(s))return null;const l=n.slice(1,-1).split(s),d=[];for(const c of l){const p=gu(c.trim(),t);if(!p)return null;d.push(p)}return{clauses:d,combinator:r}}const hu=Lt(),Mn=hu.responsiveManager,To=hu.BREAKPOINT_PRESETS;function yu(e){const t=e.groupPrefixes;return{group:typeof e.group=="string"?e.group:void 0,apply:e.apply&&typeof e.apply=="object"?e.apply:void 0,options:Array.isArray(e.options)?e.options:void 0,scale:Array.isArray(e.scale)?e.scale:void 0,groupPrefixes:Array.isArray(t)?t.filter(n=>typeof n=="string"&&n.length>0):void 0,groupTokens:Array.isArray(e.groupTokens)?e.groupTokens.filter(n=>typeof n=="string"&&n.length>0):void 0}}function bu(e){const t={...e??{}},n=t.style&&typeof t.style=="object"&&!Array.isArray(t.style)?{...t.style}:{};return t.style=n,{props:t,style:n}}function Kr(e){return typeof e!="string"?[]:e.split(/\s+/).filter(t=>t.length>0)}function mu(e,t=!1){const n=new Set,o=[],r=s=>{if(s){for(const a of s.tokens??[])n.add(a);s.tokenTemplate&&s.tokenTemplate.includes("{value}")&&o.push(s.tokenTemplate.split("{value}")[0]??"")}};r(e.apply);for(const s of e.options??[])r(s.apply);for(const s of e.groupPrefixes??[])o.push(s);for(const s of e.groupTokens??[])n.add(s);return s=>{if(t){if(!Yt(s))return!1;const a=qs(s);return n.has(a)?!0:o.some(l=>l.length>0&&a.startsWith(l))}return Yt(s)?!1:n.has(s)?!0:o.some(a=>a.length>0&&s.startsWith(a))}}function oC(e,t,n,o,r=!1){const s=mu(t,r),a=Kr(e.className).filter(p=>!s(p)),l=[];if(Array.isArray(n.tokens))l.push(...n.tokens);else if(n.tokenTemplate&&n.tokenTemplate.includes("{value}")&&o!=null){const p=String(o).split(/\s+/).filter(f=>f.length>0);for(const f of p)l.push(n.tokenTemplate.replace("{value}",f))}const d=r?l.map(Su):l,c=[...a,...d];c.length===0?delete e.className:e.className=c.join(" ")}const Ks={url:"backgroundImage",size:"backgroundSize",repeat:"backgroundRepeat",position:"backgroundPosition"};function rC(e){return!e||typeof e!="object"||Array.isArray(e)?!1:Object.keys(Ks).some(t=>t in e)}function iC(e){return/^url\(/i.test(e.trim())?e:`url(${e})`}function vu(e){const t=/^url\(\s*(['"]?)(.*?)\1\s*\)$/i.exec(e.trim());return t?t[2]:e}function sC(e){return e.includes("backgroundImage")}function Vs(e,t){if(e!=null){if(typeof e=="object"){const n=e[t];return typeof n=="string"||typeof n=="number"?n:void 0}return e}}function aC(e){const t=e.backgroundImage,n=Vs(e.backgroundSize,"size"),o=Vs(e.backgroundRepeat,"repeat"),r=Vs(e.backgroundPosition,"position");let s;if(typeof t=="string"?s=vu(t):t&&typeof t=="object"?s=t.url:s=void 0,s===void 0&&n===void 0&&o===void 0&&r===void 0)return;const a={};return s!==void 0&&(a.url=typeof s=="string"?vu(s):s),n!==void 0&&(a.size=n),o!==void 0&&(a.repeat=o),r!==void 0&&(a.position=r),a}function lC(e,t,n){const o=(r,s)=>{s==null||s===""?delete e[r]:e[r]=s};if(Array.isArray(t.props)){if(!t.values&&rC(n)){for(const r of t.props){const s=Object.keys(Ks).find(d=>Ks[d]===r);let a=s?n[s]:void 0;a!==null&&typeof a=="object"&&(a=void 0);const l=r==="backgroundImage"&&typeof a=="string"&&a!==""?iC(a):a;o(r,l)}return}for(const r of t.props){const s=t.values&&r in t.values?t.values[r]:n;o(r,s)}return}typeof t.prop=="string"&&o(t.prop,t.value!==void 0?t.value:n)}function dC(e,t,n){if(typeof t.varName!="string")return;const o=t.value!==void 0?t.value:n;o==null||o===""?delete e[t.varName]:e[t.varName]=o}function cC(e,t,n){if(typeof t.propKey!="string")return;const o=t.value!==void 0?t.value:n;o==null||o===""?delete e[t.propKey]:e[t.propKey]=o}function fn(e,t,n,o=Wt){const r=yu(t),s=o.colorScheme==="dark";let a,l=!1;if(r.options){const y=r.options.find(g=>g.value===n);l=y!==void 0,a=y?.apply??r.apply}else a=r.apply;const d=n==null||n===""||l&&a===void 0,p=(d?r.apply:a)?.type==="classToken"||(r.options?.some(y=>y.apply?.type==="classToken")??!1);if(s&&!p)return e;const f=o.breakpoint!=="base"&&p?(()=>{const y=e.props??{};return typeof y.className=="string"?y.className:void 0})():void 0;return ku(e,o,(y,g)=>{if(f!==void 0&&typeof y.className!="string"&&(y.className=f),d){const m=mu(r,s),h=Kr(y.className).filter(v=>!m(v));if(h.length===0?delete y.className:y.className=h.join(" "),!s&&r.apply){const v=r.apply;if(v.type==="styleProp")for(const b of v.props??(v.prop?[v.prop]:[]))delete g[b];else v.type==="cssVar"&&v.varName?delete g[v.varName]:v.type==="propValue"&&v.propKey&&delete y[v.propKey]}}else if(a)switch(a.type){case"classToken":oC(y,r,a,n,s);break;case"styleProp":lC(g,a,n);break;case"cssVar":dC(g,a,n);break;case"propValue":cC(y,a,n);break}})}function Vr(e,t){const n=Kr(e);return t?n.filter(Yt).map(qs):n.filter(o=>!Yt(o))}function uC(e,t,n,o){const r=new Set(t.filter(l=>typeof l=="string"&&l.length>0)),s=l=>o?Yt(l)?r.has(qs(l)):!1:Yt(l)?!1:r.has(l),a=Kr(e).filter(l=>!s(l));return n&&n.length>0&&a.push(o?Su(n):n),a.join(" ").trim()}function _u(e,t,n,o){if(e.options&&e.options.some(s=>s.apply)){const s=Vr(t.className,o),a=[];for(const l of e.options)l.apply&&fC(l.apply,s,n,t)&&a.push(l.value);if(a.length>0)return{value:a[0],matched:!0,conflict:a.length>1?!0:void 0};if(!e.apply)return{value:void 0,matched:!1}}const r=e.apply;if(!r)return{value:void 0,matched:!1};switch(r.type){case"styleProp":{if(!r.values&&Array.isArray(r.props)&&sC(r.props)){const l=aC(n);return l===void 0?{value:void 0,matched:!1}:{value:l,matched:!0}}const s=r.prop??r.props?.[0];if(!s)return{value:void 0,matched:!1};const a=n[s];return a===void 0?{value:void 0,matched:!1}:{value:a,matched:!0}}case"cssVar":{if(!r.varName)return{value:void 0,matched:!1};const s=n[r.varName];return s===void 0?{value:void 0,matched:!1}:{value:s,matched:!0}}case"propValue":{if(!r.propKey)return{value:void 0,matched:!1};const s=t[r.propKey];return s===void 0?{value:void 0,matched:!1}:{value:s,matched:!0}}case"classToken":{const s=Vr(t.className,o);if(r.tokenTemplate&&r.tokenTemplate.includes("{value}")){const[a,l]=r.tokenTemplate.split("{value}");if((a??"")===""&&(l??"")===""&&e.groupPrefixes?.length){const d=s.filter(c=>e.groupPrefixes.some(p=>c.startsWith(p)));return d.length>0?{value:d.join(" "),matched:!0}:{value:void 0,matched:!1}}for(const d of s)if(d.startsWith(a)&&d.endsWith(l??""))return{value:d.slice(a.length,l?d.length-l.length:void 0),matched:!0}}return Array.isArray(r.tokens)&&r.tokens.every(a=>s.includes(a))?{value:!0,matched:!0}:{value:void 0,matched:!1}}default:return{value:void 0,matched:!1}}}function xu(e){return e.style&&typeof e.style=="object"&&!Array.isArray(e.style)?e.style:{}}function Do(e,t,n=Wt){const o=yu(t),r=n.colorScheme==="dark";if(r&&!Us(o.apply)&&!o.options?.some(m=>Us(m.apply)))return{value:void 0,matched:!1,darkReadonly:!0};const s=mC(e,n),a=xu(s),l=_u(o,s,a,r);if(n.breakpoint==="base")return l;const d=l.matched?l.value:void 0,c=e.props??{},p=xu(c),f=_u(o,c,p,r),y=l.matched&&l.value!==void 0,g=y&&!pC(l.value,f.value);return{value:y?l.value:f.value,matched:g?l.matched:f.matched,conflict:g?l.conflict:f.conflict,scopedValue:g?l.value:void 0,baseFallback:g?void 0:f.value,scopedRawValue:d}}function pC(e,t){if(e===t)return!0;if(e===null||t===null||typeof e!="object"||typeof t!="object")return!1;try{return JSON.stringify(e)===JSON.stringify(t)}catch{return!1}}function fC(e,t,n,o){switch(e.type){case"classToken":return Array.isArray(e.tokens)&&e.tokens.length>0?e.tokens.every(r=>t.includes(r)):!1;case"styleProp":{const r=e.prop??e.props?.[0];if(!r)return!1;const s=e.value;return s!==void 0?n[r]===s:n[r]!==void 0}case"cssVar":return e.varName?n[e.varName]!==void 0:!1;case"propValue":return e.propKey?e.value!==void 0?o[e.propKey]===e.value:o[e.propKey]!==void 0:!1;default:return!1}}const Wt={colorScheme:"base",breakpoint:"base"},No="dark:";function gC(e){return e==="base"?!0:Mn.parseRange(e)!==null}function wu(e){return e in To}function Ur(e,t,n){if(e==="custom"){if(typeof n=="number"){const o=t.responsive;if(o&&typeof o=="object"){const r=Mn.getMatchingKey(o,n);if(r)return r}}return"base"}return e}function hC(e){return!!e&&typeof e=="object"&&Array.isArray(e.children)}function yC(e,t,n){let o,r,s;if(e!=="custom"&&e in To){o=e;const l=To[e];r=l.min,s=l.max}else if(e!=="custom"){const l=Mn.parseRange(e);if(l)o=e,r=l.min,s=l.max;else{const d=typeof n=="number"?n:Mn.getWidth();o=`${d}-${d}`,r=d,s=d}}else{const l=typeof n=="number"?n:Mn.getWidth();r=l,s=l,o=`${l}-${l}`}const a=t.responsive;if(a&&typeof a=="object"){let l=null,d=1/0;for(const c of Object.keys(a)){if(!hC(a[c]))continue;const p=Mn.parseRange(c);if(p){if(p.min===r&&p.max===s)return{mode:"merge",key:c};if(p.min<=r&&p.max>=s){const f=p.max-p.min;f<d&&(d=f,l=c)}}}if(l)return{mode:"separate",key:o,sourceKey:l}}return{mode:"separate",key:o}}function bC(e){switch(e){case"mobile":case"portable":return{i18nKey:"layout_editor.overlay.branch_mobile",raw:e};case"tablet":return{i18nKey:"layout_editor.overlay.branch_tablet",raw:e};case"desktop":return{i18nKey:"layout_editor.overlay.branch_desktop",raw:e};default:return{i18nKey:null,raw:e}}}function mC(e,t){return t.breakpoint==="base"?e.props??{}:e.responsive?.[t.breakpoint]?.props??{}}function ku(e,t,n){if(t.breakpoint==="base"){const{props:c,style:p}=bu(e.props);return n(c,p),Object.keys(p).length===0&&delete c.style,{...e,props:c}}const o=t.breakpoint,r={...e.responsive??{}},s={...r[o]??{}},{props:a,style:l}=bu(s.props);n(a,l),Object.keys(l).length===0&&delete a.style,Object.keys(a).length===0?delete s.props:s.props=a,Object.keys(s).length===0?delete r[o]:r[o]=s;const d={...e};return Object.keys(r).length===0?delete d.responsive:d.responsive=r,d}function vC(e,t){if(t.breakpoint==="base")return e.if;const n=e.responsive?.[t.breakpoint];return n&&typeof n=="object"&&"if"in n?n.if:e.if}function _C(e,t,n){const o=n==null||n==="";if(t.breakpoint==="base"){const d={...e};return o?delete d.if:d.if=n,d}const r=t.breakpoint,s={...e.responsive??{}},a={...s[r]??{}};o?delete a.if:a.if=n,Object.keys(a).length===0?delete s[r]:s[r]=a;const l={...e};return Object.keys(s).length===0?delete l.responsive:l.responsive=s,l}function Us(e){return e?.type==="classToken"}function Yt(e){return e.startsWith(No)}function qs(e){return e.startsWith(No)?e.slice(No.length):e}function Su(e){return e.startsWith(No)?e:`${No}${e}`}function qr(e){return typeof e=="string"?e.split(/\s+/).filter(t=>t.length>0):[]}function xC(e,t){if(t.colorScheme==="dark")return ku(e,t,l=>{const d=qr(l.className).filter(c=>!Yt(c));d.length===0?delete l.className:l.className=d.join(" ")});if(t.breakpoint==="base")return e;const o=t.breakpoint;if(!e.responsive?.[o])return e;const r={...e.responsive},s={...r[o]};delete s.props,Object.keys(s).length===0?delete r[o]:r[o]=s;const a={...e};return Object.keys(r).length===0?delete a.responsive:a.responsive=r,a}function Gs(e,t){if(t.colorScheme==="dark"){const c=t.breakpoint==="base"?e.props:e.responsive?.[t.breakpoint]?.props;return qr(c?.className).some(Yt)}if(t.breakpoint==="base")return!1;const o=e.responsive?.[t.breakpoint];if(!o)return!1;const r=o.props;if(Object.keys(o).filter(c=>c!=="props").length>0)return!0;if(!r)return!1;if(Object.keys(r).filter(c=>c!=="className").length>0)return!0;const l=new Set(qr(e.props?.className).filter(c=>!Yt(c))),d=qr(r.className).filter(c=>!Yt(c));return d.length!==l.size?!0:d.some(c=>!l.has(c))}function Cu(e,t,n=""){return typeof e!="string"?n:e.startsWith("$t:")?t(e.slice(3)):e}function $u(e,t,n){if(n.breakpoint!=="base")return _C(e,n,t.trim());const o={...e};if(o.props&&typeof o.props=="object"&&"if"in o.props){const r={...o.props};delete r.if,o.props=r}return t.trim()===""?delete o.if:o.if=t,o}function Eu({node:e,spec:t,t:n,onPatchNode:o,dataSourceCandidates:r,stateKeyCandidates:s,scope:a=Wt}){const l=u.useMemo(()=>XS(t?.conditionRecipes),[t]),d=a.breakpoint!=="base"?vC(e,a):e.if??e.props?.if,c=u.useMemo(()=>nC(d,l),[d,l]),p=c?.clauses??[],f=c?.combinator??"and",y=typeof d=="string"&&d.trim()!==""&&c===null;if(l.length===0)return i.jsx("div",{"data-testid":"g7le-condition-no-recipes",style:wC,children:n("layout_editor.visibility.no_recipes")});const g=(k,w)=>{o($u(e,fu(k,w,l),a))},m=(k,w)=>{g(p.map((_,C)=>C===k?w:_),f)},h=k=>{g(p.filter((w,_)=>_!==k),f)},v=k=>{const w=l[0];g([...p,{operator:w.value,params:{}}],k)},b=k=>{if(k==="datasource-picker")return r;if(k==="state-key-picker")return s};if(y)return i.jsxs("div",{className:"g7le-condition-advanced","data-testid":"g7le-condition-advanced",style:{display:"flex",flexDirection:"column",gap:8},children:[i.jsx("div",{style:DC,children:n("layout_editor.visibility.advanced_preserved")}),i.jsx("code",{"data-testid":"g7le-condition-advanced-expr",style:Au,children:String(d)}),i.jsx("button",{type:"button","data-testid":"g7le-condition-clear-advanced",onClick:()=>o($u(e,"",a)),style:NC,children:n("layout_editor.visibility.clear")})]});const x=fu(p,f,l);return i.jsxs("div",{className:"g7le-condition-builder","data-testid":"g7le-condition-builder",style:{display:"flex",flexDirection:"column",gap:10},children:[i.jsx("div",{style:kC,children:n("layout_editor.visibility.show_when")}),p.map((k,w)=>{const _=l.find(C=>C.value===k.operator)??l[0];return i.jsxs("div",{className:"g7le-condition-clause","data-testid":`g7le-condition-clause-${w}`,style:SC,children:[i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[w>0&&i.jsx("span",{"data-testid":`g7le-condition-combinator-${w}`,style:$C,children:n(f==="or"?"layout_editor.visibility.or":"layout_editor.visibility.and")}),i.jsx("select",{"data-testid":`g7le-condition-select-${w}`,value:k.operator,onChange:C=>m(w,{operator:C.target.value,params:{}}),style:CC,children:l.map(C=>i.jsx("option",{value:C.value,children:Cu(C.label,n,C.value)},C.value))}),i.jsx("button",{type:"button","data-testid":`g7le-condition-remove-${w}`,"aria-label":n("layout_editor.visibility.remove"),onClick:()=>h(w),style:EC,children:"✕"})]}),_.params.map(C=>{const E=Tn(C.widget);return i.jsxs("div",{className:"g7le-condition-param","data-testid":`g7le-condition-param-${w}-${C.key}`,style:IC,children:[i.jsx("span",{style:AC,children:Cu(C.label,n,C.key)}),i.jsx("div",{style:{flex:1},children:E?i.jsx(E,{control:{...C},value:k.params[C.key],onChange:S=>m(w,{operator:k.operator,params:{...k.params,[C.key]:S===void 0?"":String(S)}}),t:n,candidates:b(C.widget)}):i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:n("layout_editor.property_modal.unsupported_widget")})})]},C.key)})]},w)}),i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsxs("button",{type:"button","data-testid":"g7le-condition-add-and",onClick:()=>v("and"),style:Iu,children:["+ ",n("layout_editor.visibility.and")]}),i.jsxs("button",{type:"button","data-testid":"g7le-condition-add-or",onClick:()=>v("or"),style:Iu,children:["+ ",n("layout_editor.visibility.or")]})]}),x!==""&&i.jsxs("div",{"data-testid":"g7le-condition-preview",style:TC,children:[i.jsx("span",{style:{fontSize:11,color:"#64748b"},children:n("layout_editor.visibility.preview")}),i.jsx("code",{style:Au,children:x})]})]})}const wC={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},kC={fontSize:12,fontWeight:600,color:"#0f172a"},SC={border:"1px solid #e2e8f0",borderRadius:8,padding:10,display:"flex",flexDirection:"column",gap:8,background:"#f8fafc"},CC={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff"},$C={fontSize:11,fontWeight:700,color:"#2563eb",background:"#dbeafe",padding:"2px 8px",borderRadius:999},EC={border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:12},Iu={padding:"5px 12px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},IC={display:"flex",alignItems:"center",gap:8},AC={fontSize:11,color:"#64748b",minWidth:64},TC={display:"flex",flexDirection:"column",gap:4,padding:10,background:"#f1f5f9",borderRadius:6},Au={fontSize:11,color:"#0f172a",fontFamily:"monospace",wordBreak:"break-all"},DC={fontSize:11,color:"#92400e",background:"#fef3c7",padding:"6px 8px",borderRadius:6},NC={padding:"5px 12px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer",alignSelf:"flex-start"};function zC(e){return typeof e=="string"?e:e===void 0?"":JSON.stringify(e)}function MC(e){const t=e.trim();if(t==="")return"";if(/^-?\d+(\.\d+)?$/.test(t)||t==="true"||t==="false"||t==="null")try{return JSON.parse(t)}catch{return e}return e}function Tu(e){return!e||typeof e!="object"||Array.isArray(e)?[]:Object.entries(e).map(([t,n])=>({key:t,value:zC(n)}))}function Ys(e){const t={};for(const n of e){const o=n.key.trim();o!==""&&(t[o]=MC(n.value))}return t}function Du(e){return!e||typeof e!="object"||Array.isArray(e)?!1:Object.values(e).some(t=>t!==null&&typeof t=="object")}function LC(e){const t={};return typeof e.if=="string"&&e.if.trim().length>0&&(t.if=e.if),t}function PC(e,t){const n={...t},o=e.if;return typeof o=="string"&&o.trim().length>0?n.if=o:delete n.if,n}const OC=["api","static","route_params","query_params","websocket"],BC=["GET","POST","PUT","PATCH","DELETE"],WC=["none","optional","required"],jC=["progressive","blocking","background"],HC=["application/json","multipart/form-data"],FC=["public","private","presence"];function KC(e){if(!e||typeof e!="object")return{own:[],inherited:[]};const t=e.__editor,n=t&&typeof t=="object"&&!Array.isArray(t)?t.original:void 0,o=n&&typeof n=="object"&&!Array.isArray(n)?n.data_sources:e.data_sources,r=Array.isArray(o)?o.filter(c=>c&&typeof c=="object"):[],s=new Set(r.map(c=>typeof c.id=="string"?c.id:"").filter(Boolean)),a=e.data_sources,d=(Array.isArray(a)?a.filter(c=>c&&typeof c=="object"):[]).filter(c=>typeof c.id=="string"&&!s.has(c.id));return{own:r,inherited:d}}function VC(e,t){const n=(o,r="")=>typeof o=="string"?o:r;return{index:t,id:n(e.id),type:n(e.type,"api"),label_key:n(e.label_key),endpoint:n(e.endpoint),method:n(e.method,"GET"),auth_mode:n(e.auth_mode,"none"),loading_strategy:n(e.loading_strategy,"progressive"),auto_fetch:e.auto_fetch!==!1,paramRows:Tu(e.params),paramsRaw:Du(e.params),paramsRawValue:e.params&&typeof e.params=="object"&&!Array.isArray(e.params)?e.params:{},paramsRawValid:!0,fallbackEnabled:e.fallback!==void 0,fallbackValue:e.fallback,if:n(e.if),contentType:n(e.contentType),refetchOnMount:e.refetchOnMount===!0,onSuccess:Array.isArray(e.onSuccess)?e.onSuccess.filter(o=>!!o&&typeof o=="object"):[],onError:Array.isArray(e.onError)?e.onError.filter(o=>!!o&&typeof o=="object"):[],errorHandling:e.errorHandling&&typeof e.errorHandling=="object"&&!Array.isArray(e.errorHandling)?e.errorHandling:{},channel:n(e.channel),event:n(e.event),channel_type:n(e.channel_type,"public"),target_source:n(e.target_source),onReceive:Array.isArray(e.onReceive)?e.onReceive.filter(o=>!!o&&typeof o=="object"):[]}}function UC(){return{index:-1,id:"",type:"api",label_key:"",endpoint:"",method:"GET",auth_mode:"none",loading_strategy:"progressive",auto_fetch:!0,paramRows:[],paramsRaw:!1,paramsRawValue:{},paramsRawValid:!0,fallbackEnabled:!1,fallbackValue:void 0,if:"",contentType:"",refetchOnMount:!1,onSuccess:[],onError:[],errorHandling:{},channel:"",event:"",channel_type:"public",target_source:"",onReceive:[]}}function qC(e,t,n,o){const r=e.id.trim();if(!r)return{error:o("layout_editor.data_sources.error.id_required")};if(!/^[A-Za-z_][\w]*$/.test(r))return{error:o("layout_editor.data_sources.error.id_invalid")};if(n.has(r))return{error:o("layout_editor.data_sources.error.id_duplicate",{id:r})};const s=t?{...t}:{};if(s.id=r,s.type=e.type,e.label_key.trim()?s.label_key=e.label_key.trim():delete s.label_key,e.type==="api"||e.type==="websocket"?(e.endpoint.trim()?s.endpoint=e.endpoint.trim():delete s.endpoint,s.method=e.method,s.auth_mode=e.auth_mode,s.loading_strategy=e.loading_strategy):(delete s.endpoint,delete s.method),s.auto_fetch=e.auto_fetch,e.paramsRaw){if(!e.paramsRawValid)return{error:o("layout_editor.data_sources.error.params_json")};const a=e.paramsRawValue;a&&typeof a=="object"&&Object.keys(a).length>0?s.params=a:delete s.params}else{const a=Ys(e.paramRows);Object.keys(a).length>0?s.params=a:delete s.params}return e.fallbackEnabled?s.fallback=e.fallbackValue===void 0?{}:e.fallbackValue:delete s.fallback,e.if.trim()?s.if=e.if.trim():delete s.if,e.type==="api"&&e.contentType?s.contentType=e.contentType:delete s.contentType,e.refetchOnMount?s.refetchOnMount=!0:delete s.refetchOnMount,e.onSuccess.length>0?s.onSuccess=e.onSuccess:delete s.onSuccess,e.onError.length>0?s.onError=e.onError:delete s.onError,Object.keys(e.errorHandling).length>0?s.errorHandling=e.errorHandling:delete s.errorHandling,e.type==="websocket"?(e.channel?s.channel=e.channel:delete s.channel,e.event?s.event=e.event:delete s.event,s.channel_type=e.channel_type,e.target_source?s.target_source=e.target_source:delete s.target_source,e.onReceive.length>0?s.onReceive=e.onReceive:delete s.onReceive):(delete s.channel,delete s.event,delete s.channel_type,delete s.target_source,delete s.onReceive),{entry:s}}const jt={width:"100%",padding:"6px 8px",fontSize:13,border:"1px solid #cbd5e1",borderRadius:6,outline:"none",boxSizing:"border-box",fontFamily:"inherit",color:"#0f172a",background:"#ffffff"},ct={display:"block",fontSize:12,fontWeight:600,color:"#475569",marginBottom:4},ut={marginBottom:12},rn={padding:"6px 12px",fontSize:13,fontWeight:500,borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",cursor:"pointer"},Nu={...rn,background:"#2563eb",borderColor:"#2563eb",color:"#ffffff",fontWeight:600},zo={marginTop:12,padding:10,border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc"},Mo={fontSize:12,fontWeight:700,color:"#475569",marginBottom:8},GC={margin:"0 0 6px",fontSize:11,color:"#94a3b8"},zu={marginTop:8},Mu={alignSelf:"flex-start",padding:"2px 0",fontSize:11,border:"none",background:"none",color:"#64748b",cursor:"pointer"},Lu={margin:0,padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:11,lineHeight:1.5,overflow:"auto",fontFamily:"ui-monospace, monospace"};function YC({code:e,action:t,recipes:n,pools:o,t:r,onPatch:s}){const[a,l]=u.useState(!1),d=u.useMemo(()=>wo(n),[n]),c=Array.isArray(t)?t[0]:t,p=typeof c?.handler=="string"?c.handler:"",f=d.find(x=>x.id===p)??null,y=c?Yn(c,d):null,g=y?.kind==="preset"?y.values:{},m=p==="sequence"||p==="parallel",h=["","showErrorPage","navigate","openModal","toast","setState","sequence","parallel"],v=x=>{if(x===""){s({});return}const k=d.find(w=>w.id===x);s(k?xr(k,{}):{handler:x})},b=m&&c?Array.isArray(c.params?.actions)?c.params.actions:[]:[];return i.jsxs("div",{"data-testid":`g7le-ds-errorhandling-${e}`,style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[i.jsx("select",{"data-testid":`g7le-ds-eh-handler-${e}`,style:{...jt,flex:1},value:p,onChange:x=>v(x.target.value),children:h.map(x=>i.jsx("option",{value:x,children:r(x===""?"layout_editor.error.handler_none":`layout_editor.error.handler_${x}`)},x))}),p!==""?i.jsx("button",{type:"button","data-testid":`g7le-ds-eh-code-${e}`,onClick:()=>l(x=>!x),"aria-label":r("layout_editor.action_list.view_code"),title:r("layout_editor.action_list.view_code"),style:a?XC:Pu,children:"</>"}):null]}),m?i.jsx("div",{"data-testid":`g7le-ds-eh-actions-${e}`,children:i.jsx(Gt,{actions:b,onChange:x=>s({handler:p,params:{actions:x}}),t:r,recipes:n,candidatePools:o,chipContext:"error",testIdPrefix:`g7le-ds-eh-seq-${e}`})}):f&&c?i.jsx(Ts,{raw:c,recipe:f,values:g,t:r,pools:o,onChange:x=>s(x),testIdPrefix:`g7le-ds-eh-action-${e}-edit`}):null,a&&c?i.jsx("pre",{"data-testid":`g7le-ds-eh-code-view-${e}`,style:JC,children:JSON.stringify(t,null,2)}):null]})}const Pu={flex:"0 0 auto",width:26,height:26,display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:11,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",fontFamily:"ui-monospace, monospace"},XC={...Pu,background:"#0f172a",borderColor:"#0f172a",color:"#e2e8f0"},JC={margin:"4px 0 0",padding:8,background:"#0f172a",color:"#e2e8f0",borderRadius:6,fontSize:12,overflow:"auto",fontFamily:"ui-monospace, monospace"};function ZC({raw:e,onChange:t,t:n,resolveLabel:o,onClose:r,actionRecipes:s,errorRecipes:a,conditionSpec:l,bindingCandidates:d,pageCandidates:c,dataSourceCandidates:p,stateKeyCandidates:f}){const y=o??n,g=u.useMemo(()=>KC(e),[e]),m=g.inherited,[h,v]=u.useState(g.own),[b,x]=u.useState(null),[k,w]=u.useState(null),[_,C]=u.useState(!1),[E,S]=u.useState(!1),[T,$]=u.useState(()=>new Set),A=u.useCallback(I=>{$(P=>{const F=new Set(P);return F.has(I)?F.delete(I):F.add(I),F})},[]),z=u.useMemo(()=>({pageCandidates:c,dataSourceCandidates:p,stateKeyCandidates:f,bindingCandidates:d}),[c,p,f,d]),D=u.useCallback(I=>{v(I),t([...m,...I],I)},[m,t]),O=u.useCallback(()=>{w(null),x(UC())},[]),M=u.useCallback((I,P)=>{w(null),x(VC(I,P))},[]),B=u.useCallback(I=>{const P=h.filter((F,G)=>G!==I);D(P)},[h,D]),N=u.useCallback(()=>{x(null),w(null)},[]),H=u.useCallback(()=>{if(!b)return;const I=new Set;h.forEach((V,Y)=>{Y!==b.index&&typeof V.id=="string"&&I.add(V.id)}),m.forEach(V=>{typeof V.id=="string"&&I.add(V.id)});const P=b.index>=0?h[b.index]??null:null,F=qC(b,P,I,n);if("error"in F){w(F.error);return}const G=b.index>=0?h.map((V,Y)=>Y===b.index?F.entry:V):[...h,F.entry];D(G),x(null),w(null)},[b,h,m,D,n]),L=b?.type==="api"||b?.type==="websocket";return i.jsxs("div",{className:"g7le-data-sources","data-testid":"g7le-data-sources-panel",style:{flex:"1 1 auto",minHeight:0,display:"flex",flexDirection:"column",fontSize:13,color:"#0f172a",boxSizing:"border-box"},children:[i.jsxs("div",{className:"g7le-data-sources__header",style:{flex:"0 0 auto",padding:"16px 16px 12px",borderBottom:"1px solid #e2e8f0"},children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:8},children:[i.jsx("h2",{style:{margin:0,fontSize:16,fontWeight:700},children:n("layout_editor.data_sources.title")}),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-close",onClick:r,"aria-label":n("layout_editor.data_sources.close"),style:{...rn,padding:"4px 10px"},children:"✕"})]}),i.jsx("p",{style:{margin:0,fontSize:12,color:"#64748b",lineHeight:1.5},children:n("layout_editor.data_sources.description")})]}),i.jsxs("div",{className:"g7le-data-sources__body",style:{flex:"1 1 auto",minHeight:0,overflowY:"auto",padding:16},children:[b&&i.jsxs("div",{className:"g7le-data-sources__form","data-testid":"g7le-data-sources-form",children:[i.jsx("div",{style:{fontSize:13,fontWeight:700,marginBottom:12},children:b.index>=0?n("layout_editor.data_sources.form.edit_title"):n("layout_editor.data_sources.form.add_title")}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-id",children:n("layout_editor.data_sources.field.id")}),i.jsx("input",{id:"g7le-ds-id","data-testid":"g7le-data-sources-field-id",style:jt,value:b.id,onChange:I=>x({...b,id:I.target.value}),placeholder:"products"})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-label",children:n("layout_editor.data_sources.field.label_key")}),i.jsx(vt,{value:b.label_key,onChange:I=>x({...b,label_key:I??""}),t:n,placeholder:n("layout_editor.data_sources.field.label_key_placeholder"),testidPrefix:"g7le-data-sources-field-label",candidates:d,enableExpressionTree:!0,expressionTreeCollapsible:!0}),i.jsx("div",{style:{fontSize:11,color:"#94a3b8",marginTop:4},children:n("layout_editor.data_sources.field.label_key_hint")})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-type",children:n("layout_editor.data_sources.field.type")}),i.jsx("select",{id:"g7le-ds-type","data-testid":"g7le-data-sources-field-type",style:jt,value:b.type,onChange:I=>x({...b,type:I.target.value}),children:OC.map(I=>i.jsx("option",{value:I,children:n(`layout_editor.data_sources.ds_type.${I}`)},I))})]}),L&&i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,children:n("layout_editor.data_sources.field.endpoint")}),i.jsx("div",{"data-testid":"g7le-data-sources-field-endpoint",children:i.jsx(Ve,{value:b.endpoint,onChange:I=>x({...b,endpoint:I}),t:n,candidates:d,placeholder:"/api/shop/products/{{route.id}}",testidPrefix:"g7le-ds-endpoint"})})]}),i.jsxs("div",{style:{display:"flex",gap:12},children:[i.jsxs("div",{style:{...ut,flex:1},children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-method",children:n("layout_editor.data_sources.field.method")}),i.jsx("select",{id:"g7le-ds-method","data-testid":"g7le-data-sources-field-method",style:jt,value:b.method,onChange:I=>x({...b,method:I.target.value}),children:BC.map(I=>i.jsx("option",{value:I,children:I},I))})]}),i.jsxs("div",{style:{...ut,flex:1},children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-auth",children:n("layout_editor.data_sources.field.auth_mode")}),i.jsx("select",{id:"g7le-ds-auth","data-testid":"g7le-data-sources-field-auth",style:jt,value:b.auth_mode,onChange:I=>x({...b,auth_mode:I.target.value}),children:WC.map(I=>i.jsx("option",{value:I,children:n(`layout_editor.data_sources.auth_mode.${I}`)},I))})]})]}),i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-loading",children:n("layout_editor.data_sources.field.loading_strategy")}),i.jsx("select",{id:"g7le-ds-loading","data-testid":"g7le-data-sources-field-loading",style:jt,value:b.loading_strategy,onChange:I=>x({...b,loading_strategy:I.target.value}),children:jC.map(I=>i.jsx("option",{value:I,children:n(`layout_editor.data_sources.loading_strategy.${I}`)},I))})]}),b.type==="api"&&i.jsxs("div",{style:ut,children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-contenttype",children:n("layout_editor.data_sources.field.content_type")}),i.jsx("select",{id:"g7le-ds-contenttype","data-testid":"g7le-ds-contenttype",style:jt,value:b.contentType||"application/json",onChange:I=>x({...b,contentType:I.target.value}),children:HC.map(I=>i.jsx("option",{value:I,children:I},I))})]})]}),i.jsx("div",{style:ut,children:i.jsxs("label",{style:{...ct,display:"flex",alignItems:"center",gap:8,cursor:"pointer"},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-data-sources-field-autofetch",checked:b.auto_fetch,onChange:I=>x({...b,auto_fetch:I.target.checked})}),n("layout_editor.data_sources.field.auto_fetch")]})}),i.jsx("div",{style:ut,children:i.jsxs("label",{style:{...ct,display:"flex",alignItems:"center",gap:8,cursor:"pointer"},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-ds-refetch",checked:b.refetchOnMount,onChange:I=>x({...b,refetchOnMount:I.target.checked})}),n("layout_editor.data_sources.field.refetch_on_mount")]})}),i.jsxs("div",{style:ut,children:[i.jsxs("div",{style:{display:"flex",alignItems:"center",justifyContent:"space-between"},children:[i.jsx("label",{style:ct,htmlFor:"g7le-ds-params",children:n("layout_editor.data_sources.field.params")}),i.jsx("button",{type:"button","data-testid":"g7le-ds-params-mode-toggle",onClick:()=>{if(b.paramsRaw){if(b.paramsRawValid){const I=b.paramsRawValue;I&&typeof I=="object"&&!Array.isArray(I)&&!Du(I)&&x({...b,paramsRaw:!1,paramRows:Tu(I)})}}else{const I=Ys(b.paramRows);x({...b,paramsRaw:!0,paramsRawValue:I,paramsRawValid:!0})}},style:{...rn,padding:"2px 8px",fontSize:11},children:b.paramsRaw?n("layout_editor.data_sources.field.params_mode_block"):n("layout_editor.data_sources.field.params_mode_code")})]}),b.paramsRaw?i.jsx(Bs,{id:"g7le-ds-params",value:b.paramsRawValue,shape:"object",emptyValue:{},minHeight:64,testidPrefix:"g7le-data-sources-field-params",placeholder:`{ "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.jsx(Gt,{actions:b.onSuccess,onChange:I=>x({...b,onSuccess:I}),t:n,recipes:s,candidatePools:z,chipContext:"response",actionChipCandidates:l?.actionChipCandidates??null,testIdPrefix:"g7le-ds-onsuccess-list"})})]}),i.jsxs("div",{style:zo,children:[i.jsx("div",{style:Mo,children:n("layout_editor.data_sources.section.on_error")}),i.jsx("div",{"data-testid":"g7le-ds-onerror",children:i.jsx(Gt,{actions:b.onError,onChange:I=>x({...b,onError:I}),t:n,recipes:s,candidatePools:z,chipContext:"error",actionChipCandidates:l?.actionChipCandidates??null,testIdPrefix:"g7le-ds-onerror-list"})})]}),i.jsxs("div",{style:zo,children:[i.jsx("div",{style:Mo,children:n("layout_editor.data_sources.section.error_handling")}),i.jsxs("div",{"data-testid":"g7le-ds-errorhandling",children:[i.jsx(Fs,{value:b.errorHandling,onChange:I=>x({...b,errorHandling:I}),t:n,mode:"local",testIdPrefix:"g7le-ds-eh",renderActionList:(I,P,F)=>i.jsx(YC,{code:I,action:P,recipes:a,pools:z,t:n,onPatch:F})}),i.jsxs("button",{type:"button","data-testid":"g7le-ds-errorhandling-add",onClick:()=>{const I=Object.keys(b.errorHandling),P=["403","404","500","422","default"].find(F=>!I.includes(F))??"default";x({...b,errorHandling:{...b.errorHandling,[P]:{handler:"toast"}}})},style:{...rn,padding:"4px 10px",marginTop:6},children:["+ ",n("layout_editor.data_sources.add_error_code")]})]})]}),i.jsxs("div",{style:zo,children:[i.jsx("div",{style:Mo,children:n("layout_editor.data_sources.section.load_condition")}),i.jsx("div",{"data-testid":"g7le-ds-loadcondition",children:i.jsx(Eu,{node:LC({if:b.if||void 0}),spec:l??null,t:n,dataSourceCandidates:p,stateKeyCandidates:f,onPatchNode:I=>{const P=PC(I,{});x({...b,if:typeof P.if=="string"?P.if:""})}})})]})]}),!b&&i.jsxs("button",{type:"button","data-testid":"g7le-data-sources-add",onClick:O,style:{...Nu,marginBottom:16},children:["+ ",n("layout_editor.data_sources.add")]}),i.jsxs("div",{className:"g7le-data-sources__list","data-testid":"g7le-data-sources-list",style:{display:"flex",flexDirection:"column",gap:8},children:[h.length===0&&!b&&i.jsx("div",{"data-testid":"g7le-data-sources-empty",style:{fontSize:12,color:"#94a3b8",padding:"12px 0",textAlign:"center"},children:n("layout_editor.data_sources.empty")}),h.map((I,P)=>{const F=typeof I.id=="string"?I.id:`#${P}`,G=typeof I.endpoint=="string"?I.endpoint:"",V=typeof I.label_key=="string"?I.label_key:"",Y=rs(I,y);return i.jsxs("div",{"data-testid":"g7le-data-sources-item","data-ds-id":F,style:{border:"1px solid #e2e8f0",borderRadius:8,padding:"10px 12px",display:"flex",alignItems:"center",gap:8,background:"#ffffff"},children:[i.jsxs("div",{style:{flex:1,minWidth:0},children:[i.jsxs("div",{"data-testid":"g7le-data-sources-item-title",style:{fontWeight:600,fontSize:13,display:"flex",alignItems:"baseline",gap:6,flexWrap:"wrap"},children:[i.jsx("span",{children:Y??F}),Y&&i.jsx("span",{"data-testid":"g7le-data-sources-item-id",style:{fontSize:11,fontWeight:400,color:"#94a3b8"},children:F}),(()=>{const Z=is(I,n);return Z?i.jsx("span",{"data-testid":"g7le-data-sources-ext-badge",style:{fontSize:10,fontWeight:600,color:"#7c3aed",background:"#f5f3ff",border:"1px solid #ddd6fe",borderRadius:4,padding:"1px 6px"},children:Z}):null})()]}),G&&i.jsx("div",{style:{fontSize:11,color:"#94a3b8",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:G}),!V&&i.jsx("div",{"data-testid":"g7le-data-sources-nolabel",style:{fontSize:11,color:"#f59e0b"},children:n("layout_editor.data_sources.no_label")})]}),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-edit",onClick:()=>M(I,P),style:{...rn,padding:"4px 10px"},children:n("layout_editor.data_sources.edit")}),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-remove",onClick:()=>B(P),style:{...rn,padding:"4px 10px",color:"#dc2626",borderColor:"#fecaca"},children:n("layout_editor.data_sources.remove")})]},`${F}-${P}`)})]}),m.length>0&&i.jsxs("div",{style:{marginTop:16},children:[i.jsx("div",{style:{fontSize:12,fontWeight:600,color:"#64748b",textTransform:"uppercase",letterSpacing:.4,marginBottom:8},children:n("layout_editor.data_sources.inherited_title")}),i.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6},children:m.map((I,P)=>{const F=typeof I.id=="string"?I.id:`#${P}`,G=T.has(F),V=[],Y=(Z,se)=>{const re=I[Z];re!=null&&V.push([se,typeof re=="object"?JSON.stringify(re):String(re)])};return Y("type",n("layout_editor.data_sources.field.type")),Y("endpoint",n("layout_editor.data_sources.field.endpoint")),Y("method",n("layout_editor.data_sources.field.method")),Y("auth_mode",n("layout_editor.data_sources.field.auth_mode")),Y("loading_strategy",n("layout_editor.data_sources.field.loading_strategy")),Y("auto_fetch",n("layout_editor.data_sources.field.auto_fetch")),Y("params",n("layout_editor.data_sources.field.params")),i.jsxs("div",{"data-testid":"g7le-data-sources-inherited-item","data-ds-id":F,"data-expanded":G?"true":"false",style:{border:"1px dashed #e2e8f0",borderRadius:6,fontSize:12,color:"#64748b",background:"#f8fafc",overflow:"hidden"},children:[i.jsxs("button",{type:"button","data-testid":"g7le-data-sources-inherited-toggle",onClick:()=>A(F),"aria-expanded":G,style:{width:"100%",display:"flex",alignItems:"center",gap:8,padding:"8px 12px",background:"transparent",border:"none",cursor:"pointer",color:"inherit",fontSize:12,textAlign:"left",outline:"none"},children:[i.jsx("span",{style:{flex:"0 0 auto",width:12,transition:"transform 120ms",transform:G?"rotate(90deg)":"none"},children:"▶"}),(()=>{const Z=rs(I,y),se=is(I,n);return i.jsxs("span",{style:{flex:1,display:"inline-flex",alignItems:"baseline",gap:6,flexWrap:"wrap"},children:[i.jsx("span",{style:{fontWeight:600},children:Z??F}),Z&&i.jsx("span",{style:{fontSize:11,fontWeight:400,color:"#94a3b8"},children:F}),se&&i.jsx("span",{"data-testid":"g7le-data-sources-ext-badge",style:{fontSize:10,fontWeight:600,color:"#7c3aed",background:"#f5f3ff",border:"1px solid #ddd6fe",borderRadius:4,padding:"1px 6px"},children:se})]})})()]}),G&&i.jsxs("div",{"data-testid":"g7le-data-sources-inherited-info",style:{padding:"0 12px 10px 32px",display:"flex",flexDirection:"column",gap:3},children:[V.length===0&&i.jsx("span",{style:{color:"#94a3b8"},children:"—"}),V.map(([Z,se])=>i.jsxs("div",{style:{display:"flex",gap:6},children:[i.jsxs("span",{style:{flex:"0 0 auto",color:"#94a3b8"},children:[Z,":"]}),i.jsx("span",{style:{flex:1,minWidth:0,wordBreak:"break-all",color:"#475569"},children:se})]},Z))]})]},`inh-${F}-${P}`)})})]})]}),b&&i.jsxs("div",{className:"g7le-data-sources__footer",style:{flex:"0 0 auto",padding:"12px 16px",borderTop:"1px solid #e2e8f0",background:"#ffffff"},children:[k&&i.jsx("div",{"data-testid":"g7le-data-sources-form-error",role:"alert",style:{fontSize:12,color:"#dc2626",marginBottom:10},children:k}),i.jsxs("div",{style:{display:"flex",gap:8,justifyContent:"flex-end"},children:[i.jsx("button",{type:"button","data-testid":"g7le-data-sources-form-cancel",onClick:N,style:rn,children:n("layout_editor.data_sources.form.cancel")}),i.jsx("button",{type:"button","data-testid":"g7le-data-sources-form-submit",onClick:H,style:Nu,children:n("layout_editor.data_sources.form.save")})]})]})]})}function QC(e){const t=e?.globalHeaders;return Array.isArray(t)?t:[]}function RC(e){const t=e?.scripts;return Array.isArray(t)?t:[]}function e$(e){const t=QC(e.raw),n=RC(e.raw);return i.jsxs("div",{className:"g7le-data-source-tab","data-testid":"g7le-data-source-tab",style:t$,children:[i.jsx(ZC,{...e}),i.jsxs("div",{style:Ou,"data-testid":"g7le-global-headers",children:[i.jsx("div",{style:Bu,children:e.t("layout_editor.data_sources.global_headers")}),t.length===0?i.jsx("p",{"data-testid":"g7le-global-header-empty",style:Ku,children:e.t("layout_editor.data_sources.global_headers_empty")}):t.map((o,r)=>i.jsxs("div",{"data-testid":`g7le-global-header-${r}`,style:Wu,children:[i.jsx("code",{style:ju,children:o.pattern??"*"}),i.jsx("span",{style:n$,children:"→"}),i.jsx("span",{style:Hu,children:Object.keys(o.headers??{}).join(", ")})]},r)),i.jsxs("p",{style:Fu,children:["ⓘ ",e.t("layout_editor.data_sources.global_headers_hint")]})]}),i.jsxs("div",{style:Ou,"data-testid":"g7le-scripts",children:[i.jsx("div",{style:Bu,children:e.t("layout_editor.data_sources.scripts")}),n.length===0?i.jsx("p",{"data-testid":"g7le-script-empty",style:Ku,children:e.t("layout_editor.data_sources.scripts_empty")}):n.map((o,r)=>i.jsxs("div",{"data-testid":`g7le-script-${o.id??r}`,style:Wu,children:[i.jsx("code",{style:ju,children:o.id??`#${r}`}),i.jsx("span",{style:Hu,children:o.src??""}),o.loading?i.jsx("span",{style:o$,children:o.loading}):null]},o.id??r)),i.jsxs("p",{style:Fu,children:["ⓘ ",e.t("layout_editor.data_sources.scripts_hint")]})]})]})}const t$={display:"flex",flexDirection:"column",gap:16,minWidth:0},Ou={borderTop:"1px solid #e2e8f0",paddingTop:12,display:"flex",flexDirection:"column",gap:6},Bu={fontSize:12,fontWeight:700,color:"#475569"},Wu={display:"flex",alignItems:"center",gap:8,fontSize:12,padding:"4px 8px",border:"1px solid #f1f5f9",borderRadius:6},ju={fontSize:11,color:"#0f172a",fontFamily:"monospace"},n$={color:"#94a3b8"},Hu={flex:1,minWidth:0,color:"#475569",wordBreak:"break-all"},o$={fontSize:10,color:"#64748b",background:"#f1f5f9",borderRadius:4,padding:"0 4px"},Fu={margin:0,fontSize:11,color:"#94a3b8"},Ku={margin:0,fontSize:12,color:"#94a3b8"},r$=["meta","seo","init","overlay","computed","state","error","data"],i$=["401","403","404","500","503"],s$={meta:"layout_editor.page_settings.tab.meta",seo:"layout_editor.page_settings.tab.seo",init:"layout_editor.page_settings.tab.init",overlay:"layout_editor.page_settings.tab.overlay",computed:"layout_editor.page_settings.tab.computed",state:"layout_editor.page_settings.tab.state",error:"layout_editor.page_settings.tab.error",data:"layout_editor.page_settings.tab.data"};async function a$(e){if(typeof fetch!="function"||!e)return[];try{const t=await fetch(en(`/api/admin/templates/${encodeURIComponent(e)}/editor/seo-candidates`,"json"),{credentials:"same-origin",headers:je()});if(!t.ok)return[];const o=(await t.json().catch(()=>null))?.data?.extensions;if(!Array.isArray(o))return[];const r=[];for(const s of o)if(s&&typeof s=="object"){const a=s.type,l=s.id,d=s.label;typeof a=="string"&&typeof l=="string"&&l.length>0&&r.push({type:a,id:l,name:typeof d=="string"?d:l})}return r}catch{return[]}}function l$(e,t){let n=0;for(const o of e)Yn(o,t).kind==="advanced"&&(n+=1);return n}function d$(e,t){let n=0;for(const o of Object.keys(e))Ed(e[o],t).kind==="advanced"&&(n+=1);return n}function c$(e,t,n){const o=e("meta",{})??{},r=o.title;if(typeof r=="string"&&r.length>0){if(r.startsWith("$t:")){const a=n(r.slice(3));if(a&&a!==r.slice(3))return a}else if(!/\{\{[\s\S]*\}\}/.test(r))return r}const s=o.editor_label;return typeof s=="string"&&s.length>0?s.startsWith("$t:")?n(s.slice(3)):s:t||""}function u$({templateIdentifier:e,spec:t,t:n,resolveLabel:o,permissionCandidates:r,onClose:s,initialTab:a="meta",extensionsFetcher:l}){const{state:d}=Je(),{raw:c,getValue:p,patch:f}=zm(),[y,g]=u.useState(a),m=d.selectedRoute?.path??"",h=u.useMemo(()=>zd(d.routeTree,o),[d.routeTree,o]),v=u.useMemo(()=>Md(c),[c]),b=u.useMemo(()=>Ld(t),[t]),x=u.useMemo(()=>Qm(c,o),[c,o]),k=Nd({raw:c,spec:t}),w=u.useMemo(()=>tb(t),[t]),_=u.useMemo(()=>nb(t),[t]),C=u.useMemo(()=>ob(t),[t]),E=u.useMemo(()=>rb(t),[t]),S=u.useMemo(()=>wo(w),[w]),T=u.useMemo(()=>xd(_),[_]),$=p("init_actions",void 0)??p("initActions",[]),A=p("computed",{}),z=p("__computedSource",{}),D=p("errorHandling",{}),O=p("transition_overlay",void 0),M=(p("__editor",{})??{}).original,B=u.useMemo(()=>{if(M===void 0)return;const U=M.transition_overlay;return U&&typeof U=="object"&&!Array.isArray(U)?U:{}},[M]),N=u.useMemo(()=>{const U=O;if(!U||typeof U!="object"||Array.isArray(U))return;const ee=B??{},fe={};for(const[K,ne]of Object.entries(U))K in ee||(fe[K]=ne);return Object.keys(fe).length>0?fe:void 0},[O,B]),[H,L]=u.useState([]);u.useEffect(()=>{let U=!1;const ee=l??a$;return(async()=>{const fe=await ee(e);U||L(fe)})(),()=>{U=!0}},[e,l]);const I=(p("meta",{})??{}).seo?.page_type,P=Jm({raw:c,spec:t,pageType:typeof I=="string"?I:null}),F=u.useMemo(()=>Xm(c,t),[c,t]),G=u.useMemo(()=>({meta:0,seo:0,init:l$($,S),overlay:0,computed:d$(A,T),state:0,error:0,data:0}),[$,S,A,T]),V=u.useMemo(()=>Object.values(G).reduce((U,ee)=>U+ee,0),[G]),Y=u.useMemo(()=>Zm(c,o,n),[c,o,n]),Z=u.useCallback(U=>{const fe={...M?.computed??{}};delete fe[U];const K={...A};delete K[U],f("computed",K,Object.keys(fe).length===0?void 0:fe)},[M,A,f]),se=c$(p,m,n),re=()=>{switch(y){case"meta":return i.jsx(yx,{getValue:p,patch:f,t:n,fieldT:o,spec:t,permissionCandidates:r,bindingCandidates:k});case"seo":return i.jsx(nw,{getValue:p,patch:f,t:n,templateIdentifier:e,candidates:P,availableExtensions:H,dataSourceOptions:Y,previewUrl:m||"/",seedContext:F});case"init":return i.jsx(wk,{actions:$,onChange:U=>f("init_actions",U),recipes:w,t:o,bindingCandidates:k,pageCandidates:h,dataSourceCandidates:v,stateKeyCandidates:b,modalCandidates:x});case"overlay":return i.jsx(Uk,{value:O,ownValue:B,baseValue:N,patch:U=>f("transition_overlay",U),t:o,loadingComponents:E,progressiveDataSources:Y,bindingCandidates:k});case"computed":return i.jsx(z1,{computed:A,onChange:U=>f("computed",U),recipes:_,t:o,sampleContext:F,dataSourceCandidates:v,bindingCandidates:k,computedSource:z,onRevert:Z});case"state":return i.jsx(gS,{raw:c,own:M?{initLocal:M.initLocal,initGlobal:M.initGlobal,initIsolated:M.initIsolated,state:M.state}:void 0,patch:f,t:n});case"error":return i.jsx(jS,{value:D,onChange:U=>f("errorHandling",U),t:o,errorConfigCodes:i$,templateCodes:Object.keys(D),ownCodes:Object.keys(M?.errorHandling??{}),recipes:C,pageCandidates:h,modalCandidates:x,dataSourceCandidates:v,stateKeyCandidates:b});case"data":return i.jsx(e$,{raw:c,t:n,resolveLabel:o,onChange:(U,ee)=>f("data_sources",U,ee),onClose:s,actionRecipes:w,errorRecipes:C,conditionSpec:t,bindingCandidates:k,pageCandidates:h,dataSourceCandidates:v,stateKeyCandidates:b});default:return i.jsx("div",{})}};return i.jsxs("div",{className:"g7le-page-settings","data-testid":"g7le-page-settings",style:p$,children:[i.jsxs("div",{style:f$,children:[i.jsx("span",{"data-testid":"g7le-page-settings-title",style:g$,children:n("layout_editor.page_settings.title",{name:se})}),i.jsx("button",{type:"button","data-testid":"g7le-page-settings-close-x",onClick:s,"aria-label":n("layout_editor.page_settings.close"),style:h$,children:"✕"})]}),i.jsx("div",{role:"tablist",style:y$,children:r$.map(U=>{const ee=G[U];return i.jsxs("button",{type:"button",role:"tab","aria-selected":y===U,"data-testid":`g7le-page-settings-tab-${U}`,"data-active":y===U?"true":"false",onClick:()=>g(U),style:y===U?b$:Vu,children:[n(s$[U]),ee>0?i.jsxs("span",{"data-testid":`g7le-page-settings-tab-badge-${U}`,style:m$,children:["(",ee,")"]}):null]},U)})}),V>0?i.jsxs("p",{"data-testid":"g7le-page-settings-advanced-summary",style:v$,children:["⚠ ",n("layout_editor.page_settings.advanced_summary",{count:V})," ⓘ"]}):null,i.jsx("div",{"data-testid":"g7le-page-settings-panel",style:_$,children:re()}),i.jsx("div",{style:x$,children:i.jsx("button",{type:"button","data-testid":"g7le-page-settings-close",onClick:s,style:w$,children:n("layout_editor.page_settings.close")})})]})}const p$={display:"flex",flexDirection:"column",minWidth:0,maxWidth:"100%",padding:"20px 24px",boxSizing:"border-box",flex:1,minHeight:0,maxHeight:"100%",overflow:"hidden",fontFamily:"system-ui, -apple-system, sans-serif"},f$={display:"flex",alignItems:"center",gap:8,paddingBottom:8,borderBottom:"1px solid #e2e8f0"},g$={flex:1,fontSize:15,fontWeight:700,color:"#0f172a",minWidth:0},h$={border:"none",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:16,lineHeight:1,padding:4},y$={display:"flex",gap:2,paddingTop:8,borderBottom:"1px solid #e2e8f0",flexWrap:"nowrap",overflowX:"auto",overflowY:"hidden",flexShrink:0,minWidth:0},Vu={border:"none",borderBottomWidth:"2px",borderBottomStyle:"solid",borderBottomColor:"transparent",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:12,padding:"6px 10px",whiteSpace:"nowrap"},b$={...Vu,color:"#2563eb",borderBottomColor:"#2563eb",fontWeight:600},m$={marginLeft:4,fontSize:10,color:"#b45309"},v$={margin:0,marginTop:8,fontSize:11,color:"#b45309",background:"#fffbeb",padding:"6px 8px",borderRadius:6},_$={flex:1,overflowY:"auto",paddingTop:12,minWidth:0,minHeight:0},x$={display:"flex",justifyContent:"flex-end",paddingTop:8,borderTop:"1px solid #e2e8f0"},w$={border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",fontSize:13,padding:"6px 16px"};function Xs(e,t){const n=`/api/admin/templates/${encodeURIComponent(e)}`;return t.kind==="layout"?`${n}/layouts/${t.layoutName}/versions`:`${n}/layout-extensions/${encodeURIComponent(t.extensionId)}/versions`}const Uu=100,qu=500;function k$(e,t,n){const[o,r]=u.useState([]),[s,a]=u.useState(!1),[l,d]=u.useState(!1),[c,p]=u.useState(null),[f,y]=u.useState(null),[g,m]=u.useState(Uu),[h,v]=u.useState(!1),b=u.useCallback(async(C,E)=>{if(!t){r([]),p(null),v(!1);return}E==="more"?d(!0):a(!0),p(null);try{const S=`${Xs(e,t)}?limit=${C}`,T=await fetch(S,{credentials:"same-origin",headers:je()}),$=await T.json().catch(()=>null);if(!T.ok){p($?.message??`HTTP ${T.status}`),E==="reload"&&r([]);return}const A=$?.data,z=Array.isArray(A)?A:[];r(z),v(z.length>=C&&C<qu),m(C)}catch(S){p(S instanceof Error?S.message:"network error"),E==="reload"&&r([])}finally{E==="more"?d(!1):a(!1)}},[e,t]),x=u.useCallback(async()=>{await b(g,"reload")},[b,g]),k=u.useCallback(async()=>{const C=Math.min(g+Uu,qu);if(C<=g){v(!1);return}await b(C,"more")},[b,g]),w=u.useCallback(async C=>{if(!t)return{kind:"not_found"};y(C);try{const E=`${Xs(e,t)}/${C}/restore`,S=await fetch(E,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"})}),T=await S.json().catch(()=>null);if(S.status===404)return{kind:"not_found"};if(!S.ok)return{kind:"network_error",message:T?.message??`HTTP ${S.status}`};const $=typeof T?.data?.version=="number"?T.data.version:-1;return await n?.($>=0?$:void 0),{kind:"success",newVersion:$}}catch(E){return{kind:"network_error",message:E instanceof Error?E.message:"network error"}}finally{y(null)}},[e,t,n]),_=u.useCallback(async C=>{if(!t)return{kind:"not_found"};try{const E=`${Xs(e,t)}/${C}`,S=await fetch(E,{credentials:"same-origin",headers:je()}),T=await S.json().catch(()=>null);if(S.status===404)return{kind:"not_found"};if(!S.ok)return{kind:"network_error",message:T?.message??`HTTP ${S.status}`};const $=T?.data;if(!$)return{kind:"not_found"};if(t.kind==="extension"&&!$.full_content){const A=$.content;if(typeof A=="string")try{const z=JSON.parse(A);z&&typeof z=="object"&&($.full_content=z)}catch{}}return{kind:"success",detail:$}}catch(E){return{kind:"network_error",message:E instanceof Error?E.message:"network error"}}},[e,t]);return{versions:o,isLoading:s,isLoadingMore:l,error:c,restoringId:f,hasMore:h,loadVersions:x,loadMore:k,restore:w,loadVersionDetail:_}}function S$(e,t){const n=e.length,o=t.length,r=Array.from({length:n+1},()=>new Array(o+1).fill(0));for(let s=n-1;s>=0;s--)for(let a=o-1;a>=0;a--)e[s]===t[a]?r[s][a]=r[s+1][a+1]+1:r[s][a]=Math.max(r[s+1][a],r[s][a+1]);return r}function C$(e,t,n){const o=[];let r=0,s=0;const a=e.length,l=t.length;for(;r<a&&s<l;)e[r]===t[s]?(o.push({kind:"context",oldIndex:r,newIndex:s,content:e[r]}),r++,s++):n[r+1][s]>=n[r][s+1]?(o.push({kind:"remove",oldIndex:r,content:e[r]}),r++):(o.push({kind:"add",newIndex:s,content:t[s]}),s++);for(;r<a;)o.push({kind:"remove",oldIndex:r,content:e[r]}),r++;for(;s<l;)o.push({kind:"add",newIndex:s,content:t[s]}),s++;return o}function Gu(e){const t=e.replace(/\r\n/g,` `).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":`g7le-version-diff-hunk-${p}`,children:[i.jsx("div",{style:M$,"data-testid":`g7le-version-diff-hunk-header-${p}`,children:c.header}),c.lines.map((f,y)=>{const g=L$(f.kind);return i.jsxs("div",{style:{display:"flex",alignItems:"stretch",...g.row},"data-kind":f.kind,"data-testid":`g7le-version-diff-line-${p}-${y}`,children:[i.jsx("span",{style:{...Zu,...g.gutter},"aria-hidden":"true",children:f.oldLine??""}),i.jsx("span",{style:{...Zu,...g.gutter},"aria-hidden":"true",children:f.newLine??""}),i.jsx("span",{style:{display:"inline-block",width:16,textAlign:"center",color:g.signColor,userSelect:"none",fontWeight:700},"aria-hidden":"true",children:g.sign}),i.jsx("span",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word",flex:1,padding:"0 4px"},children:f.content})]},y)})]},p))})]})}const O$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},B$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},W$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},j$={flex:1,minHeight:0,overflowY:"auto",padding:12},Qu={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},H$={margin:"8px 16px 0",padding:"8px 12px",borderRadius:6,background:"#fef2f2",color:"#b91c1c",fontSize:12,border:"1px solid #fecaca"},F$={display:"flex",flexDirection:"column",gap:8},K$={display:"flex",alignItems:"center",gap:12,padding:"10px 12px",borderRadius:8,border:"1px solid #e2e8f0",background:"#ffffff"},V$={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:44,padding:"2px 8px",borderRadius:999,background:"#eff6ff",color:"#1d4ed8",fontSize:12,fontWeight:700},U$={display:"inline-flex",alignItems:"center",padding:"1px 8px",borderRadius:999,background:"#0f172a",color:"#ffffff",fontSize:10,fontWeight:700,letterSpacing:.3},q$={display:"flex",alignItems:"center",gap:10,fontSize:11,fontWeight:600},G$={display:"flex",flexDirection:"column",gap:2,minWidth:0,flex:1},Ru={padding:"6px 12px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none",whiteSpace:"nowrap"};function ep(e){return e.full_content&&typeof e.full_content=="object"?e.full_content:{endpoint:e.endpoint,components:e.components,data_sources:e.data_sources,metadata:e.metadata}}function Y$(e){if(!e)return"";const t=Date.parse(e);if(Number.isNaN(t))return e;try{return new Date(t).toLocaleString()}catch{return e}}function X$({templateIdentifier:e,target:t,t:n,onRestored:o,onClose:r}){const{versions:s,isLoading:a,isLoadingMore:l,error:d,restoringId:c,hasMore:p,loadVersions:f,loadMore:y,restore:g,loadVersionDetail:m}=k$(e,t,o),[h,v]=u.useState(null),[b,x]=u.useState(null),[k,w]=u.useState(null);u.useEffect(()=>{f()},[f]);const _=u.useCallback(async S=>{v(null),w(S.version);try{const T=await m(S.version);if(T.kind!=="success"){v(T.kind==="not_found"?n("layout_editor.version_history.restore_not_found"):T.message);return}const $=S.version-1,A=s.some(D=>D.version===$);let z={};if(A){const D=await m($);if(D.kind==="success")z=ep(D.detail);else if(D.kind==="network_error"){v(D.message);return}}x({oldVersion:A?$:0,newVersion:S.version,oldContent:z,newContent:ep(T.detail)})}finally{w(null)}},[m,s,n]),C=u.useCallback(async S=>{v(null);const T=n("layout_editor.version_history.restore_confirm");if(!(typeof window<"u"?window.confirm(T):!0))return;const A=await g(S);A.kind==="success"?r():A.kind==="not_found"?v(n("layout_editor.version_history.restore_not_found")):v(A.message)},[g,r,n]),E=h??d;return b?i.jsx(P$,{oldVersion:b.oldVersion,newVersion:b.newVersion,oldContent:b.oldContent,newContent:b.newContent,t:n,onBack:()=>x(null),onClose:r}):i.jsxs("div",{className:"g7le-version-history","data-testid":"g7le-version-history",style:O$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:B$,children:[i.jsxs("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-history-title",children:["🕘 ",n("layout_editor.version_history.title")]}),i.jsx("button",{type:"button","aria-label":n("layout_editor.version_history.close"),onClick:r,style:W$,"data-testid":"g7le-version-history-close",children:"✕"})]}),E&&i.jsx("div",{style:H$,"data-testid":"g7le-version-history-error",children:E}),i.jsx("div",{style:j$,children:a?i.jsx("div",{style:Qu,"data-testid":"g7le-version-history-loading",children:n("layout_editor.version_history.loading")}):s.length===0?i.jsx("div",{style:Qu,"data-testid":"g7le-version-history-empty",children:n("layout_editor.version_history.empty")}):i.jsxs("div",{"data-testid":"g7le-version-history-list",style:F$,children:[s.map((S,T)=>{const $=S.changes_summary,A=$?.added_count??0,z=$?.removed_count??0,D=$?.char_diff??0,O=Y$(S.created_at),M=c===S.id,B=T===0;return i.jsxs("div",{style:K$,"data-testid":`g7le-version-row-${S.id}`,children:[i.jsxs("span",{style:V$,"data-testid":`g7le-version-badge-${S.id}`,children:["v",S.version]}),i.jsxs("div",{style:G$,children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:8},children:[O&&i.jsx("span",{style:{fontSize:12,color:"#475569",fontWeight:600},children:O}),B&&i.jsx("span",{style:U$,"data-testid":`g7le-version-latest-${S.id}`,children:n("layout_editor.version_history.latest")})]}),i.jsxs("span",{style:q$,"data-testid":`g7le-version-summary-${S.id}`,children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":`g7le-version-added-${S.id}`,children:n("layout_editor.version_history.added",{count:A})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":`g7le-version-removed-${S.id}`,children:n("layout_editor.version_history.removed",{count:z})}),i.jsx("span",{style:{color:"#94a3b8",fontWeight:500},"data-testid":`g7le-version-chars-${S.id}`,children:n("layout_editor.version_history.chars",{diff:`${D>=0?"+":""}${D}`})})]}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},"data-testid":`g7le-version-author-${S.id}`,children:n("layout_editor.version_history.saved_by",{name:S.created_by_name??n("layout_editor.version_history.unknown_author")})})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[i.jsx("button",{type:"button",onClick:()=>void _(S),disabled:k!==null,style:{...Ru,background:"#f8fafc",opacity:k!==null?.6:1},"data-testid":`g7le-version-compare-${S.id}`,children:k===S.version?n("layout_editor.version_history.comparing"):n("layout_editor.version_history.compare")}),!B&&i.jsx("button",{type:"button",onClick:()=>void C(S.id),disabled:M||c!==null,style:{...Ru,opacity:M||c!==null?.6:1},"data-testid":`g7le-version-restore-${S.id}`,children:n(M?"layout_editor.version_history.restoring":"layout_editor.version_history.restore")})]})]},S.id)}),p&&i.jsx("button",{type:"button","data-testid":"g7le-version-history-load-more",style:J$,disabled:l,onClick:()=>{y()},children:n(l?"layout_editor.version_history.loading":"layout_editor.version_history.load_more")})]})})]})}const J$={width:"100%",marginTop:8,padding:"8px 12px",fontSize:12,color:"#475569",background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:6,cursor:"pointer"},tp=[{id:"copy",group:"clipboard",combos:[{key:"c",mod:!0}],labelKey:"layout_editor.shortcuts.action.copy",requiresSelection:!0},{id:"cut",group:"clipboard",combos:[{key:"x",mod:!0}],labelKey:"layout_editor.shortcuts.action.cut",requiresSelection:!0},{id:"paste",group:"clipboard",combos:[{key:"v",mod:!0}],labelKey:"layout_editor.shortcuts.action.paste"},{id:"openProps",group:"element",combos:[{key:"enter"}],labelKey:"layout_editor.shortcuts.action.open_props",requiresSelection:!0},{id:"delete",group:"element",combos:[{key:"delete"},{key:"backspace"}],labelKey:"layout_editor.shortcuts.action.delete",requiresSelection:!0},{id:"selectParent",group:"element",combos:[{key:"arrowup"}],labelKey:"layout_editor.shortcuts.action.select_parent",requiresSelection:!0},{id:"deselect",group:"element",combos:[{key:"escape"}],labelKey:"layout_editor.shortcuts.action.deselect"},{id:"addElement",group:"element",combos:[{key:"/",mod:!0}],labelKey:"layout_editor.shortcuts.action.add_element"},{id:"undo",group:"history",combos:[{key:"z",mod:!0}],labelKey:"layout_editor.shortcuts.action.undo"},{id:"redo",group:"history",combos:[{key:"z",mod:!0,shift:!0},{key:"y",mod:!0}],labelKey:"layout_editor.shortcuts.action.redo"},{id:"editCode",group:"view",combos:[{key:"e",mod:!0}],labelKey:"layout_editor.shortcuts.action.edit_code"},{id:"preview",group:"view",combos:[{key:"p",mod:!0}],labelKey:"layout_editor.shortcuts.action.preview"},{id:"translations",group:"view",combos:[{key:"l",mod:!0}],labelKey:"layout_editor.shortcuts.action.translations"},{id:"help",group:"view",combos:[{key:"?"}],labelKey:"layout_editor.shortcuts.action.help"},{id:"reset",group:"document",combos:[{key:"r",mod:!0,shift:!0}],labelKey:"layout_editor.shortcuts.action.reset"},{id:"save",group:"document",combos:[{key:"s",mod:!0}],labelKey:"layout_editor.shortcuts.action.save"},{id:"exit",group:"document",combos:[{key:"escape"}],labelKey:"layout_editor.shortcuts.action.exit"}];function Z$(e){return{key:(e.key||"").toLowerCase(),mod:e.ctrlKey||e.metaKey,shift:e.shiftKey,alt:e.altKey}}function Q$(e,t){return e.key===t.key&&!!e.mod==!!t.mod&&!!e.shift==!!t.shift&&!!e.alt==!!t.alt}function R$(e){for(const t of tp)if(t.combos.some(n=>Q$(n,e)))return t;return null}function eE(e,t){const n=[];return e.mod&&n.push(t?"⌘":"Ctrl"),e.shift&&n.push(t?"⇧":"Shift"),e.alt&&n.push(t?"⌥":"Alt"),n.push(tE(e.key)),n.join(" + ")}function tE(e){return{arrowup:"↑",arrowdown:"↓",arrowleft:"←",arrowright:"→",enter:"Enter",escape:"Esc",delete:"Delete",backspace:"Backspace"," ":"Space"}[e]??(e.length===1?e.toUpperCase():e)}const nE=["clipboard","element","history","view","document"],oE={clipboard:"layout_editor.shortcuts.group.clipboard",element:"layout_editor.shortcuts.group.element",history:"layout_editor.shortcuts.group.history",view:"layout_editor.shortcuts.group.view",document:"layout_editor.shortcuts.group.document"};function rE(){return typeof navigator>"u"?!1:/mac|iphone|ipad|ipod/i.test(navigator.platform||navigator.userAgent||"")}function iE({t:e,onClose:t,isMac:n}){const o=n??rE(),r=u.useMemo(()=>{const s=new Map;for(const a of tp){const l=s.get(a.group)??[];l.push(a),s.set(a.group,l)}return s},[]);return i.jsxs("div",{className:"g7le-shortcut-help","data-testid":"g7le-shortcut-help",style:sE,children:[i.jsxs("div",{style:aE,children:[i.jsx("span",{style:lE,children:e("layout_editor.shortcuts.title")}),i.jsx("button",{type:"button","data-testid":"g7le-shortcut-help-close",onClick:t,"aria-label":e("layout_editor.shortcuts.close"),style:dE,children:"✕"})]}),i.jsx("div",{style:cE,children:nE.map(s=>{const a=r.get(s);return!a||a.length===0?null:i.jsxs("div",{style:uE,"data-testid":`g7le-shortcut-group-${s}`,children:[i.jsx("div",{style:pE,children:e(oE[s])}),i.jsx("table",{style:fE,children:i.jsx("tbody",{children:a.map(l=>i.jsxs("tr",{"data-testid":`g7le-shortcut-row-${l.id}`,children:[i.jsx("td",{style:gE,children:e(l.labelKey)}),i.jsx("td",{style:hE,children:l.combos.map((d,c)=>i.jsxs(u.Fragment,{children:[c>0&&i.jsx("span",{style:bE,children:e("layout_editor.shortcuts.or")}),i.jsx("kbd",{style:yE,children:eE(d,o)})]},c))})]},l.id))})})]},s)})}),i.jsx("div",{style:mE,children:e("layout_editor.shortcuts.footnote")})]})}const sE={display:"flex",flexDirection:"column",padding:20,gap:16,width:"100%",boxSizing:"border-box"},aE={display:"flex",alignItems:"center",justifyContent:"space-between",paddingBottom:12,borderBottom:"1px solid #e2e8f0"},lE={fontSize:15,fontWeight:700,color:"#0f172a"},dE={width:28,height:28,border:"none",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:14,borderRadius:6},cE={display:"grid",gridTemplateColumns:"1fr 1fr",columnGap:28,rowGap:18,alignItems:"start"},uE={display:"flex",flexDirection:"column",gap:6},pE={fontSize:12,fontWeight:700,color:"#2563eb"},fE={width:"100%",borderCollapse:"collapse"},gE={padding:"4px 0",fontSize:12.5,color:"#334155",textAlign:"left",whiteSpace:"nowrap",verticalAlign:"middle"},hE={padding:"4px 0",paddingLeft:12,textAlign:"right",verticalAlign:"middle"},yE={display:"inline-block",padding:"1px 6px",fontSize:11,fontFamily:"ui-monospace, monospace",color:"#0f172a",background:"#f1f5f9",border:"1px solid #cbd5e1",borderBottomWidth:2,borderRadius:5,whiteSpace:"nowrap"},bE={fontSize:11,color:"#94a3b8",margin:"0 4px"},mE={fontSize:11,color:"#94a3b8",paddingTop:12,borderTop:"1px solid #f1f5f9"};function vE(e){if(!e||!(e instanceof HTMLElement))return!1;const t=e.tagName;return!!(t==="INPUT"||t==="TEXTAREA"||t==="SELECT"||e.isContentEditable)}function _E(){return typeof document>"u"?!1:!!document.querySelector(".g7le-modal-backdrop, .g7le-modal")}function np(e){const{handlers:t,hasSelection:n,enabled:o=!0}=e;u.useEffect(()=>{if(!o||typeof window>"u")return;const r=s=>{if(vE(s.target))return;const a=Z$(s),l=R$(a);if(!l||_E()||l.requiresSelection&&!n)return;if(l.key==="escape"||a.key==="escape"){if(n){const c=t.deselect;c&&(s.preventDefault(),c())}else{const c=t.exit;c&&(s.preventDefault(),c())}return}const d=t[l.id];d&&(s.preventDefault(),d())};return window.addEventListener("keydown",r),()=>window.removeEventListener("keydown",r)},[t,n,o])}function xE(e){const{result:t,onDismiss:n,onLoadLatest:o,onKeepMyChanges:r}=e,{t:s}=tt();if(u.useEffect(()=>{if(!t||t.kind==="concurrent_modification"||t.kind==="validation_failed")return;const a=setTimeout(n,5e3);return()=>clearTimeout(a)},[t,n]),!t)return null;if(t.kind==="success")return i.jsx(Lo,{tone:"success",testId:"g7le-save-banner-success",onDismiss:n,children:s("layout_editor.save.success")});if(t.kind==="validation_failed"){const a=t.errors??{},l=Object.entries(a);return i.jsxs(Lo,{tone:"error",testId:"g7le-save-banner-validation",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.validation_failed")}),l.length>0&&i.jsx("ul",{style:op,"data-testid":"g7le-save-banner-validation-errors",children:l.map(([d,c])=>i.jsxs("li",{children:[i.jsx("code",{style:rp,children:d}),": ",c.join(", ")]},d))})]})}return t.kind==="blocked_inactive_extension"?i.jsxs(Lo,{tone:"warning",testId:"g7le-save-banner-blocked",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.blocked_inactive_extension")}),t.blockedPaths.length>0&&i.jsx("ul",{style:op,"data-testid":"g7le-save-banner-blocked-paths",children:t.blockedPaths.map(a=>i.jsx("li",{children:i.jsx("code",{style:rp,children:a})},a))})]}):t.kind==="network_error"?i.jsxs(Lo,{tone:"error",testId:"g7le-save-banner-network",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.network_error_title")}),i.jsx("div",{style:{marginTop:4,fontSize:12},children:t.message})]}):t.kind==="guard_no_document"?i.jsx(Lo,{tone:"neutral",testId:"g7le-save-banner-guard-no-document",onDismiss:n,children:s("layout_editor.save.guard_no_document")}):i.jsx("div",{role:"dialog","aria-modal":"true","aria-label":s("layout_editor.save.concurrent.title"),"data-testid":"g7le-save-banner-concurrent",style:CE,children:i.jsxs("div",{style:$E,children:[i.jsx("h2",{style:EE,children:s("layout_editor.save.concurrent.title")}),i.jsx("p",{style:IE,children:s("layout_editor.save.concurrent.message")}),i.jsx("div",{style:AE,children:s("layout_editor.save.concurrent.version_info",{current:String(t.currentVersion),yours:String(t.yourVersion)})}),i.jsxs("div",{style:TE,children:[i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-load-latest",onClick:()=>{o?.(),n()},style:DE,children:s("layout_editor.save.concurrent.load_latest")}),i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-keep-mine",onClick:()=>{r?.(),n()},style:NE,children:s("layout_editor.save.concurrent.view_my_changes")}),i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-cancel",onClick:n,style:zE,children:s("layout_editor.save.concurrent.cancel")})]})]})})}function Lo(e){const{tone:t,testId:n,onDismiss:o,children:r}=e,s=wE[t];return i.jsxs("div",{role:"status","data-testid":n,style:{...kE,background:s.bg,borderColor:s.border,color:s.fg},children:[i.jsx("div",{style:{flex:1,minWidth:0},children:r}),i.jsx("button",{type:"button",onClick:o,"aria-label":"close","data-testid":`${n}-close`,style:SE,children:"×"})]})}const wE={success:{bg:"#dcfce7",border:"#86efac",fg:"#14532d"},error:{bg:"#fee2e2",border:"#fca5a5",fg:"#7f1d1d"},warning:{bg:"#fef3c7",border:"#fcd34d",fg:"#78350f"},neutral:{bg:"#f1f5f9",border:"#cbd5e1",fg:"#334155"}},kE={display:"flex",alignItems:"flex-start",gap:12,padding:"12px 16px",border:"1px solid",borderRadius:0,fontSize:13,lineHeight:1.5},SE={background:"transparent",border:"none",fontSize:18,cursor:"pointer",lineHeight:1,color:"inherit",padding:"0 4px",flexShrink:0},op={margin:"8px 0 0",paddingLeft:20,fontSize:12},rp={background:"rgba(0, 0, 0, 0.06)",padding:"1px 4px",borderRadius:3,fontFamily:"monospace"},CE={position:"fixed",inset:0,background:"rgba(15, 23, 42, 0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e4},$E={background:"#fff",borderRadius:8,padding:24,width:"min(480px, 90vw)",boxShadow:"0 10px 25px rgba(0,0,0,0.15)"},EE={margin:"0 0 8px",fontSize:16,fontWeight:600,color:"#0f172a"},IE={margin:"0 0 12px",color:"#334155",fontSize:13,lineHeight:1.5},AE={marginBottom:16,padding:"8px 12px",background:"#f8fafc",borderRadius:6,fontSize:12,color:"#64748b",fontFamily:"monospace"},TE={display:"flex",gap:8,justifyContent:"flex-end"},DE={padding:"8px 14px",background:"#2563eb",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},NE={padding:"8px 14px",background:"#fff",color:"#334155",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"},zE={padding:"8px 14px",background:"transparent",color:"#64748b",border:"none",borderRadius:6,fontSize:13,cursor:"pointer"};function ip(e){if(!e)return null;const n=new URLSearchParams(e).get("route");return n&&n.length>0?n:null}function Zs(e,t,n){const o=`/admin/layout-editor/${encodeURIComponent(e)}`;if(!t||t.length===0)return o;if(t.startsWith("__")){let r=`${o}?edit=${encodeURIComponent(t)}`;return(t.startsWith("__modal__/")||t.startsWith("__iteration__/"))&&n&&(r+=`&host=${encodeURIComponent(n)}`),r}return`${o}?route=${encodeURIComponent(t)}`}function sp(e){if(!e)return null;const n=new URLSearchParams(e).get("host");return n&&n.length>0?n:null}function ap(e){if(!e)return null;const n=new URLSearchParams(e).get("edit");return n&&n.length>0&&n.startsWith("__")?n:null}function ME(e,t){const n=`/admin/templates/${encodeURIComponent(e)}/edit`;if(!t||t.length===0||t.startsWith("__"))return n;const o=t.replace(/^\*+/,"");return o.length===0?n:`${n}?route=${encodeURIComponent(o)}`}const LE="g7le-editor-route",PE=new Set;function Qs(e){return e.path.startsWith("__group__/")||e.path.startsWith("__conngroup__/")}function lp(e,t){const{t:n,translationEngine:o,translationContext:r,templateIdentifier:s}=t,a=Qs(e);if(!e.label.startsWith("$t:"))return e.label;const l=e.label.slice(3),d=l.indexOf("|"),c=d===-1?l:l.slice(0,d),p={};if(d!==-1)for(const y of l.slice(d+1).split("|")){const g=y.indexOf("=");g!==-1&&(p[y.slice(0,g)]=y.slice(g+1))}if((e.kind==="route"||e.kind==="modal")&&!a&&o&&r&&s!==r.templateId){const y={templateId:s,locale:r.locale},g=Object.keys(p).length>0?"|"+Object.entries(p).map(([h,v])=>`${h}=${v}`).join("|"):void 0,m=o.translate(c,y,g);if(m!==c)return m}const f=n(c,p);if(f===c&&typeof window<"u"){const y=window.G7Core?.t;if(typeof y=="function")try{const g=y(c,p);if(g!==c)return g}catch{}}if(f===c&&e.kind==="route"&&!a){const y=e.path;if(y&&y!=="/"){const g=y.split("/").filter(Boolean);return g[g.length-1]??y}return y==="/"?"/":c}return f}function OE(e){return e.trim().toLocaleLowerCase()}function dp(e,t){if(!t)return e;const n=e.toLocaleLowerCase(),o=[];let r=0,s=n.indexOf(t,r);if(s===-1)return e;let a=0;for(;s!==-1;)s>r&&o.push(e.slice(r,s)),o.push(i.jsx("mark",{className:"g7le-route-tree__highlight",style:{background:"#fde68a",color:"inherit",borderRadius:2,padding:"0 1px"},children:e.slice(s,s+t.length)},`m${a++}`)),r=s+t.length,s=n.indexOf(t,r);return r<e.length&&o.push(e.slice(r)),o}function BE(e){if(!e.layoutName)return null;const t=`${e.layoutName}.json`;switch(e.source.kind){case"module":case"plugin":return`${e.source.identifier?`${e.source.identifier} · `:""}resources/layouts/${t}`;case"template":return`${e.source.identifier?`${e.source.identifier} · `:""}layouts/${t}`;default:return`layouts/${t}`}}function WE({node:e}){const{t}=tt(),n=[],o={display:"inline-flex",alignItems:"center",padding:"1px 6px",fontSize:10,fontWeight:600,borderRadius:4,letterSpacing:.3,textTransform:"uppercase",border:"1px solid transparent"};return e.isHidden&&n.push(i.jsx("span",{className:"g7le-route-tree__badge g7le-route-tree__badge--hidden","data-testid":"g7le-route-tree-badge-hidden",style:{...o,background:"#f1f5f9",color:"#64748b",borderColor:"#e2e8f0"},children:t("layout_editor.chrome.route_tree.badge.hidden")},"hidden")),e.isRedirect&&n.push(i.jsx("span",{className:"g7le-route-tree__badge g7le-route-tree__badge--redirect","data-testid":"g7le-route-tree-badge-redirect",style:{...o,background:"#fef3c7",color:"#92400e",borderColor:"#fde68a"},children:t("layout_editor.chrome.route_tree.badge.redirect")},"redirect")),n.length===0?null:i.jsx("span",{style:{marginLeft:"auto",display:"inline-flex",gap:4},children:n})}function cp({node:e,depth:t,onSelect:n,selectedRoutePath:o,activeHostRoutePath:r,dirtyLayoutNames:s,query:a,forceExpand:l}){const{t:d,translationEngine:c,translationContext:p}=tt(),{state:f}=Je(),y=Qs(e),g=e.path.startsWith("__conngroup__/"),m=u.useMemo(()=>{const N=f.selectedRoute?.path??"";return N.startsWith("__extension__/")||N.startsWith("__modal__/")?N.slice(N.lastIndexOf("/")+1):null},[f.selectedRoute?.path]),h=u.useMemo(()=>!g||!m?!1:(e.children??[]).some(N=>N.kind==="extension"&&N.extensionId===m||N.kind==="modal"&&N.modalId===m),[g,m,e.children]),[v,b]=u.useState(!0);u.useEffect(()=>{h&&b(!1)},[h]);const x=f.editMode==="extension"?f.selectedRoute?.layoutName??null:null,k=e.kind==="extension"?up(e,f.routeTree)??null:null,w=e.kind==="extension"&&o===e.path&&(x===null||k===null||x===k),_=f.editMode==="modal"&&o?.startsWith("__modal__/")?o.slice(o.lastIndexOf("/")+1):null,C=f.editMode==="modal"?f.selectedRoute?.layoutName??null:null,E=e.kind==="modal"&&!!_&&e.modalId===_&&(C===null||e.modalHostLayout==null||C===e.modalHostLayout),S=e.kind!=="extension"&&e.kind!=="modal"&&o===e.path||w||E||e.kind==="route"&&!y&&r===e.path,$=(e.children?.length??0)>0&&(!g||l||!v),A=!y&&!!e.layoutName&&s.has(e.layoutName),z=y?void 0:e.kind==="extension"?e.extensionId?f.extensionVersions[e.extensionId]:void 0:e.layoutName?f.layoutVersions[e.layoutName]:void 0,D=()=>{if(g){b(N=>!N);return}y||n(e)},O=lp(e,{t:d,translationEngine:c,translationContext:p,templateIdentifier:f.templateIdentifier}),M=y?null:BE(e),B={paddingLeft:12+t*14,paddingRight:12,paddingTop:4,paddingBottom:4,opacity:e.isHidden?.55:1,cursor:g?"pointer":y?"default":"pointer",background:S?"#eff6ff":"transparent",borderLeft:S?"2px solid #2563eb":"2px solid transparent",fontWeight:y||S?600:400,fontSize:y?11:13,color:y?"#64748b":S?"#1d4ed8":"#1e293b",textTransform:y?"uppercase":"none",letterSpacing:y?.6:void 0,transition:"background-color 100ms, color 100ms",display:"flex",flexDirection:"column",alignItems:"stretch",gap:2,userSelect:"none"};return i.jsxs("li",{children:[i.jsxs("div",{"data-testid":y?"g7le-route-tree-group":"g7le-route-tree-item","data-route-path":e.path,"data-route-kind":e.kind,"data-conn-collapsed":g?l?"false":String(v):void 0,role:g||!y?"button":void 0,tabIndex:g||!y?0:void 0,"aria-expanded":g?l?!0:!v:void 0,onClick:D,onKeyDown:N=>{g&&(N.key==="Enter"||N.key===" ")?(N.preventDefault(),b(H=>!H)):!y&&(N.key==="Enter"||N.key===" ")&&(N.preventDefault(),n(e))},style:B,children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:6,minWidth:0},children:[g&&i.jsx("span",{"aria-hidden":"true",style:{flex:"0 0 auto",fontSize:9,color:"#94a3b8",width:10,display:"inline-block"},children:l||!v?"▾":"▸"}),A&&i.jsx("span",{className:"g7le-route-tree__dirty-dot","data-testid":"g7le-route-tree-dirty",title:d("layout_editor.chrome.route_tree.badge.modified"),"aria-label":d("layout_editor.chrome.route_tree.badge.modified"),style:{width:7,height:7,borderRadius:"50%",background:"#f59e0b",flex:"0 0 auto"}}),y?O:dp(O,a),typeof z=="number"&&i.jsxs("span",{className:"g7le-route-tree__version-badge","data-testid":"g7le-route-tree-version",title:d("layout_editor.chrome.route_tree.badge.version_tooltip",{version:String(z)}),style:{flex:"0 0 auto",padding:"0 5px",fontSize:10,fontWeight:600,lineHeight:"16px",borderRadius:4,background:"#eef2ff",color:"#4f46e5",border:"1px solid #e0e7ff",letterSpacing:.2},children:["v",z]}),i.jsx(WE,{node:e})]}),M&&i.jsx("span",{className:"g7le-route-tree__layout-path","data-testid":"g7le-route-tree-layout-path",title:M,style:{fontSize:11,fontWeight:400,color:"#94a3b8",textTransform:"none",letterSpacing:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:dp(M,a)})]}),$&&i.jsx("ul",{style:{listStyle:"none",margin:0,padding:0},children:e.children.map(N=>i.jsx(cp,{node:N,depth:t+1,onSelect:n,selectedRoutePath:o,activeHostRoutePath:r,dirtyLayoutNames:s,query:a,forceExpand:l},N.path))})]})}function jE(e,t){if(!t||!t.startsWith("__"))return null;const n=t.startsWith("__modal__/"),o=t.startsWith("__extension__/");if(!n&&!o)return null;const r=t.slice(t.lastIndexOf("/")+1),s=a=>{for(const l of a){if(l.connectedHostRoutePath&&(n&&l.kind==="modal"&&l.modalId===r||o&&l.kind==="extension"&&l.extensionId===r))return l.connectedHostRoutePath;if(l.children&&l.children.length>0){const d=s(l.children);if(d)return d}}return null};return s(e)}function up(e,t){if(e.connectedHostRoutePath){const n=pp(t,e.connectedHostRoutePath);if(n?.layoutName)return n.layoutName}if(e.extensionType==="overlay"&&e.extensionTargetName)return e.extensionTargetName}function pp(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=pp(n.children,t);if(o)return o}}return null}function fp(e,t,n){const o=[];for(const r of e){const s=Qs(r),a=r.children&&r.children.length>0?fp(r.children,t,n):[];if(s){a.length>0&&o.push({...r,children:a});continue}const l=lp(r,n).toLocaleLowerCase(),d=r.path.toLocaleLowerCase();(l.includes(t)||!d.startsWith("__")&&d.includes(t)||a.length>0)&&o.push({...r,children:a})}return o}function HE(){const{t:e,translationEngine:t,translationContext:n}=tt(),{state:o,dispatch:r}=Je(),a=Ft()?.dirtyLayoutNames??PE,[l,d]=u.useState(""),c=OE(l),p=u.useMemo(()=>c?fp(o.routeTree,c,{t:e,translationEngine:t,translationContext:n,templateIdentifier:o.templateIdentifier}):o.routeTree,[c,o.routeTree,o.templateIdentifier,e,t,n]),f=h=>{if(h.kind==="base"&&h.layoutName){r({type:"ENTER_BASE_EDIT",layoutName:h.layoutName});return}if(h.kind==="modal"&&h.modalId&&h.modalHostLayout){r({type:"ENTER_MODAL_EDIT",modalId:h.modalId,hostLayout:h.modalHostLayout});return}if(h.kind==="extension"&&h.extensionId){const v=up(h,o.routeTree);r({type:"ENTER_EXTENSION_EDIT",extensionId:h.extensionId,extensionHost:v});return}if(r({type:"SELECT_ROUTE",route:{path:h.path,layoutName:h.layoutName}}),typeof window<"u"&&window.history?.pushState){const v=Zs(o.templateIdentifier,h.path);`${window.location.pathname}${window.location.search}`!==v&&window.history.pushState({source:LE,routePath:h.path},"",v)}},y=o.selectedRoute?.path??null,g=u.useMemo(()=>jE(o.routeTree,y),[o.routeTree,y]);if(o.isRouteTreeCollapsed)return i.jsx("aside",{className:"g7le-route-tree g7le-route-tree--collapsed","data-testid":"g7le-route-tree-panel","data-collapsed":"true",style:{width:28,borderRight:"1px solid #e2e8f0",background:"#ffffff"}});const m=o.routeTree.length>0;return i.jsxs("aside",{className:"g7le-route-tree","data-testid":"g7le-route-tree-panel","data-collapsed":"false",style:{width:280,borderRight:"1px solid #e2e8f0",background:"#ffffff",position:"sticky",top:"var(--g7le-header-h, 0px)",alignSelf:"start",height:"calc(100vh - var(--g7le-header-h, 0px))",overflow:"hidden",minHeight:0,display:"flex",flexDirection:"column"},"aria-label":e("layout_editor.chrome.route_tree.panel_title"),children:[i.jsx("div",{className:"g7le-route-tree__title",style:{padding:"10px 14px",fontSize:12,fontWeight:600,color:"#475569",textTransform:"uppercase",letterSpacing:.6,borderBottom:"1px solid #e2e8f0",background:"#f8fafc",flex:"0 0 auto"},children:e("layout_editor.chrome.route_tree.panel_title")}),m&&i.jsxs("div",{className:"g7le-route-tree__search",style:{padding:"8px 10px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",flex:"0 0 auto",position:"relative"},children:[i.jsx("input",{type:"text",className:"g7le-route-tree__search-input","data-testid":"g7le-route-tree-search",value:l,onChange:h=>d(h.target.value),placeholder:e("layout_editor.chrome.route_tree.search_placeholder"),"aria-label":e("layout_editor.chrome.route_tree.search_placeholder"),style:{width:"100%",boxSizing:"border-box",padding:l?"6px 26px 6px 10px":"6px 10px",fontSize:12,lineHeight:1.4,color:"#1e293b",border:"1px solid #cbd5e1",borderRadius:6,outline:"none",background:"#f8fafc"}}),l&&i.jsx("button",{type:"button",className:"g7le-route-tree__search-clear","data-testid":"g7le-route-tree-search-clear",onClick:()=>d(""),"aria-label":e("layout_editor.chrome.route_tree.search_clear"),title:e("layout_editor.chrome.route_tree.search_clear"),style:{position:"absolute",right:16,top:"50%",transform:"translateY(-50%)",width:18,height:18,display:"inline-flex",alignItems:"center",justifyContent:"center",border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:14,lineHeight:1,padding:0},children:"✕"})]}),!m&&i.jsx("div",{className:"g7le-route-tree__empty","data-testid":"g7le-route-tree-empty",style:{padding:16,fontSize:12,color:"#94a3b8",textAlign:"center"},children:e("layout_editor.chrome.route_tree.empty")}),m&&c&&p.length===0&&i.jsx("div",{className:"g7le-route-tree__no-results","data-testid":"g7le-route-tree-no-results",style:{padding:16,fontSize:12,color:"#94a3b8",textAlign:"center"},children:e("layout_editor.chrome.route_tree.search_no_results")}),m&&p.length>0&&i.jsx("nav",{className:"g7le-route-tree__scroll","data-testid":"g7le-route-tree-scroll",style:{flex:"1 1 auto",minHeight:0,overflowY:"auto"},children:i.jsx("ul",{style:{listStyle:"none",margin:0,padding:0},children:p.map(h=>i.jsx(cp,{node:h,depth:0,onSelect:f,selectedRoutePath:y,activeHostRoutePath:g,dirtyLayoutNames:a,query:c,forceExpand:!!c},h.path))})})]})}const FE=Lt().DynamicRenderer,Rs=Lt(),KE=Rs.ResponsiveProvider;Rs.useResponsive,Rs.ResponsiveContext;const VE=Lt().ActionDispatcher,gp=Lt(),UE=gp.DataSourceManager;gp.dataSourceManager;function Gr(e,t,n){const o=t??{},r=o.message||o.error||`HTTP ${e}`,s=o?.data?.required_permissions??o?.required_permissions;let a;return e===401?a="unauthorized":e===403?a="forbidden":e===404?a="not_found":e>=500?a="server_error":a="unknown",{kind:a,status:e,message:String(r),requiredPermissions:s?String(s):void 0,source:n}}function io(e,t){return{kind:"network",status:0,message:e instanceof Error?e.message:String(e??"Unknown network error"),source:t}}function hp(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.kind=="string"&&typeof t.status=="number"&&typeof t.message=="string"}function Ze(e){if(!e)return[];const t=[],n=e.split(".");for(let o=0;o<n.length;o++){const r=n[o];if(r==="children")continue;if(r==="iteration"||r==="sortable"){o+=1;continue}if(r==="responsive"){const a=n[o+1];typeof a=="string"&&a.length>0&&(t.push({responsive:a}),o+=1);continue}const s=Number(r);Number.isFinite(s)&&Number.isInteger(s)&&s>=0&&t.push(s)}return t}function qE(e,t){if(!e)return{affordance:"none",targetPath:null};const n=GE(e);return n?YE(n)?{affordance:"dynamic_path",targetPath:n}:/^(https?:)?\/\//.test(n)?{affordance:"external_url",targetPath:n}:n.startsWith("/")?t?{affordance:t(n),targetPath:n}:{affordance:"route_not_in_tree",targetPath:n}:{affordance:"route_not_in_tree",targetPath:n}:{affordance:"none",targetPath:null}}function GE(e){const t=e.actions;if(Array.isArray(t)){for(const n of t)if(n&&typeof n=="object"&&n.handler==="navigate"){const o=n.params?.path;if(typeof o=="string"&&o.length>0)return o}}if(e.name==="A"||e.type==="A"){const n=e.props?.href;if(typeof n=="string"&&n.length>0)return n}return null}function YE(e){return/\{\{[\s\S]*?\}\}/.test(e)}function XE(e,t,n){const o=s=>!!(s?.iteration&&s.iteration.source!==void 0),r=s=>{if(!s)return s;const a=s.indexOf(".iteration."),l=s.indexOf(".sortable."),d=[a,l].filter(c=>c>=0).sort((c,p)=>c-p)[0];return d===void 0?s:s.slice(0,d)};return o(e)?{isIteration:!0,iterationSourcePath:r(n)}:n&&n.includes(".iteration.")?{isIteration:!0,iterationSourcePath:r(n)}:t.some(o)?{isIteration:!0,iterationSourcePath:r(n)}:{isIteration:!1,iterationSourcePath:null}}function JE(e,t){const n=a=>{const l=a?.iteration&&a.iteration.source;return typeof l=="string"&&l.length>0?l:null},o=n(e)??t.map(n).find(a=>a!==null)??null;if(o!==null){const a=QE(o);if(a!==null)return a}const r=[];if(typeof e.text=="string"&&r.push(...yp(e.text)),e.props&&typeof e.props=="object")for(const a of Object.values(e.props))typeof a=="string"&&r.push(...yp(a));return r.length===0?null:r.find(a=>!ZE.has(a))??r[0]}const ZE=new Set(["_local","_global","_computed","route","query","props","item","index","event","$event","$response","response"]);function yp(e){const t=[],n=/\{\{([\s\S]*?)\}\}/g;let o;for(;(o=n.exec(e))!==null;){const r=bp(o[1]);r!==null&&t.push(r)}return t}function QE(e){const t=e.replace(/\{\{|\}\}/g,"");return bp(t)}function bp(e){const t=/[A-Za-z_$][A-Za-z0-9_$]*/.exec(e.trim());return t?t[0]:null}function gn(e,t,n,o=[]){if(!e)return"none";if(td(e)||typeof e.slot=="string"||typeof e.__editorSlotName=="string")return"extension_point";if(t!=="route"&&Ji(e,t,n)){const s=e.__source?.kind;return s==="base"?"base":s==="partial"?"partial":s==="extension"?"extension":"base"}if(Cb(e,o))return"data_bound";if(!Ji(e,t,n))return"none";const r=e.__source?.kind;return r==="base"?"base":r==="partial"?"partial":r==="extension"?"extension":"none"}function mp(e){if(!e)return null;if(e.__source?.kind==="extension"&&typeof e.__source.extensionId=="number")return e.__source.extensionId;const t=e.__injectedProps;if(Array.isArray(t)&&t.length>0){const n=t[0]?.extensionId;if(typeof n=="number")return n}return null}function RE(e,t){if(t<=0)return e;const n=e.split(".");let o=0;const r=[];for(let s=0;s<n.length;s++){const a=n[s];if(a==="children"){if(o>=t)break;r.push(a);continue}if(a==="iteration"||a==="sortable"){if(o>=t)break;r.push(a);continue}r.push(a);const l=n[s-1];if((l===void 0||l==="children")&&(o+=1),o>=t)break}return r.join(".")}function vp(e,t,n,o){if(!e||!t)return t;const r=n(t),s=Ib(e,r,o);return s.length===r.length?t:RE(t,s.length)}function _p(e){return e==="none"||e==="data_bound"}function e7(e){const{rootNode:t,editMode:n,currentExtensionId:o,editableRootPath:r,pathParser:s=Ze,resolveRouteMatch:a}=e,[l,d]=u.useState(null),[c,p]=u.useState(null),f=u.useCallback(x=>{if(x===null||n==="route")return!0;if(n==="iteration_item"||n==="modal"){if(!r||r.length===0)return!0;const C=s(x);return xp(C,r)}const k=ea(t,x,s);if(!k)return!0;const w=ta(t,x,s),_=gn(k,n,o,w);return _==="none"||_==="data_bound"},[t,s,n,o,r]),y=u.useCallback((x,k)=>{const _=(k?.currentTarget?.dataset??k?.dataset)?.editorPath??null,C=vp(t,_,s,o);f(C)&&d(C)},[t,s,o,f]),g=u.useCallback((x,k)=>{if(x===null){p(null);return}const _=(k?.currentTarget?.dataset??k?.dataset)?.editorPath??null,C=vp(t,_,s,o);if(!f(C)){p(null);return}p(C)},[t,s,o,f]),m=u.useCallback(()=>{d(null)},[]),h=u.useMemo(()=>{const x=ea(t,l,s),k=ea(t,c,s),w=x?ta(t,l,s):[];let _=gn(x,n,o,w);(n==="iteration_item"||n==="modal")&&l!==null&&x&&r&&r.length>0&&xp(s(l),r)&&(nd(x)?_="data_bound":_==="data_bound"&&(_="none"));const{affordance:C,targetPath:E}=qE(x,a),S=_==="base"&&typeof x?.__source?.layout=="string"?x.__source.layout:null,T=_==="data_bound"&&x?JE(x,w):null,{isIteration:$,iterationSourcePath:A}=_==="data_bound"&&x?XE(x,w,l):{isIteration:!1,iterationSourcePath:null};return{selectedNode:x,hoverNode:k,selectedLockKind:_,selectedNavAffordance:C,selectedNavTargetPath:E,selectedBaseLayout:S,selectedDataSourceId:T,selectedIsIteration:$,selectedIterationSourcePath:A}},[t,l,c,n,o,r,s,a]),v=u.useRef(void 0);v.current!==l&&(v.current=l,Cl({op:l===null?"clear":"select",componentPath:l,componentName:wp(h.selectedNode),lockKind:h.selectedLockKind,navAffordance:h.selectedNavAffordance,sourceExtensionId:mp(h.selectedNode),timestamp:Date.now()}));const b=u.useRef(void 0);return b.current!==c&&(b.current=c,c!==null&&Cl({op:"hover",componentPath:c,componentName:wp(h.hoverNode),lockKind:gn(h.hoverNode,n,o,ta(t,c,s)),navAffordance:"none",sourceExtensionId:mp(h.hoverNode),timestamp:Date.now()})),{selectedPath:l,hoverPath:c,...h,handleSelect:y,handleHover:g,clearSelection:m}}function ea(e,t,n){if(!e||!t)return null;const o=n(t);return We(e,o)}function xp(e,t){if(e.length<t.length)return!1;for(let n=0;n<t.length;n++)if(e[n]!==t[n])return!1;return!0}function ta(e,t,n){if(!e||!t)return[];const o=n(t),r=[];for(let s=0;s<o.length;s++){const a=o.slice(0,s),l=We(e,a);l&&r.push(l)}return r}function wp(e){return e?typeof e.name=="string"&&e.name.length>0?e.name:typeof e.type=="string"&&e.type.length>0?e.type:null:null}let kp=0;function Sp(){return kp}function na(){kp+=1}function t7(e){if(typeof e!="string")return null;const t="__modal__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n===""||n.includes("/")?null:n}function n7(e){if(typeof e!="string")return null;const t="__iteration__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n!==""?n:null}function Cp(){const{state:e}=Je(),t=e.templateIdentifier,n=e.selectedRoute?.layoutName??null,o=e.editMode,r=o==="modal"?t7(e.selectedRoute?.path):null,s=o==="iteration_item"?n7(e.selectedRoute?.path):null,[a,l]=u.useState(null),[d,c]=u.useState(!1),[p,f]=u.useState(null),[y,g]=u.useState(!1),[m,h]=u.useState(0),[v,b]=u.useState(0),x=u.useRef(new Map),[k,w]=u.useState(()=>new Set),_=u.useCallback((L,I)=>`${L}|${I??""}`,[]),C=u.useRef(null);u.useEffect(()=>{C.current=a},[a]);const E=u.useCallback((L,I)=>{const P=_(o,n);x.current.set(P,{raw:L,lockVersion:I}),w(F=>{if(F.has(P))return F;const G=new Set(F);return G.add(P),G})},[_,o,n]),S=u.useCallback(L=>{x.current.delete(L),w(I=>{if(!I.has(L))return I;const P=new Set(I);return P.delete(L),P})},[]),T=u.useCallback(L=>{const I=`|${L??""}`,P=[];for(const F of x.current.keys())F.endsWith(I)&&P.push(F);for(const F of P)x.current.delete(F);P.length>0&&w(F=>{let G=!1;const V=new Set(F);for(const Y of P)V.delete(Y)&&(G=!0);return G?V:F})},[]),$=u.useCallback(async()=>{if(!n){l(null),f(null),g(!1);return}const L=_(o,n),I=x.current.get(L);if(I){l({layoutName:n,raw:I.raw,lockVersion:I.lockVersion}),g(!0),f(null),c(!1),st({op:"load",layoutName:n,editMode:o,statusCode:0,isDirty:!0,timestamp:Date.now()});return}c(!0),f(null);try{const P=window.G7Config?.cache_version??0,F=en(`/api/layouts/${encodeURIComponent(t)}/${n}`,"json",`${P}.${Sp()}`,"with_source_meta=1"),G=ar(),V={Accept:"application/json"};G&&(V.Authorization=`Bearer ${G}`);const Y=await fetch(F,{headers:V,credentials:"same-origin"}),Z=await Y.json().catch(()=>null);if(!Y.ok){f(Gr(Y.status,Z,"layout")),l(null),g(!1);return}const se=(Z&&(Z.data||Z))??{},re=typeof se.lock_version=="number"?se.lock_version:0;if(o==="modal"&&r){const ee=se,fe=Array.isArray(ee.modals)?ee.modals:[],K=fe.findIndex(ne=>ne&&(ne.id===r||ne.modal_id===r));if(K>=0){const ne=fe[K],me={...ne,props:{...ne.props??{},isOpen:!0}},ze=[...Array.isArray(ee.components)?ee.components:[],me],Se=[ze.length-1];l({layoutName:n,raw:{...ee,components:ze},lockVersion:re,modalContext:{modalId:r,modalIndex:K,hostRaw:ee,originalModalNode:ne,editIndexPath:Se}}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()});return}}if(o==="iteration_item"&&s){const ee=se,fe=Ze(s),K={children:ee.components??[]},ne=We(K,fe);if(ne&&Array.isArray(ne.children)){l({layoutName:n,raw:ee,lockVersion:re,iterationContext:{sourcePath:s,sourceIndexPath:fe,hostRaw:ee}}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()});return}}l({layoutName:n,raw:se,lockVersion:re}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()})}catch(P){f(io(P,"layout")),l(null),g(!1)}finally{c(!1)}},[t,n,o,r,s,_]);u.useEffect(()=>{$()},[$]);const A=u.useCallback(async()=>{S(_(o,n)),na(),b(L=>L+1),await $()},[S,_,o,n,$]),z=u.useCallback(()=>{S(_(o,n))},[S,_,o,n]),D=u.useCallback(L=>{l(I=>{if(!I)return I;const P=I.raw.components??[],F=L(P),G={...I.raw,components:F};return E(G,I.lockVersion),{...I,raw:G}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),O=u.useCallback(()=>{g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o]),M=u.useCallback(L=>{l(I=>{if(!I)return I;const P={...I.raw,components:L};return E(P,I.lockVersion),{...I,raw:P}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),B=u.useCallback((L,I,...P)=>{const F=P.length>=1?P[0]:I;l(G=>{if(!G)return G;const V={...G.raw,[L]:I},Y=G.raw.__editor;if(Y&&typeof Y=="object"&&!Array.isArray(Y)){const Z=Y,se=Z.original;se&&typeof se=="object"&&!Array.isArray(se)&&(V.__editor={...Z,original:{...se,[L]:F}})}return E(V,G.lockVersion),{...G,raw:V}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),N=u.useCallback(async L=>{const I=C.current;if(!I)return st({op:"save_guard_result",layoutName:n,editMode:o,guardBlocked:!1,timestamp:Date.now()}),{kind:"guard_no_document"};if(L?.resolveActiveExtensions&&L.sessionAddedPaths?.length)try{const fe=await L.resolveActiveExtensions(),K=o7(I.raw.components,L.sessionAddedPaths,fe);if(K.length>0)return st({op:"save_guard_result",layoutName:n,editMode:o,guardBlocked:!0,guardBlockedPaths:K,timestamp:Date.now()}),{kind:"blocked_inactive_extension",blockedPaths:K}}catch{}if(Hb())try{await Fb(t)}catch{}const P=`/api/admin/templates/${encodeURIComponent(t)}/layouts/${I.layoutName}`,F=ar(),G={Accept:"application/json","Content-Type":"application/json"};F&&(G.Authorization=`Bearer ${F}`);const V=!!I.modalContext,Y=!!I.iterationContext,Z=V||Y?"host_layout_modal_patch":"layout";st({op:"save",layoutName:I.layoutName,editMode:o,saveTarget:Z,endpoint:P,isDirty:y,timestamp:Date.now()});let se;if(V&&I.modalContext){const{modalIndex:fe,hostRaw:K,originalModalNode:ne,editIndexPath:me}=I.modalContext,Ce=Array.isArray(K.modals)?[...K.modals]:[],ze=Array.isArray(I.raw.components)?I.raw.components:[],Se=me&&me.length===1?me[0]:0,qe=ze.length>Se?ze[Se]:void 0,De=qe?Vn([qe]):[],Pe=De.length>0?{...De[0]}:void 0;if(fe>=0&&fe<Ce.length&&Pe){const Me=ne.props??{},Re={...Pe.props??{}};"isOpen"in Me?Re.isOpen=Me.isOpen:delete Re.isOpen,Pe.props=Re,Ce[fe]=Pe}se={...vo(K),modals:Ce}}else if(Y&&I.iterationContext){const{sourceIndexPath:fe,hostRaw:K}=I.iterationContext,ne=vo(I.raw),me=Array.isArray(ne.components)?ne.components:[],Ce=We({children:me},fe),ze=Array.isArray(Ce?.children)?Ce.children:[],Se=vo(K),De={children:Array.isArray(Se.components)?Se.components:[]},Qe=Ot(De,fe,Me=>({...Me,children:ze})).children??[];se={...Se,components:Qe}}else se=vo(I.raw);const re=se;let U;try{U=await fetch(P,{method:"PUT",headers:G,credentials:"same-origin",body:JSON.stringify({content:JSON.stringify(re),expected_lock_version:I.lockVersion})})}catch(fe){const K=fe instanceof Error?fe.message:"network error";return st({op:"save_response",layoutName:I.layoutName,editMode:o,endpoint:P,statusCode:0,timestamp:Date.now()}),{kind:"network_error",message:K}}const ee=await U.json().catch(()=>null);if(st({op:"save_response",layoutName:I.layoutName,editMode:o,endpoint:P,statusCode:U.status,conflict:U.status===409?{currentVersion:ee?.current_version,yourVersion:ee?.your_version}:void 0,timestamp:Date.now()}),U.ok){const fe=typeof ee?.data?.lock_version=="number"?ee.data.lock_version:I.lockVersion+1;l({...I,lockVersion:fe}),g(!1),S(_(o,I.layoutName)),T(I.layoutName),na(),h(ne=>ne+1);const K=typeof ee?.data?.current_version=="number"?ee.data.current_version:void 0;return{kind:"success",newLockVersion:fe,newContentVersion:K,savedLayoutName:I.layoutName}}return U.status===409?{kind:"concurrent_modification",currentVersion:ee?.current_version??-1,yourVersion:ee?.your_version??I.lockVersion}:U.status===422?{kind:"validation_failed",status:422,errors:ee?.errors??null}:{kind:"network_error",message:ee?.message??`HTTP ${U.status}`}},[t,n,o,y,S,T,_,r,s]),H=u.useMemo(()=>{const L=new Set,I="route|";for(const P of k)if(P.startsWith(I)){const F=P.slice(I.length);F&&L.add(F)}return L},[k]);return{document:a,isLoading:d,error:p,isDirty:y,saveSuccessCounter:m,reloadCounter:v,dirtyKeys:k,dirtyLayoutNames:H,invalidateCurrentCache:z,reload:A,patchLayout:D,setLayoutComponents:M,patchDocumentRaw:B,save:N,markDirty:O}}function o7(e,t,n){if(!e||e.length===0)return[];const o=[],r=new Set([...n.moduleIds,...n.pluginIds]);for(const s of t){const a=r7(e,s);if(!a)continue;const l=a.__source;if(!l||l.kind!=="extension")continue;const d=String(l.extensionId??"");d.length!==0&&(r.has(d)||o.push(s))}return o}function r7(e,t){const n=Ze(t);return n.length===0?null:We({children:e},n)}function so(e,t){if(!e)return null;const n=e.getBoundingClientRect();if(n.width===0&&n.height===0)return null;if(!t)return{left:n.left,top:n.top,width:n.width,height:n.height,scale:1};const o=t.getBoundingClientRect();return{left:n.left-o.left,top:n.top-o.top,width:n.width,height:n.height,scale:1}}function ao(e,t){if(!e)return!1;if(!t)return!0;const n=t.getBoundingClientRect(),o=1;return!(e.left>=n.width-o||e.left+e.width<=o||e.top>=n.height-o||e.top+e.height<=o)}function $p(e,t){if(typeof window>"u")return()=>{};const n=()=>{e()};window.addEventListener("scroll",n,!0),window.addEventListener("resize",n);let o=null;t&&typeof ResizeObserver<"u"&&(o=new ResizeObserver(()=>e()),o.observe(t));let r=null,s=null;const a=typeof requestAnimationFrame<"u",l=()=>{if(!a){e();return}s===null&&(s=requestAnimationFrame(()=>{s=null,e()}))};return t&&typeof MutationObserver<"u"&&(r=new MutationObserver(l),r.observe(t,{childList:!0,subtree:!0})),()=>{window.removeEventListener("scroll",n,!0),window.removeEventListener("resize",n),o&&(o.disconnect(),o=null),r&&(r.disconnect(),r=null),s!==null&&a&&typeof cancelAnimationFrame<"u"&&(cancelAnimationFrame(s),s=null)}}const Ep=44;function Ip(e){if(!e)return"inside";const t=e.scale||1,n=e.width*t,o=e.height*t;return n<Ep||o<Ep?"outside":"inside"}const i7=24,Ap=6;function Tp(e){const t=i7/2,n=e.width/2,o=e.height/2,r=t+Ap,s=2*t-Math.min(n,o)+Ap,a=Math.max(r,s);return{above:{left:n-t,top:-a-t},below:{left:n-t,top:e.height+a-t},left:{left:-a-t,top:o-t},right:{left:e.width+a-t,top:o-t}}}function Dp(e){if(!e||typeof window>"u"||typeof window.getComputedStyle!="function")return{kind:"unknown",display:"",flexDirection:"",flexWrap:""};let t=e,n=(window.getComputedStyle(t).display??"").toLowerCase();for(;n==="contents"&&t.parentElement;)t=t.parentElement,n=(window.getComputedStyle(t).display??"").toLowerCase();const o=window.getComputedStyle(t),r=(o.display??"").toLowerCase(),s=(o.flexDirection??"").toLowerCase(),a=(o.flexWrap??"").toLowerCase(),l=r==="flex"||r==="inline-flex";if(r==="grid"||r==="inline-grid"){const f=Array.from(e.children).filter(h=>h.offsetParent!==null||h.getClientRects().length>0);if(f.length<=1)return{kind:"grid_2d",display:r,flexDirection:s,flexWrap:a};const y=f.map(h=>h.getBoundingClientRect()),g=new Set(y.map(h=>Math.round(h.top))),m=new Set(y.map(h=>Math.round(h.left)));return g.size===1?{kind:"grid_single_row",display:r,flexDirection:s,flexWrap:a}:m.size===1?{kind:"grid_single_column",display:r,flexDirection:s,flexWrap:a}:{kind:"grid_2d",display:r,flexDirection:s,flexWrap:a}}if(!l)return{kind:"block",display:r,flexDirection:s,flexWrap:a};const c=a==="wrap"||a==="wrap-reverse";return s==="column"||s==="column-reverse"?{kind:c?"flex_column_wrap":"flex_column_single",display:r,flexDirection:s,flexWrap:a}:{kind:c?"flex_row_wrap":"flex_row_single",display:r,flexDirection:s,flexWrap:a}}function s7(e){const{parentEl:t,selectedPath:n,selectedEl:o,explicitFlow:r}=e;return u.useMemo(()=>{if(!n||n.length===0)return{points:[],flow:"unknown"};const s=r??Dp(t).kind,a=n.slice(0,-1),l=n[n.length-1]??0;if((s==="flex_row_wrap"||s==="flex_column_wrap"||s==="grid_2d")&&!!o){const p=a7(s,o,a,l);if(p)return{points:p,flow:s}}return{points:l7(s,a,l),flow:s}},[t,n,o,r])}function a7(e,t,n,o){let r=t.parentElement;for(;r&&Np(r)==="contents";)r=r.parentElement;if(!r)return null;const s=[];if(zp(r,s),s.length===0)return null;const a=t.getBoundingClientRect(),l=a.top+a.height/2,d=[],c=[],p=[];for(const v of s){const b=v.rect,x=b.top+b.height/2,k=Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top),w=Math.min(a.height,b.height)||1;k/w>.4?d.push({rect:b,path:v.path}):x<l?c.push({rect:b,path:v.path}):p.push({rect:b,path:v.path})}d.sort((v,b)=>v.rect.left-b.rect.left),c.sort((v,b)=>v.rect.top-b.rect.top),p.sort((v,b)=>v.rect.top-b.rect.top);const f=d.findIndex(v=>Math.abs(v.rect.left-a.left)<1&&Math.abs(v.rect.top-a.top)<1),y={direction:"above",disabled:!0,insertion:null},g={direction:"below",disabled:!0,insertion:null},m={direction:"left",disabled:!0,insertion:null},h={direction:"right",disabled:!0,insertion:null};if(f>0){const v=d[f-1],b=Xr(v.path);b&&(m.disabled=!1,m.insertion=b)}else if(d.length>0){const v=d[0],b=Yr(v.path);b&&(m.disabled=!1,m.insertion=b)}if(f>=0&&f<d.length-1){const v=d[f+1],b=Yr(v.path);b&&(h.disabled=!1,h.insertion=b)}else if(d.length>0){const v=d[d.length-1],b=Xr(v.path);b&&(h.disabled=!1,h.insertion=b)}if(c.length>0){const v=c[c.length-1],b=Xr(v.path);b&&(y.disabled=!1,y.insertion=b)}else if(s.length>0){const v=s[0],b=Yr(v.path);b&&(y.disabled=!1,y.insertion=b)}if(p.length>0){const v=p[0],b=Yr(v.path);b&&(g.disabled=!1,g.insertion=b)}else if(s.length>0){const v=s[s.length-1],b=Xr(v.path);b&&(g.disabled=!1,g.insertion=b)}else g.disabled=!1,g.insertion={parentPath:n,index:o+1};return y.disabled&&g.disabled&&m.disabled&&h.disabled?null:[y,g,m,h]}function Np(e){return typeof window>"u"?"":(window.getComputedStyle(e).display??"").toLowerCase()}function zp(e,t){const n=Array.from(e.children);for(const o of n){if(Np(o)==="contents"){zp(o,t);continue}const s=o.getAttribute("data-editor-path");if(s&&/\.iteration\.\d+(?:$|\.)/.test(s)||!s)continue;const a=o.getBoundingClientRect();a.width===0&&a.height===0||t.push({el:o,rect:a,path:Ze(s)})}}function Yr(e){if(!e||e.length===0)return null;const t=e[e.length-1];return typeof t!="number"?null:{parentPath:e.slice(0,-1),index:t}}function Xr(e){if(!e||e.length===0)return null;const t=e[e.length-1];return typeof t!="number"?null:{parentPath:e.slice(0,-1),index:t+1}}function l7(e,t,n){const o={direction:"above",disabled:!0,insertion:null},r={direction:"below",disabled:!0,insertion:null},s={direction:"left",disabled:!0,insertion:null},a={direction:"right",disabled:!0,insertion:null},l=d=>({parentPath:t,index:d});switch(e){case"block":case"unknown":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1);break}case"flex_row_single":case"grid_single_row":{s.disabled=!1,s.insertion=l(n),a.disabled=!1,a.insertion=l(n+1);break}case"flex_column_single":case"grid_single_column":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1);break}case"flex_row_wrap":case"grid_2d":{s.disabled=!1,s.insertion=l(n),a.disabled=!1,a.insertion=l(n+1),o.disabled=!1,o.insertion=l(0),r.disabled=!1,r.insertion=l(-1);break}case"flex_column_wrap":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1),s.disabled=!1,s.insertion=l(0),a.disabled=!1,a.insertion=l(-1);break}}return[o,r,s,a]}function d7(e=50){const t=u.useRef([]),n=u.useRef(-1),[,o]=u.useState(0),r=()=>{o(p=>(p+1)%1e6)},s=(p,f)=>{const y=t.current.length,g=n.current;Gh({op:p,actionKind:f?.actionKind,label:f?.label,stackSize:y,cursor:g,canUndo:g>0,canRedo:g>=0&&g<y-1,timestamp:Date.now()})},a=u.useCallback(p=>{const y=t.current.slice(0,n.current+1);for(y.push(p);y.length>e;)y.shift();t.current=y,n.current=y.length-1,r(),s("push",p)},[e]),l=u.useCallback(()=>{if(n.current<=0)return null;n.current-=1,r();const p=t.current[n.current]??null;return s("undo",p??void 0),p},[]),d=u.useCallback(()=>{const p=n.current+1;if(p>=t.current.length)return null;n.current=p,r();const f=t.current[p]??null;return s("redo",f??void 0),f},[]),c=u.useCallback(()=>{t.current=[],n.current=-1,r(),s("clear")},[]);return{push:a,undo:l,redo:d,clear:c,canUndo:n.current>0,canRedo:n.current>=0&&n.current<t.current.length-1}}function c7(e){if(!e||!(e instanceof HTMLElement))return!1;const t=e.tagName;return!!(t==="INPUT"||t==="TEXTAREA"||t==="SELECT"||e.isContentEditable)}function u7(e){const{onUndo:t,onRedo:n,enabled:o=!0}=e;u.useEffect(()=>{if(!o||typeof window>"u")return;const r=s=>{if(c7(s.target)||!(s.ctrlKey||s.metaKey))return;const l=s.key.toLowerCase();if(l==="z"&&!s.shiftKey){s.preventDefault(),t();return}if(l==="z"&&s.shiftKey||l==="y"){s.preventDefault(),n();return}};return window.addEventListener("keydown",r),()=>{window.removeEventListener("keydown",r)}},[t,n,o])}const p7=20,Mp=50,f7=90,Ln=120,g7=130,Lp=80,h7=9e3,Pp=1e4;function y7(e){const t=e<0?0:e>Mp?Mp:e;return p7+t}function b7(e){const{anchor:t,open:n,onClose:o,onEditProps:r,onDuplicate:s,onDelete:a,onSeparateBranch:l,separateBranchLabel:d,onMergeBranch:c,mergeBranchLabel:p}=e,{t:f}=tt(),[y,g]=u.useState(null);if(!n||!t)return null;const m=[{index:0,testid:"g7le-context-menu-edit-props",label:f("layout_editor.context_menu.edit_props"),onSelect:r},{index:1,testid:"g7le-context-menu-duplicate",label:f("layout_editor.context_menu.duplicate"),onSelect:s}];return l?m.push({index:m.length,testid:"g7le-context-menu-separate-branch",label:d??f("layout_editor.context_menu.separate_branch"),onSelect:l,hint:f("layout_editor.context_menu.separate_branch_hint")}):c&&m.push({index:m.length,testid:"g7le-context-menu-merge-branch",label:p??f("layout_editor.context_menu.merge_branch"),onSelect:c,hint:f("layout_editor.context_menu.merge_branch_hint")}),m.push({index:m.length,testid:"g7le-context-menu-delete",label:f("layout_editor.context_menu.delete"),onSelect:a,danger:!0}),i.jsx("div",{className:"g7le-context-menu","data-testid":"g7le-context-menu",role:"menu",style:{position:"absolute",left:t.left,top:t.top,background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:1200,minWidth:160,padding:4,pointerEvents:"auto"},onMouseLeave:()=>{g(null),o()},children:m.map(h=>{const v=y===h.index,b=h.danger?"#dc2626":"#0f172a",x=h.danger?"#fee2e2":"#e0e7ff",k=h.danger?"#b91c1c":"#1d4ed8";return i.jsx("button",{type:"button",role:"menuitem","data-testid":h.testid,title:h.hint,onClick:w=>{w.stopPropagation(),h.onSelect(),g(null),o()},onMouseEnter:()=>g(h.index),onFocus:()=>g(h.index),style:{...m7,color:v?k:b,background:v?x:"transparent"},children:h.label},h.testid)})})}const m7={display:"block",width:"100%",textAlign:"left",padding:"6px 12px",border:"none",fontSize:13,cursor:"pointer",borderRadius:4,pointerEvents:"auto",transition:"background-color 120ms ease, color 120ms ease"};function v7(e){const{hoverBox:t,selectedBox:n,lockKind:o,navAffordance:r,onEditProps:s,onDuplicate:a,onDelete:l,onSeparateBranch:d,separateBranchLabel:c,onMergeBranch:p,mergeBranchLabel:f,definedDeviceBranches:y,onJumpToDevice:g,onLinkEditDestination:m,onEditBase:h,onEditExtension:v,extensionLabel:b,baseLayoutLabel:x,dataSourceLabel:k,branchLabel:w,onEditIteration:_,resizeEnabledAxes:C,onResizeHandlePointerDown:E,selectedName:S,onSelectParent:T}=e,{t:$}=tt(),[A,z]=u.useState(!1),D=_p(o),O=A&&D,M=Ip(n),B=M==="outside",N=B&&n?Tp(n):null,H=N?{left:N.right.left,top:N.above.top}:{right:-30,top:-30},L={top:-28,left:0},P=B&&!!S?{top:-50,left:0}:L,F=N?{top:N.below.top+28,left:0}:{top:"auto",bottom:-28},G=N?{top:N.below.top+56,left:0}:{top:"auto",bottom:-56};return i.jsxs("div",{className:"g7le-element-overlay","data-testid":"g7le-element-overlay",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[t&&i.jsx("div",{"data-testid":"g7le-overlay-hover",style:{position:"absolute",left:t.left,top:t.top,width:t.width,height:t.height,border:"1px dashed #2563eb",pointerEvents:"none"}}),n&&i.jsxs("div",{"data-testid":"g7le-overlay-selected","data-lock-kind":o,style:{position:"absolute",left:n.left,top:n.top,width:n.width,height:n.height,border:o==="none"?"2px solid #2563eb":"2px dashed #f59e0b",pointerEvents:"none"},children:[w?i.jsxs("span",{"data-testid":"g7le-overlay-branch-badge",style:x7,children:["📱 ",w]}):null,S&&(T?i.jsxs("button",{type:"button","data-testid":"g7le-overlay-type-label","data-placement":B?"outside":"inside",title:$("layout_editor.overlay.select_parent"),"aria-label":$("layout_editor.overlay.select_parent"),onClick:V=>{V.preventDefault(),V.stopPropagation(),T()},style:B?k7:w7,children:[i.jsx("span",{"aria-hidden":!0,style:{marginRight:3,opacity:.85},children:"↑"}),S]}):i.jsx("span",{"data-testid":"g7le-overlay-type-label","data-placement":B?"outside":"inside",style:B?Bp:Op,children:S})),D&&i.jsx("button",{type:"button","data-testid":"g7le-overlay-info-button","aria-label":"info",onClick:V=>{V.stopPropagation(),z(Y=>!Y)},"data-placement":M,style:{position:"absolute",...H,width:20,height:20,borderRadius:10,border:"1px solid #2563eb",background:"#fff",color:"#2563eb",fontSize:12,fontWeight:700,cursor:"pointer",pointerEvents:"auto",padding:0,lineHeight:"18px",zIndex:Ln},children:"ⓘ"}),o==="none"&&i.jsx(_7,{enabledAxes:C,onHandlePointerDown:E}),o==="base"&&h&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-base",onClick:V=>{V.stopPropagation(),h()},style:{...Po,...L},children:["🔒 ",$("layout_editor.overlay.edit_common_layout"),x?i.jsx("span",{"data-testid":"g7le-overlay-base-layout-label",style:ra,children:x}):null]}),o==="extension"&&v&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-extension",onClick:V=>{V.stopPropagation(),v()},style:{...Po,...L},children:["🔒 ",$("layout_editor.overlay.edit_extension"),b?i.jsx("span",{"data-testid":"g7le-overlay-extension-label",style:ra,children:b}):null]}),o==="data_bound"&&i.jsxs("span",{"data-testid":"g7le-overlay-data-bound-notice",style:{...Po,...P,pointerEvents:"none",cursor:"default"},children:[$("layout_editor.overlay.locked_data_bound"),k?i.jsx("span",{"data-testid":"g7le-overlay-data-source-label",style:ra,children:k}):null]}),o==="data_bound"&&_&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-iteration",onClick:V=>{V.stopPropagation(),_()},style:{...Po,...F},children:["↳ ",$("layout_editor.overlay.edit_iteration_item")]}),r==="route_in_tree"&&i.jsx("button",{type:"button","data-testid":"g7le-overlay-link-edit-destination",onClick:V=>{V.stopPropagation(),m()},style:{...Po,...F},children:$("layout_editor.overlay.edit_destination_route")}),(d||p||g&&(y??[]).length>0)&&i.jsxs("div",{"data-testid":"g7le-overlay-branch-affordances",style:{position:"absolute",...G,display:"flex",flexDirection:"column",alignItems:"flex-start",gap:4,pointerEvents:"none"},children:[g&&(y??[]).map(V=>i.jsxs("button",{type:"button","data-testid":`g7le-overlay-jump-device-${V.key}`,title:$("layout_editor.overlay.jump_device_hint",{device:V.label}),onClick:Y=>{Y.stopPropagation(),g(V.key)},style:oa,children:["↗ ",$("layout_editor.overlay.jump_device",{device:V.label})]},V.key)),d&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-separate-branch",title:$("layout_editor.context_menu.separate_branch_hint"),onClick:V=>{V.stopPropagation(),d()},style:oa,children:["📱 ",c??$("layout_editor.context_menu.separate_branch")]}),p&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-merge-branch",title:$("layout_editor.context_menu.merge_branch_hint"),onClick:V=>{V.stopPropagation(),p()},style:oa,children:["↩️ ",f??$("layout_editor.context_menu.merge_branch")]})]})]}),i.jsx(b7,{anchor:n&&O?{left:n.left+n.width+8,top:n.top-4}:null,open:O,onClose:()=>z(!1),onEditProps:s,onDuplicate:a,onDelete:l,onSeparateBranch:d,separateBranchLabel:c,onMergeBranch:p,mergeBranchLabel:f})]})}function _7(e){const{enabledAxes:t,onHandlePointerDown:n}=e,o=t?.width??!1,r=t?.height??!1,s=[{key:"nw",style:{left:-4,top:-4},cursor:"nwse-resize",needsWidth:!0,needsHeight:!0},{key:"n",style:{left:"50%",top:-4,transform:"translateX(-50%)"},cursor:"ns-resize",needsWidth:!1,needsHeight:!0},{key:"ne",style:{right:-4,top:-4},cursor:"nesw-resize",needsWidth:!0,needsHeight:!0},{key:"e",style:{right:-4,top:"50%",transform:"translateY(-50%)"},cursor:"ew-resize",needsWidth:!0,needsHeight:!1},{key:"se",style:{right:-4,bottom:-4},cursor:"nwse-resize",needsWidth:!0,needsHeight:!0},{key:"s",style:{left:"50%",bottom:-4,transform:"translateX(-50%)"},cursor:"ns-resize",needsWidth:!1,needsHeight:!0},{key:"sw",style:{left:-4,bottom:-4},cursor:"nesw-resize",needsWidth:!0,needsHeight:!0},{key:"w",style:{left:-4,top:"50%",transform:"translateY(-50%)"},cursor:"ew-resize",needsWidth:!0,needsHeight:!1}];return i.jsx(i.Fragment,{children:s.map(a=>{const l=!a.needsWidth||o,d=!a.needsHeight||r,c=l&&d&&!!n;return!l||!d?null:i.jsx("span",{"data-testid":`g7le-resize-handle-${a.key}`,"data-resize-handle-active":c?"true":"false",onPointerDown:c?p=>{p.preventDefault(),p.stopPropagation(),n?.(a.key,{clientX:p.clientX,clientY:p.clientY})}:void 0,style:{position:"absolute",...a.style,width:8,height:8,background:"#fff",border:"1px solid #2563eb",cursor:a.cursor,pointerEvents:c?"auto":"none",zIndex:Ln}},a.key)})})}const Po={position:"absolute",top:-28,left:0,background:"#fff",border:"1px solid #2563eb",color:"#2563eb",padding:"2px 6px",fontSize:11,borderRadius:4,cursor:"pointer",pointerEvents:"auto",whiteSpace:"nowrap",zIndex:Ln},oa={background:"#fff",border:"1px solid #2563eb",color:"#2563eb",padding:"2px 6px",fontSize:11,borderRadius:4,cursor:"pointer",pointerEvents:"auto",whiteSpace:"nowrap",zIndex:Ln},ra={marginLeft:6,padding:"0 5px",borderRadius:3,background:"#eef2ff",border:"1px solid #c7d2fe",fontSize:10,fontWeight:600,color:"#3730a3"},x7={position:"absolute",top:0,right:0,transform:"translateY(-100%)",background:"rgba(124,58,237,0.92)",color:"#fff",padding:"1px 6px",borderRadius:"3px 3px 0 0",fontSize:10,fontWeight:600,whiteSpace:"nowrap",pointerEvents:"none"},Op={position:"absolute",top:0,left:0,background:"rgba(37,99,235,0.55)",color:"#fff",padding:"1px 6px",fontSize:10,fontWeight:600,lineHeight:"14px",borderBottomRightRadius:4,letterSpacing:"0.02em",whiteSpace:"nowrap",pointerEvents:"none",userSelect:"none",maxWidth:"100%",overflow:"hidden",textOverflow:"ellipsis"},Bp={position:"absolute",bottom:"100%",left:0,marginBottom:2,background:"rgba(37,99,235,0.55)",color:"#fff",padding:"1px 6px",fontSize:10,fontWeight:600,lineHeight:"14px",borderRadius:4,letterSpacing:"0.02em",whiteSpace:"nowrap",pointerEvents:"none",userSelect:"none"},w7={...Op,pointerEvents:"auto",cursor:"pointer",background:"rgba(37,99,235,0.82)",border:"none",display:"inline-flex",alignItems:"center",font:"inherit",fontSize:10,fontWeight:600,zIndex:Ln},k7={...Bp,pointerEvents:"auto",cursor:"pointer",background:"rgba(37,99,235,0.82)",border:"none",display:"inline-flex",alignItems:"center",font:"inherit",fontSize:10,fontWeight:600,zIndex:Ln},S7={above:{top:-30,left:"50%",transform:"translateX(-50%)"},below:{bottom:-30,left:"50%",transform:"translateX(-50%)"},left:{left:-30,top:"50%",transform:"translateY(-50%)"},right:{right:-30,top:"50%",transform:"translateY(-50%)"}};function C7(e){const{selectedBox:t,points:n,onAddRequest:o}=e,{t:r}=tt();if(!t||n.length===0)return null;const s=Ip(t),l=s==="outside"?Tp(t):null;return i.jsx("div",{className:"g7le-insertion-affordances","data-testid":"g7le-insertion-affordances",style:{position:"absolute",left:t.left,top:t.top,width:t.width,height:t.height,pointerEvents:"none"},children:n.map(d=>{const c=l?{left:l[d.direction].left,top:l[d.direction].top}:S7[d.direction],p=r(`layout_editor.insertion.${d.direction}`);return i.jsx("button",{type:"button",disabled:d.disabled,"data-testid":`g7le-insertion-${d.direction}`,"data-disabled":d.disabled?"true":"false","data-placement":s,"aria-label":p,title:p,onClick:f=>{f.stopPropagation(),d.disabled||o(d)},style:{position:"absolute",...c,width:24,height:24,borderRadius:12,border:"1px solid #2563eb",background:d.disabled?"#cbd5e1":"#fff",color:d.disabled?"#94a3b8":"#2563eb",cursor:d.disabled?"not-allowed":"pointer",opacity:d.disabled?.4:1,fontSize:16,fontWeight:700,lineHeight:"20px",padding:0,pointerEvents:"auto",boxShadow:"0 1px 3px rgba(0,0,0,0.15)",zIndex:Ln},children:"+"},d.direction)})})}const Jr=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function lo(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function ia(e){return"nodeType"in e}function _t(e){var t,n;return e?lo(e)?e:ia(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function sa(e){const{Document:t}=_t(e);return e instanceof t}function Oo(e){return lo(e)?!1:e instanceof _t(e).HTMLElement}function Wp(e){return e instanceof _t(e).SVGElement}function co(e){return e?lo(e)?e.document:ia(e)?sa(e)?e:Oo(e)||Wp(e)?e.ownerDocument:document:document:document}const sn=Jr?u.useLayoutEffect:u.useEffect;function Zr(e){const t=u.useRef(e);return sn(()=>{t.current=e}),u.useCallback(function(){for(var n=arguments.length,o=new Array(n),r=0;r<n;r++)o[r]=arguments[r];return t.current==null?void 0:t.current(...o)},[])}function $7(){const e=u.useRef(null),t=u.useCallback((o,r)=>{e.current=setInterval(o,r)},[]),n=u.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function Bo(e,t){t===void 0&&(t=[e]);const n=u.useRef(e);return sn(()=>{n.current!==e&&(n.current=e)},t),n}function Wo(e,t){const n=u.useRef();return u.useMemo(()=>{const o=e(n.current);return n.current=o,o},[...t])}function Qr(e){const t=Zr(e),n=u.useRef(null),o=u.useCallback(r=>{r!==n.current&&t?.(r,n.current),n.current=r},[]);return[n,o]}function Rr(e){const t=u.useRef();return u.useEffect(()=>{t.current=e},[e]),t.current}let aa={};function ei(e,t){return u.useMemo(()=>{if(t)return t;const n=aa[e]==null?0:aa[e]+1;return aa[e]=n,e+"-"+n},[e,t])}function jp(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return o.reduce((s,a)=>{const l=Object.entries(a);for(const[d,c]of l){const p=s[d];p!=null&&(s[d]=p+e*c)}return s},{...t})}}const uo=jp(1),ti=jp(-1);function E7(e){return"clientX"in e&&"clientY"in e}function la(e){if(!e)return!1;const{KeyboardEvent:t}=_t(e.target);return t&&e instanceof t}function I7(e){if(!e)return!1;const{TouchEvent:t}=_t(e.target);return t&&e instanceof t}function ni(e){if(I7(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return E7(e)?{x:e.clientX,y:e.clientY}:null}const jo=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[jo.Translate.toString(e),jo.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:o}=e;return t+" "+n+"ms "+o}}}),Hp="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function A7(e){return e.matches(Hp)?e:e.querySelector(Hp)}const T7={display:"none"};function D7(e){let{id:t,value:n}=e;return u.createElement("div",{id:t,style:T7},n)}function N7(e){let{id:t,announcement:n,ariaLiveType:o="assertive"}=e;const r={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return u.createElement("div",{id:t,style:r,role:"status","aria-live":o,"aria-atomic":!0},n)}function z7(){const[e,t]=u.useState("");return{announce:u.useCallback(o=>{o!=null&&t(o)},[]),announcement:e}}const Fp=u.createContext(null);function M7(e){const t=u.useContext(Fp);u.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function L7(){const[e]=u.useState(()=>new Set),t=u.useCallback(o=>(e.add(o),()=>e.delete(o)),[e]);return[u.useCallback(o=>{let{type:r,event:s}=o;e.forEach(a=>{var l;return(l=a[r])==null?void 0:l.call(a,s)})},[e]),t]}const P7={draggable:` +`);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":`g7le-version-diff-hunk-${p}`,children:[i.jsx("div",{style:M$,"data-testid":`g7le-version-diff-hunk-header-${p}`,children:c.header}),c.lines.map((f,y)=>{const g=L$(f.kind);return i.jsxs("div",{style:{display:"flex",alignItems:"stretch",...g.row},"data-kind":f.kind,"data-testid":`g7le-version-diff-line-${p}-${y}`,children:[i.jsx("span",{style:{...Zu,...g.gutter},"aria-hidden":"true",children:f.oldLine??""}),i.jsx("span",{style:{...Zu,...g.gutter},"aria-hidden":"true",children:f.newLine??""}),i.jsx("span",{style:{display:"inline-block",width:16,textAlign:"center",color:g.signColor,userSelect:"none",fontWeight:700},"aria-hidden":"true",children:g.sign}),i.jsx("span",{style:{whiteSpace:"pre-wrap",wordBreak:"break-word",flex:1,padding:"0 4px"},children:f.content})]},y)})]},p))})]})}const O$={display:"flex",flexDirection:"column",height:"100%",minHeight:0},B$={display:"flex",alignItems:"center",gap:8,padding:"12px 16px",borderBottom:"1px solid #e2e8f0",background:"#f8fafc"},W$={marginLeft:"auto",border:"none",background:"transparent",fontSize:16,color:"#64748b",cursor:"pointer",outline:"none"},j$={flex:1,minHeight:0,overflowY:"auto",padding:12},Qu={padding:"24px 12px",textAlign:"center",color:"#94a3b8",fontSize:13},H$={margin:"8px 16px 0",padding:"8px 12px",borderRadius:6,background:"#fef2f2",color:"#b91c1c",fontSize:12,border:"1px solid #fecaca"},F$={display:"flex",flexDirection:"column",gap:8},K$={display:"flex",alignItems:"center",gap:12,padding:"10px 12px",borderRadius:8,border:"1px solid #e2e8f0",background:"#ffffff"},V$={display:"inline-flex",alignItems:"center",justifyContent:"center",minWidth:44,padding:"2px 8px",borderRadius:999,background:"#eff6ff",color:"#1d4ed8",fontSize:12,fontWeight:700},U$={display:"inline-flex",alignItems:"center",padding:"1px 8px",borderRadius:999,background:"#0f172a",color:"#ffffff",fontSize:10,fontWeight:700,letterSpacing:.3},q$={display:"flex",alignItems:"center",gap:10,fontSize:11,fontWeight:600},G$={display:"flex",flexDirection:"column",gap:2,minWidth:0,flex:1},Ru={padding:"6px 12px",borderRadius:6,border:"1px solid #cbd5e1",background:"#ffffff",color:"#0f172a",fontSize:12,fontWeight:600,cursor:"pointer",outline:"none",whiteSpace:"nowrap"};function ep(e){return e.full_content&&typeof e.full_content=="object"?e.full_content:{endpoint:e.endpoint,components:e.components,data_sources:e.data_sources,metadata:e.metadata}}function Y$(e){if(!e)return"";const t=Date.parse(e);if(Number.isNaN(t))return e;try{return new Date(t).toLocaleString()}catch{return e}}function X$({templateIdentifier:e,target:t,t:n,onRestored:o,onClose:r}){const{versions:s,isLoading:a,isLoadingMore:l,error:d,restoringId:c,hasMore:p,loadVersions:f,loadMore:y,restore:g,loadVersionDetail:m}=k$(e,t,o),[h,v]=u.useState(null),[b,x]=u.useState(null),[k,w]=u.useState(null);u.useEffect(()=>{f()},[f]);const _=u.useCallback(async S=>{v(null),w(S.version);try{const T=await m(S.version);if(T.kind!=="success"){v(T.kind==="not_found"?n("layout_editor.version_history.restore_not_found"):T.message);return}const $=S.version-1,A=s.some(D=>D.version===$);let z={};if(A){const D=await m($);if(D.kind==="success")z=ep(D.detail);else if(D.kind==="network_error"){v(D.message);return}}x({oldVersion:A?$:0,newVersion:S.version,oldContent:z,newContent:ep(T.detail)})}finally{w(null)}},[m,s,n]),C=u.useCallback(async S=>{v(null);const T=n("layout_editor.version_history.restore_confirm");if(!(typeof window<"u"?window.confirm(T):!0))return;const A=await g(S);A.kind==="success"?r():A.kind==="not_found"?v(n("layout_editor.version_history.restore_not_found")):v(A.message)},[g,r,n]),E=h??d;return b?i.jsx(P$,{oldVersion:b.oldVersion,newVersion:b.newVersion,oldContent:b.oldContent,newContent:b.newContent,t:n,onBack:()=>x(null),onClose:r}):i.jsxs("div",{className:"g7le-version-history","data-testid":"g7le-version-history",style:O$,children:[i.jsxs("div",{"data-modal-drag-handle":!0,style:B$,children:[i.jsxs("span",{style:{fontSize:14,fontWeight:700},"data-testid":"g7le-version-history-title",children:["🕘 ",n("layout_editor.version_history.title")]}),i.jsx("button",{type:"button","aria-label":n("layout_editor.version_history.close"),onClick:r,style:W$,"data-testid":"g7le-version-history-close",children:"✕"})]}),E&&i.jsx("div",{style:H$,"data-testid":"g7le-version-history-error",children:E}),i.jsx("div",{style:j$,children:a?i.jsx("div",{style:Qu,"data-testid":"g7le-version-history-loading",children:n("layout_editor.version_history.loading")}):s.length===0?i.jsx("div",{style:Qu,"data-testid":"g7le-version-history-empty",children:n("layout_editor.version_history.empty")}):i.jsxs("div",{"data-testid":"g7le-version-history-list",style:F$,children:[s.map((S,T)=>{const $=S.changes_summary,A=$?.added_count??0,z=$?.removed_count??0,D=$?.char_diff??0,O=Y$(S.created_at),M=c===S.id,B=T===0;return i.jsxs("div",{style:K$,"data-testid":`g7le-version-row-${S.id}`,children:[i.jsxs("span",{style:V$,"data-testid":`g7le-version-badge-${S.id}`,children:["v",S.version]}),i.jsxs("div",{style:G$,children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:8},children:[O&&i.jsx("span",{style:{fontSize:12,color:"#475569",fontWeight:600},children:O}),B&&i.jsx("span",{style:U$,"data-testid":`g7le-version-latest-${S.id}`,children:n("layout_editor.version_history.latest")})]}),i.jsxs("span",{style:q$,"data-testid":`g7le-version-summary-${S.id}`,children:[i.jsx("span",{style:{color:"#16a34a"},"data-testid":`g7le-version-added-${S.id}`,children:n("layout_editor.version_history.added",{count:A})}),i.jsx("span",{style:{color:"#dc2626"},"data-testid":`g7le-version-removed-${S.id}`,children:n("layout_editor.version_history.removed",{count:z})}),i.jsx("span",{style:{color:"#94a3b8",fontWeight:500},"data-testid":`g7le-version-chars-${S.id}`,children:n("layout_editor.version_history.chars",{diff:`${D>=0?"+":""}${D}`})})]}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},"data-testid":`g7le-version-author-${S.id}`,children:n("layout_editor.version_history.saved_by",{name:S.created_by_name??n("layout_editor.version_history.unknown_author")})})]}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6},children:[i.jsx("button",{type:"button",onClick:()=>{_(S)},disabled:k!==null,style:{...Ru,background:"#f8fafc",opacity:k!==null?.6:1},"data-testid":`g7le-version-compare-${S.id}`,children:k===S.version?n("layout_editor.version_history.comparing"):n("layout_editor.version_history.compare")}),!B&&i.jsx("button",{type:"button",onClick:()=>{C(S.id)},disabled:M||c!==null,style:{...Ru,opacity:M||c!==null?.6:1},"data-testid":`g7le-version-restore-${S.id}`,children:n(M?"layout_editor.version_history.restoring":"layout_editor.version_history.restore")})]})]},S.id)}),p&&i.jsx("button",{type:"button","data-testid":"g7le-version-history-load-more",style:J$,disabled:l,onClick:()=>{y()},children:n(l?"layout_editor.version_history.loading":"layout_editor.version_history.load_more")})]})})]})}const J$={width:"100%",marginTop:8,padding:"8px 12px",fontSize:12,color:"#475569",background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:6,cursor:"pointer"},tp=[{id:"copy",group:"clipboard",combos:[{key:"c",mod:!0}],labelKey:"layout_editor.shortcuts.action.copy",requiresSelection:!0},{id:"cut",group:"clipboard",combos:[{key:"x",mod:!0}],labelKey:"layout_editor.shortcuts.action.cut",requiresSelection:!0},{id:"paste",group:"clipboard",combos:[{key:"v",mod:!0}],labelKey:"layout_editor.shortcuts.action.paste"},{id:"openProps",group:"element",combos:[{key:"enter"}],labelKey:"layout_editor.shortcuts.action.open_props",requiresSelection:!0},{id:"delete",group:"element",combos:[{key:"delete"},{key:"backspace"}],labelKey:"layout_editor.shortcuts.action.delete",requiresSelection:!0},{id:"selectParent",group:"element",combos:[{key:"arrowup"}],labelKey:"layout_editor.shortcuts.action.select_parent",requiresSelection:!0},{id:"deselect",group:"element",combos:[{key:"escape"}],labelKey:"layout_editor.shortcuts.action.deselect"},{id:"addElement",group:"element",combos:[{key:"/",mod:!0}],labelKey:"layout_editor.shortcuts.action.add_element"},{id:"undo",group:"history",combos:[{key:"z",mod:!0}],labelKey:"layout_editor.shortcuts.action.undo"},{id:"redo",group:"history",combos:[{key:"z",mod:!0,shift:!0},{key:"y",mod:!0}],labelKey:"layout_editor.shortcuts.action.redo"},{id:"editCode",group:"view",combos:[{key:"e",mod:!0}],labelKey:"layout_editor.shortcuts.action.edit_code"},{id:"preview",group:"view",combos:[{key:"p",mod:!0}],labelKey:"layout_editor.shortcuts.action.preview"},{id:"translations",group:"view",combos:[{key:"l",mod:!0}],labelKey:"layout_editor.shortcuts.action.translations"},{id:"help",group:"view",combos:[{key:"?"}],labelKey:"layout_editor.shortcuts.action.help"},{id:"reset",group:"document",combos:[{key:"r",mod:!0,shift:!0}],labelKey:"layout_editor.shortcuts.action.reset"},{id:"save",group:"document",combos:[{key:"s",mod:!0}],labelKey:"layout_editor.shortcuts.action.save"},{id:"exit",group:"document",combos:[{key:"escape"}],labelKey:"layout_editor.shortcuts.action.exit"}];function Z$(e){return{key:(e.key||"").toLowerCase(),mod:e.ctrlKey||e.metaKey,shift:e.shiftKey,alt:e.altKey}}function Q$(e,t){return e.key===t.key&&!!e.mod==!!t.mod&&!!e.shift==!!t.shift&&!!e.alt==!!t.alt}function R$(e){for(const t of tp)if(t.combos.some(n=>Q$(n,e)))return t;return null}function eE(e,t){const n=[];return e.mod&&n.push(t?"⌘":"Ctrl"),e.shift&&n.push(t?"⇧":"Shift"),e.alt&&n.push(t?"⌥":"Alt"),n.push(tE(e.key)),n.join(" + ")}function tE(e){return{arrowup:"↑",arrowdown:"↓",arrowleft:"←",arrowright:"→",enter:"Enter",escape:"Esc",delete:"Delete",backspace:"Backspace"," ":"Space"}[e]??(e.length===1?e.toUpperCase():e)}const nE=["clipboard","element","history","view","document"],oE={clipboard:"layout_editor.shortcuts.group.clipboard",element:"layout_editor.shortcuts.group.element",history:"layout_editor.shortcuts.group.history",view:"layout_editor.shortcuts.group.view",document:"layout_editor.shortcuts.group.document"};function rE(){return typeof navigator>"u"?!1:/mac|iphone|ipad|ipod/i.test(navigator.platform||navigator.userAgent||"")}function iE({t:e,onClose:t,isMac:n}){const o=n??rE(),r=u.useMemo(()=>{const s=new Map;for(const a of tp){const l=s.get(a.group)??[];l.push(a),s.set(a.group,l)}return s},[]);return i.jsxs("div",{className:"g7le-shortcut-help","data-testid":"g7le-shortcut-help",style:sE,children:[i.jsxs("div",{style:aE,children:[i.jsx("span",{style:lE,children:e("layout_editor.shortcuts.title")}),i.jsx("button",{type:"button","data-testid":"g7le-shortcut-help-close",onClick:t,"aria-label":e("layout_editor.shortcuts.close"),style:dE,children:"✕"})]}),i.jsx("div",{style:cE,children:nE.map(s=>{const a=r.get(s);return!a||a.length===0?null:i.jsxs("div",{style:uE,"data-testid":`g7le-shortcut-group-${s}`,children:[i.jsx("div",{style:pE,children:e(oE[s])}),i.jsx("table",{style:fE,children:i.jsx("tbody",{children:a.map(l=>i.jsxs("tr",{"data-testid":`g7le-shortcut-row-${l.id}`,children:[i.jsx("td",{style:gE,children:e(l.labelKey)}),i.jsx("td",{style:hE,children:l.combos.map((d,c)=>i.jsxs(u.Fragment,{children:[c>0&&i.jsx("span",{style:bE,children:e("layout_editor.shortcuts.or")}),i.jsx("kbd",{style:yE,children:eE(d,o)})]},c))})]},l.id))})})]},s)})}),i.jsx("div",{style:mE,children:e("layout_editor.shortcuts.footnote")})]})}const sE={display:"flex",flexDirection:"column",padding:20,gap:16,width:"100%",boxSizing:"border-box"},aE={display:"flex",alignItems:"center",justifyContent:"space-between",paddingBottom:12,borderBottom:"1px solid #e2e8f0"},lE={fontSize:15,fontWeight:700,color:"#0f172a"},dE={width:28,height:28,border:"none",background:"transparent",color:"#64748b",cursor:"pointer",fontSize:14,borderRadius:6},cE={display:"grid",gridTemplateColumns:"1fr 1fr",columnGap:28,rowGap:18,alignItems:"start"},uE={display:"flex",flexDirection:"column",gap:6},pE={fontSize:12,fontWeight:700,color:"#2563eb"},fE={width:"100%",borderCollapse:"collapse"},gE={padding:"4px 0",fontSize:12.5,color:"#334155",textAlign:"left",whiteSpace:"nowrap",verticalAlign:"middle"},hE={padding:"4px 0",paddingLeft:12,textAlign:"right",verticalAlign:"middle"},yE={display:"inline-block",padding:"1px 6px",fontSize:11,fontFamily:"ui-monospace, monospace",color:"#0f172a",background:"#f1f5f9",border:"1px solid #cbd5e1",borderBottomWidth:2,borderRadius:5,whiteSpace:"nowrap"},bE={fontSize:11,color:"#94a3b8",margin:"0 4px"},mE={fontSize:11,color:"#94a3b8",paddingTop:12,borderTop:"1px solid #f1f5f9"};function vE(e){if(!e||!(e instanceof HTMLElement))return!1;const t=e.tagName;return!!(t==="INPUT"||t==="TEXTAREA"||t==="SELECT"||e.isContentEditable)}function _E(){return typeof document>"u"?!1:!!document.querySelector(".g7le-modal-backdrop, .g7le-modal")}function np(e){const{handlers:t,hasSelection:n,enabled:o=!0}=e;u.useEffect(()=>{if(!o||typeof window>"u")return;const r=s=>{if(vE(s.target))return;const a=Z$(s),l=R$(a);if(!l||_E()||l.requiresSelection&&!n)return;if(l.key==="escape"||a.key==="escape"){if(n){const c=t.deselect;c&&(s.preventDefault(),c())}else{const c=t.exit;c&&(s.preventDefault(),c())}return}const d=t[l.id];d&&(s.preventDefault(),d())};return window.addEventListener("keydown",r),()=>window.removeEventListener("keydown",r)},[t,n,o])}function xE(e){const{result:t,onDismiss:n,onLoadLatest:o,onKeepMyChanges:r}=e,{t:s}=tt();if(u.useEffect(()=>{if(!t||t.kind==="concurrent_modification"||t.kind==="validation_failed")return;const a=setTimeout(n,5e3);return()=>clearTimeout(a)},[t,n]),!t)return null;if(t.kind==="success")return i.jsx(Lo,{tone:"success",testId:"g7le-save-banner-success",onDismiss:n,children:s("layout_editor.save.success")});if(t.kind==="validation_failed"){const a=t.errors??{},l=Object.entries(a);return i.jsxs(Lo,{tone:"error",testId:"g7le-save-banner-validation",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.validation_failed")}),l.length>0&&i.jsx("ul",{style:op,"data-testid":"g7le-save-banner-validation-errors",children:l.map(([d,c])=>i.jsxs("li",{children:[i.jsx("code",{style:rp,children:d}),": ",c.join(", ")]},d))})]})}return t.kind==="blocked_inactive_extension"?i.jsxs(Lo,{tone:"warning",testId:"g7le-save-banner-blocked",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.blocked_inactive_extension")}),t.blockedPaths.length>0&&i.jsx("ul",{style:op,"data-testid":"g7le-save-banner-blocked-paths",children:t.blockedPaths.map(a=>i.jsx("li",{children:i.jsx("code",{style:rp,children:a})},a))})]}):t.kind==="network_error"?i.jsxs(Lo,{tone:"error",testId:"g7le-save-banner-network",onDismiss:n,children:[i.jsx("strong",{children:s("layout_editor.save.network_error_title")}),i.jsx("div",{style:{marginTop:4,fontSize:12},children:t.message})]}):t.kind==="guard_no_document"?i.jsx(Lo,{tone:"neutral",testId:"g7le-save-banner-guard-no-document",onDismiss:n,children:s("layout_editor.save.guard_no_document")}):i.jsx("div",{role:"dialog","aria-modal":"true","aria-label":s("layout_editor.save.concurrent.title"),"data-testid":"g7le-save-banner-concurrent",style:CE,children:i.jsxs("div",{style:$E,children:[i.jsx("h2",{style:EE,children:s("layout_editor.save.concurrent.title")}),i.jsx("p",{style:IE,children:s("layout_editor.save.concurrent.message")}),i.jsx("div",{style:AE,children:s("layout_editor.save.concurrent.version_info",{current:String(t.currentVersion),yours:String(t.yourVersion)})}),i.jsxs("div",{style:TE,children:[i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-load-latest",onClick:()=>{o?.(),n()},style:DE,children:s("layout_editor.save.concurrent.load_latest")}),i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-keep-mine",onClick:()=>{r?.(),n()},style:NE,children:s("layout_editor.save.concurrent.view_my_changes")}),i.jsx("button",{type:"button","data-testid":"g7le-save-banner-concurrent-cancel",onClick:n,style:zE,children:s("layout_editor.save.concurrent.cancel")})]})]})})}function Lo(e){const{tone:t,testId:n,onDismiss:o,children:r}=e,s=wE[t];return i.jsxs("div",{role:"status","data-testid":n,style:{...kE,background:s.bg,borderColor:s.border,color:s.fg},children:[i.jsx("div",{style:{flex:1,minWidth:0},children:r}),i.jsx("button",{type:"button",onClick:o,"aria-label":"close","data-testid":`${n}-close`,style:SE,children:"×"})]})}const wE={success:{bg:"#dcfce7",border:"#86efac",fg:"#14532d"},error:{bg:"#fee2e2",border:"#fca5a5",fg:"#7f1d1d"},warning:{bg:"#fef3c7",border:"#fcd34d",fg:"#78350f"},neutral:{bg:"#f1f5f9",border:"#cbd5e1",fg:"#334155"}},kE={display:"flex",alignItems:"flex-start",gap:12,padding:"12px 16px",border:"1px solid",borderRadius:0,fontSize:13,lineHeight:1.5},SE={background:"transparent",border:"none",fontSize:18,cursor:"pointer",lineHeight:1,color:"inherit",padding:"0 4px",flexShrink:0},op={margin:"8px 0 0",paddingLeft:20,fontSize:12},rp={background:"rgba(0, 0, 0, 0.06)",padding:"1px 4px",borderRadius:3,fontFamily:"monospace"},CE={position:"fixed",inset:0,background:"rgba(15, 23, 42, 0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1e4},$E={background:"#fff",borderRadius:8,padding:24,width:"min(480px, 90vw)",boxShadow:"0 10px 25px rgba(0,0,0,0.15)"},EE={margin:"0 0 8px",fontSize:16,fontWeight:600,color:"#0f172a"},IE={margin:"0 0 12px",color:"#334155",fontSize:13,lineHeight:1.5},AE={marginBottom:16,padding:"8px 12px",background:"#f8fafc",borderRadius:6,fontSize:12,color:"#64748b",fontFamily:"monospace"},TE={display:"flex",gap:8,justifyContent:"flex-end"},DE={padding:"8px 14px",background:"#2563eb",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"},NE={padding:"8px 14px",background:"#fff",color:"#334155",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13,fontWeight:500,cursor:"pointer"},zE={padding:"8px 14px",background:"transparent",color:"#64748b",border:"none",borderRadius:6,fontSize:13,cursor:"pointer"};function ip(e){if(!e)return null;const n=new URLSearchParams(e).get("route");return n&&n.length>0?n:null}function Zs(e,t,n){const o=`/admin/layout-editor/${encodeURIComponent(e)}`;if(!t||t.length===0)return o;if(t.startsWith("__")){let r=`${o}?edit=${encodeURIComponent(t)}`;return(t.startsWith("__modal__/")||t.startsWith("__iteration__/"))&&n&&(r+=`&host=${encodeURIComponent(n)}`),r}return`${o}?route=${encodeURIComponent(t)}`}function sp(e){if(!e)return null;const n=new URLSearchParams(e).get("host");return n&&n.length>0?n:null}function ap(e){if(!e)return null;const n=new URLSearchParams(e).get("edit");return n&&n.length>0&&n.startsWith("__")?n:null}function ME(e,t){const n=`/admin/templates/${encodeURIComponent(e)}/edit`;if(!t||t.length===0||t.startsWith("__"))return n;const o=t.replace(/^\*+/,"");return o.length===0?n:`${n}?route=${encodeURIComponent(o)}`}const LE="g7le-editor-route",PE=new Set;function Qs(e){return e.path.startsWith("__group__/")||e.path.startsWith("__conngroup__/")}function lp(e,t){const{t:n,translationEngine:o,translationContext:r,templateIdentifier:s}=t,a=Qs(e);if(!e.label.startsWith("$t:"))return e.label;const l=e.label.slice(3),d=l.indexOf("|"),c=d===-1?l:l.slice(0,d),p={};if(d!==-1)for(const y of l.slice(d+1).split("|")){const g=y.indexOf("=");g!==-1&&(p[y.slice(0,g)]=y.slice(g+1))}if((e.kind==="route"||e.kind==="modal")&&!a&&o&&r&&s!==r.templateId){const y={templateId:s,locale:r.locale},g=Object.keys(p).length>0?"|"+Object.entries(p).map(([h,v])=>`${h}=${v}`).join("|"):void 0,m=o.translate(c,y,g);if(m!==c)return m}const f=n(c,p);if(f===c&&typeof window<"u"){const y=window.G7Core?.t;if(typeof y=="function")try{const g=y(c,p);if(g!==c)return g}catch{}}if(f===c&&e.kind==="route"&&!a){const y=e.path;if(y&&y!=="/"){const g=y.split("/").filter(Boolean);return g[g.length-1]??y}return y==="/"?"/":c}return f}function OE(e){return e.trim().toLocaleLowerCase()}function dp(e,t){if(!t)return e;const n=e.toLocaleLowerCase(),o=[];let r=0,s=n.indexOf(t,r);if(s===-1)return e;let a=0;for(;s!==-1;)s>r&&o.push(e.slice(r,s)),o.push(i.jsx("mark",{className:"g7le-route-tree__highlight",style:{background:"#fde68a",color:"inherit",borderRadius:2,padding:"0 1px"},children:e.slice(s,s+t.length)},`m${a++}`)),r=s+t.length,s=n.indexOf(t,r);return r<e.length&&o.push(e.slice(r)),o}function BE(e){if(!e.layoutName)return null;const t=`${e.layoutName}.json`;switch(e.source.kind){case"module":case"plugin":return`${e.source.identifier?`${e.source.identifier} · `:""}resources/layouts/${t}`;case"template":return`${e.source.identifier?`${e.source.identifier} · `:""}layouts/${t}`;default:return`layouts/${t}`}}function WE({node:e}){const{t}=tt(),n=[],o={display:"inline-flex",alignItems:"center",padding:"1px 6px",fontSize:10,fontWeight:600,borderRadius:4,letterSpacing:.3,textTransform:"uppercase",border:"1px solid transparent"};return e.isHidden&&n.push(i.jsx("span",{className:"g7le-route-tree__badge g7le-route-tree__badge--hidden","data-testid":"g7le-route-tree-badge-hidden",style:{...o,background:"#f1f5f9",color:"#64748b",borderColor:"#e2e8f0"},children:t("layout_editor.chrome.route_tree.badge.hidden")},"hidden")),e.isRedirect&&n.push(i.jsx("span",{className:"g7le-route-tree__badge g7le-route-tree__badge--redirect","data-testid":"g7le-route-tree-badge-redirect",style:{...o,background:"#fef3c7",color:"#92400e",borderColor:"#fde68a"},children:t("layout_editor.chrome.route_tree.badge.redirect")},"redirect")),n.length===0?null:i.jsx("span",{style:{marginLeft:"auto",display:"inline-flex",gap:4},children:n})}function cp({node:e,depth:t,onSelect:n,selectedRoutePath:o,activeHostRoutePath:r,dirtyLayoutNames:s,query:a,forceExpand:l}){const{t:d,translationEngine:c,translationContext:p}=tt(),{state:f}=Je(),y=Qs(e),g=e.path.startsWith("__conngroup__/"),m=u.useMemo(()=>{const N=f.selectedRoute?.path??"";return N.startsWith("__extension__/")||N.startsWith("__modal__/")?N.slice(N.lastIndexOf("/")+1):null},[f.selectedRoute?.path]),h=u.useMemo(()=>!g||!m?!1:(e.children??[]).some(N=>N.kind==="extension"&&N.extensionId===m||N.kind==="modal"&&N.modalId===m),[g,m,e.children]),[v,b]=u.useState(!0);u.useEffect(()=>{h&&b(!1)},[h]);const x=f.editMode==="extension"?f.selectedRoute?.layoutName??null:null,k=e.kind==="extension"?up(e,f.routeTree)??null:null,w=e.kind==="extension"&&o===e.path&&(x===null||k===null||x===k),_=f.editMode==="modal"&&o?.startsWith("__modal__/")?o.slice(o.lastIndexOf("/")+1):null,C=f.editMode==="modal"?f.selectedRoute?.layoutName??null:null,E=e.kind==="modal"&&!!_&&e.modalId===_&&(C===null||e.modalHostLayout==null||C===e.modalHostLayout),S=e.kind!=="extension"&&e.kind!=="modal"&&o===e.path||w||E||e.kind==="route"&&!y&&r===e.path,$=(e.children?.length??0)>0&&(!g||l||!v),A=!y&&!!e.layoutName&&s.has(e.layoutName),z=y?void 0:e.kind==="extension"?e.extensionId?f.extensionVersions[e.extensionId]:void 0:e.layoutName?f.layoutVersions[e.layoutName]:void 0,D=()=>{if(g){b(N=>!N);return}y||n(e)},O=lp(e,{t:d,translationEngine:c,translationContext:p,templateIdentifier:f.templateIdentifier}),M=y?null:BE(e),B={paddingLeft:12+t*14,paddingRight:12,paddingTop:4,paddingBottom:4,opacity:e.isHidden?.55:1,cursor:g?"pointer":y?"default":"pointer",background:S?"#eff6ff":"transparent",borderLeft:S?"2px solid #2563eb":"2px solid transparent",fontWeight:y||S?600:400,fontSize:y?11:13,color:y?"#64748b":S?"#1d4ed8":"#1e293b",textTransform:y?"uppercase":"none",letterSpacing:y?.6:void 0,transition:"background-color 100ms, color 100ms",display:"flex",flexDirection:"column",alignItems:"stretch",gap:2,userSelect:"none"};return i.jsxs("li",{children:[i.jsxs("div",{"data-testid":y?"g7le-route-tree-group":"g7le-route-tree-item","data-route-path":e.path,"data-route-kind":e.kind,"data-conn-collapsed":g?l?"false":String(v):void 0,role:g||!y?"button":void 0,tabIndex:g||!y?0:void 0,"aria-expanded":g?l?!0:!v:void 0,onClick:D,onKeyDown:N=>{g&&(N.key==="Enter"||N.key===" ")?(N.preventDefault(),b(H=>!H)):!y&&(N.key==="Enter"||N.key===" ")&&(N.preventDefault(),n(e))},style:B,children:[i.jsxs("span",{style:{display:"flex",alignItems:"center",gap:6,minWidth:0},children:[g&&i.jsx("span",{"aria-hidden":"true",style:{flex:"0 0 auto",fontSize:9,color:"#94a3b8",width:10,display:"inline-block"},children:l||!v?"▾":"▸"}),A&&i.jsx("span",{className:"g7le-route-tree__dirty-dot","data-testid":"g7le-route-tree-dirty",title:d("layout_editor.chrome.route_tree.badge.modified"),"aria-label":d("layout_editor.chrome.route_tree.badge.modified"),style:{width:7,height:7,borderRadius:"50%",background:"#f59e0b",flex:"0 0 auto"}}),y?O:dp(O,a),typeof z=="number"&&i.jsxs("span",{className:"g7le-route-tree__version-badge","data-testid":"g7le-route-tree-version",title:d("layout_editor.chrome.route_tree.badge.version_tooltip",{version:String(z)}),style:{flex:"0 0 auto",padding:"0 5px",fontSize:10,fontWeight:600,lineHeight:"16px",borderRadius:4,background:"#eef2ff",color:"#4f46e5",border:"1px solid #e0e7ff",letterSpacing:.2},children:["v",z]}),i.jsx(WE,{node:e})]}),M&&i.jsx("span",{className:"g7le-route-tree__layout-path","data-testid":"g7le-route-tree-layout-path",title:M,style:{fontSize:11,fontWeight:400,color:"#94a3b8",textTransform:"none",letterSpacing:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:dp(M,a)})]}),$&&i.jsx("ul",{style:{listStyle:"none",margin:0,padding:0},children:e.children.map(N=>i.jsx(cp,{node:N,depth:t+1,onSelect:n,selectedRoutePath:o,activeHostRoutePath:r,dirtyLayoutNames:s,query:a,forceExpand:l},N.path))})]})}function jE(e,t){if(!t||!t.startsWith("__"))return null;const n=t.startsWith("__modal__/"),o=t.startsWith("__extension__/");if(!n&&!o)return null;const r=t.slice(t.lastIndexOf("/")+1),s=a=>{for(const l of a){if(l.connectedHostRoutePath&&(n&&l.kind==="modal"&&l.modalId===r||o&&l.kind==="extension"&&l.extensionId===r))return l.connectedHostRoutePath;if(l.children&&l.children.length>0){const d=s(l.children);if(d)return d}}return null};return s(e)}function up(e,t){if(e.connectedHostRoutePath){const n=pp(t,e.connectedHostRoutePath);if(n?.layoutName)return n.layoutName}if(e.extensionType==="overlay"&&e.extensionTargetName)return e.extensionTargetName}function pp(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=pp(n.children,t);if(o)return o}}return null}function fp(e,t,n){const o=[];for(const r of e){const s=Qs(r),a=r.children&&r.children.length>0?fp(r.children,t,n):[];if(s){a.length>0&&o.push({...r,children:a});continue}const l=lp(r,n).toLocaleLowerCase(),d=r.path.toLocaleLowerCase();(l.includes(t)||!d.startsWith("__")&&d.includes(t)||a.length>0)&&o.push({...r,children:a})}return o}function HE(){const{t:e,translationEngine:t,translationContext:n}=tt(),{state:o,dispatch:r}=Je(),a=Ft()?.dirtyLayoutNames??PE,[l,d]=u.useState(""),c=OE(l),p=u.useMemo(()=>c?fp(o.routeTree,c,{t:e,translationEngine:t,translationContext:n,templateIdentifier:o.templateIdentifier}):o.routeTree,[c,o.routeTree,o.templateIdentifier,e,t,n]),f=h=>{if(h.kind==="base"&&h.layoutName){r({type:"ENTER_BASE_EDIT",layoutName:h.layoutName});return}if(h.kind==="modal"&&h.modalId&&h.modalHostLayout){r({type:"ENTER_MODAL_EDIT",modalId:h.modalId,hostLayout:h.modalHostLayout});return}if(h.kind==="extension"&&h.extensionId){const v=up(h,o.routeTree);r({type:"ENTER_EXTENSION_EDIT",extensionId:h.extensionId,extensionHost:v});return}if(r({type:"SELECT_ROUTE",route:{path:h.path,layoutName:h.layoutName}}),typeof window<"u"&&window.history?.pushState){const v=Zs(o.templateIdentifier,h.path);`${window.location.pathname}${window.location.search}`!==v&&window.history.pushState({source:LE,routePath:h.path},"",v)}},y=o.selectedRoute?.path??null,g=u.useMemo(()=>jE(o.routeTree,y),[o.routeTree,y]);if(o.isRouteTreeCollapsed)return i.jsx("aside",{className:"g7le-route-tree g7le-route-tree--collapsed","data-testid":"g7le-route-tree-panel","data-collapsed":"true",style:{width:28,borderRight:"1px solid #e2e8f0",background:"#ffffff"}});const m=o.routeTree.length>0;return i.jsxs("aside",{className:"g7le-route-tree","data-testid":"g7le-route-tree-panel","data-collapsed":"false",style:{width:280,borderRight:"1px solid #e2e8f0",background:"#ffffff",position:"sticky",top:"var(--g7le-header-h, 0px)",alignSelf:"start",height:"calc(100vh - var(--g7le-header-h, 0px))",overflow:"hidden",minHeight:0,display:"flex",flexDirection:"column"},"aria-label":e("layout_editor.chrome.route_tree.panel_title"),children:[i.jsx("div",{className:"g7le-route-tree__title",style:{padding:"10px 14px",fontSize:12,fontWeight:600,color:"#475569",textTransform:"uppercase",letterSpacing:.6,borderBottom:"1px solid #e2e8f0",background:"#f8fafc",flex:"0 0 auto"},children:e("layout_editor.chrome.route_tree.panel_title")}),m&&i.jsxs("div",{className:"g7le-route-tree__search",style:{padding:"8px 10px",borderBottom:"1px solid #e2e8f0",background:"#ffffff",flex:"0 0 auto",position:"relative"},children:[i.jsx("input",{type:"text",className:"g7le-route-tree__search-input","data-testid":"g7le-route-tree-search",value:l,onChange:h=>d(h.target.value),placeholder:e("layout_editor.chrome.route_tree.search_placeholder"),"aria-label":e("layout_editor.chrome.route_tree.search_placeholder"),style:{width:"100%",boxSizing:"border-box",padding:l?"6px 26px 6px 10px":"6px 10px",fontSize:12,lineHeight:1.4,color:"#1e293b",border:"1px solid #cbd5e1",borderRadius:6,outline:"none",background:"#f8fafc"}}),l&&i.jsx("button",{type:"button",className:"g7le-route-tree__search-clear","data-testid":"g7le-route-tree-search-clear",onClick:()=>d(""),"aria-label":e("layout_editor.chrome.route_tree.search_clear"),title:e("layout_editor.chrome.route_tree.search_clear"),style:{position:"absolute",right:16,top:"50%",transform:"translateY(-50%)",width:18,height:18,display:"inline-flex",alignItems:"center",justifyContent:"center",border:"none",background:"transparent",color:"#94a3b8",cursor:"pointer",fontSize:14,lineHeight:1,padding:0},children:"✕"})]}),!m&&i.jsx("div",{className:"g7le-route-tree__empty","data-testid":"g7le-route-tree-empty",style:{padding:16,fontSize:12,color:"#94a3b8",textAlign:"center"},children:e("layout_editor.chrome.route_tree.empty")}),m&&c&&p.length===0&&i.jsx("div",{className:"g7le-route-tree__no-results","data-testid":"g7le-route-tree-no-results",style:{padding:16,fontSize:12,color:"#94a3b8",textAlign:"center"},children:e("layout_editor.chrome.route_tree.search_no_results")}),m&&p.length>0&&i.jsx("nav",{className:"g7le-route-tree__scroll","data-testid":"g7le-route-tree-scroll",style:{flex:"1 1 auto",minHeight:0,overflowY:"auto"},children:i.jsx("ul",{style:{listStyle:"none",margin:0,padding:0},children:p.map(h=>i.jsx(cp,{node:h,depth:0,onSelect:f,selectedRoutePath:y,activeHostRoutePath:g,dirtyLayoutNames:a,query:c,forceExpand:!!c},h.path))})})]})}const FE=Lt().DynamicRenderer,Rs=Lt(),KE=Rs.ResponsiveProvider;Rs.useResponsive,Rs.ResponsiveContext;const VE=Lt().ActionDispatcher,gp=Lt(),UE=gp.DataSourceManager;gp.dataSourceManager;function Gr(e,t,n){const o=t??{},r=o.message||o.error||`HTTP ${e}`,s=o?.data?.required_permissions??o?.required_permissions;let a;return e===401?a="unauthorized":e===403?a="forbidden":e===404?a="not_found":e>=500?a="server_error":a="unknown",{kind:a,status:e,message:String(r),requiredPermissions:s?String(s):void 0,source:n}}function io(e,t){return{kind:"network",status:0,message:e instanceof Error?e.message:String(e??"Unknown network error"),source:t}}function hp(e){if(typeof e!="object"||e===null)return!1;const t=e;return typeof t.kind=="string"&&typeof t.status=="number"&&typeof t.message=="string"}function Ze(e){if(!e)return[];const t=[],n=e.split(".");for(let o=0;o<n.length;o++){const r=n[o];if(r==="children")continue;if(r==="iteration"||r==="sortable"){o+=1;continue}if(r==="responsive"){const a=n[o+1];typeof a=="string"&&a.length>0&&(t.push({responsive:a}),o+=1);continue}const s=Number(r);Number.isFinite(s)&&Number.isInteger(s)&&s>=0&&t.push(s)}return t}function qE(e,t){if(!e)return{affordance:"none",targetPath:null};const n=GE(e);return n?YE(n)?{affordance:"dynamic_path",targetPath:n}:/^(https?:)?\/\//.test(n)?{affordance:"external_url",targetPath:n}:n.startsWith("/")?t?{affordance:t(n),targetPath:n}:{affordance:"route_not_in_tree",targetPath:n}:{affordance:"route_not_in_tree",targetPath:n}:{affordance:"none",targetPath:null}}function GE(e){const t=e.actions;if(Array.isArray(t)){for(const n of t)if(n&&typeof n=="object"&&n.handler==="navigate"){const o=n.params?.path;if(typeof o=="string"&&o.length>0)return o}}if(e.name==="A"||e.type==="A"){const n=e.props?.href;if(typeof n=="string"&&n.length>0)return n}return null}function YE(e){return/\{\{[\s\S]*?\}\}/.test(e)}function XE(e,t,n){const o=s=>!!(s?.iteration&&s.iteration.source!==void 0),r=s=>{if(!s)return s;const a=s.indexOf(".iteration."),l=s.indexOf(".sortable."),d=[a,l].filter(c=>c>=0).sort((c,p)=>c-p)[0];return d===void 0?s:s.slice(0,d)};return o(e)?{isIteration:!0,iterationSourcePath:r(n)}:n&&n.includes(".iteration.")?{isIteration:!0,iterationSourcePath:r(n)}:t.some(o)?{isIteration:!0,iterationSourcePath:r(n)}:{isIteration:!1,iterationSourcePath:null}}function JE(e,t){const n=a=>{const l=a?.iteration&&a.iteration.source;return typeof l=="string"&&l.length>0?l:null},o=n(e)??t.map(n).find(a=>a!==null)??null;if(o!==null){const a=QE(o);if(a!==null)return a}const r=[];if(typeof e.text=="string"&&r.push(...yp(e.text)),e.props&&typeof e.props=="object")for(const a of Object.values(e.props))typeof a=="string"&&r.push(...yp(a));return r.length===0?null:r.find(a=>!ZE.has(a))??r[0]}const ZE=new Set(["_local","_global","_computed","route","query","props","item","index","event","$event","$response","response"]);function yp(e){const t=[],n=/\{\{([\s\S]*?)\}\}/g;let o;for(;(o=n.exec(e))!==null;){const r=bp(o[1]);r!==null&&t.push(r)}return t}function QE(e){const t=e.replace(/\{\{|\}\}/g,"");return bp(t)}function bp(e){const t=/[A-Za-z_$][A-Za-z0-9_$]*/.exec(e.trim());return t?t[0]:null}function gn(e,t,n,o=[]){if(!e)return"none";if(td(e)||typeof e.slot=="string"||typeof e.__editorSlotName=="string")return"extension_point";if(t!=="route"&&Ji(e,t,n)){const s=e.__source?.kind;return s==="base"?"base":s==="partial"?"partial":s==="extension"?"extension":"base"}if(Cb(e,o))return"data_bound";if(!Ji(e,t,n))return"none";const r=e.__source?.kind;return r==="base"?"base":r==="partial"?"partial":r==="extension"?"extension":"none"}function mp(e){if(!e)return null;if(e.__source?.kind==="extension"&&typeof e.__source.extensionId=="number")return e.__source.extensionId;const t=e.__injectedProps;if(Array.isArray(t)&&t.length>0){const n=t[0]?.extensionId;if(typeof n=="number")return n}return null}function RE(e,t){if(t<=0)return e;const n=e.split(".");let o=0;const r=[];for(let s=0;s<n.length;s++){const a=n[s];if(a==="children"){if(o>=t)break;r.push(a);continue}if(a==="iteration"||a==="sortable"){if(o>=t)break;r.push(a);continue}r.push(a);const l=n[s-1];if((l===void 0||l==="children")&&(o+=1),o>=t)break}return r.join(".")}function vp(e,t,n,o){if(!e||!t)return t;const r=n(t),s=Ib(e,r,o);return s.length===r.length?t:RE(t,s.length)}function _p(e){return e==="none"||e==="data_bound"}function e7(e){const{rootNode:t,editMode:n,currentExtensionId:o,editableRootPath:r,pathParser:s=Ze,resolveRouteMatch:a}=e,[l,d]=u.useState(null),[c,p]=u.useState(null),f=u.useCallback(x=>{if(x===null||n==="route")return!0;if(n==="iteration_item"||n==="modal"){if(!r||r.length===0)return!0;const C=s(x);return xp(C,r)}const k=ea(t,x,s);if(!k)return!0;const w=ta(t,x,s),_=gn(k,n,o,w);return _==="none"||_==="data_bound"},[t,s,n,o,r]),y=u.useCallback((x,k)=>{const _=(k?.currentTarget?.dataset??k?.dataset)?.editorPath??null,C=vp(t,_,s,o);f(C)&&d(C)},[t,s,o,f]),g=u.useCallback((x,k)=>{if(x===null){p(null);return}const _=(k?.currentTarget?.dataset??k?.dataset)?.editorPath??null,C=vp(t,_,s,o);if(!f(C)){p(null);return}p(C)},[t,s,o,f]),m=u.useCallback(()=>{d(null)},[]),h=u.useMemo(()=>{const x=ea(t,l,s),k=ea(t,c,s),w=x?ta(t,l,s):[];let _=gn(x,n,o,w);(n==="iteration_item"||n==="modal")&&l!==null&&x&&r&&r.length>0&&xp(s(l),r)&&(nd(x)?_="data_bound":_==="data_bound"&&(_="none"));const{affordance:C,targetPath:E}=qE(x,a),S=_==="base"&&typeof x?.__source?.layout=="string"?x.__source.layout:null,T=_==="data_bound"&&x?JE(x,w):null,{isIteration:$,iterationSourcePath:A}=_==="data_bound"&&x?XE(x,w,l):{isIteration:!1,iterationSourcePath:null};return{selectedNode:x,hoverNode:k,selectedLockKind:_,selectedNavAffordance:C,selectedNavTargetPath:E,selectedBaseLayout:S,selectedDataSourceId:T,selectedIsIteration:$,selectedIterationSourcePath:A}},[t,l,c,n,o,r,s,a]),v=u.useRef(void 0);v.current!==l&&(v.current=l,Cl({op:l===null?"clear":"select",componentPath:l,componentName:wp(h.selectedNode),lockKind:h.selectedLockKind,navAffordance:h.selectedNavAffordance,sourceExtensionId:mp(h.selectedNode),timestamp:Date.now()}));const b=u.useRef(void 0);return b.current!==c&&(b.current=c,c!==null&&Cl({op:"hover",componentPath:c,componentName:wp(h.hoverNode),lockKind:gn(h.hoverNode,n,o,ta(t,c,s)),navAffordance:"none",sourceExtensionId:mp(h.hoverNode),timestamp:Date.now()})),{selectedPath:l,hoverPath:c,...h,handleSelect:y,handleHover:g,clearSelection:m}}function ea(e,t,n){if(!e||!t)return null;const o=n(t);return We(e,o)}function xp(e,t){if(e.length<t.length)return!1;for(let n=0;n<t.length;n++)if(e[n]!==t[n])return!1;return!0}function ta(e,t,n){if(!e||!t)return[];const o=n(t),r=[];for(let s=0;s<o.length;s++){const a=o.slice(0,s),l=We(e,a);l&&r.push(l)}return r}function wp(e){return e?typeof e.name=="string"&&e.name.length>0?e.name:typeof e.type=="string"&&e.type.length>0?e.type:null:null}let kp=0;function Sp(){return kp}function na(){kp+=1}function t7(e){if(typeof e!="string")return null;const t="__modal__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n===""||n.includes("/")?null:n}function n7(e){if(typeof e!="string")return null;const t="__iteration__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n!==""?n:null}function Cp(){const{state:e}=Je(),t=e.templateIdentifier,n=e.selectedRoute?.layoutName??null,o=e.editMode,r=o==="modal"?t7(e.selectedRoute?.path):null,s=o==="iteration_item"?n7(e.selectedRoute?.path):null,[a,l]=u.useState(null),[d,c]=u.useState(!1),[p,f]=u.useState(null),[y,g]=u.useState(!1),[m,h]=u.useState(0),[v,b]=u.useState(0),x=u.useRef(new Map),[k,w]=u.useState(()=>new Set),_=u.useCallback((L,I)=>`${L}|${I??""}`,[]),C=u.useRef(null);u.useEffect(()=>{C.current=a},[a]);const E=u.useCallback((L,I)=>{const P=_(o,n);x.current.set(P,{raw:L,lockVersion:I}),w(F=>{if(F.has(P))return F;const G=new Set(F);return G.add(P),G})},[_,o,n]),S=u.useCallback(L=>{x.current.delete(L),w(I=>{if(!I.has(L))return I;const P=new Set(I);return P.delete(L),P})},[]),T=u.useCallback(L=>{const I=`|${L??""}`,P=[];for(const F of x.current.keys())F.endsWith(I)&&P.push(F);for(const F of P)x.current.delete(F);P.length>0&&w(F=>{let G=!1;const V=new Set(F);for(const Y of P)V.delete(Y)&&(G=!0);return G?V:F})},[]),$=u.useCallback(async()=>{if(!n){l(null),f(null),g(!1);return}const L=_(o,n),I=x.current.get(L);if(I){l({layoutName:n,raw:I.raw,lockVersion:I.lockVersion}),g(!0),f(null),c(!1),st({op:"load",layoutName:n,editMode:o,statusCode:0,isDirty:!0,timestamp:Date.now()});return}c(!0),f(null);try{const P=window.G7Config?.cache_version??0,F=en(`/api/layouts/${encodeURIComponent(t)}/${n}`,"json",`${P}.${Sp()}`,"with_source_meta=1"),G=ar(),V={Accept:"application/json"};G&&(V.Authorization=`Bearer ${G}`);const Y=await fetch(F,{headers:V,credentials:"same-origin"}),Z=await Y.json().catch(()=>null);if(!Y.ok){f(Gr(Y.status,Z,"layout")),l(null),g(!1);return}const se=(Z&&(Z.data||Z))??{},re=typeof se.lock_version=="number"?se.lock_version:0;if(o==="modal"&&r){const ee=se,fe=Array.isArray(ee.modals)?ee.modals:[],K=fe.findIndex(ne=>ne&&(ne.id===r||ne.modal_id===r));if(K>=0){const ne=fe[K],me={...ne,props:{...ne.props??{},isOpen:!0}},ze=[...Array.isArray(ee.components)?ee.components:[],me],Se=[ze.length-1];l({layoutName:n,raw:{...ee,components:ze},lockVersion:re,modalContext:{modalId:r,modalIndex:K,hostRaw:ee,originalModalNode:ne,editIndexPath:Se}}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()});return}}if(o==="iteration_item"&&s){const ee=se,fe=Ze(s),K={children:ee.components??[]},ne=We(K,fe);if(ne&&Array.isArray(ne.children)){l({layoutName:n,raw:ee,lockVersion:re,iterationContext:{sourcePath:s,sourceIndexPath:fe,hostRaw:ee}}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()});return}}l({layoutName:n,raw:se,lockVersion:re}),g(!1),st({op:"load",layoutName:n,editMode:o,statusCode:Y.status,timestamp:Date.now()})}catch(P){f(io(P,"layout")),l(null),g(!1)}finally{c(!1)}},[t,n,o,r,s,_]);u.useEffect(()=>{$()},[$]);const A=u.useCallback(async()=>{S(_(o,n)),na(),b(L=>L+1),await $()},[S,_,o,n,$]),z=u.useCallback(()=>{S(_(o,n))},[S,_,o,n]),D=u.useCallback(L=>{l(I=>{if(!I)return I;const P=I.raw.components??[],F=L(P),G={...I.raw,components:F};return E(G,I.lockVersion),{...I,raw:G}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),O=u.useCallback(()=>{g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o]),M=u.useCallback(L=>{l(I=>{if(!I)return I;const P={...I.raw,components:L};return E(P,I.lockVersion),{...I,raw:P}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),B=u.useCallback((L,I,...P)=>{const F=P.length>=1?P[0]:I;l(G=>{if(!G)return G;const V={...G.raw,[L]:I},Y=G.raw.__editor;if(Y&&typeof Y=="object"&&!Array.isArray(Y)){const Z=Y,se=Z.original;se&&typeof se=="object"&&!Array.isArray(se)&&(V.__editor={...Z,original:{...se,[L]:F}})}return E(V,G.lockVersion),{...G,raw:V}}),g(!0),st({op:"patch",layoutName:n,editMode:o,isDirty:!0,timestamp:Date.now()})},[n,o,E]),N=u.useCallback(async L=>{const I=C.current;if(!I)return st({op:"save_guard_result",layoutName:n,editMode:o,guardBlocked:!1,timestamp:Date.now()}),{kind:"guard_no_document"};if(L?.resolveActiveExtensions&&L.sessionAddedPaths?.length)try{const fe=await L.resolveActiveExtensions(),K=o7(I.raw.components,L.sessionAddedPaths,fe);if(K.length>0)return st({op:"save_guard_result",layoutName:n,editMode:o,guardBlocked:!0,guardBlockedPaths:K,timestamp:Date.now()}),{kind:"blocked_inactive_extension",blockedPaths:K}}catch{}if(Hb())try{await Fb(t)}catch{}const P=`/api/admin/templates/${encodeURIComponent(t)}/layouts/${I.layoutName}`,F=ar(),G={Accept:"application/json","Content-Type":"application/json"};F&&(G.Authorization=`Bearer ${F}`);const V=!!I.modalContext,Y=!!I.iterationContext,Z=V||Y?"host_layout_modal_patch":"layout";st({op:"save",layoutName:I.layoutName,editMode:o,saveTarget:Z,endpoint:P,isDirty:y,timestamp:Date.now()});let se;if(V&&I.modalContext){const{modalIndex:fe,hostRaw:K,originalModalNode:ne,editIndexPath:me}=I.modalContext,Ce=Array.isArray(K.modals)?[...K.modals]:[],ze=Array.isArray(I.raw.components)?I.raw.components:[],Se=me&&me.length===1?me[0]:0,qe=ze.length>Se?ze[Se]:void 0,De=qe?Vn([qe]):[],Pe=De.length>0?{...De[0]}:void 0;if(fe>=0&&fe<Ce.length&&Pe){const Me=ne.props??{},Re={...Pe.props??{}};"isOpen"in Me?Re.isOpen=Me.isOpen:delete Re.isOpen,Pe.props=Re,Ce[fe]=Pe}se={...vo(K),modals:Ce}}else if(Y&&I.iterationContext){const{sourceIndexPath:fe,hostRaw:K}=I.iterationContext,ne=vo(I.raw),me=Array.isArray(ne.components)?ne.components:[],Ce=We({children:me},fe),ze=Array.isArray(Ce?.children)?Ce.children:[],Se=vo(K),De={children:Array.isArray(Se.components)?Se.components:[]},Qe=Ot(De,fe,Me=>({...Me,children:ze})).children??[];se={...Se,components:Qe}}else se=vo(I.raw);const re=se;let U;try{U=await fetch(P,{method:"PUT",headers:G,credentials:"same-origin",body:JSON.stringify({content:JSON.stringify(re),expected_lock_version:I.lockVersion})})}catch(fe){const K=fe instanceof Error?fe.message:"network error";return st({op:"save_response",layoutName:I.layoutName,editMode:o,endpoint:P,statusCode:0,timestamp:Date.now()}),{kind:"network_error",message:K}}const ee=await U.json().catch(()=>null);if(st({op:"save_response",layoutName:I.layoutName,editMode:o,endpoint:P,statusCode:U.status,conflict:U.status===409?{currentVersion:ee?.current_version,yourVersion:ee?.your_version}:void 0,timestamp:Date.now()}),U.ok){const fe=typeof ee?.data?.lock_version=="number"?ee.data.lock_version:I.lockVersion+1;l({...I,lockVersion:fe}),g(!1),S(_(o,I.layoutName)),T(I.layoutName),na(),h(ne=>ne+1);const K=typeof ee?.data?.current_version=="number"?ee.data.current_version:void 0;return{kind:"success",newLockVersion:fe,newContentVersion:K,savedLayoutName:I.layoutName}}return U.status===409?{kind:"concurrent_modification",currentVersion:ee?.current_version??-1,yourVersion:ee?.your_version??I.lockVersion}:U.status===422?{kind:"validation_failed",status:422,errors:ee?.errors??null}:{kind:"network_error",message:ee?.message??`HTTP ${U.status}`}},[t,n,o,y,S,T,_,r,s]),H=u.useMemo(()=>{const L=new Set,I="route|";for(const P of k)if(P.startsWith(I)){const F=P.slice(I.length);F&&L.add(F)}return L},[k]);return{document:a,isLoading:d,error:p,isDirty:y,saveSuccessCounter:m,reloadCounter:v,dirtyKeys:k,dirtyLayoutNames:H,invalidateCurrentCache:z,reload:A,patchLayout:D,setLayoutComponents:M,patchDocumentRaw:B,save:N,markDirty:O}}function o7(e,t,n){if(!e||e.length===0)return[];const o=[],r=new Set([...n.moduleIds,...n.pluginIds]);for(const s of t){const a=r7(e,s);if(!a)continue;const l=a.__source;if(!l||l.kind!=="extension")continue;const d=String(l.extensionId??"");d.length!==0&&(r.has(d)||o.push(s))}return o}function r7(e,t){const n=Ze(t);return n.length===0?null:We({children:e},n)}function so(e,t){if(!e)return null;const n=e.getBoundingClientRect();if(n.width===0&&n.height===0)return null;if(!t)return{left:n.left,top:n.top,width:n.width,height:n.height,scale:1};const o=t.getBoundingClientRect();return{left:n.left-o.left,top:n.top-o.top,width:n.width,height:n.height,scale:1}}function ao(e,t){if(!e)return!1;if(!t)return!0;const n=t.getBoundingClientRect(),o=1;return!(e.left>=n.width-o||e.left+e.width<=o||e.top>=n.height-o||e.top+e.height<=o)}function $p(e,t){if(typeof window>"u")return()=>{};const n=()=>{e()};window.addEventListener("scroll",n,!0),window.addEventListener("resize",n);let o=null;t&&typeof ResizeObserver<"u"&&(o=new ResizeObserver(()=>e()),o.observe(t));let r=null,s=null;const a=typeof requestAnimationFrame<"u",l=()=>{if(!a){e();return}s===null&&(s=requestAnimationFrame(()=>{s=null,e()}))};return t&&typeof MutationObserver<"u"&&(r=new MutationObserver(l),r.observe(t,{childList:!0,subtree:!0})),()=>{window.removeEventListener("scroll",n,!0),window.removeEventListener("resize",n),o&&(o.disconnect(),o=null),r&&(r.disconnect(),r=null),s!==null&&a&&typeof cancelAnimationFrame<"u"&&(cancelAnimationFrame(s),s=null)}}const Ep=44;function Ip(e){if(!e)return"inside";const t=e.scale||1,n=e.width*t,o=e.height*t;return n<Ep||o<Ep?"outside":"inside"}const i7=24,Ap=6;function Tp(e){const t=i7/2,n=e.width/2,o=e.height/2,r=t+Ap,s=2*t-Math.min(n,o)+Ap,a=Math.max(r,s);return{above:{left:n-t,top:-a-t},below:{left:n-t,top:e.height+a-t},left:{left:-a-t,top:o-t},right:{left:e.width+a-t,top:o-t}}}function Dp(e){if(!e||typeof window>"u"||typeof window.getComputedStyle!="function")return{kind:"unknown",display:"",flexDirection:"",flexWrap:""};let t=e,n=(window.getComputedStyle(t).display??"").toLowerCase();for(;n==="contents"&&t.parentElement;)t=t.parentElement,n=(window.getComputedStyle(t).display??"").toLowerCase();const o=window.getComputedStyle(t),r=(o.display??"").toLowerCase(),s=(o.flexDirection??"").toLowerCase(),a=(o.flexWrap??"").toLowerCase(),l=r==="flex"||r==="inline-flex";if(r==="grid"||r==="inline-grid"){const f=Array.from(e.children).filter(h=>h.offsetParent!==null||h.getClientRects().length>0);if(f.length<=1)return{kind:"grid_2d",display:r,flexDirection:s,flexWrap:a};const y=f.map(h=>h.getBoundingClientRect()),g=new Set(y.map(h=>Math.round(h.top))),m=new Set(y.map(h=>Math.round(h.left)));return g.size===1?{kind:"grid_single_row",display:r,flexDirection:s,flexWrap:a}:m.size===1?{kind:"grid_single_column",display:r,flexDirection:s,flexWrap:a}:{kind:"grid_2d",display:r,flexDirection:s,flexWrap:a}}if(!l)return{kind:"block",display:r,flexDirection:s,flexWrap:a};const c=a==="wrap"||a==="wrap-reverse";return s==="column"||s==="column-reverse"?{kind:c?"flex_column_wrap":"flex_column_single",display:r,flexDirection:s,flexWrap:a}:{kind:c?"flex_row_wrap":"flex_row_single",display:r,flexDirection:s,flexWrap:a}}function s7(e){const{parentEl:t,selectedPath:n,selectedEl:o,explicitFlow:r}=e;return u.useMemo(()=>{if(!n||n.length===0)return{points:[],flow:"unknown"};const s=r??Dp(t).kind,a=n.slice(0,-1),l=n[n.length-1]??0;if((s==="flex_row_wrap"||s==="flex_column_wrap"||s==="grid_2d")&&!!o){const p=a7(s,o,a,l);if(p)return{points:p,flow:s}}return{points:l7(s,a,l),flow:s}},[t,n,o,r])}function a7(e,t,n,o){let r=t.parentElement;for(;r&&Np(r)==="contents";)r=r.parentElement;if(!r)return null;const s=[];if(zp(r,s),s.length===0)return null;const a=t.getBoundingClientRect(),l=a.top+a.height/2,d=[],c=[],p=[];for(const v of s){const b=v.rect,x=b.top+b.height/2,k=Math.min(a.bottom,b.bottom)-Math.max(a.top,b.top),w=Math.min(a.height,b.height)||1;k/w>.4?d.push({rect:b,path:v.path}):x<l?c.push({rect:b,path:v.path}):p.push({rect:b,path:v.path})}d.sort((v,b)=>v.rect.left-b.rect.left),c.sort((v,b)=>v.rect.top-b.rect.top),p.sort((v,b)=>v.rect.top-b.rect.top);const f=d.findIndex(v=>Math.abs(v.rect.left-a.left)<1&&Math.abs(v.rect.top-a.top)<1),y={direction:"above",disabled:!0,insertion:null},g={direction:"below",disabled:!0,insertion:null},m={direction:"left",disabled:!0,insertion:null},h={direction:"right",disabled:!0,insertion:null};if(f>0){const v=d[f-1],b=Xr(v.path);b&&(m.disabled=!1,m.insertion=b)}else if(d.length>0){const v=d[0],b=Yr(v.path);b&&(m.disabled=!1,m.insertion=b)}if(f>=0&&f<d.length-1){const v=d[f+1],b=Yr(v.path);b&&(h.disabled=!1,h.insertion=b)}else if(d.length>0){const v=d[d.length-1],b=Xr(v.path);b&&(h.disabled=!1,h.insertion=b)}if(c.length>0){const v=c[c.length-1],b=Xr(v.path);b&&(y.disabled=!1,y.insertion=b)}else if(s.length>0){const v=s[0],b=Yr(v.path);b&&(y.disabled=!1,y.insertion=b)}if(p.length>0){const v=p[0],b=Yr(v.path);b&&(g.disabled=!1,g.insertion=b)}else if(s.length>0){const v=s[s.length-1],b=Xr(v.path);b&&(g.disabled=!1,g.insertion=b)}else g.disabled=!1,g.insertion={parentPath:n,index:o+1};return y.disabled&&g.disabled&&m.disabled&&h.disabled?null:[y,g,m,h]}function Np(e){return typeof window>"u"?"":(window.getComputedStyle(e).display??"").toLowerCase()}function zp(e,t){const n=Array.from(e.children);for(const o of n){if(Np(o)==="contents"){zp(o,t);continue}const s=o.getAttribute("data-editor-path");if(s&&/\.iteration\.\d+(?:$|\.)/.test(s)||!s)continue;const a=o.getBoundingClientRect();a.width===0&&a.height===0||t.push({el:o,rect:a,path:Ze(s)})}}function Yr(e){if(!e||e.length===0)return null;const t=e[e.length-1];return typeof t!="number"?null:{parentPath:e.slice(0,-1),index:t}}function Xr(e){if(!e||e.length===0)return null;const t=e[e.length-1];return typeof t!="number"?null:{parentPath:e.slice(0,-1),index:t+1}}function l7(e,t,n){const o={direction:"above",disabled:!0,insertion:null},r={direction:"below",disabled:!0,insertion:null},s={direction:"left",disabled:!0,insertion:null},a={direction:"right",disabled:!0,insertion:null},l=d=>({parentPath:t,index:d});switch(e){case"block":case"unknown":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1);break}case"flex_row_single":case"grid_single_row":{s.disabled=!1,s.insertion=l(n),a.disabled=!1,a.insertion=l(n+1);break}case"flex_column_single":case"grid_single_column":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1);break}case"flex_row_wrap":case"grid_2d":{s.disabled=!1,s.insertion=l(n),a.disabled=!1,a.insertion=l(n+1),o.disabled=!1,o.insertion=l(0),r.disabled=!1,r.insertion=l(-1);break}case"flex_column_wrap":{o.disabled=!1,o.insertion=l(n),r.disabled=!1,r.insertion=l(n+1),s.disabled=!1,s.insertion=l(0),a.disabled=!1,a.insertion=l(-1);break}}return[o,r,s,a]}function d7(e=50){const t=u.useRef([]),n=u.useRef(-1),[,o]=u.useState(0),r=()=>{o(p=>(p+1)%1e6)},s=(p,f)=>{const y=t.current.length,g=n.current;Gh({op:p,actionKind:f?.actionKind,label:f?.label,stackSize:y,cursor:g,canUndo:g>0,canRedo:g>=0&&g<y-1,timestamp:Date.now()})},a=u.useCallback(p=>{const y=t.current.slice(0,n.current+1);for(y.push(p);y.length>e;)y.shift();t.current=y,n.current=y.length-1,r(),s("push",p)},[e]),l=u.useCallback(()=>{if(n.current<=0)return null;n.current-=1,r();const p=t.current[n.current]??null;return s("undo",p??void 0),p},[]),d=u.useCallback(()=>{const p=n.current+1;if(p>=t.current.length)return null;n.current=p,r();const f=t.current[p]??null;return s("redo",f??void 0),f},[]),c=u.useCallback(()=>{t.current=[],n.current=-1,r(),s("clear")},[]);return{push:a,undo:l,redo:d,clear:c,canUndo:n.current>0,canRedo:n.current>=0&&n.current<t.current.length-1}}function c7(e){if(!e||!(e instanceof HTMLElement))return!1;const t=e.tagName;return!!(t==="INPUT"||t==="TEXTAREA"||t==="SELECT"||e.isContentEditable)}function u7(e){const{onUndo:t,onRedo:n,enabled:o=!0}=e;u.useEffect(()=>{if(!o||typeof window>"u")return;const r=s=>{if(c7(s.target)||!(s.ctrlKey||s.metaKey))return;const l=s.key.toLowerCase();if(l==="z"&&!s.shiftKey){s.preventDefault(),t();return}if(l==="z"&&s.shiftKey||l==="y"){s.preventDefault(),n();return}};return window.addEventListener("keydown",r),()=>{window.removeEventListener("keydown",r)}},[t,n,o])}const p7=20,Mp=50,f7=90,Ln=120,g7=130,Lp=80,h7=9e3,Pp=1e4;function y7(e){const t=e<0?0:e>Mp?Mp:e;return p7+t}function b7(e){const{anchor:t,open:n,onClose:o,onEditProps:r,onDuplicate:s,onDelete:a,onSeparateBranch:l,separateBranchLabel:d,onMergeBranch:c,mergeBranchLabel:p}=e,{t:f}=tt(),[y,g]=u.useState(null);if(!n||!t)return null;const m=[{index:0,testid:"g7le-context-menu-edit-props",label:f("layout_editor.context_menu.edit_props"),onSelect:r},{index:1,testid:"g7le-context-menu-duplicate",label:f("layout_editor.context_menu.duplicate"),onSelect:s}];return l?m.push({index:m.length,testid:"g7le-context-menu-separate-branch",label:d??f("layout_editor.context_menu.separate_branch"),onSelect:l,hint:f("layout_editor.context_menu.separate_branch_hint")}):c&&m.push({index:m.length,testid:"g7le-context-menu-merge-branch",label:p??f("layout_editor.context_menu.merge_branch"),onSelect:c,hint:f("layout_editor.context_menu.merge_branch_hint")}),m.push({index:m.length,testid:"g7le-context-menu-delete",label:f("layout_editor.context_menu.delete"),onSelect:a,danger:!0}),i.jsx("div",{className:"g7le-context-menu","data-testid":"g7le-context-menu",role:"menu",style:{position:"absolute",left:t.left,top:t.top,background:"#fff",border:"1px solid #cbd5e1",borderRadius:6,boxShadow:"0 4px 12px rgba(0,0,0,0.15)",zIndex:1200,minWidth:160,padding:4,pointerEvents:"auto"},onMouseLeave:()=>{g(null),o()},children:m.map(h=>{const v=y===h.index,b=h.danger?"#dc2626":"#0f172a",x=h.danger?"#fee2e2":"#e0e7ff",k=h.danger?"#b91c1c":"#1d4ed8";return i.jsx("button",{type:"button",role:"menuitem","data-testid":h.testid,title:h.hint,onClick:w=>{w.stopPropagation(),h.onSelect(),g(null),o()},onMouseEnter:()=>g(h.index),onFocus:()=>g(h.index),style:{...m7,color:v?k:b,background:v?x:"transparent"},children:h.label},h.testid)})})}const m7={display:"block",width:"100%",textAlign:"left",padding:"6px 12px",border:"none",fontSize:13,cursor:"pointer",borderRadius:4,pointerEvents:"auto",transition:"background-color 120ms ease, color 120ms ease"};function v7(e){const{hoverBox:t,selectedBox:n,lockKind:o,navAffordance:r,onEditProps:s,onDuplicate:a,onDelete:l,onSeparateBranch:d,separateBranchLabel:c,onMergeBranch:p,mergeBranchLabel:f,definedDeviceBranches:y,onJumpToDevice:g,onLinkEditDestination:m,onEditBase:h,onEditExtension:v,extensionLabel:b,baseLayoutLabel:x,dataSourceLabel:k,branchLabel:w,onEditIteration:_,resizeEnabledAxes:C,onResizeHandlePointerDown:E,selectedName:S,onSelectParent:T}=e,{t:$}=tt(),[A,z]=u.useState(!1),D=_p(o),O=A&&D,M=Ip(n),B=M==="outside",N=B&&n?Tp(n):null,H=N?{left:N.right.left,top:N.above.top}:{right:-30,top:-30},L={top:-28,left:0},P=B&&!!S?{top:-50,left:0}:L,F=N?{top:N.below.top+28,left:0}:{top:"auto",bottom:-28},G=N?{top:N.below.top+56,left:0}:{top:"auto",bottom:-56};return i.jsxs("div",{className:"g7le-element-overlay","data-testid":"g7le-element-overlay",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[t&&i.jsx("div",{"data-testid":"g7le-overlay-hover",style:{position:"absolute",left:t.left,top:t.top,width:t.width,height:t.height,border:"1px dashed #2563eb",pointerEvents:"none"}}),n&&i.jsxs("div",{"data-testid":"g7le-overlay-selected","data-lock-kind":o,style:{position:"absolute",left:n.left,top:n.top,width:n.width,height:n.height,border:o==="none"?"2px solid #2563eb":"2px dashed #f59e0b",pointerEvents:"none"},children:[w?i.jsxs("span",{"data-testid":"g7le-overlay-branch-badge",style:x7,children:["📱 ",w]}):null,S&&(T?i.jsxs("button",{type:"button","data-testid":"g7le-overlay-type-label","data-placement":B?"outside":"inside",title:$("layout_editor.overlay.select_parent"),"aria-label":$("layout_editor.overlay.select_parent"),onClick:V=>{V.preventDefault(),V.stopPropagation(),T()},style:B?k7:w7,children:[i.jsx("span",{"aria-hidden":!0,style:{marginRight:3,opacity:.85},children:"↑"}),S]}):i.jsx("span",{"data-testid":"g7le-overlay-type-label","data-placement":B?"outside":"inside",style:B?Bp:Op,children:S})),D&&i.jsx("button",{type:"button","data-testid":"g7le-overlay-info-button","aria-label":"info",onClick:V=>{V.stopPropagation(),z(Y=>!Y)},"data-placement":M,style:{position:"absolute",...H,width:20,height:20,borderRadius:10,border:"1px solid #2563eb",background:"#fff",color:"#2563eb",fontSize:12,fontWeight:700,cursor:"pointer",pointerEvents:"auto",padding:0,lineHeight:"18px",zIndex:Ln},children:"ⓘ"}),o==="none"&&i.jsx(_7,{enabledAxes:C,onHandlePointerDown:E}),o==="base"&&h&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-base",onClick:V=>{V.stopPropagation(),h()},style:{...Po,...L},children:["🔒 ",$("layout_editor.overlay.edit_common_layout"),x?i.jsx("span",{"data-testid":"g7le-overlay-base-layout-label",style:ra,children:x}):null]}),o==="extension"&&v&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-extension",onClick:V=>{V.stopPropagation(),v()},style:{...Po,...L},children:["🔒 ",$("layout_editor.overlay.edit_extension"),b?i.jsx("span",{"data-testid":"g7le-overlay-extension-label",style:ra,children:b}):null]}),o==="data_bound"&&i.jsxs("span",{"data-testid":"g7le-overlay-data-bound-notice",style:{...Po,...P,pointerEvents:"none",cursor:"default"},children:[$("layout_editor.overlay.locked_data_bound"),k?i.jsx("span",{"data-testid":"g7le-overlay-data-source-label",style:ra,children:k}):null]}),o==="data_bound"&&_&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-edit-iteration",onClick:V=>{V.stopPropagation(),_()},style:{...Po,...F},children:["↳ ",$("layout_editor.overlay.edit_iteration_item")]}),r==="route_in_tree"&&i.jsx("button",{type:"button","data-testid":"g7le-overlay-link-edit-destination",onClick:V=>{V.stopPropagation(),m()},style:{...Po,...F},children:$("layout_editor.overlay.edit_destination_route")}),(d||p||g&&(y??[]).length>0)&&i.jsxs("div",{"data-testid":"g7le-overlay-branch-affordances",style:{position:"absolute",...G,display:"flex",flexDirection:"column",alignItems:"flex-start",gap:4,pointerEvents:"none"},children:[g&&(y??[]).map(V=>i.jsxs("button",{type:"button","data-testid":`g7le-overlay-jump-device-${V.key}`,title:$("layout_editor.overlay.jump_device_hint",{device:V.label}),onClick:Y=>{Y.stopPropagation(),g(V.key)},style:oa,children:["↗ ",$("layout_editor.overlay.jump_device",{device:V.label})]},V.key)),d&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-separate-branch",title:$("layout_editor.context_menu.separate_branch_hint"),onClick:V=>{V.stopPropagation(),d()},style:oa,children:["📱 ",c??$("layout_editor.context_menu.separate_branch")]}),p&&i.jsxs("button",{type:"button","data-testid":"g7le-overlay-merge-branch",title:$("layout_editor.context_menu.merge_branch_hint"),onClick:V=>{V.stopPropagation(),p()},style:oa,children:["↩️ ",f??$("layout_editor.context_menu.merge_branch")]})]})]}),i.jsx(b7,{anchor:n&&O?{left:n.left+n.width+8,top:n.top-4}:null,open:O,onClose:()=>z(!1),onEditProps:s,onDuplicate:a,onDelete:l,onSeparateBranch:d,separateBranchLabel:c,onMergeBranch:p,mergeBranchLabel:f})]})}function _7(e){const{enabledAxes:t,onHandlePointerDown:n}=e,o=t?.width??!1,r=t?.height??!1,s=[{key:"nw",style:{left:-4,top:-4},cursor:"nwse-resize",needsWidth:!0,needsHeight:!0},{key:"n",style:{left:"50%",top:-4,transform:"translateX(-50%)"},cursor:"ns-resize",needsWidth:!1,needsHeight:!0},{key:"ne",style:{right:-4,top:-4},cursor:"nesw-resize",needsWidth:!0,needsHeight:!0},{key:"e",style:{right:-4,top:"50%",transform:"translateY(-50%)"},cursor:"ew-resize",needsWidth:!0,needsHeight:!1},{key:"se",style:{right:-4,bottom:-4},cursor:"nwse-resize",needsWidth:!0,needsHeight:!0},{key:"s",style:{left:"50%",bottom:-4,transform:"translateX(-50%)"},cursor:"ns-resize",needsWidth:!1,needsHeight:!0},{key:"sw",style:{left:-4,bottom:-4},cursor:"nesw-resize",needsWidth:!0,needsHeight:!0},{key:"w",style:{left:-4,top:"50%",transform:"translateY(-50%)"},cursor:"ew-resize",needsWidth:!0,needsHeight:!1}];return i.jsx(i.Fragment,{children:s.map(a=>{const l=!a.needsWidth||o,d=!a.needsHeight||r,c=l&&d&&!!n;return!l||!d?null:i.jsx("span",{"data-testid":`g7le-resize-handle-${a.key}`,"data-resize-handle-active":c?"true":"false",onPointerDown:c?p=>{p.preventDefault(),p.stopPropagation(),n?.(a.key,{clientX:p.clientX,clientY:p.clientY})}:void 0,style:{position:"absolute",...a.style,width:8,height:8,background:"#fff",border:"1px solid #2563eb",cursor:a.cursor,pointerEvents:c?"auto":"none",zIndex:Ln}},a.key)})})}const Po={position:"absolute",top:-28,left:0,background:"#fff",border:"1px solid #2563eb",color:"#2563eb",padding:"2px 6px",fontSize:11,borderRadius:4,cursor:"pointer",pointerEvents:"auto",whiteSpace:"nowrap",zIndex:Ln},oa={background:"#fff",border:"1px solid #2563eb",color:"#2563eb",padding:"2px 6px",fontSize:11,borderRadius:4,cursor:"pointer",pointerEvents:"auto",whiteSpace:"nowrap",zIndex:Ln},ra={marginLeft:6,padding:"0 5px",borderRadius:3,background:"#eef2ff",border:"1px solid #c7d2fe",fontSize:10,fontWeight:600,color:"#3730a3"},x7={position:"absolute",top:0,right:0,transform:"translateY(-100%)",background:"rgba(124,58,237,0.92)",color:"#fff",padding:"1px 6px",borderRadius:"3px 3px 0 0",fontSize:10,fontWeight:600,whiteSpace:"nowrap",pointerEvents:"none"},Op={position:"absolute",top:0,left:0,background:"rgba(37,99,235,0.55)",color:"#fff",padding:"1px 6px",fontSize:10,fontWeight:600,lineHeight:"14px",borderBottomRightRadius:4,letterSpacing:"0.02em",whiteSpace:"nowrap",pointerEvents:"none",userSelect:"none",maxWidth:"100%",overflow:"hidden",textOverflow:"ellipsis"},Bp={position:"absolute",bottom:"100%",left:0,marginBottom:2,background:"rgba(37,99,235,0.55)",color:"#fff",padding:"1px 6px",fontSize:10,fontWeight:600,lineHeight:"14px",borderRadius:4,letterSpacing:"0.02em",whiteSpace:"nowrap",pointerEvents:"none",userSelect:"none"},w7={...Op,pointerEvents:"auto",cursor:"pointer",background:"rgba(37,99,235,0.82)",border:"none",display:"inline-flex",alignItems:"center",font:"inherit",fontSize:10,fontWeight:600,zIndex:Ln},k7={...Bp,pointerEvents:"auto",cursor:"pointer",background:"rgba(37,99,235,0.82)",border:"none",display:"inline-flex",alignItems:"center",font:"inherit",fontSize:10,fontWeight:600,zIndex:Ln},S7={above:{top:-30,left:"50%",transform:"translateX(-50%)"},below:{bottom:-30,left:"50%",transform:"translateX(-50%)"},left:{left:-30,top:"50%",transform:"translateY(-50%)"},right:{right:-30,top:"50%",transform:"translateY(-50%)"}};function C7(e){const{selectedBox:t,points:n,onAddRequest:o}=e,{t:r}=tt();if(!t||n.length===0)return null;const s=Ip(t),l=s==="outside"?Tp(t):null;return i.jsx("div",{className:"g7le-insertion-affordances","data-testid":"g7le-insertion-affordances",style:{position:"absolute",left:t.left,top:t.top,width:t.width,height:t.height,pointerEvents:"none"},children:n.map(d=>{const c=l?{left:l[d.direction].left,top:l[d.direction].top}:S7[d.direction],p=r(`layout_editor.insertion.${d.direction}`);return i.jsx("button",{type:"button",disabled:d.disabled,"data-testid":`g7le-insertion-${d.direction}`,"data-disabled":d.disabled?"true":"false","data-placement":s,"aria-label":p,title:p,onClick:f=>{f.stopPropagation(),d.disabled||o(d)},style:{position:"absolute",...c,width:24,height:24,borderRadius:12,border:"1px solid #2563eb",background:d.disabled?"#cbd5e1":"#fff",color:d.disabled?"#94a3b8":"#2563eb",cursor:d.disabled?"not-allowed":"pointer",opacity:d.disabled?.4:1,fontSize:16,fontWeight:700,lineHeight:"20px",padding:0,pointerEvents:"auto",boxShadow:"0 1px 3px rgba(0,0,0,0.15)",zIndex:Ln},children:"+"},d.direction)})})}const Jr=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function lo(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function ia(e){return"nodeType"in e}function _t(e){var t,n;return e?lo(e)?e:ia(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function sa(e){const{Document:t}=_t(e);return e instanceof t}function Oo(e){return lo(e)?!1:e instanceof _t(e).HTMLElement}function Wp(e){return e instanceof _t(e).SVGElement}function co(e){return e?lo(e)?e.document:ia(e)?sa(e)?e:Oo(e)||Wp(e)?e.ownerDocument:document:document:document}const sn=Jr?u.useLayoutEffect:u.useEffect;function Zr(e){const t=u.useRef(e);return sn(()=>{t.current=e}),u.useCallback(function(){for(var n=arguments.length,o=new Array(n),r=0;r<n;r++)o[r]=arguments[r];return t.current==null?void 0:t.current(...o)},[])}function $7(){const e=u.useRef(null),t=u.useCallback((o,r)=>{e.current=setInterval(o,r)},[]),n=u.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function Bo(e,t){t===void 0&&(t=[e]);const n=u.useRef(e);return sn(()=>{n.current!==e&&(n.current=e)},t),n}function Wo(e,t){const n=u.useRef();return u.useMemo(()=>{const o=e(n.current);return n.current=o,o},[...t])}function Qr(e){const t=Zr(e),n=u.useRef(null),o=u.useCallback(r=>{r!==n.current&&t?.(r,n.current),n.current=r},[]);return[n,o]}function Rr(e){const t=u.useRef();return u.useEffect(()=>{t.current=e},[e]),t.current}let aa={};function ei(e,t){return u.useMemo(()=>{if(t)return t;const n=aa[e]==null?0:aa[e]+1;return aa[e]=n,e+"-"+n},[e,t])}function jp(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return o.reduce((s,a)=>{const l=Object.entries(a);for(const[d,c]of l){const p=s[d];p!=null&&(s[d]=p+e*c)}return s},{...t})}}const uo=jp(1),ti=jp(-1);function E7(e){return"clientX"in e&&"clientY"in e}function la(e){if(!e)return!1;const{KeyboardEvent:t}=_t(e.target);return t&&e instanceof t}function I7(e){if(!e)return!1;const{TouchEvent:t}=_t(e.target);return t&&e instanceof t}function ni(e){if(I7(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return E7(e)?{x:e.clientX,y:e.clientY}:null}const jo=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[jo.Translate.toString(e),jo.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:o}=e;return t+" "+n+"ms "+o}}}),Hp="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function A7(e){return e.matches(Hp)?e:e.querySelector(Hp)}const T7={display:"none"};function D7(e){let{id:t,value:n}=e;return u.createElement("div",{id:t,style:T7},n)}function N7(e){let{id:t,announcement:n,ariaLiveType:o="assertive"}=e;const r={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return u.createElement("div",{id:t,style:r,role:"status","aria-live":o,"aria-atomic":!0},n)}function z7(){const[e,t]=u.useState("");return{announce:u.useCallback(o=>{o!=null&&t(o)},[]),announcement:e}}const Fp=u.createContext(null);function M7(e){const t=u.useContext(Fp);u.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function L7(){const[e]=u.useState(()=>new Set),t=u.useCallback(o=>(e.add(o),()=>e.delete(o)),[e]);return[u.useCallback(o=>{let{type:r,event:s}=o;e.forEach(a=>{var l;return(l=a[r])==null?void 0:l.call(a,s)})},[e]),t]}const P7={draggable:` 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=[];function o(r){if(t!=null&&n.length>=t||!r)return n;if(sa(r)&&r.scrollingElement!=null&&!n.includes(r.scrollingElement))return n.push(r.scrollingElement),n;if(!Oo(r)||Wp(r)||n.includes(r))return n;const s=_t(e).getComputedStyle(r);return r!==e&&r2(r,s)&&n.push(r),o2(r,s)?n:o(r.parentNode)}return e?o(e):n}function qp(e){const[t]=da(e,1);return t??null}function ca(e){return!Jr||!e?null:lo(e)?e:ia(e)?sa(e)||e===co(e).scrollingElement?window:Oo(e)?e:null:null}function Gp(e){return lo(e)?e.scrollX:e.scrollLeft}function Yp(e){return lo(e)?e.scrollY:e.scrollTop}function ua(e){return{x:Gp(e),y:Yp(e)}}var lt;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(lt||(lt={}));function Xp(e){return!Jr||!e?!1:e===document.scrollingElement}function Jp(e){const t={x:0,y:0},n=Xp(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},o={x:e.scrollWidth-n.width,y:e.scrollHeight-n.height},r=e.scrollTop<=t.y,s=e.scrollLeft<=t.x,a=e.scrollTop>=o.y,l=e.scrollLeft>=o.x;return{isTop:r,isLeft:s,isBottom:a,isRight:l,maxScroll:o,minScroll:t}}const i2={x:.2,y:.2};function s2(e,t,n,o,r){let{top:s,left:a,right:l,bottom:d}=n;o===void 0&&(o=10),r===void 0&&(r=i2);const{isTop:c,isBottom:p,isLeft:f,isRight:y}=Jp(e),g={x:0,y:0},m={x:0,y:0},h={height:t.height*r.y,width:t.width*r.x};return!c&&s<=t.top+h.height?(g.y=lt.Backward,m.y=o*Math.abs((t.top+h.height-s)/h.height)):!p&&d>=t.bottom-h.height&&(g.y=lt.Forward,m.y=o*Math.abs((t.bottom-h.height-d)/h.height)),!y&&l>=t.right-h.width?(g.x=lt.Forward,m.x=o*Math.abs((t.right-h.width-l)/h.width)):!f&&a<=t.left+h.width&&(g.x=lt.Backward,m.x=o*Math.abs((t.left+h.width-a)/h.width)),{direction:g,speed:m}}function a2(e){if(e===document.scrollingElement){const{innerWidth:s,innerHeight:a}=window;return{top:0,left:0,right:s,bottom:a,width:s,height:a}}const{top:t,left:n,right:o,bottom:r}=e.getBoundingClientRect();return{top:t,left:n,right:o,bottom:r,width:e.clientWidth,height:e.clientHeight}}function Zp(e){return e.reduce((t,n)=>uo(t,ua(n)),Ht)}function l2(e){return e.reduce((t,n)=>t+Gp(n),0)}function d2(e){return e.reduce((t,n)=>t+Yp(n),0)}function Qp(e,t){if(t===void 0&&(t=Ho),!e)return;const{top:n,left:o,bottom:r,right:s}=t(e);qp(e)&&(r<=0||s<=0||n>=window.innerHeight||o>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const c2=[["x",["left","right"],l2],["y",["top","bottom"],d2]];class pa{constructor(t,n){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const o=da(n),r=Zp(o);this.rect={...t},this.width=t.width,this.height=t.height;for(const[s,a,l]of c2)for(const d of a)Object.defineProperty(this,d,{get:()=>{const c=l(o),p=r[s]-c;return this.rect[d]+p},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Fo{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(n=>{var o;return(o=this.target)==null?void 0:o.removeEventListener(...n)})},this.target=t}add(t,n,o){var r;(r=this.target)==null||r.addEventListener(t,n,o),this.listeners.push([t,n,o])}}function u2(e){const{EventTarget:t}=_t(e);return e instanceof t?e:co(e)}function fa(e,t){const n=Math.abs(e.x),o=Math.abs(e.y);return typeof t=="number"?Math.sqrt(n**2+o**2)>t:"x"in t&&"y"in t?n>t.x&&o>t.y:"x"in t?n>t.x:"y"in t?o>t.y:!1}var At;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(At||(At={}));function Rp(e){e.preventDefault()}function p2(e){e.stopPropagation()}var Ke;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(Ke||(Ke={}));const ef={start:[Ke.Space,Ke.Enter],cancel:[Ke.Esc],end:[Ke.Space,Ke.Enter,Ke.Tab]},f2=(e,t)=>{let{currentCoordinates:n}=t;switch(e.code){case Ke.Right:return{...n,x:n.x+25};case Ke.Left:return{...n,x:n.x-25};case Ke.Down:return{...n,y:n.y+25};case Ke.Up:return{...n,y:n.y-25}}};class tf{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:n}}=t;this.props=t,this.listeners=new Fo(co(n)),this.windowListeners=new Fo(_t(n)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(At.Resize,this.handleCancel),this.windowListeners.add(At.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(At.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:n}=this.props,o=t.node.current;o&&Qp(o),n(Ht)}handleKeyDown(t){if(la(t)){const{active:n,context:o,options:r}=this.props,{keyboardCodes:s=ef,coordinateGetter:a=f2,scrollBehavior:l="smooth"}=r,{code:d}=t;if(s.end.includes(d)){this.handleEnd(t);return}if(s.cancel.includes(d)){this.handleCancel(t);return}const{collisionRect:c}=o.current,p=c?{x:c.left,y:c.top}:Ht;this.referenceCoordinates||(this.referenceCoordinates=p);const f=a(t,{active:n,context:o.current,currentCoordinates:p});if(f){const y=ti(f,p),g={x:0,y:0},{scrollableAncestors:m}=o.current;for(const h of m){const v=t.code,{isTop:b,isRight:x,isLeft:k,isBottom:w,maxScroll:_,minScroll:C}=Jp(h),E=a2(h),S={x:Math.min(v===Ke.Right?E.right-E.width/2:E.right,Math.max(v===Ke.Right?E.left:E.left+E.width/2,f.x)),y:Math.min(v===Ke.Down?E.bottom-E.height/2:E.bottom,Math.max(v===Ke.Down?E.top:E.top+E.height/2,f.y))},T=v===Ke.Right&&!x||v===Ke.Left&&!k,$=v===Ke.Down&&!w||v===Ke.Up&&!b;if(T&&S.x!==f.x){const A=h.scrollLeft+y.x,z=v===Ke.Right&&A<=_.x||v===Ke.Left&&A>=C.x;if(z&&!y.y){h.scrollTo({left:A,behavior:l});return}z?g.x=h.scrollLeft-A:g.x=v===Ke.Right?h.scrollLeft-_.x:h.scrollLeft-C.x,g.x&&h.scrollBy({left:-g.x,behavior:l});break}else if($&&S.y!==f.y){const A=h.scrollTop+y.y,z=v===Ke.Down&&A<=_.y||v===Ke.Up&&A>=C.y;if(z&&!y.x){h.scrollTo({top:A,behavior:l});return}z?g.y=h.scrollTop-A:g.y=v===Ke.Down?h.scrollTop-_.y:h.scrollTop-C.y,g.y&&h.scrollBy({top:-g.y,behavior:l});break}}this.handleMove(t,uo(ti(f,this.referenceCoordinates),g))}}}handleMove(t,n){const{onMove:o}=this.props;t.preventDefault(),o(n)}handleEnd(t){const{onEnd:n}=this.props;t.preventDefault(),this.detach(),n()}handleCancel(t){const{onCancel:n}=this.props;t.preventDefault(),this.detach(),n()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}tf.activators=[{eventName:"onKeyDown",handler:(e,t,n)=>{let{keyboardCodes:o=ef,onActivation:r}=t,{active:s}=n;const{code:a}=e.nativeEvent;if(o.start.includes(a)){const l=s.activatorNode.current;return l&&e.target!==l?!1:(e.preventDefault(),r?.({event:e.nativeEvent}),!0)}return!1}}];function nf(e){return!!(e&&"distance"in e)}function of(e){return!!(e&&"delay"in e)}class ga{constructor(t,n,o){var r;o===void 0&&(o=u2(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=n;const{event:s}=t,{target:a}=s;this.props=t,this.events=n,this.document=co(a),this.documentListeners=new Fo(this.document),this.listeners=new Fo(o),this.windowListeners=new Fo(_t(a)),this.initialCoordinates=(r=ni(s))!=null?r:Ht,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:n,bypassActivationConstraint:o}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(At.Resize,this.handleCancel),this.windowListeners.add(At.DragStart,Rp),this.windowListeners.add(At.VisibilityChange,this.handleCancel),this.windowListeners.add(At.ContextMenu,Rp),this.documentListeners.add(At.Keydown,this.handleKeydown),n){if(o!=null&&o({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(of(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if(nf(n)){this.handlePending(n);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,n){const{active:o,onPending:r}=this.props;r(o,t,this.initialCoordinates,n)}handleStart(){const{initialCoordinates:t}=this,{onStart:n}=this.props;t&&(this.activated=!0,this.documentListeners.add(At.Click,p2,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(At.SelectionChange,this.removeTextSelection),n(t))}handleMove(t){var n;const{activated:o,initialCoordinates:r,props:s}=this,{onMove:a,options:{activationConstraint:l}}=s;if(!r)return;const d=(n=ni(t))!=null?n:Ht,c=ti(r,d);if(!o&&l){if(nf(l)){if(l.tolerance!=null&&fa(c,l.tolerance))return this.handleCancel();if(fa(c,l.distance))return this.handleStart()}if(of(l)&&fa(c,l.tolerance))return this.handleCancel();this.handlePending(l,c);return}t.cancelable&&t.preventDefault(),a(d)}handleEnd(){const{onAbort:t,onEnd:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleCancel(){const{onAbort:t,onCancel:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleKeydown(t){t.code===Ke.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const g2={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class ha extends ga{constructor(t){const{event:n}=t,o=co(n.target);super(t,g2,o)}}ha.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;return!n.isPrimary||n.button!==0?!1:(o?.({event:n}),!0)}}];const h2={move:{name:"mousemove"},end:{name:"mouseup"}};var ya;(function(e){e[e.RightClick=2]="RightClick"})(ya||(ya={}));class y2 extends ga{constructor(t){super(t,h2,co(t.event.target))}}y2.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;return n.button===ya.RightClick?!1:(o?.({event:n}),!0)}}];const ba={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class b2 extends ga{constructor(t){super(t,ba)}static setup(){return window.addEventListener(ba.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(ba.move.name,t)};function t(){}}}b2.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;const{touches:r}=n;return r.length>1?!1:(o?.({event:n}),!0)}}];var Ko;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(Ko||(Ko={}));var ri;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(ri||(ri={}));function m2(e){let{acceleration:t,activator:n=Ko.Pointer,canScroll:o,draggingRect:r,enabled:s,interval:a=5,order:l=ri.TreeOrder,pointerCoordinates:d,scrollableAncestors:c,scrollableAncestorRects:p,delta:f,threshold:y}=e;const g=_2({delta:f,disabled:!s}),[m,h]=$7(),v=u.useRef({x:0,y:0}),b=u.useRef({x:0,y:0}),x=u.useMemo(()=>{switch(n){case Ko.Pointer:return d?{top:d.y,bottom:d.y,left:d.x,right:d.x}:null;case Ko.DraggableRect:return r}},[n,r,d]),k=u.useRef(null),w=u.useCallback(()=>{const C=k.current;if(!C)return;const E=v.current.x*b.current.x,S=v.current.y*b.current.y;C.scrollBy(E,S)},[]),_=u.useMemo(()=>l===ri.TreeOrder?[...c].reverse():c,[l,c]);u.useEffect(()=>{if(!s||!c.length||!x){h();return}for(const C of _){if(o?.(C)===!1)continue;const E=c.indexOf(C),S=p[E];if(!S)continue;const{direction:T,speed:$}=s2(C,S,x,t,y);for(const A of["x","y"])g[A][T[A]]||($[A]=0,T[A]=0);if($.x>0||$.y>0){h(),k.current=C,m(w,a),v.current=$,b.current=T;return}}v.current={x:0,y:0},b.current={x:0,y:0},h()},[t,w,o,h,s,a,JSON.stringify(x),JSON.stringify(g),m,c,_,p,JSON.stringify(y)])}const v2={x:{[lt.Backward]:!1,[lt.Forward]:!1},y:{[lt.Backward]:!1,[lt.Forward]:!1}};function _2(e){let{delta:t,disabled:n}=e;const o=Rr(t);return Wo(r=>{if(n||!o||!r)return v2;const s={x:Math.sign(t.x-o.x),y:Math.sign(t.y-o.y)};return{x:{[lt.Backward]:r.x[lt.Backward]||s.x===-1,[lt.Forward]:r.x[lt.Forward]||s.x===1},y:{[lt.Backward]:r.y[lt.Backward]||s.y===-1,[lt.Forward]:r.y[lt.Forward]||s.y===1}}},[n,t,o])}function x2(e,t){const n=t!=null?e.get(t):void 0,o=n?n.node.current:null;return Wo(r=>{var s;return t==null?null:(s=o??r)!=null?s:null},[o,t])}function w2(e,t){return u.useMemo(()=>e.reduce((n,o)=>{const{sensor:r}=o,s=r.activators.map(a=>({eventName:a.eventName,handler:t(a.handler,o)}));return[...n,...s]},[]),[e,t])}var Vo;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(Vo||(Vo={}));var ma;(function(e){e.Optimized="optimized"})(ma||(ma={}));const rf=new Map;function k2(e,t){let{dragging:n,dependencies:o,config:r}=t;const[s,a]=u.useState(null),{frequency:l,measure:d,strategy:c}=r,p=u.useRef(e),f=v(),y=Bo(f),g=u.useCallback(function(b){b===void 0&&(b=[]),!y.current&&a(x=>x===null?b:x.concat(b.filter(k=>!x.includes(k))))},[y]),m=u.useRef(null),h=Wo(b=>{if(f&&!n)return rf;if(!b||b===rf||p.current!==e||s!=null){const x=new Map;for(let k of e){if(!k)continue;if(s&&s.length>0&&!s.includes(k.id)&&k.rect.current){x.set(k.id,k.rect.current);continue}const w=k.node.current,_=w?new pa(d(w),w):null;k.rect.current=_,_&&x.set(k.id,_)}return x}return b},[e,s,n,f,d]);return u.useEffect(()=>{p.current=e},[e]),u.useEffect(()=>{f||g()},[n,f]),u.useEffect(()=>{s&&s.length>0&&a(null)},[JSON.stringify(s)]),u.useEffect(()=>{f||typeof l!="number"||m.current!==null||(m.current=setTimeout(()=>{g(),m.current=null},l))},[l,f,g,...o]),{droppableRects:h,measureDroppableContainers:g,measuringScheduled:s!=null};function v(){switch(c){case Vo.Always:return!1;case Vo.BeforeDragging:return n;default:return!n}}}function va(e,t){return Wo(n=>e?n||(typeof t=="function"?t(e):e):null,[t,e])}function S2(e,t){return va(e,t)}function C2(e){let{callback:t,disabled:n}=e;const o=Zr(t),r=u.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:s}=window;return new s(o)},[o,n]);return u.useEffect(()=>()=>r?.disconnect(),[r]),r}function ii(e){let{callback:t,disabled:n}=e;const o=Zr(t),r=u.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:s}=window;return new s(o)},[n]);return u.useEffect(()=>()=>r?.disconnect(),[r]),r}function $2(e){return new pa(Ho(e),e)}function sf(e,t,n){t===void 0&&(t=$2);const[o,r]=u.useState(null);function s(){r(d=>{if(!e)return null;if(e.isConnected===!1){var c;return(c=d??n)!=null?c:null}const p=t(e);return JSON.stringify(d)===JSON.stringify(p)?d:p})}const a=C2({callback(d){if(e)for(const c of d){const{type:p,target:f}=c;if(p==="childList"&&f instanceof HTMLElement&&f.contains(e)){s();break}}}}),l=ii({callback:s});return sn(()=>{s(),e?(l?.observe(e),a?.observe(document.body,{childList:!0,subtree:!0})):(l?.disconnect(),a?.disconnect())},[e]),o}function E2(e){const t=va(e);return Kp(e,t)}const af=[];function I2(e){const t=u.useRef(e),n=Wo(o=>e?o&&o!==af&&e&&t.current&&e.parentNode===t.current.parentNode?o:da(e):af,[e]);return u.useEffect(()=>{t.current=e},[e]),n}function A2(e){const[t,n]=u.useState(null),o=u.useRef(e),r=u.useCallback(s=>{const a=ca(s.target);a&&n(l=>l?(l.set(a,ua(a)),new Map(l)):null)},[]);return u.useEffect(()=>{const s=o.current;if(e!==s){a(s);const l=e.map(d=>{const c=ca(d);return c?(c.addEventListener("scroll",r,{passive:!0}),[c,ua(c)]):null}).filter(d=>d!=null);n(l.length?new Map(l):null),o.current=e}return()=>{a(e),a(s)};function a(l){l.forEach(d=>{const c=ca(d);c?.removeEventListener("scroll",r)})}},[r,e]),u.useMemo(()=>e.length?t?Array.from(t.values()).reduce((s,a)=>uo(s,a),Ht):Zp(e):Ht,[e,t])}function lf(e,t){t===void 0&&(t=[]);const n=u.useRef(null);return u.useEffect(()=>{n.current=null},t),u.useEffect(()=>{const o=e!==Ht;o&&!n.current&&(n.current=e),!o&&n.current&&(n.current=null)},[e]),n.current?ti(e,n.current):Ht}function T2(e){u.useEffect(()=>{if(!Jr)return;const t=e.map(n=>{let{sensor:o}=n;return o.setup==null?void 0:o.setup()});return()=>{for(const n of t)n?.()}},e.map(t=>{let{sensor:n}=t;return n}))}function D2(e,t){return u.useMemo(()=>e.reduce((n,o)=>{let{eventName:r,handler:s}=o;return n[r]=a=>{s(a,t)},n},{}),[e,t])}function df(e){return u.useMemo(()=>e?n2(e):null,[e])}const cf=[];function N2(e,t){t===void 0&&(t=Ho);const[n]=e,o=df(n?_t(n):null),[r,s]=u.useState(cf);function a(){s(()=>e.length?e.map(d=>Xp(d)?o:new pa(t(d),d)):cf)}const l=ii({callback:a});return sn(()=>{l?.disconnect(),a(),e.forEach(d=>l?.observe(d))},[e]),r}function uf(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return Oo(t)?t:e}function z2(e){let{measure:t}=e;const[n,o]=u.useState(null),r=u.useCallback(c=>{for(const{target:p}of c)if(Oo(p)){o(f=>{const y=t(p);return f?{...f,width:y.width,height:y.height}:y});break}},[t]),s=ii({callback:r}),a=u.useCallback(c=>{const p=uf(c);s?.disconnect(),p&&s?.observe(p),o(p?t(p):null)},[t,s]),[l,d]=Qr(a);return u.useMemo(()=>({nodeRef:l,rect:n,setRef:d}),[n,l,d])}const M2=[{sensor:ha,options:{}},{sensor:tf,options:{}}],L2={current:{}},si={draggable:{measure:Up},droppable:{measure:Up,strategy:Vo.WhileDragging,frequency:ma.Optimized},dragOverlay:{measure:Ho}};class Uo extends Map{get(t){var n;return t!=null&&(n=super.get(t))!=null?n:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:n}=t;return!n})}getNodeFor(t){var n,o;return(n=(o=this.get(t))==null?void 0:o.node.current)!=null?n:void 0}}const P2={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new Uo,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:oi},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:si,measureDroppableContainers:oi,windowRect:null,measuringScheduled:!1},pf={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:oi,draggableNodes:new Map,over:null,measureDroppableContainers:oi},qo=u.createContext(pf),ff=u.createContext(P2);function O2(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Uo}}}function B2(e,t){switch(t.type){case it.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case it.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case it.DragEnd:case it.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case it.RegisterDroppable:{const{element:n}=t,{id:o}=n,r=new Uo(e.droppable.containers);return r.set(o,n),{...e,droppable:{...e.droppable,containers:r}}}case it.SetDroppableDisabled:{const{id:n,key:o,disabled:r}=t,s=e.droppable.containers.get(n);if(!s||o!==s.key)return e;const a=new Uo(e.droppable.containers);return a.set(n,{...s,disabled:r}),{...e,droppable:{...e.droppable,containers:a}}}case it.UnregisterDroppable:{const{id:n,key:o}=t,r=e.droppable.containers.get(n);if(!r||o!==r.key)return e;const s=new Uo(e.droppable.containers);return s.delete(n),{...e,droppable:{...e.droppable,containers:s}}}default:return e}}function W2(e){let{disabled:t}=e;const{active:n,activatorEvent:o,draggableNodes:r}=u.useContext(qo),s=Rr(o),a=Rr(n?.id);return u.useEffect(()=>{if(!t&&!o&&s&&a!=null){if(!la(s)||document.activeElement===s.target)return;const l=r.get(a);if(!l)return;const{activatorNode:d,node:c}=l;if(!d.current&&!c.current)return;requestAnimationFrame(()=>{for(const p of[d.current,c.current]){if(!p)continue;const f=A7(p);if(f){f.focus();break}}})}},[o,t,r,a,s]),null}function gf(e,t){let{transform:n,...o}=t;return e!=null&&e.length?e.reduce((r,s)=>s({transform:r,...o}),n):n}function j2(e){return u.useMemo(()=>({draggable:{...si.draggable,...e?.draggable},droppable:{...si.droppable,...e?.droppable},dragOverlay:{...si.dragOverlay,...e?.dragOverlay}}),[e?.draggable,e?.droppable,e?.dragOverlay])}function H2(e){let{activeNode:t,measure:n,initialRect:o,config:r=!0}=e;const s=u.useRef(!1),{x:a,y:l}=typeof r=="boolean"?{x:r,y:r}:r;sn(()=>{if(!a&&!l||!t){s.current=!1;return}if(s.current||!o)return;const c=t?.node.current;if(!c||c.isConnected===!1)return;const p=n(c),f=Kp(p,o);if(a||(f.x=0),l||(f.y=0),s.current=!0,Math.abs(f.x)>0||Math.abs(f.y)>0){const y=qp(c);y&&y.scrollBy({top:f.y,left:f.x})}},[t,a,l,o,n])}const ai=u.createContext({...Ht,scaleX:1,scaleY:1});var hn;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(hn||(hn={}));const F2=u.memo(function(t){var n,o,r,s;let{id:a,accessibility:l,autoScroll:d=!0,children:c,sensors:p=M2,collisionDetection:f=Y7,measuring:y,modifiers:g,...m}=t;const h=u.useReducer(B2,void 0,O2),[v,b]=h,[x,k]=L7(),[w,_]=u.useState(hn.Uninitialized),C=w===hn.Initialized,{draggable:{active:E,nodes:S,translate:T},droppable:{containers:$}}=v,A=E!=null?S.get(E):null,z=u.useRef({initial:null,translated:null}),D=u.useMemo(()=>{var J;return E!=null?{id:E,data:(J=A?.data)!=null?J:L2,rect:z}:null},[E,A]),O=u.useRef(null),[M,B]=u.useState(null),[N,H]=u.useState(null),L=Bo(m,Object.values(m)),I=ei("DndDescribedBy",a),P=u.useMemo(()=>$.getEnabled(),[$]),F=j2(y),{droppableRects:G,measureDroppableContainers:V,measuringScheduled:Y}=k2(P,{dragging:C,dependencies:[T.x,T.y],config:F.droppable}),Z=x2(S,E),se=u.useMemo(()=>N?ni(N):null,[N]),re=ue(),U=S2(Z,F.draggable.measure);H2({activeNode:E!=null?S.get(E):null,config:re.layoutShiftCompensation,initialRect:U,measure:F.draggable.measure});const ee=sf(Z,F.draggable.measure,U),fe=sf(Z?Z.parentElement:null),K=u.useRef({activatorEvent:null,active:null,activeNode:Z,collisionRect:null,collisions:null,droppableRects:G,draggableNodes:S,draggingNode:null,draggingNodeRect:null,droppableContainers:$,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),ne=$.getNodeFor((n=K.current.over)==null?void 0:n.id),me=z2({measure:F.dragOverlay.measure}),Ce=(o=me.nodeRef.current)!=null?o:Z,ze=C?(r=me.rect)!=null?r:ee:null,Se=!!(me.nodeRef.current&&me.rect),qe=E2(Se?null:ee),De=df(Ce?_t(Ce):null),Pe=I2(C?ne??Z:null),Qe=N2(Pe),Me=gf(g,{transform:{x:T.x-qe.x,y:T.y-qe.y,scaleX:1,scaleY:1},activatorEvent:N,active:D,activeNodeRect:ee,containerNodeRect:fe,draggingNodeRect:ze,over:K.current.over,overlayNodeRect:me.rect,scrollableAncestors:Pe,scrollableAncestorRects:Qe,windowRect:De}),Re=se?uo(se,T):null,ve=A2(Pe),te=lf(ve),he=lf(ve,[ee]),Q=uo(Me,te),ie=ze?R7(ze,Me):null,_e=D&&ie?f({active:D,collisionRect:ie,droppableRects:G,droppableContainers:P,pointerCoordinates:Re}):null,ge=q7(_e,"id"),[Ae,Ne]=u.useState(null),Le=Se?Me:uo(Me,he),q=Z7(Le,(s=Ae?.rect)!=null?s:null,ee),ae=u.useRef(null),pe=u.useCallback((J,le)=>{let{sensor:Ie,options:dt}=le;if(O.current==null)return;const pt=S.get(O.current);if(!pt)return;const ot=J.nativeEvent,Oe=new Ie({active:O.current,activeNode:pt,event:ot,options:dt,context:K,onAbort(Ye){if(!S.get(Ye))return;const{onDragAbort:Dt}=L.current,Nt={id:Ye};Dt?.(Nt),x({type:"onDragAbort",event:Nt})},onPending(Ye,Tt,Dt,Nt){if(!S.get(Ye))return;const{onDragPending:wn}=L.current,Zt={id:Ye,constraint:Tt,initialCoordinates:Dt,offset:Nt};wn?.(Zt),x({type:"onDragPending",event:Zt})},onStart(Ye){const Tt=O.current;if(Tt==null)return;const Dt=S.get(Tt);if(!Dt)return;const{onDragStart:Nt}=L.current,xn={activatorEvent:ot,active:{id:Tt,data:Dt.data,rect:z}};Cn.unstable_batchedUpdates(()=>{Nt?.(xn),_(hn.Initializing),b({type:it.DragStart,initialCoordinates:Ye,active:Tt}),x({type:"onDragStart",event:xn}),B(ae.current),H(ot)})},onMove(Ye){b({type:it.DragMove,coordinates:Ye})},onEnd:He(it.DragEnd),onCancel:He(it.DragCancel)});ae.current=Oe;function He(Ye){return async function(){const{active:Dt,collisions:Nt,over:xn,scrollAdjustedTranslate:wn}=K.current;let Zt=null;if(Dt&&wn){const{cancelDrop:Hn}=L.current;Zt={activatorEvent:ot,active:Dt,collisions:Nt,delta:wn,over:xn},Ye===it.DragEnd&&typeof Hn=="function"&&await Promise.resolve(Hn(Zt))&&(Ye=it.DragCancel)}O.current=null,Cn.unstable_batchedUpdates(()=>{b({type:Ye}),_(hn.Uninitialized),Ne(null),B(null),H(null),ae.current=null;const Hn=Ye===it.DragEnd?"onDragEnd":"onDragCancel";if(Zt){const ln=L.current[Hn];ln?.(Zt),x({type:Hn,event:Zt})}})}}},[S]),$e=u.useCallback((J,le)=>(Ie,dt)=>{const pt=Ie.nativeEvent,ot=S.get(dt);if(O.current!==null||!ot||pt.dndKit||pt.defaultPrevented)return;const Oe={active:ot};J(Ie,le.options,Oe)===!0&&(pt.dndKit={capturedBy:le.sensor},O.current=dt,pe(Ie,le))},[S,pe]),be=w2(p,$e);T2(p),sn(()=>{ee&&w===hn.Initializing&&_(hn.Initialized)},[ee,w]),u.useEffect(()=>{const{onDragMove:J}=L.current,{active:le,activatorEvent:Ie,collisions:dt,over:pt}=K.current;if(!le||!Ie)return;const ot={active:le,activatorEvent:Ie,collisions:dt,delta:{x:Q.x,y:Q.y},over:pt};Cn.unstable_batchedUpdates(()=>{J?.(ot),x({type:"onDragMove",event:ot})})},[Q.x,Q.y]),u.useEffect(()=>{const{active:J,activatorEvent:le,collisions:Ie,droppableContainers:dt,scrollAdjustedTranslate:pt}=K.current;if(!J||O.current==null||!le||!pt)return;const{onDragOver:ot}=L.current,Oe=dt.get(ge),He=Oe&&Oe.rect.current?{id:Oe.id,rect:Oe.rect.current,data:Oe.data,disabled:Oe.disabled}:null,Ye={active:J,activatorEvent:le,collisions:Ie,delta:{x:pt.x,y:pt.y},over:He};Cn.unstable_batchedUpdates(()=>{Ne(He),ot?.(Ye),x({type:"onDragOver",event:Ye})})},[ge]),sn(()=>{K.current={activatorEvent:N,active:D,activeNode:Z,collisionRect:ie,collisions:_e,droppableRects:G,draggableNodes:S,draggingNode:Ce,draggingNodeRect:ze,droppableContainers:$,over:Ae,scrollableAncestors:Pe,scrollAdjustedTranslate:Q},z.current={initial:ze,translated:ie}},[D,Z,_e,ie,S,Ce,ze,G,$,Ae,Pe,Q]),m2({...re,delta:T,draggingRect:ie,pointerCoordinates:Re,scrollableAncestors:Pe,scrollableAncestorRects:Qe});const j=u.useMemo(()=>({active:D,activeNode:Z,activeNodeRect:ee,activatorEvent:N,collisions:_e,containerNodeRect:fe,dragOverlay:me,draggableNodes:S,droppableContainers:$,droppableRects:G,over:Ae,measureDroppableContainers:V,scrollableAncestors:Pe,scrollableAncestorRects:Qe,measuringConfiguration:F,measuringScheduled:Y,windowRect:De}),[D,Z,ee,N,_e,fe,me,S,$,G,Ae,V,Pe,Qe,F,Y,De]),oe=u.useMemo(()=>({activatorEvent:N,activators:be,active:D,activeNodeRect:ee,ariaDescribedById:{draggable:I},dispatch:b,draggableNodes:S,over:Ae,measureDroppableContainers:V}),[N,be,D,ee,b,I,S,Ae,V]);return u.createElement(Fp.Provider,{value:k},u.createElement(qo.Provider,{value:oe},u.createElement(ff.Provider,{value:j},u.createElement(ai.Provider,{value:q},c)),u.createElement(W2,{disabled:l?.restoreFocus===!1})),u.createElement(B7,{...l,hiddenTextDescribedById:I}));function ue(){const J=M?.autoScrollEnabled===!1,le=typeof d=="object"?d.enabled===!1:d===!1,Ie=C&&!J&&!le;return typeof d=="object"?{...d,enabled:Ie}:{enabled:Ie}}}),K2=u.createContext(null),hf="button",V2="Draggable";function U2(e){let{id:t,data:n,disabled:o=!1,attributes:r}=e;const s=ei(V2),{activators:a,activatorEvent:l,active:d,activeNodeRect:c,ariaDescribedById:p,draggableNodes:f,over:y}=u.useContext(qo),{role:g=hf,roleDescription:m="draggable",tabIndex:h=0}=r??{},v=d?.id===t,b=u.useContext(v?ai:K2),[x,k]=Qr(),[w,_]=Qr(),C=D2(a,t),E=Bo(n);sn(()=>(f.set(t,{id:t,key:s,node:x,activatorNode:w,data:E}),()=>{const T=f.get(t);T&&T.key===s&&f.delete(t)}),[f,t]);const S=u.useMemo(()=>({role:g,tabIndex:h,"aria-disabled":o,"aria-pressed":v&&g===hf?!0:void 0,"aria-roledescription":m,"aria-describedby":p.draggable}),[o,g,h,v,m,p.draggable]);return{active:d,activatorEvent:l,activeNodeRect:c,attributes:S,isDragging:v,listeners:o?void 0:C,node:x,over:y,setNodeRef:k,setActivatorNodeRef:_,transform:b}}function q2(){return u.useContext(ff)}const G2="Droppable",Y2={timeout:25};function X2(e){let{data:t,disabled:n=!1,id:o,resizeObserverConfig:r}=e;const s=ei(G2),{active:a,dispatch:l,over:d,measureDroppableContainers:c}=u.useContext(qo),p=u.useRef({disabled:n}),f=u.useRef(!1),y=u.useRef(null),g=u.useRef(null),{disabled:m,updateMeasurementsFor:h,timeout:v}={...Y2,...r},b=Bo(h??o),x=u.useCallback(()=>{if(!f.current){f.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{c(Array.isArray(b.current)?b.current:[b.current]),g.current=null},v)},[v]),k=ii({callback:x,disabled:m||!a}),w=u.useCallback((S,T)=>{k&&(T&&(k.unobserve(T),f.current=!1),S&&k.observe(S))},[k]),[_,C]=Qr(w),E=Bo(t);return u.useEffect(()=>{!k||!_.current||(k.disconnect(),f.current=!1,k.observe(_.current))},[_,k]),u.useEffect(()=>(l({type:it.RegisterDroppable,element:{id:o,key:s,disabled:n,node:_,rect:y,data:E}}),()=>l({type:it.UnregisterDroppable,key:s,id:o})),[o]),u.useEffect(()=>{n!==p.current.disabled&&(l({type:it.SetDroppableDisabled,id:o,key:s,disabled:n}),p.current.disabled=n)},[o,s,n,l]),{active:a,rect:y,isOver:d?.id===o,node:_,over:d,setNodeRef:C}}function J2(e){let{animation:t,children:n}=e;const[o,r]=u.useState(null),[s,a]=u.useState(null),l=Rr(n);return!n&&!o&&l&&r(l),sn(()=>{if(!s)return;const d=o?.key,c=o?.props.id;if(d==null||c==null){r(null);return}Promise.resolve(t(c,s)).then(()=>{r(null)})},[t,o,s]),u.createElement(u.Fragment,null,n,o?u.cloneElement(o,{ref:a}):null)}const Z2={x:0,y:0,scaleX:1,scaleY:1};function Q2(e){let{children:t}=e;return u.createElement(qo.Provider,{value:pf},u.createElement(ai.Provider,{value:Z2},t))}const R2={position:"fixed",touchAction:"none"},eI=e=>la(e)?"transform 250ms ease":void 0,tI=u.forwardRef((e,t)=>{let{as:n,activatorEvent:o,adjustScale:r,children:s,className:a,rect:l,style:d,transform:c,transition:p=eI}=e;if(!l)return null;const f=r?c:{...c,scaleX:1,scaleY:1},y={...R2,width:l.width,height:l.height,top:l.top,left:l.left,transform:jo.Transform.toString(f),transformOrigin:r&&o?F7(o,l):void 0,transition:typeof p=="function"?p(o):p,...d};return u.createElement(n,{className:a,style:y,ref:t},s)}),nI={duration:250,easing:"ease",keyframes:e=>{let{transform:{initial:t,final:n}}=e;return[{transform:jo.Transform.toString(t)},{transform:jo.Transform.toString(n)}]},sideEffects:(e=>t=>{let{active:n,dragOverlay:o}=t;const r={},{styles:s,className:a}=e;if(s!=null&&s.active)for(const[l,d]of Object.entries(s.active))d!==void 0&&(r[l]=n.node.style.getPropertyValue(l),n.node.style.setProperty(l,d));if(s!=null&&s.dragOverlay)for(const[l,d]of Object.entries(s.dragOverlay))d!==void 0&&o.node.style.setProperty(l,d);return a!=null&&a.active&&n.node.classList.add(a.active),a!=null&&a.dragOverlay&&o.node.classList.add(a.dragOverlay),function(){for(const[d,c]of Object.entries(r))n.node.style.setProperty(d,c);a!=null&&a.active&&n.node.classList.remove(a.active)}})({styles:{active:{opacity:"0"}}})};function oI(e){let{config:t,draggableNodes:n,droppableContainers:o,measuringConfiguration:r}=e;return Zr((s,a)=>{if(t===null)return;const l=n.get(s);if(!l)return;const d=l.node.current;if(!d)return;const c=uf(a);if(!c)return;const{transform:p}=_t(a).getComputedStyle(a),f=Vp(p);if(!f)return;const y=typeof t=="function"?t:rI(t);return Qp(d,r.draggable.measure),y({active:{id:s,data:l.data,node:d,rect:r.draggable.measure(d)},draggableNodes:n,dragOverlay:{node:a,rect:r.dragOverlay.measure(c)},droppableContainers:o,measuringConfiguration:r,transform:f})})}function rI(e){const{duration:t,easing:n,sideEffects:o,keyframes:r}={...nI,...e};return s=>{let{active:a,dragOverlay:l,transform:d,...c}=s;if(!t)return;const p={x:l.rect.left-a.rect.left,y:l.rect.top-a.rect.top},f={scaleX:d.scaleX!==1?a.rect.width*d.scaleX/l.rect.width:1,scaleY:d.scaleY!==1?a.rect.height*d.scaleY/l.rect.height:1},y={x:d.x-p.x,y:d.y-p.y,...f},g=r({...c,active:a,dragOverlay:l,transform:{initial:d,final:y}}),[m]=g,h=g[g.length-1];if(JSON.stringify(m)===JSON.stringify(h))return;const v=o?.({active:a,dragOverlay:l,...c}),b=l.node.animate(g,{duration:t,easing:n,fill:"forwards"});return new Promise(x=>{b.onfinish=()=>{v?.(),x()}})}}let yf=0;function iI(e){return u.useMemo(()=>{if(e!=null)return yf++,yf},[e])}const sI=u.memo(e=>{let{adjustScale:t=!1,children:n,dropAnimation:o,style:r,transition:s,modifiers:a,wrapperElement:l="div",className:d,zIndex:c=999}=e;const{activatorEvent:p,active:f,activeNodeRect:y,containerNodeRect:g,draggableNodes:m,droppableContainers:h,dragOverlay:v,over:b,measuringConfiguration:x,scrollableAncestors:k,scrollableAncestorRects:w,windowRect:_}=q2(),C=u.useContext(ai),E=iI(f?.id),S=gf(a,{activatorEvent:p,active:f,activeNodeRect:y,containerNodeRect:g,draggingNodeRect:v.rect,over:b,overlayNodeRect:v.rect,scrollableAncestors:k,scrollableAncestorRects:w,transform:C,windowRect:_}),T=va(y),$=oI({config:o,draggableNodes:m,droppableContainers:h,measuringConfiguration:x}),A=T?v.setRef:void 0;return u.createElement(Q2,null,u.createElement(J2,{animation:$},f&&E?u.createElement(tI,{key:E,id:f.id,ref:A,as:l,activatorEvent:p,adjustScale:t,className:d,transition:s,rect:T,style:{zIndex:c,...r},transform:S},n):null))});function Go(e,t){return!t||!Array.isArray(t.draggable)?!1:t.draggable.includes(e)}function aI({draggedComponentName:e,targetContainerName:t,nesting:n}){if(!n||!Go(e,n))return!1;const o=n.containers;if(!o||typeof o!="object")return!1;const r=o[t];return!r||!Array.isArray(r.accepts)?!1:r.accepts.includes(e)}function bf(e,t){if(!t?.containers)return!1;const n=t.containers[e];return!!(n&&Array.isArray(n.accepts)&&n.accepts.length>0)}function lI(e,t,n,o,r){if(!n||n.length===0)return{parentPath:[],index:r};if(e&&bf(e,o))return{parentPath:n,index:t};const s=n.slice(0,-1),a=n[n.length-1]??0;return{parentPath:s,index:a+1}}function mf(e,t){return e===t||e.startsWith(`${t}.`)}function vf(e,t){if(e==="")return/^\d+$/.test(t)?Number(t):null;const n=`${e}.children.`;if(!t.startsWith(n))return null;const o=t.slice(n.length);return/^\d+$/.test(o)?Number(o):null}function dI(e,t){const n=[];return e.querySelectorAll("[data-editor-path]").forEach(o=>{if(o.hasAttribute("data-dnd-placeholder")||o.closest("[data-dnd-placeholder]"))return;const r=o.dataset.editorPath;typeof r=="string"&&n.push({path:t(r),domPath:r,rect:o.getBoundingClientRect()})}),n}const cI=16;function uI(e){const{frameEl:t,draggedPath:n,acceptsContainer:o,allowsNestSlot:r,includeContainer:s}=e,a=e.normalizeContainerPath??(g=>g),l=t.getBoundingClientRect(),d=dI(t,a),c=new Map;for(const g of d)c.has(g.path)||c.set(g.path,g.domPath);const p=a(n),f=new Set([""]);for(const g of d){const m=fI(g.path);m!==null&&f.add(m)}for(const g of d)f.add(g.path);const y=[];for(const g of f){const m=g==="",h=m?null:c.get(g)??g,v=m?t:t.querySelector(`[data-editor-path="${wf(h)}"]`);if(!v&&!m||!m&&mf(g,p))continue;const b=g;if(!m&&s&&!s(b))continue;const x=C=>{const E=c.get(C);if(!E||typeof window>"u")return!1;const S=t.querySelector(`[data-editor-path="${wf(E)}"]`);return S?(window.getComputedStyle(S).display??"").toLowerCase()==="contents":!1},k=d.filter(C=>vf(g,C.path)!==null).filter(C=>!mf(C.path,p)).filter(C=>!x(C.path)).map(C=>({origIndex:vf(g,C.path),rect:pI(C.rect,C.path,d)})).filter(C=>C.rect.width>0||C.rect.height>0).sort((C,E)=>C.origIndex-E.origIndex),w=Dp(m?t:v).kind,_=w==="flex_row_single"||w==="flex_row_wrap"||w==="grid_single_row"||w==="grid_2d";if(k.length>0){if(o(b)){k.forEach(E=>{y.push(_f(b,E.origIndex,E.rect,l,_,"before"))});const C=k[k.length-1];y.push(_f(b,C.origIndex+1,C.rect,l,_,"after"))}}else if(!m&&r(b)&&v){const C=v.getBoundingClientRect();C.width>0&&C.height>0&&y.push({id:`slot:${b}:0`,kind:"nest",containerPath:b,index:0,box:xf(C,l),orientation:"area"})}else m&&y.push({id:"slot::0",kind:"gap",containerPath:"",index:0,box:xf(l,l),orientation:"area"})}return y.filter(g=>ao(g.box,t))}function _f(e,t,n,o,r,s){const a=cI;let l;if(r)l={left:(s==="before"?n.left:n.right)-a/2,top:n.top,width:a,height:n.height};else{const d=s==="before"?n.top:n.bottom;l={left:n.left,top:d-a/2,width:n.width,height:a}}return{id:`slot:${e}:${t}`,kind:"gap",containerPath:e,index:t,box:{left:l.left-o.left,top:l.top-o.top,width:l.width,height:l.height,scale:1},orientation:r?"vertical":"horizontal"}}function pI(e,t,n){if(e.width>0||e.height>0)return e;const o=`${t}.children.`;let r=1/0,s=1/0,a=-1/0,l=-1/0;for(const d of n)d.path.startsWith(o)&&(d.rect.width===0&&d.rect.height===0||(r=Math.min(r,d.rect.left),s=Math.min(s,d.rect.top),a=Math.max(a,d.rect.right),l=Math.max(l,d.rect.bottom)));return r===1/0?e:{left:r,top:s,width:a-r,height:l-s,right:a,bottom:l,x:r,y:s,toJSON:()=>({})}}function xf(e,t){return{left:e.left-t.left,top:e.top-t.top,width:e.width,height:e.height,scale:1}}function fI(e){if(e==="")return null;const t=e.lastIndexOf(".children.");return t<0?"":e.slice(0,t)}function gI(e){if(!e.startsWith("slot:"))return null;const t=e.slice(5),n=t.lastIndexOf(":");if(n<0)return null;const o=t.slice(0,n),r=Number(t.slice(n+1));return Number.isFinite(r)?{containerPath:o,index:r}:null}function wf(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function _a(e){return typeof e=="string"&&e.length>0?e:null}function xa(e,t){return t&&e.startsWith(`${t}.`)?t:e}function kf(e){if(typeof e!="string")return null;const t=gI(e);return t?{containerPath:t.containerPath,index:t.index}:null}function wa(e,t){return t?e.startsWith(`${t}.iteration.`):!1}function ka(e,t,n){return t?n==="iteration_item"?wa(e,t):e===t||e.startsWith(`${t}.`):!1}function hI(e){const{frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onMovePath:c,selectedPath:p,showBranchBoundaryHint:f}=e,y=(o==="iteration_item"||o==="modal")&&s&&s.length>0?at(s):null,[g,m]=u.useState(null),[h,v]=u.useState(null),[b,x]=u.useState(null),k=u.useRef({components:a,nesting:n,editMode:o,currentExtensionId:r,selectedPath:p,editableRootSourcePath:y});k.current={components:a,nesting:n,editMode:o,currentExtensionId:r,selectedPath:p,editableRootSourcePath:y};const w=u.useRef(null),_=u.useCallback(M=>({children:M}),[]),C=u.useCallback((M,B)=>{const N=We(_(M),B);return N?typeof N.name=="string"?N.name:typeof N.type=="string"?N.type:null:null},[_]),E=u.useCallback(M=>B=>{const N=k.current;if(B==="")return Go(M,N.nesting);const H=Ze(B),L=We(_(N.components),H);if(!L)return!1;const I=typeof L.name=="string"?L.name:typeof L.type=="string"?L.type:"";if(!I)return!1;const P=!!N.editableRootSourcePath&&(B===N.editableRootSourcePath||B.startsWith(`${N.editableRootSourcePath}.`));if(N.editableRootSourcePath&&!P)return!1;if(!P){const F=Sa(_(N.components),H);if(gn(L,N.editMode,N.currentExtensionId,F)!=="none")return!1}return bf(I,N.nesting)?aI({draggedComponentName:M,targetContainerName:I,nesting:N.nesting}):!1},[_]),S=u.useCallback(M=>{if(M==="")return!0;const B=k.current,N=Ze(M),H=w.current;if(H&&H.length>0){const P=H.slice(0,-1);if(li(N,P))return!0}const L=We(_(B.components),N);return L?(typeof L.name=="string"?L.name:typeof L.type=="string"?L.type:"").length>0:!1},[_]),T=u.useCallback(M=>{const B=E(M),N=k.current,H=w.current,L=H?H.slice(0,-1):null,I=new Set;if(L)for(let V=L.length;V>=0;V--)I.add(di(L.slice(0,V)));const P=di(H??[]),F=V=>!!H&&(V===P||V.startsWith(`${P}.`)),G=(V,Y)=>{for(let Z=0;Z<V.length;Z++){const se=[...Y,Z],re=di(se);!I.has(re)&&!F(re)&&B(re)&&S(re)&&I.add(re);const U=V[Z];if(U&&Array.isArray(U.children)&&G(U.children,se),U&&U.responsive&&typeof U.responsive=="object")for(const ee of Object.keys(U.responsive)){const fe=U.responsive[ee];fe&&Array.isArray(fe.children)&&G(fe.children,[...se,{responsive:ee}])}}};return G(N.components,[]),{acceptsContainer:B,includeContainer:V=>V===""?!0:F(V)?!1:I.has(V),allowsNestSlot:V=>{if(!B(V))return!1;if(L){const Y=Ze(V);if(li(Y,L))return!1}return S(V)}}},[E,S,_]),$=u.useCallback(M=>{const B=_a(M.active.id),N=k.current;if(!B)return;const H=xa(B,N.selectedPath),L=Ze(H),I=We(_(N.components),L),P=I&&typeof I.name=="string"?I.name:I&&typeof I.type=="string"?I.type:null;m(H),v(P),x(null),w.current=I?L:null;let F="allowed";if(!P||!Go(P,N.nesting))F="denied_no_draggable";else if(I){const G=Sa(_(N.components),L),V=gn(I,N.editMode,N.currentExtensionId,G);N.editableRootSourcePath&&!ka(H,N.editableRootSourcePath,N.editMode)||Xi(G)&&!wa(H,N.editableRootSourcePath)?F="denied_data_bound":V==="base"||V==="partial"?F="denied_base_locked":(V==="extension"||V==="extension_point")&&(F="denied_extension_locked")}$n({source:"drag",draggedComponentName:P,targetContainerName:null,targetContainerPath:null,decision:F,result:F==="allowed"?"completed":"denied",timestamp:Date.now()})},[_]),A=u.useCallback(M=>{if(!t)return;const B=_a(M.active.id);if(!B)return;const N=h;if(!N){x(null);return}const H=k.current,L=xa(B,H.selectedPath),I=Ze(L),P=We(_(H.components),I);if(!P||!Go(N,H.nesting)){x(null);return}const F=Sa(_(H.components),I);if(H.editableRootSourcePath&&!ka(L,H.editableRootSourcePath,H.editMode)){x(null);return}const G=gn(P,H.editMode,H.currentExtensionId,F),V=wa(L,H.editableRootSourcePath);if(Xi(F)&&!V||G!=="none"&&G!=="data_bound"&&!V){x(null);return}x(kf(M.over?.id))},[t,h,_]),z=u.useCallback(()=>{m(null),v(null),x(null),w.current=null},[]),D=u.useCallback(M=>{const B=_a(M.active.id),N=h,H=k.current,L=B?xa(B,H.selectedPath):null;if(!t||!L||!N){z();return}const I=kf(M.over?.id),P=Ze(L);if(!I){$n({source:"drag",draggedComponentName:N,targetContainerName:null,targetContainerPath:null,decision:"denied_no_accepts",result:"cancelled",timestamp:Date.now()}),z();return}const F=I.containerPath===""?[]:Ze(I.containerPath),G=I.containerPath===""?null:C(H.components,F);if(!yI(P.slice(0,-1),F)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"denied_responsive_branch_boundary",result:"denied",timestamp:Date.now()}),f?.(),z();return}if(H.editableRootSourcePath&&!ka(I.containerPath,H.editableRootSourcePath,H.editMode)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"denied_data_bound",result:"denied",timestamp:Date.now()}),z();return}const V=P.slice(0,-1),Y=P[P.length-1]??-1;if(li(V,F)&&(I.index===Y||I.index===Y+1)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"allowed",insertionIndex:I.index,result:"cancelled",timestamp:Date.now()}),z();return}const re=li(V,F),U=I.index;let ee=[];l(me=>{const Se=Db({children:me},P,F,U).children??[];return ee=Se,Se}),d(ee,`move ${N}`);const fe=id(F,P),K=re&&Y<U?U-1:U,ne=di([...fe,K]);c?.(ne),$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"allowed",insertionIndex:I.index,result:"completed",timestamp:Date.now()}),z()},[t,h,E,S,C,l,d,z,c]),O=u.useCallback(()=>{z()},[z]);return{activeDragPath:g,activeDragName:h,activeDropZone:b,onDragStart:$,onDragMove:A,onDragEnd:D,onDragCancel:O,buildSlotPredicates:T}}function Sa(e,t){const n=[];let o=e,r=Array.isArray(e.children)?e.children:[];for(let s=0;s<t.length-1;s++){const a=t[s];if(et(a)){const d=o.responsive?.[a.responsive];r=d&&Array.isArray(d.children)?d.children:[];continue}const l=r[a]??null;if(!l)break;n.push(l),o=l,r=Array.isArray(l.children)?l.children:[]}return n}function li(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(!fr(e[n],t[n]))return!1;return!0}function yI(e,t){const n=e.filter(et),o=t.filter(et);if(n.length!==o.length)return!1;for(let r=0;r<n.length;r++)if(n[r].responsive!==o[r].responsive)return!1;return!0}function di(e){return at(e)}function bI(e){const t=/^(.*?)\.iteration\.\d+(?:\.|$)/.exec(e);return t?t[1]:null}function mI(e){const t=Ze(e);return at(t)}function Yo(e,t){return t?e.startsWith(`${t}.iteration.`):!1}function vI(e,t,n){return t?n==="iteration_item"?Yo(e,t):e===t||e.startsWith(`${t}.`):!1}function _I(e){if(e.length===0)return null;let t=1/0,n=1/0,o=-1/0,r=-1/0;for(const s of e)t=Math.min(t,s.left),n=Math.min(n,s.top),o=Math.max(o,s.left+s.width),r=Math.max(r,s.top+s.height);return{left:t,top:n,width:o-t,height:r-n,scale:e[0].scale}}function xI(e){const{frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onSelectPath:c,onMovePath:p,onRequestInlineEdit:f,selectedPath:y}=e,g=(o==="iteration_item"||o==="modal")&&s&&s.length>0?at(s):null,{t:m}=tt(),[h,v]=u.useState(!1),b=u.useRef(null),x=u.useCallback(()=>{v(!0),b.current&&clearTimeout(b.current),b.current=setTimeout(()=>v(!1),2600)},[]);u.useEffect(()=>()=>{b.current&&clearTimeout(b.current)},[]);const k=hI({frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onMovePath:p,selectedPath:y,showBranchBoundaryHint:x}),w=j7(W7(ha,{activationConstraint:{distance:8}})),[_,C]=u.useState([]),E=u.useCallback(()=>{if(!t){C([]);return}if(k.activeDragPath!==null)return;const L={children:a},I=t.querySelectorAll("[data-editor-path]"),P=[],F=new Map;I.forEach(G=>{const V=G.dataset.editorPath;if(typeof V!="string"||o!=="iteration_item"&&g&&!vI(V,g,o))return;const Y=bI(V);if(Y!==null&&!Yo(V,g)){if(/\.iteration\.\d+$/.test(V)){const K=so(G,t);if(K&&ao(K,t)){const ne=F.get(Y)??[];ne.push(K),F.set(Y,ne)}}return}if(Yo(V,g)&&/\.iteration\.\d+$/.test(V))return;const Z=Ze(V),se=We(L,Z);if(!se||se.iteration&&se.iteration.source!==void 0)return;const re=typeof se.name=="string"?se.name:typeof se.type=="string"?se.type:null;if(!re||!Go(re,n))return;const U=Cf(L,Z);if(Xi(U)&&!Yo(V,g))return;const ee=gn(se,o,r,U);if(ee!=="none"&&ee!=="data_bound"&&!Yo(V,g))return;const fe=so(G,t);fe&&ao(fe,t)&&P.push({path:V,name:re,box:fe})});for(const[G,V]of F){const Y=Ze(G),Z=We(L,Y);if(!Z)continue;const se=typeof Z.name=="string"?Z.name:typeof Z.type=="string"?Z.type:null;if(!se)continue;const re=Cf(L,Y),U=gn(Z,o,r,re);if(U!=="none"&&U!=="data_bound")continue;const ee=_I(V);ee&&ao(ee,t)&&P.push({path:G,name:se,box:ee,isIterationGroup:!0})}C(P)},[t,a,n,o,r,g,k.activeDragPath]);u.useEffect(()=>{E()},[E]),u.useEffect(()=>{if(t)return $p(E,t)},[t,E]);const S=k.activeDragPath!==null,T=k.activeDragName,$=k.activeDragPath,[A,z]=u.useState(null),D=u.useRef(null);u.useEffect(()=>{if(!$||!t){$||(D.current=null,z(null));return}if(D.current===$)return;D.current=$;const L=t.querySelector(`[data-editor-path="${Sf($)}"]`);if(L){const re=L.getBoundingClientRect();z({node:L.cloneNode(!0),width:re.width,height:re.height});return}const I=Array.from(t.querySelectorAll(`[data-editor-path^="${Sf($)}.iteration."]`)).filter(re=>/\.iteration\.\d+$/.test(re.dataset.editorPath??""));if(I.length===0){z(null);return}let P=1/0,F=1/0,G=-1/0,V=-1/0;for(const re of I){const U=re.getBoundingClientRect();P=Math.min(P,U.left),F=Math.min(F,U.top),G=Math.max(G,U.right),V=Math.max(V,U.bottom)}const Y=G-P,Z=V-F,se=document.createElement("div");se.style.position="relative",se.style.width=`${Y}px`,se.style.height=`${Z}px`;for(const re of I){const U=re.getBoundingClientRect(),ee=re.cloneNode(!0);ee.style.position="absolute",ee.style.left=`${U.left-P}px`,ee.style.top=`${U.top-F}px`,ee.style.width=`${U.width}px`,ee.style.height=`${U.height}px`,ee.style.margin="0",se.appendChild(ee)}z({node:se,width:Y,height:Z})},[$,t]);const[O,M]=u.useState([]),B=u.useRef(null),N=u.useRef(k.buildSlotPredicates);if(N.current=k.buildSlotPredicates,u.useEffect(()=>{if(!$||!T||!t){B.current=null,M(F=>F.length===0?F:[]);return}if(B.current===$)return;B.current=$;const{acceptsContainer:L,allowsNestSlot:I,includeContainer:P}=N.current(T);M(uI({frameEl:t,draggedPath:$,acceptsContainer:L,allowsNestSlot:I,includeContainer:P,normalizeContainerPath:F=>mI(F)}))},[$,T,t]),!t)return null;const H=k.activeDropZone?`slot:${k.activeDropZone.containerPath}:${k.activeDropZone.index}`:null;return i.jsxs(F2,{sensors:w,collisionDetection:J7,onDragStart:k.onDragStart,onDragMove:k.onDragMove,onDragEnd:k.onDragEnd,onDragCancel:k.onDragCancel,children:[_.map(L=>i.jsx(SI,{path:L.path,name:L.name,box:L.box,dimmed:S&&k.activeDragPath!==L.path,isIterationGroup:L.isIterationGroup,onSelect:c,onRequestInlineEdit:f},L.path)),S&&O.map(L=>i.jsx(kI,{slot:L,active:L.id===H},L.id)),Cn.createPortal(i.jsx(sI,{dropAnimation:null,zIndex:Pp,children:T?A?i.jsx(wI,{name:T,node:A.node,width:A.width,height:A.height}):i.jsx("div",{className:"g7le-dnd-drag-ghost","data-testid":"g7le-dnd-drag-ghost",style:{padding:"4px 10px",background:"rgba(79,70,229,0.95)",color:"#fff",fontSize:12,fontWeight:600,borderRadius:4,pointerEvents:"none",whiteSpace:"nowrap"},children:`<${T}>`}):null}),document.body),h&&Cn.createPortal(i.jsxs("div",{"data-testid":"g7le-branch-boundary-hint",role:"status",style:{position:"absolute",left:"50%",top:12,transform:"translateX(-50%)",background:"rgba(245,158,11,0.97)",color:"#fff",fontSize:13,fontWeight:600,padding:"8px 16px",borderRadius:8,boxShadow:"0 4px 14px rgba(0,0,0,0.2)",pointerEvents:"none",whiteSpace:"nowrap",zIndex:Pp},children:["⚠️ ",m("layout_editor.overlay.branch_boundary_denied")]}),t)]})}function wI(e){const{node:t,width:n,height:o}=e,r=u.useRef(null);return u.useEffect(()=>{const s=r.current;if(s)return s.replaceChildren(),s.appendChild(t.cloneNode(!0)),()=>{s.replaceChildren()}},[t]),i.jsx("div",{ref:r,className:"g7le-dnd-drag-ghost","data-testid":"g7le-dnd-drag-ghost",style:{width:n,height:o,opacity:.7,pointerEvents:"none",boxShadow:"0 6px 18px rgba(0,0,0,0.25)",borderRadius:4,overflow:"hidden",cursor:"grabbing"}})}function kI(e){const{slot:t,active:n}=e,{setNodeRef:o}=X2({id:t.id}),r=t.orientation!=="area";return i.jsx("div",{ref:o,"data-testid":`g7le-dnd-slot-${t.id}`,"data-dnd-slot-id":t.id,style:{position:"absolute",left:t.box.left,top:t.box.top,width:t.box.width,height:t.box.height,pointerEvents:"auto",zIndex:h7,boxSizing:"border-box",...r?{background:n?"rgba(79,70,229,0.15)":"transparent",...n?t.orientation==="vertical"?{borderLeft:"2px solid #4f46e5"}:{borderTop:"2px solid #4f46e5"}:{}}:{border:n?"2px dashed #4f46e5":"1px dashed transparent",background:n?"rgba(79,70,229,0.08)":"transparent",borderRadius:4}}})}function Sf(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function SI(e){const{path:t,box:n,dimmed:o,isIterationGroup:r,onSelect:s,onRequestInlineEdit:a}=e,{attributes:l,listeners:d,setNodeRef:c,isDragging:p}=U2({id:t}),f=(t.match(/\.children\./g)||[]).length,y=y7(f);return i.jsx("div",{ref:c,"data-testid":`g7le-dnd-handle-${t}`,"data-dnd-handle-path":t,"data-dnd-iteration-group":r?"true":void 0,...d,...l,onClick:g=>{if(g.stopPropagation(),!r&&typeof document<"u"&&typeof document.elementsFromPoint=="function"){const m=document.elementsFromPoint(g.clientX,g.clientY);for(const h of m){const b=h.closest?.("[data-editor-path]")?.dataset?.editorPath;if(typeof b=="string"&&(b===t||b.startsWith(`${t}.`))){s(b);return}}}s(t)},onDoubleClick:g=>{g.stopPropagation(),g.preventDefault(),a?.(t)},style:{position:"absolute",left:n.left,top:n.top,width:n.width,height:n.height,cursor:"grab",pointerEvents:"auto",background:p?"rgba(79,70,229,0.06)":"transparent",...r?{border:"1px dashed rgba(79,70,229,0.5)",borderRadius:4}:{},opacity:o?.6:1,zIndex:y,touchAction:"none"}})}function Cf(e,t){const n=[];let o=e,r=Array.isArray(e.children)?e.children:[];for(let s=0;s<t.length-1;s++){const a=t[s];if(et(a)){const d=o.responsive?.[a.responsive];r=d&&Array.isArray(d.children)?d.children:[];continue}const l=r[a]??null;if(!l)break;n.push(l),o=l,r=Array.isArray(l.children)?l.children:[]}return n}function CI(e,t,n,o){if(t==="route")return[];if(t==="modal"||t==="iteration_item")return o&&o.length>0?[o]:[];const r=[],s=(a,l)=>{a.forEach((d,c)=>{const p=[...l,c];if(!Ji(d,t,n)){r.push(p);return}const y=Array.isArray(d.children)?d.children:[];y.length>0&&s(y,p)})};return s(e,[]),r}function $I(e){return at(e)}function ci(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/["\\]/g,"\\$&")}function EI({frameEl:e,components:t,editMode:n,currentExtensionId:o,editableRootPath:r}){const[s,a]=u.useState(null),[l]=u.useState(()=>`g7le-dim-mask-${Math.floor(Math.random()*1e9)}`);if(u.useEffect(()=>{if(!e){a(null);return}let g=0;const m=()=>{const x=CI(t,n,o,r);if(x.length===0){a(null);return}const k=e.getBoundingClientRect(),w={left:0,top:0,width:k.width,height:k.height},_=[];for(const C of x){const E=$I(C),S=e.querySelector(`[data-editor-path="${ci(E)}"]`);let T=null;if(S){const $=S.getBoundingClientRect();$.width>0&&$.height>0&&(T={left:$.left,top:$.top,width:$.width,height:$.height})}if(!T){const $=Array.from(e.querySelectorAll(`[data-editor-path^="${ci(E)}.iteration."], [data-editor-path^="${ci(E)}.sortable."], [data-editor-path^="${ci(E)}.children."]`));let A=1/0,z=1/0,D=-1/0,O=-1/0;for(const M of $){const B=M.getBoundingClientRect();B.width<=0||B.height<=0||(A=Math.min(A,B.left),z=Math.min(z,B.top),D=Math.max(D,B.right),O=Math.max(O,B.bottom))}if(D>A&&O>z&&(T={left:A,top:z,width:D-A,height:O-z}),T&&$.length>0){const M=$[0].closest('[role="dialog"]');if(M&&e.contains(M)){const B=M.getBoundingClientRect();B.width>0&&B.height>0&&(T={left:B.left,top:B.top,width:B.width,height:B.height})}}}T&&_.push({left:T.left-k.left,top:T.top-k.top,width:T.width,height:T.height})}if(_.length===0){a(null);return}a({frame:w,holes:_})},h=()=>{g&&cancelAnimationFrame(g),g=requestAnimationFrame(m)};h();const v=typeof ResizeObserver<"u"?new ResizeObserver(h):null;v?.observe(e);const b=typeof MutationObserver<"u"?new MutationObserver(h):null;return b?.observe(e,{childList:!0,subtree:!0,attributes:!0}),window.addEventListener("scroll",h,!0),window.addEventListener("resize",h),()=>{g&&cancelAnimationFrame(g),v?.disconnect(),b?.disconnect(),window.removeEventListener("scroll",h,!0),window.removeEventListener("resize",h)}},[e,t,n,o,r?.join(".")??""]),!s||s.holes.length===0)return null;const{frame:d,holes:c}=s,p=n==="modal"||n==="iteration_item";let f=[];if(p){let g=1/0,m=1/0,h=-1/0,v=-1/0;for(const C of c)g=Math.min(g,C.left),m=Math.min(m,C.top),h=Math.max(h,C.left+C.width),v=Math.max(v,C.top+C.height);const b=(C,E,S)=>Math.max(E,Math.min(S,C)),x=b(g,0,d.width),k=b(m,0,d.height),w=b(h,0,d.width),_=b(v,0,d.height);f=[{left:0,top:0,width:d.width,height:k},{left:0,top:_,width:d.width,height:d.height-_},{left:0,top:k,width:x,height:_-k},{left:w,top:k,width:d.width-w,height:_-k}].filter(C=>C.width>0&&C.height>0)}const y=g=>{g.preventDefault(),g.stopPropagation()};return i.jsxs(i.Fragment,{children:[f.map((g,m)=>i.jsx("div",{className:"g7le-source-lock-block","data-testid":"g7le-source-lock-block",onClickCapture:y,onPointerDownCapture:y,onMouseDownCapture:y,onDragStartCapture:y,style:{position:"absolute",left:g.left,top:g.top,width:g.width,height:g.height,zIndex:Lp,pointerEvents:"auto",cursor:"not-allowed",background:"transparent"}},m)),i.jsxs("svg",{className:"g7le-source-lock-dim","data-testid":"g7le-source-lock-dim",width:d.width,height:d.height,style:{position:"absolute",left:0,top:0,width:d.width,height:d.height,zIndex:Lp,pointerEvents:"none"},children:[i.jsx("defs",{children:i.jsxs("mask",{id:l,children:[i.jsx("rect",{x:0,y:0,width:d.width,height:d.height,fill:"white"}),c.map((g,m)=>i.jsx("rect",{"data-testid":"g7le-source-lock-dim-hole",x:g.left,y:g.top,width:g.width,height:g.height,rx:4,fill:"black"},m))]})}),i.jsx("rect",{"data-testid":"g7le-source-lock-dim-box",x:0,y:0,width:d.width,height:d.height,fill:"rgba(15, 23, 42, 0.45)",mask:`url(#${l})`})]})]})}function $f(e){const t={type:e.type??"basic",name:e.name},n={};if(e.props)for(const[o,r]of Object.entries(e.props))o!=="children"&&r?.default!==void 0&&(n[o]=r.default);return Object.keys(n).length>0&&(t.props=n),t}function II(e){if(!e?.components)return[];const t=[];for(const[n,o]of Object.entries(e.components))if(Array.isArray(o))for(const r of o)t.push({...r,type:r.type??n});return t}function AI(e,t,n,o,r){const s=II(e);let a=null;if(n&&t?.containers){const l=t.containers[n];l&&Array.isArray(l.accepts)?a=new Set(l.accepts):a=new Set}else!n&&t?.draggable&&(a=new Set(t.draggable));return s.filter(l=>{if(a&&!a.has(l.name))return!1;if(o){const d=o.toLowerCase(),c=r(l).toLowerCase(),p=l.name.toLowerCase();if(!c.includes(d)&&!p.includes(d)&&!`<${p}>`.includes(d)&&!(l.description??"").toLowerCase().includes(d))return!1}return!0})}function TI(e,t){const n={key:"all",label:"layout_editor.palette.category.all",entries:e,isI18nKey:!0};if(t?.groups&&t.groups.length>0){const a=new Map(e.map(p=>[p.name,p])),l=new Set,d=[];for(const p of t.groups){const f=[];for(const y of p.components??[]){const g=a.get(y);g&&(f.push(g),l.add(y))}f.length!==0&&d.push({key:p.kind??p.label,label:p.label,entries:f,isI18nKey:p.label.startsWith("$t:")||/^[a-z_.]+$/i.test(p.label)})}const c=e.filter(p=>!l.has(p.name));return c.length>0&&d.push({key:"other",label:"layout_editor.palette.category.other",entries:c,isI18nKey:!0}),[n,...d]}const o=new Map;for(const a of e){const l=(a.type??"basic").toLowerCase(),d=o.get(l)??[];d.push(a),o.set(l,d)}const r=["basic","composite","layout"],s=[];for(const a of r){const l=o.get(a);l&&l.length>0&&(s.push({key:a,label:`layout_editor.palette.category.fallback.${a}`,entries:l,isI18nKey:!0}),o.delete(a))}for(const[a,l]of o.entries())s.push({key:a,label:a,entries:l,isI18nKey:!1});return[n,...s]}function Ef(e){const{manifest:t,nesting:n,componentPalette:o,targetParentPath:r,targetContainerName:s,onInsert:a,insertionIndex:l,onClose:d,editorTemplateId:c,editorLocale:p}=e,{t:f,translationEngine:y}=tt(),g=u.useMemo(()=>{if(!c||!y)return f;const A=p??null;return z=>{const D={templateId:c,locale:A??"ko"},O=y.translate(z,D);return O!==z?O:f(z)}},[c,p,y,f]),[m,h]=u.useState(""),[v,b]=u.useState("all"),x=o?.entries??null,k=u.useMemo(()=>A=>{const z=x?.[A.name];return z?.label?ui(z.label,g):A.description??A.name},[x,g]),w=u.useMemo(()=>AI(t,n,s,m,k),[t,n,s,m,k]),_=u.useMemo(()=>TI(w,o??null),[w,o]),C=_.find(A=>A.key===v)??_[0]??null,E=!n||!n.draggable&&!n.containers,S=w.length===0,T=C?.entries??[],$=A=>{const z=x?.[A.name],D=!!z?.defaultNode&&typeof z.defaultNode=="object";if(z?.requiresDefaultNode&&!D)return;const O=D?JSON.parse(JSON.stringify(z.defaultNode)):$f(A),M=r??[];a(O,M,l),$n({source:"palette",draggedComponentName:A.name,targetContainerName:s,targetContainerPath:M.length===0?null:M.join("/"),decision:"allowed",insertionIndex:l,result:"completed",timestamp:Date.now()}),d()};return i.jsxs("div",{className:"g7le-palette","data-testid":"g7le-palette",style:MI,children:[i.jsxs("div",{style:LI,children:[i.jsx("strong",{style:{fontSize:16},children:f("layout_editor.palette.title")}),i.jsx("button",{type:"button",onClick:d,"aria-label":"close","data-testid":"g7le-palette-close",style:PI,children:"×"})]}),i.jsxs("div",{style:OI,children:[i.jsx("aside",{style:BI,"data-testid":"g7le-palette-sidebar",children:i.jsx("nav",{children:i.jsx("ul",{style:WI,children:_.map(A=>{const z=C?.key===A.key,D=A.isI18nKey?ui(A.label,g):A.label;return i.jsx("li",{children:i.jsxs("button",{type:"button","data-testid":`g7le-palette-category-${A.key}`,"data-active":z?"true":"false",onClick:()=>b(A.key),style:{...jI,background:z?"#eff6ff":"transparent",color:z?"#1d4ed8":"#334155",fontWeight:z?600:500},children:[i.jsx("span",{children:D}),i.jsx("span",{style:VI,children:A.entries.length})]})},A.key)})})})}),i.jsxs("main",{style:HI,children:[i.jsx("input",{type:"text",placeholder:f("layout_editor.palette.search_placeholder"),value:m,onChange:A=>h(A.target.value),"data-testid":"g7le-palette-search",style:FI}),i.jsx("div",{style:KI,children:E?i.jsx("div",{style:If,"data-testid":"g7le-palette-spec-missing",children:f("layout_editor.palette.spec_missing")}):S||T.length===0?i.jsx("div",{style:If,"data-testid":"g7le-palette-empty",children:f("layout_editor.palette.empty")}):i.jsx("div",{"data-testid":`g7le-palette-group-${C?.key??"all"}`,style:{marginBottom:16},children:i.jsx("div",{style:UI,children:T.map(A=>i.jsx(DI,{entry:A,entrySpec:x?.[A.name]??null,onClick:$,t:f,entryT:g},A.name))})})})]})]})]})}function DI(e){const{entry:t,entrySpec:n,onClick:o,t:r,entryT:s}=e,a=n?.label?ui(n.label,s):t.description??t.name,l=(t.type??"").toLowerCase(),d=l==="basic"||l==="composite"||l==="layout",c=!!n?.defaultNode&&typeof n.defaultNode=="object",p=!!n?.requiresDefaultNode&&!c,f=!c,y=p?r("layout_editor.palette.requires_default_node_missing"):n?.label?ui(n.label,s):t.name;return i.jsxs("button",{type:"button",onClick:()=>o(t),disabled:p,"aria-disabled":p||void 0,"data-testid":`g7le-palette-item-${t.name}`,"data-component-type":l||void 0,"data-blocked":p?"true":void 0,"data-default-node-missing":f?"true":void 0,title:y,style:p?GI:qI,children:[i.jsx("div",{style:XI,"aria-hidden":"true",children:zI(t)}),i.jsx("div",{style:JI,children:a}),i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-tag`,style:ZI,children:`<${t.name}>`}),d&&i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-badge`,style:NI(l),children:l}),f&&i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-default-missing-badge`,style:YI,children:r("layout_editor.palette.default_node_missing_badge")})]})}function ui(e,t){return e.startsWith("$t:")?t(e.slice(3)):t(e)}function NI(e){const n={basic:{bg:"#dbeafe",fg:"#1e40af"},composite:{bg:"#dcfce7",fg:"#14532d"},layout:{bg:"#fef3c7",fg:"#854d0e"}}[e]??{bg:"#e2e8f0",fg:"#334155"};return{fontSize:9,fontWeight:600,color:n.fg,background:n.bg,padding:"1px 6px",borderRadius:999,textTransform:"uppercase",letterSpacing:.4,lineHeight:1.6}}function zI(e){const t=e.name;return{Button:"🔘",A:"🔗",Img:"🖼",Icon:"✨",Hr:"➖",P:"📄",Span:"T",H1:"H₁",H2:"H₂",H3:"H₃",H4:"H₄",Ul:"☰",Li:"•",Nav:"🧭",Div:"▢",Container:"▢",Flex:"⇆",Grid:"▦",SectionLayout:"▤",ThreeColumnLayout:"⫼",Form:"📝",Input:"⎕",Textarea:"▭",Select:"⏷",Checkbox:"☑",PasswordInput:"🔒",Label:"🏷",DataGrid:"▦",Pagination:"↔",SearchBar:"🔍",ProductCard:"🛍",ImageGallery:"🖼",Avatar:"👤",UserInfo:"👥",HtmlContent:"⟨/⟩",HtmlEditor:"✏",RichTextEditor:"✏",TabNavigation:"◰",QuantitySelector:"#",ProductImageViewer:"🖼",FileUploader:"📎",AvatarUploader:"📷",SocialLoginButtons:"🌐",PostReactions:"❤"}[t]??"◻"}const MI={display:"flex",flexDirection:"column",width:"100%",height:"100%",minHeight:0,fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif'},LI={display:"flex",justifyContent:"space-between",alignItems:"center",padding:"14px 20px",borderBottom:"1px solid #e2e8f0",flexShrink:0},PI={background:"transparent",border:"none",fontSize:24,cursor:"pointer",lineHeight:1,color:"#64748b",padding:"0 4px"},OI={display:"flex",flex:1,minHeight:0},BI={width:180,flexShrink:0,borderRight:"1px solid #e2e8f0",padding:"14px 8px",background:"#f8fafc",overflowY:"auto"},WI={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:4},jI={display:"flex",width:"100%",alignItems:"center",justifyContent:"space-between",textAlign:"left",padding:"8px 12px",border:"none",borderRadius:6,fontSize:13,cursor:"pointer",transition:"background-color 120ms"},HI={flex:1,display:"flex",flexDirection:"column",minHeight:0,padding:"14px 20px"},FI={width:"100%",padding:"8px 10px",border:"1px solid #cbd5e1",borderRadius:6,marginBottom:12,fontSize:13,flexShrink:0,boxSizing:"border-box"},KI={flex:1,overflowY:"auto",minHeight:0},VI={fontSize:11,color:"#94a3b8",marginLeft:8,fontWeight:500},UI={display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(96px, 1fr))",gap:10},qI={display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:6,padding:"14px 6px",background:"#fff",border:"1px solid #e2e8f0",borderRadius:8,cursor:"pointer",fontSize:12,minHeight:84,transition:"border-color 120ms, box-shadow 120ms"},GI={display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:6,padding:"14px 6px",background:"#f8fafc",border:"1px dashed #cbd5e1",borderRadius:8,cursor:"not-allowed",fontSize:12,minHeight:84,color:"#94a3b8",opacity:.65},YI={fontSize:9,fontWeight:600,color:"#92400e",background:"#fef3c7",padding:"1px 6px",borderRadius:999,textTransform:"none",letterSpacing:0,lineHeight:1.6},XI={fontSize:22,lineHeight:1,color:"#475569"},JI={fontSize:12,fontWeight:500,color:"#0f172a",textAlign:"center",wordBreak:"break-word"},ZI={fontFamily:'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',fontSize:10,color:"#64748b",textAlign:"center",wordBreak:"break-all"},If={padding:24,color:"#64748b",fontSize:13,textAlign:"center"};function QI(e){const t=[],n=r=>{if(Array.isArray(r))for(const s of r)s&&typeof s=="object"&&t.push(s)};n(e.children);const o=e.responsive;if(o&&typeof o=="object")for(const r of Object.values(o))r&&typeof r=="object"&&n(r.children);return t}function Af(e){const t=[],n=o=>{const r="isolatedState"in o&&o.isolatedState!=null,s=o.isolatedScopeId;(r||typeof s=="string"&&s.length>0)&&t.push({scopeId:typeof s=="string"?s:void 0,initialState:o.isolatedState&&typeof o.isolatedState=="object"?o.isolatedState:void 0});for(const a of QI(o))n(a)};if(Array.isArray(e))for(const o of e)o&&typeof o=="object"&&n(o);return t}const RI=["scroll","selector","wizard","slider"];function eA(e,t){const n=new Set;for(const o of e)o.scopeId&&n.add(o.scopeId);for(const o of t){n.add(o);for(const r of RI)n.add(`${o}-${r}`)}return Array.from(n)}function tA(e,t,n){const o=t.label;return typeof o=="string"?o.startsWith("$t:")?n(o.slice(3)):o:e}function pi({controlKey:e,control:t,node:n,t:o,onPatch:r,candidates:s,bindingCandidates:a,scope:l=Wt}){const d=Tn(t.widget),c=tA(e,t,o),p=u.useMemo(()=>Do(n,t,l),[n,t,l]),f=t.widget==="i18n-text"&&t.apply?.type==="propValue",y=t.widget==="text"&&t.apply?.type==="propValue",g=p.darkReadonly===!0||l.colorScheme==="dark"&&t.apply!==void 0&&!Us(t.apply),m=t.apply,h=l.colorScheme==="dark"&&!g&&m?.type==="classToken"&&typeof m.tokenTemplate=="string"&&m.tokenTemplate.includes("{value}"),v=p.scopedValue===void 0&&p.baseFallback!==void 0,b=u.useCallback(x=>{if(g)return;const k=fn(n,t,x,l);r(k)},[n,t,r,l,g]);return i.jsxs("div",{className:"g7le-control-row","data-testid":`g7le-control-${e}`,style:nA,children:[i.jsxs("span",{className:"g7le-control-label",style:oA,children:[c,p.conflict&&i.jsx("span",{"data-testid":`g7le-control-conflict-${e}`,title:o("layout_editor.property_modal.value_conflict"),style:rA,children:"!"})]}),i.jsx("div",{className:"g7le-control-widget",style:{flex:1,minWidth:0,opacity:v?.45:1},"data-placeholder":v?"true":void 0,children:f?i.jsx(vt,{value:typeof p.value=="string"?p.value:"",onChange:x=>b(x),t:o,testidPrefix:`g7le-prop-i18n-${e}`,candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0}):y?i.jsx(Ve,{value:typeof p.value=="string"?p.value:"",onChange:x=>b(x===""?void 0:x),t:o,testidPrefix:`g7le-prop-value-${e}`,candidates:a}):g?i.jsx("span",{"data-testid":`g7le-control-dark-readonly-${e}`,style:sA,children:o("layout_editor.property_modal.dark_code_only")}):d?i.jsx(d,{control:t,value:p.value,onChange:b,t:o,candidates:s,bindingCandidates:a,freeValueDisabled:h}):i.jsx("span",{"data-testid":`g7le-control-unsupported-${e}`,style:iA,children:o("layout_editor.property_modal.unsupported_widget")})})]})}const nA={display:"flex",alignItems:"center",gap:12,padding:"6px 0",minHeight:32,minWidth:0},oA={fontSize:12,color:"#475569",minWidth:88,display:"inline-flex",alignItems:"center",gap:4},rA={display:"inline-flex",width:14,height:14,borderRadius:7,background:"#f59e0b",color:"#fff",fontSize:10,alignItems:"center",justifyContent:"center",fontWeight:700},iA={fontSize:11,color:"#94a3b8",fontStyle:"italic"},sA={fontSize:11,color:"#94a3b8",fontStyle:"italic"};function fi(e,t,n){return typeof e=="string"?e.startsWith("$t:")?n(e.slice(3)):e:t}function aA(e,t){const n=e.props??{};return t.key in n?n[t.key]:t.default}function lA({spec:e,node:t,t:n,onPatchProp:o}){const r=e.groups??[];return i.jsx("div",{className:"g7le-composite-settings","data-testid":"g7le-composite-settings",style:cA,children:r.map((s,a)=>i.jsxs("div",{className:"g7le-settings-group",style:uA,children:[i.jsx("div",{style:pA,children:fi(s.label,n("layout_editor.property_modal.settings_group",{index:a+1}),n)}),(s.fields??[]).map(l=>i.jsxs("div",{className:"g7le-settings-field","data-testid":`g7le-setting-${l.key}`,style:fA,children:[i.jsx("span",{style:gA,children:fi(l.label,l.key,n)}),i.jsx("div",{style:{flex:1},children:dA(l,t,n,o)})]},l.key))]},a))})}function dA(e,t,n,o){const r=aA(t,e);switch(e.type){case"number":return i.jsx("input",{type:"number","data-testid":`g7le-setting-input-${e.key}`,value:typeof r=="number"?r:"",min:e.min,max:e.max,onChange:s=>o(e.key,s.target.value===""?void 0:Number(s.target.value)),style:Ca});case"text":return i.jsx("input",{type:"text","data-testid":`g7le-setting-input-${e.key}`,value:typeof r=="string"?r:"",onChange:s=>o(e.key,s.target.value===""?void 0:s.target.value),style:Ca});case"toggle":return i.jsx("input",{type:"checkbox","data-testid":`g7le-setting-input-${e.key}`,checked:r===!0,onChange:s=>o(e.key,s.target.checked)});case"checkbox-group":{const s=Array.isArray(r)?r:[];return i.jsx("div",{style:hA,"data-testid":`g7le-setting-input-${e.key}`,children:(e.options??[]).map(a=>{const l=s.includes(a.value);return i.jsxs("label",{style:yA,children:[i.jsx("input",{type:"checkbox",checked:l,onChange:d=>{const c=d.target.checked?[...s,a.value]:s.filter(p=>p!==a.value);o(e.key,c)}}),fi(a.label,String(a.value),n)]},String(a.value))})})}case"select":case"board-select":case"datasource-select":default:return i.jsxs("select",{"data-testid":`g7le-setting-input-${e.key}`,value:r==null?"":String(r),onChange:s=>{const a=s.target.value;if(a===""){o(e.key,void 0);return}const l=(e.options??[]).find(d=>String(d.value)===a);o(e.key,l?l.value:a)},style:Ca,children:[i.jsx("option",{value:"",children:"—"}),(e.options??[]).map(s=>i.jsx("option",{value:String(s.value),children:fi(s.label,String(s.value),n)},String(s.value)))]})}}const cA={display:"flex",flexDirection:"column",gap:16},uA={display:"flex",flexDirection:"column",gap:6},pA={fontSize:12,fontWeight:700,color:"#0f172a",borderBottom:"1px solid #e2e8f0",paddingBottom:4},fA={display:"flex",alignItems:"center",gap:12,minHeight:30},gA={fontSize:12,color:"#475569",minWidth:96},Ca={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",boxSizing:"border-box"},hA={display:"flex",flexWrap:"wrap",gap:10},yA={display:"inline-flex",alignItems:"center",gap:4,fontSize:12,color:"#0f172a"},bA=["data_binding","isolatedScopeId","parentFormContextProp","skipBindingKeys","isolatedState","component_layout","sortable","itemTemplate"],mA=/\{\{|\$switch|\$get|\|\s*(date|number|truncate|currency|relative)\b/;function gi(e,t=0){return t>6?!1:typeof e=="string"?mA.test(e):Array.isArray(e)?e.some(n=>gi(n,t+1)):e&&typeof e=="object"?Object.values(e).some(n=>gi(n,t+1)):!1}function vA(e){if(Zl(e))return!1;const t=Fl(e);return t.length===0?!1:t.some(n=>n.isComplex)}function Tf(e){const t=[],n=e.props??{};for(const o of bA)o in e&&e[o]!==void 0?t.push({key:o,kind:"developer_prop"}):o in n&&n[o]!==void 0&&t.push({key:`props.${o}`,kind:"developer_prop"});typeof e.text=="string"?vA(e.text)&&t.push({key:"text",kind:"complex_expression"}):gi(e.text)&&t.push({key:"text",kind:"complex_expression"});for(const[o,r]of Object.entries(n))o==="style"||o==="className"||gi(r)&&t.push({key:`props.${o}`,kind:"complex_expression"});return t}function _A(e){return Tf(e).length>0}const xA=new Set(["permissions","blur_until_loaded","comment"]);function wA({node:e,advanced:t,t:n,onPatch:o,permissionCandidates:r}){const s=u.useMemo(()=>Tf(e),[e]),a=(l,d)=>{const c={...e};d===void 0?delete c[l]:c[l]=d,o(c)};return i.jsxs("div",{className:"g7le-advanced-props","data-testid":"g7le-advanced-props",style:kA,children:[t.map(l=>{if(!xA.has(l))return i.jsxs("div",{"data-testid":`g7le-advanced-deferred-${l}`,style:CA,children:[n("layout_editor.property_modal.advanced_deferred"),":",l]},l);if(l==="permissions")return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-permissions",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.permissions")}),i.jsx(ys,{control:{widget:"tag-input"},value:e.permissions,onChange:d=>a("permissions",d),t:n,candidates:r})]},l);if(l==="blur_until_loaded"){const d=e.blur_until_loaded===!0||e.blur_until_loaded!=null&&e.blur_until_loaded!==!1;return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-blur",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.blur_until_loaded")}),i.jsxs("label",{style:{display:"inline-flex",alignItems:"center",gap:6,fontSize:12},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-advanced-blur-toggle",checked:!!d,onChange:c=>a("blur_until_loaded",c.target.checked?!0:void 0)}),n("layout_editor.property_modal.advanced.blur_hint")]})]},l)}return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-comment",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.comment")}),i.jsx("input",{type:"text","data-testid":"g7le-advanced-comment-input",value:typeof e.comment=="string"?e.comment:"",onChange:d=>a("comment",d.target.value===""?void 0:d.target.value),style:SA})]},l)}),s.length>0&&i.jsxs("div",{className:"g7le-advanced-preserved","data-testid":"g7le-advanced-preserved",style:$A,children:[i.jsx("div",{style:EA,children:n("layout_editor.property_modal.advanced_preserved_title",{count:s.length})}),i.jsx("ul",{style:IA,children:s.map(l=>i.jsxs("li",{"data-testid":`g7le-advanced-preserved-${l.key}`,style:AA,children:[i.jsx("code",{style:{fontSize:11},children:l.key}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:l.kind==="developer_prop"?n("layout_editor.property_modal.advanced_kind_developer"):n("layout_editor.property_modal.advanced_kind_expression")})]},l.key))})]})]})}const kA={display:"flex",flexDirection:"column",gap:14},$a={display:"flex",flexDirection:"column",gap:6},Ea={fontSize:12,fontWeight:700,color:"#0f172a"},SA={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",boxSizing:"border-box"},CA={fontSize:11,color:"#94a3b8",fontStyle:"italic"},$A={borderTop:"1px dashed #cbd5e1",paddingTop:10,marginTop:4},EA={fontSize:11,color:"#b45309",marginBottom:6},IA={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:4},AA={display:"flex",justifyContent:"space-between",gap:8,padding:"3px 0"},TA={onClick:"click",onHover:"mouseenter",onChange:"change",onSubmit:"submit",onMount:"mount",onUnmount:"unmount",onFocus:"focus",onBlur:"blur"};function Ia(e){return TA[e]??e.replace(/^on/,"").toLowerCase()}function Df(e){const t=e.actions;if(Array.isArray(t))return t.filter(r=>!!r&&typeof r=="object");const n=e.props??{};if(Array.isArray(n.actions))return n.actions.filter(r=>!!r&&typeof r=="object");const o=n.events;if(o&&typeof o=="object"){const r=[];for(const[s,a]of Object.entries(o)){if(!Array.isArray(a))continue;const l=Ia(s);for(const d of a)d&&typeof d=="object"&&r.push("type"in d?d:{type:l,...d})}return r}return[]}function DA(e,t){const n=Ia(t);return Df(e).filter(o=>o.type===n||t==="onClick"&&o.type===void 0)}function NA(e,t,n){const o=Ia(t),r=Df(e).filter(d=>!(d.type===o||t==="onClick"&&d.type===void 0)),s=n.map(d=>"type"in d?d:{type:o,...d}),a={...e};if(a.props&&typeof a.props=="object"){const d={...a.props};let c=!1;"actions"in d&&(delete d.actions,c=!0),"events"in d&&(delete d.events,c=!0),c&&(a.props=d)}const l=[...r,...s];return l.length===0?delete a.actions:a.actions=l,a}function zA({node:e,spec:t,capability:n,t:o,onPatchNode:r,pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,bindingCandidates:d}){const c=t?.actionRecipes,p=u.useMemo(()=>({pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,bindingCandidates:d}),[s,a,l,d]),f=Array.isArray(n?.events)?n.events:[];return f.length===0?i.jsx("div",{"data-testid":"g7le-action-no-events",style:Nf,children:o("layout_editor.action.no_events")}):!c||Object.keys(c).length===0?i.jsx("div",{"data-testid":"g7le-action-no-recipes",style:Nf,children:o("layout_editor.action.no_recipes")}):i.jsx("div",{className:"g7le-action-editor","data-testid":"g7le-action-editor",style:{display:"flex",flexDirection:"column",gap:14},children:f.map(y=>i.jsxs("div",{className:"g7le-action-event-slot","data-testid":`g7le-action-event-${y}`,children:[i.jsx("div",{style:MA,children:o(`layout_editor.action.event.${y}`)}),i.jsx(Gt,{actions:DA(e,y),onChange:g=>r(NA(e,y,g)),t:o,recipes:c,candidatePools:p,actionChipCandidates:t?.actionChipCandidates??null,addContext:"component",showSourceBadge:!0,testIdPrefix:`g7le-action-slot-${y}`})]},y))})}const Nf={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},MA={fontSize:12,fontWeight:700,color:"#475569",marginBottom:6};function Aa(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function LA(e,t){return Aa(t,e.labelKey,e.path||e.sourceId)}function PA({node:e,spec:t,candidates:n,t:o,onPatchNode:r}){const a=(e.props??{})[t.propKey],l=typeof a=="string"?a:void 0,d=u.useMemo(()=>Kt(a),[a]),c=l!==void 0&&l!==""&&d===null,[p,f]=u.useState(!1),[y,g]=u.useState(""),m=Aa(o,t.label,t.propKey),h=o(t.shape==="array"?"layout_editor.binding.shape_array":t.shape==="object"?"layout_editor.binding.shape_object":"layout_editor.binding.shape_scalar"),v=u.useMemo(()=>Fi(n,t.shape).map(_=>({..._,resolvedLabel:LA(_,o)})),[n,t.shape,o]),b=u.useMemo(()=>Ki(v,y),[v,y]),x=w=>{const _={...e.props??{}};w===void 0?delete _[t.propKey]:_[t.propKey]=w,r({...e,props:_})},k=w=>{x(En(w.sourceId,w.path,t.shape)),f(!1),g("")};return i.jsxs("div",{"data-testid":`g7le-binding-row-${t.propKey}`,style:jA,children:[i.jsxs("div",{style:HA,children:[i.jsx("span",{style:{fontWeight:600,fontSize:12},children:m}),i.jsx("span",{style:FA,children:h}),t.required&&i.jsx("span",{"data-testid":`g7le-binding-required-${t.propKey}`,style:KA,children:o("layout_editor.binding.required")})]}),c?i.jsxs("div",{"data-testid":`g7le-binding-complex-${t.propKey}`,style:GA,children:[o("layout_editor.binding.complex"),i.jsx("code",{style:zf,children:l})]}):d?i.jsxs("div",{style:VA,children:[i.jsx("code",{"data-testid":`g7le-binding-expr-${t.propKey}`,style:zf,children:l}),i.jsx("button",{type:"button","data-testid":`g7le-binding-clear-${t.propKey}`,onClick:()=>x(void 0),style:qA,children:o("layout_editor.binding.clear")})]}):i.jsx("div",{"data-testid":`g7le-binding-empty-${t.propKey}`,style:UA,children:o("layout_editor.binding.not_connected")}),d&&(t.shape==="array"||t.shape==="object")&&t.itemFields&&t.itemFields.length>0&&i.jsxs("div",{style:YA,children:[o("layout_editor.binding.item_fields"),": ",t.itemFields.join(", ")]}),!c&&i.jsxs("div",{style:{marginTop:4},children:[i.jsxs("button",{type:"button","data-testid":`g7le-binding-search-toggle-${t.propKey}`,onClick:()=>f(w=>!w),style:XA,children:["🔍 ",o("layout_editor.binding.search")]}),p&&i.jsxs("div",{"data-testid":`g7le-binding-picker-${t.propKey}`,style:JA,children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":`g7le-binding-search-input-${t.propKey}`,value:y,placeholder:o("layout_editor.binding.search_placeholder"),onChange:w=>g(w.target.value),style:ZA}),i.jsx("div",{style:QA,children:b.length===0?i.jsx("div",{style:RA,children:o("layout_editor.binding.no_results")}):b.map(w=>i.jsxs("button",{type:"button","data-testid":`g7le-binding-candidate-${w.expression}`,onClick:()=>k(w),style:e6,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:w.resolvedLabel}),i.jsxs("span",{style:t6,children:[Aa(o,w.groupLabelKey,w.sourceId)," · ",w.path||w.sourceId]})]}),i.jsx("span",{style:n6,children:w.preview})]},w.expression))})]})]})]})}function OA({node:e,dataProps:t,candidates:n,t:o,onPatchNode:r}){return!Array.isArray(t)||t.length===0?null:i.jsxs("div",{"data-testid":"g7le-data-binding-section",style:BA,children:[i.jsxs("div",{style:WA,children:["🔗 ",o("layout_editor.binding.section_title")]}),t.map(s=>i.jsx(PA,{node:e,spec:s,candidates:n,t:o,onPatchNode:r},s.propKey)),i.jsx("div",{style:o6})]})}const BA={display:"flex",flexDirection:"column",marginBottom:8},WA={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},jA={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},HA={display:"flex",alignItems:"center",gap:6},FA={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#e0e7ff",color:"#3730a3"},KA={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#fef3c7",color:"#92400e"},VA={display:"flex",alignItems:"center",gap:6},UA={fontSize:11,color:"#94a3b8"},zf={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},qA={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},GA={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},YA={fontSize:10,color:"#94a3b8"},XA={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 10px",cursor:"pointer",textAlign:"left",width:"100%"},JA={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},ZA={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},QA={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},RA={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},e6={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},t6={fontSize:10,color:"#94a3b8"},n6={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},o6={borderTop:"1px solid #e2e8f0",marginTop:4};function r6({node:e,candidates:t,t:n,onPatchNode:o,templateIdentifier:r}){const s=typeof e.text=="string"?e.text:"",{state:a}=Je(),l=r??a.templateIdentifier,d=a.selectedRoute?.layoutName??null,c=a.locale,[p,f]=u.useState(!1),y=/^\s*\$t:custom\.[A-Za-z0-9._-]+\s*$/.test(s),m=Zl(s)&&(mt(s)?.key.startsWith("custom.")??!1)||y,h=u.useMemo(()=>m?mt(s):null,[m,s]),v=u.useMemo(()=>m?[]:Fl(s),[m,s]),b=T=>{o({...e,text:T})},x=()=>{if(!(typeof window>"u"))try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:l,locale:c}}))}catch{}},k=(T,$)=>{const A=En($.sourceId,$.path,"scalar");b(wb(s,T,A))},w=T=>{const $=mt(s);b(mo(s,T));const A=$?.key;l&&A&&Fd(l,A,T).then(x)},_=T=>{const $=n(T);return $&&$!==T&&!$.startsWith("$t:")?$:""},C=T=>T.replace(/\$t:[a-zA-Z0-9._-]+/g,$=>_($.slice(3))).replace(/\s+/g," ").trim(),E=T=>{p||(f(!0),ls(l,d,c,s,s.length,T.sourceId,T.path,"scalar",C,$=>n($)).then($=>{$.kind==="ok"&&(b($.text),x())}).finally(()=>f(!1)))},S=T=>{p||!h&&!y||(f(!0),wr(l,c,s,Number.MAX_SAFE_INTEGER,T.sourceId,T.path,"scalar").then($=>{$.kind==="ok"&&(b($.text),x())}).finally(()=>f(!1)))};if(m){const T=h?.params??[];return i.jsxs("div",{"data-testid":"g7le-inline-binding-section",style:Mf,children:[i.jsxs("div",{style:Lf,children:["🔗 ",n("layout_editor.inline_binding.section_title")]}),T.length===0?i.jsx("div",{"data-testid":"g7le-inline-binding-none",style:Bf,children:n("layout_editor.inline_binding.none")}):T.map($=>i.jsx("div",{"data-testid":`g7le-inline-binding-param-${$.name}`,style:Pf,children:$.parsed===null?i.jsxs("div",{"data-testid":`g7le-inline-binding-param-complex-${$.name}`,style:s6,children:[n("layout_editor.inline_binding.complex"),i.jsx("code",{style:Ta,children:$.expression})]}):i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Of,children:[i.jsx("code",{"data-testid":`g7le-inline-binding-param-expr-${$.name}`,style:Ta,children:$.expression}),i.jsx("button",{type:"button","data-testid":`g7le-inline-binding-param-clear-${$.name}`,onClick:()=>w($.name),style:i6,children:n("layout_editor.inline_binding.clear")})]}),i.jsx($t,{candidates:t,t:n,onSelect:A=>k($.name,A),testIdSuffix:`param-${$.name}`})]})},$.name)),i.jsxs("div",{"data-testid":"g7le-inline-binding-param-append",style:Wf,children:[i.jsx("div",{style:Da,children:n("layout_editor.inline_binding.append_hint")}),i.jsx($t,{candidates:t,t:n,onSelect:S,testIdSuffix:"param-append"})]}),i.jsx("div",{"data-testid":"g7le-inline-binding-param-hint",style:Da,children:n("layout_editor.inline_binding.param_hint")}),i.jsx("div",{style:jf})]})}return i.jsxs("div",{"data-testid":"g7le-inline-binding-section",style:Mf,children:[i.jsxs("div",{style:Lf,children:["🔗 ",n("layout_editor.inline_binding.section_title")]}),v.length===0?i.jsx("div",{"data-testid":"g7le-inline-binding-none",style:Bf,children:n("layout_editor.inline_binding.none")}):v.map(T=>i.jsx("div",{"data-testid":`g7le-inline-binding-row-${T.bindingIndex}`,style:Pf,children:i.jsx("div",{style:Of,children:i.jsx("code",{"data-testid":`g7le-inline-binding-expr-${T.bindingIndex}`,style:Ta,children:T.expression})})},T.bindingIndex)),i.jsxs("div",{"data-testid":"g7le-inline-binding-append",style:Wf,children:[i.jsx("div",{style:Da,children:n("layout_editor.inline_binding.append_hint")}),i.jsx($t,{candidates:t,t:n,onSelect:E,testIdSuffix:"append"})]}),i.jsx("div",{style:jf})]})}const Mf={display:"flex",flexDirection:"column",marginBottom:8},Lf={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},Pf={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},Of={display:"flex",alignItems:"center",gap:6},Bf={fontSize:11,color:"#94a3b8"},Ta={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},i6={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},s6={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},Wf={display:"flex",flexDirection:"column",gap:2,padding:"8px 0 0"},Da={fontSize:11,color:"#475569",fontWeight:600},jf={borderTop:"1px solid #e2e8f0",marginTop:4};function Hf(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function a6(e,t){return Hf(t,e.labelKey,e.path||e.sourceId)}function l6(e){const t=e.iteration;return t&&typeof t=="object"?t:null}function d6({node:e,candidates:t,t:n,onPatchNode:o}){const r=l6(e),s=r?.source,a=typeof s=="string"?s:void 0,l=u.useMemo(()=>Kt(s),[s]),d=a!==void 0&&a!==""&&l===null,[c,p]=u.useState(!1),[f,y]=u.useState(""),g=u.useMemo(()=>Fi(t,"array").map(w=>({...w,resolvedLabel:a6(w,n)})),[t,n]),m=u.useMemo(()=>Ki(g,f),[g,f]);if(!r)return null;const h=typeof r.item_var=="string"?r.item_var:void 0,v=typeof r.index_var=="string"?r.index_var:void 0,b=k=>{const w={...r};k===void 0?delete w.source:w.source=k,o({...e,iteration:w})},x=k=>{b(En(k.sourceId,k.path,"array")),p(!1),y("")};return i.jsxs("div",{"data-testid":"g7le-iteration-binding-section",style:c6,children:[i.jsxs("div",{style:u6,children:["🔁 ",n("layout_editor.binding.iteration_title")]}),i.jsx("div",{style:p6,children:n("layout_editor.binding.iteration_hint")}),i.jsxs("div",{style:f6,children:[i.jsxs("div",{style:g6,children:[i.jsx("span",{style:{fontWeight:600,fontSize:12},children:n("layout_editor.binding.iteration_label")}),i.jsx("span",{style:h6,children:n("layout_editor.binding.shape_array")})]}),d?i.jsxs("div",{"data-testid":"g7le-iteration-binding-complex",style:v6,children:[n("layout_editor.binding.complex"),i.jsx("code",{style:Ff,children:a})]}):l?i.jsxs("div",{style:y6,children:[i.jsx("code",{"data-testid":"g7le-iteration-binding-expr",style:Ff,children:a}),i.jsx("button",{type:"button","data-testid":"g7le-iteration-binding-clear",onClick:()=>b(void 0),style:m6,children:n("layout_editor.binding.clear")})]}):i.jsx("div",{"data-testid":"g7le-iteration-binding-empty",style:b6,children:n("layout_editor.binding.not_connected")}),(h||v)&&i.jsxs("div",{"data-testid":"g7le-iteration-vars",style:_6,children:[h&&i.jsxs("span",{children:[n("layout_editor.binding.iteration_item_var"),": ",i.jsx("code",{style:Kf,children:h})]}),v&&i.jsxs("span",{children:[n("layout_editor.binding.iteration_index_var"),": ",i.jsx("code",{style:Kf,children:v})]})]}),!d&&i.jsxs("div",{style:{marginTop:4},children:[i.jsxs("button",{type:"button","data-testid":"g7le-iteration-binding-search-toggle",onClick:()=>p(k=>!k),style:x6,children:["🔍 ",n("layout_editor.binding.search")]}),c&&i.jsxs("div",{"data-testid":"g7le-iteration-binding-picker",style:w6,children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":"g7le-iteration-binding-search-input",value:f,placeholder:n("layout_editor.binding.search_placeholder"),onChange:k=>y(k.target.value),style:k6}),i.jsx("div",{style:S6,children:m.length===0?i.jsx("div",{style:C6,children:n("layout_editor.binding.no_results")}):m.map(k=>i.jsxs("button",{type:"button","data-testid":`g7le-iteration-binding-candidate-${k.expression}`,onClick:()=>x(k),style:$6,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:k.resolvedLabel}),i.jsxs("span",{style:E6,children:[Hf(n,k.groupLabelKey,k.sourceId)," · ",k.path||k.sourceId]})]}),i.jsx("span",{style:I6,children:k.preview})]},k.expression))})]})]})]}),i.jsx("div",{style:A6})]})}const c6={display:"flex",flexDirection:"column",marginBottom:8},u6={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:4},p6={fontSize:10,color:"#94a3b8",marginBottom:6,lineHeight:1.4},f6={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},g6={display:"flex",alignItems:"center",gap:6},h6={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#e0e7ff",color:"#3730a3"},y6={display:"flex",alignItems:"center",gap:6},b6={fontSize:11,color:"#94a3b8"},Ff={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},m6={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},v6={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},_6={display:"flex",gap:12,fontSize:10,color:"#94a3b8",marginTop:2},Kf={fontSize:10,fontFamily:"monospace",background:"#f1f5f9",padding:"1px 4px",borderRadius:3,color:"#475569"},x6={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 10px",cursor:"pointer",textAlign:"left",width:"100%"},w6={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},k6={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},S6={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},C6={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},$6={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},E6={fontSize:10,color:"#94a3b8"},I6={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},A6={borderTop:"1px solid #e2e8f0",marginTop:4},T6=new Set(["Span","P","H1","H2","H3","H4","H5","H6","A","Button","Li","Label"]);function D6(e,t){if(!e||e.iteration&&typeof e.iteration=="object")return!1;const n=t?.textBinding;if(n===!1)return!1;if(n===!0)return!0;const o=typeof e.name=="string"?e.name:"";return T6.has(o)?!0:typeof e.text=="string"}const yn={direction:"flexDirection",justify:"flexJustify",align:"flexAlign",wrap:"flexWrap",gap:"flexGap",enable:"flexEnable"},hi={grow:"flexItemGrow",selfAlign:"flexItemAlign",order:"flexItemOrder"};function Vf(e){const t=e?.flexEditor;return t==="container"||t==="item"||t==="auto"?t:null}function N6(e){if(!e||typeof window>"u"||typeof window.getComputedStyle!="function")return!1;const t=(window.getComputedStyle(e).display??"").toLowerCase();return t==="flex"||t==="inline-flex"}function z6(e,t,n=Wt){if(!e||!t)return!1;const o=Do(e,t,n),r=t.options,s=Array.isArray(r)?t.onValue??r[0]?.value:void 0,a=l=>s!==void 0?l===s:l!==void 0&&l!==!1;return n.breakpoint!=="base"?a(o.scopedRawValue):o.matched&&a(o.value)}function M6(e,t,n=!1,o=!1,r=Wt){if(e==="container")return{showContainer:!0,showEnableButton:!1,showDisableButton:!1};if(e==="item")return{showContainer:!1,showEnableButton:!1,showDisableButton:!1};if(e==="auto"){const s=r.breakpoint==="base";return!o&&!s?{showContainer:!1,showEnableButton:!1,showDisableButton:!1}:(o?n:N6(t))?{showContainer:!0,showEnableButton:!1,showDisableButton:o}:{showContainer:!1,showEnableButton:!0,showDisableButton:!1}}return{showContainer:!1,showEnableButton:!1,showDisableButton:!1}}const L6={[yn.direction]:"layout_editor.flex.direction",[yn.justify]:"layout_editor.flex.justify",[yn.align]:"layout_editor.flex.align",[yn.wrap]:"layout_editor.flex.wrap",[yn.gap]:"layout_editor.flex.gap"},P6={[hi.grow]:"layout_editor.flex.item_grow",[hi.selfAlign]:"layout_editor.flex.item_align",[hi.order]:"layout_editor.flex.item_order"};function O6({node:e,spec:t,capability:n,t:o,onPatchNode:r,liveElement:s,scope:a=Wt}){const l=Vf(n),d=Ct(t,yn.enable),c=z6(e,d,a),{showContainer:p,showEnableButton:f,showDisableButton:y}=M6(l,s,c,!!d,a),g=Object.values(yn).filter(_=>_!==yn.enable&&Ct(t,_)),m=Object.values(hi).filter(_=>Ct(t,_)),h=d?.options,v=d?.onValue??h?.[0]?.value??!0,b=Array.isArray(h)&&h.length>=2?h[1].value:void 0,x=()=>{if(!d)return;const _=fn(e,d,v,a);r(_)},k=()=>{if(!d)return;const _=a.breakpoint!=="base"&&b!==void 0?b:void 0,C=fn(e,d,_,a);r(C)},w=(_,C,E)=>i.jsx("div",{"data-testid":E,style:{display:"flex",flexDirection:"column"},children:_.map(S=>{const T=Ct(t,S);if(!T)return null;const $=T.label?T:{...T,label:`$t:${C[S]}`};return i.jsx(pi,{controlKey:S,control:$,node:e,t:o,onPatch:r,scope:a},S)})});return i.jsxs("div",{className:"g7le-flex-editor","data-testid":"g7le-flex-editor",style:{display:"flex",flexDirection:"column",gap:12},children:[f&&i.jsx("button",{type:"button","data-testid":"g7le-flex-enable",onClick:x,style:B6,children:o("layout_editor.flex.make_flex")}),p&&g.length>0&&i.jsxs("div",{className:"g7le-flex-container-section","data-testid":"g7le-flex-container-section",children:[i.jsx("div",{style:Uf,children:o("layout_editor.flex.container_title")}),w(g,L6,"g7le-flex-container-controls"),y&&i.jsx("button",{type:"button","data-testid":"g7le-flex-disable",onClick:k,style:W6,children:o("layout_editor.flex.unmake_flex")})]}),l==="item"&&m.length>0&&i.jsxs("div",{className:"g7le-flex-item-section","data-testid":"g7le-flex-item-section",children:[i.jsx("div",{style:Uf,children:o("layout_editor.flex.item_title")}),w(m,P6,"g7le-flex-item-controls")]}),!p&&!f&&l!=="item"&&i.jsx("div",{"data-testid":"g7le-flex-empty",style:j6,children:o("layout_editor.flex.not_flex")})]})}const Uf={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},B6={padding:"8px 12px",fontSize:12,border:"1px dashed #2563eb",borderRadius:6,background:"#eff6ff",color:"#2563eb",cursor:"pointer"},W6={marginTop:8,padding:"6px 10px",fontSize:12,border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc",color:"#64748b",cursor:"pointer"},j6={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},qf=["desktop","tablet","mobile","portable"],Na=1280,H6=820,F6=390,K6=1920,V6=320;function U6(e){const t=[],n=new Set,o=r=>{if(!r||typeof r!="object")return;const s=r.responsive;if(s&&typeof s=="object")for(const a of Object.keys(s)){n.has(a)||(n.add(a),t.push(a));const l=s[a]?.children;if(Array.isArray(l))for(const d of l)o(d)}if(Array.isArray(r.children))for(const a of r.children)o(a)};if(Array.isArray(e))for(const r of e)o(r);return t}function q6(e){const t=[...qf],n=new Set(qf);for(const o of U6(e))!n.has(o)&&!t.includes(o)&&t.push(o);return t}function G6(e,t){const n=e?.responsive;if(!n||typeof n!="object")return[];const o=[];for(const r of Object.keys(n)){const s=n[r];!s||!Array.isArray(s.children)||r!==t&&o.push(r)}return o}function Y6(e){switch(e){case"desktop":return Na;case"tablet":return H6;case"mobile":return F6}const t=e in To?To[e]:Mn.parseRange(e);return!t||!Number.isFinite(t.max)?Na:Math.round(Math.max(V6,Math.min(K6,t.max)))}function Gf(){const t=Ft()?.document?.raw?.components;return u.useMemo(()=>q6(t??null),[t])}const X6={desktop:"layout_editor.property_modal.scope.device_desktop",tablet:"layout_editor.property_modal.scope.device_tablet",mobile:"layout_editor.property_modal.scope.device_mobile",portable:"layout_editor.property_modal.scope.device_portable"},J6=[{key:"base",labelKey:"layout_editor.property_modal.scope.scheme_light"},{key:"dark",labelKey:"layout_editor.property_modal.scope.scheme_dark"}];function Yf({scope:e,onChange:t,node:n,t:o,showColorScheme:r=!0,onClearScope:s}){const[a,l]=u.useState(!1),[d,c]=u.useState(""),[p,f]=u.useState(""),[y,g]=u.useState(null),m=Gf(),h=u.useMemo(()=>{const k=["base",...m],w=new Set(k);for(const _ of Object.keys(n.responsive??{}))_!=="base"&&!w.has(_)&&(w.add(_),k.push(_));return e.breakpoint!=="base"&&!w.has(e.breakpoint)&&k.push(e.breakpoint),k},[m,n.responsive,e.breakpoint]),v=k=>t({...e,colorScheme:k}),b=k=>t({...e,breakpoint:k}),x=()=>{const k=d.trim(),w=p.trim();if(k===""&&w===""){g(o("layout_editor.property_modal.scope.custom_invalid"));return}const _=`${k}-${w}`;if(!gC(_)){g(o("layout_editor.property_modal.scope.custom_invalid"));return}g(null),l(!1),c(""),f(""),b(_)};return i.jsxs("div",{className:"g7le-style-scope-tabs","data-testid":"g7le-style-scope-tabs",style:Z6,children:[r&&i.jsx("div",{className:"g7le-style-scope-scheme","data-testid":"g7le-style-scope-scheme",role:"tablist","aria-label":o("layout_editor.property_modal.scope.scheme_light"),style:Xf,children:J6.map(k=>{const w=e.colorScheme===k.key;return i.jsxs("button",{type:"button",role:"tab","aria-selected":w,"data-testid":`g7le-style-scheme-${k.key}`,"data-active":w?"true":"false",onClick:()=>v(k.key),style:Jf(w),children:[o(k.labelKey),k.key==="dark"&&Gs(n,{colorScheme:"dark",breakpoint:e.breakpoint})&&i.jsx("span",{"data-testid":"g7le-style-scheme-dark-dot",style:Zf})]},k.key)})}),i.jsxs("div",{className:"g7le-style-scope-device","data-testid":"g7le-style-scope-device",role:"tablist","aria-label":o("layout_editor.property_modal.scope.device_base"),style:Xf,children:[h.map(k=>{const w=e.breakpoint===k,_=k==="base"||wu(k),C=k==="base"?"layout_editor.property_modal.scope.device_base":X6[k],E=C?o(C):k,S=_?`g7le-style-bp-${k}`:`g7le-style-bp-custom-${k}`;return i.jsxs("button",{type:"button",role:"tab","aria-selected":w,"data-testid":S,"data-active":w?"true":"false",onClick:()=>b(k),style:Jf(w),children:[E,Gs(n,{colorScheme:"base",breakpoint:k})&&i.jsx("span",{"data-testid":`${S}-dot`,style:Zf})]},String(k))}),i.jsxs("button",{type:"button","data-testid":"g7le-style-bp-add-custom",onClick:()=>l(k=>!k),style:eT,children:["+ ",o("layout_editor.property_modal.scope.add_custom")]})]}),a&&i.jsxs("div",{className:"g7le-style-scope-custom-input","data-testid":"g7le-style-scope-custom-input",style:Q6,children:[i.jsx("input",{type:"number","data-testid":"g7le-style-custom-min",placeholder:o("layout_editor.property_modal.scope.custom_min"),value:d,onChange:k=>c(k.target.value),style:Qf}),i.jsx("span",{style:{color:"#94a3b8"},children:"—"}),i.jsx("input",{type:"number","data-testid":"g7le-style-custom-max",placeholder:o("layout_editor.property_modal.scope.custom_max"),value:p,onChange:k=>f(k.target.value),style:Qf}),i.jsx("button",{type:"button","data-testid":"g7le-style-custom-confirm",onClick:x,style:tT,children:o("layout_editor.property_modal.scope.add_custom")})]}),y&&i.jsx("div",{"data-testid":"g7le-style-custom-error",style:nT,children:y}),e.breakpoint!=="base"&&!wu(e.breakpoint)&&i.jsx("div",{"data-testid":"g7le-style-custom-priority-note",style:oT,children:o("layout_editor.property_modal.scope.custom_priority_note")}),s&&Gs(n,e)&&i.jsxs("button",{type:"button","data-testid":"g7le-style-scope-reset",onClick:s,style:R6,children:["↺ ",o("layout_editor.property_modal.scope.reset_to_base")]})]})}const Z6={display:"flex",flexDirection:"column",gap:0,padding:"0 0 8px",borderBottom:"1px solid #e2e8f0",marginBottom:8},Q6={display:"flex",gap:6,alignItems:"center",marginTop:4},Xf={display:"flex",gap:2,flexWrap:"wrap",borderBottom:"1px solid #e2e8f0",margin:"0 -16px",padding:"0 12px"};function Jf(e){return{display:"inline-flex",alignItems:"center",gap:4,padding:"6px 12px",fontSize:12,border:"none",borderBottom:`2px solid ${e?"#2563eb":"transparent"}`,background:"transparent",color:e?"#2563eb":"#64748b",cursor:"pointer",fontWeight:e?700:500,marginBottom:-1}}const Zf={display:"inline-block",width:5,height:5,borderRadius:3,background:"#f59e0b"},R6={alignSelf:"flex-start",marginTop:2,padding:"3px 8px",fontSize:10,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},eT={padding:"6px 12px",fontSize:11,border:"none",borderBottom:"2px dashed #cbd5e1",background:"transparent",color:"#475569",cursor:"pointer",marginBottom:-1},Qf={width:70,padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6},tT={padding:"4px 10px",fontSize:11,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},nT={fontSize:11,color:"#dc2626"},oT={fontSize:10,color:"#94a3b8"},Rf=new Map;function Pn(e,t){Rf.set(e,t)}function rT(e){return e?Rf.get(e)??null:null}const za={id:{label:"$t:layout_editor.core_props.id.label",widget:"core-id",placeholder:"$t:layout_editor.core_props.id.placeholder",apply:{type:"propValue",propKey:"id"}},dataKey:{label:"$t:layout_editor.core_props.dataKey.label",widget:"core-datakey",placeholder:"$t:layout_editor.core_props.dataKey.placeholder",apply:{type:"nodeKey",nodeKey:"dataKey"}},isolatedState:{label:"$t:layout_editor.core_props.isolatedState.label",widget:"toggle",apply:{type:"nodeKey",nodeKey:"isolatedState"}},isolatedScopeId:{label:"$t:layout_editor.core_props.isolatedScopeId.label",widget:"core-scopeid",placeholder:"$t:layout_editor.core_props.isolatedScopeId.placeholder",apply:{type:"nodeKey",nodeKey:"isolatedScopeId"}}},iT=["id"];function sT(e){return e===!1?[]:Array.isArray(e)?e.filter(t=>t in za):[...iT]}function eg(e){const t=e.props?.id??e.id;return typeof t=="string"&&t.length>0?`${t}-scope`:"scope"}function aT({node:e,onPatchNode:t,t:n,scopeIdCandidates:o=[],usedScopeIds:r=[]}){const s=e,a="isolatedState"in s&&s.isolatedState!=null,l=typeof s.isolatedScopeId=="string"?s.isolatedScopeId:"",[d,c]=u.useState(""),p=()=>{const h={...e};a?(delete h.isolatedState,delete h.isolatedScopeId):(h.isolatedState={},h.isolatedScopeId=eg(e)),t(h)},f=h=>{const v={...e};v.isolatedScopeId=h.trim()===""?eg(e):h,t(v)},y=h=>{const v={...e};v.isolatedState=h,t(v)},g=u.useMemo(()=>{const h=d.trim().toLowerCase();return o.filter(v=>!h||v.toLowerCase().includes(h))},[o,d]),m=l!==""&&r.includes(l);return i.jsxs("div",{className:"g7le-isolated-scope","data-testid":"g7le-isolated-scope-control",style:lT,children:[i.jsx("div",{style:dT,children:n("layout_editor.core_props.isolatedState.label")}),i.jsxs("div",{style:cT,children:[i.jsx("button",{type:"button","data-testid":"g7le-isolated-toggle",role:"switch","aria-checked":a,onClick:p,style:uT(a),children:n(a?"layout_editor.overlay.on":"layout_editor.overlay.off")}),a?null:i.jsx("span",{"data-testid":"g7le-isolated-off-hint",style:pT,children:n("layout_editor.core_props.isolatedState.off_hint")})]}),a?i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:tg,children:[i.jsx("label",{style:ng,children:n("layout_editor.core_props.isolatedScopeId.label")}),i.jsx("input",{type:"text","data-testid":"g7le-isolated-scopeid-input",value:l,placeholder:n("layout_editor.core_props.isolatedScopeId.placeholder"),onChange:h=>f(h.target.value),style:fT}),g.length>0?i.jsxs("div",{"data-testid":"g7le-isolated-scopeid-picker",style:gT,children:[i.jsx("input",{type:"text","data-testid":"g7le-isolated-scopeid-search",value:d,placeholder:n("layout_editor.core_props.isolatedScopeId.search"),onChange:h=>c(h.target.value),style:hT}),i.jsx("div",{style:yT,children:g.map(h=>i.jsx("button",{type:"button","data-testid":`g7le-isolated-scopeid-candidate-${h}`,onClick:()=>{f(h),c("")},style:bT,children:h},h))})]}):null,m?i.jsxs("p",{"data-testid":"g7le-isolated-scopeid-dup",style:mT,children:["ⓘ ",n("layout_editor.core_props.isolatedScopeId.duplicate")]}):null]}),i.jsxs("div",{style:tg,children:[i.jsx("label",{style:ng,children:n("layout_editor.core_props.isolatedState.initial_label")}),i.jsx("div",{"data-testid":"g7le-isolated-initial-value",children:i.jsx(Nn,{value:s.isolatedState??{},onChange:y,t:n,path:"isolatedState",scope:"isolated-node"})})]})]}):null]})}const lT={display:"flex",flexDirection:"column",gap:8,padding:10,border:"1px solid #e2e8f0",borderRadius:8,minWidth:0},dT={fontSize:12,fontWeight:700,color:"#0f172a"},cT={display:"flex",gap:8,alignItems:"center"},uT=e=>({padding:"4px 12px",fontSize:11,border:`1px solid ${e?"#3b82f6":"#cbd5e1"}`,borderRadius:14,background:e?"#eff6ff":"#fff",color:e?"#1d4ed8":"#64748b",cursor:"pointer"}),pT={fontSize:11,color:"#94a3b8"},tg={display:"flex",flexDirection:"column",gap:4,minWidth:0},ng={fontSize:11,color:"#64748b"},fT={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box",width:"100%"},gT={border:"1px solid #e2e8f0",borderRadius:6,padding:4,background:"#fff"},hT={width:"100%",padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:4,boxSizing:"border-box"},yT={display:"flex",flexDirection:"column",maxHeight:120,overflowY:"auto"},bT={textAlign:"left",padding:"4px 6px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:4,color:"#0f172a"},mT={margin:0,fontSize:11,color:"#64748b"},vT={borderTop:"2px solid #c7d2fe",background:"#eef2ff",padding:"10px 12px",display:"flex",flexDirection:"column",gap:8},og={fontSize:13,fontWeight:700,color:"#3730a3",display:"flex",alignItems:"center",gap:6},_T={fontSize:11,fontWeight:600,color:"#4338ca",background:"#e0e7ff",border:"1px solid #c7d2fe",borderRadius:4,padding:"1px 6px"},Ma={fontSize:11,color:"#6366f1"},xT={alignSelf:"flex-start",fontSize:12,fontWeight:600,color:"#fff",background:"#4f46e5",border:"none",borderRadius:4,padding:"4px 10px",cursor:"pointer"},wT={opacity:.5,cursor:"not-allowed"},kT={fontSize:11,color:"#dc2626"};function ST({entry:e,t,onSave:n}){const[o,r]=u.useState(()=>e.props),[s,a]=u.useState(!0),[l,d]=u.useState(null),[c,p]=u.useState(!1),[f,y]=u.useState(!1),g=e.extensionName??e.extensionIdentifier??`#${e.extensionId}`,m=async()=>{if(!s){d(t("layout_editor.property_modal.injected_props.invalid_json"));return}d(null),p(!0),y(!1);try{await n(o),y(!0)}catch(h){d(h instanceof Error?h.message:String(h))}finally{p(!1)}};return i.jsxs("div",{"data-testid":`g7le-injected-props-entry-${e.extensionId}`,style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:og,children:i.jsx("span",{style:_T,"data-testid":"g7le-injected-props-origin-badge",children:t("layout_editor.property_modal.injected_props.origin",{source:g})})}),i.jsx(Bs,{value:o,shape:"object",emptyValue:{},testidPrefix:"g7le-injected-props-json",minHeight:80,invalidErrorKey:"layout_editor.property_modal.injected_props.invalid_json",shapeErrorKey:"layout_editor.property_modal.injected_props.invalid_json",t,onChange:h=>{r(h??{}),y(!1)},onValidityChange:h=>{a(h),h&&d(null),y(!1)}}),l&&i.jsx("div",{style:kT,"data-testid":"g7le-injected-props-error",children:l}),f&&i.jsx("div",{style:{...Ma,color:"#16a34a"},"data-testid":"g7le-injected-props-saved",children:t("layout_editor.property_modal.injected_props.saved")}),i.jsx("div",{style:Ma,children:t("layout_editor.property_modal.injected_props.save_target")}),i.jsx("button",{type:"button","data-testid":"g7le-injected-props-save",style:{...xT,...c||!s?wT:null},disabled:c||!s,onClick:m,children:c?t("layout_editor.property_modal.injected_props.saving"):t("layout_editor.property_modal.injected_props.save_button",{source:g})})]})}function CT({injectedProps:e,t,onSaveInjectedProps:n}){return!Array.isArray(e)||e.length===0?null:i.jsxs("div",{style:vT,"data-testid":"g7le-injected-props-section",children:[i.jsxs("div",{style:og,children:["🧩 ",t("layout_editor.property_modal.injected_props.title")]}),i.jsx("div",{style:Ma,children:t("layout_editor.property_modal.injected_props.help")}),e.map(o=>i.jsx(ST,{entry:o,t,onSave:r=>n(o.extensionId,r)},o.extensionId))]})}function $T(e,t){const n=e?.components??{};for(const o of Object.values(n)){const r=o?.find(s=>s.name===t);if(r)return r}return null}function ET({node:e,spec:t,manifest:n,t:o,onPatchNode:r,onClose:s,onDelete:a,permissionCandidates:l,pageCandidates:d,dataSourceCandidates:c,stateKeyCandidates:p,bindingCandidates:f,liveElement:y,scope:g=Wt,onScopeChange:m,templateIdentifier:h,initialTab:v,onSaveInjectedProps:b,branchLabel:x,isolatedScopeIdCandidates:k,usedScopeIds:w}){const _=e.__injectedProps,C=Array.isArray(_)&&_.length>0&&!!b,E=ve=>{m?.(ve)},S=()=>{r(xC(e,g))},T=typeof e.name=="string"?e.name:"",$=Oi(t,T),z=$T(n,T)?.settings,D=!!(z?.groups&&z.groups.length>0),O=Array.isArray($?.propControls)?$.propControls:[],M=sT($?.coreProps),B=new Set(M.map(ve=>za[ve].apply?.propKey)),N=O.filter(ve=>{const he=Ct(t,ve)?.apply?.propKey;return!(he!==void 0&&B.has(he))}),H=typeof $?.nodeEditor?.kind=="string"?$.nodeEditor.kind:void 0,L=$?.nodeEditor?.params,I=H?rT(H):null,P=!!I,F=Array.isArray($?.dataProps)?$.dataProps:[],G=F.length>0,V=!!(e.iteration&&typeof e.iteration=="object"),Y=D6(e,$),Z=M.length>0||N.length>0||P||G||Y||V,se=Array.isArray($?.styleControls)?$.styleControls:[],re=Array.isArray($?.advanced)?$.advanced:[],ee=(Array.isArray($?.events)?$.events:[]).length>0,fe=Vf($),K=se.length>0||fe!==null,me=!($?.visibilityCondition===!1)&&(Z||K||ee||D||re.length>0),Ce=u.useMemo(()=>{const ve=[];return D&&ve.push("settings"),Z&&ve.push("props"),K&&ve.push("style"),ee&&ve.push("action"),me&&ve.push("visibility"),ve.push("translation"),re.length>0&&ve.push("advanced"),ve},[D,Z,K,ee,me,re.length]),ze=N.length>0||P||G||V,[Se,qe]=u.useState(v&&Ce.includes(v)?v:D?"settings":ze?"props":K?"style":Z?"props":Ce[0]??"style"),De=_A(e),Pe=!Z&&!K&&!D&&!ee&&!me&&re.length===0,Qe=$?.label?$.label.startsWith("$t:")?o($.label.slice(3)):$.label:T||o("layout_editor.property_modal.element"),Me=(ve,te)=>{const he={...e.props??{}};te===void 0?delete he[ve]:he[ve]=te,r({...e,props:he})},Re={settings:o("layout_editor.property_modal.tab.settings"),props:o("layout_editor.property_modal.tab.props"),style:o("layout_editor.property_modal.tab.style"),action:o("layout_editor.property_modal.tab.action"),visibility:o("layout_editor.property_modal.tab.visibility"),translation:o("layout_editor.property_modal.tab.translation"),advanced:o("layout_editor.property_modal.tab.advanced")};return i.jsxs("div",{className:"g7le-property-modal","data-testid":"g7le-property-modal",style:IT,children:[i.jsxs("div",{"data-modal-drag-handle":!0,"data-testid":"g7le-property-modal-header",style:AT,children:[i.jsx("span",{"data-testid":"g7le-property-modal-title",style:{fontSize:14,fontWeight:700},children:D&&Se==="settings"?o("layout_editor.property_modal.settings_title",{name:Qe}):o("layout_editor.property_modal.title",{name:Qe})}),i.jsx("button",{type:"button","data-testid":"g7le-property-modal-close","aria-label":"close",onClick:s,style:TT,children:"✕"})]}),De&&i.jsx("div",{"data-testid":"g7le-property-modal-advanced-badge",style:DT,children:o("layout_editor.property_modal.advanced_badge")}),x&&i.jsx("div",{"data-testid":"g7le-property-modal-branch-badge",style:NT,children:o("layout_editor.property_modal.branch_badge",{device:x})}),i.jsx("div",{style:zT,role:"tablist",children:Ce.map(ve=>i.jsx("button",{type:"button",role:"tab","aria-selected":Se===ve,"data-testid":`g7le-property-tab-${ve}`,"data-active":Se===ve?"true":"false",onClick:()=>qe(ve),style:{...MT,color:Se===ve?"#2563eb":"#64748b",borderBottomColor:Se===ve?"#2563eb":"transparent"},children:Re[ve]},ve))}),i.jsxs("div",{style:LT,"data-testid":"g7le-property-modal-body",children:[Pe&&!C&&i.jsx("div",{"data-testid":"g7le-property-modal-no-editable",style:ig,children:o("layout_editor.property_modal.no_editable_props")}),C&&i.jsx(CT,{injectedProps:_,t:o,onSaveInjectedProps:b}),Se==="settings"&&z&&i.jsx(lA,{spec:z,node:e,t:o,onPatchProp:Me}),Se==="props"&&i.jsxs("div",{"data-testid":"g7le-property-props-tab",style:{display:"flex",flexDirection:"column"},children:[V&&i.jsx(d6,{node:e,candidates:f??[],t:o,onPatchNode:r}),G&&i.jsx(OA,{node:e,dataProps:F,candidates:f??[],t:o,onPatchNode:r}),Y&&i.jsx(r6,{node:e,candidates:f??[],t:o,onPatchNode:r,templateIdentifier:h}),P&&I&&i.jsx(I,{node:e,params:L,spec:t,manifest:n,t:o,onPatchNode:r,templateIdentifier:h,candidates:f}),M.map(ve=>i.jsx(pi,{controlKey:`core-${ve}`,control:za[ve],node:e,t:o,onPatch:r,scope:Wt,bindingCandidates:f},`core-${ve}`)),N.map(ve=>{const te=Ct(t,ve);return te?i.jsx(pi,{controlKey:ve,control:te,node:e,t:o,onPatch:r,scope:Wt,bindingCandidates:f},ve):null}),i.jsx(aT,{node:e,onPatchNode:r,t:o,scopeIdCandidates:k,usedScopeIds:w})]}),Se==="style"&&i.jsxs("div",{"data-testid":"g7le-property-style-tab",style:{display:"flex",flexDirection:"column"},children:[i.jsx("div",{style:rg,children:i.jsx(Yf,{scope:g,onChange:E,node:e,t:o,onClearScope:S})}),fe!==null&&i.jsx(O6,{node:e,spec:t,capability:$,t:o,onPatchNode:r,liveElement:y,scope:g}),se.map(ve=>{const te=Ct(t,ve);return te?i.jsx(pi,{controlKey:ve,control:te,node:e,t:o,onPatch:r,scope:g},ve):null})]}),Se==="action"&&i.jsx(zA,{node:e,spec:t,capability:$,t:o,onPatchNode:r,pageCandidates:d,dataSourceCandidates:c,stateKeyCandidates:p,bindingCandidates:f}),Se==="visibility"&&i.jsxs("div",{"data-testid":"g7le-property-visibility-tab",style:{display:"flex",flexDirection:"column"},children:[i.jsx("div",{style:rg,children:i.jsx(Yf,{scope:g,onChange:E,node:e,t:o,showColorScheme:!1})}),i.jsx(Eu,{node:e,spec:t,t:o,onPatchNode:r,dataSourceCandidates:c,stateKeyCandidates:p,scope:g})]}),Se==="translation"&&(h?i.jsx(Ud,{customKey:Vd(e),templateIdentifier:h,t:o,paramLabels:hv(e),onRemoveParam:ve=>{const te=typeof e.text=="string"?e.text:"";r({...e,text:mo(te,ve)});const he=Vd(e);h&&he&&kr(h,he,ve)}}):i.jsx("div",{"data-testid":"g7le-property-translation-deferred",style:ig,children:o("layout_editor.translation.not_a_key")})),Se==="advanced"&&i.jsx(wA,{node:e,advanced:re,t:o,onPatch:r,permissionCandidates:l})]}),i.jsxs("div",{style:PT,children:[i.jsx("span",{"data-testid":"g7le-property-modal-node-id",style:{fontSize:11,color:"#94a3b8"},children:typeof e.id=="string"||typeof e.id=="number"?`ID: ${e.id}`:""}),i.jsxs("div",{style:{display:"flex",gap:8},children:[i.jsx("button",{type:"button","data-testid":"g7le-property-modal-delete",onClick:a,style:OT,children:o("layout_editor.property_modal.delete_element")}),i.jsx("button",{type:"button","data-testid":"g7le-property-modal-done",onClick:s,style:BT,children:o("layout_editor.property_modal.close")})]})]})]})}const IT={display:"flex",flexDirection:"column",width:420,maxWidth:"92vw"},AT={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0",cursor:"move",userSelect:"none"},TT={border:"none",background:"transparent",fontSize:14,cursor:"pointer",color:"#64748b"},DT={margin:"8px 16px 0",padding:"6px 10px",fontSize:11,background:"#fef3c7",color:"#92400e",borderRadius:6},NT={margin:"8px 16px 0",padding:"6px 10px",fontSize:11,background:"#dbeafe",color:"#1e40af",borderRadius:6},zT={display:"flex",gap:2,padding:"0 12px",borderBottom:"1px solid #e2e8f0",overflowX:"auto"},MT={padding:"8px 10px",fontSize:12,border:"none",borderBottom:"2px solid transparent",background:"transparent",cursor:"pointer",whiteSpace:"nowrap"},LT={padding:16,overflowY:"auto",maxHeight:"60vh"},rg={position:"sticky",top:-16,zIndex:2,background:"#fff",margin:"-16px -16px 0",padding:"6px 16px 0"},ig={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},PT={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderTop:"1px solid #e2e8f0"},OT={padding:"6px 12px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},BT={padding:"6px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},La="g7le:component-target-picked",Pa="g7le:component-target-pick-request";let WT=1;function jT({value:e,onChange:t,t:n,control:o}){const r=yo(),s=e==null?"":String(e),a=o?.allowManualInput!==!1,l=u.useCallback(()=>{if(typeof window>"u")return;const d=`pick-${WT++}`,p=r.stack[r.stack.length-1]?.id;p&&r.minimize(p,n("layout_editor.target_picker.minimized_hint"));const f=y=>{const g=y.detail;g?.requestId===d&&(window.removeEventListener(La,f),p&&r.restore(p),!g.cancelled&&typeof g.id=="string"&&t(g.id))};window.addEventListener(La,f),window.dispatchEvent(new CustomEvent(Pa,{detail:{requestId:d}}))},[r,t,n]);return i.jsx("div",{className:"g7le-component-target-picker","data-testid":"g7le-component-target-picker",children:i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[a?i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--text","data-testid":"g7le-component-target-input",value:s,placeholder:n("layout_editor.target_picker.id_placeholder"),onChange:d=>t(d.target.value===""?void 0:d.target.value),style:{flex:1,minWidth:0,padding:"6px 8px",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13}}):i.jsx("span",{"data-testid":"g7le-component-target-value",style:{flex:1,minWidth:0},children:s||n("layout_editor.target_picker.none")}),i.jsxs("button",{type:"button","data-testid":"g7le-component-target-pick",onClick:l,style:{display:"flex",alignItems:"center",gap:4,padding:"6px 10px",background:"#1e293b",color:"#fff",border:"none",borderRadius:6,cursor:"pointer",fontSize:13,whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",children:"🎯"}),n("layout_editor.target_picker.select_area")]})]})})}async function HT(e,t,n,o,r=fetch){const s=`/api/admin/templates/${encodeURIComponent(e)}/layout-extensions/${t}`;let a;try{a=await r(s,{headers:je(),credentials:"same-origin"})}catch(b){return{kind:"error",message:b instanceof Error?b.message:String(b)}}if(a.status===404)return{kind:"not_found"};if(!a.ok)return{kind:"error",message:`HTTP ${a.status}`};const l=await a.json().catch(()=>null),d=(l&&(l.data||l))??{};let c={};const p=d.content;if(typeof p=="string")try{c=JSON.parse(p)}catch{return{kind:"error",message:"invalid extension content"}}else p&&typeof p=="object"&&(c=p);const f=typeof d.lock_version=="number"?d.lock_version:0,y=Array.isArray(c.injections)?[...c.injections]:[],g=y.findIndex(b=>b&&b.position==="inject_props"&&b.target_id===n);if(g<0)return{kind:"injection_not_found"};y[g]={...y[g],props:o};const m={...c,injections:y};let h;try{h=await r(s,{method:"PUT",headers:je({"Content-Type":"application/json"}),credentials:"same-origin",body:JSON.stringify({content:m,expected_lock_version:f})})}catch(b){return{kind:"error",message:b instanceof Error?b.message:String(b)}}const v=await h.json().catch(()=>null);return h.ok?{kind:"success",newLockVersion:typeof v?.data?.lock_version=="number"?v.data.lock_version:f+1}:h.status===409?{kind:"conflict",currentVersion:v?.current_version??-1,yourVersion:v?.your_version??f}:{kind:"error",message:v?.message??`HTTP ${h.status}`}}const sg=new Map;function Oa(e,t){sg.set(e,t)}function FT(e){return e?sg.get(e)??null:null}const ag="g7le.clipboard";function lg(){try{return typeof window>"u"||!window.sessionStorage?null:window.sessionStorage}catch{return null}}function KT(e){const t=JSON.parse(JSON.stringify(e)),n=o=>{for(const r of Object.keys(o))r.startsWith("__")&&delete o[r];Array.isArray(o.children)&&o.children.forEach(n)};return n(t),t}function dg(e,t=0){const n=lg();if(!n)return!1;try{const o={node:KT(e),copiedAt:t};return n.setItem(ag,JSON.stringify(o)),!0}catch{return!1}}function VT(){const e=lg();if(!e)return null;try{const t=e.getItem(ag);if(!t)return null;const n=JSON.parse(t);return!n||typeof n.node!="object"||n.node===null?null:JSON.parse(JSON.stringify(n.node))}catch{return null}}const Xo={nw:{width:-1,height:-1},n:{width:0,height:-1},ne:{width:1,height:-1},e:{width:1,height:0},se:{width:1,height:1},s:{width:0,height:1},sw:{width:-1,height:1},w:{width:-1,height:0}};function Ba(e){if(typeof e=="number")return e;if(typeof e=="string"){const t=e.match(/^(-?\d+(?:\.\d+)?)px$/);if(t)return Number(t[1])}return null}function UT(e,t){const n=Array.isArray(e.options)?e.options:[],o=Array.isArray(e.scale)?e.scale:[],r=n.length>0?n.map(l=>l.value):o;if(r.length===0)return;let s=r[0],a=1/0;return r.forEach((l,d)=>{const c=Ba(l)??d*40,p=Math.abs(c-t);p<a&&(a=p,s=l)}),s}function qT(e,t){const n=typeof t.min=="number"?t.min:void 0,o=typeof t.max=="number"?t.max:void 0;let r=e;return n!==void 0&&(r=Math.max(n,r)),o!==void 0&&(r=Math.min(o,r)),r}function cg(e,t){const n=e.apply,o=qT(Math.max(0,Math.round(t)),e);return n?.type==="styleProp"?`${o}px`:n?.type==="classToken"&&n.tokenTemplate?`${o}px`:UT(e,o)}function GT(e){const{node:t,widthControl:n,heightControl:o,scale:r,measureStartSize:s,onResize:a,onResizeEnd:l,onResizeStateChange:d}=e,c={width:!!n,height:!!o},p=u.useRef(null),f=u.useCallback((y,g)=>{if(!t)return;const m=Xo[y];if(m.width!==0&&!n||m.height!==0&&!o)return;const h=t.props?.style??{},v=s?.()??null,b=Ba(h.width)??(v?Math.round(v.width):0),x=Ba(h.height)??(v?Math.round(v.height):0);p.current={handle:y,startX:g.clientX,startY:g.clientY,startW:b,startH:x,node:t};const k=m.width!==0&&m.height!==0?"both":m.width!==0?"width":"height";d?.(k);let w=null;const _=E=>{const S=p.current;if(!S)return;const T=r||1,$=(E.clientX-S.startX)/T*Xo[S.handle].width,A=(E.clientY-S.startY)/T*Xo[S.handle].height;let z=S.node;if(Xo[S.handle].width!==0&&n){const D=cg(n,S.startW+$);z=fn(z,n,D)}if(Xo[S.handle].height!==0&&o){const D=cg(o,S.startH+A);z=fn(z,o,D)}w=z,a(z,k)},C=()=>{p.current=null,d?.(null),window.removeEventListener("pointermove",_),window.removeEventListener("pointerup",C),w&&l?.(w,k)};window.addEventListener("pointermove",_),window.addEventListener("pointerup",C)},[t,n,o,r,a,l,d]);return{enabledAxes:c,onHandlePointerDown:f}}const ug=320,pg=1920,YT=1024;function Wa(e){return Number.isFinite(e)?Math.round(Math.max(ug,Math.min(pg,e))):YT}function ja(e={}){const{state:t,dispatch:n}=Je(),{previewDevice:o,previewZoom:r,previewCustomWidth:s,previewColorScheme:a}=t,l=o==="custom"?Wa(s):Y6(o),d=u.useMemo(()=>{const g=e.availableWidth??l,m=l*r;return r>=1||m<=g?r:Math.min(r,g/l)},[l,r,e.availableWidth]),c=u.useCallback(g=>{n({type:"SET_PREVIEW_DEVICE",device:g})},[n]),p=u.useCallback(g=>{const m=Math.max(.5,Math.min(1,g));n({type:"SET_PREVIEW_ZOOM",zoom:m})},[n]),f=u.useCallback(g=>{n({type:"SET_PREVIEW_CUSTOM_WIDTH",width:Wa(g)})},[n]),y=u.useCallback(g=>{n({type:"SET_PREVIEW_COLOR_SCHEME",scheme:g})},[n]);return{device:o,deviceWidth:l,zoom:r,scale:d,customWidth:Wa(s),setDevice:c,setZoom:p,setCustomWidth:f,colorScheme:a,setColorScheme:y}}const yi="g7le-dnd-handle-";function XT(e){if(typeof e!="string"||!e.startsWith(yi))return null;const t=e.slice(yi.length);return t.length>0?t:null}function JT(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/["\\\]]/g,"\\$&")}function ZT(e,t,n,o){const r=o?.handleFallback??!1;if(e instanceof HTMLElement){const s=e.closest("[data-editor-path]");if(s)return s;const a=e.closest('[data-testid^="g7le-"]');if(a&&!(a.getAttribute("data-testid")??"").startsWith(yi))return null;if(r){const l=e.closest(`[data-testid^="${yi}"]`);if(l&&t){const d=XT(l.getAttribute("data-testid"));if(d){const c=t.querySelector(`[data-editor-path="${JT(d)}"]`);if(c)return c}}}}if(r&&n&&typeof document<"u"&&typeof document.elementsFromPoint=="function")for(const s of document.elementsFromPoint(n.x,n.y)){const a=s.closest?.("[data-editor-path]");if(a)return a}return null}function QT(e,t,n){if(typeof e=="string"&&e.trim().length>0)return{color:e};const o=typeof n?.color=="string"&&n.color.length>0,r=typeof t=="string"&&t.trim().length>0;return o||r?{}:{color:"#0f172a"}}const RT="#ffffff",e4="#0f172a";function t4(e){const t=e.trim(),n=t.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);if(n)return[Number(n[1]),Number(n[2]),Number(n[3])];const o=t.replace(/^#/,"");return/^[0-9a-f]{3}$/i.test(o)?[parseInt(o[0]+o[0],16),parseInt(o[1]+o[1],16),parseInt(o[2]+o[2],16)]:/^[0-9a-f]{6}$/i.test(o)?[parseInt(o.slice(0,2),16),parseInt(o.slice(2,4),16),parseInt(o.slice(4,6),16)]:null}function n4(e){if(!e||/transparent/i.test(e))return!1;const t=e.trim().match(/^oklch\(\s*([\d.]+)(%?)/i);if(t)return(t[2]==="%"?Number(t[1])/100:Number(t[1]))>=.62;const n=t4(e);if(!n)return!1;const[o,r,s]=n;return .299*o+.587*r+.114*s>=180}function o4(e,t,n){const o=typeof n?.color=="string"?n.color:null;return n4(e??o??t)?e4:RT}function r4({box:e,initialValue:t,isCustomKey:n,t:o,onCommit:r,onCancel:s,mirrorClassName:a,mirrorStyle:l,nodeEffectiveColor:d,onOpenTranslations:c}){const p=u.useRef(null),f=u.useRef(!1),[y,g]=u.useState(t.trim().length>0),[m,h]=u.useState(null);u.useEffect(()=>{const w=p.current;if(w){w.textContent=t,w.focus();try{const _=window.document.createRange();_.selectNodeContents(w);const C=window.getSelection();C?.removeAllRanges(),C?.addRange(_)}catch{}f.current=!1}},[t]),u.useEffect(()=>{const w=p.current;if(!w||typeof window.getComputedStyle!="function"){h(null);return}try{h(window.getComputedStyle(w).color||null)}catch{h(null)}},[a,l]);const v=()=>(p.current?.textContent??"").trim(),b=()=>{f.current||(f.current=!0,r(v()))},x=()=>{f.current||(f.current=!0,s())},k=w=>{w.key==="Enter"&&!w.shiftKey?(w.preventDefault(),b()):w.key==="Escape"&&(w.preventDefault(),x())};return i.jsxs("div",{className:"g7le-inline-text-editor","data-testid":"g7le-inline-text-editor","data-custom-key":n?"true":"false",style:{position:"absolute",left:e.left,top:e.top,minWidth:Math.max(e.width,60),minHeight:e.height,pointerEvents:"auto",zIndex:1e4},children:[i.jsx("div",{ref:p,role:"textbox","aria-label":o("layout_editor.inline_edit.aria_label"),"aria-multiline":"false","data-testid":"g7le-inline-text-editable",className:a,contentEditable:!0,suppressContentEditableWarning:!0,spellCheck:!1,onInput:()=>g(v().length>0),onKeyDown:k,onBlur:b,style:{outline:"2px solid #2563eb",outlineOffset:1,borderRadius:3,background:o4(d,m,l),padding:"2px 4px",minHeight:e.height,whiteSpace:"pre-wrap",wordBreak:"break-word",boxShadow:"0 2px 8px rgba(37, 99, 235, 0.25)",...l??{},...QT(d,a,l)}}),i.jsxs("div",{className:"g7le-inline-text-editor__hint","data-testid":"g7le-inline-text-editor-hint",style:{marginTop:2,display:"inline-flex",alignItems:"center",gap:4,whiteSpace:"nowrap"},children:[c?i.jsxs("button",{type:"button","data-testid":"g7le-inline-text-editor-open-translations",onMouseDown:w=>w.preventDefault(),onClick:w=>{w.preventDefault(),w.stopPropagation(),c()},title:o("layout_editor.inline_edit.open_translations"),style:{padding:"2px 8px",fontSize:11,background:n?"#eff6ff":"#f0fdf4",color:n?"#1d4ed8":"#15803d",border:`1px solid ${n?"#bfdbfe":"#bbf7d0"}`,borderRadius:4,display:"inline-flex",alignItems:"center",gap:4,cursor:"pointer",whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",children:"🌐"}),i.jsx("span",{children:o(n?"layout_editor.inline_edit.existing_key_hint":"layout_editor.inline_edit.new_key_hint")}),i.jsxs("span",{"aria-hidden":"true",style:{opacity:.7},children:["· ",o("layout_editor.inline_edit.open_translations")," ›"]})]}):i.jsx("span",{style:{padding:"2px 6px",fontSize:11,background:n?"#eff6ff":"#f0fdf4",color:n?"#1d4ed8":"#15803d",border:`1px solid ${n?"#bfdbfe":"#bbf7d0"}`,borderRadius:4,display:"inline-flex",alignItems:"center",gap:4,whiteSpace:"nowrap"},children:n?i.jsxs(i.Fragment,{children:["⌐ ",o("layout_editor.inline_edit.existing_key_hint")]}):i.jsxs(i.Fragment,{children:["✎ ",o("layout_editor.inline_edit.new_key_hint")]})}),!y&&i.jsx("span",{"data-testid":"g7le-inline-text-editor-empty-warn",style:{color:"#b91c1c",marginLeft:4},children:o("layout_editor.inline_edit.empty_warning")})]})]})}function i4({box:e,node:t,customKey:n,initialChipValue:o,chipParamLabels:r,onKeyify:s,templateIdentifier:a,locale:l,t:d,candidates:c,onInsertBinding:p,onRemoveBinding:f,onCommit:y,onCancel:g}){const[m,h]=u.useState(null),v=u.useRef(""),[b,x]=u.useState(!1),k=u.useRef(!1),w=u.useRef(null),_=Ft(),C=u.useRef(()=>{}),[E,S]=u.useState(null),[T,$]=u.useState(!1),A=u.useRef(n??null);u.useEffect(()=>{n&&A.current!==n&&(A.current=n)},[n]);const z=u.useMemo(()=>{if(!n&&r)return r;const L={},I=mt(typeof t.text=="string"?t.text:"");for(const P of I?.params??[])L[P.name]=Vt(P.expression);return L},[t.text,n,r]);u.useEffect(()=>{let L=!1;if(!n){const I=o??"";return v.current=I,h(I),()=>{L=!0}}return(async()=>{const I=It(n,l);if(I!==void 0){v.current=I,h(I);return}const P=await cn(a,n);if(L)return;const F=P?.values?.[l]??"";v.current=F,h(F)})(),()=>{L=!0}},[a,n,l,o]);const D=async()=>{if(k.current)return;k.current=!0;const L=m??"";if(L===v.current){y();return}x(!0);let I=A.current;if(!I){if(!s){x(!1),k.current=!1;return}if(I=await s(L),x(!1),!I){k.current=!1;return}A.current=I,y();return}const P=await ko(a,I,l,L);if(x(!1),P.kind==="error"){k.current=!1;return}_?.markDirty?.(),y()},O=()=>{k.current||(k.current=!0,g())},M=L=>{S(L)},B=async L=>{if(!(E===null||!p||T)){$(!0);try{const I=A.current;I&&m!==null&&m!==v.current&&(await ko(a,I,l,m),v.current=m);const P=await p(E,L);P!==null&&(h(P),v.current=P,_?.markDirty?.())}finally{$(!1),S(null)}}},N=async L=>{if(!(!f||T)){$(!0);try{const I=A.current;I&&m!==null&&m!==v.current&&(await ko(a,I,l,m),v.current=m);const P=await f(L);P!==null&&(h(P),v.current=P,_?.markDirty?.())}finally{$(!1)}}},H=L=>{L.key==="Enter"&&!L.shiftKey?(L.preventDefault(),D()):L.key==="Escape"&&(L.preventDefault(),O())};return C.current=()=>{D()},u.useEffect(()=>{const L=P=>{const F=w.current;if(!F)return;const G=P.target;if(G&&F.contains(G))return;const V=G;V&&V.closest&&V.closest('[data-testid="g7le-inline-toolbar"], .g7le-inline-toolbar')||C.current()},I=w.current?.ownerDocument??document;return I.addEventListener("pointerdown",L,!0),()=>I.removeEventListener("pointerdown",L,!0)},[]),i.jsxs("div",{ref:w,className:"g7le-inline-param-chip-editor","data-testid":"g7le-inline-param-chip-editor",onKeyDown:H,style:{position:"absolute",left:e.left,top:e.top,minWidth:Math.max(e.width,120),pointerEvents:"auto",zIndex:1e4,background:"#fff",border:"2px solid #2563eb",borderRadius:4,boxShadow:"0 2px 8px rgba(37,99,235,0.25)",padding:4},children:[m===null?i.jsx("div",{"data-testid":"g7le-inline-param-chip-loading",style:{fontSize:12,color:"#94a3b8",padding:4},children:d("layout_editor.translation.loading")}):i.jsx(mr,{value:m,onChange:h,t:d,paramLabels:z,testIdSuffix:"inline",onRequestInsert:p?M:void 0,onRemoveChip:f?L=>void N(L):void 0}),E!==null&&p&&i.jsx("div",{"data-testid":"g7le-inline-param-chip-insert-picker",style:{marginTop:4},children:i.jsx($t,{candidates:c??[],t:d,onSelect:L=>void B(L),testIdSuffix:"inline-insert",defaultOpen:!0,floating:!1})}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginTop:4},children:[i.jsx("button",{type:"button","data-testid":"g7le-inline-param-chip-save",disabled:b,onMouseDown:L=>L.preventDefault(),onClick:()=>void D(),style:{fontSize:11,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",padding:"4px 12px",cursor:"pointer"},children:d(b?"layout_editor.translation.saving":"layout_editor.translation.save")}),i.jsx("span",{style:{fontSize:10,color:"#94a3b8"},children:d("layout_editor.translation.placeholder_locked")})]})]})}const s4=["fontWeight","fontBold","fontStyle","fontItalic","textDecoration","textUnderline","textAlign","fontSize","textColor"],a4={fontWeight:"B",fontBold:"B",fontStyle:"I",fontItalic:"I",textDecoration:"U",textUnderline:"U",textAlign:"≡",fontSize:"T",textColor:"A"},l4=[["fontWeight","fontBold"],["fontItalic","fontStyle"],["textUnderline","textDecoration"]];function fg({node:e,spec:t,t:n,onApplyControl:o,box:r,scope:s=Wt}){const a=typeof e.name=="string"?e.name:"",l=Oi(t,a),d=Array.isArray(l?.styleControls)?l.styleControls:[];let c=s4.filter(p=>d.includes(p)&&Ct(t,p)!==null);for(const p of l4){const f=p.filter(y=>c.includes(y));if(f.length>1){const y=f[0];c=c.filter(g=>!p.includes(g)||g===y)}}return c.length===0?null:i.jsx("div",{className:"g7le-inline-toolbar","data-testid":"g7le-inline-toolbar",role:"toolbar","aria-label":n("layout_editor.inline_edit.toolbar_label"),style:{position:"absolute",left:r.left,top:r.top-40,display:"inline-flex",gap:2,padding:4,background:"#0f172a",borderRadius:8,boxShadow:"0 4px 12px rgba(15, 23, 42, 0.35)",pointerEvents:"auto",zIndex:10001},children:c.map(p=>i.jsx(c4,{controlKey:p,node:e,spec:t,t:n,scope:s,onApplyControl:o},p))})}const d4={fontWeight:"bold",fontBold:"bold",fontStyle:"italic",fontItalic:"italic",textDecoration:"underline",textUnderline:"underline"};function gg(e,t){const n=r=>(r.split(/[-\s]/).pop()??r)===t||r===t;return e.find(r=>{const s=typeof r.value=="string"?r.value:"",a=Array.isArray(r.apply?.tokens)?r.apply.tokens:[];return n(s)||a.some(n)})?.value}function c4({controlKey:e,node:t,spec:n,t:o,scope:r,onApplyControl:s}){const[a,l]=u.useState(!1),d=Ct(n,e);if(!d)return null;const c=d.options,p=Array.isArray(c),f=Do(t,d,r),y=d4[e],g=y&&Array.isArray(c)?gg(c,y):void 0,m=!y,h=g!==void 0?f.value===g:p&&f.matched&&f.value!==void 0&&f.value!=="",v=o(`layout_editor.inline_edit.format.${e}`),b=_=>typeof _.label=="string"?_.label.startsWith("$t:")?o(_.label.slice(3)):_.label:String(_.value??""),x=()=>{s(e,_=>{const C=Do(_,d,r),E=Array.isArray(c)?gg(c,y):void 0,S=E!==void 0&&C.value===E?void 0:E;return fn(_,d,S,r)})},k=_=>{s(e,C=>{const S=Do(C,d,r).value===_?void 0:_;return fn(C,d,S,r)}),l(!1)},w=_=>{if(_.preventDefault(),_.stopPropagation(),!m&&Array.isArray(c)){x();return}l(C=>!C)};return i.jsxs("div",{style:{position:"relative",display:"inline-flex"},children:[i.jsxs("button",{type:"button","data-testid":`g7le-inline-format-${e}`,"data-active":h?"true":"false","aria-label":v,"aria-pressed":p?h:void 0,title:v,onMouseDown:_=>_.preventDefault(),onClick:w,style:{display:"inline-flex",alignItems:"center",gap:5,height:28,padding:"0 9px",fontSize:12,border:"none",borderRadius:5,cursor:"pointer",background:h?"#2563eb":"transparent",color:"#ffffff",whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",style:{fontWeight:e==="fontWeight"||e==="fontBold"?700:600,fontStyle:e==="fontStyle"||e==="fontItalic"?"italic":"normal",textDecoration:e==="textDecoration"||e==="textUnderline"?"underline":"none"},children:a4[e]}),i.jsx("span",{children:v}),m&&i.jsx("span",{"aria-hidden":"true",style:{fontSize:9,opacity:.8},children:"▾"})]}),m&&a&&i.jsx("div",{"data-testid":`g7le-inline-popover-${e}`,role:"listbox",onMouseDown:_=>_.preventDefault(),style:{position:"absolute",top:32,left:0,minWidth:140,maxHeight:240,overflowY:"auto",background:"#0f172a",border:"1px solid #334155",borderRadius:8,boxShadow:"0 8px 24px rgba(15,23,42,0.45)",padding:4,zIndex:10002},children:Array.isArray(c)&&c.length>0?c.map((_,C)=>{const E=f.value===_.value;return i.jsxs("button",{type:"button",role:"option","aria-selected":E,"data-testid":`g7le-inline-option-${e}-${String(_.value??C)}`,onMouseDown:S=>S.preventDefault(),onClick:S=>{S.preventDefault(),S.stopPropagation(),k(_.value)},style:{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"6px 8px",fontSize:12,textAlign:"left",border:"none",borderRadius:5,cursor:"pointer",background:E?"#2563eb":"transparent",color:"#ffffff"},children:[typeof _.swatch=="string"&&i.jsx("span",{"aria-hidden":"true",style:{width:14,height:14,borderRadius:3,background:_.swatch,border:"1px solid rgba(255,255,255,0.3)"}}),i.jsx("span",{children:b(_)}),E&&i.jsx("span",{"aria-hidden":"true",style:{marginLeft:"auto"},children:"✓"})]},String(_.value??C))}):i.jsx(u4,{testid:`g7le-inline-freeinput-${e}`,placeholder:o(`layout_editor.inline_edit.prompt.${e}`),initial:typeof f.value=="string"?f.value:"",onSubmit:_=>{s(e,C=>fn(C,d,_,r)),l(!1)}})})]})}function u4({testid:e,placeholder:t,initial:n,onSubmit:o}){const[r,s]=u.useState(n);return i.jsx("input",{type:"text","data-testid":e,value:r,placeholder:t,autoFocus:!0,onMouseDown:a=>a.stopPropagation(),onChange:a=>s(a.currentTarget.value),onKeyDown:a=>{a.key==="Enter"&&(a.preventDefault(),o(r.trim()))},style:{width:130,padding:"6px 8px",fontSize:12,border:"1px solid #334155",borderRadius:5,background:"#1e293b",color:"#ffffff"}})}function p4(e){const{frameEl:t,manifest:n,nesting:o,componentPalette:r,spec:s=null,permissionCandidates:a,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c,onEditProps:p}=e,{state:f,dispatch:y}=Je(),{t:g}=tt(),m=ja(),h=u.useCallback((W,X)=>{const R=window.G7Core;if(R&&typeof R.t=="function"){const de=R.t(W,X);if(de&&de!==W&&!de.startsWith("$t:"))return de}return g(W,X)},[g]),v=Ft(),b=d7(50),x=yo(),k=u.useRef([]),w=u.useMemo(()=>v?.document?.raw?.components??[],[v?.document?.raw?.components]),_=u.useMemo(()=>w.length===0?null:{children:w},[w]),C=u.useMemo(()=>{if(f.editMode!=="extension")return;const W=/^__extension__\/(\d+)/.exec(f.selectedRoute?.path??"");return W?Number(W[1]):void 0},[f.editMode,f.selectedRoute?.path]),E=u.useMemo(()=>{if(f.editMode==="iteration_item"){const W=v?.document?.iterationContext?.sourceIndexPath;return Array.isArray(W)&&W.length>0?W:null}if(f.editMode==="modal"){const W=v?.document?.modalContext?.editIndexPath;return Array.isArray(W)&&W.length>0?W:null}return null},[f.editMode,v?.document?.iterationContext?.sourceIndexPath,v?.document?.modalContext?.editIndexPath]),S=e7({rootNode:_,editMode:f.editMode,currentExtensionId:C,editableRootPath:E,pathParser:Ze,resolveRouteMatch:l}),[T,$]=u.useState(null),[A,z]=u.useState(null),[D,O]=u.useState(null),M=u.useRef(null);M.current=D;const[B,N]=u.useState(null),H=u.useRef(null),L=u.useRef(null);L.current=S.selectedPath;const I=Wb(),[P,F]=u.useState(null),G=u.useCallback(W=>{if(!t)return;const X=Ze(W);if(!X)return;const R={children:Me.current.components},de=We(R,X),ye=I.classify(de);if(!ye.editable){Et({op:"inline_edit_blocked_binding",sourceState:ye.sourceState,componentPath:at(X),timestamp:Date.now()});return}const we=po(t,W);if(!we)return;const Te=de?.props??{},ke=yg(t,W);F({path:X,domPath:W,box:we,initialValue:ye.displayValue,isCustomKey:ye.sourceState==="custom_key",isParamKey:ye.isParamKey===!0,customKey:ye.customKey,chipValue:ye.chipValue,chipParamLabels:ye.chipParamLabels,mirrorClassName:typeof Te.className=="string"?Te.className:void 0,mirrorStyle:Te.style??void 0,nodeEffectiveColor:ke}),Et({op:"inline_edit_enter",sourceState:ye.sourceState,componentPath:at(X),timestamp:Date.now()})},[t,I]),V=u.useCallback(()=>{if(!t){$(null),z(null);return}z(po(t,S.selectedPath)),$(po(t,S.hoverPath))},[t,S.selectedPath,S.hoverPath,w]);u.useEffect(()=>{if(V(),typeof window<"u"&&typeof window.requestAnimationFrame=="function"){const W=window.requestAnimationFrame(()=>V());return()=>window.cancelAnimationFrame(W)}},[V]),u.useEffect(()=>{if(t)return $p(V,t)},[t,V]);const Y=u.useCallback((W,X)=>{if(typeof window>"u")return;window.dispatchEvent(new CustomEvent(La,{detail:X===null?{requestId:W,cancelled:!0}:{requestId:W,id:X}})),O(null),N(null);const R=H.current;R?S.handleSelect("",{dataset:{editorPath:R}}):S.clearSelection(),H.current=null},[S]);u.useEffect(()=>{if(typeof window>"u")return;const W=X=>{const R=X.detail;if(typeof R?.requestId!="string")return;const de=M.current;de&&de!==R.requestId&&Y(de,null),H.current=L.current,O(R.requestId)};return window.addEventListener(Pa,W),()=>window.removeEventListener(Pa,W)},[Y]),u.useEffect(()=>{if(!D)return;const W=X=>{X.key==="Escape"&&(X.preventDefault(),Y(D,null))};return window.addEventListener("keydown",W,!0),()=>window.removeEventListener("keydown",W,!0)},[D,Y]),u.useEffect(()=>{if(!t)return;const W=f.editMode==="iteration_item"||f.editMode==="modal",X=(xe,Ee)=>ZT(xe,t,Ee?{x:Ee.clientX,y:Ee.clientY}:void 0,{handleFallback:W}),R=xe=>{const Ee=xe.dataset.editorPath??"",Fe=Ee?Ze(Ee):null,zt={children:Me.current.components},gt=Fe?We(zt,Fe):null,Mt=gt&&typeof gt.props?.id=="string"?gt.props.id:typeof gt?.id=="string"?gt.id:"";return Mt.startsWith("auto_")?"":Mt},de=xe=>{const Ee=X(xe.target,xe);if(!Ee)return;const Fe=M.current;if(Fe){xe.preventDefault(),xe.stopPropagation();const zt=R(Ee);zt&&Y(Fe,zt);return}xe.preventDefault(),xe.stopPropagation(),S.handleSelect(Ee.dataset.editorId??"",{currentTarget:Ee,dataset:Ee.dataset})},ye=xe=>{const Ee=X(xe.target,xe);if(M.current){if(!Ee){N(null);return}const Fe=po(t,Ee.dataset.editorPath??null);if(!Fe){N(null);return}N({box:Fe,hasId:R(Ee)!==""});return}if(!Ee){S.handleHover(null,{});return}S.handleHover(Ee.dataset.editorId??"",{currentTarget:Ee,dataset:Ee.dataset})},we=()=>{if(M.current){N(null);return}S.handleHover(null,{})},Te=xe=>{const Ee=X(xe.target,xe);if(!Ee)return;const Fe=Ee.dataset.editorPath??"";Fe&&(xe.preventDefault(),xe.stopPropagation(),G(Fe))},ke=t.parentElement??t;return ke.addEventListener("click",de,!0),ke.addEventListener("mousemove",ye,!0),ke.addEventListener("mouseleave",we,!0),ke.addEventListener("dblclick",Te,!0),()=>{ke.removeEventListener("click",de,!0),ke.removeEventListener("mousemove",ye,!0),ke.removeEventListener("mouseleave",we,!0),ke.removeEventListener("dblclick",Te,!0)}},[t,S,G,f.editMode,Y]);const Z=u.useMemo(()=>{const W=S.selectedPath;if(!W)return null;const X=W.match(/^(.*)\.children\.\d+$/);return X?X[1]:null},[S.selectedPath]),se=u.useCallback(()=>{Z&&S.handleSelect("",{dataset:{editorPath:Z}})},[Z,S]),re=u.useMemo(()=>!t||!S.selectedPath?null:t.querySelector(`[data-editor-path="${an(S.selectedPath)}"]`),[t,S.selectedPath]),U=u.useMemo(()=>S.selectedPath?Ze(S.selectedPath):null,[S.selectedPath]),ee=u.useMemo(()=>{if(!U)return null;const W=U.find(et);if(!W)return null;const{i18nKey:X,raw:R}=bC(W.responsive);return X?g(X):g("layout_editor.overlay.branch_custom",{range:R})},[U,g]),fe=u.useMemo(()=>S.selectedPath?S.selectedIsIteration&&S.selectedIterationSourcePath?S.selectedIterationSourcePath:S.selectedPath:null,[S.selectedPath,S.selectedIsIteration,S.selectedIterationSourcePath]),K=u.useMemo(()=>fe?Ze(fe):null,[fe]),ne=u.useMemo(()=>!t||!fe?null:t.querySelector(`[data-editor-path="${an(fe)}"]`),[t,fe]),me=u.useMemo(()=>{if(ne?.parentElement)return ne.parentElement;if(!t||!fe)return null;const W=fe.match(/^(.*)\.children\.\d+$/);return W?t.querySelector(`[data-editor-path="${an(W[1])}"]`):null},[ne,t,fe]),Ce=u.useMemo(()=>{const W=S.selectedNode;return W&&(typeof W.name=="string"?W.name:typeof W.type=="string"?W.type:"")||null},[S.selectedNode]),ze=u.useMemo(()=>{if(S.selectedLockKind!=="extension")return null;const W=S.selectedNode?.__source;if(!W||W.kind!=="extension")return null;const X=W.extensionName?.trim(),R=W.extensionIdentifier?.trim();return X&&R?`${X} (${R})`:X||R||W.extensionSourceType||null},[S.selectedLockKind,S.selectedNode]),Se=s7({parentEl:me,selectedPath:K,selectedEl:ne}),qe=u.useMemo(()=>!t||!fe||fe===S.selectedPath?A:po(t,fe),[t,fe,A,S.selectedPath,w]),De=u.useRef(null),Pe=u.useCallback(()=>{const W=De.current;W&&(x.close(W),De.current=null)},[x]),Qe=u.useRef(()=>{}),Me=u.useRef({manifest:n,nesting:o,componentPalette:r,components:w});Me.current={manifest:n,nesting:o,componentPalette:r,components:w};const Re=u.useCallback(W=>{const X=W.insertion?.parentPath??[],R=W.insertion?.index??0,de=Me.current,ye=hg(de.components,X);Pe();const we=x.open({ariaLabel:h("layout_editor.palette.title"),width:880,maxHeightRatio:.82,content:u.createElement(Ef,{manifest:de.manifest,nesting:de.nesting,componentPalette:de.componentPalette??null,targetParentPath:X,targetContainerName:ye,onInsert:(Te,ke,xe)=>{Qe.current(Te,ke,xe)},insertionIndex:R,onClose:()=>Pe(),editorTemplateId:f.templateIdentifier,editorLocale:f.locale}),onClose:()=>{De.current=null}});De.current=we},[x,Pe]),ve=u.useMemo(()=>{if(!f.isPaletteOpen)return null;if(!U)return{parentPath:[],index:w.length};if(S.selectedLockKind!=="none"){const de=K;return!de||de.length===0?{parentPath:[],index:w.length}:{parentPath:de.slice(0,-1),index:(de[de.length-1]??0)+1}}const X=S.selectedNode,R=Array.isArray(X?.children)?X.children.length:0;return lI(X?.name,R,U,o,w.length)},[f.isPaletteOpen,U,K,S.selectedLockKind,S.selectedNode,w.length,o]),te=u.useCallback(()=>{y({type:"SET_PALETTE_OPEN",open:!1})},[y]),he=u.useRef(null);u.useEffect(()=>{if(!f.isPaletteOpen){const ye=he.current;ye&&(x.close(ye),he.current=null);return}if(!ve||he.current)return;const W=ve,X=Me.current,R=hg(X.components,W.parentPath),de=x.open({ariaLabel:h("layout_editor.palette.title"),width:880,maxHeightRatio:.82,content:u.createElement(Ef,{manifest:X.manifest,nesting:X.nesting,componentPalette:X.componentPalette??null,targetParentPath:W.parentPath,targetContainerName:R,onInsert:(ye,we,Te)=>{Qe.current(ye,we,Te),te()},insertionIndex:W.index,onClose:()=>te(),editorTemplateId:f.templateIdentifier,editorLocale:f.locale}),onClose:()=>{he.current=null,y({type:"SET_PALETTE_OPEN",open:!1})}});he.current=de},[f.isPaletteOpen,ve,x,te,y]);const Q=u.useRef({layoutName:null,pushed:!1});u.useEffect(()=>{const W=v?.document?.layoutName??null;if(!W){Q.current={layoutName:null,pushed:!1};return}Q.current.layoutName!==W&&(b.clear(),b.push({actionKind:"inline_text_edit",label:"baseline",snapshot:w}),Q.current={layoutName:W,pushed:!0})},[v?.document?.layoutName,w,b]);const ie=u.useRef(0);u.useEffect(()=>{const W=v?.saveSuccessCounter??0;W!==ie.current&&(ie.current=W,W!==0&&(b.clear(),b.push({actionKind:"inline_text_edit",label:"save_baseline",snapshot:w}),k.current=[],c?.([])))},[v?.saveSuccessCounter,w,b,c]);const _e=u.useRef(0);u.useEffect(()=>{const W=v?.reloadCounter??0;W!==_e.current&&(_e.current=W,W!==0&&(Q.current={layoutName:null,pushed:!1},k.current=[],c?.([])))},[v?.reloadCounter,c]);const ge=u.useCallback((W,X,R)=>{if(!v)return;let de=[];v.patchLayout(ke=>{const xe={children:ke},Ee=Ha(xe,X),Fe=R<0?Array.isArray(Ee?.children)?Ee.children.length:0:R,gt=Qi(xe,X,Fe,W).children??[];return de=gt,gt}),b.push({actionKind:"insert",label:`insert ${W.name??W.type??""}`,snapshot:de});const ye=Ha({children:de},X),we=R<0?Array.isArray(ye?.children)?ye.children.length-1:0:R,Te=at([...X,we]);if(k.current=[...k.current,Te],c?.(k.current),t){const ke=`[data-editor-path="${an(Te)}"]`,xe=()=>{const Ee=t.querySelector(ke);Ee&&typeof Ee.scrollIntoView=="function"&&Ee.scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"})};typeof window<"u"&&typeof window.requestAnimationFrame=="function"?window.requestAnimationFrame(()=>window.requestAnimationFrame(xe)):setTimeout(xe,32)}},[v,b,c,t]);Qe.current=ge;const Ae=u.useCallback(()=>{if(!v||!S.selectedNode||!U)return;const W=es(S.selectedNode);let X=[];v.patchLayout(R=>{const de={children:R},ye=U.slice(0,-1),we=U[U.length-1]??0,ke=Qi(de,ye,we+1,W).children??[];return X=ke,ke}),b.push({actionKind:"insert",label:"duplicate",snapshot:X})},[v,S.selectedNode,U,b]),Ne=u.useCallback(()=>{if(!v||!U)return;let W=[];v.patchLayout(X=>{const ye=Ri({children:X},U).children??[];return W=ye,ye}),b.push({actionKind:"remove",label:"remove",snapshot:W}),S.clearSelection()},[v,U,b,S]),Le=u.useMemo(()=>{if(!S.selectedNode||!U||U.some(et))return null;const W=S.selectedNode;if(!Array.isArray(W.children))return null;const X=yC(f.previewDevice,W,f.previewCustomWidth);return{mode:X.mode,key:X.key,sourceKey:X.sourceKey}},[S.selectedNode,U,f.previewDevice,f.previewCustomWidth]),q=u.useCallback(()=>{if(!v||!U||Le?.mode!=="separate")return;const W=Le.key,X=Le.sourceKey;let R=[];v.patchLayout(de=>(R=Ab({children:de},U,W,X).children??[],R)),b.push({actionKind:"insert",label:"separate_branch",snapshot:R})},[v,U,Le,b]),ae=u.useCallback(()=>{if(!v||!U||Le?.mode!=="merge")return;const W=Le.key;let X=[];v.patchLayout(R=>(X=Tb({children:R},U,W).children??[],X)),b.push({actionKind:"remove",label:"merge_branch",snapshot:X})},[v,U,Le,b]),pe=u.useCallback(W=>{switch(W){case"mobile":return g("layout_editor.context_menu.device_mobile");case"portable":return g("layout_editor.context_menu.device_portable");case"tablet":return g("layout_editor.context_menu.device_tablet");case"desktop":return g("layout_editor.context_menu.device_desktop");default:return g("layout_editor.context_menu.device_custom",{range:W})}},[g]),$e=u.useMemo(()=>{if(!Le)return null;const W=pe(Le.key);return{separate:g("layout_editor.context_menu.separate_branch_device",{device:W}),merge:g("layout_editor.context_menu.merge_branch_device",{device:W})}},[Le,g,pe]),be=u.useMemo(()=>{if(!S.selectedNode||!U)return[];if(U.some(et))return[];const W=Ur(f.previewDevice,S.selectedNode,f.previewCustomWidth);return G6(S.selectedNode,W).map(X=>({key:X,label:pe(X)}))},[S.selectedNode,U,f.previewDevice,f.previewCustomWidth,pe]),j=u.useCallback(W=>{if(W==="desktop"||W==="tablet"||W==="mobile"||W==="portable"){m.setDevice(W);return}m.setDevice(W)},[m]),oe=u.useCallback(()=>{S.selectedNode&&dg(S.selectedNode)},[S.selectedNode]),ue=u.useCallback(()=>{S.selectedNode&&(dg(S.selectedNode),Ne())},[S.selectedNode,Ne]),J=u.useCallback(()=>{if(!v)return;const W=VT();if(!W)return;const X=es(W);if(U&&U.length>0){const R=U.slice(0,-1),de=(U[U.length-1]??0)+1;ge(X,R,de)}else ge(X,[],w.length)},[v,U,ge,w.length]),le=u.useCallback((W,X)=>{b.push({actionKind:"move",label:X,snapshot:W})},[b]),Ie=u.useCallback(()=>{if(S.selectedNavAffordance==="route_in_tree"&&S.selectedNavTargetPath){if(d){d(S.selectedNavTargetPath);return}y({type:"SELECT_ROUTE",route:{path:S.selectedNavTargetPath,layoutName:null}})}},[S.selectedNavAffordance,S.selectedNavTargetPath,d,y]),dt=u.useCallback(()=>{const W=Eb(S.selectedBaseLayout,f.selectedRoute?.layoutName);W&&y({type:"ENTER_BASE_EDIT",layoutName:W})},[S.selectedBaseLayout,f.selectedRoute?.layoutName,y]),pt=u.useCallback(()=>{const W=S.selectedNode?.__source?.extensionId;W!=null&&y({type:"ENTER_EXTENSION_EDIT",extensionId:String(W)})},[S.selectedNode,y]),ot=u.useCallback(()=>{const W=S.selectedIterationSourcePath;W&&y({type:"ENTER_ITERATION_ITEM_EDIT",sourcePath:W})},[S.selectedIterationSourcePath,y]),Oe=u.useRef(null),He=u.useRef(null),Ye=u.useRef(null),Tt=u.useRef({colorScheme:"base",breakpoint:"base"}),Dt=Oe.current!==null?x.stack.some(W=>W.id===Oe.current):!1,Nt=u.useMemo(()=>zd(f.routeTree,h),[f.routeTree,h]),xn=u.useMemo(()=>Md(v?.document?.raw),[v?.document?.raw]),wn=u.useMemo(()=>Ld(s),[s?.states]),Zt=u.useMemo(()=>{const W=v?.document?.raw,X=Af(W?.components),R=W?.initIsolated,de=R&&typeof R=="object"&&!Array.isArray(R)?Object.keys(R):[];return eA(X,de)},[v?.document?.raw]),Hn=u.useMemo(()=>{const W=v?.document?.raw;return Af(W?.components).map(X=>X.scopeId).filter(X=>typeof X=="string"&&X.length>0)},[v?.document?.raw]),ln=Nd({raw:v?.document?.raw,spec:s}),Ii=u.useCallback((W,X)=>{if(!v)return;const R={children:Me.current.components},de=We(R,W);if(!de)return;Tt.current={colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:Ur(f.previewDevice,de,f.previewCustomWidth)};const ye=t?.getBoundingClientRect(),we=A&&ye?{left:ye.left+A.left,top:ye.top+A.top,width:A.width,height:A.height}:void 0,Te=re,ke=W.find(et),xe=ke?pe(ke.responsive):null,Ee=W.filter(kn=>typeof kn=="number"),Fe=cb({children:Me.current.components},Ee,ub(ln)),zt=Fe.length>0?[...ji({iterationVars:Fe}),...ln]:ln,gt=(kn,lM)=>u.createElement(ET,{key:`pem-${at(He.current??[])}`,node:kn,spec:s,manifest:Me.current.manifest,t:h,permissionCandidates:a,pageCandidates:Nt,dataSourceCandidates:xn,stateKeyCandidates:wn,bindingCandidates:zt,liveElement:Te,templateIdentifier:f.templateIdentifier,initialTab:lM,branchLabel:xe,isolatedScopeIdCandidates:Zt,usedScopeIds:Hn,onSaveInjectedProps:async(bt,Sn)=>{const Ai=typeof kn.id=="string"?kn.id:"",Fn=await HT(f.templateIdentifier,bt,Ai,Sn);if(st({op:"save",layoutName:`extension:${bt}`,editMode:f.editMode,saveTarget:"layout_extension",endpoint:`/api/admin/templates/${f.templateIdentifier}/layout-extensions/${bt}`,isDirty:!0,timestamp:Date.now()}),Fn.kind!=="success")throw new Error(Fn.kind==="conflict"?h("layout_editor.save.conflict"):Fn.kind==="injection_not_found"?"injection not found":Fn.kind==="not_found"?"extension not found":Fn.message??"save failed")},scope:Tt.current,onScopeChange:bt=>{Tt.current=bt;const Sn=Oe.current;Sn&&x.open({id:Sn,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(kn),onClose:()=>{Oe.current=null,He.current=null}})},onPatchNode:bt=>{let Sn=[];v.patchLayout(Fn=>(Sn=Ot({children:Fn},W,()=>bt).children??[],Sn)),b.push({actionKind:"inline_text_edit",label:"edit_props",snapshot:Sn}),sr({source:"style_control",componentPath:at(W),componentName:typeof bt.name=="string"?bt.name:null,patchKey:null,timestamp:Date.now()});const Ai=Oe.current;Ai&&x.open({id:Ai,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(bt),onClose:()=>{Oe.current=null,He.current=null}})},onDelete:()=>{const bt=Oe.current;bt&&x.close(bt),Ne()},onClose:()=>{const bt=Oe.current;bt&&x.close(bt)}});He.current=W;const Mt=x.open({ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(de,X),onClose:()=>{Oe.current=null,He.current=null,Ye.current=null}});Oe.current=Mt,Ye.current=kn=>{x.open({id:Mt,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(kn),onClose:()=>{Oe.current=null,He.current=null,Ye.current=null}})}},[v,s,h,a,Nt,xn,wn,ln,re,b,x,Ne,t,A,f.previewColorScheme,f.previewDevice,f.previewCustomWidth,pe]);u.useEffect(()=>{const W=Ye.current,X=He.current;if(!W||!X)return;const R={children:Me.current.components},de=We(R,X);de?W(de):Oe.current&&x.close(Oe.current)},[w]);const Uz=()=>{if(!Dt)return null;const X={position:"absolute",background:"rgba(15, 23, 42, 0.45)",pointerEvents:"auto",zIndex:f7},R=ke=>{ke.preventDefault(),ke.stopPropagation()};if(!A)return i.jsx("div",{"data-testid":"g7le-edit-lock-full",onClick:R,onMouseMove:R,style:{...X,inset:0}});const{left:de,top:ye,width:we,height:Te}=A;return i.jsxs("div",{"data-testid":"g7le-edit-lock",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[i.jsx("div",{"data-testid":"g7le-edit-lock-top",onClick:R,onMouseMove:R,style:{...X,left:0,top:0,right:0,height:Math.max(0,ye)}}),i.jsx("div",{"data-testid":"g7le-edit-lock-bottom",onClick:R,onMouseMove:R,style:{...X,left:0,top:ye+Te,right:0,bottom:0}}),i.jsx("div",{"data-testid":"g7le-edit-lock-left",onClick:R,onMouseMove:R,style:{...X,left:0,top:ye,width:Math.max(0,de),height:Te}}),i.jsx("div",{"data-testid":"g7le-edit-lock-right",onClick:R,onMouseMove:R,style:{...X,left:de+we,top:ye,right:0,height:Te}})]})},ml=u.useCallback(()=>{if(U&&_p(S.selectedLockKind)){if(p&&S.selectedPath){p(S.selectedPath);return}Ii(U)}},[U,S.selectedPath,S.selectedLockKind,p,Ii]),{scale:vl}=m,_l=typeof S.selectedNode?.name=="string"?S.selectedNode.name:"",Bh=s?.componentCapabilities?.[_l]??null,Wh=Array.isArray(Bh?.styleControls)?Bh.styleControls:[],qz=Wh.includes("width")?Ct(s,"width"):null,Gz=Wh.includes("height")?Ct(s,"height"):null,Yz=u.useCallback((W,X)=>{if(!v||!U)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},U,()=>W).children??[],R)),sr({source:"resize",componentPath:at(U),componentName:_l||null,patchKey:X==="height"?"height":"width",resizeAxis:X,timestamp:Date.now()})},[v,U,_l]),Xz=u.useCallback((W,X)=>{if(!v||!U)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},U,()=>W).children??[],R)),b.push({actionKind:"inline_text_edit",label:`resize ${X}`,snapshot:R})},[v,U,b]),Jz=u.useCallback(()=>{if(!re)return null;const W=re.getBoundingClientRect(),X=vl||1;return{width:W.width/X,height:W.height/X}},[re,vl]),ir=GT({node:S.selectedNode,widthControl:qz,heightControl:Gz,scale:vl,measureStartSize:Jz,onResize:Yz,onResizeEnd:Xz}),Zz=u.useCallback((W,X)=>{ir.onHandlePointerDown(W,X)},[ir]),xl=u.useCallback(()=>{const W=b.undo();!W||!v||v.setLayoutComponents(W.snapshot)},[b,v]),wl=u.useCallback(()=>{const W=b.redo();!W||!v||v.setLayoutComponents(W.snapshot)},[b,v]);u7({onUndo:xl,onRedo:wl});const Qz=u.useMemo(()=>({copy:oe,cut:ue,paste:J,delete:Ne,openProps:ml,selectParent:Z?se:void 0,deselect:S.clearSelection}),[oe,ue,J,Ne,ml,Z,se,S.clearSelection]);np({handlers:Qz,hasSelection:!!S.selectedPath}),u.useEffect(()=>(window.__g7LayoutEditorHasSelection=!!S.selectedPath,()=>{try{delete window.__g7LayoutEditorHasSelection}catch{}}),[S.selectedPath]),u.useEffect(()=>(window.__g7LayoutEditorHistory={undo:xl,redo:wl,canUndo:b.canUndo,canRedo:b.canRedo},()=>{try{delete window.__g7LayoutEditorHistory}catch{}}),[xl,wl,b.canUndo,b.canRedo]);const Rz=u.useCallback(W=>{const X=P;F(null),X&&(async()=>{if((await I.commit(X.path,W)).kind==="created"&&v){const de=v.document?.raw?.components??[];b.push({actionKind:"inline_text_edit",label:"inline_create_key",snapshot:de})}})()},[P,I,v,b]),jh=u.useCallback(()=>{const W=P;F(null),I.trackCancel(W?.path??null)},[P,I]),eM=u.useCallback(()=>{F(null)},[]),tM=u.useCallback(async W=>{const X=P;if(!X)return null;const R=await I.keyifyChipValue(X.path,W);if(R.kind!=="created"||!R.translationKey)return null;const de=v?.document?.raw?.components??[];return b.push({actionKind:"inline_text_edit",label:"inline_keyify_chip",snapshot:de}),R.translationKey},[P,I,v,b]),nM=u.useCallback(()=>{const W=P;W&&(F(null),S.handleSelect("",{dataset:{editorPath:W.domPath}}),Ii(W.path,"translation"))},[P,S,Ii]),xt=u.useMemo(()=>{if(!P)return null;const W=v?.document?.raw?.components??w;return We({children:W},P.path)},[P,v?.document,w]),oM=u.useCallback(async(W,X)=>{const R=P;if(!v||!R)return null;const de=typeof xt?.text=="string"?xt.text:"";let ye;if(R.isParamKey)ye=await wr(f.templateIdentifier,f.locale,de,W,X.sourceId,X.path,"scalar");else{const Fe=zt=>zt.replace(/\$t:[a-zA-Z0-9._-]+/g,gt=>{const Mt=h(gt.slice(3));return Mt&&Mt!==gt.slice(3)&&!Mt.startsWith("$t:")?Mt:""}).replace(/\s+/g," ").trim();ye=await ls(f.templateIdentifier,f.selectedRoute?.layoutName??null,f.locale,de,W,X.sourceId,X.path,"scalar",Fe,h)}if(ye.kind==="error")return null;const we=R.path;let Te=[];v.patchLayout(Fe=>(Te=Ot({children:Fe},we,Mt=>({...Mt,text:ye.text})).children??[],Te)),b.push({actionKind:"inline_text_edit",label:"inline_insert_binding",snapshot:Te});const ke=ye.kind==="ok"&&"key"in ye?ye.key:void 0;!R.isParamKey&&ke&&F(Fe=>Fe&&Fe.path===we?{...Fe,isParamKey:!0,customKey:ke,chipValue:void 0,chipParamLabels:void 0}:Fe);try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:f.templateIdentifier,locale:f.locale}}))}catch{}const xe=mt(ye.text),Ee=R.customKey??xe?.key??"";return Ee?It(Ee,f.locale)??null:null},[v,P,xt,f.templateIdentifier,f.locale,f.selectedRoute,b,h]),rM=u.useCallback(async W=>{const X=P;if(!v||!X)return null;const R=typeof xt?.text=="string"?xt.text:"",de=mt(R),ye=X.customKey??de?.key??null,we=mo(R,W),Te=X.path;let ke=[];return v.patchLayout(xe=>(ke=Ot({children:xe},Te,zt=>({...zt,text:we})).children??[],ke)),b.push({actionKind:"inline_text_edit",label:"inline_remove_binding",snapshot:ke}),ye&&await kr(f.templateIdentifier,ye,W,f.locale),ye?It(ye,f.locale)??null:null},[v,P,xt,f.templateIdentifier,f.locale,b]),[kl,Hh]=u.useState({}),iM=["fontSize","color","fontStyle","textDecoration","textAlign","letterSpacing","lineHeight","fontFamily"];u.useEffect(()=>{if(!P||!t||typeof MutationObserver>"u"){Hh({});return}const W=`[data-editor-path="${an(P.domPath)}"]`,X=()=>{const ye=t.querySelector(W);if(!ye)return;const we={};for(const ke of iM){const xe=ye.style.getPropertyValue(ke.replace(/([A-Z])/g,"-$1").toLowerCase());xe&&(we[ke]=xe)}const Te=yg(t,P.domPath)??void 0;Hh({className:ye.className,style:we,effectiveColor:Te})};X();const R=t.querySelector(W);if(!R)return;const de=new MutationObserver(X);return de.observe(R,{attributes:!0,attributeFilter:["class","style"]}),de.observe(t,{childList:!0,subtree:!0}),()=>de.disconnect()},[P?.path?at(P.path):null,t]);const Fh=u.useCallback((W,X)=>{if(!v||!P)return;const R=P.path;let de=[],ye=null;v.patchLayout(we=>(de=Ot({children:we},R,xe=>{const Ee=X(xe);return ye=typeof Ee.name=="string"?Ee.name:null,Ee}).children??[],de)),b.push({actionKind:"inline_text_edit",label:`inline_format ${W}`,snapshot:de}),sr({source:"style_control",componentPath:at(R),componentName:ye,patchKey:W,timestamp:Date.now()})},[v,P,b]),ft=u.useMemo(()=>{const W=S.selectedPath;if(!W||!_)return null;const X=[];let R=W;for(;R;){X.push(R);const de=R.match(/^(.*)\.children\.\d+$/);R=de?de[1]:null}for(const de of X){const ye=Ze(de);if(!ye)continue;const we=We(_,ye),Te=typeof we?.name=="string"?we.name:null;if(!Te)continue;const ke=Oi(s,Te),xe=ke?.canvasOverlay?.kind;if(typeof xe!="string"||!xe||!we)continue;const Ee=ke.canvasOverlay.params&&typeof ke.canvasOverlay.params=="object"?ke.canvasOverlay.params:void 0,Fe=de===W?null:W.slice(de.length+1);return{kind:xe,params:Ee,tablePath:de,tableNode:we,selectedCellRel:Fe}}return null},[S.selectedPath,_,s]),ho=ft?FT(ft.kind):null,Kh=u.useMemo(()=>!ho||!t||!ft?null:po(t,ft.tablePath),[ho,t,ft,A,w]),sM=u.useMemo(()=>{if(!ho||!t||!ft)return[];const W=ft.tablePath,X=an(W),R=[];return t.querySelectorAll(`[data-editor-path^="${X}.children."]`).forEach(we=>{const Te=we.dataset.editorPath??"";if(!Te.startsWith(W+"."))return;const ke=Te.slice(W.length+1),xe=so(we,t);xe&&R.push({path:ke,top:xe.top,left:xe.left,width:xe.width,height:xe.height})}),t.querySelectorAll(`[data-editor-item-path^="${X}."]`).forEach(we=>{const Te=we.dataset.editorItemPath??"";if(!Te.startsWith(W+"."))return;const ke=Te.slice(W.length+1),xe=so(we,t);xe&&R.push({path:ke,top:xe.top,left:xe.left,width:xe.width,height:xe.height})}),R},[ho,t,ft,A,w]),aM=u.useCallback(W=>{const X=ft?Ze(ft.tablePath):null;if(!v||!X)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},X,()=>W).children??[],R)),b.push({actionKind:"inline_text_edit",label:"inplace_edit",snapshot:R}),sr({source:"style_control",componentPath:ft?.tablePath??"",componentName:typeof W.name=="string"?W.name:null,patchKey:"canvas_inplace",timestamp:Date.now()})},[v,U,b]);return t?i.jsxs("div",{className:"g7le-editor-canvas-overlay","data-testid":"g7le-editor-canvas-overlay",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[!D&&Uz(),D&&i.jsxs("div",{"data-testid":"g7le-canvas-pick-banner",style:{position:"absolute",top:8,left:"50%",transform:"translateX(-50%)",zIndex:9999,display:"flex",alignItems:"center",gap:12,padding:"8px 14px",background:"#1e293b",color:"#fff",borderRadius:8,fontSize:13,boxShadow:"0 4px 12px rgba(0,0,0,0.25)",pointerEvents:"auto"},children:[i.jsxs("span",{children:["🎯 ",h("layout_editor.target_picker.canvas_pick_hint")]}),i.jsx("button",{type:"button","data-testid":"g7le-canvas-pick-cancel",onClick:()=>Y(D,null),style:{border:"1px solid #475569",borderRadius:6,background:"transparent",color:"#cbd5e1",cursor:"pointer",fontSize:12,padding:"3px 10px"},children:h("layout_editor.target_picker.cancel")})]}),f.editMode!=="route"&&i.jsx(EI,{frameEl:t,components:w,editMode:f.editMode,currentExtensionId:C,editableRootPath:E}),P&&((P.isParamKey&&P.customKey||P.chipValue)&&xt?i.jsxs(i.Fragment,{children:[xt&&i.jsx(fg,{node:xt,spec:s,t:h,onApplyControl:Fh,box:P.box,scope:{colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:Ur(f.previewDevice,xt,f.previewCustomWidth)}}),i.jsx(i4,{box:P.box,node:xt,customKey:P.customKey??null,initialChipValue:P.chipValue,chipParamLabels:P.chipParamLabels,onKeyify:tM,templateIdentifier:f.templateIdentifier,locale:f.locale,t:h,candidates:ln,onInsertBinding:oM,onRemoveBinding:rM,onCommit:eM,onCancel:jh})]}):i.jsxs(i.Fragment,{children:[xt&&i.jsx(fg,{node:xt,spec:s,t:h,onApplyControl:Fh,box:P.box,scope:{colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:xt?Ur(f.previewDevice,xt,f.previewCustomWidth):"base"}}),i.jsx(r4,{box:P.box,initialValue:P.initialValue,isCustomKey:P.isCustomKey,t:h,onCommit:Rz,onCancel:jh,mirrorClassName:kl.className,mirrorStyle:kl.style,nodeEffectiveColor:kl.effectiveColor??P.nodeEffectiveColor,onOpenTranslations:nM})]})),!D&&i.jsx(v7,{hoverBox:T,selectedBox:A,lockKind:S.selectedLockKind,navAffordance:S.selectedNavAffordance,onEditProps:ml,onDuplicate:Ae,onDelete:Ne,onSeparateBranch:Le?.mode==="separate"?q:void 0,separateBranchLabel:$e?.separate,onMergeBranch:Le?.mode==="merge"?ae:void 0,mergeBranchLabel:$e?.merge,definedDeviceBranches:be,onJumpToDevice:be.length>0?j:void 0,onLinkEditDestination:Ie,onEditBase:dt,onEditExtension:pt,extensionLabel:ze,baseLayoutLabel:S.selectedBaseLayout,dataSourceLabel:S.selectedDataSourceId,branchLabel:ee,onEditIteration:S.selectedIsIteration&&f.editMode!=="iteration_item"?ot:void 0,resizeEnabledAxes:ir.enabledAxes,onResizeHandlePointerDown:ir.enabledAxes.width||ir.enabledAxes.height?Zz:void 0,selectedName:Ce,onSelectParent:Z?se:void 0}),D&&B&&i.jsx("div",{"data-testid":"g7le-canvas-pick-hover","data-pick-has-id":B.hasId?"true":"false",style:{position:"absolute",left:B.box.left,top:B.box.top,width:B.box.width,height:B.box.height,pointerEvents:"none",zIndex:9998,boxSizing:"border-box",border:B.hasId?"2px solid #2563eb":"2px dashed #94a3b8",background:B.hasId?"rgba(37,99,235,0.10)":"repeating-linear-gradient(45deg, rgba(148,163,184,0.18) 0, rgba(148,163,184,0.18) 6px, transparent 6px, transparent 12px)",cursor:B.hasId?"pointer":"not-allowed"},children:!B.hasId&&i.jsx("span",{"data-testid":"g7le-canvas-pick-hover-hint",style:{position:"absolute",top:0,left:0,transform:"translateY(-100%)",background:"#475569",color:"#fff",fontSize:11,lineHeight:1.4,padding:"2px 6px",borderRadius:4,whiteSpace:"nowrap"},children:h("layout_editor.target_picker.needs_id")})}),!D&&i.jsx(C7,{selectedBox:qe,points:Se.points,onAddRequest:Re}),ho&&ft&&Kh&&S.selectedLockKind==="none"&&i.jsx(ho,{node:ft.tableNode,params:ft.params,nodeBox:Kh,cellBoxes:sM,selectedCellPath:ft.selectedCellRel??void 0,colorScheme:f.previewColorScheme==="dark"?"dark":"light",t:h,onPatchNode:aM,onInsertChild:ge,onRemoveChild:W=>{const X=Ze(W);if(!v||!X)return;let R=[];v.patchLayout(de=>(R=Ri({children:de},X).children??[],R)),b.push({actionKind:"remove",label:"inplace_remove",snapshot:R})},onRequestInlineEdit:W=>{ft&&G(`${ft.tablePath}.${W}`)}}),v&&i.jsx(xI,{frameEl:t,nesting:o,editMode:f.editMode,currentExtensionId:C,editableRootPath:E,components:w,patchLayout:v.patchLayout,pushHistory:le,selectedPath:S.selectedPath,onSelectPath:W=>S.handleSelect("",{dataset:{editorPath:W}}),onMovePath:W=>S.handleSelect("",{dataset:{editorPath:W}}),onRequestInlineEdit:G})]}):null}function hg(e,t){if(t.length===0)return null;const o=Ha({children:e},t);return o&&typeof o.name=="string"?o.name:null}function Ha(e,t){return We(e,t)}function an(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function yg(e,t){if(!t)return null;const n=e.querySelector(`[data-editor-path="${an(t)}"]`);if(!n||typeof window.getComputedStyle!="function")return null;let o=n;const r=n.ownerDocument.createTreeWalker(n,NodeFilter.SHOW_TEXT);let s=r.nextNode();for(;s;){if((s.textContent??"").trim().length>0){const a=s.parentElement;if(a&&n.contains(a)){o=a;break}}s=r.nextNode()}try{return window.getComputedStyle(o).color||null}catch{return null}}function po(e,t){if(!t)return null;const n=e.querySelector(`[data-editor-path="${an(t)}"]`);if(n){const c=so(n,e);return c&&ao(c,e)?c:null}const o=e.querySelectorAll(`[data-editor-path^="${an(t)}.iteration."]`);let r=1/0,s=1/0,a=-1/0,l=-1/0,d=!1;return o.forEach(c=>{const p=c.dataset.editorPath??"";if(!new RegExp("\\.iteration\\.\\d+$").test(p))return;const f=so(c,e);f&&ao(f,e)&&(d=!0,r=Math.min(r,f.left),s=Math.min(s,f.top),a=Math.max(a,f.left+f.width),l=Math.max(l,f.top+f.height))}),d?{left:r,top:s,width:a-r,height:l-s,scale:1}:null}function f4({isEditMode:e,editorSpec:t,sampleOverride:n}){const o=t?.sampleData;return u.useMemo(()=>Pm({spec:o,overrideSpec:n,onMatch:r=>{Uh(r)}}),[e,o,n])}const g4=Lt().ComponentRegistry;function h4(e){return e.split(/[-_]/).map(t=>t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()).join("")}async function y4(e){const t=typeof window<"u"?window.localStorage?.getItem("auth_token"):null,n={Accept:"application/json"};t&&(n.Authorization=`Bearer ${t}`);const o=await fetch(`/api/admin/templates/${encodeURIComponent(e)}/editor-assets`,{headers:n,credentials:"same-origin"});if(!o.ok){const s=await o.json().catch(()=>null);throw Gr(o.status,s,"assets")}const r=await o.json();return r.data||r}async function b4(e){try{if(typeof fetch!="function")return 0;const t=await fetch(en(`/api/templates/${encodeURIComponent(e)}/config`,"json"),{credentials:"same-origin",headers:{Accept:"application/json"}});if(!t.ok)return 0;const n=await t.json().catch(()=>null),o=n?.data?.cache_version??n?.cache_version;return typeof o=="number"&&o>0?o:0}catch{return 0}}function m4(e){return new Promise((t,n)=>{if(typeof document>"u"){n(new Error("document undefined"));return}if(document.querySelector(`script[data-g7le-asset="${e}"]`)){t();return}const r=document.createElement("script");r.src=e,r.async=!1,r.setAttribute("data-g7le-asset",e),r.addEventListener("load",()=>t()),r.addEventListener("error",()=>n(new Error(`script load failed: ${e}`))),document.head.appendChild(r)})}async function v4(e){if(typeof document>"u")return;const t=`[data-g7le-asset="${e}"]`;if(document.querySelector(`link${t}, style${t}`))return;if(e.includes("/api/admin/"))try{const r=typeof window<"u"?window.localStorage?.getItem("auth_token"):null,s={Accept:"text/css"};r&&(s.Authorization=`Bearer ${r}`);const a=await fetch(e,{headers:s,credentials:"same-origin"});if(a.ok){const l=await a.text(),d=document.createElement("style");d.textContent=l,d.setAttribute("data-g7le-asset",e),document.head.appendChild(d);return}}catch{}const o=document.createElement("link");o.rel="stylesheet",o.href=e,o.setAttribute("data-g7le-asset",e),document.head.appendChild(o)}function _4(e,t,n="user"){const[o,r]=u.useState({componentRegistry:null,translationEngine:null,isReady:!1,error:null});return u.useEffect(()=>{let s=!1;return(async()=>{r(a=>({...a,isReady:!1,error:null}));try{const a=await y4(e);if(s)return;for(const y of a.css??[])if(await v4(y),s)return;const l=h4(e);if(!(typeof window<"u"&&typeof window[l]=="object")){for(const y of a.js??[])if(await m4(y),s)return}const c=g4.createIsolatedInstance();if(await c.loadComponents(e,n),s)return;const p=await b4(e);if(s)return;const f=new Pt({defaultLocale:t});if(p>0&&f.setCacheVersion(p),await f.loadTranslations(e,t),s)return;cd(f,e);try{const y=Pt.getInstance();p>y.getCacheVersion()&&y.setCacheVersion(p),await y.loadTranslations(e,t),cd(y,e)}catch{}if(s)return;r({componentRegistry:c,translationEngine:f,isReady:!0,error:null})}catch(a){if(s)return;const l=hp(a)?a:io(a,"assets");r({componentRegistry:null,translationEngine:null,isReady:!1,error:l})}})(),()=>{s=!0}},[e,t,n]),o}const bg=Lt().AuthManager;function x4(e){switch(e){case"unauthorized":return{icon:"🔒",tone:"blue",titleKey:"layout_editor.access_error.unauthorized.title",messageKey:"layout_editor.access_error.unauthorized.message",showRequiredPermissions:!0};case"forbidden":return{icon:"⛔",tone:"rose",titleKey:"layout_editor.access_error.forbidden.title",messageKey:"layout_editor.access_error.forbidden.message",showRequiredPermissions:!0};case"not_found":return{icon:"🗂️",tone:"slate",titleKey:"layout_editor.access_error.not_found.title",messageKey:"layout_editor.access_error.not_found.message",showRequiredPermissions:!1};case"server_error":return{icon:"🛠️",tone:"amber",titleKey:"layout_editor.access_error.server_error.title",messageKey:"layout_editor.access_error.server_error.message",showRequiredPermissions:!1};case"network":return{icon:"📡",tone:"amber",titleKey:"layout_editor.access_error.network.title",messageKey:"layout_editor.access_error.network.message",showRequiredPermissions:!1};default:return{icon:"⚠️",tone:"slate",titleKey:"layout_editor.access_error.unknown.title",messageKey:"layout_editor.access_error.unknown.message",showRequiredPermissions:!1}}}const w4={blue:{bg:"#eff6ff",border:"#bfdbfe",iconBg:"#dbeafe"},rose:{bg:"#fff1f2",border:"#fecdd3",iconBg:"#ffe4e6"},amber:{bg:"#fffbeb",border:"#fde68a",iconBg:"#fef3c7"},slate:{bg:"#f8fafc",border:"#e2e8f0",iconBg:"#f1f5f9"}};function k4({error:e}){return u.useEffect(()=>{if(e.kind!=="unauthorized"||typeof window>"u")return;const t=bg.getInstance(),n=`${window.location.pathname}${window.location.search}`,o=t.getLoginRedirectUrl("admin",n,"session_expired"),r=window.setTimeout(()=>{window.location.href=o},600);return()=>window.clearTimeout(r)},[e.kind]),null}function Fa({error:e}){const{t}=tt(),n=x4(e.kind),o=w4[n.tone],r=()=>{typeof window<"u"&&(window.location.href="/admin")},s=()=>{typeof window<"u"&&window.location.reload()},a=()=>{if(typeof window>"u")return;const l=bg.getInstance(),d=`${window.location.pathname}${window.location.search}`;window.location.href=l.getLoginRedirectUrl("admin",d)};return i.jsxs("div",{className:"g7le-access-error","data-testid":"g7le-access-error","data-error-kind":e.kind,style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:24,background:"#f1f5f9"},children:[i.jsx(k4,{error:e}),i.jsxs("div",{style:{maxWidth:480,width:"100%",background:o.bg,border:`1px solid ${o.border}`,borderRadius:12,padding:"28px 32px",boxShadow:"0 2px 8px rgba(15, 23, 42, 0.05)",textAlign:"center"},children:[i.jsx("div",{style:{width:64,height:64,margin:"0 auto 16px",borderRadius:"50%",background:o.iconBg,display:"flex",alignItems:"center",justifyContent:"center",fontSize:32,lineHeight:1},"aria-hidden":"true",children:n.icon}),i.jsx("h2",{"data-testid":"g7le-access-error-title",style:{margin:"0 0 8px",fontSize:18,fontWeight:700,color:"#0f172a"},children:t(n.titleKey)}),i.jsx("p",{"data-testid":"g7le-access-error-message",style:{margin:"0 0 16px",fontSize:13,lineHeight:1.6,color:"#475569"},children:t(n.messageKey)}),n.showRequiredPermissions&&e.requiredPermissions&&i.jsxs("div",{"data-testid":"g7le-access-error-permissions",style:{display:"inline-flex",flexWrap:"wrap",gap:6,justifyContent:"center",padding:"8px 12px",marginBottom:16,background:"rgba(15, 23, 42, 0.04)",borderRadius:8,fontSize:12,color:"#475569"},children:[i.jsx("span",{style:{fontWeight:600},children:t("layout_editor.access_error.required_permission_label")}),e.requiredPermissions.split(/[,\s]+/).filter(Boolean).map(l=>i.jsx("code",{style:{padding:"2px 6px",background:"#fff",border:"1px solid #e2e8f0",borderRadius:4,fontFamily:"ui-monospace, SFMono-Regular, Menlo, monospace",fontSize:11,color:"#1e293b"},children:l},l))]}),e.message&&i.jsxs("details",{"data-testid":"g7le-access-error-detail",style:{marginBottom:16,fontSize:11,color:"#94a3b8"},children:[i.jsx("summary",{style:{cursor:"pointer"},children:t("layout_editor.access_error.detail_summary")}),i.jsxs("pre",{style:{marginTop:8,padding:8,background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,overflowX:"auto",whiteSpace:"pre-wrap",wordBreak:"break-word",textAlign:"left",fontSize:11,color:"#475569"},children:[e.status>0?`HTTP ${e.status} -`:"",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!="object")return e;o=o[n[a]]}if(!Array.isArray(o)||o.length<=1)return e;const r={...e};let s=r;for(let a=0;a<n.length-1;a++){const l=n[a],d=s[l];if(d==null||typeof d!="object")return e;s[l]=Array.isArray(d)?[...d]:{...d},s=s[l]}return s[n[n.length-1]]=o.slice(0,1),r}const M4="SlotContainer";function _g(e,t=new Set){for(const n of e){if(!n||typeof n!="object")continue;const o=n;if(o.name===M4){const r=o.props?.slotId;typeof r=="string"&&t.add(r)}Array.isArray(o.children)&&_g(o.children,t)}return t}function L4(e){const t=_g(e),n=o=>o.map(r=>{if(!r||typeof r!="object")return r;const s=r;let a=s;if(typeof s.slot=="string"&&!t.has(s.slot)){const{slot:l,...d}=s;a={...d,__editorSlotName:l}}if(Array.isArray(a.children)){const l=n(a.children);l!==a.children&&(a={...a,children:l})}return a});return n(e)}function St(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Xt(e){if(Array.isArray(e))return e.map(t=>Xt(t));if(St(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=Xt(o);return t}return e}function mi(e,t){for(const[n,o]of Object.entries(t)){const r=e[n];St(o)&&St(r)?mi(r,o):e[n]=Xt(o)}}function P4(e){const t=[];let n="";for(let o=0;o<e.length;o+=1){const r=e[o];if(r==="["){n.length>0&&(t.push(n),n="");const s=e[o+1];if(s==="'"||s==='"'){const l=e.indexOf(`${s}]`,o+2);if(l!==-1){t.push(e.slice(o+2,l)),o=l+1;continue}}const a=e.indexOf("]",o+1);if(a!==-1){t.push(e.slice(o+1,a)),o=a;continue}n+=r}else r==="."?n.length>0&&(t.push(n),n=""):n+=r}return n.length>0&&t.push(n),t.filter(o=>o.length>0)}function Ua(e,t,n){const o=P4(t);if(o.length===0)return;let r=e;for(let s=0;s<o.length-1;s+=1){const a=o[s],l=r[a];if(St(l)){const d=Xt(l);r[a]=d,r=d}else{const d={};r[a]=d,r=d}}r[o[o.length-1]]=Xt(n)}function O4(e){const t=Xt(e.globalBaseline??{}),n=Xt(e.localBaseline??{}),o=Xt(e.queryBaseline??{}),r=Xt(e.routeBaseline??{});if(e.patch&&(St(e.patch.global)&&mi(t,e.patch.global),St(e.patch.local)&&mi(n,e.patch.local),St(e.patch.query)&&mi(o,e.patch.query),St(e.patch.route)))for(const[s,a]of Object.entries(e.patch.route))a===null?delete r[s]:r[s]=Xt(a);if(St(e.formErrors))for(const[s,a]of Object.entries(e.formErrors))s.startsWith("_global.")?Ua(t,s.slice(8),a):s.startsWith("_local.")?Ua(n,s.slice(7),a):Ua(n,s,a);return{global:t,local:n,query:o,route:r}}function xg(e){if(!e||!St(e.sampleDataOverrides))return;const t=e.sampleDataOverrides;return St(t.byDataSourceId)||St(t.byEndpointPattern)||St(t.bySource)?t:void 0}function qa(e){if(!e||!St(e.formErrors))return;const t=e.formErrors;return Object.keys(t).length>0?t:void 0}const Ga="g7le:editor-t-ready",wg="data-g7le-vh-expanded";function B4(e,t){if(!e||!t)return;const n=t;e.querySelectorAll("*").forEach(r=>{const s=window.getComputedStyle(r),a=parseFloat(s.height),l=Number.isFinite(a)&&Math.abs(a-n)<=2,d=parseFloat(s.minHeight),c=Number.isFinite(d)&&Math.abs(d-n)<=2,p=s.overflowY==="auto"||s.overflowY==="scroll"||s.overflowY==="hidden"||s.overflow==="auto"||s.overflow==="scroll"||s.overflow==="hidden";l&&p?(r.style.setProperty("height","auto","important"),c&&r.style.setProperty("min-height","0","important"),r.style.setProperty("overflow","visible","important"),r.setAttribute(wg,"1")):(s.overflowY==="auto"||s.overflowY==="scroll")&&r.scrollHeight>r.clientHeight+4&&r.clientHeight>120&&(r.style.setProperty("overflow","visible","important"),r.style.setProperty("height","auto","important"),r.setAttribute(wg,"1"))})}const W4=[];function j4(e={}){const{manifest:t=null,nesting:n=null,componentPalette:o=null,spec:r=null,permissionCandidates:s,sampleGlobalSources:a=W4,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c}=e,{state:p}=Je(),f=Ft(),y=Cp(),{document:g,isLoading:m,error:h}=f??y,v=u.useRef(null),[b,x]=u.useState(null),{deviceWidth:k,scale:w,colorScheme:_}=ja(),C=u.useMemo(()=>p.activeStateId?p.availableStates.find(Q=>Q.id===p.activeStateId)??null:null,[p.activeStateId,p.availableStates]),E=u.useMemo(()=>xg(C),[C]),S=f4({isEditMode:!0,editorSpec:r??void 0,sampleOverride:E}),{t:T}=tt(),{componentRegistry:$,translationEngine:A,isReady:z,error:D}=_4(p.templateIdentifier,p.locale),O=u.useRef(null);O.current===null&&(O.current=new vd);const M=u.useRef(null);if(M.current===null){M.current=new VE({navigate:()=>{}}),M.current.setPreviewMode(!0);try{const ie=window.__templateApp?.getActionDispatcher?.();if(ie&&typeof ie.getRegisteredHandlers=="function"){const _e=ie.getRegisteredHandlers();for(const ge of _e){if(!ge.includes("."))continue;const Ae=typeof ie.getHandler=="function"?ie.getHandler(ge):void 0;typeof Ae=="function"&&M.current.registerHandler(ge,Ae,{category:"plugin",source:"preview-canvas-cloned-from-host"})}}}catch{}}u.useEffect(()=>{if(!z)return;const Q=M.current;if(Q)try{const ie=window.G7Core?.getActionDispatcher?.();if(!ie||typeof ie.getRegisteredHandlers!="function")return;const _e=ie.getRegisteredHandlers();for(const ge of _e){if(!ge.includes(".")||typeof Q.getHandler=="function"&&Q.getHandler(ge))continue;const Ae=typeof ie.getHandler=="function"?ie.getHandler(ge):void 0;typeof Ae=="function"&&Q.registerHandler(ge,Ae,{category:"plugin",source:"preview-canvas-iife-reclone"})}}catch{}},[z]);const B=u.useMemo(()=>new UE({sampleProvider:S,actionDispatcher:M.current}),[S]),N=u.useMemo(()=>g?.raw?.meta?.guest_only===!0,[g]),H=u.useMemo(()=>Va({sources:a,isGuestOnly:N}),[a,N]),L=u.useMemo(()=>{const Q=H4(qa(C),A,{templateId:p.templateIdentifier,locale:p.locale}),ie=H._local,{global:_e,local:ge}=O4({globalBaseline:H,localBaseline:ie??{},patch:C?.initialState??null,formErrors:Q});return{..._e,_local:ge}},[H,C,A,p.templateIdentifier,p.locale]),I=u.useMemo(()=>!C||!(!!C.initialState?.local||!!qa(C))?null:{...L._local??{},_forceLocalInit:C.id,_merge:"deep"},[C,L]),P=Q=>typeof Q=="object"&&Q!==null&&!Array.isArray(Q),F=u.useMemo(()=>P(C?.initialState?.query)?C.initialState.query:null,[C]),G=u.useMemo(()=>P(C?.initialState?.route)?C.initialState.route:null,[C]),[V,Y]=u.useState(()=>Va({sources:[],isGuestOnly:!1})),Z=u.useRef(null);u.useEffect(()=>{const Q={...L},ie=C4({actionDispatcher:M.current,locale:p.locale,initialGlobalState:Q,onChange:ge=>{Y(ge.globalState)}});Z.current=ie;const _e=window.__templateApp;return _e?.setGlobalState&&M.current.setGlobalStateUpdater((ge,Ae)=>{_e.setGlobalState(ge,Ae)}),Y(Q),$l({kind:"applyPatch",activeStateId:p.activeStateId,routePath:p.selectedRoute?.path??null,hasInitialPatch:!!C?.initialState,hasFormErrors:!!qa(C),hasSampleOverride:!!xg(C)}),()=>{ie.restore(),Z.current=null}},[p.locale,L]),u.useEffect(()=>{if(!A)return;const Q=window.G7Core;if(!Q||typeof Q.t!="function")return;const ie=Q.t,_e=p.templateIdentifier,ge=p.locale;return Q.t=(Ae,Ne)=>{try{const Le=Ne?"|"+Object.entries(Ne).map(([ae,pe])=>`${ae}=${pe}`).join("|"):void 0,q=A.translate(Ae,{templateId:_e,locale:ge},Le);if(q!==Ae&&!q.startsWith("$t:"))return q}catch{}return ie(Ae,Ne)},typeof window<"u"&&window.dispatchEvent(new CustomEvent(Ga)),()=>{window.G7Core?.t&&window.G7Core.t!==ie&&(window.G7Core.t=ie)}},[A,p.templateIdentifier,p.locale]);const se=u.useMemo(()=>{const Q=g?.raw?.components??[];return p.editMode!=="base"?Q:L4(Q)},[g,p.editMode]),re=g?.raw?.__editability,U=g?.raw?.__extensionId,ee=u.useMemo(()=>p.editMode!=="extension"||re!=="ok"?[]:typeof U!="number"?[]:F4(se,U),[p.editMode,re,U,se]),[fe,K]=u.useState(null),[ne,me]=u.useState(!1),Ce=g?.layoutName??null,ze=u.useMemo(()=>{const Q=g?.raw?.scripts??[];return Array.isArray(Q)?Q.map(ie=>ie&&typeof ie.src=="string"?ie.src:"").join("|"):""},[g]);u.useEffect(()=>{me(!1)},[Ce,ze]),u.useEffect(()=>{let Q=!1;const ie=g?.raw?.scripts??[];if(!Array.isArray(ie)||ie.length===0){me(!0);return}const _e=[];for(const ge of ie){if(!ge?.src||typeof ge.src!="string")continue;const Ae=ge.id?`script#${CSS.escape(ge.id)}, script[data-g7le-canvas-script="${ge.src}"]`:`script[data-g7le-canvas-script="${ge.src}"], script[src="${ge.src}"]`,Ne=window.document.querySelector(Ae);if(Ne){const Le=Ne.hasAttribute("data-g7le-canvas-script");if(Ne.hasAttribute("data-g7le-script-loaded")||!Le)continue;_e.push(new Promise(ae=>{if(Ne.hasAttribute("data-g7le-script-loaded")){ae();return}Ne.addEventListener("load",()=>ae()),Ne.addEventListener("error",()=>ae())}));continue}_e.push(new Promise(Le=>{const q=window.document.createElement("script");q.src=ge.src,q.async=!1,ge.id&&(q.id=ge.id),q.setAttribute("data-g7le-canvas-script",ge.src);const ae=()=>{q.setAttribute("data-g7le-script-loaded","1"),Le()};q.addEventListener("load",ae),q.addEventListener("error",ae),window.document.head.appendChild(q)}))}if(_e.length===0){me(!0);return}return Promise.all(_e).then(()=>{Q||requestAnimationFrame(()=>{requestAnimationFrame(()=>{Q||me(!0)})})}),()=>{Q=!0}},[g]);const Se=u.useMemo(()=>g?.raw?.data_sources??[],[g]),qe=u.useMemo(()=>g?.raw?.computed??{},[g]),[De,Pe]=u.useState({}),Qe=u.useMemo(()=>{if(p.editMode!=="iteration_item")return De;const Q=g?.iterationContext;if(!Q)return De;const ie=g?.raw?.components??[],_e=We({children:ie},Q.sourceIndexPath),ge=_e&&_e.iteration&&typeof _e.iteration.source=="string"?_e.iteration.source:null;return ge?z4(De,ge):De},[p.editMode,De,g]),[Me,Re]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const Q=()=>Re(ie=>ie+1);return window.addEventListener(kt,Q),()=>window.removeEventListener(kt,Q)},[]);const ve=u.useMemo(()=>E4(p.selectedRoute?.path??null),[p.selectedRoute?.path]);if(u.useEffect(()=>{let Q=!1;return(async()=>{if(Se.length===0){Q||Pe({});return}try{const ie=await B.fetchDataSources(Se,ve,new URLSearchParams,void 0,void 0);Q||Pe(ie)}catch{Q||Pe({})}})(),()=>{Q=!0}},[Se,B,ve]),u.useEffect(()=>{const Q=b;if(!Q||typeof window>"u")return;let ie=0,_e=0,ge=0;const Ae=()=>B4(Q,window.innerHeight);ie=window.requestAnimationFrame(()=>{_e=window.requestAnimationFrame(Ae)});let Ne=null;return typeof MutationObserver<"u"&&(Ne=new MutationObserver(()=>{ge||(ge=window.requestAnimationFrame(()=>{ge=0,Ae()}))}),Ne.observe(Q,{childList:!0,subtree:!0})),()=>{window.cancelAnimationFrame(ie),window.cancelAnimationFrame(_e),ge&&window.cancelAnimationFrame(ge),Ne&&Ne.disconnect()}},[b,se,ne,k,De]),u.useEffect(()=>{const Q=b;if(!Q||typeof window>"u"||p.editMode!=="extension"||re!=="ok"||ee.length===0){K(null);return}let ie=0,_e=0;const ge=ae=>{if(!ae)return!1;const pe=ae.getBoundingClientRect();return pe.width>0&&pe.height>0},Ae=ae=>{const pe=Q.querySelector(`[data-editor-path="${ae}"]`);if(ge(pe))return!0;const $e=Q.querySelectorAll(`[data-editor-path^="${ae}.children."]`);for(const be of Array.from($e))if(ge(be))return!0;return!1},Ne=()=>{K(ee.some(Ae))};ie=window.requestAnimationFrame(()=>{_e=window.requestAnimationFrame(Ne)});let Le=null,q=0;return typeof MutationObserver<"u"&&(Le=new MutationObserver(()=>{q||(q=window.requestAnimationFrame(()=>{q=0,Ne()}))}),Le.observe(Q,{childList:!0,subtree:!0})),()=>{window.cancelAnimationFrame(ie),window.cancelAnimationFrame(_e),q&&window.cancelAnimationFrame(q),Le&&Le.disconnect()}},[b,p.editMode,re,ee,ne,De]),u.useEffect(()=>{if(typeof window>"u"||p.editMode!=="base")return;const Q=window.document,ie="g7le-base-slot-style";if(Q.getElementById(ie))return;const _e=Q.createElement("style");_e.id=ie;const ge=T("layout_editor.preview.slot_area_label");return _e.textContent=`.g7le-preview-frame [data-editor-slot]{position:relative;min-height:96px;outline:2px dashed #94a3b8;outline-offset:-2px;}.g7le-preview-frame [data-editor-slot]::after{content:'${ge.replace(/'/g,"\\'")} · ' attr(data-editor-slot);position:absolute;top:8px;left:50%;transform:translateX(-50%);font-size:12px;line-height:1;color:#64748b;background:rgba(241,245,249,0.92);border:1px solid #cbd5e1;border-radius:9999px;padding:4px 12px;pointer-events:none;white-space:nowrap;}`,Q.head.appendChild(_e),()=>{Q.getElementById(ie)?.remove()}},[p.editMode,T]),!(p.editMode==="extension"||p.editMode==="iteration_item")&&(p.selectedRoute?.layoutName===null||p.selectedRoute===null))return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--empty","data-testid":"g7le-preview-empty",style:Zo,children:T("layout_editor.preview.empty")});if(m)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--loading","data-testid":"g7le-preview-loading",style:Zo,children:T("layout_editor.preview.loading")});if(h)return i.jsx(Fa,{error:h});if(D)return i.jsx(Fa,{error:D});if(!z||$===null||A===null)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--assets-loading","data-testid":"g7le-preview-assets-loading",style:Zo,children:T("layout_editor.preview.assets_loading")});if(se.length===0)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--empty-layout","data-testid":"g7le-preview-empty-layout",style:Zo,children:T("layout_editor.preview.empty_layout")});const he=p.editMode==="extension"?re==="no-injection"?"no-injection":re==="ok"&&ee.length>0&&fe!==!0?"gated-no-state":null:null;return ne?i.jsx("div",{className:"g7le-preview-canvas","data-testid":"g7le-preview-canvas",style:{overflow:"visible",background:"#f1f5f9",padding:24,display:"flex",justifyContent:"center",alignItems:"flex-start",minHeight:"calc(100vh - var(--g7le-header-h, 0px))",boxSizing:"border-box"},children:i.jsxs("div",{className:"g7le-preview-frame-wrapper","data-testid":"g7le-preview-frame-wrapper",style:{position:"relative",width:k,transform:`scale(${w})`,transformOrigin:"top center"},children:[i.jsx("div",{className:_==="dark"?"g7le-preview-frame g7le-preview-dark":"g7le-preview-frame","data-testid":"g7le-preview-frame","data-color-scheme":_,ref:Q=>{v.current=Q,x(Q)},style:{width:k,background:"#fff",border:"1px solid #cbd5e1",boxShadow:"0 1px 3px rgba(0,0,0,0.05)",minHeight:400,position:"relative",transform:"translateZ(0)",overflow:"hidden"},children:i.jsx(KE,{overrideWidth:k,children:se.map((Q,ie)=>i.jsx(FE,{componentDef:Q,dataContext:{...Qe,...T4(V),_global:D4(V),_computedDefinitions:qe,...I?{_localInit:I,_local:{...De._local??{},...V._local??{}}}:{},route:K4({...De.route??{},...ve,path:p.selectedRoute?.path??""},G),query:{...De.query??{},...F??{}}},translationContext:{templateId:p.templateIdentifier,locale:p.locale},registry:$,bindingEngine:O.current,translationEngine:Pt.getInstance(),actionDispatcher:M.current,isEditMode:!0,isRootRenderer:ie===0,componentPath:`${ie}`,onComponentSelect:(_e,ge)=>{},onComponentHover:(_e,ge)=>{}},`${Q.id??`preview-${ie}`}:t${Me}${p.editMode==="extension"?`:s${ne?1:0}`:""}`))})}),i.jsx("div",{className:"g7le-overlay-layer","data-testid":"g7le-overlay-layer",style:{position:"absolute",left:0,top:0,width:k,height:b?b.getBoundingClientRect().height/(w||1):"100%",overflow:"visible",pointerEvents:"none"},children:i.jsx(p4,{frameEl:b,manifest:t,nesting:n,componentPalette:o,spec:r,permissionCandidates:s,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c})}),he!==null&&i.jsx("div",{className:"g7le-preview-canvas--extension-fallback","data-testid":"g7le-preview-extension-fallback","data-fallback-reason":he,style:{position:"absolute",left:0,top:0,right:0,bottom:0,minHeight:b?b.getBoundingClientRect().height/(w||1):400,background:"#f1f5f9",display:"flex",alignItems:"flex-start",justifyContent:"center",padding:24,boxSizing:"border-box",pointerEvents:"auto",zIndex:50},children:i.jsxs("div",{className:"g7le-extension-fallback-card",style:{maxWidth:440,textAlign:"center",position:"sticky",top:"calc(40vh - var(--g7le-header-h, 0px))"},children:[i.jsx("p",{className:"g7le-extension-fallback-title",style:{fontWeight:600,marginBottom:8,color:"#0f172a"},children:T(he==="no-injection"?"layout_editor.preview.extension_no_injection_title":"layout_editor.preview.extension_gated_title")}),i.jsx("p",{className:"g7le-extension-fallback-desc",style:{color:"#475569",lineHeight:1.6},children:he==="no-injection"?T("layout_editor.preview.extension_no_injection_desc"):p.availableStates.length>1?T("layout_editor.preview.extension_gated_desc_with_states"):T("layout_editor.preview.extension_gated_desc")})]})})]})}):i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--scripts-loading","data-testid":"g7le-preview-scripts-loading",style:Zo,children:T("layout_editor.preview.loading")})}function H4(e,t,n){if(!e)return;if(!t)return e;const o=s=>{if(typeof s=="string"&&s.startsWith("$t:"))try{const a=s.slice(3),l=a.indexOf("|");if(l===-1)return t.translate(a,n);const d=a.slice(0,l),c=a.slice(l);return t.translate(d,n,c)}catch{return s}return Array.isArray(s)?s.map(o):s},r={};for(const[s,a]of Object.entries(e))r[s]=o(a);return r}function F4(e,t){const n=[],o=(r,s)=>{Array.isArray(r)&&r.forEach((a,l)=>{if(!a||typeof a!="object")return;const d=s?`${s}.children.${l}`:`${l}`,c=a.__source;if(c?.kind==="extension"&&c.extensionId===t){n.push(d);return}Array.isArray(a.children)&&o(a.children,d)})};return o(e,""),n}function K4(e,t){if(!t)return e;const n={...e};for(const[o,r]of Object.entries(t))r===null?delete n[o]:n[o]=r;return n}const Zo={flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:24,background:"#f1f5f9",color:"#64748b",fontSize:13},V4={desktop:"layout_editor.device.desktop",tablet:"layout_editor.device.tablet",mobile:"layout_editor.device.mobile",portable:"layout_editor.device.portable"};function U4(){const{device:e,zoom:t,customWidth:n,setDevice:o,setZoom:r,setCustomWidth:s,colorScheme:a,setColorScheme:l}=ja(),{t:d}=tt(),c=Gf();return i.jsxs("div",{className:"g7le-device-toolbar","data-testid":"g7le-device-toolbar",style:{display:"inline-flex",alignItems:"center",gap:8,padding:"4px 8px",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6},children:[i.jsxs("div",{role:"group","aria-label":d("layout_editor.device.group_label"),children:[c.map(p=>{const f=e===p,y=V4[p],g=y?d(y):p;return i.jsx("button",{type:"button","data-testid":`g7le-device-${p}`,"aria-pressed":f,onClick:()=>o(p),style:{padding:"2px 8px",marginRight:2,fontSize:12,cursor:"pointer",background:f?"#0f172a":"#f8fafc",color:f?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:g},p)}),(()=>{const p=e==="custom";return i.jsx("button",{type:"button","data-testid":"g7le-device-custom","aria-pressed":p,onClick:()=>o("custom"),style:{padding:"2px 8px",marginRight:2,fontSize:12,cursor:"pointer",background:p?"#0f172a":"#f8fafc",color:p?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:d("layout_editor.device.custom")})})()]}),e==="custom"&&i.jsxs("div",{style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("label",{htmlFor:"g7le-custom-width-input",style:{fontSize:11,color:"#475569"},children:d("layout_editor.device.custom_width_label")}),i.jsx("input",{id:"g7le-custom-width-input",type:"number",min:ug,max:pg,step:10,value:n,"data-testid":"g7le-custom-width-input","aria-label":d("layout_editor.device.custom_width_label"),onChange:p=>{const f=parseInt(p.currentTarget.value,10);Number.isFinite(f)&&s(f)},style:{width:72,padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4}}),i.jsx("span",{style:{fontSize:11,color:"#475569"},children:"px"})]}),i.jsx("div",{role:"group","aria-label":d("layout_editor.toolbar.color_scheme_group"),style:{display:"inline-flex",gap:2},children:["light","dark"].map(p=>{const f=a===p;return i.jsx("button",{type:"button","data-testid":`g7le-toolbar-scheme-${p}`,"aria-pressed":f,onClick:()=>l(p),style:{padding:"2px 8px",fontSize:12,cursor:"pointer",background:f?"#0f172a":"#f8fafc",color:f?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:d(p==="light"?"layout_editor.toolbar.color_scheme_light":"layout_editor.toolbar.color_scheme_dark")},p)})}),i.jsxs("div",{style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("span",{style:{fontSize:11,color:"#475569"},children:d("layout_editor.zoom.label")}),i.jsx("input",{type:"range",min:.5,max:1,step:.05,value:t,"data-testid":"g7le-zoom-slider","aria-label":d("layout_editor.zoom.aria_label"),onChange:p=>r(parseFloat(p.currentTarget.value)),style:{width:100}}),i.jsxs("span",{style:{fontSize:11,color:"#475569",minWidth:36,textAlign:"right"},children:[Math.round(t*100),"%"]})]}),i.jsx(Kb,{})]})}function q4(){const{t:e}=tt(),{state:t,dispatch:n}=Je(),{availableStates:o,activeStateId:r}=t,[,s]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const p=()=>s(y=>y+1),f=window.requestAnimationFrame(p);return window.addEventListener(Ga,p),()=>{window.cancelAnimationFrame(f),window.removeEventListener(Ga,p)}},[]);const a=u.useCallback((p,f)=>{if(!p)return f;if(!p.startsWith("$t:"))return p;const y=p.slice(3),g=window.G7Core;if(g&&typeof g.t=="function"){const h=g.t(y);if(h&&h!==y&&!h.startsWith("$t:"))return h}const m=e(y);return m&&m!==y?m:f},[e]);if(!Array.isArray(o)||o.length<=1)return null;const l=p=>{const f=p.target.value;n({type:"SET_ACTIVE_STATE",activeStateId:f}),$l({kind:"change",activeStateId:f,routePath:t.selectedRoute?.path??null,availableCount:o.length})},d=o.find(p=>p.id===r)??o[0],c=d?.description?a(d.description,""):"";return i.jsxs("div",{className:"g7le-state-switcher","data-testid":"g7le-state-switcher","data-active-state":r??"",style:{display:"inline-flex",alignItems:"center",gap:6,padding:"4px 8px",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6},children:[i.jsx("span",{style:{fontSize:12,fontWeight:600,color:"#475569",whiteSpace:"nowrap"},children:e("layout_editor.toolbar.state")}),i.jsx("select",{"data-testid":"g7le-state-switcher-select","aria-label":e("layout_editor.toolbar.state"),value:r??o[0].id,onChange:l,style:{fontSize:12,padding:"2px 6px",border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",color:"#0f172a",cursor:"pointer",outline:"none"},children:o.map(p=>i.jsx("option",{value:p.id,children:a(p.label,p.id)},p.id))}),c&&i.jsx("span",{"data-testid":"g7le-state-switcher-description",style:{fontSize:11,color:"#94a3b8",whiteSpace:"nowrap"},children:c})]})}function G4(e,t,n){if(e==="extension"){if(typeof n=="string"&&n.length>0){const o=Ya(n,"__base__/");return o?{kind:"base",match:o}:{kind:"route",match:n}}return null}if(!t)return null;switch(e){case"route":return{kind:"route",match:t};case"base":{const o=Ya(t,"__base__/");return o?{kind:"base",match:o}:null}case"modal":{const o=Ya(t,"__modal__/");return o?{kind:"modal",match:o}:null}default:return null}}function Y4(e,t){if(!t||!Array.isArray(e)||e.length===0)return[];const n=[];for(const o of e)if(J4(o.scope,t))for(const r of o.items??[])r&&typeof r.id=="string"&&r.id.length>0&&n.push(r);return n}function X4(e){if(e.length===0)return null;const t=e.find(n=>n.default===!0);return t?t.id:e[0].id}function J4(e,t){if(!e||e.kind!==t.kind)return!1;const n=e.match;return typeof n!="string"||n.length===0?!1:t.kind==="route"?Z4(t.match,n):n===t.match}function Z4(e,t){if(t===e)return!0;if(!t.includes("*"))return!1;const n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\/\*\\\?/g,"(?:/[^/]+)?").replace(/\*/g,"[^/]+");return new RegExp(`^${n}$`).test(e)}function Ya(e,t){return e.startsWith(t)?e.slice(t.length):""}const Q4="__conngroup__/modals/",R4="__conngroup__/extensions/";function eD(e){return e.meta?.editor_label?{label:e.meta.editor_label,labelSource:"editor_label"}:e.meta?.title?{label:e.meta.title,labelSource:"title"}:{label:e.path,labelSource:"path"}}function tD(e){switch(e.source.kind){case"template":return"template";case"module":return`module:${e.source.identifier??""}`;case"plugin":return`plugin:${e.source.identifier??""}`;case"core":return null;default:return null}}function nD(e,t){if(e.meta?.parent&&t.has(e.meta.parent))return e.meta.parent;const n=e.path.split("/").filter(Boolean);for(let o=n.length-1;o>=1;o--){const r="/"+n.slice(0,o).join("/");if(r!==e.path&&t.has(r))return r}return null}function oD(e){const t=new Map;for(const n of e)n.layout&&t.set(n.layout,(t.get(n.layout)??0)+1);return t}function rD(e){const t=new Map;for(const n of e){if(!n.hostLayout)continue;const o=t.get(n.hostLayout)??[];o.push(n),t.set(n.hostLayout,o)}return t}function iD(e){const t=new Map;for(const n of e)for(const o of n.extensions)for(const r of o.hostLayouts??[]){if(!r)continue;const s=t.get(r)??[];s.push({extension:o,group:n}),t.set(r,s)}return t}function sD(e,t){return{path:`__modal__/${e.hostLayout}/${e.modalId}`,layoutName:e.hostLayout,label:e.label??e.modalId,labelSource:e.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"modal",modalId:e.modalId,modalHostLayout:e.hostLayout,connectedHostRoutePath:t,children:[]}}function aD(e,t){const{extension:n,group:o}=e,r=o.sourceType==="plugin"?"🔌":"🧩";return{path:`__extension__/${n.id}`,layoutName:null,label:`${r} ${o.sourceLabel} · ${n.targetName}`,labelSource:"path",source:{kind:o.sourceType,identifier:o.sourceIdentifier},kind:"extension",extensionId:String(n.id),extensionType:n.extensionType,extensionPriority:n.priority,extensionTargetName:n.targetName,isModified:n.isModified,isInactive:!n.isActive,connectedHostRoutePath:t,children:[]}}function kg(e,t,n){if(e.kind!=="route"&&e.kind!=="base"||!e.layoutName)return;const o=[],r=t.get(e.layoutName)??[];r.length>0&&o.push({path:`${Q4}${e.path}`,layoutName:null,label:`$t:layout_editor.chrome.route_tree.connected.modals|count=${r.length}`,labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:r.map(a=>sD(a,e.path))});const s=n.get(e.layoutName)??[];s.length>0&&o.push({path:`${R4}${e.path}`,layoutName:null,label:`$t:layout_editor.chrome.route_tree.connected.extensions|count=${s.length}`,labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:s.map(a=>aD(a,e.path))}),o.length>0&&(e.children=[...o,...e.children??[]])}function lD(e){const t=[],n=new Set(e.routes.map(o=>o.path));for(const o of e.routes){const r=tD(o);if(!r)continue;const{label:s,labelSource:a}=eD(o),l=nD(o,n),d={path:o.path,layoutName:o.layout??null,label:s,labelSource:a,icon:o.meta?.icon??null,source:o.source,isRedirect:!!o.redirect,isHidden:!!o.meta?.hidden,kind:"route",children:[]};t.push({node:d,parentKey:l,groupKey:r})}return t}function dD(e,t){if(e==="template")return"$t:layout_editor.chrome.route_tree.group.template";if(e.startsWith("module:")){const n=e.slice(7);return`$t:layout_editor.chrome.route_tree.group.module|name=${t.moduleDisplayNames[n]??n}`}if(e.startsWith("plugin:")){const n=e.slice(7);return`$t:layout_editor.chrome.route_tree.group.plugin|name=${t.pluginDisplayNames[n]??n}`}return e}function cD(e,t){oD(e.routes);const n=rD(e.modals),o=iD(e.extensionGroups??[]),r=new Map;for(const d of t)r.has(d.groupKey)||r.set(d.groupKey,[]),r.get(d.groupKey).push(d);const s=Array.from(r.keys()).sort((d,c)=>{const p=g=>g==="template"?1:g.startsWith("module:")?2:g.startsWith("plugin:")?3:4,f=p(d),y=p(c);return f!==y?f-y:d.localeCompare(c)}),a=[];if(e.baseLayouts.length>0){const d=e.baseLayouts.map(c=>({path:`__base__/${c.layoutName}`,layoutName:c.layoutName,label:c.label??c.layoutName,labelSource:c.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"base",children:[]}));for(const c of d)kg(c,n,o);a.push({path:"__group__/base",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.base",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}for(const d of s){const c=r.get(d),p=new Map;for(const y of c)p.set(y.node.path,y);const f=[];for(const y of c)if(y.parentKey&&p.has(y.parentKey)){const g=p.get(y.parentKey);g.node.children=g.node.children??[],g.node.children.push(y.node)}else f.push(y.node);for(const y of c)kg(y.node,n,o);a.push({path:`__group__/${d}`,layoutName:null,label:dD(d,e),labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:f})}if(e.modals.length>0){const d=e.modals.map(c=>({path:`__modal__/${c.hostLayout}/${c.modalId}`,layoutName:c.hostLayout,label:c.label??c.modalId,labelSource:c.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"modal",modalId:c.modalId,modalHostLayout:c.hostLayout,children:[]}));a.push({path:"__group__/modal",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.modal",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}const l=e.extensionGroups??[];if(l.length>0){const d=l.map(c=>{const p=c.sourceType==="plugin"?"🔌":"🧩",f=c.extensions.map(y=>({path:`__extension__/${y.id}`,layoutName:null,label:`${c.sourceLabel} · ${y.targetName}`,labelSource:"path",source:{kind:c.sourceType,identifier:c.sourceIdentifier},kind:"extension",extensionId:String(y.id),extensionType:y.extensionType,extensionPriority:y.priority,extensionTargetName:y.targetName,isModified:y.isModified,isInactive:!y.isActive,children:[]}));return{path:`__extgroup__/${c.sourceType}/${c.sourceIdentifier}`,layoutName:null,label:`${p} ${c.sourceLabel}`,labelSource:"path",source:{kind:c.sourceType,identifier:c.sourceIdentifier},kind:"route",children:f}});a.push({path:"__group__/extension",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.extension",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}return a}function uD(e){const t=lD(e);return cD(e,t)}const On=bo("useEditorRoutes");function pD(e,t){if(typeof e=="string")return e.length>0?e:null;if(e&&typeof e=="object"){const n=e[t];if(typeof n=="string"&&n.length>0)return n;if(typeof e.en=="string"&&e.en.length>0)return e.en;for(const o of Object.values(e))if(typeof o=="string"&&o.length>0)return o}return null}function Sg(e,t){const n={};if(typeof window>"u")return n;const r=window.G7Config?.[e];if(!Array.isArray(r))return n;for(const s of r)s&&typeof s.identifier=="string"&&(n[s.identifier]=pD(s.display_name,t)??s.identifier);return n}function fD(e){const t=new vd,n=typeof window<"u"?window.G7Config:void 0,o={_global:{settings:n?.settings??{},plugins:n?.plugins??{},modules:n?.modules??{},appConfig:n?.appConfig??{}}};return e.map(r=>{const s={...r};if(typeof s.path=="string"&&s.path.includes("{{")){const a=s.path;let l=t.resolveBindings(s.path,o);l=l.replace(/\/\/+/g,"/")||"/",l.includes("{{")&&(On.warn("편집기 route path 표현식 평가 실패 — fallback 적용",{originalPath:a}),l=a.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/"),s.path=l}if(typeof s.redirect=="string"&&s.redirect.includes("{{")){const a=s.redirect;let l=t.resolveBindings(a,o);l=l.replace(/\/\/+/g,"/")||"/",l.includes("{{")&&(On.warn("편집기 route redirect 표현식 평가 실패 — fallback 적용",{originalRedirect:a}),l=a.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/"),s.redirect=l}return s})}function gD(e){const{dispatch:t}=Je(),{translationEngine:n,translationContext:o}=tt(),{templateIdentifier:r,cacheVersion:s}=e;u.useEffect(()=>{let a=!1;const l=s??(typeof window<"u"?window.G7Config?.cache_version:void 0)??"",d=l!==""?`?v=${encodeURIComponent(String(l))}`:"",c=en(`/api/admin/templates/${encodeURIComponent(r)}/editor/routes`,"json",null,d.replace(/^[?&]+/,"")),p=o?.locale??"ko",f=n&&r!==o?.templateId?n.loadTranslations(r,p).then(()=>{On.log("편집 대상 lang 로드 완료",{targetTemplate:r,locale:p})}).catch(h=>{On.warn("편집 대상 lang 로드 실패 — 라우트 라벨 fallback 사용",h)}):Promise.resolve(),y=(()=>{if(typeof fetch!="function")return Promise.reject(io(new Error("fetch unavailable"),"routes"));try{const h=fetch(c,{credentials:"same-origin",headers:je()});return!h||typeof h.then!="function"?Promise.reject(io(new Error("fetch returned non-Promise (test stub?)"),"routes")):h.then(async v=>{if(!v.ok){const b=await v.json().catch(()=>null);throw Gr(v.status,b,"routes")}return v.json()})}catch(h){return Promise.reject(h)}})(),g=`/api/admin/templates/${encodeURIComponent(r)}/layout-extensions${d}`,m=(()=>{if(typeof fetch!="function")return Promise.resolve(null);try{const h=fetch(g,{credentials:"same-origin",headers:je()});return!h||typeof h.then!="function"?Promise.resolve(null):h.then(async v=>v.ok?await v.json():null).catch(()=>null)}catch{return Promise.resolve(null)}})();return Promise.all([y,f,m]).then(([h,,v])=>{if(a)return;if(!h||!h.success||!Array.isArray(h.data?.routes)){On.warn("routes 응답 형식이 비정상 — empty tree 로 둠",h),t({type:"SET_ROUTES_ERROR",error:{kind:"unknown",status:200,message:"routes 응답 형식이 비정상입니다.",source:"routes"}});return}const b=fD(h.data.routes),x=Sg("activeModules",p),k=Sg("activePlugins",p),w=(h.data.base_layouts??[]).map(A=>({layoutName:A.layout_name,label:A.label??void 0})),_=(h.data.modals??[]).map(A=>({modalId:A.modal_id,hostLayout:A.host_layout,label:A.label??void 0})),C=(v?.success&&Array.isArray(v.data)?v.data:[]).map(A=>({sourceIdentifier:A.source_identifier,sourceType:A.source_type,sourceLabel:A.source_label,extensions:(A.extensions??[]).map(z=>({id:z.id,extensionType:z.extension_type,targetName:z.target_name,priority:z.priority,isActive:z.is_active,isModified:z.is_modified,hostLayouts:Array.isArray(z.host_layouts)?z.host_layouts:[],currentVersion:typeof z.current_version=="number"?z.current_version:void 0}))})),E=uD({routes:b,modals:_,baseLayouts:w,extensionGroups:C,moduleDisplayNames:x,pluginDisplayNames:k});t({type:"SET_ROUTE_TREE",tree:E});const S=h.data.layout_versions,T={};if(S&&typeof S=="object"&&!Array.isArray(S))for(const[A,z]of Object.entries(S))typeof z=="number"&&Number.isFinite(z)&&(T[A]=z);t({type:"SET_LAYOUT_VERSIONS",versions:T});const $={};for(const A of C)for(const z of A.extensions){const D=z.currentVersion;typeof D=="number"&&Number.isFinite(D)&&($[String(z.id)]=D)}t({type:"SET_EXTENSION_VERSIONS",versions:$}),On.log("routes 로드 완료",{count:b.length,groups:E.length})}).catch(h=>{if(a)return;On.error("routes 로드 실패",h);const v=hp(h)?h:io(h,"routes");t({type:"SET_ROUTES_ERROR",error:v})}),()=>{a=!0}},[r,s,t,n,o?.locale,o?.templateId])}function Cg(e,t,n,o=[]){const r=t?$g(o,t):null,s=r!==null?{path:r.path,layoutName:r.layoutName}:void 0;if(e.startsWith("__base__/")){const a=e.slice(9);if(a)return n({type:"ENTER_BASE_EDIT",layoutName:a,returnRoute:s}),!0}else if(e.startsWith("__modal__/")){const a=e.slice(10);return a&&!a.includes("/")&&t?(n({type:"ENTER_MODAL_EDIT",modalId:a,hostLayout:t,returnRoute:s}),!0):!1}else if(e.startsWith("__extension__/")){const a=e.slice(14);if(a&&!a.includes("/"))return n({type:"ENTER_EXTENSION_EDIT",extensionId:a,returnRoute:s}),!0}else if(e.startsWith("__iteration__/")){const a=e.slice(14);return a&&t?(n({type:"ENTER_ITERATION_ITEM_EDIT",sourcePath:a,hostLayout:t,returnRoute:s}),!0):!1}return!1}function $g(e,t){for(const n of e){if(n.kind==="route"&&n.layoutName===t)return n;if(n.children&&n.children.length>0){const o=$g(n.children,t);if(o)return o}}return null}function Xa(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=Xa(n.children,t);if(o)return o}}return null}function hD(e){const{state:t,dispatch:n}=Je(),o=u.useRef(!1),r=u.useRef(t.editMode);r.current=t.editMode,u.useEffect(()=>{if(o.current||t.routeTree.length===0||typeof window>"u")return;const a=ap(window.location.search);if(a){const c=sp(window.location.search),p=Cg(a,c,n,t.routeTree);if(o.current=!0,p)return}const l=ip(window.location.search);if(!l){o.current=!0;return}const d=Xa(t.routeTree,l);d&&n({type:"SELECT_ROUTE",route:{path:d.path,layoutName:d.layoutName}}),o.current=!0},[t.routeTree,n]),u.useEffect(()=>{if(typeof window>"u")return;const a=()=>{const l=ap(window.location.search);if(l){const p=sp(window.location.search);if(Cg(l,p,n,t.routeTree))return}if(r.current!=="route")switch(r.current){case"base":n({type:"EXIT_BASE_EDIT"});return;case"modal":n({type:"EXIT_MODAL_EDIT"});return;case"extension":n({type:"EXIT_EXTENSION_EDIT"});return;case"iteration_item":n({type:"EXIT_ITERATION_ITEM_EDIT"});return}const d=ip(window.location.search);if(!d){n({type:"SELECT_ROUTE",route:null});return}const c=Xa(t.routeTree,d);c&&n({type:"SELECT_ROUTE",route:{path:c.path,layoutName:c.layoutName}})};return window.addEventListener("popstate",a),()=>{window.removeEventListener("popstate",a)}},[t.routeTree,n]);const s=u.useRef("route");u.useEffect(()=>{if(typeof window>"u"||!window.history?.replaceState||t.routeTree.length===0)return;const a=Zs(e,t.selectedRoute?.path??null,t.selectedRoute?.layoutName??null),l=`${window.location.pathname}${window.location.search}`,d=s.current;if(s.current=t.editMode,l===a)return;d==="route"&&t.editMode!=="route"&&window.history.pushState?window.history.pushState(window.history.state,"",a):window.history.replaceState(window.history.state,"",a)},[t.selectedRoute,t.editMode,t.routeTree,e])}function yD(){u.useEffect(()=>{if(typeof document>"u")return;const e=document.documentElement;let t=e.classList.contains("dark");const n=()=>{e.classList.contains("dark")&&(t=!0,e.classList.remove("dark"))};n();let o=null;return typeof MutationObserver<"u"&&(o=new MutationObserver(n),o.observe(e,{attributes:!0,attributeFilter:["class"]})),()=>{o?.disconnect(),t&&e.classList.add("dark")}},[])}function bD(){u.useEffect(()=>{if(typeof document>"u"||!document.body)return;const e=document.body,t=()=>{e.style.overflow==="hidden"&&(e.style.overflow=""),e.style.overflowY==="hidden"&&(e.style.overflowY="")};t();let n=null;return typeof MutationObserver<"u"&&(n=new MutationObserver(t),n.observe(e,{attributes:!0,attributeFilter:["style"]})),()=>{n?.disconnect()}},[])}function mD(e){if(typeof e!="string")return null;const t="__extension__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n===""||n.includes("/")?null:n}function vD(e){if(Array.isArray(e.components))return e.components;if(Array.isArray(e.injections)){const t=[];return e.injections.forEach((n,o)=>{if(n&&Array.isArray(n.components))for(const r of n.components)t.push({...r,__injectionIndex:o})}),t}return[]}function _D(e,t){if(Array.isArray(e.components)){const n=t.map(o=>Ja(o));return{...e,components:n}}if(Array.isArray(e.injections)){const n=e.injections.map(o=>({...o}));n.forEach(o=>{Array.isArray(o.components)&&(o.components=[])});for(const o of t){const r=o.__injectionIndex,s=typeof r=="number"&&r>=0&&r<n.length?n[r]:null;s&&(Array.isArray(s.components)||(s.components=[]),s.components.push(Ja(o)))}return{...e,injections:n}}return e}function Ja(e){if(!e||typeof e!="object")return e;const t={...e};return delete t.__injectionIndex,delete t.__source,delete t.__injectedProps,delete t._fromBase,Array.isArray(t.children)&&(t.children=t.children.map(n=>Ja(n))),t}function Eg(e,t){const n=[],o=r=>{if(Array.isArray(r))for(const s of r){if(!s||typeof s!="object")continue;const a=s.__source;if(a?.kind==="extension"&&a.extensionId===t){n.push(s);continue}Array.isArray(s.children)&&o(s.children)}};return o(e),n}function Ig(e,t){const o=Eg(e,t)[0]?.__source;return o&&o.kind==="extension"&&o.extensionId===t?o:{kind:"extension",extensionId:t}}function Za(e,t){for(const n of e){const o=n?.__source;if(o?.kind==="extension"&&o.extensionId===t||Array.isArray(n.children)&&Za(n.children,t))return!0}return!1}function xD(){const{state:e}=Je(),t=e.templateIdentifier,n=e.editMode==="extension"?mD(e.selectedRoute?.path):null,o=e.editMode==="extension"&&typeof e.selectedRoute?.layoutName=="string"?e.selectedRoute.layoutName:null,[r,s]=u.useState(null),[a,l]=u.useState(!1),[d,c]=u.useState(null),[p,f]=u.useState(!1),[y,g]=u.useState(0),[m,h]=u.useState(null),v=u.useRef({}),b=u.useRef({kind:"extension",extensionId:0}),x=u.useRef(null);u.useEffect(()=>{x.current=r},[r]),u.useEffect(()=>{h(null)},[n]);const k=u.useCallback(async A=>{const z=window.G7Config?.cache_version??0,D=en(`/api/layouts/${encodeURIComponent(t)}/${A}`,"json",`${z}.${Sp()}`,"with_source_meta=1"),O=ar(),M={Accept:"application/json"};O&&(M.Authorization=`Bearer ${O}`);const B=await fetch(D,{headers:M,credentials:"same-origin"});if(!B.ok)return null;const N=await B.json().catch(()=>null);return(N&&(N.data||N))??{}},[t]),w=u.useCallback(async()=>{if(!n){s(null),c(null),f(!1);return}l(!0),c(null);try{const A=window.G7Config?.cache_version??0,z=`/api/admin/templates/${encodeURIComponent(t)}/layout-extensions/${encodeURIComponent(n)}?v=${A}`,D=await fetch(z,{headers:je(),credentials:"same-origin"}),O=await D.json().catch(()=>null);if(!D.ok){c(Gr(D.status,O,"layout")),s(null),f(!1);return}const M=(O&&(O.data||O))??{};let B={};const N=M.content;if(typeof N=="string")try{B=JSON.parse(N)}catch{B={}}else N&&typeof N=="object"&&(B=N);const H=M.extension_type==="overlay"?"overlay":"extension_point",L=typeof M.lock_version=="number"?M.lock_version:0,I=Array.isArray(M?.host_layouts)?M.host_layouts.filter(re=>typeof re=="string"):[],P=o??(I.length===1?I[0]:m&&I.includes(m)?m:null),F=Number(n);v.current=B;let V=vD(B),Y={},Z="pending";const se={kind:"extension",extensionId:F};if(b.current=se,P){const re=await k(P);if(re){Y=re;const U=Array.isArray(re.components)?re.components:[],ee=Ig(U,F);if(b.current=ee,Za(U,F))V=U,Z="ok";else{const K=(Array.isArray(re.modals)?re.modals:[]).filter(ne=>ne&&typeof ne=="object"&&Za([ne],F));if(K.length>0){const ne=K.map(me=>({...me,props:{...me.props??{},isOpen:!0}}));V=[...U,...ne],b.current=Ig(K,F),Z="ok"}else V=U,Z="no-injection"}}}s({extensionId:F,content:B,extensionType:H,components:V,lockVersion:L,hostLayouts:I,hostLayoutName:P,hostRaw:Y,editability:Z}),f(!1),st({op:"load",layoutName:`extension:${n}`,editMode:"extension",statusCode:D.status,timestamp:Date.now()})}catch(A){c(io(A,"layout")),s(null),f(!1)}finally{l(!1)}},[t,n,m,o,k]);u.useEffect(()=>{w()},[w]);const _=u.useCallback(A=>{h(A)},[]),C=u.useCallback(A=>{s(z=>z&&{...z,components:A}),f(!0),st({op:"patch",layoutName:`extension:${x.current?.extensionId??""}`,editMode:"extension",isDirty:!0,timestamp:Date.now()})},[]),E=u.useCallback(A=>{s(z=>z&&{...z,components:A(z.components)}),f(!0)},[]),S=u.useCallback(async()=>{const A=x.current;if(!A)return{kind:"guard_no_document"};const z=`/api/admin/templates/${encodeURIComponent(t)}/layout-extensions/${A.extensionId}`,D=Eg(A.components,A.extensionId),O=D.length>0?D:A.components,M=_D(v.current,O);st({op:"save",layoutName:`extension:${A.extensionId}`,editMode:"extension",saveTarget:"layout_extension",endpoint:z,isDirty:p,timestamp:Date.now()});let B;try{B=await fetch(z,{method:"PUT",headers:je({"Content-Type":"application/json"}),credentials:"same-origin",body:JSON.stringify({content:M,expected_lock_version:A.lockVersion})})}catch(H){return{kind:"network_error",message:H instanceof Error?H.message:"network error"}}const N=await B.json().catch(()=>null);if(st({op:"save_response",layoutName:`extension:${A.extensionId}`,editMode:"extension",endpoint:z,statusCode:B.status,conflict:B.status===409?{currentVersion:N?.errors?.current_version??N?.current_version,yourVersion:N?.errors?.your_version??N?.your_version}:void 0,timestamp:Date.now()}),B.ok){const H=N?.data??{},L=typeof H.lock_version=="number"?H.lock_version:A.lockVersion+1;s(P=>P&&{...P,lockVersion:L}),f(!1),g(P=>P+1);const I=typeof H.current_version=="number"?H.current_version:void 0;return{kind:"success",newLockVersion:L,newContentVersion:I,savedExtensionId:String(A.extensionId)}}return B.status===409?{kind:"concurrent_modification",currentVersion:N?.current_version??-1,yourVersion:N?.your_version??A.lockVersion}:B.status===422?{kind:"validation_failed",status:422,errors:N?.errors??null}:{kind:"network_error",message:N?.message??`HTTP ${B.status}`}},[t,p]),T=u.useCallback(async()=>{na(),await w()},[w]),$=!!r&&r.hostLayouts.length>1&&r.hostLayoutName===null;return{document:r,isLoading:a,error:d,isDirty:p,saveSuccessCounter:y,setLayoutComponents:C,patchLayout:E,save:S,reload:T,needsHostPicker:$,selectHost:_}}const Ag=new Set;function wD(e,t,n){const r=e&&Object.keys(e).length>0?e:t,s=Array.isArray(r.data_sources)?r.data_sources:[],a=Array.isArray(t.data_sources)?t.data_sources:[],l=new Set,d=[];for(const c of[...s,...a]){const p=c&&typeof c=="object"?c.id:void 0,f=typeof p=="string"?p:`__anon_${d.length}`;l.has(f)||(l.add(f),d.push(c))}return{...r,components:n,...d.length>0?{data_sources:d}:{},lock_version:void 0}}function kD(e){return{document:e.document?{layoutName:`extension:${e.document.extensionId}`,raw:{...wD(e.document.hostRaw,e.document.content,e.document.components),__editability:e.document.editability,__extensionId:e.document.extensionId},lockVersion:e.document.lockVersion}:null,isLoading:e.isLoading,error:e.error,isDirty:e.isDirty,saveSuccessCounter:e.saveSuccessCounter,reload:e.reload,patchLayout:e.patchLayout,setLayoutComponents:e.setLayoutComponents,patchDocumentRaw:()=>{},dirtyKeys:Ag,dirtyLayoutNames:Ag,invalidateCurrentCache:()=>{},reloadCounter:0,save:()=>e.save(),markDirty:()=>{}}}function SD({hostLayouts:e,extensionLabel:t,onSelect:n}){const{t:o}=tt();return i.jsx("div",{className:"g7le-host-picker__backdrop","data-testid":"g7le-host-picker",role:"dialog","aria-modal":"true","aria-label":o("layout_editor.host_picker.title"),style:{position:"fixed",inset:0,zIndex:9500,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(15, 23, 42, 0.55)"},children:i.jsxs("div",{className:"g7le-host-picker__panel",style:{width:"min(520px, 92vw)",maxHeight:"80vh",overflow:"auto",background:"#ffffff",color:"#0f172a",borderRadius:12,boxShadow:"0 20px 48px rgba(15, 23, 42, 0.28)",padding:"20px 22px"},children:[i.jsx("h2",{className:"g7le-host-picker__title",style:{margin:"0 0 6px",fontSize:16,fontWeight:700},children:o("layout_editor.host_picker.title")}),i.jsx("p",{className:"g7le-host-picker__desc",style:{margin:"0 0 16px",fontSize:13,color:"#475569",lineHeight:1.6},children:t?o("layout_editor.host_picker.description_with_label",{label:t}):o("layout_editor.host_picker.description")}),i.jsx("ul",{className:"g7le-host-picker__list",style:{listStyle:"none",margin:0,padding:0,display:"grid",gap:8},children:e.map(r=>i.jsx("li",{children:i.jsxs("button",{type:"button",className:"g7le-host-picker__item","data-testid":"g7le-host-picker-item","data-host":r,onClick:()=>n(r),style:{width:"100%",textAlign:"left",padding:"12px 14px",border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc",cursor:"pointer",fontSize:13,fontWeight:600,color:"#0f172a",display:"flex",alignItems:"center",gap:8},children:[i.jsx("span",{"aria-hidden":!0,style:{fontSize:14},children:"🖼"}),i.jsx("span",{style:{flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis"},children:r}),i.jsx("span",{"aria-hidden":!0,style:{color:"#94a3b8"},children:"→"})]})},r))})]})})}function CD(e,t){const[n,o]=u.useState(!1),[r,s]=u.useState(null),a=u.useCallback(async l=>{if(!t||!l)return{kind:"no_document"};o(!0),s(null);try{const d=vo(l),c=`/api/admin/templates/${encodeURIComponent(e)}/layouts/${t}/preview`,p=await fetch(c,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({content:JSON.stringify(d)})}),f=await p.json().catch(()=>null);if(!p.ok){const h=f?.message??`HTTP ${p.status}`;return s(h),{kind:"network_error",message:h}}const y=f?.data,g=typeof y?.token=="string"?y.token:"",m=typeof y?.preview_url=="string"&&y.preview_url?y.preview_url:g?`/preview/${g}`:"";return m?(window.G7Core?.dispatch?.({handler:"openWindow",params:{path:m}}),{kind:"success",previewUrl:m,token:g}):(s("invalid preview response"),{kind:"network_error",message:"invalid preview response"})}catch(d){const c=d instanceof Error?d.message:"network error";return s(c),{kind:"network_error",message:c}}finally{o(!1)}},[e,t]);return{isCreating:n,error:r,createPreview:a}}const ce="layout_editor.action_recipe",bn={setState:{label:`$t:${ce}.set_state.label`,params:[{key:"target",label:`$t:${ce}.set_state.param_target`,widget:"select",options:[{value:"local"},{value:"global"}]},{key:"state",label:`$t:${ce}.set_state.param_state`,widget:"state-key-value"},{key:"merge",label:`$t:${ce}.set_state.param_merge`,widget:"toggle"}],build:{handler:"setState",params:{target:"{{target}}",merge:"{{merge}}","...":"{{state}}"}}},loadFromLocalStorage:{label:`$t:${ce}.load_local.label`,params:[{key:"key",label:`$t:${ce}.load_local.param_key`,widget:"text",required:!0},{key:"stateKey",label:`$t:${ce}.load_local.param_state_key`,widget:"state-key-picker"},{key:"defaultValue",label:`$t:${ce}.load_local.param_default`,widget:"text"}],build:{handler:"loadFromLocalStorage",params:{key:"{{key}}",stateKey:"{{stateKey}}",defaultValue:"{{defaultValue}}"}}},saveToLocalStorage:{label:`$t:${ce}.save_local.label`,params:[{key:"key",label:`$t:${ce}.save_local.param_key`,widget:"text",required:!0},{key:"value",label:`$t:${ce}.save_local.param_value`,widget:"data-chip"}],build:{handler:"saveToLocalStorage",params:{key:"{{key}}",value:"{{value}}"}}},navigate:{label:`$t:${ce}.navigate.label`,params:[{key:"path",label:`$t:${ce}.navigate.param_path`,widget:"page-picker",required:!0},{key:"query",label:`$t:${ce}.navigate.param_query`,widget:"key-value"},{key:"mergeQuery",label:`$t:${ce}.navigate.param_merge_query`,widget:"toggle"},{key:"scroll",label:`$t:${ce}.navigate.param_scroll`,widget:"select",options:[{value:"top"},{value:"preserve"},{value:"none"}]},{key:"replace",label:`$t:${ce}.navigate.param_replace`,widget:"toggle"},{key:"transition_overlay_target",label:`$t:${ce}.navigate.param_overlay_target`,widget:"component-target-picker",dependsOn:{param:"replace",equals:!0}}],build:{handler:"navigate",params:{path:"{{path}}",query:"{{query}}",mergeQuery:"{{mergeQuery}}",scroll:"{{scroll}}",replace:"{{replace}}",transition_overlay_target:"{{transition_overlay_target}}"}}},openWindow:{label:`$t:${ce}.open_window.label`,params:[{key:"target",label:`$t:${ce}.open_window.param_target`,widget:"page-picker",required:!0},{key:"windowTarget",label:`$t:${ce}.open_window.param_window_target`,widget:"select",options:[{value:"_blank"},{value:"_self"}]}],build:{handler:"openWindow",target:"{{target}}",params:{windowTarget:"{{windowTarget}}"}}},navigateBack:{label:`$t:${ce}.navigate_back.label`,params:[],build:{handler:"navigateBack"}},navigateForward:{label:`$t:${ce}.navigate_forward.label`,params:[],build:{handler:"navigateForward"}},replaceUrl:{label:`$t:${ce}.replace_url.label`,params:[{key:"path",label:`$t:${ce}.replace_url.param_path`,widget:"page-picker"},{key:"query",label:`$t:${ce}.replace_url.param_query`,widget:"key-value"}],build:{handler:"replaceUrl",params:{path:"{{path}}",query:"{{query}}"}}},toast:{label:`$t:${ce}.toast.label`,params:[{key:"message",label:`$t:${ce}.toast.param_message`,widget:"i18n-text",required:!0},{key:"type",label:`$t:${ce}.toast.param_type`,widget:"select",options:[{value:"info"},{value:"success"},{value:"warning"},{value:"error"}]},{key:"duration",label:`$t:${ce}.toast.param_duration`,widget:"number"},{key:"icon",label:`$t:${ce}.toast.param_icon`,widget:"text"}],build:{handler:"toast",params:{message:"{{message}}",type:"{{type}}",duration:"{{duration}}",icon:"{{icon}}"}}},openModal:{label:`$t:${ce}.open_modal.label`,params:[{key:"target",label:`$t:${ce}.open_modal.param_target`,widget:"modal-picker",required:!0}],build:{handler:"openModal",target:"{{target}}"}},closeModal:{label:`$t:${ce}.close_modal.label`,params:[],build:{handler:"closeModal"}},showAlert:{label:`$t:${ce}.show_alert.label`,params:[{key:"target",label:`$t:${ce}.show_alert.param_message`,widget:"i18n-text",required:!0}],build:{handler:"showAlert",target:"{{target}}"}},setError:{label:`$t:${ce}.set_error.label`,params:[{key:"target",label:`$t:${ce}.set_error.param_message`,widget:"i18n-text"},{key:"stateTarget",label:`$t:${ce}.set_error.param_state_target`,widget:"select",options:[{value:"local"},{value:"global"}]}],build:{handler:"setError",target:"{{target}}",params:{stateTarget:"{{stateTarget}}"}}},refetchDataSource:{label:`$t:${ce}.refetch.label`,params:[{key:"dataSourceId",label:`$t:${ce}.refetch.param_data_source`,widget:"datasource-picker",required:!0},{key:"sync",label:`$t:${ce}.refetch.param_sync`,widget:"toggle"}],build:{handler:"refetchDataSource",params:{dataSourceId:"{{dataSourceId}}",sync:"{{sync}}"}}},appendDataSource:{label:`$t:${ce}.append_data.label`,params:[{key:"dataSourceId",label:`$t:${ce}.append_data.param_data_source`,widget:"datasource-picker",required:!0},{key:"dataPath",label:`$t:${ce}.append_data.param_data_path`,widget:"text"},{key:"newData",label:`$t:${ce}.append_data.param_new_data`,widget:"data-chip"}],build:{handler:"appendDataSource",params:{dataSourceId:"{{dataSourceId}}",dataPath:"{{dataPath}}",newData:"{{newData}}"}}},updateDataSource:{label:`$t:${ce}.update_data.label`,params:[{key:"dataSourceId",label:`$t:${ce}.update_data.param_data_source`,widget:"datasource-picker",required:!0},{key:"data",label:`$t:${ce}.update_data.param_data`,widget:"data-chip"},{key:"merge",label:`$t:${ce}.update_data.param_merge`,widget:"toggle"}],build:{handler:"updateDataSource",params:{dataSourceId:"{{dataSourceId}}",data:"{{data}}",merge:"{{merge}}"}}},scrollIntoView:{label:`$t:${ce}.scroll.label`,params:[{key:"selector",label:`$t:${ce}.scroll.param_selector`,widget:"text",required:!0},{key:"behavior",label:`$t:${ce}.scroll.param_behavior`,widget:"select",options:[{value:"smooth"},{value:"auto"}]},{key:"block",label:`$t:${ce}.scroll.param_block`,widget:"select",options:[{value:"start"},{value:"center"},{value:"end"},{value:"nearest"}]}],build:{handler:"scrollIntoView",params:{selector:"{{selector}}",behavior:"{{behavior}}",block:"{{block}}"}}},login:{label:`$t:${ce}.login.label`,params:[{key:"body",label:`$t:${ce}.login.param_body`,widget:"key-value"}],build:{handler:"login",params:{body:"{{body}}"}}},logout:{label:`$t:${ce}.logout.label`,params:[{key:"target",label:`$t:${ce}.logout.param_target`,widget:"text"}],build:{handler:"logout",target:"{{target}}"}},setLocale:{label:`$t:${ce}.set_locale.label`,params:[{key:"target",label:`$t:${ce}.set_locale.param_target`,widget:"locale-picker",required:!0}],build:{handler:"setLocale",target:"{{target}}"}},emitEvent:{label:`$t:${ce}.emit_event.label`,params:[{key:"event",label:`$t:${ce}.emit_event.param_event`,widget:"text",required:!0},{key:"data",label:`$t:${ce}.emit_event.param_data`,widget:"data-chip"}],build:{handler:"emitEvent",params:{event:"{{event}}",data:"{{data}}"}}},apiCall:{label:`$t:${ce}.api_call.label`,params:[{key:"target",label:`$t:${ce}.api_call.param_target`,widget:"page-picker",required:!0},{key:"method",label:`$t:${ce}.api_call.param_method`,widget:"select",options:[{value:"GET"},{value:"POST"},{value:"PUT"},{value:"PATCH"},{value:"DELETE"}]},{key:"body",label:`$t:${ce}.api_call.param_body`,widget:"key-value"},{key:"query",label:`$t:${ce}.api_call.param_query`,widget:"key-value"},{key:"identity_target_email",label:`$t:${ce}.api_call.param_identity_target_email`,widget:"data-chip"},{key:"identity_target_phone",label:`$t:${ce}.api_call.param_identity_target_phone`,widget:"data-chip"},{key:"onSuccess",label:`$t:${ce}.api_call.param_on_success`,widget:"action-list"},{key:"onError",label:`$t:${ce}.api_call.param_on_error`,widget:"action-list"}],build:{handler:"apiCall",target:"{{target}}",identity_target:{email:"{{identity_target_email}}",phone:"{{identity_target_phone}}"},onSuccess:"{{onSuccess}}",onError:"{{onError}}",params:{method:"{{method}}",body:"{{body}}",query:"{{query}}"}}},conditions:{label:`$t:${ce}.conditions.label`,params:[{key:"branches",label:`$t:${ce}.conditions.param_branches`,widget:"branch-list"}],build:{handler:"conditions",conditions:"{{branches}}"}},sequence:{label:`$t:${ce}.sequence.label`,params:[{key:"actions",label:`$t:${ce}.sequence.param_actions`,widget:"action-list"}],build:{handler:"sequence",params:{actions:"{{actions}}"}}},parallel:{label:`$t:${ce}.parallel.label`,params:[{key:"actions",label:`$t:${ce}.parallel.param_actions`,widget:"action-list"}],build:{handler:"parallel",params:{actions:"{{actions}}"}}},switch:{label:`$t:${ce}.switch.label`,params:[{key:"value",label:`$t:${ce}.switch.param_value`,widget:"data-chip"},{key:"cases",label:`$t:${ce}.switch.param_cases`,widget:"action-list",advanced:!0}],build:{handler:"switch",params:{value:"{{value}}",cases:"{{cases}}"}}},suppress:{label:`$t:${ce}.suppress.label`,params:[],build:{handler:"suppress"}}};function $D(){return{actionRecipes:bn,initActionRecipes:bn,errorRecipes:{navigate:bn.navigate,openModal:bn.openModal,toast:bn.toast,setState:bn.setState,sequence:bn.sequence,parallel:bn.parallel,showErrorPage:{label:`$t:${ce}.show_error_page.label`,params:[{key:"target",label:`$t:${ce}.show_error_page.param_target`,widget:"select",options:[{value:"content"},{value:"full"}]}],build:{handler:"showErrorPage",params:{target:"{{target}}"}}}}}}function Qa(e,t){return typeof e.label=="string"?e.label.startsWith("$t:")?t(e.label.slice(3)):e.label:String(e.value)}function vi(e){return Array.isArray(e.options)?e.options:[]}function ED({control:e,value:t,onChange:n,t:o}){const r=vi(e);return i.jsx("div",{className:"g7le-widget g7le-widget--segmented","data-testid":"g7le-widget-segmented",style:WD,children:r.map(s=>{const a=s.value===t;return i.jsx("button",{type:"button","data-testid":`g7le-segment-${String(s.value)}`,"data-active":a?"true":"false",onClick:()=>n(a?void 0:s.value),style:{...GD,background:a?"#2563eb":"#fff",color:a?"#fff":"#0f172a",borderColor:a?"#2563eb":"#cbd5e1"},children:Qa(s,o)},String(s.value))})})}function ID({control:e,value:t,onChange:n,t:o}){const r=Array.isArray(e.scale)?e.scale:[],s=r.findIndex(l=>l===t),a=t!=null;return i.jsxs("div",{className:"g7le-widget g7le-widget--slider","data-testid":"g7le-widget-slider",style:Dg,children:[i.jsxs("label",{style:Ra,children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-slider-enabled",checked:a,onChange:l=>{l.target.checked?n(r.length>0?r[Math.max(0,s===-1?0:s)]:void 0):n(void 0)}}),i.jsx("span",{style:{fontSize:12,color:"#64748b"},children:o("layout_editor.control.use")})]}),i.jsx("input",{type:"range","data-testid":"g7le-slider-range",min:0,max:Math.max(0,r.length-1),step:1,disabled:!a||r.length===0,value:s===-1?0:s,onChange:l=>{const d=Number(l.target.value);n(r[d])},style:{flex:1}}),i.jsx("span",{"data-testid":"g7le-slider-value",style:{fontSize:12,color:"#0f172a",minWidth:48,textAlign:"right"},children:a?String(t):o("layout_editor.control.default")})]})}function AD({control:e,value:t,onChange:n,t:o}){const r=vi(e);return i.jsxs("select",{className:"g7le-widget g7le-widget--select","data-testid":"g7le-widget-select",value:t==null?"":String(t),onChange:s=>{const a=s.target.value;if(a===""){n(void 0);return}const l=r.find(d=>String(d.value)===a);n(l?l.value:a)},style:YD,children:[i.jsx("option",{value:"",children:o("layout_editor.control.default")}),r.map(s=>i.jsx("option",{value:String(s.value),children:Qa(s,o)},String(s.value)))]})}function TD({control:e,value:t,onChange:n}){const o=vi(e),r=o[1]?.value??!0,s=o[0]?.value??void 0,a=t===r;return i.jsx("button",{type:"button",className:"g7le-widget g7le-widget--toggle","data-testid":"g7le-widget-toggle",role:"switch","aria-checked":a,onClick:()=>n(a?s:r),style:{...XD,background:a?"#2563eb":"#cbd5e1"},children:i.jsx("span",{style:{...JD,transform:a?"translateX(18px)":"translateX(0)"}})})}function DD({control:e,value:t,onChange:n,t:o,freeValueDisabled:r}){const s=vi(e),a=t==null?"":String(t),[l,d]=u.useState(a);u.useEffect(()=>{d(a)},[a]);const c=p=>{const f=p.trim();if(f===""){n(void 0);return}n(f)};return i.jsxs("div",{className:"g7le-widget g7le-widget--dimension","data-testid":"g7le-widget-dimension",style:jD,children:[r?i.jsx("div",{"data-testid":"g7le-dimension-free-disabled",style:UD,children:o("layout_editor.property_modal.dark_preset_only")}):i.jsxs("div",{style:HD,children:[i.jsx("input",{type:"text","data-testid":"g7le-dimension-input",value:l,placeholder:o("layout_editor.control.dimension.placeholder"),onChange:p=>d(p.target.value),onBlur:p=>c(p.target.value),onKeyDown:p=>{p.key==="Enter"&&(p.preventDefault(),c(p.target.value))},style:FD}),i.jsx("button",{type:"button","data-testid":"g7le-dimension-clear",title:o("layout_editor.control.default"),onClick:()=>{d(""),n(void 0)},style:KD,children:o("layout_editor.control.default")})]}),s.length>0&&i.jsx("div",{style:VD,"data-testid":"g7le-dimension-chips",children:s.map(p=>{const f=String(p.value),y=f===a;return i.jsx("button",{type:"button","data-testid":`g7le-dimension-chip-${f}`,"data-active":y?"true":"false",onClick:()=>{d(f),n(f)},style:{...qD,background:y?"#2563eb":"#fff",color:y?"#fff":"#0f172a",borderColor:y?"#2563eb":"#cbd5e1"},children:Qa(p,o)},f)})})]})}function ND(e){return[{key:"t",infix:`${e}t-`,labelKey:"layout_editor.control.spacing.top"},{key:"r",infix:`${e}r-`,labelKey:"layout_editor.control.spacing.right"},{key:"b",infix:`${e}b-`,labelKey:"layout_editor.control.spacing.bottom"},{key:"l",infix:`${e}l-`,labelKey:"layout_editor.control.spacing.left"}]}function zD(e,t,n,o){const r={all:-1,sides:{}};if(typeof e!="string"||e.trim()==="")return r;const s=e.split(/\s+/).filter(a=>a.length>0);for(const a of s){const l=n.find(d=>a.startsWith(d.infix));if(l){r.sides[l.key]=o.indexOf(a.slice(l.infix.length));continue}if(a.startsWith(`${t}x-`)){const d=o.indexOf(a.slice(`${t}x-`.length));r.sides.l=d,r.sides.r=d;continue}if(a.startsWith(`${t}y-`)){const d=o.indexOf(a.slice(`${t}y-`.length));r.sides.t=d,r.sides.b=d;continue}a.startsWith(`${t}-`)&&(r.all=o.indexOf(a.slice(`${t}-`.length)))}return r}function MD({control:e,value:t,onChange:n,t:o}){const r=typeof e.spacingPrefix=="string"?e.spacingPrefix:"p",s=(Array.isArray(e.scale)?e.scale:[]).map(b=>String(b)),a=ND(r),l=zD(t,r,a,s),d=Object.values(l.sides).some(b=>b>=0),c=l.all>=0||d,[p,f]=u.useState(d);u.useEffect(()=>{d&&f(!0)},[d]);const y=Math.min(4,Math.max(0,s.length-1)),g=b=>{const x=[];for(const k of a){const w=b[k.key];w!==void 0&&w>=0&&x.push(`${k.infix}${s[w]}`)}n(x.length>0?x.join(" "):void 0)},m=b=>{n(b>=0?`${r}-${s[Math.max(0,Math.min(b,s.length-1))]}`:void 0)},h=()=>({...l.sides}),v=(b,x,k,w,_)=>{const C=k>=0;return i.jsxs("div",{style:BD,"data-testid":b,children:[i.jsxs("label",{style:{...Ra,minWidth:64},children:[i.jsx("input",{type:"checkbox","data-testid":`${b}-enabled`,checked:C,onChange:E=>_(E.target.checked)}),i.jsx("span",{style:{fontSize:12,color:"#0f172a"},children:o(x)})]}),i.jsx("input",{type:"range","data-testid":`${b}-range`,min:0,max:Math.max(0,s.length-1),step:1,disabled:!C||s.length===0,value:k<0?0:k,onChange:E=>w(Number(E.target.value)),style:{flex:1}}),i.jsx("span",{"data-testid":`${b}-value`,style:{fontSize:12,color:C?"#0f172a":"#94a3b8",minWidth:28,textAlign:"right"},children:C?s[k]:"–"})]},b)};return i.jsxs("div",{className:"g7le-widget g7le-widget--spacing","data-testid":"g7le-widget-spacing",style:LD,children:[i.jsxs("div",{style:PD,children:[i.jsxs("div",{style:OD,"data-testid":"g7le-spacing-mode",children:[i.jsx("button",{type:"button","data-testid":"g7le-spacing-mode-all","data-active":p?"false":"true",onClick:()=>{f(!1),d&&n(void 0)},style:{...Tg,background:p?"#fff":"#2563eb",color:p?"#0f172a":"#fff",borderColor:p?"#cbd5e1":"#2563eb"},children:o("layout_editor.control.spacing.mode_all")}),i.jsx("button",{type:"button","data-testid":"g7le-spacing-mode-sides","data-active":p?"true":"false",onClick:()=>{if(f(!0),l.all>=0){const b={};for(const x of a)b[x.key]=l.all;g(b)}},style:{...Tg,background:p?"#2563eb":"#fff",color:p?"#fff":"#0f172a",borderColor:p?"#2563eb":"#cbd5e1"},children:o("layout_editor.control.spacing.mode_sides")})]}),i.jsx("span",{"data-testid":"g7le-spacing-value",style:{fontSize:11,color:"#64748b",minWidth:80,textAlign:"right"},children:c?String(t):o("layout_editor.control.default")})]}),p?i.jsx("div",{"data-testid":"g7le-spacing-sides",style:{display:"flex",flexDirection:"column",gap:4},children:a.map(b=>v(`g7le-spacing-side-${b.key}`,b.labelKey,l.sides[b.key]??-1,x=>{const k=h();k[b.key]=x,g(k)},x=>{const k=h();x?k[b.key]=l.sides[b.key]>=0?l.sides[b.key]:y:delete k[b.key],g(k)}))}):i.jsxs("div",{style:Dg,children:[i.jsxs("label",{style:{...Ra,minWidth:64},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-spacing-all-enabled",checked:l.all>=0,onChange:b=>m(b.target.checked?y:-1)}),i.jsx("span",{style:{fontSize:12,color:"#64748b"},children:o("layout_editor.control.spacing.all")})]}),i.jsx("input",{type:"range","data-testid":"g7le-spacing-all-range",min:0,max:Math.max(0,s.length-1),step:1,disabled:l.all<0||s.length===0,value:l.all<0?0:l.all,onChange:b=>m(Number(b.target.value)),style:{flex:1}}),i.jsx("span",{"data-testid":"g7le-spacing-all-value",style:{fontSize:12,color:l.all>=0?"#0f172a":"#94a3b8",minWidth:28,textAlign:"right"},children:l.all>=0?s[l.all]:"–"})]})]})}const LD={display:"flex",flexDirection:"column",gap:6,width:"100%"},PD={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},OD={display:"inline-flex",gap:0,border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},Tg={padding:"3px 10px",fontSize:11,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},BD={display:"flex",alignItems:"center",gap:8,width:"100%"},WD={display:"inline-flex",gap:0,border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},jD={display:"flex",flexDirection:"column",gap:6,width:"100%"},HD={display:"flex",gap:6,alignItems:"center"},FD={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:80},KD={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",whiteSpace:"nowrap"},VD={display:"flex",flexWrap:"wrap",gap:4},UD={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"2px 0"},qD={padding:"3px 9px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:999,background:"#fff",cursor:"pointer"},GD={padding:"4px 10px",fontSize:12,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},Dg={display:"flex",alignItems:"center",gap:8,width:"100%"},Ra={display:"inline-flex",alignItems:"center",gap:4},YD={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",minWidth:120},XD={position:"relative",width:38,height:20,borderRadius:10,border:"none",cursor:"pointer",padding:2,display:"inline-flex",alignItems:"center",transition:"background 120ms ease"},JD={display:"block",width:16,height:16,borderRadius:8,background:"#fff",transition:"transform 120ms ease"},ZD=["#0f172a","#334155","#64748b","#94a3b8","#e2e8f0","#ffffff","#dc2626","#ea580c","#d97706","#16a34a","#0891b2","#2563eb","#7c3aed","#db2777"],Ng=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function QD(e){return Array.isArray(e.options)?e.options.filter(t=>typeof t.value=="string").map(t=>({value:t.value,swatch:typeof t.swatch=="string"?t.swatch:void 0,label:typeof t.label=="string"?t.label:void 0})):[]}function RD({control:e,value:t,onChange:n,t:o,freeValueDisabled:r}){const s=QD(e),a=s.length>0,l=typeof t=="string"&&s.some(g=>g.value===t)?t:"",d=typeof t=="string"&&Ng.test(t)?t:"",[c,p]=u.useState(d);u.useEffect(()=>{p(d)},[d]);const f=g=>{const m=g.trim();if(m===""){n(void 0);return}const h=m.startsWith("#")?m:`#${m}`;Ng.test(h)&&n(h)},y=ZD;return i.jsxs("div",{className:"g7le-widget g7le-widget--color","data-testid":"g7le-widget-color",style:eN,children:[r?i.jsx("div",{"data-testid":"g7le-color-free-disabled",style:sN,children:o("layout_editor.property_modal.dark_preset_only")}):i.jsxs("div",{style:tN,children:[i.jsxs("label",{style:nN,children:[i.jsx("span",{"data-testid":"g7le-color-swatch",style:{...oN,background:d||"transparent",backgroundImage:d?void 0:"linear-gradient(45deg,#e2e8f0 25%,transparent 25%,transparent 75%,#e2e8f0 75%),linear-gradient(45deg,#e2e8f0 25%,#fff 25%,#fff 75%,#e2e8f0 75%)",backgroundSize:"8px 8px",backgroundPosition:"0 0, 4px 4px"}}),i.jsx("input",{type:"color","data-testid":"g7le-color-native",value:d||"#000000",onChange:g=>n(g.target.value),style:{width:0,height:0,opacity:0,position:"absolute"}})]}),i.jsx("input",{type:"text","data-testid":"g7le-color-hex",value:c,placeholder:o("layout_editor.control.color_hex_placeholder"),onChange:g=>p(g.target.value),onBlur:g=>f(g.target.value),onKeyDown:g=>{g.key==="Enter"&&f(g.target.value)},style:rN}),i.jsx("button",{type:"button","data-testid":"g7le-color-clear",onClick:()=>n(void 0),style:iN,children:o("layout_editor.control.default")})]}),a?i.jsx("div",{style:zg,"data-testid":"g7le-color-token-presets",children:s.map(g=>{const m=l===g.value,h=g.label?g.label.startsWith("$t:")?o(g.label.slice(3)):g.label:g.value;return i.jsx("button",{type:"button","aria-label":h,title:h,"data-testid":`g7le-color-token-${g.value}`,"data-active":m?"true":"false",onClick:()=>n(m?void 0:g.value),style:{...Mg,background:g.swatch||"#e2e8f0",outline:m?"2px solid #2563eb":"none"}},g.value)})}):!r&&i.jsx("div",{style:zg,"data-testid":"g7le-color-presets",children:y.map(g=>i.jsx("button",{type:"button","aria-label":g,"data-testid":`g7le-color-preset-${g}`,onClick:()=>n(g),style:{...Mg,background:g,outline:d.toLowerCase()===g.toLowerCase()?"2px solid #2563eb":"none"}},g))})]})}const eN={display:"flex",flexDirection:"column",gap:8},tN={display:"flex",alignItems:"center",gap:6},nN={position:"relative",display:"inline-flex",cursor:"pointer"},oN={width:24,height:24,borderRadius:4,border:"1px solid #cbd5e1",display:"inline-block"},rN={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,fontFamily:"monospace"},iN={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",color:"#64748b"},zg={display:"grid",gridTemplateColumns:"repeat(7, 1fr)",gap:4},Mg={width:"100%",height:18,borderRadius:4,border:"1px solid #cbd5e1",cursor:"pointer",padding:0},sN={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"2px 0"},Qo=[{value:"fill",labelKey:"layout_editor.control.background_image.mode_fill",size:"cover",repeat:"no-repeat"},{value:"fit",labelKey:"layout_editor.control.background_image.mode_fit",size:"contain",repeat:"no-repeat"},{value:"tile",labelKey:"layout_editor.control.background_image.mode_tile",size:"auto",repeat:"repeat"}];function aN(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}function Lg(e){return Qo.find(n=>n.size===e.size&&n.repeat===e.repeat)?.value??"fill"}function lN({value:e,onChange:t,t:n}){const o=aN(e),{state:r}=Je(),s=yo(),[a,l]=u.useState(!1),[d,c]=u.useState(null),[p,f]=u.useState(o.url??""),[y,g]=u.useState([]),m=u.useRef(null),h=r.templateIdentifier,v=r.selectedRoute?.layoutName??"";u.useEffect(()=>{f(o.url??"")},[o.url]);const b=u.useCallback(async()=>{if(!h||!v){g([]);return}const E=await Dl(h,v);E.ok&&g(E.data)},[h,v]);u.useEffect(()=>{b()},[b]);const x=E=>{if(!E){t(void 0);return}const S=Qo.find(T=>T.value===Lg(o))??Qo[0];t({url:E,size:o.size??S.size,repeat:o.repeat??S.repeat,position:o.position??"center"})},k=E=>{const S=Qo.find(T=>T.value===E);!S||!o.url||t({url:o.url,size:S.size,repeat:S.repeat,position:o.position??"center"})},w=async E=>{const S=E.target.files?.[0];if(!S)return;l(!0),c(null);const T=await Nl(h,v,S);T.ok?(x(T.data.url),b()):c(n("layout_editor.control.background_image.upload_failed")),l(!1),m.current&&(m.current.value="")},_=async E=>{(await zl(E.id)).ok&&g(T=>T.filter($=>$.id!==E.id))},C=()=>{const E=s.open({ariaLabel:n("layout_editor.attachment_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(Ml,{templateIdentifier:h,layoutName:v,t:n,onSelect:S=>{x(S),s.close(E)},onChanged:()=>{b()},onClose:()=>s.close(E)})})};return i.jsxs("div",{className:"g7le-widget g7le-widget--image","data-testid":"g7le-widget-image",style:dN,children:[o.url&&i.jsx("div",{"data-testid":"g7le-image-preview",style:{...cN,backgroundImage:`url(${o.url})`,backgroundSize:o.size??"cover",backgroundRepeat:o.repeat??"no-repeat",backgroundPosition:o.position??"center"}}),i.jsxs("div",{style:uN,children:[i.jsxs("label",{style:pN,children:[a?"…":n("layout_editor.control.background_image.upload"),i.jsx("input",{ref:m,type:"file",accept:"image/*","data-testid":"g7le-image-file",onChange:w,disabled:a,style:{display:"none"}})]}),i.jsx("button",{type:"button","data-testid":"g7le-image-clear",onClick:()=>t(void 0),style:fN,children:n("layout_editor.control.background_image.clear")})]}),i.jsx("input",{type:"text","data-testid":"g7le-image-url",placeholder:"https://… / URL",value:p,onChange:E=>f(E.target.value),onBlur:E=>x(E.target.value.trim()||void 0),style:gN}),i.jsx("div",{style:hN,"data-testid":"g7le-image-modes",children:Qo.map(E=>{const S=Lg(o)===E.value;return i.jsx("button",{type:"button","data-testid":`g7le-image-mode-${E.value}`,"data-active":S?"true":"false",disabled:!o.url,onClick:()=>k(E.value),style:{...yN,background:S&&o.url?"#2563eb":"#fff",color:S&&o.url?"#fff":"#0f172a"},children:n(E.labelKey)},E.value)})}),i.jsxs("div",{style:bN,"data-testid":"g7le-image-gallery",children:[i.jsxs("div",{style:mN,children:[i.jsx("span",{style:{fontSize:11,color:"#64748b"},children:n("layout_editor.attachment_manager.recent")}),i.jsxs("button",{type:"button","data-testid":"g7le-image-manage",onClick:C,style:vN,children:["🖼 ",n("layout_editor.attachment_manager.manage_link")]})]}),y.length>0?i.jsx("div",{style:_N,"data-testid":"g7le-image-thumbs",children:y.slice(0,8).map(E=>i.jsxs("div",{style:xN,"data-testid":`g7le-image-thumb-${E.id}`,children:[i.jsx("button",{type:"button",title:E.original_name,onClick:()=>x(E.url),style:{...wN,backgroundImage:`url(${E.url})`},"data-testid":`g7le-image-thumb-use-${E.id}`}),i.jsx("button",{type:"button",title:n("layout_editor.attachment_manager.delete"),onClick:()=>_(E),style:kN,"data-testid":`g7le-image-thumb-del-${E.id}`,children:"✕"})]},E.id))}):i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:n("layout_editor.attachment_manager.empty")})]}),d&&i.jsx("span",{"data-testid":"g7le-image-error",style:{fontSize:11,color:"#dc2626"},children:d})]})}const dN={display:"flex",flexDirection:"column",gap:6},cN={width:"100%",height:80,borderRadius:6,border:"1px solid #cbd5e1"},uN={display:"flex",gap:6},pN={padding:"5px 10px",fontSize:12,border:"1px solid #2563eb",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",display:"inline-flex",alignItems:"center"},fN={padding:"5px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},gN={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},hN={display:"inline-flex",border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},yN={padding:"4px 10px",fontSize:12,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},bN={display:"flex",flexDirection:"column",gap:4,border:"1px solid #e2e8f0",borderRadius:6,padding:6,marginTop:2},mN={display:"flex",alignItems:"center",justifyContent:"space-between"},vN={fontSize:11,color:"#2563eb",background:"transparent",border:"none",cursor:"pointer",padding:0},_N={display:"flex",gap:6,flexWrap:"wrap"},xN={position:"relative",width:44,height:44},wN={width:44,height:44,borderRadius:6,border:"1px solid #cbd5e1",backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",padding:0},kN={position:"absolute",top:-6,right:-6,width:16,height:16,borderRadius:"50%",border:"none",background:"#dc2626",color:"#fff",fontSize:9,lineHeight:"16px",cursor:"pointer",padding:0};function SN(e){return Array.isArray(e)&&e.every(t=>t!==null&&typeof t=="object"&&!Array.isArray(t))}function CN(e){return typeof e=="string"&&/\{\{.*\}\}/.test(e)}function $N({value:e,onChange:t,t:n,bindingCandidates:o}){if(CN(e))return i.jsx("div",{className:"g7le-widget g7le-widget--options-list","data-testid":"g7le-widget-options-list-bound",style:MN,children:n("layout_editor.list_editor.bound_degraded")});const r=SN(e)?e:[],s=y=>{t(y.length===0?void 0:y)},a=(y,g)=>{const m=r.map((h,v)=>v===y?{...h,...g}:h);s(m)},l=y=>{s(r.filter((g,m)=>m!==y))},d=(y,g)=>{const m=y+g;if(m<0||m>=r.length)return;const h=[...r];[h[y],h[m]]=[h[m],h[y]],s(h)},c=()=>{s([...r,{value:"",label:""}])},p=y=>typeof y.label=="string"?y.label:y.label==null?"":String(y.label),f=y=>y.value===void 0||y.value===null?"":String(y.value);return i.jsxs("div",{className:"g7le-widget g7le-widget--options-list","data-testid":"g7le-widget-options-list",style:EN,children:[r.length===0&&i.jsx("div",{"data-testid":"g7le-options-empty",style:zN,children:n("layout_editor.list_editor.empty")}),r.map((y,g)=>i.jsxs("div",{"data-testid":`g7le-options-row-${g}`,style:IN,children:[i.jsxs("div",{style:Pg,children:[i.jsx("span",{style:Og,children:n("layout_editor.list_editor.option_value")}),i.jsx("input",{type:"text","data-testid":`g7le-options-value-${g}`,value:f(y),placeholder:n("layout_editor.list_editor.option_value"),onChange:m=>a(g,{value:m.target.value}),style:TN})]}),i.jsxs("div",{style:Pg,children:[i.jsx("span",{style:Og,children:n("layout_editor.list_editor.option_label")}),i.jsx("div",{"data-testid":`g7le-options-label-${g}`,style:{flex:1,minWidth:0},children:i.jsx(vt,{value:p(y),onChange:m=>a(g,{label:m??""}),t:n,placeholder:n("layout_editor.list_editor.option_label"),testidPrefix:`g7le-options-label-i18n-${g}`,candidates:o,enableExpressionTree:!0,expressionTreeCollapsible:!0})})]}),i.jsxs("div",{style:AN,children:[i.jsx("button",{type:"button","data-testid":`g7le-options-up-${g}`,title:n("layout_editor.list_editor.move_up"),disabled:g===0,onClick:()=>d(g,-1),style:Bg,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-options-down-${g}`,title:n("layout_editor.list_editor.move_down"),disabled:g===r.length-1,onClick:()=>d(g,1),style:Bg,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-options-remove-${g}`,title:n("layout_editor.list_editor.remove"),onClick:()=>l(g),style:DN,children:"✕"})]})]},g)),i.jsx("button",{type:"button","data-testid":"g7le-options-add",onClick:c,style:NN,children:n("layout_editor.list_editor.add")})]})}const EN={display:"flex",flexDirection:"column",gap:8,width:"100%"},IN={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0,paddingBottom:6,borderBottom:"1px solid #f1f5f9"},Pg={display:"flex",gap:6,alignItems:"center",width:"100%",minWidth:0},Og={flexShrink:0,width:36,fontSize:11,color:"#94a3b8",textAlign:"right"},AN={display:"flex",gap:4,alignItems:"center",justifyContent:"flex-end"},TN={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Bg={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},DN={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},NN={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},zN={fontSize:11,color:"#94a3b8",fontStyle:"italic"},MN={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"},Wg=/[^A-Za-z0-9\-_:.]/g;function LN(e){return typeof e=="string"&&Ui(e)}function jg(e){return Ui(e)?In(e).map(t=>t.kind==="binding"?t.raw:t.raw.replace(Wg,"")).join(""):e.replace(Wg,"")}const PN={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4,fontFamily:"monospace"},ON={marginTop:4,fontSize:11,color:"#94a3b8"},BN={display:"flex",gap:6,alignItems:"center"},WN={flexShrink:0,padding:"5px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:4,background:"#f8fafc",cursor:"pointer",whiteSpace:"nowrap"};function jN({value:e,onChange:t,t:n,bindingCandidates:o}){const r=e==null?"":String(e),s=LN(e),a=!!o&&o.length>0,[l,d]=u.useState(!1),c=p=>{const f=jg(p);t(f===""?void 0:f)};return s||l?i.jsxs("div",{"data-testid":"g7le-core-id-chip",children:[i.jsx(bs,{value:r,onChange:c,t:n,candidates:o,onDone:()=>d(!1),testidPrefix:"g7le-core-id-chip-input"}),i.jsx("div",{style:ON,children:n("layout_editor.core_props.id.binding_hint")})]}):i.jsx("div",{children:i.jsxs("div",{style:BN,children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-id","data-testid":"g7le-widget-core-id",value:r,onChange:p=>{const f=jg(p.target.value);t(f===""?void 0:f)},spellCheck:!1,autoComplete:"off",style:PN}),a&&i.jsxs("button",{type:"button","data-testid":"g7le-core-id-add-data",onClick:()=>d(!0),style:WN,title:n("layout_editor.core_props.id.add_data"),children:["🔗 ",n("layout_editor.core_props.id.add_data")]})]})})}const HN=/[^A-Za-z0-9.\-_]/g;function FN(e){return typeof e=="string"&&/\{\{[\s\S]*?\}\}/.test(e)}function KN(e){return e.replace(HN,"")}const Hg={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4,fontFamily:"monospace",boxSizing:"border-box"},VN={...Hg,background:"#f1f5f9",color:"#64748b",cursor:"not-allowed"},Fg={marginTop:4,fontSize:11,color:"#94a3b8"};function UN({value:e,onChange:t,t:n}){const o=e==null?"":String(e);return FN(e)?i.jsxs("div",{"data-testid":"g7le-core-datakey-bound",children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-datakey-bound","data-testid":"g7le-widget-core-datakey",value:o,readOnly:!0,disabled:!0,style:VN}),i.jsx("div",{style:Fg,children:n("layout_editor.core_props.dataKey.bound_degraded")})]}):i.jsxs("div",{children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-datakey","data-testid":"g7le-widget-core-datakey",value:o,placeholder:n("layout_editor.core_props.dataKey.placeholder"),onChange:s=>{const a=KN(s.target.value);t(a===""?void 0:a)},spellCheck:!1,autoComplete:"off",style:Hg}),i.jsx("div",{style:Fg,children:n("layout_editor.core_props.dataKey.hint")})]})}let Kg=!1;function qN(){Kg||(Xe("segmented",ED),Xe("slider",ID),Xe("select",AD),Xe("dimension",DD),Xe("spacing",MD),Xe("toggle",TD),Xe("color",RD),Xe("image",lN),Xe("tag-input",ys),Xe("page-picker",Aw),Xe("datasource-picker",Bc),Xe("binding-picker",Bc),Xe("binding-list",Nw),Xe("state-key-picker",Es),Xe("modal-picker",Tw),Xe("i18n-text",Dw),Xe("text",zw),Xe("options-list",$N),Xe("core-id",jN),Xe("core-datakey",UN),Xe("component-target-picker",jT),Kg=!0)}function Ro(e){return Array.isArray(e.children)?e.children:[]}function er(e){if(typeof e.text=="string")return[];let t=null;const n=(o,r)=>{const s=Ro(o);for(let a=0;a<s.length;a++){const l=s[a],d=[...r,a];if(typeof l.text=="string")if(l.text.trim().length<=1)t===null&&(t=d);else return d;const c=n(l,d);if(c)return c}return null};return n(e,[])??t}function _i(e,t){let n=e;for(const o of t){if(!n)return null;n=Ro(n)[o]??null}return n}function tr(e,t,n){if(t.length===0)return{...e,text:n};const[o,...r]=t,a=Ro(e).map((l,d)=>d===o?tr(l,r,n):l);return{...e,children:a}}function Vg(e,t){const n=r=>typeof r.props?.[t]=="string";if(n(e))return[];const o=(r,s)=>{const a=Ro(r);for(let l=0;l<a.length;l++){const d=a[l],c=[...s,l];if(n(d))return c;const p=o(d,c);if(p)return p}return null};return o(e,[])}function Ug(e,t,n,o){if(t.length===0)return{...e,props:{...e.props??{},[n]:o}};const[r,...s]=t,l=Ro(e).map((d,c)=>c===r?Ug(d,s,n,o):d);return{...e,children:l}}function GN(e){return Array.isArray(e.children)?e.children:[]}function YN(e){const t=e?.itemFields;if(!Array.isArray(t))return[{kind:"text"}];const n=[];for(const o of t){if(!o||typeof o!="object")continue;const r=o;r.kind==="text"?n.push({kind:"text",label:typeof r.label=="string"?r.label:void 0}):r.kind==="prop"&&typeof r.prop=="string"&&r.prop.length>0&&n.push({kind:"prop",prop:r.prop,label:typeof r.label=="string"?r.label:void 0})}return n.length>0?n:[{kind:"text"}]}function el(e,t){if(!(typeof e!="string"||e.length===0))return e.startsWith("$t:")?t(e.slice(3)):e}function qg({node:e,params:t,spec:n,manifest:o,t:r,onPatchNode:s,templateIdentifier:a,candidates:l}){const d=typeof t?.childComponent=="string"?t.childComponent:null,c=t?.childTemplate&&typeof t.childTemplate=="object"?t.childTemplate:null,p=YN(t),f=el(typeof t?.childLabel=="string"?t.childLabel:void 0,r)??d??"",y=GN(e),g=u.useCallback(_=>{s({...e,children:_})},[e,s]),m=u.useCallback(()=>{if(c)return JSON.parse(JSON.stringify(c));if(!d)return null;const _=n?.componentPalette?.entries?.[d];if(_?.defaultNode&&typeof _.defaultNode=="object")return JSON.parse(JSON.stringify(_.defaultNode));const C=o?.components??{};let E=null;for(const S of Object.values(C))if(Array.isArray(S)){const T=S.find($=>$.name===d);if(T){E=T;break}}return E?$f(E):{type:"basic",name:d}},[c,d,n,o]),h=u.useCallback(()=>{const _=m();if(_){if(_.text==="")_.text=r("layout_editor.list_editor.new_item");else{const C=er(_);if(C!==null&&C.length>0){const E=_i(_,C);if(E&&E.text===""){const S=tr(_,C,r("layout_editor.list_editor.new_item"));g([...y,S]);return}}}g([...y,_])}},[m,g,y,r]),v=u.useCallback(_=>{g(y.filter((C,E)=>E!==_))},[g,y]),b=u.useCallback((_,C)=>{const E=_+C;if(E<0||E>=y.length)return;const S=[...y];[S[_],S[E]]=[S[E],S[_]],g(S)},[g,y]),x=u.useCallback((_,C)=>{if(typeof C!="string"||C.length===0)return;const E=y[_];if(!E)return;const S=er(E);if(S===null)return;const T=y.map(($,A)=>A===_?tr($,S,C):$);g(T)},[y,g]),k=u.useCallback((_,C,E)=>{if(typeof E!="string"||E.length===0)return;const S=y[_];if(!S)return;const T=Vg(S,C);if(T===null)return;const $=y.map((A,z)=>z===_?Ug(A,T,C,E):A);g($)},[y,g]);if(!d&&!c)return i.jsx("div",{className:"g7le-node-editor g7le-node-editor--children","data-testid":"g7le-children-editor-misconfigured",style:Xg,children:r("layout_editor.list_editor.no_child_component")});const w=p.length>1||p.some(_=>_.label);return i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--children","data-testid":"g7le-children-editor",style:XN,children:[i.jsx("div",{style:JN,children:r("layout_editor.list_editor.items_title")}),y.length===0&&i.jsx("div",{"data-testid":"g7le-children-empty",style:Xg,children:r("layout_editor.list_editor.empty")}),y.map((_,C)=>{const E=[];for(const T of p)if(T.kind==="text"){const $=er(_),A=$!==null?_i(_,$):null;A&&typeof A.text=="string"&&E.push({key:"text",caption:el(T.label,r)??(w?r("layout_editor.list_editor.item_label"):void 0),value:A.text,onChange:z=>x(C,z),testid:`g7le-children-i18n-${C}`})}else if(T.kind==="prop"&&T.prop){const $=Vg(_,T.prop);if($!==null){const z=_i(_,$)?.props?.[T.prop];E.push({key:`prop-${T.prop}`,caption:el(T.label,r)??T.prop,value:typeof z=="string"?z:"",onChange:D=>k(C,T.prop,D),testid:`g7le-children-prop-${T.prop}-${C}`})}}const S=typeof _?.name=="string"?_.name:r("layout_editor.list_editor.item");return i.jsxs("div",{"data-testid":`g7le-children-row-${C}`,style:ZN,children:[E.length>0?E.map(T=>i.jsx("div",{style:Gg,children:i.jsxs("div",{"data-testid":T.key==="text"?`g7le-children-text-${C}`:`g7le-children-field-${T.key}-${C}`,style:QN,children:[T.caption!==void 0&&i.jsx("div",{"data-testid":`g7le-children-caption-${T.key}-${C}`,style:RN,children:T.caption}),i.jsx(vt,{value:T.value,onChange:T.onChange,t:r,placeholder:r("layout_editor.list_editor.item_text"),testidPrefix:T.testid,candidates:l,enableExpressionTree:!0,expressionTreeCollapsible:!0})]})},T.key)):i.jsx("div",{style:Gg,children:i.jsx("span",{"data-testid":`g7le-children-label-${C}`,style:t8,children:S})}),i.jsxs("div",{style:e8,children:[i.jsx("button",{type:"button","data-testid":`g7le-children-up-${C}`,title:r("layout_editor.list_editor.move_up"),disabled:C===0,onClick:()=>b(C,-1),style:Yg,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-children-down-${C}`,title:r("layout_editor.list_editor.move_down"),disabled:C===y.length-1,onClick:()=>b(C,1),style:Yg,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-children-remove-${C}`,title:r("layout_editor.list_editor.remove"),onClick:()=>v(C),style:n8,children:"✕"})]})]},C)}),i.jsx("button",{type:"button","data-testid":"g7le-children-add",onClick:h,style:o8,children:r("layout_editor.list_editor.add_child",{component:f})})]})}const XN={display:"flex",flexDirection:"column",gap:6,width:"100%",marginBottom:12},JN={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},ZN={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0,paddingBottom:6,borderBottom:"1px solid #f1f5f9"},Gg={display:"flex",gap:4,alignItems:"flex-start",width:"100%",minWidth:0},QN={flex:1,minWidth:0},RN={fontSize:11,color:"#64748b",marginBottom:2},e8={display:"flex",gap:4,alignItems:"center",justifyContent:"flex-end"},t8={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,color:"#475569",fontStyle:"italic",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},Yg={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},n8={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},o8={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Xg={fontSize:11,color:"#94a3b8",fontStyle:"italic"},fo={rowContainer:"Tbody",row:"Tr",cell:"Td",headerCell:"Th",colSpanProp:"colSpan",rowSpanProp:"rowSpan"};function ht(e){return{rowContainer:e?.rowContainer??fo.rowContainer,row:e?.row??fo.row,cell:e?.cell??fo.cell,headerCell:e?.headerCell??fo.headerCell,colSpanProp:e?.colSpanProp??fo.colSpanProp,rowSpanProp:e?.rowSpanProp??fo.rowSpanProp}}function Bn(e){return e?Array.isArray(e.children)?e.children:[]:[]}function Jg(e,t){const n=e.props?.[t],o=typeof n=="number"?n:typeof n=="string"?parseInt(n,10):NaN;return Number.isFinite(o)&&o>=1?Math.floor(o):1}function Be(e,t){const n=ht(t),o=[],r=Bn(e),s=[];r.forEach((f,y)=>{(Bn(f).some(h=>h.name===n.row)||f.name===n.rowContainer||f.name==="Thead"||f.name==="Tfoot")&&s.push({container:f,containerIndex:y,isHeader:f.name==="Thead"})}),s.length===0&&r.some(f=>f.name===n.row)&&s.push({container:e,containerIndex:-1,isHeader:!1});const a=[],l=[];let d=0,c=0;const p=f=>{for(;a.length<=f;)a.push([]);return a[f]};s.forEach((f,y)=>{Bn(f.container).filter(m=>m.name===n.row).forEach((m,h)=>{const v=[],b=p(d);let x=0;Bn(m).filter(w=>w.name===n.cell||w.name===n.headerCell).forEach((w,_)=>{for(;b[x];)x++;const C=Jg(w,n.colSpanProp),E=Jg(w,n.rowSpanProp),S={cell:w,sectionIndex:y,rowIndexInSection:h,cellIndexInRow:_,gridRow:d,gridCol:x,colSpan:C,rowSpan:E,isHeader:w.name===n.headerCell};v.push(S);for(let T=0;T<E;T++){const $=p(d+T);for(let A=0;A<C;A++)$[x+A]&&o.push(`overlap at row ${d+T}, col ${x+A}`),$[x+A]=!0}x+=C}),c=Math.max(c,x),l.push({cells:v,sectionIndex:y,rowIndexInSection:h,rowNode:m}),d++})});for(let f=0;f<d;f++){const y=a[f]??[];for(let g=0;g<c;g++)y[g]||o.push(`hole at row ${f}, col ${g}`)}return{sections:s,rows:l,colCount:c,valid:o.length===0,issues:o}}function tl(e,t,n){const o=ht(n),s=Be(e,n).sections[t.sectionIndex],a=[];let l;s&&s.containerIndex>=0?(a.push(s.containerIndex),l=Bn(e)[s.containerIndex]??e):l=e;let d=-1,c=-1,p=null;const f=Bn(l);for(let m=0;m<f.length;m++)if(f[m].name===o.row&&(d++,d===t.rowIndexInSection)){c=m,p=f[m];break}if(c<0||!p)return a;a.push(c);let y=-1;const g=Bn(p);for(let m=0;m<g.length;m++){const h=g[m].name;if((h===o.cell||h===o.headerCell)&&(y++,y===t.cellIndexInRow))return a.push(m),a}return a}function Ue(e,t,n){for(const o of e.rows)for(const r of o.cells)if(t>=r.gridRow&&t<r.gridRow+r.rowSpan&&n>=r.gridCol&&n<r.gridCol+r.colSpan)return r;return null}function Zg(e){if(e.colCount===0)return[];const t=new Set([0,e.colCount]);for(let r=1;r<e.colCount;r++){let s=!1;for(const a of e.rows){for(const l of a.cells)if(l.gridCol<r&&l.gridCol+l.colSpan>r){s=!0;break}if(s)break}s||t.add(r)}const n=Array.from(t).sort((r,s)=>r-s),o=[];for(let r=0;r<n.length-1;r++)o.push({start:n[r],end:n[r+1]});return o}function Qg(e){const t=e.rows.length;if(t===0)return[];const n=a=>e.rows.find(l=>(l.cells[0]?.gridRow??-1)===a)?.sectionIndex??e.rows[a]?.sectionIndex??0,o=new Set([0,t]);for(let a=1;a<t;a++){if(n(a)!==n(a-1)){o.add(a);continue}let l=!1;for(const d of e.rows){for(const c of d.cells)if(c.gridRow<a&&c.gridRow+c.rowSpan>a){l=!0;break}if(l)break}l||o.add(a)}const r=Array.from(o).sort((a,l)=>a-l),s=[];for(let a=0;a<r.length-1;a++)s.push({start:r[a],end:r[a+1],sectionIndex:n(r[a])});return s}function xi(e,t,n){const o=Qg(e),r=o.findIndex(a=>t>=a.start&&t<a.end);if(r<0)return{ok:!1,reason:"boundary"};const s=r+n;return s<0||s>=o.length?{ok:!1,reason:"boundary"}:o[r].sectionIndex!==o[s].sectionIndex?{ok:!1,reason:"section"}:{ok:!0}}function wi(e,t,n){const o=Zg(e),r=o.findIndex(a=>t>=a.start&&t<a.end);if(r<0)return{ok:!1,reason:"boundary"};const s=r+n;return s<0||s>=o.length?{ok:!1,reason:"boundary"}:{ok:!0}}function Ge(e){return e?Array.isArray(e.children)?e.children:[]:[]}function mn(e,t,n){const o={...e.props??{}};n<=1?delete o[t]:o[t]=n;const r={...e,props:o};return Object.keys(o).length===0&&delete r.props,r}function ki(e,t,n=!1){const o={type:"basic",name:n?e.headerCell:e.cell},r=t?.props?.className;return typeof r=="string"&&r&&(o.props={className:r}),o.text="",o}function nr(e,t){return e.name===t.cell||e.name===t.headerCell}function nt(e,t,n,o){const r=t.sections[n];if(!r)return e;if(r.containerIndex===-1)return o(e);const s=Ge(e).map((a,l)=>l===r.containerIndex?o(a):a);return{...e,children:s}}function yt(e,t,n,o){let r=-1;const s=Ge(e).map(a=>a.name===t&&(r++,r===n)?o(a):a);return{...e,children:s}}function Si(e,t,n){let o=-1;const r=Ge(e);for(let s=0;s<r.length;s++)if(nr(r[s],t)&&(o++,o===n))return s;return-1}function nl(e,t,n){const o=ht(t),r=Be(e,t);if(r.rows.length===0||r.colCount===0){const b={type:"basic",name:o.row,children:[ki(o)]};return r.sections.length>0?nt(e,r,r.sections.length-1,x=>({...x,children:[...Ge(x),b]})):{...e,children:[...Ge(e),b]}}const s=Math.max(-1,Math.min(n,r.rows.length-1)),a=s<0?r.rows[0]:r.rows[s],l=a.cells[0]?.gridRow??s,d=s<0?0:l+1,c=a.sectionIndex,p=s<0?0:a.rowIndexInSection+1,f=[],y=[];let g=0;for(;g<r.colCount;){const b=d>0?Ue(r,d-1,g):null;b&&b.gridRow+b.rowSpan>d&&b.rowSpan>1?(y.push(b),g+=b.colSpan):(f.push(ki(o)),g+=1)}let m=e;for(const b of y)m=nt(m,r,b.sectionIndex,x=>yt(x,o.row,b.rowIndexInSection,k=>({...k,children:Ge(k).map((w,_)=>_===b.cellIndexInRow?mn(w,o.rowSpanProp,b.rowSpan+1):w)})));const h={type:"basic",name:o.row,children:f},v=Be(m,t);return m=nt(m,v,c,b=>{const x=Ge(b);let k=0,w=x.length;for(let _=0;_<x.length;_++)if(x[_].name===o.row){if(k===p){w=_;break}k++}return{...b,children:[...x.slice(0,w),h,...x.slice(w)]}}),m}function Rg(e,t,n){const o=ht(t),r=Be(e,t);if(n<0||n>=r.rows.length||r.rows.length<=1)return e;const s=r.rows[n],a=s.cells[0]?.gridRow??n;let l=e;const d=new Set;for(let p=0;p<r.colCount;){const f=Ue(r,a,p);f&&f.gridRow<a&&!d.has(f.cell)&&(d.add(f.cell),l=nt(l,r,f.sectionIndex,y=>yt(y,o.row,f.rowIndexInSection,g=>({...g,children:Ge(g).map((m,h)=>h===f.cellIndexInRow?mn(m,o.rowSpanProp,f.rowSpan-1):m)})))),p+=f?f.colSpan:1}const c=Be(l,t);return l=nt(l,c,s.sectionIndex,p=>{let f=-1;const y=Ge(p).filter(g=>g.name===o.row?(f++,f!==s.rowIndexInSection):!0);return{...p,children:y}}),l}function eh(e,t,n,o){const r=ht(t),s=Be(e,t);if(!xi(s,n,o).ok)return e;const a=Qg(s),l=a.findIndex(h=>n>=h.start&&n<h.end),d=l+o,c=a[l],p=a[d],f=c.sectionIndex,y=h=>s.rows.filter(v=>{const b=v.cells[0]?.gridRow??-1;return v.sectionIndex===f&&b>=h.start&&b<h.end}).map(v=>v.rowIndexInSection),g=y(c),m=y(p);return g.length===0||m.length===0?e:nt(e,s,f,h=>{const v=[...Ge(h)],b=[];v.forEach((M,B)=>{M.name===r.row&&b.push(B)});const x=g.map(M=>b[M]).filter(M=>M!==void 0),k=m.map(M=>b[M]).filter(M=>M!==void 0);if(x.length===0||k.length===0)return h;const w=x.map(M=>v[M]),_=k.map(M=>v[M]),C=Math.min(...x)<Math.min(...k),E=C?x:k,S=C?w:_,T=C?k:x,$=C?_:w,A=Math.min(...E),z=Math.max(...T),D=[...$,...S],O=[...v.slice(0,A),...D,...v.slice(z+1)];return{...h,children:O}})}function th(e,t,n,o){const r=ht(t),s=Be(e,t);if(!wi(s,n,o).ok)return e;const a=Zg(s),l=a.findIndex(m=>n>=m.start&&n<m.end),d=l+o,c=a[l],p=a[d],f=c.start<p.start?c:p,y=c.start<p.start?p:c;let g=e;for(const m of s.rows){const h=m.cells.filter(w=>w.gridCol>=f.start&&w.gridCol<f.end),v=m.cells.filter(w=>w.gridCol>=y.start&&w.gridCol<y.end);if(h.length===0||v.length===0)continue;const b=h.map(w=>w.cellIndexInRow),x=v.map(w=>w.cellIndexInRow),k=Be(g,t);g=nt(g,k,m.sectionIndex,w=>yt(w,r.row,m.rowIndexInSection,_=>{const C=[...Ge(_)],E=b.map(M=>Si(_,r,M)).filter(M=>M>=0),S=x.map(M=>Si(_,r,M)).filter(M=>M>=0);if(E.length===0||S.length===0)return _;const T=E.map(M=>C[M]),$=S.map(M=>C[M]),A=Math.min(...E,...S),z=Math.max(...E,...S),D=[...$,...T],O=[...C.slice(0,A),...D,...C.slice(z+1)];return{..._,children:O}}))}return g}function nh(e,t,n){const o=ht(t),r=Be(e,t);if(r.rows.length===0||r.colCount===0)return nl(e,t,-1);const s=Math.max(0,Math.min(n+1,r.colCount));let a=e;const l=new Set;for(const d of r.rows){const c=d.cells[0]?.gridRow??0,p=s>0?Ue(r,c,s-1):null,f=s<r.colCount?Ue(r,c,s):null;if(p&&f&&p.cell===f.cell&&p.colSpan>1){if(!l.has(p.cell)){l.add(p.cell);const h=Be(a,t);a=nt(a,h,p.sectionIndex,v=>yt(v,o.row,p.rowIndexInSection,b=>({...b,children:Ge(b).map((x,k)=>k===p.cellIndexInRow?mn(x,o.colSpanProp,p.colSpan+1):x)})))}continue}if(p&&p.gridRow<c||f&&f.gridRow<c)continue;const y=p&&p.gridRow===c?p.cellIndexInRow:-1,g=d.cells.length>0&&d.cells.every(h=>h.isHeader),m=Be(a,t);a=nt(a,m,d.sectionIndex,h=>yt(h,o.row,d.rowIndexInSection,v=>{const b=[...Ge(v)];let x;if(y<0)x=b.findIndex(k=>nr(k,o)),x<0&&(x=b.length);else{const k=Si(v,o,y);x=k<0?b.length:k+1}return b.splice(x,0,ki(o,void 0,g)),{...v,children:b}}))}return a}function oh(e,t,n){const o=ht(t),r=Be(e,t);if(n<0||n>=r.colCount||r.colCount<=1)return e;let s=e;const a=new Set;for(const l of r.rows){const d=l.cells[0]?.gridRow??0,c=Ue(r,d,n);if(!(!c||a.has(c.cell)||c.gridRow<d))if(a.add(c.cell),c.colSpan>1){const p=Be(s,t);s=nt(s,p,c.sectionIndex,f=>yt(f,o.row,c.rowIndexInSection,y=>({...y,children:Ge(y).map((g,m)=>m===c.cellIndexInRow?mn(g,o.colSpanProp,c.colSpan-1):g)})))}else{const p=Be(s,t);s=nt(s,p,c.sectionIndex,f=>yt(f,o.row,c.rowIndexInSection,y=>{let g=-1;const m=Ge(y).filter(h=>nr(h,o)?(g++,g!==c.cellIndexInRow):!0);return{...y,children:m}}))}}return s}function ol(e,t,n,o,r,s){const a=ht(t),l=Be(e,t),d=Math.min(n,r),c=Math.max(n,r),p=Math.min(o,s),f=Math.max(o,s);if(d===c&&p===f)return{table:e,ok:!1,reason:"single"};if(!l.valid)return{table:e,ok:!1,reason:"invalid-grid"};const y=new Set;for(let b=d;b<=c;b++)for(let x=p;x<=f;x++){const k=Ue(l,b,x);if(!k)return{table:e,ok:!1,reason:"hole"};if(k.gridRow<d||k.gridCol<p||k.gridRow+k.rowSpan-1>c||k.gridCol+k.colSpan-1>f)return{table:e,ok:!1,reason:"not-rectangular"};y.add(k)}const g=Ue(l,d,p),m=Array.from(y).filter(b=>b!==g);let h=nt(e,l,g.sectionIndex,b=>yt(b,a.row,g.rowIndexInSection,x=>({...x,children:Ge(x).map((k,w)=>{if(w!==g.cellIndexInRow)return k;let _=mn(k,a.colSpanProp,f-p+1);return _=mn(_,a.rowSpanProp,c-d+1),_})})));const v=new Map;for(const b of m){const x=`${b.sectionIndex}:${b.rowIndexInSection}`,k=v.get(x)??[];k.push(b),v.set(x,k)}for(const[b,x]of v){const k=parseInt(b.split(":")[0],10),w=x[0].rowIndexInSection,_=new Set(x.map(E=>E.cellIndexInRow)),C=Be(h,t);h=nt(h,C,k,E=>yt(E,a.row,w,S=>{let T=-1;const $=Ge(S).filter(A=>nr(A,a)?(T++,!_.has(T)):!0);return{...S,children:$}}))}return{table:h,ok:!0}}function rh(e,t,n,o){const r=ht(t),s=Be(e,t),a=Ue(s,n,o);if(!a||a.colSpan<=1&&a.rowSpan<=1)return e;const l=a.gridRow,d=a.gridCol,c=a.gridRow+a.rowSpan-1,p=a.gridCol+a.colSpan-1,f=a.isHeader,y=typeof a.cell.props?.className=="string"?a.cell.props.className:void 0;let g=nt(e,s,a.sectionIndex,m=>yt(m,r.row,a.rowIndexInSection,h=>({...h,children:Ge(h).map((v,b)=>{if(b!==a.cellIndexInRow)return v;let x=mn(v,r.colSpanProp,1);return x=mn(x,r.rowSpanProp,1),x})})));for(let m=l;m<=c;m++)for(let h=d;h<=p;h++){if(m===l&&h===d)continue;const v=Be(g,t),b=v.rows.find(w=>w.cells.some(_=>_.gridRow===m))??v.rows[m];if(!b)continue;let x=-1;for(const w of b.cells)w.gridRow===m&&w.gridCol<h&&(x=Math.max(x,w.cellIndexInRow));const k=y?{props:{className:y}}:null;g=nt(g,v,b.sectionIndex,w=>yt(w,r.row,b.rowIndexInSection,_=>{const C=[...Ge(_)];let E;if(x<0)E=C.findIndex(S=>nr(S,r)),E<0&&(E=C.length);else{const S=Si(_,r,x);E=S<0?C.length:S+1}return C.splice(E,0,ki(r,k,f)),{..._,children:C}}))}return g}function Ci(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>{if(f!==l.cellIndexInRow)return p;const y={...p.props??{}};r.trim()===""?delete y.className:y.className=r;const g={...p,props:y};return Object.keys(y).length===0&&delete g.props,g})}))):e}function Jt(e,t,n,o,r,s){const a=ht(t),l=Be(e,t),d=Ue(l,n,o);return d?nt(e,l,d.sectionIndex,c=>yt(c,a.row,d.rowIndexInSection,p=>({...p,children:Ge(p).map((f,y)=>{if(y!==d.cellIndexInRow)return f;const g={...f.props??{}},m={...g.style??{}};!s||s.trim()===""?delete m[r]:m[r]=s,Object.keys(m).length===0?delete g.style:g.style=m;const h={...f,props:g};return Object.keys(g).length===0&&delete h.props,h})}))):e}function go(e,t,n,o,r,s,a){const l=ht(t),d=Be(e,t),c=Ue(d,n,o);if(!c)return e;const p=a==="dark";return nt(e,d,c.sectionIndex,f=>yt(f,l.row,c.rowIndexInSection,y=>({...y,children:Ge(y).map((g,m)=>{if(m!==c.cellIndexInRow)return g;const h={...g.props??{}},v=typeof h.className=="string"?h.className:"",b=uC(v,s,r,p);b===""?delete h.className:h.className=b;const x={...g,props:h};return Object.keys(h).length===0&&delete x.props,x})})))}function ih(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>f===l.cellIndexInRow?{...p,text:r}:p)}))):e}function sh(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>f===l.cellIndexInRow?r:p)}))):e}const ah=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function r8(e){return e.split(/\s+/).filter(Boolean)}function lh(e,t){for(const n of t)if(e===n||/^-?\d+$/.test(e.slice(n.length+1))&&e.startsWith(n+"-"))return!0;return!1}function dh({className:e,catalog:t,t:n,onChange:o,colorScheme:r="light",onPresetToken:s,colorStyle:a,onColorStyle:l,disabled:d}){const c=t?.sides??[],p=t?.widths??[],f=t?.colors??[],y=r==="dark",g=u.useMemo(()=>c.map($=>$.prefix),[c]),m=u.useMemo(()=>new Set(f.map($=>$.token)),[f]),h=u.useMemo(()=>r8(e),[e]),v=u.useMemo(()=>Vr(e,y),[e,y]),b=u.useMemo(()=>v.find($=>m.has($))??null,[v,m]),x=u.useMemo(()=>{const $=new Set;for(const A of c)h.some(D=>D===A.prefix||D.startsWith(A.prefix+"-")&&/^-?\d+$/.test(D.slice(A.prefix.length+1)))&&$.add(A.value);return $},[h,c]),k=u.useMemo(()=>{const $=h.find(z=>lh(z,g));if(!$)return"none";const A=p.filter(z=>z.value!=="none");for(const z of c){if($===z.prefix)return A.find(D=>(D.suffix??"")==="")?.value??"thin";if($.startsWith(z.prefix+"-")){const D=$.slice(z.prefix.length),O=A.find(M=>(M.suffix??"")===D);if(O)return O.value}}return"thin"},[h,g,c,p]);if(!t||c.length===0||p.length===0)return null;function w($){return $.filter(A=>!lh(A,g))}function _($,A){const z=w(h),D=p.find(B=>B.value===A);if(!D||A==="none"||$.size===0)return z.join(" ").trim();const O=D.suffix??"",M=[...z];for(const B of c)$.has(B.value)&&M.push(O?`${B.prefix}${O}`:B.prefix);return M.join(" ").trim()}const C=$=>{const A=new Set(x);A.has($)?A.delete($):($==="all"?A.clear():A.delete("all"),A.add($));const z=k==="none"?p.find(D=>D.value!=="none")?.value??"thin":k;o(_(A,z))},E=$=>{const A=x.size===0?new Set(["all"]):x;o(_(A,$))},S=$=>{s?.($)},T=($,A)=>$&&$.startsWith("$t:")?n($.slice(3)):$||A;return i.jsxs("div",{className:"g7le-cell-border","data-testid":"g7le-cell-border",style:i8,children:[i.jsx("div",{style:s8,children:i.jsx("span",{style:a8,children:n("layout_editor.table_editor.cell_border")})}),i.jsx("div",{style:rl,"data-testid":"g7le-cell-border-widths",children:p.map($=>i.jsx("button",{type:"button",disabled:d,"data-testid":`g7le-cell-border-width-${$.value}`,"aria-pressed":k===$.value,onClick:()=>E($.value),style:k===$.value?ch:il,children:T($.label,$.value)},$.value))}),k!=="none"&&i.jsx("div",{style:rl,"data-testid":"g7le-cell-border-sides",children:c.map($=>i.jsx("button",{type:"button",disabled:d,"data-testid":`g7le-cell-border-side-${$.value}`,"aria-pressed":x.has($.value),onClick:()=>C($.value),style:x.has($.value)?ch:il,children:T($.label,$.value)},$.value))}),k!=="none"&&(f.length>0||l)&&i.jsx(i.Fragment,{children:i.jsxs("div",{style:rl,"data-testid":"g7le-cell-border-colors",children:[f.map($=>{const A=b===$.token;return i.jsx("button",{type:"button",disabled:d,title:T($.label,$.value),"data-testid":`g7le-cell-border-color-${$.value}`,"aria-pressed":A,onClick:()=>S($.token),style:{...uh,background:$.swatch??"#fff",outline:A?"2px solid #2563eb":"1px solid #cbd5e1"}},$.value)}),l&&(()=>{const $=!!a&&f.some(z=>z.swatch&&z.swatch.toLowerCase()===a.toLowerCase()),A=!y&&!!a&&!$;return i.jsx("label",{title:n(y?"layout_editor.table_editor.dark_free_color_hint":"layout_editor.table_inplace.custom_color"),"data-testid":"g7le-cell-border-color-custom",style:{...uh,position:"relative",overflow:"hidden",display:"inline-flex",alignItems:"center",justifyContent:"center",background:A&&ah.test(a)?a:"conic-gradient(red, yellow, lime, aqua, blue, magenta, red)",outline:A?"2px solid #2563eb":"1px solid #cbd5e1",opacity:y?.4:1,cursor:y?"not-allowed":"pointer"},children:i.jsx("input",{type:"color",disabled:d||y,"data-testid":"g7le-cell-border-color-picker",value:a&&ah.test(a)?a:"#000000",onChange:z=>l(z.target.value),style:{position:"absolute",width:0,height:0,opacity:0}})})})()]})})]})}const i8={display:"flex",flexDirection:"column",gap:6},s8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},a8={fontSize:12,fontWeight:600,color:"#475569"},rl={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},il={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},ch={...il,border:"1px solid #2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},uh={width:22,height:22,borderRadius:5,padding:0,cursor:"pointer",boxSizing:"border-box"},ph=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function fh({colorStyle:e,className:t,catalog:n,t:o,colorScheme:r="light",onPresetToken:s,onCustomColor:a,onClear:l,disabled:d}){const c=n?.colors??[],p=r==="dark",f=e??"",y=Vr(t??"",p),g=c.find(x=>x.token&&y.includes(x.token))?.token??null,m=(x,k)=>x&&x.startsWith("$t:")?o(x.slice(3)):x||k,h=!g&&(p||f.trim()===""),v=!!f&&c.some(x=>x.swatch&&x.swatch.toLowerCase()===f.toLowerCase()),b=!p&&!!f&&!v;return i.jsxs("div",{className:"g7le-cell-fill","data-testid":"g7le-cell-fill",style:l8,children:[i.jsx("div",{style:d8,children:i.jsx("span",{style:c8,children:o("layout_editor.table_editor.cell_fill")})}),i.jsxs("div",{style:u8,"data-testid":"g7le-cell-fill-colors",children:[i.jsx("button",{type:"button",disabled:d,title:o("layout_editor.table_editor.cell_fill_none"),"data-testid":"g7le-cell-fill-none","aria-pressed":h,onClick:()=>l?.(),style:{...sl,background:"repeating-conic-gradient(#e2e8f0 0% 25%, #ffffff 0% 50%) 50% / 10px 10px",outline:h?"2px solid #2563eb":"1px solid #cbd5e1"}}),c.map(x=>{const k=!!x.token&&g===x.token,w=!p&&!!x.swatch&&f.toLowerCase()===(x.swatch??"").toLowerCase(),_=k||w,C=d||p&&!x.token;return i.jsx("button",{type:"button",disabled:C,title:m(x.label,x.value),"data-testid":`g7le-cell-fill-color-${x.value}`,"aria-pressed":_,onClick:()=>{x.token&&s?s(x.token):!p&&x.swatch&&a&&a(x.swatch)},style:{...sl,background:x.swatch??"#fff",outline:_?"2px solid #2563eb":"1px solid #cbd5e1",opacity:C?.4:1,cursor:C?"not-allowed":"pointer"}},x.value)}),a&&i.jsx("label",{title:o(p?"layout_editor.table_editor.dark_free_color_hint":"layout_editor.table_editor.cell_fill_custom"),"data-testid":"g7le-cell-fill-custom",style:{...sl,position:"relative",overflow:"hidden",display:"inline-flex",alignItems:"center",justifyContent:"center",background:b&&ph.test(f)?f:"conic-gradient(red, yellow, lime, aqua, blue, magenta, red)",outline:b?"2px solid #2563eb":"1px solid #cbd5e1",opacity:p?.4:1,cursor:p?"not-allowed":"pointer"},children:i.jsx("input",{type:"color",disabled:d||p,"data-testid":"g7le-cell-fill-color-picker",value:f&&ph.test(f)?f:"#ffffff",onChange:x=>a(x.target.value),style:{position:"absolute",width:0,height:0,opacity:0}})})]})]})}const l8={display:"flex",flexDirection:"column",gap:6},d8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},c8={fontSize:12,fontWeight:600,color:"#475569"},u8={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},sl={width:22,height:22,borderRadius:5,padding:0,cursor:"pointer",boxSizing:"border-box"};function p8(e){const t=/^(\d+)px$/.exec(e.trim());return t?parseInt(t[1],10):null}function gh({paddingStyle:e,catalog:t,t:n,onChange:o,disabled:r}){const s=t?.steps??[],a=e??"",l=p8(a),d=(h,v)=>h&&h.startsWith("$t:")?n(h.slice(3)):h||v,c=s.some(h=>(h.px??0)===(l??-1)),p=l!==null&&!c?String(l):"",[f,y]=u.useState(p);u.useEffect(()=>{y(p)},[e]);const g=h=>{o(h&&h>0?`${h}px`:""),y("")},m=h=>{const v=parseInt(h,10);Number.isFinite(v)&&v>=0&&o(v>0?`${v}px`:"")};return i.jsxs("div",{className:"g7le-cell-padding","data-testid":"g7le-cell-padding",style:f8,children:[i.jsx("div",{style:g8,children:n("layout_editor.table_editor.cell_padding")}),i.jsxs("div",{style:h8,"data-testid":"g7le-cell-padding-steps",children:[s.map(h=>{const v=(h.px??0)===(l??-1)||!h.px&&a.trim()==="";return i.jsx("button",{type:"button",disabled:r,"data-testid":`g7le-cell-padding-step-${h.value}`,"aria-pressed":v,onClick:()=>g(h.px),style:v?y8:hh,children:d(h.label,h.value)},h.value)}),i.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:3},children:[i.jsx("input",{type:"number",min:0,disabled:r,"data-testid":"g7le-cell-padding-free",value:f,placeholder:n("layout_editor.table_editor.cell_padding_custom"),onChange:h=>y(h.target.value),onBlur:()=>f.trim()!==""&&m(f),onKeyDown:h=>{h.key==="Enter"&&f.trim()!==""&&m(f)},style:b8}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:"px"})]})]})]})}const f8={display:"flex",flexDirection:"column",gap:6},g8={fontSize:12,fontWeight:600,color:"#475569"},h8={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},hh={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},y8={...hh,border:"1px solid #2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},b8={width:48,padding:"3px 5px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"};function yh({colorScheme:e,onChange:t,t:n,disabled:o}){const r=e==="dark";return i.jsxs("div",{className:"g7le-color-scheme-tabs","data-testid":"g7le-color-scheme-tabs",style:m8,children:[i.jsxs("div",{style:v8,children:[i.jsx("span",{style:_8,children:n("layout_editor.table_editor.cell_color")}),i.jsxs("div",{style:x8,children:[i.jsx("button",{type:"button",disabled:o,"data-testid":"g7le-color-scheme-light","aria-pressed":!r,onClick:()=>t("light"),style:r?al:bh,children:n("layout_editor.property_modal.scope.scheme_light")}),i.jsx("button",{type:"button",disabled:o,"data-testid":"g7le-color-scheme-dark","aria-pressed":r,onClick:()=>t("dark"),style:r?bh:al,children:n("layout_editor.property_modal.scope.scheme_dark")})]})]}),r&&i.jsx("div",{style:w8,"data-testid":"g7le-color-scheme-dark-hint",children:n("layout_editor.table_editor.dark_free_color_hint")})]})}const m8={display:"flex",flexDirection:"column",gap:3},v8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},_8={fontSize:12,fontWeight:700,color:"#0f172a"},x8={display:"flex",gap:2},al={padding:"2px 9px",fontSize:11,border:"none",borderBottom:"2px solid transparent",background:"transparent",color:"#94a3b8",cursor:"pointer"},bh={...al,color:"#1d4ed8",borderBottom:"2px solid #2563eb",fontWeight:600},w8={fontSize:10,color:"#94a3b8",fontStyle:"italic"},ll="dark:";function mh(e){return typeof e=="string"?e.split(/\s+/).filter(Boolean):[]}function vh(e,t){return t==="dark"?e.filter(n=>n.startsWith(ll)).map(n=>n.slice(ll.length)):e.filter(n=>!n.startsWith(ll))}const k8={none:0,thin:1,medium:2,thick:4},_h={top:{w:"borderTopWidth",s:"borderTopStyle",c:"borderTopColor"},bottom:{w:"borderBottomWidth",s:"borderBottomStyle",c:"borderBottomColor"},left:{w:"borderLeftWidth",s:"borderLeftStyle",c:"borderLeftColor"},right:{w:"borderRightWidth",s:"borderRightStyle",c:"borderRightColor"}},xh=["top","bottom","left","right"];function S8(e,t,n="light"){if(!e||!t)return{};const o=t.sides??[],r=t.widths??[],s=t.colors??[],a=mh(e.props?.className);if(a.length===0&&!e.props?.style)return{};const l=[...r].filter(b=>b.value!=="none").sort((b,x)=>(x.suffix??"").length-(b.suffix??"").length),d={};for(const b of o)for(const x of l){const k=x.suffix?`${b.prefix}${x.suffix}`:b.prefix;if(a.includes(k)){d[b.value]=x.value;break}}const c=e.props?.style??{},p=vh(a,n),f=s.find(b=>p.includes(b.token))?.swatch??"",g=(n==="light"&&typeof c.borderColor=="string"?c.borderColor:"")||f||"#94a3b8",m=b=>{const x=_h[b]?.c,k=x?c[x]:void 0;return typeof k=="string"&&k?k:g},h={},v=(b,x)=>{const k=_h[b];if(!k)return;const w=k8[x]??1;h[k.w]=`${w}px`,h[k.s]=w>0?"solid":"none",w>0&&(h[k.c]=m(b))};if(d.all)for(const b of xh)v(b,d.all);for(const b of xh)d[b]&&v(b,d[b]);return h}function C8(e,t,n="light"){if(!e?.props)return{};const o=e.props.style??{},r={},s=mh(e.props.className),a=t?.colors?.find(c=>c.token&&vh(s,n).includes(c.token))?.swatch??"",d=(n==="light"&&typeof o.backgroundColor=="string"?o.backgroundColor:"")||a;return d&&(r.backgroundColor=d),typeof o.padding=="string"&&o.padding&&(r.padding=o.padding),r}const $8=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/,E8=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,I8=/\{\{.*?\}\}/;function wh(e,t,n){const o=e?.text;if(typeof o!="string")return{display:"",editable:!1,isKey:!1};const r=mt(o);if(r&&r.key.startsWith("custom.")){let a="";try{const l=Pt.getInstance().translate(r.key,{templateId:t,locale:n});a=l&&l!==r.key?l:""}catch{a=""}return{display:a,editable:!0,isKey:!0}}if(I8.test(o))return{display:o,editable:!1,isKey:!1};const s=E8.exec(o);if(s){const a=s[1];let l="";try{const d=Pt.getInstance().translate(a,{templateId:t,locale:n});l=d&&d!==a?d:""}catch{l=""}return{display:l,editable:!0,isKey:!0}}return{display:o,editable:!0,isKey:!1}}function dl(e){const t=er(e);return t===null?{node:null,path:null}:{node:_i(e,t),path:t}}function A8(e){if(!e||typeof e.text!="string")return null;const t=$8.exec(e.text);if(t)return t[1];const n=mt(e.text);return n&&n.key.startsWith("custom.")?n.key:null}function T8({node:e,params:t,t:n,onPatchNode:o,templateIdentifier:r,candidates:s}){const{state:a}=Je(),l=a.selectedRoute?.layoutName??null,d=a.locale,c=r??a.templateIdentifier,p=u.useMemo(()=>t&&typeof t=="object"?t:null,[t]);u.useMemo(()=>ht(p),[p]);const f=u.useMemo(()=>Be(e,p),[e,p]),y=u.useMemo(()=>{const q=p?.cellBorder;return q&&typeof q=="object"?q:null},[p]),g=u.useMemo(()=>{const q=p?.cellBackground;return q&&typeof q=="object"?q:null},[p]),m=u.useMemo(()=>{const q=p?.cellPadding;return q&&typeof q=="object"?q:null},[p]),[h,v]=u.useState(null),[b,x]=u.useState(null),[k,w]=u.useState({}),[_,C]=u.useState(!1),[E,S]=u.useState(null),[T,$]=u.useState(null),[A,z]=u.useState("light"),D=u.useCallback(q=>o(q),[o]),O=u.useMemo(()=>h?Ue(f,h.r,h.c):null,[f,h]),M=T!==null?T:h?.c??null,B=E!==null?E:h?.r??null,N=u.useMemo(()=>{const q=(y?.sides??[]).find($e=>$e.value==="all")??(y?.sides??[])[0],ae=(y?.widths??[]).find($e=>$e.value!=="none");if(!q||!ae)return[];const pe=ae.suffix??"";return[pe?`${q.prefix}${pe}`:q.prefix]},[y]),H=u.useCallback((q,ae,pe)=>{if(N.length===0)return q;const $e=(y?.sides??[]).map(J=>J.prefix),be=Ue(Be(q,p),ae,pe),oe=(typeof be?.cell?.props?.className=="string"?be.cell.props.className:"").split(/\s+/).filter(Boolean);return oe.some(J=>$e.some(le=>J===le||J.startsWith(le+"-")&&/^-?\d+$/.test(J.slice(le.length+1))))?q:Ci(q,p,ae,pe,[...oe,...N].join(" ").trim())},[N,y,p]),L=u.useCallback(q=>{if(!h)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c),j=new Set,oe=[];for(let J=ae;J<=pe;J++)for(let le=$e;le<=be;le++)j.add(`${J}:${le}`),oe.push({r:J,c:le});let ue=e;for(const J of oe){if(ue=H(ue,J.r,J.c),ue=Jt(ue,p,J.r,J.c,"borderColor",q),J.r>0&&!j.has(`${J.r-1}:${J.c}`)){const le=Ue(Be(ue,p),J.r-1,J.c);le&&(ue=Jt(ue,p,le.gridRow,le.gridCol,"borderBottomColor",q))}if(J.c>0&&!j.has(`${J.r}:${J.c-1}`)){const le=Ue(Be(ue,p),J.r,J.c-1);le&&(ue=Jt(ue,p,le.gridRow,le.gridCol,"borderRightColor",q))}}D(ue)},[h,b,e,p,D,H]),I=u.useCallback(q=>L(q),[L]),P=u.useMemo(()=>(y?.colors??[]).map(q=>q.token).filter(Boolean),[y]),F=u.useCallback(q=>{if(!h||P.length===0)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c);let j=e;for(let oe=ae;oe<=pe;oe++)for(let ue=$e;ue<=be;ue++)j=H(j,oe,ue),j=go(j,p,oe,ue,q,P,A);D(j)},[h,b,e,p,D,H,P,A]),G=u.useMemo(()=>{if(A==="dark")return"";const q=O?.cell.props?.style;return typeof q?.borderColor=="string"?q.borderColor:""},[O,A]),V=u.useCallback((q,ae)=>{if(!h)return;const pe=Math.min(h.r,b?.r??h.r),$e=Math.max(h.r,b?.r??h.r),be=Math.min(h.c,b?.c??h.c),j=Math.max(h.c,b?.c??h.c);let oe=e;for(let ue=pe;ue<=$e;ue++)for(let J=be;J<=j;J++)oe=Jt(oe,p,ue,J,q,ae);D(oe)},[h,b,e,p,D]),Y=u.useMemo(()=>(g?.colors??[]).map(q=>q.token).filter(q=>!!q),[g]),Z=u.useCallback(()=>h?{r0:Math.min(h.r,b?.r??h.r),r1:Math.max(h.r,b?.r??h.r),c0:Math.min(h.c,b?.c??h.c),c1:Math.max(h.c,b?.c??h.c)}:null,[h,b]),se=u.useCallback(q=>{const ae=Z();if(!ae||Y.length===0)return;let pe=e;for(let $e=ae.r0;$e<=ae.r1;$e++)for(let be=ae.c0;be<=ae.c1;be++)pe=go(pe,p,$e,be,q,Y,A);D(pe)},[Z,e,p,D,Y,A]),re=u.useCallback(q=>V("backgroundColor",q),[V]),U=u.useCallback(()=>{const q=Z();if(!q)return;let ae=e;for(let pe=q.r0;pe<=q.r1;pe++)for(let $e=q.c0;$e<=q.c1;$e++)ae=go(ae,p,pe,$e,void 0,Y,A),A==="light"&&(ae=Jt(ae,p,pe,$e,"backgroundColor",void 0));D(ae)},[Z,e,p,D,Y,A]),ee=u.useCallback(q=>V("padding",q),[V]),fe=u.useMemo(()=>{if(A==="dark")return"";const q=O?.cell.props?.style;return typeof q?.backgroundColor=="string"?q.backgroundColor:""},[O,A]),K=u.useMemo(()=>typeof O?.cell.props?.className=="string"?O.cell.props.className:"",[O]),ne=u.useMemo(()=>{const q=O?.cell.props?.style;return typeof q?.padding=="string"?q.padding:""},[O]),me=u.useCallback(q=>q==="section"?n("layout_editor.table_editor.move_blocked_section"):"",[n]),Ce=u.useCallback((q,ae)=>xi(f,q,ae),[f]),ze=u.useCallback((q,ae)=>wi(f,q,ae),[f]),Se=u.useCallback(q=>D(nl(e,p,q)),[e,p,D]),qe=u.useCallback(q=>{D(Rg(e,p,q)),v(null),x(null)},[e,p,D]),De=u.useCallback((q,ae)=>D(eh(e,p,q,ae)),[e,p,D]),Pe=u.useCallback(q=>D(nh(e,p,q)),[e,p,D]),Qe=u.useCallback(q=>{D(oh(e,p,q)),v(null),x(null)},[e,p,D]),Me=u.useCallback((q,ae)=>D(th(e,p,q,ae)),[e,p,D]),Re=u.useCallback(()=>{if(!h||!b)return;const q=ol(e,p,h.r,h.c,b.r,b.c);q.ok&&(D(q.table),v({r:Math.min(h.r,b.r),c:Math.min(h.c,b.c)}),x(null))},[e,p,h,b,D]),ve=u.useCallback(()=>{h&&(D(rh(e,p,h.r,h.c)),x(null))},[e,p,h,D]),te=u.useCallback(q=>{if(!h)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c);let j=e;for(let oe=ae;oe<=pe;oe++)for(let ue=$e;ue<=be;ue++)j=Ci(j,p,oe,ue,q);D(j)},[e,p,h,b,D]),he=u.useCallback(async(q,ae)=>{const pe=`${q}:${ae}`,$e=k[pe];if($e===void 0)return;const be=Ue(f,q,ae);if(!be)return;const{node:j,path:oe}=dl(be.cell);if(oe===null)return;const ue=wh(j,c,d);if(ue.editable){if($e===ue.display){w(J=>{const le={...J};return delete le[pe],le});return}C(!0);try{const J=A8(j);if(J)await yr(c,J,d,$e),await tn(c,d);else{const le=await qn(c,l,d,$e);if(le.kind==="ok"&&le.resource){const Ie=`$t:${le.resource.translation_key}`;if(oe.length===0)D(ih(e,p,q,ae,Ie));else{const dt=tr(be.cell,oe,Ie);D(sh(e,p,q,ae,dt))}await tn(c,d)}}}finally{C(!1),w(J=>{const le={...J};return delete le[pe],le})}}},[k,f,c,d,l,e,p,D]),Q=u.useMemo(()=>{if(!O)return null;const{node:q,path:ae}=dl(O.cell);return ae===null?null:{value:typeof q?.text=="string"?q.text:"",editable:!0}},[O]),ie=u.useCallback(q=>{if(typeof q!="string"||q.length===0||!h)return;const ae=Ue(f,h.r,h.c);if(!ae)return;const pe=er(ae.cell);if(pe!==null)if(pe.length===0)D(ih(e,p,h.r,h.c,q));else{const $e=tr(ae.cell,pe,q);D(sh(e,p,h.r,h.c,$e))}},[h,f,e,p,D]),_e=u.useCallback((q,ae,pe)=>{pe?x({r:q,c:ae}):(v({r:q,c:ae}),x(null))},[]),ge=u.useMemo(()=>Array.from({length:f.colCount},(q,ae)=>ae),[f.colCount]),Ae=!!(h&&b&&(h.r!==b.r||h.c!==b.c)),Ne=!!(O&&(O.colSpan>1||O.rowSpan>1)),Le=O&&typeof O.cell.props?.className=="string"?O.cell.props.className:"";return i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--table","data-testid":"g7le-table-editor",style:D8,children:[i.jsx("div",{style:N8,children:n("layout_editor.table_editor.title")}),i.jsx("div",{style:{overflowX:"auto"},children:i.jsxs("table",{"data-testid":"g7le-table-editor-grid",style:z8,children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{style:M8}),ge.map(q=>{const ae=ze(q,-1),pe=ze(q,1),$e=M===q;return i.jsx("th",{style:L8,"data-testid":`g7le-table-colgutter-${q}`,onMouseEnter:()=>$(q),onMouseLeave:()=>$(be=>be===q?null:be),children:i.jsxs("div",{style:{...O8,visibility:$e?"visible":"hidden"},children:[i.jsx("button",{type:"button",title:ae.ok?n("layout_editor.table_editor.move_col_left"):me(ae.reason)||n("layout_editor.table_editor.move_col_left"),disabled:!ae.ok||_,onClick:()=>Me(q,-1),"data-testid":`g7le-table-col-left-${q}`,style:ae.ok?vn:$i,children:"←"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.add_col"),disabled:_,onClick:()=>Pe(q),"data-testid":`g7le-table-col-add-${q}`,style:vn,children:"+"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.remove_col"),disabled:f.colCount<=1||_,onClick:()=>Qe(q),"data-testid":`g7le-table-col-remove-${q}`,style:kh,children:"✕"}),i.jsx("button",{type:"button",title:pe.ok?n("layout_editor.table_editor.move_col_right"):me(pe.reason)||n("layout_editor.table_editor.move_col_right"),disabled:!pe.ok||_,onClick:()=>Me(q,1),"data-testid":`g7le-table-col-right-${q}`,style:pe.ok?vn:$i,children:"→"})]})},q)})]})}),i.jsx("tbody",{children:f.rows.map((q,ae)=>{const pe=q.cells[0]?.gridRow??ae,$e=B===pe;return i.jsxs("tr",{"data-testid":`g7le-table-row-${pe}`,onMouseEnter:()=>S(pe),onMouseLeave:()=>S(be=>be===pe?null:be),children:[i.jsx("td",{style:P8,"data-testid":`g7le-table-rowgutter-${pe}`,children:(()=>{const be=Ce(pe,-1),j=Ce(pe,1);return i.jsxs("div",{style:{...B8,visibility:$e?"visible":"hidden"},children:[i.jsx("button",{type:"button",title:be.ok?n("layout_editor.table_editor.move_row_up"):me(be.reason)||n("layout_editor.table_editor.move_row_up"),disabled:!be.ok||_,onClick:()=>De(pe,-1),"data-testid":`g7le-table-row-up-${pe}`,style:be.ok?vn:$i,children:"↑"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.add_row"),disabled:_,onClick:()=>Se(pe),"data-testid":`g7le-table-row-add-${pe}`,style:vn,children:"+"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.remove_row"),disabled:f.rows.length<=1||_,onClick:()=>qe(pe),"data-testid":`g7le-table-row-remove-${pe}`,style:kh,children:"✕"}),i.jsx("button",{type:"button",title:j.ok?n("layout_editor.table_editor.move_row_down"):me(j.reason)||n("layout_editor.table_editor.move_row_down"),disabled:!j.ok||_,onClick:()=>De(pe,1),"data-testid":`g7le-table-row-down-${pe}`,style:j.ok?vn:$i,children:"↓"})]})})()}),q.cells.map(be=>{const j=`${be.gridRow}:${be.gridCol}`,{node:oe,path:ue}=dl(be.cell),J=wh(oe,c,d),le=ue!==null&&J.editable,Ie=ue===null,dt=k[j],pt=dt!==void 0?dt:J.display,ot=h&&h.r===be.gridRow&&h.c===be.gridCol,Oe=h&&b&&be.gridRow>=Math.min(h.r,b.r)&&be.gridRow<=Math.max(h.r,b.r)&&be.gridCol>=Math.min(h.c,b.c)&&be.gridCol<=Math.max(h.c,b.c);return i.jsx("td",{colSpan:be.colSpan,rowSpan:be.rowSpan,"data-testid":`g7le-table-cell-${be.gridRow}-${be.gridCol}`,onClick:He=>_e(be.gridRow,be.gridCol,He.shiftKey),style:{...W8,...S8(be.cell,y,A),...C8(be.cell,g,A),...be.isHeader?j8:{},...ot?H8:{},...Oe&&!ot?F8:{}},children:le?i.jsx("input",{type:"text","data-testid":`g7le-table-cell-input-${be.gridRow}-${be.gridCol}`,value:pt,placeholder:n("layout_editor.table_editor.cell_text"),disabled:_,onMouseDown:He=>{He.shiftKey&&He.preventDefault(),_e(be.gridRow,be.gridCol,He.shiftKey)},onClick:He=>He.stopPropagation(),onChange:He=>w(Ye=>({...Ye,[j]:He.target.value})),onBlur:()=>void he(be.gridRow,be.gridCol),style:K8}):Ie?i.jsx("span",{"data-testid":`g7le-table-cell-struct-${be.gridRow}-${be.gridCol}`,style:Sh,children:n("layout_editor.table_editor.structural_cell")}):i.jsx("span",{style:Sh,children:J.display})},j)})]},ae)})})]})}),i.jsxs("button",{type:"button","data-testid":"g7le-table-add-row-bottom",onClick:()=>Se(f.rows.length-1),disabled:_,style:V8,children:["+ ",n("layout_editor.table_editor.add_row")]}),i.jsxs("div",{style:U8,"data-testid":"g7le-table-cell-tools",children:[i.jsx("button",{type:"button","data-testid":"g7le-table-merge",onClick:Re,disabled:!Ae||_,style:Ch,children:n("layout_editor.table_editor.merge_cells")}),i.jsx("button",{type:"button","data-testid":"g7le-table-unmerge",onClick:ve,disabled:!Ne||_,style:Ch,children:n("layout_editor.table_editor.unmerge_cells")})]}),O&&Q&&i.jsxs("div",{style:G8,"data-testid":"g7le-table-cell-i18n-section",children:[i.jsx("div",{style:Y8,children:n("layout_editor.table_editor.cell_i18n_label")}),i.jsx(vt,{value:Q.value,onChange:ie,t:n,placeholder:n("layout_editor.table_editor.cell_text"),testidPrefix:"g7le-table-cell-i18n",candidates:s,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),O&&i.jsxs("div",{style:q8,"data-testid":"g7le-table-cell-color-section",children:[i.jsx(yh,{colorScheme:A,onChange:z,t:n,disabled:_}),y&&i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-border-row",children:i.jsx(dh,{className:Le,catalog:y,t:n,onChange:te,colorScheme:A,onPresetToken:F,colorStyle:G,onColorStyle:I,disabled:_})}),i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-fill-row",children:i.jsx(fh,{colorStyle:fe,className:K,catalog:g,t:n,colorScheme:A,onPresetToken:se,onCustomColor:re,onClear:U,disabled:_})})]}),O&&i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-padding-row",children:i.jsx(gh,{paddingStyle:ne,catalog:m,t:n,onChange:ee,disabled:_})}),O&&!y&&i.jsxs("label",{style:X8,"data-testid":"g7le-table-cell-border-row",children:[i.jsx("span",{style:J8,children:n("layout_editor.table_editor.cell_border")}),i.jsx("input",{type:"text","data-testid":"g7le-table-cell-border-input",value:Le,placeholder:n("layout_editor.table_editor.cell_class_placeholder"),onChange:q=>te(q.target.value),disabled:_,style:Z8})]}),!f.valid&&i.jsx("div",{"data-testid":"g7le-table-grid-warning",style:Q8,children:n("layout_editor.table_editor.grid_invalid")}),i.jsx("div",{style:R8,children:n("layout_editor.table_editor.select_hint")})]})}const D8={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},N8={fontSize:12,fontWeight:700,color:"#0f172a"},z8={borderCollapse:"collapse",width:"auto"},M8={width:28,background:"transparent",border:"none"},L8={padding:2,border:"none",background:"transparent"},P8={padding:2,border:"none",background:"transparent",verticalAlign:"middle"},O8={display:"flex",gap:2,justifyContent:"center"},B8={display:"grid",gridTemplateColumns:"repeat(2, 16px)",gap:2,justifyContent:"center"},vn={width:16,height:16,lineHeight:"14px",padding:0,fontSize:10,border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",color:"#475569",cursor:"pointer"},kh={...vn,border:"1px solid #fecaca",color:"#dc2626"},$i={...vn,cursor:"not-allowed",opacity:.4,color:"#94a3b8",borderColor:"#e2e8f0"},W8={border:"1px solid #cbd5e1",padding:0,width:64,minWidth:64,maxWidth:120,height:24,background:"#fff",cursor:"pointer"},j8={background:"#f1f5f9",fontWeight:600},H8={outline:"2px solid #2563eb",outlineOffset:-2},F8={background:"#dbeafe"},K8={width:"100%",boxSizing:"border-box",padding:"2px 5px",fontSize:11,height:22,lineHeight:"18px",border:"none",background:"transparent",outline:"none"},Sh={display:"block",padding:"2px 5px",fontSize:10,color:"#64748b",fontStyle:"italic",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:110},V8={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},U8={display:"flex",gap:6},Ch={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},q8={display:"flex",flexDirection:"column",gap:8,borderTop:"1px solid #f1f5f9",paddingTop:8},G8={display:"flex",flexDirection:"column",gap:6,borderTop:"1px solid #f1f5f9",paddingTop:8},Y8={fontSize:12,fontWeight:700,color:"#0f172a"},cl={display:"flex",flexDirection:"column",gap:6},X8={display:"flex",alignItems:"center",gap:6},J8={fontSize:12,color:"#475569",minWidth:60},Z8={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Q8={fontSize:11,color:"#b45309",background:"#fffbeb",border:"1px solid #fde68a",borderRadius:6,padding:"4px 6px"},R8={fontSize:11,color:"#94a3b8"},ez=/\{\{.*?\}\}/;function tz(e,t,n){const r=(e.props??{})[t];return Array.isArray(r)?r:r===void 0&&n?JSON.parse(JSON.stringify(n)):[]}function nz(e){return typeof e=="string"&&ez.test(e)}function ul(e,t,n){return e?e.startsWith("$t:")?t(e.slice(3)):e:n}function oz(e,t,n){return typeof e=="object"&&e!==null?e[t.key]:n?e:void 0}function rz(e,t,n,o){if(typeof e=="object"&&e!==null){const r={...e};return o===void 0?delete r[t.key]:r[t.key]=o,r}return n?o:{[t.key]:o}}function $h({node:e,params:t,t:n,onPatchNode:o,templateIdentifier:r,candidates:s}){const a=typeof t?.arrayProp=="string"?t.arrayProp:null,l=Array.isArray(t?.fields)?t.fields:[],d=ul(typeof t?.itemLabel=="string"?t.itemLabel:void 0,n,n("layout_editor.array_editor.item")),c=t?.newItem,p=c&&typeof c=="object"?c:null,f=typeof c=="string"||typeof c=="number"?c:null,y=(l.find(C=>C.primary)??l[0])?.key,g=(e.props??{})[a??""],m=Array.isArray(t?.defaultItems)?t.defaultItems:null,h=a?tz(e,a,m):[],v=u.useCallback(C=>{if(!a)return;const E={...e.props??{}};E[a]=C,o({...e,props:E})},[a,e,o]),b=u.useCallback(()=>{if(p)return JSON.parse(JSON.stringify(p));if(f!==null)return f;const C={};for(const E of l)(E.widget??"text")==="boolean"?C[E.key]=!1:C[E.key]="";return C},[p,f,l]),x=u.useCallback(()=>{v([...h,b()])},[v,h,b]),k=u.useCallback(C=>{v(h.filter((E,S)=>S!==C))},[v,h]),w=u.useCallback((C,E)=>{const S=C+E;if(S<0||S>=h.length)return;const T=[...h];[T[C],T[S]]=[T[S],T[C]],v(T)},[v,h]),_=u.useCallback((C,E,S)=>{const T=E.key===y,$=h.map((A,z)=>z===C?rz(A,E,T,S):A);v($)},[h,v,y]);return a?nz(g)||g!==void 0&&!Array.isArray(g)?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor-bound",style:fz,children:n("layout_editor.array_editor.bound_degraded")}):i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor",style:iz,children:[i.jsx("div",{style:sz,children:n("layout_editor.array_editor.items_title")}),h.length===0&&i.jsx("div",{"data-testid":"g7le-array-empty",style:Ih,children:n("layout_editor.array_editor.empty")}),h.map((C,E)=>i.jsxs("div",{"data-testid":`g7le-array-row-${E}`,style:az,children:[i.jsxs("div",{style:lz,children:[i.jsxs("span",{style:dz,children:["#",E+1]}),i.jsx("div",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-array-up-${E}`,title:n("layout_editor.array_editor.move_up"),disabled:E===0,onClick:()=>w(E,-1),style:Eh,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-array-down-${E}`,title:n("layout_editor.array_editor.move_down"),disabled:E===h.length-1,onClick:()=>w(E,1),style:Eh,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-array-remove-${E}`,title:n("layout_editor.array_editor.remove"),onClick:()=>k(E),style:uz,children:"✕"})]}),l.map(S=>{const T=S.widget??"text",$=S.key===y,A=ul(S.label,n,S.key),z=oz(C,S,$),D=`g7le-array-field-${E}-${S.key}`;if(T==="boolean")return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"checkbox","data-testid":D,checked:z===!0,onChange:M=>_(E,S,M.target.checked)})]},S.key);if(T==="select"){const M=Array.isArray(S.options)?S.options:[];return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsxs("select",{"data-testid":D,value:z==null?"":String(z),onChange:B=>_(E,S,B.target.value===""?void 0:B.target.value),style:or,children:[i.jsx("option",{value:"",children:"—"}),M.map((B,N)=>i.jsx("option",{value:String(B.value),children:ul(B.label,n,String(B.value))},N))]})]},S.key)}if(T==="i18n-text"){const M=typeof z=="string"?z:z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("div",{"data-testid":D,style:{flex:1,minWidth:0},children:i.jsx(vt,{value:M,onChange:B=>_(E,S,B===""?void 0:B),t:n,placeholder:A,testidPrefix:`${D}-i18n`,candidates:s,enableExpressionTree:!0,expressionTreeCollapsible:!0})})]},S.key)}if(T==="number"){const M=z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"number","data-testid":D,value:M,onChange:B=>{const N=B.target.value;_(E,S,N===""?void 0:Number(N))},style:or})]},S.key)}if(T==="color"){const M=typeof z=="string"?z:"",B=/^#[0-9a-fA-F]{6}$/.test(M)?M:"#000000";return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"color","data-testid":`${D}-swatch`,value:B,onChange:N=>_(E,S,N.target.value),style:cz}),i.jsx("input",{type:"text","data-testid":D,value:M,placeholder:"#7C3AED",onChange:N=>_(E,S,N.target.value===""?void 0:N.target.value),style:or})]},S.key)}if(T==="number-list"){const M=Array.isArray(z)?z.join(", "):"";return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"text","data-testid":D,value:M,placeholder:n("layout_editor.array_editor.number_list_placeholder"),onChange:B=>{const N=B.target.value.split(",").map(H=>H.trim()).filter(H=>H!=="").map(H=>Number(H)).filter(H=>!Number.isNaN(H));_(E,S,N)},style:or})]},S.key)}const O=z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"text","data-testid":D,value:O,placeholder:T==="icon"?n("layout_editor.array_editor.icon_placeholder"):A,onChange:M=>_(E,S,M.target.value===""?void 0:M.target.value),style:or})]},S.key)})]},E)),i.jsx("button",{type:"button","data-testid":"g7le-array-add",onClick:x,style:pz,children:n("layout_editor.array_editor.add",{item:d})})]}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor-misconfigured",style:Ih,children:n("layout_editor.array_editor.no_array_prop")})}const iz={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},sz={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},az={display:"flex",flexDirection:"column",gap:4,padding:8,border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc"},lz={display:"flex",gap:4,alignItems:"center"},dz={fontSize:11,fontWeight:700,color:"#64748b"},Wn={display:"flex",flexWrap:"wrap",gap:6,alignItems:"center"},jn={fontSize:11,color:"#475569",minWidth:64,flexShrink:0},or={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},cz={width:32,height:28,padding:0,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",flexShrink:0},Eh={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},uz={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},pz={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Ih={fontSize:11,color:"#94a3b8",fontStyle:"italic"},fz={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"};function gz(e,t){return e?e.startsWith("$t:")?t(e.slice(3)):e:null}function hz(e){const{params:t,t:n}=e,o=Array.isArray(t?.groups)?t.groups:[];return o.length===0?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array-group","data-testid":"g7le-array-group-misconfigured",style:vz,children:n("layout_editor.array_editor.no_array_prop")}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array-group","data-testid":"g7le-array-group-editor",style:yz,children:o.map((r,s)=>{const a=gz(r.title,n),{title:l,...d}=r;return i.jsxs("div",{"data-testid":`g7le-array-group-${r.arrayProp??s}`,style:bz,children:[a&&i.jsx("div",{style:mz,children:a}),i.jsx($h,{...e,params:d})]},r.arrayProp??s)})})}const yz={display:"flex",flexDirection:"column",gap:16,width:"100%",marginBottom:12},bz={display:"flex",flexDirection:"column",gap:4},mz={fontSize:12,fontWeight:700,color:"#1e293b",borderBottom:"1px solid #e2e8f0",paddingBottom:4},vz={fontSize:11,color:"#94a3b8",fontStyle:"italic"},_z=/\{\{.*?\}\}/;function xz(e){return typeof e=="string"&&_z.test(e)}function wz(e,t,n){return e?e.startsWith("$t:")?t(e.slice(3)):e:n}function kz(e){const{node:t,params:n,t:o,onPatchNode:r}=e,s=typeof n?.arrayProp=="string"?n.arrayProp:null,a=typeof n?.idField=="string"?n.idField:"id",l=typeof n?.cellChildrenProp=="string"?n.cellChildrenProp:"cellChildren",d=typeof n?.cellChildComponent=="string"?n.cellChildComponent:"Div",c=wz(typeof n?.itemLabel=="string"?n.itemLabel:void 0,o,o("layout_editor.array_editor.item")),[p,f]=u.useState(null),y=s?(t.props??{})[s]:void 0,g=Array.isArray(y)?y:[],m=u.useCallback(C=>{if(!s)return;const E={...t.props??{}};E[s]=C,r({...t,props:E})},[s,t,r]),h=u.useCallback(()=>{const C=n?.newItem;return C&&typeof C=="object"?JSON.parse(JSON.stringify(C)):{[a]:"",[l]:[]}},[n,a,l]),v=u.useCallback(()=>{m([...g,h()])},[m,g,h]),b=u.useCallback(C=>{m(g.filter((E,S)=>S!==C)),f(null)},[m,g]),x=u.useCallback((C,E)=>{const S=C+E;if(S<0||S>=g.length)return;const T=[...g];[T[C],T[S]]=[T[S],T[C]],m(T)},[m,g]),k=u.useCallback((C,E)=>{const S=g.map((T,$)=>$===C?{...T,[a]:E}:T);m(S)},[g,m,a]),w=u.useCallback(C=>({type:"composite",name:"CardGridCell",children:Array.isArray(C[l])?C[l]:[]}),[l]),_=u.useCallback(C=>E=>{const S=Array.isArray(E.children)?E.children:[],T=g.map(($,A)=>A===C?{...$,[l]:S}:$);m(T)},[g,m,l]);return s?xz(y)||y!==void 0&&!Array.isArray(y)?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-bound",style:Mz,children:o("layout_editor.array_editor.bound_degraded")}):i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-editor",style:Sz,children:[i.jsx("div",{style:Cz,children:o("layout_editor.cell_tree_editor.columns_title")}),g.length===0&&i.jsx("div",{"data-testid":"g7le-cell-tree-empty",style:Th,children:o("layout_editor.array_editor.empty")}),g.map((C,E)=>{const S=typeof C[a]=="string"?C[a]:"",T=p===E;return i.jsxs("div",{"data-testid":`g7le-cell-tree-row-${E}`,style:$z,children:[i.jsxs("div",{style:Ez,children:[i.jsxs("span",{style:Iz,children:["#",E+1]}),i.jsx("input",{type:"text","data-testid":`g7le-cell-tree-id-${E}`,value:S,placeholder:o("layout_editor.cell_tree_editor.id_placeholder"),onChange:$=>k(E,$.target.value),style:Az}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-up-${E}`,title:o("layout_editor.array_editor.move_up"),disabled:E===0,onClick:()=>x(E,-1),style:Ah,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-down-${E}`,title:o("layout_editor.array_editor.move_down"),disabled:E===g.length-1,onClick:()=>x(E,1),style:Ah,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-remove-${E}`,title:o("layout_editor.array_editor.remove"),onClick:()=>b(E),style:Tz,children:"✕"})]}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-toggle-${E}`,onClick:()=>f(T?null:E),style:Dz,children:o(T?"layout_editor.cell_tree_editor.collapse_cells":"layout_editor.cell_tree_editor.expand_cells")}),T&&i.jsx("div",{"data-testid":`g7le-cell-tree-cells-${E}`,style:Nz,children:i.jsx(qg,{...e,node:w(C),params:{childComponent:d},onPatchNode:_(E)})})]},E)}),i.jsx("button",{type:"button","data-testid":"g7le-cell-tree-add",onClick:v,style:zz,children:o("layout_editor.cell_tree_editor.add_column",{item:c})})]}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-misconfigured",style:Th,children:o("layout_editor.array_editor.no_array_prop")})}const Sz={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},Cz={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},$z={display:"flex",flexDirection:"column",gap:6,padding:8,border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc"},Ez={display:"flex",gap:4,alignItems:"center"},Iz={fontSize:11,fontWeight:700,color:"#64748b",flexShrink:0},Az={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Ah={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Tz={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Dz={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",alignSelf:"flex-start"},Nz={paddingLeft:8,borderLeft:"2px solid #e2e8f0"},zz={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Th={fontSize:11,color:"#94a3b8",fontStyle:"italic"},Mz={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"};function Lz(e,t,n){const o=Be(e,t);if(!n||n.length===0)return{measured:[],grid:o};const r=[];for(const s of o.rows)for(const a of s.cells){const d=tl(e,a,t).map(p=>`children.${p}`).join("."),c=n.find(p=>p.path===d||p.path.endsWith("."+d));c&&r.push({ref:a,box:{top:c.top,left:c.left,width:c.width,height:c.height}})}return{measured:r,grid:o}}const pl=26,fl=50,gl=6,Dh=44,Nh=18,zh="g7le-table-inplace-empty-cell-style";function Pz(){if(typeof document>"u"||document.getElementById(zh))return;const e=document.createElement("style");e.id=zh,e.textContent=".g7le-preview-frame td[data-editor-name],.g7le-preview-frame th[data-editor-name]{min-width:56px;height:38px;}.g7le-preview-frame table[data-editor-name]{min-width:120px;}",document.head.appendChild(e)}function Oz({node:e,params:t,nodeBox:n,cellBoxes:o,selectedCellPath:r,colorScheme:s,t:a,onPatchNode:l,onRequestInlineEdit:d}){const c=u.useMemo(()=>t&&typeof t=="object"?t:null,[t]),{measured:p,grid:f}=u.useMemo(()=>Lz(e,c,o),[e,c,o]),[y,g]=u.useState(null),[m,h]=u.useState(null),v=u.useMemo(()=>{if(!r)return null;for(const j of f.rows)for(const oe of j.cells){const ue=tl(e,oe,c).map(J=>`children.${J}`).join(".");if(r===ue||r.startsWith(ue+"."))return{r:oe.gridRow,c:oe.gridCol}}return null},[r,f,e,c]),b=u.useMemo(()=>y&&Ue(f,y.r,y.c)?y:v,[y,v,f]),[x,k]=u.useState(!1),[w,_]=u.useState(s==="dark"?"dark":"light"),[C,E]=u.useState(null),[S,T]=u.useState(null),$=u.useRef(null),A=u.useCallback(()=>{$.current!==null&&(clearTimeout($.current),$.current=null)},[]),z=u.useCallback(()=>{A(),$.current=setTimeout(()=>{E(null),T(null),$.current=null},240)},[A]);u.useEffect(()=>()=>A(),[A]),u.useEffect(()=>{Pz()},[]);const D=u.useCallback(j=>l(j),[l]),O=u.useMemo(()=>b?Ue(f,b.r,b.c):null,[f,b]),M=u.useMemo(()=>O&&typeof O.cell.props?.className=="string"?O.cell.props.className:"",[O]),B=u.useCallback(j=>D(nl(e,c,j)),[e,c,D]),N=u.useCallback(j=>D(Rg(e,c,j)),[e,c,D]),H=u.useCallback((j,oe)=>D(eh(e,c,j,oe)),[e,c,D]),L=u.useCallback(j=>D(nh(e,c,j)),[e,c,D]),I=u.useCallback(j=>D(oh(e,c,j)),[e,c,D]),P=u.useCallback((j,oe)=>D(th(e,c,j,oe)),[e,c,D]),F=u.useCallback(j=>{if(!O)return;const oe=O.gridRow,ue=O.gridCol,J=j==="down"?oe+O.rowSpan:oe+O.rowSpan-1,le=j==="right"?ue+O.colSpan:ue+O.colSpan-1;if(J>=f.rows.length||le>=f.colCount)return;const Ie=ol(e,c,oe,ue,J,le);Ie.ok&&D(Ie.table)},[O,f,e,c,D]),G=!!(O&&O.gridCol+O.colSpan<f.colCount),V=!!(O&&O.gridRow+O.rowSpan<f.rows.length),Y=u.useMemo(()=>!b||!m||!Ue(f,m.r,m.c)?null:{r0:Math.min(b.r,m.r),c0:Math.min(b.c,m.c),r1:Math.max(b.r,m.r),c1:Math.max(b.c,m.c)},[b,m,f]),Z=u.useMemo(()=>{if(!Y)return[];const j=new Set,oe=[];for(const ue of f.rows)for(const J of ue.cells)if(J.gridRow>=Y.r0&&J.gridRow<=Y.r1&&J.gridCol>=Y.c0&&J.gridCol<=Y.c1){const le=`${J.gridRow}:${J.gridCol}`;j.has(le)||(j.add(le),oe.push({r:J.gridRow,c:J.gridCol}))}return oe},[Y,f]),se=Z.length>1,re=u.useCallback(()=>{if(!Y)return;const j=ol(e,c,Y.r0,Y.c0,Y.r1,Y.c1);j.ok&&(D(j.table),h(null))},[Y,e,c,D]),U=u.useCallback(()=>{b&&D(rh(e,c,b.r,b.c))},[e,c,b,D]),ee=u.useMemo(()=>{const j=c?.cellBorder;return j&&typeof j=="object"?j:null},[c]),fe=u.useMemo(()=>{const j=c?.cellBackground;return j&&typeof j=="object"?j:null},[c]),K=u.useMemo(()=>{const j=c?.cellPadding;return j&&typeof j=="object"?j:null},[c]),ne=u.useMemo(()=>{if(x){const j=new Set,oe=[];for(const ue of f.rows)for(const J of ue.cells){const le=`${J.gridRow}:${J.gridCol}`;j.has(le)||(j.add(le),oe.push({r:J.gridRow,c:J.gridCol}))}return oe}return se?Z:b?[{r:b.r,c:b.c}]:[]},[x,se,Z,b,f]),me=u.useCallback(j=>{if(ne.length===0)return;let oe=e;for(const ue of ne){const J=(ee?.colors??[]).map(Ie=>Ie.token),le=j.split(/\s+/).filter(Ie=>Ie&&!J.includes(Ie)).join(" ");oe=Ci(oe,c,ue.r,ue.c,le)}D(oe)},[e,c,ne,ee,D]),Ce=u.useMemo(()=>{const j=(ee?.sides??[]).find(J=>J.value==="all")??(ee?.sides??[])[0],oe=(ee?.widths??[]).find(J=>J.value!=="none");if(!j||!oe)return[];const ue=oe.suffix??"";return[ue?`${j.prefix}${ue}`:j.prefix]},[ee]),ze=u.useCallback((j,oe,ue)=>{if(Ce.length===0)return j;const J=(ee?.sides??[]).map(Oe=>Oe.prefix),le=Ue(Be(j,c),oe,ue),dt=(typeof le?.cell?.props?.className=="string"?le.cell.props.className:"").split(/\s+/).filter(Boolean);if(dt.some(Oe=>J.some(He=>Oe===He||Oe.startsWith(He+"-")&&/^-?\d+$/.test(Oe.slice(He.length+1)))))return j;const ot=[...dt,...Ce].join(" ").trim();return Ci(j,c,oe,ue,ot)},[Ce,ee,c]),Se=u.useCallback((j,oe)=>{if(j.length===0)return;const ue=new Set(j.map(le=>`${le.r}:${le.c}`));let J=e;for(const le of j){if(J=ze(J,le.r,le.c),J=Jt(J,c,le.r,le.c,"borderColor",oe),le.r>0&&!ue.has(`${le.r-1}:${le.c}`)){const Ie=Ue(Be(J,c),le.r-1,le.c);Ie&&(J=Jt(J,c,Ie.gridRow,Ie.gridCol,"borderBottomColor",oe))}if(le.c>0&&!ue.has(`${le.r}:${le.c-1}`)){const Ie=Ue(Be(J,c),le.r,le.c-1);Ie&&(J=Jt(J,c,Ie.gridRow,Ie.gridCol,"borderRightColor",oe))}}D(J)},[e,c,D,ze]),qe=u.useMemo(()=>(ee?.colors??[]).map(j=>j.token).filter(Boolean),[ee]),De=u.useCallback(j=>{if(ne.length===0||qe.length===0)return;let oe=e;for(const ue of ne)oe=ze(oe,ue.r,ue.c),oe=go(oe,c,ue.r,ue.c,j,qe,w);D(oe)},[ne,e,c,D,ze,qe,w]),Pe=u.useCallback(j=>Se(ne,j),[Se,ne]),Qe=u.useMemo(()=>{if(w==="dark")return"";const j=O?.cell.props?.style;return typeof j?.borderColor=="string"?j.borderColor:""},[O,w]),Me=u.useCallback((j,oe)=>{if(ne.length===0)return;let ue=e;for(const J of ne)ue=Jt(ue,c,J.r,J.c,j,oe);D(ue)},[ne,e,c,D]),Re=u.useMemo(()=>(fe?.colors??[]).map(j=>j.token).filter(j=>!!j),[fe]),ve=u.useCallback(j=>{if(ne.length===0||Re.length===0)return;let oe=e;for(const ue of ne)oe=go(oe,c,ue.r,ue.c,j,Re,w);D(oe)},[ne,e,c,D,Re,w]),te=u.useCallback(j=>Me("backgroundColor",j),[Me]),he=u.useCallback(()=>{if(ne.length===0)return;let j=e;for(const oe of ne)j=go(j,c,oe.r,oe.c,void 0,Re,w),w==="light"&&(j=Jt(j,c,oe.r,oe.c,"backgroundColor",void 0));D(j)},[ne,e,c,D,Re,w]),Q=u.useCallback(j=>Me("padding",j),[Me]),ie=u.useMemo(()=>{if(w==="dark")return"";const j=O?.cell.props?.style;return typeof j?.backgroundColor=="string"?j.backgroundColor:""},[O,w]),_e=u.useMemo(()=>typeof O?.cell.props?.className=="string"?O.cell.props.className:"",[O]),ge=u.useMemo(()=>{const j=O?.cell.props?.style;return typeof j?.padding=="string"?j.padding:""},[O]),Ae=u.useCallback(j=>{const oe=j.currentTarget,ue=oe.style.pointerEvents;oe.style.pointerEvents="none";const J=document.elementFromPoint(j.clientX,j.clientY);oe.style.pointerEvents=ue;const le=J?.closest?.("[data-dnd-handle-path]");le&&le.dispatchEvent(new PointerEvent("pointerdown",{bubbles:!0,cancelable:!0,clientX:j.clientX,clientY:j.clientY,pointerId:j.pointerId,pointerType:j.pointerType,button:j.button,isPrimary:!0}))},[]),Ne=u.useMemo(()=>{const j=new Map;for(const oe of p)j.has(oe.ref.gridCol)||j.set(oe.ref.gridCol,oe.box);return j},[p]),Le=u.useMemo(()=>{const j=new Map;for(const oe of p)j.has(oe.ref.gridRow)||j.set(oe.ref.gridRow,oe.box);return j},[p]);if(p.length===0)return null;const q=!!(O&&(O.colSpan>1||O.rowSpan>1)),ae=S!==null?S:b?.c??null,pe=C!==null?C:b?.r??null,$e=n.top-pl-gl-Dh,be=n.left-fl-gl-Dh;return i.jsxs("div",{className:"g7le-table-inplace","data-testid":"g7le-table-inplace",style:{position:"absolute",inset:0,pointerEvents:"none",zIndex:g7},children:[p.map(j=>{const oe=b&&b.r===j.ref.gridRow&&b.c===j.ref.gridCol,ue=Y&&j.ref.gridRow>=Y.r0&&j.ref.gridRow<=Y.r1&&j.ref.gridCol>=Y.c0&&j.ref.gridCol<=Y.c1;return i.jsx("div",{"data-testid":`g7le-inplace-cell-${j.ref.gridRow}-${j.ref.gridCol}`,role:"button",tabIndex:-1,"aria-label":a("layout_editor.table_inplace.select_cell"),onMouseEnter:()=>{A(),E(j.ref.gridRow),T(j.ref.gridCol)},onMouseLeave:()=>{z()},onPointerDown:J=>Ae(J),onClick:J=>{if(J.stopPropagation(),J.shiftKey&&(y||b))h({r:j.ref.gridRow,c:j.ref.gridCol});else{g({r:j.ref.gridRow,c:j.ref.gridCol}),h(null);const le=tl(e,j.ref,c).map(Ie=>`children.${Ie}`).join(".");d?.(le)}},style:{position:"absolute",top:j.box.top,left:j.box.left,width:j.box.width,height:j.box.height,minWidth:Nh,minHeight:Nh,boxSizing:"border-box",pointerEvents:"auto",cursor:"pointer",outline:oe?"2px solid #2563eb":"none",outlineOffset:-2,background:ue&&!oe?"rgba(37,99,235,0.14)":"transparent"}},`cell-${j.ref.gridRow}-${j.ref.gridCol}`)}),i.jsx("div",{"data-testid":"g7le-inplace-col-rail",onMouseEnter:A,onMouseLeave:z,style:{position:"absolute",top:$e,left:n.left,width:n.width,height:pl,pointerEvents:"none"},children:ae!==null&&Ne.has(ae)&&(()=>{const j=ae,oe=Ne.get(j),ue=wi(f,j,-1),J=wi(f,j,1),le=oe.left-n.left;return i.jsxs("div",{"data-testid":`g7le-inplace-colgutter-${j}`,onMouseEnter:()=>{A(),T(j)},onMouseLeave:z,style:{position:"absolute",top:0,left:le,width:oe.width,minWidth:96,height:pl,display:"flex",gap:3,justifyContent:"center",alignItems:"center",pointerEvents:"auto"},children:[i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_col_left"),disabled:!ue.ok,onClick:()=>P(j,-1),"data-testid":`g7le-inplace-col-left-${j}`,style:ue.ok?_n:Ei,children:"←"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.add_col"),onClick:()=>L(j),"data-testid":`g7le-inplace-col-add-${j}`,style:_n,children:"+"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.remove_col"),disabled:f.colCount<=1,onClick:()=>I(j),"data-testid":`g7le-inplace-col-remove-${j}`,style:Mh,children:"✕"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_col_right"),disabled:!J.ok,onClick:()=>P(j,1),"data-testid":`g7le-inplace-col-right-${j}`,style:J.ok?_n:Ei,children:"→"})]},`colgut-${j}`)})()}),i.jsx("div",{"data-testid":"g7le-inplace-row-rail",style:{position:"absolute",top:n.top,left:be,width:fl,height:n.height,pointerEvents:"none"},children:pe!==null&&Le.has(pe)&&(()=>{const j=pe,oe=Le.get(j),ue=xi(f,j,-1),J=xi(f,j,1),le=oe.top-n.top;return i.jsxs("div",{"data-testid":`g7le-inplace-rowgutter-${j}`,onMouseEnter:()=>{A(),E(j)},onMouseLeave:z,style:{position:"absolute",top:le,left:0,width:fl,height:oe.height,minHeight:48,display:"grid",gridTemplateColumns:"repeat(2, 22px)",gridAutoRows:"22px",gap:3,justifyContent:"center",alignContent:"center",pointerEvents:"auto"},children:[i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_row_up"),disabled:!ue.ok,onClick:()=>H(j,-1),"data-testid":`g7le-inplace-row-up-${j}`,style:ue.ok?_n:Ei,children:"↑"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.add_row"),onClick:()=>B(j),"data-testid":`g7le-inplace-row-add-${j}`,style:_n,children:"+"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.remove_row"),disabled:f.rows.length<=1,onClick:()=>N(j),"data-testid":`g7le-inplace-row-remove-${j}`,style:Mh,children:"✕"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_row_down"),disabled:!J.ok,onClick:()=>H(j,1),"data-testid":`g7le-inplace-row-down-${j}`,style:J.ok?_n:Ei,children:"↓"})]},`rowgut-${j}`)})()}),i.jsxs("div",{"data-testid":"g7le-inplace-corner-add",style:{position:"absolute",top:n.top+n.height+gl,left:n.left,display:"flex",flexDirection:"column",gap:6,pointerEvents:"auto",minWidth:200,maxWidth:Math.max(240,n.width),background:"#fff",border:"1px solid #e2e8f0",borderRadius:8,padding:8,boxShadow:"0 2px 8px rgba(0,0,0,0.12)"},children:[i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":"g7le-inplace-add-row-bottom",onClick:()=>B(f.rows.length-1),style:Lh,children:["+ ",a("layout_editor.table_inplace.add_row")]}),i.jsxs("button",{type:"button","data-testid":"g7le-inplace-add-col-right",onClick:()=>L(f.colCount-1),style:Lh,children:["+ ",a("layout_editor.table_inplace.add_col")]})]}),!b&&i.jsx("div",{"data-testid":"g7le-inplace-select-hint",style:{fontSize:11,color:"#94a3b8",borderTop:"1px solid #f1f5f9",paddingTop:6},children:a("layout_editor.table_inplace.select_cell_hint")}),b&&i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6,borderTop:"1px solid #f1f5f9",paddingTop:6},children:[se&&i.jsx("div",{"data-testid":"g7le-inplace-range-count",style:{fontSize:11,color:"#2563eb"},children:a("layout_editor.table_inplace.range_selected",{count:Z.length})}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap",alignItems:"center"},children:[se?i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-range",onClick:re,style:rr,children:a("layout_editor.table_inplace.merge_range")}):i.jsxs(i.Fragment,{children:[i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-right",disabled:!G,onClick:()=>F("right"),style:G?rr:hl,children:a("layout_editor.table_inplace.merge_right")}),i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-down",disabled:!V,onClick:()=>F("down"),style:V?rr:hl,children:a("layout_editor.table_inplace.merge_down")})]}),i.jsx("button",{type:"button","data-testid":"g7le-inplace-unmerge",disabled:!q,onClick:U,style:q?rr:hl,children:a("layout_editor.table_inplace.unmerge")})]}),i.jsxs("div",{"data-testid":"g7le-inplace-cell-color",style:{borderTop:"1px solid #f1f5f9",paddingTop:6,display:"flex",flexDirection:"column",gap:6},children:[i.jsxs("label",{style:{display:"flex",alignItems:"center",gap:6,fontSize:11,color:"#475569",cursor:"pointer"},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-inplace-border-all-toggle",checked:x,onChange:j=>k(j.target.checked)}),a("layout_editor.table_inplace.border_apply_all")]}),i.jsx(yh,{colorScheme:w,onChange:_,t:a}),ee&&i.jsx("div",{"data-testid":"g7le-inplace-cell-border",children:i.jsx(dh,{className:x?"":M,catalog:ee,t:a,onChange:me,colorScheme:w,onPresetToken:De,colorStyle:x?"":Qe,onColorStyle:Pe})}),i.jsx("div",{"data-testid":"g7le-inplace-cell-fill",children:i.jsx(fh,{colorStyle:x?"":ie,className:x?"":_e,catalog:fe,t:a,colorScheme:w,onPresetToken:ve,onCustomColor:te,onClear:he})})]}),i.jsx("div",{"data-testid":"g7le-inplace-cell-padding",style:{borderTop:"1px solid #f1f5f9",paddingTop:6},children:i.jsx(gh,{paddingStyle:x?"":ge,catalog:K,t:a,onChange:Q})})]})]})]})}const _n={width:22,height:22,lineHeight:"20px",padding:0,fontSize:13,border:"1px solid #cbd5e1",borderRadius:5,background:"#fff",color:"#475569",cursor:"pointer",boxShadow:"0 1px 3px rgba(0,0,0,0.18)"},Mh={..._n,border:"1px solid #fecaca",color:"#dc2626"},Ei={..._n,cursor:"not-allowed",opacity:.4,color:"#94a3b8",borderColor:"#e2e8f0"},rr={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",boxShadow:"0 1px 2px rgba(0,0,0,0.12)"},hl={...rr,cursor:"not-allowed",opacity:.4},Lh={padding:"3px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};let Ph=!1;function Bz(){Ph||(Pn("children",qg),Pn("table",T8),Pn("array",$h),Pn("array-group",hz),Pn("array-cell-tree",kz),Oa("table",Oz),Ph=!0)}function Wz(){if(typeof window>"u")return;const e=window.G7Core;if(!e)return;const t=e.layoutEditor,n=Array.isArray(t?.__queue)?t.__queue:[],o=Array.isArray(t?.__readyCallbacks)?t.__readyCallbacks:[];e.layoutEditor={registerWidget:Xe,registerNodeEditor:Pn,registerCanvasOverlay:Oa,onReady:r=>{typeof r=="function"&&r()}};for(const r of n){const[s,...a]=r;try{s==="widget"?Xe(a[0],a[1]):s==="nodeEditor"?Pn(a[0],a[1]):s==="canvasOverlay"&&Oa(a[0],a[1])}catch{}}for(const r of o)try{r()}catch{}}const jz=1280;qN(),Bz(),Wz();async function Hz(e){if(typeof fetch!="function")return[];try{const t=await fetch(en(`/api/admin/templates/${encodeURIComponent(e)}/editor/permission-candidates`,"json"),{credentials:"same-origin",headers:je()});if(!t.ok)return[];const o=(await t.json().catch(()=>null))?.data?.permissions;if(!Array.isArray(o))return[];const r=[];for(const s of o)if(s&&typeof s=="object"){const a=s.key,l=s.name;typeof a=="string"&&a.length>0&&r.push({value:a,label:typeof l=="string"&&l?l:a})}return r}catch{return[]}}function Fz({templateIdentifier:e}){gD({templateIdentifier:e}),hD(e);const{state:t,dispatch:n}=Je(),{t:o}=tt(),r=yo(),s=Cp(),a=xD(),l=t.selectedRoute?.layoutName??null,d=CD(e,l),c=t.editMode==="extension",p=u.useMemo(()=>c?kD(a):s,[c,a,s]),[f,y]=u.useState(null),[g,m]=u.useState(null),[h,v]=u.useState([]),[b,x]=u.useState("user"),[k,w]=u.useState(null),[_,C]=u.useState(null),[E,S]=u.useState([]);u.useEffect(()=>{let te=!1;return(async()=>{try{const he=await fetch(en(`/api/templates/${encodeURIComponent(e)}/components`,"json"),{credentials:"same-origin",headers:{Accept:"application/json"}});if(!he.ok)return;const Q=await he.json().catch(()=>null);!te&&Q&&typeof Q=="object"&&y(Q)}catch{}})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{const{spec:he,sampleGlobalSources:Q}=await eb({templateIdentifier:e,coreSeed:$D()});te||(m(he),v(Q),Vz(e,he,Q))})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{if(typeof fetch=="function")try{const he=await fetch(`/api/admin/templates/${encodeURIComponent(e)}`,{credentials:"same-origin",headers:je()});if(!he.ok)return;const ie=(await he.json().catch(()=>null))?.data,_e=ie?.type;te||((_e==="admin"||_e==="user")&&x(_e),typeof ie?.name=="string"&&ie.name&&w(ie.name),typeof ie?.version=="string"&&ie.version&&C(ie.version))}catch{}})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{if(typeof fetch=="function")try{const he=await fetch("/api/admin/templates/?per_page=100&page=1",{credentials:"same-origin",headers:je()});if(!he.ok)return;const ie=(await he.json().catch(()=>null))?.data?.data;if(!te&&Array.isArray(ie)){const _e=[];for(const ge of ie){if(!ge||typeof ge!="object")continue;const Ae=ge.identifier;if(typeof Ae!="string"||!Ae||ge.is_pending===!0)continue;const Le=ge.type==="admin"?"admin":"user",q=ge.name,ae=ge.version;_e.push({identifier:Ae,name:typeof q=="string"?q:Ae,version:typeof ae=="string"?ae:"",type:Le})}S(_e)}}catch{}})(),()=>{te=!0}},[e]);const T=g?.nesting??null,$=t.selectedRoute?.path??null,A=t.editMode,z=t.selectedRoute?.layoutName??(c?a.document?.hostLayoutName??null:null),D=t.routeTree,O=u.useMemo(()=>{if(A!=="extension"||!z)return null;const te=bl(D,z);return te||`__base__/${z}`},[A,z,D]),M=c&&t.returnRoute===null&&!!z;u.useEffect(()=>{if(!M||!z)return;const te=bl(D,z);te&&n({type:"SET_RETURN_ROUTE",route:{path:te,layoutName:z}})},[M,z,D,n]),u.useEffect(()=>{const te=G4(A,$,O),he=Y4(g?.states?.groups,te);n({type:"SET_AVAILABLE_STATES",states:he,activeStateId:X4(he)})},[g,A,$,O,n]);const[B,N]=u.useState([]);u.useEffect(()=>{let te=!1;return(async()=>{const he=await Hz(e);te||N(he)})(),()=>{te=!0}},[e]),yD(),bD();const H=u.useCallback(te=>{const he=t.routeTree,Q=te.split("?")[0]?.split("#")[0]??te;return Kz(he,Q)?"route_in_tree":"route_not_in_tree"},[t.routeTree]),L=u.useCallback(te=>{const he=te.split("?")[0]?.split("#")[0]??te,Q=yl(t.routeTree,he);Q&&(p.isDirty&&!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.dirty_guard.message")))||n({type:"SELECT_ROUTE",route:{path:Q.path,layoutName:Q.layoutName}}))},[t.routeTree,p.isDirty,n]),I=u.useRef([]),P=u.useCallback(te=>{I.current=te},[]),[F,G]=u.useState(null),V=u.useCallback(()=>G(null),[]),Y=u.useRef(null),Z=u.useRef(null);u.useEffect(()=>{const te=Z.current,he=Y.current;if(!te||!he||typeof ResizeObserver>"u")return;const Q=()=>{he.style.setProperty("--g7le-header-h",`${Math.round(te.getBoundingClientRect().height)}px`)};Q();const ie=new ResizeObserver(Q);return ie.observe(te),()=>ie.disconnect()},[]);const se=u.useCallback(()=>{p.reload()},[p]),re=u.useCallback(()=>{p.isDirty&&!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.dirty_guard.message")))||window.G7Core?.dispatch?.({handler:"openWindow",params:{path:`/admin/templates/${b}`,target:"_self"}})},[p.isDirty,o,b]),U=u.useCallback(te=>{if(!te||te===e||p.isDirty&&!(typeof window<"u"?window.confirm(o("layout_editor.chrome.dirty_guard.message")):!0))return;const he=Zs(te);window.G7Core?.dispatch?.({handler:"openWindow",params:{path:he,target:"_self"}})},[e,p.isDirty,o]),ee=u.useCallback(()=>{const te=ME(e,t.selectedRoute?.path??null);window.G7Core?.dispatch?.({handler:"openWindow",params:{path:te}})},[e,t.selectedRoute]),fe=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"";if(!te)return;const he=r.open({ariaLabel:o("layout_editor.attachment_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(Ml,{templateIdentifier:e,layoutName:te,t:o,onClose:()=>r.close(he)})})},[r,e,t.selectedRoute,o]),K=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"";if(!te)return;const he=Hy(s.document?.raw??{}),Q=r.open({ariaLabel:o("layout_editor.translation_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(dm,{templateIdentifier:e,layoutName:te,t:o,referencedKeys:he,onClose:()=>r.close(Q)})})},[r,e,t.selectedRoute,o,s.document]),ne=u.useCallback((te,he)=>{const Q=window.G7Core;if(Q&&typeof Q.t=="function"){const ie=Q.t(te,he);if(ie&&ie!==te&&!ie.startsWith("$t:"))return ie}return o(te,he)},[o]),me=u.useCallback(()=>{if(!t.selectedRoute||t.editMode==="extension"||t.editMode==="iteration_item")return;const te=r.open({ariaLabel:o("layout_editor.page_settings.title",{name:t.selectedRoute.path??""}),width:1080,maxHeightRatio:.88,closeOnEscape:!1,content:u.createElement(u$,{templateIdentifier:e,spec:g,t:o,resolveLabel:ne,permissionCandidates:B,onClose:()=>r.close(te)})})},[r,t.selectedRoute,t.editMode,o,ne,e,g,B]),Ce=u.useCallback(async()=>{const te=await d.createPreview(s.document?.raw??null);if(te.kind!=="success"){const he=te.kind==="network_error"?te.message:o("layout_editor.preview_action.no_document");typeof window<"u"&&window.alert(he)}},[d,s.document,o]),ze=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"",he=t.selectedRoute?.path??"",Q=t.editMode==="extension"&&he.startsWith("__extension__/")?he.slice(14):null,ie=Q?{kind:"extension",extensionId:Q}:te?{kind:"layout",layoutName:te}:null;if(!ie)return;const _e=r.open({ariaLabel:o("layout_editor.version_history.title"),width:640,maxHeightRatio:.82,content:u.createElement(X$,{templateIdentifier:e,target:ie,t:o,onRestored:async ge=>{typeof ge=="number"&&(ie.kind==="extension"?n({type:"SET_EXTENSION_VERSION",extensionId:ie.extensionId,version:ge}):n({type:"SET_LAYOUT_VERSION",layoutName:ie.layoutName,version:ge})),await p.reload()},onClose:()=>r.close(_e)})})},[r,e,t.selectedRoute,t.editMode,o,p,n]),Se=p.dirtyKeys.size>0||p.isDirty;u.useEffect(()=>{if(!Se)return;const te=he=>{he.preventDefault(),he.returnValue=""};return window.addEventListener("beforeunload",te),()=>window.removeEventListener("beforeunload",te)},[Se]);const qe=u.useCallback(()=>{!p.isDirty||!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.reset_guard.message")))||p.reload()},[p,o]),De=u.useCallback(async()=>{const te=await p.save({sessionAddedPaths:I.current});G(te),te.kind==="success"&&typeof te.newContentVersion=="number"&&(te.savedExtensionId?n({type:"SET_EXTENSION_VERSION",extensionId:te.savedExtensionId,version:te.newContentVersion}):te.savedLayoutName&&n({type:"SET_LAYOUT_VERSION",layoutName:te.savedLayoutName,version:te.newContentVersion}))},[p,n]),Pe=u.useCallback(()=>{const te=r.open({ariaLabel:o("layout_editor.shortcuts.title"),width:560,maxHeightRatio:.85,content:u.createElement(iE,{t:o,onClose:()=>r.close(te)})})},[r,o]),Qe=u.useMemo(()=>({save:()=>{De()},exit:re,editCode:ee,reset:qe,translations:K,addElement:()=>{t.selectedRoute&&n({type:"TOGGLE_PALETTE"})},help:Pe,preview:()=>{t.selectedRoute?.layoutName&&Ce()}}),[De,re,ee,qe,K,t.selectedRoute,n,Pe,Ce]),[Me,Re]=u.useState(!1);u.useEffect(()=>{const te=window.setInterval(()=>{Re(!!window.__g7LayoutEditorHasSelection)},200);return()=>window.clearInterval(te)},[]),np({handlers:Qe,hasSelection:Me});const ve=p;return t.routesError?i.jsx("div",{className:"g7le-chrome g7le-chrome--routes-error","data-testid":"g7le-chrome-routes-error",style:{position:"fixed",inset:0,display:"flex",background:"#f8fafc",color:"#0f172a",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',fontSize:13,lineHeight:1.5,zIndex:9e3},children:i.jsx(Fa,{error:t.routesError})}):i.jsx("div",{className:"g7le-chrome","data-testid":"g7le-chrome",ref:Y,style:{position:"static",minHeight:"100vh",minWidth:jz,background:"#f8fafc",color:"#0f172a",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',fontSize:13,lineHeight:1.5,zIndex:9e3},children:i.jsxs(dy,{value:ve,children:[c&&a.needsHostPicker&&a.document&&i.jsx(SD,{hostLayouts:a.document.hostLayouts,extensionLabel:null,onSelect:a.selectHost}),i.jsxs("div",{className:"g7le-chrome__header","data-testid":"g7le-chrome-header",ref:Z,style:{position:"sticky",top:0,zIndex:20,background:"#f8fafc"},children:[i.jsx(by,{onSave:()=>{De()},isDirty:p.isDirty,onExit:re,onEditCode:ee,onManageImages:fe,onManageTranslations:K,onPageSettings:me,onReset:qe,onShowShortcuts:Pe,onPreview:Ce,onShowVersions:ze,templateName:k,templateVersion:_,templateList:E,onSwitchTemplate:U}),i.jsx(xE,{result:F,onDismiss:V,onLoadLatest:se}),i.jsxs("div",{className:"g7le-chrome__device-toolbar","data-testid":"g7le-chrome-device-toolbar",style:{display:"flex",justifyContent:"center",padding:"6px 12px",background:"#f1f5f9",borderBottom:"1px solid #e2e8f0"},children:[i.jsx(U4,{}),i.jsx(q4,{})]})]}),i.jsxs("div",{className:"g7le-chrome__body",style:{display:"grid",gridTemplateColumns:"auto 1fr",alignItems:"stretch"},children:[i.jsx(HE,{}),i.jsx(j4,{manifest:f,nesting:T,componentPalette:g?.componentPalette??null,spec:g,permissionCandidates:B,sampleGlobalSources:h,resolveRouteMatch:H,onNavigateToDestination:L,onSessionAddedPathsChange:P})]}),i.jsx(py,{})]})})}function yl(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=yl(n.children,t);if(o)return o}}return null}function Kz(e,t){return yl(e,t)!==null}function bl(e,t){for(const n of e){if(n.kind==="route"&&n.layoutName===t&&typeof n.path=="string")return n.path;if(n.children&&n.children.length>0){const o=bl(n.children,t);if(o)return o}}return null}function Vz(e,t,n){const o=[];Va({sources:n,isGuestOnly:!1,warn:s=>{const a=/extension '([^']+)' override of core key '([^']+)'/.exec(s);a&&o.push(`${a[1]}:${a[2]}`)}});const r=t?.sampleData?.byDataSourceId?Object.keys(t.sampleData.byDataSourceId).length:0;Qh({templateIdentifier:e,mergedSources:[...n.map(s=>({kind:s.kind,id:s.id}))],blockCounts:{controls:t?.controls?Object.keys(t.controls).length:0,componentCapabilities:t?.componentCapabilities?Object.keys(t.componentCapabilities).length:0,actionRecipes:t?.actionRecipes?Object.keys(t.actionRecipes).length:0,conditionRecipes:t?.conditionRecipes?Object.keys(t.conditionRecipes).length:0,paletteGroups:t?.componentPalette?.groups?.length??0,stateGroups:t?.states?.groups?.length??0,sampleDataIds:r},sampleGlobalSourceCount:n.length,sampleGlobalConflicts:o,timestamp:Date.now()})}function Oh({templateIdentifier:e,initialLocale:t}){return u.useEffect(()=>()=>{Vh(),qh(),Yh(),Xh(),Jh(),Zh(),Rh(),ey(),ty()},[]),i.jsx(ly,{templateIdentifier:e,initialLocale:t,children:i.jsx(uy,{children:i.jsx(Fz,{templateIdentifier:e})})})}if(typeof window<"u"){const e=window.G7Core||(window.G7Core={});if(e.__LayoutEditorChrome=Oh,typeof e.__onChromeReady=="function")try{e.__onChromeReady()}catch{}}dn.LayoutEditorChrome=Oh,Object.defineProperty(dn,Symbol.toStringTag,{value:"Module"})})(this.G7LayoutEditor=this.G7LayoutEditor||{},ReactJSXRuntime,React,ReactDOM); -//# sourceMappingURL=layout-editor.min.js.map + `},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=[];function o(r){if(t!=null&&n.length>=t||!r)return n;if(sa(r)&&r.scrollingElement!=null&&!n.includes(r.scrollingElement))return n.push(r.scrollingElement),n;if(!Oo(r)||Wp(r)||n.includes(r))return n;const s=_t(e).getComputedStyle(r);return r!==e&&r2(r,s)&&n.push(r),o2(r,s)?n:o(r.parentNode)}return e?o(e):n}function qp(e){const[t]=da(e,1);return t??null}function ca(e){return!Jr||!e?null:lo(e)?e:ia(e)?sa(e)||e===co(e).scrollingElement?window:Oo(e)?e:null:null}function Gp(e){return lo(e)?e.scrollX:e.scrollLeft}function Yp(e){return lo(e)?e.scrollY:e.scrollTop}function ua(e){return{x:Gp(e),y:Yp(e)}}var lt;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(lt||(lt={}));function Xp(e){return!Jr||!e?!1:e===document.scrollingElement}function Jp(e){const t={x:0,y:0},n=Xp(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},o={x:e.scrollWidth-n.width,y:e.scrollHeight-n.height},r=e.scrollTop<=t.y,s=e.scrollLeft<=t.x,a=e.scrollTop>=o.y,l=e.scrollLeft>=o.x;return{isTop:r,isLeft:s,isBottom:a,isRight:l,maxScroll:o,minScroll:t}}const i2={x:.2,y:.2};function s2(e,t,n,o,r){let{top:s,left:a,right:l,bottom:d}=n;o===void 0&&(o=10),r===void 0&&(r=i2);const{isTop:c,isBottom:p,isLeft:f,isRight:y}=Jp(e),g={x:0,y:0},m={x:0,y:0},h={height:t.height*r.y,width:t.width*r.x};return!c&&s<=t.top+h.height?(g.y=lt.Backward,m.y=o*Math.abs((t.top+h.height-s)/h.height)):!p&&d>=t.bottom-h.height&&(g.y=lt.Forward,m.y=o*Math.abs((t.bottom-h.height-d)/h.height)),!y&&l>=t.right-h.width?(g.x=lt.Forward,m.x=o*Math.abs((t.right-h.width-l)/h.width)):!f&&a<=t.left+h.width&&(g.x=lt.Backward,m.x=o*Math.abs((t.left+h.width-a)/h.width)),{direction:g,speed:m}}function a2(e){if(e===document.scrollingElement){const{innerWidth:s,innerHeight:a}=window;return{top:0,left:0,right:s,bottom:a,width:s,height:a}}const{top:t,left:n,right:o,bottom:r}=e.getBoundingClientRect();return{top:t,left:n,right:o,bottom:r,width:e.clientWidth,height:e.clientHeight}}function Zp(e){return e.reduce((t,n)=>uo(t,ua(n)),Ht)}function l2(e){return e.reduce((t,n)=>t+Gp(n),0)}function d2(e){return e.reduce((t,n)=>t+Yp(n),0)}function Qp(e,t){if(t===void 0&&(t=Ho),!e)return;const{top:n,left:o,bottom:r,right:s}=t(e);qp(e)&&(r<=0||s<=0||n>=window.innerHeight||o>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const c2=[["x",["left","right"],l2],["y",["top","bottom"],d2]];class pa{constructor(t,n){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const o=da(n),r=Zp(o);this.rect={...t},this.width=t.width,this.height=t.height;for(const[s,a,l]of c2)for(const d of a)Object.defineProperty(this,d,{get:()=>{const c=l(o),p=r[s]-c;return this.rect[d]+p},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Fo{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(n=>{var o;return(o=this.target)==null?void 0:o.removeEventListener(...n)})},this.target=t}add(t,n,o){var r;(r=this.target)==null||r.addEventListener(t,n,o),this.listeners.push([t,n,o])}}function u2(e){const{EventTarget:t}=_t(e);return e instanceof t?e:co(e)}function fa(e,t){const n=Math.abs(e.x),o=Math.abs(e.y);return typeof t=="number"?Math.sqrt(n**2+o**2)>t:"x"in t&&"y"in t?n>t.x&&o>t.y:"x"in t?n>t.x:"y"in t?o>t.y:!1}var At;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(At||(At={}));function Rp(e){e.preventDefault()}function p2(e){e.stopPropagation()}var Ke;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(Ke||(Ke={}));const ef={start:[Ke.Space,Ke.Enter],cancel:[Ke.Esc],end:[Ke.Space,Ke.Enter,Ke.Tab]},f2=(e,t)=>{let{currentCoordinates:n}=t;switch(e.code){case Ke.Right:return{...n,x:n.x+25};case Ke.Left:return{...n,x:n.x-25};case Ke.Down:return{...n,y:n.y+25};case Ke.Up:return{...n,y:n.y-25}}};class tf{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:n}}=t;this.props=t,this.listeners=new Fo(co(n)),this.windowListeners=new Fo(_t(n)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(At.Resize,this.handleCancel),this.windowListeners.add(At.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(At.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:n}=this.props,o=t.node.current;o&&Qp(o),n(Ht)}handleKeyDown(t){if(la(t)){const{active:n,context:o,options:r}=this.props,{keyboardCodes:s=ef,coordinateGetter:a=f2,scrollBehavior:l="smooth"}=r,{code:d}=t;if(s.end.includes(d)){this.handleEnd(t);return}if(s.cancel.includes(d)){this.handleCancel(t);return}const{collisionRect:c}=o.current,p=c?{x:c.left,y:c.top}:Ht;this.referenceCoordinates||(this.referenceCoordinates=p);const f=a(t,{active:n,context:o.current,currentCoordinates:p});if(f){const y=ti(f,p),g={x:0,y:0},{scrollableAncestors:m}=o.current;for(const h of m){const v=t.code,{isTop:b,isRight:x,isLeft:k,isBottom:w,maxScroll:_,minScroll:C}=Jp(h),E=a2(h),S={x:Math.min(v===Ke.Right?E.right-E.width/2:E.right,Math.max(v===Ke.Right?E.left:E.left+E.width/2,f.x)),y:Math.min(v===Ke.Down?E.bottom-E.height/2:E.bottom,Math.max(v===Ke.Down?E.top:E.top+E.height/2,f.y))},T=v===Ke.Right&&!x||v===Ke.Left&&!k,$=v===Ke.Down&&!w||v===Ke.Up&&!b;if(T&&S.x!==f.x){const A=h.scrollLeft+y.x,z=v===Ke.Right&&A<=_.x||v===Ke.Left&&A>=C.x;if(z&&!y.y){h.scrollTo({left:A,behavior:l});return}z?g.x=h.scrollLeft-A:g.x=v===Ke.Right?h.scrollLeft-_.x:h.scrollLeft-C.x,g.x&&h.scrollBy({left:-g.x,behavior:l});break}else if($&&S.y!==f.y){const A=h.scrollTop+y.y,z=v===Ke.Down&&A<=_.y||v===Ke.Up&&A>=C.y;if(z&&!y.x){h.scrollTo({top:A,behavior:l});return}z?g.y=h.scrollTop-A:g.y=v===Ke.Down?h.scrollTop-_.y:h.scrollTop-C.y,g.y&&h.scrollBy({top:-g.y,behavior:l});break}}this.handleMove(t,uo(ti(f,this.referenceCoordinates),g))}}}handleMove(t,n){const{onMove:o}=this.props;t.preventDefault(),o(n)}handleEnd(t){const{onEnd:n}=this.props;t.preventDefault(),this.detach(),n()}handleCancel(t){const{onCancel:n}=this.props;t.preventDefault(),this.detach(),n()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}tf.activators=[{eventName:"onKeyDown",handler:(e,t,n)=>{let{keyboardCodes:o=ef,onActivation:r}=t,{active:s}=n;const{code:a}=e.nativeEvent;if(o.start.includes(a)){const l=s.activatorNode.current;return l&&e.target!==l?!1:(e.preventDefault(),r?.({event:e.nativeEvent}),!0)}return!1}}];function nf(e){return!!(e&&"distance"in e)}function of(e){return!!(e&&"delay"in e)}class ga{constructor(t,n,o){var r;o===void 0&&(o=u2(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=n;const{event:s}=t,{target:a}=s;this.props=t,this.events=n,this.document=co(a),this.documentListeners=new Fo(this.document),this.listeners=new Fo(o),this.windowListeners=new Fo(_t(a)),this.initialCoordinates=(r=ni(s))!=null?r:Ht,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:n,bypassActivationConstraint:o}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(At.Resize,this.handleCancel),this.windowListeners.add(At.DragStart,Rp),this.windowListeners.add(At.VisibilityChange,this.handleCancel),this.windowListeners.add(At.ContextMenu,Rp),this.documentListeners.add(At.Keydown,this.handleKeydown),n){if(o!=null&&o({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(of(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if(nf(n)){this.handlePending(n);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,n){const{active:o,onPending:r}=this.props;r(o,t,this.initialCoordinates,n)}handleStart(){const{initialCoordinates:t}=this,{onStart:n}=this.props;t&&(this.activated=!0,this.documentListeners.add(At.Click,p2,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(At.SelectionChange,this.removeTextSelection),n(t))}handleMove(t){var n;const{activated:o,initialCoordinates:r,props:s}=this,{onMove:a,options:{activationConstraint:l}}=s;if(!r)return;const d=(n=ni(t))!=null?n:Ht,c=ti(r,d);if(!o&&l){if(nf(l)){if(l.tolerance!=null&&fa(c,l.tolerance))return this.handleCancel();if(fa(c,l.distance))return this.handleStart()}if(of(l)&&fa(c,l.tolerance))return this.handleCancel();this.handlePending(l,c);return}t.cancelable&&t.preventDefault(),a(d)}handleEnd(){const{onAbort:t,onEnd:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleCancel(){const{onAbort:t,onCancel:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleKeydown(t){t.code===Ke.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const g2={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class ha extends ga{constructor(t){const{event:n}=t,o=co(n.target);super(t,g2,o)}}ha.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;return!n.isPrimary||n.button!==0?!1:(o?.({event:n}),!0)}}];const h2={move:{name:"mousemove"},end:{name:"mouseup"}};var ya;(function(e){e[e.RightClick=2]="RightClick"})(ya||(ya={}));class y2 extends ga{constructor(t){super(t,h2,co(t.event.target))}}y2.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;return n.button===ya.RightClick?!1:(o?.({event:n}),!0)}}];const ba={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class b2 extends ga{constructor(t){super(t,ba)}static setup(){return window.addEventListener(ba.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(ba.move.name,t)};function t(){}}}b2.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:o}=t;const{touches:r}=n;return r.length>1?!1:(o?.({event:n}),!0)}}];var Ko;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(Ko||(Ko={}));var ri;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(ri||(ri={}));function m2(e){let{acceleration:t,activator:n=Ko.Pointer,canScroll:o,draggingRect:r,enabled:s,interval:a=5,order:l=ri.TreeOrder,pointerCoordinates:d,scrollableAncestors:c,scrollableAncestorRects:p,delta:f,threshold:y}=e;const g=_2({delta:f,disabled:!s}),[m,h]=$7(),v=u.useRef({x:0,y:0}),b=u.useRef({x:0,y:0}),x=u.useMemo(()=>{switch(n){case Ko.Pointer:return d?{top:d.y,bottom:d.y,left:d.x,right:d.x}:null;case Ko.DraggableRect:return r}},[n,r,d]),k=u.useRef(null),w=u.useCallback(()=>{const C=k.current;if(!C)return;const E=v.current.x*b.current.x,S=v.current.y*b.current.y;C.scrollBy(E,S)},[]),_=u.useMemo(()=>l===ri.TreeOrder?[...c].reverse():c,[l,c]);u.useEffect(()=>{if(!s||!c.length||!x){h();return}for(const C of _){if(o?.(C)===!1)continue;const E=c.indexOf(C),S=p[E];if(!S)continue;const{direction:T,speed:$}=s2(C,S,x,t,y);for(const A of["x","y"])g[A][T[A]]||($[A]=0,T[A]=0);if($.x>0||$.y>0){h(),k.current=C,m(w,a),v.current=$,b.current=T;return}}v.current={x:0,y:0},b.current={x:0,y:0},h()},[t,w,o,h,s,a,JSON.stringify(x),JSON.stringify(g),m,c,_,p,JSON.stringify(y)])}const v2={x:{[lt.Backward]:!1,[lt.Forward]:!1},y:{[lt.Backward]:!1,[lt.Forward]:!1}};function _2(e){let{delta:t,disabled:n}=e;const o=Rr(t);return Wo(r=>{if(n||!o||!r)return v2;const s={x:Math.sign(t.x-o.x),y:Math.sign(t.y-o.y)};return{x:{[lt.Backward]:r.x[lt.Backward]||s.x===-1,[lt.Forward]:r.x[lt.Forward]||s.x===1},y:{[lt.Backward]:r.y[lt.Backward]||s.y===-1,[lt.Forward]:r.y[lt.Forward]||s.y===1}}},[n,t,o])}function x2(e,t){const n=t!=null?e.get(t):void 0,o=n?n.node.current:null;return Wo(r=>{var s;return t==null?null:(s=o??r)!=null?s:null},[o,t])}function w2(e,t){return u.useMemo(()=>e.reduce((n,o)=>{const{sensor:r}=o,s=r.activators.map(a=>({eventName:a.eventName,handler:t(a.handler,o)}));return[...n,...s]},[]),[e,t])}var Vo;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(Vo||(Vo={}));var ma;(function(e){e.Optimized="optimized"})(ma||(ma={}));const rf=new Map;function k2(e,t){let{dragging:n,dependencies:o,config:r}=t;const[s,a]=u.useState(null),{frequency:l,measure:d,strategy:c}=r,p=u.useRef(e),f=v(),y=Bo(f),g=u.useCallback(function(b){b===void 0&&(b=[]),!y.current&&a(x=>x===null?b:x.concat(b.filter(k=>!x.includes(k))))},[y]),m=u.useRef(null),h=Wo(b=>{if(f&&!n)return rf;if(!b||b===rf||p.current!==e||s!=null){const x=new Map;for(let k of e){if(!k)continue;if(s&&s.length>0&&!s.includes(k.id)&&k.rect.current){x.set(k.id,k.rect.current);continue}const w=k.node.current,_=w?new pa(d(w),w):null;k.rect.current=_,_&&x.set(k.id,_)}return x}return b},[e,s,n,f,d]);return u.useEffect(()=>{p.current=e},[e]),u.useEffect(()=>{f||g()},[n,f]),u.useEffect(()=>{s&&s.length>0&&a(null)},[JSON.stringify(s)]),u.useEffect(()=>{f||typeof l!="number"||m.current!==null||(m.current=setTimeout(()=>{g(),m.current=null},l))},[l,f,g,...o]),{droppableRects:h,measureDroppableContainers:g,measuringScheduled:s!=null};function v(){switch(c){case Vo.Always:return!1;case Vo.BeforeDragging:return n;default:return!n}}}function va(e,t){return Wo(n=>e?n||(typeof t=="function"?t(e):e):null,[t,e])}function S2(e,t){return va(e,t)}function C2(e){let{callback:t,disabled:n}=e;const o=Zr(t),r=u.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:s}=window;return new s(o)},[o,n]);return u.useEffect(()=>()=>r?.disconnect(),[r]),r}function ii(e){let{callback:t,disabled:n}=e;const o=Zr(t),r=u.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:s}=window;return new s(o)},[n]);return u.useEffect(()=>()=>r?.disconnect(),[r]),r}function $2(e){return new pa(Ho(e),e)}function sf(e,t,n){t===void 0&&(t=$2);const[o,r]=u.useState(null);function s(){r(d=>{if(!e)return null;if(e.isConnected===!1){var c;return(c=d??n)!=null?c:null}const p=t(e);return JSON.stringify(d)===JSON.stringify(p)?d:p})}const a=C2({callback(d){if(e)for(const c of d){const{type:p,target:f}=c;if(p==="childList"&&f instanceof HTMLElement&&f.contains(e)){s();break}}}}),l=ii({callback:s});return sn(()=>{s(),e?(l?.observe(e),a?.observe(document.body,{childList:!0,subtree:!0})):(l?.disconnect(),a?.disconnect())},[e]),o}function E2(e){const t=va(e);return Kp(e,t)}const af=[];function I2(e){const t=u.useRef(e),n=Wo(o=>e?o&&o!==af&&e&&t.current&&e.parentNode===t.current.parentNode?o:da(e):af,[e]);return u.useEffect(()=>{t.current=e},[e]),n}function A2(e){const[t,n]=u.useState(null),o=u.useRef(e),r=u.useCallback(s=>{const a=ca(s.target);a&&n(l=>l?(l.set(a,ua(a)),new Map(l)):null)},[]);return u.useEffect(()=>{const s=o.current;if(e!==s){a(s);const l=e.map(d=>{const c=ca(d);return c?(c.addEventListener("scroll",r,{passive:!0}),[c,ua(c)]):null}).filter(d=>d!=null);n(l.length?new Map(l):null),o.current=e}return()=>{a(e),a(s)};function a(l){l.forEach(d=>{const c=ca(d);c?.removeEventListener("scroll",r)})}},[r,e]),u.useMemo(()=>e.length?t?Array.from(t.values()).reduce((s,a)=>uo(s,a),Ht):Zp(e):Ht,[e,t])}function lf(e,t){t===void 0&&(t=[]);const n=u.useRef(null);return u.useEffect(()=>{n.current=null},t),u.useEffect(()=>{const o=e!==Ht;o&&!n.current&&(n.current=e),!o&&n.current&&(n.current=null)},[e]),n.current?ti(e,n.current):Ht}function T2(e){u.useEffect(()=>{if(!Jr)return;const t=e.map(n=>{let{sensor:o}=n;return o.setup==null?void 0:o.setup()});return()=>{for(const n of t)n?.()}},e.map(t=>{let{sensor:n}=t;return n}))}function D2(e,t){return u.useMemo(()=>e.reduce((n,o)=>{let{eventName:r,handler:s}=o;return n[r]=a=>{s(a,t)},n},{}),[e,t])}function df(e){return u.useMemo(()=>e?n2(e):null,[e])}const cf=[];function N2(e,t){t===void 0&&(t=Ho);const[n]=e,o=df(n?_t(n):null),[r,s]=u.useState(cf);function a(){s(()=>e.length?e.map(d=>Xp(d)?o:new pa(t(d),d)):cf)}const l=ii({callback:a});return sn(()=>{l?.disconnect(),a(),e.forEach(d=>l?.observe(d))},[e]),r}function uf(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return Oo(t)?t:e}function z2(e){let{measure:t}=e;const[n,o]=u.useState(null),r=u.useCallback(c=>{for(const{target:p}of c)if(Oo(p)){o(f=>{const y=t(p);return f?{...f,width:y.width,height:y.height}:y});break}},[t]),s=ii({callback:r}),a=u.useCallback(c=>{const p=uf(c);s?.disconnect(),p&&s?.observe(p),o(p?t(p):null)},[t,s]),[l,d]=Qr(a);return u.useMemo(()=>({nodeRef:l,rect:n,setRef:d}),[n,l,d])}const M2=[{sensor:ha,options:{}},{sensor:tf,options:{}}],L2={current:{}},si={draggable:{measure:Up},droppable:{measure:Up,strategy:Vo.WhileDragging,frequency:ma.Optimized},dragOverlay:{measure:Ho}};class Uo extends Map{get(t){var n;return t!=null&&(n=super.get(t))!=null?n:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:n}=t;return!n})}getNodeFor(t){var n,o;return(n=(o=this.get(t))==null?void 0:o.node.current)!=null?n:void 0}}const P2={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new Uo,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:oi},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:si,measureDroppableContainers:oi,windowRect:null,measuringScheduled:!1},pf={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:oi,draggableNodes:new Map,over:null,measureDroppableContainers:oi},qo=u.createContext(pf),ff=u.createContext(P2);function O2(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Uo}}}function B2(e,t){switch(t.type){case it.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case it.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case it.DragEnd:case it.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case it.RegisterDroppable:{const{element:n}=t,{id:o}=n,r=new Uo(e.droppable.containers);return r.set(o,n),{...e,droppable:{...e.droppable,containers:r}}}case it.SetDroppableDisabled:{const{id:n,key:o,disabled:r}=t,s=e.droppable.containers.get(n);if(!s||o!==s.key)return e;const a=new Uo(e.droppable.containers);return a.set(n,{...s,disabled:r}),{...e,droppable:{...e.droppable,containers:a}}}case it.UnregisterDroppable:{const{id:n,key:o}=t,r=e.droppable.containers.get(n);if(!r||o!==r.key)return e;const s=new Uo(e.droppable.containers);return s.delete(n),{...e,droppable:{...e.droppable,containers:s}}}default:return e}}function W2(e){let{disabled:t}=e;const{active:n,activatorEvent:o,draggableNodes:r}=u.useContext(qo),s=Rr(o),a=Rr(n?.id);return u.useEffect(()=>{if(!t&&!o&&s&&a!=null){if(!la(s)||document.activeElement===s.target)return;const l=r.get(a);if(!l)return;const{activatorNode:d,node:c}=l;if(!d.current&&!c.current)return;requestAnimationFrame(()=>{for(const p of[d.current,c.current]){if(!p)continue;const f=A7(p);if(f){f.focus();break}}})}},[o,t,r,a,s]),null}function gf(e,t){let{transform:n,...o}=t;return e!=null&&e.length?e.reduce((r,s)=>s({transform:r,...o}),n):n}function j2(e){return u.useMemo(()=>({draggable:{...si.draggable,...e?.draggable},droppable:{...si.droppable,...e?.droppable},dragOverlay:{...si.dragOverlay,...e?.dragOverlay}}),[e?.draggable,e?.droppable,e?.dragOverlay])}function H2(e){let{activeNode:t,measure:n,initialRect:o,config:r=!0}=e;const s=u.useRef(!1),{x:a,y:l}=typeof r=="boolean"?{x:r,y:r}:r;sn(()=>{if(!a&&!l||!t){s.current=!1;return}if(s.current||!o)return;const c=t?.node.current;if(!c||c.isConnected===!1)return;const p=n(c),f=Kp(p,o);if(a||(f.x=0),l||(f.y=0),s.current=!0,Math.abs(f.x)>0||Math.abs(f.y)>0){const y=qp(c);y&&y.scrollBy({top:f.y,left:f.x})}},[t,a,l,o,n])}const ai=u.createContext({...Ht,scaleX:1,scaleY:1});var hn;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(hn||(hn={}));const F2=u.memo(function(t){var n,o,r,s;let{id:a,accessibility:l,autoScroll:d=!0,children:c,sensors:p=M2,collisionDetection:f=Y7,measuring:y,modifiers:g,...m}=t;const h=u.useReducer(B2,void 0,O2),[v,b]=h,[x,k]=L7(),[w,_]=u.useState(hn.Uninitialized),C=w===hn.Initialized,{draggable:{active:E,nodes:S,translate:T},droppable:{containers:$}}=v,A=E!=null?S.get(E):null,z=u.useRef({initial:null,translated:null}),D=u.useMemo(()=>{var J;return E!=null?{id:E,data:(J=A?.data)!=null?J:L2,rect:z}:null},[E,A]),O=u.useRef(null),[M,B]=u.useState(null),[N,H]=u.useState(null),L=Bo(m,Object.values(m)),I=ei("DndDescribedBy",a),P=u.useMemo(()=>$.getEnabled(),[$]),F=j2(y),{droppableRects:G,measureDroppableContainers:V,measuringScheduled:Y}=k2(P,{dragging:C,dependencies:[T.x,T.y],config:F.droppable}),Z=x2(S,E),se=u.useMemo(()=>N?ni(N):null,[N]),re=ue(),U=S2(Z,F.draggable.measure);H2({activeNode:E!=null?S.get(E):null,config:re.layoutShiftCompensation,initialRect:U,measure:F.draggable.measure});const ee=sf(Z,F.draggable.measure,U),fe=sf(Z?Z.parentElement:null),K=u.useRef({activatorEvent:null,active:null,activeNode:Z,collisionRect:null,collisions:null,droppableRects:G,draggableNodes:S,draggingNode:null,draggingNodeRect:null,droppableContainers:$,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),ne=$.getNodeFor((n=K.current.over)==null?void 0:n.id),me=z2({measure:F.dragOverlay.measure}),Ce=(o=me.nodeRef.current)!=null?o:Z,ze=C?(r=me.rect)!=null?r:ee:null,Se=!!(me.nodeRef.current&&me.rect),qe=E2(Se?null:ee),De=df(Ce?_t(Ce):null),Pe=I2(C?ne??Z:null),Qe=N2(Pe),Me=gf(g,{transform:{x:T.x-qe.x,y:T.y-qe.y,scaleX:1,scaleY:1},activatorEvent:N,active:D,activeNodeRect:ee,containerNodeRect:fe,draggingNodeRect:ze,over:K.current.over,overlayNodeRect:me.rect,scrollableAncestors:Pe,scrollableAncestorRects:Qe,windowRect:De}),Re=se?uo(se,T):null,ve=A2(Pe),te=lf(ve),he=lf(ve,[ee]),Q=uo(Me,te),ie=ze?R7(ze,Me):null,_e=D&&ie?f({active:D,collisionRect:ie,droppableRects:G,droppableContainers:P,pointerCoordinates:Re}):null,ge=q7(_e,"id"),[Ae,Ne]=u.useState(null),Le=Se?Me:uo(Me,he),q=Z7(Le,(s=Ae?.rect)!=null?s:null,ee),ae=u.useRef(null),pe=u.useCallback((J,le)=>{let{sensor:Ie,options:dt}=le;if(O.current==null)return;const pt=S.get(O.current);if(!pt)return;const ot=J.nativeEvent,Oe=new Ie({active:O.current,activeNode:pt,event:ot,options:dt,context:K,onAbort(Ye){if(!S.get(Ye))return;const{onDragAbort:Dt}=L.current,Nt={id:Ye};Dt?.(Nt),x({type:"onDragAbort",event:Nt})},onPending(Ye,Tt,Dt,Nt){if(!S.get(Ye))return;const{onDragPending:wn}=L.current,Zt={id:Ye,constraint:Tt,initialCoordinates:Dt,offset:Nt};wn?.(Zt),x({type:"onDragPending",event:Zt})},onStart(Ye){const Tt=O.current;if(Tt==null)return;const Dt=S.get(Tt);if(!Dt)return;const{onDragStart:Nt}=L.current,xn={activatorEvent:ot,active:{id:Tt,data:Dt.data,rect:z}};Cn.unstable_batchedUpdates(()=>{Nt?.(xn),_(hn.Initializing),b({type:it.DragStart,initialCoordinates:Ye,active:Tt}),x({type:"onDragStart",event:xn}),B(ae.current),H(ot)})},onMove(Ye){b({type:it.DragMove,coordinates:Ye})},onEnd:He(it.DragEnd),onCancel:He(it.DragCancel)});ae.current=Oe;function He(Ye){return async function(){const{active:Dt,collisions:Nt,over:xn,scrollAdjustedTranslate:wn}=K.current;let Zt=null;if(Dt&&wn){const{cancelDrop:Hn}=L.current;Zt={activatorEvent:ot,active:Dt,collisions:Nt,delta:wn,over:xn},Ye===it.DragEnd&&typeof Hn=="function"&&await Promise.resolve(Hn(Zt))&&(Ye=it.DragCancel)}O.current=null,Cn.unstable_batchedUpdates(()=>{b({type:Ye}),_(hn.Uninitialized),Ne(null),B(null),H(null),ae.current=null;const Hn=Ye===it.DragEnd?"onDragEnd":"onDragCancel";if(Zt){const ln=L.current[Hn];ln?.(Zt),x({type:Hn,event:Zt})}})}}},[S]),$e=u.useCallback((J,le)=>(Ie,dt)=>{const pt=Ie.nativeEvent,ot=S.get(dt);if(O.current!==null||!ot||pt.dndKit||pt.defaultPrevented)return;const Oe={active:ot};J(Ie,le.options,Oe)===!0&&(pt.dndKit={capturedBy:le.sensor},O.current=dt,pe(Ie,le))},[S,pe]),be=w2(p,$e);T2(p),sn(()=>{ee&&w===hn.Initializing&&_(hn.Initialized)},[ee,w]),u.useEffect(()=>{const{onDragMove:J}=L.current,{active:le,activatorEvent:Ie,collisions:dt,over:pt}=K.current;if(!le||!Ie)return;const ot={active:le,activatorEvent:Ie,collisions:dt,delta:{x:Q.x,y:Q.y},over:pt};Cn.unstable_batchedUpdates(()=>{J?.(ot),x({type:"onDragMove",event:ot})})},[Q.x,Q.y]),u.useEffect(()=>{const{active:J,activatorEvent:le,collisions:Ie,droppableContainers:dt,scrollAdjustedTranslate:pt}=K.current;if(!J||O.current==null||!le||!pt)return;const{onDragOver:ot}=L.current,Oe=dt.get(ge),He=Oe&&Oe.rect.current?{id:Oe.id,rect:Oe.rect.current,data:Oe.data,disabled:Oe.disabled}:null,Ye={active:J,activatorEvent:le,collisions:Ie,delta:{x:pt.x,y:pt.y},over:He};Cn.unstable_batchedUpdates(()=>{Ne(He),ot?.(Ye),x({type:"onDragOver",event:Ye})})},[ge]),sn(()=>{K.current={activatorEvent:N,active:D,activeNode:Z,collisionRect:ie,collisions:_e,droppableRects:G,draggableNodes:S,draggingNode:Ce,draggingNodeRect:ze,droppableContainers:$,over:Ae,scrollableAncestors:Pe,scrollAdjustedTranslate:Q},z.current={initial:ze,translated:ie}},[D,Z,_e,ie,S,Ce,ze,G,$,Ae,Pe,Q]),m2({...re,delta:T,draggingRect:ie,pointerCoordinates:Re,scrollableAncestors:Pe,scrollableAncestorRects:Qe});const j=u.useMemo(()=>({active:D,activeNode:Z,activeNodeRect:ee,activatorEvent:N,collisions:_e,containerNodeRect:fe,dragOverlay:me,draggableNodes:S,droppableContainers:$,droppableRects:G,over:Ae,measureDroppableContainers:V,scrollableAncestors:Pe,scrollableAncestorRects:Qe,measuringConfiguration:F,measuringScheduled:Y,windowRect:De}),[D,Z,ee,N,_e,fe,me,S,$,G,Ae,V,Pe,Qe,F,Y,De]),oe=u.useMemo(()=>({activatorEvent:N,activators:be,active:D,activeNodeRect:ee,ariaDescribedById:{draggable:I},dispatch:b,draggableNodes:S,over:Ae,measureDroppableContainers:V}),[N,be,D,ee,b,I,S,Ae,V]);return u.createElement(Fp.Provider,{value:k},u.createElement(qo.Provider,{value:oe},u.createElement(ff.Provider,{value:j},u.createElement(ai.Provider,{value:q},c)),u.createElement(W2,{disabled:l?.restoreFocus===!1})),u.createElement(B7,{...l,hiddenTextDescribedById:I}));function ue(){const J=M?.autoScrollEnabled===!1,le=typeof d=="object"?d.enabled===!1:d===!1,Ie=C&&!J&&!le;return typeof d=="object"?{...d,enabled:Ie}:{enabled:Ie}}}),K2=u.createContext(null),hf="button",V2="Draggable";function U2(e){let{id:t,data:n,disabled:o=!1,attributes:r}=e;const s=ei(V2),{activators:a,activatorEvent:l,active:d,activeNodeRect:c,ariaDescribedById:p,draggableNodes:f,over:y}=u.useContext(qo),{role:g=hf,roleDescription:m="draggable",tabIndex:h=0}=r??{},v=d?.id===t,b=u.useContext(v?ai:K2),[x,k]=Qr(),[w,_]=Qr(),C=D2(a,t),E=Bo(n);sn(()=>(f.set(t,{id:t,key:s,node:x,activatorNode:w,data:E}),()=>{const T=f.get(t);T&&T.key===s&&f.delete(t)}),[f,t]);const S=u.useMemo(()=>({role:g,tabIndex:h,"aria-disabled":o,"aria-pressed":v&&g===hf?!0:void 0,"aria-roledescription":m,"aria-describedby":p.draggable}),[o,g,h,v,m,p.draggable]);return{active:d,activatorEvent:l,activeNodeRect:c,attributes:S,isDragging:v,listeners:o?void 0:C,node:x,over:y,setNodeRef:k,setActivatorNodeRef:_,transform:b}}function q2(){return u.useContext(ff)}const G2="Droppable",Y2={timeout:25};function X2(e){let{data:t,disabled:n=!1,id:o,resizeObserverConfig:r}=e;const s=ei(G2),{active:a,dispatch:l,over:d,measureDroppableContainers:c}=u.useContext(qo),p=u.useRef({disabled:n}),f=u.useRef(!1),y=u.useRef(null),g=u.useRef(null),{disabled:m,updateMeasurementsFor:h,timeout:v}={...Y2,...r},b=Bo(h??o),x=u.useCallback(()=>{if(!f.current){f.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{c(Array.isArray(b.current)?b.current:[b.current]),g.current=null},v)},[v]),k=ii({callback:x,disabled:m||!a}),w=u.useCallback((S,T)=>{k&&(T&&(k.unobserve(T),f.current=!1),S&&k.observe(S))},[k]),[_,C]=Qr(w),E=Bo(t);return u.useEffect(()=>{!k||!_.current||(k.disconnect(),f.current=!1,k.observe(_.current))},[_,k]),u.useEffect(()=>(l({type:it.RegisterDroppable,element:{id:o,key:s,disabled:n,node:_,rect:y,data:E}}),()=>l({type:it.UnregisterDroppable,key:s,id:o})),[o]),u.useEffect(()=>{n!==p.current.disabled&&(l({type:it.SetDroppableDisabled,id:o,key:s,disabled:n}),p.current.disabled=n)},[o,s,n,l]),{active:a,rect:y,isOver:d?.id===o,node:_,over:d,setNodeRef:C}}function J2(e){let{animation:t,children:n}=e;const[o,r]=u.useState(null),[s,a]=u.useState(null),l=Rr(n);return!n&&!o&&l&&r(l),sn(()=>{if(!s)return;const d=o?.key,c=o?.props.id;if(d==null||c==null){r(null);return}Promise.resolve(t(c,s)).then(()=>{r(null)})},[t,o,s]),u.createElement(u.Fragment,null,n,o?u.cloneElement(o,{ref:a}):null)}const Z2={x:0,y:0,scaleX:1,scaleY:1};function Q2(e){let{children:t}=e;return u.createElement(qo.Provider,{value:pf},u.createElement(ai.Provider,{value:Z2},t))}const R2={position:"fixed",touchAction:"none"},eI=e=>la(e)?"transform 250ms ease":void 0,tI=u.forwardRef((e,t)=>{let{as:n,activatorEvent:o,adjustScale:r,children:s,className:a,rect:l,style:d,transform:c,transition:p=eI}=e;if(!l)return null;const f=r?c:{...c,scaleX:1,scaleY:1},y={...R2,width:l.width,height:l.height,top:l.top,left:l.left,transform:jo.Transform.toString(f),transformOrigin:r&&o?F7(o,l):void 0,transition:typeof p=="function"?p(o):p,...d};return u.createElement(n,{className:a,style:y,ref:t},s)}),nI={duration:250,easing:"ease",keyframes:e=>{let{transform:{initial:t,final:n}}=e;return[{transform:jo.Transform.toString(t)},{transform:jo.Transform.toString(n)}]},sideEffects:(e=>t=>{let{active:n,dragOverlay:o}=t;const r={},{styles:s,className:a}=e;if(s!=null&&s.active)for(const[l,d]of Object.entries(s.active))d!==void 0&&(r[l]=n.node.style.getPropertyValue(l),n.node.style.setProperty(l,d));if(s!=null&&s.dragOverlay)for(const[l,d]of Object.entries(s.dragOverlay))d!==void 0&&o.node.style.setProperty(l,d);return a!=null&&a.active&&n.node.classList.add(a.active),a!=null&&a.dragOverlay&&o.node.classList.add(a.dragOverlay),function(){for(const[d,c]of Object.entries(r))n.node.style.setProperty(d,c);a!=null&&a.active&&n.node.classList.remove(a.active)}})({styles:{active:{opacity:"0"}}})};function oI(e){let{config:t,draggableNodes:n,droppableContainers:o,measuringConfiguration:r}=e;return Zr((s,a)=>{if(t===null)return;const l=n.get(s);if(!l)return;const d=l.node.current;if(!d)return;const c=uf(a);if(!c)return;const{transform:p}=_t(a).getComputedStyle(a),f=Vp(p);if(!f)return;const y=typeof t=="function"?t:rI(t);return Qp(d,r.draggable.measure),y({active:{id:s,data:l.data,node:d,rect:r.draggable.measure(d)},draggableNodes:n,dragOverlay:{node:a,rect:r.dragOverlay.measure(c)},droppableContainers:o,measuringConfiguration:r,transform:f})})}function rI(e){const{duration:t,easing:n,sideEffects:o,keyframes:r}={...nI,...e};return s=>{let{active:a,dragOverlay:l,transform:d,...c}=s;if(!t)return;const p={x:l.rect.left-a.rect.left,y:l.rect.top-a.rect.top},f={scaleX:d.scaleX!==1?a.rect.width*d.scaleX/l.rect.width:1,scaleY:d.scaleY!==1?a.rect.height*d.scaleY/l.rect.height:1},y={x:d.x-p.x,y:d.y-p.y,...f},g=r({...c,active:a,dragOverlay:l,transform:{initial:d,final:y}}),[m]=g,h=g[g.length-1];if(JSON.stringify(m)===JSON.stringify(h))return;const v=o?.({active:a,dragOverlay:l,...c}),b=l.node.animate(g,{duration:t,easing:n,fill:"forwards"});return new Promise(x=>{b.onfinish=()=>{v?.(),x()}})}}let yf=0;function iI(e){return u.useMemo(()=>{if(e!=null)return yf++,yf},[e])}const sI=u.memo(e=>{let{adjustScale:t=!1,children:n,dropAnimation:o,style:r,transition:s,modifiers:a,wrapperElement:l="div",className:d,zIndex:c=999}=e;const{activatorEvent:p,active:f,activeNodeRect:y,containerNodeRect:g,draggableNodes:m,droppableContainers:h,dragOverlay:v,over:b,measuringConfiguration:x,scrollableAncestors:k,scrollableAncestorRects:w,windowRect:_}=q2(),C=u.useContext(ai),E=iI(f?.id),S=gf(a,{activatorEvent:p,active:f,activeNodeRect:y,containerNodeRect:g,draggingNodeRect:v.rect,over:b,overlayNodeRect:v.rect,scrollableAncestors:k,scrollableAncestorRects:w,transform:C,windowRect:_}),T=va(y),$=oI({config:o,draggableNodes:m,droppableContainers:h,measuringConfiguration:x}),A=T?v.setRef:void 0;return u.createElement(Q2,null,u.createElement(J2,{animation:$},f&&E?u.createElement(tI,{key:E,id:f.id,ref:A,as:l,activatorEvent:p,adjustScale:t,className:d,transition:s,rect:T,style:{zIndex:c,...r},transform:S},n):null))});function Go(e,t){return!t||!Array.isArray(t.draggable)?!1:t.draggable.includes(e)}function aI({draggedComponentName:e,targetContainerName:t,nesting:n}){if(!n||!Go(e,n))return!1;const o=n.containers;if(!o||typeof o!="object")return!1;const r=o[t];return!r||!Array.isArray(r.accepts)?!1:r.accepts.includes(e)}function bf(e,t){if(!t?.containers)return!1;const n=t.containers[e];return!!(n&&Array.isArray(n.accepts)&&n.accepts.length>0)}function lI(e,t,n,o,r){if(!n||n.length===0)return{parentPath:[],index:r};if(e&&bf(e,o))return{parentPath:n,index:t};const s=n.slice(0,-1),a=n[n.length-1]??0;return{parentPath:s,index:a+1}}function mf(e,t){return e===t||e.startsWith(`${t}.`)}function vf(e,t){if(e==="")return/^\d+$/.test(t)?Number(t):null;const n=`${e}.children.`;if(!t.startsWith(n))return null;const o=t.slice(n.length);return/^\d+$/.test(o)?Number(o):null}function dI(e,t){const n=[];return e.querySelectorAll("[data-editor-path]").forEach(o=>{if(o.hasAttribute("data-dnd-placeholder")||o.closest("[data-dnd-placeholder]"))return;const r=o.dataset.editorPath;typeof r=="string"&&n.push({path:t(r),domPath:r,rect:o.getBoundingClientRect()})}),n}const cI=16;function uI(e){const{frameEl:t,draggedPath:n,acceptsContainer:o,allowsNestSlot:r,includeContainer:s}=e,a=e.normalizeContainerPath??(g=>g),l=t.getBoundingClientRect(),d=dI(t,a),c=new Map;for(const g of d)c.has(g.path)||c.set(g.path,g.domPath);const p=a(n),f=new Set([""]);for(const g of d){const m=fI(g.path);m!==null&&f.add(m)}for(const g of d)f.add(g.path);const y=[];for(const g of f){const m=g==="",h=m?null:c.get(g)??g,v=m?t:t.querySelector(`[data-editor-path="${wf(h)}"]`);if(!v&&!m||!m&&mf(g,p))continue;const b=g;if(!m&&s&&!s(b))continue;const x=C=>{const E=c.get(C);if(!E||typeof window>"u")return!1;const S=t.querySelector(`[data-editor-path="${wf(E)}"]`);return S?(window.getComputedStyle(S).display??"").toLowerCase()==="contents":!1},k=d.filter(C=>vf(g,C.path)!==null).filter(C=>!mf(C.path,p)).filter(C=>!x(C.path)).map(C=>({origIndex:vf(g,C.path),rect:pI(C.rect,C.path,d)})).filter(C=>C.rect.width>0||C.rect.height>0).sort((C,E)=>C.origIndex-E.origIndex),w=Dp(m?t:v).kind,_=w==="flex_row_single"||w==="flex_row_wrap"||w==="grid_single_row"||w==="grid_2d";if(k.length>0){if(o(b)){k.forEach(E=>{y.push(_f(b,E.origIndex,E.rect,l,_,"before"))});const C=k[k.length-1];y.push(_f(b,C.origIndex+1,C.rect,l,_,"after"))}}else if(!m&&r(b)&&v){const C=v.getBoundingClientRect();C.width>0&&C.height>0&&y.push({id:`slot:${b}:0`,kind:"nest",containerPath:b,index:0,box:xf(C,l),orientation:"area"})}else m&&y.push({id:"slot::0",kind:"gap",containerPath:"",index:0,box:xf(l,l),orientation:"area"})}return y.filter(g=>ao(g.box,t))}function _f(e,t,n,o,r,s){const a=cI;let l;if(r)l={left:(s==="before"?n.left:n.right)-a/2,top:n.top,width:a,height:n.height};else{const d=s==="before"?n.top:n.bottom;l={left:n.left,top:d-a/2,width:n.width,height:a}}return{id:`slot:${e}:${t}`,kind:"gap",containerPath:e,index:t,box:{left:l.left-o.left,top:l.top-o.top,width:l.width,height:l.height,scale:1},orientation:r?"vertical":"horizontal"}}function pI(e,t,n){if(e.width>0||e.height>0)return e;const o=`${t}.children.`;let r=1/0,s=1/0,a=-1/0,l=-1/0;for(const d of n)d.path.startsWith(o)&&(d.rect.width===0&&d.rect.height===0||(r=Math.min(r,d.rect.left),s=Math.min(s,d.rect.top),a=Math.max(a,d.rect.right),l=Math.max(l,d.rect.bottom)));return r===1/0?e:{left:r,top:s,width:a-r,height:l-s,right:a,bottom:l,x:r,y:s,toJSON:()=>({})}}function xf(e,t){return{left:e.left-t.left,top:e.top-t.top,width:e.width,height:e.height,scale:1}}function fI(e){if(e==="")return null;const t=e.lastIndexOf(".children.");return t<0?"":e.slice(0,t)}function gI(e){if(!e.startsWith("slot:"))return null;const t=e.slice(5),n=t.lastIndexOf(":");if(n<0)return null;const o=t.slice(0,n),r=Number(t.slice(n+1));return Number.isFinite(r)?{containerPath:o,index:r}:null}function wf(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function _a(e){return typeof e=="string"&&e.length>0?e:null}function xa(e,t){return t&&e.startsWith(`${t}.`)?t:e}function kf(e){if(typeof e!="string")return null;const t=gI(e);return t?{containerPath:t.containerPath,index:t.index}:null}function wa(e,t){return t?e.startsWith(`${t}.iteration.`):!1}function ka(e,t,n){return t?n==="iteration_item"?wa(e,t):e===t||e.startsWith(`${t}.`):!1}function hI(e){const{frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onMovePath:c,selectedPath:p,showBranchBoundaryHint:f}=e,y=(o==="iteration_item"||o==="modal")&&s&&s.length>0?at(s):null,[g,m]=u.useState(null),[h,v]=u.useState(null),[b,x]=u.useState(null),k=u.useRef({components:a,nesting:n,editMode:o,currentExtensionId:r,selectedPath:p,editableRootSourcePath:y});k.current={components:a,nesting:n,editMode:o,currentExtensionId:r,selectedPath:p,editableRootSourcePath:y};const w=u.useRef(null),_=u.useCallback(M=>({children:M}),[]),C=u.useCallback((M,B)=>{const N=We(_(M),B);return N?typeof N.name=="string"?N.name:typeof N.type=="string"?N.type:null:null},[_]),E=u.useCallback(M=>B=>{const N=k.current;if(B==="")return Go(M,N.nesting);const H=Ze(B),L=We(_(N.components),H);if(!L)return!1;const I=typeof L.name=="string"?L.name:typeof L.type=="string"?L.type:"";if(!I)return!1;const P=!!N.editableRootSourcePath&&(B===N.editableRootSourcePath||B.startsWith(`${N.editableRootSourcePath}.`));if(N.editableRootSourcePath&&!P)return!1;if(!P){const F=Sa(_(N.components),H);if(gn(L,N.editMode,N.currentExtensionId,F)!=="none")return!1}return bf(I,N.nesting)?aI({draggedComponentName:M,targetContainerName:I,nesting:N.nesting}):!1},[_]),S=u.useCallback(M=>{if(M==="")return!0;const B=k.current,N=Ze(M),H=w.current;if(H&&H.length>0){const P=H.slice(0,-1);if(li(N,P))return!0}const L=We(_(B.components),N);return L?(typeof L.name=="string"?L.name:typeof L.type=="string"?L.type:"").length>0:!1},[_]),T=u.useCallback(M=>{const B=E(M),N=k.current,H=w.current,L=H?H.slice(0,-1):null,I=new Set;if(L)for(let V=L.length;V>=0;V--)I.add(di(L.slice(0,V)));const P=di(H??[]),F=V=>!!H&&(V===P||V.startsWith(`${P}.`)),G=(V,Y)=>{for(let Z=0;Z<V.length;Z++){const se=[...Y,Z],re=di(se);!I.has(re)&&!F(re)&&B(re)&&S(re)&&I.add(re);const U=V[Z];if(U&&Array.isArray(U.children)&&G(U.children,se),U&&U.responsive&&typeof U.responsive=="object")for(const ee of Object.keys(U.responsive)){const fe=U.responsive[ee];fe&&Array.isArray(fe.children)&&G(fe.children,[...se,{responsive:ee}])}}};return G(N.components,[]),{acceptsContainer:B,includeContainer:V=>V===""?!0:F(V)?!1:I.has(V),allowsNestSlot:V=>{if(!B(V))return!1;if(L){const Y=Ze(V);if(li(Y,L))return!1}return S(V)}}},[E,S,_]),$=u.useCallback(M=>{const B=_a(M.active.id),N=k.current;if(!B)return;const H=xa(B,N.selectedPath),L=Ze(H),I=We(_(N.components),L),P=I&&typeof I.name=="string"?I.name:I&&typeof I.type=="string"?I.type:null;m(H),v(P),x(null),w.current=I?L:null;let F="allowed";if(!P||!Go(P,N.nesting))F="denied_no_draggable";else if(I){const G=Sa(_(N.components),L),V=gn(I,N.editMode,N.currentExtensionId,G);N.editableRootSourcePath&&!ka(H,N.editableRootSourcePath,N.editMode)||Xi(G)&&!wa(H,N.editableRootSourcePath)?F="denied_data_bound":V==="base"||V==="partial"?F="denied_base_locked":(V==="extension"||V==="extension_point")&&(F="denied_extension_locked")}$n({source:"drag",draggedComponentName:P,targetContainerName:null,targetContainerPath:null,decision:F,result:F==="allowed"?"completed":"denied",timestamp:Date.now()})},[_]),A=u.useCallback(M=>{if(!t)return;const B=_a(M.active.id);if(!B)return;const N=h;if(!N){x(null);return}const H=k.current,L=xa(B,H.selectedPath),I=Ze(L),P=We(_(H.components),I);if(!P||!Go(N,H.nesting)){x(null);return}const F=Sa(_(H.components),I);if(H.editableRootSourcePath&&!ka(L,H.editableRootSourcePath,H.editMode)){x(null);return}const G=gn(P,H.editMode,H.currentExtensionId,F),V=wa(L,H.editableRootSourcePath);if(Xi(F)&&!V||G!=="none"&&G!=="data_bound"&&!V){x(null);return}x(kf(M.over?.id))},[t,h,_]),z=u.useCallback(()=>{m(null),v(null),x(null),w.current=null},[]),D=u.useCallback(M=>{const B=_a(M.active.id),N=h,H=k.current,L=B?xa(B,H.selectedPath):null;if(!t||!L||!N){z();return}const I=kf(M.over?.id),P=Ze(L);if(!I){$n({source:"drag",draggedComponentName:N,targetContainerName:null,targetContainerPath:null,decision:"denied_no_accepts",result:"cancelled",timestamp:Date.now()}),z();return}const F=I.containerPath===""?[]:Ze(I.containerPath),G=I.containerPath===""?null:C(H.components,F);if(!yI(P.slice(0,-1),F)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"denied_responsive_branch_boundary",result:"denied",timestamp:Date.now()}),f?.(),z();return}if(H.editableRootSourcePath&&!ka(I.containerPath,H.editableRootSourcePath,H.editMode)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"denied_data_bound",result:"denied",timestamp:Date.now()}),z();return}const V=P.slice(0,-1),Y=P[P.length-1]??-1;if(li(V,F)&&(I.index===Y||I.index===Y+1)){$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"allowed",insertionIndex:I.index,result:"cancelled",timestamp:Date.now()}),z();return}const re=li(V,F),U=I.index;let ee=[];l(me=>{const Se=Db({children:me},P,F,U).children??[];return ee=Se,Se}),d(ee,`move ${N}`);const fe=id(F,P),K=re&&Y<U?U-1:U,ne=di([...fe,K]);c?.(ne),$n({source:"drag",draggedComponentName:N,targetContainerName:G,targetContainerPath:I.containerPath||null,decision:"allowed",insertionIndex:I.index,result:"completed",timestamp:Date.now()}),z()},[t,h,E,S,C,l,d,z,c]),O=u.useCallback(()=>{z()},[z]);return{activeDragPath:g,activeDragName:h,activeDropZone:b,onDragStart:$,onDragMove:A,onDragEnd:D,onDragCancel:O,buildSlotPredicates:T}}function Sa(e,t){const n=[];let o=e,r=Array.isArray(e.children)?e.children:[];for(let s=0;s<t.length-1;s++){const a=t[s];if(et(a)){const d=o.responsive?.[a.responsive];r=d&&Array.isArray(d.children)?d.children:[];continue}const l=r[a]??null;if(!l)break;n.push(l),o=l,r=Array.isArray(l.children)?l.children:[]}return n}function li(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(!fr(e[n],t[n]))return!1;return!0}function yI(e,t){const n=e.filter(et),o=t.filter(et);if(n.length!==o.length)return!1;for(let r=0;r<n.length;r++)if(n[r].responsive!==o[r].responsive)return!1;return!0}function di(e){return at(e)}function bI(e){const t=/^(.*?)\.iteration\.\d+(?:\.|$)/.exec(e);return t?t[1]:null}function mI(e){const t=Ze(e);return at(t)}function Yo(e,t){return t?e.startsWith(`${t}.iteration.`):!1}function vI(e,t,n){return t?n==="iteration_item"?Yo(e,t):e===t||e.startsWith(`${t}.`):!1}function _I(e){if(e.length===0)return null;let t=1/0,n=1/0,o=-1/0,r=-1/0;for(const s of e)t=Math.min(t,s.left),n=Math.min(n,s.top),o=Math.max(o,s.left+s.width),r=Math.max(r,s.top+s.height);return{left:t,top:n,width:o-t,height:r-n,scale:e[0].scale}}function xI(e){const{frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onSelectPath:c,onMovePath:p,onRequestInlineEdit:f,selectedPath:y}=e,g=(o==="iteration_item"||o==="modal")&&s&&s.length>0?at(s):null,{t:m}=tt(),[h,v]=u.useState(!1),b=u.useRef(null),x=u.useCallback(()=>{v(!0),b.current&&clearTimeout(b.current),b.current=setTimeout(()=>v(!1),2600)},[]);u.useEffect(()=>()=>{b.current&&clearTimeout(b.current)},[]);const k=hI({frameEl:t,nesting:n,editMode:o,currentExtensionId:r,editableRootPath:s,components:a,patchLayout:l,pushHistory:d,onMovePath:p,selectedPath:y,showBranchBoundaryHint:x}),w=j7(W7(ha,{activationConstraint:{distance:8}})),[_,C]=u.useState([]),E=u.useCallback(()=>{if(!t){C([]);return}if(k.activeDragPath!==null)return;const L={children:a},I=t.querySelectorAll("[data-editor-path]"),P=[],F=new Map;I.forEach(G=>{const V=G.dataset.editorPath;if(typeof V!="string"||o!=="iteration_item"&&g&&!vI(V,g,o))return;const Y=bI(V);if(Y!==null&&!Yo(V,g)){if(/\.iteration\.\d+$/.test(V)){const K=so(G,t);if(K&&ao(K,t)){const ne=F.get(Y)??[];ne.push(K),F.set(Y,ne)}}return}if(Yo(V,g)&&/\.iteration\.\d+$/.test(V))return;const Z=Ze(V),se=We(L,Z);if(!se||se.iteration&&se.iteration.source!==void 0)return;const re=typeof se.name=="string"?se.name:typeof se.type=="string"?se.type:null;if(!re||!Go(re,n))return;const U=Cf(L,Z);if(Xi(U)&&!Yo(V,g))return;const ee=gn(se,o,r,U);if(ee!=="none"&&ee!=="data_bound"&&!Yo(V,g))return;const fe=so(G,t);fe&&ao(fe,t)&&P.push({path:V,name:re,box:fe})});for(const[G,V]of F){const Y=Ze(G),Z=We(L,Y);if(!Z)continue;const se=typeof Z.name=="string"?Z.name:typeof Z.type=="string"?Z.type:null;if(!se)continue;const re=Cf(L,Y),U=gn(Z,o,r,re);if(U!=="none"&&U!=="data_bound")continue;const ee=_I(V);ee&&ao(ee,t)&&P.push({path:G,name:se,box:ee,isIterationGroup:!0})}C(P)},[t,a,n,o,r,g,k.activeDragPath]);u.useEffect(()=>{E()},[E]),u.useEffect(()=>{if(t)return $p(E,t)},[t,E]);const S=k.activeDragPath!==null,T=k.activeDragName,$=k.activeDragPath,[A,z]=u.useState(null),D=u.useRef(null);u.useEffect(()=>{if(!$||!t){$||(D.current=null,z(null));return}if(D.current===$)return;D.current=$;const L=t.querySelector(`[data-editor-path="${Sf($)}"]`);if(L){const re=L.getBoundingClientRect();z({node:L.cloneNode(!0),width:re.width,height:re.height});return}const I=Array.from(t.querySelectorAll(`[data-editor-path^="${Sf($)}.iteration."]`)).filter(re=>/\.iteration\.\d+$/.test(re.dataset.editorPath??""));if(I.length===0){z(null);return}let P=1/0,F=1/0,G=-1/0,V=-1/0;for(const re of I){const U=re.getBoundingClientRect();P=Math.min(P,U.left),F=Math.min(F,U.top),G=Math.max(G,U.right),V=Math.max(V,U.bottom)}const Y=G-P,Z=V-F,se=document.createElement("div");se.style.position="relative",se.style.width=`${Y}px`,se.style.height=`${Z}px`;for(const re of I){const U=re.getBoundingClientRect(),ee=re.cloneNode(!0);ee.style.position="absolute",ee.style.left=`${U.left-P}px`,ee.style.top=`${U.top-F}px`,ee.style.width=`${U.width}px`,ee.style.height=`${U.height}px`,ee.style.margin="0",se.appendChild(ee)}z({node:se,width:Y,height:Z})},[$,t]);const[O,M]=u.useState([]),B=u.useRef(null),N=u.useRef(k.buildSlotPredicates);if(N.current=k.buildSlotPredicates,u.useEffect(()=>{if(!$||!T||!t){B.current=null,M(F=>F.length===0?F:[]);return}if(B.current===$)return;B.current=$;const{acceptsContainer:L,allowsNestSlot:I,includeContainer:P}=N.current(T);M(uI({frameEl:t,draggedPath:$,acceptsContainer:L,allowsNestSlot:I,includeContainer:P,normalizeContainerPath:F=>mI(F)}))},[$,T,t]),!t)return null;const H=k.activeDropZone?`slot:${k.activeDropZone.containerPath}:${k.activeDropZone.index}`:null;return i.jsxs(F2,{sensors:w,collisionDetection:J7,onDragStart:k.onDragStart,onDragMove:k.onDragMove,onDragEnd:k.onDragEnd,onDragCancel:k.onDragCancel,children:[_.map(L=>i.jsx(SI,{path:L.path,name:L.name,box:L.box,dimmed:S&&k.activeDragPath!==L.path,isIterationGroup:L.isIterationGroup,onSelect:c,onRequestInlineEdit:f},L.path)),S&&O.map(L=>i.jsx(kI,{slot:L,active:L.id===H},L.id)),Cn.createPortal(i.jsx(sI,{dropAnimation:null,zIndex:Pp,children:T?A?i.jsx(wI,{name:T,node:A.node,width:A.width,height:A.height}):i.jsx("div",{className:"g7le-dnd-drag-ghost","data-testid":"g7le-dnd-drag-ghost",style:{padding:"4px 10px",background:"rgba(79,70,229,0.95)",color:"#fff",fontSize:12,fontWeight:600,borderRadius:4,pointerEvents:"none",whiteSpace:"nowrap"},children:`<${T}>`}):null}),document.body),h&&Cn.createPortal(i.jsxs("div",{"data-testid":"g7le-branch-boundary-hint",role:"status",style:{position:"absolute",left:"50%",top:12,transform:"translateX(-50%)",background:"rgba(245,158,11,0.97)",color:"#fff",fontSize:13,fontWeight:600,padding:"8px 16px",borderRadius:8,boxShadow:"0 4px 14px rgba(0,0,0,0.2)",pointerEvents:"none",whiteSpace:"nowrap",zIndex:Pp},children:["⚠️ ",m("layout_editor.overlay.branch_boundary_denied")]}),t)]})}function wI(e){const{node:t,width:n,height:o}=e,r=u.useRef(null);return u.useEffect(()=>{const s=r.current;if(s)return s.replaceChildren(),s.appendChild(t.cloneNode(!0)),()=>{s.replaceChildren()}},[t]),i.jsx("div",{ref:r,className:"g7le-dnd-drag-ghost","data-testid":"g7le-dnd-drag-ghost",style:{width:n,height:o,opacity:.7,pointerEvents:"none",boxShadow:"0 6px 18px rgba(0,0,0,0.25)",borderRadius:4,overflow:"hidden",cursor:"grabbing"}})}function kI(e){const{slot:t,active:n}=e,{setNodeRef:o}=X2({id:t.id}),r=t.orientation!=="area";return i.jsx("div",{ref:o,"data-testid":`g7le-dnd-slot-${t.id}`,"data-dnd-slot-id":t.id,style:{position:"absolute",left:t.box.left,top:t.box.top,width:t.box.width,height:t.box.height,pointerEvents:"auto",zIndex:h7,boxSizing:"border-box",...r?{background:n?"rgba(79,70,229,0.15)":"transparent",...n?t.orientation==="vertical"?{borderLeft:"2px solid #4f46e5"}:{borderTop:"2px solid #4f46e5"}:{}}:{border:n?"2px dashed #4f46e5":"1px dashed transparent",background:n?"rgba(79,70,229,0.08)":"transparent",borderRadius:4}}})}function Sf(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function SI(e){const{path:t,box:n,dimmed:o,isIterationGroup:r,onSelect:s,onRequestInlineEdit:a}=e,{attributes:l,listeners:d,setNodeRef:c,isDragging:p}=U2({id:t}),f=(t.match(/\.children\./g)||[]).length,y=y7(f);return i.jsx("div",{ref:c,"data-testid":`g7le-dnd-handle-${t}`,"data-dnd-handle-path":t,"data-dnd-iteration-group":r?"true":void 0,...d,...l,onClick:g=>{if(g.stopPropagation(),!r&&typeof document<"u"&&typeof document.elementsFromPoint=="function"){const m=document.elementsFromPoint(g.clientX,g.clientY);for(const h of m){const b=h.closest?.("[data-editor-path]")?.dataset?.editorPath;if(typeof b=="string"&&(b===t||b.startsWith(`${t}.`))){s(b);return}}}s(t)},onDoubleClick:g=>{g.stopPropagation(),g.preventDefault(),a?.(t)},style:{position:"absolute",left:n.left,top:n.top,width:n.width,height:n.height,cursor:"grab",pointerEvents:"auto",background:p?"rgba(79,70,229,0.06)":"transparent",...r?{border:"1px dashed rgba(79,70,229,0.5)",borderRadius:4}:{},opacity:o?.6:1,zIndex:y,touchAction:"none"}})}function Cf(e,t){const n=[];let o=e,r=Array.isArray(e.children)?e.children:[];for(let s=0;s<t.length-1;s++){const a=t[s];if(et(a)){const d=o.responsive?.[a.responsive];r=d&&Array.isArray(d.children)?d.children:[];continue}const l=r[a]??null;if(!l)break;n.push(l),o=l,r=Array.isArray(l.children)?l.children:[]}return n}function CI(e,t,n,o){if(t==="route")return[];if(t==="modal"||t==="iteration_item")return o&&o.length>0?[o]:[];const r=[],s=(a,l)=>{a.forEach((d,c)=>{const p=[...l,c];if(!Ji(d,t,n)){r.push(p);return}const y=Array.isArray(d.children)?d.children:[];y.length>0&&s(y,p)})};return s(e,[]),r}function $I(e){return at(e)}function ci(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/["\\]/g,"\\$&")}function EI({frameEl:e,components:t,editMode:n,currentExtensionId:o,editableRootPath:r}){const[s,a]=u.useState(null),[l]=u.useState(()=>`g7le-dim-mask-${Math.floor(Math.random()*1e9)}`);if(u.useEffect(()=>{if(!e){a(null);return}let g=0;const m=()=>{const x=CI(t,n,o,r);if(x.length===0){a(null);return}const k=e.getBoundingClientRect(),w={left:0,top:0,width:k.width,height:k.height},_=[];for(const C of x){const E=$I(C),S=e.querySelector(`[data-editor-path="${ci(E)}"]`);let T=null;if(S){const $=S.getBoundingClientRect();$.width>0&&$.height>0&&(T={left:$.left,top:$.top,width:$.width,height:$.height})}if(!T){const $=Array.from(e.querySelectorAll(`[data-editor-path^="${ci(E)}.iteration."], [data-editor-path^="${ci(E)}.sortable."], [data-editor-path^="${ci(E)}.children."]`));let A=1/0,z=1/0,D=-1/0,O=-1/0;for(const M of $){const B=M.getBoundingClientRect();B.width<=0||B.height<=0||(A=Math.min(A,B.left),z=Math.min(z,B.top),D=Math.max(D,B.right),O=Math.max(O,B.bottom))}if(D>A&&O>z&&(T={left:A,top:z,width:D-A,height:O-z}),T&&$.length>0){const M=$[0].closest('[role="dialog"]');if(M&&e.contains(M)){const B=M.getBoundingClientRect();B.width>0&&B.height>0&&(T={left:B.left,top:B.top,width:B.width,height:B.height})}}}T&&_.push({left:T.left-k.left,top:T.top-k.top,width:T.width,height:T.height})}if(_.length===0){a(null);return}a({frame:w,holes:_})},h=()=>{g&&cancelAnimationFrame(g),g=requestAnimationFrame(m)};h();const v=typeof ResizeObserver<"u"?new ResizeObserver(h):null;v?.observe(e);const b=typeof MutationObserver<"u"?new MutationObserver(h):null;return b?.observe(e,{childList:!0,subtree:!0,attributes:!0}),window.addEventListener("scroll",h,!0),window.addEventListener("resize",h),()=>{g&&cancelAnimationFrame(g),v?.disconnect(),b?.disconnect(),window.removeEventListener("scroll",h,!0),window.removeEventListener("resize",h)}},[e,t,n,o,r?.join(".")??""]),!s||s.holes.length===0)return null;const{frame:d,holes:c}=s,p=n==="modal"||n==="iteration_item";let f=[];if(p){let g=1/0,m=1/0,h=-1/0,v=-1/0;for(const C of c)g=Math.min(g,C.left),m=Math.min(m,C.top),h=Math.max(h,C.left+C.width),v=Math.max(v,C.top+C.height);const b=(C,E,S)=>Math.max(E,Math.min(S,C)),x=b(g,0,d.width),k=b(m,0,d.height),w=b(h,0,d.width),_=b(v,0,d.height);f=[{left:0,top:0,width:d.width,height:k},{left:0,top:_,width:d.width,height:d.height-_},{left:0,top:k,width:x,height:_-k},{left:w,top:k,width:d.width-w,height:_-k}].filter(C=>C.width>0&&C.height>0)}const y=g=>{g.preventDefault(),g.stopPropagation()};return i.jsxs(i.Fragment,{children:[f.map((g,m)=>i.jsx("div",{className:"g7le-source-lock-block","data-testid":"g7le-source-lock-block",onClickCapture:y,onPointerDownCapture:y,onMouseDownCapture:y,onDragStartCapture:y,style:{position:"absolute",left:g.left,top:g.top,width:g.width,height:g.height,zIndex:Lp,pointerEvents:"auto",cursor:"not-allowed",background:"transparent"}},m)),i.jsxs("svg",{className:"g7le-source-lock-dim","data-testid":"g7le-source-lock-dim",width:d.width,height:d.height,style:{position:"absolute",left:0,top:0,width:d.width,height:d.height,zIndex:Lp,pointerEvents:"none"},children:[i.jsx("defs",{children:i.jsxs("mask",{id:l,children:[i.jsx("rect",{x:0,y:0,width:d.width,height:d.height,fill:"white"}),c.map((g,m)=>i.jsx("rect",{"data-testid":"g7le-source-lock-dim-hole",x:g.left,y:g.top,width:g.width,height:g.height,rx:4,fill:"black"},m))]})}),i.jsx("rect",{"data-testid":"g7le-source-lock-dim-box",x:0,y:0,width:d.width,height:d.height,fill:"rgba(15, 23, 42, 0.45)",mask:`url(#${l})`})]})]})}function $f(e){const t={type:e.type??"basic",name:e.name},n={};if(e.props)for(const[o,r]of Object.entries(e.props))o!=="children"&&r?.default!==void 0&&(n[o]=r.default);return Object.keys(n).length>0&&(t.props=n),t}function II(e){if(!e?.components)return[];const t=[];for(const[n,o]of Object.entries(e.components))if(Array.isArray(o))for(const r of o)t.push({...r,type:r.type??n});return t}function AI(e,t,n,o,r){const s=II(e);let a=null;if(n&&t?.containers){const l=t.containers[n];l&&Array.isArray(l.accepts)?a=new Set(l.accepts):a=new Set}else!n&&t?.draggable&&(a=new Set(t.draggable));return s.filter(l=>{if(a&&!a.has(l.name))return!1;if(o){const d=o.toLowerCase(),c=r(l).toLowerCase(),p=l.name.toLowerCase();if(!c.includes(d)&&!p.includes(d)&&!`<${p}>`.includes(d)&&!(l.description??"").toLowerCase().includes(d))return!1}return!0})}function TI(e,t){const n={key:"all",label:"layout_editor.palette.category.all",entries:e,isI18nKey:!0};if(t?.groups&&t.groups.length>0){const a=new Map(e.map(p=>[p.name,p])),l=new Set,d=[];for(const p of t.groups){const f=[];for(const y of p.components??[]){const g=a.get(y);g&&(f.push(g),l.add(y))}f.length!==0&&d.push({key:p.kind??p.label,label:p.label,entries:f,isI18nKey:p.label.startsWith("$t:")||/^[a-z_.]+$/i.test(p.label)})}const c=e.filter(p=>!l.has(p.name));return c.length>0&&d.push({key:"other",label:"layout_editor.palette.category.other",entries:c,isI18nKey:!0}),[n,...d]}const o=new Map;for(const a of e){const l=(a.type??"basic").toLowerCase(),d=o.get(l)??[];d.push(a),o.set(l,d)}const r=["basic","composite","layout"],s=[];for(const a of r){const l=o.get(a);l&&l.length>0&&(s.push({key:a,label:`layout_editor.palette.category.fallback.${a}`,entries:l,isI18nKey:!0}),o.delete(a))}for(const[a,l]of o.entries())s.push({key:a,label:a,entries:l,isI18nKey:!1});return[n,...s]}function Ef(e){const{manifest:t,nesting:n,componentPalette:o,targetParentPath:r,targetContainerName:s,onInsert:a,insertionIndex:l,onClose:d,editorTemplateId:c,editorLocale:p}=e,{t:f,translationEngine:y}=tt(),g=u.useMemo(()=>{if(!c||!y)return f;const A=p??null;return z=>{const D={templateId:c,locale:A??"ko"},O=y.translate(z,D);return O!==z?O:f(z)}},[c,p,y,f]),[m,h]=u.useState(""),[v,b]=u.useState("all"),x=o?.entries??null,k=u.useMemo(()=>A=>{const z=x?.[A.name];return z?.label?ui(z.label,g):A.description??A.name},[x,g]),w=u.useMemo(()=>AI(t,n,s,m,k),[t,n,s,m,k]),_=u.useMemo(()=>TI(w,o??null),[w,o]),C=_.find(A=>A.key===v)??_[0]??null,E=!n||!n.draggable&&!n.containers,S=w.length===0,T=C?.entries??[],$=A=>{const z=x?.[A.name],D=!!z?.defaultNode&&typeof z.defaultNode=="object";if(z?.requiresDefaultNode&&!D)return;const O=D?JSON.parse(JSON.stringify(z.defaultNode)):$f(A),M=r??[];a(O,M,l),$n({source:"palette",draggedComponentName:A.name,targetContainerName:s,targetContainerPath:M.length===0?null:M.join("/"),decision:"allowed",insertionIndex:l,result:"completed",timestamp:Date.now()}),d()};return i.jsxs("div",{className:"g7le-palette","data-testid":"g7le-palette",style:MI,children:[i.jsxs("div",{style:LI,children:[i.jsx("strong",{style:{fontSize:16},children:f("layout_editor.palette.title")}),i.jsx("button",{type:"button",onClick:d,"aria-label":"close","data-testid":"g7le-palette-close",style:PI,children:"×"})]}),i.jsxs("div",{style:OI,children:[i.jsx("aside",{style:BI,"data-testid":"g7le-palette-sidebar",children:i.jsx("nav",{children:i.jsx("ul",{style:WI,children:_.map(A=>{const z=C?.key===A.key,D=A.isI18nKey?ui(A.label,g):A.label;return i.jsx("li",{children:i.jsxs("button",{type:"button","data-testid":`g7le-palette-category-${A.key}`,"data-active":z?"true":"false",onClick:()=>b(A.key),style:{...jI,background:z?"#eff6ff":"transparent",color:z?"#1d4ed8":"#334155",fontWeight:z?600:500},children:[i.jsx("span",{children:D}),i.jsx("span",{style:VI,children:A.entries.length})]})},A.key)})})})}),i.jsxs("main",{style:HI,children:[i.jsx("input",{type:"text",placeholder:f("layout_editor.palette.search_placeholder"),value:m,onChange:A=>h(A.target.value),"data-testid":"g7le-palette-search",style:FI}),i.jsx("div",{style:KI,children:E?i.jsx("div",{style:If,"data-testid":"g7le-palette-spec-missing",children:f("layout_editor.palette.spec_missing")}):S||T.length===0?i.jsx("div",{style:If,"data-testid":"g7le-palette-empty",children:f("layout_editor.palette.empty")}):i.jsx("div",{"data-testid":`g7le-palette-group-${C?.key??"all"}`,style:{marginBottom:16},children:i.jsx("div",{style:UI,children:T.map(A=>i.jsx(DI,{entry:A,entrySpec:x?.[A.name]??null,onClick:$,t:f,entryT:g},A.name))})})})]})]})]})}function DI(e){const{entry:t,entrySpec:n,onClick:o,t:r,entryT:s}=e,a=n?.label?ui(n.label,s):t.description??t.name,l=(t.type??"").toLowerCase(),d=l==="basic"||l==="composite"||l==="layout",c=!!n?.defaultNode&&typeof n.defaultNode=="object",p=!!n?.requiresDefaultNode&&!c,f=!c,y=p?r("layout_editor.palette.requires_default_node_missing"):n?.label?ui(n.label,s):t.name;return i.jsxs("button",{type:"button",onClick:()=>o(t),disabled:p,"aria-disabled":p||void 0,"data-testid":`g7le-palette-item-${t.name}`,"data-component-type":l||void 0,"data-blocked":p?"true":void 0,"data-default-node-missing":f?"true":void 0,title:y,style:p?GI:qI,children:[i.jsx("div",{style:XI,"aria-hidden":"true",children:zI(t)}),i.jsx("div",{style:JI,children:a}),i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-tag`,style:ZI,children:`<${t.name}>`}),d&&i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-badge`,style:NI(l),children:l}),f&&i.jsx("div",{"data-testid":`g7le-palette-item-${t.name}-default-missing-badge`,style:YI,children:r("layout_editor.palette.default_node_missing_badge")})]})}function ui(e,t){return e.startsWith("$t:")?t(e.slice(3)):t(e)}function NI(e){const n={basic:{bg:"#dbeafe",fg:"#1e40af"},composite:{bg:"#dcfce7",fg:"#14532d"},layout:{bg:"#fef3c7",fg:"#854d0e"}}[e]??{bg:"#e2e8f0",fg:"#334155"};return{fontSize:9,fontWeight:600,color:n.fg,background:n.bg,padding:"1px 6px",borderRadius:999,textTransform:"uppercase",letterSpacing:.4,lineHeight:1.6}}function zI(e){const t=e.name;return{Button:"🔘",A:"🔗",Img:"🖼",Icon:"✨",Hr:"➖",P:"📄",Span:"T",H1:"H₁",H2:"H₂",H3:"H₃",H4:"H₄",Ul:"☰",Li:"•",Nav:"🧭",Div:"▢",Container:"▢",Flex:"⇆",Grid:"▦",SectionLayout:"▤",ThreeColumnLayout:"⫼",Form:"📝",Input:"⎕",Textarea:"▭",Select:"⏷",Checkbox:"☑",PasswordInput:"🔒",Label:"🏷",DataGrid:"▦",Pagination:"↔",SearchBar:"🔍",ProductCard:"🛍",ImageGallery:"🖼",Avatar:"👤",UserInfo:"👥",HtmlContent:"⟨/⟩",HtmlEditor:"✏",RichTextEditor:"✏",TabNavigation:"◰",QuantitySelector:"#",ProductImageViewer:"🖼",FileUploader:"📎",AvatarUploader:"📷",SocialLoginButtons:"🌐",PostReactions:"❤"}[t]??"◻"}const MI={display:"flex",flexDirection:"column",width:"100%",height:"100%",minHeight:0,fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif'},LI={display:"flex",justifyContent:"space-between",alignItems:"center",padding:"14px 20px",borderBottom:"1px solid #e2e8f0",flexShrink:0},PI={background:"transparent",border:"none",fontSize:24,cursor:"pointer",lineHeight:1,color:"#64748b",padding:"0 4px"},OI={display:"flex",flex:1,minHeight:0},BI={width:180,flexShrink:0,borderRight:"1px solid #e2e8f0",padding:"14px 8px",background:"#f8fafc",overflowY:"auto"},WI={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:4},jI={display:"flex",width:"100%",alignItems:"center",justifyContent:"space-between",textAlign:"left",padding:"8px 12px",border:"none",borderRadius:6,fontSize:13,cursor:"pointer",transition:"background-color 120ms"},HI={flex:1,display:"flex",flexDirection:"column",minHeight:0,padding:"14px 20px"},FI={width:"100%",padding:"8px 10px",border:"1px solid #cbd5e1",borderRadius:6,marginBottom:12,fontSize:13,flexShrink:0,boxSizing:"border-box"},KI={flex:1,overflowY:"auto",minHeight:0},VI={fontSize:11,color:"#94a3b8",marginLeft:8,fontWeight:500},UI={display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(96px, 1fr))",gap:10},qI={display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:6,padding:"14px 6px",background:"#fff",border:"1px solid #e2e8f0",borderRadius:8,cursor:"pointer",fontSize:12,minHeight:84,transition:"border-color 120ms, box-shadow 120ms"},GI={display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:6,padding:"14px 6px",background:"#f8fafc",border:"1px dashed #cbd5e1",borderRadius:8,cursor:"not-allowed",fontSize:12,minHeight:84,color:"#94a3b8",opacity:.65},YI={fontSize:9,fontWeight:600,color:"#92400e",background:"#fef3c7",padding:"1px 6px",borderRadius:999,textTransform:"none",letterSpacing:0,lineHeight:1.6},XI={fontSize:22,lineHeight:1,color:"#475569"},JI={fontSize:12,fontWeight:500,color:"#0f172a",textAlign:"center",wordBreak:"break-word"},ZI={fontFamily:'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',fontSize:10,color:"#64748b",textAlign:"center",wordBreak:"break-all"},If={padding:24,color:"#64748b",fontSize:13,textAlign:"center"};function QI(e){const t=[],n=r=>{if(Array.isArray(r))for(const s of r)s&&typeof s=="object"&&t.push(s)};n(e.children);const o=e.responsive;if(o&&typeof o=="object")for(const r of Object.values(o))r&&typeof r=="object"&&n(r.children);return t}function Af(e){const t=[],n=o=>{const r="isolatedState"in o&&o.isolatedState!=null,s=o.isolatedScopeId;(r||typeof s=="string"&&s.length>0)&&t.push({scopeId:typeof s=="string"?s:void 0,initialState:o.isolatedState&&typeof o.isolatedState=="object"?o.isolatedState:void 0});for(const a of QI(o))n(a)};if(Array.isArray(e))for(const o of e)o&&typeof o=="object"&&n(o);return t}const RI=["scroll","selector","wizard","slider"];function eA(e,t){const n=new Set;for(const o of e)o.scopeId&&n.add(o.scopeId);for(const o of t){n.add(o);for(const r of RI)n.add(`${o}-${r}`)}return Array.from(n)}function tA(e,t,n){const o=t.label;return typeof o=="string"?o.startsWith("$t:")?n(o.slice(3)):o:e}function pi({controlKey:e,control:t,node:n,t:o,onPatch:r,candidates:s,bindingCandidates:a,scope:l=Wt}){const d=Tn(t.widget),c=tA(e,t,o),p=u.useMemo(()=>Do(n,t,l),[n,t,l]),f=t.widget==="i18n-text"&&t.apply?.type==="propValue",y=t.widget==="text"&&t.apply?.type==="propValue",g=p.darkReadonly===!0||l.colorScheme==="dark"&&t.apply!==void 0&&!Us(t.apply),m=t.apply,h=l.colorScheme==="dark"&&!g&&m?.type==="classToken"&&typeof m.tokenTemplate=="string"&&m.tokenTemplate.includes("{value}"),v=p.scopedValue===void 0&&p.baseFallback!==void 0,b=u.useCallback(x=>{if(g)return;const k=fn(n,t,x,l);r(k)},[n,t,r,l,g]);return i.jsxs("div",{className:"g7le-control-row","data-testid":`g7le-control-${e}`,style:nA,children:[i.jsxs("span",{className:"g7le-control-label",style:oA,children:[c,p.conflict&&i.jsx("span",{"data-testid":`g7le-control-conflict-${e}`,title:o("layout_editor.property_modal.value_conflict"),style:rA,children:"!"})]}),i.jsx("div",{className:"g7le-control-widget",style:{flex:1,minWidth:0,opacity:v?.45:1},"data-placeholder":v?"true":void 0,children:f?i.jsx(vt,{value:typeof p.value=="string"?p.value:"",onChange:x=>b(x),t:o,testidPrefix:`g7le-prop-i18n-${e}`,candidates:a,enableExpressionTree:!0,expressionTreeCollapsible:!0}):y?i.jsx(Ve,{value:typeof p.value=="string"?p.value:"",onChange:x=>b(x===""?void 0:x),t:o,testidPrefix:`g7le-prop-value-${e}`,candidates:a}):g?i.jsx("span",{"data-testid":`g7le-control-dark-readonly-${e}`,style:sA,children:o("layout_editor.property_modal.dark_code_only")}):d?i.jsx(d,{control:t,value:p.value,onChange:b,t:o,candidates:s,bindingCandidates:a,freeValueDisabled:h}):i.jsx("span",{"data-testid":`g7le-control-unsupported-${e}`,style:iA,children:o("layout_editor.property_modal.unsupported_widget")})})]})}const nA={display:"flex",alignItems:"center",gap:12,padding:"6px 0",minHeight:32,minWidth:0},oA={fontSize:12,color:"#475569",minWidth:88,display:"inline-flex",alignItems:"center",gap:4},rA={display:"inline-flex",width:14,height:14,borderRadius:7,background:"#f59e0b",color:"#fff",fontSize:10,alignItems:"center",justifyContent:"center",fontWeight:700},iA={fontSize:11,color:"#94a3b8",fontStyle:"italic"},sA={fontSize:11,color:"#94a3b8",fontStyle:"italic"};function fi(e,t,n){return typeof e=="string"?e.startsWith("$t:")?n(e.slice(3)):e:t}function aA(e,t){const n=e.props??{};return t.key in n?n[t.key]:t.default}function lA({spec:e,node:t,t:n,onPatchProp:o}){const r=e.groups??[];return i.jsx("div",{className:"g7le-composite-settings","data-testid":"g7le-composite-settings",style:cA,children:r.map((s,a)=>i.jsxs("div",{className:"g7le-settings-group",style:uA,children:[i.jsx("div",{style:pA,children:fi(s.label,n("layout_editor.property_modal.settings_group",{index:a+1}),n)}),(s.fields??[]).map(l=>i.jsxs("div",{className:"g7le-settings-field","data-testid":`g7le-setting-${l.key}`,style:fA,children:[i.jsx("span",{style:gA,children:fi(l.label,l.key,n)}),i.jsx("div",{style:{flex:1},children:dA(l,t,n,o)})]},l.key))]},a))})}function dA(e,t,n,o){const r=aA(t,e);switch(e.type){case"number":return i.jsx("input",{type:"number","data-testid":`g7le-setting-input-${e.key}`,value:typeof r=="number"?r:"",min:e.min,max:e.max,onChange:s=>o(e.key,s.target.value===""?void 0:Number(s.target.value)),style:Ca});case"text":return i.jsx("input",{type:"text","data-testid":`g7le-setting-input-${e.key}`,value:typeof r=="string"?r:"",onChange:s=>o(e.key,s.target.value===""?void 0:s.target.value),style:Ca});case"toggle":return i.jsx("input",{type:"checkbox","data-testid":`g7le-setting-input-${e.key}`,checked:r===!0,onChange:s=>o(e.key,s.target.checked)});case"checkbox-group":{const s=Array.isArray(r)?r:[];return i.jsx("div",{style:hA,"data-testid":`g7le-setting-input-${e.key}`,children:(e.options??[]).map(a=>{const l=s.includes(a.value);return i.jsxs("label",{style:yA,children:[i.jsx("input",{type:"checkbox",checked:l,onChange:d=>{const c=d.target.checked?[...s,a.value]:s.filter(p=>p!==a.value);o(e.key,c)}}),fi(a.label,String(a.value),n)]},String(a.value))})})}default:return i.jsxs("select",{"data-testid":`g7le-setting-input-${e.key}`,value:r==null?"":String(r),onChange:s=>{const a=s.target.value;if(a===""){o(e.key,void 0);return}const l=(e.options??[]).find(d=>String(d.value)===a);o(e.key,l?l.value:a)},style:Ca,children:[i.jsx("option",{value:"",children:"—"}),(e.options??[]).map(s=>i.jsx("option",{value:String(s.value),children:fi(s.label,String(s.value),n)},String(s.value)))]})}}const cA={display:"flex",flexDirection:"column",gap:16},uA={display:"flex",flexDirection:"column",gap:6},pA={fontSize:12,fontWeight:700,color:"#0f172a",borderBottom:"1px solid #e2e8f0",paddingBottom:4},fA={display:"flex",alignItems:"center",gap:12,minHeight:30},gA={fontSize:12,color:"#475569",minWidth:96},Ca={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",boxSizing:"border-box"},hA={display:"flex",flexWrap:"wrap",gap:10},yA={display:"inline-flex",alignItems:"center",gap:4,fontSize:12,color:"#0f172a"},bA=["data_binding","isolatedScopeId","parentFormContextProp","skipBindingKeys","isolatedState","component_layout","sortable","itemTemplate"],mA=/\{\{|\$switch|\$get|\|\s*(date|number|truncate|currency|relative)\b/;function gi(e,t=0){return t>6?!1:typeof e=="string"?mA.test(e):Array.isArray(e)?e.some(n=>gi(n,t+1)):e&&typeof e=="object"?Object.values(e).some(n=>gi(n,t+1)):!1}function vA(e){if(Zl(e))return!1;const t=Fl(e);return t.length===0?!1:t.some(n=>n.isComplex)}function Tf(e){const t=[],n=e.props??{};for(const o of bA)o in e&&e[o]!==void 0?t.push({key:o,kind:"developer_prop"}):o in n&&n[o]!==void 0&&t.push({key:`props.${o}`,kind:"developer_prop"});typeof e.text=="string"?vA(e.text)&&t.push({key:"text",kind:"complex_expression"}):gi(e.text)&&t.push({key:"text",kind:"complex_expression"});for(const[o,r]of Object.entries(n))o==="style"||o==="className"||gi(r)&&t.push({key:`props.${o}`,kind:"complex_expression"});return t}function _A(e){return Tf(e).length>0}const xA=new Set(["permissions","blur_until_loaded","comment"]);function wA({node:e,advanced:t,t:n,onPatch:o,permissionCandidates:r}){const s=u.useMemo(()=>Tf(e),[e]),a=(l,d)=>{const c={...e};d===void 0?delete c[l]:c[l]=d,o(c)};return i.jsxs("div",{className:"g7le-advanced-props","data-testid":"g7le-advanced-props",style:kA,children:[t.map(l=>{if(!xA.has(l))return i.jsxs("div",{"data-testid":`g7le-advanced-deferred-${l}`,style:CA,children:[n("layout_editor.property_modal.advanced_deferred"),":",l]},l);if(l==="permissions")return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-permissions",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.permissions")}),i.jsx(ys,{control:{widget:"tag-input"},value:e.permissions,onChange:d=>a("permissions",d),t:n,candidates:r})]},l);if(l==="blur_until_loaded"){const d=e.blur_until_loaded===!0||e.blur_until_loaded!=null&&e.blur_until_loaded!==!1;return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-blur",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.blur_until_loaded")}),i.jsxs("label",{style:{display:"inline-flex",alignItems:"center",gap:6,fontSize:12},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-advanced-blur-toggle",checked:!!d,onChange:c=>a("blur_until_loaded",c.target.checked?!0:void 0)}),n("layout_editor.property_modal.advanced.blur_hint")]})]},l)}return i.jsxs("div",{className:"g7le-advanced-group","data-testid":"g7le-advanced-comment",style:$a,children:[i.jsx("div",{style:Ea,children:n("layout_editor.property_modal.advanced.comment")}),i.jsx("input",{type:"text","data-testid":"g7le-advanced-comment-input",value:typeof e.comment=="string"?e.comment:"",onChange:d=>a("comment",d.target.value===""?void 0:d.target.value),style:SA})]},l)}),s.length>0&&i.jsxs("div",{className:"g7le-advanced-preserved","data-testid":"g7le-advanced-preserved",style:$A,children:[i.jsx("div",{style:EA,children:n("layout_editor.property_modal.advanced_preserved_title",{count:s.length})}),i.jsx("ul",{style:IA,children:s.map(l=>i.jsxs("li",{"data-testid":`g7le-advanced-preserved-${l.key}`,style:AA,children:[i.jsx("code",{style:{fontSize:11},children:l.key}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:l.kind==="developer_prop"?n("layout_editor.property_modal.advanced_kind_developer"):n("layout_editor.property_modal.advanced_kind_expression")})]},l.key))})]})]})}const kA={display:"flex",flexDirection:"column",gap:14},$a={display:"flex",flexDirection:"column",gap:6},Ea={fontSize:12,fontWeight:700,color:"#0f172a"},SA={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%",boxSizing:"border-box"},CA={fontSize:11,color:"#94a3b8",fontStyle:"italic"},$A={borderTop:"1px dashed #cbd5e1",paddingTop:10,marginTop:4},EA={fontSize:11,color:"#b45309",marginBottom:6},IA={listStyle:"none",margin:0,padding:0,display:"flex",flexDirection:"column",gap:4},AA={display:"flex",justifyContent:"space-between",gap:8,padding:"3px 0"},TA={onClick:"click",onHover:"mouseenter",onChange:"change",onSubmit:"submit",onMount:"mount",onUnmount:"unmount",onFocus:"focus",onBlur:"blur"};function Ia(e){return TA[e]??e.replace(/^on/,"").toLowerCase()}function Df(e){const t=e.actions;if(Array.isArray(t))return t.filter(r=>!!r&&typeof r=="object");const n=e.props??{};if(Array.isArray(n.actions))return n.actions.filter(r=>!!r&&typeof r=="object");const o=n.events;if(o&&typeof o=="object"){const r=[];for(const[s,a]of Object.entries(o)){if(!Array.isArray(a))continue;const l=Ia(s);for(const d of a)d&&typeof d=="object"&&r.push("type"in d?d:{type:l,...d})}return r}return[]}function DA(e,t){const n=Ia(t);return Df(e).filter(o=>o.type===n||t==="onClick"&&o.type===void 0)}function NA(e,t,n){const o=Ia(t),r=Df(e).filter(d=>!(d.type===o||t==="onClick"&&d.type===void 0)),s=n.map(d=>"type"in d?d:{type:o,...d}),a={...e};if(a.props&&typeof a.props=="object"){const d={...a.props};let c=!1;"actions"in d&&(delete d.actions,c=!0),"events"in d&&(delete d.events,c=!0),c&&(a.props=d)}const l=[...r,...s];return l.length===0?delete a.actions:a.actions=l,a}function zA({node:e,spec:t,capability:n,t:o,onPatchNode:r,pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,bindingCandidates:d}){const c=t?.actionRecipes,p=u.useMemo(()=>({pageCandidates:s,dataSourceCandidates:a,stateKeyCandidates:l,bindingCandidates:d}),[s,a,l,d]),f=Array.isArray(n?.events)?n.events:[];return f.length===0?i.jsx("div",{"data-testid":"g7le-action-no-events",style:Nf,children:o("layout_editor.action.no_events")}):!c||Object.keys(c).length===0?i.jsx("div",{"data-testid":"g7le-action-no-recipes",style:Nf,children:o("layout_editor.action.no_recipes")}):i.jsx("div",{className:"g7le-action-editor","data-testid":"g7le-action-editor",style:{display:"flex",flexDirection:"column",gap:14},children:f.map(y=>i.jsxs("div",{className:"g7le-action-event-slot","data-testid":`g7le-action-event-${y}`,children:[i.jsx("div",{style:MA,children:o(`layout_editor.action.event.${y}`)}),i.jsx(Gt,{actions:DA(e,y),onChange:g=>r(NA(e,y,g)),t:o,recipes:c,candidatePools:p,actionChipCandidates:t?.actionChipCandidates??null,addContext:"component",showSourceBadge:!0,testIdPrefix:`g7le-action-slot-${y}`})]},y))})}const Nf={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},MA={fontSize:12,fontWeight:700,color:"#475569",marginBottom:6};function Aa(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function LA(e,t){return Aa(t,e.labelKey,e.path||e.sourceId)}function PA({node:e,spec:t,candidates:n,t:o,onPatchNode:r}){const a=(e.props??{})[t.propKey],l=typeof a=="string"?a:void 0,d=u.useMemo(()=>Kt(a),[a]),c=l!==void 0&&l!==""&&d===null,[p,f]=u.useState(!1),[y,g]=u.useState(""),m=Aa(o,t.label,t.propKey),h=o(t.shape==="array"?"layout_editor.binding.shape_array":t.shape==="object"?"layout_editor.binding.shape_object":"layout_editor.binding.shape_scalar"),v=u.useMemo(()=>Fi(n,t.shape).map(_=>({..._,resolvedLabel:LA(_,o)})),[n,t.shape,o]),b=u.useMemo(()=>Ki(v,y),[v,y]),x=w=>{const _={...e.props??{}};w===void 0?delete _[t.propKey]:_[t.propKey]=w,r({...e,props:_})},k=w=>{x(En(w.sourceId,w.path,t.shape)),f(!1),g("")};return i.jsxs("div",{"data-testid":`g7le-binding-row-${t.propKey}`,style:jA,children:[i.jsxs("div",{style:HA,children:[i.jsx("span",{style:{fontWeight:600,fontSize:12},children:m}),i.jsx("span",{style:FA,children:h}),t.required&&i.jsx("span",{"data-testid":`g7le-binding-required-${t.propKey}`,style:KA,children:o("layout_editor.binding.required")})]}),c?i.jsxs("div",{"data-testid":`g7le-binding-complex-${t.propKey}`,style:GA,children:[o("layout_editor.binding.complex"),i.jsx("code",{style:zf,children:l})]}):d?i.jsxs("div",{style:VA,children:[i.jsx("code",{"data-testid":`g7le-binding-expr-${t.propKey}`,style:zf,children:l}),i.jsx("button",{type:"button","data-testid":`g7le-binding-clear-${t.propKey}`,onClick:()=>x(void 0),style:qA,children:o("layout_editor.binding.clear")})]}):i.jsx("div",{"data-testid":`g7le-binding-empty-${t.propKey}`,style:UA,children:o("layout_editor.binding.not_connected")}),d&&(t.shape==="array"||t.shape==="object")&&t.itemFields&&t.itemFields.length>0&&i.jsxs("div",{style:YA,children:[o("layout_editor.binding.item_fields"),": ",t.itemFields.join(", ")]}),!c&&i.jsxs("div",{style:{marginTop:4},children:[i.jsxs("button",{type:"button","data-testid":`g7le-binding-search-toggle-${t.propKey}`,onClick:()=>f(w=>!w),style:XA,children:["🔍 ",o("layout_editor.binding.search")]}),p&&i.jsxs("div",{"data-testid":`g7le-binding-picker-${t.propKey}`,style:JA,children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":`g7le-binding-search-input-${t.propKey}`,value:y,placeholder:o("layout_editor.binding.search_placeholder"),onChange:w=>g(w.target.value),style:ZA}),i.jsx("div",{style:QA,children:b.length===0?i.jsx("div",{style:RA,children:o("layout_editor.binding.no_results")}):b.map(w=>i.jsxs("button",{type:"button","data-testid":`g7le-binding-candidate-${w.expression}`,onClick:()=>k(w),style:e6,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:w.resolvedLabel}),i.jsxs("span",{style:t6,children:[Aa(o,w.groupLabelKey,w.sourceId)," · ",w.path||w.sourceId]})]}),i.jsx("span",{style:n6,children:w.preview})]},w.expression))})]})]})]})}function OA({node:e,dataProps:t,candidates:n,t:o,onPatchNode:r}){return!Array.isArray(t)||t.length===0?null:i.jsxs("div",{"data-testid":"g7le-data-binding-section",style:BA,children:[i.jsxs("div",{style:WA,children:["🔗 ",o("layout_editor.binding.section_title")]}),t.map(s=>i.jsx(PA,{node:e,spec:s,candidates:n,t:o,onPatchNode:r},s.propKey)),i.jsx("div",{style:o6})]})}const BA={display:"flex",flexDirection:"column",marginBottom:8},WA={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},jA={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},HA={display:"flex",alignItems:"center",gap:6},FA={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#e0e7ff",color:"#3730a3"},KA={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#fef3c7",color:"#92400e"},VA={display:"flex",alignItems:"center",gap:6},UA={fontSize:11,color:"#94a3b8"},zf={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},qA={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},GA={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},YA={fontSize:10,color:"#94a3b8"},XA={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 10px",cursor:"pointer",textAlign:"left",width:"100%"},JA={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},ZA={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},QA={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},RA={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},e6={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},t6={fontSize:10,color:"#94a3b8"},n6={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},o6={borderTop:"1px solid #e2e8f0",marginTop:4};function r6({node:e,candidates:t,t:n,onPatchNode:o,templateIdentifier:r}){const s=typeof e.text=="string"?e.text:"",{state:a}=Je(),l=r??a.templateIdentifier,d=a.selectedRoute?.layoutName??null,c=a.locale,[p,f]=u.useState(!1),y=/^\s*\$t:custom\.[A-Za-z0-9._-]+\s*$/.test(s),m=Zl(s)&&(mt(s)?.key.startsWith("custom.")??!1)||y,h=u.useMemo(()=>m?mt(s):null,[m,s]),v=u.useMemo(()=>m?[]:Fl(s),[m,s]),b=T=>{o({...e,text:T})},x=()=>{if(!(typeof window>"u"))try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:l,locale:c}}))}catch{}},k=(T,$)=>{const A=En($.sourceId,$.path,"scalar");b(wb(s,T,A))},w=T=>{const $=mt(s);b(mo(s,T));const A=$?.key;l&&A&&Fd(l,A,T).then(x)},_=T=>{const $=n(T);return $&&$!==T&&!$.startsWith("$t:")?$:""},C=T=>T.replace(/\$t:[a-zA-Z0-9._-]+/g,$=>_($.slice(3))).replace(/\s+/g," ").trim(),E=T=>{p||(f(!0),ls(l,d,c,s,s.length,T.sourceId,T.path,"scalar",C,$=>n($)).then($=>{$.kind==="ok"&&(b($.text),x())}).finally(()=>f(!1)))},S=T=>{p||!h&&!y||(f(!0),wr(l,c,s,Number.MAX_SAFE_INTEGER,T.sourceId,T.path,"scalar").then($=>{$.kind==="ok"&&(b($.text),x())}).finally(()=>f(!1)))};if(m){const T=h?.params??[];return i.jsxs("div",{"data-testid":"g7le-inline-binding-section",style:Mf,children:[i.jsxs("div",{style:Lf,children:["🔗 ",n("layout_editor.inline_binding.section_title")]}),T.length===0?i.jsx("div",{"data-testid":"g7le-inline-binding-none",style:Bf,children:n("layout_editor.inline_binding.none")}):T.map($=>i.jsx("div",{"data-testid":`g7le-inline-binding-param-${$.name}`,style:Pf,children:$.parsed===null?i.jsxs("div",{"data-testid":`g7le-inline-binding-param-complex-${$.name}`,style:s6,children:[n("layout_editor.inline_binding.complex"),i.jsx("code",{style:Ta,children:$.expression})]}):i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:Of,children:[i.jsx("code",{"data-testid":`g7le-inline-binding-param-expr-${$.name}`,style:Ta,children:$.expression}),i.jsx("button",{type:"button","data-testid":`g7le-inline-binding-param-clear-${$.name}`,onClick:()=>w($.name),style:i6,children:n("layout_editor.inline_binding.clear")})]}),i.jsx($t,{candidates:t,t:n,onSelect:A=>k($.name,A),testIdSuffix:`param-${$.name}`})]})},$.name)),i.jsxs("div",{"data-testid":"g7le-inline-binding-param-append",style:Wf,children:[i.jsx("div",{style:Da,children:n("layout_editor.inline_binding.append_hint")}),i.jsx($t,{candidates:t,t:n,onSelect:S,testIdSuffix:"param-append"})]}),i.jsx("div",{"data-testid":"g7le-inline-binding-param-hint",style:Da,children:n("layout_editor.inline_binding.param_hint")}),i.jsx("div",{style:jf})]})}return i.jsxs("div",{"data-testid":"g7le-inline-binding-section",style:Mf,children:[i.jsxs("div",{style:Lf,children:["🔗 ",n("layout_editor.inline_binding.section_title")]}),v.length===0?i.jsx("div",{"data-testid":"g7le-inline-binding-none",style:Bf,children:n("layout_editor.inline_binding.none")}):v.map(T=>i.jsx("div",{"data-testid":`g7le-inline-binding-row-${T.bindingIndex}`,style:Pf,children:i.jsx("div",{style:Of,children:i.jsx("code",{"data-testid":`g7le-inline-binding-expr-${T.bindingIndex}`,style:Ta,children:T.expression})})},T.bindingIndex)),i.jsxs("div",{"data-testid":"g7le-inline-binding-append",style:Wf,children:[i.jsx("div",{style:Da,children:n("layout_editor.inline_binding.append_hint")}),i.jsx($t,{candidates:t,t:n,onSelect:E,testIdSuffix:"append"})]}),i.jsx("div",{style:jf})]})}const Mf={display:"flex",flexDirection:"column",marginBottom:8},Lf={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},Pf={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},Of={display:"flex",alignItems:"center",gap:6},Bf={fontSize:11,color:"#94a3b8"},Ta={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},i6={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},s6={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},Wf={display:"flex",flexDirection:"column",gap:2,padding:"8px 0 0"},Da={fontSize:11,color:"#475569",fontWeight:600},jf={borderTop:"1px solid #e2e8f0",marginTop:4};function Hf(e,t,n){if(!t)return n;const o=e(t);return!o||o===t||o.startsWith("$t:")?n:o}function a6(e,t){return Hf(t,e.labelKey,e.path||e.sourceId)}function l6(e){const t=e.iteration;return t&&typeof t=="object"?t:null}function d6({node:e,candidates:t,t:n,onPatchNode:o}){const r=l6(e),s=r?.source,a=typeof s=="string"?s:void 0,l=u.useMemo(()=>Kt(s),[s]),d=a!==void 0&&a!==""&&l===null,[c,p]=u.useState(!1),[f,y]=u.useState(""),g=u.useMemo(()=>Fi(t,"array").map(w=>({...w,resolvedLabel:a6(w,n)})),[t,n]),m=u.useMemo(()=>Ki(g,f),[g,f]);if(!r)return null;const h=typeof r.item_var=="string"?r.item_var:void 0,v=typeof r.index_var=="string"?r.index_var:void 0,b=k=>{const w={...r};k===void 0?delete w.source:w.source=k,o({...e,iteration:w})},x=k=>{b(En(k.sourceId,k.path,"array")),p(!1),y("")};return i.jsxs("div",{"data-testid":"g7le-iteration-binding-section",style:c6,children:[i.jsxs("div",{style:u6,children:["🔁 ",n("layout_editor.binding.iteration_title")]}),i.jsx("div",{style:p6,children:n("layout_editor.binding.iteration_hint")}),i.jsxs("div",{style:f6,children:[i.jsxs("div",{style:g6,children:[i.jsx("span",{style:{fontWeight:600,fontSize:12},children:n("layout_editor.binding.iteration_label")}),i.jsx("span",{style:h6,children:n("layout_editor.binding.shape_array")})]}),d?i.jsxs("div",{"data-testid":"g7le-iteration-binding-complex",style:v6,children:[n("layout_editor.binding.complex"),i.jsx("code",{style:Ff,children:a})]}):l?i.jsxs("div",{style:y6,children:[i.jsx("code",{"data-testid":"g7le-iteration-binding-expr",style:Ff,children:a}),i.jsx("button",{type:"button","data-testid":"g7le-iteration-binding-clear",onClick:()=>b(void 0),style:m6,children:n("layout_editor.binding.clear")})]}):i.jsx("div",{"data-testid":"g7le-iteration-binding-empty",style:b6,children:n("layout_editor.binding.not_connected")}),(h||v)&&i.jsxs("div",{"data-testid":"g7le-iteration-vars",style:_6,children:[h&&i.jsxs("span",{children:[n("layout_editor.binding.iteration_item_var"),": ",i.jsx("code",{style:Kf,children:h})]}),v&&i.jsxs("span",{children:[n("layout_editor.binding.iteration_index_var"),": ",i.jsx("code",{style:Kf,children:v})]})]}),!d&&i.jsxs("div",{style:{marginTop:4},children:[i.jsxs("button",{type:"button","data-testid":"g7le-iteration-binding-search-toggle",onClick:()=>p(k=>!k),style:x6,children:["🔍 ",n("layout_editor.binding.search")]}),c&&i.jsxs("div",{"data-testid":"g7le-iteration-binding-picker",style:w6,children:[i.jsx("input",{type:"text",autoFocus:!0,"data-testid":"g7le-iteration-binding-search-input",value:f,placeholder:n("layout_editor.binding.search_placeholder"),onChange:k=>y(k.target.value),style:k6}),i.jsx("div",{style:S6,children:m.length===0?i.jsx("div",{style:C6,children:n("layout_editor.binding.no_results")}):m.map(k=>i.jsxs("button",{type:"button","data-testid":`g7le-iteration-binding-candidate-${k.expression}`,onClick:()=>x(k),style:$6,children:[i.jsxs("span",{style:{display:"flex",flexDirection:"column",alignItems:"flex-start"},children:[i.jsx("span",{style:{fontSize:12},children:k.resolvedLabel}),i.jsxs("span",{style:E6,children:[Hf(n,k.groupLabelKey,k.sourceId)," · ",k.path||k.sourceId]})]}),i.jsx("span",{style:I6,children:k.preview})]},k.expression))})]})]})]}),i.jsx("div",{style:A6})]})}const c6={display:"flex",flexDirection:"column",marginBottom:8},u6={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:4},p6={fontSize:10,color:"#94a3b8",marginBottom:6,lineHeight:1.4},f6={display:"flex",flexDirection:"column",gap:4,padding:"8px 0"},g6={display:"flex",alignItems:"center",gap:6},h6={fontSize:10,padding:"1px 6px",borderRadius:4,background:"#e0e7ff",color:"#3730a3"},y6={display:"flex",alignItems:"center",gap:6},b6={fontSize:11,color:"#94a3b8"},Ff={fontSize:11,fontFamily:"monospace",background:"#f1f5f9",padding:"2px 6px",borderRadius:4,color:"#0f172a",wordBreak:"break-all"},m6={fontSize:11,border:"1px solid #e2e8f0",borderRadius:4,background:"#fff",color:"#dc2626",padding:"2px 8px",cursor:"pointer"},v6={display:"flex",flexDirection:"column",gap:2,fontSize:11,color:"#64748b"},_6={display:"flex",gap:12,fontSize:10,color:"#94a3b8",marginTop:2},Kf={fontSize:10,fontFamily:"monospace",background:"#f1f5f9",padding:"1px 4px",borderRadius:3,color:"#475569"},x6={fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#334155",padding:"4px 10px",cursor:"pointer",textAlign:"left",width:"100%"},w6={marginTop:4,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",padding:6},k6={width:"100%",padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"},S6={marginTop:4,maxHeight:200,overflowY:"auto",display:"flex",flexDirection:"column",gap:2},C6={fontSize:11,color:"#94a3b8",padding:"8px 0",textAlign:"center"},$6={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8,border:"none",background:"transparent",cursor:"pointer",padding:"6px 8px",borderRadius:4,textAlign:"left"},E6={fontSize:10,color:"#94a3b8"},I6={fontSize:10,color:"#64748b",fontFamily:"monospace",whiteSpace:"nowrap"},A6={borderTop:"1px solid #e2e8f0",marginTop:4},T6=new Set(["Span","P","H1","H2","H3","H4","H5","H6","A","Button","Li","Label"]);function D6(e,t){if(!e||e.iteration&&typeof e.iteration=="object")return!1;const n=t?.textBinding;if(n===!1)return!1;if(n===!0)return!0;const o=typeof e.name=="string"?e.name:"";return T6.has(o)?!0:typeof e.text=="string"}const yn={direction:"flexDirection",justify:"flexJustify",align:"flexAlign",wrap:"flexWrap",gap:"flexGap",enable:"flexEnable"},hi={grow:"flexItemGrow",selfAlign:"flexItemAlign",order:"flexItemOrder"};function Vf(e){const t=e?.flexEditor;return t==="container"||t==="item"||t==="auto"?t:null}function N6(e){if(!e||typeof window>"u"||typeof window.getComputedStyle!="function")return!1;const t=(window.getComputedStyle(e).display??"").toLowerCase();return t==="flex"||t==="inline-flex"}function z6(e,t,n=Wt){if(!e||!t)return!1;const o=Do(e,t,n),r=t.options,s=Array.isArray(r)?t.onValue??r[0]?.value:void 0,a=l=>s!==void 0?l===s:l!==void 0&&l!==!1;return n.breakpoint!=="base"?a(o.scopedRawValue):o.matched&&a(o.value)}function M6(e,t,n=!1,o=!1,r=Wt){if(e==="container")return{showContainer:!0,showEnableButton:!1,showDisableButton:!1};if(e==="item")return{showContainer:!1,showEnableButton:!1,showDisableButton:!1};if(e==="auto"){const s=r.breakpoint==="base";return!o&&!s?{showContainer:!1,showEnableButton:!1,showDisableButton:!1}:(o?n:N6(t))?{showContainer:!0,showEnableButton:!1,showDisableButton:o}:{showContainer:!1,showEnableButton:!0,showDisableButton:!1}}return{showContainer:!1,showEnableButton:!1,showDisableButton:!1}}const L6={[yn.direction]:"layout_editor.flex.direction",[yn.justify]:"layout_editor.flex.justify",[yn.align]:"layout_editor.flex.align",[yn.wrap]:"layout_editor.flex.wrap",[yn.gap]:"layout_editor.flex.gap"},P6={[hi.grow]:"layout_editor.flex.item_grow",[hi.selfAlign]:"layout_editor.flex.item_align",[hi.order]:"layout_editor.flex.item_order"};function O6({node:e,spec:t,capability:n,t:o,onPatchNode:r,liveElement:s,scope:a=Wt}){const l=Vf(n),d=Ct(t,yn.enable),c=z6(e,d,a),{showContainer:p,showEnableButton:f,showDisableButton:y}=M6(l,s,c,!!d,a),g=Object.values(yn).filter(_=>_!==yn.enable&&Ct(t,_)),m=Object.values(hi).filter(_=>Ct(t,_)),h=d?.options,v=d?.onValue??h?.[0]?.value??!0,b=Array.isArray(h)&&h.length>=2?h[1].value:void 0,x=()=>{if(!d)return;const _=fn(e,d,v,a);r(_)},k=()=>{if(!d)return;const _=a.breakpoint!=="base"&&b!==void 0?b:void 0,C=fn(e,d,_,a);r(C)},w=(_,C,E)=>i.jsx("div",{"data-testid":E,style:{display:"flex",flexDirection:"column"},children:_.map(S=>{const T=Ct(t,S);if(!T)return null;const $=T.label?T:{...T,label:`$t:${C[S]}`};return i.jsx(pi,{controlKey:S,control:$,node:e,t:o,onPatch:r,scope:a},S)})});return i.jsxs("div",{className:"g7le-flex-editor","data-testid":"g7le-flex-editor",style:{display:"flex",flexDirection:"column",gap:12},children:[f&&i.jsx("button",{type:"button","data-testid":"g7le-flex-enable",onClick:x,style:B6,children:o("layout_editor.flex.make_flex")}),p&&g.length>0&&i.jsxs("div",{className:"g7le-flex-container-section","data-testid":"g7le-flex-container-section",children:[i.jsx("div",{style:Uf,children:o("layout_editor.flex.container_title")}),w(g,L6,"g7le-flex-container-controls"),y&&i.jsx("button",{type:"button","data-testid":"g7le-flex-disable",onClick:k,style:W6,children:o("layout_editor.flex.unmake_flex")})]}),l==="item"&&m.length>0&&i.jsxs("div",{className:"g7le-flex-item-section","data-testid":"g7le-flex-item-section",children:[i.jsx("div",{style:Uf,children:o("layout_editor.flex.item_title")}),w(m,P6,"g7le-flex-item-controls")]}),!p&&!f&&l!=="item"&&i.jsx("div",{"data-testid":"g7le-flex-empty",style:j6,children:o("layout_editor.flex.not_flex")})]})}const Uf={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:6},B6={padding:"8px 12px",fontSize:12,border:"1px dashed #2563eb",borderRadius:6,background:"#eff6ff",color:"#2563eb",cursor:"pointer"},W6={marginTop:8,padding:"6px 10px",fontSize:12,border:"1px solid #e2e8f0",borderRadius:6,background:"#f8fafc",color:"#64748b",cursor:"pointer"},j6={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},qf=["desktop","tablet","mobile","portable"],Na=1280,H6=820,F6=390,K6=1920,V6=320;function U6(e){const t=[],n=new Set,o=r=>{if(!r||typeof r!="object")return;const s=r.responsive;if(s&&typeof s=="object")for(const a of Object.keys(s)){n.has(a)||(n.add(a),t.push(a));const l=s[a]?.children;if(Array.isArray(l))for(const d of l)o(d)}if(Array.isArray(r.children))for(const a of r.children)o(a)};if(Array.isArray(e))for(const r of e)o(r);return t}function q6(e){const t=[...qf],n=new Set(qf);for(const o of U6(e))!n.has(o)&&!t.includes(o)&&t.push(o);return t}function G6(e,t){const n=e?.responsive;if(!n||typeof n!="object")return[];const o=[];for(const r of Object.keys(n)){const s=n[r];!s||!Array.isArray(s.children)||r!==t&&o.push(r)}return o}function Y6(e){switch(e){case"desktop":return Na;case"tablet":return H6;case"mobile":return F6}const t=e in To?To[e]:Mn.parseRange(e);return!t||!Number.isFinite(t.max)?Na:Math.round(Math.max(V6,Math.min(K6,t.max)))}function Gf(){const t=Ft()?.document?.raw?.components;return u.useMemo(()=>q6(t??null),[t])}const X6={desktop:"layout_editor.property_modal.scope.device_desktop",tablet:"layout_editor.property_modal.scope.device_tablet",mobile:"layout_editor.property_modal.scope.device_mobile",portable:"layout_editor.property_modal.scope.device_portable"},J6=[{key:"base",labelKey:"layout_editor.property_modal.scope.scheme_light"},{key:"dark",labelKey:"layout_editor.property_modal.scope.scheme_dark"}];function Yf({scope:e,onChange:t,node:n,t:o,showColorScheme:r=!0,onClearScope:s}){const[a,l]=u.useState(!1),[d,c]=u.useState(""),[p,f]=u.useState(""),[y,g]=u.useState(null),m=Gf(),h=u.useMemo(()=>{const k=["base",...m],w=new Set(k);for(const _ of Object.keys(n.responsive??{}))_!=="base"&&!w.has(_)&&(w.add(_),k.push(_));return e.breakpoint!=="base"&&!w.has(e.breakpoint)&&k.push(e.breakpoint),k},[m,n.responsive,e.breakpoint]),v=k=>t({...e,colorScheme:k}),b=k=>t({...e,breakpoint:k}),x=()=>{const k=d.trim(),w=p.trim();if(k===""&&w===""){g(o("layout_editor.property_modal.scope.custom_invalid"));return}const _=`${k}-${w}`;if(!gC(_)){g(o("layout_editor.property_modal.scope.custom_invalid"));return}g(null),l(!1),c(""),f(""),b(_)};return i.jsxs("div",{className:"g7le-style-scope-tabs","data-testid":"g7le-style-scope-tabs",style:Z6,children:[r&&i.jsx("div",{className:"g7le-style-scope-scheme","data-testid":"g7le-style-scope-scheme",role:"tablist","aria-label":o("layout_editor.property_modal.scope.scheme_light"),style:Xf,children:J6.map(k=>{const w=e.colorScheme===k.key;return i.jsxs("button",{type:"button",role:"tab","aria-selected":w,"data-testid":`g7le-style-scheme-${k.key}`,"data-active":w?"true":"false",onClick:()=>v(k.key),style:Jf(w),children:[o(k.labelKey),k.key==="dark"&&Gs(n,{colorScheme:"dark",breakpoint:e.breakpoint})&&i.jsx("span",{"data-testid":"g7le-style-scheme-dark-dot",style:Zf})]},k.key)})}),i.jsxs("div",{className:"g7le-style-scope-device","data-testid":"g7le-style-scope-device",role:"tablist","aria-label":o("layout_editor.property_modal.scope.device_base"),style:Xf,children:[h.map(k=>{const w=e.breakpoint===k,_=k==="base"||wu(k),C=k==="base"?"layout_editor.property_modal.scope.device_base":X6[k],E=C?o(C):k,S=_?`g7le-style-bp-${k}`:`g7le-style-bp-custom-${k}`;return i.jsxs("button",{type:"button",role:"tab","aria-selected":w,"data-testid":S,"data-active":w?"true":"false",onClick:()=>b(k),style:Jf(w),children:[E,Gs(n,{colorScheme:"base",breakpoint:k})&&i.jsx("span",{"data-testid":`${S}-dot`,style:Zf})]},String(k))}),i.jsxs("button",{type:"button","data-testid":"g7le-style-bp-add-custom",onClick:()=>l(k=>!k),style:eT,children:["+ ",o("layout_editor.property_modal.scope.add_custom")]})]}),a&&i.jsxs("div",{className:"g7le-style-scope-custom-input","data-testid":"g7le-style-scope-custom-input",style:Q6,children:[i.jsx("input",{type:"number","data-testid":"g7le-style-custom-min",placeholder:o("layout_editor.property_modal.scope.custom_min"),value:d,onChange:k=>c(k.target.value),style:Qf}),i.jsx("span",{style:{color:"#94a3b8"},children:"—"}),i.jsx("input",{type:"number","data-testid":"g7le-style-custom-max",placeholder:o("layout_editor.property_modal.scope.custom_max"),value:p,onChange:k=>f(k.target.value),style:Qf}),i.jsx("button",{type:"button","data-testid":"g7le-style-custom-confirm",onClick:x,style:tT,children:o("layout_editor.property_modal.scope.add_custom")})]}),y&&i.jsx("div",{"data-testid":"g7le-style-custom-error",style:nT,children:y}),e.breakpoint!=="base"&&!wu(e.breakpoint)&&i.jsx("div",{"data-testid":"g7le-style-custom-priority-note",style:oT,children:o("layout_editor.property_modal.scope.custom_priority_note")}),s&&Gs(n,e)&&i.jsxs("button",{type:"button","data-testid":"g7le-style-scope-reset",onClick:s,style:R6,children:["↺ ",o("layout_editor.property_modal.scope.reset_to_base")]})]})}const Z6={display:"flex",flexDirection:"column",gap:0,padding:"0 0 8px",borderBottom:"1px solid #e2e8f0",marginBottom:8},Q6={display:"flex",gap:6,alignItems:"center",marginTop:4},Xf={display:"flex",gap:2,flexWrap:"wrap",borderBottom:"1px solid #e2e8f0",margin:"0 -16px",padding:"0 12px"};function Jf(e){return{display:"inline-flex",alignItems:"center",gap:4,padding:"6px 12px",fontSize:12,border:"none",borderBottom:`2px solid ${e?"#2563eb":"transparent"}`,background:"transparent",color:e?"#2563eb":"#64748b",cursor:"pointer",fontWeight:e?700:500,marginBottom:-1}}const Zf={display:"inline-block",width:5,height:5,borderRadius:3,background:"#f59e0b"},R6={alignSelf:"flex-start",marginTop:2,padding:"3px 8px",fontSize:10,border:"1px solid #cbd5e1",borderRadius:6,background:"#f8fafc",color:"#475569",cursor:"pointer"},eT={padding:"6px 12px",fontSize:11,border:"none",borderBottom:"2px dashed #cbd5e1",background:"transparent",color:"#475569",cursor:"pointer",marginBottom:-1},Qf={width:70,padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6},tT={padding:"4px 10px",fontSize:11,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},nT={fontSize:11,color:"#dc2626"},oT={fontSize:10,color:"#94a3b8"},Rf=new Map;function Pn(e,t){Rf.set(e,t)}function rT(e){return e?Rf.get(e)??null:null}const za={id:{label:"$t:layout_editor.core_props.id.label",widget:"core-id",placeholder:"$t:layout_editor.core_props.id.placeholder",apply:{type:"propValue",propKey:"id"}},dataKey:{label:"$t:layout_editor.core_props.dataKey.label",widget:"core-datakey",placeholder:"$t:layout_editor.core_props.dataKey.placeholder",apply:{type:"nodeKey",nodeKey:"dataKey"}},isolatedState:{label:"$t:layout_editor.core_props.isolatedState.label",widget:"toggle",apply:{type:"nodeKey",nodeKey:"isolatedState"}},isolatedScopeId:{label:"$t:layout_editor.core_props.isolatedScopeId.label",widget:"core-scopeid",placeholder:"$t:layout_editor.core_props.isolatedScopeId.placeholder",apply:{type:"nodeKey",nodeKey:"isolatedScopeId"}}},iT=["id"];function sT(e){return e===!1?[]:Array.isArray(e)?e.filter(t=>t in za):[...iT]}function eg(e){const t=e.props?.id??e.id;return typeof t=="string"&&t.length>0?`${t}-scope`:"scope"}function aT({node:e,onPatchNode:t,t:n,scopeIdCandidates:o=[],usedScopeIds:r=[]}){const s=e,a="isolatedState"in s&&s.isolatedState!=null,l=typeof s.isolatedScopeId=="string"?s.isolatedScopeId:"",[d,c]=u.useState(""),p=()=>{const h={...e};a?(delete h.isolatedState,delete h.isolatedScopeId):(h.isolatedState={},h.isolatedScopeId=eg(e)),t(h)},f=h=>{const v={...e};v.isolatedScopeId=h.trim()===""?eg(e):h,t(v)},y=h=>{const v={...e};v.isolatedState=h,t(v)},g=u.useMemo(()=>{const h=d.trim().toLowerCase();return o.filter(v=>!h||v.toLowerCase().includes(h))},[o,d]),m=l!==""&&r.includes(l);return i.jsxs("div",{className:"g7le-isolated-scope","data-testid":"g7le-isolated-scope-control",style:lT,children:[i.jsx("div",{style:dT,children:n("layout_editor.core_props.isolatedState.label")}),i.jsxs("div",{style:cT,children:[i.jsx("button",{type:"button","data-testid":"g7le-isolated-toggle",role:"switch","aria-checked":a,onClick:p,style:uT(a),children:n(a?"layout_editor.overlay.on":"layout_editor.overlay.off")}),a?null:i.jsx("span",{"data-testid":"g7le-isolated-off-hint",style:pT,children:n("layout_editor.core_props.isolatedState.off_hint")})]}),a?i.jsxs(i.Fragment,{children:[i.jsxs("div",{style:tg,children:[i.jsx("label",{style:ng,children:n("layout_editor.core_props.isolatedScopeId.label")}),i.jsx("input",{type:"text","data-testid":"g7le-isolated-scopeid-input",value:l,placeholder:n("layout_editor.core_props.isolatedScopeId.placeholder"),onChange:h=>f(h.target.value),style:fT}),g.length>0?i.jsxs("div",{"data-testid":"g7le-isolated-scopeid-picker",style:gT,children:[i.jsx("input",{type:"text","data-testid":"g7le-isolated-scopeid-search",value:d,placeholder:n("layout_editor.core_props.isolatedScopeId.search"),onChange:h=>c(h.target.value),style:hT}),i.jsx("div",{style:yT,children:g.map(h=>i.jsx("button",{type:"button","data-testid":`g7le-isolated-scopeid-candidate-${h}`,onClick:()=>{f(h),c("")},style:bT,children:h},h))})]}):null,m?i.jsxs("p",{"data-testid":"g7le-isolated-scopeid-dup",style:mT,children:["ⓘ ",n("layout_editor.core_props.isolatedScopeId.duplicate")]}):null]}),i.jsxs("div",{style:tg,children:[i.jsx("label",{style:ng,children:n("layout_editor.core_props.isolatedState.initial_label")}),i.jsx("div",{"data-testid":"g7le-isolated-initial-value",children:i.jsx(Nn,{value:s.isolatedState??{},onChange:y,t:n,path:"isolatedState",scope:"isolated-node"})})]})]}):null]})}const lT={display:"flex",flexDirection:"column",gap:8,padding:10,border:"1px solid #e2e8f0",borderRadius:8,minWidth:0},dT={fontSize:12,fontWeight:700,color:"#0f172a"},cT={display:"flex",gap:8,alignItems:"center"},uT=e=>({padding:"4px 12px",fontSize:11,border:`1px solid ${e?"#3b82f6":"#cbd5e1"}`,borderRadius:14,background:e?"#eff6ff":"#fff",color:e?"#1d4ed8":"#64748b",cursor:"pointer"}),pT={fontSize:11,color:"#94a3b8"},tg={display:"flex",flexDirection:"column",gap:4,minWidth:0},ng={fontSize:11,color:"#64748b"},fT={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box",width:"100%"},gT={border:"1px solid #e2e8f0",borderRadius:6,padding:4,background:"#fff"},hT={width:"100%",padding:"4px 6px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,marginBottom:4,boxSizing:"border-box"},yT={display:"flex",flexDirection:"column",maxHeight:120,overflowY:"auto"},bT={textAlign:"left",padding:"4px 6px",fontSize:12,border:"none",background:"transparent",cursor:"pointer",borderRadius:4,color:"#0f172a"},mT={margin:0,fontSize:11,color:"#64748b"},vT={borderTop:"2px solid #c7d2fe",background:"#eef2ff",padding:"10px 12px",display:"flex",flexDirection:"column",gap:8},og={fontSize:13,fontWeight:700,color:"#3730a3",display:"flex",alignItems:"center",gap:6},_T={fontSize:11,fontWeight:600,color:"#4338ca",background:"#e0e7ff",border:"1px solid #c7d2fe",borderRadius:4,padding:"1px 6px"},Ma={fontSize:11,color:"#6366f1"},xT={alignSelf:"flex-start",fontSize:12,fontWeight:600,color:"#fff",background:"#4f46e5",border:"none",borderRadius:4,padding:"4px 10px",cursor:"pointer"},wT={opacity:.5,cursor:"not-allowed"},kT={fontSize:11,color:"#dc2626"};function ST({entry:e,t,onSave:n}){const[o,r]=u.useState(()=>e.props),[s,a]=u.useState(!0),[l,d]=u.useState(null),[c,p]=u.useState(!1),[f,y]=u.useState(!1),g=e.extensionName??e.extensionIdentifier??`#${e.extensionId}`,m=async()=>{if(!s){d(t("layout_editor.property_modal.injected_props.invalid_json"));return}d(null),p(!0),y(!1);try{await n(o),y(!0)}catch(h){d(h instanceof Error?h.message:String(h))}finally{p(!1)}};return i.jsxs("div",{"data-testid":`g7le-injected-props-entry-${e.extensionId}`,style:{display:"flex",flexDirection:"column",gap:6},children:[i.jsx("div",{style:og,children:i.jsx("span",{style:_T,"data-testid":"g7le-injected-props-origin-badge",children:t("layout_editor.property_modal.injected_props.origin",{source:g})})}),i.jsx(Bs,{value:o,shape:"object",emptyValue:{},testidPrefix:"g7le-injected-props-json",minHeight:80,invalidErrorKey:"layout_editor.property_modal.injected_props.invalid_json",shapeErrorKey:"layout_editor.property_modal.injected_props.invalid_json",t,onChange:h=>{r(h??{}),y(!1)},onValidityChange:h=>{a(h),h&&d(null),y(!1)}}),l&&i.jsx("div",{style:kT,"data-testid":"g7le-injected-props-error",children:l}),f&&i.jsx("div",{style:{...Ma,color:"#16a34a"},"data-testid":"g7le-injected-props-saved",children:t("layout_editor.property_modal.injected_props.saved")}),i.jsx("div",{style:Ma,children:t("layout_editor.property_modal.injected_props.save_target")}),i.jsx("button",{type:"button","data-testid":"g7le-injected-props-save",style:{...xT,...c||!s?wT:null},disabled:c||!s,onClick:m,children:c?t("layout_editor.property_modal.injected_props.saving"):t("layout_editor.property_modal.injected_props.save_button",{source:g})})]})}function CT({injectedProps:e,t,onSaveInjectedProps:n}){return!Array.isArray(e)||e.length===0?null:i.jsxs("div",{style:vT,"data-testid":"g7le-injected-props-section",children:[i.jsxs("div",{style:og,children:["🧩 ",t("layout_editor.property_modal.injected_props.title")]}),i.jsx("div",{style:Ma,children:t("layout_editor.property_modal.injected_props.help")}),e.map(o=>i.jsx(ST,{entry:o,t,onSave:r=>n(o.extensionId,r)},o.extensionId))]})}function $T(e,t){const n=e?.components??{};for(const o of Object.values(n)){const r=o?.find(s=>s.name===t);if(r)return r}return null}function ET({node:e,spec:t,manifest:n,t:o,onPatchNode:r,onClose:s,onDelete:a,permissionCandidates:l,pageCandidates:d,dataSourceCandidates:c,stateKeyCandidates:p,bindingCandidates:f,liveElement:y,scope:g=Wt,onScopeChange:m,templateIdentifier:h,initialTab:v,onSaveInjectedProps:b,branchLabel:x,isolatedScopeIdCandidates:k,usedScopeIds:w}){const _=e.__injectedProps,C=Array.isArray(_)&&_.length>0&&!!b,E=ve=>{m?.(ve)},S=()=>{r(xC(e,g))},T=typeof e.name=="string"?e.name:"",$=Oi(t,T),z=$T(n,T)?.settings,D=!!(z?.groups&&z.groups.length>0),O=Array.isArray($?.propControls)?$.propControls:[],M=sT($?.coreProps),B=new Set(M.map(ve=>za[ve].apply?.propKey)),N=O.filter(ve=>{const he=Ct(t,ve)?.apply?.propKey;return!(he!==void 0&&B.has(he))}),H=typeof $?.nodeEditor?.kind=="string"?$.nodeEditor.kind:void 0,L=$?.nodeEditor?.params,I=H?rT(H):null,P=!!I,F=Array.isArray($?.dataProps)?$.dataProps:[],G=F.length>0,V=!!(e.iteration&&typeof e.iteration=="object"),Y=D6(e,$),Z=M.length>0||N.length>0||P||G||Y||V,se=Array.isArray($?.styleControls)?$.styleControls:[],re=Array.isArray($?.advanced)?$.advanced:[],ee=(Array.isArray($?.events)?$.events:[]).length>0,fe=Vf($),K=se.length>0||fe!==null,me=!($?.visibilityCondition===!1)&&(Z||K||ee||D||re.length>0),Ce=u.useMemo(()=>{const ve=[];return D&&ve.push("settings"),Z&&ve.push("props"),K&&ve.push("style"),ee&&ve.push("action"),me&&ve.push("visibility"),ve.push("translation"),re.length>0&&ve.push("advanced"),ve},[D,Z,K,ee,me,re.length]),ze=N.length>0||P||G||V,[Se,qe]=u.useState(v&&Ce.includes(v)?v:D?"settings":ze?"props":K?"style":Z?"props":Ce[0]??"style"),De=_A(e),Pe=!Z&&!K&&!D&&!ee&&!me&&re.length===0,Qe=$?.label?$.label.startsWith("$t:")?o($.label.slice(3)):$.label:T||o("layout_editor.property_modal.element"),Me=(ve,te)=>{const he={...e.props??{}};te===void 0?delete he[ve]:he[ve]=te,r({...e,props:he})},Re={settings:o("layout_editor.property_modal.tab.settings"),props:o("layout_editor.property_modal.tab.props"),style:o("layout_editor.property_modal.tab.style"),action:o("layout_editor.property_modal.tab.action"),visibility:o("layout_editor.property_modal.tab.visibility"),translation:o("layout_editor.property_modal.tab.translation"),advanced:o("layout_editor.property_modal.tab.advanced")};return i.jsxs("div",{className:"g7le-property-modal","data-testid":"g7le-property-modal",style:IT,children:[i.jsxs("div",{"data-modal-drag-handle":!0,"data-testid":"g7le-property-modal-header",style:AT,children:[i.jsx("span",{"data-testid":"g7le-property-modal-title",style:{fontSize:14,fontWeight:700},children:D&&Se==="settings"?o("layout_editor.property_modal.settings_title",{name:Qe}):o("layout_editor.property_modal.title",{name:Qe})}),i.jsx("button",{type:"button","data-testid":"g7le-property-modal-close","aria-label":"close",onClick:s,style:TT,children:"✕"})]}),De&&i.jsx("div",{"data-testid":"g7le-property-modal-advanced-badge",style:DT,children:o("layout_editor.property_modal.advanced_badge")}),x&&i.jsx("div",{"data-testid":"g7le-property-modal-branch-badge",style:NT,children:o("layout_editor.property_modal.branch_badge",{device:x})}),i.jsx("div",{style:zT,role:"tablist",children:Ce.map(ve=>i.jsx("button",{type:"button",role:"tab","aria-selected":Se===ve,"data-testid":`g7le-property-tab-${ve}`,"data-active":Se===ve?"true":"false",onClick:()=>qe(ve),style:{...MT,color:Se===ve?"#2563eb":"#64748b",borderBottomColor:Se===ve?"#2563eb":"transparent"},children:Re[ve]},ve))}),i.jsxs("div",{style:LT,"data-testid":"g7le-property-modal-body",children:[Pe&&!C&&i.jsx("div",{"data-testid":"g7le-property-modal-no-editable",style:ig,children:o("layout_editor.property_modal.no_editable_props")}),C&&i.jsx(CT,{injectedProps:_,t:o,onSaveInjectedProps:b}),Se==="settings"&&z&&i.jsx(lA,{spec:z,node:e,t:o,onPatchProp:Me}),Se==="props"&&i.jsxs("div",{"data-testid":"g7le-property-props-tab",style:{display:"flex",flexDirection:"column"},children:[V&&i.jsx(d6,{node:e,candidates:f??[],t:o,onPatchNode:r}),G&&i.jsx(OA,{node:e,dataProps:F,candidates:f??[],t:o,onPatchNode:r}),Y&&i.jsx(r6,{node:e,candidates:f??[],t:o,onPatchNode:r,templateIdentifier:h}),P&&I&&i.jsx(I,{node:e,params:L,spec:t,manifest:n,t:o,onPatchNode:r,templateIdentifier:h,candidates:f}),M.map(ve=>i.jsx(pi,{controlKey:`core-${ve}`,control:za[ve],node:e,t:o,onPatch:r,scope:Wt,bindingCandidates:f},`core-${ve}`)),N.map(ve=>{const te=Ct(t,ve);return te?i.jsx(pi,{controlKey:ve,control:te,node:e,t:o,onPatch:r,scope:Wt,bindingCandidates:f},ve):null}),i.jsx(aT,{node:e,onPatchNode:r,t:o,scopeIdCandidates:k,usedScopeIds:w})]}),Se==="style"&&i.jsxs("div",{"data-testid":"g7le-property-style-tab",style:{display:"flex",flexDirection:"column"},children:[i.jsx("div",{style:rg,children:i.jsx(Yf,{scope:g,onChange:E,node:e,t:o,onClearScope:S})}),fe!==null&&i.jsx(O6,{node:e,spec:t,capability:$,t:o,onPatchNode:r,liveElement:y,scope:g}),se.map(ve=>{const te=Ct(t,ve);return te?i.jsx(pi,{controlKey:ve,control:te,node:e,t:o,onPatch:r,scope:g},ve):null})]}),Se==="action"&&i.jsx(zA,{node:e,spec:t,capability:$,t:o,onPatchNode:r,pageCandidates:d,dataSourceCandidates:c,stateKeyCandidates:p,bindingCandidates:f}),Se==="visibility"&&i.jsxs("div",{"data-testid":"g7le-property-visibility-tab",style:{display:"flex",flexDirection:"column"},children:[i.jsx("div",{style:rg,children:i.jsx(Yf,{scope:g,onChange:E,node:e,t:o,showColorScheme:!1})}),i.jsx(Eu,{node:e,spec:t,t:o,onPatchNode:r,dataSourceCandidates:c,stateKeyCandidates:p,scope:g})]}),Se==="translation"&&(h?i.jsx(Ud,{customKey:Vd(e),templateIdentifier:h,t:o,paramLabels:hv(e),onRemoveParam:ve=>{const te=typeof e.text=="string"?e.text:"";r({...e,text:mo(te,ve)});const he=Vd(e);h&&he&&kr(h,he,ve)}}):i.jsx("div",{"data-testid":"g7le-property-translation-deferred",style:ig,children:o("layout_editor.translation.not_a_key")})),Se==="advanced"&&i.jsx(wA,{node:e,advanced:re,t:o,onPatch:r,permissionCandidates:l})]}),i.jsxs("div",{style:PT,children:[i.jsx("span",{"data-testid":"g7le-property-modal-node-id",style:{fontSize:11,color:"#94a3b8"},children:typeof e.id=="string"||typeof e.id=="number"?`ID: ${e.id}`:""}),i.jsxs("div",{style:{display:"flex",gap:8},children:[i.jsx("button",{type:"button","data-testid":"g7le-property-modal-delete",onClick:a,style:OT,children:o("layout_editor.property_modal.delete_element")}),i.jsx("button",{type:"button","data-testid":"g7le-property-modal-done",onClick:s,style:BT,children:o("layout_editor.property_modal.close")})]})]})]})}const IT={display:"flex",flexDirection:"column",width:420,maxWidth:"92vw"},AT={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderBottom:"1px solid #e2e8f0",cursor:"move",userSelect:"none"},TT={border:"none",background:"transparent",fontSize:14,cursor:"pointer",color:"#64748b"},DT={margin:"8px 16px 0",padding:"6px 10px",fontSize:11,background:"#fef3c7",color:"#92400e",borderRadius:6},NT={margin:"8px 16px 0",padding:"6px 10px",fontSize:11,background:"#dbeafe",color:"#1e40af",borderRadius:6},zT={display:"flex",gap:2,padding:"0 12px",borderBottom:"1px solid #e2e8f0",overflowX:"auto"},MT={padding:"8px 10px",fontSize:12,border:"none",borderBottom:"2px solid transparent",background:"transparent",cursor:"pointer",whiteSpace:"nowrap"},LT={padding:16,overflowY:"auto",maxHeight:"60vh"},rg={position:"sticky",top:-16,zIndex:2,background:"#fff",margin:"-16px -16px 0",padding:"6px 16px 0"},ig={fontSize:12,color:"#94a3b8",padding:"16px 0",textAlign:"center"},PT={display:"flex",alignItems:"center",justifyContent:"space-between",padding:"12px 16px",borderTop:"1px solid #e2e8f0"},OT={padding:"6px 12px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},BT={padding:"6px 12px",fontSize:12,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",cursor:"pointer"},La="g7le:component-target-picked",Pa="g7le:component-target-pick-request";let WT=1;function jT({value:e,onChange:t,t:n,control:o}){const r=yo(),s=e==null?"":String(e),a=o?.allowManualInput!==!1,l=u.useCallback(()=>{if(typeof window>"u")return;const d=`pick-${WT++}`,p=r.stack[r.stack.length-1]?.id;p&&r.minimize(p,n("layout_editor.target_picker.minimized_hint"));const f=y=>{const g=y.detail;g?.requestId===d&&(window.removeEventListener(La,f),p&&r.restore(p),!g.cancelled&&typeof g.id=="string"&&t(g.id))};window.addEventListener(La,f),window.dispatchEvent(new CustomEvent(Pa,{detail:{requestId:d}}))},[r,t,n]);return i.jsx("div",{className:"g7le-component-target-picker","data-testid":"g7le-component-target-picker",children:i.jsxs("div",{style:{display:"flex",gap:6,alignItems:"center"},children:[a?i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--text","data-testid":"g7le-component-target-input",value:s,placeholder:n("layout_editor.target_picker.id_placeholder"),onChange:d=>t(d.target.value===""?void 0:d.target.value),style:{flex:1,minWidth:0,padding:"6px 8px",border:"1px solid #cbd5e1",borderRadius:6,fontSize:13}}):i.jsx("span",{"data-testid":"g7le-component-target-value",style:{flex:1,minWidth:0},children:s||n("layout_editor.target_picker.none")}),i.jsxs("button",{type:"button","data-testid":"g7le-component-target-pick",onClick:l,style:{display:"flex",alignItems:"center",gap:4,padding:"6px 10px",background:"#1e293b",color:"#fff",border:"none",borderRadius:6,cursor:"pointer",fontSize:13,whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",children:"🎯"}),n("layout_editor.target_picker.select_area")]})]})})}async function HT(e,t,n,o,r=fetch){const s=`/api/admin/templates/${encodeURIComponent(e)}/layout-extensions/${t}`;let a;try{a=await r(s,{headers:je(),credentials:"same-origin"})}catch(b){return{kind:"error",message:b instanceof Error?b.message:String(b)}}if(a.status===404)return{kind:"not_found"};if(!a.ok)return{kind:"error",message:`HTTP ${a.status}`};const l=await a.json().catch(()=>null),d=(l&&(l.data||l))??{};let c={};const p=d.content;if(typeof p=="string")try{c=JSON.parse(p)}catch{return{kind:"error",message:"invalid extension content"}}else p&&typeof p=="object"&&(c=p);const f=typeof d.lock_version=="number"?d.lock_version:0,y=Array.isArray(c.injections)?[...c.injections]:[],g=y.findIndex(b=>b&&b.position==="inject_props"&&b.target_id===n);if(g<0)return{kind:"injection_not_found"};y[g]={...y[g],props:o};const m={...c,injections:y};let h;try{h=await r(s,{method:"PUT",headers:je({"Content-Type":"application/json"}),credentials:"same-origin",body:JSON.stringify({content:m,expected_lock_version:f})})}catch(b){return{kind:"error",message:b instanceof Error?b.message:String(b)}}const v=await h.json().catch(()=>null);return h.ok?{kind:"success",newLockVersion:typeof v?.data?.lock_version=="number"?v.data.lock_version:f+1}:h.status===409?{kind:"conflict",currentVersion:v?.current_version??-1,yourVersion:v?.your_version??f}:{kind:"error",message:v?.message??`HTTP ${h.status}`}}const sg=new Map;function Oa(e,t){sg.set(e,t)}function FT(e){return e?sg.get(e)??null:null}const ag="g7le.clipboard";function lg(){try{return typeof window>"u"||!window.sessionStorage?null:window.sessionStorage}catch{return null}}function KT(e){const t=JSON.parse(JSON.stringify(e)),n=o=>{for(const r of Object.keys(o))r.startsWith("__")&&delete o[r];Array.isArray(o.children)&&o.children.forEach(n)};return n(t),t}function dg(e,t=0){const n=lg();if(!n)return!1;try{const o={node:KT(e),copiedAt:t};return n.setItem(ag,JSON.stringify(o)),!0}catch{return!1}}function VT(){const e=lg();if(!e)return null;try{const t=e.getItem(ag);if(!t)return null;const n=JSON.parse(t);return!n||typeof n.node!="object"||n.node===null?null:JSON.parse(JSON.stringify(n.node))}catch{return null}}const Xo={nw:{width:-1,height:-1},n:{width:0,height:-1},ne:{width:1,height:-1},e:{width:1,height:0},se:{width:1,height:1},s:{width:0,height:1},sw:{width:-1,height:1},w:{width:-1,height:0}};function Ba(e){if(typeof e=="number")return e;if(typeof e=="string"){const t=e.match(/^(-?\d+(?:\.\d+)?)px$/);if(t)return Number(t[1])}return null}function UT(e,t){const n=Array.isArray(e.options)?e.options:[],o=Array.isArray(e.scale)?e.scale:[],r=n.length>0?n.map(l=>l.value):o;if(r.length===0)return;let s=r[0],a=1/0;return r.forEach((l,d)=>{const c=Ba(l)??d*40,p=Math.abs(c-t);p<a&&(a=p,s=l)}),s}function qT(e,t){const n=typeof t.min=="number"?t.min:void 0,o=typeof t.max=="number"?t.max:void 0;let r=e;return n!==void 0&&(r=Math.max(n,r)),o!==void 0&&(r=Math.min(o,r)),r}function cg(e,t){const n=e.apply,o=qT(Math.max(0,Math.round(t)),e);return n?.type==="styleProp"?`${o}px`:n?.type==="classToken"&&n.tokenTemplate?`${o}px`:UT(e,o)}function GT(e){const{node:t,widthControl:n,heightControl:o,scale:r,measureStartSize:s,onResize:a,onResizeEnd:l,onResizeStateChange:d}=e,c={width:!!n,height:!!o},p=u.useRef(null),f=u.useCallback((y,g)=>{if(!t)return;const m=Xo[y];if(m.width!==0&&!n||m.height!==0&&!o)return;const h=t.props?.style??{},v=s?.()??null,b=Ba(h.width)??(v?Math.round(v.width):0),x=Ba(h.height)??(v?Math.round(v.height):0);p.current={handle:y,startX:g.clientX,startY:g.clientY,startW:b,startH:x,node:t};const k=m.width!==0&&m.height!==0?"both":m.width!==0?"width":"height";d?.(k);let w=null;const _=E=>{const S=p.current;if(!S)return;const T=r||1,$=(E.clientX-S.startX)/T*Xo[S.handle].width,A=(E.clientY-S.startY)/T*Xo[S.handle].height;let z=S.node;if(Xo[S.handle].width!==0&&n){const D=cg(n,S.startW+$);z=fn(z,n,D)}if(Xo[S.handle].height!==0&&o){const D=cg(o,S.startH+A);z=fn(z,o,D)}w=z,a(z,k)},C=()=>{p.current=null,d?.(null),window.removeEventListener("pointermove",_),window.removeEventListener("pointerup",C),w&&l?.(w,k)};window.addEventListener("pointermove",_),window.addEventListener("pointerup",C)},[t,n,o,r,a,l,d]);return{enabledAxes:c,onHandlePointerDown:f}}const ug=320,pg=1920,YT=1024;function Wa(e){return Number.isFinite(e)?Math.round(Math.max(ug,Math.min(pg,e))):YT}function ja(e={}){const{state:t,dispatch:n}=Je(),{previewDevice:o,previewZoom:r,previewCustomWidth:s,previewColorScheme:a}=t,l=o==="custom"?Wa(s):Y6(o),d=u.useMemo(()=>{const g=e.availableWidth??l,m=l*r;return r>=1||m<=g?r:Math.min(r,g/l)},[l,r,e.availableWidth]),c=u.useCallback(g=>{n({type:"SET_PREVIEW_DEVICE",device:g})},[n]),p=u.useCallback(g=>{const m=Math.max(.5,Math.min(1,g));n({type:"SET_PREVIEW_ZOOM",zoom:m})},[n]),f=u.useCallback(g=>{n({type:"SET_PREVIEW_CUSTOM_WIDTH",width:Wa(g)})},[n]),y=u.useCallback(g=>{n({type:"SET_PREVIEW_COLOR_SCHEME",scheme:g})},[n]);return{device:o,deviceWidth:l,zoom:r,scale:d,customWidth:Wa(s),setDevice:c,setZoom:p,setCustomWidth:f,colorScheme:a,setColorScheme:y}}const yi="g7le-dnd-handle-";function XT(e){if(typeof e!="string"||!e.startsWith(yi))return null;const t=e.slice(yi.length);return t.length>0?t:null}function JT(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/["\\\]]/g,"\\$&")}function ZT(e,t,n,o){const r=o?.handleFallback??!1;if(e instanceof HTMLElement){const s=e.closest("[data-editor-path]");if(s)return s;const a=e.closest('[data-testid^="g7le-"]');if(a&&!(a.getAttribute("data-testid")??"").startsWith(yi))return null;if(r){const l=e.closest(`[data-testid^="${yi}"]`);if(l&&t){const d=XT(l.getAttribute("data-testid"));if(d){const c=t.querySelector(`[data-editor-path="${JT(d)}"]`);if(c)return c}}}}if(r&&n&&typeof document<"u"&&typeof document.elementsFromPoint=="function")for(const s of document.elementsFromPoint(n.x,n.y)){const a=s.closest?.("[data-editor-path]");if(a)return a}return null}function QT(e,t,n){if(typeof e=="string"&&e.trim().length>0)return{color:e};const o=typeof n?.color=="string"&&n.color.length>0,r=typeof t=="string"&&t.trim().length>0;return o||r?{}:{color:"#0f172a"}}const RT="#ffffff",e4="#0f172a";function t4(e){const t=e.trim(),n=t.match(/^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i);if(n)return[Number(n[1]),Number(n[2]),Number(n[3])];const o=t.replace(/^#/,"");return/^[0-9a-f]{3}$/i.test(o)?[parseInt(o[0]+o[0],16),parseInt(o[1]+o[1],16),parseInt(o[2]+o[2],16)]:/^[0-9a-f]{6}$/i.test(o)?[parseInt(o.slice(0,2),16),parseInt(o.slice(2,4),16),parseInt(o.slice(4,6),16)]:null}function n4(e){if(!e||/transparent/i.test(e))return!1;const t=e.trim().match(/^oklch\(\s*([\d.]+)(%?)/i);if(t)return(t[2]==="%"?Number(t[1])/100:Number(t[1]))>=.62;const n=t4(e);if(!n)return!1;const[o,r,s]=n;return .299*o+.587*r+.114*s>=180}function o4(e,t,n){const o=typeof n?.color=="string"?n.color:null;return n4(e??o??t)?e4:RT}function r4({box:e,initialValue:t,isCustomKey:n,t:o,onCommit:r,onCancel:s,mirrorClassName:a,mirrorStyle:l,nodeEffectiveColor:d,onOpenTranslations:c}){const p=u.useRef(null),f=u.useRef(!1),[y,g]=u.useState(t.trim().length>0),[m,h]=u.useState(null);u.useEffect(()=>{const w=p.current;if(w){w.textContent=t,w.focus();try{const _=window.document.createRange();_.selectNodeContents(w);const C=window.getSelection();C?.removeAllRanges(),C?.addRange(_)}catch{}f.current=!1}},[t]),u.useEffect(()=>{const w=p.current;if(!w||typeof window.getComputedStyle!="function"){h(null);return}try{h(window.getComputedStyle(w).color||null)}catch{h(null)}},[a,l]);const v=()=>(p.current?.textContent??"").trim(),b=()=>{f.current||(f.current=!0,r(v()))},x=()=>{f.current||(f.current=!0,s())},k=w=>{w.key==="Enter"&&!w.shiftKey?(w.preventDefault(),b()):w.key==="Escape"&&(w.preventDefault(),x())};return i.jsxs("div",{className:"g7le-inline-text-editor","data-testid":"g7le-inline-text-editor","data-custom-key":n?"true":"false",style:{position:"absolute",left:e.left,top:e.top,minWidth:Math.max(e.width,60),minHeight:e.height,pointerEvents:"auto",zIndex:1e4},children:[i.jsx("div",{ref:p,role:"textbox","aria-label":o("layout_editor.inline_edit.aria_label"),"aria-multiline":"false","data-testid":"g7le-inline-text-editable",className:a,contentEditable:!0,suppressContentEditableWarning:!0,spellCheck:!1,onInput:()=>g(v().length>0),onKeyDown:k,onBlur:b,style:{outline:"2px solid #2563eb",outlineOffset:1,borderRadius:3,background:o4(d,m,l),padding:"2px 4px",minHeight:e.height,whiteSpace:"pre-wrap",wordBreak:"break-word",boxShadow:"0 2px 8px rgba(37, 99, 235, 0.25)",...l??{},...QT(d,a,l)}}),i.jsxs("div",{className:"g7le-inline-text-editor__hint","data-testid":"g7le-inline-text-editor-hint",style:{marginTop:2,display:"inline-flex",alignItems:"center",gap:4,whiteSpace:"nowrap"},children:[c?i.jsxs("button",{type:"button","data-testid":"g7le-inline-text-editor-open-translations",onMouseDown:w=>w.preventDefault(),onClick:w=>{w.preventDefault(),w.stopPropagation(),c()},title:o("layout_editor.inline_edit.open_translations"),style:{padding:"2px 8px",fontSize:11,background:n?"#eff6ff":"#f0fdf4",color:n?"#1d4ed8":"#15803d",border:`1px solid ${n?"#bfdbfe":"#bbf7d0"}`,borderRadius:4,display:"inline-flex",alignItems:"center",gap:4,cursor:"pointer",whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",children:"🌐"}),i.jsx("span",{children:o(n?"layout_editor.inline_edit.existing_key_hint":"layout_editor.inline_edit.new_key_hint")}),i.jsxs("span",{"aria-hidden":"true",style:{opacity:.7},children:["· ",o("layout_editor.inline_edit.open_translations")," ›"]})]}):i.jsx("span",{style:{padding:"2px 6px",fontSize:11,background:n?"#eff6ff":"#f0fdf4",color:n?"#1d4ed8":"#15803d",border:`1px solid ${n?"#bfdbfe":"#bbf7d0"}`,borderRadius:4,display:"inline-flex",alignItems:"center",gap:4,whiteSpace:"nowrap"},children:n?i.jsxs(i.Fragment,{children:["⌐ ",o("layout_editor.inline_edit.existing_key_hint")]}):i.jsxs(i.Fragment,{children:["✎ ",o("layout_editor.inline_edit.new_key_hint")]})}),!y&&i.jsx("span",{"data-testid":"g7le-inline-text-editor-empty-warn",style:{color:"#b91c1c",marginLeft:4},children:o("layout_editor.inline_edit.empty_warning")})]})]})}function i4({box:e,node:t,customKey:n,initialChipValue:o,chipParamLabels:r,onKeyify:s,templateIdentifier:a,locale:l,t:d,candidates:c,onInsertBinding:p,onRemoveBinding:f,onCommit:y,onCancel:g}){const[m,h]=u.useState(null),v=u.useRef(""),[b,x]=u.useState(!1),k=u.useRef(!1),w=u.useRef(null),_=Ft(),C=u.useRef(()=>{}),[E,S]=u.useState(null),[T,$]=u.useState(!1),A=u.useRef(n??null);u.useEffect(()=>{n&&A.current!==n&&(A.current=n)},[n]);const z=u.useMemo(()=>{if(!n&&r)return r;const L={},I=mt(typeof t.text=="string"?t.text:"");for(const P of I?.params??[])L[P.name]=Vt(P.expression);return L},[t.text,n,r]);u.useEffect(()=>{let L=!1;if(!n){const I=o??"";return v.current=I,h(I),()=>{L=!0}}return(async()=>{const I=It(n,l);if(I!==void 0){v.current=I,h(I);return}const P=await cn(a,n);if(L)return;const F=P?.values?.[l]??"";v.current=F,h(F)})(),()=>{L=!0}},[a,n,l,o]);const D=async()=>{if(k.current)return;k.current=!0;const L=m??"";if(L===v.current){y();return}x(!0);let I=A.current;if(!I){if(!s){x(!1),k.current=!1;return}if(I=await s(L),x(!1),!I){k.current=!1;return}A.current=I,y();return}const P=await ko(a,I,l,L);if(x(!1),P.kind==="error"){k.current=!1;return}_?.markDirty?.(),y()},O=()=>{k.current||(k.current=!0,g())},M=L=>{S(L)},B=async L=>{if(!(E===null||!p||T)){$(!0);try{const I=A.current;I&&m!==null&&m!==v.current&&(await ko(a,I,l,m),v.current=m);const P=await p(E,L);P!==null&&(h(P),v.current=P,_?.markDirty?.())}finally{$(!1),S(null)}}},N=async L=>{if(!(!f||T)){$(!0);try{const I=A.current;I&&m!==null&&m!==v.current&&(await ko(a,I,l,m),v.current=m);const P=await f(L);P!==null&&(h(P),v.current=P,_?.markDirty?.())}finally{$(!1)}}},H=L=>{L.key==="Enter"&&!L.shiftKey?(L.preventDefault(),D()):L.key==="Escape"&&(L.preventDefault(),O())};return C.current=()=>{D()},u.useEffect(()=>{const L=P=>{const F=w.current;if(!F)return;const G=P.target;if(G&&F.contains(G))return;const V=G;V&&V.closest&&V.closest('[data-testid="g7le-inline-toolbar"], .g7le-inline-toolbar')||C.current()},I=w.current?.ownerDocument??document;return I.addEventListener("pointerdown",L,!0),()=>I.removeEventListener("pointerdown",L,!0)},[]),i.jsxs("div",{ref:w,className:"g7le-inline-param-chip-editor","data-testid":"g7le-inline-param-chip-editor",onKeyDown:H,style:{position:"absolute",left:e.left,top:e.top,minWidth:Math.max(e.width,120),pointerEvents:"auto",zIndex:1e4,background:"#fff",border:"2px solid #2563eb",borderRadius:4,boxShadow:"0 2px 8px rgba(37,99,235,0.25)",padding:4},children:[m===null?i.jsx("div",{"data-testid":"g7le-inline-param-chip-loading",style:{fontSize:12,color:"#94a3b8",padding:4},children:d("layout_editor.translation.loading")}):i.jsx(mr,{value:m,onChange:h,t:d,paramLabels:z,testIdSuffix:"inline",onRequestInsert:p?M:void 0,onRemoveChip:f?L=>{N(L)}:void 0}),E!==null&&p&&i.jsx("div",{"data-testid":"g7le-inline-param-chip-insert-picker",style:{marginTop:4},children:i.jsx($t,{candidates:c??[],t:d,onSelect:L=>{B(L)},testIdSuffix:"inline-insert",defaultOpen:!0,floating:!1})}),i.jsxs("div",{style:{display:"flex",alignItems:"center",gap:6,marginTop:4},children:[i.jsx("button",{type:"button","data-testid":"g7le-inline-param-chip-save",disabled:b,onMouseDown:L=>L.preventDefault(),onClick:()=>{D()},style:{fontSize:11,border:"none",borderRadius:6,background:"#2563eb",color:"#fff",padding:"4px 12px",cursor:"pointer"},children:d(b?"layout_editor.translation.saving":"layout_editor.translation.save")}),i.jsx("span",{style:{fontSize:10,color:"#94a3b8"},children:d("layout_editor.translation.placeholder_locked")})]})]})}const s4=["fontWeight","fontBold","fontStyle","fontItalic","textDecoration","textUnderline","textAlign","fontSize","textColor"],a4={fontWeight:"B",fontBold:"B",fontStyle:"I",fontItalic:"I",textDecoration:"U",textUnderline:"U",textAlign:"≡",fontSize:"T",textColor:"A"},l4=[["fontWeight","fontBold"],["fontItalic","fontStyle"],["textUnderline","textDecoration"]];function fg({node:e,spec:t,t:n,onApplyControl:o,box:r,scope:s=Wt}){const a=typeof e.name=="string"?e.name:"",l=Oi(t,a),d=Array.isArray(l?.styleControls)?l.styleControls:[];let c=s4.filter(p=>d.includes(p)&&Ct(t,p)!==null);for(const p of l4){const f=p.filter(y=>c.includes(y));if(f.length>1){const y=f[0];c=c.filter(g=>!p.includes(g)||g===y)}}return c.length===0?null:i.jsx("div",{className:"g7le-inline-toolbar","data-testid":"g7le-inline-toolbar",role:"toolbar","aria-label":n("layout_editor.inline_edit.toolbar_label"),style:{position:"absolute",left:r.left,top:r.top-40,display:"inline-flex",gap:2,padding:4,background:"#0f172a",borderRadius:8,boxShadow:"0 4px 12px rgba(15, 23, 42, 0.35)",pointerEvents:"auto",zIndex:10001},children:c.map(p=>i.jsx(c4,{controlKey:p,node:e,spec:t,t:n,scope:s,onApplyControl:o},p))})}const d4={fontWeight:"bold",fontBold:"bold",fontStyle:"italic",fontItalic:"italic",textDecoration:"underline",textUnderline:"underline"};function gg(e,t){const n=r=>(r.split(/[-\s]/).pop()??r)===t||r===t;return e.find(r=>{const s=typeof r.value=="string"?r.value:"",a=Array.isArray(r.apply?.tokens)?r.apply.tokens:[];return n(s)||a.some(n)})?.value}function c4({controlKey:e,node:t,spec:n,t:o,scope:r,onApplyControl:s}){const[a,l]=u.useState(!1),d=Ct(n,e);if(!d)return null;const c=d.options,p=Array.isArray(c),f=Do(t,d,r),y=d4[e],g=y&&Array.isArray(c)?gg(c,y):void 0,m=!y,h=g!==void 0?f.value===g:p&&f.matched&&f.value!==void 0&&f.value!=="",v=o(`layout_editor.inline_edit.format.${e}`),b=_=>typeof _.label=="string"?_.label.startsWith("$t:")?o(_.label.slice(3)):_.label:String(_.value??""),x=()=>{s(e,_=>{const C=Do(_,d,r),E=Array.isArray(c)?gg(c,y):void 0,S=E!==void 0&&C.value===E?void 0:E;return fn(_,d,S,r)})},k=_=>{s(e,C=>{const S=Do(C,d,r).value===_?void 0:_;return fn(C,d,S,r)}),l(!1)},w=_=>{if(_.preventDefault(),_.stopPropagation(),!m&&Array.isArray(c)){x();return}l(C=>!C)};return i.jsxs("div",{style:{position:"relative",display:"inline-flex"},children:[i.jsxs("button",{type:"button","data-testid":`g7le-inline-format-${e}`,"data-active":h?"true":"false","aria-label":v,"aria-pressed":p?h:void 0,title:v,onMouseDown:_=>_.preventDefault(),onClick:w,style:{display:"inline-flex",alignItems:"center",gap:5,height:28,padding:"0 9px",fontSize:12,border:"none",borderRadius:5,cursor:"pointer",background:h?"#2563eb":"transparent",color:"#ffffff",whiteSpace:"nowrap"},children:[i.jsx("span",{"aria-hidden":"true",style:{fontWeight:e==="fontWeight"||e==="fontBold"?700:600,fontStyle:e==="fontStyle"||e==="fontItalic"?"italic":"normal",textDecoration:e==="textDecoration"||e==="textUnderline"?"underline":"none"},children:a4[e]}),i.jsx("span",{children:v}),m&&i.jsx("span",{"aria-hidden":"true",style:{fontSize:9,opacity:.8},children:"▾"})]}),m&&a&&i.jsx("div",{"data-testid":`g7le-inline-popover-${e}`,role:"listbox",onMouseDown:_=>_.preventDefault(),style:{position:"absolute",top:32,left:0,minWidth:140,maxHeight:240,overflowY:"auto",background:"#0f172a",border:"1px solid #334155",borderRadius:8,boxShadow:"0 8px 24px rgba(15,23,42,0.45)",padding:4,zIndex:10002},children:Array.isArray(c)&&c.length>0?c.map((_,C)=>{const E=f.value===_.value;return i.jsxs("button",{type:"button",role:"option","aria-selected":E,"data-testid":`g7le-inline-option-${e}-${String(_.value??C)}`,onMouseDown:S=>S.preventDefault(),onClick:S=>{S.preventDefault(),S.stopPropagation(),k(_.value)},style:{display:"flex",alignItems:"center",gap:8,width:"100%",padding:"6px 8px",fontSize:12,textAlign:"left",border:"none",borderRadius:5,cursor:"pointer",background:E?"#2563eb":"transparent",color:"#ffffff"},children:[typeof _.swatch=="string"&&i.jsx("span",{"aria-hidden":"true",style:{width:14,height:14,borderRadius:3,background:_.swatch,border:"1px solid rgba(255,255,255,0.3)"}}),i.jsx("span",{children:b(_)}),E&&i.jsx("span",{"aria-hidden":"true",style:{marginLeft:"auto"},children:"✓"})]},String(_.value??C))}):i.jsx(u4,{testid:`g7le-inline-freeinput-${e}`,placeholder:o(`layout_editor.inline_edit.prompt.${e}`),initial:typeof f.value=="string"?f.value:"",onSubmit:_=>{s(e,C=>fn(C,d,_,r)),l(!1)}})})]})}function u4({testid:e,placeholder:t,initial:n,onSubmit:o}){const[r,s]=u.useState(n);return i.jsx("input",{type:"text","data-testid":e,value:r,placeholder:t,autoFocus:!0,onMouseDown:a=>a.stopPropagation(),onChange:a=>s(a.currentTarget.value),onKeyDown:a=>{a.key==="Enter"&&(a.preventDefault(),o(r.trim()))},style:{width:130,padding:"6px 8px",fontSize:12,border:"1px solid #334155",borderRadius:5,background:"#1e293b",color:"#ffffff"}})}function p4(e){const{frameEl:t,manifest:n,nesting:o,componentPalette:r,spec:s=null,permissionCandidates:a,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c,onEditProps:p}=e,{state:f,dispatch:y}=Je(),{t:g}=tt(),m=ja(),h=u.useCallback((W,X)=>{const R=window.G7Core;if(R&&typeof R.t=="function"){const de=R.t(W,X);if(de&&de!==W&&!de.startsWith("$t:"))return de}return g(W,X)},[g]),v=Ft(),b=d7(50),x=yo(),k=u.useRef([]),w=u.useMemo(()=>v?.document?.raw?.components??[],[v?.document?.raw?.components]),_=u.useMemo(()=>w.length===0?null:{children:w},[w]),C=u.useMemo(()=>{if(f.editMode!=="extension")return;const W=/^__extension__\/(\d+)/.exec(f.selectedRoute?.path??"");return W?Number(W[1]):void 0},[f.editMode,f.selectedRoute?.path]),E=u.useMemo(()=>{if(f.editMode==="iteration_item"){const W=v?.document?.iterationContext?.sourceIndexPath;return Array.isArray(W)&&W.length>0?W:null}if(f.editMode==="modal"){const W=v?.document?.modalContext?.editIndexPath;return Array.isArray(W)&&W.length>0?W:null}return null},[f.editMode,v?.document?.iterationContext?.sourceIndexPath,v?.document?.modalContext?.editIndexPath]),S=e7({rootNode:_,editMode:f.editMode,currentExtensionId:C,editableRootPath:E,pathParser:Ze,resolveRouteMatch:l}),[T,$]=u.useState(null),[A,z]=u.useState(null),[D,O]=u.useState(null),M=u.useRef(null);M.current=D;const[B,N]=u.useState(null),H=u.useRef(null),L=u.useRef(null);L.current=S.selectedPath;const I=Wb(),[P,F]=u.useState(null),G=u.useCallback(W=>{if(!t)return;const X=Ze(W);if(!X)return;const R={children:Me.current.components},de=We(R,X),ye=I.classify(de);if(!ye.editable){Et({op:"inline_edit_blocked_binding",sourceState:ye.sourceState,componentPath:at(X),timestamp:Date.now()});return}const we=po(t,W);if(!we)return;const Te=de?.props??{},ke=yg(t,W);F({path:X,domPath:W,box:we,initialValue:ye.displayValue,isCustomKey:ye.sourceState==="custom_key",isParamKey:ye.isParamKey===!0,customKey:ye.customKey,chipValue:ye.chipValue,chipParamLabels:ye.chipParamLabels,mirrorClassName:typeof Te.className=="string"?Te.className:void 0,mirrorStyle:Te.style??void 0,nodeEffectiveColor:ke}),Et({op:"inline_edit_enter",sourceState:ye.sourceState,componentPath:at(X),timestamp:Date.now()})},[t,I]),V=u.useCallback(()=>{if(!t){$(null),z(null);return}z(po(t,S.selectedPath)),$(po(t,S.hoverPath))},[t,S.selectedPath,S.hoverPath,w]);u.useEffect(()=>{if(V(),typeof window<"u"&&typeof window.requestAnimationFrame=="function"){const W=window.requestAnimationFrame(()=>V());return()=>window.cancelAnimationFrame(W)}},[V]),u.useEffect(()=>{if(t)return $p(V,t)},[t,V]);const Y=u.useCallback((W,X)=>{if(typeof window>"u")return;window.dispatchEvent(new CustomEvent(La,{detail:X===null?{requestId:W,cancelled:!0}:{requestId:W,id:X}})),O(null),N(null);const R=H.current;R?S.handleSelect("",{dataset:{editorPath:R}}):S.clearSelection(),H.current=null},[S]);u.useEffect(()=>{if(typeof window>"u")return;const W=X=>{const R=X.detail;if(typeof R?.requestId!="string")return;const de=M.current;de&&de!==R.requestId&&Y(de,null),H.current=L.current,O(R.requestId)};return window.addEventListener(Pa,W),()=>window.removeEventListener(Pa,W)},[Y]),u.useEffect(()=>{if(!D)return;const W=X=>{X.key==="Escape"&&(X.preventDefault(),Y(D,null))};return window.addEventListener("keydown",W,!0),()=>window.removeEventListener("keydown",W,!0)},[D,Y]),u.useEffect(()=>{if(!t)return;const W=f.editMode==="iteration_item"||f.editMode==="modal",X=(xe,Ee)=>ZT(xe,t,Ee?{x:Ee.clientX,y:Ee.clientY}:void 0,{handleFallback:W}),R=xe=>{const Ee=xe.dataset.editorPath??"",Fe=Ee?Ze(Ee):null,zt={children:Me.current.components},gt=Fe?We(zt,Fe):null,Mt=gt&&typeof gt.props?.id=="string"?gt.props.id:typeof gt?.id=="string"?gt.id:"";return Mt.startsWith("auto_")?"":Mt},de=xe=>{const Ee=X(xe.target,xe);if(!Ee)return;const Fe=M.current;if(Fe){xe.preventDefault(),xe.stopPropagation();const zt=R(Ee);zt&&Y(Fe,zt);return}xe.preventDefault(),xe.stopPropagation(),S.handleSelect(Ee.dataset.editorId??"",{currentTarget:Ee,dataset:Ee.dataset})},ye=xe=>{const Ee=X(xe.target,xe);if(M.current){if(!Ee){N(null);return}const Fe=po(t,Ee.dataset.editorPath??null);if(!Fe){N(null);return}N({box:Fe,hasId:R(Ee)!==""});return}if(!Ee){S.handleHover(null,{});return}S.handleHover(Ee.dataset.editorId??"",{currentTarget:Ee,dataset:Ee.dataset})},we=()=>{if(M.current){N(null);return}S.handleHover(null,{})},Te=xe=>{const Ee=X(xe.target,xe);if(!Ee)return;const Fe=Ee.dataset.editorPath??"";Fe&&(xe.preventDefault(),xe.stopPropagation(),G(Fe))},ke=t.parentElement??t;return ke.addEventListener("click",de,!0),ke.addEventListener("mousemove",ye,!0),ke.addEventListener("mouseleave",we,!0),ke.addEventListener("dblclick",Te,!0),()=>{ke.removeEventListener("click",de,!0),ke.removeEventListener("mousemove",ye,!0),ke.removeEventListener("mouseleave",we,!0),ke.removeEventListener("dblclick",Te,!0)}},[t,S,G,f.editMode,Y]);const Z=u.useMemo(()=>{const W=S.selectedPath;if(!W)return null;const X=W.match(/^(.*)\.children\.\d+$/);return X?X[1]:null},[S.selectedPath]),se=u.useCallback(()=>{Z&&S.handleSelect("",{dataset:{editorPath:Z}})},[Z,S]),re=u.useMemo(()=>!t||!S.selectedPath?null:t.querySelector(`[data-editor-path="${an(S.selectedPath)}"]`),[t,S.selectedPath]),U=u.useMemo(()=>S.selectedPath?Ze(S.selectedPath):null,[S.selectedPath]),ee=u.useMemo(()=>{if(!U)return null;const W=U.find(et);if(!W)return null;const{i18nKey:X,raw:R}=bC(W.responsive);return X?g(X):g("layout_editor.overlay.branch_custom",{range:R})},[U,g]),fe=u.useMemo(()=>S.selectedPath?S.selectedIsIteration&&S.selectedIterationSourcePath?S.selectedIterationSourcePath:S.selectedPath:null,[S.selectedPath,S.selectedIsIteration,S.selectedIterationSourcePath]),K=u.useMemo(()=>fe?Ze(fe):null,[fe]),ne=u.useMemo(()=>!t||!fe?null:t.querySelector(`[data-editor-path="${an(fe)}"]`),[t,fe]),me=u.useMemo(()=>{if(ne?.parentElement)return ne.parentElement;if(!t||!fe)return null;const W=fe.match(/^(.*)\.children\.\d+$/);return W?t.querySelector(`[data-editor-path="${an(W[1])}"]`):null},[ne,t,fe]),Ce=u.useMemo(()=>{const W=S.selectedNode;return W&&(typeof W.name=="string"?W.name:typeof W.type=="string"?W.type:"")||null},[S.selectedNode]),ze=u.useMemo(()=>{if(S.selectedLockKind!=="extension")return null;const W=S.selectedNode?.__source;if(!W||W.kind!=="extension")return null;const X=W.extensionName?.trim(),R=W.extensionIdentifier?.trim();return X&&R?`${X} (${R})`:X||R||W.extensionSourceType||null},[S.selectedLockKind,S.selectedNode]),Se=s7({parentEl:me,selectedPath:K,selectedEl:ne}),qe=u.useMemo(()=>!t||!fe||fe===S.selectedPath?A:po(t,fe),[t,fe,A,S.selectedPath,w]),De=u.useRef(null),Pe=u.useCallback(()=>{const W=De.current;W&&(x.close(W),De.current=null)},[x]),Qe=u.useRef(()=>{}),Me=u.useRef({manifest:n,nesting:o,componentPalette:r,components:w});Me.current={manifest:n,nesting:o,componentPalette:r,components:w};const Re=u.useCallback(W=>{const X=W.insertion?.parentPath??[],R=W.insertion?.index??0,de=Me.current,ye=hg(de.components,X);Pe();const we=x.open({ariaLabel:h("layout_editor.palette.title"),width:880,maxHeightRatio:.82,content:u.createElement(Ef,{manifest:de.manifest,nesting:de.nesting,componentPalette:de.componentPalette??null,targetParentPath:X,targetContainerName:ye,onInsert:(Te,ke,xe)=>{Qe.current(Te,ke,xe)},insertionIndex:R,onClose:()=>Pe(),editorTemplateId:f.templateIdentifier,editorLocale:f.locale}),onClose:()=>{De.current=null}});De.current=we},[x,Pe]),ve=u.useMemo(()=>{if(!f.isPaletteOpen)return null;if(!U)return{parentPath:[],index:w.length};if(S.selectedLockKind!=="none"){const de=K;return!de||de.length===0?{parentPath:[],index:w.length}:{parentPath:de.slice(0,-1),index:(de[de.length-1]??0)+1}}const X=S.selectedNode,R=Array.isArray(X?.children)?X.children.length:0;return lI(X?.name,R,U,o,w.length)},[f.isPaletteOpen,U,K,S.selectedLockKind,S.selectedNode,w.length,o]),te=u.useCallback(()=>{y({type:"SET_PALETTE_OPEN",open:!1})},[y]),he=u.useRef(null);u.useEffect(()=>{if(!f.isPaletteOpen){const ye=he.current;ye&&(x.close(ye),he.current=null);return}if(!ve||he.current)return;const W=ve,X=Me.current,R=hg(X.components,W.parentPath),de=x.open({ariaLabel:h("layout_editor.palette.title"),width:880,maxHeightRatio:.82,content:u.createElement(Ef,{manifest:X.manifest,nesting:X.nesting,componentPalette:X.componentPalette??null,targetParentPath:W.parentPath,targetContainerName:R,onInsert:(ye,we,Te)=>{Qe.current(ye,we,Te),te()},insertionIndex:W.index,onClose:()=>te(),editorTemplateId:f.templateIdentifier,editorLocale:f.locale}),onClose:()=>{he.current=null,y({type:"SET_PALETTE_OPEN",open:!1})}});he.current=de},[f.isPaletteOpen,ve,x,te,y]);const Q=u.useRef({layoutName:null,pushed:!1});u.useEffect(()=>{const W=v?.document?.layoutName??null;if(!W){Q.current={layoutName:null,pushed:!1};return}Q.current.layoutName!==W&&(b.clear(),b.push({actionKind:"inline_text_edit",label:"baseline",snapshot:w}),Q.current={layoutName:W,pushed:!0})},[v?.document?.layoutName,w,b]);const ie=u.useRef(0);u.useEffect(()=>{const W=v?.saveSuccessCounter??0;W!==ie.current&&(ie.current=W,W!==0&&(b.clear(),b.push({actionKind:"inline_text_edit",label:"save_baseline",snapshot:w}),k.current=[],c?.([])))},[v?.saveSuccessCounter,w,b,c]);const _e=u.useRef(0);u.useEffect(()=>{const W=v?.reloadCounter??0;W!==_e.current&&(_e.current=W,W!==0&&(Q.current={layoutName:null,pushed:!1},k.current=[],c?.([])))},[v?.reloadCounter,c]);const ge=u.useCallback((W,X,R)=>{if(!v)return;let de=[];v.patchLayout(ke=>{const xe={children:ke},Ee=Ha(xe,X),Fe=R<0?Array.isArray(Ee?.children)?Ee.children.length:0:R,gt=Qi(xe,X,Fe,W).children??[];return de=gt,gt}),b.push({actionKind:"insert",label:`insert ${W.name??W.type??""}`,snapshot:de});const ye=Ha({children:de},X),we=R<0?Array.isArray(ye?.children)?ye.children.length-1:0:R,Te=at([...X,we]);if(k.current=[...k.current,Te],c?.(k.current),t){const ke=`[data-editor-path="${an(Te)}"]`,xe=()=>{const Ee=t.querySelector(ke);Ee&&typeof Ee.scrollIntoView=="function"&&Ee.scrollIntoView({behavior:"smooth",block:"center",inline:"nearest"})};typeof window<"u"&&typeof window.requestAnimationFrame=="function"?window.requestAnimationFrame(()=>window.requestAnimationFrame(xe)):setTimeout(xe,32)}},[v,b,c,t]);Qe.current=ge;const Ae=u.useCallback(()=>{if(!v||!S.selectedNode||!U)return;const W=es(S.selectedNode);let X=[];v.patchLayout(R=>{const de={children:R},ye=U.slice(0,-1),we=U[U.length-1]??0,ke=Qi(de,ye,we+1,W).children??[];return X=ke,ke}),b.push({actionKind:"insert",label:"duplicate",snapshot:X})},[v,S.selectedNode,U,b]),Ne=u.useCallback(()=>{if(!v||!U)return;let W=[];v.patchLayout(X=>{const ye=Ri({children:X},U).children??[];return W=ye,ye}),b.push({actionKind:"remove",label:"remove",snapshot:W}),S.clearSelection()},[v,U,b,S]),Le=u.useMemo(()=>{if(!S.selectedNode||!U||U.some(et))return null;const W=S.selectedNode;if(!Array.isArray(W.children))return null;const X=yC(f.previewDevice,W,f.previewCustomWidth);return{mode:X.mode,key:X.key,sourceKey:X.sourceKey}},[S.selectedNode,U,f.previewDevice,f.previewCustomWidth]),q=u.useCallback(()=>{if(!v||!U||Le?.mode!=="separate")return;const W=Le.key,X=Le.sourceKey;let R=[];v.patchLayout(de=>(R=Ab({children:de},U,W,X).children??[],R)),b.push({actionKind:"insert",label:"separate_branch",snapshot:R})},[v,U,Le,b]),ae=u.useCallback(()=>{if(!v||!U||Le?.mode!=="merge")return;const W=Le.key;let X=[];v.patchLayout(R=>(X=Tb({children:R},U,W).children??[],X)),b.push({actionKind:"remove",label:"merge_branch",snapshot:X})},[v,U,Le,b]),pe=u.useCallback(W=>{switch(W){case"mobile":return g("layout_editor.context_menu.device_mobile");case"portable":return g("layout_editor.context_menu.device_portable");case"tablet":return g("layout_editor.context_menu.device_tablet");case"desktop":return g("layout_editor.context_menu.device_desktop");default:return g("layout_editor.context_menu.device_custom",{range:W})}},[g]),$e=u.useMemo(()=>{if(!Le)return null;const W=pe(Le.key);return{separate:g("layout_editor.context_menu.separate_branch_device",{device:W}),merge:g("layout_editor.context_menu.merge_branch_device",{device:W})}},[Le,g,pe]),be=u.useMemo(()=>{if(!S.selectedNode||!U)return[];if(U.some(et))return[];const W=Ur(f.previewDevice,S.selectedNode,f.previewCustomWidth);return G6(S.selectedNode,W).map(X=>({key:X,label:pe(X)}))},[S.selectedNode,U,f.previewDevice,f.previewCustomWidth,pe]),j=u.useCallback(W=>{if(W==="desktop"||W==="tablet"||W==="mobile"||W==="portable"){m.setDevice(W);return}m.setDevice(W)},[m]),oe=u.useCallback(()=>{S.selectedNode&&dg(S.selectedNode)},[S.selectedNode]),ue=u.useCallback(()=>{S.selectedNode&&(dg(S.selectedNode),Ne())},[S.selectedNode,Ne]),J=u.useCallback(()=>{if(!v)return;const W=VT();if(!W)return;const X=es(W);if(U&&U.length>0){const R=U.slice(0,-1),de=(U[U.length-1]??0)+1;ge(X,R,de)}else ge(X,[],w.length)},[v,U,ge,w.length]),le=u.useCallback((W,X)=>{b.push({actionKind:"move",label:X,snapshot:W})},[b]),Ie=u.useCallback(()=>{if(S.selectedNavAffordance==="route_in_tree"&&S.selectedNavTargetPath){if(d){d(S.selectedNavTargetPath);return}y({type:"SELECT_ROUTE",route:{path:S.selectedNavTargetPath,layoutName:null}})}},[S.selectedNavAffordance,S.selectedNavTargetPath,d,y]),dt=u.useCallback(()=>{const W=Eb(S.selectedBaseLayout,f.selectedRoute?.layoutName);W&&y({type:"ENTER_BASE_EDIT",layoutName:W})},[S.selectedBaseLayout,f.selectedRoute?.layoutName,y]),pt=u.useCallback(()=>{const W=S.selectedNode?.__source?.extensionId;W!=null&&y({type:"ENTER_EXTENSION_EDIT",extensionId:String(W)})},[S.selectedNode,y]),ot=u.useCallback(()=>{const W=S.selectedIterationSourcePath;W&&y({type:"ENTER_ITERATION_ITEM_EDIT",sourcePath:W})},[S.selectedIterationSourcePath,y]),Oe=u.useRef(null),He=u.useRef(null),Ye=u.useRef(null),Tt=u.useRef({colorScheme:"base",breakpoint:"base"}),Dt=Oe.current!==null?x.stack.some(W=>W.id===Oe.current):!1,Nt=u.useMemo(()=>zd(f.routeTree,h),[f.routeTree,h]),xn=u.useMemo(()=>Md(v?.document?.raw),[v?.document?.raw]),wn=u.useMemo(()=>Ld(s),[s?.states]),Zt=u.useMemo(()=>{const W=v?.document?.raw,X=Af(W?.components),R=W?.initIsolated,de=R&&typeof R=="object"&&!Array.isArray(R)?Object.keys(R):[];return eA(X,de)},[v?.document?.raw]),Hn=u.useMemo(()=>{const W=v?.document?.raw;return Af(W?.components).map(X=>X.scopeId).filter(X=>typeof X=="string"&&X.length>0)},[v?.document?.raw]),ln=Nd({raw:v?.document?.raw,spec:s}),Ii=u.useCallback((W,X)=>{if(!v)return;const R={children:Me.current.components},de=We(R,W);if(!de)return;Tt.current={colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:Ur(f.previewDevice,de,f.previewCustomWidth)};const ye=t?.getBoundingClientRect(),we=A&&ye?{left:ye.left+A.left,top:ye.top+A.top,width:A.width,height:A.height}:void 0,Te=re,ke=W.find(et),xe=ke?pe(ke.responsive):null,Ee=W.filter(kn=>typeof kn=="number"),Fe=cb({children:Me.current.components},Ee,ub(ln)),zt=Fe.length>0?[...ji({iterationVars:Fe}),...ln]:ln,gt=(kn,lM)=>u.createElement(ET,{key:`pem-${at(He.current??[])}`,node:kn,spec:s,manifest:Me.current.manifest,t:h,permissionCandidates:a,pageCandidates:Nt,dataSourceCandidates:xn,stateKeyCandidates:wn,bindingCandidates:zt,liveElement:Te,templateIdentifier:f.templateIdentifier,initialTab:lM,branchLabel:xe,isolatedScopeIdCandidates:Zt,usedScopeIds:Hn,onSaveInjectedProps:async(bt,Sn)=>{const Ai=typeof kn.id=="string"?kn.id:"",Fn=await HT(f.templateIdentifier,bt,Ai,Sn);if(st({op:"save",layoutName:`extension:${bt}`,editMode:f.editMode,saveTarget:"layout_extension",endpoint:`/api/admin/templates/${f.templateIdentifier}/layout-extensions/${bt}`,isDirty:!0,timestamp:Date.now()}),Fn.kind!=="success")throw new Error(Fn.kind==="conflict"?h("layout_editor.save.conflict"):Fn.kind==="injection_not_found"?"injection not found":Fn.kind==="not_found"?"extension not found":Fn.message??"save failed")},scope:Tt.current,onScopeChange:bt=>{Tt.current=bt;const Sn=Oe.current;Sn&&x.open({id:Sn,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(kn),onClose:()=>{Oe.current=null,He.current=null}})},onPatchNode:bt=>{let Sn=[];v.patchLayout(Fn=>(Sn=Ot({children:Fn},W,()=>bt).children??[],Sn)),b.push({actionKind:"inline_text_edit",label:"edit_props",snapshot:Sn}),sr({source:"style_control",componentPath:at(W),componentName:typeof bt.name=="string"?bt.name:null,patchKey:null,timestamp:Date.now()});const Ai=Oe.current;Ai&&x.open({id:Ai,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(bt),onClose:()=>{Oe.current=null,He.current=null}})},onDelete:()=>{const bt=Oe.current;bt&&x.close(bt),Ne()},onClose:()=>{const bt=Oe.current;bt&&x.close(bt)}});He.current=W;const Mt=x.open({ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(de,X),onClose:()=>{Oe.current=null,He.current=null,Ye.current=null}});Oe.current=Mt,Ye.current=kn=>{x.open({id:Mt,ariaLabel:h("layout_editor.property_modal.title"),width:420,draggable:!0,avoidRect:we,content:gt(kn),onClose:()=>{Oe.current=null,He.current=null,Ye.current=null}})}},[v,s,h,a,Nt,xn,wn,ln,re,b,x,Ne,t,A,f.previewColorScheme,f.previewDevice,f.previewCustomWidth,pe]);u.useEffect(()=>{const W=Ye.current,X=He.current;if(!W||!X)return;const R={children:Me.current.components},de=We(R,X);de?W(de):Oe.current&&x.close(Oe.current)},[w]);const Uz=()=>{if(!Dt)return null;const X={position:"absolute",background:"rgba(15, 23, 42, 0.45)",pointerEvents:"auto",zIndex:f7},R=ke=>{ke.preventDefault(),ke.stopPropagation()};if(!A)return i.jsx("div",{"data-testid":"g7le-edit-lock-full",onClick:R,onMouseMove:R,style:{...X,inset:0}});const{left:de,top:ye,width:we,height:Te}=A;return i.jsxs("div",{"data-testid":"g7le-edit-lock",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[i.jsx("div",{"data-testid":"g7le-edit-lock-top",onClick:R,onMouseMove:R,style:{...X,left:0,top:0,right:0,height:Math.max(0,ye)}}),i.jsx("div",{"data-testid":"g7le-edit-lock-bottom",onClick:R,onMouseMove:R,style:{...X,left:0,top:ye+Te,right:0,bottom:0}}),i.jsx("div",{"data-testid":"g7le-edit-lock-left",onClick:R,onMouseMove:R,style:{...X,left:0,top:ye,width:Math.max(0,de),height:Te}}),i.jsx("div",{"data-testid":"g7le-edit-lock-right",onClick:R,onMouseMove:R,style:{...X,left:de+we,top:ye,right:0,height:Te}})]})},ml=u.useCallback(()=>{if(U&&_p(S.selectedLockKind)){if(p&&S.selectedPath){p(S.selectedPath);return}Ii(U)}},[U,S.selectedPath,S.selectedLockKind,p,Ii]),{scale:vl}=m,_l=typeof S.selectedNode?.name=="string"?S.selectedNode.name:"",Bh=s?.componentCapabilities?.[_l]??null,Wh=Array.isArray(Bh?.styleControls)?Bh.styleControls:[],qz=Wh.includes("width")?Ct(s,"width"):null,Gz=Wh.includes("height")?Ct(s,"height"):null,Yz=u.useCallback((W,X)=>{if(!v||!U)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},U,()=>W).children??[],R)),sr({source:"resize",componentPath:at(U),componentName:_l||null,patchKey:X==="height"?"height":"width",resizeAxis:X,timestamp:Date.now()})},[v,U,_l]),Xz=u.useCallback((W,X)=>{if(!v||!U)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},U,()=>W).children??[],R)),b.push({actionKind:"inline_text_edit",label:`resize ${X}`,snapshot:R})},[v,U,b]),Jz=u.useCallback(()=>{if(!re)return null;const W=re.getBoundingClientRect(),X=vl||1;return{width:W.width/X,height:W.height/X}},[re,vl]),ir=GT({node:S.selectedNode,widthControl:qz,heightControl:Gz,scale:vl,measureStartSize:Jz,onResize:Yz,onResizeEnd:Xz}),Zz=u.useCallback((W,X)=>{ir.onHandlePointerDown(W,X)},[ir]),xl=u.useCallback(()=>{const W=b.undo();!W||!v||v.setLayoutComponents(W.snapshot)},[b,v]),wl=u.useCallback(()=>{const W=b.redo();!W||!v||v.setLayoutComponents(W.snapshot)},[b,v]);u7({onUndo:xl,onRedo:wl});const Qz=u.useMemo(()=>({copy:oe,cut:ue,paste:J,delete:Ne,openProps:ml,selectParent:Z?se:void 0,deselect:S.clearSelection}),[oe,ue,J,Ne,ml,Z,se,S.clearSelection]);np({handlers:Qz,hasSelection:!!S.selectedPath}),u.useEffect(()=>(window.__g7LayoutEditorHasSelection=!!S.selectedPath,()=>{try{delete window.__g7LayoutEditorHasSelection}catch{}}),[S.selectedPath]),u.useEffect(()=>(window.__g7LayoutEditorHistory={undo:xl,redo:wl,canUndo:b.canUndo,canRedo:b.canRedo},()=>{try{delete window.__g7LayoutEditorHistory}catch{}}),[xl,wl,b.canUndo,b.canRedo]);const Rz=u.useCallback(W=>{const X=P;F(null),X&&(async()=>{if((await I.commit(X.path,W)).kind==="created"&&v){const de=v.document?.raw?.components??[];b.push({actionKind:"inline_text_edit",label:"inline_create_key",snapshot:de})}})()},[P,I,v,b]),jh=u.useCallback(()=>{const W=P;F(null),I.trackCancel(W?.path??null)},[P,I]),eM=u.useCallback(()=>{F(null)},[]),tM=u.useCallback(async W=>{const X=P;if(!X)return null;const R=await I.keyifyChipValue(X.path,W);if(R.kind!=="created"||!R.translationKey)return null;const de=v?.document?.raw?.components??[];return b.push({actionKind:"inline_text_edit",label:"inline_keyify_chip",snapshot:de}),R.translationKey},[P,I,v,b]),nM=u.useCallback(()=>{const W=P;W&&(F(null),S.handleSelect("",{dataset:{editorPath:W.domPath}}),Ii(W.path,"translation"))},[P,S,Ii]),xt=u.useMemo(()=>{if(!P)return null;const W=v?.document?.raw?.components??w;return We({children:W},P.path)},[P,v?.document,w]),oM=u.useCallback(async(W,X)=>{const R=P;if(!v||!R)return null;const de=typeof xt?.text=="string"?xt.text:"";let ye;if(R.isParamKey)ye=await wr(f.templateIdentifier,f.locale,de,W,X.sourceId,X.path,"scalar");else{const Fe=zt=>zt.replace(/\$t:[a-zA-Z0-9._-]+/g,gt=>{const Mt=h(gt.slice(3));return Mt&&Mt!==gt.slice(3)&&!Mt.startsWith("$t:")?Mt:""}).replace(/\s+/g," ").trim();ye=await ls(f.templateIdentifier,f.selectedRoute?.layoutName??null,f.locale,de,W,X.sourceId,X.path,"scalar",Fe,h)}if(ye.kind==="error")return null;const we=R.path;let Te=[];v.patchLayout(Fe=>(Te=Ot({children:Fe},we,Mt=>({...Mt,text:ye.text})).children??[],Te)),b.push({actionKind:"inline_text_edit",label:"inline_insert_binding",snapshot:Te});const ke=ye.kind==="ok"&&"key"in ye?ye.key:void 0;!R.isParamKey&&ke&&F(Fe=>Fe&&Fe.path===we?{...Fe,isParamKey:!0,customKey:ke,chipValue:void 0,chipParamLabels:void 0}:Fe);try{window.dispatchEvent(new CustomEvent(kt,{detail:{templateIdentifier:f.templateIdentifier,locale:f.locale}}))}catch{}const xe=mt(ye.text),Ee=R.customKey??xe?.key??"";return Ee?It(Ee,f.locale)??null:null},[v,P,xt,f.templateIdentifier,f.locale,f.selectedRoute,b,h]),rM=u.useCallback(async W=>{const X=P;if(!v||!X)return null;const R=typeof xt?.text=="string"?xt.text:"",de=mt(R),ye=X.customKey??de?.key??null,we=mo(R,W),Te=X.path;let ke=[];return v.patchLayout(xe=>(ke=Ot({children:xe},Te,zt=>({...zt,text:we})).children??[],ke)),b.push({actionKind:"inline_text_edit",label:"inline_remove_binding",snapshot:ke}),ye&&await kr(f.templateIdentifier,ye,W,f.locale),ye?It(ye,f.locale)??null:null},[v,P,xt,f.templateIdentifier,f.locale,b]),[kl,Hh]=u.useState({}),iM=["fontSize","color","fontStyle","textDecoration","textAlign","letterSpacing","lineHeight","fontFamily"];u.useEffect(()=>{if(!P||!t||typeof MutationObserver>"u"){Hh({});return}const W=`[data-editor-path="${an(P.domPath)}"]`,X=()=>{const ye=t.querySelector(W);if(!ye)return;const we={};for(const ke of iM){const xe=ye.style.getPropertyValue(ke.replace(/([A-Z])/g,"-$1").toLowerCase());xe&&(we[ke]=xe)}const Te=yg(t,P.domPath)??void 0;Hh({className:ye.className,style:we,effectiveColor:Te})};X();const R=t.querySelector(W);if(!R)return;const de=new MutationObserver(X);return de.observe(R,{attributes:!0,attributeFilter:["class","style"]}),de.observe(t,{childList:!0,subtree:!0}),()=>de.disconnect()},[P?.path?at(P.path):null,t]);const Fh=u.useCallback((W,X)=>{if(!v||!P)return;const R=P.path;let de=[],ye=null;v.patchLayout(we=>(de=Ot({children:we},R,xe=>{const Ee=X(xe);return ye=typeof Ee.name=="string"?Ee.name:null,Ee}).children??[],de)),b.push({actionKind:"inline_text_edit",label:`inline_format ${W}`,snapshot:de}),sr({source:"style_control",componentPath:at(R),componentName:ye,patchKey:W,timestamp:Date.now()})},[v,P,b]),ft=u.useMemo(()=>{const W=S.selectedPath;if(!W||!_)return null;const X=[];let R=W;for(;R;){X.push(R);const de=R.match(/^(.*)\.children\.\d+$/);R=de?de[1]:null}for(const de of X){const ye=Ze(de);if(!ye)continue;const we=We(_,ye),Te=typeof we?.name=="string"?we.name:null;if(!Te)continue;const ke=Oi(s,Te),xe=ke?.canvasOverlay?.kind;if(typeof xe!="string"||!xe||!we)continue;const Ee=ke.canvasOverlay.params&&typeof ke.canvasOverlay.params=="object"?ke.canvasOverlay.params:void 0,Fe=de===W?null:W.slice(de.length+1);return{kind:xe,params:Ee,tablePath:de,tableNode:we,selectedCellRel:Fe}}return null},[S.selectedPath,_,s]),ho=ft?FT(ft.kind):null,Kh=u.useMemo(()=>!ho||!t||!ft?null:po(t,ft.tablePath),[ho,t,ft,A,w]),sM=u.useMemo(()=>{if(!ho||!t||!ft)return[];const W=ft.tablePath,X=an(W),R=[];return t.querySelectorAll(`[data-editor-path^="${X}.children."]`).forEach(we=>{const Te=we.dataset.editorPath??"";if(!Te.startsWith(W+"."))return;const ke=Te.slice(W.length+1),xe=so(we,t);xe&&R.push({path:ke,top:xe.top,left:xe.left,width:xe.width,height:xe.height})}),t.querySelectorAll(`[data-editor-item-path^="${X}."]`).forEach(we=>{const Te=we.dataset.editorItemPath??"";if(!Te.startsWith(W+"."))return;const ke=Te.slice(W.length+1),xe=so(we,t);xe&&R.push({path:ke,top:xe.top,left:xe.left,width:xe.width,height:xe.height})}),R},[ho,t,ft,A,w]),aM=u.useCallback(W=>{const X=ft?Ze(ft.tablePath):null;if(!v||!X)return;let R=[];v.patchLayout(de=>(R=Ot({children:de},X,()=>W).children??[],R)),b.push({actionKind:"inline_text_edit",label:"inplace_edit",snapshot:R}),sr({source:"style_control",componentPath:ft?.tablePath??"",componentName:typeof W.name=="string"?W.name:null,patchKey:"canvas_inplace",timestamp:Date.now()})},[v,U,b]);return t?i.jsxs("div",{className:"g7le-editor-canvas-overlay","data-testid":"g7le-editor-canvas-overlay",style:{position:"absolute",inset:0,pointerEvents:"none"},children:[!D&&Uz(),D&&i.jsxs("div",{"data-testid":"g7le-canvas-pick-banner",style:{position:"absolute",top:8,left:"50%",transform:"translateX(-50%)",zIndex:9999,display:"flex",alignItems:"center",gap:12,padding:"8px 14px",background:"#1e293b",color:"#fff",borderRadius:8,fontSize:13,boxShadow:"0 4px 12px rgba(0,0,0,0.25)",pointerEvents:"auto"},children:[i.jsxs("span",{children:["🎯 ",h("layout_editor.target_picker.canvas_pick_hint")]}),i.jsx("button",{type:"button","data-testid":"g7le-canvas-pick-cancel",onClick:()=>Y(D,null),style:{border:"1px solid #475569",borderRadius:6,background:"transparent",color:"#cbd5e1",cursor:"pointer",fontSize:12,padding:"3px 10px"},children:h("layout_editor.target_picker.cancel")})]}),f.editMode!=="route"&&i.jsx(EI,{frameEl:t,components:w,editMode:f.editMode,currentExtensionId:C,editableRootPath:E}),P&&((P.isParamKey&&P.customKey||P.chipValue)&&xt?i.jsxs(i.Fragment,{children:[xt&&i.jsx(fg,{node:xt,spec:s,t:h,onApplyControl:Fh,box:P.box,scope:{colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:Ur(f.previewDevice,xt,f.previewCustomWidth)}}),i.jsx(i4,{box:P.box,node:xt,customKey:P.customKey??null,initialChipValue:P.chipValue,chipParamLabels:P.chipParamLabels,onKeyify:tM,templateIdentifier:f.templateIdentifier,locale:f.locale,t:h,candidates:ln,onInsertBinding:oM,onRemoveBinding:rM,onCommit:eM,onCancel:jh})]}):i.jsxs(i.Fragment,{children:[xt&&i.jsx(fg,{node:xt,spec:s,t:h,onApplyControl:Fh,box:P.box,scope:{colorScheme:f.previewColorScheme==="dark"?"dark":"base",breakpoint:xt?Ur(f.previewDevice,xt,f.previewCustomWidth):"base"}}),i.jsx(r4,{box:P.box,initialValue:P.initialValue,isCustomKey:P.isCustomKey,t:h,onCommit:Rz,onCancel:jh,mirrorClassName:kl.className,mirrorStyle:kl.style,nodeEffectiveColor:kl.effectiveColor??P.nodeEffectiveColor,onOpenTranslations:nM})]})),!D&&i.jsx(v7,{hoverBox:T,selectedBox:A,lockKind:S.selectedLockKind,navAffordance:S.selectedNavAffordance,onEditProps:ml,onDuplicate:Ae,onDelete:Ne,onSeparateBranch:Le?.mode==="separate"?q:void 0,separateBranchLabel:$e?.separate,onMergeBranch:Le?.mode==="merge"?ae:void 0,mergeBranchLabel:$e?.merge,definedDeviceBranches:be,onJumpToDevice:be.length>0?j:void 0,onLinkEditDestination:Ie,onEditBase:dt,onEditExtension:pt,extensionLabel:ze,baseLayoutLabel:S.selectedBaseLayout,dataSourceLabel:S.selectedDataSourceId,branchLabel:ee,onEditIteration:S.selectedIsIteration&&f.editMode!=="iteration_item"?ot:void 0,resizeEnabledAxes:ir.enabledAxes,onResizeHandlePointerDown:ir.enabledAxes.width||ir.enabledAxes.height?Zz:void 0,selectedName:Ce,onSelectParent:Z?se:void 0}),D&&B&&i.jsx("div",{"data-testid":"g7le-canvas-pick-hover","data-pick-has-id":B.hasId?"true":"false",style:{position:"absolute",left:B.box.left,top:B.box.top,width:B.box.width,height:B.box.height,pointerEvents:"none",zIndex:9998,boxSizing:"border-box",border:B.hasId?"2px solid #2563eb":"2px dashed #94a3b8",background:B.hasId?"rgba(37,99,235,0.10)":"repeating-linear-gradient(45deg, rgba(148,163,184,0.18) 0, rgba(148,163,184,0.18) 6px, transparent 6px, transparent 12px)",cursor:B.hasId?"pointer":"not-allowed"},children:!B.hasId&&i.jsx("span",{"data-testid":"g7le-canvas-pick-hover-hint",style:{position:"absolute",top:0,left:0,transform:"translateY(-100%)",background:"#475569",color:"#fff",fontSize:11,lineHeight:1.4,padding:"2px 6px",borderRadius:4,whiteSpace:"nowrap"},children:h("layout_editor.target_picker.needs_id")})}),!D&&i.jsx(C7,{selectedBox:qe,points:Se.points,onAddRequest:Re}),ho&&ft&&Kh&&S.selectedLockKind==="none"&&i.jsx(ho,{node:ft.tableNode,params:ft.params,nodeBox:Kh,cellBoxes:sM,selectedCellPath:ft.selectedCellRel??void 0,colorScheme:f.previewColorScheme==="dark"?"dark":"light",t:h,onPatchNode:aM,onInsertChild:ge,onRemoveChild:W=>{const X=Ze(W);if(!v||!X)return;let R=[];v.patchLayout(de=>(R=Ri({children:de},X).children??[],R)),b.push({actionKind:"remove",label:"inplace_remove",snapshot:R})},onRequestInlineEdit:W=>{ft&&G(`${ft.tablePath}.${W}`)}}),v&&i.jsx(xI,{frameEl:t,nesting:o,editMode:f.editMode,currentExtensionId:C,editableRootPath:E,components:w,patchLayout:v.patchLayout,pushHistory:le,selectedPath:S.selectedPath,onSelectPath:W=>S.handleSelect("",{dataset:{editorPath:W}}),onMovePath:W=>S.handleSelect("",{dataset:{editorPath:W}}),onRequestInlineEdit:G})]}):null}function hg(e,t){if(t.length===0)return null;const o=Ha({children:e},t);return o&&typeof o.name=="string"?o.name:null}function Ha(e,t){return We(e,t)}function an(e){return typeof CSS<"u"&&typeof CSS.escape=="function"?CSS.escape(e):e.replace(/[^a-zA-Z0-9_\-.]/g,t=>`\\${t}`)}function yg(e,t){if(!t)return null;const n=e.querySelector(`[data-editor-path="${an(t)}"]`);if(!n||typeof window.getComputedStyle!="function")return null;let o=n;const r=n.ownerDocument.createTreeWalker(n,NodeFilter.SHOW_TEXT);let s=r.nextNode();for(;s;){if((s.textContent??"").trim().length>0){const a=s.parentElement;if(a&&n.contains(a)){o=a;break}}s=r.nextNode()}try{return window.getComputedStyle(o).color||null}catch{return null}}function po(e,t){if(!t)return null;const n=e.querySelector(`[data-editor-path="${an(t)}"]`);if(n){const c=so(n,e);return c&&ao(c,e)?c:null}const o=e.querySelectorAll(`[data-editor-path^="${an(t)}.iteration."]`);let r=1/0,s=1/0,a=-1/0,l=-1/0,d=!1;return o.forEach(c=>{const p=c.dataset.editorPath??"";if(!new RegExp("\\.iteration\\.\\d+$").test(p))return;const f=so(c,e);f&&ao(f,e)&&(d=!0,r=Math.min(r,f.left),s=Math.min(s,f.top),a=Math.max(a,f.left+f.width),l=Math.max(l,f.top+f.height))}),d?{left:r,top:s,width:a-r,height:l-s,scale:1}:null}function f4({isEditMode:e,editorSpec:t,sampleOverride:n}){const o=t?.sampleData;return u.useMemo(()=>Pm({spec:o,overrideSpec:n,onMatch:r=>{Uh(r)}}),[e,o,n])}const g4=Lt().ComponentRegistry;function h4(e){return e.split(/[-_]/).map(t=>t.charAt(0).toUpperCase()+t.slice(1).toLowerCase()).join("")}async function y4(e){const t=typeof window<"u"?window.localStorage?.getItem("auth_token"):null,n={Accept:"application/json"};t&&(n.Authorization=`Bearer ${t}`);const o=await fetch(`/api/admin/templates/${encodeURIComponent(e)}/editor-assets`,{headers:n,credentials:"same-origin"});if(!o.ok){const s=await o.json().catch(()=>null);throw Gr(o.status,s,"assets")}const r=await o.json();return r.data||r}async function b4(e){try{if(typeof fetch!="function")return 0;const t=await fetch(en(`/api/templates/${encodeURIComponent(e)}/config`,"json"),{credentials:"same-origin",headers:{Accept:"application/json"}});if(!t.ok)return 0;const n=await t.json().catch(()=>null),o=n?.data?.cache_version??n?.cache_version;return typeof o=="number"&&o>0?o:0}catch{return 0}}function m4(e){return new Promise((t,n)=>{if(typeof document>"u"){n(new Error("document undefined"));return}if(document.querySelector(`script[data-g7le-asset="${e}"]`)){t();return}const r=document.createElement("script");r.src=e,r.async=!1,r.setAttribute("data-g7le-asset",e),r.addEventListener("load",()=>t()),r.addEventListener("error",()=>n(new Error(`script load failed: ${e}`))),document.head.appendChild(r)})}async function v4(e){if(typeof document>"u")return;const t=`[data-g7le-asset="${e}"]`;if(document.querySelector(`link${t}, style${t}`))return;if(e.includes("/api/admin/"))try{const r=typeof window<"u"?window.localStorage?.getItem("auth_token"):null,s={Accept:"text/css"};r&&(s.Authorization=`Bearer ${r}`);const a=await fetch(e,{headers:s,credentials:"same-origin"});if(a.ok){const l=await a.text(),d=document.createElement("style");d.textContent=l,d.setAttribute("data-g7le-asset",e),document.head.appendChild(d);return}}catch{}const o=document.createElement("link");o.rel="stylesheet",o.href=e,o.setAttribute("data-g7le-asset",e),document.head.appendChild(o)}function _4(e,t,n="user"){const[o,r]=u.useState({componentRegistry:null,translationEngine:null,isReady:!1,error:null});return u.useEffect(()=>{let s=!1;return(async()=>{r(a=>({...a,isReady:!1,error:null}));try{const a=await y4(e);if(s)return;for(const y of a.css??[])if(await v4(y),s)return;const l=h4(e);if(!(typeof window<"u"&&typeof window[l]=="object")){for(const y of a.js??[])if(await m4(y),s)return}const c=g4.createIsolatedInstance();if(await c.loadComponents(e,n),s)return;const p=await b4(e);if(s)return;const f=new Pt({defaultLocale:t});if(p>0&&f.setCacheVersion(p),await f.loadTranslations(e,t),s)return;cd(f,e);try{const y=Pt.getInstance();p>y.getCacheVersion()&&y.setCacheVersion(p),await y.loadTranslations(e,t),cd(y,e)}catch{}if(s)return;r({componentRegistry:c,translationEngine:f,isReady:!0,error:null})}catch(a){if(s)return;const l=hp(a)?a:io(a,"assets");r({componentRegistry:null,translationEngine:null,isReady:!1,error:l})}})(),()=>{s=!0}},[e,t,n]),o}const bg=Lt().AuthManager;function x4(e){switch(e){case"unauthorized":return{icon:"🔒",tone:"blue",titleKey:"layout_editor.access_error.unauthorized.title",messageKey:"layout_editor.access_error.unauthorized.message",showRequiredPermissions:!0};case"forbidden":return{icon:"⛔",tone:"rose",titleKey:"layout_editor.access_error.forbidden.title",messageKey:"layout_editor.access_error.forbidden.message",showRequiredPermissions:!0};case"not_found":return{icon:"🗂️",tone:"slate",titleKey:"layout_editor.access_error.not_found.title",messageKey:"layout_editor.access_error.not_found.message",showRequiredPermissions:!1};case"server_error":return{icon:"🛠️",tone:"amber",titleKey:"layout_editor.access_error.server_error.title",messageKey:"layout_editor.access_error.server_error.message",showRequiredPermissions:!1};case"network":return{icon:"📡",tone:"amber",titleKey:"layout_editor.access_error.network.title",messageKey:"layout_editor.access_error.network.message",showRequiredPermissions:!1};default:return{icon:"⚠️",tone:"slate",titleKey:"layout_editor.access_error.unknown.title",messageKey:"layout_editor.access_error.unknown.message",showRequiredPermissions:!1}}}const w4={blue:{bg:"#eff6ff",border:"#bfdbfe",iconBg:"#dbeafe"},rose:{bg:"#fff1f2",border:"#fecdd3",iconBg:"#ffe4e6"},amber:{bg:"#fffbeb",border:"#fde68a",iconBg:"#fef3c7"},slate:{bg:"#f8fafc",border:"#e2e8f0",iconBg:"#f1f5f9"}};function k4({error:e}){return u.useEffect(()=>{if(e.kind!=="unauthorized"||typeof window>"u")return;const t=bg.getInstance(),n=`${window.location.pathname}${window.location.search}`,o=t.getLoginRedirectUrl("admin",n,"session_expired"),r=window.setTimeout(()=>{window.location.href=o},600);return()=>window.clearTimeout(r)},[e.kind]),null}function Fa({error:e}){const{t}=tt(),n=x4(e.kind),o=w4[n.tone],r=()=>{typeof window<"u"&&(window.location.href="/admin")},s=()=>{typeof window<"u"&&window.location.reload()},a=()=>{if(typeof window>"u")return;const l=bg.getInstance(),d=`${window.location.pathname}${window.location.search}`;window.location.href=l.getLoginRedirectUrl("admin",d)};return i.jsxs("div",{className:"g7le-access-error","data-testid":"g7le-access-error","data-error-kind":e.kind,style:{flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:24,background:"#f1f5f9"},children:[i.jsx(k4,{error:e}),i.jsxs("div",{style:{maxWidth:480,width:"100%",background:o.bg,border:`1px solid ${o.border}`,borderRadius:12,padding:"28px 32px",boxShadow:"0 2px 8px rgba(15, 23, 42, 0.05)",textAlign:"center"},children:[i.jsx("div",{style:{width:64,height:64,margin:"0 auto 16px",borderRadius:"50%",background:o.iconBg,display:"flex",alignItems:"center",justifyContent:"center",fontSize:32,lineHeight:1},"aria-hidden":"true",children:n.icon}),i.jsx("h2",{"data-testid":"g7le-access-error-title",style:{margin:"0 0 8px",fontSize:18,fontWeight:700,color:"#0f172a"},children:t(n.titleKey)}),i.jsx("p",{"data-testid":"g7le-access-error-message",style:{margin:"0 0 16px",fontSize:13,lineHeight:1.6,color:"#475569"},children:t(n.messageKey)}),n.showRequiredPermissions&&e.requiredPermissions&&i.jsxs("div",{"data-testid":"g7le-access-error-permissions",style:{display:"inline-flex",flexWrap:"wrap",gap:6,justifyContent:"center",padding:"8px 12px",marginBottom:16,background:"rgba(15, 23, 42, 0.04)",borderRadius:8,fontSize:12,color:"#475569"},children:[i.jsx("span",{style:{fontWeight:600},children:t("layout_editor.access_error.required_permission_label")}),e.requiredPermissions.split(/[,\s]+/).filter(Boolean).map(l=>i.jsx("code",{style:{padding:"2px 6px",background:"#fff",border:"1px solid #e2e8f0",borderRadius:4,fontFamily:"ui-monospace, SFMono-Regular, Menlo, monospace",fontSize:11,color:"#1e293b"},children:l},l))]}),e.message&&i.jsxs("details",{"data-testid":"g7le-access-error-detail",style:{marginBottom:16,fontSize:11,color:"#94a3b8"},children:[i.jsx("summary",{style:{cursor:"pointer"},children:t("layout_editor.access_error.detail_summary")}),i.jsxs("pre",{style:{marginTop:8,padding:8,background:"#fff",border:"1px solid #e2e8f0",borderRadius:6,overflowX:"auto",whiteSpace:"pre-wrap",wordBreak:"break-word",textAlign:"left",fontSize:11,color:"#475569"},children:[e.status>0?`HTTP ${e.status} +`:"",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!="object")return e;o=o[n[a]]}if(!Array.isArray(o)||o.length<=1)return e;const r={...e};let s=r;for(let a=0;a<n.length-1;a++){const l=n[a],d=s[l];if(d==null||typeof d!="object")return e;s[l]=Array.isArray(d)?[...d]:{...d},s=s[l]}return s[n[n.length-1]]=o.slice(0,1),r}const M4="SlotContainer";function _g(e,t=new Set){for(const n of e){if(!n||typeof n!="object")continue;const o=n;if(o.name===M4){const r=o.props?.slotId;typeof r=="string"&&t.add(r)}Array.isArray(o.children)&&_g(o.children,t)}return t}function L4(e){const t=_g(e),n=o=>o.map(r=>{if(!r||typeof r!="object")return r;const s=r;let a=s;if(typeof s.slot=="string"&&!t.has(s.slot)){const{slot:l,...d}=s;a={...d,__editorSlotName:l}}if(Array.isArray(a.children)){const l=n(a.children);l!==a.children&&(a={...a,children:l})}return a});return n(e)}function St(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Xt(e){if(Array.isArray(e))return e.map(t=>Xt(t));if(St(e)){const t={};for(const[n,o]of Object.entries(e))t[n]=Xt(o);return t}return e}function mi(e,t){for(const[n,o]of Object.entries(t)){const r=e[n];St(o)&&St(r)?mi(r,o):e[n]=Xt(o)}}function P4(e){const t=[];let n="";for(let o=0;o<e.length;o+=1){const r=e[o];if(r==="["){n.length>0&&(t.push(n),n="");const s=e[o+1];if(s==="'"||s==='"'){const l=e.indexOf(`${s}]`,o+2);if(l!==-1){t.push(e.slice(o+2,l)),o=l+1;continue}}const a=e.indexOf("]",o+1);if(a!==-1){t.push(e.slice(o+1,a)),o=a;continue}n+=r}else r==="."?n.length>0&&(t.push(n),n=""):n+=r}return n.length>0&&t.push(n),t.filter(o=>o.length>0)}function Ua(e,t,n){const o=P4(t);if(o.length===0)return;let r=e;for(let s=0;s<o.length-1;s+=1){const a=o[s],l=r[a];if(St(l)){const d=Xt(l);r[a]=d,r=d}else{const d={};r[a]=d,r=d}}r[o[o.length-1]]=Xt(n)}function O4(e){const t=Xt(e.globalBaseline??{}),n=Xt(e.localBaseline??{}),o=Xt(e.queryBaseline??{}),r=Xt(e.routeBaseline??{});if(e.patch&&(St(e.patch.global)&&mi(t,e.patch.global),St(e.patch.local)&&mi(n,e.patch.local),St(e.patch.query)&&mi(o,e.patch.query),St(e.patch.route)))for(const[s,a]of Object.entries(e.patch.route))a===null?delete r[s]:r[s]=Xt(a);if(St(e.formErrors))for(const[s,a]of Object.entries(e.formErrors))s.startsWith("_global.")?Ua(t,s.slice(8),a):s.startsWith("_local.")?Ua(n,s.slice(7),a):Ua(n,s,a);return{global:t,local:n,query:o,route:r}}function xg(e){if(!e||!St(e.sampleDataOverrides))return;const t=e.sampleDataOverrides;return St(t.byDataSourceId)||St(t.byEndpointPattern)||St(t.bySource)?t:void 0}function qa(e){if(!e||!St(e.formErrors))return;const t=e.formErrors;return Object.keys(t).length>0?t:void 0}const Ga="g7le:editor-t-ready",wg="data-g7le-vh-expanded";function B4(e,t){if(!e||!t)return;const n=t;e.querySelectorAll("*").forEach(r=>{const s=window.getComputedStyle(r),a=parseFloat(s.height),l=Number.isFinite(a)&&Math.abs(a-n)<=2,d=parseFloat(s.minHeight),c=Number.isFinite(d)&&Math.abs(d-n)<=2,p=s.overflowY==="auto"||s.overflowY==="scroll"||s.overflowY==="hidden"||s.overflow==="auto"||s.overflow==="scroll"||s.overflow==="hidden";l&&p?(r.style.setProperty("height","auto","important"),c&&r.style.setProperty("min-height","0","important"),r.style.setProperty("overflow","visible","important"),r.setAttribute(wg,"1")):(s.overflowY==="auto"||s.overflowY==="scroll")&&r.scrollHeight>r.clientHeight+4&&r.clientHeight>120&&(r.style.setProperty("overflow","visible","important"),r.style.setProperty("height","auto","important"),r.setAttribute(wg,"1"))})}const W4=[];function j4(e={}){const{manifest:t=null,nesting:n=null,componentPalette:o=null,spec:r=null,permissionCandidates:s,sampleGlobalSources:a=W4,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c}=e,{state:p}=Je(),f=Ft(),y=Cp(),{document:g,isLoading:m,error:h}=f??y,v=u.useRef(null),[b,x]=u.useState(null),{deviceWidth:k,scale:w,colorScheme:_}=ja(),C=u.useMemo(()=>p.activeStateId?p.availableStates.find(Q=>Q.id===p.activeStateId)??null:null,[p.activeStateId,p.availableStates]),E=u.useMemo(()=>xg(C),[C]),S=f4({isEditMode:!0,editorSpec:r??void 0,sampleOverride:E}),{t:T}=tt(),{componentRegistry:$,translationEngine:A,isReady:z,error:D}=_4(p.templateIdentifier,p.locale),O=u.useRef(null);O.current===null&&(O.current=new vd);const M=u.useRef(null);if(M.current===null){M.current=new VE({navigate:()=>{}}),M.current.setPreviewMode(!0);try{const ie=window.__templateApp?.getActionDispatcher?.();if(ie&&typeof ie.getRegisteredHandlers=="function"){const _e=ie.getRegisteredHandlers();for(const ge of _e){if(!ge.includes("."))continue;const Ae=typeof ie.getHandler=="function"?ie.getHandler(ge):void 0;typeof Ae=="function"&&M.current.registerHandler(ge,Ae,{category:"plugin",source:"preview-canvas-cloned-from-host"})}}}catch{}}u.useEffect(()=>{if(!z)return;const Q=M.current;if(Q)try{const ie=window.G7Core?.getActionDispatcher?.();if(!ie||typeof ie.getRegisteredHandlers!="function")return;const _e=ie.getRegisteredHandlers();for(const ge of _e){if(!ge.includes(".")||typeof Q.getHandler=="function"&&Q.getHandler(ge))continue;const Ae=typeof ie.getHandler=="function"?ie.getHandler(ge):void 0;typeof Ae=="function"&&Q.registerHandler(ge,Ae,{category:"plugin",source:"preview-canvas-iife-reclone"})}}catch{}},[z]);const B=u.useMemo(()=>new UE({sampleProvider:S,actionDispatcher:M.current}),[S]),N=u.useMemo(()=>g?.raw?.meta?.guest_only===!0,[g]),H=u.useMemo(()=>Va({sources:a,isGuestOnly:N}),[a,N]),L=u.useMemo(()=>{const Q=H4(qa(C),A,{templateId:p.templateIdentifier,locale:p.locale}),ie=H._local,{global:_e,local:ge}=O4({globalBaseline:H,localBaseline:ie??{},patch:C?.initialState??null,formErrors:Q});return{..._e,_local:ge}},[H,C,A,p.templateIdentifier,p.locale]),I=u.useMemo(()=>!C||!(!!C.initialState?.local||!!qa(C))?null:{...L._local??{},_forceLocalInit:C.id,_merge:"deep"},[C,L]),P=Q=>typeof Q=="object"&&Q!==null&&!Array.isArray(Q),F=u.useMemo(()=>P(C?.initialState?.query)?C.initialState.query:null,[C]),G=u.useMemo(()=>P(C?.initialState?.route)?C.initialState.route:null,[C]),[V,Y]=u.useState(()=>Va({sources:[],isGuestOnly:!1})),Z=u.useRef(null);u.useEffect(()=>{const Q={...L},ie=C4({actionDispatcher:M.current,locale:p.locale,initialGlobalState:Q,onChange:ge=>{Y(ge.globalState)}});Z.current=ie;const _e=window.__templateApp;return _e?.setGlobalState&&M.current.setGlobalStateUpdater((ge,Ae)=>{_e.setGlobalState(ge,Ae)}),Y(Q),$l({kind:"applyPatch",activeStateId:p.activeStateId,routePath:p.selectedRoute?.path??null,hasInitialPatch:!!C?.initialState,hasFormErrors:!!qa(C),hasSampleOverride:!!xg(C)}),()=>{ie.restore(),Z.current=null}},[p.locale,L]),u.useEffect(()=>{if(!A)return;const Q=window.G7Core;if(!Q||typeof Q.t!="function")return;const ie=Q.t,_e=p.templateIdentifier,ge=p.locale;return Q.t=(Ae,Ne)=>{try{const Le=Ne?"|"+Object.entries(Ne).map(([ae,pe])=>`${ae}=${pe}`).join("|"):void 0,q=A.translate(Ae,{templateId:_e,locale:ge},Le);if(q!==Ae&&!q.startsWith("$t:"))return q}catch{}return ie(Ae,Ne)},typeof window<"u"&&window.dispatchEvent(new CustomEvent(Ga)),()=>{window.G7Core?.t&&window.G7Core.t!==ie&&(window.G7Core.t=ie)}},[A,p.templateIdentifier,p.locale]);const se=u.useMemo(()=>{const Q=g?.raw?.components??[];return p.editMode!=="base"?Q:L4(Q)},[g,p.editMode]),re=g?.raw?.__editability,U=g?.raw?.__extensionId,ee=u.useMemo(()=>p.editMode!=="extension"||re!=="ok"?[]:typeof U!="number"?[]:F4(se,U),[p.editMode,re,U,se]),[fe,K]=u.useState(null),[ne,me]=u.useState(!1),Ce=g?.layoutName??null,ze=u.useMemo(()=>{const Q=g?.raw?.scripts??[];return Array.isArray(Q)?Q.map(ie=>ie&&typeof ie.src=="string"?ie.src:"").join("|"):""},[g]);u.useEffect(()=>{me(!1)},[Ce,ze]),u.useEffect(()=>{let Q=!1;const ie=g?.raw?.scripts??[];if(!Array.isArray(ie)||ie.length===0){me(!0);return}const _e=[];for(const ge of ie){if(!ge?.src||typeof ge.src!="string")continue;const Ae=ge.id?`script#${CSS.escape(ge.id)}, script[data-g7le-canvas-script="${ge.src}"]`:`script[data-g7le-canvas-script="${ge.src}"], script[src="${ge.src}"]`,Ne=window.document.querySelector(Ae);if(Ne){const Le=Ne.hasAttribute("data-g7le-canvas-script");if(Ne.hasAttribute("data-g7le-script-loaded")||!Le)continue;_e.push(new Promise(ae=>{if(Ne.hasAttribute("data-g7le-script-loaded")){ae();return}Ne.addEventListener("load",()=>ae()),Ne.addEventListener("error",()=>ae())}));continue}_e.push(new Promise(Le=>{const q=window.document.createElement("script");q.src=ge.src,q.async=!1,ge.id&&(q.id=ge.id),q.setAttribute("data-g7le-canvas-script",ge.src);const ae=()=>{q.setAttribute("data-g7le-script-loaded","1"),Le()};q.addEventListener("load",ae),q.addEventListener("error",ae),window.document.head.appendChild(q)}))}if(_e.length===0){me(!0);return}return Promise.all(_e).then(()=>{Q||requestAnimationFrame(()=>{requestAnimationFrame(()=>{Q||me(!0)})})}),()=>{Q=!0}},[g]);const Se=u.useMemo(()=>g?.raw?.data_sources??[],[g]),qe=u.useMemo(()=>g?.raw?.computed??{},[g]),[De,Pe]=u.useState({}),Qe=u.useMemo(()=>{if(p.editMode!=="iteration_item")return De;const Q=g?.iterationContext;if(!Q)return De;const ie=g?.raw?.components??[],_e=We({children:ie},Q.sourceIndexPath),ge=_e&&_e.iteration&&typeof _e.iteration.source=="string"?_e.iteration.source:null;return ge?z4(De,ge):De},[p.editMode,De,g]),[Me,Re]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const Q=()=>Re(ie=>ie+1);return window.addEventListener(kt,Q),()=>window.removeEventListener(kt,Q)},[]);const ve=u.useMemo(()=>E4(p.selectedRoute?.path??null),[p.selectedRoute?.path]);if(u.useEffect(()=>{let Q=!1;return(async()=>{if(Se.length===0){Q||Pe({});return}try{const ie=await B.fetchDataSources(Se,ve,new URLSearchParams,void 0,void 0);Q||Pe(ie)}catch{Q||Pe({})}})(),()=>{Q=!0}},[Se,B,ve]),u.useEffect(()=>{const Q=b;if(!Q||typeof window>"u")return;let ie=0,_e=0,ge=0;const Ae=()=>B4(Q,window.innerHeight);ie=window.requestAnimationFrame(()=>{_e=window.requestAnimationFrame(Ae)});let Ne=null;return typeof MutationObserver<"u"&&(Ne=new MutationObserver(()=>{ge||(ge=window.requestAnimationFrame(()=>{ge=0,Ae()}))}),Ne.observe(Q,{childList:!0,subtree:!0})),()=>{window.cancelAnimationFrame(ie),window.cancelAnimationFrame(_e),ge&&window.cancelAnimationFrame(ge),Ne&&Ne.disconnect()}},[b,se,ne,k,De]),u.useEffect(()=>{const Q=b;if(!Q||typeof window>"u"||p.editMode!=="extension"||re!=="ok"||ee.length===0){K(null);return}let ie=0,_e=0;const ge=ae=>{if(!ae)return!1;const pe=ae.getBoundingClientRect();return pe.width>0&&pe.height>0},Ae=ae=>{const pe=Q.querySelector(`[data-editor-path="${ae}"]`);if(ge(pe))return!0;const $e=Q.querySelectorAll(`[data-editor-path^="${ae}.children."]`);for(const be of Array.from($e))if(ge(be))return!0;return!1},Ne=()=>{K(ee.some(Ae))};ie=window.requestAnimationFrame(()=>{_e=window.requestAnimationFrame(Ne)});let Le=null,q=0;return typeof MutationObserver<"u"&&(Le=new MutationObserver(()=>{q||(q=window.requestAnimationFrame(()=>{q=0,Ne()}))}),Le.observe(Q,{childList:!0,subtree:!0})),()=>{window.cancelAnimationFrame(ie),window.cancelAnimationFrame(_e),q&&window.cancelAnimationFrame(q),Le&&Le.disconnect()}},[b,p.editMode,re,ee,ne,De]),u.useEffect(()=>{if(typeof window>"u"||p.editMode!=="base")return;const Q=window.document,ie="g7le-base-slot-style";if(Q.getElementById(ie))return;const _e=Q.createElement("style");_e.id=ie;const ge=T("layout_editor.preview.slot_area_label");return _e.textContent=`.g7le-preview-frame [data-editor-slot]{position:relative;min-height:96px;outline:2px dashed #94a3b8;outline-offset:-2px;}.g7le-preview-frame [data-editor-slot]::after{content:'${ge.replace(/'/g,"\\'")} · ' attr(data-editor-slot);position:absolute;top:8px;left:50%;transform:translateX(-50%);font-size:12px;line-height:1;color:#64748b;background:rgba(241,245,249,0.92);border:1px solid #cbd5e1;border-radius:9999px;padding:4px 12px;pointer-events:none;white-space:nowrap;}`,Q.head.appendChild(_e),()=>{Q.getElementById(ie)?.remove()}},[p.editMode,T]),!(p.editMode==="extension"||p.editMode==="iteration_item")&&(p.selectedRoute?.layoutName===null||p.selectedRoute===null))return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--empty","data-testid":"g7le-preview-empty",style:Zo,children:T("layout_editor.preview.empty")});if(m)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--loading","data-testid":"g7le-preview-loading",style:Zo,children:T("layout_editor.preview.loading")});if(h)return i.jsx(Fa,{error:h});if(D)return i.jsx(Fa,{error:D});if(!z||$===null||A===null)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--assets-loading","data-testid":"g7le-preview-assets-loading",style:Zo,children:T("layout_editor.preview.assets_loading")});if(se.length===0)return i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--empty-layout","data-testid":"g7le-preview-empty-layout",style:Zo,children:T("layout_editor.preview.empty_layout")});const he=p.editMode==="extension"?re==="no-injection"?"no-injection":re==="ok"&&ee.length>0&&fe!==!0?"gated-no-state":null:null;return ne?i.jsx("div",{className:"g7le-preview-canvas","data-testid":"g7le-preview-canvas",style:{overflow:"visible",background:"#f1f5f9",padding:24,display:"flex",justifyContent:"center",alignItems:"flex-start",minHeight:"calc(100vh - var(--g7le-header-h, 0px))",boxSizing:"border-box"},children:i.jsxs("div",{className:"g7le-preview-frame-wrapper","data-testid":"g7le-preview-frame-wrapper",style:{position:"relative",width:k,transform:`scale(${w})`,transformOrigin:"top center"},children:[i.jsx("div",{className:_==="dark"?"g7le-preview-frame g7le-preview-dark":"g7le-preview-frame","data-testid":"g7le-preview-frame","data-color-scheme":_,ref:Q=>{v.current=Q,x(Q)},style:{width:k,background:"#fff",border:"1px solid #cbd5e1",boxShadow:"0 1px 3px rgba(0,0,0,0.05)",minHeight:400,position:"relative",transform:"translateZ(0)",overflow:"hidden"},children:i.jsx(KE,{overrideWidth:k,children:se.map((Q,ie)=>i.jsx(FE,{componentDef:Q,dataContext:{...Qe,...T4(V),_global:D4(V),_computedDefinitions:qe,...I?{_localInit:I,_local:{...De._local??{},...V._local??{}}}:{},route:K4({...De.route??{},...ve,path:p.selectedRoute?.path??""},G),query:{...De.query??{},...F??{}}},translationContext:{templateId:p.templateIdentifier,locale:p.locale},registry:$,bindingEngine:O.current,translationEngine:Pt.getInstance(),actionDispatcher:M.current,isEditMode:!0,isRootRenderer:ie===0,componentPath:`${ie}`,onComponentSelect:(_e,ge)=>{},onComponentHover:(_e,ge)=>{}},`${Q.id??`preview-${ie}`}:t${Me}${p.editMode==="extension"?`:s${ne?1:0}`:""}`))})}),i.jsx("div",{className:"g7le-overlay-layer","data-testid":"g7le-overlay-layer",style:{position:"absolute",left:0,top:0,width:k,height:b?b.getBoundingClientRect().height/(w||1):"100%",overflow:"visible",pointerEvents:"none"},children:i.jsx(p4,{frameEl:b,manifest:t,nesting:n,componentPalette:o,spec:r,permissionCandidates:s,resolveRouteMatch:l,onNavigateToDestination:d,onSessionAddedPathsChange:c})}),he!==null&&i.jsx("div",{className:"g7le-preview-canvas--extension-fallback","data-testid":"g7le-preview-extension-fallback","data-fallback-reason":he,style:{position:"absolute",left:0,top:0,right:0,bottom:0,minHeight:b?b.getBoundingClientRect().height/(w||1):400,background:"#f1f5f9",display:"flex",alignItems:"flex-start",justifyContent:"center",padding:24,boxSizing:"border-box",pointerEvents:"auto",zIndex:50},children:i.jsxs("div",{className:"g7le-extension-fallback-card",style:{maxWidth:440,textAlign:"center",position:"sticky",top:"calc(40vh - var(--g7le-header-h, 0px))"},children:[i.jsx("p",{className:"g7le-extension-fallback-title",style:{fontWeight:600,marginBottom:8,color:"#0f172a"},children:T(he==="no-injection"?"layout_editor.preview.extension_no_injection_title":"layout_editor.preview.extension_gated_title")}),i.jsx("p",{className:"g7le-extension-fallback-desc",style:{color:"#475569",lineHeight:1.6},children:he==="no-injection"?T("layout_editor.preview.extension_no_injection_desc"):p.availableStates.length>1?T("layout_editor.preview.extension_gated_desc_with_states"):T("layout_editor.preview.extension_gated_desc")})]})})]})}):i.jsx("div",{className:"g7le-preview-canvas g7le-preview-canvas--scripts-loading","data-testid":"g7le-preview-scripts-loading",style:Zo,children:T("layout_editor.preview.loading")})}function H4(e,t,n){if(!e)return;if(!t)return e;const o=s=>{if(typeof s=="string"&&s.startsWith("$t:"))try{const a=s.slice(3),l=a.indexOf("|");if(l===-1)return t.translate(a,n);const d=a.slice(0,l),c=a.slice(l);return t.translate(d,n,c)}catch{return s}return Array.isArray(s)?s.map(o):s},r={};for(const[s,a]of Object.entries(e))r[s]=o(a);return r}function F4(e,t){const n=[],o=(r,s)=>{Array.isArray(r)&&r.forEach((a,l)=>{if(!a||typeof a!="object")return;const d=s?`${s}.children.${l}`:`${l}`,c=a.__source;if(c?.kind==="extension"&&c.extensionId===t){n.push(d);return}Array.isArray(a.children)&&o(a.children,d)})};return o(e,""),n}function K4(e,t){if(!t)return e;const n={...e};for(const[o,r]of Object.entries(t))r===null?delete n[o]:n[o]=r;return n}const Zo={flex:1,display:"flex",alignItems:"center",justifyContent:"center",padding:24,background:"#f1f5f9",color:"#64748b",fontSize:13},V4={desktop:"layout_editor.device.desktop",tablet:"layout_editor.device.tablet",mobile:"layout_editor.device.mobile",portable:"layout_editor.device.portable"};function U4(){const{device:e,zoom:t,customWidth:n,setDevice:o,setZoom:r,setCustomWidth:s,colorScheme:a,setColorScheme:l}=ja(),{t:d}=tt(),c=Gf();return i.jsxs("div",{className:"g7le-device-toolbar","data-testid":"g7le-device-toolbar",style:{display:"inline-flex",alignItems:"center",gap:8,padding:"4px 8px",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6},children:[i.jsxs("div",{role:"group","aria-label":d("layout_editor.device.group_label"),children:[c.map(p=>{const f=e===p,y=V4[p],g=y?d(y):p;return i.jsx("button",{type:"button","data-testid":`g7le-device-${p}`,"aria-pressed":f,onClick:()=>o(p),style:{padding:"2px 8px",marginRight:2,fontSize:12,cursor:"pointer",background:f?"#0f172a":"#f8fafc",color:f?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:g},p)}),(()=>{const p=e==="custom";return i.jsx("button",{type:"button","data-testid":"g7le-device-custom","aria-pressed":p,onClick:()=>o("custom"),style:{padding:"2px 8px",marginRight:2,fontSize:12,cursor:"pointer",background:p?"#0f172a":"#f8fafc",color:p?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:d("layout_editor.device.custom")})})()]}),e==="custom"&&i.jsxs("div",{style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("label",{htmlFor:"g7le-custom-width-input",style:{fontSize:11,color:"#475569"},children:d("layout_editor.device.custom_width_label")}),i.jsx("input",{id:"g7le-custom-width-input",type:"number",min:ug,max:pg,step:10,value:n,"data-testid":"g7le-custom-width-input","aria-label":d("layout_editor.device.custom_width_label"),onChange:p=>{const f=parseInt(p.currentTarget.value,10);Number.isFinite(f)&&s(f)},style:{width:72,padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4}}),i.jsx("span",{style:{fontSize:11,color:"#475569"},children:"px"})]}),i.jsx("div",{role:"group","aria-label":d("layout_editor.toolbar.color_scheme_group"),style:{display:"inline-flex",gap:2},children:["light","dark"].map(p=>{const f=a===p;return i.jsx("button",{type:"button","data-testid":`g7le-toolbar-scheme-${p}`,"aria-pressed":f,onClick:()=>l(p),style:{padding:"2px 8px",fontSize:12,cursor:"pointer",background:f?"#0f172a":"#f8fafc",color:f?"#fff":"#0f172a",border:"1px solid #cbd5e1",borderRadius:4},children:d(p==="light"?"layout_editor.toolbar.color_scheme_light":"layout_editor.toolbar.color_scheme_dark")},p)})}),i.jsxs("div",{style:{display:"inline-flex",alignItems:"center",gap:4},children:[i.jsx("span",{style:{fontSize:11,color:"#475569"},children:d("layout_editor.zoom.label")}),i.jsx("input",{type:"range",min:.5,max:1,step:.05,value:t,"data-testid":"g7le-zoom-slider","aria-label":d("layout_editor.zoom.aria_label"),onChange:p=>r(parseFloat(p.currentTarget.value)),style:{width:100}}),i.jsxs("span",{style:{fontSize:11,color:"#475569",minWidth:36,textAlign:"right"},children:[Math.round(t*100),"%"]})]}),i.jsx(Kb,{})]})}function q4(){const{t:e}=tt(),{state:t,dispatch:n}=Je(),{availableStates:o,activeStateId:r}=t,[,s]=u.useState(0);u.useEffect(()=>{if(typeof window>"u")return;const p=()=>s(y=>y+1),f=window.requestAnimationFrame(p);return window.addEventListener(Ga,p),()=>{window.cancelAnimationFrame(f),window.removeEventListener(Ga,p)}},[]);const a=u.useCallback((p,f)=>{if(!p)return f;if(!p.startsWith("$t:"))return p;const y=p.slice(3),g=window.G7Core;if(g&&typeof g.t=="function"){const h=g.t(y);if(h&&h!==y&&!h.startsWith("$t:"))return h}const m=e(y);return m&&m!==y?m:f},[e]);if(!Array.isArray(o)||o.length<=1)return null;const l=p=>{const f=p.target.value;n({type:"SET_ACTIVE_STATE",activeStateId:f}),$l({kind:"change",activeStateId:f,routePath:t.selectedRoute?.path??null,availableCount:o.length})},d=o.find(p=>p.id===r)??o[0],c=d?.description?a(d.description,""):"";return i.jsxs("div",{className:"g7le-state-switcher","data-testid":"g7le-state-switcher","data-active-state":r??"",style:{display:"inline-flex",alignItems:"center",gap:6,padding:"4px 8px",background:"#fff",border:"1px solid #cbd5e1",borderRadius:6},children:[i.jsx("span",{style:{fontSize:12,fontWeight:600,color:"#475569",whiteSpace:"nowrap"},children:e("layout_editor.toolbar.state")}),i.jsx("select",{"data-testid":"g7le-state-switcher-select","aria-label":e("layout_editor.toolbar.state"),value:r??o[0].id,onChange:l,style:{fontSize:12,padding:"2px 6px",border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",color:"#0f172a",cursor:"pointer",outline:"none"},children:o.map(p=>i.jsx("option",{value:p.id,children:a(p.label,p.id)},p.id))}),c&&i.jsx("span",{"data-testid":"g7le-state-switcher-description",style:{fontSize:11,color:"#94a3b8",whiteSpace:"nowrap"},children:c})]})}function G4(e,t,n){if(e==="extension"){if(typeof n=="string"&&n.length>0){const o=Ya(n,"__base__/");return o?{kind:"base",match:o}:{kind:"route",match:n}}return null}if(!t)return null;switch(e){case"route":return{kind:"route",match:t};case"base":{const o=Ya(t,"__base__/");return o?{kind:"base",match:o}:null}case"modal":{const o=Ya(t,"__modal__/");return o?{kind:"modal",match:o}:null}default:return null}}function Y4(e,t){if(!t||!Array.isArray(e)||e.length===0)return[];const n=[];for(const o of e)if(J4(o.scope,t))for(const r of o.items??[])r&&typeof r.id=="string"&&r.id.length>0&&n.push(r);return n}function X4(e){if(e.length===0)return null;const t=e.find(n=>n.default===!0);return t?t.id:e[0].id}function J4(e,t){if(!e||e.kind!==t.kind)return!1;const n=e.match;return typeof n!="string"||n.length===0?!1:t.kind==="route"?Z4(t.match,n):n===t.match}function Z4(e,t){if(t===e)return!0;if(!t.includes("*"))return!1;const n=t.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\/\*\\\?/g,"(?:/[^/]+)?").replace(/\*/g,"[^/]+");return new RegExp(`^${n}$`).test(e)}function Ya(e,t){return e.startsWith(t)?e.slice(t.length):""}const Q4="__conngroup__/modals/",R4="__conngroup__/extensions/";function eD(e){return e.meta?.editor_label?{label:e.meta.editor_label,labelSource:"editor_label"}:e.meta?.title?{label:e.meta.title,labelSource:"title"}:{label:e.path,labelSource:"path"}}function tD(e){switch(e.source.kind){case"template":return"template";case"module":return`module:${e.source.identifier??""}`;case"plugin":return`plugin:${e.source.identifier??""}`;case"core":return null;default:return null}}function nD(e,t){if(e.meta?.parent&&t.has(e.meta.parent))return e.meta.parent;const n=e.path.split("/").filter(Boolean);for(let o=n.length-1;o>=1;o--){const r="/"+n.slice(0,o).join("/");if(r!==e.path&&t.has(r))return r}return null}function oD(e){const t=new Map;for(const n of e)n.layout&&t.set(n.layout,(t.get(n.layout)??0)+1);return t}function rD(e){const t=new Map;for(const n of e){if(!n.hostLayout)continue;const o=t.get(n.hostLayout)??[];o.push(n),t.set(n.hostLayout,o)}return t}function iD(e){const t=new Map;for(const n of e)for(const o of n.extensions)for(const r of o.hostLayouts??[]){if(!r)continue;const s=t.get(r)??[];s.push({extension:o,group:n}),t.set(r,s)}return t}function sD(e,t){return{path:`__modal__/${e.hostLayout}/${e.modalId}`,layoutName:e.hostLayout,label:e.label??e.modalId,labelSource:e.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"modal",modalId:e.modalId,modalHostLayout:e.hostLayout,connectedHostRoutePath:t,children:[]}}function aD(e,t){const{extension:n,group:o}=e,r=o.sourceType==="plugin"?"🔌":"🧩";return{path:`__extension__/${n.id}`,layoutName:null,label:`${r} ${o.sourceLabel} · ${n.targetName}`,labelSource:"path",source:{kind:o.sourceType,identifier:o.sourceIdentifier},kind:"extension",extensionId:String(n.id),extensionType:n.extensionType,extensionPriority:n.priority,extensionTargetName:n.targetName,isModified:n.isModified,isInactive:!n.isActive,connectedHostRoutePath:t,children:[]}}function kg(e,t,n){if(e.kind!=="route"&&e.kind!=="base"||!e.layoutName)return;const o=[],r=t.get(e.layoutName)??[];r.length>0&&o.push({path:`${Q4}${e.path}`,layoutName:null,label:`$t:layout_editor.chrome.route_tree.connected.modals|count=${r.length}`,labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:r.map(a=>sD(a,e.path))});const s=n.get(e.layoutName)??[];s.length>0&&o.push({path:`${R4}${e.path}`,layoutName:null,label:`$t:layout_editor.chrome.route_tree.connected.extensions|count=${s.length}`,labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:s.map(a=>aD(a,e.path))}),o.length>0&&(e.children=[...o,...e.children??[]])}function lD(e){const t=[],n=new Set(e.routes.map(o=>o.path));for(const o of e.routes){const r=tD(o);if(!r)continue;const{label:s,labelSource:a}=eD(o),l=nD(o,n),d={path:o.path,layoutName:o.layout??null,label:s,labelSource:a,icon:o.meta?.icon??null,source:o.source,isRedirect:!!o.redirect,isHidden:!!o.meta?.hidden,kind:"route",children:[]};t.push({node:d,parentKey:l,groupKey:r})}return t}function dD(e,t){if(e==="template")return"$t:layout_editor.chrome.route_tree.group.template";if(e.startsWith("module:")){const n=e.slice(7);return`$t:layout_editor.chrome.route_tree.group.module|name=${t.moduleDisplayNames[n]??n}`}if(e.startsWith("plugin:")){const n=e.slice(7);return`$t:layout_editor.chrome.route_tree.group.plugin|name=${t.pluginDisplayNames[n]??n}`}return e}function cD(e,t){oD(e.routes);const n=rD(e.modals),o=iD(e.extensionGroups??[]),r=new Map;for(const d of t)r.has(d.groupKey)||r.set(d.groupKey,[]),r.get(d.groupKey).push(d);const s=Array.from(r.keys()).sort((d,c)=>{const p=g=>g==="template"?1:g.startsWith("module:")?2:g.startsWith("plugin:")?3:4,f=p(d),y=p(c);return f!==y?f-y:d.localeCompare(c)}),a=[];if(e.baseLayouts.length>0){const d=e.baseLayouts.map(c=>({path:`__base__/${c.layoutName}`,layoutName:c.layoutName,label:c.label??c.layoutName,labelSource:c.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"base",children:[]}));for(const c of d)kg(c,n,o);a.push({path:"__group__/base",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.base",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}for(const d of s){const c=r.get(d),p=new Map;for(const y of c)p.set(y.node.path,y);const f=[];for(const y of c)if(y.parentKey&&p.has(y.parentKey)){const g=p.get(y.parentKey);g.node.children=g.node.children??[],g.node.children.push(y.node)}else f.push(y.node);for(const y of c)kg(y.node,n,o);a.push({path:`__group__/${d}`,layoutName:null,label:dD(d,e),labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:f})}if(e.modals.length>0){const d=e.modals.map(c=>({path:`__modal__/${c.hostLayout}/${c.modalId}`,layoutName:c.hostLayout,label:c.label??c.modalId,labelSource:c.label?"editor_label":"path",source:{kind:"core",identifier:null},kind:"modal",modalId:c.modalId,modalHostLayout:c.hostLayout,children:[]}));a.push({path:"__group__/modal",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.modal",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}const l=e.extensionGroups??[];if(l.length>0){const d=l.map(c=>{const p=c.sourceType==="plugin"?"🔌":"🧩",f=c.extensions.map(y=>({path:`__extension__/${y.id}`,layoutName:null,label:`${c.sourceLabel} · ${y.targetName}`,labelSource:"path",source:{kind:c.sourceType,identifier:c.sourceIdentifier},kind:"extension",extensionId:String(y.id),extensionType:y.extensionType,extensionPriority:y.priority,extensionTargetName:y.targetName,isModified:y.isModified,isInactive:!y.isActive,children:[]}));return{path:`__extgroup__/${c.sourceType}/${c.sourceIdentifier}`,layoutName:null,label:`${p} ${c.sourceLabel}`,labelSource:"path",source:{kind:c.sourceType,identifier:c.sourceIdentifier},kind:"route",children:f}});a.push({path:"__group__/extension",layoutName:null,label:"$t:layout_editor.chrome.route_tree.group.extension",labelSource:"editor_label",source:{kind:"core",identifier:null},kind:"route",children:d})}return a}function uD(e){const t=lD(e);return cD(e,t)}const On=bo("useEditorRoutes");function pD(e,t){if(typeof e=="string")return e.length>0?e:null;if(e&&typeof e=="object"){const n=e[t];if(typeof n=="string"&&n.length>0)return n;if(typeof e.en=="string"&&e.en.length>0)return e.en;for(const o of Object.values(e))if(typeof o=="string"&&o.length>0)return o}return null}function Sg(e,t){const n={};if(typeof window>"u")return n;const r=window.G7Config?.[e];if(!Array.isArray(r))return n;for(const s of r)s&&typeof s.identifier=="string"&&(n[s.identifier]=pD(s.display_name,t)??s.identifier);return n}function fD(e){const t=new vd,n=typeof window<"u"?window.G7Config:void 0,o={_global:{settings:n?.settings??{},plugins:n?.plugins??{},modules:n?.modules??{},appConfig:n?.appConfig??{}}};return e.map(r=>{const s={...r};if(typeof s.path=="string"&&s.path.includes("{{")){const a=s.path;let l=t.resolveBindings(s.path,o);l=l.replace(/\/\/+/g,"/")||"/",l.includes("{{")&&(On.warn("편집기 route path 표현식 평가 실패 — fallback 적용",{originalPath:a}),l=a.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/"),s.path=l}if(typeof s.redirect=="string"&&s.redirect.includes("{{")){const a=s.redirect;let l=t.resolveBindings(a,o);l=l.replace(/\/\/+/g,"/")||"/",l.includes("{{")&&(On.warn("편집기 route redirect 표현식 평가 실패 — fallback 적용",{originalRedirect:a}),l=a.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/"),s.redirect=l}return s})}function gD(e){const{dispatch:t}=Je(),{translationEngine:n,translationContext:o}=tt(),{templateIdentifier:r,cacheVersion:s}=e;u.useEffect(()=>{let a=!1;const l=s??(typeof window<"u"?window.G7Config?.cache_version:void 0)??"",d=l!==""?`?v=${encodeURIComponent(String(l))}`:"",c=en(`/api/admin/templates/${encodeURIComponent(r)}/editor/routes`,"json",null,d.replace(/^[?&]+/,"")),p=o?.locale??"ko",f=n&&r!==o?.templateId?n.loadTranslations(r,p).then(()=>{On.log("편집 대상 lang 로드 완료",{targetTemplate:r,locale:p})}).catch(h=>{On.warn("편집 대상 lang 로드 실패 — 라우트 라벨 fallback 사용",h)}):Promise.resolve(),y=(()=>{if(typeof fetch!="function")return Promise.reject(io(new Error("fetch unavailable"),"routes"));try{const h=fetch(c,{credentials:"same-origin",headers:je()});return!h||typeof h.then!="function"?Promise.reject(io(new Error("fetch returned non-Promise (test stub?)"),"routes")):h.then(async v=>{if(!v.ok){const b=await v.json().catch(()=>null);throw Gr(v.status,b,"routes")}return v.json()})}catch(h){return Promise.reject(h)}})(),g=`/api/admin/templates/${encodeURIComponent(r)}/layout-extensions${d}`,m=(()=>{if(typeof fetch!="function")return Promise.resolve(null);try{const h=fetch(g,{credentials:"same-origin",headers:je()});return!h||typeof h.then!="function"?Promise.resolve(null):h.then(async v=>v.ok?await v.json():null).catch(()=>null)}catch{return Promise.resolve(null)}})();return Promise.all([y,f,m]).then(([h,,v])=>{if(a)return;if(!h||!h.success||!Array.isArray(h.data?.routes)){On.warn("routes 응답 형식이 비정상 — empty tree 로 둠",h),t({type:"SET_ROUTES_ERROR",error:{kind:"unknown",status:200,message:"routes 응답 형식이 비정상입니다.",source:"routes"}});return}const b=fD(h.data.routes),x=Sg("activeModules",p),k=Sg("activePlugins",p),w=(h.data.base_layouts??[]).map(A=>({layoutName:A.layout_name,label:A.label??void 0})),_=(h.data.modals??[]).map(A=>({modalId:A.modal_id,hostLayout:A.host_layout,label:A.label??void 0})),C=(v?.success&&Array.isArray(v.data)?v.data:[]).map(A=>({sourceIdentifier:A.source_identifier,sourceType:A.source_type,sourceLabel:A.source_label,extensions:(A.extensions??[]).map(z=>({id:z.id,extensionType:z.extension_type,targetName:z.target_name,priority:z.priority,isActive:z.is_active,isModified:z.is_modified,hostLayouts:Array.isArray(z.host_layouts)?z.host_layouts:[],currentVersion:typeof z.current_version=="number"?z.current_version:void 0}))})),E=uD({routes:b,modals:_,baseLayouts:w,extensionGroups:C,moduleDisplayNames:x,pluginDisplayNames:k});t({type:"SET_ROUTE_TREE",tree:E});const S=h.data.layout_versions,T={};if(S&&typeof S=="object"&&!Array.isArray(S))for(const[A,z]of Object.entries(S))typeof z=="number"&&Number.isFinite(z)&&(T[A]=z);t({type:"SET_LAYOUT_VERSIONS",versions:T});const $={};for(const A of C)for(const z of A.extensions){const D=z.currentVersion;typeof D=="number"&&Number.isFinite(D)&&($[String(z.id)]=D)}t({type:"SET_EXTENSION_VERSIONS",versions:$}),On.log("routes 로드 완료",{count:b.length,groups:E.length})}).catch(h=>{if(a)return;On.error("routes 로드 실패",h);const v=hp(h)?h:io(h,"routes");t({type:"SET_ROUTES_ERROR",error:v})}),()=>{a=!0}},[r,s,t,n,o?.locale,o?.templateId])}function Cg(e,t,n,o=[]){const r=t?$g(o,t):null,s=r!==null?{path:r.path,layoutName:r.layoutName}:void 0;if(e.startsWith("__base__/")){const a=e.slice(9);if(a)return n({type:"ENTER_BASE_EDIT",layoutName:a,returnRoute:s}),!0}else if(e.startsWith("__modal__/")){const a=e.slice(10);return a&&!a.includes("/")&&t?(n({type:"ENTER_MODAL_EDIT",modalId:a,hostLayout:t,returnRoute:s}),!0):!1}else if(e.startsWith("__extension__/")){const a=e.slice(14);if(a&&!a.includes("/"))return n({type:"ENTER_EXTENSION_EDIT",extensionId:a,returnRoute:s}),!0}else if(e.startsWith("__iteration__/")){const a=e.slice(14);return a&&t?(n({type:"ENTER_ITERATION_ITEM_EDIT",sourcePath:a,hostLayout:t,returnRoute:s}),!0):!1}return!1}function $g(e,t){for(const n of e){if(n.kind==="route"&&n.layoutName===t)return n;if(n.children&&n.children.length>0){const o=$g(n.children,t);if(o)return o}}return null}function Xa(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=Xa(n.children,t);if(o)return o}}return null}function hD(e){const{state:t,dispatch:n}=Je(),o=u.useRef(!1),r=u.useRef(t.editMode);r.current=t.editMode,u.useEffect(()=>{if(o.current||t.routeTree.length===0||typeof window>"u")return;const a=ap(window.location.search);if(a){const c=sp(window.location.search),p=Cg(a,c,n,t.routeTree);if(o.current=!0,p)return}const l=ip(window.location.search);if(!l){o.current=!0;return}const d=Xa(t.routeTree,l);d&&n({type:"SELECT_ROUTE",route:{path:d.path,layoutName:d.layoutName}}),o.current=!0},[t.routeTree,n]),u.useEffect(()=>{if(typeof window>"u")return;const a=()=>{const l=ap(window.location.search);if(l){const p=sp(window.location.search);if(Cg(l,p,n,t.routeTree))return}if(r.current!=="route")switch(r.current){case"base":n({type:"EXIT_BASE_EDIT"});return;case"modal":n({type:"EXIT_MODAL_EDIT"});return;case"extension":n({type:"EXIT_EXTENSION_EDIT"});return;case"iteration_item":n({type:"EXIT_ITERATION_ITEM_EDIT"});return}const d=ip(window.location.search);if(!d){n({type:"SELECT_ROUTE",route:null});return}const c=Xa(t.routeTree,d);c&&n({type:"SELECT_ROUTE",route:{path:c.path,layoutName:c.layoutName}})};return window.addEventListener("popstate",a),()=>{window.removeEventListener("popstate",a)}},[t.routeTree,n]);const s=u.useRef("route");u.useEffect(()=>{if(typeof window>"u"||!window.history?.replaceState||t.routeTree.length===0)return;const a=Zs(e,t.selectedRoute?.path??null,t.selectedRoute?.layoutName??null),l=`${window.location.pathname}${window.location.search}`,d=s.current;if(s.current=t.editMode,l===a)return;d==="route"&&t.editMode!=="route"&&window.history.pushState?window.history.pushState(window.history.state,"",a):window.history.replaceState(window.history.state,"",a)},[t.selectedRoute,t.editMode,t.routeTree,e])}function yD(){u.useEffect(()=>{if(typeof document>"u")return;const e=document.documentElement;let t=e.classList.contains("dark");const n=()=>{e.classList.contains("dark")&&(t=!0,e.classList.remove("dark"))};n();let o=null;return typeof MutationObserver<"u"&&(o=new MutationObserver(n),o.observe(e,{attributes:!0,attributeFilter:["class"]})),()=>{o?.disconnect(),t&&e.classList.add("dark")}},[])}function bD(){u.useEffect(()=>{if(typeof document>"u"||!document.body)return;const e=document.body,t=()=>{e.style.overflow==="hidden"&&(e.style.overflow=""),e.style.overflowY==="hidden"&&(e.style.overflowY="")};t();let n=null;return typeof MutationObserver<"u"&&(n=new MutationObserver(t),n.observe(e,{attributes:!0,attributeFilter:["style"]})),()=>{n?.disconnect()}},[])}function mD(e){if(typeof e!="string")return null;const t="__extension__/";if(!e.startsWith(t))return null;const n=e.slice(t.length);return n===""||n.includes("/")?null:n}function vD(e){if(Array.isArray(e.components))return e.components;if(Array.isArray(e.injections)){const t=[];return e.injections.forEach((n,o)=>{if(n&&Array.isArray(n.components))for(const r of n.components)t.push({...r,__injectionIndex:o})}),t}return[]}function _D(e,t){if(Array.isArray(e.components)){const n=t.map(o=>Ja(o));return{...e,components:n}}if(Array.isArray(e.injections)){const n=e.injections.map(o=>({...o}));n.forEach(o=>{Array.isArray(o.components)&&(o.components=[])});for(const o of t){const r=o.__injectionIndex,s=typeof r=="number"&&r>=0&&r<n.length?n[r]:null;s&&(Array.isArray(s.components)||(s.components=[]),s.components.push(Ja(o)))}return{...e,injections:n}}return e}function Ja(e){if(!e||typeof e!="object")return e;const t={...e};return delete t.__injectionIndex,delete t.__source,delete t.__injectedProps,delete t._fromBase,Array.isArray(t.children)&&(t.children=t.children.map(n=>Ja(n))),t}function Eg(e,t){const n=[],o=r=>{if(Array.isArray(r))for(const s of r){if(!s||typeof s!="object")continue;const a=s.__source;if(a?.kind==="extension"&&a.extensionId===t){n.push(s);continue}Array.isArray(s.children)&&o(s.children)}};return o(e),n}function Ig(e,t){const o=Eg(e,t)[0]?.__source;return o&&o.kind==="extension"&&o.extensionId===t?o:{kind:"extension",extensionId:t}}function Za(e,t){for(const n of e){const o=n?.__source;if(o?.kind==="extension"&&o.extensionId===t||Array.isArray(n.children)&&Za(n.children,t))return!0}return!1}function xD(){const{state:e}=Je(),t=e.templateIdentifier,n=e.editMode==="extension"?mD(e.selectedRoute?.path):null,o=e.editMode==="extension"&&typeof e.selectedRoute?.layoutName=="string"?e.selectedRoute.layoutName:null,[r,s]=u.useState(null),[a,l]=u.useState(!1),[d,c]=u.useState(null),[p,f]=u.useState(!1),[y,g]=u.useState(0),[m,h]=u.useState(null),v=u.useRef({}),b=u.useRef({kind:"extension",extensionId:0}),x=u.useRef(null);u.useEffect(()=>{x.current=r},[r]),u.useEffect(()=>{h(null)},[n]);const k=u.useCallback(async A=>{const z=window.G7Config?.cache_version??0,D=en(`/api/layouts/${encodeURIComponent(t)}/${A}`,"json",`${z}.${Sp()}`,"with_source_meta=1"),O=ar(),M={Accept:"application/json"};O&&(M.Authorization=`Bearer ${O}`);const B=await fetch(D,{headers:M,credentials:"same-origin"});if(!B.ok)return null;const N=await B.json().catch(()=>null);return(N&&(N.data||N))??{}},[t]),w=u.useCallback(async()=>{if(!n){s(null),c(null),f(!1);return}l(!0),c(null);try{const A=window.G7Config?.cache_version??0,z=`/api/admin/templates/${encodeURIComponent(t)}/layout-extensions/${encodeURIComponent(n)}?v=${A}`,D=await fetch(z,{headers:je(),credentials:"same-origin"}),O=await D.json().catch(()=>null);if(!D.ok){c(Gr(D.status,O,"layout")),s(null),f(!1);return}const M=(O&&(O.data||O))??{};let B={};const N=M.content;if(typeof N=="string")try{B=JSON.parse(N)}catch{B={}}else N&&typeof N=="object"&&(B=N);const H=M.extension_type==="overlay"?"overlay":"extension_point",L=typeof M.lock_version=="number"?M.lock_version:0,I=Array.isArray(M?.host_layouts)?M.host_layouts.filter(re=>typeof re=="string"):[],P=o??(I.length===1?I[0]:m&&I.includes(m)?m:null),F=Number(n);v.current=B;let V=vD(B),Y={},Z="pending";const se={kind:"extension",extensionId:F};if(b.current=se,P){const re=await k(P);if(re){Y=re;const U=Array.isArray(re.components)?re.components:[],ee=Ig(U,F);if(b.current=ee,Za(U,F))V=U,Z="ok";else{const K=(Array.isArray(re.modals)?re.modals:[]).filter(ne=>ne&&typeof ne=="object"&&Za([ne],F));if(K.length>0){const ne=K.map(me=>({...me,props:{...me.props??{},isOpen:!0}}));V=[...U,...ne],b.current=Ig(K,F),Z="ok"}else V=U,Z="no-injection"}}}s({extensionId:F,content:B,extensionType:H,components:V,lockVersion:L,hostLayouts:I,hostLayoutName:P,hostRaw:Y,editability:Z}),f(!1),st({op:"load",layoutName:`extension:${n}`,editMode:"extension",statusCode:D.status,timestamp:Date.now()})}catch(A){c(io(A,"layout")),s(null),f(!1)}finally{l(!1)}},[t,n,m,o,k]);u.useEffect(()=>{w()},[w]);const _=u.useCallback(A=>{h(A)},[]),C=u.useCallback(A=>{s(z=>z&&{...z,components:A}),f(!0),st({op:"patch",layoutName:`extension:${x.current?.extensionId??""}`,editMode:"extension",isDirty:!0,timestamp:Date.now()})},[]),E=u.useCallback(A=>{s(z=>z&&{...z,components:A(z.components)}),f(!0)},[]),S=u.useCallback(async()=>{const A=x.current;if(!A)return{kind:"guard_no_document"};const z=`/api/admin/templates/${encodeURIComponent(t)}/layout-extensions/${A.extensionId}`,D=Eg(A.components,A.extensionId),O=D.length>0?D:A.components,M=_D(v.current,O);st({op:"save",layoutName:`extension:${A.extensionId}`,editMode:"extension",saveTarget:"layout_extension",endpoint:z,isDirty:p,timestamp:Date.now()});let B;try{B=await fetch(z,{method:"PUT",headers:je({"Content-Type":"application/json"}),credentials:"same-origin",body:JSON.stringify({content:M,expected_lock_version:A.lockVersion})})}catch(H){return{kind:"network_error",message:H instanceof Error?H.message:"network error"}}const N=await B.json().catch(()=>null);if(st({op:"save_response",layoutName:`extension:${A.extensionId}`,editMode:"extension",endpoint:z,statusCode:B.status,conflict:B.status===409?{currentVersion:N?.errors?.current_version??N?.current_version,yourVersion:N?.errors?.your_version??N?.your_version}:void 0,timestamp:Date.now()}),B.ok){const H=N?.data??{},L=typeof H.lock_version=="number"?H.lock_version:A.lockVersion+1;s(P=>P&&{...P,lockVersion:L}),f(!1),g(P=>P+1);const I=typeof H.current_version=="number"?H.current_version:void 0;return{kind:"success",newLockVersion:L,newContentVersion:I,savedExtensionId:String(A.extensionId)}}return B.status===409?{kind:"concurrent_modification",currentVersion:N?.current_version??-1,yourVersion:N?.your_version??A.lockVersion}:B.status===422?{kind:"validation_failed",status:422,errors:N?.errors??null}:{kind:"network_error",message:N?.message??`HTTP ${B.status}`}},[t,p]),T=u.useCallback(async()=>{na(),await w()},[w]),$=!!r&&r.hostLayouts.length>1&&r.hostLayoutName===null;return{document:r,isLoading:a,error:d,isDirty:p,saveSuccessCounter:y,setLayoutComponents:C,patchLayout:E,save:S,reload:T,needsHostPicker:$,selectHost:_}}const Ag=new Set;function wD(e,t,n){const r=e&&Object.keys(e).length>0?e:t,s=Array.isArray(r.data_sources)?r.data_sources:[],a=Array.isArray(t.data_sources)?t.data_sources:[],l=new Set,d=[];for(const c of[...s,...a]){const p=c&&typeof c=="object"?c.id:void 0,f=typeof p=="string"?p:`__anon_${d.length}`;l.has(f)||(l.add(f),d.push(c))}return{...r,components:n,...d.length>0?{data_sources:d}:{},lock_version:void 0}}function kD(e){return{document:e.document?{layoutName:`extension:${e.document.extensionId}`,raw:{...wD(e.document.hostRaw,e.document.content,e.document.components),__editability:e.document.editability,__extensionId:e.document.extensionId},lockVersion:e.document.lockVersion}:null,isLoading:e.isLoading,error:e.error,isDirty:e.isDirty,saveSuccessCounter:e.saveSuccessCounter,reload:e.reload,patchLayout:e.patchLayout,setLayoutComponents:e.setLayoutComponents,patchDocumentRaw:()=>{},dirtyKeys:Ag,dirtyLayoutNames:Ag,invalidateCurrentCache:()=>{},reloadCounter:0,save:()=>e.save(),markDirty:()=>{}}}function SD({hostLayouts:e,extensionLabel:t,onSelect:n}){const{t:o}=tt();return i.jsx("div",{className:"g7le-host-picker__backdrop","data-testid":"g7le-host-picker",role:"dialog","aria-modal":"true","aria-label":o("layout_editor.host_picker.title"),style:{position:"fixed",inset:0,zIndex:9500,display:"flex",alignItems:"center",justifyContent:"center",background:"rgba(15, 23, 42, 0.55)"},children:i.jsxs("div",{className:"g7le-host-picker__panel",style:{width:"min(520px, 92vw)",maxHeight:"80vh",overflow:"auto",background:"#ffffff",color:"#0f172a",borderRadius:12,boxShadow:"0 20px 48px rgba(15, 23, 42, 0.28)",padding:"20px 22px"},children:[i.jsx("h2",{className:"g7le-host-picker__title",style:{margin:"0 0 6px",fontSize:16,fontWeight:700},children:o("layout_editor.host_picker.title")}),i.jsx("p",{className:"g7le-host-picker__desc",style:{margin:"0 0 16px",fontSize:13,color:"#475569",lineHeight:1.6},children:t?o("layout_editor.host_picker.description_with_label",{label:t}):o("layout_editor.host_picker.description")}),i.jsx("ul",{className:"g7le-host-picker__list",style:{listStyle:"none",margin:0,padding:0,display:"grid",gap:8},children:e.map(r=>i.jsx("li",{children:i.jsxs("button",{type:"button",className:"g7le-host-picker__item","data-testid":"g7le-host-picker-item","data-host":r,onClick:()=>n(r),style:{width:"100%",textAlign:"left",padding:"12px 14px",border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc",cursor:"pointer",fontSize:13,fontWeight:600,color:"#0f172a",display:"flex",alignItems:"center",gap:8},children:[i.jsx("span",{"aria-hidden":!0,style:{fontSize:14},children:"🖼"}),i.jsx("span",{style:{flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis"},children:r}),i.jsx("span",{"aria-hidden":!0,style:{color:"#94a3b8"},children:"→"})]})},r))})]})})}function CD(e,t){const[n,o]=u.useState(!1),[r,s]=u.useState(null),a=u.useCallback(async l=>{if(!t||!l)return{kind:"no_document"};o(!0),s(null);try{const d=vo(l),c=`/api/admin/templates/${encodeURIComponent(e)}/layouts/${t}/preview`,p=await fetch(c,{method:"POST",credentials:"same-origin",headers:je({"Content-Type":"application/json"}),body:JSON.stringify({content:JSON.stringify(d)})}),f=await p.json().catch(()=>null);if(!p.ok){const h=f?.message??`HTTP ${p.status}`;return s(h),{kind:"network_error",message:h}}const y=f?.data,g=typeof y?.token=="string"?y.token:"",m=typeof y?.preview_url=="string"&&y.preview_url?y.preview_url:g?`/preview/${g}`:"";return m?(window.G7Core?.dispatch?.({handler:"openWindow",params:{path:m}}),{kind:"success",previewUrl:m,token:g}):(s("invalid preview response"),{kind:"network_error",message:"invalid preview response"})}catch(d){const c=d instanceof Error?d.message:"network error";return s(c),{kind:"network_error",message:c}}finally{o(!1)}},[e,t]);return{isCreating:n,error:r,createPreview:a}}const ce="layout_editor.action_recipe",bn={setState:{label:`$t:${ce}.set_state.label`,params:[{key:"target",label:`$t:${ce}.set_state.param_target`,widget:"select",options:[{value:"local"},{value:"global"}]},{key:"state",label:`$t:${ce}.set_state.param_state`,widget:"state-key-value"},{key:"merge",label:`$t:${ce}.set_state.param_merge`,widget:"toggle"}],build:{handler:"setState",params:{target:"{{target}}",merge:"{{merge}}","...":"{{state}}"}}},loadFromLocalStorage:{label:`$t:${ce}.load_local.label`,params:[{key:"key",label:`$t:${ce}.load_local.param_key`,widget:"text",required:!0},{key:"stateKey",label:`$t:${ce}.load_local.param_state_key`,widget:"state-key-picker"},{key:"defaultValue",label:`$t:${ce}.load_local.param_default`,widget:"text"}],build:{handler:"loadFromLocalStorage",params:{key:"{{key}}",stateKey:"{{stateKey}}",defaultValue:"{{defaultValue}}"}}},saveToLocalStorage:{label:`$t:${ce}.save_local.label`,params:[{key:"key",label:`$t:${ce}.save_local.param_key`,widget:"text",required:!0},{key:"value",label:`$t:${ce}.save_local.param_value`,widget:"data-chip"}],build:{handler:"saveToLocalStorage",params:{key:"{{key}}",value:"{{value}}"}}},navigate:{label:`$t:${ce}.navigate.label`,params:[{key:"path",label:`$t:${ce}.navigate.param_path`,widget:"page-picker",required:!0},{key:"query",label:`$t:${ce}.navigate.param_query`,widget:"key-value"},{key:"mergeQuery",label:`$t:${ce}.navigate.param_merge_query`,widget:"toggle"},{key:"scroll",label:`$t:${ce}.navigate.param_scroll`,widget:"select",options:[{value:"top"},{value:"preserve"},{value:"none"}]},{key:"replace",label:`$t:${ce}.navigate.param_replace`,widget:"toggle"},{key:"transition_overlay_target",label:`$t:${ce}.navigate.param_overlay_target`,widget:"component-target-picker",dependsOn:{param:"replace",equals:!0}}],build:{handler:"navigate",params:{path:"{{path}}",query:"{{query}}",mergeQuery:"{{mergeQuery}}",scroll:"{{scroll}}",replace:"{{replace}}",transition_overlay_target:"{{transition_overlay_target}}"}}},openWindow:{label:`$t:${ce}.open_window.label`,params:[{key:"target",label:`$t:${ce}.open_window.param_target`,widget:"page-picker",required:!0},{key:"windowTarget",label:`$t:${ce}.open_window.param_window_target`,widget:"select",options:[{value:"_blank"},{value:"_self"}]}],build:{handler:"openWindow",target:"{{target}}",params:{windowTarget:"{{windowTarget}}"}}},navigateBack:{label:`$t:${ce}.navigate_back.label`,params:[],build:{handler:"navigateBack"}},navigateForward:{label:`$t:${ce}.navigate_forward.label`,params:[],build:{handler:"navigateForward"}},replaceUrl:{label:`$t:${ce}.replace_url.label`,params:[{key:"path",label:`$t:${ce}.replace_url.param_path`,widget:"page-picker"},{key:"query",label:`$t:${ce}.replace_url.param_query`,widget:"key-value"}],build:{handler:"replaceUrl",params:{path:"{{path}}",query:"{{query}}"}}},toast:{label:`$t:${ce}.toast.label`,params:[{key:"message",label:`$t:${ce}.toast.param_message`,widget:"i18n-text",required:!0},{key:"type",label:`$t:${ce}.toast.param_type`,widget:"select",options:[{value:"info"},{value:"success"},{value:"warning"},{value:"error"}]},{key:"duration",label:`$t:${ce}.toast.param_duration`,widget:"number"},{key:"icon",label:`$t:${ce}.toast.param_icon`,widget:"text"}],build:{handler:"toast",params:{message:"{{message}}",type:"{{type}}",duration:"{{duration}}",icon:"{{icon}}"}}},openModal:{label:`$t:${ce}.open_modal.label`,params:[{key:"target",label:`$t:${ce}.open_modal.param_target`,widget:"modal-picker",required:!0}],build:{handler:"openModal",target:"{{target}}"}},closeModal:{label:`$t:${ce}.close_modal.label`,params:[],build:{handler:"closeModal"}},showAlert:{label:`$t:${ce}.show_alert.label`,params:[{key:"target",label:`$t:${ce}.show_alert.param_message`,widget:"i18n-text",required:!0}],build:{handler:"showAlert",target:"{{target}}"}},setError:{label:`$t:${ce}.set_error.label`,params:[{key:"target",label:`$t:${ce}.set_error.param_message`,widget:"i18n-text"},{key:"stateTarget",label:`$t:${ce}.set_error.param_state_target`,widget:"select",options:[{value:"local"},{value:"global"}]}],build:{handler:"setError",target:"{{target}}",params:{stateTarget:"{{stateTarget}}"}}},refetchDataSource:{label:`$t:${ce}.refetch.label`,params:[{key:"dataSourceId",label:`$t:${ce}.refetch.param_data_source`,widget:"datasource-picker",required:!0},{key:"sync",label:`$t:${ce}.refetch.param_sync`,widget:"toggle"}],build:{handler:"refetchDataSource",params:{dataSourceId:"{{dataSourceId}}",sync:"{{sync}}"}}},appendDataSource:{label:`$t:${ce}.append_data.label`,params:[{key:"dataSourceId",label:`$t:${ce}.append_data.param_data_source`,widget:"datasource-picker",required:!0},{key:"dataPath",label:`$t:${ce}.append_data.param_data_path`,widget:"text"},{key:"newData",label:`$t:${ce}.append_data.param_new_data`,widget:"data-chip"}],build:{handler:"appendDataSource",params:{dataSourceId:"{{dataSourceId}}",dataPath:"{{dataPath}}",newData:"{{newData}}"}}},updateDataSource:{label:`$t:${ce}.update_data.label`,params:[{key:"dataSourceId",label:`$t:${ce}.update_data.param_data_source`,widget:"datasource-picker",required:!0},{key:"data",label:`$t:${ce}.update_data.param_data`,widget:"data-chip"},{key:"merge",label:`$t:${ce}.update_data.param_merge`,widget:"toggle"}],build:{handler:"updateDataSource",params:{dataSourceId:"{{dataSourceId}}",data:"{{data}}",merge:"{{merge}}"}}},scrollIntoView:{label:`$t:${ce}.scroll.label`,params:[{key:"selector",label:`$t:${ce}.scroll.param_selector`,widget:"text",required:!0},{key:"behavior",label:`$t:${ce}.scroll.param_behavior`,widget:"select",options:[{value:"smooth"},{value:"auto"}]},{key:"block",label:`$t:${ce}.scroll.param_block`,widget:"select",options:[{value:"start"},{value:"center"},{value:"end"},{value:"nearest"}]}],build:{handler:"scrollIntoView",params:{selector:"{{selector}}",behavior:"{{behavior}}",block:"{{block}}"}}},login:{label:`$t:${ce}.login.label`,params:[{key:"body",label:`$t:${ce}.login.param_body`,widget:"key-value"}],build:{handler:"login",params:{body:"{{body}}"}}},logout:{label:`$t:${ce}.logout.label`,params:[{key:"target",label:`$t:${ce}.logout.param_target`,widget:"text"}],build:{handler:"logout",target:"{{target}}"}},setLocale:{label:`$t:${ce}.set_locale.label`,params:[{key:"target",label:`$t:${ce}.set_locale.param_target`,widget:"locale-picker",required:!0}],build:{handler:"setLocale",target:"{{target}}"}},emitEvent:{label:`$t:${ce}.emit_event.label`,params:[{key:"event",label:`$t:${ce}.emit_event.param_event`,widget:"text",required:!0},{key:"data",label:`$t:${ce}.emit_event.param_data`,widget:"data-chip"}],build:{handler:"emitEvent",params:{event:"{{event}}",data:"{{data}}"}}},apiCall:{label:`$t:${ce}.api_call.label`,params:[{key:"target",label:`$t:${ce}.api_call.param_target`,widget:"page-picker",required:!0},{key:"method",label:`$t:${ce}.api_call.param_method`,widget:"select",options:[{value:"GET"},{value:"POST"},{value:"PUT"},{value:"PATCH"},{value:"DELETE"}]},{key:"body",label:`$t:${ce}.api_call.param_body`,widget:"key-value"},{key:"query",label:`$t:${ce}.api_call.param_query`,widget:"key-value"},{key:"identity_target_email",label:`$t:${ce}.api_call.param_identity_target_email`,widget:"data-chip"},{key:"identity_target_phone",label:`$t:${ce}.api_call.param_identity_target_phone`,widget:"data-chip"},{key:"onSuccess",label:`$t:${ce}.api_call.param_on_success`,widget:"action-list"},{key:"onError",label:`$t:${ce}.api_call.param_on_error`,widget:"action-list"}],build:{handler:"apiCall",target:"{{target}}",identity_target:{email:"{{identity_target_email}}",phone:"{{identity_target_phone}}"},onSuccess:"{{onSuccess}}",onError:"{{onError}}",params:{method:"{{method}}",body:"{{body}}",query:"{{query}}"}}},conditions:{label:`$t:${ce}.conditions.label`,params:[{key:"branches",label:`$t:${ce}.conditions.param_branches`,widget:"branch-list"}],build:{handler:"conditions",conditions:"{{branches}}"}},sequence:{label:`$t:${ce}.sequence.label`,params:[{key:"actions",label:`$t:${ce}.sequence.param_actions`,widget:"action-list"}],build:{handler:"sequence",params:{actions:"{{actions}}"}}},parallel:{label:`$t:${ce}.parallel.label`,params:[{key:"actions",label:`$t:${ce}.parallel.param_actions`,widget:"action-list"}],build:{handler:"parallel",params:{actions:"{{actions}}"}}},switch:{label:`$t:${ce}.switch.label`,params:[{key:"value",label:`$t:${ce}.switch.param_value`,widget:"data-chip"},{key:"cases",label:`$t:${ce}.switch.param_cases`,widget:"action-list",advanced:!0}],build:{handler:"switch",params:{value:"{{value}}",cases:"{{cases}}"}}},suppress:{label:`$t:${ce}.suppress.label`,params:[],build:{handler:"suppress"}}};function $D(){return{actionRecipes:bn,initActionRecipes:bn,errorRecipes:{navigate:bn.navigate,openModal:bn.openModal,toast:bn.toast,setState:bn.setState,sequence:bn.sequence,parallel:bn.parallel,showErrorPage:{label:`$t:${ce}.show_error_page.label`,params:[{key:"target",label:`$t:${ce}.show_error_page.param_target`,widget:"select",options:[{value:"content"},{value:"full"}]}],build:{handler:"showErrorPage",params:{target:"{{target}}"}}}}}}function Qa(e,t){return typeof e.label=="string"?e.label.startsWith("$t:")?t(e.label.slice(3)):e.label:String(e.value)}function vi(e){return Array.isArray(e.options)?e.options:[]}function ED({control:e,value:t,onChange:n,t:o}){const r=vi(e);return i.jsx("div",{className:"g7le-widget g7le-widget--segmented","data-testid":"g7le-widget-segmented",style:WD,children:r.map(s=>{const a=s.value===t;return i.jsx("button",{type:"button","data-testid":`g7le-segment-${String(s.value)}`,"data-active":a?"true":"false",onClick:()=>n(a?void 0:s.value),style:{...GD,background:a?"#2563eb":"#fff",color:a?"#fff":"#0f172a",borderColor:a?"#2563eb":"#cbd5e1"},children:Qa(s,o)},String(s.value))})})}function ID({control:e,value:t,onChange:n,t:o}){const r=Array.isArray(e.scale)?e.scale:[],s=r.findIndex(l=>l===t),a=t!=null;return i.jsxs("div",{className:"g7le-widget g7le-widget--slider","data-testid":"g7le-widget-slider",style:Dg,children:[i.jsxs("label",{style:Ra,children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-slider-enabled",checked:a,onChange:l=>{l.target.checked?n(r.length>0?r[Math.max(0,s===-1?0:s)]:void 0):n(void 0)}}),i.jsx("span",{style:{fontSize:12,color:"#64748b"},children:o("layout_editor.control.use")})]}),i.jsx("input",{type:"range","data-testid":"g7le-slider-range",min:0,max:Math.max(0,r.length-1),step:1,disabled:!a||r.length===0,value:s===-1?0:s,onChange:l=>{const d=Number(l.target.value);n(r[d])},style:{flex:1}}),i.jsx("span",{"data-testid":"g7le-slider-value",style:{fontSize:12,color:"#0f172a",minWidth:48,textAlign:"right"},children:a?String(t):o("layout_editor.control.default")})]})}function AD({control:e,value:t,onChange:n,t:o}){const r=vi(e);return i.jsxs("select",{className:"g7le-widget g7le-widget--select","data-testid":"g7le-widget-select",value:t==null?"":String(t),onChange:s=>{const a=s.target.value;if(a===""){n(void 0);return}const l=r.find(d=>String(d.value)===a);n(l?l.value:a)},style:YD,children:[i.jsx("option",{value:"",children:o("layout_editor.control.default")}),r.map(s=>i.jsx("option",{value:String(s.value),children:Qa(s,o)},String(s.value)))]})}function TD({control:e,value:t,onChange:n}){const o=vi(e),r=o[1]?.value??!0,s=o[0]?.value??void 0,a=t===r;return i.jsx("button",{type:"button",className:"g7le-widget g7le-widget--toggle","data-testid":"g7le-widget-toggle",role:"switch","aria-checked":a,onClick:()=>n(a?s:r),style:{...XD,background:a?"#2563eb":"#cbd5e1"},children:i.jsx("span",{style:{...JD,transform:a?"translateX(18px)":"translateX(0)"}})})}function DD({control:e,value:t,onChange:n,t:o,freeValueDisabled:r}){const s=vi(e),a=t==null?"":String(t),[l,d]=u.useState(a);u.useEffect(()=>{d(a)},[a]);const c=p=>{const f=p.trim();if(f===""){n(void 0);return}n(f)};return i.jsxs("div",{className:"g7le-widget g7le-widget--dimension","data-testid":"g7le-widget-dimension",style:jD,children:[r?i.jsx("div",{"data-testid":"g7le-dimension-free-disabled",style:UD,children:o("layout_editor.property_modal.dark_preset_only")}):i.jsxs("div",{style:HD,children:[i.jsx("input",{type:"text","data-testid":"g7le-dimension-input",value:l,placeholder:o("layout_editor.control.dimension.placeholder"),onChange:p=>d(p.target.value),onBlur:p=>c(p.target.value),onKeyDown:p=>{p.key==="Enter"&&(p.preventDefault(),c(p.target.value))},style:FD}),i.jsx("button",{type:"button","data-testid":"g7le-dimension-clear",title:o("layout_editor.control.default"),onClick:()=>{d(""),n(void 0)},style:KD,children:o("layout_editor.control.default")})]}),s.length>0&&i.jsx("div",{style:VD,"data-testid":"g7le-dimension-chips",children:s.map(p=>{const f=String(p.value),y=f===a;return i.jsx("button",{type:"button","data-testid":`g7le-dimension-chip-${f}`,"data-active":y?"true":"false",onClick:()=>{d(f),n(f)},style:{...qD,background:y?"#2563eb":"#fff",color:y?"#fff":"#0f172a",borderColor:y?"#2563eb":"#cbd5e1"},children:Qa(p,o)},f)})})]})}function ND(e){return[{key:"t",infix:`${e}t-`,labelKey:"layout_editor.control.spacing.top"},{key:"r",infix:`${e}r-`,labelKey:"layout_editor.control.spacing.right"},{key:"b",infix:`${e}b-`,labelKey:"layout_editor.control.spacing.bottom"},{key:"l",infix:`${e}l-`,labelKey:"layout_editor.control.spacing.left"}]}function zD(e,t,n,o){const r={all:-1,sides:{}};if(typeof e!="string"||e.trim()==="")return r;const s=e.split(/\s+/).filter(a=>a.length>0);for(const a of s){const l=n.find(d=>a.startsWith(d.infix));if(l){r.sides[l.key]=o.indexOf(a.slice(l.infix.length));continue}if(a.startsWith(`${t}x-`)){const d=o.indexOf(a.slice(`${t}x-`.length));r.sides.l=d,r.sides.r=d;continue}if(a.startsWith(`${t}y-`)){const d=o.indexOf(a.slice(`${t}y-`.length));r.sides.t=d,r.sides.b=d;continue}a.startsWith(`${t}-`)&&(r.all=o.indexOf(a.slice(`${t}-`.length)))}return r}function MD({control:e,value:t,onChange:n,t:o}){const r=typeof e.spacingPrefix=="string"?e.spacingPrefix:"p",s=(Array.isArray(e.scale)?e.scale:[]).map(b=>String(b)),a=ND(r),l=zD(t,r,a,s),d=Object.values(l.sides).some(b=>b>=0),c=l.all>=0||d,[p,f]=u.useState(d);u.useEffect(()=>{d&&f(!0)},[d]);const y=Math.min(4,Math.max(0,s.length-1)),g=b=>{const x=[];for(const k of a){const w=b[k.key];w!==void 0&&w>=0&&x.push(`${k.infix}${s[w]}`)}n(x.length>0?x.join(" "):void 0)},m=b=>{n(b>=0?`${r}-${s[Math.max(0,Math.min(b,s.length-1))]}`:void 0)},h=()=>({...l.sides}),v=(b,x,k,w,_)=>{const C=k>=0;return i.jsxs("div",{style:BD,"data-testid":b,children:[i.jsxs("label",{style:{...Ra,minWidth:64},children:[i.jsx("input",{type:"checkbox","data-testid":`${b}-enabled`,checked:C,onChange:E=>_(E.target.checked)}),i.jsx("span",{style:{fontSize:12,color:"#0f172a"},children:o(x)})]}),i.jsx("input",{type:"range","data-testid":`${b}-range`,min:0,max:Math.max(0,s.length-1),step:1,disabled:!C||s.length===0,value:k<0?0:k,onChange:E=>w(Number(E.target.value)),style:{flex:1}}),i.jsx("span",{"data-testid":`${b}-value`,style:{fontSize:12,color:C?"#0f172a":"#94a3b8",minWidth:28,textAlign:"right"},children:C?s[k]:"–"})]},b)};return i.jsxs("div",{className:"g7le-widget g7le-widget--spacing","data-testid":"g7le-widget-spacing",style:LD,children:[i.jsxs("div",{style:PD,children:[i.jsxs("div",{style:OD,"data-testid":"g7le-spacing-mode",children:[i.jsx("button",{type:"button","data-testid":"g7le-spacing-mode-all","data-active":p?"false":"true",onClick:()=>{f(!1),d&&n(void 0)},style:{...Tg,background:p?"#fff":"#2563eb",color:p?"#0f172a":"#fff",borderColor:p?"#cbd5e1":"#2563eb"},children:o("layout_editor.control.spacing.mode_all")}),i.jsx("button",{type:"button","data-testid":"g7le-spacing-mode-sides","data-active":p?"true":"false",onClick:()=>{if(f(!0),l.all>=0){const b={};for(const x of a)b[x.key]=l.all;g(b)}},style:{...Tg,background:p?"#2563eb":"#fff",color:p?"#fff":"#0f172a",borderColor:p?"#2563eb":"#cbd5e1"},children:o("layout_editor.control.spacing.mode_sides")})]}),i.jsx("span",{"data-testid":"g7le-spacing-value",style:{fontSize:11,color:"#64748b",minWidth:80,textAlign:"right"},children:c?String(t):o("layout_editor.control.default")})]}),p?i.jsx("div",{"data-testid":"g7le-spacing-sides",style:{display:"flex",flexDirection:"column",gap:4},children:a.map(b=>v(`g7le-spacing-side-${b.key}`,b.labelKey,l.sides[b.key]??-1,x=>{const k=h();k[b.key]=x,g(k)},x=>{const k=h();x?k[b.key]=l.sides[b.key]>=0?l.sides[b.key]:y:delete k[b.key],g(k)}))}):i.jsxs("div",{style:Dg,children:[i.jsxs("label",{style:{...Ra,minWidth:64},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-spacing-all-enabled",checked:l.all>=0,onChange:b=>m(b.target.checked?y:-1)}),i.jsx("span",{style:{fontSize:12,color:"#64748b"},children:o("layout_editor.control.spacing.all")})]}),i.jsx("input",{type:"range","data-testid":"g7le-spacing-all-range",min:0,max:Math.max(0,s.length-1),step:1,disabled:l.all<0||s.length===0,value:l.all<0?0:l.all,onChange:b=>m(Number(b.target.value)),style:{flex:1}}),i.jsx("span",{"data-testid":"g7le-spacing-all-value",style:{fontSize:12,color:l.all>=0?"#0f172a":"#94a3b8",minWidth:28,textAlign:"right"},children:l.all>=0?s[l.all]:"–"})]})]})}const LD={display:"flex",flexDirection:"column",gap:6,width:"100%"},PD={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},OD={display:"inline-flex",gap:0,border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},Tg={padding:"3px 10px",fontSize:11,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},BD={display:"flex",alignItems:"center",gap:8,width:"100%"},WD={display:"inline-flex",gap:0,border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},jD={display:"flex",flexDirection:"column",gap:6,width:"100%"},HD={display:"flex",gap:6,alignItems:"center"},FD={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,minWidth:80},KD={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer",whiteSpace:"nowrap"},VD={display:"flex",flexWrap:"wrap",gap:4},UD={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"2px 0"},qD={padding:"3px 9px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:999,background:"#fff",cursor:"pointer"},GD={padding:"4px 10px",fontSize:12,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},Dg={display:"flex",alignItems:"center",gap:8,width:"100%"},Ra={display:"inline-flex",alignItems:"center",gap:4},YD={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",minWidth:120},XD={position:"relative",width:38,height:20,borderRadius:10,border:"none",cursor:"pointer",padding:2,display:"inline-flex",alignItems:"center",transition:"background 120ms ease"},JD={display:"block",width:16,height:16,borderRadius:8,background:"#fff",transition:"transform 120ms ease"},ZD=["#0f172a","#334155","#64748b","#94a3b8","#e2e8f0","#ffffff","#dc2626","#ea580c","#d97706","#16a34a","#0891b2","#2563eb","#7c3aed","#db2777"],Ng=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function QD(e){return Array.isArray(e.options)?e.options.filter(t=>typeof t.value=="string").map(t=>({value:t.value,swatch:typeof t.swatch=="string"?t.swatch:void 0,label:typeof t.label=="string"?t.label:void 0})):[]}function RD({control:e,value:t,onChange:n,t:o,freeValueDisabled:r}){const s=QD(e),a=s.length>0,l=typeof t=="string"&&s.some(g=>g.value===t)?t:"",d=typeof t=="string"&&Ng.test(t)?t:"",[c,p]=u.useState(d);u.useEffect(()=>{p(d)},[d]);const f=g=>{const m=g.trim();if(m===""){n(void 0);return}const h=m.startsWith("#")?m:`#${m}`;Ng.test(h)&&n(h)},y=ZD;return i.jsxs("div",{className:"g7le-widget g7le-widget--color","data-testid":"g7le-widget-color",style:eN,children:[r?i.jsx("div",{"data-testid":"g7le-color-free-disabled",style:sN,children:o("layout_editor.property_modal.dark_preset_only")}):i.jsxs("div",{style:tN,children:[i.jsxs("label",{style:nN,children:[i.jsx("span",{"data-testid":"g7le-color-swatch",style:{...oN,background:d||"transparent",backgroundImage:d?void 0:"linear-gradient(45deg,#e2e8f0 25%,transparent 25%,transparent 75%,#e2e8f0 75%),linear-gradient(45deg,#e2e8f0 25%,#fff 25%,#fff 75%,#e2e8f0 75%)",backgroundSize:"8px 8px",backgroundPosition:"0 0, 4px 4px"}}),i.jsx("input",{type:"color","data-testid":"g7le-color-native",value:d||"#000000",onChange:g=>n(g.target.value),style:{width:0,height:0,opacity:0,position:"absolute"}})]}),i.jsx("input",{type:"text","data-testid":"g7le-color-hex",value:c,placeholder:o("layout_editor.control.color_hex_placeholder"),onChange:g=>p(g.target.value),onBlur:g=>f(g.target.value),onKeyDown:g=>{g.key==="Enter"&&f(g.target.value)},style:rN}),i.jsx("button",{type:"button","data-testid":"g7le-color-clear",onClick:()=>n(void 0),style:iN,children:o("layout_editor.control.default")})]}),a?i.jsx("div",{style:zg,"data-testid":"g7le-color-token-presets",children:s.map(g=>{const m=l===g.value,h=g.label?g.label.startsWith("$t:")?o(g.label.slice(3)):g.label:g.value;return i.jsx("button",{type:"button","aria-label":h,title:h,"data-testid":`g7le-color-token-${g.value}`,"data-active":m?"true":"false",onClick:()=>n(m?void 0:g.value),style:{...Mg,background:g.swatch||"#e2e8f0",outline:m?"2px solid #2563eb":"none"}},g.value)})}):!r&&i.jsx("div",{style:zg,"data-testid":"g7le-color-presets",children:y.map(g=>i.jsx("button",{type:"button","aria-label":g,"data-testid":`g7le-color-preset-${g}`,onClick:()=>n(g),style:{...Mg,background:g,outline:d.toLowerCase()===g.toLowerCase()?"2px solid #2563eb":"none"}},g))})]})}const eN={display:"flex",flexDirection:"column",gap:8},tN={display:"flex",alignItems:"center",gap:6},nN={position:"relative",display:"inline-flex",cursor:"pointer"},oN={width:24,height:24,borderRadius:4,border:"1px solid #cbd5e1",display:"inline-block"},rN={flex:1,padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,fontFamily:"monospace"},iN={padding:"4px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",color:"#64748b"},zg={display:"grid",gridTemplateColumns:"repeat(7, 1fr)",gap:4},Mg={width:"100%",height:18,borderRadius:4,border:"1px solid #cbd5e1",cursor:"pointer",padding:0},sN={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"2px 0"},Qo=[{value:"fill",labelKey:"layout_editor.control.background_image.mode_fill",size:"cover",repeat:"no-repeat"},{value:"fit",labelKey:"layout_editor.control.background_image.mode_fit",size:"contain",repeat:"no-repeat"},{value:"tile",labelKey:"layout_editor.control.background_image.mode_tile",size:"auto",repeat:"repeat"}];function aN(e){return e&&typeof e=="object"&&!Array.isArray(e)?e:{}}function Lg(e){return Qo.find(n=>n.size===e.size&&n.repeat===e.repeat)?.value??"fill"}function lN({value:e,onChange:t,t:n}){const o=aN(e),{state:r}=Je(),s=yo(),[a,l]=u.useState(!1),[d,c]=u.useState(null),[p,f]=u.useState(o.url??""),[y,g]=u.useState([]),m=u.useRef(null),h=r.templateIdentifier,v=r.selectedRoute?.layoutName??"";u.useEffect(()=>{f(o.url??"")},[o.url]);const b=u.useCallback(async()=>{if(!h||!v){g([]);return}const E=await Dl(h,v);E.ok&&g(E.data)},[h,v]);u.useEffect(()=>{b()},[b]);const x=E=>{if(!E){t(void 0);return}const S=Qo.find(T=>T.value===Lg(o))??Qo[0];t({url:E,size:o.size??S.size,repeat:o.repeat??S.repeat,position:o.position??"center"})},k=E=>{const S=Qo.find(T=>T.value===E);!S||!o.url||t({url:o.url,size:S.size,repeat:S.repeat,position:o.position??"center"})},w=async E=>{const S=E.target.files?.[0];if(!S)return;l(!0),c(null);const T=await Nl(h,v,S);T.ok?(x(T.data.url),b()):c(n("layout_editor.control.background_image.upload_failed")),l(!1),m.current&&(m.current.value="")},_=async E=>{(await zl(E.id)).ok&&g(T=>T.filter($=>$.id!==E.id))},C=()=>{const E=s.open({ariaLabel:n("layout_editor.attachment_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(Ml,{templateIdentifier:h,layoutName:v,t:n,onSelect:S=>{x(S),s.close(E)},onChanged:()=>{b()},onClose:()=>s.close(E)})})};return i.jsxs("div",{className:"g7le-widget g7le-widget--image","data-testid":"g7le-widget-image",style:dN,children:[o.url&&i.jsx("div",{"data-testid":"g7le-image-preview",style:{...cN,backgroundImage:`url(${o.url})`,backgroundSize:o.size??"cover",backgroundRepeat:o.repeat??"no-repeat",backgroundPosition:o.position??"center"}}),i.jsxs("div",{style:uN,children:[i.jsxs("label",{style:pN,children:[a?"…":n("layout_editor.control.background_image.upload"),i.jsx("input",{ref:m,type:"file",accept:"image/*","data-testid":"g7le-image-file",onChange:w,disabled:a,style:{display:"none"}})]}),i.jsx("button",{type:"button","data-testid":"g7le-image-clear",onClick:()=>t(void 0),style:fN,children:n("layout_editor.control.background_image.clear")})]}),i.jsx("input",{type:"text","data-testid":"g7le-image-url",placeholder:"https://… / URL",value:p,onChange:E=>f(E.target.value),onBlur:E=>x(E.target.value.trim()||void 0),style:gN}),i.jsx("div",{style:hN,"data-testid":"g7le-image-modes",children:Qo.map(E=>{const S=Lg(o)===E.value;return i.jsx("button",{type:"button","data-testid":`g7le-image-mode-${E.value}`,"data-active":S?"true":"false",disabled:!o.url,onClick:()=>k(E.value),style:{...yN,background:S&&o.url?"#2563eb":"#fff",color:S&&o.url?"#fff":"#0f172a"},children:n(E.labelKey)},E.value)})}),i.jsxs("div",{style:bN,"data-testid":"g7le-image-gallery",children:[i.jsxs("div",{style:mN,children:[i.jsx("span",{style:{fontSize:11,color:"#64748b"},children:n("layout_editor.attachment_manager.recent")}),i.jsxs("button",{type:"button","data-testid":"g7le-image-manage",onClick:C,style:vN,children:["🖼 ",n("layout_editor.attachment_manager.manage_link")]})]}),y.length>0?i.jsx("div",{style:_N,"data-testid":"g7le-image-thumbs",children:y.slice(0,8).map(E=>i.jsxs("div",{style:xN,"data-testid":`g7le-image-thumb-${E.id}`,children:[i.jsx("button",{type:"button",title:E.original_name,onClick:()=>x(E.url),style:{...wN,backgroundImage:`url(${E.url})`},"data-testid":`g7le-image-thumb-use-${E.id}`}),i.jsx("button",{type:"button",title:n("layout_editor.attachment_manager.delete"),onClick:()=>_(E),style:kN,"data-testid":`g7le-image-thumb-del-${E.id}`,children:"✕"})]},E.id))}):i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:n("layout_editor.attachment_manager.empty")})]}),d&&i.jsx("span",{"data-testid":"g7le-image-error",style:{fontSize:11,color:"#dc2626"},children:d})]})}const dN={display:"flex",flexDirection:"column",gap:6},cN={width:"100%",height:80,borderRadius:6,border:"1px solid #cbd5e1"},uN={display:"flex",gap:6},pN={padding:"5px 10px",fontSize:12,border:"1px solid #2563eb",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",display:"inline-flex",alignItems:"center"},fN={padding:"5px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#64748b",cursor:"pointer"},gN={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},hN={display:"inline-flex",border:"1px solid #cbd5e1",borderRadius:6,overflow:"hidden"},yN={padding:"4px 10px",fontSize:12,border:"none",borderRight:"1px solid #e2e8f0",cursor:"pointer"},bN={display:"flex",flexDirection:"column",gap:4,border:"1px solid #e2e8f0",borderRadius:6,padding:6,marginTop:2},mN={display:"flex",alignItems:"center",justifyContent:"space-between"},vN={fontSize:11,color:"#2563eb",background:"transparent",border:"none",cursor:"pointer",padding:0},_N={display:"flex",gap:6,flexWrap:"wrap"},xN={position:"relative",width:44,height:44},wN={width:44,height:44,borderRadius:6,border:"1px solid #cbd5e1",backgroundSize:"cover",backgroundPosition:"center",cursor:"pointer",padding:0},kN={position:"absolute",top:-6,right:-6,width:16,height:16,borderRadius:"50%",border:"none",background:"#dc2626",color:"#fff",fontSize:9,lineHeight:"16px",cursor:"pointer",padding:0};function SN(e){return Array.isArray(e)&&e.every(t=>t!==null&&typeof t=="object"&&!Array.isArray(t))}function CN(e){return typeof e=="string"&&/\{\{.*\}\}/.test(e)}function $N({value:e,onChange:t,t:n,bindingCandidates:o}){if(CN(e))return i.jsx("div",{className:"g7le-widget g7le-widget--options-list","data-testid":"g7le-widget-options-list-bound",style:MN,children:n("layout_editor.list_editor.bound_degraded")});const r=SN(e)?e:[],s=y=>{t(y.length===0?void 0:y)},a=(y,g)=>{const m=r.map((h,v)=>v===y?{...h,...g}:h);s(m)},l=y=>{s(r.filter((g,m)=>m!==y))},d=(y,g)=>{const m=y+g;if(m<0||m>=r.length)return;const h=[...r];[h[y],h[m]]=[h[m],h[y]],s(h)},c=()=>{s([...r,{value:"",label:""}])},p=y=>typeof y.label=="string"?y.label:y.label==null?"":String(y.label),f=y=>y.value===void 0||y.value===null?"":String(y.value);return i.jsxs("div",{className:"g7le-widget g7le-widget--options-list","data-testid":"g7le-widget-options-list",style:EN,children:[r.length===0&&i.jsx("div",{"data-testid":"g7le-options-empty",style:zN,children:n("layout_editor.list_editor.empty")}),r.map((y,g)=>i.jsxs("div",{"data-testid":`g7le-options-row-${g}`,style:IN,children:[i.jsxs("div",{style:Pg,children:[i.jsx("span",{style:Og,children:n("layout_editor.list_editor.option_value")}),i.jsx("input",{type:"text","data-testid":`g7le-options-value-${g}`,value:f(y),placeholder:n("layout_editor.list_editor.option_value"),onChange:m=>a(g,{value:m.target.value}),style:TN})]}),i.jsxs("div",{style:Pg,children:[i.jsx("span",{style:Og,children:n("layout_editor.list_editor.option_label")}),i.jsx("div",{"data-testid":`g7le-options-label-${g}`,style:{flex:1,minWidth:0},children:i.jsx(vt,{value:p(y),onChange:m=>a(g,{label:m??""}),t:n,placeholder:n("layout_editor.list_editor.option_label"),testidPrefix:`g7le-options-label-i18n-${g}`,candidates:o,enableExpressionTree:!0,expressionTreeCollapsible:!0})})]}),i.jsxs("div",{style:AN,children:[i.jsx("button",{type:"button","data-testid":`g7le-options-up-${g}`,title:n("layout_editor.list_editor.move_up"),disabled:g===0,onClick:()=>d(g,-1),style:Bg,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-options-down-${g}`,title:n("layout_editor.list_editor.move_down"),disabled:g===r.length-1,onClick:()=>d(g,1),style:Bg,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-options-remove-${g}`,title:n("layout_editor.list_editor.remove"),onClick:()=>l(g),style:DN,children:"✕"})]})]},g)),i.jsx("button",{type:"button","data-testid":"g7le-options-add",onClick:c,style:NN,children:n("layout_editor.list_editor.add")})]})}const EN={display:"flex",flexDirection:"column",gap:8,width:"100%"},IN={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0,paddingBottom:6,borderBottom:"1px solid #f1f5f9"},Pg={display:"flex",gap:6,alignItems:"center",width:"100%",minWidth:0},Og={flexShrink:0,width:36,fontSize:11,color:"#94a3b8",textAlign:"right"},AN={display:"flex",gap:4,alignItems:"center",justifyContent:"flex-end"},TN={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Bg={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},DN={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},NN={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},zN={fontSize:11,color:"#94a3b8",fontStyle:"italic"},MN={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"},Wg=/[^A-Za-z0-9\-_:.]/g;function LN(e){return typeof e=="string"&&Ui(e)}function jg(e){return Ui(e)?In(e).map(t=>t.kind==="binding"?t.raw:t.raw.replace(Wg,"")).join(""):e.replace(Wg,"")}const PN={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4,fontFamily:"monospace"},ON={marginTop:4,fontSize:11,color:"#94a3b8"},BN={display:"flex",gap:6,alignItems:"center"},WN={flexShrink:0,padding:"5px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:4,background:"#f8fafc",cursor:"pointer",whiteSpace:"nowrap"};function jN({value:e,onChange:t,t:n,bindingCandidates:o}){const r=e==null?"":String(e),s=LN(e),a=!!o&&o.length>0,[l,d]=u.useState(!1),c=p=>{const f=jg(p);t(f===""?void 0:f)};return s||l?i.jsxs("div",{"data-testid":"g7le-core-id-chip",children:[i.jsx(bs,{value:r,onChange:c,t:n,candidates:o,onDone:()=>d(!1),testidPrefix:"g7le-core-id-chip-input"}),i.jsx("div",{style:ON,children:n("layout_editor.core_props.id.binding_hint")})]}):i.jsx("div",{children:i.jsxs("div",{style:BN,children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-id","data-testid":"g7le-widget-core-id",value:r,onChange:p=>{const f=jg(p.target.value);t(f===""?void 0:f)},spellCheck:!1,autoComplete:"off",style:PN}),a&&i.jsxs("button",{type:"button","data-testid":"g7le-core-id-add-data",onClick:()=>d(!0),style:WN,title:n("layout_editor.core_props.id.add_data"),children:["🔗 ",n("layout_editor.core_props.id.add_data")]})]})})}const HN=/[^A-Za-z0-9.\-_]/g;function FN(e){return typeof e=="string"&&/\{\{[\s\S]*?\}\}/.test(e)}function KN(e){return e.replace(HN,"")}const Hg={width:"100%",padding:"6px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:4,fontFamily:"monospace",boxSizing:"border-box"},VN={...Hg,background:"#f1f5f9",color:"#64748b",cursor:"not-allowed"},Fg={marginTop:4,fontSize:11,color:"#94a3b8"};function UN({value:e,onChange:t,t:n}){const o=e==null?"":String(e);return FN(e)?i.jsxs("div",{"data-testid":"g7le-core-datakey-bound",children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-datakey-bound","data-testid":"g7le-widget-core-datakey",value:o,readOnly:!0,disabled:!0,style:VN}),i.jsx("div",{style:Fg,children:n("layout_editor.core_props.dataKey.bound_degraded")})]}):i.jsxs("div",{children:[i.jsx("input",{type:"text",className:"g7le-widget g7le-widget--core-datakey","data-testid":"g7le-widget-core-datakey",value:o,placeholder:n("layout_editor.core_props.dataKey.placeholder"),onChange:s=>{const a=KN(s.target.value);t(a===""?void 0:a)},spellCheck:!1,autoComplete:"off",style:Hg}),i.jsx("div",{style:Fg,children:n("layout_editor.core_props.dataKey.hint")})]})}let Kg=!1;function qN(){Kg||(Xe("segmented",ED),Xe("slider",ID),Xe("select",AD),Xe("dimension",DD),Xe("spacing",MD),Xe("toggle",TD),Xe("color",RD),Xe("image",lN),Xe("tag-input",ys),Xe("page-picker",Aw),Xe("datasource-picker",Bc),Xe("binding-picker",Bc),Xe("binding-list",Nw),Xe("state-key-picker",Es),Xe("modal-picker",Tw),Xe("i18n-text",Dw),Xe("text",zw),Xe("options-list",$N),Xe("core-id",jN),Xe("core-datakey",UN),Xe("component-target-picker",jT),Kg=!0)}function Ro(e){return Array.isArray(e.children)?e.children:[]}function er(e){if(typeof e.text=="string")return[];let t=null;const n=(o,r)=>{const s=Ro(o);for(let a=0;a<s.length;a++){const l=s[a],d=[...r,a];if(typeof l.text=="string")if(l.text.trim().length<=1)t===null&&(t=d);else return d;const c=n(l,d);if(c)return c}return null};return n(e,[])??t}function _i(e,t){let n=e;for(const o of t){if(!n)return null;n=Ro(n)[o]??null}return n}function tr(e,t,n){if(t.length===0)return{...e,text:n};const[o,...r]=t,a=Ro(e).map((l,d)=>d===o?tr(l,r,n):l);return{...e,children:a}}function Vg(e,t){const n=r=>typeof r.props?.[t]=="string";if(n(e))return[];const o=(r,s)=>{const a=Ro(r);for(let l=0;l<a.length;l++){const d=a[l],c=[...s,l];if(n(d))return c;const p=o(d,c);if(p)return p}return null};return o(e,[])}function Ug(e,t,n,o){if(t.length===0)return{...e,props:{...e.props??{},[n]:o}};const[r,...s]=t,l=Ro(e).map((d,c)=>c===r?Ug(d,s,n,o):d);return{...e,children:l}}function GN(e){return Array.isArray(e.children)?e.children:[]}function YN(e){const t=e?.itemFields;if(!Array.isArray(t))return[{kind:"text"}];const n=[];for(const o of t){if(!o||typeof o!="object")continue;const r=o;r.kind==="text"?n.push({kind:"text",label:typeof r.label=="string"?r.label:void 0}):r.kind==="prop"&&typeof r.prop=="string"&&r.prop.length>0&&n.push({kind:"prop",prop:r.prop,label:typeof r.label=="string"?r.label:void 0})}return n.length>0?n:[{kind:"text"}]}function el(e,t){if(!(typeof e!="string"||e.length===0))return e.startsWith("$t:")?t(e.slice(3)):e}function qg({node:e,params:t,spec:n,manifest:o,t:r,onPatchNode:s,templateIdentifier:a,candidates:l}){const d=typeof t?.childComponent=="string"?t.childComponent:null,c=t?.childTemplate&&typeof t.childTemplate=="object"?t.childTemplate:null,p=YN(t),f=el(typeof t?.childLabel=="string"?t.childLabel:void 0,r)??d??"",y=GN(e),g=u.useCallback(_=>{s({...e,children:_})},[e,s]),m=u.useCallback(()=>{if(c)return JSON.parse(JSON.stringify(c));if(!d)return null;const _=n?.componentPalette?.entries?.[d];if(_?.defaultNode&&typeof _.defaultNode=="object")return JSON.parse(JSON.stringify(_.defaultNode));const C=o?.components??{};let E=null;for(const S of Object.values(C))if(Array.isArray(S)){const T=S.find($=>$.name===d);if(T){E=T;break}}return E?$f(E):{type:"basic",name:d}},[c,d,n,o]),h=u.useCallback(()=>{const _=m();if(_){if(_.text==="")_.text=r("layout_editor.list_editor.new_item");else{const C=er(_);if(C!==null&&C.length>0){const E=_i(_,C);if(E&&E.text===""){const S=tr(_,C,r("layout_editor.list_editor.new_item"));g([...y,S]);return}}}g([...y,_])}},[m,g,y,r]),v=u.useCallback(_=>{g(y.filter((C,E)=>E!==_))},[g,y]),b=u.useCallback((_,C)=>{const E=_+C;if(E<0||E>=y.length)return;const S=[...y];[S[_],S[E]]=[S[E],S[_]],g(S)},[g,y]),x=u.useCallback((_,C)=>{if(typeof C!="string"||C.length===0)return;const E=y[_];if(!E)return;const S=er(E);if(S===null)return;const T=y.map(($,A)=>A===_?tr($,S,C):$);g(T)},[y,g]),k=u.useCallback((_,C,E)=>{if(typeof E!="string"||E.length===0)return;const S=y[_];if(!S)return;const T=Vg(S,C);if(T===null)return;const $=y.map((A,z)=>z===_?Ug(A,T,C,E):A);g($)},[y,g]);if(!d&&!c)return i.jsx("div",{className:"g7le-node-editor g7le-node-editor--children","data-testid":"g7le-children-editor-misconfigured",style:Xg,children:r("layout_editor.list_editor.no_child_component")});const w=p.length>1||p.some(_=>_.label);return i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--children","data-testid":"g7le-children-editor",style:XN,children:[i.jsx("div",{style:JN,children:r("layout_editor.list_editor.items_title")}),y.length===0&&i.jsx("div",{"data-testid":"g7le-children-empty",style:Xg,children:r("layout_editor.list_editor.empty")}),y.map((_,C)=>{const E=[];for(const T of p)if(T.kind==="text"){const $=er(_),A=$!==null?_i(_,$):null;A&&typeof A.text=="string"&&E.push({key:"text",caption:el(T.label,r)??(w?r("layout_editor.list_editor.item_label"):void 0),value:A.text,onChange:z=>x(C,z),testid:`g7le-children-i18n-${C}`})}else if(T.kind==="prop"&&T.prop){const $=Vg(_,T.prop);if($!==null){const z=_i(_,$)?.props?.[T.prop];E.push({key:`prop-${T.prop}`,caption:el(T.label,r)??T.prop,value:typeof z=="string"?z:"",onChange:D=>k(C,T.prop,D),testid:`g7le-children-prop-${T.prop}-${C}`})}}const S=typeof _?.name=="string"?_.name:r("layout_editor.list_editor.item");return i.jsxs("div",{"data-testid":`g7le-children-row-${C}`,style:ZN,children:[E.length>0?E.map(T=>i.jsx("div",{style:Gg,children:i.jsxs("div",{"data-testid":T.key==="text"?`g7le-children-text-${C}`:`g7le-children-field-${T.key}-${C}`,style:QN,children:[T.caption!==void 0&&i.jsx("div",{"data-testid":`g7le-children-caption-${T.key}-${C}`,style:RN,children:T.caption}),i.jsx(vt,{value:T.value,onChange:T.onChange,t:r,placeholder:r("layout_editor.list_editor.item_text"),testidPrefix:T.testid,candidates:l,enableExpressionTree:!0,expressionTreeCollapsible:!0})]})},T.key)):i.jsx("div",{style:Gg,children:i.jsx("span",{"data-testid":`g7le-children-label-${C}`,style:t8,children:S})}),i.jsxs("div",{style:e8,children:[i.jsx("button",{type:"button","data-testid":`g7le-children-up-${C}`,title:r("layout_editor.list_editor.move_up"),disabled:C===0,onClick:()=>b(C,-1),style:Yg,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-children-down-${C}`,title:r("layout_editor.list_editor.move_down"),disabled:C===y.length-1,onClick:()=>b(C,1),style:Yg,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-children-remove-${C}`,title:r("layout_editor.list_editor.remove"),onClick:()=>v(C),style:n8,children:"✕"})]})]},C)}),i.jsx("button",{type:"button","data-testid":"g7le-children-add",onClick:h,style:o8,children:r("layout_editor.list_editor.add_child",{component:f})})]})}const XN={display:"flex",flexDirection:"column",gap:6,width:"100%",marginBottom:12},JN={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},ZN={display:"flex",flexDirection:"column",gap:4,width:"100%",minWidth:0,paddingBottom:6,borderBottom:"1px solid #f1f5f9"},Gg={display:"flex",gap:4,alignItems:"flex-start",width:"100%",minWidth:0},QN={flex:1,minWidth:0},RN={fontSize:11,color:"#64748b",marginBottom:2},e8={display:"flex",gap:4,alignItems:"center",justifyContent:"flex-end"},t8={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,color:"#475569",fontStyle:"italic",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},Yg={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},n8={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},o8={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Xg={fontSize:11,color:"#94a3b8",fontStyle:"italic"},fo={rowContainer:"Tbody",row:"Tr",cell:"Td",headerCell:"Th",colSpanProp:"colSpan",rowSpanProp:"rowSpan"};function ht(e){return{rowContainer:e?.rowContainer??fo.rowContainer,row:e?.row??fo.row,cell:e?.cell??fo.cell,headerCell:e?.headerCell??fo.headerCell,colSpanProp:e?.colSpanProp??fo.colSpanProp,rowSpanProp:e?.rowSpanProp??fo.rowSpanProp}}function Bn(e){return e?Array.isArray(e.children)?e.children:[]:[]}function Jg(e,t){const n=e.props?.[t],o=typeof n=="number"?n:typeof n=="string"?parseInt(n,10):NaN;return Number.isFinite(o)&&o>=1?Math.floor(o):1}function Be(e,t){const n=ht(t),o=[],r=Bn(e),s=[];r.forEach((f,y)=>{(Bn(f).some(h=>h.name===n.row)||f.name===n.rowContainer||f.name==="Thead"||f.name==="Tfoot")&&s.push({container:f,containerIndex:y,isHeader:f.name==="Thead"})}),s.length===0&&r.some(f=>f.name===n.row)&&s.push({container:e,containerIndex:-1,isHeader:!1});const a=[],l=[];let d=0,c=0;const p=f=>{for(;a.length<=f;)a.push([]);return a[f]};s.forEach((f,y)=>{Bn(f.container).filter(m=>m.name===n.row).forEach((m,h)=>{const v=[],b=p(d);let x=0;Bn(m).filter(w=>w.name===n.cell||w.name===n.headerCell).forEach((w,_)=>{for(;b[x];)x++;const C=Jg(w,n.colSpanProp),E=Jg(w,n.rowSpanProp),S={cell:w,sectionIndex:y,rowIndexInSection:h,cellIndexInRow:_,gridRow:d,gridCol:x,colSpan:C,rowSpan:E,isHeader:w.name===n.headerCell};v.push(S);for(let T=0;T<E;T++){const $=p(d+T);for(let A=0;A<C;A++)$[x+A]&&o.push(`overlap at row ${d+T}, col ${x+A}`),$[x+A]=!0}x+=C}),c=Math.max(c,x),l.push({cells:v,sectionIndex:y,rowIndexInSection:h,rowNode:m}),d++})});for(let f=0;f<d;f++){const y=a[f]??[];for(let g=0;g<c;g++)y[g]||o.push(`hole at row ${f}, col ${g}`)}return{sections:s,rows:l,colCount:c,valid:o.length===0,issues:o}}function tl(e,t,n){const o=ht(n),s=Be(e,n).sections[t.sectionIndex],a=[];let l;s&&s.containerIndex>=0?(a.push(s.containerIndex),l=Bn(e)[s.containerIndex]??e):l=e;let d=-1,c=-1,p=null;const f=Bn(l);for(let m=0;m<f.length;m++)if(f[m].name===o.row&&(d++,d===t.rowIndexInSection)){c=m,p=f[m];break}if(c<0||!p)return a;a.push(c);let y=-1;const g=Bn(p);for(let m=0;m<g.length;m++){const h=g[m].name;if((h===o.cell||h===o.headerCell)&&(y++,y===t.cellIndexInRow))return a.push(m),a}return a}function Ue(e,t,n){for(const o of e.rows)for(const r of o.cells)if(t>=r.gridRow&&t<r.gridRow+r.rowSpan&&n>=r.gridCol&&n<r.gridCol+r.colSpan)return r;return null}function Zg(e){if(e.colCount===0)return[];const t=new Set([0,e.colCount]);for(let r=1;r<e.colCount;r++){let s=!1;for(const a of e.rows){for(const l of a.cells)if(l.gridCol<r&&l.gridCol+l.colSpan>r){s=!0;break}if(s)break}s||t.add(r)}const n=Array.from(t).sort((r,s)=>r-s),o=[];for(let r=0;r<n.length-1;r++)o.push({start:n[r],end:n[r+1]});return o}function Qg(e){const t=e.rows.length;if(t===0)return[];const n=a=>e.rows.find(l=>(l.cells[0]?.gridRow??-1)===a)?.sectionIndex??e.rows[a]?.sectionIndex??0,o=new Set([0,t]);for(let a=1;a<t;a++){if(n(a)!==n(a-1)){o.add(a);continue}let l=!1;for(const d of e.rows){for(const c of d.cells)if(c.gridRow<a&&c.gridRow+c.rowSpan>a){l=!0;break}if(l)break}l||o.add(a)}const r=Array.from(o).sort((a,l)=>a-l),s=[];for(let a=0;a<r.length-1;a++)s.push({start:r[a],end:r[a+1],sectionIndex:n(r[a])});return s}function xi(e,t,n){const o=Qg(e),r=o.findIndex(a=>t>=a.start&&t<a.end);if(r<0)return{ok:!1,reason:"boundary"};const s=r+n;return s<0||s>=o.length?{ok:!1,reason:"boundary"}:o[r].sectionIndex!==o[s].sectionIndex?{ok:!1,reason:"section"}:{ok:!0}}function wi(e,t,n){const o=Zg(e),r=o.findIndex(a=>t>=a.start&&t<a.end);if(r<0)return{ok:!1,reason:"boundary"};const s=r+n;return s<0||s>=o.length?{ok:!1,reason:"boundary"}:{ok:!0}}function Ge(e){return e?Array.isArray(e.children)?e.children:[]:[]}function mn(e,t,n){const o={...e.props??{}};n<=1?delete o[t]:o[t]=n;const r={...e,props:o};return Object.keys(o).length===0&&delete r.props,r}function ki(e,t,n=!1){const o={type:"basic",name:n?e.headerCell:e.cell},r=t?.props?.className;return typeof r=="string"&&r&&(o.props={className:r}),o.text="",o}function nr(e,t){return e.name===t.cell||e.name===t.headerCell}function nt(e,t,n,o){const r=t.sections[n];if(!r)return e;if(r.containerIndex===-1)return o(e);const s=Ge(e).map((a,l)=>l===r.containerIndex?o(a):a);return{...e,children:s}}function yt(e,t,n,o){let r=-1;const s=Ge(e).map(a=>a.name===t&&(r++,r===n)?o(a):a);return{...e,children:s}}function Si(e,t,n){let o=-1;const r=Ge(e);for(let s=0;s<r.length;s++)if(nr(r[s],t)&&(o++,o===n))return s;return-1}function nl(e,t,n){const o=ht(t),r=Be(e,t);if(r.rows.length===0||r.colCount===0){const b={type:"basic",name:o.row,children:[ki(o)]};return r.sections.length>0?nt(e,r,r.sections.length-1,x=>({...x,children:[...Ge(x),b]})):{...e,children:[...Ge(e),b]}}const s=Math.max(-1,Math.min(n,r.rows.length-1)),a=s<0?r.rows[0]:r.rows[s],l=a.cells[0]?.gridRow??s,d=s<0?0:l+1,c=a.sectionIndex,p=s<0?0:a.rowIndexInSection+1,f=[],y=[];let g=0;for(;g<r.colCount;){const b=d>0?Ue(r,d-1,g):null;b&&b.gridRow+b.rowSpan>d&&b.rowSpan>1?(y.push(b),g+=b.colSpan):(f.push(ki(o)),g+=1)}let m=e;for(const b of y)m=nt(m,r,b.sectionIndex,x=>yt(x,o.row,b.rowIndexInSection,k=>({...k,children:Ge(k).map((w,_)=>_===b.cellIndexInRow?mn(w,o.rowSpanProp,b.rowSpan+1):w)})));const h={type:"basic",name:o.row,children:f},v=Be(m,t);return m=nt(m,v,c,b=>{const x=Ge(b);let k=0,w=x.length;for(let _=0;_<x.length;_++)if(x[_].name===o.row){if(k===p){w=_;break}k++}return{...b,children:[...x.slice(0,w),h,...x.slice(w)]}}),m}function Rg(e,t,n){const o=ht(t),r=Be(e,t);if(n<0||n>=r.rows.length||r.rows.length<=1)return e;const s=r.rows[n],a=s.cells[0]?.gridRow??n;let l=e;const d=new Set;for(let p=0;p<r.colCount;){const f=Ue(r,a,p);f&&f.gridRow<a&&!d.has(f.cell)&&(d.add(f.cell),l=nt(l,r,f.sectionIndex,y=>yt(y,o.row,f.rowIndexInSection,g=>({...g,children:Ge(g).map((m,h)=>h===f.cellIndexInRow?mn(m,o.rowSpanProp,f.rowSpan-1):m)})))),p+=f?f.colSpan:1}const c=Be(l,t);return l=nt(l,c,s.sectionIndex,p=>{let f=-1;const y=Ge(p).filter(g=>g.name===o.row?(f++,f!==s.rowIndexInSection):!0);return{...p,children:y}}),l}function eh(e,t,n,o){const r=ht(t),s=Be(e,t);if(!xi(s,n,o).ok)return e;const a=Qg(s),l=a.findIndex(h=>n>=h.start&&n<h.end),d=l+o,c=a[l],p=a[d],f=c.sectionIndex,y=h=>s.rows.filter(v=>{const b=v.cells[0]?.gridRow??-1;return v.sectionIndex===f&&b>=h.start&&b<h.end}).map(v=>v.rowIndexInSection),g=y(c),m=y(p);return g.length===0||m.length===0?e:nt(e,s,f,h=>{const v=[...Ge(h)],b=[];v.forEach((M,B)=>{M.name===r.row&&b.push(B)});const x=g.map(M=>b[M]).filter(M=>M!==void 0),k=m.map(M=>b[M]).filter(M=>M!==void 0);if(x.length===0||k.length===0)return h;const w=x.map(M=>v[M]),_=k.map(M=>v[M]),C=Math.min(...x)<Math.min(...k),E=C?x:k,S=C?w:_,T=C?k:x,$=C?_:w,A=Math.min(...E),z=Math.max(...T),D=[...$,...S],O=[...v.slice(0,A),...D,...v.slice(z+1)];return{...h,children:O}})}function th(e,t,n,o){const r=ht(t),s=Be(e,t);if(!wi(s,n,o).ok)return e;const a=Zg(s),l=a.findIndex(m=>n>=m.start&&n<m.end),d=l+o,c=a[l],p=a[d],f=c.start<p.start?c:p,y=c.start<p.start?p:c;let g=e;for(const m of s.rows){const h=m.cells.filter(w=>w.gridCol>=f.start&&w.gridCol<f.end),v=m.cells.filter(w=>w.gridCol>=y.start&&w.gridCol<y.end);if(h.length===0||v.length===0)continue;const b=h.map(w=>w.cellIndexInRow),x=v.map(w=>w.cellIndexInRow),k=Be(g,t);g=nt(g,k,m.sectionIndex,w=>yt(w,r.row,m.rowIndexInSection,_=>{const C=[...Ge(_)],E=b.map(M=>Si(_,r,M)).filter(M=>M>=0),S=x.map(M=>Si(_,r,M)).filter(M=>M>=0);if(E.length===0||S.length===0)return _;const T=E.map(M=>C[M]),$=S.map(M=>C[M]),A=Math.min(...E,...S),z=Math.max(...E,...S),D=[...$,...T],O=[...C.slice(0,A),...D,...C.slice(z+1)];return{..._,children:O}}))}return g}function nh(e,t,n){const o=ht(t),r=Be(e,t);if(r.rows.length===0||r.colCount===0)return nl(e,t,-1);const s=Math.max(0,Math.min(n+1,r.colCount));let a=e;const l=new Set;for(const d of r.rows){const c=d.cells[0]?.gridRow??0,p=s>0?Ue(r,c,s-1):null,f=s<r.colCount?Ue(r,c,s):null;if(p&&f&&p.cell===f.cell&&p.colSpan>1){if(!l.has(p.cell)){l.add(p.cell);const h=Be(a,t);a=nt(a,h,p.sectionIndex,v=>yt(v,o.row,p.rowIndexInSection,b=>({...b,children:Ge(b).map((x,k)=>k===p.cellIndexInRow?mn(x,o.colSpanProp,p.colSpan+1):x)})))}continue}if(p&&p.gridRow<c||f&&f.gridRow<c)continue;const y=p&&p.gridRow===c?p.cellIndexInRow:-1,g=d.cells.length>0&&d.cells.every(h=>h.isHeader),m=Be(a,t);a=nt(a,m,d.sectionIndex,h=>yt(h,o.row,d.rowIndexInSection,v=>{const b=[...Ge(v)];let x;if(y<0)x=b.findIndex(k=>nr(k,o)),x<0&&(x=b.length);else{const k=Si(v,o,y);x=k<0?b.length:k+1}return b.splice(x,0,ki(o,void 0,g)),{...v,children:b}}))}return a}function oh(e,t,n){const o=ht(t),r=Be(e,t);if(n<0||n>=r.colCount||r.colCount<=1)return e;let s=e;const a=new Set;for(const l of r.rows){const d=l.cells[0]?.gridRow??0,c=Ue(r,d,n);if(!(!c||a.has(c.cell)||c.gridRow<d))if(a.add(c.cell),c.colSpan>1){const p=Be(s,t);s=nt(s,p,c.sectionIndex,f=>yt(f,o.row,c.rowIndexInSection,y=>({...y,children:Ge(y).map((g,m)=>m===c.cellIndexInRow?mn(g,o.colSpanProp,c.colSpan-1):g)})))}else{const p=Be(s,t);s=nt(s,p,c.sectionIndex,f=>yt(f,o.row,c.rowIndexInSection,y=>{let g=-1;const m=Ge(y).filter(h=>nr(h,o)?(g++,g!==c.cellIndexInRow):!0);return{...y,children:m}}))}}return s}function ol(e,t,n,o,r,s){const a=ht(t),l=Be(e,t),d=Math.min(n,r),c=Math.max(n,r),p=Math.min(o,s),f=Math.max(o,s);if(d===c&&p===f)return{table:e,ok:!1,reason:"single"};if(!l.valid)return{table:e,ok:!1,reason:"invalid-grid"};const y=new Set;for(let b=d;b<=c;b++)for(let x=p;x<=f;x++){const k=Ue(l,b,x);if(!k)return{table:e,ok:!1,reason:"hole"};if(k.gridRow<d||k.gridCol<p||k.gridRow+k.rowSpan-1>c||k.gridCol+k.colSpan-1>f)return{table:e,ok:!1,reason:"not-rectangular"};y.add(k)}const g=Ue(l,d,p),m=Array.from(y).filter(b=>b!==g);let h=nt(e,l,g.sectionIndex,b=>yt(b,a.row,g.rowIndexInSection,x=>({...x,children:Ge(x).map((k,w)=>{if(w!==g.cellIndexInRow)return k;let _=mn(k,a.colSpanProp,f-p+1);return _=mn(_,a.rowSpanProp,c-d+1),_})})));const v=new Map;for(const b of m){const x=`${b.sectionIndex}:${b.rowIndexInSection}`,k=v.get(x)??[];k.push(b),v.set(x,k)}for(const[b,x]of v){const k=parseInt(b.split(":")[0],10),w=x[0].rowIndexInSection,_=new Set(x.map(E=>E.cellIndexInRow)),C=Be(h,t);h=nt(h,C,k,E=>yt(E,a.row,w,S=>{let T=-1;const $=Ge(S).filter(A=>nr(A,a)?(T++,!_.has(T)):!0);return{...S,children:$}}))}return{table:h,ok:!0}}function rh(e,t,n,o){const r=ht(t),s=Be(e,t),a=Ue(s,n,o);if(!a||a.colSpan<=1&&a.rowSpan<=1)return e;const l=a.gridRow,d=a.gridCol,c=a.gridRow+a.rowSpan-1,p=a.gridCol+a.colSpan-1,f=a.isHeader,y=typeof a.cell.props?.className=="string"?a.cell.props.className:void 0;let g=nt(e,s,a.sectionIndex,m=>yt(m,r.row,a.rowIndexInSection,h=>({...h,children:Ge(h).map((v,b)=>{if(b!==a.cellIndexInRow)return v;let x=mn(v,r.colSpanProp,1);return x=mn(x,r.rowSpanProp,1),x})})));for(let m=l;m<=c;m++)for(let h=d;h<=p;h++){if(m===l&&h===d)continue;const v=Be(g,t),b=v.rows.find(w=>w.cells.some(_=>_.gridRow===m))??v.rows[m];if(!b)continue;let x=-1;for(const w of b.cells)w.gridRow===m&&w.gridCol<h&&(x=Math.max(x,w.cellIndexInRow));const k=y?{props:{className:y}}:null;g=nt(g,v,b.sectionIndex,w=>yt(w,r.row,b.rowIndexInSection,_=>{const C=[...Ge(_)];let E;if(x<0)E=C.findIndex(S=>nr(S,r)),E<0&&(E=C.length);else{const S=Si(_,r,x);E=S<0?C.length:S+1}return C.splice(E,0,ki(r,k,f)),{..._,children:C}}))}return g}function Ci(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>{if(f!==l.cellIndexInRow)return p;const y={...p.props??{}};r.trim()===""?delete y.className:y.className=r;const g={...p,props:y};return Object.keys(y).length===0&&delete g.props,g})}))):e}function Jt(e,t,n,o,r,s){const a=ht(t),l=Be(e,t),d=Ue(l,n,o);return d?nt(e,l,d.sectionIndex,c=>yt(c,a.row,d.rowIndexInSection,p=>({...p,children:Ge(p).map((f,y)=>{if(y!==d.cellIndexInRow)return f;const g={...f.props??{}},m={...g.style??{}};!s||s.trim()===""?delete m[r]:m[r]=s,Object.keys(m).length===0?delete g.style:g.style=m;const h={...f,props:g};return Object.keys(g).length===0&&delete h.props,h})}))):e}function go(e,t,n,o,r,s,a){const l=ht(t),d=Be(e,t),c=Ue(d,n,o);if(!c)return e;const p=a==="dark";return nt(e,d,c.sectionIndex,f=>yt(f,l.row,c.rowIndexInSection,y=>({...y,children:Ge(y).map((g,m)=>{if(m!==c.cellIndexInRow)return g;const h={...g.props??{}},v=typeof h.className=="string"?h.className:"",b=uC(v,s,r,p);b===""?delete h.className:h.className=b;const x={...g,props:h};return Object.keys(h).length===0&&delete x.props,x})})))}function ih(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>f===l.cellIndexInRow?{...p,text:r}:p)}))):e}function sh(e,t,n,o,r){const s=ht(t),a=Be(e,t),l=Ue(a,n,o);return l?nt(e,a,l.sectionIndex,d=>yt(d,s.row,l.rowIndexInSection,c=>({...c,children:Ge(c).map((p,f)=>f===l.cellIndexInRow?r:p)}))):e}const ah=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function r8(e){return e.split(/\s+/).filter(Boolean)}function lh(e,t){for(const n of t)if(e===n||/^-?\d+$/.test(e.slice(n.length+1))&&e.startsWith(n+"-"))return!0;return!1}function dh({className:e,catalog:t,t:n,onChange:o,colorScheme:r="light",onPresetToken:s,colorStyle:a,onColorStyle:l,disabled:d}){const c=t?.sides??[],p=t?.widths??[],f=t?.colors??[],y=r==="dark",g=u.useMemo(()=>c.map($=>$.prefix),[c]),m=u.useMemo(()=>new Set(f.map($=>$.token)),[f]),h=u.useMemo(()=>r8(e),[e]),v=u.useMemo(()=>Vr(e,y),[e,y]),b=u.useMemo(()=>v.find($=>m.has($))??null,[v,m]),x=u.useMemo(()=>{const $=new Set;for(const A of c)h.some(D=>D===A.prefix||D.startsWith(A.prefix+"-")&&/^-?\d+$/.test(D.slice(A.prefix.length+1)))&&$.add(A.value);return $},[h,c]),k=u.useMemo(()=>{const $=h.find(z=>lh(z,g));if(!$)return"none";const A=p.filter(z=>z.value!=="none");for(const z of c){if($===z.prefix)return A.find(D=>(D.suffix??"")==="")?.value??"thin";if($.startsWith(z.prefix+"-")){const D=$.slice(z.prefix.length),O=A.find(M=>(M.suffix??"")===D);if(O)return O.value}}return"thin"},[h,g,c,p]);if(!t||c.length===0||p.length===0)return null;function w($){return $.filter(A=>!lh(A,g))}function _($,A){const z=w(h),D=p.find(B=>B.value===A);if(!D||A==="none"||$.size===0)return z.join(" ").trim();const O=D.suffix??"",M=[...z];for(const B of c)$.has(B.value)&&M.push(O?`${B.prefix}${O}`:B.prefix);return M.join(" ").trim()}const C=$=>{const A=new Set(x);A.has($)?A.delete($):($==="all"?A.clear():A.delete("all"),A.add($));const z=k==="none"?p.find(D=>D.value!=="none")?.value??"thin":k;o(_(A,z))},E=$=>{const A=x.size===0?new Set(["all"]):x;o(_(A,$))},S=$=>{s?.($)},T=($,A)=>$&&$.startsWith("$t:")?n($.slice(3)):$||A;return i.jsxs("div",{className:"g7le-cell-border","data-testid":"g7le-cell-border",style:i8,children:[i.jsx("div",{style:s8,children:i.jsx("span",{style:a8,children:n("layout_editor.table_editor.cell_border")})}),i.jsx("div",{style:rl,"data-testid":"g7le-cell-border-widths",children:p.map($=>i.jsx("button",{type:"button",disabled:d,"data-testid":`g7le-cell-border-width-${$.value}`,"aria-pressed":k===$.value,onClick:()=>E($.value),style:k===$.value?ch:il,children:T($.label,$.value)},$.value))}),k!=="none"&&i.jsx("div",{style:rl,"data-testid":"g7le-cell-border-sides",children:c.map($=>i.jsx("button",{type:"button",disabled:d,"data-testid":`g7le-cell-border-side-${$.value}`,"aria-pressed":x.has($.value),onClick:()=>C($.value),style:x.has($.value)?ch:il,children:T($.label,$.value)},$.value))}),k!=="none"&&(f.length>0||l)&&i.jsx(i.Fragment,{children:i.jsxs("div",{style:rl,"data-testid":"g7le-cell-border-colors",children:[f.map($=>{const A=b===$.token;return i.jsx("button",{type:"button",disabled:d,title:T($.label,$.value),"data-testid":`g7le-cell-border-color-${$.value}`,"aria-pressed":A,onClick:()=>S($.token),style:{...uh,background:$.swatch??"#fff",outline:A?"2px solid #2563eb":"1px solid #cbd5e1"}},$.value)}),l&&(()=>{const $=!!a&&f.some(z=>z.swatch&&z.swatch.toLowerCase()===a.toLowerCase()),A=!y&&!!a&&!$;return i.jsx("label",{title:n(y?"layout_editor.table_editor.dark_free_color_hint":"layout_editor.table_inplace.custom_color"),"data-testid":"g7le-cell-border-color-custom",style:{...uh,position:"relative",overflow:"hidden",display:"inline-flex",alignItems:"center",justifyContent:"center",background:A&&ah.test(a)?a:"conic-gradient(red, yellow, lime, aqua, blue, magenta, red)",outline:A?"2px solid #2563eb":"1px solid #cbd5e1",opacity:y?.4:1,cursor:y?"not-allowed":"pointer"},children:i.jsx("input",{type:"color",disabled:d||y,"data-testid":"g7le-cell-border-color-picker",value:a&&ah.test(a)?a:"#000000",onChange:z=>l(z.target.value),style:{position:"absolute",width:0,height:0,opacity:0}})})})()]})})]})}const i8={display:"flex",flexDirection:"column",gap:6},s8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},a8={fontSize:12,fontWeight:600,color:"#475569"},rl={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},il={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},ch={...il,border:"1px solid #2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},uh={width:22,height:22,borderRadius:5,padding:0,cursor:"pointer",boxSizing:"border-box"},ph=/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;function fh({colorStyle:e,className:t,catalog:n,t:o,colorScheme:r="light",onPresetToken:s,onCustomColor:a,onClear:l,disabled:d}){const c=n?.colors??[],p=r==="dark",f=e??"",y=Vr(t??"",p),g=c.find(x=>x.token&&y.includes(x.token))?.token??null,m=(x,k)=>x&&x.startsWith("$t:")?o(x.slice(3)):x||k,h=!g&&(p||f.trim()===""),v=!!f&&c.some(x=>x.swatch&&x.swatch.toLowerCase()===f.toLowerCase()),b=!p&&!!f&&!v;return i.jsxs("div",{className:"g7le-cell-fill","data-testid":"g7le-cell-fill",style:l8,children:[i.jsx("div",{style:d8,children:i.jsx("span",{style:c8,children:o("layout_editor.table_editor.cell_fill")})}),i.jsxs("div",{style:u8,"data-testid":"g7le-cell-fill-colors",children:[i.jsx("button",{type:"button",disabled:d,title:o("layout_editor.table_editor.cell_fill_none"),"data-testid":"g7le-cell-fill-none","aria-pressed":h,onClick:()=>l?.(),style:{...sl,background:"repeating-conic-gradient(#e2e8f0 0% 25%, #ffffff 0% 50%) 50% / 10px 10px",outline:h?"2px solid #2563eb":"1px solid #cbd5e1"}}),c.map(x=>{const k=!!x.token&&g===x.token,w=!p&&!!x.swatch&&f.toLowerCase()===(x.swatch??"").toLowerCase(),_=k||w,C=d||p&&!x.token;return i.jsx("button",{type:"button",disabled:C,title:m(x.label,x.value),"data-testid":`g7le-cell-fill-color-${x.value}`,"aria-pressed":_,onClick:()=>{x.token&&s?s(x.token):!p&&x.swatch&&a&&a(x.swatch)},style:{...sl,background:x.swatch??"#fff",outline:_?"2px solid #2563eb":"1px solid #cbd5e1",opacity:C?.4:1,cursor:C?"not-allowed":"pointer"}},x.value)}),a&&i.jsx("label",{title:o(p?"layout_editor.table_editor.dark_free_color_hint":"layout_editor.table_editor.cell_fill_custom"),"data-testid":"g7le-cell-fill-custom",style:{...sl,position:"relative",overflow:"hidden",display:"inline-flex",alignItems:"center",justifyContent:"center",background:b&&ph.test(f)?f:"conic-gradient(red, yellow, lime, aqua, blue, magenta, red)",outline:b?"2px solid #2563eb":"1px solid #cbd5e1",opacity:p?.4:1,cursor:p?"not-allowed":"pointer"},children:i.jsx("input",{type:"color",disabled:d||p,"data-testid":"g7le-cell-fill-color-picker",value:f&&ph.test(f)?f:"#ffffff",onChange:x=>a(x.target.value),style:{position:"absolute",width:0,height:0,opacity:0}})})]})]})}const l8={display:"flex",flexDirection:"column",gap:6},d8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},c8={fontSize:12,fontWeight:600,color:"#475569"},u8={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},sl={width:22,height:22,borderRadius:5,padding:0,cursor:"pointer",boxSizing:"border-box"};function p8(e){const t=/^(\d+)px$/.exec(e.trim());return t?parseInt(t[1],10):null}function gh({paddingStyle:e,catalog:t,t:n,onChange:o,disabled:r}){const s=t?.steps??[],a=e??"",l=p8(a),d=(h,v)=>h&&h.startsWith("$t:")?n(h.slice(3)):h||v,c=s.some(h=>(h.px??0)===(l??-1)),p=l!==null&&!c?String(l):"",[f,y]=u.useState(p);u.useEffect(()=>{y(p)},[e]);const g=h=>{o(h&&h>0?`${h}px`:""),y("")},m=h=>{const v=parseInt(h,10);Number.isFinite(v)&&v>=0&&o(v>0?`${v}px`:"")};return i.jsxs("div",{className:"g7le-cell-padding","data-testid":"g7le-cell-padding",style:f8,children:[i.jsx("div",{style:g8,children:n("layout_editor.table_editor.cell_padding")}),i.jsxs("div",{style:h8,"data-testid":"g7le-cell-padding-steps",children:[s.map(h=>{const v=(h.px??0)===(l??-1)||!h.px&&a.trim()==="";return i.jsx("button",{type:"button",disabled:r,"data-testid":`g7le-cell-padding-step-${h.value}`,"aria-pressed":v,onClick:()=>g(h.px),style:v?y8:hh,children:d(h.label,h.value)},h.value)}),i.jsxs("span",{style:{display:"inline-flex",alignItems:"center",gap:3},children:[i.jsx("input",{type:"number",min:0,disabled:r,"data-testid":"g7le-cell-padding-free",value:f,placeholder:n("layout_editor.table_editor.cell_padding_custom"),onChange:h=>y(h.target.value),onBlur:()=>f.trim()!==""&&m(f),onKeyDown:h=>{h.key==="Enter"&&f.trim()!==""&&m(f)},style:b8}),i.jsx("span",{style:{fontSize:11,color:"#94a3b8"},children:"px"})]})]})]})}const f8={display:"flex",flexDirection:"column",gap:6},g8={fontSize:12,fontWeight:600,color:"#475569"},h8={display:"flex",gap:4,flexWrap:"wrap",alignItems:"center"},hh={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},y8={...hh,border:"1px solid #2563eb",background:"#eff6ff",color:"#1d4ed8",fontWeight:600},b8={width:48,padding:"3px 5px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,boxSizing:"border-box"};function yh({colorScheme:e,onChange:t,t:n,disabled:o}){const r=e==="dark";return i.jsxs("div",{className:"g7le-color-scheme-tabs","data-testid":"g7le-color-scheme-tabs",style:m8,children:[i.jsxs("div",{style:v8,children:[i.jsx("span",{style:_8,children:n("layout_editor.table_editor.cell_color")}),i.jsxs("div",{style:x8,children:[i.jsx("button",{type:"button",disabled:o,"data-testid":"g7le-color-scheme-light","aria-pressed":!r,onClick:()=>t("light"),style:r?al:bh,children:n("layout_editor.property_modal.scope.scheme_light")}),i.jsx("button",{type:"button",disabled:o,"data-testid":"g7le-color-scheme-dark","aria-pressed":r,onClick:()=>t("dark"),style:r?bh:al,children:n("layout_editor.property_modal.scope.scheme_dark")})]})]}),r&&i.jsx("div",{style:w8,"data-testid":"g7le-color-scheme-dark-hint",children:n("layout_editor.table_editor.dark_free_color_hint")})]})}const m8={display:"flex",flexDirection:"column",gap:3},v8={display:"flex",alignItems:"center",justifyContent:"space-between",gap:8},_8={fontSize:12,fontWeight:700,color:"#0f172a"},x8={display:"flex",gap:2},al={padding:"2px 9px",fontSize:11,border:"none",borderBottom:"2px solid transparent",background:"transparent",color:"#94a3b8",cursor:"pointer"},bh={...al,color:"#1d4ed8",borderBottom:"2px solid #2563eb",fontWeight:600},w8={fontSize:10,color:"#94a3b8",fontStyle:"italic"},ll="dark:";function mh(e){return typeof e=="string"?e.split(/\s+/).filter(Boolean):[]}function vh(e,t){return t==="dark"?e.filter(n=>n.startsWith(ll)).map(n=>n.slice(ll.length)):e.filter(n=>!n.startsWith(ll))}const k8={none:0,thin:1,medium:2,thick:4},_h={top:{w:"borderTopWidth",s:"borderTopStyle",c:"borderTopColor"},bottom:{w:"borderBottomWidth",s:"borderBottomStyle",c:"borderBottomColor"},left:{w:"borderLeftWidth",s:"borderLeftStyle",c:"borderLeftColor"},right:{w:"borderRightWidth",s:"borderRightStyle",c:"borderRightColor"}},xh=["top","bottom","left","right"];function S8(e,t,n="light"){if(!e||!t)return{};const o=t.sides??[],r=t.widths??[],s=t.colors??[],a=mh(e.props?.className);if(a.length===0&&!e.props?.style)return{};const l=[...r].filter(b=>b.value!=="none").sort((b,x)=>(x.suffix??"").length-(b.suffix??"").length),d={};for(const b of o)for(const x of l){const k=x.suffix?`${b.prefix}${x.suffix}`:b.prefix;if(a.includes(k)){d[b.value]=x.value;break}}const c=e.props?.style??{},p=vh(a,n),f=s.find(b=>p.includes(b.token))?.swatch??"",g=(n==="light"&&typeof c.borderColor=="string"?c.borderColor:"")||f||"#94a3b8",m=b=>{const x=_h[b]?.c,k=x?c[x]:void 0;return typeof k=="string"&&k?k:g},h={},v=(b,x)=>{const k=_h[b];if(!k)return;const w=k8[x]??1;h[k.w]=`${w}px`,h[k.s]=w>0?"solid":"none",w>0&&(h[k.c]=m(b))};if(d.all)for(const b of xh)v(b,d.all);for(const b of xh)d[b]&&v(b,d[b]);return h}function C8(e,t,n="light"){if(!e?.props)return{};const o=e.props.style??{},r={},s=mh(e.props.className),a=t?.colors?.find(c=>c.token&&vh(s,n).includes(c.token))?.swatch??"",d=(n==="light"&&typeof o.backgroundColor=="string"?o.backgroundColor:"")||a;return d&&(r.backgroundColor=d),typeof o.padding=="string"&&o.padding&&(r.padding=o.padding),r}const $8=/^\s*\$t:(custom\.[a-zA-Z0-9._-]+)\s*$/,E8=/^\s*\$t:([a-zA-Z0-9._-]+)\s*$/,I8=/\{\{.*?\}\}/;function wh(e,t,n){const o=e?.text;if(typeof o!="string")return{display:"",editable:!1,isKey:!1};const r=mt(o);if(r&&r.key.startsWith("custom.")){let a="";try{const l=Pt.getInstance().translate(r.key,{templateId:t,locale:n});a=l&&l!==r.key?l:""}catch{a=""}return{display:a,editable:!0,isKey:!0}}if(I8.test(o))return{display:o,editable:!1,isKey:!1};const s=E8.exec(o);if(s){const a=s[1];let l="";try{const d=Pt.getInstance().translate(a,{templateId:t,locale:n});l=d&&d!==a?d:""}catch{l=""}return{display:l,editable:!0,isKey:!0}}return{display:o,editable:!0,isKey:!1}}function dl(e){const t=er(e);return t===null?{node:null,path:null}:{node:_i(e,t),path:t}}function A8(e){if(!e||typeof e.text!="string")return null;const t=$8.exec(e.text);if(t)return t[1];const n=mt(e.text);return n&&n.key.startsWith("custom.")?n.key:null}function T8({node:e,params:t,t:n,onPatchNode:o,templateIdentifier:r,candidates:s}){const{state:a}=Je(),l=a.selectedRoute?.layoutName??null,d=a.locale,c=r??a.templateIdentifier,p=u.useMemo(()=>t&&typeof t=="object"?t:null,[t]);u.useMemo(()=>ht(p),[p]);const f=u.useMemo(()=>Be(e,p),[e,p]),y=u.useMemo(()=>{const q=p?.cellBorder;return q&&typeof q=="object"?q:null},[p]),g=u.useMemo(()=>{const q=p?.cellBackground;return q&&typeof q=="object"?q:null},[p]),m=u.useMemo(()=>{const q=p?.cellPadding;return q&&typeof q=="object"?q:null},[p]),[h,v]=u.useState(null),[b,x]=u.useState(null),[k,w]=u.useState({}),[_,C]=u.useState(!1),[E,S]=u.useState(null),[T,$]=u.useState(null),[A,z]=u.useState("light"),D=u.useCallback(q=>o(q),[o]),O=u.useMemo(()=>h?Ue(f,h.r,h.c):null,[f,h]),M=T!==null?T:h?.c??null,B=E!==null?E:h?.r??null,N=u.useMemo(()=>{const q=(y?.sides??[]).find($e=>$e.value==="all")??(y?.sides??[])[0],ae=(y?.widths??[]).find($e=>$e.value!=="none");if(!q||!ae)return[];const pe=ae.suffix??"";return[pe?`${q.prefix}${pe}`:q.prefix]},[y]),H=u.useCallback((q,ae,pe)=>{if(N.length===0)return q;const $e=(y?.sides??[]).map(J=>J.prefix),be=Ue(Be(q,p),ae,pe),oe=(typeof be?.cell?.props?.className=="string"?be.cell.props.className:"").split(/\s+/).filter(Boolean);return oe.some(J=>$e.some(le=>J===le||J.startsWith(le+"-")&&/^-?\d+$/.test(J.slice(le.length+1))))?q:Ci(q,p,ae,pe,[...oe,...N].join(" ").trim())},[N,y,p]),L=u.useCallback(q=>{if(!h)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c),j=new Set,oe=[];for(let J=ae;J<=pe;J++)for(let le=$e;le<=be;le++)j.add(`${J}:${le}`),oe.push({r:J,c:le});let ue=e;for(const J of oe){if(ue=H(ue,J.r,J.c),ue=Jt(ue,p,J.r,J.c,"borderColor",q),J.r>0&&!j.has(`${J.r-1}:${J.c}`)){const le=Ue(Be(ue,p),J.r-1,J.c);le&&(ue=Jt(ue,p,le.gridRow,le.gridCol,"borderBottomColor",q))}if(J.c>0&&!j.has(`${J.r}:${J.c-1}`)){const le=Ue(Be(ue,p),J.r,J.c-1);le&&(ue=Jt(ue,p,le.gridRow,le.gridCol,"borderRightColor",q))}}D(ue)},[h,b,e,p,D,H]),I=u.useCallback(q=>L(q),[L]),P=u.useMemo(()=>(y?.colors??[]).map(q=>q.token).filter(Boolean),[y]),F=u.useCallback(q=>{if(!h||P.length===0)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c);let j=e;for(let oe=ae;oe<=pe;oe++)for(let ue=$e;ue<=be;ue++)j=H(j,oe,ue),j=go(j,p,oe,ue,q,P,A);D(j)},[h,b,e,p,D,H,P,A]),G=u.useMemo(()=>{if(A==="dark")return"";const q=O?.cell.props?.style;return typeof q?.borderColor=="string"?q.borderColor:""},[O,A]),V=u.useCallback((q,ae)=>{if(!h)return;const pe=Math.min(h.r,b?.r??h.r),$e=Math.max(h.r,b?.r??h.r),be=Math.min(h.c,b?.c??h.c),j=Math.max(h.c,b?.c??h.c);let oe=e;for(let ue=pe;ue<=$e;ue++)for(let J=be;J<=j;J++)oe=Jt(oe,p,ue,J,q,ae);D(oe)},[h,b,e,p,D]),Y=u.useMemo(()=>(g?.colors??[]).map(q=>q.token).filter(q=>!!q),[g]),Z=u.useCallback(()=>h?{r0:Math.min(h.r,b?.r??h.r),r1:Math.max(h.r,b?.r??h.r),c0:Math.min(h.c,b?.c??h.c),c1:Math.max(h.c,b?.c??h.c)}:null,[h,b]),se=u.useCallback(q=>{const ae=Z();if(!ae||Y.length===0)return;let pe=e;for(let $e=ae.r0;$e<=ae.r1;$e++)for(let be=ae.c0;be<=ae.c1;be++)pe=go(pe,p,$e,be,q,Y,A);D(pe)},[Z,e,p,D,Y,A]),re=u.useCallback(q=>V("backgroundColor",q),[V]),U=u.useCallback(()=>{const q=Z();if(!q)return;let ae=e;for(let pe=q.r0;pe<=q.r1;pe++)for(let $e=q.c0;$e<=q.c1;$e++)ae=go(ae,p,pe,$e,void 0,Y,A),A==="light"&&(ae=Jt(ae,p,pe,$e,"backgroundColor",void 0));D(ae)},[Z,e,p,D,Y,A]),ee=u.useCallback(q=>V("padding",q),[V]),fe=u.useMemo(()=>{if(A==="dark")return"";const q=O?.cell.props?.style;return typeof q?.backgroundColor=="string"?q.backgroundColor:""},[O,A]),K=u.useMemo(()=>typeof O?.cell.props?.className=="string"?O.cell.props.className:"",[O]),ne=u.useMemo(()=>{const q=O?.cell.props?.style;return typeof q?.padding=="string"?q.padding:""},[O]),me=u.useCallback(q=>q==="section"?n("layout_editor.table_editor.move_blocked_section"):"",[n]),Ce=u.useCallback((q,ae)=>xi(f,q,ae),[f]),ze=u.useCallback((q,ae)=>wi(f,q,ae),[f]),Se=u.useCallback(q=>D(nl(e,p,q)),[e,p,D]),qe=u.useCallback(q=>{D(Rg(e,p,q)),v(null),x(null)},[e,p,D]),De=u.useCallback((q,ae)=>D(eh(e,p,q,ae)),[e,p,D]),Pe=u.useCallback(q=>D(nh(e,p,q)),[e,p,D]),Qe=u.useCallback(q=>{D(oh(e,p,q)),v(null),x(null)},[e,p,D]),Me=u.useCallback((q,ae)=>D(th(e,p,q,ae)),[e,p,D]),Re=u.useCallback(()=>{if(!h||!b)return;const q=ol(e,p,h.r,h.c,b.r,b.c);q.ok&&(D(q.table),v({r:Math.min(h.r,b.r),c:Math.min(h.c,b.c)}),x(null))},[e,p,h,b,D]),ve=u.useCallback(()=>{h&&(D(rh(e,p,h.r,h.c)),x(null))},[e,p,h,D]),te=u.useCallback(q=>{if(!h)return;const ae=Math.min(h.r,b?.r??h.r),pe=Math.max(h.r,b?.r??h.r),$e=Math.min(h.c,b?.c??h.c),be=Math.max(h.c,b?.c??h.c);let j=e;for(let oe=ae;oe<=pe;oe++)for(let ue=$e;ue<=be;ue++)j=Ci(j,p,oe,ue,q);D(j)},[e,p,h,b,D]),he=u.useCallback(async(q,ae)=>{const pe=`${q}:${ae}`,$e=k[pe];if($e===void 0)return;const be=Ue(f,q,ae);if(!be)return;const{node:j,path:oe}=dl(be.cell);if(oe===null)return;const ue=wh(j,c,d);if(ue.editable){if($e===ue.display){w(J=>{const le={...J};return delete le[pe],le});return}C(!0);try{const J=A8(j);if(J)await yr(c,J,d,$e),await tn(c,d);else{const le=await qn(c,l,d,$e);if(le.kind==="ok"&&le.resource){const Ie=`$t:${le.resource.translation_key}`;if(oe.length===0)D(ih(e,p,q,ae,Ie));else{const dt=tr(be.cell,oe,Ie);D(sh(e,p,q,ae,dt))}await tn(c,d)}}}finally{C(!1),w(J=>{const le={...J};return delete le[pe],le})}}},[k,f,c,d,l,e,p,D]),Q=u.useMemo(()=>{if(!O)return null;const{node:q,path:ae}=dl(O.cell);return ae===null?null:{value:typeof q?.text=="string"?q.text:"",editable:!0}},[O]),ie=u.useCallback(q=>{if(typeof q!="string"||q.length===0||!h)return;const ae=Ue(f,h.r,h.c);if(!ae)return;const pe=er(ae.cell);if(pe!==null)if(pe.length===0)D(ih(e,p,h.r,h.c,q));else{const $e=tr(ae.cell,pe,q);D(sh(e,p,h.r,h.c,$e))}},[h,f,e,p,D]),_e=u.useCallback((q,ae,pe)=>{pe?x({r:q,c:ae}):(v({r:q,c:ae}),x(null))},[]),ge=u.useMemo(()=>Array.from({length:f.colCount},(q,ae)=>ae),[f.colCount]),Ae=!!(h&&b&&(h.r!==b.r||h.c!==b.c)),Ne=!!(O&&(O.colSpan>1||O.rowSpan>1)),Le=O&&typeof O.cell.props?.className=="string"?O.cell.props.className:"";return i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--table","data-testid":"g7le-table-editor",style:D8,children:[i.jsx("div",{style:N8,children:n("layout_editor.table_editor.title")}),i.jsx("div",{style:{overflowX:"auto"},children:i.jsxs("table",{"data-testid":"g7le-table-editor-grid",style:z8,children:[i.jsx("thead",{children:i.jsxs("tr",{children:[i.jsx("th",{style:M8}),ge.map(q=>{const ae=ze(q,-1),pe=ze(q,1),$e=M===q;return i.jsx("th",{style:L8,"data-testid":`g7le-table-colgutter-${q}`,onMouseEnter:()=>$(q),onMouseLeave:()=>$(be=>be===q?null:be),children:i.jsxs("div",{style:{...O8,visibility:$e?"visible":"hidden"},children:[i.jsx("button",{type:"button",title:ae.ok?n("layout_editor.table_editor.move_col_left"):me(ae.reason)||n("layout_editor.table_editor.move_col_left"),disabled:!ae.ok||_,onClick:()=>Me(q,-1),"data-testid":`g7le-table-col-left-${q}`,style:ae.ok?vn:$i,children:"←"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.add_col"),disabled:_,onClick:()=>Pe(q),"data-testid":`g7le-table-col-add-${q}`,style:vn,children:"+"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.remove_col"),disabled:f.colCount<=1||_,onClick:()=>Qe(q),"data-testid":`g7le-table-col-remove-${q}`,style:kh,children:"✕"}),i.jsx("button",{type:"button",title:pe.ok?n("layout_editor.table_editor.move_col_right"):me(pe.reason)||n("layout_editor.table_editor.move_col_right"),disabled:!pe.ok||_,onClick:()=>Me(q,1),"data-testid":`g7le-table-col-right-${q}`,style:pe.ok?vn:$i,children:"→"})]})},q)})]})}),i.jsx("tbody",{children:f.rows.map((q,ae)=>{const pe=q.cells[0]?.gridRow??ae,$e=B===pe;return i.jsxs("tr",{"data-testid":`g7le-table-row-${pe}`,onMouseEnter:()=>S(pe),onMouseLeave:()=>S(be=>be===pe?null:be),children:[i.jsx("td",{style:P8,"data-testid":`g7le-table-rowgutter-${pe}`,children:(()=>{const be=Ce(pe,-1),j=Ce(pe,1);return i.jsxs("div",{style:{...B8,visibility:$e?"visible":"hidden"},children:[i.jsx("button",{type:"button",title:be.ok?n("layout_editor.table_editor.move_row_up"):me(be.reason)||n("layout_editor.table_editor.move_row_up"),disabled:!be.ok||_,onClick:()=>De(pe,-1),"data-testid":`g7le-table-row-up-${pe}`,style:be.ok?vn:$i,children:"↑"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.add_row"),disabled:_,onClick:()=>Se(pe),"data-testid":`g7le-table-row-add-${pe}`,style:vn,children:"+"}),i.jsx("button",{type:"button",title:n("layout_editor.table_editor.remove_row"),disabled:f.rows.length<=1||_,onClick:()=>qe(pe),"data-testid":`g7le-table-row-remove-${pe}`,style:kh,children:"✕"}),i.jsx("button",{type:"button",title:j.ok?n("layout_editor.table_editor.move_row_down"):me(j.reason)||n("layout_editor.table_editor.move_row_down"),disabled:!j.ok||_,onClick:()=>De(pe,1),"data-testid":`g7le-table-row-down-${pe}`,style:j.ok?vn:$i,children:"↓"})]})})()}),q.cells.map(be=>{const j=`${be.gridRow}:${be.gridCol}`,{node:oe,path:ue}=dl(be.cell),J=wh(oe,c,d),le=ue!==null&&J.editable,Ie=ue===null,dt=k[j],pt=dt!==void 0?dt:J.display,ot=h&&h.r===be.gridRow&&h.c===be.gridCol,Oe=h&&b&&be.gridRow>=Math.min(h.r,b.r)&&be.gridRow<=Math.max(h.r,b.r)&&be.gridCol>=Math.min(h.c,b.c)&&be.gridCol<=Math.max(h.c,b.c);return i.jsx("td",{colSpan:be.colSpan,rowSpan:be.rowSpan,"data-testid":`g7le-table-cell-${be.gridRow}-${be.gridCol}`,onClick:He=>_e(be.gridRow,be.gridCol,He.shiftKey),style:{...W8,...S8(be.cell,y,A),...C8(be.cell,g,A),...be.isHeader?j8:{},...ot?H8:{},...Oe&&!ot?F8:{}},children:le?i.jsx("input",{type:"text","data-testid":`g7le-table-cell-input-${be.gridRow}-${be.gridCol}`,value:pt,placeholder:n("layout_editor.table_editor.cell_text"),disabled:_,onMouseDown:He=>{He.shiftKey&&He.preventDefault(),_e(be.gridRow,be.gridCol,He.shiftKey)},onClick:He=>He.stopPropagation(),onChange:He=>w(Ye=>({...Ye,[j]:He.target.value})),onBlur:()=>{he(be.gridRow,be.gridCol)},style:K8}):Ie?i.jsx("span",{"data-testid":`g7le-table-cell-struct-${be.gridRow}-${be.gridCol}`,style:Sh,children:n("layout_editor.table_editor.structural_cell")}):i.jsx("span",{style:Sh,children:J.display})},j)})]},ae)})})]})}),i.jsxs("button",{type:"button","data-testid":"g7le-table-add-row-bottom",onClick:()=>Se(f.rows.length-1),disabled:_,style:V8,children:["+ ",n("layout_editor.table_editor.add_row")]}),i.jsxs("div",{style:U8,"data-testid":"g7le-table-cell-tools",children:[i.jsx("button",{type:"button","data-testid":"g7le-table-merge",onClick:Re,disabled:!Ae||_,style:Ch,children:n("layout_editor.table_editor.merge_cells")}),i.jsx("button",{type:"button","data-testid":"g7le-table-unmerge",onClick:ve,disabled:!Ne||_,style:Ch,children:n("layout_editor.table_editor.unmerge_cells")})]}),O&&Q&&i.jsxs("div",{style:G8,"data-testid":"g7le-table-cell-i18n-section",children:[i.jsx("div",{style:Y8,children:n("layout_editor.table_editor.cell_i18n_label")}),i.jsx(vt,{value:Q.value,onChange:ie,t:n,placeholder:n("layout_editor.table_editor.cell_text"),testidPrefix:"g7le-table-cell-i18n",candidates:s,enableExpressionTree:!0,expressionTreeCollapsible:!0})]}),O&&i.jsxs("div",{style:q8,"data-testid":"g7le-table-cell-color-section",children:[i.jsx(yh,{colorScheme:A,onChange:z,t:n,disabled:_}),y&&i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-border-row",children:i.jsx(dh,{className:Le,catalog:y,t:n,onChange:te,colorScheme:A,onPresetToken:F,colorStyle:G,onColorStyle:I,disabled:_})}),i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-fill-row",children:i.jsx(fh,{colorStyle:fe,className:K,catalog:g,t:n,colorScheme:A,onPresetToken:se,onCustomColor:re,onClear:U,disabled:_})})]}),O&&i.jsx("div",{style:cl,"data-testid":"g7le-table-cell-padding-row",children:i.jsx(gh,{paddingStyle:ne,catalog:m,t:n,onChange:ee,disabled:_})}),O&&!y&&i.jsxs("label",{style:X8,"data-testid":"g7le-table-cell-border-row",children:[i.jsx("span",{style:J8,children:n("layout_editor.table_editor.cell_border")}),i.jsx("input",{type:"text","data-testid":"g7le-table-cell-border-input",value:Le,placeholder:n("layout_editor.table_editor.cell_class_placeholder"),onChange:q=>te(q.target.value),disabled:_,style:Z8})]}),!f.valid&&i.jsx("div",{"data-testid":"g7le-table-grid-warning",style:Q8,children:n("layout_editor.table_editor.grid_invalid")}),i.jsx("div",{style:R8,children:n("layout_editor.table_editor.select_hint")})]})}const D8={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},N8={fontSize:12,fontWeight:700,color:"#0f172a"},z8={borderCollapse:"collapse",width:"auto"},M8={width:28,background:"transparent",border:"none"},L8={padding:2,border:"none",background:"transparent"},P8={padding:2,border:"none",background:"transparent",verticalAlign:"middle"},O8={display:"flex",gap:2,justifyContent:"center"},B8={display:"grid",gridTemplateColumns:"repeat(2, 16px)",gap:2,justifyContent:"center"},vn={width:16,height:16,lineHeight:"14px",padding:0,fontSize:10,border:"1px solid #cbd5e1",borderRadius:4,background:"#fff",color:"#475569",cursor:"pointer"},kh={...vn,border:"1px solid #fecaca",color:"#dc2626"},$i={...vn,cursor:"not-allowed",opacity:.4,color:"#94a3b8",borderColor:"#e2e8f0"},W8={border:"1px solid #cbd5e1",padding:0,width:64,minWidth:64,maxWidth:120,height:24,background:"#fff",cursor:"pointer"},j8={background:"#f1f5f9",fontWeight:600},H8={outline:"2px solid #2563eb",outlineOffset:-2},F8={background:"#dbeafe"},K8={width:"100%",boxSizing:"border-box",padding:"2px 5px",fontSize:11,height:22,lineHeight:"18px",border:"none",background:"transparent",outline:"none"},Sh={display:"block",padding:"2px 5px",fontSize:10,color:"#64748b",fontStyle:"italic",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:110},V8={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},U8={display:"flex",gap:6},Ch={padding:"4px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},q8={display:"flex",flexDirection:"column",gap:8,borderTop:"1px solid #f1f5f9",paddingTop:8},G8={display:"flex",flexDirection:"column",gap:6,borderTop:"1px solid #f1f5f9",paddingTop:8},Y8={fontSize:12,fontWeight:700,color:"#0f172a"},cl={display:"flex",flexDirection:"column",gap:6},X8={display:"flex",alignItems:"center",gap:6},J8={fontSize:12,color:"#475569",minWidth:60},Z8={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Q8={fontSize:11,color:"#b45309",background:"#fffbeb",border:"1px solid #fde68a",borderRadius:6,padding:"4px 6px"},R8={fontSize:11,color:"#94a3b8"},ez=/\{\{.*?\}\}/;function tz(e,t,n){const r=(e.props??{})[t];return Array.isArray(r)?r:r===void 0&&n?JSON.parse(JSON.stringify(n)):[]}function nz(e){return typeof e=="string"&&ez.test(e)}function ul(e,t,n){return e?e.startsWith("$t:")?t(e.slice(3)):e:n}function oz(e,t,n){return typeof e=="object"&&e!==null?e[t.key]:n?e:void 0}function rz(e,t,n,o){if(typeof e=="object"&&e!==null){const r={...e};return o===void 0?delete r[t.key]:r[t.key]=o,r}return n?o:{[t.key]:o}}function $h({node:e,params:t,t:n,onPatchNode:o,templateIdentifier:r,candidates:s}){const a=typeof t?.arrayProp=="string"?t.arrayProp:null,l=Array.isArray(t?.fields)?t.fields:[],d=ul(typeof t?.itemLabel=="string"?t.itemLabel:void 0,n,n("layout_editor.array_editor.item")),c=t?.newItem,p=c&&typeof c=="object"?c:null,f=typeof c=="string"||typeof c=="number"?c:null,y=(l.find(C=>C.primary)??l[0])?.key,g=(e.props??{})[a??""],m=Array.isArray(t?.defaultItems)?t.defaultItems:null,h=a?tz(e,a,m):[],v=u.useCallback(C=>{if(!a)return;const E={...e.props??{}};E[a]=C,o({...e,props:E})},[a,e,o]),b=u.useCallback(()=>{if(p)return JSON.parse(JSON.stringify(p));if(f!==null)return f;const C={};for(const E of l)(E.widget??"text")==="boolean"?C[E.key]=!1:C[E.key]="";return C},[p,f,l]),x=u.useCallback(()=>{v([...h,b()])},[v,h,b]),k=u.useCallback(C=>{v(h.filter((E,S)=>S!==C))},[v,h]),w=u.useCallback((C,E)=>{const S=C+E;if(S<0||S>=h.length)return;const T=[...h];[T[C],T[S]]=[T[S],T[C]],v(T)},[v,h]),_=u.useCallback((C,E,S)=>{const T=E.key===y,$=h.map((A,z)=>z===C?rz(A,E,T,S):A);v($)},[h,v,y]);return a?nz(g)||g!==void 0&&!Array.isArray(g)?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor-bound",style:fz,children:n("layout_editor.array_editor.bound_degraded")}):i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor",style:iz,children:[i.jsx("div",{style:sz,children:n("layout_editor.array_editor.items_title")}),h.length===0&&i.jsx("div",{"data-testid":"g7le-array-empty",style:Ih,children:n("layout_editor.array_editor.empty")}),h.map((C,E)=>i.jsxs("div",{"data-testid":`g7le-array-row-${E}`,style:az,children:[i.jsxs("div",{style:lz,children:[i.jsxs("span",{style:dz,children:["#",E+1]}),i.jsx("div",{style:{flex:1}}),i.jsx("button",{type:"button","data-testid":`g7le-array-up-${E}`,title:n("layout_editor.array_editor.move_up"),disabled:E===0,onClick:()=>w(E,-1),style:Eh,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-array-down-${E}`,title:n("layout_editor.array_editor.move_down"),disabled:E===h.length-1,onClick:()=>w(E,1),style:Eh,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-array-remove-${E}`,title:n("layout_editor.array_editor.remove"),onClick:()=>k(E),style:uz,children:"✕"})]}),l.map(S=>{const T=S.widget??"text",$=S.key===y,A=ul(S.label,n,S.key),z=oz(C,S,$),D=`g7le-array-field-${E}-${S.key}`;if(T==="boolean")return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"checkbox","data-testid":D,checked:z===!0,onChange:M=>_(E,S,M.target.checked)})]},S.key);if(T==="select"){const M=Array.isArray(S.options)?S.options:[];return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsxs("select",{"data-testid":D,value:z==null?"":String(z),onChange:B=>_(E,S,B.target.value===""?void 0:B.target.value),style:or,children:[i.jsx("option",{value:"",children:"—"}),M.map((B,N)=>i.jsx("option",{value:String(B.value),children:ul(B.label,n,String(B.value))},N))]})]},S.key)}if(T==="i18n-text"){const M=typeof z=="string"?z:z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("div",{"data-testid":D,style:{flex:1,minWidth:0},children:i.jsx(vt,{value:M,onChange:B=>_(E,S,B===""?void 0:B),t:n,placeholder:A,testidPrefix:`${D}-i18n`,candidates:s,enableExpressionTree:!0,expressionTreeCollapsible:!0})})]},S.key)}if(T==="number"){const M=z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"number","data-testid":D,value:M,onChange:B=>{const N=B.target.value;_(E,S,N===""?void 0:Number(N))},style:or})]},S.key)}if(T==="color"){const M=typeof z=="string"?z:"",B=/^#[0-9a-fA-F]{6}$/.test(M)?M:"#000000";return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"color","data-testid":`${D}-swatch`,value:B,onChange:N=>_(E,S,N.target.value),style:cz}),i.jsx("input",{type:"text","data-testid":D,value:M,placeholder:"#7C3AED",onChange:N=>_(E,S,N.target.value===""?void 0:N.target.value),style:or})]},S.key)}if(T==="number-list"){const M=Array.isArray(z)?z.join(", "):"";return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"text","data-testid":D,value:M,placeholder:n("layout_editor.array_editor.number_list_placeholder"),onChange:B=>{const N=B.target.value.split(",").map(H=>H.trim()).filter(H=>H!=="").map(H=>Number(H)).filter(H=>!Number.isNaN(H));_(E,S,N)},style:or})]},S.key)}const O=z==null?"":String(z);return i.jsxs("label",{style:Wn,children:[i.jsx("span",{style:jn,children:A}),i.jsx("input",{type:"text","data-testid":D,value:O,placeholder:T==="icon"?n("layout_editor.array_editor.icon_placeholder"):A,onChange:M=>_(E,S,M.target.value===""?void 0:M.target.value),style:or})]},S.key)})]},E)),i.jsx("button",{type:"button","data-testid":"g7le-array-add",onClick:x,style:pz,children:n("layout_editor.array_editor.add",{item:d})})]}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array","data-testid":"g7le-array-editor-misconfigured",style:Ih,children:n("layout_editor.array_editor.no_array_prop")})}const iz={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},sz={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},az={display:"flex",flexDirection:"column",gap:4,padding:8,border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc"},lz={display:"flex",gap:4,alignItems:"center"},dz={fontSize:11,fontWeight:700,color:"#64748b"},Wn={display:"flex",flexWrap:"wrap",gap:6,alignItems:"center"},jn={fontSize:11,color:"#475569",minWidth:64,flexShrink:0},or={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},cz={width:32,height:28,padding:0,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",cursor:"pointer",flexShrink:0},Eh={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},uz={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},pz={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Ih={fontSize:11,color:"#94a3b8",fontStyle:"italic"},fz={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"};function gz(e,t){return e?e.startsWith("$t:")?t(e.slice(3)):e:null}function hz(e){const{params:t,t:n}=e,o=Array.isArray(t?.groups)?t.groups:[];return o.length===0?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array-group","data-testid":"g7le-array-group-misconfigured",style:vz,children:n("layout_editor.array_editor.no_array_prop")}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--array-group","data-testid":"g7le-array-group-editor",style:yz,children:o.map((r,s)=>{const a=gz(r.title,n),{title:l,...d}=r;return i.jsxs("div",{"data-testid":`g7le-array-group-${r.arrayProp??s}`,style:bz,children:[a&&i.jsx("div",{style:mz,children:a}),i.jsx($h,{...e,params:d})]},r.arrayProp??s)})})}const yz={display:"flex",flexDirection:"column",gap:16,width:"100%",marginBottom:12},bz={display:"flex",flexDirection:"column",gap:4},mz={fontSize:12,fontWeight:700,color:"#1e293b",borderBottom:"1px solid #e2e8f0",paddingBottom:4},vz={fontSize:11,color:"#94a3b8",fontStyle:"italic"},_z=/\{\{.*?\}\}/;function xz(e){return typeof e=="string"&&_z.test(e)}function wz(e,t,n){return e?e.startsWith("$t:")?t(e.slice(3)):e:n}function kz(e){const{node:t,params:n,t:o,onPatchNode:r}=e,s=typeof n?.arrayProp=="string"?n.arrayProp:null,a=typeof n?.idField=="string"?n.idField:"id",l=typeof n?.cellChildrenProp=="string"?n.cellChildrenProp:"cellChildren",d=typeof n?.cellChildComponent=="string"?n.cellChildComponent:"Div",c=wz(typeof n?.itemLabel=="string"?n.itemLabel:void 0,o,o("layout_editor.array_editor.item")),[p,f]=u.useState(null),y=s?(t.props??{})[s]:void 0,g=Array.isArray(y)?y:[],m=u.useCallback(C=>{if(!s)return;const E={...t.props??{}};E[s]=C,r({...t,props:E})},[s,t,r]),h=u.useCallback(()=>{const C=n?.newItem;return C&&typeof C=="object"?JSON.parse(JSON.stringify(C)):{[a]:"",[l]:[]}},[n,a,l]),v=u.useCallback(()=>{m([...g,h()])},[m,g,h]),b=u.useCallback(C=>{m(g.filter((E,S)=>S!==C)),f(null)},[m,g]),x=u.useCallback((C,E)=>{const S=C+E;if(S<0||S>=g.length)return;const T=[...g];[T[C],T[S]]=[T[S],T[C]],m(T)},[m,g]),k=u.useCallback((C,E)=>{const S=g.map((T,$)=>$===C?{...T,[a]:E}:T);m(S)},[g,m,a]),w=u.useCallback(C=>({type:"composite",name:"CardGridCell",children:Array.isArray(C[l])?C[l]:[]}),[l]),_=u.useCallback(C=>E=>{const S=Array.isArray(E.children)?E.children:[],T=g.map(($,A)=>A===C?{...$,[l]:S}:$);m(T)},[g,m,l]);return s?xz(y)||y!==void 0&&!Array.isArray(y)?i.jsx("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-bound",style:Mz,children:o("layout_editor.array_editor.bound_degraded")}):i.jsxs("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-editor",style:Sz,children:[i.jsx("div",{style:Cz,children:o("layout_editor.cell_tree_editor.columns_title")}),g.length===0&&i.jsx("div",{"data-testid":"g7le-cell-tree-empty",style:Th,children:o("layout_editor.array_editor.empty")}),g.map((C,E)=>{const S=typeof C[a]=="string"?C[a]:"",T=p===E;return i.jsxs("div",{"data-testid":`g7le-cell-tree-row-${E}`,style:$z,children:[i.jsxs("div",{style:Ez,children:[i.jsxs("span",{style:Iz,children:["#",E+1]}),i.jsx("input",{type:"text","data-testid":`g7le-cell-tree-id-${E}`,value:S,placeholder:o("layout_editor.cell_tree_editor.id_placeholder"),onChange:$=>k(E,$.target.value),style:Az}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-up-${E}`,title:o("layout_editor.array_editor.move_up"),disabled:E===0,onClick:()=>x(E,-1),style:Ah,children:"↑"}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-down-${E}`,title:o("layout_editor.array_editor.move_down"),disabled:E===g.length-1,onClick:()=>x(E,1),style:Ah,children:"↓"}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-remove-${E}`,title:o("layout_editor.array_editor.remove"),onClick:()=>b(E),style:Tz,children:"✕"})]}),i.jsx("button",{type:"button","data-testid":`g7le-cell-tree-toggle-${E}`,onClick:()=>f(T?null:E),style:Dz,children:o(T?"layout_editor.cell_tree_editor.collapse_cells":"layout_editor.cell_tree_editor.expand_cells")}),T&&i.jsx("div",{"data-testid":`g7le-cell-tree-cells-${E}`,style:Nz,children:i.jsx(qg,{...e,node:w(C),params:{childComponent:d},onPatchNode:_(E)})})]},E)}),i.jsx("button",{type:"button","data-testid":"g7le-cell-tree-add",onClick:v,style:zz,children:o("layout_editor.cell_tree_editor.add_column",{item:c})})]}):i.jsx("div",{className:"g7le-node-editor g7le-node-editor--cell-tree","data-testid":"g7le-cell-tree-misconfigured",style:Th,children:o("layout_editor.array_editor.no_array_prop")})}const Sz={display:"flex",flexDirection:"column",gap:8,width:"100%",marginBottom:12},Cz={fontSize:12,fontWeight:700,color:"#0f172a",marginBottom:2},$z={display:"flex",flexDirection:"column",gap:6,padding:8,border:"1px solid #e2e8f0",borderRadius:8,background:"#f8fafc"},Ez={display:"flex",gap:4,alignItems:"center"},Iz={fontSize:11,fontWeight:700,color:"#64748b",flexShrink:0},Az={flex:1,minWidth:0,padding:"4px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6},Ah={padding:"2px 6px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"},Tz={padding:"2px 6px",fontSize:12,border:"1px solid #fecaca",borderRadius:6,background:"#fff",color:"#dc2626",cursor:"pointer"},Dz={padding:"3px 8px",fontSize:11,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#2563eb",cursor:"pointer",alignSelf:"flex-start"},Nz={paddingLeft:8,borderLeft:"2px solid #e2e8f0"},zz={padding:"4px 8px",fontSize:12,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",alignSelf:"flex-start"},Th={fontSize:11,color:"#94a3b8",fontStyle:"italic"},Mz={fontSize:11,color:"#b45309",fontStyle:"italic",padding:"4px 2px"};function Lz(e,t,n){const o=Be(e,t);if(!n||n.length===0)return{measured:[],grid:o};const r=[];for(const s of o.rows)for(const a of s.cells){const d=tl(e,a,t).map(p=>`children.${p}`).join("."),c=n.find(p=>p.path===d||p.path.endsWith("."+d));c&&r.push({ref:a,box:{top:c.top,left:c.left,width:c.width,height:c.height}})}return{measured:r,grid:o}}const pl=26,fl=50,gl=6,Dh=44,Nh=18,zh="g7le-table-inplace-empty-cell-style";function Pz(){if(typeof document>"u"||document.getElementById(zh))return;const e=document.createElement("style");e.id=zh,e.textContent=".g7le-preview-frame td[data-editor-name],.g7le-preview-frame th[data-editor-name]{min-width:56px;height:38px;}.g7le-preview-frame table[data-editor-name]{min-width:120px;}",document.head.appendChild(e)}function Oz({node:e,params:t,nodeBox:n,cellBoxes:o,selectedCellPath:r,colorScheme:s,t:a,onPatchNode:l,onRequestInlineEdit:d}){const c=u.useMemo(()=>t&&typeof t=="object"?t:null,[t]),{measured:p,grid:f}=u.useMemo(()=>Lz(e,c,o),[e,c,o]),[y,g]=u.useState(null),[m,h]=u.useState(null),v=u.useMemo(()=>{if(!r)return null;for(const j of f.rows)for(const oe of j.cells){const ue=tl(e,oe,c).map(J=>`children.${J}`).join(".");if(r===ue||r.startsWith(ue+"."))return{r:oe.gridRow,c:oe.gridCol}}return null},[r,f,e,c]),b=u.useMemo(()=>y&&Ue(f,y.r,y.c)?y:v,[y,v,f]),[x,k]=u.useState(!1),[w,_]=u.useState(s==="dark"?"dark":"light"),[C,E]=u.useState(null),[S,T]=u.useState(null),$=u.useRef(null),A=u.useCallback(()=>{$.current!==null&&(clearTimeout($.current),$.current=null)},[]),z=u.useCallback(()=>{A(),$.current=setTimeout(()=>{E(null),T(null),$.current=null},240)},[A]);u.useEffect(()=>()=>A(),[A]),u.useEffect(()=>{Pz()},[]);const D=u.useCallback(j=>l(j),[l]),O=u.useMemo(()=>b?Ue(f,b.r,b.c):null,[f,b]),M=u.useMemo(()=>O&&typeof O.cell.props?.className=="string"?O.cell.props.className:"",[O]),B=u.useCallback(j=>D(nl(e,c,j)),[e,c,D]),N=u.useCallback(j=>D(Rg(e,c,j)),[e,c,D]),H=u.useCallback((j,oe)=>D(eh(e,c,j,oe)),[e,c,D]),L=u.useCallback(j=>D(nh(e,c,j)),[e,c,D]),I=u.useCallback(j=>D(oh(e,c,j)),[e,c,D]),P=u.useCallback((j,oe)=>D(th(e,c,j,oe)),[e,c,D]),F=u.useCallback(j=>{if(!O)return;const oe=O.gridRow,ue=O.gridCol,J=j==="down"?oe+O.rowSpan:oe+O.rowSpan-1,le=j==="right"?ue+O.colSpan:ue+O.colSpan-1;if(J>=f.rows.length||le>=f.colCount)return;const Ie=ol(e,c,oe,ue,J,le);Ie.ok&&D(Ie.table)},[O,f,e,c,D]),G=!!(O&&O.gridCol+O.colSpan<f.colCount),V=!!(O&&O.gridRow+O.rowSpan<f.rows.length),Y=u.useMemo(()=>!b||!m||!Ue(f,m.r,m.c)?null:{r0:Math.min(b.r,m.r),c0:Math.min(b.c,m.c),r1:Math.max(b.r,m.r),c1:Math.max(b.c,m.c)},[b,m,f]),Z=u.useMemo(()=>{if(!Y)return[];const j=new Set,oe=[];for(const ue of f.rows)for(const J of ue.cells)if(J.gridRow>=Y.r0&&J.gridRow<=Y.r1&&J.gridCol>=Y.c0&&J.gridCol<=Y.c1){const le=`${J.gridRow}:${J.gridCol}`;j.has(le)||(j.add(le),oe.push({r:J.gridRow,c:J.gridCol}))}return oe},[Y,f]),se=Z.length>1,re=u.useCallback(()=>{if(!Y)return;const j=ol(e,c,Y.r0,Y.c0,Y.r1,Y.c1);j.ok&&(D(j.table),h(null))},[Y,e,c,D]),U=u.useCallback(()=>{b&&D(rh(e,c,b.r,b.c))},[e,c,b,D]),ee=u.useMemo(()=>{const j=c?.cellBorder;return j&&typeof j=="object"?j:null},[c]),fe=u.useMemo(()=>{const j=c?.cellBackground;return j&&typeof j=="object"?j:null},[c]),K=u.useMemo(()=>{const j=c?.cellPadding;return j&&typeof j=="object"?j:null},[c]),ne=u.useMemo(()=>{if(x){const j=new Set,oe=[];for(const ue of f.rows)for(const J of ue.cells){const le=`${J.gridRow}:${J.gridCol}`;j.has(le)||(j.add(le),oe.push({r:J.gridRow,c:J.gridCol}))}return oe}return se?Z:b?[{r:b.r,c:b.c}]:[]},[x,se,Z,b,f]),me=u.useCallback(j=>{if(ne.length===0)return;let oe=e;for(const ue of ne){const J=(ee?.colors??[]).map(Ie=>Ie.token),le=j.split(/\s+/).filter(Ie=>Ie&&!J.includes(Ie)).join(" ");oe=Ci(oe,c,ue.r,ue.c,le)}D(oe)},[e,c,ne,ee,D]),Ce=u.useMemo(()=>{const j=(ee?.sides??[]).find(J=>J.value==="all")??(ee?.sides??[])[0],oe=(ee?.widths??[]).find(J=>J.value!=="none");if(!j||!oe)return[];const ue=oe.suffix??"";return[ue?`${j.prefix}${ue}`:j.prefix]},[ee]),ze=u.useCallback((j,oe,ue)=>{if(Ce.length===0)return j;const J=(ee?.sides??[]).map(Oe=>Oe.prefix),le=Ue(Be(j,c),oe,ue),dt=(typeof le?.cell?.props?.className=="string"?le.cell.props.className:"").split(/\s+/).filter(Boolean);if(dt.some(Oe=>J.some(He=>Oe===He||Oe.startsWith(He+"-")&&/^-?\d+$/.test(Oe.slice(He.length+1)))))return j;const ot=[...dt,...Ce].join(" ").trim();return Ci(j,c,oe,ue,ot)},[Ce,ee,c]),Se=u.useCallback((j,oe)=>{if(j.length===0)return;const ue=new Set(j.map(le=>`${le.r}:${le.c}`));let J=e;for(const le of j){if(J=ze(J,le.r,le.c),J=Jt(J,c,le.r,le.c,"borderColor",oe),le.r>0&&!ue.has(`${le.r-1}:${le.c}`)){const Ie=Ue(Be(J,c),le.r-1,le.c);Ie&&(J=Jt(J,c,Ie.gridRow,Ie.gridCol,"borderBottomColor",oe))}if(le.c>0&&!ue.has(`${le.r}:${le.c-1}`)){const Ie=Ue(Be(J,c),le.r,le.c-1);Ie&&(J=Jt(J,c,Ie.gridRow,Ie.gridCol,"borderRightColor",oe))}}D(J)},[e,c,D,ze]),qe=u.useMemo(()=>(ee?.colors??[]).map(j=>j.token).filter(Boolean),[ee]),De=u.useCallback(j=>{if(ne.length===0||qe.length===0)return;let oe=e;for(const ue of ne)oe=ze(oe,ue.r,ue.c),oe=go(oe,c,ue.r,ue.c,j,qe,w);D(oe)},[ne,e,c,D,ze,qe,w]),Pe=u.useCallback(j=>Se(ne,j),[Se,ne]),Qe=u.useMemo(()=>{if(w==="dark")return"";const j=O?.cell.props?.style;return typeof j?.borderColor=="string"?j.borderColor:""},[O,w]),Me=u.useCallback((j,oe)=>{if(ne.length===0)return;let ue=e;for(const J of ne)ue=Jt(ue,c,J.r,J.c,j,oe);D(ue)},[ne,e,c,D]),Re=u.useMemo(()=>(fe?.colors??[]).map(j=>j.token).filter(j=>!!j),[fe]),ve=u.useCallback(j=>{if(ne.length===0||Re.length===0)return;let oe=e;for(const ue of ne)oe=go(oe,c,ue.r,ue.c,j,Re,w);D(oe)},[ne,e,c,D,Re,w]),te=u.useCallback(j=>Me("backgroundColor",j),[Me]),he=u.useCallback(()=>{if(ne.length===0)return;let j=e;for(const oe of ne)j=go(j,c,oe.r,oe.c,void 0,Re,w),w==="light"&&(j=Jt(j,c,oe.r,oe.c,"backgroundColor",void 0));D(j)},[ne,e,c,D,Re,w]),Q=u.useCallback(j=>Me("padding",j),[Me]),ie=u.useMemo(()=>{if(w==="dark")return"";const j=O?.cell.props?.style;return typeof j?.backgroundColor=="string"?j.backgroundColor:""},[O,w]),_e=u.useMemo(()=>typeof O?.cell.props?.className=="string"?O.cell.props.className:"",[O]),ge=u.useMemo(()=>{const j=O?.cell.props?.style;return typeof j?.padding=="string"?j.padding:""},[O]),Ae=u.useCallback(j=>{const oe=j.currentTarget,ue=oe.style.pointerEvents;oe.style.pointerEvents="none";const J=document.elementFromPoint(j.clientX,j.clientY);oe.style.pointerEvents=ue;const le=J?.closest?.("[data-dnd-handle-path]");le&&le.dispatchEvent(new PointerEvent("pointerdown",{bubbles:!0,cancelable:!0,clientX:j.clientX,clientY:j.clientY,pointerId:j.pointerId,pointerType:j.pointerType,button:j.button,isPrimary:!0}))},[]),Ne=u.useMemo(()=>{const j=new Map;for(const oe of p)j.has(oe.ref.gridCol)||j.set(oe.ref.gridCol,oe.box);return j},[p]),Le=u.useMemo(()=>{const j=new Map;for(const oe of p)j.has(oe.ref.gridRow)||j.set(oe.ref.gridRow,oe.box);return j},[p]);if(p.length===0)return null;const q=!!(O&&(O.colSpan>1||O.rowSpan>1)),ae=S!==null?S:b?.c??null,pe=C!==null?C:b?.r??null,$e=n.top-pl-gl-Dh,be=n.left-fl-gl-Dh;return i.jsxs("div",{className:"g7le-table-inplace","data-testid":"g7le-table-inplace",style:{position:"absolute",inset:0,pointerEvents:"none",zIndex:g7},children:[p.map(j=>{const oe=b&&b.r===j.ref.gridRow&&b.c===j.ref.gridCol,ue=Y&&j.ref.gridRow>=Y.r0&&j.ref.gridRow<=Y.r1&&j.ref.gridCol>=Y.c0&&j.ref.gridCol<=Y.c1;return i.jsx("div",{"data-testid":`g7le-inplace-cell-${j.ref.gridRow}-${j.ref.gridCol}`,role:"button",tabIndex:-1,"aria-label":a("layout_editor.table_inplace.select_cell"),onMouseEnter:()=>{A(),E(j.ref.gridRow),T(j.ref.gridCol)},onMouseLeave:()=>{z()},onPointerDown:J=>Ae(J),onClick:J=>{if(J.stopPropagation(),J.shiftKey&&(y||b))h({r:j.ref.gridRow,c:j.ref.gridCol});else{g({r:j.ref.gridRow,c:j.ref.gridCol}),h(null);const le=tl(e,j.ref,c).map(Ie=>`children.${Ie}`).join(".");d?.(le)}},style:{position:"absolute",top:j.box.top,left:j.box.left,width:j.box.width,height:j.box.height,minWidth:Nh,minHeight:Nh,boxSizing:"border-box",pointerEvents:"auto",cursor:"pointer",outline:oe?"2px solid #2563eb":"none",outlineOffset:-2,background:ue&&!oe?"rgba(37,99,235,0.14)":"transparent"}},`cell-${j.ref.gridRow}-${j.ref.gridCol}`)}),i.jsx("div",{"data-testid":"g7le-inplace-col-rail",onMouseEnter:A,onMouseLeave:z,style:{position:"absolute",top:$e,left:n.left,width:n.width,height:pl,pointerEvents:"none"},children:ae!==null&&Ne.has(ae)&&(()=>{const j=ae,oe=Ne.get(j),ue=wi(f,j,-1),J=wi(f,j,1),le=oe.left-n.left;return i.jsxs("div",{"data-testid":`g7le-inplace-colgutter-${j}`,onMouseEnter:()=>{A(),T(j)},onMouseLeave:z,style:{position:"absolute",top:0,left:le,width:oe.width,minWidth:96,height:pl,display:"flex",gap:3,justifyContent:"center",alignItems:"center",pointerEvents:"auto"},children:[i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_col_left"),disabled:!ue.ok,onClick:()=>P(j,-1),"data-testid":`g7le-inplace-col-left-${j}`,style:ue.ok?_n:Ei,children:"←"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.add_col"),onClick:()=>L(j),"data-testid":`g7le-inplace-col-add-${j}`,style:_n,children:"+"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.remove_col"),disabled:f.colCount<=1,onClick:()=>I(j),"data-testid":`g7le-inplace-col-remove-${j}`,style:Mh,children:"✕"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_col_right"),disabled:!J.ok,onClick:()=>P(j,1),"data-testid":`g7le-inplace-col-right-${j}`,style:J.ok?_n:Ei,children:"→"})]},`colgut-${j}`)})()}),i.jsx("div",{"data-testid":"g7le-inplace-row-rail",style:{position:"absolute",top:n.top,left:be,width:fl,height:n.height,pointerEvents:"none"},children:pe!==null&&Le.has(pe)&&(()=>{const j=pe,oe=Le.get(j),ue=xi(f,j,-1),J=xi(f,j,1),le=oe.top-n.top;return i.jsxs("div",{"data-testid":`g7le-inplace-rowgutter-${j}`,onMouseEnter:()=>{A(),E(j)},onMouseLeave:z,style:{position:"absolute",top:le,left:0,width:fl,height:oe.height,minHeight:48,display:"grid",gridTemplateColumns:"repeat(2, 22px)",gridAutoRows:"22px",gap:3,justifyContent:"center",alignContent:"center",pointerEvents:"auto"},children:[i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_row_up"),disabled:!ue.ok,onClick:()=>H(j,-1),"data-testid":`g7le-inplace-row-up-${j}`,style:ue.ok?_n:Ei,children:"↑"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.add_row"),onClick:()=>B(j),"data-testid":`g7le-inplace-row-add-${j}`,style:_n,children:"+"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.remove_row"),disabled:f.rows.length<=1,onClick:()=>N(j),"data-testid":`g7le-inplace-row-remove-${j}`,style:Mh,children:"✕"}),i.jsx("button",{type:"button",title:a("layout_editor.table_inplace.move_row_down"),disabled:!J.ok,onClick:()=>H(j,1),"data-testid":`g7le-inplace-row-down-${j}`,style:J.ok?_n:Ei,children:"↓"})]},`rowgut-${j}`)})()}),i.jsxs("div",{"data-testid":"g7le-inplace-corner-add",style:{position:"absolute",top:n.top+n.height+gl,left:n.left,display:"flex",flexDirection:"column",gap:6,pointerEvents:"auto",minWidth:200,maxWidth:Math.max(240,n.width),background:"#fff",border:"1px solid #e2e8f0",borderRadius:8,padding:8,boxShadow:"0 2px 8px rgba(0,0,0,0.12)"},children:[i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap"},children:[i.jsxs("button",{type:"button","data-testid":"g7le-inplace-add-row-bottom",onClick:()=>B(f.rows.length-1),style:Lh,children:["+ ",a("layout_editor.table_inplace.add_row")]}),i.jsxs("button",{type:"button","data-testid":"g7le-inplace-add-col-right",onClick:()=>L(f.colCount-1),style:Lh,children:["+ ",a("layout_editor.table_inplace.add_col")]})]}),!b&&i.jsx("div",{"data-testid":"g7le-inplace-select-hint",style:{fontSize:11,color:"#94a3b8",borderTop:"1px solid #f1f5f9",paddingTop:6},children:a("layout_editor.table_inplace.select_cell_hint")}),b&&i.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:6,borderTop:"1px solid #f1f5f9",paddingTop:6},children:[se&&i.jsx("div",{"data-testid":"g7le-inplace-range-count",style:{fontSize:11,color:"#2563eb"},children:a("layout_editor.table_inplace.range_selected",{count:Z.length})}),i.jsxs("div",{style:{display:"flex",gap:6,flexWrap:"wrap",alignItems:"center"},children:[se?i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-range",onClick:re,style:rr,children:a("layout_editor.table_inplace.merge_range")}):i.jsxs(i.Fragment,{children:[i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-right",disabled:!G,onClick:()=>F("right"),style:G?rr:hl,children:a("layout_editor.table_inplace.merge_right")}),i.jsx("button",{type:"button","data-testid":"g7le-inplace-merge-down",disabled:!V,onClick:()=>F("down"),style:V?rr:hl,children:a("layout_editor.table_inplace.merge_down")})]}),i.jsx("button",{type:"button","data-testid":"g7le-inplace-unmerge",disabled:!q,onClick:U,style:q?rr:hl,children:a("layout_editor.table_inplace.unmerge")})]}),i.jsxs("div",{"data-testid":"g7le-inplace-cell-color",style:{borderTop:"1px solid #f1f5f9",paddingTop:6,display:"flex",flexDirection:"column",gap:6},children:[i.jsxs("label",{style:{display:"flex",alignItems:"center",gap:6,fontSize:11,color:"#475569",cursor:"pointer"},children:[i.jsx("input",{type:"checkbox","data-testid":"g7le-inplace-border-all-toggle",checked:x,onChange:j=>k(j.target.checked)}),a("layout_editor.table_inplace.border_apply_all")]}),i.jsx(yh,{colorScheme:w,onChange:_,t:a}),ee&&i.jsx("div",{"data-testid":"g7le-inplace-cell-border",children:i.jsx(dh,{className:x?"":M,catalog:ee,t:a,onChange:me,colorScheme:w,onPresetToken:De,colorStyle:x?"":Qe,onColorStyle:Pe})}),i.jsx("div",{"data-testid":"g7le-inplace-cell-fill",children:i.jsx(fh,{colorStyle:x?"":ie,className:x?"":_e,catalog:fe,t:a,colorScheme:w,onPresetToken:ve,onCustomColor:te,onClear:he})})]}),i.jsx("div",{"data-testid":"g7le-inplace-cell-padding",style:{borderTop:"1px solid #f1f5f9",paddingTop:6},children:i.jsx(gh,{paddingStyle:x?"":ge,catalog:K,t:a,onChange:Q})})]})]})]})}const _n={width:22,height:22,lineHeight:"20px",padding:0,fontSize:13,border:"1px solid #cbd5e1",borderRadius:5,background:"#fff",color:"#475569",cursor:"pointer",boxShadow:"0 1px 3px rgba(0,0,0,0.18)"},Mh={..._n,border:"1px solid #fecaca",color:"#dc2626"},Ei={..._n,cursor:"not-allowed",opacity:.4,color:"#94a3b8",borderColor:"#e2e8f0"},rr={padding:"4px 10px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer",boxShadow:"0 1px 2px rgba(0,0,0,0.12)"},hl={...rr,cursor:"not-allowed",opacity:.4},Lh={padding:"3px 8px",fontSize:11,border:"1px dashed #94a3b8",borderRadius:6,background:"#fff",color:"#475569",cursor:"pointer"};let Ph=!1;function Bz(){Ph||(Pn("children",qg),Pn("table",T8),Pn("array",$h),Pn("array-group",hz),Pn("array-cell-tree",kz),Oa("table",Oz),Ph=!0)}function Wz(){if(typeof window>"u")return;const e=window.G7Core;if(!e)return;const t=e.layoutEditor,n=Array.isArray(t?.__queue)?t.__queue:[],o=Array.isArray(t?.__readyCallbacks)?t.__readyCallbacks:[];e.layoutEditor={registerWidget:Xe,registerNodeEditor:Pn,registerCanvasOverlay:Oa,onReady:r=>{typeof r=="function"&&r()}};for(const r of n){const[s,...a]=r;try{s==="widget"?Xe(a[0],a[1]):s==="nodeEditor"?Pn(a[0],a[1]):s==="canvasOverlay"&&Oa(a[0],a[1])}catch{}}for(const r of o)try{r()}catch{}}const jz=1280;qN(),Bz(),Wz();async function Hz(e){if(typeof fetch!="function")return[];try{const t=await fetch(en(`/api/admin/templates/${encodeURIComponent(e)}/editor/permission-candidates`,"json"),{credentials:"same-origin",headers:je()});if(!t.ok)return[];const o=(await t.json().catch(()=>null))?.data?.permissions;if(!Array.isArray(o))return[];const r=[];for(const s of o)if(s&&typeof s=="object"){const a=s.key,l=s.name;typeof a=="string"&&a.length>0&&r.push({value:a,label:typeof l=="string"&&l?l:a})}return r}catch{return[]}}function Fz({templateIdentifier:e}){gD({templateIdentifier:e}),hD(e);const{state:t,dispatch:n}=Je(),{t:o}=tt(),r=yo(),s=Cp(),a=xD(),l=t.selectedRoute?.layoutName??null,d=CD(e,l),c=t.editMode==="extension",p=u.useMemo(()=>c?kD(a):s,[c,a,s]),[f,y]=u.useState(null),[g,m]=u.useState(null),[h,v]=u.useState([]),[b,x]=u.useState("user"),[k,w]=u.useState(null),[_,C]=u.useState(null),[E,S]=u.useState([]);u.useEffect(()=>{let te=!1;return(async()=>{try{const he=await fetch(en(`/api/templates/${encodeURIComponent(e)}/components`,"json"),{credentials:"same-origin",headers:{Accept:"application/json"}});if(!he.ok)return;const Q=await he.json().catch(()=>null);!te&&Q&&typeof Q=="object"&&y(Q)}catch{}})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{const{spec:he,sampleGlobalSources:Q}=await eb({templateIdentifier:e,coreSeed:$D()});te||(m(he),v(Q),Vz(e,he,Q))})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{if(typeof fetch=="function")try{const he=await fetch(`/api/admin/templates/${encodeURIComponent(e)}`,{credentials:"same-origin",headers:je()});if(!he.ok)return;const ie=(await he.json().catch(()=>null))?.data,_e=ie?.type;te||((_e==="admin"||_e==="user")&&x(_e),typeof ie?.name=="string"&&ie.name&&w(ie.name),typeof ie?.version=="string"&&ie.version&&C(ie.version))}catch{}})(),()=>{te=!0}},[e]),u.useEffect(()=>{let te=!1;return(async()=>{if(typeof fetch=="function")try{const he=await fetch("/api/admin/templates/?per_page=100&page=1",{credentials:"same-origin",headers:je()});if(!he.ok)return;const ie=(await he.json().catch(()=>null))?.data?.data;if(!te&&Array.isArray(ie)){const _e=[];for(const ge of ie){if(!ge||typeof ge!="object")continue;const Ae=ge.identifier;if(typeof Ae!="string"||!Ae||ge.is_pending===!0)continue;const Le=ge.type==="admin"?"admin":"user",q=ge.name,ae=ge.version;_e.push({identifier:Ae,name:typeof q=="string"?q:Ae,version:typeof ae=="string"?ae:"",type:Le})}S(_e)}}catch{}})(),()=>{te=!0}},[e]);const T=g?.nesting??null,$=t.selectedRoute?.path??null,A=t.editMode,z=t.selectedRoute?.layoutName??(c?a.document?.hostLayoutName??null:null),D=t.routeTree,O=u.useMemo(()=>{if(A!=="extension"||!z)return null;const te=bl(D,z);return te||`__base__/${z}`},[A,z,D]),M=c&&t.returnRoute===null&&!!z;u.useEffect(()=>{if(!M||!z)return;const te=bl(D,z);te&&n({type:"SET_RETURN_ROUTE",route:{path:te,layoutName:z}})},[M,z,D,n]),u.useEffect(()=>{const te=G4(A,$,O),he=Y4(g?.states?.groups,te);n({type:"SET_AVAILABLE_STATES",states:he,activeStateId:X4(he)})},[g,A,$,O,n]);const[B,N]=u.useState([]);u.useEffect(()=>{let te=!1;return(async()=>{const he=await Hz(e);te||N(he)})(),()=>{te=!0}},[e]),yD(),bD();const H=u.useCallback(te=>{const he=t.routeTree,Q=te.split("?")[0]?.split("#")[0]??te;return Kz(he,Q)?"route_in_tree":"route_not_in_tree"},[t.routeTree]),L=u.useCallback(te=>{const he=te.split("?")[0]?.split("#")[0]??te,Q=yl(t.routeTree,he);Q&&(p.isDirty&&!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.dirty_guard.message")))||n({type:"SELECT_ROUTE",route:{path:Q.path,layoutName:Q.layoutName}}))},[t.routeTree,p.isDirty,n]),I=u.useRef([]),P=u.useCallback(te=>{I.current=te},[]),[F,G]=u.useState(null),V=u.useCallback(()=>G(null),[]),Y=u.useRef(null),Z=u.useRef(null);u.useEffect(()=>{const te=Z.current,he=Y.current;if(!te||!he||typeof ResizeObserver>"u")return;const Q=()=>{he.style.setProperty("--g7le-header-h",`${Math.round(te.getBoundingClientRect().height)}px`)};Q();const ie=new ResizeObserver(Q);return ie.observe(te),()=>ie.disconnect()},[]);const se=u.useCallback(()=>{p.reload()},[p]),re=u.useCallback(()=>{p.isDirty&&!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.dirty_guard.message")))||window.G7Core?.dispatch?.({handler:"openWindow",params:{path:`/admin/templates/${b}`,target:"_self"}})},[p.isDirty,o,b]),U=u.useCallback(te=>{if(!te||te===e||p.isDirty&&!(typeof window<"u"?window.confirm(o("layout_editor.chrome.dirty_guard.message")):!0))return;const he=Zs(te);window.G7Core?.dispatch?.({handler:"openWindow",params:{path:he,target:"_self"}})},[e,p.isDirty,o]),ee=u.useCallback(()=>{const te=ME(e,t.selectedRoute?.path??null);window.G7Core?.dispatch?.({handler:"openWindow",params:{path:te}})},[e,t.selectedRoute]),fe=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"";if(!te)return;const he=r.open({ariaLabel:o("layout_editor.attachment_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(Ml,{templateIdentifier:e,layoutName:te,t:o,onClose:()=>r.close(he)})})},[r,e,t.selectedRoute,o]),K=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"";if(!te)return;const he=Hy(s.document?.raw??{}),Q=r.open({ariaLabel:o("layout_editor.translation_manager.title"),width:720,maxHeightRatio:.82,content:u.createElement(dm,{templateIdentifier:e,layoutName:te,t:o,referencedKeys:he,onClose:()=>r.close(Q)})})},[r,e,t.selectedRoute,o,s.document]),ne=u.useCallback((te,he)=>{const Q=window.G7Core;if(Q&&typeof Q.t=="function"){const ie=Q.t(te,he);if(ie&&ie!==te&&!ie.startsWith("$t:"))return ie}return o(te,he)},[o]),me=u.useCallback(()=>{if(!t.selectedRoute||t.editMode==="extension"||t.editMode==="iteration_item")return;const te=r.open({ariaLabel:o("layout_editor.page_settings.title",{name:t.selectedRoute.path??""}),width:1080,maxHeightRatio:.88,closeOnEscape:!1,content:u.createElement(u$,{templateIdentifier:e,spec:g,t:o,resolveLabel:ne,permissionCandidates:B,onClose:()=>r.close(te)})})},[r,t.selectedRoute,t.editMode,o,ne,e,g,B]),Ce=u.useCallback(async()=>{const te=await d.createPreview(s.document?.raw??null);if(te.kind!=="success"){const he=te.kind==="network_error"?te.message:o("layout_editor.preview_action.no_document");typeof window<"u"&&window.alert(he)}},[d,s.document,o]),ze=u.useCallback(()=>{const te=t.selectedRoute?.layoutName??"",he=t.selectedRoute?.path??"",Q=t.editMode==="extension"&&he.startsWith("__extension__/")?he.slice(14):null,ie=Q?{kind:"extension",extensionId:Q}:te?{kind:"layout",layoutName:te}:null;if(!ie)return;const _e=r.open({ariaLabel:o("layout_editor.version_history.title"),width:640,maxHeightRatio:.82,content:u.createElement(X$,{templateIdentifier:e,target:ie,t:o,onRestored:async ge=>{typeof ge=="number"&&(ie.kind==="extension"?n({type:"SET_EXTENSION_VERSION",extensionId:ie.extensionId,version:ge}):n({type:"SET_LAYOUT_VERSION",layoutName:ie.layoutName,version:ge})),await p.reload()},onClose:()=>r.close(_e)})})},[r,e,t.selectedRoute,t.editMode,o,p,n]),Se=p.dirtyKeys.size>0||p.isDirty;u.useEffect(()=>{if(!Se)return;const te=he=>{he.preventDefault(),he.returnValue=""};return window.addEventListener("beforeunload",te),()=>window.removeEventListener("beforeunload",te)},[Se]);const qe=u.useCallback(()=>{!p.isDirty||!(!(typeof window<"u")||window.confirm(o("layout_editor.chrome.reset_guard.message")))||p.reload()},[p,o]),De=u.useCallback(async()=>{const te=await p.save({sessionAddedPaths:I.current});G(te),te.kind==="success"&&typeof te.newContentVersion=="number"&&(te.savedExtensionId?n({type:"SET_EXTENSION_VERSION",extensionId:te.savedExtensionId,version:te.newContentVersion}):te.savedLayoutName&&n({type:"SET_LAYOUT_VERSION",layoutName:te.savedLayoutName,version:te.newContentVersion}))},[p,n]),Pe=u.useCallback(()=>{const te=r.open({ariaLabel:o("layout_editor.shortcuts.title"),width:560,maxHeightRatio:.85,content:u.createElement(iE,{t:o,onClose:()=>r.close(te)})})},[r,o]),Qe=u.useMemo(()=>({save:()=>{De()},exit:re,editCode:ee,reset:qe,translations:K,addElement:()=>{t.selectedRoute&&n({type:"TOGGLE_PALETTE"})},help:Pe,preview:()=>{t.selectedRoute?.layoutName&&Ce()}}),[De,re,ee,qe,K,t.selectedRoute,n,Pe,Ce]),[Me,Re]=u.useState(!1);u.useEffect(()=>{const te=window.setInterval(()=>{Re(!!window.__g7LayoutEditorHasSelection)},200);return()=>window.clearInterval(te)},[]),np({handlers:Qe,hasSelection:Me});const ve=p;return t.routesError?i.jsx("div",{className:"g7le-chrome g7le-chrome--routes-error","data-testid":"g7le-chrome-routes-error",style:{position:"fixed",inset:0,display:"flex",background:"#f8fafc",color:"#0f172a",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',fontSize:13,lineHeight:1.5,zIndex:9e3},children:i.jsx(Fa,{error:t.routesError})}):i.jsx("div",{className:"g7le-chrome","data-testid":"g7le-chrome",ref:Y,style:{position:"static",minHeight:"100vh",minWidth:jz,background:"#f8fafc",color:"#0f172a",fontFamily:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Apple SD Gothic Neo", "Noto Sans KR", sans-serif',fontSize:13,lineHeight:1.5,zIndex:9e3},children:i.jsxs(dy,{value:ve,children:[c&&a.needsHostPicker&&a.document&&i.jsx(SD,{hostLayouts:a.document.hostLayouts,extensionLabel:null,onSelect:a.selectHost}),i.jsxs("div",{className:"g7le-chrome__header","data-testid":"g7le-chrome-header",ref:Z,style:{position:"sticky",top:0,zIndex:20,background:"#f8fafc"},children:[i.jsx(by,{onSave:()=>{De()},isDirty:p.isDirty,onExit:re,onEditCode:ee,onManageImages:fe,onManageTranslations:K,onPageSettings:me,onReset:qe,onShowShortcuts:Pe,onPreview:Ce,onShowVersions:ze,templateName:k,templateVersion:_,templateList:E,onSwitchTemplate:U}),i.jsx(xE,{result:F,onDismiss:V,onLoadLatest:se}),i.jsxs("div",{className:"g7le-chrome__device-toolbar","data-testid":"g7le-chrome-device-toolbar",style:{display:"flex",justifyContent:"center",padding:"6px 12px",background:"#f1f5f9",borderBottom:"1px solid #e2e8f0"},children:[i.jsx(U4,{}),i.jsx(q4,{})]})]}),i.jsxs("div",{className:"g7le-chrome__body",style:{display:"grid",gridTemplateColumns:"auto 1fr",alignItems:"stretch"},children:[i.jsx(HE,{}),i.jsx(j4,{manifest:f,nesting:T,componentPalette:g?.componentPalette??null,spec:g,permissionCandidates:B,sampleGlobalSources:h,resolveRouteMatch:H,onNavigateToDestination:L,onSessionAddedPathsChange:P})]}),i.jsx(py,{})]})})}function yl(e,t){for(const n of e){if(n.kind==="route"&&n.path===t)return n;if(n.children&&n.children.length>0){const o=yl(n.children,t);if(o)return o}}return null}function Kz(e,t){return yl(e,t)!==null}function bl(e,t){for(const n of e){if(n.kind==="route"&&n.layoutName===t&&typeof n.path=="string")return n.path;if(n.children&&n.children.length>0){const o=bl(n.children,t);if(o)return o}}return null}function Vz(e,t,n){const o=[];Va({sources:n,isGuestOnly:!1,warn:s=>{const a=/extension '([^']+)' override of core key '([^']+)'/.exec(s);a&&o.push(`${a[1]}:${a[2]}`)}});const r=t?.sampleData?.byDataSourceId?Object.keys(t.sampleData.byDataSourceId).length:0;Qh({templateIdentifier:e,mergedSources:[...n.map(s=>({kind:s.kind,id:s.id}))],blockCounts:{controls:t?.controls?Object.keys(t.controls).length:0,componentCapabilities:t?.componentCapabilities?Object.keys(t.componentCapabilities).length:0,actionRecipes:t?.actionRecipes?Object.keys(t.actionRecipes).length:0,conditionRecipes:t?.conditionRecipes?Object.keys(t.conditionRecipes).length:0,paletteGroups:t?.componentPalette?.groups?.length??0,stateGroups:t?.states?.groups?.length??0,sampleDataIds:r},sampleGlobalSourceCount:n.length,sampleGlobalConflicts:o,timestamp:Date.now()})}function Oh({templateIdentifier:e,initialLocale:t}){return u.useEffect(()=>()=>{Vh(),qh(),Yh(),Xh(),Jh(),Zh(),Rh(),ey(),ty()},[]),i.jsx(ly,{templateIdentifier:e,initialLocale:t,children:i.jsx(uy,{children:i.jsx(Fz,{templateIdentifier:e})})})}if(typeof window<"u"){const e=window.G7Core||(window.G7Core={});if(e.__LayoutEditorChrome=Oh,typeof e.__onChromeReady=="function")try{e.__onChromeReady()}catch{}}dn.LayoutEditorChrome=Oh,Object.defineProperty(dn,Symbol.toStringTag,{value:"Module"})})(this.G7LayoutEditor=this.G7LayoutEditor||{},ReactJSXRuntime,React,ReactDOM); diff --git a/public/build/core/template-engine.min.js b/public/build/core/template-engine.min.js index 06a956a5..6b5429d3 100644 --- a/public/build/core/template-engine.min.js +++ b/public/build/core/template-engine.min.js @@ -1,57 +1,23 @@ -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},Ke.cloneElement=function(H,T,ge){if(H==null)throw Error("The argument must be a React element, but you passed "+H+".");var de=A({},H.props),me=H.key;if(T!=null)for(ae in T.key!==void 0&&(me=""+T.key),T)!K.call(T,ae)||ae==="key"||ae==="__self"||ae==="__source"||ae==="ref"&&T.ref===void 0||(de[ae]=T[ae]);var ae=arguments.length-2;if(ae===1)de.children=ge;else if(1<ae){for(var Re=Array(ae),ze=0;ze<ae;ze++)Re[ze]=arguments[ze+2];de.children=Re}return fe(H.type,me,de)},Ke.createContext=function(H){return H={$$typeof:c,_currentValue:H,_currentValue2:H,_threadCount:0,Provider:null,Consumer:null},H.Provider=H,H.Consumer={$$typeof:l,_context:H},H},Ke.createElement=function(H,T,ge){var de,me={},ae=null;if(T!=null)for(de in T.key!==void 0&&(ae=""+T.key),T)K.call(T,de)&&de!=="key"&&de!=="__self"&&de!=="__source"&&(me[de]=T[de]);var Re=arguments.length-2;if(Re===1)me.children=ge;else if(1<Re){for(var ze=Array(Re),$e=0;$e<Re;$e++)ze[$e]=arguments[$e+2];me.children=ze}if(H&&H.defaultProps)for(de in Re=H.defaultProps,Re)me[de]===void 0&&(me[de]=Re[de]);return fe(H,ae,me)},Ke.createRef=function(){return{current:null}},Ke.forwardRef=function(H){return{$$typeof:d,render:H}},Ke.isValidElement=ke,Ke.lazy=function(H){return{$$typeof:m,_payload:{_status:-1,_result:H},_init:Ee}},Ke.memo=function(H,T){return{$$typeof:g,type:H,compare:T===void 0?null:T}},Ke.startTransition=function(H){var T=B.T,ge={};B.T=ge;try{var de=H(),me=B.S;me!==null&&me(ge,de),typeof de=="object"&&de!==null&&typeof de.then=="function"&&de.then(G,Z)}catch(ae){Z(ae)}finally{T!==null&&ge.types!==null&&(T.types=ge.types),B.T=T}},Ke.unstable_useCacheRefresh=function(){return B.H.useCacheRefresh()},Ke.use=function(H){return B.H.use(H)},Ke.useActionState=function(H,T,ge){return B.H.useActionState(H,T,ge)},Ke.useCallback=function(H,T){return B.H.useCallback(H,T)},Ke.useContext=function(H){return B.H.useContext(H)},Ke.useDebugValue=function(){},Ke.useDeferredValue=function(H,T){return B.H.useDeferredValue(H,T)},Ke.useEffect=function(H,T){return B.H.useEffect(H,T)},Ke.useEffectEvent=function(H){return B.H.useEffectEvent(H)},Ke.useId=function(){return B.H.useId()},Ke.useImperativeHandle=function(H,T,ge){return B.H.useImperativeHandle(H,T,ge)},Ke.useInsertionEffect=function(H,T){return B.H.useInsertionEffect(H,T)},Ke.useLayoutEffect=function(H,T){return B.H.useLayoutEffect(H,T)},Ke.useMemo=function(H,T){return B.H.useMemo(H,T)},Ke.useOptimistic=function(H,T){return B.H.useOptimistic(H,T)},Ke.useReducer=function(H,T,ge){return B.H.useReducer(H,T,ge)},Ke.useRef=function(H){return B.H.useRef(H)},Ke.useState=function(H){return B.H.useState(H)},Ke.useSyncExternalStore=function(H,T,ge){return B.H.useSyncExternalStore(H,T,ge)},Ke.useTransition=function(){return B.H.useTransition()},Ke.version="19.2.0",Ke}var Mg;function Qu(){return Mg||(Mg=1,Ju.exports=IS()),Ju.exports}var N=Qu();const We=Qr(N);var Zu={exports:{}},Qs={},ed={exports:{}},td={};/** - * @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` -`+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` +var zT=Object.defineProperty;var UT=(Xt,Qr,Zr)=>Qr in Xt?zT(Xt,Qr,{enumerable:!0,configurable:!0,writable:!0,value:Zr}):Xt[Qr]=Zr;var $=(Xt,Qr,Zr)=>UT(Xt,typeof Qr!="symbol"?Qr+"":Qr,Zr);(function(Xt){"use strict";function Qr(s,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 s)){const l=Object.getOwnPropertyDescriptor(a,i);l&&Object.defineProperty(s,i,l.get?l:{enumerable:!0,get:()=>a[i]})}}}return Object.freeze(Object.defineProperty(s,Symbol.toStringTag,{value:"Module"}))}function Zr(s){return s&&s.__esModule&&Object.prototype.hasOwnProperty.call(s,"default")?s.default:s}var nd={exports:{}},We={};var rg;function dw(){if(rg)return We;rg=1;var s=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"),h=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 k(H,T,ce){this.props=H,this.context=T,this.refs=x,this.updater=ce||_}var O=k.prototype=new M;O.constructor=k,A(O,L.prototype),O.isPureReactComponent=!0;var B=Array.isArray;function G(){}var P={H:null,A:null,T:null,S:null},W=Object.prototype.hasOwnProperty;function pe(H,T,ce){var de=ce.ref;return{$$typeof:s,type:H,key:T,ref:de!==void 0?de:null,props:ce}}function Se(H,T){return pe(H.type,T,H.props)}function we(H){return typeof H=="object"&&H!==null&&H.$$typeof===s}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 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,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 s: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&&(we(ye)&&(ye=Se(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(wt(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)},ge={map:fe,forEach:function(H,T,ce){fe(H,function(){T.apply(this,arguments)},ce)},count:function(H){var T=0;return fe(H,function(){T++}),T},toArray:function(H){return fe(H,function(T){return T})||[]},only:function(H){if(!we(H))throw Error("React.Children.only expected to receive a single React element child.");return H}};return We.Activity=b,We.Children=ge,We.Component=L,We.Fragment=n,We.Profiler=i,We.PureComponent=k,We.StrictMode=a,We.Suspense=f,We.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=P,We.__COMPILER_RUNTIME={__proto__:null,c:function(H){return P.H.useMemoCache(H)}},We.cache=function(H){return function(){return H.apply(null,arguments)}},We.cacheSignal=function(){return null},We.cloneElement=function(H,T,ce){if(H==null)throw Error("The argument must be a React element, but you passed "+H+".");var de=A({},H.props),ye=H.key;if(T!=null)for(ie in T.key!==void 0&&(ye=""+T.key),T)!W.call(T,ie)||ie==="key"||ie==="__self"||ie==="__source"||ie==="ref"&&T.ref===void 0||(de[ie]=T[ie]);var ie=arguments.length-2;if(ie===1)de.children=ce;else if(1<ie){for(var xe=Array(ie),_e=0;_e<ie;_e++)xe[_e]=arguments[_e+2];de.children=xe}return pe(H.type,ye,de)},We.createContext=function(H){return H={$$typeof:c,_currentValue:H,_currentValue2:H,_threadCount:0,Provider:null,Consumer:null},H.Provider=H,H.Consumer={$$typeof:l,_context:H},H},We.createElement=function(H,T,ce){var de,ye={},ie=null;if(T!=null)for(de in T.key!==void 0&&(ie=""+T.key),T)W.call(T,de)&&de!=="key"&&de!=="__self"&&de!=="__source"&&(ye[de]=T[de]);var xe=arguments.length-2;if(xe===1)ye.children=ce;else if(1<xe){for(var _e=Array(xe),Te=0;Te<xe;Te++)_e[Te]=arguments[Te+2];ye.children=_e}if(H&&H.defaultProps)for(de in xe=H.defaultProps,xe)ye[de]===void 0&&(ye[de]=xe[de]);return pe(H,ie,ye)},We.createRef=function(){return{current:null}},We.forwardRef=function(H){return{$$typeof:d,render:H}},We.isValidElement=we,We.lazy=function(H){return{$$typeof:m,_payload:{_status:-1,_result:H},_init:Le}},We.memo=function(H,T){return{$$typeof:h,type:H,compare:T===void 0?null:T}},We.startTransition=function(H){var T=P.T,ce={};P.T=ce;try{var de=H(),ye=P.S;ye!==null&&ye(ce,de),typeof de=="object"&&de!==null&&typeof de.then=="function"&&de.then(G,Z)}catch(ie){Z(ie)}finally{T!==null&&ce.types!==null&&(T.types=ce.types),P.T=T}},We.unstable_useCacheRefresh=function(){return P.H.useCacheRefresh()},We.use=function(H){return P.H.use(H)},We.useActionState=function(H,T,ce){return P.H.useActionState(H,T,ce)},We.useCallback=function(H,T){return P.H.useCallback(H,T)},We.useContext=function(H){return P.H.useContext(H)},We.useDebugValue=function(){},We.useDeferredValue=function(H,T){return P.H.useDeferredValue(H,T)},We.useEffect=function(H,T){return P.H.useEffect(H,T)},We.useEffectEvent=function(H){return P.H.useEffectEvent(H)},We.useId=function(){return P.H.useId()},We.useImperativeHandle=function(H,T,ce){return P.H.useImperativeHandle(H,T,ce)},We.useInsertionEffect=function(H,T){return P.H.useInsertionEffect(H,T)},We.useLayoutEffect=function(H,T){return P.H.useLayoutEffect(H,T)},We.useMemo=function(H,T){return P.H.useMemo(H,T)},We.useOptimistic=function(H,T){return P.H.useOptimistic(H,T)},We.useReducer=function(H,T,ce){return P.H.useReducer(H,T,ce)},We.useRef=function(H){return P.H.useRef(H)},We.useState=function(H){return P.H.useState(H)},We.useSyncExternalStore=function(H,T,ce){return P.H.useSyncExternalStore(H,T,ce)},We.useTransition=function(){return P.H.useTransition()},We.version="19.2.6",We}var ag;function rd(){return ag||(ag=1,nd.exports=dw()),nd.exports}var N=rd();const Ye=Zr(N);var ad={exports:{}},Zs={},id={exports:{}},sd={};var ig;function fw(){return ig||(ig=1,(function(s){function e(J,fe){var Le=J.length;J.push(fe);e:for(;0<Le;){var Z=Le-1>>>1,ge=J[Z];if(0<i(ge,fe))J[Z]=fe,J[Le]=ge,Le=Z;else break e}}function n(J){return J.length===0?null:J[0]}function a(J){if(J.length===0)return null;var fe=J[0],Le=J.pop();if(Le!==fe){J[0]=Le;e:for(var Z=0,ge=J.length,H=ge>>>1;Z<H;){var T=2*(Z+1)-1,ce=J[T],de=T+1,ye=J[de];if(0>i(ce,Le))de<ge&&0>i(ye,ce)?(J[Z]=ye,J[de]=Le,Z=de):(J[Z]=ce,J[T]=Le,Z=T);else if(de<ge&&0>i(ye,Le))J[Z]=ye,J[de]=Le,Z=de;else break e}}return fe}function i(J,fe){var Le=J.sortIndex-fe.sortIndex;return Le!==0?Le:J.id-fe.id}if(s.unstable_now=void 0,typeof performance=="object"&&typeof performance.now=="function"){var l=performance;s.unstable_now=function(){return l.now()}}else{var c=Date,d=c.now();s.unstable_now=function(){return c.now()-d}}var f=[],h=[],m=1,b=null,S=3,v=!1,_=!1,A=!1,x=!1,L=typeof setTimeout=="function"?setTimeout:null,M=typeof clearTimeout=="function"?clearTimeout:null,k=typeof setImmediate<"u"?setImmediate:null;function O(J){for(var fe=n(h);fe!==null;){if(fe.callback===null)a(h);else if(fe.startTime<=J)a(h),fe.sortIndex=fe.expirationTime,e(f,fe);else break;fe=n(h)}}function B(J){if(A=!1,O(J),!_)if(n(f)!==null)_=!0,G||(G=!0,Ue());else{var fe=n(h);fe!==null&&wt(B,fe.startTime-J)}}var G=!1,P=-1,W=5,pe=-1;function Se(){return x?!0:!(s.unstable_now()-pe<W)}function we(){if(x=!1,G){var J=s.unstable_now();pe=J;var fe=!0;try{e:{_=!1,A&&(A=!1,M(P),P=-1),v=!0;var Le=S;try{t:{for(O(J),b=n(f);b!==null&&!(b.expirationTime>J&&Se());){var Z=b.callback;if(typeof Z=="function"){b.callback=null,S=b.priorityLevel;var ge=Z(b.expirationTime<=J);if(J=s.unstable_now(),typeof ge=="function"){b.callback=ge,O(J),fe=!0;break t}b===n(f)&&a(f),O(J)}else a(f);b=n(f)}if(b!==null)fe=!0;else{var H=n(h);H!==null&&wt(B,H.startTime-J),fe=!1}}break e}finally{b=null,S=Le,v=!1}fe=void 0}}finally{fe?Ue():G=!1}}}var Ue;if(typeof k=="function")Ue=function(){k(we)};else if(typeof MessageChannel<"u"){var Ve=new MessageChannel,qe=Ve.port2;Ve.port1.onmessage=we,Ue=function(){qe.postMessage(null)}}else Ue=function(){L(we,0)};function wt(J,fe){P=L(function(){J(s.unstable_now())},fe)}s.unstable_IdlePriority=5,s.unstable_ImmediatePriority=1,s.unstable_LowPriority=4,s.unstable_NormalPriority=3,s.unstable_Profiling=null,s.unstable_UserBlockingPriority=2,s.unstable_cancelCallback=function(J){J.callback=null},s.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"):W=0<J?Math.floor(1e3/J):5},s.unstable_getCurrentPriorityLevel=function(){return S},s.unstable_next=function(J){switch(S){case 1:case 2:case 3:var fe=3;break;default:fe=S}var Le=S;S=fe;try{return J()}finally{S=Le}},s.unstable_requestPaint=function(){x=!0},s.unstable_runWithPriority=function(J,fe){switch(J){case 1:case 2:case 3:case 4:case 5:break;default:J=3}var Le=S;S=J;try{return fe()}finally{S=Le}},s.unstable_scheduleCallback=function(J,fe,Le){var Z=s.unstable_now();switch(typeof Le=="object"&&Le!==null?(Le=Le.delay,Le=typeof Le=="number"&&0<Le?Z+Le:Z):Le=Z,J){case 1:var ge=-1;break;case 2:ge=250;break;case 5:ge=1073741823;break;case 4:ge=1e4;break;default:ge=5e3}return ge=Le+ge,J={id:m++,callback:fe,priorityLevel:J,startTime:Le,expirationTime:ge,sortIndex:-1},Le>Z?(J.sortIndex=Le,e(h,J),n(f)===null&&J===n(h)&&(A?(M(P),P=-1):A=!0,wt(B,Le-Z))):(J.sortIndex=ge,e(f,J),_||v||(_=!0,G||(G=!0,Ue()))),J},s.unstable_shouldYield=Se,s.unstable_wrapCallback=function(J){var fe=S;return function(){var Le=S;S=fe;try{return J.apply(this,arguments)}finally{S=Le}}}})(sd)),sd}var sg;function hw(){return sg||(sg=1,id.exports=fw()),id.exports}var od={exports:{}},Cn={};var og;function pw(){if(og)return Cn;og=1;var s=rd();function e(f){var h="https://react.dev/errors/"+f;if(1<arguments.length){h+="?args[]="+encodeURIComponent(arguments[1]);for(var m=2;m<arguments.length;m++)h+="&args[]="+encodeURIComponent(arguments[m])}return"Minified React error #"+f+"; visit "+h+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}function n(){}var a={d:{f:n,r:function(){throw Error(e(522))},D:n,C:n,L:n,m:n,X:n,S:n,M:n},p:0,findDOMNode:null},i=Symbol.for("react.portal");function l(f,h,m){var b=3<arguments.length&&arguments[3]!==void 0?arguments[3]:null;return{$$typeof:i,key:b==null?null:""+b,children:f,containerInfo:h,implementation:m}}var c=s.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;function d(f,h){if(f==="font")return"";if(typeof h=="string")return h==="use-credentials"?h:""}return Cn.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE=a,Cn.createPortal=function(f,h){var m=2<arguments.length&&arguments[2]!==void 0?arguments[2]:null;if(!h||h.nodeType!==1&&h.nodeType!==9&&h.nodeType!==11)throw Error(e(299));return l(f,h,null,m)},Cn.flushSync=function(f){var h=c.T,m=a.p;try{if(c.T=null,a.p=2,f)return f()}finally{c.T=h,a.p=m,a.d.f()}},Cn.preconnect=function(f,h){typeof f=="string"&&(h?(h=h.crossOrigin,h=typeof h=="string"?h==="use-credentials"?h:"":void 0):h=null,a.d.C(f,h))},Cn.prefetchDNS=function(f){typeof f=="string"&&a.d.D(f)},Cn.preinit=function(f,h){if(typeof f=="string"&&h&&typeof h.as=="string"){var m=h.as,b=d(m,h.crossOrigin),S=typeof h.integrity=="string"?h.integrity:void 0,v=typeof h.fetchPriority=="string"?h.fetchPriority:void 0;m==="style"?a.d.S(f,typeof h.precedence=="string"?h.precedence:void 0,{crossOrigin:b,integrity:S,fetchPriority:v}):m==="script"&&a.d.X(f,{crossOrigin:b,integrity:S,fetchPriority:v,nonce:typeof h.nonce=="string"?h.nonce:void 0})}},Cn.preinitModule=function(f,h){if(typeof f=="string")if(typeof h=="object"&&h!==null){if(h.as==null||h.as==="script"){var m=d(h.as,h.crossOrigin);a.d.M(f,{crossOrigin:m,integrity:typeof h.integrity=="string"?h.integrity:void 0,nonce:typeof h.nonce=="string"?h.nonce:void 0})}}else h==null&&a.d.M(f)},Cn.preload=function(f,h){if(typeof f=="string"&&typeof h=="object"&&h!==null&&typeof h.as=="string"){var m=h.as,b=d(m,h.crossOrigin);a.d.L(f,m,{crossOrigin:b,integrity:typeof h.integrity=="string"?h.integrity:void 0,nonce:typeof h.nonce=="string"?h.nonce:void 0,type:typeof h.type=="string"?h.type:void 0,fetchPriority:typeof h.fetchPriority=="string"?h.fetchPriority:void 0,referrerPolicy:typeof h.referrerPolicy=="string"?h.referrerPolicy:void 0,imageSrcSet:typeof h.imageSrcSet=="string"?h.imageSrcSet:void 0,imageSizes:typeof h.imageSizes=="string"?h.imageSizes:void 0,media:typeof h.media=="string"?h.media:void 0})}},Cn.preloadModule=function(f,h){if(typeof f=="string")if(h){var m=d(h.as,h.crossOrigin);a.d.m(f,{as:typeof h.as=="string"&&h.as!=="script"?h.as:void 0,crossOrigin:m,integrity:typeof h.integrity=="string"?h.integrity:void 0})}else a.d.m(f)},Cn.requestFormReset=function(f){a.d.r(f)},Cn.unstable_batchedUpdates=function(f,h){return f(h)},Cn.useFormState=function(f,h,m){return c.H.useFormState(f,h,m)},Cn.useFormStatus=function(){return c.H.useHostTransitionStatus()},Cn.version="19.2.6",Cn}var lg;function cg(){if(lg)return od.exports;lg=1;function s(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(e){console.error(e)}}return s(),od.exports=pw(),od.exports}var ug;function gw(){if(ug)return Zs;ug=1;var s=hw(),e=rd(),n=cg();function a(t){var r="https://react.dev/errors/"+t;if(1<arguments.length){r+="?args[]="+encodeURIComponent(arguments[1]);for(var o=2;o<arguments.length;o++)r+="&args[]="+encodeURIComponent(arguments[o])}return"Minified React error #"+t+"; visit "+r+" 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 r=t,o=t;if(t.alternate)for(;r.return;)r=r.return;else{t=r;do r=t,(r.flags&4098)!==0&&(o=r.return),t=r.return;while(t)}return r.tag===3?o:null}function c(t){if(t.tag===13){var r=t.memoizedState;if(r===null&&(t=t.alternate,t!==null&&(r=t.memoizedState)),r!==null)return r.dehydrated}return null}function d(t){if(t.tag===31){var r=t.memoizedState;if(r===null&&(t=t.alternate,t!==null&&(r=t.memoizedState)),r!==null)return r.dehydrated}return null}function f(t){if(l(t)!==t)throw Error(a(188))}function h(t){var r=t.alternate;if(!r){if(r=l(t),r===null)throw Error(a(188));return r!==t?null:t}for(var o=t,u=r;;){var p=o.return;if(p===null)break;var g=p.alternate;if(g===null){if(u=p.return,u!==null){o=u;continue}break}if(p.child===g.child){for(g=p.child;g;){if(g===o)return f(p),t;if(g===u)return f(p),r;g=g.sibling}throw Error(a(188))}if(o.return!==u.return)o=p,u=g;else{for(var w=!1,R=p.child;R;){if(R===o){w=!0,o=p,u=g;break}if(R===u){w=!0,u=p,o=g;break}R=R.sibling}if(!w){for(R=g.child;R;){if(R===o){w=!0,o=g,u=p;break}if(R===u){w=!0,u=g,o=p;break}R=R.sibling}if(!w)throw Error(a(189))}}if(o.alternate!==u)throw Error(a(190))}if(o.tag!==3)throw Error(a(188));return o.stateNode.current===o?t:r}function m(t){var r=t.tag;if(r===5||r===26||r===27||r===6)return t;for(t=t.child;t!==null;){if(r=m(t),r!==null)return r;t=t.sibling}return null}var b=Object.assign,S=Symbol.for("react.element"),v=Symbol.for("react.transitional.element"),_=Symbol.for("react.portal"),A=Symbol.for("react.fragment"),x=Symbol.for("react.strict_mode"),L=Symbol.for("react.profiler"),M=Symbol.for("react.consumer"),k=Symbol.for("react.context"),O=Symbol.for("react.forward_ref"),B=Symbol.for("react.suspense"),G=Symbol.for("react.suspense_list"),P=Symbol.for("react.memo"),W=Symbol.for("react.lazy"),pe=Symbol.for("react.activity"),Se=Symbol.for("react.memo_cache_sentinel"),we=Symbol.iterator;function Ue(t){return t===null||typeof t!="object"?null:(t=we&&t[we]||t["@@iterator"],typeof t=="function"?t:null)}var Ve=Symbol.for("react.client.reference");function qe(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===Ve?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case A:return"Fragment";case L:return"Profiler";case x:return"StrictMode";case B:return"Suspense";case G:return"SuspenseList";case pe:return"Activity"}if(typeof t=="object")switch(t.$$typeof){case _:return"Portal";case k:return t.displayName||"Context";case M:return(t._context.displayName||"Context")+".Consumer";case O:var r=t.render;return t=t.displayName,t||(t=r.displayName||r.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case P:return r=t.displayName||null,r!==null?r:qe(t.type)||"Memo";case W:r=t._payload,t=t._init;try{return qe(t(r))}catch{}}return null}var wt=Array.isArray,J=e.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,fe=n.__DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,Le={pending:!1,data:null,method:null,action:null},Z=[],ge=-1;function H(t){return{current:t}}function T(t){0>ge||(t.current=Z[ge],Z[ge]=null,ge--)}function ce(t,r){ge++,Z[ge]=t.current,t.current=r}var de=H(null),ye=H(null),ie=H(null),xe=H(null);function _e(t,r){switch(ce(ie,r),ce(ye,t),ce(de,null),r.nodeType){case 9:case 11:t=(t=r.documentElement)&&(t=t.namespaceURI)?OS(t):0;break;default:if(t=r.tagName,r=r.namespaceURI)r=OS(r),t=LS(r,t);else switch(t){case"svg":t=1;break;case"math":t=2;break;default:t=0}}T(de),ce(de,t)}function Te(){T(de),T(ye),T(ie)}function $e(t){t.memoizedState!==null&&ce(xe,t);var r=de.current,o=LS(r,t.type);r!==o&&(ce(ye,t),ce(de,o))}function pt(t){ye.current===t&&(T(de),T(ye)),xe.current===t&&(T(xe),Ol._currentValue=Le)}var Ct,Dt;function ht(t){if(Ct===void 0)try{throw Error()}catch(o){var r=o.stack.trim().match(/\n( *(at )?)/);Ct=r&&r[1]||"",Dt=-1<o.stack.indexOf(` + at`)?" (<anonymous>)":-1<o.stack.indexOf("@")?"@unknown:0:0":""}return` +`+Ct+t+Dt}var rn=!1;function Kt(t,r){if(!t||rn)return"";rn=!0;var o=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 p=Object.getOwnPropertyDescriptor(u.DetermineComponentFrameRoot,"name");p&&p.configurable&&Object.defineProperty(u.DetermineComponentFrameRoot,"name",{value:"DetermineComponentFrameRoot"});var g=u.DetermineComponentFrameRoot(),w=g[0],R=g[1];if(w&&R){var z=w.split(` +`),X=R.split(` +`);for(p=u=0;u<z.length&&!z[u].includes("DetermineComponentFrameRoot");)u++;for(;p<X.length&&!X[p].includes("DetermineComponentFrameRoot");)p++;if(u===z.length||p===X.length)for(u=z.length-1,p=X.length-1;1<=u&&0<=p&&z[u]!==X[p];)p--;for(;1<=u&&0<=p;u--,p--)if(z[u]!==X[p]){if(u!==1||p!==1)do if(u--,p--,0>p||z[u]!==X[p]){var re=` +`+z[u].replace(" at new "," at ");return t.displayName&&re.includes("<anonymous>")&&(re=re.replace("<anonymous>",t.displayName)),re}while(1<=u&&0<=p);break}}}finally{rn=!1,Error.prepareStackTrace=o}return(o=t?t.displayName||t.name:"")?ht(o):""}function It(t,r){switch(t.tag){case 26:case 27:case 5:return ht(t.type);case 16:return ht("Lazy");case 13:return t.child!==r&&r!==null?ht("Suspense Fallback"):ht("Suspense");case 19:return ht("SuspenseList");case 0:case 15:return Kt(t.type,!1);case 11:return Kt(t.type.render,!1);case 1:return Kt(t.type,!0);case 31:return ht("Activity");default:return""}}function qr(t){try{var r="",o=null;do r+=It(t,o),o=t,t=t.return;while(t);return r}catch(u){return` Error generating stack: `+u.message+` -`+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-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$"),Ho={},Gr={};function jo(t){return wn.call(Gr,t)?!0:wn.call(Ho,t)?!1:kr.test(t)?Gr[t]=!0:(Ho[t]=!0,!1)}function gs(t,n,s){if(jo(n))if(s===null)t.removeAttribute(n);else{switch(typeof s){case"undefined":case"function":case"symbol":t.removeAttribute(n);return;case"boolean":var u=n.toLowerCase().slice(0,5);if(u!=="data-"&&u!=="aria-"){t.removeAttribute(n);return}}t.setAttribute(n,""+s)}}function ps(t,n,s){if(s===null)t.removeAttribute(n);else{switch(typeof s){case"undefined":case"function":case"symbol":case"boolean":t.removeAttribute(n);return}t.setAttribute(n,""+s)}}function Rr(t,n,s,u){if(u===null)t.removeAttribute(s);else{switch(typeof u){case"undefined":case"function":case"symbol":case"boolean":t.removeAttribute(s);return}t.setAttributeNS(n,s,""+u)}}function Hn(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function ms(t){var n=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(n==="checkbox"||n==="radio")}function Cf(t,n,s){var u=Object.getOwnPropertyDescriptor(t.constructor.prototype,n);if(!t.hasOwnProperty(n)&&typeof u<"u"&&typeof u.get=="function"&&typeof u.set=="function"){var h=u.get,p=u.set;return Object.defineProperty(t,n,{configurable:!0,get:function(){return h.call(this)},set:function(C){s=""+C,p.call(this,C)}}),Object.defineProperty(t,n,{enumerable:u.enumerable}),{getValue:function(){return s},setValue:function(C){s=""+C},stopTracking:function(){t._valueTracker=null,delete t[n]}}}}function zo(t){if(!t._valueTracker){var n=ms(t)?"checked":"value";t._valueTracker=Cf(t,n,""+t[n])}}function Uo(t){if(!t)return!1;var n=t._valueTracker;if(!n)return!0;var s=n.getValue(),u="";return t&&(u=ms(t)?t.checked?"true":"false":t.value),t=u,t!==s?(n.setValue(t),!0):!1}function oa(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var Ai=/[\n"\\]/g;function jn(t){return t.replace(Ai,function(n){return"\\"+n.charCodeAt(0).toString(16)+" "})}function la(t,n,s,u,h,p,C,k){t.name="",C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"?t.type=C:t.removeAttribute("type"),n!=null?C==="number"?(n===0&&t.value===""||t.value!=n)&&(t.value=""+Hn(n)):t.value!==""+Hn(n)&&(t.value=""+Hn(n)):C!=="submit"&&C!=="reset"||t.removeAttribute("value"),n!=null?Bo(t,C,Hn(n)):s!=null?Bo(t,C,Hn(s)):u!=null&&t.removeAttribute("value"),h==null&&p!=null&&(t.defaultChecked=!!p),h!=null&&(t.checked=h&&typeof h!="function"&&typeof h!="symbol"),k!=null&&typeof k!="function"&&typeof k!="symbol"&&typeof k!="boolean"?t.name=""+Hn(k):t.removeAttribute("name")}function Nc(t,n,s,u,h,p,C,k){if(p!=null&&typeof p!="function"&&typeof p!="symbol"&&typeof p!="boolean"&&(t.type=p),n!=null||s!=null){if(!(p!=="submit"&&p!=="reset"||n!=null)){zo(t);return}s=s!=null?""+Hn(s):"",n=n!=null?""+Hn(n):s,k||n===t.value||(t.value=n),t.defaultValue=n}u=u??h,u=typeof u!="function"&&typeof u!="symbol"&&!!u,t.checked=k?t.checked:!!u,t.defaultChecked=!!u,C!=null&&typeof C!="function"&&typeof C!="symbol"&&typeof C!="boolean"&&(t.name=C),zo(t)}function Bo(t,n,s){n==="number"&&oa(t.ownerDocument)===t||t.defaultValue===""+s||(t.defaultValue=""+s)}function za(t,n,s,u){if(t=t.options,n){n={};for(var h=0;h<s.length;h++)n["$"+s[h]]=!0;for(s=0;s<t.length;s++)h=n.hasOwnProperty("$"+t[s].value),t[s].selected!==h&&(t[s].selected=h),h&&u&&(t[s].defaultSelected=!0)}else{for(s=""+Hn(s),n=null,h=0;h<t.length;h++){if(t[h].value===s){t[h].selected=!0,u&&(t[h].defaultSelected=!0);return}n!==null||t[h].disabled||(n=t[h])}n!==null&&(n.selected=!0)}}function Ic(t,n,s){if(n!=null&&(n=""+Hn(n),n!==t.value&&(t.value=n),s==null)){t.defaultValue!==n&&(t.defaultValue=n);return}t.defaultValue=s!=null?""+Hn(s):""}function Hc(t,n,s,u){if(n==null){if(u!=null){if(s!=null)throw Error(a(92));if(wt(u)){if(1<u.length)throw Error(a(93));u=u[0]}s=u}s==null&&(s=""),n=s}s=Hn(n),t.defaultValue=s,u=t.textContent,u===s&&u!==""&&u!==null&&(t.value=u),zo(t)}function Ua(t,n){if(n){var s=t.firstChild;if(s&&s===t.lastChild&&s.nodeType===3){s.nodeValue=n;return}}t.textContent=n}var Ef=new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split(" "));function ys(t,n,s){var u=n.indexOf("--")===0;s==null||typeof s=="boolean"||s===""?u?t.setProperty(n,""):n==="float"?t.cssFloat="":t[n]="":u?t.setProperty(n,s):typeof s!="number"||s===0||Ef.has(n)?n==="float"?t.cssFloat=s:t[n]=(""+s).trim():t[n]=s+"px"}function Vr(t,n,s){if(n!=null&&typeof n!="object")throw Error(a(62));if(t=t.style,s!=null){for(var u in s)!s.hasOwnProperty(u)||n!=null&&n.hasOwnProperty(u)||(u.indexOf("--")===0?t.setProperty(u,""):u==="float"?t.cssFloat="":t[u]="");for(var h in n)u=n[h],n.hasOwnProperty(h)&&s[h]!==u&&ys(t,h,u)}else for(var p in n)n.hasOwnProperty(p)&&ys(t,p,n[p])}function Po(t){if(t.indexOf("-")===-1)return!1;switch(t){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var _f=new Map([["acceptCharset","accept-charset"],["htmlFor","for"],["httpEquiv","http-equiv"],["crossOrigin","crossorigin"],["accentHeight","accent-height"],["alignmentBaseline","alignment-baseline"],["arabicForm","arabic-form"],["baselineShift","baseline-shift"],["capHeight","cap-height"],["clipPath","clip-path"],["clipRule","clip-rule"],["colorInterpolation","color-interpolation"],["colorInterpolationFilters","color-interpolation-filters"],["colorProfile","color-profile"],["colorRendering","color-rendering"],["dominantBaseline","dominant-baseline"],["enableBackground","enable-background"],["fillOpacity","fill-opacity"],["fillRule","fill-rule"],["floodColor","flood-color"],["floodOpacity","flood-opacity"],["fontFamily","font-family"],["fontSize","font-size"],["fontSizeAdjust","font-size-adjust"],["fontStretch","font-stretch"],["fontStyle","font-style"],["fontVariant","font-variant"],["fontWeight","font-weight"],["glyphName","glyph-name"],["glyphOrientationHorizontal","glyph-orientation-horizontal"],["glyphOrientationVertical","glyph-orientation-vertical"],["horizAdvX","horiz-adv-x"],["horizOriginX","horiz-origin-x"],["imageRendering","image-rendering"],["letterSpacing","letter-spacing"],["lightingColor","lighting-color"],["markerEnd","marker-end"],["markerMid","marker-mid"],["markerStart","marker-start"],["overlinePosition","overline-position"],["overlineThickness","overline-thickness"],["paintOrder","paint-order"],["panose-1","panose-1"],["pointerEvents","pointer-events"],["renderingIntent","rendering-intent"],["shapeRendering","shape-rendering"],["stopColor","stop-color"],["stopOpacity","stop-opacity"],["strikethroughPosition","strikethrough-position"],["strikethroughThickness","strikethrough-thickness"],["strokeDasharray","stroke-dasharray"],["strokeDashoffset","stroke-dashoffset"],["strokeLinecap","stroke-linecap"],["strokeLinejoin","stroke-linejoin"],["strokeMiterlimit","stroke-miterlimit"],["strokeOpacity","stroke-opacity"],["strokeWidth","stroke-width"],["textAnchor","text-anchor"],["textDecoration","text-decoration"],["textRendering","text-rendering"],["transformOrigin","transform-origin"],["underlinePosition","underline-position"],["underlineThickness","underline-thickness"],["unicodeBidi","unicode-bidi"],["unicodeRange","unicode-range"],["unitsPerEm","units-per-em"],["vAlphabetic","v-alphabetic"],["vHanging","v-hanging"],["vIdeographic","v-ideographic"],["vMathematical","v-mathematical"],["vectorEffect","vector-effect"],["vertAdvY","vert-adv-y"],["vertOriginX","vert-origin-x"],["vertOriginY","vert-origin-y"],["wordSpacing","word-spacing"],["writingMode","writing-mode"],["xmlnsXlink","xmlns:xlink"],["xHeight","x-height"]]),Af=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;function bs(t){return Af.test(""+t)?"javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')":t}function or(){}var qo=null;function vs(t){return t=t.target||t.srcElement||window,t.correspondingUseElement&&(t=t.correspondingUseElement),t.nodeType===3?t.parentNode:t}var Ba=null,Pa=null;function jc(t){var n=Ha(t);if(n&&(t=n.stateNode)){var s=t[gn]||null;e:switch(t=n.stateNode,n.type){case"input":if(la(t,s.value,s.defaultValue,s.defaultValue,s.checked,s.defaultChecked,s.type,s.name),n=s.name,s.type==="radio"&&n!=null){for(s=t;s.parentNode;)s=s.parentNode;for(s=s.querySelectorAll('input[name="'+jn(""+n)+'"][type="radio"]'),n=0;n<s.length;n++){var u=s[n];if(u!==t&&u.form===t.form){var h=u[gn]||null;if(!h)throw Error(a(90));la(u,h.value,h.defaultValue,h.defaultValue,h.checked,h.defaultChecked,h.type,h.name)}}for(n=0;n<s.length;n++)u=s[n],u.form===t.form&&Uo(u)}break e;case"textarea":Ic(t,s.value,s.defaultValue);break e;case"select":n=s.value,n!=null&&za(t,!!s.multiple,n,!1)}}}var Go=!1;function zc(t,n,s){if(Go)return t(n,s);Go=!0;try{var u=t(n);return u}finally{if(Go=!1,(Ba!==null||Pa!==null)&&(Ru(),Ba&&(n=Ba,t=Pa,Pa=Ba=null,jc(n),t)))for(n=0;n<t.length;n++)jc(t[n])}}function xi(t,n){var s=t.stateNode;if(s===null)return null;var u=s[gn]||null;if(u===null)return null;s=u[n];e:switch(n){case"onClick":case"onClickCapture":case"onDoubleClick":case"onDoubleClickCapture":case"onMouseDown":case"onMouseDownCapture":case"onMouseMove":case"onMouseMoveCapture":case"onMouseUp":case"onMouseUpCapture":case"onMouseEnter":(u=!u.disabled)||(t=t.type,u=!(t==="button"||t==="input"||t==="select"||t==="textarea")),t=!u;break e;default:t=!1}if(t)return null;if(s&&typeof s!="function")throw Error(a(231,n,typeof s));return s}var Dr=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Vo=!1;if(Dr)try{var Ti={};Object.defineProperty(Ti,"passive",{get:function(){Vo=!0}}),window.addEventListener("test",Ti,Ti),window.removeEventListener("test",Ti,Ti)}catch{Vo=!1}var Or=null,Fo=null,Ss=null;function Ve(){if(Ss)return Ss;var t,n=Fo,s=n.length,u,h="value"in Or?Or.value:Or.textContent,p=h.length;for(t=0;t<s&&n[t]===h[t];t++);var C=s-t;for(u=1;u<=C&&n[s-u]===h[p-u];u++);return Ss=h.slice(t,1<u?1-u:void 0)}function qa(t){var n=t.keyCode;return"charCode"in t?(t=t.charCode,t===0&&n===13&&(t=13)):t=n,t===10&&(t=13),32<=t||t===13?t:0}function ca(){return!0}function Uc(){return!1}function En(t){function n(s,u,h,p,C){this._reactName=s,this._targetInst=h,this.type=u,this.nativeEvent=p,this.target=C,this.currentTarget=null;for(var k in t)t.hasOwnProperty(k)&&(s=t[k],this[k]=s?s(p):p[k]);return this.isDefaultPrevented=(p.defaultPrevented!=null?p.defaultPrevented:p.returnValue===!1)?ca:Uc,this.isPropagationStopped=Uc,this}return y(n.prototype,{preventDefault:function(){this.defaultPrevented=!0;var s=this.nativeEvent;s&&(s.preventDefault?s.preventDefault():typeof s.returnValue!="unknown"&&(s.returnValue=!1),this.isDefaultPrevented=ca)},stopPropagation:function(){var s=this.nativeEvent;s&&(s.stopPropagation?s.stopPropagation():typeof s.cancelBubble!="unknown"&&(s.cancelBubble=!0),this.isPropagationStopped=ca)},persist:function(){},isPersistent:ca}),n}var Fr={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(t){return t.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},ws=En(Fr),ki=y({},Fr,{view:0,detail:0}),xf=En(ki),Ko,Wo,Cs,Es=y({},ki,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:Xo,button:0,buttons:0,relatedTarget:function(t){return t.relatedTarget===void 0?t.fromElement===t.srcElement?t.toElement:t.fromElement:t.relatedTarget},movementX:function(t){return"movementX"in t?t.movementX:(t!==Cs&&(Cs&&t.type==="mousemove"?(Ko=t.screenX-Cs.screenX,Wo=t.screenY-Cs.screenY):Wo=Ko=0,Cs=t),Ko)},movementY:function(t){return"movementY"in t?t.movementY:Wo}}),Bc=En(Es),by=y({},Es,{dataTransfer:0}),Tf=En(by),kf=y({},ki,{relatedTarget:0}),Yo=En(kf),Rf=y({},Fr,{animationName:0,elapsedTime:0,pseudoElement:0}),Df=En(Rf),Of=y({},Fr,{clipboardData:function(t){return"clipboardData"in t?t.clipboardData:window.clipboardData}}),Lf=En(Of),Mf=y({},Fr,{data:0}),Pc=En(Mf),$f={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},Nf={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},If={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"};function Hf(t){var n=this.nativeEvent;return n.getModifierState?n.getModifierState(t):(t=If[t])?!!n[t]:!1}function Xo(){return Hf}var jf=y({},ki,{key:function(t){if(t.key){var n=$f[t.key]||t.key;if(n!=="Unidentified")return n}return t.type==="keypress"?(t=qa(t),t===13?"Enter":String.fromCharCode(t)):t.type==="keydown"||t.type==="keyup"?Nf[t.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:Xo,charCode:function(t){return t.type==="keypress"?qa(t):0},keyCode:function(t){return t.type==="keydown"||t.type==="keyup"?t.keyCode:0},which:function(t){return t.type==="keypress"?qa(t):t.type==="keydown"||t.type==="keyup"?t.keyCode:0}}),rn=En(jf),Jo=y({},Es,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0}),qc=En(Jo),_=y({},ki,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:Xo}),b=En(_),E=y({},Fr,{propertyName:0,elapsedTime:0,pseudoElement:0}),D=En(E),I=y({},Es,{deltaX:function(t){return"deltaX"in t?t.deltaX:"wheelDeltaX"in t?-t.wheelDeltaX:0},deltaY:function(t){return"deltaY"in t?t.deltaY:"wheelDeltaY"in t?-t.wheelDeltaY:"wheelDelta"in t?-t.wheelDelta:0},deltaZ:0,deltaMode:0}),ie=En(I),ye=y({},Fr,{newState:0,oldState:0}),Oe=En(ye),Fe=[9,13,27,32],at=Dr&&"CompositionEvent"in window,lt=null;Dr&&"documentMode"in document&&(lt=document.documentMode);var Bt=Dr&&"TextEvent"in window&&!lt,Ri=Dr&&(!at||lt&&8<lt&&11>=lt),Gc=" ",Qo=!1;function Vc(t,n){switch(t){case"keyup":return Fe.indexOf(n.keyCode)!==-1;case"keydown":return n.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Zo(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Ga=!1;function Fc(t,n){switch(t){case"compositionend":return Zo(n);case"keypress":return n.which!==32?null:(Qo=!0,Gc);case"textInput":return t=n.data,t===Gc&&Qo?null:t;default:return null}}function zf(t,n){if(Ga)return t==="compositionend"||!at&&Vc(t,n)?(t=Ve(),Ss=Fo=Or=null,Ga=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(n.ctrlKey||n.altKey||n.metaKey)||n.ctrlKey&&n.altKey){if(n.char&&1<n.char.length)return n.char;if(n.which)return String.fromCharCode(n.which)}return null;case"compositionend":return Ri&&n.locale!=="ko"?null:n.data;default:return null}}var Uf={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function el(t){var n=t&&t.nodeName&&t.nodeName.toLowerCase();return n==="input"?!!Uf[t.type]:n==="textarea"}function tl(t,n,s,u){Ba?Pa?Pa.push(u):Pa=[u]:Ba=u,n=Iu(n,"onChange"),0<n.length&&(s=new ws("onChange","change",null,s,u),t.push({event:s,listeners:n}))}var Va=null,ua=null;function nl(t){Jv(t,0)}function Di(t){var n=_i(t);if(Uo(n))return t}function Kc(t,n){if(t==="change")return n}var Wc=!1;if(Dr){var rl;if(Dr){var al="oninput"in document;if(!al){var il=document.createElement("div");il.setAttribute("oninput","return;"),al=typeof il.oninput=="function"}rl=al}else rl=!1;Wc=rl&&(!document.documentMode||9<document.documentMode)}function vy(){Va&&(Va.detachEvent("onpropertychange",Sy),ua=Va=null)}function Sy(t){if(t.propertyName==="value"&&Di(ua)){var n=[];tl(n,ua,t,vs(t)),zc(nl,n)}}function yA(t,n,s){t==="focusin"?(vy(),Va=n,ua=s,Va.attachEvent("onpropertychange",Sy)):t==="focusout"&&vy()}function bA(t){if(t==="selectionchange"||t==="keyup"||t==="keydown")return Di(ua)}function vA(t,n){if(t==="click")return Di(n)}function SA(t,n){if(t==="input"||t==="change")return Di(n)}function wA(t,n){return t===n&&(t!==0||1/t===1/n)||t!==t&&n!==n}var Yn=typeof Object.is=="function"?Object.is:wA;function sl(t,n){if(Yn(t,n))return!0;if(typeof t!="object"||t===null||typeof n!="object"||n===null)return!1;var s=Object.keys(t),u=Object.keys(n);if(s.length!==u.length)return!1;for(u=0;u<s.length;u++){var h=s[u];if(!wn.call(n,h)||!Yn(t[h],n[h]))return!1}return!0}function wy(t){for(;t&&t.firstChild;)t=t.firstChild;return t}function Cy(t,n){var s=wy(t);t=0;for(var u;s;){if(s.nodeType===3){if(u=t+s.textContent.length,t<=n&&u>=n)return{node:s,offset:n-t};t=u}e:{for(;s;){if(s.nextSibling){s=s.nextSibling;break e}s=s.parentNode}s=void 0}s=wy(s)}}function Ey(t,n){return t&&n?t===n?!0:t&&t.nodeType===3?!1:n&&n.nodeType===3?Ey(t,n.parentNode):"contains"in t?t.contains(n):t.compareDocumentPosition?!!(t.compareDocumentPosition(n)&16):!1:!1}function _y(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var n=oa(t.document);n instanceof t.HTMLIFrameElement;){try{var s=typeof n.contentWindow.location.href=="string"}catch{s=!1}if(s)t=n.contentWindow;else break;n=oa(t.document)}return n}function Bf(t){var n=t&&t.nodeName&&t.nodeName.toLowerCase();return n&&(n==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||n==="textarea"||t.contentEditable==="true")}var CA=Dr&&"documentMode"in document&&11>=document.documentMode,_s=null,Pf=null,ol=null,qf=!1;function Ay(t,n,s){var u=s.window===s?s.document:s.nodeType===9?s:s.ownerDocument;qf||_s==null||_s!==oa(u)||(u=_s,"selectionStart"in u&&Bf(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),ol&&sl(ol,u)||(ol=u,u=Iu(Pf,"onSelect"),0<u.length&&(n=new ws("onSelect","select",null,n,s),t.push({event:n,listeners:u}),n.target=_s)))}function Oi(t,n){var s={};return s[t.toLowerCase()]=n.toLowerCase(),s["Webkit"+t]="webkit"+n,s["Moz"+t]="moz"+n,s}var As={animationend:Oi("Animation","AnimationEnd"),animationiteration:Oi("Animation","AnimationIteration"),animationstart:Oi("Animation","AnimationStart"),transitionrun:Oi("Transition","TransitionRun"),transitionstart:Oi("Transition","TransitionStart"),transitioncancel:Oi("Transition","TransitionCancel"),transitionend:Oi("Transition","TransitionEnd")},Gf={},xy={};Dr&&(xy=document.createElement("div").style,"AnimationEvent"in window||(delete As.animationend.animation,delete As.animationiteration.animation,delete As.animationstart.animation),"TransitionEvent"in window||delete As.transitionend.transition);function Li(t){if(Gf[t])return Gf[t];if(!As[t])return t;var n=As[t],s;for(s in n)if(n.hasOwnProperty(s)&&s in xy)return Gf[t]=n[s];return t}var Ty=Li("animationend"),ky=Li("animationiteration"),Ry=Li("animationstart"),EA=Li("transitionrun"),_A=Li("transitionstart"),AA=Li("transitioncancel"),Dy=Li("transitionend"),Oy=new Map,Vf="abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");Vf.push("scrollEnd");function Lr(t,n){Oy.set(t,n),sa(n,[t])}var Yc=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var n=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(n))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)},lr=[],xs=0,Ff=0;function Xc(){for(var t=xs,n=Ff=xs=0;n<t;){var s=lr[n];lr[n++]=null;var u=lr[n];lr[n++]=null;var h=lr[n];lr[n++]=null;var p=lr[n];if(lr[n++]=null,u!==null&&h!==null){var C=u.pending;C===null?h.next=h:(h.next=C.next,C.next=h),u.pending=h}p!==0&&Ly(s,h,p)}}function Jc(t,n,s,u){lr[xs++]=t,lr[xs++]=n,lr[xs++]=s,lr[xs++]=u,Ff|=u,t.lanes|=u,t=t.alternate,t!==null&&(t.lanes|=u)}function Kf(t,n,s,u){return Jc(t,n,s,u),Qc(t)}function Mi(t,n){return Jc(t,null,null,n),Qc(t)}function Ly(t,n,s){t.lanes|=s;var u=t.alternate;u!==null&&(u.lanes|=s);for(var h=!1,p=t.return;p!==null;)p.childLanes|=s,u=p.alternate,u!==null&&(u.childLanes|=s),p.tag===22&&(t=p.stateNode,t===null||t._visibility&1||(h=!0)),t=p,p=p.return;return t.tag===3?(p=t.stateNode,h&&n!==null&&(h=31-nn(s),t=p.hiddenUpdates,u=t[h],u===null?t[h]=[n]:u.push(n),n.lane=s|536870912),p):null}function Qc(t){if(50<Rl)throw Rl=0,ng=null,Error(a(185));for(var n=t.return;n!==null;)t=n,n=t.return;return t.tag===3?t.stateNode:null}var Ts={};function xA(t,n,s,u){this.tag=t,this.key=s,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.refCleanup=this.ref=null,this.pendingProps=n,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=u,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Xn(t,n,s,u){return new xA(t,n,s,u)}function Wf(t){return t=t.prototype,!(!t||!t.isReactComponent)}function da(t,n){var s=t.alternate;return s===null?(s=Xn(t.tag,n,t.key,t.mode),s.elementType=t.elementType,s.type=t.type,s.stateNode=t.stateNode,s.alternate=t,t.alternate=s):(s.pendingProps=n,s.type=t.type,s.flags=0,s.subtreeFlags=0,s.deletions=null),s.flags=t.flags&65011712,s.childLanes=t.childLanes,s.lanes=t.lanes,s.child=t.child,s.memoizedProps=t.memoizedProps,s.memoizedState=t.memoizedState,s.updateQueue=t.updateQueue,n=t.dependencies,s.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext},s.sibling=t.sibling,s.index=t.index,s.ref=t.ref,s.refCleanup=t.refCleanup,s}function My(t,n){t.flags&=65011714;var s=t.alternate;return s===null?(t.childLanes=0,t.lanes=n,t.child=null,t.subtreeFlags=0,t.memoizedProps=null,t.memoizedState=null,t.updateQueue=null,t.dependencies=null,t.stateNode=null):(t.childLanes=s.childLanes,t.lanes=s.lanes,t.child=s.child,t.subtreeFlags=0,t.deletions=null,t.memoizedProps=s.memoizedProps,t.memoizedState=s.memoizedState,t.updateQueue=s.updateQueue,t.type=s.type,n=s.dependencies,t.dependencies=n===null?null:{lanes:n.lanes,firstContext:n.firstContext}),t}function Zc(t,n,s,u,h,p){var C=0;if(u=t,typeof t=="function")Wf(t)&&(C=1);else if(typeof t=="string")C=Ox(t,s,de.current)?26:t==="html"||t==="head"||t==="body"?27:5;else e:switch(t){case fe:return t=Xn(31,s,n,h),t.elementType=fe,t.lanes=p,t;case A:return $i(s.children,h,p,n);case x:C=8,h|=24;break;case R:return t=Xn(12,s,n,h|2),t.elementType=R,t.lanes=p,t;case q:return t=Xn(13,s,n,h),t.elementType=q,t.lanes=p,t;case G:return t=Xn(19,s,n,h),t.elementType=G,t.lanes=p,t;default:if(typeof t=="object"&&t!==null)switch(t.$$typeof){case O:C=10;break e;case M:C=9;break e;case L:C=11;break e;case B:C=14;break e;case K:C=16,u=null;break e}C=29,s=Error(a(130,t===null?"null":typeof t,"")),u=null}return n=Xn(C,s,n,h),n.elementType=t,n.type=u,n.lanes=p,n}function $i(t,n,s,u){return t=Xn(7,t,u,n),t.lanes=s,t}function Yf(t,n,s){return t=Xn(6,t,null,n),t.lanes=s,t}function $y(t){var n=Xn(18,null,null,0);return n.stateNode=t,n}function Xf(t,n,s){return n=Xn(4,t.children!==null?t.children:[],t.key,n),n.lanes=s,n.stateNode={containerInfo:t.containerInfo,pendingChildren:null,implementation:t.implementation},n}var Ny=new WeakMap;function cr(t,n){if(typeof t=="object"&&t!==null){var s=Ny.get(t);return s!==void 0?s:(n={value:t,source:n,stack:Tr(n)},Ny.set(t,n),n)}return{value:t,source:n,stack:Tr(n)}}var ks=[],Rs=0,eu=null,ll=0,ur=[],dr=0,Fa=null,Kr=1,Wr="";function fa(t,n){ks[Rs++]=ll,ks[Rs++]=eu,eu=t,ll=n}function Iy(t,n,s){ur[dr++]=Kr,ur[dr++]=Wr,ur[dr++]=Fa,Fa=t;var u=Kr;t=Wr;var h=32-nn(u)-1;u&=~(1<<h),s+=1;var p=32-nn(n)+h;if(30<p){var C=h-h%5;p=(u&(1<<C)-1).toString(32),u>>=C,h-=C,Kr=1<<32-nn(n)+h|s<<h|u,Wr=p+t}else Kr=1<<p|s<<h|u,Wr=t}function Jf(t){t.return!==null&&(fa(t,1),Iy(t,1,0))}function Qf(t){for(;t===eu;)eu=ks[--Rs],ks[Rs]=null,ll=ks[--Rs],ks[Rs]=null;for(;t===Fa;)Fa=ur[--dr],ur[dr]=null,Wr=ur[--dr],ur[dr]=null,Kr=ur[--dr],ur[dr]=null}function Hy(t,n){ur[dr++]=Kr,ur[dr++]=Wr,ur[dr++]=Fa,Kr=n.id,Wr=n.overflow,Fa=t}var pn=null,Rt=null,ct=!1,Ka=null,fr=!1,Zf=Error(a(519));function Wa(t){var n=Error(a(418,1<arguments.length&&arguments[1]!==void 0&&arguments[1]?"text":"HTML",""));throw cl(cr(n,t)),Zf}function jy(t){var n=t.stateNode,s=t.type,u=t.memoizedProps;switch(n[Nt]=t,n[gn]=u,s){case"dialog":nt("cancel",n),nt("close",n);break;case"iframe":case"object":case"embed":nt("load",n);break;case"video":case"audio":for(s=0;s<Ol.length;s++)nt(Ol[s],n);break;case"source":nt("error",n);break;case"img":case"image":case"link":nt("error",n),nt("load",n);break;case"details":nt("toggle",n);break;case"input":nt("invalid",n),Nc(n,u.value,u.defaultValue,u.checked,u.defaultChecked,u.type,u.name,!0);break;case"select":nt("invalid",n);break;case"textarea":nt("invalid",n),Hc(n,u.value,u.defaultValue,u.children)}s=u.children,typeof s!="string"&&typeof s!="number"&&typeof s!="bigint"||n.textContent===""+s||u.suppressHydrationWarning===!0||tS(n.textContent,s)?(u.popover!=null&&(nt("beforetoggle",n),nt("toggle",n)),u.onScroll!=null&&nt("scroll",n),u.onScrollEnd!=null&&nt("scrollend",n),u.onClick!=null&&(n.onclick=or),n=!0):n=!1,n||Wa(t,!0)}function zy(t){for(pn=t.return;pn;)switch(pn.tag){case 5:case 31:case 13:fr=!1;return;case 27:case 3:fr=!0;return;default:pn=pn.return}}function Ds(t){if(t!==pn)return!1;if(!ct)return zy(t),ct=!0,!1;var n=t.tag,s;if((s=n!==3&&n!==27)&&((s=n===5)&&(s=t.type,s=!(s!=="form"&&s!=="button")||yg(t.type,t.memoizedProps)),s=!s),s&&Rt&&Wa(t),zy(t),n===13){if(t=t.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(317));Rt=uS(t)}else if(n===31){if(t=t.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(317));Rt=uS(t)}else n===27?(n=Rt,li(t.type)?(t=Cg,Cg=null,Rt=t):Rt=n):Rt=pn?gr(t.stateNode.nextSibling):null;return!0}function Ni(){Rt=pn=null,ct=!1}function eh(){var t=Ka;return t!==null&&(Pn===null?Pn=t:Pn.push.apply(Pn,t),Ka=null),t}function cl(t){Ka===null?Ka=[t]:Ka.push(t)}var th=H(null),Ii=null,ha=null;function Ya(t,n,s){ge(th,n._currentValue),n._currentValue=s}function ga(t){t._currentValue=th.current,T(th)}function nh(t,n,s){for(;t!==null;){var u=t.alternate;if((t.childLanes&n)!==n?(t.childLanes|=n,u!==null&&(u.childLanes|=n)):u!==null&&(u.childLanes&n)!==n&&(u.childLanes|=n),t===s)break;t=t.return}}function rh(t,n,s,u){var h=t.child;for(h!==null&&(h.return=t);h!==null;){var p=h.dependencies;if(p!==null){var C=h.child;p=p.firstContext;e:for(;p!==null;){var k=p;p=h;for(var z=0;z<n.length;z++)if(k.context===n[z]){p.lanes|=s,k=p.alternate,k!==null&&(k.lanes|=s),nh(p.return,s,t),u||(C=null);break e}p=k.next}}else if(h.tag===18){if(C=h.return,C===null)throw Error(a(341));C.lanes|=s,p=C.alternate,p!==null&&(p.lanes|=s),nh(C,s,t),C=null}else C=h.child;if(C!==null)C.return=h;else for(C=h;C!==null;){if(C===t){C=null;break}if(h=C.sibling,h!==null){h.return=C.return,C=h;break}C=C.return}h=C}}function Os(t,n,s,u){t=null;for(var h=n,p=!1;h!==null;){if(!p){if((h.flags&524288)!==0)p=!0;else if((h.flags&262144)!==0)break}if(h.tag===10){var C=h.alternate;if(C===null)throw Error(a(387));if(C=C.memoizedProps,C!==null){var k=h.type;Yn(h.pendingProps.value,C.value)||(t!==null?t.push(k):t=[k])}}else if(h===Re.current){if(C=h.alternate,C===null)throw Error(a(387));C.memoizedState.memoizedState!==h.memoizedState.memoizedState&&(t!==null?t.push(Il):t=[Il])}h=h.return}t!==null&&rh(n,t,s,u),n.flags|=262144}function tu(t){for(t=t.firstContext;t!==null;){if(!Yn(t.context._currentValue,t.memoizedValue))return!0;t=t.next}return!1}function Hi(t){Ii=t,ha=null,t=t.dependencies,t!==null&&(t.firstContext=null)}function mn(t){return Uy(Ii,t)}function nu(t,n){return Ii===null&&Hi(t),Uy(t,n)}function Uy(t,n){var s=n._currentValue;if(n={context:n,memoizedValue:s,next:null},ha===null){if(t===null)throw Error(a(308));ha=n,t.dependencies={lanes:0,firstContext:n},t.flags|=524288}else ha=ha.next=n;return s}var TA=typeof AbortController<"u"?AbortController:function(){var t=[],n=this.signal={aborted:!1,addEventListener:function(s,u){t.push(u)}};this.abort=function(){n.aborted=!0,t.forEach(function(s){return s()})}},kA=o.unstable_scheduleCallback,RA=o.unstable_NormalPriority,Jt={$$typeof:O,Consumer:null,Provider:null,_currentValue:null,_currentValue2:null,_threadCount:0};function ah(){return{controller:new TA,data:new Map,refCount:0}}function ul(t){t.refCount--,t.refCount===0&&kA(RA,function(){t.controller.abort()})}var dl=null,ih=0,Ls=0,Ms=null;function DA(t,n){if(dl===null){var s=dl=[];ih=0,Ls=lg(),Ms={status:"pending",value:void 0,then:function(u){s.push(u)}}}return ih++,n.then(By,By),n}function By(){if(--ih===0&&dl!==null){Ms!==null&&(Ms.status="fulfilled");var t=dl;dl=null,Ls=0,Ms=null;for(var n=0;n<t.length;n++)(0,t[n])()}}function OA(t,n){var s=[],u={status:"pending",value:null,reason:null,then:function(h){s.push(h)}};return t.then(function(){u.status="fulfilled",u.value=n;for(var h=0;h<s.length;h++)(0,s[h])(n)},function(h){for(u.status="rejected",u.reason=h,h=0;h<s.length;h++)(0,s[h])(void 0)}),u}var Py=J.S;J.S=function(t,n){Av=Ut(),typeof n=="object"&&n!==null&&typeof n.then=="function"&&DA(t,n),Py!==null&&Py(t,n)};var ji=H(null);function sh(){var t=ji.current;return t!==null?t:xt.pooledCache}function ru(t,n){n===null?ge(ji,ji.current):ge(ji,n.pool)}function qy(){var t=sh();return t===null?null:{parent:Jt._currentValue,pool:t}}var $s=Error(a(460)),oh=Error(a(474)),au=Error(a(542)),iu={then:function(){}};function Gy(t){return t=t.status,t==="fulfilled"||t==="rejected"}function Vy(t,n,s){switch(s=t[s],s===void 0?t.push(n):s!==n&&(n.then(or,or),n=s),n.status){case"fulfilled":return n.value;case"rejected":throw t=n.reason,Ky(t),t;default:if(typeof n.status=="string")n.then(or,or);else{if(t=xt,t!==null&&100<t.shellSuspendCounter)throw Error(a(482));t=n,t.status="pending",t.then(function(u){if(n.status==="pending"){var h=n;h.status="fulfilled",h.value=u}},function(u){if(n.status==="pending"){var h=n;h.status="rejected",h.reason=u}})}switch(n.status){case"fulfilled":return n.value;case"rejected":throw t=n.reason,Ky(t),t}throw Ui=n,$s}}function zi(t){try{var n=t._init;return n(t._payload)}catch(s){throw s!==null&&typeof s=="object"&&typeof s.then=="function"?(Ui=s,$s):s}}var Ui=null;function Fy(){if(Ui===null)throw Error(a(459));var t=Ui;return Ui=null,t}function Ky(t){if(t===$s||t===au)throw Error(a(483))}var Ns=null,fl=0;function su(t){var n=fl;return fl+=1,Ns===null&&(Ns=[]),Vy(Ns,t,n)}function hl(t,n){n=n.props.ref,t.ref=n!==void 0?n:null}function ou(t,n){throw n.$$typeof===w?Error(a(525)):(t=Object.prototype.toString.call(n),Error(a(31,t==="[object Object]"?"object with keys {"+Object.keys(n).join(", ")+"}":t)))}function Wy(t){function n(F,P){if(t){var W=F.deletions;W===null?(F.deletions=[P],F.flags|=16):W.push(P)}}function s(F,P){if(!t)return null;for(;P!==null;)n(F,P),P=P.sibling;return null}function u(F){for(var P=new Map;F!==null;)F.key!==null?P.set(F.key,F):P.set(F.index,F),F=F.sibling;return P}function h(F,P){return F=da(F,P),F.index=0,F.sibling=null,F}function p(F,P,W){return F.index=W,t?(W=F.alternate,W!==null?(W=W.index,W<P?(F.flags|=67108866,P):W):(F.flags|=67108866,P)):(F.flags|=1048576,P)}function C(F){return t&&F.alternate===null&&(F.flags|=67108866),F}function k(F,P,W,se){return P===null||P.tag!==6?(P=Yf(W,F.mode,se),P.return=F,P):(P=h(P,W),P.return=F,P)}function z(F,P,W,se){var Ie=W.type;return Ie===A?ne(F,P,W.props.children,se,W.key):P!==null&&(P.elementType===Ie||typeof Ie=="object"&&Ie!==null&&Ie.$$typeof===K&&zi(Ie)===P.type)?(P=h(P,W.props),hl(P,W),P.return=F,P):(P=Zc(W.type,W.key,W.props,null,F.mode,se),hl(P,W),P.return=F,P)}function Y(F,P,W,se){return P===null||P.tag!==4||P.stateNode.containerInfo!==W.containerInfo||P.stateNode.implementation!==W.implementation?(P=Xf(W,F.mode,se),P.return=F,P):(P=h(P,W.children||[]),P.return=F,P)}function ne(F,P,W,se,Ie){return P===null||P.tag!==7?(P=$i(W,F.mode,se,Ie),P.return=F,P):(P=h(P,W),P.return=F,P)}function le(F,P,W){if(typeof P=="string"&&P!==""||typeof P=="number"||typeof P=="bigint")return P=Yf(""+P,F.mode,W),P.return=F,P;if(typeof P=="object"&&P!==null){switch(P.$$typeof){case S:return W=Zc(P.type,P.key,P.props,null,F.mode,W),hl(W,P),W.return=F,W;case v:return P=Xf(P,F.mode,W),P.return=F,P;case K:return P=zi(P),le(F,P,W)}if(wt(P)||Ue(P))return P=$i(P,F.mode,W,null),P.return=F,P;if(typeof P.then=="function")return le(F,su(P),W);if(P.$$typeof===O)return le(F,nu(F,P),W);ou(F,P)}return null}function Q(F,P,W,se){var Ie=P!==null?P.key:null;if(typeof W=="string"&&W!==""||typeof W=="number"||typeof W=="bigint")return Ie!==null?null:k(F,P,""+W,se);if(typeof W=="object"&&W!==null){switch(W.$$typeof){case S:return W.key===Ie?z(F,P,W,se):null;case v:return W.key===Ie?Y(F,P,W,se):null;case K:return W=zi(W),Q(F,P,W,se)}if(wt(W)||Ue(W))return Ie!==null?null:ne(F,P,W,se,null);if(typeof W.then=="function")return Q(F,P,su(W),se);if(W.$$typeof===O)return Q(F,P,nu(F,W),se);ou(F,W)}return null}function te(F,P,W,se,Ie){if(typeof se=="string"&&se!==""||typeof se=="number"||typeof se=="bigint")return F=F.get(W)||null,k(P,F,""+se,Ie);if(typeof se=="object"&&se!==null){switch(se.$$typeof){case S:return F=F.get(se.key===null?W:se.key)||null,z(P,F,se,Ie);case v:return F=F.get(se.key===null?W:se.key)||null,Y(P,F,se,Ie);case K:return se=zi(se),te(F,P,W,se,Ie)}if(wt(se)||Ue(se))return F=F.get(W)||null,ne(P,F,se,Ie,null);if(typeof se.then=="function")return te(F,P,W,su(se),Ie);if(se.$$typeof===O)return te(F,P,W,nu(P,se),Ie);ou(P,se)}return null}function Te(F,P,W,se){for(var Ie=null,ft=null,Le=P,Je=P=0,st=null;Le!==null&&Je<W.length;Je++){Le.index>Je?(st=Le,Le=null):st=Le.sibling;var ht=Q(F,Le,W[Je],se);if(ht===null){Le===null&&(Le=st);break}t&&Le&&ht.alternate===null&&n(F,Le),P=p(ht,P,Je),ft===null?Ie=ht:ft.sibling=ht,ft=ht,Le=st}if(Je===W.length)return s(F,Le),ct&&fa(F,Je),Ie;if(Le===null){for(;Je<W.length;Je++)Le=le(F,W[Je],se),Le!==null&&(P=p(Le,P,Je),ft===null?Ie=Le:ft.sibling=Le,ft=Le);return ct&&fa(F,Je),Ie}for(Le=u(Le);Je<W.length;Je++)st=te(Le,F,Je,W[Je],se),st!==null&&(t&&st.alternate!==null&&Le.delete(st.key===null?Je:st.key),P=p(st,P,Je),ft===null?Ie=st:ft.sibling=st,ft=st);return t&&Le.forEach(function(hi){return n(F,hi)}),ct&&fa(F,Je),Ie}function He(F,P,W,se){if(W==null)throw Error(a(151));for(var Ie=null,ft=null,Le=P,Je=P=0,st=null,ht=W.next();Le!==null&&!ht.done;Je++,ht=W.next()){Le.index>Je?(st=Le,Le=null):st=Le.sibling;var hi=Q(F,Le,ht.value,se);if(hi===null){Le===null&&(Le=st);break}t&&Le&&hi.alternate===null&&n(F,Le),P=p(hi,P,Je),ft===null?Ie=hi:ft.sibling=hi,ft=hi,Le=st}if(ht.done)return s(F,Le),ct&&fa(F,Je),Ie;if(Le===null){for(;!ht.done;Je++,ht=W.next())ht=le(F,ht.value,se),ht!==null&&(P=p(ht,P,Je),ft===null?Ie=ht:ft.sibling=ht,ft=ht);return ct&&fa(F,Je),Ie}for(Le=u(Le);!ht.done;Je++,ht=W.next())ht=te(Le,F,Je,ht.value,se),ht!==null&&(t&&ht.alternate!==null&&Le.delete(ht.key===null?Je:ht.key),P=p(ht,P,Je),ft===null?Ie=ht:ft.sibling=ht,ft=ht);return t&&Le.forEach(function(Px){return n(F,Px)}),ct&&fa(F,Je),Ie}function _t(F,P,W,se){if(typeof W=="object"&&W!==null&&W.type===A&&W.key===null&&(W=W.props.children),typeof W=="object"&&W!==null){switch(W.$$typeof){case S:e:{for(var Ie=W.key;P!==null;){if(P.key===Ie){if(Ie=W.type,Ie===A){if(P.tag===7){s(F,P.sibling),se=h(P,W.props.children),se.return=F,F=se;break e}}else if(P.elementType===Ie||typeof Ie=="object"&&Ie!==null&&Ie.$$typeof===K&&zi(Ie)===P.type){s(F,P.sibling),se=h(P,W.props),hl(se,W),se.return=F,F=se;break e}s(F,P);break}else n(F,P);P=P.sibling}W.type===A?(se=$i(W.props.children,F.mode,se,W.key),se.return=F,F=se):(se=Zc(W.type,W.key,W.props,null,F.mode,se),hl(se,W),se.return=F,F=se)}return C(F);case v:e:{for(Ie=W.key;P!==null;){if(P.key===Ie)if(P.tag===4&&P.stateNode.containerInfo===W.containerInfo&&P.stateNode.implementation===W.implementation){s(F,P.sibling),se=h(P,W.children||[]),se.return=F,F=se;break e}else{s(F,P);break}else n(F,P);P=P.sibling}se=Xf(W,F.mode,se),se.return=F,F=se}return C(F);case K:return W=zi(W),_t(F,P,W,se)}if(wt(W))return Te(F,P,W,se);if(Ue(W)){if(Ie=Ue(W),typeof Ie!="function")throw Error(a(150));return W=Ie.call(W),He(F,P,W,se)}if(typeof W.then=="function")return _t(F,P,su(W),se);if(W.$$typeof===O)return _t(F,P,nu(F,W),se);ou(F,W)}return typeof W=="string"&&W!==""||typeof W=="number"||typeof W=="bigint"?(W=""+W,P!==null&&P.tag===6?(s(F,P.sibling),se=h(P,W),se.return=F,F=se):(s(F,P),se=Yf(W,F.mode,se),se.return=F,F=se),C(F)):s(F,P)}return function(F,P,W,se){try{fl=0;var Ie=_t(F,P,W,se);return Ns=null,Ie}catch(Le){if(Le===$s||Le===au)throw Le;var ft=Xn(29,Le,null,F.mode);return ft.lanes=se,ft.return=F,ft}finally{}}}var Bi=Wy(!0),Yy=Wy(!1),Xa=!1;function lh(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function ch(t,n){t=t.updateQueue,n.updateQueue===t&&(n.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Ja(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(t,n,s){var u=t.updateQueue;if(u===null)return null;if(u=u.shared,(pt&2)!==0){var h=u.pending;return h===null?n.next=n:(n.next=h.next,h.next=n),u.pending=n,n=Qc(t),Ly(t,null,s),n}return Jc(t,u,n,s),Qc(t)}function gl(t,n,s){if(n=n.updateQueue,n!==null&&(n=n.shared,(s&4194048)!==0)){var u=n.lanes;u&=t.pendingLanes,s|=u,n.lanes=s,Se(t,s)}}function uh(t,n){var s=t.updateQueue,u=t.alternate;if(u!==null&&(u=u.updateQueue,s===u)){var h=null,p=null;if(s=s.firstBaseUpdate,s!==null){do{var C={lane:s.lane,tag:s.tag,payload:s.payload,callback:null,next:null};p===null?h=p=C:p=p.next=C,s=s.next}while(s!==null);p===null?h=p=n:p=p.next=n}else h=p=n;s={baseState:u.baseState,firstBaseUpdate:h,lastBaseUpdate:p,shared:u.shared,callbacks:u.callbacks},t.updateQueue=s;return}t=s.lastBaseUpdate,t===null?s.firstBaseUpdate=n:t.next=n,s.lastBaseUpdate=n}var dh=!1;function pl(){if(dh){var t=Ms;if(t!==null)throw t}}function ml(t,n,s,u){dh=!1;var h=t.updateQueue;Xa=!1;var p=h.firstBaseUpdate,C=h.lastBaseUpdate,k=h.shared.pending;if(k!==null){h.shared.pending=null;var z=k,Y=z.next;z.next=null,C===null?p=Y:C.next=Y,C=z;var ne=t.alternate;ne!==null&&(ne=ne.updateQueue,k=ne.lastBaseUpdate,k!==C&&(k===null?ne.firstBaseUpdate=Y:k.next=Y,ne.lastBaseUpdate=z))}if(p!==null){var le=h.baseState;C=0,ne=Y=z=null,k=p;do{var Q=k.lane&-536870913,te=Q!==k.lane;if(te?(it&Q)===Q:(u&Q)===Q){Q!==0&&Q===Ls&&(dh=!0),ne!==null&&(ne=ne.next={lane:0,tag:k.tag,payload:k.payload,callback:null,next:null});e:{var Te=t,He=k;Q=n;var _t=s;switch(He.tag){case 1:if(Te=He.payload,typeof Te=="function"){le=Te.call(_t,le,Q);break e}le=Te;break e;case 3:Te.flags=Te.flags&-65537|128;case 0:if(Te=He.payload,Q=typeof Te=="function"?Te.call(_t,le,Q):Te,Q==null)break e;le=y({},le,Q);break e;case 2:Xa=!0}}Q=k.callback,Q!==null&&(t.flags|=64,te&&(t.flags|=8192),te=h.callbacks,te===null?h.callbacks=[Q]:te.push(Q))}else te={lane:Q,tag:k.tag,payload:k.payload,callback:k.callback,next:null},ne===null?(Y=ne=te,z=le):ne=ne.next=te,C|=Q;if(k=k.next,k===null){if(k=h.shared.pending,k===null)break;te=k,k=te.next,te.next=null,h.lastBaseUpdate=te,h.shared.pending=null}}while(!0);ne===null&&(z=le),h.baseState=z,h.firstBaseUpdate=Y,h.lastBaseUpdate=ne,p===null&&(h.shared.lanes=0),ri|=C,t.lanes=C,t.memoizedState=le}}function Xy(t,n){if(typeof t!="function")throw Error(a(191,t));t.call(n)}function Jy(t,n){var s=t.callbacks;if(s!==null)for(t.callbacks=null,t=0;t<s.length;t++)Xy(s[t],n)}var Is=H(null),lu=H(0);function Qy(t,n){t=Ea,ge(lu,t),ge(Is,n),Ea=t|n.baseLanes}function fh(){ge(lu,Ea),ge(Is,Is.current)}function hh(){Ea=lu.current,T(Is),T(lu)}var Jn=H(null),hr=null;function Za(t){var n=t.alternate;ge(Ft,Ft.current&1),ge(Jn,t),hr===null&&(n===null||Is.current!==null||n.memoizedState!==null)&&(hr=t)}function gh(t){ge(Ft,Ft.current),ge(Jn,t),hr===null&&(hr=t)}function Zy(t){t.tag===22?(ge(Ft,Ft.current),ge(Jn,t),hr===null&&(hr=t)):ei()}function ei(){ge(Ft,Ft.current),ge(Jn,Jn.current)}function Qn(t){T(Jn),hr===t&&(hr=null),T(Ft)}var Ft=H(0);function cu(t){for(var n=t;n!==null;){if(n.tag===13){var s=n.memoizedState;if(s!==null&&(s=s.dehydrated,s===null||Sg(s)||wg(s)))return n}else if(n.tag===19&&(n.memoizedProps.revealOrder==="forwards"||n.memoizedProps.revealOrder==="backwards"||n.memoizedProps.revealOrder==="unstable_legacy-backwards"||n.memoizedProps.revealOrder==="together")){if((n.flags&128)!==0)return n}else if(n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return null;n=n.return}n.sibling.return=n.return,n=n.sibling}return null}var pa=0,Xe=null,Ct=null,Qt=null,uu=!1,Hs=!1,Pi=!1,du=0,yl=0,js=null,LA=0;function Pt(){throw Error(a(321))}function ph(t,n){if(n===null)return!1;for(var s=0;s<n.length&&s<t.length;s++)if(!Yn(t[s],n[s]))return!1;return!0}function mh(t,n,s,u,h,p){return pa=p,Xe=n,n.memoizedState=null,n.updateQueue=null,n.lanes=0,J.H=t===null||t.memoizedState===null?Ib:Oh,Pi=!1,p=s(u,h),Pi=!1,Hs&&(p=tb(n,s,u,h)),eb(t),p}function eb(t){J.H=Sl;var n=Ct!==null&&Ct.next!==null;if(pa=0,Qt=Ct=Xe=null,uu=!1,yl=0,js=null,n)throw Error(a(300));t===null||Zt||(t=t.dependencies,t!==null&&tu(t)&&(Zt=!0))}function tb(t,n,s,u){Xe=t;var h=0;do{if(Hs&&(js=null),yl=0,Hs=!1,25<=h)throw Error(a(301));if(h+=1,Qt=Ct=null,t.updateQueue!=null){var p=t.updateQueue;p.lastEffect=null,p.events=null,p.stores=null,p.memoCache!=null&&(p.memoCache.index=0)}J.H=Hb,p=n(s,u)}while(Hs);return p}function MA(){var t=J.H,n=t.useState()[0];return n=typeof n.then=="function"?bl(n):n,t=t.useState()[0],(Ct!==null?Ct.memoizedState:null)!==t&&(Xe.flags|=1024),n}function yh(){var t=du!==0;return du=0,t}function bh(t,n,s){n.updateQueue=t.updateQueue,n.flags&=-2053,t.lanes&=~s}function vh(t){if(uu){for(t=t.memoizedState;t!==null;){var n=t.queue;n!==null&&(n.pending=null),t=t.next}uu=!1}pa=0,Qt=Ct=Xe=null,Hs=!1,yl=du=0,js=null}function Tn(){var t={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Qt===null?Xe.memoizedState=Qt=t:Qt=Qt.next=t,Qt}function Kt(){if(Ct===null){var t=Xe.alternate;t=t!==null?t.memoizedState:null}else t=Ct.next;var n=Qt===null?Xe.memoizedState:Qt.next;if(n!==null)Qt=n,Ct=t;else{if(t===null)throw Xe.alternate===null?Error(a(467)):Error(a(310));Ct=t,t={memoizedState:Ct.memoizedState,baseState:Ct.baseState,baseQueue:Ct.baseQueue,queue:Ct.queue,next:null},Qt===null?Xe.memoizedState=Qt=t:Qt=Qt.next=t}return Qt}function fu(){return{lastEffect:null,events:null,stores:null,memoCache:null}}function bl(t){var n=yl;return yl+=1,js===null&&(js=[]),t=Vy(js,t,n),n=Xe,(Qt===null?n.memoizedState:Qt.next)===null&&(n=n.alternate,J.H=n===null||n.memoizedState===null?Ib:Oh),t}function hu(t){if(t!==null&&typeof t=="object"){if(typeof t.then=="function")return bl(t);if(t.$$typeof===O)return mn(t)}throw Error(a(438,String(t)))}function Sh(t){var n=null,s=Xe.updateQueue;if(s!==null&&(n=s.memoCache),n==null){var u=Xe.alternate;u!==null&&(u=u.updateQueue,u!==null&&(u=u.memoCache,u!=null&&(n={data:u.data.map(function(h){return h.slice()}),index:0})))}if(n==null&&(n={data:[],index:0}),s===null&&(s=fu(),Xe.updateQueue=s),s.memoCache=n,s=n.data[n.index],s===void 0)for(s=n.data[n.index]=Array(t),u=0;u<t;u++)s[u]=Ce;return n.index++,s}function ma(t,n){return typeof n=="function"?n(t):n}function gu(t){var n=Kt();return wh(n,Ct,t)}function wh(t,n,s){var u=t.queue;if(u===null)throw Error(a(311));u.lastRenderedReducer=s;var h=t.baseQueue,p=u.pending;if(p!==null){if(h!==null){var C=h.next;h.next=p.next,p.next=C}n.baseQueue=h=p,u.pending=null}if(p=t.baseState,h===null)t.memoizedState=p;else{n=h.next;var k=C=null,z=null,Y=n,ne=!1;do{var le=Y.lane&-536870913;if(le!==Y.lane?(it&le)===le:(pa&le)===le){var Q=Y.revertLane;if(Q===0)z!==null&&(z=z.next={lane:0,revertLane:0,gesture:null,action:Y.action,hasEagerState:Y.hasEagerState,eagerState:Y.eagerState,next:null}),le===Ls&&(ne=!0);else if((pa&Q)===Q){Y=Y.next,Q===Ls&&(ne=!0);continue}else le={lane:0,revertLane:Y.revertLane,gesture:null,action:Y.action,hasEagerState:Y.hasEagerState,eagerState:Y.eagerState,next:null},z===null?(k=z=le,C=p):z=z.next=le,Xe.lanes|=Q,ri|=Q;le=Y.action,Pi&&s(p,le),p=Y.hasEagerState?Y.eagerState:s(p,le)}else Q={lane:le,revertLane:Y.revertLane,gesture:Y.gesture,action:Y.action,hasEagerState:Y.hasEagerState,eagerState:Y.eagerState,next:null},z===null?(k=z=Q,C=p):z=z.next=Q,Xe.lanes|=le,ri|=le;Y=Y.next}while(Y!==null&&Y!==n);if(z===null?C=p:z.next=k,!Yn(p,t.memoizedState)&&(Zt=!0,ne&&(s=Ms,s!==null)))throw s;t.memoizedState=p,t.baseState=C,t.baseQueue=z,u.lastRenderedState=p}return h===null&&(u.lanes=0),[t.memoizedState,u.dispatch]}function Ch(t){var n=Kt(),s=n.queue;if(s===null)throw Error(a(311));s.lastRenderedReducer=t;var u=s.dispatch,h=s.pending,p=n.memoizedState;if(h!==null){s.pending=null;var C=h=h.next;do p=t(p,C.action),C=C.next;while(C!==h);Yn(p,n.memoizedState)||(Zt=!0),n.memoizedState=p,n.baseQueue===null&&(n.baseState=p),s.lastRenderedState=p}return[p,u]}function nb(t,n,s){var u=Xe,h=Kt(),p=ct;if(p){if(s===void 0)throw Error(a(407));s=s()}else s=n();var C=!Yn((Ct||h).memoizedState,s);if(C&&(h.memoizedState=s,Zt=!0),h=h.queue,Ah(ib.bind(null,u,h,t),[t]),h.getSnapshot!==n||C||Qt!==null&&Qt.memoizedState.tag&1){if(u.flags|=2048,zs(9,{destroy:void 0},ab.bind(null,u,h,s,n),null),xt===null)throw Error(a(349));p||(pa&127)!==0||rb(u,n,s)}return s}function rb(t,n,s){t.flags|=16384,t={getSnapshot:n,value:s},n=Xe.updateQueue,n===null?(n=fu(),Xe.updateQueue=n,n.stores=[t]):(s=n.stores,s===null?n.stores=[t]:s.push(t))}function ab(t,n,s,u){n.value=s,n.getSnapshot=u,sb(n)&&ob(t)}function ib(t,n,s){return s(function(){sb(n)&&ob(t)})}function sb(t){var n=t.getSnapshot;t=t.value;try{var s=n();return!Yn(t,s)}catch{return!0}}function ob(t){var n=Mi(t,2);n!==null&&qn(n,t,2)}function Eh(t){var n=Tn();if(typeof t=="function"){var s=t;if(t=s(),Pi){Cn(!0);try{s()}finally{Cn(!1)}}}return n.memoizedState=n.baseState=t,n.queue={pending:null,lanes:0,dispatch:null,lastRenderedReducer:ma,lastRenderedState:t},n}function lb(t,n,s,u){return t.baseState=s,wh(t,Ct,typeof u=="function"?u:ma)}function $A(t,n,s,u,h){if(yu(t))throw Error(a(485));if(t=n.action,t!==null){var p={payload:h,action:t,next:null,isTransition:!0,status:"pending",value:null,reason:null,listeners:[],then:function(C){p.listeners.push(C)}};J.T!==null?s(!0):p.isTransition=!1,u(p),s=n.pending,s===null?(p.next=n.pending=p,cb(n,p)):(p.next=s.next,n.pending=s.next=p)}}function cb(t,n){var s=n.action,u=n.payload,h=t.state;if(n.isTransition){var p=J.T,C={};J.T=C;try{var k=s(h,u),z=J.S;z!==null&&z(C,k),ub(t,n,k)}catch(Y){_h(t,n,Y)}finally{p!==null&&C.types!==null&&(p.types=C.types),J.T=p}}else try{p=s(h,u),ub(t,n,p)}catch(Y){_h(t,n,Y)}}function ub(t,n,s){s!==null&&typeof s=="object"&&typeof s.then=="function"?s.then(function(u){db(t,n,u)},function(u){return _h(t,n,u)}):db(t,n,s)}function db(t,n,s){n.status="fulfilled",n.value=s,fb(n),t.state=s,n=t.pending,n!==null&&(s=n.next,s===n?t.pending=null:(s=s.next,n.next=s,cb(t,s)))}function _h(t,n,s){var u=t.pending;if(t.pending=null,u!==null){u=u.next;do n.status="rejected",n.reason=s,fb(n),n=n.next;while(n!==u)}t.action=null}function fb(t){t=t.listeners;for(var n=0;n<t.length;n++)(0,t[n])()}function hb(t,n){return n}function gb(t,n){if(ct){var s=xt.formState;if(s!==null){e:{var u=Xe;if(ct){if(Rt){t:{for(var h=Rt,p=fr;h.nodeType!==8;){if(!p){h=null;break t}if(h=gr(h.nextSibling),h===null){h=null;break t}}p=h.data,h=p==="F!"||p==="F"?h:null}if(h){Rt=gr(h.nextSibling),u=h.data==="F!";break e}}Wa(u)}u=!1}u&&(n=s[0])}}return s=Tn(),s.memoizedState=s.baseState=n,u={pending:null,lanes:0,dispatch:null,lastRenderedReducer:hb,lastRenderedState:n},s.queue=u,s=Mb.bind(null,Xe,u),u.dispatch=s,u=Eh(!1),p=Dh.bind(null,Xe,!1,u.queue),u=Tn(),h={state:n,dispatch:null,action:t,pending:null},u.queue=h,s=$A.bind(null,Xe,h,p,s),h.dispatch=s,u.memoizedState=t,[n,s,!1]}function pb(t){var n=Kt();return mb(n,Ct,t)}function mb(t,n,s){if(n=wh(t,n,hb)[0],t=gu(ma)[0],typeof n=="object"&&n!==null&&typeof n.then=="function")try{var u=bl(n)}catch(C){throw C===$s?au:C}else u=n;n=Kt();var h=n.queue,p=h.dispatch;return s!==n.memoizedState&&(Xe.flags|=2048,zs(9,{destroy:void 0},NA.bind(null,h,s),null)),[u,p,t]}function NA(t,n){t.action=n}function yb(t){var n=Kt(),s=Ct;if(s!==null)return mb(n,s,t);Kt(),n=n.memoizedState,s=Kt();var u=s.queue.dispatch;return s.memoizedState=t,[n,u,!1]}function zs(t,n,s,u){return t={tag:t,create:s,deps:u,inst:n,next:null},n=Xe.updateQueue,n===null&&(n=fu(),Xe.updateQueue=n),s=n.lastEffect,s===null?n.lastEffect=t.next=t:(u=s.next,s.next=t,t.next=u,n.lastEffect=t),t}function bb(){return Kt().memoizedState}function pu(t,n,s,u){var h=Tn();Xe.flags|=t,h.memoizedState=zs(1|n,{destroy:void 0},s,u===void 0?null:u)}function mu(t,n,s,u){var h=Kt();u=u===void 0?null:u;var p=h.memoizedState.inst;Ct!==null&&u!==null&&ph(u,Ct.memoizedState.deps)?h.memoizedState=zs(n,p,s,u):(Xe.flags|=t,h.memoizedState=zs(1|n,p,s,u))}function vb(t,n){pu(8390656,8,t,n)}function Ah(t,n){mu(2048,8,t,n)}function IA(t){Xe.flags|=4;var n=Xe.updateQueue;if(n===null)n=fu(),Xe.updateQueue=n,n.events=[t];else{var s=n.events;s===null?n.events=[t]:s.push(t)}}function Sb(t){var n=Kt().memoizedState;return IA({ref:n,nextImpl:t}),function(){if((pt&2)!==0)throw Error(a(440));return n.impl.apply(void 0,arguments)}}function wb(t,n){return mu(4,2,t,n)}function Cb(t,n){return mu(4,4,t,n)}function Eb(t,n){if(typeof n=="function"){t=t();var s=n(t);return function(){typeof s=="function"?s():n(null)}}if(n!=null)return t=t(),n.current=t,function(){n.current=null}}function _b(t,n,s){s=s!=null?s.concat([t]):null,mu(4,4,Eb.bind(null,n,t),s)}function xh(){}function Ab(t,n){var s=Kt();n=n===void 0?null:n;var u=s.memoizedState;return n!==null&&ph(n,u[1])?u[0]:(s.memoizedState=[t,n],t)}function xb(t,n){var s=Kt();n=n===void 0?null:n;var u=s.memoizedState;if(n!==null&&ph(n,u[1]))return u[0];if(u=t(),Pi){Cn(!0);try{t()}finally{Cn(!1)}}return s.memoizedState=[u,n],u}function Th(t,n,s){return s===void 0||(pa&1073741824)!==0&&(it&261930)===0?t.memoizedState=n:(t.memoizedState=s,t=Tv(),Xe.lanes|=t,ri|=t,s)}function Tb(t,n,s,u){return Yn(s,n)?s:Is.current!==null?(t=Th(t,s,u),Yn(t,n)||(Zt=!0),t):(pa&42)===0||(pa&1073741824)!==0&&(it&261930)===0?(Zt=!0,t.memoizedState=s):(t=Tv(),Xe.lanes|=t,ri|=t,n)}function kb(t,n,s,u,h){var p=he.p;he.p=p!==0&&8>p?p:8;var C=J.T,k={};J.T=k,Dh(t,!1,n,s);try{var z=h(),Y=J.S;if(Y!==null&&Y(k,z),z!==null&&typeof z=="object"&&typeof z.then=="function"){var ne=OA(z,u);vl(t,n,ne,tr(t))}else vl(t,n,u,tr(t))}catch(le){vl(t,n,{then:function(){},status:"rejected",reason:le},tr())}finally{he.p=p,C!==null&&k.types!==null&&(C.types=k.types),J.T=C}}function HA(){}function kh(t,n,s,u){if(t.tag!==5)throw Error(a(476));var h=Rb(t).queue;kb(t,h,n,Ee,s===null?HA:function(){return Db(t),s(u)})}function Rb(t){var n=t.memoizedState;if(n!==null)return n;n={memoizedState:Ee,baseState:Ee,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ma,lastRenderedState:Ee},next:null};var s={};return n.next={memoizedState:s,baseState:s,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ma,lastRenderedState:s},next:null},t.memoizedState=n,t=t.alternate,t!==null&&(t.memoizedState=n),n}function Db(t){var n=Rb(t);n.next===null&&(n=t.alternate.memoizedState),vl(t,n.next.queue,{},tr())}function Rh(){return mn(Il)}function Ob(){return Kt().memoizedState}function Lb(){return Kt().memoizedState}function jA(t){for(var n=t.return;n!==null;){switch(n.tag){case 24:case 3:var s=tr();t=Ja(s);var u=Qa(n,t,s);u!==null&&(qn(u,n,s),gl(u,n,s)),n={cache:ah()},t.payload=n;return}n=n.return}}function zA(t,n,s){var u=tr();s={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null},yu(t)?$b(n,s):(s=Kf(t,n,s,u),s!==null&&(qn(s,t,u),Nb(s,n,u)))}function Mb(t,n,s){var u=tr();vl(t,n,s,u)}function vl(t,n,s,u){var h={lane:u,revertLane:0,gesture:null,action:s,hasEagerState:!1,eagerState:null,next:null};if(yu(t))$b(n,h);else{var p=t.alternate;if(t.lanes===0&&(p===null||p.lanes===0)&&(p=n.lastRenderedReducer,p!==null))try{var C=n.lastRenderedState,k=p(C,s);if(h.hasEagerState=!0,h.eagerState=k,Yn(k,C))return Jc(t,n,h,0),xt===null&&Xc(),!1}catch{}finally{}if(s=Kf(t,n,h,u),s!==null)return qn(s,t,u),Nb(s,n,u),!0}return!1}function Dh(t,n,s,u){if(u={lane:2,revertLane:lg(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},yu(t)){if(n)throw Error(a(479))}else n=Kf(t,s,u,2),n!==null&&qn(n,t,2)}function yu(t){var n=t.alternate;return t===Xe||n!==null&&n===Xe}function $b(t,n){Hs=uu=!0;var s=t.pending;s===null?n.next=n:(n.next=s.next,s.next=n),t.pending=n}function Nb(t,n,s){if((s&4194048)!==0){var u=n.lanes;u&=t.pendingLanes,s|=u,n.lanes=s,Se(t,s)}}var Sl={readContext:mn,use:hu,useCallback:Pt,useContext:Pt,useEffect:Pt,useImperativeHandle:Pt,useLayoutEffect:Pt,useInsertionEffect:Pt,useMemo:Pt,useReducer:Pt,useRef:Pt,useState:Pt,useDebugValue:Pt,useDeferredValue:Pt,useTransition:Pt,useSyncExternalStore:Pt,useId:Pt,useHostTransitionStatus:Pt,useFormState:Pt,useActionState:Pt,useOptimistic:Pt,useMemoCache:Pt,useCacheRefresh:Pt};Sl.useEffectEvent=Pt;var Ib={readContext:mn,use:hu,useCallback:function(t,n){return Tn().memoizedState=[t,n===void 0?null:n],t},useContext:mn,useEffect:vb,useImperativeHandle:function(t,n,s){s=s!=null?s.concat([t]):null,pu(4194308,4,Eb.bind(null,n,t),s)},useLayoutEffect:function(t,n){return pu(4194308,4,t,n)},useInsertionEffect:function(t,n){pu(4,2,t,n)},useMemo:function(t,n){var s=Tn();n=n===void 0?null:n;var u=t();if(Pi){Cn(!0);try{t()}finally{Cn(!1)}}return s.memoizedState=[u,n],u},useReducer:function(t,n,s){var u=Tn();if(s!==void 0){var h=s(n);if(Pi){Cn(!0);try{s(n)}finally{Cn(!1)}}}else h=n;return u.memoizedState=u.baseState=h,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:h},u.queue=t,t=t.dispatch=zA.bind(null,Xe,t),[u.memoizedState,t]},useRef:function(t){var n=Tn();return t={current:t},n.memoizedState=t},useState:function(t){t=Eh(t);var n=t.queue,s=Mb.bind(null,Xe,n);return n.dispatch=s,[t.memoizedState,s]},useDebugValue:xh,useDeferredValue:function(t,n){var s=Tn();return Th(s,t,n)},useTransition:function(){var t=Eh(!1);return t=kb.bind(null,Xe,t.queue,!0,!1),Tn().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,n,s){var u=Xe,h=Tn();if(ct){if(s===void 0)throw Error(a(407));s=s()}else{if(s=n(),xt===null)throw Error(a(349));(it&127)!==0||rb(u,n,s)}h.memoizedState=s;var p={value:s,getSnapshot:n};return h.queue=p,vb(ib.bind(null,u,p,t),[t]),u.flags|=2048,zs(9,{destroy:void 0},ab.bind(null,u,p,s,n),null),s},useId:function(){var t=Tn(),n=xt.identifierPrefix;if(ct){var s=Wr,u=Kr;s=(u&~(1<<32-nn(u)-1)).toString(32)+s,n="_"+n+"R_"+s,s=du++,0<s&&(n+="H"+s.toString(32)),n+="_"}else s=LA++,n="_"+n+"r_"+s.toString(32)+"_";return t.memoizedState=n},useHostTransitionStatus:Rh,useFormState:gb,useActionState:gb,useOptimistic:function(t){var n=Tn();n.memoizedState=n.baseState=t;var s={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return n.queue=s,n=Dh.bind(null,Xe,!0,s),s.dispatch=n,[t,n]},useMemoCache:Sh,useCacheRefresh:function(){return Tn().memoizedState=jA.bind(null,Xe)},useEffectEvent:function(t){var n=Tn(),s={impl:t};return n.memoizedState=s,function(){if((pt&2)!==0)throw Error(a(440));return s.impl.apply(void 0,arguments)}}},Oh={readContext:mn,use:hu,useCallback:Ab,useContext:mn,useEffect:Ah,useImperativeHandle:_b,useInsertionEffect:wb,useLayoutEffect:Cb,useMemo:xb,useReducer:gu,useRef:bb,useState:function(){return gu(ma)},useDebugValue:xh,useDeferredValue:function(t,n){var s=Kt();return Tb(s,Ct.memoizedState,t,n)},useTransition:function(){var t=gu(ma)[0],n=Kt().memoizedState;return[typeof t=="boolean"?t:bl(t),n]},useSyncExternalStore:nb,useId:Ob,useHostTransitionStatus:Rh,useFormState:pb,useActionState:pb,useOptimistic:function(t,n){var s=Kt();return lb(s,Ct,t,n)},useMemoCache:Sh,useCacheRefresh:Lb};Oh.useEffectEvent=Sb;var Hb={readContext:mn,use:hu,useCallback:Ab,useContext:mn,useEffect:Ah,useImperativeHandle:_b,useInsertionEffect:wb,useLayoutEffect:Cb,useMemo:xb,useReducer:Ch,useRef:bb,useState:function(){return Ch(ma)},useDebugValue:xh,useDeferredValue:function(t,n){var s=Kt();return Ct===null?Th(s,t,n):Tb(s,Ct.memoizedState,t,n)},useTransition:function(){var t=Ch(ma)[0],n=Kt().memoizedState;return[typeof t=="boolean"?t:bl(t),n]},useSyncExternalStore:nb,useId:Ob,useHostTransitionStatus:Rh,useFormState:yb,useActionState:yb,useOptimistic:function(t,n){var s=Kt();return Ct!==null?lb(s,Ct,t,n):(s.baseState=t,[t,s.queue.dispatch])},useMemoCache:Sh,useCacheRefresh:Lb};Hb.useEffectEvent=Sb;function Lh(t,n,s,u){n=t.memoizedState,s=s(u,n),s=s==null?n:y({},n,s),t.memoizedState=s,t.lanes===0&&(t.updateQueue.baseState=s)}var Mh={enqueueSetState:function(t,n,s){t=t._reactInternals;var u=tr(),h=Ja(u);h.payload=n,s!=null&&(h.callback=s),n=Qa(t,h,u),n!==null&&(qn(n,t,u),gl(n,t,u))},enqueueReplaceState:function(t,n,s){t=t._reactInternals;var u=tr(),h=Ja(u);h.tag=1,h.payload=n,s!=null&&(h.callback=s),n=Qa(t,h,u),n!==null&&(qn(n,t,u),gl(n,t,u))},enqueueForceUpdate:function(t,n){t=t._reactInternals;var s=tr(),u=Ja(s);u.tag=2,n!=null&&(u.callback=n),n=Qa(t,u,s),n!==null&&(qn(n,t,s),gl(n,t,s))}};function jb(t,n,s,u,h,p,C){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(u,p,C):n.prototype&&n.prototype.isPureReactComponent?!sl(s,u)||!sl(h,p):!0}function zb(t,n,s,u){t=n.state,typeof n.componentWillReceiveProps=="function"&&n.componentWillReceiveProps(s,u),typeof n.UNSAFE_componentWillReceiveProps=="function"&&n.UNSAFE_componentWillReceiveProps(s,u),n.state!==t&&Mh.enqueueReplaceState(n,n.state,null)}function qi(t,n){var s=n;if("ref"in n){s={};for(var u in n)u!=="ref"&&(s[u]=n[u])}if(t=t.defaultProps){s===n&&(s=y({},s));for(var h in t)s[h]===void 0&&(s[h]=t[h])}return s}function Ub(t){Yc(t)}function Bb(t){console.error(t)}function Pb(t){Yc(t)}function bu(t,n){try{var s=t.onUncaughtError;s(n.value,{componentStack:n.stack})}catch(u){setTimeout(function(){throw u})}}function qb(t,n,s){try{var u=t.onCaughtError;u(s.value,{componentStack:s.stack,errorBoundary:n.tag===1?n.stateNode:null})}catch(h){setTimeout(function(){throw h})}}function $h(t,n,s){return s=Ja(s),s.tag=3,s.payload={element:null},s.callback=function(){bu(t,n)},s}function Gb(t){return t=Ja(t),t.tag=3,t}function Vb(t,n,s,u){var h=s.type.getDerivedStateFromError;if(typeof h=="function"){var p=u.value;t.payload=function(){return h(p)},t.callback=function(){qb(n,s,u)}}var C=s.stateNode;C!==null&&typeof C.componentDidCatch=="function"&&(t.callback=function(){qb(n,s,u),typeof h!="function"&&(ai===null?ai=new Set([this]):ai.add(this));var k=u.stack;this.componentDidCatch(u.value,{componentStack:k!==null?k:""})})}function UA(t,n,s,u,h){if(s.flags|=32768,u!==null&&typeof u=="object"&&typeof u.then=="function"){if(n=s.alternate,n!==null&&Os(n,s,h,!0),s=Jn.current,s!==null){switch(s.tag){case 31:case 13:return hr===null?Du():s.alternate===null&&qt===0&&(qt=3),s.flags&=-257,s.flags|=65536,s.lanes=h,u===iu?s.flags|=16384:(n=s.updateQueue,n===null?s.updateQueue=new Set([u]):n.add(u),ig(t,u,h)),!1;case 22:return s.flags|=65536,u===iu?s.flags|=16384:(n=s.updateQueue,n===null?(n={transitions:null,markerInstances:null,retryQueue:new Set([u])},s.updateQueue=n):(s=n.retryQueue,s===null?n.retryQueue=new Set([u]):s.add(u)),ig(t,u,h)),!1}throw Error(a(435,s.tag))}return ig(t,u,h),Du(),!1}if(ct)return n=Jn.current,n!==null?((n.flags&65536)===0&&(n.flags|=256),n.flags|=65536,n.lanes=h,u!==Zf&&(t=Error(a(422),{cause:u}),cl(cr(t,s)))):(u!==Zf&&(n=Error(a(423),{cause:u}),cl(cr(n,s))),t=t.current.alternate,t.flags|=65536,h&=-h,t.lanes|=h,u=cr(u,s),h=$h(t.stateNode,u,h),uh(t,h),qt!==4&&(qt=2)),!1;var p=Error(a(520),{cause:u});if(p=cr(p,s),kl===null?kl=[p]:kl.push(p),qt!==4&&(qt=2),n===null)return!0;u=cr(u,s),s=n;do{switch(s.tag){case 3:return s.flags|=65536,t=h&-h,s.lanes|=t,t=$h(s.stateNode,u,t),uh(s,t),!1;case 1:if(n=s.type,p=s.stateNode,(s.flags&128)===0&&(typeof n.getDerivedStateFromError=="function"||p!==null&&typeof p.componentDidCatch=="function"&&(ai===null||!ai.has(p))))return s.flags|=65536,h&=-h,s.lanes|=h,h=Gb(h),Vb(h,t,s,u),uh(s,h),!1}s=s.return}while(s!==null);return!1}var Nh=Error(a(461)),Zt=!1;function yn(t,n,s,u){n.child=t===null?Yy(n,null,s,u):Bi(n,t.child,s,u)}function Fb(t,n,s,u,h){s=s.render;var p=n.ref;if("ref"in u){var C={};for(var k in u)k!=="ref"&&(C[k]=u[k])}else C=u;return Hi(n),u=mh(t,n,s,C,p,h),k=yh(),t!==null&&!Zt?(bh(t,n,h),ya(t,n,h)):(ct&&k&&Jf(n),n.flags|=1,yn(t,n,u,h),n.child)}function Kb(t,n,s,u,h){if(t===null){var p=s.type;return typeof p=="function"&&!Wf(p)&&p.defaultProps===void 0&&s.compare===null?(n.tag=15,n.type=p,Wb(t,n,p,u,h)):(t=Zc(s.type,null,u,n,n.mode,h),t.ref=n.ref,t.return=n,n.child=t)}if(p=t.child,!qh(t,h)){var C=p.memoizedProps;if(s=s.compare,s=s!==null?s:sl,s(C,u)&&t.ref===n.ref)return ya(t,n,h)}return n.flags|=1,t=da(p,u),t.ref=n.ref,t.return=n,n.child=t}function Wb(t,n,s,u,h){if(t!==null){var p=t.memoizedProps;if(sl(p,u)&&t.ref===n.ref)if(Zt=!1,n.pendingProps=u=p,qh(t,h))(t.flags&131072)!==0&&(Zt=!0);else return n.lanes=t.lanes,ya(t,n,h)}return Ih(t,n,s,u,h)}function Yb(t,n,s,u){var h=u.children,p=t!==null?t.memoizedState:null;if(t===null&&n.stateNode===null&&(n.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),u.mode==="hidden"){if((n.flags&128)!==0){if(p=p!==null?p.baseLanes|s:s,t!==null){for(u=n.child=t.child,h=0;u!==null;)h=h|u.lanes|u.childLanes,u=u.sibling;u=h&~p}else u=0,n.child=null;return Xb(t,n,p,s,u)}if((s&536870912)!==0)n.memoizedState={baseLanes:0,cachePool:null},t!==null&&ru(n,p!==null?p.cachePool:null),p!==null?Qy(n,p):fh(),Zy(n);else return u=n.lanes=536870912,Xb(t,n,p!==null?p.baseLanes|s:s,s,u)}else p!==null?(ru(n,p.cachePool),Qy(n,p),ei(),n.memoizedState=null):(t!==null&&ru(n,null),fh(),ei());return yn(t,n,h,s),n.child}function wl(t,n){return t!==null&&t.tag===22||n.stateNode!==null||(n.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),n.sibling}function Xb(t,n,s,u,h){var p=sh();return p=p===null?null:{parent:Jt._currentValue,pool:p},n.memoizedState={baseLanes:s,cachePool:p},t!==null&&ru(n,null),fh(),Zy(n),t!==null&&Os(t,n,u,!0),n.childLanes=h,null}function vu(t,n){return n=wu({mode:n.mode,children:n.children},t.mode),n.ref=t.ref,t.child=n,n.return=t,n}function Jb(t,n,s){return Bi(n,t.child,null,s),t=vu(n,n.pendingProps),t.flags|=2,Qn(n),n.memoizedState=null,t}function BA(t,n,s){var u=n.pendingProps,h=(n.flags&128)!==0;if(n.flags&=-129,t===null){if(ct){if(u.mode==="hidden")return t=vu(n,u),n.lanes=536870912,wl(null,t);if(gh(n),(t=Rt)?(t=cS(t,fr),t=t!==null&&t.data==="&"?t:null,t!==null&&(n.memoizedState={dehydrated:t,treeContext:Fa!==null?{id:Kr,overflow:Wr}:null,retryLane:536870912,hydrationErrors:null},s=$y(t),s.return=n,n.child=s,pn=n,Rt=null)):t=null,t===null)throw Wa(n);return n.lanes=536870912,null}return vu(n,u)}var p=t.memoizedState;if(p!==null){var C=p.dehydrated;if(gh(n),h)if(n.flags&256)n.flags&=-257,n=Jb(t,n,s);else if(n.memoizedState!==null)n.child=t.child,n.flags|=128,n=null;else throw Error(a(558));else if(Zt||Os(t,n,s,!1),h=(s&t.childLanes)!==0,Zt||h){if(u=xt,u!==null&&(C=Me(u,s),C!==0&&C!==p.retryLane))throw p.retryLane=C,Mi(t,C),qn(u,t,C),Nh;Du(),n=Jb(t,n,s)}else t=p.treeContext,Rt=gr(C.nextSibling),pn=n,ct=!0,Ka=null,fr=!1,t!==null&&Hy(n,t),n=vu(n,u),n.flags|=4096;return n}return t=da(t.child,{mode:u.mode,children:u.children}),t.ref=n.ref,n.child=t,t.return=n,t}function Su(t,n){var s=n.ref;if(s===null)t!==null&&t.ref!==null&&(n.flags|=4194816);else{if(typeof s!="function"&&typeof s!="object")throw Error(a(284));(t===null||t.ref!==s)&&(n.flags|=4194816)}}function Ih(t,n,s,u,h){return Hi(n),s=mh(t,n,s,u,void 0,h),u=yh(),t!==null&&!Zt?(bh(t,n,h),ya(t,n,h)):(ct&&u&&Jf(n),n.flags|=1,yn(t,n,s,h),n.child)}function Qb(t,n,s,u,h,p){return Hi(n),n.updateQueue=null,s=tb(n,u,s,h),eb(t),u=yh(),t!==null&&!Zt?(bh(t,n,p),ya(t,n,p)):(ct&&u&&Jf(n),n.flags|=1,yn(t,n,s,p),n.child)}function Zb(t,n,s,u,h){if(Hi(n),n.stateNode===null){var p=Ts,C=s.contextType;typeof C=="object"&&C!==null&&(p=mn(C)),p=new s(u,p),n.memoizedState=p.state!==null&&p.state!==void 0?p.state:null,p.updater=Mh,n.stateNode=p,p._reactInternals=n,p=n.stateNode,p.props=u,p.state=n.memoizedState,p.refs={},lh(n),C=s.contextType,p.context=typeof C=="object"&&C!==null?mn(C):Ts,p.state=n.memoizedState,C=s.getDerivedStateFromProps,typeof C=="function"&&(Lh(n,s,C,u),p.state=n.memoizedState),typeof s.getDerivedStateFromProps=="function"||typeof p.getSnapshotBeforeUpdate=="function"||typeof p.UNSAFE_componentWillMount!="function"&&typeof p.componentWillMount!="function"||(C=p.state,typeof p.componentWillMount=="function"&&p.componentWillMount(),typeof p.UNSAFE_componentWillMount=="function"&&p.UNSAFE_componentWillMount(),C!==p.state&&Mh.enqueueReplaceState(p,p.state,null),ml(n,u,p,h),pl(),p.state=n.memoizedState),typeof p.componentDidMount=="function"&&(n.flags|=4194308),u=!0}else if(t===null){p=n.stateNode;var k=n.memoizedProps,z=qi(s,k);p.props=z;var Y=p.context,ne=s.contextType;C=Ts,typeof ne=="object"&&ne!==null&&(C=mn(ne));var le=s.getDerivedStateFromProps;ne=typeof le=="function"||typeof p.getSnapshotBeforeUpdate=="function",k=n.pendingProps!==k,ne||typeof p.UNSAFE_componentWillReceiveProps!="function"&&typeof p.componentWillReceiveProps!="function"||(k||Y!==C)&&zb(n,p,u,C),Xa=!1;var Q=n.memoizedState;p.state=Q,ml(n,u,p,h),pl(),Y=n.memoizedState,k||Q!==Y||Xa?(typeof le=="function"&&(Lh(n,s,le,u),Y=n.memoizedState),(z=Xa||jb(n,s,z,u,Q,Y,C))?(ne||typeof p.UNSAFE_componentWillMount!="function"&&typeof p.componentWillMount!="function"||(typeof p.componentWillMount=="function"&&p.componentWillMount(),typeof p.UNSAFE_componentWillMount=="function"&&p.UNSAFE_componentWillMount()),typeof p.componentDidMount=="function"&&(n.flags|=4194308)):(typeof p.componentDidMount=="function"&&(n.flags|=4194308),n.memoizedProps=u,n.memoizedState=Y),p.props=u,p.state=Y,p.context=C,u=z):(typeof p.componentDidMount=="function"&&(n.flags|=4194308),u=!1)}else{p=n.stateNode,ch(t,n),C=n.memoizedProps,ne=qi(s,C),p.props=ne,le=n.pendingProps,Q=p.context,Y=s.contextType,z=Ts,typeof Y=="object"&&Y!==null&&(z=mn(Y)),k=s.getDerivedStateFromProps,(Y=typeof k=="function"||typeof p.getSnapshotBeforeUpdate=="function")||typeof p.UNSAFE_componentWillReceiveProps!="function"&&typeof p.componentWillReceiveProps!="function"||(C!==le||Q!==z)&&zb(n,p,u,z),Xa=!1,Q=n.memoizedState,p.state=Q,ml(n,u,p,h),pl();var te=n.memoizedState;C!==le||Q!==te||Xa||t!==null&&t.dependencies!==null&&tu(t.dependencies)?(typeof k=="function"&&(Lh(n,s,k,u),te=n.memoizedState),(ne=Xa||jb(n,s,ne,u,Q,te,z)||t!==null&&t.dependencies!==null&&tu(t.dependencies))?(Y||typeof p.UNSAFE_componentWillUpdate!="function"&&typeof p.componentWillUpdate!="function"||(typeof p.componentWillUpdate=="function"&&p.componentWillUpdate(u,te,z),typeof p.UNSAFE_componentWillUpdate=="function"&&p.UNSAFE_componentWillUpdate(u,te,z)),typeof p.componentDidUpdate=="function"&&(n.flags|=4),typeof p.getSnapshotBeforeUpdate=="function"&&(n.flags|=1024)):(typeof p.componentDidUpdate!="function"||C===t.memoizedProps&&Q===t.memoizedState||(n.flags|=4),typeof p.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&Q===t.memoizedState||(n.flags|=1024),n.memoizedProps=u,n.memoizedState=te),p.props=u,p.state=te,p.context=z,u=ne):(typeof p.componentDidUpdate!="function"||C===t.memoizedProps&&Q===t.memoizedState||(n.flags|=4),typeof p.getSnapshotBeforeUpdate!="function"||C===t.memoizedProps&&Q===t.memoizedState||(n.flags|=1024),u=!1)}return p=u,Su(t,n),u=(n.flags&128)!==0,p||u?(p=n.stateNode,s=u&&typeof s.getDerivedStateFromError!="function"?null:p.render(),n.flags|=1,t!==null&&u?(n.child=Bi(n,t.child,null,h),n.child=Bi(n,null,s,h)):yn(t,n,s,h),n.memoizedState=p.state,t=n.child):t=ya(t,n,h),t}function ev(t,n,s,u){return Ni(),n.flags|=256,yn(t,n,s,u),n.child}var Hh={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function jh(t){return{baseLanes:t,cachePool:qy()}}function zh(t,n,s){return t=t!==null?t.childLanes&~s:0,n&&(t|=er),t}function tv(t,n,s){var u=n.pendingProps,h=!1,p=(n.flags&128)!==0,C;if((C=p)||(C=t!==null&&t.memoizedState===null?!1:(Ft.current&2)!==0),C&&(h=!0,n.flags&=-129),C=(n.flags&32)!==0,n.flags&=-33,t===null){if(ct){if(h?Za(n):ei(),(t=Rt)?(t=cS(t,fr),t=t!==null&&t.data!=="&"?t:null,t!==null&&(n.memoizedState={dehydrated:t,treeContext:Fa!==null?{id:Kr,overflow:Wr}:null,retryLane:536870912,hydrationErrors:null},s=$y(t),s.return=n,n.child=s,pn=n,Rt=null)):t=null,t===null)throw Wa(n);return wg(t)?n.lanes=32:n.lanes=536870912,null}var k=u.children;return u=u.fallback,h?(ei(),h=n.mode,k=wu({mode:"hidden",children:k},h),u=$i(u,h,s,null),k.return=n,u.return=n,k.sibling=u,n.child=k,u=n.child,u.memoizedState=jh(s),u.childLanes=zh(t,C,s),n.memoizedState=Hh,wl(null,u)):(Za(n),Uh(n,k))}var z=t.memoizedState;if(z!==null&&(k=z.dehydrated,k!==null)){if(p)n.flags&256?(Za(n),n.flags&=-257,n=Bh(t,n,s)):n.memoizedState!==null?(ei(),n.child=t.child,n.flags|=128,n=null):(ei(),k=u.fallback,h=n.mode,u=wu({mode:"visible",children:u.children},h),k=$i(k,h,s,null),k.flags|=2,u.return=n,k.return=n,u.sibling=k,n.child=u,Bi(n,t.child,null,s),u=n.child,u.memoizedState=jh(s),u.childLanes=zh(t,C,s),n.memoizedState=Hh,n=wl(null,u));else if(Za(n),wg(k)){if(C=k.nextSibling&&k.nextSibling.dataset,C)var Y=C.dgst;C=Y,u=Error(a(419)),u.stack="",u.digest=C,cl({value:u,source:null,stack:null}),n=Bh(t,n,s)}else if(Zt||Os(t,n,s,!1),C=(s&t.childLanes)!==0,Zt||C){if(C=xt,C!==null&&(u=Me(C,s),u!==0&&u!==z.retryLane))throw z.retryLane=u,Mi(t,u),qn(C,t,u),Nh;Sg(k)||Du(),n=Bh(t,n,s)}else Sg(k)?(n.flags|=192,n.child=t.child,n=null):(t=z.treeContext,Rt=gr(k.nextSibling),pn=n,ct=!0,Ka=null,fr=!1,t!==null&&Hy(n,t),n=Uh(n,u.children),n.flags|=4096);return n}return h?(ei(),k=u.fallback,h=n.mode,z=t.child,Y=z.sibling,u=da(z,{mode:"hidden",children:u.children}),u.subtreeFlags=z.subtreeFlags&65011712,Y!==null?k=da(Y,k):(k=$i(k,h,s,null),k.flags|=2),k.return=n,u.return=n,u.sibling=k,n.child=u,wl(null,u),u=n.child,k=t.child.memoizedState,k===null?k=jh(s):(h=k.cachePool,h!==null?(z=Jt._currentValue,h=h.parent!==z?{parent:z,pool:z}:h):h=qy(),k={baseLanes:k.baseLanes|s,cachePool:h}),u.memoizedState=k,u.childLanes=zh(t,C,s),n.memoizedState=Hh,wl(t.child,u)):(Za(n),s=t.child,t=s.sibling,s=da(s,{mode:"visible",children:u.children}),s.return=n,s.sibling=null,t!==null&&(C=n.deletions,C===null?(n.deletions=[t],n.flags|=16):C.push(t)),n.child=s,n.memoizedState=null,s)}function Uh(t,n){return n=wu({mode:"visible",children:n},t.mode),n.return=t,t.child=n}function wu(t,n){return t=Xn(22,t,null,n),t.lanes=0,t}function Bh(t,n,s){return Bi(n,t.child,null,s),t=Uh(n,n.pendingProps.children),t.flags|=2,n.memoizedState=null,t}function nv(t,n,s){t.lanes|=n;var u=t.alternate;u!==null&&(u.lanes|=n),nh(t.return,n,s)}function Ph(t,n,s,u,h,p){var C=t.memoizedState;C===null?t.memoizedState={isBackwards:n,rendering:null,renderingStartTime:0,last:u,tail:s,tailMode:h,treeForkCount:p}:(C.isBackwards=n,C.rendering=null,C.renderingStartTime=0,C.last=u,C.tail=s,C.tailMode=h,C.treeForkCount=p)}function rv(t,n,s){var u=n.pendingProps,h=u.revealOrder,p=u.tail;u=u.children;var C=Ft.current,k=(C&2)!==0;if(k?(C=C&1|2,n.flags|=128):C&=1,ge(Ft,C),yn(t,n,u,s),u=ct?ll:0,!k&&t!==null&&(t.flags&128)!==0)e:for(t=n.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&nv(t,s,n);else if(t.tag===19)nv(t,s,n);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===n)break e;for(;t.sibling===null;){if(t.return===null||t.return===n)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}switch(h){case"forwards":for(s=n.child,h=null;s!==null;)t=s.alternate,t!==null&&cu(t)===null&&(h=s),s=s.sibling;s=h,s===null?(h=n.child,n.child=null):(h=s.sibling,s.sibling=null),Ph(n,!1,h,s,p,u);break;case"backwards":case"unstable_legacy-backwards":for(s=null,h=n.child,n.child=null;h!==null;){if(t=h.alternate,t!==null&&cu(t)===null){n.child=h;break}t=h.sibling,h.sibling=s,s=h,h=t}Ph(n,!0,s,null,p,u);break;case"together":Ph(n,!1,null,null,void 0,u);break;default:n.memoizedState=null}return n.child}function ya(t,n,s){if(t!==null&&(n.dependencies=t.dependencies),ri|=n.lanes,(s&n.childLanes)===0)if(t!==null){if(Os(t,n,s,!1),(s&n.childLanes)===0)return null}else return null;if(t!==null&&n.child!==t.child)throw Error(a(153));if(n.child!==null){for(t=n.child,s=da(t,t.pendingProps),n.child=s,s.return=n;t.sibling!==null;)t=t.sibling,s=s.sibling=da(t,t.pendingProps),s.return=n;s.sibling=null}return n.child}function qh(t,n){return(t.lanes&n)!==0?!0:(t=t.dependencies,!!(t!==null&&tu(t)))}function PA(t,n,s){switch(n.tag){case 3:ze(n,n.stateNode.containerInfo),Ya(n,Jt,t.memoizedState.cache),Ni();break;case 27:case 5:Ge(n);break;case 4:ze(n,n.stateNode.containerInfo);break;case 10:Ya(n,n.type,n.memoizedProps.value);break;case 31:if(n.memoizedState!==null)return n.flags|=128,gh(n),null;break;case 13:var u=n.memoizedState;if(u!==null)return u.dehydrated!==null?(Za(n),n.flags|=128,null):(s&n.child.childLanes)!==0?tv(t,n,s):(Za(n),t=ya(t,n,s),t!==null?t.sibling:null);Za(n);break;case 19:var h=(t.flags&128)!==0;if(u=(s&n.childLanes)!==0,u||(Os(t,n,s,!1),u=(s&n.childLanes)!==0),h){if(u)return rv(t,n,s);n.flags|=128}if(h=n.memoizedState,h!==null&&(h.rendering=null,h.tail=null,h.lastEffect=null),ge(Ft,Ft.current),u)break;return null;case 22:return n.lanes=0,Yb(t,n,s,n.pendingProps);case 24:Ya(n,Jt,t.memoizedState.cache)}return ya(t,n,s)}function av(t,n,s){if(t!==null)if(t.memoizedProps!==n.pendingProps)Zt=!0;else{if(!qh(t,s)&&(n.flags&128)===0)return Zt=!1,PA(t,n,s);Zt=(t.flags&131072)!==0}else Zt=!1,ct&&(n.flags&1048576)!==0&&Iy(n,ll,n.index);switch(n.lanes=0,n.tag){case 16:e:{var u=n.pendingProps;if(t=zi(n.elementType),n.type=t,typeof t=="function")Wf(t)?(u=qi(t,u),n.tag=1,n=Zb(null,n,t,u,s)):(n.tag=0,n=Ih(null,n,t,u,s));else{if(t!=null){var h=t.$$typeof;if(h===L){n.tag=11,n=Fb(null,n,t,u,s);break e}else if(h===B){n.tag=14,n=Kb(null,n,t,u,s);break e}}throw n=qe(t)||t,Error(a(306,n,""))}}return n;case 0:return Ih(t,n,n.type,n.pendingProps,s);case 1:return u=n.type,h=qi(u,n.pendingProps),Zb(t,n,u,h,s);case 3:e:{if(ze(n,n.stateNode.containerInfo),t===null)throw Error(a(387));u=n.pendingProps;var p=n.memoizedState;h=p.element,ch(t,n),ml(n,u,null,s);var C=n.memoizedState;if(u=C.cache,Ya(n,Jt,u),u!==p.cache&&rh(n,[Jt],s,!0),pl(),u=C.element,p.isDehydrated)if(p={element:u,isDehydrated:!1,cache:C.cache},n.updateQueue.baseState=p,n.memoizedState=p,n.flags&256){n=ev(t,n,u,s);break e}else if(u!==h){h=cr(Error(a(424)),n),cl(h),n=ev(t,n,u,s);break e}else{switch(t=n.stateNode.containerInfo,t.nodeType){case 9:t=t.body;break;default:t=t.nodeName==="HTML"?t.ownerDocument.body:t}for(Rt=gr(t.firstChild),pn=n,ct=!0,Ka=null,fr=!0,s=Yy(n,null,u,s),n.child=s;s;)s.flags=s.flags&-3|4096,s=s.sibling}else{if(Ni(),u===h){n=ya(t,n,s);break e}yn(t,n,u,s)}n=n.child}return n;case 26:return Su(t,n),t===null?(s=pS(n.type,null,n.pendingProps,null))?n.memoizedState=s:ct||(s=n.type,t=n.pendingProps,u=Hu(ae.current).createElement(s),u[Nt]=n,u[gn]=t,bn(u,s,t),Xt(u),n.stateNode=u):n.memoizedState=pS(n.type,t.memoizedProps,n.pendingProps,t.memoizedState),null;case 27:return Ge(n),t===null&&ct&&(u=n.stateNode=fS(n.type,n.pendingProps,ae.current),pn=n,fr=!0,h=Rt,li(n.type)?(Cg=h,Rt=gr(u.firstChild)):Rt=h),yn(t,n,n.pendingProps.children,s),Su(t,n),t===null&&(n.flags|=4194304),n.child;case 5:return t===null&&ct&&((h=u=Rt)&&(u=bx(u,n.type,n.pendingProps,fr),u!==null?(n.stateNode=u,pn=n,Rt=gr(u.firstChild),fr=!1,h=!0):h=!1),h||Wa(n)),Ge(n),h=n.type,p=n.pendingProps,C=t!==null?t.memoizedProps:null,u=p.children,yg(h,p)?u=null:C!==null&&yg(h,C)&&(n.flags|=32),n.memoizedState!==null&&(h=mh(t,n,MA,null,null,s),Il._currentValue=h),Su(t,n),yn(t,n,u,s),n.child;case 6:return t===null&&ct&&((t=s=Rt)&&(s=vx(s,n.pendingProps,fr),s!==null?(n.stateNode=s,pn=n,Rt=null,t=!0):t=!1),t||Wa(n)),null;case 13:return tv(t,n,s);case 4:return ze(n,n.stateNode.containerInfo),u=n.pendingProps,t===null?n.child=Bi(n,null,u,s):yn(t,n,u,s),n.child;case 11:return Fb(t,n,n.type,n.pendingProps,s);case 7:return yn(t,n,n.pendingProps,s),n.child;case 8:return yn(t,n,n.pendingProps.children,s),n.child;case 12:return yn(t,n,n.pendingProps.children,s),n.child;case 10:return u=n.pendingProps,Ya(n,n.type,u.value),yn(t,n,u.children,s),n.child;case 9:return h=n.type._context,u=n.pendingProps.children,Hi(n),h=mn(h),u=u(h),n.flags|=1,yn(t,n,u,s),n.child;case 14:return Kb(t,n,n.type,n.pendingProps,s);case 15:return Wb(t,n,n.type,n.pendingProps,s);case 19:return rv(t,n,s);case 31:return BA(t,n,s);case 22:return Yb(t,n,s,n.pendingProps);case 24:return Hi(n),u=mn(Jt),t===null?(h=sh(),h===null&&(h=xt,p=ah(),h.pooledCache=p,p.refCount++,p!==null&&(h.pooledCacheLanes|=s),h=p),n.memoizedState={parent:u,cache:h},lh(n),Ya(n,Jt,h)):((t.lanes&s)!==0&&(ch(t,n),ml(n,null,null,s),pl()),h=t.memoizedState,p=n.memoizedState,h.parent!==u?(h={parent:u,cache:u},n.memoizedState=h,n.lanes===0&&(n.memoizedState=n.updateQueue.baseState=h),Ya(n,Jt,u)):(u=p.cache,Ya(n,Jt,u),u!==h.cache&&rh(n,[Jt],s,!0))),yn(t,n,n.pendingProps.children,s),n.child;case 29:throw n.pendingProps}throw Error(a(156,n.tag))}function ba(t){t.flags|=4}function Gh(t,n,s,u,h){if((n=(t.mode&32)!==0)&&(n=!1),n){if(t.flags|=16777216,(h&335544128)===h)if(t.stateNode.complete)t.flags|=8192;else if(Ov())t.flags|=8192;else throw Ui=iu,oh}else t.flags&=-16777217}function iv(t,n){if(n.type!=="stylesheet"||(n.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!SS(n))if(Ov())t.flags|=8192;else throw Ui=iu,oh}function Cu(t,n){n!==null&&(t.flags|=4),t.flags&16384&&(n=t.tag!==22?De():536870912,t.lanes|=n,qs|=n)}function Cl(t,n){if(!ct)switch(t.tailMode){case"hidden":n=t.tail;for(var s=null;n!==null;)n.alternate!==null&&(s=n),n=n.sibling;s===null?t.tail=null:s.sibling=null;break;case"collapsed":s=t.tail;for(var u=null;s!==null;)s.alternate!==null&&(u=s),s=s.sibling;u===null?n||t.tail===null?t.tail=null:t.tail.sibling=null:u.sibling=null}}function Dt(t){var n=t.alternate!==null&&t.alternate.child===t.child,s=0,u=0;if(n)for(var h=t.child;h!==null;)s|=h.lanes|h.childLanes,u|=h.subtreeFlags&65011712,u|=h.flags&65011712,h.return=t,h=h.sibling;else for(h=t.child;h!==null;)s|=h.lanes|h.childLanes,u|=h.subtreeFlags,u|=h.flags,h.return=t,h=h.sibling;return t.subtreeFlags|=u,t.childLanes=s,n}function qA(t,n,s){var u=n.pendingProps;switch(Qf(n),n.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Dt(n),null;case 1:return Dt(n),null;case 3:return s=n.stateNode,u=null,t!==null&&(u=t.memoizedState.cache),n.memoizedState.cache!==u&&(n.flags|=2048),ga(Jt),$e(),s.pendingContext&&(s.context=s.pendingContext,s.pendingContext=null),(t===null||t.child===null)&&(Ds(n)?ba(n):t===null||t.memoizedState.isDehydrated&&(n.flags&256)===0||(n.flags|=1024,eh())),Dt(n),null;case 26:var h=n.type,p=n.memoizedState;return t===null?(ba(n),p!==null?(Dt(n),iv(n,p)):(Dt(n),Gh(n,h,null,u,s))):p?p!==t.memoizedState?(ba(n),Dt(n),iv(n,p)):(Dt(n),n.flags&=-16777217):(t=t.memoizedProps,t!==u&&ba(n),Dt(n),Gh(n,h,t,u,s)),null;case 27:if(At(n),s=ae.current,h=n.type,t!==null&&n.stateNode!=null)t.memoizedProps!==u&&ba(n);else{if(!u){if(n.stateNode===null)throw Error(a(166));return Dt(n),null}t=de.current,Ds(n)?jy(n):(t=fS(h,u,s),n.stateNode=t,ba(n))}return Dt(n),null;case 5:if(At(n),h=n.type,t!==null&&n.stateNode!=null)t.memoizedProps!==u&&ba(n);else{if(!u){if(n.stateNode===null)throw Error(a(166));return Dt(n),null}if(p=de.current,Ds(n))jy(n);else{var C=Hu(ae.current);switch(p){case 1:p=C.createElementNS("http://www.w3.org/2000/svg",h);break;case 2:p=C.createElementNS("http://www.w3.org/1998/Math/MathML",h);break;default:switch(h){case"svg":p=C.createElementNS("http://www.w3.org/2000/svg",h);break;case"math":p=C.createElementNS("http://www.w3.org/1998/Math/MathML",h);break;case"script":p=C.createElement("div"),p.innerHTML="<script><\/script>",p=p.removeChild(p.firstChild);break;case"select":p=typeof u.is=="string"?C.createElement("select",{is:u.is}):C.createElement("select"),u.multiple?p.multiple=!0:u.size&&(p.size=u.size);break;default:p=typeof u.is=="string"?C.createElement(h,{is:u.is}):C.createElement(h)}}p[Nt]=n,p[gn]=u;e:for(C=n.child;C!==null;){if(C.tag===5||C.tag===6)p.appendChild(C.stateNode);else if(C.tag!==4&&C.tag!==27&&C.child!==null){C.child.return=C,C=C.child;continue}if(C===n)break e;for(;C.sibling===null;){if(C.return===null||C.return===n)break e;C=C.return}C.sibling.return=C.return,C=C.sibling}n.stateNode=p;e:switch(bn(p,h,u),h){case"button":case"input":case"select":case"textarea":u=!!u.autoFocus;break e;case"img":u=!0;break e;default:u=!1}u&&ba(n)}}return Dt(n),Gh(n,n.type,t===null?null:t.memoizedProps,n.pendingProps,s),null;case 6:if(t&&n.stateNode!=null)t.memoizedProps!==u&&ba(n);else{if(typeof u!="string"&&n.stateNode===null)throw Error(a(166));if(t=ae.current,Ds(n)){if(t=n.stateNode,s=n.memoizedProps,u=null,h=pn,h!==null)switch(h.tag){case 27:case 5:u=h.memoizedProps}t[Nt]=n,t=!!(t.nodeValue===s||u!==null&&u.suppressHydrationWarning===!0||tS(t.nodeValue,s)),t||Wa(n,!0)}else t=Hu(t).createTextNode(u),t[Nt]=n,n.stateNode=t}return Dt(n),null;case 31:if(s=n.memoizedState,t===null||t.memoizedState!==null){if(u=Ds(n),s!==null){if(t===null){if(!u)throw Error(a(318));if(t=n.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(557));t[Nt]=n}else Ni(),(n.flags&128)===0&&(n.memoizedState=null),n.flags|=4;Dt(n),t=!1}else s=eh(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=s),t=!0;if(!t)return n.flags&256?(Qn(n),n):(Qn(n),null);if((n.flags&128)!==0)throw Error(a(558))}return Dt(n),null;case 13:if(u=n.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(h=Ds(n),u!==null&&u.dehydrated!==null){if(t===null){if(!h)throw Error(a(318));if(h=n.memoizedState,h=h!==null?h.dehydrated:null,!h)throw Error(a(317));h[Nt]=n}else Ni(),(n.flags&128)===0&&(n.memoizedState=null),n.flags|=4;Dt(n),h=!1}else h=eh(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=h),h=!0;if(!h)return n.flags&256?(Qn(n),n):(Qn(n),null)}return Qn(n),(n.flags&128)!==0?(n.lanes=s,n):(s=u!==null,t=t!==null&&t.memoizedState!==null,s&&(u=n.child,h=null,u.alternate!==null&&u.alternate.memoizedState!==null&&u.alternate.memoizedState.cachePool!==null&&(h=u.alternate.memoizedState.cachePool.pool),p=null,u.memoizedState!==null&&u.memoizedState.cachePool!==null&&(p=u.memoizedState.cachePool.pool),p!==h&&(u.flags|=2048)),s!==t&&s&&(n.child.flags|=8192),Cu(n,n.updateQueue),Dt(n),null);case 4:return $e(),t===null&&fg(n.stateNode.containerInfo),Dt(n),null;case 10:return ga(n.type),Dt(n),null;case 19:if(T(Ft),u=n.memoizedState,u===null)return Dt(n),null;if(h=(n.flags&128)!==0,p=u.rendering,p===null)if(h)Cl(u,!1);else{if(qt!==0||t!==null&&(t.flags&128)!==0)for(t=n.child;t!==null;){if(p=cu(t),p!==null){for(n.flags|=128,Cl(u,!1),t=p.updateQueue,n.updateQueue=t,Cu(n,t),n.subtreeFlags=0,t=s,s=n.child;s!==null;)My(s,t),s=s.sibling;return ge(Ft,Ft.current&1|2),ct&&fa(n,u.treeForkCount),n.child}t=t.sibling}u.tail!==null&&Ut()>Tu&&(n.flags|=128,h=!0,Cl(u,!1),n.lanes=4194304)}else{if(!h)if(t=cu(p),t!==null){if(n.flags|=128,h=!0,t=t.updateQueue,n.updateQueue=t,Cu(n,t),Cl(u,!0),u.tail===null&&u.tailMode==="hidden"&&!p.alternate&&!ct)return Dt(n),null}else 2*Ut()-u.renderingStartTime>Tu&&s!==536870912&&(n.flags|=128,h=!0,Cl(u,!1),n.lanes=4194304);u.isBackwards?(p.sibling=n.child,n.child=p):(t=u.last,t!==null?t.sibling=p:n.child=p,u.last=p)}return u.tail!==null?(t=u.tail,u.rendering=t,u.tail=t.sibling,u.renderingStartTime=Ut(),t.sibling=null,s=Ft.current,ge(Ft,h?s&1|2:s&1),ct&&fa(n,u.treeForkCount),t):(Dt(n),null);case 22:case 23:return Qn(n),hh(),u=n.memoizedState!==null,t!==null?t.memoizedState!==null!==u&&(n.flags|=8192):u&&(n.flags|=8192),u?(s&536870912)!==0&&(n.flags&128)===0&&(Dt(n),n.subtreeFlags&6&&(n.flags|=8192)):Dt(n),s=n.updateQueue,s!==null&&Cu(n,s.retryQueue),s=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(s=t.memoizedState.cachePool.pool),u=null,n.memoizedState!==null&&n.memoizedState.cachePool!==null&&(u=n.memoizedState.cachePool.pool),u!==s&&(n.flags|=2048),t!==null&&T(ji),null;case 24:return s=null,t!==null&&(s=t.memoizedState.cache),n.memoizedState.cache!==s&&(n.flags|=2048),ga(Jt),Dt(n),null;case 25:return null;case 30:return null}throw Error(a(156,n.tag))}function GA(t,n){switch(Qf(n),n.tag){case 1:return t=n.flags,t&65536?(n.flags=t&-65537|128,n):null;case 3:return ga(Jt),$e(),t=n.flags,(t&65536)!==0&&(t&128)===0?(n.flags=t&-65537|128,n):null;case 26:case 27:case 5:return At(n),null;case 31:if(n.memoizedState!==null){if(Qn(n),n.alternate===null)throw Error(a(340));Ni()}return t=n.flags,t&65536?(n.flags=t&-65537|128,n):null;case 13:if(Qn(n),t=n.memoizedState,t!==null&&t.dehydrated!==null){if(n.alternate===null)throw Error(a(340));Ni()}return t=n.flags,t&65536?(n.flags=t&-65537|128,n):null;case 19:return T(Ft),null;case 4:return $e(),null;case 10:return ga(n.type),null;case 22:case 23:return Qn(n),hh(),t!==null&&T(ji),t=n.flags,t&65536?(n.flags=t&-65537|128,n):null;case 24:return ga(Jt),null;case 25:return null;default:return null}}function sv(t,n){switch(Qf(n),n.tag){case 3:ga(Jt),$e();break;case 26:case 27:case 5:At(n);break;case 4:$e();break;case 31:n.memoizedState!==null&&Qn(n);break;case 13:Qn(n);break;case 19:T(Ft);break;case 10:ga(n.type);break;case 22:case 23:Qn(n),hh(),t!==null&&T(ji);break;case 24:ga(Jt)}}function El(t,n){try{var s=n.updateQueue,u=s!==null?s.lastEffect:null;if(u!==null){var h=u.next;s=h;do{if((s.tag&t)===t){u=void 0;var p=s.create,C=s.inst;u=p(),C.destroy=u}s=s.next}while(s!==h)}}catch(k){St(n,n.return,k)}}function ti(t,n,s){try{var u=n.updateQueue,h=u!==null?u.lastEffect:null;if(h!==null){var p=h.next;u=p;do{if((u.tag&t)===t){var C=u.inst,k=C.destroy;if(k!==void 0){C.destroy=void 0,h=n;var z=s,Y=k;try{Y()}catch(ne){St(h,z,ne)}}}u=u.next}while(u!==p)}}catch(ne){St(n,n.return,ne)}}function ov(t){var n=t.updateQueue;if(n!==null){var s=t.stateNode;try{Jy(n,s)}catch(u){St(t,t.return,u)}}}function lv(t,n,s){s.props=qi(t.type,t.memoizedProps),s.state=t.memoizedState;try{s.componentWillUnmount()}catch(u){St(t,n,u)}}function _l(t,n){try{var s=t.ref;if(s!==null){switch(t.tag){case 26:case 27:case 5:var u=t.stateNode;break;case 30:u=t.stateNode;break;default:u=t.stateNode}typeof s=="function"?t.refCleanup=s(u):s.current=u}}catch(h){St(t,n,h)}}function Yr(t,n){var s=t.ref,u=t.refCleanup;if(s!==null)if(typeof u=="function")try{u()}catch(h){St(t,n,h)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof s=="function")try{s(null)}catch(h){St(t,n,h)}else s.current=null}function cv(t){var n=t.type,s=t.memoizedProps,u=t.stateNode;try{e:switch(n){case"button":case"input":case"select":case"textarea":s.autoFocus&&u.focus();break e;case"img":s.src?u.src=s.src:s.srcSet&&(u.srcset=s.srcSet)}}catch(h){St(t,t.return,h)}}function Vh(t,n,s){try{var u=t.stateNode;fx(u,t.type,s,n),u[gn]=n}catch(h){St(t,t.return,h)}}function uv(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&li(t.type)||t.tag===4}function Fh(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||uv(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&li(t.type)||t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function Kh(t,n,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,n?(s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s).insertBefore(t,n):(n=s.nodeType===9?s.body:s.nodeName==="HTML"?s.ownerDocument.body:s,n.appendChild(t),s=s._reactRootContainer,s!=null||n.onclick!==null||(n.onclick=or));else if(u!==4&&(u===27&&li(t.type)&&(s=t.stateNode,n=null),t=t.child,t!==null))for(Kh(t,n,s),t=t.sibling;t!==null;)Kh(t,n,s),t=t.sibling}function Eu(t,n,s){var u=t.tag;if(u===5||u===6)t=t.stateNode,n?s.insertBefore(t,n):s.appendChild(t);else if(u!==4&&(u===27&&li(t.type)&&(s=t.stateNode),t=t.child,t!==null))for(Eu(t,n,s),t=t.sibling;t!==null;)Eu(t,n,s),t=t.sibling}function dv(t){var n=t.stateNode,s=t.memoizedProps;try{for(var u=t.type,h=n.attributes;h.length;)n.removeAttributeNode(h[0]);bn(n,u,s),n[Nt]=t,n[gn]=s}catch(p){St(t,t.return,p)}}var va=!1,en=!1,Wh=!1,fv=typeof WeakSet=="function"?WeakSet:Set,un=null;function VA(t,n){if(t=t.containerInfo,pg=Gu,t=_y(t),Bf(t)){if("selectionStart"in t)var s={start:t.selectionStart,end:t.selectionEnd};else e:{s=(s=t.ownerDocument)&&s.defaultView||window;var u=s.getSelection&&s.getSelection();if(u&&u.rangeCount!==0){s=u.anchorNode;var h=u.anchorOffset,p=u.focusNode;u=u.focusOffset;try{s.nodeType,p.nodeType}catch{s=null;break e}var C=0,k=-1,z=-1,Y=0,ne=0,le=t,Q=null;t:for(;;){for(var te;le!==s||h!==0&&le.nodeType!==3||(k=C+h),le!==p||u!==0&&le.nodeType!==3||(z=C+u),le.nodeType===3&&(C+=le.nodeValue.length),(te=le.firstChild)!==null;)Q=le,le=te;for(;;){if(le===t)break t;if(Q===s&&++Y===h&&(k=C),Q===p&&++ne===u&&(z=C),(te=le.nextSibling)!==null)break;le=Q,Q=le.parentNode}le=te}s=k===-1||z===-1?null:{start:k,end:z}}else s=null}s=s||{start:0,end:0}}else s=null;for(mg={focusedElem:t,selectionRange:s},Gu=!1,un=n;un!==null;)if(n=un,t=n.child,(n.subtreeFlags&1028)!==0&&t!==null)t.return=n,un=t;else for(;un!==null;){switch(n=un,p=n.alternate,t=n.flags,n.tag){case 0:if((t&4)!==0&&(t=n.updateQueue,t=t!==null?t.events:null,t!==null))for(s=0;s<t.length;s++)h=t[s],h.ref.impl=h.nextImpl;break;case 11:case 15:break;case 1:if((t&1024)!==0&&p!==null){t=void 0,s=n,h=p.memoizedProps,p=p.memoizedState,u=s.stateNode;try{var Te=qi(s.type,h);t=u.getSnapshotBeforeUpdate(Te,p),u.__reactInternalSnapshotBeforeUpdate=t}catch(He){St(s,s.return,He)}}break;case 3:if((t&1024)!==0){if(t=n.stateNode.containerInfo,s=t.nodeType,s===9)vg(t);else if(s===1)switch(t.nodeName){case"HEAD":case"HTML":case"BODY":vg(t);break;default:t.textContent=""}}break;case 5:case 26:case 27:case 6:case 4:case 17:break;default:if((t&1024)!==0)throw Error(a(163))}if(t=n.sibling,t!==null){t.return=n.return,un=t;break}un=n.return}}function hv(t,n,s){var u=s.flags;switch(s.tag){case 0:case 11:case 15:wa(t,s),u&4&&El(5,s);break;case 1:if(wa(t,s),u&4)if(t=s.stateNode,n===null)try{t.componentDidMount()}catch(C){St(s,s.return,C)}else{var h=qi(s.type,n.memoizedProps);n=n.memoizedState;try{t.componentDidUpdate(h,n,t.__reactInternalSnapshotBeforeUpdate)}catch(C){St(s,s.return,C)}}u&64&&ov(s),u&512&&_l(s,s.return);break;case 3:if(wa(t,s),u&64&&(t=s.updateQueue,t!==null)){if(n=null,s.child!==null)switch(s.child.tag){case 27:case 5:n=s.child.stateNode;break;case 1:n=s.child.stateNode}try{Jy(t,n)}catch(C){St(s,s.return,C)}}break;case 27:n===null&&u&4&&dv(s);case 26:case 5:wa(t,s),n===null&&u&4&&cv(s),u&512&&_l(s,s.return);break;case 12:wa(t,s);break;case 31:wa(t,s),u&4&&mv(t,s);break;case 13:wa(t,s),u&4&&yv(t,s),u&64&&(t=s.memoizedState,t!==null&&(t=t.dehydrated,t!==null&&(s=ex.bind(null,s),Sx(t,s))));break;case 22:if(u=s.memoizedState!==null||va,!u){n=n!==null&&n.memoizedState!==null||en,h=va;var p=en;va=u,(en=n)&&!p?Ca(t,s,(s.subtreeFlags&8772)!==0):wa(t,s),va=h,en=p}break;case 30:break;default:wa(t,s)}}function gv(t){var n=t.alternate;n!==null&&(t.alternate=null,gv(n)),t.child=null,t.deletions=null,t.sibling=null,t.tag===5&&(n=t.stateNode,n!==null&&Io(n)),t.stateNode=null,t.return=null,t.dependencies=null,t.memoizedProps=null,t.memoizedState=null,t.pendingProps=null,t.stateNode=null,t.updateQueue=null}var Lt=null,zn=!1;function Sa(t,n,s){for(s=s.child;s!==null;)pv(t,n,s),s=s.sibling}function pv(t,n,s){if(Vt&&typeof Vt.onCommitFiberUnmount=="function")try{Vt.onCommitFiberUnmount($n,s)}catch{}switch(s.tag){case 26:en||Yr(s,n),Sa(t,n,s),s.memoizedState?s.memoizedState.count--:s.stateNode&&(s=s.stateNode,s.parentNode.removeChild(s));break;case 27:en||Yr(s,n);var u=Lt,h=zn;li(s.type)&&(Lt=s.stateNode,zn=!1),Sa(t,n,s),Ml(s.stateNode),Lt=u,zn=h;break;case 5:en||Yr(s,n);case 6:if(u=Lt,h=zn,Lt=null,Sa(t,n,s),Lt=u,zn=h,Lt!==null)if(zn)try{(Lt.nodeType===9?Lt.body:Lt.nodeName==="HTML"?Lt.ownerDocument.body:Lt).removeChild(s.stateNode)}catch(p){St(s,n,p)}else try{Lt.removeChild(s.stateNode)}catch(p){St(s,n,p)}break;case 18:Lt!==null&&(zn?(t=Lt,oS(t.nodeType===9?t.body:t.nodeName==="HTML"?t.ownerDocument.body:t,s.stateNode),Js(t)):oS(Lt,s.stateNode));break;case 4:u=Lt,h=zn,Lt=s.stateNode.containerInfo,zn=!0,Sa(t,n,s),Lt=u,zn=h;break;case 0:case 11:case 14:case 15:ti(2,s,n),en||ti(4,s,n),Sa(t,n,s);break;case 1:en||(Yr(s,n),u=s.stateNode,typeof u.componentWillUnmount=="function"&&lv(s,n,u)),Sa(t,n,s);break;case 21:Sa(t,n,s);break;case 22:en=(u=en)||s.memoizedState!==null,Sa(t,n,s),en=u;break;default:Sa(t,n,s)}}function mv(t,n){if(n.memoizedState===null&&(t=n.alternate,t!==null&&(t=t.memoizedState,t!==null))){t=t.dehydrated;try{Js(t)}catch(s){St(n,n.return,s)}}}function yv(t,n){if(n.memoizedState===null&&(t=n.alternate,t!==null&&(t=t.memoizedState,t!==null&&(t=t.dehydrated,t!==null))))try{Js(t)}catch(s){St(n,n.return,s)}}function FA(t){switch(t.tag){case 31:case 13:case 19:var n=t.stateNode;return n===null&&(n=t.stateNode=new fv),n;case 22:return t=t.stateNode,n=t._retryCache,n===null&&(n=t._retryCache=new fv),n;default:throw Error(a(435,t.tag))}}function _u(t,n){var s=FA(t);n.forEach(function(u){if(!s.has(u)){s.add(u);var h=tx.bind(null,t,u);u.then(h,h)}})}function Un(t,n){var s=n.deletions;if(s!==null)for(var u=0;u<s.length;u++){var h=s[u],p=t,C=n,k=C;e:for(;k!==null;){switch(k.tag){case 27:if(li(k.type)){Lt=k.stateNode,zn=!1;break e}break;case 5:Lt=k.stateNode,zn=!1;break e;case 3:case 4:Lt=k.stateNode.containerInfo,zn=!0;break e}k=k.return}if(Lt===null)throw Error(a(160));pv(p,C,h),Lt=null,zn=!1,p=h.alternate,p!==null&&(p.return=null),h.return=null}if(n.subtreeFlags&13886)for(n=n.child;n!==null;)bv(n,t),n=n.sibling}var Mr=null;function bv(t,n){var s=t.alternate,u=t.flags;switch(t.tag){case 0:case 11:case 14:case 15:Un(n,t),Bn(t),u&4&&(ti(3,t,t.return),El(3,t),ti(5,t,t.return));break;case 1:Un(n,t),Bn(t),u&512&&(en||s===null||Yr(s,s.return)),u&64&&va&&(t=t.updateQueue,t!==null&&(u=t.callbacks,u!==null&&(s=t.shared.hiddenCallbacks,t.shared.hiddenCallbacks=s===null?u:s.concat(u))));break;case 26:var h=Mr;if(Un(n,t),Bn(t),u&512&&(en||s===null||Yr(s,s.return)),u&4){var p=s!==null?s.memoizedState:null;if(u=t.memoizedState,s===null)if(u===null)if(t.stateNode===null){e:{u=t.type,s=t.memoizedProps,h=h.ownerDocument||h;t:switch(u){case"title":p=h.getElementsByTagName("title")[0],(!p||p[ia]||p[Nt]||p.namespaceURI==="http://www.w3.org/2000/svg"||p.hasAttribute("itemprop"))&&(p=h.createElement(u),h.head.insertBefore(p,h.querySelector("head > title"))),bn(p,u,s),p[Nt]=t,Xt(p),u=p;break e;case"link":var C=bS("link","href",h).get(u+(s.href||""));if(C){for(var k=0;k<C.length;k++)if(p=C[k],p.getAttribute("href")===(s.href==null||s.href===""?null:s.href)&&p.getAttribute("rel")===(s.rel==null?null:s.rel)&&p.getAttribute("title")===(s.title==null?null:s.title)&&p.getAttribute("crossorigin")===(s.crossOrigin==null?null:s.crossOrigin)){C.splice(k,1);break t}}p=h.createElement(u),bn(p,u,s),h.head.appendChild(p);break;case"meta":if(C=bS("meta","content",h).get(u+(s.content||""))){for(k=0;k<C.length;k++)if(p=C[k],p.getAttribute("content")===(s.content==null?null:""+s.content)&&p.getAttribute("name")===(s.name==null?null:s.name)&&p.getAttribute("property")===(s.property==null?null:s.property)&&p.getAttribute("http-equiv")===(s.httpEquiv==null?null:s.httpEquiv)&&p.getAttribute("charset")===(s.charSet==null?null:s.charSet)){C.splice(k,1);break t}}p=h.createElement(u),bn(p,u,s),h.head.appendChild(p);break;default:throw Error(a(468,u))}p[Nt]=t,Xt(p),u=p}t.stateNode=u}else vS(h,t.type,t.stateNode);else t.stateNode=yS(h,u,t.memoizedProps);else p!==u?(p===null?s.stateNode!==null&&(s=s.stateNode,s.parentNode.removeChild(s)):p.count--,u===null?vS(h,t.type,t.stateNode):yS(h,u,t.memoizedProps)):u===null&&t.stateNode!==null&&Vh(t,t.memoizedProps,s.memoizedProps)}break;case 27:Un(n,t),Bn(t),u&512&&(en||s===null||Yr(s,s.return)),s!==null&&u&4&&Vh(t,t.memoizedProps,s.memoizedProps);break;case 5:if(Un(n,t),Bn(t),u&512&&(en||s===null||Yr(s,s.return)),t.flags&32){h=t.stateNode;try{Ua(h,"")}catch(Te){St(t,t.return,Te)}}u&4&&t.stateNode!=null&&(h=t.memoizedProps,Vh(t,h,s!==null?s.memoizedProps:h)),u&1024&&(Wh=!0);break;case 6:if(Un(n,t),Bn(t),u&4){if(t.stateNode===null)throw Error(a(162));u=t.memoizedProps,s=t.stateNode;try{s.nodeValue=u}catch(Te){St(t,t.return,Te)}}break;case 3:if(Uu=null,h=Mr,Mr=ju(n.containerInfo),Un(n,t),Mr=h,Bn(t),u&4&&s!==null&&s.memoizedState.isDehydrated)try{Js(n.containerInfo)}catch(Te){St(t,t.return,Te)}Wh&&(Wh=!1,vv(t));break;case 4:u=Mr,Mr=ju(t.stateNode.containerInfo),Un(n,t),Bn(t),Mr=u;break;case 12:Un(n,t),Bn(t);break;case 31:Un(n,t),Bn(t),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,_u(t,u)));break;case 13:Un(n,t),Bn(t),t.child.flags&8192&&t.memoizedState!==null!=(s!==null&&s.memoizedState!==null)&&(xu=Ut()),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,_u(t,u)));break;case 22:h=t.memoizedState!==null;var z=s!==null&&s.memoizedState!==null,Y=va,ne=en;if(va=Y||h,en=ne||z,Un(n,t),en=ne,va=Y,Bn(t),u&8192)e:for(n=t.stateNode,n._visibility=h?n._visibility&-2:n._visibility|1,h&&(s===null||z||va||en||Gi(t)),s=null,n=t;;){if(n.tag===5||n.tag===26){if(s===null){z=s=n;try{if(p=z.stateNode,h)C=p.style,typeof C.setProperty=="function"?C.setProperty("display","none","important"):C.display="none";else{k=z.stateNode;var le=z.memoizedProps.style,Q=le!=null&&le.hasOwnProperty("display")?le.display:null;k.style.display=Q==null||typeof Q=="boolean"?"":(""+Q).trim()}}catch(Te){St(z,z.return,Te)}}}else if(n.tag===6){if(s===null){z=n;try{z.stateNode.nodeValue=h?"":z.memoizedProps}catch(Te){St(z,z.return,Te)}}}else if(n.tag===18){if(s===null){z=n;try{var te=z.stateNode;h?lS(te,!0):lS(z.stateNode,!1)}catch(Te){St(z,z.return,Te)}}}else if((n.tag!==22&&n.tag!==23||n.memoizedState===null||n===t)&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break e;for(;n.sibling===null;){if(n.return===null||n.return===t)break e;s===n&&(s=null),n=n.return}s===n&&(s=null),n.sibling.return=n.return,n=n.sibling}u&4&&(u=t.updateQueue,u!==null&&(s=u.retryQueue,s!==null&&(u.retryQueue=null,_u(t,s))));break;case 19:Un(n,t),Bn(t),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,_u(t,u)));break;case 30:break;case 21:break;default:Un(n,t),Bn(t)}}function Bn(t){var n=t.flags;if(n&2){try{for(var s,u=t.return;u!==null;){if(uv(u)){s=u;break}u=u.return}if(s==null)throw Error(a(160));switch(s.tag){case 27:var h=s.stateNode,p=Fh(t);Eu(t,p,h);break;case 5:var C=s.stateNode;s.flags&32&&(Ua(C,""),s.flags&=-33);var k=Fh(t);Eu(t,k,C);break;case 3:case 4:var z=s.stateNode.containerInfo,Y=Fh(t);Kh(t,Y,z);break;default:throw Error(a(161))}}catch(ne){St(t,t.return,ne)}t.flags&=-3}n&4096&&(t.flags&=-4097)}function vv(t){if(t.subtreeFlags&1024)for(t=t.child;t!==null;){var n=t;vv(n),n.tag===5&&n.flags&1024&&n.stateNode.reset(),t=t.sibling}}function wa(t,n){if(n.subtreeFlags&8772)for(n=n.child;n!==null;)hv(t,n.alternate,n),n=n.sibling}function Gi(t){for(t=t.child;t!==null;){var n=t;switch(n.tag){case 0:case 11:case 14:case 15:ti(4,n,n.return),Gi(n);break;case 1:Yr(n,n.return);var s=n.stateNode;typeof s.componentWillUnmount=="function"&&lv(n,n.return,s),Gi(n);break;case 27:Ml(n.stateNode);case 26:case 5:Yr(n,n.return),Gi(n);break;case 22:n.memoizedState===null&&Gi(n);break;case 30:Gi(n);break;default:Gi(n)}t=t.sibling}}function Ca(t,n,s){for(s=s&&(n.subtreeFlags&8772)!==0,n=n.child;n!==null;){var u=n.alternate,h=t,p=n,C=p.flags;switch(p.tag){case 0:case 11:case 15:Ca(h,p,s),El(4,p);break;case 1:if(Ca(h,p,s),u=p,h=u.stateNode,typeof h.componentDidMount=="function")try{h.componentDidMount()}catch(Y){St(u,u.return,Y)}if(u=p,h=u.updateQueue,h!==null){var k=u.stateNode;try{var z=h.shared.hiddenCallbacks;if(z!==null)for(h.shared.hiddenCallbacks=null,h=0;h<z.length;h++)Xy(z[h],k)}catch(Y){St(u,u.return,Y)}}s&&C&64&&ov(p),_l(p,p.return);break;case 27:dv(p);case 26:case 5:Ca(h,p,s),s&&u===null&&C&4&&cv(p),_l(p,p.return);break;case 12:Ca(h,p,s);break;case 31:Ca(h,p,s),s&&C&4&&mv(h,p);break;case 13:Ca(h,p,s),s&&C&4&&yv(h,p);break;case 22:p.memoizedState===null&&Ca(h,p,s),_l(p,p.return);break;case 30:break;default:Ca(h,p,s)}n=n.sibling}}function Yh(t,n){var s=null;t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(s=t.memoizedState.cachePool.pool),t=null,n.memoizedState!==null&&n.memoizedState.cachePool!==null&&(t=n.memoizedState.cachePool.pool),t!==s&&(t!=null&&t.refCount++,s!=null&&ul(s))}function Xh(t,n){t=null,n.alternate!==null&&(t=n.alternate.memoizedState.cache),n=n.memoizedState.cache,n!==t&&(n.refCount++,t!=null&&ul(t))}function $r(t,n,s,u){if(n.subtreeFlags&10256)for(n=n.child;n!==null;)Sv(t,n,s,u),n=n.sibling}function Sv(t,n,s,u){var h=n.flags;switch(n.tag){case 0:case 11:case 15:$r(t,n,s,u),h&2048&&El(9,n);break;case 1:$r(t,n,s,u);break;case 3:$r(t,n,s,u),h&2048&&(t=null,n.alternate!==null&&(t=n.alternate.memoizedState.cache),n=n.memoizedState.cache,n!==t&&(n.refCount++,t!=null&&ul(t)));break;case 12:if(h&2048){$r(t,n,s,u),t=n.stateNode;try{var p=n.memoizedProps,C=p.id,k=p.onPostCommit;typeof k=="function"&&k(C,n.alternate===null?"mount":"update",t.passiveEffectDuration,-0)}catch(z){St(n,n.return,z)}}else $r(t,n,s,u);break;case 31:$r(t,n,s,u);break;case 13:$r(t,n,s,u);break;case 23:break;case 22:p=n.stateNode,C=n.alternate,n.memoizedState!==null?p._visibility&2?$r(t,n,s,u):Al(t,n):p._visibility&2?$r(t,n,s,u):(p._visibility|=2,Us(t,n,s,u,(n.subtreeFlags&10256)!==0||!1)),h&2048&&Yh(C,n);break;case 24:$r(t,n,s,u),h&2048&&Xh(n.alternate,n);break;default:$r(t,n,s,u)}}function Us(t,n,s,u,h){for(h=h&&((n.subtreeFlags&10256)!==0||!1),n=n.child;n!==null;){var p=t,C=n,k=s,z=u,Y=C.flags;switch(C.tag){case 0:case 11:case 15:Us(p,C,k,z,h),El(8,C);break;case 23:break;case 22:var ne=C.stateNode;C.memoizedState!==null?ne._visibility&2?Us(p,C,k,z,h):Al(p,C):(ne._visibility|=2,Us(p,C,k,z,h)),h&&Y&2048&&Yh(C.alternate,C);break;case 24:Us(p,C,k,z,h),h&&Y&2048&&Xh(C.alternate,C);break;default:Us(p,C,k,z,h)}n=n.sibling}}function Al(t,n){if(n.subtreeFlags&10256)for(n=n.child;n!==null;){var s=t,u=n,h=u.flags;switch(u.tag){case 22:Al(s,u),h&2048&&Yh(u.alternate,u);break;case 24:Al(s,u),h&2048&&Xh(u.alternate,u);break;default:Al(s,u)}n=n.sibling}}var xl=8192;function Bs(t,n,s){if(t.subtreeFlags&xl)for(t=t.child;t!==null;)wv(t,n,s),t=t.sibling}function wv(t,n,s){switch(t.tag){case 26:Bs(t,n,s),t.flags&xl&&t.memoizedState!==null&&Lx(s,Mr,t.memoizedState,t.memoizedProps);break;case 5:Bs(t,n,s);break;case 3:case 4:var u=Mr;Mr=ju(t.stateNode.containerInfo),Bs(t,n,s),Mr=u;break;case 22:t.memoizedState===null&&(u=t.alternate,u!==null&&u.memoizedState!==null?(u=xl,xl=16777216,Bs(t,n,s),xl=u):Bs(t,n,s));break;default:Bs(t,n,s)}}function Cv(t){var n=t.alternate;if(n!==null&&(t=n.child,t!==null)){n.child=null;do n=t.sibling,t.sibling=null,t=n;while(t!==null)}}function Tl(t){var n=t.deletions;if((t.flags&16)!==0){if(n!==null)for(var s=0;s<n.length;s++){var u=n[s];un=u,_v(u,t)}Cv(t)}if(t.subtreeFlags&10256)for(t=t.child;t!==null;)Ev(t),t=t.sibling}function Ev(t){switch(t.tag){case 0:case 11:case 15:Tl(t),t.flags&2048&&ti(9,t,t.return);break;case 3:Tl(t);break;case 12:Tl(t);break;case 22:var n=t.stateNode;t.memoizedState!==null&&n._visibility&2&&(t.return===null||t.return.tag!==13)?(n._visibility&=-3,Au(t)):Tl(t);break;default:Tl(t)}}function Au(t){var n=t.deletions;if((t.flags&16)!==0){if(n!==null)for(var s=0;s<n.length;s++){var u=n[s];un=u,_v(u,t)}Cv(t)}for(t=t.child;t!==null;){switch(n=t,n.tag){case 0:case 11:case 15:ti(8,n,n.return),Au(n);break;case 22:s=n.stateNode,s._visibility&2&&(s._visibility&=-3,Au(n));break;default:Au(n)}t=t.sibling}}function _v(t,n){for(;un!==null;){var s=un;switch(s.tag){case 0:case 11:case 15:ti(8,s,n);break;case 23:case 22:if(s.memoizedState!==null&&s.memoizedState.cachePool!==null){var u=s.memoizedState.cachePool.pool;u!=null&&u.refCount++}break;case 24:ul(s.memoizedState.cache)}if(u=s.child,u!==null)u.return=s,un=u;else e:for(s=t;un!==null;){u=un;var h=u.sibling,p=u.return;if(gv(u),u===s){un=null;break e}if(h!==null){h.return=p,un=h;break e}un=p}}}var KA={getCacheForType:function(t){var n=mn(Jt),s=n.data.get(t);return s===void 0&&(s=t(),n.data.set(t,s)),s},cacheSignal:function(){return mn(Jt).controller.signal}},WA=typeof WeakMap=="function"?WeakMap:Map,pt=0,xt=null,tt=null,it=0,vt=0,Zn=null,ni=!1,Ps=!1,Jh=!1,Ea=0,qt=0,ri=0,Vi=0,Qh=0,er=0,qs=0,kl=null,Pn=null,Zh=!1,xu=0,Av=0,Tu=1/0,ku=null,ai=null,an=0,ii=null,Gs=null,_a=0,eg=0,tg=null,xv=null,Rl=0,ng=null;function tr(){return(pt&2)!==0&&it!==0?it&-it:J.T!==null?lg():Ot()}function Tv(){if(er===0)if((it&536870912)===0||ct){var t=ve;ve<<=1,(ve&3932160)===0&&(ve=262144),er=t}else er=536870912;return t=Jn.current,t!==null&&(t.flags|=32),er}function qn(t,n,s){(t===xt&&(vt===2||vt===9)||t.cancelPendingCommit!==null)&&(Vs(t,0),si(t,it,er,!1)),et(t,s),((pt&2)===0||t!==xt)&&(t===xt&&((pt&2)===0&&(Vi|=s),qt===4&&si(t,it,er,!1)),Xr(t))}function kv(t,n,s){if((pt&6)!==0)throw Error(a(327));var u=!s&&(n&127)===0&&(n&t.expiredLanes)===0||Be(t,n),h=u?JA(t,n):ag(t,n,!0),p=u;do{if(h===0){Ps&&!u&&si(t,n,0,!1);break}else{if(s=t.current.alternate,p&&!YA(s)){h=ag(t,n,!1),p=!1;continue}if(h===2){if(p=n,t.errorRecoveryDisabledLanes&p)var C=0;else C=t.pendingLanes&-536870913,C=C!==0?C:C&536870912?536870912:0;if(C!==0){n=C;e:{var k=t;h=kl;var z=k.current.memoizedState.isDehydrated;if(z&&(Vs(k,C).flags|=256),C=ag(k,C,!1),C!==2){if(Jh&&!z){k.errorRecoveryDisabledLanes|=p,Vi|=p,h=4;break e}p=Pn,Pn=h,p!==null&&(Pn===null?Pn=p:Pn.push.apply(Pn,p))}h=C}if(p=!1,h!==2)continue}}if(h===1){Vs(t,0),si(t,n,0,!0);break}e:{switch(u=t,p=h,p){case 0:case 1:throw Error(a(345));case 4:if((n&4194048)!==n)break;case 6:si(u,n,er,!ni);break e;case 2:Pn=null;break;case 3:case 5:break;default:throw Error(a(329))}if((n&62914560)===n&&(h=xu+300-Ut(),10<h)){if(si(u,n,er,!ni),_e(u,0,!0)!==0)break e;_a=n,u.timeoutHandle=iS(Rv.bind(null,u,s,Pn,ku,Zh,n,er,Vi,qs,ni,p,"Throttled",-0,0),h);break e}Rv(u,s,Pn,ku,Zh,n,er,Vi,qs,ni,p,null,-0,0)}}break}while(!0);Xr(t)}function Rv(t,n,s,u,h,p,C,k,z,Y,ne,le,Q,te){if(t.timeoutHandle=-1,le=n.subtreeFlags,le&8192||(le&16785408)===16785408){le={stylesheets:null,count:0,imgCount:0,imgBytes:0,suspenseyImages:[],waitingForImages:!0,waitingForViewTransition:!1,unsuspend:or},wv(n,p,le);var Te=(p&62914560)===p?xu-Ut():(p&4194048)===p?Av-Ut():0;if(Te=Mx(le,Te),Te!==null){_a=p,t.cancelPendingCommit=Te(Hv.bind(null,t,n,p,s,u,h,C,k,z,ne,le,null,Q,te)),si(t,p,C,!Y);return}}Hv(t,n,p,s,u,h,C,k,z)}function YA(t){for(var n=t;;){var s=n.tag;if((s===0||s===11||s===15)&&n.flags&16384&&(s=n.updateQueue,s!==null&&(s=s.stores,s!==null)))for(var u=0;u<s.length;u++){var h=s[u],p=h.getSnapshot;h=h.value;try{if(!Yn(p(),h))return!1}catch{return!1}}if(s=n.child,n.subtreeFlags&16384&&s!==null)s.return=n,n=s;else{if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return!0;n=n.return}n.sibling.return=n.return,n=n.sibling}}return!0}function si(t,n,s,u){n&=~Qh,n&=~Vi,t.suspendedLanes|=n,t.pingedLanes&=~n,u&&(t.warmLanes|=n),u=t.expirationTimes;for(var h=n;0<h;){var p=31-nn(h),C=1<<p;u[p]=-1,h&=~C}s!==0&&je(t,s,n)}function Ru(){return(pt&6)===0?(Dl(0),!1):!0}function rg(){if(tt!==null){if(vt===0)var t=tt.return;else t=tt,ha=Ii=null,vh(t),Ns=null,fl=0,t=tt;for(;t!==null;)sv(t.alternate,t),t=t.return;tt=null}}function Vs(t,n){var s=t.timeoutHandle;s!==-1&&(t.timeoutHandle=-1,px(s)),s=t.cancelPendingCommit,s!==null&&(t.cancelPendingCommit=null,s()),_a=0,rg(),xt=t,tt=s=da(t.current,null),it=n,vt=0,Zn=null,ni=!1,Ps=Be(t,n),Jh=!1,qs=er=Qh=Vi=ri=qt=0,Pn=kl=null,Zh=!1,(n&8)!==0&&(n|=n&32);var u=t.entangledLanes;if(u!==0)for(t=t.entanglements,u&=n;0<u;){var h=31-nn(u),p=1<<h;n|=t[h],u&=~p}return Ea=n,Xc(),s}function Dv(t,n){Xe=null,J.H=Sl,n===$s||n===au?(n=Fy(),vt=3):n===oh?(n=Fy(),vt=4):vt=n===Nh?8:n!==null&&typeof n=="object"&&typeof n.then=="function"?6:1,Zn=n,tt===null&&(qt=1,bu(t,cr(n,t.current)))}function Ov(){var t=Jn.current;return t===null?!0:(it&4194048)===it?hr===null:(it&62914560)===it||(it&536870912)!==0?t===hr:!1}function Lv(){var t=J.H;return J.H=Sl,t===null?Sl:t}function Mv(){var t=J.A;return J.A=KA,t}function Du(){qt=4,ni||(it&4194048)!==it&&Jn.current!==null||(Ps=!0),(ri&134217727)===0&&(Vi&134217727)===0||xt===null||si(xt,it,er,!1)}function ag(t,n,s){var u=pt;pt|=2;var h=Lv(),p=Mv();(xt!==t||it!==n)&&(ku=null,Vs(t,n)),n=!1;var C=qt;e:do try{if(vt!==0&&tt!==null){var k=tt,z=Zn;switch(vt){case 8:rg(),C=6;break e;case 3:case 2:case 9:case 6:Jn.current===null&&(n=!0);var Y=vt;if(vt=0,Zn=null,Fs(t,k,z,Y),s&&Ps){C=0;break e}break;default:Y=vt,vt=0,Zn=null,Fs(t,k,z,Y)}}XA(),C=qt;break}catch(ne){Dv(t,ne)}while(!0);return n&&t.shellSuspendCounter++,ha=Ii=null,pt=u,J.H=h,J.A=p,tt===null&&(xt=null,it=0,Xc()),C}function XA(){for(;tt!==null;)$v(tt)}function JA(t,n){var s=pt;pt|=2;var u=Lv(),h=Mv();xt!==t||it!==n?(ku=null,Tu=Ut()+500,Vs(t,n)):Ps=Be(t,n);e:do try{if(vt!==0&&tt!==null){n=tt;var p=Zn;t:switch(vt){case 1:vt=0,Zn=null,Fs(t,n,p,1);break;case 2:case 9:if(Gy(p)){vt=0,Zn=null,Nv(n);break}n=function(){vt!==2&&vt!==9||xt!==t||(vt=7),Xr(t)},p.then(n,n);break e;case 3:vt=7;break e;case 4:vt=5;break e;case 7:Gy(p)?(vt=0,Zn=null,Nv(n)):(vt=0,Zn=null,Fs(t,n,p,7));break;case 5:var C=null;switch(tt.tag){case 26:C=tt.memoizedState;case 5:case 27:var k=tt;if(C?SS(C):k.stateNode.complete){vt=0,Zn=null;var z=k.sibling;if(z!==null)tt=z;else{var Y=k.return;Y!==null?(tt=Y,Ou(Y)):tt=null}break t}}vt=0,Zn=null,Fs(t,n,p,5);break;case 6:vt=0,Zn=null,Fs(t,n,p,6);break;case 8:rg(),qt=6;break e;default:throw Error(a(462))}}QA();break}catch(ne){Dv(t,ne)}while(!0);return ha=Ii=null,J.H=u,J.A=h,pt=s,tt!==null?0:(xt=null,it=0,Xc(),qt)}function QA(){for(;tt!==null&&!sr();)$v(tt)}function $v(t){var n=av(t.alternate,t,Ea);t.memoizedProps=t.pendingProps,n===null?Ou(t):tt=n}function Nv(t){var n=t,s=n.alternate;switch(n.tag){case 15:case 0:n=Qb(s,n,n.pendingProps,n.type,void 0,it);break;case 11:n=Qb(s,n,n.pendingProps,n.type.render,n.ref,it);break;case 5:vh(n);default:sv(s,n),n=tt=My(n,Ea),n=av(s,n,Ea)}t.memoizedProps=t.pendingProps,n===null?Ou(t):tt=n}function Fs(t,n,s,u){ha=Ii=null,vh(n),Ns=null,fl=0;var h=n.return;try{if(UA(t,h,n,s,it)){qt=1,bu(t,cr(s,t.current)),tt=null;return}}catch(p){if(h!==null)throw tt=h,p;qt=1,bu(t,cr(s,t.current)),tt=null;return}n.flags&32768?(ct||u===1?t=!0:Ps||(it&536870912)!==0?t=!1:(ni=t=!0,(u===2||u===9||u===3||u===6)&&(u=Jn.current,u!==null&&u.tag===13&&(u.flags|=16384))),Iv(n,t)):Ou(n)}function Ou(t){var n=t;do{if((n.flags&32768)!==0){Iv(n,ni);return}t=n.return;var s=qA(n.alternate,n,Ea);if(s!==null){tt=s;return}if(n=n.sibling,n!==null){tt=n;return}tt=n=t}while(n!==null);qt===0&&(qt=5)}function Iv(t,n){do{var s=GA(t.alternate,t);if(s!==null){s.flags&=32767,tt=s;return}if(s=t.return,s!==null&&(s.flags|=32768,s.subtreeFlags=0,s.deletions=null),!n&&(t=t.sibling,t!==null)){tt=t;return}tt=t=s}while(t!==null);qt=6,tt=null}function Hv(t,n,s,u,h,p,C,k,z){t.cancelPendingCommit=null;do Lu();while(an!==0);if((pt&6)!==0)throw Error(a(327));if(n!==null){if(n===t.current)throw Error(a(177));if(p=n.lanes|n.childLanes,p|=Ff,Ne(t,s,p,C,k,z),t===xt&&(tt=xt=null,it=0),Gs=n,ii=t,_a=s,eg=p,tg=h,xv=u,(n.subtreeFlags&10256)!==0||(n.flags&10256)!==0?(t.callbackNode=null,t.callbackPriority=0,nx(An,function(){return Pv(),null})):(t.callbackNode=null,t.callbackPriority=0),u=(n.flags&13878)!==0,(n.subtreeFlags&13878)!==0||u){u=J.T,J.T=null,h=he.p,he.p=2,C=pt,pt|=4;try{VA(t,n,s)}finally{pt=C,he.p=h,J.T=u}}an=1,jv(),zv(),Uv()}}function jv(){if(an===1){an=0;var t=ii,n=Gs,s=(n.flags&13878)!==0;if((n.subtreeFlags&13878)!==0||s){s=J.T,J.T=null;var u=he.p;he.p=2;var h=pt;pt|=4;try{bv(n,t);var p=mg,C=_y(t.containerInfo),k=p.focusedElem,z=p.selectionRange;if(C!==k&&k&&k.ownerDocument&&Ey(k.ownerDocument.documentElement,k)){if(z!==null&&Bf(k)){var Y=z.start,ne=z.end;if(ne===void 0&&(ne=Y),"selectionStart"in k)k.selectionStart=Y,k.selectionEnd=Math.min(ne,k.value.length);else{var le=k.ownerDocument||document,Q=le&&le.defaultView||window;if(Q.getSelection){var te=Q.getSelection(),Te=k.textContent.length,He=Math.min(z.start,Te),_t=z.end===void 0?He:Math.min(z.end,Te);!te.extend&&He>_t&&(C=_t,_t=He,He=C);var F=Cy(k,He),P=Cy(k,_t);if(F&&P&&(te.rangeCount!==1||te.anchorNode!==F.node||te.anchorOffset!==F.offset||te.focusNode!==P.node||te.focusOffset!==P.offset)){var W=le.createRange();W.setStart(F.node,F.offset),te.removeAllRanges(),He>_t?(te.addRange(W),te.extend(P.node,P.offset)):(W.setEnd(P.node,P.offset),te.addRange(W))}}}}for(le=[],te=k;te=te.parentNode;)te.nodeType===1&&le.push({element:te,left:te.scrollLeft,top:te.scrollTop});for(typeof k.focus=="function"&&k.focus(),k=0;k<le.length;k++){var se=le[k];se.element.scrollLeft=se.left,se.element.scrollTop=se.top}}Gu=!!pg,mg=pg=null}finally{pt=h,he.p=u,J.T=s}}t.current=n,an=2}}function zv(){if(an===2){an=0;var t=ii,n=Gs,s=(n.flags&8772)!==0;if((n.subtreeFlags&8772)!==0||s){s=J.T,J.T=null;var u=he.p;he.p=2;var h=pt;pt|=4;try{hv(t,n.alternate,n)}finally{pt=h,he.p=u,J.T=s}}an=3}}function Uv(){if(an===4||an===3){an=0,Wn();var t=ii,n=Gs,s=_a,u=xv;(n.subtreeFlags&10256)!==0||(n.flags&10256)!==0?an=5:(an=0,Gs=ii=null,Bv(t,t.pendingLanes));var h=t.pendingLanes;if(h===0&&(ai=null),kt(s),n=n.stateNode,Vt&&typeof Vt.onCommitFiberRoot=="function")try{Vt.onCommitFiberRoot($n,n,void 0,(n.current.flags&128)===128)}catch{}if(u!==null){n=J.T,h=he.p,he.p=2,J.T=null;try{for(var p=t.onRecoverableError,C=0;C<u.length;C++){var k=u[C];p(k.value,{componentStack:k.stack})}}finally{J.T=n,he.p=h}}(_a&3)!==0&&Lu(),Xr(t),h=t.pendingLanes,(s&261930)!==0&&(h&42)!==0?t===ng?Rl++:(Rl=0,ng=t):Rl=0,Dl(0)}}function Bv(t,n){(t.pooledCacheLanes&=n)===0&&(n=t.pooledCache,n!=null&&(t.pooledCache=null,ul(n)))}function Lu(){return jv(),zv(),Uv(),Pv()}function Pv(){if(an!==5)return!1;var t=ii,n=eg;eg=0;var s=kt(_a),u=J.T,h=he.p;try{he.p=32>s?32:s,J.T=null,s=tg,tg=null;var p=ii,C=_a;if(an=0,Gs=ii=null,_a=0,(pt&6)!==0)throw Error(a(331));var k=pt;if(pt|=4,Ev(p.current),Sv(p,p.current,C,s),pt=k,Dl(0,!1),Vt&&typeof Vt.onPostCommitFiberRoot=="function")try{Vt.onPostCommitFiberRoot($n,p)}catch{}return!0}finally{he.p=h,J.T=u,Bv(t,n)}}function qv(t,n,s){n=cr(s,n),n=$h(t.stateNode,n,2),t=Qa(t,n,2),t!==null&&(et(t,2),Xr(t))}function St(t,n,s){if(t.tag===3)qv(t,t,s);else for(;n!==null;){if(n.tag===3){qv(n,t,s);break}else if(n.tag===1){var u=n.stateNode;if(typeof n.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(ai===null||!ai.has(u))){t=cr(s,t),s=Gb(2),u=Qa(n,s,2),u!==null&&(Vb(s,u,n,t),et(u,2),Xr(u));break}}n=n.return}}function ig(t,n,s){var u=t.pingCache;if(u===null){u=t.pingCache=new WA;var h=new Set;u.set(n,h)}else h=u.get(n),h===void 0&&(h=new Set,u.set(n,h));h.has(s)||(Jh=!0,h.add(s),t=ZA.bind(null,t,n,s),n.then(t,t))}function ZA(t,n,s){var u=t.pingCache;u!==null&&u.delete(n),t.pingedLanes|=t.suspendedLanes&s,t.warmLanes&=~s,xt===t&&(it&s)===s&&(qt===4||qt===3&&(it&62914560)===it&&300>Ut()-xu?(pt&2)===0&&Vs(t,0):Qh|=s,qs===it&&(qs=0)),Xr(t)}function Gv(t,n){n===0&&(n=De()),t=Mi(t,n),t!==null&&(et(t,n),Xr(t))}function ex(t){var n=t.memoizedState,s=0;n!==null&&(s=n.retryLane),Gv(t,s)}function tx(t,n){var s=0;switch(t.tag){case 31:case 13:var u=t.stateNode,h=t.memoizedState;h!==null&&(s=h.retryLane);break;case 19:u=t.stateNode;break;case 22:u=t.stateNode._retryCache;break;default:throw Error(a(314))}u!==null&&u.delete(n),Gv(t,s)}function nx(t,n){return cn(t,n)}var Mu=null,Ks=null,sg=!1,$u=!1,og=!1,oi=0;function Xr(t){t!==Ks&&t.next===null&&(Ks===null?Mu=Ks=t:Ks=Ks.next=t),$u=!0,sg||(sg=!0,ax())}function Dl(t,n){if(!og&&$u){og=!0;do for(var s=!1,u=Mu;u!==null;){if(t!==0){var h=u.pendingLanes;if(h===0)var p=0;else{var C=u.suspendedLanes,k=u.pingedLanes;p=(1<<31-nn(42|t)+1)-1,p&=h&~(C&~k),p=p&201326741?p&201326741|1:p?p|2:0}p!==0&&(s=!0,Wv(u,p))}else p=it,p=_e(u,u===xt?p:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(p&3)===0||Be(u,p)||(s=!0,Wv(u,p));u=u.next}while(s);og=!1}}function rx(){Vv()}function Vv(){$u=sg=!1;var t=0;oi!==0&&gx()&&(t=oi);for(var n=Ut(),s=null,u=Mu;u!==null;){var h=u.next,p=Fv(u,n);p===0?(u.next=null,s===null?Mu=h:s.next=h,h===null&&(Ks=s)):(s=u,(t!==0||(p&3)!==0)&&($u=!0)),u=h}an!==0&&an!==5||Dl(t),oi!==0&&(oi=0)}function Fv(t,n){for(var s=t.suspendedLanes,u=t.pingedLanes,h=t.expirationTimes,p=t.pendingLanes&-62914561;0<p;){var C=31-nn(p),k=1<<C,z=h[C];z===-1?((k&s)===0||(k&u)!==0)&&(h[C]=xe(k,n)):z<=n&&(t.expiredLanes|=k),p&=~k}if(n=xt,s=it,s=_e(t,t===n?s:0,t.cancelPendingCommit!==null||t.timeoutHandle!==-1),u=t.callbackNode,s===0||t===n&&(vt===2||vt===9)||t.cancelPendingCommit!==null)return u!==null&&u!==null&&fn(u),t.callbackNode=null,t.callbackPriority=0;if((s&3)===0||Be(t,s)){if(n=s&-s,n===t.callbackPriority)return n;switch(u!==null&&fn(u),kt(s)){case 2:case 8:s=Ln;break;case 32:s=An;break;case 268435456:s=Mn;break;default:s=An}return u=Kv.bind(null,t),s=cn(s,u),t.callbackPriority=n,t.callbackNode=s,n}return u!==null&&u!==null&&fn(u),t.callbackPriority=2,t.callbackNode=null,2}function Kv(t,n){if(an!==0&&an!==5)return t.callbackNode=null,t.callbackPriority=0,null;var s=t.callbackNode;if(Lu()&&t.callbackNode!==s)return null;var u=it;return u=_e(t,t===xt?u:0,t.cancelPendingCommit!==null||t.timeoutHandle!==-1),u===0?null:(kv(t,u,n),Fv(t,Ut()),t.callbackNode!=null&&t.callbackNode===s?Kv.bind(null,t):null)}function Wv(t,n){if(Lu())return null;kv(t,n,!0)}function ax(){mx(function(){(pt&6)!==0?cn(ra,rx):Vv()})}function lg(){if(oi===0){var t=Ls;t===0&&(t=X,X<<=1,(X&261888)===0&&(X=256)),oi=t}return oi}function Yv(t){return t==null||typeof t=="symbol"||typeof t=="boolean"?null:typeof t=="function"?t:bs(""+t)}function Xv(t,n){var s=n.ownerDocument.createElement("input");return s.name=n.name,s.value=n.value,t.id&&s.setAttribute("form",t.id),n.parentNode.insertBefore(s,n),t=new FormData(t),s.parentNode.removeChild(s),t}function ix(t,n,s,u,h){if(n==="submit"&&s&&s.stateNode===h){var p=Yv((h[gn]||null).action),C=u.submitter;C&&(n=(n=C[gn]||null)?Yv(n.formAction):C.getAttribute("formAction"),n!==null&&(p=n,C=null));var k=new ws("action","action",null,u,h);t.push({event:k,listeners:[{instance:null,listener:function(){if(u.defaultPrevented){if(oi!==0){var z=C?Xv(h,C):new FormData(h);kh(s,{pending:!0,data:z,method:h.method,action:p},null,z)}}else typeof p=="function"&&(k.preventDefault(),z=C?Xv(h,C):new FormData(h),kh(s,{pending:!0,data:z,method:h.method,action:p},p,z))},currentTarget:h}]})}}for(var cg=0;cg<Vf.length;cg++){var ug=Vf[cg],sx=ug.toLowerCase(),ox=ug[0].toUpperCase()+ug.slice(1);Lr(sx,"on"+ox)}Lr(Ty,"onAnimationEnd"),Lr(ky,"onAnimationIteration"),Lr(Ry,"onAnimationStart"),Lr("dblclick","onDoubleClick"),Lr("focusin","onFocus"),Lr("focusout","onBlur"),Lr(EA,"onTransitionRun"),Lr(_A,"onTransitionStart"),Lr(AA,"onTransitionCancel"),Lr(Dy,"onTransitionEnd"),ja("onMouseEnter",["mouseout","mouseover"]),ja("onMouseLeave",["mouseout","mouseover"]),ja("onPointerEnter",["pointerout","pointerover"]),ja("onPointerLeave",["pointerout","pointerover"]),sa("onChange","change click focusin focusout input keydown keyup selectionchange".split(" ")),sa("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" ")),sa("onBeforeInput",["compositionend","keypress","textInput","paste"]),sa("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" ")),sa("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" ")),sa("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));var Ol="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),lx=new Set("beforetoggle cancel close invalid load scroll scrollend toggle".split(" ").concat(Ol));function Jv(t,n){n=(n&4)!==0;for(var s=0;s<t.length;s++){var u=t[s],h=u.event;u=u.listeners;e:{var p=void 0;if(n)for(var C=u.length-1;0<=C;C--){var k=u[C],z=k.instance,Y=k.currentTarget;if(k=k.listener,z!==p&&h.isPropagationStopped())break e;p=k,h.currentTarget=Y;try{p(h)}catch(ne){Yc(ne)}h.currentTarget=null,p=z}else for(C=0;C<u.length;C++){if(k=u[C],z=k.instance,Y=k.currentTarget,k=k.listener,z!==p&&h.isPropagationStopped())break e;p=k,h.currentTarget=Y;try{p(h)}catch(ne){Yc(ne)}h.currentTarget=null,p=z}}}}function nt(t,n){var s=n[No];s===void 0&&(s=n[No]=new Set);var u=t+"__bubble";s.has(u)||(Qv(n,t,2,!1),s.add(u))}function dg(t,n,s){var u=0;n&&(u|=4),Qv(s,t,u,n)}var Nu="_reactListening"+Math.random().toString(36).slice(2);function fg(t){if(!t[Nu]){t[Nu]=!0,Mc.forEach(function(s){s!=="selectionchange"&&(lx.has(s)||dg(s,!1,t),dg(s,!0,t))});var n=t.nodeType===9?t:t.ownerDocument;n===null||n[Nu]||(n[Nu]=!0,dg("selectionchange",!1,n))}}function Qv(t,n,s,u){switch(TS(n)){case 2:var h=Ix;break;case 8:h=Hx;break;default:h=Tg}s=h.bind(null,n,s,t),h=void 0,!Vo||n!=="touchstart"&&n!=="touchmove"&&n!=="wheel"||(h=!0),u?h!==void 0?t.addEventListener(n,s,{capture:!0,passive:h}):t.addEventListener(n,s,!0):h!==void 0?t.addEventListener(n,s,{passive:h}):t.addEventListener(n,s,!1)}function hg(t,n,s,u,h){var p=u;if((n&1)===0&&(n&2)===0&&u!==null)e:for(;;){if(u===null)return;var C=u.tag;if(C===3||C===4){var k=u.stateNode.containerInfo;if(k===h)break;if(C===4)for(C=u.return;C!==null;){var z=C.tag;if((z===3||z===4)&&C.stateNode.containerInfo===h)return;C=C.return}for(;k!==null;){if(C=Ia(k),C===null)return;if(z=C.tag,z===5||z===6||z===26||z===27){u=p=C;continue e}k=k.parentNode}}u=u.return}zc(function(){var Y=p,ne=vs(s),le=[];e:{var Q=Oy.get(t);if(Q!==void 0){var te=ws,Te=t;switch(t){case"keypress":if(qa(s)===0)break e;case"keydown":case"keyup":te=rn;break;case"focusin":Te="focus",te=Yo;break;case"focusout":Te="blur",te=Yo;break;case"beforeblur":case"afterblur":te=Yo;break;case"click":if(s.button===2)break e;case"auxclick":case"dblclick":case"mousedown":case"mousemove":case"mouseup":case"mouseout":case"mouseover":case"contextmenu":te=Bc;break;case"drag":case"dragend":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"dragstart":case"drop":te=Tf;break;case"touchcancel":case"touchend":case"touchmove":case"touchstart":te=b;break;case Ty:case ky:case Ry:te=Df;break;case Dy:te=D;break;case"scroll":case"scrollend":te=xf;break;case"wheel":te=ie;break;case"copy":case"cut":case"paste":te=Lf;break;case"gotpointercapture":case"lostpointercapture":case"pointercancel":case"pointerdown":case"pointermove":case"pointerout":case"pointerover":case"pointerup":te=qc;break;case"toggle":case"beforetoggle":te=Oe}var He=(n&4)!==0,_t=!He&&(t==="scroll"||t==="scrollend"),F=He?Q!==null?Q+"Capture":null:Q;He=[];for(var P=Y,W;P!==null;){var se=P;if(W=se.stateNode,se=se.tag,se!==5&&se!==26&&se!==27||W===null||F===null||(se=xi(P,F),se!=null&&He.push(Ll(P,se,W))),_t)break;P=P.return}0<He.length&&(Q=new te(Q,Te,null,s,ne),le.push({event:Q,listeners:He}))}}if((n&7)===0){e:{if(Q=t==="mouseover"||t==="pointerover",te=t==="mouseout"||t==="pointerout",Q&&s!==qo&&(Te=s.relatedTarget||s.fromElement)&&(Ia(Te)||Te[In]))break e;if((te||Q)&&(Q=ne.window===ne?ne:(Q=ne.ownerDocument)?Q.defaultView||Q.parentWindow:window,te?(Te=s.relatedTarget||s.toElement,te=Y,Te=Te?Ia(Te):null,Te!==null&&(_t=l(Te),He=Te.tag,Te!==_t||He!==5&&He!==27&&He!==6)&&(Te=null)):(te=null,Te=Y),te!==Te)){if(He=Bc,se="onMouseLeave",F="onMouseEnter",P="mouse",(t==="pointerout"||t==="pointerover")&&(He=qc,se="onPointerLeave",F="onPointerEnter",P="pointer"),_t=te==null?Q:_i(te),W=Te==null?Q:_i(Te),Q=new He(se,P+"leave",te,s,ne),Q.target=_t,Q.relatedTarget=W,se=null,Ia(ne)===Y&&(He=new He(F,P+"enter",Te,s,ne),He.target=W,He.relatedTarget=_t,se=He),_t=se,te&&Te)t:{for(He=cx,F=te,P=Te,W=0,se=F;se;se=He(se))W++;se=0;for(var Ie=P;Ie;Ie=He(Ie))se++;for(;0<W-se;)F=He(F),W--;for(;0<se-W;)P=He(P),se--;for(;W--;){if(F===P||P!==null&&F===P.alternate){He=F;break t}F=He(F),P=He(P)}He=null}else He=null;te!==null&&Zv(le,Q,te,He,!1),Te!==null&&_t!==null&&Zv(le,_t,Te,He,!0)}}e:{if(Q=Y?_i(Y):window,te=Q.nodeName&&Q.nodeName.toLowerCase(),te==="select"||te==="input"&&Q.type==="file")var ft=Kc;else if(el(Q))if(Wc)ft=SA;else{ft=bA;var Le=yA}else te=Q.nodeName,!te||te.toLowerCase()!=="input"||Q.type!=="checkbox"&&Q.type!=="radio"?Y&&Po(Y.elementType)&&(ft=Kc):ft=vA;if(ft&&(ft=ft(t,Y))){tl(le,ft,s,ne);break e}Le&&Le(t,Q,Y),t==="focusout"&&Y&&Q.type==="number"&&Y.memoizedProps.value!=null&&Bo(Q,"number",Q.value)}switch(Le=Y?_i(Y):window,t){case"focusin":(el(Le)||Le.contentEditable==="true")&&(_s=Le,Pf=Y,ol=null);break;case"focusout":ol=Pf=_s=null;break;case"mousedown":qf=!0;break;case"contextmenu":case"mouseup":case"dragend":qf=!1,Ay(le,s,ne);break;case"selectionchange":if(CA)break;case"keydown":case"keyup":Ay(le,s,ne)}var Je;if(at)e:{switch(t){case"compositionstart":var st="onCompositionStart";break e;case"compositionend":st="onCompositionEnd";break e;case"compositionupdate":st="onCompositionUpdate";break e}st=void 0}else Ga?Vc(t,s)&&(st="onCompositionEnd"):t==="keydown"&&s.keyCode===229&&(st="onCompositionStart");st&&(Ri&&s.locale!=="ko"&&(Ga||st!=="onCompositionStart"?st==="onCompositionEnd"&&Ga&&(Je=Ve()):(Or=ne,Fo="value"in Or?Or.value:Or.textContent,Ga=!0)),Le=Iu(Y,st),0<Le.length&&(st=new Pc(st,t,null,s,ne),le.push({event:st,listeners:Le}),Je?st.data=Je:(Je=Zo(s),Je!==null&&(st.data=Je)))),(Je=Bt?Fc(t,s):zf(t,s))&&(st=Iu(Y,"onBeforeInput"),0<st.length&&(Le=new Pc("onBeforeInput","beforeinput",null,s,ne),le.push({event:Le,listeners:st}),Le.data=Je)),ix(le,t,Y,s,ne)}Jv(le,n)})}function Ll(t,n,s){return{instance:t,listener:n,currentTarget:s}}function Iu(t,n){for(var s=n+"Capture",u=[];t!==null;){var h=t,p=h.stateNode;if(h=h.tag,h!==5&&h!==26&&h!==27||p===null||(h=xi(t,s),h!=null&&u.unshift(Ll(t,h,p)),h=xi(t,n),h!=null&&u.push(Ll(t,h,p))),t.tag===3)return u;t=t.return}return[]}function cx(t){if(t===null)return null;do t=t.return;while(t&&t.tag!==5&&t.tag!==27);return t||null}function Zv(t,n,s,u,h){for(var p=n._reactName,C=[];s!==null&&s!==u;){var k=s,z=k.alternate,Y=k.stateNode;if(k=k.tag,z!==null&&z===u)break;k!==5&&k!==26&&k!==27||Y===null||(z=Y,h?(Y=xi(s,p),Y!=null&&C.unshift(Ll(s,Y,z))):h||(Y=xi(s,p),Y!=null&&C.push(Ll(s,Y,z)))),s=s.return}C.length!==0&&t.push({event:n,listeners:C})}var ux=/\r\n?/g,dx=/\u0000|\uFFFD/g;function eS(t){return(typeof t=="string"?t:""+t).replace(ux,` -`).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"innerHTML":case"ref":break;case"innerText":case"textContent":break;default:if(!$c.hasOwnProperty(s))e:{if(s[0]==="o"&&s[1]==="n"&&(h=s.endsWith("Capture"),n=s.slice(2,h?s.length-7:void 0),p=t[gn]||null,p=p!=null?p[s]:null,typeof p=="function"&&t.removeEventListener(n,p,h),typeof u=="function")){typeof p!="function"&&p!==null&&(s in t?t[s]=null:t.hasAttribute(s)&&t.removeAttribute(s)),t.addEventListener(n,u,h);break e}s in t?t[s]=u:u===!0?t.setAttribute(s,""):gs(t,s,u)}}}function bn(t,n,s){switch(n){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"img":nt("error",t),nt("load",t);var u=!1,h=!1,p;for(p in s)if(s.hasOwnProperty(p)){var C=s[p];if(C!=null)switch(p){case"src":u=!0;break;case"srcSet":h=!0;break;case"children":case"dangerouslySetInnerHTML":throw Error(a(137,n));default:Et(t,n,p,C,s,null)}}h&&Et(t,n,"srcSet",s.srcSet,s,null),u&&Et(t,n,"src",s.src,s,null);return;case"input":nt("invalid",t);var k=p=C=h=null,z=null,Y=null;for(u in s)if(s.hasOwnProperty(u)){var ne=s[u];if(ne!=null)switch(u){case"name":h=ne;break;case"type":C=ne;break;case"checked":z=ne;break;case"defaultChecked":Y=ne;break;case"value":p=ne;break;case"defaultValue":k=ne;break;case"children":case"dangerouslySetInnerHTML":if(ne!=null)throw Error(a(137,n));break;default:Et(t,n,u,ne,s,null)}}Nc(t,p,k,z,Y,C,h,!1);return;case"select":nt("invalid",t),u=C=p=null;for(h in s)if(s.hasOwnProperty(h)&&(k=s[h],k!=null))switch(h){case"value":p=k;break;case"defaultValue":C=k;break;case"multiple":u=k;default:Et(t,n,h,k,s,null)}n=p,s=C,t.multiple=!!u,n!=null?za(t,!!u,n,!1):s!=null&&za(t,!!u,s,!0);return;case"textarea":nt("invalid",t),p=h=u=null;for(C in s)if(s.hasOwnProperty(C)&&(k=s[C],k!=null))switch(C){case"value":u=k;break;case"defaultValue":h=k;break;case"children":p=k;break;case"dangerouslySetInnerHTML":if(k!=null)throw Error(a(91));break;default:Et(t,n,C,k,s,null)}Hc(t,u,h,p);return;case"option":for(z in s)if(s.hasOwnProperty(z)&&(u=s[z],u!=null))switch(z){case"selected":t.selected=u&&typeof u!="function"&&typeof u!="symbol";break;default:Et(t,n,z,u,s,null)}return;case"dialog":nt("beforetoggle",t),nt("toggle",t),nt("cancel",t),nt("close",t);break;case"iframe":case"object":nt("load",t);break;case"video":case"audio":for(u=0;u<Ol.length;u++)nt(Ol[u],t);break;case"image":nt("error",t),nt("load",t);break;case"details":nt("toggle",t);break;case"embed":case"source":case"link":nt("error",t),nt("load",t);case"area":case"base":case"br":case"col":case"hr":case"keygen":case"meta":case"param":case"track":case"wbr":case"menuitem":for(Y in s)if(s.hasOwnProperty(Y)&&(u=s[Y],u!=null))switch(Y){case"children":case"dangerouslySetInnerHTML":throw Error(a(137,n));default:Et(t,n,Y,u,s,null)}return;default:if(Po(n)){for(ne in s)s.hasOwnProperty(ne)&&(u=s[ne],u!==void 0&&gg(t,n,ne,u,s,void 0));return}}for(k in s)s.hasOwnProperty(k)&&(u=s[k],u!=null&&Et(t,n,k,u,s,null))}function fx(t,n,s,u){switch(n){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"input":var h=null,p=null,C=null,k=null,z=null,Y=null,ne=null;for(te in s){var le=s[te];if(s.hasOwnProperty(te)&&le!=null)switch(te){case"checked":break;case"value":break;case"defaultValue":z=le;default:u.hasOwnProperty(te)||Et(t,n,te,null,u,le)}}for(var Q in u){var te=u[Q];if(le=s[Q],u.hasOwnProperty(Q)&&(te!=null||le!=null))switch(Q){case"type":p=te;break;case"name":h=te;break;case"checked":Y=te;break;case"defaultChecked":ne=te;break;case"value":C=te;break;case"defaultValue":k=te;break;case"children":case"dangerouslySetInnerHTML":if(te!=null)throw Error(a(137,n));break;default:te!==le&&Et(t,n,Q,te,u,le)}}la(t,C,k,z,Y,ne,p,h);return;case"select":te=C=k=Q=null;for(p in s)if(z=s[p],s.hasOwnProperty(p)&&z!=null)switch(p){case"value":break;case"multiple":te=z;default:u.hasOwnProperty(p)||Et(t,n,p,null,u,z)}for(h in u)if(p=u[h],z=s[h],u.hasOwnProperty(h)&&(p!=null||z!=null))switch(h){case"value":Q=p;break;case"defaultValue":k=p;break;case"multiple":C=p;default:p!==z&&Et(t,n,h,p,u,z)}n=k,s=C,u=te,Q!=null?za(t,!!s,Q,!1):!!u!=!!s&&(n!=null?za(t,!!s,n,!0):za(t,!!s,s?[]:"",!1));return;case"textarea":te=Q=null;for(k in s)if(h=s[k],s.hasOwnProperty(k)&&h!=null&&!u.hasOwnProperty(k))switch(k){case"value":break;case"children":break;default:Et(t,n,k,null,u,h)}for(C in u)if(h=u[C],p=s[C],u.hasOwnProperty(C)&&(h!=null||p!=null))switch(C){case"value":Q=h;break;case"defaultValue":te=h;break;case"children":break;case"dangerouslySetInnerHTML":if(h!=null)throw Error(a(91));break;default:h!==p&&Et(t,n,C,h,u,p)}Ic(t,Q,te);return;case"option":for(var Te in s)if(Q=s[Te],s.hasOwnProperty(Te)&&Q!=null&&!u.hasOwnProperty(Te))switch(Te){case"selected":t.selected=!1;break;default:Et(t,n,Te,null,u,Q)}for(z in u)if(Q=u[z],te=s[z],u.hasOwnProperty(z)&&Q!==te&&(Q!=null||te!=null))switch(z){case"selected":t.selected=Q&&typeof Q!="function"&&typeof Q!="symbol";break;default:Et(t,n,z,Q,u,te)}return;case"img":case"link":case"area":case"base":case"br":case"col":case"embed":case"hr":case"keygen":case"meta":case"param":case"source":case"track":case"wbr":case"menuitem":for(var He in s)Q=s[He],s.hasOwnProperty(He)&&Q!=null&&!u.hasOwnProperty(He)&&Et(t,n,He,null,u,Q);for(Y in u)if(Q=u[Y],te=s[Y],u.hasOwnProperty(Y)&&Q!==te&&(Q!=null||te!=null))switch(Y){case"children":case"dangerouslySetInnerHTML":if(Q!=null)throw Error(a(137,n));break;default:Et(t,n,Y,Q,u,te)}return;default:if(Po(n)){for(var _t in s)Q=s[_t],s.hasOwnProperty(_t)&&Q!==void 0&&!u.hasOwnProperty(_t)&&gg(t,n,_t,void 0,u,Q);for(ne in u)Q=u[ne],te=s[ne],!u.hasOwnProperty(ne)||Q===te||Q===void 0&&te===void 0||gg(t,n,ne,Q,u,te);return}}for(var F in s)Q=s[F],s.hasOwnProperty(F)&&Q!=null&&!u.hasOwnProperty(F)&&Et(t,n,F,null,u,Q);for(le in u)Q=u[le],te=s[le],!u.hasOwnProperty(le)||Q===te||Q==null&&te==null||Et(t,n,le,Q,u,te)}function nS(t){switch(t){case"css":case"script":case"font":case"img":case"image":case"input":case"link":return!0;default:return!1}}function hx(){if(typeof performance.getEntriesByType=="function"){for(var t=0,n=0,s=performance.getEntriesByType("resource"),u=0;u<s.length;u++){var h=s[u],p=h.transferSize,C=h.initiatorType,k=h.duration;if(p&&k&&nS(C)){for(C=0,k=h.responseEnd,u+=1;u<s.length;u++){var z=s[u],Y=z.startTime;if(Y>k)break;var ne=z.transferSize,le=z.initiatorType;ne&&nS(le)&&(z=z.responseEnd,C+=ne*(z<k?1:(k-Y)/(z-Y)))}if(--u,n+=8*(p+C)/(h.duration/1e3),t++,10<t)break}}if(0<t)return n/t/1e6}return navigator.connection&&(t=navigator.connection.downlink,typeof t=="number")?t:5}var pg=null,mg=null;function Hu(t){return t.nodeType===9?t:t.ownerDocument}function rS(t){switch(t){case"http://www.w3.org/2000/svg":return 1;case"http://www.w3.org/1998/Math/MathML":return 2;default:return 0}}function aS(t,n){if(t===0)switch(n){case"svg":return 1;case"math":return 2;default:return 0}return t===1&&n==="foreignObject"?0:t}function yg(t,n){return t==="textarea"||t==="noscript"||typeof n.children=="string"||typeof n.children=="number"||typeof n.children=="bigint"||typeof n.dangerouslySetInnerHTML=="object"&&n.dangerouslySetInnerHTML!==null&&n.dangerouslySetInnerHTML.__html!=null}var bg=null;function gx(){var t=window.event;return t&&t.type==="popstate"?t===bg?!1:(bg=t,!0):(bg=null,!1)}var iS=typeof setTimeout=="function"?setTimeout:void 0,px=typeof clearTimeout=="function"?clearTimeout:void 0,sS=typeof Promise=="function"?Promise:void 0,mx=typeof queueMicrotask=="function"?queueMicrotask:typeof sS<"u"?function(t){return sS.resolve(null).then(t).catch(yx)}:iS;function yx(t){setTimeout(function(){throw t})}function li(t){return t==="head"}function oS(t,n){var s=n,u=0;do{var h=s.nextSibling;if(t.removeChild(s),h&&h.nodeType===8)if(s=h.data,s==="/$"||s==="/&"){if(u===0){t.removeChild(h),Js(n);return}u--}else if(s==="$"||s==="$?"||s==="$~"||s==="$!"||s==="&")u++;else if(s==="html")Ml(t.ownerDocument.documentElement);else if(s==="head"){s=t.ownerDocument.head,Ml(s);for(var p=s.firstChild;p;){var C=p.nextSibling,k=p.nodeName;p[ia]||k==="SCRIPT"||k==="STYLE"||k==="LINK"&&p.rel.toLowerCase()==="stylesheet"||s.removeChild(p),p=C}}else s==="body"&&Ml(t.ownerDocument.body);s=h}while(s);Js(n)}function lS(t,n){var s=t;t=0;do{var u=s.nextSibling;if(s.nodeType===1?n?(s._stashedDisplay=s.style.display,s.style.display="none"):(s.style.display=s._stashedDisplay||"",s.getAttribute("style")===""&&s.removeAttribute("style")):s.nodeType===3&&(n?(s._stashedText=s.nodeValue,s.nodeValue=""):s.nodeValue=s._stashedText||""),u&&u.nodeType===8)if(s=u.data,s==="/$"){if(t===0)break;t--}else s!=="$"&&s!=="$?"&&s!=="$~"&&s!=="$!"||t++;s=u}while(s)}function vg(t){var n=t.firstChild;for(n&&n.nodeType===10&&(n=n.nextSibling);n;){var s=n;switch(n=n.nextSibling,s.nodeName){case"HTML":case"HEAD":case"BODY":vg(s),Io(s);continue;case"SCRIPT":case"STYLE":continue;case"LINK":if(s.rel.toLowerCase()==="stylesheet")continue}t.removeChild(s)}}function bx(t,n,s,u){for(;t.nodeType===1;){var h=s;if(t.nodeName.toLowerCase()!==n.toLowerCase()){if(!u&&(t.nodeName!=="INPUT"||t.type!=="hidden"))break}else if(u){if(!t[ia])switch(n){case"meta":if(!t.hasAttribute("itemprop"))break;return t;case"link":if(p=t.getAttribute("rel"),p==="stylesheet"&&t.hasAttribute("data-precedence"))break;if(p!==h.rel||t.getAttribute("href")!==(h.href==null||h.href===""?null:h.href)||t.getAttribute("crossorigin")!==(h.crossOrigin==null?null:h.crossOrigin)||t.getAttribute("title")!==(h.title==null?null:h.title))break;return t;case"style":if(t.hasAttribute("data-precedence"))break;return t;case"script":if(p=t.getAttribute("src"),(p!==(h.src==null?null:h.src)||t.getAttribute("type")!==(h.type==null?null:h.type)||t.getAttribute("crossorigin")!==(h.crossOrigin==null?null:h.crossOrigin))&&p&&t.hasAttribute("async")&&!t.hasAttribute("itemprop"))break;return t;default:return t}}else if(n==="input"&&t.type==="hidden"){var p=h.name==null?null:""+h.name;if(h.type==="hidden"&&t.getAttribute("name")===p)return t}else return t;if(t=gr(t.nextSibling),t===null)break}return null}function vx(t,n,s){if(n==="")return null;for(;t.nodeType!==3;)if((t.nodeType!==1||t.nodeName!=="INPUT"||t.type!=="hidden")&&!s||(t=gr(t.nextSibling),t===null))return null;return t}function cS(t,n){for(;t.nodeType!==8;)if((t.nodeType!==1||t.nodeName!=="INPUT"||t.type!=="hidden")&&!n||(t=gr(t.nextSibling),t===null))return null;return t}function Sg(t){return t.data==="$?"||t.data==="$~"}function wg(t){return t.data==="$!"||t.data==="$?"&&t.ownerDocument.readyState!=="loading"}function Sx(t,n){var s=t.ownerDocument;if(t.data==="$~")t._reactRetry=n;else if(t.data!=="$?"||s.readyState!=="loading")n();else{var u=function(){n(),s.removeEventListener("DOMContentLoaded",u)};s.addEventListener("DOMContentLoaded",u),t._reactRetry=u}}function gr(t){for(;t!=null;t=t.nextSibling){var n=t.nodeType;if(n===1||n===3)break;if(n===8){if(n=t.data,n==="$"||n==="$!"||n==="$?"||n==="$~"||n==="&"||n==="F!"||n==="F")break;if(n==="/$"||n==="/&")return null}}return t}var Cg=null;function uS(t){t=t.nextSibling;for(var n=0;t;){if(t.nodeType===8){var s=t.data;if(s==="/$"||s==="/&"){if(n===0)return gr(t.nextSibling);n--}else s!=="$"&&s!=="$!"&&s!=="$?"&&s!=="$~"&&s!=="&"||n++}t=t.nextSibling}return null}function dS(t){t=t.previousSibling;for(var n=0;t;){if(t.nodeType===8){var s=t.data;if(s==="$"||s==="$!"||s==="$?"||s==="$~"||s==="&"){if(n===0)return t;n--}else s!=="/$"&&s!=="/&"||n++}t=t.previousSibling}return null}function fS(t,n,s){switch(n=Hu(s),t){case"html":if(t=n.documentElement,!t)throw Error(a(452));return t;case"head":if(t=n.head,!t)throw Error(a(453));return t;case"body":if(t=n.body,!t)throw Error(a(454));return t;default:throw Error(a(451))}}function Ml(t){for(var n=t.attributes;n.length;)t.removeAttributeNode(n[0]);Io(t)}var pr=new Map,hS=new Set;function ju(t){return typeof t.getRootNode=="function"?t.getRootNode():t.nodeType===9?t:t.ownerDocument}var Aa=he.d;he.d={f:wx,r:Cx,D:Ex,C:_x,L:Ax,m:xx,X:kx,S:Tx,M:Rx};function wx(){var t=Aa.f(),n=Ru();return t||n}function Cx(t){var n=Ha(t);n!==null&&n.tag===5&&n.type==="form"?Db(n):Aa.r(t)}var Ws=typeof document>"u"?null:document;function gS(t,n,s){var u=Ws;if(u&&typeof n=="string"&&n){var h=jn(n);h='link[rel="'+t+'"][href="'+h+'"]',typeof s=="string"&&(h+='[crossorigin="'+s+'"]'),hS.has(h)||(hS.add(h),t={rel:t,crossOrigin:s,href:n},u.querySelector(h)===null&&(n=u.createElement("link"),bn(n,"link",t),Xt(n),u.head.appendChild(n)))}}function Ex(t){Aa.D(t),gS("dns-prefetch",t,null)}function _x(t,n){Aa.C(t,n),gS("preconnect",t,n)}function Ax(t,n,s){Aa.L(t,n,s);var u=Ws;if(u&&t&&n){var h='link[rel="preload"][as="'+jn(n)+'"]';n==="image"&&s&&s.imageSrcSet?(h+='[imagesrcset="'+jn(s.imageSrcSet)+'"]',typeof s.imageSizes=="string"&&(h+='[imagesizes="'+jn(s.imageSizes)+'"]')):h+='[href="'+jn(t)+'"]';var p=h;switch(n){case"style":p=Ys(t);break;case"script":p=Xs(t)}pr.has(p)||(t=y({rel:"preload",href:n==="image"&&s&&s.imageSrcSet?void 0:t,as:n},s),pr.set(p,t),u.querySelector(h)!==null||n==="style"&&u.querySelector($l(p))||n==="script"&&u.querySelector(Nl(p))||(n=u.createElement("link"),bn(n,"link",t),Xt(n),u.head.appendChild(n)))}}function xx(t,n){Aa.m(t,n);var s=Ws;if(s&&t){var u=n&&typeof n.as=="string"?n.as:"script",h='link[rel="modulepreload"][as="'+jn(u)+'"][href="'+jn(t)+'"]',p=h;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":p=Xs(t)}if(!pr.has(p)&&(t=y({rel:"modulepreload",href:t},n),pr.set(p,t),s.querySelector(h)===null)){switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(s.querySelector(Nl(p)))return}u=s.createElement("link"),bn(u,"link",t),Xt(u),s.head.appendChild(u)}}}function Tx(t,n,s){Aa.S(t,n,s);var u=Ws;if(u&&t){var h=qr(u).hoistableStyles,p=Ys(t);n=n||"default";var C=h.get(p);if(!C){var k={loading:0,preload:null};if(C=u.querySelector($l(p)))k.loading=5;else{t=y({rel:"stylesheet",href:t,"data-precedence":n},s),(s=pr.get(p))&&Eg(t,s);var z=C=u.createElement("link");Xt(z),bn(z,"link",t),z._p=new Promise(function(Y,ne){z.onload=Y,z.onerror=ne}),z.addEventListener("load",function(){k.loading|=1}),z.addEventListener("error",function(){k.loading|=2}),k.loading|=4,zu(C,n,u)}C={type:"stylesheet",instance:C,count:1,state:k},h.set(p,C)}}}function kx(t,n){Aa.X(t,n);var s=Ws;if(s&&t){var u=qr(s).hoistableScripts,h=Xs(t),p=u.get(h);p||(p=s.querySelector(Nl(h)),p||(t=y({src:t,async:!0},n),(n=pr.get(h))&&_g(t,n),p=s.createElement("script"),Xt(p),bn(p,"link",t),s.head.appendChild(p)),p={type:"script",instance:p,count:1,state:null},u.set(h,p))}}function Rx(t,n){Aa.M(t,n);var s=Ws;if(s&&t){var u=qr(s).hoistableScripts,h=Xs(t),p=u.get(h);p||(p=s.querySelector(Nl(h)),p||(t=y({src:t,async:!0,type:"module"},n),(n=pr.get(h))&&_g(t,n),p=s.createElement("script"),Xt(p),bn(p,"link",t),s.head.appendChild(p)),p={type:"script",instance:p,count:1,state:null},u.set(h,p))}}function pS(t,n,s,u){var h=(h=ae.current)?ju(h):null;if(!h)throw Error(a(446));switch(t){case"meta":case"title":return null;case"style":return typeof s.precedence=="string"&&typeof s.href=="string"?(n=Ys(s.href),s=qr(h).hoistableStyles,u=s.get(n),u||(u={type:"style",instance:null,count:0,state:null},s.set(n,u)),u):{type:"void",instance:null,count:0,state:null};case"link":if(s.rel==="stylesheet"&&typeof s.href=="string"&&typeof s.precedence=="string"){t=Ys(s.href);var p=qr(h).hoistableStyles,C=p.get(t);if(C||(h=h.ownerDocument||h,C={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},p.set(t,C),(p=h.querySelector($l(t)))&&!p._p&&(C.instance=p,C.state.loading=5),pr.has(t)||(s={rel:"preload",as:"style",href:s.href,crossOrigin:s.crossOrigin,integrity:s.integrity,media:s.media,hrefLang:s.hrefLang,referrerPolicy:s.referrerPolicy},pr.set(t,s),p||Dx(h,t,s,C.state))),n&&u===null)throw Error(a(528,""));return C}if(n&&u!==null)throw Error(a(529,""));return null;case"script":return n=s.async,s=s.src,typeof s=="string"&&n&&typeof n!="function"&&typeof n!="symbol"?(n=Xs(s),s=qr(h).hoistableScripts,u=s.get(n),u||(u={type:"script",instance:null,count:0,state:null},s.set(n,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(a(444,t))}}function Ys(t){return'href="'+jn(t)+'"'}function $l(t){return'link[rel="stylesheet"]['+t+"]"}function mS(t){return y({},t,{"data-precedence":t.precedence,precedence:null})}function Dx(t,n,s,u){t.querySelector('link[rel="preload"][as="style"]['+n+"]")?u.loading=1:(n=t.createElement("link"),u.preload=n,n.addEventListener("load",function(){return u.loading|=1}),n.addEventListener("error",function(){return u.loading|=2}),bn(n,"link",s),Xt(n),t.head.appendChild(n))}function Xs(t){return'[src="'+jn(t)+'"]'}function Nl(t){return"script[async]"+t}function yS(t,n,s){if(n.count++,n.instance===null)switch(n.type){case"style":var u=t.querySelector('style[data-href~="'+jn(s.href)+'"]');if(u)return n.instance=u,Xt(u),u;var h=y({},s,{"data-href":s.href,"data-precedence":s.precedence,href:null,precedence:null});return u=(t.ownerDocument||t).createElement("style"),Xt(u),bn(u,"style",h),zu(u,s.precedence,t),n.instance=u;case"stylesheet":h=Ys(s.href);var p=t.querySelector($l(h));if(p)return n.state.loading|=4,n.instance=p,Xt(p),p;u=mS(s),(h=pr.get(h))&&Eg(u,h),p=(t.ownerDocument||t).createElement("link"),Xt(p);var C=p;return C._p=new Promise(function(k,z){C.onload=k,C.onerror=z}),bn(p,"link",u),n.state.loading|=4,zu(p,s.precedence,t),n.instance=p;case"script":return p=Xs(s.src),(h=t.querySelector(Nl(p)))?(n.instance=h,Xt(h),h):(u=s,(h=pr.get(p))&&(u=y({},s),_g(u,h)),t=t.ownerDocument||t,h=t.createElement("script"),Xt(h),bn(h,"link",u),t.head.appendChild(h),n.instance=h);case"void":return null;default:throw Error(a(443,n.type))}else n.type==="stylesheet"&&(n.state.loading&4)===0&&(u=n.instance,n.state.loading|=4,zu(u,s.precedence,t));return n.instance}function zu(t,n,s){for(var u=s.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),h=u.length?u[u.length-1]:null,p=h,C=0;C<u.length;C++){var k=u[C];if(k.dataset.precedence===n)p=k;else if(p!==h)break}p?p.parentNode.insertBefore(t,p.nextSibling):(n=s.nodeType===9?s.head:s,n.insertBefore(t,n.firstChild))}function Eg(t,n){t.crossOrigin==null&&(t.crossOrigin=n.crossOrigin),t.referrerPolicy==null&&(t.referrerPolicy=n.referrerPolicy),t.title==null&&(t.title=n.title)}function _g(t,n){t.crossOrigin==null&&(t.crossOrigin=n.crossOrigin),t.referrerPolicy==null&&(t.referrerPolicy=n.referrerPolicy),t.integrity==null&&(t.integrity=n.integrity)}var Uu=null;function bS(t,n,s){if(Uu===null){var u=new Map,h=Uu=new Map;h.set(s,u)}else h=Uu,u=h.get(s),u||(u=new Map,h.set(s,u));if(u.has(t))return u;for(u.set(t,null),s=s.getElementsByTagName(t),h=0;h<s.length;h++){var p=s[h];if(!(p[ia]||p[Nt]||t==="link"&&p.getAttribute("rel")==="stylesheet")&&p.namespaceURI!=="http://www.w3.org/2000/svg"){var C=p.getAttribute(n)||"";C=t+C;var k=u.get(C);k?k.push(p):u.set(C,[p])}}return u}function vS(t,n,s){t=t.ownerDocument||t,t.head.insertBefore(s,n==="title"?t.querySelector("head > title"):null)}function Ox(t,n,s){if(s===1||n.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof n.precedence!="string"||typeof n.href!="string"||n.href==="")break;return!0;case"link":if(typeof n.rel!="string"||typeof n.href!="string"||n.href===""||n.onLoad||n.onError)break;switch(n.rel){case"stylesheet":return t=n.disabled,typeof n.precedence=="string"&&t==null;default:return!0}case"script":if(n.async&&typeof n.async!="function"&&typeof n.async!="symbol"&&!n.onLoad&&!n.onError&&n.src&&typeof n.src=="string")return!0}return!1}function SS(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function Lx(t,n,s,u){if(s.type==="stylesheet"&&(typeof u.media!="string"||matchMedia(u.media).matches!==!1)&&(s.state.loading&4)===0){if(s.instance===null){var h=Ys(u.href),p=n.querySelector($l(h));if(p){n=p._p,n!==null&&typeof n=="object"&&typeof n.then=="function"&&(t.count++,t=Bu.bind(t),n.then(t,t)),s.state.loading|=4,s.instance=p,Xt(p);return}p=n.ownerDocument||n,u=mS(u),(h=pr.get(h))&&Eg(u,h),p=p.createElement("link"),Xt(p);var C=p;C._p=new Promise(function(k,z){C.onload=k,C.onerror=z}),bn(p,"link",u),s.instance=p}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(s,n),(n=s.state.preload)&&(s.state.loading&3)===0&&(t.count++,s=Bu.bind(t),n.addEventListener("load",s),n.addEventListener("error",s))}}var Ag=0;function Mx(t,n){return t.stylesheets&&t.count===0&&qu(t,t.stylesheets),0<t.count||0<t.imgCount?function(s){var u=setTimeout(function(){if(t.stylesheets&&qu(t,t.stylesheets),t.unsuspend){var p=t.unsuspend;t.unsuspend=null,p()}},6e4+n);0<t.imgBytes&&Ag===0&&(Ag=62500*hx());var h=setTimeout(function(){if(t.waitingForImages=!1,t.count===0&&(t.stylesheets&&qu(t,t.stylesheets),t.unsuspend)){var p=t.unsuspend;t.unsuspend=null,p()}},(t.imgBytes>Ag?50:800)+n);return t.unsuspend=s,function(){t.unsuspend=null,clearTimeout(u),clearTimeout(h)}}:null}function Bu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)qu(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Pu=null;function qu(t,n){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Pu=new Map,n.forEach($x,t),Pu=null,Bu.call(t))}function $x(t,n){if(!(n.state.loading&4)){var s=Pu.get(t);if(s)var u=s.get(null);else{s=new Map,Pu.set(t,s);for(var h=t.querySelectorAll("link[data-precedence],style[data-precedence]"),p=0;p<h.length;p++){var C=h[p];(C.nodeName==="LINK"||C.getAttribute("media")!=="not all")&&(s.set(C.dataset.precedence,C),u=C)}u&&s.set(null,u)}h=n.instance,C=h.getAttribute("data-precedence"),p=s.get(C)||u,p===u&&s.set(null,h),s.set(C,h),this.count++,u=Bu.bind(this),h.addEventListener("load",u),h.addEventListener("error",u),p?p.parentNode.insertBefore(h,p.nextSibling):(t=t.nodeType===9?t.head:t,t.insertBefore(h,t.firstChild)),n.state.loading|=4}}var Il={$$typeof:O,Provider:null,Consumer:null,_currentValue:Ee,_currentValue2:Ee,_threadCount:0};function Nx(t,n,s,u,h,p,C,k,z){this.tag=1,this.containerInfo=t,this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.next=this.pendingContext=this.context=this.cancelPendingCommit=null,this.callbackPriority=0,this.expirationTimes=Ae(-1),this.entangledLanes=this.shellSuspendCounter=this.errorRecoveryDisabledLanes=this.expiredLanes=this.warmLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Ae(0),this.hiddenUpdates=Ae(null),this.identifierPrefix=u,this.onUncaughtError=h,this.onCaughtError=p,this.onRecoverableError=C,this.pooledCache=null,this.pooledCacheLanes=0,this.formState=z,this.incompleteTransitions=new Map}function wS(t,n,s,u,h,p,C,k,z,Y,ne,le){return t=new Nx(t,n,s,C,z,Y,ne,le,k),n=1,p===!0&&(n|=24),p=Xn(3,null,null,n),t.current=p,p.stateNode=t,n=ah(),n.refCount++,t.pooledCache=n,n.refCount++,p.memoizedState={element:u,isDehydrated:s,cache:n},lh(p),t}function CS(t){return t?(t=Ts,t):Ts}function ES(t,n,s,u,h,p){h=CS(h),u.context===null?u.context=h:u.pendingContext=h,u=Ja(n),u.payload={element:s},p=p===void 0?null:p,p!==null&&(u.callback=p),s=Qa(t,u,n),s!==null&&(qn(s,t,n),gl(s,t,n))}function _S(t,n){if(t=t.memoizedState,t!==null&&t.dehydrated!==null){var s=t.retryLane;t.retryLane=s!==0&&s<n?s:n}}function xg(t,n){_S(t,n),(t=t.alternate)&&_S(t,n)}function AS(t){if(t.tag===13||t.tag===31){var n=Mi(t,67108864);n!==null&&qn(n,t,67108864),xg(t,67108864)}}function xS(t){if(t.tag===13||t.tag===31){var n=tr();n=Ze(n);var s=Mi(t,n);s!==null&&qn(s,t,n),xg(t,n)}}var Gu=!0;function Ix(t,n,s,u){var h=J.T;J.T=null;var p=he.p;try{he.p=2,Tg(t,n,s,u)}finally{he.p=p,J.T=h}}function Hx(t,n,s,u){var h=J.T;J.T=null;var p=he.p;try{he.p=8,Tg(t,n,s,u)}finally{he.p=p,J.T=h}}function Tg(t,n,s,u){if(Gu){var h=kg(u);if(h===null)hg(t,n,u,Vu,s),kS(t,u);else if(zx(h,t,n,s,u))u.stopPropagation();else if(kS(t,u),n&4&&-1<jx.indexOf(t)){for(;h!==null;){var p=Ha(h);if(p!==null)switch(p.tag){case 3:if(p=p.stateNode,p.current.memoizedState.isDehydrated){var C=we(p.pendingLanes);if(C!==0){var k=p;for(k.pendingLanes|=2,k.entangledLanes|=2;C;){var z=1<<31-nn(C);k.entanglements[1]|=z,C&=~z}Xr(p),(pt&6)===0&&(Tu=Ut()+500,Dl(0))}}break;case 31:case 13:k=Mi(p,2),k!==null&&qn(k,p,2),Ru(),xg(p,2)}if(p=kg(u),p===null&&hg(t,n,u,Vu,s),p===h)break;h=p}h!==null&&u.stopPropagation()}else hg(t,n,u,null,s)}}function kg(t){return t=vs(t),Rg(t)}var Vu=null;function Rg(t){if(Vu=null,t=Ia(t),t!==null){var n=l(t);if(n===null)t=null;else{var s=n.tag;if(s===13){if(t=c(n),t!==null)return t;t=null}else if(s===31){if(t=d(n),t!==null)return t;t=null}else if(s===3){if(n.stateNode.current.memoizedState.isDehydrated)return n.tag===3?n.stateNode.containerInfo:null;t=null}else n!==t&&(t=null)}}return Vu=t,null}function TS(t){switch(t){case"beforetoggle":case"cancel":case"click":case"close":case"contextmenu":case"copy":case"cut":case"auxclick":case"dblclick":case"dragend":case"dragstart":case"drop":case"focusin":case"focusout":case"input":case"invalid":case"keydown":case"keypress":case"keyup":case"mousedown":case"mouseup":case"paste":case"pause":case"play":case"pointercancel":case"pointerdown":case"pointerup":case"ratechange":case"reset":case"resize":case"seeked":case"submit":case"toggle":case"touchcancel":case"touchend":case"touchstart":case"volumechange":case"change":case"selectionchange":case"textInput":case"compositionstart":case"compositionend":case"compositionupdate":case"beforeblur":case"afterblur":case"beforeinput":case"blur":case"fullscreenchange":case"focus":case"hashchange":case"popstate":case"select":case"selectstart":return 2;case"drag":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"mousemove":case"mouseout":case"mouseover":case"pointermove":case"pointerout":case"pointerover":case"scroll":case"touchmove":case"wheel":case"mouseenter":case"mouseleave":case"pointerenter":case"pointerleave":return 8;case"message":switch(bt()){case ra:return 2;case Ln:return 8;case An:case xn:return 32;case Mn:return 268435456;default:return 32}default:return 32}}var Dg=!1,ci=null,ui=null,di=null,Hl=new Map,jl=new Map,fi=[],jx="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset".split(" ");function kS(t,n){switch(t){case"focusin":case"focusout":ci=null;break;case"dragenter":case"dragleave":ui=null;break;case"mouseover":case"mouseout":di=null;break;case"pointerover":case"pointerout":Hl.delete(n.pointerId);break;case"gotpointercapture":case"lostpointercapture":jl.delete(n.pointerId)}}function zl(t,n,s,u,h,p){return t===null||t.nativeEvent!==p?(t={blockedOn:n,domEventName:s,eventSystemFlags:u,nativeEvent:p,targetContainers:[h]},n!==null&&(n=Ha(n),n!==null&&AS(n)),t):(t.eventSystemFlags|=u,n=t.targetContainers,h!==null&&n.indexOf(h)===-1&&n.push(h),t)}function zx(t,n,s,u,h){switch(n){case"focusin":return ci=zl(ci,t,n,s,u,h),!0;case"dragenter":return ui=zl(ui,t,n,s,u,h),!0;case"mouseover":return di=zl(di,t,n,s,u,h),!0;case"pointerover":var p=h.pointerId;return Hl.set(p,zl(Hl.get(p)||null,t,n,s,u,h)),!0;case"gotpointercapture":return p=h.pointerId,jl.set(p,zl(jl.get(p)||null,t,n,s,u,h)),!0}return!1}function RS(t){var n=Ia(t.target);if(n!==null){var s=l(n);if(s!==null){if(n=s.tag,n===13){if(n=c(s),n!==null){t.blockedOn=n,Nn(t.priority,function(){xS(s)});return}}else if(n===31){if(n=d(s),n!==null){t.blockedOn=n,Nn(t.priority,function(){xS(s)});return}}else if(n===3&&s.stateNode.current.memoizedState.isDehydrated){t.blockedOn=s.tag===3?s.stateNode.containerInfo:null;return}}}t.blockedOn=null}function Fu(t){if(t.blockedOn!==null)return!1;for(var n=t.targetContainers;0<n.length;){var s=kg(t.nativeEvent);if(s===null){s=t.nativeEvent;var u=new s.constructor(s.type,s);qo=u,s.target.dispatchEvent(u),qo=null}else return n=Ha(s),n!==null&&AS(n),t.blockedOn=s,!1;n.shift()}return!0}function DS(t,n,s){Fu(t)&&s.delete(n)}function Ux(){Dg=!1,ci!==null&&Fu(ci)&&(ci=null),ui!==null&&Fu(ui)&&(ui=null),di!==null&&Fu(di)&&(di=null),Hl.forEach(DS),jl.forEach(DS)}function Ku(t,n){t.blockedOn===n&&(t.blockedOn=null,Dg||(Dg=!0,o.unstable_scheduleCallback(o.unstable_NormalPriority,Ux)))}var Wu=null;function OS(t){Wu!==t&&(Wu=t,o.unstable_scheduleCallback(o.unstable_NormalPriority,function(){Wu===t&&(Wu=null);for(var n=0;n<t.length;n+=3){var s=t[n],u=t[n+1],h=t[n+2];if(typeof u!="function"){if(Rg(u||s)===null)continue;break}var p=Ha(s);p!==null&&(t.splice(n,3),n-=3,kh(p,{pending:!0,data:h,method:s.method,action:u},u,h))}}))}function Js(t){function n(z){return Ku(z,t)}ci!==null&&Ku(ci,t),ui!==null&&Ku(ui,t),di!==null&&Ku(di,t),Hl.forEach(n),jl.forEach(n);for(var s=0;s<fi.length;s++){var u=fi[s];u.blockedOn===t&&(u.blockedOn=null)}for(;0<fi.length&&(s=fi[0],s.blockedOn===null);)RS(s),s.blockedOn===null&&fi.shift();if(s=(t.ownerDocument||t).$$reactFormReplay,s!=null)for(u=0;u<s.length;u+=3){var h=s[u],p=s[u+1],C=h[gn]||null;if(typeof p=="function")C||OS(s);else if(C){var k=null;if(p&&p.hasAttribute("formAction")){if(h=p,C=p[gn]||null)k=C.formAction;else if(Rg(h)!==null)continue}else k=C.action;typeof k=="function"?s[u+1]=k:(s.splice(u,3),u-=3),OS(s)}}}function LS(){function t(p){p.canIntercept&&p.info==="react-transition"&&p.intercept({handler:function(){return new Promise(function(C){return h=C})},focusReset:"manual",scroll:"manual"})}function n(){h!==null&&(h(),h=null),u||setTimeout(s,20)}function s(){if(!u&&!navigation.transition){var p=navigation.currentEntry;p&&p.url!=null&&navigation.navigate(p.url,{state:p.getState(),info:"react-transition",history:"replace"})}}if(typeof navigation=="object"){var u=!1,h=null;return navigation.addEventListener("navigate",t),navigation.addEventListener("navigatesuccess",n),navigation.addEventListener("navigateerror",n),setTimeout(s,100),function(){u=!0,navigation.removeEventListener("navigate",t),navigation.removeEventListener("navigatesuccess",n),navigation.removeEventListener("navigateerror",n),h!==null&&(h(),h=null)}}}function Og(t){this._internalRoot=t}Yu.prototype.render=Og.prototype.render=function(t){var n=this._internalRoot;if(n===null)throw Error(a(409));var s=n.current,u=tr();ES(s,u,t,n,null,null)},Yu.prototype.unmount=Og.prototype.unmount=function(){var t=this._internalRoot;if(t!==null){this._internalRoot=null;var n=t.containerInfo;ES(t.current,2,null,t,null,null),Ru(),n[In]=null}};function Yu(t){this._internalRoot=t}Yu.prototype.unstable_scheduleHydration=function(t){if(t){var n=Ot();t={blockedOn:null,target:t,priority:n};for(var s=0;s<fi.length&&n!==0&&n<fi[s].priority;s++);fi.splice(s,0,t),s===0&&RS(t)}};var MS=e.version;if(MS!=="19.2.0")throw Error(a(527,MS,"19.2.0"));he.findDOMNode=function(t){var n=t._reactInternals;if(n===void 0)throw typeof t.render=="function"?Error(a(188)):(t=Object.keys(t).join(","),Error(a(268,t)));return t=g(n),t=t!==null?m(t):null,t=t===null?null:t.stateNode,t};var Bx={bundleType:0,version:"19.2.0",rendererPackageName:"react-dom",currentDispatcherRef:J,reconcilerVersion:"19.2.0"};if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"){var Xu=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!Xu.isDisabled&&Xu.supportsFiber)try{$n=Xu.inject(Bx),Vt=Xu}catch{}}return Qs.createRoot=function(t,n){if(!i(t))throw Error(a(299));var s=!1,u="",h=Ub,p=Bb,C=Pb;return n!=null&&(n.unstable_strictMode===!0&&(s=!0),n.identifierPrefix!==void 0&&(u=n.identifierPrefix),n.onUncaughtError!==void 0&&(h=n.onUncaughtError),n.onCaughtError!==void 0&&(p=n.onCaughtError),n.onRecoverableError!==void 0&&(C=n.onRecoverableError)),n=wS(t,1,!1,null,null,s,u,null,h,p,C,LS),t[In]=n.current,fg(t),new Og(n)},Qs.hydrateRoot=function(t,n,s){if(!i(t))throw Error(a(299));var u=!1,h="",p=Ub,C=Bb,k=Pb,z=null;return s!=null&&(s.unstable_strictMode===!0&&(u=!0),s.identifierPrefix!==void 0&&(h=s.identifierPrefix),s.onUncaughtError!==void 0&&(p=s.onUncaughtError),s.onCaughtError!==void 0&&(C=s.onCaughtError),s.onRecoverableError!==void 0&&(k=s.onRecoverableError),s.formState!==void 0&&(z=s.formState)),n=wS(t,1,!0,n,s??null,u,h,z,p,C,k,LS),n.context=CS(null),s=n.current,u=tr(),u=Ze(u),h=Ja(u),h.callback=null,Qa(s,h,u),s=u,n.current.lanes=s,et(n,s),Xr(n),t[In]=n.current,fg(t),new Yu(n)},Qs.version="19.2.0",Qs}var Ug;function BS(){if(Ug)return Zu.exports;Ug=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(),Zu.exports=US(),Zu.exports}var rd=BS();const ad=Qr(rd),Zs=[],PS=50;let id=!1;function Bg(){try{return window.G7Core?.devTools}catch{return}}function Fi(o,e,r){const a=Bg();if(id&&a?.isEnabled?.()){a.trackLog?.(o,e,r);return}id||(o==="error"||o==="warn")&&Zs.length<PS&&Zs.push({level:o,prefix:e,args:r,timestamp:Date.now()})}function sd(){id=!0;const o=Bg();if(!o?.isEnabled?.()){Zs.length=0;return}for(const e of Zs)o.trackLog?.(e.level,e.prefix,e.args);Zs.length=0}const wi=class wi{constructor(){$(this,"debug");$(this,"prefix","[TemplateEngine]");this.debug=!1}static getInstance(){return wi.instance||(wi.instance=new wi),wi.instance}setDebug(e){this.debug=e}isDebugEnabled(){return this.debug}log(...e){this.debug&&console.log(this.prefix,...e),Fi("log","TemplateEngine",e)}error(...e){this.debug&&console.error(this.prefix,...e),Fi("error","TemplateEngine",e)}warn(...e){this.debug&&console.warn(this.prefix,...e),Fi("warn","TemplateEngine",e)}};$(wi,"instance");let eo=wi;function ut(o){const e=`[${o}]`,r=eo.getInstance();return{log:(...a)=>{r.isDebugEnabled()&&console.log(e,...a),Fi("log",o,a)},warn:(...a)=>{r.isDebugEnabled()&&console.warn(e,...a),Fi("warn",o,a)},error:(...a)=>{r.isDebugEnabled()&&console.error(e,...a),Fi("error",o,a)}}}eo.getInstance();const Ki=ut("networkResilience"),Pg=2,qg=300,Gg=2e3,qS=15e3;function od(o){return o?.name==="AbortError"}function Vg(o){return od(o)?!1:o instanceof TypeError}let Ul=!1,Fg=!1;function ld(){return Ul}function GS(){typeof window>"u"||Fg||(Fg=!0,window.addEventListener("pagehide",()=>{Ul=!0}),window.addEventListener("pageshow",o=>{o.persisted&&(Ul=!1)}),window.addEventListener("beforeunload",()=>{Ul=!0}))}function Kg(o,e,r){const a=Math.min(e*Math.pow(2,o),r),i=a*.25*(Math.random()*2-1);return Math.max(0,Math.round(a+i))}function Wg(o){return new Promise(e=>setTimeout(e,o))}async function Bl(o,e={}){const{retries:r=Pg,baseDelayMs:a=qg,maxDelayMs:i=Gg,timeoutMs:l=qS,label:c=o,init:d}=e,f=r+1;let g;for(let m=0;m<f;m++){let y=!1,w;const S=l>0?new AbortController:null;try{const v={...d};if(S){const x=d?.signal;x&&(x.aborted?S.abort():x.addEventListener("abort",()=>S.abort(),{once:!0})),v.signal=S.signal,w=setTimeout(()=>{y=!0,S.abort()},l)}return await fetch(o,v)}catch(v){if(g=v,od(v)&&!y||!(y||Vg(v)))throw v;if(ld())throw Ki.warn(`Document unloading, aborting retries: ${c}`),v;if(m===f-1)throw Ki.warn(`All ${f} attempts failed: ${c}`,v),v;const R=Kg(m,a,i);Ki.warn(`Network failure (attempt ${m+1}/${f}), retrying in ${R}ms: ${c}`),await Wg(R)}finally{w!==void 0&&clearTimeout(w)}}throw g}async function Yg(o,e={},r={}){const{retries:a=Pg,baseDelayMs:i=qg,maxDelayMs:l=Gg,label:c=o}=r,d=a+1;for(let f=0;f<d;f++)try{await VS(o,e);return}catch(g){if(e.id&&document.getElementById(e.id)?.remove(),ld())throw Ki.warn(`Document unloading, aborting script retries: ${c}`),g;if(f===d-1)throw Ki.warn(`All ${d} script attempts failed: ${c}`),g;const y=Kg(f,i,l);Ki.warn(`Script load failed (attempt ${f+1}/${d}), retrying in ${y}ms: ${c}`),await Wg(y)}}function VS(o,e){return new Promise((r,a)=>{const i=document.createElement("script");i.src=o,i.async=!1;for(const[l,c]of Object.entries(e))l==="id"?i.id=c:i.setAttribute(l,c);i.onload=()=>r(),i.onerror=()=>a(new Error(`Failed to load script: ${o}`)),document.head.appendChild(i)})}const Xg="extension",Wi="extensionless",FS="file";function KS(){const o=globalThis?.G7Config;if(globalThis?.__g7AssetUrlMode===Wi)return Wi;const r=o?.assetUrlMode;return r===Wi||r===Xg?r:o?.settings?.general?.asset_url_mode===Wi?Wi:Xg}function Jg(){return KS()===Wi}function Nr(o,e,r,a){const i=o.replace(/\/+$/,""),l=e.replace(/^\.+/,""),c=Jg()?i:`${i}.${l}`,d=[];return a&&d.push(a.replace(/^[?&]+/,"")),r!=null&&r!==""&&d.push(`v=${r}`),d.length>0?`${c}?${d.join("&")}`:c}function xa(o){if(!o||!Jg())return o;const e=globalThis?.location?.origin,r=e&&o.startsWith(e)?o.slice(e.length):o;if(!r.startsWith("/api/"))return o;const[a,i]=WS(r),l=a.match(/^\/api\/(modules|plugins)\/bundle\.(js|css)$/);if(l)return Qg(`/api/${l[1]}/bundle/${l[2]}`,i);const c=a.match(/^\/api\/(templates|modules|plugins)\/assets\/([^/]+)\/(.+)$/);if(c){const[,f,g,m]=c,y=`${FS}=${encodeURIComponent(decodeURIComponent(m))}`;return`/api/${f}/assets/${g}?${y}${i?`&${i}`:""}`}const d=a.match(/^(\/api\/.+)\.(json|js|css)$/);return d?Qg(d[1],i):o}function WS(o){const e=o.indexOf("?");return e===-1?[o,""]:[o.slice(0,e),o.slice(e+1)]}function Qg(o,e){return e?`${o}?${e}`:o}const Ir=ut("ComponentRegistry");class sn extends Error{constructor(e,r,a){super(e),this.code=r,this.details=a,this.name="ComponentRegistryError"}}const Kn=class Kn{constructor(){$(this,"registry",{});$(this,"manifest",null);$(this,"loadingState","idle");$(this,"error",null);$(this,"templateId",null);$(this,"templateType",null)}static getInstance(){return Kn.instance||(Kn.instance=new Kn),Kn.instance}static createIsolatedInstance(){return new Kn}static resetInstance(){Kn.instance=null}async loadComponents(e,r){if(this.loadingState==="loading")throw new sn("Components are already being loaded","LOADING_IN_PROGRESS");if(this.loadingState==="loaded"&&this.templateId===e&&this.templateType===r){Ir.log("Components already loaded for template:",e,r);return}this.loadingState="loading",this.templateId=e,this.templateType=r,this.error=null;try{await this.loadManifest(),await this.loadComponentBundle(),this.loadingState="loaded",Ir.log("Successfully loaded components:",Object.keys(this.registry).length)}catch(a){throw this.loadingState="error",this.error=a instanceof Error?a:new Error(String(a)),new sn(`Failed to load components: ${this.error.message}`,"LOAD_FAILED",{originalError:this.error})}}async loadManifest(){try{if(!this.templateId)throw new sn("Template ID not set","TEMPLATE_ID_NOT_SET");const e=`${this.templateId}:${this.templateType}`;if(Kn.manifestCache.has(e)){this.manifest=Kn.manifestCache.get(e),Ir.log("Manifest loaded from cache:",this.manifest.templateId);return}const r=Nr(`/api/templates/${this.templateId}/components`,"json"),a=await Bl(r,{label:"components.json"});if(!a.ok)throw new sn(`Failed to fetch manifest: ${a.status} ${a.statusText}`,"MANIFEST_FETCH_FAILED",{status:a.status,statusText:a.statusText});const i=await a.json();this.validateManifest(i),this.manifest=i,Kn.manifestCache.set(e,i),Ir.log("Manifest loaded and cached:",i.templateId)}catch(e){throw e instanceof sn?e:new sn("Failed to load component manifest","MANIFEST_LOAD_FAILED",{originalError:e})}}validateManifest(e){if(!e.version)throw new sn("Manifest missing required field: version","MANIFEST_INVALID",{field:"version"});if(!e.templateId)throw new sn("Manifest missing required field: templateId","MANIFEST_INVALID",{field:"templateId"});if(!e.components||typeof e.components!="object")throw new sn("Manifest missing required field: components","MANIFEST_INVALID",{field:"components"});const r=["basic","composite","layout"];for(const a of r){if(!Array.isArray(e.components[a]))throw new sn(`Manifest field 'components.${a}' must be an array`,"MANIFEST_INVALID",{field:`components.${a}`,value:e.components[a]});e.components[a].forEach((i,l)=>{if(!i.name||typeof i.name!="string")throw new sn(`Invalid component metadata at ${a}[${l}]: missing or invalid 'name'`,"MANIFEST_INVALID",{type:a,index:l,metadata:i});if(!i.type||typeof i.type!="string")throw new sn(`Invalid component metadata at ${a}[${l}]: missing or invalid 'type'`,"MANIFEST_INVALID",{type:a,index:l,metadata:i})})}Ir.log("Manifest validation passed")}async loadComponentBundle(){try{if(!this.templateId)throw new sn("Template ID not set","TEMPLATE_ID_NOT_SET");const e=this.getGlobalVariableName(),r=window[e];if(!r||typeof r!="object")throw new sn(`Component bundle not loaded. Expected global variable: ${e}. Ensure admin.blade.php includes the IIFE bundle script.`,"BUNDLE_NOT_LOADED",{expectedVariable:e});await this.registerComponentsFromManifest(r),Ir.log("Component bundle loaded from global variable:",e)}catch(e){throw e instanceof sn?e:new sn("Failed to load component bundle","BUNDLE_LOAD_FAILED",{originalError:e})}}getGlobalVariableName(){if(!this.templateId)throw new sn("Template ID not set","TEMPLATE_ID_NOT_SET");return this.templateId.split(/[-_]/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}async registerComponentsFromManifest(e){if(!this.manifest)throw new sn("Manifest not loaded","MANIFEST_NOT_LOADED");const r=["basic","composite","layout"];for(const a of r){const i=this.manifest.components[a]||[];for(const l of i){const c=l.name,d=e[c];if(!d){Ir.warn(`Component '${c}' not found in bundle`);continue}this.registerComponent(c,d,l)}}}registerComponent(e,r,a){this.registry[e]&&Ir.warn(`Component '${e}' already registered, overwriting`);const i=We.memo(r);this.registry[e]={component:i,metadata:a},Ir.log(`[ComponentRegistry] Registered component: ${e} (${a.type})`)}getComponent(e){const r=this.registry[e];return r?r.component:null}getMetadata(e){const r=this.registry[e];return r?r.metadata:null}hasComponent(e){return e in this.registry}getComponentsByType(e){return Object.entries(this.registry).filter(([r,a])=>a.metadata.type===e).map(([r,a])=>r)}getAllComponents(){return Object.keys(this.registry)}getComponentMap(){const e={};for(const[r,a]of Object.entries(this.registry))e[r]=a.component;return e}getLoadingState(){return this.loadingState}getError(){return this.error}getTemplateId(){return this.templateId}getManifest(){return this.manifest}clear(){this.registry={},this.manifest=null,this.loadingState="idle",this.error=null,this.templateId=null,this.templateType=null,Ir.log("Registry cleared")}};$(Kn,"instance",null),$(Kn,"manifestCache",new Map);let mr=Kn;const Zg=ut("TranslationEngine");class cd extends Error{constructor(e,r,a){super(e),this.key=r,this.locale=a,this.name="TranslationError"}}const On=class On{constructor(e={}){$(this,"cache",new Map);$(this,"translations",new Map);$(this,"options");$(this,"cacheVersion",0);this.options={defaultLocale:e.defaultLocale||"ko",fallbackLocale:e.fallbackLocale||"en",cacheTTL:e.cacheTTL||3e5}}static getInstance(e={}){return On.instance||(On.instance=new On(e)),On.instance}static resetInstance(){On.instance=null}setCacheVersion(e){this.cacheVersion!==e&&(Zg.log("Cache version updated:",this.cacheVersion,"->",e),this.cacheVersion=e,this.cache.clear())}getCacheVersion(){return this.cacheVersion}async loadTranslations(e,r,a="/api",i=!1){const l=`${e}:${r}`;if(!i){const c=this.getFromCache(l);if(c)return this.translations.set(l,c),c}try{const c=[];i&&c.push(`_=${Date.now()}`);const d=Nr(`${a}/templates/${e}/lang/${r}`,"json",this.cacheVersion>0?this.cacheVersion:null,c.length>0?c.join("&"):void 0),f=await fetch(d);if(!f.ok)throw new cd(`Failed to load translations: ${f.statusText}`,void 0,r);const m=await f.json();return this.saveToCache(l,m),this.translations.set(l,m),m}catch(c){throw new cd(`Failed to fetch translations for ${r}: ${c instanceof Error?c.message:String(c)}`,void 0,r)}}resolveTranslations(e,r,a){let i=e;const l=5;let c=0;for(;c<l&&i.includes("=$t:");){const d=i;if(i=i.replace(On.NESTED_TRANSLATION_PARAM_PATTERN,(f,g,m)=>"="+this.translate(m,r,void 0,a)),i===d)break;c++}return i.replace(On.TRANSLATION_PATTERN,(d,f,g)=>{const{cleanedParams:m,trailing:y}=this.separateTrailingText(g);return this.translate(f,r,m,a)+y})}translate(e,r,a,i){const l=this.getTranslation(e,r),c=a&&this.cleanParamsStr(a);if(c){const d=this.parseParams(c,i);return this.replaceParams(l,d)}return l}getTranslation(e,r){const{templateId:a,locale:i}=r,l=`${a}:${i}`,c=this.translations.get(l);if(c){const d=this.getNestedValue(c,e);if(d!==null)return d}if(i!==this.options.fallbackLocale){const d=`${a}:${this.options.fallbackLocale}`,f=this.translations.get(d);if(f){const g=this.getNestedValue(f,e);if(g!==null)return g}}return e}getNestedValue(e,r){const a=r.split(".");let i=e;for(const l of a){if(i==null||typeof i!="object")return null;i=i[l]}return typeof i=="string"?i:null}setTranslationValue(e,r,a,i){const l=`${e}:${r}`,c=this.translations.get(l)??{},d=a.split(".");let f=c;for(let g=0;g<d.length-1;g+=1){const m=d[g],y=f[m];(y==null||typeof y!="object"||Array.isArray(y))&&(f[m]={}),f=f[m]}f[d[d.length-1]]=i,this.translations.set(l,c)}parseParams(e,r){const a={},i=e.startsWith("|")?e.slice(1):e,l=[],d=i.replace(/\{\{(?:[^}]|\}(?!\}))*\}\}/g,f=>(l.push(f),`__PLACEHOLDER_${l.length-1}__`)).matchAll(On.PARAM_PATTERN);for(const f of d){const[,g,m]=f,y=m.replace(/__PLACEHOLDER_(\d+)__/g,(w,S)=>l[parseInt(S,10)]);a[g.trim()]=this.resolveParamValue(y.trim(),r)}return a}resolveParamValue(e,r){if(e.startsWith("{{")&&e.endsWith("}}")){const a=e.slice(2,-2).trim();if(/[|&()[\]!?:+\-*/%<>=\s]/.test(a)&&r)try{const l=pd.evaluateExpression(a,r);return String(l??"")}catch(l){return Zg.error("Expression evaluation failed:",a,l),""}else{const l=this.getNestedDataValue(r,a);return String(l??"")}}return e}separateTrailingText(e){if(!e)return{cleanedParams:void 0,trailing:""};if(e.trimEnd().endsWith("}}"))return{cleanedParams:e,trailing:""};if(!(e.startsWith("|")?e.slice(1):e).includes("="))return{cleanedParams:void 0,trailing:e};const a=e.match(/(\s+[^\p{L}\w=|&\s][^\p{L}\w=]*\s*)$/u);return a?{cleanedParams:e.slice(0,-a[1].length),trailing:a[1]}:{cleanedParams:e,trailing:""}}cleanParamsStr(e){return this.separateTrailingText(e).cleanedParams||""}getNestedDataValue(e,r){if(!e)return;const a=r.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}replaceParams(e,r){let a=e;for(const[i,l]of Object.entries(r)){const c=new RegExp(`\\{\\{${i}\\}\\}`,"g");a=a.replace(c,String(l));const d=new RegExp(`\\{${i}\\}`,"g");a=a.replace(d,String(l))}return a}getFromCache(e){const r=this.cache.get(e);return r?Date.now()-r.timestamp>this.options.cacheTTL?(this.cache.delete(e),null):r.data:null}saveToCache(e,r){this.cache.set(e,{data:r,timestamp:Date.now()})}clearCache(){this.cache.clear(),this.translations.clear()}pruneCache(){const e=Date.now(),r=[];for(const[a,i]of this.cache.entries())e-i.timestamp>this.options.cacheTTL&&r.push(a);for(const a of r)this.cache.delete(a)}getCacheStats(){const e=Date.now();let r=0;for(const a of this.cache.values())e-a.timestamp>this.options.cacheTTL&&r++;return{size:this.cache.size,expired:r}}};$(On,"instance",null),$(On,"TRANSLATION_PATTERN",/\$t:(?:defer:)?([a-zA-Z0-9._-]+)(\|(?:(?!\$t:).)+)?/g),$(On,"NESTED_TRANSLATION_PARAM_PATTERN",/=(\$t:([a-zA-Z0-9._-]+))(?=[|&\s]|$)/g),$(On,"PARAM_PATTERN",/([^=|&]+)=([^|&]+)/g);let gi=On;const ep=Object.freeze(Object.defineProperty({__proto__:null,TranslationEngine:gi,TranslationError:cd},Symbol.toStringTag,{value:"Module"})),Yi=ut("PipeRegistry");function tp(o,e){const r=o.getFullYear(),a=String(o.getMonth()+1).padStart(2,"0"),i=String(o.getDate()).padStart(2,"0"),l=String(o.getHours()).padStart(2,"0"),c=String(o.getMinutes()).padStart(2,"0"),d=String(o.getSeconds()).padStart(2,"0");return e.replace("YYYY",String(r)).replace("YY",String(r).slice(-2)).replace("MM",a).replace("M",String(o.getMonth()+1)).replace("DD",i).replace("D",String(o.getDate())).replace("HH",l).replace("H",String(o.getHours())).replace("mm",c).replace("m",String(o.getMinutes())).replace("ss",d).replace("s",String(o.getSeconds()))}function YS(o,e="ko"){const a=new Date().getTime()-o.getTime(),i=Math.floor(a/1e3),l=Math.floor(i/60),c=Math.floor(l/60),d=Math.floor(c/24),f=Math.floor(d/7),g=Math.floor(d/30),m=Math.floor(d/365);return e==="ko"?i<60?"방금 전":l<60?`${l}분 전`:c<24?`${c}시간 전`:d<7?`${d}일 전`:f<4?`${f}주 전`:g<12?`${g}개월 전`:`${m}년 전`:i<60?"just now":l<60?`${l} minute${l===1?"":"s"} ago`:c<24?`${c} hour${c===1?"":"s"} ago`:d<7?`${d} day${d===1?"":"s"} ago`:f<4?`${f} week${f===1?"":"s"} ago`:g<12?`${g} month${g===1?"":"s"} ago`:`${m} year${m===1?"":"s"} ago`}function ud(o){if(o==null)return null;if(o instanceof Date)return o;if(typeof o=="number")return new Date(o);if(typeof o=="string"){const e=new Date(o);return isNaN(e.getTime())?null:e}return null}const to={date:{fn:(o,e="YYYY-MM-DD")=>{const r=ud(o);return r?tp(r,e):""},description:"날짜 포맷 (기본: YYYY-MM-DD)"},datetime:{fn:(o,e="YYYY-MM-DD HH:mm")=>{const r=ud(o);return r?tp(r,e):""},description:"날짜+시간 포맷 (기본: YYYY-MM-DD HH:mm)"},relativeTime:{fn:(o,e="ko")=>{const r=ud(o);return r?YS(r,e):""},description:'상대 시간 표시 (예: "3분 전")'},number:{fn:(o,e)=>{const r=Number(o);if(isNaN(r))return String(o??"");const a={};return e!==void 0&&(a.minimumFractionDigits=e,a.maximumFractionDigits=e),r.toLocaleString(void 0,a)},description:"숫자 포맷 (천단위 구분, 선택적 소수점)"},truncate:{fn:(o,e=100,r="...")=>typeof o!="string"?String(o??""):o.length<=e?o:o.slice(0,e)+r,description:'문자열 자르기 (기본: 100자, 접미사: "...")'},uppercase:{fn:o=>typeof o!="string"?String(o??""):o.toUpperCase(),description:"대문자 변환"},lowercase:{fn:o=>typeof o!="string"?String(o??""):o.toLowerCase(),description:"소문자 변환"},stripHtml:{fn:o=>typeof o!="string"?String(o??""):o.replace(/<[^>]*>/g,""),description:"HTML 태그 제거"},default:{fn:(o,e="")=>o==null||o===""?e:o,description:"기본값 설정 (null, undefined, 빈문자열 시)"},fallback:{fn:(o,e)=>o??e,description:"폴백 값 설정 (null, undefined 시만)"},first:{fn:o=>{if(Array.isArray(o))return o[0]},description:"배열의 첫 번째 요소"},last:{fn:o=>{if(Array.isArray(o))return o[o.length-1]},description:"배열의 마지막 요소"},join:{fn:(o,e=", ")=>Array.isArray(o)?o.join(e):"",description:'배열을 문자열로 결합 (기본 구분자: ", ")'},length:{fn:o=>Array.isArray(o)||typeof o=="string"?o.length:0,description:"배열/문자열 길이"},filterBy:{fn:(o,e,r)=>Array.isArray(o)?Array.isArray(e)?o.filter(a=>{const i=r?a?.[r]:a;return e.includes(i)}):o:[],description:"배열 필터링 (allowList에 포함된 항목만 반환)"},keys:{fn:o=>o==null||typeof o!="object"?[]:Object.keys(o),description:"객체의 키 배열"},values:{fn:o=>o==null||typeof o!="object"?[]:Object.values(o),description:"객체의 값 배열"},json:{fn:(o,e)=>{try{return JSON.stringify(o,null,e)}catch{return""}},description:"JSON 문자열로 변환"},localized:{fn:(o,e)=>o==null?"":typeof o=="string"?o:typeof o!="object"?String(o):o[e||"ko"]||o.ko||o.en||Object.values(o)[0]||"",description:"다국어 객체에서 로케일에 맞는 값 추출"}},Xi=new Map,Ci=class Ci{static getInstance(){return Ci.instance||(Ci.instance=new Ci),Ci.instance}register(e,r,a){if(to[e]){Yi.warn(`[PipeRegistry] 내장 파이프를 덮어쓸 수 없습니다: ${e}`);return}Xi.set(e,{fn:r,description:a}),Yi.log(`[PipeRegistry] 커스텀 파이프 등록됨: ${e}`)}unregister(e){if(to[e])return Yi.warn(`[PipeRegistry] 내장 파이프는 해제할 수 없습니다: ${e}`),!1;const r=Xi.delete(e);return r&&Yi.log(`[PipeRegistry] 커스텀 파이프 해제됨: ${e}`),r}get(e){const r=to[e];return r?r.fn:Xi.get(e)?.fn}has(e){return!!to[e]||Xi.has(e)}execute(e,r,a=[]){const i=this.get(e);if(!i)return Yi.warn(`[PipeRegistry] 알 수 없는 파이프: ${e}`),r;try{return i(r,...a)}catch(l){return Yi.error(`[PipeRegistry] 파이프 실행 오류 (${e}):`,l),r}}list(){const e=[];for(const[r,a]of Object.entries(to))e.push({name:r,description:a.description,type:"built-in"});for(const[r,a]of Xi.entries())e.push({name:r,description:a.description,type:"custom"});return e.sort((r,a)=>r.name.localeCompare(a.name))}clearCustomPipes(){Xi.clear()}};$(Ci,"instance",null);let Pl=Ci;Pl.getInstance();function XS(o){const e=o.trim(),r=e.indexOf("(");if(r===-1)return{name:e,args:[]};const a=e.slice(0,r).trim(),i=e.slice(r+1,-1);if(!i.trim())return{name:a,args:[]};const l=[];let c="",d=null,f=0;for(let g=0;g<i.length;g++){const m=i[g];if((g>0?i[g-1]:"")==="\\"){c+=m;continue}if((m==='"'||m==="'")&&!d){d=m;continue}if(m===d){d=null;continue}if(d){c+=m;continue}if(m==="("||m==="["||m==="{"){f++,c+=m;continue}if(m===")"||m==="]"||m==="}"){f--,c+=m;continue}if(m===","&&f===0){l.push(np(c.trim())),c="";continue}c+=m}return c.trim()&&l.push(np(c.trim())),{name:a,args:l}}function np(o){if(o==="null")return null;if(o==="undefined")return;if(o==="true")return!0;if(o==="false")return!1;const e=Number(o);return!isNaN(e)&&o!==""?e:o}function JS(o){const e=[];let r="",a=null,i=0;for(let l=0;l<o.length;l++){const c=o[l],d=l>0?o[l-1]:"",f=l<o.length-1?o[l+1]:"";if(d==="\\"){r+=c;continue}if((c==='"'||c==="'")&&!a){a=c,r+=c;continue}if(c===a){a=null,r+=c;continue}if(a){r+=c;continue}if(c==="("||c==="["||c==="{"){i++,r+=c;continue}if(c===")"||c==="]"||c==="}"){i--,r+=c;continue}if(c==="|"&&f!=="|"&&d!=="|"&&i===0){e.push(r.trim()),r="";continue}r+=c}return r.trim()&&e.push(r.trim()),e.length===0?[o,[]]:[e[0],e.slice(1)]}function Gn(o){let e=null,r=0;for(let a=0;a<o.length;a++){const i=o[a],l=a>0?o[a-1]:"",c=a<o.length-1?o[a+1]:"";if(l!=="\\"){if((i==='"'||i==="'")&&!e){e=i;continue}if(i===e){e=null;continue}if(!e){if(i==="("||i==="["||i==="{"){r++;continue}if(i===")"||i==="]"||i==="}"){r--;continue}if(i==="|"&&c!=="|"&&l!=="|"&&r===0)return!0}}}return!1}function QS(o,e){const r=Pl.getInstance();let a=o;for(const i of e){const l=XS(i);a=r.execute(l.name,a,l.args)}return a}const dd=new Map([["true",!0],["false",!1],["null",null],["undefined",void 0]]);function Ta(o){if(typeof o!="string"||!o.startsWith("{{")||!o.endsWith("}}")||o.length<4)return null;const e=o.slice(2,-2);let r=null,a=0;for(let i=0;i<e.length;i++){const l=e[i];if(r!==null){if(l==="\\"){i++;continue}l===r&&(r=null);continue}if(l==='"'||l==="'"||l==="`"){r=l;continue}if(l==="{"){a++;continue}if(l==="}"){if(a>0){a--;continue}return null}}return a===0?e.trim():null}function ZS(o){const e=[];if(typeof o!="string")return e;for(let r=0;r<o.length-1;r++){if(o[r]!=="{"||o[r+1]!=="{")continue;let a=null,i=0,l=-1;for(let c=r+2;c<o.length;c++){const d=o[c];if(a!==null){if(d==="\\"){c++;continue}d===a&&(a=null);continue}if(d==='"'||d==="'"||d==="`"){a=d;continue}if(d==="{"){i++;continue}if(d==="}"){if(i>0){i--;continue}if(o[c+1]==="}"){l=c;break}break}}l!==-1&&(e.push({start:r,end:l+2,expr:o.slice(r+2,l)}),r=l+1)}return e}function no(o){return/[?:|&!+\-*/<>=()[\]{}]/.test(o)}function rp(o){const e=typeof o=="string"?o.trim():"";return e===""?"empty":dd.has(e)?"literal":Gn(e)?"pipe":no(e)?"expression":"path"}function fd(o,e,r,a){const i=typeof o=="string"?o.trim():"",l=rp(i),c=a?.skipCache?{skipCache:!0}:void 0;if(l==="empty"){a?.onEmpty?.(o);return}if(l==="literal")return dd.get(i);try{return l==="pipe"?r.evaluatePipeExpression(i,e,c,a?.trackingInfo):l==="expression"?r.evaluateExpression(i,e,a?.trackingInfo):r.resolve(i,e,c,a?.trackingInfo)}catch(d){if(a?.onError)return a.onError(d,i);throw d}}const nr="raw:";function ap(o){return o.startsWith(nr)?o.slice(nr.length):o}const ro="﷐",pi="﷑",ka="﷒";function ao(o){return ro+o+pi}function ql(o){return o.length>=2&&o.charCodeAt(0)===64976&&o.charCodeAt(o.length-1)===64977}function Gl(o){return o.includes(ro)}function hd(o){return o.slice(1,-1)}function Ji(o){if(typeof o=="string")return ao(o);if(Array.isArray(o))return o.map(Ji);if(o&&typeof o=="object"){const e={};for(const[r,a]of Object.entries(o))e[r]=Ji(a);return e}return o}function Ra(o){if(typeof o=="string")return ql(o)?hd(o):Gl(o)||o.includes(pi)?o.split(ro).join("").split(pi).join(""):o;if(Array.isArray(o)){let e=!1;const r=o.map(a=>{const i=Ra(a);return i!==a&&(e=!0),i});return e?r:o}if(o&&typeof o=="object"){if(o.$$typeof!==void 0)return o;let e=!1;const r={};for(const[a,i]of Object.entries(o)){const l=Ra(i);l!==i&&(e=!0),r[a]=l}return e?r:o}return o}const Da=ut("DataBindingEngine"),ew=/^[$A-Za-z_][$A-Za-z0-9_]*$/,tw=new Set(["break","case","catch","class","const","continue","debugger","default","delete","do","else","enum","export","extends","false","finally","for","function","if","implements","import","in","instanceof","interface","let","new","null","package","private","protected","public","return","static","super","switch","this","throw","true","try","typeof","var","void","while","with","yield","arguments","eval"]);function io(){try{return window.G7Core?.devTools}catch{return}}class gd extends Error{constructor(e,r,a){super(e),this.path=r,this.context=a,this.name="DataBindingError"}}const na=class na{constructor(e){$(this,"cache",new Map);$(this,"expressionFnCache",new Map);$(this,"renderCycleCache",new Map);$(this,"currentRenderCycleId",0);$(this,"defaultOptions",{defaultValue:void 0,nullSafe:!0,detectCircular:!0,maxDepth:10});e&&(this.defaultOptions={...this.defaultOptions,...e})}startRenderCycle(){this.currentRenderCycleId++,this.renderCycleCache.clear()}isRenderCycleCacheActive(){return this.currentRenderCycleId>0}getFromRenderCycleCache(e){if(this.isRenderCycleCacheActive())return this.renderCycleCache.get(e)}saveToRenderCycleCache(e,r){this.isRenderCycleCacheActive()&&this.renderCycleCache.set(e,r)}resolveBindings(e,r,a,i){const l={...this.defaultOptions,...a},c=io(),d=r._computed&&!r.$computed?{...r,$computed:r._computed}:r,f=(w,S)=>{const v=S.trim();let A=!1,x=v;if(v.startsWith(nr)&&(A=!0,x=v.slice(nr.length)),Gn(x))try{const B=this.evaluatePipeExpression(x,d,l,{...i,method:"resolveBindings",displayExpression:`{{${v}}}`}),K=this.formatValue(B);return A?ao(K):K}catch(B){return Da.error("Pipe expression evaluation failed:",x,B),w}if(no(x)){const B=c?.isEnabled()?performance.now():0;try{let K,fe=!1;if(l.skipCache)K=this.evaluateExpression(x,d);else{const ke=`expr:${x}`,Ue=this.getFromRenderCycleCache(ke);Ue!==void 0?(K=Ue,fe=!0):(K=this.evaluateExpression(x,d),this.saveToRenderCycleCache(ke,K))}if(c?.isEnabled()){const ke=performance.now()-B;c.trackExpressionEval({expression:`{{${v}}}`,result:this.sanitizeResultForTracking(K),resultType:this.getResultType(K),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:fe,duration:ke,method:"resolveBindings",skipCache:l.skipCache})}const Ce=this.formatValue(K);return A?ao(Ce):Ce}catch(K){return Da.error("Expression evaluation failed:",x,K),w}}if(l.skipCache){const B=c?.isEnabled()?performance.now():0,K=this.resolvePath(x,d,l);if(c?.isEnabled()){const Ce=performance.now()-B;c.trackExpressionEval({expression:`{{${v}}}`,result:this.sanitizeResultForTracking(K),resultType:this.getResultType(K),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!1,duration:Ce,method:"resolveBindings",skipCache:!0})}const fe=this.formatValue(K);return A?ao(fe):fe}const M=x.startsWith("_global")||x.startsWith("_local")||x.startsWith("_isolated")||x.startsWith("$parent"),O=c?.isEnabled()?performance.now():0;let L,q=!1;if(M){const B=this.getFromRenderCycleCache(x);B!==void 0?(L=B,q=!0):(L=this.resolvePath(x,d,l),this.saveToRenderCycleCache(x,L))}else{const B=this.getFromCache(x);B!==void 0?(L=B,q=!0):(L=this.resolvePath(x,d,l),this.saveToCache(x,L))}if(c?.isEnabled()){const B=performance.now()-O;c.trackExpressionEval({expression:`{{${v}}}`,result:this.sanitizeResultForTracking(L),resultType:this.getResultType(L),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:q,duration:B,method:"resolveBindings",skipCache:l.skipCache})}const G=this.formatValue(L);return A?ao(G):G},g=ZS(e);if(g.length===0)return e;let m="",y=0;for(const w of g)m+=e.slice(y,w.start),m+=f(e.slice(w.start,w.end),w.expr),y=w.end;return m+e.slice(y)}evaluatePipeExpression(e,r,a,i){const l={...this.defaultOptions,...a},c=io(),d=c?.isEnabled()?performance.now():0,f=r._computed&&!r.$computed?{...r,$computed:r._computed}:r,[g,m]=JS(e);let y,w=!1;const S=no(g),v=g.startsWith("_global")||g.startsWith("_local")||g.startsWith("_isolated")||g.startsWith("$parent");if(l.skipCache)S?y=this.evaluateExpression(g,f):y=this.resolvePath(g,f,l);else if(S){const x=this.getFromRenderCycleCache(`expr:${g}`);x!==void 0?(y=x,w=!0):(y=this.evaluateExpression(g,f),this.saveToRenderCycleCache(`expr:${g}`,y))}else if(v){const x=this.getFromRenderCycleCache(g);x!==void 0?(y=x,w=!0):(y=this.resolvePath(g,f,l),this.saveToRenderCycleCache(g,y))}else{const x=this.getFromCache(g);x!==void 0?(y=x,w=!0):(y=this.resolvePath(g,f,l),this.saveToCache(g,y))}const A=QS(y,m);if(c?.isEnabled()){const x=performance.now()-d;c.trackExpressionEval({expression:i?.displayExpression??`{{${e}}}`,result:this.sanitizeResultForTracking(A),resultType:this.getResultType(A),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:w,duration:x,method:i?.method??"evaluatePipeExpression",skipCache:l.skipCache})}return A}resolve(e,r,a,i){const l={...this.defaultOptions,...a},c=io(),d=c?.isEnabled()?performance.now():0;if(l.skipCache){const y=this.resolvePath(e,r,l);return c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(y),resultType:this.getResultType(y),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!1,duration:performance.now()-d,method:"resolve",skipCache:!0}),y}const f=e.startsWith("_global")||e.startsWith("_local")||e.startsWith("_isolated")||e.startsWith("$parent");let g=!1;if(f){const y=this.getFromRenderCycleCache(e);if(y!==void 0)return g=!0,c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(y),resultType:this.getResultType(y),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!0,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),y}else{const y=this.getFromCache(e);if(y!==void 0)return g=!0,c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(y),resultType:this.getResultType(y),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!0,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),y}const m=this.resolvePath(e,r,l);return f?this.saveToRenderCycleCache(e,m):this.saveToCache(e,m),c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(m),resultType:this.getResultType(m),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:g,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),m}resolvePath(e,r,a,i=0,l=new WeakSet){if(typeof e!="string"||e.trim()===""){Da.warn("resolvePath: 빈 경로가 전달되었습니다 — undefined 로 해석합니다.");return}if(a.maxDepth&&i>a.maxDepth){if(a.detectCircular)throw new gd(`Maximum depth exceeded (${a.maxDepth}). Possible circular reference.`,e,r);return a.defaultValue}const c=this.parsePath(e);let d=r;for(let f=0;f<c.length;f++){const g=c[f];if(d==null){if(a.nullSafe)return a.defaultValue;throw new gd(`Cannot read property '${g}' of ${d}`,e,r)}if(a.detectCircular&&typeof d=="object"){if(l.has(d))throw new gd(`Circular reference detected at path: ${c.slice(0,f+1).join(".")}`,e,r);l.add(d)}d=d[g],e==="_global.toasts"&&Da.log(`[DataBindingEngine] segment: ${g}, current:`,d)}return d!==void 0?d:a.defaultValue}parsePath(e){const r=[];let a;for(na.PATH_SEGMENT_PATTERN.lastIndex=0;(a=na.PATH_SEGMENT_PATTERN.exec(e))!==null;){const i=a[1]||a[2];i&&r.push(i)}return r}formatValue(e){if(e==null)return"";if(typeof e=="object")try{return JSON.stringify(e)}catch{return"[Object]"}return String(e)}getFromCache(e){const r=this.cache.get(e),a=io();if(!r){a?.isEnabled()&&a.recordCacheMiss();return}if(Date.now()-r.timestamp>na.CACHE_EXPIRY){this.cache.delete(e),a?.isEnabled()&&a.recordCacheMiss();return}return a?.isEnabled()&&a.recordCacheHit(),r.value}saveToCache(e,r){this.cache.set(e,{value:r,timestamp:Date.now()})}isActionDefinition(e){return typeof e.handler!="string"?!1:e.params!==void 0||Array.isArray(e.actions)||typeof e.target=="string"||e.onSuccess!==void 0||e.onError!==void 0}resolveObject(e,r,a){if(this.isSwitchExpression(e)){const d=e;return this.resolveSwitch(d,r,a)}if(this.isActionDefinition(e))return{...e};if("iteration"in e)return{...e};const i={},c=[...["cellChildren","expandChildren","expandContext","render"],...a?.skipBindingKeys||[]];for(const[d,f]of Object.entries(e)){if(c.includes(d)){i[d]=f;continue}try{this.resolveObjectEntry(i,d,f,r,a)}catch(g){Da.warn(`resolveObject: 값 해석 실패 (key: ${d}):`,g),i[d]=void 0}}return i}resolveObjectEntry(e,r,a,i,l){if(typeof a=="string"){const c=Ta(a);if(c!==null){const d=c.trim();let f=!1,g=d;d.startsWith(nr)&&(f=!0,g=d.slice(nr.length));let m;switch(rp(g)){case"empty":Da.warn(`resolveObject: 빈 바인딩 \`{{}}\` (key: ${r}) — undefined 로 해석합니다.`),m=void 0;break;case"literal":m=dd.get(g);break;case"pipe":m=this.evaluatePipeExpression(g,i,l);break;case"expression":m=this.evaluateExpression(g,i,l);break;default:m=this.resolve(g,i,l)}e[r]=f&&m!=null?Ji(m):m}else e[r]=this.resolveBindings(a,i,l)}else Array.isArray(a)?e[r]=a.map(c=>typeof c=="string"?this.resolveBindings(c,i,l):typeof c=="object"&&c!==null?this.resolveObject(c,i,l):c):a&&typeof a=="object"?e[r]=this.resolveObject(a,i,l):e[r]=a}clearCache(){this.cache.clear(),this.expressionFnCache.clear()}invalidateCacheByKeys(e){for(const r of this.cache.keys())for(const a of e)if(r===a||r.startsWith(`${a}.`)||r.startsWith(`${a}[`)){this.cache.delete(r);break}}pruneCache(){const e=Date.now();for(const[r,a]of this.cache.entries())e-a.timestamp>na.CACHE_EXPIRY&&this.cache.delete(r)}getCacheStats(){const e=Date.now();let r=0;for(const a of this.cache.values())e-a.timestamp>na.CACHE_EXPIRY&&r++;return{size:this.cache.size,expired:r}}evaluateExpression(e,r,a){const i=io(),l=i?.isEnabled()?performance.now():0;i?.isEnabled()&&i.trackBindingEval();try{let c=this.preprocessOptionalChaining(e);c=this.preprocessTranslationTokens(c);const d=this.extractVariablesFromExpression(c),f={...r};for(const L of d)L in f||(f[L]=void 0);r._computed&&!f.$computed&&(f.$computed=r._computed);let g=r.$templateId,m=r.$locale;if(typeof window<"u"&&(!g||!m)){const L=window.__templateApp?.getConfig?.();g??(g=L?.templateId),m??(m=L?.locale)}const y=m||"ko";f.$localized=(L,q)=>{if(L==null&&!q)return"";if(typeof L=="string")return L;if(L&&typeof L=="object"&&L[y])return L[y];if(q&&typeof q=="string"){const G=f.$t?f.$t(q):q;if(G&&G!==q)return G}return L&&typeof L=="object"?L.ko||L.en||Object.values(L)[0]||"":L==null?"":String(L)};const w=typeof window<"u"?window.G7Core:void 0;f.$uuid=()=>w?.uuid?w.uuid():typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,L=>{const q=Math.random()*16|0;return(L==="x"?q:q&3|8).toString(16)});const S={templateId:g||"",locale:y};f.$t=L=>{if(!L||typeof L!="string")return"";try{return gi.getInstance().translate(L,S)}catch(q){return Da.warn("$t() translation failed for key:",L,q),L}},f.$get=(L,q,G=void 0)=>{if(L==null)return G;const B=Array.isArray(q)?q:[q];if(B.length===0)return L;let K=L;for(const fe of B){if(K==null||fe==null)return G;K=K[fe]}return K??G};const v=[],A=[];for(const[L,q]of Object.entries(f))!ew.test(L)||tw.has(L)||(v.push(L),A.push(q));const x=`${c}|${v.join(",")}`;let R=this.expressionFnCache.get(x);const M=!!R;R||(R=new Function(...v,`return (${c});`),this.expressionFnCache.set(x,R));const O=R(...A);if(i?.isEnabled()){const L=performance.now()-l;i.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(O),resultType:this.getResultType(O),componentId:a?.componentId,componentName:a?.componentName,propName:a?.propName,fromCache:M,duration:L,method:"evaluateExpression",skipCache:a?.skipCache})}return O}catch(c){throw new Error(`Failed to evaluate expression "${e}": ${c instanceof Error?c.message:String(c)}`)}}getResultType(e){return e===null?"null":e===void 0?"undefined":Array.isArray(e)?"array":typeof e}sanitizeResultForTracking(e){if(e==null||typeof e!="object")return e;try{if(Array.isArray(e))return e.length>10?`[Array(${e.length})]`:e.slice(0,10);const r=Object.keys(e);return r.length>20?`{Object(${r.length} keys)}`:(JSON.stringify(e),e)}catch{return"[Complex Object]"}}preprocessTranslationTokens(e){return e=e.replace(/(['"])(\$t:([a-zA-Z_][a-zA-Z0-9_.\-]*))\1/g,(r,a,i,l)=>`$t('${l}')`),e.replace(/(?<!['"]\s*)(\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*)(?!\s*['"])/g,'"$1"')}preprocessOptionalChaining(e){const r=[];let a=e.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g,l=>(r.push(l),`__STRING_LITERAL_${r.length-1}__`));const i=[];return a=a.replace(/\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*(?:\|[^'"\s,)]+)?/g,l=>(i.push(l),`__TRANSLATION_TOKEN_${i.length-1}__`)),a=a.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\.(?!\?)/g,"$1?."),a=a.replace(/__TRANSLATION_TOKEN_(\d+)__/g,(l,c)=>i[parseInt(c,10)]),a=a.replace(/__STRING_LITERAL_(\d+)__/g,(l,c)=>r[parseInt(c,10)]),a}extractVariablesFromExpression(e){const r=new Set(["true","false","null","undefined","NaN","Infinity","if","else","for","while","do","switch","case","break","continue","return","function","class","const","let","var","new","delete","typeof","instanceof","this","super","import","export","default","try","catch","finally","throw","Math","Date","JSON","Array","Object","String","Number","Boolean","RegExp","Set","Map","WeakSet","WeakMap","Symbol","Promise","BigInt","Error","parseInt","parseFloat","isNaN","isFinite","encodeURI","decodeURI","encodeURIComponent","decodeURIComponent"]),a=/(?<![.\w$])([a-zA-Z_$][a-zA-Z0-9_$]*)/g,i=new Set;let l;for(;(l=a.exec(e))!==null;){const c=l[1];r.has(c)||i.add(c)}return Array.from(i)}isSwitchExpression(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"$switch"in e&&"$cases"in e}resolveSwitch(e,r,a){const i={...a,skipCache:!0};let l;try{l=this.resolveBindings(e.$switch,r,i)?.toString()?.trim()??""}catch(d){Da.warn("resolveSwitch: $switch 키 평가 실패:",e.$switch,d),l=""}let c;if(l&&e.$cases&&l in e.$cases)c=e.$cases[l];else if("$default"in e)c=e.$default;else return;return typeof c=="string"?c.includes("{{")?this.resolveBindings(c,r,i):c:c!==null&&typeof c=="object"?this.resolveObject(c,r,i):c}};$(na,"PATH_SEGMENT_PATTERN",/([^.[\]]+)|\[(\d+)\]/g),$(na,"CACHE_EXPIRY",3e4);let _n=na;const pd=new _n,Vl={mobile:{min:0,max:767},tablet:{min:768,max:1023},desktop:{min:1024,max:1/0},portable:{min:0,max:1023}},Fl=new Map;function nw(o,e){let r=null;return((...a)=>{r!==null&&clearTimeout(r),r=setTimeout(()=>{o(...a)},e)})}class rw{constructor(){$(this,"subscribers",new Set);$(this,"currentWidth",1024);$(this,"debouncedHandler",null);$(this,"isInitialized",!1);this.initialize()}initialize(){typeof window>"u"||(this.currentWidth=window.innerWidth,this.isInitialized=!0,this.debouncedHandler=nw(()=>{this.currentWidth=window.innerWidth,this.notifySubscribers()},150),window.addEventListener("resize",this.debouncedHandler))}notifySubscribers(){this.subscribers.forEach(e=>{e(this.currentWidth)})}subscribe(e){return this.subscribers.add(e),e(this.currentWidth),()=>{this.subscribers.delete(e)}}getWidth(){return this.currentWidth}parseRange(e){if(Fl.has(e))return Fl.get(e)??null;let r=null;if(Vl[e])r={...Vl[e]};else{const a=e.match(/^(-?\d*)-(-?\d*)$/);if(a){const[,i,l]=a,c=i===""?0:parseInt(i,10),d=l===""?1/0:parseInt(l,10);!isNaN(c)&&!isNaN(d)&&c<=d&&(r={min:c,max:d})}}return Fl.set(e,r),r}getMatchingKey(e,r){const a=[];for(const i of Object.keys(e)){const l=this.parseRange(i);l&&r>=l.min&&r<=l.max&&a.push({key:i,range:l,isPreset:!!Vl[i]})}return a.length===0?null:(a.sort((i,l)=>{if(i.isPreset!==l.isPreset)return i.isPreset?1:-1;const c=i.range.max-i.range.min,d=l.range.max-l.range.min;return c-d}),a[0].key)}matches(e){const r=this.parseRange(e);return r?this.currentWidth>=r.min&&this.currentWidth<=r.max:!1}clearSubscribers(){this.subscribers.clear()}destroy(){this.debouncedHandler&&typeof window<"u"&&window.removeEventListener("resize",this.debouncedHandler),this.clearSubscribers(),Fl.clear()}_setWidthForTesting(e){this.currentWidth=e,this.notifySubscribers()}}const mi=new rw,so=ut("ConditionEvaluator");function aw(o,e,r,a){return fd(ap(o),e,r,{skipCache:!0,onEmpty:()=>{so.warn(`${a}: 빈 바인딩 \`{{}}\` — undefined 로 해석합니다.`)}})}function ip(o,e,r){if(!o)return!0;try{const a=Ta(o);let i;if(a!==null?i=aw(a,e,r,"evaluateStringCondition"):i=r.resolveBindings(o,e,{skipCache:!0}),typeof i=="string"){const l=i.toLowerCase().trim();if(l==="false"||l==="0"||l===""||l==="null"||l==="undefined")return!1}return!!i}catch(a){return so.warn(`evaluateStringCondition: 조건 평가 실패: ${o}`,a),!1}}function Kl(o,e,r){if(typeof o=="string")return ip(o,e,r);if("and"in o){if(!Array.isArray(o.and)||o.and.length===0)return so.warn("evaluateConditionExpression: AND 그룹이 비어있습니다"),!0;for(const a of o.and)if(!Kl(a,e,r))return!1;return!0}if("or"in o){if(!Array.isArray(o.or)||o.or.length===0)return so.warn("evaluateConditionExpression: OR 그룹이 비어있습니다"),!1;for(const a of o.or)if(Kl(a,e,r))return!0;return!1}return so.warn("evaluateConditionExpression: 알 수 없는 조건 형식",o),!1}function sp(o,e,r){if(!Array.isArray(o)||o.length===0)return{matched:!1,branchIndex:-1};for(let a=0;a<o.length;a++){const i=o[a];if(i.if===void 0)return{matched:!0,branchIndex:a};if(Kl(i.if,e,r))return{matched:!0,branchIndex:a}}return{matched:!1,branchIndex:-1}}function iw(o){return typeof o=="string"?!0:o&&typeof o=="object"?"and"in o||"or"in o:!1}function sw(o){if(!Array.isArray(o)||o.length===0)return!1;const e=o[0];return!e||typeof e!="object"?!1:"if"in e||"then"in e}const rr=ut("RenderHelpers");function op(){try{return window.G7Core?.devTools}catch{return}}const lp=new _n,ow=no,cp=Ta;function md(o,e,r){let a=o;if(o.startsWith("{{")&&o.endsWith("}}")&&(a=o.slice(2,-2).trim()),Gn(a))try{return r.evaluatePipeExpression(a,e,{skipCache:!0})}catch(i){rr.warn(`resolveIterationSource: 파이프 표현식 평가 실패: ${o}`,i);return}if(ow(a))try{return r.evaluateExpression(a,e)}catch(i){rr.warn(`resolveIterationSource: 표현식 평가 실패: ${o}`,i);return}return r.resolve(a,e,{skipCache:!0})}function lw(o,e){const r={...o};if(e?.parentDataContext)for(const[a,i]of Object.entries(e.parentDataContext))a in r||(r[a]=i);if(e?.state){const a=r._local||{};r._local={...a,...e.state}}return e?.isolatedContext?.state&&(r._isolated=e.isolatedContext.state),r}function yd(o,e,r,a){if(!e||!Array.isArray(e)||e.length===0)return o;const i=a?.actionDispatcher??am(),l={...o,actions:e},c=i.bindActionsToProps(l,r,a?.componentContext);return delete c.actions,c}function up(o,e,r,a){if(typeof o=="boolean")return o;if(!o)return!0;try{const i=cp(o);let l;if(i!==null?l=fd(ap(i),e,r,{skipCache:!0,onEmpty:()=>{rr.warn(`evaluateIfCondition: 빈 바인딩 \`{{}}\` (컴포넌트: ${a||"unknown"}) — undefined 로 해석합니다.`)}}):l=r.resolveBindings(o,e,{skipCache:!0}),typeof l=="string"){const c=l.toLowerCase().trim();if(c==="false"||c==="0"||c===""||c==="null"||c==="undefined")return!1}return!!l}catch(i){return rr.warn(`evaluateIfCondition: 조건 평가 실패 (컴포넌트: ${a||"unknown"}):`,i),!1}}function Qi(o,e,r,a){if(o.if!==void 0)return up(o.if,e,r,a);if(o.condition!==void 0)return up(o.condition,e,r,a);if(o.conditions===void 0)return!0;const i=o.conditions;try{return iw(i)?Kl(i,e,r):sw(i)?sp(i,e,r).matched:(rr.warn(`evaluateRenderCondition: 알 수 없는 conditions 형식 (컴포넌트: ${a||"unknown"})`),!0)}catch(l){return rr.warn(`evaluateRenderCondition: conditions 평가 실패 (컴포넌트: ${a||"unknown"}):`,l),!1}}function dp(o){if(!o.responsive)return o;const e=mi.getWidth(),r=mi.getMatchingKey(o.responsive,e);if(!r)return o;const a=o.responsive[r];return{...o,props:{...o.props,...a.props},children:a.children??o.children,text:a.text??o.text,if:a.if??o.if,iteration:a.iteration??o.iteration}}function oo(o,e,r,a,i){if(!o||o.length===0)return[];const l=i?.bindingEngine??new _n,c=i?.translationEngine??gi.getInstance(),d=i?.translationContext??{templateId:"",locale:"ko"},f=lw(e,i?.componentContext),g=(S,v)=>{if(typeof S=="string")return ql(S)||Gl(S)||!/\$t:[a-zA-Z0-9._-]+/.test(S)?S:c.resolveTranslations(S,d,v);if(Array.isArray(S))return S.map(A=>g(A,v));if(S&&typeof S=="object"){const A={};for(const[x,R]of Object.entries(S))A[x]=g(R,v);return A}return S},m=(S,v)=>{if(typeof S=="string"){if(ql(S))return hd(S);if(Gl(S)){const x=[],R=S.replace(new RegExp(`${ro}([^${pi}]*)${pi}`,"g"),(O,L)=>(x.push(L),`${ka}${x.length-1}${ka}`));let M=R;return/\$t:[a-zA-Z0-9._-]+/.test(R)&&(M=c.resolveTranslations(R,d,v)),M.replace(new RegExp(`${ka}(\\d+)${ka}`,"g"),(O,L)=>x[parseInt(L)])}if(S.startsWith("$t:defer:")){const x="$t:"+S.slice(9);return c.resolveTranslations(x,d,v)}if(S.startsWith("$t:"))return c.resolveTranslations(S,d,v);if(/\$t:[a-zA-Z0-9._-]+/.test(S)){const x=S.trim();if(!(x.startsWith("{")&&x.endsWith("}")||x.startsWith("[")&&x.endsWith("]"))&&!S.includes("{{"))return c.resolveTranslations(S,d,v)}const A=cp(S);if(A!==null){let x=!1,R=A;A.startsWith(nr)&&(x=!0,R=A.slice(nr.length));let M=!1,O=fd(R,v,l,{skipCache:!0,onError:L=>{rr.warn("renderItemChildren: 표현식 평가 실패:",L),M=!0},onEmpty:()=>{rr.warn("renderItemChildren: 빈 바인딩 `{{}}` 은 해석하지 않습니다.")}});return M?void 0:(x||(O=g(O,v)),x&&O!=null?Ji(O):O)}if(S.includes("{{")){const x=l.resolveBindings(S,v,{skipCache:!0});return typeof x=="string"&&/\$t:[a-zA-Z0-9._-]+/.test(x)?c.resolveTranslations(x,d,v):x}return S}if(Array.isArray(S))return S.map(A=>m(A,v));if(S&&typeof S=="object"){if(l.isSwitchExpression(S))return l.resolveSwitch(S,v,{skipCache:!0});if(l.isActionDefinition(S))return{...S};const A={};for(const[x,R]of Object.entries(S))A[x]=m(R,v);return A}return S},y=(S,v)=>{if(!S)return{};const A={};for(const[x,R]of Object.entries(S))A[x]=m(R,v);return A},w=(S,v,A)=>{const x=dp(S),R=x.iteration;if(!R)return[];const M=md(R.source,A,l);if(!Array.isArray(M))return rr.warn(`renderItemChildren: iteration.source가 배열이 아닙니다: ${R.source}`),[];const O=op();if(O?.isEnabled()){const q=`${v}-iteration`;O.trackIteration(q,R.source,R.item_var,R.index_var,M.length)}const L=r[x.name];return L?M.flatMap((q,G)=>{const B={...A,[R.item_var]:q,[`${R.item_var}_index`]:G,...R.index_var?{[R.index_var]:G}:{}},K=Qi({if:x.if,condition:x.condition,conditions:x.conditions},B,l,x.id);if(x.if&&O?.isEnabled()){const wt=`${v}-iter-${G}-if`;O.trackIfCondition(wt,x.if,K,x.name)}if(!K)return[];const fe=x.id?String(Ra(m(x.id,B))):void 0,Ce=`${v}-iter-${G}`,ke=i?.getRemountKey?i.getRemountKey(fe,Ce):Ce,Ue=y(x.props,B),Pe=yd(Ue,x.actions,B,{actionDispatcher:i?.actionDispatcher,componentContext:i?.componentContext});let qe=null;return x.text!==void 0?qe=m(x.text,B):x.children&&x.children.length>0&&(qe=oo(x.children,B,r,ke,i)),O?.isEnabled()&&O.trackRender(x.name),[We.createElement(L,{key:ke,...Ra(Pe)},Ra(qe))]}):(rr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${x.name}`),[])};return o.flatMap((S,v)=>{const A=dp(S),x=A.id?String(Ra(m(A.id,f))):void 0,R=a?`${a}-${x||v}`:x||`child-${v}`,M=i?.getRemountKey?i.getRemountKey(x,R):R;if(A.iteration)return w(A,M,f);const O=Qi({if:A.if,condition:A.condition,conditions:A.conditions},f,l,A.id),L=op();if(A.if&&L?.isEnabled()){const fe=`${M}-if`;L.trackIfCondition(fe,A.if,O,A.name)}if(!O)return[];const q=r[A.name];if(!q)return rr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${A.name}`),[];const G=y(A.props,f),B=yd(G,A.actions,f,{actionDispatcher:i?.actionDispatcher,componentContext:i?.componentContext});let K=null;return A.text!==void 0?K=m(A.text,f):A.children&&A.children.length>0&&(K=oo(A.children,f,r,M,i)),L?.isEnabled()&&L.trackRender(A.name),[We.createElement(q,{key:M,...Ra(B)},Ra(K))]})}function Zi(o,e,r,a){let i,l;return r instanceof _n?(i=r,l=a):(i=lp,l=r),i.resolveBindings(o,e,l)}function cw(o,e,r,a){const i=r??lp,l={skipCache:!0,...a};let c;try{c=i.resolveBindings(o.key,e,{skipCache:l.skipCache})?.toString()?.trim()??""}catch(g){rr.warn("resolveClassMap: key 평가 실패:",o.key,g),c=""}let d="";c&&o.variants&&c in o.variants?d=o.variants[c]:o.default&&(d=o.default);const f=[];return o.base?.trim()&&f.push(o.base.trim()),d?.trim()&&f.push(d.trim()),f.join(" ")}function fp(o,e){return function(){return o.apply(e,arguments)}}const{toString:uw}=Object.prototype,{getPrototypeOf:bd}=Object,{iterator:Wl,toStringTag:hp}=Symbol,Yl=(o=>e=>{const r=uw.call(e);return o[r]||(o[r]=r.slice(8,-1).toLowerCase())})(Object.create(null)),yr=o=>(o=o.toLowerCase(),e=>Yl(e)===o),Xl=o=>e=>typeof e===o,{isArray:es}=Array,ts=Xl("undefined");function lo(o){return o!==null&&!ts(o)&&o.constructor!==null&&!ts(o.constructor)&&kn(o.constructor.isBuffer)&&o.constructor.isBuffer(o)}const gp=yr("ArrayBuffer");function dw(o){let e;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?e=ArrayBuffer.isView(o):e=o&&o.buffer&&gp(o.buffer),e}const fw=Xl("string"),kn=Xl("function"),pp=Xl("number"),co=o=>o!==null&&typeof o=="object",hw=o=>o===!0||o===!1,Jl=o=>{if(Yl(o)!=="object")return!1;const e=bd(o);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(hp in o)&&!(Wl in o)},gw=o=>{if(!co(o)||lo(o))return!1;try{return Object.keys(o).length===0&&Object.getPrototypeOf(o)===Object.prototype}catch{return!1}},pw=yr("Date"),mw=yr("File"),yw=yr("Blob"),bw=yr("FileList"),vw=o=>co(o)&&kn(o.pipe),Sw=o=>{let e;return o&&(typeof FormData=="function"&&o instanceof FormData||kn(o.append)&&((e=Yl(o))==="formdata"||e==="object"&&kn(o.toString)&&o.toString()==="[object FormData]"))},ww=yr("URLSearchParams"),[Cw,Ew,_w,Aw]=["ReadableStream","Request","Response","Headers"].map(yr),xw=o=>o.trim?o.trim():o.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function uo(o,e,{allOwnKeys:r=!1}={}){if(o===null||typeof o>"u")return;let a,i;if(typeof o!="object"&&(o=[o]),es(o))for(a=0,i=o.length;a<i;a++)e.call(null,o[a],a,o);else{if(lo(o))return;const l=r?Object.getOwnPropertyNames(o):Object.keys(o),c=l.length;let d;for(a=0;a<c;a++)d=l[a],e.call(null,o[d],d,o)}}function mp(o,e){if(lo(o))return null;e=e.toLowerCase();const r=Object.keys(o);let a=r.length,i;for(;a-- >0;)if(i=r[a],e===i.toLowerCase())return i;return null}const yi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,yp=o=>!ts(o)&&o!==yi;function vd(){const{caseless:o,skipUndefined:e}=yp(this)&&this||{},r={},a=(i,l)=>{const c=o&&mp(r,l)||l;Jl(r[c])&&Jl(i)?r[c]=vd(r[c],i):Jl(i)?r[c]=vd({},i):es(i)?r[c]=i.slice():(!e||!ts(i))&&(r[c]=i)};for(let i=0,l=arguments.length;i<l;i++)arguments[i]&&uo(arguments[i],a);return r}const Tw=(o,e,r,{allOwnKeys:a}={})=>(uo(e,(i,l)=>{r&&kn(i)?o[l]=fp(i,r):o[l]=i},{allOwnKeys:a}),o),kw=o=>(o.charCodeAt(0)===65279&&(o=o.slice(1)),o),Rw=(o,e,r,a)=>{o.prototype=Object.create(e.prototype,a),o.prototype.constructor=o,Object.defineProperty(o,"super",{value:e.prototype}),r&&Object.assign(o.prototype,r)},Dw=(o,e,r,a)=>{let i,l,c;const d={};if(e=e||{},o==null)return e;do{for(i=Object.getOwnPropertyNames(o),l=i.length;l-- >0;)c=i[l],(!a||a(c,o,e))&&!d[c]&&(e[c]=o[c],d[c]=!0);o=r!==!1&&bd(o)}while(o&&(!r||r(o,e))&&o!==Object.prototype);return e},Ow=(o,e,r)=>{o=String(o),(r===void 0||r>o.length)&&(r=o.length),r-=e.length;const a=o.indexOf(e,r);return a!==-1&&a===r},Lw=o=>{if(!o)return null;if(es(o))return o;let e=o.length;if(!pp(e))return null;const r=new Array(e);for(;e-- >0;)r[e]=o[e];return r},Mw=(o=>e=>o&&e instanceof o)(typeof Uint8Array<"u"&&bd(Uint8Array)),$w=(o,e)=>{const a=(o&&o[Wl]).call(o);let i;for(;(i=a.next())&&!i.done;){const l=i.value;e.call(o,l[0],l[1])}},Nw=(o,e)=>{let r;const a=[];for(;(r=o.exec(e))!==null;)a.push(r);return a},Iw=yr("HTMLFormElement"),Hw=o=>o.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(r,a,i){return a.toUpperCase()+i}),bp=(({hasOwnProperty:o})=>(e,r)=>o.call(e,r))(Object.prototype),jw=yr("RegExp"),vp=(o,e)=>{const r=Object.getOwnPropertyDescriptors(o),a={};uo(r,(i,l)=>{let c;(c=e(i,l,o))!==!1&&(a[l]=c||i)}),Object.defineProperties(o,a)},zw=o=>{vp(o,(e,r)=>{if(kn(o)&&["arguments","caller","callee"].indexOf(r)!==-1)return!1;const a=o[r];if(kn(a)){if(e.enumerable=!1,"writable"in e){e.writable=!1;return}e.set||(e.set=()=>{throw Error("Can not rewrite read-only method '"+r+"'")})}})},Uw=(o,e)=>{const r={},a=i=>{i.forEach(l=>{r[l]=!0})};return es(o)?a(o):a(String(o).split(e)),r},Bw=()=>{},Pw=(o,e)=>o!=null&&Number.isFinite(o=+o)?o:e;function qw(o){return!!(o&&kn(o.append)&&o[hp]==="FormData"&&o[Wl])}const Gw=o=>{const e=new Array(10),r=(a,i)=>{if(co(a)){if(e.indexOf(a)>=0)return;if(lo(a))return a;if(!("toJSON"in a)){e[i]=a;const l=es(a)?[]:{};return uo(a,(c,d)=>{const f=r(c,i+1);!ts(f)&&(l[d]=f)}),e[i]=void 0,l}}return a};return r(o,0)},Vw=yr("AsyncFunction"),Fw=o=>o&&(co(o)||kn(o))&&kn(o.then)&&kn(o.catch),Sp=((o,e)=>o?setImmediate:e?((r,a)=>(yi.addEventListener("message",({source:i,data:l})=>{i===yi&&l===r&&a.length&&a.shift()()},!1),i=>{a.push(i),yi.postMessage(r,"*")}))(`axios@${Math.random()}`,[]):r=>setTimeout(r))(typeof setImmediate=="function",kn(yi.postMessage)),Kw=typeof queueMicrotask<"u"?queueMicrotask.bind(yi):typeof process<"u"&&process.nextTick||Sp,ee={isArray:es,isArrayBuffer:gp,isBuffer:lo,isFormData:Sw,isArrayBufferView:dw,isString:fw,isNumber:pp,isBoolean:hw,isObject:co,isPlainObject:Jl,isEmptyObject:gw,isReadableStream:Cw,isRequest:Ew,isResponse:_w,isHeaders:Aw,isUndefined:ts,isDate:pw,isFile:mw,isBlob:yw,isRegExp:jw,isFunction:kn,isStream:vw,isURLSearchParams:ww,isTypedArray:Mw,isFileList:bw,forEach:uo,merge:vd,extend:Tw,trim:xw,stripBOM:kw,inherits:Rw,toFlatObject:Dw,kindOf:Yl,kindOfTest:yr,endsWith:Ow,toArray:Lw,forEachEntry:$w,matchAll:Nw,isHTMLForm:Iw,hasOwnProperty:bp,hasOwnProp:bp,reduceDescriptors:vp,freezeMethods:zw,toObjectSet:Uw,toCamelCase:Hw,noop:Bw,toFiniteNumber:Pw,findKey:mp,global:yi,isContextDefined:yp,isSpecCompliantForm:qw,toJSONObject:Gw,isAsyncFn:Vw,isThenable:Fw,setImmediate:Sp,asap:Kw,isIterable:o=>o!=null&&kn(o[Wl])};function Ye(o,e,r,a,i){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=o,this.name="AxiosError",e&&(this.code=e),r&&(this.config=r),a&&(this.request=a),i&&(this.response=i,this.status=i.status?i.status:null)}ee.inherits(Ye,Error,{toJSON:function(){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:ee.toJSONObject(this.config),code:this.code,status:this.status}}});const wp=Ye.prototype,Cp={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(o=>{Cp[o]={value:o}}),Object.defineProperties(Ye,Cp),Object.defineProperty(wp,"isAxiosError",{value:!0}),Ye.from=(o,e,r,a,i,l)=>{const c=Object.create(wp);ee.toFlatObject(o,c,function(m){return m!==Error.prototype},g=>g!=="isAxiosError");const d=o&&o.message?o.message:"Error",f=e==null&&o?o.code:e;return Ye.call(c,d,f,r,a,i),o&&c.cause==null&&Object.defineProperty(c,"cause",{value:o,configurable:!0}),c.name=o&&o.name||"Error",l&&Object.assign(c,l),c};const Ww=null;function Sd(o){return ee.isPlainObject(o)||ee.isArray(o)}function Ep(o){return ee.endsWith(o,"[]")?o.slice(0,-2):o}function _p(o,e,r){return o?o.concat(e).map(function(i,l){return i=Ep(i),!r&&l?"["+i+"]":i}).join(r?".":""):e}function Yw(o){return ee.isArray(o)&&!o.some(Sd)}const Xw=ee.toFlatObject(ee,{},null,function(e){return/^is[A-Z]/.test(e)});function Ql(o,e,r){if(!ee.isObject(o))throw new TypeError("target must be an object");e=e||new FormData,r=ee.toFlatObject(r,{metaTokens:!0,dots:!1,indexes:!1},!1,function(A,x){return!ee.isUndefined(x[A])});const a=r.metaTokens,i=r.visitor||m,l=r.dots,c=r.indexes,f=(r.Blob||typeof Blob<"u"&&Blob)&&ee.isSpecCompliantForm(e);if(!ee.isFunction(i))throw new TypeError("visitor must be a function");function g(v){if(v===null)return"";if(ee.isDate(v))return v.toISOString();if(ee.isBoolean(v))return v.toString();if(!f&&ee.isBlob(v))throw new Ye("Blob is not supported. Use a Buffer instead.");return ee.isArrayBuffer(v)||ee.isTypedArray(v)?f&&typeof Blob=="function"?new Blob([v]):Buffer.from(v):v}function m(v,A,x){let R=v;if(v&&!x&&typeof v=="object"){if(ee.endsWith(A,"{}"))A=a?A:A.slice(0,-2),v=JSON.stringify(v);else if(ee.isArray(v)&&Yw(v)||(ee.isFileList(v)||ee.endsWith(A,"[]"))&&(R=ee.toArray(v)))return A=Ep(A),R.forEach(function(O,L){!(ee.isUndefined(O)||O===null)&&e.append(c===!0?_p([A],L,l):c===null?A:A+"[]",g(O))}),!1}return Sd(v)?!0:(e.append(_p(x,A,l),g(v)),!1)}const y=[],w=Object.assign(Xw,{defaultVisitor:m,convertValue:g,isVisitable:Sd});function S(v,A){if(!ee.isUndefined(v)){if(y.indexOf(v)!==-1)throw Error("Circular reference detected in "+A.join("."));y.push(v),ee.forEach(v,function(R,M){(!(ee.isUndefined(R)||R===null)&&i.call(e,R,ee.isString(M)?M.trim():M,A,w))===!0&&S(R,A?A.concat(M):[M])}),y.pop()}}if(!ee.isObject(o))throw new TypeError("data must be an object");return S(o),e}function Ap(o){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(o).replace(/[!'()~]|%20|%00/g,function(a){return e[a]})}function wd(o,e){this._pairs=[],o&&Ql(o,this,e)}const xp=wd.prototype;xp.append=function(e,r){this._pairs.push([e,r])},xp.toString=function(e){const r=e?function(a){return e.call(this,a,Ap)}:Ap;return this._pairs.map(function(i){return r(i[0])+"="+r(i[1])},"").join("&")};function Jw(o){return encodeURIComponent(o).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function Tp(o,e,r){if(!e)return o;const a=r&&r.encode||Jw;ee.isFunction(r)&&(r={serialize:r});const i=r&&r.serialize;let l;if(i?l=i(e,r):l=ee.isURLSearchParams(e)?e.toString():new wd(e,r).toString(a),l){const c=o.indexOf("#");c!==-1&&(o=o.slice(0,c)),o+=(o.indexOf("?")===-1?"?":"&")+l}return o}class kp{constructor(){this.handlers=[]}use(e,r,a){return this.handlers.push({fulfilled:e,rejected:r,synchronous:a?a.synchronous:!1,runWhen:a?a.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){ee.forEach(this.handlers,function(a){a!==null&&e(a)})}}const Rp={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},Qw={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<"u"?URLSearchParams:wd,FormData:typeof FormData<"u"?FormData:null,Blob:typeof Blob<"u"?Blob:null},protocols:["http","https","file","blob","url","data"]},Cd=typeof window<"u"&&typeof document<"u",Ed=typeof navigator=="object"&&navigator||void 0,Zw=Cd&&(!Ed||["ReactNative","NativeScript","NS"].indexOf(Ed.product)<0),e0=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",t0=Cd&&window.location.href||"http://localhost",Sn={...Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Cd,hasStandardBrowserEnv:Zw,hasStandardBrowserWebWorkerEnv:e0,navigator:Ed,origin:t0},Symbol.toStringTag,{value:"Module"})),...Qw};function n0(o,e){return Ql(o,new Sn.classes.URLSearchParams,{visitor:function(r,a,i,l){return Sn.isNode&&ee.isBuffer(r)?(this.append(a,r.toString("base64")),!1):l.defaultVisitor.apply(this,arguments)},...e})}function r0(o){return ee.matchAll(/\w+|\[(\w*)]/g,o).map(e=>e[0]==="[]"?"":e[1]||e[0])}function a0(o){const e={},r=Object.keys(o);let a;const i=r.length;let l;for(a=0;a<i;a++)l=r[a],e[l]=o[l];return e}function Dp(o){function e(r,a,i,l){let c=r[l++];if(c==="__proto__")return!0;const d=Number.isFinite(+c),f=l>=r.length;return c=!c&&ee.isArray(i)?i.length:c,f?(ee.hasOwnProp(i,c)?i[c]=[i[c],a]:i[c]=a,!d):((!i[c]||!ee.isObject(i[c]))&&(i[c]=[]),e(r,a,i[c],l)&&ee.isArray(i[c])&&(i[c]=a0(i[c])),!d)}if(ee.isFormData(o)&&ee.isFunction(o.entries)){const r={};return ee.forEachEntry(o,(a,i)=>{e(r0(a),i,r,0)}),r}return null}function i0(o,e,r){if(ee.isString(o))try{return(e||JSON.parse)(o),ee.trim(o)}catch(a){if(a.name!=="SyntaxError")throw a}return(r||JSON.stringify)(o)}const fo={transitional:Rp,adapter:["xhr","http","fetch"],transformRequest:[function(e,r){const a=r.getContentType()||"",i=a.indexOf("application/json")>-1,l=ee.isObject(e);if(l&&ee.isHTMLForm(e)&&(e=new FormData(e)),ee.isFormData(e))return i?JSON.stringify(Dp(e)):e;if(ee.isArrayBuffer(e)||ee.isBuffer(e)||ee.isStream(e)||ee.isFile(e)||ee.isBlob(e)||ee.isReadableStream(e))return e;if(ee.isArrayBufferView(e))return e.buffer;if(ee.isURLSearchParams(e))return r.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let d;if(l){if(a.indexOf("application/x-www-form-urlencoded")>-1)return n0(e,this.formSerializer).toString();if((d=ee.isFileList(e))||a.indexOf("multipart/form-data")>-1){const f=this.env&&this.env.FormData;return Ql(d?{"files[]":e}:e,f&&new f,this.formSerializer)}}return l||i?(r.setContentType("application/json",!1),i0(e)):e}],transformResponse:[function(e){const r=this.transitional||fo.transitional,a=r&&r.forcedJSONParsing,i=this.responseType==="json";if(ee.isResponse(e)||ee.isReadableStream(e))return e;if(e&&ee.isString(e)&&(a&&!this.responseType||i)){const c=!(r&&r.silentJSONParsing)&&i;try{return JSON.parse(e,this.parseReviver)}catch(d){if(c)throw d.name==="SyntaxError"?Ye.from(d,Ye.ERR_BAD_RESPONSE,this,null,this.response):d}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:Sn.classes.FormData,Blob:Sn.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};ee.forEach(["delete","get","head","post","put","patch"],o=>{fo.headers[o]={}});const s0=ee.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),o0=o=>{const e={};let r,a,i;return o&&o.split(` -`).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)),a||a!==!1&&p0(e.url))){const f=i&&l&&m0.read(l);f&&c.set(i,f)}return e},v0=typeof XMLHttpRequest<"u"&&function(o){return new Promise(function(r,a){const i=jp(o);let l=i.data;const c=Rn.from(i.headers).normalize();let{responseType:d,onUploadProgress:f,onDownloadProgress:g}=i,m,y,w,S,v;function A(){S&&S(),v&&v(),i.cancelToken&&i.cancelToken.unsubscribe(m),i.signal&&i.signal.removeEventListener("abort",m)}let x=new XMLHttpRequest;x.open(i.method.toUpperCase(),i.url,!0),x.timeout=i.timeout;function R(){if(!x)return;const O=Rn.from("getAllResponseHeaders"in x&&x.getAllResponseHeaders()),q={data:!d||d==="text"||d==="json"?x.responseText:x.response,status:x.status,statusText:x.statusText,headers:O,config:o,request:x};Mp(function(B){r(B),A()},function(B){a(B),A()},q),x=null}"onloadend"in x?x.onloadend=R:x.onreadystatechange=function(){!x||x.readyState!==4||x.status===0&&!(x.responseURL&&x.responseURL.indexOf("file:")===0)||setTimeout(R)},x.onabort=function(){x&&(a(new Ye("Request aborted",Ye.ECONNABORTED,o,x)),x=null)},x.onerror=function(L){const q=L&&L.message?L.message:"Network Error",G=new Ye(q,Ye.ERR_NETWORK,o,x);G.event=L||null,a(G),x=null},x.ontimeout=function(){let L=i.timeout?"timeout of "+i.timeout+"ms exceeded":"timeout exceeded";const q=i.transitional||Rp;i.timeoutErrorMessage&&(L=i.timeoutErrorMessage),a(new Ye(L,q.clarifyTimeoutError?Ye.ETIMEDOUT:Ye.ECONNABORTED,o,x)),x=null},l===void 0&&c.setContentType(null),"setRequestHeader"in x&&ee.forEach(c.toJSON(),function(L,q){x.setRequestHeader(q,L)}),ee.isUndefined(i.withCredentials)||(x.withCredentials=!!i.withCredentials),d&&d!=="json"&&(x.responseType=i.responseType),g&&([w,v]=ec(g,!0),x.addEventListener("progress",w)),f&&x.upload&&([y,S]=ec(f),x.upload.addEventListener("progress",y),x.upload.addEventListener("loadend",S)),(i.cancelToken||i.signal)&&(m=O=>{x&&(a(!O||O.type?new ns(null,o,x):O),x.abort(),x=null)},i.cancelToken&&i.cancelToken.subscribe(m),i.signal&&(i.signal.aborted?m():i.signal.addEventListener("abort",m)));const M=f0(i.url);if(M&&Sn.protocols.indexOf(M)===-1){a(new Ye("Unsupported protocol "+M+":",Ye.ERR_BAD_REQUEST,o));return}x.send(l||null)})},S0=(o,e)=>{const{length:r}=o=o?o.filter(Boolean):[];if(e||r){let a=new AbortController,i;const l=function(g){if(!i){i=!0,d();const m=g instanceof Error?g:this.reason;a.abort(m instanceof Ye?m:new ns(m instanceof Error?m.message:m))}};let c=e&&setTimeout(()=>{c=null,l(new Ye(`timeout ${e} of ms exceeded`,Ye.ETIMEDOUT))},e);const d=()=>{o&&(c&&clearTimeout(c),c=null,o.forEach(g=>{g.unsubscribe?g.unsubscribe(l):g.removeEventListener("abort",l)}),o=null)};o.forEach(g=>g.addEventListener("abort",l));const{signal:f}=a;return f.unsubscribe=()=>ee.asap(d),f}},w0=function*(o,e){let r=o.byteLength;if(r<e){yield o;return}let a=0,i;for(;a<r;)i=a+e,yield o.slice(a,i),a=i},C0=async function*(o,e){for await(const r of E0(o))yield*w0(r,e)},E0=async function*(o){if(o[Symbol.asyncIterator]){yield*o;return}const e=o.getReader();try{for(;;){const{done:r,value:a}=await e.read();if(r)break;yield a}}finally{await e.cancel()}},zp=(o,e,r,a)=>{const i=C0(o,e);let l=0,c,d=f=>{c||(c=!0,a&&a(f))};return new ReadableStream({async pull(f){try{const{done:g,value:m}=await i.next();if(g){d(),f.close();return}let y=m.byteLength;if(r){let w=l+=y;r(w)}f.enqueue(new Uint8Array(m))}catch(g){throw d(g),g}},cancel(f){return d(f),i.return()}},{highWaterMark:2})},Up=64*1024,{isFunction:tc}=ee,_0=(({Request:o,Response:e})=>({Request:o,Response:e}))(ee.global),{ReadableStream:Bp,TextEncoder:Pp}=ee.global,qp=(o,...e)=>{try{return!!o(...e)}catch{return!1}},A0=o=>{o=ee.merge.call({skipUndefined:!0},_0,o);const{fetch:e,Request:r,Response:a}=o,i=e?tc(e):typeof fetch=="function",l=tc(r),c=tc(a);if(!i)return!1;const d=i&&tc(Bp),f=i&&(typeof Pp=="function"?(v=>A=>v.encode(A))(new Pp):async v=>new Uint8Array(await new r(v).arrayBuffer())),g=l&&d&&qp(()=>{let v=!1;const A=new r(Sn.origin,{body:new Bp,method:"POST",get duplex(){return v=!0,"half"}}).headers.has("Content-Type");return v&&!A}),m=c&&d&&qp(()=>ee.isReadableStream(new a("").body)),y={stream:m&&(v=>v.body)};i&&["text","arrayBuffer","blob","formData","stream"].forEach(v=>{!y[v]&&(y[v]=(A,x)=>{let R=A&&A[v];if(R)return R.call(A);throw new Ye(`Response type '${v}' is not supported`,Ye.ERR_NOT_SUPPORT,x)})});const w=async v=>{if(v==null)return 0;if(ee.isBlob(v))return v.size;if(ee.isSpecCompliantForm(v))return(await new r(Sn.origin,{method:"POST",body:v}).arrayBuffer()).byteLength;if(ee.isArrayBufferView(v)||ee.isArrayBuffer(v))return v.byteLength;if(ee.isURLSearchParams(v)&&(v=v+""),ee.isString(v))return(await f(v)).byteLength},S=async(v,A)=>{const x=ee.toFiniteNumber(v.getContentLength());return x??w(A)};return async v=>{let{url:A,method:x,data:R,signal:M,cancelToken:O,timeout:L,onDownloadProgress:q,onUploadProgress:G,responseType:B,headers:K,withCredentials:fe="same-origin",fetchOptions:Ce}=jp(v),ke=e||fetch;B=B?(B+"").toLowerCase():"text";let Ue=S0([M,O&&O.toAbortSignal()],L),Pe=null;const qe=Ue&&Ue.unsubscribe&&(()=>{Ue.unsubscribe()});let wt;try{if(G&&g&&x!=="get"&&x!=="head"&&(wt=await S(K,R))!==0){let H=new r(A,{method:"POST",body:R,duplex:"half"}),T;if(ee.isFormData(R)&&(T=H.headers.get("content-type"))&&K.setContentType(T),H.body){const[ge,de]=$p(wt,ec(Np(G)));R=zp(H.body,Up,ge,de)}}ee.isString(fe)||(fe=fe?"include":"omit");const J=l&&"credentials"in r.prototype,he={...Ce,signal:Ue,method:x.toUpperCase(),headers:K.normalize().toJSON(),body:R,duplex:"half",credentials:J?fe:void 0};Pe=l&&new r(A,he);let Ee=await(l?ke(Pe,Ce):ke(A,he));const Z=m&&(B==="stream"||B==="response");if(m&&(q||Z&&qe)){const H={};["status","statusText","headers"].forEach(me=>{H[me]=Ee[me]});const T=ee.toFiniteNumber(Ee.headers.get("content-length")),[ge,de]=q&&$p(T,ec(Np(q),!0))||[];Ee=new a(zp(Ee.body,Up,ge,()=>{de&&de(),qe&&qe()}),H)}B=B||"text";let pe=await y[ee.findKey(y,B)||"text"](Ee,v);return!Z&&qe&&qe(),await new Promise((H,T)=>{Mp(H,T,{data:pe,headers:Rn.from(Ee.headers),status:Ee.status,statusText:Ee.statusText,config:v,request:Pe})})}catch(J){throw qe&&qe(),J&&J.name==="TypeError"&&/Load failed|fetch/i.test(J.message)?Object.assign(new Ye("Network Error",Ye.ERR_NETWORK,v,Pe),{cause:J.cause||J}):Ye.from(J,J&&J.code,v,Pe)}}},x0=new Map,Gp=o=>{let e=o?o.env:{};const{fetch:r,Request:a,Response:i}=e,l=[a,i,r];let c=l.length,d=c,f,g,m=x0;for(;d--;)f=l[d],g=m.get(f),g===void 0&&m.set(f,g=d?new Map:A0(e)),m=g;return g};Gp();const xd={http:Ww,xhr:v0,fetch:{get:Gp}};ee.forEach(xd,(o,e)=>{if(o){try{Object.defineProperty(o,"name",{value:e})}catch{}Object.defineProperty(o,"adapterName",{value:e})}});const Vp=o=>`- ${o}`,T0=o=>ee.isFunction(o)||o===null||o===!1,Fp={getAdapter:(o,e)=>{o=ee.isArray(o)?o:[o];const{length:r}=o;let a,i;const l={};for(let c=0;c<r;c++){a=o[c];let d;if(i=a,!T0(a)&&(i=xd[(d=String(a)).toLowerCase()],i===void 0))throw new Ye(`Unknown adapter '${d}'`);if(i&&(ee.isFunction(i)||(i=i.get(e))))break;l[d||"#"+c]=i}if(!i){const c=Object.entries(l).map(([f,g])=>`adapter ${f} `+(g===!1?"is not supported by the environment":"is not available in the build"));let d=r?c.length>1?`since : -`+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;const{Axios:Jx,AxiosError:Qx,CanceledError:Zx,isCancel:eT,CancelToken:tT,VERSION:nT,all:rT,Cancel:aT,isAxiosError:iT,spread:sT,toFormData:oT,AxiosHeaders:lT,HttpStatusCode:cT,formToJSON:uT,getAdapter:dT,mergeConfig:fT}=Gt,Rd="g7.identity.redirectStash",ac=ut("IdentityGuardInterceptor");class mt{static setLauncher(e){this.launcher=e}static hasLauncher(){return this.launcher!==null}static markDomainNoticeShown(){this.domainNoticeShown=!0}static consumeDomainNoticeShown(){const e=this.domainNoticeShown;return this.domainNoticeShown=!1,e}static resolveDeferred(e){const r=this.deferredResolver;if(!r){ac.warn("IdentityGuardInterceptor.resolveDeferred 호출됐지만 대기 중인 launcher 가 없습니다. 모달이 launcher 외부에서 열렸는지 확인하세요.");return}this.deferredResolver=null,r(e)}static createDeferred(){if(this.deferredResolver){const e=this.deferredResolver;this.deferredResolver=null,e({status:"cancelled"})}return new Promise(e=>{this.deferredResolver=e})}static isIdentityRequired(e,r){return e===428&&typeof r=="object"&&r!==null&&r.error_code==="identity_verification_required"}static async handle(e,r,a){this.domainNoticeShown=!1;const i=this.launcher??M0,c=!!(a&&(a.email||a.phone))?{...e.verification,target:a}:e.verification,d=await i(c);if(d.status!=="verified")return null;const f=c.return_request;if(!f)return null;const g=L0(f.url,d.token);return fetch(g,{method:f.method,headers:r?.headers??{Accept:"application/json","Content-Type":"application/json"},body:r?.body,credentials:r?.credentials??"same-origin"})}static redirectExternally(e){const r=e.redirect_url;if(!r)return ac.error("redirectExternally 호출 시 verification.redirect_url 이 없습니다. payload:",e),Promise.resolve({status:"failed",failureCode:"MISSING_REDIRECT_URL",reason:"verification.redirect_url is required for external_redirect flow"});if(typeof window<"u"){const a={return_url:window.location.href,payload:e,stashed_at:Date.now()};try{window.sessionStorage?.setItem(Rd,JSON.stringify(a))}catch(i){ac.warn("sessionStorage 접근 실패 — stash 없이 redirect 합니다.",i)}window.location.href=r}return new Promise(()=>{})}static reset(){if(this.deferredResolver){const e=this.deferredResolver;this.deferredResolver=null;try{e({status:"cancelled"})}catch{}}this.launcher=null,this.domainNoticeShown=!1}}$(mt,"launcher",null),$(mt,"deferredResolver",null),$(mt,"domainNoticeShown",!1);function L0(o,e){if(!e)return o;try{const r=typeof window<"u"?window.location.origin:"http://localhost",a=new URL(o,r);return a.searchParams.set("verification_token",e),/^https?:\/\//i.test(o)?a.toString():`${a.pathname}${a.search}${a.hash}`}catch{const r=o.includes("?")?"&":"?";return`${o}${r}verification_token=${encodeURIComponent(e)}`}}const M0=async o=>{if(typeof window>"u")return{status:"cancelled"};if(o.render_hint==="external_redirect"||o.redirect_url)return mt.redirectExternally(o);const e=window.G7Core;if(!e?.dispatch)return console.error("[IdentityGuardInterceptor] defaultLauncher: G7Core 가 초기화되지 않아 본인인증 흐름을 시작할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};try{await e.dispatch({handler:"toast",params:{message:"본인 확인이 필요합니다.",variant:"warning"}})}catch{}const r=window.location.href,a={return_url:r,payload:o,stashed_at:Date.now()};try{window.sessionStorage?.setItem(Rd,JSON.stringify(a))}catch{}const i=`/identity/challenge?return=${encodeURIComponent(r)}`;try{await e.dispatch({handler:"navigate",params:{path:i}})}catch(l){ac.warn("navigate 실패 — window.location 으로 폴백",l),window.location.href=i}return new Promise(()=>{})},$0=ut("ApiClient");function Dd(){return typeof window<"u"&&window.__G7_DEVTOOLS__?window.__G7_DEVTOOLS__:null}class N0{constructor(e={}){$(this,"client");$(this,"config");$(this,"TOKEN_KEY","auth_token");this.config={baseURL:e.baseURL||"/api",timeout:e.timeout||3e4,onTokenExpired:e.onTokenExpired,onUnauthorized:e.onUnauthorized,onError:e.onError},this.client=Gt.create({baseURL:this.config.baseURL,timeout:this.config.timeout,headers:{"Content-Type":"application/json",Accept:"application/json"},paramsSerializer:{serialize:r=>{const a=[];for(const[i,l]of Object.entries(r))if(l!=null)if(Array.isArray(l)){const c=i.endsWith("[]")?i:`${i}[]`;for(const d of l)a.push(`${encodeURIComponent(c)}=${encodeURIComponent(String(d))}`)}else a.push(`${encodeURIComponent(i)}=${encodeURIComponent(String(l))}`);return a.join("&")}}}),this.setupInterceptors()}setToken(e){typeof window<"u"&&localStorage.setItem(this.TOKEN_KEY,e)}getToken(){return typeof window<"u"?localStorage.getItem(this.TOKEN_KEY):null}removeToken(){typeof window<"u"&&localStorage.removeItem(this.TOKEN_KEY)}setupInterceptors(){this.client.interceptors.request.use(e=>{e.url?.startsWith("/api")&&e.baseURL==="/api"&&(e.baseURL="");const r=this.getToken();if(r&&e.headers&&(e.headers.Authorization=`Bearer ${r}`),typeof window<"u"&&e.headers){const i=localStorage.getItem("g7_locale");i&&(e.headers["Accept-Language"]=i)}const a=Dd();if(a?.isEnabled()){const i=this.buildFullUrl(e),l=(e.method||"GET").toUpperCase(),c=a.trackRequest(i,l,{requestBody:e.data});e._devToolsRequestId=c}return e},e=>Promise.reject(e)),this.client.interceptors.response.use(e=>{const r=e.config,a=r._devToolsRequestId;if(a){const i=Dd();i?.isEnabled()&&i.completeRequest(a,e.status,e.data),r._devToolsRequestId=null}return e},async e=>{const r=e.config,a=r?.url||"",l=["/auth/","/layouts/"].some(c=>a.includes(c));if(e.response?.status===401&&!r?._retry&&!l){if(r._retry=!0,await br.getInstance().refreshToken()){const f=this.getToken();return f&&(r.headers.Authorization=`Bearer ${f}`),r._devToolsRequestId=null,this.client(r)}return this.completeDevToolsRequest(r,e),this.removeToken(),this.config.onUnauthorized&&this.config.onUnauthorized(),Promise.reject(e)}if(e.response?.status===401&&l)return this.completeDevToolsRequest(r,e),Promise.reject(e);if(mt.isIdentityRequired(e.response?.status,e.response?.data)){this.completeDevToolsRequest(r,e);const c={},d=r?.headers??{};for(const[w,S]of Object.entries(d))typeof S=="string"&&(c[w]=S);let f;r?.data!==void 0&&r?.data!==null&&(f=typeof r.data=="string"?r.data:JSON.stringify(r.data),c["Content-Type"]=c["Content-Type"]??"application/json");const g=r?.identity_target,m=await mt.handle(e.response.data,{headers:c,body:f,credentials:"same-origin"},g);if(!m)return Promise.reject(e);let y=null;try{y=await m.json()}catch{y=null}return m.ok?{...e.response,status:m.status,data:y}:Promise.reject(Object.assign(new Error("replay failed"),{response:{status:m.status,data:y}}))}return e.response?.status===403?(this.completeDevToolsRequest(r,e),this.callOnError(e),Promise.reject(e)):(this.completeDevToolsRequest(r,e),e.response&&this.callOnError(e),Promise.reject(e))})}async get(e,r){return(await this.client.get(e,r)).data}async post(e,r,a){return(await this.client.post(e,r,a)).data}async put(e,r,a){return(await this.client.put(e,r,a)).data}async patch(e,r,a){return(await this.client.patch(e,r,a)).data}async delete(e,r){return(await this.client.delete(e,r)).data}getInstance(){return this.client}setOnUnauthorized(e){this.config.onUnauthorized=e}setOnError(e){this.config.onError=e}completeDevToolsRequest(e,r){if(!e?._devToolsRequestId)return;const a=Dd();if(!a?.isEnabled())return;const i=e._devToolsRequestId;e._devToolsRequestId=null,r.response?a.completeRequest(i,r.response.status,r.response.data):r.request?a.failRequest(i,r.message||"Network error"):a.failRequest(i,r.message||"Request error")}buildFullUrl(e){let r=e.url||"";if(e.baseURL&&!r.startsWith("http")&&(r=`${e.baseURL}${r.startsWith("/")?"":"/"}${r}`),e.params&&typeof e.params=="object"){const a=e.params,i=[];for(const[l,c]of Object.entries(a))if(c!=null)if(Array.isArray(c)){const d=l.endsWith("[]")?l:`${l}[]`;for(const f of c)i.push(`${encodeURIComponent(d)}=${encodeURIComponent(String(f))}`)}else i.push(`${encodeURIComponent(l)}=${encodeURIComponent(String(c))}`);i.length>0&&(r+=(r.includes("?")?"&":"?")+i.join("&"))}return r}callOnError(e){if(!this.config.onError)return;const r={status:e.response?.status||0,message:e.response?.data?.message||e.message||"Unknown error",data:e.response?.data,statusText:e.response?.statusText};try{this.config.onError(r)}catch(a){$0.error("Error in onError handler:",a)}}}let Od=null;function jr(){return Od||(Od=new N0),Od}const go=ut("AuthManager");function rs(o,e,r,a){try{window.G7Core?.devTools?.trackAuthEvent?.(o,e,r,a)}catch{}}const ic={admin:{type:"admin",loginPath:"/admin/login",defaultPath:"/admin",userEndpoint:"/admin/auth/user",loginEndpoint:"/auth/admin/login",logoutEndpoint:"/admin/auth/logout",refreshEndpoint:"/admin/auth/refresh"},user:{type:"user",loginPath:"/login",defaultPath:"/",userEndpoint:"/auth/user",loginEndpoint:"/auth/login",logoutEndpoint:"/auth/logout",refreshEndpoint:"/auth/refresh"}},Ar=class Ar{constructor(){$(this,"state");$(this,"config");$(this,"isRefreshing",!1);$(this,"refreshPromise",null);$(this,"eventHandlers",new Map);this.state={isAuthenticated:!1,user:null,type:null},this.config=new Map,this.config.set("admin",ic.admin),this.config.set("user",ic.user)}static getInstance(){return Ar.instance||(Ar.instance=new Ar),Ar.instance}on(e,r){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(r)}emit(e,...r){const a=this.eventHandlers.get(e);a&&a.forEach(i=>i(...r))}isAuthenticated(){return this.state.isAuthenticated}getUser(){return this.state.user}getAuthType(){return this.state.type}async checkAuth(e){const r=jr();if(!r.getToken())return this.clearState(),!1;const i=this.config.get(e);if(!i)return go.error(`Unknown auth type: ${e}`),!1;try{const l=await r.get(i.userEndpoint);return l.success&&l.data?(this.state={isAuthenticated:!0,user:l.data,type:e},this.emit("authStateChange",this.state),!0):(this.clearState(),!1)}catch(l){return l.response?.status===401&&await this.refreshToken()?this.checkAuth(e):(this.clearState(),!1)}}async preloadAuth(e){try{return await this.checkAuth(e)}catch(r){return go.warn(`Preload auth failed for type ${e}:`,r),!1}}async login(e,r,a){const i=jr();if(!this.config.get(e))throw new Error(`Unknown auth type: ${e}`);const c=e==="admin"?ic.admin.loginEndpoint:ic.user.loginEndpoint;try{const d=a?.headers?{headers:a.headers}:void 0,f=await i.post(c,r,d);if(f.success&&f.data){i.setToken(f.data.token);const g=f.data.user.language,m=localStorage.getItem(Ar.LOCALE_STORAGE_KEY),y=g&&g!==m;if(g)try{localStorage.setItem(Ar.LOCALE_STORAGE_KEY,g)}catch(w){go.warn("Failed to save user language to localStorage:",w)}return this.state={isAuthenticated:!0,user:f.data.user,type:e},this.emit("login",this.state),this.emit("authStateChange",this.state),rs("login",!0,void 0,{userId:f.data.user.uuid,email:f.data.user.email,type:e}),y&&window.__templateApp&&window.__templateApp.changeLocale(g),f.data.user}throw new Error("Login failed")}catch(d){this.clearState();const f=d.response?.data?.message,g=new Error(f||d.message||"Login failed");throw g.response=d.response,g.status=d.response?.status,rs("login",!1,g.message,{type:e,status:d.response?.status}),g}}async logout(){const e=jr(),r=this.state.type?this.config.get(this.state.type):null;try{r&&await e.post(r.logoutEndpoint)}catch(a){go.warn("Logout API call failed:",a)}finally{e.removeToken();const a={...this.state};if(this.clearState(),this.emit("logout",a),this.emit("authStateChange",this.state),rs("logout",!0,void 0,{previousType:a.type}),r&&a.type){const i=window.location.pathname+window.location.search;window.location.href=this.getLoginRedirectUrl(a.type,i)}}}async refreshToken(){if(this.isRefreshing&&this.refreshPromise)return this.refreshPromise;this.isRefreshing=!0,this.refreshPromise=this.doRefreshToken();try{return await this.refreshPromise}finally{this.isRefreshing=!1,this.refreshPromise=null}}async doRefreshToken(){const e=this.state.type;if(!e)return!1;const r=this.config.get(e);if(!r)return!1;const a=jr();try{const i=await a.post(r.refreshEndpoint);return i.success&&i.data?.token?(a.setToken(i.data.token),this.emit("tokenRefreshed"),rs("token-refresh",!0,void 0,{type:e}),!0):(rs("token-refresh",!1,"No token in response",{type:e}),!1)}catch(i){return go.error("Token refresh failed:",i),rs("token-refresh",!1,i instanceof Error?i.message:"Unknown error",{type:e}),!1}}getLoginRedirectUrl(e,r,a){const i=this.config.get(e);if(!i)return"/login";const l=encodeURIComponent(r);let c=`${i.loginPath}?redirect=${l}`;return a&&(c+=`&reason=${encodeURIComponent(a)}`),c}updateConfig(e,r){if(r.loginPath!==void 0){const i=r.loginPath;if(!i.startsWith("/")||i.startsWith("//"))throw new Error(`AuthManager.updateConfig: loginPath must be a same-origin path starting with '/' (got: ${i})`)}const a=this.config.get(e);a&&this.config.set(e,{...a,...r})}getRedirectUrl(e){const a=this.config.get(e)?.defaultPath||"/",l=new URLSearchParams(window.location.search).get("redirect");if(l)try{const c=decodeURIComponent(l);if(c.startsWith("/"))return c}catch{}return a}getConfig(e){return this.config.get(e)}clearState(){this.state={isAuthenticated:!1,user:null,type:null}}static resetInstance(){Ar.instance=null}};$(Ar,"instance"),$(Ar,"LOCALE_STORAGE_KEY","g7_locale");let br=Ar;const Jp={401:{handler:"navigate",params:{path:"{{auth.loginPath}}"}},403:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},404:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},422:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},default:{handler:"toast",params:{type:"error",message:"{{error.message}}"}}},Zr={findHandler(o,e){if(o){if(o[e])return o[e];if(o[String(e)])return o[String(e)];if(o.default)return o.default}},normalizeOnError(o){return o?Array.isArray(o)?o:[o]:[]},injectErrorCode(o,e,r){if(o.handler!=="showErrorPage"||o.params?.errorCode!==void 0)return o;const a=typeof e=="string"?parseInt(e,10):e,i=isNaN(a)?r:a;return i===void 0?o:{...o,params:{...o.params,errorCode:i}}}},vr=ut("ErrorHandlingResolver"),Na=class Na{constructor(){$(this,"templateErrorHandling",null);$(this,"layoutErrorHandling",null);$(this,"executeAction",null)}static getInstance(){return Na.instance||(Na.instance=new Na),Na.instance}static resetInstance(){Na.instance=null}setTemplateConfig(e){this.templateErrorHandling=e,vr.log("Template config set:",e)}setLayoutConfig(e){this.layoutErrorHandling=e,vr.log("Layout config set:",e)}setActionExecutor(e){this.executeAction=e}resolve(e,r={}){const{errorHandling:a,onError:i}=r;vr.log("Resolving error:",e,{hasActionErrorHandling:!!a,hasActionOnError:!!i,hasLayoutConfig:!!this.layoutErrorHandling,hasTemplateConfig:!!this.templateErrorHandling});let l=Zr.findHandler(a,e);if(l){const c=this.getMatchedKey(a,e);return{handler:Zr.injectErrorCode(l,c,e),level:"action",matchedKey:c}}if(i){const c=Zr.normalizeOnError(i);if(c.length>0)return{handler:c.length===1?c[0]:{handler:"sequence",actions:c},level:"action",matchedKey:"onError"}}if(l=Zr.findHandler(this.layoutErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.layoutErrorHandling,e);return{handler:Zr.injectErrorCode(l,c,e),level:"layout",matchedKey:c}}if(l=Zr.findHandler(this.templateErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.templateErrorHandling,e);return{handler:Zr.injectErrorCode(l,c,e),level:"template",matchedKey:c}}if(l=Zr.findHandler(Jp,e),l){const c=this.getMatchedKey(Jp,e);return{handler:Zr.injectErrorCode(l,c,e),level:"system",matchedKey:c}}return{handler:null,level:null,matchedKey:null}}async execute(e,r){if(!this.executeAction){vr.error("Action executor is not set");return}vr.log("Executing handler:",e.handler,{errorContext:r});try{return await this.executeAction(e,{error:r})}catch(a){throw vr.error("Failed to execute handler:",a),a}}async resolveAndExecute(e,r,a={}){const i=this.resolve(e,a);if(vr.log("Resolve result:",{errorCode:e,handler:i.handler?.handler,level:i.level,matchedKey:i.matchedKey}),!i.handler)return vr.warn("No handler found for error:",e),{handled:!1};try{return{handled:!0,result:await this.execute(i.handler,r)}}catch(l){return vr.error("Handler execution failed:",l),{handled:!1}}}getMatchedKey(e,r){return e[r]?r:e[String(r)]?String(r):"default"}clearLayoutConfig(){this.layoutErrorHandling=null,vr.log("Layout config cleared")}clearAllConfig(){this.templateErrorHandling=null,this.layoutErrorHandling=null,vr.log("All config cleared")}getConfigStatus(){return{hasTemplate:this.templateErrorHandling!==null,hasLayout:this.layoutErrorHandling!==null,hasExecutor:this.executeAction!==null}}};$(Na,"instance",null);let Ld=Na;function Sr(){return Ld.getInstance()}var Md={exports:{}},po={};/** - * @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?{localStateOverride:m}:{},...y?{isolatedStateOverride:y}:{}})}else j.warn("refetchDataSource: G7Core.dataSource.refetch is not available")}),this.registerHandler("appendDataSource",async(r,a)=>{const{dataSourceId:i,dataPath:l,newData:c}=r.params||{};if(!i){j.warn("appendDataSource: dataSourceId is required");return}if(c===void 0){j.warn("appendDataSource: newData is required");return}if(typeof window<"u"&&window.G7Core?.dataSource?.updateData){if(!Array.isArray(c)){j.warn("appendDataSource: newData must be an array, got:",typeof c,c);return}await window.G7Core.dataSource.updateData(i,l||null,c,"append")}else j.warn("appendDataSource: G7Core.dataSource.updateData is not available")}),this.registerHandler("updateDataSource",async(r,a)=>{const{dataSourceId:i,data:l,merge:c=!1}=r.params||{};if(!i){j.warn("updateDataSource: dataSourceId is required");return}if(l===void 0){j.warn("updateDataSource: data is required");return}typeof window<"u"&&window.G7Core?.dataSource?.set?(j.log(`[updateDataSource] Updating dataSource '${i}' with:`,l),window.G7Core.dataSource.set(i,l,{merge:c})):j.warn("updateDataSource: G7Core.dataSource.set is not available")}),this.registerHandler("scrollIntoView",async(r,a)=>{const{selector:i,behavior:l="smooth",block:c="nearest",inline:d="nearest",waitForElement:f=!1,timeout:g=2e3,delay:m=0,retryCount:y=0,retryInterval:w=50,scrollContainer:S}=r.params||{};if(!i){j.warn("scrollIntoView: selector is required");return}if(typeof window>"u"||typeof document>"u"){j.warn("scrollIntoView: window/document is not available");return}let v=null;if(f){if(v=await new Promise(A=>{const x=document.querySelector(i);if(x){A(x);return}const R=setTimeout(()=>{M.disconnect(),A(null)},g),M=new MutationObserver(()=>{const O=document.querySelector(i);O&&(clearTimeout(R),M.disconnect(),A(O))});M.observe(document.body,{childList:!0,subtree:!0})}),!v){j.warn(`scrollIntoView: element not found for selector "${i}" after ${g}ms timeout`);return}}else{m>0&&await new Promise(x=>setTimeout(x,m)),v=document.querySelector(i);let A=0;for(;!v&&A<y;)await new Promise(x=>setTimeout(x,w)),v=document.querySelector(i),A++;if(!v){j.warn(`scrollIntoView: element not found for selector "${i}" after ${A+1} attempts`);return}}if(S){const A=document.querySelector(S);if(!A){j.warn(`scrollIntoView: container not found for selector "${S}"`);return}const x=v.getBoundingClientRect(),R=A.getBoundingClientRect(),M=x.top-R.top+A.scrollTop,O=M+x.height;let L;switch(c){case"start":L=M;break;case"center":L=M-A.clientHeight/2+x.height/2;break;case"end":L=O-A.clientHeight;break;case"nearest":default:const q=x.top<R.top,G=x.bottom>R.bottom;if(q)L=M;else if(G)L=O-A.clientHeight;else return;break}A.scrollTo({top:Math.max(0,L),behavior:l});return}v.scrollIntoView({behavior:l,block:c,inline:d})}),this.registerHandler("reloadExtensions",async(r,a)=>{if(typeof window>"u"){j.warn("reloadExtensions: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadExtensions: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState()}catch(d){throw j.error("reloadExtensions: reloadExtensionState failed",d),d}else j.warn("reloadExtensions: TemplateApp.reloadExtensionState unavailable");const{moduleInfo:l,pluginInfo:c}=r.params||{};if(l)try{await this.executeAction({handler:"reloadModuleHandlers",params:r.params},a)}catch(d){j.error("reloadExtensions: reloadModuleHandlers failed",d)}if(c)try{await this.executeAction({handler:"reloadPluginHandlers",params:r.params},a)}catch(d){j.error("reloadExtensions: reloadPluginHandlers failed",d)}j.log("reloadExtensions: done")}),this.registerHandler("reloadRoutes",async(r,a)=>{if(typeof window>"u"){j.warn("reloadRoutes: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadRoutes: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState(),j.log("reloadRoutes: delegated to reloadExtensionState")}catch(l){throw j.error("reloadRoutes: Failed to reload routes",l),l}else{const l=i.getRouter?.();l&&(await l.loadRoutes(),j.log("reloadRoutes: Routes reloaded (legacy fallback)"))}}),this.registerHandler("refresh",async(r,a)=>{if(typeof window>"u"){j.warn("refresh: window is not available");return}const i=Number(r.params?.delayMs??0);i>0&&await new Promise(l=>setTimeout(l,i)),window.location.reload()}),this.registerHandler("remount",async(r,a)=>{const{componentId:i}=r.params||{};if(!i){j.warn("remount: componentId parameter is required");return}if(!this.globalStateUpdater){j.warn("remount: globalStateUpdater is not set");return}const d=(window.G7Core?.state?.get()||{})._global?._remountKeys||{},f=d[i]||0;this.globalStateUpdater({_remountKeys:{...d,[i]:f+1}}),j.log(`remount: ${i} key incremented to ${f+1}`)}),this.registerHandler("reloadTranslations",async(r,a)=>{if(typeof window>"u"){j.warn("reloadTranslations: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadTranslations: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState(),j.log("reloadTranslations: delegated to reloadExtensionState")}catch(l){throw j.error("reloadTranslations: Failed to reload translations",l),l}}),this.registerHandler("reloadModuleHandlers",async(r,a)=>{if(typeof window>"u"){j.warn("reloadModuleHandlers: window is not available");return}const{moduleInfo:i,action:l}=r.params||{};if(!i){j.warn("reloadModuleHandlers: moduleInfo is required");return}const c=i.data||i;if(!c.identifier){j.warn("reloadModuleHandlers: moduleInfo with identifier is required");return}if(!l||l!=="add"&&l!=="remove"){j.warn('reloadModuleHandlers: action must be "add" or "remove"');return}const d=window.G7Config;if(!d){j.warn("reloadModuleHandlers: G7Config not available");return}const f=c.identifier;try{if(l==="add"){if(c.assets&&(d.moduleAssets=d.moduleAssets||{},d.moduleAssets[f]=c.assets,j.log(`reloadModuleHandlers: Added assets for ${f}`),c.assets.js)){const g=c.assets.js,m=`module-${f}`;if(document.getElementById(m)){j.warn(`reloadModuleHandlers: Script ${m} already loaded`);return}const y=document.createElement("script");if(y.id=m,y.src=g,y.async=!0,await new Promise((w,S)=>{y.onload=()=>{j.log(`reloadModuleHandlers: Script loaded successfully for ${f}`),w()},y.onerror=()=>{j.error(`reloadModuleHandlers: Failed to load script for ${f}`),S(new Error(`Failed to load module script: ${g}`))},document.head.appendChild(y)}),c.assets.css){const w=c.assets.css,S=`module-css-${f}`;if(!document.getElementById(S)){const v=document.createElement("link");v.id=S,v.rel="stylesheet",v.href=w,document.head.appendChild(v),j.log(`reloadModuleHandlers: CSS loaded for ${f}`)}}}}else if(l==="remove"){d.moduleAssets&&d.moduleAssets[f]&&(delete d.moduleAssets[f],j.log(`reloadModuleHandlers: Removed assets for ${f}`));const g=`module-${f}`,m=document.getElementById(g);m&&(m.remove(),j.log(`reloadModuleHandlers: Removed script for ${f}`));const y=`module-css-${f}`,w=document.getElementById(y);w&&(w.remove(),j.log(`reloadModuleHandlers: Removed CSS for ${f}`))}}catch(g){throw j.error(`reloadModuleHandlers: Failed to ${l} module assets`,g),g}}),this.registerHandler("reloadPluginHandlers",async(r,a)=>{if(typeof window>"u"){j.warn("reloadPluginHandlers: window is not available");return}const{pluginInfo:i,action:l}=r.params||{};if(!i){j.warn("reloadPluginHandlers: pluginInfo is required");return}const c=i.data||i;if(!c.identifier){j.warn("reloadPluginHandlers: pluginInfo with identifier is required");return}if(!l||l!=="add"&&l!=="remove"){j.warn('reloadPluginHandlers: action must be "add" or "remove"');return}const d=window.G7Config;if(!d){j.warn("reloadPluginHandlers: G7Config not available");return}const f=c.identifier;try{if(l==="add"){if(c.assets&&(d.pluginAssets=d.pluginAssets||{},d.pluginAssets[f]=c.assets,j.log(`reloadPluginHandlers: Added assets for ${f}`),c.assets.js)){const g=c.assets.js,m=`plugin-${f}`;if(document.getElementById(m)){j.warn(`reloadPluginHandlers: Script ${m} already loaded`);return}const y=document.createElement("script");if(y.id=m,y.src=g,y.async=!0,await new Promise((w,S)=>{y.onload=()=>{j.log(`reloadPluginHandlers: Script loaded successfully for ${f}`),w()},y.onerror=()=>{j.error(`reloadPluginHandlers: Failed to load script for ${f}`),S(new Error(`Failed to load plugin script: ${g}`))},document.head.appendChild(y)}),c.assets.css){const w=c.assets.css,S=`plugin-css-${f}`;if(!document.getElementById(S)){const v=document.createElement("link");v.id=S,v.rel="stylesheet",v.href=w,document.head.appendChild(v),j.log(`reloadPluginHandlers: CSS loaded for ${f}`)}}}}else if(l==="remove"){d.pluginAssets&&d.pluginAssets[f]&&(delete d.pluginAssets[f],j.log(`reloadPluginHandlers: Removed assets for ${f}`));const g=`plugin-${f}`,m=document.getElementById(g);m&&(m.remove(),j.log(`reloadPluginHandlers: Removed script for ${f}`));const y=`plugin-css-${f}`,w=document.getElementById(y);w&&(w.remove(),j.log(`reloadPluginHandlers: Removed CSS for ${f}`))}}catch(g){throw j.error(`reloadPluginHandlers: Failed to ${l} plugin assets`,g),g}});let e=!1;this.registerHandler("showErrorPage",async(r,a)=>{if(e){j.log("showErrorPage: Already active, skipping duplicate call");return}if(e=!0,typeof window>"u"){e=!1,j.warn("showErrorPage: window is not available");return}const i=window.__templateApp;if(!i){e=!1,j.warn("showErrorPage: TemplateApp not initialized");return}const l=i.getErrorPageHandler?.();if(!l){e=!1,j.warn("showErrorPage: ErrorPageHandler not available");return}const c=r.params?.errorCode||500,d=r.params?.target||"content";let f=r.params?.containerId;f||(f=d==="full"?"app":"main_content"),j.log("showErrorPage:",{errorCode:c,target:d,containerId:f});const g=(m,y=6e4)=>new Promise((w,S)=>{const v=document.getElementById(m);if(v){w(v);return}j.log(`showErrorPage: Waiting for container #${m}...`);let A;const x=new MutationObserver((R,M)=>{const O=document.getElementById(m);O&&(j.log(`showErrorPage: Container #${m} found`),M.disconnect(),clearTimeout(A),w(O))});x.observe(document.body,{childList:!0,subtree:!0}),A=setTimeout(()=>{x.disconnect(),S(new Error(`Container #${m} not found within ${y}ms`))},y)});try{await g(f);const m=Sr(),y=m.layoutErrorHandling;m.clearLayoutConfig();try{await l.renderError(c,f)||j.warn(`showErrorPage: Failed to render error page for code ${c}`)}finally{m.setLayoutConfig(y)}}catch(m){throw j.error("showErrorPage: Error rendering error page:",m),m}finally{e=!1}}),this.registerHandler("emitEvent",async(r,a)=>{const i=r.params?.event,l=r.params?.data;if(!i){j.warn("emitEvent: event parameter is required");return}if(typeof window>"u"){j.warn("emitEvent: window is not available");return}const c=window.G7Core;if(!c?.componentEvent?.emit){j.warn("emitEvent: G7Core.componentEvent is not available");return}try{j.log(`emitEvent: Emitting "${i}"`,l);const d={...l,_context:{data:a.data,state:a.state}},f=await c.componentEvent.emit(i,d);if(!f||f.length===0?j.warn(`emitEvent: No listeners found for "${i}"`):j.log(`emitEvent: Event "${i}" completed with ${f.length} listener(s)`,f),this.globalStateUpdater){const g=(c?.state?.get()||{})._local||{},m={event:i,success:!0,data:f.length===1?f[0]:f,listeners:f.length};this.globalStateUpdater({_local:{...g,_eventResult:m}});const y=a.state&&typeof a.state=="object"&&!Array.isArray(a.state)?a.state:g;window.__g7SequenceLocalSync={...y,_eventResult:m}}}catch(d){if(j.error(`emitEvent: Event "${i}" failed`,d),this.globalStateUpdater){const f=c?.state?.get()||{};this.globalStateUpdater({_local:{...f._local,_eventResult:{event:i,success:!1,error:d instanceof Error?d.message:String(d)}}})}throw d}}),this.registerHandler("updateProductField",async(r,a)=>{const{productId:i,field:l,value:c}=r.params||{};if(!i||!l){j.warn("updateProductField: productId and field are required");return}if(typeof window>"u"){j.warn("updateProductField: window is not available");return}const d=window.G7Core;if(!d?.state?.get||!this.globalStateUpdater){j.warn("updateProductField: G7Core.state or globalStateUpdater is not available");return}try{const f=d.state.get()||{},m=(f.products?.data?.data||[]).map(w=>w.id===i?{...w,[l]:c,_modified:!0}:w),y=new Set(f._local?.modifiedProductIds||[]);y.add(i),this.globalStateUpdater({products:{...f.products,data:{...f.products?.data,data:m}},_local:{...f._local,modifiedProductIds:Array.from(y)}}),j.log(`updateProductField: Updated product ${i}, field: ${l}, value:`,c)}catch(f){throw j.error("updateProductField: Error updating product field",f),f}}),this.registerHandler("updateOptionField",async(r,a)=>{const{productId:i,optionId:l,field:c,value:d}=r.params||{};if(!i||!l||!c){j.warn("updateOptionField: productId, optionId and field are required");return}if(typeof window>"u"){j.warn("updateOptionField: window is not available");return}const f=window.G7Core;if(!f?.state?.get||!this.globalStateUpdater){j.warn("updateOptionField: G7Core.state or globalStateUpdater is not available");return}try{const g=f.state.get()||{},y=(g.products?.data?.data||[]).map(S=>{if(S.id===i&&S.options){const v=S.options.map(A=>A.id===l?{...A,[c]:d,_modified:!0}:A);return{...S,options:v,_modified:!0}}return S}),w=new Set(g._local?.modifiedProductIds||[]);w.add(i),this.globalStateUpdater({products:{...g.products,data:{...g.products?.data,data:y}},_local:{...g._local,modifiedProductIds:Array.from(w)}}),j.log(`updateOptionField: Updated product ${i} option ${l}, field: ${c}, value:`,d)}catch(g){throw j.error("updateOptionField: Error updating option field",g),g}}),this.registerHandler("setLocale",async(r,a)=>{const i=r.target;if(!i||typeof i!="string"){j.warn("setLocale: Invalid locale:",i);return}const l=window.__templateApp;if(l&&typeof l.changeLocale=="function")try{await l.changeLocale(i),j.log("setLocale: Locale changed to",i)}catch(c){j.error("setLocale: Failed to change locale:",c),window.location.reload()}else{j.warn("setLocale: TemplateApp not found, falling back to page reload");try{localStorage.setItem("g7_locale",i)}catch{}window.location.reload()}}),this.registerHandler("suppress",async()=>{j.log("suppress: Error intentionally suppressed")}),this.registerBuiltInHandlerMetadata()}registerBuiltInHandlerMetadata(){const e=Vn();if(!e?.isEnabled())return;const r=[{name:"refetchDataSource",description:"데이터 소스를 다시 fetch합니다"},{name:"appendDataSource",description:"데이터 소스에 새 데이터를 병합합니다 (무한 스크롤용)"},{name:"updateDataSource",description:"API 응답으로 데이터 소스를 직접 업데이트합니다 (refetch 대체)"},{name:"reloadRoutes",description:"라우트를 다시 로드합니다"},{name:"refresh",description:"현재 페이지를 새로고침합니다"},{name:"remount",description:"컴포넌트를 리마운트합니다"},{name:"reloadTranslations",description:"다국어 파일을 다시 로드합니다"},{name:"showErrorPage",description:"에러 페이지를 표시합니다"},{name:"emitEvent",description:"이벤트를 발생시킵니다"},{name:"updateProductField",description:"상품 필드를 인라인 수정합니다"},{name:"updateOptionField",description:"상품 옵션 필드를 인라인 수정합니다"},{name:"setLocale",description:"언어를 변경합니다 (DB 저장 + UI 리렌더링)"}];for(const a of r)e.trackHandlerRegistration(a.name,"built-in",a.description)}createHandler(e,r,a){j.log("createHandler called for:",e.handler,e.type);const i=window.__g7DevTools,l=Date.now(),c=a?.state?{...a.state}:null,d=`handler_${e.handler}_${l}`;return async f=>{if(j.log("Handler invoked for:",e.handler,"event:",f.type),i?.isEnabled?.()&&c){const m=window.G7Core?.state?.getLocal?.()??a?.state??{},y=Date.now()-l;for(const w of Object.keys(c)){const S=c[w],v=m[w];S!==v&&y>100&&(i.trackStaleClosureWarning?.({type:"event-handler-stale",location:`createHandler(${e.handler})`,capturedPath:`_local.${w}`,capturedValue:S,capturedAt:l,currentValue:v,actionId:d,stackTrace:new Error().stack}),j.warn(`[Stale Closure] _local.${w} changed after handler creation:`,`captured="${S}" → current="${v}" (${y}ms ago)`))}}try{f.type!=="change"&&f.preventDefault();let g={};if(f.type==="submit"&&f.target instanceof HTMLFormElement){const w=f.target;new FormData(w).forEach((v,A)=>{g[A]=v})}const m={...this.defaultContext,data:{...r,form:g,_local:a?.state||{},$event:f},event:f,...a&&{state:a.state,setState:a.setState},isolatedContext:a?.isolatedContext};if(e.confirm){let w=this.resolveValue(e.confirm,m.data);if(this.translationEngine&&this.translationContext&&w.startsWith("$t:")&&(w=this.translationEngine.resolveTranslations(w,this.translationContext,m.data)),!confirm(w))return}const y=window.__g7ActionContext;window.__g7ActionContext={...a,data:m.data};try{await this.executeAction(e,m)}finally{window.__g7ActionContext=y}}catch(g){if(j.error("Action execution failed:",g),e.onError){const m=g instanceof It&&g.originalError?g.originalError:g,y={...this.defaultContext,data:{...r,error:m,_local:a?.state||{},$event:f},event:f,...a&&{state:a.state,setState:a.setState}},w=Array.isArray(e.onError)?e.onError:[e.onError];for(const S of w)await this.executeAction(S,y)}}}}generateActionId(e,r){const a=e.handler,i=r||"no-target",l=Date.now(),c=Math.random().toString(36).substring(2,9);return`${a}_${i.replace(/[^a-zA-Z0-9]/g,"_")}_${l}_${c}`}async executeAction(e,r){if(e=this.resolveActionRef(e),typeof e.handler=="string"&&e.handler.includes("{{")){const f=this.evaluateExpression(e.handler,r.data);e={...e,handler:f==null?"":String(f)}}const a=Vn(),i=a?.isEnabled()?`action_${Date.now()}_${Math.random().toString(36).substring(2,11)}`:void 0,l=performance.now();let c;if(i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),context:this.sanitizeForDevTools({hasState:!!r.state,hasSetState:!!r.setState,dataKeys:r.data?Object.keys(r.data):[]}),startTime:l,status:"started"}),e.if!==void 0)try{const f=this.resolveValue(e.if,r.data),g=f===!0||f==="true"||f&&f!=="false"&&f!=="0"&&f!==!1;if(j.log("[executeAction] if condition result:",e.handler,g),!g)return i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"success",result:{skipped:!0,reason:"if condition false"}}),{success:!0,data:void 0}}catch(f){return j.error("[executeAction] if condition error:",e.handler,f),i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"error",error:{name:"ConditionEvaluationError",message:f instanceof Error?f.message:String(f),stack:f instanceof Error?f.stack:void 0}}),{success:!0,data:void 0}}let d;try{const f=this.resolveParams(e.params,r.data);i&&(c=this.sanitizeForDevTools(f));const g=e.target?this.resolveValue(e.target,r.data):void 0;if(e.handler==="apiCall"&&r.setState){d=this.generateActionId(e,g);const y=r.state?.loadingActions||{};r.setState({loadingActions:{...y,[d]:!0}})}if(this.previewMode&&nm.has(e.handler))return j.warn(`Preview mode: "${e.handler}" suppressed`,g||f),i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"skipped",metadata:{reason:"preview_mode_suppressed"}}),{success:!0,data:void 0};let m;switch(e.handler){case"ensureIdentityVerified":m=await this.handleEnsureIdentityVerified(f);break;case"resolveIdentityChallenge":m=this.handleResolveIdentityChallenge(f);break;case"navigate":const y=f.path||g;m=await this.handleNavigate(y,f,r);break;case"navigateBack":m=await this.handleNavigateBack();break;case"navigateForward":m=await this.handleNavigateForward();break;case"openWindow":{const v=f.path||g;m=await this.handleOpenWindow(v,f);break}case"replaceUrl":{const v=f.path||g||window.location.pathname;m=await this.handleReplaceUrl(v,f);break}case"apiCall":if(e.onSuccess&&a?.isEnabled()&&i){const v=window.G7Core,A={};if(v?.state?.get){const x=v.state.get();A._global=x}r.state&&(A._local=r.state),a.registerStateCaptureForHandler?.(i,["_global","_local"],A)}const w=e.auth_mode??(e.auth_required?"required":"none"),S=e.identity_target?this.resolveParams(e.identity_target,r.data):void 0;m=await this.handleApiCall(g,f,r,w,S);break;case"login":m=await this.handleLogin(g,f,r);break;case"logout":m=await this.handleLogout(g,r);break;case"setState":j.log("[executeAction] setState resolvedParams:",f),m=await this.handleSetState(e.render!==void 0?{...f,__render:e.render}:f,r);break;case"setError":m=await this.handleSetError(g,f,r);break;case"openModal":m=await this.handleOpenModal(g,r);break;case"closeModal":m=await this.handleCloseModal(r);break;case"showAlert":m=await this.handleShowAlert(g,r);break;case"toast":m=await this.handleToast(f,r);break;case"switch":m=await this.handleSwitch(e,r);break;case"conditions":m=await this.handleConditions(e,r);break;case"sequence":m=await this.handleSequence(e,r);break;case"parallel":m=await this.handleParallel(e,r);break;case"startInterval":m=this.handleStartInterval(f,r);break;case"stopInterval":m=this.handleStopInterval(f);break;case"loadScript":m=await this.handleLoadScript(f,e,r);break;case"callExternal":m=await this.handleCallExternal(f,e,r);break;case"callExternalEmbed":m=await this.handleCallExternalEmbed(f,e,r);break;case"saveToLocalStorage":m=await this.handleSaveToLocalStorage(f,r);break;case"loadFromLocalStorage":m=await this.handleLoadFromLocalStorage(f,r);break;default:m=await this.handleCustomAction({...e,target:g,params:f},r);break}if(e.resultTo&&m!==void 0){const{target:y,key:w,merge:S}=e.resultTo,v=this.resolveValue(w,r.data),A=S==="replace"?"replace":S==="shallow"?"shallow":"deep";if(y==="_local"&&r.setState){const x=this.buildNestedUpdate(v,m),R=A!=="deep"?{...x,__mergeMode:A}:x;r.setState(R),j.log(`[resultTo] Saved to _local.${v} (merge=${A}):`,m)}else if(y==="_local"&&this.globalStateUpdater){const M=(window.G7Core?.state?.get()||{})._local||{},O=this.buildNestedUpdate(v,m);let L;A==="replace"?L=O:A==="shallow"?L={...M,...O}:L=this.deepMergeWithState(O,M),this.globalStateUpdater({_local:L}),j.log(`[resultTo] Saved to _local.${v} via globalStateUpdater (merge=${A}):`,m)}else if(y==="_global"&&this.globalStateUpdater){const x=this.buildNestedUpdate(v,m);this.globalStateUpdater(x),j.log(`[resultTo] Saved to _global.${v}:`,m)}else if(y==="_isolated"&&r.isolatedContext){const x=this.buildNestedUpdate(v,m);r.isolatedContext.mergeState(x,A),j.log(`[resultTo] Saved to _isolated.${v} (merge=${A}):`,m)}else j.warn(`[resultTo] Cannot save result: target=${y}, setState=${!!r.setState}, globalStateUpdater=${!!this.globalStateUpdater}, isolatedContext=${!!r.isolatedContext}`)}if(e.onSuccess){if(a?.isEnabled()&&i&&e.handler==="apiCall"){const S=window.G7Core,v={};S?.state?.get&&(v._global=S.state.get()),r.state&&(v._local=r.state),a.detectStaleClosure?.(i,`${e.handler} → onSuccess`,v,"callback-state-capture",i)}const y={...r,data:{...r.data,result:m,response:m}},w=Array.isArray(e.onSuccess)?e.onSuccess:[e.onSuccess];w.length>1?await this.handleSequence({handler:"sequence",type:"click",actions:w},y):w.length===1&&await this.executeAction(w[0],y)}return i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),resolvedParams:c,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"success",result:this.sanitizeForDevTools(m)}),{success:!0,data:m}}catch(f){const g=f instanceof It?f:new It(`Failed to execute action: ${e.handler}`,e,f instanceof Error?f:void 0),m=g.originalError?.response||{},y=m.data||{},w=g.originalError?.status||m.status||500;let S=U0(g.originalError??f,e.handler,y.message,g.message);this.translationEngine&&this.translationContext&&S.startsWith("$t:")&&(S=this.translationEngine.resolveTranslations(S,this.translationContext));const v={status:w,message:S,errors:y.errors||m.errors,data:y,statusText:g.originalError?.statusText,error_code:y.error_code??m.error_code};if(g.unknownHandler)throw j.warn(`Unknown action handler "${e.handler}" — skipped (extension not loaded?). Not surfaced to the user.`),g;const x=Sr().resolve(w,{errorHandling:e.errorHandling,onError:e.onError});if(x.handler){if(a?.isEnabled()&&i&&e.handler==="apiCall"){const M=window.G7Core,O={};M?.state?.get&&(O._global=M.state.get()),r.state&&(O._local=r.state),a.detectStaleClosure?.(i,`${e.handler} → onError`,O,"callback-state-capture",i)}const R={...r,data:{...r.data,error:v}};try{const M={type:"click",handler:x.handler.handler,target:x.handler.target,params:x.handler.params,actions:x.handler.actions};return await this.executeAction(M,R),{success:!1,error:g}}catch(M){return j.error("Error executing error handler:",M),{success:!1,error:g}}}throw i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),resolvedParams:c,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"error",error:{name:g.name,message:g.message,stack:g.stack}}),g}finally{if(e.handler==="apiCall"&&r.setState&&d){const f=r.state?.loadingActions||{},{[d]:g,...m}=f;r.setState({loadingActions:m})}}}async handleNavigate(e,r,a){let i=e;if(r.query||r.mergeQuery===!0)if(r.mergeQuery===!0)i=this.buildMergedQueryPath(e,r.query??{});else{const c=new URLSearchParams;for(const[f,g]of Object.entries(r.query))if(g!=null&&g!=="")if(Array.isArray(g)){const m=f.endsWith("[]")?f:`${f}[]`;for(const y of g)y!=null&&y!==""&&c.append(m,String(y))}else c.set(f,String(g));const d=c.toString();d&&(i=`${e}?${d}`)}if(j.log("handleNavigate:",{target:e,params:r,finalPath:i,replace:r.replace,windowLocationSearch:window.location.search}),r.replace===!0){const c=window.G7Core;if(c?.updateQueryParams){const d=typeof r.transition_overlay_target=="string"?r.transition_overlay_target:void 0;await c.updateQueryParams(i,d?{transitionOverlayTarget:d}:void 0),j.log("handleNavigate: Used updateQueryParams for replace mode",d?{transitionOverlayTarget:d}:void 0),this.applyScrollOption(r.scroll,r.scrollBehavior,"top");return}j.warn("handleNavigate: G7Core.updateQueryParams not available, falling back to React Router")}const l=this.resolveNavigateFallbackAction(i,r);if(l){j.warn(`handleNavigate: No route matched, falling back to "${l.handler}"`),await this.dispatchAction({type:"click",handler:l.handler,params:l.params},a),this.applyScrollOption(r.scroll,r.scrollBehavior,"top");return}if(!a.navigate)throw new It("Navigate function is not provided in context");a.navigate(i,{replace:r.replace===!0}),this.applyScrollOption(r.scroll,r.scrollBehavior,"top")}resolveNavigateFallbackAction(e,r){const a=r.fallback;if(a===!1||r.replace===!0)return null;const l=window.__templateApp?.getRouter?.();if(!l||typeof l.match!="function"||typeof l.getRoutes=="function"&&l.getRoutes().length===0)return null;const c=e.split("?")[0];return l.match(c)?null:this.resolveNavigateFallback(a,e,r)}resolveNavigateFallback(e,r,a){return e==null?{handler:"openWindow",params:{path:r,target:"_self"}}:typeof e=="string"?{handler:e,params:{path:r}}:typeof e=="object"&&typeof e.handler=="string"?{handler:e.handler,params:{path:r,...e.params||{}}}:(j.warn("resolveNavigateFallback: unrecognized fallback option, using openWindow",e),{handler:"openWindow",params:{path:r,target:"_blank"}})}applyScrollOption(e,r,a){const i=e===void 0?a:e;if(i==="preserve")return;const l=r==="smooth"?"smooth":"instant",c=y=>{if(typeof y!="object"||y===null)return!1;const w=y;return"container"in w||"to"in w||"block"in w||"offset"in w},d=(y,w,S=0)=>{y===window?window.scrollTo({top:w,left:S,behavior:l}):y.scrollTo({top:w,left:S,behavior:l})},f=(y,w,S,v)=>{if(w===window){y.scrollIntoView({behavior:l,block:S}),v&&window.scrollBy({top:-v,left:0,behavior:l});return}const A=w,x=y.getBoundingClientRect(),R=A.getBoundingClientRect(),M=A.scrollTop+(x.top-R.top);let O;S==="center"?O=M-(A.clientHeight-y.clientHeight)/2:S==="end"?O=M-(A.clientHeight-y.clientHeight):O=M,O-=v,A.scrollTo({top:Math.max(0,O),left:0,behavior:l})},g=()=>{window.scrollTo({top:0,left:0,behavior:l});const y=document.getElementById("app")??document.body;if(!y)return;y.querySelectorAll("*").forEach(S=>{if(S.scrollTop===0&&S.scrollLeft===0)return;const v=window.getComputedStyle(S),A=v.overflowY,x=v.overflowX;(A==="auto"||A==="scroll"||x==="auto"||x==="scroll")&&S.scrollTo({top:0,left:0,behavior:l})})},m=()=>{try{if(c(i)){const y=i,w=y.block??"start",S=typeof y.offset=="number"?y.offset:0;let v=window;if(typeof y.container=="string"&&y.container.length>0){const x=document.querySelector(y.container);if(!x){j.warn(`applyScrollOption: container not found: ${y.container}`);return}v=x}const A=y.to??"top";if(A==="top"){v===window?g():d(v,0);return}if(typeof A=="number"){d(v,A-S);return}if(typeof A=="object"&&A!==null&&("x"in A||"y"in A)){const{x=0,y:R=0}=A;d(v,R-S,x);return}if(typeof A=="string"&&(A.startsWith("#")||A.startsWith("."))){const x=document.querySelector(A);x?f(x,v,w,S):j.warn(`applyScrollOption: target element not found: ${A}`);return}j.warn('applyScrollOption: invalid "to" value in extended form',A);return}if(i==="top"){g();return}if(typeof i=="number"){window.scrollTo({top:i,left:0,behavior:l});return}if(typeof i=="object"&&i!==null&&("x"in i||"y"in i)){const{x:y=0,y:w=0}=i;window.scrollTo({top:w,left:y,behavior:l});return}if(typeof i=="string"&&(i.startsWith("#")||i.startsWith("."))){const y=document.querySelector(i);y&&y.scrollIntoView({behavior:l,block:"start"});return}}catch(y){j.warn("applyScrollOption: failed to apply scroll",y)}};typeof window.requestAnimationFrame=="function"?window.requestAnimationFrame(m):m()}async handleOpenWindow(e,r){let a=e;if(r.query){const l=new URLSearchParams;for(const[d,f]of Object.entries(r.query))f!=null&&f!==""&&l.set(d,String(f));const c=l.toString();c&&(a=`${e}?${c}`)}const i=r.target==="_self"?"_self":"_blank";j.log("handleOpenWindow:",{target:e,params:r,finalPath:a,windowTarget:i}),i==="_self"?window.location.assign(a):window.open(a,"_blank")}async handleNavigateBack(){j.log("handleNavigateBack"),window.history.back()}async handleNavigateForward(){j.log("handleNavigateForward"),window.history.forward()}async handleReplaceUrl(e,r){let a=e;if(r.query||r.mergeQuery===!0)if(r.mergeQuery===!0)a=this.buildMergedQueryPath(e,r.query??{});else{const i=new URLSearchParams;for(const[c,d]of Object.entries(r.query))if(d!=null&&d!=="")if(Array.isArray(d)){const f=c.endsWith("[]")?c:`${c}[]`;for(const g of d)g!=null&&g!==""&&i.append(f,String(g))}else i.set(c,String(d));const l=i.toString();l&&(a=`${e}?${l}`)}j.log("handleReplaceUrl:",{target:e,params:r,finalPath:a}),window.history.replaceState(null,"",a),this.applyScrollOption(r.scroll,r.scrollBehavior,"preserve")}buildMergedQueryPath(e,r){const a=new URLSearchParams(window.location.search);for(const[c,d]of Object.entries(r))if(d==null||d==="")a.delete(c);else if(Array.isArray(d)){const f=c.endsWith("[]")?c:`${c}[]`;a.delete(c),a.delete(f);for(const g of d)g!=null&&g!==""&&a.append(f,String(g))}else a.set(c,String(d));const i=a.toString(),l=e.split("?")[0];return i?`${l}?${i}`:l}async ensureCsrfToken(){try{await fetch("/sanctum/csrf-cookie",{credentials:"include"})}catch(e){throw new It("Failed to fetch CSRF token",void 0,e instanceof Error?e:void 0)}}getCsrfTokenFromCookie(){const a=`; ${document.cookie}`.split("; XSRF-TOKEN=");if(a.length===2){const i=a.pop()?.split(";").shift();return i?decodeURIComponent(i):null}return null}buildQueryString(e){const r=new URLSearchParams;for(const[a,i]of Object.entries(e))if(!(i==null||i===""))if(Array.isArray(i))for(const l of i)l!=null&&l!==""&&r.append(`${a}[]`,String(l));else typeof i=="object"?r.append(a,JSON.stringify(i)):r.append(a,String(i));return r.toString()}async handleEnsureIdentityVerified(e){const r=typeof e.purpose=="string"?e.purpose:"sensitive_action",a=e.target&&typeof e.target=="object"?e.target:void 0;return await mt.handle({success:!1,error_code:"identity_verification_required",message:"",verification:{policy_key:e.policy_key??"",purpose:r,provider_id:e.provider_id??null,render_hint:e.render_hint??null,return_request:null}},void 0,a)!==null}handleResolveIdentityChallenge(e){const r=typeof e.result=="string"?e.result:"cancelled";let a;switch(r){case"verified":{const i=typeof e.token=="string"?e.token:"";if(!i){j.warn("resolveIdentityChallenge: result=verified 인데 token 이 비어있습니다. failed 로 강등합니다."),a={status:"failed",failureCode:"MISSING_TOKEN"};break}a={status:"verified",token:i,providerData:e.providerData&&typeof e.providerData=="object"?e.providerData:void 0};break}case"pending":{const i=typeof e.pollUrl=="string"?e.pollUrl:"",l=typeof e.expiresAt=="string"?e.expiresAt:"";if(!i||!l){j.warn("resolveIdentityChallenge: result=pending 인데 pollUrl/expiresAt 누락 — failed 로 강등합니다."),a={status:"failed",failureCode:"MALFORMED_PENDING"};break}a={status:"pending",pollUrl:i,pollIntervalMs:typeof e.pollIntervalMs=="number"?e.pollIntervalMs:void 0,expiresAt:l};break}case"failed":a={status:"failed",failureCode:typeof e.failureCode=="string"?e.failureCode:"UNKNOWN",reason:typeof e.reason=="string"?e.reason:void 0};break;case"cancelled":default:a={status:"cancelled"};break}return mt.resolveDeferred(a),!0}async handleApiCall(e,r,a,i="none",l){const{method:c="GET",body:d,headers:f,contentType:g}=r;c!=="GET"&&c!=="HEAD"&&await this.ensureCsrfToken();const m=this.getCsrfTokenFromCookie();let y={};if(i==="required"||i==="optional"){const K=jr().getToken();K&&(y={Authorization:`Bearer ${K}`})}let w=e;const S=r.query||(c==="GET"?d:null);if(S&&typeof S=="object"){const B=this.buildQueryString(S);if(B){const K=e.includes("?")?"&":"?";w=`${e}${K}${B}`}}const v=window.G7Core?.state?.getGlobal?.()||a.state?._global||{},A=window.G7Core?.state?.getLocal?.()||a.state?._local||{},x={_global:v,_local:A},R=this.getMatchingGlobalHeaders(e,x),M={};if(typeof window<"u"){const B=localStorage.getItem("g7_locale");B&&(M["Accept-Language"]=B)}const O=g==="multipart/form-data",L={method:c,headers:{...O?{}:{"Content-Type":"application/json"},Accept:"application/json",...M,...m&&{"X-XSRF-TOKEN":m},...y,...R,...f},credentials:"include"};if(d&&c!=="GET")if(O){const B=new FormData;for(const[K,fe]of Object.entries(d))fe instanceof File||fe instanceof Blob?B.append(K,fe):fe!=null&&B.append(K,typeof fe=="object"?JSON.stringify(fe):String(fe));L.body=B}else L.body=JSON.stringify(d);const q=Vn();let G=null;q?.isEnabled()&&(G=q.trackRequest(w,c));try{let B=await fetch(w,L),K;try{K=await B.json()}catch{K=null}if(mt.isIdentityRequired(B.status,K)){const fe=await mt.handle(K,{body:L.body,headers:L.headers,credentials:L.credentials},l);if(fe){B=fe;try{K=await B.json()}catch{K=null}}}if(G&&q?.isEnabled()&&(q.completeRequest(G,B.status,K),G=null),!B.ok){const fe=K||{},Ce=new Error(fe.message||`API call failed: ${B.statusText}`);throw Ce.response=fe,Ce.status=B.status,Ce.statusText=B.statusText,new It(fe.message||`API call failed: ${B.statusText}`,void 0,Ce)}return K}catch(B){throw G&&q?.isEnabled()&&q.failRequest(G,B instanceof Error?B.message:String(B)),B}}async handleLogin(e,r,a){const{body:i}=r;if(!i||!i.email||!i.password)throw new It("Login requires email and password in body params");const l=e==="user"?"user":"admin",c=l==="admin"?"/api/auth/admin/login":"/api/auth/login",d=window.G7Core?.state?.getGlobal?.()||a.state?._global||{},f=window.G7Core?.state?.getLocal?.()||a.state?._local||{},g={_global:d,_local:f},m=this.getMatchingGlobalHeaders(c,g),y=br.getInstance();try{const w=Object.keys(m).length>0?{headers:m}:void 0;return{user:await y.login(l,{email:i.email,password:i.password},w)}}catch(w){const S=w.message||"Login failed",v=new Error(S);throw v.response=w.response?.data||{},v.status=w.status||w.response?.status||500,new It(S,void 0,v)}}async handleLogout(e,r){await br.getInstance().logout()}async handleSetState(e,r){j.log("[handleSetState] START, params:",e);const{target:a="component",scope:i,merge:l,__render:c,...d}=e,f=Vn(),g=window.G7Core;if(typeof a=="string"&&(a.startsWith("$parent.")||a.startsWith("$root.")))return this.handleParentScopeSetState(a,d,l,r);const m=r.state&&Object.keys(r.state).length>0?r.state:g?.state?.get()||{},y=a==="global"?"_global":a.startsWith("_local.")?a:"_local",w=y==="_global"?m._global:y.startsWith("_local.")?this.getNestedProperty(m._local||{},y.slice(7)):m._local,S=f?.isEnabled()?f.startStateChange(y,w,d,{actionId:r.actionId,handlerType:"setState",source:`target=${a}`}):void 0,v=l==="replace"?"replace":l==="shallow"?"shallow":"deep",A=v!=="deep"?{...d,__mergeMode:v,...S?{__setStateId:S}:{}}:{...d,...S?{__setStateId:S}:{}};if(a==="global"){if(!this.globalStateUpdater){j.warn("Global state updater is not set"),S&&f&&f.completeStateChange(S);return}const x=g?.state?.get()||{},{__setStateId:R,__mergeMode:M,...O}=A,L=v==="deep"?this.deepMergeWithState(O,x):O,q={...L,...S?{__setStateId:S}:{},...v!=="deep"?{__mergeMode:v}:{}};return j.log("setState global:",q),this.globalStateUpdater(q,{render:c}),S&&f&&setTimeout(()=>f.completeStateChange(S),0),{__target:"global",...v!=="deep"?{__mergeMode:v}:{},...L}}else if(a==="isolated"){const x=r.isolatedContext;if(x){const{__mergeMode:R,__setStateId:M,...O}=A;return x.mergeState(O,v),j.log("[handleSetState] isolated state updated:",O,"mergeMode:",v),S&&f&&setTimeout(()=>f.completeStateChange(S),0),{__target:"isolated",...O}}else if(j.warn('[handleSetState] isolated target used but no IsolatedStateContext found. Make sure the component has "isolatedState" attribute. Falling back to local state.'),r.setState){const M=window.__g7PendingLocalState||r.state||{},O=v==="deep"?this.deepMergeWithState(A,M):A;return r.setState(O),S&&f&&setTimeout(()=>f.completeStateChange(S),0),O}else throw S&&f&&f.completeStateChange(S),new It("isolated target used but no IsolatedStateContext found and no setState function in context")}else if(a==="local"||a==="component"||a==="_local"){if(i==="parent"||i==="root"){const R=window.__g7LayoutContextStack||[];if(R.length>0){const M=i==="parent"?R[R.length-1]:R[0];if(M?.setState){const{__mergeMode:O,__setStateId:L,...q}=A,G=v==="deep"?this.deepMergeWithState(q,M.state||{}):q;return j.log(`[handleSetState] scope=${i}: 타겟 컨텍스트에 상태 업데이트`,G),M.setState(G),S&&f&&setTimeout(()=>f.completeStateChange(S),0),G}}j.warn(`[handleSetState] scope=${i}: 레이아웃 컨텍스트 스택이 비어있습니다. current로 폴백합니다.`)}const x=r.setState&&!r._isDispatchFallbackContext;if(j.log("[handleSetState] isRealComponentContext:",x,"context.setState:",!!r.setState,"_isDispatchFallbackContext:",r._isDispatchFallbackContext),x){j.log("[handleSetState] Using COMPONENT setState path"),j.log("[handleSetState] resolvedPayload:",A),j.log("[handleSetState] context.state:",r.state),j.log("[handleSetState] mergeMode:",v);const M=window.__g7PendingLocalState||r.state||{},O=v==="deep"?this.deepMergeWithState(A,{}):A;j.log("[handleSetState] convertedPayload (변경 필드만, dot notation 변환):",O),r.setState(O);const L=v==="deep"?this.deepMergeWithState(A,M):v==="shallow"?{...r.state||{},...A}:A,{__mergeMode:q,__setStateId:G,...B}=L;window.__g7PendingLocalState=B,j.log("[handleSetState] __g7PendingLocalState updated:",B),this.globalStateUpdater&&(this.globalStateUpdater({_local:B},{render:!1}),j.log("[handleSetState] _global._local synced (render:false):",B));const{__mergeMode:K,__setStateId:fe,...Ce}=A;if(v==="replace")window.__g7ForcedLocalFields=Ce;else{const ke=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=this.deepMergeWithState(Ce,ke)}return j.log("[handleSetState] __g7ForcedLocalFields updated:",Ce),S&&f&&setTimeout(()=>f.completeStateChange(S),0),{__target:"local",...L}}else if(this.globalStateUpdater){j.log("[handleSetState] Using GLOBAL STATE UPDATER path for _local");const M=window.__g7PendingLocalState||m._local||{};j.log("[handleSetState] currentLocal (with pending):",M);const O=v==="deep"?this.deepMergeWithState(A,M):A;return j.log("[handleSetState] finalLocal (merged):",O),this.globalStateUpdater({_local:O},{render:c}),window.__g7PendingLocalState=O,j.log("[handleSetState] __g7PendingLocalState updated for globalStateUpdater path"),S&&f&&setTimeout(()=>f.completeStateChange(S),0),{__target:"local",...O}}else throw S&&f&&f.completeStateChange(S),new It("setState function is not provided in context and globalStateUpdater is not set")}else if(a.startsWith("_local.")){const x=a.slice(7),{__mergeMode:R,__setStateId:M,...O}=A,L=O.value!==void 0?O.value:Object.values(O)[0];if(r.setState){const G=window.__g7PendingLocalState||r.state||{},B=this.createNestedUpdate(x,L,G),K={...G,...B};if(r.setState(B),this.globalStateUpdater&&i!=="parent"&&i!=="root"){const fe=window.__templateApp?.getGlobalState?.()?._local,Ce=fe&&typeof fe=="object"?{...fe,...B}:K;this.globalStateUpdater({_local:Ce},{render:!1}),j.log("[handleSetState] _global._local synced for dot notation (render:false):",Ce)}return S&&f&&setTimeout(()=>f.completeStateChange(S),0),K}else throw S&&f&&f.completeStateChange(S),new It("setState function is not provided in context")}else{if(!r.setState)throw S&&f&&f.completeStateChange(S),new It("setState function is not provided in context");const R=window.__g7PendingLocalState||r.state||{},M=v==="deep"?this.deepMergeWithState(A,R):A;return r.setState(M),S&&f&&setTimeout(()=>f.completeStateChange(S),0),M}}async handleParentScopeSetState(e,r,a,i){const l=Vn(),c=window.G7Core,d=a==="replace"?"replace":a==="shallow"?"shallow":"deep",f=e.startsWith("$parent."),g=f?"$parent.":"$root.",m=e.slice(g.length);let y,w;if(m==="_local"||m==="_global")y=m,w=void 0;else if(m.startsWith("_local."))y="_local",w=m.slice(7);else if(m.startsWith("_global."))y="_global",w=m.slice(8);else{j.warn(`[handleParentScopeSetState] 지원하지 않는 타겟 형식: ${e}. _local 또는 _global으로 시작해야 합니다.`);return}const S=window.__g7LayoutContextStack||[];if(S.length===0){j.warn(`[handleParentScopeSetState] 레이아웃 컨텍스트 스택이 비어있습니다. target=${e}`);return}const v=f?S[S.length-1]:S[0];if(!v){j.warn(`[handleParentScopeSetState] 타겟 컨텍스트를 찾을 수 없습니다. target=${e}`);return}const A=y==="_global"?v.state?._global:v.state?._local,x=l?.isEnabled()?l.startStateChange(e,A,r,{actionId:i.actionId,handlerType:"setState",source:`target=${e}`}):void 0,{__mergeMode:R,__setStateId:M,...O}=r;if(y==="_global"){if(!this.globalStateUpdater){j.warn("[handleParentScopeSetState] globalStateUpdater가 설정되지 않았습니다."),x&&l&&l.completeStateChange(x);return}const L=c?.state?.get()||{};if(w){const q=O.value!==void 0?O.value:Object.values(O)[0],G=this.createNestedUpdate(w,q,L),B={...L,...G};j.log(`[handleParentScopeSetState] ${e}: 전역 상태 중첩 경로 업데이트`,B),this.globalStateUpdater(B)}else{const q=d==="deep"?this.deepMergeWithState(O,L):O;j.log(`[handleParentScopeSetState] ${e}: 전역 상태 업데이트 (mergeMode=${d})`,q),this.globalStateUpdater(d!=="deep"?{...q,__mergeMode:d}:q)}return x&&l&&setTimeout(()=>l.completeStateChange(x),0),{__target:e,...O}}else{if(!v.setState){j.warn(`[handleParentScopeSetState] 타겟 컨텍스트에 setState가 없습니다. target=${e}`),x&&l&&l.completeStateChange(x);return}const L=v.state||{};if(w){const q=O.value!==void 0?O.value:Object.values(O)[0],G=this.createNestedUpdate(w,q,L),B={...L,...G};return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 중첩 경로 업데이트`,G),v.setState(G),v.state=B,v.dataContext&&(v.dataContext._local=B),Nd(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),B}else{const q=d!=="deep"?{...O,__mergeMode:d}:O,G=d==="replace"?O:d==="shallow"?{...L,...O}:this.deepMergeWithState(O,L);return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 업데이트 (mergeMode=${d})`,G),v.setState(q),v.state=G,v.dataContext&&(v.dataContext._local=G),Nd(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),G}}}deepMergeWithState(e,r){const a=["errors"];let i=e;if("..."in e){const c=e["..."];if(c&&typeof c=="object"&&!Array.isArray(c)){const{"...":d,...f}=e;i={...c,...f}}else{const{"...":d,...f}=e;i=f}}const l={...r};for(const[c,d]of Object.entries(i)){if(a.includes(c)){l[c]=d;continue}if(c.includes(".")&&c!=="..."){const f=this.createNestedUpdate(c,d,l);this.deepMergeInto(l,f,l);continue}if(d!==null&&typeof d=="object"&&!Array.isArray(d)&&Object.getPrototypeOf(d)!==Object.prototype&&Object.getPrototypeOf(d)!==null){l[c]=d;continue}d!==null&&typeof d=="object"&&!Array.isArray(d)&&r[c]!==null&&typeof r[c]=="object"&&!Array.isArray(r[c])?l[c]=this.deepMergeWithState(d,r[c]):l[c]=d}return l}deepMergeInto(e,r,a){for(const[i,l]of Object.entries(r)){if(l===null){e[i]=l;continue}if(e[i]===null){e[i]=l;continue}typeof l=="object"&&!Array.isArray(l)&&e[i]!==void 0&&e[i]!==null&&typeof e[i]=="object"&&!Array.isArray(e[i])?(e[i]===a[i]&&(e[i]={...e[i]}),this.deepMergeInto(e[i],l,a[i]||{})):typeof l=="object"&&!Array.isArray(l)&&e[i]===void 0&&a[i]!==null&&typeof a[i]=="object"&&!Array.isArray(a[i])?e[i]={...a[i],...l}:e[i]=l}}createNestedUpdate(e,r,a){const i=e.split(".");if(i.length===1)return{[i[0]]:r};const l={};let c=l,d=a;for(let f=0;f<i.length-1;f++){const g=i[f],m=d?.[g],y=m!=null&&typeof m=="object"&&!Array.isArray(m);y?c[g]={...m}:c[g]={},c=c[g],d=y?m:{}}return c[i[i.length-1]]=r,l}buildNestedUpdate(e,r){const a=e.split(".");if(a.length===1)return{[a[0]]:r};const i={};let l=i;for(let c=0;c<a.length-1;c++)l[a[c]]={},l=l[a[c]];return l[a[a.length-1]]=r,i}async handleSetError(e,r,a){let i=e;i=this.resolveValue(i,a.data),this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,a.data));const l=r?.stateTarget||"local";l==="isolated"&&a.isolatedContext?(a.isolatedContext.mergeState({apiError:i}),j.log("[handleSetError] Error set to isolated state:",i)):l==="global"&&this.globalStateUpdater?(this.globalStateUpdater({apiError:i}),j.log("[handleSetError] Error set to global state:",i)):a.setState?(a.setState({apiError:i}),j.log("[handleSetError] Error set to local state:",i)):j.warn("[handleSetError] Cannot set error: no state updater available")}async handleOpenModal(e,r){if(!this.globalStateUpdater){j.warn("Global state updater is not set for openModal");return}const a=r.data?._global?.modalStack||[];if(a.includes(e)){j.warn(`Modal "${e}" is already open`);return}const i=window.G7Core,l=r.setState&&r.state!==void 0,c=l?void 0:i?.state?.getLocal?.()||{},d=!l&&c!==void 0&&this.globalStateUpdater;if(l||d){const m=window.__g7LayoutContextStack||[],y=l?r.state:c,w=l?r.setState:S=>{this.globalStateUpdater({_local:S})};m.push({state:y,setState:w,dataContext:{_local:y||r.data?._local,_global:l?r.data?._global:r.data?._global||i?.state?.get?.(),_computed:r.data?._computed}}),window.__g7LayoutContextStack=m,j.log(`[handleOpenModal] 레이아웃 컨텍스트 스택에 push, 스택 크기: ${m.length}, state:`,y,l?"(direct)":"(fallback via G7Core)")}else j.warn(`[handleOpenModal] 컨텍스트 스택에 push 실패 - setState: ${!!r.setState}, state: ${r.state!==void 0}, G7Core: ${!!i?.state?.getLocal}`);const f=[...a,e];this.globalStateUpdater({modalStack:f,activeModal:e});const g=Vn();if(g?.isEnabled()){const m=a.length>0?a[a.length-1]:void 0;g.trackModalOpen?.({modalId:e,modalName:e,scopeType:"isolated",parentModalId:m,initialState:r.state?{...r.state}:{}})}}async handleCloseModal(e){if(!this.globalStateUpdater){j.warn("Global state updater is not set for closeModal");return}const r=e?.data?._global?.modalStack||[],a=r.length>0?r[r.length-1]:null,i=r.slice(0,-1),l=i.length>0?i[i.length-1]:null,c=window.__g7LayoutContextStack||[];if(c.length>0){const d=c[c.length-1];d&&(d.dataContext=void 0),c.pop(),window.__g7LayoutContextStack=c,j.log(`[handleCloseModal] 레이아웃 컨텍스트 스택에서 pop, 스택 크기: ${c.length}`)}if(a){const d=Vn();d?.isEnabled()&&d.trackModalClose?.(a,e?.state)}this.globalStateUpdater({modalStack:i,activeModal:l})}async handleShowAlert(e,r){let a=e;this.translationEngine&&this.translationContext&&e.startsWith("$t:")&&(a=this.translationEngine.resolveTranslations(e,this.translationContext,r.data)),alert(a)}async handleToast(e,r){const{type:a="info",message:i,icon:l,duration:c}=e;if(a==="error"){const f=r.data?.error;if((f?.error_code==="identity_verification_required"||f?.data?.error_code==="identity_verification_required")&&mt.consumeDomainNoticeShown()){j.log("[Toast] IDV 도메인 안내 표출됨 — 중복 가드 토스트 1건 skip");return}}let d=i;if(this.translationEngine&&this.translationContext&&i?.startsWith("$t:")&&(d=this.translationEngine.resolveTranslations(i,this.translationContext,r.data)),this.globalStateUpdater){const g={id:`toast_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,type:a,message:d,...l&&{icon:l},...c&&{duration:c}},S=[...window.G7Core?.state?.get()?.toasts||[],g];this.globalStateUpdater({toasts:S})}else j.log(`[Toast ${a}] ${d}`)}async handleSwitch(e,r){const{cases:a}=e;if(!a){j.warn("switch handler requires cases property");return}const i=this.resolveParams(e.params,r.data);let l;if(i?.value!==void 0?(l=i.value,j.log("switch handler: using params.value as case key:",l)):(l=r.data?.$args?.[0],j.log("switch handler: using $args[0] as case key:",l)),l==null){if(a.default)return j.log("switch handler: case key is undefined, using default case"),await this.executeAction(a.default,r);j.warn("switch handler: case key is not defined and no default case");return}const c=String(l);let d=a[c];if(!d&&a.default&&(j.log(`switch handler: no case found for key "${c}", using default case`),d=a.default),!d){j.warn(`switch handler: no case found for key "${c}" and no default case`);return}return await this.executeAction(d,r)}async handleConditions(e,r){const{conditions:a}=e;if(!a||!Array.isArray(a)){j.warn("conditions handler requires conditions array");return}const i=r.data||{},l=sp(a,i,this.bindingEngine);if(!l.matched){j.log("conditions handler: no matching branch found");return}const c=a[l.branchIndex];if(j.log(`conditions handler: matched branch ${l.branchIndex}`),!c.then){j.warn('conditions handler: matched branch has no "then" action');return}return Array.isArray(c.then)?await this.handleSequence({...e,actions:c.then},r):await this.executeAction(c.then,r)}handleStartInterval(e,r){const a=typeof e.id=="string"?e.id:"",i=Number(e.intervalMs??1e3),l=Array.isArray(e.actions)?e.actions:[];if(a===""||!l.length||i<=0)return j.warn("startInterval requires non-empty id, positive intervalMs, and actions array"),{success:!1};const c=this.intervals.get(a);c&&(clearInterval(c),this.intervals.delete(a));const d=setInterval(()=>{for(const f of l)this.executeAction(f,r).catch(g=>{j.error(`[startInterval:${a}] tick action failed`,g)})},i);return this.intervals.set(a,d),{success:!0,id:a}}handleStopInterval(e){const r=typeof e.id=="string"?e.id:"";if(r==="")return j.warn("stopInterval requires id parameter"),{success:!1};const a=this.intervals.get(r);return a&&(clearInterval(a),this.intervals.delete(r)),{success:!0,id:r}}stopAllIntervals(){for(const e of this.intervals.values())clearInterval(e);this.intervals.clear()}async handleSequence(e,r){const a=e.actions||e.params?.actions;if(!a||!Array.isArray(a)||a.length===0)return j.warn("sequence handler requires non-empty actions array"),[];const i=Vn(),l=i?.isEnabled()&&i.startSequenceExecution?.({eventType:e.type})||"";window.__g7SequenceLocalSync=void 0;const c=[];let d,f=r.state?{...r.state}:{},g=r.isolatedContext?.state?{...r.isolatedContext.state}:{},m=r.data?._computed||{};const y=window.G7Core,w=()=>({_global:y?.state?.get()||{},_local:{...f},_isolated:Object.keys(g).length>0?{...g}:void 0});for(let S=0;S<a.length;S++){const v=a[S],A=Date.now();l&&i?.isEnabled()&&i.captureSequenceActionBefore?.(l,S,v.handler||"unknown",v.params||{},w());const x={...r,state:f,data:{...r.data,_local:f,_computed:m,$computed:m,$prev:d,$results:[...c],_isolated:g},isolatedContext:r.isolatedContext?{...r.isolatedContext,state:g}:null};try{const R=await this.executeAction(v,x);if(d=R.data,c.push(R.data),v.handler==="setState"&&R.data!==void 0){if(R.data.__target==="global"){const{__target:O,__mergeMode:L,...q}=R.data;L==="replace"?f={...f,_global:q}:f={...f,_global:{...f._global||{},...q}},j.log("[handleSequence] global state synchronized (mergeMode=%s):",L||"deep",f._global)}else if(R.data.__target==="isolated"){const{__target:O,__mergeMode:L,...q}=R.data;L==="replace"?g=q:g={...g,...q},j.log("[handleSequence] isolated state synchronized (mergeMode=%s):",L||"deep",g)}else if(R.data.__target==="local"){const{__target:O,__mergeMode:L,__setStateId:q,...G}=R.data;L==="replace"?f=G:f={...f,...G},j.log("[handleSequence] local state synchronized (mergeMode=%s):",L||"deep",f)}else f=R.data,j.log("[handleSequence] state synchronized (legacy):",f);const M=r.data?._computedDefinitions;if(M&&Object.keys(M).length>0){const O={},L={...r.data,_local:f,_computed:O,$computed:O,_isolated:g};for(const[q,G]of Object.entries(M))if(typeof G=="string"){const B=G.trim();if(B.startsWith("{{")&&B.endsWith("}}"))try{const K=B.slice(2,-2).trim();O[q]=Gn(K)?this.bindingEngine.evaluatePipeExpression(K,L,{skipCache:!0}):this.bindingEngine.evaluateExpression(K,L,{skipCache:!0})}catch{O[q]=m[q]}}m=O,j.log("[handleSequence] _computed recalculated after setState:",m)}}if(v.handler!=="setState"){const M=window.__g7SequenceLocalSync;if(M&&M!==f){f=M,window.__g7SequenceLocalSync=void 0,j.log("[handleSequence] currentState synchronized from __g7SequenceLocalSync after custom handler:",v.handler);const O=r.data?._computedDefinitions;if(O&&Object.keys(O).length>0){const L={},q={...r.data,_local:f,_computed:L,$computed:L,_isolated:g};for(const[G,B]of Object.entries(O))if(typeof B=="string"){const K=B.trim();if(K.startsWith("{{")&&K.endsWith("}}"))try{const fe=K.slice(2,-2).trim();L[G]=Gn(fe)?this.bindingEngine.evaluatePipeExpression(fe,q,{skipCache:!0}):this.bindingEngine.evaluateExpression(fe,q,{skipCache:!0})}catch{L[G]=m[G]}}m=L,j.log("[handleSequence] _computed recalculated after custom handler:",m)}}}l&&i?.isEnabled()&&i.captureSequenceActionAfter?.(l,S,w(),Date.now()-A,R.data)}catch(R){throw l&&i?.isEnabled()&&(i.captureSequenceActionAfter?.(l,S,w(),Date.now()-A,void 0,R),i.endSequenceExecution?.(l,R)),j.error(`sequence handler: action[${S}] failed:`,R),R}}return window.__g7SequenceLocalSync=void 0,l&&i?.isEnabled()&&i.endSequenceExecution?.(l),c}async handleParallel(e,r){const a=e.actions||e.params?.actions;if(!a||!Array.isArray(a)||a.length===0)return j.warn("parallel handler requires non-empty actions array"),[];j.log(`parallel handler: executing ${a.length} actions in parallel`);const i=r.isolatedContext?{...r.isolatedContext,state:{...r.isolatedContext.state}}:null,l=a.map((f,g)=>{const m={...r,isolatedContext:i};return this.executeAction(f,m).catch(y=>{throw j.error(`parallel handler: action[${g}] failed:`,y),y})}),c=await Promise.allSettled(l),d=c.filter(f=>f.status==="rejected").length;return d>0&&j.warn(`parallel handler: ${d}/${a.length} actions failed`),c}async handleLoadScript(e,r,a){const{src:i,id:l,async:c=!0,defer:d=!1}=e;if(!i)throw new It('loadScript handler requires "src" parameter',r);const f=l||`script_${i.replace(/[^a-zA-Z0-9]/g,"_")}`;return fs.loadedScripts.has(f)?(j.log(`loadScript: script already loaded, skipping: ${f}`),r.onLoad&&await this.executeAction(r.onLoad,a),!0):document.getElementById(f)?(j.log(`loadScript: script element already exists: ${f}`),fs.loadedScripts.add(f),r.onLoad&&await this.executeAction(r.onLoad,a),!0):(j.log(`loadScript: loading script: ${i}`),new Promise((m,y)=>{const w=document.createElement("script");w.id=f,w.src=i,w.async=c,w.defer=d,w.onload=async()=>{if(j.log(`loadScript: script loaded successfully: ${f}`),fs.loadedScripts.add(f),r.onLoad)try{await this.executeAction(r.onLoad,a)}catch(S){j.error("loadScript: onLoad action failed:",S)}m(!0)},w.onerror=S=>{j.error(`loadScript: failed to load script: ${i}`,S),y(new It(`Failed to load script: ${i}`,r))},document.head.appendChild(w)}))}async handleCallExternal(e,r,a){const i=e.constructor,l=e.args||{},c=e.method,d=e.methodArgs||[],f=e.callbackEvent,g=e.embedTarget,m=e.callbackSetState,y=e.callbackAction;if(!i)throw new It('callExternal handler requires "constructor" parameter',r);const w=this.getNestedProperty(window,i);if(!w||typeof w!="function")throw new It(`Constructor not found or not a function: ${i}. Make sure the script is loaded first using loadScript handler.`,r);j.log(`callExternal: calling constructor ${i}`);const S={...l};for(const[A,x]of Object.entries(l))x===!0&&(S[A]=R=>{if(j.log(`callExternal: callback triggered for ${A}`,R),f&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(f,R),m&&a.setState){const M=q=>{const G={};for(const[B,K]of Object.entries(q))typeof K=="string"?G[B]=this.getNestedProperty(R,K):typeof K=="object"&&K!==null&&(G[B]=M(K));return G},O=M(m);j.log("callExternal: callbackSetState mapping result",O);const L=this.deepMergeWithState(O,a.state||{});j.log("callExternal: merged with existing state",L),a.setState(L)}else f&&a.setState&&a.setState({[`${f.replace(/:/g,"_")}_result`]:R});if(y){const M={...a,data:{...a.data,$event:R}},O=Array.isArray(y)?y:[y];for(const L of O)try{this.executeAction(L,M)}catch(q){j.error("callExternal: callbackAction failed:",q)}}});const v=new w(S);if(c&&typeof v[c]=="function"){if(j.log(`callExternal: calling method ${c}`),c==="embed"&&g){const A=document.querySelector(g);if(A)return v[c](A,...d);throw new It(`Embed target element not found: ${g}`,r)}return v[c](...d)}return v}async handleCallExternalEmbed(e,r,a){const i=e.constructor,l=e.args||{},c=e.callbackSetState,d=e.callbackEvent,f=e.layerClassName,g=e.callbackAction;if(!i)throw new It('callExternalEmbed handler requires "constructor" parameter',r);const m=this.getNestedProperty(window,i);if(!m||typeof m!="function")throw new It(`Constructor not found or not a function: ${i}. Make sure the script is loaded first.`,r);j.log(`callExternalEmbed: creating layer for ${i}`);const{layer:y,closeLayer:w}=this.createEmbedLayer(f),S={...l};for(const[A,x]of Object.entries(l))x===!0&&(S[A]=R=>{if(j.log(`callExternalEmbed: callback triggered for ${A}`,R),w(),d&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(d,R),c&&a.setState){const M=q=>{const G={};for(const[B,K]of Object.entries(q))typeof K=="string"?G[B]=this.getNestedProperty(R,K):typeof K=="object"&&K!==null&&(G[B]=M(K));return G},O=M(c);j.log("callExternalEmbed: callbackSetState mapping result",O);const L=this.deepMergeWithState(O,a.state||{});j.log("callExternalEmbed: merged with existing state",L),a.setState(L)}if(g){const M={...a,data:{...a.data,$event:R}},O=Array.isArray(g)?g:[g];for(const L of O)try{this.executeAction(L,M)}catch(q){j.error("callExternalEmbed: callbackAction failed:",q)}}});const v=new m(S);if(typeof v.embed=="function")v.embed(y),j.log("callExternalEmbed: embedded in layer");else throw w(),new It(`Constructor ${i} does not have embed method`,r);return v}isImeComposing(e){return e?.isComposing===!0||e?.keyCode===229}createEmbedLayer(e){const r=document.createElement("div");r.id="g7-embed-overlay",r.style.cssText=` +`+u.stack}}var hn=Object.prototype.hasOwnProperty,an=s.unstable_scheduleCallback,pn=s.unstable_cancelCallback,Kn=s.unstable_shouldYield,Gr=s.unstable_requestPaint,qt=s.unstable_now,_i=s.unstable_getCurrentPriorityLevel,aa=s.unstable_ImmediatePriority,gn=s.unstable_UserBlockingPriority,Rn=s.unstable_NormalPriority,nt=s.unstable_LowPriority,Nn=s.unstable_IdlePriority,mn=s.log,ia=s.unstable_setDisableYieldValue,or=null,sn=null;function _n(t){if(typeof mn=="function"&&ia(t),sn&&typeof sn.setStrictMode=="function")try{sn.setStrictMode(or,t)}catch{}}var on=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 Ee(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,o){var u=t.pendingLanes;if(u===0)return 0;var p=0,g=t.suspendedLanes,w=t.pingedLanes;t=t.warmLanes;var R=u&134217727;return R!==0?(u=R&~g,u!==0?p=Ee(u):(w&=R,w!==0?p=Ee(w):o||(o=R&~t,o!==0&&(p=Ee(o))))):(R=u&~g,R!==0?p=Ee(R):w!==0?p=Ee(w):o||(o=u&~t,o!==0&&(p=Ee(o)))),p===0?0:r!==0&&r!==p&&(r&g)===0&&(g=p&-p,o=r&-r,g>=o||g===32&&(o&4194048)!==0)?r:p}function Fe(t,r){return(t.pendingLanes&~(t.suspendedLanes&~t.pingedLanes)&r)===0}function ke(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 Re(t){for(var r=[],o=0;31>o;o++)r.push(t);return r}function et(t,r){t.pendingLanes|=r,r!==268435456&&(t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0)}function He(t,r,o,u,p,g){var w=t.pendingLanes;t.pendingLanes=o,t.suspendedLanes=0,t.pingedLanes=0,t.warmLanes=0,t.expiredLanes&=o,t.entangledLanes&=o,t.errorRecoveryDisabledLanes&=o,t.shellSuspendCounter=0;var R=t.entanglements,z=t.expirationTimes,X=t.hiddenUpdates;for(o=w&~o;0<o;){var re=31-on(o),le=1<<re;R[re]=0,z[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)}o&=~le}u!==0&&Ge(t,u,0),g!==0&&p===0&&t.tag!==0&&(t.suspendedLanes|=g&~(w&~r))}function Ge(t,r,o){t.pendingLanes|=r,t.suspendedLanes&=~r;var u=31-on(r);t.entangledLanes|=r,t.entanglements[u]=t.entanglements[u]|1073741824|o&261930}function Ce(t,r){var o=t.entangledLanes|=r;for(t=t.entanglements;o;){var u=31-on(o),p=1<<u;p&r|t[u]&r&&(t[u]|=r),o&=~p}}function je(t,r){var o=r&-r;return o=(o&42)!==0?1:tt(o),(o&(t.suspendedLanes|r))!==0?0:o}function tt(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 At(t){return t&=-t,2<t?8<t?(t&134217727)!==0?32:268435456:8:2}function Mt(){var t=fe.p;return t!==0?t:(t=window.event,t===void 0?32:tw(t.type))}function Dn(t,r){var o=fe.p;try{return fe.p=t,r()}finally{fe.p=o}}var jt=Math.random().toString(36).slice(2),Ht="__reactFiber$"+jt,An="__reactProps$"+jt,Ia="__reactContainer$"+jt,Ho="__reactEvents$"+jt,zf="__reactListeners$"+jt,Dc="__reactHandles$"+jt,Vr="__reactResources$"+jt,Ai="__reactMarker$"+jt;function zo(t){delete t[Ht],delete t[An],delete t[Ho],delete t[zf],delete t[Dc]}function ja(t){var r=t[Ht];if(r)return r;for(var o=t.parentNode;o;){if(r=o[Ia]||o[Ht]){if(o=r.alternate,r.child!==null||o!==null&&o.child!==null)for(t=zS(t);t!==null;){if(o=t[Ht])return o;t=zS(t)}return r}t=o,o=t.parentNode}return null}function Fr(t){if(t=t[Ht]||t[Ia]){var r=t.tag;if(r===5||r===6||r===13||r===31||r===26||r===27||r===3)return t}return null}function sa(t){var r=t.tag;if(r===5||r===26||r===27||r===6)return t.stateNode;throw Error(a(33))}function Ha(t){var r=t[Vr];return r||(r=t[Vr]={hoistableStyles:new Map,hoistableScripts:new Map}),r}function Jt(t){t[Ai]=!0}var Oc=new Set,Lc={};function kr(t,r){za(t,r),za(t+"Capture",r)}function za(t,r){for(Lc[t]=r,t=0;t<r.length;t++)Oc.add(r[t])}var Uf=RegExp("^[:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD][:A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040]*$"),Mc={},$c={};function Pf(t){return hn.call($c,t)?!0:hn.call(Mc,t)?!1:Uf.test(t)?$c[t]=!0:(Mc[t]=!0,!1)}function In(t,r,o){if(Pf(r))if(o===null)t.removeAttribute(r);else{switch(typeof o){case"undefined":case"function":case"symbol":t.removeAttribute(r);return;case"boolean":var u=r.toLowerCase().slice(0,5);if(u!=="data-"&&u!=="aria-"){t.removeAttribute(r);return}}t.setAttribute(r,""+o)}}function xi(t,r,o){if(o===null)t.removeAttribute(r);else{switch(typeof o){case"undefined":case"function":case"symbol":case"boolean":t.removeAttribute(r);return}t.setAttribute(r,""+o)}}function yn(t,r,o,u){if(u===null)t.removeAttribute(o);else{switch(typeof u){case"undefined":case"function":case"symbol":case"boolean":t.removeAttribute(o);return}t.setAttributeNS(r,o,""+u)}}function xn(t){switch(typeof t){case"bigint":case"boolean":case"number":case"string":case"undefined":return t;case"object":return t;default:return""}}function Nc(t){var r=t.type;return(t=t.nodeName)&&t.toLowerCase()==="input"&&(r==="checkbox"||r==="radio")}function Bf(t,r,o){var u=Object.getOwnPropertyDescriptor(t.constructor.prototype,r);if(!t.hasOwnProperty(r)&&typeof u<"u"&&typeof u.get=="function"&&typeof u.set=="function"){var p=u.get,g=u.set;return Object.defineProperty(t,r,{configurable:!0,get:function(){return p.call(this)},set:function(w){o=""+w,g.call(this,w)}}),Object.defineProperty(t,r,{enumerable:u.enumerable}),{getValue:function(){return o},setValue:function(w){o=""+w},stopTracking:function(){t._valueTracker=null,delete t[r]}}}}function Uo(t){if(!t._valueTracker){var r=Nc(t)?"checked":"value";t._valueTracker=Bf(t,r,""+t[r])}}function Ic(t){if(!t)return!1;var r=t._valueTracker;if(!r)return!0;var o=r.getValue(),u="";return t&&(u=Nc(t)?t.checked?"true":"false":t.value),t=u,t!==o?(r.setValue(t),!0):!1}function Ua(t){if(t=t||(typeof document<"u"?document:void 0),typeof t>"u")return null;try{return t.activeElement||t.body}catch{return t.body}}var qf=/[\n"\\]/g;function jn(t){return t.replace(qf,function(r){return"\\"+r.charCodeAt(0).toString(16)+" "})}function vs(t,r,o,u,p,g,w,R){t.name="",w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"?t.type=w:t.removeAttribute("type"),r!=null?w==="number"?(r===0&&t.value===""||t.value!=r)&&(t.value=""+xn(r)):t.value!==""+xn(r)&&(t.value=""+xn(r)):w!=="submit"&&w!=="reset"||t.removeAttribute("value"),r!=null?oa(t,w,xn(r)):o!=null?oa(t,w,xn(o)):u!=null&&t.removeAttribute("value"),p==null&&g!=null&&(t.defaultChecked=!!g),p!=null&&(t.checked=p&&typeof p!="function"&&typeof p!="symbol"),R!=null&&typeof R!="function"&&typeof R!="symbol"&&typeof R!="boolean"?t.name=""+xn(R):t.removeAttribute("name")}function Ti(t,r,o,u,p,g,w,R){if(g!=null&&typeof g!="function"&&typeof g!="symbol"&&typeof g!="boolean"&&(t.type=g),r!=null||o!=null){if(!(g!=="submit"&&g!=="reset"||r!=null)){Uo(t);return}o=o!=null?""+xn(o):"",r=r!=null?""+xn(r):o,R||r===t.value||(t.value=r),t.defaultValue=r}u=u??p,u=typeof u!="function"&&typeof u!="symbol"&&!!u,t.checked=R?t.checked:!!u,t.defaultChecked=!!u,w!=null&&typeof w!="function"&&typeof w!="symbol"&&typeof w!="boolean"&&(t.name=w),Uo(t)}function oa(t,r,o){r==="number"&&Ua(t.ownerDocument)===t||t.defaultValue===""+o||(t.defaultValue=""+o)}function Pa(t,r,o,u){if(t=t.options,r){r={};for(var p=0;p<o.length;p++)r["$"+o[p]]=!0;for(o=0;o<t.length;o++)p=r.hasOwnProperty("$"+t[o].value),t[o].selected!==p&&(t[o].selected=p),p&&u&&(t[o].defaultSelected=!0)}else{for(o=""+xn(o),r=null,p=0;p<t.length;p++){if(t[p].value===o){t[p].selected=!0,u&&(t[p].defaultSelected=!0);return}r!==null||t[p].disabled||(r=t[p])}r!==null&&(r.selected=!0)}}function Ba(t,r,o){if(r!=null&&(r=""+xn(r),r!==t.value&&(t.value=r),o==null)){t.defaultValue!==r&&(t.defaultValue=r);return}t.defaultValue=o!=null?""+xn(o):""}function jc(t,r,o,u){if(r==null){if(u!=null){if(o!=null)throw Error(a(92));if(wt(u)){if(1<u.length)throw Error(a(93));u=u[0]}o=u}o==null&&(o=""),r=o}o=xn(r),t.defaultValue=o,u=t.textContent,u===o&&u!==""&&u!==null&&(t.value=u),Uo(t)}function qa(t,r){if(r){var o=t.firstChild;if(o&&o===t.lastChild&&o.nodeType===3){o.nodeValue=r;return}}t.textContent=r}var Gf=new Set("animationIterationCount aspectRatio borderImageOutset borderImageSlice borderImageWidth boxFlex boxFlexGroup boxOrdinalGroup columnCount columns flex flexGrow flexPositive flexShrink flexNegative flexOrder gridArea gridRow gridRowEnd gridRowSpan gridRowStart gridColumn gridColumnEnd gridColumnSpan gridColumnStart fontWeight lineClamp lineHeight opacity order orphans scale tabSize widows zIndex zoom fillOpacity floodOpacity stopOpacity strokeDasharray strokeDashoffset strokeMiterlimit strokeOpacity strokeWidth MozAnimationIterationCount MozBoxFlex MozBoxFlexGroup MozLineClamp msAnimationIterationCount msFlex msZoom msFlexGrow msFlexNegative msFlexOrder msFlexPositive msFlexShrink msGridColumn msGridColumnSpan msGridRow msGridRowSpan WebkitAnimationIterationCount WebkitBoxFlex WebKitBoxFlexGroup WebkitBoxOrdinalGroup WebkitColumnCount WebkitColumns WebkitFlex WebkitFlexGrow WebkitFlexPositive WebkitFlexShrink WebkitLineClamp".split(" "));function Hc(t,r,o){var u=r.indexOf("--")===0;o==null||typeof o=="boolean"||o===""?u?t.setProperty(r,""):r==="float"?t.cssFloat="":t[r]="":u?t.setProperty(r,o):typeof o!="number"||o===0||Gf.has(r)?r==="float"?t.cssFloat=o:t[r]=(""+o).trim():t[r]=o+"px"}function zc(t,r,o){if(r!=null&&typeof r!="object")throw Error(a(62));if(t=t.style,o!=null){for(var u in o)!o.hasOwnProperty(u)||r!=null&&r.hasOwnProperty(u)||(u.indexOf("--")===0?t.setProperty(u,""):u==="float"?t.cssFloat="":t[u]="");for(var p in r)u=r[p],r.hasOwnProperty(p)&&o[p]!==u&&Hc(t,p,u)}else for(var g in r)r.hasOwnProperty(g)&&Hc(t,g,r[g])}function Po(t){if(t.indexOf("-")===-1)return!1;switch(t){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var Vf=new Map([["acceptCharset","accept-charset"],["htmlFor","for"],["httpEquiv","http-equiv"],["crossOrigin","crossorigin"],["accentHeight","accent-height"],["alignmentBaseline","alignment-baseline"],["arabicForm","arabic-form"],["baselineShift","baseline-shift"],["capHeight","cap-height"],["clipPath","clip-path"],["clipRule","clip-rule"],["colorInterpolation","color-interpolation"],["colorInterpolationFilters","color-interpolation-filters"],["colorProfile","color-profile"],["colorRendering","color-rendering"],["dominantBaseline","dominant-baseline"],["enableBackground","enable-background"],["fillOpacity","fill-opacity"],["fillRule","fill-rule"],["floodColor","flood-color"],["floodOpacity","flood-opacity"],["fontFamily","font-family"],["fontSize","font-size"],["fontSizeAdjust","font-size-adjust"],["fontStretch","font-stretch"],["fontStyle","font-style"],["fontVariant","font-variant"],["fontWeight","font-weight"],["glyphName","glyph-name"],["glyphOrientationHorizontal","glyph-orientation-horizontal"],["glyphOrientationVertical","glyph-orientation-vertical"],["horizAdvX","horiz-adv-x"],["horizOriginX","horiz-origin-x"],["imageRendering","image-rendering"],["letterSpacing","letter-spacing"],["lightingColor","lighting-color"],["markerEnd","marker-end"],["markerMid","marker-mid"],["markerStart","marker-start"],["overlinePosition","overline-position"],["overlineThickness","overline-thickness"],["paintOrder","paint-order"],["panose-1","panose-1"],["pointerEvents","pointer-events"],["renderingIntent","rendering-intent"],["shapeRendering","shape-rendering"],["stopColor","stop-color"],["stopOpacity","stop-opacity"],["strikethroughPosition","strikethrough-position"],["strikethroughThickness","strikethrough-thickness"],["strokeDasharray","stroke-dasharray"],["strokeDashoffset","stroke-dashoffset"],["strokeLinecap","stroke-linecap"],["strokeLinejoin","stroke-linejoin"],["strokeMiterlimit","stroke-miterlimit"],["strokeOpacity","stroke-opacity"],["strokeWidth","stroke-width"],["textAnchor","text-anchor"],["textDecoration","text-decoration"],["textRendering","text-rendering"],["transformOrigin","transform-origin"],["underlinePosition","underline-position"],["underlineThickness","underline-thickness"],["unicodeBidi","unicode-bidi"],["unicodeRange","unicode-range"],["unitsPerEm","units-per-em"],["vAlphabetic","v-alphabetic"],["vHanging","v-hanging"],["vIdeographic","v-ideographic"],["vMathematical","v-mathematical"],["vectorEffect","vector-effect"],["vertAdvY","vert-adv-y"],["vertOriginX","vert-origin-x"],["vertOriginY","vert-origin-y"],["wordSpacing","word-spacing"],["writingMode","writing-mode"],["xmlnsXlink","xmlns:xlink"],["xHeight","x-height"]]),Bo=/^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;function lr(t){return Bo.test(""+t)?"javascript:throw new Error('React has blocked a javascript: URL as a security precaution.')":t}function Rr(){}var qo=null;function Go(t){return t=t.target||t.srcElement||window,t.correspondingUseElement&&(t=t.correspondingUseElement),t.nodeType===3?t.parentNode:t}var Ga=null,la=null;function Uc(t){var r=Fr(t);if(r&&(t=r.stateNode)){var o=t[An]||null;e:switch(t=r.stateNode,r.type){case"input":if(vs(t,o.value,o.defaultValue,o.defaultValue,o.checked,o.defaultChecked,o.type,o.name),r=o.name,o.type==="radio"&&r!=null){for(o=t;o.parentNode;)o=o.parentNode;for(o=o.querySelectorAll('input[name="'+jn(""+r)+'"][type="radio"]'),r=0;r<o.length;r++){var u=o[r];if(u!==t&&u.form===t.form){var p=u[An]||null;if(!p)throw Error(a(90));vs(u,p.value,p.defaultValue,p.defaultValue,p.checked,p.defaultChecked,p.type,p.name)}}for(r=0;r<o.length;r++)u=o[r],u.form===t.form&&Ic(u)}break e;case"textarea":Ba(t,o.value,o.defaultValue);break e;case"select":r=o.value,r!=null&&Pa(t,!!o.multiple,r,!1)}}}var Ss=!1;function Pc(t,r,o){if(Ss)return t(r,o);Ss=!0;try{var u=t(r);return u}finally{if(Ss=!1,(Ga!==null||la!==null)&&($u(),Ga&&(r=Ga,t=la,la=Ga=null,Uc(r),t)))for(r=0;r<t.length;r++)Uc(t[r])}}function ki(t,r){var o=t.stateNode;if(o===null)return null;var u=o[An]||null;if(u===null)return null;o=u[r];e:switch(r){case"onClick":case"onClickCapture":case"onDoubleClick":case"onDoubleClickCapture":case"onMouseDown":case"onMouseDownCapture":case"onMouseMove":case"onMouseMoveCapture":case"onMouseUp":case"onMouseUpCapture":case"onMouseEnter":(u=!u.disabled)||(t=t.type,u=!(t==="button"||t==="input"||t==="select"||t==="textarea")),t=!u;break e;default:t=!1}if(t)return null;if(o&&typeof o!="function")throw Error(a(231,r,typeof o));return o}var Dr=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Vo=!1;if(Dr)try{var Ri={};Object.defineProperty(Ri,"passive",{get:function(){Vo=!0}}),window.addEventListener("test",Ri,Ri),window.removeEventListener("test",Ri,Ri)}catch{Vo=!1}var Kr=null,Fo=null,ws=null;function Bc(){if(ws)return ws;var t,r=Fo,o=r.length,u,p="value"in Kr?Kr.value:Kr.textContent,g=p.length;for(t=0;t<o&&r[t]===p[t];t++);var w=o-t;for(u=1;u<=w&&r[o-u]===p[g-u];u++);return ws=p.slice(t,1<u?1-u:void 0)}function Di(t){var r=t.keyCode;return"charCode"in t?(t=t.charCode,t===0&&r===13&&(t=13)):t=r,t===10&&(t=13),32<=t||t===13?t:0}function Ko(){return!0}function qc(){return!1}function ze(t){function r(o,u,p,g,w){this._reactName=o,this._targetInst=p,this.type=u,this.nativeEvent=g,this.target=w,this.currentTarget=null;for(var R in t)t.hasOwnProperty(R)&&(o=t[R],this[R]=o?o(g):g[R]);return this.isDefaultPrevented=(g.defaultPrevented!=null?g.defaultPrevented:g.returnValue===!1)?Ko:qc,this.isPropagationStopped=qc,this}return b(r.prototype,{preventDefault:function(){this.defaultPrevented=!0;var o=this.nativeEvent;o&&(o.preventDefault?o.preventDefault():typeof o.returnValue!="unknown"&&(o.returnValue=!1),this.isDefaultPrevented=Ko)},stopPropagation:function(){var o=this.nativeEvent;o&&(o.stopPropagation?o.stopPropagation():typeof o.cancelBubble!="unknown"&&(o.cancelBubble=!0),this.isPropagationStopped=Ko)},persist:function(){},isPersistent:Ko}),r}var Or={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(t){return t.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},ca=ze(Or),Oi=b({},Or,{view:0,detail:0}),Ff=ze(Oi),Cs,Wo,Li,Es=b({},Oi,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:Xo,button:0,buttons:0,relatedTarget:function(t){return t.relatedTarget===void 0?t.fromElement===t.srcElement?t.toElement:t.fromElement:t.relatedTarget},movementX:function(t){return"movementX"in t?t.movementX:(t!==Li&&(Li&&t.type==="mousemove"?(Cs=t.screenX-Li.screenX,Wo=t.screenY-Li.screenY):Wo=Cs=0,Li=t),Cs)},movementY:function(t){return"movementY"in t?t.movementY:Wo}}),Gc=ze(Es),Kf=b({},Es,{dataTransfer:0}),Ky=ze(Kf),Wf=b({},Oi,{relatedTarget:0}),Yo=ze(Wf),Wy=b({},Or,{animationName:0,elapsedTime:0,pseudoElement:0}),Yf=ze(Wy),Xf=b({},Or,{clipboardData:function(t){return"clipboardData"in t?t.clipboardData:window.clipboardData}}),Vc=ze(Xf),Jf=b({},Or,{data:0}),Fc=ze(Jf),Qf={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},Zf={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},eh={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"};function Kc(t){var r=this.nativeEvent;return r.getModifierState?r.getModifierState(t):(t=eh[t])?!!r[t]:!1}function Xo(){return Kc}var th=b({},Oi,{key:function(t){if(t.key){var r=Qf[t.key]||t.key;if(r!=="Unidentified")return r}return t.type==="keypress"?(t=Di(t),t===13?"Enter":String.fromCharCode(t)):t.type==="keydown"||t.type==="keyup"?Zf[t.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:Xo,charCode:function(t){return t.type==="keypress"?Di(t):0},keyCode:function(t){return t.type==="keydown"||t.type==="keyup"?t.keyCode:0},which:function(t){return t.type==="keypress"?Di(t):t.type==="keydown"||t.type==="keyup"?t.keyCode:0}}),nh=ze(th),rh=b({},Es,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0}),Wc=ze(rh),ah=b({},Oi,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:Xo}),ih=ze(ah),ln=b({},Or,{propertyName:0,elapsedTime:0,pseudoElement:0}),_s=ze(ln),sh=b({},Es,{deltaX:function(t){return"deltaX"in t?t.deltaX:"wheelDeltaX"in t?-t.wheelDeltaX:0},deltaY:function(t){return"deltaY"in t?t.deltaY:"wheelDeltaY"in t?-t.wheelDeltaY:"wheelDelta"in t?-t.wheelDelta:0},deltaZ:0,deltaMode:0}),E=ze(sh),y=b({},Or,{newState:0,oldState:0}),C=ze(y),D=[9,13,27,32],I=Dr&&"CompositionEvent"in window,ne=null;Dr&&"documentMode"in document&&(ne=document.documentMode);var me=Dr&&"TextEvent"in window&&!ne,Me=Dr&&(!I||ne&&8<ne&&11>=ne),Ke=" ",ct=!1;function gt(t,r){switch(t){case"keyup":return D.indexOf(r.keyCode)!==-1;case"keydown":return r.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function zt(t){return t=t.detail,typeof t=="object"&&"data"in t?t.data:null}var Lr=!1;function oh(t,r){switch(t){case"compositionend":return zt(r);case"keypress":return r.which!==32?null:(ct=!0,Ke);case"textInput":return t=r.data,t===Ke&&ct?null:t;default:return null}}function Yc(t,r){if(Lr)return t==="compositionend"||!I&>(t,r)?(t=Bc(),ws=Fo=Kr=null,Lr=!1,t):null;switch(t){case"paste":return null;case"keypress":if(!(r.ctrlKey||r.altKey||r.metaKey)||r.ctrlKey&&r.altKey){if(r.char&&1<r.char.length)return r.char;if(r.which)return String.fromCharCode(r.which)}return null;case"compositionend":return Me&&r.locale!=="ko"?null:r.data;default:return null}}var lh={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Jo(t){var r=t&&t.nodeName&&t.nodeName.toLowerCase();return r==="input"?!!lh[t.type]:r==="textarea"}function Xc(t,r,o,u){Ga?la?la.push(u):la=[u]:Ga=u,r=Pu(r,"onChange"),0<r.length&&(o=new ca("onChange","change",null,o,u),t.push({event:o,listeners:r}))}var Va=null,Mi=null;function ch(t){AS(t,0)}function $i(t){var r=sa(t);if(Ic(r))return t}function Qo(t,r){if(t==="change")return r}var Zo=!1;if(Dr){var Ni;if(Dr){var Ii="oninput"in document;if(!Ii){var el=document.createElement("div");el.setAttribute("oninput","return;"),Ii=typeof el.oninput=="function"}Ni=Ii}else Ni=!1;Zo=Ni&&(!document.documentMode||9<document.documentMode)}function Jc(){Va&&(Va.detachEvent("onpropertychange",Qc),Mi=Va=null)}function Qc(t){if(t.propertyName==="value"&&$i(Mi)){var r=[];Xc(r,Mi,t,Go(t)),Pc(ch,r)}}function uh(t,r,o){t==="focusin"?(Jc(),Va=r,Mi=o,Va.attachEvent("onpropertychange",Qc)):t==="focusout"&&Jc()}function dh(t){if(t==="selectionchange"||t==="keyup"||t==="keydown")return $i(Mi)}function Zc(t,r){if(t==="click")return $i(r)}function mx(t,r){if(t==="input"||t==="change")return $i(r)}function yx(t,r){return t===r&&(t!==0||1/t===1/r)||t!==t&&r!==r}var Wn=typeof Object.is=="function"?Object.is:yx;function tl(t,r){if(Wn(t,r))return!0;if(typeof t!="object"||t===null||typeof r!="object"||r===null)return!1;var o=Object.keys(t),u=Object.keys(r);if(o.length!==u.length)return!1;for(u=0;u<o.length;u++){var p=o[u];if(!hn.call(r,p)||!Wn(t[p],r[p]))return!1}return!0}function Yy(t){for(;t&&t.firstChild;)t=t.firstChild;return t}function Xy(t,r){var o=Yy(t);t=0;for(var u;o;){if(o.nodeType===3){if(u=t+o.textContent.length,t<=r&&u>=r)return{node:o,offset:r-t};t=u}e:{for(;o;){if(o.nextSibling){o=o.nextSibling;break e}o=o.parentNode}o=void 0}o=Yy(o)}}function Jy(t,r){return t&&r?t===r?!0:t&&t.nodeType===3?!1:r&&r.nodeType===3?Jy(t,r.parentNode):"contains"in t?t.contains(r):t.compareDocumentPosition?!!(t.compareDocumentPosition(r)&16):!1:!1}function Qy(t){t=t!=null&&t.ownerDocument!=null&&t.ownerDocument.defaultView!=null?t.ownerDocument.defaultView:window;for(var r=Ua(t.document);r instanceof t.HTMLIFrameElement;){try{var o=typeof r.contentWindow.location.href=="string"}catch{o=!1}if(o)t=r.contentWindow;else break;r=Ua(t.document)}return r}function fh(t){var r=t&&t.nodeName&&t.nodeName.toLowerCase();return r&&(r==="input"&&(t.type==="text"||t.type==="search"||t.type==="tel"||t.type==="url"||t.type==="password")||r==="textarea"||t.contentEditable==="true")}var bx=Dr&&"documentMode"in document&&11>=document.documentMode,As=null,hh=null,nl=null,ph=!1;function Zy(t,r,o){var u=o.window===o?o.document:o.nodeType===9?o:o.ownerDocument;ph||As==null||As!==Ua(u)||(u=As,"selectionStart"in u&&fh(u)?u={start:u.selectionStart,end:u.selectionEnd}:(u=(u.ownerDocument&&u.ownerDocument.defaultView||window).getSelection(),u={anchorNode:u.anchorNode,anchorOffset:u.anchorOffset,focusNode:u.focusNode,focusOffset:u.focusOffset}),nl&&tl(nl,u)||(nl=u,u=Pu(hh,"onSelect"),0<u.length&&(r=new ca("onSelect","select",null,r,o),t.push({event:r,listeners:u}),r.target=As)))}function ji(t,r){var o={};return o[t.toLowerCase()]=r.toLowerCase(),o["Webkit"+t]="webkit"+r,o["Moz"+t]="moz"+r,o}var xs={animationend:ji("Animation","AnimationEnd"),animationiteration:ji("Animation","AnimationIteration"),animationstart:ji("Animation","AnimationStart"),transitionrun:ji("Transition","TransitionRun"),transitionstart:ji("Transition","TransitionStart"),transitioncancel:ji("Transition","TransitionCancel"),transitionend:ji("Transition","TransitionEnd")},gh={},eb={};Dr&&(eb=document.createElement("div").style,"AnimationEvent"in window||(delete xs.animationend.animation,delete xs.animationiteration.animation,delete xs.animationstart.animation),"TransitionEvent"in window||delete xs.transitionend.transition);function Hi(t){if(gh[t])return gh[t];if(!xs[t])return t;var r=xs[t],o;for(o in r)if(r.hasOwnProperty(o)&&o in eb)return gh[t]=r[o];return t}var tb=Hi("animationend"),nb=Hi("animationiteration"),rb=Hi("animationstart"),vx=Hi("transitionrun"),Sx=Hi("transitionstart"),wx=Hi("transitioncancel"),ab=Hi("transitionend"),ib=new Map,mh="abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");mh.push("scrollEnd");function Mr(t,r){ib.set(t,r),kr(r,[t])}var eu=typeof reportError=="function"?reportError:function(t){if(typeof window=="object"&&typeof window.ErrorEvent=="function"){var r=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:typeof t=="object"&&t!==null&&typeof t.message=="string"?String(t.message):String(t),error:t});if(!window.dispatchEvent(r))return}else if(typeof process=="object"&&typeof process.emit=="function"){process.emit("uncaughtException",t);return}console.error(t)},cr=[],Ts=0,yh=0;function tu(){for(var t=Ts,r=yh=Ts=0;r<t;){var o=cr[r];cr[r++]=null;var u=cr[r];cr[r++]=null;var p=cr[r];cr[r++]=null;var g=cr[r];if(cr[r++]=null,u!==null&&p!==null){var w=u.pending;w===null?p.next=p:(p.next=w.next,w.next=p),u.pending=p}g!==0&&sb(o,p,g)}}function nu(t,r,o,u){cr[Ts++]=t,cr[Ts++]=r,cr[Ts++]=o,cr[Ts++]=u,yh|=u,t.lanes|=u,t=t.alternate,t!==null&&(t.lanes|=u)}function bh(t,r,o,u){return nu(t,r,o,u),ru(t)}function zi(t,r){return nu(t,null,null,r),ru(t)}function sb(t,r,o){t.lanes|=o;var u=t.alternate;u!==null&&(u.lanes|=o);for(var p=!1,g=t.return;g!==null;)g.childLanes|=o,u=g.alternate,u!==null&&(u.childLanes|=o),g.tag===22&&(t=g.stateNode,t===null||t._visibility&1||(p=!0)),t=g,g=g.return;return t.tag===3?(g=t.stateNode,p&&r!==null&&(p=31-on(o),t=g.hiddenUpdates,u=t[p],u===null?t[p]=[r]:u.push(r),r.lane=o|536870912),g):null}function ru(t){if(50<_l)throw _l=0,Tp=null,Error(a(185));for(var r=t.return;r!==null;)t=r,r=t.return;return t.tag===3?t.stateNode:null}var ks={};function Cx(t,r,o,u){this.tag=t,this.key=o,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.refCleanup=this.ref=null,this.pendingProps=r,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=u,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Yn(t,r,o,u){return new Cx(t,r,o,u)}function vh(t){return t=t.prototype,!(!t||!t.isReactComponent)}function ua(t,r){var o=t.alternate;return o===null?(o=Yn(t.tag,r,t.key,t.mode),o.elementType=t.elementType,o.type=t.type,o.stateNode=t.stateNode,o.alternate=t,t.alternate=o):(o.pendingProps=r,o.type=t.type,o.flags=0,o.subtreeFlags=0,o.deletions=null),o.flags=t.flags&65011712,o.childLanes=t.childLanes,o.lanes=t.lanes,o.child=t.child,o.memoizedProps=t.memoizedProps,o.memoizedState=t.memoizedState,o.updateQueue=t.updateQueue,r=t.dependencies,o.dependencies=r===null?null:{lanes:r.lanes,firstContext:r.firstContext},o.sibling=t.sibling,o.index=t.index,o.ref=t.ref,o.refCleanup=t.refCleanup,o}function ob(t,r){t.flags&=65011714;var o=t.alternate;return o===null?(t.childLanes=0,t.lanes=r,t.child=null,t.subtreeFlags=0,t.memoizedProps=null,t.memoizedState=null,t.updateQueue=null,t.dependencies=null,t.stateNode=null):(t.childLanes=o.childLanes,t.lanes=o.lanes,t.child=o.child,t.subtreeFlags=0,t.deletions=null,t.memoizedProps=o.memoizedProps,t.memoizedState=o.memoizedState,t.updateQueue=o.updateQueue,t.type=o.type,r=o.dependencies,t.dependencies=r===null?null:{lanes:r.lanes,firstContext:r.firstContext}),t}function au(t,r,o,u,p,g){var w=0;if(u=t,typeof t=="function")vh(t)&&(w=1);else if(typeof t=="string")w=TT(t,o,de.current)?26:t==="html"||t==="head"||t==="body"?27:5;else e:switch(t){case pe:return t=Yn(31,o,r,p),t.elementType=pe,t.lanes=g,t;case A:return Ui(o.children,p,g,r);case x:w=8,p|=24;break;case L:return t=Yn(12,o,r,p|2),t.elementType=L,t.lanes=g,t;case B:return t=Yn(13,o,r,p),t.elementType=B,t.lanes=g,t;case G:return t=Yn(19,o,r,p),t.elementType=G,t.lanes=g,t;default:if(typeof t=="object"&&t!==null)switch(t.$$typeof){case k:w=10;break e;case M:w=9;break e;case O:w=11;break e;case P:w=14;break e;case W:w=16,u=null;break e}w=29,o=Error(a(130,t===null?"null":typeof t,"")),u=null}return r=Yn(w,o,r,p),r.elementType=t,r.type=u,r.lanes=g,r}function Ui(t,r,o,u){return t=Yn(7,t,u,r),t.lanes=o,t}function Sh(t,r,o){return t=Yn(6,t,null,r),t.lanes=o,t}function lb(t){var r=Yn(18,null,null,0);return r.stateNode=t,r}function wh(t,r,o){return r=Yn(4,t.children!==null?t.children:[],t.key,r),r.lanes=o,r.stateNode={containerInfo:t.containerInfo,pendingChildren:null,implementation:t.implementation},r}var cb=new WeakMap;function ur(t,r){if(typeof t=="object"&&t!==null){var o=cb.get(t);return o!==void 0?o:(r={value:t,source:r,stack:qr(r)},cb.set(t,r),r)}return{value:t,source:r,stack:qr(r)}}var Rs=[],Ds=0,iu=null,rl=0,dr=[],fr=0,Fa=null,Wr=1,Yr="";function da(t,r){Rs[Ds++]=rl,Rs[Ds++]=iu,iu=t,rl=r}function ub(t,r,o){dr[fr++]=Wr,dr[fr++]=Yr,dr[fr++]=Fa,Fa=t;var u=Wr;t=Yr;var p=32-on(u)-1;u&=~(1<<p),o+=1;var g=32-on(r)+p;if(30<g){var w=p-p%5;g=(u&(1<<w)-1).toString(32),u>>=w,p-=w,Wr=1<<32-on(r)+p|o<<p|u,Yr=g+t}else Wr=1<<g|o<<p|u,Yr=t}function Ch(t){t.return!==null&&(da(t,1),ub(t,1,0))}function Eh(t){for(;t===iu;)iu=Rs[--Ds],Rs[Ds]=null,rl=Rs[--Ds],Rs[Ds]=null;for(;t===Fa;)Fa=dr[--fr],dr[fr]=null,Yr=dr[--fr],dr[fr]=null,Wr=dr[--fr],dr[fr]=null}function db(t,r){dr[fr++]=Wr,dr[fr++]=Yr,dr[fr++]=Fa,Wr=r.id,Yr=r.overflow,Fa=t}var bn=null,Ot=null,ut=!1,Ka=null,hr=!1,_h=Error(a(519));function Wa(t){var r=Error(a(418,1<arguments.length&&arguments[1]!==void 0&&arguments[1]?"text":"HTML",""));throw al(ur(r,t)),_h}function fb(t){var r=t.stateNode,o=t.type,u=t.memoizedProps;switch(r[Ht]=t,r[An]=u,o){case"dialog":at("cancel",r),at("close",r);break;case"iframe":case"object":case"embed":at("load",r);break;case"video":case"audio":for(o=0;o<xl.length;o++)at(xl[o],r);break;case"source":at("error",r);break;case"img":case"image":case"link":at("error",r),at("load",r);break;case"details":at("toggle",r);break;case"input":at("invalid",r),Ti(r,u.value,u.defaultValue,u.checked,u.defaultChecked,u.type,u.name,!0);break;case"select":at("invalid",r);break;case"textarea":at("invalid",r),jc(r,u.value,u.defaultValue,u.children)}o=u.children,typeof o!="string"&&typeof o!="number"&&typeof o!="bigint"||r.textContent===""+o||u.suppressHydrationWarning===!0||RS(r.textContent,o)?(u.popover!=null&&(at("beforetoggle",r),at("toggle",r)),u.onScroll!=null&&at("scroll",r),u.onScrollEnd!=null&&at("scrollend",r),u.onClick!=null&&(r.onclick=Rr),r=!0):r=!1,r||Wa(t,!0)}function hb(t){for(bn=t.return;bn;)switch(bn.tag){case 5:case 31:case 13:hr=!1;return;case 27:case 3:hr=!0;return;default:bn=bn.return}}function Os(t){if(t!==bn)return!1;if(!ut)return hb(t),ut=!0,!1;var r=t.tag,o;if((o=r!==3&&r!==27)&&((o=r===5)&&(o=t.type,o=!(o!=="form"&&o!=="button")||Bp(t.type,t.memoizedProps)),o=!o),o&&Ot&&Wa(t),hb(t),r===13){if(t=t.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(317));Ot=HS(t)}else if(r===31){if(t=t.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(317));Ot=HS(t)}else r===27?(r=Ot,li(t.type)?(t=Kp,Kp=null,Ot=t):Ot=r):Ot=bn?gr(t.stateNode.nextSibling):null;return!0}function Pi(){Ot=bn=null,ut=!1}function Ah(){var t=Ka;return t!==null&&(Pn===null?Pn=t:Pn.push.apply(Pn,t),Ka=null),t}function al(t){Ka===null?Ka=[t]:Ka.push(t)}var xh=H(null),Bi=null,fa=null;function Ya(t,r,o){ce(xh,r._currentValue),r._currentValue=o}function ha(t){t._currentValue=xh.current,T(xh)}function Th(t,r,o){for(;t!==null;){var u=t.alternate;if((t.childLanes&r)!==r?(t.childLanes|=r,u!==null&&(u.childLanes|=r)):u!==null&&(u.childLanes&r)!==r&&(u.childLanes|=r),t===o)break;t=t.return}}function kh(t,r,o,u){var p=t.child;for(p!==null&&(p.return=t);p!==null;){var g=p.dependencies;if(g!==null){var w=p.child;g=g.firstContext;e:for(;g!==null;){var R=g;g=p;for(var z=0;z<r.length;z++)if(R.context===r[z]){g.lanes|=o,R=g.alternate,R!==null&&(R.lanes|=o),Th(g.return,o,t),u||(w=null);break e}g=R.next}}else if(p.tag===18){if(w=p.return,w===null)throw Error(a(341));w.lanes|=o,g=w.alternate,g!==null&&(g.lanes|=o),Th(w,o,t),w=null}else w=p.child;if(w!==null)w.return=p;else for(w=p;w!==null;){if(w===t){w=null;break}if(p=w.sibling,p!==null){p.return=w.return,w=p;break}w=w.return}p=w}}function Ls(t,r,o,u){t=null;for(var p=r,g=!1;p!==null;){if(!g){if((p.flags&524288)!==0)g=!0;else if((p.flags&262144)!==0)break}if(p.tag===10){var w=p.alternate;if(w===null)throw Error(a(387));if(w=w.memoizedProps,w!==null){var R=p.type;Wn(p.pendingProps.value,w.value)||(t!==null?t.push(R):t=[R])}}else if(p===xe.current){if(w=p.alternate,w===null)throw Error(a(387));w.memoizedState.memoizedState!==p.memoizedState.memoizedState&&(t!==null?t.push(Ol):t=[Ol])}p=p.return}t!==null&&kh(r,t,o,u),r.flags|=262144}function su(t){for(t=t.firstContext;t!==null;){if(!Wn(t.context._currentValue,t.memoizedValue))return!0;t=t.next}return!1}function qi(t){Bi=t,fa=null,t=t.dependencies,t!==null&&(t.firstContext=null)}function vn(t){return pb(Bi,t)}function ou(t,r){return Bi===null&&qi(t),pb(t,r)}function pb(t,r){var o=r._currentValue;if(r={context:r,memoizedValue:o,next:null},fa===null){if(t===null)throw Error(a(308));fa=r,t.dependencies={lanes:0,firstContext:r},t.flags|=524288}else fa=fa.next=r;return o}var Ex=typeof AbortController<"u"?AbortController:function(){var t=[],r=this.signal={aborted:!1,addEventListener:function(o,u){t.push(u)}};this.abort=function(){r.aborted=!0,t.forEach(function(o){return o()})}},_x=s.unstable_scheduleCallback,Ax=s.unstable_NormalPriority,Qt={$$typeof:k,Consumer:null,Provider:null,_currentValue:null,_currentValue2:null,_threadCount:0};function Rh(){return{controller:new Ex,data:new Map,refCount:0}}function il(t){t.refCount--,t.refCount===0&&_x(Ax,function(){t.controller.abort()})}var sl=null,Dh=0,Ms=0,$s=null;function xx(t,r){if(sl===null){var o=sl=[];Dh=0,Ms=Mp(),$s={status:"pending",value:void 0,then:function(u){o.push(u)}}}return Dh++,r.then(gb,gb),r}function gb(){if(--Dh===0&&sl!==null){$s!==null&&($s.status="fulfilled");var t=sl;sl=null,Ms=0,$s=null;for(var r=0;r<t.length;r++)(0,t[r])()}}function Tx(t,r){var o=[],u={status:"pending",value:null,reason:null,then:function(p){o.push(p)}};return t.then(function(){u.status="fulfilled",u.value=r;for(var p=0;p<o.length;p++)(0,o[p])(r)},function(p){for(u.status="rejected",u.reason=p,p=0;p<o.length;p++)(0,o[p])(void 0)}),u}var mb=J.S;J.S=function(t,r){Zv=qt(),typeof r=="object"&&r!==null&&typeof r.then=="function"&&xx(t,r),mb!==null&&mb(t,r)};var Gi=H(null);function Oh(){var t=Gi.current;return t!==null?t:Rt.pooledCache}function lu(t,r){r===null?ce(Gi,Gi.current):ce(Gi,r.pool)}function yb(){var t=Oh();return t===null?null:{parent:Qt._currentValue,pool:t}}var Ns=Error(a(460)),Lh=Error(a(474)),cu=Error(a(542)),uu={then:function(){}};function bb(t){return t=t.status,t==="fulfilled"||t==="rejected"}function vb(t,r,o){switch(o=t[o],o===void 0?t.push(r):o!==r&&(r.then(Rr,Rr),r=o),r.status){case"fulfilled":return r.value;case"rejected":throw t=r.reason,wb(t),t;default:if(typeof r.status=="string")r.then(Rr,Rr);else{if(t=Rt,t!==null&&100<t.shellSuspendCounter)throw Error(a(482));t=r,t.status="pending",t.then(function(u){if(r.status==="pending"){var p=r;p.status="fulfilled",p.value=u}},function(u){if(r.status==="pending"){var p=r;p.status="rejected",p.reason=u}})}switch(r.status){case"fulfilled":return r.value;case"rejected":throw t=r.reason,wb(t),t}throw Fi=r,Ns}}function Vi(t){try{var r=t._init;return r(t._payload)}catch(o){throw o!==null&&typeof o=="object"&&typeof o.then=="function"?(Fi=o,Ns):o}}var Fi=null;function Sb(){if(Fi===null)throw Error(a(459));var t=Fi;return Fi=null,t}function wb(t){if(t===Ns||t===cu)throw Error(a(483))}var Is=null,ol=0;function du(t){var r=ol;return ol+=1,Is===null&&(Is=[]),vb(Is,t,r)}function ll(t,r){r=r.props.ref,t.ref=r!==void 0?r:null}function fu(t,r){throw r.$$typeof===S?Error(a(525)):(t=Object.prototype.toString.call(r),Error(a(31,t==="[object Object]"?"object with keys {"+Object.keys(r).join(", ")+"}":t)))}function Cb(t){function r(F,q){if(t){var Y=F.deletions;Y===null?(F.deletions=[q],F.flags|=16):Y.push(q)}}function o(F,q){if(!t)return null;for(;q!==null;)r(F,q),q=q.sibling;return null}function u(F){for(var q=new Map;F!==null;)F.key!==null?q.set(F.key,F):q.set(F.index,F),F=F.sibling;return q}function p(F,q){return F=ua(F,q),F.index=0,F.sibling=null,F}function g(F,q,Y){return F.index=Y,t?(Y=F.alternate,Y!==null?(Y=Y.index,Y<q?(F.flags|=67108866,q):Y):(F.flags|=67108866,q)):(F.flags|=1048576,q)}function w(F){return t&&F.alternate===null&&(F.flags|=67108866),F}function R(F,q,Y,oe){return q===null||q.tag!==6?(q=Sh(Y,F.mode,oe),q.return=F,q):(q=p(q,Y),q.return=F,q)}function z(F,q,Y,oe){var Pe=Y.type;return Pe===A?re(F,q,Y.props.children,oe,Y.key):q!==null&&(q.elementType===Pe||typeof Pe=="object"&&Pe!==null&&Pe.$$typeof===W&&Vi(Pe)===q.type)?(q=p(q,Y.props),ll(q,Y),q.return=F,q):(q=au(Y.type,Y.key,Y.props,null,F.mode,oe),ll(q,Y),q.return=F,q)}function X(F,q,Y,oe){return q===null||q.tag!==4||q.stateNode.containerInfo!==Y.containerInfo||q.stateNode.implementation!==Y.implementation?(q=wh(Y,F.mode,oe),q.return=F,q):(q=p(q,Y.children||[]),q.return=F,q)}function re(F,q,Y,oe,Pe){return q===null||q.tag!==7?(q=Ui(Y,F.mode,oe,Pe),q.return=F,q):(q=p(q,Y),q.return=F,q)}function le(F,q,Y){if(typeof q=="string"&&q!==""||typeof q=="number"||typeof q=="bigint")return q=Sh(""+q,F.mode,Y),q.return=F,q;if(typeof q=="object"&&q!==null){switch(q.$$typeof){case v:return Y=au(q.type,q.key,q.props,null,F.mode,Y),ll(Y,q),Y.return=F,Y;case _:return q=wh(q,F.mode,Y),q.return=F,q;case W:return q=Vi(q),le(F,q,Y)}if(wt(q)||Ue(q))return q=Ui(q,F.mode,Y,null),q.return=F,q;if(typeof q.then=="function")return le(F,du(q),Y);if(q.$$typeof===k)return le(F,ou(F,q),Y);fu(F,q)}return null}function Q(F,q,Y,oe){var Pe=q!==null?q.key:null;if(typeof Y=="string"&&Y!==""||typeof Y=="number"||typeof Y=="bigint")return Pe!==null?null:R(F,q,""+Y,oe);if(typeof Y=="object"&&Y!==null){switch(Y.$$typeof){case v:return Y.key===Pe?z(F,q,Y,oe):null;case _:return Y.key===Pe?X(F,q,Y,oe):null;case W:return Y=Vi(Y),Q(F,q,Y,oe)}if(wt(Y)||Ue(Y))return Pe!==null?null:re(F,q,Y,oe,null);if(typeof Y.then=="function")return Q(F,q,du(Y),oe);if(Y.$$typeof===k)return Q(F,q,ou(F,Y),oe);fu(F,Y)}return null}function te(F,q,Y,oe,Pe){if(typeof oe=="string"&&oe!==""||typeof oe=="number"||typeof oe=="bigint")return F=F.get(Y)||null,R(q,F,""+oe,Pe);if(typeof oe=="object"&&oe!==null){switch(oe.$$typeof){case v:return F=F.get(oe.key===null?Y:oe.key)||null,z(q,F,oe,Pe);case _:return F=F.get(oe.key===null?Y:oe.key)||null,X(q,F,oe,Pe);case W:return oe=Vi(oe),te(F,q,Y,oe,Pe)}if(wt(oe)||Ue(oe))return F=F.get(Y)||null,re(q,F,oe,Pe,null);if(typeof oe.then=="function")return te(F,q,Y,du(oe),Pe);if(oe.$$typeof===k)return te(F,q,Y,ou(q,oe),Pe);fu(q,oe)}return null}function De(F,q,Y,oe){for(var Pe=null,mt=null,Ie=q,Je=q=0,ot=null;Ie!==null&&Je<Y.length;Je++){Ie.index>Je?(ot=Ie,Ie=null):ot=Ie.sibling;var yt=Q(F,Ie,Y[Je],oe);if(yt===null){Ie===null&&(Ie=ot);break}t&&Ie&&yt.alternate===null&&r(F,Ie),q=g(yt,q,Je),mt===null?Pe=yt:mt.sibling=yt,mt=yt,Ie=ot}if(Je===Y.length)return o(F,Ie),ut&&da(F,Je),Pe;if(Ie===null){for(;Je<Y.length;Je++)Ie=le(F,Y[Je],oe),Ie!==null&&(q=g(Ie,q,Je),mt===null?Pe=Ie:mt.sibling=Ie,mt=Ie);return ut&&da(F,Je),Pe}for(Ie=u(Ie);Je<Y.length;Je++)ot=te(Ie,F,Je,Y[Je],oe),ot!==null&&(t&&ot.alternate!==null&&Ie.delete(ot.key===null?Je:ot.key),q=g(ot,q,Je),mt===null?Pe=ot:mt.sibling=ot,mt=ot);return t&&Ie.forEach(function(hi){return r(F,hi)}),ut&&da(F,Je),Pe}function Be(F,q,Y,oe){if(Y==null)throw Error(a(151));for(var Pe=null,mt=null,Ie=q,Je=q=0,ot=null,yt=Y.next();Ie!==null&&!yt.done;Je++,yt=Y.next()){Ie.index>Je?(ot=Ie,Ie=null):ot=Ie.sibling;var hi=Q(F,Ie,yt.value,oe);if(hi===null){Ie===null&&(Ie=ot);break}t&&Ie&&hi.alternate===null&&r(F,Ie),q=g(hi,q,Je),mt===null?Pe=hi:mt.sibling=hi,mt=hi,Ie=ot}if(yt.done)return o(F,Ie),ut&&da(F,Je),Pe;if(Ie===null){for(;!yt.done;Je++,yt=Y.next())yt=le(F,yt.value,oe),yt!==null&&(q=g(yt,q,Je),mt===null?Pe=yt:mt.sibling=yt,mt=yt);return ut&&da(F,Je),Pe}for(Ie=u(Ie);!yt.done;Je++,yt=Y.next())yt=te(Ie,F,Je,yt.value,oe),yt!==null&&(t&&yt.alternate!==null&&Ie.delete(yt.key===null?Je:yt.key),q=g(yt,q,Je),mt===null?Pe=yt:mt.sibling=yt,mt=yt);return t&&Ie.forEach(function(HT){return r(F,HT)}),ut&&da(F,Je),Pe}function kt(F,q,Y,oe){if(typeof Y=="object"&&Y!==null&&Y.type===A&&Y.key===null&&(Y=Y.props.children),typeof Y=="object"&&Y!==null){switch(Y.$$typeof){case v:e:{for(var Pe=Y.key;q!==null;){if(q.key===Pe){if(Pe=Y.type,Pe===A){if(q.tag===7){o(F,q.sibling),oe=p(q,Y.props.children),oe.return=F,F=oe;break e}}else if(q.elementType===Pe||typeof Pe=="object"&&Pe!==null&&Pe.$$typeof===W&&Vi(Pe)===q.type){o(F,q.sibling),oe=p(q,Y.props),ll(oe,Y),oe.return=F,F=oe;break e}o(F,q);break}else r(F,q);q=q.sibling}Y.type===A?(oe=Ui(Y.props.children,F.mode,oe,Y.key),oe.return=F,F=oe):(oe=au(Y.type,Y.key,Y.props,null,F.mode,oe),ll(oe,Y),oe.return=F,F=oe)}return w(F);case _:e:{for(Pe=Y.key;q!==null;){if(q.key===Pe)if(q.tag===4&&q.stateNode.containerInfo===Y.containerInfo&&q.stateNode.implementation===Y.implementation){o(F,q.sibling),oe=p(q,Y.children||[]),oe.return=F,F=oe;break e}else{o(F,q);break}else r(F,q);q=q.sibling}oe=wh(Y,F.mode,oe),oe.return=F,F=oe}return w(F);case W:return Y=Vi(Y),kt(F,q,Y,oe)}if(wt(Y))return De(F,q,Y,oe);if(Ue(Y)){if(Pe=Ue(Y),typeof Pe!="function")throw Error(a(150));return Y=Pe.call(Y),Be(F,q,Y,oe)}if(typeof Y.then=="function")return kt(F,q,du(Y),oe);if(Y.$$typeof===k)return kt(F,q,ou(F,Y),oe);fu(F,Y)}return typeof Y=="string"&&Y!==""||typeof Y=="number"||typeof Y=="bigint"?(Y=""+Y,q!==null&&q.tag===6?(o(F,q.sibling),oe=p(q,Y),oe.return=F,F=oe):(o(F,q),oe=Sh(Y,F.mode,oe),oe.return=F,F=oe),w(F)):o(F,q)}return function(F,q,Y,oe){try{ol=0;var Pe=kt(F,q,Y,oe);return Is=null,Pe}catch(Ie){if(Ie===Ns||Ie===cu)throw Ie;var mt=Yn(29,Ie,null,F.mode);return mt.lanes=oe,mt.return=F,mt}}}var Ki=Cb(!0),Eb=Cb(!1),Xa=!1;function Mh(t){t.updateQueue={baseState:t.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function $h(t,r){t=t.updateQueue,r.updateQueue===t&&(r.updateQueue={baseState:t.baseState,firstBaseUpdate:t.firstBaseUpdate,lastBaseUpdate:t.lastBaseUpdate,shared:t.shared,callbacks:null})}function Ja(t){return{lane:t,tag:0,payload:null,callback:null,next:null}}function Qa(t,r,o){var u=t.updateQueue;if(u===null)return null;if(u=u.shared,(vt&2)!==0){var p=u.pending;return p===null?r.next=r:(r.next=p.next,p.next=r),u.pending=r,r=ru(t),sb(t,null,o),r}return nu(t,u,r,o),ru(t)}function cl(t,r,o){if(r=r.updateQueue,r!==null&&(r=r.shared,(o&4194048)!==0)){var u=r.lanes;u&=t.pendingLanes,o|=u,r.lanes=o,Ce(t,o)}}function Nh(t,r){var o=t.updateQueue,u=t.alternate;if(u!==null&&(u=u.updateQueue,o===u)){var p=null,g=null;if(o=o.firstBaseUpdate,o!==null){do{var w={lane:o.lane,tag:o.tag,payload:o.payload,callback:null,next:null};g===null?p=g=w:g=g.next=w,o=o.next}while(o!==null);g===null?p=g=r:g=g.next=r}else p=g=r;o={baseState:u.baseState,firstBaseUpdate:p,lastBaseUpdate:g,shared:u.shared,callbacks:u.callbacks},t.updateQueue=o;return}t=o.lastBaseUpdate,t===null?o.firstBaseUpdate=r:t.next=r,o.lastBaseUpdate=r}var Ih=!1;function ul(){if(Ih){var t=$s;if(t!==null)throw t}}function dl(t,r,o,u){Ih=!1;var p=t.updateQueue;Xa=!1;var g=p.firstBaseUpdate,w=p.lastBaseUpdate,R=p.shared.pending;if(R!==null){p.shared.pending=null;var z=R,X=z.next;z.next=null,w===null?g=X:w.next=X,w=z;var re=t.alternate;re!==null&&(re=re.updateQueue,R=re.lastBaseUpdate,R!==w&&(R===null?re.firstBaseUpdate=X:R.next=X,re.lastBaseUpdate=z))}if(g!==null){var le=p.baseState;w=0,re=X=z=null,R=g;do{var Q=R.lane&-536870913,te=Q!==R.lane;if(te?(st&Q)===Q:(u&Q)===Q){Q!==0&&Q===Ms&&(Ih=!0),re!==null&&(re=re.next={lane:0,tag:R.tag,payload:R.payload,callback:null,next:null});e:{var De=t,Be=R;Q=r;var kt=o;switch(Be.tag){case 1:if(De=Be.payload,typeof De=="function"){le=De.call(kt,le,Q);break e}le=De;break e;case 3:De.flags=De.flags&-65537|128;case 0:if(De=Be.payload,Q=typeof De=="function"?De.call(kt,le,Q):De,Q==null)break e;le=b({},le,Q);break e;case 2:Xa=!0}}Q=R.callback,Q!==null&&(t.flags|=64,te&&(t.flags|=8192),te=p.callbacks,te===null?p.callbacks=[Q]:te.push(Q))}else te={lane:Q,tag:R.tag,payload:R.payload,callback:R.callback,next:null},re===null?(X=re=te,z=le):re=re.next=te,w|=Q;if(R=R.next,R===null){if(R=p.shared.pending,R===null)break;te=R,R=te.next,te.next=null,p.lastBaseUpdate=te,p.shared.pending=null}}while(!0);re===null&&(z=le),p.baseState=z,p.firstBaseUpdate=X,p.lastBaseUpdate=re,g===null&&(p.shared.lanes=0),ri|=w,t.lanes=w,t.memoizedState=le}}function _b(t,r){if(typeof t!="function")throw Error(a(191,t));t.call(r)}function Ab(t,r){var o=t.callbacks;if(o!==null)for(t.callbacks=null,t=0;t<o.length;t++)_b(o[t],r)}var js=H(null),hu=H(0);function xb(t,r){t=Ca,ce(hu,t),ce(js,r),Ca=t|r.baseLanes}function jh(){ce(hu,Ca),ce(js,js.current)}function Hh(){Ca=hu.current,T(js),T(hu)}var Xn=H(null),pr=null;function Za(t){var r=t.alternate;ce(Wt,Wt.current&1),ce(Xn,t),pr===null&&(r===null||js.current!==null||r.memoizedState!==null)&&(pr=t)}function zh(t){ce(Wt,Wt.current),ce(Xn,t),pr===null&&(pr=t)}function Tb(t){t.tag===22?(ce(Wt,Wt.current),ce(Xn,t),pr===null&&(pr=t)):ei()}function ei(){ce(Wt,Wt.current),ce(Xn,Xn.current)}function Jn(t){T(Xn),pr===t&&(pr=null),T(Wt)}var Wt=H(0);function pu(t){for(var r=t;r!==null;){if(r.tag===13){var o=r.memoizedState;if(o!==null&&(o=o.dehydrated,o===null||Vp(o)||Fp(o)))return r}else if(r.tag===19&&(r.memoizedProps.revealOrder==="forwards"||r.memoizedProps.revealOrder==="backwards"||r.memoizedProps.revealOrder==="unstable_legacy-backwards"||r.memoizedProps.revealOrder==="together")){if((r.flags&128)!==0)return r}else if(r.child!==null){r.child.return=r,r=r.child;continue}if(r===t)break;for(;r.sibling===null;){if(r.return===null||r.return===t)return null;r=r.return}r.sibling.return=r.return,r=r.sibling}return null}var pa=0,Xe=null,xt=null,Zt=null,gu=!1,Hs=!1,Wi=!1,mu=0,fl=0,zs=null,kx=0;function Gt(){throw Error(a(321))}function Uh(t,r){if(r===null)return!1;for(var o=0;o<r.length&&o<t.length;o++)if(!Wn(t[o],r[o]))return!1;return!0}function Ph(t,r,o,u,p,g){return pa=g,Xe=r,r.memoizedState=null,r.updateQueue=null,r.lanes=0,J.H=t===null||t.memoizedState===null?uv:np,Wi=!1,g=o(u,p),Wi=!1,Hs&&(g=Rb(r,o,u,p)),kb(t),g}function kb(t){J.H=gl;var r=xt!==null&&xt.next!==null;if(pa=0,Zt=xt=Xe=null,gu=!1,fl=0,zs=null,r)throw Error(a(300));t===null||en||(t=t.dependencies,t!==null&&su(t)&&(en=!0))}function Rb(t,r,o,u){Xe=t;var p=0;do{if(Hs&&(zs=null),fl=0,Hs=!1,25<=p)throw Error(a(301));if(p+=1,Zt=xt=null,t.updateQueue!=null){var g=t.updateQueue;g.lastEffect=null,g.events=null,g.stores=null,g.memoCache!=null&&(g.memoCache.index=0)}J.H=dv,g=r(o,u)}while(Hs);return g}function Rx(){var t=J.H,r=t.useState()[0];return r=typeof r.then=="function"?hl(r):r,t=t.useState()[0],(xt!==null?xt.memoizedState:null)!==t&&(Xe.flags|=1024),r}function Bh(){var t=mu!==0;return mu=0,t}function qh(t,r,o){r.updateQueue=t.updateQueue,r.flags&=-2053,t.lanes&=~o}function Gh(t){if(gu){for(t=t.memoizedState;t!==null;){var r=t.queue;r!==null&&(r.pending=null),t=t.next}gu=!1}pa=0,Zt=xt=Xe=null,Hs=!1,fl=mu=0,zs=null}function On(){var t={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Zt===null?Xe.memoizedState=Zt=t:Zt=Zt.next=t,Zt}function Yt(){if(xt===null){var t=Xe.alternate;t=t!==null?t.memoizedState:null}else t=xt.next;var r=Zt===null?Xe.memoizedState:Zt.next;if(r!==null)Zt=r,xt=t;else{if(t===null)throw Xe.alternate===null?Error(a(467)):Error(a(310));xt=t,t={memoizedState:xt.memoizedState,baseState:xt.baseState,baseQueue:xt.baseQueue,queue:xt.queue,next:null},Zt===null?Xe.memoizedState=Zt=t:Zt=Zt.next=t}return Zt}function yu(){return{lastEffect:null,events:null,stores:null,memoCache:null}}function hl(t){var r=fl;return fl+=1,zs===null&&(zs=[]),t=vb(zs,t,r),r=Xe,(Zt===null?r.memoizedState:Zt.next)===null&&(r=r.alternate,J.H=r===null||r.memoizedState===null?uv:np),t}function bu(t){if(t!==null&&typeof t=="object"){if(typeof t.then=="function")return hl(t);if(t.$$typeof===k)return vn(t)}throw Error(a(438,String(t)))}function Vh(t){var r=null,o=Xe.updateQueue;if(o!==null&&(r=o.memoCache),r==null){var u=Xe.alternate;u!==null&&(u=u.updateQueue,u!==null&&(u=u.memoCache,u!=null&&(r={data:u.data.map(function(p){return p.slice()}),index:0})))}if(r==null&&(r={data:[],index:0}),o===null&&(o=yu(),Xe.updateQueue=o),o.memoCache=r,o=r.data[r.index],o===void 0)for(o=r.data[r.index]=Array(t),u=0;u<t;u++)o[u]=Se;return r.index++,o}function ga(t,r){return typeof r=="function"?r(t):r}function vu(t){var r=Yt();return Fh(r,xt,t)}function Fh(t,r,o){var u=t.queue;if(u===null)throw Error(a(311));u.lastRenderedReducer=o;var p=t.baseQueue,g=u.pending;if(g!==null){if(p!==null){var w=p.next;p.next=g.next,g.next=w}r.baseQueue=p=g,u.pending=null}if(g=t.baseState,p===null)t.memoizedState=g;else{r=p.next;var R=w=null,z=null,X=r,re=!1;do{var le=X.lane&-536870913;if(le!==X.lane?(st&le)===le:(pa&le)===le){var Q=X.revertLane;if(Q===0)z!==null&&(z=z.next={lane:0,revertLane:0,gesture:null,action:X.action,hasEagerState:X.hasEagerState,eagerState:X.eagerState,next:null}),le===Ms&&(re=!0);else if((pa&Q)===Q){X=X.next,Q===Ms&&(re=!0);continue}else le={lane:0,revertLane:X.revertLane,gesture:null,action:X.action,hasEagerState:X.hasEagerState,eagerState:X.eagerState,next:null},z===null?(R=z=le,w=g):z=z.next=le,Xe.lanes|=Q,ri|=Q;le=X.action,Wi&&o(g,le),g=X.hasEagerState?X.eagerState:o(g,le)}else Q={lane:le,revertLane:X.revertLane,gesture:X.gesture,action:X.action,hasEagerState:X.hasEagerState,eagerState:X.eagerState,next:null},z===null?(R=z=Q,w=g):z=z.next=Q,Xe.lanes|=le,ri|=le;X=X.next}while(X!==null&&X!==r);if(z===null?w=g:z.next=R,!Wn(g,t.memoizedState)&&(en=!0,re&&(o=$s,o!==null)))throw o;t.memoizedState=g,t.baseState=w,t.baseQueue=z,u.lastRenderedState=g}return p===null&&(u.lanes=0),[t.memoizedState,u.dispatch]}function Kh(t){var r=Yt(),o=r.queue;if(o===null)throw Error(a(311));o.lastRenderedReducer=t;var u=o.dispatch,p=o.pending,g=r.memoizedState;if(p!==null){o.pending=null;var w=p=p.next;do g=t(g,w.action),w=w.next;while(w!==p);Wn(g,r.memoizedState)||(en=!0),r.memoizedState=g,r.baseQueue===null&&(r.baseState=g),o.lastRenderedState=g}return[g,u]}function Db(t,r,o){var u=Xe,p=Yt(),g=ut;if(g){if(o===void 0)throw Error(a(407));o=o()}else o=r();var w=!Wn((xt||p).memoizedState,o);if(w&&(p.memoizedState=o,en=!0),p=p.queue,Xh(Mb.bind(null,u,p,t),[t]),p.getSnapshot!==r||w||Zt!==null&&Zt.memoizedState.tag&1){if(u.flags|=2048,Us(9,{destroy:void 0},Lb.bind(null,u,p,o,r),null),Rt===null)throw Error(a(349));g||(pa&127)!==0||Ob(u,r,o)}return o}function Ob(t,r,o){t.flags|=16384,t={getSnapshot:r,value:o},r=Xe.updateQueue,r===null?(r=yu(),Xe.updateQueue=r,r.stores=[t]):(o=r.stores,o===null?r.stores=[t]:o.push(t))}function Lb(t,r,o,u){r.value=o,r.getSnapshot=u,$b(r)&&Nb(t)}function Mb(t,r,o){return o(function(){$b(r)&&Nb(t)})}function $b(t){var r=t.getSnapshot;t=t.value;try{var o=r();return!Wn(t,o)}catch{return!0}}function Nb(t){var r=zi(t,2);r!==null&&Bn(r,t,2)}function Wh(t){var r=On();if(typeof t=="function"){var o=t;if(t=o(),Wi){_n(!0);try{o()}finally{_n(!1)}}}return r.memoizedState=r.baseState=t,r.queue={pending:null,lanes:0,dispatch:null,lastRenderedReducer:ga,lastRenderedState:t},r}function Ib(t,r,o,u){return t.baseState=o,Fh(t,xt,typeof u=="function"?u:ga)}function Dx(t,r,o,u,p){if(Cu(t))throw Error(a(485));if(t=r.action,t!==null){var g={payload:p,action:t,next:null,isTransition:!0,status:"pending",value:null,reason:null,listeners:[],then:function(w){g.listeners.push(w)}};J.T!==null?o(!0):g.isTransition=!1,u(g),o=r.pending,o===null?(g.next=r.pending=g,jb(r,g)):(g.next=o.next,r.pending=o.next=g)}}function jb(t,r){var o=r.action,u=r.payload,p=t.state;if(r.isTransition){var g=J.T,w={};J.T=w;try{var R=o(p,u),z=J.S;z!==null&&z(w,R),Hb(t,r,R)}catch(X){Yh(t,r,X)}finally{g!==null&&w.types!==null&&(g.types=w.types),J.T=g}}else try{g=o(p,u),Hb(t,r,g)}catch(X){Yh(t,r,X)}}function Hb(t,r,o){o!==null&&typeof o=="object"&&typeof o.then=="function"?o.then(function(u){zb(t,r,u)},function(u){return Yh(t,r,u)}):zb(t,r,o)}function zb(t,r,o){r.status="fulfilled",r.value=o,Ub(r),t.state=o,r=t.pending,r!==null&&(o=r.next,o===r?t.pending=null:(o=o.next,r.next=o,jb(t,o)))}function Yh(t,r,o){var u=t.pending;if(t.pending=null,u!==null){u=u.next;do r.status="rejected",r.reason=o,Ub(r),r=r.next;while(r!==u)}t.action=null}function Ub(t){t=t.listeners;for(var r=0;r<t.length;r++)(0,t[r])()}function Pb(t,r){return r}function Bb(t,r){if(ut){var o=Rt.formState;if(o!==null){e:{var u=Xe;if(ut){if(Ot){t:{for(var p=Ot,g=hr;p.nodeType!==8;){if(!g){p=null;break t}if(p=gr(p.nextSibling),p===null){p=null;break t}}g=p.data,p=g==="F!"||g==="F"?p:null}if(p){Ot=gr(p.nextSibling),u=p.data==="F!";break e}}Wa(u)}u=!1}u&&(r=o[0])}}return o=On(),o.memoizedState=o.baseState=r,u={pending:null,lanes:0,dispatch:null,lastRenderedReducer:Pb,lastRenderedState:r},o.queue=u,o=ov.bind(null,Xe,u),u.dispatch=o,u=Wh(!1),g=tp.bind(null,Xe,!1,u.queue),u=On(),p={state:r,dispatch:null,action:t,pending:null},u.queue=p,o=Dx.bind(null,Xe,p,g,o),p.dispatch=o,u.memoizedState=t,[r,o,!1]}function qb(t){var r=Yt();return Gb(r,xt,t)}function Gb(t,r,o){if(r=Fh(t,r,Pb)[0],t=vu(ga)[0],typeof r=="object"&&r!==null&&typeof r.then=="function")try{var u=hl(r)}catch(w){throw w===Ns?cu:w}else u=r;r=Yt();var p=r.queue,g=p.dispatch;return o!==r.memoizedState&&(Xe.flags|=2048,Us(9,{destroy:void 0},Ox.bind(null,p,o),null)),[u,g,t]}function Ox(t,r){t.action=r}function Vb(t){var r=Yt(),o=xt;if(o!==null)return Gb(r,o,t);Yt(),r=r.memoizedState,o=Yt();var u=o.queue.dispatch;return o.memoizedState=t,[r,u,!1]}function Us(t,r,o,u){return t={tag:t,create:o,deps:u,inst:r,next:null},r=Xe.updateQueue,r===null&&(r=yu(),Xe.updateQueue=r),o=r.lastEffect,o===null?r.lastEffect=t.next=t:(u=o.next,o.next=t,t.next=u,r.lastEffect=t),t}function Fb(){return Yt().memoizedState}function Su(t,r,o,u){var p=On();Xe.flags|=t,p.memoizedState=Us(1|r,{destroy:void 0},o,u===void 0?null:u)}function wu(t,r,o,u){var p=Yt();u=u===void 0?null:u;var g=p.memoizedState.inst;xt!==null&&u!==null&&Uh(u,xt.memoizedState.deps)?p.memoizedState=Us(r,g,o,u):(Xe.flags|=t,p.memoizedState=Us(1|r,g,o,u))}function Kb(t,r){Su(8390656,8,t,r)}function Xh(t,r){wu(2048,8,t,r)}function Lx(t){Xe.flags|=4;var r=Xe.updateQueue;if(r===null)r=yu(),Xe.updateQueue=r,r.events=[t];else{var o=r.events;o===null?r.events=[t]:o.push(t)}}function Wb(t){var r=Yt().memoizedState;return Lx({ref:r,nextImpl:t}),function(){if((vt&2)!==0)throw Error(a(440));return r.impl.apply(void 0,arguments)}}function Yb(t,r){return wu(4,2,t,r)}function Xb(t,r){return wu(4,4,t,r)}function Jb(t,r){if(typeof r=="function"){t=t();var o=r(t);return function(){typeof o=="function"?o():r(null)}}if(r!=null)return t=t(),r.current=t,function(){r.current=null}}function Qb(t,r,o){o=o!=null?o.concat([t]):null,wu(4,4,Jb.bind(null,r,t),o)}function Jh(){}function Zb(t,r){var o=Yt();r=r===void 0?null:r;var u=o.memoizedState;return r!==null&&Uh(r,u[1])?u[0]:(o.memoizedState=[t,r],t)}function ev(t,r){var o=Yt();r=r===void 0?null:r;var u=o.memoizedState;if(r!==null&&Uh(r,u[1]))return u[0];if(u=t(),Wi){_n(!0);try{t()}finally{_n(!1)}}return o.memoizedState=[u,r],u}function Qh(t,r,o){return o===void 0||(pa&1073741824)!==0&&(st&261930)===0?t.memoizedState=r:(t.memoizedState=o,t=tS(),Xe.lanes|=t,ri|=t,o)}function tv(t,r,o,u){return Wn(o,r)?o:js.current!==null?(t=Qh(t,o,u),Wn(t,r)||(en=!0),t):(pa&42)===0||(pa&1073741824)!==0&&(st&261930)===0?(en=!0,t.memoizedState=o):(t=tS(),Xe.lanes|=t,ri|=t,r)}function nv(t,r,o,u,p){var g=fe.p;fe.p=g!==0&&8>g?g:8;var w=J.T,R={};J.T=R,tp(t,!1,r,o);try{var z=p(),X=J.S;if(X!==null&&X(R,z),z!==null&&typeof z=="object"&&typeof z.then=="function"){var re=Tx(z,u);pl(t,r,re,er(t))}else pl(t,r,u,er(t))}catch(le){pl(t,r,{then:function(){},status:"rejected",reason:le},er())}finally{fe.p=g,w!==null&&R.types!==null&&(w.types=R.types),J.T=w}}function Mx(){}function Zh(t,r,o,u){if(t.tag!==5)throw Error(a(476));var p=rv(t).queue;nv(t,p,r,Le,o===null?Mx:function(){return av(t),o(u)})}function rv(t){var r=t.memoizedState;if(r!==null)return r;r={memoizedState:Le,baseState:Le,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ga,lastRenderedState:Le},next:null};var o={};return r.next={memoizedState:o,baseState:o,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:ga,lastRenderedState:o},next:null},t.memoizedState=r,t=t.alternate,t!==null&&(t.memoizedState=r),r}function av(t){var r=rv(t);r.next===null&&(r=t.alternate.memoizedState),pl(t,r.next.queue,{},er())}function ep(){return vn(Ol)}function iv(){return Yt().memoizedState}function sv(){return Yt().memoizedState}function $x(t){for(var r=t.return;r!==null;){switch(r.tag){case 24:case 3:var o=er();t=Ja(o);var u=Qa(r,t,o);u!==null&&(Bn(u,r,o),cl(u,r,o)),r={cache:Rh()},t.payload=r;return}r=r.return}}function Nx(t,r,o){var u=er();o={lane:u,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null},Cu(t)?lv(r,o):(o=bh(t,r,o,u),o!==null&&(Bn(o,t,u),cv(o,r,u)))}function ov(t,r,o){var u=er();pl(t,r,o,u)}function pl(t,r,o,u){var p={lane:u,revertLane:0,gesture:null,action:o,hasEagerState:!1,eagerState:null,next:null};if(Cu(t))lv(r,p);else{var g=t.alternate;if(t.lanes===0&&(g===null||g.lanes===0)&&(g=r.lastRenderedReducer,g!==null))try{var w=r.lastRenderedState,R=g(w,o);if(p.hasEagerState=!0,p.eagerState=R,Wn(R,w))return nu(t,r,p,0),Rt===null&&tu(),!1}catch{}if(o=bh(t,r,p,u),o!==null)return Bn(o,t,u),cv(o,r,u),!0}return!1}function tp(t,r,o,u){if(u={lane:2,revertLane:Mp(),gesture:null,action:u,hasEagerState:!1,eagerState:null,next:null},Cu(t)){if(r)throw Error(a(479))}else r=bh(t,o,u,2),r!==null&&Bn(r,t,2)}function Cu(t){var r=t.alternate;return t===Xe||r!==null&&r===Xe}function lv(t,r){Hs=gu=!0;var o=t.pending;o===null?r.next=r:(r.next=o.next,o.next=r),t.pending=r}function cv(t,r,o){if((o&4194048)!==0){var u=r.lanes;u&=t.pendingLanes,o|=u,r.lanes=o,Ce(t,o)}}var gl={readContext:vn,use:bu,useCallback:Gt,useContext:Gt,useEffect:Gt,useImperativeHandle:Gt,useLayoutEffect:Gt,useInsertionEffect:Gt,useMemo:Gt,useReducer:Gt,useRef:Gt,useState:Gt,useDebugValue:Gt,useDeferredValue:Gt,useTransition:Gt,useSyncExternalStore:Gt,useId:Gt,useHostTransitionStatus:Gt,useFormState:Gt,useActionState:Gt,useOptimistic:Gt,useMemoCache:Gt,useCacheRefresh:Gt};gl.useEffectEvent=Gt;var uv={readContext:vn,use:bu,useCallback:function(t,r){return On().memoizedState=[t,r===void 0?null:r],t},useContext:vn,useEffect:Kb,useImperativeHandle:function(t,r,o){o=o!=null?o.concat([t]):null,Su(4194308,4,Jb.bind(null,r,t),o)},useLayoutEffect:function(t,r){return Su(4194308,4,t,r)},useInsertionEffect:function(t,r){Su(4,2,t,r)},useMemo:function(t,r){var o=On();r=r===void 0?null:r;var u=t();if(Wi){_n(!0);try{t()}finally{_n(!1)}}return o.memoizedState=[u,r],u},useReducer:function(t,r,o){var u=On();if(o!==void 0){var p=o(r);if(Wi){_n(!0);try{o(r)}finally{_n(!1)}}}else p=r;return u.memoizedState=u.baseState=p,t={pending:null,lanes:0,dispatch:null,lastRenderedReducer:t,lastRenderedState:p},u.queue=t,t=t.dispatch=Nx.bind(null,Xe,t),[u.memoizedState,t]},useRef:function(t){var r=On();return t={current:t},r.memoizedState=t},useState:function(t){t=Wh(t);var r=t.queue,o=ov.bind(null,Xe,r);return r.dispatch=o,[t.memoizedState,o]},useDebugValue:Jh,useDeferredValue:function(t,r){var o=On();return Qh(o,t,r)},useTransition:function(){var t=Wh(!1);return t=nv.bind(null,Xe,t.queue,!0,!1),On().memoizedState=t,[!1,t]},useSyncExternalStore:function(t,r,o){var u=Xe,p=On();if(ut){if(o===void 0)throw Error(a(407));o=o()}else{if(o=r(),Rt===null)throw Error(a(349));(st&127)!==0||Ob(u,r,o)}p.memoizedState=o;var g={value:o,getSnapshot:r};return p.queue=g,Kb(Mb.bind(null,u,g,t),[t]),u.flags|=2048,Us(9,{destroy:void 0},Lb.bind(null,u,g,o,r),null),o},useId:function(){var t=On(),r=Rt.identifierPrefix;if(ut){var o=Yr,u=Wr;o=(u&~(1<<32-on(u)-1)).toString(32)+o,r="_"+r+"R_"+o,o=mu++,0<o&&(r+="H"+o.toString(32)),r+="_"}else o=kx++,r="_"+r+"r_"+o.toString(32)+"_";return t.memoizedState=r},useHostTransitionStatus:ep,useFormState:Bb,useActionState:Bb,useOptimistic:function(t){var r=On();r.memoizedState=r.baseState=t;var o={pending:null,lanes:0,dispatch:null,lastRenderedReducer:null,lastRenderedState:null};return r.queue=o,r=tp.bind(null,Xe,!0,o),o.dispatch=r,[t,r]},useMemoCache:Vh,useCacheRefresh:function(){return On().memoizedState=$x.bind(null,Xe)},useEffectEvent:function(t){var r=On(),o={impl:t};return r.memoizedState=o,function(){if((vt&2)!==0)throw Error(a(440));return o.impl.apply(void 0,arguments)}}},np={readContext:vn,use:bu,useCallback:Zb,useContext:vn,useEffect:Xh,useImperativeHandle:Qb,useInsertionEffect:Yb,useLayoutEffect:Xb,useMemo:ev,useReducer:vu,useRef:Fb,useState:function(){return vu(ga)},useDebugValue:Jh,useDeferredValue:function(t,r){var o=Yt();return tv(o,xt.memoizedState,t,r)},useTransition:function(){var t=vu(ga)[0],r=Yt().memoizedState;return[typeof t=="boolean"?t:hl(t),r]},useSyncExternalStore:Db,useId:iv,useHostTransitionStatus:ep,useFormState:qb,useActionState:qb,useOptimistic:function(t,r){var o=Yt();return Ib(o,xt,t,r)},useMemoCache:Vh,useCacheRefresh:sv};np.useEffectEvent=Wb;var dv={readContext:vn,use:bu,useCallback:Zb,useContext:vn,useEffect:Xh,useImperativeHandle:Qb,useInsertionEffect:Yb,useLayoutEffect:Xb,useMemo:ev,useReducer:Kh,useRef:Fb,useState:function(){return Kh(ga)},useDebugValue:Jh,useDeferredValue:function(t,r){var o=Yt();return xt===null?Qh(o,t,r):tv(o,xt.memoizedState,t,r)},useTransition:function(){var t=Kh(ga)[0],r=Yt().memoizedState;return[typeof t=="boolean"?t:hl(t),r]},useSyncExternalStore:Db,useId:iv,useHostTransitionStatus:ep,useFormState:Vb,useActionState:Vb,useOptimistic:function(t,r){var o=Yt();return xt!==null?Ib(o,xt,t,r):(o.baseState=t,[t,o.queue.dispatch])},useMemoCache:Vh,useCacheRefresh:sv};dv.useEffectEvent=Wb;function rp(t,r,o,u){r=t.memoizedState,o=o(u,r),o=o==null?r:b({},r,o),t.memoizedState=o,t.lanes===0&&(t.updateQueue.baseState=o)}var ap={enqueueSetState:function(t,r,o){t=t._reactInternals;var u=er(),p=Ja(u);p.payload=r,o!=null&&(p.callback=o),r=Qa(t,p,u),r!==null&&(Bn(r,t,u),cl(r,t,u))},enqueueReplaceState:function(t,r,o){t=t._reactInternals;var u=er(),p=Ja(u);p.tag=1,p.payload=r,o!=null&&(p.callback=o),r=Qa(t,p,u),r!==null&&(Bn(r,t,u),cl(r,t,u))},enqueueForceUpdate:function(t,r){t=t._reactInternals;var o=er(),u=Ja(o);u.tag=2,r!=null&&(u.callback=r),r=Qa(t,u,o),r!==null&&(Bn(r,t,o),cl(r,t,o))}};function fv(t,r,o,u,p,g,w){return t=t.stateNode,typeof t.shouldComponentUpdate=="function"?t.shouldComponentUpdate(u,g,w):r.prototype&&r.prototype.isPureReactComponent?!tl(o,u)||!tl(p,g):!0}function hv(t,r,o,u){t=r.state,typeof r.componentWillReceiveProps=="function"&&r.componentWillReceiveProps(o,u),typeof r.UNSAFE_componentWillReceiveProps=="function"&&r.UNSAFE_componentWillReceiveProps(o,u),r.state!==t&&ap.enqueueReplaceState(r,r.state,null)}function Yi(t,r){var o=r;if("ref"in r){o={};for(var u in r)u!=="ref"&&(o[u]=r[u])}if(t=t.defaultProps){o===r&&(o=b({},o));for(var p in t)o[p]===void 0&&(o[p]=t[p])}return o}function pv(t){eu(t)}function gv(t){console.error(t)}function mv(t){eu(t)}function Eu(t,r){try{var o=t.onUncaughtError;o(r.value,{componentStack:r.stack})}catch(u){setTimeout(function(){throw u})}}function yv(t,r,o){try{var u=t.onCaughtError;u(o.value,{componentStack:o.stack,errorBoundary:r.tag===1?r.stateNode:null})}catch(p){setTimeout(function(){throw p})}}function ip(t,r,o){return o=Ja(o),o.tag=3,o.payload={element:null},o.callback=function(){Eu(t,r)},o}function bv(t){return t=Ja(t),t.tag=3,t}function vv(t,r,o,u){var p=o.type.getDerivedStateFromError;if(typeof p=="function"){var g=u.value;t.payload=function(){return p(g)},t.callback=function(){yv(r,o,u)}}var w=o.stateNode;w!==null&&typeof w.componentDidCatch=="function"&&(t.callback=function(){yv(r,o,u),typeof p!="function"&&(ai===null?ai=new Set([this]):ai.add(this));var R=u.stack;this.componentDidCatch(u.value,{componentStack:R!==null?R:""})})}function Ix(t,r,o,u,p){if(o.flags|=32768,u!==null&&typeof u=="object"&&typeof u.then=="function"){if(r=o.alternate,r!==null&&Ls(r,o,p,!0),o=Xn.current,o!==null){switch(o.tag){case 31:case 13:return pr===null?Nu():o.alternate===null&&Vt===0&&(Vt=3),o.flags&=-257,o.flags|=65536,o.lanes=p,u===uu?o.flags|=16384:(r=o.updateQueue,r===null?o.updateQueue=new Set([u]):r.add(u),Dp(t,u,p)),!1;case 22:return o.flags|=65536,u===uu?o.flags|=16384:(r=o.updateQueue,r===null?(r={transitions:null,markerInstances:null,retryQueue:new Set([u])},o.updateQueue=r):(o=r.retryQueue,o===null?r.retryQueue=new Set([u]):o.add(u)),Dp(t,u,p)),!1}throw Error(a(435,o.tag))}return Dp(t,u,p),Nu(),!1}if(ut)return r=Xn.current,r!==null?((r.flags&65536)===0&&(r.flags|=256),r.flags|=65536,r.lanes=p,u!==_h&&(t=Error(a(422),{cause:u}),al(ur(t,o)))):(u!==_h&&(r=Error(a(423),{cause:u}),al(ur(r,o))),t=t.current.alternate,t.flags|=65536,p&=-p,t.lanes|=p,u=ur(u,o),p=ip(t.stateNode,u,p),Nh(t,p),Vt!==4&&(Vt=2)),!1;var g=Error(a(520),{cause:u});if(g=ur(g,o),El===null?El=[g]:El.push(g),Vt!==4&&(Vt=2),r===null)return!0;u=ur(u,o),o=r;do{switch(o.tag){case 3:return o.flags|=65536,t=p&-p,o.lanes|=t,t=ip(o.stateNode,u,t),Nh(o,t),!1;case 1:if(r=o.type,g=o.stateNode,(o.flags&128)===0&&(typeof r.getDerivedStateFromError=="function"||g!==null&&typeof g.componentDidCatch=="function"&&(ai===null||!ai.has(g))))return o.flags|=65536,p&=-p,o.lanes|=p,p=bv(p),vv(p,t,o,u),Nh(o,p),!1}o=o.return}while(o!==null);return!1}var sp=Error(a(461)),en=!1;function Sn(t,r,o,u){r.child=t===null?Eb(r,null,o,u):Ki(r,t.child,o,u)}function Sv(t,r,o,u,p){o=o.render;var g=r.ref;if("ref"in u){var w={};for(var R in u)R!=="ref"&&(w[R]=u[R])}else w=u;return qi(r),u=Ph(t,r,o,w,g,p),R=Bh(),t!==null&&!en?(qh(t,r,p),ma(t,r,p)):(ut&&R&&Ch(r),r.flags|=1,Sn(t,r,u,p),r.child)}function wv(t,r,o,u,p){if(t===null){var g=o.type;return typeof g=="function"&&!vh(g)&&g.defaultProps===void 0&&o.compare===null?(r.tag=15,r.type=g,Cv(t,r,g,u,p)):(t=au(o.type,null,u,r,r.mode,p),t.ref=r.ref,t.return=r,r.child=t)}if(g=t.child,!pp(t,p)){var w=g.memoizedProps;if(o=o.compare,o=o!==null?o:tl,o(w,u)&&t.ref===r.ref)return ma(t,r,p)}return r.flags|=1,t=ua(g,u),t.ref=r.ref,t.return=r,r.child=t}function Cv(t,r,o,u,p){if(t!==null){var g=t.memoizedProps;if(tl(g,u)&&t.ref===r.ref)if(en=!1,r.pendingProps=u=g,pp(t,p))(t.flags&131072)!==0&&(en=!0);else return r.lanes=t.lanes,ma(t,r,p)}return op(t,r,o,u,p)}function Ev(t,r,o,u){var p=u.children,g=t!==null?t.memoizedState:null;if(t===null&&r.stateNode===null&&(r.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),u.mode==="hidden"){if((r.flags&128)!==0){if(g=g!==null?g.baseLanes|o:o,t!==null){for(u=r.child=t.child,p=0;u!==null;)p=p|u.lanes|u.childLanes,u=u.sibling;u=p&~g}else u=0,r.child=null;return _v(t,r,g,o,u)}if((o&536870912)!==0)r.memoizedState={baseLanes:0,cachePool:null},t!==null&&lu(r,g!==null?g.cachePool:null),g!==null?xb(r,g):jh(),Tb(r);else return u=r.lanes=536870912,_v(t,r,g!==null?g.baseLanes|o:o,o,u)}else g!==null?(lu(r,g.cachePool),xb(r,g),ei(),r.memoizedState=null):(t!==null&&lu(r,null),jh(),ei());return Sn(t,r,p,o),r.child}function ml(t,r){return t!==null&&t.tag===22||r.stateNode!==null||(r.stateNode={_visibility:1,_pendingMarkers:null,_retryCache:null,_transitions:null}),r.sibling}function _v(t,r,o,u,p){var g=Oh();return g=g===null?null:{parent:Qt._currentValue,pool:g},r.memoizedState={baseLanes:o,cachePool:g},t!==null&&lu(r,null),jh(),Tb(r),t!==null&&Ls(t,r,u,!0),r.childLanes=p,null}function _u(t,r){return r=xu({mode:r.mode,children:r.children},t.mode),r.ref=t.ref,t.child=r,r.return=t,r}function Av(t,r,o){return Ki(r,t.child,null,o),t=_u(r,r.pendingProps),t.flags|=2,Jn(r),r.memoizedState=null,t}function jx(t,r,o){var u=r.pendingProps,p=(r.flags&128)!==0;if(r.flags&=-129,t===null){if(ut){if(u.mode==="hidden")return t=_u(r,u),r.lanes=536870912,ml(null,t);if(zh(r),(t=Ot)?(t=jS(t,hr),t=t!==null&&t.data==="&"?t:null,t!==null&&(r.memoizedState={dehydrated:t,treeContext:Fa!==null?{id:Wr,overflow:Yr}:null,retryLane:536870912,hydrationErrors:null},o=lb(t),o.return=r,r.child=o,bn=r,Ot=null)):t=null,t===null)throw Wa(r);return r.lanes=536870912,null}return _u(r,u)}var g=t.memoizedState;if(g!==null){var w=g.dehydrated;if(zh(r),p)if(r.flags&256)r.flags&=-257,r=Av(t,r,o);else if(r.memoizedState!==null)r.child=t.child,r.flags|=128,r=null;else throw Error(a(558));else if(en||Ls(t,r,o,!1),p=(o&t.childLanes)!==0,en||p){if(u=Rt,u!==null&&(w=je(u,o),w!==0&&w!==g.retryLane))throw g.retryLane=w,zi(t,w),Bn(u,t,w),sp;Nu(),r=Av(t,r,o)}else t=g.treeContext,Ot=gr(w.nextSibling),bn=r,ut=!0,Ka=null,hr=!1,t!==null&&db(r,t),r=_u(r,u),r.flags|=4096;return r}return t=ua(t.child,{mode:u.mode,children:u.children}),t.ref=r.ref,r.child=t,t.return=r,t}function Au(t,r){var o=r.ref;if(o===null)t!==null&&t.ref!==null&&(r.flags|=4194816);else{if(typeof o!="function"&&typeof o!="object")throw Error(a(284));(t===null||t.ref!==o)&&(r.flags|=4194816)}}function op(t,r,o,u,p){return qi(r),o=Ph(t,r,o,u,void 0,p),u=Bh(),t!==null&&!en?(qh(t,r,p),ma(t,r,p)):(ut&&u&&Ch(r),r.flags|=1,Sn(t,r,o,p),r.child)}function xv(t,r,o,u,p,g){return qi(r),r.updateQueue=null,o=Rb(r,u,o,p),kb(t),u=Bh(),t!==null&&!en?(qh(t,r,g),ma(t,r,g)):(ut&&u&&Ch(r),r.flags|=1,Sn(t,r,o,g),r.child)}function Tv(t,r,o,u,p){if(qi(r),r.stateNode===null){var g=ks,w=o.contextType;typeof w=="object"&&w!==null&&(g=vn(w)),g=new o(u,g),r.memoizedState=g.state!==null&&g.state!==void 0?g.state:null,g.updater=ap,r.stateNode=g,g._reactInternals=r,g=r.stateNode,g.props=u,g.state=r.memoizedState,g.refs={},Mh(r),w=o.contextType,g.context=typeof w=="object"&&w!==null?vn(w):ks,g.state=r.memoizedState,w=o.getDerivedStateFromProps,typeof w=="function"&&(rp(r,o,w,u),g.state=r.memoizedState),typeof o.getDerivedStateFromProps=="function"||typeof g.getSnapshotBeforeUpdate=="function"||typeof g.UNSAFE_componentWillMount!="function"&&typeof g.componentWillMount!="function"||(w=g.state,typeof g.componentWillMount=="function"&&g.componentWillMount(),typeof g.UNSAFE_componentWillMount=="function"&&g.UNSAFE_componentWillMount(),w!==g.state&&ap.enqueueReplaceState(g,g.state,null),dl(r,u,g,p),ul(),g.state=r.memoizedState),typeof g.componentDidMount=="function"&&(r.flags|=4194308),u=!0}else if(t===null){g=r.stateNode;var R=r.memoizedProps,z=Yi(o,R);g.props=z;var X=g.context,re=o.contextType;w=ks,typeof re=="object"&&re!==null&&(w=vn(re));var le=o.getDerivedStateFromProps;re=typeof le=="function"||typeof g.getSnapshotBeforeUpdate=="function",R=r.pendingProps!==R,re||typeof g.UNSAFE_componentWillReceiveProps!="function"&&typeof g.componentWillReceiveProps!="function"||(R||X!==w)&&hv(r,g,u,w),Xa=!1;var Q=r.memoizedState;g.state=Q,dl(r,u,g,p),ul(),X=r.memoizedState,R||Q!==X||Xa?(typeof le=="function"&&(rp(r,o,le,u),X=r.memoizedState),(z=Xa||fv(r,o,z,u,Q,X,w))?(re||typeof g.UNSAFE_componentWillMount!="function"&&typeof g.componentWillMount!="function"||(typeof g.componentWillMount=="function"&&g.componentWillMount(),typeof g.UNSAFE_componentWillMount=="function"&&g.UNSAFE_componentWillMount()),typeof g.componentDidMount=="function"&&(r.flags|=4194308)):(typeof g.componentDidMount=="function"&&(r.flags|=4194308),r.memoizedProps=u,r.memoizedState=X),g.props=u,g.state=X,g.context=w,u=z):(typeof g.componentDidMount=="function"&&(r.flags|=4194308),u=!1)}else{g=r.stateNode,$h(t,r),w=r.memoizedProps,re=Yi(o,w),g.props=re,le=r.pendingProps,Q=g.context,X=o.contextType,z=ks,typeof X=="object"&&X!==null&&(z=vn(X)),R=o.getDerivedStateFromProps,(X=typeof R=="function"||typeof g.getSnapshotBeforeUpdate=="function")||typeof g.UNSAFE_componentWillReceiveProps!="function"&&typeof g.componentWillReceiveProps!="function"||(w!==le||Q!==z)&&hv(r,g,u,z),Xa=!1,Q=r.memoizedState,g.state=Q,dl(r,u,g,p),ul();var te=r.memoizedState;w!==le||Q!==te||Xa||t!==null&&t.dependencies!==null&&su(t.dependencies)?(typeof R=="function"&&(rp(r,o,R,u),te=r.memoizedState),(re=Xa||fv(r,o,re,u,Q,te,z)||t!==null&&t.dependencies!==null&&su(t.dependencies))?(X||typeof g.UNSAFE_componentWillUpdate!="function"&&typeof g.componentWillUpdate!="function"||(typeof g.componentWillUpdate=="function"&&g.componentWillUpdate(u,te,z),typeof g.UNSAFE_componentWillUpdate=="function"&&g.UNSAFE_componentWillUpdate(u,te,z)),typeof g.componentDidUpdate=="function"&&(r.flags|=4),typeof g.getSnapshotBeforeUpdate=="function"&&(r.flags|=1024)):(typeof g.componentDidUpdate!="function"||w===t.memoizedProps&&Q===t.memoizedState||(r.flags|=4),typeof g.getSnapshotBeforeUpdate!="function"||w===t.memoizedProps&&Q===t.memoizedState||(r.flags|=1024),r.memoizedProps=u,r.memoizedState=te),g.props=u,g.state=te,g.context=z,u=re):(typeof g.componentDidUpdate!="function"||w===t.memoizedProps&&Q===t.memoizedState||(r.flags|=4),typeof g.getSnapshotBeforeUpdate!="function"||w===t.memoizedProps&&Q===t.memoizedState||(r.flags|=1024),u=!1)}return g=u,Au(t,r),u=(r.flags&128)!==0,g||u?(g=r.stateNode,o=u&&typeof o.getDerivedStateFromError!="function"?null:g.render(),r.flags|=1,t!==null&&u?(r.child=Ki(r,t.child,null,p),r.child=Ki(r,null,o,p)):Sn(t,r,o,p),r.memoizedState=g.state,t=r.child):t=ma(t,r,p),t}function kv(t,r,o,u){return Pi(),r.flags|=256,Sn(t,r,o,u),r.child}var lp={dehydrated:null,treeContext:null,retryLane:0,hydrationErrors:null};function cp(t){return{baseLanes:t,cachePool:yb()}}function up(t,r,o){return t=t!==null?t.childLanes&~o:0,r&&(t|=Zn),t}function Rv(t,r,o){var u=r.pendingProps,p=!1,g=(r.flags&128)!==0,w;if((w=g)||(w=t!==null&&t.memoizedState===null?!1:(Wt.current&2)!==0),w&&(p=!0,r.flags&=-129),w=(r.flags&32)!==0,r.flags&=-33,t===null){if(ut){if(p?Za(r):ei(),(t=Ot)?(t=jS(t,hr),t=t!==null&&t.data!=="&"?t:null,t!==null&&(r.memoizedState={dehydrated:t,treeContext:Fa!==null?{id:Wr,overflow:Yr}:null,retryLane:536870912,hydrationErrors:null},o=lb(t),o.return=r,r.child=o,bn=r,Ot=null)):t=null,t===null)throw Wa(r);return Fp(t)?r.lanes=32:r.lanes=536870912,null}var R=u.children;return u=u.fallback,p?(ei(),p=r.mode,R=xu({mode:"hidden",children:R},p),u=Ui(u,p,o,null),R.return=r,u.return=r,R.sibling=u,r.child=R,u=r.child,u.memoizedState=cp(o),u.childLanes=up(t,w,o),r.memoizedState=lp,ml(null,u)):(Za(r),dp(r,R))}var z=t.memoizedState;if(z!==null&&(R=z.dehydrated,R!==null)){if(g)r.flags&256?(Za(r),r.flags&=-257,r=fp(t,r,o)):r.memoizedState!==null?(ei(),r.child=t.child,r.flags|=128,r=null):(ei(),R=u.fallback,p=r.mode,u=xu({mode:"visible",children:u.children},p),R=Ui(R,p,o,null),R.flags|=2,u.return=r,R.return=r,u.sibling=R,r.child=u,Ki(r,t.child,null,o),u=r.child,u.memoizedState=cp(o),u.childLanes=up(t,w,o),r.memoizedState=lp,r=ml(null,u));else if(Za(r),Fp(R)){if(w=R.nextSibling&&R.nextSibling.dataset,w)var X=w.dgst;w=X,u=Error(a(419)),u.stack="",u.digest=w,al({value:u,source:null,stack:null}),r=fp(t,r,o)}else if(en||Ls(t,r,o,!1),w=(o&t.childLanes)!==0,en||w){if(w=Rt,w!==null&&(u=je(w,o),u!==0&&u!==z.retryLane))throw z.retryLane=u,zi(t,u),Bn(w,t,u),sp;Vp(R)||Nu(),r=fp(t,r,o)}else Vp(R)?(r.flags|=192,r.child=t.child,r=null):(t=z.treeContext,Ot=gr(R.nextSibling),bn=r,ut=!0,Ka=null,hr=!1,t!==null&&db(r,t),r=dp(r,u.children),r.flags|=4096);return r}return p?(ei(),R=u.fallback,p=r.mode,z=t.child,X=z.sibling,u=ua(z,{mode:"hidden",children:u.children}),u.subtreeFlags=z.subtreeFlags&65011712,X!==null?R=ua(X,R):(R=Ui(R,p,o,null),R.flags|=2),R.return=r,u.return=r,u.sibling=R,r.child=u,ml(null,u),u=r.child,R=t.child.memoizedState,R===null?R=cp(o):(p=R.cachePool,p!==null?(z=Qt._currentValue,p=p.parent!==z?{parent:z,pool:z}:p):p=yb(),R={baseLanes:R.baseLanes|o,cachePool:p}),u.memoizedState=R,u.childLanes=up(t,w,o),r.memoizedState=lp,ml(t.child,u)):(Za(r),o=t.child,t=o.sibling,o=ua(o,{mode:"visible",children:u.children}),o.return=r,o.sibling=null,t!==null&&(w=r.deletions,w===null?(r.deletions=[t],r.flags|=16):w.push(t)),r.child=o,r.memoizedState=null,o)}function dp(t,r){return r=xu({mode:"visible",children:r},t.mode),r.return=t,t.child=r}function xu(t,r){return t=Yn(22,t,null,r),t.lanes=0,t}function fp(t,r,o){return Ki(r,t.child,null,o),t=dp(r,r.pendingProps.children),t.flags|=2,r.memoizedState=null,t}function Dv(t,r,o){t.lanes|=r;var u=t.alternate;u!==null&&(u.lanes|=r),Th(t.return,r,o)}function hp(t,r,o,u,p,g){var w=t.memoizedState;w===null?t.memoizedState={isBackwards:r,rendering:null,renderingStartTime:0,last:u,tail:o,tailMode:p,treeForkCount:g}:(w.isBackwards=r,w.rendering=null,w.renderingStartTime=0,w.last=u,w.tail=o,w.tailMode=p,w.treeForkCount=g)}function Ov(t,r,o){var u=r.pendingProps,p=u.revealOrder,g=u.tail;u=u.children;var w=Wt.current,R=(w&2)!==0;if(R?(w=w&1|2,r.flags|=128):w&=1,ce(Wt,w),Sn(t,r,u,o),u=ut?rl:0,!R&&t!==null&&(t.flags&128)!==0)e:for(t=r.child;t!==null;){if(t.tag===13)t.memoizedState!==null&&Dv(t,o,r);else if(t.tag===19)Dv(t,o,r);else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===r)break e;for(;t.sibling===null;){if(t.return===null||t.return===r)break e;t=t.return}t.sibling.return=t.return,t=t.sibling}switch(p){case"forwards":for(o=r.child,p=null;o!==null;)t=o.alternate,t!==null&&pu(t)===null&&(p=o),o=o.sibling;o=p,o===null?(p=r.child,r.child=null):(p=o.sibling,o.sibling=null),hp(r,!1,p,o,g,u);break;case"backwards":case"unstable_legacy-backwards":for(o=null,p=r.child,r.child=null;p!==null;){if(t=p.alternate,t!==null&&pu(t)===null){r.child=p;break}t=p.sibling,p.sibling=o,o=p,p=t}hp(r,!0,o,null,g,u);break;case"together":hp(r,!1,null,null,void 0,u);break;default:r.memoizedState=null}return r.child}function ma(t,r,o){if(t!==null&&(r.dependencies=t.dependencies),ri|=r.lanes,(o&r.childLanes)===0)if(t!==null){if(Ls(t,r,o,!1),(o&r.childLanes)===0)return null}else return null;if(t!==null&&r.child!==t.child)throw Error(a(153));if(r.child!==null){for(t=r.child,o=ua(t,t.pendingProps),r.child=o,o.return=r;t.sibling!==null;)t=t.sibling,o=o.sibling=ua(t,t.pendingProps),o.return=r;o.sibling=null}return r.child}function pp(t,r){return(t.lanes&r)!==0?!0:(t=t.dependencies,!!(t!==null&&su(t)))}function Hx(t,r,o){switch(r.tag){case 3:_e(r,r.stateNode.containerInfo),Ya(r,Qt,t.memoizedState.cache),Pi();break;case 27:case 5:$e(r);break;case 4:_e(r,r.stateNode.containerInfo);break;case 10:Ya(r,r.type,r.memoizedProps.value);break;case 31:if(r.memoizedState!==null)return r.flags|=128,zh(r),null;break;case 13:var u=r.memoizedState;if(u!==null)return u.dehydrated!==null?(Za(r),r.flags|=128,null):(o&r.child.childLanes)!==0?Rv(t,r,o):(Za(r),t=ma(t,r,o),t!==null?t.sibling:null);Za(r);break;case 19:var p=(t.flags&128)!==0;if(u=(o&r.childLanes)!==0,u||(Ls(t,r,o,!1),u=(o&r.childLanes)!==0),p){if(u)return Ov(t,r,o);r.flags|=128}if(p=r.memoizedState,p!==null&&(p.rendering=null,p.tail=null,p.lastEffect=null),ce(Wt,Wt.current),u)break;return null;case 22:return r.lanes=0,Ev(t,r,o,r.pendingProps);case 24:Ya(r,Qt,t.memoizedState.cache)}return ma(t,r,o)}function Lv(t,r,o){if(t!==null)if(t.memoizedProps!==r.pendingProps)en=!0;else{if(!pp(t,o)&&(r.flags&128)===0)return en=!1,Hx(t,r,o);en=(t.flags&131072)!==0}else en=!1,ut&&(r.flags&1048576)!==0&&ub(r,rl,r.index);switch(r.lanes=0,r.tag){case 16:e:{var u=r.pendingProps;if(t=Vi(r.elementType),r.type=t,typeof t=="function")vh(t)?(u=Yi(t,u),r.tag=1,r=Tv(null,r,t,u,o)):(r.tag=0,r=op(null,r,t,u,o));else{if(t!=null){var p=t.$$typeof;if(p===O){r.tag=11,r=Sv(null,r,t,u,o);break e}else if(p===P){r.tag=14,r=wv(null,r,t,u,o);break e}}throw r=qe(t)||t,Error(a(306,r,""))}}return r;case 0:return op(t,r,r.type,r.pendingProps,o);case 1:return u=r.type,p=Yi(u,r.pendingProps),Tv(t,r,u,p,o);case 3:e:{if(_e(r,r.stateNode.containerInfo),t===null)throw Error(a(387));u=r.pendingProps;var g=r.memoizedState;p=g.element,$h(t,r),dl(r,u,null,o);var w=r.memoizedState;if(u=w.cache,Ya(r,Qt,u),u!==g.cache&&kh(r,[Qt],o,!0),ul(),u=w.element,g.isDehydrated)if(g={element:u,isDehydrated:!1,cache:w.cache},r.updateQueue.baseState=g,r.memoizedState=g,r.flags&256){r=kv(t,r,u,o);break e}else if(u!==p){p=ur(Error(a(424)),r),al(p),r=kv(t,r,u,o);break e}else for(t=r.stateNode.containerInfo,t.nodeType===9?t=t.body:t=t.nodeName==="HTML"?t.ownerDocument.body:t,Ot=gr(t.firstChild),bn=r,ut=!0,Ka=null,hr=!0,o=Eb(r,null,u,o),r.child=o;o;)o.flags=o.flags&-3|4096,o=o.sibling;else{if(Pi(),u===p){r=ma(t,r,o);break e}Sn(t,r,u,o)}r=r.child}return r;case 26:return Au(t,r),t===null?(o=qS(r.type,null,r.pendingProps,null))?r.memoizedState=o:ut||(o=r.type,t=r.pendingProps,u=Bu(ie.current).createElement(o),u[Ht]=r,u[An]=t,wn(u,o,t),Jt(u),r.stateNode=u):r.memoizedState=qS(r.type,t.memoizedProps,r.pendingProps,t.memoizedState),null;case 27:return $e(r),t===null&&ut&&(u=r.stateNode=US(r.type,r.pendingProps,ie.current),bn=r,hr=!0,p=Ot,li(r.type)?(Kp=p,Ot=gr(u.firstChild)):Ot=p),Sn(t,r,r.pendingProps.children,o),Au(t,r),t===null&&(r.flags|=4194304),r.child;case 5:return t===null&&ut&&((p=u=Ot)&&(u=pT(u,r.type,r.pendingProps,hr),u!==null?(r.stateNode=u,bn=r,Ot=gr(u.firstChild),hr=!1,p=!0):p=!1),p||Wa(r)),$e(r),p=r.type,g=r.pendingProps,w=t!==null?t.memoizedProps:null,u=g.children,Bp(p,g)?u=null:w!==null&&Bp(p,w)&&(r.flags|=32),r.memoizedState!==null&&(p=Ph(t,r,Rx,null,null,o),Ol._currentValue=p),Au(t,r),Sn(t,r,u,o),r.child;case 6:return t===null&&ut&&((t=o=Ot)&&(o=gT(o,r.pendingProps,hr),o!==null?(r.stateNode=o,bn=r,Ot=null,t=!0):t=!1),t||Wa(r)),null;case 13:return Rv(t,r,o);case 4:return _e(r,r.stateNode.containerInfo),u=r.pendingProps,t===null?r.child=Ki(r,null,u,o):Sn(t,r,u,o),r.child;case 11:return Sv(t,r,r.type,r.pendingProps,o);case 7:return Sn(t,r,r.pendingProps,o),r.child;case 8:return Sn(t,r,r.pendingProps.children,o),r.child;case 12:return Sn(t,r,r.pendingProps.children,o),r.child;case 10:return u=r.pendingProps,Ya(r,r.type,u.value),Sn(t,r,u.children,o),r.child;case 9:return p=r.type._context,u=r.pendingProps.children,qi(r),p=vn(p),u=u(p),r.flags|=1,Sn(t,r,u,o),r.child;case 14:return wv(t,r,r.type,r.pendingProps,o);case 15:return Cv(t,r,r.type,r.pendingProps,o);case 19:return Ov(t,r,o);case 31:return jx(t,r,o);case 22:return Ev(t,r,o,r.pendingProps);case 24:return qi(r),u=vn(Qt),t===null?(p=Oh(),p===null&&(p=Rt,g=Rh(),p.pooledCache=g,g.refCount++,g!==null&&(p.pooledCacheLanes|=o),p=g),r.memoizedState={parent:u,cache:p},Mh(r),Ya(r,Qt,p)):((t.lanes&o)!==0&&($h(t,r),dl(r,null,null,o),ul()),p=t.memoizedState,g=r.memoizedState,p.parent!==u?(p={parent:u,cache:u},r.memoizedState=p,r.lanes===0&&(r.memoizedState=r.updateQueue.baseState=p),Ya(r,Qt,u)):(u=g.cache,Ya(r,Qt,u),u!==p.cache&&kh(r,[Qt],o,!0))),Sn(t,r,r.pendingProps.children,o),r.child;case 29:throw r.pendingProps}throw Error(a(156,r.tag))}function ya(t){t.flags|=4}function gp(t,r,o,u,p){if((r=(t.mode&32)!==0)&&(r=!1),r){if(t.flags|=16777216,(p&335544128)===p)if(t.stateNode.complete)t.flags|=8192;else if(iS())t.flags|=8192;else throw Fi=uu,Lh}else t.flags&=-16777217}function Mv(t,r){if(r.type!=="stylesheet"||(r.state.loading&4)!==0)t.flags&=-16777217;else if(t.flags|=16777216,!WS(r))if(iS())t.flags|=8192;else throw Fi=uu,Lh}function Tu(t,r){r!==null&&(t.flags|=4),t.flags&16384&&(r=t.tag!==22?Ne():536870912,t.lanes|=r,Gs|=r)}function yl(t,r){if(!ut)switch(t.tailMode){case"hidden":r=t.tail;for(var o=null;r!==null;)r.alternate!==null&&(o=r),r=r.sibling;o===null?t.tail=null:o.sibling=null;break;case"collapsed":o=t.tail;for(var u=null;o!==null;)o.alternate!==null&&(u=o),o=o.sibling;u===null?r||t.tail===null?t.tail=null:t.tail.sibling=null:u.sibling=null}}function Lt(t){var r=t.alternate!==null&&t.alternate.child===t.child,o=0,u=0;if(r)for(var p=t.child;p!==null;)o|=p.lanes|p.childLanes,u|=p.subtreeFlags&65011712,u|=p.flags&65011712,p.return=t,p=p.sibling;else for(p=t.child;p!==null;)o|=p.lanes|p.childLanes,u|=p.subtreeFlags,u|=p.flags,p.return=t,p=p.sibling;return t.subtreeFlags|=u,t.childLanes=o,r}function zx(t,r,o){var u=r.pendingProps;switch(Eh(r),r.tag){case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Lt(r),null;case 1:return Lt(r),null;case 3:return o=r.stateNode,u=null,t!==null&&(u=t.memoizedState.cache),r.memoizedState.cache!==u&&(r.flags|=2048),ha(Qt),Te(),o.pendingContext&&(o.context=o.pendingContext,o.pendingContext=null),(t===null||t.child===null)&&(Os(r)?ya(r):t===null||t.memoizedState.isDehydrated&&(r.flags&256)===0||(r.flags|=1024,Ah())),Lt(r),null;case 26:var p=r.type,g=r.memoizedState;return t===null?(ya(r),g!==null?(Lt(r),Mv(r,g)):(Lt(r),gp(r,p,null,u,o))):g?g!==t.memoizedState?(ya(r),Lt(r),Mv(r,g)):(Lt(r),r.flags&=-16777217):(t=t.memoizedProps,t!==u&&ya(r),Lt(r),gp(r,p,t,u,o)),null;case 27:if(pt(r),o=ie.current,p=r.type,t!==null&&r.stateNode!=null)t.memoizedProps!==u&&ya(r);else{if(!u){if(r.stateNode===null)throw Error(a(166));return Lt(r),null}t=de.current,Os(r)?fb(r):(t=US(p,u,o),r.stateNode=t,ya(r))}return Lt(r),null;case 5:if(pt(r),p=r.type,t!==null&&r.stateNode!=null)t.memoizedProps!==u&&ya(r);else{if(!u){if(r.stateNode===null)throw Error(a(166));return Lt(r),null}if(g=de.current,Os(r))fb(r);else{var w=Bu(ie.current);switch(g){case 1:g=w.createElementNS("http://www.w3.org/2000/svg",p);break;case 2:g=w.createElementNS("http://www.w3.org/1998/Math/MathML",p);break;default:switch(p){case"svg":g=w.createElementNS("http://www.w3.org/2000/svg",p);break;case"math":g=w.createElementNS("http://www.w3.org/1998/Math/MathML",p);break;case"script":g=w.createElement("div"),g.innerHTML="<script><\/script>",g=g.removeChild(g.firstChild);break;case"select":g=typeof u.is=="string"?w.createElement("select",{is:u.is}):w.createElement("select"),u.multiple?g.multiple=!0:u.size&&(g.size=u.size);break;default:g=typeof u.is=="string"?w.createElement(p,{is:u.is}):w.createElement(p)}}g[Ht]=r,g[An]=u;e:for(w=r.child;w!==null;){if(w.tag===5||w.tag===6)g.appendChild(w.stateNode);else if(w.tag!==4&&w.tag!==27&&w.child!==null){w.child.return=w,w=w.child;continue}if(w===r)break e;for(;w.sibling===null;){if(w.return===null||w.return===r)break e;w=w.return}w.sibling.return=w.return,w=w.sibling}r.stateNode=g;e:switch(wn(g,p,u),p){case"button":case"input":case"select":case"textarea":u=!!u.autoFocus;break e;case"img":u=!0;break e;default:u=!1}u&&ya(r)}}return Lt(r),gp(r,r.type,t===null?null:t.memoizedProps,r.pendingProps,o),null;case 6:if(t&&r.stateNode!=null)t.memoizedProps!==u&&ya(r);else{if(typeof u!="string"&&r.stateNode===null)throw Error(a(166));if(t=ie.current,Os(r)){if(t=r.stateNode,o=r.memoizedProps,u=null,p=bn,p!==null)switch(p.tag){case 27:case 5:u=p.memoizedProps}t[Ht]=r,t=!!(t.nodeValue===o||u!==null&&u.suppressHydrationWarning===!0||RS(t.nodeValue,o)),t||Wa(r,!0)}else t=Bu(t).createTextNode(u),t[Ht]=r,r.stateNode=t}return Lt(r),null;case 31:if(o=r.memoizedState,t===null||t.memoizedState!==null){if(u=Os(r),o!==null){if(t===null){if(!u)throw Error(a(318));if(t=r.memoizedState,t=t!==null?t.dehydrated:null,!t)throw Error(a(557));t[Ht]=r}else Pi(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Lt(r),t=!1}else o=Ah(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=o),t=!0;if(!t)return r.flags&256?(Jn(r),r):(Jn(r),null);if((r.flags&128)!==0)throw Error(a(558))}return Lt(r),null;case 13:if(u=r.memoizedState,t===null||t.memoizedState!==null&&t.memoizedState.dehydrated!==null){if(p=Os(r),u!==null&&u.dehydrated!==null){if(t===null){if(!p)throw Error(a(318));if(p=r.memoizedState,p=p!==null?p.dehydrated:null,!p)throw Error(a(317));p[Ht]=r}else Pi(),(r.flags&128)===0&&(r.memoizedState=null),r.flags|=4;Lt(r),p=!1}else p=Ah(),t!==null&&t.memoizedState!==null&&(t.memoizedState.hydrationErrors=p),p=!0;if(!p)return r.flags&256?(Jn(r),r):(Jn(r),null)}return Jn(r),(r.flags&128)!==0?(r.lanes=o,r):(o=u!==null,t=t!==null&&t.memoizedState!==null,o&&(u=r.child,p=null,u.alternate!==null&&u.alternate.memoizedState!==null&&u.alternate.memoizedState.cachePool!==null&&(p=u.alternate.memoizedState.cachePool.pool),g=null,u.memoizedState!==null&&u.memoizedState.cachePool!==null&&(g=u.memoizedState.cachePool.pool),g!==p&&(u.flags|=2048)),o!==t&&o&&(r.child.flags|=8192),Tu(r,r.updateQueue),Lt(r),null);case 4:return Te(),t===null&&jp(r.stateNode.containerInfo),Lt(r),null;case 10:return ha(r.type),Lt(r),null;case 19:if(T(Wt),u=r.memoizedState,u===null)return Lt(r),null;if(p=(r.flags&128)!==0,g=u.rendering,g===null)if(p)yl(u,!1);else{if(Vt!==0||t!==null&&(t.flags&128)!==0)for(t=r.child;t!==null;){if(g=pu(t),g!==null){for(r.flags|=128,yl(u,!1),t=g.updateQueue,r.updateQueue=t,Tu(r,t),r.subtreeFlags=0,t=o,o=r.child;o!==null;)ob(o,t),o=o.sibling;return ce(Wt,Wt.current&1|2),ut&&da(r,u.treeForkCount),r.child}t=t.sibling}u.tail!==null&&qt()>Lu&&(r.flags|=128,p=!0,yl(u,!1),r.lanes=4194304)}else{if(!p)if(t=pu(g),t!==null){if(r.flags|=128,p=!0,t=t.updateQueue,r.updateQueue=t,Tu(r,t),yl(u,!0),u.tail===null&&u.tailMode==="hidden"&&!g.alternate&&!ut)return Lt(r),null}else 2*qt()-u.renderingStartTime>Lu&&o!==536870912&&(r.flags|=128,p=!0,yl(u,!1),r.lanes=4194304);u.isBackwards?(g.sibling=r.child,r.child=g):(t=u.last,t!==null?t.sibling=g:r.child=g,u.last=g)}return u.tail!==null?(t=u.tail,u.rendering=t,u.tail=t.sibling,u.renderingStartTime=qt(),t.sibling=null,o=Wt.current,ce(Wt,p?o&1|2:o&1),ut&&da(r,u.treeForkCount),t):(Lt(r),null);case 22:case 23:return Jn(r),Hh(),u=r.memoizedState!==null,t!==null?t.memoizedState!==null!==u&&(r.flags|=8192):u&&(r.flags|=8192),u?(o&536870912)!==0&&(r.flags&128)===0&&(Lt(r),r.subtreeFlags&6&&(r.flags|=8192)):Lt(r),o=r.updateQueue,o!==null&&Tu(r,o.retryQueue),o=null,t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(o=t.memoizedState.cachePool.pool),u=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(u=r.memoizedState.cachePool.pool),u!==o&&(r.flags|=2048),t!==null&&T(Gi),null;case 24:return o=null,t!==null&&(o=t.memoizedState.cache),r.memoizedState.cache!==o&&(r.flags|=2048),ha(Qt),Lt(r),null;case 25:return null;case 30:return null}throw Error(a(156,r.tag))}function Ux(t,r){switch(Eh(r),r.tag){case 1:return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 3:return ha(Qt),Te(),t=r.flags,(t&65536)!==0&&(t&128)===0?(r.flags=t&-65537|128,r):null;case 26:case 27:case 5:return pt(r),null;case 31:if(r.memoizedState!==null){if(Jn(r),r.alternate===null)throw Error(a(340));Pi()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 13:if(Jn(r),t=r.memoizedState,t!==null&&t.dehydrated!==null){if(r.alternate===null)throw Error(a(340));Pi()}return t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 19:return T(Wt),null;case 4:return Te(),null;case 10:return ha(r.type),null;case 22:case 23:return Jn(r),Hh(),t!==null&&T(Gi),t=r.flags,t&65536?(r.flags=t&-65537|128,r):null;case 24:return ha(Qt),null;case 25:return null;default:return null}}function $v(t,r){switch(Eh(r),r.tag){case 3:ha(Qt),Te();break;case 26:case 27:case 5:pt(r);break;case 4:Te();break;case 31:r.memoizedState!==null&&Jn(r);break;case 13:Jn(r);break;case 19:T(Wt);break;case 10:ha(r.type);break;case 22:case 23:Jn(r),Hh(),t!==null&&T(Gi);break;case 24:ha(Qt)}}function bl(t,r){try{var o=r.updateQueue,u=o!==null?o.lastEffect:null;if(u!==null){var p=u.next;o=p;do{if((o.tag&t)===t){u=void 0;var g=o.create,w=o.inst;u=g(),w.destroy=u}o=o.next}while(o!==p)}}catch(R){_t(r,r.return,R)}}function ti(t,r,o){try{var u=r.updateQueue,p=u!==null?u.lastEffect:null;if(p!==null){var g=p.next;u=g;do{if((u.tag&t)===t){var w=u.inst,R=w.destroy;if(R!==void 0){w.destroy=void 0,p=r;var z=o,X=R;try{X()}catch(re){_t(p,z,re)}}}u=u.next}while(u!==g)}}catch(re){_t(r,r.return,re)}}function Nv(t){var r=t.updateQueue;if(r!==null){var o=t.stateNode;try{Ab(r,o)}catch(u){_t(t,t.return,u)}}}function Iv(t,r,o){o.props=Yi(t.type,t.memoizedProps),o.state=t.memoizedState;try{o.componentWillUnmount()}catch(u){_t(t,r,u)}}function vl(t,r){try{var o=t.ref;if(o!==null){switch(t.tag){case 26:case 27:case 5:var u=t.stateNode;break;case 30:u=t.stateNode;break;default:u=t.stateNode}typeof o=="function"?t.refCleanup=o(u):o.current=u}}catch(p){_t(t,r,p)}}function Xr(t,r){var o=t.ref,u=t.refCleanup;if(o!==null)if(typeof u=="function")try{u()}catch(p){_t(t,r,p)}finally{t.refCleanup=null,t=t.alternate,t!=null&&(t.refCleanup=null)}else if(typeof o=="function")try{o(null)}catch(p){_t(t,r,p)}else o.current=null}function jv(t){var r=t.type,o=t.memoizedProps,u=t.stateNode;try{e:switch(r){case"button":case"input":case"select":case"textarea":o.autoFocus&&u.focus();break e;case"img":o.src?u.src=o.src:o.srcSet&&(u.srcset=o.srcSet)}}catch(p){_t(t,t.return,p)}}function mp(t,r,o){try{var u=t.stateNode;lT(u,t.type,o,r),u[An]=r}catch(p){_t(t,t.return,p)}}function Hv(t){return t.tag===5||t.tag===3||t.tag===26||t.tag===27&&li(t.type)||t.tag===4}function yp(t){e:for(;;){for(;t.sibling===null;){if(t.return===null||Hv(t.return))return null;t=t.return}for(t.sibling.return=t.return,t=t.sibling;t.tag!==5&&t.tag!==6&&t.tag!==18;){if(t.tag===27&&li(t.type)||t.flags&2||t.child===null||t.tag===4)continue e;t.child.return=t,t=t.child}if(!(t.flags&2))return t.stateNode}}function bp(t,r,o){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?(o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o).insertBefore(t,r):(r=o.nodeType===9?o.body:o.nodeName==="HTML"?o.ownerDocument.body:o,r.appendChild(t),o=o._reactRootContainer,o!=null||r.onclick!==null||(r.onclick=Rr));else if(u!==4&&(u===27&&li(t.type)&&(o=t.stateNode,r=null),t=t.child,t!==null))for(bp(t,r,o),t=t.sibling;t!==null;)bp(t,r,o),t=t.sibling}function ku(t,r,o){var u=t.tag;if(u===5||u===6)t=t.stateNode,r?o.insertBefore(t,r):o.appendChild(t);else if(u!==4&&(u===27&&li(t.type)&&(o=t.stateNode),t=t.child,t!==null))for(ku(t,r,o),t=t.sibling;t!==null;)ku(t,r,o),t=t.sibling}function zv(t){var r=t.stateNode,o=t.memoizedProps;try{for(var u=t.type,p=r.attributes;p.length;)r.removeAttributeNode(p[0]);wn(r,u,o),r[Ht]=t,r[An]=o}catch(g){_t(t,t.return,g)}}var ba=!1,tn=!1,vp=!1,Uv=typeof WeakSet=="function"?WeakSet:Set,fn=null;function Px(t,r){if(t=t.containerInfo,Up=Yu,t=Qy(t),fh(t)){if("selectionStart"in t)var o={start:t.selectionStart,end:t.selectionEnd};else e:{o=(o=t.ownerDocument)&&o.defaultView||window;var u=o.getSelection&&o.getSelection();if(u&&u.rangeCount!==0){o=u.anchorNode;var p=u.anchorOffset,g=u.focusNode;u=u.focusOffset;try{o.nodeType,g.nodeType}catch{o=null;break e}var w=0,R=-1,z=-1,X=0,re=0,le=t,Q=null;t:for(;;){for(var te;le!==o||p!==0&&le.nodeType!==3||(R=w+p),le!==g||u!==0&&le.nodeType!==3||(z=w+u),le.nodeType===3&&(w+=le.nodeValue.length),(te=le.firstChild)!==null;)Q=le,le=te;for(;;){if(le===t)break t;if(Q===o&&++X===p&&(R=w),Q===g&&++re===u&&(z=w),(te=le.nextSibling)!==null)break;le=Q,Q=le.parentNode}le=te}o=R===-1||z===-1?null:{start:R,end:z}}else o=null}o=o||{start:0,end:0}}else o=null;for(Pp={focusedElem:t,selectionRange:o},Yu=!1,fn=r;fn!==null;)if(r=fn,t=r.child,(r.subtreeFlags&1028)!==0&&t!==null)t.return=r,fn=t;else for(;fn!==null;){switch(r=fn,g=r.alternate,t=r.flags,r.tag){case 0:if((t&4)!==0&&(t=r.updateQueue,t=t!==null?t.events:null,t!==null))for(o=0;o<t.length;o++)p=t[o],p.ref.impl=p.nextImpl;break;case 11:case 15:break;case 1:if((t&1024)!==0&&g!==null){t=void 0,o=r,p=g.memoizedProps,g=g.memoizedState,u=o.stateNode;try{var De=Yi(o.type,p);t=u.getSnapshotBeforeUpdate(De,g),u.__reactInternalSnapshotBeforeUpdate=t}catch(Be){_t(o,o.return,Be)}}break;case 3:if((t&1024)!==0){if(t=r.stateNode.containerInfo,o=t.nodeType,o===9)Gp(t);else if(o===1)switch(t.nodeName){case"HEAD":case"HTML":case"BODY":Gp(t);break;default:t.textContent=""}}break;case 5:case 26:case 27:case 6:case 4:case 17:break;default:if((t&1024)!==0)throw Error(a(163))}if(t=r.sibling,t!==null){t.return=r.return,fn=t;break}fn=r.return}}function Pv(t,r,o){var u=o.flags;switch(o.tag){case 0:case 11:case 15:Sa(t,o),u&4&&bl(5,o);break;case 1:if(Sa(t,o),u&4)if(t=o.stateNode,r===null)try{t.componentDidMount()}catch(w){_t(o,o.return,w)}else{var p=Yi(o.type,r.memoizedProps);r=r.memoizedState;try{t.componentDidUpdate(p,r,t.__reactInternalSnapshotBeforeUpdate)}catch(w){_t(o,o.return,w)}}u&64&&Nv(o),u&512&&vl(o,o.return);break;case 3:if(Sa(t,o),u&64&&(t=o.updateQueue,t!==null)){if(r=null,o.child!==null)switch(o.child.tag){case 27:case 5:r=o.child.stateNode;break;case 1:r=o.child.stateNode}try{Ab(t,r)}catch(w){_t(o,o.return,w)}}break;case 27:r===null&&u&4&&zv(o);case 26:case 5:Sa(t,o),r===null&&u&4&&jv(o),u&512&&vl(o,o.return);break;case 12:Sa(t,o);break;case 31:Sa(t,o),u&4&&Gv(t,o);break;case 13:Sa(t,o),u&4&&Vv(t,o),u&64&&(t=o.memoizedState,t!==null&&(t=t.dehydrated,t!==null&&(o=Xx.bind(null,o),mT(t,o))));break;case 22:if(u=o.memoizedState!==null||ba,!u){r=r!==null&&r.memoizedState!==null||tn,p=ba;var g=tn;ba=u,(tn=r)&&!g?wa(t,o,(o.subtreeFlags&8772)!==0):Sa(t,o),ba=p,tn=g}break;case 30:break;default:Sa(t,o)}}function Bv(t){var r=t.alternate;r!==null&&(t.alternate=null,Bv(r)),t.child=null,t.deletions=null,t.sibling=null,t.tag===5&&(r=t.stateNode,r!==null&&zo(r)),t.stateNode=null,t.return=null,t.dependencies=null,t.memoizedProps=null,t.memoizedState=null,t.pendingProps=null,t.stateNode=null,t.updateQueue=null}var $t=null,Hn=!1;function va(t,r,o){for(o=o.child;o!==null;)qv(t,r,o),o=o.sibling}function qv(t,r,o){if(sn&&typeof sn.onCommitFiberUnmount=="function")try{sn.onCommitFiberUnmount(or,o)}catch{}switch(o.tag){case 26:tn||Xr(o,r),va(t,r,o),o.memoizedState?o.memoizedState.count--:o.stateNode&&(o=o.stateNode,o.parentNode.removeChild(o));break;case 27:tn||Xr(o,r);var u=$t,p=Hn;li(o.type)&&($t=o.stateNode,Hn=!1),va(t,r,o),kl(o.stateNode),$t=u,Hn=p;break;case 5:tn||Xr(o,r);case 6:if(u=$t,p=Hn,$t=null,va(t,r,o),$t=u,Hn=p,$t!==null)if(Hn)try{($t.nodeType===9?$t.body:$t.nodeName==="HTML"?$t.ownerDocument.body:$t).removeChild(o.stateNode)}catch(g){_t(o,r,g)}else try{$t.removeChild(o.stateNode)}catch(g){_t(o,r,g)}break;case 18:$t!==null&&(Hn?(t=$t,NS(t.nodeType===9?t.body:t.nodeName==="HTML"?t.ownerDocument.body:t,o.stateNode),Qs(t)):NS($t,o.stateNode));break;case 4:u=$t,p=Hn,$t=o.stateNode.containerInfo,Hn=!0,va(t,r,o),$t=u,Hn=p;break;case 0:case 11:case 14:case 15:ti(2,o,r),tn||ti(4,o,r),va(t,r,o);break;case 1:tn||(Xr(o,r),u=o.stateNode,typeof u.componentWillUnmount=="function"&&Iv(o,r,u)),va(t,r,o);break;case 21:va(t,r,o);break;case 22:tn=(u=tn)||o.memoizedState!==null,va(t,r,o),tn=u;break;default:va(t,r,o)}}function Gv(t,r){if(r.memoizedState===null&&(t=r.alternate,t!==null&&(t=t.memoizedState,t!==null))){t=t.dehydrated;try{Qs(t)}catch(o){_t(r,r.return,o)}}}function Vv(t,r){if(r.memoizedState===null&&(t=r.alternate,t!==null&&(t=t.memoizedState,t!==null&&(t=t.dehydrated,t!==null))))try{Qs(t)}catch(o){_t(r,r.return,o)}}function Bx(t){switch(t.tag){case 31:case 13:case 19:var r=t.stateNode;return r===null&&(r=t.stateNode=new Uv),r;case 22:return t=t.stateNode,r=t._retryCache,r===null&&(r=t._retryCache=new Uv),r;default:throw Error(a(435,t.tag))}}function Ru(t,r){var o=Bx(t);r.forEach(function(u){if(!o.has(u)){o.add(u);var p=Jx.bind(null,t,u);u.then(p,p)}})}function zn(t,r){var o=r.deletions;if(o!==null)for(var u=0;u<o.length;u++){var p=o[u],g=t,w=r,R=w;e:for(;R!==null;){switch(R.tag){case 27:if(li(R.type)){$t=R.stateNode,Hn=!1;break e}break;case 5:$t=R.stateNode,Hn=!1;break e;case 3:case 4:$t=R.stateNode.containerInfo,Hn=!0;break e}R=R.return}if($t===null)throw Error(a(160));qv(g,w,p),$t=null,Hn=!1,g=p.alternate,g!==null&&(g.return=null),p.return=null}if(r.subtreeFlags&13886)for(r=r.child;r!==null;)Fv(r,t),r=r.sibling}var $r=null;function Fv(t,r){var o=t.alternate,u=t.flags;switch(t.tag){case 0:case 11:case 14:case 15:zn(r,t),Un(t),u&4&&(ti(3,t,t.return),bl(3,t),ti(5,t,t.return));break;case 1:zn(r,t),Un(t),u&512&&(tn||o===null||Xr(o,o.return)),u&64&&ba&&(t=t.updateQueue,t!==null&&(u=t.callbacks,u!==null&&(o=t.shared.hiddenCallbacks,t.shared.hiddenCallbacks=o===null?u:o.concat(u))));break;case 26:var p=$r;if(zn(r,t),Un(t),u&512&&(tn||o===null||Xr(o,o.return)),u&4){var g=o!==null?o.memoizedState:null;if(u=t.memoizedState,o===null)if(u===null)if(t.stateNode===null){e:{u=t.type,o=t.memoizedProps,p=p.ownerDocument||p;t:switch(u){case"title":g=p.getElementsByTagName("title")[0],(!g||g[Ai]||g[Ht]||g.namespaceURI==="http://www.w3.org/2000/svg"||g.hasAttribute("itemprop"))&&(g=p.createElement(u),p.head.insertBefore(g,p.querySelector("head > title"))),wn(g,u,o),g[Ht]=t,Jt(g),u=g;break e;case"link":var w=FS("link","href",p).get(u+(o.href||""));if(w){for(var R=0;R<w.length;R++)if(g=w[R],g.getAttribute("href")===(o.href==null||o.href===""?null:o.href)&&g.getAttribute("rel")===(o.rel==null?null:o.rel)&&g.getAttribute("title")===(o.title==null?null:o.title)&&g.getAttribute("crossorigin")===(o.crossOrigin==null?null:o.crossOrigin)){w.splice(R,1);break t}}g=p.createElement(u),wn(g,u,o),p.head.appendChild(g);break;case"meta":if(w=FS("meta","content",p).get(u+(o.content||""))){for(R=0;R<w.length;R++)if(g=w[R],g.getAttribute("content")===(o.content==null?null:""+o.content)&&g.getAttribute("name")===(o.name==null?null:o.name)&&g.getAttribute("property")===(o.property==null?null:o.property)&&g.getAttribute("http-equiv")===(o.httpEquiv==null?null:o.httpEquiv)&&g.getAttribute("charset")===(o.charSet==null?null:o.charSet)){w.splice(R,1);break t}}g=p.createElement(u),wn(g,u,o),p.head.appendChild(g);break;default:throw Error(a(468,u))}g[Ht]=t,Jt(g),u=g}t.stateNode=u}else KS(p,t.type,t.stateNode);else t.stateNode=VS(p,u,t.memoizedProps);else g!==u?(g===null?o.stateNode!==null&&(o=o.stateNode,o.parentNode.removeChild(o)):g.count--,u===null?KS(p,t.type,t.stateNode):VS(p,u,t.memoizedProps)):u===null&&t.stateNode!==null&&mp(t,t.memoizedProps,o.memoizedProps)}break;case 27:zn(r,t),Un(t),u&512&&(tn||o===null||Xr(o,o.return)),o!==null&&u&4&&mp(t,t.memoizedProps,o.memoizedProps);break;case 5:if(zn(r,t),Un(t),u&512&&(tn||o===null||Xr(o,o.return)),t.flags&32){p=t.stateNode;try{qa(p,"")}catch(De){_t(t,t.return,De)}}u&4&&t.stateNode!=null&&(p=t.memoizedProps,mp(t,p,o!==null?o.memoizedProps:p)),u&1024&&(vp=!0);break;case 6:if(zn(r,t),Un(t),u&4){if(t.stateNode===null)throw Error(a(162));u=t.memoizedProps,o=t.stateNode;try{o.nodeValue=u}catch(De){_t(t,t.return,De)}}break;case 3:if(Vu=null,p=$r,$r=qu(r.containerInfo),zn(r,t),$r=p,Un(t),u&4&&o!==null&&o.memoizedState.isDehydrated)try{Qs(r.containerInfo)}catch(De){_t(t,t.return,De)}vp&&(vp=!1,Kv(t));break;case 4:u=$r,$r=qu(t.stateNode.containerInfo),zn(r,t),Un(t),$r=u;break;case 12:zn(r,t),Un(t);break;case 31:zn(r,t),Un(t),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,Ru(t,u)));break;case 13:zn(r,t),Un(t),t.child.flags&8192&&t.memoizedState!==null!=(o!==null&&o.memoizedState!==null)&&(Ou=qt()),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,Ru(t,u)));break;case 22:p=t.memoizedState!==null;var z=o!==null&&o.memoizedState!==null,X=ba,re=tn;if(ba=X||p,tn=re||z,zn(r,t),tn=re,ba=X,Un(t),u&8192)e:for(r=t.stateNode,r._visibility=p?r._visibility&-2:r._visibility|1,p&&(o===null||z||ba||tn||Xi(t)),o=null,r=t;;){if(r.tag===5||r.tag===26){if(o===null){z=o=r;try{if(g=z.stateNode,p)w=g.style,typeof w.setProperty=="function"?w.setProperty("display","none","important"):w.display="none";else{R=z.stateNode;var le=z.memoizedProps.style,Q=le!=null&&le.hasOwnProperty("display")?le.display:null;R.style.display=Q==null||typeof Q=="boolean"?"":(""+Q).trim()}}catch(De){_t(z,z.return,De)}}}else if(r.tag===6){if(o===null){z=r;try{z.stateNode.nodeValue=p?"":z.memoizedProps}catch(De){_t(z,z.return,De)}}}else if(r.tag===18){if(o===null){z=r;try{var te=z.stateNode;p?IS(te,!0):IS(z.stateNode,!1)}catch(De){_t(z,z.return,De)}}}else if((r.tag!==22&&r.tag!==23||r.memoizedState===null||r===t)&&r.child!==null){r.child.return=r,r=r.child;continue}if(r===t)break e;for(;r.sibling===null;){if(r.return===null||r.return===t)break e;o===r&&(o=null),r=r.return}o===r&&(o=null),r.sibling.return=r.return,r=r.sibling}u&4&&(u=t.updateQueue,u!==null&&(o=u.retryQueue,o!==null&&(u.retryQueue=null,Ru(t,o))));break;case 19:zn(r,t),Un(t),u&4&&(u=t.updateQueue,u!==null&&(t.updateQueue=null,Ru(t,u)));break;case 30:break;case 21:break;default:zn(r,t),Un(t)}}function Un(t){var r=t.flags;if(r&2){try{for(var o,u=t.return;u!==null;){if(Hv(u)){o=u;break}u=u.return}if(o==null)throw Error(a(160));switch(o.tag){case 27:var p=o.stateNode,g=yp(t);ku(t,g,p);break;case 5:var w=o.stateNode;o.flags&32&&(qa(w,""),o.flags&=-33);var R=yp(t);ku(t,R,w);break;case 3:case 4:var z=o.stateNode.containerInfo,X=yp(t);bp(t,X,z);break;default:throw Error(a(161))}}catch(re){_t(t,t.return,re)}t.flags&=-3}r&4096&&(t.flags&=-4097)}function Kv(t){if(t.subtreeFlags&1024)for(t=t.child;t!==null;){var r=t;Kv(r),r.tag===5&&r.flags&1024&&r.stateNode.reset(),t=t.sibling}}function Sa(t,r){if(r.subtreeFlags&8772)for(r=r.child;r!==null;)Pv(t,r.alternate,r),r=r.sibling}function Xi(t){for(t=t.child;t!==null;){var r=t;switch(r.tag){case 0:case 11:case 14:case 15:ti(4,r,r.return),Xi(r);break;case 1:Xr(r,r.return);var o=r.stateNode;typeof o.componentWillUnmount=="function"&&Iv(r,r.return,o),Xi(r);break;case 27:kl(r.stateNode);case 26:case 5:Xr(r,r.return),Xi(r);break;case 22:r.memoizedState===null&&Xi(r);break;case 30:Xi(r);break;default:Xi(r)}t=t.sibling}}function wa(t,r,o){for(o=o&&(r.subtreeFlags&8772)!==0,r=r.child;r!==null;){var u=r.alternate,p=t,g=r,w=g.flags;switch(g.tag){case 0:case 11:case 15:wa(p,g,o),bl(4,g);break;case 1:if(wa(p,g,o),u=g,p=u.stateNode,typeof p.componentDidMount=="function")try{p.componentDidMount()}catch(X){_t(u,u.return,X)}if(u=g,p=u.updateQueue,p!==null){var R=u.stateNode;try{var z=p.shared.hiddenCallbacks;if(z!==null)for(p.shared.hiddenCallbacks=null,p=0;p<z.length;p++)_b(z[p],R)}catch(X){_t(u,u.return,X)}}o&&w&64&&Nv(g),vl(g,g.return);break;case 27:zv(g);case 26:case 5:wa(p,g,o),o&&u===null&&w&4&&jv(g),vl(g,g.return);break;case 12:wa(p,g,o);break;case 31:wa(p,g,o),o&&w&4&&Gv(p,g);break;case 13:wa(p,g,o),o&&w&4&&Vv(p,g);break;case 22:g.memoizedState===null&&wa(p,g,o),vl(g,g.return);break;case 30:break;default:wa(p,g,o)}r=r.sibling}}function Sp(t,r){var o=null;t!==null&&t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(o=t.memoizedState.cachePool.pool),t=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(t=r.memoizedState.cachePool.pool),t!==o&&(t!=null&&t.refCount++,o!=null&&il(o))}function wp(t,r){t=null,r.alternate!==null&&(t=r.alternate.memoizedState.cache),r=r.memoizedState.cache,r!==t&&(r.refCount++,t!=null&&il(t))}function Nr(t,r,o,u){if(r.subtreeFlags&10256)for(r=r.child;r!==null;)Wv(t,r,o,u),r=r.sibling}function Wv(t,r,o,u){var p=r.flags;switch(r.tag){case 0:case 11:case 15:Nr(t,r,o,u),p&2048&&bl(9,r);break;case 1:Nr(t,r,o,u);break;case 3:Nr(t,r,o,u),p&2048&&(t=null,r.alternate!==null&&(t=r.alternate.memoizedState.cache),r=r.memoizedState.cache,r!==t&&(r.refCount++,t!=null&&il(t)));break;case 12:if(p&2048){Nr(t,r,o,u),t=r.stateNode;try{var g=r.memoizedProps,w=g.id,R=g.onPostCommit;typeof R=="function"&&R(w,r.alternate===null?"mount":"update",t.passiveEffectDuration,-0)}catch(z){_t(r,r.return,z)}}else Nr(t,r,o,u);break;case 31:Nr(t,r,o,u);break;case 13:Nr(t,r,o,u);break;case 23:break;case 22:g=r.stateNode,w=r.alternate,r.memoizedState!==null?g._visibility&2?Nr(t,r,o,u):Sl(t,r):g._visibility&2?Nr(t,r,o,u):(g._visibility|=2,Ps(t,r,o,u,(r.subtreeFlags&10256)!==0||!1)),p&2048&&Sp(w,r);break;case 24:Nr(t,r,o,u),p&2048&&wp(r.alternate,r);break;default:Nr(t,r,o,u)}}function Ps(t,r,o,u,p){for(p=p&&((r.subtreeFlags&10256)!==0||!1),r=r.child;r!==null;){var g=t,w=r,R=o,z=u,X=w.flags;switch(w.tag){case 0:case 11:case 15:Ps(g,w,R,z,p),bl(8,w);break;case 23:break;case 22:var re=w.stateNode;w.memoizedState!==null?re._visibility&2?Ps(g,w,R,z,p):Sl(g,w):(re._visibility|=2,Ps(g,w,R,z,p)),p&&X&2048&&Sp(w.alternate,w);break;case 24:Ps(g,w,R,z,p),p&&X&2048&&wp(w.alternate,w);break;default:Ps(g,w,R,z,p)}r=r.sibling}}function Sl(t,r){if(r.subtreeFlags&10256)for(r=r.child;r!==null;){var o=t,u=r,p=u.flags;switch(u.tag){case 22:Sl(o,u),p&2048&&Sp(u.alternate,u);break;case 24:Sl(o,u),p&2048&&wp(u.alternate,u);break;default:Sl(o,u)}r=r.sibling}}var wl=8192;function Bs(t,r,o){if(t.subtreeFlags&wl)for(t=t.child;t!==null;)Yv(t,r,o),t=t.sibling}function Yv(t,r,o){switch(t.tag){case 26:Bs(t,r,o),t.flags&wl&&t.memoizedState!==null&&kT(o,$r,t.memoizedState,t.memoizedProps);break;case 5:Bs(t,r,o);break;case 3:case 4:var u=$r;$r=qu(t.stateNode.containerInfo),Bs(t,r,o),$r=u;break;case 22:t.memoizedState===null&&(u=t.alternate,u!==null&&u.memoizedState!==null?(u=wl,wl=16777216,Bs(t,r,o),wl=u):Bs(t,r,o));break;default:Bs(t,r,o)}}function Xv(t){var r=t.alternate;if(r!==null&&(t=r.child,t!==null)){r.child=null;do r=t.sibling,t.sibling=null,t=r;while(t!==null)}}function Cl(t){var r=t.deletions;if((t.flags&16)!==0){if(r!==null)for(var o=0;o<r.length;o++){var u=r[o];fn=u,Qv(u,t)}Xv(t)}if(t.subtreeFlags&10256)for(t=t.child;t!==null;)Jv(t),t=t.sibling}function Jv(t){switch(t.tag){case 0:case 11:case 15:Cl(t),t.flags&2048&&ti(9,t,t.return);break;case 3:Cl(t);break;case 12:Cl(t);break;case 22:var r=t.stateNode;t.memoizedState!==null&&r._visibility&2&&(t.return===null||t.return.tag!==13)?(r._visibility&=-3,Du(t)):Cl(t);break;default:Cl(t)}}function Du(t){var r=t.deletions;if((t.flags&16)!==0){if(r!==null)for(var o=0;o<r.length;o++){var u=r[o];fn=u,Qv(u,t)}Xv(t)}for(t=t.child;t!==null;){switch(r=t,r.tag){case 0:case 11:case 15:ti(8,r,r.return),Du(r);break;case 22:o=r.stateNode,o._visibility&2&&(o._visibility&=-3,Du(r));break;default:Du(r)}t=t.sibling}}function Qv(t,r){for(;fn!==null;){var o=fn;switch(o.tag){case 0:case 11:case 15:ti(8,o,r);break;case 23:case 22:if(o.memoizedState!==null&&o.memoizedState.cachePool!==null){var u=o.memoizedState.cachePool.pool;u!=null&&u.refCount++}break;case 24:il(o.memoizedState.cache)}if(u=o.child,u!==null)u.return=o,fn=u;else e:for(o=t;fn!==null;){u=fn;var p=u.sibling,g=u.return;if(Bv(u),u===o){fn=null;break e}if(p!==null){p.return=g,fn=p;break e}fn=g}}}var qx={getCacheForType:function(t){var r=vn(Qt),o=r.data.get(t);return o===void 0&&(o=t(),r.data.set(t,o)),o},cacheSignal:function(){return vn(Qt).controller.signal}},Gx=typeof WeakMap=="function"?WeakMap:Map,vt=0,Rt=null,rt=null,st=0,Et=0,Qn=null,ni=!1,qs=!1,Cp=!1,Ca=0,Vt=0,ri=0,Ji=0,Ep=0,Zn=0,Gs=0,El=null,Pn=null,_p=!1,Ou=0,Zv=0,Lu=1/0,Mu=null,ai=null,cn=0,ii=null,Vs=null,Ea=0,Ap=0,xp=null,eS=null,_l=0,Tp=null;function er(){return(vt&2)!==0&&st!==0?st&-st:J.T!==null?Mp():Mt()}function tS(){if(Zn===0)if((st&536870912)===0||ut){var t=ve;ve<<=1,(ve&3932160)===0&&(ve=262144),Zn=t}else Zn=536870912;return t=Xn.current,t!==null&&(t.flags|=32),Zn}function Bn(t,r,o){(t===Rt&&(Et===2||Et===9)||t.cancelPendingCommit!==null)&&(Fs(t,0),si(t,st,Zn,!1)),et(t,o),((vt&2)===0||t!==Rt)&&(t===Rt&&((vt&2)===0&&(Ji|=o),Vt===4&&si(t,st,Zn,!1)),Jr(t))}function nS(t,r,o){if((vt&6)!==0)throw Error(a(327));var u=!o&&(r&127)===0&&(r&t.expiredLanes)===0||Fe(t,r),p=u?Kx(t,r):Rp(t,r,!0),g=u;do{if(p===0){qs&&!u&&si(t,r,0,!1);break}else{if(o=t.current.alternate,g&&!Vx(o)){p=Rp(t,r,!1),g=!1;continue}if(p===2){if(g=r,t.errorRecoveryDisabledLanes&g)var w=0;else w=t.pendingLanes&-536870913,w=w!==0?w:w&536870912?536870912:0;if(w!==0){r=w;e:{var R=t;p=El;var z=R.current.memoizedState.isDehydrated;if(z&&(Fs(R,w).flags|=256),w=Rp(R,w,!1),w!==2){if(Cp&&!z){R.errorRecoveryDisabledLanes|=g,Ji|=g,p=4;break e}g=Pn,Pn=p,g!==null&&(Pn===null?Pn=g:Pn.push.apply(Pn,g))}p=w}if(g=!1,p!==2)continue}}if(p===1){Fs(t,0),si(t,r,0,!0);break}e:{switch(u=t,g=p,g){case 0:case 1:throw Error(a(345));case 4:if((r&4194048)!==r)break;case 6:si(u,r,Zn,!ni);break e;case 2:Pn=null;break;case 3:case 5:break;default:throw Error(a(329))}if((r&62914560)===r&&(p=Ou+300-qt(),10<p)){if(si(u,r,Zn,!ni),Ae(u,0,!0)!==0)break e;Ea=r,u.timeoutHandle=MS(rS.bind(null,u,o,Pn,Mu,_p,r,Zn,Ji,Gs,ni,g,"Throttled",-0,0),p);break e}rS(u,o,Pn,Mu,_p,r,Zn,Ji,Gs,ni,g,null,-0,0)}}break}while(!0);Jr(t)}function rS(t,r,o,u,p,g,w,R,z,X,re,le,Q,te){if(t.timeoutHandle=-1,le=r.subtreeFlags,le&8192||(le&16785408)===16785408){le={stylesheets:null,count:0,imgCount:0,imgBytes:0,suspenseyImages:[],waitingForImages:!0,waitingForViewTransition:!1,unsuspend:Rr},Yv(r,g,le);var De=(g&62914560)===g?Ou-qt():(g&4194048)===g?Zv-qt():0;if(De=RT(le,De),De!==null){Ea=g,t.cancelPendingCommit=De(dS.bind(null,t,r,g,o,u,p,w,R,z,re,le,null,Q,te)),si(t,g,w,!X);return}}dS(t,r,g,o,u,p,w,R,z)}function Vx(t){for(var r=t;;){var o=r.tag;if((o===0||o===11||o===15)&&r.flags&16384&&(o=r.updateQueue,o!==null&&(o=o.stores,o!==null)))for(var u=0;u<o.length;u++){var p=o[u],g=p.getSnapshot;p=p.value;try{if(!Wn(g(),p))return!1}catch{return!1}}if(o=r.child,r.subtreeFlags&16384&&o!==null)o.return=r,r=o;else{if(r===t)break;for(;r.sibling===null;){if(r.return===null||r.return===t)return!0;r=r.return}r.sibling.return=r.return,r=r.sibling}}return!0}function si(t,r,o,u){r&=~Ep,r&=~Ji,t.suspendedLanes|=r,t.pingedLanes&=~r,u&&(t.warmLanes|=r),u=t.expirationTimes;for(var p=r;0<p;){var g=31-on(p),w=1<<g;u[g]=-1,p&=~w}o!==0&&Ge(t,o,r)}function $u(){return(vt&6)===0?(Al(0),!1):!0}function kp(){if(rt!==null){if(Et===0)var t=rt.return;else t=rt,fa=Bi=null,Gh(t),Is=null,ol=0,t=rt;for(;t!==null;)$v(t.alternate,t),t=t.return;rt=null}}function Fs(t,r){var o=t.timeoutHandle;o!==-1&&(t.timeoutHandle=-1,dT(o)),o=t.cancelPendingCommit,o!==null&&(t.cancelPendingCommit=null,o()),Ea=0,kp(),Rt=t,rt=o=ua(t.current,null),st=r,Et=0,Qn=null,ni=!1,qs=Fe(t,r),Cp=!1,Gs=Zn=Ep=Ji=ri=Vt=0,Pn=El=null,_p=!1,(r&8)!==0&&(r|=r&32);var u=t.entangledLanes;if(u!==0)for(t=t.entanglements,u&=r;0<u;){var p=31-on(u),g=1<<p;r|=t[p],u&=~g}return Ca=r,tu(),o}function aS(t,r){Xe=null,J.H=gl,r===Ns||r===cu?(r=Sb(),Et=3):r===Lh?(r=Sb(),Et=4):Et=r===sp?8:r!==null&&typeof r=="object"&&typeof r.then=="function"?6:1,Qn=r,rt===null&&(Vt=1,Eu(t,ur(r,t.current)))}function iS(){var t=Xn.current;return t===null?!0:(st&4194048)===st?pr===null:(st&62914560)===st||(st&536870912)!==0?t===pr:!1}function sS(){var t=J.H;return J.H=gl,t===null?gl:t}function oS(){var t=J.A;return J.A=qx,t}function Nu(){Vt=4,ni||(st&4194048)!==st&&Xn.current!==null||(qs=!0),(ri&134217727)===0&&(Ji&134217727)===0||Rt===null||si(Rt,st,Zn,!1)}function Rp(t,r,o){var u=vt;vt|=2;var p=sS(),g=oS();(Rt!==t||st!==r)&&(Mu=null,Fs(t,r)),r=!1;var w=Vt;e:do try{if(Et!==0&&rt!==null){var R=rt,z=Qn;switch(Et){case 8:kp(),w=6;break e;case 3:case 2:case 9:case 6:Xn.current===null&&(r=!0);var X=Et;if(Et=0,Qn=null,Ks(t,R,z,X),o&&qs){w=0;break e}break;default:X=Et,Et=0,Qn=null,Ks(t,R,z,X)}}Fx(),w=Vt;break}catch(re){aS(t,re)}while(!0);return r&&t.shellSuspendCounter++,fa=Bi=null,vt=u,J.H=p,J.A=g,rt===null&&(Rt=null,st=0,tu()),w}function Fx(){for(;rt!==null;)lS(rt)}function Kx(t,r){var o=vt;vt|=2;var u=sS(),p=oS();Rt!==t||st!==r?(Mu=null,Lu=qt()+500,Fs(t,r)):qs=Fe(t,r);e:do try{if(Et!==0&&rt!==null){r=rt;var g=Qn;t:switch(Et){case 1:Et=0,Qn=null,Ks(t,r,g,1);break;case 2:case 9:if(bb(g)){Et=0,Qn=null,cS(r);break}r=function(){Et!==2&&Et!==9||Rt!==t||(Et=7),Jr(t)},g.then(r,r);break e;case 3:Et=7;break e;case 4:Et=5;break e;case 7:bb(g)?(Et=0,Qn=null,cS(r)):(Et=0,Qn=null,Ks(t,r,g,7));break;case 5:var w=null;switch(rt.tag){case 26:w=rt.memoizedState;case 5:case 27:var R=rt;if(w?WS(w):R.stateNode.complete){Et=0,Qn=null;var z=R.sibling;if(z!==null)rt=z;else{var X=R.return;X!==null?(rt=X,Iu(X)):rt=null}break t}}Et=0,Qn=null,Ks(t,r,g,5);break;case 6:Et=0,Qn=null,Ks(t,r,g,6);break;case 8:kp(),Vt=6;break e;default:throw Error(a(462))}}Wx();break}catch(re){aS(t,re)}while(!0);return fa=Bi=null,J.H=u,J.A=p,vt=o,rt!==null?0:(Rt=null,st=0,tu(),Vt)}function Wx(){for(;rt!==null&&!Kn();)lS(rt)}function lS(t){var r=Lv(t.alternate,t,Ca);t.memoizedProps=t.pendingProps,r===null?Iu(t):rt=r}function cS(t){var r=t,o=r.alternate;switch(r.tag){case 15:case 0:r=xv(o,r,r.pendingProps,r.type,void 0,st);break;case 11:r=xv(o,r,r.pendingProps,r.type.render,r.ref,st);break;case 5:Gh(r);default:$v(o,r),r=rt=ob(r,Ca),r=Lv(o,r,Ca)}t.memoizedProps=t.pendingProps,r===null?Iu(t):rt=r}function Ks(t,r,o,u){fa=Bi=null,Gh(r),Is=null,ol=0;var p=r.return;try{if(Ix(t,p,r,o,st)){Vt=1,Eu(t,ur(o,t.current)),rt=null;return}}catch(g){if(p!==null)throw rt=p,g;Vt=1,Eu(t,ur(o,t.current)),rt=null;return}r.flags&32768?(ut||u===1?t=!0:qs||(st&536870912)!==0?t=!1:(ni=t=!0,(u===2||u===9||u===3||u===6)&&(u=Xn.current,u!==null&&u.tag===13&&(u.flags|=16384))),uS(r,t)):Iu(r)}function Iu(t){var r=t;do{if((r.flags&32768)!==0){uS(r,ni);return}t=r.return;var o=zx(r.alternate,r,Ca);if(o!==null){rt=o;return}if(r=r.sibling,r!==null){rt=r;return}rt=r=t}while(r!==null);Vt===0&&(Vt=5)}function uS(t,r){do{var o=Ux(t.alternate,t);if(o!==null){o.flags&=32767,rt=o;return}if(o=t.return,o!==null&&(o.flags|=32768,o.subtreeFlags=0,o.deletions=null),!r&&(t=t.sibling,t!==null)){rt=t;return}rt=t=o}while(t!==null);Vt=6,rt=null}function dS(t,r,o,u,p,g,w,R,z){t.cancelPendingCommit=null;do ju();while(cn!==0);if((vt&6)!==0)throw Error(a(327));if(r!==null){if(r===t.current)throw Error(a(177));if(g=r.lanes|r.childLanes,g|=yh,He(t,o,g,w,R,z),t===Rt&&(rt=Rt=null,st=0),Vs=r,ii=t,Ea=o,Ap=g,xp=p,eS=u,(r.subtreeFlags&10256)!==0||(r.flags&10256)!==0?(t.callbackNode=null,t.callbackPriority=0,Qx(Rn,function(){return mS(),null})):(t.callbackNode=null,t.callbackPriority=0),u=(r.flags&13878)!==0,(r.subtreeFlags&13878)!==0||u){u=J.T,J.T=null,p=fe.p,fe.p=2,w=vt,vt|=4;try{Px(t,r,o)}finally{vt=w,fe.p=p,J.T=u}}cn=1,fS(),hS(),pS()}}function fS(){if(cn===1){cn=0;var t=ii,r=Vs,o=(r.flags&13878)!==0;if((r.subtreeFlags&13878)!==0||o){o=J.T,J.T=null;var u=fe.p;fe.p=2;var p=vt;vt|=4;try{Fv(r,t);var g=Pp,w=Qy(t.containerInfo),R=g.focusedElem,z=g.selectionRange;if(w!==R&&R&&R.ownerDocument&&Jy(R.ownerDocument.documentElement,R)){if(z!==null&&fh(R)){var X=z.start,re=z.end;if(re===void 0&&(re=X),"selectionStart"in R)R.selectionStart=X,R.selectionEnd=Math.min(re,R.value.length);else{var le=R.ownerDocument||document,Q=le&&le.defaultView||window;if(Q.getSelection){var te=Q.getSelection(),De=R.textContent.length,Be=Math.min(z.start,De),kt=z.end===void 0?Be:Math.min(z.end,De);!te.extend&&Be>kt&&(w=kt,kt=Be,Be=w);var F=Xy(R,Be),q=Xy(R,kt);if(F&&q&&(te.rangeCount!==1||te.anchorNode!==F.node||te.anchorOffset!==F.offset||te.focusNode!==q.node||te.focusOffset!==q.offset)){var Y=le.createRange();Y.setStart(F.node,F.offset),te.removeAllRanges(),Be>kt?(te.addRange(Y),te.extend(q.node,q.offset)):(Y.setEnd(q.node,q.offset),te.addRange(Y))}}}}for(le=[],te=R;te=te.parentNode;)te.nodeType===1&&le.push({element:te,left:te.scrollLeft,top:te.scrollTop});for(typeof R.focus=="function"&&R.focus(),R=0;R<le.length;R++){var oe=le[R];oe.element.scrollLeft=oe.left,oe.element.scrollTop=oe.top}}Yu=!!Up,Pp=Up=null}finally{vt=p,fe.p=u,J.T=o}}t.current=r,cn=2}}function hS(){if(cn===2){cn=0;var t=ii,r=Vs,o=(r.flags&8772)!==0;if((r.subtreeFlags&8772)!==0||o){o=J.T,J.T=null;var u=fe.p;fe.p=2;var p=vt;vt|=4;try{Pv(t,r.alternate,r)}finally{vt=p,fe.p=u,J.T=o}}cn=3}}function pS(){if(cn===4||cn===3){cn=0,Gr();var t=ii,r=Vs,o=Ea,u=eS;(r.subtreeFlags&10256)!==0||(r.flags&10256)!==0?cn=5:(cn=0,Vs=ii=null,gS(t,t.pendingLanes));var p=t.pendingLanes;if(p===0&&(ai=null),At(o),r=r.stateNode,sn&&typeof sn.onCommitFiberRoot=="function")try{sn.onCommitFiberRoot(or,r,void 0,(r.current.flags&128)===128)}catch{}if(u!==null){r=J.T,p=fe.p,fe.p=2,J.T=null;try{for(var g=t.onRecoverableError,w=0;w<u.length;w++){var R=u[w];g(R.value,{componentStack:R.stack})}}finally{J.T=r,fe.p=p}}(Ea&3)!==0&&ju(),Jr(t),p=t.pendingLanes,(o&261930)!==0&&(p&42)!==0?t===Tp?_l++:(_l=0,Tp=t):_l=0,Al(0)}}function gS(t,r){(t.pooledCacheLanes&=r)===0&&(r=t.pooledCache,r!=null&&(t.pooledCache=null,il(r)))}function ju(){return fS(),hS(),pS(),mS()}function mS(){if(cn!==5)return!1;var t=ii,r=Ap;Ap=0;var o=At(Ea),u=J.T,p=fe.p;try{fe.p=32>o?32:o,J.T=null,o=xp,xp=null;var g=ii,w=Ea;if(cn=0,Vs=ii=null,Ea=0,(vt&6)!==0)throw Error(a(331));var R=vt;if(vt|=4,Jv(g.current),Wv(g,g.current,w,o),vt=R,Al(0,!1),sn&&typeof sn.onPostCommitFiberRoot=="function")try{sn.onPostCommitFiberRoot(or,g)}catch{}return!0}finally{fe.p=p,J.T=u,gS(t,r)}}function yS(t,r,o){r=ur(o,r),r=ip(t.stateNode,r,2),t=Qa(t,r,2),t!==null&&(et(t,2),Jr(t))}function _t(t,r,o){if(t.tag===3)yS(t,t,o);else for(;r!==null;){if(r.tag===3){yS(r,t,o);break}else if(r.tag===1){var u=r.stateNode;if(typeof r.type.getDerivedStateFromError=="function"||typeof u.componentDidCatch=="function"&&(ai===null||!ai.has(u))){t=ur(o,t),o=bv(2),u=Qa(r,o,2),u!==null&&(vv(o,u,r,t),et(u,2),Jr(u));break}}r=r.return}}function Dp(t,r,o){var u=t.pingCache;if(u===null){u=t.pingCache=new Gx;var p=new Set;u.set(r,p)}else p=u.get(r),p===void 0&&(p=new Set,u.set(r,p));p.has(o)||(Cp=!0,p.add(o),t=Yx.bind(null,t,r,o),r.then(t,t))}function Yx(t,r,o){var u=t.pingCache;u!==null&&u.delete(r),t.pingedLanes|=t.suspendedLanes&o,t.warmLanes&=~o,Rt===t&&(st&o)===o&&(Vt===4||Vt===3&&(st&62914560)===st&&300>qt()-Ou?(vt&2)===0&&Fs(t,0):Ep|=o,Gs===st&&(Gs=0)),Jr(t)}function bS(t,r){r===0&&(r=Ne()),t=zi(t,r),t!==null&&(et(t,r),Jr(t))}function Xx(t){var r=t.memoizedState,o=0;r!==null&&(o=r.retryLane),bS(t,o)}function Jx(t,r){var o=0;switch(t.tag){case 31:case 13:var u=t.stateNode,p=t.memoizedState;p!==null&&(o=p.retryLane);break;case 19:u=t.stateNode;break;case 22:u=t.stateNode._retryCache;break;default:throw Error(a(314))}u!==null&&u.delete(r),bS(t,o)}function Qx(t,r){return an(t,r)}var Hu=null,Ws=null,Op=!1,zu=!1,Lp=!1,oi=0;function Jr(t){t!==Ws&&t.next===null&&(Ws===null?Hu=Ws=t:Ws=Ws.next=t),zu=!0,Op||(Op=!0,eT())}function Al(t,r){if(!Lp&&zu){Lp=!0;do for(var o=!1,u=Hu;u!==null;){if(t!==0){var p=u.pendingLanes;if(p===0)var g=0;else{var w=u.suspendedLanes,R=u.pingedLanes;g=(1<<31-on(42|t)+1)-1,g&=p&~(w&~R),g=g&201326741?g&201326741|1:g?g|2:0}g!==0&&(o=!0,CS(u,g))}else g=st,g=Ae(u,u===Rt?g:0,u.cancelPendingCommit!==null||u.timeoutHandle!==-1),(g&3)===0||Fe(u,g)||(o=!0,CS(u,g));u=u.next}while(o);Lp=!1}}function Zx(){vS()}function vS(){zu=Op=!1;var t=0;oi!==0&&uT()&&(t=oi);for(var r=qt(),o=null,u=Hu;u!==null;){var p=u.next,g=SS(u,r);g===0?(u.next=null,o===null?Hu=p:o.next=p,p===null&&(Ws=o)):(o=u,(t!==0||(g&3)!==0)&&(zu=!0)),u=p}cn!==0&&cn!==5||Al(t),oi!==0&&(oi=0)}function SS(t,r){for(var o=t.suspendedLanes,u=t.pingedLanes,p=t.expirationTimes,g=t.pendingLanes&-62914561;0<g;){var w=31-on(g),R=1<<w,z=p[w];z===-1?((R&o)===0||(R&u)!==0)&&(p[w]=ke(R,r)):z<=r&&(t.expiredLanes|=R),g&=~R}if(r=Rt,o=st,o=Ae(t,t===r?o:0,t.cancelPendingCommit!==null||t.timeoutHandle!==-1),u=t.callbackNode,o===0||t===r&&(Et===2||Et===9)||t.cancelPendingCommit!==null)return u!==null&&u!==null&&pn(u),t.callbackNode=null,t.callbackPriority=0;if((o&3)===0||Fe(t,o)){if(r=o&-o,r===t.callbackPriority)return r;switch(u!==null&&pn(u),At(o)){case 2:case 8:o=gn;break;case 32:o=Rn;break;case 268435456:o=Nn;break;default:o=Rn}return u=wS.bind(null,t),o=an(o,u),t.callbackPriority=r,t.callbackNode=o,r}return u!==null&&u!==null&&pn(u),t.callbackPriority=2,t.callbackNode=null,2}function wS(t,r){if(cn!==0&&cn!==5)return t.callbackNode=null,t.callbackPriority=0,null;var o=t.callbackNode;if(ju()&&t.callbackNode!==o)return null;var u=st;return u=Ae(t,t===Rt?u:0,t.cancelPendingCommit!==null||t.timeoutHandle!==-1),u===0?null:(nS(t,u,r),SS(t,qt()),t.callbackNode!=null&&t.callbackNode===o?wS.bind(null,t):null)}function CS(t,r){if(ju())return null;nS(t,r,!0)}function eT(){fT(function(){(vt&6)!==0?an(aa,Zx):vS()})}function Mp(){if(oi===0){var t=Ms;t===0&&(t=ee,ee<<=1,(ee&261888)===0&&(ee=256)),oi=t}return oi}function ES(t){return t==null||typeof t=="symbol"||typeof t=="boolean"?null:typeof t=="function"?t:lr(""+t)}function _S(t,r){var o=r.ownerDocument.createElement("input");return o.name=r.name,o.value=r.value,t.id&&o.setAttribute("form",t.id),r.parentNode.insertBefore(o,r),t=new FormData(t),o.parentNode.removeChild(o),t}function tT(t,r,o,u,p){if(r==="submit"&&o&&o.stateNode===p){var g=ES((p[An]||null).action),w=u.submitter;w&&(r=(r=w[An]||null)?ES(r.formAction):w.getAttribute("formAction"),r!==null&&(g=r,w=null));var R=new ca("action","action",null,u,p);t.push({event:R,listeners:[{instance:null,listener:function(){if(u.defaultPrevented){if(oi!==0){var z=w?_S(p,w):new FormData(p);Zh(o,{pending:!0,data:z,method:p.method,action:g},null,z)}}else typeof g=="function"&&(R.preventDefault(),z=w?_S(p,w):new FormData(p),Zh(o,{pending:!0,data:z,method:p.method,action:g},g,z))},currentTarget:p}]})}}for(var $p=0;$p<mh.length;$p++){var Np=mh[$p],nT=Np.toLowerCase(),rT=Np[0].toUpperCase()+Np.slice(1);Mr(nT,"on"+rT)}Mr(tb,"onAnimationEnd"),Mr(nb,"onAnimationIteration"),Mr(rb,"onAnimationStart"),Mr("dblclick","onDoubleClick"),Mr("focusin","onFocus"),Mr("focusout","onBlur"),Mr(vx,"onTransitionRun"),Mr(Sx,"onTransitionStart"),Mr(wx,"onTransitionCancel"),Mr(ab,"onTransitionEnd"),za("onMouseEnter",["mouseout","mouseover"]),za("onMouseLeave",["mouseout","mouseover"]),za("onPointerEnter",["pointerout","pointerover"]),za("onPointerLeave",["pointerout","pointerover"]),kr("onChange","change click focusin focusout input keydown keyup selectionchange".split(" ")),kr("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" ")),kr("onBeforeInput",["compositionend","keypress","textInput","paste"]),kr("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" ")),kr("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" ")),kr("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));var xl="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),aT=new Set("beforetoggle cancel close invalid load scroll scrollend toggle".split(" ").concat(xl));function AS(t,r){r=(r&4)!==0;for(var o=0;o<t.length;o++){var u=t[o],p=u.event;u=u.listeners;e:{var g=void 0;if(r)for(var w=u.length-1;0<=w;w--){var R=u[w],z=R.instance,X=R.currentTarget;if(R=R.listener,z!==g&&p.isPropagationStopped())break e;g=R,p.currentTarget=X;try{g(p)}catch(re){eu(re)}p.currentTarget=null,g=z}else for(w=0;w<u.length;w++){if(R=u[w],z=R.instance,X=R.currentTarget,R=R.listener,z!==g&&p.isPropagationStopped())break e;g=R,p.currentTarget=X;try{g(p)}catch(re){eu(re)}p.currentTarget=null,g=z}}}}function at(t,r){var o=r[Ho];o===void 0&&(o=r[Ho]=new Set);var u=t+"__bubble";o.has(u)||(xS(r,t,2,!1),o.add(u))}function Ip(t,r,o){var u=0;r&&(u|=4),xS(o,t,u,r)}var Uu="_reactListening"+Math.random().toString(36).slice(2);function jp(t){if(!t[Uu]){t[Uu]=!0,Oc.forEach(function(o){o!=="selectionchange"&&(aT.has(o)||Ip(o,!1,t),Ip(o,!0,t))});var r=t.nodeType===9?t:t.ownerDocument;r===null||r[Uu]||(r[Uu]=!0,Ip("selectionchange",!1,r))}}function xS(t,r,o,u){switch(tw(r)){case 2:var p=LT;break;case 8:p=MT;break;default:p=Qp}o=p.bind(null,r,o,t),p=void 0,!Vo||r!=="touchstart"&&r!=="touchmove"&&r!=="wheel"||(p=!0),u?p!==void 0?t.addEventListener(r,o,{capture:!0,passive:p}):t.addEventListener(r,o,!0):p!==void 0?t.addEventListener(r,o,{passive:p}):t.addEventListener(r,o,!1)}function Hp(t,r,o,u,p){var g=u;if((r&1)===0&&(r&2)===0&&u!==null)e:for(;;){if(u===null)return;var w=u.tag;if(w===3||w===4){var R=u.stateNode.containerInfo;if(R===p)break;if(w===4)for(w=u.return;w!==null;){var z=w.tag;if((z===3||z===4)&&w.stateNode.containerInfo===p)return;w=w.return}for(;R!==null;){if(w=ja(R),w===null)return;if(z=w.tag,z===5||z===6||z===26||z===27){u=g=w;continue e}R=R.parentNode}}u=u.return}Pc(function(){var X=g,re=Go(o),le=[];e:{var Q=ib.get(t);if(Q!==void 0){var te=ca,De=t;switch(t){case"keypress":if(Di(o)===0)break e;case"keydown":case"keyup":te=nh;break;case"focusin":De="focus",te=Yo;break;case"focusout":De="blur",te=Yo;break;case"beforeblur":case"afterblur":te=Yo;break;case"click":if(o.button===2)break e;case"auxclick":case"dblclick":case"mousedown":case"mousemove":case"mouseup":case"mouseout":case"mouseover":case"contextmenu":te=Gc;break;case"drag":case"dragend":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"dragstart":case"drop":te=Ky;break;case"touchcancel":case"touchend":case"touchmove":case"touchstart":te=ih;break;case tb:case nb:case rb:te=Yf;break;case ab:te=_s;break;case"scroll":case"scrollend":te=Ff;break;case"wheel":te=E;break;case"copy":case"cut":case"paste":te=Vc;break;case"gotpointercapture":case"lostpointercapture":case"pointercancel":case"pointerdown":case"pointermove":case"pointerout":case"pointerover":case"pointerup":te=Wc;break;case"toggle":case"beforetoggle":te=C}var Be=(r&4)!==0,kt=!Be&&(t==="scroll"||t==="scrollend"),F=Be?Q!==null?Q+"Capture":null:Q;Be=[];for(var q=X,Y;q!==null;){var oe=q;if(Y=oe.stateNode,oe=oe.tag,oe!==5&&oe!==26&&oe!==27||Y===null||F===null||(oe=ki(q,F),oe!=null&&Be.push(Tl(q,oe,Y))),kt)break;q=q.return}0<Be.length&&(Q=new te(Q,De,null,o,re),le.push({event:Q,listeners:Be}))}}if((r&7)===0){e:{if(Q=t==="mouseover"||t==="pointerover",te=t==="mouseout"||t==="pointerout",Q&&o!==qo&&(De=o.relatedTarget||o.fromElement)&&(ja(De)||De[Ia]))break e;if((te||Q)&&(Q=re.window===re?re:(Q=re.ownerDocument)?Q.defaultView||Q.parentWindow:window,te?(De=o.relatedTarget||o.toElement,te=X,De=De?ja(De):null,De!==null&&(kt=l(De),Be=De.tag,De!==kt||Be!==5&&Be!==27&&Be!==6)&&(De=null)):(te=null,De=X),te!==De)){if(Be=Gc,oe="onMouseLeave",F="onMouseEnter",q="mouse",(t==="pointerout"||t==="pointerover")&&(Be=Wc,oe="onPointerLeave",F="onPointerEnter",q="pointer"),kt=te==null?Q:sa(te),Y=De==null?Q:sa(De),Q=new Be(oe,q+"leave",te,o,re),Q.target=kt,Q.relatedTarget=Y,oe=null,ja(re)===X&&(Be=new Be(F,q+"enter",De,o,re),Be.target=Y,Be.relatedTarget=kt,oe=Be),kt=oe,te&&De)t:{for(Be=iT,F=te,q=De,Y=0,oe=F;oe;oe=Be(oe))Y++;oe=0;for(var Pe=q;Pe;Pe=Be(Pe))oe++;for(;0<Y-oe;)F=Be(F),Y--;for(;0<oe-Y;)q=Be(q),oe--;for(;Y--;){if(F===q||q!==null&&F===q.alternate){Be=F;break t}F=Be(F),q=Be(q)}Be=null}else Be=null;te!==null&&TS(le,Q,te,Be,!1),De!==null&&kt!==null&&TS(le,kt,De,Be,!0)}}e:{if(Q=X?sa(X):window,te=Q.nodeName&&Q.nodeName.toLowerCase(),te==="select"||te==="input"&&Q.type==="file")var mt=Qo;else if(Jo(Q))if(Zo)mt=mx;else{mt=dh;var Ie=uh}else te=Q.nodeName,!te||te.toLowerCase()!=="input"||Q.type!=="checkbox"&&Q.type!=="radio"?X&&Po(X.elementType)&&(mt=Qo):mt=Zc;if(mt&&(mt=mt(t,X))){Xc(le,mt,o,re);break e}Ie&&Ie(t,Q,X),t==="focusout"&&X&&Q.type==="number"&&X.memoizedProps.value!=null&&oa(Q,"number",Q.value)}switch(Ie=X?sa(X):window,t){case"focusin":(Jo(Ie)||Ie.contentEditable==="true")&&(As=Ie,hh=X,nl=null);break;case"focusout":nl=hh=As=null;break;case"mousedown":ph=!0;break;case"contextmenu":case"mouseup":case"dragend":ph=!1,Zy(le,o,re);break;case"selectionchange":if(bx)break;case"keydown":case"keyup":Zy(le,o,re)}var Je;if(I)e:{switch(t){case"compositionstart":var ot="onCompositionStart";break e;case"compositionend":ot="onCompositionEnd";break e;case"compositionupdate":ot="onCompositionUpdate";break e}ot=void 0}else Lr?gt(t,o)&&(ot="onCompositionEnd"):t==="keydown"&&o.keyCode===229&&(ot="onCompositionStart");ot&&(Me&&o.locale!=="ko"&&(Lr||ot!=="onCompositionStart"?ot==="onCompositionEnd"&&Lr&&(Je=Bc()):(Kr=re,Fo="value"in Kr?Kr.value:Kr.textContent,Lr=!0)),Ie=Pu(X,ot),0<Ie.length&&(ot=new Fc(ot,t,null,o,re),le.push({event:ot,listeners:Ie}),Je?ot.data=Je:(Je=zt(o),Je!==null&&(ot.data=Je)))),(Je=me?oh(t,o):Yc(t,o))&&(ot=Pu(X,"onBeforeInput"),0<ot.length&&(Ie=new Fc("onBeforeInput","beforeinput",null,o,re),le.push({event:Ie,listeners:ot}),Ie.data=Je)),tT(le,t,X,o,re)}AS(le,r)})}function Tl(t,r,o){return{instance:t,listener:r,currentTarget:o}}function Pu(t,r){for(var o=r+"Capture",u=[];t!==null;){var p=t,g=p.stateNode;if(p=p.tag,p!==5&&p!==26&&p!==27||g===null||(p=ki(t,o),p!=null&&u.unshift(Tl(t,p,g)),p=ki(t,r),p!=null&&u.push(Tl(t,p,g))),t.tag===3)return u;t=t.return}return[]}function iT(t){if(t===null)return null;do t=t.return;while(t&&t.tag!==5&&t.tag!==27);return t||null}function TS(t,r,o,u,p){for(var g=r._reactName,w=[];o!==null&&o!==u;){var R=o,z=R.alternate,X=R.stateNode;if(R=R.tag,z!==null&&z===u)break;R!==5&&R!==26&&R!==27||X===null||(z=X,p?(X=ki(o,g),X!=null&&w.unshift(Tl(o,X,z))):p||(X=ki(o,g),X!=null&&w.push(Tl(o,X,z)))),o=o.return}w.length!==0&&t.push({event:r,listeners:w})}var sT=/\r\n?/g,oT=/\u0000|\uFFFD/g;function kS(t){return(typeof t=="string"?t:""+t).replace(sT,` +`).replace(oT,"")}function RS(t,r){return r=kS(r),kS(t)===r}function Tt(t,r,o,u,p,g){switch(o){case"children":typeof u=="string"?r==="body"||r==="textarea"&&u===""||qa(t,u):(typeof u=="number"||typeof u=="bigint")&&r!=="body"&&qa(t,""+u);break;case"className":xi(t,"class",u);break;case"tabIndex":xi(t,"tabindex",u);break;case"dir":case"role":case"viewBox":case"width":case"height":xi(t,o,u);break;case"style":zc(t,u,g);break;case"data":if(r!=="object"){xi(t,"data",u);break}case"src":case"href":if(u===""&&(r!=="a"||o!=="href")){t.removeAttribute(o);break}if(u==null||typeof u=="function"||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(o);break}u=lr(""+u),t.setAttribute(o,u);break;case"action":case"formAction":if(typeof u=="function"){t.setAttribute(o,"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 g=="function"&&(o==="formAction"?(r!=="input"&&Tt(t,r,"name",p.name,p,null),Tt(t,r,"formEncType",p.formEncType,p,null),Tt(t,r,"formMethod",p.formMethod,p,null),Tt(t,r,"formTarget",p.formTarget,p,null)):(Tt(t,r,"encType",p.encType,p,null),Tt(t,r,"method",p.method,p,null),Tt(t,r,"target",p.target,p,null)));if(u==null||typeof u=="symbol"||typeof u=="boolean"){t.removeAttribute(o);break}u=lr(""+u),t.setAttribute(o,u);break;case"onClick":u!=null&&(t.onclick=Rr);break;case"onScroll":u!=null&&at("scroll",t);break;case"onScrollEnd":u!=null&&at("scrollend",t);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(o=u.__html,o!=null){if(p.children!=null)throw Error(a(60));t.innerHTML=o}}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}o=lr(""+u),t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",o);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(o,""+u):t.removeAttribute(o);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(o,""):t.removeAttribute(o);break;case"capture":case"download":u===!0?t.setAttribute(o,""):u!==!1&&u!=null&&typeof u!="function"&&typeof u!="symbol"?t.setAttribute(o,u):t.removeAttribute(o);break;case"cols":case"rows":case"size":case"span":u!=null&&typeof u!="function"&&typeof u!="symbol"&&!isNaN(u)&&1<=u?t.setAttribute(o,u):t.removeAttribute(o);break;case"rowSpan":case"start":u==null||typeof u=="function"||typeof u=="symbol"||isNaN(u)?t.removeAttribute(o):t.setAttribute(o,u);break;case"popover":at("beforetoggle",t),at("toggle",t),In(t,"popover",u);break;case"xlinkActuate":yn(t,"http://www.w3.org/1999/xlink","xlink:actuate",u);break;case"xlinkArcrole":yn(t,"http://www.w3.org/1999/xlink","xlink:arcrole",u);break;case"xlinkRole":yn(t,"http://www.w3.org/1999/xlink","xlink:role",u);break;case"xlinkShow":yn(t,"http://www.w3.org/1999/xlink","xlink:show",u);break;case"xlinkTitle":yn(t,"http://www.w3.org/1999/xlink","xlink:title",u);break;case"xlinkType":yn(t,"http://www.w3.org/1999/xlink","xlink:type",u);break;case"xmlBase":yn(t,"http://www.w3.org/XML/1998/namespace","xml:base",u);break;case"xmlLang":yn(t,"http://www.w3.org/XML/1998/namespace","xml:lang",u);break;case"xmlSpace":yn(t,"http://www.w3.org/XML/1998/namespace","xml:space",u);break;case"is":In(t,"is",u);break;case"innerText":case"textContent":break;default:(!(2<o.length)||o[0]!=="o"&&o[0]!=="O"||o[1]!=="n"&&o[1]!=="N")&&(o=Vf.get(o)||o,In(t,o,u))}}function zp(t,r,o,u,p,g){switch(o){case"style":zc(t,u,g);break;case"dangerouslySetInnerHTML":if(u!=null){if(typeof u!="object"||!("__html"in u))throw Error(a(61));if(o=u.__html,o!=null){if(p.children!=null)throw Error(a(60));t.innerHTML=o}}break;case"children":typeof u=="string"?qa(t,u):(typeof u=="number"||typeof u=="bigint")&&qa(t,""+u);break;case"onScroll":u!=null&&at("scroll",t);break;case"onScrollEnd":u!=null&&at("scrollend",t);break;case"onClick":u!=null&&(t.onclick=Rr);break;case"suppressContentEditableWarning":case"suppressHydrationWarning":case"innerHTML":case"ref":break;case"innerText":case"textContent":break;default:if(!Lc.hasOwnProperty(o))e:{if(o[0]==="o"&&o[1]==="n"&&(p=o.endsWith("Capture"),r=o.slice(2,p?o.length-7:void 0),g=t[An]||null,g=g!=null?g[o]:null,typeof g=="function"&&t.removeEventListener(r,g,p),typeof u=="function")){typeof g!="function"&&g!==null&&(o in t?t[o]=null:t.hasAttribute(o)&&t.removeAttribute(o)),t.addEventListener(r,u,p);break e}o in t?t[o]=u:u===!0?t.setAttribute(o,""):In(t,o,u)}}}function wn(t,r,o){switch(r){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"img":at("error",t),at("load",t);var u=!1,p=!1,g;for(g in o)if(o.hasOwnProperty(g)){var w=o[g];if(w!=null)switch(g){case"src":u=!0;break;case"srcSet":p=!0;break;case"children":case"dangerouslySetInnerHTML":throw Error(a(137,r));default:Tt(t,r,g,w,o,null)}}p&&Tt(t,r,"srcSet",o.srcSet,o,null),u&&Tt(t,r,"src",o.src,o,null);return;case"input":at("invalid",t);var R=g=w=p=null,z=null,X=null;for(u in o)if(o.hasOwnProperty(u)){var re=o[u];if(re!=null)switch(u){case"name":p=re;break;case"type":w=re;break;case"checked":z=re;break;case"defaultChecked":X=re;break;case"value":g=re;break;case"defaultValue":R=re;break;case"children":case"dangerouslySetInnerHTML":if(re!=null)throw Error(a(137,r));break;default:Tt(t,r,u,re,o,null)}}Ti(t,g,R,z,X,w,p,!1);return;case"select":at("invalid",t),u=w=g=null;for(p in o)if(o.hasOwnProperty(p)&&(R=o[p],R!=null))switch(p){case"value":g=R;break;case"defaultValue":w=R;break;case"multiple":u=R;default:Tt(t,r,p,R,o,null)}r=g,o=w,t.multiple=!!u,r!=null?Pa(t,!!u,r,!1):o!=null&&Pa(t,!!u,o,!0);return;case"textarea":at("invalid",t),g=p=u=null;for(w in o)if(o.hasOwnProperty(w)&&(R=o[w],R!=null))switch(w){case"value":u=R;break;case"defaultValue":p=R;break;case"children":g=R;break;case"dangerouslySetInnerHTML":if(R!=null)throw Error(a(91));break;default:Tt(t,r,w,R,o,null)}jc(t,u,p,g);return;case"option":for(z in o)o.hasOwnProperty(z)&&(u=o[z],u!=null)&&(z==="selected"?t.selected=u&&typeof u!="function"&&typeof u!="symbol":Tt(t,r,z,u,o,null));return;case"dialog":at("beforetoggle",t),at("toggle",t),at("cancel",t),at("close",t);break;case"iframe":case"object":at("load",t);break;case"video":case"audio":for(u=0;u<xl.length;u++)at(xl[u],t);break;case"image":at("error",t),at("load",t);break;case"details":at("toggle",t);break;case"embed":case"source":case"link":at("error",t),at("load",t);case"area":case"base":case"br":case"col":case"hr":case"keygen":case"meta":case"param":case"track":case"wbr":case"menuitem":for(X in o)if(o.hasOwnProperty(X)&&(u=o[X],u!=null))switch(X){case"children":case"dangerouslySetInnerHTML":throw Error(a(137,r));default:Tt(t,r,X,u,o,null)}return;default:if(Po(r)){for(re in o)o.hasOwnProperty(re)&&(u=o[re],u!==void 0&&zp(t,r,re,u,o,void 0));return}}for(R in o)o.hasOwnProperty(R)&&(u=o[R],u!=null&&Tt(t,r,R,u,o,null))}function lT(t,r,o,u){switch(r){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"input":var p=null,g=null,w=null,R=null,z=null,X=null,re=null;for(te in o){var le=o[te];if(o.hasOwnProperty(te)&&le!=null)switch(te){case"checked":break;case"value":break;case"defaultValue":z=le;default:u.hasOwnProperty(te)||Tt(t,r,te,null,u,le)}}for(var Q in u){var te=u[Q];if(le=o[Q],u.hasOwnProperty(Q)&&(te!=null||le!=null))switch(Q){case"type":g=te;break;case"name":p=te;break;case"checked":X=te;break;case"defaultChecked":re=te;break;case"value":w=te;break;case"defaultValue":R=te;break;case"children":case"dangerouslySetInnerHTML":if(te!=null)throw Error(a(137,r));break;default:te!==le&&Tt(t,r,Q,te,u,le)}}vs(t,w,R,z,X,re,g,p);return;case"select":te=w=R=Q=null;for(g in o)if(z=o[g],o.hasOwnProperty(g)&&z!=null)switch(g){case"value":break;case"multiple":te=z;default:u.hasOwnProperty(g)||Tt(t,r,g,null,u,z)}for(p in u)if(g=u[p],z=o[p],u.hasOwnProperty(p)&&(g!=null||z!=null))switch(p){case"value":Q=g;break;case"defaultValue":R=g;break;case"multiple":w=g;default:g!==z&&Tt(t,r,p,g,u,z)}r=R,o=w,u=te,Q!=null?Pa(t,!!o,Q,!1):!!u!=!!o&&(r!=null?Pa(t,!!o,r,!0):Pa(t,!!o,o?[]:"",!1));return;case"textarea":te=Q=null;for(R in o)if(p=o[R],o.hasOwnProperty(R)&&p!=null&&!u.hasOwnProperty(R))switch(R){case"value":break;case"children":break;default:Tt(t,r,R,null,u,p)}for(w in u)if(p=u[w],g=o[w],u.hasOwnProperty(w)&&(p!=null||g!=null))switch(w){case"value":Q=p;break;case"defaultValue":te=p;break;case"children":break;case"dangerouslySetInnerHTML":if(p!=null)throw Error(a(91));break;default:p!==g&&Tt(t,r,w,p,u,g)}Ba(t,Q,te);return;case"option":for(var De in o)Q=o[De],o.hasOwnProperty(De)&&Q!=null&&!u.hasOwnProperty(De)&&(De==="selected"?t.selected=!1:Tt(t,r,De,null,u,Q));for(z in u)Q=u[z],te=o[z],u.hasOwnProperty(z)&&Q!==te&&(Q!=null||te!=null)&&(z==="selected"?t.selected=Q&&typeof Q!="function"&&typeof Q!="symbol":Tt(t,r,z,Q,u,te));return;case"img":case"link":case"area":case"base":case"br":case"col":case"embed":case"hr":case"keygen":case"meta":case"param":case"source":case"track":case"wbr":case"menuitem":for(var Be in o)Q=o[Be],o.hasOwnProperty(Be)&&Q!=null&&!u.hasOwnProperty(Be)&&Tt(t,r,Be,null,u,Q);for(X in u)if(Q=u[X],te=o[X],u.hasOwnProperty(X)&&Q!==te&&(Q!=null||te!=null))switch(X){case"children":case"dangerouslySetInnerHTML":if(Q!=null)throw Error(a(137,r));break;default:Tt(t,r,X,Q,u,te)}return;default:if(Po(r)){for(var kt in o)Q=o[kt],o.hasOwnProperty(kt)&&Q!==void 0&&!u.hasOwnProperty(kt)&&zp(t,r,kt,void 0,u,Q);for(re in u)Q=u[re],te=o[re],!u.hasOwnProperty(re)||Q===te||Q===void 0&&te===void 0||zp(t,r,re,Q,u,te);return}}for(var F in o)Q=o[F],o.hasOwnProperty(F)&&Q!=null&&!u.hasOwnProperty(F)&&Tt(t,r,F,null,u,Q);for(le in u)Q=u[le],te=o[le],!u.hasOwnProperty(le)||Q===te||Q==null&&te==null||Tt(t,r,le,Q,u,te)}function DS(t){switch(t){case"css":case"script":case"font":case"img":case"image":case"input":case"link":return!0;default:return!1}}function cT(){if(typeof performance.getEntriesByType=="function"){for(var t=0,r=0,o=performance.getEntriesByType("resource"),u=0;u<o.length;u++){var p=o[u],g=p.transferSize,w=p.initiatorType,R=p.duration;if(g&&R&&DS(w)){for(w=0,R=p.responseEnd,u+=1;u<o.length;u++){var z=o[u],X=z.startTime;if(X>R)break;var re=z.transferSize,le=z.initiatorType;re&&DS(le)&&(z=z.responseEnd,w+=re*(z<R?1:(R-X)/(z-X)))}if(--u,r+=8*(g+w)/(p.duration/1e3),t++,10<t)break}}if(0<t)return r/t/1e6}return navigator.connection&&(t=navigator.connection.downlink,typeof t=="number")?t:5}var Up=null,Pp=null;function Bu(t){return t.nodeType===9?t:t.ownerDocument}function OS(t){switch(t){case"http://www.w3.org/2000/svg":return 1;case"http://www.w3.org/1998/Math/MathML":return 2;default:return 0}}function LS(t,r){if(t===0)switch(r){case"svg":return 1;case"math":return 2;default:return 0}return t===1&&r==="foreignObject"?0:t}function Bp(t,r){return t==="textarea"||t==="noscript"||typeof r.children=="string"||typeof r.children=="number"||typeof r.children=="bigint"||typeof r.dangerouslySetInnerHTML=="object"&&r.dangerouslySetInnerHTML!==null&&r.dangerouslySetInnerHTML.__html!=null}var qp=null;function uT(){var t=window.event;return t&&t.type==="popstate"?t===qp?!1:(qp=t,!0):(qp=null,!1)}var MS=typeof setTimeout=="function"?setTimeout:void 0,dT=typeof clearTimeout=="function"?clearTimeout:void 0,$S=typeof Promise=="function"?Promise:void 0,fT=typeof queueMicrotask=="function"?queueMicrotask:typeof $S<"u"?function(t){return $S.resolve(null).then(t).catch(hT)}:MS;function hT(t){setTimeout(function(){throw t})}function li(t){return t==="head"}function NS(t,r){var o=r,u=0;do{var p=o.nextSibling;if(t.removeChild(o),p&&p.nodeType===8)if(o=p.data,o==="/$"||o==="/&"){if(u===0){t.removeChild(p),Qs(r);return}u--}else if(o==="$"||o==="$?"||o==="$~"||o==="$!"||o==="&")u++;else if(o==="html")kl(t.ownerDocument.documentElement);else if(o==="head"){o=t.ownerDocument.head,kl(o);for(var g=o.firstChild;g;){var w=g.nextSibling,R=g.nodeName;g[Ai]||R==="SCRIPT"||R==="STYLE"||R==="LINK"&&g.rel.toLowerCase()==="stylesheet"||o.removeChild(g),g=w}}else o==="body"&&kl(t.ownerDocument.body);o=p}while(o);Qs(r)}function IS(t,r){var o=t;t=0;do{var u=o.nextSibling;if(o.nodeType===1?r?(o._stashedDisplay=o.style.display,o.style.display="none"):(o.style.display=o._stashedDisplay||"",o.getAttribute("style")===""&&o.removeAttribute("style")):o.nodeType===3&&(r?(o._stashedText=o.nodeValue,o.nodeValue=""):o.nodeValue=o._stashedText||""),u&&u.nodeType===8)if(o=u.data,o==="/$"){if(t===0)break;t--}else o!=="$"&&o!=="$?"&&o!=="$~"&&o!=="$!"||t++;o=u}while(o)}function Gp(t){var r=t.firstChild;for(r&&r.nodeType===10&&(r=r.nextSibling);r;){var o=r;switch(r=r.nextSibling,o.nodeName){case"HTML":case"HEAD":case"BODY":Gp(o),zo(o);continue;case"SCRIPT":case"STYLE":continue;case"LINK":if(o.rel.toLowerCase()==="stylesheet")continue}t.removeChild(o)}}function pT(t,r,o,u){for(;t.nodeType===1;){var p=o;if(t.nodeName.toLowerCase()!==r.toLowerCase()){if(!u&&(t.nodeName!=="INPUT"||t.type!=="hidden"))break}else if(u){if(!t[Ai])switch(r){case"meta":if(!t.hasAttribute("itemprop"))break;return t;case"link":if(g=t.getAttribute("rel"),g==="stylesheet"&&t.hasAttribute("data-precedence"))break;if(g!==p.rel||t.getAttribute("href")!==(p.href==null||p.href===""?null:p.href)||t.getAttribute("crossorigin")!==(p.crossOrigin==null?null:p.crossOrigin)||t.getAttribute("title")!==(p.title==null?null:p.title))break;return t;case"style":if(t.hasAttribute("data-precedence"))break;return t;case"script":if(g=t.getAttribute("src"),(g!==(p.src==null?null:p.src)||t.getAttribute("type")!==(p.type==null?null:p.type)||t.getAttribute("crossorigin")!==(p.crossOrigin==null?null:p.crossOrigin))&&g&&t.hasAttribute("async")&&!t.hasAttribute("itemprop"))break;return t;default:return t}}else if(r==="input"&&t.type==="hidden"){var g=p.name==null?null:""+p.name;if(p.type==="hidden"&&t.getAttribute("name")===g)return t}else return t;if(t=gr(t.nextSibling),t===null)break}return null}function gT(t,r,o){if(r==="")return null;for(;t.nodeType!==3;)if((t.nodeType!==1||t.nodeName!=="INPUT"||t.type!=="hidden")&&!o||(t=gr(t.nextSibling),t===null))return null;return t}function jS(t,r){for(;t.nodeType!==8;)if((t.nodeType!==1||t.nodeName!=="INPUT"||t.type!=="hidden")&&!r||(t=gr(t.nextSibling),t===null))return null;return t}function Vp(t){return t.data==="$?"||t.data==="$~"}function Fp(t){return t.data==="$!"||t.data==="$?"&&t.ownerDocument.readyState!=="loading"}function mT(t,r){var o=t.ownerDocument;if(t.data==="$~")t._reactRetry=r;else if(t.data!=="$?"||o.readyState!=="loading")r();else{var u=function(){r(),o.removeEventListener("DOMContentLoaded",u)};o.addEventListener("DOMContentLoaded",u),t._reactRetry=u}}function gr(t){for(;t!=null;t=t.nextSibling){var r=t.nodeType;if(r===1||r===3)break;if(r===8){if(r=t.data,r==="$"||r==="$!"||r==="$?"||r==="$~"||r==="&"||r==="F!"||r==="F")break;if(r==="/$"||r==="/&")return null}}return t}var Kp=null;function HS(t){t=t.nextSibling;for(var r=0;t;){if(t.nodeType===8){var o=t.data;if(o==="/$"||o==="/&"){if(r===0)return gr(t.nextSibling);r--}else o!=="$"&&o!=="$!"&&o!=="$?"&&o!=="$~"&&o!=="&"||r++}t=t.nextSibling}return null}function zS(t){t=t.previousSibling;for(var r=0;t;){if(t.nodeType===8){var o=t.data;if(o==="$"||o==="$!"||o==="$?"||o==="$~"||o==="&"){if(r===0)return t;r--}else o!=="/$"&&o!=="/&"||r++}t=t.previousSibling}return null}function US(t,r,o){switch(r=Bu(o),t){case"html":if(t=r.documentElement,!t)throw Error(a(452));return t;case"head":if(t=r.head,!t)throw Error(a(453));return t;case"body":if(t=r.body,!t)throw Error(a(454));return t;default:throw Error(a(451))}}function kl(t){for(var r=t.attributes;r.length;)t.removeAttributeNode(r[0]);zo(t)}var mr=new Map,PS=new Set;function qu(t){return typeof t.getRootNode=="function"?t.getRootNode():t.nodeType===9?t:t.ownerDocument}var _a=fe.d;fe.d={f:yT,r:bT,D:vT,C:ST,L:wT,m:CT,X:_T,S:ET,M:AT};function yT(){var t=_a.f(),r=$u();return t||r}function bT(t){var r=Fr(t);r!==null&&r.tag===5&&r.type==="form"?av(r):_a.r(t)}var Ys=typeof document>"u"?null:document;function BS(t,r,o){var u=Ys;if(u&&typeof r=="string"&&r){var p=jn(r);p='link[rel="'+t+'"][href="'+p+'"]',typeof o=="string"&&(p+='[crossorigin="'+o+'"]'),PS.has(p)||(PS.add(p),t={rel:t,crossOrigin:o,href:r},u.querySelector(p)===null&&(r=u.createElement("link"),wn(r,"link",t),Jt(r),u.head.appendChild(r)))}}function vT(t){_a.D(t),BS("dns-prefetch",t,null)}function ST(t,r){_a.C(t,r),BS("preconnect",t,r)}function wT(t,r,o){_a.L(t,r,o);var u=Ys;if(u&&t&&r){var p='link[rel="preload"][as="'+jn(r)+'"]';r==="image"&&o&&o.imageSrcSet?(p+='[imagesrcset="'+jn(o.imageSrcSet)+'"]',typeof o.imageSizes=="string"&&(p+='[imagesizes="'+jn(o.imageSizes)+'"]')):p+='[href="'+jn(t)+'"]';var g=p;switch(r){case"style":g=Xs(t);break;case"script":g=Js(t)}mr.has(g)||(t=b({rel:"preload",href:r==="image"&&o&&o.imageSrcSet?void 0:t,as:r},o),mr.set(g,t),u.querySelector(p)!==null||r==="style"&&u.querySelector(Rl(g))||r==="script"&&u.querySelector(Dl(g))||(r=u.createElement("link"),wn(r,"link",t),Jt(r),u.head.appendChild(r)))}}function CT(t,r){_a.m(t,r);var o=Ys;if(o&&t){var u=r&&typeof r.as=="string"?r.as:"script",p='link[rel="modulepreload"][as="'+jn(u)+'"][href="'+jn(t)+'"]',g=p;switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":g=Js(t)}if(!mr.has(g)&&(t=b({rel:"modulepreload",href:t},r),mr.set(g,t),o.querySelector(p)===null)){switch(u){case"audioworklet":case"paintworklet":case"serviceworker":case"sharedworker":case"worker":case"script":if(o.querySelector(Dl(g)))return}u=o.createElement("link"),wn(u,"link",t),Jt(u),o.head.appendChild(u)}}}function ET(t,r,o){_a.S(t,r,o);var u=Ys;if(u&&t){var p=Ha(u).hoistableStyles,g=Xs(t);r=r||"default";var w=p.get(g);if(!w){var R={loading:0,preload:null};if(w=u.querySelector(Rl(g)))R.loading=5;else{t=b({rel:"stylesheet",href:t,"data-precedence":r},o),(o=mr.get(g))&&Wp(t,o);var z=w=u.createElement("link");Jt(z),wn(z,"link",t),z._p=new Promise(function(X,re){z.onload=X,z.onerror=re}),z.addEventListener("load",function(){R.loading|=1}),z.addEventListener("error",function(){R.loading|=2}),R.loading|=4,Gu(w,r,u)}w={type:"stylesheet",instance:w,count:1,state:R},p.set(g,w)}}}function _T(t,r){_a.X(t,r);var o=Ys;if(o&&t){var u=Ha(o).hoistableScripts,p=Js(t),g=u.get(p);g||(g=o.querySelector(Dl(p)),g||(t=b({src:t,async:!0},r),(r=mr.get(p))&&Yp(t,r),g=o.createElement("script"),Jt(g),wn(g,"link",t),o.head.appendChild(g)),g={type:"script",instance:g,count:1,state:null},u.set(p,g))}}function AT(t,r){_a.M(t,r);var o=Ys;if(o&&t){var u=Ha(o).hoistableScripts,p=Js(t),g=u.get(p);g||(g=o.querySelector(Dl(p)),g||(t=b({src:t,async:!0,type:"module"},r),(r=mr.get(p))&&Yp(t,r),g=o.createElement("script"),Jt(g),wn(g,"link",t),o.head.appendChild(g)),g={type:"script",instance:g,count:1,state:null},u.set(p,g))}}function qS(t,r,o,u){var p=(p=ie.current)?qu(p):null;if(!p)throw Error(a(446));switch(t){case"meta":case"title":return null;case"style":return typeof o.precedence=="string"&&typeof o.href=="string"?(r=Xs(o.href),o=Ha(p).hoistableStyles,u=o.get(r),u||(u={type:"style",instance:null,count:0,state:null},o.set(r,u)),u):{type:"void",instance:null,count:0,state:null};case"link":if(o.rel==="stylesheet"&&typeof o.href=="string"&&typeof o.precedence=="string"){t=Xs(o.href);var g=Ha(p).hoistableStyles,w=g.get(t);if(w||(p=p.ownerDocument||p,w={type:"stylesheet",instance:null,count:0,state:{loading:0,preload:null}},g.set(t,w),(g=p.querySelector(Rl(t)))&&!g._p&&(w.instance=g,w.state.loading=5),mr.has(t)||(o={rel:"preload",as:"style",href:o.href,crossOrigin:o.crossOrigin,integrity:o.integrity,media:o.media,hrefLang:o.hrefLang,referrerPolicy:o.referrerPolicy},mr.set(t,o),g||xT(p,t,o,w.state))),r&&u===null)throw Error(a(528,""));return w}if(r&&u!==null)throw Error(a(529,""));return null;case"script":return r=o.async,o=o.src,typeof o=="string"&&r&&typeof r!="function"&&typeof r!="symbol"?(r=Js(o),o=Ha(p).hoistableScripts,u=o.get(r),u||(u={type:"script",instance:null,count:0,state:null},o.set(r,u)),u):{type:"void",instance:null,count:0,state:null};default:throw Error(a(444,t))}}function Xs(t){return'href="'+jn(t)+'"'}function Rl(t){return'link[rel="stylesheet"]['+t+"]"}function GS(t){return b({},t,{"data-precedence":t.precedence,precedence:null})}function xT(t,r,o,u){t.querySelector('link[rel="preload"][as="style"]['+r+"]")?u.loading=1:(r=t.createElement("link"),u.preload=r,r.addEventListener("load",function(){return u.loading|=1}),r.addEventListener("error",function(){return u.loading|=2}),wn(r,"link",o),Jt(r),t.head.appendChild(r))}function Js(t){return'[src="'+jn(t)+'"]'}function Dl(t){return"script[async]"+t}function VS(t,r,o){if(r.count++,r.instance===null)switch(r.type){case"style":var u=t.querySelector('style[data-href~="'+jn(o.href)+'"]');if(u)return r.instance=u,Jt(u),u;var p=b({},o,{"data-href":o.href,"data-precedence":o.precedence,href:null,precedence:null});return u=(t.ownerDocument||t).createElement("style"),Jt(u),wn(u,"style",p),Gu(u,o.precedence,t),r.instance=u;case"stylesheet":p=Xs(o.href);var g=t.querySelector(Rl(p));if(g)return r.state.loading|=4,r.instance=g,Jt(g),g;u=GS(o),(p=mr.get(p))&&Wp(u,p),g=(t.ownerDocument||t).createElement("link"),Jt(g);var w=g;return w._p=new Promise(function(R,z){w.onload=R,w.onerror=z}),wn(g,"link",u),r.state.loading|=4,Gu(g,o.precedence,t),r.instance=g;case"script":return g=Js(o.src),(p=t.querySelector(Dl(g)))?(r.instance=p,Jt(p),p):(u=o,(p=mr.get(g))&&(u=b({},o),Yp(u,p)),t=t.ownerDocument||t,p=t.createElement("script"),Jt(p),wn(p,"link",u),t.head.appendChild(p),r.instance=p);case"void":return null;default:throw Error(a(443,r.type))}else r.type==="stylesheet"&&(r.state.loading&4)===0&&(u=r.instance,r.state.loading|=4,Gu(u,o.precedence,t));return r.instance}function Gu(t,r,o){for(var u=o.querySelectorAll('link[rel="stylesheet"][data-precedence],style[data-precedence]'),p=u.length?u[u.length-1]:null,g=p,w=0;w<u.length;w++){var R=u[w];if(R.dataset.precedence===r)g=R;else if(g!==p)break}g?g.parentNode.insertBefore(t,g.nextSibling):(r=o.nodeType===9?o.head:o,r.insertBefore(t,r.firstChild))}function Wp(t,r){t.crossOrigin==null&&(t.crossOrigin=r.crossOrigin),t.referrerPolicy==null&&(t.referrerPolicy=r.referrerPolicy),t.title==null&&(t.title=r.title)}function Yp(t,r){t.crossOrigin==null&&(t.crossOrigin=r.crossOrigin),t.referrerPolicy==null&&(t.referrerPolicy=r.referrerPolicy),t.integrity==null&&(t.integrity=r.integrity)}var Vu=null;function FS(t,r,o){if(Vu===null){var u=new Map,p=Vu=new Map;p.set(o,u)}else p=Vu,u=p.get(o),u||(u=new Map,p.set(o,u));if(u.has(t))return u;for(u.set(t,null),o=o.getElementsByTagName(t),p=0;p<o.length;p++){var g=o[p];if(!(g[Ai]||g[Ht]||t==="link"&&g.getAttribute("rel")==="stylesheet")&&g.namespaceURI!=="http://www.w3.org/2000/svg"){var w=g.getAttribute(r)||"";w=t+w;var R=u.get(w);R?R.push(g):u.set(w,[g])}}return u}function KS(t,r,o){t=t.ownerDocument||t,t.head.insertBefore(o,r==="title"?t.querySelector("head > title"):null)}function TT(t,r,o){if(o===1||r.itemProp!=null)return!1;switch(t){case"meta":case"title":return!0;case"style":if(typeof r.precedence!="string"||typeof r.href!="string"||r.href==="")break;return!0;case"link":if(typeof r.rel!="string"||typeof r.href!="string"||r.href===""||r.onLoad||r.onError)break;return r.rel==="stylesheet"?(t=r.disabled,typeof r.precedence=="string"&&t==null):!0;case"script":if(r.async&&typeof r.async!="function"&&typeof r.async!="symbol"&&!r.onLoad&&!r.onError&&r.src&&typeof r.src=="string")return!0}return!1}function WS(t){return!(t.type==="stylesheet"&&(t.state.loading&3)===0)}function kT(t,r,o,u){if(o.type==="stylesheet"&&(typeof u.media!="string"||matchMedia(u.media).matches!==!1)&&(o.state.loading&4)===0){if(o.instance===null){var p=Xs(u.href),g=r.querySelector(Rl(p));if(g){r=g._p,r!==null&&typeof r=="object"&&typeof r.then=="function"&&(t.count++,t=Fu.bind(t),r.then(t,t)),o.state.loading|=4,o.instance=g,Jt(g);return}g=r.ownerDocument||r,u=GS(u),(p=mr.get(p))&&Wp(u,p),g=g.createElement("link"),Jt(g);var w=g;w._p=new Promise(function(R,z){w.onload=R,w.onerror=z}),wn(g,"link",u),o.instance=g}t.stylesheets===null&&(t.stylesheets=new Map),t.stylesheets.set(o,r),(r=o.state.preload)&&(o.state.loading&3)===0&&(t.count++,o=Fu.bind(t),r.addEventListener("load",o),r.addEventListener("error",o))}}var Xp=0;function RT(t,r){return t.stylesheets&&t.count===0&&Wu(t,t.stylesheets),0<t.count||0<t.imgCount?function(o){var u=setTimeout(function(){if(t.stylesheets&&Wu(t,t.stylesheets),t.unsuspend){var g=t.unsuspend;t.unsuspend=null,g()}},6e4+r);0<t.imgBytes&&Xp===0&&(Xp=62500*cT());var p=setTimeout(function(){if(t.waitingForImages=!1,t.count===0&&(t.stylesheets&&Wu(t,t.stylesheets),t.unsuspend)){var g=t.unsuspend;t.unsuspend=null,g()}},(t.imgBytes>Xp?50:800)+r);return t.unsuspend=o,function(){t.unsuspend=null,clearTimeout(u),clearTimeout(p)}}:null}function Fu(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Wu(this,this.stylesheets);else if(this.unsuspend){var t=this.unsuspend;this.unsuspend=null,t()}}}var Ku=null;function Wu(t,r){t.stylesheets=null,t.unsuspend!==null&&(t.count++,Ku=new Map,r.forEach(DT,t),Ku=null,Fu.call(t))}function DT(t,r){if(!(r.state.loading&4)){var o=Ku.get(t);if(o)var u=o.get(null);else{o=new Map,Ku.set(t,o);for(var p=t.querySelectorAll("link[data-precedence],style[data-precedence]"),g=0;g<p.length;g++){var w=p[g];(w.nodeName==="LINK"||w.getAttribute("media")!=="not all")&&(o.set(w.dataset.precedence,w),u=w)}u&&o.set(null,u)}p=r.instance,w=p.getAttribute("data-precedence"),g=o.get(w)||u,g===u&&o.set(null,p),o.set(w,p),this.count++,u=Fu.bind(this),p.addEventListener("load",u),p.addEventListener("error",u),g?g.parentNode.insertBefore(p,g.nextSibling):(t=t.nodeType===9?t.head:t,t.insertBefore(p,t.firstChild)),r.state.loading|=4}}var Ol={$$typeof:k,Provider:null,Consumer:null,_currentValue:Le,_currentValue2:Le,_threadCount:0};function OT(t,r,o,u,p,g,w,R,z){this.tag=1,this.containerInfo=t,this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.next=this.pendingContext=this.context=this.cancelPendingCommit=null,this.callbackPriority=0,this.expirationTimes=Re(-1),this.entangledLanes=this.shellSuspendCounter=this.errorRecoveryDisabledLanes=this.expiredLanes=this.warmLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Re(0),this.hiddenUpdates=Re(null),this.identifierPrefix=u,this.onUncaughtError=p,this.onCaughtError=g,this.onRecoverableError=w,this.pooledCache=null,this.pooledCacheLanes=0,this.formState=z,this.incompleteTransitions=new Map}function YS(t,r,o,u,p,g,w,R,z,X,re,le){return t=new OT(t,r,o,w,z,X,re,le,R),r=1,g===!0&&(r|=24),g=Yn(3,null,null,r),t.current=g,g.stateNode=t,r=Rh(),r.refCount++,t.pooledCache=r,r.refCount++,g.memoizedState={element:u,isDehydrated:o,cache:r},Mh(g),t}function XS(t){return t?(t=ks,t):ks}function JS(t,r,o,u,p,g){p=XS(p),u.context===null?u.context=p:u.pendingContext=p,u=Ja(r),u.payload={element:o},g=g===void 0?null:g,g!==null&&(u.callback=g),o=Qa(t,u,r),o!==null&&(Bn(o,t,r),cl(o,t,r))}function QS(t,r){if(t=t.memoizedState,t!==null&&t.dehydrated!==null){var o=t.retryLane;t.retryLane=o!==0&&o<r?o:r}}function Jp(t,r){QS(t,r),(t=t.alternate)&&QS(t,r)}function ZS(t){if(t.tag===13||t.tag===31){var r=zi(t,67108864);r!==null&&Bn(r,t,67108864),Jp(t,67108864)}}function ew(t){if(t.tag===13||t.tag===31){var r=er();r=tt(r);var o=zi(t,r);o!==null&&Bn(o,t,r),Jp(t,r)}}var Yu=!0;function LT(t,r,o,u){var p=J.T;J.T=null;var g=fe.p;try{fe.p=2,Qp(t,r,o,u)}finally{fe.p=g,J.T=p}}function MT(t,r,o,u){var p=J.T;J.T=null;var g=fe.p;try{fe.p=8,Qp(t,r,o,u)}finally{fe.p=g,J.T=p}}function Qp(t,r,o,u){if(Yu){var p=Zp(u);if(p===null)Hp(t,r,u,Xu,o),nw(t,u);else if(NT(p,t,r,o,u))u.stopPropagation();else if(nw(t,u),r&4&&-1<$T.indexOf(t)){for(;p!==null;){var g=Fr(p);if(g!==null)switch(g.tag){case 3:if(g=g.stateNode,g.current.memoizedState.isDehydrated){var w=Ee(g.pendingLanes);if(w!==0){var R=g;for(R.pendingLanes|=2,R.entangledLanes|=2;w;){var z=1<<31-on(w);R.entanglements[1]|=z,w&=~z}Jr(g),(vt&6)===0&&(Lu=qt()+500,Al(0))}}break;case 31:case 13:R=zi(g,2),R!==null&&Bn(R,g,2),$u(),Jp(g,2)}if(g=Zp(u),g===null&&Hp(t,r,u,Xu,o),g===p)break;p=g}p!==null&&u.stopPropagation()}else Hp(t,r,u,null,o)}}function Zp(t){return t=Go(t),eg(t)}var Xu=null;function eg(t){if(Xu=null,t=ja(t),t!==null){var r=l(t);if(r===null)t=null;else{var o=r.tag;if(o===13){if(t=c(r),t!==null)return t;t=null}else if(o===31){if(t=d(r),t!==null)return t;t=null}else if(o===3){if(r.stateNode.current.memoizedState.isDehydrated)return r.tag===3?r.stateNode.containerInfo:null;t=null}else r!==t&&(t=null)}}return Xu=t,null}function tw(t){switch(t){case"beforetoggle":case"cancel":case"click":case"close":case"contextmenu":case"copy":case"cut":case"auxclick":case"dblclick":case"dragend":case"dragstart":case"drop":case"focusin":case"focusout":case"input":case"invalid":case"keydown":case"keypress":case"keyup":case"mousedown":case"mouseup":case"paste":case"pause":case"play":case"pointercancel":case"pointerdown":case"pointerup":case"ratechange":case"reset":case"resize":case"seeked":case"submit":case"toggle":case"touchcancel":case"touchend":case"touchstart":case"volumechange":case"change":case"selectionchange":case"textInput":case"compositionstart":case"compositionend":case"compositionupdate":case"beforeblur":case"afterblur":case"beforeinput":case"blur":case"fullscreenchange":case"focus":case"hashchange":case"popstate":case"select":case"selectstart":return 2;case"drag":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"mousemove":case"mouseout":case"mouseover":case"pointermove":case"pointerout":case"pointerover":case"scroll":case"touchmove":case"wheel":case"mouseenter":case"mouseleave":case"pointerenter":case"pointerleave":return 8;case"message":switch(_i()){case aa:return 2;case gn:return 8;case Rn:case nt:return 32;case Nn:return 268435456;default:return 32}default:return 32}}var tg=!1,ci=null,ui=null,di=null,Ll=new Map,Ml=new Map,fi=[],$T="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset".split(" ");function nw(t,r){switch(t){case"focusin":case"focusout":ci=null;break;case"dragenter":case"dragleave":ui=null;break;case"mouseover":case"mouseout":di=null;break;case"pointerover":case"pointerout":Ll.delete(r.pointerId);break;case"gotpointercapture":case"lostpointercapture":Ml.delete(r.pointerId)}}function $l(t,r,o,u,p,g){return t===null||t.nativeEvent!==g?(t={blockedOn:r,domEventName:o,eventSystemFlags:u,nativeEvent:g,targetContainers:[p]},r!==null&&(r=Fr(r),r!==null&&ZS(r)),t):(t.eventSystemFlags|=u,r=t.targetContainers,p!==null&&r.indexOf(p)===-1&&r.push(p),t)}function NT(t,r,o,u,p){switch(r){case"focusin":return ci=$l(ci,t,r,o,u,p),!0;case"dragenter":return ui=$l(ui,t,r,o,u,p),!0;case"mouseover":return di=$l(di,t,r,o,u,p),!0;case"pointerover":var g=p.pointerId;return Ll.set(g,$l(Ll.get(g)||null,t,r,o,u,p)),!0;case"gotpointercapture":return g=p.pointerId,Ml.set(g,$l(Ml.get(g)||null,t,r,o,u,p)),!0}return!1}function rw(t){var r=ja(t.target);if(r!==null){var o=l(r);if(o!==null){if(r=o.tag,r===13){if(r=c(o),r!==null){t.blockedOn=r,Dn(t.priority,function(){ew(o)});return}}else if(r===31){if(r=d(o),r!==null){t.blockedOn=r,Dn(t.priority,function(){ew(o)});return}}else if(r===3&&o.stateNode.current.memoizedState.isDehydrated){t.blockedOn=o.tag===3?o.stateNode.containerInfo:null;return}}}t.blockedOn=null}function Ju(t){if(t.blockedOn!==null)return!1;for(var r=t.targetContainers;0<r.length;){var o=Zp(t.nativeEvent);if(o===null){o=t.nativeEvent;var u=new o.constructor(o.type,o);qo=u,o.target.dispatchEvent(u),qo=null}else return r=Fr(o),r!==null&&ZS(r),t.blockedOn=o,!1;r.shift()}return!0}function aw(t,r,o){Ju(t)&&o.delete(r)}function IT(){tg=!1,ci!==null&&Ju(ci)&&(ci=null),ui!==null&&Ju(ui)&&(ui=null),di!==null&&Ju(di)&&(di=null),Ll.forEach(aw),Ml.forEach(aw)}function Qu(t,r){t.blockedOn===r&&(t.blockedOn=null,tg||(tg=!0,s.unstable_scheduleCallback(s.unstable_NormalPriority,IT)))}var Zu=null;function iw(t){Zu!==t&&(Zu=t,s.unstable_scheduleCallback(s.unstable_NormalPriority,function(){Zu===t&&(Zu=null);for(var r=0;r<t.length;r+=3){var o=t[r],u=t[r+1],p=t[r+2];if(typeof u!="function"){if(eg(u||o)===null)continue;break}var g=Fr(o);g!==null&&(t.splice(r,3),r-=3,Zh(g,{pending:!0,data:p,method:o.method,action:u},u,p))}}))}function Qs(t){function r(z){return Qu(z,t)}ci!==null&&Qu(ci,t),ui!==null&&Qu(ui,t),di!==null&&Qu(di,t),Ll.forEach(r),Ml.forEach(r);for(var o=0;o<fi.length;o++){var u=fi[o];u.blockedOn===t&&(u.blockedOn=null)}for(;0<fi.length&&(o=fi[0],o.blockedOn===null);)rw(o),o.blockedOn===null&&fi.shift();if(o=(t.ownerDocument||t).$$reactFormReplay,o!=null)for(u=0;u<o.length;u+=3){var p=o[u],g=o[u+1],w=p[An]||null;if(typeof g=="function")w||iw(o);else if(w){var R=null;if(g&&g.hasAttribute("formAction")){if(p=g,w=g[An]||null)R=w.formAction;else if(eg(p)!==null)continue}else R=w.action;typeof R=="function"?o[u+1]=R:(o.splice(u,3),u-=3),iw(o)}}}function sw(){function t(g){g.canIntercept&&g.info==="react-transition"&&g.intercept({handler:function(){return new Promise(function(w){return p=w})},focusReset:"manual",scroll:"manual"})}function r(){p!==null&&(p(),p=null),u||setTimeout(o,20)}function o(){if(!u&&!navigation.transition){var g=navigation.currentEntry;g&&g.url!=null&&navigation.navigate(g.url,{state:g.getState(),info:"react-transition",history:"replace"})}}if(typeof navigation=="object"){var u=!1,p=null;return navigation.addEventListener("navigate",t),navigation.addEventListener("navigatesuccess",r),navigation.addEventListener("navigateerror",r),setTimeout(o,100),function(){u=!0,navigation.removeEventListener("navigate",t),navigation.removeEventListener("navigatesuccess",r),navigation.removeEventListener("navigateerror",r),p!==null&&(p(),p=null)}}}function ng(t){this._internalRoot=t}ed.prototype.render=ng.prototype.render=function(t){var r=this._internalRoot;if(r===null)throw Error(a(409));var o=r.current,u=er();JS(o,u,t,r,null,null)},ed.prototype.unmount=ng.prototype.unmount=function(){var t=this._internalRoot;if(t!==null){this._internalRoot=null;var r=t.containerInfo;JS(t.current,2,null,t,null,null),$u(),r[Ia]=null}};function ed(t){this._internalRoot=t}ed.prototype.unstable_scheduleHydration=function(t){if(t){var r=Mt();t={blockedOn:null,target:t,priority:r};for(var o=0;o<fi.length&&r!==0&&r<fi[o].priority;o++);fi.splice(o,0,t),o===0&&rw(t)}};var ow=e.version;if(ow!=="19.2.6")throw Error(a(527,ow,"19.2.6"));fe.findDOMNode=function(t){var r=t._reactInternals;if(r===void 0)throw typeof t.render=="function"?Error(a(188)):(t=Object.keys(t).join(","),Error(a(268,t)));return t=h(r),t=t!==null?m(t):null,t=t===null?null:t.stateNode,t};var jT={bundleType:0,version:"19.2.6",rendererPackageName:"react-dom",currentDispatcherRef:J,reconcilerVersion:"19.2.6"};if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<"u"){var td=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!td.isDisabled&&td.supportsFiber)try{or=td.inject(jT),sn=td}catch{}}return Zs.createRoot=function(t,r){if(!i(t))throw Error(a(299));var o=!1,u="",p=pv,g=gv,w=mv;return r!=null&&(r.unstable_strictMode===!0&&(o=!0),r.identifierPrefix!==void 0&&(u=r.identifierPrefix),r.onUncaughtError!==void 0&&(p=r.onUncaughtError),r.onCaughtError!==void 0&&(g=r.onCaughtError),r.onRecoverableError!==void 0&&(w=r.onRecoverableError)),r=YS(t,1,!1,null,null,o,u,null,p,g,w,sw),t[Ia]=r.current,jp(t),new ng(r)},Zs.hydrateRoot=function(t,r,o){if(!i(t))throw Error(a(299));var u=!1,p="",g=pv,w=gv,R=mv,z=null;return o!=null&&(o.unstable_strictMode===!0&&(u=!0),o.identifierPrefix!==void 0&&(p=o.identifierPrefix),o.onUncaughtError!==void 0&&(g=o.onUncaughtError),o.onCaughtError!==void 0&&(w=o.onCaughtError),o.onRecoverableError!==void 0&&(R=o.onRecoverableError),o.formState!==void 0&&(z=o.formState)),r=YS(t,1,!0,r,o??null,u,p,z,g,w,R,sw),r.context=XS(null),o=r.current,u=er(),u=tt(u),p=Ja(u),p.callback=null,Qa(o,p,u),o=u,r.current.lanes=o,et(r,o),Jr(r),t[Ia]=r.current,jp(t),new ed(r)},Zs.version="19.2.6",Zs}var dg;function mw(){if(dg)return ad.exports;dg=1;function s(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(s)}catch(e){console.error(e)}}return s(),ad.exports=gw(),ad.exports}var ld=mw();const cd=Zr(ld),eo=[],yw=50;let ud=!1;function fg(){try{return window.G7Core?.devTools}catch{return}}function Qi(s,e,n){const a=fg();if(ud&&a?.isEnabled?.()){a.trackLog?.(s,e,n);return}ud||(s==="error"||s==="warn")&&eo.length<yw&&eo.push({level:s,prefix:e,args:n,timestamp:Date.now()})}function dd(){ud=!0;const s=fg();if(!s?.isEnabled?.()){eo.length=0;return}for(const e of eo)s.trackLog?.(e.level,e.prefix,e.args);eo.length=0}const wi=class wi{constructor(){$(this,"debug");$(this,"prefix","[TemplateEngine]");this.debug=!1}static getInstance(){return wi.instance||(wi.instance=new wi),wi.instance}setDebug(e){this.debug=e}isDebugEnabled(){return this.debug}log(...e){this.debug&&console.log(this.prefix,...e),Qi("log","TemplateEngine",e)}error(...e){this.debug&&console.error(this.prefix,...e),Qi("error","TemplateEngine",e)}warn(...e){this.debug&&console.warn(this.prefix,...e),Qi("warn","TemplateEngine",e)}};$(wi,"instance");let to=wi;function dt(s){const e=`[${s}]`,n=to.getInstance();return{log:(...a)=>{n.isDebugEnabled()&&console.log(e,...a),Qi("log",s,a)},warn:(...a)=>{n.isDebugEnabled()&&console.warn(e,...a),Qi("warn",s,a)},error:(...a)=>{n.isDebugEnabled()&&console.error(e,...a),Qi("error",s,a)}}}to.getInstance();const Zi=dt("networkResilience"),hg=2,pg=300,gg=2e3,bw=15e3;function fd(s){return s?.name==="AbortError"}function mg(s){return fd(s)?!1:s instanceof TypeError}let Nl=!1,yg=!1;function hd(){return Nl}function vw(){typeof window>"u"||yg||(yg=!0,window.addEventListener("pagehide",()=>{Nl=!0}),window.addEventListener("pageshow",s=>{s.persisted&&(Nl=!1)}),window.addEventListener("beforeunload",()=>{Nl=!0}))}function bg(s,e,n){const a=Math.min(e*Math.pow(2,s),n),i=a*.25*(Math.random()*2-1);return Math.max(0,Math.round(a+i))}function vg(s){return new Promise(e=>setTimeout(e,s))}async function Il(s,e={}){const{retries:n=hg,baseDelayMs:a=pg,maxDelayMs:i=gg,timeoutMs:l=bw,label:c=s,init:d}=e,f=n+1;let h;for(let m=0;m<f;m++){let b=!1,S;const v=l>0?new AbortController:null;try{const _={...d};if(v){const x=d?.signal;x&&(x.aborted?v.abort():x.addEventListener("abort",()=>v.abort(),{once:!0})),_.signal=v.signal,S=setTimeout(()=>{b=!0,v.abort()},l)}return await fetch(s,_)}catch(_){if(h=_,fd(_)&&!b||!(b||mg(_)))throw _;if(hd())throw Zi.warn(`Document unloading, aborting retries: ${c}`),_;if(m===f-1)throw Zi.warn(`All ${f} attempts failed: ${c}`,_),_;const L=bg(m,a,i);Zi.warn(`Network failure (attempt ${m+1}/${f}), retrying in ${L}ms: ${c}`),await vg(L)}finally{S!==void 0&&clearTimeout(S)}}throw h}async function Sg(s,e={},n={}){const{retries:a=hg,baseDelayMs:i=pg,maxDelayMs:l=gg,label:c=s}=n,d=a+1;for(let f=0;f<d;f++)try{await Sw(s,e);return}catch(h){if(e.id&&document.getElementById(e.id)?.remove(),hd())throw Zi.warn(`Document unloading, aborting script retries: ${c}`),h;if(f===d-1)throw Zi.warn(`All ${d} script attempts failed: ${c}`),h;const b=bg(f,i,l);Zi.warn(`Script load failed (attempt ${f+1}/${d}), retrying in ${b}ms: ${c}`),await vg(b)}}function Sw(s,e){return new Promise((n,a)=>{const i=document.createElement("script");i.src=s,i.async=!1;for(const[l,c]of Object.entries(e))l==="id"?i.id=c:i.setAttribute(l,c);i.onload=()=>n(),i.onerror=()=>a(new Error(`Failed to load script: ${s}`)),document.head.appendChild(i)})}const wg="extension",es="extensionless",ww="file";function Cw(){const s=globalThis?.G7Config;if(globalThis?.__g7AssetUrlMode===es)return es;const n=s?.assetUrlMode;return n===es||n===wg?n:s?.settings?.general?.asset_url_mode===es?es:wg}function Cg(){return Cw()===es}function Ir(s,e,n,a){const i=s.replace(/\/+$/,""),l=e.replace(/^\.+/,""),c=Cg()?i:`${i}.${l}`,d=[];return a&&d.push(a.replace(/^[?&]+/,"")),n!=null&&n!==""&&d.push(`v=${n}`),d.length>0?`${c}?${d.join("&")}`:c}function Aa(s){if(!s||!Cg())return s;const e=globalThis?.location?.origin,n=e&&s.startsWith(e)?s.slice(e.length):s;if(!n.startsWith("/api/"))return s;const[a,i]=Ew(n),l=a.match(/^\/api\/(modules|plugins)\/bundle\.(js|css)$/);if(l)return Eg(`/api/${l[1]}/bundle/${l[2]}`,i);const c=a.match(/^\/api\/(templates|modules|plugins)\/assets\/([^/]+)\/(.+)$/);if(c){const[,f,h,m]=c,b=`${ww}=${encodeURIComponent(decodeURIComponent(m))}`;return`/api/${f}/assets/${h}?${b}${i?`&${i}`:""}`}const d=a.match(/^(\/api\/.+)\.(json|js|css)$/);return d?Eg(d[1],i):s}function Ew(s){const e=s.indexOf("?");return e===-1?[s,""]:[s.slice(0,e),s.slice(e+1)]}function Eg(s,e){return e?`${s}?${e}`:s}const jr=dt("ComponentRegistry");class un extends Error{constructor(n,a,i){super(n);$(this,"code");$(this,"details");this.code=a,this.details=i,this.name="ComponentRegistryError"}}const Fn=class Fn{constructor(){$(this,"registry",{});$(this,"manifest",null);$(this,"loadingState","idle");$(this,"error",null);$(this,"templateId",null);$(this,"templateType",null)}static getInstance(){return Fn.instance||(Fn.instance=new Fn),Fn.instance}static createIsolatedInstance(){return new Fn}static resetInstance(){Fn.instance=null}async loadComponents(e,n){if(this.loadingState==="loading")throw new un("Components are already being loaded","LOADING_IN_PROGRESS");if(this.loadingState==="loaded"&&this.templateId===e&&this.templateType===n){jr.log("Components already loaded for template:",e,n);return}this.loadingState="loading",this.templateId=e,this.templateType=n,this.error=null;try{await this.loadManifest(),await this.loadComponentBundle(),this.loadingState="loaded",jr.log("Successfully loaded components:",Object.keys(this.registry).length)}catch(a){throw this.loadingState="error",this.error=a instanceof Error?a:new Error(String(a)),new un(`Failed to load components: ${this.error.message}`,"LOAD_FAILED",{originalError:this.error})}}async loadManifest(){try{if(!this.templateId)throw new un("Template ID not set","TEMPLATE_ID_NOT_SET");const e=`${this.templateId}:${this.templateType}`;if(Fn.manifestCache.has(e)){this.manifest=Fn.manifestCache.get(e),jr.log("Manifest loaded from cache:",this.manifest.templateId);return}const n=Ir(`/api/templates/${this.templateId}/components`,"json"),a=await Il(n,{label:"components.json"});if(!a.ok)throw new un(`Failed to fetch manifest: ${a.status} ${a.statusText}`,"MANIFEST_FETCH_FAILED",{status:a.status,statusText:a.statusText});const i=await a.json();this.validateManifest(i),this.manifest=i,Fn.manifestCache.set(e,i),jr.log("Manifest loaded and cached:",i.templateId)}catch(e){throw e instanceof un?e:new un("Failed to load component manifest","MANIFEST_LOAD_FAILED",{originalError:e})}}validateManifest(e){if(!e.version)throw new un("Manifest missing required field: version","MANIFEST_INVALID",{field:"version"});if(!e.templateId)throw new un("Manifest missing required field: templateId","MANIFEST_INVALID",{field:"templateId"});if(!e.components||typeof e.components!="object")throw new un("Manifest missing required field: components","MANIFEST_INVALID",{field:"components"});const n=["basic","composite","layout"];for(const a of n){if(!Array.isArray(e.components[a]))throw new un(`Manifest field 'components.${a}' must be an array`,"MANIFEST_INVALID",{field:`components.${a}`,value:e.components[a]});e.components[a].forEach((i,l)=>{if(!i.name||typeof i.name!="string")throw new un(`Invalid component metadata at ${a}[${l}]: missing or invalid 'name'`,"MANIFEST_INVALID",{type:a,index:l,metadata:i});if(!i.type||typeof i.type!="string")throw new un(`Invalid component metadata at ${a}[${l}]: missing or invalid 'type'`,"MANIFEST_INVALID",{type:a,index:l,metadata:i})})}jr.log("Manifest validation passed")}async loadComponentBundle(){try{if(!this.templateId)throw new un("Template ID not set","TEMPLATE_ID_NOT_SET");const e=this.getGlobalVariableName(),n=window[e];if(!n||typeof n!="object")throw new un(`Component bundle not loaded. Expected global variable: ${e}. Ensure admin.blade.php includes the IIFE bundle script.`,"BUNDLE_NOT_LOADED",{expectedVariable:e});await this.registerComponentsFromManifest(n),jr.log("Component bundle loaded from global variable:",e)}catch(e){throw e instanceof un?e:new un("Failed to load component bundle","BUNDLE_LOAD_FAILED",{originalError:e})}}getGlobalVariableName(){if(!this.templateId)throw new un("Template ID not set","TEMPLATE_ID_NOT_SET");return this.templateId.split(/[-_]/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}async registerComponentsFromManifest(e){if(!this.manifest)throw new un("Manifest not loaded","MANIFEST_NOT_LOADED");const n=["basic","composite","layout"];for(const a of n){const i=this.manifest.components[a]||[];for(const l of i){const c=l.name,d=e[c];if(!d){jr.warn(`Component '${c}' not found in bundle`);continue}this.registerComponent(c,d,l)}}}registerComponent(e,n,a){this.registry[e]&&jr.warn(`Component '${e}' already registered, overwriting`);const i=Ye.memo(n);this.registry[e]={component:i,metadata:a},jr.log(`[ComponentRegistry] Registered component: ${e} (${a.type})`)}getComponent(e){const n=this.registry[e];return n?n.component:null}getMetadata(e){const n=this.registry[e];return n?n.metadata:null}hasComponent(e){return e in this.registry}getComponentsByType(e){return Object.entries(this.registry).filter(([n,a])=>a.metadata.type===e).map(([n,a])=>n)}getAllComponents(){return Object.keys(this.registry)}getComponentMap(){const e={};for(const[n,a]of Object.entries(this.registry))e[n]=a.component;return e}getLoadingState(){return this.loadingState}getError(){return this.error}getTemplateId(){return this.templateId}getManifest(){return this.manifest}clear(){this.registry={},this.manifest=null,this.loadingState="idle",this.error=null,this.templateId=null,this.templateType=null,jr.log("Registry cleared")}};$(Fn,"instance",null),$(Fn,"manifestCache",new Map);let yr=Fn;const _g=dt("TranslationEngine");class pd extends Error{constructor(n,a,i){super(n);$(this,"key");$(this,"locale");this.key=a,this.locale=i,this.name="TranslationError"}}const $n=class $n{constructor(e={}){$(this,"cache",new Map);$(this,"translations",new Map);$(this,"options");$(this,"cacheVersion",0);this.options={defaultLocale:e.defaultLocale||"ko",fallbackLocale:e.fallbackLocale||"en",cacheTTL:e.cacheTTL||3e5}}static getInstance(e={}){return $n.instance||($n.instance=new $n(e)),$n.instance}static resetInstance(){$n.instance=null}setCacheVersion(e){this.cacheVersion!==e&&(_g.log("Cache version updated:",this.cacheVersion,"->",e),this.cacheVersion=e,this.cache.clear())}getCacheVersion(){return this.cacheVersion}async loadTranslations(e,n,a="/api",i=!1){const l=`${e}:${n}`;if(!i){const c=this.getFromCache(l);if(c)return this.translations.set(l,c),c}try{const c=[];i&&c.push(`_=${Date.now()}`);const d=Ir(`${a}/templates/${e}/lang/${n}`,"json",this.cacheVersion>0?this.cacheVersion:null,c.length>0?c.join("&"):void 0),f=await fetch(d);if(!f.ok)throw new pd(`Failed to load translations: ${f.statusText}`,void 0,n);const m=await f.json();return this.saveToCache(l,m),this.translations.set(l,m),m}catch(c){throw new pd(`Failed to fetch translations for ${n}: ${c instanceof Error?c.message:String(c)}`,void 0,n)}}resolveTranslations(e,n,a){let i=e;const l=5;let c=0;for(;c<l&&i.includes("=$t:");){const d=i;if(i=i.replace($n.NESTED_TRANSLATION_PARAM_PATTERN,(f,h,m)=>"="+this.translate(m,n,void 0,a)),i===d)break;c++}return i.replace($n.TRANSLATION_PATTERN,(d,f,h)=>{const{cleanedParams:m,trailing:b}=this.separateTrailingText(h);return this.translate(f,n,m,a)+b})}translate(e,n,a,i){const l=this.getTranslation(e,n),c=a&&this.cleanParamsStr(a);if(c){const d=this.parseParams(c,i);return this.replaceParams(l,d)}return l}getTranslation(e,n){const{templateId:a,locale:i}=n,l=`${a}:${i}`,c=this.translations.get(l);if(c){const d=this.getNestedValue(c,e);if(d!==null)return d}if(i!==this.options.fallbackLocale){const d=`${a}:${this.options.fallbackLocale}`,f=this.translations.get(d);if(f){const h=this.getNestedValue(f,e);if(h!==null)return h}}return e}getNestedValue(e,n){const a=n.split(".");let i=e;for(const l of a){if(i==null||typeof i!="object")return null;i=i[l]}return typeof i=="string"?i:null}setTranslationValue(e,n,a,i){const l=`${e}:${n}`,c=this.translations.get(l)??{},d=a.split(".");let f=c;for(let h=0;h<d.length-1;h+=1){const m=d[h],b=f[m];(b==null||typeof b!="object"||Array.isArray(b))&&(f[m]={}),f=f[m]}f[d[d.length-1]]=i,this.translations.set(l,c)}parseParams(e,n){const a={},i=e.startsWith("|")?e.slice(1):e,l=[],d=i.replace(/\{\{(?:[^}]|\}(?!\}))*\}\}/g,f=>(l.push(f),`__PLACEHOLDER_${l.length-1}__`)).matchAll($n.PARAM_PATTERN);for(const f of d){const[,h,m]=f,b=m.replace(/__PLACEHOLDER_(\d+)__/g,(S,v)=>l[parseInt(v,10)]);a[h.trim()]=this.resolveParamValue(b.trim(),n)}return a}resolveParamValue(e,n){if(e.startsWith("{{")&&e.endsWith("}}")){const a=e.slice(2,-2).trim();if(/[|&()[\]!?:+\-*/%<>=\s]/.test(a)&&n)try{const l=Dd.evaluateExpression(a,n);return String(l??"")}catch(l){return _g.error("Expression evaluation failed:",a,l),""}else{const l=this.getNestedDataValue(n,a);return String(l??"")}}return e}separateTrailingText(e){if(!e)return{cleanedParams:void 0,trailing:""};if(e.trimEnd().endsWith("}}"))return{cleanedParams:e,trailing:""};if(!(e.startsWith("|")?e.slice(1):e).includes("="))return{cleanedParams:void 0,trailing:e};const a=e.match(/(\s+[^\p{L}\w=|&\s][^\p{L}\w=]*\s*)$/u);return a?{cleanedParams:e.slice(0,-a[1].length),trailing:a[1]}:{cleanedParams:e,trailing:""}}cleanParamsStr(e){return this.separateTrailingText(e).cleanedParams||""}getNestedDataValue(e,n){if(!e)return;const a=n.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}replaceParams(e,n){let a=e;for(const[i,l]of Object.entries(n)){const c=new RegExp(`\\{\\{${i}\\}\\}`,"g");a=a.replace(c,String(l));const d=new RegExp(`\\{${i}\\}`,"g");a=a.replace(d,String(l))}return a}getFromCache(e){const n=this.cache.get(e);return n?Date.now()-n.timestamp>this.options.cacheTTL?(this.cache.delete(e),null):n.data:null}saveToCache(e,n){this.cache.set(e,{data:n,timestamp:Date.now()})}clearCache(){this.cache.clear(),this.translations.clear()}pruneCache(){const e=Date.now(),n=[];for(const[a,i]of this.cache.entries())e-i.timestamp>this.options.cacheTTL&&n.push(a);for(const a of n)this.cache.delete(a)}getCacheStats(){const e=Date.now();let n=0;for(const a of this.cache.values())e-a.timestamp>this.options.cacheTTL&&n++;return{size:this.cache.size,expired:n}}};$($n,"instance",null),$($n,"TRANSLATION_PATTERN",/\$t:(?:defer:)?([a-zA-Z0-9._-]+)(\|(?:(?!\$t:).)+)?/g),$($n,"NESTED_TRANSLATION_PARAM_PATTERN",/=(\$t:([a-zA-Z0-9._-]+))(?=[|&\s]|$)/g),$($n,"PARAM_PATTERN",/([^=|&]+)=([^|&]+)/g);let xa=$n,gd=null;function _w(s){return gd||(gd=new xa(s)),gd}const Ag=Object.freeze(Object.defineProperty({__proto__:null,TranslationEngine:xa,TranslationError:pd,getTranslationEngine:_w},Symbol.toStringTag,{value:"Module"})),ts=dt("PipeRegistry");function xg(s,e){const n=s.getFullYear(),a=String(s.getMonth()+1).padStart(2,"0"),i=String(s.getDate()).padStart(2,"0"),l=String(s.getHours()).padStart(2,"0"),c=String(s.getMinutes()).padStart(2,"0"),d=String(s.getSeconds()).padStart(2,"0");return e.replace("YYYY",String(n)).replace("YY",String(n).slice(-2)).replace("MM",a).replace("M",String(s.getMonth()+1)).replace("DD",i).replace("D",String(s.getDate())).replace("HH",l).replace("H",String(s.getHours())).replace("mm",c).replace("m",String(s.getMinutes())).replace("ss",d).replace("s",String(s.getSeconds()))}function Aw(s,e="ko"){const a=new Date().getTime()-s.getTime(),i=Math.floor(a/1e3),l=Math.floor(i/60),c=Math.floor(l/60),d=Math.floor(c/24),f=Math.floor(d/7),h=Math.floor(d/30),m=Math.floor(d/365);return e==="ko"?i<60?"방금 전":l<60?`${l}분 전`:c<24?`${c}시간 전`:d<7?`${d}일 전`:f<4?`${f}주 전`:h<12?`${h}개월 전`:`${m}년 전`:i<60?"just now":l<60?`${l} minute${l===1?"":"s"} ago`:c<24?`${c} hour${c===1?"":"s"} ago`:d<7?`${d} day${d===1?"":"s"} ago`:f<4?`${f} week${f===1?"":"s"} ago`:h<12?`${h} month${h===1?"":"s"} ago`:`${m} year${m===1?"":"s"} ago`}function md(s){if(s==null)return null;if(s instanceof Date)return s;if(typeof s=="number")return new Date(s);if(typeof s=="string"){const e=new Date(s);return isNaN(e.getTime())?null:e}return null}const no={date:{fn:(s,e="YYYY-MM-DD")=>{const n=md(s);return n?xg(n,e):""},description:"날짜 포맷 (기본: YYYY-MM-DD)"},datetime:{fn:(s,e="YYYY-MM-DD HH:mm")=>{const n=md(s);return n?xg(n,e):""},description:"날짜+시간 포맷 (기본: YYYY-MM-DD HH:mm)"},relativeTime:{fn:(s,e="ko")=>{const n=md(s);return n?Aw(n,e):""},description:'상대 시간 표시 (예: "3분 전")'},number:{fn:(s,e)=>{const n=Number(s);if(isNaN(n))return String(s??"");const a={};return e!==void 0&&(a.minimumFractionDigits=e,a.maximumFractionDigits=e),n.toLocaleString(void 0,a)},description:"숫자 포맷 (천단위 구분, 선택적 소수점)"},truncate:{fn:(s,e=100,n="...")=>typeof s!="string"?String(s??""):s.length<=e?s:s.slice(0,e)+n,description:'문자열 자르기 (기본: 100자, 접미사: "...")'},uppercase:{fn:s=>typeof s!="string"?String(s??""):s.toUpperCase(),description:"대문자 변환"},lowercase:{fn:s=>typeof s!="string"?String(s??""):s.toLowerCase(),description:"소문자 변환"},stripHtml:{fn:s=>typeof s!="string"?String(s??""):s.replace(/<[^>]*>/g,""),description:"HTML 태그 제거"},default:{fn:(s,e="")=>s==null||s===""?e:s,description:"기본값 설정 (null, undefined, 빈문자열 시)"},fallback:{fn:(s,e)=>s??e,description:"폴백 값 설정 (null, undefined 시만)"},first:{fn:s=>{if(Array.isArray(s))return s[0]},description:"배열의 첫 번째 요소"},last:{fn:s=>{if(Array.isArray(s))return s[s.length-1]},description:"배열의 마지막 요소"},join:{fn:(s,e=", ")=>Array.isArray(s)?s.join(e):"",description:'배열을 문자열로 결합 (기본 구분자: ", ")'},length:{fn:s=>Array.isArray(s)||typeof s=="string"?s.length:0,description:"배열/문자열 길이"},filterBy:{fn:(s,e,n)=>Array.isArray(s)?Array.isArray(e)?s.filter(a=>{const i=n?a?.[n]:a;return e.includes(i)}):s:[],description:"배열 필터링 (allowList에 포함된 항목만 반환)"},keys:{fn:s=>s==null||typeof s!="object"?[]:Object.keys(s),description:"객체의 키 배열"},values:{fn:s=>s==null||typeof s!="object"?[]:Object.values(s),description:"객체의 값 배열"},json:{fn:(s,e)=>{try{return JSON.stringify(s,null,e)}catch{return""}},description:"JSON 문자열로 변환"},localized:{fn:(s,e)=>s==null?"":typeof s=="string"?s:typeof s!="object"?String(s):s[e||"ko"]||s.ko||s.en||Object.values(s)[0]||"",description:"다국어 객체에서 로케일에 맞는 값 추출"}},ns=new Map,Ci=class Ci{static getInstance(){return Ci.instance||(Ci.instance=new Ci),Ci.instance}register(e,n,a){if(no[e]){ts.warn(`[PipeRegistry] 내장 파이프를 덮어쓸 수 없습니다: ${e}`);return}ns.set(e,{fn:n,description:a}),ts.log(`[PipeRegistry] 커스텀 파이프 등록됨: ${e}`)}unregister(e){if(no[e])return ts.warn(`[PipeRegistry] 내장 파이프는 해제할 수 없습니다: ${e}`),!1;const n=ns.delete(e);return n&&ts.log(`[PipeRegistry] 커스텀 파이프 해제됨: ${e}`),n}get(e){const n=no[e];return n?n.fn:ns.get(e)?.fn}has(e){return!!no[e]||ns.has(e)}execute(e,n,a=[]){const i=this.get(e);if(!i)return ts.warn(`[PipeRegistry] 알 수 없는 파이프: ${e}`),n;try{return i(n,...a)}catch(l){return ts.error(`[PipeRegistry] 파이프 실행 오류 (${e}):`,l),n}}list(){const e=[];for(const[n,a]of Object.entries(no))e.push({name:n,description:a.description,type:"built-in"});for(const[n,a]of ns.entries())e.push({name:n,description:a.description,type:"custom"});return e.sort((n,a)=>n.name.localeCompare(a.name))}clearCustomPipes(){ns.clear()}};$(Ci,"instance",null);let jl=Ci;jl.getInstance();function xw(s){const e=s.trim(),n=e.indexOf("(");if(n===-1)return{name:e,args:[]};const a=e.slice(0,n).trim(),i=e.slice(n+1,-1);if(!i.trim())return{name:a,args:[]};const l=[];let c="",d=null,f=0;for(let h=0;h<i.length;h++){const m=i[h];if((h>0?i[h-1]:"")==="\\"){c+=m;continue}if((m==='"'||m==="'")&&!d){d=m;continue}if(m===d){d=null;continue}if(d){c+=m;continue}if(m==="("||m==="["||m==="{"){f++,c+=m;continue}if(m===")"||m==="]"||m==="}"){f--,c+=m;continue}if(m===","&&f===0){l.push(Tg(c.trim())),c="";continue}c+=m}return c.trim()&&l.push(Tg(c.trim())),{name:a,args:l}}function Tg(s){if(s==="null")return null;if(s==="undefined")return;if(s==="true")return!0;if(s==="false")return!1;const e=Number(s);return!isNaN(e)&&s!==""?e:s}function Tw(s){const e=[];let n="",a=null,i=0;for(let l=0;l<s.length;l++){const c=s[l],d=l>0?s[l-1]:"",f=l<s.length-1?s[l+1]:"";if(d==="\\"){n+=c;continue}if((c==='"'||c==="'")&&!a){a=c,n+=c;continue}if(c===a){a=null,n+=c;continue}if(a){n+=c;continue}if(c==="("||c==="["||c==="{"){i++,n+=c;continue}if(c===")"||c==="]"||c==="}"){i--,n+=c;continue}if(c==="|"&&f!=="|"&&d!=="|"&&i===0){e.push(n.trim()),n="";continue}n+=c}return n.trim()&&e.push(n.trim()),e.length===0?[s,[]]:[e[0],e.slice(1)]}function qn(s){let e=null,n=0;for(let a=0;a<s.length;a++){const i=s[a],l=a>0?s[a-1]:"",c=a<s.length-1?s[a+1]:"";if(l!=="\\"){if((i==='"'||i==="'")&&!e){e=i;continue}if(i===e){e=null;continue}if(!e){if(i==="("||i==="["||i==="{"){n++;continue}if(i===")"||i==="]"||i==="}"){n--;continue}if(i==="|"&&c!=="|"&&l!=="|"&&n===0)return!0}}}return!1}function kw(s,e){const n=jl.getInstance();let a=s;for(const i of e){const l=xw(i);a=n.execute(l.name,a,l.args)}return a}const yd=new Map([["true",!0],["false",!1],["null",null],["undefined",void 0]]);function Ta(s){if(typeof s!="string"||!s.startsWith("{{")||!s.endsWith("}}")||s.length<4)return null;const e=s.slice(2,-2);let n=null,a=0;for(let i=0;i<e.length;i++){const l=e[i];if(n!==null){if(l==="\\"){i++;continue}l===n&&(n=null);continue}if(l==='"'||l==="'"||l==="`"){n=l;continue}if(l==="{"){a++;continue}if(l==="}"){if(a>0){a--;continue}return null}}return a===0?e.trim():null}function Rw(s){const e=[];if(typeof s!="string")return e;for(let n=0;n<s.length-1;n++){if(s[n]!=="{"||s[n+1]!=="{")continue;let a=null,i=0,l=-1;for(let c=n+2;c<s.length;c++){const d=s[c];if(a!==null){if(d==="\\"){c++;continue}d===a&&(a=null);continue}if(d==='"'||d==="'"||d==="`"){a=d;continue}if(d==="{"){i++;continue}if(d==="}"){if(i>0){i--;continue}if(s[c+1]==="}"){l=c;break}break}}l!==-1&&(e.push({start:n,end:l+2,expr:s.slice(n+2,l)}),n=l+1)}return e}function ro(s){return/[?:|&!+\-*/<>=()[\]{}]/.test(s)}function kg(s){const e=typeof s=="string"?s.trim():"";return e===""?"empty":yd.has(e)?"literal":qn(e)?"pipe":ro(e)?"expression":"path"}function bd(s,e,n,a){const i=typeof s=="string"?s.trim():"",l=kg(i),c=a?.skipCache?{skipCache:!0}:void 0;if(l==="empty"){a?.onEmpty?.(s);return}if(l==="literal")return yd.get(i);try{return l==="pipe"?n.evaluatePipeExpression(i,e,c,a?.trackingInfo):l==="expression"?n.evaluateExpression(i,e,a?.trackingInfo):n.resolve(i,e,c,a?.trackingInfo)}catch(d){if(a?.onError)return a.onError(d,i);throw d}}const tr="raw:";function Rg(s){return s.startsWith(tr)?s.slice(tr.length):s}const ao="﷐",pi="﷑",ka="﷒";function io(s){return ao+s+pi}function Hl(s){return s.length>=2&&s.charCodeAt(0)===64976&&s.charCodeAt(s.length-1)===64977}function zl(s){return s.includes(ao)}function vd(s){return s.slice(1,-1)}function rs(s){if(typeof s=="string")return io(s);if(Array.isArray(s))return s.map(rs);if(s&&typeof s=="object"){const e={};for(const[n,a]of Object.entries(s))e[n]=rs(a);return e}return s}function Ra(s){if(typeof s=="string")return Hl(s)?vd(s):zl(s)||s.includes(pi)?s.split(ao).join("").split(pi).join(""):s;if(Array.isArray(s)){let e=!1;const n=s.map(a=>{const i=Ra(a);return i!==a&&(e=!0),i});return e?n:s}if(s&&typeof s=="object"){if(s.$$typeof!==void 0)return s;let e=!1;const n={};for(const[a,i]of Object.entries(s)){const l=Ra(i);l!==i&&(e=!0),n[a]=l}return e?n:s}return s}const so=new Set(["constructor","__proto__","prototype","__lookupGetter__","__lookupSetter__","__defineGetter__","__defineSetter__"]),Dw=new Set(["Function","eval","globalThis","window","self","document","require","module","process","Reflect","Proxy","WebAssembly","import","constructor"]);function Dg(s,e){if(s!==null&&(typeof s=="object"||typeof s=="function")&&Og.has(s))throw new Error(`Object.${e} on a built-in global object is not allowed`);return s}const Ow=Object.freeze({keys:Object.keys,values:Object.values,entries:Object.entries,assign:(s,...e)=>{const n=Dg(s,"assign");for(const a of e)if(a!=null)for(const i of Object.keys(a)){if(so.has(i))throw new Error(`Access to "${i}" is forbidden`);Td(n,i,a[i])}return n},freeze:s=>Object.freeze(Dg(s,"freeze")),fromEntries:Object.fromEntries,create:Object.create,isFrozen:Object.isFrozen}),Sd={Math,JSON,Date,Array,Object:Ow,Number,String,Boolean,Set,Map,WeakSet,WeakMap,parseInt,parseFloat,isNaN,isFinite},wd={Date,Set,Map,WeakSet,WeakMap,Array,Number,String,Boolean,Object},Og=new Set([...Object.values(Sd),...Object.values(wd)].filter(s=>s!==null&&(typeof s=="object"||typeof s=="function"))),Ul=new Set(["function"]),br=Symbol("short-circuit");class Cd{constructor(e){$(this,"value");this.value=e}}class Ed{}class _d{}function Lw(s){return/[A-Za-z_$]/.test(s)}function Mw(s){return/[A-Za-z0-9_$]/.test(s)}function gi(s){return s>="0"&&s<="9"}function $w(s,e,n){let a=e+1;for(;a<s.length;){if(s[a]==="\\"){a+=2;continue}if(s[a]===n)return a+1;a++}throw new Error(`Unterminated string literal at position ${e}`)}function Lg(s,e){let n=e+1;const a=[],i=[];let l="";for(;n<s.length;){const c=s[n];if(c==="\\"){const d=s[n+1];switch(d){case"n":l+=` +`;break;case"t":l+=" ";break;case"r":l+="\r";break;case"b":l+="\b";break;case"f":l+="\f";break;case"v":l+="\v";break;case"0":l+="\0";break;case"\\":l+="\\";break;case"`":l+="`";break;case"$":l+="$";break;default:l+=d;break}n+=2;continue}if(c==="`")return a.push(l),{quasis:a,exprs:i,end:n+1};if(c==="$"&&s[n+1]==="{"){a.push(l),l="",n+=2;const d=n;let f=1;for(;n<s.length&&f>0;){const h=s[n];if(h==="{")f++,n++;else if(h==="}"){if(f--,f===0)break;n++}else h==='"'||h==="'"?n=$w(s,n,h):h==="`"?n=Lg(s,n).end:n++}if(f!==0)throw new Error("Unterminated ${...} in template literal");i.push(s.slice(d,n)),n++;continue}l+=c,n++}throw new Error(`Unterminated template literal at position ${e}`)}function Nw(s){const e=[];let n=0;const a=s.length,i=l=>n+l<a?s[n+l]:"";for(;n<a;){const l=s[n];if(l===" "||l===" "||l===` +`||l==="\r"||l==="\f"||l==="\v"){n++;continue}if(gi(l)||l==="."&&gi(i(1))){const h=n;for(;n<a&&gi(s[n]);)n++;if(s[n]===".")for(n++;n<a&&gi(s[n]);)n++;if(s[n]==="e"||s[n]==="E"){if(n++,(s[n]==="+"||s[n]==="-")&&n++,!gi(s[n]))throw new Error(`Invalid number literal at position ${h}`);for(;n<a&&gi(s[n]);)n++}const m=s.slice(h,n);e.push({type:"num",value:Number(m),pos:h});continue}if(l==='"'||l==="'"){const h=n,m=l;n++;let b="";for(;n<a&&s[n]!==m;)if(s[n]==="\\"){n++;const S=s[n];switch(S){case"n":b+=` +`;break;case"t":b+=" ";break;case"r":b+="\r";break;case"b":b+="\b";break;case"f":b+="\f";break;case"v":b+="\v";break;case"0":b+="\0";break;case"\\":b+="\\";break;case"'":b+="'";break;case'"':b+='"';break;case"`":b+="`";break;default:b+=S;break}n++}else b+=s[n],n++;if(n>=a)throw new Error(`Unterminated string literal at position ${h}`);n++,e.push({type:"str",value:b,pos:h});continue}if(Lw(l)){const h=n;for(n++;n<a&&Mw(s[n]);)n++;e.push({type:"ident",value:s.slice(h,n),pos:h});continue}if(l==="`"){const h=Lg(s,n);e.push({type:"template",value:"",pos:n,quasis:h.quasis,exprs:h.exprs}),n=h.end;continue}const c=n,d=s.substr(n,2);if(s.substr(n,3)==="..."){e.push({type:"punct",value:"...",pos:c}),n+=3;continue}switch(l){case"=":{i(1)===">"?(e.push({type:"punct",value:"=>",pos:c}),n+=2):i(1)==="="?i(2)==="="?(e.push({type:"punct",value:"===",pos:c}),n+=3):(e.push({type:"punct",value:"==",pos:c}),n+=2):(e.push({type:"punct",value:"=",pos:c}),n+=1);continue}case"!":{i(1)==="="?i(2)==="="?(e.push({type:"punct",value:"!==",pos:c}),n+=3):(e.push({type:"punct",value:"!=",pos:c}),n+=2):(e.push({type:"punct",value:"!",pos:c}),n+=1);continue}case"<":{if(i(1)==="=")e.push({type:"punct",value:"<=",pos:c}),n+=2;else{if(i(1)==="<")throw new Error("Bitwise/shift operators are not allowed");e.push({type:"punct",value:"<",pos:c}),n+=1}continue}case">":{if(i(1)==="=")e.push({type:"punct",value:">=",pos:c}),n+=2;else{if(i(1)===">")throw new Error("Bitwise/shift operators are not allowed");e.push({type:"punct",value:">",pos:c}),n+=1}continue}case"&":{if(i(1)==="&")e.push({type:"punct",value:"&&",pos:c}),n+=2;else throw new Error("Bitwise operators are not allowed");continue}case"|":{if(i(1)==="|")e.push({type:"punct",value:"||",pos:c}),n+=2;else throw new Error("Bitwise operators are not allowed");continue}case"?":{i(1)==="."&&!gi(i(2))?(e.push({type:"punct",value:"?.",pos:c}),n+=2):i(1)==="?"?(e.push({type:"punct",value:"??",pos:c}),n+=2):(e.push({type:"punct",value:"?",pos:c}),n+=1);continue}case"+":{if(i(1)==="+")throw new Error("Increment operator is not allowed");if(i(1)==="=")throw new Error("Assignment operators are not allowed");e.push({type:"punct",value:"+",pos:c}),n+=1;continue}case"-":{if(i(1)==="-")throw new Error("Decrement operator is not allowed");if(i(1)==="=")throw new Error("Assignment operators are not allowed");e.push({type:"punct",value:"-",pos:c}),n+=1;continue}case"*":{if(i(1)==="=")throw new Error("Assignment operators are not allowed");if(i(1)==="*")throw new Error("Exponentiation operator is not supported");e.push({type:"punct",value:"*",pos:c}),n+=1;continue}case"/":{if(i(1)==="=")throw new Error("Assignment operators are not allowed");e.push({type:"punct",value:"/",pos:c}),n+=1;continue}case"%":{if(i(1)==="=")throw new Error("Assignment operators are not allowed");e.push({type:"punct",value:"%",pos:c}),n+=1;continue}case"~":case"^":throw new Error("Bitwise operators are not allowed");case".":case":":case"(":case")":case"[":case"]":case"{":case"}":case",":e.push({type:"punct",value:l,pos:c}),n+=1;continue;case";":e.push({type:"punct",value:";",pos:c}),n+=1;continue;default:throw new Error(`Unexpected character "${l}" at position ${c}`)}}return e}const Iw={"??":1,"||":1,"&&":2,"===":3,"!==":3,"==":3,"!=":3,"<":4,">":4,"<=":4,">=":4,"+":5,"-":5,"*":6,"/":6,"%":6},jw=new Set(["??","||","&&"]);class Hw{constructor(e){$(this,"tokens");$(this,"pos",0);this.tokens=e}atEnd(){return this.pos>=this.tokens.length}peek(e=0){const n=this.pos+e;return n<this.tokens.length?this.tokens[n]:null}next(){if(this.atEnd())throw new Error("Unexpected end of expression");return this.tokens[this.pos++]}isPunct(e,n=0){const a=this.peek(n);return!!a&&a.type==="punct"&&a.value===e}expectPunct(e){const n=this.peek();if(!n||n.type!=="punct"||n.value!==e)throw new Error(`Expected "${e}" but found "${n?n.value:"<end>"}"`);this.pos++}parseExpression(){const e=this.tryParseArrow();return e||this.parseTernary()}tryParseArrow(){const e=this.pos,n=this.peek();if(!n)return null;if(n.type==="ident"&&this.isPunct("=>",1)&&!Ul.has(String(n.value))){this.pos+=1,this.pos+=1;const{body:a,isBlock:i}=this.parseArrowBody();return{type:"Arrow",params:[{name:String(n.value),default:null}],body:a,isBlock:i}}if(n.type==="punct"&&n.value==="("){this.pos+=1;const a=this.tryParseParamList();if(a&&this.isPunct(")")&&(this.pos+=1,this.isPunct("=>"))){this.pos+=1;const{body:i,isBlock:l}=this.parseArrowBody();return{type:"Arrow",params:a,body:i,isBlock:l}}return this.pos=e,null}return this.pos=e,null}tryParseParamList(){const e=[];if(this.isPunct(")"))return e;for(;;){const n=this.peek();let a=null,i;if(n&&n.type==="punct"&&n.value==="["){const c=this.tryParseArrayPattern();if(!c)return null;i=c}else if(n&&n.type==="ident"&&!Ul.has(String(n.value)))a=String(n.value),this.pos+=1;else return null;let l=null;if(this.isPunct("=")&&(this.pos+=1,l=this.parseExpression()),e.push(i?{name:a,elements:i,default:l}:{name:a,default:l}),this.isPunct(",")){this.pos+=1;continue}break}return e}tryParseArrayPattern(){const e=this.pos;this.pos+=1;const n=[];for(;;){if(this.isPunct("]"))return this.pos+=1,n;if(this.isPunct(",")){n.push(null),this.pos+=1;continue}const a=this.peek();if(!a||a.type!=="ident"||Ul.has(String(a.value)))return this.pos=e,null;if(n.push(String(a.value)),this.pos+=1,this.isPunct(",")){this.pos+=1;continue}return this.isPunct("]")?(this.pos+=1,n):(this.pos=e,null)}}parseArrowBody(){return this.isPunct("{")?{body:this.parseBlock(),isBlock:!0}:{body:this.parseExpression(),isBlock:!1}}parseBlock(){this.expectPunct("{");const e=[];for(;!this.isPunct("}");){if(this.atEnd())throw new Error("Unterminated block");e.push(this.parseStatement())}return this.expectPunct("}"),{type:"Block",body:e}}parseStatement(){const e=this.peek();if(!e)throw new Error("Unexpected end of statement");if(e.type==="punct"&&e.value==="{")return this.parseBlock();if(e.type==="punct"&&e.value===";")return this.pos+=1,{type:"Empty"};if(e.type==="ident")switch(String(e.value)){case"const":case"let":return this.parseVarDecl();case"if":return this.parseIf();case"for":return this.parseForOf();case"return":return this.parseReturn();case"try":return this.parseTry();case"break":return this.pos+=1,this.consumeSemicolon(),{type:"Break"};case"continue":return this.pos+=1,this.consumeSemicolon(),{type:"Continue"}}const n=this.parseExpression();return this.consumeSemicolon(),{type:"ExprStmt",expression:n}}consumeSemicolon(){this.isPunct(";")&&(this.pos+=1)}parseVarDecl(){this.pos+=1;const e=[];for(;;){const n=this.peek();if(!n||n.type!=="ident")throw new Error("Expected variable name in declaration");const a=String(n.value);this.pos+=1,this.expectPunct("=");const i=this.parseExpression();if(e.push({name:a,init:i}),this.isPunct(",")){this.pos+=1;continue}break}return this.consumeSemicolon(),{type:"VarDecl",declarations:e}}parseIf(){this.pos+=1,this.expectPunct("(");const e=this.parseExpression();this.expectPunct(")");const n=this.parseStatement();let a=null;const i=this.peek();return i&&i.type==="ident"&&i.value==="else"&&(this.pos+=1,a=this.parseStatement()),{type:"If",test:e,consequent:n,alternate:a}}parseForOf(){this.pos+=1,this.expectPunct("(");const e=this.peek();if(!e||e.type!=="ident"||e.value!=="const"&&e.value!=="let")throw new Error('Only "for (const x of …)" / "for (let x of …)" loops are supported');this.pos+=1;const n=this.peek();if(!n||n.type!=="ident")throw new Error("Expected loop variable name");const a=String(n.value);this.pos+=1;const i=this.peek();if(!i||i.type!=="ident"||i.value!=="of")throw new Error("Only for-of loops are supported (for-in / C-style for are not allowed)");this.pos+=1;const l=this.parseExpression();this.expectPunct(")");const c=this.parseStatement();return{type:"ForOf",name:a,iterable:l,body:c}}parseReturn(){if(this.pos+=1,this.isPunct(";")||this.isPunct("}")||this.atEnd())return this.consumeSemicolon(),{type:"Return",argument:null};const e=this.parseExpression();return this.consumeSemicolon(),{type:"Return",argument:e}}parseTry(){this.pos+=1;const e=this.parseBlock();let n=null,a=null,i=null;const l=this.peek();if(l&&l.type==="ident"&&l.value==="catch"){if(this.pos+=1,this.isPunct("(")){this.pos+=1;const d=this.peek();d&&d.type==="ident"&&(n=String(d.value),this.pos+=1),this.expectPunct(")")}a=this.parseBlock()}const c=this.peek();if(c&&c.type==="ident"&&c.value==="finally"&&(this.pos+=1,i=this.parseBlock()),!a&&!i)throw new Error("Missing catch or finally after try");return{type:"Try",block:e,handlerParam:n,handler:a,finalizer:i}}parseFunctionExpression(){this.pos+=1;let e=null;const n=this.peek();n&&n.type==="ident"&&n.value!==void 0&&!this.isPunct("(")&&(e=String(n.value),this.pos+=1),this.expectPunct("(");const a=this.tryParseParamList();if(!a)throw new Error("Invalid function parameter list");this.expectPunct(")");const i=this.parseBlock();return{type:"Function",name:e,params:a,body:i}}parseTernary(){const e=this.parseBinary(0);if(this.isPunct("?")){this.pos+=1;const n=this.parseExpression();this.expectPunct(":");const a=this.parseExpression();return{type:"Conditional",test:e,consequent:n,alternate:a}}return e}parseBinary(e){let n=this.parseUnary();for(;;){const a=this.peek();if(!a||a.type!=="punct")break;const i=String(a.value),l=Iw[i];if(l===void 0||l<e)break;this.pos+=1;const c=this.parseBinary(l+1);jw.has(i)?n={type:"Logical",operator:i,left:n,right:c}:n={type:"Binary",operator:i,left:n,right:c}}return n}parseUnary(){const e=this.peek();if(e){if(e.type==="punct"&&(e.value==="!"||e.value==="-"||e.value==="+")){this.pos+=1;const n=this.parseUnary();return{type:"Unary",operator:String(e.value),argument:n}}if(e.type==="ident"&&e.value==="typeof")return this.pos+=1,{type:"Unary",operator:"typeof",argument:this.parseUnary()};if(e.type==="ident"&&e.value==="delete"){this.pos+=1;const n=this.parseUnary();if(n.type!=="Member")throw new Error("delete is only allowed on a property reference");return{type:"Delete",argument:n}}}return this.parsePostfix()}parsePostfix(){let e=this.parsePrimary();for(;;){const n=this.peek();if(!n||n.type!=="punct")break;if(n.value==="."){this.pos+=1;const a=this.next();if(a.type!=="ident")throw new Error(`Expected property name after "." but found "${a.value}"`);const i=String(a.value);if(so.has(i))throw new Error(`Access to "${i}" is forbidden`);e={type:"Member",object:e,property:{type:"Literal",value:i},computed:!1,optional:!1}}else if(n.value==="?.")if(this.pos+=1,this.isPunct("[")){this.pos+=1;const a=this.parseExpression();this.expectPunct("]"),this.assertComputedKeyNotBlocked(a),e={type:"Member",object:e,property:a,computed:!0,optional:!0}}else if(this.isPunct("(")){const a=this.parseArguments();e={type:"Call",callee:e,args:a,optional:!0}}else{const a=this.next();if(a.type!=="ident")throw new Error(`Expected property name after "?." but found "${a.value}"`);const i=String(a.value);if(so.has(i))throw new Error(`Access to "${i}" is forbidden`);e={type:"Member",object:e,property:{type:"Literal",value:i},computed:!1,optional:!0}}else if(n.value==="["){this.pos+=1;const a=this.parseExpression();this.expectPunct("]"),this.assertComputedKeyNotBlocked(a),e={type:"Member",object:e,property:a,computed:!0,optional:!1}}else if(n.value==="("){const a=this.parseArguments();e={type:"Call",callee:e,args:a,optional:!1}}else break}return e}assertComputedKeyNotBlocked(e){if(e.type==="Literal"&&typeof e.value=="string"&&so.has(e.value))throw new Error(`Access to "${e.value}" is forbidden`)}parseArguments(){this.expectPunct("(");const e=[];if(!this.isPunct(")"))for(;;){if(this.isPunct("...")?(this.pos+=1,e.push({type:"Spread",argument:this.parseExpression()})):e.push(this.parseExpression()),this.isPunct(",")){this.pos+=1;continue}break}return this.expectPunct(")"),e}parseNew(){this.expectIdent("new");const e=this.peek();if(!e||e.type!=="ident")throw new Error("new is only allowed on whitelisted built-in constructors");const n=String(e.value);if(!Object.prototype.hasOwnProperty.call(wd,n))throw new Error("new is only allowed on whitelisted built-in constructors");this.pos+=1;let a=[];return this.isPunct("(")&&(a=this.parseArguments()),{type:"New",ctor:n,args:a}}expectIdent(e){const n=this.peek();if(!n||n.type!=="ident"||n.value!==e)throw new Error(`Expected "${e}"`);this.pos+=1}parsePrimary(){const e=this.peek();if(!e)throw new Error("Unexpected end of expression");if(e.type==="num")return this.pos+=1,{type:"Literal",value:e.value};if(e.type==="str")return this.pos+=1,{type:"Literal",value:e.value};if(e.type==="template"){this.pos+=1;const n=e.quasis||[""],i=(e.exprs||[]).map(l=>Mg(l));return{type:"Template",quasis:n,expressions:i}}if(e.type==="ident"&&e.value==="new")return this.parseNew();if(e.type==="ident"&&e.value==="function")return this.parseFunctionExpression();if(e.type==="ident"){const n=String(e.value);if(Ul.has(n))throw new Error(`Keyword "${n}" is not allowed`);switch(this.pos+=1,n){case"true":return{type:"Literal",value:!0};case"false":return{type:"Literal",value:!1};case"null":return{type:"Literal",value:null};case"undefined":return{type:"Literal",value:void 0};default:return{type:"Identifier",name:n}}}if(e.type==="punct"){if(e.value==="("){this.pos+=1;const n=this.parseExpression();return this.expectPunct(")"),n}if(e.value==="[")return this.parseArrayLiteral();if(e.value==="{")return this.parseObjectLiteral()}throw new Error(`Unexpected token "${e.value}"`)}parseArrayLiteral(){this.expectPunct("[");const e=[];for(;!this.isPunct("]");){if(this.isPunct(",")){this.pos+=1,e.push({type:"Literal",value:void 0});continue}if(this.isPunct("...")?(this.pos+=1,e.push({type:"Spread",argument:this.parseExpression()})):e.push(this.parseExpression()),this.isPunct(",")){this.pos+=1;continue}break}return this.expectPunct("]"),{type:"Array",elements:e}}parseObjectLiteral(){this.expectPunct("{");const e=[];for(;!this.isPunct("}");){if(this.isPunct("..."))this.pos+=1,e.push({kind:"spread",value:this.parseExpression()});else{let n,a=!1;const i=this.peek();if(!i)throw new Error("Unexpected end of expression in object literal");if(i.type==="punct"&&i.value==="[")this.pos+=1,n=this.parseExpression(),this.expectPunct("]"),a=!0;else if(i.type==="str")this.pos+=1,n={type:"Literal",value:i.value};else if(i.type==="num")this.pos+=1,n={type:"Literal",value:String(i.value)};else if(i.type==="ident")this.pos+=1,n={type:"Literal",value:String(i.value)};else throw new Error(`Unexpected token "${i.value}" in object literal`);if(this.isPunct(":")){this.pos+=1;const l=this.parseExpression();e.push({kind:"init",key:n,computed:a,value:l})}else{if(a||n.type!=="Literal"||typeof n.value!="string")throw new Error("Invalid shorthand property in object literal");if(i.type!=="ident")throw new Error("Invalid shorthand property in object literal");e.push({kind:"init",key:n,computed:!1,value:{type:"Identifier",name:n.value}})}}if(this.isPunct(",")){this.pos+=1;continue}break}return this.expectPunct("}"),{type:"Object",properties:e}}}function Mg(s){const e=Nw(s),n=new Hw(e),a=n.parseExpression();if(!n.atEnd()){const i=n.peek();throw i&&i.type==="punct"&&i.value===","?new Error("The comma/sequence operator is not allowed"):new Error(`Unexpected token "${i?i.value:"<end>"}" after expression`)}return a}function zw(s,e){let n=e;for(;n;){if(Object.prototype.hasOwnProperty.call(n.vars,s))return n.vars[s];n=n.parent}if(Object.prototype.hasOwnProperty.call(Sd,s))return Sd[s];if(Dw.has(s))throw new Error(`Reference to forbidden global "${s}" is not allowed`)}function Ad(s){const e=typeof s=="symbol"?s:String(s);if(typeof e=="string"&&so.has(e))throw new Error(`Access to "${e}" is forbidden`);return e}function xd(s,e){return s.type==="Member"?$g(s,e):s.type==="Call"?Ng(s,e):Qe(s,e)}function $g(s,e){const n=xd(s.object,e);if(n===br||s.optional&&n==null)return br;let a;s.computed?a=Qe(s.property,e):a=s.property.value;const i=Ad(a);if(n!=null)return n[i]}function Ng(s,e){let n,a;if(s.callee.type==="Member"){const l=s.callee,c=xd(l.object,e);if(c===br||l.optional&&c==null)return br;let d;l.computed?d=Qe(l.property,e):d=l.property.value;const f=Ad(d);c==null?n=void 0:(n=c[f],a=c)}else if(n=xd(s.callee,e),n===br)return br;if(s.optional&&n==null)return br;if(typeof n!="function")throw new Error("Attempted to call a non-function value");const i=Ig(s.args,e);return n.apply(a,i)}function Ig(s,e){const n=[];for(const a of s)if(a.type==="Spread"){const i=Qe(a.argument,e);if(i!=null)for(const l of i)n.push(l)}else n.push(Qe(a,e));return n}function jg(s,e,n){for(let a=0;a<s.length;a++){const i=s[a];let l=e[a];if(l===void 0&&i.default&&(l=Qe(i.default,n)),i.elements){if(l==null)throw new TypeError(`Cannot destructure ${String(l)}: value is not iterable`);const c=Array.isArray(l)?l:Array.from(l);for(let d=0;d<i.elements.length;d++){const f=i.elements[d];f!==null&&(n.vars[f]=c[d])}}else i.name!==null&&(n.vars[i.name]=l)}}function Hg(s,e){try{Qe(s,e)}catch(n){if(n instanceof Cd)return n.value;throw n}}function Qe(s,e){switch(s.type){case"Literal":return s.value;case"Identifier":return zw(s.name,e);case"Member":{const n=$g(s,e);return n===br?void 0:n}case"Call":{const n=Ng(s,e);return n===br?void 0:n}case"Unary":{const n=Qe(s.argument,e);switch(s.operator){case"!":return!n;case"-":return-n;case"+":return+n;case"typeof":return typeof n;default:throw new Error(`Unknown unary operator "${s.operator}"`)}}case"Binary":{const n=Qe(s.left,e),a=Qe(s.right,e);switch(s.operator){case"+":return n+a;case"-":return n-a;case"*":return n*a;case"/":return n/a;case"%":return n%a;case"===":return n===a;case"!==":return n!==a;case"==":return n==a;case"!=":return n!=a;case"<":return n<a;case">":return n>a;case"<=":return n<=a;case">=":return n>=a;default:throw new Error(`Unknown binary operator "${s.operator}"`)}}case"Logical":{const n=Qe(s.left,e);switch(s.operator){case"&&":return n&&Qe(s.right,e);case"||":return n||Qe(s.right,e);case"??":return n??Qe(s.right,e);default:throw new Error(`Unknown logical operator "${s.operator}"`)}}case"Conditional":return Qe(s.test,e)?Qe(s.consequent,e):Qe(s.alternate,e);case"Array":{const n=[];for(const a of s.elements)if(a.type==="Spread"){const i=Qe(a.argument,e);if(i!=null)for(const l of i)n.push(l)}else n.push(Qe(a,e));return n}case"Object":{const n={};for(const a of s.properties)if(a.kind==="spread"){const i=Qe(a.value,e);if(i!=null&&typeof i=="object")for(const l of Object.keys(i))Td(n,l,i[l])}else{let i;a.computed?i=Qe(a.key,e):i=a.key.value;const l=Qe(a.value,e);Td(n,String(i),l)}return n}case"Arrow":{const n=e,a=s.params,i=s.body,l=s.isBlock;return function(...d){const f={vars:{},parent:n};return jg(a,d,f),l?Hg(i,f):Qe(i,f)}}case"Function":{const n=e,a=s.params,i=s.body,l=s.name,c=function(...f){const h={vars:{},parent:n};return l&&(h.vars[l]=c),jg(a,f,h),Hg(i,h)};return c}case"Delete":{const n=s.argument,a=Qe(n.object,e);let i;n.computed?i=Qe(n.property,e):i=n.property.value;const l=Ad(i);if(a==null||typeof a!="object"&&typeof a!="function")return!0;if(Og.has(a))throw new Error("delete on a built-in global object is not allowed");return delete a[l]}case"Block":{const n={vars:{},parent:e};for(const a of s.body)Qe(a,n);return}case"VarDecl":{for(const n of s.declarations)e.vars[n.name]=Qe(n.init,e);return}case"If":{Qe(s.test,e)?Qe(s.consequent,e):s.alternate&&Qe(s.alternate,e);return}case"ForOf":{const n=Qe(s.iterable,e);if(n!=null)for(const a of n){const i={vars:{},parent:e};i.vars[s.name]=a;try{Qe(s.body,i)}catch(l){if(l instanceof _d)continue;if(l instanceof Ed)break;throw l}}return}case"Return":throw new Cd(s.argument?Qe(s.argument,e):void 0);case"ExprStmt":Qe(s.expression,e);return;case"Break":throw new Ed;case"Continue":throw new _d;case"Empty":return;case"Try":{try{try{Qe(s.block,e)}catch(n){if(n instanceof Cd||n instanceof Ed||n instanceof _d)throw n;if(s.handler){const a={vars:{},parent:e};s.handlerParam&&(a.vars[s.handlerParam]=n),Qe(s.handler,a)}else throw n}}finally{s.finalizer&&Qe(s.finalizer,e)}return}case"New":{const n=wd[s.ctor];if(typeof n!="function")throw new Error("new on non-whitelisted constructor");const a=Ig(s.args,e);return new n(...a)}case"Template":{let n=s.quasis[0]??"";for(let a=0;a<s.expressions.length;a++)n+=String(Qe(s.expressions[a],e)),n+=s.quasis[a+1]??"";return n}case"Spread":throw new Error("Unexpected spread element");default:throw new Error(`Unknown node type "${s.type}"`)}}function Td(s,e,n){Object.defineProperty(s,e,{value:n,enumerable:!0,writable:!0,configurable:!0})}function kd(s,e){if(typeof s!="string")throw new Error("Expression must be a string");const n=Mg(s);return Qe(n,{vars:e||{},parent:null})}const Da=dt("DataBindingEngine");function oo(){try{return window.G7Core?.devTools}catch{return}}class Rd extends Error{constructor(n,a,i){super(n);$(this,"path");$(this,"context");this.path=a,this.context=i,this.name="DataBindingError"}}const ra=class ra{constructor(e){$(this,"cache",new Map);$(this,"renderCycleCache",new Map);$(this,"currentRenderCycleId",0);$(this,"defaultOptions",{defaultValue:void 0,nullSafe:!0,detectCircular:!0,maxDepth:10});e&&(this.defaultOptions={...this.defaultOptions,...e})}startRenderCycle(){this.currentRenderCycleId++,this.renderCycleCache.clear()}isRenderCycleCacheActive(){return this.currentRenderCycleId>0}getFromRenderCycleCache(e){if(this.isRenderCycleCacheActive())return this.renderCycleCache.get(e)}saveToRenderCycleCache(e,n){this.isRenderCycleCacheActive()&&this.renderCycleCache.set(e,n)}resolveBindings(e,n,a,i){const l={...this.defaultOptions,...a},c=oo(),d=n._computed&&!n.$computed?{...n,$computed:n._computed}:n,f=(S,v)=>{const _=v.trim();let A=!1,x=_;if(_.startsWith(tr)&&(A=!0,x=_.slice(tr.length)),qn(x))try{const P=this.evaluatePipeExpression(x,d,l,{...i,method:"resolveBindings",displayExpression:`{{${_}}}`}),W=this.formatValue(P);return A?io(W):W}catch(P){return Da.error("Pipe expression evaluation failed:",x,P),S}if(ro(x)){const P=c?.isEnabled()?performance.now():0;try{let W,pe=!1;if(l.skipCache)W=this.evaluateExpression(x,d);else{const we=`expr:${x}`,Ue=this.getFromRenderCycleCache(we);Ue!==void 0?(W=Ue,pe=!0):(W=this.evaluateExpression(x,d),this.saveToRenderCycleCache(we,W))}if(c?.isEnabled()){const we=performance.now()-P;c.trackExpressionEval({expression:`{{${_}}}`,result:this.sanitizeResultForTracking(W),resultType:this.getResultType(W),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:pe,duration:we,method:"resolveBindings",skipCache:l.skipCache})}const Se=this.formatValue(W);return A?io(Se):Se}catch(W){return Da.error("Expression evaluation failed:",x,W),S}}if(l.skipCache){const P=c?.isEnabled()?performance.now():0,W=this.resolvePath(x,d,l);if(c?.isEnabled()){const Se=performance.now()-P;c.trackExpressionEval({expression:`{{${_}}}`,result:this.sanitizeResultForTracking(W),resultType:this.getResultType(W),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!1,duration:Se,method:"resolveBindings",skipCache:!0})}const pe=this.formatValue(W);return A?io(pe):pe}const M=x.startsWith("_global")||x.startsWith("_local")||x.startsWith("_isolated")||x.startsWith("$parent"),k=c?.isEnabled()?performance.now():0;let O,B=!1;if(M){const P=this.getFromRenderCycleCache(x);P!==void 0?(O=P,B=!0):(O=this.resolvePath(x,d,l),this.saveToRenderCycleCache(x,O))}else{const P=this.getFromCache(x);P!==void 0?(O=P,B=!0):(O=this.resolvePath(x,d,l),this.saveToCache(x,O))}if(c?.isEnabled()){const P=performance.now()-k;c.trackExpressionEval({expression:`{{${_}}}`,result:this.sanitizeResultForTracking(O),resultType:this.getResultType(O),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:B,duration:P,method:"resolveBindings",skipCache:l.skipCache})}const G=this.formatValue(O);return A?io(G):G},h=Rw(e);if(h.length===0)return e;let m="",b=0;for(const S of h)m+=e.slice(b,S.start),m+=f(e.slice(S.start,S.end),S.expr),b=S.end;return m+e.slice(b)}evaluatePipeExpression(e,n,a,i){const l={...this.defaultOptions,...a},c=oo(),d=c?.isEnabled()?performance.now():0,f=n._computed&&!n.$computed?{...n,$computed:n._computed}:n,[h,m]=Tw(e);let b,S=!1;const v=ro(h),_=h.startsWith("_global")||h.startsWith("_local")||h.startsWith("_isolated")||h.startsWith("$parent");if(l.skipCache)v?b=this.evaluateExpression(h,f):b=this.resolvePath(h,f,l);else if(v){const x=this.getFromRenderCycleCache(`expr:${h}`);x!==void 0?(b=x,S=!0):(b=this.evaluateExpression(h,f),this.saveToRenderCycleCache(`expr:${h}`,b))}else if(_){const x=this.getFromRenderCycleCache(h);x!==void 0?(b=x,S=!0):(b=this.resolvePath(h,f,l),this.saveToRenderCycleCache(h,b))}else{const x=this.getFromCache(h);x!==void 0?(b=x,S=!0):(b=this.resolvePath(h,f,l),this.saveToCache(h,b))}const A=kw(b,m);if(c?.isEnabled()){const x=performance.now()-d;c.trackExpressionEval({expression:i?.displayExpression??`{{${e}}}`,result:this.sanitizeResultForTracking(A),resultType:this.getResultType(A),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:S,duration:x,method:i?.method??"evaluatePipeExpression",skipCache:l.skipCache})}return A}resolve(e,n,a,i){const l={...this.defaultOptions,...a},c=oo(),d=c?.isEnabled()?performance.now():0;if(l.skipCache){const b=this.resolvePath(e,n,l);return c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(b),resultType:this.getResultType(b),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!1,duration:performance.now()-d,method:"resolve",skipCache:!0}),b}const f=e.startsWith("_global")||e.startsWith("_local")||e.startsWith("_isolated")||e.startsWith("$parent");let h=!1;if(f){const b=this.getFromRenderCycleCache(e);if(b!==void 0)return h=!0,c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(b),resultType:this.getResultType(b),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!0,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),b}else{const b=this.getFromCache(e);if(b!==void 0)return h=!0,c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(b),resultType:this.getResultType(b),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:!0,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),b}const m=this.resolvePath(e,n,l);return f?this.saveToRenderCycleCache(e,m):this.saveToCache(e,m),c?.isEnabled()&&c.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(m),resultType:this.getResultType(m),componentId:i?.componentId,componentName:i?.componentName,propName:i?.propName,fromCache:h,duration:performance.now()-d,method:"resolve",skipCache:l.skipCache}),m}resolvePath(e,n,a,i=0,l=new WeakSet){if(typeof e!="string"||e.trim()===""){Da.warn("resolvePath: 빈 경로가 전달되었습니다 — undefined 로 해석합니다.");return}if(a.maxDepth&&i>a.maxDepth){if(a.detectCircular)throw new Rd(`Maximum depth exceeded (${a.maxDepth}). Possible circular reference.`,e,n);return a.defaultValue}const c=this.parsePath(e);let d=n;for(let f=0;f<c.length;f++){const h=c[f];if(d==null){if(a.nullSafe)return a.defaultValue;throw new Rd(`Cannot read property '${h}' of ${d}`,e,n)}if(a.detectCircular&&typeof d=="object"){if(l.has(d))throw new Rd(`Circular reference detected at path: ${c.slice(0,f+1).join(".")}`,e,n);l.add(d)}d=d[h],e==="_global.toasts"&&Da.log(`[DataBindingEngine] segment: ${h}, current:`,d)}return d!==void 0?d:a.defaultValue}parsePath(e){const n=[];let a;for(ra.PATH_SEGMENT_PATTERN.lastIndex=0;(a=ra.PATH_SEGMENT_PATTERN.exec(e))!==null;){const i=a[1]||a[2];i&&n.push(i)}return n}formatValue(e){if(e==null)return"";if(typeof e=="object")try{return JSON.stringify(e)}catch{return"[Object]"}return String(e)}getFromCache(e){const n=this.cache.get(e),a=oo();if(!n){a?.isEnabled()&&a.recordCacheMiss();return}if(Date.now()-n.timestamp>ra.CACHE_EXPIRY){this.cache.delete(e),a?.isEnabled()&&a.recordCacheMiss();return}return a?.isEnabled()&&a.recordCacheHit(),n.value}saveToCache(e,n){this.cache.set(e,{value:n,timestamp:Date.now()})}isActionDefinition(e){return typeof e.handler!="string"?!1:e.params!==void 0||Array.isArray(e.actions)||typeof e.target=="string"||e.onSuccess!==void 0||e.onError!==void 0}resolveObject(e,n,a){if(this.isSwitchExpression(e)){const d=e;return this.resolveSwitch(d,n,a)}if(this.isActionDefinition(e))return{...e};if("iteration"in e)return{...e};const i={},c=[...["cellChildren","expandChildren","expandContext","render"],...a?.skipBindingKeys||[]];for(const[d,f]of Object.entries(e)){if(c.includes(d)){i[d]=f;continue}try{this.resolveObjectEntry(i,d,f,n,a)}catch(h){Da.warn(`resolveObject: 값 해석 실패 (key: ${d}):`,h),i[d]=void 0}}return i}resolveObjectEntry(e,n,a,i,l){if(typeof a=="string"){const c=Ta(a);if(c!==null){const d=c.trim();let f=!1,h=d;d.startsWith(tr)&&(f=!0,h=d.slice(tr.length));let m;switch(kg(h)){case"empty":Da.warn(`resolveObject: 빈 바인딩 \`{{}}\` (key: ${n}) — undefined 로 해석합니다.`),m=void 0;break;case"literal":m=yd.get(h);break;case"pipe":m=this.evaluatePipeExpression(h,i,l);break;case"expression":m=this.evaluateExpression(h,i,l);break;default:m=this.resolve(h,i,l)}e[n]=f&&m!=null?rs(m):m}else e[n]=this.resolveBindings(a,i,l)}else Array.isArray(a)?e[n]=a.map(c=>typeof c=="string"?this.resolveBindings(c,i,l):typeof c=="object"&&c!==null?this.resolveObject(c,i,l):c):a&&typeof a=="object"?e[n]=this.resolveObject(a,i,l):e[n]=a}clearCache(){this.cache.clear()}invalidateCacheByKeys(e){for(const n of this.cache.keys())for(const a of e)if(n===a||n.startsWith(`${a}.`)||n.startsWith(`${a}[`)){this.cache.delete(n);break}}pruneCache(){const e=Date.now();for(const[n,a]of this.cache.entries())e-a.timestamp>ra.CACHE_EXPIRY&&this.cache.delete(n)}getCacheStats(){const e=Date.now();let n=0;for(const a of this.cache.values())e-a.timestamp>ra.CACHE_EXPIRY&&n++;return{size:this.cache.size,expired:n}}evaluateExpression(e,n,a){const i=oo(),l=i?.isEnabled()?performance.now():0;i?.isEnabled()&&i.trackBindingEval();try{let c=this.preprocessOptionalChaining(e);c=this.preprocessTranslationTokens(c);const d=this.extractVariablesFromExpression(c),f={...n};for(const x of d)x in f||(f[x]=void 0);n._computed&&!f.$computed&&(f.$computed=n._computed);let h=n.$templateId,m=n.$locale;if(typeof window<"u"&&(!h||!m)){const x=window.__templateApp?.getConfig?.();h??(h=x?.templateId),m??(m=x?.locale)}const b=m||"ko";f.$localized=(x,L)=>{if(x==null&&!L)return"";if(typeof x=="string")return x;if(x&&typeof x=="object"&&x[b])return x[b];if(L&&typeof L=="string"){const M=f.$t?f.$t(L):L;if(M&&M!==L)return M}return x&&typeof x=="object"?x.ko||x.en||Object.values(x)[0]||"":x==null?"":String(x)};const S=typeof window<"u"?window.G7Core:void 0;f.$uuid=()=>S?.uuid?S.uuid():typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,x=>{const L=Math.random()*16|0;return(x==="x"?L:L&3|8).toString(16)});const v={templateId:h||"",locale:b};f.$t=x=>{if(!x||typeof x!="string")return"";try{return xa.getInstance().translate(x,v)}catch(L){return Da.warn("$t() translation failed for key:",x,L),x}},f.$get=(x,L,M=void 0)=>{if(x==null)return M;const k=Array.isArray(L)?L:[L];if(k.length===0)return x;let O=x;for(const B of k){if(O==null||B==null)return M;O=O[B]}return O??M};const _=!1,A=kd(c,f);if(i?.isEnabled()){const x=performance.now()-l;i.trackExpressionEval({expression:e,result:this.sanitizeResultForTracking(A),resultType:this.getResultType(A),componentId:a?.componentId,componentName:a?.componentName,propName:a?.propName,fromCache:_,duration:x,method:"evaluateExpression",skipCache:a?.skipCache})}return A}catch(c){throw new Error(`Failed to evaluate expression "${e}": ${c instanceof Error?c.message:String(c)}`)}}getResultType(e){return e===null?"null":e===void 0?"undefined":Array.isArray(e)?"array":typeof e}sanitizeResultForTracking(e){if(e==null||typeof e!="object")return e;try{if(Array.isArray(e))return e.length>10?`[Array(${e.length})]`:e.slice(0,10);const n=Object.keys(e);return n.length>20?`{Object(${n.length} keys)}`:(JSON.stringify(e),e)}catch{return"[Complex Object]"}}preprocessTranslationTokens(e){return e=e.replace(/(['"])(\$t:([a-zA-Z_][a-zA-Z0-9_.\-]*))\1/g,(n,a,i,l)=>`$t('${l}')`),e.replace(/(?<!['"]\s*)(\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*)(?!\s*['"])/g,'"$1"')}preprocessOptionalChaining(e){const n=[];let a=e.replace(/(['"])(?:(?!\1|\\).|\\.)*\1/g,l=>(n.push(l),`__STRING_LITERAL_${n.length-1}__`));const i=[];return a=a.replace(/\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*(?:\|[^'"\s,)]+)?/g,l=>(i.push(l),`__TRANSLATION_TOKEN_${i.length-1}__`)),a=a.replace(/([a-zA-Z_$][a-zA-Z0-9_$]*)\.(?!\?)/g,"$1?."),a=a.replace(/__TRANSLATION_TOKEN_(\d+)__/g,(l,c)=>i[parseInt(c,10)]),a=a.replace(/__STRING_LITERAL_(\d+)__/g,(l,c)=>n[parseInt(c,10)]),a}extractVariablesFromExpression(e){const n=new Set(["true","false","null","undefined","NaN","Infinity","if","else","for","while","do","switch","case","break","continue","return","function","class","const","let","var","new","delete","typeof","instanceof","this","super","import","export","default","try","catch","finally","throw","Math","Date","JSON","Array","Object","String","Number","Boolean","RegExp","Set","Map","WeakSet","WeakMap","Symbol","Promise","BigInt","Error","parseInt","parseFloat","isNaN","isFinite","encodeURI","decodeURI","encodeURIComponent","decodeURIComponent"]),a=/(?<![.\w$])([a-zA-Z_$][a-zA-Z0-9_$]*)/g,i=new Set;let l;for(;(l=a.exec(e))!==null;){const c=l[1];n.has(c)||i.add(c)}return Array.from(i)}isSwitchExpression(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"$switch"in e&&"$cases"in e}resolveSwitch(e,n,a){const i={...a,skipCache:!0};let l;try{l=this.resolveBindings(e.$switch,n,i)?.toString()?.trim()??""}catch(d){Da.warn("resolveSwitch: $switch 키 평가 실패:",e.$switch,d),l=""}let c;if(l&&e.$cases&&l in e.$cases)c=e.$cases[l];else if("$default"in e)c=e.$default;else return;return typeof c=="string"?c.includes("{{")?this.resolveBindings(c,n,i):c:c!==null&&typeof c=="object"?this.resolveObject(c,n,i):c}};$(ra,"PATH_SEGMENT_PATTERN",/([^.[\]]+)|\[(\d+)\]/g),$(ra,"CACHE_EXPIRY",3e4);let Tn=ra;const Dd=new Tn,Pl={mobile:{min:0,max:767},tablet:{min:768,max:1023},desktop:{min:1024,max:1/0},portable:{min:0,max:1023}},Bl=new Map;function Uw(s,e){let n=null;return((...a)=>{n!==null&&clearTimeout(n),n=setTimeout(()=>{s(...a)},e)})}class Pw{constructor(){$(this,"subscribers",new Set);$(this,"currentWidth",1024);$(this,"debouncedHandler",null);$(this,"isInitialized",!1);this.initialize()}initialize(){typeof window>"u"||(this.currentWidth=window.innerWidth,this.isInitialized=!0,this.debouncedHandler=Uw(()=>{this.currentWidth=window.innerWidth,this.notifySubscribers()},150),window.addEventListener("resize",this.debouncedHandler))}notifySubscribers(){this.subscribers.forEach(e=>{e(this.currentWidth)})}subscribe(e){return this.subscribers.add(e),e(this.currentWidth),()=>{this.subscribers.delete(e)}}getWidth(){return this.currentWidth}parseRange(e){if(Bl.has(e))return Bl.get(e)??null;let n=null;if(Pl[e])n={...Pl[e]};else{const a=e.match(/^(-?\d*)-(-?\d*)$/);if(a){const[,i,l]=a,c=i===""?0:parseInt(i,10),d=l===""?1/0:parseInt(l,10);!isNaN(c)&&!isNaN(d)&&c<=d&&(n={min:c,max:d})}}return Bl.set(e,n),n}getMatchingKey(e,n){const a=[];for(const i of Object.keys(e)){const l=this.parseRange(i);l&&n>=l.min&&n<=l.max&&a.push({key:i,range:l,isPreset:!!Pl[i]})}return a.length===0?null:(a.sort((i,l)=>{if(i.isPreset!==l.isPreset)return i.isPreset?1:-1;const c=i.range.max-i.range.min,d=l.range.max-l.range.min;return c-d}),a[0].key)}matches(e){const n=this.parseRange(e);return n?this.currentWidth>=n.min&&this.currentWidth<=n.max:!1}clearSubscribers(){this.subscribers.clear()}destroy(){this.debouncedHandler&&typeof window<"u"&&window.removeEventListener("resize",this.debouncedHandler),this.clearSubscribers(),Bl.clear()}_setWidthForTesting(e){this.currentWidth=e,this.notifySubscribers()}}const mi=new Pw,lo=dt("ConditionEvaluator");function Bw(s,e,n,a){return bd(Rg(s),e,n,{skipCache:!0,onEmpty:()=>{lo.warn(`${a}: 빈 바인딩 \`{{}}\` — undefined 로 해석합니다.`)}})}function zg(s,e,n){if(!s)return!0;try{const a=Ta(s);let i;if(a!==null?i=Bw(a,e,n,"evaluateStringCondition"):i=n.resolveBindings(s,e,{skipCache:!0}),typeof i=="string"){const l=i.toLowerCase().trim();if(l==="false"||l==="0"||l===""||l==="null"||l==="undefined")return!1}return!!i}catch(a){return lo.warn(`evaluateStringCondition: 조건 평가 실패: ${s}`,a),!1}}function ql(s,e,n){if(typeof s=="string")return zg(s,e,n);if("and"in s){if(!Array.isArray(s.and)||s.and.length===0)return lo.warn("evaluateConditionExpression: AND 그룹이 비어있습니다"),!0;for(const a of s.and)if(!ql(a,e,n))return!1;return!0}if("or"in s){if(!Array.isArray(s.or)||s.or.length===0)return lo.warn("evaluateConditionExpression: OR 그룹이 비어있습니다"),!1;for(const a of s.or)if(ql(a,e,n))return!0;return!1}return lo.warn("evaluateConditionExpression: 알 수 없는 조건 형식",s),!1}function Ug(s,e,n){if(!Array.isArray(s)||s.length===0)return{matched:!1,branchIndex:-1};for(let a=0;a<s.length;a++){const i=s[a];if(i.if===void 0)return{matched:!0,branchIndex:a};if(ql(i.if,e,n))return{matched:!0,branchIndex:a}}return{matched:!1,branchIndex:-1}}function qw(s){return typeof s=="string"?!0:s&&typeof s=="object"?"and"in s||"or"in s:!1}function Gw(s){if(!Array.isArray(s)||s.length===0)return!1;const e=s[0];return!e||typeof e!="object"?!1:"if"in e||"then"in e}const nr=dt("RenderHelpers");function Pg(){try{return window.G7Core?.devTools}catch{return}}const Bg=new Tn,Vw=ro,qg=Ta;function Od(s,e,n){let a=s;if(s.startsWith("{{")&&s.endsWith("}}")&&(a=s.slice(2,-2).trim()),qn(a))try{return n.evaluatePipeExpression(a,e,{skipCache:!0})}catch(i){nr.warn(`resolveIterationSource: 파이프 표현식 평가 실패: ${s}`,i);return}if(Vw(a))try{return n.evaluateExpression(a,e)}catch(i){nr.warn(`resolveIterationSource: 표현식 평가 실패: ${s}`,i);return}return n.resolve(a,e,{skipCache:!0})}function Fw(s,e){const n={...s};if(e?.parentDataContext)for(const[a,i]of Object.entries(e.parentDataContext))a in n||(n[a]=i);if(e?.state){const a=n._local||{};n._local={...a,...e.state}}return e?.isolatedContext?.state&&(n._isolated=e.isolatedContext.state),n}function Ld(s,e,n,a){if(!e||!Array.isArray(e)||e.length===0)return s;const i=a?.actionDispatcher??Mm(),l={...s,actions:e},c=i.bindActionsToProps(l,n,a?.componentContext);return delete c.actions,c}function Gg(s,e,n,a){if(typeof s=="boolean")return s;if(!s)return!0;try{const i=qg(s);let l;if(i!==null?l=bd(Rg(i),e,n,{skipCache:!0,onEmpty:()=>{nr.warn(`evaluateIfCondition: 빈 바인딩 \`{{}}\` (컴포넌트: ${a||"unknown"}) — undefined 로 해석합니다.`)}}):l=n.resolveBindings(s,e,{skipCache:!0}),typeof l=="string"){const c=l.toLowerCase().trim();if(c==="false"||c==="0"||c===""||c==="null"||c==="undefined")return!1}return!!l}catch(i){return nr.warn(`evaluateIfCondition: 조건 평가 실패 (컴포넌트: ${a||"unknown"}):`,i),!1}}function as(s,e,n,a){if(s.if!==void 0)return Gg(s.if,e,n,a);if(s.condition!==void 0)return Gg(s.condition,e,n,a);if(s.conditions===void 0)return!0;const i=s.conditions;try{return qw(i)?ql(i,e,n):Gw(i)?Ug(i,e,n).matched:(nr.warn(`evaluateRenderCondition: 알 수 없는 conditions 형식 (컴포넌트: ${a||"unknown"})`),!0)}catch(l){return nr.warn(`evaluateRenderCondition: conditions 평가 실패 (컴포넌트: ${a||"unknown"}):`,l),!1}}function Vg(s){if(!s.responsive)return s;const e=mi.getWidth(),n=mi.getMatchingKey(s.responsive,e);if(!n)return s;const a=s.responsive[n];return{...s,props:{...s.props,...a.props},children:a.children??s.children,text:a.text??s.text,if:a.if??s.if,iteration:a.iteration??s.iteration}}function co(s,e,n,a,i){if(!s||s.length===0)return[];const l=i?.bindingEngine??new Tn,c=i?.translationEngine??xa.getInstance(),d=i?.translationContext??{templateId:"",locale:"ko"},f=Fw(e,i?.componentContext),h=(v,_)=>{if(typeof v=="string")return Hl(v)||zl(v)||!/\$t:[a-zA-Z0-9._-]+/.test(v)?v:c.resolveTranslations(v,d,_);if(Array.isArray(v))return v.map(A=>h(A,_));if(v&&typeof v=="object"){const A={};for(const[x,L]of Object.entries(v))A[x]=h(L,_);return A}return v},m=(v,_)=>{if(typeof v=="string"){if(Hl(v))return vd(v);if(zl(v)){const x=[],L=v.replace(new RegExp(`${ao}([^${pi}]*)${pi}`,"g"),(k,O)=>(x.push(O),`${ka}${x.length-1}${ka}`));let M=L;return/\$t:[a-zA-Z0-9._-]+/.test(L)&&(M=c.resolveTranslations(L,d,_)),M.replace(new RegExp(`${ka}(\\d+)${ka}`,"g"),(k,O)=>x[parseInt(O)])}if(v.startsWith("$t:defer:")){const x="$t:"+v.slice(9);return c.resolveTranslations(x,d,_)}if(v.startsWith("$t:"))return c.resolveTranslations(v,d,_);if(/\$t:[a-zA-Z0-9._-]+/.test(v)){const x=v.trim();if(!(x.startsWith("{")&&x.endsWith("}")||x.startsWith("[")&&x.endsWith("]"))&&!v.includes("{{"))return c.resolveTranslations(v,d,_)}const A=qg(v);if(A!==null){let x=!1,L=A;A.startsWith(tr)&&(x=!0,L=A.slice(tr.length));let M=!1,k=bd(L,_,l,{skipCache:!0,onError:O=>{nr.warn("renderItemChildren: 표현식 평가 실패:",O),M=!0},onEmpty:()=>{nr.warn("renderItemChildren: 빈 바인딩 `{{}}` 은 해석하지 않습니다.")}});return M?void 0:(x||(k=h(k,_)),x&&k!=null?rs(k):k)}if(v.includes("{{")){const x=l.resolveBindings(v,_,{skipCache:!0});return typeof x=="string"&&/\$t:[a-zA-Z0-9._-]+/.test(x)?c.resolveTranslations(x,d,_):x}return v}if(Array.isArray(v))return v.map(A=>m(A,_));if(v&&typeof v=="object"){if(l.isSwitchExpression(v))return l.resolveSwitch(v,_,{skipCache:!0});if(l.isActionDefinition(v))return{...v};const A={};for(const[x,L]of Object.entries(v))A[x]=m(L,_);return A}return v},b=(v,_)=>{if(!v)return{};const A={};for(const[x,L]of Object.entries(v))A[x]=m(L,_);return A},S=(v,_,A)=>{const x=Vg(v),L=x.iteration;if(!L)return[];const M=Od(L.source,A,l);if(!Array.isArray(M))return nr.warn(`renderItemChildren: iteration.source가 배열이 아닙니다: ${L.source}`),[];const k=Pg();if(k?.isEnabled()){const B=`${_}-iteration`;k.trackIteration(B,L.source,L.item_var,L.index_var,M.length)}const O=n[x.name];return O?M.flatMap((B,G)=>{const P={...A,[L.item_var]:B,[`${L.item_var}_index`]:G,...L.index_var?{[L.index_var]:G}:{}},W=as({if:x.if,condition:x.condition,conditions:x.conditions},P,l,x.id);if(x.if&&k?.isEnabled()){const wt=`${_}-iter-${G}-if`;k.trackIfCondition(wt,x.if,W,x.name)}if(!W)return[];const pe=x.id?String(Ra(m(x.id,P))):void 0,Se=`${_}-iter-${G}`,we=i?.getRemountKey?i.getRemountKey(pe,Se):Se,Ue=b(x.props,P),Ve=Ld(Ue,x.actions,P,{actionDispatcher:i?.actionDispatcher,componentContext:i?.componentContext});let qe=null;return x.text!==void 0?qe=m(x.text,P):x.children&&x.children.length>0&&(qe=co(x.children,P,n,we,i)),k?.isEnabled()&&k.trackRender(x.name),[Ye.createElement(O,{key:we,...Ra(Ve)},Ra(qe))]}):(nr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${x.name}`),[])};return s.flatMap((v,_)=>{const A=Vg(v),x=A.id?String(Ra(m(A.id,f))):void 0,L=a?`${a}-${x||_}`:x||`child-${_}`,M=i?.getRemountKey?i.getRemountKey(x,L):L;if(A.iteration)return S(A,M,f);const k=as({if:A.if,condition:A.condition,conditions:A.conditions},f,l,A.id),O=Pg();if(A.if&&O?.isEnabled()){const pe=`${M}-if`;O.trackIfCondition(pe,A.if,k,A.name)}if(!k)return[];const B=n[A.name];if(!B)return nr.warn(`renderItemChildren: 컴포넌트를 찾을 수 없습니다: ${A.name}`),[];const G=b(A.props,f),P=Ld(G,A.actions,f,{actionDispatcher:i?.actionDispatcher,componentContext:i?.componentContext});let W=null;return A.text!==void 0?W=m(A.text,f):A.children&&A.children.length>0&&(W=co(A.children,f,n,M,i)),O?.isEnabled()&&O.trackRender(A.name),[Ye.createElement(B,{key:M,...Ra(P)},Ra(W))]})}function is(s,e,n,a){let i,l;return n instanceof Tn?(i=n,l=a):(i=Bg,l=n),i.resolveBindings(s,e,l)}function Kw(s,e,n,a){const i=n??Bg,l={skipCache:!0,...a};let c;try{c=i.resolveBindings(s.key,e,{skipCache:l.skipCache})?.toString()?.trim()??""}catch(h){nr.warn("resolveClassMap: key 평가 실패:",s.key,h),c=""}let d="";c&&s.variants&&c in s.variants?d=s.variants[c]:s.default&&(d=s.default);const f=[];return s.base?.trim()&&f.push(s.base.trim()),d?.trim()&&f.push(d.trim()),f.join(" ")}function Fg(s,e){return function(){return s.apply(e,arguments)}}const{toString:Ww}=Object.prototype,{getPrototypeOf:Gl}=Object,{iterator:Vl,toStringTag:Kg}=Symbol,Fl=(s=>e=>{const n=Ww.call(e);return s[n]||(s[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),vr=s=>(s=s.toLowerCase(),e=>Fl(e)===s),Kl=s=>e=>typeof e===s,{isArray:ss}=Array,os=Kl("undefined");function uo(s){return s!==null&&!os(s)&&s.constructor!==null&&!os(s.constructor)&&Ln(s.constructor.isBuffer)&&s.constructor.isBuffer(s)}const Wg=vr("ArrayBuffer");function Yw(s){let e;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?e=ArrayBuffer.isView(s):e=s&&s.buffer&&Wg(s.buffer),e}const Xw=Kl("string"),Ln=Kl("function"),Yg=Kl("number"),fo=s=>s!==null&&typeof s=="object",Jw=s=>s===!0||s===!1,Wl=s=>{if(Fl(s)!=="object")return!1;const e=Gl(s);return(e===null||e===Object.prototype||Object.getPrototypeOf(e)===null)&&!(Kg in s)&&!(Vl in s)},Qw=s=>{if(!fo(s)||uo(s))return!1;try{return Object.keys(s).length===0&&Object.getPrototypeOf(s)===Object.prototype}catch{return!1}},Zw=vr("Date"),e0=vr("File"),t0=s=>!!(s&&typeof s.uri<"u"),n0=s=>s&&typeof s.getParts<"u",r0=vr("Blob"),a0=vr("FileList"),i0=s=>fo(s)&&Ln(s.pipe);function s0(){return typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:typeof global<"u"?global:{}}const Xg=s0(),Jg=typeof Xg.FormData<"u"?Xg.FormData:void 0,o0=s=>{if(!s)return!1;if(Jg&&s instanceof Jg)return!0;const e=Gl(s);if(!e||e===Object.prototype||!Ln(s.append))return!1;const n=Fl(s);return n==="formdata"||n==="object"&&Ln(s.toString)&&s.toString()==="[object FormData]"},l0=vr("URLSearchParams"),[c0,u0,d0,f0]=["ReadableStream","Request","Response","Headers"].map(vr),h0=s=>s.trim?s.trim():s.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function ho(s,e,{allOwnKeys:n=!1}={}){if(s===null||typeof s>"u")return;let a,i;if(typeof s!="object"&&(s=[s]),ss(s))for(a=0,i=s.length;a<i;a++)e.call(null,s[a],a,s);else{if(uo(s))return;const l=n?Object.getOwnPropertyNames(s):Object.keys(s),c=l.length;let d;for(a=0;a<c;a++)d=l[a],e.call(null,s[d],d,s)}}function Qg(s,e){if(uo(s))return null;e=e.toLowerCase();const n=Object.keys(s);let a=n.length,i;for(;a-- >0;)if(i=n[a],e===i.toLowerCase())return i;return null}const yi=typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global,Zg=s=>!os(s)&&s!==yi;function Md(...s){const{caseless:e,skipUndefined:n}=Zg(this)&&this||{},a={},i=(l,c)=>{if(c==="__proto__"||c==="constructor"||c==="prototype")return;const d=e&&Qg(a,c)||c,f=$d(a,d)?a[d]:void 0;Wl(f)&&Wl(l)?a[d]=Md(f,l):Wl(l)?a[d]=Md({},l):ss(l)?a[d]=l.slice():(!n||!os(l))&&(a[d]=l)};for(let l=0,c=s.length;l<c;l++)s[l]&&ho(s[l],i);return a}const p0=(s,e,n,{allOwnKeys:a}={})=>(ho(e,(i,l)=>{n&&Ln(i)?Object.defineProperty(s,l,{__proto__:null,value:Fg(i,n),writable:!0,enumerable:!0,configurable:!0}):Object.defineProperty(s,l,{__proto__:null,value:i,writable:!0,enumerable:!0,configurable:!0})},{allOwnKeys:a}),s),g0=s=>(s.charCodeAt(0)===65279&&(s=s.slice(1)),s),m0=(s,e,n,a)=>{s.prototype=Object.create(e.prototype,a),Object.defineProperty(s.prototype,"constructor",{__proto__:null,value:s,writable:!0,enumerable:!1,configurable:!0}),Object.defineProperty(s,"super",{__proto__:null,value:e.prototype}),n&&Object.assign(s.prototype,n)},y0=(s,e,n,a)=>{let i,l,c;const d={};if(e=e||{},s==null)return e;do{for(i=Object.getOwnPropertyNames(s),l=i.length;l-- >0;)c=i[l],(!a||a(c,s,e))&&!d[c]&&(e[c]=s[c],d[c]=!0);s=n!==!1&&Gl(s)}while(s&&(!n||n(s,e))&&s!==Object.prototype);return e},b0=(s,e,n)=>{s=String(s),(n===void 0||n>s.length)&&(n=s.length),n-=e.length;const a=s.indexOf(e,n);return a!==-1&&a===n},v0=s=>{if(!s)return null;if(ss(s))return s;let e=s.length;if(!Yg(e))return null;const n=new Array(e);for(;e-- >0;)n[e]=s[e];return n},S0=(s=>e=>s&&e instanceof s)(typeof Uint8Array<"u"&&Gl(Uint8Array)),w0=(s,e)=>{const a=(s&&s[Vl]).call(s);let i;for(;(i=a.next())&&!i.done;){const l=i.value;e.call(s,l[0],l[1])}},C0=(s,e)=>{let n;const a=[];for(;(n=s.exec(e))!==null;)a.push(n);return a},E0=vr("HTMLFormElement"),_0=s=>s.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,a,i){return a.toUpperCase()+i}),$d=(({hasOwnProperty:s})=>(e,n)=>s.call(e,n))(Object.prototype),A0=vr("RegExp"),em=(s,e)=>{const n=Object.getOwnPropertyDescriptors(s),a={};ho(n,(i,l)=>{let c;(c=e(i,l,s))!==!1&&(a[l]=c||i)}),Object.defineProperties(s,a)},x0=s=>{em(s,(e,n)=>{if(Ln(s)&&["arguments","caller","callee"].includes(n))return!1;const a=s[n];if(Ln(a)){if(e.enumerable=!1,"writable"in e){e.writable=!1;return}e.set||(e.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},T0=(s,e)=>{const n={},a=i=>{i.forEach(l=>{n[l]=!0})};return ss(s)?a(s):a(String(s).split(e)),n},k0=()=>{},R0=(s,e)=>s!=null&&Number.isFinite(s=+s)?s:e;function D0(s){return!!(s&&Ln(s.append)&&s[Kg]==="FormData"&&s[Vl])}const O0=s=>{const e=new WeakSet,n=a=>{if(fo(a)){if(e.has(a))return;if(uo(a))return a;if(!("toJSON"in a)){e.add(a);const i=ss(a)?[]:{};return ho(a,(l,c)=>{const d=n(l);!os(d)&&(i[c]=d)}),e.delete(a),i}}return a};return n(s)},L0=vr("AsyncFunction"),M0=s=>s&&(fo(s)||Ln(s))&&Ln(s.then)&&Ln(s.catch),tm=((s,e)=>s?setImmediate:e?((n,a)=>(yi.addEventListener("message",({source:i,data:l})=>{i===yi&&l===n&&a.length&&a.shift()()},!1),i=>{a.push(i),yi.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",Ln(yi.postMessage)),$0=typeof queueMicrotask<"u"?queueMicrotask.bind(yi):typeof process<"u"&&process.nextTick||tm,K={isArray:ss,isArrayBuffer:Wg,isBuffer:uo,isFormData:o0,isArrayBufferView:Yw,isString:Xw,isNumber:Yg,isBoolean:Jw,isObject:fo,isPlainObject:Wl,isEmptyObject:Qw,isReadableStream:c0,isRequest:u0,isResponse:d0,isHeaders:f0,isUndefined:os,isDate:Zw,isFile:e0,isReactNativeBlob:t0,isReactNative:n0,isBlob:r0,isRegExp:A0,isFunction:Ln,isStream:i0,isURLSearchParams:l0,isTypedArray:S0,isFileList:a0,forEach:ho,merge:Md,extend:p0,trim:h0,stripBOM:g0,inherits:m0,toFlatObject:y0,kindOf:Fl,kindOfTest:vr,endsWith:b0,toArray:v0,forEachEntry:w0,matchAll:C0,isHTMLForm:E0,hasOwnProperty:$d,hasOwnProp:$d,reduceDescriptors:em,freezeMethods:x0,toObjectSet:T0,toCamelCase:_0,noop:k0,toFiniteNumber:R0,findKey:Qg,global:yi,isContextDefined:Zg,isSpecCompliantForm:D0,toJSONObject:O0,isAsyncFn:L0,isThenable:M0,setImmediate:tm,asap:$0,isIterable:s=>s!=null&&Ln(s[Vl])},N0=K.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),I0=s=>{const e={};let n,a,i;return s&&s.split(` +`).forEach(function(c){i=c.indexOf(":"),n=c.substring(0,i).trim().toLowerCase(),a=c.substring(i+1).trim(),!(!n||e[n]&&N0[n])&&(n==="set-cookie"?e[n]?e[n].push(a):e[n]=[a]:e[n]=e[n]?e[n]+", "+a:a)}),e};function j0(s){let e=0,n=s.length;for(;e<n;){const a=s.charCodeAt(e);if(a!==9&&a!==32)break;e+=1}for(;n>e;){const a=s.charCodeAt(n-1);if(a!==9&&a!==32)break;n-=1}return e===0&&n===s.length?s:s.slice(e,n)}const H0=new RegExp("[\\u0000-\\u0008\\u000a-\\u001f\\u007f]+","g"),z0=new RegExp("[^\\u0009\\u0020-\\u007e\\u0080-\\u00ff]+","g");function Nd(s,e){return K.isArray(s)?s.map(n=>Nd(n,e)):j0(String(s).replace(e,""))}const U0=s=>Nd(s,H0),P0=s=>Nd(s,z0);function nm(s){const e=Object.create(null);return K.forEach(s.toJSON(),(n,a)=>{e[a]=P0(n)}),e}const rm=Symbol("internals");function po(s){return s&&String(s).trim().toLowerCase()}function Yl(s){return s===!1||s==null?s:K.isArray(s)?s.map(Yl):U0(String(s))}function B0(s){const e=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let a;for(;a=n.exec(s);)e[a[1]]=a[2];return e}const q0=s=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(s.trim());function Id(s,e,n,a,i){if(K.isFunction(a))return a.call(this,e,n);if(i&&(e=n),!!K.isString(e)){if(K.isString(a))return e.indexOf(a)!==-1;if(K.isRegExp(a))return a.test(e)}}function G0(s){return s.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(e,n,a)=>n.toUpperCase()+a)}function V0(s,e){const n=K.toCamelCase(" "+e);["get","set","has"].forEach(a=>{Object.defineProperty(s,a+n,{__proto__:null,value:function(i,l,c){return this[a].call(this,e,i,l,c)},configurable:!0})})}let kn=class{constructor(e){e&&this.set(e)}set(e,n,a){const i=this;function l(d,f,h){const m=po(f);if(!m)throw new Error("header name must be a non-empty string");const b=K.findKey(i,m);(!b||i[b]===void 0||h===!0||h===void 0&&i[b]!==!1)&&(i[b||f]=Yl(d))}const c=(d,f)=>K.forEach(d,(h,m)=>l(h,m,f));if(K.isPlainObject(e)||e instanceof this.constructor)c(e,n);else if(K.isString(e)&&(e=e.trim())&&!q0(e))c(I0(e),n);else if(K.isObject(e)&&K.isIterable(e)){let d={},f,h;for(const m of e){if(!K.isArray(m))throw TypeError("Object iterator must return a key-value pair");d[h=m[0]]=(f=d[h])?K.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=po(e),e){const a=K.findKey(this,e);if(a){const i=this[a];if(!n)return i;if(n===!0)return B0(i);if(K.isFunction(n))return n.call(this,i,a);if(K.isRegExp(n))return n.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(e,n){if(e=po(e),e){const a=K.findKey(this,e);return!!(a&&this[a]!==void 0&&(!n||Id(this,this[a],a,n)))}return!1}delete(e,n){const a=this;let i=!1;function l(c){if(c=po(c),c){const d=K.findKey(a,c);d&&(!n||Id(a,a[d],d,n))&&(delete a[d],i=!0)}}return K.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||Id(this,this[l],l,e,!0))&&(delete this[l],i=!0)}return i}normalize(e){const n=this,a={};return K.forEach(this,(i,l)=>{const c=K.findKey(a,l);if(c){n[c]=Yl(i),delete n[l];return}const d=e?G0(l):String(l).trim();d!==l&&delete n[l],n[d]=Yl(i),a[d]=!0}),this}concat(...e){return this.constructor.concat(this,...e)}toJSON(e){const n=Object.create(null);return K.forEach(this,(a,i)=>{a!=null&&a!==!1&&(n[i]=e&&K.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[rm]=this[rm]={accessors:{}}).accessors,i=this.prototype;function l(c){const d=po(c);a[d]||(V0(i,c),a[d]=!0)}return K.isArray(e)?e.forEach(l):l(e),this}};kn.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]),K.reduceDescriptors(kn.prototype,({value:s},e)=>{let n=e[0].toUpperCase()+e.slice(1);return{get:()=>s,set(a){this[n]=a}}}),K.freezeMethods(kn);const F0="[REDACTED ****]";function K0(s){if(K.hasOwnProp(s,"toJSON"))return!0;let e=Object.getPrototypeOf(s);for(;e&&e!==Object.prototype;){if(K.hasOwnProp(e,"toJSON"))return!0;e=Object.getPrototypeOf(e)}return!1}function W0(s,e){const n=new Set(e.map(l=>String(l).toLowerCase())),a=[],i=l=>{if(l===null||typeof l!="object"||K.isBuffer(l))return l;if(a.indexOf(l)!==-1)return;l instanceof kn&&(l=l.toJSON()),a.push(l);let c;if(K.isArray(l))c=[],l.forEach((d,f)=>{const h=i(d);K.isUndefined(h)||(c[f]=h)});else{if(!K.isPlainObject(l)&&K0(l))return a.pop(),l;c=Object.create(null);for(const[d,f]of Object.entries(l)){const h=n.has(d.toLowerCase())?F0:i(f);K.isUndefined(h)||(c[d]=h)}}return a.pop(),c};return i(s)}let Oe=class lw extends Error{static from(e,n,a,i,l,c){const d=new lw(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&&K.hasOwnProp(e,"redact")?e.redact:void 0,a=K.isArray(n)&&n.length>0?W0(e,n):K.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 Y0=null;function jd(s){return K.isPlainObject(s)||K.isArray(s)}function am(s){return K.endsWith(s,"[]")?s.slice(0,-2):s}function Hd(s,e,n){return s?s.concat(e).map(function(i,l){return i=am(i),!n&&l?"["+i+"]":i}).join(n?".":""):e}function X0(s){return K.isArray(s)&&!s.some(jd)}const J0=K.toFlatObject(K,{},null,function(e){return/^is[A-Z]/.test(e)});function Xl(s,e,n){if(!K.isObject(s))throw new TypeError("target must be an object");e=e||new FormData,n=K.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(x,L){return!K.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,h=d&&K.isSpecCompliantForm(e);if(!K.isFunction(i))throw new TypeError("visitor must be a function");function m(A){if(A===null)return"";if(K.isDate(A))return A.toISOString();if(K.isBoolean(A))return A.toString();if(!h&&K.isBlob(A))throw new Oe("Blob is not supported. Use a Buffer instead.");return K.isArrayBuffer(A)||K.isTypedArray(A)?h&&typeof Blob=="function"?new Blob([A]):Buffer.from(A):A}function b(A,x,L){let M=A;if(K.isReactNative(e)&&K.isReactNativeBlob(A))return e.append(Hd(L,x,l),m(A)),!1;if(A&&!L&&typeof A=="object"){if(K.endsWith(x,"{}"))x=a?x:x.slice(0,-2),A=JSON.stringify(A);else if(K.isArray(A)&&X0(A)||(K.isFileList(A)||K.endsWith(x,"[]"))&&(M=K.toArray(A)))return x=am(x),M.forEach(function(O,B){!(K.isUndefined(O)||O===null)&&e.append(c===!0?Hd([x],B,l):c===null?x:x+"[]",m(O))}),!1}return jd(A)?!0:(e.append(Hd(L,x,l),m(A)),!1)}const S=[],v=Object.assign(J0,{defaultVisitor:b,convertValue:m,isVisitable:jd});function _(A,x,L=0){if(!K.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),K.forEach(A,function(k,O){(!(K.isUndefined(k)||k===null)&&i.call(e,k,K.isString(O)?O.trim():O,x,v))===!0&&_(k,x?x.concat(O):[O],L+1)}),S.pop()}}if(!K.isObject(s))throw new TypeError("data must be an object");return _(s),e}function im(s){const e={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"};return encodeURIComponent(s).replace(/[!'()~]|%20/g,function(a){return e[a]})}function zd(s,e){this._pairs=[],s&&Xl(s,this,e)}const sm=zd.prototype;sm.append=function(e,n){this._pairs.push([e,n])},sm.toString=function(e){const n=e?function(a){return e.call(this,a,im)}:im;return this._pairs.map(function(i){return n(i[0])+"="+n(i[1])},"").join("&")};function Q0(s){return encodeURIComponent(s).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+")}function om(s,e,n){if(!e)return s;const a=n&&n.encode||Q0,i=K.isFunction(n)?{serialize:n}:n,l=i&&i.serialize;let c;if(l?c=l(e,i):c=K.isURLSearchParams(e)?e.toString():new zd(e,i).toString(a),c){const d=s.indexOf("#");d!==-1&&(s=s.slice(0,d)),s+=(s.indexOf("?")===-1?"?":"&")+c}return s}class lm{constructor(){this.handlers=[]}use(e,n,a){return this.handlers.push({fulfilled:e,rejected:n,synchronous:a?a.synchronous:!1,runWhen:a?a.runWhen:null}),this.handlers.length-1}eject(e){this.handlers[e]&&(this.handlers[e]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(e){K.forEach(this.handlers,function(a){a!==null&&e(a)})}}const Ud={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1,legacyInterceptorReqResOrdering:!0},Z0={isBrowser:!0,classes:{URLSearchParams:typeof URLSearchParams<"u"?URLSearchParams:zd,FormData:typeof FormData<"u"?FormData:null,Blob:typeof Blob<"u"?Blob:null},protocols:["http","https","file","blob","url","data"]},Pd=typeof window<"u"&&typeof document<"u",Bd=typeof navigator=="object"&&navigator||void 0,eC=Pd&&(!Bd||["ReactNative","NativeScript","NS"].indexOf(Bd.product)<0),tC=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function",nC=Pd&&window.location.href||"http://localhost",En={...Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Pd,hasStandardBrowserEnv:eC,hasStandardBrowserWebWorkerEnv:tC,navigator:Bd,origin:nC},Symbol.toStringTag,{value:"Module"})),...Z0};function rC(s,e){return Xl(s,new En.classes.URLSearchParams,{visitor:function(n,a,i,l){return En.isNode&&K.isBuffer(n)?(this.append(a,n.toString("base64")),!1):l.defaultVisitor.apply(this,arguments)},...e})}function aC(s){return K.matchAll(/\w+|\[(\w*)]/g,s).map(e=>e[0]==="[]"?"":e[1]||e[0])}function iC(s){const e={},n=Object.keys(s);let a;const i=n.length;let l;for(a=0;a<i;a++)l=n[a],e[l]=s[l];return e}function cm(s){function e(n,a,i,l){let c=n[l++];if(c==="__proto__")return!0;const d=Number.isFinite(+c),f=l>=n.length;return c=!c&&K.isArray(i)?i.length:c,f?(K.hasOwnProp(i,c)?i[c]=K.isArray(i[c])?i[c].concat(a):[i[c],a]:i[c]=a,!d):((!K.hasOwnProp(i,c)||!K.isObject(i[c]))&&(i[c]=[]),e(n,a,i[c],l)&&K.isArray(i[c])&&(i[c]=iC(i[c])),!d)}if(K.isFormData(s)&&K.isFunction(s.entries)){const n={};return K.forEachEntry(s,(a,i)=>{e(aC(a),i,n,0)}),n}return null}const ls=(s,e)=>s!=null&&K.hasOwnProp(s,e)?s[e]:void 0;function sC(s,e,n){if(K.isString(s))try{return(e||JSON.parse)(s),K.trim(s)}catch(a){if(a.name!=="SyntaxError")throw a}return(n||JSON.stringify)(s)}const go={transitional:Ud,adapter:["xhr","http","fetch"],transformRequest:[function(e,n){const a=n.getContentType()||"",i=a.indexOf("application/json")>-1,l=K.isObject(e);if(l&&K.isHTMLForm(e)&&(e=new FormData(e)),K.isFormData(e))return i?JSON.stringify(cm(e)):e;if(K.isArrayBuffer(e)||K.isBuffer(e)||K.isStream(e)||K.isFile(e)||K.isBlob(e)||K.isReadableStream(e))return e;if(K.isArrayBufferView(e))return e.buffer;if(K.isURLSearchParams(e))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),e.toString();let d;if(l){const f=ls(this,"formSerializer");if(a.indexOf("application/x-www-form-urlencoded")>-1)return rC(e,f).toString();if((d=K.isFileList(e))||a.indexOf("multipart/form-data")>-1){const h=ls(this,"env"),m=h&&h.FormData;return Xl(d?{"files[]":e}:e,m&&new m,f)}}return l||i?(n.setContentType("application/json",!1),sC(e)):e}],transformResponse:[function(e){const n=ls(this,"transitional")||go.transitional,a=n&&n.forcedJSONParsing,i=ls(this,"responseType"),l=i==="json";if(K.isResponse(e)||K.isReadableStream(e))return e;if(e&&K.isString(e)&&(a&&!i||l)){const d=!(n&&n.silentJSONParsing)&&l;try{return JSON.parse(e,ls(this,"parseReviver"))}catch(f){if(d)throw f.name==="SyntaxError"?Oe.from(f,Oe.ERR_BAD_RESPONSE,this,null,ls(this,"response")):f}}return e}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:En.classes.FormData,Blob:En.classes.Blob},validateStatus:function(e){return e>=200&&e<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};K.forEach(["delete","get","head","post","put","patch","query"],s=>{go.headers[s]={}});function qd(s,e){const n=this||go,a=e||n,i=kn.from(a.headers);let l=a.data;return K.forEach(s,function(d){l=d.call(n,l,i.normalize(),e?e.status:void 0)}),i.normalize(),l}function um(s){return!!(s&&s.__CANCEL__)}let mo=class extends Oe{constructor(e,n,a){super(e??"canceled",Oe.ERR_CANCELED,n,a),this.name="CanceledError",this.__CANCEL__=!0}};function dm(s,e,n){const a=n.config.validateStatus;!n.status||!a||a(n.status)?s(n):e(new Oe("Request failed with status code "+n.status,n.status>=400&&n.status<500?Oe.ERR_BAD_REQUEST:Oe.ERR_BAD_RESPONSE,n.config,n.request,n))}function oC(s){const e=/^([-+\w]{1,25}):(?:\/\/)?/.exec(s);return e&&e[1]||""}function lC(s,e){s=s||10;const n=new Array(s),a=new Array(s);let i=0,l=0,c;return e=e!==void 0?e:1e3,function(f){const h=Date.now(),m=a[l];c||(c=h),n[i]=f,a[i]=h;let b=l,S=0;for(;b!==i;)S+=n[b++],b=b%s;if(i=(i+1)%s,i===l&&(l=(l+1)%s),h-c<e)return;const v=m&&h-m;return v?Math.round(S*1e3/v):void 0}}function cC(s,e){let n=0,a=1e3/e,i,l;const c=(h,m=Date.now())=>{n=m,i=null,l&&(clearTimeout(l),l=null),s(...h)};return[(...h)=>{const m=Date.now(),b=m-n;b>=a?c(h,m):(i=h,l||(l=setTimeout(()=>{l=null,c(i)},a-b)))},()=>i&&c(i)]}const Jl=(s,e,n=3)=>{let a=0;const i=lC(50,250);return cC(l=>{if(!l||typeof l.loaded!="number")return;const c=l.loaded,d=l.lengthComputable?l.total:void 0,f=d!=null?Math.min(c,d):c,h=Math.max(0,f-a),m=i(h);a=Math.max(a,f);const b={loaded:f,total:d,progress:d?f/d:void 0,bytes:h,rate:m||void 0,estimated:m&&d?(d-f)/m:void 0,event:l,lengthComputable:d!=null,[e?"download":"upload"]:!0};s(b)},n)},fm=(s,e)=>{const n=s!=null;return[a=>e[0]({lengthComputable:n,total:s,loaded:a}),e[1]]},hm=s=>(...e)=>K.asap(()=>s(...e)),uC=En.hasStandardBrowserEnv?((s,e)=>n=>(n=new URL(n,En.origin),s.protocol===n.protocol&&s.host===n.host&&(e||s.port===n.port)))(new URL(En.origin),En.navigator&&/(msie|trident)/i.test(En.navigator.userAgent)):()=>!0,dC=En.hasStandardBrowserEnv?{write(s,e,n,a,i,l,c){if(typeof document>"u")return;const d=[`${s}=${encodeURIComponent(e)}`];K.isNumber(n)&&d.push(`expires=${new Date(n).toUTCString()}`),K.isString(a)&&d.push(`path=${a}`),K.isString(i)&&d.push(`domain=${i}`),l===!0&&d.push("secure"),K.isString(c)&&d.push(`SameSite=${c}`),document.cookie=d.join("; ")},read(s){if(typeof document>"u")return null;const e=document.cookie.split(";");for(let n=0;n<e.length;n++){const a=e[n].replace(/^\s+/,""),i=a.indexOf("=");if(i!==-1&&a.slice(0,i)===s)return decodeURIComponent(a.slice(i+1))}return null},remove(s){this.write(s,"",Date.now()-864e5,"/")}}:{write(){},read(){return null},remove(){}};function fC(s){return typeof s!="string"?!1:/^([a-z][a-z\d+\-.]*:)?\/\//i.test(s)}function hC(s,e){return e?s.replace(/\/?\/$/,"")+"/"+e.replace(/^\/+/,""):s}function pm(s,e,n){let a=!fC(e);return s&&(a||n===!1)?hC(s,e):e}const gm=s=>s instanceof kn?{...s}:s;function bi(s,e){e=e||{};const n=Object.create(null);Object.defineProperty(n,"hasOwnProperty",{__proto__:null,value:Object.prototype.hasOwnProperty,enumerable:!1,writable:!0,configurable:!0});function a(h,m,b,S){return K.isPlainObject(h)&&K.isPlainObject(m)?K.merge.call({caseless:S},h,m):K.isPlainObject(m)?K.merge({},m):K.isArray(m)?m.slice():m}function i(h,m,b,S){if(K.isUndefined(m)){if(!K.isUndefined(h))return a(void 0,h,b,S)}else return a(h,m,b,S)}function l(h,m){if(!K.isUndefined(m))return a(void 0,m)}function c(h,m){if(K.isUndefined(m)){if(!K.isUndefined(h))return a(void 0,h)}else return a(void 0,m)}function d(h,m,b){if(K.hasOwnProp(e,b))return a(h,m);if(K.hasOwnProp(s,b))return a(void 0,h)}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,allowedSocketPaths:c,responseEncoding:c,validateStatus:d,headers:(h,m,b)=>i(gm(h),gm(m),b,!0)};return K.forEach(Object.keys({...s,...e}),function(m){if(m==="__proto__"||m==="constructor"||m==="prototype")return;const b=K.hasOwnProp(f,m)?f[m]:i,S=K.hasOwnProp(s,m)?s[m]:void 0,v=K.hasOwnProp(e,m)?e[m]:void 0,_=b(S,v,m);K.isUndefined(_)&&b!==d||(n[m]=_)}),n}const pC=["content-type","content-length"];function gC(s,e,n){if(n!=="content-only"){s.set(e);return}Object.entries(e).forEach(([a,i])=>{pC.includes(a.toLowerCase())&&s.set(a,i)})}const mC=s=>encodeURIComponent(s).replace(/%([0-9A-F]{2})/gi,(e,n)=>String.fromCharCode(parseInt(n,16))),mm=s=>{const e=bi({},s),n=S=>K.hasOwnProp(e,S)?e[S]:void 0,a=n("data");let i=n("withXSRFToken");const l=n("xsrfHeaderName"),c=n("xsrfCookieName");let d=n("headers");const f=n("auth"),h=n("baseURL"),m=n("allowAbsoluteUrls"),b=n("url");if(e.headers=d=kn.from(d),e.url=om(pm(h,b,m),s.params,s.paramsSerializer),f&&d.set("Authorization","Basic "+btoa((f.username||"")+":"+(f.password?mC(f.password):""))),K.isFormData(a)&&(En.hasStandardBrowserEnv||En.hasStandardBrowserWebWorkerEnv?d.setContentType(void 0):K.isFunction(a.getHeaders)&&gC(d,a.getHeaders(),n("formDataHeaderPolicy"))),En.hasStandardBrowserEnv&&(K.isFunction(i)&&(i=i(e)),i===!0||i==null&&uC(e.url))){const v=l&&c&&dC.read(c);v&&d.set(l,v)}return e},yC=typeof XMLHttpRequest<"u"&&function(s){return new Promise(function(n,a){const i=mm(s);let l=i.data;const c=kn.from(i.headers).normalize();let{responseType:d,onUploadProgress:f,onDownloadProgress:h}=i,m,b,S,v,_;function A(){v&&v(),_&&_(),i.cancelToken&&i.cancelToken.unsubscribe(m),i.signal&&i.signal.removeEventListener("abort",m)}let x=new XMLHttpRequest;x.open(i.method.toUpperCase(),i.url,!0),x.timeout=i.timeout;function L(){if(!x)return;const k=kn.from("getAllResponseHeaders"in x&&x.getAllResponseHeaders()),B={data:!d||d==="text"||d==="json"?x.responseText:x.response,status:x.status,statusText:x.statusText,headers:k,config:s,request:x};dm(function(P){n(P),A()},function(P){a(P),A()},B),x=null}"onloadend"in x?x.onloadend=L:x.onreadystatechange=function(){!x||x.readyState!==4||x.status===0&&!(x.responseURL&&x.responseURL.startsWith("file:"))||setTimeout(L)},x.onabort=function(){x&&(a(new Oe("Request aborted",Oe.ECONNABORTED,s,x)),A(),x=null)},x.onerror=function(O){const B=O&&O.message?O.message:"Network Error",G=new Oe(B,Oe.ERR_NETWORK,s,x);G.event=O||null,a(G),A(),x=null},x.ontimeout=function(){let O=i.timeout?"timeout of "+i.timeout+"ms exceeded":"timeout exceeded";const B=i.transitional||Ud;i.timeoutErrorMessage&&(O=i.timeoutErrorMessage),a(new Oe(O,B.clarifyTimeoutError?Oe.ETIMEDOUT:Oe.ECONNABORTED,s,x)),A(),x=null},l===void 0&&c.setContentType(null),"setRequestHeader"in x&&K.forEach(nm(c),function(O,B){x.setRequestHeader(B,O)}),K.isUndefined(i.withCredentials)||(x.withCredentials=!!i.withCredentials),d&&d!=="json"&&(x.responseType=i.responseType),h&&([S,_]=Jl(h,!0),x.addEventListener("progress",S)),f&&x.upload&&([b,v]=Jl(f),x.upload.addEventListener("progress",b),x.upload.addEventListener("loadend",v)),(i.cancelToken||i.signal)&&(m=k=>{x&&(a(!k||k.type?new mo(null,s,x):k),x.abort(),A(),x=null)},i.cancelToken&&i.cancelToken.subscribe(m),i.signal&&(i.signal.aborted?m():i.signal.addEventListener("abort",m)));const M=oC(i.url);if(M&&!En.protocols.includes(M)){a(new Oe("Unsupported protocol "+M+":",Oe.ERR_BAD_REQUEST,s));return}x.send(l||null)})},bC=(s,e)=>{if(s=s?s.filter(Boolean):[],!e&&!s.length)return;const n=new AbortController;let a=!1;const i=function(f){if(!a){a=!0,c();const h=f instanceof Error?f:this.reason;n.abort(h instanceof Oe?h:new mo(h instanceof Error?h.message:h))}};let l=e&&setTimeout(()=>{l=null,i(new Oe(`timeout of ${e}ms exceeded`,Oe.ETIMEDOUT))},e);const c=()=>{s&&(l&&clearTimeout(l),l=null,s.forEach(f=>{f.unsubscribe?f.unsubscribe(i):f.removeEventListener("abort",i)}),s=null)};s.forEach(f=>f.addEventListener("abort",i));const{signal:d}=n;return d.unsubscribe=()=>K.asap(c),d},vC=function*(s,e){let n=s.byteLength;if(n<e){yield s;return}let a=0,i;for(;a<n;)i=a+e,yield s.slice(a,i),a=i},SC=async function*(s,e){for await(const n of wC(s))yield*vC(n,e)},wC=async function*(s){if(s[Symbol.asyncIterator]){yield*s;return}const e=s.getReader();try{for(;;){const{done:n,value:a}=await e.read();if(n)break;yield a}}finally{await e.cancel()}},ym=(s,e,n,a)=>{const i=SC(s,e);let l=0,c,d=f=>{c||(c=!0,a&&a(f))};return new ReadableStream({async pull(f){try{const{done:h,value:m}=await i.next();if(h){d(),f.close();return}let b=m.byteLength;if(n){let S=l+=b;n(S)}f.enqueue(new Uint8Array(m))}catch(h){throw d(h),h}},cancel(f){return d(f),i.return()}},{highWaterMark:2})};function CC(s){if(!s||typeof s!="string"||!s.startsWith("data:"))return 0;const e=s.indexOf(",");if(e<0)return 0;const n=s.slice(5,e),a=s.slice(e+1);if(/;base64/i.test(n)){let c=a.length;const d=a.length;for(let v=0;v<d;v++)if(a.charCodeAt(v)===37&&v+2<d){const _=a.charCodeAt(v+1),A=a.charCodeAt(v+2);(_>=48&&_<=57||_>=65&&_<=70||_>=97&&_<=102)&&(A>=48&&A<=57||A>=65&&A<=70||A>=97&&A<=102)&&(c-=2,v+=2)}let f=0,h=d-1;const m=v=>v>=2&&a.charCodeAt(v-2)===37&&a.charCodeAt(v-1)===51&&(a.charCodeAt(v)===68||a.charCodeAt(v)===100);h>=0&&(a.charCodeAt(h)===61?(f++,h--):m(h)&&(f++,h-=3)),f===1&&h>=0&&(a.charCodeAt(h)===61||m(h))&&f++;const S=Math.floor(c/4)*3-(f||0);return S>0?S:0}if(typeof Buffer<"u"&&typeof Buffer.byteLength=="function")return Buffer.byteLength(a,"utf8");let l=0;for(let c=0,d=a.length;c<d;c++){const f=a.charCodeAt(c);if(f<128)l+=1;else if(f<2048)l+=2;else if(f>=55296&&f<=56319&&c+1<d){const h=a.charCodeAt(c+1);h>=56320&&h<=57343?(l+=4,c++):l+=3}else l+=3}return l}const Gd="1.16.1",bm=64*1024,{isFunction:Ql}=K,vm=(s,...e)=>{try{return!!s(...e)}catch{return!1}},EC=s=>{const e=K.global!==void 0&&K.global!==null?K.global:globalThis,{ReadableStream:n,TextEncoder:a}=e;s=K.merge.call({skipUndefined:!0},{Request:e.Request,Response:e.Response},s);const{fetch:i,Request:l,Response:c}=s,d=i?Ql(i):typeof fetch=="function",f=Ql(l),h=Ql(c);if(!d)return!1;const m=d&&Ql(n),b=d&&(typeof a=="function"?(L=>M=>L.encode(M))(new a):async L=>new Uint8Array(await new l(L).arrayBuffer())),S=f&&m&&vm(()=>{let L=!1;const M=new l(En.origin,{body:new n,method:"POST",get duplex(){return L=!0,"half"}}),k=M.headers.has("Content-Type");return M.body!=null&&M.body.cancel(),L&&!k}),v=h&&m&&vm(()=>K.isReadableStream(new c("").body)),_={stream:v&&(L=>L.body)};d&&["text","arrayBuffer","blob","formData","stream"].forEach(L=>{!_[L]&&(_[L]=(M,k)=>{let O=M&&M[L];if(O)return O.call(M);throw new Oe(`Response type '${L}' is not supported`,Oe.ERR_NOT_SUPPORT,k)})});const A=async L=>{if(L==null)return 0;if(K.isBlob(L))return L.size;if(K.isSpecCompliantForm(L))return(await new l(En.origin,{method:"POST",body:L}).arrayBuffer()).byteLength;if(K.isArrayBufferView(L)||K.isArrayBuffer(L))return L.byteLength;if(K.isURLSearchParams(L)&&(L=L+""),K.isString(L))return(await b(L)).byteLength},x=async(L,M)=>{const k=K.toFiniteNumber(L.getContentLength());return k??A(M)};return async L=>{let{url:M,method:k,data:O,signal:B,cancelToken:G,timeout:P,onDownloadProgress:W,onUploadProgress:pe,responseType:Se,headers:we,withCredentials:Ue="same-origin",fetchOptions:Ve,maxContentLength:qe,maxBodyLength:wt}=mm(L);const J=K.isNumber(qe)&&qe>-1,fe=K.isNumber(wt)&&wt>-1;let Le=i||fetch;Se=Se?(Se+"").toLowerCase():"text";let Z=bC([B,G&&G.toAbortSignal()],P),ge=null;const H=Z&&Z.unsubscribe&&(()=>{Z.unsubscribe()});let T;try{if(J&&typeof M=="string"&&M.startsWith("data:")&&CC(M)>qe)throw new Oe("maxContentLength size of "+qe+" exceeded",Oe.ERR_BAD_RESPONSE,L,ge);if(fe&&k!=="get"&&k!=="head"){const _e=await x(we,O);if(typeof _e=="number"&&isFinite(_e)&&_e>wt)throw new Oe("Request body larger than maxBodyLength limit",Oe.ERR_BAD_REQUEST,L,ge)}if(pe&&S&&k!=="get"&&k!=="head"&&(T=await x(we,O))!==0){let _e=new l(M,{method:"POST",body:O,duplex:"half"}),Te;if(K.isFormData(O)&&(Te=_e.headers.get("content-type"))&&we.setContentType(Te),_e.body){const[$e,pt]=fm(T,Jl(hm(pe)));O=ym(_e.body,bm,$e,pt)}}K.isString(Ue)||(Ue=Ue?"include":"omit");const ce=f&&"credentials"in l.prototype;if(K.isFormData(O)){const _e=we.getContentType();_e&&/^multipart\/form-data/i.test(_e)&&!/boundary=/i.test(_e)&&we.delete("content-type")}we.set("User-Agent","axios/"+Gd,!1);const de={...Ve,signal:Z,method:k.toUpperCase(),headers:nm(we.normalize()),body:O,duplex:"half",credentials:ce?Ue:void 0};ge=f&&new l(M,de);let ye=await(f?Le(ge,Ve):Le(M,de));if(J){const _e=K.toFiniteNumber(ye.headers.get("content-length"));if(_e!=null&&_e>qe)throw new Oe("maxContentLength size of "+qe+" exceeded",Oe.ERR_BAD_RESPONSE,L,ge)}const ie=v&&(Se==="stream"||Se==="response");if(v&&ye.body&&(W||J||ie&&H)){const _e={};["status","statusText","headers"].forEach(ht=>{_e[ht]=ye[ht]});const Te=K.toFiniteNumber(ye.headers.get("content-length")),[$e,pt]=W&&fm(Te,Jl(hm(W),!0))||[];let Ct=0;const Dt=ht=>{if(J&&(Ct=ht,Ct>qe))throw new Oe("maxContentLength size of "+qe+" exceeded",Oe.ERR_BAD_RESPONSE,L,ge);$e&&$e(ht)};ye=new c(ym(ye.body,bm,Dt,()=>{pt&&pt(),H&&H()}),_e)}Se=Se||"text";let xe=await _[K.findKey(_,Se)||"text"](ye,L);if(J&&!v&&!ie){let _e;if(xe!=null&&(typeof xe.byteLength=="number"?_e=xe.byteLength:typeof xe.size=="number"?_e=xe.size:typeof xe=="string"&&(_e=typeof a=="function"?new a().encode(xe).byteLength:xe.length)),typeof _e=="number"&&_e>qe)throw new Oe("maxContentLength size of "+qe+" exceeded",Oe.ERR_BAD_RESPONSE,L,ge)}return!ie&&H&&H(),await new Promise((_e,Te)=>{dm(_e,Te,{data:xe,headers:kn.from(ye.headers),status:ye.status,statusText:ye.statusText,config:L,request:ge})})}catch(ce){if(H&&H(),Z&&Z.aborted&&Z.reason instanceof Oe){const de=Z.reason;throw de.config=L,ge&&(de.request=ge),ce!==de&&(de.cause=ce),de}throw ce&&ce.name==="TypeError"&&/Load failed|fetch/i.test(ce.message)?Object.assign(new Oe("Network Error",Oe.ERR_NETWORK,L,ge,ce&&ce.response),{cause:ce.cause||ce}):Oe.from(ce,ce&&ce.code,L,ge,ce&&ce.response)}}},_C=new Map,Sm=s=>{let e=s&&s.env||{};const{fetch:n,Request:a,Response:i}=e,l=[a,i,n];let c=l.length,d=c,f,h,m=_C;for(;d--;)f=l[d],h=m.get(f),h===void 0&&m.set(f,h=d?new Map:EC(e)),m=h;return h};Sm();const Vd={http:Y0,xhr:yC,fetch:{get:Sm}};K.forEach(Vd,(s,e)=>{if(s){try{Object.defineProperty(s,"name",{__proto__:null,value:e})}catch{}Object.defineProperty(s,"adapterName",{__proto__:null,value:e})}});const wm=s=>`- ${s}`,AC=s=>K.isFunction(s)||s===null||s===!1;function xC(s,e){s=K.isArray(s)?s:[s];const{length:n}=s;let a,i;const l={};for(let c=0;c<n;c++){a=s[c];let d;if(i=a,!AC(a)&&(i=Vd[(d=String(a)).toLowerCase()],i===void 0))throw new Oe(`Unknown adapter '${d}'`);if(i&&(K.isFunction(i)||(i=i.get(e))))break;l[d||"#"+c]=i}if(!i){const c=Object.entries(l).map(([f,h])=>`adapter ${f} `+(h===!1?"is not supported by the environment":"is not available in the build"));let d=n?c.length>1?`since : +`+c.map(wm).join(` +`):" "+wm(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 Cm={getAdapter:xC,adapters:Vd};function Fd(s){if(s.cancelToken&&s.cancelToken.throwIfRequested(),s.signal&&s.signal.aborted)throw new mo(null,s)}function Em(s){return Fd(s),s.headers=kn.from(s.headers),s.data=qd.call(s,s.transformRequest),["post","put","patch"].indexOf(s.method)!==-1&&s.headers.setContentType("application/x-www-form-urlencoded",!1),Cm.getAdapter(s.adapter||go.adapter,s)(s).then(function(a){Fd(s),s.response=a;try{a.data=qd.call(s,s.transformResponse,a)}finally{delete s.response}return a.headers=kn.from(a.headers),a},function(a){if(!um(a)&&(Fd(s),a&&a.response)){s.response=a.response;try{a.response.data=qd.call(s,s.transformResponse,a.response)}finally{delete s.response}a.response.headers=kn.from(a.response.headers)}return Promise.reject(a)})}const Zl={};["object","boolean","number","function","string","symbol"].forEach((s,e)=>{Zl[s]=function(a){return typeof a===s||"a"+(e<1?"n ":" ")+s}});const _m={};Zl.transitional=function(e,n,a){function i(l,c){return"[Axios v"+Gd+"] 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&&!_m[c]&&(_m[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}},Zl.spelling=function(e){return(n,a)=>(console.warn(`${a} is likely a misspelling of ${e}`),!0)};function TC(s,e,n){if(typeof s!="object")throw new Oe("options must be an object",Oe.ERR_BAD_OPTION_VALUE);const a=Object.keys(s);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=s[l],f=d===void 0||c(d,l,s);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 ec={assertOptions:TC,validators:Zl},rr=ec.validators;let vi=class{constructor(e){this.defaults=e||{},this.interceptors={request:new lm,response:new lm}}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=bi(this.defaults,n);const{transitional:a,paramsSerializer:i,headers:l}=n;a!==void 0&&ec.assertOptions(a,{silentJSONParsing:rr.transitional(rr.boolean),forcedJSONParsing:rr.transitional(rr.boolean),clarifyTimeoutError:rr.transitional(rr.boolean),legacyInterceptorReqResOrdering:rr.transitional(rr.boolean)},!1),i!=null&&(K.isFunction(i)?n.paramsSerializer={serialize:i}:ec.assertOptions(i,{encode:rr.function,serialize:rr.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),ec.assertOptions(n,{baseUrl:rr.spelling("baseURL"),withXsrfToken:rr.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let c=l&&K.merge(l.common,l[n.method]);l&&K.forEach(["delete","get","head","post","put","patch","query","common"],_=>{delete l[_]}),n.headers=kn.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||Ud;x&&x.legacyInterceptorReqResOrdering?d.unshift(A.fulfilled,A.rejected):d.push(A.fulfilled,A.rejected)});const h=[];this.interceptors.response.forEach(function(A){h.push(A.fulfilled,A.rejected)});let m,b=0,S;if(!f){const _=[Em.bind(this),void 0];for(_.unshift(...d),_.push(...h),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=h.length;b<S;)m=m.then(h[b++],h[b++]);return m}getUri(e){e=bi(this.defaults,e);const n=pm(e.baseURL,e.url,e.allowAbsoluteUrls);return om(n,e.params,e.paramsSerializer)}};K.forEach(["delete","get","head","options"],function(e){vi.prototype[e]=function(n,a){return this.request(bi(a||{},{method:e,url:n,data:(a||{}).data}))}}),K.forEach(["post","put","patch","query"],function(e){function n(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]=n(),e!=="query"&&(vi.prototype[e+"Form"]=n(!0))});let kC=class cw{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 mo(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 cw(function(i){e=i}),cancel:e}}};function RC(s){return function(n){return s.apply(null,n)}}function DC(s){return K.isObject(s)&&s.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,WebServerIsDown:521,ConnectionTimedOut:522,OriginIsUnreachable:523,TimeoutOccurred:524,SslHandshakeFailed:525,InvalidSslCertificate:526};Object.entries(Kd).forEach(([s,e])=>{Kd[e]=s});function Am(s){const e=new vi(s),n=Fg(vi.prototype.request,e);return K.extend(n,vi.prototype,e,{allOwnKeys:!0}),K.extend(n,e,null,{allOwnKeys:!0}),n.create=function(i){return Am(bi(s,i))},n}const Ft=Am(go);Ft.Axios=vi,Ft.CanceledError=mo,Ft.CancelToken=kC,Ft.isCancel=um,Ft.VERSION=Gd,Ft.toFormData=Xl,Ft.AxiosError=Oe,Ft.Cancel=Ft.CanceledError,Ft.all=function(e){return Promise.all(e)},Ft.spread=RC,Ft.isAxiosError=DC,Ft.mergeConfig=bi,Ft.AxiosHeaders=kn,Ft.formToJSON=s=>cm(K.isHTMLForm(s)?new FormData(s):s),Ft.getAdapter=Cm.getAdapter,Ft.HttpStatusCode=Kd,Ft.default=Ft;const{Axios:KT,AxiosError:WT,CanceledError:YT,isCancel:XT,CancelToken:JT,VERSION:QT,all:ZT,Cancel:ek,isAxiosError:tk,spread:nk,toFormData:rk,AxiosHeaders:ak,HttpStatusCode:ik,formToJSON:sk,getAdapter:ok,mergeConfig:lk,create:ck}=Ft,Wd="g7.identity.redirectStash",tc=dt("IdentityGuardInterceptor");class St{static setLauncher(e){this.launcher=e}static hasLauncher(){return this.launcher!==null}static markDomainNoticeShown(){this.domainNoticeShown=!0}static consumeDomainNoticeShown(){const e=this.domainNoticeShown;return this.domainNoticeShown=!1,e}static resolveDeferred(e){const n=this.deferredResolver;if(!n){tc.warn("IdentityGuardInterceptor.resolveDeferred 호출됐지만 대기 중인 launcher 가 없습니다. 모달이 launcher 외부에서 열렸는지 확인하세요.");return}this.deferredResolver=null,n(e)}static createDeferred(){if(this.deferredResolver){const e=this.deferredResolver;this.deferredResolver=null,e({status:"cancelled"})}return new Promise(e=>{this.deferredResolver=e})}static isIdentityRequired(e,n){return e===428&&typeof n=="object"&&n!==null&&n.error_code==="identity_verification_required"}static async handle(e,n,a){this.domainNoticeShown=!1;const i=this.launcher??LC,c=!!(a&&(a.email||a.phone))?{...e.verification,target:a}:e.verification,d=await i(c);if(d.status!=="verified")return null;const f=c.return_request;if(!f)return null;const h=OC(f.url,d.token);return fetch(h,{method:f.method,headers:n?.headers??{Accept:"application/json","Content-Type":"application/json"},body:n?.body,credentials:n?.credentials??"same-origin"})}static redirectExternally(e){const n=e.redirect_url;if(!n)return tc.error("redirectExternally 호출 시 verification.redirect_url 이 없습니다. payload:",e),Promise.resolve({status:"failed",failureCode:"MISSING_REDIRECT_URL",reason:"verification.redirect_url is required for external_redirect flow"});if(typeof window<"u"){const a={return_url:window.location.href,payload:e,stashed_at:Date.now()};try{window.sessionStorage?.setItem(Wd,JSON.stringify(a))}catch(i){tc.warn("sessionStorage 접근 실패 — stash 없이 redirect 합니다.",i)}window.location.href=n}return new Promise(()=>{})}static reset(){if(this.deferredResolver){const e=this.deferredResolver;this.deferredResolver=null;try{e({status:"cancelled"})}catch{}}this.launcher=null,this.domainNoticeShown=!1}}$(St,"launcher",null),$(St,"deferredResolver",null),$(St,"domainNoticeShown",!1);function OC(s,e){if(!e)return s;try{const n=typeof window<"u"?window.location.origin:"http://localhost",a=new URL(s,n);return a.searchParams.set("verification_token",e),/^https?:\/\//i.test(s)?a.toString():`${a.pathname}${a.search}${a.hash}`}catch{const n=s.includes("?")?"&":"?";return`${s}${n}verification_token=${encodeURIComponent(e)}`}}const LC=async s=>{if(typeof window>"u")return{status:"cancelled"};if(s.render_hint==="external_redirect"||s.redirect_url)return St.redirectExternally(s);const e=window.G7Core;if(!e?.dispatch)return console.error("[IdentityGuardInterceptor] defaultLauncher: G7Core 가 초기화되지 않아 본인인증 흐름을 시작할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};try{await e.dispatch({handler:"toast",params:{message:"본인 확인이 필요합니다.",variant:"warning"}})}catch{}const n=window.location.href,a={return_url:n,payload:s,stashed_at:Date.now()};try{window.sessionStorage?.setItem(Wd,JSON.stringify(a))}catch{}const i=`/identity/challenge?return=${encodeURIComponent(n)}`;try{await e.dispatch({handler:"navigate",params:{path:i}})}catch(l){tc.warn("navigate 실패 — window.location 으로 폴백",l),window.location.href=i}return new Promise(()=>{})},MC=dt("ApiClient");function Yd(){return typeof window<"u"&&window.__G7_DEVTOOLS__?window.__G7_DEVTOOLS__:null}class $C{constructor(e={}){$(this,"client");$(this,"config");$(this,"TOKEN_KEY","auth_token");this.config={baseURL:e.baseURL||"/api",timeout:e.timeout||3e4,onTokenExpired:e.onTokenExpired,onUnauthorized:e.onUnauthorized,onError:e.onError},this.client=Ft.create({baseURL:this.config.baseURL,timeout:this.config.timeout,headers:{"Content-Type":"application/json",Accept:"application/json"},paramsSerializer:{serialize:n=>{const a=[];for(const[i,l]of Object.entries(n))if(l!=null)if(Array.isArray(l)){const c=i.endsWith("[]")?i:`${i}[]`;for(const d of l)a.push(`${encodeURIComponent(c)}=${encodeURIComponent(String(d))}`)}else a.push(`${encodeURIComponent(i)}=${encodeURIComponent(String(l))}`);return a.join("&")}}}),this.setupInterceptors()}setToken(e){typeof window<"u"&&localStorage.setItem(this.TOKEN_KEY,e)}getToken(){return typeof window<"u"?localStorage.getItem(this.TOKEN_KEY):null}removeToken(){typeof window<"u"&&localStorage.removeItem(this.TOKEN_KEY)}setupInterceptors(){this.client.interceptors.request.use(e=>{e.url?.startsWith("/api")&&e.baseURL==="/api"&&(e.baseURL="");const n=this.getToken();if(n&&e.headers&&!this.isCrossOriginRequest(e.url)&&(e.headers.Authorization=`Bearer ${n}`),typeof window<"u"&&e.headers){const i=localStorage.getItem("g7_locale");i&&(e.headers["Accept-Language"]=i)}const a=Yd();if(a?.isEnabled()){const i=this.buildFullUrl(e),l=(e.method||"GET").toUpperCase(),c=a.trackRequest(i,l,{requestBody:e.data});e._devToolsRequestId=c}return e},e=>Promise.reject(e)),this.client.interceptors.response.use(e=>{const n=e.config,a=n._devToolsRequestId;if(a){const i=Yd();i?.isEnabled()&&i.completeRequest(a,e.status,e.data),n._devToolsRequestId=null}return e},async e=>{const n=e.config,a=n?.url||"",l=["/auth/","/layouts/"].some(c=>a.includes(c));if(e.response?.status===401&&!n?._retry&&!l){if(n._retry=!0,await Sr.getInstance().refreshToken()){const f=this.getToken();return f&&(n.headers.Authorization=`Bearer ${f}`),n._devToolsRequestId=null,this.client(n)}return this.completeDevToolsRequest(n,e),this.removeToken(),this.config.onUnauthorized&&this.config.onUnauthorized(),Promise.reject(e)}if(e.response?.status===401&&l)return this.completeDevToolsRequest(n,e),Promise.reject(e);if(St.isIdentityRequired(e.response?.status,e.response?.data)){this.completeDevToolsRequest(n,e);const c={},d=n?.headers??{};for(const[S,v]of Object.entries(d))typeof v=="string"&&(c[S]=v);let f;n?.data!==void 0&&n?.data!==null&&(f=typeof n.data=="string"?n.data:JSON.stringify(n.data),c["Content-Type"]=c["Content-Type"]??"application/json");const h=n?.identity_target,m=await St.handle(e.response.data,{headers:c,body:f,credentials:"same-origin"},h);if(!m)return Promise.reject(e);let b=null;try{b=await m.json()}catch{b=null}return m.ok?{...e.response,status:m.status,data:b}:Promise.reject(Object.assign(new Error("replay failed"),{response:{status:m.status,data:b}}))}return e.response?.status===403?(this.completeDevToolsRequest(n,e),this.callOnError(e),Promise.reject(e)):(this.completeDevToolsRequest(n,e),e.response&&this.callOnError(e),Promise.reject(e))})}async get(e,n){return(await this.client.get(e,n)).data}async post(e,n,a){return(await this.client.post(e,n,a)).data}async put(e,n,a){return(await this.client.put(e,n,a)).data}async patch(e,n,a){return(await this.client.patch(e,n,a)).data}async delete(e,n){return(await this.client.delete(e,n)).data}getInstance(){return this.client}setOnUnauthorized(e){this.config.onUnauthorized=e}setOnError(e){this.config.onError=e}completeDevToolsRequest(e,n){if(!e?._devToolsRequestId)return;const a=Yd();if(!a?.isEnabled())return;const i=e._devToolsRequestId;e._devToolsRequestId=null,n.response?a.completeRequest(i,n.response.status,n.response.data):n.request?a.failRequest(i,n.message||"Network error"):a.failRequest(i,n.message||"Request error")}isCrossOriginRequest(e){if(!e||typeof window>"u"||!window.location||!/^(https?:)?\/\//i.test(e))return!1;try{return new URL(e,window.location.href).origin!==window.location.origin}catch{return!1}}buildFullUrl(e){let n=e.url||"";if(e.baseURL&&!n.startsWith("http")&&(n=`${e.baseURL}${n.startsWith("/")?"":"/"}${n}`),e.params&&typeof e.params=="object"){const a=e.params,i=[];for(const[l,c]of Object.entries(a))if(c!=null)if(Array.isArray(c)){const d=l.endsWith("[]")?l:`${l}[]`;for(const f of c)i.push(`${encodeURIComponent(d)}=${encodeURIComponent(String(f))}`)}else i.push(`${encodeURIComponent(l)}=${encodeURIComponent(String(c))}`);i.length>0&&(n+=(n.includes("?")?"&":"?")+i.join("&"))}return n}callOnError(e){if(!this.config.onError)return;const n={status:e.response?.status||0,message:e.response?.data?.message||e.message||"Unknown error",data:e.response?.data,statusText:e.response?.statusText};try{this.config.onError(n)}catch(a){MC.error("Error in onError handler:",a)}}}let Xd=null;function Hr(){return Xd||(Xd=new $C),Xd}const yo=dt("AuthManager");function cs(s,e,n,a){try{window.G7Core?.devTools?.trackAuthEvent?.(s,e,n,a)}catch{}}const nc={admin:{type:"admin",loginPath:"/admin/login",defaultPath:"/admin",userEndpoint:"/admin/auth/user",loginEndpoint:"/auth/admin/login",logoutEndpoint:"/admin/auth/logout",refreshEndpoint:"/admin/auth/refresh"},user:{type:"user",loginPath:"/login",defaultPath:"/",userEndpoint:"/auth/user",loginEndpoint:"/auth/login",logoutEndpoint:"/auth/logout",refreshEndpoint:"/auth/refresh"}},Tr=class Tr{constructor(){$(this,"state");$(this,"config");$(this,"isRefreshing",!1);$(this,"refreshPromise",null);$(this,"eventHandlers",new Map);this.state={isAuthenticated:!1,user:null,type:null},this.config=new Map,this.config.set("admin",nc.admin),this.config.set("user",nc.user)}static getInstance(){return Tr.instance||(Tr.instance=new Tr),Tr.instance}on(e,n){this.eventHandlers.has(e)||this.eventHandlers.set(e,[]),this.eventHandlers.get(e).push(n)}emit(e,...n){const a=this.eventHandlers.get(e);a&&a.forEach(i=>i(...n))}isAuthenticated(){return this.state.isAuthenticated}getUser(){return this.state.user}getAuthType(){return this.state.type}async checkAuth(e){const n=Hr();if(!n.getToken())return this.clearState(),!1;const i=this.config.get(e);if(!i)return yo.error(`Unknown auth type: ${e}`),!1;try{const l=await n.get(i.userEndpoint);return l.success&&l.data?(this.state={isAuthenticated:!0,user:l.data,type:e},this.emit("authStateChange",this.state),!0):(this.clearState(),!1)}catch(l){return l.response?.status===401&&await this.refreshToken()?this.checkAuth(e):(this.clearState(),!1)}}async preloadAuth(e){try{return await this.checkAuth(e)}catch(n){return yo.warn(`Preload auth failed for type ${e}:`,n),!1}}async login(e,n,a){const i=Hr();if(!this.config.get(e))throw new Error(`Unknown auth type: ${e}`);const c=e==="admin"?nc.admin.loginEndpoint:nc.user.loginEndpoint;try{const d=a?.headers?{headers:a.headers}:void 0,f=await i.post(c,n,d);if(f.success&&f.data){i.setToken(f.data.token);const h=f.data.user.language,m=localStorage.getItem(Tr.LOCALE_STORAGE_KEY),b=h&&h!==m;if(h)try{localStorage.setItem(Tr.LOCALE_STORAGE_KEY,h)}catch(S){yo.warn("Failed to save user language to localStorage:",S)}return this.state={isAuthenticated:!0,user:f.data.user,type:e},this.emit("login",this.state),this.emit("authStateChange",this.state),cs("login",!0,void 0,{userId:f.data.user.uuid,email:f.data.user.email,type:e}),b&&window.__templateApp&&window.__templateApp.changeLocale(h),f.data.user}throw new Error("Login failed")}catch(d){this.clearState();const f=d.response?.data?.message,h=new Error(f||d.message||"Login failed");throw h.response=d.response,h.status=d.response?.status,cs("login",!1,h.message,{type:e,status:d.response?.status}),h}}async logout(){const e=Hr(),n=this.state.type?this.config.get(this.state.type):null;try{n&&await e.post(n.logoutEndpoint)}catch(a){yo.warn("Logout API call failed:",a)}finally{e.removeToken();const a={...this.state};if(this.clearState(),this.emit("logout",a),this.emit("authStateChange",this.state),cs("logout",!0,void 0,{previousType:a.type}),n&&a.type){const i=window.location.pathname+window.location.search;window.location.href=this.getLoginRedirectUrl(a.type,i)}}}async refreshToken(){if(this.isRefreshing&&this.refreshPromise)return this.refreshPromise;this.isRefreshing=!0,this.refreshPromise=this.doRefreshToken();try{return await this.refreshPromise}finally{this.isRefreshing=!1,this.refreshPromise=null}}async doRefreshToken(){const e=this.state.type;if(!e)return!1;const n=this.config.get(e);if(!n)return!1;const a=Hr();try{const i=await a.post(n.refreshEndpoint);return i.success&&i.data?.token?(a.setToken(i.data.token),this.emit("tokenRefreshed"),cs("token-refresh",!0,void 0,{type:e}),!0):(cs("token-refresh",!1,"No token in response",{type:e}),!1)}catch(i){return yo.error("Token refresh failed:",i),cs("token-refresh",!1,i instanceof Error?i.message:"Unknown error",{type:e}),!1}}getLoginRedirectUrl(e,n,a){const i=this.config.get(e);if(!i)return"/login";const l=encodeURIComponent(n);let c=`${i.loginPath}?redirect=${l}`;return a&&(c+=`&reason=${encodeURIComponent(a)}`),c}updateConfig(e,n){if(n.loginPath!==void 0){const i=n.loginPath;if(!i.startsWith("/")||i.startsWith("//"))throw new Error(`AuthManager.updateConfig: loginPath must be a same-origin path starting with '/' (got: ${i})`)}const a=this.config.get(e);a&&this.config.set(e,{...a,...n})}getRedirectUrl(e){const a=this.config.get(e)?.defaultPath||"/",l=new URLSearchParams(window.location.search).get("redirect");if(l)try{const c=decodeURIComponent(l);if(c.startsWith("/"))return c}catch{}return a}getConfig(e){return this.config.get(e)}clearState(){this.state={isAuthenticated:!1,user:null,type:null}}static resetInstance(){Tr.instance=null}};$(Tr,"instance"),$(Tr,"LOCALE_STORAGE_KEY","g7_locale");let Sr=Tr;const xm={401:{handler:"navigate",params:{path:"{{auth.loginPath}}"}},403:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},404:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},422:{handler:"toast",params:{type:"error",message:"{{error.message}}"}},default:{handler:"toast",params:{type:"error",message:"{{error.message}}"}}},ea={findHandler(s,e){if(s){if(s[e])return s[e];if(s[String(e)])return s[String(e)];if(s.default)return s.default}},normalizeOnError(s){return s?Array.isArray(s)?s:[s]:[]},injectErrorCode(s,e,n){if(s.handler!=="showErrorPage"||s.params?.errorCode!==void 0)return s;const a=typeof e=="string"?parseInt(e,10):e,i=isNaN(a)?n:a;return i===void 0?s:{...s,params:{...s.params,errorCode:i}}}},wr=dt("ErrorHandlingResolver"),Na=class Na{constructor(){$(this,"templateErrorHandling",null);$(this,"layoutErrorHandling",null);$(this,"executeAction",null)}static getInstance(){return Na.instance||(Na.instance=new Na),Na.instance}static resetInstance(){Na.instance=null}setTemplateConfig(e){this.templateErrorHandling=e,wr.log("Template config set:",e)}setLayoutConfig(e){this.layoutErrorHandling=e,wr.log("Layout config set:",e)}setActionExecutor(e){this.executeAction=e}resolve(e,n={}){const{errorHandling:a,onError:i}=n;wr.log("Resolving error:",e,{hasActionErrorHandling:!!a,hasActionOnError:!!i,hasLayoutConfig:!!this.layoutErrorHandling,hasTemplateConfig:!!this.templateErrorHandling});let l=ea.findHandler(a,e);if(l){const c=this.getMatchedKey(a,e);return{handler:ea.injectErrorCode(l,c,e),level:"action",matchedKey:c}}if(i){const c=ea.normalizeOnError(i);if(c.length>0)return{handler:c.length===1?c[0]:{handler:"sequence",actions:c},level:"action",matchedKey:"onError"}}if(l=ea.findHandler(this.layoutErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.layoutErrorHandling,e);return{handler:ea.injectErrorCode(l,c,e),level:"layout",matchedKey:c}}if(l=ea.findHandler(this.templateErrorHandling||void 0,e),l){const c=this.getMatchedKey(this.templateErrorHandling,e);return{handler:ea.injectErrorCode(l,c,e),level:"template",matchedKey:c}}if(l=ea.findHandler(xm,e),l){const c=this.getMatchedKey(xm,e);return{handler:ea.injectErrorCode(l,c,e),level:"system",matchedKey:c}}return{handler:null,level:null,matchedKey:null}}async execute(e,n){if(!this.executeAction){wr.error("Action executor is not set");return}wr.log("Executing handler:",e.handler,{errorContext:n});try{return await this.executeAction(e,{error:n})}catch(a){throw wr.error("Failed to execute handler:",a),a}}async resolveAndExecute(e,n,a={}){const i=this.resolve(e,a);if(wr.log("Resolve result:",{errorCode:e,handler:i.handler?.handler,level:i.level,matchedKey:i.matchedKey}),!i.handler)return wr.warn("No handler found for error:",e),{handled:!1};try{return{handled:!0,result:await this.execute(i.handler,n)}}catch(l){return wr.error("Handler execution failed:",l),{handled:!1}}}getMatchedKey(e,n){return e[n]?n:e[String(n)]?String(n):"default"}clearLayoutConfig(){this.layoutErrorHandling=null,wr.log("Layout config cleared")}clearAllConfig(){this.templateErrorHandling=null,this.layoutErrorHandling=null,wr.log("All config cleared")}getConfigStatus(){return{hasTemplate:this.templateErrorHandling!==null,hasLayout:this.layoutErrorHandling!==null,hasExecutor:this.executeAction!==null}}};$(Na,"instance",null);let Jd=Na;function Cr(){return Jd.getInstance()}var Qd={exports:{}},bo={};var Tm;function NC(){if(Tm)return bo;Tm=1;var s=Symbol.for("react.transitional.element"),e=Symbol.for("react.fragment");function n(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:s,type:a,key:c,ref:i!==void 0?i:null,props:l}}return bo.Fragment=e,bo.jsx=n,bo.jsxs=n,bo}var km;function IC(){return km||(km=1,Qd.exports=NC()),Qd.exports}var ft=IC();const jC=Qr({__proto__:null,default:Zr(ft)},[ft]),Zd=dt("ParentContextProvider"),Rm=N.createContext(null);let rc=null;function ef(){rc?(Zd.log("[triggerModalParentUpdate] 모달 부모 컨텍스트 업데이트 트리거"),rc()):Zd.warn("[triggerModalParentUpdate] Provider가 아직 마운트되지 않음")}const Dm=({children:s})=>{const[e,n]=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,rc=()=>{a.current&&(n(c=>c+1),Zd.log("[ParentContextProvider] 버전 업데이트됨"))},()=>{a.current=!1,rc=null}),[]);const l={version:e,getParentDataContext:i};return ft.jsx(Rm.Provider,{value:l,children:s})};function HC(){return N.useContext(Rm)}const j=dt("ActionDispatcher");function zC(s,e,n,a){return n||(mg(s)||fd(s)?"$t:core.errors.network_request_failed":a||`Failed to execute action: ${e}`)}const Om=new Set(["navigate","navigateBack","navigateForward","replaceUrl","refresh","logout"]),UC=new Set(["redirect"]);function Gn(){try{return window.G7Core?.devTools}catch{return}}class Ut extends Error{constructor(n,a,i){super(n);$(this,"action");$(this,"originalError");$(this,"unknownHandler",!1);this.action=a,this.originalError=i,this.name="ActionError"}}const Lm={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"},bs=class bs{constructor(e={},n,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 Tn,this.translationEngine=n,this.translationContext=a,this.defaultContext=e,this.registerDefaultHandlers(),this.setupErrorHandling()}setGlobalHeaders(e){this.globalHeaders=e||[]}setNamedActions(e){this.namedActions=e||{};const n=Gn();n?.isEnabled?.()&&n.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 Om}static getPreviewSuppressedLayoutFeatures(){return UC}resolveActionRef(e){if(!e.actionRef)return e;const n=this.namedActions[e.actionRef];if(!n)return j.warn(`[resolveActionRef] named action not found: "${e.actionRef}"`),e;const a=Gn();a?.isEnabled?.()&&a.trackNamedActionRef?.({actionRefName:e.actionRef,resolvedHandler:n.handler,timestamp:Date.now()});const{actionRef:i,type:l,key:c,event:d,...f}=e,h={...n,...Object.fromEntries(Object.entries(f).filter(([,m])=>m!==void 0)),type:l??n.type};return c!==void 0&&(h.key=c),d!==void 0&&(h.event=d),h}matchesPattern(e,n){if(n==="*")return!0;const a=n.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${a}$`).test(e)}getMatchingGlobalHeaders(e,n){const a={};for(const i of this.globalHeaders)this.matchesPattern(e,i.pattern)&&Object.entries(i.headers).forEach(([l,c])=>{const d=is(c,n,{skipCache:!0});d!=null&&d!==""&&(a[l]=String(d))});return a}setupErrorHandling(){if(this.errorHandlingSetup)return;Cr().setActionExecutor(async(n,a)=>{const i={type:"click",handler:n.handler,target:n.target,params:n.params,actions:n.actions},l={data:{...this.defaultContext.data,error:a.error}};return await this.dispatchAction(i,l)}),this.errorHandlingSetup=!0}registerDefaultHandlers(){this.registerHandler("refetchDataSource",async(n,a)=>{const i=n.params?.dataSourceId,l=n.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?.()||{},h=a.data?._local||a.state||{},m={...f,...h},b=a.isolatedContext?.state;j.log("[refetchDataSource] localStateOverride:",m),await window.G7Core.dataSource.refetch(i,{...l?{sync:!0}:{},...d?{globalStateOverride:d}:{},...m?{localStateOverride:m}:{},...b?{isolatedStateOverride:b}:{}})}else j.warn("refetchDataSource: G7Core.dataSource.refetch is not available")}),this.registerHandler("appendDataSource",async(n,a)=>{const{dataSourceId:i,dataPath:l,newData:c}=n.params||{};if(!i){j.warn("appendDataSource: dataSourceId is required");return}if(c===void 0){j.warn("appendDataSource: newData is required");return}if(typeof window<"u"&&window.G7Core?.dataSource?.updateData){if(!Array.isArray(c)){j.warn("appendDataSource: newData must be an array, got:",typeof c,c);return}await window.G7Core.dataSource.updateData(i,l||null,c,"append")}else j.warn("appendDataSource: G7Core.dataSource.updateData is not available")}),this.registerHandler("updateDataSource",async(n,a)=>{const{dataSourceId:i,data:l,merge:c=!1}=n.params||{};if(!i){j.warn("updateDataSource: dataSourceId is required");return}if(l===void 0){j.warn("updateDataSource: data is required");return}typeof window<"u"&&window.G7Core?.dataSource?.set?(j.log(`[updateDataSource] Updating dataSource '${i}' with:`,l),window.G7Core.dataSource.set(i,l,{merge:c})):j.warn("updateDataSource: G7Core.dataSource.set is not available")}),this.registerHandler("scrollIntoView",async(n,a)=>{const{selector:i,behavior:l="smooth",block:c="nearest",inline:d="nearest",waitForElement:f=!1,timeout:h=2e3,delay:m=0,retryCount:b=0,retryInterval:S=50,scrollContainer:v}=n.params||{};if(!i){j.warn("scrollIntoView: selector is required");return}if(typeof window>"u"||typeof document>"u"){j.warn("scrollIntoView: window/document is not available");return}let _=null;if(f){if(_=await new Promise(A=>{const x=document.querySelector(i);if(x){A(x);return}const L=setTimeout(()=>{M.disconnect(),A(null)},h),M=new MutationObserver(()=>{const k=document.querySelector(i);k&&(clearTimeout(L),M.disconnect(),A(k))});M.observe(document.body,{childList:!0,subtree:!0})}),!_){j.warn(`scrollIntoView: element not found for selector "${i}" after ${h}ms timeout`);return}}else{m>0&&await new Promise(x=>setTimeout(x,m)),_=document.querySelector(i);let A=0;for(;!_&&A<b;)await new Promise(x=>setTimeout(x,S)),_=document.querySelector(i),A++;if(!_){j.warn(`scrollIntoView: element not found for selector "${i}" after ${A+1} attempts`);return}}if(v){const A=document.querySelector(v);if(!A){j.warn(`scrollIntoView: container not found for selector "${v}"`);return}const x=_.getBoundingClientRect(),L=A.getBoundingClientRect(),M=x.top-L.top+A.scrollTop,k=M+x.height;let O;switch(c){case"start":O=M;break;case"center":O=M-A.clientHeight/2+x.height/2;break;case"end":O=k-A.clientHeight;break;default:const B=x.top<L.top,G=x.bottom>L.bottom;if(B)O=M;else if(G)O=k-A.clientHeight;else return;break}A.scrollTo({top:Math.max(0,O),behavior:l});return}_.scrollIntoView({behavior:l,block:c,inline:d})}),this.registerHandler("reloadExtensions",async(n,a)=>{if(typeof window>"u"){j.warn("reloadExtensions: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadExtensions: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState()}catch(d){throw j.error("reloadExtensions: reloadExtensionState failed",d),d}else j.warn("reloadExtensions: TemplateApp.reloadExtensionState unavailable");const{moduleInfo:l,pluginInfo:c}=n.params||{};if(l)try{await this.executeAction({handler:"reloadModuleHandlers",params:n.params},a)}catch(d){j.error("reloadExtensions: reloadModuleHandlers failed",d)}if(c)try{await this.executeAction({handler:"reloadPluginHandlers",params:n.params},a)}catch(d){j.error("reloadExtensions: reloadPluginHandlers failed",d)}j.log("reloadExtensions: done")}),this.registerHandler("reloadRoutes",async(n,a)=>{if(typeof window>"u"){j.warn("reloadRoutes: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadRoutes: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState(),j.log("reloadRoutes: delegated to reloadExtensionState")}catch(l){throw j.error("reloadRoutes: Failed to reload routes",l),l}else{const l=i.getRouter?.();l&&(await l.loadRoutes(),j.log("reloadRoutes: Routes reloaded (legacy fallback)"))}}),this.registerHandler("refresh",async(n,a)=>{if(typeof window>"u"){j.warn("refresh: window is not available");return}const i=Number(n.params?.delayMs??0);i>0&&await new Promise(l=>setTimeout(l,i)),window.location.reload()}),this.registerHandler("remount",async(n,a)=>{const{componentId:i}=n.params||{};if(!i){j.warn("remount: componentId parameter is required");return}if(!this.globalStateUpdater){j.warn("remount: globalStateUpdater is not set");return}const d=(window.G7Core?.state?.get()||{})._global?._remountKeys||{},f=d[i]||0;this.globalStateUpdater({_remountKeys:{...d,[i]:f+1}}),j.log(`remount: ${i} key incremented to ${f+1}`)}),this.registerHandler("reloadTranslations",async(n,a)=>{if(typeof window>"u"){j.warn("reloadTranslations: window is not available");return}const i=window.__templateApp;if(!i){j.warn("reloadTranslations: TemplateApp not initialized");return}if(typeof i.reloadExtensionState=="function")try{await i.reloadExtensionState(),j.log("reloadTranslations: delegated to reloadExtensionState")}catch(l){throw j.error("reloadTranslations: Failed to reload translations",l),l}}),this.registerHandler("reloadModuleHandlers",async(n,a)=>{if(typeof window>"u"){j.warn("reloadModuleHandlers: window is not available");return}const{moduleInfo:i,action:l}=n.params||{};if(!i){j.warn("reloadModuleHandlers: moduleInfo is required");return}const c=i.data||i;if(!c.identifier){j.warn("reloadModuleHandlers: moduleInfo with identifier is required");return}if(!l||l!=="add"&&l!=="remove"){j.warn('reloadModuleHandlers: action must be "add" or "remove"');return}const d=window.G7Config;if(!d){j.warn("reloadModuleHandlers: G7Config not available");return}const f=c.identifier;try{if(l==="add"){if(c.assets&&(d.moduleAssets=d.moduleAssets||{},d.moduleAssets[f]=c.assets,j.log(`reloadModuleHandlers: Added assets for ${f}`),c.assets.js)){const h=c.assets.js,m=`module-${f}`;if(document.getElementById(m)){j.warn(`reloadModuleHandlers: Script ${m} already loaded`);return}const b=document.createElement("script");if(b.id=m,b.src=h,b.async=!0,await new Promise((S,v)=>{b.onload=()=>{j.log(`reloadModuleHandlers: Script loaded successfully for ${f}`),S()},b.onerror=()=>{j.error(`reloadModuleHandlers: Failed to load script for ${f}`),v(new Error(`Failed to load module script: ${h}`))},document.head.appendChild(b)}),c.assets.css){const S=c.assets.css,v=`module-css-${f}`;if(!document.getElementById(v)){const _=document.createElement("link");_.id=v,_.rel="stylesheet",_.href=S,document.head.appendChild(_),j.log(`reloadModuleHandlers: CSS loaded for ${f}`)}}}}else if(l==="remove"){d.moduleAssets&&d.moduleAssets[f]&&(delete d.moduleAssets[f],j.log(`reloadModuleHandlers: Removed assets for ${f}`));const h=`module-${f}`,m=document.getElementById(h);m&&(m.remove(),j.log(`reloadModuleHandlers: Removed script for ${f}`));const b=`module-css-${f}`,S=document.getElementById(b);S&&(S.remove(),j.log(`reloadModuleHandlers: Removed CSS for ${f}`))}}catch(h){throw j.error(`reloadModuleHandlers: Failed to ${l} module assets`,h),h}}),this.registerHandler("reloadPluginHandlers",async(n,a)=>{if(typeof window>"u"){j.warn("reloadPluginHandlers: window is not available");return}const{pluginInfo:i,action:l}=n.params||{};if(!i){j.warn("reloadPluginHandlers: pluginInfo is required");return}const c=i.data||i;if(!c.identifier){j.warn("reloadPluginHandlers: pluginInfo with identifier is required");return}if(!l||l!=="add"&&l!=="remove"){j.warn('reloadPluginHandlers: action must be "add" or "remove"');return}const d=window.G7Config;if(!d){j.warn("reloadPluginHandlers: G7Config not available");return}const f=c.identifier;try{if(l==="add"){if(c.assets&&(d.pluginAssets=d.pluginAssets||{},d.pluginAssets[f]=c.assets,j.log(`reloadPluginHandlers: Added assets for ${f}`),c.assets.js)){const h=c.assets.js,m=`plugin-${f}`;if(document.getElementById(m)){j.warn(`reloadPluginHandlers: Script ${m} already loaded`);return}const b=document.createElement("script");if(b.id=m,b.src=h,b.async=!0,await new Promise((S,v)=>{b.onload=()=>{j.log(`reloadPluginHandlers: Script loaded successfully for ${f}`),S()},b.onerror=()=>{j.error(`reloadPluginHandlers: Failed to load script for ${f}`),v(new Error(`Failed to load plugin script: ${h}`))},document.head.appendChild(b)}),c.assets.css){const S=c.assets.css,v=`plugin-css-${f}`;if(!document.getElementById(v)){const _=document.createElement("link");_.id=v,_.rel="stylesheet",_.href=S,document.head.appendChild(_),j.log(`reloadPluginHandlers: CSS loaded for ${f}`)}}}}else if(l==="remove"){d.pluginAssets&&d.pluginAssets[f]&&(delete d.pluginAssets[f],j.log(`reloadPluginHandlers: Removed assets for ${f}`));const h=`plugin-${f}`,m=document.getElementById(h);m&&(m.remove(),j.log(`reloadPluginHandlers: Removed script for ${f}`));const b=`plugin-css-${f}`,S=document.getElementById(b);S&&(S.remove(),j.log(`reloadPluginHandlers: Removed CSS for ${f}`))}}catch(h){throw j.error(`reloadPluginHandlers: Failed to ${l} plugin assets`,h),h}});let e=!1;this.registerHandler("showErrorPage",async(n,a)=>{if(e){j.log("showErrorPage: Already active, skipping duplicate call");return}if(e=!0,typeof window>"u"){e=!1,j.warn("showErrorPage: window is not available");return}const i=window.__templateApp;if(!i){e=!1,j.warn("showErrorPage: TemplateApp not initialized");return}const l=i.getErrorPageHandler?.();if(!l){e=!1,j.warn("showErrorPage: ErrorPageHandler not available");return}const c=n.params?.errorCode||500,d=n.params?.target||"content";let f=n.params?.containerId;f||(f=d==="full"?"app":"main_content"),j.log("showErrorPage:",{errorCode:c,target:d,containerId:f});const h=(m,b=6e4)=>new Promise((S,v)=>{const _=document.getElementById(m);if(_){S(_);return}j.log(`showErrorPage: Waiting for container #${m}...`);let A;const x=new MutationObserver((L,M)=>{const k=document.getElementById(m);k&&(j.log(`showErrorPage: Container #${m} found`),M.disconnect(),clearTimeout(A),S(k))});x.observe(document.body,{childList:!0,subtree:!0}),A=setTimeout(()=>{x.disconnect(),v(new Error(`Container #${m} not found within ${b}ms`))},b)});try{await h(f);const m=Cr(),b=m.layoutErrorHandling;m.clearLayoutConfig();try{await l.renderError(c,f)||j.warn(`showErrorPage: Failed to render error page for code ${c}`)}finally{m.setLayoutConfig(b)}}catch(m){throw j.error("showErrorPage: Error rendering error page:",m),m}finally{e=!1}}),this.registerHandler("emitEvent",async(n,a)=>{const i=n.params?.event,l=n.params?.data;if(!i){j.warn("emitEvent: event parameter is required");return}if(typeof window>"u"){j.warn("emitEvent: window is not available");return}const c=window.G7Core;if(!c?.componentEvent?.emit){j.warn("emitEvent: G7Core.componentEvent is not available");return}try{j.log(`emitEvent: Emitting "${i}"`,l);const d={...l,_context:{data:a.data,state:a.state}},f=await c.componentEvent.emit(i,d);if(!f||f.length===0?j.warn(`emitEvent: No listeners found for "${i}"`):j.log(`emitEvent: Event "${i}" completed with ${f.length} listener(s)`,f),this.globalStateUpdater){const h=(c?.state?.get()||{})._local||{},m={event:i,success:!0,data:f.length===1?f[0]:f,listeners:f.length};this.globalStateUpdater({_local:{...h,_eventResult:m}});const b=a.state&&typeof a.state=="object"&&!Array.isArray(a.state)?a.state:h;window.__g7SequenceLocalSync={...b,_eventResult:m}}}catch(d){if(j.error(`emitEvent: Event "${i}" failed`,d),this.globalStateUpdater){const f=c?.state?.get()||{};this.globalStateUpdater({_local:{...f._local,_eventResult:{event:i,success:!1,error:d instanceof Error?d.message:String(d)}}})}throw d}}),this.registerHandler("updateProductField",async(n,a)=>{const{productId:i,field:l,value:c}=n.params||{};if(!i||!l){j.warn("updateProductField: productId and field are required");return}if(typeof window>"u"){j.warn("updateProductField: window is not available");return}const d=window.G7Core;if(!d?.state?.get||!this.globalStateUpdater){j.warn("updateProductField: G7Core.state or globalStateUpdater is not available");return}try{const f=d.state.get()||{},m=(f.products?.data?.data||[]).map(S=>S.id===i?{...S,[l]:c,_modified:!0}:S),b=new Set(f._local?.modifiedProductIds||[]);b.add(i),this.globalStateUpdater({products:{...f.products,data:{...f.products?.data,data:m}},_local:{...f._local,modifiedProductIds:Array.from(b)}}),j.log(`updateProductField: Updated product ${i}, field: ${l}, value:`,c)}catch(f){throw j.error("updateProductField: Error updating product field",f),f}}),this.registerHandler("updateOptionField",async(n,a)=>{const{productId:i,optionId:l,field:c,value:d}=n.params||{};if(!i||!l||!c){j.warn("updateOptionField: productId, optionId and field are required");return}if(typeof window>"u"){j.warn("updateOptionField: window is not available");return}const f=window.G7Core;if(!f?.state?.get||!this.globalStateUpdater){j.warn("updateOptionField: G7Core.state or globalStateUpdater is not available");return}try{const h=f.state.get()||{},b=(h.products?.data?.data||[]).map(v=>{if(v.id===i&&v.options){const _=v.options.map(A=>A.id===l?{...A,[c]:d,_modified:!0}:A);return{...v,options:_,_modified:!0}}return v}),S=new Set(h._local?.modifiedProductIds||[]);S.add(i),this.globalStateUpdater({products:{...h.products,data:{...h.products?.data,data:b}},_local:{...h._local,modifiedProductIds:Array.from(S)}}),j.log(`updateOptionField: Updated product ${i} option ${l}, field: ${c}, value:`,d)}catch(h){throw j.error("updateOptionField: Error updating option field",h),h}}),this.registerHandler("setLocale",async(n,a)=>{const i=n.target;if(!i||typeof i!="string"){j.warn("setLocale: Invalid locale:",i);return}const l=window.__templateApp;if(l&&typeof l.changeLocale=="function")try{await l.changeLocale(i),j.log("setLocale: Locale changed to",i)}catch(c){j.error("setLocale: Failed to change locale:",c),window.location.reload()}else{j.warn("setLocale: TemplateApp not found, falling back to page reload");try{localStorage.setItem("g7_locale",i)}catch{}window.location.reload()}}),this.registerHandler("suppress",async()=>{j.log("suppress: Error intentionally suppressed")}),this.registerBuiltInHandlerMetadata()}registerBuiltInHandlerMetadata(){const e=Gn();if(!e?.isEnabled())return;const n=[{name:"refetchDataSource",description:"데이터 소스를 다시 fetch합니다"},{name:"appendDataSource",description:"데이터 소스에 새 데이터를 병합합니다 (무한 스크롤용)"},{name:"updateDataSource",description:"API 응답으로 데이터 소스를 직접 업데이트합니다 (refetch 대체)"},{name:"reloadRoutes",description:"라우트를 다시 로드합니다"},{name:"refresh",description:"현재 페이지를 새로고침합니다"},{name:"remount",description:"컴포넌트를 리마운트합니다"},{name:"reloadTranslations",description:"다국어 파일을 다시 로드합니다"},{name:"showErrorPage",description:"에러 페이지를 표시합니다"},{name:"emitEvent",description:"이벤트를 발생시킵니다"},{name:"updateProductField",description:"상품 필드를 인라인 수정합니다"},{name:"updateOptionField",description:"상품 옵션 필드를 인라인 수정합니다"},{name:"setLocale",description:"언어를 변경합니다 (DB 저장 + UI 리렌더링)"}];for(const a of n)e.trackHandlerRegistration(a.name,"built-in",a.description)}createHandler(e,n,a){j.log("createHandler called for:",e.handler,e.type);const i=window.__g7DevTools,l=Date.now(),c=a?.state?{...a.state}:null,d=`handler_${e.handler}_${l}`;return async f=>{if(j.log("Handler invoked for:",e.handler,"event:",f.type),i?.isEnabled?.()&&c){const m=window.G7Core?.state?.getLocal?.()??a?.state??{},b=Date.now()-l;for(const S of Object.keys(c)){const v=c[S],_=m[S];v!==_&&b>100&&(i.trackStaleClosureWarning?.({type:"event-handler-stale",location:`createHandler(${e.handler})`,capturedPath:`_local.${S}`,capturedValue:v,capturedAt:l,currentValue:_,actionId:d,stackTrace:new Error().stack}),j.warn(`[Stale Closure] _local.${S} changed after handler creation:`,`captured="${v}" → current="${_}" (${b}ms ago)`))}}try{f.type!=="change"&&f.preventDefault();let h={};if(f.type==="submit"&&f.target instanceof HTMLFormElement){const S=f.target;new FormData(S).forEach((_,A)=>{h[A]=_})}const m={...this.defaultContext,data:{...n,form:h,_local:a?.state||{},$event:f},event:f,...a&&{state:a.state,setState:a.setState},isolatedContext:a?.isolatedContext};if(e.confirm){let S=this.resolveValue(e.confirm,m.data);if(this.translationEngine&&this.translationContext&&S.startsWith("$t:")&&(S=this.translationEngine.resolveTranslations(S,this.translationContext,m.data)),!confirm(S))return}const b=window.__g7ActionContext;window.__g7ActionContext={...a,data:m.data};try{await this.executeAction(e,m)}finally{window.__g7ActionContext=b}}catch(h){if(j.error("Action execution failed:",h),e.onError){const m=h instanceof Ut&&h.originalError?h.originalError:h,b={...this.defaultContext,data:{...n,error:m,_local:a?.state||{},$event:f},event:f,...a&&{state:a.state,setState:a.setState}},S=Array.isArray(e.onError)?e.onError:[e.onError];for(const v of S)await this.executeAction(v,b)}}}}generateActionId(e,n){const a=e.handler,i=n||"no-target",l=Date.now(),c=Math.random().toString(36).substring(2,9);return`${a}_${i.replace(/[^a-zA-Z0-9]/g,"_")}_${l}_${c}`}async executeAction(e,n){if(e=this.resolveActionRef(e),typeof e.handler=="string"&&e.handler.includes("{{")){const f=this.evaluateExpression(e.handler,n.data);e={...e,handler:f==null?"":String(f)}}const a=Gn(),i=a?.isEnabled()?`action_${Date.now()}_${Math.random().toString(36).substring(2,11)}`:void 0,l=performance.now();let c;if(i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),context:this.sanitizeForDevTools({hasState:!!n.state,hasSetState:!!n.setState,dataKeys:n.data?Object.keys(n.data):[]}),startTime:l,status:"started"}),e.if!==void 0)try{const f=this.resolveValue(e.if,n.data),h=f===!0||f==="true"||f&&f!=="false"&&f!=="0"&&f!==!1;if(j.log("[executeAction] if condition result:",e.handler,h),!h)return i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"success",result:{skipped:!0,reason:"if condition false"}}),{success:!0,data:void 0}}catch(f){return j.error("[executeAction] if condition error:",e.handler,f),i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"error",error:{name:"ConditionEvaluationError",message:f instanceof Error?f.message:String(f),stack:f instanceof Error?f.stack:void 0}}),{success:!0,data:void 0}}let d;try{const f=this.resolveParams(e.params,n.data);i&&(c=this.sanitizeForDevTools(f));const h=e.target?this.resolveValue(e.target,n.data):void 0;if(e.handler==="apiCall"&&n.setState){d=this.generateActionId(e,h);const b=n.state?.loadingActions||{};n.setState({loadingActions:{...b,[d]:!0}})}if(this.previewMode&&Om.has(e.handler))return j.warn(`Preview mode: "${e.handler}" suppressed`,h||f),i&&a&&a.logAction({id:i,type:e.handler,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"skipped",metadata:{reason:"preview_mode_suppressed"}}),{success:!0,data:void 0};let m;switch(e.handler){case"ensureIdentityVerified":m=await this.handleEnsureIdentityVerified(f);break;case"resolveIdentityChallenge":m=this.handleResolveIdentityChallenge(f);break;case"navigate":const b=f.path||h;m=await this.handleNavigate(b,f,n);break;case"navigateBack":m=await this.handleNavigateBack();break;case"navigateForward":m=await this.handleNavigateForward();break;case"openWindow":{const _=f.path||h;m=await this.handleOpenWindow(_,f);break}case"replaceUrl":{const _=f.path||h||window.location.pathname;m=await this.handleReplaceUrl(_,f);break}case"apiCall":if(e.onSuccess&&a?.isEnabled()&&i){const _=window.G7Core,A={};if(_?.state?.get){const x=_.state.get();A._global=x}n.state&&(A._local=n.state),a.registerStateCaptureForHandler?.(i,["_global","_local"],A)}const S=e.auth_mode??(e.auth_required?"required":"none"),v=e.identity_target?this.resolveParams(e.identity_target,n.data):void 0;m=await this.handleApiCall(h,f,n,S,v);break;case"login":m=await this.handleLogin(h,f,n);break;case"logout":m=await this.handleLogout(h,n);break;case"setState":j.log("[executeAction] setState resolvedParams:",f),m=await this.handleSetState(e.render!==void 0?{...f,__render:e.render}:f,n);break;case"setError":m=await this.handleSetError(h,f,n);break;case"openModal":m=await this.handleOpenModal(h,n);break;case"closeModal":m=await this.handleCloseModal(n);break;case"showAlert":m=await this.handleShowAlert(h,n);break;case"toast":m=await this.handleToast(f,n);break;case"switch":m=await this.handleSwitch(e,n);break;case"conditions":m=await this.handleConditions(e,n);break;case"sequence":m=await this.handleSequence(e,n);break;case"parallel":m=await this.handleParallel(e,n);break;case"startInterval":m=this.handleStartInterval(f,n);break;case"stopInterval":m=this.handleStopInterval(f);break;case"loadScript":m=await this.handleLoadScript(f,e,n);break;case"callExternal":m=await this.handleCallExternal(f,e,n);break;case"callExternalEmbed":m=await this.handleCallExternalEmbed(f,e,n);break;case"saveToLocalStorage":m=await this.handleSaveToLocalStorage(f,n);break;case"loadFromLocalStorage":m=await this.handleLoadFromLocalStorage(f,n);break;default:m=await this.handleCustomAction({...e,target:h,params:f},n);break}if(e.resultTo&&m!==void 0){const{target:b,key:S,merge:v}=e.resultTo,_=this.resolveValue(S,n.data),A=v==="replace"?"replace":v==="shallow"?"shallow":"deep";if(b==="_local"&&n.setState){const x=this.buildNestedUpdate(_,m),L=A!=="deep"?{...x,__mergeMode:A}:x;n.setState(L),j.log(`[resultTo] Saved to _local.${_} (merge=${A}):`,m)}else if(b==="_local"&&this.globalStateUpdater){const M=(window.G7Core?.state?.get()||{})._local||{},k=this.buildNestedUpdate(_,m);let O;A==="replace"?O=k:A==="shallow"?O={...M,...k}:O=this.deepMergeWithState(k,M),this.globalStateUpdater({_local:O}),j.log(`[resultTo] Saved to _local.${_} via globalStateUpdater (merge=${A}):`,m)}else if(b==="_global"&&this.globalStateUpdater){const x=this.buildNestedUpdate(_,m);this.globalStateUpdater(x),j.log(`[resultTo] Saved to _global.${_}:`,m)}else if(b==="_isolated"&&n.isolatedContext){const x=this.buildNestedUpdate(_,m);n.isolatedContext.mergeState(x,A),j.log(`[resultTo] Saved to _isolated.${_} (merge=${A}):`,m)}else j.warn(`[resultTo] Cannot save result: target=${b}, setState=${!!n.setState}, globalStateUpdater=${!!this.globalStateUpdater}, isolatedContext=${!!n.isolatedContext}`)}if(e.onSuccess){if(a?.isEnabled()&&i&&e.handler==="apiCall"){const v=window.G7Core,_={};v?.state?.get&&(_._global=v.state.get()),n.state&&(_._local=n.state),a.detectStaleClosure?.(i,`${e.handler} → onSuccess`,_,"callback-state-capture",i)}const b={...n,data:{...n.data,result:m,response:m}},S=Array.isArray(e.onSuccess)?e.onSuccess:[e.onSuccess];S.length>1?await this.handleSequence({handler:"sequence",type:"click",actions:S},b):S.length===1&&await this.executeAction(S[0],b)}return i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),resolvedParams:c,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"success",result:this.sanitizeForDevTools(m)}),{success:!0,data:m}}catch(f){const h=f instanceof Ut?f:new Ut(`Failed to execute action: ${e.handler}`,e,f instanceof Error?f:void 0),m=h.originalError?.response||{},b=m.data||{},S=h.originalError?.status||m.status||500;let v=zC(h.originalError??f,e.handler,b.message,h.message);this.translationEngine&&this.translationContext&&v.startsWith("$t:")&&(v=this.translationEngine.resolveTranslations(v,this.translationContext));const _={status:S,message:v,errors:b.errors||m.errors,data:b,statusText:h.originalError?.statusText,error_code:b.error_code??m.error_code};if(h.unknownHandler)throw j.warn(`Unknown action handler "${e.handler}" — skipped (extension not loaded?). Not surfaced to the user.`),h;const x=Cr().resolve(S,{errorHandling:e.errorHandling,onError:e.onError});if(x.handler){if(a?.isEnabled()&&i&&e.handler==="apiCall"){const M=window.G7Core,k={};M?.state?.get&&(k._global=M.state.get()),n.state&&(k._local=n.state),a.detectStaleClosure?.(i,`${e.handler} → onError`,k,"callback-state-capture",i)}const L={...n,data:{...n.data,error:_}};try{const M={type:"click",handler:x.handler.handler,target:x.handler.target,params:x.handler.params,actions:x.handler.actions};return await this.executeAction(M,L),{success:!1,error:h}}catch(M){return j.error("Error executing error handler:",M),{success:!1,error:h}}}throw i&&a&&a.logAction({id:i,type:e.handler,params:this.sanitizeForDevTools(e.params),resolvedParams:c,startTime:l,endTime:performance.now(),duration:performance.now()-l,status:"error",error:{name:h.name,message:h.message,stack:h.stack}}),h}finally{if(e.handler==="apiCall"&&n.setState&&d){const f=n.state?.loadingActions||{},{[d]:h,...m}=f;n.setState({loadingActions:m})}}}async handleNavigate(e,n,a){let i=e;if(n.query||n.mergeQuery===!0)if(n.mergeQuery===!0)i=this.buildMergedQueryPath(e,n.query??{});else{const c=new URLSearchParams;for(const[f,h]of Object.entries(n.query))if(h!=null&&h!=="")if(Array.isArray(h)){const m=f.endsWith("[]")?f:`${f}[]`;for(const b of h)b!=null&&b!==""&&c.append(m,String(b))}else c.set(f,String(h));const d=c.toString();d&&(i=`${e}?${d}`)}if(j.log("handleNavigate:",{target:e,params:n,finalPath:i,replace:n.replace,windowLocationSearch:window.location.search}),n.replace===!0){const c=window.G7Core;if(c?.updateQueryParams){const d=typeof n.transition_overlay_target=="string"?n.transition_overlay_target:void 0;await c.updateQueryParams(i,d?{transitionOverlayTarget:d}:void 0),j.log("handleNavigate: Used updateQueryParams for replace mode",d?{transitionOverlayTarget:d}:void 0),this.applyScrollOption(n.scroll,n.scrollBehavior,"top");return}j.warn("handleNavigate: G7Core.updateQueryParams not available, falling back to React Router")}const l=this.resolveNavigateFallbackAction(i,n);if(l){j.warn(`handleNavigate: No route matched, falling back to "${l.handler}"`),await this.dispatchAction({type:"click",handler:l.handler,params:l.params},a),this.applyScrollOption(n.scroll,n.scrollBehavior,"top");return}if(!a.navigate)throw new Ut("Navigate function is not provided in context");a.navigate(i,{replace:n.replace===!0}),this.applyScrollOption(n.scroll,n.scrollBehavior,"top")}resolveNavigateFallbackAction(e,n){const a=n.fallback;if(a===!1||n.replace===!0)return null;const l=window.__templateApp?.getRouter?.();if(!l||typeof l.match!="function"||typeof l.getRoutes=="function"&&l.getRoutes().length===0)return null;const c=e.split("?")[0];return l.match(c)?null:this.resolveNavigateFallback(a,e,n)}resolveNavigateFallback(e,n,a){return e==null?{handler:"openWindow",params:{path:n,target:"_self"}}:typeof e=="string"?{handler:e,params:{path:n}}:typeof e=="object"&&typeof e.handler=="string"?{handler:e.handler,params:{path:n,...e.params||{}}}:(j.warn("resolveNavigateFallback: unrecognized fallback option, using openWindow",e),{handler:"openWindow",params:{path:n,target:"_blank"}})}applyScrollOption(e,n,a){const i=e===void 0?a:e;if(i==="preserve")return;const l=n==="smooth"?"smooth":"instant",c=b=>{if(typeof b!="object"||b===null)return!1;const S=b;return"container"in S||"to"in S||"block"in S||"offset"in S},d=(b,S,v=0)=>{b===window?window.scrollTo({top:S,left:v,behavior:l}):b.scrollTo({top:S,left:v,behavior:l})},f=(b,S,v,_)=>{if(S===window){b.scrollIntoView({behavior:l,block:v}),_&&window.scrollBy({top:-_,left:0,behavior:l});return}const A=S,x=b.getBoundingClientRect(),L=A.getBoundingClientRect(),M=A.scrollTop+(x.top-L.top);let k;v==="center"?k=M-(A.clientHeight-b.clientHeight)/2:v==="end"?k=M-(A.clientHeight-b.clientHeight):k=M,k-=_,A.scrollTo({top:Math.max(0,k),left:0,behavior:l})},h=()=>{window.scrollTo({top:0,left:0,behavior:l});const b=document.getElementById("app")??document.body;if(!b)return;b.querySelectorAll("*").forEach(v=>{if(v.scrollTop===0&&v.scrollLeft===0)return;const _=window.getComputedStyle(v),A=_.overflowY,x=_.overflowX;(A==="auto"||A==="scroll"||x==="auto"||x==="scroll")&&v.scrollTo({top:0,left:0,behavior:l})})},m=()=>{try{if(c(i)){const b=i,S=b.block??"start",v=typeof b.offset=="number"?b.offset:0;let _=window;if(typeof b.container=="string"&&b.container.length>0){const x=document.querySelector(b.container);if(!x){j.warn(`applyScrollOption: container not found: ${b.container}`);return}_=x}const A=b.to??"top";if(A==="top"){_===window?h():d(_,0);return}if(typeof A=="number"){d(_,A-v);return}if(typeof A=="object"&&A!==null&&("x"in A||"y"in A)){const{x=0,y:L=0}=A;d(_,L-v,x);return}if(typeof A=="string"&&(A.startsWith("#")||A.startsWith("."))){const x=document.querySelector(A);x?f(x,_,S,v):j.warn(`applyScrollOption: target element not found: ${A}`);return}j.warn('applyScrollOption: invalid "to" value in extended form',A);return}if(i==="top"){h();return}if(typeof i=="number"){window.scrollTo({top:i,left:0,behavior:l});return}if(typeof i=="object"&&i!==null&&("x"in i||"y"in i)){const{x:b=0,y:S=0}=i;window.scrollTo({top:S,left:b,behavior:l});return}if(typeof i=="string"&&(i.startsWith("#")||i.startsWith("."))){const b=document.querySelector(i);b&&b.scrollIntoView({behavior:l,block:"start"});return}}catch(b){j.warn("applyScrollOption: failed to apply scroll",b)}};typeof window.requestAnimationFrame=="function"?window.requestAnimationFrame(m):m()}async handleOpenWindow(e,n){let a=e;if(n.query){const l=new URLSearchParams;for(const[d,f]of Object.entries(n.query))f!=null&&f!==""&&l.set(d,String(f));const c=l.toString();c&&(a=`${e}?${c}`)}const i=n.target==="_self"?"_self":"_blank";j.log("handleOpenWindow:",{target:e,params:n,finalPath:a,windowTarget:i}),i==="_self"?window.location.assign(a):window.open(a,"_blank")}async handleNavigateBack(){j.log("handleNavigateBack"),window.history.back()}async handleNavigateForward(){j.log("handleNavigateForward"),window.history.forward()}async handleReplaceUrl(e,n){let a=e;if(n.query||n.mergeQuery===!0)if(n.mergeQuery===!0)a=this.buildMergedQueryPath(e,n.query??{});else{const i=new URLSearchParams;for(const[c,d]of Object.entries(n.query))if(d!=null&&d!=="")if(Array.isArray(d)){const f=c.endsWith("[]")?c:`${c}[]`;for(const h of d)h!=null&&h!==""&&i.append(f,String(h))}else i.set(c,String(d));const l=i.toString();l&&(a=`${e}?${l}`)}j.log("handleReplaceUrl:",{target:e,params:n,finalPath:a}),window.history.replaceState(null,"",a),this.applyScrollOption(n.scroll,n.scrollBehavior,"preserve")}buildMergedQueryPath(e,n){const a=new URLSearchParams(window.location.search);for(const[c,d]of Object.entries(n))if(d==null||d==="")a.delete(c);else if(Array.isArray(d)){const f=c.endsWith("[]")?c:`${c}[]`;a.delete(c),a.delete(f);for(const h of d)h!=null&&h!==""&&a.append(f,String(h))}else a.set(c,String(d));const i=a.toString(),l=e.split("?")[0];return i?`${l}?${i}`:l}async ensureCsrfToken(){try{await fetch("/sanctum/csrf-cookie",{credentials:"include"})}catch(e){throw new Ut("Failed to fetch CSRF token",void 0,e instanceof Error?e:void 0)}}getCsrfTokenFromCookie(){const a=`; ${document.cookie}`.split("; XSRF-TOKEN=");if(a.length===2){const i=a.pop()?.split(";").shift();return i?decodeURIComponent(i):null}return null}buildQueryString(e){const n=new URLSearchParams;for(const[a,i]of Object.entries(e))if(!(i==null||i===""))if(Array.isArray(i))for(const l of i)l!=null&&l!==""&&n.append(`${a}[]`,String(l));else typeof i=="object"?n.append(a,JSON.stringify(i)):n.append(a,String(i));return n.toString()}async handleEnsureIdentityVerified(e){const n=typeof e.purpose=="string"?e.purpose:"sensitive_action",a=e.target&&typeof e.target=="object"?e.target:void 0;return await St.handle({success:!1,error_code:"identity_verification_required",message:"",verification:{policy_key:e.policy_key??"",purpose:n,provider_id:e.provider_id??null,render_hint:e.render_hint??null,return_request:null}},void 0,a)!==null}handleResolveIdentityChallenge(e){const n=typeof e.result=="string"?e.result:"cancelled";let a;switch(n){case"verified":{const i=typeof e.token=="string"?e.token:"";if(!i){j.warn("resolveIdentityChallenge: result=verified 인데 token 이 비어있습니다. failed 로 강등합니다."),a={status:"failed",failureCode:"MISSING_TOKEN"};break}a={status:"verified",token:i,providerData:e.providerData&&typeof e.providerData=="object"?e.providerData:void 0};break}case"pending":{const i=typeof e.pollUrl=="string"?e.pollUrl:"",l=typeof e.expiresAt=="string"?e.expiresAt:"";if(!i||!l){j.warn("resolveIdentityChallenge: result=pending 인데 pollUrl/expiresAt 누락 — failed 로 강등합니다."),a={status:"failed",failureCode:"MALFORMED_PENDING"};break}a={status:"pending",pollUrl:i,pollIntervalMs:typeof e.pollIntervalMs=="number"?e.pollIntervalMs:void 0,expiresAt:l};break}case"failed":a={status:"failed",failureCode:typeof e.failureCode=="string"?e.failureCode:"UNKNOWN",reason:typeof e.reason=="string"?e.reason:void 0};break;default:a={status:"cancelled"};break}return St.resolveDeferred(a),!0}async handleApiCall(e,n,a,i="none",l){const{method:c="GET",body:d,headers:f,contentType:h}=n;c!=="GET"&&c!=="HEAD"&&await this.ensureCsrfToken();const m=this.getCsrfTokenFromCookie();let b={};if(i==="required"||i==="optional"){const W=Hr().getToken();W&&(b={Authorization:`Bearer ${W}`})}let S=e;const v=n.query||(c==="GET"?d:null);if(v&&typeof v=="object"){const P=this.buildQueryString(v);if(P){const W=e.includes("?")?"&":"?";S=`${e}${W}${P}`}}const _=window.G7Core?.state?.getGlobal?.()||a.state?._global||{},A=window.G7Core?.state?.getLocal?.()||a.state?._local||{},x={_global:_,_local:A},L=this.getMatchingGlobalHeaders(e,x),M={};if(typeof window<"u"){const P=localStorage.getItem("g7_locale");P&&(M["Accept-Language"]=P)}const k=h==="multipart/form-data",O={method:c,headers:{...k?{}:{"Content-Type":"application/json"},Accept:"application/json",...M,...m&&{"X-XSRF-TOKEN":m},...b,...L,...f},credentials:"include"};if(d&&c!=="GET")if(k){const P=new FormData;for(const[W,pe]of Object.entries(d))pe instanceof File||pe instanceof Blob?P.append(W,pe):pe!=null&&P.append(W,typeof pe=="object"?JSON.stringify(pe):String(pe));O.body=P}else O.body=JSON.stringify(d);const B=Gn();let G=null;B?.isEnabled()&&(G=B.trackRequest(S,c));try{let P=await fetch(S,O),W;try{W=await P.json()}catch{W=null}if(St.isIdentityRequired(P.status,W)){const pe=await St.handle(W,{body:O.body,headers:O.headers,credentials:O.credentials},l);if(pe){P=pe;try{W=await P.json()}catch{W=null}}}if(G&&B?.isEnabled()&&(B.completeRequest(G,P.status,W),G=null),!P.ok){const pe=W||{},Se=new Error(pe.message||`API call failed: ${P.statusText}`);throw Se.response=pe,Se.status=P.status,Se.statusText=P.statusText,new Ut(pe.message||`API call failed: ${P.statusText}`,void 0,Se)}return W}catch(P){throw G&&B?.isEnabled()&&B.failRequest(G,P instanceof Error?P.message:String(P)),P}}async handleLogin(e,n,a){const{body:i}=n;if(!i||!i.email||!i.password)throw new Ut("Login requires email and password in body params");const l=e==="user"?"user":"admin",c=l==="admin"?"/api/auth/admin/login":"/api/auth/login",d=window.G7Core?.state?.getGlobal?.()||a.state?._global||{},f=window.G7Core?.state?.getLocal?.()||a.state?._local||{},h={_global:d,_local:f},m=this.getMatchingGlobalHeaders(c,h),b=Sr.getInstance();try{const S=Object.keys(m).length>0?{headers:m}:void 0;return{user:await b.login(l,{email:i.email,password:i.password},S)}}catch(S){const v=S.message||"Login failed",_=new Error(v);throw _.response=S.response?.data||{},_.status=S.status||S.response?.status||500,new Ut(v,void 0,_)}}async handleLogout(e,n){await Sr.getInstance().logout()}async handleSetState(e,n){j.log("[handleSetState] START, params:",e);const{target:a="component",scope:i,merge:l,__render:c,...d}=e,f=Gn(),h=window.G7Core;if(typeof a=="string"&&(a.startsWith("$parent.")||a.startsWith("$root.")))return this.handleParentScopeSetState(a,d,l,n);const m=n.state&&Object.keys(n.state).length>0?n.state:h?.state?.get()||{},b=a==="global"?"_global":a.startsWith("_local.")?a:"_local",S=b==="_global"?m._global:b.startsWith("_local.")?this.getNestedProperty(m._local||{},b.slice(7)):m._local,v=f?.isEnabled()?f.startStateChange(b,S,d,{actionId:n.actionId,handlerType:"setState",source:`target=${a}`}):void 0,_=l==="replace"?"replace":l==="shallow"?"shallow":"deep",A=_!=="deep"?{...d,__mergeMode:_,...v?{__setStateId:v}:{}}:{...d,...v?{__setStateId:v}:{}};if(a==="global"){if(!this.globalStateUpdater){j.warn("Global state updater is not set"),v&&f&&f.completeStateChange(v);return}const x=h?.state?.get()||{},{__setStateId:L,__mergeMode:M,...k}=A,O=_==="deep"?this.deepMergeWithState(k,x):k,B={...O,...v?{__setStateId:v}:{},..._!=="deep"?{__mergeMode:_}:{}};return j.log("setState global:",B),this.globalStateUpdater(B,{render:c}),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"global",..._!=="deep"?{__mergeMode:_}:{},...O}}else if(a==="isolated"){const x=n.isolatedContext;if(x){const{__mergeMode:L,__setStateId:M,...k}=A;return x.mergeState(k,_),j.log("[handleSetState] isolated state updated:",k,"mergeMode:",_),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"isolated",...k}}else if(j.warn('[handleSetState] isolated target used but no IsolatedStateContext found. Make sure the component has "isolatedState" attribute. Falling back to local state.'),n.setState){const M=window.__g7PendingLocalState||n.state||{},k=_==="deep"?this.deepMergeWithState(A,M):A;return n.setState(k),v&&f&&setTimeout(()=>f.completeStateChange(v),0),k}else throw v&&f&&f.completeStateChange(v),new Ut("isolated target used but no IsolatedStateContext found and no setState function in context")}else if(a==="local"||a==="component"||a==="_local"){if(i==="parent"||i==="root"){const L=window.__g7LayoutContextStack||[];if(L.length>0){const M=i==="parent"?L[L.length-1]:L[0];if(M?.setState){const{__mergeMode:k,__setStateId:O,...B}=A,G=_==="deep"?this.deepMergeWithState(B,M.state||{}):B;return j.log(`[handleSetState] scope=${i}: 타겟 컨텍스트에 상태 업데이트`,G),M.setState(G),v&&f&&setTimeout(()=>f.completeStateChange(v),0),G}}j.warn(`[handleSetState] scope=${i}: 레이아웃 컨텍스트 스택이 비어있습니다. current로 폴백합니다.`)}const x=n.setState&&!n._isDispatchFallbackContext;if(j.log("[handleSetState] isRealComponentContext:",x,"context.setState:",!!n.setState,"_isDispatchFallbackContext:",n._isDispatchFallbackContext),x){j.log("[handleSetState] Using COMPONENT setState path"),j.log("[handleSetState] resolvedPayload:",A),j.log("[handleSetState] context.state:",n.state),j.log("[handleSetState] mergeMode:",_);const M=window.__g7PendingLocalState||n.state||{},k=_==="deep"?this.deepMergeWithState(A,{}):A;j.log("[handleSetState] convertedPayload (변경 필드만, dot notation 변환):",k),n.setState(k);const O=_==="deep"?this.deepMergeWithState(A,M):_==="shallow"?{...n.state||{},...A}:A,{__mergeMode:B,__setStateId:G,...P}=O;window.__g7PendingLocalState=P,j.log("[handleSetState] __g7PendingLocalState updated:",P),this.globalStateUpdater&&(this.globalStateUpdater({_local:P},{render:!1}),j.log("[handleSetState] _global._local synced (render:false):",P));const{__mergeMode:W,__setStateId:pe,...Se}=A;if(_==="replace")window.__g7ForcedLocalFields=Se;else{const we=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=this.deepMergeWithState(Se,we)}return j.log("[handleSetState] __g7ForcedLocalFields updated:",Se),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"local",...O}}else if(this.globalStateUpdater){j.log("[handleSetState] Using GLOBAL STATE UPDATER path for _local");const M=window.__g7PendingLocalState||m._local||{};j.log("[handleSetState] currentLocal (with pending):",M);const k=_==="deep"?this.deepMergeWithState(A,M):A;return j.log("[handleSetState] finalLocal (merged):",k),this.globalStateUpdater({_local:k},{render:c}),window.__g7PendingLocalState=k,j.log("[handleSetState] __g7PendingLocalState updated for globalStateUpdater path"),v&&f&&setTimeout(()=>f.completeStateChange(v),0),{__target:"local",...k}}else throw v&&f&&f.completeStateChange(v),new Ut("setState function is not provided in context and globalStateUpdater is not set")}else if(a.startsWith("_local.")){const x=a.slice(7),{__mergeMode:L,__setStateId:M,...k}=A,O=k.value!==void 0?k.value:Object.values(k)[0];if(n.setState){const G=window.__g7PendingLocalState||n.state||{},P=this.createNestedUpdate(x,O,G),W={...G,...P};if(n.setState(P),this.globalStateUpdater&&i!=="parent"&&i!=="root"){const pe=window.__templateApp?.getGlobalState?.()?._local,Se=pe&&typeof pe=="object"?{...pe,...P}:W;this.globalStateUpdater({_local:Se},{render:!1}),j.log("[handleSetState] _global._local synced for dot notation (render:false):",Se)}return v&&f&&setTimeout(()=>f.completeStateChange(v),0),W}else throw v&&f&&f.completeStateChange(v),new Ut("setState function is not provided in context")}else{if(!n.setState)throw v&&f&&f.completeStateChange(v),new Ut("setState function is not provided in context");const L=window.__g7PendingLocalState||n.state||{},M=_==="deep"?this.deepMergeWithState(A,L):A;return n.setState(M),v&&f&&setTimeout(()=>f.completeStateChange(v),0),M}}async handleParentScopeSetState(e,n,a,i){const l=Gn(),c=window.G7Core,d=a==="replace"?"replace":a==="shallow"?"shallow":"deep",f=e.startsWith("$parent."),h=f?"$parent.":"$root.",m=e.slice(h.length);let b,S;if(m==="_local"||m==="_global")b=m,S=void 0;else if(m.startsWith("_local."))b="_local",S=m.slice(7);else if(m.startsWith("_global."))b="_global",S=m.slice(8);else{j.warn(`[handleParentScopeSetState] 지원하지 않는 타겟 형식: ${e}. _local 또는 _global으로 시작해야 합니다.`);return}const v=window.__g7LayoutContextStack||[];if(v.length===0){j.warn(`[handleParentScopeSetState] 레이아웃 컨텍스트 스택이 비어있습니다. target=${e}`);return}const _=f?v[v.length-1]:v[0];if(!_){j.warn(`[handleParentScopeSetState] 타겟 컨텍스트를 찾을 수 없습니다. target=${e}`);return}const A=b==="_global"?_.state?._global:_.state?._local,x=l?.isEnabled()?l.startStateChange(e,A,n,{actionId:i.actionId,handlerType:"setState",source:`target=${e}`}):void 0,{__mergeMode:L,__setStateId:M,...k}=n;if(b==="_global"){if(!this.globalStateUpdater){j.warn("[handleParentScopeSetState] globalStateUpdater가 설정되지 않았습니다."),x&&l&&l.completeStateChange(x);return}const O=c?.state?.get()||{};if(S){const B=k.value!==void 0?k.value:Object.values(k)[0],G=this.createNestedUpdate(S,B,O),P={...O,...G};j.log(`[handleParentScopeSetState] ${e}: 전역 상태 중첩 경로 업데이트`,P),this.globalStateUpdater(P)}else{const B=d==="deep"?this.deepMergeWithState(k,O):k;j.log(`[handleParentScopeSetState] ${e}: 전역 상태 업데이트 (mergeMode=${d})`,B),this.globalStateUpdater(d!=="deep"?{...B,__mergeMode:d}:B)}return x&&l&&setTimeout(()=>l.completeStateChange(x),0),{__target:e,...k}}else{if(!_.setState){j.warn(`[handleParentScopeSetState] 타겟 컨텍스트에 setState가 없습니다. target=${e}`),x&&l&&l.completeStateChange(x);return}const O=_.state||{};if(S){const B=k.value!==void 0?k.value:Object.values(k)[0],G=this.createNestedUpdate(S,B,O),P={...O,...G};return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 중첩 경로 업데이트`,G),_.setState(G),_.state=P,_.dataContext&&(_.dataContext._local=P),ef(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),P}else{const B=d!=="deep"?{...k,__mergeMode:d}:k,G=d==="replace"?k:d==="shallow"?{...O,...k}:this.deepMergeWithState(k,O);return j.log(`[handleParentScopeSetState] ${e}: 로컬 상태 업데이트 (mergeMode=${d})`,G),_.setState(B),_.state=G,_.dataContext&&(_.dataContext._local=G),ef(),x&&l&&setTimeout(()=>l.completeStateChange(x),0),G}}}deepMergeWithState(e,n){const a=["errors"];let i=e;if("..."in e){const c=e["..."];if(c&&typeof c=="object"&&!Array.isArray(c)){const{"...":d,...f}=e;i={...c,...f}}else{const{"...":d,...f}=e;i=f}}const l={...n};for(const[c,d]of Object.entries(i)){if(a.includes(c)){l[c]=d;continue}if(c.includes(".")&&c!=="..."){const f=this.createNestedUpdate(c,d,l);this.deepMergeInto(l,f,l);continue}if(d!==null&&typeof d=="object"&&!Array.isArray(d)&&Object.getPrototypeOf(d)!==Object.prototype&&Object.getPrototypeOf(d)!==null){l[c]=d;continue}d!==null&&typeof d=="object"&&!Array.isArray(d)&&n[c]!==null&&typeof n[c]=="object"&&!Array.isArray(n[c])?l[c]=this.deepMergeWithState(d,n[c]):l[c]=d}return l}deepMergeInto(e,n,a){for(const[i,l]of Object.entries(n)){if(l===null){e[i]=l;continue}if(e[i]===null){e[i]=l;continue}typeof l=="object"&&!Array.isArray(l)&&e[i]!==void 0&&e[i]!==null&&typeof e[i]=="object"&&!Array.isArray(e[i])?(e[i]===a[i]&&(e[i]={...e[i]}),this.deepMergeInto(e[i],l,a[i]||{})):typeof l=="object"&&!Array.isArray(l)&&e[i]===void 0&&a[i]!==null&&typeof a[i]=="object"&&!Array.isArray(a[i])?e[i]={...a[i],...l}:e[i]=l}}createNestedUpdate(e,n,a){const i=e.split(".");if(i.length===1)return{[i[0]]:n};const l={};let c=l,d=a;for(let f=0;f<i.length-1;f++){const h=i[f],m=d?.[h],b=m!=null&&typeof m=="object"&&!Array.isArray(m);b?c[h]={...m}:c[h]={},c=c[h],d=b?m:{}}return c[i[i.length-1]]=n,l}buildNestedUpdate(e,n){const a=e.split(".");if(a.length===1)return{[a[0]]:n};const i={};let l=i;for(let c=0;c<a.length-1;c++)l[a[c]]={},l=l[a[c]];return l[a[a.length-1]]=n,i}async handleSetError(e,n,a){let i=e;i=this.resolveValue(i,a.data),this.translationEngine&&this.translationContext&&i.startsWith("$t:")&&(i=this.translationEngine.resolveTranslations(i,this.translationContext,a.data));const l=n?.stateTarget||"local";l==="isolated"&&a.isolatedContext?(a.isolatedContext.mergeState({apiError:i}),j.log("[handleSetError] Error set to isolated state:",i)):l==="global"&&this.globalStateUpdater?(this.globalStateUpdater({apiError:i}),j.log("[handleSetError] Error set to global state:",i)):a.setState?(a.setState({apiError:i}),j.log("[handleSetError] Error set to local state:",i)):j.warn("[handleSetError] Cannot set error: no state updater available")}async handleOpenModal(e,n){if(!this.globalStateUpdater){j.warn("Global state updater is not set for openModal");return}const a=n.data?._global?.modalStack||[];if(a.includes(e)){j.warn(`Modal "${e}" is already open`);return}const i=window.G7Core,l=n.setState&&n.state!==void 0,c=l?void 0:i?.state?.getLocal?.()||{},d=!l&&c!==void 0&&this.globalStateUpdater;if(l||d){const m=window.__g7LayoutContextStack||[],b=l?n.state:c,S=l?n.setState:v=>{this.globalStateUpdater({_local:v})};m.push({state:b,setState:S,dataContext:{_local:b||n.data?._local,_global:l?n.data?._global:n.data?._global||i?.state?.get?.(),_computed:n.data?._computed}}),window.__g7LayoutContextStack=m,j.log(`[handleOpenModal] 레이아웃 컨텍스트 스택에 push, 스택 크기: ${m.length}, state:`,b,l?"(direct)":"(fallback via G7Core)")}else j.warn(`[handleOpenModal] 컨텍스트 스택에 push 실패 - setState: ${!!n.setState}, state: ${n.state!==void 0}, G7Core: ${!!i?.state?.getLocal}`);const f=[...a,e];this.globalStateUpdater({modalStack:f,activeModal:e});const h=Gn();if(h?.isEnabled()){const m=a.length>0?a[a.length-1]:void 0;h.trackModalOpen?.({modalId:e,modalName:e,scopeType:"isolated",parentModalId:m,initialState:n.state?{...n.state}:{}})}}async handleCloseModal(e){if(!this.globalStateUpdater){j.warn("Global state updater is not set for closeModal");return}const n=e?.data?._global?.modalStack||[],a=n.length>0?n[n.length-1]:null,i=n.slice(0,-1),l=i.length>0?i[i.length-1]:null,c=window.__g7LayoutContextStack||[];if(c.length>0){const d=c[c.length-1];d&&(d.dataContext=void 0),c.pop(),window.__g7LayoutContextStack=c,j.log(`[handleCloseModal] 레이아웃 컨텍스트 스택에서 pop, 스택 크기: ${c.length}`)}if(a){const d=Gn();d?.isEnabled()&&d.trackModalClose?.(a,e?.state)}this.globalStateUpdater({modalStack:i,activeModal:l})}async handleShowAlert(e,n){let a=e;this.translationEngine&&this.translationContext&&e.startsWith("$t:")&&(a=this.translationEngine.resolveTranslations(e,this.translationContext,n.data)),alert(a)}async handleToast(e,n){const{type:a="info",message:i,icon:l,duration:c}=e;if(a==="error"){const f=n.data?.error;if((f?.error_code==="identity_verification_required"||f?.data?.error_code==="identity_verification_required")&&St.consumeDomainNoticeShown()){j.log("[Toast] IDV 도메인 안내 표출됨 — 중복 가드 토스트 1건 skip");return}}let d=i;if(this.translationEngine&&this.translationContext&&i?.startsWith("$t:")&&(d=this.translationEngine.resolveTranslations(i,this.translationContext,n.data)),this.globalStateUpdater){const h={id:`toast_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,type:a,message:d,...l&&{icon:l},...c&&{duration:c}},v=[...window.G7Core?.state?.get()?.toasts||[],h];this.globalStateUpdater({toasts:v})}else j.log(`[Toast ${a}] ${d}`)}async handleSwitch(e,n){const{cases:a}=e;if(!a){j.warn("switch handler requires cases property");return}const i=this.resolveParams(e.params,n.data);let l;if(i?.value!==void 0?(l=i.value,j.log("switch handler: using params.value as case key:",l)):(l=n.data?.$args?.[0],j.log("switch handler: using $args[0] as case key:",l)),l==null){if(a.default)return j.log("switch handler: case key is undefined, using default case"),await this.executeAction(a.default,n);j.warn("switch handler: case key is not defined and no default case");return}const c=String(l);let d=a[c];if(!d&&a.default&&(j.log(`switch handler: no case found for key "${c}", using default case`),d=a.default),!d){j.warn(`switch handler: no case found for key "${c}" and no default case`);return}return await this.executeAction(d,n)}async handleConditions(e,n){const{conditions:a}=e;if(!a||!Array.isArray(a)){j.warn("conditions handler requires conditions array");return}const i=n.data||{},l=Ug(a,i,this.bindingEngine);if(!l.matched){j.log("conditions handler: no matching branch found");return}const c=a[l.branchIndex];if(j.log(`conditions handler: matched branch ${l.branchIndex}`),!c.then){j.warn('conditions handler: matched branch has no "then" action');return}return Array.isArray(c.then)?await this.handleSequence({...e,actions:c.then},n):await this.executeAction(c.then,n)}handleStartInterval(e,n){const a=typeof e.id=="string"?e.id:"",i=Number(e.intervalMs??1e3),l=Array.isArray(e.actions)?e.actions:[];if(a===""||!l.length||i<=0)return j.warn("startInterval requires non-empty id, positive intervalMs, and actions array"),{success:!1};const c=this.intervals.get(a);c&&(clearInterval(c),this.intervals.delete(a));const d=setInterval(()=>{for(const f of l)this.executeAction(f,n).catch(h=>{j.error(`[startInterval:${a}] tick action failed`,h)})},i);return this.intervals.set(a,d),{success:!0,id:a}}handleStopInterval(e){const n=typeof e.id=="string"?e.id:"";if(n==="")return j.warn("stopInterval requires id parameter"),{success:!1};const a=this.intervals.get(n);return a&&(clearInterval(a),this.intervals.delete(n)),{success:!0,id:n}}stopAllIntervals(){for(const e of this.intervals.values())clearInterval(e);this.intervals.clear()}async handleSequence(e,n){const a=e.actions||e.params?.actions;if(!a||!Array.isArray(a)||a.length===0)return j.warn("sequence handler requires non-empty actions array"),[];const i=Gn(),l=i?.isEnabled()&&i.startSequenceExecution?.({eventType:e.type})||"";window.__g7SequenceLocalSync=void 0;const c=[];let d,f=n.state?{...n.state}:{},h=n.isolatedContext?.state?{...n.isolatedContext.state}:{},m=n.data?._computed||{};const b=window.G7Core,S=()=>({_global:b?.state?.get()||{},_local:{...f},_isolated:Object.keys(h).length>0?{...h}:void 0});for(let v=0;v<a.length;v++){const _=a[v],A=Date.now();l&&i?.isEnabled()&&i.captureSequenceActionBefore?.(l,v,_.handler||"unknown",_.params||{},S());const x={...n,state:f,data:{...n.data,_local:f,_computed:m,$computed:m,$prev:d,$results:[...c],_isolated:h},isolatedContext:n.isolatedContext?{...n.isolatedContext,state:h}:null};try{const L=await this.executeAction(_,x);if(d=L.data,c.push(L.data),_.handler==="setState"&&L.data!==void 0){if(L.data.__target==="global"){const{__target:k,__mergeMode:O,...B}=L.data;O==="replace"?f={...f,_global:B}:f={...f,_global:{...f._global||{},...B}},j.log("[handleSequence] global state synchronized (mergeMode=%s):",O||"deep",f._global)}else if(L.data.__target==="isolated"){const{__target:k,__mergeMode:O,...B}=L.data;O==="replace"?h=B:h={...h,...B},j.log("[handleSequence] isolated state synchronized (mergeMode=%s):",O||"deep",h)}else if(L.data.__target==="local"){const{__target:k,__mergeMode:O,__setStateId:B,...G}=L.data;O==="replace"?f=G:f={...f,...G},j.log("[handleSequence] local state synchronized (mergeMode=%s):",O||"deep",f)}else f=L.data,j.log("[handleSequence] state synchronized (legacy):",f);const M=n.data?._computedDefinitions;if(M&&Object.keys(M).length>0){const k={},O={...n.data,_local:f,_computed:k,$computed:k,_isolated:h};for(const[B,G]of Object.entries(M))if(typeof G=="string"){const P=G.trim();if(P.startsWith("{{")&&P.endsWith("}}"))try{const W=P.slice(2,-2).trim();k[B]=qn(W)?this.bindingEngine.evaluatePipeExpression(W,O,{skipCache:!0}):this.bindingEngine.evaluateExpression(W,O,{skipCache:!0})}catch{k[B]=m[B]}}m=k,j.log("[handleSequence] _computed recalculated after setState:",m)}}if(_.handler!=="setState"){const M=window.__g7SequenceLocalSync;if(M&&M!==f){f=M,window.__g7SequenceLocalSync=void 0,j.log("[handleSequence] currentState synchronized from __g7SequenceLocalSync after custom handler:",_.handler);const k=n.data?._computedDefinitions;if(k&&Object.keys(k).length>0){const O={},B={...n.data,_local:f,_computed:O,$computed:O,_isolated:h};for(const[G,P]of Object.entries(k))if(typeof P=="string"){const W=P.trim();if(W.startsWith("{{")&&W.endsWith("}}"))try{const pe=W.slice(2,-2).trim();O[G]=qn(pe)?this.bindingEngine.evaluatePipeExpression(pe,B,{skipCache:!0}):this.bindingEngine.evaluateExpression(pe,B,{skipCache:!0})}catch{O[G]=m[G]}}m=O,j.log("[handleSequence] _computed recalculated after custom handler:",m)}}}l&&i?.isEnabled()&&i.captureSequenceActionAfter?.(l,v,S(),Date.now()-A,L.data)}catch(L){throw l&&i?.isEnabled()&&(i.captureSequenceActionAfter?.(l,v,S(),Date.now()-A,void 0,L),i.endSequenceExecution?.(l,L)),j.error(`sequence handler: action[${v}] failed:`,L),L}}return window.__g7SequenceLocalSync=void 0,l&&i?.isEnabled()&&i.endSequenceExecution?.(l),c}async handleParallel(e,n){const a=e.actions||e.params?.actions;if(!a||!Array.isArray(a)||a.length===0)return j.warn("parallel handler requires non-empty actions array"),[];j.log(`parallel handler: executing ${a.length} actions in parallel`);const i=n.isolatedContext?{...n.isolatedContext,state:{...n.isolatedContext.state}}:null,l=a.map((f,h)=>{const m={...n,isolatedContext:i};return this.executeAction(f,m).catch(b=>{throw j.error(`parallel handler: action[${h}] failed:`,b),b})}),c=await Promise.allSettled(l),d=c.filter(f=>f.status==="rejected").length;return d>0&&j.warn(`parallel handler: ${d}/${a.length} actions failed`),c}async handleLoadScript(e,n,a){const{src:i,id:l,async:c=!0,defer:d=!1}=e;if(!i)throw new Ut('loadScript handler requires "src" parameter',n);const f=l||`script_${i.replace(/[^a-zA-Z0-9]/g,"_")}`;return bs.loadedScripts.has(f)?(j.log(`loadScript: script already loaded, skipping: ${f}`),n.onLoad&&await this.executeAction(n.onLoad,a),!0):document.getElementById(f)?(j.log(`loadScript: script element already exists: ${f}`),bs.loadedScripts.add(f),n.onLoad&&await this.executeAction(n.onLoad,a),!0):(j.log(`loadScript: loading script: ${i}`),new Promise((m,b)=>{const S=document.createElement("script");S.id=f,S.src=i,S.async=c,S.defer=d,S.onload=async()=>{if(j.log(`loadScript: script loaded successfully: ${f}`),bs.loadedScripts.add(f),n.onLoad)try{await this.executeAction(n.onLoad,a)}catch(v){j.error("loadScript: onLoad action failed:",v)}m(!0)},S.onerror=v=>{j.error(`loadScript: failed to load script: ${i}`,v),b(new Ut(`Failed to load script: ${i}`,n))},document.head.appendChild(S)}))}async handleCallExternal(e,n,a){const i=e.constructor,l=e.args||{},c=e.method,d=e.methodArgs||[],f=e.callbackEvent,h=e.embedTarget,m=e.callbackSetState,b=e.callbackAction;if(!i)throw new Ut('callExternal handler requires "constructor" parameter',n);const S=this.getNestedProperty(window,i);if(!S||typeof S!="function")throw new Ut(`Constructor not found or not a function: ${i}. Make sure the script is loaded first using loadScript handler.`,n);j.log(`callExternal: calling constructor ${i}`);const v={...l};for(const[A,x]of Object.entries(l))x===!0&&(v[A]=L=>{if(j.log(`callExternal: callback triggered for ${A}`,L),f&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(f,L),m&&a.setState){const M=B=>{const G={};for(const[P,W]of Object.entries(B))typeof W=="string"?G[P]=this.getNestedProperty(L,W):typeof W=="object"&&W!==null&&(G[P]=M(W));return G},k=M(m);j.log("callExternal: callbackSetState mapping result",k);const O=this.deepMergeWithState(k,a.state||{});j.log("callExternal: merged with existing state",O),a.setState(O)}else f&&a.setState&&a.setState({[`${f.replace(/:/g,"_")}_result`]:L});if(b){const M={...a,data:{...a.data,$event:L}},k=Array.isArray(b)?b:[b];for(const O of k)try{this.executeAction(O,M)}catch(B){j.error("callExternal: callbackAction failed:",B)}}});const _=new S(v);if(c&&typeof _[c]=="function"){if(j.log(`callExternal: calling method ${c}`),c==="embed"&&h){const A=document.querySelector(h);if(A)return _[c](A,...d);throw new Ut(`Embed target element not found: ${h}`,n)}return _[c](...d)}return _}async handleCallExternalEmbed(e,n,a){const i=e.constructor,l=e.args||{},c=e.callbackSetState,d=e.callbackEvent,f=e.layerClassName,h=e.callbackAction;if(!i)throw new Ut('callExternalEmbed handler requires "constructor" parameter',n);const m=this.getNestedProperty(window,i);if(!m||typeof m!="function")throw new Ut(`Constructor not found or not a function: ${i}. Make sure the script is loaded first.`,n);j.log(`callExternalEmbed: creating layer for ${i}`);const{layer:b,closeLayer:S}=this.createEmbedLayer(f),v={...l};for(const[A,x]of Object.entries(l))x===!0&&(v[A]=L=>{if(j.log(`callExternalEmbed: callback triggered for ${A}`,L),S(),d&&typeof window<"u"&&window.G7Core?.componentEvent&&window.G7Core.componentEvent.emit(d,L),c&&a.setState){const M=B=>{const G={};for(const[P,W]of Object.entries(B))typeof W=="string"?G[P]=this.getNestedProperty(L,W):typeof W=="object"&&W!==null&&(G[P]=M(W));return G},k=M(c);j.log("callExternalEmbed: callbackSetState mapping result",k);const O=this.deepMergeWithState(k,a.state||{});j.log("callExternalEmbed: merged with existing state",O),a.setState(O)}if(h){const M={...a,data:{...a.data,$event:L}},k=Array.isArray(h)?h:[h];for(const O of k)try{this.executeAction(O,M)}catch(B){j.error("callExternalEmbed: callbackAction failed:",B)}}});const _=new m(v);if(typeof _.embed=="function")_.embed(b),j.log("callExternalEmbed: embedded in layer");else throw S(),new Ut(`Constructor ${i} does not have embed method`,n);return _}isImeComposing(e){return e?.isComposing===!0||e?.keyCode===229}createEmbedLayer(e){const n=document.createElement("div");n.id="g7-embed-overlay",n.style.cssText=` position: fixed; inset: 0; background-color: rgba(0, 0, 0, 0.5); @@ -84,20 +50,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=()=>{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{return await this.executeAction(e,a)}catch(i){if(j.error("dispatchAction failed:",i),e.onError){const l=i instanceof It&&i.originalError?{message:i.originalError.message,response:i.originalError.response}:{message:i instanceof Error?i.message:String(i)},c={...a,data:{...a.data,error:l}},d=Array.isArray(e.onError)?e.onError:[e.onError];for(const f of d)await this.executeAction(f,c)}return{success:!1,error:i instanceof Error?i:new Error(String(i))}}}registerHandler(e,r,a){this.customHandlers.set(e,r);const i=Vn();i?.isEnabled()&&i.trackHandlerRegistration(e,a?.category??"custom",a?.description,a?.source)}unregisterHandler(e){this.customHandlers.delete(e);const r=Vn();r?.isEnabled()&&r.trackHandlerUnregistration(e)}getRegisteredHandlers(){return Array.from(this.customHandlers.keys())}getHandler(e){return this.customHandlers.get(e)}normalizeDebounceConfig(e){return typeof e=="number"?{delay:e,leading:!1,trailing:!0}:{delay:e.delay,leading:e.leading??!1,trailing:e.trailing??!0}}extractEventData(e){const r=e.target;if(r&&!("tagName"in r)){const l={type:e.type??"custom",target:{value:r.value,name:r.name??""}};return e._changedKeys&&(l._changedKeys=e._changedKeys),l}const i=r?{value:r.value,name:r.name??"",checked:r.checked??!1,type:r.type??"",tagName:r.tagName}:{};if(e.type==="scroll"&&r){const l=r;i.scrollHeight=l.scrollHeight??0,i.scrollTop=l.scrollTop??0,i.clientHeight=l.clientHeight??0,i.scrollLeft=l.scrollLeft??0,i.clientWidth=l.clientWidth??0,i.scrollWidth=l.scrollWidth??0}return{type:e.type,target:r?i:null}}executeDebouncedAction(e,r,a,i,l){const c={...a,$event:r,$args:l},d={type:r.type,target:r.target,preventDefault:()=>{},stopPropagation:()=>{}};this.createHandler(e,c,i)(d)}handleDebouncedAction(e,r,a,i,l,c){const d=this.normalizeDebounceConfig(e.debounce),f=l||`default-${e.handler}-${Date.now()}`,g=this.extractEventData(r),m=g._changedKeys;if(m&&Array.isArray(m)&&g?.target?.value&&typeof g.target.value=="object"&&!Array.isArray(g.target.value)){const v=this.debounceAccumulatedValues.get(f);if(v){const A={...v};for(const x of m)A[x]=g.target.value[x];g.target.value=A}this.debounceAccumulatedValues.set(f,g.target.value)}const y=this.debounceTimers.get(f);y&&(clearTimeout(y),this.debounceTimers.delete(f));const w=Vn();w?.isEnabled()&&w.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"pending",params:e.params,debounce:{delay:d.delay,status:"pending",scheduledAt:Date.now()}});const S=!this.debounceTimers.has(f+"_leading");if(d.leading&&S&&(this.debounceTimers.set(f+"_leading",setTimeout(()=>{},0)),this.executeDebouncedAction(e,g,a,i,c),w?.isEnabled()&&w.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"success",params:e.params,debounce:{delay:d.delay,status:"executed",executedAt:Date.now(),mode:"leading"}})),d.trailing){const v=()=>{this.executeDebouncedAction(e,g,a,i,c),w?.isEnabled()&&w.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"success",params:e.params,debounce:{delay:d.delay,status:"executed",executedAt:Date.now(),mode:"trailing"}})},A=setTimeout(()=>{this.debounceTimers.delete(f),this.debounceTimers.delete(f+"_leading"),this.pendingDebounceFlushers.delete(f),this.debounceAccumulatedValues.delete(f),v()},d.delay);this.debounceTimers.set(f,A),this.pendingDebounceFlushers.set(f,v)}}clearDebounceTimers(e){if(e)for(const[r,a]of this.debounceTimers)r.startsWith(e+"-")&&(clearTimeout(a),this.debounceTimers.delete(r),this.pendingDebounceFlushers.delete(r),this.debounceAccumulatedValues.delete(r));else{for(const r of this.debounceTimers.values())clearTimeout(r);this.debounceTimers.clear(),this.pendingDebounceFlushers.clear(),this.debounceAccumulatedValues.clear()}}debouncedCall(e,r,a){const i=this.debounceTimers.get(e);i&&(clearTimeout(i),this.debounceTimers.delete(e));const l=setTimeout(()=>{this.debounceTimers.delete(e),this.pendingDebounceFlushers.delete(e),a()},r);this.debounceTimers.set(e,l),this.pendingDebounceFlushers.set(e,a)}flushPendingDebounceTimers(){if(this.pendingDebounceFlushers.size===0)return;const e=new Map(this.pendingDebounceFlushers);for(const[r,a]of e){const i=this.debounceTimers.get(r);i&&(clearTimeout(i),this.debounceTimers.delete(r)),this.debounceTimers.delete(r+"_leading"),this.pendingDebounceFlushers.delete(r),a()}this.globalStateUpdater&&this.globalStateUpdater({})}bindActionsToProps(e,r,a){const i={...e};if(e.actions&&Array.isArray(e.actions)){const l=new Map;for(const m of e.actions){const y=this.resolveActionRef(m),w=y.event?this.normalizeEventPropName(y.event):this.getEventHandlerName(y.type);l.has(w)||l.set(w,[]),l.get(w).push(y)}const c=e.name||e.id||"unknown",d=l.has("onDrop"),f=l.has("onDragOver"),g=l.has("onDragEnter");d&&!f&&(i.onDragOver=m=>{m.preventDefault()}),d&&!g&&(i.onDragEnter=m=>{m.preventDefault()});for(const[m,y]of l)i[m]=(...w)=>{for(const S of y){const v=w[0],A=v&&typeof v=="object"&&"preventDefault"in v;if(A&&(m==="onDragOver"||m==="onDrop")&&v.preventDefault(),A&&S.key&&"key"in v){if(this.isImeComposing(v)){j.log("Key filter skipped (IME composing):",S.key);continue}if(v.key!==S.key){j.log("Key filter not matched:",S.key,"actual:",v.key);continue}j.log("Key filter matched:",S.key)}if(S.debounce){const x=`${c}-${S.handler}-${m}`,R=v&&typeof v=="object"&&!("preventDefault"in v)&&"target"in v&&v.target!==null;let M;if(A)M=v;else if(R){const L={type:"custom",target:v.target,preventDefault:()=>{},stopPropagation:()=>{}};v._changedKeys&&(L._changedKeys=v._changedKeys),M=L}else M=new Event("custom");const O=S.event?{...r,$args:w}:r;this.handleDebouncedAction(S,M,O,a,x,w)}else if(this.flushPendingDebounceTimers(),S.event){const x={...r,$args:w},R=this.resolveEventForHandler(v,A);this.createHandler(S,x,a)(R)}else if(A)this.createHandler(S,r,a)(v);else if(v&&typeof v=="object"&&!("preventDefault"in v)&&"target"in v&&v.target!==null){const R={type:"custom",target:v.target,preventDefault:()=>{},stopPropagation:()=>{}};this.createHandler(S,r,a)(R)}}}}return i}getEventHandlerName(e){return rm[e]||`on${e.charAt(0).toUpperCase()}${e.slice(1)}`}normalizeEventPropName(e){return rm[e]??e}resolveEventForHandler(e,r){if(r)return e;if(!(e&&typeof e=="object"&&!("preventDefault"in e)&&"target"in e&&e.target!==null))return new Event("custom");const i={type:"custom",target:e.target,preventDefault:()=>{},stopPropagation:()=>{}};return e._changedKeys&&(i._changedKeys=e._changedKeys),i}sanitizeForDevTools(e,r=5){const a=new WeakSet,i=(l,c)=>{if(l==null||typeof l=="string"||typeof l=="number"||typeof l=="boolean")return l;if(c>r)return"[Max Depth Exceeded]";if(typeof l=="function")return`[Function: ${l.name||"anonymous"}]`;if(l instanceof Element)return`[Element: ${l.tagName}${l.className?"."+l.className.split(" ").join("."):""}]`;if(l instanceof Event)return`[Event: ${l.type}]`;if(Array.isArray(l)){if(a.has(l))return"[Circular Array]";a.add(l);const d=l.slice(0,10).map(f=>i(f,c+1));return l.length>10&&d.push(`... ${l.length-10} more items`),d}if(typeof l=="object"){if(a.has(l))return"[Circular Reference]";a.add(l);const d={},f=Object.keys(l),g=f.slice(0,20);for(const m of g)try{d[m]=i(l[m],c+1)}catch{d[m]="[Error accessing property]"}return f.length>20&&(d["..."]=`${f.length-20} more properties`),d}return String(l)};return i(e,0)}};$(fs,"loadedScripts",new Set);let mo=fs,oc=null;function am(o){return oc||(oc=new mo(o)),oc}function P0(o){oc=o}const im=ut("TransitionManager");class q0{constructor(){$(this,"isPending",!1);$(this,"subscribers",new Set)}setPending(e){this.isPending!==e&&(im.log("setPending:",{from:this.isPending,to:e,stack:new Error().stack}),this.isPending=e,this.notifySubscribers())}getIsPending(){return this.isPending}subscribe(e){return this.subscribers.add(e),()=>{this.subscribers.delete(e)}}notifySubscribers(){im.log("notifySubscribers:",{isPending:this.isPending,subscriberCount:this.subscribers.size}),this.subscribers.forEach(e=>{e(this.isPending)})}clearSubscribers(){this.subscribers.clear()}}const zr=new q0,lc=ut("TransitionContext"),sm=N.createContext({isTransitioning:!1}),Id=({children:o})=>{const[e,r]=N.useState(()=>{const i=zr.getIsPending();return lc.log("Initial state:",i),i});N.useEffect(()=>{lc.log("Subscribing to TransitionManager");const i=zr.subscribe(l=>{lc.log("Received isPending update:",l),r(l)});return()=>{lc.log("Unsubscribing from TransitionManager"),i()}},[]);const a=N.useMemo(()=>({isTransitioning:e}),[e]);return dt.jsx(sm.Provider,{value:a,children:o})},om=()=>N.useContext(sm),cc="__g7LocalInitTracking";function Hd(){const o=globalThis;return o[cc]||(o[cc]={hash:"",timestamp:void 0,consumed:void 0}),o[cc]}function G0(){const o=globalThis;o[cc]={hash:"",timestamp:void 0,consumed:void 0}}function V0(o){Hd().consumed=o}function F0(o){return Hd().consumed===o}function K0(o){return o.globalTrackedKey!==o.trackingKey?"apply":o.instanceHandledKey===o.trackingKey?"skip":"prune"}function W0(o,e){return typeof o=="number"&&typeof e=="number"?Math.max(o,e):e??o}function Y0(o,e){if(e===void 0)return o;if(!(o!==null&&typeof o=="object"&&e!==null&&typeof e=="object"&&!F0(o)))return e;const{_forceLocalInit:a,...i}=o,{_forceLocalInit:l,...c}=e,d={...i,...c},f=W0(a,l);return f!==void 0&&(d._forceLocalInit=f),d}function X0(o){return o<768?"mobile":o<1024?"tablet":"desktop"}function lm(o){const e=X0(o);return{width:o,isMobile:e==="mobile",isTablet:e==="tablet",isDesktop:e==="desktop",matchedPreset:e}}const jd=N.createContext(lm(1024)),uc=({children:o,overrideWidth:e})=>{const[r,a]=N.useState(()=>mi.getWidth());N.useEffect(()=>{const c=mi.subscribe(d=>{a(d)});return()=>{c()}},[]);const i=e??r,l=N.useMemo(()=>lm(i),[i]);return dt.jsx(jd.Provider,{value:l,children:o})},zd=()=>N.useContext(jd);window.G7Core?.createLogger?.("Core:State");function J0(o,e,r,a){const{syncOnControlledChange:i=!0,isEqual:l}=a??{},c=o!==void 0;N.useRef(c);const[d,f]=N.useState(o!==void 0?o:e),g=d,m=N.useRef(o),y=N.useRef(!1);N.useEffect(()=>{if(y.current){m.current=o;return}i&&c&&o!==m.current&&(l?!l(d,o):d!==o)&&f(o),m.current=o},[o,c,i,l,d]);const w=N.useCallback(S=>{const v=typeof S=="function"?S(g):S;(l?l(g,v):g===v)||(y.current=!0,f(v),r?.(v),requestAnimationFrame(()=>{y.current=!1}))},[g,c,r,l]);return[g,w]}function Q0(o,e){return o===e?!0:o.length!==e.length?!1:o.every((r,a)=>r===e[a])}function cm(o,e){if(o===e)return!0;const r=Object.keys(o),a=Object.keys(e);return r.length!==a.length?!1:r.every(i=>o[i]===e[i])}const um=N.createContext({}),Z0=({children:o,value:e})=>dt.jsx(um.Provider,{value:e,children:o}),eC=()=>N.useContext(um),tC=(o,e)=>{if(!o)return;const r=e.split(".");let a=o;for(const i of r){if(a==null)return;a=a[i]}return a},nC=(o,e,r)=>{const a=e.split(".");if(a.length===1)return{...o,[a[0]]:r};const i={...o};let l=i;for(let c=0;c<a.length-1;c++){const d=a[c],f=a[c+1],g=/^\d+$/.test(f);Array.isArray(l[d])&&!g?l[d]={...l[d]}:Array.isArray(l[d])?l[d]=[...l[d]]:g?l[d]=Array.isArray(l[d])?[...l[d]]:[]:l[d]={...l[d]||{}},l=l[d]}return l[a[a.length-1]]=r,i},as=ut("IsolatedStateContext"),rC={state:{},setState:()=>{as.warn("IsolatedStateContext not available: setState called outside IsolatedStateProvider")},getState:()=>{},mergeState:()=>{as.warn("IsolatedStateContext not available: mergeState called outside IsolatedStateProvider")},stateRef:{current:{}},scopeId:""},dm=N.createContext(rC);let aC=0;function iC(){return`isolated-${++aC}-${Date.now().toString(36)}`}function fm(o,e){if(e==null)return o;if(typeof e!="object"||Array.isArray(e))return e;const r={...o};for(const a of Object.keys(e)){const i=e[a],l=o?.[a];i!==null&&typeof i=="object"&&!Array.isArray(i)&&l!==null&&typeof l=="object"&&!Array.isArray(l)?r[a]=fm(l,i):r[a]=i}return r}function sC(o,e){return e?e.split(".").reduce((r,a)=>r?.[a],o):o}function oC(o,e,r){const a=e.split("."),i={...o};let l=i;for(let c=0;c<a.length-1;c++){const d=a[c];l[d]={...l[d]},l=l[d]}return l[a[a.length-1]]=r,i}const lC=({initialState:o={},scopeId:e,children:r})=>{const i=N.useRef(e||iC()).current,[l,c]=N.useState(o),d=N.useRef(l);d.current=l;const f=N.useCallback(S=>sC(d.current,S),[]),g=N.useCallback((S,v)=>{c(A=>{const x=oC(A,S,v);return as.log(`[${i}] setState: ${S} =`,v),x})},[i]),m=N.useCallback((S,v="deep")=>{c(A=>{let x;return v==="replace"?x=S:v==="shallow"?x={...A,...S}:x=fm(A,S),as.log(`[${i}] mergeState (${v}):`,S),x})},[i]),y=N.useMemo(()=>({state:l,setState:g,getState:f,mergeState:m,stateRef:d,scopeId:i}),[l,g,f,m,i]),w=N.useMemo(()=>({state:d.current,setState:g,getState:f,mergeState:m}),[g,f,m]);return N.useEffect(()=>{const S=window.G7Core,v=window.__g7IsolatedStates||{};return window.__g7IsolatedStates||(window.__g7IsolatedStates=v),v[i]=w,S&&(S._isolatedStates||(S._isolatedStates={}),S._isolatedStates[i]=l),S?.devTools?.isEnabled?.()&&S.devTools.emit?.("isolated:created",{id:i,state:l}),as.log(`[${i}] IsolatedStateProvider mounted with initial state:`,l),()=>{delete v[i],S?._isolatedStates&&delete S._isolatedStates[i],S?.devTools?.isEnabled?.()&&S.devTools.emit?.("isolated:destroyed",{id:i}),as.log(`[${i}] IsolatedStateProvider unmounted`)}},[i,w]),N.useEffect(()=>{const S=window.G7Core,v=window.__g7IsolatedStates;v?.[i]&&(v[i].state=l),S?._isolatedStates&&(S._isolatedStates[i]=l),S?.devTools?.isEnabled?.()&&S.devTools.emit?.("isolated:updated",{id:i,state:l})},[l,i]),dt.jsx(dm.Provider,{value:y,children:r})},cC=()=>{const o=N.useContext(dm);return o.scopeId?o:null};var Oa=jg();function uC(){for(var o=arguments.length,e=new Array(o),r=0;r<o;r++)e[r]=arguments[r];return N.useMemo(()=>a=>{e.forEach(i=>i(a))},e)}const dc=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function is(o){const e=Object.prototype.toString.call(o);return e==="[object Window]"||e==="[object global]"}function Ud(o){return"nodeType"in o}function Dn(o){var e,r;return o?is(o)?o:Ud(o)&&(e=(r=o.ownerDocument)==null?void 0:r.defaultView)!=null?e:window:window}function Bd(o){const{Document:e}=Dn(o);return o instanceof e}function yo(o){return is(o)?!1:o instanceof Dn(o).HTMLElement}function hm(o){return o instanceof Dn(o).SVGElement}function ss(o){return o?is(o)?o.document:Ud(o)?Bd(o)?o:yo(o)||hm(o)?o.ownerDocument:document:document:document}const Ur=dc?N.useLayoutEffect:N.useEffect;function Pd(o){const e=N.useRef(o);return Ur(()=>{e.current=o}),N.useCallback(function(){for(var r=arguments.length,a=new Array(r),i=0;i<r;i++)a[i]=arguments[i];return e.current==null?void 0:e.current(...a)},[])}function dC(){const o=N.useRef(null),e=N.useCallback((a,i)=>{o.current=setInterval(a,i)},[]),r=N.useCallback(()=>{o.current!==null&&(clearInterval(o.current),o.current=null)},[]);return[e,r]}function bo(o,e){e===void 0&&(e=[o]);const r=N.useRef(o);return Ur(()=>{r.current!==o&&(r.current=o)},e),r}function vo(o,e){const r=N.useRef();return N.useMemo(()=>{const a=o(r.current);return r.current=a,a},[...e])}function fc(o){const e=Pd(o),r=N.useRef(null),a=N.useCallback(i=>{i!==r.current&&e?.(i,r.current),r.current=i},[]);return[r,a]}function qd(o){const e=N.useRef();return N.useEffect(()=>{e.current=o},[o]),e.current}let Gd={};function So(o,e){return N.useMemo(()=>{if(e)return e;const r=Gd[o]==null?0:Gd[o]+1;return Gd[o]=r,o+"-"+r},[o,e])}function gm(o){return function(e){for(var r=arguments.length,a=new Array(r>1?r-1:0),i=1;i<r;i++)a[i-1]=arguments[i];return a.reduce((l,c)=>{const d=Object.entries(c);for(const[f,g]of d){const m=l[f];m!=null&&(l[f]=m+o*g)}return l},{...e})}}const os=gm(1),wo=gm(-1);function fC(o){return"clientX"in o&&"clientY"in o}function Vd(o){if(!o)return!1;const{KeyboardEvent:e}=Dn(o.target);return e&&o instanceof e}function hC(o){if(!o)return!1;const{TouchEvent:e}=Dn(o.target);return e&&o instanceof e}function Fd(o){if(hC(o)){if(o.touches&&o.touches.length){const{clientX:e,clientY:r}=o.touches[0];return{x:e,y:r}}else if(o.changedTouches&&o.changedTouches.length){const{clientX:e,clientY:r}=o.changedTouches[0];return{x:e,y:r}}}return fC(o)?{x:o.clientX,y:o.clientY}:null}const Co=Object.freeze({Translate:{toString(o){if(!o)return;const{x:e,y:r}=o;return"translate3d("+(e?Math.round(e):0)+"px, "+(r?Math.round(r):0)+"px, 0)"}},Scale:{toString(o){if(!o)return;const{scaleX:e,scaleY:r}=o;return"scaleX("+e+") scaleY("+r+")"}},Transform:{toString(o){if(o)return[Co.Translate.toString(o),Co.Scale.toString(o)].join(" ")}},Transition:{toString(o){let{property:e,duration:r,easing:a}=o;return e+" "+r+"ms "+a}}}),pm="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function gC(o){return o.matches(pm)?o:o.querySelector(pm)}const pC={display:"none"};function mC(o){let{id:e,value:r}=o;return We.createElement("div",{id:e,style:pC},r)}function yC(o){let{id:e,announcement:r,ariaLiveType:a="assertive"}=o;const i={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return We.createElement("div",{id:e,style:i,role:"status","aria-live":a,"aria-atomic":!0},r)}function bC(){const[o,e]=N.useState("");return{announce:N.useCallback(a=>{a!=null&&e(a)},[]),announcement:o}}const mm=N.createContext(null);function vC(o){const e=N.useContext(mm);N.useEffect(()=>{if(!e)throw new Error("useDndMonitor must be used within a children of <DndContext>");return e(o)},[o,e])}function SC(){const[o]=N.useState(()=>new Set),e=N.useCallback(a=>(o.add(a),()=>o.delete(a)),[o]);return[N.useCallback(a=>{let{type:i,event:l}=a;o.forEach(c=>{var d;return(d=c[i])==null?void 0:d.call(c,l)})},[o]),e]}const wC={draggable:` + `,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 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,n){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&&n.setState&&n.setState({[l]:c}),c;let f;try{f=JSON.parse(d)}catch{f=d}return j.log(`loadFromLocalStorage: loaded ${a}`,f),l&&n.setState&&n.setState({[l]:f}),f}catch(d){return j.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){j.warn(`[Preview] Unknown action handler "${e.handler}" — skipped (preview mode)`);return}const i=new Ut(`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("{{")&&j.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=Ta(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")&&(j.log("[evaluateExpression] expression:",i),j.log("[evaluateExpression] dataContext._global:",l._global),j.log("[evaluateExpression] dataContext._global?.modules:",l._global?.modules));try{const c=qn(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,n):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,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{return await this.executeAction(e,a)}catch(i){if(j.error("dispatchAction failed:",i),e.onError){const l=i instanceof Ut&&i.originalError?{message:i.originalError.message,response:i.originalError.response}:{message:i instanceof Error?i.message:String(i)},c={...a,data:{...a.data,error:l}},d=Array.isArray(e.onError)?e.onError:[e.onError];for(const f of d)await this.executeAction(f,c)}return{success:!1,error:i instanceof Error?i:new Error(String(i))}}}registerHandler(e,n,a){this.customHandlers.set(e,n);const i=Gn();i?.isEnabled()&&i.trackHandlerRegistration(e,a?.category??"custom",a?.description,a?.source)}unregisterHandler(e){this.customHandlers.delete(e);const n=Gn();n?.isEnabled()&&n.trackHandlerUnregistration(e)}getRegisteredHandlers(){return Array.from(this.customHandlers.keys())}getHandler(e){return this.customHandlers.get(e)}normalizeDebounceConfig(e){return typeof e=="number"?{delay:e,leading:!1,trailing:!0}:{delay:e.delay,leading:e.leading??!1,trailing:e.trailing??!0}}extractEventData(e){const n=e.target;if(n&&!("tagName"in n)){const l={type:e.type??"custom",target:{value:n.value,name:n.name??""}};return e._changedKeys&&(l._changedKeys=e._changedKeys),l}const i=n?{value:n.value,name:n.name??"",checked:n.checked??!1,type:n.type??"",tagName:n.tagName}:{};if(e.type==="scroll"&&n){const l=n;i.scrollHeight=l.scrollHeight??0,i.scrollTop=l.scrollTop??0,i.clientHeight=l.clientHeight??0,i.scrollLeft=l.scrollLeft??0,i.clientWidth=l.clientWidth??0,i.scrollWidth=l.scrollWidth??0}return{type:e.type,target:n?i:null}}executeDebouncedAction(e,n,a,i,l){const c={...a,$event:n,$args:l},d={type:n.type,target:n.target,preventDefault:()=>{},stopPropagation:()=>{}};this.createHandler(e,c,i)(d)}handleDebouncedAction(e,n,a,i,l,c){const d=this.normalizeDebounceConfig(e.debounce),f=l||`default-${e.handler}-${Date.now()}`,h=this.extractEventData(n),m=h._changedKeys;if(m&&Array.isArray(m)&&h?.target?.value&&typeof h.target.value=="object"&&!Array.isArray(h.target.value)){const _=this.debounceAccumulatedValues.get(f);if(_){const A={..._};for(const x of m)A[x]=h.target.value[x];h.target.value=A}this.debounceAccumulatedValues.set(f,h.target.value)}const b=this.debounceTimers.get(f);b&&(clearTimeout(b),this.debounceTimers.delete(f));const S=Gn();S?.isEnabled()&&S.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"pending",params:e.params,debounce:{delay:d.delay,status:"pending",scheduledAt:Date.now()}});const v=!this.debounceTimers.has(f+"_leading");if(d.leading&&v&&(this.debounceTimers.set(f+"_leading",setTimeout(()=>{},0)),this.executeDebouncedAction(e,h,a,i,c),S?.isEnabled()&&S.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"success",params:e.params,debounce:{delay:d.delay,status:"executed",executedAt:Date.now(),mode:"leading"}})),d.trailing){const _=()=>{this.executeDebouncedAction(e,h,a,i,c),S?.isEnabled()&&S.trackAction({handler:typeof e.handler=="string"?e.handler:String(e.handler),type:e.type,status:"success",params:e.params,debounce:{delay:d.delay,status:"executed",executedAt:Date.now(),mode:"trailing"}})},A=setTimeout(()=>{this.debounceTimers.delete(f),this.debounceTimers.delete(f+"_leading"),this.pendingDebounceFlushers.delete(f),this.debounceAccumulatedValues.delete(f),_()},d.delay);this.debounceTimers.set(f,A),this.pendingDebounceFlushers.set(f,_)}}clearDebounceTimers(e){if(e)for(const[n,a]of this.debounceTimers)n.startsWith(e+"-")&&(clearTimeout(a),this.debounceTimers.delete(n),this.pendingDebounceFlushers.delete(n),this.debounceAccumulatedValues.delete(n));else{for(const n of this.debounceTimers.values())clearTimeout(n);this.debounceTimers.clear(),this.pendingDebounceFlushers.clear(),this.debounceAccumulatedValues.clear()}}debouncedCall(e,n,a){const i=this.debounceTimers.get(e);i&&(clearTimeout(i),this.debounceTimers.delete(e));const l=setTimeout(()=>{this.debounceTimers.delete(e),this.pendingDebounceFlushers.delete(e),a()},n);this.debounceTimers.set(e,l),this.pendingDebounceFlushers.set(e,a)}flushPendingDebounceTimers(){if(this.pendingDebounceFlushers.size===0)return;const e=new Map(this.pendingDebounceFlushers);for(const[n,a]of e){const i=this.debounceTimers.get(n);i&&(clearTimeout(i),this.debounceTimers.delete(n)),this.debounceTimers.delete(n+"_leading"),this.pendingDebounceFlushers.delete(n),a()}this.globalStateUpdater&&this.globalStateUpdater({})}bindActionsToProps(e,n,a){const i={...e};if(e.actions&&Array.isArray(e.actions)){const l=new Map;for(const m of e.actions){const b=this.resolveActionRef(m),S=b.event?this.normalizeEventPropName(b.event):this.getEventHandlerName(b.type);l.has(S)||l.set(S,[]),l.get(S).push(b)}const c=e.name||e.id||"unknown",d=l.has("onDrop"),f=l.has("onDragOver"),h=l.has("onDragEnter");d&&!f&&(i.onDragOver=m=>{m.preventDefault()}),d&&!h&&(i.onDragEnter=m=>{m.preventDefault()});for(const[m,b]of l)i[m]=(...S)=>{for(const v of b){const _=S[0],A=_&&typeof _=="object"&&"preventDefault"in _;if(A&&(m==="onDragOver"||m==="onDrop")&&_.preventDefault(),A&&v.key&&"key"in _){if(this.isImeComposing(_)){j.log("Key filter skipped (IME composing):",v.key);continue}if(_.key!==v.key){j.log("Key filter not matched:",v.key,"actual:",_.key);continue}j.log("Key filter matched:",v.key)}if(v.debounce){const x=`${c}-${v.handler}-${m}`,L=_&&typeof _=="object"&&!("preventDefault"in _)&&"target"in _&&_.target!==null;let M;if(A)M=_;else if(L){const O={type:"custom",target:_.target,preventDefault:()=>{},stopPropagation:()=>{}};_._changedKeys&&(O._changedKeys=_._changedKeys),M=O}else M=new Event("custom");const k=v.event?{...n,$args:S}:n;this.handleDebouncedAction(v,M,k,a,x,S)}else if(this.flushPendingDebounceTimers(),v.event){const x={...n,$args:S},L=this.resolveEventForHandler(_,A);this.createHandler(v,x,a)(L)}else if(A)this.createHandler(v,n,a)(_);else if(_&&typeof _=="object"&&!("preventDefault"in _)&&"target"in _&&_.target!==null){const L={type:"custom",target:_.target,preventDefault:()=>{},stopPropagation:()=>{}};this.createHandler(v,n,a)(L)}}}}return i}getEventHandlerName(e){return Lm[e]||`on${e.charAt(0).toUpperCase()}${e.slice(1)}`}normalizeEventPropName(e){return Lm[e]??e}resolveEventForHandler(e,n){if(n)return e;if(!(e&&typeof e=="object"&&!("preventDefault"in e)&&"target"in e&&e.target!==null))return new Event("custom");const i={type:"custom",target:e.target,preventDefault:()=>{},stopPropagation:()=>{}};return e._changedKeys&&(i._changedKeys=e._changedKeys),i}sanitizeForDevTools(e,n=5){const a=new WeakSet,i=(l,c)=>{if(l==null||typeof l=="string"||typeof l=="number"||typeof l=="boolean")return l;if(c>n)return"[Max Depth Exceeded]";if(typeof l=="function")return`[Function: ${l.name||"anonymous"}]`;if(l instanceof Element)return`[Element: ${l.tagName}${l.className?"."+l.className.split(" ").join("."):""}]`;if(l instanceof Event)return`[Event: ${l.type}]`;if(Array.isArray(l)){if(a.has(l))return"[Circular Array]";a.add(l);const d=l.slice(0,10).map(f=>i(f,c+1));return l.length>10&&d.push(`... ${l.length-10} more items`),d}if(typeof l=="object"){if(a.has(l))return"[Circular Reference]";a.add(l);const d={},f=Object.keys(l),h=f.slice(0,20);for(const m of h)try{d[m]=i(l[m],c+1)}catch{d[m]="[Error accessing property]"}return f.length>20&&(d["..."]=`${f.length-20} more properties`),d}return String(l)};return i(e,0)}};$(bs,"loadedScripts",new Set);let vo=bs,ac=null;function Mm(s){return ac||(ac=new vo(s)),ac}function PC(s){ac=s}const $m=dt("TransitionManager");class BC{constructor(){$(this,"isPending",!1);$(this,"subscribers",new Set)}setPending(e){this.isPending!==e&&($m.log("setPending:",{from:this.isPending,to:e,stack:new Error().stack}),this.isPending=e,this.notifySubscribers())}getIsPending(){return this.isPending}subscribe(e){return this.subscribers.add(e),()=>{this.subscribers.delete(e)}}notifySubscribers(){$m.log("notifySubscribers:",{isPending:this.isPending,subscriberCount:this.subscribers.size}),this.subscribers.forEach(e=>{e(this.isPending)})}clearSubscribers(){this.subscribers.clear()}}const zr=new BC,ic=dt("TransitionContext"),Nm=N.createContext({isTransitioning:!1}),tf=({children:s})=>{const[e,n]=N.useState(()=>{const i=zr.getIsPending();return ic.log("Initial state:",i),i});N.useEffect(()=>{ic.log("Subscribing to TransitionManager");const i=zr.subscribe(l=>{ic.log("Received isPending update:",l),n(l)});return()=>{ic.log("Unsubscribing from TransitionManager"),i()}},[]);const a=N.useMemo(()=>({isTransitioning:e}),[e]);return ft.jsx(Nm.Provider,{value:a,children:s})},Im=()=>N.useContext(Nm),sc="__g7LocalInitTracking";function nf(){const s=globalThis;return s[sc]||(s[sc]={hash:"",timestamp:void 0,consumed:void 0}),s[sc]}function qC(){const s=globalThis;s[sc]={hash:"",timestamp:void 0,consumed:void 0}}function GC(s){nf().consumed=s}function VC(s){return nf().consumed===s}function FC(s){return s.globalTrackedKey!==s.trackingKey?"apply":s.instanceHandledKey===s.trackingKey?"skip":"prune"}function KC(s,e){return typeof s=="number"&&typeof e=="number"?Math.max(s,e):e??s}function WC(s,e){if(e===void 0)return s;if(!(s!==null&&typeof s=="object"&&e!==null&&typeof e=="object"&&!VC(s)))return e;const{_forceLocalInit:a,...i}=s,{_forceLocalInit:l,...c}=e,d={...i,...c},f=KC(a,l);return f!==void 0&&(d._forceLocalInit=f),d}function YC(s){return s<768?"mobile":s<1024?"tablet":"desktop"}function jm(s){const e=YC(s);return{width:s,isMobile:e==="mobile",isTablet:e==="tablet",isDesktop:e==="desktop",matchedPreset:e}}const rf=N.createContext(jm(1024)),oc=({children:s,overrideWidth:e})=>{const[n,a]=N.useState(()=>mi.getWidth());N.useEffect(()=>{const c=mi.subscribe(d=>{a(d)});return()=>{c()}},[]);const i=e??n,l=N.useMemo(()=>jm(i),[i]);return ft.jsx(rf.Provider,{value:l,children:s})},af=()=>N.useContext(rf);window.G7Core?.createLogger?.("Core:State");function XC(s,e,n,a){const{syncOnControlledChange:i=!0,isEqual:l}=a??{},c=s!==void 0;N.useRef(c);const[d,f]=N.useState(s!==void 0?s:e),h=d,m=N.useRef(s),b=N.useRef(!1);N.useEffect(()=>{if(b.current){m.current=s;return}i&&c&&s!==m.current&&(l?!l(d,s):d!==s)&&f(s),m.current=s},[s,c,i,l,d]);const S=N.useCallback(v=>{const _=typeof v=="function"?v(h):v;(l?l(h,_):h===_)||(b.current=!0,f(_),n?.(_),requestAnimationFrame(()=>{b.current=!1}))},[h,c,n,l]);return[h,S]}function JC(s,e){return s===e?!0:s.length!==e.length?!1:s.every((n,a)=>n===e[a])}function Hm(s,e){if(s===e)return!0;const n=Object.keys(s),a=Object.keys(e);return n.length!==a.length?!1:n.every(i=>s[i]===e[i])}const zm=N.createContext({}),QC=({children:s,value:e})=>ft.jsx(zm.Provider,{value:e,children:s}),ZC=()=>N.useContext(zm),eE=(s,e)=>{if(!s)return;const n=e.split(".");let a=s;for(const i of n){if(a==null)return;a=a[i]}return a},tE=(s,e,n)=>{const a=e.split(".");if(a.length===1)return{...s,[a[0]]:n};const i={...s};let l=i;for(let c=0;c<a.length-1;c++){const d=a[c],f=a[c+1],h=/^\d+$/.test(f);Array.isArray(l[d])&&!h?l[d]={...l[d]}:Array.isArray(l[d])?l[d]=[...l[d]]:h?l[d]=Array.isArray(l[d])?[...l[d]]:[]:l[d]={...l[d]||{}},l=l[d]}return l[a[a.length-1]]=n,i},us=dt("IsolatedStateContext"),nE={state:{},setState:()=>{us.warn("IsolatedStateContext not available: setState called outside IsolatedStateProvider")},getState:()=>{},mergeState:()=>{us.warn("IsolatedStateContext not available: mergeState called outside IsolatedStateProvider")},stateRef:{current:{}},scopeId:""},Um=N.createContext(nE);let rE=0;function aE(){return`isolated-${++rE}-${Date.now().toString(36)}`}function Pm(s,e){if(e==null)return s;if(typeof e!="object"||Array.isArray(e))return e;const n={...s};for(const a of Object.keys(e)){const i=e[a],l=s?.[a];i!==null&&typeof i=="object"&&!Array.isArray(i)&&l!==null&&typeof l=="object"&&!Array.isArray(l)?n[a]=Pm(l,i):n[a]=i}return n}function iE(s,e){return e?e.split(".").reduce((n,a)=>n?.[a],s):s}function sE(s,e,n){const a=e.split("."),i={...s};let l=i;for(let c=0;c<a.length-1;c++){const d=a[c];l[d]={...l[d]},l=l[d]}return l[a[a.length-1]]=n,i}const oE=({initialState:s={},scopeId:e,children:n})=>{const i=N.useRef(e||aE()).current,[l,c]=N.useState(s),d=N.useRef(l);d.current=l;const f=N.useCallback(v=>iE(d.current,v),[]),h=N.useCallback((v,_)=>{c(A=>{const x=sE(A,v,_);return us.log(`[${i}] setState: ${v} =`,_),x})},[i]),m=N.useCallback((v,_="deep")=>{c(A=>{let x;return _==="replace"?x=v:_==="shallow"?x={...A,...v}:x=Pm(A,v),us.log(`[${i}] mergeState (${_}):`,v),x})},[i]),b=N.useMemo(()=>({state:l,setState:h,getState:f,mergeState:m,stateRef:d,scopeId:i}),[l,h,f,m,i]),S=N.useMemo(()=>({state:d.current,setState:h,getState:f,mergeState:m}),[h,f,m]);return N.useEffect(()=>{const v=window.G7Core,_=window.__g7IsolatedStates||{};return window.__g7IsolatedStates||(window.__g7IsolatedStates=_),_[i]=S,v&&(v._isolatedStates||(v._isolatedStates={}),v._isolatedStates[i]=l),v?.devTools?.isEnabled?.()&&v.devTools.emit?.("isolated:created",{id:i,state:l}),us.log(`[${i}] IsolatedStateProvider mounted with initial state:`,l),()=>{delete _[i],v?._isolatedStates&&delete v._isolatedStates[i],v?.devTools?.isEnabled?.()&&v.devTools.emit?.("isolated:destroyed",{id:i}),us.log(`[${i}] IsolatedStateProvider unmounted`)}},[i,S]),N.useEffect(()=>{const v=window.G7Core,_=window.__g7IsolatedStates;_?.[i]&&(_[i].state=l),v?._isolatedStates&&(v._isolatedStates[i]=l),v?.devTools?.isEnabled?.()&&v.devTools.emit?.("isolated:updated",{id:i,state:l})},[l,i]),ft.jsx(Um.Provider,{value:b,children:n})},lE=()=>{const s=N.useContext(Um);return s.scopeId?s:null};var Oa=cg();function cE(){for(var s=arguments.length,e=new Array(s),n=0;n<s;n++)e[n]=arguments[n];return N.useMemo(()=>a=>{e.forEach(i=>i(a))},e)}const lc=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function ds(s){const e=Object.prototype.toString.call(s);return e==="[object Window]"||e==="[object global]"}function sf(s){return"nodeType"in s}function Mn(s){var e,n;return s?ds(s)?s:sf(s)&&(e=(n=s.ownerDocument)==null?void 0:n.defaultView)!=null?e:window:window}function of(s){const{Document:e}=Mn(s);return s instanceof e}function So(s){return ds(s)?!1:s instanceof Mn(s).HTMLElement}function Bm(s){return s instanceof Mn(s).SVGElement}function fs(s){return s?ds(s)?s.document:sf(s)?of(s)?s:So(s)||Bm(s)?s.ownerDocument:document:document:document}const Ur=lc?N.useLayoutEffect:N.useEffect;function lf(s){const e=N.useRef(s);return Ur(()=>{e.current=s}),N.useCallback(function(){for(var n=arguments.length,a=new Array(n),i=0;i<n;i++)a[i]=arguments[i];return e.current==null?void 0:e.current(...a)},[])}function uE(){const s=N.useRef(null),e=N.useCallback((a,i)=>{s.current=setInterval(a,i)},[]),n=N.useCallback(()=>{s.current!==null&&(clearInterval(s.current),s.current=null)},[]);return[e,n]}function wo(s,e){e===void 0&&(e=[s]);const n=N.useRef(s);return Ur(()=>{n.current!==s&&(n.current=s)},e),n}function Co(s,e){const n=N.useRef();return N.useMemo(()=>{const a=s(n.current);return n.current=a,a},[...e])}function cc(s){const e=lf(s),n=N.useRef(null),a=N.useCallback(i=>{i!==n.current&&e?.(i,n.current),n.current=i},[]);return[n,a]}function cf(s){const e=N.useRef();return N.useEffect(()=>{e.current=s},[s]),e.current}let uf={};function Eo(s,e){return N.useMemo(()=>{if(e)return e;const n=uf[s]==null?0:uf[s]+1;return uf[s]=n,s+"-"+n},[s,e])}function qm(s){return function(e){for(var n=arguments.length,a=new Array(n>1?n-1:0),i=1;i<n;i++)a[i-1]=arguments[i];return a.reduce((l,c)=>{const d=Object.entries(c);for(const[f,h]of d){const m=l[f];m!=null&&(l[f]=m+s*h)}return l},{...e})}}const hs=qm(1),_o=qm(-1);function dE(s){return"clientX"in s&&"clientY"in s}function df(s){if(!s)return!1;const{KeyboardEvent:e}=Mn(s.target);return e&&s instanceof e}function fE(s){if(!s)return!1;const{TouchEvent:e}=Mn(s.target);return e&&s instanceof e}function ff(s){if(fE(s)){if(s.touches&&s.touches.length){const{clientX:e,clientY:n}=s.touches[0];return{x:e,y:n}}else if(s.changedTouches&&s.changedTouches.length){const{clientX:e,clientY:n}=s.changedTouches[0];return{x:e,y:n}}}return dE(s)?{x:s.clientX,y:s.clientY}:null}const Ao=Object.freeze({Translate:{toString(s){if(!s)return;const{x:e,y:n}=s;return"translate3d("+(e?Math.round(e):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(s){if(!s)return;const{scaleX:e,scaleY:n}=s;return"scaleX("+e+") scaleY("+n+")"}},Transform:{toString(s){if(s)return[Ao.Translate.toString(s),Ao.Scale.toString(s)].join(" ")}},Transition:{toString(s){let{property:e,duration:n,easing:a}=s;return e+" "+n+"ms "+a}}}),Gm="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function hE(s){return s.matches(Gm)?s:s.querySelector(Gm)}const pE={display:"none"};function gE(s){let{id:e,value:n}=s;return Ye.createElement("div",{id:e,style:pE},n)}function mE(s){let{id:e,announcement:n,ariaLiveType:a="assertive"}=s;const i={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return Ye.createElement("div",{id:e,style:i,role:"status","aria-live":a,"aria-atomic":!0},n)}function yE(){const[s,e]=N.useState("");return{announce:N.useCallback(a=>{a!=null&&e(a)},[]),announcement:s}}const Vm=N.createContext(null);function bE(s){const e=N.useContext(Vm);N.useEffect(()=>{if(!e)throw new Error("useDndMonitor must be used within a children of <DndContext>");return e(s)},[s,e])}function vE(){const[s]=N.useState(()=>new Set),e=N.useCallback(a=>(s.add(a),()=>s.delete(a)),[s]);return[N.useCallback(a=>{let{type:i,event:l}=a;s.forEach(c=>{var d;return(d=c[i])==null?void 0:d.call(c,l)})},[s]),e]}const SE={draggable:` 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. - `},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","overflowY"].some(i=>{const l=e[i];return typeof l=="string"?r.test(l):!1})}function gc(o,e){const r=[];function a(i){if(e!=null&&r.length>=e||!i)return r;if(Bd(i)&&i.scrollingElement!=null&&!r.includes(i.scrollingElement))return r.push(i.scrollingElement),r;if(!yo(i)||hm(i)||r.includes(i))return r;const l=Dn(o).getComputedStyle(i);return i!==o&&jC(i,l)&&r.push(i),HC(i,l)?r:a(i.parentNode)}return o?a(o):r}function Am(o){const[e]=gc(o,1);return e??null}function Kd(o){return!dc||!o?null:is(o)?o:Ud(o)?Bd(o)||o===ss(o).scrollingElement?window:yo(o)?o:null:null}function xm(o){return is(o)?o.scrollX:o.scrollLeft}function Tm(o){return is(o)?o.scrollY:o.scrollTop}function Wd(o){return{x:xm(o),y:Tm(o)}}var on;(function(o){o[o.Forward=1]="Forward",o[o.Backward=-1]="Backward"})(on||(on={}));function km(o){return!dc||!o?!1:o===document.scrollingElement}function Rm(o){const e={x:0,y:0},r=km(o)?{height:window.innerHeight,width:window.innerWidth}:{height:o.clientHeight,width:o.clientWidth},a={x:o.scrollWidth-r.width,y:o.scrollHeight-r.height},i=o.scrollTop<=e.y,l=o.scrollLeft<=e.x,c=o.scrollTop>=a.y,d=o.scrollLeft>=a.x;return{isTop:i,isLeft:l,isBottom:c,isRight:d,maxScroll:a,minScroll:e}}const zC={x:.2,y:.2};function UC(o,e,r,a,i){let{top:l,left:c,right:d,bottom:f}=r;a===void 0&&(a=10),i===void 0&&(i=zC);const{isTop:g,isBottom:m,isLeft:y,isRight:w}=Rm(o),S={x:0,y:0},v={x:0,y:0},A={height:e.height*i.y,width:e.width*i.x};return!g&&l<=e.top+A.height?(S.y=on.Backward,v.y=a*Math.abs((e.top+A.height-l)/A.height)):!m&&f>=e.bottom-A.height&&(S.y=on.Forward,v.y=a*Math.abs((e.bottom-A.height-f)/A.height)),!w&&d>=e.right-A.width?(S.x=on.Forward,v.x=a*Math.abs((e.right-A.width-d)/A.width)):!y&&c<=e.left+A.width&&(S.x=on.Backward,v.x=a*Math.abs((e.left+A.width-c)/A.width)),{direction:S,speed:v}}function BC(o){if(o===document.scrollingElement){const{innerWidth:l,innerHeight:c}=window;return{top:0,left:0,right:l,bottom:c,width:l,height:c}}const{top:e,left:r,right:a,bottom:i}=o.getBoundingClientRect();return{top:e,left:r,right:a,bottom:i,width:o.clientWidth,height:o.clientHeight}}function Dm(o){return o.reduce((e,r)=>os(e,Wd(r)),wr)}function PC(o){return o.reduce((e,r)=>e+xm(r),0)}function qC(o){return o.reduce((e,r)=>e+Tm(r),0)}function GC(o,e){if(e===void 0&&(e=ls),!o)return;const{top:r,left:a,bottom:i,right:l}=e(o);Am(o)&&(i<=0||l<=0||r>=window.innerHeight||a>=window.innerWidth)&&o.scrollIntoView({block:"center",inline:"center"})}const VC=[["x",["left","right"],PC],["y",["top","bottom"],qC]];class Yd{constructor(e,r){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const a=gc(r),i=Dm(a);this.rect={...e},this.width=e.width,this.height=e.height;for(const[l,c,d]of VC)for(const f of c)Object.defineProperty(this,f,{get:()=>{const g=d(a),m=i[l]-g;return this.rect[f]+m},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Eo{constructor(e){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(r=>{var a;return(a=this.target)==null?void 0:a.removeEventListener(...r)})},this.target=e}add(e,r,a){var i;(i=this.target)==null||i.addEventListener(e,r,a),this.listeners.push([e,r,a])}}function FC(o){const{EventTarget:e}=Dn(o);return o instanceof e?o:ss(o)}function Xd(o,e){const r=Math.abs(o.x),a=Math.abs(o.y);return typeof e=="number"?Math.sqrt(r**2+a**2)>e:"x"in e&&"y"in e?r>e.x&&a>e.y:"x"in e?r>e.x:"y"in e?a>e.y:!1}var ar;(function(o){o.Click="click",o.DragStart="dragstart",o.Keydown="keydown",o.ContextMenu="contextmenu",o.Resize="resize",o.SelectionChange="selectionchange",o.VisibilityChange="visibilitychange"})(ar||(ar={}));function Om(o){o.preventDefault()}function KC(o){o.stopPropagation()}var ot;(function(o){o.Space="Space",o.Down="ArrowDown",o.Right="ArrowRight",o.Left="ArrowLeft",o.Up="ArrowUp",o.Esc="Escape",o.Enter="Enter",o.Tab="Tab"})(ot||(ot={}));const Lm={start:[ot.Space,ot.Enter],cancel:[ot.Esc],end:[ot.Space,ot.Enter,ot.Tab]},WC=(o,e)=>{let{currentCoordinates:r}=e;switch(o.code){case ot.Right:return{...r,x:r.x+25};case ot.Left:return{...r,x:r.x-25};case ot.Down:return{...r,y:r.y+25};case ot.Up:return{...r,y:r.y-25}}};class Jd{constructor(e){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=e;const{event:{target:r}}=e;this.props=e,this.listeners=new Eo(ss(r)),this.windowListeners=new Eo(Dn(r)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(ar.Resize,this.handleCancel),this.windowListeners.add(ar.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(ar.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:e,onStart:r}=this.props,a=e.node.current;a&&GC(a),r(wr)}handleKeyDown(e){if(Vd(e)){const{active:r,context:a,options:i}=this.props,{keyboardCodes:l=Lm,coordinateGetter:c=WC,scrollBehavior:d="smooth"}=i,{code:f}=e;if(l.end.includes(f)){this.handleEnd(e);return}if(l.cancel.includes(f)){this.handleCancel(e);return}const{collisionRect:g}=a.current,m=g?{x:g.left,y:g.top}:wr;this.referenceCoordinates||(this.referenceCoordinates=m);const y=c(e,{active:r,context:a.current,currentCoordinates:m});if(y){const w=wo(y,m),S={x:0,y:0},{scrollableAncestors:v}=a.current;for(const A of v){const x=e.code,{isTop:R,isRight:M,isLeft:O,isBottom:L,maxScroll:q,minScroll:G}=Rm(A),B=BC(A),K={x:Math.min(x===ot.Right?B.right-B.width/2:B.right,Math.max(x===ot.Right?B.left:B.left+B.width/2,y.x)),y:Math.min(x===ot.Down?B.bottom-B.height/2:B.bottom,Math.max(x===ot.Down?B.top:B.top+B.height/2,y.y))},fe=x===ot.Right&&!M||x===ot.Left&&!O,Ce=x===ot.Down&&!L||x===ot.Up&&!R;if(fe&&K.x!==y.x){const ke=A.scrollLeft+w.x,Ue=x===ot.Right&&ke<=q.x||x===ot.Left&&ke>=G.x;if(Ue&&!w.y){A.scrollTo({left:ke,behavior:d});return}Ue?S.x=A.scrollLeft-ke:S.x=x===ot.Right?A.scrollLeft-q.x:A.scrollLeft-G.x,S.x&&A.scrollBy({left:-S.x,behavior:d});break}else if(Ce&&K.y!==y.y){const ke=A.scrollTop+w.y,Ue=x===ot.Down&&ke<=q.y||x===ot.Up&&ke>=G.y;if(Ue&&!w.x){A.scrollTo({top:ke,behavior:d});return}Ue?S.y=A.scrollTop-ke:S.y=x===ot.Down?A.scrollTop-q.y:A.scrollTop-G.y,S.y&&A.scrollBy({top:-S.y,behavior:d});break}}this.handleMove(e,os(wo(y,this.referenceCoordinates),S))}}}handleMove(e,r){const{onMove:a}=this.props;e.preventDefault(),a(r)}handleEnd(e){const{onEnd:r}=this.props;e.preventDefault(),this.detach(),r()}handleCancel(e){const{onCancel:r}=this.props;e.preventDefault(),this.detach(),r()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}Jd.activators=[{eventName:"onKeyDown",handler:(o,e,r)=>{let{keyboardCodes:a=Lm,onActivation:i}=e,{active:l}=r;const{code:c}=o.nativeEvent;if(a.start.includes(c)){const d=l.activatorNode.current;return d&&o.target!==d?!1:(o.preventDefault(),i?.({event:o.nativeEvent}),!0)}return!1}}];function Mm(o){return!!(o&&"distance"in o)}function $m(o){return!!(o&&"delay"in o)}class Qd{constructor(e,r,a){var i;a===void 0&&(a=FC(e.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=e,this.events=r;const{event:l}=e,{target:c}=l;this.props=e,this.events=r,this.document=ss(c),this.documentListeners=new Eo(this.document),this.listeners=new Eo(a),this.windowListeners=new Eo(Dn(c)),this.initialCoordinates=(i=Fd(l))!=null?i:wr,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:e,props:{options:{activationConstraint:r,bypassActivationConstraint:a}}}=this;if(this.listeners.add(e.move.name,this.handleMove,{passive:!1}),this.listeners.add(e.end.name,this.handleEnd),e.cancel&&this.listeners.add(e.cancel.name,this.handleCancel),this.windowListeners.add(ar.Resize,this.handleCancel),this.windowListeners.add(ar.DragStart,Om),this.windowListeners.add(ar.VisibilityChange,this.handleCancel),this.windowListeners.add(ar.ContextMenu,Om),this.documentListeners.add(ar.Keydown,this.handleKeydown),r){if(a!=null&&a({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if($m(r)){this.timeoutId=setTimeout(this.handleStart,r.delay),this.handlePending(r);return}if(Mm(r)){this.handlePending(r);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(e,r){const{active:a,onPending:i}=this.props;i(a,e,this.initialCoordinates,r)}handleStart(){const{initialCoordinates:e}=this,{onStart:r}=this.props;e&&(this.activated=!0,this.documentListeners.add(ar.Click,KC,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(ar.SelectionChange,this.removeTextSelection),r(e))}handleMove(e){var r;const{activated:a,initialCoordinates:i,props:l}=this,{onMove:c,options:{activationConstraint:d}}=l;if(!i)return;const f=(r=Fd(e))!=null?r:wr,g=wo(i,f);if(!a&&d){if(Mm(d)){if(d.tolerance!=null&&Xd(g,d.tolerance))return this.handleCancel();if(Xd(g,d.distance))return this.handleStart()}if($m(d)&&Xd(g,d.tolerance))return this.handleCancel();this.handlePending(d,g);return}e.cancelable&&e.preventDefault(),c(f)}handleEnd(){const{onAbort:e,onEnd:r}=this.props;this.detach(),this.activated||e(this.props.active),r()}handleCancel(){const{onAbort:e,onCancel:r}=this.props;this.detach(),this.activated||e(this.props.active),r()}handleKeydown(e){e.code===ot.Esc&&this.handleCancel()}removeTextSelection(){var e;(e=this.document.getSelection())==null||e.removeAllRanges()}}const YC={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class Zd extends Qd{constructor(e){const{event:r}=e,a=ss(r.target);super(e,YC,a)}}Zd.activators=[{eventName:"onPointerDown",handler:(o,e)=>{let{nativeEvent:r}=o,{onActivation:a}=e;return!r.isPrimary||r.button!==0?!1:(a?.({event:r}),!0)}}];const XC={move:{name:"mousemove"},end:{name:"mouseup"}};var ef;(function(o){o[o.RightClick=2]="RightClick"})(ef||(ef={}));class JC extends Qd{constructor(e){super(e,XC,ss(e.event.target))}}JC.activators=[{eventName:"onMouseDown",handler:(o,e)=>{let{nativeEvent:r}=o,{onActivation:a}=e;return r.button===ef.RightClick?!1:(a?.({event:r}),!0)}}];const tf={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class QC extends Qd{constructor(e){super(e,tf)}static setup(){return window.addEventListener(tf.move.name,e,{capture:!1,passive:!1}),function(){window.removeEventListener(tf.move.name,e)};function e(){}}}QC.activators=[{eventName:"onTouchStart",handler:(o,e)=>{let{nativeEvent:r}=o,{onActivation:a}=e;const{touches:i}=r;return i.length>1?!1:(a?.({event:r}),!0)}}];var _o;(function(o){o[o.Pointer=0]="Pointer",o[o.DraggableRect=1]="DraggableRect"})(_o||(_o={}));var pc;(function(o){o[o.TreeOrder=0]="TreeOrder",o[o.ReversedTreeOrder=1]="ReversedTreeOrder"})(pc||(pc={}));function ZC(o){let{acceleration:e,activator:r=_o.Pointer,canScroll:a,draggingRect:i,enabled:l,interval:c=5,order:d=pc.TreeOrder,pointerCoordinates:f,scrollableAncestors:g,scrollableAncestorRects:m,delta:y,threshold:w}=o;const S=tE({delta:y,disabled:!l}),[v,A]=dC(),x=N.useRef({x:0,y:0}),R=N.useRef({x:0,y:0}),M=N.useMemo(()=>{switch(r){case _o.Pointer:return f?{top:f.y,bottom:f.y,left:f.x,right:f.x}:null;case _o.DraggableRect:return i}},[r,i,f]),O=N.useRef(null),L=N.useCallback(()=>{const G=O.current;if(!G)return;const B=x.current.x*R.current.x,K=x.current.y*R.current.y;G.scrollBy(B,K)},[]),q=N.useMemo(()=>d===pc.TreeOrder?[...g].reverse():g,[d,g]);N.useEffect(()=>{if(!l||!g.length||!M){A();return}for(const G of q){if(a?.(G)===!1)continue;const B=g.indexOf(G),K=m[B];if(!K)continue;const{direction:fe,speed:Ce}=UC(G,K,M,e,w);for(const ke of["x","y"])S[ke][fe[ke]]||(Ce[ke]=0,fe[ke]=0);if(Ce.x>0||Ce.y>0){A(),O.current=G,v(L,c),x.current=Ce,R.current=fe;return}}x.current={x:0,y:0},R.current={x:0,y:0},A()},[e,L,a,A,l,c,JSON.stringify(M),JSON.stringify(S),v,g,q,m,JSON.stringify(w)])}const eE={x:{[on.Backward]:!1,[on.Forward]:!1},y:{[on.Backward]:!1,[on.Forward]:!1}};function tE(o){let{delta:e,disabled:r}=o;const a=qd(e);return vo(i=>{if(r||!a||!i)return eE;const l={x:Math.sign(e.x-a.x),y:Math.sign(e.y-a.y)};return{x:{[on.Backward]:i.x[on.Backward]||l.x===-1,[on.Forward]:i.x[on.Forward]||l.x===1},y:{[on.Backward]:i.y[on.Backward]||l.y===-1,[on.Forward]:i.y[on.Forward]||l.y===1}}},[r,e,a])}function nE(o,e){const r=e!=null?o.get(e):void 0,a=r?r.node.current:null;return vo(i=>{var l;return e==null?null:(l=a??i)!=null?l:null},[a,e])}function rE(o,e){return N.useMemo(()=>o.reduce((r,a)=>{const{sensor:i}=a,l=i.activators.map(c=>({eventName:c.eventName,handler:e(c.handler,a)}));return[...r,...l]},[]),[o,e])}var Ao;(function(o){o[o.Always=0]="Always",o[o.BeforeDragging=1]="BeforeDragging",o[o.WhileDragging=2]="WhileDragging"})(Ao||(Ao={}));var nf;(function(o){o.Optimized="optimized"})(nf||(nf={}));const Nm=new Map;function aE(o,e){let{dragging:r,dependencies:a,config:i}=e;const[l,c]=N.useState(null),{frequency:d,measure:f,strategy:g}=i,m=N.useRef(o),y=x(),w=bo(y),S=N.useCallback(function(R){R===void 0&&(R=[]),!w.current&&c(M=>M===null?R:M.concat(R.filter(O=>!M.includes(O))))},[w]),v=N.useRef(null),A=vo(R=>{if(y&&!r)return Nm;if(!R||R===Nm||m.current!==o||l!=null){const M=new Map;for(let O of o){if(!O)continue;if(l&&l.length>0&&!l.includes(O.id)&&O.rect.current){M.set(O.id,O.rect.current);continue}const L=O.node.current,q=L?new Yd(f(L),L):null;O.rect.current=q,q&&M.set(O.id,q)}return M}return R},[o,l,r,y,f]);return N.useEffect(()=>{m.current=o},[o]),N.useEffect(()=>{y||S()},[r,y]),N.useEffect(()=>{l&&l.length>0&&c(null)},[JSON.stringify(l)]),N.useEffect(()=>{y||typeof d!="number"||v.current!==null||(v.current=setTimeout(()=>{S(),v.current=null},d))},[d,y,S,...a]),{droppableRects:A,measureDroppableContainers:S,measuringScheduled:l!=null};function x(){switch(g){case Ao.Always:return!1;case Ao.BeforeDragging:return r;default:return!r}}}function Im(o,e){return vo(r=>o?r||(typeof e=="function"?e(o):o):null,[e,o])}function iE(o,e){return Im(o,e)}function sE(o){let{callback:e,disabled:r}=o;const a=Pd(e),i=N.useMemo(()=>{if(r||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:l}=window;return new l(a)},[a,r]);return N.useEffect(()=>()=>i?.disconnect(),[i]),i}function mc(o){let{callback:e,disabled:r}=o;const a=Pd(e),i=N.useMemo(()=>{if(r||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:l}=window;return new l(a)},[r]);return N.useEffect(()=>()=>i?.disconnect(),[i]),i}function oE(o){return new Yd(ls(o),o)}function Hm(o,e,r){e===void 0&&(e=oE);const[a,i]=N.useState(null);function l(){i(f=>{if(!o)return null;if(o.isConnected===!1){var g;return(g=f??r)!=null?g:null}const m=e(o);return JSON.stringify(f)===JSON.stringify(m)?f:m})}const c=sE({callback(f){if(o)for(const g of f){const{type:m,target:y}=g;if(m==="childList"&&y instanceof HTMLElement&&y.contains(o)){l();break}}}}),d=mc({callback:l});return Ur(()=>{l(),o?(d?.observe(o),c?.observe(document.body,{childList:!0,subtree:!0})):(d?.disconnect(),c?.disconnect())},[o]),a}function lE(o){const e=Im(o);return Em(o,e)}const jm=[];function cE(o){const e=N.useRef(o),r=vo(a=>o?a&&a!==jm&&o&&e.current&&o.parentNode===e.current.parentNode?a:gc(o):jm,[o]);return N.useEffect(()=>{e.current=o},[o]),r}function uE(o){const[e,r]=N.useState(null),a=N.useRef(o),i=N.useCallback(l=>{const c=Kd(l.target);c&&r(d=>d?(d.set(c,Wd(c)),new Map(d)):null)},[]);return N.useEffect(()=>{const l=a.current;if(o!==l){c(l);const d=o.map(f=>{const g=Kd(f);return g?(g.addEventListener("scroll",i,{passive:!0}),[g,Wd(g)]):null}).filter(f=>f!=null);r(d.length?new Map(d):null),a.current=o}return()=>{c(o),c(l)};function c(d){d.forEach(f=>{const g=Kd(f);g?.removeEventListener("scroll",i)})}},[i,o]),N.useMemo(()=>o.length?e?Array.from(e.values()).reduce((l,c)=>os(l,c),wr):Dm(o):wr,[o,e])}function zm(o,e){e===void 0&&(e=[]);const r=N.useRef(null);return N.useEffect(()=>{r.current=null},e),N.useEffect(()=>{const a=o!==wr;a&&!r.current&&(r.current=o),!a&&r.current&&(r.current=null)},[o]),r.current?wo(o,r.current):wr}function dE(o){N.useEffect(()=>{if(!dc)return;const e=o.map(r=>{let{sensor:a}=r;return a.setup==null?void 0:a.setup()});return()=>{for(const r of e)r?.()}},o.map(e=>{let{sensor:r}=e;return r}))}function fE(o,e){return N.useMemo(()=>o.reduce((r,a)=>{let{eventName:i,handler:l}=a;return r[i]=c=>{l(c,e)},r},{}),[o,e])}function Um(o){return N.useMemo(()=>o?IC(o):null,[o])}const Bm=[];function hE(o,e){e===void 0&&(e=ls);const[r]=o,a=Um(r?Dn(r):null),[i,l]=N.useState(Bm);function c(){l(()=>o.length?o.map(f=>km(f)?a:new Yd(e(f),f)):Bm)}const d=mc({callback:c});return Ur(()=>{d?.disconnect(),c(),o.forEach(f=>d?.observe(f))},[o]),i}function gE(o){if(!o)return null;if(o.children.length>1)return o;const e=o.children[0];return yo(e)?e:o}function pE(o){let{measure:e}=o;const[r,a]=N.useState(null),i=N.useCallback(g=>{for(const{target:m}of g)if(yo(m)){a(y=>{const w=e(m);return y?{...y,width:w.width,height:w.height}:w});break}},[e]),l=mc({callback:i}),c=N.useCallback(g=>{const m=gE(g);l?.disconnect(),m&&l?.observe(m),a(m?e(m):null)},[e,l]),[d,f]=fc(c);return N.useMemo(()=>({nodeRef:d,rect:r,setRef:f}),[r,d,f])}const mE=[{sensor:Zd,options:{}},{sensor:Jd,options:{}}],yE={current:{}},yc={draggable:{measure:_m},droppable:{measure:_m,strategy:Ao.WhileDragging,frequency:nf.Optimized},dragOverlay:{measure:ls}};class xo extends Map{get(e){var r;return e!=null&&(r=super.get(e))!=null?r:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(e=>{let{disabled:r}=e;return!r})}getNodeFor(e){var r,a;return(r=(a=this.get(e))==null?void 0:a.node.current)!=null?r:void 0}}const bE={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new xo,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:hc},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:yc,measureDroppableContainers:hc,windowRect:null,measuringScheduled:!1},vE={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:hc,draggableNodes:new Map,over:null,measureDroppableContainers:hc},bc=N.createContext(vE),Pm=N.createContext(bE);function SE(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new xo}}}function wE(o,e){switch(e.type){case tn.DragStart:return{...o,draggable:{...o.draggable,initialCoordinates:e.initialCoordinates,active:e.active}};case tn.DragMove:return o.draggable.active==null?o:{...o,draggable:{...o.draggable,translate:{x:e.coordinates.x-o.draggable.initialCoordinates.x,y:e.coordinates.y-o.draggable.initialCoordinates.y}}};case tn.DragEnd:case tn.DragCancel:return{...o,draggable:{...o.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case tn.RegisterDroppable:{const{element:r}=e,{id:a}=r,i=new xo(o.droppable.containers);return i.set(a,r),{...o,droppable:{...o.droppable,containers:i}}}case tn.SetDroppableDisabled:{const{id:r,key:a,disabled:i}=e,l=o.droppable.containers.get(r);if(!l||a!==l.key)return o;const c=new xo(o.droppable.containers);return c.set(r,{...l,disabled:i}),{...o,droppable:{...o.droppable,containers:c}}}case tn.UnregisterDroppable:{const{id:r,key:a}=e,i=o.droppable.containers.get(r);if(!i||a!==i.key)return o;const l=new xo(o.droppable.containers);return l.delete(r),{...o,droppable:{...o.droppable,containers:l}}}default:return o}}function CE(o){let{disabled:e}=o;const{active:r,activatorEvent:a,draggableNodes:i}=N.useContext(bc),l=qd(a),c=qd(r?.id);return N.useEffect(()=>{if(!e&&!a&&l&&c!=null){if(!Vd(l)||document.activeElement===l.target)return;const d=i.get(c);if(!d)return;const{activatorNode:f,node:g}=d;if(!f.current&&!g.current)return;requestAnimationFrame(()=>{for(const m of[f.current,g.current]){if(!m)continue;const y=gC(m);if(y){y.focus();break}}})}},[a,e,i,c,l]),null}function EE(o,e){let{transform:r,...a}=e;return o!=null&&o.length?o.reduce((i,l)=>l({transform:i,...a}),r):r}function _E(o){return N.useMemo(()=>({draggable:{...yc.draggable,...o?.draggable},droppable:{...yc.droppable,...o?.droppable},dragOverlay:{...yc.dragOverlay,...o?.dragOverlay}}),[o?.draggable,o?.droppable,o?.dragOverlay])}function AE(o){let{activeNode:e,measure:r,initialRect:a,config:i=!0}=o;const l=N.useRef(!1),{x:c,y:d}=typeof i=="boolean"?{x:i,y:i}:i;Ur(()=>{if(!c&&!d||!e){l.current=!1;return}if(l.current||!a)return;const g=e?.node.current;if(!g||g.isConnected===!1)return;const m=r(g),y=Em(m,a);if(c||(y.x=0),d||(y.y=0),l.current=!0,Math.abs(y.x)>0||Math.abs(y.y)>0){const w=Am(g);w&&w.scrollBy({top:y.y,left:y.x})}},[e,c,d,a,r])}const qm=N.createContext({...wr,scaleX:1,scaleY:1});var La;(function(o){o[o.Uninitialized=0]="Uninitialized",o[o.Initializing=1]="Initializing",o[o.Initialized=2]="Initialized"})(La||(La={}));const xE=N.memo(function(e){var r,a,i,l;let{id:c,accessibility:d,autoScroll:f=!0,children:g,sensors:m=mE,collisionDetection:y=RC,measuring:w,modifiers:S,...v}=e;const A=N.useReducer(wE,void 0,SE),[x,R]=A,[M,O]=SC(),[L,q]=N.useState(La.Uninitialized),G=L===La.Initialized,{draggable:{active:B,nodes:K,translate:fe},droppable:{containers:Ce}}=x,ke=B!=null?K.get(B):null,Ue=N.useRef({initial:null,translated:null}),Pe=N.useMemo(()=>{var ue;return B!=null?{id:B,data:(ue=ke?.data)!=null?ue:yE,rect:Ue}:null},[B,ke]),qe=N.useRef(null),[wt,J]=N.useState(null),[he,Ee]=N.useState(null),Z=bo(v,Object.values(v)),pe=So("DndDescribedBy",c),H=N.useMemo(()=>Ce.getEnabled(),[Ce]),T=_E(w),{droppableRects:ge,measureDroppableContainers:de,measuringScheduled:me}=aE(H,{dragging:G,dependencies:[fe.x,fe.y],config:T.droppable}),ae=nE(K,B),Re=N.useMemo(()=>he?Fd(he):null,[he]),ze=ve(),$e=iE(ae,T.draggable.measure);AE({activeNode:B!=null?K.get(B):null,config:ze.layoutShiftCompensation,initialRect:$e,measure:T.draggable.measure});const Ge=Hm(ae,T.draggable.measure,$e),At=Hm(ae?ae.parentElement:null),yt=N.useRef({activatorEvent:null,active:null,activeNode:ae,collisionRect:null,collisions:null,droppableRects:ge,draggableNodes:K,draggingNode:null,draggingNodeRect:null,droppableContainers:Ce,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),dn=Ce.getNodeFor((r=yt.current.over)==null?void 0:r.id),Tt=pE({measure:T.dragOverlay.measure}),Yt=(a=Tt.nodeRef.current)!=null?a:ae,ln=G?(i=Tt.rect)!=null?i:Ge:null,zt=!!(Tt.nodeRef.current&&Tt.rect),Tr=lE(zt?null:Ge),wn=Um(Yt?Dn(Yt):null),cn=cE(G?dn??ae:null),fn=hE(cn),sr=EE(S,{transform:{x:fe.x-Tr.x,y:fe.y-Tr.y,scaleX:1,scaleY:1},activatorEvent:he,active:Pe,activeNodeRect:Ge,containerNodeRect:At,draggingNodeRect:ln,over:yt.current.over,overlayNodeRect:Tt.rect,scrollableAncestors:cn,scrollableAncestorRects:fn,windowRect:wn}),Wn=Re?os(Re,fe):null,Ut=uE(cn),bt=zm(Ut),ra=zm(Ut,[Ge]),Ln=os(sr,bt),An=ln?LC(ln,sr):null,xn=Pe&&An?y({active:Pe,collisionRect:An,droppableRects:ge,droppableContainers:H,pointerCoordinates:Wn}):null,Mn=wm(xn,"id"),[hn,aa]=N.useState(null),$n=zt?sr:os(sr,ra),Vt=DC($n,(l=hn?.rect)!=null?l:null,Ge),Cn=N.useRef(null),nn=N.useCallback((ue,we)=>{let{sensor:_e,options:Be}=we;if(qe.current==null)return;const xe=K.get(qe.current);if(!xe)return;const De=ue.nativeEvent,Ae=new _e({active:qe.current,activeNode:xe,event:De,options:Be,context:yt,onAbort(Ne){if(!K.get(Ne))return;const{onDragAbort:Se}=Z.current,Me={id:Ne};Se?.(Me),M({type:"onDragAbort",event:Me})},onPending(Ne,je,Se,Me){if(!K.get(Ne))return;const{onDragPending:kt}=Z.current,Ot={id:Ne,constraint:je,initialCoordinates:Se,offset:Me};kt?.(Ot),M({type:"onDragPending",event:Ot})},onStart(Ne){const je=qe.current;if(je==null)return;const Se=K.get(je);if(!Se)return;const{onDragStart:Me}=Z.current,Ze={activatorEvent:De,active:{id:je,data:Se.data,rect:Ue}};Oa.unstable_batchedUpdates(()=>{Me?.(Ze),q(La.Initializing),R({type:tn.DragStart,initialCoordinates:Ne,active:je}),M({type:"onDragStart",event:Ze}),J(Cn.current),Ee(De)})},onMove(Ne){R({type:tn.DragMove,coordinates:Ne})},onEnd:et(tn.DragEnd),onCancel:et(tn.DragCancel)});Cn.current=Ae;function et(Ne){return async function(){const{active:Se,collisions:Me,over:Ze,scrollAdjustedTranslate:kt}=yt.current;let Ot=null;if(Se&&kt){const{cancelDrop:Nn}=Z.current;Ot={activatorEvent:De,active:Se,collisions:Me,delta:kt,over:Ze},Ne===tn.DragEnd&&typeof Nn=="function"&&await Promise.resolve(Nn(Ot))&&(Ne=tn.DragCancel)}qe.current=null,Oa.unstable_batchedUpdates(()=>{R({type:Ne}),q(La.Uninitialized),aa(null),J(null),Ee(null),Cn.current=null;const Nn=Ne===tn.DragEnd?"onDragEnd":"onDragCancel";if(Ot){const $t=Z.current[Nn];$t?.(Ot),M({type:Nn,event:Ot})}})}}},[K]),V=N.useCallback((ue,we)=>(_e,Be)=>{const xe=_e.nativeEvent,De=K.get(Be);if(qe.current!==null||!De||xe.dndKit||xe.defaultPrevented)return;const Ae={active:De};ue(_e,we.options,Ae)===!0&&(xe.dndKit={capturedBy:we.sensor},qe.current=Be,nn(_e,we))},[K,nn]),oe=rE(m,V);dE(m),Ur(()=>{Ge&&L===La.Initializing&&q(La.Initialized)},[Ge,L]),N.useEffect(()=>{const{onDragMove:ue}=Z.current,{active:we,activatorEvent:_e,collisions:Be,over:xe}=yt.current;if(!we||!_e)return;const De={active:we,activatorEvent:_e,collisions:Be,delta:{x:Ln.x,y:Ln.y},over:xe};Oa.unstable_batchedUpdates(()=>{ue?.(De),M({type:"onDragMove",event:De})})},[Ln.x,Ln.y]),N.useEffect(()=>{const{active:ue,activatorEvent:we,collisions:_e,droppableContainers:Be,scrollAdjustedTranslate:xe}=yt.current;if(!ue||qe.current==null||!we||!xe)return;const{onDragOver:De}=Z.current,Ae=Be.get(Mn),et=Ae&&Ae.rect.current?{id:Ae.id,rect:Ae.rect.current,data:Ae.data,disabled:Ae.disabled}:null,Ne={active:ue,activatorEvent:we,collisions:_e,delta:{x:xe.x,y:xe.y},over:et};Oa.unstable_batchedUpdates(()=>{aa(et),De?.(Ne),M({type:"onDragOver",event:Ne})})},[Mn]),Ur(()=>{yt.current={activatorEvent:he,active:Pe,activeNode:ae,collisionRect:An,collisions:xn,droppableRects:ge,draggableNodes:K,draggingNode:Yt,draggingNodeRect:ln,droppableContainers:Ce,over:hn,scrollableAncestors:cn,scrollAdjustedTranslate:Ln},Ue.current={initial:ln,translated:An}},[Pe,ae,xn,An,K,Yt,ln,ge,Ce,hn,cn,Ln]),ZC({...ze,delta:fe,draggingRect:An,pointerCoordinates:Wn,scrollableAncestors:cn,scrollableAncestorRects:fn});const ce=N.useMemo(()=>({active:Pe,activeNode:ae,activeNodeRect:Ge,activatorEvent:he,collisions:xn,containerNodeRect:At,dragOverlay:Tt,draggableNodes:K,droppableContainers:Ce,droppableRects:ge,over:hn,measureDroppableContainers:de,scrollableAncestors:cn,scrollableAncestorRects:fn,measuringConfiguration:T,measuringScheduled:me,windowRect:wn}),[Pe,ae,Ge,he,xn,At,Tt,K,Ce,ge,hn,de,cn,fn,T,me,wn]),X=N.useMemo(()=>({activatorEvent:he,activators:oe,active:Pe,activeNodeRect:Ge,ariaDescribedById:{draggable:pe},dispatch:R,draggableNodes:K,over:hn,measureDroppableContainers:de}),[he,oe,Pe,Ge,R,pe,K,hn,de]);return We.createElement(mm.Provider,{value:O},We.createElement(bc.Provider,{value:X},We.createElement(Pm.Provider,{value:ce},We.createElement(qm.Provider,{value:Vt},g)),We.createElement(CE,{disabled:d?.restoreFocus===!1})),We.createElement(EC,{...d,hiddenTextDescribedById:pe}));function ve(){const ue=wt?.autoScrollEnabled===!1,we=typeof f=="object"?f.enabled===!1:f===!1,_e=G&&!ue&&!we;return typeof f=="object"?{...f,enabled:_e}:{enabled:_e}}}),TE=N.createContext(null),Gm="button",kE="Draggable";function RE(o){let{id:e,data:r,disabled:a=!1,attributes:i}=o;const l=So(kE),{activators:c,activatorEvent:d,active:f,activeNodeRect:g,ariaDescribedById:m,draggableNodes:y,over:w}=N.useContext(bc),{role:S=Gm,roleDescription:v="draggable",tabIndex:A=0}=i??{},x=f?.id===e,R=N.useContext(x?qm:TE),[M,O]=fc(),[L,q]=fc(),G=fE(c,e),B=bo(r);Ur(()=>(y.set(e,{id:e,key:l,node:M,activatorNode:L,data:B}),()=>{const fe=y.get(e);fe&&fe.key===l&&y.delete(e)}),[y,e]);const K=N.useMemo(()=>({role:S,tabIndex:A,"aria-disabled":a,"aria-pressed":x&&S===Gm?!0:void 0,"aria-roledescription":v,"aria-describedby":m.draggable}),[a,S,A,x,v,m.draggable]);return{active:f,activatorEvent:d,activeNodeRect:g,attributes:K,isDragging:x,listeners:a?void 0:G,node:M,over:w,setNodeRef:O,setActivatorNodeRef:q,transform:R}}function DE(){return N.useContext(Pm)}const OE="Droppable",LE={timeout:25};function ME(o){let{data:e,disabled:r=!1,id:a,resizeObserverConfig:i}=o;const l=So(OE),{active:c,dispatch:d,over:f,measureDroppableContainers:g}=N.useContext(bc),m=N.useRef({disabled:r}),y=N.useRef(!1),w=N.useRef(null),S=N.useRef(null),{disabled:v,updateMeasurementsFor:A,timeout:x}={...LE,...i},R=bo(A??a),M=N.useCallback(()=>{if(!y.current){y.current=!0;return}S.current!=null&&clearTimeout(S.current),S.current=setTimeout(()=>{g(Array.isArray(R.current)?R.current:[R.current]),S.current=null},x)},[x]),O=mc({callback:M,disabled:v||!c}),L=N.useCallback((K,fe)=>{O&&(fe&&(O.unobserve(fe),y.current=!1),K&&O.observe(K))},[O]),[q,G]=fc(L),B=bo(e);return N.useEffect(()=>{!O||!q.current||(O.disconnect(),y.current=!1,O.observe(q.current))},[q,O]),N.useEffect(()=>(d({type:tn.RegisterDroppable,element:{id:a,key:l,disabled:r,node:q,rect:w,data:B}}),()=>d({type:tn.UnregisterDroppable,key:l,id:a})),[a]),N.useEffect(()=>{r!==m.current.disabled&&(d({type:tn.SetDroppableDisabled,id:a,key:l,disabled:r}),m.current.disabled=r)},[a,l,r,d]),{active:c,rect:w,isOver:f?.id===a,node:q,over:f,setNodeRef:G}}function rf(o,e,r){const a=o.slice();return a.splice(r<0?a.length+r:r,0,a.splice(e,1)[0]),a}function $E(o,e){return o.reduce((r,a,i)=>{const l=e.get(a);return l&&(r[i]=l),r},Array(o.length))}function vc(o){return o!==null&&o>=0}function NE(o,e){if(o===e)return!0;if(o.length!==e.length)return!1;for(let r=0;r<o.length;r++)if(o[r]!==e[r])return!1;return!0}function IE(o){return typeof o=="boolean"?{draggable:o,droppable:o}:o}const Sc={scaleX:1,scaleY:1},HE=o=>{var e;let{rects:r,activeNodeRect:a,activeIndex:i,overIndex:l,index:c}=o;const d=(e=r[i])!=null?e:a;if(!d)return null;const f=jE(r,c,i);if(c===i){const g=r[l];return g?{x:i<l?g.left+g.width-(d.left+d.width):g.left-d.left,y:0,...Sc}:null}return c>i&&c<=l?{x:-d.width-f,y:0,...Sc}:c<i&&c>=l?{x:d.width+f,y:0,...Sc}:{x:0,y:0,...Sc}};function jE(o,e,r){const a=o[e],i=o[e-1],l=o[e+1];return!a||!i&&!l?0:r<e?i?a.left-(i.left+i.width):l.left-(a.left+a.width):l?l.left-(a.left+a.width):a.left-(i.left+i.width)}const af=o=>{let{rects:e,activeIndex:r,overIndex:a,index:i}=o;const l=rf(e,a,r),c=e[i],d=l[i];return!d||!c?null:{x:d.left-c.left,y:d.top-c.top,scaleX:d.width/c.width,scaleY:d.height/c.height}},wc={scaleX:1,scaleY:1},zE=o=>{var e;let{activeIndex:r,activeNodeRect:a,index:i,rects:l,overIndex:c}=o;const d=(e=l[r])!=null?e:a;if(!d)return null;if(i===r){const g=l[c];return g?{x:0,y:r<c?g.top+g.height-(d.top+d.height):g.top-d.top,...wc}:null}const f=UE(l,i,r);return i>r&&i<=c?{x:0,y:-d.height-f,...wc}:i<r&&i>=c?{x:0,y:d.height+f,...wc}:{x:0,y:0,...wc}};function UE(o,e,r){const a=o[e],i=o[e-1],l=o[e+1];return a?r<e?i?a.top-(i.top+i.height):l?l.top-(a.top+a.height):0:l?l.top-(a.top+a.height):i?a.top-(i.top+i.height):0:0}const Vm="Sortable",Fm=We.createContext({activeIndex:-1,containerId:Vm,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:af,disabled:{draggable:!1,droppable:!1}});function BE(o){let{children:e,id:r,items:a,strategy:i=af,disabled:l=!1}=o;const{active:c,dragOverlay:d,droppableRects:f,over:g,measureDroppableContainers:m}=DE(),y=So(Vm,r),w=d.rect!==null,S=N.useMemo(()=>a.map(G=>typeof G=="object"&&"id"in G?G.id:G),[a]),v=c!=null,A=c?S.indexOf(c.id):-1,x=g?S.indexOf(g.id):-1,R=N.useRef(S),M=!NE(S,R.current),O=x!==-1&&A===-1||M,L=IE(l);Ur(()=>{M&&v&&m(S)},[M,S,v,m]),N.useEffect(()=>{R.current=S},[S]);const q=N.useMemo(()=>({activeIndex:A,containerId:y,disabled:L,disableTransforms:O,items:S,overIndex:x,useDragOverlay:w,sortedRects:$E(S,f),strategy:i}),[A,y,L.draggable,L.droppable,O,S,x,f,w,i]);return We.createElement(Fm.Provider,{value:q},e)}const PE=o=>{let{id:e,items:r,activeIndex:a,overIndex:i}=o;return rf(r,a,i).indexOf(e)},qE=o=>{let{containerId:e,isSorting:r,wasDragging:a,index:i,items:l,newIndex:c,previousItems:d,previousContainerId:f,transition:g}=o;return!g||!a||d!==l&&i===c?!1:r?!0:c!==i&&e===f},GE={duration:200,easing:"ease"},Km="transform",VE=Co.Transition.toString({property:Km,duration:0,easing:"linear"}),FE={roleDescription:"sortable"};function KE(o){let{disabled:e,index:r,node:a,rect:i}=o;const[l,c]=N.useState(null),d=N.useRef(r);return Ur(()=>{if(!e&&r!==d.current&&a.current){const f=i.current;if(f){const g=ls(a.current,{ignoreTransform:!0}),m={x:f.left-g.left,y:f.top-g.top,scaleX:f.width/g.width,scaleY:f.height/g.height};(m.x||m.y)&&c(m)}}r!==d.current&&(d.current=r)},[e,r,a,i]),N.useEffect(()=>{l&&c(null)},[l]),l}function WE(o){let{animateLayoutChanges:e=qE,attributes:r,disabled:a,data:i,getNewIndex:l=PE,id:c,strategy:d,resizeObserverConfig:f,transition:g=GE}=o;const{items:m,containerId:y,activeIndex:w,disabled:S,disableTransforms:v,sortedRects:A,overIndex:x,useDragOverlay:R,strategy:M}=N.useContext(Fm),O=YE(a,S),L=m.indexOf(c),q=N.useMemo(()=>({sortable:{containerId:y,index:L,items:m},...i}),[y,i,L,m]),G=N.useMemo(()=>m.slice(m.indexOf(c)),[m,c]),{rect:B,node:K,isOver:fe,setNodeRef:Ce}=ME({id:c,data:q,disabled:O.droppable,resizeObserverConfig:{updateMeasurementsFor:G,...f}}),{active:ke,activatorEvent:Ue,activeNodeRect:Pe,attributes:qe,setNodeRef:wt,listeners:J,isDragging:he,over:Ee,setActivatorNodeRef:Z,transform:pe}=RE({id:c,data:q,attributes:{...FE,...r},disabled:O.draggable}),H=uC(Ce,wt),T=!!ke,ge=T&&!v&&vc(w)&&vc(x),de=!R&&he,me=de&&ge?pe:null,Re=ge?me??(d??M)({rects:A,activeNodeRect:Pe,activeIndex:w,overIndex:x,index:L}):null,ze=vc(w)&&vc(x)?l({id:c,items:m,activeIndex:w,overIndex:x}):L,$e=ke?.id,Ge=N.useRef({activeId:$e,items:m,newIndex:ze,containerId:y}),At=m!==Ge.current.items,yt=e({active:ke,containerId:y,isDragging:he,isSorting:T,id:c,index:L,items:m,newIndex:Ge.current.newIndex,previousItems:Ge.current.items,previousContainerId:Ge.current.containerId,transition:g,wasDragging:Ge.current.activeId!=null}),dn=KE({disabled:!yt,index:L,node:K,rect:B});return N.useEffect(()=>{T&&Ge.current.newIndex!==ze&&(Ge.current.newIndex=ze),y!==Ge.current.containerId&&(Ge.current.containerId=y),m!==Ge.current.items&&(Ge.current.items=m)},[T,ze,y,m]),N.useEffect(()=>{if($e===Ge.current.activeId)return;if($e!=null&&Ge.current.activeId==null){Ge.current.activeId=$e;return}const Yt=setTimeout(()=>{Ge.current.activeId=$e},50);return()=>clearTimeout(Yt)},[$e]),{active:ke,activeIndex:w,attributes:qe,data:q,rect:B,index:L,newIndex:ze,items:m,isOver:fe,isSorting:T,isDragging:he,listeners:J,node:K,overIndex:x,over:Ee,setNodeRef:H,setActivatorNodeRef:Z,setDroppableNodeRef:Ce,setDraggableNodeRef:wt,transform:dn??Re,transition:Tt()};function Tt(){if(dn||At&&Ge.current.newIndex===L)return VE;if(!(de&&!Vd(Ue)||!g)&&(T||yt))return Co.Transition.toString({...g,property:Km})}}function YE(o,e){var r,a;return typeof o=="boolean"?{draggable:o,droppable:!1}:{draggable:(r=o?.draggable)!=null?r:e.draggable,droppable:(a=o?.droppable)!=null?a:e.droppable}}function Cc(o){if(!o)return!1;const e=o.data.current;return!!(e&&"sortable"in e&&typeof e.sortable=="object"&&"containerId"in e.sortable&&"items"in e.sortable&&"index"in e.sortable)}const XE=[ot.Down,ot.Right,ot.Up,ot.Left],JE=(o,e)=>{let{context:{active:r,collisionRect:a,droppableRects:i,droppableContainers:l,over:c,scrollableAncestors:d}}=e;if(XE.includes(o.code)){if(o.preventDefault(),!r||!a)return;const f=[];l.getEnabled().forEach(y=>{if(!y||y!=null&&y.disabled)return;const w=i.get(y.id);if(w)switch(o.code){case ot.Down:a.top<w.top&&f.push(y);break;case ot.Up:a.top>w.top&&f.push(y);break;case ot.Left:a.left>w.left&&f.push(y);break;case ot.Right:a.left<w.left&&f.push(y);break}});const g=TC({collisionRect:a,droppableRects:i,droppableContainers:f});let m=wm(g,"id");if(m===c?.id&&g.length>1&&(m=g[1].id),m!=null){const y=l.get(r.id),w=l.get(m),S=w?i.get(w.id):null,v=w?.node.current;if(v&&S&&y&&w){const x=gc(v).some((G,B)=>d[B]!==G),R=Wm(y,w),M=QE(y,w),O=x||!R?{x:0,y:0}:{x:M?a.width-S.width:0,y:M?a.height-S.height:0},L={x:S.left,y:S.top};return O.x&&O.y?L:wo(L,O)}}}};function Wm(o,e){return!Cc(o)||!Cc(e)?!1:o.data.current.sortable.containerId===e.data.current.sortable.containerId}function QE(o,e){return!Cc(o)||!Cc(e)||!Wm(o,e)?!1:o.data.current.sortable.index<e.data.current.sortable.index}const ZE={verticalList:zE,horizontalList:HE,rectSorting:af},e_=({items:o,itemKey:e,strategy:r,children:a,onSortEnd:i,onSortStart:l,sortVersion:c})=>{const d=N.useId(),f=N.useRef({inProgress:!1,lastSortTime:0,lastActiveId:null}),g=_C(ym(Zd,{activationConstraint:{distance:5}}),ym(Jd,{coordinateGetter:JE})),m=N.useCallback(S=>{f.current={inProgress:!0,lastSortTime:Date.now(),lastActiveId:S.active.id},l?.({activeId:S.active.id})},[l]),y=N.useCallback(S=>{const{active:v,over:A}=S,x=Date.now();if(!A||v.id===A.id){f.current.inProgress=!1;return}const{lastSortTime:R,lastActiveId:M,inProgress:O}=f.current;if(M===v.id&&x-R<500&&!O)return;f.current={inProgress:!1,lastSortTime:x,lastActiveId:v.id};const L=o.findIndex(G=>String(G[e])===String(v.id)),q=o.findIndex(G=>String(G[e])===String(A.id));if(L!==-1&&q!==-1&&L!==q){const G=rf(o,L,q);i?.(G,{oldIndex:L,newIndex:q})}},[o,e,i]),w=o.map(S=>String(S[e]));return dt.jsx(xE,{id:d,sensors:g,collisionDetection:xC,onDragStart:m,onDragEnd:y,children:dt.jsx(BE,{items:w,strategy:ZE[r],children:a})},c!==void 0?`${d}-v${c}`:d)},Ym=N.createContext(null),t_=({value:o,children:e})=>dt.jsx(Ym.Provider,{value:o,children:e}),n_=()=>N.useContext(Ym),r_=({id:o,handle:e,as:r="div",children:a})=>{const{attributes:i,listeners:l,setNodeRef:c,transform:d,transition:f,isDragging:g}=WE({id:o}),m={transform:Co.Transform.toString(d),transition:f,opacity:g?.5:1,position:"relative"};return dt.jsx(r,{ref:c,style:m,...i,...e?{}:l,"data-sortable-item":!0,"data-sortable-id":String(o),"data-dragging":g,children:dt.jsx(t_,{value:{listeners:l,handle:e,isDragging:g},children:a})})},rt=ut("DynamicRenderer");function ea(){try{return window.G7Core?.devTools}catch{return}}function sf(o,e,r,a){if(typeof o=="string"){if(/\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*/.test(o))try{return e.resolveTranslations(o,r,a)}catch{return o}return o}if(Array.isArray(o))return o.map(i=>sf(i,e,r,a));if(o&&typeof o=="object"){const i={};for(const[l,c]of Object.entries(o))i[l]=sf(c,e,r,a);return i}return o}function a_(o){const e=Object.keys(o);return e.length>0&&e.every(r=>/^\d+$/.test(r))}const Cr=(o,e)=>{const r=["errors"],a={...o};for(const i of Object.keys(e)){const l=e[i],c=o[i];if(l===null){a[i]=null;continue}if(r.includes(i)){a[i]=l;continue}if(Array.isArray(l)){a[i]=l;continue}if(Array.isArray(c)&&l!==null&&typeof l=="object"&&!Array.isArray(l)&&a_(l)){const d=Object.keys(l).map(g=>parseInt(g,10));if((d.length>0?Math.max(...d):0)>=c.length+d.length+10)a[i]={...l};else{const g=[...c];for(const[m,y]of Object.entries(l)){const w=parseInt(m,10);w>=0&&w<g.length?y!==null&&typeof y=="object"&&!Array.isArray(y)&&g[w]!==null&&typeof g[w]=="object"?g[w]=Cr(g[w],y):g[w]=y:w>=g.length&&(g[w]=y)}a[i]=g}}else l!==null&&typeof l=="object"&&c!==null&&typeof c=="object"&&!Array.isArray(c)?a[i]=Cr(c,l):a[i]=l}return a},Ec=(o,e)=>{let r=null;const a=()=>(r===null&&(r={...o}),r);for(const i of Object.keys(e)){if(!(i in o))continue;const l=e[i],c=o[i];if(l!==null&&typeof l=="object"&&!Array.isArray(l)&&c!==null&&typeof c=="object"&&!Array.isArray(c)){const d=Ec(c,l);if(d===c)continue;Object.keys(d).length===0?delete a()[i]:a()[i]=d}else delete a()[i]}return r??o},i_=o=>{const e={},r=o.split(";").filter(a=>a.trim());for(const a of r){const i=a.indexOf(":");if(i===-1)continue;const l=a.substring(0,i).trim(),c=a.substring(i+1).trim();if(l&&c){const d=l.replace(/-([a-z])/g,(f,g)=>g.toUpperCase());e[d]=c}}return e};class s_ extends N.Component{constructor(e){super(e),this.state={hasError:!1,error:null}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}componentDidCatch(e,r){rt.error(`컴포넌트 렌더링 에러 (ID: ${this.props.componentId}, Name: ${this.props.componentName}):`,e,r)}render(){return this.state.hasError?this.props.fallback?this.props.fallback:dt.jsxs("div",{className:"p-4 my-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-400",children:[dt.jsx("div",{className:"font-semibold mb-1",children:"컴포넌트 로드 실패"}),dt.jsxs("div",{className:"text-xs opacity-80",children:[this.props.componentName&&`[${this.props.componentName}] `,"데이터를 표시할 수 없습니다."]}),this.state.error&&dt.jsxs("details",{className:"mt-2",children:[dt.jsx("summary",{className:"cursor-pointer text-xs opacity-70 hover:opacity-100",children:"상세 정보"}),dt.jsx("div",{className:"mt-1 p-2 bg-red-100 dark:bg-red-900/30 rounded text-xs font-mono whitespace-pre-wrap break-all",children:this.state.error.message})]})]}):this.props.children}}const Br=N.memo(({componentDef:o,dataContext:e,translationContext:r,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:d,parentFormContextProp:f,parentDataContext:g,isRootRenderer:m=!1,isInsideIteration:y=!1,isEditMode:w=!1,onComponentSelect:S,onComponentHover:v,onDragStart:A,onDragEnd:x,componentPath:R,layoutKey:M})=>{const[O,L]=N.useState({loadingActions:{}});N.useLayoutEffect(()=>{d||(i.startRenderCycle(),window.__g7SetLocalOverrideKeys||(window.__g7ForcedLocalFields=void 0),window.__g7PendingLocalState=null)});const q=N.useRef(null);N.useLayoutEffect(()=>{if(!d&&e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,_merge:oe,...ce}=e._localInit,X=`${JSON.stringify(ce)}:${V||"no-force"}`;if(q.current!==X){q.current=X;const ve=oe||"shallow",we=window.G7Core?.state?.get?.()?._local||{},_e=window.__g7PendingLocalState||{},Be={...we,..._e};ve==="replace"?window.__g7PendingLocalState={loadingActions:Be.loadingActions||{},...ce,hasChanges:!1}:ve==="deep"?window.__g7PendingLocalState=Cr(Be,{...ce,hasChanges:!1}):window.__g7PendingLocalState={...Be,...ce,hasChanges:!1};const xe=window.__g7SetLocalOverrideKeys;xe&&typeof xe=="object"&&(ve==="replace"?window.__g7SetLocalOverrideKeys={...ce,hasChanges:!1}:window.__g7SetLocalOverrideKeys=Cr(xe,{...ce,hasChanges:!1}))}}}),N.useLayoutEffect(()=>{if(!d){const V=window.__g7SetLocalOverrideKeys;if(V){L(ve=>Ec(ve,V));const oe=V,ce=window.__g7ForcedLocalFields,X=window.__g7LastSetLocalSnapshot;queueMicrotask(()=>{window.__g7SetLocalOverrideKeys===oe&&(window.__g7SetLocalOverrideKeys=void 0),window.__g7ForcedLocalFields===ce&&(window.__g7ForcedLocalFields=void 0),window.__g7LastSetLocalSnapshot===X&&(window.__g7LastSetLocalSnapshot=void 0)})}}},[e._local]);const G=N.useRef(M);N.useEffect(()=>{o._fromBase&&M&&G.current!==M&&(G.current=M,L({loadingActions:{}}))},[M,o._fromBase]);const B=N.useRef(""),K=N.useRef(!1),fe=N.useRef(null),Ce=N.useRef(null),ke=N.useRef({}),Ue=N.useRef({}),Pe=N.useRef(g),qe=cC(),wt=z0(),J=wt?.getParentDataContext()??g;N.useEffect(()=>{if(e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,...oe}=e._localInit,ce=JSON.stringify(oe),X=V!==void 0,ve=Hd(),ue=`${ce}:${V||"no-force"}`,we=K0({globalTrackedKey:ve.hash,instanceHandledKey:Ce.current,trackingKey:ue});if(we==="apply"){Ce.current=ue,ve.hash=ue,ve.timestamp=V,B.current=ce,X&&(K.current=!0);const{_merge:Be,...xe}=oe,De=Be||"shallow";L(Ae=>De==="replace"?{loadingActions:Ae.loadingActions||{},...xe,hasChanges:!1}:De==="deep"?Cr(Ae,{...xe,hasChanges:!1}):{...Ae,...xe,hasChanges:!1}),e._globalSetState&&e._globalSetState(Ae=>{const et=Ae?._local||{};let Ne;return De==="replace"?Ne={...xe,hasChanges:!1}:De==="deep"?Ne=Cr(et,{...xe,hasChanges:!1}):Ne={...et,...xe,hasChanges:!1},{...Ae,_local:Ne}}),i.invalidateCacheByKeys(["_local"]),rt.log("_localInit applied (data changed):",Object.keys(oe))}else if(we==="prune"){Ce.current=ue;const{_merge:Be,...xe}=oe;L(Ae=>Ec(Ae,xe)),Ec(O,xe)!==O&&(i.invalidateCacheByKeys(["_local"]),rt.log("_localInit pruned (applied by another renderer):",Object.keys(xe)))}fe.current=e._localInit,V0(e._localInit)}},[e._localInit,g,i]);const{isTransitioning:he}=om(),Ee=eC(),Z=f??Ee,{width:pe}=zd(),H=n_(),T=N.useMemo(()=>{let V=o;if(o.type==="iterator"&&o.data){const{data:X,itemName:ve,indexName:ue,...we}=o;V={...we,iteration:{source:X,item_var:ve||"item",index_var:ue}}}if(!V.responsive)return V;const oe=mi.getMatchingKey(V.responsive,pe);if(!oe)return V;const ce=V.responsive[oe];return{...V,props:{...V.props,...ce.props},children:ce.children??V.children,text:ce.text??V.text,if:ce.if??V.if,iteration:ce.iteration??V.iteration,__responsiveChildrenKey:ce.children!==void 0?oe:void 0}},[o,pe]),ge=N.useCallback(V=>{const oe=V?.__setStateId;if(L(ce=>{const X=window.__g7PendingLocalState,ue=window.__g7SetLocalOverrideKeys||X,we=ue?Cr(ce,ue):ce;if(typeof V=="function"){const Ae=V(we);return Cr(we,Ae)}const{__mergeMode:_e,__setStateId:Be,...xe}=V;let De;if(_e==="replace"?De=xe:_e==="shallow"?De={...we,...xe}:De=Cr(we,xe),_e!==void 0){const Ae=window.__g7ForcedLocalFields;Ae&&(window.__g7ForcedLocalFields={...Ae,...xe})}return De}),oe){const ce=ea();if(ce?.isEnabled()){const X=T.id||"unknown",ve=T.name||"Unknown";setTimeout(()=>{ce.trackComponentRender?.(X,ve,0,["_local"],[]),ce.completeStateChange?.(oe)},0)}}},[T.id,T.name]),de=d?.state??O,me=d?.setState??ge,ae=N.useMemo(()=>{const V=e._localInit&&e._localInit!==fe.current;let oe;V&&e._local?oe=e._local:oe=e._local?Cr(e._local,de):de;const ce=window.__g7ForcedLocalFields;ce&&(oe=Cr(oe,ce));const X={...e,_local:oe},ve=e._computedDefinitions;if(ve&&Object.keys(ve).length>0){const ue=new _n,we={...X,...qe?{_isolated:qe.state}:{}},_e={},Be=ea(),xe=Be?.isEnabled()??!1;for(const[De,Ae]of Object.entries(ve)){const et=xe?Date.now():0,Ne=xe?e._computed?.[De]:void 0;let je;try{if(typeof Ae=="string")if(Ae.startsWith("{{")&&Ae.endsWith("}}")){const Se=Ae.slice(2,-2).trim(),Me=Gn(Se)?ue.evaluatePipeExpression(Se,we,{skipCache:!0}):ue.evaluateExpression(Se,we,{skipCache:!0});_e[De]=Me}else _e[De]=Ae;else if(Ae&&typeof Ae=="object"&&"$switch"in Ae){const Se=ue.resolveSwitch(Ae,we,{skipCache:!0});_e[De]=Se}}catch(Se){rt.warn(`Failed to recalculate computed value: ${De}`,Se),_e[De]=e._computed?.[De],je=Se instanceof Error?Se.message:String(Se)}if(xe&&(je||Ne!==_e[De])){const Me=Date.now()-et;Be.trackComputedProperty?.(De,typeof Ae=="string"?Ae:JSON.stringify(Ae),[{type:"_local",path:"_local",value:X._local}],_e[De],Me,T.id,je),Ne!==_e[De]&&Be.trackComputedRecalc?.(De,"dependency-change",Ne,_e[De],Me,{type:"_local",path:"_local"},T.id)}}X._computed=_e}if(qe&&(X._isolated=qe.state),J&&(X.$parent=J),o.extensionPointProps){const ue=i.resolveObject(o.extensionPointProps,X,y?{skipCache:!0}:void 0);X.extensionPointProps=w&&l?sf(ue,l,r,X):ue}return o.extensionPointCallbacks&&(X.extensionPointCallbacks=o.extensionPointCallbacks),w&&(X.$templateId===void 0&&r?.templateId&&(X.$templateId=r.templateId),X.$locale===void 0&&r?.locale&&(X.$locale=r.locale)),X},[e,de,qe?.state,J,wt?.version,o.extensionPointProps,o.extensionPointCallbacks]);ke.current=ae._local,Ue.current=ae._computed||{},Pe.current=J,N.useEffect(()=>{const V=ea();if(!V?.isEnabled())return;const oe=ae._local;oe&&V.updateLocalState(oe);const ce=ae._computed;ce&&V.updateComputedState(ce),J&&V.updateParentContext?.(J);const X=T.id||`comp-${Date.now()}`,ve=T.name||"Unknown",ue={global:Object.keys(e._global||{}),local:Object.keys(oe||{}),context:d?Object.keys(d.state||{}):[]},we={type:d?"componentContext":"dynamicState",hasComponentContext:!!d,parentId:d?"parent":void 0};V.trackComponentStateSource?.(X,ve,ue,we),de&&Object.keys(de).length>0&&V.trackDynamicState?.(X,de);const _e=!!(T.expandChildren||T.props?.expandChildren);V.trackContextFlow?.(X,ve,!!d,_e,!0)},[ae._local,e,T.id,T.name,d,de,T.expandChildren,T.props?.expandChildren,J]);const Re=N.useMemo(()=>({state:{...ae._local},setState:me,stateRef:ke,computedRef:Ue,isolatedContext:qe,parentDataContext:ae}),[ae._local,me,O,qe,ae]),ze=N.useMemo(()=>{const V=Qi({if:T.if,condition:T.condition,conditions:T.conditions},ae,i,T.id);if(T.if||T.condition!==void 0||T.conditions){const ce=ea();ce?.isEnabled()&&T.if&&ce.trackIfCondition(T.id||`if-${Date.now()}`,T.if,V)}return V},[T.if,T.conditions,ae,i,T.id]),$e=N.useMemo(()=>{if(!T.slot)return null;const V=T.slot;if(V.startsWith("{{")&&V.endsWith("}}")){const oe=V.slice(2,-2).trim();try{const ce={componentId:T.id,componentName:T.name,propName:"slot"},X=Gn(oe)?i.evaluatePipeExpression(oe,ae,void 0,ce):i.evaluateExpression(oe,ae,ce);return rt.log(`[Slot] 표현식 평가: "${oe}" => "${X}" (컴포넌트: ${T.id})`),typeof X=="string"?X:null}catch(ce){return rt.warn(`slot 표현식 평가 실패 (컴포넌트: ${T.id}):`,ce),null}}return rt.log(`[Slot] 정적 슬롯 ID: "${V}" (컴포넌트: ${T.id})`),V},[T.slot,T.id,ae,i]),Ge=N.useMemo(()=>!$e||!T.id?"":`${T.id}-${$e}-${JSON.stringify(T.slotOrder??0)}`,[$e,T.id,T.slotOrder]),At=N.useRef(null);N.useEffect(()=>{const V=window.__slotContextValue,oe=V?.isEnabled??!1;if(T.slot&&rt.log(`[Slot Registration] 컴포넌트: ${T.id}, isSlotEnabled: ${oe}, shouldRender: ${ze}, resolvedSlotId: ${$e}`),!oe||!ze){T.slot&&rt.log(`[Slot Registration] 등록 스킵 - isSlotEnabled: ${oe}, shouldRender: ${ze}`),At.current&&T.id&&V&&(V.unregisterFromSlot(At.current,T.id),At.current=null);return}if(!$e||!T.id){T.slot&&rt.log(`[Slot Registration] 등록 스킵 - resolvedSlotId: ${$e}, id: ${T.id}`),At.current&&T.id&&V&&(V.unregisterFromSlot(At.current,T.id),At.current=null);return}At.current&&At.current!==$e&&V.unregisterFromSlot(At.current,T.id);const ce={...T,slot:void 0,slotOrder:void 0};return V.registerToSlot($e,T.id,{componentDef:ce,dataContext:e,order:T.slotOrder??0,parentFormContext:f??null,getParentComponentContext:()=>Re,translationContext:r,registrationKey:Ge}),At.current=$e,()=>{const X=window.__slotContextValue;$e&&T.id&&X&&X.unregisterFromSlot($e,T.id)}},[ze,$e,T,e,f,Re,r,Ge]);const yt=N.useCallback(V=>{if(typeof V=="string"){if(ql(V))return hd(V);if(Gl(V)){const oe=[],ce=V.replace(new RegExp(`${ro}([^${pi}]*)${pi}`,"g"),(ve,ue)=>(oe.push(ue),`${ka}${oe.length-1}${ka}`));let X=ce;return/\$t:[a-zA-Z0-9._-]+/.test(ce)&&(X=l.resolveTranslations(ce,r,ae)),X.replace(new RegExp(`${ka}(\\d+)${ka}`,"g"),(ve,ue)=>oe[parseInt(ue)])}if(V.startsWith("$t:defer:"))return V;if(/\$t:[a-zA-Z0-9._-]+/.test(V)){const oe=V.trim();return oe.startsWith("{")&&oe.endsWith("}")||oe.startsWith("[")&&oe.endsWith("]")?V:l.resolveTranslations(V,r,ae)}return V}if(Array.isArray(V))return V.map(oe=>yt(oe));if(V&&typeof V=="object"){const oe={};for(const[ce,X]of Object.entries(V))oe[ce]=yt(X);return oe}return V},[l,r,ae]),dn=N.useCallback(V=>no(V),[]),Tt=N.useCallback(V=>Ta(V),[]),Yt=N.useMemo(()=>{let V={...T.props||{}};const oe=["cellChildren","expandChildren","expandContext","render"],X=a.getMetadata(T.name)?.skipBindingKeys||[],ve=[...new Set([...oe,...X])],ue=y?{skipCache:!0}:void 0,we={...ue??{},skipBindingKeys:ve},_e=Se=>{if(ue)return ue;if(Se.startsWith("_computed")||Se.startsWith("$computed"))return{skipCache:!0}};for(const[Se,Me]of Object.entries(V))if(!ve.includes(Se))if(typeof Me=="string"){const Ze=Tt(Me);if(Ze!==null){let kt=!1,Ot=Ze;Ze.startsWith(nr)&&(kt=!0,Ot=Ze.slice(nr.length));const Nn={componentId:T.id,componentName:T.name,propName:Se};let $t;if(Gn(Ot))try{$t=i.evaluatePipeExpression(Ot,ae,_e(Ot),Nn)}catch(Nt){rt.warn(`파이프 표현식 평가 실패 (컴포넌트: ${T.id}, prop: ${Se}):`,Nt),$t=void 0}else if(dn(Ot))try{$t=i.evaluateExpression(Ot,ae,Nn)}catch(Nt){rt.warn(`표현식 평가 실패 (컴포넌트: ${T.id}, prop: ${Se}):`,Nt),$t=void 0}else $t=i.resolve(Ot,ae,_e(Ot),Nn);V[Se]=kt&&$t!=null?Ji($t):$t}else{const kt={componentId:T.id,componentName:T.name,propName:Se};V[Se]=i.resolveBindings(Me,ae,ue,kt)}}else Array.isArray(Me)?V[Se]=Me.map(Ze=>typeof Ze=="string"?i.resolveBindings(Ze,ae,ue):typeof Ze=="object"&&Ze!==null?i.resolveObject(Ze,ae,we):Ze):Me&&typeof Me=="object"&&(V[Se]=i.resolveObject(Me,ae,we));for(const[Se,Me]of Object.entries(V))V[Se]=yt(Me);if(typeof V.style=="string"&&(V.style=i_(V.style)),V=yd(V,w?void 0:T.actions,ae,{componentContext:Re,actionDispatcher:c}),T.classMap){const Se=cw(T.classMap,ae,i,{skipCache:!0});if(Se){const Me=V.className||"";V.className=Me?`${Me} ${Se}`:Se}}T.style&&(V.style=T.style),T.actions&&T.actions.length>0&&T.type==="composite"&&Object.assign(V,de),T.component_layout&&(V.__componentLayoutDefs=T.component_layout);const Be=T.expandChildren||T.props?.expandChildren,xe=T.props?.cellChildren,De=T.children&&T.children.length>0,Ae=T.component_layout,et=T.slot,Ne=T.props?.modalId,je=T.type==="composite";return(Be||xe||De||Ae||et||Ne||je)&&(V.__componentContext=Re),V},[T.props,T.actions,T.style,T.classMap,T.id,ae,r,i,l,Re,de,yt,dn,Tt,y]),ln=N.useRef(null),zt=N.useMemo(()=>ln.current&&cm(ln.current,Yt)?ln.current:(ln.current=Yt,Yt),[Yt]),Tr=N.useMemo(()=>{const V=T.id;if(typeof V!="string"||!V.includes("{{"))return V;const oe=i.resolveBindings(V,ae,{skipCache:!0});return typeof oe=="string"?oe:V},[T.id,ae,i]),wn=N.useMemo(()=>{const V=T.dataKey,oe=T.trackChanges,ce=T.debounce;if(V){const X=V.startsWith("_global."),ve=X?V.slice(8):V,ue=X?e._global:ae._local;return{dataKey:ve,trackChanges:oe??!1,debounce:ce,setState:X?_e=>{e._globalSetState?e._globalSetState(_e):rt.warn("_global.formData 사용 시 _globalSetState가 필요합니다.")}:me,state:ue??{},_isGlobal:X}}if(Z.dataKey)return Z},[T.dataKey,T.trackChanges,T.debounce,T.id,me,ae._local,e._global,e._globalSetState,Z]),cn=N.useRef(new Set);N.useEffect(()=>{cn.current.clear()},[e._global?._remountKeys]);const fn=N.useCallback((V,oe)=>{const ce=e._global?._remountKeys;return V&&ce?.[V]&&!cn.current.has(V)?(cn.current.add(V),`${V}-remount-${ce[V]}`):V||oe},[e._global?._remountKeys]),sr=N.useMemo(()=>{if(T.text!==void 0){if(typeof T.text=="string"){const V=T.text;let oe=V;const ce={componentId:T.id,componentName:T.name,propName:"text",skipCache:!0},X=Tt(V);if(X!==null){let ve=!1,ue=X;if(X.startsWith(nr)&&(ve=!0,ue=X.slice(nr.length)),Gn(ue))try{oe=i.evaluatePipeExpression(ue,ae,{skipCache:!0},ce)}catch(we){rt.warn(`text 파이프 표현식 평가 실패 (컴포넌트: ${T.id}):`,we),oe=""}else if(dn(ue))try{oe=i.evaluateExpression(ue,ae,ce)}catch(we){rt.warn(`text 표현식 평가 실패 (컴포넌트: ${T.id}):`,we),oe=""}else oe=i.resolve(ue,ae,{skipCache:!0},ce);ve&&oe!=null&&(oe=Ji(oe))}else V.includes("{{")&&(oe=i.resolveBindings(V,ae,{skipCache:!0},ce));return oe=yt(oe),oe??""}return T.text}return!T.children||T.children.length===0?null:T.children.map((V,oe)=>{if(typeof V=="string")return V;const ce=T.__responsiveChildrenKey,X=ce?`responsive.${ce}.children`:"children",ve=R?`${R}.${X}.${oe}`:`${oe}`,ue=V._fromBase?V.id||`child-${oe}`:fn(V.id,`child-${oe}`),we=!V._fromBase&&M?`${ue}_${M}`:ue;return dt.jsx(Br,{componentDef:V,dataContext:ae,translationContext:r,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:Re,parentFormContextProp:wn,isInsideIteration:y,isEditMode:w,onComponentSelect:S,onComponentHover:v,componentPath:ve,onDragStart:A,onDragEnd:x,layoutKey:M},we)})},[T.text,T.children,T.id,e,ae,r,a,i,l,c,Re,wn,Tt,dn,yt,fn,y,w,S,v,R,A,x,M]),Wn=N.useMemo(()=>{if(!T.iteration)return null;try{const V=md(T.iteration.source,ae,i);if(!Array.isArray(V))return rt.warn(`iteration source가 배열이 아닙니다 (컴포넌트: ${T.id}):`,V),null;const oe=ea();return oe?.isEnabled()&&oe.trackIteration(T.id||`iteration-${Date.now()}`,T.iteration.source,T.iteration.item_var,T.iteration.index_var,V.length),V.map((ce,X)=>{const ve={...ae,[T.iteration.item_var]:ce,[`${T.iteration.item_var}_index`]:X};T.iteration.index_var&&(ve[T.iteration.index_var]=X);const ue=X===0||X===V.length-1;oe?.isEnabled()&&ue&&oe.trackNestedContext?.({componentId:`${T.id||"unknown"}-iter-${X}`,componentType:"iteration",parentContext:{available:Object.keys(ae),values:{}},ownContext:{added:[T.iteration.item_var,...T.iteration.index_var?[T.iteration.index_var]:[]],values:{[T.iteration.item_var]:ce,...T.iteration.index_var?{[T.iteration.index_var]:X}:{}}},depth:1});const we={...T,iteration:void 0,responsive:void 0},_e=R?`${R}.iteration.${X}`:`iteration.${X}`,Be=`${fn(T.id,"item")}-${X}`;return dt.jsx(Br,{componentDef:we,dataContext:ve,translationContext:r,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:Re,parentFormContextProp:wn,isInsideIteration:!0,isEditMode:w,onComponentSelect:S,onComponentHover:v,componentPath:_e,onDragStart:A,onDragEnd:x,layoutKey:M},Be)})}catch(V){return rt.error(`iteration 렌더링 실패 (컴포넌트: ${T.id}):`,V),null}},[T.iteration,T.children,T.id,e,ae,r,a,i,l,c,Re,wn,fn,w,S,v,R,A,x,M]),[Ut,bt]=N.useState(0),ra=N.useMemo(()=>{if(!T.sortable||!T.itemTemplate)return null;const{sortable:V,itemTemplate:oe}=T;try{const ce=md(V.source,ae,i);if(!Array.isArray(ce))return rt.warn(`sortable source가 배열이 아닙니다 (컴포넌트: ${T.id}):`,ce),null;if(ce.length===0)return null;const X=V.itemKey||"id",ve=V.strategy||"verticalList",ue=V.itemVar||"$item",we=V.indexVar||"$index",_e=V.handle,Be=V.wrapperElement,xe=(et,Ne)=>{const je=T.actions?.filter(Se=>Se.event==="onSortEnd"||Se.type==="onSortEnd");if(bt(Se=>Se+1),je&&je.length>0&&c){const Se={...ae,$sortedItems:et,$sortEvent:Ne};je.forEach(Me=>{try{const Ze=c.createHandler(Me,Se,Re),kt=new Event("sortend");Ze(kt)}catch(Ze){rt.error(`onSortEnd action failed: ${Me.handler}`,Ze)}})}},De=et=>{const Ne=T.actions?.filter(je=>je.event==="onSortStart"||je.type==="onSortStart");if(Ne&&Ne.length>0&&c){const je={...ae,$activeId:et.activeId};Ne.forEach(Se=>{try{const Me=c.createHandler(Se,je,Re),Ze=new Event("sortstart");Me(Ze)}catch(Me){rt.error(`onSortStart action failed: ${Se.handler}`,Me)}})}},Ae=ce.map((et,Ne)=>{const je=String(et[X]),Se={...ae,[ue]:et,[we]:Ne},Me=R?`${R}.sortable.${Ne}`:`sortable.${Ne}`,Ze=`${fn(T.id,"sortable-item")}-v${Ut}-${je}`;return dt.jsx(r_,{id:je,handle:_e,...Be?{as:Be}:{},children:(Be&&oe.children?oe.children:[oe]).map((kt,Ot)=>dt.jsx(Br,{componentDef:kt,dataContext:Se,translationContext:r,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:Re,parentFormContextProp:void 0,isInsideIteration:!0,isEditMode:w,onComponentSelect:S,onComponentHover:v,componentPath:Me,onDragStart:A,onDragEnd:x,layoutKey:M},`${Ze}-child-${Ot}`))},Ze)});return dt.jsx(e_,{items:ce,itemKey:X,strategy:ve,onSortEnd:xe,onSortStart:De,sortVersion:Ut,children:Ae})}catch(ce){return rt.error(`sortable 렌더링 실패 (컴포넌트: ${T.id}):`,ce),null}},[T.sortable,T.itemTemplate,T.actions,T.id,e,ae,r,a,i,l,c,Re,fn,w,S,v,R,A,x,Ut,M]),Ln=N.useRef(!1);N.useEffect(()=>{if(Ln.current)return;Ln.current=!0;const V=ea();if(V?.isEnabled()&&V.trackMount(o.id,{name:o.name,type:o.type||"component",props:zt}),o.lifecycle?.onMount&&o.lifecycle.onMount.length>0)for(const ce of o.lifecycle.onMount)try{const X=c.createHandler(ce,ae,Re),ve=new Event("mount");X(ve),rt.log(`Lifecycle onMount executed: ${ce.handler} (Component: ${o.id})`)}catch(X){rt.error(`Lifecycle onMount failed: ${ce.handler} (Component: ${o.id})`,X)}const oe=[];if(o.onComponentEvent&&o.onComponentEvent.length>0){const ce=window.G7Core;if(ce?.componentEvent?.on)for(const X of o.onComponentEvent){if(!X.event){rt.warn(`onComponentEvent: event name is required (Component: ${o.id})`);continue}const ve=async we=>{try{const _e={...ae,_eventData:we},xe=await c.createHandler({handler:X.handler,params:X.params,onSuccess:X.onSuccess,onError:X.onError},_e,Re)(new CustomEvent(X.event,{detail:we}));return rt.log(`onComponentEvent "${X.event}" handled: ${X.handler} (Component: ${o.id})`,{eventData:we,result:xe}),xe}catch(_e){throw rt.error(`onComponentEvent "${X.event}" failed: ${X.handler} (Component: ${o.id})`,_e),_e}},ue=ce.componentEvent.on(X.event,ve);oe.push(ue),rt.log(`onComponentEvent: Subscribed to "${X.event}" (Component: ${o.id})`)}else rt.warn(`onComponentEvent: G7Core.componentEvent is not available (Component: ${o.id})`)}return()=>{const ce=ea();ce?.isEnabled()&&ce.trackUnmount(o.id);for(const X of oe)try{X()}catch(ve){rt.error(`onComponentEvent: Failed to unsubscribe (Component: ${o.id})`,ve)}if(oe.length>0&&rt.log(`onComponentEvent: Unsubscribed ${oe.length} event(s) (Component: ${o.id})`),o.lifecycle?.onUnmount&&o.lifecycle.onUnmount.length>0)for(const X of o.lifecycle.onUnmount)try{const ve=c.createHandler(X,ae,Re),ue=new Event("unmount");ve(ue),rt.log(`Lifecycle onUnmount executed: ${X.handler} (Component: ${o.id})`)}catch(ve){rt.error(`Lifecycle onUnmount failed: ${X.handler} (Component: ${o.id})`,ve)}}},[]);const An=N.useMemo(()=>{const V=zt.dataKey,oe=zt.trackChanges,ce=zt.debounce;return V?{dataKey:V,trackChanges:oe??!1,debounce:ce,setState:me,state:de}:null},[zt.dataKey,zt.trackChanges,zt.debounce,me,de]);N.useEffect(()=>{const V=T.dataKey||zt.dataKey;if(!V)return;const oe=ea();if(!oe?.isEnabled())return;const ce=[],X=ue=>{if(ue)for(const we of ue){const _e=we?.props?.name;if(_e&&ce.push({name:_e,type:we.name||we.type||"unknown"}),we?.children&&X(we.children),we?.slots)for(const Be of Object.values(we.slots))Array.isArray(Be)&&X(Be)}};if(o.children&&X(o.children),o.slots)for(const ue of Object.values(o.slots))Array.isArray(ue)&&X(ue);const ve=o.id||`form-${V}`;return oe.trackForm(ve,V,ce),()=>{oe.untrackForm(ve)}},[T,zt.dataKey,o.id,o.children]);const xn=N.useRef(null),Mn=N.useRef(void 0);N.useEffect(()=>{const V=zt?.name;if(!V||!Z.dataKey||!Z.setState||zt?.autoBinding===!1)return;const oe=`${Z.dataKey}.${V}`,ce=window.__g7AutoBindingPaths??new Map;return window.__g7AutoBindingPaths=ce,ce.set(oe,(ce.get(oe)??0)+1),()=>{const X=ce.get(oe)??0;X<=1?ce.delete(oe):ce.set(oe,X-1)}},[zt?.name,zt?.autoBinding,Z.dataKey,Z.setState]);const hn=N.useMemo(()=>{const{dataKey:V,trackChanges:oe,debounce:ce,...X}=zt,ve=X.name;if(!ve||!Z.dataKey||!Z.setState)return X;if(X.autoBinding===!1){const{autoBinding:je,...Se}=X;return Se}const ue=`${Z.dataKey}.${ve}`,we=tC(Z.state,ue),_e=we!==void 0?we:X.value,xe=a.getMetadata(o.name)?.bindingType,De=typeof _e=="boolean"&&(xe==="checked"||xe==="checkable"&&["checkbox","radio"].includes(X?.type)),Ae=Z.debounce,et=je=>{const Se=Z.state||{},Me=nC(Se,ue,je);if(Z.trackChanges&&(Me.hasChanges=!0),window.__g7PendingLocalState=Me,Z.setState(Me),Z._isGlobal)return;const Ze=window.G7Core,kt={[ue]:je};Z.trackChanges&&(kt.hasChanges=!0),Ze?.state?.setLocal?.(kt,{render:!1})},Ne=(je,Se)=>{Ae&&Ae>0?(xn.current&&clearTimeout(xn.current),xn.current=setTimeout(()=>{et(je),xn.current=null,Se&&(Se(),window.__g7PendingLocalState=null)},Ae)):(et(je),Se&&(Se(),window.__g7PendingLocalState=null))};if(De)return{...X,checked:_e,onChange:Se=>{const Me=Se.target.checked;et(Me),X.onChange&&X.onChange(Se)}};{const je=Me=>{const Ze=Me?.target?.value!==void 0?Me.target.value:Me;Ae&&Ae>0&&(Mn.current=Ze);const kt=Me?.target?{target:{value:Ze,name:Me.target.name,type:Me.target.type,checked:Me.target.checked},currentTarget:Me.currentTarget?{value:Ze,name:Me.currentTarget.name}:void 0}:Me;Ne(Ze,()=>{X.onChange&&X.onChange(kt)})};let Se;return Mn.current!==void 0?xn.current!==null?Se=Mn.current:String(_e??"")===String(Mn.current)?(Mn.current=void 0,Se=_e??""):Se=Mn.current:Se=_e??"",{...X,value:Se,onChange:je}}},[zt,Z,Z.state,Z.debounce]),aa=N.useMemo(()=>{const V=ea();if(V?.isEnabled()&&V.trackRender(o.name),!ze)return T.iteration?Wn:null;if(T.sortable&&T.itemTemplate)return ra;if(T.iteration)return Wn;if(T.type==="extension_point"){const{layout:xe,columns:De,gap:Ae,className:et,...Ne}=T.props||{};let je=et||"";return xe==="grid"&&De?je=`${je} grid grid-cols-${De} gap-${Ae||4}`.trim():xe==="flex"&&(je=`${je} flex flex-col gap-${Ae||4}`.trim()),dt.jsx("div",{id:Tr,className:je||void 0,...Ne,children:sr})}const oe=a.getComponent(T.name);if(!oe)return rt.error(`컴포넌트를 찾을 수 없습니다: ${T.name} (ID: ${T.id})`,"componentDef:",JSON.stringify(T,null,2)),null;const ce=e._global?._remountKeys,X={...hn,id:hn.id??Tr,...ce&&Object.keys(ce).length>0&&{__remountTrigger:JSON.stringify(ce)}};if(H?.listeners&&H.handle&&hn["data-drag-handle"]!==void 0&&(Object.assign(X,H.listeners),X.style={...X.style,cursor:H.isDragging?"grabbing":"grab"}),w){const xe=T.id||`auto_${T.name||T.type||"unknown"}_${Math.random().toString(36).substring(2,8)}`,De=Ne=>{typeof Ne.preventDefault=="function"&&Ne.preventDefault(),S&&(Ne.stopPropagation(),S(xe,Ne))},Ae={"data-editor-id":xe,"data-editor-name":T.name,"data-editor-type":T.type,onClick:De};R&&(Ae["data-editor-path"]=R);const et=T.__editorSlotName??T.slot;typeof et=="string"&&(Ae["data-editor-slot"]=et),v&&(Ae.onMouseMove=Ne=>{Ne.stopPropagation(),v(xe,Ne)},Ae.onMouseLeave=Ne=>{v(null,Ne)}),Object.assign(X,Ae),X.editorAttrs=Ae}const ve=T.type==="basic",ue=T.type==="layout",_e=ve||ue?(()=>{const xe={};for(const[De,Ae]of Object.entries(X))De.startsWith("__")||De==="editorAttrs"&&ve||(xe[De]=Ae);return xe})():X;let Be=dt.jsx(oe,{..._e,children:sr});if(wn&&(Be=dt.jsx(Z0,{value:wn,children:Be})),T.isolatedState){const xe=typeof T.isolatedState=="object"?T.isolatedState:{},De=e?._isolatedInit,Ae=De?{...xe,...De}:xe;Be=dt.jsx(lC,{initialState:Ae,scopeId:T.isolatedScopeId,children:Be})}return Be},[ze,T.iteration,T.sortable,T.itemTemplate,T.type,T.name,T.id,T.props,T.isolatedState,T.isolatedScopeId,a,hn,sr,Wn,ra,H,An,w,S,v,A,x,R,e._global?._remountKeys]),$n=N.useMemo(()=>{const V=T.blur_until_loaded;if(!V||e?._global?.__isPreview)return!1;if(typeof V=="string"){const X=Tt(V);if(X!==null)try{const ve={componentId:T.id,componentName:T.name,propName:"blur_until_loaded"};return!!i.evaluateExpression(X,ae,ve)}catch(ve){return rt.warn(`blur_until_loaded 표현식 평가 실패 (컴포넌트: ${T.id}):`,ve),!1}return!1}if(typeof V=="object"&&V!==null){if(!V.enabled)return!1;if(V.data_sources)return(Array.isArray(V.data_sources)?V.data_sources:[V.data_sources]).some(we=>e[we]===void 0);if(he)return!0;const X=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(ue=>!X.includes(ue)&&!ue.startsWith("_")).some(ue=>e[ue]===void 0)}if(he)return!0;const oe=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(X=>!oe.includes(X)&&!X.startsWith("_")).some(X=>e[X]===void 0)},[T.blur_until_loaded,T.id,he,e,ae,i,Tt]);if($n){const V=T.blur_until_loaded;if(typeof V=="object"&&V?.data_sources){const ce=(Array.isArray(V.data_sources)?V.data_sources:[V.data_sources]).reduce((ve,ue)=>(ve[ue]=e[ue]===void 0?"UNDEFINED":"LOADED",ve),{}),X=Object.keys(e).filter(ve=>!ve.startsWith("_"));rt.log(`[BLUR APPLIED] Component: ${T.id}, data_sources status:`,ce,"Available keys:",X)}else rt.log(`[BLUR APPLIED] Component: ${T.id}, blur_until_loaded:`,V)}const Vt=N.useMemo(()=>{if(!T.blur_until_loaded)return{wrapperClasses:"",innerClasses:""};const V=Yt?.className||"";if(!V)return{wrapperClasses:"",innerClasses:""};const oe=[/^(sm:|md:|lg:|xl:|2xl:)?(col-span-|row-span-|col-start-|col-end-|row-start-|row-end-)/,/^(sm:|md:|lg:|xl:|2xl:)?(self-|place-self-|order-)/,/^(sm:|md:|lg:|xl:|2xl:)?(flex-shrink|flex-grow|flex-\d|basis-)/],ce=V.split(/\s+/).filter(Boolean),X=[],ve=[];for(const ue of ce)oe.some(_e=>_e.test(ue))?X.push(ue):ve.push(ue);return{wrapperClasses:X.join(" "),innerClasses:ve.join(" ")}},[T.blur_until_loaded,Yt?.className]),Cn=window.__slotContextValue;return $e&&Cn?.isEnabled?null:dt.jsx(s_,{componentId:o.id,componentName:o.name,children:T.blur_until_loaded?dt.jsx("div",{className:[Vt.wrapperClasses,$n?"opacity-50 blur-sm transition-all duration-300 pointer-events-none":void 0].filter(Boolean).join(" ")||void 0,children:aa}):aa})});Br.displayName="DynamicRenderer",typeof window<"u"&&(window.__DynamicRenderer=Br);function o_(o){const e={};try{const a=new URL(o,typeof window<"u"?window.location.origin:"http://localhost").searchParams,i=new Set;for(const l of a.keys()){if(i.has(l))continue;i.add(l);const c=a.getAll(l);c.length>1||l.endsWith("[]")?e[l]=c:e[l]=c[0]}}catch{}return e}function l_(o){try{return new URL(o,typeof window<"u"?window.location.origin:"http://localhost").pathname}catch{return o.split("?")[0]}}const Ei=class Ei{constructor(){$(this,"config",{enabled:!1,maxHistorySize:100,logLevel:"info",serverEndpoint:"/_boost/g7-debug/dump-state",autoCapture:!0});$(this,"stateHistory",[]);$(this,"snapshotIdCounter",0);$(this,"stateWatchers",new Map);$(this,"currentLocalState",{});$(this,"currentComputedState",{});$(this,"currentParentContext");$(this,"actionHistory",[]);$(this,"actionWatchers",new Set);$(this,"cacheStats",{hits:0,misses:0,entries:0});$(this,"cacheDecisions",[]);$(this,"cacheDecisionIdCounter",0);$(this,"maxCacheDecisions",200);$(this,"currentRenderCycleId",null);$(this,"isInActionExecution",!1);$(this,"isInIteration",!1);$(this,"mountedComponents",new Map);$(this,"eventListeners",new Map);$(this,"mountWatchers",new Set);$(this,"unmountWatchers",new Set);$(this,"renderCounts",new Map);$(this,"bindingEvalCount",0);$(this,"profilingData",[]);$(this,"isProfiling",!1);$(this,"profilingStartTime",0);$(this,"activeRequests",new Map);$(this,"requestHistory",[]);$(this,"pendingDataSources",new Set);$(this,"requestWatchers",new Set);$(this,"wsConnections",new Map);$(this,"wsMessageHistory",[]);$(this,"wsMessageWatchers",new Set);$(this,"ifConditions",new Map);$(this,"iterations",new Map);$(this,"conditionWatchers",new Set);$(this,"forms",new Map);$(this,"formWatchers",new Set);$(this,"expressionHistory",[]);$(this,"expressionWatchers",new Set);$(this,"expressionIdCounter",0);$(this,"dataSources",new Map);$(this,"dataPathTransforms",[]);$(this,"maxDataPathTransforms",100);$(this,"nestedContexts",[]);$(this,"nestedContextIdCounter",0);$(this,"maxNestedContexts",100);$(this,"formBindingIssues",[]);$(this,"formBindingValidations",[]);$(this,"formBindingIssueIdCounter",0);$(this,"maxFormBindingIssues",100);$(this,"computedProperties",new Map);$(this,"computedRecalcLogs",[]);$(this,"computedRecalcIdCounter",0);$(this,"maxComputedRecalcLogs",100);$(this,"handlers",new Map);$(this,"componentEventSubscriptions",new Map);$(this,"componentEventEmitHistory",[]);$(this,"componentEventIdCounter",0);$(this,"maxComponentEventHistory",100);$(this,"stateRenderingLogs",[]);$(this,"currentStateChangeContext",null);$(this,"stateRenderingIdCounter",0);$(this,"componentRenderCounts",new Map);$(this,"stateToComponentMap",new Map);$(this,"maxStateRenderingLogs",100);$(this,"componentStateSources",new Map);$(this,"contextFlowNodes",new Map);$(this,"dynamicStates",new Map);$(this,"styleIssues",[]);$(this,"componentStyles",new Map);$(this,"authEvents",[]);$(this,"authHeaderHistory",[]);$(this,"authEventIdCounter",0);$(this,"maxAuthEventHistory",50);$(this,"logHistory",[]);$(this,"logIdCounter",0);$(this,"maxLogHistory",500);$(this,"currentLayout",null);$(this,"layoutHistory",[]);$(this,"layoutIdCounter",0);$(this,"layoutStats",{totalLoads:0,cacheHits:0,apiLoads:0});$(this,"maxLayoutHistory",50);$(this,"executionDetails",new Map);$(this,"stateChangeHistory",[]);$(this,"dataSourceChangeHistory",[]);$(this,"changeAlerts",[]);$(this,"changeExpectations",new Map);$(this,"executionIdCounter",0);$(this,"stateChangeIdCounter",0);$(this,"alertIdCounter",0);$(this,"maxExecutionDetails",100);$(this,"maxStateChangeHistory",200);$(this,"maxChangeAlerts",100);$(this,"sequenceExecutions",[]);$(this,"sequenceExecutionStack",[]);$(this,"sequenceIdCounter",0);$(this,"maxSequenceExecutions",50);$(this,"staleClosureWarnings",[]);$(this,"staleClosureIdCounter",0);$(this,"maxStaleClosureWarnings",100);$(this,"stateCaptureRegistry",new Map);$(this,"modalStates",new Map);$(this,"modalStateIssues",[]);$(this,"modalStateRelations",[]);$(this,"modalStateChangeLogs",[]);$(this,"modalStateIssueIdCounter",0);$(this,"modalStateChangeLogIdCounter",0);$(this,"maxModalStateIssues",100);$(this,"maxModalStateChangeLogs",200);$(this,"namedActionDefinitions",{});$(this,"namedActionRefLogs",[]);$(this,"namedActionRefIdCounter",0);$(this,"maxNamedActionRefLogs",200);$(this,"debounceActionHistory",[]);$(this,"dataSourceUpdateHistory",[]);$(this,"debounceActionIdCounter",0);$(this,"dataSourceUpdateIdCounter",0)}static getInstance(){return Ei.instance||(Ei.instance=new Ei),Ei.instance}initialize(){if(this.config.enabled=this.checkDebugMode(),!this.config.enabled){console.log("[G7DevTools] 비활성화됨 (환경설정 > 고급 설정 > 디버그 모드를 켜세요)");return}console.log("[G7DevTools] 활성화됨"),this.setupGlobalErrorHandler()}checkDebugMode(){try{if(window.G7Config?.debug===!0)return!0;const a=window.G7Core?.state?.get?.();return a?.settings?.advanced?.debug_mode===!0||a?._global?.settings?.advanced?.debug_mode===!0}catch{return!1}}isEnabled(){return this.config.enabled}setupGlobalErrorHandler(){window.addEventListener("error",e=>{this.config.enabled&&this.logAction({id:this.generateId(),type:"globalError",startTime:Date.now(),status:"error",error:{name:"Error",message:e.message,stack:e.error?.stack}})}),window.addEventListener("unhandledrejection",e=>{this.config.enabled&&this.logAction({id:this.generateId(),type:"unhandledRejection",startTime:Date.now(),status:"error",error:{name:"UnhandledRejection",message:String(e.reason)}})})}captureStateSnapshot(e){if(!this.config.enabled)return;const r={id:++this.snapshotIdCounter,timestamp:Date.now(),source:e.source,prev:this.sanitizeObject(e.prev),next:this.sanitizeObject(e.next),diff:e.diff};this.stateHistory.push(r),this.stateHistory.length>this.config.maxHistorySize&&this.stateHistory.shift(),this.notifyStateWatchers(r)}getState(){try{const e=window.G7Core,r=e?.state?.get?.()||{},a={};if(e?._isolatedStates&&typeof e._isolatedStates=="object")for(const[i,l]of Object.entries(e._isolatedStates))a[i]=this.sanitizeObject(l);return{_global:r._global||r,_local:this.currentLocalState,_computed:this.currentComputedState,_isolated:Object.keys(a).length>0?a:void 0,$parent:this.currentParentContext}}catch{return{_global:{},_local:this.currentLocalState,_computed:this.currentComputedState}}}updateLocalState(e){this.config.enabled&&(this.currentLocalState=this.sanitizeObject(e))}updateComputedState(e){this.config.enabled&&(this.currentComputedState=this.sanitizeObject(e))}updateParentContext(e){this.config.enabled&&(this.currentParentContext=e?this.sanitizeObject(e):void 0)}getStateHistory(){return[...this.stateHistory]}getDualStorageMismatch(){const e=this.currentLocalState||{};let r={};try{r=(window.G7Core?.state?.get?.()||{})._local||{}}catch{r={}}const a=this.findMismatchedLeafPaths(e,r);return{hasMismatch:a.length>0,mismatchedPaths:a,storageA:e,storageB:r}}findMismatchedLeafPaths(e,r,a=""){const i=[],l=new Set([...Object.keys(e||{}),...Object.keys(r||{})]);for(const c of l){const d=a?`${a}.${c}`:c,f=e?.[c],g=r?.[c],m=f&&typeof f=="object"&&!Array.isArray(f),y=g&&typeof g=="object"&&!Array.isArray(g);if(m&&y)i.push(...this.findMismatchedLeafPaths(f,g,d));else{const w=f===void 0?"__undefined__":JSON.stringify(f),S=g===void 0?"__undefined__":JSON.stringify(g);w!==S&&i.push(d)}}return i}watchState(e,r){return this.stateWatchers.has(e)||this.stateWatchers.set(e,new Set),this.stateWatchers.get(e).add(r),()=>{this.stateWatchers.get(e)?.delete(r)}}notifyStateWatchers(e){if(this.stateWatchers.get("*")?.forEach(r=>{try{r(e.next,e.prev,"*")}catch(a){console.error("[G7DevTools] State watcher error:",a)}}),e.diff)for(const r of e.diff.changed)this.stateWatchers.get(r.path)?.forEach(a=>{try{a(r.newValue,r.oldValue,r.path)}catch(i){console.error("[G7DevTools] State watcher error:",i)}})}diffSnapshots(e,r){const a=this.stateHistory.find(l=>l.id===e),i=this.stateHistory.find(l=>l.id===r);return!a||!i?null:this.calculateDiff(a.next,i.next)}calculateDiff(e,r,a=""){const i={added:[],removed:[],changed:[]},l=new Set(Object.keys(e)),c=new Set(Object.keys(r));for(const d of c){const f=a?`${a}.${d}`:d;l.has(d)?JSON.stringify(e[d])!==JSON.stringify(r[d])&&i.changed.push({path:f,oldValue:e[d],newValue:r[d]}):i.added.push(f)}for(const d of l){const f=a?`${a}.${d}`:d;c.has(d)||i.removed.push(f)}return i}logAction(e){if(!this.config.enabled)return;const r=this.actionHistory.findIndex(a=>a.id===e.id);r>=0?this.actionHistory[r]={...this.actionHistory[r],...e}:(this.actionHistory.push(e),this.actionHistory.length>this.config.maxHistorySize&&this.actionHistory.shift()),this.isProfiling&&this.profilingData.push({type:"action",action:e.type,timestamp:performance.now(),duration:e.duration}),this.notifyActionWatchers(e)}getActionHistory(){return[...this.actionHistory]}watchActions(e){return this.actionWatchers.add(e),()=>{this.actionWatchers.delete(e)}}notifyActionWatchers(e){this.actionWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] Action watcher error:",a)}})}getActionMetrics(){const e=this.actionHistory.filter(l=>l.status==="success"),r=this.actionHistory.filter(l=>l.status==="error"),a={};for(const l of this.actionHistory)a[l.type]=(a[l.type]||0)+1;const i=e.reduce((l,c)=>l+(c.duration||0),0);return{totalActions:this.actionHistory.length,successCount:e.length,errorCount:r.length,averageDuration:e.length>0?i/e.length:0,actionsByType:a}}recordCacheHit(){this.config.enabled&&this.cacheStats.hits++}recordCacheMiss(){this.config.enabled&&this.cacheStats.misses++}updateCacheEntries(e){this.config.enabled&&(this.cacheStats.entries=e)}getCacheStats(){const e=this.cacheStats.hits+this.cacheStats.misses;return{...this.cacheStats,hitRate:e>0?this.cacheStats.hits/e:0}}resetCacheStats(){this.cacheStats={hits:0,misses:0,entries:0}}trackCacheDecision(e){if(!this.config.enabled)return;const r={id:`cache-${++this.cacheDecisionIdCounter}`,timestamp:Date.now(),expression:e.expression,decision:e.decision,reason:e.reason,context:{isInIteration:this.isInIteration,isInAction:this.isInActionExecution,renderCycleId:this.currentRenderCycleId||void 0,componentId:e.componentId,skipCacheOption:e.skipCacheOption},cachedValue:e.cachedValue!==void 0?this.safeClone(e.cachedValue):void 0,freshValue:e.freshValue!==void 0?this.safeClone(e.freshValue):void 0,valueMatch:e.cachedValue!==void 0&&e.freshValue!==void 0?JSON.stringify(e.cachedValue)===JSON.stringify(e.freshValue):void 0,duration:e.duration};this.cacheDecisions.push(r),this.cacheDecisions.length>this.maxCacheDecisions&&this.cacheDecisions.shift(),e.decision==="cache_hit"?this.cacheStats.hits++:(e.decision==="cache_miss"||e.decision==="skip_cache")&&this.cacheStats.misses++}startRenderCycle(e){return this.currentRenderCycleId=`render-${Date.now()}-${e||"root"}`,this.currentRenderCycleId}endRenderCycle(){this.currentRenderCycleId=null}startActionContext(){this.isInActionExecution=!0}endActionContext(){this.isInActionExecution=!1}startIterationContext(){this.isInIteration=!0}endIterationContext(){this.isInIteration=!1}getCacheDecisionTrackingInfo(){const e=this.getCacheDecisionStats();return{decisions:[...this.cacheDecisions],stats:e,timestamp:Date.now()}}getCacheDecisionStats(){const e=this.cacheDecisions;let r=0,a=0,i=0,l=0;const c={},d={};for(const g of e)g.decision==="cache_hit"?r++:g.decision==="cache_miss"?a++:g.decision==="skip_cache"?i++:g.decision==="invalidate"&&l++,c[g.reason]=(c[g.reason]||0)+1,g.context.componentId&&(d[g.context.componentId]=(d[g.context.componentId]||0)+1);const f=r+a+i;return{totalDecisions:e.length,cacheHits:r,cacheMisses:a,skipCacheCount:i,invalidateCount:l,byReason:c,byComponent:d,avgHitRate:f>0?r/f:0}}getRecentCacheDecisions(e=20,r){let a=[...this.cacheDecisions];return r&&(a=a.filter(i=>i.decision===r)),a.slice(-e)}clearCacheDecisionData(){this.cacheDecisions=[],this.cacheDecisionIdCounter=0}summarizePropsForLifecycle(e){if(!e||typeof e!="object")return e;const r={},a=["children","ref","key"];for(const[i,l]of Object.entries(e))if(!a.includes(i))if(l==null)r[i]=l;else if(Array.isArray(l))r[i]=`[Array(${l.length})]`;else if(typeof l=="object"){const c=Object.keys(l);c.length<=3?r[i]=`{${c.join(", ")}}`:r[i]=`{${c.slice(0,3).join(", ")}, ... +${c.length-3}}`}else typeof l=="string"&&l.length>50?r[i]=l.substring(0,50)+"...":typeof l=="function"?r[i]="[Function]":r[i]=l;return r}trackMount(e,r){if(!this.config.enabled)return;const a=this.summarizePropsForLifecycle(r.props),i={...r,props:a,id:e,mountTime:Date.now()};this.mountedComponents.set(e,i),this.mountWatchers.forEach(l=>{try{l({componentId:e,componentName:r.name,timestamp:i.mountTime})}catch(c){console.error("[G7DevTools] Mount watcher error:",c)}})}trackUnmount(e){if(!this.config.enabled)return;const r=this.mountedComponents.get(e),a=this.eventListeners.get(e)||[];a.length>0&&console.warn(`[G7DevTools] 컴포넌트 ${e} 언마운트 시 ${a.length}개 리스너 미정리`),r&&this.unmountWatchers.forEach(i=>{try{i({componentId:e,componentName:r.name,timestamp:Date.now(),orphanedListeners:a.length})}catch(l){console.error("[G7DevTools] Unmount watcher error:",l)}}),this.mountedComponents.delete(e)}trackListener(e,r,a){if(!this.config.enabled)return;const i=this.eventListeners.get(e)||[];i.push({type:r,target:a,addedAt:Date.now()}),this.eventListeners.set(e,i)}removeListener(e,r){if(!this.config.enabled)return;const a=this.eventListeners.get(e)||[],i=a.findIndex(l=>l.type===r);i>=0&&a.splice(i,1)}getMountedComponents(){return Array.from(this.mountedComponents.values())}getOrphanedListeners(){const e=[];for(const[r,a]of this.eventListeners)this.mountedComponents.has(r)||e.push(...a.map(i=>({...i,componentId:r})));return e}watchMount(e){return this.mountWatchers.add(e),()=>{this.mountWatchers.delete(e)}}watchUnmount(e){return this.unmountWatchers.add(e),()=>{this.unmountWatchers.delete(e)}}trackRender(e){if(!this.config.enabled)return;const r=this.renderCounts.get(e)||0;this.renderCounts.set(e,r+1),this.isProfiling&&this.profilingData.push({type:"render",component:e,timestamp:performance.now()})}trackBindingEval(e){this.config.enabled&&(this.bindingEvalCount++,this.isProfiling&&e&&this.profilingData.push({type:"binding",expression:e,timestamp:performance.now()}))}getRenderCount(){return new Map(this.renderCounts)}getBindingEvalCount(){return this.bindingEvalCount}getMemoryWarnings(){const e=[];try{const i=this.getState(),l=JSON.stringify(i).length;l>1e6&&e.push({type:"large-state",message:`상태 크기가 ${(l/1024/1024).toFixed(2)}MB입니다`,suggestion:"불필요한 데이터 정리 필요",severity:l>5e6?"error":"warning"})}catch{}this.stateHistory.length>this.config.maxHistorySize*.8&&e.push({type:"large-history",message:`상태 이력이 ${this.stateHistory.length}개입니다`,suggestion:"maxHistory 설정 조정 필요",severity:"warning"});const r=this.getOrphanedListeners().length;r>0&&e.push({type:"orphaned-listeners",message:`${r}개의 정리되지 않은 이벤트 리스너`,suggestion:"useEffect cleanup 또는 removeEventListener 확인",severity:r>10?"error":"warning"});const a=Array.from(this.renderCounts.entries()).filter(([,i])=>i>50);return a.length>0&&e.push({type:"excessive-renders",message:`${a.length}개 컴포넌트가 50회 이상 렌더링`,suggestion:"React.memo, useMemo, useCallback 사용 고려",severity:"warning"}),e}startProfiling(){this.config.enabled&&(this.isProfiling=!0,this.profilingData=[],this.profilingStartTime=performance.now(),console.log("[G7DevTools] 프로파일링 시작"))}stopProfiling(){this.isProfiling=!1;const e=performance.now()-this.profilingStartTime,r=this.analyzeProfile(e);return console.log("[G7DevTools] 프로파일링 완료",r),r}analyzeProfile(e){const r=this.profilingData.filter(g=>g.type==="render"),a=this.profilingData.filter(g=>g.type==="binding"),i=this.profilingData.filter(g=>g.type==="action"),l={};for(const g of r)g.component&&(l[g.component]||(l[g.component]={count:0,durations:[]}),l[g.component].count++,g.duration&&l[g.component].durations.push(g.duration));const c=Object.entries(l).map(([g,m])=>({name:g,renderCount:m.count,avgDuration:m.durations.length>0?m.durations.reduce((y,w)=>y+w,0)/m.durations.length:0})).sort((g,m)=>m.renderCount-g.renderCount).slice(0,10),d={};for(const g of a)g.expression&&(d[g.expression]=(d[g.expression]||0)+1);const f=Object.entries(d).sort(([,g],[,m])=>m-g).slice(0,10).map(([g])=>g);return{duration:e,entries:this.profilingData,summary:{totalRenders:r.length,totalBindings:a.length,totalActions:i.length,slowestComponents:c,hotPaths:f}}}resetPerformanceStats(){this.renderCounts.clear(),this.bindingEvalCount=0,this.profilingData=[]}trackRequest(e,r,a){if(!this.config.enabled)return"";const i=this.generateId(),l=l_(e),c=o_(e);return this.activeRequests.set(i,{id:i,url:l,fullUrl:e,queryParams:c,method:r,startTime:Date.now(),status:"pending",requestBody:a?.requestBody?this.sanitizeObject(a.requestBody):void 0,dataSourceId:a?.dataSourceId}),this.isProfiling&&this.profilingData.push({type:"network",timestamp:performance.now()}),i}completeRequest(e,r,a){if(!this.config.enabled)return;const i=this.activeRequests.get(e);if(!i)return;const l={...i,status:r>=200&&r<300?"success":"error",statusCode:r,duration:Date.now()-i.startTime,endTime:Date.now(),response:this.sanitizeObject(a)};this.requestHistory.push(l),this.activeRequests.delete(e),this.requestHistory.length>this.config.maxHistorySize&&this.requestHistory.shift(),this.notifyRequestWatchers(l)}failRequest(e,r){if(!this.config.enabled)return;const a=this.activeRequests.get(e);if(!a)return;const i={...a,status:"error",error:r,duration:Date.now()-a.startTime,endTime:Date.now()};this.requestHistory.push(i),this.activeRequests.delete(e),this.notifyRequestWatchers(i)}trackDataSource(e){this.config.enabled&&this.pendingDataSources.add(e)}completeDataSource(e){this.config.enabled&&this.pendingDataSources.delete(e)}getActiveRequests(){return Array.from(this.activeRequests.values())}getRequestHistory(){return[...this.requestHistory]}getPendingDataSources(){return Array.from(this.pendingDataSources)}watchRequest(e){return this.requestWatchers.add(e),()=>{this.requestWatchers.delete(e)}}notifyRequestWatchers(e){this.requestWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] Request watcher error:",a)}})}trackWebSocketConnection(e,r,a){this.config.enabled&&this.wsConnections.set(e,{id:e,url:r,state:a,connectedAt:a==="open"?Date.now():void 0})}trackWebSocketMessage(e,r,a,i){if(!this.config.enabled)return;const l={id:this.generateId(),connectionId:e,direction:r,type:a,payload:this.sanitizeObject(i),timestamp:Date.now(),sequence:this.wsMessageHistory.length};this.wsMessageHistory.push(l),this.wsMessageHistory.length>this.config.maxHistorySize&&this.wsMessageHistory.shift(),this.notifyWebSocketWatchers(l)}getWebSocketConnections(){return Array.from(this.wsConnections.values())}getWebSocketMessageHistory(){return[...this.wsMessageHistory]}getWebSocketConnectionState(){const e=Array.from(this.wsConnections.values());return e.some(r=>r.state==="open")?"connected":e.some(r=>r.state==="connecting")?"reconnecting":"disconnected"}watchWebSocketMessage(e){return this.wsMessageWatchers.add(e),()=>{this.wsMessageWatchers.delete(e)}}notifyWebSocketWatchers(e){this.wsMessageWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] WebSocket watcher error:",a)}})}trackIfCondition(e,r,a){if(!this.config.enabled)return;const i=this.ifConditions.get(e),l=i?.evaluatedValue;this.ifConditions.set(e,{id:e,expression:r,evaluatedValue:a,evaluationCount:(i?.evaluationCount||0)+1,lastEvaluated:Date.now()}),i&&l!==a&&this.notifyConditionWatchers({id:e,expression:r,oldValue:l,newValue:a,timestamp:Date.now()})}trackIteration(e,r){this.config.enabled&&this.iterations.set(e,{id:e,...r,lastRendered:Date.now()})}getIfConditions(){return Array.from(this.ifConditions.values())}getIterations(){return Array.from(this.iterations.values())}getScopeChain(e){const r=[],a=this.getState();return r.push({name:"_global",value:a._global,source:"global"}),r.push({name:"_local",value:a._local,source:"local"}),r.push({name:"_computed",value:a._computed,source:"computed"}),r}watchConditionChange(e){return this.conditionWatchers.add(e),()=>{this.conditionWatchers.delete(e)}}notifyConditionWatchers(e){this.conditionWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] Condition watcher error:",a)}})}trackForm(e,r,a){this.config.enabled&&this.forms.set(e,{id:e,dataKey:r,inputs:a,trackedAt:Date.now()})}trackFormChange(e,r,a){if(!this.config.enabled)return;const i={formId:e,inputName:r,value:a,timestamp:Date.now()};this.notifyFormWatchers(i)}untrackForm(e){this.forms.delete(e)}getForms(){return Array.from(this.forms.values())}getFormState(e){const r=this.getState();return this.getNestedValue(r,e)||{}}getBindingPath(e,r){return`${e}.${r}`}watchFormChange(e){return this.formWatchers.add(e),()=>{this.formWatchers.delete(e)}}notifyFormWatchers(e){this.formWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] Form watcher error:",a)}})}trackFormBindingIssue(e,r,a){if(!this.config.enabled)return;const i={id:`form-issue-${++this.formBindingIssueIdCounter}`,timestamp:Date.now(),type:e,severity:r,formId:a.formId,formDataKey:a.formDataKey,inputInfo:a.inputInfo,contextInfo:a.contextInfo,description:a.description,suggestion:a.suggestion,docLink:a.docLink};this.formBindingIssues.push(i),this.formBindingIssues.length>this.maxFormBindingIssues&&this.formBindingIssues.shift()}validateFormBinding(e,r,a,i){const l=a.map(f=>{const g=[];let m=!0;return f.name||(g.push("Input에 name 속성이 없습니다"),m=!1,this.trackFormBindingIssue("missing-input-name","error",{formId:e,formDataKey:r,inputInfo:{name:f.name,type:f.type},description:"Input에 name 속성이 없어 자동 바인딩이 동작하지 않습니다",suggestion:"Input에 name 속성을 추가하세요",docLink:"troubleshooting-components-form.md"})),r||(g.push("Form에 dataKey가 없습니다"),m=!1),{inputName:f.name||"(unnamed)",inputType:f.type,bindingPath:r&&f.name?`${r}.${f.name}`:"-",isValid:m,currentValue:f.value,issues:g}});if(r||this.trackFormBindingIssue("missing-datakey","error",{formId:e,description:"Form에 dataKey가 설정되지 않아 자동 바인딩이 동작하지 않습니다",suggestion:"Form 컴포넌트에 dataKey props를 설정하세요",docLink:"troubleshooting-components-form.md"}),i.isContextBroken){const f=i.breakReason?.includes("sortable")?"sortable-context-break":i.breakReason?.includes("modal")?"modal-context-isolation":"context-not-propagated";this.trackFormBindingIssue(f,"warning",{formId:e,formDataKey:r,contextInfo:{hasParentFormContext:i.hasParentContext,parentFormContextProp:i.parentContextProp,isInsideSortable:i.breakReason?.includes("sortable")||!1,isInsideModal:i.breakReason?.includes("modal")||!1,depth:0},description:i.breakReason||"Form 컨텍스트가 제대로 전파되지 않았습니다",suggestion:"parentFormContextProp={undefined}를 확인하거나 중첩 구조를 검토하세요",docLink:"troubleshooting-components-form.md"})}const c={formId:e,dataKey:r,timestamp:Date.now(),contextPropagation:i,inputBindings:l,isValid:l.every(f=>f.isValid)&&!!r&&!i.isContextBroken,issueCount:l.filter(f=>!f.isValid).length+(r?0:1)+(i.isContextBroken?1:0)},d=this.formBindingValidations.findIndex(f=>f.formId===e);return d>=0?this.formBindingValidations[d]=c:this.formBindingValidations.push(c),c}getFormBindingValidationTrackingInfo(){const e=this.getFormBindingValidationStats();return{issues:[...this.formBindingIssues],validations:[...this.formBindingValidations],stats:e,timestamp:Date.now()}}getFormBindingValidationStats(){const e={"missing-datakey":0,"missing-input-name":0,"context-not-propagated":0,"sortable-context-break":0,"modal-context-isolation":0,"duplicate-input-name":0,"value-type-mismatch":0,"binding-path-invalid":0},r={info:0,warning:0,error:0};for(const i of this.formBindingIssues)e[i.type]++,r[i.severity]++;const a=Object.entries(e).filter(([,i])=>i>0).sort((i,l)=>l[1]-i[1]).slice(0,5).map(([i,l])=>({type:i,count:l,description:this.getIssueTypeDescription(i)}));return{totalFormsValidated:this.formBindingValidations.length,validForms:this.formBindingValidations.filter(i=>i.isValid).length,formsWithIssues:this.formBindingValidations.filter(i=>!i.isValid).length,totalIssues:this.formBindingIssues.length,byIssueType:e,bySeverity:r,topIssues:a}}getIssueTypeDescription(e){return{"missing-datakey":"Form에 dataKey 누락","missing-input-name":"Input에 name 속성 누락","context-not-propagated":"Form 컨텍스트 전파 실패","sortable-context-break":"Sortable 컨테이너에서 컨텍스트 단절","modal-context-isolation":"모달에서 부모 Form 컨텍스트 격리","duplicate-input-name":"동일 Form 내 중복 Input name","value-type-mismatch":"값 타입과 Input 타입 불일치","binding-path-invalid":"바인딩 경로 유효하지 않음"}[e]||e}clearFormBindingIssues(){this.formBindingIssues=[],this.formBindingValidations=[],this.formBindingIssueIdCounter=0}trackComputedProperty(e,r,a,i,l,c,d){if(!this.config.enabled)return;const f=`computed-${e}-${c||"global"}`,g={id:f,name:e,expression:r,componentId:c,dependencies:a,currentValue:this.safeClone(i),lastComputedAt:Date.now(),computationTime:l,error:d};this.computedProperties.set(f,g)}trackComputedRecalc(e,r,a,i,l,c,d){if(!this.config.enabled)return;const f=`computed-${e}-${d||"global"}`,g=JSON.stringify(a)!==JSON.stringify(i),m={id:`recalc-${++this.computedRecalcIdCounter}`,computedId:f,computedName:e,timestamp:Date.now(),trigger:r,triggeredBy:c,previousValue:this.safeClone(a),newValue:this.safeClone(i),valueChanged:g,computationTime:l,cascadeCount:0};this.computedRecalcLogs.push(m),this.computedRecalcLogs.length>this.maxComputedRecalcLogs&&this.computedRecalcLogs.shift();const y=this.computedProperties.get(f);y&&(y.currentValue=this.safeClone(i),y.lastComputedAt=Date.now(),y.computationTime=l)}analyzeComputedDependencyChain(e,r){const a=new Set,i=[];let l=!1,c,d=0;const f=(m,y)=>{if(d=Math.max(d,y),a.has(m))return l=!0,c=[...i,m],{name:m,type:"computed",children:[],depth:y};a.add(m),i.push(m);const w=this.computedProperties.get(`computed-${m}-${r||"global"}`),S=[];if(w)for(const v of w.dependencies)v.type==="computed"?S.push(f(v.path,y+1)):S.push({name:v.path,type:v.type,children:[],depth:y+1});return i.pop(),a.delete(m),{name:m,type:"computed",children:S,depth:y}},g=f(e,0);return{root:e,tree:g,hasCycle:l,cyclePath:c,maxDepth:d}}getComputedDependencyTrackingInfo(){const e=Array.from(this.computedProperties.values()),r=this.getComputedDependencyStats(),a=[];for(const i of e){const l=this.analyzeComputedDependencyChain(i.name,i.componentId);a.push(l)}return{properties:e,recalcLogs:[...this.computedRecalcLogs],dependencyChains:a,stats:r,timestamp:Date.now()}}getComputedDependencyStats(){const e={"state-change":0,"datasource-update":0,"dependency-change":0,manual:0,initial:0},r={},a={};let i=0,l=0;for(const g of this.computedRecalcLogs)e[g.trigger]++,g.valueChanged||l++,r[g.computedName]=(r[g.computedName]||0)+1,a[g.computedName]||(a[g.computedName]=[]),a[g.computedName].push(g.computationTime),i+=g.computationTime;const c=Object.entries(r).sort((g,m)=>m[1]-g[1]).slice(0,5).map(([g,m])=>({name:g,count:m})),d=Object.entries(a).map(([g,m])=>({name:g,avgTime:m.reduce((y,w)=>y+w,0)/m.length})).sort((g,m)=>m.avgTime-g.avgTime).slice(0,5);let f=0;for(const g of this.computedProperties.values())this.analyzeComputedDependencyChain(g.name,g.componentId).hasCycle&&f++;return{totalComputed:this.computedProperties.size,totalRecalculations:this.computedRecalcLogs.length,unnecessaryRecalculations:l,avgComputationTime:this.computedRecalcLogs.length>0?i/this.computedRecalcLogs.length:0,topRecalculated:c,slowestComputed:d,cycleDetectionCount:f,byTrigger:e}}clearComputedTracking(){this.computedProperties.clear(),this.computedRecalcLogs=[],this.computedRecalcIdCounter=0}trackModalOpen(e){if(!this.config.enabled)return;const r={modalId:e.modalId,modalName:e.modalName,openedAt:Date.now(),closedAt:null,scopeType:e.scopeType||"isolated",parentModalId:e.parentModalId,componentId:e.componentId,initialState:e.initialState?{...e.initialState}:{},currentState:e.initialState?{...e.initialState}:{},stateChangeCount:0,isolatedStateKeys:e.isolatedStateKeys||[],sharedStateKeys:e.sharedStateKeys||[]};this.modalStates.set(e.modalId,r),this.validateModalDefinitionExists(e.modalId,e.modalName),e.parentModalId&&(this.modalStateRelations.find(i=>i.parentModalId===e.parentModalId&&i.childModalId===e.modalId)||this.modalStateRelations.push({parentModalId:e.parentModalId,childModalId:e.modalId,sharedKeys:e.sharedStateKeys||[],isolatedKeys:e.isolatedStateKeys||[],relationType:"parent-child"}))}trackModalClose(e,r){if(!this.config.enabled)return;const a=this.modalStates.get(e);a&&(a.closedAt=Date.now(),r&&(a.currentState={...r}),this.detectStateLeakage(a))}validateModalDefinitionExists(e,r){const a=this.currentLayout;if(!a?.layoutJson)return;const i=a.layoutJson.modals;if(!i){this.recordModalStateIssue({type:"missing-definition",modalId:e,modalName:r,severity:"error",description:`모달 "${e}"이(가) modalStack에 추가되었으나, 현재 레이아웃에 modals 섹션이 없습니다. 레이아웃 경로: ${a.layoutPath}`,affectedStateKeys:[],expectedValue:`modals 섹션에 id="${e}" 정의 존재`,actualValue:"modals 섹션 없음"});return}const l=Array.isArray(i)?i:Object.values(i);l.some(d=>d?.id===e||d?.props?.id===e)||this.recordModalStateIssue({type:"missing-definition",modalId:e,modalName:r,severity:"error",description:`모달 "${e}"이(가) modalStack에 추가되었으나, 렌더링된 레이아웃의 modals 섹션에 해당 ID의 정의가 없습니다. partial 로딩 실패, 레이아웃 병합 문제, extends 상속 누락 등을 확인하세요. 레이아웃 경로: ${a.layoutPath}`,affectedStateKeys:[],expectedValue:`modals 배열에 id="${e}" 항목 존재`,actualValue:`modals 배열에 ${l.length}개 모달 정의 (${l.map(d=>d?.id||"unknown").join(", ")})`})}trackModalStateChange(e){if(!this.config.enabled)return;const r=this.modalStates.get(e.modalId);if(!r)return;r.stateChangeCount++,r.currentState[e.stateKey]=e.newValue;const a=this.checkIsolationViolation(r,e.stateKey),i={id:`modal-change-${++this.modalStateChangeLogIdCounter}`,modalId:e.modalId,modalName:r.modalName,timestamp:Date.now(),stateKey:e.stateKey,previousValue:e.previousValue,newValue:e.newValue,changeSource:e.changeSource||"user-action",violatesIsolation:a};this.modalStateChangeLogs.push(i),this.modalStateChangeLogs.length>this.maxModalStateChangeLogs&&this.modalStateChangeLogs.shift(),a&&this.recordModalStateIssue({type:"isolation-violation",modalId:e.modalId,modalName:r.modalName,severity:"warning",description:`모달 '${r.modalName}'에서 격리된 상태 키 '${e.stateKey}'가 변경됨`,affectedStateKeys:[e.stateKey]})}detectStateLeakage(e){if(e.scopeType==="isolated")for(const r of e.isolatedStateKeys){const a=e.initialState[r],i=e.currentState[r];if(a!==i&&e.parentModalId){const l=this.modalStates.get(e.parentModalId);l&&l.currentState.hasOwnProperty(r)&&this.recordModalStateIssue({type:"state-leakage",modalId:e.modalId,modalName:e.modalName,severity:"error",description:`모달 닫힘 후 상태 '${r}'가 부모 모달로 유출될 수 있음`,affectedStateKeys:[r],leakedValue:i,expectedValue:a})}}}checkIsolationViolation(e,r){return e.scopeType==="isolated",!1}recordModalStateIssue(e){const r={id:`modal-issue-${++this.modalStateIssueIdCounter}`,type:e.type,modalId:e.modalId,modalName:e.modalName,timestamp:Date.now(),severity:e.severity,description:e.description,affectedStateKeys:e.affectedStateKeys,leakedValue:e.leakedValue,expectedValue:e.expectedValue,actualValue:e.actualValue,stackInfo:e.stackInfo};this.modalStateIssues.push(r),this.modalStateIssues.length>this.maxModalStateIssues&&this.modalStateIssues.shift()}getModalStateScopeTrackingInfo(){const e=Array.from(this.modalStates.values()),r=this.getModalStateScopeStats();return{modals:e,issues:[...this.modalStateIssues],relations:[...this.modalStateRelations],changeLogs:[...this.modalStateChangeLogs],stats:r,timestamp:Date.now()}}getModalStateScopeStats(){const e=Array.from(this.modalStates.values()),r={warning:0,error:0},a={"state-leakage":0,"isolation-violation":0,"parent-mutation":0,"orphaned-state":0,"scope-mismatch":0,"cleanup-failure":0,"missing-definition":0};for(const l of this.modalStateIssues)r[l.severity]++,a[l.type]++;const i={isolated:0,shared:0,inherited:0};for(const l of e)i[l.scopeType]++;return{totalModals:e.length,openModals:e.filter(l=>l.closedAt===null).length,nestedModals:e.filter(l=>l.parentModalId!==void 0).length,totalIssues:this.modalStateIssues.length,issuesBySeverity:r,issuesByType:a,byScope:i,leakageDetectionCount:a["state-leakage"],cleanupFailureCount:a["cleanup-failure"]}}clearModalStateScopeTracking(){this.modalStates.clear(),this.modalStateIssues=[],this.modalStateRelations=[],this.modalStateChangeLogs=[],this.modalStateIssueIdCounter=0,this.modalStateChangeLogIdCounter=0}setNamedActionDefinitions(e){this.config.enabled&&(this.namedActionDefinitions=e||{})}trackNamedActionRef(e){if(!this.config.enabled)return;const r={...e,id:`named_action_ref_${++this.namedActionRefIdCounter}`};this.namedActionRefLogs.push(r),this.namedActionRefLogs.length>this.maxNamedActionRefLogs&&(this.namedActionRefLogs=this.namedActionRefLogs.slice(-this.maxNamedActionRefLogs))}getNamedActionTrackingInfo(){const e={};for(const a of this.namedActionRefLogs)e[a.actionRefName]=(e[a.actionRefName]||0)+1;const r=Object.keys(this.namedActionDefinitions).filter(a=>!e[a]);return{definitions:this.namedActionDefinitions,refLogs:[...this.namedActionRefLogs],stats:{totalDefinitions:Object.keys(this.namedActionDefinitions).length,totalRefs:this.namedActionRefLogs.length,refCountByName:e,unusedDefinitions:r},timestamp:Date.now()}}clearNamedActionTracking(){this.namedActionDefinitions={},this.namedActionRefLogs=[],this.namedActionRefIdCounter=0}trackExpressionEval(e){if(!this.config.enabled)return;const r={...e,id:`expr-${++this.expressionIdCounter}`,timestamp:Date.now(),warning:this.detectExpressionWarning(e)};this.expressionHistory.push(r),this.expressionHistory.length>500&&this.expressionHistory.shift(),this.isProfiling&&this.profilingData.push({type:"binding",expression:e.expression,timestamp:performance.now(),duration:e.duration}),this.notifyExpressionWatchers(r)}detectExpressionWarning(e){const{expression:r,result:a,resultType:i,method:l,duration:c}=e;if(i==="undefined"&&!r.includes("??")&&!r.includes("||")&&/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(r.replace(/{{|}}/g,"").trim()))return{type:"undefined-result",message:"표현식 결과가 undefined입니다",suggestion:`데이터가 로드되었는지 확인하거나 fallback 값을 추가하세요: ${r} ?? ''`};if(i==="null")return{type:"null-result",message:"표현식 결과가 null입니다",suggestion:`nullish coalescing 연산자를 사용하세요: ${r} ?? ''`};if(l==="resolveBindings"&&i==="string"){const d=String(a);if(d.includes("[object Object]")||/^\[.*\]$/.test(d))return{type:"array-to-string",message:"배열/객체가 문자열로 변환되었습니다",suggestion:"evaluateExpression()을 사용하여 원본 타입을 유지하세요"}}if(l==="resolveBindings"&&i==="string"&&String(a).includes("[object Object]"))return{type:"object-to-string",message:"객체가 [object Object]로 변환되었습니다",suggestion:"evaluateExpression()을 사용하거나 특정 속성에 접근하세요"};if(i==="undefined"&&r.includes(".")&&!r.includes("?.")){const d=r.replace(/{{|}}/g,"").trim().split(".");if(d.length>=2)return{type:"missing-optional-chain",message:"Optional chaining(?.) 누락 가능성",suggestion:`안전한 접근을 위해 ?. 사용을 고려하세요: ${d.join("?.")}`}}if((r.includes("{{item.")||r.includes("{{index}}"))&&Array.from(this.iterations.values()).some(g=>g.itemVar!=="item"))return{type:"wrong-iteration-var",message:"'item'/'index' 대신 iteration에서 정의한 변수명을 사용해야 합니다",suggestion:"iteration의 item_var/index_var에 정의된 변수명을 확인하세요"};if(c&&c>10)return{type:"slow-evaluation",message:`표현식 평가에 ${c.toFixed(2)}ms 소요됨`,suggestion:"복잡한 표현식을 단순화하거나 computed 값으로 분리하세요"}}getExpressions(){return[...this.expressionHistory]}getExpressionWarnings(){return this.expressionHistory.filter(e=>e.warning!=null)}searchExpressions(e){const r=e.toLowerCase();return this.expressionHistory.filter(a=>a.expression.toLowerCase().includes(r)||a.componentName?.toLowerCase().includes(r)||a.propName?.toLowerCase().includes(r))}getExpressionStats(){const e=this.expressionHistory,r=new Set(e.map(f=>f.expression)),a=e.filter(f=>f.warning!=null),i=e.filter(f=>f.fromCache),l=e.filter(f=>f.duration!=null).map(f=>f.duration),c={};for(const f of e)c[f.resultType]=(c[f.resultType]||0)+1;const d={};for(const f of a)f.warning&&(d[f.warning.type]=(d[f.warning.type]||0)+1);return{totalEvaluations:e.length,uniqueExpressions:r.size,warningCount:a.length,cacheHitRate:e.length>0?i.length/e.length:0,averageDuration:l.length>0?l.reduce((f,g)=>f+g,0)/l.length:0,byType:c,byWarning:d}}clearExpressions(){this.expressionHistory=[],this.expressionIdCounter=0}watchExpressions(e){return this.expressionWatchers.add(e),()=>{this.expressionWatchers.delete(e)}}notifyExpressionWatchers(e){this.expressionWatchers.forEach(r=>{try{r(e)}catch(a){console.error("[G7DevTools] Expression watcher error:",a)}})}setLogLevel(e){this.config.logLevel=e}setMaxHistory(e){this.config.maxHistorySize=Math.max(10,Math.min(1e3,e))}setServerEndpoint(e){this.config.serverEndpoint=e}getConfig(){return{...this.config}}generateId(){return`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}sanitizeObject(e,r=new WeakSet){if(e===null||typeof e!="object")return e;if(r.has(e))return"[Circular]";if(r.add(e),Array.isArray(e))return e.map(i=>this.sanitizeObject(i,r));const a={};for(const i of Object.keys(e))try{a[i]=this.sanitizeObject(e[i],r)}catch{a[i]="[Unable to serialize]"}return a}getNestedValue(e,r){if(!r)return e;const a=r.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}getLifecycleInfo(){return{mountedComponents:Array.from(this.mountedComponents.values()),orphanedListeners:this.getOrphanedListeners()}}getPerformanceInfo(){return{renderCounts:this.renderCounts,bindingEvalCount:this.bindingEvalCount,memoryWarnings:this.getMemoryWarnings()}}getNetworkInfo(){return{activeRequests:Array.from(this.activeRequests.values()),requestHistory:[...this.requestHistory],pendingDataSources:Array.from(this.pendingDataSources)}}getConditionalInfo(){return{ifConditions:Array.from(this.ifConditions.values()),iterations:Array.from(this.iterations.values())}}getFormInfo(){return Array.from(this.forms.values())}getWebSocketInfo(){return{connections:Array.from(this.wsConnections.values()),messageHistory:[...this.wsMessageHistory],connectionState:this.getWebSocketConnectionState()}}getStateAtTime(e){const r=[...this.stateHistory].reverse().find(a=>a.timestamp<=e);if(r)return{_global:r.next,_local:{}}}reset(){this.stateHistory=[],this.actionHistory=[],this.cacheStats={hits:0,misses:0,entries:0},this.cacheDecisions=[],this.cacheDecisionIdCounter=0,this.currentRenderCycleId=null,this.isInActionExecution=!1,this.isInIteration=!1,this.mountedComponents.clear(),this.eventListeners.clear(),this.renderCounts.clear(),this.bindingEvalCount=0,this.profilingData=[],this.activeRequests.clear(),this.requestHistory=[],this.pendingDataSources.clear(),this.wsConnections.clear(),this.wsMessageHistory=[],this.ifConditions.clear(),this.iterations.clear(),this.forms.clear(),this.expressionHistory=[],this.expressionIdCounter=0,this.dataSources.clear(),this.dataPathTransforms=[],this.nestedContexts=[],this.nestedContextIdCounter=0,this.formBindingIssues=[],this.formBindingValidations=[],this.formBindingIssueIdCounter=0,this.computedProperties.clear(),this.computedRecalcLogs=[],this.computedRecalcIdCounter=0,this.modalStates.clear(),this.modalStateIssues=[],this.modalStateRelations=[],this.modalStateChangeLogs=[],this.modalStateIssueIdCounter=0,this.modalStateChangeLogIdCounter=0,this.logHistory=[],this.logIdCounter=0}getExpressionInfo(){return{expressions:this.getExpressions(),stats:this.getExpressionStats()}}trackLog(e,r,a){if(!this.config.enabled)return;const i=a.map(c=>{if(typeof c=="string")return c;try{return JSON.stringify(c)}catch{return String(c)}}).join(" "),l={id:`log-${++this.logIdCounter}`,level:e,prefix:r,message:i,args:this.sanitizeObject(a),timestamp:Date.now(),stack:e==="error"?new Error().stack:void 0};this.logHistory.push(l),this.logHistory.length>this.maxLogHistory&&this.logHistory.shift()}trackAction(e){if(!this.config.enabled)return;const r={id:`debounce-action-${++this.debounceActionIdCounter}`,...e,timestamp:Date.now()};this.debounceActionHistory.push(r),this.debounceActionHistory.length>100&&this.debounceActionHistory.shift()}trackDataSourceUpdate(e){if(!this.config.enabled)return;const r={id:`ds-update-${++this.dataSourceUpdateIdCounter}`,...e};this.dataSourceUpdateHistory.push(r),this.dataSourceUpdateHistory.length>100&&this.dataSourceUpdateHistory.shift()}getDebounceActionHistory(e){let r=[...this.debounceActionHistory];return e?.handler&&(r=r.filter(a=>a.handler.includes(e.handler))),e?.status&&(r=r.filter(a=>a.status===e.status)),e?.limit&&(r=r.slice(-e.limit)),r}getDataSourceUpdateHistory(e){let r=[...this.dataSourceUpdateHistory];return e?.dataSourceId&&(r=r.filter(a=>a.dataSourceId===e.dataSourceId)),e?.updateType&&(r=r.filter(a=>a.updateType===e.updateType)),e?.limit&&(r=r.slice(-e.limit)),r}getLogs(e){let r=[...this.logHistory];if(e?.level){const a=Array.isArray(e.level)?e.level:[e.level];r=r.filter(i=>a.includes(i.level))}if(e?.prefix){const a=e.prefix.toLowerCase();r=r.filter(i=>i.prefix.toLowerCase().includes(a))}if(e?.search){const a=e.search.toLowerCase();r=r.filter(i=>i.message.toLowerCase().includes(a))}return e?.since&&(r=r.filter(a=>a.timestamp>=e.since)),e?.limit&&(r=r.slice(-e.limit)),r}getLogStats(){const r=Date.now()-6e4,a={log:0,warn:0,error:0,debug:0,info:0},i={};let l=0,c=0;for(const d of this.logHistory)a[d.level]++,i[d.prefix]=(i[d.prefix]||0)+1,d.timestamp>=r&&(d.level==="error"&&l++,d.level==="warn"&&c++);return{totalLogs:this.logHistory.length,byLevel:a,byPrefix:i,recentErrors:l,recentWarnings:c}}clearLogs(){this.logHistory=[],this.logIdCounter=0}getLogInfo(){return{entries:this.getLogs(),stats:this.getLogStats()}}trackDataSourceDefinition(e){this.config.enabled&&this.dataSources.set(e.id,{...e,status:"idle"})}trackDataSourceLoading(e){if(!this.config.enabled)return;const r=this.dataSources.get(e);r&&this.dataSources.set(e,{...r,status:"loading"})}trackDataSourceLoaded(e,r,a){if(!this.config.enabled)return;const i=this.dataSources.get(e);if(!i)return;let l,c,d=a;r&&(Array.isArray(r)?(l=r.length,c=r.length>0&&typeof r[0]=="object"?Object.keys(r[0]):void 0):typeof r=="object"&&(Array.isArray(r.data)?(d=d||"data",l=r.data.length,c=r.data.length>0&&typeof r.data[0]=="object"?Object.keys(r.data[0]):void 0):Array.isArray(r.items)?(d=d||"items",l=r.items.length,c=r.items.length>0&&typeof r.items[0]=="object"?Object.keys(r.items[0]):void 0):c=Object.keys(r))),this.dataSources.set(e,{...i,status:"loaded",dataPath:d,itemCount:l,keys:c,lastLoadedAt:Date.now(),error:void 0})}trackDataSourceError(e,r){if(!this.config.enabled)return;const a=this.dataSources.get(e);a&&this.dataSources.set(e,{...a,status:"error",error:r,lastLoadedAt:Date.now()})}untrackDataSource(e){this.dataSources.delete(e)}getDataSources(){return Array.from(this.dataSources.values())}getDataSource(e){return this.dataSources.get(e)}clearDataSources(){this.dataSources.clear()}trackDataPathTransform(e){if(!this.config.enabled)return;let r=this.dataPathTransforms.find(a=>a.dataSourceId===e.dataSourceId);r||(r={dataSourceId:e.dataSourceId,timestamp:Date.now(),transformSteps:[],warnings:[]},this.dataPathTransforms.push(r),this.dataPathTransforms.length>this.maxDataPathTransforms&&this.dataPathTransforms.shift()),r.transformSteps.push({step:e.step,inputPath:e.inputPath,inputValue:this.safeClone(e.inputValue),outputPath:e.outputPath,outputValue:this.safeClone(e.outputValue),config:e.config}),e.warning&&r.warnings.push(e.warning)}setDataPathFinalBinding(e,r){if(!this.config.enabled)return;const a=this.dataPathTransforms.find(i=>i.dataSourceId===e);a&&(a.finalBinding={expression:r.expression,resolvedPath:r.resolvedPath,value:this.safeClone(r.value)})}getDataPathTransformTrackingInfo(){const e=this.getDataPathTransformStats();return{transforms:[...this.dataPathTransforms],stats:e,timestamp:Date.now()}}getDataPathTransformStats(){const e=this.dataPathTransforms,r={};let a=0;const i={};for(const c of e){r[c.dataSourceId]=(r[c.dataSourceId]||0)+1,a+=c.warnings.length;for(const d of c.warnings)i[d]=(i[d]||0)+1}const l=Object.entries(i).map(([c,d])=>({warning:c,count:d})).sort((c,d)=>d.count-c.count).slice(0,10);return{totalTransforms:e.length,byDataSource:r,warningsCount:a,commonWarnings:l}}getDataPathTransformForDataSource(e){return this.dataPathTransforms.find(r=>r.dataSourceId===e)}clearDataPathTransforms(){this.dataPathTransforms=[]}trackNestedContext(e){if(!this.config.enabled)return"";const r=`nested-${++this.nestedContextIdCounter}`,a=[...new Set([...e.parentContext.available,...e.ownContext.added])],i={...e.parentContext.values,...e.ownContext.values},l={id:r,timestamp:Date.now(),componentId:e.componentId,componentType:e.componentType,parentContext:{available:e.parentContext.available,values:this.safeClone(e.parentContext.values)},ownContext:{added:e.ownContext.added,values:this.safeClone(e.ownContext.values)},mergedContext:{all:a,values:this.safeClone(i)},accessAttempts:[],depth:e.depth,parentId:e.parentId};return this.nestedContexts.push(l),this.nestedContexts.length>this.maxNestedContexts&&this.nestedContexts.shift(),r}trackNestedContextAccess(e,r){if(!this.config.enabled)return;const a=this.nestedContexts.find(i=>i.id===e);a&&a.accessAttempts.push({path:r.path,found:r.found,value:r.found?this.safeClone(r.value):void 0,error:r.error})}getNestedContextTrackingInfo(){const e=this.getNestedContextStats();return{contexts:[...this.nestedContexts],stats:e,timestamp:Date.now()}}getNestedContextStats(){const e=this.nestedContexts,r={expandChildren:0,cellChildren:0,iteration:0,modal:0,slot:0};let a=0,i=0;const l={};for(const d of e){r[d.componentType]++,d.depth>a&&(a=d.depth);for(const f of d.accessAttempts)f.found||(i++,l[f.path]=(l[f.path]||0)+1)}const c=Object.entries(l).map(([d,f])=>({path:d,count:f})).sort((d,f)=>f.count-d.count).slice(0,10);return{totalContexts:e.length,byType:r,maxDepth:a,failedAccessCount:i,commonFailedPaths:c}}getNestedContextForComponent(e){return this.nestedContexts.find(r=>r.componentId===e)}clearNestedContexts(){this.nestedContexts=[],this.nestedContextIdCounter=0}trackHandlerRegistration(e,r="custom",a,i){this.isEnabled&&this.handlers.set(e,{name:e,category:r,description:a,registeredAt:Date.now(),source:i})}trackHandlerUnregistration(e){this.isEnabled&&this.handlers.delete(e)}getHandlers(){return Array.from(this.handlers.values())}getHandler(e){return this.handlers.get(e)}getHandlersByCategory(e){return Array.from(this.handlers.values()).filter(r=>r.category===e)}clearHandlers(){this.handlers.clear()}trackEventSubscribe(e){if(!this.isEnabled)return;const r=this.componentEventSubscriptions.get(e),a=Date.now();r?(r.subscriberCount++,r.lastSubscribedAt=a):this.componentEventSubscriptions.set(e,{eventName:e,subscriberCount:1,firstSubscribedAt:a,lastSubscribedAt:a})}trackEventUnsubscribe(e){if(!this.isEnabled)return;const r=this.componentEventSubscriptions.get(e);r&&(r.subscriberCount--,r.subscriberCount<=0&&this.componentEventSubscriptions.delete(e))}trackEventEmit(e,r,a,i,l){if(!this.isEnabled)return;const c={id:`evt_${++this.componentEventIdCounter}`,eventName:e,data:this.sanitizeForLogging(r),timestamp:Date.now(),listenerCount:a,results:i?this.sanitizeForLogging(i):void 0,hasError:!!l,errorMessage:l?.message};this.componentEventEmitHistory.push(c),this.componentEventEmitHistory.length>this.maxComponentEventHistory&&(this.componentEventEmitHistory=this.componentEventEmitHistory.slice(-this.maxComponentEventHistory))}trackEventOff(e){this.isEnabled&&this.componentEventSubscriptions.delete(e)}trackEventClear(){this.isEnabled&&this.componentEventSubscriptions.clear()}getComponentEventInfo(){const e=Array.from(this.componentEventSubscriptions.values());return{subscriptions:e,emitHistory:[...this.componentEventEmitHistory],totalSubscribers:e.reduce((r,a)=>r+a.subscriberCount,0),totalEmits:this.componentEventEmitHistory.length}}getEventEmitHistory(){return[...this.componentEventEmitHistory]}getEventSubscriptions(){return Array.from(this.componentEventSubscriptions.values())}clearComponentEvents(){this.componentEventSubscriptions.clear(),this.componentEventEmitHistory=[],this.componentEventIdCounter=0}sanitizeForLogging(e){if(e==null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{return"[Circular or Non-serializable]"}}startStateChange(e,r,a,i){if(!this.config.enabled)return"";const l=`setState_${++this.stateRenderingIdCounter}`;this.currentStateChangeContext={setStateId:l,startTime:performance.now(),changedPath:e,trigger:i||{},renderedComponents:[]};const c={id:`sr_${this.stateRenderingIdCounter}`,setStateId:l,statePath:e,oldValue:this.sanitizeForLogging(r),newValue:this.sanitizeForLogging(a),timestamp:Date.now(),triggeredBy:i||{},renderedComponents:[],totalRenderDuration:0,affectedBindingsCount:0};return this.stateRenderingLogs.push(c),this.stateRenderingLogs.length>this.maxStateRenderingLogs&&this.stateRenderingLogs.shift(),l}trackComponentRender(e,r,a,i=[],l=[],c){if(!this.config.enabled)return;const d=this.componentRenderCounts.get(r)||0;this.componentRenderCounts.set(r,d+1);for(const f of i)this.stateToComponentMap.has(f)||this.stateToComponentMap.set(f,new Set),this.stateToComponentMap.get(f).add(r);if(this.currentStateChangeContext){const f={componentId:e,componentName:r,renderDuration:a,accessedStatePaths:i,evaluatedBindings:l,renderOrder:this.currentStateChangeContext.renderedComponents.length,parentId:c};this.currentStateChangeContext.renderedComponents.push(f)}this.trackRender(r)}completeStateChange(e){if(!this.config.enabled||!this.currentStateChangeContext||this.currentStateChangeContext.setStateId!==e)return;const r=this.currentStateChangeContext,i=performance.now()-r.startTime,l=this.stateRenderingLogs.findIndex(c=>c.setStateId===e);if(l>=0){const c=this.stateRenderingLogs[l];c.renderedComponents=r.renderedComponents,c.totalRenderDuration=i,c.affectedBindingsCount=r.renderedComponents.reduce((d,f)=>d+f.evaluatedBindings.length,0)}this.currentStateChangeContext=null}getCurrentStateChangeContext(){return this.currentStateChangeContext}getStateRenderingLogs(){return[...this.stateRenderingLogs]}getStateRenderingInfo(){const e=this.stateRenderingLogs,r=Object.fromEntries(this.componentRenderCounts),a={};for(const[i,l]of this.stateToComponentMap)a[i]=Array.from(l);return{logs:e,componentRenderCounts:r,stateToComponentMap:a,stats:this.calculateStateRenderingStats()}}calculateStateRenderingStats(){const e=this.stateRenderingLogs;if(e.length===0)return{totalStateChanges:0,totalRenders:0,avgRenderDuration:0,avgComponentsPerChange:0,topRenderedComponents:[],topInfluentialPaths:[]};const r=e.reduce((d,f)=>d+f.renderedComponents.length,0),a=e.reduce((d,f)=>d+f.totalRenderDuration,0)/e.length,i=r/e.length,l=Array.from(this.componentRenderCounts.entries()).sort((d,f)=>f[1]-d[1]).slice(0,5).map(([d,f])=>({name:d,count:f})),c=Array.from(this.stateToComponentMap.entries()).sort((d,f)=>f[1].size-d[1].size).slice(0,5).map(([d,f])=>({path:d,affectedComponents:f.size}));return{totalStateChanges:e.length,totalRenders:r,avgRenderDuration:a,avgComponentsPerChange:i,topRenderedComponents:l,topInfluentialPaths:c}}getComponentsAffectedByState(e){const r=this.stateToComponentMap.get(e);return r?Array.from(r):[]}getComponentRenderHistory(e){return this.stateRenderingLogs.filter(r=>r.renderedComponents.some(a=>a.componentName===e))}clearStateRenderingLogs(){this.stateRenderingLogs=[],this.componentRenderCounts.clear(),this.stateToComponentMap.clear(),this.currentStateChangeContext=null,this.stateRenderingIdCounter=0}trackComponentStateSource(e,r,a,i){this.config.enabled&&this.componentStateSources.set(e,{componentId:e,componentName:r,stateSource:a,stateProvider:i})}trackDynamicState(e,r){this.config.enabled&&this.dynamicStates.set(e,r)}trackContextFlow(e,r,a,i,l,c){if(!this.config.enabled)return;const d={component:r,componentId:e,contextReceived:a,passedToChildren:i,usedInRender:l};if(this.contextFlowNodes.set(e,d),c){const f=this.contextFlowNodes.get(c);f&&(f.children||(f.children=[]),f.children.push(d))}}getStateHierarchyInfo(e=!0){const r=this.buildStateHierarchyLayers(),a=this.detectStateConflicts(r),i=Array.from(this.componentStateSources.values());return{layers:e?r.map(c=>({...c,values:this.summarizeValues(c.values,2)})):r,conflicts:a,componentStateSources:i,timestamp:Date.now()}}summarizeValues(e,r,a=0){if(e==null)return e;if(typeof e!="object")return typeof e=="string"&&e.length>100?e.substring(0,100)+`... (${e.length}자)`:e;if(a>=r){if(Array.isArray(e))return`[Array(${e.length})]`;const c=Object.keys(e);return`{${c.slice(0,5).join(", ")}${c.length>5?`, ... +${c.length-5}`:""}}`}if(Array.isArray(e))return e.length<=3?e.map(c=>this.summarizeValues(c,r,a+1)):{__type:"array",__length:e.length,__preview:e.slice(0,3).map(c=>this.summarizeValues(c,r,a+1))};const i={},l=Object.keys(e);for(const c of l)i[c]=this.summarizeValues(e[c],r,a+1);return i}buildStateHierarchyLayers(){const e=[];try{const a=window.G7Core?.state?.get?.();if(!a)return e;a._local&&e.push({name:"Global _local",type:"global",path:"_local",values:a._local,priority:1}),a._global&&e.push({name:"Global _global",type:"global",path:"_global",values:a._global,priority:1});for(const[l,c]of this.dynamicStates)c&&Object.keys(c).length>0&&e.push({name:`DynamicState (${l})`,type:"dynamicState",componentId:l,values:c,priority:2});const i=this.computeEffectiveState(a._local||{});e.push({name:"Effective _local (merged)",type:"effective",values:i,priority:3})}catch(r){console.warn("[G7DevTools] 상태 계층 빌드 실패:",r)}return e}computeEffectiveState(e){const r={...e};for(const[,a]of this.dynamicStates)a._local&&Object.assign(r,a._local);return r}detectStateConflicts(e){const r=[],a=e.find(l=>l.type==="global"&&l.path==="_local");if(!a)return r;const i=e.filter(l=>l.type==="dynamicState");for(const l of i){const c=l.values._local||l.values;for(const[d,f]of Object.entries(c)){const g=a.values[d];if(JSON.stringify(g)!==JSON.stringify(f)){const m=[],y=[];for(const w of this.componentStateSources.values())w.stateSource.local.some(v=>v===`_local.${d}`||v===d)&&(w.stateProvider.type==="dynamicState"?m.push(w.componentName):w.stateProvider.type==="globalState"&&y.push(w.componentName));r.push({path:`_local.${d}`,globalValue:g,dynamicStateValue:f,effectiveValue:f,usedBy:m,notUsedBy:y,severity:y.length>0?"warning":"info",description:y.length>0?`${y.join(", ")}은(는) globalState._local을 읽어 dynamicState 값을 사용하지 못함`:"dynamicState 값이 globalState와 다르지만 모든 컴포넌트가 dynamicState를 사용"})}}}return r}getContextFlowInfo(){const e=[],r=new Set(this.contextFlowNodes.keys());for(const a of this.contextFlowNodes.values())if(a.children)for(const i of a.children)r.delete(i.componentId);for(const a of r){const i=this.contextFlowNodes.get(a);i&&e.push(i)}return{rootComponent:e.length>0?e[0].component:"Unknown",contextFlow:e,timestamp:Date.now()}}clearStateHierarchyData(){this.componentStateSources.clear(),this.contextFlowNodes.clear(),this.dynamicStates.clear()}trackComponentStyle(e,r,a,i){if(!this.config.enabled)return;const l=this.analyzeTailwindClasses(a),c={componentId:e,componentName:r,classes:a,computedStyles:i,tailwindAnalysis:l};this.componentStyles.set(e,c),this.detectStyleIssues(c)}analyzeTailwindClasses(e){const r=[],a=[],i=[],l=[],c=["sm:","md:","lg:","xl:","2xl:"],d=[/bg-\w+-\d+/,/text-\w+-\d+/,/border-\w+-\d+/];for(const f of e){l.push(f),f.startsWith("dark:")&&r.push(f);for(const g of c)if(f.startsWith(g)){a.push(f);break}for(const g of d)if(g.test(f)){i.push(f);break}}return{usedClasses:l,darkClasses:r,responsiveClasses:a,dynamicClasses:i}}detectStyleIssues(e){const{componentId:r,componentName:a,computedStyles:i,tailwindAnalysis:l}=e;(i.opacity==="0"||i.visibility==="hidden"||i.display==="none"||i.width==="0px"&&i.height==="0px")&&this.addStyleIssue({id:this.generateId(),type:"invisible-element",componentId:r,componentName:a,property:i.opacity==="0"?"opacity":i.visibility==="hidden"?"visibility":i.display==="none"?"display":"width/height",currentValue:i.opacity==="0"?"0":i.visibility==="hidden"?"hidden":i.display==="none"?"none":"0px",severity:"warning",description:"요소가 화면에 보이지 않습니다.",suggestion:"opacity, visibility, display, width/height 값을 확인하세요."});const c=l.usedClasses.filter(f=>f.startsWith("bg-")&&!f.startsWith("dark:")),d=l.darkClasses.filter(f=>f.includes("bg-"));c.length>0&&d.length===0&&this.addStyleIssue({id:this.generateId(),type:"dark-mode-missing",componentId:r,componentName:a,property:"background-color",currentValue:c.join(", "),expectedValue:"dark:bg-* 클래스 필요",severity:"info",description:"다크 모드 배경색 클래스가 누락되었습니다.",suggestion:`${c[0]}에 대응하는 dark: 클래스를 추가하세요.`}),l.dynamicClasses.length>0&&this.addStyleIssue({id:this.generateId(),type:"tailwind-purging",componentId:r,componentName:a,property:"class",currentValue:l.dynamicClasses.join(", "),severity:"info",description:"동적으로 생성된 Tailwind 클래스가 있습니다.",suggestion:"tailwind.config.js의 safelist에 추가하거나 전체 클래스명을 상수로 정의하세요."})}addStyleIssue(e){this.styleIssues.some(a=>a.componentId===e.componentId&&a.type===e.type&&a.property===e.property)||this.styleIssues.push(e)}getStyleValidationInfo(){const e=Array.from(this.componentStyles.values());return{issues:[...this.styleIssues],componentStyles:e,stats:{totalComponents:e.length,invisibleCount:this.styleIssues.filter(r=>r.type==="invisible-element").length,tailwindIssueCount:this.styleIssues.filter(r=>r.type==="tailwind-purging").length,darkModeIssueCount:this.styleIssues.filter(r=>r.type==="dark-mode-missing").length},timestamp:Date.now()}}clearStyleValidationData(){this.styleIssues=[],this.componentStyles.clear()}trackAuthEvent(e,r,a,i){if(!this.config.enabled)return;const l={id:`auth_${++this.authEventIdCounter}`,type:e,timestamp:Date.now(),success:r,error:a,details:i};this.authEvents.push(l),this.authEvents.length>this.maxAuthEventHistory&&(this.authEvents=this.authEvents.slice(-this.maxAuthEventHistory))}trackAuthHeader(e,r,a,i,l){if(!this.config.enabled)return;const c={url:e,hasAuthHeader:r,headerType:a,tokenValid:i??!1,responseStatus:l,timestamp:Date.now()};this.authHeaderHistory.push(c),this.authHeaderHistory.length>this.maxAuthEventHistory&&(this.authHeaderHistory=this.authHeaderHistory.slice(-this.maxAuthEventHistory)),l===401&&this.trackAuthEvent("api-unauthorized",!1,`401 Unauthorized: ${e}`,{url:e})}getAuthState(){try{const e=window.G7Core,r=e?.api;let a=e?.auth;!a&&e?.AuthManager?.getInstance&&(a=e.AuthManager.getInstance());const i=r?.getToken?.()||a?.getAccessToken?.()||localStorage.getItem("auth_token")||localStorage.getItem("access_token"),l=a?.getRefreshToken?.()||localStorage.getItem("refresh_token");let c="unknown",d=a?.getUser?.();if(d&&(c="AuthManager"),!d){const S=e?.state?.get?.();d=S?._global?.user||S?._global?.currentUser||S?.user||S?.currentUser,d&&(c="G7Core.state")}const f=a?.isAuthenticated?.()??!1;let g="memory";localStorage.getItem("auth_token")||localStorage.getItem("access_token")?g="localStorage":(sessionStorage.getItem("auth_token")||sessionStorage.getItem("access_token"))&&(g="sessionStorage");const m=!!i&&(!!d||f);let y;i&&typeof i=="string"&&i.length>20?y=`${i.substring(0,10)}...${i.substring(i.length-6)}`:i&&(y="***");const w=d?{id:d.id,email:d.email,name:d.name,roles:d.roles?.map(S=>typeof S=="string"?{name:S}:{id:S.id,name:S.name,guard_name:S.guard_name}),permissions:d.permissions?.map(S=>typeof S=="string"?{name:S}:{id:S.id,name:S.name,guard_name:S.guard_name}),avatar:d.avatar||d.profile_photo_url||d.profile_image,created_at:d.created_at,updated_at:d.updated_at,last_login_at:d.last_login_at,email_verified_at:d.email_verified_at,...d.phone&&{phone:d.phone},...d.nickname&&{nickname:d.nickname},...d.status&&{status:d.status},...d.locale&&{locale:d.locale}}:void 0;return{isAuthenticated:m,user:w,tokens:{hasAccessToken:!!i,hasRefreshToken:!!l,storage:g,accessTokenPreview:y},lastActivity:Date.now(),source:c}}catch{return{isAuthenticated:!1,tokens:{hasAccessToken:!1,hasRefreshToken:!1,storage:"memory"},source:"unknown"}}}getAuthDebugInfo(){const e=[...this.authEvents];return{state:this.getAuthState(),events:e,headerAnalysis:[...this.authHeaderHistory],stats:{loginAttempts:e.filter(r=>r.type==="login").length,successfulLogins:e.filter(r=>r.type==="login"&&r.success).length,failedLogins:e.filter(r=>r.type==="login"&&!r.success).length,tokenRefreshes:e.filter(r=>r.type==="token-refresh").length,unauthorizedResponses:e.filter(r=>r.type==="api-unauthorized").length},timestamp:Date.now()}}clearAuthData(){this.authEvents=[],this.authHeaderHistory=[],this.authEventIdCounter=0}trackLayoutLoad(e,r,a,i="api"){if(!this.config.enabled)return;const l=Date.now();this.currentLayout={layoutPath:e,templateId:r,layoutJson:this.sanitizeObject(a),loadedAt:l,version:a.version,layoutName:a.layout_name,source:i};const c={id:`layout-${++this.layoutIdCounter}`,layoutPath:e,templateId:r,loadedAt:l,source:i,version:a.version};this.layoutHistory.push(c),this.layoutHistory.length>this.maxLayoutHistory&&this.layoutHistory.shift(),this.layoutStats.totalLoads++,i==="cache"?this.layoutStats.cacheHits++:this.layoutStats.apiLoads++}getCurrentLayout(){return this.currentLayout}getLayoutHistory(){return[...this.layoutHistory]}getLayoutDebugInfo(){return{current:this.currentLayout,history:[...this.layoutHistory],stats:{...this.layoutStats}}}clearLayoutData(){this.currentLayout=null,this.layoutHistory=[],this.layoutIdCounter=0,this.layoutStats={totalLoads:0,cacheHits:0,apiLoads:0}}startHandlerExecution(e){if(!this.config.enabled)return"";const r=`exec_${++this.executionIdCounter}_${Date.now()}`,a={handlerName:e,executionId:r,startTime:Date.now(),stateChanges:[],dataSourceChanges:[],alerts:[]};if(this.executionDetails.set(r,a),this.executionDetails.size>this.maxExecutionDetails){const i=Array.from(this.executionDetails.keys());i.slice(0,i.length-this.maxExecutionDetails).forEach(c=>this.executionDetails.delete(c))}return r}endHandlerExecution(e,r,a,i){if(!this.config.enabled||!e)return;const l=this.executionDetails.get(e);if(!l)return;l.endTime=Date.now(),l.duration=l.endTime-l.startTime,l.exitReason=r,l.exitLocation=a,l.exitDescription=i;const c=this.changeExpectations.get(e);c&&(l.expectedChanges=c,this.checkExpectations(e,l,c),this.changeExpectations.delete(e)),r==="normal"&&l.stateChanges.length===0&&l.dataSourceChanges.length===0&&this.addChangeAlert(e,{type:"no-state-change",severity:"warning",message:`핸들러 "${l.handlerName}"이(가) 상태 변경 없이 완료됨`,description:"핸들러가 성공적으로 완료되었지만 상태나 데이터소스 변경이 없습니다. 의도한 동작인지 확인하세요.",handlerName:l.handlerName,suggestion:'핸들러 내부 조건문을 확인하거나 DevTools의 "변경감지" 탭에서 exitLocation을 확인하세요.',docLink:"troubleshooting-state.md"}),(r==="early-return-condition"||r==="early-return-validation")&&this.addChangeAlert(e,{type:"early-return-detected",severity:"info",message:`핸들러 "${l.handlerName}"이(가) early return으로 종료됨`,description:i||`종료 위치: ${a||"알 수 없음"}`,handlerName:l.handlerName,suggestion:r==="early-return-validation"?"검증 실패로 인한 early return입니다. 입력 데이터를 확인하세요.":"조건부 early return입니다. 조건문의 평가 결과를 확인하세요."})}recordExitReason(e,r,a,i){if(!this.config.enabled||!e)return;const l=this.executionDetails.get(e);l&&(l.exitReason=r,l.exitLocation=a,l.exitDescription=i)}recordStateChange(e,r,a,i,l){if(!this.config.enabled)return;const c={id:`sc_${++this.stateChangeIdCounter}`,path:r,changeType:a,oldValue:this.safeClone(i),newValue:this.safeClone(l),timestamp:Date.now(),comparison:this.compareValues(i,l),executionId:e};if(this.stateChangeHistory.push(c),e){const d=this.executionDetails.get(e);d&&d.stateChanges.push(c)}this.stateChangeHistory.length>this.maxStateChangeHistory&&this.stateChangeHistory.splice(0,this.stateChangeHistory.length-this.maxStateChangeHistory),c.comparison.isDeepEqual&&typeof i=="object"&&i!==null&&this.addChangeAlert(e||"",{type:"object-reference-same",severity:"warning",message:`상태 "${r}"가 변경되었으나 값이 동일함`,description:"객체 참조가 동일하거나 깊은 비교 결과 변경이 없습니다. 변경 감지가 실패할 수 있습니다.",handlerName:e&&this.executionDetails.get(e)?.handlerName||"unknown",statePath:r,suggestion:"불변성을 유지하여 새 객체를 생성하세요. 예: { ...oldObject, newField: value }",docLink:"troubleshooting-state.md#object-mutation"})}recordDataSourceChange(e,r,a,i,l){if(!this.config.enabled)return;const c={dataSourceId:r,changeType:a,timestamp:Date.now(),previousStatus:i,newStatus:l,executionId:e};if(this.dataSourceChangeHistory.push(c),e){const d=this.executionDetails.get(e);d&&d.dataSourceChanges.push(c)}this.dataSourceChangeHistory.length>this.maxStateChangeHistory&&this.dataSourceChangeHistory.splice(0,this.dataSourceChangeHistory.length-this.maxStateChangeHistory)}expectChange(e,r,a){if(!this.config.enabled||!e)return;const i=this.executionDetails.get(e);if(!i)return;const l={expectedStatePaths:r,expectedDataSources:a,setAt:Date.now(),source:i.handlerName};this.changeExpectations.set(e,l)}checkExpectations(e,r,a){const i=new Set(r.stateChanges.map(c=>c.path)),l=new Set(r.dataSourceChanges.map(c=>c.dataSourceId));for(const c of a.expectedStatePaths)i.has(c)||this.addChangeAlert(e,{type:"expected-not-fulfilled",severity:"error",message:`기대한 상태 변경 "${c}"가 발생하지 않음`,description:`핸들러 "${r.handlerName}"이(가) "${c}" 변경을 예고했지만 실제로 변경되지 않았습니다.`,handlerName:r.handlerName,statePath:c,suggestion:"핸들러 내부 로직을 확인하세요. early return이나 조건부 분기로 인해 변경이 생략되었을 수 있습니다.",docLink:"troubleshooting-state.md"});for(const c of a.expectedDataSources)l.has(c)||this.addChangeAlert(e,{type:"expected-not-fulfilled",severity:"error",message:`기대한 데이터소스 갱신 "${c}"가 발생하지 않음`,description:`핸들러 "${r.handlerName}"이(가) "${c}" 갱신을 예고했지만 실제로 갱신되지 않았습니다.`,handlerName:r.handlerName,dataSourceId:c,suggestion:"refetchDataSource 호출이 실행되었는지 확인하세요.",docLink:"data-sources.md"})}addChangeAlert(e,r){const a={...r,id:`alert_${++this.alertIdCounter}`,timestamp:Date.now()};if(this.changeAlerts.push(a),e){const i=this.executionDetails.get(e);i&&i.alerts.push(a)}this.changeAlerts.length>this.maxChangeAlerts&&this.changeAlerts.splice(0,this.changeAlerts.length-this.maxChangeAlerts)}compareValues(e,r){const a=this.getValueType(e),i=this.getValueType(r),l=a!==i;let c=!1,d;return!l&&(a==="object"||a==="array")?(c=this.deepEqual(e,r),!c&&a==="object"&&(d=this.getChangedKeys(e,r))):c=e===r,{typeChanged:l,oldType:a,newType:i,isDeepEqual:c,changedKeys:d}}getValueType(e){return e===null?"null":e===void 0?"undefined":Array.isArray(e)?"array":typeof e}deepEqual(e,r){if(e===r)return!0;if(typeof e!=typeof r)return!1;if(e===null||r===null||typeof e!="object")return e===r;if(Array.isArray(e)!==Array.isArray(r))return!1;const a=Object.keys(e),i=Object.keys(r);if(a.length!==i.length)return!1;for(const l of a)if(!i.includes(l)||!this.deepEqual(e[l],r[l]))return!1;return!0}getChangedKeys(e,r){const a=[],i=new Set([...Object.keys(e),...Object.keys(r)]);for(const l of i)this.deepEqual(e[l],r[l])||a.push(l);return a}getChangeDetectionInfo(){const e=Array.from(this.executionDetails.values()),r={totalExecutions:e.length,executionsWithStateChange:e.filter(a=>a.stateChanges.length>0).length,executionsWithoutStateChange:e.filter(a=>a.stateChanges.length===0&&a.exitReason==="normal").length,earlyReturnCount:e.filter(a=>a.exitReason==="early-return-condition"||a.exitReason==="early-return-validation").length,alertCount:this.changeAlerts.length,alertsByType:this.getAlertsByType()};return{executionDetails:e,stateChangeHistory:[...this.stateChangeHistory],dataSourceChangeHistory:[...this.dataSourceChangeHistory],alerts:[...this.changeAlerts],stats:r,timestamp:Date.now()}}getAlertsByType(){const e={"no-state-change":0,"no-datasource-change":0,"expected-not-fulfilled":0,"object-reference-same":0,"early-return-detected":0,"async-timing-issue":0};for(const r of this.changeAlerts)e[r.type]++;return e}clearChangeDetectionData(){this.executionDetails.clear(),this.stateChangeHistory=[],this.dataSourceChangeHistory=[],this.changeAlerts=[],this.changeExpectations.clear(),this.executionIdCounter=0,this.stateChangeIdCounter=0,this.alertIdCounter=0}getRecentAlerts(e=10,r){let a=[...this.changeAlerts];return r&&(a=a.filter(i=>i.severity===r)),a.slice(-e)}getHandlerExecutions(e,r=10){return Array.from(this.executionDetails.values()).filter(i=>i.handlerName===e).slice(-r)}findExecutionInStack(e){return this.sequenceExecutionStack.find(r=>r.sequenceId===e)}startSequenceExecution(e){if(!this.config.enabled)return"";const r=`seq_${++this.sequenceIdCounter}_${Date.now()}`,a={sequenceId:r,startTime:Date.now(),totalDuration:0,trigger:e,actions:[],status:"running"};return this.sequenceExecutionStack.push(a),r}captureSequenceActionBefore(e,r,a,i,l){if(!this.config.enabled)return;const c=this.findExecutionInStack(e);if(!c)return;const d={index:r,handler:a,params:this.safeClone(i),stateBeforeAction:{_global:this.safeClone(l._global),_local:this.safeClone(l._local),_isolated:l._isolated?this.safeClone(l._isolated):void 0},stateAfterAction:{_global:{},_local:{}},duration:0,status:"success"};c.actions.push(d)}captureSequenceActionAfter(e,r,a,i,l,c){if(!this.config.enabled)return;const d=this.findExecutionInStack(e);if(!d)return;const f=d.actions.find(m=>m.index===r);if(!f)return;f.stateAfterAction={_global:this.safeClone(a._global),_local:this.safeClone(a._local),_isolated:a._isolated?this.safeClone(a._isolated):void 0},f.duration=i,f.result=this.safeClone(l),f.stateDiff={global:this.computeStateDiff(f.stateBeforeAction._global,f.stateAfterAction._global),local:this.computeStateDiff(f.stateBeforeAction._local,f.stateAfterAction._local)},f.stateBeforeAction._isolated&&f.stateAfterAction._isolated&&(f.stateDiff.isolated=this.computeStateDiff(f.stateBeforeAction._isolated,f.stateAfterAction._isolated));const g=window.__g7PendingLocalState;g&&(f.pendingState=this.safeClone(g)),c&&(f.status="error",f.error={name:c.name,message:c.message,stack:c.stack})}endSequenceExecution(e,r){if(!this.config.enabled)return;const a=this.sequenceExecutionStack.findIndex(l=>l.sequenceId===e);if(a===-1)return;const i=this.sequenceExecutionStack[a];if(i.endTime=Date.now(),i.totalDuration=i.endTime-i.startTime,r){i.status="error",i.error={name:r.name,message:r.message,stack:r.stack};const l=i.actions.find(c=>c.status==="error");l&&(i.failedAtIndex=l.index)}else i.status="success";this.sequenceExecutions.push(i),this.sequenceExecutions.length>this.maxSequenceExecutions&&this.sequenceExecutions.shift(),this.sequenceExecutionStack.splice(a,1)}computeStateDiff(e,r){const a=[],i=[],l=[],c=new Set(Object.keys(e)),d=new Set(Object.keys(r));for(const f of d)c.has(f)||a.push(f);for(const f of c)d.has(f)||i.push(f);for(const f of c)if(d.has(f)){const g=e[f],m=r[f];JSON.stringify(g)!==JSON.stringify(m)&&l.push({path:f,oldValue:g,newValue:m})}return{added:a,removed:i,changed:l}}getSequenceTrackingInfo(){const e=this.getSequenceStats(),r=this.sequenceExecutionStack.length>0?this.sequenceExecutionStack[this.sequenceExecutionStack.length-1]:void 0;return{executions:[...this.sequenceExecutions],currentExecution:r,stats:e}}getSequenceStats(){const e=this.sequenceExecutions,r=e.length,a=e.filter(g=>g.status==="success").length,i=e.filter(g=>g.status==="error").length,l=e.reduce((g,m)=>g+m.actions.length,0),c=r>0?e.reduce((g,m)=>g+m.totalDuration,0)/r:0,d={};for(const g of e)for(const m of g.actions)d[m.handler]=(d[m.handler]||0)+1;const f=Object.entries(d).sort(([,g],[,m])=>m-g).slice(0,5).map(([g,m])=>({handler:g,count:m}));return{totalExecutions:r,successCount:a,errorCount:i,totalActions:l,avgDuration:c,topHandlers:f}}clearSequenceData(){this.sequenceExecutions=[],this.sequenceExecutionStack=[],this.sequenceIdCounter=0}getSequenceExecution(e){const r=this.findExecutionInStack(e);return r||this.sequenceExecutions.find(a=>a.sequenceId===e)}getRecentSequences(e=10){return this.sequenceExecutions.slice(-e)}registerStateCaptureForHandler(e,r,a){if(!this.config.enabled)return;const i=new Map,l=Date.now();for(const c of r)i.set(c,{value:this.safeClone(a[c]),capturedAt:l});this.stateCaptureRegistry.set(e,i)}detectStaleClosure(e,r,a,i,l){if(!this.config.enabled)return[];const c=this.stateCaptureRegistry.get(e);if(!c)return[];const d=Date.now(),f=[];for(const[g,m]of c){const y=this.getValueByPath(a,g),w=d-m.capturedAt;if(!this.isDeepEqual(m.value,y)&&w>0){const S=this.createStaleClosureWarning(i,r,g,m.value,m.capturedAt,y,d,w,l);f.push(S),this.addStaleClosureWarning(S)}}return this.stateCaptureRegistry.delete(e),f}trackStaleClosureWarning(e){if(!this.config.enabled)return;const r=Date.now(),a=this.createStaleClosureWarning(e.type,e.location,e.capturedPath,e.capturedValue,e.capturedAt,e.currentValue,r,r-e.capturedAt,e.actionId,e.stackTrace);this.addStaleClosureWarning(a)}createStaleClosureWarning(e,r,a,i,l,c,d,f,g,m){const y=this.getStaleClosureSeverity(e,f),{description:w,suggestion:S,docLink:v}=this.getStaleClosureMessages(e,a,f);return{id:`stale_${++this.staleClosureIdCounter}_${Date.now()}`,timestamp:Date.now(),type:e,location:r,capturedState:{path:a,capturedValue:this.safeClone(i),capturedAt:l},currentState:{path:a,currentValue:this.safeClone(c),retrievedAt:d},timeDiff:f,severity:y,description:w,suggestion:S,docLink:v,actionId:g,stackTrace:m}}getStaleClosureSeverity(e,r){if(r>5e3)return"error";if(r>1e3)return"warning";switch(e){case"async-state-capture":case"sequence-state-mismatch":return"warning";case"callback-state-capture":case"timeout-state-capture":return"info";case"event-handler-stale":return"warning";default:return"info"}}getStaleClosureMessages(e,r,a){const i=a<1e3?`${a}ms`:`${(a/1e3).toFixed(1)}s`;switch(e){case"async-state-capture":return{description:`await 후 ${i} 경과 시점에 '${r}' 상태가 변경됨. 캡처된 상태 대신 최신 상태를 사용해야 함`,suggestion:"await 이후에는 G7Core.state.get()으로 최신 상태를 다시 조회하거나, useRef + getter 패턴을 사용하세요",docLink:"troubleshooting-state-closure.md"};case"callback-state-capture":return{description:`콜백에서 ${i} 전에 캡처된 '${r}' 상태를 사용 중. 최신 상태와 다름`,suggestion:"콜백에서 상태를 참조할 때는 stateRef.current 패턴 또는 G7Core.state.get()를 사용하세요",docLink:"troubleshooting-state-closure.md"};case"timeout-state-capture":return{description:`setTimeout/setInterval 콜백에서 '${r}' 상태가 ${i} 전 값 사용 중`,suggestion:"타이머 콜백 내에서는 G7Core.state.get()으로 최신 상태를 조회하세요",docLink:"troubleshooting-state-closure.md"};case"sequence-state-mismatch":return{description:`sequence 내 '${r}' 상태가 이전 액션 결과와 불일치. context.state 대신 캡처된 값 사용 의심`,suggestion:"sequence 내에서는 context.state 또는 $prev를 사용하여 최신 상태를 참조하세요",docLink:"troubleshooting-state-setstate.md"};case"event-handler-stale":return{description:`이벤트 핸들러에서 ${i} 전에 바인딩된 '${r}' 상태 사용 중`,suggestion:"이벤트 핸들러에서 상태를 참조할 때는 useRef 패턴 또는 G7Core.state.get()를 사용하세요",docLink:"troubleshooting-state-closure.md"};default:return{description:`'${r}' 상태에서 stale closure 감지됨 (${i} 경과)`,suggestion:"상태 참조 시 최신 값을 사용하는지 확인하세요"}}}addStaleClosureWarning(e){this.staleClosureWarnings.push(e),this.staleClosureWarnings.length>this.maxStaleClosureWarnings&&this.staleClosureWarnings.shift()}getValueByPath(e,r){const a=r.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}isDeepEqual(e,r){if(e===r)return!0;if(e===null||r===null)return e===r;if(typeof e!=typeof r)return!1;if(typeof e!="object")return e===r;try{return JSON.stringify(e)===JSON.stringify(r)}catch{return!1}}getStaleClosureTrackingInfo(){const e=this.getStaleClosureStats();return{warnings:[...this.staleClosureWarnings],stats:e,timestamp:Date.now()}}getStaleClosureStats(){const e=this.staleClosureWarnings,r={"async-state-capture":0,"callback-state-capture":0,"timeout-state-capture":0,"sequence-state-mismatch":0,"event-handler-stale":0};for(const f of e)r[f.type]++;const a={info:0,warning:0,error:0};for(const f of e)a[f.severity]++;const i={};for(const f of e)i[f.location]=(i[f.location]||0)+1;const l=Object.entries(i).sort(([,f],[,g])=>g-f).slice(0,5).map(([f,g])=>({location:f,count:g})),c={};for(const f of e)c[f.capturedState.path]=(c[f.capturedState.path]||0)+1;const d=Object.entries(c).sort(([,f],[,g])=>g-f).slice(0,5).map(([f,g])=>({path:f,count:g}));return{totalWarnings:e.length,warningsByType:r,warningsBySeverity:a,topLocations:l,topAffectedPaths:d}}clearStaleClosureData(){this.staleClosureWarnings=[],this.staleClosureIdCounter=0,this.stateCaptureRegistry.clear()}getRecentStaleClosureWarnings(e=10,r){let a=[...this.staleClosureWarnings];return r&&(a=a.filter(i=>i.severity===r)),a.slice(-e)}safeClone(e){if(e==null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{if(Array.isArray(e))return e.map(a=>{try{return JSON.parse(JSON.stringify(a))}catch{return"[Unserializable]"}});const r={};for(const a of Object.keys(e))try{r[a]=JSON.parse(JSON.stringify(e[a]))}catch{r[a]="[Unserializable]"}return r}}};$(Ei,"instance");let Ma=Ei;const Fn=ut("LayoutLoader");class Er extends Error{constructor(e,r,a){super(e),this.code=r,this.details=a,this.name="LayoutLoaderError"}}class of{constructor(e){$(this,"componentRegistry");$(this,"currentLayout",null);$(this,"layoutCache",new Map);$(this,"cacheVersion",0);this.componentRegistry=e}setCacheVersion(e){this.cacheVersion!==e&&(Fn.log("Cache version updated:",this.cacheVersion,"->",e),this.cacheVersion=e,this.layoutCache.clear())}getCacheVersion(){return this.cacheVersion}async loadLayout(e,r){const a=`${e}:${r}`;if(this.layoutCache.has(a)){Fn.log("Loading layout from cache:",r);const l=await this.layoutCache.get(a),c=JSON.parse(JSON.stringify(l));return this.currentLayout=c,Ma.getInstance().trackLayoutLoad(r,e,c,"cache"),c}const i=this.fetchLayout(e,r);return this.layoutCache.set(a,i),i.catch(()=>{this.layoutCache.get(a)===i&&this.layoutCache.delete(a)}),i}async fetchLayout(e,r,a=!1){try{const i=this.cacheVersion>0?this.cacheVersion:null;let l;if(r.startsWith("__preview__/")){const w=r.replace("__preview__/","");l=Nr(`/api/layouts/preview/${w}`,"json",i)}else l=Nr(`/api/layouts/${e}/${r}`,"json",i);Fn.log("Fetching layout from API:",l);const c=jr(),d=a?null:c.getToken(),f={Accept:"application/json"};d&&(f.Authorization=`Bearer ${d}`);const g=await Bl(l,{init:{headers:f},label:`layout: ${r}`});let m;try{m=await g.json()}catch{m=null}if(!g.ok){if(g.status===401&&!a&&d){Fn.log("Token invalid, removing and retrying without token"),c.removeToken();try{return await this.fetchLayout(e,r,!0)}catch(S){throw S instanceof Er&&S.details?.status===401?new Er(S.message,S.code,{...S.details,hadToken:!0}):S}}const w=m?.message||m?.error;throw new Er(`Failed to fetch layout: ${g.status} ${g.statusText}`,"FETCH_FAILED",{status:g.status,statusText:g.statusText,url:l,apiMessage:w})}const y=m.data||m;return this.validateLayoutData(y),this.currentLayout=y,Ma.getInstance().trackLayoutLoad(r,e,y,"api"),Fn.log("Layout fetched and cached successfully:",y.layout_name),y}catch(i){const l=`${e}:${r}`;throw this.layoutCache.delete(l),i instanceof Er?i:new Er("Failed to load layout","LOAD_FAILED",{originalError:i})}}prefetchLayout(e,r){return this.loadLayout(e,r)}validateLayoutData(e){if(!e.version)throw new Er("Layout data missing required field: version","VALIDATION_FAILED",{field:"version"});if(!e.layout_name)throw new Er("Layout data missing required field: layout_name","VALIDATION_FAILED",{field:"layout_name"});if(!Array.isArray(e.components))throw new Er('Layout data field "components" must be an array',"VALIDATION_FAILED",{field:"components",value:e.components});Fn.log("Layout data validation passed")}renderLayout(e,r){try{const a=r||this.currentLayout;if(!a)throw new Er("No layout data to render","NO_LAYOUT_DATA");Fn.log("Rendering layout:",a.layout_name),e.innerHTML="",this.renderComponents(e,a.components),Fn.log("Layout rendered successfully")}catch(a){Fn.error("Render error:",a),this.renderErrorState(e,a)}}renderComponents(e,r){for(const a of r){const i=this.renderComponent(a);i&&e.appendChild(i)}}renderComponent(e){try{if(!this.componentRegistry.getComponent(e.type))return Fn.warn(`Component not found: ${e.type}`),this.createPlaceholderElement(e.type);const a=document.createElement("div");return a.setAttribute("data-component",e.type),e.props&&Object.entries(e.props).forEach(([i,l])=>{a.setAttribute(`data-prop-${i}`,JSON.stringify(l))}),e.children&&e.children.length>0?this.renderComponents(a,e.children):e.text&&(a.textContent=e.text),a}catch(r){return Fn.error(`Error rendering component ${e.type}:`,r),this.createErrorElement(e.type,r)}}createPlaceholderElement(e){const r=document.createElement("div");return r.className="component-placeholder",r.setAttribute("data-component-type",e),r.innerHTML=` + `},wE={onDragStart(s){let{active:e}=s;return"Picked up draggable item "+e.id+"."},onDragOver(s){let{active:e,over:n}=s;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(s){let{active:e,over:n}=s;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(s){let{active:e}=s;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function CE(s){let{announcements:e=wE,container:n,hiddenTextDescribedById:a,screenReaderInstructions:i=SE}=s;const{announce:l,announcement:c}=yE(),d=Eo("DndLiveRegion"),[f,h]=N.useState(!1);if(N.useEffect(()=>{h(!0)},[]),bE(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(gE,{id:a,value:i.draggable}),Ye.createElement(mE,{id:d,announcement:c}));return n?Oa.createPortal(m,n):m}var nn;(function(s){s.DragStart="dragStart",s.DragMove="dragMove",s.DragEnd="dragEnd",s.DragCancel="dragCancel",s.DragOver="dragOver",s.RegisterDroppable="registerDroppable",s.SetDroppableDisabled="setDroppableDisabled",s.UnregisterDroppable="unregisterDroppable"})(nn||(nn={}));function uc(){}function Fm(s,e){return N.useMemo(()=>({sensor:s,options:e??{}}),[s,e])}function EE(){for(var s=arguments.length,e=new Array(s),n=0;n<s;n++)e[n]=arguments[n];return N.useMemo(()=>[...e].filter(a=>a!=null),[...e])}const Er=Object.freeze({x:0,y:0});function Km(s,e){return Math.sqrt(Math.pow(s.x-e.x,2)+Math.pow(s.y-e.y,2))}function Wm(s,e){let{data:{value:n}}=s,{data:{value:a}}=e;return n-a}function _E(s,e){let{data:{value:n}}=s,{data:{value:a}}=e;return a-n}function Ym(s){let{left:e,top:n,height:a,width:i}=s;return[{x:e,y:n},{x:e+i,y:n},{x:e,y:n+a},{x:e+i,y:n+a}]}function Xm(s,e){if(!s||s.length===0)return null;const[n]=s;return n[e]}function Jm(s,e,n){return e===void 0&&(e=s.left),n===void 0&&(n=s.top),{x:e+s.width*.5,y:n+s.height*.5}}const AE=s=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=s;const i=Jm(e,e.left,e.top),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const h=Km(Jm(f),i);l.push({id:d,data:{droppableContainer:c,value:h}})}}return l.sort(Wm)},xE=s=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=s;const i=Ym(e),l=[];for(const c of a){const{id:d}=c,f=n.get(d);if(f){const h=Ym(f),m=i.reduce((S,v,_)=>S+Km(h[_],v),0),b=Number((m/4).toFixed(4));l.push({id:d,data:{droppableContainer:c,value:b}})}}return l.sort(Wm)};function TE(s,e){const n=Math.max(e.top,s.top),a=Math.max(e.left,s.left),i=Math.min(e.left+e.width,s.left+s.width),l=Math.min(e.top+e.height,s.top+s.height),c=i-a,d=l-n;if(a<i&&n<l){const f=e.width*e.height,h=s.width*s.height,m=c*d,b=m/(f+h-m);return Number(b.toFixed(4))}return 0}const kE=s=>{let{collisionRect:e,droppableRects:n,droppableContainers:a}=s;const i=[];for(const l of a){const{id:c}=l,d=n.get(c);if(d){const f=TE(d,e);f>0&&i.push({id:c,data:{droppableContainer:l,value:f}})}}return i.sort(_E)};function RE(s,e,n){return{...s,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function Qm(s,e){return s&&e?{x:s.left-e.left,y:s.top-e.top}:Er}function DE(s){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+s*d.y,bottom:c.bottom+s*d.y,left:c.left+s*d.x,right:c.right+s*d.x}),{...n})}}const OE=DE(1);function LE(s){if(s.startsWith("matrix3d(")){const e=s.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(s.startsWith("matrix(")){const e=s.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function ME(s,e,n){const a=LE(e);if(!a)return s;const{scaleX:i,scaleY:l,x:c,y:d}=a,f=s.left-c-(1-i)*parseFloat(n),h=s.top-d-(1-l)*parseFloat(n.slice(n.indexOf(" ")+1)),m=i?s.width/i:s.width,b=l?s.height/l:s.height;return{width:m,height:b,top:h,right:f+m,bottom:h+b,left:f}}const $E={ignoreTransform:!1};function ps(s,e){e===void 0&&(e=$E);let n=s.getBoundingClientRect();if(e.ignoreTransform){const{transform:h,transformOrigin:m}=Mn(s).getComputedStyle(s);h&&(n=ME(n,h,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 Zm(s){return ps(s,{ignoreTransform:!0})}function NE(s){const e=s.innerWidth,n=s.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function IE(s,e){return e===void 0&&(e=Mn(s).getComputedStyle(s)),e.position==="fixed"}function jE(s,e){e===void 0&&(e=Mn(s).getComputedStyle(s));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(i=>{const l=e[i];return typeof l=="string"?n.test(l):!1})}function dc(s,e){const n=[];function a(i){if(e!=null&&n.length>=e||!i)return n;if(of(i)&&i.scrollingElement!=null&&!n.includes(i.scrollingElement))return n.push(i.scrollingElement),n;if(!So(i)||Bm(i)||n.includes(i))return n;const l=Mn(s).getComputedStyle(i);return i!==s&&jE(i,l)&&n.push(i),IE(i,l)?n:a(i.parentNode)}return s?a(s):n}function ey(s){const[e]=dc(s,1);return e??null}function hf(s){return!lc||!s?null:ds(s)?s:sf(s)?of(s)||s===fs(s).scrollingElement?window:So(s)?s:null:null}function ty(s){return ds(s)?s.scrollX:s.scrollLeft}function ny(s){return ds(s)?s.scrollY:s.scrollTop}function pf(s){return{x:ty(s),y:ny(s)}}var dn;(function(s){s[s.Forward=1]="Forward",s[s.Backward=-1]="Backward"})(dn||(dn={}));function ry(s){return!lc||!s?!1:s===document.scrollingElement}function ay(s){const e={x:0,y:0},n=ry(s)?{height:window.innerHeight,width:window.innerWidth}:{height:s.clientHeight,width:s.clientWidth},a={x:s.scrollWidth-n.width,y:s.scrollHeight-n.height},i=s.scrollTop<=e.y,l=s.scrollLeft<=e.x,c=s.scrollTop>=a.y,d=s.scrollLeft>=a.x;return{isTop:i,isLeft:l,isBottom:c,isRight:d,maxScroll:a,minScroll:e}}const HE={x:.2,y:.2};function zE(s,e,n,a,i){let{top:l,left:c,right:d,bottom:f}=n;a===void 0&&(a=10),i===void 0&&(i=HE);const{isTop:h,isBottom:m,isLeft:b,isRight:S}=ay(s),v={x:0,y:0},_={x:0,y:0},A={height:e.height*i.y,width:e.width*i.x};return!h&&l<=e.top+A.height?(v.y=dn.Backward,_.y=a*Math.abs((e.top+A.height-l)/A.height)):!m&&f>=e.bottom-A.height&&(v.y=dn.Forward,_.y=a*Math.abs((e.bottom-A.height-f)/A.height)),!S&&d>=e.right-A.width?(v.x=dn.Forward,_.x=a*Math.abs((e.right-A.width-d)/A.width)):!b&&c<=e.left+A.width&&(v.x=dn.Backward,_.x=a*Math.abs((e.left+A.width-c)/A.width)),{direction:v,speed:_}}function UE(s){if(s===document.scrollingElement){const{innerWidth:l,innerHeight:c}=window;return{top:0,left:0,right:l,bottom:c,width:l,height:c}}const{top:e,left:n,right:a,bottom:i}=s.getBoundingClientRect();return{top:e,left:n,right:a,bottom:i,width:s.clientWidth,height:s.clientHeight}}function iy(s){return s.reduce((e,n)=>hs(e,pf(n)),Er)}function PE(s){return s.reduce((e,n)=>e+ty(n),0)}function BE(s){return s.reduce((e,n)=>e+ny(n),0)}function qE(s,e){if(e===void 0&&(e=ps),!s)return;const{top:n,left:a,bottom:i,right:l}=e(s);ey(s)&&(i<=0||l<=0||n>=window.innerHeight||a>=window.innerWidth)&&s.scrollIntoView({block:"center",inline:"center"})}const GE=[["x",["left","right"],PE],["y",["top","bottom"],BE]];class gf{constructor(e,n){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const a=dc(n),i=iy(a);this.rect={...e},this.width=e.width,this.height=e.height;for(const[l,c,d]of GE)for(const f of c)Object.defineProperty(this,f,{get:()=>{const h=d(a),m=i[l]-h;return this.rect[f]+m},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class xo{constructor(e){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(n=>{var a;return(a=this.target)==null?void 0:a.removeEventListener(...n)})},this.target=e}add(e,n,a){var i;(i=this.target)==null||i.addEventListener(e,n,a),this.listeners.push([e,n,a])}}function VE(s){const{EventTarget:e}=Mn(s);return s instanceof e?s:fs(s)}function mf(s,e){const n=Math.abs(s.x),a=Math.abs(s.y);return typeof e=="number"?Math.sqrt(n**2+a**2)>e:"x"in e&&"y"in e?n>e.x&&a>e.y:"x"in e?n>e.x:"y"in e?a>e.y:!1}var ar;(function(s){s.Click="click",s.DragStart="dragstart",s.Keydown="keydown",s.ContextMenu="contextmenu",s.Resize="resize",s.SelectionChange="selectionchange",s.VisibilityChange="visibilitychange"})(ar||(ar={}));function sy(s){s.preventDefault()}function FE(s){s.stopPropagation()}var lt;(function(s){s.Space="Space",s.Down="ArrowDown",s.Right="ArrowRight",s.Left="ArrowLeft",s.Up="ArrowUp",s.Esc="Escape",s.Enter="Enter",s.Tab="Tab"})(lt||(lt={}));const oy={start:[lt.Space,lt.Enter],cancel:[lt.Esc],end:[lt.Space,lt.Enter,lt.Tab]},KE=(s,e)=>{let{currentCoordinates:n}=e;switch(s.code){case lt.Right:return{...n,x:n.x+25};case lt.Left:return{...n,x:n.x-25};case lt.Down:return{...n,y:n.y+25};case lt.Up:return{...n,y:n.y-25}}};class yf{constructor(e){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=e;const{event:{target:n}}=e;this.props=e,this.listeners=new xo(fs(n)),this.windowListeners=new xo(Mn(n)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(ar.Resize,this.handleCancel),this.windowListeners.add(ar.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(ar.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:e,onStart:n}=this.props,a=e.node.current;a&&qE(a),n(Er)}handleKeyDown(e){if(df(e)){const{active:n,context:a,options:i}=this.props,{keyboardCodes:l=oy,coordinateGetter:c=KE,scrollBehavior:d="smooth"}=i,{code:f}=e;if(l.end.includes(f)){this.handleEnd(e);return}if(l.cancel.includes(f)){this.handleCancel(e);return}const{collisionRect:h}=a.current,m=h?{x:h.left,y:h.top}:Er;this.referenceCoordinates||(this.referenceCoordinates=m);const b=c(e,{active:n,context:a.current,currentCoordinates:m});if(b){const S=_o(b,m),v={x:0,y:0},{scrollableAncestors:_}=a.current;for(const A of _){const x=e.code,{isTop:L,isRight:M,isLeft:k,isBottom:O,maxScroll:B,minScroll:G}=ay(A),P=UE(A),W={x:Math.min(x===lt.Right?P.right-P.width/2:P.right,Math.max(x===lt.Right?P.left:P.left+P.width/2,b.x)),y:Math.min(x===lt.Down?P.bottom-P.height/2:P.bottom,Math.max(x===lt.Down?P.top:P.top+P.height/2,b.y))},pe=x===lt.Right&&!M||x===lt.Left&&!k,Se=x===lt.Down&&!O||x===lt.Up&&!L;if(pe&&W.x!==b.x){const we=A.scrollLeft+S.x,Ue=x===lt.Right&&we<=B.x||x===lt.Left&&we>=G.x;if(Ue&&!S.y){A.scrollTo({left:we,behavior:d});return}Ue?v.x=A.scrollLeft-we:v.x=x===lt.Right?A.scrollLeft-B.x:A.scrollLeft-G.x,v.x&&A.scrollBy({left:-v.x,behavior:d});break}else if(Se&&W.y!==b.y){const we=A.scrollTop+S.y,Ue=x===lt.Down&&we<=B.y||x===lt.Up&&we>=G.y;if(Ue&&!S.x){A.scrollTo({top:we,behavior:d});return}Ue?v.y=A.scrollTop-we:v.y=x===lt.Down?A.scrollTop-B.y:A.scrollTop-G.y,v.y&&A.scrollBy({top:-v.y,behavior:d});break}}this.handleMove(e,hs(_o(b,this.referenceCoordinates),v))}}}handleMove(e,n){const{onMove:a}=this.props;e.preventDefault(),a(n)}handleEnd(e){const{onEnd:n}=this.props;e.preventDefault(),this.detach(),n()}handleCancel(e){const{onCancel:n}=this.props;e.preventDefault(),this.detach(),n()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}yf.activators=[{eventName:"onKeyDown",handler:(s,e,n)=>{let{keyboardCodes:a=oy,onActivation:i}=e,{active:l}=n;const{code:c}=s.nativeEvent;if(a.start.includes(c)){const d=l.activatorNode.current;return d&&s.target!==d?!1:(s.preventDefault(),i?.({event:s.nativeEvent}),!0)}return!1}}];function ly(s){return!!(s&&"distance"in s)}function cy(s){return!!(s&&"delay"in s)}class bf{constructor(e,n,a){var i;a===void 0&&(a=VE(e.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=e,this.events=n;const{event:l}=e,{target:c}=l;this.props=e,this.events=n,this.document=fs(c),this.documentListeners=new xo(this.document),this.listeners=new xo(a),this.windowListeners=new xo(Mn(c)),this.initialCoordinates=(i=ff(l))!=null?i:Er,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:e,props:{options:{activationConstraint:n,bypassActivationConstraint:a}}}=this;if(this.listeners.add(e.move.name,this.handleMove,{passive:!1}),this.listeners.add(e.end.name,this.handleEnd),e.cancel&&this.listeners.add(e.cancel.name,this.handleCancel),this.windowListeners.add(ar.Resize,this.handleCancel),this.windowListeners.add(ar.DragStart,sy),this.windowListeners.add(ar.VisibilityChange,this.handleCancel),this.windowListeners.add(ar.ContextMenu,sy),this.documentListeners.add(ar.Keydown,this.handleKeydown),n){if(a!=null&&a({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(cy(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if(ly(n)){this.handlePending(n);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(e,n){const{active:a,onPending:i}=this.props;i(a,e,this.initialCoordinates,n)}handleStart(){const{initialCoordinates:e}=this,{onStart:n}=this.props;e&&(this.activated=!0,this.documentListeners.add(ar.Click,FE,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(ar.SelectionChange,this.removeTextSelection),n(e))}handleMove(e){var n;const{activated:a,initialCoordinates:i,props:l}=this,{onMove:c,options:{activationConstraint:d}}=l;if(!i)return;const f=(n=ff(e))!=null?n:Er,h=_o(i,f);if(!a&&d){if(ly(d)){if(d.tolerance!=null&&mf(h,d.tolerance))return this.handleCancel();if(mf(h,d.distance))return this.handleStart()}if(cy(d)&&mf(h,d.tolerance))return this.handleCancel();this.handlePending(d,h);return}e.cancelable&&e.preventDefault(),c(f)}handleEnd(){const{onAbort:e,onEnd:n}=this.props;this.detach(),this.activated||e(this.props.active),n()}handleCancel(){const{onAbort:e,onCancel:n}=this.props;this.detach(),this.activated||e(this.props.active),n()}handleKeydown(e){e.code===lt.Esc&&this.handleCancel()}removeTextSelection(){var e;(e=this.document.getSelection())==null||e.removeAllRanges()}}const WE={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class vf extends bf{constructor(e){const{event:n}=e,a=fs(n.target);super(e,WE,a)}}vf.activators=[{eventName:"onPointerDown",handler:(s,e)=>{let{nativeEvent:n}=s,{onActivation:a}=e;return!n.isPrimary||n.button!==0?!1:(a?.({event:n}),!0)}}];const YE={move:{name:"mousemove"},end:{name:"mouseup"}};var Sf;(function(s){s[s.RightClick=2]="RightClick"})(Sf||(Sf={}));class XE extends bf{constructor(e){super(e,YE,fs(e.event.target))}}XE.activators=[{eventName:"onMouseDown",handler:(s,e)=>{let{nativeEvent:n}=s,{onActivation:a}=e;return n.button===Sf.RightClick?!1:(a?.({event:n}),!0)}}];const wf={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class JE extends bf{constructor(e){super(e,wf)}static setup(){return window.addEventListener(wf.move.name,e,{capture:!1,passive:!1}),function(){window.removeEventListener(wf.move.name,e)};function e(){}}}JE.activators=[{eventName:"onTouchStart",handler:(s,e)=>{let{nativeEvent:n}=s,{onActivation:a}=e;const{touches:i}=n;return i.length>1?!1:(a?.({event:n}),!0)}}];var To;(function(s){s[s.Pointer=0]="Pointer",s[s.DraggableRect=1]="DraggableRect"})(To||(To={}));var fc;(function(s){s[s.TreeOrder=0]="TreeOrder",s[s.ReversedTreeOrder=1]="ReversedTreeOrder"})(fc||(fc={}));function QE(s){let{acceleration:e,activator:n=To.Pointer,canScroll:a,draggingRect:i,enabled:l,interval:c=5,order:d=fc.TreeOrder,pointerCoordinates:f,scrollableAncestors:h,scrollableAncestorRects:m,delta:b,threshold:S}=s;const v=e_({delta:b,disabled:!l}),[_,A]=uE(),x=N.useRef({x:0,y:0}),L=N.useRef({x:0,y:0}),M=N.useMemo(()=>{switch(n){case To.Pointer:return f?{top:f.y,bottom:f.y,left:f.x,right:f.x}:null;case To.DraggableRect:return i}},[n,i,f]),k=N.useRef(null),O=N.useCallback(()=>{const G=k.current;if(!G)return;const P=x.current.x*L.current.x,W=x.current.y*L.current.y;G.scrollBy(P,W)},[]),B=N.useMemo(()=>d===fc.TreeOrder?[...h].reverse():h,[d,h]);N.useEffect(()=>{if(!l||!h.length||!M){A();return}for(const G of B){if(a?.(G)===!1)continue;const P=h.indexOf(G),W=m[P];if(!W)continue;const{direction:pe,speed:Se}=zE(G,W,M,e,S);for(const we of["x","y"])v[we][pe[we]]||(Se[we]=0,pe[we]=0);if(Se.x>0||Se.y>0){A(),k.current=G,_(O,c),x.current=Se,L.current=pe;return}}x.current={x:0,y:0},L.current={x:0,y:0},A()},[e,O,a,A,l,c,JSON.stringify(M),JSON.stringify(v),_,h,B,m,JSON.stringify(S)])}const ZE={x:{[dn.Backward]:!1,[dn.Forward]:!1},y:{[dn.Backward]:!1,[dn.Forward]:!1}};function e_(s){let{delta:e,disabled:n}=s;const a=cf(e);return Co(i=>{if(n||!a||!i)return ZE;const l={x:Math.sign(e.x-a.x),y:Math.sign(e.y-a.y)};return{x:{[dn.Backward]:i.x[dn.Backward]||l.x===-1,[dn.Forward]:i.x[dn.Forward]||l.x===1},y:{[dn.Backward]:i.y[dn.Backward]||l.y===-1,[dn.Forward]:i.y[dn.Forward]||l.y===1}}},[n,e,a])}function t_(s,e){const n=e!=null?s.get(e):void 0,a=n?n.node.current:null;return Co(i=>{var l;return e==null?null:(l=a??i)!=null?l:null},[a,e])}function n_(s,e){return N.useMemo(()=>s.reduce((n,a)=>{const{sensor:i}=a,l=i.activators.map(c=>({eventName:c.eventName,handler:e(c.handler,a)}));return[...n,...l]},[]),[s,e])}var ko;(function(s){s[s.Always=0]="Always",s[s.BeforeDragging=1]="BeforeDragging",s[s.WhileDragging=2]="WhileDragging"})(ko||(ko={}));var Cf;(function(s){s.Optimized="optimized"})(Cf||(Cf={}));const uy=new Map;function r_(s,e){let{dragging:n,dependencies:a,config:i}=e;const[l,c]=N.useState(null),{frequency:d,measure:f,strategy:h}=i,m=N.useRef(s),b=x(),S=wo(b),v=N.useCallback(function(L){L===void 0&&(L=[]),!S.current&&c(M=>M===null?L:M.concat(L.filter(k=>!M.includes(k))))},[S]),_=N.useRef(null),A=Co(L=>{if(b&&!n)return uy;if(!L||L===uy||m.current!==s||l!=null){const M=new Map;for(let k of s){if(!k)continue;if(l&&l.length>0&&!l.includes(k.id)&&k.rect.current){M.set(k.id,k.rect.current);continue}const O=k.node.current,B=O?new gf(f(O),O):null;k.rect.current=B,B&&M.set(k.id,B)}return M}return L},[s,l,n,b,f]);return N.useEffect(()=>{m.current=s},[s]),N.useEffect(()=>{b||v()},[n,b]),N.useEffect(()=>{l&&l.length>0&&c(null)},[JSON.stringify(l)]),N.useEffect(()=>{b||typeof d!="number"||_.current!==null||(_.current=setTimeout(()=>{v(),_.current=null},d))},[d,b,v,...a]),{droppableRects:A,measureDroppableContainers:v,measuringScheduled:l!=null};function x(){switch(h){case ko.Always:return!1;case ko.BeforeDragging:return n;default:return!n}}}function dy(s,e){return Co(n=>s?n||(typeof e=="function"?e(s):s):null,[e,s])}function a_(s,e){return dy(s,e)}function i_(s){let{callback:e,disabled:n}=s;const a=lf(e),i=N.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:l}=window;return new l(a)},[a,n]);return N.useEffect(()=>()=>i?.disconnect(),[i]),i}function hc(s){let{callback:e,disabled:n}=s;const a=lf(e),i=N.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:l}=window;return new l(a)},[n]);return N.useEffect(()=>()=>i?.disconnect(),[i]),i}function s_(s){return new gf(ps(s),s)}function fy(s,e,n){e===void 0&&(e=s_);const[a,i]=N.useState(null);function l(){i(f=>{if(!s)return null;if(s.isConnected===!1){var h;return(h=f??n)!=null?h:null}const m=e(s);return JSON.stringify(f)===JSON.stringify(m)?f:m})}const c=i_({callback(f){if(s)for(const h of f){const{type:m,target:b}=h;if(m==="childList"&&b instanceof HTMLElement&&b.contains(s)){l();break}}}}),d=hc({callback:l});return Ur(()=>{l(),s?(d?.observe(s),c?.observe(document.body,{childList:!0,subtree:!0})):(d?.disconnect(),c?.disconnect())},[s]),a}function o_(s){const e=dy(s);return Qm(s,e)}const hy=[];function l_(s){const e=N.useRef(s),n=Co(a=>s?a&&a!==hy&&s&&e.current&&s.parentNode===e.current.parentNode?a:dc(s):hy,[s]);return N.useEffect(()=>{e.current=s},[s]),n}function c_(s){const[e,n]=N.useState(null),a=N.useRef(s),i=N.useCallback(l=>{const c=hf(l.target);c&&n(d=>d?(d.set(c,pf(c)),new Map(d)):null)},[]);return N.useEffect(()=>{const l=a.current;if(s!==l){c(l);const d=s.map(f=>{const h=hf(f);return h?(h.addEventListener("scroll",i,{passive:!0}),[h,pf(h)]):null}).filter(f=>f!=null);n(d.length?new Map(d):null),a.current=s}return()=>{c(s),c(l)};function c(d){d.forEach(f=>{const h=hf(f);h?.removeEventListener("scroll",i)})}},[i,s]),N.useMemo(()=>s.length?e?Array.from(e.values()).reduce((l,c)=>hs(l,c),Er):iy(s):Er,[s,e])}function py(s,e){e===void 0&&(e=[]);const n=N.useRef(null);return N.useEffect(()=>{n.current=null},e),N.useEffect(()=>{const a=s!==Er;a&&!n.current&&(n.current=s),!a&&n.current&&(n.current=null)},[s]),n.current?_o(s,n.current):Er}function u_(s){N.useEffect(()=>{if(!lc)return;const e=s.map(n=>{let{sensor:a}=n;return a.setup==null?void 0:a.setup()});return()=>{for(const n of e)n?.()}},s.map(e=>{let{sensor:n}=e;return n}))}function d_(s,e){return N.useMemo(()=>s.reduce((n,a)=>{let{eventName:i,handler:l}=a;return n[i]=c=>{l(c,e)},n},{}),[s,e])}function gy(s){return N.useMemo(()=>s?NE(s):null,[s])}const my=[];function f_(s,e){e===void 0&&(e=ps);const[n]=s,a=gy(n?Mn(n):null),[i,l]=N.useState(my);function c(){l(()=>s.length?s.map(f=>ry(f)?a:new gf(e(f),f)):my)}const d=hc({callback:c});return Ur(()=>{d?.disconnect(),c(),s.forEach(f=>d?.observe(f))},[s]),i}function h_(s){if(!s)return null;if(s.children.length>1)return s;const e=s.children[0];return So(e)?e:s}function p_(s){let{measure:e}=s;const[n,a]=N.useState(null),i=N.useCallback(h=>{for(const{target:m}of h)if(So(m)){a(b=>{const S=e(m);return b?{...b,width:S.width,height:S.height}:S});break}},[e]),l=hc({callback:i}),c=N.useCallback(h=>{const m=h_(h);l?.disconnect(),m&&l?.observe(m),a(m?e(m):null)},[e,l]),[d,f]=cc(c);return N.useMemo(()=>({nodeRef:d,rect:n,setRef:f}),[n,d,f])}const g_=[{sensor:vf,options:{}},{sensor:yf,options:{}}],m_={current:{}},pc={draggable:{measure:Zm},droppable:{measure:Zm,strategy:ko.WhileDragging,frequency:Cf.Optimized},dragOverlay:{measure:ps}};class Ro extends Map{get(e){var n;return e!=null&&(n=super.get(e))!=null?n:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(e=>{let{disabled:n}=e;return!n})}getNodeFor(e){var n,a;return(n=(a=this.get(e))==null?void 0:a.node.current)!=null?n:void 0}}const y_={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new Ro,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:uc},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:pc,measureDroppableContainers:uc,windowRect:null,measuringScheduled:!1},b_={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:uc,draggableNodes:new Map,over:null,measureDroppableContainers:uc},gc=N.createContext(b_),yy=N.createContext(y_);function v_(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Ro}}}function S_(s,e){switch(e.type){case nn.DragStart:return{...s,draggable:{...s.draggable,initialCoordinates:e.initialCoordinates,active:e.active}};case nn.DragMove:return s.draggable.active==null?s:{...s,draggable:{...s.draggable,translate:{x:e.coordinates.x-s.draggable.initialCoordinates.x,y:e.coordinates.y-s.draggable.initialCoordinates.y}}};case nn.DragEnd:case nn.DragCancel:return{...s,draggable:{...s.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case nn.RegisterDroppable:{const{element:n}=e,{id:a}=n,i=new Ro(s.droppable.containers);return i.set(a,n),{...s,droppable:{...s.droppable,containers:i}}}case nn.SetDroppableDisabled:{const{id:n,key:a,disabled:i}=e,l=s.droppable.containers.get(n);if(!l||a!==l.key)return s;const c=new Ro(s.droppable.containers);return c.set(n,{...l,disabled:i}),{...s,droppable:{...s.droppable,containers:c}}}case nn.UnregisterDroppable:{const{id:n,key:a}=e,i=s.droppable.containers.get(n);if(!i||a!==i.key)return s;const l=new Ro(s.droppable.containers);return l.delete(n),{...s,droppable:{...s.droppable,containers:l}}}default:return s}}function w_(s){let{disabled:e}=s;const{active:n,activatorEvent:a,draggableNodes:i}=N.useContext(gc),l=cf(a),c=cf(n?.id);return N.useEffect(()=>{if(!e&&!a&&l&&c!=null){if(!df(l)||document.activeElement===l.target)return;const d=i.get(c);if(!d)return;const{activatorNode:f,node:h}=d;if(!f.current&&!h.current)return;requestAnimationFrame(()=>{for(const m of[f.current,h.current]){if(!m)continue;const b=hE(m);if(b){b.focus();break}}})}},[a,e,i,c,l]),null}function C_(s,e){let{transform:n,...a}=e;return s!=null&&s.length?s.reduce((i,l)=>l({transform:i,...a}),n):n}function E_(s){return N.useMemo(()=>({draggable:{...pc.draggable,...s?.draggable},droppable:{...pc.droppable,...s?.droppable},dragOverlay:{...pc.dragOverlay,...s?.dragOverlay}}),[s?.draggable,s?.droppable,s?.dragOverlay])}function __(s){let{activeNode:e,measure:n,initialRect:a,config:i=!0}=s;const l=N.useRef(!1),{x:c,y:d}=typeof i=="boolean"?{x:i,y:i}:i;Ur(()=>{if(!c&&!d||!e){l.current=!1;return}if(l.current||!a)return;const h=e?.node.current;if(!h||h.isConnected===!1)return;const m=n(h),b=Qm(m,a);if(c||(b.x=0),d||(b.y=0),l.current=!0,Math.abs(b.x)>0||Math.abs(b.y)>0){const S=ey(h);S&&S.scrollBy({top:b.y,left:b.x})}},[e,c,d,a,n])}const by=N.createContext({...Er,scaleX:1,scaleY:1});var La;(function(s){s[s.Uninitialized=0]="Uninitialized",s[s.Initializing=1]="Initializing",s[s.Initialized=2]="Initialized"})(La||(La={}));const A_=N.memo(function(e){var n,a,i,l;let{id:c,accessibility:d,autoScroll:f=!0,children:h,sensors:m=g_,collisionDetection:b=kE,measuring:S,modifiers:v,..._}=e;const A=N.useReducer(S_,void 0,v_),[x,L]=A,[M,k]=vE(),[O,B]=N.useState(La.Uninitialized),G=O===La.Initialized,{draggable:{active:P,nodes:W,translate:pe},droppable:{containers:Se}}=x,we=P!=null?W.get(P):null,Ue=N.useRef({initial:null,translated:null}),Ve=N.useMemo(()=>{var he;return P!=null?{id:P,data:(he=we?.data)!=null?he:m_,rect:Ue}:null},[P,we]),qe=N.useRef(null),[wt,J]=N.useState(null),[fe,Le]=N.useState(null),Z=wo(_,Object.values(_)),ge=Eo("DndDescribedBy",c),H=N.useMemo(()=>Se.getEnabled(),[Se]),T=E_(S),{droppableRects:ce,measureDroppableContainers:de,measuringScheduled:ye}=r_(H,{dragging:G,dependencies:[pe.x,pe.y],config:T.droppable}),ie=t_(W,P),xe=N.useMemo(()=>fe?ff(fe):null,[fe]),_e=ve(),Te=a_(ie,T.draggable.measure);__({activeNode:P!=null?W.get(P):null,config:_e.layoutShiftCompensation,initialRect:Te,measure:T.draggable.measure});const $e=fy(ie,T.draggable.measure,Te),pt=fy(ie?ie.parentElement:null),Ct=N.useRef({activatorEvent:null,active:null,activeNode:ie,collisionRect:null,collisions:null,droppableRects:ce,draggableNodes:W,draggingNode:null,draggingNodeRect:null,droppableContainers:Se,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),Dt=Se.getNodeFor((n=Ct.current.over)==null?void 0:n.id),ht=p_({measure:T.dragOverlay.measure}),rn=(a=ht.nodeRef.current)!=null?a:ie,Kt=G?(i=ht.rect)!=null?i:$e:null,It=!!(ht.nodeRef.current&&ht.rect),qr=o_(It?null:$e),hn=gy(rn?Mn(rn):null),an=l_(G?Dt??ie:null),pn=f_(an),Kn=C_(v,{transform:{x:pe.x-qr.x,y:pe.y-qr.y,scaleX:1,scaleY:1},activatorEvent:fe,active:Ve,activeNodeRect:$e,containerNodeRect:pt,draggingNodeRect:Kt,over:Ct.current.over,overlayNodeRect:ht.rect,scrollableAncestors:an,scrollableAncestorRects:pn,windowRect:hn}),Gr=xe?hs(xe,pe):null,qt=c_(an),_i=py(qt),aa=py(qt,[$e]),gn=hs(Kn,_i),Rn=Kt?OE(Kt,Kn):null,nt=Ve&&Rn?b({active:Ve,collisionRect:Rn,droppableRects:ce,droppableContainers:H,pointerCoordinates:Gr}):null,Nn=Xm(nt,"id"),[mn,ia]=N.useState(null),or=It?Kn:hs(Kn,aa),sn=RE(or,(l=mn?.rect)!=null?l:null,$e),_n=N.useRef(null),on=N.useCallback((he,Ee)=>{let{sensor:Ae,options:Fe}=Ee;if(qe.current==null)return;const ke=W.get(qe.current);if(!ke)return;const Ne=he.nativeEvent,Re=new Ae({active:qe.current,activeNode:ke,event:Ne,options:Fe,context:Ct,onAbort(He){if(!W.get(He))return;const{onDragAbort:Ce}=Z.current,je={id:He};Ce?.(je),M({type:"onDragAbort",event:je})},onPending(He,Ge,Ce,je){if(!W.get(He))return;const{onDragPending:At}=Z.current,Mt={id:He,constraint:Ge,initialCoordinates:Ce,offset:je};At?.(Mt),M({type:"onDragPending",event:Mt})},onStart(He){const Ge=qe.current;if(Ge==null)return;const Ce=W.get(Ge);if(!Ce)return;const{onDragStart:je}=Z.current,tt={activatorEvent:Ne,active:{id:Ge,data:Ce.data,rect:Ue}};Oa.unstable_batchedUpdates(()=>{je?.(tt),B(La.Initializing),L({type:nn.DragStart,initialCoordinates:He,active:Ge}),M({type:"onDragStart",event:tt}),J(_n.current),Le(Ne)})},onMove(He){L({type:nn.DragMove,coordinates:He})},onEnd:et(nn.DragEnd),onCancel:et(nn.DragCancel)});_n.current=Re;function et(He){return async function(){const{active:Ce,collisions:je,over:tt,scrollAdjustedTranslate:At}=Ct.current;let Mt=null;if(Ce&&At){const{cancelDrop:Dn}=Z.current;Mt={activatorEvent:Ne,active:Ce,collisions:je,delta:At,over:tt},He===nn.DragEnd&&typeof Dn=="function"&&await Promise.resolve(Dn(Mt))&&(He=nn.DragCancel)}qe.current=null,Oa.unstable_batchedUpdates(()=>{L({type:He}),B(La.Uninitialized),ia(null),J(null),Le(null),_n.current=null;const Dn=He===nn.DragEnd?"onDragEnd":"onDragCancel";if(Mt){const jt=Z.current[Dn];jt?.(Mt),M({type:Dn,event:Mt})}})}}},[W]),V=N.useCallback((he,Ee)=>(Ae,Fe)=>{const ke=Ae.nativeEvent,Ne=W.get(Fe);if(qe.current!==null||!Ne||ke.dndKit||ke.defaultPrevented)return;const Re={active:Ne};he(Ae,Ee.options,Re)===!0&&(ke.dndKit={capturedBy:Ee.sensor},qe.current=Fe,on(Ae,Ee))},[W,on]),se=n_(m,V);u_(m),Ur(()=>{$e&&O===La.Initializing&&B(La.Initialized)},[$e,O]),N.useEffect(()=>{const{onDragMove:he}=Z.current,{active:Ee,activatorEvent:Ae,collisions:Fe,over:ke}=Ct.current;if(!Ee||!Ae)return;const Ne={active:Ee,activatorEvent:Ae,collisions:Fe,delta:{x:gn.x,y:gn.y},over:ke};Oa.unstable_batchedUpdates(()=>{he?.(Ne),M({type:"onDragMove",event:Ne})})},[gn.x,gn.y]),N.useEffect(()=>{const{active:he,activatorEvent:Ee,collisions:Ae,droppableContainers:Fe,scrollAdjustedTranslate:ke}=Ct.current;if(!he||qe.current==null||!Ee||!ke)return;const{onDragOver:Ne}=Z.current,Re=Fe.get(Nn),et=Re&&Re.rect.current?{id:Re.id,rect:Re.rect.current,data:Re.data,disabled:Re.disabled}:null,He={active:he,activatorEvent:Ee,collisions:Ae,delta:{x:ke.x,y:ke.y},over:et};Oa.unstable_batchedUpdates(()=>{ia(et),Ne?.(He),M({type:"onDragOver",event:He})})},[Nn]),Ur(()=>{Ct.current={activatorEvent:fe,active:Ve,activeNode:ie,collisionRect:Rn,collisions:nt,droppableRects:ce,draggableNodes:W,draggingNode:rn,draggingNodeRect:Kt,droppableContainers:Se,over:mn,scrollableAncestors:an,scrollAdjustedTranslate:gn},Ue.current={initial:Kt,translated:Rn}},[Ve,ie,nt,Rn,W,rn,Kt,ce,Se,mn,an,gn]),QE({..._e,delta:pe,draggingRect:Rn,pointerCoordinates:Gr,scrollableAncestors:an,scrollableAncestorRects:pn});const ue=N.useMemo(()=>({active:Ve,activeNode:ie,activeNodeRect:$e,activatorEvent:fe,collisions:nt,containerNodeRect:pt,dragOverlay:ht,draggableNodes:W,droppableContainers:Se,droppableRects:ce,over:mn,measureDroppableContainers:de,scrollableAncestors:an,scrollableAncestorRects:pn,measuringConfiguration:T,measuringScheduled:ye,windowRect:hn}),[Ve,ie,$e,fe,nt,pt,ht,W,Se,ce,mn,de,an,pn,T,ye,hn]),ee=N.useMemo(()=>({activatorEvent:fe,activators:se,active:Ve,activeNodeRect:$e,ariaDescribedById:{draggable:ge},dispatch:L,draggableNodes:W,over:mn,measureDroppableContainers:de}),[fe,se,Ve,$e,L,ge,W,mn,de]);return Ye.createElement(Vm.Provider,{value:k},Ye.createElement(gc.Provider,{value:ee},Ye.createElement(yy.Provider,{value:ue},Ye.createElement(by.Provider,{value:sn},h)),Ye.createElement(w_,{disabled:d?.restoreFocus===!1})),Ye.createElement(CE,{...d,hiddenTextDescribedById:ge}));function ve(){const he=wt?.autoScrollEnabled===!1,Ee=typeof f=="object"?f.enabled===!1:f===!1,Ae=G&&!he&&!Ee;return typeof f=="object"?{...f,enabled:Ae}:{enabled:Ae}}}),x_=N.createContext(null),vy="button",T_="Draggable";function k_(s){let{id:e,data:n,disabled:a=!1,attributes:i}=s;const l=Eo(T_),{activators:c,activatorEvent:d,active:f,activeNodeRect:h,ariaDescribedById:m,draggableNodes:b,over:S}=N.useContext(gc),{role:v=vy,roleDescription:_="draggable",tabIndex:A=0}=i??{},x=f?.id===e,L=N.useContext(x?by:x_),[M,k]=cc(),[O,B]=cc(),G=d_(c,e),P=wo(n);Ur(()=>(b.set(e,{id:e,key:l,node:M,activatorNode:O,data:P}),()=>{const pe=b.get(e);pe&&pe.key===l&&b.delete(e)}),[b,e]);const W=N.useMemo(()=>({role:v,tabIndex:A,"aria-disabled":a,"aria-pressed":x&&v===vy?!0:void 0,"aria-roledescription":_,"aria-describedby":m.draggable}),[a,v,A,x,_,m.draggable]);return{active:f,activatorEvent:d,activeNodeRect:h,attributes:W,isDragging:x,listeners:a?void 0:G,node:M,over:S,setNodeRef:k,setActivatorNodeRef:B,transform:L}}function R_(){return N.useContext(yy)}const D_="Droppable",O_={timeout:25};function L_(s){let{data:e,disabled:n=!1,id:a,resizeObserverConfig:i}=s;const l=Eo(D_),{active:c,dispatch:d,over:f,measureDroppableContainers:h}=N.useContext(gc),m=N.useRef({disabled:n}),b=N.useRef(!1),S=N.useRef(null),v=N.useRef(null),{disabled:_,updateMeasurementsFor:A,timeout:x}={...O_,...i},L=wo(A??a),M=N.useCallback(()=>{if(!b.current){b.current=!0;return}v.current!=null&&clearTimeout(v.current),v.current=setTimeout(()=>{h(Array.isArray(L.current)?L.current:[L.current]),v.current=null},x)},[x]),k=hc({callback:M,disabled:_||!c}),O=N.useCallback((W,pe)=>{k&&(pe&&(k.unobserve(pe),b.current=!1),W&&k.observe(W))},[k]),[B,G]=cc(O),P=wo(e);return N.useEffect(()=>{!k||!B.current||(k.disconnect(),b.current=!1,k.observe(B.current))},[B,k]),N.useEffect(()=>(d({type:nn.RegisterDroppable,element:{id:a,key:l,disabled:n,node:B,rect:S,data:P}}),()=>d({type:nn.UnregisterDroppable,key:l,id:a})),[a]),N.useEffect(()=>{n!==m.current.disabled&&(d({type:nn.SetDroppableDisabled,id:a,key:l,disabled:n}),m.current.disabled=n)},[a,l,n,d]),{active:c,rect:S,isOver:f?.id===a,node:B,over:f,setNodeRef:G}}function Ef(s,e,n){const a=s.slice();return a.splice(n<0?a.length+n:n,0,a.splice(e,1)[0]),a}function M_(s,e){return s.reduce((n,a,i)=>{const l=e.get(a);return l&&(n[i]=l),n},Array(s.length))}function mc(s){return s!==null&&s>=0}function $_(s,e){if(s===e)return!0;if(s.length!==e.length)return!1;for(let n=0;n<s.length;n++)if(s[n]!==e[n])return!1;return!0}function N_(s){return typeof s=="boolean"?{draggable:s,droppable:s}:s}const yc={scaleX:1,scaleY:1},I_=s=>{var e;let{rects:n,activeNodeRect:a,activeIndex:i,overIndex:l,index:c}=s;const d=(e=n[i])!=null?e:a;if(!d)return null;const f=j_(n,c,i);if(c===i){const h=n[l];return h?{x:i<l?h.left+h.width-(d.left+d.width):h.left-d.left,y:0,...yc}:null}return c>i&&c<=l?{x:-d.width-f,y:0,...yc}:c<i&&c>=l?{x:d.width+f,y:0,...yc}:{x:0,y:0,...yc}};function j_(s,e,n){const a=s[e],i=s[e-1],l=s[e+1];return!a||!i&&!l?0:n<e?i?a.left-(i.left+i.width):l.left-(a.left+a.width):l?l.left-(a.left+a.width):a.left-(i.left+i.width)}const _f=s=>{let{rects:e,activeIndex:n,overIndex:a,index:i}=s;const l=Ef(e,a,n),c=e[i],d=l[i];return!d||!c?null:{x:d.left-c.left,y:d.top-c.top,scaleX:d.width/c.width,scaleY:d.height/c.height}},bc={scaleX:1,scaleY:1},H_=s=>{var e;let{activeIndex:n,activeNodeRect:a,index:i,rects:l,overIndex:c}=s;const d=(e=l[n])!=null?e:a;if(!d)return null;if(i===n){const h=l[c];return h?{x:0,y:n<c?h.top+h.height-(d.top+d.height):h.top-d.top,...bc}:null}const f=z_(l,i,n);return i>n&&i<=c?{x:0,y:-d.height-f,...bc}:i<n&&i>=c?{x:0,y:d.height+f,...bc}:{x:0,y:0,...bc}};function z_(s,e,n){const a=s[e],i=s[e-1],l=s[e+1];return a?n<e?i?a.top-(i.top+i.height):l?l.top-(a.top+a.height):0:l?l.top-(a.top+a.height):i?a.top-(i.top+i.height):0:0}const Sy="Sortable",wy=Ye.createContext({activeIndex:-1,containerId:Sy,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:_f,disabled:{draggable:!1,droppable:!1}});function U_(s){let{children:e,id:n,items:a,strategy:i=_f,disabled:l=!1}=s;const{active:c,dragOverlay:d,droppableRects:f,over:h,measureDroppableContainers:m}=R_(),b=Eo(Sy,n),S=d.rect!==null,v=N.useMemo(()=>a.map(G=>typeof G=="object"&&"id"in G?G.id:G),[a]),_=c!=null,A=c?v.indexOf(c.id):-1,x=h?v.indexOf(h.id):-1,L=N.useRef(v),M=!$_(v,L.current),k=x!==-1&&A===-1||M,O=N_(l);Ur(()=>{M&&_&&m(v)},[M,v,_,m]),N.useEffect(()=>{L.current=v},[v]);const B=N.useMemo(()=>({activeIndex:A,containerId:b,disabled:O,disableTransforms:k,items:v,overIndex:x,useDragOverlay:S,sortedRects:M_(v,f),strategy:i}),[A,b,O.draggable,O.droppable,k,v,x,f,S,i]);return Ye.createElement(wy.Provider,{value:B},e)}const P_=s=>{let{id:e,items:n,activeIndex:a,overIndex:i}=s;return Ef(n,a,i).indexOf(e)},B_=s=>{let{containerId:e,isSorting:n,wasDragging:a,index:i,items:l,newIndex:c,previousItems:d,previousContainerId:f,transition:h}=s;return!h||!a||d!==l&&i===c?!1:n?!0:c!==i&&e===f},q_={duration:200,easing:"ease"},Cy="transform",G_=Ao.Transition.toString({property:Cy,duration:0,easing:"linear"}),V_={roleDescription:"sortable"};function F_(s){let{disabled:e,index:n,node:a,rect:i}=s;const[l,c]=N.useState(null),d=N.useRef(n);return Ur(()=>{if(!e&&n!==d.current&&a.current){const f=i.current;if(f){const h=ps(a.current,{ignoreTransform:!0}),m={x:f.left-h.left,y:f.top-h.top,scaleX:f.width/h.width,scaleY:f.height/h.height};(m.x||m.y)&&c(m)}}n!==d.current&&(d.current=n)},[e,n,a,i]),N.useEffect(()=>{l&&c(null)},[l]),l}function K_(s){let{animateLayoutChanges:e=B_,attributes:n,disabled:a,data:i,getNewIndex:l=P_,id:c,strategy:d,resizeObserverConfig:f,transition:h=q_}=s;const{items:m,containerId:b,activeIndex:S,disabled:v,disableTransforms:_,sortedRects:A,overIndex:x,useDragOverlay:L,strategy:M}=N.useContext(wy),k=W_(a,v),O=m.indexOf(c),B=N.useMemo(()=>({sortable:{containerId:b,index:O,items:m},...i}),[b,i,O,m]),G=N.useMemo(()=>m.slice(m.indexOf(c)),[m,c]),{rect:P,node:W,isOver:pe,setNodeRef:Se}=L_({id:c,data:B,disabled:k.droppable,resizeObserverConfig:{updateMeasurementsFor:G,...f}}),{active:we,activatorEvent:Ue,activeNodeRect:Ve,attributes:qe,setNodeRef:wt,listeners:J,isDragging:fe,over:Le,setActivatorNodeRef:Z,transform:ge}=k_({id:c,data:B,attributes:{...V_,...n},disabled:k.draggable}),H=cE(Se,wt),T=!!we,ce=T&&!_&&mc(S)&&mc(x),de=!L&&fe,ye=de&&ce?ge:null,xe=ce?ye??(d??M)({rects:A,activeNodeRect:Ve,activeIndex:S,overIndex:x,index:O}):null,_e=mc(S)&&mc(x)?l({id:c,items:m,activeIndex:S,overIndex:x}):O,Te=we?.id,$e=N.useRef({activeId:Te,items:m,newIndex:_e,containerId:b}),pt=m!==$e.current.items,Ct=e({active:we,containerId:b,isDragging:fe,isSorting:T,id:c,index:O,items:m,newIndex:$e.current.newIndex,previousItems:$e.current.items,previousContainerId:$e.current.containerId,transition:h,wasDragging:$e.current.activeId!=null}),Dt=F_({disabled:!Ct,index:O,node:W,rect:P});return N.useEffect(()=>{T&&$e.current.newIndex!==_e&&($e.current.newIndex=_e),b!==$e.current.containerId&&($e.current.containerId=b),m!==$e.current.items&&($e.current.items=m)},[T,_e,b,m]),N.useEffect(()=>{if(Te===$e.current.activeId)return;if(Te!=null&&$e.current.activeId==null){$e.current.activeId=Te;return}const rn=setTimeout(()=>{$e.current.activeId=Te},50);return()=>clearTimeout(rn)},[Te]),{active:we,activeIndex:S,attributes:qe,data:B,rect:P,index:O,newIndex:_e,items:m,isOver:pe,isSorting:T,isDragging:fe,listeners:J,node:W,overIndex:x,over:Le,setNodeRef:H,setActivatorNodeRef:Z,setDroppableNodeRef:Se,setDraggableNodeRef:wt,transform:Dt??xe,transition:ht()};function ht(){if(Dt||pt&&$e.current.newIndex===O)return G_;if(!(de&&!df(Ue)||!h)&&(T||Ct))return Ao.Transition.toString({...h,property:Cy})}}function W_(s,e){var n,a;return typeof s=="boolean"?{draggable:s,droppable:!1}:{draggable:(n=s?.draggable)!=null?n:e.draggable,droppable:(a=s?.droppable)!=null?a:e.droppable}}function vc(s){if(!s)return!1;const e=s.data.current;return!!(e&&"sortable"in e&&typeof e.sortable=="object"&&"containerId"in e.sortable&&"items"in e.sortable&&"index"in e.sortable)}const Y_=[lt.Down,lt.Right,lt.Up,lt.Left],X_=(s,e)=>{let{context:{active:n,collisionRect:a,droppableRects:i,droppableContainers:l,over:c,scrollableAncestors:d}}=e;if(Y_.includes(s.code)){if(s.preventDefault(),!n||!a)return;const f=[];l.getEnabled().forEach(b=>{if(!b||b!=null&&b.disabled)return;const S=i.get(b.id);if(S)switch(s.code){case lt.Down:a.top<S.top&&f.push(b);break;case lt.Up:a.top>S.top&&f.push(b);break;case lt.Left:a.left>S.left&&f.push(b);break;case lt.Right:a.left<S.left&&f.push(b);break}});const h=xE({collisionRect:a,droppableRects:i,droppableContainers:f});let m=Xm(h,"id");if(m===c?.id&&h.length>1&&(m=h[1].id),m!=null){const b=l.get(n.id),S=l.get(m),v=S?i.get(S.id):null,_=S?.node.current;if(_&&v&&b&&S){const x=dc(_).some((G,P)=>d[P]!==G),L=Ey(b,S),M=J_(b,S),k=x||!L?{x:0,y:0}:{x:M?a.width-v.width:0,y:M?a.height-v.height:0},O={x:v.left,y:v.top};return k.x&&k.y?O:_o(O,k)}}}};function Ey(s,e){return!vc(s)||!vc(e)?!1:s.data.current.sortable.containerId===e.data.current.sortable.containerId}function J_(s,e){return!vc(s)||!vc(e)||!Ey(s,e)?!1:s.data.current.sortable.index<e.data.current.sortable.index}const Q_={verticalList:H_,horizontalList:I_,rectSorting:_f},Z_=({items:s,itemKey:e,strategy:n,children:a,onSortEnd:i,onSortStart:l,sortVersion:c})=>{const d=N.useId(),f=N.useRef({inProgress:!1,lastSortTime:0,lastActiveId:null}),h=EE(Fm(vf,{activationConstraint:{distance:5}}),Fm(yf,{coordinateGetter:X_})),m=N.useCallback(v=>{f.current={inProgress:!0,lastSortTime:Date.now(),lastActiveId:v.active.id},l?.({activeId:v.active.id})},[l]),b=N.useCallback(v=>{const{active:_,over:A}=v,x=Date.now();if(!A||_.id===A.id){f.current.inProgress=!1;return}const{lastSortTime:L,lastActiveId:M,inProgress:k}=f.current;if(M===_.id&&x-L<500&&!k)return;f.current={inProgress:!1,lastSortTime:x,lastActiveId:_.id};const O=s.findIndex(G=>String(G[e])===String(_.id)),B=s.findIndex(G=>String(G[e])===String(A.id));if(O!==-1&&B!==-1&&O!==B){const G=Ef(s,O,B);i?.(G,{oldIndex:O,newIndex:B})}},[s,e,i]),S=s.map(v=>String(v[e]));return ft.jsx(A_,{id:d,sensors:h,collisionDetection:AE,onDragStart:m,onDragEnd:b,children:ft.jsx(U_,{items:S,strategy:Q_[n],children:a})},c!==void 0?`${d}-v${c}`:d)},_y=N.createContext(null),eA=({value:s,children:e})=>ft.jsx(_y.Provider,{value:s,children:e}),tA=()=>N.useContext(_y),nA=({id:s,handle:e,as:n="div",children:a})=>{const{attributes:i,listeners:l,setNodeRef:c,transform:d,transition:f,isDragging:h}=K_({id:s}),m={transform:Ao.Transform.toString(d),transition:f,opacity:h?.5:1,position:"relative"};return ft.jsx(n,{ref:c,style:m,...i,...e?{}:l,"data-sortable-item":!0,"data-sortable-id":String(s),"data-dragging":h,children:ft.jsx(eA,{value:{listeners:l,handle:e,isDragging:h},children:a})})},it=dt("DynamicRenderer");function ta(){try{return window.G7Core?.devTools}catch{return}}function Af(s,e,n,a){if(typeof s=="string"){if(/\$t:[a-zA-Z_][a-zA-Z0-9_.\-]*/.test(s))try{return e.resolveTranslations(s,n,a)}catch{return s}return s}if(Array.isArray(s))return s.map(i=>Af(i,e,n,a));if(s&&typeof s=="object"){const i={};for(const[l,c]of Object.entries(s))i[l]=Af(c,e,n,a);return i}return s}function rA(s){const e=Object.keys(s);return e.length>0&&e.every(n=>/^\d+$/.test(n))}const _r=(s,e)=>{const n=["errors"],a={...s};for(const i of Object.keys(e)){const l=e[i],c=s[i];if(l===null){a[i]=null;continue}if(n.includes(i)){a[i]=l;continue}if(Array.isArray(l)){a[i]=l;continue}if(Array.isArray(c)&&l!==null&&typeof l=="object"&&!Array.isArray(l)&&rA(l)){const d=Object.keys(l).map(h=>parseInt(h,10));if((d.length>0?Math.max(...d):0)>=c.length+d.length+10)a[i]={...l};else{const h=[...c];for(const[m,b]of Object.entries(l)){const S=parseInt(m,10);S>=0&&S<h.length?b!==null&&typeof b=="object"&&!Array.isArray(b)&&h[S]!==null&&typeof h[S]=="object"?h[S]=_r(h[S],b):h[S]=b:S>=h.length&&(h[S]=b)}a[i]=h}}else l!==null&&typeof l=="object"&&c!==null&&typeof c=="object"&&!Array.isArray(c)?a[i]=_r(c,l):a[i]=l}return a},Sc=(s,e)=>{let n=null;const a=()=>(n===null&&(n={...s}),n);for(const i of Object.keys(e)){if(!(i in s))continue;const l=e[i],c=s[i];if(l!==null&&typeof l=="object"&&!Array.isArray(l)&&c!==null&&typeof c=="object"&&!Array.isArray(c)){const d=Sc(c,l);if(d===c)continue;Object.keys(d).length===0?delete a()[i]:a()[i]=d}else delete a()[i]}return n??s},aA=s=>{const e={},n=s.split(";").filter(a=>a.trim());for(const a of n){const i=a.indexOf(":");if(i===-1)continue;const l=a.substring(0,i).trim(),c=a.substring(i+1).trim();if(l&&c){const d=l.replace(/-([a-z])/g,(f,h)=>h.toUpperCase());e[d]=c}}return e};class iA extends N.Component{constructor(e){super(e),this.state={hasError:!1,error:null}}static getDerivedStateFromError(e){return{hasError:!0,error:e}}componentDidCatch(e,n){it.error(`컴포넌트 렌더링 에러 (ID: ${this.props.componentId}, Name: ${this.props.componentName}):`,e,n)}render(){return this.state.hasError?this.props.fallback?this.props.fallback:ft.jsxs("div",{className:"p-4 my-2 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-red-800 dark:text-red-400",children:[ft.jsx("div",{className:"font-semibold mb-1",children:"컴포넌트 로드 실패"}),ft.jsxs("div",{className:"text-xs opacity-80",children:[this.props.componentName&&`[${this.props.componentName}] `,"데이터를 표시할 수 없습니다."]}),this.state.error&&ft.jsxs("details",{className:"mt-2",children:[ft.jsx("summary",{className:"cursor-pointer text-xs opacity-70 hover:opacity-100",children:"상세 정보"}),ft.jsx("div",{className:"mt-1 p-2 bg-red-100 dark:bg-red-900/30 rounded text-xs font-mono whitespace-pre-wrap break-all",children:this.state.error.message})]})]}):this.props.children}}const Pr=N.memo(({componentDef:s,dataContext:e,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:d,parentFormContextProp:f,parentDataContext:h,isRootRenderer:m=!1,isInsideIteration:b=!1,isEditMode:S=!1,onComponentSelect:v,onComponentHover:_,onDragStart:A,onDragEnd:x,componentPath:L,layoutKey:M})=>{const[k,O]=N.useState({loadingActions:{}});N.useLayoutEffect(()=>{d||(i.startRenderCycle(),window.__g7SetLocalOverrideKeys||(window.__g7ForcedLocalFields=void 0),window.__g7PendingLocalState=null)});const B=N.useRef(null);N.useLayoutEffect(()=>{if(!d&&e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,_merge:se,...ue}=e._localInit,ee=`${JSON.stringify(ue)}:${V||"no-force"}`;if(B.current!==ee){B.current=ee;const ve=se||"shallow",Ee=window.G7Core?.state?.get?.()?._local||{},Ae=window.__g7PendingLocalState||{},Fe={...Ee,...Ae};ve==="replace"?window.__g7PendingLocalState={loadingActions:Fe.loadingActions||{},...ue,hasChanges:!1}:ve==="deep"?window.__g7PendingLocalState=_r(Fe,{...ue,hasChanges:!1}):window.__g7PendingLocalState={...Fe,...ue,hasChanges:!1};const ke=window.__g7SetLocalOverrideKeys;ke&&typeof ke=="object"&&(ve==="replace"?window.__g7SetLocalOverrideKeys={...ue,hasChanges:!1}:window.__g7SetLocalOverrideKeys=_r(ke,{...ue,hasChanges:!1}))}}}),N.useLayoutEffect(()=>{if(!d){const V=window.__g7SetLocalOverrideKeys;if(V){O(ve=>Sc(ve,V));const se=V,ue=window.__g7ForcedLocalFields,ee=window.__g7LastSetLocalSnapshot;queueMicrotask(()=>{window.__g7SetLocalOverrideKeys===se&&(window.__g7SetLocalOverrideKeys=void 0),window.__g7ForcedLocalFields===ue&&(window.__g7ForcedLocalFields=void 0),window.__g7LastSetLocalSnapshot===ee&&(window.__g7LastSetLocalSnapshot=void 0)})}}},[e._local]);const G=N.useRef(M);N.useEffect(()=>{s._fromBase&&M&&G.current!==M&&(G.current=M,O({loadingActions:{}}))},[M,s._fromBase]);const P=N.useRef(""),W=N.useRef(!1),pe=N.useRef(null),Se=N.useRef(null),we=N.useRef({}),Ue=N.useRef({}),Ve=N.useRef(h),qe=lE(),wt=HC(),J=wt?.getParentDataContext()??h;N.useEffect(()=>{if(e._localInit&&typeof e._localInit=="object"){const{_forceLocalInit:V,...se}=e._localInit,ue=JSON.stringify(se),ee=V!==void 0,ve=nf(),he=`${ue}:${V||"no-force"}`,Ee=FC({globalTrackedKey:ve.hash,instanceHandledKey:Se.current,trackingKey:he});if(Ee==="apply"){Se.current=he,ve.hash=he,ve.timestamp=V,P.current=ue,ee&&(W.current=!0);const{_merge:Fe,...ke}=se,Ne=Fe||"shallow";O(Re=>Ne==="replace"?{loadingActions:Re.loadingActions||{},...ke,hasChanges:!1}:Ne==="deep"?_r(Re,{...ke,hasChanges:!1}):{...Re,...ke,hasChanges:!1}),e._globalSetState&&e._globalSetState(Re=>{const et=Re?._local||{};let He;return Ne==="replace"?He={...ke,hasChanges:!1}:Ne==="deep"?He=_r(et,{...ke,hasChanges:!1}):He={...et,...ke,hasChanges:!1},{...Re,_local:He}}),i.invalidateCacheByKeys(["_local"]),it.log("_localInit applied (data changed):",Object.keys(se))}else if(Ee==="prune"){Se.current=he;const{_merge:Fe,...ke}=se;O(Re=>Sc(Re,ke)),Sc(k,ke)!==k&&(i.invalidateCacheByKeys(["_local"]),it.log("_localInit pruned (applied by another renderer):",Object.keys(ke)))}pe.current=e._localInit,GC(e._localInit)}},[e._localInit,h,i]);const{isTransitioning:fe}=Im(),Le=ZC(),Z=f??Le,{width:ge}=af(),H=tA(),T=N.useMemo(()=>{let V=s;if(s.type==="iterator"&&s.data){const{data:ee,itemName:ve,indexName:he,...Ee}=s;V={...Ee,iteration:{source:ee,item_var:ve||"item",index_var:he}}}if(!V.responsive)return V;const se=mi.getMatchingKey(V.responsive,ge);if(!se)return V;const ue=V.responsive[se];return{...V,props:{...V.props,...ue.props},children:ue.children??V.children,text:ue.text??V.text,if:ue.if??V.if,iteration:ue.iteration??V.iteration,__responsiveChildrenKey:ue.children!==void 0?se:void 0}},[s,ge]),ce=N.useCallback(V=>{const se=V?.__setStateId;if(O(ue=>{const ee=window.__g7PendingLocalState,he=window.__g7SetLocalOverrideKeys||ee,Ee=he?_r(ue,he):ue;if(typeof V=="function"){const Re=V(Ee);return _r(Ee,Re)}const{__mergeMode:Ae,__setStateId:Fe,...ke}=V;let Ne;if(Ae==="replace"?Ne=ke:Ae==="shallow"?Ne={...Ee,...ke}:Ne=_r(Ee,ke),Ae!==void 0){const Re=window.__g7ForcedLocalFields;Re&&(window.__g7ForcedLocalFields={...Re,...ke})}return Ne}),se){const ue=ta();if(ue?.isEnabled()){const ee=T.id||"unknown",ve=T.name||"Unknown";setTimeout(()=>{ue.trackComponentRender?.(ee,ve,0,["_local"],[]),ue.completeStateChange?.(se)},0)}}},[T.id,T.name]),de=d?.state??k,ye=d?.setState??ce,ie=N.useMemo(()=>{const V=e._localInit&&e._localInit!==pe.current;let se;V&&e._local?se=e._local:se=e._local?_r(e._local,de):de;const ue=window.__g7ForcedLocalFields;ue&&(se=_r(se,ue));const ee={...e,_local:se},ve=e._computedDefinitions;if(ve&&Object.keys(ve).length>0){const he=new Tn,Ee={...ee,...qe?{_isolated:qe.state}:{}},Ae={},Fe=ta(),ke=Fe?.isEnabled()??!1;for(const[Ne,Re]of Object.entries(ve)){const et=ke?Date.now():0,He=ke?e._computed?.[Ne]:void 0;let Ge;try{if(typeof Re=="string")if(Re.startsWith("{{")&&Re.endsWith("}}")){const Ce=Re.slice(2,-2).trim(),je=qn(Ce)?he.evaluatePipeExpression(Ce,Ee,{skipCache:!0}):he.evaluateExpression(Ce,Ee,{skipCache:!0});Ae[Ne]=je}else Ae[Ne]=Re;else if(Re&&typeof Re=="object"&&"$switch"in Re){const Ce=he.resolveSwitch(Re,Ee,{skipCache:!0});Ae[Ne]=Ce}}catch(Ce){it.warn(`Failed to recalculate computed value: ${Ne}`,Ce),Ae[Ne]=e._computed?.[Ne],Ge=Ce instanceof Error?Ce.message:String(Ce)}if(ke&&(Ge||He!==Ae[Ne])){const je=Date.now()-et;Fe.trackComputedProperty?.(Ne,typeof Re=="string"?Re:JSON.stringify(Re),[{type:"_local",path:"_local",value:ee._local}],Ae[Ne],je,T.id,Ge),He!==Ae[Ne]&&Fe.trackComputedRecalc?.(Ne,"dependency-change",He,Ae[Ne],je,{type:"_local",path:"_local"},T.id)}}ee._computed=Ae}if(qe&&(ee._isolated=qe.state),J&&(ee.$parent=J),s.extensionPointProps){const he=i.resolveObject(s.extensionPointProps,ee,b?{skipCache:!0}:void 0);ee.extensionPointProps=S&&l?Af(he,l,n,ee):he}return s.extensionPointCallbacks&&(ee.extensionPointCallbacks=s.extensionPointCallbacks),S&&(ee.$templateId===void 0&&n?.templateId&&(ee.$templateId=n.templateId),ee.$locale===void 0&&n?.locale&&(ee.$locale=n.locale)),ee},[e,de,qe?.state,J,wt?.version,s.extensionPointProps,s.extensionPointCallbacks]);we.current=ie._local,Ue.current=ie._computed||{},Ve.current=J,N.useEffect(()=>{const V=ta();if(!V?.isEnabled())return;const se=ie._local;se&&V.updateLocalState(se);const ue=ie._computed;ue&&V.updateComputedState(ue),J&&V.updateParentContext?.(J);const ee=T.id||`comp-${Date.now()}`,ve=T.name||"Unknown",he={global:Object.keys(e._global||{}),local:Object.keys(se||{}),context:d?Object.keys(d.state||{}):[]},Ee={type:d?"componentContext":"dynamicState",hasComponentContext:!!d,parentId:d?"parent":void 0};V.trackComponentStateSource?.(ee,ve,he,Ee),de&&Object.keys(de).length>0&&V.trackDynamicState?.(ee,de);const Ae=!!(T.expandChildren||T.props?.expandChildren);V.trackContextFlow?.(ee,ve,!!d,Ae,!0)},[ie._local,e,T.id,T.name,d,de,T.expandChildren,T.props?.expandChildren,J]);const xe=N.useMemo(()=>({state:{...ie._local},setState:ye,stateRef:we,computedRef:Ue,isolatedContext:qe,parentDataContext:ie}),[ie._local,ye,k,qe,ie]),_e=N.useMemo(()=>{const V=as({if:T.if,condition:T.condition,conditions:T.conditions},ie,i,T.id);if(T.if||T.condition!==void 0||T.conditions){const ue=ta();ue?.isEnabled()&&T.if&&ue.trackIfCondition(T.id||`if-${Date.now()}`,T.if,V)}return V},[T.if,T.conditions,ie,i,T.id]),Te=N.useMemo(()=>{if(!T.slot)return null;const V=T.slot;if(V.startsWith("{{")&&V.endsWith("}}")){const se=V.slice(2,-2).trim();try{const ue={componentId:T.id,componentName:T.name,propName:"slot"},ee=qn(se)?i.evaluatePipeExpression(se,ie,void 0,ue):i.evaluateExpression(se,ie,ue);return it.log(`[Slot] 표현식 평가: "${se}" => "${ee}" (컴포넌트: ${T.id})`),typeof ee=="string"?ee:null}catch(ue){return it.warn(`slot 표현식 평가 실패 (컴포넌트: ${T.id}):`,ue),null}}return it.log(`[Slot] 정적 슬롯 ID: "${V}" (컴포넌트: ${T.id})`),V},[T.slot,T.id,ie,i]),$e=N.useMemo(()=>!Te||!T.id?"":`${T.id}-${Te}-${JSON.stringify(T.slotOrder??0)}`,[Te,T.id,T.slotOrder]),pt=N.useRef(null);N.useEffect(()=>{const V=window.__slotContextValue,se=V?.isEnabled??!1;if(T.slot&&it.log(`[Slot Registration] 컴포넌트: ${T.id}, isSlotEnabled: ${se}, shouldRender: ${_e}, resolvedSlotId: ${Te}`),!se||!_e){T.slot&&it.log(`[Slot Registration] 등록 스킵 - isSlotEnabled: ${se}, shouldRender: ${_e}`),pt.current&&T.id&&V&&(V.unregisterFromSlot(pt.current,T.id),pt.current=null);return}if(!Te||!T.id){T.slot&&it.log(`[Slot Registration] 등록 스킵 - resolvedSlotId: ${Te}, id: ${T.id}`),pt.current&&T.id&&V&&(V.unregisterFromSlot(pt.current,T.id),pt.current=null);return}pt.current&&pt.current!==Te&&V.unregisterFromSlot(pt.current,T.id);const ue={...T,slot:void 0,slotOrder:void 0};return V.registerToSlot(Te,T.id,{componentDef:ue,dataContext:e,order:T.slotOrder??0,parentFormContext:f??null,getParentComponentContext:()=>xe,translationContext:n,registrationKey:$e}),pt.current=Te,()=>{const ee=window.__slotContextValue;Te&&T.id&&ee&&ee.unregisterFromSlot(Te,T.id)}},[_e,Te,T,e,f,xe,n,$e]);const Ct=N.useCallback(V=>{if(typeof V=="string"){if(Hl(V))return vd(V);if(zl(V)){const se=[],ue=V.replace(new RegExp(`${ao}([^${pi}]*)${pi}`,"g"),(ve,he)=>(se.push(he),`${ka}${se.length-1}${ka}`));let ee=ue;return/\$t:[a-zA-Z0-9._-]+/.test(ue)&&(ee=l.resolveTranslations(ue,n,ie)),ee.replace(new RegExp(`${ka}(\\d+)${ka}`,"g"),(ve,he)=>se[parseInt(he)])}if(V.startsWith("$t:defer:"))return V;if(/\$t:[a-zA-Z0-9._-]+/.test(V)){const se=V.trim();return se.startsWith("{")&&se.endsWith("}")||se.startsWith("[")&&se.endsWith("]")?V:l.resolveTranslations(V,n,ie)}return V}if(Array.isArray(V))return V.map(se=>Ct(se));if(V&&typeof V=="object"){const se={};for(const[ue,ee]of Object.entries(V))se[ue]=Ct(ee);return se}return V},[l,n,ie]),Dt=N.useCallback(V=>ro(V),[]),ht=N.useCallback(V=>Ta(V),[]),rn=N.useMemo(()=>{let V={...T.props||{}};const se=["cellChildren","expandChildren","expandContext","render"],ee=a.getMetadata(T.name)?.skipBindingKeys||[],ve=[...new Set([...se,...ee])],he=b?{skipCache:!0}:void 0,Ee={...he??{},skipBindingKeys:ve},Ae=Ce=>{if(he)return he;if(Ce.startsWith("_computed")||Ce.startsWith("$computed"))return{skipCache:!0}};for(const[Ce,je]of Object.entries(V))if(!ve.includes(Ce))if(typeof je=="string"){const tt=ht(je);if(tt!==null){let At=!1,Mt=tt;tt.startsWith(tr)&&(At=!0,Mt=tt.slice(tr.length));const Dn={componentId:T.id,componentName:T.name,propName:Ce};let jt;if(qn(Mt))try{jt=i.evaluatePipeExpression(Mt,ie,Ae(Mt),Dn)}catch(Ht){it.warn(`파이프 표현식 평가 실패 (컴포넌트: ${T.id}, prop: ${Ce}):`,Ht),jt=void 0}else if(Dt(Mt))try{jt=i.evaluateExpression(Mt,ie,Dn)}catch(Ht){it.warn(`표현식 평가 실패 (컴포넌트: ${T.id}, prop: ${Ce}):`,Ht),jt=void 0}else jt=i.resolve(Mt,ie,Ae(Mt),Dn);V[Ce]=At&&jt!=null?rs(jt):jt}else{const At={componentId:T.id,componentName:T.name,propName:Ce};V[Ce]=i.resolveBindings(je,ie,he,At)}}else Array.isArray(je)?V[Ce]=je.map(tt=>typeof tt=="string"?i.resolveBindings(tt,ie,he):typeof tt=="object"&&tt!==null?i.resolveObject(tt,ie,Ee):tt):je&&typeof je=="object"&&(V[Ce]=i.resolveObject(je,ie,Ee));for(const[Ce,je]of Object.entries(V))V[Ce]=Ct(je);if(typeof V.style=="string"&&(V.style=aA(V.style)),V=Ld(V,S?void 0:T.actions,ie,{componentContext:xe,actionDispatcher:c}),T.classMap){const Ce=Kw(T.classMap,ie,i,{skipCache:!0});if(Ce){const je=V.className||"";V.className=je?`${je} ${Ce}`:Ce}}T.style&&(V.style=T.style),T.actions&&T.actions.length>0&&T.type==="composite"&&Object.assign(V,de),T.component_layout&&(V.__componentLayoutDefs=T.component_layout);const Fe=T.expandChildren||T.props?.expandChildren,ke=T.props?.cellChildren,Ne=T.children&&T.children.length>0,Re=T.component_layout,et=T.slot,He=T.props?.modalId,Ge=T.type==="composite";return(Fe||ke||Ne||Re||et||He||Ge)&&(V.__componentContext=xe),V},[T.props,T.actions,T.style,T.classMap,T.id,ie,n,i,l,xe,de,Ct,Dt,ht,b]),Kt=N.useRef(null),It=N.useMemo(()=>Kt.current&&Hm(Kt.current,rn)?Kt.current:(Kt.current=rn,rn),[rn]),qr=N.useMemo(()=>{const V=T.id;if(typeof V!="string"||!V.includes("{{"))return V;const se=i.resolveBindings(V,ie,{skipCache:!0});return typeof se=="string"?se:V},[T.id,ie,i]),hn=N.useMemo(()=>{const V=T.dataKey,se=T.trackChanges,ue=T.debounce;if(V){const ee=V.startsWith("_global."),ve=ee?V.slice(8):V,he=ee?e._global:ie._local;return{dataKey:ve,trackChanges:se??!1,debounce:ue,setState:ee?Ae=>{e._globalSetState?e._globalSetState(Ae):it.warn("_global.formData 사용 시 _globalSetState가 필요합니다.")}:ye,state:he??{},_isGlobal:ee}}if(Z.dataKey)return Z},[T.dataKey,T.trackChanges,T.debounce,T.id,ye,ie._local,e._global,e._globalSetState,Z]),an=N.useRef(new Set);N.useEffect(()=>{an.current.clear()},[e._global?._remountKeys]);const pn=N.useCallback((V,se)=>{const ue=e._global?._remountKeys;return V&&ue?.[V]&&!an.current.has(V)?(an.current.add(V),`${V}-remount-${ue[V]}`):V||se},[e._global?._remountKeys]),Kn=N.useMemo(()=>{if(T.text!==void 0){if(typeof T.text=="string"){const V=T.text;let se=V;const ue={componentId:T.id,componentName:T.name,propName:"text",skipCache:!0},ee=ht(V);if(ee!==null){let ve=!1,he=ee;if(ee.startsWith(tr)&&(ve=!0,he=ee.slice(tr.length)),qn(he))try{se=i.evaluatePipeExpression(he,ie,{skipCache:!0},ue)}catch(Ee){it.warn(`text 파이프 표현식 평가 실패 (컴포넌트: ${T.id}):`,Ee),se=""}else if(Dt(he))try{se=i.evaluateExpression(he,ie,ue)}catch(Ee){it.warn(`text 표현식 평가 실패 (컴포넌트: ${T.id}):`,Ee),se=""}else se=i.resolve(he,ie,{skipCache:!0},ue);ve&&se!=null&&(se=rs(se))}else V.includes("{{")&&(se=i.resolveBindings(V,ie,{skipCache:!0},ue));return se=Ct(se),se??""}return T.text}return!T.children||T.children.length===0?null:T.children.map((V,se)=>{if(typeof V=="string")return V;const ue=T.__responsiveChildrenKey,ee=ue?`responsive.${ue}.children`:"children",ve=L?`${L}.${ee}.${se}`:`${se}`,he=V._fromBase?V.id||`child-${se}`:pn(V.id,`child-${se}`),Ee=!V._fromBase&&M?`${he}_${M}`:he;return ft.jsx(Pr,{componentDef:V,dataContext:ie,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:xe,parentFormContextProp:hn,isInsideIteration:b,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:ve,onDragStart:A,onDragEnd:x,layoutKey:M},Ee)})},[T.text,T.children,T.id,e,ie,n,a,i,l,c,xe,hn,ht,Dt,Ct,pn,b,S,v,_,L,A,x,M]),Gr=N.useMemo(()=>{if(!T.iteration)return null;try{const V=Od(T.iteration.source,ie,i);if(!Array.isArray(V))return it.warn(`iteration source가 배열이 아닙니다 (컴포넌트: ${T.id}):`,V),null;const se=ta();return se?.isEnabled()&&se.trackIteration(T.id||`iteration-${Date.now()}`,T.iteration.source,T.iteration.item_var,T.iteration.index_var,V.length),V.map((ue,ee)=>{const ve={...ie,[T.iteration.item_var]:ue,[`${T.iteration.item_var}_index`]:ee};T.iteration.index_var&&(ve[T.iteration.index_var]=ee);const he=ee===0||ee===V.length-1;se?.isEnabled()&&he&&se.trackNestedContext?.({componentId:`${T.id||"unknown"}-iter-${ee}`,componentType:"iteration",parentContext:{available:Object.keys(ie),values:{}},ownContext:{added:[T.iteration.item_var,...T.iteration.index_var?[T.iteration.index_var]:[]],values:{[T.iteration.item_var]:ue,...T.iteration.index_var?{[T.iteration.index_var]:ee}:{}}},depth:1});const Ee={...T,iteration:void 0,responsive:void 0},Ae=L?`${L}.iteration.${ee}`:`iteration.${ee}`,Fe=`${pn(T.id,"item")}-${ee}`;return ft.jsx(Pr,{componentDef:Ee,dataContext:ve,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:xe,parentFormContextProp:hn,isInsideIteration:!0,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:Ae,onDragStart:A,onDragEnd:x,layoutKey:M},Fe)})}catch(V){return it.error(`iteration 렌더링 실패 (컴포넌트: ${T.id}):`,V),null}},[T.iteration,T.children,T.id,e,ie,n,a,i,l,c,xe,hn,pn,S,v,_,L,A,x,M]),[qt,_i]=N.useState(0),aa=N.useMemo(()=>{if(!T.sortable||!T.itemTemplate)return null;const{sortable:V,itemTemplate:se}=T;try{const ue=Od(V.source,ie,i);if(!Array.isArray(ue))return it.warn(`sortable source가 배열이 아닙니다 (컴포넌트: ${T.id}):`,ue),null;if(ue.length===0)return null;const ee=V.itemKey||"id",ve=V.strategy||"verticalList",he=V.itemVar||"$item",Ee=V.indexVar||"$index",Ae=V.handle,Fe=V.wrapperElement,ke=(et,He)=>{const Ge=T.actions?.filter(Ce=>Ce.event==="onSortEnd"||Ce.type==="onSortEnd");if(_i(Ce=>Ce+1),Ge&&Ge.length>0&&c){const Ce={...ie,$sortedItems:et,$sortEvent:He};Ge.forEach(je=>{try{const tt=c.createHandler(je,Ce,xe),At=new Event("sortend");tt(At)}catch(tt){it.error(`onSortEnd action failed: ${je.handler}`,tt)}})}},Ne=et=>{const He=T.actions?.filter(Ge=>Ge.event==="onSortStart"||Ge.type==="onSortStart");if(He&&He.length>0&&c){const Ge={...ie,$activeId:et.activeId};He.forEach(Ce=>{try{const je=c.createHandler(Ce,Ge,xe),tt=new Event("sortstart");je(tt)}catch(je){it.error(`onSortStart action failed: ${Ce.handler}`,je)}})}},Re=ue.map((et,He)=>{const Ge=String(et[ee]),Ce={...ie,[he]:et,[Ee]:He},je=L?`${L}.sortable.${He}`:`sortable.${He}`,tt=`${pn(T.id,"sortable-item")}-v${qt}-${Ge}`;return ft.jsx(nA,{id:Ge,handle:Ae,...Fe?{as:Fe}:{},children:(Fe&&se.children?se.children:[se]).map((At,Mt)=>ft.jsx(Pr,{componentDef:At,dataContext:Ce,translationContext:n,registry:a,bindingEngine:i,translationEngine:l,actionDispatcher:c,parentComponentContext:xe,parentFormContextProp:void 0,isInsideIteration:!0,isEditMode:S,onComponentSelect:v,onComponentHover:_,componentPath:je,onDragStart:A,onDragEnd:x,layoutKey:M},`${tt}-child-${Mt}`))},tt)});return ft.jsx(Z_,{items:ue,itemKey:ee,strategy:ve,onSortEnd:ke,onSortStart:Ne,sortVersion:qt,children:Re})}catch(ue){return it.error(`sortable 렌더링 실패 (컴포넌트: ${T.id}):`,ue),null}},[T.sortable,T.itemTemplate,T.actions,T.id,e,ie,n,a,i,l,c,xe,pn,S,v,_,L,A,x,qt,M]),gn=N.useRef(!1);N.useEffect(()=>{if(gn.current)return;gn.current=!0;const V=ta();if(V?.isEnabled()&&V.trackMount(s.id,{name:s.name,type:s.type||"component",props:It}),s.lifecycle?.onMount&&s.lifecycle.onMount.length>0)for(const ue of s.lifecycle.onMount)try{const ee=c.createHandler(ue,ie,xe),ve=new Event("mount");ee(ve),it.log(`Lifecycle onMount executed: ${ue.handler} (Component: ${s.id})`)}catch(ee){it.error(`Lifecycle onMount failed: ${ue.handler} (Component: ${s.id})`,ee)}const se=[];if(s.onComponentEvent&&s.onComponentEvent.length>0){const ue=window.G7Core;if(ue?.componentEvent?.on)for(const ee of s.onComponentEvent){if(!ee.event){it.warn(`onComponentEvent: event name is required (Component: ${s.id})`);continue}const ve=async Ee=>{try{const Ae={...ie,_eventData:Ee},ke=await c.createHandler({handler:ee.handler,params:ee.params,onSuccess:ee.onSuccess,onError:ee.onError},Ae,xe)(new CustomEvent(ee.event,{detail:Ee}));return it.log(`onComponentEvent "${ee.event}" handled: ${ee.handler} (Component: ${s.id})`,{eventData:Ee,result:ke}),ke}catch(Ae){throw it.error(`onComponentEvent "${ee.event}" failed: ${ee.handler} (Component: ${s.id})`,Ae),Ae}},he=ue.componentEvent.on(ee.event,ve);se.push(he),it.log(`onComponentEvent: Subscribed to "${ee.event}" (Component: ${s.id})`)}else it.warn(`onComponentEvent: G7Core.componentEvent is not available (Component: ${s.id})`)}return()=>{const ue=ta();ue?.isEnabled()&&ue.trackUnmount(s.id);for(const ee of se)try{ee()}catch(ve){it.error(`onComponentEvent: Failed to unsubscribe (Component: ${s.id})`,ve)}if(se.length>0&&it.log(`onComponentEvent: Unsubscribed ${se.length} event(s) (Component: ${s.id})`),s.lifecycle?.onUnmount&&s.lifecycle.onUnmount.length>0)for(const ee of s.lifecycle.onUnmount)try{const ve=c.createHandler(ee,ie,xe),he=new Event("unmount");ve(he),it.log(`Lifecycle onUnmount executed: ${ee.handler} (Component: ${s.id})`)}catch(ve){it.error(`Lifecycle onUnmount failed: ${ee.handler} (Component: ${s.id})`,ve)}}},[]);const Rn=N.useMemo(()=>{const V=It.dataKey,se=It.trackChanges,ue=It.debounce;return V?{dataKey:V,trackChanges:se??!1,debounce:ue,setState:ye,state:de}:null},[It.dataKey,It.trackChanges,It.debounce,ye,de]);N.useEffect(()=>{const V=T.dataKey||It.dataKey;if(!V)return;const se=ta();if(!se?.isEnabled())return;const ue=[],ee=he=>{if(he)for(const Ee of he){const Ae=Ee?.props?.name;if(Ae&&ue.push({name:Ae,type:Ee.name||Ee.type||"unknown"}),Ee?.children&&ee(Ee.children),Ee?.slots)for(const Fe of Object.values(Ee.slots))Array.isArray(Fe)&&ee(Fe)}};if(s.children&&ee(s.children),s.slots)for(const he of Object.values(s.slots))Array.isArray(he)&&ee(he);const ve=s.id||`form-${V}`;return se.trackForm(ve,V,ue),()=>{se.untrackForm(ve)}},[T,It.dataKey,s.id,s.children]);const nt=N.useRef(null),Nn=N.useRef(void 0);N.useEffect(()=>{const V=It?.name;if(!V||!Z.dataKey||!Z.setState||It?.autoBinding===!1)return;const se=`${Z.dataKey}.${V}`,ue=window.__g7AutoBindingPaths??new Map;return window.__g7AutoBindingPaths=ue,ue.set(se,(ue.get(se)??0)+1),()=>{const ee=ue.get(se)??0;ee<=1?ue.delete(se):ue.set(se,ee-1)}},[It?.name,It?.autoBinding,Z.dataKey,Z.setState]);const mn=N.useMemo(()=>{const{dataKey:V,trackChanges:se,debounce:ue,...ee}=It,ve=ee.name;if(!ve||!Z.dataKey||!Z.setState)return ee;if(ee.autoBinding===!1){const{autoBinding:Ge,...Ce}=ee;return Ce}const he=`${Z.dataKey}.${ve}`,Ee=eE(Z.state,he),Ae=Ee!==void 0?Ee:ee.value,ke=a.getMetadata(s.name)?.bindingType,Ne=typeof Ae=="boolean"&&(ke==="checked"||ke==="checkable"&&["checkbox","radio"].includes(ee?.type)),Re=Z.debounce,et=Ge=>{const Ce=Z.state||{},je=tE(Ce,he,Ge);if(Z.trackChanges&&(je.hasChanges=!0),window.__g7PendingLocalState=je,Z.setState(je),Z._isGlobal)return;const tt=window.G7Core,At={[he]:Ge};Z.trackChanges&&(At.hasChanges=!0),tt?.state?.setLocal?.(At,{render:!1})},He=(Ge,Ce)=>{Re&&Re>0?(nt.current&&clearTimeout(nt.current),nt.current=setTimeout(()=>{et(Ge),nt.current=null,Ce&&(Ce(),window.__g7PendingLocalState=null)},Re)):(et(Ge),Ce&&(Ce(),window.__g7PendingLocalState=null))};if(Ne)return{...ee,checked:Ae,onChange:Ce=>{const je=Ce.target.checked;et(je),ee.onChange&&ee.onChange(Ce)}};{const Ge=je=>{const tt=je?.target?.value!==void 0?je.target.value:je;Re&&Re>0&&(Nn.current=tt);const At=je?.target?{target:{value:tt,name:je.target.name,type:je.target.type,checked:je.target.checked},currentTarget:je.currentTarget?{value:tt,name:je.currentTarget.name}:void 0}:je;He(tt,()=>{ee.onChange&&ee.onChange(At)})};let Ce;return Nn.current!==void 0?nt.current!==null?Ce=Nn.current:String(Ae??"")===String(Nn.current)?(Nn.current=void 0,Ce=Ae??""):Ce=Nn.current:Ce=Ae??"",{...ee,value:Ce,onChange:Ge}}},[It,Z,Z.state,Z.debounce]),ia=N.useMemo(()=>{const V=ta();if(V?.isEnabled()&&V.trackRender(s.name),!_e)return T.iteration?Gr:null;if(T.sortable&&T.itemTemplate)return aa;if(T.iteration)return Gr;if(T.type==="extension_point"){const{layout:ke,columns:Ne,gap:Re,className:et,...He}=T.props||{};let Ge=et||"";return ke==="grid"&&Ne?Ge=`${Ge} grid grid-cols-${Ne} gap-${Re||4}`.trim():ke==="flex"&&(Ge=`${Ge} flex flex-col gap-${Re||4}`.trim()),ft.jsx("div",{id:qr,className:Ge||void 0,...He,children:Kn})}const se=a.getComponent(T.name);if(!se)return it.error(`컴포넌트를 찾을 수 없습니다: ${T.name} (ID: ${T.id})`,"componentDef:",JSON.stringify(T,null,2)),null;const ue=e._global?._remountKeys,ee={...mn,id:mn.id??qr,...ue&&Object.keys(ue).length>0&&{__remountTrigger:JSON.stringify(ue)}};if(H?.listeners&&H.handle&&mn["data-drag-handle"]!==void 0&&(Object.assign(ee,H.listeners),ee.style={...ee.style,cursor:H.isDragging?"grabbing":"grab"}),S){const ke=T.id||`auto_${T.name||T.type||"unknown"}_${Math.random().toString(36).substring(2,8)}`,Ne=He=>{typeof He.preventDefault=="function"&&He.preventDefault(),v&&(He.stopPropagation(),v(ke,He))},Re={"data-editor-id":ke,"data-editor-name":T.name,"data-editor-type":T.type,onClick:Ne};L&&(Re["data-editor-path"]=L);const et=T.__editorSlotName??T.slot;typeof et=="string"&&(Re["data-editor-slot"]=et),_&&(Re.onMouseMove=He=>{He.stopPropagation(),_(ke,He)},Re.onMouseLeave=He=>{_(null,He)}),Object.assign(ee,Re),ee.editorAttrs=Re}const ve=T.type==="basic",he=T.type==="layout",Ae=ve||he?(()=>{const ke={};for(const[Ne,Re]of Object.entries(ee))Ne.startsWith("__")||Ne==="editorAttrs"&&ve||(ke[Ne]=Re);return ke})():ee;let Fe=ft.jsx(se,{...Ae,children:Kn});if(hn&&(Fe=ft.jsx(QC,{value:hn,children:Fe})),T.isolatedState){const ke=typeof T.isolatedState=="object"?T.isolatedState:{},Ne=e?._isolatedInit,Re=Ne?{...ke,...Ne}:ke;Fe=ft.jsx(oE,{initialState:Re,scopeId:T.isolatedScopeId,children:Fe})}return Fe},[_e,T.iteration,T.sortable,T.itemTemplate,T.type,T.name,T.id,T.props,T.isolatedState,T.isolatedScopeId,a,mn,Kn,Gr,aa,H,Rn,S,v,_,A,x,L,e._global?._remountKeys]),or=N.useMemo(()=>{const V=T.blur_until_loaded;if(!V||e?._global?.__isPreview)return!1;if(typeof V=="string"){const ee=ht(V);if(ee!==null)try{const ve={componentId:T.id,componentName:T.name,propName:"blur_until_loaded"};return!!i.evaluateExpression(ee,ie,ve)}catch(ve){return it.warn(`blur_until_loaded 표현식 평가 실패 (컴포넌트: ${T.id}):`,ve),!1}return!1}if(typeof V=="object"&&V!==null){if(!V.enabled)return!1;if(V.data_sources)return(Array.isArray(V.data_sources)?V.data_sources:[V.data_sources]).some(Ee=>e[Ee]===void 0);if(fe)return!0;const ee=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(he=>!ee.includes(he)&&!he.startsWith("_")).some(he=>e[he]===void 0)}if(fe)return!0;const se=["route","query","_global","_local","_dataSourceErrors"];return Object.keys(e).filter(ee=>!se.includes(ee)&&!ee.startsWith("_")).some(ee=>e[ee]===void 0)},[T.blur_until_loaded,T.id,fe,e,ie,i,ht]);if(or){const V=T.blur_until_loaded;if(typeof V=="object"&&V?.data_sources){const ue=(Array.isArray(V.data_sources)?V.data_sources:[V.data_sources]).reduce((ve,he)=>(ve[he]=e[he]===void 0?"UNDEFINED":"LOADED",ve),{}),ee=Object.keys(e).filter(ve=>!ve.startsWith("_"));it.log(`[BLUR APPLIED] Component: ${T.id}, data_sources status:`,ue,"Available keys:",ee)}else it.log(`[BLUR APPLIED] Component: ${T.id}, blur_until_loaded:`,V)}const sn=N.useMemo(()=>{if(!T.blur_until_loaded)return{wrapperClasses:"",innerClasses:""};const V=rn?.className||"";if(!V)return{wrapperClasses:"",innerClasses:""};const se=[/^(sm:|md:|lg:|xl:|2xl:)?(col-span-|row-span-|col-start-|col-end-|row-start-|row-end-)/,/^(sm:|md:|lg:|xl:|2xl:)?(self-|place-self-|order-)/,/^(sm:|md:|lg:|xl:|2xl:)?(flex-shrink|flex-grow|flex-\d|basis-)/],ue=V.split(/\s+/).filter(Boolean),ee=[],ve=[];for(const he of ue)se.some(Ae=>Ae.test(he))?ee.push(he):ve.push(he);return{wrapperClasses:ee.join(" "),innerClasses:ve.join(" ")}},[T.blur_until_loaded,rn?.className]),_n=window.__slotContextValue;return Te&&_n?.isEnabled?null:ft.jsx(iA,{componentId:s.id,componentName:s.name,children:T.blur_until_loaded?ft.jsx("div",{className:[sn.wrapperClasses,or?"opacity-50 blur-sm transition-all duration-300 pointer-events-none":void 0].filter(Boolean).join(" ")||void 0,children:ia}):ia})});Pr.displayName="DynamicRenderer",typeof window<"u"&&(window.__DynamicRenderer=Pr);function sA(s){const e={};try{const a=new URL(s,typeof window<"u"?window.location.origin:"http://localhost").searchParams,i=new Set;for(const l of a.keys()){if(i.has(l))continue;i.add(l);const c=a.getAll(l);c.length>1||l.endsWith("[]")?e[l]=c:e[l]=c[0]}}catch{}return e}function oA(s){try{return new URL(s,typeof window<"u"?window.location.origin:"http://localhost").pathname}catch{return s.split("?")[0]}}const Ei=class Ei{constructor(){$(this,"config",{enabled:!1,maxHistorySize:100,logLevel:"info",serverEndpoint:"/_boost/g7-debug/dump-state",autoCapture:!0});$(this,"stateHistory",[]);$(this,"snapshotIdCounter",0);$(this,"stateWatchers",new Map);$(this,"currentLocalState",{});$(this,"currentComputedState",{});$(this,"currentParentContext");$(this,"actionHistory",[]);$(this,"actionWatchers",new Set);$(this,"cacheStats",{hits:0,misses:0,entries:0});$(this,"cacheDecisions",[]);$(this,"cacheDecisionIdCounter",0);$(this,"maxCacheDecisions",200);$(this,"currentRenderCycleId",null);$(this,"isInActionExecution",!1);$(this,"isInIteration",!1);$(this,"mountedComponents",new Map);$(this,"eventListeners",new Map);$(this,"mountWatchers",new Set);$(this,"unmountWatchers",new Set);$(this,"renderCounts",new Map);$(this,"bindingEvalCount",0);$(this,"profilingData",[]);$(this,"isProfiling",!1);$(this,"profilingStartTime",0);$(this,"activeRequests",new Map);$(this,"requestHistory",[]);$(this,"pendingDataSources",new Set);$(this,"requestWatchers",new Set);$(this,"wsConnections",new Map);$(this,"wsMessageHistory",[]);$(this,"wsMessageWatchers",new Set);$(this,"ifConditions",new Map);$(this,"iterations",new Map);$(this,"conditionWatchers",new Set);$(this,"forms",new Map);$(this,"formWatchers",new Set);$(this,"expressionHistory",[]);$(this,"expressionWatchers",new Set);$(this,"expressionIdCounter",0);$(this,"dataSources",new Map);$(this,"dataPathTransforms",[]);$(this,"maxDataPathTransforms",100);$(this,"nestedContexts",[]);$(this,"nestedContextIdCounter",0);$(this,"maxNestedContexts",100);$(this,"formBindingIssues",[]);$(this,"formBindingValidations",[]);$(this,"formBindingIssueIdCounter",0);$(this,"maxFormBindingIssues",100);$(this,"computedProperties",new Map);$(this,"computedRecalcLogs",[]);$(this,"computedRecalcIdCounter",0);$(this,"maxComputedRecalcLogs",100);$(this,"handlers",new Map);$(this,"componentEventSubscriptions",new Map);$(this,"componentEventEmitHistory",[]);$(this,"componentEventIdCounter",0);$(this,"maxComponentEventHistory",100);$(this,"stateRenderingLogs",[]);$(this,"currentStateChangeContext",null);$(this,"stateRenderingIdCounter",0);$(this,"componentRenderCounts",new Map);$(this,"stateToComponentMap",new Map);$(this,"maxStateRenderingLogs",100);$(this,"componentStateSources",new Map);$(this,"contextFlowNodes",new Map);$(this,"dynamicStates",new Map);$(this,"styleIssues",[]);$(this,"componentStyles",new Map);$(this,"authEvents",[]);$(this,"authHeaderHistory",[]);$(this,"authEventIdCounter",0);$(this,"maxAuthEventHistory",50);$(this,"logHistory",[]);$(this,"logIdCounter",0);$(this,"maxLogHistory",500);$(this,"currentLayout",null);$(this,"layoutHistory",[]);$(this,"layoutIdCounter",0);$(this,"layoutStats",{totalLoads:0,cacheHits:0,apiLoads:0});$(this,"maxLayoutHistory",50);$(this,"executionDetails",new Map);$(this,"stateChangeHistory",[]);$(this,"dataSourceChangeHistory",[]);$(this,"changeAlerts",[]);$(this,"changeExpectations",new Map);$(this,"executionIdCounter",0);$(this,"stateChangeIdCounter",0);$(this,"alertIdCounter",0);$(this,"maxExecutionDetails",100);$(this,"maxStateChangeHistory",200);$(this,"maxChangeAlerts",100);$(this,"sequenceExecutions",[]);$(this,"sequenceExecutionStack",[]);$(this,"sequenceIdCounter",0);$(this,"maxSequenceExecutions",50);$(this,"staleClosureWarnings",[]);$(this,"staleClosureIdCounter",0);$(this,"maxStaleClosureWarnings",100);$(this,"stateCaptureRegistry",new Map);$(this,"modalStates",new Map);$(this,"modalStateIssues",[]);$(this,"modalStateRelations",[]);$(this,"modalStateChangeLogs",[]);$(this,"modalStateIssueIdCounter",0);$(this,"modalStateChangeLogIdCounter",0);$(this,"maxModalStateIssues",100);$(this,"maxModalStateChangeLogs",200);$(this,"namedActionDefinitions",{});$(this,"namedActionRefLogs",[]);$(this,"namedActionRefIdCounter",0);$(this,"maxNamedActionRefLogs",200);$(this,"debounceActionHistory",[]);$(this,"dataSourceUpdateHistory",[]);$(this,"debounceActionIdCounter",0);$(this,"dataSourceUpdateIdCounter",0)}static getInstance(){return Ei.instance||(Ei.instance=new Ei),Ei.instance}initialize(){if(this.config.enabled=this.checkDebugMode(),!this.config.enabled){console.log("[G7DevTools] 비활성화됨 (환경설정 > 고급 설정 > 디버그 모드를 켜세요)");return}console.log("[G7DevTools] 활성화됨"),this.setupGlobalErrorHandler()}checkDebugMode(){try{if(window.G7Config?.debug===!0)return!0;const a=window.G7Core?.state?.get?.();return a?.settings?.advanced?.debug_mode===!0||a?._global?.settings?.advanced?.debug_mode===!0}catch{return!1}}isEnabled(){return this.config.enabled}setupGlobalErrorHandler(){window.addEventListener("error",e=>{this.config.enabled&&this.logAction({id:this.generateId(),type:"globalError",startTime:Date.now(),status:"error",error:{name:"Error",message:e.message,stack:e.error?.stack}})}),window.addEventListener("unhandledrejection",e=>{this.config.enabled&&this.logAction({id:this.generateId(),type:"unhandledRejection",startTime:Date.now(),status:"error",error:{name:"UnhandledRejection",message:String(e.reason)}})})}captureStateSnapshot(e){if(!this.config.enabled)return;const n={id:++this.snapshotIdCounter,timestamp:Date.now(),source:e.source,prev:this.sanitizeObject(e.prev),next:this.sanitizeObject(e.next),diff:e.diff};this.stateHistory.push(n),this.stateHistory.length>this.config.maxHistorySize&&this.stateHistory.shift(),this.notifyStateWatchers(n)}getState(){try{const e=window.G7Core,n=e?.state?.get?.()||{},a={};if(e?._isolatedStates&&typeof e._isolatedStates=="object")for(const[i,l]of Object.entries(e._isolatedStates))a[i]=this.sanitizeObject(l);return{_global:n._global||n,_local:this.currentLocalState,_computed:this.currentComputedState,_isolated:Object.keys(a).length>0?a:void 0,$parent:this.currentParentContext}}catch{return{_global:{},_local:this.currentLocalState,_computed:this.currentComputedState}}}updateLocalState(e){this.config.enabled&&(this.currentLocalState=this.sanitizeObject(e))}updateComputedState(e){this.config.enabled&&(this.currentComputedState=this.sanitizeObject(e))}updateParentContext(e){this.config.enabled&&(this.currentParentContext=e?this.sanitizeObject(e):void 0)}getStateHistory(){return[...this.stateHistory]}getDualStorageMismatch(){const e=this.currentLocalState||{};let n={};try{n=(window.G7Core?.state?.get?.()||{})._local||{}}catch{n={}}const a=this.findMismatchedLeafPaths(e,n);return{hasMismatch:a.length>0,mismatchedPaths:a,storageA:e,storageB:n}}findMismatchedLeafPaths(e,n,a=""){const i=[],l=new Set([...Object.keys(e||{}),...Object.keys(n||{})]);for(const c of l){const d=a?`${a}.${c}`:c,f=e?.[c],h=n?.[c],m=f&&typeof f=="object"&&!Array.isArray(f),b=h&&typeof h=="object"&&!Array.isArray(h);if(m&&b)i.push(...this.findMismatchedLeafPaths(f,h,d));else{const S=f===void 0?"__undefined__":JSON.stringify(f),v=h===void 0?"__undefined__":JSON.stringify(h);S!==v&&i.push(d)}}return i}watchState(e,n){return this.stateWatchers.has(e)||this.stateWatchers.set(e,new Set),this.stateWatchers.get(e).add(n),()=>{this.stateWatchers.get(e)?.delete(n)}}notifyStateWatchers(e){if(this.stateWatchers.get("*")?.forEach(n=>{try{n(e.next,e.prev,"*")}catch(a){console.error("[G7DevTools] State watcher error:",a)}}),e.diff)for(const n of e.diff.changed)this.stateWatchers.get(n.path)?.forEach(a=>{try{a(n.newValue,n.oldValue,n.path)}catch(i){console.error("[G7DevTools] State watcher error:",i)}})}diffSnapshots(e,n){const a=this.stateHistory.find(l=>l.id===e),i=this.stateHistory.find(l=>l.id===n);return!a||!i?null:this.calculateDiff(a.next,i.next)}calculateDiff(e,n,a=""){const i={added:[],removed:[],changed:[]},l=new Set(Object.keys(e)),c=new Set(Object.keys(n));for(const d of c){const f=a?`${a}.${d}`:d;l.has(d)?JSON.stringify(e[d])!==JSON.stringify(n[d])&&i.changed.push({path:f,oldValue:e[d],newValue:n[d]}):i.added.push(f)}for(const d of l){const f=a?`${a}.${d}`:d;c.has(d)||i.removed.push(f)}return i}logAction(e){if(!this.config.enabled)return;const n=this.actionHistory.findIndex(a=>a.id===e.id);n>=0?this.actionHistory[n]={...this.actionHistory[n],...e}:(this.actionHistory.push(e),this.actionHistory.length>this.config.maxHistorySize&&this.actionHistory.shift()),this.isProfiling&&this.profilingData.push({type:"action",action:e.type,timestamp:performance.now(),duration:e.duration}),this.notifyActionWatchers(e)}getActionHistory(){return[...this.actionHistory]}watchActions(e){return this.actionWatchers.add(e),()=>{this.actionWatchers.delete(e)}}notifyActionWatchers(e){this.actionWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] Action watcher error:",a)}})}getActionMetrics(){const e=this.actionHistory.filter(l=>l.status==="success"),n=this.actionHistory.filter(l=>l.status==="error"),a={};for(const l of this.actionHistory)a[l.type]=(a[l.type]||0)+1;const i=e.reduce((l,c)=>l+(c.duration||0),0);return{totalActions:this.actionHistory.length,successCount:e.length,errorCount:n.length,averageDuration:e.length>0?i/e.length:0,actionsByType:a}}recordCacheHit(){this.config.enabled&&this.cacheStats.hits++}recordCacheMiss(){this.config.enabled&&this.cacheStats.misses++}updateCacheEntries(e){this.config.enabled&&(this.cacheStats.entries=e)}getCacheStats(){const e=this.cacheStats.hits+this.cacheStats.misses;return{...this.cacheStats,hitRate:e>0?this.cacheStats.hits/e:0}}resetCacheStats(){this.cacheStats={hits:0,misses:0,entries:0}}trackCacheDecision(e){if(!this.config.enabled)return;const n={id:`cache-${++this.cacheDecisionIdCounter}`,timestamp:Date.now(),expression:e.expression,decision:e.decision,reason:e.reason,context:{isInIteration:this.isInIteration,isInAction:this.isInActionExecution,renderCycleId:this.currentRenderCycleId||void 0,componentId:e.componentId,skipCacheOption:e.skipCacheOption},cachedValue:e.cachedValue!==void 0?this.safeClone(e.cachedValue):void 0,freshValue:e.freshValue!==void 0?this.safeClone(e.freshValue):void 0,valueMatch:e.cachedValue!==void 0&&e.freshValue!==void 0?JSON.stringify(e.cachedValue)===JSON.stringify(e.freshValue):void 0,duration:e.duration};this.cacheDecisions.push(n),this.cacheDecisions.length>this.maxCacheDecisions&&this.cacheDecisions.shift(),e.decision==="cache_hit"?this.cacheStats.hits++:(e.decision==="cache_miss"||e.decision==="skip_cache")&&this.cacheStats.misses++}startRenderCycle(e){return this.currentRenderCycleId=`render-${Date.now()}-${e||"root"}`,this.currentRenderCycleId}endRenderCycle(){this.currentRenderCycleId=null}startActionContext(){this.isInActionExecution=!0}endActionContext(){this.isInActionExecution=!1}startIterationContext(){this.isInIteration=!0}endIterationContext(){this.isInIteration=!1}getCacheDecisionTrackingInfo(){const e=this.getCacheDecisionStats();return{decisions:[...this.cacheDecisions],stats:e,timestamp:Date.now()}}getCacheDecisionStats(){const e=this.cacheDecisions;let n=0,a=0,i=0,l=0;const c={},d={};for(const h of e)h.decision==="cache_hit"?n++:h.decision==="cache_miss"?a++:h.decision==="skip_cache"?i++:h.decision==="invalidate"&&l++,c[h.reason]=(c[h.reason]||0)+1,h.context.componentId&&(d[h.context.componentId]=(d[h.context.componentId]||0)+1);const f=n+a+i;return{totalDecisions:e.length,cacheHits:n,cacheMisses:a,skipCacheCount:i,invalidateCount:l,byReason:c,byComponent:d,avgHitRate:f>0?n/f:0}}getRecentCacheDecisions(e=20,n){let a=[...this.cacheDecisions];return n&&(a=a.filter(i=>i.decision===n)),a.slice(-e)}clearCacheDecisionData(){this.cacheDecisions=[],this.cacheDecisionIdCounter=0}summarizePropsForLifecycle(e){if(!e||typeof e!="object")return e;const n={},a=["children","ref","key"];for(const[i,l]of Object.entries(e))if(!a.includes(i))if(l==null)n[i]=l;else if(Array.isArray(l))n[i]=`[Array(${l.length})]`;else if(typeof l=="object"){const c=Object.keys(l);c.length<=3?n[i]=`{${c.join(", ")}}`:n[i]=`{${c.slice(0,3).join(", ")}, ... +${c.length-3}}`}else typeof l=="string"&&l.length>50?n[i]=l.substring(0,50)+"...":typeof l=="function"?n[i]="[Function]":n[i]=l;return n}trackMount(e,n){if(!this.config.enabled)return;const a=this.summarizePropsForLifecycle(n.props),i={...n,props:a,id:e,mountTime:Date.now()};this.mountedComponents.set(e,i),this.mountWatchers.forEach(l=>{try{l({componentId:e,componentName:n.name,timestamp:i.mountTime})}catch(c){console.error("[G7DevTools] Mount watcher error:",c)}})}trackUnmount(e){if(!this.config.enabled)return;const n=this.mountedComponents.get(e),a=this.eventListeners.get(e)||[];a.length>0&&console.warn(`[G7DevTools] 컴포넌트 ${e} 언마운트 시 ${a.length}개 리스너 미정리`),n&&this.unmountWatchers.forEach(i=>{try{i({componentId:e,componentName:n.name,timestamp:Date.now(),orphanedListeners:a.length})}catch(l){console.error("[G7DevTools] Unmount watcher error:",l)}}),this.mountedComponents.delete(e)}trackListener(e,n,a){if(!this.config.enabled)return;const i=this.eventListeners.get(e)||[];i.push({type:n,target:a,addedAt:Date.now()}),this.eventListeners.set(e,i)}removeListener(e,n){if(!this.config.enabled)return;const a=this.eventListeners.get(e)||[],i=a.findIndex(l=>l.type===n);i>=0&&a.splice(i,1)}getMountedComponents(){return Array.from(this.mountedComponents.values())}getOrphanedListeners(){const e=[];for(const[n,a]of this.eventListeners)this.mountedComponents.has(n)||e.push(...a.map(i=>({...i,componentId:n})));return e}watchMount(e){return this.mountWatchers.add(e),()=>{this.mountWatchers.delete(e)}}watchUnmount(e){return this.unmountWatchers.add(e),()=>{this.unmountWatchers.delete(e)}}trackRender(e){if(!this.config.enabled)return;const n=this.renderCounts.get(e)||0;this.renderCounts.set(e,n+1),this.isProfiling&&this.profilingData.push({type:"render",component:e,timestamp:performance.now()})}trackBindingEval(e){this.config.enabled&&(this.bindingEvalCount++,this.isProfiling&&e&&this.profilingData.push({type:"binding",expression:e,timestamp:performance.now()}))}getRenderCount(){return new Map(this.renderCounts)}getBindingEvalCount(){return this.bindingEvalCount}getMemoryWarnings(){const e=[];try{const i=this.getState(),l=JSON.stringify(i).length;l>1e6&&e.push({type:"large-state",message:`상태 크기가 ${(l/1024/1024).toFixed(2)}MB입니다`,suggestion:"불필요한 데이터 정리 필요",severity:l>5e6?"error":"warning"})}catch{}this.stateHistory.length>this.config.maxHistorySize*.8&&e.push({type:"large-history",message:`상태 이력이 ${this.stateHistory.length}개입니다`,suggestion:"maxHistory 설정 조정 필요",severity:"warning"});const n=this.getOrphanedListeners().length;n>0&&e.push({type:"orphaned-listeners",message:`${n}개의 정리되지 않은 이벤트 리스너`,suggestion:"useEffect cleanup 또는 removeEventListener 확인",severity:n>10?"error":"warning"});const a=Array.from(this.renderCounts.entries()).filter(([,i])=>i>50);return a.length>0&&e.push({type:"excessive-renders",message:`${a.length}개 컴포넌트가 50회 이상 렌더링`,suggestion:"React.memo, useMemo, useCallback 사용 고려",severity:"warning"}),e}startProfiling(){this.config.enabled&&(this.isProfiling=!0,this.profilingData=[],this.profilingStartTime=performance.now(),console.log("[G7DevTools] 프로파일링 시작"))}stopProfiling(){this.isProfiling=!1;const e=performance.now()-this.profilingStartTime,n=this.analyzeProfile(e);return console.log("[G7DevTools] 프로파일링 완료",n),n}analyzeProfile(e){const n=this.profilingData.filter(h=>h.type==="render"),a=this.profilingData.filter(h=>h.type==="binding"),i=this.profilingData.filter(h=>h.type==="action"),l={};for(const h of n)h.component&&(l[h.component]||(l[h.component]={count:0,durations:[]}),l[h.component].count++,h.duration&&l[h.component].durations.push(h.duration));const c=Object.entries(l).map(([h,m])=>({name:h,renderCount:m.count,avgDuration:m.durations.length>0?m.durations.reduce((b,S)=>b+S,0)/m.durations.length:0})).sort((h,m)=>m.renderCount-h.renderCount).slice(0,10),d={};for(const h of a)h.expression&&(d[h.expression]=(d[h.expression]||0)+1);const f=Object.entries(d).sort(([,h],[,m])=>m-h).slice(0,10).map(([h])=>h);return{duration:e,entries:this.profilingData,summary:{totalRenders:n.length,totalBindings:a.length,totalActions:i.length,slowestComponents:c,hotPaths:f}}}resetPerformanceStats(){this.renderCounts.clear(),this.bindingEvalCount=0,this.profilingData=[]}trackRequest(e,n,a){if(!this.config.enabled)return"";const i=this.generateId(),l=oA(e),c=sA(e);return this.activeRequests.set(i,{id:i,url:l,fullUrl:e,queryParams:c,method:n,startTime:Date.now(),status:"pending",requestBody:a?.requestBody?this.sanitizeObject(a.requestBody):void 0,dataSourceId:a?.dataSourceId}),this.isProfiling&&this.profilingData.push({type:"network",timestamp:performance.now()}),i}completeRequest(e,n,a){if(!this.config.enabled)return;const i=this.activeRequests.get(e);if(!i)return;const l={...i,status:n>=200&&n<300?"success":"error",statusCode:n,duration:Date.now()-i.startTime,endTime:Date.now(),response:this.sanitizeObject(a)};this.requestHistory.push(l),this.activeRequests.delete(e),this.requestHistory.length>this.config.maxHistorySize&&this.requestHistory.shift(),this.notifyRequestWatchers(l)}failRequest(e,n){if(!this.config.enabled)return;const a=this.activeRequests.get(e);if(!a)return;const i={...a,status:"error",error:n,duration:Date.now()-a.startTime,endTime:Date.now()};this.requestHistory.push(i),this.activeRequests.delete(e),this.notifyRequestWatchers(i)}trackDataSource(e){this.config.enabled&&this.pendingDataSources.add(e)}completeDataSource(e){this.config.enabled&&this.pendingDataSources.delete(e)}getActiveRequests(){return Array.from(this.activeRequests.values())}getRequestHistory(){return[...this.requestHistory]}getPendingDataSources(){return Array.from(this.pendingDataSources)}watchRequest(e){return this.requestWatchers.add(e),()=>{this.requestWatchers.delete(e)}}notifyRequestWatchers(e){this.requestWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] Request watcher error:",a)}})}trackWebSocketConnection(e,n,a){this.config.enabled&&this.wsConnections.set(e,{id:e,url:n,state:a,connectedAt:a==="open"?Date.now():void 0})}trackWebSocketMessage(e,n,a,i){if(!this.config.enabled)return;const l={id:this.generateId(),connectionId:e,direction:n,type:a,payload:this.sanitizeObject(i),timestamp:Date.now(),sequence:this.wsMessageHistory.length};this.wsMessageHistory.push(l),this.wsMessageHistory.length>this.config.maxHistorySize&&this.wsMessageHistory.shift(),this.notifyWebSocketWatchers(l)}getWebSocketConnections(){return Array.from(this.wsConnections.values())}getWebSocketMessageHistory(){return[...this.wsMessageHistory]}getWebSocketConnectionState(){const e=Array.from(this.wsConnections.values());return e.some(n=>n.state==="open")?"connected":e.some(n=>n.state==="connecting")?"reconnecting":"disconnected"}watchWebSocketMessage(e){return this.wsMessageWatchers.add(e),()=>{this.wsMessageWatchers.delete(e)}}notifyWebSocketWatchers(e){this.wsMessageWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] WebSocket watcher error:",a)}})}trackIfCondition(e,n,a){if(!this.config.enabled)return;const i=this.ifConditions.get(e),l=i?.evaluatedValue;this.ifConditions.set(e,{id:e,expression:n,evaluatedValue:a,evaluationCount:(i?.evaluationCount||0)+1,lastEvaluated:Date.now()}),i&&l!==a&&this.notifyConditionWatchers({id:e,expression:n,oldValue:l,newValue:a,timestamp:Date.now()})}trackIteration(e,n){this.config.enabled&&this.iterations.set(e,{id:e,...n,lastRendered:Date.now()})}getIfConditions(){return Array.from(this.ifConditions.values())}getIterations(){return Array.from(this.iterations.values())}getScopeChain(e){const n=[],a=this.getState();return n.push({name:"_global",value:a._global,source:"global"}),n.push({name:"_local",value:a._local,source:"local"}),n.push({name:"_computed",value:a._computed,source:"computed"}),n}watchConditionChange(e){return this.conditionWatchers.add(e),()=>{this.conditionWatchers.delete(e)}}notifyConditionWatchers(e){this.conditionWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] Condition watcher error:",a)}})}trackForm(e,n,a){this.config.enabled&&this.forms.set(e,{id:e,dataKey:n,inputs:a,trackedAt:Date.now()})}trackFormChange(e,n,a){if(!this.config.enabled)return;const i={formId:e,inputName:n,value:a,timestamp:Date.now()};this.notifyFormWatchers(i)}untrackForm(e){this.forms.delete(e)}getForms(){return Array.from(this.forms.values())}getFormState(e){const n=this.getState();return this.getNestedValue(n,e)||{}}getBindingPath(e,n){return`${e}.${n}`}watchFormChange(e){return this.formWatchers.add(e),()=>{this.formWatchers.delete(e)}}notifyFormWatchers(e){this.formWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] Form watcher error:",a)}})}trackFormBindingIssue(e,n,a){if(!this.config.enabled)return;const i={id:`form-issue-${++this.formBindingIssueIdCounter}`,timestamp:Date.now(),type:e,severity:n,formId:a.formId,formDataKey:a.formDataKey,inputInfo:a.inputInfo,contextInfo:a.contextInfo,description:a.description,suggestion:a.suggestion,docLink:a.docLink};this.formBindingIssues.push(i),this.formBindingIssues.length>this.maxFormBindingIssues&&this.formBindingIssues.shift()}validateFormBinding(e,n,a,i){const l=a.map(f=>{const h=[];let m=!0;return f.name||(h.push("Input에 name 속성이 없습니다"),m=!1,this.trackFormBindingIssue("missing-input-name","error",{formId:e,formDataKey:n,inputInfo:{name:f.name,type:f.type},description:"Input에 name 속성이 없어 자동 바인딩이 동작하지 않습니다",suggestion:"Input에 name 속성을 추가하세요",docLink:"troubleshooting-components-form.md"})),n||(h.push("Form에 dataKey가 없습니다"),m=!1),{inputName:f.name||"(unnamed)",inputType:f.type,bindingPath:n&&f.name?`${n}.${f.name}`:"-",isValid:m,currentValue:f.value,issues:h}});if(n||this.trackFormBindingIssue("missing-datakey","error",{formId:e,description:"Form에 dataKey가 설정되지 않아 자동 바인딩이 동작하지 않습니다",suggestion:"Form 컴포넌트에 dataKey props를 설정하세요",docLink:"troubleshooting-components-form.md"}),i.isContextBroken){const f=i.breakReason?.includes("sortable")?"sortable-context-break":i.breakReason?.includes("modal")?"modal-context-isolation":"context-not-propagated";this.trackFormBindingIssue(f,"warning",{formId:e,formDataKey:n,contextInfo:{hasParentFormContext:i.hasParentContext,parentFormContextProp:i.parentContextProp,isInsideSortable:i.breakReason?.includes("sortable")||!1,isInsideModal:i.breakReason?.includes("modal")||!1,depth:0},description:i.breakReason||"Form 컨텍스트가 제대로 전파되지 않았습니다",suggestion:"parentFormContextProp={undefined}를 확인하거나 중첩 구조를 검토하세요",docLink:"troubleshooting-components-form.md"})}const c={formId:e,dataKey:n,timestamp:Date.now(),contextPropagation:i,inputBindings:l,isValid:l.every(f=>f.isValid)&&!!n&&!i.isContextBroken,issueCount:l.filter(f=>!f.isValid).length+(n?0:1)+(i.isContextBroken?1:0)},d=this.formBindingValidations.findIndex(f=>f.formId===e);return d>=0?this.formBindingValidations[d]=c:this.formBindingValidations.push(c),c}getFormBindingValidationTrackingInfo(){const e=this.getFormBindingValidationStats();return{issues:[...this.formBindingIssues],validations:[...this.formBindingValidations],stats:e,timestamp:Date.now()}}getFormBindingValidationStats(){const e={"missing-datakey":0,"missing-input-name":0,"context-not-propagated":0,"sortable-context-break":0,"modal-context-isolation":0,"duplicate-input-name":0,"value-type-mismatch":0,"binding-path-invalid":0},n={info:0,warning:0,error:0};for(const i of this.formBindingIssues)e[i.type]++,n[i.severity]++;const a=Object.entries(e).filter(([,i])=>i>0).sort((i,l)=>l[1]-i[1]).slice(0,5).map(([i,l])=>({type:i,count:l,description:this.getIssueTypeDescription(i)}));return{totalFormsValidated:this.formBindingValidations.length,validForms:this.formBindingValidations.filter(i=>i.isValid).length,formsWithIssues:this.formBindingValidations.filter(i=>!i.isValid).length,totalIssues:this.formBindingIssues.length,byIssueType:e,bySeverity:n,topIssues:a}}getIssueTypeDescription(e){return{"missing-datakey":"Form에 dataKey 누락","missing-input-name":"Input에 name 속성 누락","context-not-propagated":"Form 컨텍스트 전파 실패","sortable-context-break":"Sortable 컨테이너에서 컨텍스트 단절","modal-context-isolation":"모달에서 부모 Form 컨텍스트 격리","duplicate-input-name":"동일 Form 내 중복 Input name","value-type-mismatch":"값 타입과 Input 타입 불일치","binding-path-invalid":"바인딩 경로 유효하지 않음"}[e]||e}clearFormBindingIssues(){this.formBindingIssues=[],this.formBindingValidations=[],this.formBindingIssueIdCounter=0}trackComputedProperty(e,n,a,i,l,c,d){if(!this.config.enabled)return;const f=`computed-${e}-${c||"global"}`,h={id:f,name:e,expression:n,componentId:c,dependencies:a,currentValue:this.safeClone(i),lastComputedAt:Date.now(),computationTime:l,error:d};this.computedProperties.set(f,h)}trackComputedRecalc(e,n,a,i,l,c,d){if(!this.config.enabled)return;const f=`computed-${e}-${d||"global"}`,h=JSON.stringify(a)!==JSON.stringify(i),m={id:`recalc-${++this.computedRecalcIdCounter}`,computedId:f,computedName:e,timestamp:Date.now(),trigger:n,triggeredBy:c,previousValue:this.safeClone(a),newValue:this.safeClone(i),valueChanged:h,computationTime:l,cascadeCount:0};this.computedRecalcLogs.push(m),this.computedRecalcLogs.length>this.maxComputedRecalcLogs&&this.computedRecalcLogs.shift();const b=this.computedProperties.get(f);b&&(b.currentValue=this.safeClone(i),b.lastComputedAt=Date.now(),b.computationTime=l)}analyzeComputedDependencyChain(e,n){const a=new Set,i=[];let l=!1,c,d=0;const f=(m,b)=>{if(d=Math.max(d,b),a.has(m))return l=!0,c=[...i,m],{name:m,type:"computed",children:[],depth:b};a.add(m),i.push(m);const S=this.computedProperties.get(`computed-${m}-${n||"global"}`),v=[];if(S)for(const _ of S.dependencies)_.type==="computed"?v.push(f(_.path,b+1)):v.push({name:_.path,type:_.type,children:[],depth:b+1});return i.pop(),a.delete(m),{name:m,type:"computed",children:v,depth:b}},h=f(e,0);return{root:e,tree:h,hasCycle:l,cyclePath:c,maxDepth:d}}getComputedDependencyTrackingInfo(){const e=Array.from(this.computedProperties.values()),n=this.getComputedDependencyStats(),a=[];for(const i of e){const l=this.analyzeComputedDependencyChain(i.name,i.componentId);a.push(l)}return{properties:e,recalcLogs:[...this.computedRecalcLogs],dependencyChains:a,stats:n,timestamp:Date.now()}}getComputedDependencyStats(){const e={"state-change":0,"datasource-update":0,"dependency-change":0,manual:0,initial:0},n={},a={};let i=0,l=0;for(const h of this.computedRecalcLogs)e[h.trigger]++,h.valueChanged||l++,n[h.computedName]=(n[h.computedName]||0)+1,a[h.computedName]||(a[h.computedName]=[]),a[h.computedName].push(h.computationTime),i+=h.computationTime;const c=Object.entries(n).sort((h,m)=>m[1]-h[1]).slice(0,5).map(([h,m])=>({name:h,count:m})),d=Object.entries(a).map(([h,m])=>({name:h,avgTime:m.reduce((b,S)=>b+S,0)/m.length})).sort((h,m)=>m.avgTime-h.avgTime).slice(0,5);let f=0;for(const h of this.computedProperties.values())this.analyzeComputedDependencyChain(h.name,h.componentId).hasCycle&&f++;return{totalComputed:this.computedProperties.size,totalRecalculations:this.computedRecalcLogs.length,unnecessaryRecalculations:l,avgComputationTime:this.computedRecalcLogs.length>0?i/this.computedRecalcLogs.length:0,topRecalculated:c,slowestComputed:d,cycleDetectionCount:f,byTrigger:e}}clearComputedTracking(){this.computedProperties.clear(),this.computedRecalcLogs=[],this.computedRecalcIdCounter=0}trackModalOpen(e){if(!this.config.enabled)return;const n={modalId:e.modalId,modalName:e.modalName,openedAt:Date.now(),closedAt:null,scopeType:e.scopeType||"isolated",parentModalId:e.parentModalId,componentId:e.componentId,initialState:e.initialState?{...e.initialState}:{},currentState:e.initialState?{...e.initialState}:{},stateChangeCount:0,isolatedStateKeys:e.isolatedStateKeys||[],sharedStateKeys:e.sharedStateKeys||[]};this.modalStates.set(e.modalId,n),this.validateModalDefinitionExists(e.modalId,e.modalName),e.parentModalId&&(this.modalStateRelations.find(i=>i.parentModalId===e.parentModalId&&i.childModalId===e.modalId)||this.modalStateRelations.push({parentModalId:e.parentModalId,childModalId:e.modalId,sharedKeys:e.sharedStateKeys||[],isolatedKeys:e.isolatedStateKeys||[],relationType:"parent-child"}))}trackModalClose(e,n){if(!this.config.enabled)return;const a=this.modalStates.get(e);a&&(a.closedAt=Date.now(),n&&(a.currentState={...n}),this.detectStateLeakage(a))}validateModalDefinitionExists(e,n){const a=this.currentLayout;if(!a?.layoutJson)return;const i=a.layoutJson.modals;if(!i){this.recordModalStateIssue({type:"missing-definition",modalId:e,modalName:n,severity:"error",description:`모달 "${e}"이(가) modalStack에 추가되었으나, 현재 레이아웃에 modals 섹션이 없습니다. 레이아웃 경로: ${a.layoutPath}`,affectedStateKeys:[],expectedValue:`modals 섹션에 id="${e}" 정의 존재`,actualValue:"modals 섹션 없음"});return}const l=Array.isArray(i)?i:Object.values(i);l.some(d=>d?.id===e||d?.props?.id===e)||this.recordModalStateIssue({type:"missing-definition",modalId:e,modalName:n,severity:"error",description:`모달 "${e}"이(가) modalStack에 추가되었으나, 렌더링된 레이아웃의 modals 섹션에 해당 ID의 정의가 없습니다. partial 로딩 실패, 레이아웃 병합 문제, extends 상속 누락 등을 확인하세요. 레이아웃 경로: ${a.layoutPath}`,affectedStateKeys:[],expectedValue:`modals 배열에 id="${e}" 항목 존재`,actualValue:`modals 배열에 ${l.length}개 모달 정의 (${l.map(d=>d?.id||"unknown").join(", ")})`})}trackModalStateChange(e){if(!this.config.enabled)return;const n=this.modalStates.get(e.modalId);if(!n)return;n.stateChangeCount++,n.currentState[e.stateKey]=e.newValue;const a=this.checkIsolationViolation(n,e.stateKey),i={id:`modal-change-${++this.modalStateChangeLogIdCounter}`,modalId:e.modalId,modalName:n.modalName,timestamp:Date.now(),stateKey:e.stateKey,previousValue:e.previousValue,newValue:e.newValue,changeSource:e.changeSource||"user-action",violatesIsolation:a};this.modalStateChangeLogs.push(i),this.modalStateChangeLogs.length>this.maxModalStateChangeLogs&&this.modalStateChangeLogs.shift(),a&&this.recordModalStateIssue({type:"isolation-violation",modalId:e.modalId,modalName:n.modalName,severity:"warning",description:`모달 '${n.modalName}'에서 격리된 상태 키 '${e.stateKey}'가 변경됨`,affectedStateKeys:[e.stateKey]})}detectStateLeakage(e){if(e.scopeType==="isolated")for(const n of e.isolatedStateKeys){const a=e.initialState[n],i=e.currentState[n];if(a!==i&&e.parentModalId){const l=this.modalStates.get(e.parentModalId);l&&l.currentState.hasOwnProperty(n)&&this.recordModalStateIssue({type:"state-leakage",modalId:e.modalId,modalName:e.modalName,severity:"error",description:`모달 닫힘 후 상태 '${n}'가 부모 모달로 유출될 수 있음`,affectedStateKeys:[n],leakedValue:i,expectedValue:a})}}}checkIsolationViolation(e,n){return e.scopeType==="isolated",!1}recordModalStateIssue(e){const n={id:`modal-issue-${++this.modalStateIssueIdCounter}`,type:e.type,modalId:e.modalId,modalName:e.modalName,timestamp:Date.now(),severity:e.severity,description:e.description,affectedStateKeys:e.affectedStateKeys,leakedValue:e.leakedValue,expectedValue:e.expectedValue,actualValue:e.actualValue,stackInfo:e.stackInfo};this.modalStateIssues.push(n),this.modalStateIssues.length>this.maxModalStateIssues&&this.modalStateIssues.shift()}getModalStateScopeTrackingInfo(){const e=Array.from(this.modalStates.values()),n=this.getModalStateScopeStats();return{modals:e,issues:[...this.modalStateIssues],relations:[...this.modalStateRelations],changeLogs:[...this.modalStateChangeLogs],stats:n,timestamp:Date.now()}}getModalStateScopeStats(){const e=Array.from(this.modalStates.values()),n={warning:0,error:0},a={"state-leakage":0,"isolation-violation":0,"parent-mutation":0,"orphaned-state":0,"scope-mismatch":0,"cleanup-failure":0,"missing-definition":0};for(const l of this.modalStateIssues)n[l.severity]++,a[l.type]++;const i={isolated:0,shared:0,inherited:0};for(const l of e)i[l.scopeType]++;return{totalModals:e.length,openModals:e.filter(l=>l.closedAt===null).length,nestedModals:e.filter(l=>l.parentModalId!==void 0).length,totalIssues:this.modalStateIssues.length,issuesBySeverity:n,issuesByType:a,byScope:i,leakageDetectionCount:a["state-leakage"],cleanupFailureCount:a["cleanup-failure"]}}clearModalStateScopeTracking(){this.modalStates.clear(),this.modalStateIssues=[],this.modalStateRelations=[],this.modalStateChangeLogs=[],this.modalStateIssueIdCounter=0,this.modalStateChangeLogIdCounter=0}setNamedActionDefinitions(e){this.config.enabled&&(this.namedActionDefinitions=e||{})}trackNamedActionRef(e){if(!this.config.enabled)return;const n={...e,id:`named_action_ref_${++this.namedActionRefIdCounter}`};this.namedActionRefLogs.push(n),this.namedActionRefLogs.length>this.maxNamedActionRefLogs&&(this.namedActionRefLogs=this.namedActionRefLogs.slice(-this.maxNamedActionRefLogs))}getNamedActionTrackingInfo(){const e={};for(const a of this.namedActionRefLogs)e[a.actionRefName]=(e[a.actionRefName]||0)+1;const n=Object.keys(this.namedActionDefinitions).filter(a=>!e[a]);return{definitions:this.namedActionDefinitions,refLogs:[...this.namedActionRefLogs],stats:{totalDefinitions:Object.keys(this.namedActionDefinitions).length,totalRefs:this.namedActionRefLogs.length,refCountByName:e,unusedDefinitions:n},timestamp:Date.now()}}clearNamedActionTracking(){this.namedActionDefinitions={},this.namedActionRefLogs=[],this.namedActionRefIdCounter=0}trackExpressionEval(e){if(!this.config.enabled)return;const n={...e,id:`expr-${++this.expressionIdCounter}`,timestamp:Date.now(),warning:this.detectExpressionWarning(e)};this.expressionHistory.push(n),this.expressionHistory.length>500&&this.expressionHistory.shift(),this.isProfiling&&this.profilingData.push({type:"binding",expression:e.expression,timestamp:performance.now(),duration:e.duration}),this.notifyExpressionWatchers(n)}detectExpressionWarning(e){const{expression:n,result:a,resultType:i,method:l,duration:c}=e;if(i==="undefined"&&!n.includes("??")&&!n.includes("||")&&/^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(n.replace(/{{|}}/g,"").trim()))return{type:"undefined-result",message:"표현식 결과가 undefined입니다",suggestion:`데이터가 로드되었는지 확인하거나 fallback 값을 추가하세요: ${n} ?? ''`};if(i==="null")return{type:"null-result",message:"표현식 결과가 null입니다",suggestion:`nullish coalescing 연산자를 사용하세요: ${n} ?? ''`};if(l==="resolveBindings"&&i==="string"){const d=String(a);if(d.includes("[object Object]")||/^\[.*\]$/.test(d))return{type:"array-to-string",message:"배열/객체가 문자열로 변환되었습니다",suggestion:"evaluateExpression()을 사용하여 원본 타입을 유지하세요"}}if(l==="resolveBindings"&&i==="string"&&String(a).includes("[object Object]"))return{type:"object-to-string",message:"객체가 [object Object]로 변환되었습니다",suggestion:"evaluateExpression()을 사용하거나 특정 속성에 접근하세요"};if(i==="undefined"&&n.includes(".")&&!n.includes("?.")){const d=n.replace(/{{|}}/g,"").trim().split(".");if(d.length>=2)return{type:"missing-optional-chain",message:"Optional chaining(?.) 누락 가능성",suggestion:`안전한 접근을 위해 ?. 사용을 고려하세요: ${d.join("?.")}`}}if((n.includes("{{item.")||n.includes("{{index}}"))&&Array.from(this.iterations.values()).some(h=>h.itemVar!=="item"))return{type:"wrong-iteration-var",message:"'item'/'index' 대신 iteration에서 정의한 변수명을 사용해야 합니다",suggestion:"iteration의 item_var/index_var에 정의된 변수명을 확인하세요"};if(c&&c>10)return{type:"slow-evaluation",message:`표현식 평가에 ${c.toFixed(2)}ms 소요됨`,suggestion:"복잡한 표현식을 단순화하거나 computed 값으로 분리하세요"}}getExpressions(){return[...this.expressionHistory]}getExpressionWarnings(){return this.expressionHistory.filter(e=>e.warning!=null)}searchExpressions(e){const n=e.toLowerCase();return this.expressionHistory.filter(a=>a.expression.toLowerCase().includes(n)||a.componentName?.toLowerCase().includes(n)||a.propName?.toLowerCase().includes(n))}getExpressionStats(){const e=this.expressionHistory,n=new Set(e.map(f=>f.expression)),a=e.filter(f=>f.warning!=null),i=e.filter(f=>f.fromCache),l=e.filter(f=>f.duration!=null).map(f=>f.duration),c={};for(const f of e)c[f.resultType]=(c[f.resultType]||0)+1;const d={};for(const f of a)f.warning&&(d[f.warning.type]=(d[f.warning.type]||0)+1);return{totalEvaluations:e.length,uniqueExpressions:n.size,warningCount:a.length,cacheHitRate:e.length>0?i.length/e.length:0,averageDuration:l.length>0?l.reduce((f,h)=>f+h,0)/l.length:0,byType:c,byWarning:d}}clearExpressions(){this.expressionHistory=[],this.expressionIdCounter=0}watchExpressions(e){return this.expressionWatchers.add(e),()=>{this.expressionWatchers.delete(e)}}notifyExpressionWatchers(e){this.expressionWatchers.forEach(n=>{try{n(e)}catch(a){console.error("[G7DevTools] Expression watcher error:",a)}})}setLogLevel(e){this.config.logLevel=e}setMaxHistory(e){this.config.maxHistorySize=Math.max(10,Math.min(1e3,e))}setServerEndpoint(e){this.config.serverEndpoint=e}getConfig(){return{...this.config}}generateId(){return`${Date.now()}-${Math.random().toString(36).substr(2,9)}`}sanitizeObject(e,n=new WeakSet){if(e===null||typeof e!="object")return e;if(n.has(e))return"[Circular]";if(n.add(e),Array.isArray(e))return e.map(i=>this.sanitizeObject(i,n));const a={};for(const i of Object.keys(e))try{a[i]=this.sanitizeObject(e[i],n)}catch{a[i]="[Unable to serialize]"}return a}getNestedValue(e,n){if(!n)return e;const a=n.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}getLifecycleInfo(){return{mountedComponents:Array.from(this.mountedComponents.values()),orphanedListeners:this.getOrphanedListeners()}}getPerformanceInfo(){return{renderCounts:this.renderCounts,bindingEvalCount:this.bindingEvalCount,memoryWarnings:this.getMemoryWarnings()}}getNetworkInfo(){return{activeRequests:Array.from(this.activeRequests.values()),requestHistory:[...this.requestHistory],pendingDataSources:Array.from(this.pendingDataSources)}}getConditionalInfo(){return{ifConditions:Array.from(this.ifConditions.values()),iterations:Array.from(this.iterations.values())}}getFormInfo(){return Array.from(this.forms.values())}getWebSocketInfo(){return{connections:Array.from(this.wsConnections.values()),messageHistory:[...this.wsMessageHistory],connectionState:this.getWebSocketConnectionState()}}getStateAtTime(e){const n=[...this.stateHistory].reverse().find(a=>a.timestamp<=e);if(n)return{_global:n.next,_local:{}}}reset(){this.stateHistory=[],this.actionHistory=[],this.cacheStats={hits:0,misses:0,entries:0},this.cacheDecisions=[],this.cacheDecisionIdCounter=0,this.currentRenderCycleId=null,this.isInActionExecution=!1,this.isInIteration=!1,this.mountedComponents.clear(),this.eventListeners.clear(),this.renderCounts.clear(),this.bindingEvalCount=0,this.profilingData=[],this.activeRequests.clear(),this.requestHistory=[],this.pendingDataSources.clear(),this.wsConnections.clear(),this.wsMessageHistory=[],this.ifConditions.clear(),this.iterations.clear(),this.forms.clear(),this.expressionHistory=[],this.expressionIdCounter=0,this.dataSources.clear(),this.dataPathTransforms=[],this.nestedContexts=[],this.nestedContextIdCounter=0,this.formBindingIssues=[],this.formBindingValidations=[],this.formBindingIssueIdCounter=0,this.computedProperties.clear(),this.computedRecalcLogs=[],this.computedRecalcIdCounter=0,this.modalStates.clear(),this.modalStateIssues=[],this.modalStateRelations=[],this.modalStateChangeLogs=[],this.modalStateIssueIdCounter=0,this.modalStateChangeLogIdCounter=0,this.logHistory=[],this.logIdCounter=0}getExpressionInfo(){return{expressions:this.getExpressions(),stats:this.getExpressionStats()}}trackLog(e,n,a){if(!this.config.enabled)return;const i=a.map(c=>{if(typeof c=="string")return c;try{return JSON.stringify(c)}catch{return String(c)}}).join(" "),l={id:`log-${++this.logIdCounter}`,level:e,prefix:n,message:i,args:this.sanitizeObject(a),timestamp:Date.now(),stack:e==="error"?new Error().stack:void 0};this.logHistory.push(l),this.logHistory.length>this.maxLogHistory&&this.logHistory.shift()}trackAction(e){if(!this.config.enabled)return;const n={id:`debounce-action-${++this.debounceActionIdCounter}`,...e,timestamp:Date.now()};this.debounceActionHistory.push(n),this.debounceActionHistory.length>100&&this.debounceActionHistory.shift()}trackDataSourceUpdate(e){if(!this.config.enabled)return;const n={id:`ds-update-${++this.dataSourceUpdateIdCounter}`,...e};this.dataSourceUpdateHistory.push(n),this.dataSourceUpdateHistory.length>100&&this.dataSourceUpdateHistory.shift()}getDebounceActionHistory(e){let n=[...this.debounceActionHistory];return e?.handler&&(n=n.filter(a=>a.handler.includes(e.handler))),e?.status&&(n=n.filter(a=>a.status===e.status)),e?.limit&&(n=n.slice(-e.limit)),n}getDataSourceUpdateHistory(e){let n=[...this.dataSourceUpdateHistory];return e?.dataSourceId&&(n=n.filter(a=>a.dataSourceId===e.dataSourceId)),e?.updateType&&(n=n.filter(a=>a.updateType===e.updateType)),e?.limit&&(n=n.slice(-e.limit)),n}getLogs(e){let n=[...this.logHistory];if(e?.level){const a=Array.isArray(e.level)?e.level:[e.level];n=n.filter(i=>a.includes(i.level))}if(e?.prefix){const a=e.prefix.toLowerCase();n=n.filter(i=>i.prefix.toLowerCase().includes(a))}if(e?.search){const a=e.search.toLowerCase();n=n.filter(i=>i.message.toLowerCase().includes(a))}return e?.since&&(n=n.filter(a=>a.timestamp>=e.since)),e?.limit&&(n=n.slice(-e.limit)),n}getLogStats(){const n=Date.now()-6e4,a={log:0,warn:0,error:0,debug:0,info:0},i={};let l=0,c=0;for(const d of this.logHistory)a[d.level]++,i[d.prefix]=(i[d.prefix]||0)+1,d.timestamp>=n&&(d.level==="error"&&l++,d.level==="warn"&&c++);return{totalLogs:this.logHistory.length,byLevel:a,byPrefix:i,recentErrors:l,recentWarnings:c}}clearLogs(){this.logHistory=[],this.logIdCounter=0}getLogInfo(){return{entries:this.getLogs(),stats:this.getLogStats()}}trackDataSourceDefinition(e){this.config.enabled&&this.dataSources.set(e.id,{...e,status:"idle"})}trackDataSourceLoading(e){if(!this.config.enabled)return;const n=this.dataSources.get(e);n&&this.dataSources.set(e,{...n,status:"loading"})}trackDataSourceLoaded(e,n,a){if(!this.config.enabled)return;const i=this.dataSources.get(e);if(!i)return;let l,c,d=a;n&&(Array.isArray(n)?(l=n.length,c=n.length>0&&typeof n[0]=="object"?Object.keys(n[0]):void 0):typeof n=="object"&&(Array.isArray(n.data)?(d=d||"data",l=n.data.length,c=n.data.length>0&&typeof n.data[0]=="object"?Object.keys(n.data[0]):void 0):Array.isArray(n.items)?(d=d||"items",l=n.items.length,c=n.items.length>0&&typeof n.items[0]=="object"?Object.keys(n.items[0]):void 0):c=Object.keys(n))),this.dataSources.set(e,{...i,status:"loaded",dataPath:d,itemCount:l,keys:c,lastLoadedAt:Date.now(),error:void 0})}trackDataSourceError(e,n){if(!this.config.enabled)return;const a=this.dataSources.get(e);a&&this.dataSources.set(e,{...a,status:"error",error:n,lastLoadedAt:Date.now()})}untrackDataSource(e){this.dataSources.delete(e)}getDataSources(){return Array.from(this.dataSources.values())}getDataSource(e){return this.dataSources.get(e)}clearDataSources(){this.dataSources.clear()}trackDataPathTransform(e){if(!this.config.enabled)return;let n=this.dataPathTransforms.find(a=>a.dataSourceId===e.dataSourceId);n||(n={dataSourceId:e.dataSourceId,timestamp:Date.now(),transformSteps:[],warnings:[]},this.dataPathTransforms.push(n),this.dataPathTransforms.length>this.maxDataPathTransforms&&this.dataPathTransforms.shift()),n.transformSteps.push({step:e.step,inputPath:e.inputPath,inputValue:this.safeClone(e.inputValue),outputPath:e.outputPath,outputValue:this.safeClone(e.outputValue),config:e.config}),e.warning&&n.warnings.push(e.warning)}setDataPathFinalBinding(e,n){if(!this.config.enabled)return;const a=this.dataPathTransforms.find(i=>i.dataSourceId===e);a&&(a.finalBinding={expression:n.expression,resolvedPath:n.resolvedPath,value:this.safeClone(n.value)})}getDataPathTransformTrackingInfo(){const e=this.getDataPathTransformStats();return{transforms:[...this.dataPathTransforms],stats:e,timestamp:Date.now()}}getDataPathTransformStats(){const e=this.dataPathTransforms,n={};let a=0;const i={};for(const c of e){n[c.dataSourceId]=(n[c.dataSourceId]||0)+1,a+=c.warnings.length;for(const d of c.warnings)i[d]=(i[d]||0)+1}const l=Object.entries(i).map(([c,d])=>({warning:c,count:d})).sort((c,d)=>d.count-c.count).slice(0,10);return{totalTransforms:e.length,byDataSource:n,warningsCount:a,commonWarnings:l}}getDataPathTransformForDataSource(e){return this.dataPathTransforms.find(n=>n.dataSourceId===e)}clearDataPathTransforms(){this.dataPathTransforms=[]}trackNestedContext(e){if(!this.config.enabled)return"";const n=`nested-${++this.nestedContextIdCounter}`,a=[...new Set([...e.parentContext.available,...e.ownContext.added])],i={...e.parentContext.values,...e.ownContext.values},l={id:n,timestamp:Date.now(),componentId:e.componentId,componentType:e.componentType,parentContext:{available:e.parentContext.available,values:this.safeClone(e.parentContext.values)},ownContext:{added:e.ownContext.added,values:this.safeClone(e.ownContext.values)},mergedContext:{all:a,values:this.safeClone(i)},accessAttempts:[],depth:e.depth,parentId:e.parentId};return this.nestedContexts.push(l),this.nestedContexts.length>this.maxNestedContexts&&this.nestedContexts.shift(),n}trackNestedContextAccess(e,n){if(!this.config.enabled)return;const a=this.nestedContexts.find(i=>i.id===e);a&&a.accessAttempts.push({path:n.path,found:n.found,value:n.found?this.safeClone(n.value):void 0,error:n.error})}getNestedContextTrackingInfo(){const e=this.getNestedContextStats();return{contexts:[...this.nestedContexts],stats:e,timestamp:Date.now()}}getNestedContextStats(){const e=this.nestedContexts,n={expandChildren:0,cellChildren:0,iteration:0,modal:0,slot:0};let a=0,i=0;const l={};for(const d of e){n[d.componentType]++,d.depth>a&&(a=d.depth);for(const f of d.accessAttempts)f.found||(i++,l[f.path]=(l[f.path]||0)+1)}const c=Object.entries(l).map(([d,f])=>({path:d,count:f})).sort((d,f)=>f.count-d.count).slice(0,10);return{totalContexts:e.length,byType:n,maxDepth:a,failedAccessCount:i,commonFailedPaths:c}}getNestedContextForComponent(e){return this.nestedContexts.find(n=>n.componentId===e)}clearNestedContexts(){this.nestedContexts=[],this.nestedContextIdCounter=0}trackHandlerRegistration(e,n="custom",a,i){this.isEnabled&&this.handlers.set(e,{name:e,category:n,description:a,registeredAt:Date.now(),source:i})}trackHandlerUnregistration(e){this.isEnabled&&this.handlers.delete(e)}getHandlers(){return Array.from(this.handlers.values())}getHandler(e){return this.handlers.get(e)}getHandlersByCategory(e){return Array.from(this.handlers.values()).filter(n=>n.category===e)}clearHandlers(){this.handlers.clear()}trackEventSubscribe(e){if(!this.isEnabled)return;const n=this.componentEventSubscriptions.get(e),a=Date.now();n?(n.subscriberCount++,n.lastSubscribedAt=a):this.componentEventSubscriptions.set(e,{eventName:e,subscriberCount:1,firstSubscribedAt:a,lastSubscribedAt:a})}trackEventUnsubscribe(e){if(!this.isEnabled)return;const n=this.componentEventSubscriptions.get(e);n&&(n.subscriberCount--,n.subscriberCount<=0&&this.componentEventSubscriptions.delete(e))}trackEventEmit(e,n,a,i,l){if(!this.isEnabled)return;const c={id:`evt_${++this.componentEventIdCounter}`,eventName:e,data:this.sanitizeForLogging(n),timestamp:Date.now(),listenerCount:a,results:i?this.sanitizeForLogging(i):void 0,hasError:!!l,errorMessage:l?.message};this.componentEventEmitHistory.push(c),this.componentEventEmitHistory.length>this.maxComponentEventHistory&&(this.componentEventEmitHistory=this.componentEventEmitHistory.slice(-this.maxComponentEventHistory))}trackEventOff(e){this.isEnabled&&this.componentEventSubscriptions.delete(e)}trackEventClear(){this.isEnabled&&this.componentEventSubscriptions.clear()}getComponentEventInfo(){const e=Array.from(this.componentEventSubscriptions.values());return{subscriptions:e,emitHistory:[...this.componentEventEmitHistory],totalSubscribers:e.reduce((n,a)=>n+a.subscriberCount,0),totalEmits:this.componentEventEmitHistory.length}}getEventEmitHistory(){return[...this.componentEventEmitHistory]}getEventSubscriptions(){return Array.from(this.componentEventSubscriptions.values())}clearComponentEvents(){this.componentEventSubscriptions.clear(),this.componentEventEmitHistory=[],this.componentEventIdCounter=0}sanitizeForLogging(e){if(e==null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{return"[Circular or Non-serializable]"}}startStateChange(e,n,a,i){if(!this.config.enabled)return"";const l=`setState_${++this.stateRenderingIdCounter}`;this.currentStateChangeContext={setStateId:l,startTime:performance.now(),changedPath:e,trigger:i||{},renderedComponents:[]};const c={id:`sr_${this.stateRenderingIdCounter}`,setStateId:l,statePath:e,oldValue:this.sanitizeForLogging(n),newValue:this.sanitizeForLogging(a),timestamp:Date.now(),triggeredBy:i||{},renderedComponents:[],totalRenderDuration:0,affectedBindingsCount:0};return this.stateRenderingLogs.push(c),this.stateRenderingLogs.length>this.maxStateRenderingLogs&&this.stateRenderingLogs.shift(),l}trackComponentRender(e,n,a,i=[],l=[],c){if(!this.config.enabled)return;const d=this.componentRenderCounts.get(n)||0;this.componentRenderCounts.set(n,d+1);for(const f of i)this.stateToComponentMap.has(f)||this.stateToComponentMap.set(f,new Set),this.stateToComponentMap.get(f).add(n);if(this.currentStateChangeContext){const f={componentId:e,componentName:n,renderDuration:a,accessedStatePaths:i,evaluatedBindings:l,renderOrder:this.currentStateChangeContext.renderedComponents.length,parentId:c};this.currentStateChangeContext.renderedComponents.push(f)}this.trackRender(n)}completeStateChange(e){if(!this.config.enabled||!this.currentStateChangeContext||this.currentStateChangeContext.setStateId!==e)return;const n=this.currentStateChangeContext,i=performance.now()-n.startTime,l=this.stateRenderingLogs.findIndex(c=>c.setStateId===e);if(l>=0){const c=this.stateRenderingLogs[l];c.renderedComponents=n.renderedComponents,c.totalRenderDuration=i,c.affectedBindingsCount=n.renderedComponents.reduce((d,f)=>d+f.evaluatedBindings.length,0)}this.currentStateChangeContext=null}getCurrentStateChangeContext(){return this.currentStateChangeContext}getStateRenderingLogs(){return[...this.stateRenderingLogs]}getStateRenderingInfo(){const e=this.stateRenderingLogs,n=Object.fromEntries(this.componentRenderCounts),a={};for(const[i,l]of this.stateToComponentMap)a[i]=Array.from(l);return{logs:e,componentRenderCounts:n,stateToComponentMap:a,stats:this.calculateStateRenderingStats()}}calculateStateRenderingStats(){const e=this.stateRenderingLogs;if(e.length===0)return{totalStateChanges:0,totalRenders:0,avgRenderDuration:0,avgComponentsPerChange:0,topRenderedComponents:[],topInfluentialPaths:[]};const n=e.reduce((d,f)=>d+f.renderedComponents.length,0),a=e.reduce((d,f)=>d+f.totalRenderDuration,0)/e.length,i=n/e.length,l=Array.from(this.componentRenderCounts.entries()).sort((d,f)=>f[1]-d[1]).slice(0,5).map(([d,f])=>({name:d,count:f})),c=Array.from(this.stateToComponentMap.entries()).sort((d,f)=>f[1].size-d[1].size).slice(0,5).map(([d,f])=>({path:d,affectedComponents:f.size}));return{totalStateChanges:e.length,totalRenders:n,avgRenderDuration:a,avgComponentsPerChange:i,topRenderedComponents:l,topInfluentialPaths:c}}getComponentsAffectedByState(e){const n=this.stateToComponentMap.get(e);return n?Array.from(n):[]}getComponentRenderHistory(e){return this.stateRenderingLogs.filter(n=>n.renderedComponents.some(a=>a.componentName===e))}clearStateRenderingLogs(){this.stateRenderingLogs=[],this.componentRenderCounts.clear(),this.stateToComponentMap.clear(),this.currentStateChangeContext=null,this.stateRenderingIdCounter=0}trackComponentStateSource(e,n,a,i){this.config.enabled&&this.componentStateSources.set(e,{componentId:e,componentName:n,stateSource:a,stateProvider:i})}trackDynamicState(e,n){this.config.enabled&&this.dynamicStates.set(e,n)}trackContextFlow(e,n,a,i,l,c){if(!this.config.enabled)return;const d={component:n,componentId:e,contextReceived:a,passedToChildren:i,usedInRender:l};if(this.contextFlowNodes.set(e,d),c){const f=this.contextFlowNodes.get(c);f&&(f.children||(f.children=[]),f.children.push(d))}}getStateHierarchyInfo(e=!0){const n=this.buildStateHierarchyLayers(),a=this.detectStateConflicts(n),i=Array.from(this.componentStateSources.values());return{layers:e?n.map(c=>({...c,values:this.summarizeValues(c.values,2)})):n,conflicts:a,componentStateSources:i,timestamp:Date.now()}}summarizeValues(e,n,a=0){if(e==null)return e;if(typeof e!="object")return typeof e=="string"&&e.length>100?e.substring(0,100)+`... (${e.length}자)`:e;if(a>=n){if(Array.isArray(e))return`[Array(${e.length})]`;const c=Object.keys(e);return`{${c.slice(0,5).join(", ")}${c.length>5?`, ... +${c.length-5}`:""}}`}if(Array.isArray(e))return e.length<=3?e.map(c=>this.summarizeValues(c,n,a+1)):{__type:"array",__length:e.length,__preview:e.slice(0,3).map(c=>this.summarizeValues(c,n,a+1))};const i={},l=Object.keys(e);for(const c of l)i[c]=this.summarizeValues(e[c],n,a+1);return i}buildStateHierarchyLayers(){const e=[];try{const a=window.G7Core?.state?.get?.();if(!a)return e;a._local&&e.push({name:"Global _local",type:"global",path:"_local",values:a._local,priority:1}),a._global&&e.push({name:"Global _global",type:"global",path:"_global",values:a._global,priority:1});for(const[l,c]of this.dynamicStates)c&&Object.keys(c).length>0&&e.push({name:`DynamicState (${l})`,type:"dynamicState",componentId:l,values:c,priority:2});const i=this.computeEffectiveState(a._local||{});e.push({name:"Effective _local (merged)",type:"effective",values:i,priority:3})}catch(n){console.warn("[G7DevTools] 상태 계층 빌드 실패:",n)}return e}computeEffectiveState(e){const n={...e};for(const[,a]of this.dynamicStates)a._local&&Object.assign(n,a._local);return n}detectStateConflicts(e){const n=[],a=e.find(l=>l.type==="global"&&l.path==="_local");if(!a)return n;const i=e.filter(l=>l.type==="dynamicState");for(const l of i){const c=l.values._local||l.values;for(const[d,f]of Object.entries(c)){const h=a.values[d];if(JSON.stringify(h)!==JSON.stringify(f)){const m=[],b=[];for(const S of this.componentStateSources.values())S.stateSource.local.some(_=>_===`_local.${d}`||_===d)&&(S.stateProvider.type==="dynamicState"?m.push(S.componentName):S.stateProvider.type==="globalState"&&b.push(S.componentName));n.push({path:`_local.${d}`,globalValue:h,dynamicStateValue:f,effectiveValue:f,usedBy:m,notUsedBy:b,severity:b.length>0?"warning":"info",description:b.length>0?`${b.join(", ")}은(는) globalState._local을 읽어 dynamicState 값을 사용하지 못함`:"dynamicState 값이 globalState와 다르지만 모든 컴포넌트가 dynamicState를 사용"})}}}return n}getContextFlowInfo(){const e=[],n=new Set(this.contextFlowNodes.keys());for(const a of this.contextFlowNodes.values())if(a.children)for(const i of a.children)n.delete(i.componentId);for(const a of n){const i=this.contextFlowNodes.get(a);i&&e.push(i)}return{rootComponent:e.length>0?e[0].component:"Unknown",contextFlow:e,timestamp:Date.now()}}clearStateHierarchyData(){this.componentStateSources.clear(),this.contextFlowNodes.clear(),this.dynamicStates.clear()}trackComponentStyle(e,n,a,i){if(!this.config.enabled)return;const l=this.analyzeTailwindClasses(a),c={componentId:e,componentName:n,classes:a,computedStyles:i,tailwindAnalysis:l};this.componentStyles.set(e,c),this.detectStyleIssues(c)}analyzeTailwindClasses(e){const n=[],a=[],i=[],l=[],c=["sm:","md:","lg:","xl:","2xl:"],d=[/bg-\w+-\d+/,/text-\w+-\d+/,/border-\w+-\d+/];for(const f of e){l.push(f),f.startsWith("dark:")&&n.push(f);for(const h of c)if(f.startsWith(h)){a.push(f);break}for(const h of d)if(h.test(f)){i.push(f);break}}return{usedClasses:l,darkClasses:n,responsiveClasses:a,dynamicClasses:i}}detectStyleIssues(e){const{componentId:n,componentName:a,computedStyles:i,tailwindAnalysis:l}=e;(i.opacity==="0"||i.visibility==="hidden"||i.display==="none"||i.width==="0px"&&i.height==="0px")&&this.addStyleIssue({id:this.generateId(),type:"invisible-element",componentId:n,componentName:a,property:i.opacity==="0"?"opacity":i.visibility==="hidden"?"visibility":i.display==="none"?"display":"width/height",currentValue:i.opacity==="0"?"0":i.visibility==="hidden"?"hidden":i.display==="none"?"none":"0px",severity:"warning",description:"요소가 화면에 보이지 않습니다.",suggestion:"opacity, visibility, display, width/height 값을 확인하세요."});const c=l.usedClasses.filter(f=>f.startsWith("bg-")&&!f.startsWith("dark:")),d=l.darkClasses.filter(f=>f.includes("bg-"));c.length>0&&d.length===0&&this.addStyleIssue({id:this.generateId(),type:"dark-mode-missing",componentId:n,componentName:a,property:"background-color",currentValue:c.join(", "),expectedValue:"dark:bg-* 클래스 필요",severity:"info",description:"다크 모드 배경색 클래스가 누락되었습니다.",suggestion:`${c[0]}에 대응하는 dark: 클래스를 추가하세요.`}),l.dynamicClasses.length>0&&this.addStyleIssue({id:this.generateId(),type:"tailwind-purging",componentId:n,componentName:a,property:"class",currentValue:l.dynamicClasses.join(", "),severity:"info",description:"동적으로 생성된 Tailwind 클래스가 있습니다.",suggestion:"tailwind.config.js의 safelist에 추가하거나 전체 클래스명을 상수로 정의하세요."})}addStyleIssue(e){this.styleIssues.some(a=>a.componentId===e.componentId&&a.type===e.type&&a.property===e.property)||this.styleIssues.push(e)}getStyleValidationInfo(){const e=Array.from(this.componentStyles.values());return{issues:[...this.styleIssues],componentStyles:e,stats:{totalComponents:e.length,invisibleCount:this.styleIssues.filter(n=>n.type==="invisible-element").length,tailwindIssueCount:this.styleIssues.filter(n=>n.type==="tailwind-purging").length,darkModeIssueCount:this.styleIssues.filter(n=>n.type==="dark-mode-missing").length},timestamp:Date.now()}}clearStyleValidationData(){this.styleIssues=[],this.componentStyles.clear()}trackAuthEvent(e,n,a,i){if(!this.config.enabled)return;const l={id:`auth_${++this.authEventIdCounter}`,type:e,timestamp:Date.now(),success:n,error:a,details:i};this.authEvents.push(l),this.authEvents.length>this.maxAuthEventHistory&&(this.authEvents=this.authEvents.slice(-this.maxAuthEventHistory))}trackAuthHeader(e,n,a,i,l){if(!this.config.enabled)return;const c={url:e,hasAuthHeader:n,headerType:a,tokenValid:i??!1,responseStatus:l,timestamp:Date.now()};this.authHeaderHistory.push(c),this.authHeaderHistory.length>this.maxAuthEventHistory&&(this.authHeaderHistory=this.authHeaderHistory.slice(-this.maxAuthEventHistory)),l===401&&this.trackAuthEvent("api-unauthorized",!1,`401 Unauthorized: ${e}`,{url:e})}getAuthState(){try{const e=window.G7Core,n=e?.api;let a=e?.auth;!a&&e?.AuthManager?.getInstance&&(a=e.AuthManager.getInstance());const i=n?.getToken?.()||a?.getAccessToken?.()||localStorage.getItem("auth_token")||localStorage.getItem("access_token"),l=a?.getRefreshToken?.()||localStorage.getItem("refresh_token");let c="unknown",d=a?.getUser?.();if(d&&(c="AuthManager"),!d){const v=e?.state?.get?.();d=v?._global?.user||v?._global?.currentUser||v?.user||v?.currentUser,d&&(c="G7Core.state")}const f=a?.isAuthenticated?.()??!1;let h="memory";localStorage.getItem("auth_token")||localStorage.getItem("access_token")?h="localStorage":(sessionStorage.getItem("auth_token")||sessionStorage.getItem("access_token"))&&(h="sessionStorage");const m=!!i&&(!!d||f);let b;i&&typeof i=="string"&&i.length>20?b=`${i.substring(0,10)}...${i.substring(i.length-6)}`:i&&(b="***");const S=d?{id:d.id,email:d.email,name:d.name,roles:d.roles?.map(v=>typeof v=="string"?{name:v}:{id:v.id,name:v.name,guard_name:v.guard_name}),permissions:d.permissions?.map(v=>typeof v=="string"?{name:v}:{id:v.id,name:v.name,guard_name:v.guard_name}),avatar:d.avatar||d.profile_photo_url||d.profile_image,created_at:d.created_at,updated_at:d.updated_at,last_login_at:d.last_login_at,email_verified_at:d.email_verified_at,...d.phone&&{phone:d.phone},...d.nickname&&{nickname:d.nickname},...d.status&&{status:d.status},...d.locale&&{locale:d.locale}}:void 0;return{isAuthenticated:m,user:S,tokens:{hasAccessToken:!!i,hasRefreshToken:!!l,storage:h,accessTokenPreview:b},lastActivity:Date.now(),source:c}}catch{return{isAuthenticated:!1,tokens:{hasAccessToken:!1,hasRefreshToken:!1,storage:"memory"},source:"unknown"}}}getAuthDebugInfo(){const e=[...this.authEvents];return{state:this.getAuthState(),events:e,headerAnalysis:[...this.authHeaderHistory],stats:{loginAttempts:e.filter(n=>n.type==="login").length,successfulLogins:e.filter(n=>n.type==="login"&&n.success).length,failedLogins:e.filter(n=>n.type==="login"&&!n.success).length,tokenRefreshes:e.filter(n=>n.type==="token-refresh").length,unauthorizedResponses:e.filter(n=>n.type==="api-unauthorized").length},timestamp:Date.now()}}clearAuthData(){this.authEvents=[],this.authHeaderHistory=[],this.authEventIdCounter=0}trackLayoutLoad(e,n,a,i="api"){if(!this.config.enabled)return;const l=Date.now();this.currentLayout={layoutPath:e,templateId:n,layoutJson:this.sanitizeObject(a),loadedAt:l,version:a.version,layoutName:a.layout_name,source:i};const c={id:`layout-${++this.layoutIdCounter}`,layoutPath:e,templateId:n,loadedAt:l,source:i,version:a.version};this.layoutHistory.push(c),this.layoutHistory.length>this.maxLayoutHistory&&this.layoutHistory.shift(),this.layoutStats.totalLoads++,i==="cache"?this.layoutStats.cacheHits++:this.layoutStats.apiLoads++}getCurrentLayout(){return this.currentLayout}getLayoutHistory(){return[...this.layoutHistory]}getLayoutDebugInfo(){return{current:this.currentLayout,history:[...this.layoutHistory],stats:{...this.layoutStats}}}clearLayoutData(){this.currentLayout=null,this.layoutHistory=[],this.layoutIdCounter=0,this.layoutStats={totalLoads:0,cacheHits:0,apiLoads:0}}startHandlerExecution(e){if(!this.config.enabled)return"";const n=`exec_${++this.executionIdCounter}_${Date.now()}`,a={handlerName:e,executionId:n,startTime:Date.now(),stateChanges:[],dataSourceChanges:[],alerts:[]};if(this.executionDetails.set(n,a),this.executionDetails.size>this.maxExecutionDetails){const i=Array.from(this.executionDetails.keys());i.slice(0,i.length-this.maxExecutionDetails).forEach(c=>this.executionDetails.delete(c))}return n}endHandlerExecution(e,n,a,i){if(!this.config.enabled||!e)return;const l=this.executionDetails.get(e);if(!l)return;l.endTime=Date.now(),l.duration=l.endTime-l.startTime,l.exitReason=n,l.exitLocation=a,l.exitDescription=i;const c=this.changeExpectations.get(e);c&&(l.expectedChanges=c,this.checkExpectations(e,l,c),this.changeExpectations.delete(e)),n==="normal"&&l.stateChanges.length===0&&l.dataSourceChanges.length===0&&this.addChangeAlert(e,{type:"no-state-change",severity:"warning",message:`핸들러 "${l.handlerName}"이(가) 상태 변경 없이 완료됨`,description:"핸들러가 성공적으로 완료되었지만 상태나 데이터소스 변경이 없습니다. 의도한 동작인지 확인하세요.",handlerName:l.handlerName,suggestion:'핸들러 내부 조건문을 확인하거나 DevTools의 "변경감지" 탭에서 exitLocation을 확인하세요.',docLink:"troubleshooting-state.md"}),(n==="early-return-condition"||n==="early-return-validation")&&this.addChangeAlert(e,{type:"early-return-detected",severity:"info",message:`핸들러 "${l.handlerName}"이(가) early return으로 종료됨`,description:i||`종료 위치: ${a||"알 수 없음"}`,handlerName:l.handlerName,suggestion:n==="early-return-validation"?"검증 실패로 인한 early return입니다. 입력 데이터를 확인하세요.":"조건부 early return입니다. 조건문의 평가 결과를 확인하세요."})}recordExitReason(e,n,a,i){if(!this.config.enabled||!e)return;const l=this.executionDetails.get(e);l&&(l.exitReason=n,l.exitLocation=a,l.exitDescription=i)}recordStateChange(e,n,a,i,l){if(!this.config.enabled)return;const c={id:`sc_${++this.stateChangeIdCounter}`,path:n,changeType:a,oldValue:this.safeClone(i),newValue:this.safeClone(l),timestamp:Date.now(),comparison:this.compareValues(i,l),executionId:e};if(this.stateChangeHistory.push(c),e){const d=this.executionDetails.get(e);d&&d.stateChanges.push(c)}this.stateChangeHistory.length>this.maxStateChangeHistory&&this.stateChangeHistory.splice(0,this.stateChangeHistory.length-this.maxStateChangeHistory),c.comparison.isDeepEqual&&typeof i=="object"&&i!==null&&this.addChangeAlert(e||"",{type:"object-reference-same",severity:"warning",message:`상태 "${n}"가 변경되었으나 값이 동일함`,description:"객체 참조가 동일하거나 깊은 비교 결과 변경이 없습니다. 변경 감지가 실패할 수 있습니다.",handlerName:e&&this.executionDetails.get(e)?.handlerName||"unknown",statePath:n,suggestion:"불변성을 유지하여 새 객체를 생성하세요. 예: { ...oldObject, newField: value }",docLink:"troubleshooting-state.md#object-mutation"})}recordDataSourceChange(e,n,a,i,l){if(!this.config.enabled)return;const c={dataSourceId:n,changeType:a,timestamp:Date.now(),previousStatus:i,newStatus:l,executionId:e};if(this.dataSourceChangeHistory.push(c),e){const d=this.executionDetails.get(e);d&&d.dataSourceChanges.push(c)}this.dataSourceChangeHistory.length>this.maxStateChangeHistory&&this.dataSourceChangeHistory.splice(0,this.dataSourceChangeHistory.length-this.maxStateChangeHistory)}expectChange(e,n,a){if(!this.config.enabled||!e)return;const i=this.executionDetails.get(e);if(!i)return;const l={expectedStatePaths:n,expectedDataSources:a,setAt:Date.now(),source:i.handlerName};this.changeExpectations.set(e,l)}checkExpectations(e,n,a){const i=new Set(n.stateChanges.map(c=>c.path)),l=new Set(n.dataSourceChanges.map(c=>c.dataSourceId));for(const c of a.expectedStatePaths)i.has(c)||this.addChangeAlert(e,{type:"expected-not-fulfilled",severity:"error",message:`기대한 상태 변경 "${c}"가 발생하지 않음`,description:`핸들러 "${n.handlerName}"이(가) "${c}" 변경을 예고했지만 실제로 변경되지 않았습니다.`,handlerName:n.handlerName,statePath:c,suggestion:"핸들러 내부 로직을 확인하세요. early return이나 조건부 분기로 인해 변경이 생략되었을 수 있습니다.",docLink:"troubleshooting-state.md"});for(const c of a.expectedDataSources)l.has(c)||this.addChangeAlert(e,{type:"expected-not-fulfilled",severity:"error",message:`기대한 데이터소스 갱신 "${c}"가 발생하지 않음`,description:`핸들러 "${n.handlerName}"이(가) "${c}" 갱신을 예고했지만 실제로 갱신되지 않았습니다.`,handlerName:n.handlerName,dataSourceId:c,suggestion:"refetchDataSource 호출이 실행되었는지 확인하세요.",docLink:"data-sources.md"})}addChangeAlert(e,n){const a={...n,id:`alert_${++this.alertIdCounter}`,timestamp:Date.now()};if(this.changeAlerts.push(a),e){const i=this.executionDetails.get(e);i&&i.alerts.push(a)}this.changeAlerts.length>this.maxChangeAlerts&&this.changeAlerts.splice(0,this.changeAlerts.length-this.maxChangeAlerts)}compareValues(e,n){const a=this.getValueType(e),i=this.getValueType(n),l=a!==i;let c=!1,d;return!l&&(a==="object"||a==="array")?(c=this.deepEqual(e,n),!c&&a==="object"&&(d=this.getChangedKeys(e,n))):c=e===n,{typeChanged:l,oldType:a,newType:i,isDeepEqual:c,changedKeys:d}}getValueType(e){return e===null?"null":e===void 0?"undefined":Array.isArray(e)?"array":typeof e}deepEqual(e,n){if(e===n)return!0;if(typeof e!=typeof n)return!1;if(e===null||n===null||typeof e!="object")return e===n;if(Array.isArray(e)!==Array.isArray(n))return!1;const a=Object.keys(e),i=Object.keys(n);if(a.length!==i.length)return!1;for(const l of a)if(!i.includes(l)||!this.deepEqual(e[l],n[l]))return!1;return!0}getChangedKeys(e,n){const a=[],i=new Set([...Object.keys(e),...Object.keys(n)]);for(const l of i)this.deepEqual(e[l],n[l])||a.push(l);return a}getChangeDetectionInfo(){const e=Array.from(this.executionDetails.values()),n={totalExecutions:e.length,executionsWithStateChange:e.filter(a=>a.stateChanges.length>0).length,executionsWithoutStateChange:e.filter(a=>a.stateChanges.length===0&&a.exitReason==="normal").length,earlyReturnCount:e.filter(a=>a.exitReason==="early-return-condition"||a.exitReason==="early-return-validation").length,alertCount:this.changeAlerts.length,alertsByType:this.getAlertsByType()};return{executionDetails:e,stateChangeHistory:[...this.stateChangeHistory],dataSourceChangeHistory:[...this.dataSourceChangeHistory],alerts:[...this.changeAlerts],stats:n,timestamp:Date.now()}}getAlertsByType(){const e={"no-state-change":0,"no-datasource-change":0,"expected-not-fulfilled":0,"object-reference-same":0,"early-return-detected":0,"async-timing-issue":0};for(const n of this.changeAlerts)e[n.type]++;return e}clearChangeDetectionData(){this.executionDetails.clear(),this.stateChangeHistory=[],this.dataSourceChangeHistory=[],this.changeAlerts=[],this.changeExpectations.clear(),this.executionIdCounter=0,this.stateChangeIdCounter=0,this.alertIdCounter=0}getRecentAlerts(e=10,n){let a=[...this.changeAlerts];return n&&(a=a.filter(i=>i.severity===n)),a.slice(-e)}getHandlerExecutions(e,n=10){return Array.from(this.executionDetails.values()).filter(i=>i.handlerName===e).slice(-n)}findExecutionInStack(e){return this.sequenceExecutionStack.find(n=>n.sequenceId===e)}startSequenceExecution(e){if(!this.config.enabled)return"";const n=`seq_${++this.sequenceIdCounter}_${Date.now()}`,a={sequenceId:n,startTime:Date.now(),totalDuration:0,trigger:e,actions:[],status:"running"};return this.sequenceExecutionStack.push(a),n}captureSequenceActionBefore(e,n,a,i,l){if(!this.config.enabled)return;const c=this.findExecutionInStack(e);if(!c)return;const d={index:n,handler:a,params:this.safeClone(i),stateBeforeAction:{_global:this.safeClone(l._global),_local:this.safeClone(l._local),_isolated:l._isolated?this.safeClone(l._isolated):void 0},stateAfterAction:{_global:{},_local:{}},duration:0,status:"success"};c.actions.push(d)}captureSequenceActionAfter(e,n,a,i,l,c){if(!this.config.enabled)return;const d=this.findExecutionInStack(e);if(!d)return;const f=d.actions.find(m=>m.index===n);if(!f)return;f.stateAfterAction={_global:this.safeClone(a._global),_local:this.safeClone(a._local),_isolated:a._isolated?this.safeClone(a._isolated):void 0},f.duration=i,f.result=this.safeClone(l),f.stateDiff={global:this.computeStateDiff(f.stateBeforeAction._global,f.stateAfterAction._global),local:this.computeStateDiff(f.stateBeforeAction._local,f.stateAfterAction._local)},f.stateBeforeAction._isolated&&f.stateAfterAction._isolated&&(f.stateDiff.isolated=this.computeStateDiff(f.stateBeforeAction._isolated,f.stateAfterAction._isolated));const h=window.__g7PendingLocalState;h&&(f.pendingState=this.safeClone(h)),c&&(f.status="error",f.error={name:c.name,message:c.message,stack:c.stack})}endSequenceExecution(e,n){if(!this.config.enabled)return;const a=this.sequenceExecutionStack.findIndex(l=>l.sequenceId===e);if(a===-1)return;const i=this.sequenceExecutionStack[a];if(i.endTime=Date.now(),i.totalDuration=i.endTime-i.startTime,n){i.status="error",i.error={name:n.name,message:n.message,stack:n.stack};const l=i.actions.find(c=>c.status==="error");l&&(i.failedAtIndex=l.index)}else i.status="success";this.sequenceExecutions.push(i),this.sequenceExecutions.length>this.maxSequenceExecutions&&this.sequenceExecutions.shift(),this.sequenceExecutionStack.splice(a,1)}computeStateDiff(e,n){const a=[],i=[],l=[],c=new Set(Object.keys(e)),d=new Set(Object.keys(n));for(const f of d)c.has(f)||a.push(f);for(const f of c)d.has(f)||i.push(f);for(const f of c)if(d.has(f)){const h=e[f],m=n[f];JSON.stringify(h)!==JSON.stringify(m)&&l.push({path:f,oldValue:h,newValue:m})}return{added:a,removed:i,changed:l}}getSequenceTrackingInfo(){const e=this.getSequenceStats(),n=this.sequenceExecutionStack.length>0?this.sequenceExecutionStack[this.sequenceExecutionStack.length-1]:void 0;return{executions:[...this.sequenceExecutions],currentExecution:n,stats:e}}getSequenceStats(){const e=this.sequenceExecutions,n=e.length,a=e.filter(h=>h.status==="success").length,i=e.filter(h=>h.status==="error").length,l=e.reduce((h,m)=>h+m.actions.length,0),c=n>0?e.reduce((h,m)=>h+m.totalDuration,0)/n:0,d={};for(const h of e)for(const m of h.actions)d[m.handler]=(d[m.handler]||0)+1;const f=Object.entries(d).sort(([,h],[,m])=>m-h).slice(0,5).map(([h,m])=>({handler:h,count:m}));return{totalExecutions:n,successCount:a,errorCount:i,totalActions:l,avgDuration:c,topHandlers:f}}clearSequenceData(){this.sequenceExecutions=[],this.sequenceExecutionStack=[],this.sequenceIdCounter=0}getSequenceExecution(e){const n=this.findExecutionInStack(e);return n||this.sequenceExecutions.find(a=>a.sequenceId===e)}getRecentSequences(e=10){return this.sequenceExecutions.slice(-e)}registerStateCaptureForHandler(e,n,a){if(!this.config.enabled)return;const i=new Map,l=Date.now();for(const c of n)i.set(c,{value:this.safeClone(a[c]),capturedAt:l});this.stateCaptureRegistry.set(e,i)}detectStaleClosure(e,n,a,i,l){if(!this.config.enabled)return[];const c=this.stateCaptureRegistry.get(e);if(!c)return[];const d=Date.now(),f=[];for(const[h,m]of c){const b=this.getValueByPath(a,h),S=d-m.capturedAt;if(!this.isDeepEqual(m.value,b)&&S>0){const v=this.createStaleClosureWarning(i,n,h,m.value,m.capturedAt,b,d,S,l);f.push(v),this.addStaleClosureWarning(v)}}return this.stateCaptureRegistry.delete(e),f}trackStaleClosureWarning(e){if(!this.config.enabled)return;const n=Date.now(),a=this.createStaleClosureWarning(e.type,e.location,e.capturedPath,e.capturedValue,e.capturedAt,e.currentValue,n,n-e.capturedAt,e.actionId,e.stackTrace);this.addStaleClosureWarning(a)}createStaleClosureWarning(e,n,a,i,l,c,d,f,h,m){const b=this.getStaleClosureSeverity(e,f),{description:S,suggestion:v,docLink:_}=this.getStaleClosureMessages(e,a,f);return{id:`stale_${++this.staleClosureIdCounter}_${Date.now()}`,timestamp:Date.now(),type:e,location:n,capturedState:{path:a,capturedValue:this.safeClone(i),capturedAt:l},currentState:{path:a,currentValue:this.safeClone(c),retrievedAt:d},timeDiff:f,severity:b,description:S,suggestion:v,docLink:_,actionId:h,stackTrace:m}}getStaleClosureSeverity(e,n){if(n>5e3)return"error";if(n>1e3)return"warning";switch(e){case"async-state-capture":case"sequence-state-mismatch":return"warning";case"callback-state-capture":case"timeout-state-capture":return"info";case"event-handler-stale":return"warning";default:return"info"}}getStaleClosureMessages(e,n,a){const i=a<1e3?`${a}ms`:`${(a/1e3).toFixed(1)}s`;switch(e){case"async-state-capture":return{description:`await 후 ${i} 경과 시점에 '${n}' 상태가 변경됨. 캡처된 상태 대신 최신 상태를 사용해야 함`,suggestion:"await 이후에는 G7Core.state.get()으로 최신 상태를 다시 조회하거나, useRef + getter 패턴을 사용하세요",docLink:"troubleshooting-state-closure.md"};case"callback-state-capture":return{description:`콜백에서 ${i} 전에 캡처된 '${n}' 상태를 사용 중. 최신 상태와 다름`,suggestion:"콜백에서 상태를 참조할 때는 stateRef.current 패턴 또는 G7Core.state.get()를 사용하세요",docLink:"troubleshooting-state-closure.md"};case"timeout-state-capture":return{description:`setTimeout/setInterval 콜백에서 '${n}' 상태가 ${i} 전 값 사용 중`,suggestion:"타이머 콜백 내에서는 G7Core.state.get()으로 최신 상태를 조회하세요",docLink:"troubleshooting-state-closure.md"};case"sequence-state-mismatch":return{description:`sequence 내 '${n}' 상태가 이전 액션 결과와 불일치. context.state 대신 캡처된 값 사용 의심`,suggestion:"sequence 내에서는 context.state 또는 $prev를 사용하여 최신 상태를 참조하세요",docLink:"troubleshooting-state-setstate.md"};case"event-handler-stale":return{description:`이벤트 핸들러에서 ${i} 전에 바인딩된 '${n}' 상태 사용 중`,suggestion:"이벤트 핸들러에서 상태를 참조할 때는 useRef 패턴 또는 G7Core.state.get()를 사용하세요",docLink:"troubleshooting-state-closure.md"};default:return{description:`'${n}' 상태에서 stale closure 감지됨 (${i} 경과)`,suggestion:"상태 참조 시 최신 값을 사용하는지 확인하세요"}}}addStaleClosureWarning(e){this.staleClosureWarnings.push(e),this.staleClosureWarnings.length>this.maxStaleClosureWarnings&&this.staleClosureWarnings.shift()}getValueByPath(e,n){const a=n.split(".");let i=e;for(const l of a){if(i==null)return;i=i[l]}return i}isDeepEqual(e,n){if(e===n)return!0;if(e===null||n===null)return e===n;if(typeof e!=typeof n)return!1;if(typeof e!="object")return e===n;try{return JSON.stringify(e)===JSON.stringify(n)}catch{return!1}}getStaleClosureTrackingInfo(){const e=this.getStaleClosureStats();return{warnings:[...this.staleClosureWarnings],stats:e,timestamp:Date.now()}}getStaleClosureStats(){const e=this.staleClosureWarnings,n={"async-state-capture":0,"callback-state-capture":0,"timeout-state-capture":0,"sequence-state-mismatch":0,"event-handler-stale":0};for(const f of e)n[f.type]++;const a={info:0,warning:0,error:0};for(const f of e)a[f.severity]++;const i={};for(const f of e)i[f.location]=(i[f.location]||0)+1;const l=Object.entries(i).sort(([,f],[,h])=>h-f).slice(0,5).map(([f,h])=>({location:f,count:h})),c={};for(const f of e)c[f.capturedState.path]=(c[f.capturedState.path]||0)+1;const d=Object.entries(c).sort(([,f],[,h])=>h-f).slice(0,5).map(([f,h])=>({path:f,count:h}));return{totalWarnings:e.length,warningsByType:n,warningsBySeverity:a,topLocations:l,topAffectedPaths:d}}clearStaleClosureData(){this.staleClosureWarnings=[],this.staleClosureIdCounter=0,this.stateCaptureRegistry.clear()}getRecentStaleClosureWarnings(e=10,n){let a=[...this.staleClosureWarnings];return n&&(a=a.filter(i=>i.severity===n)),a.slice(-e)}safeClone(e){if(e==null||typeof e!="object")return e;try{return JSON.parse(JSON.stringify(e))}catch{if(Array.isArray(e))return e.map(a=>{try{return JSON.parse(JSON.stringify(a))}catch{return"[Unserializable]"}});const n={};for(const a of Object.keys(e))try{n[a]=JSON.parse(JSON.stringify(e[a]))}catch{n[a]="[Unserializable]"}return n}}};$(Ei,"instance");let Ma=Ei;const Vn=dt("LayoutLoader");class Ar extends Error{constructor(n,a,i){super(n);$(this,"code");$(this,"details");this.code=a,this.details=i,this.name="LayoutLoaderError"}}class xf{constructor(e){$(this,"componentRegistry");$(this,"currentLayout",null);$(this,"layoutCache",new Map);$(this,"cacheVersion",0);this.componentRegistry=e}setCacheVersion(e){this.cacheVersion!==e&&(Vn.log("Cache version updated:",this.cacheVersion,"->",e),this.cacheVersion=e,this.layoutCache.clear())}getCacheVersion(){return this.cacheVersion}async loadLayout(e,n){const a=`${e}:${n}`;if(this.layoutCache.has(a)){Vn.log("Loading layout from cache:",n);const l=await this.layoutCache.get(a),c=JSON.parse(JSON.stringify(l));return this.currentLayout=c,Ma.getInstance().trackLayoutLoad(n,e,c,"cache"),c}const i=this.fetchLayout(e,n);return this.layoutCache.set(a,i),i.catch(()=>{this.layoutCache.get(a)===i&&this.layoutCache.delete(a)}),i}async fetchLayout(e,n,a=!1){try{const i=this.cacheVersion>0?this.cacheVersion:null;let l;if(n.startsWith("__preview__/")){const S=n.replace("__preview__/","");l=Ir(`/api/layouts/preview/${S}`,"json",i)}else l=Ir(`/api/layouts/${e}/${n}`,"json",i);Vn.log("Fetching layout from API:",l);const c=Hr(),d=a?null:c.getToken(),f={Accept:"application/json"};d&&(f.Authorization=`Bearer ${d}`);const h=await Il(l,{init:{headers:f},label:`layout: ${n}`});let m;try{m=await h.json()}catch{m=null}if(!h.ok){if(h.status===401&&!a&&d){Vn.log("Token invalid, removing and retrying without token"),c.removeToken();try{return await this.fetchLayout(e,n,!0)}catch(v){throw v instanceof Ar&&v.details?.status===401?new Ar(v.message,v.code,{...v.details,hadToken:!0}):v}}const S=m?.message||m?.error;throw new Ar(`Failed to fetch layout: ${h.status} ${h.statusText}`,"FETCH_FAILED",{status:h.status,statusText:h.statusText,url:l,apiMessage:S})}const b=m.data||m;return this.validateLayoutData(b),this.currentLayout=b,Ma.getInstance().trackLayoutLoad(n,e,b,"api"),Vn.log("Layout fetched and cached successfully:",b.layout_name),b}catch(i){const l=`${e}:${n}`;throw this.layoutCache.delete(l),i instanceof Ar?i:new Ar("Failed to load layout","LOAD_FAILED",{originalError:i})}}prefetchLayout(e,n){return this.loadLayout(e,n)}validateLayoutData(e){if(!e.version)throw new Ar("Layout data missing required field: version","VALIDATION_FAILED",{field:"version"});if(!e.layout_name)throw new Ar("Layout data missing required field: layout_name","VALIDATION_FAILED",{field:"layout_name"});if(!Array.isArray(e.components))throw new Ar('Layout data field "components" must be an array',"VALIDATION_FAILED",{field:"components",value:e.components});Vn.log("Layout data validation passed")}renderLayout(e,n){try{const a=n||this.currentLayout;if(!a)throw new Ar("No layout data to render","NO_LAYOUT_DATA");Vn.log("Rendering layout:",a.layout_name),e.innerHTML="",this.renderComponents(e,a.components),Vn.log("Layout rendered successfully")}catch(a){Vn.error("Render error:",a),this.renderErrorState(e,a)}}renderComponents(e,n){for(const a of n){const i=this.renderComponent(a);i&&e.appendChild(i)}}renderComponent(e){try{if(!this.componentRegistry.getComponent(e.type))return Vn.warn(`Component not found: ${e.type}`),this.createPlaceholderElement(e.type);const a=document.createElement("div");return a.setAttribute("data-component",e.type),e.props&&Object.entries(e.props).forEach(([i,l])=>{a.setAttribute(`data-prop-${i}`,JSON.stringify(l))}),e.children&&e.children.length>0?this.renderComponents(a,e.children):e.text&&(a.textContent=e.text),a}catch(n){return Vn.error(`Error rendering component ${e.type}:`,n),this.createErrorElement(e.type,n)}}createPlaceholderElement(e){const n=document.createElement("div");return n.className="component-placeholder",n.setAttribute("data-component-type",e),n.innerHTML=` <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> - `,r}createErrorElement(e,r){const a=document.createElement("div");return a.className="component-error",a.setAttribute("data-component-type",e),a.innerHTML=` + `,n}createErrorElement(e,n){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;">${r?.message||"Unknown error"}</p> + <p style="margin: 0; color: #666; font-size: 0.875rem;">${n?.message||"Unknown error"}</p> </div> - `,a}renderErrorState(e,r){const a=r instanceof Error?r.message:String(r),i=r instanceof Er?r.code:"UNKNOWN_ERROR";e.innerHTML=` + `,a}renderErrorState(e,n){const a=n instanceof Error?n.message:String(n),i=n instanceof Ar?n.code:"UNKNOWN_ERROR";e.innerHTML=` <div class="layout-error" data-error-code="${i}" style=" padding: 2rem; text-align: center; @@ -139,14 +105,8 @@ Error generating stack: `+u.message+` " >페이지 새로고침</button> </div> - `,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.options=a,this.eventFormatter=new Jm(this.options.namespace),this.subscribe()}subscribe(){this.socket.emit("subscribe",{channel:this.name,auth:this.options.auth||{}})}unsubscribe(){this.unbind(),this.socket.emit("unsubscribe",{channel:this.name,auth:this.options.auth||{}})}listen(e,r){return this.on(this.eventFormatter.format(e),r),this}stopListening(e,r){return this.unbindEvent(this.eventFormatter.format(e),r),this}subscribed(e){return this.on("connect",r=>{e(r)}),this}error(e){return this}on(e,r){return this.listeners[e]=this.listeners[e]||[],this.events[e]||(this.events[e]=(a,i)=>{this.name===a&&this.listeners[e]&&this.listeners[e].forEach(l=>l(i))},this.socket.on(e,this.events[e])),this.listeners[e].push(r),this}unbind(){Object.keys(this.events).forEach(e=>{this.unbindEvent(e)})}unbindEvent(e,r){this.listeners[e]=this.listeners[e]||[],r&&(this.listeners[e]=this.listeners[e].filter(a=>a!==r)),(!r||this.listeners[e].length===0)&&(this.events[e]&&(this.socket.removeListener(e,this.events[e]),delete this.events[e]),delete this.listeners[e])}}class ey extends Zm{whisper(e,r){return this.socket.emit("client event",{channel:this.name,event:`client-${e}`,data:r}),this}}class p_ extends ey{here(e){return this.on("presence:subscribed",r=>{e(r.map(a=>a.user_info))}),this}joining(e){return this.on("presence:joining",r=>e(r.user_info)),this}whisper(e,r){return this.socket.emit("client event",{channel:this.name,event:`client-${e}`,data:r}),this}leaving(e){return this.on("presence:leaving",r=>e(r.user_info)),this}}class Ac extends uf{subscribe(){}unsubscribe(){}listen(e,r){return this}listenToAll(e){return this}stopListening(e,r){return this}subscribed(e){return this}error(e){return this}on(e,r){return this}}class ty extends Ac{whisper(e,r){return this}}class m_ extends Ac{whisper(e,r){return this}}class y_ extends ty{here(e){return this}joining(e){return this}whisper(e,r){return this}leaving(e){return this}}const ny=class NS{constructor(e){this.setOptions(e),this.connect()}setOptions(e){this.options={...NS._defaultOptions,...e,broadcaster:e.broadcaster};let r=this.csrfToken();r&&(this.options.auth.headers["X-CSRF-TOKEN"]=r,this.options.userAuthentication.headers["X-CSRF-TOKEN"]=r),r=this.options.bearerToken,r&&(this.options.auth.headers.Authorization="Bearer "+r,this.options.userAuthentication.headers.Authorization="Bearer "+r)}csrfToken(){var e,r;return typeof window<"u"&&(e=window.Laravel)!=null&&e.csrfToken?window.Laravel.csrfToken:this.options.csrfToken?this.options.csrfToken:typeof document<"u"&&typeof document.querySelector=="function"?((r=document.querySelector('meta[name="csrf-token"]'))==null?void 0:r.getAttribute("content"))??null:null}};ny._defaultOptions={auth:{headers:{}},authEndpoint:"/broadcasting/auth",userAuthentication:{endpoint:"/broadcasting/user-auth",headers:{}},csrfToken:null,bearerToken:null,host:null,key:null,namespace:"App.Events"};let ff=ny;class xc extends ff{constructor(){super(...arguments),this.channels={}}connect(){if(typeof this.options.client<"u")this.pusher=this.options.client;else if(this.options.Pusher)this.pusher=new this.options.Pusher(this.options.key,this.options);else if(typeof window<"u"&&typeof window.Pusher<"u")this.pusher=new window.Pusher(this.options.key,this.options);else throw new Error("Pusher client not found. Should be globally available or passed via options.client")}signin(){this.pusher.signin()}listen(e,r,a){return this.channel(e).listen(r,a)}channel(e){return this.channels[e]||(this.channels[e]=new df(this.pusher,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new Qm(this.pusher,"private-"+e,this.options)),this.channels["private-"+e]}encryptedPrivateChannel(e){return this.channels["private-encrypted-"+e]||(this.channels["private-encrypted-"+e]=new h_(this.pusher,"private-encrypted-"+e,this.options)),this.channels["private-encrypted-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new g_(this.pusher,"presence-"+e,this.options)),this.channels["presence-"+e]}leave(e){[e,"private-"+e,"private-encrypted-"+e,"presence-"+e].forEach(r=>{this.leaveChannel(r)})}leaveChannel(e){this.channels[e]&&(this.channels[e].unsubscribe(),delete this.channels[e])}socketId(){return this.pusher.connection.socket_id}disconnect(){this.pusher.disconnect()}}class b_ extends ff{constructor(){super(...arguments),this.channels={}}connect(){let e=this.getSocketIO();this.socket=e(this.options.host??void 0,this.options),this.socket.io.on("reconnect",()=>{Object.values(this.channels).forEach(r=>{r.subscribe()})})}getSocketIO(){if(typeof this.options.client<"u")return this.options.client;if(typeof window<"u"&&typeof window.io<"u")return window.io;throw new Error("Socket.io client not found. Should be globally available or passed via options.client")}listen(e,r,a){return this.channel(e).listen(r,a)}channel(e){return this.channels[e]||(this.channels[e]=new Zm(this.socket,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new ey(this.socket,"private-"+e,this.options)),this.channels["private-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new p_(this.socket,"presence-"+e,this.options)),this.channels["presence-"+e]}leave(e){[e,"private-"+e,"presence-"+e].forEach(r=>{this.leaveChannel(r)})}leaveChannel(e){this.channels[e]&&(this.channels[e].unsubscribe(),delete this.channels[e])}socketId(){return this.socket.id}disconnect(){this.socket.disconnect()}}class ry extends ff{constructor(){super(...arguments),this.channels={}}connect(){}listen(e,r,a){return new Ac}channel(e){return new Ac}privateChannel(e){return new ty}encryptedPrivateChannel(e){return new m_}presenceChannel(e){return new y_}leave(e){}leaveChannel(e){}socketId(){return"fake-socket-id"}disconnect(){}}class v_{constructor(e){this.options=e,this.connect(),this.options.withoutInterceptors||this.registerInterceptors()}channel(e){return this.connector.channel(e)}connect(){if(this.options.broadcaster==="reverb")this.connector=new xc({...this.options,cluster:""});else if(this.options.broadcaster==="pusher")this.connector=new xc(this.options);else if(this.options.broadcaster==="ably")this.connector=new xc({...this.options,cluster:"",broadcaster:"pusher"});else if(this.options.broadcaster==="socket.io")this.connector=new b_(this.options);else if(this.options.broadcaster==="null")this.connector=new ry(this.options);else if(typeof this.options.broadcaster=="function"&&f_(this.options.broadcaster))this.connector=new this.options.broadcaster(this.options);else throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} is not supported.`)}disconnect(){this.connector.disconnect()}join(e){return this.connector.presenceChannel(e)}leave(e){this.connector.leave(e)}leaveChannel(e){this.connector.leaveChannel(e)}leaveAllChannels(){for(const e in this.connector.channels)this.leaveChannel(e)}listen(e,r,a){return this.connector.listen(e,r,a)}private(e){return this.connector.privateChannel(e)}encryptedPrivate(e){if(this.connectorSupportsEncryptedPrivateChannels(this.connector))return this.connector.encryptedPrivateChannel(e);throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} does not support encrypted private channels.`)}connectorSupportsEncryptedPrivateChannels(e){return e instanceof xc||e instanceof ry}socketId(){return this.connector.socketId()}registerInterceptors(){typeof Vue<"u"&&Vue!=null&&Vue.http&&this.registerVueRequestInterceptor(),typeof axios=="function"&&this.registerAxiosRequestInterceptor(),typeof jQuery=="function"&&this.registerjQueryAjaxSetup(),typeof Turbo=="object"&&this.registerTurboRequestInterceptor()}registerVueRequestInterceptor(){Vue.http.interceptors.push((e,r)=>{this.socketId()&&e.headers.set("X-Socket-ID",this.socketId()),r()})}registerAxiosRequestInterceptor(){axios.interceptors.request.use(e=>(this.socketId()&&(e.headers["X-Socket-Id"]=this.socketId()),e))}registerjQueryAjaxSetup(){typeof jQuery.ajax<"u"&&jQuery.ajaxPrefilter((e,r,a)=>{this.socketId()&&a.setRequestHeader("X-Socket-Id",this.socketId())})}registerTurboRequestInterceptor(){document.addEventListener("turbo:before-fetch-request",e=>{e.detail.fetchOptions.headers["X-Socket-Id"]=this.socketId()})}}var hf={exports:{}};/*! - * 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=0;w<m.length;w++){var S=m.charCodeAt(w);if(S<128)y+=1;else if(S<2048)y+=2;else if(S<55296)y+=3;else if(S<=57343){if(w>=m.length-1)throw new Error(l);w++,y+=4}else throw new Error(l)}return y}a.encodedLength=f;function g(m){for(var y=[],w=0;w<m.length;w++){var S=m[w];if(S&128){var v=void 0;if(S<224){if(w>=m.length)throw new Error(c);var A=m[++w];if((A&192)!==128)throw new Error(c);S=(S&31)<<6|A&63,v=128}else if(S<240){if(w>=m.length-1)throw new Error(c);var A=m[++w],x=m[++w];if((A&192)!==128||(x&192)!==128)throw new Error(c);S=(S&15)<<12|(A&63)<<6|x&63,v=2048}else if(S<248){if(w>=m.length-2)throw new Error(c);var A=m[++w],x=m[++w],R=m[++w];if((A&192)!==128||(x&192)!==128||(R&192)!==128)throw new Error(c);S=(S&15)<<18|(A&63)<<12|(x&63)<<6|R&63,v=65536}else throw new Error(c);if(S<v||S>=55296&&S<=57343)throw new Error(c);if(S>=65536){if(S>1114111)throw new Error(c);S-=65536,y.push(String.fromCharCode(55296|S>>10)),S=56320|S&1023}}y.push(String.fromCharCode(S))}return y.join("")}a.decode=g}),(function(r,a,i){r.exports=i(3).default}),(function(r,a,i){i.r(a);class l{constructor(b,E){this.lastId=0,this.prefix=b,this.name=E}create(b){this.lastId++;var E=this.lastId,D=this.prefix+E,I=this.name+"["+E+"]",ie=!1,ye=function(){ie||(b.apply(null,arguments),ie=!0)};return this[E]=ye,{number:E,id:D,name:I,callback:ye}}remove(b){delete this[b.number]}}var c=new l("_pusher_script_","Pusher.ScriptReceivers"),d={VERSION:"8.4.0",PROTOCOL:7,wsPort:80,wssPort:443,wsPath:"",httpHost:"sockjs.pusher.com",httpPort:80,httpsPort:443,httpPath:"/pusher",stats_host:"stats.pusher.com",authEndpoint:"/pusher/auth",authTransport:"ajax",activityTimeout:12e4,pongTimeout:3e4,unavailableTimeout:1e4,userAuthentication:{endpoint:"/pusher/user-auth",transport:"ajax"},channelAuthorization:{endpoint:"/pusher/auth",transport:"ajax"},cdn_http:"http://js.pusher.com",cdn_https:"https://js.pusher.com",dependency_suffix:""},f=d;class g{constructor(b){this.options=b,this.receivers=b.receivers||c,this.loading={}}load(b,E,D){var I=this;if(I.loading[b]&&I.loading[b].length>0)I.loading[b].push(D);else{I.loading[b]=[D];var ie=Ve.createScriptRequest(I.getPath(b,E)),ye=I.receivers.create(function(Oe){if(I.receivers.remove(ye),I.loading[b]){var Fe=I.loading[b];delete I.loading[b];for(var at=function(Bt){Bt||ie.cleanup()},lt=0;lt<Fe.length;lt++)Fe[lt](Oe,at)}});ie.send(ye)}}getRoot(b){var E,D=Ve.getDocument().location.protocol;return b&&b.useTLS||D==="https:"?E=this.options.cdn_https:E=this.options.cdn_http,E.replace(/\/*$/,"")+"/"+this.options.version}getPath(b,E){return this.getRoot(E)+"/"+b+this.options.suffix+".js"}}var m=new l("_pusher_dependencies","Pusher.DependenciesReceivers"),y=new g({cdn_http:f.cdn_http,cdn_https:f.cdn_https,version:f.VERSION,suffix:f.dependency_suffix,receivers:m});const w={baseUrl:"https://pusher.com",urls:{authenticationEndpoint:{path:"/docs/channels/server_api/authenticating_users"},authorizationEndpoint:{path:"/docs/channels/server_api/authorizing-users/"},javascriptQuickStart:{path:"/docs/javascript_quick_start"},triggeringClientEvents:{path:"/docs/client_api_guide/client_events#trigger-events"},encryptedChannelSupport:{fullUrl:"https://github.com/pusher/pusher-js/tree/cc491015371a4bde5743d1c87a0fbac0feb53195#encrypted-channel-support"}}};var v={buildLogSuffix:function(_){const b="See:",E=w.urls[_];if(!E)return"";let D;return E.fullUrl?D=E.fullUrl:E.path&&(D=w.baseUrl+E.path),D?`${b} ${D}`:""}},A;(function(_){_.UserAuthentication="user-authentication",_.ChannelAuthorization="channel-authorization"})(A||(A={}));class x extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class R extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class M extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class O extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class L extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class q extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class G extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class B extends Error{constructor(b){super(b),Object.setPrototypeOf(this,new.target.prototype)}}class K extends Error{constructor(b,E){super(E),this.status=b,Object.setPrototypeOf(this,new.target.prototype)}}var Ce=function(_,b,E,D,I){const ie=Ve.createXHR();ie.open("POST",E.endpoint,!0),ie.setRequestHeader("Content-Type","application/x-www-form-urlencoded");for(var ye in E.headers)ie.setRequestHeader(ye,E.headers[ye]);if(E.headersProvider!=null){let Oe=E.headersProvider();for(var ye in Oe)ie.setRequestHeader(ye,Oe[ye])}return ie.onreadystatechange=function(){if(ie.readyState===4)if(ie.status===200){let Oe,Fe=!1;try{Oe=JSON.parse(ie.responseText),Fe=!0}catch{I(new K(200,`JSON returned from ${D.toString()} endpoint was invalid, yet status code was 200. Data was: ${ie.responseText}`),null)}Fe&&I(null,Oe)}else{let Oe="";switch(D){case A.UserAuthentication:Oe=v.buildLogSuffix("authenticationEndpoint");break;case A.ChannelAuthorization:Oe=`Clients must be authorized to join private or presence channels. ${v.buildLogSuffix("authorizationEndpoint")}`;break}I(new K(ie.status,`Unable to retrieve auth string from ${D.toString()} endpoint - received status: ${ie.status} from ${E.endpoint}. ${Oe}`),null)}},ie.send(b),ie};function ke(_){return he(wt(_))}var Ue=String.fromCharCode,Pe="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",qe=function(_){var b=_.charCodeAt(0);return b<128?_:b<2048?Ue(192|b>>>6)+Ue(128|b&63):Ue(224|b>>>12&15)+Ue(128|b>>>6&63)+Ue(128|b&63)},wt=function(_){return _.replace(/[^\x00-\x7F]/g,qe)},J=function(_){var b=[0,2,1][_.length%3],E=_.charCodeAt(0)<<16|(_.length>1?_.charCodeAt(1):0)<<8|(_.length>2?_.charCodeAt(2):0),D=[Pe.charAt(E>>>18),Pe.charAt(E>>>12&63),b>=2?"=":Pe.charAt(E>>>6&63),b>=1?"=":Pe.charAt(E&63)];return D.join("")},he=window.btoa||function(_){return _.replace(/[\s\S]{1,3}/g,J)};class Ee{constructor(b,E,D,I){this.clear=E,this.timer=b(()=>{this.timer&&(this.timer=I(this.timer))},D)}isRunning(){return this.timer!==null}ensureAborted(){this.timer&&(this.clear(this.timer),this.timer=null)}}var Z=Ee;function pe(_){window.clearTimeout(_)}function H(_){window.clearInterval(_)}class T extends Z{constructor(b,E){super(setTimeout,pe,b,function(D){return E(),null})}}class ge extends Z{constructor(b,E){super(setInterval,H,b,function(D){return E(),D})}}var de={now(){return Date.now?Date.now():new Date().valueOf()},defer(_){return new T(0,_)},method(_,...b){var E=Array.prototype.slice.call(arguments,1);return function(D){return D[_].apply(D,E.concat(arguments))}}},me=de;function ae(_,...b){for(var E=0;E<b.length;E++){var D=b[E];for(var I in D)D[I]&&D[I].constructor&&D[I].constructor===Object?_[I]=ae(_[I]||{},D[I]):_[I]=D[I]}return _}function Re(){for(var _=["Pusher"],b=0;b<arguments.length;b++)typeof arguments[b]=="string"?_.push(arguments[b]):_.push(Wn(arguments[b]));return _.join(" : ")}function ze(_,b){var E=Array.prototype.indexOf;if(_===null)return-1;if(E&&_.indexOf===E)return _.indexOf(b);for(var D=0,I=_.length;D<I;D++)if(_[D]===b)return D;return-1}function $e(_,b){for(var E in _)Object.prototype.hasOwnProperty.call(_,E)&&b(_[E],E,_)}function Ge(_){var b=[];return $e(_,function(E,D){b.push(D)}),b}function At(_){var b=[];return $e(_,function(E){b.push(E)}),b}function yt(_,b,E){for(var D=0;D<_.length;D++)b.call(E||window,_[D],D,_)}function dn(_,b){for(var E=[],D=0;D<_.length;D++)E.push(b(_[D],D,_,E));return E}function Tt(_,b){var E={};return $e(_,function(D,I){E[I]=b(D)}),E}function Yt(_,b){b=b||function(I){return!!I};for(var E=[],D=0;D<_.length;D++)b(_[D],D,_,E)&&E.push(_[D]);return E}function ln(_,b){var E={};return $e(_,function(D,I){(b&&b(D,I,_,E)||D)&&(E[I]=D)}),E}function zt(_){var b=[];return $e(_,function(E,D){b.push([D,E])}),b}function Tr(_,b){for(var E=0;E<_.length;E++)if(b(_[E],E,_))return!0;return!1}function wn(_,b){for(var E=0;E<_.length;E++)if(!b(_[E],E,_))return!1;return!0}function cn(_){return Tt(_,function(b){return typeof b=="object"&&(b=Wn(b)),encodeURIComponent(ke(b.toString()))})}function fn(_){var b=ln(_,function(D){return D!==void 0}),E=dn(zt(cn(b)),me.method("join","=")).join("&");return E}function sr(_){var b=[],E=[];return(function D(I,ie){var ye,Oe,Fe;switch(typeof I){case"object":if(!I)return null;for(ye=0;ye<b.length;ye+=1)if(b[ye]===I)return{$ref:E[ye]};if(b.push(I),E.push(ie),Object.prototype.toString.apply(I)==="[object Array]")for(Fe=[],ye=0;ye<I.length;ye+=1)Fe[ye]=D(I[ye],ie+"["+ye+"]");else{Fe={};for(Oe in I)Object.prototype.hasOwnProperty.call(I,Oe)&&(Fe[Oe]=D(I[Oe],ie+"["+JSON.stringify(Oe)+"]"))}return Fe;case"number":case"string":case"boolean":return I}})(_,"$")}function Wn(_){try{return JSON.stringify(_)}catch{return JSON.stringify(sr(_))}}class Ut{constructor(){this.globalLog=b=>{window.console&&window.console.log&&window.console.log(b)}}debug(...b){this.log(this.globalLog,b)}warn(...b){this.log(this.globalLogWarn,b)}error(...b){this.log(this.globalLogError,b)}globalLogWarn(b){window.console&&window.console.warn?window.console.warn(b):this.globalLog(b)}globalLogError(b){window.console&&window.console.error?window.console.error(b):this.globalLogWarn(b)}log(b,...E){var D=Re.apply(this,arguments);Jo.log?Jo.log(D):Jo.logToConsole&&b.bind(this)(D)}}var bt=new Ut,ra=function(_,b,E,D,I){(E.headers!==void 0||E.headersProvider!=null)&&bt.warn(`To send headers with the ${D.toString()} request, you must use AJAX, rather than JSONP.`);var ie=_.nextAuthCallbackID.toString();_.nextAuthCallbackID++;var ye=_.getDocument(),Oe=ye.createElement("script");_.auth_callbacks[ie]=function(lt){I(null,lt)};var Fe="Pusher.auth_callbacks['"+ie+"']";Oe.src=E.endpoint+"?callback="+encodeURIComponent(Fe)+"&"+b;var at=ye.getElementsByTagName("head")[0]||ye.documentElement;at.insertBefore(Oe,at.firstChild)},Ln=ra;class An{constructor(b){this.src=b}send(b){var E=this,D="Error loading "+E.src;E.script=document.createElement("script"),E.script.id=b.id,E.script.src=E.src,E.script.type="text/javascript",E.script.charset="UTF-8",E.script.addEventListener?(E.script.onerror=function(){b.callback(D)},E.script.onload=function(){b.callback(null)}):E.script.onreadystatechange=function(){(E.script.readyState==="loaded"||E.script.readyState==="complete")&&b.callback(null)},E.script.async===void 0&&document.attachEvent&&/opera/i.test(navigator.userAgent)?(E.errorScript=document.createElement("script"),E.errorScript.id=b.id+"_error",E.errorScript.text=b.name+"('"+D+"');",E.script.async=E.errorScript.async=!1):E.script.async=!0;var I=document.getElementsByTagName("head")[0];I.insertBefore(E.script,I.firstChild),E.errorScript&&I.insertBefore(E.errorScript,E.script.nextSibling)}cleanup(){this.script&&(this.script.onload=this.script.onerror=null,this.script.onreadystatechange=null),this.script&&this.script.parentNode&&this.script.parentNode.removeChild(this.script),this.errorScript&&this.errorScript.parentNode&&this.errorScript.parentNode.removeChild(this.errorScript),this.script=null,this.errorScript=null}}class xn{constructor(b,E){this.url=b,this.data=E}send(b){if(!this.request){var E=fn(this.data),D=this.url+"/"+b.number+"?"+E;this.request=Ve.createScriptRequest(D),this.request.send(b)}}cleanup(){this.request&&this.request.cleanup()}}var Mn=function(_,b){return function(E,D){var I="http"+(b?"s":"")+"://",ie=I+(_.host||_.options.host)+_.options.path,ye=Ve.createJSONPRequest(ie,E),Oe=Ve.ScriptReceivers.create(function(Fe,at){c.remove(Oe),ye.cleanup(),at&&at.host&&(_.host=at.host),D&&D(Fe,at)});ye.send(Oe)}},hn={name:"jsonp",getAgent:Mn},aa=hn;function $n(_,b,E){var D=_+(b.useTLS?"s":""),I=b.useTLS?b.hostTLS:b.hostNonTLS;return D+"://"+I+E}function Vt(_,b){var E="/app/"+_,D="?protocol="+f.PROTOCOL+"&client=js&version="+f.VERSION+(b?"&"+b:"");return E+D}var Cn={getInitial:function(_,b){var E=(b.httpPath||"")+Vt(_,"flash=false");return $n("ws",b,E)}},nn={getInitial:function(_,b){var E=(b.httpPath||"/pusher")+Vt(_);return $n("http",b,E)}},V={getInitial:function(_,b){return $n("http",b,b.httpPath||"/pusher")},getPath:function(_,b){return Vt(_)}};class oe{constructor(){this._callbacks={}}get(b){return this._callbacks[ce(b)]}add(b,E,D){var I=ce(b);this._callbacks[I]=this._callbacks[I]||[],this._callbacks[I].push({fn:E,context:D})}remove(b,E,D){if(!b&&!E&&!D){this._callbacks={};return}var I=b?[ce(b)]:Ge(this._callbacks);E||D?this.removeCallback(I,E,D):this.removeAllCallbacks(I)}removeCallback(b,E,D){yt(b,function(I){this._callbacks[I]=Yt(this._callbacks[I]||[],function(ie){return E&&E!==ie.fn||D&&D!==ie.context}),this._callbacks[I].length===0&&delete this._callbacks[I]},this)}removeAllCallbacks(b){yt(b,function(E){delete this._callbacks[E]},this)}}function ce(_){return"_"+_}class X{constructor(b){this.callbacks=new oe,this.global_callbacks=[],this.failThrough=b}bind(b,E,D){return this.callbacks.add(b,E,D),this}bind_global(b){return this.global_callbacks.push(b),this}unbind(b,E,D){return this.callbacks.remove(b,E,D),this}unbind_global(b){return b?(this.global_callbacks=Yt(this.global_callbacks||[],E=>E!==b),this):(this.global_callbacks=[],this)}unbind_all(){return this.unbind(),this.unbind_global(),this}emit(b,E,D){for(var I=0;I<this.global_callbacks.length;I++)this.global_callbacks[I](b,E);var ie=this.callbacks.get(b),ye=[];if(D?ye.push(E,D):E&&ye.push(E),ie&&ie.length>0)for(var I=0;I<ie.length;I++)ie[I].fn.apply(ie[I].context||window,ye);else this.failThrough&&this.failThrough(b,E);return this}}class ve extends X{constructor(b,E,D,I,ie){super(),this.initialize=Ve.transportConnectionInitializer,this.hooks=b,this.name=E,this.priority=D,this.key=I,this.options=ie,this.state="new",this.timeline=ie.timeline,this.activityTimeout=ie.activityTimeout,this.id=this.timeline.generateUniqueID()}handlesActivityChecks(){return!!this.hooks.handlesActivityChecks}supportsPing(){return!!this.hooks.supportsPing}connect(){if(this.socket||this.state!=="initialized")return!1;var b=this.hooks.urls.getInitial(this.key,this.options);try{this.socket=this.hooks.getSocket(b,this.options)}catch(E){return me.defer(()=>{this.onError(E),this.changeState("closed")}),!1}return this.bindListeners(),bt.debug("Connecting",{transport:this.name,url:b}),this.changeState("connecting"),!0}close(){return this.socket?(this.socket.close(),!0):!1}send(b){return this.state==="open"?(me.defer(()=>{this.socket&&this.socket.send(b)}),!0):!1}ping(){this.state==="open"&&this.supportsPing()&&this.socket.ping()}onOpen(){this.hooks.beforeOpen&&this.hooks.beforeOpen(this.socket,this.hooks.urls.getPath(this.key,this.options)),this.changeState("open"),this.socket.onopen=void 0}onError(b){this.emit("error",{type:"WebSocketError",error:b}),this.timeline.error(this.buildTimelineMessage({error:b.toString()}))}onClose(b){b?this.changeState("closed",{code:b.code,reason:b.reason,wasClean:b.wasClean}):this.changeState("closed"),this.unbindListeners(),this.socket=void 0}onMessage(b){this.emit("message",b)}onActivity(){this.emit("activity")}bindListeners(){this.socket.onopen=()=>{this.onOpen()},this.socket.onerror=b=>{this.onError(b)},this.socket.onclose=b=>{this.onClose(b)},this.socket.onmessage=b=>{this.onMessage(b)},this.supportsPing()&&(this.socket.onactivity=()=>{this.onActivity()})}unbindListeners(){this.socket&&(this.socket.onopen=void 0,this.socket.onerror=void 0,this.socket.onclose=void 0,this.socket.onmessage=void 0,this.supportsPing()&&(this.socket.onactivity=void 0))}changeState(b,E){this.state=b,this.timeline.info(this.buildTimelineMessage({state:b,params:E})),this.emit(b,E)}buildTimelineMessage(b){return ae({cid:this.id},b)}}class ue{constructor(b){this.hooks=b}isSupported(b){return this.hooks.isSupported(b)}createConnection(b,E,D,I){return new ve(this.hooks,b,E,D,I)}}var we=new ue({urls:Cn,handlesActivityChecks:!1,supportsPing:!1,isInitialized:function(){return!!Ve.getWebSocketAPI()},isSupported:function(){return!!Ve.getWebSocketAPI()},getSocket:function(_){return Ve.createWebSocket(_)}}),_e={urls:nn,handlesActivityChecks:!1,supportsPing:!0,isInitialized:function(){return!0}},Be=ae({getSocket:function(_){return Ve.HTTPFactory.createStreamingSocket(_)}},_e),xe=ae({getSocket:function(_){return Ve.HTTPFactory.createPollingSocket(_)}},_e),De={isSupported:function(){return Ve.isXHRSupported()}},Ae=new ue(ae({},Be,De)),et=new ue(ae({},xe,De)),Ne={ws:we,xhr_streaming:Ae,xhr_polling:et},je=Ne,Se=new ue({file:"sockjs",urls:V,handlesActivityChecks:!0,supportsPing:!1,isSupported:function(){return!0},isInitialized:function(){return window.SockJS!==void 0},getSocket:function(_,b){return new window.SockJS(_,null,{js_path:y.getPath("sockjs",{useTLS:b.useTLS}),ignore_null_origin:b.ignoreNullOrigin})},beforeOpen:function(_,b){_.send(JSON.stringify({path:b}))}}),Me={isSupported:function(_){var b=Ve.isXDRSupported(_.useTLS);return b}},Ze=new ue(ae({},Be,Me)),kt=new ue(ae({},xe,Me));je.xdr_streaming=Ze,je.xdr_polling=kt,je.sockjs=Se;var Ot=je;class Nn extends X{constructor(){super();var b=this;window.addEventListener!==void 0&&(window.addEventListener("online",function(){b.emit("online")},!1),window.addEventListener("offline",function(){b.emit("offline")},!1))}isOnline(){return window.navigator.onLine===void 0?!0:window.navigator.onLine}}var $t=new Nn;class Nt{constructor(b,E,D){this.manager=b,this.transport=E,this.minPingDelay=D.minPingDelay,this.maxPingDelay=D.maxPingDelay,this.pingDelay=void 0}createConnection(b,E,D,I){I=ae({},I,{activityTimeout:this.pingDelay});var ie=this.transport.createConnection(b,E,D,I),ye=null,Oe=function(){ie.unbind("open",Oe),ie.bind("closed",Fe),ye=me.now()},Fe=at=>{if(ie.unbind("closed",Fe),at.code===1002||at.code===1003)this.manager.reportDeath();else if(!at.wasClean&&ye){var lt=me.now()-ye;lt<2*this.maxPingDelay&&(this.manager.reportDeath(),this.pingDelay=Math.max(lt/2,this.minPingDelay))}};return ie.bind("open",Oe),ie}isSupported(b){return this.manager.isAlive()&&this.transport.isSupported(b)}}const gn={decodeMessage:function(_){try{var b=JSON.parse(_.data),E=b.data;if(typeof E=="string")try{E=JSON.parse(b.data)}catch{}var D={event:b.event,channel:b.channel,data:E};return b.user_id&&(D.user_id=b.user_id),D}catch(I){throw{type:"MessageParseError",error:I,data:_.data}}},encodeMessage:function(_){return JSON.stringify(_)},processHandshake:function(_){var b=gn.decodeMessage(_);if(b.event==="pusher:connection_established"){if(!b.data.activity_timeout)throw"No activity timeout specified in handshake";return{action:"connected",id:b.data.socket_id,activityTimeout:b.data.activity_timeout*1e3}}else{if(b.event==="pusher:error")return{action:this.getCloseAction(b.data),error:this.getCloseError(b.data)};throw"Invalid handshake"}},getCloseAction:function(_){return _.code<4e3?_.code>=1002&&_.code<=1004?"backoff":null:_.code===4e3?"tls_only":_.code<4100?"refused":_.code<4200?"backoff":_.code<4300?"retry":"refused"},getCloseError:function(_){return _.code!==1e3&&_.code!==1001?{type:"PusherError",data:{code:_.code,message:_.reason||_.message}}:null}};var In=gn;class No extends X{constructor(b,E){super(),this.id=b,this.transport=E,this.activityTimeout=E.activityTimeout,this.bindListeners()}handlesActivityChecks(){return this.transport.handlesActivityChecks()}send(b){return this.transport.send(b)}send_event(b,E,D){var I={event:b,data:E};return D&&(I.channel=D),bt.debug("Event sent",I),this.send(In.encodeMessage(I))}ping(){this.transport.supportsPing()?this.transport.ping():this.send_event("pusher:ping",{})}close(){this.transport.close()}bindListeners(){var b={message:D=>{var I;try{I=In.decodeMessage(D)}catch(ie){this.emit("error",{type:"MessageParseError",error:ie,data:D.data})}if(I!==void 0){switch(bt.debug("Event recd",I),I.event){case"pusher:error":this.emit("error",{type:"PusherError",data:I.data});break;case"pusher:ping":this.emit("ping");break;case"pusher:pong":this.emit("pong");break}this.emit("message",I)}},activity:()=>{this.emit("activity")},error:D=>{this.emit("error",D)},closed:D=>{E(),D&&D.code&&this.handleCloseEvent(D),this.transport=null,this.emit("closed")}},E=()=>{$e(b,(D,I)=>{this.transport.unbind(I,D)})};$e(b,(D,I)=>{this.transport.bind(I,D)})}handleCloseEvent(b){var E=In.getCloseAction(b),D=In.getCloseError(b);D&&this.emit("error",D),E&&this.emit(E,{action:E,error:D})}}class Sf{constructor(b,E){this.transport=b,this.callback=E,this.bindListeners()}close(){this.unbindListeners(),this.transport.close()}bindListeners(){this.onMessage=b=>{this.unbindListeners();var E;try{E=In.processHandshake(b)}catch(D){this.finish("error",{error:D}),this.transport.close();return}E.action==="connected"?this.finish("connected",{connection:new No(E.id,this.transport),activityTimeout:E.activityTimeout}):(this.finish(E.action,{error:E.error}),this.transport.close())},this.onClosed=b=>{this.unbindListeners();var E=In.getCloseAction(b)||"backoff",D=In.getCloseError(b);this.finish(E,{error:D})},this.transport.bind("message",this.onMessage),this.transport.bind("closed",this.onClosed)}unbindListeners(){this.transport.unbind("message",this.onMessage),this.transport.unbind("closed",this.onClosed)}finish(b,E){this.callback(ae({transport:this.transport,action:b},E))}}class wf{constructor(b,E){this.timeline=b,this.options=E||{}}send(b,E){this.timeline.isEmpty()||this.timeline.send(Ve.TimelineTransport.getAgent(this,b),E)}}class hs extends X{constructor(b,E){super(function(D,I){bt.debug("No callbacks on "+b+" for "+D)}),this.name=b,this.pusher=E,this.subscribed=!1,this.subscriptionPending=!1,this.subscriptionCancelled=!1}authorize(b,E){return E(null,{auth:""})}trigger(b,E){if(b.indexOf("client-")!==0)throw new x("Event '"+b+"' does not start with 'client-'");if(!this.subscribed){var D=v.buildLogSuffix("triggeringClientEvents");bt.warn(`Client event triggered before channel 'subscription_succeeded' event . ${D}`)}return this.pusher.send_event(b,E,this.name)}disconnect(){this.subscribed=!1,this.subscriptionPending=!1}handleEvent(b){var E=b.event,D=b.data;if(E==="pusher_internal:subscription_succeeded")this.handleSubscriptionSucceededEvent(b);else if(E==="pusher_internal:subscription_count")this.handleSubscriptionCountEvent(b);else if(E.indexOf("pusher_internal:")!==0){var I={};this.emit(E,D,I)}}handleSubscriptionSucceededEvent(b){this.subscriptionPending=!1,this.subscribed=!0,this.subscriptionCancelled?this.pusher.unsubscribe(this.name):this.emit("pusher:subscription_succeeded",b.data)}handleSubscriptionCountEvent(b){b.data.subscription_count&&(this.subscriptionCount=b.data.subscription_count),this.emit("pusher:subscription_count",b.data)}subscribe(){this.subscribed||(this.subscriptionPending=!0,this.subscriptionCancelled=!1,this.authorize(this.pusher.connection.socket_id,(b,E)=>{b?(this.subscriptionPending=!1,bt.error(b.toString()),this.emit("pusher:subscription_error",Object.assign({},{type:"AuthError",error:b.message},b instanceof K?{status:b.status}:{}))):this.pusher.send_event("pusher:subscribe",{auth:E.auth,channel_data:E.channel_data,channel:this.name})}))}unsubscribe(){this.subscribed=!1,this.pusher.send_event("pusher:unsubscribe",{channel:this.name})}cancelSubscription(){this.subscriptionCancelled=!0}reinstateSubscription(){this.subscriptionCancelled=!1}}class ia extends hs{authorize(b,E){return this.pusher.config.channelAuthorizer({channelName:this.name,socketId:b},E)}}class Io{constructor(){this.reset()}get(b){return Object.prototype.hasOwnProperty.call(this.members,b)?{id:b,info:this.members[b]}:null}each(b){$e(this.members,(E,D)=>{b(this.get(D))})}setMyID(b){this.myID=b}onSubscription(b){this.members=b.presence.hash,this.count=b.presence.count,this.me=this.get(this.myID)}addMember(b){return this.get(b.user_id)===null&&this.count++,this.members[b.user_id]=b.user_info,this.get(b.user_id)}removeMember(b){var E=this.get(b.user_id);return E&&(delete this.members[b.user_id],this.count--),E}reset(){this.members={},this.count=0,this.myID=null,this.me=null}}var Ia=function(_,b,E,D){function I(ie){return ie instanceof E?ie:new E(function(ye){ye(ie)})}return new(E||(E=Promise))(function(ie,ye){function Oe(lt){try{at(D.next(lt))}catch(Bt){ye(Bt)}}function Fe(lt){try{at(D.throw(lt))}catch(Bt){ye(Bt)}}function at(lt){lt.done?ie(lt.value):I(lt.value).then(Oe,Fe)}at((D=D.apply(_,b||[])).next())})};class Ha extends ia{constructor(b,E){super(b,E),this.members=new Io}authorize(b,E){super.authorize(b,(D,I)=>Ia(this,void 0,void 0,function*(){if(!D)if(I=I,I.channel_data!=null){var ie=JSON.parse(I.channel_data);this.members.setMyID(ie.user_id)}else if(yield this.pusher.user.signinDonePromise,this.pusher.user.user_data!=null)this.members.setMyID(this.pusher.user.user_data.id);else{let ye=v.buildLogSuffix("authorizationEndpoint");bt.error(`Invalid auth response for channel '${this.name}', expected 'channel_data' field. ${ye}, or the user should be signed in.`),E("Invalid auth response");return}E(D,I)}))}handleEvent(b){var E=b.event;if(E.indexOf("pusher_internal:")===0)this.handleInternalEvent(b);else{var D=b.data,I={};b.user_id&&(I.user_id=b.user_id),this.emit(E,D,I)}}handleInternalEvent(b){var E=b.event,D=b.data;switch(E){case"pusher_internal:subscription_succeeded":this.handleSubscriptionSucceededEvent(b);break;case"pusher_internal:subscription_count":this.handleSubscriptionCountEvent(b);break;case"pusher_internal:member_added":var I=this.members.addMember(D);this.emit("pusher:member_added",I);break;case"pusher_internal:member_removed":var ie=this.members.removeMember(D);ie&&this.emit("pusher:member_removed",ie);break}}handleSubscriptionSucceededEvent(b){this.subscriptionPending=!1,this.subscribed=!0,this.subscriptionCancelled?this.pusher.unsubscribe(this.name):(this.members.onSubscription(b.data),this.emit("pusher:subscription_succeeded",this.members))}disconnect(){this.members.reset(),super.disconnect()}}var _i=i(1),qr=i(0);class Xt extends ia{constructor(b,E,D){super(b,E),this.key=null,this.nacl=D}authorize(b,E){super.authorize(b,(D,I)=>{if(D){E(D,I);return}let ie=I.shared_secret;if(!ie){E(new Error(`No shared_secret key in auth payload for encrypted channel: ${this.name}`),null);return}this.key=Object(qr.decode)(ie),delete I.shared_secret,E(null,I)})}trigger(b,E){throw new q("Client events are not currently supported for encrypted channels")}handleEvent(b){var E=b.event,D=b.data;if(E.indexOf("pusher_internal:")===0||E.indexOf("pusher:")===0){super.handleEvent(b);return}this.handleEncryptedEvent(E,D)}handleEncryptedEvent(b,E){if(!this.key){bt.debug("Received encrypted event before key has been retrieved from the authEndpoint");return}if(!E.ciphertext||!E.nonce){bt.error("Unexpected format for encrypted event, expected object with `ciphertext` and `nonce` fields, got: "+E);return}let D=Object(qr.decode)(E.ciphertext);if(D.length<this.nacl.secretbox.overheadLength){bt.error(`Expected encrypted event ciphertext length to be ${this.nacl.secretbox.overheadLength}, got: ${D.length}`);return}let I=Object(qr.decode)(E.nonce);if(I.length<this.nacl.secretbox.nonceLength){bt.error(`Expected encrypted event nonce length to be ${this.nacl.secretbox.nonceLength}, got: ${I.length}`);return}let ie=this.nacl.secretbox.open(D,I,this.key);if(ie===null){bt.debug("Failed to decrypt an event, probably because it was encrypted with a different key. Fetching a new key from the authEndpoint..."),this.authorize(this.pusher.connection.socket_id,(ye,Oe)=>{if(ye){bt.error(`Failed to make a request to the authEndpoint: ${Oe}. Unable to fetch new key, so dropping encrypted event`);return}if(ie=this.nacl.secretbox.open(D,I,this.key),ie===null){bt.error("Failed to decrypt event with new key. Dropping encrypted event");return}this.emit(b,this.getDataToEmit(ie))});return}this.emit(b,this.getDataToEmit(ie))}getDataToEmit(b){let E=Object(_i.decode)(b);try{return JSON.parse(E)}catch{return E}}}class Mc extends X{constructor(b,E){super(),this.state="initialized",this.connection=null,this.key=b,this.options=E,this.timeline=this.options.timeline,this.usingTLS=this.options.useTLS,this.errorCallbacks=this.buildErrorCallbacks(),this.connectionCallbacks=this.buildConnectionCallbacks(this.errorCallbacks),this.handshakeCallbacks=this.buildHandshakeCallbacks(this.errorCallbacks);var D=Ve.getNetwork();D.bind("online",()=>{this.timeline.info({netinfo:"online"}),(this.state==="connecting"||this.state==="unavailable")&&this.retryIn(0)}),D.bind("offline",()=>{this.timeline.info({netinfo:"offline"}),this.connection&&this.sendActivityCheck()}),this.updateStrategy()}connect(){if(!(this.connection||this.runner)){if(!this.strategy.isSupported()){this.updateState("failed");return}this.updateState("connecting"),this.startConnecting(),this.setUnavailableTimer()}}send(b){return this.connection?this.connection.send(b):!1}send_event(b,E,D){return this.connection?this.connection.send_event(b,E,D):!1}disconnect(){this.disconnectInternally(),this.updateState("disconnected")}isUsingTLS(){return this.usingTLS}startConnecting(){var b=(E,D)=>{E?this.runner=this.strategy.connect(0,b):D.action==="error"?(this.emit("error",{type:"HandshakeError",error:D.error}),this.timeline.error({handshakeError:D.error})):(this.abortConnecting(),this.handshakeCallbacks[D.action](D))};this.runner=this.strategy.connect(0,b)}abortConnecting(){this.runner&&(this.runner.abort(),this.runner=null)}disconnectInternally(){if(this.abortConnecting(),this.clearRetryTimer(),this.clearUnavailableTimer(),this.connection){var b=this.abandonConnection();b.close()}}updateStrategy(){this.strategy=this.options.getStrategy({key:this.key,timeline:this.timeline,useTLS:this.usingTLS})}retryIn(b){this.timeline.info({action:"retry",delay:b}),b>0&&this.emit("connecting_in",Math.round(b/1e3)),this.retryTimer=new T(b||0,()=>{this.disconnectInternally(),this.connect()})}clearRetryTimer(){this.retryTimer&&(this.retryTimer.ensureAborted(),this.retryTimer=null)}setUnavailableTimer(){this.unavailableTimer=new T(this.options.unavailableTimeout,()=>{this.updateState("unavailable")})}clearUnavailableTimer(){this.unavailableTimer&&this.unavailableTimer.ensureAborted()}sendActivityCheck(){this.stopActivityCheck(),this.connection.ping(),this.activityTimer=new T(this.options.pongTimeout,()=>{this.timeline.error({pong_timed_out:this.options.pongTimeout}),this.retryIn(0)})}resetActivityCheck(){this.stopActivityCheck(),this.connection&&!this.connection.handlesActivityChecks()&&(this.activityTimer=new T(this.activityTimeout,()=>{this.sendActivityCheck()}))}stopActivityCheck(){this.activityTimer&&this.activityTimer.ensureAborted()}buildConnectionCallbacks(b){return ae({},b,{message:E=>{this.resetActivityCheck(),this.emit("message",E)},ping:()=>{this.send_event("pusher:pong",{})},activity:()=>{this.resetActivityCheck()},error:E=>{this.emit("error",E)},closed:()=>{this.abandonConnection(),this.shouldRetry()&&this.retryIn(1e3)}})}buildHandshakeCallbacks(b){return ae({},b,{connected:E=>{this.activityTimeout=Math.min(this.options.activityTimeout,E.activityTimeout,E.connection.activityTimeout||1/0),this.clearUnavailableTimer(),this.setConnection(E.connection),this.socket_id=this.connection.id,this.updateState("connected",{socket_id:this.socket_id})}})}buildErrorCallbacks(){let b=E=>D=>{D.error&&this.emit("error",{type:"WebSocketError",error:D.error}),E(D)};return{tls_only:b(()=>{this.usingTLS=!0,this.updateStrategy(),this.retryIn(0)}),refused:b(()=>{this.disconnect()}),backoff:b(()=>{this.retryIn(1e3)}),retry:b(()=>{this.retryIn(0)})}}setConnection(b){this.connection=b;for(var E in this.connectionCallbacks)this.connection.bind(E,this.connectionCallbacks[E]);this.resetActivityCheck()}abandonConnection(){if(this.connection){this.stopActivityCheck();for(var b in this.connectionCallbacks)this.connection.unbind(b,this.connectionCallbacks[b]);var E=this.connection;return this.connection=null,E}}updateState(b,E){var D=this.state;if(this.state=b,D!==b){var I=b;I==="connected"&&(I+=" with new socket ID "+E.socket_id),bt.debug("State changed",D+" -> "+I),this.timeline.info({state:b,params:E}),this.emit("state_change",{previous:D,current:b}),this.emit(b,E)}}shouldRetry(){return this.state==="connecting"||this.state==="connected"}}class $c{constructor(){this.channels={}}add(b,E){return this.channels[b]||(this.channels[b]=sa(b,E)),this.channels[b]}all(){return At(this.channels)}find(b){return this.channels[b]}remove(b){var E=this.channels[b];return delete this.channels[b],E}disconnect(){$e(this.channels,function(b){b.disconnect()})}}function sa(_,b){if(_.indexOf("private-encrypted-")===0){if(b.config.nacl)return kr.createEncryptedChannel(_,b,b.config.nacl);let E="Tried to subscribe to a private-encrypted- channel but no nacl implementation available",D=v.buildLogSuffix("encryptedChannelSupport");throw new q(`${E}. ${D}`)}else{if(_.indexOf("private-")===0)return kr.createPrivateChannel(_,b);if(_.indexOf("presence-")===0)return kr.createPresenceChannel(_,b);if(_.indexOf("#")===0)throw new R('Cannot create a channel with name "'+_+'".');return kr.createChannel(_,b)}}var ja={createChannels(){return new $c},createConnectionManager(_,b){return new Mc(_,b)},createChannel(_,b){return new hs(_,b)},createPrivateChannel(_,b){return new ia(_,b)},createPresenceChannel(_,b){return new Ha(_,b)},createEncryptedChannel(_,b,E){return new Xt(_,b,E)},createTimelineSender(_,b){return new wf(_,b)},createHandshake(_,b){return new Sf(_,b)},createAssistantToTheTransportManager(_,b,E){return new Nt(_,b,E)}},kr=ja;class Ho{constructor(b){this.options=b||{},this.livesLeft=this.options.lives||1/0}getAssistant(b){return kr.createAssistantToTheTransportManager(this,b,{minPingDelay:this.options.minPingDelay,maxPingDelay:this.options.maxPingDelay})}isAlive(){return this.livesLeft>0}reportDeath(){this.livesLeft-=1}}class Gr{constructor(b,E){this.strategies=b,this.loop=!!E.loop,this.failFast=!!E.failFast,this.timeout=E.timeout,this.timeoutLimit=E.timeoutLimit}isSupported(){return Tr(this.strategies,me.method("isSupported"))}connect(b,E){var D=this.strategies,I=0,ie=this.timeout,ye=null,Oe=(Fe,at)=>{at?E(null,at):(I=I+1,this.loop&&(I=I%D.length),I<D.length?(ie&&(ie=ie*2,this.timeoutLimit&&(ie=Math.min(ie,this.timeoutLimit))),ye=this.tryStrategy(D[I],b,{timeout:ie,failFast:this.failFast},Oe)):E(!0))};return ye=this.tryStrategy(D[I],b,{timeout:ie,failFast:this.failFast},Oe),{abort:function(){ye.abort()},forceMinPriority:function(Fe){b=Fe,ye&&ye.forceMinPriority(Fe)}}}tryStrategy(b,E,D,I){var ie=null,ye=null;return D.timeout>0&&(ie=new T(D.timeout,function(){ye.abort(),I(!0)})),ye=b.connect(E,function(Oe,Fe){Oe&&ie&&ie.isRunning()&&!D.failFast||(ie&&ie.ensureAborted(),I(Oe,Fe))}),{abort:function(){ie&&ie.ensureAborted(),ye.abort()},forceMinPriority:function(Oe){ye.forceMinPriority(Oe)}}}}class jo{constructor(b){this.strategies=b}isSupported(){return Tr(this.strategies,me.method("isSupported"))}connect(b,E){return gs(this.strategies,b,function(D,I){return function(ie,ye){if(I[D].error=ie,ie){ps(I)&&E(!0);return}yt(I,function(Oe){Oe.forceMinPriority(ye.transport.priority)}),E(null,ye)}})}}function gs(_,b,E){var D=dn(_,function(I,ie,ye,Oe){return I.connect(b,E(ie,Oe))});return{abort:function(){yt(D,Rr)},forceMinPriority:function(I){yt(D,function(ie){ie.forceMinPriority(I)})}}}function ps(_){return wn(_,function(b){return!!b.error})}function Rr(_){!_.error&&!_.aborted&&(_.abort(),_.aborted=!0)}class Hn{constructor(b,E,D){this.strategy=b,this.transports=E,this.ttl=D.ttl||1800*1e3,this.usingTLS=D.useTLS,this.timeline=D.timeline}isSupported(){return this.strategy.isSupported()}connect(b,E){var D=this.usingTLS,I=Cf(D),ie=I&&I.cacheSkipCount?I.cacheSkipCount:0,ye=[this.strategy];if(I&&I.timestamp+this.ttl>=me.now()){var Oe=this.transports[I.transport];Oe&&(["ws","wss"].includes(I.transport)||ie>3?(this.timeline.info({cached:!0,transport:I.transport,latency:I.latency}),ye.push(new Gr([Oe],{timeout:I.latency*2+1e3,failFast:!0}))):ie++)}var Fe=me.now(),at=ye.pop().connect(b,function lt(Bt,Ri){Bt?(Uo(D),ye.length>0?(Fe=me.now(),at=ye.pop().connect(b,lt)):E(Bt)):(zo(D,Ri.transport.name,me.now()-Fe,ie),E(null,Ri))});return{abort:function(){at.abort()},forceMinPriority:function(lt){b=lt,at&&at.forceMinPriority(lt)}}}}function ms(_){return"pusherTransport"+(_?"TLS":"NonTLS")}function Cf(_){var b=Ve.getLocalStorage();if(b)try{var E=b[ms(_)];if(E)return JSON.parse(E)}catch{Uo(_)}return null}function zo(_,b,E,D){var I=Ve.getLocalStorage();if(I)try{I[ms(_)]=Wn({timestamp:me.now(),transport:b,latency:E,cacheSkipCount:D})}catch{}}function Uo(_){var b=Ve.getLocalStorage();if(b)try{delete b[ms(_)]}catch{}}class oa{constructor(b,{delay:E}){this.strategy=b,this.options={delay:E}}isSupported(){return this.strategy.isSupported()}connect(b,E){var D=this.strategy,I,ie=new T(this.options.delay,function(){I=D.connect(b,E)});return{abort:function(){ie.ensureAborted(),I&&I.abort()},forceMinPriority:function(ye){b=ye,I&&I.forceMinPriority(ye)}}}}class Ai{constructor(b,E,D){this.test=b,this.trueBranch=E,this.falseBranch=D}isSupported(){var b=this.test()?this.trueBranch:this.falseBranch;return b.isSupported()}connect(b,E){var D=this.test()?this.trueBranch:this.falseBranch;return D.connect(b,E)}}class jn{constructor(b){this.strategy=b}isSupported(){return this.strategy.isSupported()}connect(b,E){var D=this.strategy.connect(b,function(I,ie){ie&&D.abort(),E(I,ie)});return D}}function la(_){return function(){return _.isSupported()}}var Nc=function(_,b,E){var D={};function I(Di,Kc,Wc,rl,al){var il=E(_,Di,Kc,Wc,rl,al);return D[Di]=il,il}var ie=Object.assign({},b,{hostNonTLS:_.wsHost+":"+_.wsPort,hostTLS:_.wsHost+":"+_.wssPort,httpPath:_.wsPath}),ye=Object.assign({},ie,{useTLS:!0}),Oe=Object.assign({},b,{hostNonTLS:_.httpHost+":"+_.httpPort,hostTLS:_.httpHost+":"+_.httpsPort,httpPath:_.httpPath}),Fe={loop:!0,timeout:15e3,timeoutLimit:6e4},at=new Ho({minPingDelay:1e4,maxPingDelay:_.activityTimeout}),lt=new Ho({lives:2,minPingDelay:1e4,maxPingDelay:_.activityTimeout}),Bt=I("ws","ws",3,ie,at),Ri=I("wss","ws",3,ye,at),Gc=I("sockjs","sockjs",1,Oe),Qo=I("xhr_streaming","xhr_streaming",1,Oe,lt),Vc=I("xdr_streaming","xdr_streaming",1,Oe,lt),Zo=I("xhr_polling","xhr_polling",1,Oe),Ga=I("xdr_polling","xdr_polling",1,Oe),Fc=new Gr([Bt],Fe),zf=new Gr([Ri],Fe),Uf=new Gr([Gc],Fe),el=new Gr([new Ai(la(Qo),Qo,Vc)],Fe),tl=new Gr([new Ai(la(Zo),Zo,Ga)],Fe),Va=new Gr([new Ai(la(el),new jo([el,new oa(tl,{delay:4e3})]),tl)],Fe),ua=new Ai(la(Va),Va,Uf),nl;return b.useTLS?nl=new jo([Fc,new oa(ua,{delay:2e3})]):nl=new jo([Fc,new oa(zf,{delay:2e3}),new oa(ua,{delay:5e3})]),new Hn(new jn(new Ai(la(Bt),nl,ua)),D,{ttl:18e5,timeline:b.timeline,useTLS:b.useTLS})},Bo=Nc,za=(function(){var _=this;_.timeline.info(_.buildTimelineMessage({transport:_.name+(_.options.useTLS?"s":"")})),_.hooks.isInitialized()?_.changeState("initialized"):_.hooks.file?(_.changeState("initializing"),y.load(_.hooks.file,{useTLS:_.options.useTLS},function(b,E){_.hooks.isInitialized()?(_.changeState("initialized"),E(!0)):(b&&_.onError(b),_.onClose(),E(!1))})):_.onClose()}),Ic={getRequest:function(_){var b=new window.XDomainRequest;return b.ontimeout=function(){_.emit("error",new M),_.close()},b.onerror=function(E){_.emit("error",E),_.close()},b.onprogress=function(){b.responseText&&b.responseText.length>0&&_.onChunk(200,b.responseText)},b.onload=function(){b.responseText&&b.responseText.length>0&&_.onChunk(200,b.responseText),_.emit("finished",200),_.close()},b},abortRequest:function(_){_.ontimeout=_.onerror=_.onprogress=_.onload=null,_.abort()}},Hc=Ic;const Ua=256*1024;class Ef extends X{constructor(b,E,D){super(),this.hooks=b,this.method=E,this.url=D}start(b){this.position=0,this.xhr=this.hooks.getRequest(this),this.unloader=()=>{this.close()},Ve.addUnloadListener(this.unloader),this.xhr.open(this.method,this.url,!0),this.xhr.setRequestHeader&&this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.send(b)}close(){this.unloader&&(Ve.removeUnloadListener(this.unloader),this.unloader=null),this.xhr&&(this.hooks.abortRequest(this.xhr),this.xhr=null)}onChunk(b,E){for(;;){var D=this.advanceBuffer(E);if(D)this.emit("chunk",{status:b,data:D});else break}this.isBufferTooLong(E)&&this.emit("buffer_too_long")}advanceBuffer(b){var E=b.slice(this.position),D=E.indexOf(` -`);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(_,b);if(this.isXDRSupported(b.indexOf("https:")===0))return this.HTTPFactory.createXDR(_,b);throw"Cross-origin HTTP requests are not supported"},isXHRSupported(){var _=this.getXHRAPI();return!!_&&new _().withCredentials!==void 0},isXDRSupported(_){var b=_?"https:":"http:",E=this.getProtocol();return!!window.XDomainRequest&&E===b},addUnloadListener(_){window.addEventListener!==void 0?window.addEventListener("unload",_,!1):window.attachEvent!==void 0&&window.attachEvent("onunload",_)},removeUnloadListener(_){window.addEventListener!==void 0?window.removeEventListener("unload",_,!1):window.detachEvent!==void 0&&window.detachEvent("onunload",_)},randomInt(_){return Math.floor(function(){return(window.crypto||window.msCrypto).getRandomValues(new Uint32Array(1))[0]/Math.pow(2,32)}()*_)}},Ve=Ss,qa;(function(_){_[_.ERROR=3]="ERROR",_[_.INFO=6]="INFO",_[_.DEBUG=7]="DEBUG"})(qa||(qa={}));var ca=qa;class Uc{constructor(b,E,D){this.key=b,this.session=E,this.events=[],this.options=D||{},this.sent=0,this.uniqueID=0}log(b,E){b<=this.options.level&&(this.events.push(ae({},E,{timestamp:me.now()})),this.options.limit&&this.events.length>this.options.limit&&this.events.shift())}error(b){this.log(ca.ERROR,b)}info(b){this.log(ca.INFO,b)}debug(b){this.log(ca.DEBUG,b)}isEmpty(){return this.events.length===0}send(b,E){var D=ae({session:this.session,bundle:this.sent+1,key:this.key,lib:"js",version:this.options.version,cluster:this.options.cluster,features:this.options.features,timeline:this.events},this.options.params);return this.events=[],b(D,(I,ie)=>{I||this.sent++,E&&E(I,ie)}),!0}generateUniqueID(){return this.uniqueID++,this.uniqueID}}class En{constructor(b,E,D,I){this.name=b,this.priority=E,this.transport=D,this.options=I||{}}isSupported(){return this.transport.isSupported({useTLS:this.options.useTLS})}connect(b,E){if(this.isSupported()){if(this.priority<b)return Fr(new O,E)}else return Fr(new B,E);var D=!1,I=this.transport.createConnection(this.name,this.priority,this.options.key,this.options),ie=null,ye=function(){I.unbind("initialized",ye),I.connect()},Oe=function(){ie=kr.createHandshake(I,function(Bt){D=!0,lt(),E(null,Bt)})},Fe=function(Bt){lt(),E(Bt)},at=function(){lt();var Bt;Bt=Wn(I),E(new L(Bt))},lt=function(){I.unbind("initialized",ye),I.unbind("open",Oe),I.unbind("error",Fe),I.unbind("closed",at)};return I.bind("initialized",ye),I.bind("open",Oe),I.bind("error",Fe),I.bind("closed",at),I.initialize(),{abort:()=>{D||(lt(),ie?ie.close():I.close())},forceMinPriority:Bt=>{D||this.priority<Bt&&(ie?ie.close():I.close())}}}}function Fr(_,b){return me.defer(function(){b(_)}),{abort:function(){},forceMinPriority:function(){}}}const{Transports:ws}=Ve;var ki=function(_,b,E,D,I,ie){var ye=ws[E];if(!ye)throw new G(E);var Oe=(!_.enabledTransports||ze(_.enabledTransports,b)!==-1)&&(!_.disabledTransports||ze(_.disabledTransports,b)===-1),Fe;return Oe?(I=Object.assign({ignoreNullOrigin:_.ignoreNullOrigin},I),Fe=new En(b,D,ie?ie.getAssistant(ye):ye,I)):Fe=xf,Fe},xf={isSupported:function(){return!1},connect:function(_,b){var E=me.defer(function(){b(new B)});return{abort:function(){E.ensureAborted()},forceMinPriority:function(){}}}};function Ko(_){if(_==null)throw"You must pass an options object";if(_.cluster==null)throw"Options object must provide a cluster";"disableStats"in _&&bt.warn("The disableStats option is deprecated in favor of enableStats")}const Wo=(_,b)=>{var E="socket_id="+encodeURIComponent(_.socketId);for(var D in b.params)E+="&"+encodeURIComponent(D)+"="+encodeURIComponent(b.params[D]);if(b.paramsProvider!=null){let I=b.paramsProvider();for(var D in I)E+="&"+encodeURIComponent(D)+"="+encodeURIComponent(I[D])}return E};var Es=_=>{if(typeof Ve.getAuthorizers()[_.transport]>"u")throw`'${_.transport}' is not a recognized auth transport`;return(b,E)=>{const D=Wo(b,_);Ve.getAuthorizers()[_.transport](Ve,D,_,A.UserAuthentication,E)}};const Bc=(_,b)=>{var E="socket_id="+encodeURIComponent(_.socketId);E+="&channel_name="+encodeURIComponent(_.channelName);for(var D in b.params)E+="&"+encodeURIComponent(D)+"="+encodeURIComponent(b.params[D]);if(b.paramsProvider!=null){let I=b.paramsProvider();for(var D in I)E+="&"+encodeURIComponent(D)+"="+encodeURIComponent(I[D])}return E};var Tf=_=>{if(typeof Ve.getAuthorizers()[_.transport]>"u")throw`'${_.transport}' is not a recognized auth transport`;return(b,E)=>{const D=Bc(b,_);Ve.getAuthorizers()[_.transport](Ve,D,_,A.ChannelAuthorization,E)}};const kf=(_,b,E)=>{const D={authTransport:b.transport,authEndpoint:b.endpoint,auth:{params:b.params,headers:b.headers}};return(I,ie)=>{const ye=_.channel(I.channelName);E(ye,D).authorize(I.socketId,ie)}};function Yo(_,b){let E={activityTimeout:_.activityTimeout||f.activityTimeout,cluster:_.cluster,httpPath:_.httpPath||f.httpPath,httpPort:_.httpPort||f.httpPort,httpsPort:_.httpsPort||f.httpsPort,pongTimeout:_.pongTimeout||f.pongTimeout,statsHost:_.statsHost||f.stats_host,unavailableTimeout:_.unavailableTimeout||f.unavailableTimeout,wsPath:_.wsPath||f.wsPath,wsPort:_.wsPort||f.wsPort,wssPort:_.wssPort||f.wssPort,enableStats:Mf(_),httpHost:Rf(_),useTLS:Lf(_),wsHost:Df(_),userAuthenticator:Pc(_),channelAuthorizer:Nf(_,b)};return"disabledTransports"in _&&(E.disabledTransports=_.disabledTransports),"enabledTransports"in _&&(E.enabledTransports=_.enabledTransports),"ignoreNullOrigin"in _&&(E.ignoreNullOrigin=_.ignoreNullOrigin),"timelineParams"in _&&(E.timelineParams=_.timelineParams),"nacl"in _&&(E.nacl=_.nacl),E}function Rf(_){return _.httpHost?_.httpHost:_.cluster?`sockjs-${_.cluster}.pusher.com`:f.httpHost}function Df(_){return _.wsHost?_.wsHost:Of(_.cluster)}function Of(_){return`ws-${_}.pusher.com`}function Lf(_){return Ve.getProtocol()==="https:"?!0:_.forceTLS!==!1}function Mf(_){return"enableStats"in _?_.enableStats:"disableStats"in _?!_.disableStats:!1}function Pc(_){const b=Object.assign(Object.assign({},f.userAuthentication),_.userAuthentication);return"customHandler"in b&&b.customHandler!=null?b.customHandler:Es(b)}function $f(_,b){let E;return"channelAuthorization"in _?E=Object.assign(Object.assign({},f.channelAuthorization),_.channelAuthorization):(E={transport:_.authTransport||f.authTransport,endpoint:_.authEndpoint||f.authEndpoint},"auth"in _&&("params"in _.auth&&(E.params=_.auth.params),"headers"in _.auth&&(E.headers=_.auth.headers)),"authorizer"in _&&(E.customHandler=kf(b,E,_.authorizer))),E}function Nf(_,b){const E=$f(_,b);return"customHandler"in E&&E.customHandler!=null?E.customHandler:Tf(E)}class If extends X{constructor(b){super(function(E,D){bt.debug(`No callbacks on watchlist events for ${E}`)}),this.pusher=b,this.bindWatchlistInternalEvent()}handleEvent(b){b.data.events.forEach(E=>{this.emit(E.name,E)})}bindWatchlistInternalEvent(){this.pusher.connection.bind("message",b=>{var E=b.event;E==="pusher_internal:watchlist_events"&&this.handleEvent(b)})}}function Hf(){let _,b;return{promise:new Promise((D,I)=>{_=D,b=I}),resolve:_,reject:b}}var Xo=Hf;class jf extends X{constructor(b){super(function(E,D){bt.debug("No callbacks on user for "+E)}),this.signin_requested=!1,this.user_data=null,this.serverToUserChannel=null,this.signinDonePromise=null,this._signinDoneResolve=null,this._onAuthorize=(E,D)=>{if(E){bt.warn(`Error during signin: ${E}`),this._cleanup();return}this.pusher.send_event("pusher:signin",{auth:D.auth,user_data:D.user_data})},this.pusher=b,this.pusher.connection.bind("state_change",({previous:E,current:D})=>{E!=="connected"&&D==="connected"&&this._signin(),E==="connected"&&D!=="connected"&&(this._cleanup(),this._newSigninPromiseIfNeeded())}),this.watchlist=new If(b),this.pusher.connection.bind("message",E=>{var D=E.event;D==="pusher:signin_success"&&this._onSigninSuccess(E.data),this.serverToUserChannel&&this.serverToUserChannel.name===E.channel&&this.serverToUserChannel.handleEvent(E)})}signin(){this.signin_requested||(this.signin_requested=!0,this._signin())}_signin(){this.signin_requested&&(this._newSigninPromiseIfNeeded(),this.pusher.connection.state==="connected"&&this.pusher.config.userAuthenticator({socketId:this.pusher.connection.socket_id},this._onAuthorize))}_onSigninSuccess(b){try{this.user_data=JSON.parse(b.user_data)}catch{bt.error(`Failed parsing user data after signin: ${b.user_data}`),this._cleanup();return}if(typeof this.user_data.id!="string"||this.user_data.id===""){bt.error(`user_data doesn't contain an id. user_data: ${this.user_data}`),this._cleanup();return}this._signinDoneResolve(),this._subscribeChannels()}_subscribeChannels(){const b=E=>{E.subscriptionPending&&E.subscriptionCancelled?E.reinstateSubscription():!E.subscriptionPending&&this.pusher.connection.state==="connected"&&E.subscribe()};this.serverToUserChannel=new hs(`#server-to-user-${this.user_data.id}`,this.pusher),this.serverToUserChannel.bind_global((E,D)=>{E.indexOf("pusher_internal:")===0||E.indexOf("pusher:")===0||this.emit(E,D)}),b(this.serverToUserChannel)}_cleanup(){this.user_data=null,this.serverToUserChannel&&(this.serverToUserChannel.unbind_all(),this.serverToUserChannel.disconnect(),this.serverToUserChannel=null),this.signin_requested&&this._signinDoneResolve()}_newSigninPromiseIfNeeded(){if(!this.signin_requested||this.signinDonePromise&&!this.signinDonePromise.done)return;const{promise:b,resolve:E}=Xo();b.done=!1;const D=()=>{b.done=!0};b.then(D).catch(D),this.signinDonePromise=b,this._signinDoneResolve=E}}class rn{static ready(){rn.isReady=!0;for(var b=0,E=rn.instances.length;b<E;b++)rn.instances[b].connect()}static getClientFeatures(){return Ge(ln({ws:Ve.Transports.ws},function(b){return b.isSupported({})}))}constructor(b,E){qc(b),Ko(E),this.key=b,this.config=Yo(E,this),this.channels=kr.createChannels(),this.global_emitter=new X,this.sessionID=Ve.randomInt(1e9),this.timeline=new Uc(this.key,this.sessionID,{cluster:this.config.cluster,features:rn.getClientFeatures(),params:this.config.timelineParams||{},limit:50,level:ca.INFO,version:f.VERSION}),this.config.enableStats&&(this.timelineSender=kr.createTimelineSender(this.timeline,{host:this.config.statsHost,path:"/timeline/v2/"+Ve.TimelineTransport.name}));var D=I=>Ve.getDefaultStrategy(this.config,I,ki);this.connection=kr.createConnectionManager(this.key,{getStrategy:D,timeline:this.timeline,activityTimeout:this.config.activityTimeout,pongTimeout:this.config.pongTimeout,unavailableTimeout:this.config.unavailableTimeout,useTLS:!!this.config.useTLS}),this.connection.bind("connected",()=>{this.subscribeAll(),this.timelineSender&&this.timelineSender.send(this.connection.isUsingTLS())}),this.connection.bind("message",I=>{var ie=I.event,ye=ie.indexOf("pusher_internal:")===0;if(I.channel){var Oe=this.channel(I.channel);Oe&&Oe.handleEvent(I)}ye||this.global_emitter.emit(I.event,I.data)}),this.connection.bind("connecting",()=>{this.channels.disconnect()}),this.connection.bind("disconnected",()=>{this.channels.disconnect()}),this.connection.bind("error",I=>{bt.warn(I)}),rn.instances.push(this),this.timeline.info({instances:rn.instances.length}),this.user=new jf(this),rn.isReady&&this.connect()}channel(b){return this.channels.find(b)}allChannels(){return this.channels.all()}connect(){if(this.connection.connect(),this.timelineSender&&!this.timelineSenderTimer){var b=this.connection.isUsingTLS(),E=this.timelineSender;this.timelineSenderTimer=new ge(6e4,function(){E.send(b)})}}disconnect(){this.connection.disconnect(),this.timelineSenderTimer&&(this.timelineSenderTimer.ensureAborted(),this.timelineSenderTimer=null)}bind(b,E,D){return this.global_emitter.bind(b,E,D),this}unbind(b,E,D){return this.global_emitter.unbind(b,E,D),this}bind_global(b){return this.global_emitter.bind_global(b),this}unbind_global(b){return this.global_emitter.unbind_global(b),this}unbind_all(b){return this.global_emitter.unbind_all(),this}subscribeAll(){var b;for(b in this.channels.channels)this.channels.channels.hasOwnProperty(b)&&this.subscribe(b)}subscribe(b){var E=this.channels.add(b,this);return E.subscriptionPending&&E.subscriptionCancelled?E.reinstateSubscription():!E.subscriptionPending&&this.connection.state==="connected"&&E.subscribe(),E}unsubscribe(b){var E=this.channels.find(b);E&&E.subscriptionPending?E.cancelSubscription():(E=this.channels.remove(b),E&&E.subscribed&&E.unsubscribe())}send_event(b,E,D){return this.connection.send_event(b,E,D)}shouldUseTLS(){return this.config.useTLS}signin(){this.user.signin()}}rn.instances=[],rn.isReady=!1,rn.logToConsole=!1,rn.Runtime=Ve,rn.ScriptReceivers=Ve.ScriptReceivers,rn.DependenciesReceivers=Ve.DependenciesReceivers,rn.auth_callbacks=Ve.auth_callbacks;var Jo=a.default=rn;function qc(_){if(_==null)throw"You must pass your app key when you instantiate Pusher."}Ve.setup(rn)})])})})(hf)),hf.exports}var w_=S_();const iy=Qr(w_),Ht=ut("WebSocketManager");class C_{constructor(){$(this,"echo",null);$(this,"subscriptions",new Map);$(this,"initialized",!1);$(this,"config",null)}configure(e){if(this.initialized){Ht.warn("[WebSocketManager] 이미 초기화되어 설정을 변경할 수 없습니다.");return}this.config=e,Ht.log("[WebSocketManager] 설정 완료:",{appKey:e.appKey?"***":"(없음)",host:e.host,port:e.port,scheme:e.scheme})}initialize(){if(this.initialized)return;if(!this.config||!this.config.appKey){Ht.warn("[WebSocketManager] WebSocket 설정이 없습니다. initTemplateApp에서 websocket 옵션을 전달해주세요.");return}const{appKey:e,host:r="localhost",port:a=80,scheme:i="https",authEndpoint:l="/api/broadcasting/auth"}=this.config,c=Number(a)||80,d=i==="https";Ht.log("[WebSocketManager] 연결 설정:",{host:r,port:c,scheme:i,useTLS:d,authEndpoint:l}),window.Pusher=iy;const f=localStorage.getItem("auth_token"),g={wsHost:r,wsPort:c,wssPort:c,forceTLS:d,disableStats:!0,enabledTransports:["ws","wss"],cluster:"mt1",authEndpoint:l,auth:{headers:{Authorization:f?`Bearer ${f}`:"",Accept:"application/json"}}};Ht.log("[WebSocketManager] Pusher 옵션:",g);const m=new iy(e,g);m.connection.bind("connecting",()=>{Ht.log("[WebSocketManager] 연결 시도 중...")}),m.connection.bind("connected",()=>{Ht.log("[WebSocketManager] 연결 성공! Socket ID:",m.connection.socket_id)}),m.connection.bind("failed",()=>{Ht.error("[WebSocketManager] 연결 실패 - WebSocket을 사용할 수 없습니다.")}),m.connection.bind("error",y=>{Ht.error("[WebSocketManager] 연결 오류:",y)}),m.connection.bind("state_change",y=>{Ht.log(`[WebSocketManager] 상태 변경: ${y.previous} → ${y.current}`)}),m.connection.bind("unavailable",()=>{Ht.error("[WebSocketManager] WebSocket 사용 불가 - 연결할 수 없습니다.")}),m.connection.bind("disconnected",()=>{Ht.warn("[WebSocketManager] 연결이 끊어졌습니다.")}),Ht.log("[WebSocketManager] 초기 연결 상태:",m.connection.state),this.echo=new v_({broadcaster:"reverb",client:m}),Ht.log("[WebSocketManager] 연결 시작 시도..."),m.connect(),window.Echo=this.echo,this.initialized=!0,Ht.log("[WebSocketManager] Echo 초기화 완료")}subscribe(e,r,a,i={}){if(this.initialize(),!this.echo)return Ht.warn("[WebSocketManager] Echo가 초기화되지 않았습니다."),"";const{channelType:l="private"}=i,c=`${e}:${r}`;if(this.subscriptions.has(c))return Ht.log(`[WebSocketManager] 이미 구독 중: ${c}`),c;let d;switch(l){case"public":d=this.echo.channel(e);break;case"presence":d=this.echo.join(e);break;default:d=this.echo.private(e)}return d.listen(`.${r}`,a),this.subscriptions.set(c,d),Ht.log(`[WebSocketManager] 구독 완료: ${c} (${l})`),c}unsubscribe(e){const r=this.subscriptions.get(e);if(!r)return;const a=e.lastIndexOf(":"),i=a>=0?e.substring(a+1):"";if(i)try{r.stopListening(`.${i}`)}catch(l){Ht.warn(`[WebSocketManager] stopListening 실패: ${e}`,l)}this.subscriptions.delete(e),Ht.log(`[WebSocketManager] 구독 해제: ${e}`)}leaveChannel(e){if(!this.echo)return;this.echo.leave(e);const r=[];this.subscriptions.forEach((a,i)=>{i.startsWith(`${e}:`)&&r.push(i)}),r.forEach(a=>this.subscriptions.delete(a)),Ht.log(`[WebSocketManager] 채널 구독 해제: ${e}`)}disconnect(){this.echo&&(this.echo.disconnect(),this.subscriptions.clear(),this.initialized=!1,Ht.log("[WebSocketManager] 연결 종료"))}getEcho(){return this.echo}isInitialized(){return this.initialized}isConfigured(){return this.config!==null&&!!this.config.appKey}getSubscriptionCount(){return this.subscriptions.size}}const Tc=new C_,gt=ut("DataSourceManager"),sy=new _n;function kc(o,e){const r=Ta(o);if(r!==null)try{return Gn(r)?sy.evaluatePipeExpression(r,e,{skipCache:!0}):sy.evaluateExpression(r,e)}catch(a){return gt.error("Failed to evaluate param expression:",r,a),o}return Zi(o,e,{skipCache:!0})}function oy(o){const e={};for(const r of o.keys()){if(r in e)continue;const a=o.getAll(r);a.length>1?e[r]=a:a.length===1&&(r.endsWith("[]")?e[r]=a:e[r]=a[0])}return e}function E_(){try{return window.G7Core?.devTools}catch{return}}const Lc=class Lc{constructor(e={}){$(this,"options");$(this,"dataCache",new Map);$(this,"bindingEngine");$(this,"globalHeaders",[]);this.options=e,this.bindingEngine=new _n}setGlobalHeaders(e){this.globalHeaders=e||[]}matchesPattern(e,r){if(r==="*")return!0;const a=r.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${a}$`).test(e)}toFormData(e){const r=new FormData;for(const[a,i]of Object.entries(e))i instanceof File||i instanceof Blob?r.append(a,i):i!=null&&r.append(a,typeof i=="object"?JSON.stringify(i):String(i));return r}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=kc(c,r);d!=null&&d!==""&&(a[l]=String(d))});return a}filterByCondition(e,r={}){const a=new Set,i=[];for(const l of e){if(a.has(l.id))continue;if(!l.if&&!l.conditions){a.add(l.id),i.push(l);continue}Qi({if:l.if,conditions:l.conditions},r,this.bindingEngine,`data_source:${l.id}`)&&(a.add(l.id),i.push(l))}return i}checkErrorCondition(e,r){if(!e.errorCondition?.if||e.errorCondition?.errorCode===void 0)return null;try{const a={response:r};if(Qi({if:e.errorCondition.if},a,this.bindingEngine,`errorCondition:${e.id}`))return gt.log(`errorCondition matched for ${e.id}, triggering error ${e.errorCondition.errorCode}`),e.errorCondition.errorCode}catch(a){gt.error(`Failed to evaluate errorCondition for ${e.id}:`,a)}return null}async fetchDataSources(e,r={},a=new URLSearchParams,i,l){const c={},d=!!this.options.sampleProvider,f=e.filter(g=>(d||g.auto_fetch!==!1)&&g.type!=="websocket");return await Promise.all(f.map(async g=>{try{const m=await this.fetchDataSource(g,r,a,i,l);c[g.id]=m,this.dataCache.set(g.id,{data:m,timestamp:Date.now()});const y=this.checkErrorCondition(g,m);if(y!==null){const w={status:y,message:`Error condition matched for ${g.id}`,data:m},S=Sr(),v=S.resolve(y,{errorHandling:g.errorHandling,onError:g.onError});if(v.handler)try{await S.execute(v.handler,w)}catch(A){gt.error(`Error executing errorCondition handler for ${g.id}:`,A)}g.fallback!==void 0&&(c[g.id]=g.fallback,this.dataCache.set(g.id,{data:g.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${g.id} (errorCondition)`));return}if(g.onSuccess)try{const w={data:m,sourceId:g.id};await this.executeOnSuccessHandler(g.onSuccess,w)}catch(w){gt.error(`Error executing onSuccess handler for ${g.id}:`,w)}}catch(m){gt.error(`Failed to fetch data source: ${g.id}`,m);const y=m?.response?.status||m?.status||500,w=m?.response?.data,S=w?.message||m.message||"Unknown error",v={status:y,message:S,errors:w?.errors,data:w,statusText:m?.response?.statusText},A=Sr(),x=A.resolve(y,{errorHandling:g.errorHandling,onError:g.onError});if(x.handler)try{await A.execute(x.handler,v)}catch(R){gt.error(`Error executing error handler for ${g.id}:`,R)}g.fallback!==void 0&&(c[g.id]=g.fallback,this.dataCache.set(g.id,{data:g.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${g.id}`)),this.options.onError&&this.options.onError(m,g)}})),c}async fetchDataSourcesWithResults(e,r={},a=new URLSearchParams,i,l,c){if(e.some(m=>m.type==="websocket")){const m=e.filter(y=>y.type==="websocket").map(y=>y.id).join(", ");gt.warn(`fetchDataSourcesWithResults received WebSocket sources (caller should filter them out before calling): ${m}. WebSocket sources are event listeners, not data providers — they should not be passed to fetch.`)}const f=c?.ignoreAutoFetch?e.filter(m=>m.type!=="websocket"):e.filter(m=>m.auto_fetch!==!1&&m.type!=="websocket");return await Promise.all(f.map(async m=>{try{const y=await this.fetchDataSource(m,r,a,i,l);this.dataCache.set(m.id,{data:y,timestamp:Date.now()});const w=this.checkErrorCondition(m,y);if(w!==null){const S={status:w,message:`Error condition matched for ${m.id}`,data:y},v=Sr(),A=v.resolve(w,{errorHandling:m.errorHandling,onError:m.onError});let x=!1;if(A.handler)if(m.loading_strategy==="blocking")v.execute(A.handler,S).catch(R=>{gt.error(`Error executing errorCondition handler for ${m.id}:`,R)}),x=!0;else try{await v.execute(A.handler,S),x=!0}catch(R){gt.error(`Error executing errorCondition handler for ${m.id}:`,R)}return m.fallback!==void 0?(this.dataCache.set(m.id,{data:m.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${m.id} (errorCondition)`),{id:m.id,state:"success",data:m.fallback}):{id:m.id,state:"error",error:new Error(`Error condition matched: ${w}`),errorCode:w,errorHandled:x}}if(m.onSuccess)try{const S={data:y,sourceId:m.id};await this.executeOnSuccessHandler(m.onSuccess,S)}catch(S){gt.error(`Error executing onSuccess handler for ${m.id}:`,S)}return{id:m.id,state:"success",data:y}}catch(y){gt.error(`Failed to fetch data source: ${m.id}`,y);const w=y?.response?.status||y?.status||500,S=y?.response?.data,v=S?.message||y.message||"Unknown error",A={status:w,message:v,errors:S?.errors,data:S,statusText:y?.response?.statusText},x=Sr(),R=x.resolve(w,{errorHandling:m.errorHandling,onError:m.onError});let M=!1;if(R.handler)if(m.loading_strategy==="blocking")x.execute(R.handler,A).catch(O=>{gt.error(`Error executing error handler for ${m.id}:`,O)}),M=!0;else try{await x.execute(R.handler,A),M=!0}catch(O){gt.error(`Error executing error handler for ${m.id}:`,O)}return m.fallback!==void 0?(this.dataCache.set(m.id,{data:m.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${m.id}`),{id:m.id,state:"success",data:m.fallback}):(this.options.onError&&this.options.onError(y,m),{id:m.id,state:"error",error:y,errorCode:w,errorHandled:M})}}))}fetchDataSourcesInBackground(e,r={},a=new URLSearchParams,i){e.filter(c=>c.auto_fetch!==!1&&c.loading_strategy==="background"&&c.type!=="websocket").forEach(c=>{this.fetchDataSource(c,r,a).then(async d=>{this.dataCache.set(c.id,{data:d,timestamp:Date.now()});const f=this.checkErrorCondition(c,d);if(f!==null){const g={status:f,message:`Error condition matched for ${c.id}`,data:d},m=Sr(),y=m.resolve(f,{errorHandling:c.errorHandling,onError:c.onError});if(y.handler)try{await m.execute(y.handler,g)}catch(w){gt.error(`Error executing errorCondition handler for ${c.id}:`,w)}c.fallback!==void 0&&(this.dataCache.set(c.id,{data:c.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${c.id} (errorCondition)`),i&&i(c.id,c.fallback));return}if(c.onSuccess)try{const g={data:d,sourceId:c.id};await this.executeOnSuccessHandler(c.onSuccess,g)}catch(g){gt.error(`Error executing onSuccess handler for ${c.id}:`,g)}i&&i(c.id,d)}).catch(async d=>{gt.error(`Background fetch failed: ${c.id}`,d);const f=d?.response?.status||d?.status||500,g=d?.response?.data,m=g?.message||d.message||"Unknown error",y={status:f,message:m,errors:g?.errors,data:g,statusText:d?.response?.statusText},w=Sr(),S=w.resolve(f,{errorHandling:c.errorHandling,onError:c.onError});if(S.handler)try{await w.execute(S.handler,y)}catch(v){gt.error(`Error executing error handler for ${c.id}:`,v)}if(c.fallback!==void 0){this.dataCache.set(c.id,{data:c.fallback,timestamp:Date.now()}),gt.log(`Using fallback data for ${c.id}`),i&&i(c.id,c.fallback);return}this.options.onError&&this.options.onError(d,c)})})}async fetchDataSource(e,r,a,i,l){switch(e.type){case"api":return this.fetchApiDataSource(e,r,a,i,l);case"static":return e.data;case"route_params":return r;case"query_params":return oy(a);default:throw new Error(`Unknown data source type: ${e.type}`)}}async fetchApiDataSource(e,r,a,i,l){if(this.options.sampleProvider?.has(e.id))return await Promise.resolve(this.options.sampleProvider.resolve(e));if(!e.endpoint)throw new Error(`API data source ${e.id} has no endpoint`);const c={route:r,query:oy(a)};i&&(c._global=i),l&&(c._local=l);let d=Zi(e.endpoint,c,{skipCache:!0});const f=e.method||"GET",m=(e.contentType?typeof e.contentType=="string"&&e.contentType.includes("{{")?Zi(e.contentType,c,{skipCache:!0}):e.contentType:"application/json")==="multipart/form-data";let y=m?{...e.params||{}}:JSON.parse(JSON.stringify(e.params||{}));Object.entries(y).forEach(([v,A])=>{typeof A=="string"&&(y[v]=kc(A,c))}),Object.keys(y).forEach(v=>{y[v]===""&&delete y[v]});const w=E_();let S=null;if(w?.isEnabled()){w.trackDataSourceDefinition({id:e.id,type:e.type,endpoint:d,method:f,autoFetch:e.auto_fetch,initLocal:e.initLocal,initGlobal:e.initGlobal});let v=d;if(f==="GET"&&Object.keys(y).length>0){const A=new URLSearchParams;Object.entries(y).forEach(([R,M])=>{Array.isArray(M)?M.forEach(O=>A.append(R,String(O))):M!=null&&A.append(R,String(M))});const x=A.toString();v=x?`${d}?${x}`:d}S=w.trackRequest(v,f,{requestBody:f!=="GET"?y:void 0,dataSourceId:e.id}),w.trackDataSourceLoading(e.id)}try{let v;const A=e.auth_mode??(e.auth_required===!0?"required":"none"),x=br.getInstance().isAuthenticated();if(A==="required"&&!x){gt.log(`[DataSourceManager] Skipping ${e.id}: auth_mode is 'required' but no token available`),w?.isEnabled()&&(w.trackDataSourceError(e.id,"Skipped: auth_mode required but not authenticated"),S&&w.failRequest(S,"Skipped: not authenticated"));const M=new Error("Auth required but not authenticated");throw M.response={status:401,statusText:"Unauthorized",data:null},M.status=401,M._authSkipped=!0,M}const R=A==="required"||A==="optional"&&x;if(R){const M=jr(),O=d.startsWith("/api/")?d.substring(4):d.startsWith("/api")?d.substring(4)||"/":d;this.options.onError&>.log(`[DataSourceManager] Normalized endpoint: ${d} -> ${O}`);const L=this.getMatchingGlobalHeaders(d,c),q={};e.headers&&Object.entries(e.headers).forEach(([fe,Ce])=>{const ke=kc(Ce,c);ke!=null&&ke!==""&&(q[fe]=String(ke))});const G={...L,...q},B=Object.keys(G).length>0,K=m?this.toFormData(y):y;switch(f){case"GET":v=await M.get(O,{params:y,...B&&{headers:G}});break;case"POST":B?v=await M.post(O,K,{headers:G}):v=await M.post(O,K);break;case"PUT":B?v=await M.put(O,K,{headers:G}):v=await M.put(O,K);break;case"PATCH":B?v=await M.patch(O,K,{headers:G}):v=await M.patch(O,K);break;case"DELETE":v=await M.delete(O,{params:y,...B&&{headers:G}});break;default:throw new Error(`Unsupported HTTP method: ${f}`)}}else{let M=d;if(f==="GET"){const K=new URLSearchParams(y).toString();M=K?`${d}?${K}`:d}const O={...m?{}:{"Content-Type":"application/json"},Accept:"application/json"};if(typeof window<"u"){const K=localStorage.getItem("g7_locale");K&&(O["Accept-Language"]=K)}const L=this.getMatchingGlobalHeaders(d,c);Object.assign(O,L),e.headers&&Object.entries(e.headers).forEach(([K,fe])=>{const Ce=kc(fe,c);Ce!=null&&Ce!==""&&(O[K]=String(Ce))});let q;f!=="GET"&&(m?(q=this.toFormData(y),delete O["Content-Type"]):q=JSON.stringify(y));const G=await fetch(M,{method:f,headers:O,body:q});let B;try{B=await G.json()}catch{B=null}if(S&&w?.isEnabled()&&(w.completeRequest(S,G.status,B),S=null),!G.ok){w?.isEnabled()&&w.trackDataSourceError(e.id,`HTTP ${G.status}: ${G.statusText}`);const K=new Error(`HTTP error! status: ${G.status}`);throw K.response={status:G.status,statusText:G.statusText,data:B},K.status=G.status,K}v=B,w?.isEnabled()&&w.trackDataSourceLoaded(e.id,v)}return R&&S&&w?.isEnabled()&&(w.completeRequest(S,200,v),w.trackDataSourceLoaded(e.id,v)),v}catch(v){if(S&&w?.isEnabled()){const A=v;A?.response?w.completeRequest(S,A.response.status,A.response.data):w.failRequest(S,v instanceof Error?v.message:String(v)),w.trackDataSourceError(e.id,v instanceof Error?v.message:String(v))}throw v}}getCachedData(e){const r=this.dataCache.get(e);if(!r)return;if(Date.now()-r.timestamp>Lc.DATA_CACHE_TTL){this.dataCache.delete(e);return}return r.data}clearCache(){this.dataCache.clear()}subscribeWebSockets(e,r,a={}){const i=[],l=e.filter(c=>c.type==="websocket");return l.length===0||l.forEach(c=>{if(!c.channel||!c.event){gt.warn(`WebSocket source ${c.id} missing channel or event`);return}let d,f;try{d=Zi(c.channel,a,{skipCache:!0}),f=Zi(c.event,a,{skipCache:!0})}catch(y){gt.error(`Failed to resolve WebSocket channel/event for source ${c.id}:`,y);return}const g=y=>!!(!y||y.trim()===""||y.includes("{{")||y.endsWith(".")||y.endsWith(":")||y.startsWith(".")||y.startsWith(":")||y.includes("..")||y.includes("::"));if(g(d)||g(f)){gt.warn(`WebSocket source ${c.id} has invalid channel/event after expression resolution, skipping. Original channel="${c.channel}", resolved="${d}". Original event="${c.event}", resolved="${f}". Available context keys: [${Object.keys(a).join(", ")}]`);return}const m=Tc.subscribe(d,f,y=>{const w=c.target_source||c.id;this.dataCache.set(w,{data:y,timestamp:Date.now()}),r(w,y)},{channelType:c.channel_type||"private"});m&&i.push(m)}),i}unsubscribeWebSockets(e){e.length!==0&&e.forEach(r=>{Tc.unsubscribe(r)})}async executeOnSuccessHandler(e,r){const a=this.options.actionDispatcher??am();if(!a){gt.warn("ActionDispatcher not available, skipping onSuccess handler");return}const i=Array.isArray(e)?e:[e];for(const l of i)try{await a.dispatchAction({type:"click",...l},{data:{response:r}})}catch(c){throw gt.error("Failed to execute onSuccess handler:",c),c}}};$(Lc,"DATA_CACHE_TTL",3e5);let _r=Lc;const __=new _r,Si=ut("ModalDataSourceWrapper");function A_(o,e){if(!o||!e)return o;const a=e.replace(/\?\./g,".").split(/\.(?![^\[]*\])/).flatMap(l=>{const c=l.match(/^([^\[]*)((?:\[\d+\])*)$/);if(c){const[,d,f]=c,g=[];d&&g.push(d);const m=f.match(/\[\d+\]/g);return m&&g.push(...m),g}return[l]}).filter(l=>l!=="");let i=o;for(const l of a){if(i==null)return;if(l.startsWith("[")&&l.endsWith("]")){const c=parseInt(l.slice(1,-1),10);i=i[c]}else i=i[l]}return i}const ly=({isOpen:o,modalId:e,dataSources:r,dataContext:a,globalStateUpdater:i,bindingEngine:l,debug:c=!1,children:d})=>{const[f,g]=N.useState(!1),[m,y]=N.useState(!1),w=N.useRef(!1),S=N.useRef(null);S.current||(S.current=new _r);const v=N.useCallback(async()=>{if(!(!r||r.length===0)&&S.current){g(!0);try{const A=a.route||{},x=new URLSearchParams(window.location.search),R=r.map(O=>{let L={...O};if(O.endpoint&&l){const q=l.resolveBindings(O.endpoint,a,{skipCache:!0});L.endpoint=q}if(O.params&&l){const q={};Object.entries(O.params).forEach(([G,B])=>{typeof B=="string"?q[G]=l.resolveBindings(B,a,{skipCache:!0}):q[G]=B}),L.params=q}return L});c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" fetching data sources:`,R.map(O=>({id:O.id,endpoint:O.endpoint})));const M=await S.current.fetchDataSourcesWithResults(R,A,x);i&&M.forEach(O=>{if(O.state==="success"&&O.data!==void 0){const L=R.find(q=>q.id===O.id);if(L?.initGlobal){const q=O.data?.data??O.data,G=Array.isArray(L.initGlobal)?L.initGlobal:[L.initGlobal];for(const B of G)if(typeof B=="string")i({[B]:q}),c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" initGlobal: ${O.id}.data -> _global.${B}`);else if(typeof B=="object"&&B.key){const{key:K,path:fe}=B,Ce=fe?A_(q,fe):q;i({[K]:Ce}),c&&Si.log(`Modal "${e}" initGlobal: ${O.id}.data${fe?"."+fe:""} -> _global.${K}`)}}}else O.state==="error"&&Si.error(`Modal "${e}" data source error:`,O.id,O.error)}),c&&Si.log(`Modal "${e}" data sources fetched successfully`)}catch(A){Si.error(`Modal "${e}" data source fetch error:`,A)}finally{g(!1),y(!0)}}},[r,a,i,l,e,c]);return N.useEffect(()=>{if(!r||r.length===0)return;const A=window.__templateApp;return A?.registerModalDataSources&&A.registerModalDataSources(e,r),()=>{A?.unregisterModalDataSources&&A.unregisterModalDataSources(e)}},[e,r]),N.useEffect(()=>{!o&&w.current&&(y(!1),c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" closed, reset fetch state`)),o&&!w.current&&r&&r.length>0&&!m&&v(),w.current=o},[o,r,m,v,e,c]),dt.jsx(dt.Fragment,{children:d})};class Rc extends Error{constructor(r,a,i={}){super(a);$(this,"code");$(this,"userMessageKey");$(this,"showStack");$(this,"icon");$(this,"recoverable");this.name="TemplateEngineError",this.code=r,this.userMessageKey=a,this.showStack=i.showStack??!0,this.icon=i.icon??"fa-circle-exclamation",this.recoverable=i.recoverable??!0,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}getUserMessage(r,a){if(!a||!r)return this.userMessageKey;try{return a.translate(this.userMessageKey,r)}catch{return this.userMessageKey}}}class x_ extends Rc{constructor(){super("TEMPLATE_NOT_FOUND","$t:core.errors.template_not_found",{showStack:!1,icon:"fa-paint-brush",recoverable:!1}),this.name="TemplateNotFoundError"}}function cy(o){if(o instanceof Rc)return o;if(o instanceof Error){const e=new Rc("TEMPLATE_INIT_ERROR",o.message||"$t:core.errors.template_init_error",{showStack:!0,icon:"fa-circle-exclamation",recoverable:!0});return e.stack=o.stack,e}return new Rc("TEMPLATE_INIT_ERROR","$t:core.errors.template_init_error",{showStack:!1,icon:"fa-circle-exclamation",recoverable:!0})}const T_=ut("ErrorDisplay");class gf{static render(e,r){const a=document.getElementById(e);if(!a){T_.error(`Container #${e} not found`);return}const i=r.debug&&r.showStack&&r.stackTrace,l=window.matchMedia("(prefers-color-scheme: dark)").matches,c={container:` + `,Vn.error("Error state rendered:",i,a)}getCurrentLayout(){return this.currentLayout}clear(){this.currentLayout=null,this.layoutCache.clear(),Vn.log("Layout data and cache cleared")}}const lA=dt("TranslationContext"),Tf=N.createContext(null),wc=({children:s,translationEngine:e,translationContext:n})=>{const a=N.useMemo(()=>(l,c)=>{if(c){const d="|"+Object.entries(c).map(([f,h])=>`${f}=${h}`).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 ft.jsx(Tf.Provider,{value:i,children:s})},Ay=()=>{const s=N.useContext(Tf);return s||(lA.warn("TranslationProvider 외부에서 호출되었습니다. 키를 그대로 반환합니다."),{t:e=>e,translationEngine:null,translationContext:null})},$a=dt("SlotContext"),cA={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},uA=N.createContext(cA),kf=({children:s})=>{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){$a.error(`Slot subscriber error (slotId: ${m}):`,v)}})},[]),i=N.useCallback((m,b,S)=>{if(!m||!b){$a.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),$a.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),$a.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(),$a.log("All slots cleared")},[a]),h=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=h,$a.log("SlotContext exposed to window.__slotContextValue"))},[h]),ft.jsx(uA.Provider,{value:h,children:s})};class Rf{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 xy{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 dA(s){try{return Reflect.construct(String,[],s),!0}catch{return!1}}class Df extends Rf{constructor(e,n,a){super(),this.name=n,this.pusher=e,this.options=a,this.eventFormatter=new xy(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 Ty extends Df{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class fA extends Df{whisper(e,n){return this.pusher.channels.channels[this.name].trigger(`client-${e}`,n),this}}class hA extends Ty{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 ky extends Rf{constructor(e,n,a){super(),this.events={},this.listeners={},this.name=n,this.socket=e,this.options=a,this.eventFormatter=new xy(this.options.namespace),this.subscribe()}subscribe(){this.socket.emit("subscribe",{channel:this.name,auth:this.options.auth||{}})}unsubscribe(){this.unbind(),this.socket.emit("unsubscribe",{channel:this.name,auth:this.options.auth||{}})}listen(e,n){return this.on(this.eventFormatter.format(e),n),this}stopListening(e,n){return this.unbindEvent(this.eventFormatter.format(e),n),this}subscribed(e){return this.on("connect",n=>{e(n)}),this}error(e){return this}on(e,n){return this.listeners[e]=this.listeners[e]||[],this.events[e]||(this.events[e]=(a,i)=>{this.name===a&&this.listeners[e]&&this.listeners[e].forEach(l=>l(i))},this.socket.on(e,this.events[e])),this.listeners[e].push(n),this}unbind(){Object.keys(this.events).forEach(e=>{this.unbindEvent(e)})}unbindEvent(e,n){this.listeners[e]=this.listeners[e]||[],n&&(this.listeners[e]=this.listeners[e].filter(a=>a!==n)),(!n||this.listeners[e].length===0)&&(this.events[e]&&(this.socket.removeListener(e,this.events[e]),delete this.events[e]),delete this.listeners[e])}}class Ry extends ky{whisper(e,n){return this.socket.emit("client event",{channel:this.name,event:`client-${e}`,data:n}),this}}class pA extends Ry{here(e){return this.on("presence:subscribed",n=>{e(n.map(a=>a.user_info))}),this}joining(e){return this.on("presence:joining",n=>e(n.user_info)),this}whisper(e,n){return this.socket.emit("client event",{channel:this.name,event:`client-${e}`,data:n}),this}leaving(e){return this.on("presence:leaving",n=>e(n.user_info)),this}}class Cc extends Rf{subscribe(){}unsubscribe(){}listen(e,n){return this}listenToAll(e){return this}stopListening(e,n){return this}subscribed(e){return this}error(e){return this}on(e,n){return this}}class Dy extends Cc{whisper(e,n){return this}}class gA extends Cc{whisper(e,n){return this}}class mA extends Dy{here(e){return this}joining(e){return this}whisper(e,n){return this}leaving(e){return this}}const Oy=class uw{constructor(e){this.setOptions(e),this.connect()}setOptions(e){this.options={...uw._defaultOptions,...e,broadcaster:e.broadcaster};let n=this.csrfToken();n&&(this.options.auth.headers["X-CSRF-TOKEN"]=n,this.options.userAuthentication.headers["X-CSRF-TOKEN"]=n),n=this.options.bearerToken,n&&(this.options.auth.headers.Authorization="Bearer "+n,this.options.userAuthentication.headers.Authorization="Bearer "+n)}csrfToken(){var e,n;return typeof window<"u"&&(e=window.Laravel)!=null&&e.csrfToken?window.Laravel.csrfToken:this.options.csrfToken?this.options.csrfToken:typeof document<"u"&&typeof document.querySelector=="function"?((n=document.querySelector('meta[name="csrf-token"]'))==null?void 0:n.getAttribute("content"))??null:null}};Oy._defaultOptions={auth:{headers:{}},authEndpoint:"/broadcasting/auth",userAuthentication:{endpoint:"/broadcasting/user-auth",headers:{}},csrfToken:null,bearerToken:null,host:null,key:null,namespace:"App.Events"};let Of=Oy;class Ec extends Of{constructor(){super(...arguments),this.channels={}}connect(){if(typeof this.options.client<"u")this.pusher=this.options.client;else if(this.options.Pusher)this.pusher=new this.options.Pusher(this.options.key,this.options);else if(typeof window<"u"&&typeof window.Pusher<"u")this.pusher=new window.Pusher(this.options.key,this.options);else throw new Error("Pusher client not found. Should be globally available or passed via options.client")}signin(){this.pusher.signin()}listen(e,n,a){return this.channel(e).listen(n,a)}channel(e){return this.channels[e]||(this.channels[e]=new Df(this.pusher,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new Ty(this.pusher,"private-"+e,this.options)),this.channels["private-"+e]}encryptedPrivateChannel(e){return this.channels["private-encrypted-"+e]||(this.channels["private-encrypted-"+e]=new fA(this.pusher,"private-encrypted-"+e,this.options)),this.channels["private-encrypted-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new hA(this.pusher,"presence-"+e,this.options)),this.channels["presence-"+e]}leave(e){[e,"private-"+e,"private-encrypted-"+e,"presence-"+e].forEach(n=>{this.leaveChannel(n)})}leaveChannel(e){this.channels[e]&&(this.channels[e].unsubscribe(),delete this.channels[e])}socketId(){return this.pusher.connection.socket_id}connectionStatus(){const e=this.pusher.connection.state;switch(e){case"connected":case"connecting":return e;case"failed":case"unavailable":return"failed";default:return"disconnected"}}onConnectionChange(e){const n=()=>{e(this.connectionStatus())},a=["state_change","connected","disconnected"];return a.forEach(i=>{this.pusher.connection.bind(i,n)}),()=>{a.forEach(i=>{this.pusher.connection.unbind(i,n)})}}disconnect(){this.pusher.disconnect()}}class yA extends Of{constructor(){super(...arguments),this.channels={}}connect(){let e=this.getSocketIO();this.socket=e(this.options.host??void 0,this.options),this.socket.io.on("reconnect",()=>{Object.values(this.channels).forEach(n=>{n.subscribe()})})}getSocketIO(){if(typeof this.options.client<"u")return this.options.client;if(typeof window<"u"&&typeof window.io<"u")return window.io;throw new Error("Socket.io client not found. Should be globally available or passed via options.client")}listen(e,n,a){return this.channel(e).listen(n,a)}channel(e){return this.channels[e]||(this.channels[e]=new ky(this.socket,e,this.options)),this.channels[e]}privateChannel(e){return this.channels["private-"+e]||(this.channels["private-"+e]=new Ry(this.socket,"private-"+e,this.options)),this.channels["private-"+e]}presenceChannel(e){return this.channels["presence-"+e]||(this.channels["presence-"+e]=new pA(this.socket,"presence-"+e,this.options)),this.channels["presence-"+e]}leave(e){[e,"private-"+e,"presence-"+e].forEach(n=>{this.leaveChannel(n)})}leaveChannel(e){this.channels[e]&&(this.channels[e].unsubscribe(),delete this.channels[e])}socketId(){return this.socket.id}connectionStatus(){return this.socket.connected?"connected":this.socket.io._reconnecting?"reconnecting":this.socket.id!==void 0?"disconnected":"connecting"}onConnectionChange(e){const n=()=>{e(this.connectionStatus())},a=["connect","disconnect","connect_error","reconnect_attempt","reconnect","reconnect_error","reconnect_failed"];return a.forEach(i=>{this.socket.on(i,n)}),()=>{a.forEach(i=>{this.socket.off(i,n)})}}disconnect(){this.socket.disconnect()}}class Ly extends Of{constructor(){super(...arguments),this.channels={}}connect(){}listen(e,n,a){return new Cc}channel(e){return new Cc}privateChannel(e){return new Dy}encryptedPrivateChannel(e){return new gA}presenceChannel(e){return new mA}leave(e){}leaveChannel(e){}socketId(){return"fake-socket-id"}connectionStatus(){return"connected"}onConnectionChange(e){return()=>{}}disconnect(){}}class bA{constructor(e){this.options=e,this.connect(),this.options.withoutInterceptors||this.registerInterceptors()}channel(e){return this.connector.channel(e)}connect(){if(this.options.broadcaster==="reverb")this.connector=new Ec({...this.options,cluster:""});else if(this.options.broadcaster==="pusher")this.connector=new Ec(this.options);else if(this.options.broadcaster==="ably")this.connector=new Ec({...this.options,cluster:"",broadcaster:"pusher"});else if(this.options.broadcaster==="socket.io")this.connector=new yA(this.options);else if(this.options.broadcaster==="null")this.connector=new Ly(this.options);else if(typeof this.options.broadcaster=="function"&&dA(this.options.broadcaster))this.connector=new this.options.broadcaster(this.options);else throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} is not supported.`)}disconnect(){this.connector.disconnect()}join(e){return this.connector.presenceChannel(e)}leave(e){this.connector.leave(e)}leaveChannel(e){this.connector.leaveChannel(e)}leaveAllChannels(){for(const e in this.connector.channels)this.leaveChannel(e)}listen(e,n,a){return this.connector.listen(e,n,a)}private(e){return this.connector.privateChannel(e)}encryptedPrivate(e){if(this.connectorSupportsEncryptedPrivateChannels(this.connector))return this.connector.encryptedPrivateChannel(e);throw new Error(`Broadcaster ${typeof this.options.broadcaster} ${String(this.options.broadcaster)} does not support encrypted private channels.`)}connectorSupportsEncryptedPrivateChannels(e){return e instanceof Ec||e instanceof Ly}socketId(){return this.connector.socketId()}connectionStatus(){return this.connector.connectionStatus()}registerInterceptors(){typeof Vue<"u"&&Vue!=null&&Vue.http&&this.registerVueRequestInterceptor(),typeof axios=="function"&&this.registerAxiosRequestInterceptor(),typeof jQuery=="function"&&this.registerjQueryAjaxSetup(),typeof Turbo=="object"&&this.registerTurboRequestInterceptor()}registerVueRequestInterceptor(){Vue.http.interceptors.push((e,n)=>{this.socketId()&&e.headers.set("X-Socket-ID",this.socketId()),n()})}registerAxiosRequestInterceptor(){axios.interceptors.request.use(e=>(this.socketId()&&(e.headers["X-Socket-Id"]=this.socketId()),e))}registerjQueryAjaxSetup(){typeof jQuery.ajax<"u"&&jQuery.ajaxPrefilter((e,n,a)=>{this.socketId()&&a.setRequestHeader("X-Socket-Id",this.socketId())})}registerTurboRequestInterceptor(){document.addEventListener("turbo:before-fetch-request",e=>{e.detail.fetchOptions.headers["X-Socket-Id"]=this.socketId()})}}var Lf={exports:{}};var My;function vA(){return My||(My=1,(function(s,e){(function(a,i){s.exports=i()})(self,()=>(()=>{var n={594(c,d){var f=this&&this.__extends||(function(){var M=function(k,O){return M=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(B,G){B.__proto__=G}||function(B,G){for(var P in G)G.hasOwnProperty(P)&&(B[P]=G[P])},M(k,O)};return function(k,O){M(k,O);function B(){this.constructor=k}k.prototype=O===null?Object.create(O):(B.prototype=O.prototype,new B)}})();Object.defineProperty(d,"__esModule",{value:!0});var h=256,m=(function(){function M(k){k===void 0&&(k="="),this._paddingCharacter=k}return M.prototype.encodedLength=function(k){return this._paddingCharacter?(k+2)/3*4|0:(k*8+5)/6|0},M.prototype.encode=function(k){for(var O="",B=0;B<k.length-2;B+=3){var G=k[B]<<16|k[B+1]<<8|k[B+2];O+=this._encodeByte(G>>>18&63),O+=this._encodeByte(G>>>12&63),O+=this._encodeByte(G>>>6&63),O+=this._encodeByte(G>>>0&63)}var P=k.length-B;if(P>0){var G=k[B]<<16|(P===2?k[B+1]<<8:0);O+=this._encodeByte(G>>>18&63),O+=this._encodeByte(G>>>12&63),P===2?O+=this._encodeByte(G>>>6&63):O+=this._paddingCharacter||"",O+=this._paddingCharacter||""}return O},M.prototype.maxDecodedLength=function(k){return this._paddingCharacter?k/4*3|0:(k*6+7)/8|0},M.prototype.decodedLength=function(k){return this.maxDecodedLength(k.length-this._getPaddingLength(k))},M.prototype.decode=function(k){if(k.length===0)return new Uint8Array(0);for(var O=this._getPaddingLength(k),B=k.length-O,G=new Uint8Array(this.maxDecodedLength(B)),P=0,W=0,pe=0,Se=0,we=0,Ue=0,Ve=0;W<B-4;W+=4)Se=this._decodeChar(k.charCodeAt(W+0)),we=this._decodeChar(k.charCodeAt(W+1)),Ue=this._decodeChar(k.charCodeAt(W+2)),Ve=this._decodeChar(k.charCodeAt(W+3)),G[P++]=Se<<2|we>>>4,G[P++]=we<<4|Ue>>>2,G[P++]=Ue<<6|Ve,pe|=Se&h,pe|=we&h,pe|=Ue&h,pe|=Ve&h;if(W<B-1&&(Se=this._decodeChar(k.charCodeAt(W)),we=this._decodeChar(k.charCodeAt(W+1)),G[P++]=Se<<2|we>>>4,pe|=Se&h,pe|=we&h),W<B-2&&(Ue=this._decodeChar(k.charCodeAt(W+2)),G[P++]=we<<4|Ue>>>2,pe|=Ue&h),W<B-3&&(Ve=this._decodeChar(k.charCodeAt(W+3)),G[P++]=Ue<<6|Ve,pe|=Ve&h),pe!==0)throw new Error("Base64Coder: incorrect characters for decoding");return G},M.prototype._encodeByte=function(k){var O=k;return O+=65,O+=25-k>>>8&6,O+=51-k>>>8&-75,O+=61-k>>>8&-15,O+=62-k>>>8&3,String.fromCharCode(O)},M.prototype._decodeChar=function(k){var O=h;return O+=(42-k&k-44)>>>8&-h+k-43+62,O+=(46-k&k-48)>>>8&-h+k-47+63,O+=(47-k&k-58)>>>8&-h+k-48+52,O+=(64-k&k-91)>>>8&-h+k-65+0,O+=(96-k&k-123)>>>8&-h+k-97+26,O},M.prototype._getPaddingLength=function(k){var O=0;if(this._paddingCharacter){for(var B=k.length-1;B>=0&&k[B]===this._paddingCharacter;B--)O++;if(k.length<4||O>2)throw new Error("Base64Coder: incorrect padding")}return O},M})();d.Coder=m;var b=new m;function S(M){return b.encode(M)}d.encode=S;function v(M){return b.decode(M)}d.decode=v;var _=(function(M){f(k,M);function k(){return M!==null&&M.apply(this,arguments)||this}return k.prototype._encodeByte=function(O){var B=O;return B+=65,B+=25-O>>>8&6,B+=51-O>>>8&-75,B+=61-O>>>8&-13,B+=62-O>>>8&49,String.fromCharCode(B)},k.prototype._decodeChar=function(O){var B=h;return B+=(44-O&O-46)>>>8&-h+O-45+62,B+=(94-O&O-96)>>>8&-h+O-95+63,B+=(47-O&O-58)>>>8&-h+O-48+52,B+=(64-O&O-91)>>>8&-h+O-65+0,B+=(96-O&O-123)>>>8&-h+O-97+26,B},k})(m);d.URLSafeCoder=_;var A=new _;function x(M){return A.encode(M)}d.encodeURLSafe=x;function L(M){return A.decode(M)}d.decodeURLSafe=L,d.encodedLength=function(M){return b.encodedLength(M)},d.maxDecodedLength=function(M){return b.maxDecodedLength(M)},d.decodedLength=function(M){return b.decodedLength(M)}},978(c,d){var f="utf8: invalid source encoding";function h(m){for(var b=[],S=0;S<m.length;S++){var v=m[S];if(v&128){var _=void 0;if(v<224){if(S>=m.length)throw new Error(f);var A=m[++S];if((A&192)!==128)throw new Error(f);v=(v&31)<<6|A&63,_=128}else if(v<240){if(S>=m.length-1)throw new Error(f);var A=m[++S],x=m[++S];if((A&192)!==128||(x&192)!==128)throw new Error(f);v=(v&15)<<12|(A&63)<<6|x&63,_=2048}else if(v<248){if(S>=m.length-2)throw new Error(f);var A=m[++S],x=m[++S],L=m[++S];if((A&192)!==128||(x&192)!==128||(L&192)!==128)throw new Error(f);v=(v&15)<<18|(A&63)<<12|(x&63)<<6|L&63,_=65536}else throw new Error(f);if(v<_||v>=55296&&v<=57343)throw new Error(f);if(v>=65536){if(v>1114111)throw new Error(f);v-=65536,b.push(String.fromCharCode(55296|v>>10)),v=56320|v&1023}}b.push(String.fromCharCode(v))}return b.join("")}d.D4=h},721(c,d,f){c.exports=f(207).default},207(c,d,f){f.d(d,{default:()=>_s});class h{constructor(y,C){this.lastId=0,this.prefix=y,this.name=C}create(y){this.lastId++;var C=this.lastId,D=this.prefix+C,I=this.name+"["+C+"]",ne=!1,me=function(){ne||(y.apply(null,arguments),ne=!0)};return this[C]=me,{number:C,id:D,name:I,callback:me}}remove(y){delete this[y.number]}}var m=new h("_pusher_script_","Pusher.ScriptReceivers"),b={VERSION:"8.5.0",PROTOCOL:7,wsPort:80,wssPort:443,wsPath:"",httpHost:"sockjs.pusher.com",httpPort:80,httpsPort:443,httpPath:"/pusher",stats_host:"stats.pusher.com",authEndpoint:"/pusher/auth",authTransport:"ajax",activityTimeout:12e4,pongTimeout:3e4,unavailableTimeout:1e4,userAuthentication:{endpoint:"/pusher/user-auth",transport:"ajax"},channelAuthorization:{endpoint:"/pusher/auth",transport:"ajax"},cdn_http:"http://js.pusher.com",cdn_https:"https://js.pusher.com",dependency_suffix:""};const S=b;class v{constructor(y){this.options=y,this.receivers=y.receivers||m,this.loading={}}load(y,C,D){var I=this;if(I.loading[y]&&I.loading[y].length>0)I.loading[y].push(D);else{I.loading[y]=[D];var ne=ze.createScriptRequest(I.getPath(y,C)),me=I.receivers.create(function(Me){if(I.receivers.remove(me),I.loading[y]){var Ke=I.loading[y];delete I.loading[y];for(var ct=function(zt){zt||ne.cleanup()},gt=0;gt<Ke.length;gt++)Ke[gt](Me,ct)}});ne.send(me)}}getRoot(y){var C,D=ze.getDocument().location.protocol;return y&&y.useTLS||D==="https:"?C=this.options.cdn_https:C=this.options.cdn_http,C.replace(/\/*$/,"")+"/"+this.options.version}getPath(y,C){return this.getRoot(C)+"/"+y+this.options.suffix+".js"}}var _=new h("_pusher_dependencies","Pusher.DependenciesReceivers"),A=new v({cdn_http:S.cdn_http,cdn_https:S.cdn_https,version:S.VERSION,suffix:S.dependency_suffix,receivers:_});const x={baseUrl:"https://pusher.com",urls:{authenticationEndpoint:{path:"/docs/channels/server_api/authenticating_users"},authorizationEndpoint:{path:"/docs/channels/server_api/authorizing-users/"},javascriptQuickStart:{path:"/docs/javascript_quick_start"},triggeringClientEvents:{path:"/docs/client_api_guide/client_events#trigger-events"},encryptedChannelSupport:{fullUrl:"https://github.com/pusher/pusher-js/tree/cc491015371a4bde5743d1c87a0fbac0feb53195#encrypted-channel-support"}}},M={buildLogSuffix:function(E){const y="See:",C=x.urls[E];if(!C)return"";let D;return C.fullUrl?D=C.fullUrl:C.path&&(D=x.baseUrl+C.path),D?`${y} ${D}`:""}};var k;(function(E){E.UserAuthentication="user-authentication",E.ChannelAuthorization="channel-authorization"})(k||(k={}));class O extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class B extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class G extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class P extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class W extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class pe extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class Se extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class we extends Error{constructor(y){super(y),Object.setPrototypeOf(this,new.target.prototype)}}class Ue extends Error{constructor(y,C){super(C),this.status=y,Object.setPrototypeOf(this,new.target.prototype)}}const qe=function(E,y,C,D,I){const ne=ze.createXHR();ne.open("POST",C.endpoint,!0),ne.setRequestHeader("Content-Type","application/x-www-form-urlencoded");for(var me in C.headers)ne.setRequestHeader(me,C.headers[me]);if(C.headersProvider!=null){let Me=C.headersProvider();for(var me in Me)ne.setRequestHeader(me,Me[me])}return ne.onreadystatechange=function(){if(ne.readyState===4)if(ne.status===200){let Me,Ke=!1;try{Me=JSON.parse(ne.responseText),Ke=!0}catch{I(new Ue(200,`JSON returned from ${D.toString()} endpoint was invalid, yet status code was 200. Data was: ${ne.responseText}`),null)}Ke&&I(null,Me)}else{let Me="";switch(D){case k.UserAuthentication:Me=M.buildLogSuffix("authenticationEndpoint");break;case k.ChannelAuthorization:Me=`Clients must be authorized to join private or presence channels. ${M.buildLogSuffix("authorizationEndpoint")}`;break}I(new Ue(ne.status,`Unable to retrieve auth string from ${D.toString()} endpoint - received status: ${ne.status} from ${C.endpoint}. ${Me}`),null)}},ne.send(y),ne};function wt(E){return H(Z(E))}var J=String.fromCharCode,fe="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Le=function(E){var y=E.charCodeAt(0);return y<128?E:y<2048?J(192|y>>>6)+J(128|y&63):J(224|y>>>12&15)+J(128|y>>>6&63)+J(128|y&63)},Z=function(E){return E.replace(/[^\x00-\x7F]/g,Le)},ge=function(E){var y=[0,2,1][E.length%3],C=E.charCodeAt(0)<<16|(E.length>1?E.charCodeAt(1):0)<<8|(E.length>2?E.charCodeAt(2):0),D=[fe.charAt(C>>>18),fe.charAt(C>>>12&63),y>=2?"=":fe.charAt(C>>>6&63),y>=1?"=":fe.charAt(C&63)];return D.join("")},H=window.btoa||function(E){return E.replace(/[\s\S]{1,3}/g,ge)};class T{constructor(y,C,D,I){this.clear=C,this.timer=y(()=>{this.timer&&(this.timer=I(this.timer))},D)}isRunning(){return this.timer!==null}ensureAborted(){this.timer&&(this.clear(this.timer),this.timer=null)}}const ce=T;function de(E){window.clearTimeout(E)}function ye(E){window.clearInterval(E)}class ie extends ce{constructor(y,C){super(setTimeout,de,y,function(D){return C(),null})}}class xe extends ce{constructor(y,C){super(setInterval,ye,y,function(D){return C(),D})}}var _e={now(){return Date.now?Date.now():new Date().valueOf()},defer(E){return new ie(0,E)},method(E,...y){var C=Array.prototype.slice.call(arguments,1);return function(D){return D[E].apply(D,C.concat(arguments))}}};const Te=_e;function $e(E,...y){for(var C=0;C<y.length;C++){var D=y[C];for(var I in D)D[I]&&D[I].constructor&&D[I].constructor===Object?E[I]=$e(E[I]||{},D[I]):E[I]=D[I]}return E}function pt(){for(var E=["Pusher"],y=0;y<arguments.length;y++)typeof arguments[y]=="string"?E.push(arguments[y]):E.push(gn(arguments[y]));return E.join(" : ")}function Ct(E,y){var C=Array.prototype.indexOf;if(E===null)return-1;if(C&&E.indexOf===C)return E.indexOf(y);for(var D=0,I=E.length;D<I;D++)if(E[D]===y)return D;return-1}function Dt(E,y){for(var C in E)Object.prototype.hasOwnProperty.call(E,C)&&y(E[C],C,E)}function ht(E){var y=[];return Dt(E,function(C,D){y.push(D)}),y}function rn(E){var y=[];return Dt(E,function(C){y.push(C)}),y}function Kt(E,y,C){for(var D=0;D<E.length;D++)y.call(C||window,E[D],D,E)}function It(E,y){for(var C=[],D=0;D<E.length;D++)C.push(y(E[D],D,E,C));return C}function qr(E,y){var C={};return Dt(E,function(D,I){C[I]=y(D)}),C}function hn(E,y){y=y||function(I){return!!I};for(var C=[],D=0;D<E.length;D++)y(E[D],D,E,C)&&C.push(E[D]);return C}function an(E,y){var C={};return Dt(E,function(D,I){(y&&y(D,I,E,C)||D)&&(C[I]=D)}),C}function pn(E){var y=[];return Dt(E,function(C,D){y.push([D,C])}),y}function Kn(E,y){for(var C=0;C<E.length;C++)if(y(E[C],C,E))return!0;return!1}function Gr(E,y){for(var C=0;C<E.length;C++)if(!y(E[C],C,E))return!1;return!0}function qt(E){return qr(E,function(y){return typeof y=="object"&&(y=gn(y)),encodeURIComponent(wt(y.toString()))})}function _i(E){var y=an(E,function(D){return D!==void 0}),C=It(pn(qt(y)),Te.method("join","=")).join("&");return C}function aa(E){var y=[],C=[];return(function D(I,ne){var me,Me,Ke;switch(typeof I){case"object":if(!I)return null;for(me=0;me<y.length;me+=1)if(y[me]===I)return{$ref:C[me]};if(y.push(I),C.push(ne),Object.prototype.toString.apply(I)==="[object Array]")for(Ke=[],me=0;me<I.length;me+=1)Ke[me]=D(I[me],ne+"["+me+"]");else{Ke={};for(Me in I)Object.prototype.hasOwnProperty.call(I,Me)&&(Ke[Me]=D(I[Me],ne+"["+JSON.stringify(Me)+"]"))}return Ke;case"number":case"string":case"boolean":return I}})(E,"$")}function gn(E){try{return JSON.stringify(E)}catch{return JSON.stringify(aa(E))}}class Rn{constructor(){this.globalLog=y=>{window.console&&window.console.log&&window.console.log(y)}}debug(...y){this.log(this.globalLog,y)}warn(...y){this.log(this.globalLogWarn,y)}error(...y){this.log(this.globalLogError,y)}globalLogWarn(y){window.console&&window.console.warn?window.console.warn(y):this.globalLog(y)}globalLogError(y){window.console&&window.console.error?window.console.error(y):this.globalLogWarn(y)}log(y,...C){var D=pt.apply(this,arguments);_s.log?_s.log(D):_s.logToConsole&&y.bind(this)(D)}}const nt=new Rn;var Nn=function(E,y,C,D,I){(C.headers!==void 0||C.headersProvider!=null)&&nt.warn(`To send headers with the ${D.toString()} request, you must use AJAX, rather than JSONP.`);var ne=E.nextAuthCallbackID.toString();E.nextAuthCallbackID++;var me=E.getDocument(),Me=me.createElement("script");E.auth_callbacks[ne]=function(gt){I(null,gt)};var Ke="Pusher.auth_callbacks['"+ne+"']";Me.src=C.endpoint+"?callback="+encodeURIComponent(Ke)+"&"+y;var ct=me.getElementsByTagName("head")[0]||me.documentElement;ct.insertBefore(Me,ct.firstChild)};const mn=Nn;class ia{constructor(y){this.src=y}send(y){var C=this,D="Error loading "+C.src;C.script=document.createElement("script"),C.script.id=y.id,C.script.src=C.src,C.script.type="text/javascript",C.script.charset="UTF-8",C.script.addEventListener?(C.script.onerror=function(){y.callback(D)},C.script.onload=function(){y.callback(null)}):C.script.onreadystatechange=function(){(C.script.readyState==="loaded"||C.script.readyState==="complete")&&y.callback(null)},C.script.async===void 0&&document.attachEvent&&/opera/i.test(navigator.userAgent)?(C.errorScript=document.createElement("script"),C.errorScript.id=y.id+"_error",C.errorScript.text=y.name+"('"+D+"');",C.script.async=C.errorScript.async=!1):C.script.async=!0;var I=document.getElementsByTagName("head")[0];I.insertBefore(C.script,I.firstChild),C.errorScript&&I.insertBefore(C.errorScript,C.script.nextSibling)}cleanup(){this.script&&(this.script.onload=this.script.onerror=null,this.script.onreadystatechange=null),this.script&&this.script.parentNode&&this.script.parentNode.removeChild(this.script),this.errorScript&&this.errorScript.parentNode&&this.errorScript.parentNode.removeChild(this.errorScript),this.script=null,this.errorScript=null}}class or{constructor(y,C){this.url=y,this.data=C}send(y){if(!this.request){var C=_i(this.data),D=this.url+"/"+y.number+"?"+C;this.request=ze.createScriptRequest(D),this.request.send(y)}}cleanup(){this.request&&this.request.cleanup()}}var sn=function(E,y){return function(C,D){var I="http"+(y?"s":"")+"://",ne=I+(E.host||E.options.host)+E.options.path,me=ze.createJSONPRequest(ne,C),Me=ze.ScriptReceivers.create(function(Ke,ct){m.remove(Me),me.cleanup(),ct&&ct.host&&(E.host=ct.host),D&&D(Ke,ct)});me.send(Me)}},_n={name:"jsonp",getAgent:sn};const on=_n;function V(E,y,C){var D=E+(y.useTLS?"s":""),I=y.useTLS?y.hostTLS:y.hostNonTLS;return D+"://"+I+C}function se(E,y){var C="/app/"+E,D="?protocol="+S.PROTOCOL+"&client=js&version="+S.VERSION+(y?"&"+y:"");return C+D}var ue={getInitial:function(E,y){var C=(y.httpPath||"")+se(E,"flash=false");return V("ws",y,C)}},ee={getInitial:function(E,y){var C=(y.httpPath||"/pusher")+se(E);return V("http",y,C)}},ve={getInitial:function(E,y){return V("http",y,y.httpPath||"/pusher")},getPath:function(E,y){return se(E)}};class he{constructor(){this._callbacks={}}get(y){return this._callbacks[Ee(y)]}add(y,C,D){var I=Ee(y);this._callbacks[I]=this._callbacks[I]||[],this._callbacks[I].push({fn:C,context:D})}remove(y,C,D){if(!y&&!C&&!D){this._callbacks={};return}var I=y?[Ee(y)]:ht(this._callbacks);C||D?this.removeCallback(I,C,D):this.removeAllCallbacks(I)}removeCallback(y,C,D){Kt(y,function(I){this._callbacks[I]=hn(this._callbacks[I]||[],function(ne){return C&&C!==ne.fn||D&&D!==ne.context}),this._callbacks[I].length===0&&delete this._callbacks[I]},this)}removeAllCallbacks(y){Kt(y,function(C){delete this._callbacks[C]},this)}}function Ee(E){return"_"+E}class Ae{constructor(y){this.callbacks=new he,this.global_callbacks=[],this.failThrough=y}bind(y,C,D){return this.callbacks.add(y,C,D),this}bind_global(y){return this.global_callbacks.push(y),this}unbind(y,C,D){return this.callbacks.remove(y,C,D),this}unbind_global(y){return y?(this.global_callbacks=hn(this.global_callbacks||[],C=>C!==y),this):(this.global_callbacks=[],this)}unbind_all(){return this.unbind(),this.unbind_global(),this}emit(y,C,D){for(var I=0;I<this.global_callbacks.length;I++)this.global_callbacks[I](y,C);var ne=this.callbacks.get(y),me=[];if(D?me.push(C,D):C&&me.push(C),ne&&ne.length>0)for(var I=0;I<ne.length;I++)ne[I].fn.apply(ne[I].context||window,me);else this.failThrough&&this.failThrough(y,C);return this}}class Fe extends Ae{constructor(y,C,D,I,ne){super(),this.initialize=ze.transportConnectionInitializer,this.hooks=y,this.name=C,this.priority=D,this.key=I,this.options=ne,this.state="new",this.timeline=ne.timeline,this.activityTimeout=ne.activityTimeout,this.id=this.timeline.generateUniqueID()}handlesActivityChecks(){return!!this.hooks.handlesActivityChecks}supportsPing(){return!!this.hooks.supportsPing}connect(){if(this.socket||this.state!=="initialized")return!1;var y=this.hooks.urls.getInitial(this.key,this.options);try{this.socket=this.hooks.getSocket(y,this.options)}catch(C){return Te.defer(()=>{this.onError(C),this.changeState("closed")}),!1}return this.bindListeners(),nt.debug("Connecting",{transport:this.name,url:y}),this.changeState("connecting"),!0}close(){return this.socket?(this.socket.close(),!0):!1}send(y){return this.state==="open"?(Te.defer(()=>{this.socket&&this.socket.send(y)}),!0):!1}ping(){this.state==="open"&&this.supportsPing()&&this.socket.ping()}onOpen(){this.hooks.beforeOpen&&this.hooks.beforeOpen(this.socket,this.hooks.urls.getPath(this.key,this.options)),this.changeState("open"),this.socket.onopen=void 0}onError(y){this.emit("error",{type:"WebSocketError",error:y}),this.timeline.error(this.buildTimelineMessage({error:y.toString()}))}onClose(y){y?this.changeState("closed",{code:y.code,reason:y.reason,wasClean:y.wasClean}):this.changeState("closed"),this.unbindListeners(),this.socket=void 0}onMessage(y){this.emit("message",y)}onActivity(){this.emit("activity")}bindListeners(){this.socket.onopen=()=>{this.onOpen()},this.socket.onerror=y=>{this.onError(y)},this.socket.onclose=y=>{this.onClose(y)},this.socket.onmessage=y=>{this.onMessage(y)},this.supportsPing()&&(this.socket.onactivity=()=>{this.onActivity()})}unbindListeners(){this.socket&&(this.socket.onopen=void 0,this.socket.onerror=void 0,this.socket.onclose=void 0,this.socket.onmessage=void 0,this.supportsPing()&&(this.socket.onactivity=void 0))}changeState(y,C){this.state=y,this.timeline.info(this.buildTimelineMessage({state:y,params:C})),this.emit(y,C)}buildTimelineMessage(y){return $e({cid:this.id},y)}}class ke{constructor(y){this.hooks=y}isSupported(y){return this.hooks.isSupported(y)}createConnection(y,C,D,I){return new Fe(this.hooks,y,C,D,I)}}var Ne=new ke({urls:ue,handlesActivityChecks:!1,supportsPing:!1,isInitialized:function(){return!!ze.getWebSocketAPI()},isSupported:function(){return!!ze.getWebSocketAPI()},getSocket:function(E){return ze.createWebSocket(E)}}),Re={urls:ee,handlesActivityChecks:!1,supportsPing:!0,isInitialized:function(){return!0}},et=$e({getSocket:function(E){return ze.HTTPFactory.createStreamingSocket(E)}},Re),He=$e({getSocket:function(E){return ze.HTTPFactory.createPollingSocket(E)}},Re),Ge={isSupported:function(){return ze.isXHRSupported()}},Ce=new ke($e({},et,Ge)),je=new ke($e({},He,Ge)),tt={ws:Ne,xhr_streaming:Ce,xhr_polling:je};const At=tt;var Mt=new ke({file:"sockjs",urls:ve,handlesActivityChecks:!0,supportsPing:!1,isSupported:function(){return!0},isInitialized:function(){return window.SockJS!==void 0},getSocket:function(E,y){return new window.SockJS(E,null,{js_path:A.getPath("sockjs",{useTLS:y.useTLS}),ignore_null_origin:y.ignoreNullOrigin})},beforeOpen:function(E,y){E.send(JSON.stringify({path:y}))}}),Dn={isSupported:function(E){var y=ze.isXDRSupported(E.useTLS);return y}},jt=new ke($e({},et,Dn)),Ht=new ke($e({},He,Dn));At.xdr_streaming=jt,At.xdr_polling=Ht,At.sockjs=Mt;const An=At;class Ia extends Ae{constructor(){super();var y=this;window.addEventListener!==void 0&&(window.addEventListener("online",function(){y.emit("online")},!1),window.addEventListener("offline",function(){y.emit("offline")},!1))}isOnline(){return window.navigator.onLine===void 0?!0:window.navigator.onLine}}var Ho=new Ia;class zf{constructor(y,C,D){this.manager=y,this.transport=C,this.minPingDelay=D.minPingDelay,this.maxPingDelay=D.maxPingDelay,this.pingDelay=void 0}createConnection(y,C,D,I){I=$e({},I,{activityTimeout:this.pingDelay});var ne=this.transport.createConnection(y,C,D,I),me=null,Me=function(){ne.unbind("open",Me),ne.bind("closed",Ke),me=Te.now()},Ke=ct=>{if(ne.unbind("closed",Ke),ct.code===1002||ct.code===1003)this.manager.reportDeath();else if(!ct.wasClean&&me){var gt=Te.now()-me;gt<2*this.maxPingDelay&&(this.manager.reportDeath(),this.pingDelay=Math.max(gt/2,this.minPingDelay))}};return ne.bind("open",Me),ne}isSupported(y){return this.manager.isAlive()&&this.transport.isSupported(y)}}const Dc={decodeMessage:function(E){try{var y=JSON.parse(E.data),C=y.data;if(typeof C=="string")try{C=JSON.parse(y.data)}catch{}var D={event:y.event,channel:y.channel,data:C};return y.user_id&&(D.user_id=y.user_id),D}catch(I){throw{type:"MessageParseError",error:I,data:E.data}}},encodeMessage:function(E){return JSON.stringify(E)},processHandshake:function(E){var y=Dc.decodeMessage(E);if(y.event==="pusher:connection_established"){if(!y.data.activity_timeout)throw"No activity timeout specified in handshake";return{action:"connected",id:y.data.socket_id,activityTimeout:y.data.activity_timeout*1e3}}else{if(y.event==="pusher:error")return{action:this.getCloseAction(y.data),error:this.getCloseError(y.data)};throw"Invalid handshake"}},getCloseAction:function(E){return E.code<4e3?E.code>=1002&&E.code<=1004?"backoff":null:E.code===4e3?"tls_only":E.code<4100?"refused":E.code<4200?"backoff":E.code<4300?"retry":"refused"},getCloseError:function(E){return E.code!==1e3&&E.code!==1001?{type:"PusherError",data:{code:E.code,message:E.reason||E.message}}:null}},Vr=Dc;class Ai extends Ae{constructor(y,C){super(),this.id=y,this.transport=C,this.activityTimeout=C.activityTimeout,this.bindListeners()}handlesActivityChecks(){return this.transport.handlesActivityChecks()}send(y){return this.transport.send(y)}send_event(y,C,D){var I={event:y,data:C};return D&&(I.channel=D),nt.debug("Event sent",I),this.send(Vr.encodeMessage(I))}ping(){this.transport.supportsPing()?this.transport.ping():this.send_event("pusher:ping",{})}close(){this.transport.close()}bindListeners(){var y={message:D=>{var I;try{I=Vr.decodeMessage(D)}catch(ne){this.emit("error",{type:"MessageParseError",error:ne,data:D.data})}if(I!==void 0){switch(nt.debug("Event recd",I),I.event){case"pusher:error":this.emit("error",{type:"PusherError",data:I.data});break;case"pusher:ping":this.emit("ping");break;case"pusher:pong":this.emit("pong");break}this.emit("message",I)}},activity:()=>{this.emit("activity")},error:D=>{this.emit("error",D)},closed:D=>{C(),D&&D.code&&this.handleCloseEvent(D),this.transport=null,this.emit("closed")}},C=()=>{Dt(y,(D,I)=>{this.transport.unbind(I,D)})};Dt(y,(D,I)=>{this.transport.bind(I,D)})}handleCloseEvent(y){var C=Vr.getCloseAction(y),D=Vr.getCloseError(y);D&&this.emit("error",D),C&&this.emit(C,{action:C,error:D})}}class zo{constructor(y,C){this.transport=y,this.callback=C,this.bindListeners()}close(){this.unbindListeners(),this.transport.close()}bindListeners(){this.onMessage=y=>{this.unbindListeners();var C;try{C=Vr.processHandshake(y)}catch(D){this.finish("error",{error:D}),this.transport.close();return}C.action==="connected"?this.finish("connected",{connection:new Ai(C.id,this.transport),activityTimeout:C.activityTimeout}):(this.finish(C.action,{error:C.error}),this.transport.close())},this.onClosed=y=>{this.unbindListeners();var C=Vr.getCloseAction(y)||"backoff",D=Vr.getCloseError(y);this.finish(C,{error:D})},this.transport.bind("message",this.onMessage),this.transport.bind("closed",this.onClosed)}unbindListeners(){this.transport.unbind("message",this.onMessage),this.transport.unbind("closed",this.onClosed)}finish(y,C){this.callback($e({transport:this.transport,action:y},C))}}class ja{constructor(y,C){this.timeline=y,this.options=C||{}}send(y,C){this.timeline.isEmpty()||this.timeline.send(ze.TimelineTransport.getAgent(this,y),C)}}class Fr extends Ae{constructor(y,C){super(function(D,I){nt.debug("No callbacks on "+y+" for "+D)}),this.name=y,this.pusher=C,this.subscribed=!1,this.subscriptionPending=!1,this.subscriptionCancelled=!1}authorize(y,C){return C(null,{auth:""})}trigger(y,C){if(y.indexOf("client-")!==0)throw new O("Event '"+y+"' does not start with 'client-'");if(!this.subscribed){var D=M.buildLogSuffix("triggeringClientEvents");nt.warn(`Client event triggered before channel 'subscription_succeeded' event . ${D}`)}return this.pusher.send_event(y,C,this.name)}disconnect(){this.subscribed=!1,this.subscriptionPending=!1}handleEvent(y){var C=y.event,D=y.data;if(C==="pusher_internal:subscription_succeeded")this.handleSubscriptionSucceededEvent(y);else if(C==="pusher_internal:subscription_count")this.handleSubscriptionCountEvent(y);else if(C.indexOf("pusher_internal:")!==0){var I={};this.emit(C,D,I)}}handleSubscriptionSucceededEvent(y){this.subscriptionPending=!1,this.subscribed=!0,this.subscriptionCancelled?this.pusher.unsubscribe(this.name):this.emit("pusher:subscription_succeeded",y.data)}handleSubscriptionCountEvent(y){y.data.subscription_count&&(this.subscriptionCount=y.data.subscription_count),this.emit("pusher:subscription_count",y.data)}subscribe(){this.subscribed||(this.subscriptionPending=!0,this.subscriptionCancelled=!1,this.authorize(this.pusher.connection.socket_id,(y,C)=>{y?(this.subscriptionPending=!1,nt.error(y.toString()),this.emit("pusher:subscription_error",Object.assign({},{type:"AuthError",error:y.message},y instanceof Ue?{status:y.status}:{}))):this.pusher.send_event("pusher:subscribe",{auth:C.auth,channel_data:C.channel_data,channel:this.name})}))}unsubscribe(){this.subscribed=!1,this.pusher.send_event("pusher:unsubscribe",{channel:this.name})}cancelSubscription(){this.subscriptionCancelled=!0}reinstateSubscription(){this.subscriptionCancelled=!1}}class sa extends Fr{authorize(y,C){return this.pusher.config.channelAuthorizer({channelName:this.name,socketId:y},C)}}class Ha{constructor(){this.reset()}get(y){return Object.prototype.hasOwnProperty.call(this.members,y)?{id:y,info:this.members[y]}:null}each(y){Dt(this.members,(C,D)=>{y(this.get(D))})}setMyID(y){this.myID=y}onSubscription(y){this.members=y.presence.hash,this.count=y.presence.count,this.me=this.get(this.myID)}addMember(y){return this.get(y.user_id)===null&&this.count++,this.members[y.user_id]=y.user_info,this.get(y.user_id)}removeMember(y){var C=this.get(y.user_id);return C&&(delete this.members[y.user_id],this.count--),C}reset(){this.members={},this.count=0,this.myID=null,this.me=null}}var Jt=function(E,y,C,D){function I(ne){return ne instanceof C?ne:new C(function(me){me(ne)})}return new(C||(C=Promise))(function(ne,me){function Me(gt){try{ct(D.next(gt))}catch(zt){me(zt)}}function Ke(gt){try{ct(D.throw(gt))}catch(zt){me(zt)}}function ct(gt){gt.done?ne(gt.value):I(gt.value).then(Me,Ke)}ct((D=D.apply(E,y||[])).next())})};class Oc extends sa{constructor(y,C){super(y,C),this.members=new Ha}authorize(y,C){super.authorize(y,(D,I)=>Jt(this,void 0,void 0,function*(){if(!D)if(I=I,I.channel_data!=null){var ne=JSON.parse(I.channel_data);this.members.setMyID(ne.user_id)}else if(yield this.pusher.user.signinDonePromise,this.pusher.user.user_data!=null)this.members.setMyID(this.pusher.user.user_data.id);else{let me=M.buildLogSuffix("authorizationEndpoint");nt.error(`Invalid auth response for channel '${this.name}', expected 'channel_data' field. ${me}, or the user should be signed in.`),C("Invalid auth response");return}C(D,I)}))}handleEvent(y){var C=y.event;if(C.indexOf("pusher_internal:")===0)this.handleInternalEvent(y);else{var D=y.data,I={};y.user_id&&(I.user_id=y.user_id),this.emit(C,D,I)}}handleInternalEvent(y){var C=y.event,D=y.data;switch(C){case"pusher_internal:subscription_succeeded":this.handleSubscriptionSucceededEvent(y);break;case"pusher_internal:subscription_count":this.handleSubscriptionCountEvent(y);break;case"pusher_internal:member_added":var I=this.members.addMember(D);this.emit("pusher:member_added",I);break;case"pusher_internal:member_removed":var ne=this.members.removeMember(D);ne&&this.emit("pusher:member_removed",ne);break}}handleSubscriptionSucceededEvent(y){this.subscriptionPending=!1,this.subscribed=!0,this.subscriptionCancelled?this.pusher.unsubscribe(this.name):(this.members.onSubscription(y.data),this.emit("pusher:subscription_succeeded",this.members))}disconnect(){this.members.reset(),super.disconnect()}}var Lc=f(978),kr=f(594);class za extends sa{constructor(y,C,D){super(y,C),this.key=null,this.nacl=D}authorize(y,C){super.authorize(y,(D,I)=>{if(D){C(D,I);return}let ne=I.shared_secret;if(!ne){C(new Error(`No shared_secret key in auth payload for encrypted channel: ${this.name}`),null);return}this.key=(0,kr.decode)(ne),delete I.shared_secret,C(null,I)})}trigger(y,C){throw new pe("Client events are not currently supported for encrypted channels")}handleEvent(y){var C=y.event,D=y.data;if(C.indexOf("pusher_internal:")===0||C.indexOf("pusher:")===0){super.handleEvent(y);return}this.handleEncryptedEvent(C,D)}handleEncryptedEvent(y,C){if(!this.key){nt.debug("Received encrypted event before key has been retrieved from the authEndpoint");return}if(!C.ciphertext||!C.nonce){nt.error("Unexpected format for encrypted event, expected object with `ciphertext` and `nonce` fields, got: "+C);return}let D=(0,kr.decode)(C.ciphertext);if(D.length<this.nacl.secretbox.overheadLength){nt.error(`Expected encrypted event ciphertext length to be ${this.nacl.secretbox.overheadLength}, got: ${D.length}`);return}let I=(0,kr.decode)(C.nonce);if(I.length<this.nacl.secretbox.nonceLength){nt.error(`Expected encrypted event nonce length to be ${this.nacl.secretbox.nonceLength}, got: ${I.length}`);return}let ne=this.nacl.secretbox.open(D,I,this.key);if(ne===null){nt.debug("Failed to decrypt an event, probably because it was encrypted with a different key. Fetching a new key from the authEndpoint..."),this.authorize(this.pusher.connection.socket_id,(me,Me)=>{if(me){nt.error(`Failed to make a request to the authEndpoint: ${Me}. Unable to fetch new key, so dropping encrypted event`);return}if(ne=this.nacl.secretbox.open(D,I,this.key),ne===null){nt.error("Failed to decrypt event with new key. Dropping encrypted event");return}this.emit(y,this.getDataToEmit(ne))});return}this.emit(y,this.getDataToEmit(ne))}getDataToEmit(y){let C=(0,Lc.D4)(y);try{return JSON.parse(C)}catch{return C}}}class Uf extends Ae{constructor(y,C){super(),this.state="initialized",this.connection=null,this.key=y,this.options=C,this.timeline=this.options.timeline,this.usingTLS=this.options.useTLS,this.errorCallbacks=this.buildErrorCallbacks(),this.connectionCallbacks=this.buildConnectionCallbacks(this.errorCallbacks),this.handshakeCallbacks=this.buildHandshakeCallbacks(this.errorCallbacks);var D=ze.getNetwork();D.bind("online",()=>{this.timeline.info({netinfo:"online"}),(this.state==="connecting"||this.state==="unavailable")&&this.retryIn(0)}),D.bind("offline",()=>{this.timeline.info({netinfo:"offline"}),this.connection&&this.sendActivityCheck()}),this.updateStrategy()}switchCluster(y){this.key=y,this.updateStrategy(),this.retryIn(0)}connect(){if(!(this.connection||this.runner)){if(!this.strategy.isSupported()){this.updateState("failed");return}this.updateState("connecting"),this.startConnecting(),this.setUnavailableTimer()}}send(y){return this.connection?this.connection.send(y):!1}send_event(y,C,D){return this.connection?this.connection.send_event(y,C,D):!1}disconnect(){this.disconnectInternally(),this.updateState("disconnected")}isUsingTLS(){return this.usingTLS}startConnecting(){var y=(C,D)=>{C?this.runner=this.strategy.connect(0,y):D.action==="error"?(this.emit("error",{type:"HandshakeError",error:D.error}),this.timeline.error({handshakeError:D.error})):(this.abortConnecting(),this.handshakeCallbacks[D.action](D))};this.runner=this.strategy.connect(0,y)}abortConnecting(){this.runner&&(this.runner.abort(),this.runner=null)}disconnectInternally(){if(this.abortConnecting(),this.clearRetryTimer(),this.clearUnavailableTimer(),this.connection){var y=this.abandonConnection();y.close()}}updateStrategy(){this.strategy=this.options.getStrategy({key:this.key,timeline:this.timeline,useTLS:this.usingTLS})}retryIn(y){this.timeline.info({action:"retry",delay:y}),y>0&&this.emit("connecting_in",Math.round(y/1e3)),this.retryTimer=new ie(y||0,()=>{this.disconnectInternally(),this.connect()})}clearRetryTimer(){this.retryTimer&&(this.retryTimer.ensureAborted(),this.retryTimer=null)}setUnavailableTimer(){this.unavailableTimer=new ie(this.options.unavailableTimeout,()=>{this.updateState("unavailable")})}clearUnavailableTimer(){this.unavailableTimer&&this.unavailableTimer.ensureAborted()}sendActivityCheck(){this.stopActivityCheck(),this.connection.ping(),this.activityTimer=new ie(this.options.pongTimeout,()=>{this.timeline.error({pong_timed_out:this.options.pongTimeout}),this.retryIn(0)})}resetActivityCheck(){this.stopActivityCheck(),this.connection&&!this.connection.handlesActivityChecks()&&(this.activityTimer=new ie(this.activityTimeout,()=>{this.sendActivityCheck()}))}stopActivityCheck(){this.activityTimer&&this.activityTimer.ensureAborted()}buildConnectionCallbacks(y){return $e({},y,{message:C=>{this.resetActivityCheck(),this.emit("message",C)},ping:()=>{this.send_event("pusher:pong",{})},activity:()=>{this.resetActivityCheck()},error:C=>{this.emit("error",C)},closed:()=>{this.abandonConnection(),this.shouldRetry()&&this.retryIn(1e3)}})}buildHandshakeCallbacks(y){return $e({},y,{connected:C=>{this.activityTimeout=Math.min(this.options.activityTimeout,C.activityTimeout,C.connection.activityTimeout||1/0),this.clearUnavailableTimer(),this.setConnection(C.connection),this.socket_id=this.connection.id,this.updateState("connected",{socket_id:this.socket_id})}})}buildErrorCallbacks(){let y=C=>D=>{D.error&&this.emit("error",{type:"WebSocketError",error:D.error}),C(D)};return{tls_only:y(()=>{this.usingTLS=!0,this.updateStrategy(),this.retryIn(0)}),refused:y(()=>{this.disconnect()}),backoff:y(()=>{this.retryIn(1e3)}),retry:y(()=>{this.retryIn(0)})}}setConnection(y){this.connection=y;for(var C in this.connectionCallbacks)this.connection.bind(C,this.connectionCallbacks[C]);this.resetActivityCheck()}abandonConnection(){if(this.connection){this.stopActivityCheck();for(var y in this.connectionCallbacks)this.connection.unbind(y,this.connectionCallbacks[y]);var C=this.connection;return this.connection=null,C}}updateState(y,C){var D=this.state;if(this.state=y,D!==y){var I=y;I==="connected"&&(I+=" with new socket ID "+C.socket_id),nt.debug("State changed",D+" -> "+I),this.timeline.info({state:y,params:C}),this.emit("state_change",{previous:D,current:y}),this.emit(y,C)}}shouldRetry(){return this.state==="connecting"||this.state==="connected"}}class Mc{constructor(){this.channels={}}add(y,C){return this.channels[y]||(this.channels[y]=$c(y,C)),this.channels[y]}all(){return rn(this.channels)}find(y){return this.channels[y]}remove(y){var C=this.channels[y];return delete this.channels[y],C}disconnect(){Dt(this.channels,function(y){y.disconnect()})}}function $c(E,y){if(E.indexOf("private-encrypted-")===0){if(y.config.nacl)return In.createEncryptedChannel(E,y,y.config.nacl);let C="Tried to subscribe to a private-encrypted- channel but no nacl implementation available",D=M.buildLogSuffix("encryptedChannelSupport");throw new pe(`${C}. ${D}`)}else{if(E.indexOf("private-")===0)return In.createPrivateChannel(E,y);if(E.indexOf("presence-")===0)return In.createPresenceChannel(E,y);if(E.indexOf("#")===0)throw new B('Cannot create a channel with name "'+E+'".');return In.createChannel(E,y)}}var Pf={createChannels(){return new Mc},createConnectionManager(E,y){return new Uf(E,y)},createChannel(E,y){return new Fr(E,y)},createPrivateChannel(E,y){return new sa(E,y)},createPresenceChannel(E,y){return new Oc(E,y)},createEncryptedChannel(E,y,C){return new za(E,y,C)},createTimelineSender(E,y){return new ja(E,y)},createHandshake(E,y){return new zo(E,y)},createAssistantToTheTransportManager(E,y,C){return new zf(E,y,C)}};const In=Pf;class xi{constructor(y){this.options=y||{},this.livesLeft=this.options.lives||1/0}getAssistant(y){return In.createAssistantToTheTransportManager(this,y,{minPingDelay:this.options.minPingDelay,maxPingDelay:this.options.maxPingDelay})}isAlive(){return this.livesLeft>0}reportDeath(){this.livesLeft-=1}}class yn{constructor(y,C){this.strategies=y,this.loop=!!C.loop,this.failFast=!!C.failFast,this.timeout=C.timeout,this.timeoutLimit=C.timeoutLimit}isSupported(){return Kn(this.strategies,Te.method("isSupported"))}connect(y,C){var D=this.strategies,I=0,ne=this.timeout,me=null,Me=(Ke,ct)=>{ct?C(null,ct):(I=I+1,this.loop&&(I=I%D.length),I<D.length?(ne&&(ne=ne*2,this.timeoutLimit&&(ne=Math.min(ne,this.timeoutLimit))),me=this.tryStrategy(D[I],y,{timeout:ne,failFast:this.failFast},Me)):C(!0))};return me=this.tryStrategy(D[I],y,{timeout:ne,failFast:this.failFast},Me),{abort:function(){me.abort()},forceMinPriority:function(Ke){y=Ke,me&&me.forceMinPriority(Ke)}}}tryStrategy(y,C,D,I){var ne=null,me=null;return D.timeout>0&&(ne=new ie(D.timeout,function(){me.abort(),I(!0)})),me=y.connect(C,function(Me,Ke){Me&&ne&&ne.isRunning()&&!D.failFast||(ne&&ne.ensureAborted(),I(Me,Ke))}),{abort:function(){ne&&ne.ensureAborted(),me.abort()},forceMinPriority:function(Me){me.forceMinPriority(Me)}}}}class xn{constructor(y){this.strategies=y}isSupported(){return Kn(this.strategies,Te.method("isSupported"))}connect(y,C){return Nc(this.strategies,y,function(D,I){return function(ne,me){if(I[D].error=ne,ne){Bf(I)&&C(!0);return}Kt(I,function(Me){Me.forceMinPriority(me.transport.priority)}),C(null,me)}})}}function Nc(E,y,C){var D=It(E,function(I,ne,me,Me){return I.connect(y,C(ne,Me))});return{abort:function(){Kt(D,Uo)},forceMinPriority:function(I){Kt(D,function(ne){ne.forceMinPriority(I)})}}}function Bf(E){return Gr(E,function(y){return!!y.error})}function Uo(E){!E.error&&!E.aborted&&(E.abort(),E.aborted=!0)}class Ic{constructor(y,C,D){this.strategy=y,this.transports=C,this.ttl=D.ttl||18e5,this.usingTLS=D.useTLS,this.timeline=D.timeline}isSupported(){return this.strategy.isSupported()}connect(y,C){var D=this.usingTLS,I=qf(D),ne=I&&I.cacheSkipCount?I.cacheSkipCount:0,me=[this.strategy];if(I&&I.timestamp+this.ttl>=Te.now()){var Me=this.transports[I.transport];Me&&(["ws","wss"].includes(I.transport)||ne>3?(this.timeline.info({cached:!0,transport:I.transport,latency:I.latency}),me.push(new yn([Me],{timeout:I.latency*2+1e3,failFast:!0}))):ne++)}var Ke=Te.now(),ct=me.pop().connect(y,function gt(zt,Lr){zt?(vs(D),me.length>0?(Ke=Te.now(),ct=me.pop().connect(y,gt)):C(zt)):(jn(D,Lr.transport.name,Te.now()-Ke,ne),C(null,Lr))});return{abort:function(){ct.abort()},forceMinPriority:function(gt){y=gt,ct&&ct.forceMinPriority(gt)}}}}function Ua(E){return"pusherTransport"+(E?"TLS":"NonTLS")}function qf(E){var y=ze.getLocalStorage();if(y)try{var C=y[Ua(E)];if(C)return JSON.parse(C)}catch{vs(E)}return null}function jn(E,y,C,D){var I=ze.getLocalStorage();if(I)try{I[Ua(E)]=gn({timestamp:Te.now(),transport:y,latency:C,cacheSkipCount:D})}catch{}}function vs(E){var y=ze.getLocalStorage();if(y)try{delete y[Ua(E)]}catch{}}class Ti{constructor(y,{delay:C}){this.strategy=y,this.options={delay:C}}isSupported(){return this.strategy.isSupported()}connect(y,C){var D=this.strategy,I,ne=new ie(this.options.delay,function(){I=D.connect(y,C)});return{abort:function(){ne.ensureAborted(),I&&I.abort()},forceMinPriority:function(me){y=me,I&&I.forceMinPriority(me)}}}}class oa{constructor(y,C,D){this.test=y,this.trueBranch=C,this.falseBranch=D}isSupported(){var y=this.test()?this.trueBranch:this.falseBranch;return y.isSupported()}connect(y,C){var D=this.test()?this.trueBranch:this.falseBranch;return D.connect(y,C)}}class Pa{constructor(y){this.strategy=y}isSupported(){return this.strategy.isSupported()}connect(y,C){var D=this.strategy.connect(y,function(I,ne){ne&&D.abort(),C(I,ne)});return D}}function Ba(E){return function(){return E.isSupported()}}var jc=function(E,y,C){var D={};function I(el,Jc,Qc,uh,dh){var Zc=C(E,el,Jc,Qc,uh,dh);return D[el]=Zc,Zc}var ne=Object.assign({},y,{hostNonTLS:E.wsHost+":"+E.wsPort,hostTLS:E.wsHost+":"+E.wssPort,httpPath:E.wsPath}),me=Object.assign({},ne,{useTLS:!0}),Me=Object.assign({},y,{hostNonTLS:E.httpHost+":"+E.httpPort,hostTLS:E.httpHost+":"+E.httpsPort,httpPath:E.httpPath}),Ke={loop:!0,timeout:15e3,timeoutLimit:6e4},ct=new xi({minPingDelay:1e4,maxPingDelay:E.activityTimeout}),gt=new xi({lives:2,minPingDelay:1e4,maxPingDelay:E.activityTimeout}),zt=I("ws","ws",3,ne,ct),Lr=I("wss","ws",3,me,ct),oh=I("sockjs","sockjs",1,Me),Yc=I("xhr_streaming","xhr_streaming",1,Me,gt),lh=I("xdr_streaming","xdr_streaming",1,Me,gt),Jo=I("xhr_polling","xhr_polling",1,Me),Xc=I("xdr_polling","xdr_polling",1,Me),Va=new yn([zt],Ke),Mi=new yn([Lr],Ke),ch=new yn([oh],Ke),$i=new yn([new oa(Ba(Yc),Yc,lh)],Ke),Qo=new yn([new oa(Ba(Jo),Jo,Xc)],Ke),Zo=new yn([new oa(Ba($i),new xn([$i,new Ti(Qo,{delay:4e3})]),Qo)],Ke),Ni=new oa(Ba(Zo),Zo,ch),Ii;return y.useTLS?Ii=new xn([Va,new Ti(Ni,{delay:2e3})]):Ii=new xn([Va,new Ti(Mi,{delay:2e3}),new Ti(Ni,{delay:5e3})]),new Ic(new Pa(new oa(Ba(zt),Ii,Ni)),D,{ttl:18e5,timeline:y.timeline,useTLS:y.useTLS})};const qa=jc;function Gf(){var E=this;E.timeline.info(E.buildTimelineMessage({transport:E.name+(E.options.useTLS?"s":"")})),E.hooks.isInitialized()?E.changeState("initialized"):E.hooks.file?(E.changeState("initializing"),A.load(E.hooks.file,{useTLS:E.options.useTLS},function(y,C){E.hooks.isInitialized()?(E.changeState("initialized"),C(!0)):(y&&E.onError(y),E.onClose(),C(!1))})):E.onClose()}var Hc={getRequest:function(E){var y=new window.XDomainRequest;return y.ontimeout=function(){E.emit("error",new G),E.close()},y.onerror=function(C){E.emit("error",C),E.close()},y.onprogress=function(){y.responseText&&y.responseText.length>0&&E.onChunk(200,y.responseText)},y.onload=function(){y.responseText&&y.responseText.length>0&&E.onChunk(200,y.responseText),E.emit("finished",200),E.close()},y},abortRequest:function(E){E.ontimeout=E.onerror=E.onprogress=E.onload=null,E.abort()}};const zc=Hc,Po=256*1024;class Vf extends Ae{constructor(y,C,D){super(),this.hooks=y,this.method=C,this.url=D}start(y){this.position=0,this.xhr=this.hooks.getRequest(this),this.unloader=()=>{this.close()},ze.addUnloadListener(this.unloader),this.xhr.open(this.method,this.url,!0),this.xhr.setRequestHeader&&this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.send(y)}close(){this.unloader&&(ze.removeUnloadListener(this.unloader),this.unloader=null),this.xhr&&(this.hooks.abortRequest(this.xhr),this.xhr=null)}onChunk(y,C){for(;;){var D=this.advanceBuffer(C);if(D)this.emit("chunk",{status:y,data:D});else break}this.isBufferTooLong(C)&&this.emit("buffer_too_long")}advanceBuffer(y){var C=y.slice(this.position),D=C.indexOf(` +`);return D!==-1?(this.position+=D+1,C.slice(0,D)):null}isBufferTooLong(y){return this.position===y.length&&y.length>Po}}var Bo;(function(E){E[E.CONNECTING=0]="CONNECTING",E[E.OPEN=1]="OPEN",E[E.CLOSED=3]="CLOSED"})(Bo||(Bo={}));const lr=Bo;var Rr=1;class qo{constructor(y,C){this.hooks=y,this.session=Ss(1e3)+"/"+Pc(8),this.location=Go(C),this.readyState=lr.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===lr.OPEN)try{return ze.createSocketRequest("POST",la(Ga(this.location,this.session))).start(y),!0}catch{return!1}else return!1}reconnect(){this.closeStream(),this.openStream()}onClose(y,C,D){this.closeStream(),this.readyState=lr.CLOSED,this.onclose&&this.onclose({code:y,reason:C,wasClean:D})}onChunk(y){if(y.status===200){this.readyState===lr.OPEN&&this.onActivity();var C,D=y.data.slice(0,1);switch(D){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===lr.CONNECTING?(y&&y.hostname&&(this.location.base=Uc(this.location.base,y.hostname)),this.readyState=lr.OPEN,this.onopen&&this.onopen()):this.onClose(1006,"Server lost session",!0)}onEvent(y){this.readyState===lr.OPEN&&this.onmessage&&this.onmessage({data:y})}onActivity(){this.onactivity&&this.onactivity()}onError(y){this.onerror&&this.onerror(y)}openStream(){this.stream=ze.createSocketRequest("POST",la(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 Go(E){var y=/([^\?]*)\/*(\??.*)/.exec(E);return{base:y[1],queryString:y[2]}}function Ga(E,y){return E.base+"/"+y+"/xhr_send"}function la(E){var y=E.indexOf("?")===-1?"?":"&";return E+y+"t="+ +new Date+"&n="+Rr++}function Uc(E,y){var C=/(https?:\/\/)([^\/:]+)((\/|:)?.*)/.exec(E);return C[1]+y+C[3]}function Ss(E){return ze.randomInt(E)}function Pc(E){for(var y=[],C=0;C<E;C++)y.push(Ss(32).toString(32));return y.join("")}const ki=qo;var Dr={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 Vo=Dr;var Ri={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 Kr=Ri;var Fo={getRequest:function(E){var y=ze.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 ws=Fo;var Bc={createStreamingSocket(E){return this.createSocket(Vo,E)},createPollingSocket(E){return this.createSocket(Kr,E)},createSocket(E,y){return new ki(E,y)},createXHR(E,y){return this.createRequest(ws,E,y)},createRequest(E,y,C){return new Vf(E,y,C)}};const Di=Bc;Di.createXDR=function(E,y){return this.createRequest(zc,E,y)};var qc={nextAuthCallbackID:1,auth_callbacks:{},ScriptReceivers:m,DependenciesReceivers:_,getDefaultStrategy:qa,Transports:An,transportConnectionInitializer:Gf,HTTPFactory:Di,TimelineTransport:on,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:mn}},onDocumentBody(E){document.body?E():setTimeout(()=>{this.onDocumentBody(E)},0)},createJSONPRequest(E,y){return new or(E,y)},createScriptRequest(E){return new ia(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 Ho},createWebSocket(E){var y=this.getWebSocketAPI();return new y(E)},createSocketRequest(E,y){if(this.isXHRSupported())return this.HTTPFactory.createXHR(E,y);if(this.isXDRSupported(y.indexOf("https:")===0))return this.HTTPFactory.createXDR(E,y);throw"Cross-origin HTTP requests are not supported"},isXHRSupported(){var E=this.getXHRAPI();return!!E&&new E().withCredentials!==void 0},isXDRSupported(E){var y=E?"https:":"http:",C=this.getProtocol();return!!window.XDomainRequest&&C===y},addUnloadListener(E){window.addEventListener!==void 0?window.addEventListener("unload",E,!1):window.attachEvent!==void 0&&window.attachEvent("onunload",E)},removeUnloadListener(E){window.addEventListener!==void 0?window.removeEventListener("unload",E,!1):window.detachEvent!==void 0&&window.detachEvent("onunload",E)},randomInt(E){return Math.floor(function(){return(window.crypto||window.msCrypto).getRandomValues(new Uint32Array(1))[0]/Math.pow(2,32)}()*E)}};const ze=qc;var Or;(function(E){E[E.ERROR=3]="ERROR",E[E.INFO=6]="INFO",E[E.DEBUG=7]="DEBUG"})(Or||(Or={}));const ca=Or;class Oi{constructor(y,C,D){this.key=y,this.session=C,this.events=[],this.options=D||{},this.sent=0,this.uniqueID=0}log(y,C){y<=this.options.level&&(this.events.push($e({},C,{timestamp:Te.now()})),this.options.limit&&this.events.length>this.options.limit&&this.events.shift())}error(y){this.log(ca.ERROR,y)}info(y){this.log(ca.INFO,y)}debug(y){this.log(ca.DEBUG,y)}isEmpty(){return this.events.length===0}send(y,C){var D=$e({session:this.session,bundle:this.sent+1,key:this.key,lib:"js",version:this.options.version,cluster:this.options.cluster,features:this.options.features,timeline:this.events},this.options.params);return this.events=[],y(D,(I,ne)=>{I||this.sent++,C&&C(I,ne)}),!0}generateUniqueID(){return this.uniqueID++,this.uniqueID}}class Ff{constructor(y,C,D,I){this.name=y,this.priority=C,this.transport=D,this.options=I||{}}isSupported(){return this.transport.isSupported({useTLS:this.options.useTLS})}connect(y,C){if(this.isSupported()){if(this.priority<y)return Cs(new P,C)}else return Cs(new we,C);var D=!1,I=this.transport.createConnection(this.name,this.priority,this.options.key,this.options),ne=null,me=function(){I.unbind("initialized",me),I.connect()},Me=function(){ne=In.createHandshake(I,function(zt){D=!0,gt(),C(null,zt)})},Ke=function(zt){gt(),C(zt)},ct=function(){gt();var zt;zt=gn(I),C(new W(zt))},gt=function(){I.unbind("initialized",me),I.unbind("open",Me),I.unbind("error",Ke),I.unbind("closed",ct)};return I.bind("initialized",me),I.bind("open",Me),I.bind("error",Ke),I.bind("closed",ct),I.initialize(),{abort:()=>{D||(gt(),ne?ne.close():I.close())},forceMinPriority:zt=>{D||this.priority<zt&&(ne?ne.close():I.close())}}}}function Cs(E,y){return Te.defer(function(){y(E)}),{abort:function(){},forceMinPriority:function(){}}}const{Transports:Wo}=ze;var Li=function(E,y,C,D,I,ne){var me=Wo[C];if(!me)throw new Se(C);var Me=(!E.enabledTransports||Ct(E.enabledTransports,y)!==-1)&&(!E.disabledTransports||Ct(E.disabledTransports,y)===-1),Ke;return Me?(I=Object.assign({ignoreNullOrigin:E.ignoreNullOrigin},I),Ke=new Ff(y,D,ne?ne.getAssistant(me):me,I)):Ke=Es,Ke},Es={isSupported:function(){return!1},connect:function(E,y){var C=Te.defer(function(){y(new we)});return{abort:function(){C.ensureAborted()},forceMinPriority:function(){}}}};function Gc(E){if(E==null)throw"You must pass an options object";if(E.cluster==null)throw"Options object must provide a cluster";"disableStats"in E&&nt.warn("The disableStats option is deprecated in favor of enableStats")}const Kf=(E,y)=>{var C="socket_id="+encodeURIComponent(E.socketId);for(var D in y.params)C+="&"+encodeURIComponent(D)+"="+encodeURIComponent(y.params[D]);if(y.paramsProvider!=null){let I=y.paramsProvider();for(var D in I)C+="&"+encodeURIComponent(D)+"="+encodeURIComponent(I[D])}return C},Wf=E=>{if(typeof ze.getAuthorizers()[E.transport]>"u")throw`'${E.transport}' is not a recognized auth transport`;return(y,C)=>{const D=Kf(y,E);ze.getAuthorizers()[E.transport](ze,D,E,k.UserAuthentication,C)}},Yo=(E,y)=>{var C="socket_id="+encodeURIComponent(E.socketId);C+="&channel_name="+encodeURIComponent(E.channelName);for(var D in y.params)C+="&"+encodeURIComponent(D)+"="+encodeURIComponent(y.params[D]);if(y.paramsProvider!=null){let I=y.paramsProvider();for(var D in I)C+="&"+encodeURIComponent(D)+"="+encodeURIComponent(I[D])}return C},Yf=E=>{if(typeof ze.getAuthorizers()[E.transport]>"u")throw`'${E.transport}' is not a recognized auth transport`;return(y,C)=>{const D=Yo(y,E);ze.getAuthorizers()[E.transport](ze,D,E,k.ChannelAuthorization,C)}},Xf=(E,y,C)=>{const D={authTransport:y.transport,authEndpoint:y.endpoint,auth:{params:y.params,headers:y.headers}};return(I,ne)=>{const me=E.channel(I.channelName);C(me,D).authorize(I.socketId,ne)}};function Vc(E,y){let C={activityTimeout:E.activityTimeout||S.activityTimeout,cluster:E.cluster,httpPath:E.httpPath||S.httpPath,httpPort:E.httpPort||S.httpPort,httpsPort:E.httpsPort||S.httpsPort,pongTimeout:E.pongTimeout||S.pongTimeout,statsHost:E.statsHost||S.stats_host,unavailableTimeout:E.unavailableTimeout||S.unavailableTimeout,wsPath:E.wsPath||S.wsPath,wsPort:E.wsPort||S.wsPort,wssPort:E.wssPort||S.wssPort,enableStats:eh(E),httpHost:Jf(E),useTLS:Zf(E),wsHost:Fc(E),userAuthenticator:Xo(E),channelAuthorizer:nh(E,y)};return"disabledTransports"in E&&(C.disabledTransports=E.disabledTransports),"enabledTransports"in E&&(C.enabledTransports=E.enabledTransports),"ignoreNullOrigin"in E&&(C.ignoreNullOrigin=E.ignoreNullOrigin),"timelineParams"in E&&(C.timelineParams=E.timelineParams),"nacl"in E&&(C.nacl=E.nacl),C}function Jf(E){return E.httpHost?E.httpHost:E.cluster?`sockjs-${E.cluster}.pusher.com`:S.httpHost}function Fc(E){return E.wsHost?E.wsHost:Qf(E.cluster)}function Qf(E){return`ws-${E}.pusher.com`}function Zf(E){return ze.getProtocol()==="https:"?!0:E.forceTLS!==!1}function eh(E){return"enableStats"in E?E.enableStats:"disableStats"in E?!E.disableStats:!1}const Kc=E=>"customHandler"in E&&E.customHandler!=null;function Xo(E){const y=Object.assign(Object.assign({},S.userAuthentication),E.userAuthentication);return Kc(y)?y.customHandler:Wf(y)}function th(E,y){let C;if("channelAuthorization"in E)C=Object.assign(Object.assign({},S.channelAuthorization),E.channelAuthorization);else if(C={transport:E.authTransport||S.authTransport,endpoint:E.authEndpoint||S.authEndpoint},"auth"in E&&("params"in E.auth&&(C.params=E.auth.params),"headers"in E.auth&&(C.headers=E.auth.headers)),"authorizer"in E)return{customHandler:Xf(y,C,E.authorizer)};return C}function nh(E,y){const C=th(E,y);return Kc(C)?C.customHandler:Yf(C)}class rh extends Ae{constructor(y){super(function(C,D){nt.debug(`No callbacks on watchlist events for ${C}`)}),this.pusher=y,this.bindWatchlistInternalEvent()}handleEvent(y){y.data.events.forEach(C=>{this.emit(C.name,C)})}bindWatchlistInternalEvent(){this.pusher.connection.bind("message",y=>{var C=y.event;C==="pusher_internal:watchlist_events"&&this.handleEvent(y)})}}function Wc(){let E,y;return{promise:new Promise((D,I)=>{E=D,y=I}),resolve:E,reject:y}}const ah=Wc;class ih extends Ae{constructor(y){super(function(C,D){nt.debug("No callbacks on user for "+C)}),this.signin_requested=!1,this.user_data=null,this.serverToUserChannel=null,this.signinDonePromise=null,this._signinDoneResolve=null,this._onAuthorize=(C,D)=>{if(C){nt.warn(`Error during signin: ${C}`),this._cleanup();return}this.pusher.send_event("pusher:signin",{auth:D.auth,user_data:D.user_data})},this.pusher=y,this.pusher.connection.bind("state_change",({previous:C,current:D})=>{C!=="connected"&&D==="connected"&&this._signin(),C==="connected"&&D!=="connected"&&(this._cleanup(),this._newSigninPromiseIfNeeded())}),this.watchlist=new rh(y),this.pusher.connection.bind("message",C=>{var D=C.event;D==="pusher:signin_success"&&this._onSigninSuccess(C.data),this.serverToUserChannel&&this.serverToUserChannel.name===C.channel&&this.serverToUserChannel.handleEvent(C)})}signin(){this.signin_requested||(this.signin_requested=!0,this._signin())}_signin(){this.signin_requested&&(this._newSigninPromiseIfNeeded(),this.pusher.connection.state==="connected"&&this.pusher.config.userAuthenticator({socketId:this.pusher.connection.socket_id},this._onAuthorize))}_onSigninSuccess(y){try{this.user_data=JSON.parse(y.user_data)}catch{nt.error(`Failed parsing user data after signin: ${y.user_data}`),this._cleanup();return}if(typeof this.user_data.id!="string"||this.user_data.id===""){nt.error(`user_data doesn't contain an id. user_data: ${this.user_data}`),this._cleanup();return}this._signinDoneResolve(),this._subscribeChannels()}_subscribeChannels(){const y=C=>{C.subscriptionPending&&C.subscriptionCancelled?C.reinstateSubscription():!C.subscriptionPending&&this.pusher.connection.state==="connected"&&C.subscribe()};this.serverToUserChannel=new Fr(`#server-to-user-${this.user_data.id}`,this.pusher),this.serverToUserChannel.bind_global((C,D)=>{C.indexOf("pusher_internal:")===0||C.indexOf("pusher:")===0||this.emit(C,D)}),y(this.serverToUserChannel)}_cleanup(){this.user_data=null,this.serverToUserChannel&&(this.serverToUserChannel.unbind_all(),this.serverToUserChannel.disconnect(),this.serverToUserChannel=null),this.signin_requested&&this._signinDoneResolve()}_newSigninPromiseIfNeeded(){if(!this.signin_requested||this.signinDonePromise&&!this.signinDonePromise.done)return;const{promise:y,resolve:C}=ah();y.done=!1;const D=()=>{y.done=!0};y.then(D).catch(D),this.signinDonePromise=y,this._signinDoneResolve=C}}class ln{static ready(){ln.isReady=!0;for(var y=0,C=ln.instances.length;y<C;y++)ln.instances[y].connect()}static getClientFeatures(){return ht(an({ws:ze.Transports.ws},function(y){return y.isSupported({})}))}constructor(y,C){sh(y),Gc(C),this.key=y,this.options=C,this.config=Vc(this.options,this),this.channels=In.createChannels(),this.global_emitter=new Ae,this.sessionID=ze.randomInt(1e9),this.timeline=new Oi(this.key,this.sessionID,{cluster:this.config.cluster,features:ln.getClientFeatures(),params:this.config.timelineParams||{},limit:50,level:ca.INFO,version:S.VERSION}),this.config.enableStats&&(this.timelineSender=In.createTimelineSender(this.timeline,{host:this.config.statsHost,path:"/timeline/v2/"+ze.TimelineTransport.name}));var D=I=>ze.getDefaultStrategy(this.config,I,Li);this.connection=In.createConnectionManager(this.key,{getStrategy:D,timeline:this.timeline,activityTimeout:this.config.activityTimeout,pongTimeout:this.config.pongTimeout,unavailableTimeout:this.config.unavailableTimeout,useTLS:!!this.config.useTLS}),this.connection.bind("connected",()=>{this.subscribeAll(),this.timelineSender&&this.timelineSender.send(this.connection.isUsingTLS())}),this.connection.bind("message",I=>{var ne=I.event,me=ne.indexOf("pusher_internal:")===0;if(I.channel){var Me=this.channel(I.channel);Me&&Me.handleEvent(I)}me||this.global_emitter.emit(I.event,I.data)}),this.connection.bind("connecting",()=>{this.channels.disconnect()}),this.connection.bind("disconnected",()=>{this.channels.disconnect()}),this.connection.bind("error",I=>{nt.warn(I)}),ln.instances.push(this),this.timeline.info({instances:ln.instances.length}),this.user=new ih(this),ln.isReady&&this.connect()}switchCluster(y){const{appKey:C,cluster:D}=y;this.key=C,this.options=Object.assign(Object.assign({},this.options),{cluster:D}),this.config=Vc(this.options,this),this.connection.switchCluster(this.key)}channel(y){return this.channels.find(y)}allChannels(){return this.channels.all()}connect(){if(this.connection.connect(),this.timelineSender&&!this.timelineSenderTimer){var y=this.connection.isUsingTLS(),C=this.timelineSender;this.timelineSenderTimer=new xe(6e4,function(){C.send(y)})}}disconnect(){this.connection.disconnect(),this.timelineSenderTimer&&(this.timelineSenderTimer.ensureAborted(),this.timelineSenderTimer=null)}bind(y,C,D){return this.global_emitter.bind(y,C,D),this}unbind(y,C,D){return this.global_emitter.unbind(y,C,D),this}bind_global(y){return this.global_emitter.bind_global(y),this}unbind_global(y){return this.global_emitter.unbind_global(y),this}unbind_all(y){return this.global_emitter.unbind_all(),this}subscribeAll(){var y;for(y in this.channels.channels)this.channels.channels.hasOwnProperty(y)&&this.subscribe(y)}subscribe(y){var C=this.channels.add(y,this);return C.subscriptionPending&&C.subscriptionCancelled?C.reinstateSubscription():!C.subscriptionPending&&this.connection.state==="connected"&&C.subscribe(),C}unsubscribe(y){var C=this.channels.find(y);C&&C.subscriptionPending?C.cancelSubscription():(C=this.channels.remove(y),C&&C.subscribed&&C.unsubscribe())}send_event(y,C,D){return this.connection.send_event(y,C,D)}shouldUseTLS(){return this.config.useTLS}signin(){this.user.signin()}}ln.instances=[],ln.isReady=!1,ln.logToConsole=!1,ln.Runtime=ze,ln.ScriptReceivers=ze.ScriptReceivers,ln.DependenciesReceivers=ze.DependenciesReceivers,ln.auth_callbacks=ze.auth_callbacks;const _s=ln;function sh(E){if(E==null)throw"You must pass your app key when you instantiate Pusher."}ze.setup(ln)}},a={};function i(c){var d=a[c];if(d!==void 0)return d.exports;var f=a[c]={exports:{}};return n[c].call(f.exports,f,f.exports,i),f.exports}i.d=(c,d)=>{for(var f in d)i.o(d,f)&&!i.o(c,f)&&Object.defineProperty(c,f,{enumerable:!0,get:d[f]})},i.o=(c,d)=>Object.prototype.hasOwnProperty.call(c,d);var l=i(721);return l})())})(Lf)),Lf.exports}var SA=vA();const $y=Zr(SA),Pt=dt("WebSocketManager");class wA{constructor(){$(this,"echo",null);$(this,"subscriptions",new Map);$(this,"initialized",!1);$(this,"config",null)}configure(e){if(this.initialized){Pt.warn("[WebSocketManager] 이미 초기화되어 설정을 변경할 수 없습니다.");return}this.config=e,Pt.log("[WebSocketManager] 설정 완료:",{appKey:e.appKey?"***":"(없음)",host:e.host,port:e.port,scheme:e.scheme})}initialize(){if(this.initialized)return;if(!this.config||!this.config.appKey){Pt.warn("[WebSocketManager] WebSocket 설정이 없습니다. initTemplateApp에서 websocket 옵션을 전달해주세요.");return}const{appKey:e,host:n="localhost",port:a=80,scheme:i="https",authEndpoint:l="/api/broadcasting/auth"}=this.config,c=Number(a)||80,d=i==="https";Pt.log("[WebSocketManager] 연결 설정:",{host:n,port:c,scheme:i,useTLS:d,authEndpoint:l}),window.Pusher=$y;const f=localStorage.getItem("auth_token"),h={wsHost:n,wsPort:c,wssPort:c,forceTLS:d,disableStats:!0,enabledTransports:["ws","wss"],cluster:"mt1",authEndpoint:l,auth:{headers:{Authorization:f?`Bearer ${f}`:"",Accept:"application/json"}}};Pt.log("[WebSocketManager] Pusher 옵션:",h);const m=new $y(e,h);m.connection.bind("connecting",()=>{Pt.log("[WebSocketManager] 연결 시도 중...")}),m.connection.bind("connected",()=>{Pt.log("[WebSocketManager] 연결 성공! Socket ID:",m.connection.socket_id)}),m.connection.bind("failed",()=>{Pt.error("[WebSocketManager] 연결 실패 - WebSocket을 사용할 수 없습니다.")}),m.connection.bind("error",b=>{Pt.error("[WebSocketManager] 연결 오류:",b)}),m.connection.bind("state_change",b=>{Pt.log(`[WebSocketManager] 상태 변경: ${b.previous} → ${b.current}`)}),m.connection.bind("unavailable",()=>{Pt.error("[WebSocketManager] WebSocket 사용 불가 - 연결할 수 없습니다.")}),m.connection.bind("disconnected",()=>{Pt.warn("[WebSocketManager] 연결이 끊어졌습니다.")}),Pt.log("[WebSocketManager] 초기 연결 상태:",m.connection.state),this.echo=new bA({broadcaster:"reverb",client:m}),Pt.log("[WebSocketManager] 연결 시작 시도..."),m.connect(),window.Echo=this.echo,this.initialized=!0,Pt.log("[WebSocketManager] Echo 초기화 완료")}subscribe(e,n,a,i={}){if(this.initialize(),!this.echo)return Pt.warn("[WebSocketManager] Echo가 초기화되지 않았습니다."),"";const{channelType:l="private"}=i,c=`${e}:${n}`;if(this.subscriptions.has(c))return Pt.log(`[WebSocketManager] 이미 구독 중: ${c}`),c;let d;switch(l){case"public":d=this.echo.channel(e);break;case"presence":d=this.echo.join(e);break;default:d=this.echo.private(e)}return d.listen(`.${n}`,a),this.subscriptions.set(c,d),Pt.log(`[WebSocketManager] 구독 완료: ${c} (${l})`),c}unsubscribe(e){const n=this.subscriptions.get(e);if(!n)return;const a=e.lastIndexOf(":"),i=a>=0?e.substring(a+1):"";if(i)try{n.stopListening(`.${i}`)}catch(l){Pt.warn(`[WebSocketManager] stopListening 실패: ${e}`,l)}this.subscriptions.delete(e),Pt.log(`[WebSocketManager] 구독 해제: ${e}`)}leaveChannel(e){if(!this.echo)return;this.echo.leave(e);const n=[];this.subscriptions.forEach((a,i)=>{i.startsWith(`${e}:`)&&n.push(i)}),n.forEach(a=>this.subscriptions.delete(a)),Pt.log(`[WebSocketManager] 채널 구독 해제: ${e}`)}disconnect(){this.echo&&(this.echo.disconnect(),this.subscriptions.clear(),this.initialized=!1,Pt.log("[WebSocketManager] 연결 종료"))}getEcho(){return this.echo}isInitialized(){return this.initialized}isConfigured(){return this.config!==null&&!!this.config.appKey}getSubscriptionCount(){return this.subscriptions.size}}const _c=new wA,bt=dt("DataSourceManager"),Ny=new Tn;function Ac(s,e){const n=Ta(s);if(n!==null)try{return qn(n)?Ny.evaluatePipeExpression(n,e,{skipCache:!0}):Ny.evaluateExpression(n,e)}catch(a){return bt.error("Failed to evaluate param expression:",n,a),s}return is(s,e,{skipCache:!0})}function Iy(s){const e={};for(const n of s.keys()){if(n in e)continue;const a=s.getAll(n);a.length>1?e[n]=a:a.length===1&&(n.endsWith("[]")?e[n]=a:e[n]=a[0])}return e}function CA(){try{return window.G7Core?.devTools}catch{return}}const Rc=class Rc{constructor(e={}){$(this,"options");$(this,"dataCache",new Map);$(this,"bindingEngine");$(this,"globalHeaders",[]);this.options=e,this.bindingEngine=new Tn}setGlobalHeaders(e){this.globalHeaders=e||[]}matchesPattern(e,n){if(n==="*")return!0;const a=n.replace(/[.+?^${}()|[\]\\]/g,"\\$&").replace(/\*/g,".*");return new RegExp(`^${a}$`).test(e)}toFormData(e){const n=new FormData;for(const[a,i]of Object.entries(e))i instanceof File||i instanceof Blob?n.append(a,i):i!=null&&n.append(a,typeof i=="object"?JSON.stringify(i):String(i));return n}getMatchingGlobalHeaders(e,n){const a={};for(const i of this.globalHeaders)this.matchesPattern(e,i.pattern)&&Object.entries(i.headers).forEach(([l,c])=>{const d=Ac(c,n);d!=null&&d!==""&&(a[l]=String(d))});return a}filterByCondition(e,n={}){const a=new Set,i=[];for(const l of e){if(a.has(l.id))continue;if(!l.if&&!l.conditions){a.add(l.id),i.push(l);continue}as({if:l.if,conditions:l.conditions},n,this.bindingEngine,`data_source:${l.id}`)&&(a.add(l.id),i.push(l))}return i}checkErrorCondition(e,n){if(!e.errorCondition?.if||e.errorCondition?.errorCode===void 0)return null;try{const a={response:n};if(as({if:e.errorCondition.if},a,this.bindingEngine,`errorCondition:${e.id}`))return bt.log(`errorCondition matched for ${e.id}, triggering error ${e.errorCondition.errorCode}`),e.errorCondition.errorCode}catch(a){bt.error(`Failed to evaluate errorCondition for ${e.id}:`,a)}return null}async fetchDataSources(e,n={},a=new URLSearchParams,i,l){const c={},d=!!this.options.sampleProvider,f=e.filter(h=>(d||h.auto_fetch!==!1)&&h.type!=="websocket");return await Promise.all(f.map(async h=>{try{const m=await this.fetchDataSource(h,n,a,i,l);c[h.id]=m,this.dataCache.set(h.id,{data:m,timestamp:Date.now()});const b=this.checkErrorCondition(h,m);if(b!==null){const S={status:b,message:`Error condition matched for ${h.id}`,data:m},v=Cr(),_=v.resolve(b,{errorHandling:h.errorHandling,onError:h.onError});if(_.handler)try{await v.execute(_.handler,S)}catch(A){bt.error(`Error executing errorCondition handler for ${h.id}:`,A)}h.fallback!==void 0&&(c[h.id]=h.fallback,this.dataCache.set(h.id,{data:h.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${h.id} (errorCondition)`));return}if(h.onSuccess)try{const S={data:m,sourceId:h.id};await this.executeOnSuccessHandler(h.onSuccess,S)}catch(S){bt.error(`Error executing onSuccess handler for ${h.id}:`,S)}}catch(m){bt.error(`Failed to fetch data source: ${h.id}`,m);const b=m?.response?.status||m?.status||500,S=m?.response?.data,v=S?.message||m.message||"Unknown error",_={status:b,message:v,errors:S?.errors,data:S,statusText:m?.response?.statusText},A=Cr(),x=A.resolve(b,{errorHandling:h.errorHandling,onError:h.onError});if(x.handler)try{await A.execute(x.handler,_)}catch(L){bt.error(`Error executing error handler for ${h.id}:`,L)}h.fallback!==void 0&&(c[h.id]=h.fallback,this.dataCache.set(h.id,{data:h.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${h.id}`)),this.options.onError&&this.options.onError(m,h)}})),c}async fetchDataSourcesWithResults(e,n={},a=new URLSearchParams,i,l,c){if(e.some(m=>m.type==="websocket")){const m=e.filter(b=>b.type==="websocket").map(b=>b.id).join(", ");bt.warn(`fetchDataSourcesWithResults received WebSocket sources (caller should filter them out before calling): ${m}. WebSocket sources are event listeners, not data providers — they should not be passed to fetch.`)}const f=c?.ignoreAutoFetch?e.filter(m=>m.type!=="websocket"):e.filter(m=>m.auto_fetch!==!1&&m.type!=="websocket");return await Promise.all(f.map(async m=>{try{const b=await this.fetchDataSource(m,n,a,i,l);this.dataCache.set(m.id,{data:b,timestamp:Date.now()});const S=this.checkErrorCondition(m,b);if(S!==null){const v={status:S,message:`Error condition matched for ${m.id}`,data:b},_=Cr(),A=_.resolve(S,{errorHandling:m.errorHandling,onError:m.onError});let x=!1;if(A.handler)if(m.loading_strategy==="blocking")_.execute(A.handler,v).catch(L=>{bt.error(`Error executing errorCondition handler for ${m.id}:`,L)}),x=!0;else try{await _.execute(A.handler,v),x=!0}catch(L){bt.error(`Error executing errorCondition handler for ${m.id}:`,L)}return m.fallback!==void 0?(this.dataCache.set(m.id,{data:m.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${m.id} (errorCondition)`),{id:m.id,state:"success",data:m.fallback}):{id:m.id,state:"error",error:new Error(`Error condition matched: ${S}`),errorCode:S,errorHandled:x}}if(m.onSuccess)try{const v={data:b,sourceId:m.id};await this.executeOnSuccessHandler(m.onSuccess,v)}catch(v){bt.error(`Error executing onSuccess handler for ${m.id}:`,v)}return{id:m.id,state:"success",data:b}}catch(b){bt.error(`Failed to fetch data source: ${m.id}`,b);const S=b?.response?.status||b?.status||500,v=b?.response?.data,_=v?.message||b.message||"Unknown error",A={status:S,message:_,errors:v?.errors,data:v,statusText:b?.response?.statusText},x=Cr(),L=x.resolve(S,{errorHandling:m.errorHandling,onError:m.onError});let M=!1;if(L.handler)if(m.loading_strategy==="blocking")x.execute(L.handler,A).catch(k=>{bt.error(`Error executing error handler for ${m.id}:`,k)}),M=!0;else try{await x.execute(L.handler,A),M=!0}catch(k){bt.error(`Error executing error handler for ${m.id}:`,k)}return m.fallback!==void 0?(this.dataCache.set(m.id,{data:m.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${m.id}`),{id:m.id,state:"success",data:m.fallback}):(this.options.onError&&this.options.onError(b,m),{id:m.id,state:"error",error:b,errorCode:S,errorHandled:M})}}))}fetchDataSourcesInBackground(e,n={},a=new URLSearchParams,i){e.filter(c=>c.auto_fetch!==!1&&c.loading_strategy==="background"&&c.type!=="websocket").forEach(c=>{this.fetchDataSource(c,n,a).then(async d=>{this.dataCache.set(c.id,{data:d,timestamp:Date.now()});const f=this.checkErrorCondition(c,d);if(f!==null){const h={status:f,message:`Error condition matched for ${c.id}`,data:d},m=Cr(),b=m.resolve(f,{errorHandling:c.errorHandling,onError:c.onError});if(b.handler)try{await m.execute(b.handler,h)}catch(S){bt.error(`Error executing errorCondition handler for ${c.id}:`,S)}c.fallback!==void 0&&(this.dataCache.set(c.id,{data:c.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${c.id} (errorCondition)`),i&&i(c.id,c.fallback));return}if(c.onSuccess)try{const h={data:d,sourceId:c.id};await this.executeOnSuccessHandler(c.onSuccess,h)}catch(h){bt.error(`Error executing onSuccess handler for ${c.id}:`,h)}i&&i(c.id,d)}).catch(async d=>{bt.error(`Background fetch failed: ${c.id}`,d);const f=d?.response?.status||d?.status||500,h=d?.response?.data,m=h?.message||d.message||"Unknown error",b={status:f,message:m,errors:h?.errors,data:h,statusText:d?.response?.statusText},S=Cr(),v=S.resolve(f,{errorHandling:c.errorHandling,onError:c.onError});if(v.handler)try{await S.execute(v.handler,b)}catch(_){bt.error(`Error executing error handler for ${c.id}:`,_)}if(c.fallback!==void 0){this.dataCache.set(c.id,{data:c.fallback,timestamp:Date.now()}),bt.log(`Using fallback data for ${c.id}`),i&&i(c.id,c.fallback);return}this.options.onError&&this.options.onError(d,c)})})}async fetchDataSource(e,n,a,i,l){switch(e.type){case"api":return this.fetchApiDataSource(e,n,a,i,l);case"static":return e.data;case"route_params":return n;case"query_params":return Iy(a);default:throw new Error(`Unknown data source type: ${e.type}`)}}async fetchApiDataSource(e,n,a,i,l){if(this.options.sampleProvider?.has(e.id))return await Promise.resolve(this.options.sampleProvider.resolve(e));if(!e.endpoint)throw new Error(`API data source ${e.id} has no endpoint`);const c={route:n,query:Iy(a)};i&&(c._global=i),l&&(c._local=l);let d=is(e.endpoint,c,{skipCache:!0});const f=e.method||"GET",m=(e.contentType?typeof e.contentType=="string"&&e.contentType.includes("{{")?is(e.contentType,c,{skipCache:!0}):e.contentType:"application/json")==="multipart/form-data";let b=m?{...e.params||{}}:JSON.parse(JSON.stringify(e.params||{}));Object.entries(b).forEach(([_,A])=>{typeof A=="string"&&(b[_]=Ac(A,c))}),Object.keys(b).forEach(_=>{b[_]===""&&delete b[_]});const S=CA();let v=null;if(S?.isEnabled()){S.trackDataSourceDefinition({id:e.id,type:e.type,endpoint:d,method:f,autoFetch:e.auto_fetch,initLocal:e.initLocal,initGlobal:e.initGlobal});let _=d;if(f==="GET"&&Object.keys(b).length>0){const A=new URLSearchParams;Object.entries(b).forEach(([L,M])=>{Array.isArray(M)?M.forEach(k=>A.append(L,String(k))):M!=null&&A.append(L,String(M))});const x=A.toString();_=x?`${d}?${x}`:d}v=S.trackRequest(_,f,{requestBody:f!=="GET"?b:void 0,dataSourceId:e.id}),S.trackDataSourceLoading(e.id)}try{let _;const A=e.auth_mode??(e.auth_required===!0?"required":"none"),x=Sr.getInstance().isAuthenticated();if(A==="required"&&!x){bt.log(`[DataSourceManager] Skipping ${e.id}: auth_mode is 'required' but no token available`),S?.isEnabled()&&(S.trackDataSourceError(e.id,"Skipped: auth_mode required but not authenticated"),v&&S.failRequest(v,"Skipped: not authenticated"));const M=new Error("Auth required but not authenticated");throw M.response={status:401,statusText:"Unauthorized",data:null},M.status=401,M._authSkipped=!0,M}const L=A==="required"||A==="optional"&&x;if(L){const M=Hr(),k=d.startsWith("/api/")?d.substring(4):d.startsWith("/api")?d.substring(4)||"/":d;this.options.onError&&bt.log(`[DataSourceManager] Normalized endpoint: ${d} -> ${k}`);const O=this.getMatchingGlobalHeaders(d,c),B={};e.headers&&Object.entries(e.headers).forEach(([pe,Se])=>{const we=Ac(Se,c);we!=null&&we!==""&&(B[pe]=String(we))});const G={...O,...B},P=Object.keys(G).length>0,W=m?this.toFormData(b):b;switch(f){case"GET":_=await M.get(k,{params:b,...P&&{headers:G}});break;case"POST":P?_=await M.post(k,W,{headers:G}):_=await M.post(k,W);break;case"PUT":P?_=await M.put(k,W,{headers:G}):_=await M.put(k,W);break;case"PATCH":P?_=await M.patch(k,W,{headers:G}):_=await M.patch(k,W);break;case"DELETE":_=await M.delete(k,{params:b,...P&&{headers:G}});break;default:throw new Error(`Unsupported HTTP method: ${f}`)}}else{let M=d;if(f==="GET"){const W=new URLSearchParams(b).toString();M=W?`${d}?${W}`:d}const k={...m?{}:{"Content-Type":"application/json"},Accept:"application/json"};if(typeof window<"u"){const W=localStorage.getItem("g7_locale");W&&(k["Accept-Language"]=W)}const O=this.getMatchingGlobalHeaders(d,c);Object.assign(k,O),e.headers&&Object.entries(e.headers).forEach(([W,pe])=>{const Se=Ac(pe,c);Se!=null&&Se!==""&&(k[W]=String(Se))});let B;f!=="GET"&&(m?(B=this.toFormData(b),delete k["Content-Type"]):B=JSON.stringify(b));const G=await fetch(M,{method:f,headers:k,body:B});let P;try{P=await G.json()}catch{P=null}if(v&&S?.isEnabled()&&(S.completeRequest(v,G.status,P),v=null),!G.ok){S?.isEnabled()&&S.trackDataSourceError(e.id,`HTTP ${G.status}: ${G.statusText}`);const W=new Error(`HTTP error! status: ${G.status}`);throw W.response={status:G.status,statusText:G.statusText,data:P},W.status=G.status,W}_=P,S?.isEnabled()&&S.trackDataSourceLoaded(e.id,_)}return L&&v&&S?.isEnabled()&&(S.completeRequest(v,200,_),S.trackDataSourceLoaded(e.id,_)),_}catch(_){if(v&&S?.isEnabled()){const A=_;A?.response?S.completeRequest(v,A.response.status,A.response.data):S.failRequest(v,_ instanceof Error?_.message:String(_)),S.trackDataSourceError(e.id,_ instanceof Error?_.message:String(_))}throw _}}getCachedData(e){const n=this.dataCache.get(e);if(!n)return;if(Date.now()-n.timestamp>Rc.DATA_CACHE_TTL){this.dataCache.delete(e);return}return n.data}clearCache(){this.dataCache.clear()}subscribeWebSockets(e,n,a={}){const i=[],l=e.filter(c=>c.type==="websocket");return l.length===0||l.forEach(c=>{if(!c.channel||!c.event){bt.warn(`WebSocket source ${c.id} missing channel or event`);return}let d,f;try{d=is(c.channel,a,{skipCache:!0}),f=is(c.event,a,{skipCache:!0})}catch(b){bt.error(`Failed to resolve WebSocket channel/event for source ${c.id}:`,b);return}const h=b=>!!(!b||b.trim()===""||b.includes("{{")||b.endsWith(".")||b.endsWith(":")||b.startsWith(".")||b.startsWith(":")||b.includes("..")||b.includes("::"));if(h(d)||h(f)){bt.warn(`WebSocket source ${c.id} has invalid channel/event after expression resolution, skipping. Original channel="${c.channel}", resolved="${d}". Original event="${c.event}", resolved="${f}". Available context keys: [${Object.keys(a).join(", ")}]`);return}const m=_c.subscribe(d,f,b=>{const S=c.target_source||c.id;this.dataCache.set(S,{data:b,timestamp:Date.now()}),n(S,b)},{channelType:c.channel_type||"private"});m&&i.push(m)}),i}unsubscribeWebSockets(e){e.length!==0&&e.forEach(n=>{_c.unsubscribe(n)})}async executeOnSuccessHandler(e,n){const a=this.options.actionDispatcher??Mm();if(!a){bt.warn("ActionDispatcher not available, skipping onSuccess handler");return}const i=Array.isArray(e)?e:[e];for(const l of i)try{await a.dispatchAction({type:"click",...l},{data:{response:n}})}catch(c){throw bt.error("Failed to execute onSuccess handler:",c),c}}};$(Rc,"DATA_CACHE_TTL",3e5);let xr=Rc;const EA=new xr,Si=dt("ModalDataSourceWrapper");function _A(s,e){if(!s||!e)return s;const a=e.replace(/\?\./g,".").split(/\.(?![^\[]*\])/).flatMap(l=>{const c=l.match(/^([^\[]*)((?:\[\d+\])*)$/);if(c){const[,d,f]=c,h=[];d&&h.push(d);const m=f.match(/\[\d+\]/g);return m&&h.push(...m),h}return[l]}).filter(l=>l!=="");let i=s;for(const l of a){if(i==null)return;if(l.startsWith("[")&&l.endsWith("]")){const c=parseInt(l.slice(1,-1),10);i=i[c]}else i=i[l]}return i}const jy=({isOpen:s,modalId:e,dataSources:n,dataContext:a,globalStateUpdater:i,bindingEngine:l,debug:c=!1,children:d})=>{const[f,h]=N.useState(!1),[m,b]=N.useState(!1),S=N.useRef(!1),v=N.useRef(null);v.current||(v.current=new xr);const _=N.useCallback(async()=>{if(!(!n||n.length===0)&&v.current){h(!0);try{const A=a.route||{},x=new URLSearchParams(window.location.search),L=n.map(k=>{let O={...k};if(k.endpoint&&l){const B=l.resolveBindings(k.endpoint,a,{skipCache:!0});O.endpoint=B}if(k.params&&l){const B={};Object.entries(k.params).forEach(([G,P])=>{typeof P=="string"?B[G]=l.resolveBindings(P,a,{skipCache:!0}):B[G]=P}),O.params=B}return O});c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" fetching data sources:`,L.map(k=>({id:k.id,endpoint:k.endpoint})));const M=await v.current.fetchDataSourcesWithResults(L,A,x);i&&M.forEach(k=>{if(k.state==="success"&&k.data!==void 0){const O=L.find(B=>B.id===k.id);if(O?.initGlobal){const B=k.data?.data??k.data,G=Array.isArray(O.initGlobal)?O.initGlobal:[O.initGlobal];for(const P of G)if(typeof P=="string")i({[P]:B}),c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" initGlobal: ${k.id}.data -> _global.${P}`);else if(typeof P=="object"&&P.key){const{key:W,path:pe}=P,Se=pe?_A(B,pe):B;i({[W]:Se}),c&&Si.log(`Modal "${e}" initGlobal: ${k.id}.data${pe?"."+pe:""} -> _global.${W}`)}}}else k.state==="error"&&Si.error(`Modal "${e}" data source error:`,k.id,k.error)}),c&&Si.log(`Modal "${e}" data sources fetched successfully`)}catch(A){Si.error(`Modal "${e}" data source fetch error:`,A)}finally{h(!1),b(!0)}}},[n,a,i,l,e,c]);return N.useEffect(()=>{if(!n||n.length===0)return;const A=window.__templateApp;return A?.registerModalDataSources&&A.registerModalDataSources(e,n),()=>{A?.unregisterModalDataSources&&A.unregisterModalDataSources(e)}},[e,n]),N.useEffect(()=>{!s&&S.current&&(b(!1),c&&Si.log(`[ModalDataSourceWrapper] Modal "${e}" closed, reset fetch state`)),s&&!S.current&&n&&n.length>0&&!m&&_(),S.current=s},[s,n,m,_,e,c]),ft.jsx(ft.Fragment,{children:d})};class xc extends Error{constructor(n,a,i={}){super(a);$(this,"code");$(this,"userMessageKey");$(this,"showStack");$(this,"icon");$(this,"recoverable");this.name="TemplateEngineError",this.code=n,this.userMessageKey=a,this.showStack=i.showStack??!0,this.icon=i.icon??"fa-circle-exclamation",this.recoverable=i.recoverable??!0,Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}getUserMessage(n,a){if(!a||!n)return this.userMessageKey;try{return a.translate(this.userMessageKey,n)}catch{return this.userMessageKey}}}class AA extends xc{constructor(){super("TEMPLATE_NOT_FOUND","$t:core.errors.template_not_found",{showStack:!1,icon:"fa-paint-brush",recoverable:!1}),this.name="TemplateNotFoundError"}}function Hy(s){if(s instanceof xc)return s;if(s instanceof Error){const e=new xc("TEMPLATE_INIT_ERROR",s.message||"$t:core.errors.template_init_error",{showStack:!0,icon:"fa-circle-exclamation",recoverable:!0});return e.stack=s.stack,e}return new xc("TEMPLATE_INIT_ERROR","$t:core.errors.template_init_error",{showStack:!1,icon:"fa-circle-exclamation",recoverable:!0})}const xA=dt("ErrorDisplay");class Mf{static render(e,n){const a=document.getElementById(e);if(!a){xA.error(`Container #${e} not found`);return}const i=n.debug&&n.showStack&&n.stackTrace,l=window.matchMedia("(prefers-color-scheme: dark)").matches,c={container:` display: flex; align-items: center; justify-content: center; @@ -221,23 +181,23 @@ Error generating stack: `+u.message+` <div style="${c.card}"> <!-- 아이콘 --> <div style="${c.iconContainer}"> - <i class="fa-solid ${r.icon}" style="${c.icon}"></i> + <i class="fa-solid ${n.icon}" style="${c.icon}"></i> </div> <!-- 제목 --> <h1 style="${c.title}"> - ${this.escapeHtml(r.title)} + ${this.escapeHtml(n.title)} </h1> <!-- 사용자 메시지 --> <p style="${c.message}"> - ${this.escapeHtml(r.message)} + ${this.escapeHtml(n.message)} </p> <!-- 상세 메시지 (API 응답 등) --> - ${r.detailMessage?` + ${n.detailMessage?` <p style="${c.detailMessage}"> - ${this.escapeHtml(r.detailMessage)} + ${this.escapeHtml(n.detailMessage)} </p> `:`<div style="${c.spacer}"></div>`} @@ -247,12 +207,12 @@ Error generating stack: `+u.message+` <p style="${c.stackTitle}"> Stack Trace: </p> - <pre style="${c.stackTrace}">${this.escapeHtml(r.stackTrace)}</pre> + <pre style="${c.stackTrace}">${this.escapeHtml(n.stackTrace)}</pre> </div> `:""} <!-- 새로고침 버튼 (복구 가능할 때만) --> - ${r.showReloadButton?` + ${n.showReloadButton?` <div style="${c.buttonContainer}"> <button onclick="window.location.reload()" @@ -267,21 +227,4 @@ Error generating stack: `+u.message+` `:""} </div> </div> - `}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.errorConfig),this.errorConfig)}catch(e){return jt.error("Failed to load error config:",e),this.configLoaded=!0,null}}async renderError(e,r="app"){try{const a=await this.loadConfig();if(!a)return this.debug&&jt.warn("No error config available, cannot render error page"),!1;const i=a.layouts[e]||a.layouts[String(e)];if(!i)return this.debug&&jt.warn(`No layout defined for error code: ${e}`),!1;this.debug&&jt.log(`Loading error layout: ${i} for code: ${e}`);const l=await this.layoutLoader.loadLayout(this.templateId,i);if(!l)return this.debug&&jt.error(`Failed to load error layout: ${i}`),!1;this.debug&&jt.log("Error layout loaded:",l);let c={};const d=l.data_sources||[];if(d.length>0){this.debug&&jt.log("Fetching data sources:",d.map(S=>S.id));const m=d.filter(S=>S.loading_strategy==="blocking"),y=d.filter(S=>!S.loading_strategy||S.loading_strategy==="progressive"),w=[...m,...y];if(w.length>0)try{c=await this.dataSourceManager.fetchDataSources(w,{},new URLSearchParams),this.debug&&jt.log("Data sources fetched:",Object.keys(c))}catch(S){jt.error("Failed to fetch data sources:",S)}}const f={};d.length>0&&(this.processInitOptions(d,c,f),this.debug&&jt.log("initOptions processed:",{globalKeys:Object.keys(this.globalState),localKeys:Object.keys(f)}));const g={...c,errorCode:e,_global:{...this.globalState},_local:{...f}};return await this.renderFunction({containerId:r,layoutJson:l,dataContext:g,translationContext:{templateId:this.templateId,locale:this.locale}}),this.debug&&jt.log(`Error page ${e} rendered successfully`),!0}catch(a){return jt.error(`Failed to render error page ${e}:`,a),!1}}processInitOptions(e,r,a){for(const i of e){const l=r[i.id];if(!l)continue;const c=l?.data??l;if(i.initLocal){if(typeof i.initLocal=="string")a[i.initLocal]=c,jt.log(`initLocal: ${i.id}.data -> _local.${i.initLocal}`);else if(typeof i.initLocal=="object"&&"key"in i.initLocal){const{key:d,path:f}=i.initLocal;a[d]=f?this.getValueByPath(c,f):c,jt.log(`initLocal: ${i.id}.data${f?"."+f:""} -> _local.${d}`)}}if(i.initGlobal){const d=Array.isArray(i.initGlobal)?i.initGlobal:[i.initGlobal];for(const f of d)if(typeof f=="string")this.globalState[f]=c,jt.log(`initGlobal: ${i.id}.data -> _global.${f}`);else if(typeof f=="object"&&f!==null&&"key"in f){const{key:g,path:m}=f;this.globalState[g]=m?this.getValueByPath(c,m):c,jt.log(`initGlobal: ${i.id}.data${m?"."+m:""} -> _global.${g}`)}}}}getValueByPath(e,r){return r.split(".").reduce((a,i)=>a?.[i],e)}updateGlobalState(e){this.globalState={...this.globalState,...e}}updateLocale(e){this.locale=e,this.debug&&jt.log("Locale updated:",e)}isConfigLoaded(){return this.configLoaded}async hasErrorLayout(e){const r=await this.loadConfig();return r?!!(r.layouts[e]||r.layouts[String(e)]):!1}clearConfigCache(){this.errorConfig=null,this.configLoaded=!1,this.debug&&jt.log("Config cache cleared")}}const Mt=ut("ModuleAssetLoader");class O_{constructor(){$(this,"loadedAssets",new Map);$(this,"loadingPromises",new Map);$(this,"failedJsAssets",new Set)}hasFailedJsAssets(){return this.failedJsAssets.size>0}getFailedJsAssets(){return[...this.failedJsAssets]}async loadActiveExtensionAssets(e){if(!e||e.length===0){Mt.log("No module assets to load");return}const r=[...e].sort((d,f)=>d.priority-f.priority);Mt.log("Loading module assets:",r.map(d=>d.identifier));const a=r.filter(d=>d.css).map(d=>this.loadCSS(d.identifier,d.css)),i=r.filter(d=>d.js).map(d=>this.loadJS(d.identifier,d.js)),l=await Promise.allSettled([...a,...i]),c=l.filter(d=>d.status==="rejected");if(c.length>0){Mt.warn(`Some module assets failed to load (${c.length}/${l.length}); continuing with the rest`,this.getFailedJsAssets());return}Mt.log("All module assets loaded successfully")}async loadBundle(e,r,a){const i=[];if(a&&i.push(this.loadBundleCss(e,a)),r&&i.push(this.loadBundleJs(e,r)),i.length===0){Mt.log(`No bundle assets to load for: ${e}`);return}await Promise.all(i)}async loadBundleCss(e,r){const a=`ext-bundle-css-${e}`;if(document.getElementById(a)){Mt.log(`Bundle CSS already loaded: ${e}`);return}return new Promise(i=>{const l=document.createElement("link");l.rel="stylesheet",l.href=r,l.id=a,l.onload=()=>{Mt.log(`Bundle CSS loaded: ${e}`),this.registerLoadedAsset(`bundle-${e}`,{type:"css",element:l}),i()},l.onerror=()=>{Mt.warn(`Failed to load bundle CSS: ${e} (${r})`),i()},document.head.appendChild(l)})}async loadBundleJs(e,r){const a=`ext-bundle-js-${e}`;if(document.getElementById(a)){Mt.log(`Bundle JS already loaded: ${e}`);return}const i=this.loadingPromises.get(a);if(i)return Mt.log(`Bundle JS already loading: ${e}`),i;const l=Yg(r,{id:a},{label:`bundle JS: ${e}`}).then(()=>{Mt.log(`Bundle JS loaded: ${e}`);const c=document.getElementById(a);c&&this.registerLoadedAsset(`bundle-${e}`,{type:"js",element:c}),this.loadingPromises.delete(a)}).catch(c=>{throw Mt.warn(`Failed to load bundle JS: ${e} (${r})`,c),this.failedJsAssets.add(e),this.loadingPromises.delete(a),c});return this.loadingPromises.set(a,l),l}async loadCSS(e,r){const a=`module-css-${e}`;if(document.getElementById(a)){Mt.log(`CSS already loaded: ${e}`);return}return new Promise((i,l)=>{const c=document.createElement("link");c.rel="stylesheet",c.href=r,c.id=a,c.onload=()=>{Mt.log(`CSS loaded: ${e}`),this.registerLoadedAsset(e,{type:"css",element:c}),i()},c.onerror=()=>{Mt.warn(`Failed to load CSS: ${e} (${r})`),i()},document.head.appendChild(c)})}async loadJS(e,r){const a=`module-js-${e}`;if(document.getElementById(a)){Mt.log(`JS already loaded: ${e}`);return}const i=this.loadingPromises.get(e);if(i)return Mt.log(`JS already loading: ${e}`),i;const l=Yg(r,{id:a},{label:`JS: ${e}`}).then(()=>{Mt.log(`JS loaded: ${e}`);const c=document.getElementById(a);c&&this.registerLoadedAsset(e,{type:"js",element:c}),this.loadingPromises.delete(e)}).catch(c=>{throw Mt.warn(`Failed to load JS: ${e} (${r})`,c),this.failedJsAssets.add(e),this.loadingPromises.delete(e),c});return this.loadingPromises.set(e,l),l}registerLoadedAsset(e,r){const a=this.loadedAssets.get(e)||[];a.push(r),this.loadedAssets.set(e,a)}unloadExtensionAsset(e){const r=this.loadedAssets.get(e);if(!r||r.length===0){Mt.log(`No assets to unload for: ${e}`);return}r.forEach(a=>{a.element.parentNode&&(a.element.parentNode.removeChild(a.element),Mt.log(`${a.type.toUpperCase()} unloaded: ${e}`))}),this.loadedAssets.delete(e),Mt.log(`All assets unloaded for: ${e}`)}unloadAllAssets(){Array.from(this.loadedAssets.keys()).forEach(r=>{this.unloadExtensionAsset(r)}),Mt.log("All module assets unloaded")}isLoaded(e){return this.loadedAssets.has(e)}getLoadedModules(){return Array.from(this.loadedAssets.keys())}}let pf=null;function mf(){return pf||(pf=new O_),pf}function L_(){if(typeof window>"u")return[];const o=window.G7Config;if(!o?.moduleAssets)return[];const e=[];for(const[r,a]of Object.entries(o.moduleAssets)){const i=a;e.push({identifier:r,js:i.js?xa(i.js):i.js,css:i.css?xa(i.css):i.css,priority:i.priority,external:i.external})}return e}function M_(){if(typeof window>"u")return null;const o=window.G7Config;if(!o?.bundleUrls)return null;const e=o.bundleUrls;return{moduleJs:e.moduleJs?xa(e.moduleJs):e.moduleJs,moduleCss:e.moduleCss?xa(e.moduleCss):e.moduleCss,pluginJs:e.pluginJs?xa(e.pluginJs):e.pluginJs,pluginCss:e.pluginCss?xa(e.pluginCss):e.pluginCss}}function $_(){if(typeof window>"u")return[];const o=window.G7Config;if(!o?.pluginAssets)return[];const e=[];for(const[r,a]of Object.entries(o.pluginAssets)){const i=a;e.push({identifier:r,js:i.js?xa(i.js):i.js,css:i.css?xa(i.css):i.css,priority:i.priority,external:i.external})}return e}const yf=ut("SystemBannerManager");class Dc{static show(e){this.banners.set(e.id,e),this.render(),yf.log(`Banner shown: ${e.id}`)}static hide(e){this.banners.delete(e)&&(this.render(),yf.log(`Banner hidden: ${e}`))}static hideAll(){this.banners.clear(),this.render(),yf.log("All banners hidden")}static detectLocale(){try{const r=window.G7Core;if(r?.locale?.current)return r.locale.current()}catch{}return(navigator.language||"ko").split("-")[0]}static resolveMessage(e,r){return typeof e=="string"?e:e[r]||e.en||e.ko||Object.values(e)[0]||""}static render(){if(typeof document>"u")return;let e=document.getElementById(this.containerId);if(this.banners.size===0){e&&(e.remove(),this.adjustAppPadding(0));return}e||(e=document.createElement("div"),e.id=this.containerId,e.style.cssText="position:fixed;top:0;left:0;right:0;z-index:99999;",document.body.prepend(e));const r=Array.from(this.banners.values()).sort((i,l)=>(i.order??0)-(l.order??0)),a=this.detectLocale();e.innerHTML=r.map(i=>{const l=this.resolveMessage(i.message,a),c=i.background||"#f59e0b",d=i.color||"white";return`<div data-banner-id="${i.id}" style="background:${c};color:${d};text-align:center;padding:8px 16px;font-size:14px;font-weight:500;">${l}</div>`}).join(""),requestAnimationFrame(()=>{e&&this.adjustAppPadding(e.offsetHeight)})}static adjustAppPadding(e){const r=document.getElementById("app");r&&(r.style.paddingTop=e>0?`${e}px`:"")}}$(Dc,"banners",new Map),$(Dc,"containerId","g7-system-banners");const U=ut("TemplateApp");function dy(o){const e={};for(const r of o.keys()){if(r in e)continue;const a=o.getAll(r);a.length>1?e[r]=a:a.length===1&&(r.endsWith("[]")?e[r]=a:e[r]=a[0])}return e}const xr=class xr{constructor(e){$(this,"router",null);$(this,"layoutLoader",null);$(this,"errorPageHandler",null);$(this,"config");$(this,"globalState");$(this,"globalStateListeners",new Set);$(this,"currentRouteChangeId",0);$(this,"currentDataSources",[]);$(this,"currentRawDataSources",[]);$(this,"currentRouteParams",{});$(this,"currentQueryParams",new URLSearchParams);$(this,"currentFetchedData",{});$(this,"currentLayoutName","");$(this,"templateErrorHandling",null);$(this,"currentWebSocketSubscriptions",[]);$(this,"extensionCacheVersion",0);$(this,"currentGlobalHeaders",[]);$(this,"transitionOverlayEl",null);$(this,"skeletonOverlayRoot",null);$(this,"skeletonOverlayContainer",null);$(this,"_spinnerState",null);$(this,"modalDataSources",new Map);this.globalState={sidebarOpen:!1},this.loadG7Config(),this.migrateLocaleStorage();const r=this.loadLocaleFromStorage(),a=r||e.locale||"ko";this.config={...e,locale:a},!r&&e.locale&&this.saveLocaleToStorage(e.locale)}loadG7Config(){if(typeof window<"u"&&window.G7Config){const e=window.G7Config;e.settings&&(this.globalState.settings=e.settings,U.log("Loaded settings from G7Config:",Object.keys(e.settings)),e.settings.upload&&(this.globalState.uploadSettings=e.settings.upload)),e.plugins&&(this.globalState.plugins=e.plugins,U.log("Loaded plugin settings from G7Config:",Object.keys(e.plugins))),e.modules&&(this.globalState.modules=e.modules,U.log("Loaded module settings from G7Config:",Object.keys(e.modules))),e.appConfig&&(this.globalState.appConfig=e.appConfig,U.log("Loaded appConfig from G7Config:",Object.keys(e.appConfig)))}}resolveRouteExpressions(e){const r=new _n,a={_global:this.globalState};return e.map(i=>{const l={...i};if(l.path&&l.path.includes("{{")){const c=l.path;l.path=r.resolveBindings(l.path,a),l.path=l.path.replace(/\/\/+/g,"/")||"/",l.path.includes("{{")&&(U.warn("Route expression resolution failed, using fallback:",c),l.path=c.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/")}if(l.redirect&&l.redirect.includes("{{")){const c=l.redirect;l.redirect=r.resolveBindings(l.redirect,a),l.redirect=l.redirect.replace(/\/\/+/g,"/")||"/",l.redirect.includes("{{")&&(U.warn("Route redirect expression resolution failed:",c),l.redirect=c.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/")}return l})}migrateLocaleStorage(){const e=["locale","g7_template_locale"];for(const r of e)try{const a=localStorage.getItem(r);a&&a!==localStorage.getItem(xr.LOCALE_STORAGE_KEY)&&(localStorage.setItem(xr.LOCALE_STORAGE_KEY,a),U.log(`Migrated locale from '${r}' to '${xr.LOCALE_STORAGE_KEY}'`)),localStorage.removeItem(r)}catch(a){U.warn(`Failed to migrate locale key '${r}':`,a)}}async init(){try{eo.getInstance().setDebug(this.config.debug),U.log("Initializing with config:",this.config),GS();const e=mr.getInstance(),r=br.getInstance(),a=this.loadCacheVersionFromStorage()||0,[i,l,c,d,f]=await Promise.all([Lo({templateId:this.config.templateId,templateType:this.config.templateType,locale:this.config.locale,debug:this.config.debug,cacheVersion:a}),e.loadComponents(this.config.templateId,this.config.templateType),Bl(Nr(`/api/templates/${this.config.templateId}/routes`,"json",a>0?a:null),{label:"routes.json"}).then(A=>{if(!A.ok)throw new Error(`Failed to load routes: ${A.statusText}`);return A.json()}).then(A=>{if(!A.success)throw new Error("Failed to load routes from API");return A.data}).catch(A=>{throw U.error("Error loading routes:",A),A}),r.preloadAuth(this.config.templateType==="admin"?"admin":"user"),fetch(Nr(`/api/templates/${this.config.templateId}/config`,"json")).then(A=>A.ok?A.json():null).then(A=>!A?.success||!A?.data?null:A.data).catch(A=>(U.warn("Error loading template config:",A),null))]);if(U.log("Template Engine initialized"),U.log("ComponentRegistry loaded"),U.log("Routes data loaded"),U.log("User info preloaded"),U.log("Template config loaded:",f),f?.cache_version!==void 0){const A=this.loadCacheVersionFromStorage();if(this.extensionCacheVersion=f.cache_version,this.saveCacheVersionToStorage(this.extensionCacheVersion),U.log("Extension cache version:",this.extensionCacheVersion),A!==null&&A!==this.extensionCacheVersion){U.log("Cache version changed, reloading routes...");const x=await Bl(Nr(`/api/templates/${this.config.templateId}/routes`,"json",this.extensionCacheVersion),{label:"routes.json (reload)"}).then(R=>{if(!R.ok)throw new Error(`Failed to reload routes: ${R.statusText}`);return R.json()}).then(R=>{if(!R.success)throw new Error("Failed to reload routes from API");return R.data});Array.isArray(x.routes)&&(x.routes=this.resolveRouteExpressions(x.routes),Object.assign(c,x),U.log("Routes reloaded with new cache version"));try{const{TranslationEngine:R}=await Promise.resolve().then(()=>ep),M=R.getInstance();M.setCacheVersion(this.extensionCacheVersion);const O=this.config.locale||"ko",L="en";await M.loadTranslations(this.config.templateId,O,"/api",!0),O!==L&&await M.loadTranslations(this.config.templateId,L,"/api",!0),U.log("Translations reloaded with new cache version")}catch(R){U.error("Failed to reload translations:",R)}}}f?.errorHandling&&(this.templateErrorHandling=f.errorHandling,Sr().setTemplateConfig(this.templateErrorHandling),U.log("Template errorHandling registered:",this.templateErrorHandling)),r.on("logout",()=>{U.log("User logged out")}),r.on("authStateChange",A=>{U.log("Auth state changed:",A)}),U.log("AuthManager event handlers registered");const g=jr(),m=this.config.templateType==="admin"?"admin":"user",y=r.getConfig(m);g.setOnUnauthorized(()=>{U.log("Unauthorized - redirecting to login page"),g.removeToken();const A=window.location.pathname+window.location.search,x=r.getLoginRedirectUrl(m,A,"session_expired");window.location.href=x}),U.log("ApiClient onUnauthorized callback registered"),this.layoutLoader=new of(e),this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),U.log("LayoutLoader initialized:",this.layoutLoader);const w=new _r({onUnauthorized:()=>{U.warn("Unauthorized request in error page")}});if(this.errorPageHandler=new D_({templateId:this.config.templateId,layoutLoader:this.layoutLoader,locale:this.config.locale,debug:this.config.debug,renderFunction:us,dataSourceManager:w,globalState:this.globalState}),U.log("ErrorPageHandler initialized with DataSourceManager"),await this.handleServerError()){U.log("Server error handled, skipping normal initialization");return}if(this.router=new k_(this.config.templateId),Array.isArray(c.routes)){const A=this.resolveRouteExpressions(c.routes);this.router.setRoutes(A)}else throw new Error("Invalid routes data format");U.log("Router initialized:",this.router),U.log("Routes set:",this.router.getRoutes());const{getActionDispatcher:S}=await Promise.resolve().then(()=>Pr),v=S();if(v&&(v.setDefaultContext({navigate:A=>this.router?.navigate(A)}),v.setGlobalStateUpdater((A,x)=>this.setGlobalState(A,x)),U.log("Navigate function and setGlobalState injected to ActionDispatcher")),await this.loadExtensionAssets(),this.reinitializeTemplateHandlers(),this.router.on("routeChange",A=>this.handleRouteChange(A)),this.router.on("routeNotFound",A=>this.handleRouteNotFound(A)),typeof window<"u"&&uy(window.location.pathname)){U.log("Layout editor mode detected — skipping router match"),await us({containerId:"app",layoutJson:{components:[]},dataContext:{},translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),U.log("Template App initialized in layout editor mode");return}this.router.navigateToCurrentPath(),U.log("Template App initialized successfully")}catch(e){U.error("Initialization failed:",e),this.showInitError(e)}}async loadExtensionAssets(){try{const e=mf(),r=M_();if(!r){await this.loadExtensionAssetsIndividually();return}await e.loadBundle("module",r.moduleJs,r.moduleCss),await e.loadBundle("plugin",r.pluginJs,r.pluginCss),U.log("Extension bundle assets loaded successfully")}catch(e){U.warn("Failed to load extension assets:",e)}}async loadExtensionAssetsIndividually(){const e=mf(),r=L_();r.length>0&&(U.log("Loading module assets (individual fallback):",r.map(i=>i.identifier)),await e.loadActiveExtensionAssets(r));const a=$_();a.length>0&&(U.log("Loading plugin assets (individual fallback):",a.map(i=>i.identifier)),await e.loadActiveExtensionAssets(a))}async handleRouteChange(e){const r=++this.currentRouteChangeId;window.__g7ForcedLocalFields=void 0,window.__g7ActionContext=void 0,window.__g7PendingLocalState=void 0,window.__g7LastSetLocalSnapshot=void 0,window.__g7SetLocalOverrideKeys=void 0,window.__g7SequenceLocalSync=void 0,window.__g7AutoBindingPaths=new Map;try{if(U.log("Route changed:",e,"requestId:",r),!this.layoutLoader)throw new Error("LayoutLoader is not initialized");if(!e.layout)throw new Error("Route layout is not defined");let a=e.layout;const i=a==="__preview__";i&&e.params?.token&&(a=`__preview__/${e.params.token}`);const l=await this.layoutLoader.loadLayout(this.config.templateId,a);if(r!==this.currentRouteChangeId){U.log("Route change cancelled (newer request exists):",r);return}U.log("Layout loaded:",l);const c=Sr();l.errorHandling?(c.setLayoutConfig(l.errorHandling),U.log("Layout errorHandling registered:",l.errorHandling)):c.clearLayoutConfig();const d=l.data_sources||[],f=e.query||{},g=new URLSearchParams;for(const[Z,pe]of Object.entries(f))if(Array.isArray(pe))for(const H of pe)g.append(Z,H);else g.set(Z,pe);const m={route:e.params||{},query:f,_global:this.globalState};l.scripts&&Array.isArray(l.scripts)&&await this.loadLayoutScripts(l.scripts,m);const{DataSourceManager:y,getActionDispatcher:w}=await Promise.resolve().then(()=>Pr),S=new y;if(i){const Z=w();Z&&Z.setPreviewMode(!0),this.setGlobalState({__isPreview:!0}),Dc.show({id:"preview-mode",message:{ko:"⚠ 미리보기 모드 — 페이지 이동이 비활성화됩니다",en:"⚠ Preview Mode — Navigation is disabled"},background:"linear-gradient(90deg, #f59e0b, #d97706)",color:"white"}),U.log("Preview mode activated")}else{const Z=w();Z?.isPreviewMode()&&(Z.setPreviewMode(!1),Dc.hide("preview-mode"))}if(this.currentGlobalHeaders=l.globalHeaders||[],this.currentGlobalHeaders.length>0){S.setGlobalHeaders(this.currentGlobalHeaders);const Z=w();Z&&Z.setGlobalHeaders(this.currentGlobalHeaders),U.log("globalHeaders set:",this.currentGlobalHeaders.map(pe=>pe.pattern))}if(l.named_actions&&Object.keys(l.named_actions).length>0){const Z=w();Z&&Z.setNamedActions(l.named_actions)}const v=S.filterByCondition(d,m);d.length!==v.length&&U.log("Data sources filtered by condition:",{before:d.map(Z=>Z.id),after:v.map(Z=>Z.id)});const A=v.filter(Z=>Z.loading_strategy==="blocking"),x=v.filter(Z=>(Z.loading_strategy||"progressive")!=="blocking"&&Z.type!=="websocket"),R=Array.isArray(l.transition_overlay?.wait_for)?l.transition_overlay.wait_for:[],M=R.length>0&&v.some(Z=>R.includes(Z.id)&&Z.type!=="websocket"&&(Z.loading_strategy||"progressive")!=="background");if((A.length>0||M)&&l.transition_overlay){const Z=typeof l.transition_overlay=="boolean"?{enabled:l.transition_overlay,style:"opaque"}:l.transition_overlay;Z.enabled&&Z.style==="skeleton"&&Z.skeleton?.component&&Z.target?this.renderSkeletonOverlay(Z.target,Z.skeleton,l,Z.fallback_target):Z.enabled&&Z.style==="spinner"&&Z.target&&this.renderSpinnerOverlay(Z.target,Z.spinner,Z.fallback_target)}let O={},L={},q={},G={};this.currentWebSocketSubscriptions.length>0&&(U.log("Unsubscribing previous WebSocket subscriptions:",this.currentWebSocketSubscriptions),S.unsubscribeWebSockets(this.currentWebSocketSubscriptions),this.currentWebSocketSubscriptions=[]),this.currentDataSources=v,this.currentRawDataSources=d,this.currentRouteParams=e.params||{},this.currentQueryParams=g,U.log(`handleRouteChange #${r} - queryObject:`,f),U.log(`handleRouteChange #${r} - queryParams:`,g.toString()),A.length>0&&(U.log("Fetching blocking data sources:",A.map(pe=>pe.id)),(await S.fetchDataSourcesWithResults(A,e.params||{},g)).forEach(pe=>{if(pe.state==="success"&&pe.data!==void 0)O[pe.id]=pe.data;else if(pe.state==="error"&&pe.error){const H=pe.error,T=H.response?.data?.message;L[pe.id]={message:T||pe.error.message,status:H.response?.status||H.status}}}),this.processInitOptions(A,O,q,G),this.currentFetchedData={...O},U.log("Blocking data loaded:",Object.keys(O)),Object.keys(L).length>0&&U.log("Data source errors:",L),Object.keys(q).length>0&&U.log("Local state init (blocking):",Object.keys(q)),Object.keys(G).length>0&&U.log("Isolated state init (blocking):",Object.keys(G)));const B=x.length>0;if(B&&(zr.setPending(!0),U.log("Transition started before rendering")),r!==this.currentRouteChangeId){U.log("Route change cancelled after blocking data (newer request exists):",r);return}const{getState:K}=await Promise.resolve().then(()=>Pr),fe=K().currentDataContext||{},Ce=x.map(Z=>Z.id),ke={};if(B){Ce.forEach(pe=>{fe[pe]!==void 0?ke[pe]=fe[pe]:ke[pe]=void 0});const Z=Object.keys(ke).filter(pe=>ke[pe]!==void 0);Z.length>0&&U.log("Preserving previous progressive data:",Z),U.log("Progressive data sources initialized:",Ce)}const Ue=l.defines||{},Pe={...ke,...e.params,...O,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState},_globalSetState:Z=>this.setGlobalState(Z),_dataSourceErrors:Object.keys(L).length>0?L:void 0,_localInit:Object.keys(q).length>0?q:void 0,_isolatedInit:Object.keys(G).length>0?G:void 0,_defines:Object.keys(Ue).length>0?Ue:void 0};if(l.computed&&Object.keys(l.computed).length>0){const Z=this.calculateComputed(l.computed,Pe);Object.keys(Z).length>0&&(Pe._computed=Z,this.globalState._computed=Z,U.log("Computed values calculated:",Object.keys(Z))),Pe._computedDefinitions=l.computed}const qe=l.layout_name||e.layout;this.currentLayoutName!==""&&this.currentLayoutName!==qe&&(U.log("_local reset due to layout change:",{from:this.currentLayoutName,to:qe}),this.globalState._local={},G0()),this.currentLayoutName=qe;const J=l.initLocal||l.state;if(J&&Object.keys(J).length>0){this.globalState._local||(this.globalState._local={});for(const[Z,pe]of Object.entries(J))this.globalState._local[Z]===void 0&&(this.globalState._local[Z]=JSON.parse(JSON.stringify(pe)));Pe._local={...this.globalState._local},U.log("initLocal applied to _local:",Object.keys(J))}if(l.initGlobal&&Object.keys(l.initGlobal).length>0){for(const[Z,pe]of Object.entries(l.initGlobal))this.globalState[Z]===void 0&&(this.globalState[Z]=JSON.parse(JSON.stringify(pe)));U.log("initGlobal applied to _global:",Object.keys(l.initGlobal))}if(l.initIsolated&&Object.keys(l.initIsolated).length>0){for(const[Z,pe]of Object.entries(l.initIsolated))G[Z]===void 0&&(G[Z]=JSON.parse(JSON.stringify(pe)));U.log("initIsolated applied:",Object.keys(l.initIsolated))}Pe._global={...this.globalState,layoutWarnings:l.warnings||[]};const he=l.initActions||l.init_actions;if(he&&he.length>0){await this.executeInitActions(he,Pe),U.log("initActions executed before render");const Z=this.globalState;if(Z._local){if(Object.keys(q).length>0){for(const[pe,H]of Object.entries(q))Z._local[pe]!==void 0?Z._local[pe]=this.deepMerge(Z._local[pe],H):Z._local[pe]=H;U.log("localInit merged into _local after initActions:",Object.keys(q))}Pe._local=Z._local,U.log("_local merged into dataContext:",Z._local)}if(Pe._global={...this.globalState,layoutWarnings:l.warnings||[]},U.log("_global merged into dataContext after initActions"),l.computed&&Object.keys(l.computed).length>0){const pe=this.calculateComputed(l.computed,Pe);Object.keys(pe).length>0&&(Pe._computed=pe,this.globalState._computed=pe,U.log("Computed values recalculated after init_actions:",Object.keys(pe)))}if(Object.keys(this.currentFetchedData).length>0){for(const[pe,H]of Object.entries(this.currentFetchedData))Pe[pe]=H;U.log("Fetched data sources merged into dataContext after initActions:",Object.keys(this.currentFetchedData))}}if(l.transition_overlay&&!this.skeletonOverlayContainer&&this.showTransitionOverlay(l.transition_overlay,l),await us({containerId:"app",layoutJson:l,dataContext:Pe,translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),U.log("Initial render complete with blocking data"),this.reattachSpinnerOverlay(),x.length>0){if(r!==this.currentRouteChangeId){U.log("Route change cancelled before progressive fetch (newer request exists):",r),zr.setPending(!1),this.hideTransitionOverlay();return}U.log("Fetching progressive/background data sources:",x.map(H=>H.id));const{updateTemplateData:Z,getState:pe}=await Promise.resolve().then(()=>Pr);try{const H=x.map(async T=>{try{const de=(await S.fetchDataSourcesWithResults([T],e.params||{},g,this.globalState))[0];if(!de){U.log(`Data source ${T.id} skipped (no fetch result)`);return}if(r!==this.currentRouteChangeId){U.log(`Data source ${T.id} fetch cancelled (newer request exists)`);return}if(de.state==="success"&&de.data!==void 0){U.log(`Progressive data source loaded: ${T.id}`),this.currentFetchedData[T.id]=de.data;const me={};this.processInitOptions([T],{[T.id]:de.data},me);const ae=pe();if(ae.bindingEngine){const ze=[T.id];T.initGlobal&&ze.push("_global"),T.initLocal&&ze.push("_local"),ae.bindingEngine.invalidateCacheByKeys(ze)}const Re={[T.id]:de.data};Object.keys(me).length>0&&(Re._localInit=me,Object.assign(q,me)),T.initGlobal&&(Re._global={...this.globalState}),Z(Re)}else if(de.state==="error"&&de.error){const me=de.error,ae=me.response?.data?.message;L[T.id]={message:ae||de.error.message,status:me.response?.status||me.status},U.log(`Progressive data source error: ${T.id}`,L[T.id]),Z({[T.id]:null,_dataSourceErrors:{...L}})}}catch(ge){U.error(`Failed to fetch data source: ${T.id}`,ge)}});if(await Promise.all(H),r!==this.currentRouteChangeId){U.log("Route change cancelled after progressive fetch (newer request exists):",r);return}U.log("All progressive data sources loaded")}finally{zr.setPending(!1),this.hideTransitionOverlay()}}else this.hideTransitionOverlay();const Ee=v.filter(Z=>Z.type==="websocket");if(Ee.length>0){U.log("Subscribing WebSocket data sources:",Ee.map(T=>T.id));const{updateTemplateData:Z,getState:pe}=await Promise.resolve().then(()=>Pr),H={...this.currentFetchedData,...e.params,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState}};U.log("WebSocket binding context keys:",Object.keys(H)),this.currentWebSocketSubscriptions=S.subscribeWebSockets(v,(T,ge)=>{U.log(`WebSocket data received for: ${T}`,ge),this.currentFetchedData[T]=ge;const de=v.find(ze=>ze.id===T),me=pe();if(me.bindingEngine){const ze=[T];de?.initGlobal&&ze.push("_global"),de?.initLocal&&ze.push("_local"),me.bindingEngine.invalidateCacheByKeys(ze)}Z({[T]:ge});const Re=v.find(ze=>ze.type==="websocket"&&(ze.target_source||ze.id)===T)?.onReceive;Array.isArray(Re)&&Re.length>0&&window.G7Core?.dispatch&&(async()=>{for(const $e of Re)try{const Ge=this.getActionDispatcher?.();Ge&&await Ge.dispatchAction($e,{navigate:this.getRouter?.()?(At,yt)=>this.getRouter().navigate(At,yt):void 0,setState:At=>this.setGlobalState(At),state:this.globalState,data:{...this.globalState,$args:[ge],$event:ge},_isDispatchFallbackContext:!0})}catch(Ge){U.error(`WebSocket onReceive action failed for ${T}:`,Ge)}})()},H),U.log("WebSocket subscriptions established:",this.currentWebSocketSubscriptions)}U.log("Layout rendered successfully")}catch(a){U.error("Route change handling failed:",a),this.hideTransitionOverlay(),this.showRouteError(a)}}processInitOptions(e,r,a,i){e.some(c=>c.refetchOnMount===!0&&c.initLocal)&&(a._forceLocalInit=Date.now(),U.log("refetchOnMount detected, forcing local state init")),e.forEach(c=>{const d=r[c.id];if(!d)return;const f=d.data??d;if(c.initLocal){if(typeof c.initLocal=="string"){let g=f;if(c.initLocalDefaults&&typeof c.initLocalDefaults=="object"){const y=this.evaluateDefaults(c.initLocalDefaults,r);g={...y,...f},U.log(`initLocalDefaults applied for ${c.initLocal}:`,Object.keys(y))}const m=this.globalState._local?.[c.initLocal];m!==void 0&&typeof m=="object"&&typeof g=="object"?(a[c.initLocal]=this.deepMerge(m,g),U.log(`initLocal (merged): ${c.id}.data -> _local.${c.initLocal}`)):(a[c.initLocal]=g,U.log(`initLocal: ${c.id}.data -> _local.${c.initLocal}`))}else if(typeof c.initLocal=="object"&&c.initLocal.key){const{key:g,path:m}=c.initLocal;let y=m?this.extractValueByPathOrExpression(f,m,c.id):f;if(c.initLocalDefaults&&typeof c.initLocalDefaults=="object"){const S=this.evaluateDefaults(c.initLocalDefaults,r);y={...S,...y},U.log(`initLocalDefaults applied for ${g}:`,Object.keys(S))}const w=this.globalState._local?.[g];w!==void 0&&typeof w=="object"&&typeof y=="object"?(a[g]=this.deepMerge(w,y),U.log(`initLocal (merged): ${c.id}.data${m?"."+m:""} -> _local.${g}`)):(a[g]=y,U.log(`initLocal: ${c.id}.data${m?"."+m:""} -> _local.${g}`))}else if(typeof c.initLocal=="object"){const g=c.initLocal._merge||"deep",m=this.flattenNestedObjectToMappings(c.initLocal);for(const{targetPath:y,sourcePath:w}of m){const S=this.extractValueByPathOrExpression(f,w,c.id);if(y.includes(".")){const v=y.split(".")[0];a[v]===void 0&&this.globalState._local?.[v]!==void 0&&(a[v]=JSON.parse(JSON.stringify(this.globalState._local[v]))),this.setValueAtPath(a,y,S,g),U.log(`initLocal map (${g}): ${c.id} ${w} -> _local.${y}`)}else{const v=this.globalState._local?.[y];g==="replace"?(a[y]=S,U.log(`initLocal map (replace): ${c.id} ${w} -> _local.${y}`)):g==="shallow"?v!==void 0&&typeof v=="object"&&typeof S=="object"?(a[y]={...v,...S},U.log(`initLocal map (shallow): ${c.id} ${w} -> _local.${y}`)):(a[y]=S,U.log(`initLocal map: ${c.id} ${w} -> _local.${y}`)):v!==void 0&&typeof v=="object"&&typeof S=="object"?(a[y]=this.deepMerge(v,S),U.log(`initLocal map (deep): ${c.id} ${w} -> _local.${y}`)):(a[y]=S,U.log(`initLocal map: ${c.id} ${w} -> _local.${y}`))}}}}if(c.initGlobal)if(typeof c.initGlobal=="object"&&!Array.isArray(c.initGlobal)&&!("key"in c.initGlobal))for(const[m,y]of Object.entries(c.initGlobal)){if(typeof y!="string"){U.warn(`initGlobal map value must be string, got ${typeof y} for key ${m}`);continue}const w=this.extractValueByPathOrExpression(f,y,c.id),S=this.globalState[m];S!==void 0&&typeof S=="object"&&typeof w=="object"?(this.globalState[m]=this.deepMerge(S,w),U.log(`initGlobal map (merged): ${c.id} ${y} -> _global.${m}`)):(this.globalState[m]=w,U.log(`initGlobal map: ${c.id} ${y} -> _global.${m}`))}else{const m=Array.isArray(c.initGlobal)?c.initGlobal:[c.initGlobal];for(const y of m)if(typeof y=="string"){const w=this.globalState[y];w!==void 0&&typeof w=="object"&&typeof f=="object"?(this.globalState[y]=this.deepMerge(w,f),U.log(`initGlobal (merged): ${c.id}.data -> _global.${y}`)):(this.globalState[y]=f,U.log(`initGlobal: ${c.id}.data -> _global.${y}`))}else if(typeof y=="object"&&y.key){const{key:w,path:S}=y,v=S?this.extractValueByPathOrExpression(f,S,c.id):f,A=this.globalState[w];A!==void 0&&typeof A=="object"&&typeof v=="object"?(this.globalState[w]=this.deepMerge(A,v),U.log(`initGlobal (merged): ${c.id}.data${S?"."+S:""} -> _global.${w}`)):(this.globalState[w]=v,U.log(`initGlobal: ${c.id}.data${S?"."+S:""} -> _global.${w}`))}}if(i&&c.initIsolated){if(typeof c.initIsolated=="string"){const g=i[c.initIsolated];g!==void 0&&typeof g=="object"&&typeof f=="object"?(i[c.initIsolated]=this.deepMerge(g,f),U.log(`initIsolated (merged): ${c.id}.data -> _isolated.${c.initIsolated}`)):(i[c.initIsolated]=f,U.log(`initIsolated: ${c.id}.data -> _isolated.${c.initIsolated}`))}else if(typeof c.initIsolated=="object"&&c.initIsolated.key){const{key:g,path:m}=c.initIsolated,y=m?this.extractValueByPathOrExpression(f,m,c.id):f,w=i[g];w!==void 0&&typeof w=="object"&&typeof y=="object"?(i[g]=this.deepMerge(w,y),U.log(`initIsolated (merged): ${c.id}.data${m?"."+m:""} -> _isolated.${g}`)):(i[g]=y,U.log(`initIsolated: ${c.id}.data${m?"."+m:""} -> _isolated.${g}`))}else if(typeof c.initIsolated=="object")for(const[g,m]of Object.entries(c.initIsolated)){if(typeof m!="string"){U.warn(`initIsolated map value must be string, got ${typeof m} for key ${g}`);continue}const y=this.extractValueByPathOrExpression(f,m,c.id),w=i[g];w!==void 0&&typeof w=="object"&&typeof y=="object"?(i[g]=this.deepMerge(w,y),U.log(`initIsolated map (merged): ${c.id} ${m} -> _isolated.${g}`)):(i[g]=y,U.log(`initIsolated map: ${c.id} ${m} -> _isolated.${g}`))}}})}evaluateDefaults(e,r){const a={};for(const[i,l]of Object.entries(e))if(typeof l=="string"&&l.startsWith("{{")&&l.endsWith("}}")){const c=l.slice(2,-2).trim();a[i]=this.evaluateExpression(c,r)}else a[i]=l;return a}evaluateExpression(e,r){try{const a=e.split("??").map(i=>i.trim());for(const i of a){if(/^['"].*['"]$/.test(i))return i.slice(1,-1);if(i==="true")return!0;if(i==="false")return!1;if(/^-?\d+(\.\d+)?$/.test(i))return Number(i);const l=this.getNestedValue(r,i);if(l!=null)return l}return}catch(a){U.warn(`Expression evaluation failed: ${e}`,a);return}}getNestedValue(e,r){const a=r.replace(/\?\./g,".");let i=e;const l=a.split(/\.(?![^\[]*\])/).flatMap(c=>{const d=c.match(/^([^\[]*)((?:\[\d+\])*)$/);if(d){const[,f,g]=d,m=[];f&&m.push(f);const y=g.match(/\[\d+\]/g);return y&&m.push(...y),m}return[c]}).filter(c=>c!=="");for(const c of l){if(i==null)return;if(c.startsWith("[")&&c.endsWith("]")){const d=parseInt(c.slice(1,-1),10);i=i[d]}else i=i[c]}return i}extractValueByPathOrExpression(e,r,a){const i=Ta(r);if(i!==null){const l=new _n,c={data:e,_global:this.globalState,_local:this.globalState._local||{}};try{const d=Gn(i)?l.evaluatePipeExpression(i,c,{skipCache:!0}):l.evaluateExpression(i,c);return U.log(`initLocal/initGlobal expression evaluated: ${r} -> `,d),d}catch(d){U.warn(`initLocal/initGlobal expression evaluation failed for ${a}:`,r,d);return}}return this.getNestedValue(e,r)}async loadLayoutScripts(e,r){const a=[];for(const i of e){if((i.if!==void 0||i.conditions!==void 0)&&!Qi({if:i.if,conditions:i.conditions},r,this.bindingEngine,`script:${i.id}`)){U.log(`Script skipped (condition not met): ${i.id}`);continue}if(document.getElementById(i.id)){U.log(`Script already loaded: ${i.id}`);continue}const c=new Promise((d,f)=>{const g=document.createElement("script");g.src=i.src,g.id=i.id,g.async=i.async??!0,g.onload=()=>{U.log(`Script loaded successfully: ${i.id}`),d()},g.onerror=()=>{U.warn(`Failed to load script: ${i.id} (${i.src})`),d()},document.head.appendChild(g)});a.push(c)}a.length>0&&(await Promise.all(a),U.log(`All scripts loaded: ${e.filter(i=>!document.getElementById(i.id)||a.length>0).map(i=>i.id).join(", ")}`))}evaluateScriptCondition(e,r){try{if(e.startsWith("{{")&&e.endsWith("}}")){const a=e.slice(2,-2).trim();return new Function("ctx",` - with(ctx) { - try { - return Boolean(${a}); - } catch (e) { - return false; - } - } - `)(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 overlay`),this.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}this.hideSkeletonOverlay();const c=l==="fullpage"?"app":l==="fallback"?a:e,d=`#${CSS.escape(c)}`,f=document.createElement("style");f.id="g7-skeleton-overlay-style",f.textContent=[`${d}{position:relative;}`,"@keyframes g7-spin{to{transform:rotate(360deg)}}"].join(""),document.head.appendChild(f),this.transitionOverlayEl=f;const g=r?.text||window.G7Core?.t?.("nav.loading")||"";this._spinnerState={target:e,fallbackTarget:a,spinnerConfig:r,resolvedText:g},this._mountSpinnerInTarget(i);const m=l==="fullpage"?"fullpage (#app)":l==="fallback"?`fallback (#${a})`:`target (#${e})`,y=r?.component||"default spinner";U.log(`Spinner overlay rendered [${m}] with "${y}"`)}_mountSpinnerInTarget(e){if(!this._spinnerState)return;const{spinnerConfig:r,resolvedText:a}=this._spinnerState;if(this.skeletonOverlayRoot){try{this.skeletonOverlayRoot.unmount()}catch{}this.skeletonOverlayRoot=null}if(this.skeletonOverlayContainer){try{this.skeletonOverlayContainer.remove()}catch{}this.skeletonOverlayContainer=null}const i=document.createElement("div");if(i.id="g7-skeleton-overlay",i.setAttribute("role","status"),i.setAttribute("aria-busy","true"),e.appendChild(i),this.skeletonOverlayContainer=i,r?.component){const c=mr.getInstance().getComponent(r.component);if(c){const d=rd.createRoot(i);this.skeletonOverlayRoot=d,Oa.flushSync(()=>{d.render(We.createElement(c,{options:{text:a}}))});return}U.log(`Spinner component "${r.component}" not found in registry, using default spinner`)}i.innerHTML='<div style="position:absolute;inset:0;z-index:2147483647;overflow:hidden;background:var(--g7-overlay-bg, rgb(249,250,251));display:flex;align-items:center;justify-content:center;"><div style="width:32px;height:32px;border:3px solid var(--g7-spinner-color, #9ca3af);border-top-color:transparent;border-radius:50%;animation:g7-spin 0.8s linear infinite;"></div></div>'}reattachSpinnerOverlay(){if(!this._spinnerState)return;const{target:e,fallbackTarget:r}=this._spinnerState;let a=document.getElementById(e);!a&&r&&(a=document.getElementById(r)),a||(a=document.getElementById("app")),a&&(this._mountSpinnerInTarget(a),U.log("Spinner overlay reattached to new DOM target"))}hideSkeletonOverlay(){if(this.skeletonOverlayRoot){try{this.skeletonOverlayRoot.unmount()}catch{}this.skeletonOverlayRoot=null}if(this.skeletonOverlayContainer){try{this.skeletonOverlayContainer.parentNode&&this.skeletonOverlayContainer.remove()}catch{}this.skeletonOverlayContainer=null}const e=document.getElementById("g7-skeleton-overlay-style");e&&e.remove(),this._spinnerState=null}showRouteError(e){if(e instanceof Er&&e.details?.status===401){const a=this.config.templateId||"",i=window.location.pathname,l=a.includes("admin")||i.startsWith("/admin")?"admin":"user",c=i+window.location.search,d=!!jr().getToken()||e.details?.hadToken===!0,f=br.getInstance().getLoginRedirectUrl(l,c,d?"session_expired":void 0);window.location.href=f;return}const r=cy(e);gf.renderFromError("app",r,"페이지 로딩 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}async handleServerError(){const e=window.G7Error;if(!e)return!1;U.warn("Server error detected:",e);try{return this.errorPageHandler&&(e.data&&(this.globalState.errorData=e.data),this.errorPageHandler.updateGlobalState(this.globalState),this.errorPageHandler.updateLocale(this.config.locale),await this.errorPageHandler.renderError(e.code,"app"))?(U.log(`${e.code} error page rendered successfully`),!0):(U.log(`Falling back to ErrorDisplay for ${e.code}`),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0)}catch(r){return U.error(`Failed to render ${e.code} page:`,r),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0}}async handleRouteNotFound(e){U.warn("Route not found:",e);try{if(this.errorPageHandler&&(this.errorPageHandler.updateGlobalState(this.globalState),this.errorPageHandler.updateLocale(this.config.locale),await this.errorPageHandler.renderError(404,"app"))){U.log("404 error page rendered successfully");return}U.log("Falling back to ErrorDisplay for 404"),this.showRouteError(new Error(`Page not found: ${e}`))}catch(r){U.error("Failed to render 404 page:",r),this.showRouteError(new Error(`Page not found: ${e}`))}}getRouter(){return this.router}getConfig(){return this.config}getLayoutLoader(){return this.layoutLoader}async reloadExtensionState(){U.log("reloadExtensionState: start");let e;try{const r=await fetch(Nr(`/api/templates/${this.config.templateId}/config`,"json",null,`_=${Date.now()}`));if(r.ok){const a=await r.json();a?.success&&a?.data?.cache_version!==void 0&&(e=a.data.cache_version)}}catch(r){U.warn("reloadExtensionState: failed to fetch config.json",r)}e!==void 0&&e!==this.extensionCacheVersion&&(U.log(`reloadExtensionState: cache version ${this.extensionCacheVersion} -> ${e}`),this.extensionCacheVersion=e,this.saveCacheVersionToStorage(e));try{this.router&&(await this.router.loadRoutes(this.extensionCacheVersion),U.log("reloadExtensionState: routes reloaded"))}catch(r){U.error("reloadExtensionState: routes reload failed",r)}try{this.layoutLoader&&(this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),this.layoutLoader.clear(),U.log("reloadExtensionState: layout cache cleared"))}catch(r){U.error("reloadExtensionState: layout cache clear failed",r)}try{const{TranslationEngine:r}=await Promise.resolve().then(()=>ep),a=r.getInstance();this.extensionCacheVersion>0&&a.setCacheVersion(this.extensionCacheVersion);const i=this.config.locale||"ko",l="en";await a.loadTranslations(this.config.templateId,i,"/api",!0),i!==l&&await a.loadTranslations(this.config.templateId,l,"/api",!0),U.log("reloadExtensionState: translations reloaded")}catch(r){U.error("reloadExtensionState: translations reload failed",r)}try{const r=await fetch(`/api/locales/active?_=${Date.now()}`);if(r.ok){const i=(await r.json())?.data?.locales;if(Array.isArray(i)&&i.length>0){const l=this.globalState.appConfig??{};this.setGlobalState({appConfig:{...l,supportedLocales:i}}),U.log("reloadExtensionState: supportedLocales refreshed",i)}}}catch(r){U.warn("reloadExtensionState: supportedLocales refresh failed",r)}U.log("reloadExtensionState: done")}getActionDispatcher(){return ta().actionDispatcher}async changeLocale(e){try{if(U.log("Changing locale to:",e),this.config.locale===e){U.log("Locale is already",e);return}this.config.locale=e,this.saveLocaleToStorage(e),await this.saveLocaleToDatabase(e),this.layoutLoader&&(this.layoutLoader.clear(),U.log("Layout cache cleared")),this.errorPageHandler&&(this.errorPageHandler.updateLocale(e),U.log("ErrorPageHandler locale updated")),Oc(),await Lo({templateId:this.config.templateId,templateType:this.config.templateType,locale:e,debug:this.config.debug,cacheVersion:this.extensionCacheVersion}),U.log("Template Engine re-initialized with new locale");const{getActionDispatcher:r}=await Promise.resolve().then(()=>Pr),a=r();a&&(a.setDefaultContext({navigate:i=>this.router?.navigate(i)}),a.setGlobalStateUpdater((i,l)=>this.setGlobalState(i,l)),U.log("Navigate function and setGlobalState re-injected to ActionDispatcher")),this.reinitializeModuleHandlers(),U.log("Module handlers re-initialized"),this.reinitializeTemplateHandlers(),U.log("Template handlers re-initialized"),this.reinitializePluginHandlers(),U.log("Plugin handlers re-initialized"),this.router&&this.router.navigateToCurrentPath(),U.log("Locale changed successfully to",e)}catch(r){throw U.error("Failed to change locale:",r),r}}loadLocaleFromStorage(){try{return localStorage.getItem(xr.LOCALE_STORAGE_KEY)}catch(e){return U.warn("Failed to load locale from storage:",e),null}}saveLocaleToStorage(e){try{localStorage.setItem(xr.LOCALE_STORAGE_KEY,e)}catch(r){U.warn("Failed to save locale to storage:",r)}}async saveLocaleToDatabase(e){const r=localStorage.getItem("auth_token");if(!r){U.log("No auth token, skipping DB locale save");return}let a,i;if(this.config.localeApi)a=this.config.localeApi.endpoint,i=this.config.localeApi.method;else{const l=this.config.templateType==="admin";a=l?"/api/admin/users/me/language":"/api/user/profile/update-language",i=l?"PATCH":"POST"}try{const l=this.getXsrfToken(),c=await fetch(a,{method:i,headers:{"Content-Type":"application/json",Accept:"application/json",...l&&{"X-XSRF-TOKEN":l},Authorization:`Bearer ${r}`},credentials:"include",body:JSON.stringify({language:e})});c.ok?U.log("Locale saved to DB:",e):U.warn("Failed to save locale to DB (UI will still change):",c.statusText)}catch(l){U.warn("Failed to call locale API (UI will still change):",l)}}getXsrfToken(){if(typeof document>"u")return null;const r=`; ${document.cookie}`.split("; XSRF-TOKEN=");return r.length===2?decodeURIComponent(r.pop()?.split(";").shift()||""):null}loadCacheVersionFromStorage(){try{const e=localStorage.getItem(xr.CACHE_VERSION_STORAGE_KEY);return e?parseInt(e,10):null}catch(e){return U.warn("Failed to load cache version from storage:",e),null}}saveCacheVersionToStorage(e){try{localStorage.setItem(xr.CACHE_VERSION_STORAGE_KEY,String(e))}catch(r){U.warn("Failed to save cache version to storage:",r)}}reinitializeModuleHandlers(){if(typeof window>"u")return;const e="__",r=window;Object.keys(r).forEach(a=>{if(a.startsWith(e)&&typeof r[a]?.initModule=="function")try{r[a].initModule(),U.log(`Module handler re-initialized: ${a}`)}catch(i){U.warn(`Failed to re-initialize module handlers for ${a}:`,i)}})}reinitializeTemplateHandlers(){if(typeof window>"u")return;const e=window.G7TemplateHandlers;if(!e){U.warn("Template handlers not found on window.G7TemplateHandlers");return}const r=this.getActionDispatcher();if(!r){U.warn("ActionDispatcher not available for template handler registration");return}Object.entries(e).forEach(([a,i])=>{r.registerHandler(a,i)}),U.log(`${Object.keys(e).length} template handler(s) re-registered:`,Object.keys(e))}reinitializePluginHandlers(){if(typeof window>"u")return;const e="__",r=window;Object.keys(r).forEach(a=>{if(a.startsWith(e)&&typeof r[a]?.initPlugin=="function")try{r[a].initPlugin(),U.log(`Plugin handler re-initialized: ${a}`)}catch(i){U.warn(`Failed to re-initialize plugin handlers for ${a}:`,i)}})}getLocale(){return this.config.locale}getErrorPageHandler(){return this.errorPageHandler}getGlobalState(){return{...this.globalState}}setGlobalState(e,r){const a={...this.globalState};typeof e=="function"?this.globalState=e(this.globalState):this.globalState={...this.globalState,...e},U.log("Global state updated:",this.globalState),window.G7Core?.devTools?.captureStateSnapshot?.({source:"setGlobalState",prev:a,next:this.globalState}),this.globalStateListeners.forEach(l=>{l(this.globalState)}),r?.render!==!1&&Promise.resolve().then(()=>Pr).then(({updateTemplateData:l,getState:c})=>{const d=c();d.reactRoot&&d.currentLayoutJson&&l({_global:{...this.globalState},_local:this.globalState._local||{}})})}onGlobalStateChange(e){this.globalStateListeners.add(e)}offGlobalStateChange(e){this.globalStateListeners.delete(e)}async refetchDataSource(e,r){let a=this.currentDataSources.find(i=>i.id===e);if(!a)for(const[,i]of this.modalDataSources){const l=i.find(c=>c.id===e);if(l){a=l;break}}if(!a){U.warn(`Data source not found: ${e}`);return}U.log(`Refetching data source: ${e}`,r?.sync?"(sync mode)":"",r?.globalStateOverride?"(with global override)":"",r?.localStateOverride?"(with local override)":""),zr.setPending(!0);try{const i=new _r;this.currentGlobalHeaders.length>0&&i.setGlobalHeaders(this.currentGlobalHeaders);const l=ta(),c=l.currentDataContext?._global||{},d=r?.globalStateOverride?{...c,...r.globalStateOverride}:c,f=l.currentDataContext?._local||{},g=r?.localStateOverride?{...f,...r.localStateOverride}:f,y=(await i.fetchDataSourcesWithResults([a],this.currentRouteParams,this.currentQueryParams,d,g,{ignoreAutoFetch:!0}))[0];if(y.state==="success"&&y.data!==void 0){this.currentFetchedData[e]=y.data;const w={};this.processInitOptions([a],{[e]:y.data},w);const S=ta();if(S.bindingEngine){const A=[e];a.initGlobal&&A.push("_global"),a.initLocal&&A.push("_local"),S.bindingEngine.invalidateCacheByKeys(A)}const v={[e]:y.data};return Object.keys(w).length>0&&(v._localInit=w),a.initGlobal&&(v._global={...this.globalState}),ds(v,r?.sync?{sync:!0}:void 0),U.log(`Data source refetched successfully: ${e}`),y.data}else if(y.state==="error"){U.error(`Failed to refetch data source: ${e}`,y.error);return}}catch(i){U.error(`Error refetching data source: ${e}`,i);return}finally{zr.setPending(!1)}}registerModalDataSources(e,r){this.modalDataSources.set(e,r),U.log(`Modal data sources registered: ${e} (${r.length} sources)`)}unregisterModalDataSources(e){this.modalDataSources.delete(e),U.log(`Modal data sources unregistered: ${e}`)}getDataSource(e){return this.currentFetchedData[e]}setDataSource(e,r,a){if(!e){U.warn("setDataSource: dataSourceId is required");return}const{merge:i=!1,sync:l=!1}=a||{};i&&this.currentFetchedData[e]?this.currentFetchedData[e]={...this.currentFetchedData[e],...r}:this.currentFetchedData[e]=r,Promise.resolve().then(()=>Pr).then(({updateTemplateData:c})=>{c({[e]:this.currentFetchedData[e]},l?{sync:!0}:void 0)}),U.log(`setDataSource: Updated ${e}`,i?"(merged)":"(replaced)")}updateDataSourceItem(e,r,a,i,l){const{idField:c="id",merge:d=!0,skipRender:f=!1}=l||{},g=this.currentFetchedData[e];if(!g)return U.warn(`updateDataSourceItem: DataSource '${e}' not found`),!1;const m=this.parseItemPath(r);let y=g;for(const v of m)if(y=y?.[v],y===void 0)return U.warn(`updateDataSourceItem: Path '${r}' not found in dataSource`),!1;if(!Array.isArray(y))return U.warn(`updateDataSourceItem: Target at '${r}' is not an array`),!1;const w=y.findIndex(v=>String(v[c])===String(a));if(w===-1)return U.warn(`updateDataSourceItem: Item with ${c}='${a}' not found`),!1;d?y[w]=this.deepMerge(y[w],i):y[w]={...y[w],...i};const S=window.G7Core?.devTools;return S?.isEnabled?.()&&S.trackDataSourceUpdate?.({dataSourceId:e,updateType:"partial",itemPath:r,itemId:a,updates:i,timestamp:Date.now()}),f||Promise.resolve().then(()=>Pr).then(({updateTemplateData:v})=>{v({[e]:this.currentFetchedData[e]},{sync:!1})}),U.log(`updateDataSourceItem: Updated ${e}.${r}[${c}=${a}]`),!0}parseItemPath(e){const r=[],a=/([^\.\[\]]+)|\[(\d+)\]/g;let i;for(;(i=a.exec(e))!==null;)i[1]!==void 0?r.push(i[1]):i[2]!==void 0&&r.push(parseInt(i[2],10));return r}deepMerge(e,r){if(r==null)return e;if(typeof r!="object"||Array.isArray(r))return r;const a={...e};for(const i of Object.keys(r))typeof r[i]=="object"&&r[i]!==null&&!Array.isArray(r[i])?a[i]=this.deepMerge(a[i]||{},r[i]):a[i]=r[i];return a}setValueAtPath(e,r,a,i="deep"){const l=r.split(".");let c=e;for(let g=0;g<l.length-1;g++){const m=l[g];(c[m]===void 0||c[m]===null||typeof c[m]!="object")&&(c[m]={}),c=c[m]}const d=l[l.length-1],f=c[d];i==="replace"||f===void 0?c[d]=a:i==="shallow"?typeof f=="object"&&typeof a=="object"&&!Array.isArray(a)?c[d]={...f,...a}:c[d]=a:typeof f=="object"&&typeof a=="object"&&!Array.isArray(a)?c[d]=this.deepMerge(f,a):c[d]=a}flattenNestedObjectToMappings(e,r=""){const a=[];for(const[i,l]of Object.entries(e)){if(i==="_merge")continue;const c=r?`${r}.${i}`:i;typeof l=="string"?a.push({targetPath:c,sourcePath:l}):typeof l=="object"&&l!==null&&!Array.isArray(l)&&a.push(...this.flattenNestedObjectToMappings(l,c))}return a}async updateQueryParams(e,r){U.log("updateQueryParams:",e);const a=new URL(e,window.location.origin),i=new URLSearchParams(a.search);window.history.replaceState(null,"",e),U.log("URL updated via replaceState"),this.currentQueryParams=i,U.log("currentQueryParams updated:",Object.fromEntries(i.entries()));const l=new _r,c=ta().currentDataContext?._global||this.globalState||{},d={route:this.currentRouteParams||{},query:dy(this.currentQueryParams),_global:c},f=Array.isArray(this.currentRawDataSources)&&this.currentRawDataSources.length>0?l.filterByCondition(this.currentRawDataSources,d):this.currentDataSources;this.currentDataSources=f;const g=f.filter(A=>A.auto_fetch!==!1&&A.type!=="websocket");if(g.length===0){U.log("No auto_fetch data sources to refetch");return}U.log(`Refetching ${g.length} auto_fetch data sources`),zr.setPending(!0);const m=ta().currentLayoutJson,y=m?.transition_overlay,w=Array.isArray(y?.wait_for)?y.wait_for:[],S=g.some(A=>(A.loading_strategy||"progressive")==="blocking"),v=w.length>0&&g.some(A=>w.includes(A.id)&&A.type!=="websocket"&&(A.loading_strategy||"progressive")!=="background");if((S||v)&&y&&typeof y=="object"){const A=r?.transitionOverlayTarget||y.target;y.enabled&&y.style==="skeleton"&&y.skeleton?.component&&A?this.renderSkeletonOverlay(A,y.skeleton,m,y.fallback_target):y.enabled&&y.style==="spinner"&&A&&this.renderSpinnerOverlay(A,y.spinner,y.fallback_target)}try{const A=new _r;this.currentGlobalHeaders.length>0&&A.setGlobalHeaders(this.currentGlobalHeaders);const x=ta(),R=x.currentDataContext?._global||{},M=await A.fetchDataSourcesWithResults(g,this.currentRouteParams,this.currentQueryParams,R,void 0,{ignoreAutoFetch:!1}),O={},L={},q=new Map(g.map(G=>[G.id,G]));for(const G of M){const B=q.get(G.id);if(!B){U.warn(`Refetch result id not found in autoFetchDataSources: ${G.id}`);continue}const K=G.id;G.state==="success"&&G.data!==void 0?(this.currentFetchedData[K]=G.data,O[K]=G.data,this.processInitOptions([B],{[K]:G.data},L),U.log(`Data source refetched: ${K}`)):G.state==="error"&&U.error(`Failed to refetch data source: ${K}`,G.error)}if(x.bindingEngine){const G=Object.keys(O);G.push("query"),x.bindingEngine.invalidateCacheByKeys(G)}if(Object.keys(L).length>0&&(O._localInit=L),Object.keys(O).length>0){if(O.query=dy(this.currentQueryParams),x.currentLayoutJson?.computed&&Object.keys(x.currentLayoutJson.computed).length>0){const G={...x.currentDataContext,...O,query:O.query},B=this.calculateComputed(x.currentLayoutJson.computed,G);Object.keys(B).length>0&&(O._computed=B,this.globalState._computed=B,U.log("Computed values recalculated in updateQueryParams:",Object.keys(B)))}ds(O,{sync:!0}),U.log("Template data updated with new query params")}}catch(A){U.error("Error in updateQueryParams:",A)}finally{zr.setPending(!1),this.hideTransitionOverlay()}}calculateComputed(e,r){const a={},i=new _n;for(const[l,c]of Object.entries(e))try{let d;if(this.isComputedSwitchDefinition(c))d=i.resolveSwitch(c,r,{skipCache:!0});else if(typeof c=="string")if(c.startsWith("{{")&&c.endsWith("}}")){const f=c.slice(2,-2).trim();d=this.evaluateComputedExpression(f,r)}else d=c;a[l]=d,U.log(`Computed ${l}:`,d)}catch(d){U.warn(`Failed to calculate computed value: ${l}`,d),a[l]=void 0}return a}isComputedSwitchDefinition(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"$switch"in e&&"$cases"in e}evaluateComputedExpression(e,r){try{return new Function("ctx",` - with(ctx) { - try { - return ${e}; - } catch (e) { - return undefined; - } - } - `)(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-)/,display:/^(block|inline-block|inline|flex|inline-flex|table|inline-table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row-group|table-row|flow-root|grid|inline-grid|contents|list-item|hidden)$/,position:/^(static|fixed|absolute|relative|sticky)$/,inset:/^(inset|top|right|bottom|left)-/,zIndex:/^z-/,width:/^w-/,minWidth:/^min-w-/,maxWidth:/^max-w-/,height:/^h-/,minHeight:/^min-h-/,maxHeight:/^max-h-/,padding:/^p[xytblr]?-/,margin:/^-?m[xytblr]?-/,space:/^space-(x|y)-/,fontSize:/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,fontWeight:/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,fontStyle:/^(italic|not-italic)$/,fontFamily:/^font-(sans|serif|mono)/,textAlign:/^text-(left|center|right|justify|start|end)$/,textColor:/^text-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,textDecoration:/^(underline|overline|line-through|no-underline)$/,textTransform:/^(uppercase|lowercase|capitalize|normal-case)$/,lineHeight:/^leading-/,letterSpacing:/^tracking-/,textOverflow:/^(truncate|text-ellipsis|text-clip)$/,whitespace:/^whitespace-/,wordBreak:/^break-/,bgColor:/^bg-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,bgGradient:/^bg-gradient-/,bgSize:/^bg-(auto|cover|contain)$/,bgPosition:/^bg-(bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top)$/,bgRepeat:/^bg-(repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/,bgAttachment:/^bg-(fixed|local|scroll)$/,bgClip:/^bg-clip-/,bgOrigin:/^bg-origin-/,borderWidth:/^border(-[xytblr])?(-0|-2|-4|-8)?$/,borderColor:/^border-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,borderStyle:/^border-(solid|dashed|dotted|double|hidden|none)$/,borderRadius:/^rounded(-[tblrse]{1,2})?(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/,ringWidth:/^ring(-0|-1|-2|-4|-8|-inset)?$/,ringColor:/^ring-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,ringOffset:/^ring-offset-/,shadow:/^shadow(-sm|-md|-lg|-xl|-2xl|-inner|-none)?$/,opacity:/^opacity-/,mixBlend:/^mix-blend-/,bgBlend:/^bg-blend-/,blur:/^blur(-none|-sm|-md|-lg|-xl|-2xl|-3xl)?$/,brightness:/^brightness-/,contrast:/^contrast-/,grayscale:/^grayscale(-0)?$/,hueRotate:/^-?hue-rotate-/,invert:/^invert(-0)?$/,saturate:/^saturate-/,sepia:/^sepia(-0)?$/,backdropBlur:/^backdrop-blur-/,backdropBrightness:/^backdrop-brightness-/,backdropContrast:/^backdrop-contrast-/,backdropGrayscale:/^backdrop-grayscale-/,backdropHueRotate:/^backdrop-hue-rotate-/,backdropInvert:/^backdrop-invert-/,backdropOpacity:/^backdrop-opacity-/,backdropSaturate:/^backdrop-saturate-/,backdropSepia:/^backdrop-sepia-/,scale:/^scale(-x|-y)?-/,rotate:/^-?rotate-/,translate:/^-?translate-[xy]-/,skew:/^-?skew-[xy]-/,transformOrigin:/^origin-/,transition:/^transition(-none|-all|-colors|-opacity|-shadow|-transform)?$/,duration:/^duration-/,ease:/^ease-(linear|in|out|in-out)$/,delay:/^delay-/,animate:/^animate-/,cursor:/^cursor-/,userSelect:/^select-/,pointerEvents:/^pointer-events-/,resize:/^resize(-none|-x|-y)?$/,scrollBehavior:/^scroll-(auto|smooth)$/,touchAction:/^touch-/,overflow:/^overflow(-x|-y)?-(auto|hidden|clip|visible|scroll)$/,overscroll:/^overscroll(-x|-y)?-(auto|contain|none)$/,visibility:/^(visible|invisible|collapse)$/,aspectRatio:/^aspect-/,columns:/^columns-/,breakAfter:/^break-after-/,breakBefore:/^break-before-/,breakInside:/^break-inside-/,boxDecorationBreak:/^box-decoration-/,boxSizing:/^box-(border|content)$/,float:/^float-(right|left|none)$/,clear:/^clear-(left|right|both|none)$/,isolation:/^(isolate|isolation-auto)$/,objectFit:/^object-(contain|cover|fill|none|scale-down)$/,objectPosition:/^object-/};function fy(o){const e=o.replace(/^(dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+/,"");for(const[r,a]of Object.entries(z_))if(a.test(e))return r;return null}function hy(o){const e=o.match(/^((dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+)/);return e?e[1]:""}function U_(o,e){if(!e||e.trim()==="")return o;if(!o||o.trim()==="")return e;const r=o.split(/\s+/).filter(Boolean),a=e.split(/\s+/).filter(Boolean),i=new Map;for(const c of a){const d=fy(c);if(d){const f=hy(c),g=`${d}:${f}`;i.set(g,c)}}return[...r.filter(c=>{const d=fy(c);if(!d)return!0;const f=hy(c),g=`${d}:${f}`;return!i.has(g)}),...a].join(" ")}function B_(o){return Object.entries(o).filter(([,e])=>e).map(([e])=>e).join(" ")}function P_(...o){return o.filter(e=>typeof e=="string"&&e.length>0).join(" ")}const be=ut("G7CoreGlobals");function q_(){window.React=We,window.ReactDOM={...ad,createPortal:Oa.createPortal,unstable_batchedUpdates:o=>o()},window.ReactJSXRuntime=j0,be.log("전역 객체 window.React, window.ReactDOM, window.ReactJSXRuntime에 노출됨")}function G_(o){const e=new Map,r=Ma.getInstance();o.componentEvent={on:(a,i)=>(e.has(a)||e.set(a,new Set),e.get(a).add(i),r.trackEventSubscribe(a),()=>{e.get(a)?.delete(i),r.trackEventUnsubscribe(a)}),emit:async(a,i)=>{const l=e.get(a),c=l?.size??0;if(!l||l.size===0)return r.trackEventEmit(a,i,0),[];let d,f=[];try{f=await Promise.all(Array.from(l).map(async g=>{try{return await g(i)}catch(m){throw be.error(`componentEvent: Error in listener for "${a}":`,m),m}}))}catch(g){d=g instanceof Error?g:new Error(String(g))}if(r.trackEventEmit(a,i,c,f,d),d)throw d;return f},off:a=>{e.delete(a),r.trackEventOff(a)},clear:()=>{e.clear(),r.trackEventClear()}},be.log("전역 객체 window.G7Core.componentEvent에 노출됨")}function V_(o){o.__runtime||(o.__runtime={DynamicRenderer:Br,ComponentRegistry:mr,TranslationEngine:gi,DataSourceManager:_r,dataSourceManager:__,DataBindingEngine:_n,dataBindingEngine:pd,ActionDispatcher:mo,TranslationReactContext:lf,TranslationProvider:_c,useTranslation:Xm,ResponsiveContext:jd,ResponsiveProvider:uc,useResponsive:zd,responsiveManager:mi,BREAKPOINT_PRESETS:Vl,AuthManager:br,createLogger:ut,G7DevToolsCore:Ma},be.log("전역 객체 window.G7Core.__runtime(코어 런타임 표면)에 노출됨"))}function F_(o){if(o.layoutEditor&&o.layoutEditor.__isStub!==!0||o.layoutEditor&&o.layoutEditor.__isStub===!0)return;const e=[],r=[];o.layoutEditor={__isStub:!0,__queue:e,__readyCallbacks:r,registerWidget:(a,i)=>e.push(["widget",a,i]),registerNodeEditor:(a,i)=>e.push(["nodeEditor",a,i]),registerCanvasOverlay:(a,i)=>e.push(["canvasOverlay",a,i]),onReady:a=>{typeof a=="function"&&r.push(a)}},be.log("전역 객체 window.G7Core.layoutEditor 예약 접수함(stub) 노출됨")}function K_(o,e){o.useTranslation=Xm,o.t=(r,a)=>{const i=e.getState();if(!i.translationEngine||!i.translationContext)return r;if(a){const l="|"+Object.entries(a).map(([c,d])=>`${c}=${d}`).join("|");return i.translationEngine.translate(r,i.translationContext,l)}return i.translationEngine.translate(r,i.translationContext)},be.log("전역 객체 window.G7Core.useTranslation에 노출됨")}function W_(o,e){o.createChangeEvent=N_,o.createClickEvent=I_,o.createSubmitEvent=H_,o.createKeyboardEvent=j_,o.uuid=()=>typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,r=>{const a=Math.random()*16|0;return(r==="x"?a:a&3|8).toString(16)}),o.createLogger=ut,be.log("전역 객체 window.G7Core.createLogger에 노출됨"),o.renderItemChildren=(r,a,i,l,c)=>{const d=e.getState(),m={_global:window.__templateApp?.getGlobalState?.()||{},_local:a._local||{},_computed:a._computed||{},...a},y=new Set,w=(v,A)=>{const x=m._global?._remountKeys;return v&&x?.[v]&&!y.has(v)?(y.add(v),`${v}-remount-${x[v]}`):v||A},S={translationContext:d.translationContext,translationEngine:d.translationEngine,bindingEngine:d.bindingEngine,actionDispatcher:d.actionDispatcher,getRemountKey:w,...c};return oo(r,m,i,l,S)},o.evaluateCondition=(r,a)=>{if(!r)return!0;const i=e.getState(),c=window.__templateApp?.getGlobalState?.()||{},d={_global:c,_local:c._local||{},_computed:c._computed||{},...a||{}},f=i.bindingEngine??pd;return ip(r,d,f)},o.getComponentMap=()=>mr.getInstance().getComponentMap(),o.renderComponentLayout=(r,a,i)=>{if(!r||!Array.isArray(r)||r.length===0)return null;const l=mr.getInstance().getComponentMap(),c=e.getState(),f=window.__templateApp?.getGlobalState?.()||{},g={_global:f._global||{},_local:f._local||{},_computed:f._computed||{},...a},m=new Set,y=(w,S)=>{const v=g._global?._remountKeys;return w&&v?.[w]&&!m.has(w)?(m.add(w),`${w}-remount-${v[w]}`):w||S};return oo(r,g,l,i,{translationContext:c.translationContext,translationEngine:c.translationEngine,bindingEngine:c.bindingEngine,actionDispatcher:c.actionDispatcher,getRemountKey:y})},o.renderExpandContent=r=>{const{children:a,row:i,expandContext:l,componentContext:c,keyPrefix:d}=r;if(!a||!Array.isArray(a)||a.length===0)return null;const f=r.componentMap||mr.getInstance().getComponentMap(),g=e.getState(),m=o.state?.get?.()||{},y=c?.stateRef?.current??c?.state??{},w={...m._local||{},...y},S=c?.computedRef?.current??m._computed??{},v=g.bindingEngine??o.getDataBindingEngine?.(),A={...m,_local:w,_computed:S,row:i,item:i,$item:i};let x={};if(l&&typeof l=="object"&&v)try{for(const[L,q]of Object.entries(l))if(typeof q=="string"){const G=Ta(q);if(G!==null)try{x[L]=Gn(G)?v.evaluatePipeExpression(G,A,{skipCache:!0}):v.evaluateExpression(G,A,{skipCache:!0})}catch{x[L]=void 0}else x[L]=v.resolveBindings(q,A,{skipCache:!0})}else x[L]=q}catch(L){be.warn("renderExpandContent: expandContext 평가 오류:",L)}const R={row:i,item:i,$item:i,_local:w,_global:m._global||{},_computed:m._computed||{},...x},M=new Set,O=(L,q)=>{const G=R._global?._remountKeys;return L&&G?.[L]&&!M.has(L)?(M.add(L),`${L}-remount-${G[L]}`):L||q};return oo(a,R,f,d,{translationContext:g.translationContext,translationEngine:g.translationEngine??void 0,bindingEngine:g.bindingEngine??void 0,actionDispatcher:g.actionDispatcher,componentContext:c,getRemountKey:O})},o.$get=function(a,i,l=void 0){if(a==null)return l;const c=Array.isArray(i)?i:[i];if(c.length===0)return a;let d=a;for(const f of c){if(d==null||f==null)return l;d=d[f]}return d??l},be.log("전역 객체 window.G7Core.renderItemChildren에 노출됨"),be.log("전역 객체 window.G7Core.renderComponentLayout에 노출됨"),be.log("전역 객체 window.G7Core.renderExpandContent에 노출됨"),be.log("전역 객체 window.G7Core.$get에 노출됨")}function Y_(o){o.dispatch=async(e,r)=>{const a=window.__templateApp;if(!a)return be.warn("G7Core.dispatch: TemplateApp이 초기화되지 않았습니다."),{success:!1,error:new Error("TemplateApp이 초기화되지 않았습니다.")};const i=a.getActionDispatcher?.();if(!i)return be.warn("G7Core.dispatch: ActionDispatcher를 찾을 수 없습니다."),{success:!1,error:new Error("ActionDispatcher를 찾을 수 없습니다.")};const l=a.getRouter?.(),c=a.getGlobalState?.(),d=a.setGlobalState?.bind(a),g=r?.componentContext||window.__g7ActionContext,m=window.__g7PendingLocalState;let y=g?.state??c;m&&g&&(y=m,be.log("[dispatch] Using __g7PendingLocalState for context.state:",m));let w=g?.data??c;m&&g?.data?._local&&(w={...g.data,_local:m},be.log("[dispatch] Updated context.data._local with pendingLocalState"));const S={navigate:l?(v,A)=>l.navigate(v,A):void 0,setState:g?.setState??d,state:y,data:w,_isDispatchFallbackContext:!g?.setState};if(e.debounce){const v=e.debounceKey||`dispatch-${e.handler}`,A=typeof e.debounce=="number"?e.debounce:e.debounce.delay;return i.debouncedCall(v,A,()=>{const{debounce:x,debounceKey:R,...M}=e;i.dispatchAction(M,S)}),{success:!0,debounced:!0}}try{return await i.dispatchAction(e,S)}catch(v){return be.error("G7Core.dispatch: 액션 실행 오류:",v),{success:!1,error:v instanceof Error?v:new Error(String(v))}}},be.log("전역 객체 window.G7Core.dispatch에 노출됨")}function ko(o){const e={};for(const[r,a]of Object.entries(o))if(r.includes(".")){const i=r.split(".");let l=e;for(let c=0;c<i.length-1;c++){const d=i[c];d in l?(typeof l[d]!="object"||l[d]===null)&&(l[d]={}):l[d]={},l=l[d]}l[i[i.length-1]]=a}else e[r]=a;return e}function gy(o,e=""){const r=[];if(!o||typeof o!="object")return r;for(const a of Object.keys(o)){const i=e?`${e}.${a}`:a,l=o[a];l&&typeof l=="object"&&!Array.isArray(l)?r.push(...gy(l,i)):r.push(i)}return r}function X_(o){const e=Object.keys(o);return e.length>0&&e.every(r=>/^\d+$/.test(r))}function py(o,e){const r={...o};for(const a of Object.keys(e))a in r?r[a]!==null&&typeof r[a]=="object"&&!Array.isArray(r[a])&&e[a]!==null&&typeof e[a]=="object"&&!Array.isArray(e[a])&&(r[a]=py(r[a],e[a])):r[a]=e[a];return r}function ir(o,e){if(Array.isArray(o)&&!Array.isArray(e)&&X_(e)){const a=Object.keys(e).map(l=>parseInt(l,10));if(!((a.length>0?Math.max(...a):0)>=o.length+a.length+10)){const l=[...o];for(const[c,d]of Object.entries(e)){const f=parseInt(c,10);f>=0&&f<l.length?d!==null&&typeof d=="object"&&!Array.isArray(d)&&l[f]!==null&&typeof l[f]=="object"?l[f]=ir(l[f],d):l[f]=d:f>=l.length&&(l[f]=d)}return l}}const r={...o};for(const[a,i]of Object.entries(e))i!==null&&typeof i=="object"&&!Array.isArray(i)?r[a]!==null&&typeof r[a]=="object"?r[a]=ir(r[a],i):r[a]={...i}:r[a]=i;return r}function J_(o){o.state={get:()=>window.__templateApp?.getGlobalState?.()||{},set:(e,r)=>{const a=window.__templateApp;if(a?.setGlobalState&&a?.getGlobalState){const i=r?.merge||"deep",l=ko(e);let c;if(i==="replace")c=l;else if(i==="shallow")c={...a.getGlobalState(),...l};else{const d=a.getGlobalState();c=ir(d,l)}a.setGlobalState(c,{render:r?.render})}else be.warn("G7Core.state.set: TemplateApp이 초기화되지 않았습니다.")},setLocal:(e,r)=>{const a=window.__templateApp;if(r?.render===!1&&!r?.selfManaged){const x=window.__g7AutoBindingPaths;x&&x.size>0&&gy(ko(e)).some(M=>x.has(M))&&(be.log("[setLocal] render:false + 자동바인딩 경로 겹침 감지 → render:true 자동 승격 (engine-v1.43.0)"),r={...r,render:!0})}if(r?.debounce){const x=a?.getActionDispatcher?.();if(x){const R=r.debounceKey||`setLocal-${Object.keys(e).join(",")}`,{debounce:M,debounceKey:O,...L}=r;x.debouncedCall(R,r.debounce,()=>{o.state.setLocal(e,L)});return}}const i=r?.scope??"current",l=r?.merge||"deep",c=window.__g7ActionContext,d=ko(e);if(i==="parent"||i==="root"){const x=window.__g7LayoutContextStack||[];if(x.length>0){const R=i==="parent"?x[x.length-1]:x[0];if(R?.setState){R.setState(M=>l==="replace"?d:l==="shallow"?{...M||{},...d}:ir(M||{},d)),be.log(`[setLocal] scope=${i}: 타겟 컨텍스트에 상태 업데이트 (mergeMode=${l})`,d);return}}be.warn(`[setLocal] scope=${i}: 레이아웃 컨텍스트 스택이 비어있습니다. current로 폴백합니다.`)}const f=window.__g7PendingLocalState,g=a?.getGlobalState?.()?._local||{},w=!((window.__g7LayoutContextStack||[]).length>0)&&c?.state?c.state:void 0,S=w?py(g,w):g,v=f||S;let A;if(l==="replace"?A=d:l==="shallow"?A={...v,...d}:A=ir(v,d),a?.setGlobalState&&(a.setGlobalState({_local:A},{render:r?.render}),be.log(`[setLocal] globalLocal updated via setGlobalState (mergeMode=${l}, render=${r?.render??!0}):`,d)),window.__g7PendingLocalState=A,be.log("[setLocal] __g7PendingLocalState updated:",A),window.__g7SequenceLocalSync=A,window.__g7LastSetLocalSnapshot=A,l==="replace")window.__g7ForcedLocalFields=d;else{const x=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=ir(x,d)}if(be.log("[setLocal] __g7ForcedLocalFields updated:",d),l==="replace")window.__g7SetLocalOverrideKeys=d;else{const x=window.__g7SetLocalOverrideKeys||{};window.__g7SetLocalOverrideKeys=ir(x,d)}be.log("[setLocal] __g7SetLocalOverrideKeys updated:",d),c?.setState&&c.setState(x=>l==="replace"?d:l==="shallow"?{...x||{},...d}:ir(x||{},d)),a?.setGlobalState||be.warn("G7Core.state.setLocal: TemplateApp이 없습니다.")},getLocal:()=>{const a=(window.__templateApp?.getGlobalState?.()||{})._local||{},i=window.__g7PendingLocalState;if(i)return ir(a,i);const l=window.__g7LastSetLocalSnapshot;return l&&l!==a?ir(a,l):a},update:e=>{const r=window.__templateApp;if(r?.getGlobalState&&r?.setGlobalState){const a=r.getGlobalState(),i=e(a);r.setGlobalState(i)}else be.warn("G7Core.state.update: TemplateApp이 초기화되지 않았습니다.")},subscribe:e=>{const r=window.__templateApp;return r?.onGlobalStateChange?r.onGlobalStateChange(e):(be.warn("G7Core.state.subscribe: TemplateApp이 초기화되지 않았습니다."),()=>{})},getDataSource:e=>window.__templateApp?.getDataSource?.(e),getIsolated:e=>e?window.__g7IsolatedStates?.[e]?.state??null:window.__g7ActionContext?.isolatedContext?.state??null,setIsolated:(e,r,a)=>{let i,l,c;typeof e=="string"?(i=e,l=r||{},c=a):(l=e,c=r&&typeof r=="object"&&"merge"in r?r:a);const d=c?.merge||"deep",f=ko(l);if(i){const y=window.__g7IsolatedStates?.[i];y?.mergeState?y.mergeState(f,d):be.warn(`G7Core.state.setIsolated: scopeId '${i}'를 찾을 수 없습니다.`);return}const g=window.__g7ActionContext;g?.isolatedContext?.mergeState?g.isolatedContext.mergeState(f,d):be.warn("G7Core.state.setIsolated: 액션 컨텍스트에 isolatedContext가 없습니다.")},getParent:()=>{const e=window.__g7LayoutContextStack||[];if(e.length===0)return be.log("[getParent] 레이아웃 컨텍스트 스택이 비어있습니다."),null;const r=e[e.length-1];if(!r)return null;const a=r.dataContext||{};return{_local:r.state||a._local||{},_global:a._global||o.state.get()||{},setState:r.setState}},setParentLocal:(e,r,a)=>{const i=window.__g7LayoutContextStack||[];if(i.length===0){be.warn("[setParentLocal] 레이아웃 컨텍스트 스택이 비어있습니다.");return}const l=i[i.length-1];if(!l?.setState){be.warn("[setParentLocal] 부모 컨텍스트에 setState가 없습니다.");return}let c,d;typeof e=="string"?(c={[e]:r},d=a):(c=e,d=r&&typeof r=="object"&&"merge"in r?r:a);const f=d?.merge||"deep",g=ko(c),m=l.state?._local!==void 0,y=m?l.state._local:l.state||{};let w;f==="replace"?w=g:f==="shallow"?w={...y,...g}:w=ir(y,g),window.__g7PendingLocalState=w;const S=window.__templateApp;if(S?.setGlobalState&&S.setGlobalState({_local:w}),l.setState(w),m?l.state._local=w:l.state=w,l.dataContext&&(l.dataContext._local=w),f==="replace")window.__g7ForcedLocalFields=g;else{const v=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=ir(v,g)}Nd()},setParentGlobal:(e,r)=>{let a;typeof e=="string"?a={[e]:r}:a=e,o.state.set(a),be.log("[setParentGlobal] 전역 상태 업데이트:",a)}},window.__g7IsolatedStates||(window.__g7IsolatedStates={}),o.state.getGlobal=o.state.get,o.state.setGlobal=o.state.set,be.log("전역 객체 window.G7Core.state에 노출됨")}function Q_(o){o.dataSource={get:e=>window.__templateApp?.getDataSource?.(e),set:(e,r,a)=>{const i=window.__templateApp;i?.setDataSource?i.setDataSource(e,r,a):be.warn("G7Core.dataSource.set: TemplateApp이 초기화되지 않았습니다.")},refetch:async(e,r)=>{const a=window.__templateApp;if(a?.refetchDataSource)return a.refetchDataSource(e,r);be.warn("G7Core.dataSource.refetch: TemplateApp이 초기화되지 않았습니다.")},updateItem:(e,r,a,i,l)=>{const c=window.__templateApp;return c?.updateDataSourceItem?c.updateDataSourceItem(e,r,a,i,l):(be.warn("G7Core.dataSource.updateItem: TemplateApp이 초기화되지 않았습니다."),!1)},updateData:(e,r,a,i="append")=>{const l=window.__templateApp;if(!l?.getDataSource||!l?.setDataSource)return be.warn("G7Core.dataSource.updateData: TemplateApp이 초기화되지 않았습니다."),!1;const c=l.getDataSource(e);if(!c)return be.warn(`G7Core.dataSource.updateData: 데이터 소스 '${e}'를 찾을 수 없습니다.`),!1;let d,f=c,g=null;if(r){const y=r.split(".");g=y.pop();for(const w of y)if(f&&typeof f=="object"&&w in f)f=f[w];else return be.warn(`G7Core.dataSource.updateData: 경로 '${r}'를 찾을 수 없습니다.`),!1;d=f[g]}else d=c;if(!Array.isArray(d))return be.warn("G7Core.dataSource.updateData: 대상 경로의 데이터가 배열이 아닙니다."),!1;if(!Array.isArray(a))return be.warn("G7Core.dataSource.updateData: newData가 배열이 아닙니다."),!1;const m=i==="prepend"?[...a,...d]:[...d,...a];return r&&g?(f[g]=m,l.setDataSource(e,c,{merge:!1})):l.setDataSource(e,m,{merge:!1}),be.log(`G7Core.dataSource.updateData: '${e}'에 ${a.length}개 항목 ${i==="prepend"?"앞에":"뒤에"} 추가됨`),!0}},be.log("전역 객체 window.G7Core.dataSource에 노출됨")}function Z_(o,e){o.locale={current:()=>window.__templateApp?.getLocale?.()||"ko",supported:()=>{const a=window.__templateApp?.globalState?.appConfig?.supportedLocales;return Array.isArray(a)&&a.length>0?a:e.getState().templateMetadata?.locales||["ko","en"]},change:async r=>{const a=window.__templateApp;a?.changeLocale?await a.changeLocale(r):be.warn("G7Core.locale.change: TemplateApp이 초기화되지 않았습니다.")}},be.log("전역 객체 window.G7Core.locale에 노출됨")}function eA(o){o.toast={show:(e,r)=>{o.dispatch({handler:"toast",params:{message:e,type:r?.type||"info",...r?.duration&&{duration:r.duration}}})},success:(e,r)=>{o.toast.show(e,{type:"success",duration:r})},error:(e,r)=>{o.toast.show(e,{type:"error",duration:r})},warning:(e,r)=>{o.toast.show(e,{type:"warning",duration:r})},info:(e,r)=>{o.toast.show(e,{type:"info",duration:r})}},be.log("전역 객체 window.G7Core.toast에 노출됨")}function tA(o){o.modal={open:e=>{o.dispatch({handler:"openModal",target:e})},close:e=>{o.dispatch({handler:"closeModal",...e&&{target:e}})},closeAll:()=>{o.dispatch({handler:"closeAllModals"})},isOpen:e=>{const r=o.state.get();return r._global?.activeModal===e||r._global?.modalStack?.includes(e)||!1},getStack:()=>o.state.get()._global?.modalStack||[]},be.log("전역 객체 window.G7Core.modal에 노출됨")}function nA(o){o.style={mergeClasses:U_,conditionalClass:B_,joinClasses:P_},be.log("전역 객체 window.G7Core.style에 노출됨")}function rA(o,e){const{webSocketManager:r}=e;o.websocket={manager:r,subscribe:(a,i,l,c)=>r.subscribe(a,i,l,c),unsubscribe:a=>r.unsubscribe(a),leaveChannel:a=>r.leaveChannel(a),disconnect:()=>r.disconnect(),isInitialized:()=>r.isInitialized(),getSubscriptionCount:()=>r.getSubscriptionCount()},be.log("전역 객체 window.G7Core.websocket에 노출됨")}function aA(o,e){const{transitionManager:r}=e;o.navigation={isPending:()=>r?.getIsPending?.()||!1,onComplete:a=>{if(!r)return a(),()=>{};let i=r.getIsPending();const l=r.subscribe(c=>{c?i=!0:i&&(a(),l())});return l}},be.log("전역 객체 window.G7Core.navigation에 노출됨")}function iA(o){o.plugin={getSettings:e=>window.G7Config?.plugins?.[e],get:(e,r,a)=>window.G7Config?.plugins?.[e]?.[r]??a,getAll:()=>window.G7Config?.plugins??{}},be.log("전역 객체 window.G7Core.plugin에 노출됨")}function sA(o){o.module={getSettings:e=>window.G7Config?.modules?.[e],get:(e,r,a)=>window.G7Config?.modules?.[e]?.[r]??a,getAll:()=>window.G7Config?.modules??{}},be.log("전역 객체 window.G7Core.module에 노출됨")}function oA(o,e){const{transitionManager:r,responsiveManager:a}=e;o.AuthManager=br,o.api=jr(),o.TransitionManager=r,o.useTransitionState=om,o.ResponsiveManager=a,o.useResponsive=zd,o.useControllableState=J0,o.shallowArrayEqual=Q0,o.shallowObjectEqual=cm,o.updateQueryParams=async(i,l)=>{const c=window.__templateApp;if(c?.updateQueryParams)return c.updateQueryParams(i,l);be.warn("G7Core.updateQueryParams: TemplateApp이 초기화되지 않았습니다.")},be.log("전역 객체 window.G7Core.AuthManager에 노출됨"),be.log("전역 객체 window.G7Core.api에 노출됨"),be.log("전역 객체 window.G7Core.ResponsiveManager에 노출됨"),be.log("전역 객체 window.G7Core.useResponsive에 노출됨"),be.log("전역 객체 window.G7Core.useControllableState에 노출됨"),be.log("전역 객체 window.G7Core.updateQueryParams에 노출됨")}function lA(o,e){o.getSlotContext=()=>window.__slotContextValue??null,o.getDynamicRenderer=()=>window.__DynamicRenderer??null,o.getComponentRegistry=()=>e.getComponentRegistry?.()??mr.getInstance(),o.getDataBindingEngine=()=>e.getDataBindingEngine?.()??e.getState().bindingEngine,o.getTranslationEngine=()=>e.getTranslationEngine?.()??e.getState().translationEngine,o.getActionDispatcher=()=>e.getActionDispatcher?.()??e.getState().actionDispatcher,be.log("전역 객체 window.G7Core 슬롯 API에 노출됨 (getSlotContext, getDynamicRenderer 등)")}function cA(o){o.identity={setLauncher:mt.setLauncher.bind(mt),handle:mt.handle.bind(mt),isIdentityRequired:mt.isIdentityRequired.bind(mt),hasLauncher:mt.hasLauncher.bind(mt),markDomainNoticeShown:mt.markDomainNoticeShown.bind(mt),redirectExternally:mt.redirectExternally.bind(mt),createDeferred:mt.createDeferred.bind(mt),resolveDeferred:mt.resolveDeferred.bind(mt),reset:mt.reset.bind(mt),redirectStashKey:Rd},be.log("전역 객체 window.G7Core 본인인증(IDV) API에 노출됨 (identity)")}function uA(o){if(typeof window>"u")return;q_(),window.G7Core||(window.G7Core={});const e=window.G7Core;V_(e),G_(e),F_(e),oA(e,o),K_(e,o),W_(e,o),Y_(e),J_(e),Q_(e),Z_(e,o),eA(e),tA(e),nA(e),rA(e,o),aA(e,o),iA(e),sA(e),lA(e,o),cA(e),be.log("G7Core 전역 객체 초기화 완료")}function dA(){const o=window.G7Core;if(!o){be.warn("G7Core가 초기화되지 않았습니다.");return}const e=()=>Ma.getInstance(),r={isEnabled:()=>{try{return e().isEnabled()}catch{return!1}},trackRender:a=>{try{e().isEnabled()&&e().trackRender(a)}catch{}},trackIteration:(a,i,l,c,d)=>{try{e().isEnabled()&&e().trackIteration(a,{source:i,itemVar:l,indexVar:c,sourceLength:d})}catch{}},trackIfCondition:(a,i,l,c)=>{try{e().isEnabled()&&e().trackIfCondition(a,i,l)}catch{}},trackMount:(a,i)=>{try{e().isEnabled()&&e().trackMount(a,{...i,id:a})}catch{}},trackUnmount:a=>{try{e().isEnabled()&&e().trackUnmount(a)}catch{}},trackExpressionEval:a=>{try{e().isEnabled()&&e().trackExpressionEval(a)}catch{}},trackBindingEval:a=>{try{e().isEnabled()&&e().trackBindingEval(a)}catch{}},recordCacheHit:()=>{try{e().isEnabled()&&e().recordCacheHit()}catch{}},recordCacheMiss:()=>{try{e().isEnabled()&&e().recordCacheMiss()}catch{}},trackHandlerRegistration:(a,i,l,c)=>{try{e().isEnabled()&&e().trackHandlerRegistration(a,i,l,c)}catch{}},trackHandlerUnregistration:a=>{try{e().isEnabled()&&e().trackHandlerUnregistration(a)}catch{}},logAction:a=>{try{e().isEnabled()&&e().logAction(a)}catch{}},trackRequest:(a,i)=>{try{return e().isEnabled()?e().trackRequest(a,i):""}catch{return""}},completeRequest:(a,i,l)=>{try{e().isEnabled()&&e().completeRequest(a,i,l)}catch{}},failRequest:(a,i)=>{try{e().isEnabled()&&e().failRequest(a,i)}catch{}},trackDataSourceDefinition:a=>{try{e().isEnabled()&&e().trackDataSourceDefinition(a)}catch{}},trackDataSourceLoading:a=>{try{e().isEnabled()&&e().trackDataSourceLoading(a)}catch{}},trackDataSourceLoaded:(a,i,l)=>{try{e().isEnabled()&&e().trackDataSourceLoaded(a,i,l)}catch{}},trackDataSourceError:(a,i)=>{try{e().isEnabled()&&e().trackDataSourceError(a,i)}catch{}},trackForm:(a,i,l)=>{try{e().isEnabled()&&e().trackForm(a,i,l)}catch{}},untrackForm:a=>{try{e().isEnabled()&&e().untrackForm(a)}catch{}},startStateChange:(a,i,l,c)=>{try{return e().isEnabled()?e().startStateChange(a,i,l,c):""}catch{return""}},completeStateChange:a=>{try{e().isEnabled()&&e().completeStateChange(a)}catch{}},trackComponentRender:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().trackComponentRender(a,i,l,c,d,f)}catch{}},updateLocalState:a=>{try{e().isEnabled()&&e().updateLocalState(a)}catch{}},updateComputedState:a=>{try{e().isEnabled()&&e().updateComputedState(a)}catch{}},updateParentContext:a=>{try{e().isEnabled()&&e().updateParentContext(a)}catch{}},trackComponentStateSource:(a,i,l,c)=>{try{e().isEnabled()&&e().trackComponentStateSource(a,i,l,c)}catch{}},trackDynamicState:(a,i)=>{try{e().isEnabled()&&e().trackDynamicState(a,i)}catch{}},trackContextFlow:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().trackContextFlow(a,i,l,c,d,f)}catch{}},trackComponentStyle:(a,i,l,c)=>{try{e().isEnabled()&&e().trackComponentStyle(a,i,l,c)}catch{}},trackAuthEvent:(a,i,l,c)=>{try{e().isEnabled()&&e().trackAuthEvent(a,i,l,c)}catch{}},trackAuthHeader:(a,i,l,c,d)=>{try{e().isEnabled()&&e().trackAuthHeader(a,i,l,c,d)}catch{}},trackLog:(a,i,l)=>{try{e().isEnabled()&&e().trackLog(a,i,l)}catch{}},trackAction:a=>{try{e().isEnabled()&&e().trackAction?.(a)}catch{}},trackDataSourceUpdate:a=>{try{e().isEnabled()&&e().trackDataSourceUpdate?.(a)}catch{}},startSequenceExecution:a=>{try{return e().isEnabled()?e().startSequenceExecution(a):""}catch{return""}},captureSequenceActionBefore:(a,i,l,c,d)=>{try{e().isEnabled()&&e().captureSequenceActionBefore(a,i,l,c,d)}catch{}},captureSequenceActionAfter:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().captureSequenceActionAfter(a,i,l,c,d,f)}catch{}},endSequenceExecution:(a,i)=>{try{e().isEnabled()&&e().endSequenceExecution(a,i)}catch{}},registerStateCaptureForHandler:(a,i,l)=>{try{e().isEnabled()&&e().registerStateCaptureForHandler(a,i,l)}catch{}},detectStaleClosure:(a,i,l,c,d)=>{try{return e().isEnabled()?e().detectStaleClosure(a,i,l,c,d):[]}catch{return[]}},trackStaleClosureWarning:a=>{try{e().isEnabled()&&e().trackStaleClosureWarning(a)}catch{}},trackModalOpen:a=>{try{e().isEnabled()&&e().trackModalOpen(a)}catch{}},trackModalClose:(a,i)=>{try{e().isEnabled()&&e().trackModalClose(a,i)}catch{}},trackModalStateChange:a=>{try{e().isEnabled()&&e().trackModalStateChange(a)}catch{}},trackNestedContext:a=>{try{return e().isEnabled()?e().trackNestedContext(a):""}catch{return""}},trackNestedContextAccess:(a,i)=>{try{e().isEnabled()&&e().trackNestedContextAccess(a,i)}catch{}},trackComputedProperty:(a,i,l,c,d,f,g)=>{try{e().isEnabled()&&e().trackComputedProperty(a,i,l,c,d,f,g)}catch{}},trackComputedRecalc:(a,i,l,c,d,f,g)=>{try{e().isEnabled()&&e().trackComputedRecalc(a,i,l,c,d,f,g)}catch{}},setNamedActionDefinitions:a=>{try{e().isEnabled()&&e().setNamedActionDefinitions(a)}catch{}},trackNamedActionRef:a=>{try{e().isEnabled()&&e().trackNamedActionRef(a)}catch{}}};o.devTools=r,be.log("G7Core.devTools 인터페이스 초기화 완료")}let Ro=null;function fA(){const o=window.G7Core;return o?.__devtools?Promise.resolve(o.__devtools):Ro||(Ro=new Promise((e,r)=>{const a=window.G7Config?.coreDevToolsAsset||"/build/core/devtools.min.js",i="g7-devtools-bundle",l=()=>{const f=window.G7Core?.__devtools;f?e(f):r(new Error("DevTools 번들 로드됨 — 그러나 __devtools 미노출"))};window.G7Core=window.G7Core||{},window.G7Core.__onDevToolsReady=l;const c=document.getElementById(i);if(c){c.addEventListener("load",l,{once:!0}),c.addEventListener("error",()=>r(new Error(`DevTools 번들 로드 실패: ${a}`)),{once:!0});return}const d=document.createElement("script");d.id=i,d.src=a,d.async=!1,d.addEventListener("load",l,{once:!0}),d.addEventListener("error",()=>{Ro=null,r(new Error(`DevTools 번들 로드 실패: ${a}`))},{once:!0}),document.head.appendChild(d)}),Ro)}function hA(){const o=window.G7Core;if(!o){be.warn("G7Core가 초기화되지 않았습니다.");return}const e=Ma.getInstance();if(e.initialize(),!e.isEnabled()){window.G7DevTools={isEnabled:()=>!1,enable:()=>{be.log("환경설정 > 고급 설정 > 디버그 모드를 켜세요")}},sd(),be.log("G7DevTools 비활성화됨 (디버그 모드 꺼짐)");return}gA(o,e)}async function gA(o,e){let r;try{r=await fA()}catch(g){be.warn("DevTools 번들 로드 실패 — 최소 API 로 폴백:",g),window.G7DevTools={isEnabled:()=>!1,enable:()=>{}},sd();return}const{DiagnosticEngine:a,getServerConnector:i,getStyleTracker:l}=r;dA(),sd();try{l().enable(),be.log("StyleTracker 활성화됨")}catch(g){be.warn("StyleTracker 활성화 실패:",g)}const c=new a,d=i(),f={isEnabled:()=>e.isEnabled(),state:{get:()=>e.getState(),getHistory:()=>e.getStateHistory(),watch:(g,m)=>e.watchState(g,m)},actions:{getHistory:()=>e.getActionHistory(),watch:g=>e.watchActions(g),getMetrics:()=>e.getActionMetrics()},binding:{evaluate:g=>{const m=o.getDataBindingEngine?.();if(!m){be.warn("DataBindingEngine이 없습니다.");return}return m.evaluateExpression(g,e.getState())},getCacheStats:()=>e.getCacheStats(),clearStats:()=>{e.resetCacheStats(),be.log("캐시 통계 초기화됨")}},diagnose:{analyze:g=>c.analyze(g),suggestFix:g=>c.suggestFix(g),getCommonIssues:()=>c.getCommonIssues(),getRulesByCategory:g=>c.getRulesByCategory(g)},server:{dumpState:g=>d.dumpState(g),sendLog:g=>d.sendLog(g),sendError:(g,m)=>d.sendError(g,m),isConnected:()=>d.isConnected(),testConnection:()=>d.testConnection()},config:{isDebugMode:()=>e.isEnabled(),setLogLevel:g=>e.setLogLevel(g),setMaxHistory:g=>e.setMaxHistory(g)},lifecycle:{getMountedComponents:()=>e.getLifecycleInfo().mountedComponents,getOrphanedListeners:()=>e.getLifecycleInfo().orphanedListeners},performance:{getRenderCount:()=>e.getPerformanceInfo().renderCounts,getBindingEvalCount:()=>e.getPerformanceInfo().bindingEvalCount,getMemoryWarnings:()=>e.getPerformanceInfo().memoryWarnings,startProfiling:()=>e.startProfiling(),stopProfiling:()=>e.stopProfiling()},network:{getActiveRequests:()=>e.getNetworkInfo().activeRequests,getRequestHistory:()=>e.getNetworkInfo().requestHistory,getPendingDataSources:()=>e.getNetworkInfo().pendingDataSources},conditional:{getIfConditions:()=>e.getConditionalInfo().ifConditions,getIterations:()=>e.getConditionalInfo().iterations},form:{getForms:()=>e.getFormInfo()},websocket:{getInfo:()=>e.getWebSocketInfo()}};window.G7DevTools=f,o.devtools=f,be.log("G7DevTools 전역 객체 초기화 완료 (window.G7DevTools)"),pA(r.DevToolsPanel)}function pA(o){if(document.getElementById("g7-devtools-root")){be.log("DevToolsPanel 이미 렌더링됨");return}const e=document.createElement("div");e.id="g7-devtools-root",document.body.appendChild(e);try{ad.createRoot(e).render(We.createElement(o)),be.log("DevToolsPanel UI 렌더링 완료")}catch(r){be.error("DevToolsPanel UI 렌더링 실패:",r)}}const Qe=ut("TemplateEngine"),re={templateId:null,locale:"ko",isInitialized:!1,reactRoot:null,containerId:null,currentLayoutJson:null,currentDataContext:{},translationContext:{templateId:"",locale:"ko"},registry:null,bindingEngine:null,translationEngine:null,actionDispatcher:null,templateMetadata:null};let Do=!1;const Oo=[];async function mA(o){try{Qe.log("템플릿 메타데이터 로드 중...",o);const e=o.split("-").map(a=>a.split("_").map(i=>i.charAt(0).toUpperCase()+i.slice(1)).join("")).join(""),r=window[e];return r?.templateMetadata?(Qe.log("템플릿 메타데이터 로드 완료 (전역 변수)",r.templateMetadata),r.templateMetadata):(Qe.warn(`템플릿 번들에 메타데이터가 없습니다. 전역 변수: ${e}`),{identifier:o,locales:["ko","en"],name:{ko:o,en:o},description:{ko:"",en:""},version:"1.0.0",type:"admin"})}catch(e){return Qe.error("템플릿 메타데이터 로드 실패",e),{identifier:o,locales:["ko","en"],name:{ko:o,en:o},description:{ko:"",en:""},version:"1.0.0",type:"admin"}}}function my(){const e=(typeof window<"u"?window.__templateApp:void 0)?.globalState?.appConfig?.supportedLocales,r=re.templateMetadata?.locales;return{$locale:re.locale,$locales:Array.isArray(e)&&e.length>0?e:r||["ko","en"],$templateLocales:r||["ko","en"],$templateId:re.templateId}}async function Lo(o){try{if(Qe.log("템플릿 엔진 초기화 시작",o),re.isInitialized)throw new Error("템플릿 엔진이 이미 초기화되었습니다. destroyTemplate()을 먼저 호출하세요.");if(!o.templateId)throw new x_;Do=o.debug??!1,typeof window<"u"&&(window.G7Config||(window.G7Config={}),window.G7Config.debug=Do),hA(),re.templateId=o.templateId,re.locale=o.locale||"ko",Qe.log("엔진 인스턴스 생성 중..."),re.registry=mr.getInstance(),re.bindingEngine=new _n,re.translationEngine=gi.getInstance(),o.cacheVersion!==void 0&&o.cacheVersion>0&&re.translationEngine.setCacheVersion(o.cacheVersion),re.translationContext={templateId:o.templateId,locale:re.locale},re.actionDispatcher=new mo({},re.translationEngine,re.translationContext),P0(re.actionDispatcher),Qe.log("다국어 파일 로드 중...",o.templateId,re.locale);const e="en",r=[re.translationEngine.loadTranslations(o.templateId,re.locale).then(()=>{Qe.log(`다국어 파일 로드 완료: ${re.locale}`)}).catch(a=>{Qe.warn(`다국어 파일 로드 실패 (${re.locale}):`,a instanceof Error?a.message:a)})];re.locale!==e&&r.push(re.translationEngine.loadTranslations(o.templateId,e).then(()=>{Qe.log(`폴백 다국어 파일 로드 완료: ${e}`)}).catch(a=>{Qe.warn(`폴백 다국어 파일 로드 실패 (${e}):`,a instanceof Error?a.message:a)})),await Promise.allSettled(r),Qe.log("템플릿 메타데이터 로드 중...",o.templateId),re.templateMetadata=await mA(o.templateId),re.isInitialized=!0,Qe.log("템플릿 엔진 초기화 완료 (ComponentRegistry는 별도 로드)")}catch(e){throw Qe.error("템플릿 엔진 초기화 실패",e),re.isInitialized=!1,re.registry=null,re.bindingEngine=null,re.translationEngine=null,re.actionDispatcher=null,e}}let Mo=null;function vf(){const o=window.G7Core;return o?.__LayoutEditorChrome?Promise.resolve(o.__LayoutEditorChrome):Mo||(Mo=new Promise((e,r)=>{const a=window.G7Config?.coreEditorAsset||"/build/core/layout-editor.min.js",i="g7-layout-editor-bundle",l=document.getElementById(i),c=()=>{const f=window.G7Core?.__LayoutEditorChrome;f?e(f):r(new Error("편집기 번들 로드됨 — 그러나 __LayoutEditorChrome 미노출"))};if(window.G7Core=window.G7Core||{},window.G7Core.__onChromeReady=c,l){l.addEventListener("load",c,{once:!0}),l.addEventListener("error",()=>r(new Error(`편집기 번들 로드 실패: ${a}`)),{once:!0});return}const d=document.createElement("script");d.id=i,d.src=a,d.async=!1,d.addEventListener("load",c,{once:!0}),d.addEventListener("error",()=>{Mo=null,r(new Error(`편집기 번들 로드 실패: ${a}`))},{once:!0}),document.head.appendChild(d)}),Mo)}async function us(o){try{if(Qe.log("템플릿 렌더링 시작",o),!re.isInitialized)throw new Error("템플릿 엔진이 초기화되지 않았습니다. initTemplateEngine()을 먼저 호출하세요.");if(!o.containerId)throw new Error("containerId는 필수입니다.");if(!o.layoutJson)throw new Error("layoutJson은 필수입니다.");const e=document.getElementById(o.containerId);if(!e)throw new Error(`컨테이너를 찾을 수 없습니다: #${o.containerId}`);const r=my();if(re.containerId=o.containerId,re.currentLayoutJson=o.layoutJson,re.currentDataContext={...r,...o.dataContext||{}},re.translationContext=o.translationContext||{templateId:re.templateId||"",locale:re.locale},re.reactRoot||(Qe.log("React Root 생성"),re.reactRoot=ad.createRoot(e)),typeof window<"u"){const l=uy(window.location.pathname);if(l){Qe.log("레이아웃 편집기 모드 진입",l);let c;try{c=await vf()}catch(f){Qe.error("레이아웃 편집기 번들 로드 실패",f),gf.render(o.containerId,{title:"레이아웃 편집기 로드 실패",message:f instanceof Error?f.message:"편집기 번들을 불러오지 못했습니다.",icon:"fas fa-triangle-exclamation",showStack:!1,showReloadButton:!0,debug:Do});return}const d=We.createElement(c,{templateIdentifier:l.templateIdentifier,initialLocale:re.locale});re.reactRoot.render(We.createElement(_c,{translationEngine:re.translationEngine,translationContext:re.translationContext,children:We.createElement(Id,{children:We.createElement(uc,{children:We.createElement(cf,{children:d})})})}));return}}const a=o.layoutJson.components||[];if(a.length===0){Qe.warn("렌더링할 컴포넌트가 없습니다.");return}const i=o.layoutJson.modals||[];if(Qe.log("modals 배열:",i),Qe.log("modals 개수:",i.length),Qe.log("DynamicRenderer로 렌더링 시작"),re.reactRoot.render(We.createElement(_c,{translationEngine:re.translationEngine,translationContext:re.translationContext},We.createElement(Id,null,We.createElement(uc,null,We.createElement(cf,null,[...a.map((l,c)=>{const d=re.currentLayoutJson?.layout_name||"";return We.createElement(Br,{key:d&&!l._fromBase?`${l.id}_${d}`:l.id,componentDef:l,dataContext:re.currentDataContext,translationContext:re.translationContext,registry:re.registry,bindingEngine:re.bindingEngine,translationEngine:re.translationEngine,actionDispatcher:re.actionDispatcher,isRootRenderer:c===0,layoutKey:d})}),We.createElement(tm,{key:"__modal_parent_context_provider"},i.map(l=>{const c=re.currentDataContext._global?.modalStack||[],f=c.includes(l.id)||re.currentDataContext._global?.activeModal===l.id,g=c.indexOf(l.id),m=g>=0?50+g:50,y=window.__g7LayoutContextStack||[],S=y[y.length-1]?.dataContext,v=We.createElement(Br,{key:`modal_${l.id}_renderer`,componentDef:{...l,props:{...l.props,isOpen:f,style:{...l.props?.style,zIndex:m},onClose:re.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},re.currentDataContext)}},dataContext:re.currentDataContext,translationContext:re.translationContext,registry:re.registry,bindingEngine:re.bindingEngine,translationEngine:re.translationEngine,actionDispatcher:re.actionDispatcher,parentDataContext:S});return l.data_sources&&l.data_sources.length>0?We.createElement(ly,{key:`modal_${l.id}`,isOpen:f,modalId:l.id,dataSources:l.data_sources,dataContext:re.currentDataContext,globalStateUpdater:re.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:re.bindingEngine,debug:Do,children:v}):v}))]))))),Qe.log("템플릿 렌더링 완료"),Oo.length>0){Qe.log(`대기 중인 데이터 업데이트 ${Oo.length}건 적용`);const l=[...Oo];Oo.length=0;for(const{data:c,options:d}of l)ds(c,d)}}catch(e){throw Qe.error("템플릿 렌더링 실패",e),e}}function ds(o,e){try{if(Qe.log("템플릿 데이터 업데이트 시작",Object.keys(o)),!re.isInitialized)throw new Error("템플릿 엔진이 초기화되지 않았습니다.");if(!re.reactRoot||!re.currentLayoutJson){Qe.log("React 미준비 - 데이터 업데이트 큐잉:",Object.keys(o)),Oo.push({data:o,options:e});return}const r=my(),a={...re.currentDataContext._global||{},...o._global||{}},i=Y0(re.currentDataContext._localInit,o._localInit);re.currentDataContext={...r,...re.currentDataContext,...o,_global:a,_localInit:i},a._local!==void 0&&(re.currentDataContext._local=a._local),a._computed!==void 0&&(re.currentDataContext._computed=a._computed);const l=re.currentLayoutJson.components||[],c=re.currentLayoutJson.modals||[];Qe.log("updateTemplateData - modals 배열:",c),Qe.log("updateTemplateData - modals 개수:",c.length),Qe.log("updateTemplateData - activeModal:",re.currentDataContext._global?.activeModal);const d=()=>{re.reactRoot.render(We.createElement(_c,{translationEngine:re.translationEngine,translationContext:re.translationContext},We.createElement(Id,null,We.createElement(uc,null,We.createElement(cf,null,[...l.map((f,g)=>{const m=re.currentLayoutJson?.layout_name||"";return We.createElement(Br,{key:m&&!f._fromBase?`${f.id}_${m}`:f.id,componentDef:f,dataContext:re.currentDataContext,translationContext:re.translationContext,registry:re.registry,bindingEngine:re.bindingEngine,translationEngine:re.translationEngine,actionDispatcher:re.actionDispatcher,isRootRenderer:g===0,layoutKey:m})}),We.createElement(tm,{key:"__modal_parent_context_provider_update"},c.map(f=>{const g=re.currentDataContext._global?.modalStack||[],y=g.includes(f.id)||re.currentDataContext._global?.activeModal===f.id,w=g.indexOf(f.id),S=w>=0?50+w:50,v=window.__g7LayoutContextStack||[],x=v[v.length-1]?.dataContext,R=We.createElement(Br,{key:`modal_${f.id}_renderer`,componentDef:{...f,props:{...f.props,isOpen:y,style:{...f.props?.style,zIndex:S},onClose:re.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},re.currentDataContext)}},dataContext:re.currentDataContext,translationContext:re.translationContext,registry:re.registry,bindingEngine:re.bindingEngine,translationEngine:re.translationEngine,actionDispatcher:re.actionDispatcher,parentDataContext:x});return f.data_sources&&f.data_sources.length>0?We.createElement(ly,{key:`modal_${f.id}`,isOpen:y,modalId:f.id,dataSources:f.data_sources,dataContext:re.currentDataContext,globalStateUpdater:re.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:re.bindingEngine,debug:Do,children:R}):R}))])))))};e?.sync?(Qe.log("재렌더링 시작 (sync mode - 즉시 렌더링)"),d()):(Qe.log("재렌더링 시작 (with startTransition)"),N.startTransition(()=>{d()})),Qe.log("템플릿 데이터 업데이트 완료")}catch(r){throw Qe.error("템플릿 데이터 업데이트 실패",r),r}}function Oc(){try{Qe.log("템플릿 정리 시작"),re.reactRoot&&(Qe.log("React Root 언마운트"),re.reactRoot.unmount(),re.reactRoot=null),re.templateId=null,re.locale="ko",re.isInitialized=!1,re.containerId=null,re.currentLayoutJson=null,re.currentDataContext={},re.translationContext={templateId:"",locale:"ko"},re.registry=null,re.bindingEngine=null,re.translationEngine=null,re.actionDispatcher=null,re.templateMetadata=null,Qe.log("템플릿 정리 완료")}catch(o){throw Qe.error("템플릿 정리 실패",o),o}}function ta(){return Object.freeze({...re})}function yy(){return re.actionDispatcher}const $o={initTemplateEngine:Lo,renderTemplate:us,updateTemplateData:ds,destroyTemplate:Oc,getState:ta};typeof window<"u"&&(window.G7Core||(window.G7Core={}),window.G7Core.TemplateEngine=$o,uA({getState:()=>({translationEngine:re.translationEngine,translationContext:re.translationContext,bindingEngine:re.bindingEngine,actionDispatcher:re.actionDispatcher,templateMetadata:re.templateMetadata}),transitionManager:zr,responsiveManager:mi,webSocketManager:Tc}),Qe.log("전역 객체 window.G7Core.TemplateEngine에 노출됨"));const Pr=Object.freeze(Object.defineProperty({__proto__:null,DataSourceManager:_r,LayoutLoader:of,TemplateApp:To,TemplateEngine:$o,default:$o,destroyTemplate:Oc,getActionDispatcher:yy,getState:ta,initTemplateApp:bf,initTemplateEngine:Lo,loadLayoutEditorBundle:vf,renderTemplate:us,updateTemplateData:ds},Symbol.toStringTag,{value:"Module"}));Wt.DataSourceManager=_r,Wt.LayoutLoader=of,Wt.TemplateApp=To,Wt.TemplateEngine=$o,Wt.default=$o,Wt.destroyTemplate=Oc,Wt.getActionDispatcher=yy,Wt.getState=ta,Wt.initTemplateApp=bf,Wt.initTemplateEngine=Lo,Wt.loadLayoutEditorBundle=vf,Wt.renderTemplate=us,Wt.updateTemplateData=ds,Object.defineProperties(Wt,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})})(this.G7Core=this.G7Core||{}); -//# sourceMappingURL=template-engine.min.js.map + `}static renderFromError(e,n,a,i=!1,l,c,d){let f,h;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?h=m.details.apiMessage:m.response?.data?.message&&(h=m.response.data.message)}this.render(e,{title:a,message:f,detailMessage:h,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 gs=dt("Router");class TA{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(Ir(`/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,gs.log(`Loaded ${this.routes.length} routes${e?` (v=${e})`:""}`);else throw new Error("Invalid routes data format")}catch(n){throw gs.error("Error loading routes:",n),n}}setRoutes(e){this.routes=e,gs.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,h)=>(a.push(h),"([^/]+)"));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,h)=>{d[f]=c[h+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){gs.warn(`No route matched for path: ${e}`),this.emit("routeNotFound",e);return}if(a.route.redirect){gs.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=Sr.getInstance();let f=d.isAuthenticated()&&d.getAuthType()===c;if(f||(f=await d.checkAuth(c)),!f){const h=d.getLoginRedirectUrl(c,e+n);gs.log(`Not authenticated, redirecting to: ${h}`),window.location.href=h;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 kA=/^\/admin\/layout-editor\/([^/?#]+)\/?$/;function zy(s){const e=kA.exec(s);return e?{templateIdentifier:e[1]}:null}const Bt=dt("ErrorPageHandler");class RA{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(Ir(`/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.errorConfig),this.errorConfig)}catch(e){return Bt.error("Failed to load error config:",e),this.configLoaded=!0,null}}async renderError(e,n="app"){try{const a=await this.loadConfig();if(!a)return this.debug&&Bt.warn("No error config available, cannot render error page"),!1;const i=a.layouts[e]||a.layouts[String(e)];if(!i)return this.debug&&Bt.warn(`No layout defined for error code: ${e}`),!1;this.debug&&Bt.log(`Loading error layout: ${i} for code: ${e}`);const l=await this.layoutLoader.loadLayout(this.templateId,i);if(!l)return this.debug&&Bt.error(`Failed to load error layout: ${i}`),!1;this.debug&&Bt.log("Error layout loaded:",l);let c={};const d=l.data_sources||[];if(d.length>0){this.debug&&Bt.log("Fetching data sources:",d.map(v=>v.id));const m=d.filter(v=>v.loading_strategy==="blocking"),b=d.filter(v=>!v.loading_strategy||v.loading_strategy==="progressive"),S=[...m,...b];if(S.length>0)try{c=await this.dataSourceManager.fetchDataSources(S,{},new URLSearchParams),this.debug&&Bt.log("Data sources fetched:",Object.keys(c))}catch(v){Bt.error("Failed to fetch data sources:",v)}}const f={};d.length>0&&(this.processInitOptions(d,c,f),this.debug&&Bt.log("initOptions processed:",{globalKeys:Object.keys(this.globalState),localKeys:Object.keys(f)}));const h={...c,errorCode:e,_global:{...this.globalState},_local:{...f}};return await this.renderFunction({containerId:n,layoutJson:l,dataContext:h,translationContext:{templateId:this.templateId,locale:this.locale}}),this.debug&&Bt.log(`Error page ${e} rendered successfully`),!0}catch(a){return Bt.error(`Failed to render error page ${e}:`,a),!1}}processInitOptions(e,n,a){for(const i of e){const l=n[i.id];if(!l)continue;const c=l?.data??l;if(i.initLocal){if(typeof i.initLocal=="string")a[i.initLocal]=c,Bt.log(`initLocal: ${i.id}.data -> _local.${i.initLocal}`);else if(typeof i.initLocal=="object"&&"key"in i.initLocal){const{key:d,path:f}=i.initLocal;a[d]=f?this.getValueByPath(c,f):c,Bt.log(`initLocal: ${i.id}.data${f?"."+f:""} -> _local.${d}`)}}if(i.initGlobal){const d=Array.isArray(i.initGlobal)?i.initGlobal:[i.initGlobal];for(const f of d)if(typeof f=="string")this.globalState[f]=c,Bt.log(`initGlobal: ${i.id}.data -> _global.${f}`);else if(typeof f=="object"&&f!==null&&"key"in f){const{key:h,path:m}=f;this.globalState[h]=m?this.getValueByPath(c,m):c,Bt.log(`initGlobal: ${i.id}.data${m?"."+m:""} -> _global.${h}`)}}}}getValueByPath(e,n){return n.split(".").reduce((a,i)=>a?.[i],e)}updateGlobalState(e){this.globalState={...this.globalState,...e}}updateLocale(e){this.locale=e,this.debug&&Bt.log("Locale updated:",e)}isConfigLoaded(){return this.configLoaded}async hasErrorLayout(e){const n=await this.loadConfig();return n?!!(n.layouts[e]||n.layouts[String(e)]):!1}clearConfigCache(){this.errorConfig=null,this.configLoaded=!1,this.debug&&Bt.log("Config cache cleared")}}const Nt=dt("ModuleAssetLoader");class DA{constructor(){$(this,"loadedAssets",new Map);$(this,"loadingPromises",new Map);$(this,"failedJsAssets",new Set)}hasFailedJsAssets(){return this.failedJsAssets.size>0}getFailedJsAssets(){return[...this.failedJsAssets]}async loadActiveExtensionAssets(e){if(!e||e.length===0){Nt.log("No module assets to load");return}const n=[...e].sort((d,f)=>d.priority-f.priority);Nt.log("Loading module assets:",n.map(d=>d.identifier));const a=n.filter(d=>d.css).map(d=>this.loadCSS(d.identifier,d.css)),i=n.filter(d=>d.js).map(d=>this.loadJS(d.identifier,d.js)),l=await Promise.allSettled([...a,...i]),c=l.filter(d=>d.status==="rejected");if(c.length>0){Nt.warn(`Some module assets failed to load (${c.length}/${l.length}); continuing with the rest`,this.getFailedJsAssets());return}Nt.log("All module assets loaded successfully")}async loadBundle(e,n,a){const i=[];if(a&&i.push(this.loadBundleCss(e,a)),n&&i.push(this.loadBundleJs(e,n)),i.length===0){Nt.log(`No bundle assets to load for: ${e}`);return}await Promise.all(i)}async loadBundleCss(e,n){const a=`ext-bundle-css-${e}`;if(document.getElementById(a)){Nt.log(`Bundle CSS already loaded: ${e}`);return}return new Promise(i=>{const l=document.createElement("link");l.rel="stylesheet",l.href=n,l.id=a,l.onload=()=>{Nt.log(`Bundle CSS loaded: ${e}`),this.registerLoadedAsset(`bundle-${e}`,{type:"css",element:l}),i()},l.onerror=()=>{Nt.warn(`Failed to load bundle CSS: ${e} (${n})`),i()},document.head.appendChild(l)})}async loadBundleJs(e,n){const a=`ext-bundle-js-${e}`;if(document.getElementById(a)){Nt.log(`Bundle JS already loaded: ${e}`);return}const i=this.loadingPromises.get(a);if(i)return Nt.log(`Bundle JS already loading: ${e}`),i;const l=Sg(n,{id:a},{label:`bundle JS: ${e}`}).then(()=>{Nt.log(`Bundle JS loaded: ${e}`);const c=document.getElementById(a);c&&this.registerLoadedAsset(`bundle-${e}`,{type:"js",element:c}),this.loadingPromises.delete(a)}).catch(c=>{throw Nt.warn(`Failed to load bundle JS: ${e} (${n})`,c),this.failedJsAssets.add(e),this.loadingPromises.delete(a),c});return this.loadingPromises.set(a,l),l}async loadCSS(e,n){const a=`module-css-${e}`;if(document.getElementById(a)){Nt.log(`CSS already loaded: ${e}`);return}return new Promise((i,l)=>{const c=document.createElement("link");c.rel="stylesheet",c.href=n,c.id=a,c.onload=()=>{Nt.log(`CSS loaded: ${e}`),this.registerLoadedAsset(e,{type:"css",element:c}),i()},c.onerror=()=>{Nt.warn(`Failed to load CSS: ${e} (${n})`),i()},document.head.appendChild(c)})}async loadJS(e,n){const a=`module-js-${e}`;if(document.getElementById(a)){Nt.log(`JS already loaded: ${e}`);return}const i=this.loadingPromises.get(e);if(i)return Nt.log(`JS already loading: ${e}`),i;const l=Sg(n,{id:a},{label:`JS: ${e}`}).then(()=>{Nt.log(`JS loaded: ${e}`);const c=document.getElementById(a);c&&this.registerLoadedAsset(e,{type:"js",element:c}),this.loadingPromises.delete(e)}).catch(c=>{throw Nt.warn(`Failed to load JS: ${e} (${n})`,c),this.failedJsAssets.add(e),this.loadingPromises.delete(e),c});return this.loadingPromises.set(e,l),l}registerLoadedAsset(e,n){const a=this.loadedAssets.get(e)||[];a.push(n),this.loadedAssets.set(e,a)}unloadExtensionAsset(e){const n=this.loadedAssets.get(e);if(!n||n.length===0){Nt.log(`No assets to unload for: ${e}`);return}n.forEach(a=>{a.element.parentNode&&(a.element.parentNode.removeChild(a.element),Nt.log(`${a.type.toUpperCase()} unloaded: ${e}`))}),this.loadedAssets.delete(e),Nt.log(`All assets unloaded for: ${e}`)}unloadAllAssets(){Array.from(this.loadedAssets.keys()).forEach(n=>{this.unloadExtensionAsset(n)}),Nt.log("All module assets unloaded")}isLoaded(e){return this.loadedAssets.has(e)}getLoadedModules(){return Array.from(this.loadedAssets.keys())}}let $f=null;function Nf(){return $f||($f=new DA),$f}function OA(){if(typeof window>"u")return[];const s=window.G7Config;if(!s?.moduleAssets)return[];const e=[];for(const[n,a]of Object.entries(s.moduleAssets)){const i=a;e.push({identifier:n,js:i.js?Aa(i.js):i.js,css:i.css?Aa(i.css):i.css,priority:i.priority,external:i.external})}return e}function LA(){if(typeof window>"u")return null;const s=window.G7Config;if(!s?.bundleUrls)return null;const e=s.bundleUrls;return{moduleJs:e.moduleJs?Aa(e.moduleJs):e.moduleJs,moduleCss:e.moduleCss?Aa(e.moduleCss):e.moduleCss,pluginJs:e.pluginJs?Aa(e.pluginJs):e.pluginJs,pluginCss:e.pluginCss?Aa(e.pluginCss):e.pluginCss}}function MA(){if(typeof window>"u")return[];const s=window.G7Config;if(!s?.pluginAssets)return[];const e=[];for(const[n,a]of Object.entries(s.pluginAssets)){const i=a;e.push({identifier:n,js:i.js?Aa(i.js):i.js,css:i.css?Aa(i.css):i.css,priority:i.priority,external:i.external})}return e}const If=dt("SystemBannerManager");class Tc{static show(e){this.banners.set(e.id,e),this.render(),If.log(`Banner shown: ${e.id}`)}static hide(e){this.banners.delete(e)&&(this.render(),If.log(`Banner hidden: ${e}`))}static hideAll(){this.banners.clear(),this.render(),If.log("All banners hidden")}static detectLocale(){try{const n=window.G7Core;if(n?.locale?.current)return n.locale.current()}catch{}return(navigator.language||"ko").split("-")[0]}static resolveMessage(e,n){return typeof e=="string"?e:e[n]||e.en||e.ko||Object.values(e)[0]||""}static render(){if(typeof document>"u")return;let e=document.getElementById(this.containerId);if(this.banners.size===0){e&&(e.remove(),this.adjustAppPadding(0));return}e||(e=document.createElement("div"),e.id=this.containerId,e.style.cssText="position:fixed;top:0;left:0;right:0;z-index:99999;",document.body.prepend(e));const n=Array.from(this.banners.values()).sort((i,l)=>(i.order??0)-(l.order??0)),a=this.detectLocale();e.innerHTML=n.map(i=>{const l=this.resolveMessage(i.message,a),c=i.background||"#f59e0b",d=i.color||"white";return`<div data-banner-id="${i.id}" style="background:${c};color:${d};text-align:center;padding:8px 16px;font-size:14px;font-weight:500;">${l}</div>`}).join(""),requestAnimationFrame(()=>{e&&this.adjustAppPadding(e.offsetHeight)})}static adjustAppPadding(e){const n=document.getElementById("app");n&&(n.style.paddingTop=e>0?`${e}px`:"")}}$(Tc,"banners",new Map),$(Tc,"containerId","g7-system-banners");const U=dt("TemplateApp");function Uy(s){const e={};for(const n of s.keys()){if(n in e)continue;const a=s.getAll(n);a.length>1?e[n]=a:a.length===1&&(n.endsWith("[]")?e[n]=a:e[n]=a[0])}return e}const sr=class sr{constructor(e){$(this,"router",null);$(this,"layoutLoader",null);$(this,"errorPageHandler",null);$(this,"config");$(this,"globalState");$(this,"globalStateListeners",new Set);$(this,"currentRouteChangeId",0);$(this,"currentDataSources",[]);$(this,"currentRawDataSources",[]);$(this,"currentRouteParams",{});$(this,"currentQueryParams",new URLSearchParams);$(this,"currentFetchedData",{});$(this,"currentLayoutName","");$(this,"templateErrorHandling",null);$(this,"currentWebSocketSubscriptions",[]);$(this,"extensionCacheVersion",0);$(this,"currentGlobalHeaders",[]);$(this,"transitionOverlayEl",null);$(this,"skeletonOverlayRoot",null);$(this,"skeletonOverlayContainer",null);$(this,"_spinnerState",null);$(this,"modalDataSources",new Map);this.globalState={sidebarOpen:!1},this.loadG7Config(),this.migrateLocaleStorage();const n=this.loadLocaleFromStorage(),a=n||e.locale||"ko";this.config={...e,locale:a},!n&&e.locale&&this.saveLocaleToStorage(e.locale)}loadG7Config(){if(typeof window<"u"&&window.G7Config){const e=window.G7Config;e.settings&&(this.globalState.settings=e.settings,U.log("Loaded settings from G7Config:",Object.keys(e.settings)),e.settings.upload&&(this.globalState.uploadSettings=e.settings.upload)),e.plugins&&(this.globalState.plugins=e.plugins,U.log("Loaded plugin settings from G7Config:",Object.keys(e.plugins))),e.modules&&(this.globalState.modules=e.modules,U.log("Loaded module settings from G7Config:",Object.keys(e.modules))),e.appConfig&&(this.globalState.appConfig=e.appConfig,U.log("Loaded appConfig from G7Config:",Object.keys(e.appConfig)))}}resolveRouteExpressions(e){const n=new Tn,a={_global:this.globalState};return e.map(i=>{const l={...i};if(l.path&&l.path.includes("{{")){const c=l.path;l.path=n.resolveBindings(l.path,a),l.path=l.path.replace(/\/\/+/g,"/")||"/",l.path.includes("{{")&&(U.warn("Route expression resolution failed, using fallback:",c),l.path=c.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/")}if(l.redirect&&l.redirect.includes("{{")){const c=l.redirect;l.redirect=n.resolveBindings(l.redirect,a),l.redirect=l.redirect.replace(/\/\/+/g,"/")||"/",l.redirect.includes("{{")&&(U.warn("Route redirect expression resolution failed:",c),l.redirect=c.replace(/\{\{[^}]+\}\}/g,"").replace(/\/\/+/g,"/")||"/")}return l})}migrateLocaleStorage(){const e=["locale","g7_template_locale"];for(const n of e)try{const a=localStorage.getItem(n);a&&a!==localStorage.getItem(sr.LOCALE_STORAGE_KEY)&&(localStorage.setItem(sr.LOCALE_STORAGE_KEY,a),U.log(`Migrated locale from '${n}' to '${sr.LOCALE_STORAGE_KEY}'`)),localStorage.removeItem(n)}catch(a){U.warn(`Failed to migrate locale key '${n}':`,a)}}async init(){try{to.getInstance().setDebug(this.config.debug),U.log("Initializing with config:",this.config),vw();const e=yr.getInstance(),n=Sr.getInstance(),a=this.loadCacheVersionFromStorage()||0,[i,l,c,d,f]=await Promise.all([No({templateId:this.config.templateId,templateType:this.config.templateType,locale:this.config.locale,debug:this.config.debug,cacheVersion:a}),e.loadComponents(this.config.templateId,this.config.templateType),Il(Ir(`/api/templates/${this.config.templateId}/routes`,"json",a>0?a:null),{label:"routes.json"}).then(A=>{if(!A.ok)throw new Error(`Failed to load routes: ${A.statusText}`);return A.json()}).then(A=>{if(!A.success)throw new Error("Failed to load routes from API");return A.data}).catch(A=>{throw U.error("Error loading routes:",A),A}),n.preloadAuth(this.config.templateType==="admin"?"admin":"user"),fetch(Ir(`/api/templates/${this.config.templateId}/config`,"json")).then(A=>A.ok?A.json():null).then(A=>!A?.success||!A?.data?null:A.data).catch(A=>(U.warn("Error loading template config:",A),null))]);if(U.log("Template Engine initialized"),U.log("ComponentRegistry loaded"),U.log("Routes data loaded"),U.log("User info preloaded"),U.log("Template config loaded:",f),f?.cache_version!==void 0){const A=this.loadCacheVersionFromStorage();if(this.extensionCacheVersion=f.cache_version,this.saveCacheVersionToStorage(this.extensionCacheVersion),U.log("Extension cache version:",this.extensionCacheVersion),A!==null&&A!==this.extensionCacheVersion){U.log("Cache version changed, reloading routes...");const x=await Il(Ir(`/api/templates/${this.config.templateId}/routes`,"json",this.extensionCacheVersion),{label:"routes.json (reload)"}).then(L=>{if(!L.ok)throw new Error(`Failed to reload routes: ${L.statusText}`);return L.json()}).then(L=>{if(!L.success)throw new Error("Failed to reload routes from API");return L.data});Array.isArray(x.routes)&&(x.routes=this.resolveRouteExpressions(x.routes),Object.assign(c,x),U.log("Routes reloaded with new cache version"));try{const{TranslationEngine:L}=await Promise.resolve().then(()=>Ag),M=L.getInstance();M.setCacheVersion(this.extensionCacheVersion);const k=this.config.locale||"ko",O="en";await M.loadTranslations(this.config.templateId,k,"/api",!0),k!==O&&await M.loadTranslations(this.config.templateId,O,"/api",!0),U.log("Translations reloaded with new cache version")}catch(L){U.error("Failed to reload translations:",L)}}}f?.errorHandling&&(this.templateErrorHandling=f.errorHandling,Cr().setTemplateConfig(this.templateErrorHandling),U.log("Template errorHandling registered:",this.templateErrorHandling)),n.on("logout",()=>{U.log("User logged out")}),n.on("authStateChange",A=>{U.log("Auth state changed:",A)}),U.log("AuthManager event handlers registered");const h=Hr(),m=this.config.templateType==="admin"?"admin":"user",b=n.getConfig(m);h.setOnUnauthorized(()=>{U.log("Unauthorized - redirecting to login page"),h.removeToken();const A=window.location.pathname+window.location.search,x=n.getLoginRedirectUrl(m,A,"session_expired");window.location.href=x}),U.log("ApiClient onUnauthorized callback registered"),this.layoutLoader=new xf(e),this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),U.log("LayoutLoader initialized:",this.layoutLoader);const S=new xr({onUnauthorized:()=>{U.warn("Unauthorized request in error page")}});if(this.errorPageHandler=new RA({templateId:this.config.templateId,layoutLoader:this.layoutLoader,locale:this.config.locale,debug:this.config.debug,renderFunction:ms,dataSourceManager:S,globalState:this.globalState}),U.log("ErrorPageHandler initialized with DataSourceManager"),await this.handleServerError()){U.log("Server error handled, skipping normal initialization");return}if(this.router=new TA(this.config.templateId),Array.isArray(c.routes)){const A=this.resolveRouteExpressions(c.routes);this.router.setRoutes(A)}else throw new Error("Invalid routes data format");U.log("Router initialized:",this.router),U.log("Routes set:",this.router.getRoutes());const{getActionDispatcher:v}=await Promise.resolve().then(()=>Br),_=v();if(_&&(_.setDefaultContext({navigate:A=>this.router?.navigate(A)}),_.setGlobalStateUpdater((A,x)=>this.setGlobalState(A,x)),U.log("Navigate function and setGlobalState injected to ActionDispatcher")),await this.loadExtensionAssets(),this.reinitializeTemplateHandlers(),this.router.on("routeChange",A=>this.handleRouteChange(A)),this.router.on("routeNotFound",A=>this.handleRouteNotFound(A)),typeof window<"u"&&zy(window.location.pathname)){U.log("Layout editor mode detected — skipping router match"),await ms({containerId:"app",layoutJson:{components:[]},dataContext:{},translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),U.log("Template App initialized in layout editor mode");return}this.router.navigateToCurrentPath(),U.log("Template App initialized successfully")}catch(e){U.error("Initialization failed:",e),this.showInitError(e)}}async loadExtensionAssets(){try{const e=Nf(),n=LA();if(!n){await this.loadExtensionAssetsIndividually();return}await e.loadBundle("module",n.moduleJs,n.moduleCss),await e.loadBundle("plugin",n.pluginJs,n.pluginCss),U.log("Extension bundle assets loaded successfully")}catch(e){U.warn("Failed to load extension assets:",e)}}async loadExtensionAssetsIndividually(){const e=Nf(),n=OA();n.length>0&&(U.log("Loading module assets (individual fallback):",n.map(i=>i.identifier)),await e.loadActiveExtensionAssets(n));const a=MA();a.length>0&&(U.log("Loading plugin assets (individual fallback):",a.map(i=>i.identifier)),await e.loadActiveExtensionAssets(a))}async handleRouteChange(e){const n=++this.currentRouteChangeId;window.__g7ForcedLocalFields=void 0,window.__g7ActionContext=void 0,window.__g7PendingLocalState=void 0,window.__g7LastSetLocalSnapshot=void 0,window.__g7SetLocalOverrideKeys=void 0,window.__g7SequenceLocalSync=void 0,window.__g7AutoBindingPaths=new Map;try{if(U.log("Route changed:",e,"requestId:",n),!this.layoutLoader)throw new Error("LayoutLoader is not initialized");if(!e.layout)throw new Error("Route layout is not defined");let a=e.layout;const i=a==="__preview__";i&&e.params?.token&&(a=`__preview__/${e.params.token}`);const l=await this.layoutLoader.loadLayout(this.config.templateId,a);if(n!==this.currentRouteChangeId){U.log("Route change cancelled (newer request exists):",n);return}U.log("Layout loaded:",l);const c=Cr();l.errorHandling?(c.setLayoutConfig(l.errorHandling),U.log("Layout errorHandling registered:",l.errorHandling)):c.clearLayoutConfig();const d=l.data_sources||[],f=e.query||{},h=new URLSearchParams;for(const[Z,ge]of Object.entries(f))if(Array.isArray(ge))for(const H of ge)h.append(Z,H);else h.set(Z,ge);const m={route:e.params||{},query:f,_global:this.globalState};l.scripts&&Array.isArray(l.scripts)&&await this.loadLayoutScripts(l.scripts,m);const{DataSourceManager:b,getActionDispatcher:S}=await Promise.resolve().then(()=>Br),v=new b;if(i){const Z=S();Z&&Z.setPreviewMode(!0),this.setGlobalState({__isPreview:!0}),Tc.show({id:"preview-mode",message:{ko:"⚠ 미리보기 모드 — 페이지 이동이 비활성화됩니다",en:"⚠ Preview Mode — Navigation is disabled"},background:"linear-gradient(90deg, #f59e0b, #d97706)",color:"white"}),U.log("Preview mode activated")}else{const Z=S();Z?.isPreviewMode()&&(Z.setPreviewMode(!1),Tc.hide("preview-mode"))}if(this.currentGlobalHeaders=l.globalHeaders||[],this.currentGlobalHeaders.length>0){v.setGlobalHeaders(this.currentGlobalHeaders);const Z=S();Z&&Z.setGlobalHeaders(this.currentGlobalHeaders),U.log("globalHeaders set:",this.currentGlobalHeaders.map(ge=>ge.pattern))}if(l.named_actions&&Object.keys(l.named_actions).length>0){const Z=S();Z&&Z.setNamedActions(l.named_actions)}const _=v.filterByCondition(d,m);d.length!==_.length&&U.log("Data sources filtered by condition:",{before:d.map(Z=>Z.id),after:_.map(Z=>Z.id)});const A=_.filter(Z=>Z.loading_strategy==="blocking"),x=_.filter(Z=>(Z.loading_strategy||"progressive")!=="blocking"&&Z.type!=="websocket"),L=Array.isArray(l.transition_overlay?.wait_for)?l.transition_overlay.wait_for:[],M=L.length>0&&_.some(Z=>L.includes(Z.id)&&Z.type!=="websocket"&&(Z.loading_strategy||"progressive")!=="background");if((A.length>0||M)&&l.transition_overlay){const Z=typeof l.transition_overlay=="boolean"?{enabled:l.transition_overlay,style:"opaque"}:l.transition_overlay;Z.enabled&&Z.style==="skeleton"&&Z.skeleton?.component&&Z.target?this.renderSkeletonOverlay(Z.target,Z.skeleton,l,Z.fallback_target):Z.enabled&&Z.style==="spinner"&&Z.target&&this.renderSpinnerOverlay(Z.target,Z.spinner,Z.fallback_target)}let k={},O={},B={},G={};this.currentWebSocketSubscriptions.length>0&&(U.log("Unsubscribing previous WebSocket subscriptions:",this.currentWebSocketSubscriptions),v.unsubscribeWebSockets(this.currentWebSocketSubscriptions),this.currentWebSocketSubscriptions=[]),this.currentDataSources=_,this.currentRawDataSources=d,this.currentRouteParams=e.params||{},this.currentQueryParams=h,U.log(`handleRouteChange #${n} - queryObject:`,f),U.log(`handleRouteChange #${n} - queryParams:`,h.toString()),A.length>0&&(U.log("Fetching blocking data sources:",A.map(ge=>ge.id)),(await v.fetchDataSourcesWithResults(A,e.params||{},h)).forEach(ge=>{if(ge.state==="success"&&ge.data!==void 0)k[ge.id]=ge.data;else if(ge.state==="error"&&ge.error){const H=ge.error,T=H.response?.data?.message;O[ge.id]={message:T||ge.error.message,status:H.response?.status||H.status}}}),this.processInitOptions(A,k,B,G),this.currentFetchedData={...k},U.log("Blocking data loaded:",Object.keys(k)),Object.keys(O).length>0&&U.log("Data source errors:",O),Object.keys(B).length>0&&U.log("Local state init (blocking):",Object.keys(B)),Object.keys(G).length>0&&U.log("Isolated state init (blocking):",Object.keys(G)));const P=x.length>0;if(P&&(zr.setPending(!0),U.log("Transition started before rendering")),n!==this.currentRouteChangeId){U.log("Route change cancelled after blocking data (newer request exists):",n);return}const{getState:W}=await Promise.resolve().then(()=>Br),pe=W().currentDataContext||{},Se=x.map(Z=>Z.id),we={};if(P){Se.forEach(ge=>{pe[ge]!==void 0?we[ge]=pe[ge]:we[ge]=void 0});const Z=Object.keys(we).filter(ge=>we[ge]!==void 0);Z.length>0&&U.log("Preserving previous progressive data:",Z),U.log("Progressive data sources initialized:",Se)}const Ue=l.defines||{},Ve={...we,...e.params,...k,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState},_globalSetState:Z=>this.setGlobalState(Z),_dataSourceErrors:Object.keys(O).length>0?O:void 0,_localInit:Object.keys(B).length>0?B:void 0,_isolatedInit:Object.keys(G).length>0?G:void 0,_defines:Object.keys(Ue).length>0?Ue:void 0};if(l.computed&&Object.keys(l.computed).length>0){const Z=this.calculateComputed(l.computed,Ve);Object.keys(Z).length>0&&(Ve._computed=Z,this.globalState._computed=Z,U.log("Computed values calculated:",Object.keys(Z))),Ve._computedDefinitions=l.computed}const qe=l.layout_name||e.layout;this.currentLayoutName!==""&&this.currentLayoutName!==qe&&(U.log("_local reset due to layout change:",{from:this.currentLayoutName,to:qe}),this.globalState._local={},qC()),this.currentLayoutName=qe;const J=l.initLocal||l.state;if(J&&Object.keys(J).length>0){this.globalState._local||(this.globalState._local={});for(const[Z,ge]of Object.entries(J))this.globalState._local[Z]===void 0&&(this.globalState._local[Z]=JSON.parse(JSON.stringify(ge)));Ve._local={...this.globalState._local},U.log("initLocal applied to _local:",Object.keys(J))}if(l.initGlobal&&Object.keys(l.initGlobal).length>0){for(const[Z,ge]of Object.entries(l.initGlobal))this.globalState[Z]===void 0&&(this.globalState[Z]=JSON.parse(JSON.stringify(ge)));U.log("initGlobal applied to _global:",Object.keys(l.initGlobal))}if(l.initIsolated&&Object.keys(l.initIsolated).length>0){for(const[Z,ge]of Object.entries(l.initIsolated))G[Z]===void 0&&(G[Z]=JSON.parse(JSON.stringify(ge)));U.log("initIsolated applied:",Object.keys(l.initIsolated))}Ve._global={...this.globalState,layoutWarnings:l.warnings||[]};const fe=l.initActions||l.init_actions;if(fe&&fe.length>0){await this.executeInitActions(fe,Ve),U.log("initActions executed before render");const Z=this.globalState;if(Z._local){if(Object.keys(B).length>0){for(const[ge,H]of Object.entries(B))Z._local[ge]!==void 0?Z._local[ge]=this.deepMerge(Z._local[ge],H):Z._local[ge]=H;U.log("localInit merged into _local after initActions:",Object.keys(B))}Ve._local=Z._local,U.log("_local merged into dataContext:",Z._local)}if(Ve._global={...this.globalState,layoutWarnings:l.warnings||[]},U.log("_global merged into dataContext after initActions"),l.computed&&Object.keys(l.computed).length>0){const ge=this.calculateComputed(l.computed,Ve);Object.keys(ge).length>0&&(Ve._computed=ge,this.globalState._computed=ge,U.log("Computed values recalculated after init_actions:",Object.keys(ge)))}if(Object.keys(this.currentFetchedData).length>0){for(const[ge,H]of Object.entries(this.currentFetchedData))Ve[ge]=H;U.log("Fetched data sources merged into dataContext after initActions:",Object.keys(this.currentFetchedData))}}if(l.transition_overlay&&!this.skeletonOverlayContainer&&this.showTransitionOverlay(l.transition_overlay,l),await ms({containerId:"app",layoutJson:l,dataContext:Ve,translationContext:{templateId:this.config.templateId,locale:this.config.locale}}),U.log("Initial render complete with blocking data"),this.reattachSpinnerOverlay(),x.length>0){if(n!==this.currentRouteChangeId){U.log("Route change cancelled before progressive fetch (newer request exists):",n),zr.setPending(!1),this.hideTransitionOverlay();return}U.log("Fetching progressive/background data sources:",x.map(H=>H.id));const{updateTemplateData:Z,getState:ge}=await Promise.resolve().then(()=>Br);try{const H=x.map(async T=>{try{const de=(await v.fetchDataSourcesWithResults([T],e.params||{},h,this.globalState))[0];if(!de){U.log(`Data source ${T.id} skipped (no fetch result)`);return}if(n!==this.currentRouteChangeId){U.log(`Data source ${T.id} fetch cancelled (newer request exists)`);return}if(de.state==="success"&&de.data!==void 0){U.log(`Progressive data source loaded: ${T.id}`),this.currentFetchedData[T.id]=de.data;const ye={};this.processInitOptions([T],{[T.id]:de.data},ye);const ie=ge();if(ie.bindingEngine){const _e=[T.id];T.initGlobal&&_e.push("_global"),T.initLocal&&_e.push("_local"),ie.bindingEngine.invalidateCacheByKeys(_e)}const xe={[T.id]:de.data};Object.keys(ye).length>0&&(xe._localInit=ye,Object.assign(B,ye)),T.initGlobal&&(xe._global={...this.globalState}),Z(xe)}else if(de.state==="error"&&de.error){const ye=de.error,ie=ye.response?.data?.message;O[T.id]={message:ie||de.error.message,status:ye.response?.status||ye.status},U.log(`Progressive data source error: ${T.id}`,O[T.id]),Z({[T.id]:null,_dataSourceErrors:{...O}})}}catch(ce){U.error(`Failed to fetch data source: ${T.id}`,ce)}});if(await Promise.all(H),n!==this.currentRouteChangeId){U.log("Route change cancelled after progressive fetch (newer request exists):",n);return}U.log("All progressive data sources loaded")}finally{zr.setPending(!1),this.hideTransitionOverlay()}}else this.hideTransitionOverlay();const Le=_.filter(Z=>Z.type==="websocket");if(Le.length>0){U.log("Subscribing WebSocket data sources:",Le.map(T=>T.id));const{updateTemplateData:Z,getState:ge}=await Promise.resolve().then(()=>Br),H={...this.currentFetchedData,...e.params,route:{...e.params||{},path:e.path},query:f,_global:{...this.globalState}};U.log("WebSocket binding context keys:",Object.keys(H)),this.currentWebSocketSubscriptions=v.subscribeWebSockets(_,(T,ce)=>{U.log(`WebSocket data received for: ${T}`,ce),this.currentFetchedData[T]=ce;const de=_.find(_e=>_e.id===T),ye=ge();if(ye.bindingEngine){const _e=[T];de?.initGlobal&&_e.push("_global"),de?.initLocal&&_e.push("_local"),ye.bindingEngine.invalidateCacheByKeys(_e)}Z({[T]:ce});const xe=_.find(_e=>_e.type==="websocket"&&(_e.target_source||_e.id)===T)?.onReceive;Array.isArray(xe)&&xe.length>0&&window.G7Core?.dispatch&&(async()=>{for(const Te of xe)try{const $e=this.getActionDispatcher?.();$e&&await $e.dispatchAction(Te,{navigate:this.getRouter?.()?(pt,Ct)=>this.getRouter().navigate(pt,Ct):void 0,setState:pt=>this.setGlobalState(pt),state:this.globalState,data:{...this.globalState,$args:[ce],$event:ce},_isDispatchFallbackContext:!0})}catch($e){U.error(`WebSocket onReceive action failed for ${T}:`,$e)}})()},H),U.log("WebSocket subscriptions established:",this.currentWebSocketSubscriptions)}U.log("Layout rendered successfully")}catch(a){U.error("Route change handling failed:",a),this.hideTransitionOverlay(),this.showRouteError(a)}}processInitOptions(e,n,a,i){e.some(c=>c.refetchOnMount===!0&&c.initLocal)&&(a._forceLocalInit=Date.now(),U.log("refetchOnMount detected, forcing local state init")),e.forEach(c=>{const d=n[c.id];if(!d)return;const f=d.data??d;if(c.initLocal){if(typeof c.initLocal=="string"){let h=f;if(c.initLocalDefaults&&typeof c.initLocalDefaults=="object"){const b=this.evaluateDefaults(c.initLocalDefaults,n);h={...b,...f},U.log(`initLocalDefaults applied for ${c.initLocal}:`,Object.keys(b))}const m=this.globalState._local?.[c.initLocal];m!==void 0&&typeof m=="object"&&typeof h=="object"?(a[c.initLocal]=this.deepMerge(m,h),U.log(`initLocal (merged): ${c.id}.data -> _local.${c.initLocal}`)):(a[c.initLocal]=h,U.log(`initLocal: ${c.id}.data -> _local.${c.initLocal}`))}else if(typeof c.initLocal=="object"&&c.initLocal.key){const{key:h,path:m}=c.initLocal;let b=m?this.extractValueByPathOrExpression(f,m,c.id):f;if(c.initLocalDefaults&&typeof c.initLocalDefaults=="object"){const v=this.evaluateDefaults(c.initLocalDefaults,n);b={...v,...b},U.log(`initLocalDefaults applied for ${h}:`,Object.keys(v))}const S=this.globalState._local?.[h];S!==void 0&&typeof S=="object"&&typeof b=="object"?(a[h]=this.deepMerge(S,b),U.log(`initLocal (merged): ${c.id}.data${m?"."+m:""} -> _local.${h}`)):(a[h]=b,U.log(`initLocal: ${c.id}.data${m?"."+m:""} -> _local.${h}`))}else if(typeof c.initLocal=="object"){const h=c.initLocal._merge||"deep",m=this.flattenNestedObjectToMappings(c.initLocal);for(const{targetPath:b,sourcePath:S}of m){const v=this.extractValueByPathOrExpression(f,S,c.id);if(b.includes(".")){const _=b.split(".")[0];a[_]===void 0&&this.globalState._local?.[_]!==void 0&&(a[_]=JSON.parse(JSON.stringify(this.globalState._local[_]))),this.setValueAtPath(a,b,v,h),U.log(`initLocal map (${h}): ${c.id} ${S} -> _local.${b}`)}else{const _=this.globalState._local?.[b];h==="replace"?(a[b]=v,U.log(`initLocal map (replace): ${c.id} ${S} -> _local.${b}`)):h==="shallow"?_!==void 0&&typeof _=="object"&&typeof v=="object"?(a[b]={..._,...v},U.log(`initLocal map (shallow): ${c.id} ${S} -> _local.${b}`)):(a[b]=v,U.log(`initLocal map: ${c.id} ${S} -> _local.${b}`)):_!==void 0&&typeof _=="object"&&typeof v=="object"?(a[b]=this.deepMerge(_,v),U.log(`initLocal map (deep): ${c.id} ${S} -> _local.${b}`)):(a[b]=v,U.log(`initLocal map: ${c.id} ${S} -> _local.${b}`))}}}}if(c.initGlobal)if(typeof c.initGlobal=="object"&&!Array.isArray(c.initGlobal)&&!("key"in c.initGlobal))for(const[m,b]of Object.entries(c.initGlobal)){if(typeof b!="string"){U.warn(`initGlobal map value must be string, got ${typeof b} for key ${m}`);continue}const S=this.extractValueByPathOrExpression(f,b,c.id),v=this.globalState[m];v!==void 0&&typeof v=="object"&&typeof S=="object"?(this.globalState[m]=this.deepMerge(v,S),U.log(`initGlobal map (merged): ${c.id} ${b} -> _global.${m}`)):(this.globalState[m]=S,U.log(`initGlobal map: ${c.id} ${b} -> _global.${m}`))}else{const m=Array.isArray(c.initGlobal)?c.initGlobal:[c.initGlobal];for(const b of m)if(typeof b=="string"){const S=this.globalState[b];S!==void 0&&typeof S=="object"&&typeof f=="object"?(this.globalState[b]=this.deepMerge(S,f),U.log(`initGlobal (merged): ${c.id}.data -> _global.${b}`)):(this.globalState[b]=f,U.log(`initGlobal: ${c.id}.data -> _global.${b}`))}else if(typeof b=="object"&&b.key){const{key:S,path:v}=b,_=v?this.extractValueByPathOrExpression(f,v,c.id):f,A=this.globalState[S];A!==void 0&&typeof A=="object"&&typeof _=="object"?(this.globalState[S]=this.deepMerge(A,_),U.log(`initGlobal (merged): ${c.id}.data${v?"."+v:""} -> _global.${S}`)):(this.globalState[S]=_,U.log(`initGlobal: ${c.id}.data${v?"."+v:""} -> _global.${S}`))}}if(i&&c.initIsolated){if(typeof c.initIsolated=="string"){const h=i[c.initIsolated];h!==void 0&&typeof h=="object"&&typeof f=="object"?(i[c.initIsolated]=this.deepMerge(h,f),U.log(`initIsolated (merged): ${c.id}.data -> _isolated.${c.initIsolated}`)):(i[c.initIsolated]=f,U.log(`initIsolated: ${c.id}.data -> _isolated.${c.initIsolated}`))}else if(typeof c.initIsolated=="object"&&c.initIsolated.key){const{key:h,path:m}=c.initIsolated,b=m?this.extractValueByPathOrExpression(f,m,c.id):f,S=i[h];S!==void 0&&typeof S=="object"&&typeof b=="object"?(i[h]=this.deepMerge(S,b),U.log(`initIsolated (merged): ${c.id}.data${m?"."+m:""} -> _isolated.${h}`)):(i[h]=b,U.log(`initIsolated: ${c.id}.data${m?"."+m:""} -> _isolated.${h}`))}else if(typeof c.initIsolated=="object")for(const[h,m]of Object.entries(c.initIsolated)){if(typeof m!="string"){U.warn(`initIsolated map value must be string, got ${typeof m} for key ${h}`);continue}const b=this.extractValueByPathOrExpression(f,m,c.id),S=i[h];S!==void 0&&typeof S=="object"&&typeof b=="object"?(i[h]=this.deepMerge(S,b),U.log(`initIsolated map (merged): ${c.id} ${m} -> _isolated.${h}`)):(i[h]=b,U.log(`initIsolated map: ${c.id} ${m} -> _isolated.${h}`))}}})}evaluateDefaults(e,n){const a={};for(const[i,l]of Object.entries(e))if(typeof l=="string"&&l.startsWith("{{")&&l.endsWith("}}")){const c=l.slice(2,-2).trim();a[i]=this.evaluateExpression(c,n)}else a[i]=l;return a}evaluateExpression(e,n){try{const a=e.split("??").map(i=>i.trim());for(const i of a){if(/^['"].*['"]$/.test(i))return i.slice(1,-1);if(i==="true")return!0;if(i==="false")return!1;if(/^-?\d+(\.\d+)?$/.test(i))return Number(i);const l=this.getNestedValue(n,i);if(l!=null)return l}return}catch(a){U.warn(`Expression evaluation failed: ${e}`,a);return}}getNestedValue(e,n){const a=n.replace(/\?\./g,".");let i=e;const l=a.split(/\.(?![^\[]*\])/).flatMap(c=>{const d=c.match(/^([^\[]*)((?:\[\d+\])*)$/);if(d){const[,f,h]=d,m=[];f&&m.push(f);const b=h.match(/\[\d+\]/g);return b&&m.push(...b),m}return[c]}).filter(c=>c!=="");for(const c of l){if(i==null)return;if(c.startsWith("[")&&c.endsWith("]")){const d=parseInt(c.slice(1,-1),10);i=i[d]}else i=i[c]}return i}extractValueByPathOrExpression(e,n,a){const i=Ta(n);if(i!==null){const l=new Tn,c={data:e,_global:this.globalState,_local:this.globalState._local||{}};try{const d=qn(i)?l.evaluatePipeExpression(i,c,{skipCache:!0}):l.evaluateExpression(i,c);return U.log(`initLocal/initGlobal expression evaluated: ${n} -> `,d),d}catch(d){U.warn(`initLocal/initGlobal expression evaluation failed for ${a}:`,n,d);return}}return this.getNestedValue(e,n)}async loadLayoutScripts(e,n){const a=[];for(const i of e){if((i.if!==void 0||i.conditions!==void 0)&&!as({if:i.if,conditions:i.conditions},n,this.bindingEngine,`script:${i.id}`)){U.log(`Script skipped (condition not met): ${i.id}`);continue}if(document.getElementById(i.id)){U.log(`Script already loaded: ${i.id}`);continue}if(!this.isAllowedScriptSrc(i.src)){U.warn(`Blocked untrusted external script src (same-origin path or declared trusted host required): ${i.id} (${i.src})`);continue}const c=new Promise((d,f)=>{const h=document.createElement("script");h.src=i.src,h.id=i.id,h.async=i.async??!0,h.onload=()=>{U.log(`Script loaded successfully: ${i.id}`),d()},h.onerror=()=>{U.warn(`Failed to load script: ${i.id} (${i.src})`),d()},document.head.appendChild(h)});a.push(c)}a.length>0&&(await Promise.all(a),U.log(`All scripts loaded: ${e.filter(i=>!document.getElementById(i.id)||a.length>0).map(i=>i.id).join(", ")}`))}isAllowedScriptSrc(e){if(typeof e!="string")return!1;const n=e.trim();if(n==="")return!1;const a=sr.normalizeScriptSrcForOriginCheck(n),i=a.startsWith("//"),l=/^[a-z][a-z0-9+.-]*:/i.test(a);if(!i&&!l&&a.startsWith("/"))return!0;const c=this.extractScriptHost(a);return c!==null&&this.getTrustedScriptHosts().includes(c)}static normalizeScriptSrcForOriginCheck(e){return e.replace(/[\t\n\r]/g,"").replace(/\\/g,"/").replace(/^([a-z][a-z0-9+.\-]*:)?\/{2,}/i,"$1//")}extractScriptHost(e){try{const n=e.startsWith("//")?`${window.location.protocol}${e}`:e,a=new URL(n,window.location.origin);return a.protocol!=="http:"&&a.protocol!=="https:"?null:a.hostname.toLowerCase()}catch{return null}}getTrustedScriptHosts(){const e=window.G7Config?.trustedScriptHosts;return Array.isArray(e)?e.map(n=>String(n).toLowerCase()):[]}evaluateScriptCondition(e,n){try{if(e.startsWith("{{")&&e.endsWith("}}")){const a=e.slice(2,-2).trim();return!!kd(a,n)}return!!e}catch(a){return U.warn(`Failed to evaluate script condition: ${e}`,a),!1}}showInitError(e){if(hd()){U.warn("Init failed while document is unloading — skipping error screen",e);return}const n=Hy(e);Mf.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 h=f.indexOf(":");h!==-1&&d.style.setProperty(f.slice(0,h).trim(),f.slice(h+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=yr.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 k=d.getBoundingClientRect(),O=window.scrollX||document.documentElement.scrollLeft,B=window.scrollY||document.documentElement.scrollTop;_.style.cssText=["position:absolute",`top:${k.top+B}px`,`left:${k.left+O}px`,`width:${k.width}px`,`height:${k.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 k=f==="fallback"?i:e;x=this.findComponentChildrenById(A,k)}const L=ld.createRoot(_);this.skeletonOverlayRoot=L,Oa.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.showTransitionOverlay({enabled:!0,style:"opaque",target:e});return}this.hideSkeletonOverlay();const c=l==="fullpage"?"app":l==="fallback"?a:e,d=`#${CSS.escape(c)}`,f=document.createElement("style");f.id="g7-skeleton-overlay-style",f.textContent=[`${d}{position:relative;}`,"@keyframes g7-spin{to{transform:rotate(360deg)}}"].join(""),document.head.appendChild(f),this.transitionOverlayEl=f;const h=n?.text||window.G7Core?.t?.("nav.loading")||"";this._spinnerState={target:e,fallbackTarget:a,spinnerConfig:n,resolvedText:h},this._mountSpinnerInTarget(i);const m=l==="fullpage"?"fullpage (#app)":l==="fallback"?`fallback (#${a})`:`target (#${e})`,b=n?.component||"default spinner";U.log(`Spinner overlay rendered [${m}] with "${b}"`)}_mountSpinnerInTarget(e){if(!this._spinnerState)return;const{spinnerConfig:n,resolvedText:a}=this._spinnerState;if(this.skeletonOverlayRoot){try{this.skeletonOverlayRoot.unmount()}catch{}this.skeletonOverlayRoot=null}if(this.skeletonOverlayContainer){try{this.skeletonOverlayContainer.remove()}catch{}this.skeletonOverlayContainer=null}const i=document.createElement("div");if(i.id="g7-skeleton-overlay",i.setAttribute("role","status"),i.setAttribute("aria-busy","true"),e.appendChild(i),this.skeletonOverlayContainer=i,n?.component){const c=yr.getInstance().getComponent(n.component);if(c){const d=ld.createRoot(i);this.skeletonOverlayRoot=d,Oa.flushSync(()=>{d.render(Ye.createElement(c,{options:{text:a}}))});return}U.log(`Spinner component "${n.component}" not found in registry, using default spinner`)}i.innerHTML='<div style="position:absolute;inset:0;z-index:2147483647;overflow:hidden;background:var(--g7-overlay-bg, rgb(249,250,251));display:flex;align-items:center;justify-content:center;"><div style="width:32px;height:32px;border:3px solid var(--g7-spinner-color, #9ca3af);border-top-color:transparent;border-radius:50%;animation:g7-spin 0.8s linear infinite;"></div></div>'}reattachSpinnerOverlay(){if(!this._spinnerState)return;const{target:e,fallbackTarget:n}=this._spinnerState;let a=document.getElementById(e);!a&&n&&(a=document.getElementById(n)),a||(a=document.getElementById("app")),a&&(this._mountSpinnerInTarget(a),U.log("Spinner overlay reattached to new DOM target"))}hideSkeletonOverlay(){if(this.skeletonOverlayRoot){try{this.skeletonOverlayRoot.unmount()}catch{}this.skeletonOverlayRoot=null}if(this.skeletonOverlayContainer){try{this.skeletonOverlayContainer.parentNode&&this.skeletonOverlayContainer.remove()}catch{}this.skeletonOverlayContainer=null}const e=document.getElementById("g7-skeleton-overlay-style");e&&e.remove(),this._spinnerState=null}showRouteError(e){if(e instanceof Ar&&e.details?.status===401){const a=this.config.templateId||"",i=window.location.pathname,l=a.includes("admin")||i.startsWith("/admin")?"admin":"user",c=i+window.location.search,d=!!Hr().getToken()||e.details?.hadToken===!0,f=Sr.getInstance().getLoginRedirectUrl(l,c,d?"session_expired":void 0);window.location.href=f;return}const n=Hy(e);Mf.renderFromError("app",n,"페이지 로딩 실패",this.config.debug,{templateId:this.config.templateId,locale:this.config.locale},void 0,e)}async handleServerError(){const e=window.G7Error;if(!e)return!1;U.warn("Server error detected:",e);try{return this.errorPageHandler&&(e.data&&(this.globalState.errorData=e.data),this.errorPageHandler.updateGlobalState(this.globalState),this.errorPageHandler.updateLocale(this.config.locale),await this.errorPageHandler.renderError(e.code,"app"))?(U.log(`${e.code} error page rendered successfully`),!0):(U.log(`Falling back to ErrorDisplay for ${e.code}`),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0)}catch(n){return U.error(`Failed to render ${e.code} page:`,n),this.showInitError(new Error(`Service unavailable (${e.code})`)),!0}}async handleRouteNotFound(e){U.warn("Route not found:",e);try{if(this.errorPageHandler&&(this.errorPageHandler.updateGlobalState(this.globalState),this.errorPageHandler.updateLocale(this.config.locale),await this.errorPageHandler.renderError(404,"app"))){U.log("404 error page rendered successfully");return}U.log("Falling back to ErrorDisplay for 404"),this.showRouteError(new Error(`Page not found: ${e}`))}catch(n){U.error("Failed to render 404 page:",n),this.showRouteError(new Error(`Page not found: ${e}`))}}getRouter(){return this.router}getConfig(){return this.config}getLayoutLoader(){return this.layoutLoader}async reloadExtensionState(){U.log("reloadExtensionState: start");let e;try{const n=await fetch(Ir(`/api/templates/${this.config.templateId}/config`,"json",null,`_=${Date.now()}`));if(n.ok){const a=await n.json();a?.success&&a?.data?.cache_version!==void 0&&(e=a.data.cache_version)}}catch(n){U.warn("reloadExtensionState: failed to fetch config.json",n)}e!==void 0&&e!==this.extensionCacheVersion&&(U.log(`reloadExtensionState: cache version ${this.extensionCacheVersion} -> ${e}`),this.extensionCacheVersion=e,this.saveCacheVersionToStorage(e));try{this.router&&(await this.router.loadRoutes(this.extensionCacheVersion),U.log("reloadExtensionState: routes reloaded"))}catch(n){U.error("reloadExtensionState: routes reload failed",n)}try{this.layoutLoader&&(this.extensionCacheVersion>0&&this.layoutLoader.setCacheVersion(this.extensionCacheVersion),this.layoutLoader.clear(),U.log("reloadExtensionState: layout cache cleared"))}catch(n){U.error("reloadExtensionState: layout cache clear failed",n)}try{const{TranslationEngine:n}=await Promise.resolve().then(()=>Ag),a=n.getInstance();this.extensionCacheVersion>0&&a.setCacheVersion(this.extensionCacheVersion);const i=this.config.locale||"ko",l="en";await a.loadTranslations(this.config.templateId,i,"/api",!0),i!==l&&await a.loadTranslations(this.config.templateId,l,"/api",!0),U.log("reloadExtensionState: translations reloaded")}catch(n){U.error("reloadExtensionState: translations reload failed",n)}try{const n=await fetch(`/api/locales/active?_=${Date.now()}`);if(n.ok){const i=(await n.json())?.data?.locales;if(Array.isArray(i)&&i.length>0){const l=this.globalState.appConfig??{};this.setGlobalState({appConfig:{...l,supportedLocales:i}}),U.log("reloadExtensionState: supportedLocales refreshed",i)}}}catch(n){U.warn("reloadExtensionState: supportedLocales refresh failed",n)}U.log("reloadExtensionState: done")}getActionDispatcher(){return na().actionDispatcher}async changeLocale(e){try{if(U.log("Changing locale to:",e),this.config.locale===e){U.log("Locale is already",e);return}this.config.locale=e,this.saveLocaleToStorage(e),await this.saveLocaleToDatabase(e),this.layoutLoader&&(this.layoutLoader.clear(),U.log("Layout cache cleared")),this.errorPageHandler&&(this.errorPageHandler.updateLocale(e),U.log("ErrorPageHandler locale updated")),kc(),await No({templateId:this.config.templateId,templateType:this.config.templateType,locale:e,debug:this.config.debug,cacheVersion:this.extensionCacheVersion}),U.log("Template Engine re-initialized with new locale");const{getActionDispatcher:n}=await Promise.resolve().then(()=>Br),a=n();a&&(a.setDefaultContext({navigate:i=>this.router?.navigate(i)}),a.setGlobalStateUpdater((i,l)=>this.setGlobalState(i,l)),U.log("Navigate function and setGlobalState re-injected to ActionDispatcher")),this.reinitializeModuleHandlers(),U.log("Module handlers re-initialized"),this.reinitializeTemplateHandlers(),U.log("Template handlers re-initialized"),this.reinitializePluginHandlers(),U.log("Plugin handlers re-initialized"),this.router&&this.router.navigateToCurrentPath(),U.log("Locale changed successfully to",e)}catch(n){throw U.error("Failed to change locale:",n),n}}loadLocaleFromStorage(){try{return localStorage.getItem(sr.LOCALE_STORAGE_KEY)}catch(e){return U.warn("Failed to load locale from storage:",e),null}}saveLocaleToStorage(e){try{localStorage.setItem(sr.LOCALE_STORAGE_KEY,e)}catch(n){U.warn("Failed to save locale to storage:",n)}}async saveLocaleToDatabase(e){const n=localStorage.getItem("auth_token");if(!n){U.log("No auth token, skipping DB locale save");return}let a,i;if(this.config.localeApi)a=this.config.localeApi.endpoint,i=this.config.localeApi.method;else{const l=this.config.templateType==="admin";a=l?"/api/admin/users/me/language":"/api/user/profile/update-language",i=l?"PATCH":"POST"}try{const l=this.getXsrfToken(),c=await fetch(a,{method:i,headers:{"Content-Type":"application/json",Accept:"application/json",...l&&{"X-XSRF-TOKEN":l},Authorization:`Bearer ${n}`},credentials:"include",body:JSON.stringify({language:e})});c.ok?U.log("Locale saved to DB:",e):U.warn("Failed to save locale to DB (UI will still change):",c.statusText)}catch(l){U.warn("Failed to call locale API (UI will still change):",l)}}getXsrfToken(){if(typeof document>"u")return null;const n=`; ${document.cookie}`.split("; XSRF-TOKEN=");return n.length===2?decodeURIComponent(n.pop()?.split(";").shift()||""):null}loadCacheVersionFromStorage(){try{const e=localStorage.getItem(sr.CACHE_VERSION_STORAGE_KEY);return e?parseInt(e,10):null}catch(e){return U.warn("Failed to load cache version from storage:",e),null}}saveCacheVersionToStorage(e){try{localStorage.setItem(sr.CACHE_VERSION_STORAGE_KEY,String(e))}catch(n){U.warn("Failed to save cache version to storage:",n)}}reinitializeModuleHandlers(){if(typeof window>"u")return;const e="__",n=window;Object.keys(n).forEach(a=>{if(a.startsWith(e)&&typeof n[a]?.initModule=="function")try{n[a].initModule(),U.log(`Module handler re-initialized: ${a}`)}catch(i){U.warn(`Failed to re-initialize module handlers for ${a}:`,i)}})}reinitializeTemplateHandlers(){if(typeof window>"u")return;const e=window.G7TemplateHandlers;if(!e){U.warn("Template handlers not found on window.G7TemplateHandlers");return}const n=this.getActionDispatcher();if(!n){U.warn("ActionDispatcher not available for template handler registration");return}Object.entries(e).forEach(([a,i])=>{n.registerHandler(a,i)}),U.log(`${Object.keys(e).length} template handler(s) re-registered:`,Object.keys(e))}reinitializePluginHandlers(){if(typeof window>"u")return;const e="__",n=window;Object.keys(n).forEach(a=>{if(a.startsWith(e)&&typeof n[a]?.initPlugin=="function")try{n[a].initPlugin(),U.log(`Plugin handler re-initialized: ${a}`)}catch(i){U.warn(`Failed to re-initialize plugin handlers for ${a}:`,i)}})}getLocale(){return this.config.locale}getErrorPageHandler(){return this.errorPageHandler}getGlobalState(){return{...this.globalState}}setGlobalState(e,n){const a={...this.globalState};typeof e=="function"?this.globalState=e(this.globalState):this.globalState={...this.globalState,...e},U.log("Global state updated:",this.globalState),window.G7Core?.devTools?.captureStateSnapshot?.({source:"setGlobalState",prev:a,next:this.globalState}),this.globalStateListeners.forEach(l=>{l(this.globalState)}),n?.render!==!1&&Promise.resolve().then(()=>Br).then(({updateTemplateData:l,getState:c})=>{const d=c();d.reactRoot&&d.currentLayoutJson&&l({_global:{...this.globalState},_local:this.globalState._local||{}})})}onGlobalStateChange(e){this.globalStateListeners.add(e)}offGlobalStateChange(e){this.globalStateListeners.delete(e)}async refetchDataSource(e,n){let a=this.currentDataSources.find(i=>i.id===e);if(!a)for(const[,i]of this.modalDataSources){const l=i.find(c=>c.id===e);if(l){a=l;break}}if(!a){U.warn(`Data source not found: ${e}`);return}U.log(`Refetching data source: ${e}`,n?.sync?"(sync mode)":"",n?.globalStateOverride?"(with global override)":"",n?.localStateOverride?"(with local override)":""),zr.setPending(!0);try{const i=new xr;this.currentGlobalHeaders.length>0&&i.setGlobalHeaders(this.currentGlobalHeaders);const l=na(),c=l.currentDataContext?._global||{},d=n?.globalStateOverride?{...c,...n.globalStateOverride}:c,f=l.currentDataContext?._local||{},h=n?.localStateOverride?{...f,...n.localStateOverride}:f,b=(await i.fetchDataSourcesWithResults([a],this.currentRouteParams,this.currentQueryParams,d,h,{ignoreAutoFetch:!0}))[0];if(b.state==="success"&&b.data!==void 0){this.currentFetchedData[e]=b.data;const S={};this.processInitOptions([a],{[e]:b.data},S);const v=na();if(v.bindingEngine){const A=[e];a.initGlobal&&A.push("_global"),a.initLocal&&A.push("_local"),v.bindingEngine.invalidateCacheByKeys(A)}const _={[e]:b.data};return Object.keys(S).length>0&&(_._localInit=S),a.initGlobal&&(_._global={...this.globalState}),ys(_,n?.sync?{sync:!0}:void 0),U.log(`Data source refetched successfully: ${e}`),b.data}else if(b.state==="error"){U.error(`Failed to refetch data source: ${e}`,b.error);return}}catch(i){U.error(`Error refetching data source: ${e}`,i);return}finally{zr.setPending(!1)}}registerModalDataSources(e,n){this.modalDataSources.set(e,n),U.log(`Modal data sources registered: ${e} (${n.length} sources)`)}unregisterModalDataSources(e){this.modalDataSources.delete(e),U.log(`Modal data sources unregistered: ${e}`)}getDataSource(e){return this.currentFetchedData[e]}setDataSource(e,n,a){if(!e){U.warn("setDataSource: dataSourceId is required");return}const{merge:i=!1,sync:l=!1}=a||{};i&&this.currentFetchedData[e]?this.currentFetchedData[e]={...this.currentFetchedData[e],...n}:this.currentFetchedData[e]=n,Promise.resolve().then(()=>Br).then(({updateTemplateData:c})=>{c({[e]:this.currentFetchedData[e]},l?{sync:!0}:void 0)}),U.log(`setDataSource: Updated ${e}`,i?"(merged)":"(replaced)")}updateDataSourceItem(e,n,a,i,l){const{idField:c="id",merge:d=!0,skipRender:f=!1}=l||{},h=this.currentFetchedData[e];if(!h)return U.warn(`updateDataSourceItem: DataSource '${e}' not found`),!1;const m=this.parseItemPath(n);let b=h;for(const _ of m)if(b=b?.[_],b===void 0)return U.warn(`updateDataSourceItem: Path '${n}' not found in dataSource`),!1;if(!Array.isArray(b))return U.warn(`updateDataSourceItem: Target at '${n}' is not an array`),!1;const S=b.findIndex(_=>String(_[c])===String(a));if(S===-1)return U.warn(`updateDataSourceItem: Item with ${c}='${a}' not found`),!1;d?b[S]=this.deepMerge(b[S],i):b[S]={...b[S],...i};const v=window.G7Core?.devTools;return v?.isEnabled?.()&&v.trackDataSourceUpdate?.({dataSourceId:e,updateType:"partial",itemPath:n,itemId:a,updates:i,timestamp:Date.now()}),f||Promise.resolve().then(()=>Br).then(({updateTemplateData:_})=>{_({[e]:this.currentFetchedData[e]},{sync:!1})}),U.log(`updateDataSourceItem: Updated ${e}.${n}[${c}=${a}]`),!0}parseItemPath(e){const n=[],a=/([^\.\[\]]+)|\[(\d+)\]/g;let i;for(;(i=a.exec(e))!==null;)i[1]!==void 0?n.push(i[1]):i[2]!==void 0&&n.push(parseInt(i[2],10));return n}deepMerge(e,n){if(n==null)return e;if(typeof n!="object"||Array.isArray(n))return n;const a={...e};for(const i of Object.keys(n))typeof n[i]=="object"&&n[i]!==null&&!Array.isArray(n[i])?a[i]=this.deepMerge(a[i]||{},n[i]):a[i]=n[i];return a}setValueAtPath(e,n,a,i="deep"){const l=n.split(".");let c=e;for(let h=0;h<l.length-1;h++){const m=l[h];(c[m]===void 0||c[m]===null||typeof c[m]!="object")&&(c[m]={}),c=c[m]}const d=l[l.length-1],f=c[d];i==="replace"||f===void 0?c[d]=a:i==="shallow"?typeof f=="object"&&typeof a=="object"&&!Array.isArray(a)?c[d]={...f,...a}:c[d]=a:typeof f=="object"&&typeof a=="object"&&!Array.isArray(a)?c[d]=this.deepMerge(f,a):c[d]=a}flattenNestedObjectToMappings(e,n=""){const a=[];for(const[i,l]of Object.entries(e)){if(i==="_merge")continue;const c=n?`${n}.${i}`:i;typeof l=="string"?a.push({targetPath:c,sourcePath:l}):typeof l=="object"&&l!==null&&!Array.isArray(l)&&a.push(...this.flattenNestedObjectToMappings(l,c))}return a}async updateQueryParams(e,n){U.log("updateQueryParams:",e);const a=new URL(e,window.location.origin),i=new URLSearchParams(a.search);window.history.replaceState(null,"",e),U.log("URL updated via replaceState"),this.currentQueryParams=i,U.log("currentQueryParams updated:",Object.fromEntries(i.entries()));const l=new xr,c=na().currentDataContext?._global||this.globalState||{},d={route:this.currentRouteParams||{},query:Uy(this.currentQueryParams),_global:c},f=Array.isArray(this.currentRawDataSources)&&this.currentRawDataSources.length>0?l.filterByCondition(this.currentRawDataSources,d):this.currentDataSources;this.currentDataSources=f;const h=f.filter(A=>A.auto_fetch!==!1&&A.type!=="websocket");if(h.length===0){U.log("No auto_fetch data sources to refetch");return}U.log(`Refetching ${h.length} auto_fetch data sources`),zr.setPending(!0);const m=na().currentLayoutJson,b=m?.transition_overlay,S=Array.isArray(b?.wait_for)?b.wait_for:[],v=h.some(A=>(A.loading_strategy||"progressive")==="blocking"),_=S.length>0&&h.some(A=>S.includes(A.id)&&A.type!=="websocket"&&(A.loading_strategy||"progressive")!=="background");if((v||_)&&b&&typeof b=="object"){const A=n?.transitionOverlayTarget||b.target;b.enabled&&b.style==="skeleton"&&b.skeleton?.component&&A?this.renderSkeletonOverlay(A,b.skeleton,m,b.fallback_target):b.enabled&&b.style==="spinner"&&A&&this.renderSpinnerOverlay(A,b.spinner,b.fallback_target)}try{const A=new xr;this.currentGlobalHeaders.length>0&&A.setGlobalHeaders(this.currentGlobalHeaders);const x=na(),L=x.currentDataContext?._global||{},M=await A.fetchDataSourcesWithResults(h,this.currentRouteParams,this.currentQueryParams,L,void 0,{ignoreAutoFetch:!1}),k={},O={},B=new Map(h.map(G=>[G.id,G]));for(const G of M){const P=B.get(G.id);if(!P){U.warn(`Refetch result id not found in autoFetchDataSources: ${G.id}`);continue}const W=G.id;G.state==="success"&&G.data!==void 0?(this.currentFetchedData[W]=G.data,k[W]=G.data,this.processInitOptions([P],{[W]:G.data},O),U.log(`Data source refetched: ${W}`)):G.state==="error"&&U.error(`Failed to refetch data source: ${W}`,G.error)}if(x.bindingEngine){const G=Object.keys(k);G.push("query"),x.bindingEngine.invalidateCacheByKeys(G)}if(Object.keys(O).length>0&&(k._localInit=O),Object.keys(k).length>0){if(k.query=Uy(this.currentQueryParams),x.currentLayoutJson?.computed&&Object.keys(x.currentLayoutJson.computed).length>0){const G={...x.currentDataContext,...k,query:k.query},P=this.calculateComputed(x.currentLayoutJson.computed,G);Object.keys(P).length>0&&(k._computed=P,this.globalState._computed=P,U.log("Computed values recalculated in updateQueryParams:",Object.keys(P)))}ys(k,{sync:!0}),U.log("Template data updated with new query params")}}catch(A){U.error("Error in updateQueryParams:",A)}finally{zr.setPending(!1),this.hideTransitionOverlay()}}calculateComputed(e,n){const a={},i=new Tn;for(const[l,c]of Object.entries(e))try{let d;if(this.isComputedSwitchDefinition(c))d=i.resolveSwitch(c,n,{skipCache:!0});else if(typeof c=="string")if(c.startsWith("{{")&&c.endsWith("}}")){const f=c.slice(2,-2).trim();d=this.evaluateComputedExpression(f,n)}else d=c;a[l]=d,U.log(`Computed ${l}:`,d)}catch(d){U.warn(`Failed to calculate computed value: ${l}`,d),a[l]=void 0}return a}isComputedSwitchDefinition(e){return e!==null&&typeof e=="object"&&!Array.isArray(e)&&"$switch"in e&&"$cases"in e}evaluateComputedExpression(e,n){try{return kd(e,n)}catch(a){U.warn(`Expression evaluation failed: ${e}`,a);return}}async executeInitActions(e,n={}){const{getActionDispatcher:a}=await Promise.resolve().then(()=>Br),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},h=i.createHandler(f,c),m=new Event("init");await h(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=Nf().getFailedJsAssets();if(d.length>0){const f=d.some(b=>b==="module"||b==="plugin"),h=n.filter(b=>!e.customHandlers?.has(b)),m=h.length>0&&h.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:h,failedAssets:d});return}}return U.log("Waiting for module handlers:",n),new Promise(f=>{const h=()=>{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(h,l)};h()})}};$(sr,"LOCALE_STORAGE_KEY","g7_locale"),$(sr,"CACHE_VERSION_STORAGE_KEY","g7_cache_version");let Do=sr;function jf(s){const e=new Do(s);return s.websocket&&(_c.configure(s.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=jf,window.G7Core.dataSource={refetch:async(s,e)=>{const n=window.__templateApp;if(!n){U.warn("TemplateApp not initialized (G7Core.dataSource.refetch)");return}return n.refetchDataSource(s,e)},get:s=>{const e=window.__templateApp;if(!e){U.warn("TemplateApp not initialized (G7Core.dataSource.get)");return}return e.getDataSource(s)}});function $A(s={}){const{checked:e,value:n,name:a,type:i="checkbox"}=s,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 NA(s={}){const{button:e=0,clientX:n=0,clientY:a=0}=s;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 jA(s,e="keydown"){return{key:s,code:s.length===1?`Key${s.toUpperCase()}`:s,preventDefault:()=>{},stopPropagation:()=>{},nativeEvent:new KeyboardEvent(e,{key:s}),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 HA={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-)/,display:/^(block|inline-block|inline|flex|inline-flex|table|inline-table|table-caption|table-cell|table-column|table-column-group|table-footer-group|table-header-group|table-row-group|table-row|flow-root|grid|inline-grid|contents|list-item|hidden)$/,position:/^(static|fixed|absolute|relative|sticky)$/,inset:/^(inset|top|right|bottom|left)-/,zIndex:/^z-/,width:/^w-/,minWidth:/^min-w-/,maxWidth:/^max-w-/,height:/^h-/,minHeight:/^min-h-/,maxHeight:/^max-h-/,padding:/^p[xytblr]?-/,margin:/^-?m[xytblr]?-/,space:/^space-(x|y)-/,fontSize:/^text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)$/,fontWeight:/^font-(thin|extralight|light|normal|medium|semibold|bold|extrabold|black)$/,fontStyle:/^(italic|not-italic)$/,fontFamily:/^font-(sans|serif|mono)/,textAlign:/^text-(left|center|right|justify|start|end)$/,textColor:/^text-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,textDecoration:/^(underline|overline|line-through|no-underline)$/,textTransform:/^(uppercase|lowercase|capitalize|normal-case)$/,lineHeight:/^leading-/,letterSpacing:/^tracking-/,textOverflow:/^(truncate|text-ellipsis|text-clip)$/,whitespace:/^whitespace-/,wordBreak:/^break-/,bgColor:/^bg-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,bgGradient:/^bg-gradient-/,bgSize:/^bg-(auto|cover|contain)$/,bgPosition:/^bg-(bottom|center|left|left-bottom|left-top|right|right-bottom|right-top|top)$/,bgRepeat:/^bg-(repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/,bgAttachment:/^bg-(fixed|local|scroll)$/,bgClip:/^bg-clip-/,bgOrigin:/^bg-origin-/,borderWidth:/^border(-[xytblr])?(-0|-2|-4|-8)?$/,borderColor:/^border-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,borderStyle:/^border-(solid|dashed|dotted|double|hidden|none)$/,borderRadius:/^rounded(-[tblrse]{1,2})?(-none|-sm|-md|-lg|-xl|-2xl|-3xl|-full)?$/,ringWidth:/^ring(-0|-1|-2|-4|-8|-inset)?$/,ringColor:/^ring-(inherit|current|transparent|black|white|slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-/,ringOffset:/^ring-offset-/,shadow:/^shadow(-sm|-md|-lg|-xl|-2xl|-inner|-none)?$/,opacity:/^opacity-/,mixBlend:/^mix-blend-/,bgBlend:/^bg-blend-/,blur:/^blur(-none|-sm|-md|-lg|-xl|-2xl|-3xl)?$/,brightness:/^brightness-/,contrast:/^contrast-/,grayscale:/^grayscale(-0)?$/,hueRotate:/^-?hue-rotate-/,invert:/^invert(-0)?$/,saturate:/^saturate-/,sepia:/^sepia(-0)?$/,backdropBlur:/^backdrop-blur-/,backdropBrightness:/^backdrop-brightness-/,backdropContrast:/^backdrop-contrast-/,backdropGrayscale:/^backdrop-grayscale-/,backdropHueRotate:/^backdrop-hue-rotate-/,backdropInvert:/^backdrop-invert-/,backdropOpacity:/^backdrop-opacity-/,backdropSaturate:/^backdrop-saturate-/,backdropSepia:/^backdrop-sepia-/,scale:/^scale(-x|-y)?-/,rotate:/^-?rotate-/,translate:/^-?translate-[xy]-/,skew:/^-?skew-[xy]-/,transformOrigin:/^origin-/,transition:/^transition(-none|-all|-colors|-opacity|-shadow|-transform)?$/,duration:/^duration-/,ease:/^ease-(linear|in|out|in-out)$/,delay:/^delay-/,animate:/^animate-/,cursor:/^cursor-/,userSelect:/^select-/,pointerEvents:/^pointer-events-/,resize:/^resize(-none|-x|-y)?$/,scrollBehavior:/^scroll-(auto|smooth)$/,touchAction:/^touch-/,overflow:/^overflow(-x|-y)?-(auto|hidden|clip|visible|scroll)$/,overscroll:/^overscroll(-x|-y)?-(auto|contain|none)$/,visibility:/^(visible|invisible|collapse)$/,aspectRatio:/^aspect-/,columns:/^columns-/,breakAfter:/^break-after-/,breakBefore:/^break-before-/,breakInside:/^break-inside-/,boxDecorationBreak:/^box-decoration-/,boxSizing:/^box-(border|content)$/,float:/^float-(right|left|none)$/,clear:/^clear-(left|right|both|none)$/,isolation:/^(isolate|isolation-auto)$/,objectFit:/^object-(contain|cover|fill|none|scale-down)$/,objectPosition:/^object-/};function Py(s){const e=s.replace(/^(dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+/,"");for(const[n,a]of Object.entries(HA))if(a.test(e))return n;return null}function By(s){const e=s.match(/^((dark:|hover:|focus:|active:|disabled:|group-hover:|sm:|md:|lg:|xl:|2xl:)+)/);return e?e[1]:""}function zA(s,e){if(!e||e.trim()==="")return s;if(!s||s.trim()==="")return e;const n=s.split(/\s+/).filter(Boolean),a=e.split(/\s+/).filter(Boolean),i=new Map;for(const c of a){const d=Py(c);if(d){const f=By(c),h=`${d}:${f}`;i.set(h,c)}}return[...n.filter(c=>{const d=Py(c);if(!d)return!0;const f=By(c),h=`${d}:${f}`;return!i.has(h)}),...a].join(" ")}function UA(s){return Object.entries(s).filter(([,e])=>e).map(([e])=>e).join(" ")}function PA(...s){return s.filter(e=>typeof e=="string"&&e.length>0).join(" ")}const be=dt("G7CoreGlobals");function BA(){window.React=Ye,window.ReactDOM={...cd,createPortal:Oa.createPortal,unstable_batchedUpdates:s=>s()},window.ReactJSXRuntime=jC,be.log("전역 객체 window.React, window.ReactDOM, window.ReactJSXRuntime에 노출됨")}function qA(s){const e=new Map,n=Ma.getInstance();s.componentEvent={on:(a,i)=>(e.has(a)||e.set(a,new Set),e.get(a).add(i),n.trackEventSubscribe(a),()=>{e.get(a)?.delete(i),n.trackEventUnsubscribe(a)}),emit:async(a,i)=>{const l=e.get(a),c=l?.size??0;if(!l||l.size===0)return n.trackEventEmit(a,i,0),[];let d,f=[];try{f=await Promise.all(Array.from(l).map(async h=>{try{return await h(i)}catch(m){throw be.error(`componentEvent: Error in listener for "${a}":`,m),m}}))}catch(h){d=h instanceof Error?h:new Error(String(h))}if(n.trackEventEmit(a,i,c,f,d),d)throw d;return f},off:a=>{e.delete(a),n.trackEventOff(a)},clear:()=>{e.clear(),n.trackEventClear()}},be.log("전역 객체 window.G7Core.componentEvent에 노출됨")}function GA(s){s.__runtime||(s.__runtime={DynamicRenderer:Pr,ComponentRegistry:yr,TranslationEngine:xa,DataSourceManager:xr,dataSourceManager:EA,DataBindingEngine:Tn,dataBindingEngine:Dd,ActionDispatcher:vo,TranslationReactContext:Tf,TranslationProvider:wc,useTranslation:Ay,ResponsiveContext:rf,ResponsiveProvider:oc,useResponsive:af,responsiveManager:mi,BREAKPOINT_PRESETS:Pl,AuthManager:Sr,createLogger:dt,G7DevToolsCore:Ma},be.log("전역 객체 window.G7Core.__runtime(코어 런타임 표면)에 노출됨"))}function VA(s){if(s.layoutEditor&&s.layoutEditor.__isStub!==!0||s.layoutEditor&&s.layoutEditor.__isStub===!0)return;const e=[],n=[];s.layoutEditor={__isStub:!0,__queue:e,__readyCallbacks:n,registerWidget:(a,i)=>e.push(["widget",a,i]),registerNodeEditor:(a,i)=>e.push(["nodeEditor",a,i]),registerCanvasOverlay:(a,i)=>e.push(["canvasOverlay",a,i]),onReady:a=>{typeof a=="function"&&n.push(a)}},be.log("전역 객체 window.G7Core.layoutEditor 예약 접수함(stub) 노출됨")}function FA(s,e){s.useTranslation=Ay,s.t=(n,a)=>{const i=e.getState();if(!i.translationEngine||!i.translationContext)return n;if(a){const l="|"+Object.entries(a).map(([c,d])=>`${c}=${d}`).join("|");return i.translationEngine.translate(n,i.translationContext,l)}return i.translationEngine.translate(n,i.translationContext)},be.log("전역 객체 window.G7Core.useTranslation에 노출됨")}function KA(s,e){s.createChangeEvent=$A,s.createClickEvent=NA,s.createSubmitEvent=IA,s.createKeyboardEvent=jA,s.uuid=()=>typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,n=>{const a=Math.random()*16|0;return(n==="x"?a:a&3|8).toString(16)}),s.createLogger=dt,be.log("전역 객체 window.G7Core.createLogger에 노출됨"),s.renderItemChildren=(n,a,i,l,c)=>{const d=e.getState(),m={_global:window.__templateApp?.getGlobalState?.()||{},_local:a._local||{},_computed:a._computed||{},...a},b=new Set,S=(_,A)=>{const x=m._global?._remountKeys;return _&&x?.[_]&&!b.has(_)?(b.add(_),`${_}-remount-${x[_]}`):_||A},v={translationContext:d.translationContext,translationEngine:d.translationEngine,bindingEngine:d.bindingEngine,actionDispatcher:d.actionDispatcher,getRemountKey:S,...c};return co(n,m,i,l,v)},s.evaluateCondition=(n,a)=>{if(!n)return!0;const i=e.getState(),c=window.__templateApp?.getGlobalState?.()||{},d={_global:c,_local:c._local||{},_computed:c._computed||{},...a||{}},f=i.bindingEngine??Dd;return zg(n,d,f)},s.getComponentMap=()=>yr.getInstance().getComponentMap(),s.renderComponentLayout=(n,a,i)=>{if(!n||!Array.isArray(n)||n.length===0)return null;const l=yr.getInstance().getComponentMap(),c=e.getState(),f=window.__templateApp?.getGlobalState?.()||{},h={_global:f._global||{},_local:f._local||{},_computed:f._computed||{},...a},m=new Set,b=(S,v)=>{const _=h._global?._remountKeys;return S&&_?.[S]&&!m.has(S)?(m.add(S),`${S}-remount-${_[S]}`):S||v};return co(n,h,l,i,{translationContext:c.translationContext,translationEngine:c.translationEngine,bindingEngine:c.bindingEngine,actionDispatcher:c.actionDispatcher,getRemountKey:b})},s.renderExpandContent=n=>{const{children:a,row:i,expandContext:l,componentContext:c,keyPrefix:d}=n;if(!a||!Array.isArray(a)||a.length===0)return null;const f=n.componentMap||yr.getInstance().getComponentMap(),h=e.getState(),m=s.state?.get?.()||{},b=c?.stateRef?.current??c?.state??{},S={...m._local||{},...b},v=c?.computedRef?.current??m._computed??{},_=h.bindingEngine??s.getDataBindingEngine?.(),A={...m,_local:S,_computed:v,row:i,item:i,$item:i};let x={};if(l&&typeof l=="object"&&_)try{for(const[O,B]of Object.entries(l))if(typeof B=="string"){const G=Ta(B);if(G!==null)try{x[O]=qn(G)?_.evaluatePipeExpression(G,A,{skipCache:!0}):_.evaluateExpression(G,A,{skipCache:!0})}catch{x[O]=void 0}else x[O]=_.resolveBindings(B,A,{skipCache:!0})}else x[O]=B}catch(O){be.warn("renderExpandContent: expandContext 평가 오류:",O)}const L={row:i,item:i,$item:i,_local:S,_global:m._global||{},_computed:m._computed||{},...x},M=new Set,k=(O,B)=>{const G=L._global?._remountKeys;return O&&G?.[O]&&!M.has(O)?(M.add(O),`${O}-remount-${G[O]}`):O||B};return co(a,L,f,d,{translationContext:h.translationContext,translationEngine:h.translationEngine??void 0,bindingEngine:h.bindingEngine??void 0,actionDispatcher:h.actionDispatcher,componentContext:c,getRemountKey:k})},s.$get=function(a,i,l=void 0){if(a==null)return l;const c=Array.isArray(i)?i:[i];if(c.length===0)return a;let d=a;for(const f of c){if(d==null||f==null)return l;d=d[f]}return d??l},be.log("전역 객체 window.G7Core.renderItemChildren에 노출됨"),be.log("전역 객체 window.G7Core.renderComponentLayout에 노출됨"),be.log("전역 객체 window.G7Core.renderExpandContent에 노출됨"),be.log("전역 객체 window.G7Core.$get에 노출됨")}function WA(s){s.dispatch=async(e,n)=>{const a=window.__templateApp;if(!a)return be.warn("G7Core.dispatch: TemplateApp이 초기화되지 않았습니다."),{success:!1,error:new Error("TemplateApp이 초기화되지 않았습니다.")};const i=a.getActionDispatcher?.();if(!i)return be.warn("G7Core.dispatch: ActionDispatcher를 찾을 수 없습니다."),{success:!1,error:new Error("ActionDispatcher를 찾을 수 없습니다.")};const l=a.getRouter?.(),c=a.getGlobalState?.(),d=a.setGlobalState?.bind(a),h=n?.componentContext||window.__g7ActionContext,m=window.__g7PendingLocalState;let b=h?.state??c;m&&h&&(b=m,be.log("[dispatch] Using __g7PendingLocalState for context.state:",m));let S=h?.data??c;m&&h?.data?._local&&(S={...h.data,_local:m},be.log("[dispatch] Updated context.data._local with pendingLocalState"));const v={navigate:l?(_,A)=>l.navigate(_,A):void 0,setState:h?.setState??d,state:b,data:S,_isDispatchFallbackContext:!h?.setState};if(e.debounce){const _=e.debounceKey||`dispatch-${e.handler}`,A=typeof e.debounce=="number"?e.debounce:e.debounce.delay;return i.debouncedCall(_,A,()=>{const{debounce:x,debounceKey:L,...M}=e;i.dispatchAction(M,v)}),{success:!0,debounced:!0}}try{return await i.dispatchAction(e,v)}catch(_){return be.error("G7Core.dispatch: 액션 실행 오류:",_),{success:!1,error:_ instanceof Error?_:new Error(String(_))}}},be.log("전역 객체 window.G7Core.dispatch에 노출됨")}function Oo(s){const e={};for(const[n,a]of Object.entries(s))if(n.includes(".")){const i=n.split(".");let l=e;for(let c=0;c<i.length-1;c++){const d=i[c];d in l?(typeof l[d]!="object"||l[d]===null)&&(l[d]={}):l[d]={},l=l[d]}l[i[i.length-1]]=a}else e[n]=a;return e}function qy(s,e=""){const n=[];if(!s||typeof s!="object")return n;for(const a of Object.keys(s)){const i=e?`${e}.${a}`:a,l=s[a];l&&typeof l=="object"&&!Array.isArray(l)?n.push(...qy(l,i)):n.push(i)}return n}function YA(s){const e=Object.keys(s);return e.length>0&&e.every(n=>/^\d+$/.test(n))}function Gy(s,e){const n={...s};for(const a of Object.keys(e))a in n?n[a]!==null&&typeof n[a]=="object"&&!Array.isArray(n[a])&&e[a]!==null&&typeof e[a]=="object"&&!Array.isArray(e[a])&&(n[a]=Gy(n[a],e[a])):n[a]=e[a];return n}function ir(s,e){if(Array.isArray(s)&&!Array.isArray(e)&&YA(e)){const a=Object.keys(e).map(l=>parseInt(l,10));if(!((a.length>0?Math.max(...a):0)>=s.length+a.length+10)){const l=[...s];for(const[c,d]of Object.entries(e)){const f=parseInt(c,10);f>=0&&f<l.length?d!==null&&typeof d=="object"&&!Array.isArray(d)&&l[f]!==null&&typeof l[f]=="object"?l[f]=ir(l[f],d):l[f]=d:f>=l.length&&(l[f]=d)}return l}}const n={...s};for(const[a,i]of Object.entries(e))i!==null&&typeof i=="object"&&!Array.isArray(i)?n[a]!==null&&typeof n[a]=="object"?n[a]=ir(n[a],i):n[a]={...i}:n[a]=i;return n}function XA(s){s.state={get:()=>window.__templateApp?.getGlobalState?.()||{},set:(e,n)=>{const a=window.__templateApp;if(a?.setGlobalState&&a?.getGlobalState){const i=n?.merge||"deep",l=Oo(e);let c;if(i==="replace")c=l;else if(i==="shallow")c={...a.getGlobalState(),...l};else{const d=a.getGlobalState();c=ir(d,l)}a.setGlobalState(c,{render:n?.render})}else be.warn("G7Core.state.set: TemplateApp이 초기화되지 않았습니다.")},setLocal:(e,n)=>{const a=window.__templateApp;if(n?.render===!1&&!n?.selfManaged){const x=window.__g7AutoBindingPaths;x&&x.size>0&&qy(Oo(e)).some(M=>x.has(M))&&(be.log("[setLocal] render:false + 자동바인딩 경로 겹침 감지 → render:true 자동 승격 (engine-v1.43.0)"),n={...n,render:!0})}if(n?.debounce){const x=a?.getActionDispatcher?.();if(x){const L=n.debounceKey||`setLocal-${Object.keys(e).join(",")}`,{debounce:M,debounceKey:k,...O}=n;x.debouncedCall(L,n.debounce,()=>{s.state.setLocal(e,O)});return}}const i=n?.scope??"current",l=n?.merge||"deep",c=window.__g7ActionContext,d=Oo(e);if(i==="parent"||i==="root"){const x=window.__g7LayoutContextStack||[];if(x.length>0){const L=i==="parent"?x[x.length-1]:x[0];if(L?.setState){L.setState(M=>l==="replace"?d:l==="shallow"?{...M||{},...d}:ir(M||{},d)),be.log(`[setLocal] scope=${i}: 타겟 컨텍스트에 상태 업데이트 (mergeMode=${l})`,d);return}}be.warn(`[setLocal] scope=${i}: 레이아웃 컨텍스트 스택이 비어있습니다. current로 폴백합니다.`)}const f=window.__g7PendingLocalState,h=a?.getGlobalState?.()?._local||{},S=!((window.__g7LayoutContextStack||[]).length>0)&&c?.state?c.state:void 0,v=S?Gy(h,S):h,_=f||v;let A;if(l==="replace"?A=d:l==="shallow"?A={..._,...d}:A=ir(_,d),a?.setGlobalState&&(a.setGlobalState({_local:A},{render:n?.render}),be.log(`[setLocal] globalLocal updated via setGlobalState (mergeMode=${l}, render=${n?.render??!0}):`,d)),window.__g7PendingLocalState=A,be.log("[setLocal] __g7PendingLocalState updated:",A),window.__g7SequenceLocalSync=A,window.__g7LastSetLocalSnapshot=A,l==="replace")window.__g7ForcedLocalFields=d;else{const x=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=ir(x,d)}if(be.log("[setLocal] __g7ForcedLocalFields updated:",d),l==="replace")window.__g7SetLocalOverrideKeys=d;else{const x=window.__g7SetLocalOverrideKeys||{};window.__g7SetLocalOverrideKeys=ir(x,d)}be.log("[setLocal] __g7SetLocalOverrideKeys updated:",d),c?.setState&&c.setState(x=>l==="replace"?d:l==="shallow"?{...x||{},...d}:ir(x||{},d)),a?.setGlobalState||be.warn("G7Core.state.setLocal: TemplateApp이 없습니다.")},getLocal:()=>{const a=(window.__templateApp?.getGlobalState?.()||{})._local||{},i=window.__g7PendingLocalState;if(i)return ir(a,i);const l=window.__g7LastSetLocalSnapshot;return l&&l!==a?ir(a,l):a},update:e=>{const n=window.__templateApp;if(n?.getGlobalState&&n?.setGlobalState){const a=n.getGlobalState(),i=e(a);n.setGlobalState(i)}else be.warn("G7Core.state.update: TemplateApp이 초기화되지 않았습니다.")},subscribe:e=>{const n=window.__templateApp;return n?.onGlobalStateChange?n.onGlobalStateChange(e):(be.warn("G7Core.state.subscribe: TemplateApp이 초기화되지 않았습니다."),()=>{})},getDataSource:e=>window.__templateApp?.getDataSource?.(e),getIsolated:e=>e?window.__g7IsolatedStates?.[e]?.state??null:window.__g7ActionContext?.isolatedContext?.state??null,setIsolated:(e,n,a)=>{let i,l,c;typeof e=="string"?(i=e,l=n||{},c=a):(l=e,c=n&&typeof n=="object"&&"merge"in n?n:a);const d=c?.merge||"deep",f=Oo(l);if(i){const b=window.__g7IsolatedStates?.[i];b?.mergeState?b.mergeState(f,d):be.warn(`G7Core.state.setIsolated: scopeId '${i}'를 찾을 수 없습니다.`);return}const h=window.__g7ActionContext;h?.isolatedContext?.mergeState?h.isolatedContext.mergeState(f,d):be.warn("G7Core.state.setIsolated: 액션 컨텍스트에 isolatedContext가 없습니다.")},getParent:()=>{const e=window.__g7LayoutContextStack||[];if(e.length===0)return be.log("[getParent] 레이아웃 컨텍스트 스택이 비어있습니다."),null;const n=e[e.length-1];if(!n)return null;const a=n.dataContext||{};return{_local:n.state||a._local||{},_global:a._global||s.state.get()||{},setState:n.setState}},setParentLocal:(e,n,a)=>{const i=window.__g7LayoutContextStack||[];if(i.length===0){be.warn("[setParentLocal] 레이아웃 컨텍스트 스택이 비어있습니다.");return}const l=i[i.length-1];if(!l?.setState){be.warn("[setParentLocal] 부모 컨텍스트에 setState가 없습니다.");return}let c,d;typeof e=="string"?(c={[e]:n},d=a):(c=e,d=n&&typeof n=="object"&&"merge"in n?n:a);const f=d?.merge||"deep",h=Oo(c),m=l.state?._local!==void 0,b=m?l.state._local:l.state||{};let S;f==="replace"?S=h:f==="shallow"?S={...b,...h}:S=ir(b,h),window.__g7PendingLocalState=S;const v=window.__templateApp;if(v?.setGlobalState&&v.setGlobalState({_local:S}),l.setState(S),m?l.state._local=S:l.state=S,l.dataContext&&(l.dataContext._local=S),f==="replace")window.__g7ForcedLocalFields=h;else{const _=window.__g7ForcedLocalFields||{};window.__g7ForcedLocalFields=ir(_,h)}ef()},setParentGlobal:(e,n)=>{let a;typeof e=="string"?a={[e]:n}:a=e,s.state.set(a),be.log("[setParentGlobal] 전역 상태 업데이트:",a)}},window.__g7IsolatedStates||(window.__g7IsolatedStates={}),s.state.getGlobal=s.state.get,s.state.setGlobal=s.state.set,be.log("전역 객체 window.G7Core.state에 노출됨")}function JA(s){s.dataSource={get:e=>window.__templateApp?.getDataSource?.(e),set:(e,n,a)=>{const i=window.__templateApp;i?.setDataSource?i.setDataSource(e,n,a):be.warn("G7Core.dataSource.set: TemplateApp이 초기화되지 않았습니다.")},refetch:async(e,n)=>{const a=window.__templateApp;if(a?.refetchDataSource)return a.refetchDataSource(e,n);be.warn("G7Core.dataSource.refetch: TemplateApp이 초기화되지 않았습니다.")},updateItem:(e,n,a,i,l)=>{const c=window.__templateApp;return c?.updateDataSourceItem?c.updateDataSourceItem(e,n,a,i,l):(be.warn("G7Core.dataSource.updateItem: TemplateApp이 초기화되지 않았습니다."),!1)},updateData:(e,n,a,i="append")=>{const l=window.__templateApp;if(!l?.getDataSource||!l?.setDataSource)return be.warn("G7Core.dataSource.updateData: TemplateApp이 초기화되지 않았습니다."),!1;const c=l.getDataSource(e);if(!c)return be.warn(`G7Core.dataSource.updateData: 데이터 소스 '${e}'를 찾을 수 없습니다.`),!1;let d,f=c,h=null;if(n){const b=n.split(".");h=b.pop();for(const S of b)if(f&&typeof f=="object"&&S in f)f=f[S];else return be.warn(`G7Core.dataSource.updateData: 경로 '${n}'를 찾을 수 없습니다.`),!1;d=f[h]}else d=c;if(!Array.isArray(d))return be.warn("G7Core.dataSource.updateData: 대상 경로의 데이터가 배열이 아닙니다."),!1;if(!Array.isArray(a))return be.warn("G7Core.dataSource.updateData: newData가 배열이 아닙니다."),!1;const m=i==="prepend"?[...a,...d]:[...d,...a];return n&&h?(f[h]=m,l.setDataSource(e,c,{merge:!1})):l.setDataSource(e,m,{merge:!1}),be.log(`G7Core.dataSource.updateData: '${e}'에 ${a.length}개 항목 ${i==="prepend"?"앞에":"뒤에"} 추가됨`),!0}},be.log("전역 객체 window.G7Core.dataSource에 노출됨")}function QA(s,e){s.locale={current:()=>window.__templateApp?.getLocale?.()||"ko",supported:()=>{const a=window.__templateApp?.globalState?.appConfig?.supportedLocales;return Array.isArray(a)&&a.length>0?a:e.getState().templateMetadata?.locales||["ko","en"]},change:async n=>{const a=window.__templateApp;a?.changeLocale?await a.changeLocale(n):be.warn("G7Core.locale.change: TemplateApp이 초기화되지 않았습니다.")}},be.log("전역 객체 window.G7Core.locale에 노출됨")}function ZA(s){s.toast={show:(e,n)=>{s.dispatch({handler:"toast",params:{message:e,type:n?.type||"info",...n?.duration&&{duration:n.duration}}})},success:(e,n)=>{s.toast.show(e,{type:"success",duration:n})},error:(e,n)=>{s.toast.show(e,{type:"error",duration:n})},warning:(e,n)=>{s.toast.show(e,{type:"warning",duration:n})},info:(e,n)=>{s.toast.show(e,{type:"info",duration:n})}},be.log("전역 객체 window.G7Core.toast에 노출됨")}function ex(s){s.modal={open:e=>{s.dispatch({handler:"openModal",target:e})},close:e=>{s.dispatch({handler:"closeModal",...e&&{target:e}})},closeAll:()=>{s.dispatch({handler:"closeAllModals"})},isOpen:e=>{const n=s.state.get();return n._global?.activeModal===e||n._global?.modalStack?.includes(e)||!1},getStack:()=>s.state.get()._global?.modalStack||[]},be.log("전역 객체 window.G7Core.modal에 노출됨")}function tx(s){s.style={mergeClasses:zA,conditionalClass:UA,joinClasses:PA},be.log("전역 객체 window.G7Core.style에 노출됨")}function nx(s,e){const{webSocketManager:n}=e;s.websocket={manager:n,subscribe:(a,i,l,c)=>n.subscribe(a,i,l,c),unsubscribe:a=>n.unsubscribe(a),leaveChannel:a=>n.leaveChannel(a),disconnect:()=>n.disconnect(),isInitialized:()=>n.isInitialized(),getSubscriptionCount:()=>n.getSubscriptionCount()},be.log("전역 객체 window.G7Core.websocket에 노출됨")}function rx(s,e){const{transitionManager:n}=e;s.navigation={isPending:()=>n?.getIsPending?.()||!1,onComplete:a=>{if(!n)return a(),()=>{};let i=n.getIsPending();const l=n.subscribe(c=>{c?i=!0:i&&(a(),l())});return l}},be.log("전역 객체 window.G7Core.navigation에 노출됨")}function ax(s){s.plugin={getSettings:e=>window.G7Config?.plugins?.[e],get:(e,n,a)=>window.G7Config?.plugins?.[e]?.[n]??a,getAll:()=>window.G7Config?.plugins??{}},be.log("전역 객체 window.G7Core.plugin에 노출됨")}function ix(s){s.module={getSettings:e=>window.G7Config?.modules?.[e],get:(e,n,a)=>window.G7Config?.modules?.[e]?.[n]??a,getAll:()=>window.G7Config?.modules??{}},be.log("전역 객체 window.G7Core.module에 노출됨")}function sx(s,e){const{transitionManager:n,responsiveManager:a}=e;s.AuthManager=Sr,s.api=Hr(),s.TransitionManager=n,s.useTransitionState=Im,s.ResponsiveManager=a,s.useResponsive=af,s.useControllableState=XC,s.shallowArrayEqual=JC,s.shallowObjectEqual=Hm,s.updateQueryParams=async(i,l)=>{const c=window.__templateApp;if(c?.updateQueryParams)return c.updateQueryParams(i,l);be.warn("G7Core.updateQueryParams: TemplateApp이 초기화되지 않았습니다.")},be.log("전역 객체 window.G7Core.AuthManager에 노출됨"),be.log("전역 객체 window.G7Core.api에 노출됨"),be.log("전역 객체 window.G7Core.ResponsiveManager에 노출됨"),be.log("전역 객체 window.G7Core.useResponsive에 노출됨"),be.log("전역 객체 window.G7Core.useControllableState에 노출됨"),be.log("전역 객체 window.G7Core.updateQueryParams에 노출됨")}function ox(s,e){s.getSlotContext=()=>window.__slotContextValue??null,s.getDynamicRenderer=()=>window.__DynamicRenderer??null,s.getComponentRegistry=()=>e.getComponentRegistry?.()??yr.getInstance(),s.getDataBindingEngine=()=>e.getDataBindingEngine?.()??e.getState().bindingEngine,s.getTranslationEngine=()=>e.getTranslationEngine?.()??e.getState().translationEngine,s.getActionDispatcher=()=>e.getActionDispatcher?.()??e.getState().actionDispatcher,be.log("전역 객체 window.G7Core 슬롯 API에 노출됨 (getSlotContext, getDynamicRenderer 등)")}function lx(s){s.identity={setLauncher:St.setLauncher.bind(St),handle:St.handle.bind(St),isIdentityRequired:St.isIdentityRequired.bind(St),hasLauncher:St.hasLauncher.bind(St),markDomainNoticeShown:St.markDomainNoticeShown.bind(St),redirectExternally:St.redirectExternally.bind(St),createDeferred:St.createDeferred.bind(St),resolveDeferred:St.resolveDeferred.bind(St),reset:St.reset.bind(St),redirectStashKey:Wd},be.log("전역 객체 window.G7Core 본인인증(IDV) API에 노출됨 (identity)")}function cx(s){if(typeof window>"u")return;BA(),window.G7Core||(window.G7Core={});const e=window.G7Core;GA(e),qA(e),VA(e),sx(e,s),FA(e,s),KA(e,s),WA(e),XA(e),JA(e),QA(e,s),ZA(e),ex(e),tx(e),nx(e,s),rx(e,s),ax(e),ix(e),ox(e,s),lx(e),be.log("G7Core 전역 객체 초기화 완료")}function ux(){const s=window.G7Core;if(!s){be.warn("G7Core가 초기화되지 않았습니다.");return}const e=()=>Ma.getInstance(),n={isEnabled:()=>{try{return e().isEnabled()}catch{return!1}},trackRender:a=>{try{e().isEnabled()&&e().trackRender(a)}catch{}},trackIteration:(a,i,l,c,d)=>{try{e().isEnabled()&&e().trackIteration(a,{source:i,itemVar:l,indexVar:c,sourceLength:d})}catch{}},trackIfCondition:(a,i,l,c)=>{try{e().isEnabled()&&e().trackIfCondition(a,i,l)}catch{}},trackMount:(a,i)=>{try{e().isEnabled()&&e().trackMount(a,{...i,id:a})}catch{}},trackUnmount:a=>{try{e().isEnabled()&&e().trackUnmount(a)}catch{}},trackExpressionEval:a=>{try{e().isEnabled()&&e().trackExpressionEval(a)}catch{}},trackBindingEval:a=>{try{e().isEnabled()&&e().trackBindingEval(a)}catch{}},recordCacheHit:()=>{try{e().isEnabled()&&e().recordCacheHit()}catch{}},recordCacheMiss:()=>{try{e().isEnabled()&&e().recordCacheMiss()}catch{}},trackHandlerRegistration:(a,i,l,c)=>{try{e().isEnabled()&&e().trackHandlerRegistration(a,i,l,c)}catch{}},trackHandlerUnregistration:a=>{try{e().isEnabled()&&e().trackHandlerUnregistration(a)}catch{}},logAction:a=>{try{e().isEnabled()&&e().logAction(a)}catch{}},trackRequest:(a,i)=>{try{return e().isEnabled()?e().trackRequest(a,i):""}catch{return""}},completeRequest:(a,i,l)=>{try{e().isEnabled()&&e().completeRequest(a,i,l)}catch{}},failRequest:(a,i)=>{try{e().isEnabled()&&e().failRequest(a,i)}catch{}},trackDataSourceDefinition:a=>{try{e().isEnabled()&&e().trackDataSourceDefinition(a)}catch{}},trackDataSourceLoading:a=>{try{e().isEnabled()&&e().trackDataSourceLoading(a)}catch{}},trackDataSourceLoaded:(a,i,l)=>{try{e().isEnabled()&&e().trackDataSourceLoaded(a,i,l)}catch{}},trackDataSourceError:(a,i)=>{try{e().isEnabled()&&e().trackDataSourceError(a,i)}catch{}},trackForm:(a,i,l)=>{try{e().isEnabled()&&e().trackForm(a,i,l)}catch{}},untrackForm:a=>{try{e().isEnabled()&&e().untrackForm(a)}catch{}},startStateChange:(a,i,l,c)=>{try{return e().isEnabled()?e().startStateChange(a,i,l,c):""}catch{return""}},completeStateChange:a=>{try{e().isEnabled()&&e().completeStateChange(a)}catch{}},trackComponentRender:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().trackComponentRender(a,i,l,c,d,f)}catch{}},updateLocalState:a=>{try{e().isEnabled()&&e().updateLocalState(a)}catch{}},updateComputedState:a=>{try{e().isEnabled()&&e().updateComputedState(a)}catch{}},updateParentContext:a=>{try{e().isEnabled()&&e().updateParentContext(a)}catch{}},trackComponentStateSource:(a,i,l,c)=>{try{e().isEnabled()&&e().trackComponentStateSource(a,i,l,c)}catch{}},trackDynamicState:(a,i)=>{try{e().isEnabled()&&e().trackDynamicState(a,i)}catch{}},trackContextFlow:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().trackContextFlow(a,i,l,c,d,f)}catch{}},trackComponentStyle:(a,i,l,c)=>{try{e().isEnabled()&&e().trackComponentStyle(a,i,l,c)}catch{}},trackAuthEvent:(a,i,l,c)=>{try{e().isEnabled()&&e().trackAuthEvent(a,i,l,c)}catch{}},trackAuthHeader:(a,i,l,c,d)=>{try{e().isEnabled()&&e().trackAuthHeader(a,i,l,c,d)}catch{}},trackLog:(a,i,l)=>{try{e().isEnabled()&&e().trackLog(a,i,l)}catch{}},trackAction:a=>{try{e().isEnabled()&&e().trackAction?.(a)}catch{}},trackDataSourceUpdate:a=>{try{e().isEnabled()&&e().trackDataSourceUpdate?.(a)}catch{}},startSequenceExecution:a=>{try{return e().isEnabled()?e().startSequenceExecution(a):""}catch{return""}},captureSequenceActionBefore:(a,i,l,c,d)=>{try{e().isEnabled()&&e().captureSequenceActionBefore(a,i,l,c,d)}catch{}},captureSequenceActionAfter:(a,i,l,c,d,f)=>{try{e().isEnabled()&&e().captureSequenceActionAfter(a,i,l,c,d,f)}catch{}},endSequenceExecution:(a,i)=>{try{e().isEnabled()&&e().endSequenceExecution(a,i)}catch{}},registerStateCaptureForHandler:(a,i,l)=>{try{e().isEnabled()&&e().registerStateCaptureForHandler(a,i,l)}catch{}},detectStaleClosure:(a,i,l,c,d)=>{try{return e().isEnabled()?e().detectStaleClosure(a,i,l,c,d):[]}catch{return[]}},trackStaleClosureWarning:a=>{try{e().isEnabled()&&e().trackStaleClosureWarning(a)}catch{}},trackModalOpen:a=>{try{e().isEnabled()&&e().trackModalOpen(a)}catch{}},trackModalClose:(a,i)=>{try{e().isEnabled()&&e().trackModalClose(a,i)}catch{}},trackModalStateChange:a=>{try{e().isEnabled()&&e().trackModalStateChange(a)}catch{}},trackNestedContext:a=>{try{return e().isEnabled()?e().trackNestedContext(a):""}catch{return""}},trackNestedContextAccess:(a,i)=>{try{e().isEnabled()&&e().trackNestedContextAccess(a,i)}catch{}},trackComputedProperty:(a,i,l,c,d,f,h)=>{try{e().isEnabled()&&e().trackComputedProperty(a,i,l,c,d,f,h)}catch{}},trackComputedRecalc:(a,i,l,c,d,f,h)=>{try{e().isEnabled()&&e().trackComputedRecalc(a,i,l,c,d,f,h)}catch{}},setNamedActionDefinitions:a=>{try{e().isEnabled()&&e().setNamedActionDefinitions(a)}catch{}},trackNamedActionRef:a=>{try{e().isEnabled()&&e().trackNamedActionRef(a)}catch{}}};s.devTools=n,be.log("G7Core.devTools 인터페이스 초기화 완료")}let Lo=null;function dx(){const s=window.G7Core;return s?.__devtools?Promise.resolve(s.__devtools):Lo||(Lo=new Promise((e,n)=>{const a=window.G7Config?.coreDevToolsAsset||"/build/core/devtools.min.js",i="g7-devtools-bundle",l=()=>{const f=window.G7Core?.__devtools;f?e(f):n(new Error("DevTools 번들 로드됨 — 그러나 __devtools 미노출"))};window.G7Core=window.G7Core||{},window.G7Core.__onDevToolsReady=l;const c=document.getElementById(i);if(c){c.addEventListener("load",l,{once:!0}),c.addEventListener("error",()=>n(new Error(`DevTools 번들 로드 실패: ${a}`)),{once:!0});return}const d=document.createElement("script");d.id=i,d.src=a,d.async=!1,d.addEventListener("load",l,{once:!0}),d.addEventListener("error",()=>{Lo=null,n(new Error(`DevTools 번들 로드 실패: ${a}`))},{once:!0}),document.head.appendChild(d)}),Lo)}function fx(){const s=window.G7Core;if(!s){be.warn("G7Core가 초기화되지 않았습니다.");return}const e=Ma.getInstance();if(e.initialize(),!e.isEnabled()){window.G7DevTools={isEnabled:()=>!1,enable:()=>{be.log("환경설정 > 고급 설정 > 디버그 모드를 켜세요")}},dd(),be.log("G7DevTools 비활성화됨 (디버그 모드 꺼짐)");return}hx(s,e)}async function hx(s,e){let n;try{n=await dx()}catch(h){be.warn("DevTools 번들 로드 실패 — 최소 API 로 폴백:",h),window.G7DevTools={isEnabled:()=>!1,enable:()=>{}},dd();return}const{DiagnosticEngine:a,getServerConnector:i,getStyleTracker:l}=n;ux(),dd();try{l().enable(),be.log("StyleTracker 활성화됨")}catch(h){be.warn("StyleTracker 활성화 실패:",h)}const c=new a,d=i(),f={isEnabled:()=>e.isEnabled(),state:{get:()=>e.getState(),getHistory:()=>e.getStateHistory(),watch:(h,m)=>e.watchState(h,m)},actions:{getHistory:()=>e.getActionHistory(),watch:h=>e.watchActions(h),getMetrics:()=>e.getActionMetrics()},binding:{evaluate:h=>{const m=s.getDataBindingEngine?.();if(!m){be.warn("DataBindingEngine이 없습니다.");return}return m.evaluateExpression(h,e.getState())},getCacheStats:()=>e.getCacheStats(),clearStats:()=>{e.resetCacheStats(),be.log("캐시 통계 초기화됨")}},diagnose:{analyze:h=>c.analyze(h),suggestFix:h=>c.suggestFix(h),getCommonIssues:()=>c.getCommonIssues(),getRulesByCategory:h=>c.getRulesByCategory(h)},server:{dumpState:h=>d.dumpState(h),sendLog:h=>d.sendLog(h),sendError:(h,m)=>d.sendError(h,m),isConnected:()=>d.isConnected(),testConnection:()=>d.testConnection()},config:{isDebugMode:()=>e.isEnabled(),setLogLevel:h=>e.setLogLevel(h),setMaxHistory:h=>e.setMaxHistory(h)},lifecycle:{getMountedComponents:()=>e.getLifecycleInfo().mountedComponents,getOrphanedListeners:()=>e.getLifecycleInfo().orphanedListeners},performance:{getRenderCount:()=>e.getPerformanceInfo().renderCounts,getBindingEvalCount:()=>e.getPerformanceInfo().bindingEvalCount,getMemoryWarnings:()=>e.getPerformanceInfo().memoryWarnings,startProfiling:()=>e.startProfiling(),stopProfiling:()=>e.stopProfiling()},network:{getActiveRequests:()=>e.getNetworkInfo().activeRequests,getRequestHistory:()=>e.getNetworkInfo().requestHistory,getPendingDataSources:()=>e.getNetworkInfo().pendingDataSources},conditional:{getIfConditions:()=>e.getConditionalInfo().ifConditions,getIterations:()=>e.getConditionalInfo().iterations},form:{getForms:()=>e.getFormInfo()},websocket:{getInfo:()=>e.getWebSocketInfo()}};window.G7DevTools=f,s.devtools=f,be.log("G7DevTools 전역 객체 초기화 완료 (window.G7DevTools)"),px(n.DevToolsPanel)}function px(s){if(document.getElementById("g7-devtools-root")){be.log("DevToolsPanel 이미 렌더링됨");return}const e=document.createElement("div");e.id="g7-devtools-root",document.body.appendChild(e);try{cd.createRoot(e).render(Ye.createElement(s)),be.log("DevToolsPanel UI 렌더링 완료")}catch(n){be.error("DevToolsPanel UI 렌더링 실패:",n)}}const Ze=dt("TemplateEngine"),ae={templateId:null,locale:"ko",isInitialized:!1,reactRoot:null,containerId:null,currentLayoutJson:null,currentDataContext:{},translationContext:{templateId:"",locale:"ko"},registry:null,bindingEngine:null,translationEngine:null,actionDispatcher:null,templateMetadata:null};let Mo=!1;const $o=[];async function gx(s){try{Ze.log("템플릿 메타데이터 로드 중...",s);const e=s.split("-").map(a=>a.split("_").map(i=>i.charAt(0).toUpperCase()+i.slice(1)).join("")).join(""),n=window[e];return n?.templateMetadata?(Ze.log("템플릿 메타데이터 로드 완료 (전역 변수)",n.templateMetadata),n.templateMetadata):(Ze.warn(`템플릿 번들에 메타데이터가 없습니다. 전역 변수: ${e}`),{identifier:s,locales:["ko","en"],name:{ko:s,en:s},description:{ko:"",en:""},version:"1.0.0",type:"admin"})}catch(e){return Ze.error("템플릿 메타데이터 로드 실패",e),{identifier:s,locales:["ko","en"],name:{ko:s,en:s},description:{ko:"",en:""},version:"1.0.0",type:"admin"}}}function Vy(){const e=(typeof window<"u"?window.__templateApp:void 0)?.globalState?.appConfig?.supportedLocales,n=ae.templateMetadata?.locales;return{$locale:ae.locale,$locales:Array.isArray(e)&&e.length>0?e:n||["ko","en"],$templateLocales:n||["ko","en"],$templateId:ae.templateId}}async function No(s){try{if(Ze.log("템플릿 엔진 초기화 시작",s),ae.isInitialized)throw new Error("템플릿 엔진이 이미 초기화되었습니다. destroyTemplate()을 먼저 호출하세요.");if(!s.templateId)throw new AA;Mo=s.debug??!1,typeof window<"u"&&(window.G7Config||(window.G7Config={}),window.G7Config.debug=Mo),fx(),ae.templateId=s.templateId,ae.locale=s.locale||"ko",Ze.log("엔진 인스턴스 생성 중..."),ae.registry=yr.getInstance(),ae.bindingEngine=new Tn,ae.translationEngine=xa.getInstance(),s.cacheVersion!==void 0&&s.cacheVersion>0&&ae.translationEngine.setCacheVersion(s.cacheVersion),ae.translationContext={templateId:s.templateId,locale:ae.locale},ae.actionDispatcher=new vo({},ae.translationEngine,ae.translationContext),PC(ae.actionDispatcher),Ze.log("다국어 파일 로드 중...",s.templateId,ae.locale);const e="en",n=[ae.translationEngine.loadTranslations(s.templateId,ae.locale).then(()=>{Ze.log(`다국어 파일 로드 완료: ${ae.locale}`)}).catch(a=>{Ze.warn(`다국어 파일 로드 실패 (${ae.locale}):`,a instanceof Error?a.message:a)})];ae.locale!==e&&n.push(ae.translationEngine.loadTranslations(s.templateId,e).then(()=>{Ze.log(`폴백 다국어 파일 로드 완료: ${e}`)}).catch(a=>{Ze.warn(`폴백 다국어 파일 로드 실패 (${e}):`,a instanceof Error?a.message:a)})),await Promise.allSettled(n),Ze.log("템플릿 메타데이터 로드 중...",s.templateId),ae.templateMetadata=await gx(s.templateId),ae.isInitialized=!0,Ze.log("템플릿 엔진 초기화 완료 (ComponentRegistry는 별도 로드)")}catch(e){throw Ze.error("템플릿 엔진 초기화 실패",e),ae.isInitialized=!1,ae.registry=null,ae.bindingEngine=null,ae.translationEngine=null,ae.actionDispatcher=null,e}}let Io=null;function Hf(){const s=window.G7Core;return s?.__LayoutEditorChrome?Promise.resolve(s.__LayoutEditorChrome):Io||(Io=new Promise((e,n)=>{const a=window.G7Config?.coreEditorAsset||"/build/core/layout-editor.min.js",i="g7-layout-editor-bundle",l=document.getElementById(i),c=()=>{const f=window.G7Core?.__LayoutEditorChrome;f?e(f):n(new Error("편집기 번들 로드됨 — 그러나 __LayoutEditorChrome 미노출"))};if(window.G7Core=window.G7Core||{},window.G7Core.__onChromeReady=c,l){l.addEventListener("load",c,{once:!0}),l.addEventListener("error",()=>n(new Error(`편집기 번들 로드 실패: ${a}`)),{once:!0});return}const d=document.createElement("script");d.id=i,d.src=a,d.async=!1,d.addEventListener("load",c,{once:!0}),d.addEventListener("error",()=>{Io=null,n(new Error(`편집기 번들 로드 실패: ${a}`))},{once:!0}),document.head.appendChild(d)}),Io)}async function ms(s){try{if(Ze.log("템플릿 렌더링 시작",s),!ae.isInitialized)throw new Error("템플릿 엔진이 초기화되지 않았습니다. initTemplateEngine()을 먼저 호출하세요.");if(!s.containerId)throw new Error("containerId는 필수입니다.");if(!s.layoutJson)throw new Error("layoutJson은 필수입니다.");const e=document.getElementById(s.containerId);if(!e)throw new Error(`컨테이너를 찾을 수 없습니다: #${s.containerId}`);const n=Vy();if(ae.containerId=s.containerId,ae.currentLayoutJson=s.layoutJson,ae.currentDataContext={...n,...s.dataContext||{}},ae.translationContext=s.translationContext||{templateId:ae.templateId||"",locale:ae.locale},ae.reactRoot||(Ze.log("React Root 생성"),ae.reactRoot=cd.createRoot(e)),typeof window<"u"){const l=zy(window.location.pathname);if(l){Ze.log("레이아웃 편집기 모드 진입",l);let c;try{c=await Hf()}catch(f){Ze.error("레이아웃 편집기 번들 로드 실패",f),Mf.render(s.containerId,{title:"레이아웃 편집기 로드 실패",message:f instanceof Error?f.message:"편집기 번들을 불러오지 못했습니다.",icon:"fas fa-triangle-exclamation",showStack:!1,showReloadButton:!0,debug:Mo});return}const d=Ye.createElement(c,{templateIdentifier:l.templateIdentifier,initialLocale:ae.locale});ae.reactRoot.render(Ye.createElement(wc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext,children:Ye.createElement(tf,{children:Ye.createElement(oc,{children:Ye.createElement(kf,{children:d})})})}));return}}const a=s.layoutJson.components||[];if(a.length===0){Ze.warn("렌더링할 컴포넌트가 없습니다.");return}const i=s.layoutJson.modals||[];if(Ze.log("modals 배열:",i),Ze.log("modals 개수:",i.length),Ze.log("DynamicRenderer로 렌더링 시작"),ae.reactRoot.render(Ye.createElement(wc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext},Ye.createElement(tf,null,Ye.createElement(oc,null,Ye.createElement(kf,null,[...a.map((l,c)=>{const d=ae.currentLayoutJson?.layout_name||"";return Ye.createElement(Pr,{key:d&&!l._fromBase?`${l.id}_${d}`:l.id,componentDef:l,dataContext:ae.currentDataContext,translationContext:ae.translationContext,registry:ae.registry,bindingEngine:ae.bindingEngine,translationEngine:ae.translationEngine,actionDispatcher:ae.actionDispatcher,isRootRenderer:c===0,layoutKey:d})}),Ye.createElement(Dm,{key:"__modal_parent_context_provider"},i.map(l=>{const c=ae.currentDataContext._global?.modalStack||[],f=c.includes(l.id)||ae.currentDataContext._global?.activeModal===l.id,h=c.indexOf(l.id),m=h>=0?50+h:50,b=window.__g7LayoutContextStack||[],v=b[b.length-1]?.dataContext,_=Ye.createElement(Pr,{key:`modal_${l.id}_renderer`,componentDef:{...l,props:{...l.props,isOpen:f,style:{...l.props?.style,zIndex:m},onClose:ae.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},ae.currentDataContext)}},dataContext:ae.currentDataContext,translationContext:ae.translationContext,registry:ae.registry,bindingEngine:ae.bindingEngine,translationEngine:ae.translationEngine,actionDispatcher:ae.actionDispatcher,parentDataContext:v});return l.data_sources&&l.data_sources.length>0?Ye.createElement(jy,{key:`modal_${l.id}`,isOpen:f,modalId:l.id,dataSources:l.data_sources,dataContext:ae.currentDataContext,globalStateUpdater:ae.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:ae.bindingEngine,debug:Mo,children:_}):_}))]))))),Ze.log("템플릿 렌더링 완료"),$o.length>0){Ze.log(`대기 중인 데이터 업데이트 ${$o.length}건 적용`);const l=[...$o];$o.length=0;for(const{data:c,options:d}of l)ys(c,d)}}catch(e){throw Ze.error("템플릿 렌더링 실패",e),e}}function ys(s,e){try{if(Ze.log("템플릿 데이터 업데이트 시작",Object.keys(s)),!ae.isInitialized)throw new Error("템플릿 엔진이 초기화되지 않았습니다.");if(!ae.reactRoot||!ae.currentLayoutJson){Ze.log("React 미준비 - 데이터 업데이트 큐잉:",Object.keys(s)),$o.push({data:s,options:e});return}const n=Vy(),a={...ae.currentDataContext._global||{},...s._global||{}},i=WC(ae.currentDataContext._localInit,s._localInit);ae.currentDataContext={...n,...ae.currentDataContext,...s,_global:a,_localInit:i},a._local!==void 0&&(ae.currentDataContext._local=a._local),a._computed!==void 0&&(ae.currentDataContext._computed=a._computed);const l=ae.currentLayoutJson.components||[],c=ae.currentLayoutJson.modals||[];Ze.log("updateTemplateData - modals 배열:",c),Ze.log("updateTemplateData - modals 개수:",c.length),Ze.log("updateTemplateData - activeModal:",ae.currentDataContext._global?.activeModal);const d=()=>{ae.reactRoot.render(Ye.createElement(wc,{translationEngine:ae.translationEngine,translationContext:ae.translationContext},Ye.createElement(tf,null,Ye.createElement(oc,null,Ye.createElement(kf,null,[...l.map((f,h)=>{const m=ae.currentLayoutJson?.layout_name||"";return Ye.createElement(Pr,{key:m&&!f._fromBase?`${f.id}_${m}`:f.id,componentDef:f,dataContext:ae.currentDataContext,translationContext:ae.translationContext,registry:ae.registry,bindingEngine:ae.bindingEngine,translationEngine:ae.translationEngine,actionDispatcher:ae.actionDispatcher,isRootRenderer:h===0,layoutKey:m})}),Ye.createElement(Dm,{key:"__modal_parent_context_provider_update"},c.map(f=>{const h=ae.currentDataContext._global?.modalStack||[],b=h.includes(f.id)||ae.currentDataContext._global?.activeModal===f.id,S=h.indexOf(f.id),v=S>=0?50+S:50,_=window.__g7LayoutContextStack||[],x=_[_.length-1]?.dataContext,L=Ye.createElement(Pr,{key:`modal_${f.id}_renderer`,componentDef:{...f,props:{...f.props,isOpen:b,style:{...f.props?.style,zIndex:v},onClose:ae.actionDispatcher?.createHandler({type:"click",handler:"closeModal"},ae.currentDataContext)}},dataContext:ae.currentDataContext,translationContext:ae.translationContext,registry:ae.registry,bindingEngine:ae.bindingEngine,translationEngine:ae.translationEngine,actionDispatcher:ae.actionDispatcher,parentDataContext:x});return f.data_sources&&f.data_sources.length>0?Ye.createElement(jy,{key:`modal_${f.id}`,isOpen:b,modalId:f.id,dataSources:f.data_sources,dataContext:ae.currentDataContext,globalStateUpdater:ae.actionDispatcher?.getGlobalStateUpdater(),bindingEngine:ae.bindingEngine,debug:Mo,children:L}):L}))])))))};e?.sync?(Ze.log("재렌더링 시작 (sync mode - 즉시 렌더링)"),d()):(Ze.log("재렌더링 시작 (with startTransition)"),N.startTransition(()=>{d()})),Ze.log("템플릿 데이터 업데이트 완료")}catch(n){throw Ze.error("템플릿 데이터 업데이트 실패",n),n}}function kc(){try{Ze.log("템플릿 정리 시작"),ae.reactRoot&&(Ze.log("React Root 언마운트"),ae.reactRoot.unmount(),ae.reactRoot=null),ae.templateId=null,ae.locale="ko",ae.isInitialized=!1,ae.containerId=null,ae.currentLayoutJson=null,ae.currentDataContext={},ae.translationContext={templateId:"",locale:"ko"},ae.registry=null,ae.bindingEngine=null,ae.translationEngine=null,ae.actionDispatcher=null,ae.templateMetadata=null,Ze.log("템플릿 정리 완료")}catch(s){throw Ze.error("템플릿 정리 실패",s),s}}function na(){return Object.freeze({...ae})}function Fy(){return ae.actionDispatcher}const jo={initTemplateEngine:No,renderTemplate:ms,updateTemplateData:ys,destroyTemplate:kc,getState:na};typeof window<"u"&&(window.G7Core||(window.G7Core={}),window.G7Core.TemplateEngine=jo,cx({getState:()=>({translationEngine:ae.translationEngine,translationContext:ae.translationContext,bindingEngine:ae.bindingEngine,actionDispatcher:ae.actionDispatcher,templateMetadata:ae.templateMetadata}),transitionManager:zr,responsiveManager:mi,webSocketManager:_c}),Ze.log("전역 객체 window.G7Core.TemplateEngine에 노출됨"));const Br=Object.freeze(Object.defineProperty({__proto__:null,DataSourceManager:xr,LayoutLoader:xf,TemplateApp:Do,TemplateEngine:jo,default:jo,destroyTemplate:kc,getActionDispatcher:Fy,getState:na,initTemplateApp:jf,initTemplateEngine:No,loadLayoutEditorBundle:Hf,renderTemplate:ms,updateTemplateData:ys},Symbol.toStringTag,{value:"Module"}));Xt.DataSourceManager=xr,Xt.LayoutLoader=xf,Xt.TemplateApp=Do,Xt.TemplateEngine=jo,Xt.default=jo,Xt.destroyTemplate=kc,Xt.getActionDispatcher=Fy,Xt.getState=na,Xt.initTemplateApp=jf,Xt.initTemplateEngine=No,Xt.loadLayoutEditorBundle=Hf,Xt.renderTemplate=ms,Xt.updateTemplateData=ys,Object.defineProperties(Xt,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})})(this.G7Core=this.G7Core||{}); diff --git a/resources/js/core/TemplateApp.ts b/resources/js/core/TemplateApp.ts index 5140963b..a9f92158 100644 --- a/resources/js/core/TemplateApp.ts +++ b/resources/js/core/TemplateApp.ts @@ -11,6 +11,7 @@ import type { Route } from './routing/Router'; import { LayoutLoader, LayoutLoaderError } from './template-engine/LayoutLoader'; import type { InitActionDefinition, LayoutScript, ComputedSwitchDefinition } from './template-engine/LayoutLoader'; import { DataBindingEngine } from './template-engine/DataBindingEngine'; +import { evaluateSafeExpression } from './template-engine/SafeExpressionEvaluator'; import { extractSingleBinding } from './template-engine/BindingShape'; import { hasPipes } from './template-engine/PipeRegistry'; import { evaluateRenderCondition } from './template-engine/helpers/RenderHelpers'; @@ -2039,6 +2040,17 @@ export class TemplateApp { continue; } + // 원격 스크립트 차단 (KVE-2026-1915 B-2 + 신뢰 출처 허용목록): src 는 same-origin + // path-only 이거나, 확장이 manifest 로 선언한 신뢰 호스트(G7Config.trustedScriptHosts)에 + // 속한 외부 스크립트만 허용한다. 미선언 외부 origin(`//`·scheme 포함)은 원격 코드 + // 로드 경로이므로 skip + 경고. (AuthManager.updateConfig loginPath same-origin 정책과 동형) + if (!this.isAllowedScriptSrc(script.src)) { + logger.warn( + `Blocked untrusted external script src (same-origin path or declared trusted host required): ${script.id} (${script.src})` + ); + continue; + } + // 스크립트 동적 로드 (Promise로 래핑) const loadPromise = new Promise<void>((resolve, reject) => { const scriptEl = document.createElement('script'); @@ -2070,6 +2082,118 @@ export class TemplateApp { } } + /** + * 레이아웃 스크립트 src 가 로드 허용 대상인지 판정합니다 + * (KVE-2026-1915 B-2 + 신뢰 출처 허용목록). + * + * 허용: + * 1. `/` 로 시작하는 same-origin 절대 경로. + * 2. 확장이 manifest(`trusted_script_hosts`)로 선언한 신뢰 호스트에 속한 외부 스크립트 + * — 코어가 집계해 `window.G7Config.trustedScriptHosts` 로 노출한다. 예: CKEditor5 + * (cdn.ckeditor.com), Daum 우편번호(t1.daumcdn.net). + * 차단: 그 외 `//`(protocol-relative)·scheme 포함 외부 origin(미선언 원격 코드 로드). + * + * @param src 스크립트 src 문자열 + * @returns 로드 허용이면 true + */ + private isAllowedScriptSrc(src: string): boolean { + if (typeof src !== 'string') { + return false; + } + + const trimmed = src.trim(); + + if (trimmed === '') { + return false; + } + + // 접두 검사 전에 브라우저 URL 파서와 동일하게 정규화한다 (아래 메서드 주석 참조) + const normalized = TemplateApp.normalizeScriptSrcForOriginCheck(trimmed); + + const isProtocolRelative = normalized.startsWith('//'); + const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(normalized); + + // same-origin path-only 절대 경로 (`/api/...`) — 항상 허용 + if (!isProtocolRelative && !hasScheme && normalized.startsWith('/')) { + return true; + } + + // 외부 origin — 확장이 선언한 신뢰 호스트만 허용 + const host = this.extractScriptHost(normalized); + + return host !== null && this.getTrustedScriptHosts().includes(host); + } + + /** + * origin 판정 전에 스크립트 src 를 브라우저 URL 파서와 동일하게 정규화합니다. + * + * 문자열 접두 검사만으로는 authority 우회를 막지 못합니다. 브라우저(WHATWG URL)는 + * 파싱 전에 ASCII tab·개행을 제거하고, special scheme(http/https)에서 백슬래시를 + * 슬래시와 동등하게 처리하기 때문입니다. 그래서 `/\/evil.com/x.js` · + * `/{tab}/evil.com/x.js` 는 `//` 로 시작하지 않고 scheme 도 없는데 실제로는 + * `https://evil.com/x.js` 로 해석되어 원격 스크립트가 로드됩니다. + * + * 정규화 후 판정하면 경로 중간의 백슬래시·탭(`/js/a\b.js`)은 authority 를 만들지 + * 않으므로 그대로 same-origin 으로 통과합니다(과차단 없음). + * + * 저장측 `SafeLayoutExpressions::normalizeForOriginCheck` · 정적 검사 + * `layout-scripts-src-same-origin` 과 3층 동형이어야 합니다. + * + * @since engine-v1.60.2 + * @param src 원본 src 문자열 + * @returns 정규화된 src + */ + private static normalizeScriptSrcForOriginCheck(src: string): string { + // ASCII tab / LF / CR 제거 (브라우저 파서가 파싱 전에 제거하는 문자) + // → 백슬래시를 슬래시로 (special scheme 에서 등가) + const slashed = src.replace(/[\t\n\r]/g, '').replace(/\\/g, '/'); + + // 선행 슬래시가 3개 이상이어도 브라우저는 authority 시작으로 접는다 + // (`///host/x` ≡ `//host/x`, `https:///host/x` ≡ `https://host/x`). + // 경로 중간의 연속 슬래시(`/js//a.js`)는 브라우저도 경로로 두므로 건드리지 않는다. + // @since engine-v1.60.3 + return slashed.replace(/^([a-z][a-z0-9+.\-]*:)?\/{2,}/i, '$1//'); + } + + /** + * 스크립트 src 에서 http(s) 호스트명을 추출합니다. + * + * `//host/...`(protocol-relative)·`https://host/...` 를 처리하며, http/https 가 아닌 + * scheme(`javascript:`·`data:` 등)은 null 을 반환해 신뢰 호스트 판정 대상에서 제외합니다. + * + * @param src 스크립트 src 문자열 + * @returns 소문자 호스트명 (판정 불가 시 null) + */ + private extractScriptHost(src: string): string | null { + try { + const normalized = src.startsWith('//') + ? `${window.location.protocol}${src}` + : src; + const url = new URL(normalized, window.location.origin); + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return null; + } + + return url.hostname.toLowerCase(); + } catch { + return null; + } + } + + /** + * 코어가 집계해 노출한 신뢰 외부 스크립트 호스트 목록을 반환합니다. + * + * @returns 소문자 호스트명 배열 (window.G7Config.trustedScriptHosts) + */ + private getTrustedScriptHosts(): string[] { + const hosts = (window as any).G7Config?.trustedScriptHosts; + + return Array.isArray(hosts) + ? hosts.map((host: unknown) => String(host).toLowerCase()) + : []; + } + /** * 스크립트 조건 평가 * @@ -2085,20 +2209,9 @@ export class TemplateApp { if (condition.startsWith('{{') && condition.endsWith('}}')) { const expression = condition.slice(2, -2).trim(); - // 간단한 표현식 평가 (점 표기법, 옵셔널 체이닝, 메서드 호출) - // Function 생성자를 사용하여 안전하게 평가 - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const fn = new Function('ctx', ` - with(ctx) { - try { - return Boolean(${expression}); - } catch (e) { - return false; - } - } - `); - - return fn(context); + // 화이트리스트 AST 평가기로 안전하게 평가(KVE-2026-1915). + // 종전의 `new Function('ctx','with(ctx){return Boolean(...)}')` 폐기. + return Boolean(evaluateSafeExpression(expression, context)); } // {{}} 형태가 아니면 truthy 체크 @@ -4060,19 +4173,10 @@ export class TemplateApp { */ private evaluateComputedExpression(expression: string, context: Record<string, any>): any { try { - // 안전한 표현식 평가를 위해 with 문과 Function 생성자 사용 - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const fn = new Function('ctx', ` - with(ctx) { - try { - return ${expression}; - } catch (e) { - return undefined; - } - } - `); - - return fn(context); + // 화이트리스트 AST 평가기로 안전하게 평가한다(KVE-2026-1915). + // 종전의 `new Function('ctx', 'with(ctx){return ...}')` 는 필터조차 거치지 않아 + // `''.constructor.constructor(...)` 샌드박스 탈출이 가능했으므로 폐기한다. + return evaluateSafeExpression(expression, context); } catch (error) { logger.warn(`Expression evaluation failed: ${expression}`, error); return undefined; diff --git a/resources/js/core/__tests__/DefinesComputed.test.ts b/resources/js/core/__tests__/DefinesComputed.test.ts index a807e12d..3ef67ab3 100644 --- a/resources/js/core/__tests__/DefinesComputed.test.ts +++ b/resources/js/core/__tests__/DefinesComputed.test.ts @@ -7,6 +7,7 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { evaluateSafeExpression } from '../template-engine/SafeExpressionEvaluator'; /** * calculateComputed 함수 로직을 테스트하기 위한 헬퍼 함수 @@ -43,22 +44,17 @@ function calculateComputed( /** * evaluateComputedExpression 함수 로직 + * + * TemplateApp.evaluateComputedExpression 과 동일하게 화이트리스트 AST + * 평가기(evaluateSafeExpression)에 위임한다. 폐기된 + * `new Function('ctx','with(ctx){…}')` 사본을 로컬에 두면 실제 싱크(안전 평가기)를 + * 검증하지 못하고, 엔진이 취약한 구현으로 회귀해도 이 테스트가 잡지 못한다 + * (KVE-2026-1915). 그래서 사본을 제거하고 실제 평가기를 직접 호출한다. */ function evaluateComputedExpression(expression: string, context: Record<string, any>): any { try { - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const fn = new Function('ctx', ` - with(ctx) { - try { - return ${expression}; - } catch (e) { - return undefined; - } - } - `); - - return fn(context); - } catch (error) { + return evaluateSafeExpression(expression, context); + } catch { return undefined; } } @@ -365,6 +361,35 @@ describe('Computed 기능', () => { }); }); +describe('보안 회귀 — computed 표현식 샌드박스 탈출 차단(KVE-2026-1915)', () => { + it("''.constructor.constructor 로 함수를 생성하려 하면 평가가 막혀 undefined 여야 함", () => { + expect( + evaluateComputedExpression("''.constructor.constructor('return 1')()", {}) + ).toBeUndefined(); + }); + + it('__proto__ 접근은 차단되어 undefined 여야 함', () => { + expect(evaluateComputedExpression('({}).__proto__', {})).toBeUndefined(); + }); + + it('prototype 접근은 차단되어 undefined 여야 함', () => { + expect(evaluateComputedExpression('[].constructor.prototype', {})).toBeUndefined(); + }); + + it('전역 Function/eval 참조는 차단되어 undefined 여야 함', () => { + expect(evaluateComputedExpression("Function('return 1')()", {})).toBeUndefined(); + expect(evaluateComputedExpression("eval('1')", {})).toBeUndefined(); + }); + + it('calculateComputed 경로에서도 위험 토큰은 undefined 로 폴백되어야 함', () => { + const result = calculateComputed( + { pwn: "{{''.constructor.constructor('return 2')()}}" }, + {} + ); + expect(result.pwn).toBeUndefined(); + }); +}); + describe('LayoutLoader defines/computed 인터페이스', () => { it('LayoutData 인터페이스에 defines 필드가 포함되어야 함', () => { // TypeScript 인터페이스 검증을 위한 타입 테스트 diff --git a/resources/js/core/__tests__/TemplateApp.safeComputed.test.ts b/resources/js/core/__tests__/TemplateApp.safeComputed.test.ts new file mode 100644 index 00000000..da131aa7 --- /dev/null +++ b/resources/js/core/__tests__/TemplateApp.safeComputed.test.ts @@ -0,0 +1,196 @@ +/** + * TemplateApp 안전 평가기 경로 테스트 (KVE-2026-1915 B-2) + * + * computed(`evaluateComputedExpression`) 와 scripts[].if(`evaluateScriptCondition`) + * 두 싱크가 실제 TemplateApp 경로에서 화이트리스트 AST 평가기(evaluateSafeExpression)를 + * 사용함을 검증한다. 종전의 `new Function('ctx','with(ctx){…}')` 로 회귀하면 + * `''.constructor.constructor('return …')()` 가 값으로 평가되어 아래 단언이 깨진다. + * + * DefinesComputed.test.ts 는 평가기 자체(evaluateSafeExpression)를 직접 호출하지만, + * 이 파일은 실제 TemplateApp private 메서드를 거쳐 "엔진이 이 싱크에서 안전 평가기를 + * 실제로 쓰는지" 를 고정한다. + * + * 효과 요약(마커 아님 — 평문): sandbox_escape_blocked, same_expression_same_value_across_paths. + * 실제 마커는 그 효과를 단언하는 개별 테스트에만 둔다 — 파일 레벨에 몰아 적으면 테스트를 + * 전부 지워도 커버리지가 green 으로 남는다. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { TemplateApp } from '../TemplateApp'; +import type { TemplateAppConfig } from '../TemplateApp'; +import { AuthManager } from '../auth/AuthManager'; + +const mockApiClient = { + post: vi.fn().mockResolvedValue({}), + get: vi.fn().mockResolvedValue({}), + removeToken: vi.fn(), + setToken: vi.fn(), + getToken: vi.fn().mockReturnValue(null), + setOnUnauthorized: vi.fn(), +}; + +vi.mock('../api/ApiClient', () => ({ + getApiClient: () => mockApiClient, +})); + +const { sharedActionDispatcher } = vi.hoisted(() => ({ + sharedActionDispatcher: { + setNavigate: vi.fn(), + setGlobalState: vi.fn(), + setDefaultContext: vi.fn(), + setGlobalStateUpdater: vi.fn(), + registerHandler: vi.fn(), + customHandlers: new Map(), + }, +})); + +vi.mock('../template-engine', () => ({ + initTemplateEngine: vi.fn().mockResolvedValue(undefined), + renderTemplate: vi.fn().mockResolvedValue(undefined), + destroyTemplate: vi.fn(), + getActionDispatcher: vi.fn().mockReturnValue(sharedActionDispatcher), + getState: vi.fn().mockReturnValue({ + actionDispatcher: sharedActionDispatcher, + reactRoot: null, + currentLayoutJson: null, + }), +})); + +vi.mock('../template-engine/TransitionManager', () => ({ + transitionManager: { + setPending: vi.fn(), + getIsPending: vi.fn(() => false), + subscribe: vi.fn(() => vi.fn()), + clearSubscribers: vi.fn(), + }, +})); + +vi.mock('../routing/Router', () => ({ + Router: vi.fn(function (this: any) { + this.loadRoutes = vi.fn().mockResolvedValue(undefined); + this.on = vi.fn(); + this.navigateToCurrentPath = vi.fn(); + this.getRoutes = vi.fn().mockReturnValue([]); + }), +})); + +vi.mock('../template-engine/LayoutLoader', async () => { + const actual = await vi.importActual<any>('../template-engine/LayoutLoader'); + return { + ...actual, + LayoutLoader: vi.fn(function (this: any) { + this.loadLayout = vi.fn().mockResolvedValue({ components: [] }); + }), + }; +}); + +vi.mock('../template-engine/ComponentRegistry', () => { + const mockInstance = { + loadComponents: vi.fn().mockResolvedValue(undefined), + getComponent: vi.fn().mockReturnValue(() => null), + hasComponent: vi.fn().mockReturnValue(true), + getInstance: vi.fn(), + }; + mockInstance.getInstance.mockReturnValue(mockInstance); + return { + ComponentRegistry: { + getInstance: vi.fn(() => mockInstance), + }, + }; +}); + +describe('TemplateApp 안전 평가기 경로 (computed / scripts[].if)', () => { + let app: TemplateApp; + + const build = (): TemplateApp => { + (window as any).G7Config = { trustedScriptHosts: [] }; + const config: TemplateAppConfig = { + templateId: 'sirsoft-admin_basic', + templateType: 'admin', + locale: 'ko', + debug: false, + }; + return new TemplateApp(config); + }; + + // computed 싱크: 이미 {{}} 가 제거된 내부 표현식을 받는다 + const computed = (a: TemplateApp, expr: string, ctx: Record<string, any> = {}): any => + (a as any).evaluateComputedExpression(expr, ctx); + + // scripts[].if 싱크: {{...}} 형태의 조건을 받아 boolean 을 돌려준다 + const condition = (a: TemplateApp, cond: string, ctx: Record<string, any> = {}): boolean => + (a as any).evaluateScriptCondition(cond, ctx); + + beforeEach(() => { + document.body.innerHTML = '<div id="app"></div>'; + Object.defineProperty(window, 'location', { + value: { + href: '', + origin: 'https://g7.test', + protocol: 'https:', + pathname: '/', + search: '', + }, + writable: true, + configurable: true, + }); + (AuthManager as any).instance = undefined; + (window as any).G7Core = { devTools: { trackAuthEvent: vi.fn() } }; + vi.clearAllMocks(); + app = build(); + }); + + afterEach(() => { + delete (window as any).G7Core; + delete (window as any).G7Config; + }); + + describe('computed 정상 평가', () => { + /** @effects same_expression_same_value_across_paths */ + it('속성 접근/산술/삼항/옵셔널 체이닝을 평가한다', () => { + expect(computed(app, 'user.name', { user: { name: '홍길동' } })).toBe('홍길동'); + expect(computed(app, 'price * qty', { price: 1000, qty: 3 })).toBe(3000); + expect(computed(app, "role === 'admin' ? 'Y' : 'N'", { role: 'admin' })).toBe('Y'); + expect(computed(app, "user?.profile?.name ?? '없음'", { user: { profile: null } })).toBe('없음'); + }); + }); + + describe('computed 보안 회귀 — 샌드박스 탈출 차단', () => { + /** @effects sandbox_escape_blocked */ + it("''.constructor.constructor 함수 생성은 undefined 로 막힌다", () => { + expect(computed(app, "''.constructor.constructor('return 1')()")).toBeUndefined(); + }); + + /** @effects sandbox_escape_blocked */ + it('전역 Function/eval 참조는 undefined 로 막힌다', () => { + expect(computed(app, "Function('return 1')()")).toBeUndefined(); + expect(computed(app, "eval('1')")).toBeUndefined(); + }); + + /** @effects sandbox_escape_blocked */ + it('__proto__/prototype 접근은 undefined 로 막힌다', () => { + expect(computed(app, '({}).__proto__')).toBeUndefined(); + expect(computed(app, '[].constructor.prototype')).toBeUndefined(); + }); + }); + + describe('scripts[].if 정상 평가', () => { + /** @effects same_expression_same_value_across_paths */ + it('{{...}} 조건을 boolean 으로 평가한다', () => { + expect(condition(app, '{{_global.on}}', { _global: { on: true } })).toBe(true); + expect(condition(app, '{{_global.on}}', { _global: { on: false } })).toBe(false); + expect(condition(app, "{{status === 'active'}}", { status: 'active' })).toBe(true); + }); + }); + + describe('scripts[].if 보안 회귀 — 샌드박스 탈출 차단', () => { + /** @effects sandbox_escape_blocked */ + it("''.constructor.constructor 조건은 false 로 막힌다", () => { + expect(condition(app, "{{''.constructor.constructor('return true')()}}")).toBe(false); + }); + + /** @effects sandbox_escape_blocked */ + it('전역 Function 참조 조건은 false 로 막힌다', () => { + expect(condition(app, "{{Function('return true')()}}")).toBe(false); + }); + }); +}); diff --git a/resources/js/core/__tests__/TemplateApp.scriptSrc.test.ts b/resources/js/core/__tests__/TemplateApp.scriptSrc.test.ts new file mode 100644 index 00000000..f293cc15 --- /dev/null +++ b/resources/js/core/__tests__/TemplateApp.scriptSrc.test.ts @@ -0,0 +1,256 @@ +/** + * TemplateApp.isAllowedScriptSrc 테스트 (KVE-2026-1915 B-2 + 신뢰 출처 허용목록) + * + * 배포 번들에서는 minify 로 이 함수를 이름으로 호출할 수 없어 E2E 는 결과(네트워크에 + * 허용목록 밖 스크립트가 없음)만 관찰한다. 결정 로직 자체는 여기서 잠근다. + * + * 효과 요약(마커 아님 — 평문): trusted_script_host_allowlist_wired, + * untrusted_external_script_blocked. 실제 마커는 그 효과를 단언하는 개별 테스트에만 둔다 — + * 파일 레벨에 몰아 적으면 테스트를 전부 지워도 커버리지가 green 으로 남는다. + * + * 레이아웃 `scripts[].src` 로더는 same-origin path 이거나, 확장이 manifest 로 선언해 + * 코어가 `window.G7Config.trustedScriptHosts` 로 노출한 신뢰 호스트에 속한 외부 스크립트만 + * 로드한다. 그 외 외부 origin(미선언 원격 코드)은 차단한다. + * + * 회귀 배경: 초기 B-2 는 모든 외부 origin 을 무조건 차단해 CKEditor5(cdn.ckeditor.com)· + * Daum 우편번호(t1.daumcdn.net) 등 번들 확장의 CDN 스크립트까지 막아 기능이 깨졌다. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { TemplateApp } from '../TemplateApp'; +import type { TemplateAppConfig } from '../TemplateApp'; +import { AuthManager } from '../auth/AuthManager'; + +const mockApiClient = { + post: vi.fn().mockResolvedValue({}), + get: vi.fn().mockResolvedValue({}), + removeToken: vi.fn(), + setToken: vi.fn(), + getToken: vi.fn().mockReturnValue(null), + setOnUnauthorized: vi.fn(), +}; + +vi.mock('../api/ApiClient', () => ({ + getApiClient: () => mockApiClient, +})); + +const { sharedActionDispatcher } = vi.hoisted(() => ({ + sharedActionDispatcher: { + setNavigate: vi.fn(), + setGlobalState: vi.fn(), + setDefaultContext: vi.fn(), + setGlobalStateUpdater: vi.fn(), + registerHandler: vi.fn(), + customHandlers: new Map(), + }, +})); + +vi.mock('../template-engine', () => ({ + initTemplateEngine: vi.fn().mockResolvedValue(undefined), + renderTemplate: vi.fn().mockResolvedValue(undefined), + destroyTemplate: vi.fn(), + getActionDispatcher: vi.fn().mockReturnValue(sharedActionDispatcher), + getState: vi.fn().mockReturnValue({ + actionDispatcher: sharedActionDispatcher, + reactRoot: null, + currentLayoutJson: null, + }), +})); + +vi.mock('../template-engine/TransitionManager', () => ({ + transitionManager: { + setPending: vi.fn(), + getIsPending: vi.fn(() => false), + subscribe: vi.fn(() => vi.fn()), + clearSubscribers: vi.fn(), + }, +})); + +vi.mock('../routing/Router', () => ({ + Router: vi.fn(function (this: any) { + this.loadRoutes = vi.fn().mockResolvedValue(undefined); + this.on = vi.fn(); + this.navigateToCurrentPath = vi.fn(); + this.getRoutes = vi.fn().mockReturnValue([]); + }), +})); + +vi.mock('../template-engine/LayoutLoader', async () => { + const actual = await vi.importActual<any>('../template-engine/LayoutLoader'); + return { + ...actual, + LayoutLoader: vi.fn(function (this: any) { + this.loadLayout = vi.fn().mockResolvedValue({ components: [] }); + }), + }; +}); + +vi.mock('../template-engine/ComponentRegistry', () => { + const mockInstance = { + loadComponents: vi.fn().mockResolvedValue(undefined), + getComponent: vi.fn().mockReturnValue(() => null), + hasComponent: vi.fn().mockReturnValue(true), + getInstance: vi.fn(), + }; + mockInstance.getInstance.mockReturnValue(mockInstance); + return { + ComponentRegistry: { + getInstance: vi.fn(() => mockInstance), + }, + }; +}); + +describe('TemplateApp.isAllowedScriptSrc (신뢰 출처 허용목록)', () => { + let app: TemplateApp; + + const build = (trustedScriptHosts: string[] = []): TemplateApp => { + (window as any).G7Config = { trustedScriptHosts }; + const config: TemplateAppConfig = { + templateId: 'sirsoft-admin_basic', + templateType: 'admin', + locale: 'ko', + debug: false, + }; + return new TemplateApp(config); + }; + + const allowed = (a: TemplateApp, src: string): boolean => + (a as any).isAllowedScriptSrc(src); + + beforeEach(() => { + document.body.innerHTML = '<div id="app"></div>'; + Object.defineProperty(window, 'location', { + value: { + href: '', + origin: 'https://g7.test', + protocol: 'https:', + pathname: '/', + search: '', + }, + writable: true, + configurable: true, + }); + (AuthManager as any).instance = undefined; + (window as any).G7Core = { devTools: { trackAuthEvent: vi.fn() } }; + vi.clearAllMocks(); + }); + + afterEach(() => { + delete (window as any).G7Core; + delete (window as any).G7Config; + }); + + /** @effects trusted_script_host_allowlist_wired */ + it('same-origin 절대 경로는 항상 허용된다', () => { + app = build([]); + expect(allowed(app, '/api/modules/x/widget.js')).toBe(true); + }); + + /** @effects untrusted_external_script_blocked */ + it('신뢰 목록이 비면 외부 origin 스크립트는 차단된다', () => { + app = build([]); + expect(allowed(app, 'https://cdn.ckeditor.com/ckeditor5/x.js')).toBe(false); + }); + + /** @effects trusted_script_host_allowlist_wired */ + it('선언된 신뢰 호스트의 scheme URL 스크립트는 허용된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, 'https://cdn.ckeditor.com/ckeditor5/43.3.1/ckeditor5.umd.js')).toBe(true); + }); + + /** @effects trusted_script_host_allowlist_wired */ + it('선언된 신뢰 호스트의 protocol-relative 스크립트도 허용된다', () => { + app = build(['t1.daumcdn.net']); + expect(allowed(app, '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js')).toBe(true); + }); + + /** @effects untrusted_external_script_blocked */ + it('신뢰 목록에 없는 외부 호스트는 선언 여부와 무관하게 차단된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, 'https://evil.com/x.js')).toBe(false); + expect(allowed(app, '//evil.com/x.js')).toBe(false); + }); + + /** @effects trusted_script_host_allowlist_wired */ + it('호스트 대소문자는 정규화되어 매칭된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, 'https://CDN.CKEditor.COM/x.js')).toBe(true); + }); + + /** @effects untrusted_external_script_blocked */ + it('javascript:·data: 등 비 http(s) scheme 은 신뢰 호스트여도 차단된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, 'javascript:alert(1)')).toBe(false); + expect(allowed(app, 'data:text/javascript,alert(1)')).toBe(false); + }); + + /** @effects untrusted_external_script_blocked */ + it('빈 문자열·공백은 차단된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, '')).toBe(false); + expect(allowed(app, ' ')).toBe(false); + }); + + // ========================================== + // authority 우회 (KVE-2026-1915 후속) + // ========================================== + // + // 브라우저 URL 파서는 (a) 파싱 전에 ASCII tab·개행을 제거하고 (b) special scheme 에서 + // 백슬래시를 슬래시와 동등하게 처리한다. 따라서 아래 형태들은 `//` 로 시작하지 않고 + // scheme 도 없으며 `/` 로 시작하지만, 실제로는 외부 origin 으로 해석되어 원격 스크립트가 + // 로드된다. 접두 문자열 검사만으로는 막을 수 없다. + // + // 실측(node `new URL(src, 'https://g7.test/')`): 6형태 전부 origin `https://evil.com`. + + const BS = String.fromCharCode(92); + const TAB = String.fromCharCode(9); + const LF = String.fromCharCode(10); + const CR = String.fromCharCode(13); + + it.each([ + ['슬래시-백슬래시-슬래시', '/' + BS + '/evil.com/x.js'], + ['슬래시-백슬래시', '/' + BS + 'evil.com/x.js'], + ['슬래시-이중백슬래시', '/' + BS + BS + 'evil.com/x.js'], + ['슬래시-탭-슬래시', '/' + TAB + '/evil.com/x.js'], + ['슬래시-LF-슬래시', '/' + LF + '/evil.com/x.js'], + ['슬래시-CR-슬래시', '/' + CR + '/evil.com/x.js'], + ])('authority 우회(%s)는 차단된다', (_name, src) => { + app = build([]); + expect(allowed(app, src)).toBe(false); + }); + + /** @effects trusted_script_host_allowlist_wired, untrusted_external_script_blocked */ + it('authority 우회로 도달한 호스트는 신뢰 목록에 있어야만 허용된다', () => { + // 정규화 후 호스트가 추출되므로, 신뢰 선언된 호스트면 허용·아니면 차단 (브라우저 해석과 일치) + app = build(['cdn.ckeditor.com']); + expect(allowed(app, '/' + BS + '/evil.com/x.js')).toBe(false); + expect(allowed(app, '/' + BS + '/cdn.ckeditor.com/x.js')).toBe(true); + }); + + /** @effects trusted_script_host_allowlist_wired */ + it('경로 중간의 백슬래시·탭은 authority 를 만들지 않으므로 허용된다', () => { + // 과차단 회귀 방지 — 브라우저도 same-origin 으로 해석한다(실측) + app = build([]); + expect(allowed(app, '/js/a' + BS + 'b.js')).toBe(true); + expect(allowed(app, '/js/c' + TAB + 'd.js')).toBe(true); + }); + + // 신뢰 호스트를 userinfo 로 위장하는 형태. 백슬래시가 슬래시로 접히면서 + // `https://evil.com/@cdn.ckeditor.com/x.js` 가 되어 실제 출처는 evil.com 이다. + // 저장측(TrustedScriptHosts::hostOf)이 정규화 없이 parse_url 로 읽으면 이 형태의 + // 호스트를 `cdn.ckeditor.com` 으로 보게 되므로, 세 계층이 같은 판정을 내는지 고정한다. + /** @effects untrusted_external_script_blocked */ + it('신뢰 호스트를 userinfo 로 위장한 src 는 차단된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, 'https://evil.com' + BS + '@cdn.ckeditor.com/x.js')).toBe(false); + expect(allowed(app, '//evil.com' + BS + '@cdn.ckeditor.com/x.js')).toBe(false); + }); + + // 브라우저는 선행 슬래시 런을 authority 시작으로 접는다(`///host` ≡ `//host`). + // 정규화가 이를 접지 않으면 런타임만 host 를 뽑고 저장측·정적검사는 못 뽑는 갈림이 생긴다. + /** @effects untrusted_external_script_blocked */ + it('선행 슬래시가 3개 이상이어도 authority 로 해석된다', () => { + app = build(['cdn.ckeditor.com']); + expect(allowed(app, '///cdn.ckeditor.com/x.js')).toBe(true); + expect(allowed(app, '///evil.com/x.js')).toBe(false); + }); +}); diff --git a/resources/js/core/api/ApiClient.ts b/resources/js/core/api/ApiClient.ts index 817906de..bf96dab7 100644 --- a/resources/js/core/api/ApiClient.ts +++ b/resources/js/core/api/ApiClient.ts @@ -155,8 +155,12 @@ class ApiClient { config.baseURL = ''; } + // 인증 토큰은 동일 출처 요청에만 첨부한다. 공개 자산 디스크(S3/CDN)를 켜면 + // 첨부 URL 이 외부 origin 절대 URL 이 되는데, 그 요청에 Authorization 을 실으면 + // ① 세션 토큰이 제3자 origin 의 접근 로그에 남고 + // ② 안전목록 밖 헤더라 preflight 가 발생해 CORS 미설정 CDN 에서는 이미지가 통째로 실패한다. const token = this.getToken(); - if (token && config.headers) { + if (token && config.headers && !this.isCrossOriginRequest(config.url)) { config.headers.Authorization = `Bearer ${token}`; } @@ -435,6 +439,32 @@ class ApiClient { } } + /** + * 요청 URL 이 현재 문서와 다른 출처인지 판정합니다. + * + * 상대 경로는 항상 동일 출처입니다. 절대 URL(`https://…`)과 프로토콜 상대 + * URL(`//host/…`)만 출처를 비교하며, 파싱 불가하거나 브라우저 밖(SSR/테스트) + * 이면 동일 출처로 간주해 기존 동작을 유지합니다. + * + * @param url 요청 URL (Axios config.url) + * @return bool 교차 출처 여부 + */ + private isCrossOriginRequest(url?: string): boolean { + if (!url || typeof window === 'undefined' || !window.location) { + return false; + } + + if (!/^(https?:)?\/\//i.test(url)) { + return false; + } + + try { + return new URL(url, window.location.href).origin !== window.location.origin; + } catch { + return false; + } + } + /** * 요청 설정에서 전체 URL을 생성합니다. * diff --git a/resources/js/core/api/__tests__/ApiClient.test.ts b/resources/js/core/api/__tests__/ApiClient.test.ts index 87535b27..1b57c900 100644 --- a/resources/js/core/api/__tests__/ApiClient.test.ts +++ b/resources/js/core/api/__tests__/ApiClient.test.ts @@ -274,4 +274,79 @@ describe('ApiClient', () => { ); }); }); + + // @scenario consumer=product, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row + // @effects cross_origin_asset_request_omits_session_token + describe('교차 출처 요청의 인증 헤더 차단', () => { + /** + * 요청 인터셉터를 캡처해 임의 config 로 실행합니다. + * + * @returns 캡처된 요청 인터셉터 + */ + function buildClientCapturingRequest(): (config: any) => any { + let captured: ((config: any) => any) | null = null; + mockedAxios.create.mockReturnValue({ + interceptors: { + request: { + use: vi.fn((onFulfilled: any) => { + captured = onFulfilled; + return 0; + }), + }, + response: { use: vi.fn(() => 0) }, + }, + get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), + }); + new ApiClient(); + return captured!; + } + + beforeEach(() => { + localStorage.setItem('auth_token', 'secret-token'); + }); + + it('동일 출처 상대 경로에는 Authorization 을 첨부한다', () => { + const onRequest = buildClientCapturingRequest(); + + const config = onRequest({ url: '/api/attachments/abc', headers: {}, baseURL: '/api' }); + + expect(config.headers.Authorization).toBe('Bearer secret-token'); + }); + + it('동일 출처 절대 URL 에는 Authorization 을 첨부한다', () => { + const onRequest = buildClientCapturingRequest(); + + const config = onRequest({ + url: `${window.location.origin}/api/attachments/abc`, + headers: {}, + baseURL: '/api', + }); + + expect(config.headers.Authorization).toBe('Bearer secret-token'); + }); + + it('교차 출처 절대 URL(공개 자산 CDN)에는 Authorization 을 첨부하지 않는다', () => { + const onRequest = buildClientCapturingRequest(); + + const config = onRequest({ + url: 'https://cdn.example.com/bucket/products/a.png', + headers: {}, + baseURL: '/api', + }); + + expect(config.headers.Authorization).toBeUndefined(); + }); + + it('프로토콜 상대 URL(//cdn) 도 교차 출처면 Authorization 을 첨부하지 않는다', () => { + const onRequest = buildClientCapturingRequest(); + + const config = onRequest({ + url: '//cdn.example.com/bucket/products/a.png', + headers: {}, + baseURL: '/api', + }); + + expect(config.headers.Authorization).toBeUndefined(); + }); + }); }); diff --git a/resources/js/core/template-engine/CHANGELOG.md b/resources/js/core/template-engine/CHANGELOG.md index a2047699..284a1ba9 100644 --- a/resources/js/core/template-engine/CHANGELOG.md +++ b/resources/js/core/template-engine/CHANGELOG.md @@ -5,6 +5,110 @@ > > 형식: [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/) +## [engine-v1.60.5] - 2026-08-19 + +### Fixed + +#### 화살표/함수 파라미터 배열 구조분해 미지원 회귀 (장바구니/바로구매 불능·권한 computed 공백) + +- engine-v1.60.0 의 SafeExpressionEvaluator 교체가 구 평가기(`new Function`)가 허용하던 **화살표·함수 파라미터의 배열 구조분해**(`([k, v]) =>`, `([, vid]) =>`)를 수용하지 않아, 해당 문법을 쓰는 표현식이 전부 `Expected ")" but found "=>"` 로 파싱 실패했다. 액션 params 는 미평가 원문 문자열 그대로 서버에 전송되어 422 가 되고(스토어프론트 장바구니 담기·바로 구매가 전 상품에서 불능), 게시판 환경설정의 권한 기본값 computed 4종이 계산되지 않아 권한 섹션이 비었으며, 플러그인 설정·이커머스 폼의 검증 오류 표시(`([field, messages]) => …`)도 발화 시점에 같은 실패에 걸리는 상태였다. 저장소 레이아웃 54개 파일 104곳이 이 문법을 사용 중이었다. +- `tryParseParamList` 에 배열 패턴(식별자 + 홀/elision, 파라미터 기본값 조합 포함)을 추가하고 `bindParams` 가 JS iterator 시맨틱(비-iterable 은 TypeError)으로 분해 바인딩하도록 했다. 중첩 패턴·rest·객체 패턴은 배포 레이아웃 사용 0건이라 지원하지 않는다(발견 시 arrow 아님으로 안전 되돌림). +- 회귀 잠금: 사용 형태 전수(단일/쌍/홀/기본값/function 선언 파라미터/실전 `_purchase_card.json` 본문·게시판 권한 computed·검증 오류 표시) 단위 테스트 + 배포 번들 E2E(`destructuring_param`) 추가. + +## [engine-v1.60.4] - 2026-08-14 + +### Security + +#### legacy 접근자를 통한 프로토타입 도달 차단 + +- `Object` facade 에서 `getPrototypeOf`/`setPrototypeOf`/`defineProperty` 를 제거했지만, 같은 능력을 **모든 객체가 상속으로 제공하는** `__lookupGetter__`/`__lookupSetter__`/`__defineGetter__`/`__defineSetter__` 로 되찾을 수 있었다. 이 4종은 프로퍼티를 **키가 아니라 문자열 인자**로 지목하므로 키 정규화(`normalizeKey`)를 원리상 거치지 않는다. 네 이름을 금지 프로퍼티에 추가해 dot·computed·문자열 조립 형태를 한꺼번에 막았다. +- 임의 코드 실행(RCE)으로는 이어지지 않았다 — `Function`/`eval` 도달은 여전히 `constructor` 키를 요구하고 그 경로는 이미 차단되어 있었다. 막은 것은 **페이지 전역 프로토타입 오염과 빌트인 메서드 변조/삭제로 인한 전역 장애**다. +- `Object.assign` 이 source 의 `__proto__` 키로 대상의 프로토타입을 바꾸던 경로도 닫았다. 네이티브 `assign` 은 대입(`[[Set]]`) 이라 `JSON.parse('{"__proto__":…}')` 결과를 합칠 때 setter 가 깨어났다 — 이제 항상 own 데이터 프로퍼티로 정의하며, 금지 키는 복사하지 않고 거부한다. +- 저장측 검증과 정적 검사에도 같은 패턴을 넣어 세 계층을 맞췄다. 배포 레이아웃 전수에서 이 4개 이름 사용은 0건이라 정상 표현식이 막히는 회귀는 없다(`toLocaleString`·`Object.assign`·`Object.create` 정당 사용은 그대로 동작). + +## [engine-v1.60.3] - 2026-08-14 + +### Security + +#### 선행 슬래시 런도 authority 로 접어 판정 (engine-v1.60.2 후속) + +- v1.60.2 의 정규화는 백슬래시를 슬래시로 바꾸기만 해서, `/\/host/x.js` 가 정규화 후 슬래시 3개(`///host/x.js`)가 됐다. 브라우저는 선행 슬래시가 몇 개든 authority 시작으로 접으므로(`///host` ≡ `//host`, `https:///host` ≡ `https://host`) 이 형태도 외부 호스트에서 로드된다. 정규화에 **선행 슬래시 런 접기**를 추가해 런타임·저장측·정적 검사 세 계층이 같은 호스트를 보도록 맞췄다. +- 경로 중간의 연속 슬래시(`/js//a.js`)는 브라우저도 경로로 두므로 건드리지 않는다(과차단 없음). +- 이 형태의 실질 영향은 저장측이었다 — 런타임은 `new URL` 로 호스트를 뽑아 이미 올바르게 판정하고 있었고, 저장측 신뢰 호스트 추출만 갈려 있었다(코어 `TrustedScriptHosts` 수정분 참조). + +## [engine-v1.60.2] - 2026-08-14 + +### Security + +#### `scripts[].src` same-origin 판정의 authority 우회 차단 (KVE-2026-1915 B-2 후속) + +- 원격 스크립트 차단이 `//` 접두·scheme 존재·`/` 시작이라는 **문자열 접두 검사**로만 same-origin 을 판정했다. 그런데 브라우저 URL 파서는 (a) 파싱 전에 ASCII tab·개행을 제거하고 (b) http/https 에서 백슬래시를 슬래시와 동등하게 처리하므로, `/\/evil.com/x.js` · `/\evil.com/x.js` · `/{tab}/evil.com/x.js` 같은 형태가 검사를 통과한 뒤 실제로는 `https://evil.com/x.js` 로 해석되어 **선언되지 않은 외부 스크립트가 그대로 로드**됐다(실측: `new URL` 로 6형태 전부 외부 origin 해석). +- 판정 전에 브라우저와 동일하게 정규화(tab·LF·CR 제거 → 백슬래시를 슬래시로)한 뒤 접두 검사를 적용하도록 고쳤다. 경로 중간의 백슬래시·탭(`/js/a\b.js`)은 authority 를 만들지 않으므로 종전대로 same-origin 으로 통과한다(과차단 없음). +- 저장측(`SafeLayoutExpressions`·`NoExternalUrls`)과 정적 검사(`layout-scripts-src-same-origin`)도 동일 정규화를 공유한다 — 세 계층이 같은 판정 로직을 쓰고 있었으므로 한 형태로 셋이 함께 뚫려 있었다. + +#### 화이트리스트 전역의 `Object.assign`/`freeze` 변조 차단 + +- `delete` 는 화이트리스트 전역(`Math`/`JSON`/`Date` 등)을 identity 로 차단하는데, facade 에 남긴 `assign`/`freeze` 는 대상을 검사하지 않아 `Object.assign(Math, { floor: … })` 로 **공유 전역을 영구 변조**할 수 있었다. 화이트리스트 전역은 실제 전역 참조를 노출하므로 그 변조는 페이지 전체(엔진·모듈·플러그인)에 지속된다. 두 메서드에도 `delete` 와 동일한 대상 검사를 적용했다. 일반 객체 대상 `assign`/`freeze` 는 그대로 동작한다. + +## [engine-v1.60.1] - 2026-08-14 + +### Security + +#### 화이트리스트 평가기의 비-문자열 computed 키 · Object 리플렉션 static 을 통한 샌드박스 탈출 차단 (KVE-2026-1915) + +- `SafeExpressionEvaluator` 의 프로퍼티 접근 하드닝이 **문자열 키만** 검사해, 배열/객체 키가 `''[['constructor']][['constructor']]('code')()` 처럼 JS ToPropertyKey 강제변환으로 `constructor` 에 도달하던 탈출을 막지 못했다. 키를 접근 전에 **1회 정규화**(심볼 외 `String()` 강제변환)한 뒤 금지 프로퍼티(`constructor`/`__proto__`/`prototype`)를 차단하고 그 정규화된 원시 키로만 접근하도록 고쳤다 — 배열·중첩 배열·객체 `toString` 강제변환·문자열 조립 등 모든 우회 형태가 접근 시점에 거부되며 재변환(TOCTOU) 여지도 없다. `evalMember`/`evalCall`/`delete` 세 경로 모두 적용. +- 화이트리스트 전역 `Object` 를 네이티브 그대로 노출해 `Object.getOwnPropertyDescriptor(Object.getPrototypeOf(String), 'constructor').value` 로 Function 에 도달하거나 `Object.setPrototypeOf`/`defineProperty` 로 프로토타입을 오염시킬 수 있었다. 이 static 들은 프로퍼티 키가 아니라 **문자열 인자**로 프로퍼티를 지목하므로 키 정규화로는 잡히지 않는다. 리플렉션·프로토타입·디스크립터 계열 static 을 제거하고 순수 데이터 계열(`keys`/`values`/`entries`/`assign`/`fromEntries`/`create`/`freeze`/`isFrozen`)만 노출하는 facade 로 교체했다. `create` 는 프로토타입/디스크립터를 읽지도 쓰지도 않아(신규 객체 생성만) 탈출 벡터가 아니며, 레이아웃이 `Object.assign(Object.create(null), …)` 로 정당하게 사용한다. +- 저장측(`app/Rules/SafeLayoutExpressions.php`)·정적 검사(`layout-expression-dangerous-token`)도 동형으로 넓혔다: 금지 프로퍼티 이름의 따옴표 문자열을 위치 무관 차단(computed 키 `['constructor']`·중첩 배열 키 `[['constructor']]`·리플렉션 문자열 인자 `…, 'constructor')` 포함), Object 리플렉션 static 이름 차단. 문자열 조립 난독화(`['const' + 'ructor']`)는 정적 토큰 매칭이 불가능하므로 런타임 인터프리터가 최종 게이트다. + +## [engine-v1.60.0] - 2026-08-14 + +### Fixed + +#### 표현식 평가기가 statement 본문(IIFE)을 거부해 저장이 원문 문자열로 전송되던 회귀 (KVE-2026-1915 후속) + +- engine-v1.59.0 에서 `new Function` → `SafeExpressionEvaluator`(AST 인터프리터)로 교체하며, 기존 30개 레이아웃이 쓰던 `(function() { const …; if (…) return {…}; })()` / `(() => { … })()` 형태의 **statement 본문 IIFE** 를 파싱 단계에서 거부하게 됐다. `DataBindingEngine.resolveBindings` 는 평가 실패 시 원본 문자열을 그대로 돌려주므로, 이 식을 body 로 쓰던 저장 액션은 미해석 `{{…}}` 문자열을 서버로 전송했고 서버는 아무것도 저장하지 못한 채 성공(200)을 반환했다. 발현: 이커머스 설정에서 문의 게시판을 지정해도 저장되지 않고, 게시판/관리자 설정 저장, 카테고리 부모 선택 옵션, 배송비 단위 표기 등 동일 형태를 쓰던 화면이 조용히 동작하지 않았다(예외·경고 없음). +- `SafeExpressionEvaluator` 에 함수/화살표 **블록 본문 해석**을 추가했다: 함수 표현식(`function (…) { … }`, 명명 함수 재귀 포함)·화살표 블록 본문·`const`/`let` 선언·`if`/`else`·`for…of`(+`break`/`continue`)·`return`·`try`/`catch`/`finally`·`delete`·기본 파라미터. 모두 인터프리터가 트리워킹으로 실행하며 코드 문자열 컴파일(`eval`/`new Function`)은 여전히 쓰지 않는다. +- 보안 경계는 그대로다 — KVE-2026-1915 의 탈출 벡터는 "function 키워드" 자체가 아니라 `''.constructor.constructor('code')()` 같은 **프로퍼티 체인을 통한 Function 생성자 접근**이었고, 그 차단(`constructor`/`__proto__`/`prototype` 접근, `Function`/`eval`/`Reflect` 등 위험 전역, 비화이트리스트 `new`)은 statement 본문 안에서도 동일하게 적용된다. 함수 표현식은 네이티브 컴파일이 아니라 해석기 클로저로만 실행되므로 새 탈출 경로가 생기지 않는다. 추가로 `delete` 는 화이트리스트 전역 객체(`Math`/`JSON`/`Array` 등)의 프로퍼티를 지우지 못하도록 identity 로 차단한다. 대입(`=`)·증감(`++`/`--`)·복합대입·sequence(`,`)·비트/시프트/거듭제곱 연산자는 계속 거부한다. + +## [engine-v1.59.0] - 2026-08-13 + +### Security + +#### 레이아웃 표현식 평가를 화이트리스트 AST 인터프리터로 교체 (KVE-2026-1915) + +- 신규 `SafeExpressionEvaluator.ts` — 표현식 문자열을 토크나이저 → Pratt 파서 → AST 트리워킹 인터프리터로 해석한다. `eval` / `new Function` / `with(ctx)` 를 일절 쓰지 않으므로 `''.constructor.constructor('code')()` 형태의 샌드박스 탈출(CWE-184/CWE-94)이 원천 차단된다. +- 위험 지점 3곳을 이 평가기로 통일했다: `DataBindingEngine.evaluateExpression`(주 평가 경로, `TranslationEngine` 의 `$t:` 파라미터 평가 포함 200여 소비처), `TemplateApp.evaluateComputedExpression`(computed), `TemplateApp.evaluateScriptCondition`(scripts[].if). 종전 `evaluateComputedExpression`/`evaluateScriptCondition` 은 `with(ctx)` 로 필터조차 거치지 않아 더 위험했다. +- 차단: `constructor`/`__proto__`/`prototype` 프로퍼티 접근(dot·문자열 리터럴 computed·런타임 해석된 computed 키 모두), `Function`/`eval`/`globalThis`/`window`/`Reflect`/`Proxy` 등 위험 전역, 함수 생성(`function` 키워드), 할당·증감, 비트/시프트/거듭제곱 연산자. +- 호환 유지: 화살표 함수(인터프리터 클로저로 실행), 스프레드(배열/객체/호출인자), optional chaining, 템플릿 리터럴(`${}` 은 같은 인터프리터로 해석), 화이트리스트 생성자의 `new`(`new Date(...)`·`Array.from(new Set(...))` 등 — `Date`/`Set`/`Map`/`WeakSet`/`WeakMap`/`Array` 등만 허용, `new Function` 은 차단), 화이트리스트 전역(`Math`/`JSON`/`Date`/`Array`/`Object`/`Number`/`String`/`Boolean`/`parseInt`/`parseFloat`/`isNaN`/`isFinite`) 및 인스턴스 메서드 호출. +- 컨텍스트 값이 항상 전역보다 우선한다. 미존재 식별자는 `with(ctx)` 시맨틱대로 `undefined` 를 반환한다(예외 없음). + +#### `scripts[].src` 원격 스크립트 차단 + 신뢰 출처 허용목록 (KVE-2026-1915 B-2) + +- `TemplateApp.loadLayoutScripts` — 레이아웃 스크립트 `src` 는 same-origin path-only(`/` 시작)만 로드한다. `//`(protocol-relative)·scheme 포함 절대 URL(외부 origin)은 skip + 경고로 원격 코드 로드를 차단한다. +- 예외: 확장이 manifest(`trusted_script_hosts`)로 선언해 코어가 `window.G7Config.trustedScriptHosts` 로 노출한 신뢰 호스트의 외부 스크립트는 허용한다(`isAllowedScriptSrc`). CKEditor5(cdn.ckeditor.com)·Daum 우편번호(t1.daumcdn.net) 등 번들 확장의 CDN 스크립트가 정상 로드되도록 하되, 편집기 저장분에 임의 원격 스크립트를 넣는 경로는 여전히 차단한다. (신뢰 경계: 미선언 외부 origin 은 항상 skip) + +## [engine-v1.58.4] - 2026-08-14 + +### Fixed + +#### 레이아웃 렌더링 테스트 하네스가 `named_actions` 를 등록하지 않던 문제 + +- `__tests__/utils/layoutTestUtils.ts::createLayoutTest` — 레이아웃 최상위 `named_actions` 를 `ActionDispatcher.setNamedActions()` 로 등록한다. 기존에는 등록 단계가 없어 `actionRef` 참조가 해석되지 않았고, 그 액션은 **경고만 남기고 아무 일도 하지 않은 채** 테스트가 통과했다. +- 하네스 자신이 검사 대상의 일부라, 이 누락은 `named_actions` 를 쓰는 레이아웃의 테스트를 통째로 무력화한다 — 액션이 붙지 않았는데도 렌더 단언은 전부 초록이므로 결함이 드러날 통로가 없다. +- `setNamedActions` 는 선택 호출(`?.`)이다 — 이 API 가 없는 디스패처 스텁으로 만든 기존 테스트가 깨지지 않게 한다. +## [engine-v1.58.3] - 2026-08-14 + +### Fixed + +#### 교차 출처(공개 자산 CDN) 요청에 세션 토큰이 실리고 이미지가 통째로 실패하던 문제 + +- `api/ApiClient.ts::setupInterceptors` — 요청 인터셉터가 `Authorization` 을 **동일 출처 요청에만** 첨부한다. 판정은 신설 `isCrossOriginRequest()` 가 담당하며, 상대 경로는 항상 동일 출처, 절대 URL(`https://…`)과 프로토콜 상대 URL(`//host/…`)만 `window.location.origin` 과 비교한다(파싱 실패·SSR 은 동일 출처로 간주해 기존 동작 유지). +- 공개 자산 디스크(S3/CDN) 옵트인이 켜지면 첨부·이미지의 `download_url` 이 외부 origin 절대 URL 이 된다. 종전 인터셉터는 URL 출처를 보지 않고 토큰을 붙였으므로 두 가지가 동시에 발생했다. + 1. **토큰 노출** — 관리자 세션 토큰이 제3자 CDN origin 으로 전송된다. CDN 이 `Access-Control-Allow-Origin` 을 허용하는 흔한 구성에서는 요청이 성공하므로 그 origin 의 접근 로그에 토큰이 남는다. + 2. **이미지 전면 실패** — `Authorization` 은 CORS 안전목록 밖이라 preflight 가 발생한다. 버킷/배포에 CORS 규칙이 없으면(AWS S3 기본값) preflight 가 거절되어 그 이미지가 전부 깨진다. +- 실측(AWS S3 실 버킷, 관리자 상품 이미지 탭): 요청 헤더에 `authorization: Bearer …` 가 실린 채 `net::ERR_FAILED` + 콘솔 CORS 오류 15건. 같은 이미지를 `<img>` 로 여는 상점 화면은 CORS 대상이 아니라 정상이어서, **관리자 화면에서만** 증상이 나타난다. +- 로컬 `public` 디스크(동일 출처)나 CORS 를 허용하는 개발용 오브젝트 스토리지에서는 재현되지 않는다 — 두 조건(외부 origin + CORS 미허용)이 겹쳐야 드러난다. + ## [engine-v1.58.2] - 2026-08-10 ### Fixed diff --git a/resources/js/core/template-engine/DataBindingEngine.ts b/resources/js/core/template-engine/DataBindingEngine.ts index d30fb094..b788b3fd 100644 --- a/resources/js/core/template-engine/DataBindingEngine.ts +++ b/resources/js/core/template-engine/DataBindingEngine.ts @@ -13,6 +13,7 @@ import { TranslationEngine } from './TranslationEngine'; import { hasPipes, splitPipes, executePipeChain } from './PipeRegistry'; import { classifyExpression, extractSingleBinding, isComplexExpression, LITERALS, scanBindings } from './BindingShape'; import { RAW_PREFIX, wrapRaw, wrapRawDeep } from './rawMarkers'; +import { evaluateSafeExpression } from './SafeExpressionEvaluator'; import type { G7DevToolsInterface } from './G7CoreGlobals'; const logger = createLogger('DataBindingEngine'); @@ -20,20 +21,9 @@ const logger = createLogger('DataBindingEngine'); /** * 함수 파라미터 이름으로 쓸 수 있는 식별자 패턴. * - * 표현식 평가는 컨텍스트 키를 `new Function` 의 파라미터로 넘긴다. 파라미터가 될 수 없는 - * 이름이 하나라도 섞이면 함수 생성이 통째로 실패하므로 미리 걸러낸다. + * 표현식 평가는 화이트리스트 AST 평가기(SafeExpressionEvaluator)가 담당한다 — + * 컨텍스트를 직접 스코프로 삼아 인터프리터로 해석하므로 `new Function` 이 필요 없다. */ -const VALID_IDENTIFIER_PATTERN = /^[$A-Za-z_][$A-Za-z0-9_]*$/; - -/** 파라미터 이름으로 쓸 수 없는 예약어 (엄격 모드 예약어 포함) */ -const RESERVED_WORDS = new Set([ - 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', - 'delete', 'do', 'else', 'enum', 'export', 'extends', 'false', 'finally', 'for', - 'function', 'if', 'implements', 'import', 'in', 'instanceof', 'interface', 'let', - 'new', 'null', 'package', 'private', 'protected', 'public', 'return', 'static', - 'super', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'var', 'void', - 'while', 'with', 'yield', 'arguments', 'eval', -]); /** * G7Core.devTools 인터페이스 가져오기 @@ -189,15 +179,6 @@ export class DataBindingEngine { */ private cache: Map<string, CacheEntry> = new Map(); - /** - * 표현식 함수 캐시 - * - * key: 전처리된 표현식, value: 컴파일된 Function - * - Function 생성자 비용이 높으므로 동일 표현식 재사용 시 캐시에서 조회 - * - 표현식 자체만 캐싱하고, 컨텍스트 값은 실행 시 전달 - */ - private expressionFnCache: Map<string, Function> = new Map(); - /** * 렌더 사이클 캐시 * @@ -1137,11 +1118,10 @@ export class DataBindingEngine { /** * 캐시 초기화 * - * 바인딩 값 캐시와 표현식 함수 캐시를 모두 초기화합니다. + * 바인딩 값 캐시를 초기화합니다. */ public clearCache(): void { this.cache.clear(); - this.expressionFnCache.clear(); } /** @@ -1383,45 +1363,15 @@ export class DataBindingEngine { return current ?? fallback; }; - // 확장된 컨텍스트의 키를 변수로 사용할 수 있도록 준비. - // - // 함수 파라미터가 될 수 없는 키(`sales_status[]`, `data-id`, 예약어 등)는 제외한다. - // 하나라도 섞이면 `new Function` 생성 자체가 SyntaxError 로 실패해 **그 컨텍스트에서 - // 평가되는 모든 표현식**이 통째로 죽는다 — 식이 그 키를 쓰지 않아도 마찬가지다. - // 예외도 화면 오류도 없이 값만 사라지므로(catch → 폴백) 원인 파악이 어렵다. - // - // 제외해도 잃는 것은 없다 — 그런 키는 애초에 식 안에서 맨이름으로 참조할 수 없었고 - // (`sales_status[]` 는 식별자가 아니다), 실제 작성은 `query['sales_status[]']` 처럼 - // 상위 객체를 거치므로 그대로 동작한다. - // @since engine-v1.56.2 - const contextKeys: string[] = []; - const contextValues: unknown[] = []; - for (const [key, value] of Object.entries(extendedContext)) { - if (!VALID_IDENTIFIER_PATTERN.test(key) || RESERVED_WORDS.has(key)) { - continue; - } - contextKeys.push(key); - contextValues.push(value); - } - - // 캐시 키 생성: 전처리된 표현식 + 컨텍스트 키 조합 - // 같은 표현식이라도 컨텍스트 키가 다르면 다른 함수가 필요 - const cacheKey = `${processedExpr}|${contextKeys.join(',')}`; - - // 캐시된 함수 조회 또는 새로 생성 - let evaluator = this.expressionFnCache.get(cacheKey); - const fromCache = !!evaluator; - if (!evaluator) { - // Function 생성자를 사용하여 표현식 평가 - // 예: expr = "!_global.sidebarOpen" - // contextKeys = ["_global", "user", ...] - // contextValues = [{sidebarOpen: false}, {...}, ...] - // eslint-disable-next-line no-new-func - evaluator = new Function(...contextKeys, `return (${processedExpr});`); - this.expressionFnCache.set(cacheKey, evaluator); - } - - const result = evaluator(...contextValues); + // 표현식은 화이트리스트 AST 평가기로 안전하게 실행한다(KVE-2026-1915). + // `new Function(...)` / `with(ctx)` 기반 평가는 `''.constructor.constructor(...)` + // 형태의 샌드박스 탈출을 허용했으므로 폐기하고, evaluateSafeExpression 이 컨텍스트를 + // 직접 스코프로 삼아 식을 인터프리터로 해석한다. 컨텍스트 키를 함수 파라미터로 + // 만들지 않으므로 `sales_status[]` 같은 비-식별자 키가 섞여도 평가가 죽지 않는다 + // (그런 키는 `query['sales_status[]']` 로 상위 객체를 거쳐 그대로 접근된다). + // @since engine-v1.59.0 + const fromCache = false; + const result = evaluateSafeExpression(processedExpr, extendedContext); // DevTools: 표현식 평가 추적 if (devTools?.isEnabled()) { diff --git a/resources/js/core/template-engine/SafeExpressionEvaluator.ts b/resources/js/core/template-engine/SafeExpressionEvaluator.ts new file mode 100644 index 00000000..68dc3a7e --- /dev/null +++ b/resources/js/core/template-engine/SafeExpressionEvaluator.ts @@ -0,0 +1,1926 @@ +/** + * SafeExpressionEvaluator + * + * G7 템플릿 엔진용 안전한 JavaScript 표현식 인터프리터. + * + * `new Function(...)` / `with(ctx)` 기반 평가를 대체한다. 그 방식은 + * `''.constructor.constructor('code')()` 형태의 샌드박스 탈출을 허용했다 + * (CWE-184 / CWE-94, KVE-2026-1915). + * + * 이 파일은 표현식 문자열을 AST 로 파싱한 뒤, `eval` / `new Function` / + * `with` / 코드 문자열 컴파일을 전혀 사용하지 않고 직접 해석(interpret)한다. + * + * 의존성이 없어야 한다 (다른 프로젝트 파일 import 금지, npm 의존성 금지) — + * strict-CSP artifact 컨텍스트에 번들되기 때문. + * + * @since engine-v1.59.0 + */ + +/* ------------------------------------------------------------------ * + * 보안 상수 + * ------------------------------------------------------------------ */ + +/** + * 접근이 금지된 프로퍼티 이름 (프로토타입 오염 / 샌드박스 탈출 방지) + * + * legacy 접근자 4종(`__lookupGetter__` 계열)은 프로퍼티를 **키가 아니라 문자열 인자**로 + * 지목하므로 `normalizeKey` 의 키 검사를 원리상 거치지 않는다. 이들은 Object facade 에서 + * 제거한 `getPrototypeOf`/`setPrototypeOf`/`defineProperty` 와 **같은 능력**을 모든 객체에서 + * 상속으로 제공하므로, 이름 자체를 막지 않으면 그 제거가 무의미해진다. 이 4개가 평가기에서 + * 도달 가능한 유일한 프로토타입 읽기·쓰기 프리미티브다. + * + * @since engine-v1.60.4 + */ +const BLOCKED_PROPERTIES = new Set([ + 'constructor', + '__proto__', + 'prototype', + '__lookupGetter__', + '__lookupSetter__', + '__defineGetter__', + '__defineSetter__', +]); + +/** context 로 shadowing 되지 않았을 때 참조 시 즉시 throw 하는 위험 전역 식별자 */ +const DANGEROUS_GLOBALS = new Set([ + 'Function', + 'eval', + 'globalThis', + 'window', + 'self', + 'document', + 'require', + 'module', + 'process', + 'Reflect', + 'Proxy', + 'WebAssembly', + 'import', + 'constructor', +]); + +/** + * 화이트리스트에 노출하는 Object 대체 facade. + * + * 네이티브 Object 를 그대로 노출하면 리플렉션 static + * (getPrototypeOf / getOwnPropertyDescriptor / setPrototypeOf / defineProperty / create 등) + * 을 통해 프로토타입·프로퍼티 디스크립터·Function 에 도달할 수 있다: + * `Object.getOwnPropertyDescriptor(Object.getPrototypeOf(String), 'constructor').value` → Function. + * 이 static 들은 프로퍼티 키가 아니라 **문자열 인자**로 프로퍼티를 지목하므로 키 정규화(normalizeKey)로는 + * 잡히지 않는다. 따라서 리플렉션·프로토타입·디스크립터 계열을 전부 제거하고 순수 데이터 계열만 노출한다. + * (KVE-2026-1915) + * + * @since engine-v1.60.1 + */ +/** + * 대상 객체가 공유 전역(화이트리스트 전역/생성자)이면 거부합니다. + * + * `WHITELIST_GLOBALS` 는 `Math`/`JSON`/`Date` 등 **실제 전역 참조**를 노출하므로, + * 대상 검사 없이 `Object.assign`/`freeze` 를 노출하면 표현식이 페이지 전체에 지속되는 + * 전역 변조(`Object.assign(Math, { floor: … })`)를 일으킬 수 있습니다. `delete` 연산자가 + * 이미 identity 로 차단하고 있으므로 facade 의 변형 메서드도 같은 강도로 맞춥니다. + * + * @since engine-v1.60.2 + * @param target 변형 대상 후보 + * @param method 거부 메시지에 넣을 메서드명 + * @returns 검사를 통과한 target (그대로 반환) + */ +function assertNotSharedGlobal<T>(target: T, method: string): T { + if ( + target !== null && + (typeof target === 'object' || typeof target === 'function') && + WHITELIST_GLOBAL_OBJECT_SET.has(target as object) + ) { + throw new Error(`Object.${method} on a built-in global object is not allowed`); + } + + return target; +} + +const SAFE_OBJECT = Object.freeze({ + keys: Object.keys, + values: Object.values, + entries: Object.entries, + // 변형 메서드는 공유 전역을 대상으로 삼지 못하게 감싼다 (delete 가드와 대칭). + // 복사는 네이티브 Object.assign 이 아니라 defineOwn 으로 한다 — 네이티브는 [[Set]] 이라 + // source 에 own `__proto__` 키가 있으면(`JSON.parse('{"__proto__":…}')` 가 정확히 그렇다) + // target 의 setter 를 깨워 프로토타입을 교체한다. defineOwn 은 항상 own data property 를 + // 정의하므로 그 경로가 구조적으로 닫힌다. 금지 키는 아예 복사하지 않는다. + assign: (target: unknown, ...sources: unknown[]): unknown => { + const dest = assertNotSharedGlobal(target, 'assign') as Record<string, unknown>; + + for (const source of sources) { + if (source === null || source === undefined) { + continue; + } + + for (const key of Object.keys(source as object)) { + if (BLOCKED_PROPERTIES.has(key)) { + throw new Error(`Access to "${key}" is forbidden`); + } + defineOwn(dest, key, (source as Record<string, unknown>)[key]); + } + } + + return dest; + }, + freeze: <T>(target: T): T => Object.freeze(assertNotSharedGlobal(target, 'freeze')), + fromEntries: Object.fromEntries, + // create 는 프로토타입·디스크립터를 *읽지도 쓰지도* 않는다(신규 객체 생성만) — normalizeKey + // 로 `.constructor`/`.__proto__` 접근이 이미 차단되므로 탈출 벡터가 아니다. 레이아웃이 + // `Object.assign(Object.create(null), …)` 로 null-proto 맵을 만드는 정당한 사용처가 있다. + create: Object.create, + isFrozen: Object.isFrozen, +}); + +/** context 에 없을 때 해석 가능한 화이트리스트 전역 (실제 JS 참조 제공) */ +const WHITELIST_GLOBALS: Record<string, unknown> = { + Math, + JSON, + Date, + Array, + Object: SAFE_OBJECT, + Number, + String, + Boolean, + Set, + Map, + WeakSet, + WeakMap, + parseInt, + parseFloat, + isNaN, + isFinite, +}; + +/** + * `new` 연산이 허용되는 화이트리스트 생성자 이름. + * + * `new X(...)` 는 context 를 무시하고 오직 이 이름의 실제 전역 생성자만 사용한다. + * `Function` / `eval` / `Proxy` / `Reflect` 등은 여기에 없으므로 `new` 대상이 될 수 없다. + */ +const WHITELIST_CONSTRUCTORS: Record<string, new (...args: unknown[]) => unknown> = { + Date: Date as unknown as new (...args: unknown[]) => unknown, + Set: Set as unknown as new (...args: unknown[]) => unknown, + Map: Map as unknown as new (...args: unknown[]) => unknown, + WeakSet: WeakSet as unknown as new (...args: unknown[]) => unknown, + WeakMap: WeakMap as unknown as new (...args: unknown[]) => unknown, + Array: Array as unknown as new (...args: unknown[]) => unknown, + Number: Number as unknown as new (...args: unknown[]) => unknown, + String: String as unknown as new (...args: unknown[]) => unknown, + Boolean: Boolean as unknown as new (...args: unknown[]) => unknown, + Object: Object as unknown as new (...args: unknown[]) => unknown, +}; + +/** + * 화이트리스트 전역 객체(및 생성자)의 실제 참조 집합. + * + * `delete` 연산자가 공유 전역(`Math`/`JSON`/`Array` 등)의 프로퍼티를 지우지 못하도록 + * identity 로 차단한다 — 로컬 스코프 객체 사본에 대한 delete 만 허용한다. + */ +const WHITELIST_GLOBAL_OBJECT_SET: Set<object> = new Set( + [...Object.values(WHITELIST_GLOBALS), ...Object.values(WHITELIST_CONSTRUCTORS)].filter( + (v): v is object => v !== null && (typeof v === 'object' || typeof v === 'function'), + ), +); + +/** + * 파서에서 파라미터 이름으로 쓸 수 없는 예약 키워드. + * + * `function` 은 함수 표현식으로 파싱되므로 파라미터 이름 위치에서만 거부된다 + * (arrow/함수 파라미터가 `function` 이 되는 것 방지). + */ +const FORBIDDEN_KEYWORDS = new Set(['function']); + +/** 옵셔널 체이닝 단락(short-circuit)을 전파하는 내부 센티널 */ +const SHORT_CIRCUIT = Symbol('short-circuit'); + +/* ------------------------------------------------------------------ * + * 토큰 + * ------------------------------------------------------------------ */ + +type TokenType = 'num' | 'str' | 'ident' | 'punct' | 'template'; + +interface Token { + type: TokenType; + /** punct: 연산자 문자열 / ident: 이름 / num: 숫자값 / str: 문자열값 / template: '' */ + value: string | number; + pos: number; + /** template 전용: 문자열 조각 (exprs.length + 1 개) */ + quasis?: string[]; + /** template 전용: 각 ${} 내부 표현식 소스 문자열 */ + exprs?: string[]; +} + +/* ------------------------------------------------------------------ * + * AST 노드 + * ------------------------------------------------------------------ */ + +type Node = + | { type: 'Literal'; value: unknown } + | { type: 'Identifier'; name: string } + | { type: 'Member'; object: Node; property: Node; computed: boolean; optional: boolean } + | { type: 'Call'; callee: Node; args: Node[]; optional: boolean } + | { type: 'Unary'; operator: string; argument: Node } + | { type: 'Delete'; argument: Node } + | { type: 'Binary'; operator: string; left: Node; right: Node } + | { type: 'Logical'; operator: string; left: Node; right: Node } + | { type: 'Conditional'; test: Node; consequent: Node; alternate: Node } + | { type: 'Array'; elements: Node[] } + | { type: 'Object'; properties: ObjectProperty[] } + | { type: 'Arrow'; params: Param[]; body: Node; isBlock: boolean } + | { type: 'Function'; name: string | null; params: Param[]; body: Node /* Block */ } + | { type: 'Template'; quasis: string[]; expressions: Node[] } + | { type: 'New'; ctor: string; args: Node[] } + | { type: 'Spread'; argument: Node } + // ── statement 노드 (function/arrow 블록 본문 안에서만 도달) ── + | { type: 'Block'; body: Node[] } + | { type: 'VarDecl'; declarations: { name: string; init: Node }[] } + | { type: 'If'; test: Node; consequent: Node; alternate: Node | null } + | { type: 'ForOf'; name: string; iterable: Node; body: Node } + | { type: 'Return'; argument: Node | null } + | { type: 'ExprStmt'; expression: Node } + | { type: 'Break' } + | { type: 'Continue' } + | { type: 'Empty' } + | { type: 'Try'; block: Node; handlerParam: string | null; handler: Node | null; finalizer: Node | null }; + +/** + * 함수/화살표 파라미터 (기본값 지원). + * + * `name` 이 null 이면 배열 구조분해 패턴 — `elements` 가 요소 이름 목록이며 + * null 요소는 홀(elision)이다 (`([, vid]) =>` → elements: [null, 'vid']). + * 구 평가기(new Function)가 허용하던 형태로, 레이아웃 전반에서 사용된다. + * + * @since engine-v1.60.5 + */ +interface Param { + name: string | null; + elements?: (string | null)[]; + default: Node | null; +} + +interface ObjectProperty { + kind: 'init' | 'spread'; + key?: Node; // init only + computed?: boolean; // init only + value: Node; +} + +/* ------------------------------------------------------------------ * + * 제어 흐름 시그널 (statement 본문 해석용) + * + * return/break/continue 는 예외로 던져 트리워킹을 되감는다. 사용자 예외와 + * 구분되도록 전용 클래스 인스턴스를 쓴다(try/catch 가 이들을 삼키지 않도록). + * ------------------------------------------------------------------ */ + +class ReturnSignal { + constructor(public value: unknown) {} +} +class BreakSignal {} +class ContinueSignal {} + +/* ------------------------------------------------------------------ * + * 스코프 (해석 환경) + * ------------------------------------------------------------------ */ + +interface Scope { + vars: Record<string, unknown>; + parent: Scope | null; +} + +/* ------------------------------------------------------------------ * + * 토크나이저 + * ------------------------------------------------------------------ */ + +function isIdentStart(ch: string): boolean { + return /[A-Za-z_$]/.test(ch); +} + +function isIdentPart(ch: string): boolean { + return /[A-Za-z0-9_$]/.test(ch); +} + +function isDigit(ch: string): boolean { + return ch >= '0' && ch <= '9'; +} + +/** + * 문자열 리터럴을 건너뛴다 (열린 따옴표 위치부터 닫는 따옴표 다음까지). + * + * @param src 소스 문자열 + * @param start 여는 따옴표 인덱스 + * @param quote 따옴표 문자 + * @return 닫는 따옴표 다음 인덱스 + */ +function skipStringLiteral(src: string, start: number, quote: string): number { + let i = start + 1; + while (i < src.length) { + if (src[i] === '\\') { + i += 2; + continue; + } + if (src[i] === quote) { + return i + 1; + } + i++; + } + throw new Error(`Unterminated string literal at position ${start}`); +} + +/** + * 백틱 템플릿 리터럴을 스캔해 문자열 조각(quasis)과 `${}` 내부 표현식 소스(exprs)로 분해. + * + * `${...}` 내부의 중첩 `{}` / 문자열 / 중첩 템플릿을 올바르게 건너뛴다. 내부 표현식은 + * 여기서 파싱하지 않고 소스 문자열로 보관했다가, 파서가 인터프리터로 재귀 파싱한다 + * (`new Function` 미사용 — 완전히 안전). + * + * @param src 소스 문자열 + * @param start 여는 백틱 인덱스 + * @return quasis / exprs / 닫는 백틱 다음 인덱스(end) + */ +function scanTemplateLiteral(src: string, start: number): { quasis: string[]; exprs: string[]; end: number } { + let i = start + 1; + const quasis: string[] = []; + const exprs: string[] = []; + let cur = ''; + + while (i < src.length) { + const ch = src[i]; + + if (ch === '\\') { + const esc = src[i + 1]; + switch (esc) { + case 'n': cur += '\n'; break; + case 't': cur += '\t'; break; + case 'r': cur += '\r'; break; + case 'b': cur += '\b'; break; + case 'f': cur += '\f'; break; + case 'v': cur += '\v'; break; + case '0': cur += '\0'; break; + case '\\': cur += '\\'; break; + case '`': cur += '`'; break; + case '$': cur += '$'; break; + default: cur += esc; break; + } + i += 2; + continue; + } + + if (ch === '`') { + quasis.push(cur); + return { quasis, exprs, end: i + 1 }; + } + + if (ch === '$' && src[i + 1] === '{') { + quasis.push(cur); + cur = ''; + i += 2; + const exprStart = i; + let depth = 1; + while (i < src.length && depth > 0) { + const c = src[i]; + if (c === '{') { + depth++; + i++; + } else if (c === '}') { + depth--; + if (depth === 0) break; + i++; + } else if (c === '"' || c === "'") { + i = skipStringLiteral(src, i, c); + } else if (c === '`') { + i = scanTemplateLiteral(src, i).end; + } else { + i++; + } + } + if (depth !== 0) { + throw new Error('Unterminated ${...} in template literal'); + } + exprs.push(src.slice(exprStart, i)); + i++; // 닫는 } + continue; + } + + cur += ch; + i++; + } + + throw new Error(`Unterminated template literal at position ${start}`); +} + +function tokenize(src: string): Token[] { + const tokens: Token[] = []; + let i = 0; + const n = src.length; + + const peekAt = (o: number): string => (i + o < n ? src[i + o] : ''); + + while (i < n) { + const ch = src[i]; + + // 공백 + if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r' || ch === '\f' || ch === '\v') { + i++; + continue; + } + + // 숫자 (.5 형태 포함) + if (isDigit(ch) || (ch === '.' && isDigit(peekAt(1)))) { + const start = i; + while (i < n && isDigit(src[i])) i++; + if (src[i] === '.') { + i++; + while (i < n && isDigit(src[i])) i++; + } + if (src[i] === 'e' || src[i] === 'E') { + i++; + if (src[i] === '+' || src[i] === '-') i++; + if (!isDigit(src[i])) throw new Error(`Invalid number literal at position ${start}`); + while (i < n && isDigit(src[i])) i++; + } + const raw = src.slice(start, i); + tokens.push({ type: 'num', value: Number(raw), pos: start }); + continue; + } + + // 문자열 + if (ch === '"' || ch === "'") { + const start = i; + const quote = ch; + i++; + let out = ''; + while (i < n && src[i] !== quote) { + if (src[i] === '\\') { + i++; + const esc = src[i]; + switch (esc) { + case 'n': out += '\n'; break; + case 't': out += '\t'; break; + case 'r': out += '\r'; break; + case 'b': out += '\b'; break; + case 'f': out += '\f'; break; + case 'v': out += '\v'; break; + case '0': out += '\0'; break; + case '\\': out += '\\'; break; + case "'": out += "'"; break; + case '"': out += '"'; break; + case '`': out += '`'; break; + default: out += esc; break; + } + i++; + } else { + out += src[i]; + i++; + } + } + if (i >= n) throw new Error(`Unterminated string literal at position ${start}`); + i++; // 닫는 따옴표 + tokens.push({ type: 'str', value: out, pos: start }); + continue; + } + + // 식별자 + if (isIdentStart(ch)) { + const start = i; + i++; + while (i < n && isIdentPart(src[i])) i++; + tokens.push({ type: 'ident', value: src.slice(start, i), pos: start }); + continue; + } + + // 템플릿 리터럴 (지원): ${} 보간부는 인터프리터로 재귀 파싱된다 + if (ch === '`') { + const scanned = scanTemplateLiteral(src, i); + tokens.push({ type: 'template', value: '', pos: i, quasis: scanned.quasis, exprs: scanned.exprs }); + i = scanned.end; + continue; + } + + // 연산자 / 구두점 + const start = i; + const two = src.substr(i, 2); + const three = src.substr(i, 3); + + // 스프레드 + if (three === '...') { + tokens.push({ type: 'punct', value: '...', pos: start }); + i += 3; + continue; + } + + switch (ch) { + case '=': { + if (peekAt(1) === '>') { + tokens.push({ type: 'punct', value: '=>', pos: start }); + i += 2; + } else if (peekAt(1) === '=') { + if (peekAt(2) === '=') { + tokens.push({ type: 'punct', value: '===', pos: start }); + i += 3; + } else { + tokens.push({ type: 'punct', value: '==', pos: start }); + i += 2; + } + } else { + // bare '=' : 변수 선언(const x = …)·기본 파라미터((a = …) =>)에서만 소비된다. + // 표현식 문법은 '=' 를 소비하지 않으므로 대입식(x = y)은 파서에서 거부된다. + tokens.push({ type: 'punct', value: '=', pos: start }); + i += 1; + } + continue; + } + case '!': { + if (peekAt(1) === '=') { + if (peekAt(2) === '=') { + tokens.push({ type: 'punct', value: '!==', pos: start }); + i += 3; + } else { + tokens.push({ type: 'punct', value: '!=', pos: start }); + i += 2; + } + } else { + tokens.push({ type: 'punct', value: '!', pos: start }); + i += 1; + } + continue; + } + case '<': { + if (peekAt(1) === '=') { + tokens.push({ type: 'punct', value: '<=', pos: start }); + i += 2; + } else if (peekAt(1) === '<') { + throw new Error('Bitwise/shift operators are not allowed'); + } else { + tokens.push({ type: 'punct', value: '<', pos: start }); + i += 1; + } + continue; + } + case '>': { + if (peekAt(1) === '=') { + tokens.push({ type: 'punct', value: '>=', pos: start }); + i += 2; + } else if (peekAt(1) === '>') { + throw new Error('Bitwise/shift operators are not allowed'); + } else { + tokens.push({ type: 'punct', value: '>', pos: start }); + i += 1; + } + continue; + } + case '&': { + if (peekAt(1) === '&') { + tokens.push({ type: 'punct', value: '&&', pos: start }); + i += 2; + } else { + throw new Error('Bitwise operators are not allowed'); + } + continue; + } + case '|': { + if (peekAt(1) === '|') { + tokens.push({ type: 'punct', value: '||', pos: start }); + i += 2; + } else { + throw new Error('Bitwise operators are not allowed'); + } + continue; + } + case '?': { + // 옵셔널 체이닝 ?. (단, ?.5 처럼 숫자 뒤는 삼항 + 소수) + if (peekAt(1) === '.' && !isDigit(peekAt(2))) { + tokens.push({ type: 'punct', value: '?.', pos: start }); + i += 2; + } else if (peekAt(1) === '?') { + tokens.push({ type: 'punct', value: '??', pos: start }); + i += 2; + } else { + tokens.push({ type: 'punct', value: '?', pos: start }); + i += 1; + } + continue; + } + case '+': { + if (peekAt(1) === '+') throw new Error('Increment operator is not allowed'); + if (peekAt(1) === '=') throw new Error('Assignment operators are not allowed'); + tokens.push({ type: 'punct', value: '+', pos: start }); + i += 1; + continue; + } + case '-': { + if (peekAt(1) === '-') throw new Error('Decrement operator is not allowed'); + if (peekAt(1) === '=') throw new Error('Assignment operators are not allowed'); + tokens.push({ type: 'punct', value: '-', pos: start }); + i += 1; + continue; + } + case '*': { + if (peekAt(1) === '=') throw new Error('Assignment operators are not allowed'); + if (peekAt(1) === '*') throw new Error('Exponentiation operator is not supported'); + tokens.push({ type: 'punct', value: '*', pos: start }); + i += 1; + continue; + } + case '/': { + if (peekAt(1) === '=') throw new Error('Assignment operators are not allowed'); + tokens.push({ type: 'punct', value: '/', pos: start }); + i += 1; + continue; + } + case '%': { + if (peekAt(1) === '=') throw new Error('Assignment operators are not allowed'); + tokens.push({ type: 'punct', value: '%', pos: start }); + i += 1; + continue; + } + case '~': + case '^': + throw new Error('Bitwise operators are not allowed'); + case '.': + case ':': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + case ',': + tokens.push({ type: 'punct', value: ch, pos: start }); + i += 1; + continue; + case ';': + // statement 구분자 : 블록 본문 파서에서만 소비된다. 최상위 표현식은 + // 단일 식만 허용하므로(parseToAst 가 atEnd 강제) 여기서 남으면 거부된다. + tokens.push({ type: 'punct', value: ';', pos: start }); + i += 1; + continue; + default: + throw new Error(`Unexpected character "${ch}" at position ${start}` + (two ? '' : '')); + } + } + + return tokens; +} + +/* ------------------------------------------------------------------ * + * 파서 (Pratt / precedence-climbing) + * ------------------------------------------------------------------ */ + +const BINARY_BP: Record<string, number> = { + '??': 1, + '||': 1, + '&&': 2, + '===': 3, + '!==': 3, + '==': 3, + '!=': 3, + '<': 4, + '>': 4, + '<=': 4, + '>=': 4, + '+': 5, + '-': 5, + '*': 6, + '/': 6, + '%': 6, +}; + +const LOGICAL_OPS = new Set(['??', '||', '&&']); + +class Parser { + private tokens: Token[]; + private pos = 0; + + constructor(tokens: Token[]) { + this.tokens = tokens; + } + + atEnd(): boolean { + return this.pos >= this.tokens.length; + } + + peek(offset = 0): Token | null { + const idx = this.pos + offset; + return idx < this.tokens.length ? this.tokens[idx] : null; + } + + private next(): Token { + if (this.atEnd()) throw new Error('Unexpected end of expression'); + return this.tokens[this.pos++]; + } + + private isPunct(value: string, offset = 0): boolean { + const t = this.peek(offset); + return !!t && t.type === 'punct' && t.value === value; + } + + private expectPunct(value: string): void { + const t = this.peek(); + if (!t || t.type !== 'punct' || t.value !== value) { + throw new Error(`Expected "${value}" but found "${t ? t.value : '<end>'}"`); + } + this.pos++; + } + + /* ------- 최상위 표현식 (arrow → ternary) ------- */ + + parseExpression(): Node { + const arrow = this.tryParseArrow(); + if (arrow) return arrow; + return this.parseTernary(); + } + + private tryParseArrow(): Node | null { + const save = this.pos; + const first = this.peek(); + if (!first) return null; + + // 단일 파라미터: x => ... + if (first.type === 'ident' && this.isPunct('=>', 1) && !FORBIDDEN_KEYWORDS.has(String(first.value))) { + this.pos += 1; // ident + this.pos += 1; // => + const { body, isBlock } = this.parseArrowBody(); + return { type: 'Arrow', params: [{ name: String(first.value), default: null }], body, isBlock }; + } + + // 괄호 파라미터: (a, b = [], c) => ... 또는 () => ... + if (first.type === 'punct' && first.value === '(') { + this.pos += 1; // ( + const params = this.tryParseParamList(); + if (params && this.isPunct(')')) { + this.pos += 1; // ) + if (this.isPunct('=>')) { + this.pos += 1; // => + const { body, isBlock } = this.parseArrowBody(); + return { type: 'Arrow', params, body, isBlock }; + } + } + // arrow 아님 → 되돌리기 + this.pos = save; + return null; + } + + this.pos = save; + return null; + } + + /** + * 파라미터 목록을 파싱한다: `(ident | [배열패턴]) (= 기본값)?` 를 `,` 로 구분. + * 열림 `(` 는 호출부가 이미 소비한 상태이며, 닫힘 `)` 는 소비하지 않는다(호출부가 검사). + * + * @return 파싱된 파라미터 배열, 또는 파라미터 형태가 아니면 null(arrow 아님) + */ + private tryParseParamList(): Param[] | null { + const params: Param[] = []; + if (this.isPunct(')')) return params; // 빈 목록 + for (;;) { + const p = this.peek(); + let name: string | null = null; + let elements: (string | null)[] | undefined; + if (p && p.type === 'punct' && p.value === '[') { + const pattern = this.tryParseArrayPattern(); + if (!pattern) return null; + elements = pattern; + } else if (p && p.type === 'ident' && !FORBIDDEN_KEYWORDS.has(String(p.value))) { + name = String(p.value); + this.pos += 1; + } else { + return null; + } + let def: Node | null = null; + if (this.isPunct('=')) { + this.pos += 1; // = + // 기본값은 assignment 레벨 식(중첩 arrow 허용). parseExpression 은 top-level + // 콤마를 소비하지 않으므로 다음 파라미터 구분자 ',' 는 그대로 남는다. + def = this.parseExpression(); + } + params.push(elements ? { name, elements, default: def } : { name, default: def }); + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + return params; + } + + /** + * 파라미터 위치의 배열 구조분해 패턴 `[a, , b]` 를 파싱한다. + * 요소는 식별자 또는 홀(elision)만 허용 — 중첩 패턴·rest·요소별 기본값은 + * 레이아웃 표현식에서 쓰이지 않으므로 지원하지 않는다(발견 시 arrow 아님으로 되돌림). + * + * @return 요소 이름 배열 (홀은 null), 패턴 형태가 아니면 null + * @since engine-v1.60.5 + */ + private tryParseArrayPattern(): (string | null)[] | null { + const save = this.pos; + this.pos += 1; // [ + const elements: (string | null)[] = []; + for (;;) { + if (this.isPunct(']')) { + this.pos += 1; + return elements; + } + if (this.isPunct(',')) { + elements.push(null); // 홀 (elision) + this.pos += 1; + continue; + } + const t = this.peek(); + if (!t || t.type !== 'ident' || FORBIDDEN_KEYWORDS.has(String(t.value))) { + this.pos = save; + return null; + } + elements.push(String(t.value)); + this.pos += 1; + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + if (this.isPunct(']')) { + this.pos += 1; + return elements; + } + this.pos = save; + return null; + } + } + + private parseArrowBody(): { body: Node; isBlock: boolean } { + // 블록 본문: (…) => { statements } (return 으로 값 반환) + if (this.isPunct('{')) { + return { body: this.parseBlock(), isBlock: true }; + } + // 식 본문: (…) => expr (중첩 arrow / ternary 허용) + return { body: this.parseExpression(), isBlock: false }; + } + + /* ------- statement 파서 (function/arrow 블록 본문 전용) ------- */ + + /** + * 블록 `{ stmt* }` 를 파싱한다. 여는 `{` 부터 닫는 `}` 까지. + * + * @return Block 노드 + */ + private parseBlock(): Node { + this.expectPunct('{'); + const body: Node[] = []; + while (!this.isPunct('}')) { + if (this.atEnd()) throw new Error('Unterminated block'); + body.push(this.parseStatement()); + } + this.expectPunct('}'); + return { type: 'Block', body }; + } + + private parseStatement(): Node { + const t = this.peek(); + if (!t) throw new Error('Unexpected end of statement'); + + if (t.type === 'punct' && t.value === '{') return this.parseBlock(); + if (t.type === 'punct' && t.value === ';') { + this.pos += 1; + return { type: 'Empty' }; + } + + if (t.type === 'ident') { + switch (String(t.value)) { + case 'const': + case 'let': + return this.parseVarDecl(); + case 'if': + return this.parseIf(); + case 'for': + return this.parseForOf(); + case 'return': + return this.parseReturn(); + case 'try': + return this.parseTry(); + case 'break': + this.pos += 1; + this.consumeSemicolon(); + return { type: 'Break' }; + case 'continue': + this.pos += 1; + this.consumeSemicolon(); + return { type: 'Continue' }; + default: + break; + } + } + + // 식 statement (호출·delete·메서드 부수효과 등) + const expression = this.parseExpression(); + this.consumeSemicolon(); + return { type: 'ExprStmt', expression }; + } + + /** 선택적 세미콜론 소비 (ASI 관대 처리) */ + private consumeSemicolon(): void { + if (this.isPunct(';')) this.pos += 1; + } + + private parseVarDecl(): Node { + this.pos += 1; // const|let + const declarations: { name: string; init: Node }[] = []; + for (;;) { + const nameTok = this.peek(); + if (!nameTok || nameTok.type !== 'ident') { + throw new Error('Expected variable name in declaration'); + } + const name = String(nameTok.value); + this.pos += 1; + this.expectPunct('='); // 선언은 초기화 필수 (const/let 재대입 없음) + // init 은 assignment 레벨 식(arrow 포함). top-level 콤마는 소비하지 않으므로 + // 다음 선언자 구분자 ',' 는 아래 루프가 처리한다. + const init = this.parseExpression(); + declarations.push({ name, init }); + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + this.consumeSemicolon(); + return { type: 'VarDecl', declarations }; + } + + private parseIf(): Node { + this.pos += 1; // if + this.expectPunct('('); + const test = this.parseExpression(); + this.expectPunct(')'); + const consequent = this.parseStatement(); + let alternate: Node | null = null; + const t = this.peek(); + if (t && t.type === 'ident' && t.value === 'else') { + this.pos += 1; // else + alternate = this.parseStatement(); + } + return { type: 'If', test, consequent, alternate }; + } + + private parseForOf(): Node { + this.pos += 1; // for + this.expectPunct('('); + const kw = this.peek(); + if (!kw || kw.type !== 'ident' || (kw.value !== 'const' && kw.value !== 'let')) { + throw new Error('Only "for (const x of …)" / "for (let x of …)" loops are supported'); + } + this.pos += 1; // const|let + const nameTok = this.peek(); + if (!nameTok || nameTok.type !== 'ident') { + throw new Error('Expected loop variable name'); + } + const name = String(nameTok.value); + this.pos += 1; + const ofTok = this.peek(); + if (!ofTok || ofTok.type !== 'ident' || ofTok.value !== 'of') { + throw new Error('Only for-of loops are supported (for-in / C-style for are not allowed)'); + } + this.pos += 1; // of + const iterable = this.parseExpression(); + this.expectPunct(')'); + const body = this.parseStatement(); + return { type: 'ForOf', name, iterable, body }; + } + + private parseReturn(): Node { + this.pos += 1; // return + // 인자 없는 return; / return } + if (this.isPunct(';') || this.isPunct('}') || this.atEnd()) { + this.consumeSemicolon(); + return { type: 'Return', argument: null }; + } + const argument = this.parseExpression(); + this.consumeSemicolon(); + return { type: 'Return', argument }; + } + + private parseTry(): Node { + this.pos += 1; // try + const block = this.parseBlock(); + let handlerParam: string | null = null; + let handler: Node | null = null; + let finalizer: Node | null = null; + const c = this.peek(); + if (c && c.type === 'ident' && c.value === 'catch') { + this.pos += 1; // catch + if (this.isPunct('(')) { + this.pos += 1; // ( + const paramTok = this.peek(); + if (paramTok && paramTok.type === 'ident') { + handlerParam = String(paramTok.value); + this.pos += 1; + } + this.expectPunct(')'); + } + handler = this.parseBlock(); + } + const f = this.peek(); + if (f && f.type === 'ident' && f.value === 'finally') { + this.pos += 1; // finally + finalizer = this.parseBlock(); + } + if (!handler && !finalizer) { + throw new Error('Missing catch or finally after try'); + } + return { type: 'Try', block, handlerParam, handler, finalizer }; + } + + /** 함수 표현식 `function name?(params) { block }` */ + private parseFunctionExpression(): Node { + this.pos += 1; // function + let name: string | null = null; + const nameTok = this.peek(); + if (nameTok && nameTok.type === 'ident' && nameTok.value !== undefined && !this.isPunct('(')) { + name = String(nameTok.value); + this.pos += 1; + } + this.expectPunct('('); + const params = this.tryParseParamList(); + if (!params) throw new Error('Invalid function parameter list'); + this.expectPunct(')'); + const body = this.parseBlock(); + return { type: 'Function', name, params, body }; + } + + private parseTernary(): Node { + const test = this.parseBinary(0); + if (this.isPunct('?')) { + this.pos += 1; + const consequent = this.parseExpression(); + this.expectPunct(':'); + const alternate = this.parseExpression(); + return { type: 'Conditional', test, consequent, alternate }; + } + return test; + } + + private parseBinary(minBp: number): Node { + let left = this.parseUnary(); + for (;;) { + const t = this.peek(); + if (!t || t.type !== 'punct') break; + const op = String(t.value); + const bp = BINARY_BP[op]; + if (bp === undefined || bp < minBp) break; + this.pos += 1; + const right = this.parseBinary(bp + 1); + if (LOGICAL_OPS.has(op)) { + left = { type: 'Logical', operator: op, left, right }; + } else { + left = { type: 'Binary', operator: op, left, right }; + } + } + return left; + } + + private parseUnary(): Node { + const t = this.peek(); + if (t) { + if (t.type === 'punct' && (t.value === '!' || t.value === '-' || t.value === '+')) { + this.pos += 1; + const argument = this.parseUnary(); + return { type: 'Unary', operator: String(t.value), argument }; + } + if (t.type === 'ident' && t.value === 'typeof') { + this.pos += 1; + const argument = this.parseUnary(); + return { type: 'Unary', operator: 'typeof', argument }; + } + if (t.type === 'ident' && t.value === 'delete') { + this.pos += 1; + const argument = this.parseUnary(); + if (argument.type !== 'Member') { + throw new Error('delete is only allowed on a property reference'); + } + return { type: 'Delete', argument }; + } + } + return this.parsePostfix(); + } + + private parsePostfix(): Node { + let node = this.parsePrimary(); + for (;;) { + const t = this.peek(); + if (!t || t.type !== 'punct') break; + + if (t.value === '.') { + this.pos += 1; + const nameTok = this.next(); + if (nameTok.type !== 'ident') { + throw new Error(`Expected property name after "." but found "${nameTok.value}"`); + } + const name = String(nameTok.value); + if (BLOCKED_PROPERTIES.has(name)) { + throw new Error(`Access to "${name}" is forbidden`); + } + node = { + type: 'Member', + object: node, + property: { type: 'Literal', value: name }, + computed: false, + optional: false, + }; + } else if (t.value === '?.') { + this.pos += 1; + if (this.isPunct('[')) { + this.pos += 1; + const prop = this.parseExpression(); + this.expectPunct(']'); + this.assertComputedKeyNotBlocked(prop); + node = { type: 'Member', object: node, property: prop, computed: true, optional: true }; + } else if (this.isPunct('(')) { + const args = this.parseArguments(); + node = { type: 'Call', callee: node, args, optional: true }; + } else { + const nameTok = this.next(); + if (nameTok.type !== 'ident') { + throw new Error(`Expected property name after "?." but found "${nameTok.value}"`); + } + const name = String(nameTok.value); + if (BLOCKED_PROPERTIES.has(name)) { + throw new Error(`Access to "${name}" is forbidden`); + } + node = { + type: 'Member', + object: node, + property: { type: 'Literal', value: name }, + computed: false, + optional: true, + }; + } + } else if (t.value === '[') { + this.pos += 1; + const prop = this.parseExpression(); + this.expectPunct(']'); + this.assertComputedKeyNotBlocked(prop); + node = { type: 'Member', object: node, property: prop, computed: true, optional: false }; + } else if (t.value === '(') { + const args = this.parseArguments(); + node = { type: 'Call', callee: node, args, optional: false }; + } else { + break; + } + } + return node; + } + + private assertComputedKeyNotBlocked(prop: Node): void { + // 문자열 리터럴 computed 키가 금지 프로퍼티라면 파싱 시점에 차단 + if (prop.type === 'Literal' && typeof prop.value === 'string' && BLOCKED_PROPERTIES.has(prop.value)) { + throw new Error(`Access to "${prop.value}" is forbidden`); + } + } + + private parseArguments(): Node[] { + this.expectPunct('('); + const args: Node[] = []; + if (!this.isPunct(')')) { + for (;;) { + if (this.isPunct('...')) { + this.pos += 1; + args.push({ type: 'Spread', argument: this.parseExpression() }); + } else { + args.push(this.parseExpression()); + } + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + } + this.expectPunct(')'); + return args; + } + + private parseNew(): Node { + this.expectIdent('new'); + const ctorTok = this.peek(); + // 생성자는 반드시 bare 식별자 (멤버 접근/computed 금지 → new x.constructor(...) 차단) + if (!ctorTok || ctorTok.type !== 'ident') { + throw new Error('new is only allowed on whitelisted built-in constructors'); + } + const ctorName = String(ctorTok.value); + if (!Object.prototype.hasOwnProperty.call(WHITELIST_CONSTRUCTORS, ctorName)) { + throw new Error('new is only allowed on whitelisted built-in constructors'); + } + this.pos += 1; // 생성자 이름 + let args: Node[] = []; + if (this.isPunct('(')) { + args = this.parseArguments(); + } + return { type: 'New', ctor: ctorName, args }; + } + + private expectIdent(name: string): void { + const t = this.peek(); + if (!t || t.type !== 'ident' || t.value !== name) { + throw new Error(`Expected "${name}"`); + } + this.pos += 1; + } + + private parsePrimary(): Node { + const t = this.peek(); + if (!t) throw new Error('Unexpected end of expression'); + + if (t.type === 'num') { + this.pos += 1; + return { type: 'Literal', value: t.value }; + } + + if (t.type === 'str') { + this.pos += 1; + return { type: 'Literal', value: t.value }; + } + + if (t.type === 'template') { + this.pos += 1; + const quasis = t.quasis || ['']; + const exprSources = t.exprs || []; + const expressions = exprSources.map((src) => parseToAst(src)); + return { type: 'Template', quasis, expressions }; + } + + if (t.type === 'ident' && t.value === 'new') { + return this.parseNew(); + } + + // 함수 표현식 `function (…) { … }` — 해석기 클로저로 실행되며 코드 컴파일이 아니다. + // `.constructor`/`Function`/`eval` 접근 차단은 그대로이므로 샌드박스 탈출로 이어지지 않는다. + if (t.type === 'ident' && t.value === 'function') { + return this.parseFunctionExpression(); + } + + if (t.type === 'ident') { + const name = String(t.value); + if (FORBIDDEN_KEYWORDS.has(name)) { + throw new Error(`Keyword "${name}" is not allowed`); + } + this.pos += 1; + switch (name) { + case 'true': + return { type: 'Literal', value: true }; + case 'false': + return { type: 'Literal', value: false }; + case 'null': + return { type: 'Literal', value: null }; + case 'undefined': + return { type: 'Literal', value: undefined }; + default: + return { type: 'Identifier', name }; + } + } + + if (t.type === 'punct') { + if (t.value === '(') { + this.pos += 1; + const expr = this.parseExpression(); + this.expectPunct(')'); + return expr; + } + if (t.value === '[') { + return this.parseArrayLiteral(); + } + if (t.value === '{') { + return this.parseObjectLiteral(); + } + } + + throw new Error(`Unexpected token "${t.value}"`); + } + + private parseArrayLiteral(): Node { + this.expectPunct('['); + const elements: Node[] = []; + while (!this.isPunct(']')) { + if (this.isPunct(',')) { + // 홀(hole) 은 지원하지 않으므로 undefined 로 취급 + this.pos += 1; + elements.push({ type: 'Literal', value: undefined }); + continue; + } + if (this.isPunct('...')) { + this.pos += 1; + elements.push({ type: 'Spread', argument: this.parseExpression() }); + } else { + elements.push(this.parseExpression()); + } + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + this.expectPunct(']'); + return { type: 'Array', elements }; + } + + private parseObjectLiteral(): Node { + this.expectPunct('{'); + const properties: ObjectProperty[] = []; + while (!this.isPunct('}')) { + if (this.isPunct('...')) { + this.pos += 1; + properties.push({ kind: 'spread', value: this.parseExpression() }); + } else { + let key: Node; + let computed = false; + const t = this.peek(); + if (!t) throw new Error('Unexpected end of expression in object literal'); + + if (t.type === 'punct' && t.value === '[') { + this.pos += 1; + key = this.parseExpression(); + this.expectPunct(']'); + computed = true; + } else if (t.type === 'str') { + this.pos += 1; + key = { type: 'Literal', value: t.value }; + } else if (t.type === 'num') { + this.pos += 1; + key = { type: 'Literal', value: String(t.value) }; + } else if (t.type === 'ident') { + this.pos += 1; + key = { type: 'Literal', value: String(t.value) }; + } else { + throw new Error(`Unexpected token "${t.value}" in object literal`); + } + + if (this.isPunct(':')) { + this.pos += 1; + const value = this.parseExpression(); + properties.push({ kind: 'init', key, computed, value }); + } else { + // shorthand: { name } → { name: name } + if (computed || key.type !== 'Literal' || typeof key.value !== 'string') { + throw new Error('Invalid shorthand property in object literal'); + } + if (t.type !== 'ident') { + throw new Error('Invalid shorthand property in object literal'); + } + properties.push({ + kind: 'init', + key, + computed: false, + value: { type: 'Identifier', name: key.value }, + }); + } + } + + if (this.isPunct(',')) { + this.pos += 1; + continue; + } + break; + } + this.expectPunct('}'); + return { type: 'Object', properties }; + } +} + +/** + * 표현식 문자열을 토크나이즈·파싱해 AST 로 반환한다. + * + * 템플릿 리터럴 `${}` 보간부도 이 함수로 재귀 파싱되므로, 금지 구문 차단 로직이 + * 보간 표현식에도 동일하게 적용된다. + * + * @param expression 파싱할 표현식 소스 + * @return 파싱된 AST 루트 노드 + * @throws {Error} 파싱 오류 또는 금지 구문 발견 시 + */ +function parseToAst(expression: string): Node { + const tokens = tokenize(expression); + const parser = new Parser(tokens); + const ast = parser.parseExpression(); + + if (!parser.atEnd()) { + const t = parser.peek(); + if (t && t.type === 'punct' && t.value === ',') { + throw new Error('The comma/sequence operator is not allowed'); + } + throw new Error(`Unexpected token "${t ? t.value : '<end>'}" after expression`); + } + + return ast; +} + +/* ------------------------------------------------------------------ * + * 인터프리터 + * ------------------------------------------------------------------ */ + +function resolveIdentifier(name: string, scope: Scope): unknown { + let s: Scope | null = scope; + while (s) { + if (Object.prototype.hasOwnProperty.call(s.vars, name)) { + return s.vars[name]; + } + s = s.parent; + } + // context 에 없을 때만 전역 검사 (context 값이 항상 우선) + if (Object.prototype.hasOwnProperty.call(WHITELIST_GLOBALS, name)) { + return WHITELIST_GLOBALS[name]; + } + if (DANGEROUS_GLOBALS.has(name)) { + throw new Error(`Reference to forbidden global "${name}" is not allowed`); + } + // with(ctx) 시맨틱: 없는 식별자는 undefined + return undefined; +} + +/** + * computed 키를 프로퍼티 키로 **1회** 정규화하고 금지 프로퍼티를 차단한다. + * + * `obj[key]` 는 JS 가 key 를 ToPropertyKey 로 강제변환한다 — `['constructor']`(배열) 이나 + * `{ toString: () => 'constructor' }`(객체) 같은 비-문자열 키는 접근 시점에 'constructor' 로 + * 변환되므로, `typeof key === 'string'` 만 검사하면 우회된다(KVE-2026-1915). + * 여기서 String 강제변환을 **미리 한 번** 수행해 그 원시 문자열로 차단·접근하므로 + * 재변환(TOCTOU) 여지도 없다. 심볼 키는 금지 문자열 이름이 될 수 없어 그대로 통과시킨다. + * + * @param key 해석된 computed 키(임의 타입) + * @return 정규화된 안전한 프로퍼티 키 + * @since engine-v1.60.1 + */ +function normalizeKey(key: unknown): string | symbol { + const normalized = typeof key === 'symbol' ? key : String(key); + if (typeof normalized === 'string' && BLOCKED_PROPERTIES.has(normalized)) { + throw new Error(`Access to "${normalized}" is forbidden`); + } + return normalized; +} + +/** Member/Call 체인 하위 노드를 SHORT_CIRCUIT 전파가 가능하도록 평가 */ +function evalChainable(node: Node, scope: Scope): unknown { + if (node.type === 'Member') return evalMember(node, scope); + if (node.type === 'Call') return evalCall(node, scope); + return evalNode(node, scope); +} + +function evalMember(node: Node & { type: 'Member' }, scope: Scope): unknown { + const obj = evalChainable(node.object, scope); + if (obj === SHORT_CIRCUIT) return SHORT_CIRCUIT; + if (node.optional && (obj === null || obj === undefined)) return SHORT_CIRCUIT; + + let key: unknown; + if (node.computed) { + key = evalNode(node.property, scope); + } else { + // 비-computed 프로퍼티는 항상 Literal 문자열 + key = (node.property as { type: 'Literal'; value: unknown }).value; + } + + // 런타임 하드닝: computed 키를 1회 정규화해 금지 프로퍼티 차단 + 정규화된 키로만 접근 + const safeKey = normalizeKey(key); + + if (obj === null || obj === undefined) { + // 비-optional nullish 접근은 with(ctx) 대비 관대하게 undefined 반환 + return undefined; + } + + return (obj as Record<PropertyKey, unknown>)[safeKey]; +} + +function evalCall(node: Node & { type: 'Call' }, scope: Scope): unknown { + let fn: unknown; + let thisArg: unknown = undefined; + + if (node.callee.type === 'Member') { + const member = node.callee; + const obj = evalChainable(member.object, scope); + if (obj === SHORT_CIRCUIT) return SHORT_CIRCUIT; + if (member.optional && (obj === null || obj === undefined)) return SHORT_CIRCUIT; + + let key: unknown; + if (member.computed) { + key = evalNode(member.property, scope); + } else { + key = (member.property as { type: 'Literal'; value: unknown }).value; + } + const safeKey = normalizeKey(key); + + if (obj === null || obj === undefined) { + fn = undefined; + } else { + fn = (obj as Record<PropertyKey, unknown>)[safeKey]; + thisArg = obj; + } + } else { + fn = evalChainable(node.callee, scope); + if (fn === SHORT_CIRCUIT) return SHORT_CIRCUIT; + } + + if (node.optional && (fn === null || fn === undefined)) { + return SHORT_CIRCUIT; + } + + if (typeof fn !== 'function') { + throw new Error('Attempted to call a non-function value'); + } + + const args = evalArguments(node.args, scope); + return (fn as (...a: unknown[]) => unknown).apply(thisArg, args); +} + +function evalArguments(argNodes: Node[], scope: Scope): unknown[] { + const out: unknown[] = []; + for (const a of argNodes) { + if (a.type === 'Spread') { + const v = evalNode(a.argument, scope); + if (v !== null && v !== undefined) { + for (const x of v as Iterable<unknown>) out.push(x); + } + } else { + out.push(evalNode(a, scope)); + } + } + return out; +} + +/** + * 함수/화살표 파라미터를 로컬 스코프에 바인딩한다 (기본값 지원). + * + * 인자가 `undefined` 이고 기본값이 있으면 기본값을 로컬 스코프에서 평가한다 + * (앞선 파라미터를 참조할 수 있는 JS 시맨틱과 일치). + * + * @param params 파라미터 목록 + * @param args 실제 인자 배열 + * @param local 바인딩 대상 로컬 스코프 + * @return void + */ +function bindParams(params: Param[], args: unknown[], local: Scope): void { + for (let idx = 0; idx < params.length; idx++) { + const p = params[idx]; + let v = args[idx]; + if (v === undefined && p.default) { + v = evalNode(p.default, local); + } + if (p.elements) { + // 배열 구조분해 패턴 — JS iterator 시맨틱과 동일하게 null/undefined 는 예외 + if (v == null) { + throw new TypeError(`Cannot destructure ${String(v)}: value is not iterable`); + } + const arr = Array.isArray(v) ? v : Array.from(v as Iterable<unknown>); + for (let e = 0; e < p.elements.length; e++) { + const name = p.elements[e]; + if (name !== null) local.vars[name] = arr[e]; + } + } else if (p.name !== null) { + local.vars[p.name] = v; + } + } +} + +/** + * 함수/화살표의 블록 본문을 실행하고 `return` 값을 돌려준다. + * + * `return` 은 `ReturnSignal` 로 던져져 여기서 잡힌다. break/continue 시그널은 + * 함수 경계를 넘지 못하므로(문법상 루프 밖 사용은 파싱되지 않음) 통과시킨다. + * + * @param body Block 노드 + * @param local 함수 로컬 스코프 + * @return return 값 (없으면 undefined) + * @since engine-v1.60.0 + */ +function runFunctionBody(body: Node, local: Scope): unknown { + try { + evalNode(body, local); + } catch (e) { + if (e instanceof ReturnSignal) return e.value; + throw e; + } + return undefined; +} + +function evalNode(node: Node, scope: Scope): unknown { + switch (node.type) { + case 'Literal': + return node.value; + + case 'Identifier': + return resolveIdentifier(node.name, scope); + + case 'Member': { + const r = evalMember(node, scope); + return r === SHORT_CIRCUIT ? undefined : r; + } + + case 'Call': { + const r = evalCall(node, scope); + return r === SHORT_CIRCUIT ? undefined : r; + } + + case 'Unary': { + const v = evalNode(node.argument, scope); + switch (node.operator) { + case '!': + return !v; + case '-': + return -(v as number); + case '+': + return +(v as number); + case 'typeof': + return typeof v; + default: + throw new Error(`Unknown unary operator "${node.operator}"`); + } + } + + case 'Binary': { + const l = evalNode(node.left, scope) as never; + const r = evalNode(node.right, scope) as never; + switch (node.operator) { + case '+': + return (l as never) + (r as never); + case '-': + return (l as number) - (r as number); + case '*': + return (l as number) * (r as number); + case '/': + return (l as number) / (r as number); + case '%': + return (l as number) % (r as number); + case '===': + return l === r; + case '!==': + return l !== r; + case '==': + /* eslint-disable-next-line eqeqeq */ + return l == r; + case '!=': + /* eslint-disable-next-line eqeqeq */ + return l != r; + case '<': + return l < r; + case '>': + return l > r; + case '<=': + return l <= r; + case '>=': + return l >= r; + default: + throw new Error(`Unknown binary operator "${node.operator}"`); + } + } + + case 'Logical': { + const l = evalNode(node.left, scope); + switch (node.operator) { + case '&&': + return l ? evalNode(node.right, scope) : l; + case '||': + return l ? l : evalNode(node.right, scope); + case '??': + return l !== null && l !== undefined ? l : evalNode(node.right, scope); + default: + throw new Error(`Unknown logical operator "${node.operator}"`); + } + } + + case 'Conditional': + return evalNode(node.test, scope) + ? evalNode(node.consequent, scope) + : evalNode(node.alternate, scope); + + case 'Array': { + const out: unknown[] = []; + for (const el of node.elements) { + if (el.type === 'Spread') { + const v = evalNode(el.argument, scope); + if (v !== null && v !== undefined) { + for (const x of v as Iterable<unknown>) out.push(x); + } + } else { + out.push(evalNode(el, scope)); + } + } + return out; + } + + case 'Object': { + const out: Record<string, unknown> = {}; + for (const prop of node.properties) { + if (prop.kind === 'spread') { + const v = evalNode(prop.value, scope); + if (v !== null && v !== undefined && typeof v === 'object') { + for (const k of Object.keys(v as Record<string, unknown>)) { + defineOwn(out, k, (v as Record<string, unknown>)[k]); + } + } + } else { + let key: unknown; + if (prop.computed) { + key = evalNode(prop.key as Node, scope); + } else { + key = (prop.key as { type: 'Literal'; value: unknown }).value; + } + const value = evalNode(prop.value, scope); + defineOwn(out, String(key), value); + } + } + return out; + } + + case 'Arrow': { + const closure = scope; + const params = node.params; + const body = node.body; + const isBlock = node.isBlock; + return function arrowFn(...args: unknown[]): unknown { + const local: Scope = { vars: {}, parent: closure }; + bindParams(params, args, local); + return isBlock ? runFunctionBody(body, local) : evalNode(body, local); + }; + } + + case 'Function': { + const closure = scope; + const params = node.params; + const body = node.body; + const fname = node.name; + const fn = function namedFn(...args: unknown[]): unknown { + const local: Scope = { vars: {}, parent: closure }; + if (fname) local.vars[fname] = fn; // 재귀 참조 (function f(){ … f() … }) + bindParams(params, args, local); + return runFunctionBody(body, local); + }; + return fn; + } + + case 'Delete': { + // 파서가 argument 를 Member 로 강제함 + const m = node.argument as Node & { type: 'Member' }; + const obj = evalNode(m.object, scope); + let key: unknown; + if (m.computed) { + key = evalNode(m.property, scope); + } else { + key = (m.property as { type: 'Literal'; value: unknown }).value; + } + const safeKey = normalizeKey(key); // constructor/__proto__/prototype 삭제 차단 + if (obj === null || obj === undefined) return true; + if (typeof obj !== 'object' && typeof obj !== 'function') return true; + if (WHITELIST_GLOBAL_OBJECT_SET.has(obj as object)) { + throw new Error('delete on a built-in global object is not allowed'); + } + return delete (obj as Record<PropertyKey, unknown>)[safeKey]; + } + + /* ── statement 노드 (function/arrow 블록 본문 안에서만 도달) — @since engine-v1.60.0 ── */ + + case 'Block': { + // 블록마다 새 스코프 → const 선언은 블록 지역, 상위 스코프 값은 상속 + const child: Scope = { vars: {}, parent: scope }; + for (const s of node.body) { + evalNode(s, child); + } + return undefined; + } + + case 'VarDecl': { + for (const d of node.declarations) { + scope.vars[d.name] = evalNode(d.init, scope); + } + return undefined; + } + + case 'If': { + if (evalNode(node.test, scope)) { + evalNode(node.consequent, scope); + } else if (node.alternate) { + evalNode(node.alternate, scope); + } + return undefined; + } + + case 'ForOf': { + const iterable = evalNode(node.iterable, scope); + if (iterable !== null && iterable !== undefined) { + for (const v of iterable as Iterable<unknown>) { + const child: Scope = { vars: {}, parent: scope }; + child.vars[node.name] = v; + try { + evalNode(node.body, child); + } catch (e) { + if (e instanceof ContinueSignal) continue; + if (e instanceof BreakSignal) break; + throw e; + } + } + } + return undefined; + } + + case 'Return': + throw new ReturnSignal(node.argument ? evalNode(node.argument, scope) : undefined); + + case 'ExprStmt': + evalNode(node.expression, scope); + return undefined; + + case 'Break': + throw new BreakSignal(); + + case 'Continue': + throw new ContinueSignal(); + + case 'Empty': + return undefined; + + case 'Try': { + try { + try { + evalNode(node.block, scope); + } catch (e) { + // 제어 흐름 시그널은 catch 로 삼키지 않고 전파 + if (e instanceof ReturnSignal || e instanceof BreakSignal || e instanceof ContinueSignal) { + throw e; + } + if (node.handler) { + const child: Scope = { vars: {}, parent: scope }; + if (node.handlerParam) child.vars[node.handlerParam] = e; + evalNode(node.handler, child); + } else { + throw e; + } + } + } finally { + if (node.finalizer) evalNode(node.finalizer, scope); + } + return undefined; + } + + case 'New': { + // context 를 무시하고 오직 화이트리스트 전역 생성자만 사용 (shadowing 무력화) + const Ctor = WHITELIST_CONSTRUCTORS[node.ctor]; + if (typeof Ctor !== 'function') { + throw new Error('new on non-whitelisted constructor'); + } + const args = evalArguments(node.args, scope); + return new Ctor(...args); + } + + case 'Template': { + let out = node.quasis[0] ?? ''; + for (let idx = 0; idx < node.expressions.length; idx++) { + out += String(evalNode(node.expressions[idx], scope)); + out += node.quasis[idx + 1] ?? ''; + } + return out; + } + + case 'Spread': + // Spread 는 Array/Object/Call 컨텍스트에서만 직접 처리된다 + throw new Error('Unexpected spread element'); + + default: + throw new Error(`Unknown node type "${(node as { type: string }).type}"`); + } +} + +/** + * `__proto__` 같은 특수 키의 setter 발동을 피하기 위해 항상 own data property 로 정의. + * + * @param obj 대상 객체 + * @param key 프로퍼티 키 + * @param value 값 + * @return void + */ +function defineOwn(obj: Record<string, unknown>, key: string, value: unknown): void { + Object.defineProperty(obj, key, { + value, + enumerable: true, + writable: true, + configurable: true, + }); +} + +/* ------------------------------------------------------------------ * + * 공개 API + * ------------------------------------------------------------------ */ + +/** + * 안전한 JavaScript 표현식을 평가한다. + * + * `{{ }}` 가 이미 제거된 단일 JS 표현식 문자열을 AST 로 파싱한 뒤 context 에 + * 대해 해석한다. `eval` / `new Function` / `with` 를 사용하지 않는다. + * + * @param expression 평가할 표현식 (단일 JS 식) + * @param context 식별자 해석용 컨텍스트 객체 + * @return 표현식 평가 결과 + * @throws {Error} 파싱 오류 또는 금지 구문(보안 위반) 발견 시 + */ +export function evaluateSafeExpression(expression: string, context: Record<string, unknown>): unknown { + if (typeof expression !== 'string') { + throw new Error('Expression must be a string'); + } + + const ast = parseToAst(expression); + const rootScope: Scope = { vars: context || {}, parent: null }; + return evalNode(ast, rootScope); +} diff --git a/resources/js/core/template-engine/__tests__/ConditionEvaluator.sandbox.test.ts b/resources/js/core/template-engine/__tests__/ConditionEvaluator.sandbox.test.ts new file mode 100644 index 00000000..47c82890 --- /dev/null +++ b/resources/js/core/template-engine/__tests__/ConditionEvaluator.sandbox.test.ts @@ -0,0 +1,76 @@ +/** + * 조건 표현식 샌드박스 단위 테스트 (KVE-2026-1915) + * + * `layout-expression-sandbox.spec.ts`(Playwright) 가 배포 번들의 공개 API + * `G7Core.evaluateCondition` 로 잠그는 `window.__g7jsi` canary 축을, 그 API 가 실제로 + * 호출하는 함수 `evaluateStringCondition` 을 통해 단위 레벨에서 고정한다. E2E 미실행 + * 환경에서도 "위험 표현식은 코드를 실행하지 못한다(전역 canary 미설정)" 축이 검증된다. + * + * scripts[].if / if / classMap 등 레이아웃 조건은 전부 이 함수를 경유하므로, 이 canary 가 + * 설정되면 렌더 경로 어디서든 샌드박스가 뚫린 것이다. + * + * 효과 요약(마커 아님 — 평문): sandbox_escape_blocked, dangerous_payload_does_not_set_global. + * 실제 마커는 그 효과를 단언하는 개별 테스트에만 둔다 — 파일 레벨에 몰아 적으면 테스트를 + * 전부 지워도 커버리지가 green 으로 남는다. + */ +import { describe, it, expect, afterEach } from 'vitest'; +import { evaluateStringCondition } from '../helpers/ConditionEvaluator'; +import { DataBindingEngine } from '../DataBindingEngine'; + +const CANARY = '__g7jsi'; + +describe('조건 표현식 샌드박스 — window.__g7jsi canary (KVE-2026-1915)', () => { + const engine = new DataBindingEngine(); + + afterEach(() => { + delete (globalThis as any)[CANARY]; + delete (window as any)[CANARY]; + }); + + it('정상 조건(삼항·nullish·비교)은 렌더 경로에서 정상 평가된다', () => { + const ctx = { user: { name: '홍길동' }, count: 2 } as any; + expect(evaluateStringCondition('{{user?.name ? true : false}}', ctx, engine)).toBe(true); + expect( + evaluateStringCondition('{{(user?.missing ?? "fallback") === "fallback"}}', ctx, engine) + ).toBe(true); + expect(evaluateStringCondition('{{count > 1}}', ctx, engine)).toBe(true); + }); + + /** @effects sandbox_escape_blocked, dangerous_payload_does_not_set_global */ + it('constructor 체인 익스플로잇은 코드를 실행하지 못한다(canary 미설정 + 거부)', () => { + delete (globalThis as any)[CANARY]; + delete (window as any)[CANARY]; + + const payloads = [ + "{{''.constructor.constructor('window.__g7jsi = 1')()}}", + "{{''['constructor']['constructor']('window.__g7jsi = 2')()}}", + '{{({}).__proto__}}', + "{{Function('window.__g7jsi = 3')()}}", + "{{eval('window.__g7jsi = 4')}}", + ]; + + const results = payloads.map((p) => { + try { + return evaluateStringCondition(p, {} as any, engine); + } catch { + return false; + } + }); + + // 익스플로잇 코드가 실행되지 않았다 — 전역 canary 미설정 + expect((globalThis as any)[CANARY]).toBeUndefined(); + expect((window as any)[CANARY]).toBeUndefined(); + // 위험 표현식은 truthy 로 평가되지 않는다(거부) + expect(results.every((r) => r === false)).toBe(true); + }); + + it('화살표 함수·템플릿 리터럴 정상 표현식은 통과한다(과차단 회귀 방지)', () => { + const ctx = { items: [{ v: 'a' }, { v: 'b' }, { v: 'a' }], $args: ['x'] } as any; + expect( + evaluateStringCondition("{{(items ?? []).filter(i => i.v === 'a').length === 2}}", ctx, engine) + ).toBe(true); + expect( + evaluateStringCondition('{{`/mypage/${$args[0]}` === "/mypage/x"}}', ctx, engine) + ).toBe(true); + }); +}); diff --git a/resources/js/core/template-engine/__tests__/SafeExpressionEvaluator.test.ts b/resources/js/core/template-engine/__tests__/SafeExpressionEvaluator.test.ts new file mode 100644 index 00000000..e537adfb --- /dev/null +++ b/resources/js/core/template-engine/__tests__/SafeExpressionEvaluator.test.ts @@ -0,0 +1,897 @@ +import { describe, it, expect } from 'vitest'; +import { evaluateSafeExpression } from '../SafeExpressionEvaluator'; + +/** + * SafeExpressionEvaluator 테스트. + * + * `new Function` / `with(ctx)` 를 대체하는 안전한 표현식 인터프리터 검증. + * 실제 레이아웃 표현식 호환성 + 샌드박스 탈출 차단(보안)을 모두 다룬다. + * + * 시나리오 축(case)은 배포 번들 E2E(layout-expression-sandbox.spec.ts)가 커버하고, + * 이 파일은 소스 레벨에서 같은 효과를 떠받친다. + * + * 효과 요약(마커 아님 — 평문): sandbox_escape_blocked, + * same_expression_same_value_across_paths. 실제 마커는 그 효과를 단언하는 개별 + * 테스트에만 둔다 — 파일 레벨에 몰아 적으면 테스트를 전부 지워도 커버리지가 green 으로 남는다. + */ + +const evalx = (expr: string, ctx: Record<string, unknown> = {}): unknown => + evaluateSafeExpression(expr, ctx); + +describe('SafeExpressionEvaluator', () => { + describe('literals', () => { + it('정수/실수', () => { + expect(evalx('42')).toBe(42); + expect(evalx('3.14')).toBe(3.14); + expect(evalx('.5')).toBe(0.5); + expect(evalx('1e3')).toBe(1000); + }); + + it('문자열 (따옴표/이스케이프)', () => { + expect(evalx("'hello'")).toBe('hello'); + expect(evalx('"world"')).toBe('world'); + expect(evalx("'a\\'b'")).toBe("a'b"); + expect(evalx('"a\\"b"')).toBe('a"b'); + expect(evalx("'a\\\\b'")).toBe('a\\b'); + expect(evalx("'a\\nb'")).toBe('a\nb'); + expect(evalx("'a\\tb'")).toBe('a\tb'); + }); + + it('boolean/null/undefined', () => { + expect(evalx('true')).toBe(true); + expect(evalx('false')).toBe(false); + expect(evalx('null')).toBe(null); + expect(evalx('undefined')).toBe(undefined); + }); + + it('배열/객체 리터럴', () => { + expect(evalx('[1, 2, 3]')).toEqual([1, 2, 3]); + expect(evalx("{ a: 1, 'b-c': 2 }")).toEqual({ a: 1, 'b-c': 2 }); + }); + }); + + describe('member & optional chaining', () => { + it('점 접근', () => { + expect(evalx('a.b.c', { a: { b: { c: 5 } } })).toBe(5); + }); + + it('옵셔널 체이닝 단락', () => { + expect(evalx('a?.b', { a: null })).toBe(undefined); + expect(evalx('a?.b?.c', { a: undefined })).toBe(undefined); + expect(evalx("user?.name ?? 'Guest'", { user: null })).toBe('Guest'); + expect(evalx("user?.name ?? 'Guest'", { user: { name: 'Kim' } })).toBe('Kim'); + }); + + it('없는 식별자는 undefined (throw 하지 않음)', () => { + expect(evalx('missing')).toBe(undefined); + expect(evalx('missing?.x')).toBe(undefined); + expect(evalx("missing ?? 'fallback'")).toBe('fallback'); + }); + + it('nullish 비-optional 접근은 undefined 반환', () => { + expect(evalx('a.b', { a: undefined })).toBe(undefined); + }); + }); + + describe('computed access', () => { + it('문자열/변수 키', () => { + expect(evalx("obj['key']", { obj: { key: 9 } })).toBe(9); + expect(evalx('obj[k]', { obj: { key: 9 }, k: 'key' })).toBe(9); + expect(evalx("query['status[]']", { query: { 'status[]': ['a'] } })).toEqual(['a']); + }); + + it('옵셔널 computed', () => { + expect(evalx('a?.[0]', { a: null })).toBe(undefined); + expect(evalx('a?.[0]', { a: [7] })).toBe(7); + }); + }); + + describe('calls & methods', () => { + it('컨텍스트 함수 호출', () => { + expect(evalx("$t('some.key')", { $t: (k: string) => k })).toBe('some.key'); + }); + + it('$get 헬퍼', () => { + const $get = (obj: unknown, path: string[], def: unknown): unknown => { + let cur: unknown = obj; + for (const p of path) { + if (cur == null) return def; + cur = (cur as Record<string, unknown>)[p]; + } + return cur ?? def; + }; + expect(evalx("$get(product, ['prices', 'KRW'], 'n/a')", { product: { prices: { KRW: 1000 } }, $get })).toBe( + 1000, + ); + expect(evalx("$get(product, ['prices', 'USD'], 'n/a')", { product: { prices: { KRW: 1000 } }, $get })).toBe( + 'n/a', + ); + }); + + it('인스턴스 메서드 체인', () => { + expect(evalx('String(value).toLocaleString()', { value: 1234 })).toBe('1234'); + }); + + it('옵셔널 호출', () => { + expect(evalx('obj?.method?.()', { obj: {} })).toBe(undefined); + expect(evalx('obj?.method?.()', { obj: { method: () => 3 } })).toBe(3); + }); + + it('Math.max 등 화이트리스트 전역', () => { + expect(evalx('Math.max(0, (products?.data?.length ?? 0) - 1)', { products: { data: [1, 2, 3] } })).toBe(2); + expect(evalx('Math.max(0, (products?.data?.length ?? 0) - 1)', {})).toBe(0); + }); + }); + + describe('arrow-function array methods', () => { + it('filter', () => { + expect(evalx("['a', 'b', 'c'].filter(v => v !== 'b')")).toEqual(['a', 'c']); + }); + + it('map returning object', () => { + expect( + evalx('items.map(i => ({ id: i.id, label: i.name })).length', { + items: [ + { id: 1, name: 'x' }, + { id: 2, name: 'y' }, + ], + }), + ).toBe(2); + }); + + it('findIndex (u, i, a) 3-params dedupe', () => { + const ctx = { + list: [{ uuid: 'a' }, { uuid: 'b' }, { uuid: 'a' }], + }; + expect(evalx('list.filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i).length', ctx)).toBe(2); + }); + }); + + describe('arrow param 배열 구조분해 (engine-v1.60.5 회귀 — 장바구니/바로구매 불능)', () => { + // 구 평가기(new Function)가 허용하던 형태 전수: 저장소 레이아웃 54파일 104곳 사용 + // (`([code])` 35 · `([field, messages])` 34 · `([k, v])` 등 — _purchase_card.json 이 대표) + it('단일 요소 ([k])', () => { + expect(evalx('Object.entries({ a: 1, b: 2 }).map(([k]) => k)')).toEqual(['a', 'b']); + }); + + it('두 요소 ([k, v])', () => { + expect(evalx('Object.entries({ a: 1, b: 2 }).map(([k, v]) => k + v)')).toEqual(['a1', 'b2']); + }); + + it('선행 홀 ([, v])', () => { + expect(evalx('Object.entries({ a: 1, b: null }).filter(([, v]) => v != null).length')).toBe(1); + }); + + it('배열 인자 직접 분해', () => { + expect(evalx('[[1, 2], [3, 4]].map(([a, b]) => a * b)')).toEqual([2, 12]); + }); + + it('파라미터 기본값 조합 (([a, b] = []) =>)', () => { + expect(evalx('[undefined].map(([a, b] = [7, 8]) => (a ?? 0) + (b ?? 0))')).toEqual([15]); + }); + + it('null/undefined 인자 분해는 JS 와 동일하게 예외', () => { + expect(() => evalx('[null].map(([a]) => a)')).toThrow(); + }); + + it('실전 회귀: _purchase_card.json 장바구니 담기 본문 표현식', () => { + const ctx = { + product: { data: { id: 98, has_options: true, options: [{ id: 1 }, { id: 2 }] } }, + _local: { + selectedOptionItems: [ + { + optionId: 11, + quantity: 2, + additionalOptionSelections: { 5: 50, 6: null }, + additionalOptionCustomTexts: { 5: '각인 문구' }, + }, + ], + noOptionQuantity: 1, + }, + }; + const expr = + 'product.data?.has_options && (product.data?.options?.length ?? 0) > 1 ? ' + + '{ product_id: product.data?.id, items: (_local.selectedOptionItems ?? []).map(item => ({ ' + + 'product_option_id: item.optionId, quantity: item.quantity, ' + + 'additional_option_selections: Object.entries(item.additionalOptionSelections ?? {})' + + '.filter(([, vid]) => vid != null)' + + '.map(([gid, vid]) => ({ additional_option_id: Number(gid), value_id: Number(vid), custom_text: item.additionalOptionCustomTexts?.[Number(gid)] })) ' + + '})) } : { product_id: product.data?.id, items: [{ quantity: _local.noOptionQuantity ?? 1 }] }'; + expect(evalx(expr, ctx)).toEqual({ + product_id: 98, + items: [ + { + product_option_id: 11, + quantity: 2, + additional_option_selections: [{ additional_option_id: 5, value_id: 50, custom_text: '각인 문구' }], + }, + ], + }); + }); + + it('실전 회귀: 게시판 환경설정 권한 computed (filter([key]) + startsWith)', () => { + const ctx = { + settings: { + data: { + basic_defaults: { + default_board_permissions: { 'admin.manage': true, read: true, write: false }, + }, + }, + }, + }; + expect( + evalx( + "Object.entries(settings?.data?.basic_defaults?.default_board_permissions ?? {}).filter(([key]) => !key.startsWith('admin.')).map(([key]) => key)", + ctx, + ), + ).toEqual(['read', 'write']); + }); + + it('실전 회귀: 검증 오류 표시 ([field, messages]) — 플러그인 설정 onError 형태', () => { + const ctx = { errors: { name: ['이름은 필수입니다'], email: ['형식 오류'] } }; + expect(evalx("Object.entries(errors ?? {}).map(([field, messages]) => field + ': ' + messages[0])", ctx)).toEqual([ + 'name: 이름은 필수입니다', + 'email: 형식 오류', + ]); + }); + + it('function 선언 파라미터의 배열 구조분해', () => { + expect(evalx('(function f([a, b]) { return a + b; })([3, 4])')).toBe(7); + }); + }); + + describe('spread (array / object / call)', () => { + it('배열 스프레드', () => { + expect(evalx('[...a, ...b, 3]', { a: [1], b: [2] })).toEqual([1, 2, 3]); + }); + + it('객체 스프레드', () => { + expect(evalx('{ ...a, c: 3 }', { a: { a: 1, b: 2 } })).toEqual({ a: 1, b: 2, c: 3 }); + }); + + it('call 스프레드', () => { + expect(evalx('Math.max(...nums)', { nums: [4, 9, 2] })).toBe(9); + }); + }); + + describe('operators & precedence', () => { + it('산술 우선순위', () => { + expect(evalx('1 + 2 * 3')).toBe(7); + expect(evalx('(1 + 2) * 3')).toBe(9); + expect(evalx('10 % 3')).toBe(1); + expect(evalx('7 / 2')).toBe(3.5); + }); + + it('단항', () => { + expect(evalx('!true')).toBe(false); + expect(evalx('-5')).toBe(-5); + expect(evalx('+"3"')).toBe(3); + expect(evalx("typeof x", { x: 'str' })).toBe('string'); + expect(evalx('typeof missing')).toBe('undefined'); + }); + + it('비교/동등', () => { + expect(evalx('1 === 1')).toBe(true); + expect(evalx("1 === '1'")).toBe(false); + expect(evalx("1 == '1'")).toBe(true); + expect(evalx('2 !== 3')).toBe(true); + expect(evalx('2 < 3 && 3 <= 3')).toBe(true); + }); + + it('(count ?? 0) + 1', () => { + expect(evalx('(count ?? 0) + 1', {})).toBe(1); + expect(evalx('(count ?? 0) + 1', { count: 5 })).toBe(6); + }); + }); + + describe('ternary / nullish / logical short-circuit', () => { + it('삼항 (중첩)', () => { + expect(evalx("a ? 'x' : b ? 'y' : 'z'", { a: false, b: true })).toBe('y'); + expect(evalx("a ? 'x' : b ? 'y' : 'z'", { a: false, b: false })).toBe('z'); + }); + + it('|| 은 첫 truthy 피연산자 반환 (boolean 아님)', () => { + expect(evalx("'' || 'fallback'")).toBe('fallback'); + expect(evalx("'first' || 'second'")).toBe('first'); + expect(evalx('0 || 42')).toBe(42); + }); + + it('&& 는 첫 falsy 또는 마지막 값', () => { + expect(evalx("'a' && 'b'")).toBe('b'); + expect(evalx("0 && 'b'")).toBe(0); + }); + + it('?? 는 nullish 만 폴백', () => { + expect(evalx("0 ?? 'x'")).toBe(0); + expect(evalx("null ?? 'x'")).toBe('x'); + expect(evalx("'' ?? 'x'")).toBe(''); + }); + + it('&& 단락으로 오른쪽 미평가', () => { + let called = false; + const ctx = { + cond: false, + boom: () => { + called = true; + return 1; + }, + }; + expect(evalx('cond && boom()', ctx)).toBe(false); + expect(called).toBe(false); + }); + }); + + describe('whitelisted globals', () => { + it('JSON / Number / Boolean / parse*', () => { + expect(evalx('JSON.stringify(a)', { a: { x: 1 } })).toBe('{"x":1}'); + expect(evalx("Number('42')")).toBe(42); + expect(evalx("parseInt('10px', 10)")).toBe(10); + expect(evalx("parseFloat('3.5rem')")).toBe(3.5); + expect(evalx('isNaN(x)', { x: NaN })).toBe(true); + expect(evalx('isFinite(1)')).toBe(true); + expect(evalx('Boolean(0)')).toBe(false); + expect(evalx('Array.isArray(a)', { a: [] })).toBe(true); + }); + }); + + describe('context shadowing', () => { + it('컨텍스트 값이 전역보다 우선', () => { + expect(evalx('String', { String: 'shadowed' })).toBe('shadowed'); + expect(evalx('Math', { Math: 123 })).toBe(123); + }); + }); + + describe('template literals', () => { + it('보간 없는 템플릿', () => { + expect(evalx('`no interpolation`')).toBe('no interpolation'); + }); + + it('단일 보간', () => { + expect(evalx('`/mypage/${$args[0]}`', { $args: ['x'] })).toBe('/mypage/x'); + }); + + it('표현식 보간', () => { + expect(evalx('`total: ${(count ?? 0) + 1}`', {})).toBe('total: 1'); + expect(evalx('`total: ${(count ?? 0) + 1}`', { count: 9 })).toBe('total: 10'); + }); + + it('다중 보간 + 리터럴 조각', () => { + expect(evalx('`${a}-${b}!`', { a: 'x', b: 'y' })).toBe('x-y!'); + }); + + it('템플릿 내 이스케이프', () => { + expect(evalx('`a\\nb`')).toBe('a\nb'); + expect(evalx('`price \\${x}`')).toBe('price ${x}'); + }); + }); + + describe('new operator (whitelisted constructors)', () => { + it('new Date(str).getTime() → number', () => { + expect(evalx("new Date('2020-01-01').getTime()")).toBe(new Date('2020-01-01').getTime()); + expect(typeof evalx("new Date('2020-01-01').getTime()")).toBe('number'); + }); + + it('new Date().toISOString().slice(0,10) → 10-char string', () => { + const r = evalx('new Date().toISOString().slice(0, 10)'); + expect(typeof r).toBe('string'); + expect((r as string).length).toBe(10); + }); + + it('Array.from(new Set([...])) dedupe', () => { + expect(evalx('Array.from(new Set([1, 1, 2]))')).toEqual([1, 2]); + }); + + it('new Map([...]).get(key)', () => { + expect(evalx("new Map([['a', 1]]).get('a')")).toBe(1); + }); + + it('new Date(query.expires_at).getTime()', () => { + const ctx = { query: { expires_at: '2021-06-15T00:00:00Z' } }; + expect(evalx('new Date(query.expires_at).getTime()', ctx)).toBe( + new Date('2021-06-15T00:00:00Z').getTime(), + ); + }); + + it('Array.from(new Set(x.flatMap(...)))', () => { + const ctx = { x: [{ tags: ['a', 'b'] }, { tags: ['b', 'c'] }] }; + expect(evalx('Array.from(new Set((x ?? []).flatMap(p => p.tags)))', ctx)).toEqual(['a', 'b', 'c']); + }); + }); + + describe('real-world compatibility', () => { + /** @effects same_expression_same_value_across_paths */ + it('상태 필터 토글 (statusFilter)', () => { + const expr = + "((_global.statusFilter ?? query['status[]']) || []).includes('pending') ? ((_global.statusFilter ?? query['status[]']) || []).filter(v => v !== 'pending') : [...((_global.statusFilter ?? query['status[]']) || []), 'pending']"; + // 현재 pending 없음 → 추가 + expect(evalx(expr, { _global: { statusFilter: ['active'] }, query: {} })).toEqual(['active', 'pending']); + // 현재 pending 있음 → 제거 + expect(evalx(expr, { _global: { statusFilter: ['pending', 'active'] }, query: {} })).toEqual(['active']); + // 둘 다 없음 → 추가 + expect(evalx(expr, { _global: {}, query: {} })).toEqual(['pending']); + }); + + /** @effects same_expression_same_value_across_paths */ + it('board_managers 병합 + dedupe', () => { + const expr = + '[...(_local.form?.board_managers ?? []), ...(_local.managerSearchResults ?? [])].filter(u => ($event.target.value ?? []).includes(u.uuid)).filter((u, i, a) => a.findIndex(x => x.uuid === u.uuid) === i)'; + const ctx = { + _local: { + form: { board_managers: [{ uuid: 'a' }, { uuid: 'b' }] }, + managerSearchResults: [{ uuid: 'b' }, { uuid: 'c' }], + }, + $event: { target: { value: ['a', 'b'] } }, + }; + expect(evalx(expr, ctx)).toEqual([{ uuid: 'a' }, { uuid: 'b' }]); + }); + + /** @effects same_expression_same_value_across_paths */ + it('comment blind 로그 판정', () => { + const expr = + "comment?.abilities?.can_manage && (comment?.action_logs ?? []).filter(log => log.action === 'blind').length > 0"; + expect( + evalx(expr, { + comment: { abilities: { can_manage: true }, action_logs: [{ action: 'blind' }] }, + }), + ).toBe(true); + expect( + evalx(expr, { + comment: { abilities: { can_manage: true }, action_logs: [] }, + }), + ).toBe(false); + }); + + /** @effects same_expression_same_value_across_paths */ + it('마지막 blind 로그 reason 추출', () => { + const expr = "(post?.data?.action_logs ?? []).filter(log => log.action === 'blind').slice(-1)[0]?.reason ?? '-'"; + expect( + evalx(expr, { + post: { data: { action_logs: [{ action: 'blind', reason: 'spam' }] } }, + }), + ).toBe('spam'); + expect(evalx(expr, { post: { data: { action_logs: [] } } })).toBe('-'); + }); + }); + + // engine-v1.60.0 — statement 본문(function/arrow 블록 IIFE) 회귀 복원. + // KVE-2026-1915 로 new Function → AST 인터프리터 교체 시, 기존 30개 레이아웃이 쓰던 + // `(function(){ const …; if(…) return …; })()` 형태가 조용히 거부돼 저장이 미해석 + // 원문 문자열로 전송되던 회귀(문의 게시판 지정 실패 등)를 고정한다. + describe('statement 본문 IIFE (engine-v1.60.0 회귀 복원)', () => { + it('const 선언 + return (arrow 블록)', () => { + expect(evalx('(() => { const x = 1; return x + 1; })()')).toBe(2); + }); + + it('const 선언 + return (function 식)', () => { + expect(evalx('(function() { const a = 2; const b = 3; return a * b; })()')).toBe(6); + }); + + it('if 분기 다중 return — 탭별 payload 조립 (이커머스 설정 저장 본문)', () => { + const expr = + "(function() { const tab = _global.activeEcommerceSettingsTab || query.tab || 'basic_info'; const form = _local.form ?? {}; if (tab === 'notification_definitions') { return { _tab: 'notifications', notifications: { channels: form.notifications?.channels || [] } }; } if (tab === 'mileage') { return { _tab: 'mileage', mileage: form.mileage ?? {} }; } return { _tab: tab, [tab]: form[tab] ?? {}, inquiry: form.inquiry ?? {} }; })()"; + // 문의 게시판 지정: basic_info 탭에서 inquiry.board_slug 가 payload 에 실려야 한다 + const result = evalx(expr, { + _global: {}, + query: { tab: 'basic_info' }, + _local: { form: { basic_info: { shop_name: 'S' }, inquiry: { board_slug: 'inquiry' } } }, + }) as Record<string, unknown>; + expect(result._tab).toBe('basic_info'); + expect((result.inquiry as Record<string, unknown>).board_slug).toBe('inquiry'); + expect((result.basic_info as Record<string, unknown>).shop_name).toBe('S'); + }); + + it('new Date() + 변형 메서드 (게시판 신고 기간 프리셋)', () => { + const expr = '(() => { const d = new Date(2026, 0, 10); d.setDate(d.getDate() - 2); return d.getDate(); })()'; + expect(evalx(expr)).toBe(8); + }); + + it('for-of + continue + 재귀 arrow + 기본 파라미터 (카테고리 flatten)', () => { + const expr = + "(() => { const result = []; const flatten = (items, path = [], depth = 0) => { if (!items || !Array.isArray(items) || items.length === 0) return; for (const item of items) { if (!item || !item.id) continue; const currentPath = [...path, item.name]; result.push({ value: item.id, label: currentPath.join(' > ') }); if (item.children && Array.isArray(item.children) && item.children.length > 0 && depth < 2) { flatten(item.children, currentPath, depth + 1); } } }; flatten(categories); return result; })()"; + const out = evalx(expr, { + categories: [ + { id: 1, name: 'A', children: [{ id: 2, name: 'A1', children: [] }] }, + { id: 3, name: 'B', children: [] }, + { id: null, name: 'skip' }, + ], + }) as Array<{ value: number; label: string }>; + expect(out).toEqual([ + { value: 1, label: 'A' }, + { value: 2, label: 'A > A1' }, + { value: 3, label: 'B' }, + ]); + }); + + it('delete 로 로컬 객체 사본 프로퍼티 제거 (edit 모드 slug 제외)', () => { + const expr = '(() => { const data = {...form}; if (mode === "edit") { delete data.slug; } return data; })()'; + expect(evalx(expr, { form: { name: 'N', slug: 's' }, mode: 'edit' })).toEqual({ name: 'N' }); + expect(evalx(expr, { form: { name: 'N', slug: 's' }, mode: 'create' })).toEqual({ name: 'N', slug: 's' }); + }); + + it('function 콜백 + concat + map (게시판 카테고리 옵션)', () => { + const expr = + "[{value:'all', label:'전체'}].concat((categories ?? []).map(function(c){return{value:c,label:c};}))"; + expect(evalx(expr, { categories: ['notice', 'qna'] })).toEqual([ + { value: 'all', label: '전체' }, + { value: 'notice', label: 'notice' }, + { value: 'qna', label: 'qna' }, + ]); + }); + + it('try/catch 블록 본문 (실제 throw 를 잡음)', () => { + // 이 평가기는 null 멤버 접근을 관대하게 undefined 로 돌려주므로(with 시맨틱), + // 실제로 throw 하는 것은 비함수 호출·JSON.parse 오류 등이다. + expect(evalx('(() => { try { return notFn(); } catch (e) { return "fallback"; } })()', { notFn: 123 })).toBe('fallback'); + expect(evalx("(() => { try { return JSON.parse('{bad'); } catch (e) { return 'invalid'; } })()")).toBe('invalid'); + expect(evalx('(() => { try { return safe; } catch (e) { return "x"; } })()', { safe: 42 })).toBe(42); + }); + + it('IIFE 반환 타입 보존 — 객체/배열/빈문자열/undefined (if 판정 계약)', () => { + expect(evalx('(function(){ return {}; })()')).toEqual({}); + expect(evalx('(function(){ return []; })()')).toEqual([]); + expect(evalx('(function(){ return ""; })()')).toBe(''); + expect(evalx('(function(){ return; })()')).toBe(undefined); + }); + }); + + describe('SECURITY — sandbox escape blocked', () => { + /** @effects sandbox_escape_blocked */ + it("''.constructor.constructor('return 1')()", () => { + expect(() => evalx("''.constructor.constructor('return 1')()")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("''['constructor']['constructor']('return 1')()", () => { + expect(() => evalx("''['constructor']['constructor']('return 1')()")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('({}).__proto__', () => { + expect(() => evalx('({}).__proto__')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('[].constructor', () => { + expect(() => evalx('[].constructor')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('x.prototype', () => { + expect(() => evalx('x.prototype', { x: {} })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('Function(...)', () => { + expect(() => evalx("Function('return 1')")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('window', () => { + expect(() => evalx('window')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('globalThis', () => { + expect(() => evalx('globalThis')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('eval', () => { + expect(() => evalx("eval('1')")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('computed constructor via variable — eval-time block', () => { + expect(() => evalx("''[c]", { c: 'constructor' })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('computed __proto__ via variable — eval-time block', () => { + expect(() => evalx('o[k]', { o: {}, k: '__proto__' })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('constructor access inside arrow callback still blocked', () => { + expect(() => evalx('[1].map(x => x.constructor)')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('constructor access inside template interpolation still blocked', () => { + expect(() => evalx("`${''.constructor.constructor('x')()}`")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('new on non-whitelisted constructor rejected', () => { + expect(() => evalx('new evil()', { evil: function () {} })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('new Function still rejected', () => { + expect(() => evalx("new Function('return 1')")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('new x() with context function rejected (not whitelisted)', () => { + expect(() => evalx('new x()', { x: function () {} })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("new ''.constructor() rejected", () => { + expect(() => evalx("new ''.constructor()")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('assignment rejected', () => { + expect(() => evalx('x = 1', { x: 0 })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('increment rejected', () => { + expect(() => evalx('x++', { x: 0 })).toThrow(); + }); + + // engine-v1.60.0: 함수 표현식/블록 본문은 허용된다(해석기 클로저로 실행). + // 탈출 벡터는 "function 키워드" 가 아니라 `.constructor`/`Function`/`eval` 접근이며, + // 그 차단은 statement 본문 안에서도 그대로 유지된다. + /** @effects sandbox_escape_blocked */ + it('함수 표현식 본문에서도 constructor 탈출은 여전히 차단', () => { + expect(() => evalx('(function(){ return "".constructor; })()')).toThrow(); + expect(() => evalx('(function(){ return [].constructor.constructor("return 1")(); })()')).toThrow(); + expect(() => evalx('(() => { const c = "".constructor; return c; })()')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('함수 본문에서도 Function/eval 전역 참조는 차단', () => { + expect(() => evalx('(function(){ return Function("return 1"); })()')).toThrow(); + expect(() => evalx('(() => { return eval("1"); })()')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('블록 본문 안의 대입식은 여전히 거부', () => { + expect(() => evalx('(function(){ x = 1; return x; })()', { x: 0 })).toThrow(); + expect(() => evalx('(() => { let y = 0; y = 2; return y; })()')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('블록 본문 안에서도 비화이트리스트 new 는 차단', () => { + expect(() => evalx('(function(){ return new evil(); })()', { evil: function () {} })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('delete 로 화이트리스트 전역 프로퍼티 삭제 차단', () => { + expect(() => evalx('(function(){ delete Math.floor; return 1; })()')).toThrow(); + expect(() => evalx('(function(){ const o = {}; return delete o.constructor; })()')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('sequence operator rejected', () => { + expect(() => evalx('1, 2')).toThrow(); + }); + }); + + // KVE-2026-1915 재발 — 비-문자열 computed 키 + Object 리플렉션 static 을 통한 + // 샌드박스 탈출. 기존 SECURITY 스위트는 문자열 키(''[c], c='constructor')만 검증해 + // 아래 벡터들이 81건 green 상태에서도 그대로 실행됐다. + describe('SECURITY — non-string computed key escape blocked (KVE-2026-1915)', () => { + /** @effects sandbox_escape_blocked */ + it("''[['constructor']] — 배열 키가 'constructor' 로 강제변환되어 접근되던 결함", () => { + expect(() => evalx("''[['constructor']]")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("''[['constructor']][['constructor']]('return 1')() — 배열 키 RCE 전체 체인", () => { + expect(() => evalx("''[['constructor']][['constructor']]('return 1')()")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("''[['const' + 'ructor']] — 배열 안 문자열 조립 우회", () => { + expect(() => evalx("''[['const' + 'ructor']]")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("''[[['constructor']]] — 중첩 배열 키 우회", () => { + expect(() => evalx("''[[['constructor']]]")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('o[[k]] — 컨텍스트 값을 담은 배열 키 우회', () => { + expect(() => evalx('o[[k]]', { o: {}, k: '__proto__' })).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it("''[{ toString: () => 'constructor' }] — 객체 toString 강제변환 우회 (TOCTOU 없음: 1회 정규화)", () => { + expect(() => evalx("''[{ toString: () => 'constructor' }]")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('delete o[[key]] — 배열 키 삭제 경로도 차단', () => { + expect(() => evalx('(function(){ const o = {}; return delete o[["constructor"]]; })()')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('정상 computed 접근은 유지 — 배열/숫자/문자열 키', () => { + expect(evalx("o['a']", { o: { a: 1 } })).toBe(1); + expect(evalx('arr[0]', { arr: [42] })).toBe(42); + expect(evalx("o[k]", { o: { name: 'x' }, k: 'name' })).toBe('x'); + }); + }); + + describe('SECURITY — Object reflection statics removed (KVE-2026-1915)', () => { + /** @effects sandbox_escape_blocked */ + it('Object.getPrototypeOf 는 노출되지 않는다', () => { + expect(() => evalx('Object.getPrototypeOf(String)')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.getOwnPropertyDescriptor(...).value 리플렉션 RCE 전체 체인', () => { + expect(() => + evalx("Object.getOwnPropertyDescriptor(Object.getPrototypeOf(String), 'constructor').value('return 1')()"), + ).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.setPrototypeOf 는 노출되지 않는다 (프로토타입 오염 차단)', () => { + expect(() => evalx('Object.setPrototypeOf({}, {})')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.defineProperty 는 노출되지 않는다', () => { + expect(() => evalx("Object.defineProperty({}, 'x', { value: 1 })")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.getOwnPropertyDescriptors 는 노출되지 않는다', () => { + expect(() => evalx('Object.getOwnPropertyDescriptors(String)')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('안전한 Object 데이터 메서드는 유지 — keys/values/entries/assign/fromEntries', () => { + expect(evalx('Object.keys({ a: 1, b: 2 })')).toEqual(['a', 'b']); + expect(evalx('Object.values({ a: 1, b: 2 })')).toEqual([1, 2]); + expect(evalx('Object.entries({ a: 1 })')).toEqual([['a', 1]]); + expect(evalx('Object.assign({ a: 1 }, { b: 2 })')).toEqual({ a: 1, b: 2 }); + expect(evalx("Object.fromEntries([['a', 1]])")).toEqual({ a: 1 }); + }); + + // 회귀 방지: create 는 프로토타입/디스크립터를 읽지도 쓰지도 않으므로 유지된다. + // 실제 레이아웃(_tab_reviews.json)의 리뷰 옵션 필터 맵 생성 패턴을 고정한다. + /** @effects sandbox_escape_blocked */ + it('Object.create(null) + assign 은 유지 — 실제 레이아웃 필터 맵 패턴', () => { + expect(evalx("Object.assign(Object.create(null), { a: '1' }, { b: '2' })")).toEqual({ + a: '1', + b: '2', + }); + expect( + evalx('Object.assign(Object.create(null), base, { [k]: v })', { + base: { x: 1 }, + k: 'y', + v: 2, + }), + ).toEqual({ x: 1, y: 2 }); + }); + }); + + // ========================================== + // 공유 전역 변조 차단 (delete 가드와 대칭) + // ========================================== + // + // WHITELIST_GLOBALS 는 Math/JSON/Date 등 **실제 전역 참조**를 노출한다. delete 는 + // identity 검사로 이미 차단하지만, facade 에 남긴 assign/freeze 는 대상 객체를 + // 검사하지 않으면 같은 공유 전역을 변조할 수 있다 — 페이지 전체(엔진·모듈·플러그인)에 + // 지속되는 오염이므로 delete 와 동일 강도로 막아야 한다. + describe('공유 전역 변조 차단', () => { + /** @effects sandbox_escape_blocked */ + it('Object.assign 으로 화이트리스트 전역을 변조할 수 없다', () => { + expect(() => evalx('Object.assign(Math, { floor: 1 })')).toThrow(); + expect(() => evalx('Object.assign(JSON, { parse: 1 })')).toThrow(); + expect(Math.floor(1.5)).toBe(1); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.freeze 로 화이트리스트 전역을 동결할 수 없다', () => { + expect(() => evalx('Object.freeze(Math)')).toThrow(); + expect(Object.isFrozen(Math)).toBe(false); + }); + + /** @effects sandbox_escape_blocked */ + it('화이트리스트 생성자도 동일하게 보호된다', () => { + expect(() => evalx('Object.assign(Date, { now: 1 })')).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('일반 객체에 대한 assign·freeze 는 정상 동작한다 (과차단 회귀 방지)', () => { + expect(evalx('Object.assign({ a: 1 }, { b: 2 })')).toEqual({ a: 1, b: 2 }); + expect(evalx("Object.assign(Object.create(null), { a: '1' })")).toEqual({ a: '1' }); + expect(evalx('Object.isFrozen(Object.freeze({ a: 1 }))')).toBe(true); + expect(evalx('Object.assign(target, { b: 2 })', { target: { a: 1 } })).toEqual({ + a: 1, + b: 2, + }); + }); + }); + + describe('SECURITY — legacy 접근자를 통한 프로토타입 도달 차단 (KVE-2026-1915)', () => { + // Object.prototype 위의 legacy 접근자 4종은 프로퍼티를 **키가 아니라 문자열 인자**로 + // 지목하므로 normalizeKey 의 키 검사를 원리상 거치지 않는다. 이는 Object facade 에서 + // getPrototypeOf/setPrototypeOf/defineProperty 를 제거한 것과 같은 능력을 우회 복원한다. + // 배포 레이아웃 599개에서 이 4개 이름 사용은 0건이라 차단해도 회귀가 없다. + + afterEach(() => { + // 오염이 실제로 일어났다면 다른 테스트로 번지지 않게 정리한다. + delete (Object.prototype as Record<string, unknown>).pwned; + delete (Array.prototype as Record<string, unknown>).pwned; + }); + + it.each([ + ['__lookupGetter__', "({}).__lookupGetter__('__proto__')"], + ['__lookupSetter__', "({}).__lookupSetter__('__proto__')"], + ['__defineGetter__', "({}).__defineGetter__('x', function () { return 1; })"], + ['__defineSetter__', "({}).__defineSetter__('x', function (v) { return v; })"], + ])('%s 접근이 거부된다', (_name, expr) => { + expect(() => evalx(expr)).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('문자열 조립으로도 우회할 수 없다 (정적 검사가 못 잡는 형태)', () => { + expect(() => evalx("({})['__lookup' + 'Getter__']('__pro' + 'to__')")).toThrow(); + expect(() => evalx("({})[['__lookupGetter__']]('__proto__')")).toThrow(); + }); + + /** @effects sandbox_escape_blocked */ + it('프로토타입을 손에 넣어 전역을 오염시킬 수 없다', () => { + // .call 없이 method-position 으로도 thisArg 가 공급되므로 두 형태를 모두 고정한다. + expect(() => + evalx("({}).__lookupGetter__('__pro' + 'to__').call({}).__defineGetter__('pwned', function () { return 1; })") + ).toThrow(); + expect(() => + evalx("({ g: ({}).__lookupGetter__('__pro' + 'to__') }).g()") + ).toThrow(); + + expect((Object.prototype as Record<string, unknown>).pwned).toBeUndefined(); + expect(({} as Record<string, unknown>).pwned).toBeUndefined(); + }); + + /** @effects sandbox_escape_blocked */ + it('배열 프로토타입도 동일하게 보호된다', () => { + expect(() => evalx("[].__lookupGetter__('__pro' + 'to__').call([])")).toThrow(); + expect((Array.prototype as Record<string, unknown>).pwned).toBeUndefined(); + expect([].map).toBeTypeOf('function'); + }); + + /** @effects sandbox_escape_blocked */ + it('함수 객체의 프로토타입에도 도달할 수 없다', () => { + expect(() => + evalx("Math.floor.__lookupGetter__('__pro' + 'to__').call(Math.floor)") + ).toThrow(); + expect(Math.floor(1.5)).toBe(1); + }); + + /** @effects sandbox_escape_blocked */ + it('Object.assign 이 source 의 __proto__ 키로 프로토타입을 바꾸지 못한다', () => { + // JSON.parse 는 __proto__ 를 own enumerable 데이터 프로퍼티로 만든다. + // 네이티브 Object.assign 은 [[Set]] 으로 복사해 target 의 __proto__ setter 를 깨운다. + const state: Record<string, unknown> = { a: 1 }; + + // 금지 키는 조용히 건너뛰지 않고 거부한다 — 평가기의 다른 금지 키 처리(normalizeKey)와 + // 같은 강도. 조용한 skip 은 공격 시도를 정상 렌더로 위장한다. + expect(() => + evalx('Object.assign(state, JSON.parse(raw))', { + state, + raw: '{"__proto__":{"isAdmin":true}}', + }) + ).toThrow(); + + expect(Object.getPrototypeOf(state)).toBe(Object.prototype); + expect((state as { isAdmin?: boolean }).isAdmin).toBeUndefined(); + }); + + /** @effects sandbox_escape_blocked */ + it('정상 표현식은 그대로 통과한다 (과차단 회귀 방지)', () => { + // 배포 레이아웃이 실제로 쓰는 형태들 — toLocaleString 19곳, Object.assign 34곳, Object.create 1곳 + expect(evalx('(1234.5).toLocaleString()')).toBeTypeOf('string'); + expect(evalx("Object.assign(Object.create(null), { a: '1' })")).toEqual({ a: '1' }); + expect(evalx('Object.assign({ a: 1 }, { b: 2 })')).toEqual({ a: 1, b: 2 }); + expect(evalx('({ a: 1 }).hasOwnProperty("a")')).toBe(true); + }); + }); +}); diff --git a/resources/js/core/template-engine/__tests__/__snapshots__/BindingShape.routingParity.test.ts.snap b/resources/js/core/template-engine/__tests__/__snapshots__/BindingShape.routingParity.test.ts.snap index 4266ca47..88cf888a 100644 --- a/resources/js/core/template-engine/__tests__/__snapshots__/BindingShape.routingParity.test.ts.snap +++ b/resources/js/core/template-engine/__tests__/__snapshots__/BindingShape.routingParity.test.ts.snap @@ -86,14 +86,14 @@ exports[`BindingShape 라우팅 diff 하네스 > 단일 바인딩 판정: greedy ] `; -exports[`BindingShape 라우팅 diff 하네스 > 단일 바인딩 판정: 구 정규식과 신 정본이 갈리는 총량 1`] = `469`; +exports[`BindingShape 라우팅 diff 하네스 > 단일 바인딩 판정: 구 정규식과 신 정본이 갈리는 총량 1`] = `476`; exports[`BindingShape 라우팅 diff 하네스 > 판정이 갈리는 식의 분류별 건수 요약 1`] = ` { - "numericIndex": 39, + "numericIndex": 29, "other": 2, "quotedBracket": 22, - "total": 63, + "total": 53, } `; @@ -151,136 +151,6 @@ exports[`BindingShape 라우팅 diff 하네스 > 표현식/경로 판정: 구 "RenderHelpers": true, }, }, - { - "canonical": true, - "expr": "_local.profileErrors.bio[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.country[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.homepage[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.language[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.mobile[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.name[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.nickname[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.phone[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.signature[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, - { - "canonical": true, - "expr": "_local.profileErrors.timezone[0]", - "files": [ - "templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json", - ], - "old": { - "DataBindingEngine.isBaseExpression": false, - "DataBindingEngine.resolveBindings": false, - "DynamicRenderer/ConditionEvaluator": false, - "RenderHelpers": true, - }, - }, { "canonical": true, "expr": "['requested','sent','verified','failed','expired','cancelled','policy_violation_logged']", @@ -384,6 +254,7 @@ exports[`BindingShape 라우팅 diff 하네스 > 표현식/경로 판정: 구 "modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json", "modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_form.json", "modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json", + "plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/ckeditor5_uploads.json", "plugins/_bundled/sirsoft-gdpr/resources/layouts/admin/gdpr_consent_log.json", "templates/_bundled/sirsoft-admin_basic/layouts/_admin_base.json", "templates/_bundled/sirsoft-admin_basic/layouts/admin_module_list.json", @@ -503,6 +374,7 @@ exports[`BindingShape 라우팅 diff 하네스 > 표현식/경로 판정: 구 "modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_product_list/_partial_product_datagrid.json", "modules/_bundled/sirsoft-ecommerce/resources/layouts/admin/partials/admin_ecommerce_shipping_policy_list/_partial_datagrid.json", "modules/_bundled/sirsoft-page/resources/layouts/admin/admin_page_list.json", + "plugins/_bundled/sirsoft-ckeditor5/resources/layouts/admin/ckeditor5_uploads.json", "templates/_bundled/sirsoft-admin_basic/layouts/admin_user_list.json", "templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_activity_log_list/_partial_datagrid.json", "templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_notification_log_list/_partial_datagrid.json", diff --git a/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts b/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts index 4ed97ada..b0f75ec2 100644 --- a/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts +++ b/resources/js/core/template-engine/__tests__/troubleshooting-components.test.ts @@ -923,6 +923,56 @@ describe('트러블슈팅 회귀 테스트 - Form 자동 바인딩 bindingType expect(merged.form.method_samsungpay).toBeNull(); }); }); + + /** + * [사례 7] 자동바인딩 value 쓰기 경로는 e.target.value 를 무변환 저장한다 + * + * troubleshooting-components-form.md 사례 8 회귀 가드 (공개 #97). + * + * bindingType 미지정(RadioGroup/Select)은 value 바인딩으로 떨어지는데, + * 그 쓰기 경로(autoOnChange)는 `eventOrValue?.target?.value !== undefined ? + * eventOrValue.target.value : eventOrValue` 로 DOM 문자열을 그대로 저장한다. + * DOM 라디오의 value 는 항상 문자열이므로 boolean 필드가 이 경로로 묶이면 + * 클릭 순간 "true"/"false" 문자열이 되어 서버 boolean 규칙에서 422 가 된다. + * 표시 계층은 String === String 느슨 비교가 구제하므로 저장 시점에야 드러난다. + */ + describe('[사례 7] 자동바인딩 value 쓰기 경로의 boolean 문자열화 (공개 #97)', () => { + /** DynamicRenderer 의 value 바인딩 autoOnChange 값 추출과 동일 */ + const calcAutoOnChangeNewValue = (eventOrValue: any): any => + eventOrValue?.target?.value !== undefined ? eventOrValue.target.value : eventOrValue; + + it('라디오 change 이벤트의 e.target.value 는 문자열 그대로 저장된다 (무변환)', () => { + // DOM 라디오는 value 를 항상 문자열로 보고한다 + const domEvent = { target: { value: 'false', name: 'is_combinable', type: 'radio' } }; + + const stored = calcAutoOnChangeNewValue(domEvent); + + expect(stored).toBe('false'); + expect(typeof stored).not.toBe('boolean'); // 이것이 서버 422 의 시작점 + }); + + it('boolean 미지정 컴포넌트(RadioGroup)는 boolean 값도 value 바인딩이라 쓰기 경로가 이 함정을 탄다', () => { + // 표시(사례 3)는 value 바인딩으로 구제되지만, 쓰기는 문자열화된다 + expect(calcIsCheckedBinding(undefined, true)).toBe(false); // value 바인딩 확정 + }); + + it('해결: change 액션의 === "true" 캐스팅은 항상 boolean 을 기록한다', () => { + // 레이아웃: "form.is_combinable": "{{$event.target.value === 'true'}}" + const castYes = 'true' === 'true'; + const castNo = ('false' as string) === 'true'; + + expect(castYes).toBe(true); + expect(castNo).toBe(false); + expect(typeof castYes).toBe('boolean'); + expect(typeof castNo).toBe('boolean'); + }); + + it('해결: 표시 바인딩 String(boolean) 은 옵션 문자열 값과 정확히 매칭된다', () => { + // 레이아웃: value: "{{String(_local.form?.is_combinable ?? true)}}" + expect(String(true)).toBe('true'); + expect(String(false)).toBe('false'); + }); + }); }); describe('트러블슈팅 회귀 테스트 - 렌더링 구조', () => { @@ -1555,8 +1605,8 @@ describe('트러블슈팅 회귀 테스트 - DataGrid cellChildren 파이프', ( * 사례: cellChildren 단일 바인딩의 파이프가 적용되지 않아 셀이 비어 보임 * * 단일 바인딩 판정 후 `|` 가 복잡 표현식 문자로 분류되어 evaluateExpression 으로 - * 라우팅되면 JS 비트 OR 로 평가된다. 인자 있는 파이프는 예외(값 소실), - * 인자 없는 파이프는 조용한 오답이 된다. 라우팅 판정 자체를 고정한다. + * 라우팅되면 안 된다. engine-v1.59.0 의 안전 평가기는 비트 연산자를 거부하므로 + * raw 파이프가 흘러들면 예외가 된다(종전엔 조용한 비트 OR 오답이었다). 라우팅 판정 자체를 고정한다. * * @see docs/frontend/troubleshooting-components-datagrid.md "DataGrid cellChildren 파이프 이슈" * @see resources/js/core/template-engine/__tests__/renderItemChildren-pipe.test.ts (렌더 결과 검증) @@ -1574,11 +1624,15 @@ describe('트러블슈팅 회귀 테스트 - DataGrid cellChildren 파이프', ( expect(hasPipes("row.flag ? '$t:common.badge|count=1' : ''")).toBe(false); }); - it('파이프를 evaluateExpression 으로 보내면 비트 OR 오답이 된다 (수정 전 동작 고정)', () => { + it('파이프를 evaluateExpression 으로 보내면 거부된다 (라우팅 판정 고정)', () => { const engine = new DataBindingEngine(); - // 인자 없는 파이프: 문자열이 0 으로 붕괴 - expect(engine.evaluateExpression('row.code | uppercase', { row: { code: 'abc' } })).toBe(0); - // 인자 있는 파이프: 함수 호출 실패로 예외 + // engine-v1.59.0: 표현식은 화이트리스트 AST 평가기로 실행되며 비트 연산자(`|`)를 + // 거부한다. 종전 `new Function` 기반에서는 인자 없는 파이프가 비트 OR 로 조용히 + // 0 이 되고 인자 있는 파이프는 예외였다 — 이제 둘 다 명확히 예외로 거부된다. + // (파이프는 hasPipes 로 먼저 분리되어야 하며 evaluateExpression 에 raw 로 오면 안 됨) + expect(() => + engine.evaluateExpression('row.code | uppercase', { row: { code: 'abc' } }) + ).toThrow(); expect(() => engine.evaluateExpression("row.created_at | datetime('YYYY-MM-DD')", { row: { created_at: '2024-01-15T14:30:00' }, diff --git a/resources/js/core/template-engine/__tests__/utils/layoutTestUtils.ts b/resources/js/core/template-engine/__tests__/utils/layoutTestUtils.ts index 83b83e37..19a92e28 100644 --- a/resources/js/core/template-engine/__tests__/utils/layoutTestUtils.ts +++ b/resources/js/core/template-engine/__tests__/utils/layoutTestUtils.ts @@ -65,6 +65,8 @@ export interface LayoutJson { components?: ComponentDefinition[]; // 모달 정의 modals?: ComponentDefinition[]; + // 재사용 액션 정의 (actionRef 로 참조) + named_actions?: Record<string, any>; // 데이터 소스 data_sources?: Array<{ id: string; @@ -580,6 +582,13 @@ export function createLayoutTest( state._global = { ...state._global, ...updates }; }); + // 레이아웃 최상위 named_actions 등록 (engine-v1.19.0+) + // 미등록 시 actionRef 가 해석되지 않아 named_actions 를 쓰는 레이아웃은 + // 렌더링 테스트에서 액션이 통째로 무반응이 된다 (경고만 남고 실패하지 않는다). + if (layoutJson.named_actions) { + actionDispatcher.setNamedActions?.(layoutJson.named_actions); + } + // 글로벌 fetch 모킹 cleanupFetch = setupGlobalFetchMock(mockApiRegistry); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/LayoutEditorReducer.selectRouteExitsEditMode.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/LayoutEditorReducer.selectRouteExitsEditMode.test.ts index 95472052..ed02120e 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/LayoutEditorReducer.selectRouteExitsEditMode.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/LayoutEditorReducer.selectRouteExitsEditMode.test.ts @@ -39,6 +39,7 @@ function baseState(overrides: Partial<LayoutEditorState> = {}): LayoutEditorStat } describe('layoutEditorReducer — SELECT_ROUTE 가 별도 편집 모드를 종료', () => { + /** @effects select_route_from_separate_edit_mode_restores_route_mode_and_renders_canvas */ it('extension 편집 모드 → SELECT_ROUTE → editMode=route 복원 + 라우트 선택', () => { const start = baseState({ editMode: 'extension', diff --git a/resources/js/core/template-engine/layout-editor/__tests__/components/ComponentPalette.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/components/ComponentPalette.test.tsx index c642f55a..e93b9843 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/components/ComponentPalette.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/components/ComponentPalette.test.tsx @@ -144,6 +144,7 @@ describe('ComponentPalette — 폴백 카테고리 (componentPalette 미제공)' }); // 결함 8 — 실제 렌더 컴포넌트 태그 배지 (React 컴포넌트명 형식) + /** @effects palette_card_shows_react_component_tag_badge */ it('카드에 React 컴포넌트명 형식의 태그 배지 표시 (<Div>, <Button>)', () => { render( withTranslation( diff --git a/resources/js/core/template-engine/layout-editor/__tests__/components/EditorToolbar.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/components/EditorToolbar.test.tsx index b6a69da4..1bc2b3f4 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/components/EditorToolbar.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/components/EditorToolbar.test.tsx @@ -36,6 +36,7 @@ describe('EditorToolbar — 나가기 버튼 (결함 4)', () => { expect(exit.disabled).toBe(true); }); + /** @effects toolbar_exit_navigates_to_template_list_by_type */ it('onExit 제공 시 활성 + 클릭하면 onExit 호출', () => { const onExit = vi.fn(); render(wrap(<EditorToolbar onExit={onExit} />)); @@ -53,6 +54,7 @@ describe('EditorToolbar — 코드편집 버튼 (결함 5)', () => { expect(code.disabled).toBe(true); }); + /** @effects toolbar_edit_code_opens_text_layout_editor_in_new_window_with_route */ it('onEditCode 제공 시 활성 + 클릭하면 호출', () => { const onEditCode = vi.fn(); render(wrap(<EditorToolbar onEditCode={onEditCode} />)); @@ -79,6 +81,7 @@ describe('EditorToolbar — 버튼 잔류 테두리 (추가 결함: hover/focus expect(/(^|;)\s*background\s*:/.test(raw)).toBe(false); }); + /** @effects toolbar_buttons_hover_transition_when_enabled_not_when_disabled */ it('마우스 올렸다 치우면 hover 스타일이 원복되어 잔류 테두리 없음', () => { render(wrap(<EditorToolbar onExit={vi.fn()} onEditCode={vi.fn()} />)); const exit = screen.getByTestId('g7le-toolbar-exit') as HTMLButtonElement; @@ -92,6 +95,7 @@ describe('EditorToolbar — 버튼 잔류 테두리 (추가 결함: hover/focus expect(exit.style.borderColor).toBe(baseBorder); }); + /** @effects toolbar_buttons_no_residual_focus_outline_after_click */ it('클릭(focus)된 뒤에도 브라우저 기본 검정 outline 이 노출되지 않음 (outline:none)', () => { render(wrap(<EditorToolbar onExit={vi.fn()} onEditCode={vi.fn()} />)); const code = screen.getByTestId('g7le-toolbar-edit-code') as HTMLButtonElement; @@ -106,6 +110,7 @@ describe('EditorToolbar — 버튼 잔류 테두리 (추가 결함: hover/focus }); describe('EditorToolbar — 저장 스피너 (결함 6)', () => { + /** @effects toolbar_save_shows_spinner_while_pending_then_restores */ it('onSave 가 Promise 면 저장 중 스피너 노출 + disabled, 완료 후 복원', async () => { let resolveSave: () => void = () => {}; const onSave = vi.fn( diff --git a/resources/js/core/template-engine/layout-editor/__tests__/components/RouteTreePanel.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/components/RouteTreePanel.test.tsx index 6da1ab1d..4564e17b 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/components/RouteTreePanel.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/components/RouteTreePanel.test.tsx @@ -570,6 +570,7 @@ describe('RouteTreePanel — 라우트↔모달/확장 연결 그룹', () => { return route; } + /** @effects connected_group_collapsed_by_default */ it('연결 그룹(__conngroup__) 은 토글 가능한 헤더로 렌더 (기본 접힘 → 자식 미표시)', () => { const route = makeRouteWithConnModal('/admin/users'); renderTree([makeGroupNode('template', '템플릿', [route])]); @@ -590,6 +591,7 @@ describe('RouteTreePanel — 라우트↔모달/확장 연결 그룹', () => { expect(modalItems).toHaveLength(0); }); + /** @effects connected_group_toggles_on_header_click */ it('연결 그룹 헤더 클릭 → 펼침 → 자식 표시, 재클릭 → 접힘', () => { const route = makeRouteWithConnModal('/admin/users'); renderTree([makeGroupNode('template', '템플릿', [route])]); @@ -613,6 +615,7 @@ describe('RouteTreePanel — 라우트↔모달/확장 연결 그룹', () => { ).toHaveLength(0); }); + /** @effects modal_child_click_enters_modal_edit_keeping_host_route_highlight */ it('연결 모달 자식 클릭 → ENTER_MODAL_EDIT + 호스트 라우트 강조 유지', () => { const route = makeRouteWithConnModal('/admin/users'); renderTreeWithProbe([makeGroupNode('template', '템플릿', [route])]); @@ -646,7 +649,8 @@ describe('RouteTreePanel — 라우트↔모달/확장 연결 그룹', () => { expect(selectedModal.getAttribute('style')).toContain('rgb(239, 246, 255)'); }); - it('연결 확장 자식 클릭 → ENTER_EXTENSION_EDIT (editMode=extension)', () => { + /** @effects extension_child_click_enters_extension_edit_keeping_host_route_highlight, connected_count_stable_when_entering_extension_edit_mode */ + it('연결 확장 자식 클릭 → ENTER_EXTENSION_EDIT + 호스트 라우트 강조·연결 건수 유지', () => { const route = makeRouteNode('/admin/users', '회원 목록'); const ext = makeConnectedExtensionNode('2', '🧩 게시판 · admin_user_list'); ext.connectedHostRoutePath = '/admin/users'; @@ -674,6 +678,27 @@ describe('RouteTreePanel — 라우트↔모달/확장 연결 그룹', () => { .find((el) => el.getAttribute('data-route-kind') === 'extension')!; fireEvent.click(extItem); expect(screen.getByTestId('edit-mode-probe').textContent).toBe('extension'); + + // 호스트 라우트(/admin/users) 강조가 유지되어야 한다 — 확장 편집에 들어가도 + // "어느 화면의 확장을 고치는 중인지" 를 트리에서 잃지 않는다. + const hostRoute = screen + .getAllByTestId('g7le-route-tree-item') + .find((el) => el.getAttribute('data-route-path') === '/admin/users')!; + expect(hostRoute.getAttribute('style')).toContain('rgb(239, 246, 255)'); + + // 연결 그룹과 그 자식이 그대로 남아 있어야 한다. 편집 모드 진입이 트리를 + // 재구성하면서 연결 목록이 비면 "클릭하니 5개가 0개가 되는" 회귀가 된다. + // (헤더 라벨은 $t: 키라 테스트 dictionary 에서 건수 파라미터가 해석되지 않으므로 + // 라벨 문자열이 아니라 실제 렌더된 자식 노드 수로 판정한다.) + const connGroupAfter = screen + .getAllByTestId('g7le-route-tree-group') + .find((g) => g.getAttribute('data-route-path') === '__conngroup__/extensions//admin/users')!; + expect(connGroupAfter).toBeDefined(); + expect(connGroupAfter.getAttribute('aria-expanded')).toBe('true'); + const extItemsAfter = screen + .getAllByTestId('g7le-route-tree-item') + .filter((el) => el.getAttribute('data-route-kind') === 'extension'); + expect(extItemsAfter).toHaveLength(1); }); }); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/components/s7-property-controls.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/components/s7-property-controls.test.tsx index c33dd624..7cf2b48c 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/components/s7-property-controls.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/components/s7-property-controls.test.tsx @@ -94,6 +94,7 @@ describe('RecipePickerControls', () => { expect(onChange).toHaveBeenCalledWith('/board'); }); + /** @effects condition_param_widget_dispatches_via_widget_registry */ it('datasource-picker 후보 select → onChange(id)', () => { const onChange = vi.fn(); render( @@ -152,6 +153,7 @@ describe('RecipePickerControls', () => { describe('ActionRecipeEditor', () => { const node: EditorNode = { name: 'Button', props: {} }; + /** @effects spec_undeclared_event_hides_action_slot */ it('events 미선언 → 안내', () => { render(<ActionRecipeEditor node={node} spec={SPEC} capability={{}} t={t} onPatchNode={vi.fn()} />); expect(screen.getByTestId('g7le-action-no-events')).toBeInTheDocument(); @@ -212,6 +214,10 @@ describe('ActionRecipeEditor', () => { expect(screen.getByTestId('g7le-widget-page-picker-input')).toBeInTheDocument(); }); + /** + * @scenario action_recipe=openModal_lazy_id, followup_chain=none + * @effects action_recipe_does_not_expose_handler_jargon_to_user + */ it('레시피 친화 명칭만 노출 — 핸들러 용어 없음(추가 목록 라벨)', () => { render(<ActionRecipeEditor node={node} spec={SPEC} capability={{ events: ['onClick'] }} t={t} onPatchNode={vi.fn()} />); fireEvent.click(screen.getByTestId('g7le-action-slot-onClick-add-picker-toggle')); @@ -221,6 +227,10 @@ describe('ActionRecipeEditor', () => { expect(goToPageItem.querySelector('span')?.textContent).not.toMatch(/navigate|apiCall|handler/); }); + /** + * @scenario action_recipe=callServerThen, followup_chain=none + * @effects nested_action_list_widget_assembles_recursive_actions + */ it('callServerThen 추가 → 중첩 action-list(onSuccess) 슬롯 렌더', () => { // 최상위 actions + type:'click' const node2: EditorNode = { name: 'Button', actions: [{ type: 'click', handler: 'apiCall', target: 'x', onSuccess: [] }] }; @@ -231,6 +241,10 @@ describe('ActionRecipeEditor', () => { expect(screen.getByTestId('g7le-action-slot-onClick-edit-onSuccess')).toBeInTheDocument(); }); + /** + * @scenario action_recipe=custom_extension_recipe, followup_chain=success_and_error_both + * @effects action_recipe_reverse_matches_existing_action_to_recipe + */ it('legacy props.actions 도 읽어 친화 편집 진입 (역호환) — 저장 시 최상위로 이관', () => { const onPatch = vi.fn(); // 과거 S7 결함으로 props.actions 에 저장된 노드도 빌더가 읽어야 한다. @@ -289,6 +303,10 @@ describe('ConditionBuilder', () => { expect((patched as { props?: { if?: unknown } }).props?.if).toBeUndefined(); }); + /** + * @scenario action_recipe=custom_extension_recipe, followup_chain=none + * @effects condition_builder_preserves_advanced_handwritten_expr + */ it('고급식(직접 작성) → 보존 + 원문 표시', () => { const node2: EditorNode = { name: 'Div', if: '{{ custom.weird && expr }}', props: {} }; render(<ConditionBuilder node={node2} spec={SPEC} t={t} onPatchNode={vi.fn()} />); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/hooks/useDevicePreview.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/hooks/useDevicePreview.test.ts index aa07b169..c065ddfc 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/hooks/useDevicePreview.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/hooks/useDevicePreview.test.ts @@ -105,6 +105,10 @@ describe('useDevicePreview', () => { expect(result.current.customWidth).toBe(1024); }); + /** + * @scenario preview_device=custom, custom_width_input=in_range + * @effects preview_custom_device_uses_clamped_input_width_as_override + */ it('setCustomWidth 입력값이 custom 디바이스의 deviceWidth 로 반영', () => { const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); @@ -117,7 +121,11 @@ describe('useDevicePreview', () => { expect(result.current.customWidth).toBe(500); }); - it('setCustomWidth 는 320~1920px 범위로 클램프', () => { + /** + * @scenario preview_device=custom, custom_width_input=below_min_320 + * @effects preview_custom_width_clamped_320_to_1920 + */ + it('setCustomWidth 하한 미만(100) 은 320 으로 클램프', () => { const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); act(() => { @@ -125,13 +133,28 @@ describe('useDevicePreview', () => { result.current.setCustomWidth(100); // 하한 미만 }); expect(result.current.customWidth).toBe(320); + expect(result.current.deviceWidth).toBe(320); + }); + + /** + * @scenario preview_device=custom, custom_width_input=above_max_1920 + * @effects preview_custom_width_clamped_320_to_1920 + */ + it('setCustomWidth 상한 초과(5000) 는 1920 으로 클램프', () => { + const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); act(() => { + result.current.setDevice('custom'); result.current.setCustomWidth(5000); // 상한 초과 }); expect(result.current.customWidth).toBe(1920); + expect(result.current.deviceWidth).toBe(1920); }); + /** + * @scenario preview_device=desktop, custom_width_input=in_range + * @effects preview_non_custom_ignores_custom_width + */ it('custom 미선택(desktop) 상태에서는 customWidth 변경이 deviceWidth 에 영향 없음', () => { const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); @@ -143,4 +166,36 @@ describe('useDevicePreview', () => { expect(result.current.device).toBe('desktop'); expect(result.current.deviceWidth).toBe(1280); }); + + /** + * @scenario preview_device=tablet, custom_width_input=in_range + * @effects preview_non_custom_ignores_custom_width + */ + it('tablet 선택 상태에서는 customWidth 변경이 deviceWidth 에 영향 없음', () => { + const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); + + act(() => { + result.current.setDevice('tablet'); + result.current.setCustomWidth(500); + }); + + expect(result.current.device).toBe('tablet'); + expect(result.current.deviceWidth).toBe(820); + }); + + /** + * @scenario preview_device=mobile, custom_width_input=in_range + * @effects preview_non_custom_ignores_custom_width + */ + it('mobile 선택 상태에서는 customWidth 변경이 deviceWidth 에 영향 없음', () => { + const { result } = renderHook(() => useDevicePreview(), { wrapper: wrap() }); + + act(() => { + result.current.setDevice('mobile'); + result.current.setCustomWidth(500); + }); + + expect(result.current.device).toBe('mobile'); + expect(result.current.deviceWidth).toBe(390); + }); }); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/hooks/useRouteTree.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/hooks/useRouteTree.test.ts index ef7c7856..ea4f0182 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/hooks/useRouteTree.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/hooks/useRouteTree.test.ts @@ -402,6 +402,10 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { return group?.children?.find((c) => c.path === path); } + /** + * @scenario connection_kind=modal, host_match=match, conn_count=many + * @effects connected_modals_attached_when_host_layout_equals_route_layout + */ it('모달 host_layout 이 라우트 layout 과 일치하면 "이 화면의 모달" 자식 그룹 부착', () => { const tree = buildRouteTree({ routes: [tplRoute('/admin/users', 'admin_user_list', { title: '회원 목록' })], @@ -427,6 +431,10 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { expect(modalNode.connectedHostRoutePath).toBe('/admin/users'); }); + /** + * @scenario connection_kind=overlay, host_match=match, conn_count=one + * @effects connected_overlay_attached_when_target_layout_equals_route_layout, connected_extensions_attached_statically_by_host_layouts_without_canvas_load + */ it('overlay 확장 target_layout 이 라우트 layout 과 일치하면 "주입되는 확장" 자식 그룹 부착', () => { const tree = buildRouteTree({ routes: [tplRoute('/admin/users', 'admin_user_list')], @@ -448,6 +456,10 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { expect(extNode.connectedHostRoutePath).toBe('/admin/users'); }); + /** + * @scenario connection_kind=extension_point, host_match=match, conn_count=one + * @effects connected_extension_point_attached_when_host_layout_in_host_layouts + */ it('extension_point 타입도 hostLayouts 가 라우트 layout 과 일치하면 정적 부착 (클릭 불필요)', () => { const tree = buildRouteTree({ // EP 의 hostLayouts=['shop_checkout'] 이 라우트 layout 과 일치 → 정적 부착 @@ -468,6 +480,10 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { expect(extNode.connectedHostRoutePath).toBe('/shop/checkout'); }); + /** + * @scenario connection_kind=overlay, host_match=no_match, conn_count=zero + * @effects connected_group_omitted_when_zero_matches + */ it('hostLayouts 에 없는 라우트에는 확장 미부착', () => { const tree = buildRouteTree({ // header_ext 슬롯명을 layout 으로 가져도, hostLayouts 매칭이 아니면 부착 안 됨 @@ -483,6 +499,10 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { expect(extGroup).toBeUndefined(); }); + /** + * @scenario connection_kind=modal, host_match=no_match, conn_count=zero + * @effects connected_group_omitted_when_zero_matches + */ it('연결 모달/확장이 0건이면 연결 자식 그룹 미부착 (노이즈 방지)', () => { const tree = buildRouteTree({ routes: [tplRoute('/admin/orphan', 'orphan_layout')], @@ -497,6 +517,102 @@ describe('buildRouteTree — 라우트↔모달/확장 정적 연결', () => { expect(connGroups).toHaveLength(0); }); + /** + * 같은 호스트에 overlay 가 여러 개 붙는 경우. 1건 경로만 검증하면 두 번째 확장이 + * 조용히 누락돼도(첫 항목만 부착) 통과한다. + * + * @scenario connection_kind=overlay, host_match=match, conn_count=many + * @effects connected_overlay_attached_when_target_layout_equals_route_layout + */ + it('같은 라우트에 overlay 확장이 여러 개 매칭되면 모두 부착된다', () => { + const tree = buildRouteTree({ + routes: [tplRoute('/admin/users', 'admin_user_list')], + modals: [], + baseLayouts: [], + extensionGroups: [ + { + sourceIdentifier: 'sirsoft-board', + sourceType: 'module' as const, + sourceLabel: '게시판', + extensions: [ + { id: 10, extensionType: 'overlay' as const, targetName: 'admin_user_list', isActive: true, isModified: false, hostLayouts: ['admin_user_list'] }, + { id: 11, extensionType: 'overlay' as const, targetName: 'admin_user_list', isActive: true, isModified: false, hostLayouts: ['admin_user_list'] }, + { id: 12, extensionType: 'overlay' as const, targetName: 'other_layout', isActive: true, isModified: false, hostLayouts: ['other_layout'] }, + ], + }, + ], + moduleDisplayNames: { 'sirsoft-board': '게시판' }, + pluginDisplayNames: NO_PLUGINS, + }); + const route = findRouteNode(tree, '/admin/users')!; + const extGroup = route.children!.find((c) => c.path.startsWith('__conngroup__/extensions/'))!; + expect(extGroup).toBeDefined(); + expect(extGroup.label).toContain('count=2'); // 매칭 2건만, 무관 확장 제외 + expect(extGroup.children).toHaveLength(2); + expect(extGroup.children!.map((c) => c.extensionId).sort()).toEqual(['10', '11']); + }); + + /** + * @scenario connection_kind=extension_point, host_match=no_match, conn_count=zero + * @effects connected_group_omitted_when_zero_matches + */ + it('extension_point 의 hostLayouts 와 라우트 layout 이 불일치하면 미부착', () => { + const tree = buildRouteTree({ + routes: [tplRoute('/admin/orders', 'admin_order_list')], + modals: [], + baseLayouts: [], + extensionGroups: [ + { + sourceIdentifier: 'sirsoft-board', + sourceType: 'module' as const, + sourceLabel: '게시판', + extensions: [ + { id: 20, extensionType: 'extension_point' as const, targetName: 'header_ext', isActive: true, isModified: false, hostLayouts: ['shop_checkout'] }, + ], + }, + ], + moduleDisplayNames: { 'sirsoft-board': '게시판' }, + pluginDisplayNames: NO_PLUGINS, + }); + const route = findRouteNode(tree, '/admin/orders')!; + const extGroup = route.children?.find((c) => c.path.startsWith('__conngroup__/extensions/')); + expect(extGroup).toBeUndefined(); + }); + + /** + * @scenario connection_kind=extension_point, host_match=match, conn_count=many + * @effects connected_extension_point_attached_when_host_layout_in_host_layouts + */ + it('같은 라우트에 extension_point 가 여러 개 매칭되면 모두 부착된다', () => { + const tree = buildRouteTree({ + routes: [tplRoute('/shop/checkout', 'shop_checkout')], + modals: [], + baseLayouts: [], + extensionGroups: [ + { + sourceIdentifier: 'sirsoft-board', + sourceType: 'module' as const, + sourceLabel: '게시판', + extensions: [ + { id: 30, extensionType: 'extension_point' as const, targetName: 'header_ext', isActive: true, isModified: false, hostLayouts: ['shop_checkout'] }, + { id: 31, extensionType: 'extension_point' as const, targetName: 'footer_ext', isActive: true, isModified: false, hostLayouts: ['shop_checkout', 'shop_cart'] }, + ], + }, + ], + moduleDisplayNames: { 'sirsoft-board': '게시판' }, + pluginDisplayNames: NO_PLUGINS, + }); + const route = findRouteNode(tree, '/shop/checkout')!; + const extGroup = route.children!.find((c) => c.path.startsWith('__conngroup__/extensions/'))!; + expect(extGroup).toBeDefined(); + expect(extGroup.label).toContain('count=2'); + expect(extGroup.children!.map((c) => c.extensionId).sort()).toEqual(['30', '31']); + }); + + /** + * @scenario connection_kind=modal, host_match=match, conn_count=one + * @effects connected_modals_attached_when_host_layout_equals_route_layout + */ it('연결 그룹은 children 앞쪽 — 하위 라우트보다 먼저 배치', () => { const tree = buildRouteTree({ routes: [ diff --git a/resources/js/core/template-engine/layout-editor/__tests__/layouts/condition-toggle-render.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/layouts/condition-toggle-render.test.tsx index 73875846..7304a07f 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/layouts/condition-toggle-render.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/layouts/condition-toggle-render.test.tsx @@ -74,6 +74,7 @@ describe('합성 if 식 ↔ ConditionEvaluator 평가', () => { expect(evalSynthesized(clauses, 'or', { _global: { currentUser: {} }, posts: { data: [] } })).toBe(false); }); + /** @effects condition_synthesized_expr_evaluates_via_condition_evaluator */ it('합성식은 정확히 단일 {{ }} 한 쌍 (중첩 보간 없음)', () => { const expr = combineConditions( [{ operator: 'isLoggedIn', params: {} }, { operator: 'fieldEquals', params: { field: 'a', value: 'b' } }], diff --git a/resources/js/core/template-engine/layout-editor/__tests__/spec/actionRecipeEngine.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/spec/actionRecipeEngine.test.ts index cd1fc47a..1511eab9 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/spec/actionRecipeEngine.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/spec/actionRecipeEngine.test.ts @@ -74,16 +74,28 @@ describe('buildAction — 코어 핸들러 규칙 준수', () => { const recipes = normalizeActionRecipes(RECIPES); const byId = (id: string) => recipes.find((r) => r.id === id)!; + /** + * @scenario action_recipe=goToPage, followup_chain=none + * @effects action_recipe_build_emits_correct_handler_name_and_top_level_target, action_recipe_navigate_uses_navigate_not_nav + */ it('goToPage → navigate (nav 아님) + path 치환', () => { const a = buildAction(byId('goToPage'), { url: '/board' }); expect(a).toEqual({ handler: 'navigate', params: { path: '/board' } }); }); + /** + * @scenario action_recipe=showMessage, followup_chain=none + * @effects action_recipe_toast_handler_not_showToast + */ it('showMessage → toast (showToast 아님) + message/type', () => { const a = buildAction(byId('showMessage'), { text: '저장되었습니다', tone: 'success' }); expect(a).toEqual({ handler: 'toast', params: { message: '저장되었습니다', type: 'success' } }); }); + /** + * @scenario action_recipe=changeState, followup_chain=none + * @effects action_recipe_setState_target_top_level_not_in_params + */ it('changeState → setState + target top-level (params 밖)', () => { const a = buildAction(byId('changeState'), { key: 'activeTab', value: 'detail' }); expect(a.handler).toBe('setState'); @@ -91,11 +103,19 @@ describe('buildAction — 코어 핸들러 규칙 준수', () => { expect(a.params).toEqual({ activeTab: 'detail' }); }); + /** + * @scenario action_recipe=refreshData, followup_chain=none + * @effects action_recipe_refetchDataSource_uses_dataSourceId_key_not_id + */ it('refreshData → refetchDataSource + dataSourceId (id 아님)', () => { const a = buildAction(byId('refreshData'), { src: 'recent_posts' }); expect(a).toEqual({ handler: 'refetchDataSource', params: { dataSourceId: 'recent_posts' } }); }); + /** + * @scenario action_recipe=callServerThen, followup_chain=success_and_error_both + * @effects action_recipe_top_level_onSuccess_onError_keys_not_in_params, action_recipe_apiCall_target_outside_params_to_avoid_resolvedTarget_undefined + */ it('callServerThen → apiCall + target/onSuccess/onError 가 top-level', () => { const onSuccess = [{ handler: 'toast', params: { message: 'ok' } }]; const a = buildAction(byId('callServerThen'), { endpoint: 'admin/users', onSuccess, onError: [] }); @@ -108,6 +128,7 @@ describe('buildAction — 코어 핸들러 규칙 준수', () => { expect(a.params).toBeUndefined(); }); + /** @effects nested_action_list_widget_assembles_recursive_actions */ it('중첩 action-list — onSuccess 안에 N개 액션 배열 조립', () => { const onSuccess = [ { handler: 'toast', params: { message: 'saved' } }, @@ -137,6 +158,7 @@ describe('assertHandlerRules — 금지 별칭 교정', () => { warn.mockRestore(); }); + /** @effects action_recipe_handler_name_follows_claude_md_critical_rules */ it('올바른 핸들러는 그대로', () => { expect(assertHandlerRules({ handler: 'navigate' }).handler).toBe('navigate'); }); @@ -145,6 +167,7 @@ describe('assertHandlerRules — 금지 별칭 교정', () => { describe('matchAction — 역해석', () => { const recipes = normalizeActionRecipes(RECIPES); + /** @effects action_recipe_reverse_matches_existing_action_to_recipe */ it('navigate 액션 → goToPage 레시피 + url 복원', () => { const m = matchAction({ handler: 'navigate', params: { path: '/board' } }, recipes); expect(m?.recipeId).toBe('goToPage'); @@ -465,6 +488,7 @@ describe('resolveActionCard / summarizeAction / if (S10-1 보강)', () => { expect(card.handler).toBe('customUnknown'); }); + /** @effects action_recipe_does_not_expose_handler_jargon_to_user */ it('summarizeAction — 라벨 + 주요 입력값 (핸들러명 미노출)', () => { const recipe = recipes.find((r) => r.id === 'goToPage')!; const t = (k: string) => (k === '$t:editor.action.go_to_page.label' ? '페이지 이동' : k); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/spec/conditionRecipeEngine.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/spec/conditionRecipeEngine.test.ts index 96869158..a19f63c2 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/spec/conditionRecipeEngine.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/spec/conditionRecipeEngine.test.ts @@ -50,11 +50,13 @@ describe('buildClauseExpr — {paramKey} 치환', () => { expect(buildClauseExpr({ operator: 'isLoggedIn', params: {} }, ops)).toBe('_global?.currentUser?.uuid'); }); + /** @effects condition_recipe_array_length_uses_length_nullish_zero_fallback */ it('{src} 치환 — 옵셔널 체이닝 + 배열 길이 ?? 0 형태 보존', () => { const e = buildClauseExpr({ operator: 'dsHasData', params: { src: 'posts' } }, ops); expect(e).toBe('(posts?.data?.length ?? 0) > 0'); }); + /** @effects condition_recipe_substitutes_params_into_expr_template */ it('{field}/{value} 치환', () => { const e = buildClauseExpr({ operator: 'fieldEquals', params: { field: 'status', value: 'open' } }, ops); expect(e).toBe("status === 'open'"); @@ -71,6 +73,7 @@ describe('combineConditions — 단일 {{ }} 합성 (코어 if 규칙)', () => { expect(r).toBe('{{ _global?.currentUser?.uuid }}'); }); + /** @effects condition_recipe_wraps_entire_combined_expression_in_single_braces */ it('AND 결합 — 각 절 괄호 + && + 전체 한 쌍 {{ }}', () => { const r = combineConditions( [{ operator: 'isLoggedIn', params: {} }, { operator: 'dsHasData', params: { src: 'posts' } }], @@ -89,6 +92,7 @@ describe('combineConditions — 단일 {{ }} 합성 (코어 if 규칙)', () => { expect(r).toBe('{{ (_global?.currentUser?.uuid) || (!_global?.currentUser?.uuid) }}'); }); + /** @effects condition_builder_does_not_emit_nested_interpolation_braces */ it('중첩 보간 {{ {{x}} }} 를 만들지 않는다', () => { const r = combineConditions( [{ operator: 'fieldEquals', params: { field: 'a', value: 'b' } }, { operator: 'isLoggedIn', params: {} }], @@ -104,6 +108,7 @@ describe('combineConditions — 단일 {{ }} 합성 (코어 if 규칙)', () => { expect(combineConditions([], 'and', ops)).toBe(''); }); + /** @effects condition_recipe_uses_optional_chaining_for_null_safety */ it('fieldHasError — 옵셔널 체이닝 인덱스 접근 보존', () => { const r = combineConditions([{ operator: 'fieldHasError', params: { field: 'email' } }], 'and', ops); expect(r).toBe("{{ !!_local?.errors?.['email'] }}"); @@ -111,6 +116,7 @@ describe('combineConditions — 단일 {{ }} 합성 (코어 if 규칙)', () => { }); describe('matchSinglePreset — 역해석', () => { + /** @effects condition_builder_reverse_matches_single_preset */ it('파라미터 없는 단일 프리셋 식 → operator 복원', () => { expect(matchSinglePreset('{{ _global?.currentUser?.uuid }}', ops)).toBe('isLoggedIn'); expect(matchSinglePreset('{{_global?.currentUser?.uuid}}', ops)).toBe('isLoggedIn'); @@ -163,6 +169,7 @@ describe('parseConditionExpr — 라운드트립 역해석 (modal remount 무상 expect(r).toEqual({ clauses, combinator: 'or' }); }); + /** @effects condition_builder_preserves_advanced_handwritten_expr */ it('손작성 임의 식 → null (고급 취급)', () => { expect(parseConditionExpr('{{ custom.weird && thing }}', ops)).toBeNull(); expect(parseConditionExpr('', ops)).toBeNull(); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/spec/editorClipboardAndShortcuts.test.tsx b/resources/js/core/template-engine/layout-editor/__tests__/spec/editorClipboardAndShortcuts.test.tsx index 3d03fd67..a265ff79 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/spec/editorClipboardAndShortcuts.test.tsx +++ b/resources/js/core/template-engine/layout-editor/__tests__/spec/editorClipboardAndShortcuts.test.tsx @@ -2,8 +2,8 @@ * editorClipboardAndShortcuts.test.tsx — 클립보드 버퍼 + 단축키 디스패처 + 단축키 맵 모달 * * - * @scenario shortcut_clipboard + shortcut_dispatch + shortcut_help_modal - * @effects clipboard_session_roundtrip_strips_internal_meta, clipboard_read_returns_fresh_copy, shortcut_dispatch_guards_input_and_modal, shortcut_escape_branches_deselect_vs_exit_by_selection, shortcut_requires_selection_guard, shortcut_help_modal_lists_all_groups_from_ssot, paste_across_layouts_via_session_storage + * 축 요약(마커 아님 — 평문): shortcut_clipboard, shortcut_dispatch, shortcut_help_modal. + * 효과 요약(마커 아님 — 평문): clipboard_session_roundtrip_strips_internal_meta, clipboard_read_returns_fresh_copy, shortcut_dispatch_guards_input_and_modal, shortcut_escape_branches_deselect_vs_exit_by_selection, shortcut_requires_selection_guard, shortcut_help_modal_lists_all_groups_from_ssot, paste_across_layouts_via_session_storage. * @since engine-v1.50.0 */ @@ -21,6 +21,7 @@ afterEach(() => { cleanup(); clearClipboard(); }); beforeEach(() => clearClipboard()); describe('editorClipboard — sessionStorage round-trip', () => { + /** @effects clipboard_session_roundtrip_strips_internal_meta */ it('write→read 복원 + 내부 메타(__source) 제거', () => { const node: EditorNode = { type: 'basic', name: 'Div', __source: { kind: 'extension' } as never, children: [{ type: 'basic', name: 'Span', text: 'x', __source: {} as never }] }; expect(writeClipboard(node)).toBe(true); @@ -32,6 +33,7 @@ describe('editorClipboard — sessionStorage round-trip', () => { expect((out.children as EditorNode[])[0].text).toBe('x'); }); + /** @effects clipboard_read_returns_fresh_copy */ it('read 는 매번 새 복제본(연속 붙여넣기 안전)', () => { writeClipboard({ type: 'basic', name: 'Div' }); const a = readClipboard(); @@ -45,6 +47,7 @@ describe('editorClipboard — sessionStorage round-trip', () => { expect(hasClipboard()).toBe(false); }); + /** @effects paste_across_layouts_via_session_storage */ it('다른 레이아웃 시뮬레이션 — write 후 (네비게이션 가정) read 가능', () => { writeClipboard({ type: 'basic', name: 'Button', text: '버튼' }); // sessionStorage 는 네비게이션/리렌더에 무관하게 유지 → read 성공. @@ -70,6 +73,7 @@ describe('useEditorShortcuts — 디스패치/가드', () => { expect(copy).toHaveBeenCalledTimes(1); }); + /** @effects shortcut_dispatch_guards_input_and_modal */ it('입력칸 포커스 시 가로채지 않음', () => { const copy = vi.fn(); setup({ copy }, true); @@ -80,6 +84,7 @@ describe('useEditorShortcuts — 디스패치/가드', () => { input.remove(); }); + /** @effects shortcut_dispatch_guards_input_and_modal */ it('모달 열림 시 가로채지 않음', () => { const copy = vi.fn(); setup({ copy }, true); @@ -91,6 +96,7 @@ describe('useEditorShortcuts — 디스패치/가드', () => { backdrop.remove(); }); + /** @effects shortcut_requires_selection_guard */ it('requiresSelection: 선택 없으면 copy 무시', () => { const copy = vi.fn(); setup({ copy }, false); @@ -98,6 +104,7 @@ describe('useEditorShortcuts — 디스패치/가드', () => { expect(copy).not.toHaveBeenCalled(); }); + /** @effects shortcut_escape_branches_deselect_vs_exit_by_selection */ it('Escape: 선택 있으면 deselect, 없으면 exit', () => { const deselect = vi.fn(); const { unmount } = setup({ deselect }, true); @@ -119,6 +126,7 @@ describe('useEditorShortcuts — 디스패치/가드', () => { }); describe('ShortcutHelpModal — SSoT 기반 렌더', () => { + /** @effects shortcut_help_modal_lists_all_groups_from_ssot */ it('모든 그룹 + 대표 액션 행 렌더(키맵 SSoT)', () => { render(<ShortcutHelpModal t={t} onClose={vi.fn()} isMac={false} />); expect(screen.getByTestId('g7le-shortcut-group-clipboard')).toBeTruthy(); diff --git a/resources/js/core/template-engine/layout-editor/__tests__/spec/flexModel.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/spec/flexModel.test.ts index 1be18c1f..3430cfeb 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/spec/flexModel.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/spec/flexModel.test.ts @@ -53,6 +53,7 @@ describe('isRenderedAsFlex — computed style 기반(className 토큰 아님)', }); describe('isNodeFlexEnabled — 노드 파생(enable 컨트롤 역해석)', () => { + /** @effects flex_auto_detection_works_across_tailwind_bootstrap_native_css */ it('className 에 flex 토큰 있으면 true', () => { expect(isNodeFlexEnabled({ props: { className: 'w-full flex gap-2' } }, ENABLE_CONTROL)).toBe(true); }); @@ -93,9 +94,11 @@ describe('isNodeFlexEnabled — 노드 파생(enable 컨트롤 역해석)', () = }); describe('resolveFlexContainerMode', () => { + /** @effects flex_container_classToken_emits_per_spec_apply_type */ it('container → 항상 컨테이너 컨트롤(해제 버튼 없음)', () => { expect(resolveFlexContainerMode('container', null)).toEqual({ showContainer: true, showEnableButton: false, showDisableButton: false }); }); + /** @effects flex_item_align_self_emits_correct_recipe, flex_item_order_property_set */ it('item → 컨테이너 컨트롤 비노출', () => { expect(resolveFlexContainerMode('item', null)).toEqual({ showContainer: false, showEnableButton: false, showDisableButton: false }); }); @@ -108,6 +111,7 @@ describe('resolveFlexContainerMode', () => { showDisableButton: true, }); }); + /** @effects flex_make_box_button_when_not_rendered_as_flex */ it('auto + 노드 파생 비flex(enable 컨트롤 있음) → "정렬 박스로 만들기" 버튼만', () => { // el 이 flex 로 보여도(패치 직후 stale 가능) 노드 파생이 우선이므로 enable 버튼. vi.spyOn(window, 'getComputedStyle').mockReturnValue({ display: 'flex' } as CSSStyleDeclaration); @@ -117,6 +121,7 @@ describe('resolveFlexContainerMode', () => { showDisableButton: false, }); }); + /** @effects flex_auto_detection_uses_computed_style_not_classname */ it('auto + enable 컨트롤 없음 → computed style 폴백(flex)', () => { vi.spyOn(window, 'getComputedStyle').mockReturnValue({ display: 'flex' } as CSSStyleDeclaration); expect(resolveFlexContainerMode('auto', document.createElement('div'), false, false)).toEqual({ diff --git a/resources/js/core/template-engine/layout-editor/__tests__/utils/overlayGeometry.affordancePlacement.test.ts b/resources/js/core/template-engine/layout-editor/__tests__/utils/overlayGeometry.affordancePlacement.test.ts index 3158d508..d4d14d0a 100644 --- a/resources/js/core/template-engine/layout-editor/__tests__/utils/overlayGeometry.affordancePlacement.test.ts +++ b/resources/js/core/template-engine/layout-editor/__tests__/utils/overlayGeometry.affordancePlacement.test.ts @@ -30,11 +30,13 @@ function overlaps( } describe('resolveAffordancePlacement', () => { + /** @effects overlay_affordances_inside_when_box_ge_44 */ it('임계값(44px) 이상의 큰 박스는 inside 배치', () => { expect(resolveAffordancePlacement(box(200, 100))).toBe('inside'); expect(resolveAffordancePlacement(box(44, 44))).toBe('inside'); }); + /** @effects overlay_affordances_outside_when_box_lt_44_no_overlap */ it('width 가 임계값 미만이면 outside 배치 (겹침 회피)', () => { expect(resolveAffordancePlacement(box(20, 100))).toBe('outside'); }); @@ -92,6 +94,7 @@ describe('resolveInsertionCrossOffsets', () => { expect(-push - half + INSERTION_AFFORDANCE_BUTTON).toBe(-gap); // 버튼 하단 = -gap }); + /** @effects insertion_affordances_cross_layout_when_box_lt_44_no_overlap_no_clip */ it('네 버튼은 서로 겹치지 않는다 (작은 6×20 박스)', () => { const c = resolveInsertionCrossOffsets(box(6, 20)); const dirs = ['above', 'below', 'left', 'right'] as const; diff --git a/resources/views/admin.blade.php b/resources/views/admin.blade.php index 6862e064..7b655e15 100644 --- a/resources/views/admin.blade.php +++ b/resources/views/admin.blade.php @@ -49,6 +49,7 @@ bundleUrls: @json($bundleUrls ?? null), activeModules: @json($activeModulesMeta ?? []), activePlugins: @json($activePluginsMeta ?? []), + trustedScriptHosts: @json($trustedScriptHosts ?? []), appConfig: @json($appConfig ?? []), // 레이아웃 편집기 lazy 번들 URL — `/admin/layout-editor/*` 진입 시에만 런타임 // <script> 주입으로 로드된다(초기 접속 payload 에 미포함). filemtime 캐시버스팅, diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 8d2f66b5..6fd257a0 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -49,6 +49,7 @@ bundleUrls: @json($bundleUrls ?? null), activeModules: @json($activeModulesMeta ?? []), activePlugins: @json($activePluginsMeta ?? []), + trustedScriptHosts: @json($trustedScriptHosts ?? []), appConfig: @json($appConfig ?? []), // 레이아웃 편집기 lazy 번들 URL — `/admin/layout-editor/*` 진입 시에만 런타임 // <script> 주입으로 로드된다(초기 접속 payload 에 미포함). filemtime 캐시버스팅, diff --git a/resources/views/dev-dashboard.blade.php b/resources/views/dev-dashboard.blade.php index db029443..c2d5df4c 100644 --- a/resources/views/dev-dashboard.blade.php +++ b/resources/views/dev-dashboard.blade.php @@ -1070,6 +1070,46 @@ if (isset($_GET['ajax_action'])) { <span>확장 번들 정리</span> <span class="text-[10px] opacity-60">(ext-bundles:cleanup)</span> </button> + <button onclick="runCommand('seo:prune-stats')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>SEO 캐시 통계 정리</span> + <span class="text-[10px] opacity-60">(seo:prune-stats)</span> + </button> + <button onclick="runCommand('schedules:prune-history')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>스케줄 이력 정리</span> + <span class="text-[10px] opacity-60">(schedules:prune-history)</span> + </button> + <button onclick="runCommand('activity-log:prune')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>활동 로그 정리</span> + <span class="text-[10px] opacity-60">(activity-log:prune)</span> + </button> + <button onclick="runCommand('notification-log:prune')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>알림 발송 이력 정리</span> + <span class="text-[10px] opacity-60">(notification-log:prune)</span> + </button> + <button onclick="runCommand('attachments:prune-orphans --dry-run')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>고아 첨부 정리 (미리보기)</span> + <span class="text-[10px] opacity-60">(attachments:prune-orphans --dry-run)</span> + </button> + <button onclick="runCommand('attachments:prune-orphans')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>고아 첨부 정리 (실삭제)</span> + <span class="text-[10px] opacity-60">(attachments:prune-orphans)</span> + </button> + <button onclick="runCommand('storage:prune-leftovers --dry-run')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>스토리지 잔존물 정리 (미리보기)</span> + <span class="text-[10px] opacity-60">(storage:prune-leftovers --dry-run)</span> + </button> + <button onclick="runCommand('storage:prune-leftovers')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>스토리지 잔존물 정리 (실행)</span> + <span class="text-[10px] opacity-60">(storage:prune-leftovers)</span> + </button> + <button onclick="runCommand('identity:expire-challenges')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>본인인증 challenge 만료 처리</span> + <span class="text-[10px] opacity-60">(identity:expire-challenges)</span> + </button> + <button onclick="runCommand('identity:prune-logs')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-amber-600 hover:bg-amber-700 text-white text-xs font-medium rounded transition-colors"> + <span>본인인증 이력 파기</span> + <span class="text-[10px] opacity-60">(identity:prune-logs)</span> + </button> <button onclick="runCommand('geoip:update')" class="inline-flex items-center gap-1.5 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded transition-colors"> <span>GeoIP DB 갱신</span> <span class="text-[10px] opacity-60">(geoip:update)</span> diff --git a/routes/api.php b/routes/api.php index caba75c5..a64b4d81 100644 --- a/routes/api.php +++ b/routes/api.php @@ -358,8 +358,10 @@ Route::get('attachment/{hash}', [PublicAttachmentController::class, 'download']) ->where('hash', '[a-zA-Z0-9]{12}') ->name('api.attachment.download'); -// 통합 검색 API (공개) -Route::get('search', [PublicSearchController::class, 'search'])->name('api.search'); +// 통합 검색 API (공개 — Bearer 토큰이 있으면 회원으로 해석해 게시판별 열람 권한을 +// 검색 결과·available_boards 에 반영한다. 미들웨어가 없으면 $request->user() 가 +// 항상 null 이라 인증 회원도 guest 수준으로 필터된다.) +Route::get('search', [PublicSearchController::class, 'search'])->middleware('optional.sanctum')->name('api.search'); // 관리자 API (인증 + 관리자 권한 필요, 속도 제한 적용) Route::prefix('admin')->middleware(['auth:sanctum', 'check.user_status', 'admin', 'throttle:'.config('auth.throttle.admin')])->group(function () { diff --git a/routes/console.php b/routes/console.php index 8e7f739e..1421a0a7 100644 --- a/routes/console.php +++ b/routes/console.php @@ -29,8 +29,42 @@ Schedule::command('dashboard:broadcast-resources') ->withoutOverlapping(5) ->runInBackground(); +/* +| 만료 데이터 정리 예약 (주기는 항목별, 다중 서버 1대만) +| +| 시각 해석 기준은 사이트 설정 시간대(`general.timezone`)다 — +| SettingsServiceProvider 가 `app.schedule_timezone` 을 세팅하고, +| Laravel 이 그 값을 Schedule 인스턴스 전체에 일괄 적용한다. +| 서머타임이 있는 시간대에서는 전환일에 특정 시각이 건너뛰거나 두 번 발생할 수 있으나, +| 아래 정리 배치는 모두 멱등이라 이중 실행이 데이터를 해치지 않고 +| 건너뛴 날은 다음 날 배치가 흡수한다. +| +| 아래 begin/end 표식 사이가 정리 예약의 전부다. 등록 전수 회귀 테스트가 이 구간을 +| 읽어 대상 목록을 도출하므로, 정리 예약을 추가할 때는 반드시 이 안에 둔다. +*/ +// gc-schedules:begin // 만료된 레이아웃 미리보기 정리 (30분마다) -Schedule::command('layout-previews:cleanup')->everyThirtyMinutes(); +Schedule::command('layout-previews:cleanup')->everyThirtyMinutes()->onOneServer(); + +Schedule::command('sanctum:prune-expired', ['--hours' => 24])->dailyAt('04:00')->onOneServer(); +Schedule::command('auth:clear-resets')->dailyAt('04:05')->onOneServer(); +Schedule::command('queue:prune-failed')->dailyAt('04:10')->onOneServer(); +Schedule::command('queue:prune-batches')->dailyAt('04:15')->onOneServer(); +Schedule::command('notification:cleanup')->dailyAt('04:20')->onOneServer(); +Schedule::command('ext-bundles:cleanup')->dailyAt('04:25')->onOneServer(); +Schedule::command('seo:prune-stats')->dailyAt('04:30')->onOneServer(); +Schedule::command('schedules:prune-history')->dailyAt('04:35')->onOneServer(); +Schedule::command('identity:prune-logs')->dailyAt('04:40')->onOneServer(); +Schedule::command('activity-log:prune')->dailyAt('04:45')->onOneServer(); +Schedule::command('notification-log:prune')->dailyAt('04:50')->onOneServer(); +// 사용자 파일을 파기하므로 기본 꺼짐 — 커맨드가 `--scheduled` 에서 설정을 false 폴백으로 재확인한다. +Schedule::command('attachments:prune-orphans --scheduled')->dailyAt('04:55')->onOneServer(); +// 중단된 업데이트·설치가 남긴 임시 산출물 + 오래된 백업본(최신 1개 보존) 정리 +Schedule::command('storage:prune-leftovers')->dailyAt('05:00')->onOneServer(); + +// 만료 시각이 지난 본인인증 challenge 상태 전환 (비파괴 — 물리 파기는 identity:prune-logs) +Schedule::command('identity:expire-challenges')->hourly()->onOneServer(); +// gc-schedules:end // 언어팩 업데이트 확인 (주 1회, GitHub 기반 언어팩 latest_version 갱신) Schedule::command('language-pack:check-updates') diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore index 05c4471f..7a0b408d 100644 --- a/storage/framework/.gitignore +++ b/storage/framework/.gitignore @@ -2,6 +2,7 @@ compiled.php config.php down events.scanned.php +lsp-*.php maintenance.php routes.php routes.scanned.php diff --git a/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md b/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md index 362bb23a..072e5e09 100644 --- a/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md +++ b/templates/_bundled/sirsoft-admin_basic/CHANGELOG.md @@ -4,6 +4,40 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.0.5] - 2026-08-19 + +### Added + +- 환경설정 > 드라이버 > 파일 스토리지에 S3 호환 스토리지 설정이 추가되었습니다 — 엔드포인트 URL 입력칸과 Path-style 주소 토글로 Cloudflare R2, MinIO 등도 연결할 수 있습니다. (#99 @lyg-kaban 님께서 제보해주셨습니다.) +- 환경설정 > 드라이버 탭에 「공개 자산 스토리지」 카드가 추가되었습니다. 완전 공개 자산(상품·카테고리·리뷰·에디터 이미지 등)을 직접 URL(CDN)로 서빙할 디스크를 고를 수 있으며, 기본값(사용 안 함)이면 종전 스트리밍 방식 그대로입니다. 파일 스토리지는 로컬로 두고 공개 자산만 S3 로 보내는 구성도 가능하며, 이 경우에도 S3 접속 정보 입력칸이 함께 표시되고 저장 전 연결 테스트를 요구합니다. (#100 @lyg-kaban 님께서 건의해주셨습니다.) +- 공개 자산 스토리지를 외부 주소(CDN)로 설정하면 관리자 화면의 첨부·이미지 미리보기도 그 주소를 그대로 사용합니다 — 별도의 스토리지 CORS 설정 없이 표시됩니다. +- 환경설정 > 업로드 탭에 「고아 첨부 정리」 카드가 추가되었습니다 — 폼을 저장하지 않고 나가서 남은 첨부파일의 자동 정리 여부와 보존기간을 지정할 수 있습니다(기본 꺼짐). +- 회원 정보 수정 화면에서 상태를 '탈퇴'로 바꿔 저장하면 확인 창을 거칩니다 — 이메일·이름·닉네임 익명화 등 되돌릴 수 없는 처리가 함께 일어나므로 무엇이 처리되는지 먼저 알려 드립니다. 회원 등록 화면에서는 '탈퇴' 상태를 선택할 수 없습니다. (#112 @Tuwasduliebst 님께서 제보해주셨습니다.) + +### Changed + +- 환경설정 > 드라이버 탭의 검색엔진 선택 목록이 서버가 제공하는 목록을 그대로 사용하도록 바뀌었습니다 — 검색엔진 플러그인을 설치하면 그 엔진이 목록에 자동으로 나타납니다. 이전에는 기본 제공 엔진 하나만 고를 수 있었습니다. +- 드라이버·발송 방식 선택란에 저장된 값이 현재 사용할 수 없는 값이면(해당 플러그인을 삭제한 경우 등) 선택란 아래에 그 값과 함께 안내가 표시됩니다. 이전에는 선택란이 빈 칸으로만 보여 무엇이 저장돼 있었는지 알 수 없었습니다. +- S3 리전이 5개 목록 선택에서 자유 입력으로 바뀌었습니다 — 새로 생기는 AWS 리전이나 R2 의 `auto` 값을 그대로 입력할 수 있습니다. +- S3 URL 항목의 이름과 설명을 "공개 URL(CDN)" 용도로 명확히 했습니다 — API 요청 주소는 새로 생긴 엔드포인트 URL 칸에 입력합니다. +- 웹소켓 연결 테스트가 서버 발송용 주소까지 함께 검사하도록 테스트 요청 항목이 확장되었습니다. +- 코어 최소 요구 버전을 7.0.7 로 상향했습니다 (S3 엔드포인트·리전 자유 입력, 공개 자산 스토리지 등 신규 설정 키가 코어 7.0.7 부터 지원됩니다). + +### Fixed + +- 회원 등록 화면의 상태 선택란이 기본값(활성) 없이 빈 칸으로 표시되던 문제를 수정했습니다. 표시만 비어 있었을 뿐 저장 시에는 '활성' 으로 등록되어, 화면 표시와 실제 저장값이 어긋나 보였습니다. +- 파일 업로더 썸네일의 업로드 진행률 막대와 오류 표시가 다크 모드에서 대비가 낮아 잘 보이지 않던 부분을 보완했습니다. +- 언어팩 재활성화 안내 창의 아이콘이 의도한 크기와 다르게 보이던 문제를 수정했습니다. +- 표 하단 합계 영역의 값이 계산되지 않고 표현식 원문 그대로 표시되던 문제를 다시 수정했습니다. 1.0.4 에 수록 예정이던 수정이 빌드 산출물에 반영되지 않은 채 배포되어, 주문 상세의 합계 행에서 수량·적립금·배송비 자리에 값 대신 알 수 없는 문자열이 보였습니다. (#98 @hwaryeon1234 님께서 제보해주셨습니다.) +- 회원 정보 수정 화면에서 회원 정보가 화면에 채워지기 전에 저장을 누르면 "이름은 필수입니다" 같은 오류로 저장이 거절되던 문제를 수정했습니다. 이제 회원 정보를 불러오는 동안에는 저장 버튼이 잠시 비활성화됩니다. +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 회원 목록에서 여러 명을 골라 일괄 탈퇴할 때, 탈퇴할 수 없는 계정(관리자 등)이 섞여 있으면 아무 일도 일어나지 않았는데 "탈퇴 처리되었습니다" 로 안내되던 문제를 수정했습니다. 이제 처리된 인원과 처리하지 못한 인원, 그리고 그 사유를 함께 알려 드립니다. +- 목록에서 항목을 선택하면 선택 건수가 `{1}개 선택됨` 처럼 중괄호가 남은 채 표시되던 문제를 수정했습니다. +- 목록에서 항목을 선택한 뒤 검색하거나 페이지를 넘기면, 화면에서 사라진 항목이 선택된 채로 남아 일괄 처리 대상에 포함되던 문제를 수정했습니다. 확인 창은 건수만 알려 주므로 실행 전에 알아챌 방법이 없었습니다. 이제 일괄 처리 대상은 언제나 화면에 보이면서 체크된 항목뿐입니다. 회원 목록·활동 기록·알림 발송 기록·스케줄 목록에 적용됩니다. +- 환경설정에서 사이트 로고를 목록에서 빼도 화면에 그대로 남아, 로고 교체가 저장되지 않던 문제를 수정했습니다. +- 스케줄 관리의 등록/수정 창에서 저장 버튼을 눌러도 아무 반응이 없던 문제를 수정했습니다. 함께, 작업 유형·실행 주기 선택란이 빈 값으로 표시되고 선택·켜기/끄기 값이 저장 요청에 실리지 않아 저장이 거절되던 문제도 수정했습니다. 실행 주기를 직접 입력(사용자 정의)하지 않은 경우에는 선택한 주기에 맞는 실행 시각이 자동으로 저장됩니다. +- 관리자 화면 전반(사이트 환경설정·역할/사용자 저장·메뉴 순서 변경·레이아웃 미리보기·메일/알림 템플릿·본인인증 메시지/정책·언어팩 재활성화·드라이버 연결 테스트 등)에서 저장·실행이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. + ## [1.0.4] - 2026-08-10 ### Added diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx new file mode 100644 index 00000000..210828c5 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx @@ -0,0 +1,102 @@ +/** + * 공개 자산(S3/CDN) 교차 출처 URL 의 인증 요청 차단 회귀 테스트 + * + * 공개 자산 스토리지를 켜면 첨부·이미지의 `download_url` 이 외부 origin 절대 URL 이 된다. + * 그 URL 을 인증 XHR(Blob)로 가져오면 + * ① CORS 헤더를 주지 않는 CDN(기본 설정 S3 버킷 등)에서 이미지가 통째로 실패하고 + * ② 응답을 읽을 수 있는 CDN 이라면 세션 토큰이 제3자 origin 으로 전송된다. + * 두 경로 모두 브라우저 실측으로 확인된 결함이므로, 교차 출처 URL 은 + * 인증 요청 없이 그대로 사용해야 한다. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { isCrossOriginAssetUrl } from '../../src/components/composite/assetOrigin'; +import { SortableThumbnailItem } from '../../src/components/composite/FileUploader/SortableThumbnailItem'; + +const apiGet = vi.fn(); + +beforeEach(() => { + apiGet.mockReset(); + apiGet.mockResolvedValue(new Blob(['x'], { type: 'image/png' })); + // 전역 G7Core 객체는 setup 이 만든 것을 그대로 쓰고 api 만 주입한다 + // (컴포넌트가 모듈 로드 시점에 같은 객체 참조를 캡처하므로 교체하면 안 된다) + (window as any).G7Core.api = { get: apiGet }; + (window as any).URL.createObjectURL = vi.fn(() => 'blob:mock-object-url'); + (window as any).URL.revokeObjectURL = vi.fn(); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('isCrossOriginAssetUrl', () => { + it('상대 경로(API 스트리밍)는 동일 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('/api/modules/sirsoft-ecommerce/product-image/abc')).toBe(false); + }); + + it('동일 출처 절대 URL 은 동일 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl(`${window.location.origin}/storage/a.png`)).toBe(false); + }); + + it('외부 CDN 절대 URL 은 교차 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('https://bucket.s3.ap-southeast-2.amazonaws.com/a.png')).toBe(true); + }); + + it('프로토콜 상대 URL 도 호스트가 다르면 교차 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('//cdn.example.com/a.png')).toBe(true); + }); + + it('빈 값은 동일 출처로 간주해 기존 경로를 유지한다', () => { + expect(isCrossOriginAssetUrl(undefined)).toBe(false); + expect(isCrossOriginAssetUrl('')).toBe(false); + }); +}); + +/** + * 썸네일 항목 렌더용 첨부 픽스처를 만듭니다. + * + * @param downloadUrl 첨부의 download_url + * @returns 첨부 객체 + */ +const makeAttachment = (downloadUrl: string) => ({ + id: 1, + hash: 'h1', + original_filename: 'probe.png', + size: 100, + mime_type: 'image/png', + is_image: true, + download_url: downloadUrl, +}); + +// @scenario consumer=product, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row +// @effects cross_origin_asset_request_omits_session_token +describe('SortableThumbnailItem 의 공개 자산 URL 처리', () => { + it('동일 출처 URL 은 종전대로 인증 요청으로 이미지를 로드한다', async () => { + render( + <SortableThumbnailItem + file={makeAttachment('/api/modules/sirsoft-ecommerce/product-image/abc') as any} + onRemove={vi.fn()} + /> + ); + + await waitFor(() => expect(apiGet).toHaveBeenCalledTimes(1)); + expect(apiGet).toHaveBeenCalledWith( + '/api/modules/sirsoft-ecommerce/product-image/abc', + { responseType: 'blob' } + ); + }); + + it('교차 출처 공개 자산 URL 은 인증 요청 없이 그대로 사용한다', async () => { + const cdnUrl = 'https://bucket.s3.ap-southeast-2.amazonaws.com/images/products/a.png'; + + render(<SortableThumbnailItem file={makeAttachment(cdnUrl) as any} onRemove={vi.fn()} />); + + await waitFor(() => { + expect(screen.getByRole('img')).toHaveAttribute('src', cdnUrl); + }); + expect(apiGet).not.toHaveBeenCalled(); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-identity-challenge-modal.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-identity-challenge-modal.test.tsx index 867ffca4..390da628 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-identity-challenge-modal.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-identity-challenge-modal.test.tsx @@ -129,6 +129,7 @@ describe('Admin 본인인증 모달 cancel 흐름 (sirsoft-admin_basic)', () => expect(onError[0].handler).toBe('suppress'); }); + /** @effects admin_basic_modal_cancel_flow_matches_basic_template */ it('cancel sequence 가 [apiCall, resolveIdentityChallenge, closeModal] 순서', () => { const seq = findCancelSequence(); const inner = (seq!.params!.actions ?? []) as Action[]; diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-schedule-form-modal-submit.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-schedule-form-modal-submit.test.tsx new file mode 100644 index 00000000..86c3e141 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-schedule-form-modal-submit.test.tsx @@ -0,0 +1,107 @@ +/** + * @file admin-schedule-form-modal-submit.test.tsx + * @description 스케줄 생성/수정 모달 — 저장 버튼의 폼 연결 회귀 + * + * 배경: + * footer 저장 버튼은 Form 밖에 있어 `props.form="{form id}"` (HTML form 속성)으로 + * 연결된다. 참조 대상 Form 에 `props.id` 가 없으면 브라우저는 버튼을 어떤 폼에도 + * 연결하지 않아 저장 클릭이 아무 일도 하지 않는다 — 예외도 콘솔 오류도 없이 + * 스케줄 등록/수정 저장만 조용히 불능이 된다 (menu_form 패턴 참조: 버튼 + * `form: "menu_form"` ↔ Form `id: "menu_form"`). + */ + +import { describe, it, expect } from 'vitest'; + +const formModal = require('../../layouts/partials/admin_schedule_list/_modal_form.json'); + +function collectNodes(node: any, predicate: (n: any) => boolean): any[] { + const result: any[] = []; + const visit = (n: any) => { + if (!n || typeof n !== 'object') return; + if (Array.isArray(n)) { + n.forEach(visit); + return; + } + if (predicate(n)) result.push(n); + if (n.children) visit(n.children); + }; + visit(node); + return result; +} + +describe('스케줄 폼 모달 — 저장 버튼 폼 연결', () => { + it('submit 버튼의 form 참조마다 같은 id 의 Form 이 존재한다', () => { + const submitButtons = collectNodes( + formModal, + (n) => n.name === 'Button' && n.props?.type === 'submit' && typeof n.props?.form === 'string' + ); + expect(submitButtons.length, 'footer 저장 버튼(form 참조)이 있어야 한다').toBeGreaterThan(0); + + const formIds = collectNodes(formModal, (n) => n.name === 'Form').map((n) => n.props?.id); + + for (const button of submitButtons) { + expect( + formIds, + `저장 버튼이 참조하는 form="${button.props.form}" 에 해당하는 Form id 가 없다 — 클릭이 무반응이 된다` + ).toContain(button.props.form); + } + }); +}); + +describe('스케줄 폼 모달 — 폼 값 바인딩', () => { + function submitBody(): Record<string, string> { + const forms = collectNodes(formModal, (n) => n.name === 'Form' && Array.isArray(n.actions)); + const submit = forms[0].actions.find((a: any) => a.type === 'submit'); + const api = submit.actions.find((a: any) => a.handler === 'apiCall'); + return api.params.body; + } + + it('커스텀 Select 는 value 로 제어된다 (defaultValue 는 무시되어 빈 표시 + 폼 미반영)', () => { + const selects = collectNodes(formModal, (n) => n.type === 'composite' && n.name === 'Select'); + expect(selects.length).toBe(2); + + for (const select of selects) { + expect( + select.props.value, + `Select(${select.props.name}) 는 value 로 제어해야 한다 — 커스텀 Select 는 defaultValue 를 렌더하지 않는다` + ).toBeTruthy(); + expect(select.props.defaultValue, `Select(${select.props.name}) 의 defaultValue 는 죽은 prop 이다`).toBeUndefined(); + + const change = (select.actions ?? []).find((a: any) => a.type === 'change'); + expect(change, `Select(${select.props.name}) 는 change 액션으로 상태에 기록해야 한다`).toBeTruthy(); + } + }); + + it('Toggle 은 change 액션으로 boolean 을 상태에 기록한다 (자동바인딩은 "on" 문자열을 보내 422)', () => { + const toggles = collectNodes(formModal, (n) => n.type === 'composite' && n.name === 'Toggle'); + expect(toggles.length).toBe(3); + + for (const toggle of toggles) { + const change = (toggle.actions ?? []).find((a: any) => a.type === 'change'); + expect(change, `Toggle(${toggle.props.name}) 에 change 액션이 없다`).toBeTruthy(); + const written = JSON.stringify(change.params); + expect( + written, + `Toggle(${toggle.props.name}) 은 $event.target.checked (boolean) 를 기록해야 한다` + ).toContain('$event.target.checked'); + } + }); + + it('submit body 의 Select/Toggle 값은 form.* 이 아니라 상태에서 읽는다', () => { + const body = submitBody(); + + for (const field of ['type', 'frequency', 'without_overlapping', 'run_in_maintenance', 'is_active']) { + expect( + body[field], + `body.${field} 가 form.* 를 읽는다 — 커스텀 Select/Toggle 값은 FormData 에 실리지 않거나 "on" 으로 실린다` + ).not.toBe(`{{form.${field}}}`); + } + }); + + it('submit body 의 expression 은 비-custom 주기에서도 유효한 cron 을 보낸다', () => { + const body = submitBody(); + // custom 이 아니면 expression 입력이 렌더되지 않아 form.expression 이 비어 서버 422 가 된다. + expect(body.expression).not.toBe('{{form.expression}}'); + expect(body.expression).toContain("'custom'"); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-catalog-binding.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-catalog-binding.test.tsx new file mode 100644 index 00000000..cb54999b --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-catalog-binding.test.tsx @@ -0,0 +1,269 @@ +/** + * @file admin-settings-drivers-catalog-binding.test.tsx + * @description 드라이버 셀렉트의 카탈로그 바인딩 + 죽은 저장값 안내 + * + * 검색엔진 셀렉트만 옵션이 레이아웃에 박혀 있어, 플러그인이 검색엔진을 등록해도 + * 화면에서 고를 수 없었다. 그리고 저장값이 카탈로그에 없으면(공급 플러그인 제거) + * 셀렉트는 빈 칸으로만 보여, 운영자가 "설정이 비어 있다" 로 오해하고 실제 저장값이 + * 무엇인지 알 방법이 없었다. + */ + +import React from 'react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +const PARTIAL_DIR = resolve(__dirname, '../../layouts/partials/admin_settings'); + +const driversPartial = JSON.parse(readFileSync(resolve(PARTIAL_DIR, '_tab_drivers.json'), 'utf-8')); +const mailPartial = JSON.parse(readFileSync(resolve(PARTIAL_DIR, '_tab_mail.json'), 'utf-8')); + +// --------------------------------------------------------------------------- +// 테스트용 컴포넌트 +// --------------------------------------------------------------------------- + +const TestDiv: React.FC<any> = ({ className, children }) => <div className={className}>{children}</div>; +const TestSelect: React.FC<any> = ({ name, options }) => ( + <select name={name} data-testid={`select-${name}`}> + {(options ?? []).map((o: any) => ( + <option key={o.value} value={o.value}> + {o.label} + </option> + ))} + </select> +); +const TestSpan: React.FC<any> = ({ children, text, className }) => <span className={className}>{children || text}</span>; +const TestP: React.FC<any> = ({ children, text, className, ...rest }) => ( + <p className={className} data-testid={rest['data-testid']}> + {children || text} + </p> +); +const TestLabel: React.FC<any> = ({ children, text }) => <label>{children || text}</label>; +const TestInput: React.FC<any> = ({ name }) => <input name={name} data-testid={name} />; +const TestButton: React.FC<any> = ({ children, text }) => <button type="button">{children || text}</button>; +const TestIcon: React.FC<any> = ({ name }) => <i data-icon={name} />; +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; + +/** + * 테스트용 컴포넌트 레지스트리를 구성합니다. + * + * @returns 구성된 레지스트리 + */ +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + Input: { component: TestInput, metadata: { name: 'Input', type: 'basic' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + + return registry; +} + +/** + * id 로 노드를 깊이 우선 탐색합니다. + * + * @param node 탐색 시작 노드 + * @param id 찾을 노드 id + * @returns 찾은 노드 또는 null + */ +function findNodeById(node: any, id: string): any { + if (!node || typeof node !== 'object') return null; + if (node.id === id) return node; + for (const child of node.children ?? []) { + const found = findNodeById(child, id); + if (found) return found; + } + return null; +} + +/** + * partial 루트들에서 id 노드를 찾습니다. + * + * @param partial 대상 partial JSON + * @param id 찾을 노드 id + * @returns 찾은 노드 또는 null + */ +function findInPartial(partial: any, id: string): any { + for (const root of partial.components ?? [partial]) { + const found = findNodeById(root, id); + if (found) return found; + } + return null; +} + +/** + * 노드 트리를 순회하며 조건을 만족하는 노드를 모읍니다. + * + * @param node 탐색 시작 노드 + * @param predicate 판정 함수 + * @param acc 누적 배열 + * @returns 조건을 만족하는 노드 목록 + */ +function collect(node: any, predicate: (n: any) => boolean, acc: any[] = []): any[] { + if (!node || typeof node !== 'object') return acc; + if (Array.isArray(node)) { + for (const item of node) collect(item, predicate, acc); + return acc; + } + if (predicate(node)) acc.push(node); + for (const key of Object.keys(node)) collect(node[key], predicate, acc); + return acc; +} + +/** + * 검색엔진 필드 블록을 주어진 상태로 렌더합니다. + * + * @param form 폼 상태 (_local.form) + * @returns 레이아웃 테스트 유틸 + */ +function renderSearchField(form: Record<string, unknown>) { + const field = findInPartial(driversPartial, 'field_search_engine_driver'); + expect(field).not.toBeNull(); + + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_drivers_catalog_binding', + components: [field], + } as any, + { initialState: { _local: { form, errors: {} } } } + ); +} + +const CORE_SEARCH_CATALOG = [{ id: 'mysql-fulltext', label: { ko: 'MySQL 전문검색', en: 'MySQL Full-Text' } }]; + +// @scenario saved_value_state=live +// @effects driver_select_options_built_from_catalog, live_saved_value_shows_no_notice +describe('드라이버 셀렉트 카탈로그 바인딩', () => { + let registry: ComponentRegistry; + + beforeEach(() => { + registry = setupTestRegistry(); + }); + + afterEach(() => { + (registry as any).registry = {}; + }); + + it('검색엔진 옵션을 카탈로그에서 만든다 — 플러그인 등록 드라이버가 화면에 뜬다', async () => { + const testUtils = renderSearchField({ + drivers: { search_engine_driver: 'meilisearch' }, + available_drivers: { + search: [ + ...CORE_SEARCH_CATALOG, + { id: 'meilisearch', label: { ko: 'Meilisearch', en: 'Meilisearch' } }, + ], + }, + }); + await testUtils.render(); + + const select = screen.getByTestId('select-drivers.search_engine_driver'); + const values = Array.from(select.querySelectorAll('option')).map((o) => o.getAttribute('value')); + + expect(values).toContain('mysql-fulltext'); + expect(values).toContain('meilisearch'); + + testUtils.cleanup(); + }); + + it('저장값이 카탈로그에 있으면 안내를 띄우지 않는다', async () => { + const testUtils = renderSearchField({ + drivers: { search_engine_driver: 'mysql-fulltext' }, + available_drivers: { search: CORE_SEARCH_CATALOG }, + }); + await testUtils.render(); + + // 존재를 먼저 확정한 뒤 부재를 단언한다 (부재 단독 단언 금지 규율) + expect(screen.getByTestId('select-drivers.search_engine_driver')).toBeInTheDocument(); + expect(screen.queryByTestId('driver-unavailable-search')).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); + + it('공급 플러그인이 사라진 저장값은 안내와 함께 값 자체를 드러낸다', async () => { + const testUtils = renderSearchField({ + drivers: { search_engine_driver: 'elasticsearch' }, + available_drivers: { search: CORE_SEARCH_CATALOG }, + }); + await testUtils.render(); + + const notice = screen.getByTestId('driver-unavailable-search'); + expect(notice).toBeInTheDocument(); + expect(notice.textContent).toContain('elasticsearch'); + + testUtils.cleanup(); + }); + + it('저장값이 비어 있으면 안내를 띄우지 않는다', async () => { + const present = renderSearchField({ + drivers: { search_engine_driver: 'elasticsearch' }, + available_drivers: { search: CORE_SEARCH_CATALOG }, + }); + await present.render(); + expect(screen.getByTestId('driver-unavailable-search')).toBeInTheDocument(); + present.cleanup(); + + const testUtils = renderSearchField({ + drivers: { search_engine_driver: '' }, + available_drivers: { search: CORE_SEARCH_CATALOG }, + }); + await testUtils.render(); + + expect(screen.queryByTestId('driver-unavailable-search')).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); +}); + +// @scenario saved_value_state=dead +// @effects dead_saved_value_notice_reveals_the_value, every_catalog_bound_select_has_dead_value_notice +describe('카탈로그 바인딩 셀렉트 전수', () => { + const CATALOG_RE = /_local\.form\?\.available_drivers\?\.([a-z_]+)\s*\?\?/; + + it('카탈로그 바인딩 셀렉트마다 죽은 저장값 안내가 붙어 있다', () => { + for (const [name, partial] of [ + ['_tab_drivers.json', driversPartial], + ['_tab_mail.json', mailPartial], + ] as const) { + const selects = collect( + partial, + (n) => n.name === 'Select' && typeof n.props?.options === 'string' && CATALOG_RE.test(n.props.options) + ); + + expect(selects.length, `${name} 에서 카탈로그 바인딩 셀렉트를 찾지 못했습니다.`).toBeGreaterThan(0); + + const notices = collect(partial, (n) => typeof n.id === 'string' && n.id.startsWith('unavailable_driver_notice_')); + + const selectCategories = selects.map((s) => s.props.options.match(CATALOG_RE)![1]).sort(); + const noticeCategories = notices.map((n) => n.id.replace('unavailable_driver_notice_', '')).sort(); + + expect(noticeCategories, `${name} 의 안내 노드가 셀렉트 카테고리와 일치하지 않습니다.`).toEqual( + selectCategories + ); + } + }); + + it('안내 조건식이 저장값과 카탈로그를 함께 본다', () => { + const notices = collect( + driversPartial, + (n) => typeof n.id === 'string' && n.id.startsWith('unavailable_driver_notice_') + ); + + expect(notices.length).toBeGreaterThan(0); + + for (const notice of notices) { + expect(notice.if).toMatch(/available_drivers\?\.[a-z_]+ \?\? \[\]\)\.some\(/); + expect(notice.if).toMatch(/^\{\{!!_local\.form\?\./); + } + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-fields.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-fields.test.tsx new file mode 100644 index 00000000..ee6b61ae --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-fields.test.tsx @@ -0,0 +1,333 @@ +/** + * @file admin-settings-drivers-s3-fields.test.tsx + * @description 드라이버 탭 S3 블록 렌더링 테스트 (공개 #99 / 내부 #563) + * + * 실제 partial(_tab_drivers.json)의 s3_settings 블록을 그대로 렌더하여 검증한다: + * - s3 선택 시 리전이 자유 입력 Input 으로 렌더 (Select 아님 — A4) + * - s3_endpoint Input · s3_use_path_style Toggle 렌더 (A3) + * - 연결 테스트 payload 에 s3_endpoint/s3_use_path_style 포함, s3_url 의도적 제외 (A10) + * - websocket 테스트 payload 에 server 3필드 포함 (A6) + * - 저장 payload 가 활성 탭 폼을 키 열거 없이 통째로 전송 (신규 키 자동 포함) + */ + +import React from 'react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +const partialPath = resolve( + __dirname, + '../../layouts/partials/admin_settings/_tab_drivers.json' +); +const driversPartial = JSON.parse(readFileSync(partialPath, 'utf-8')); + +const settingsLayoutPath = resolve(__dirname, '../../layouts/admin_settings.json'); +const settingsLayout = JSON.parse(readFileSync(settingsLayoutPath, 'utf-8')); + +// --------------------------------------------------------------------------- +// 테스트용 컴포넌트 +// --------------------------------------------------------------------------- + +const TestDiv: React.FC<any> = ({ className, children, 'data-testid': testId }) => ( + <div className={className} data-testid={testId}>{children}</div> +); + +const TestInput: React.FC<any> = ({ className, disabled, type, name, placeholder }) => ( + <input + className={className} + disabled={disabled} + type={type} + name={name} + placeholder={placeholder} + data-testid={name} + /> +); + +const TestSelect: React.FC<any> = ({ name }) => ( + <select name={name} data-testid={`select-${name}`} /> +); + +const TestToggle: React.FC<any> = ({ name, disabled }) => ( + <input type="checkbox" role="switch" name={name} disabled={disabled} data-testid={`toggle-${name}`} /> +); + +const TestButton: React.FC<any> = ({ className, disabled, type, children, text }) => ( + <button className={className} disabled={disabled} type={type} data-testid="test-button"> + {children || text} + </button> +); + +const TestSpan: React.FC<any> = ({ className, children, text }) => ( + <span className={className}>{children || text}</span> +); + +const TestP: React.FC<any> = ({ className, children, text }) => ( + <p className={className}>{children || text}</p> +); + +const TestLabel: React.FC<any> = ({ className, children, text }) => ( + <label className={className}>{children || text}</label> +); + +const TestIcon: React.FC<any> = ({ name, className }) => ( + <i className={className} data-icon={name} /> +); + +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; + +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Input: { component: TestInput, metadata: { name: 'Input', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Toggle: { component: TestToggle, metadata: { name: 'Toggle', type: 'composite' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + + return registry; +} + +// --------------------------------------------------------------------------- +// partial JSON 탐색 헬퍼 +// --------------------------------------------------------------------------- + +/** id 로 노드를 깊이 우선 탐색합니다. */ +function findNodeById(node: any, id: string): any { + if (!node || typeof node !== 'object') return null; + if (node.id === id) return node; + for (const child of node.children ?? []) { + const found = findNodeById(child, id); + if (found) return found; + } + return null; +} + +/** 루트 배열에서 id 노드를 찾습니다. */ +function findInPartial(id: string): any { + for (const root of driversPartial.components ?? [driversPartial]) { + const found = findNodeById(root, id); + if (found) return found; + } + return null; +} + +/** 노드 서브트리에서 조건에 맞는 모든 노드를 수집합니다. */ +function collectNodes(node: any, predicate: (n: any) => boolean, acc: any[] = []): any[] { + if (!node || typeof node !== 'object') return acc; + if (predicate(node)) acc.push(node); + for (const child of node.children ?? []) collectNodes(child, predicate, acc); + return acc; +} + +/** 레이아웃 JSON 어디에 있든(children/actions/modals 등) apiCall 액션을 전부 수집합니다. */ +function collectApiCallsDeep(node: any, acc: any[] = []): any[] { + if (!node || typeof node !== 'object') return acc; + if (node.handler === 'apiCall') acc.push(node); + for (const value of Object.values(node)) { + if (Array.isArray(value)) { + value.forEach((item) => collectApiCallsDeep(item, acc)); + } else if (value && typeof value === 'object') { + collectApiCallsDeep(value, acc); + } + } + return acc; +} + +/** 노드 서브트리에서 apiCall 액션들을 수집합니다 (actions 배열 내 중첩 sequence 포함). */ +function collectApiCalls(node: any, acc: any[] = []): any[] { + if (!node || typeof node !== 'object') return acc; + if (Array.isArray(node.actions)) { + const walk = (action: any) => { + if (!action || typeof action !== 'object') return; + if (action.handler === 'apiCall') acc.push(action); + for (const nested of action.actions ?? []) walk(nested); + }; + node.actions.forEach(walk); + } + for (const child of node.children ?? []) collectApiCalls(child, acc); + return acc; +} + +// --------------------------------------------------------------------------- +// 렌더링 테스트 +// --------------------------------------------------------------------------- + +describe('드라이버 탭 S3 블록 (실 partial 렌더)', () => { + let registry: ComponentRegistry; + + beforeEach(() => { + registry = setupTestRegistry(); + }); + + afterEach(() => { + (registry as any).registry = {}; + }); + + function renderS3Block() { + const s3Settings = findInPartial('s3_settings'); + expect(s3Settings).not.toBeNull(); + + const layout = { + version: '1.0.0', + layout_name: 'test_drivers_s3', + components: [s3Settings], + }; + + return createLayoutTest(layout as any, { + initialState: { + _local: { + form: { + drivers: { + storage_driver: 's3', + s3_bucket: '', + s3_region: '', + s3_access_key: '', + s3_secret_key: '', + s3_endpoint: '', + s3_use_path_style: false, + s3_url: '', + }, + }, + errors: {}, + }, + }, + }); + } + + // @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + // @effects invalid_region_rejected + it('s3 선택 시 리전이 자유 입력 Input 으로 렌더된다 (Select 아님)', async () => { + const testUtils = renderS3Block(); + await testUtils.render(); + + // 존재 확정 후 부재 단언 (부재 단언 단독 금지 규율) + expect(screen.getByTestId('drivers.s3_region')).toBeInTheDocument(); + expect(screen.getByTestId('drivers.s3_region').tagName).toBe('INPUT'); + expect(screen.queryByTestId('select-drivers.s3_region')).toBeNull(); + + testUtils.cleanup(); + }); + + it('s3_endpoint Input 과 s3_use_path_style Toggle 이 렌더된다', async () => { + const testUtils = renderS3Block(); + await testUtils.render(); + + expect(screen.getByTestId('drivers.s3_endpoint')).toBeInTheDocument(); + expect(screen.getByTestId('toggle-drivers.s3_use_path_style')).toBeInTheDocument(); + + testUtils.cleanup(); + }); + + it('기존 s3 필드(bucket/access/secret/url)가 유지된다 (회귀 가드)', async () => { + const testUtils = renderS3Block(); + await testUtils.render(); + + for (const field of ['s3_bucket', 's3_access_key', 's3_secret_key', 's3_url']) { + expect(screen.getByTestId(`drivers.${field}`)).toBeInTheDocument(); + } + + testUtils.cleanup(); + }); +}); + +// --------------------------------------------------------------------------- +// 구조(payload 계약) 테스트 — 실제 partial JSON 을 직접 검증 +// --------------------------------------------------------------------------- + +describe('드라이버 탭 연결 테스트 payload 계약', () => { + // @scenario region_shape=aws_region,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default + // @effects endpoint_injected + it('s3 테스트 payload 에 s3_endpoint/s3_use_path_style 이 포함되고 s3_url 은 제외된다', () => { + const s3Settings = findInPartial('s3_settings'); + const apiCalls = collectApiCalls(s3Settings); + const testCall = apiCalls.find( + (a) => a.target === '/api/admin/settings/test-driver' + ); + + expect(testCall).toBeDefined(); + + const body = testCall.params.body; + expect(Object.keys(body)).toEqual( + expect.arrayContaining([ + 'storage_driver', + 's3_bucket', + 's3_region', + 's3_access_key', + 's3_secret_key', + 's3_endpoint', + 's3_use_path_style', + ]) + ); + // s3_url 은 공개 URL base — 연결 테스트와 무관 (의도적 제외, A10) + expect(body).not.toHaveProperty('s3_url'); + }); + + it('websocket 테스트 payload 에 server 3필드가 포함된다 (A6)', () => { + const roots = driversPartial.components ?? [driversPartial]; + const allCalls: any[] = []; + for (const root of roots) collectApiCalls(root, allCalls); + + const websocketCall = allCalls.find( + (a) => + a.target === '/api/admin/settings/test-driver' && + a.params?.body?.websocket_enabled !== undefined + ); + + expect(websocketCall).toBeDefined(); + expect(Object.keys(websocketCall.params.body)).toEqual( + expect.arrayContaining([ + 'websocket_host', + 'websocket_port', + 'websocket_scheme', + 'websocket_server_host', + 'websocket_server_port', + 'websocket_server_scheme', + ]) + ); + }); + + it('리전 Select 하드코딩 옵션(regions.*)이 partial 에서 제거되었다', () => { + const raw = JSON.stringify(driversPartial); + // 존재 확정 — 대체된 자유 입력 힌트 키가 같은 s3 블록에 실려 있다 + // (partial 경로/구조가 바뀌어 빈 대상을 검사하면 부재 단언만으로는 공회전 통과) + expect(raw).toContain('admin.settings.drivers.storage.s3_region_desc'); + expect(raw).not.toContain('storage.regions.'); + }); + + it('onError fallback 메시지가 다국어 키를 사용한다 (a9)', () => { + const raw = JSON.stringify(driversPartial); + // 존재 확정 — 영문 하드코딩을 대체한 다국어 키가 실제로 쓰이고 있다 + expect(raw).toContain('$t:admin.settings.drivers.test_failed'); + expect(raw).not.toContain('Connection test failed'); + }); + + // @scenario region_shape=aws_region,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default + // @effects local_roundtrip_preserves_new_keys + it('저장 payload 는 활성 탭 폼을 키 열거 없이 통째로 전송한다 (신규 s3 키 자동 포함)', () => { + const saveCalls = collectApiCallsDeep(settingsLayout).filter( + (a) => a.target === '/api/admin/settings' && a.params?.method === 'POST' + ); + + // 존재 확정 후 계약 단언 + expect(saveCalls.length).toBeGreaterThan(0); + + const saveBody = saveCalls.map((a) => a.params?.body).find( + (body) => typeof body === 'string' && body.includes('_tab') + ); + expect(saveBody).toBeDefined(); + + // 탭 폼 객체를 통째로 전달 — s3_endpoint/s3_use_path_style 등 신규 키가 + // 별도 열거 없이 자동 포함된다 (열거 방식이면 키 추가 시 조용히 누락) + expect(saveBody).toContain('[tab]: form[tab] || {}'); + expect(saveBody).not.toContain('s3_'); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-visibility.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-visibility.test.tsx new file mode 100644 index 00000000..42c0c18c --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-visibility.test.tsx @@ -0,0 +1,250 @@ +/** + * @file admin-settings-drivers-s3-visibility.test.tsx + * @description S3 접속 정보 블록 노출 조건 테스트 (공개 #99 × #100 교차) + * + * 파일 스토리지는 로컬로 두고 공개 자산만 S3/CDN 으로 보내는 구성(이슈 #100 이 + * 제안한 "공개 자산 전용 디스크 분리")에서도 버킷·키·엔드포인트·공개 URL 을 + * 입력할 수 있어야 한다. 종전에는 s3 블록이 `storage_driver === 's3'` 일 때만 + * 렌더되어, 공개 자산 카드가 S3 를 제공하는데 그 접속 정보를 넣을 화면이 + * 없었다(도움말은 보이지 않는 필드를 가리켰다). + */ + +import React from 'react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import { createLayoutTest, screen } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +const driversPartial = JSON.parse( + readFileSync(resolve(__dirname, '../../layouts/partials/admin_settings/_tab_drivers.json'), 'utf-8') +); + +const settingsLayout = JSON.parse( + readFileSync(resolve(__dirname, '../../layouts/admin_settings.json'), 'utf-8') +); + +// --------------------------------------------------------------------------- +// 테스트용 컴포넌트 +// --------------------------------------------------------------------------- + +const TestDiv: React.FC<any> = ({ className, children }) => <div className={className}>{children}</div>; +const TestInput: React.FC<any> = ({ name, type }) => <input name={name} type={type} data-testid={name} />; +const TestSelect: React.FC<any> = ({ name }) => <select name={name} data-testid={`select-${name}`} />; +const TestToggle: React.FC<any> = ({ name }) => ( + <input type="checkbox" role="switch" name={name} data-testid={`toggle-${name}`} /> +); +const TestButton: React.FC<any> = ({ children, text }) => <button type="button">{children || text}</button>; +const TestSpan: React.FC<any> = ({ children, text }) => <span>{children || text}</span>; +const TestP: React.FC<any> = ({ children, text }) => <p>{children || text}</p>; +const TestLabel: React.FC<any> = ({ children, text }) => <label>{children || text}</label>; +const TestIcon: React.FC<any> = ({ name }) => <i data-icon={name} />; +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; + +/** + * 테스트용 컴포넌트 레지스트리를 구성합니다. + * + * @returns 구성된 레지스트리 + */ +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Input: { component: TestInput, metadata: { name: 'Input', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Toggle: { component: TestToggle, metadata: { name: 'Toggle', type: 'composite' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + + return registry; +} + +/** + * id 로 노드를 깊이 우선 탐색합니다. + * + * @param node 탐색 시작 노드 + * @param id 찾을 노드 id + * @returns 찾은 노드 또는 null + */ +function findNodeById(node: any, id: string): any { + if (!node || typeof node !== 'object') return null; + if (node.id === id) return node; + for (const child of node.children ?? []) { + const found = findNodeById(child, id); + if (found) return found; + } + return null; +} + +/** + * partial 루트들에서 id 노드를 찾습니다. + * + * @param id 찾을 노드 id + * @returns 찾은 노드 또는 null + */ +function findInPartial(id: string): any { + for (const root of driversPartial.components ?? [driversPartial]) { + const found = findNodeById(root, id); + if (found) return found; + } + return null; +} + +/** + * 주어진 드라이버 폼 상태로 s3 블록을 렌더합니다. + * + * @param drivers 폼의 drivers 하위 상태 + * @returns 레이아웃 테스트 유틸 + */ +function renderS3Block(drivers: Record<string, unknown>) { + const s3Settings = findInPartial('s3_settings'); + expect(s3Settings).not.toBeNull(); + + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_drivers_s3_visibility', + components: [s3Settings], + } as any, + { + initialState: { + _local: { + form: { drivers }, + errors: {}, + }, + }, + } + ); +} + +const BASE_DRIVERS = { + s3_bucket: '', + s3_region: '', + s3_access_key: '', + s3_secret_key: '', + s3_endpoint: '', + s3_use_path_style: false, + s3_url: '', +}; + +// @scenario consumer=product, disk_setting=s3_without_url, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row +// @effects s3_credentials_visible_when_public_asset_disk_is_s3 +describe('S3 접속 정보 블록 노출 조건', () => { + let registry: ComponentRegistry; + + beforeEach(() => { + registry = setupTestRegistry(); + }); + + afterEach(() => { + (registry as any).registry = {}; + }); + + it('파일 스토리지가 s3 면 종전대로 렌더된다', async () => { + const testUtils = renderS3Block({ + ...BASE_DRIVERS, + storage_driver: 's3', + public_asset_disk: 'none', + }); + await testUtils.render(); + + expect(screen.getByTestId('drivers.s3_bucket')).toBeInTheDocument(); + expect(screen.getByTestId('drivers.s3_url')).toBeInTheDocument(); + + testUtils.cleanup(); + }); + + it('파일 스토리지가 로컬이어도 공개 자산 디스크가 s3 면 접속 필드가 렌더된다', async () => { + const testUtils = renderS3Block({ + ...BASE_DRIVERS, + storage_driver: 'local', + public_asset_disk: 's3', + }); + await testUtils.render(); + + for (const field of ['s3_bucket', 's3_region', 's3_access_key', 's3_secret_key', 's3_endpoint', 's3_url']) { + expect(screen.getByTestId(`drivers.${field}`)).toBeInTheDocument(); + } + expect(screen.getByTestId('toggle-drivers.s3_use_path_style')).toBeInTheDocument(); + + testUtils.cleanup(); + }); + + it('두 축 모두 s3 가 아니면 접속 필드가 렌더되지 않는다', async () => { + const present = renderS3Block({ + ...BASE_DRIVERS, + storage_driver: 's3', + public_asset_disk: 'none', + }); + await present.render(); + // 존재를 먼저 확정한 뒤에 부재를 단언한다 (부재 단독 단언 금지 규율) + expect(screen.getByTestId('drivers.s3_bucket')).toBeInTheDocument(); + present.cleanup(); + + const absent = renderS3Block({ + ...BASE_DRIVERS, + storage_driver: 'local', + public_asset_disk: 'public', + }); + await absent.render(); + expect(screen.queryByTestId('drivers.s3_bucket')).toBeNull(); + absent.cleanup(); + }); +}); + +describe('저장 게이트의 S3 연결 테스트 요구 조건', () => { + /** + * 저장 버튼의 disabled 표현식을 찾습니다. + * + * @param node 탐색 시작 노드 + * @returns disabled 표현식 문자열 또는 null + */ + function findSaveDisabledExpression(node: any): string | null { + if (!node || typeof node !== 'object') return null; + const disabled = node.props?.disabled; + if (typeof disabled === 'string' && disabled.includes('driverTestResults')) { + return disabled; + } + for (const value of Object.values(node)) { + if (Array.isArray(value)) { + for (const item of value) { + const found = findSaveDisabledExpression(item); + if (found) return found; + } + } else if (value && typeof value === 'object') { + const found = findSaveDisabledExpression(value); + if (found) return found; + } + } + return null; + } + + it('공개 자산 디스크가 s3 인 경우도 연결 테스트 성공을 요구한다', () => { + const expression = findSaveDisabledExpression(settingsLayout); + + expect(expression).not.toBeNull(); + expect(expression).toContain( + "(_local.form?.drivers?.storage_driver === 's3' || _local.form?.drivers?.public_asset_disk === 's3')" + ); + }); + + it('재검증 트리거는 S3 접속 설정 변경으로 한정된다 (공개 자산 디스크 전환 자체는 트리거 아님)', () => { + const expression = findSaveDisabledExpression(settingsLayout); + + // 파일 스토리지가 s3 인 상태에서 공개 자산 디스크를 public 등으로 바꾸는 것은 + // S3 접속 설정과 무관하므로 연결 테스트를 다시 요구해서는 안 된다. + expect(expression).not.toContain( + "(_local.form?.drivers?.public_asset_disk || '') !== (_local.originalDrivers?.public_asset_disk || '')" + ); + // 접속 설정 변경은 종전대로 트리거로 남아 있어야 한다 (대조군) + expect(expression).toContain( + "(_local.form?.drivers?.s3_bucket || '') !== (_local.originalDrivers?.s3_bucket || '')" + ); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove-expression.test.ts b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove-expression.test.ts new file mode 100644 index 00000000..07a22b2e --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove-expression.test.ts @@ -0,0 +1,124 @@ +/** + * @file admin-settings-logo-remove-expression.test.ts + * @description 사이트 로고 제거 시 form 에서 실제로 빠지는지 — 레이아웃 표현식 자체를 평가한다. + * + * 배경(브라우저 실측): FileUploader 는 `onRemove` 에 **hash 우선**(`item.hash || item.id`)으로 + * 식별자를 넘긴다. 레이아웃이 `item.id !== $args[0]` 로만 걸러내면 저장된 첨부(항상 hash 보유)는 + * 어떤 항목도 제거되지 않아, 첨부는 서버에서 즉시 삭제됐는데 폼에는 그대로 남는다. 그 상태로 + * 저장하면 이미 없는 id 를 제출해 422 가 되고, 설정에는 삭제된 첨부 id 가 남는다. + * + * 기존 테스트는 레이아웃 액션을 파일 안에 복사해 두고 기대 결과를 손으로 넣어 트리거하므로 + * 표현식 자체는 한 번도 평가되지 않는다. 그래서 이 테스트는 **실제 레이아웃 파일**에서 식을 + * 꺼내 평가한다. + * + * @scenario protected=current_site_logo, age=past_retention + * + * @effects site_logo_replacement_drops_previous, site_logo_stale_submitted_id_filtered + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +/** + * 템플릿 루트(template.json 기준)를 위로 훑어 찾는다. + * + * @returns 템플릿 루트 절대경로 + */ +function templateRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'template.json'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('template.json 을 가진 템플릿 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + for (const child of node.children ?? []) { + const found = findNode(child, predicate); + if (found) return found; + } + + return null; +} + +const generalTab = JSON.parse( + fs.readFileSync( + path.join(templateRoot(), 'layouts/partials/admin_settings/_tab_general.json'), + 'utf-8', + ), +); + +const uploader = findNode(generalTab, (n: any) => n.id === 'site_logo_uploader'); + +const removeExpression: string = uploader.actions + .filter((a: any) => a.event === 'onRemove') + .map((a: any) => a.params?.['form.general.site_logo']) + .find((v: unknown) => typeof v === 'string'); + +/** + * `{{...}}` 바인딩 식을 주어진 컨텍스트로 평가한다. + * + * @param expression 레이아웃 바인딩 식 + * @param context 평가 컨텍스트 (_local, $args) + * @returns 평가 결과 + */ +function evaluateBinding(expression: string, context: { _local: any; $args: unknown[] }): unknown { + const body = expression.trim().replace(/^\{\{/, '').replace(/\}\}$/, ''); + + return new Function('_local', '$args', `return (${body});`)(context._local, context.$args); +} + +const SAVED_LOGO = { id: 101, hash: 'abc123', original_filename: 'logo1.png' }; +const OTHER_LOGO = { id: 202, hash: 'def456', original_filename: 'logo2.png' }; + +describe('사이트 로고 제거 — 레이아웃 표현식 평가', () => { + it('onRemove 식이 레이아웃에 선언되어 있다', () => { + expect(typeof removeExpression).toBe('string'); + }); + + it('hash 로 통지된 항목이 form 에서 제거된다', () => { + const result = evaluateBinding(removeExpression, { + _local: { form: { general: { site_logo: [SAVED_LOGO, OTHER_LOGO] } } }, + $args: [SAVED_LOGO.hash], + }) as any[]; + + expect(result.map((item) => item.id)).toEqual([OTHER_LOGO.id]); + }); + + it('hash 가 없는 항목은 id 로도 제거된다', () => { + const idOnly = { id: 303, original_filename: 'no-hash.png' }; + + const result = evaluateBinding(removeExpression, { + _local: { form: { general: { site_logo: [idOnly, OTHER_LOGO] } } }, + $args: [idOnly.id], + }) as any[]; + + expect(result.map((item) => item.id)).toEqual([OTHER_LOGO.id]); + }); + + it('대상이 아닌 식별자면 아무것도 제거하지 않는다', () => { + const result = evaluateBinding(removeExpression, { + _local: { form: { general: { site_logo: [SAVED_LOGO, OTHER_LOGO] } } }, + $args: ['nonexistent-hash'], + }) as any[]; + + expect(result).toHaveLength(2); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove.test.tsx index 1eed92c8..a36ab562 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove.test.tsx @@ -88,7 +88,9 @@ const settingsLogoLayout = { params: { target: 'local', hasChanges: true, - 'form.general.site_logo': "{{(_local.form?.general?.site_logo ?? []).filter(item => item.id !== $args[0])}}", + // 프로덕션 레이아웃과 동일 — FileUploader 는 hash 를 우선 통지한다. + // 식 자체의 동작은 admin-settings-logo-remove-expression.test.ts 가 평가해 고정한다. + 'form.general.site_logo': "{{(_local.form?.general?.site_logo ?? []).filter(item => (item.hash || item.id) !== $args[0])}}", }, }, ], diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-orphan-cleanup.test.ts b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-orphan-cleanup.test.ts new file mode 100644 index 00000000..b173f999 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-orphan-cleanup.test.ts @@ -0,0 +1,149 @@ +/** + * 환경설정 업로드 탭 — 고아 첨부 정리 필드 계약 테스트 (공개 #115) + * + * @description + * 고아 첨부 정리는 사용자 파일을 파기하므로 기본 꺼짐이다. 화면 바인딩 키가 코어 + * 기본값·서버 검증과 일치하는지 고정한다. 한 곳만 어긋나면 저장은 성공하는데 값이 + * 조용히 버려지고, 운영자는 켰다고 믿는 상태로 남는다. + * + * @effects core_settings_orphan_toggle_default_off, core_settings_orphan_keys_declared + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +import uploadTab from '../../layouts/partials/admin_settings/_tab_upload.json'; + +/** + * 저장소 루트(composer.json + artisan 기준)를 위로 훑어 찾는다. + * + * @returns 저장소 루트 절대경로 + */ +function repoRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'artisan'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('artisan 을 가진 저장소 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + for (const child of node.children ?? []) { + const found = findNode(child, predicate); + if (found) return found; + } + + return null; +} + +/** + * 템플릿 매니페스트가 선언한 컴포넌트 종류를 읽는다. + * + * 레이아웃 노드의 `type` 은 렌더러가 DOM 안전 필터링을 고르는 기준이라 + * 매니페스트 선언과 어긋나면 `editorAttrs` 가 제거된다(DynamicRenderer). + * + * @param name 컴포넌트 이름 + * @returns 매니페스트가 선언한 type + */ +function manifestType(name: string): string { + const manifest = JSON.parse( + fs.readFileSync( + path.join(repoRoot(), 'templates/_bundled/sirsoft-admin_basic/components.json'), + 'utf-8', + ), + ); + + for (const [kind, entries] of Object.entries<any>(manifest.components ?? {})) { + if ((entries as any[]).some((entry) => entry.name === name)) { + return kind; + } + } + + throw new Error(`매니페스트에 ${name} 선언이 없습니다.`); +} + +const card = findNode(uploadTab, (n) => n.id === 'card_orphan_cleanup'); + +describe('고아 첨부 정리 설정 — 계약', () => { + it('업로드 탭에 고아 첨부 정리 카드가 있다', () => { + expect(card).not.toBeNull(); + }); + + it('토글과 보존기간이 upload 카테고리 키를 바인딩한다', () => { + const toggle = findNode(card, (n) => n.name === 'Toggle'); + const input = findNode(card, (n) => n.name === 'Input'); + + expect(toggle.props.name).toBe('upload.orphan_cleanup_enabled'); + expect(input.props.name).toBe('upload.orphan_retention_days'); + expect(input.props.min).toContain('upload_orphan_retention_days_min'); + expect(input.props.max).toContain('upload_orphan_retention_days_max'); + }); + + it('토글 노드의 type 이 매니페스트 선언과 같다', () => { + const toggle = findNode(card, (n) => n.name === 'Toggle'); + + // 어긋나면 렌더러가 editorAttrs 를 떼어내 이 노드만 레이아웃 편집기에서 표식을 잃는다. + expect(toggle.type).toBe(manifestType('Toggle')); + }); + + it('읽기 전용 권한에서는 두 입력이 비활성화된다', () => { + const toggle = findNode(card, (n) => n.name === 'Toggle'); + const input = findNode(card, (n) => n.name === 'Input'); + + expect(toggle.props.disabled).toBe('{{_computed.isReadOnly}}'); + expect(input.props.disabled).toBe('{{_computed.isReadOnly}}'); + }); + + it('기본값이 꺼짐이고 보존기간 기본이 30일이다 (코어 defaults.json)', () => { + const defaults = JSON.parse( + fs.readFileSync(path.join(repoRoot(), 'config/settings/defaults.json'), 'utf-8'), + ); + + expect(defaults.defaults.upload.orphan_cleanup_enabled).toBe(false); + expect(defaults.defaults.upload.orphan_retention_days).toBe(30); + expect(defaults.frontend_schema.upload.fields.orphan_cleanup_enabled.expose).toBe(false); + }); + + it('서버 검증이 같은 키를 받고 화면과 같은 한계값 출처를 쓴다', () => { + const request = fs.readFileSync( + path.join(repoRoot(), 'app/Http/Requests/Settings/SaveSettingsRequest.php'), + 'utf-8', + ); + const coreConfig = fs.readFileSync(path.join(repoRoot(), 'config/core.php'), 'utf-8'); + + expect(request).toContain("'upload.orphan_cleanup_enabled' => ['nullable', 'boolean']"); + // 화면 입력과 저장 규칙이 같은 config 키를 읽어야 "화면이 허용한 값인데 422" 가 생기지 않는다. + expect(request).toContain('core.settings_limits.upload_orphan_retention_days_min'); + expect(request).toContain('core.settings_limits.upload_orphan_retention_days_max'); + expect(coreConfig).toContain("'upload_orphan_retention_days_min' => 1"); + expect(coreConfig).toContain("'upload_orphan_retention_days_max' => 3650"); + }); + + it('스케줄이 등록되고 커맨드가 같은 설정을 false 폴백으로 재확인한다', () => { + const console = fs.readFileSync(path.join(repoRoot(), 'routes/console.php'), 'utf-8'); + const command = fs.readFileSync( + path.join(repoRoot(), 'app/Console/Commands/PruneOrphanAttachmentsCommand.php'), + 'utf-8', + ); + + expect(console).toContain("attachments:prune-orphans --scheduled"); + expect(command).toContain("g7_core_settings('upload.orphan_cleanup_enabled', false)"); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-public-asset-storage.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-public-asset-storage.test.tsx new file mode 100644 index 00000000..a52b5948 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-public-asset-storage.test.tsx @@ -0,0 +1,270 @@ +/** + * 드라이버 탭 — 공개 자산 스토리지 카드 테스트 (공개#100) + * + * @description + * 신규 "공개 자산 스토리지" 카드는 storage 카드(#99 작업 구역)의 **형제**로 존재해야 + * 하며, Select 는 저장 키(drivers.public_asset_disk)와 서버 카탈로그 + * (available_drivers.public_asset)를 정확히 바인딩해야 한다. 화면 옵션이 카탈로그를 + * 벗어나면 저장 단계(레지스트리 조회 closure rule)에서 422 가 된다. + * + * 계약 단언(JSON 구조 + 저장 규칙 대조)에 더해, 실제 카드 서브트리를 + * createLayoutTest 로 렌더링해 옵션 계산식($localized 매핑)과 s3 도움말 조건부 + * 렌더링까지 검증한다. + * + * @effects settings_catalog_includes_plugin_registered_disks + */ + +import React from 'react'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; + +import tabDrivers from '../../layouts/partials/admin_settings/_tab_drivers.json'; + +// ─── 테스트용 컴포넌트 ─── + +const TestDiv: React.FC<{ className?: string; children?: React.ReactNode; 'data-testid'?: string }> = + ({ className, children, 'data-testid': testId }) => ( + <div className={className} data-testid={testId}>{children}</div> + ); + +const TestH3: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <h3 className={className}>{children || text}</h3>; + +const TestLabel: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <label className={className}>{children || text}</label>; + +const TestSpan: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = + ({ className, children, text }) => <span className={className}>{children || text}</span>; + +const TestSelect: React.FC<{ + name?: string; value?: string; className?: string; options?: any[]; disabled?: boolean; + error?: string; onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void; +}> = ({ name, value, className, options, disabled, onChange }) => ( + <select data-name={name} value={value} className={className} disabled={disabled} onChange={onChange}> + {options?.map((o) => <option key={String(o.value)} value={o.value}>{o.label}</option>)} + </select> +); + +// 렌더러가 컴포넌트 목록을 Fragment 로 감싸므로 반드시 등록해야 한다. +// 미등록 시 트리 전체가 렌더되지 않아 "없어야 한다" 계열 단언이 거짓 통과한다. +const TestFragment: React.FC<{ children?: React.ReactNode }> = ({ children }) => <>{children}</>; + +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + H3: { component: TestH3, metadata: { name: 'H3', type: 'basic' } }, + Label: { component: TestLabel, metadata: { name: 'Label', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + Select: { component: TestSelect, metadata: { name: 'Select', type: 'composite' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + return registry; +} + +// ─── 다국어 (드라이버 탭 공개 자산 카드가 쓰는 키) ─── + +const translations = { + admin: { + settings: { + drivers: { + public_asset: { + title: '공개 자산 스토리지', + desc: '완전 공개 자산의 직접 URL 서빙 디스크를 설정합니다.', + disk: '공개 자산 디스크', + s3_help: '스토리지 카드의 S3 URL 설정이 필요합니다.', + }, + }, + }, + }, +}; + +/** + * 저장소 루트를 위로 훑어 찾는다. + * + * @returns 저장소 루트 절대경로 + */ +function repositoryRoot(): string { + let current = path.dirname(fileURLToPath(import.meta.url)); + + for (let depth = 0; depth < 10; depth++) { + if (fs.existsSync(path.join(current, 'config/core.php'))) { + return current; + } + current = path.dirname(current); + } + + throw new Error('저장소 루트를 찾지 못했습니다.'); +} + +/** + * 레이아웃 트리에서 조건을 만족하는 첫 노드를 찾는다. + * + * @param node 탐색 시작 노드 + * @param predicate 노드 판정 함수 + * @returns 찾은 노드 또는 null + */ +function findNode(node: any, predicate: (n: any) => boolean): any { + if (!node || typeof node !== 'object') return null; + if (predicate(node)) return node; + + for (const child of node.children ?? []) { + const found = findNode(child, predicate); + if (found) return found; + } + + return null; +} + +// 실제 카드 서브트리를 그대로 렌더 대상으로 쓴다 — 사본 표현식이 아닌 배포 JSON 자체가 SSoT. +const publicAssetCard = (tabDrivers as any).children.find( + (c: any) => c.id === 'card_public_asset_settings', +); + +const CATALOG = [ + { id: 'none', label: { ko: '사용 안 함 (스트리밍)', en: 'None (streaming)' } }, + { id: 'public', label: { ko: '로컬 공개 디스크', en: 'Local public disk' } }, + { id: 'fake_cdn', label: { ko: '가짜 CDN', en: 'Fake CDN' } }, +]; + +/** + * 카드 렌더링용 테스트 유틸을 생성한다. + * + * @param selectedDisk 현재 저장된 공개 자산 디스크 값 + * @returns createLayoutTest 유틸 + */ +function makeUtils(selectedDisk: string) { + return createLayoutTest( + { + version: '1.0.0', + layout_name: 'test_public_asset_storage_card', + components: [publicAssetCard], + } as any, + { + auth: { + isAuthenticated: true, + user: { id: 1, name: 'Admin', role: 'super_admin' }, + authType: 'admin', + }, + translations, + locale: 'ko', + initialState: { + _local: { + form: { + available_drivers: { public_asset: CATALOG }, + drivers: { public_asset_disk: selectedDisk }, + }, + errors: null, + }, + }, + }, + ); +} + +describe('드라이버 탭 공개 자산 스토리지 카드 — 계약', () => { + it('storage 카드의 형제로 카드가 존재한다 (#99 s3 블록 비접촉)', () => { + const ids = (tabDrivers as any).children.map((c: any) => c.id); + + expect(ids).toContain('card_storage_settings'); + expect(ids).toContain('card_public_asset_settings'); + // storage 카드 내부가 아닌 최상위 형제 배열에 위치 + expect(ids.indexOf('card_public_asset_settings')).toBeGreaterThan( + ids.indexOf('card_storage_settings'), + ); + }); + + it('Select 가 저장 키와 카탈로그를 정확히 바인딩한다', () => { + const select = findNode(publicAssetCard, (n) => n.name === 'Select'); + + expect(select).not.toBeNull(); + expect(select.props.name).toBe('drivers.public_asset_disk'); + expect(select.props.options).toContain('available_drivers?.public_asset'); + expect(select.props.error).toContain("['drivers.public_asset_disk']"); + }); + + it('s3 선택 시에만 도움말이 노출된다', () => { + const help = findNode(publicAssetCard, (n) => n.id === 'public_asset_s3_help'); + + expect(help).not.toBeNull(); + expect(help.if).toContain("public_asset_disk === 's3'"); + }); + + it('저장 규칙(SaveSettingsRequest)이 같은 키를 검증한다', () => { + const requestSource = fs.readFileSync( + path.join(repositoryRoot(), 'app/Http/Requests/Settings/SaveSettingsRequest.php'), + 'utf-8', + ); + + expect(requestSource).toContain("'drivers.public_asset_disk'"); + expect(requestSource).toContain("isDriverAvailable('public_asset'"); + }); +}); + +describe('드라이버 탭 공개 자산 스토리지 카드 — 렌더링', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest> | null = null; + + beforeEach(() => { + registry = setupTestRegistry(); + }); + + afterEach(() => { + testUtils?.cleanup(); + testUtils = null; + (registry as any).registry = {}; + }); + + it('카드 제목/라벨이 다국어 해석되어 렌더된다', async () => { + testUtils = makeUtils('public'); + const { container } = await testUtils.render(); + + expect(container.querySelector('h3.card-title')?.textContent ?? '').toContain( + '공개 자산 스토리지', + ); + expect(container.querySelector('label.form-label')?.textContent ?? '').toContain( + '공개 자산 디스크', + ); + expect(() => testUtils!.assertNoValidationErrors()).not.toThrow(); + }); + + it('Select 옵션이 서버 카탈로그에서 $localized 매핑으로 계산된다', async () => { + testUtils = makeUtils('public'); + const { container } = await testUtils.render(); + + const select = container.querySelector('select[data-name="drivers.public_asset_disk"]'); + expect(select).not.toBeNull(); + + const options = Array.from(select!.querySelectorAll('option')); + expect(options.map((o) => o.getAttribute('value'))).toEqual([ + 'none', + 'public', + 'fake_cdn', + ]); + // $localized(d.label) — 활성 로케일(ko) 라벨로 해석되어야 한다 + expect(options.map((o) => o.textContent)).toEqual([ + '사용 안 함 (스트리밍)', + '로컬 공개 디스크', + '가짜 CDN', + ]); + }); + + it('s3 미선택 시 도움말이 렌더되지 않는다', async () => { + testUtils = makeUtils('public'); + const { container } = await testUtils.render(); + + // 긍정 앵커 — 카드가 실제로 렌더됐음을 먼저 확인해야 아래 부정 단언이 의미를 갖는다 + expect(container.querySelector('select[data-name="drivers.public_asset_disk"]')).not.toBeNull(); + expect(container.textContent ?? '').not.toContain('S3 URL 설정'); + }); + + it('s3 선택 시 도움말이 렌더된다', async () => { + testUtils = makeUtils('s3'); + const { container } = await testUtils.render(); + + expect(container.textContent ?? '').toContain('스토리지 카드의 S3 URL 설정이 필요합니다.'); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-security-password-policy.test.ts b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-security-password-policy.test.ts index f3bb1b2f..e81cdba8 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-security-password-policy.test.ts +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-security-password-policy.test.ts @@ -110,6 +110,10 @@ function parseBinding(expression: string): { limitKey: string; fallback: number return { limitKey: matched![1], fallback: Number(matched![2]) }; } +/** + * @scenario entry_point=admin_update_user, min_length_setting=raised, special_char_setting=required + * @effects admin_screen_min_length_bounds_follow_settings_limits + */ describe('보안 설정 — 비밀번호 최소 길이', () => { const limits = configuredLimits(); const input = findByName(tabSecurity, 'security.password_min_length'); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-template-layout-edit.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-template-layout-edit.test.tsx index 871d65b5..a56c7d26 100644 --- a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-template-layout-edit.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-template-layout-edit.test.tsx @@ -543,6 +543,7 @@ describe('코드 편집기 ?route= URL 동기화', () => { expect(routeInit.params.selectedNodeKind).toBe('layout'); }); + /** @effects code_editor_file_select_syncs_route_query */ it('데스크탑 파일 항목 클릭 시 replaceUrl 로 file.route_path 를 ?route= 에 반영한다', () => { // 좌측 트리 레이아웃 파일 iteration (file_var) 의 click 시퀀스 const fileIteration = findComponent(layoutEditJson, (n) => @@ -567,6 +568,7 @@ describe('코드 편집기 ?route= URL 동기화', () => { expect(replaceUrl.params.query.route).toContain('route_path'); }); + /** @effects code_editor_extension_select_clears_route_query */ it('확장 선택 시 replaceUrl 로 ?route= 를 비운다 (확장은 라우트 없음)', () => { const extIteration = findComponent(layoutEditJson, (n) => Boolean(n.iteration?.source?.includes('extGroup.extensions')) diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-form-withdraw-confirm.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-form-withdraw-confirm.test.tsx new file mode 100644 index 00000000..db1b52b1 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-form-withdraw-confirm.test.tsx @@ -0,0 +1,245 @@ +/** + * @file admin-user-form-withdraw-confirm.test.tsx + * @description 회원 수정 폼 — 상태 '탈퇴' 저장 시 확인 절차 (공개이슈 #112) + * + * 서버에서 관리자 상태 변경 경로가 정식 탈퇴(비가역 익명화)로 통일되었으므로, + * 이 폼의 저장이 경고 없이 회원 정보를 파기하지 않아야 한다. + * + * 합성 레이아웃이 아니라 **실제 admin_user_form.json 의 노드**를 그대로 렌더한다 — + * 사본으로 검증하면 배포되는 레이아웃이 바뀌어도 초록으로 남는다. + * + * @scenario entry_point=admin_form_ui, target_account=normal, target_account=already_withdrawn, confirm_action=cancel, confirm_action=submit + * @effects admin_form_shows_confirm_dialog_before_destructive_save, admin_form_cancel_leaves_user_untouched, admin_form_non_withdraw_status_saves_without_dialog, create_mode_hides_withdrawn_option + */ + +import React from 'react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; +import { createLayoutTest } from '@core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@core/template-engine/ComponentRegistry'; +import userForm from '../../layouts/admin_user_form.json'; + +type Json = Record<string, any>; + +/** 레이아웃 트리에서 id 로 노드를 찾는다. */ +function findById(node: unknown, id: string): Json | null { + if (Array.isArray(node)) { + for (const child of node) { + const found = findById(child, id); + if (found) return found; + } + return null; + } + if (node && typeof node === 'object') { + const obj = node as Json; + if (obj.id === id) return obj; + for (const value of Object.values(obj)) { + const found = findById(value, id); + if (found) return found; + } + } + return null; +} + +const TestDiv: React.FC<any> = ({ className, children }) => ( + <div className={className}>{children}</div> +); +const TestSpan: React.FC<any> = ({ children, text }) => <span>{children ?? text}</span>; +const TestP: React.FC<any> = ({ children, text }) => <p>{children ?? text}</p>; +const TestUl: React.FC<any> = ({ children }) => <ul>{children}</ul>; +const TestLi: React.FC<any> = ({ children, text }) => <li>{children ?? text}</li>; +const TestButton: React.FC<any> = ({ children, onClick, disabled, className }) => ( + <button type="button" onClick={onClick} disabled={disabled} className={className}> + {children} + </button> +); +const TestIcon: React.FC<any> = ({ name }) => <i data-icon={name} />; +const TestFragment: React.FC<any> = ({ children }) => <>{children}</>; +const TestModal: React.FC<any> = ({ isOpen, title, children }) => + isOpen ? ( + <div role="dialog" data-testid="withdraw-confirm-dialog"> + <div data-testid="withdraw-confirm-title">{title}</div> + {children} + </div> + ) : null; + +function setupRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + + (registry as any).registry = { + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + Ul: { component: TestUl, metadata: { name: 'Ul', type: 'basic' } }, + Li: { component: TestLi, metadata: { name: 'Li', type: 'basic' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } }, + Modal: { component: TestModal, metadata: { name: 'Modal', type: 'composite' } }, + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + }; + + return registry; +} + +/** 실제 레이아웃의 저장 버튼 + 확인 다이얼로그만 떼어낸 테스트 레이아웃 */ +function buildLayout(): Json { + const saveButton = findById(userForm.slots.content, 'footer_save_button'); + const dialog = findById(userForm.slots.content, 'withdraw_confirm_dialog'); + + if (!saveButton || !dialog) { + throw new Error('저장 버튼 또는 확인 다이얼로그 노드를 찾지 못했습니다.'); + } + + return { + version: '1.0.0', + layout_name: 'test_admin_user_form_withdraw', + named_actions: (userForm as Json).named_actions, + components: [saveButton, dialog], + }; +} + +describe('회원 수정 폼 — 탈퇴 저장 확인 절차', () => { + let registry: ComponentRegistry; + let testUtils: ReturnType<typeof createLayoutTest>; + + beforeEach(() => { + registry = setupRegistry(); + }); + + afterEach(() => { + testUtils?.cleanup(); + }); + + /** 테스트 하네스가 설치한 fetch spy 에 기록된 회원 저장 요청 URL 목록 */ + function saveRequests(): string[] { + const spy = globalThis.fetch as unknown as { mock?: { calls: any[][] } }; + + return (spy.mock?.calls ?? []) + .map((call) => String(call[0])) + .filter((url) => url.includes('/api/admin/users')); + } + + /** 지정한 폼 상태로 렌더하고 저장 버튼 클릭까지 수행한다. */ + async function clickSave(formStatus: string, savedStatus: string) { + testUtils = createLayoutTest(buildLayout() as any, { + componentRegistry: registry, + routeParams: { id: '7' }, + translations: {}, + // 저장 버튼은 폼이 시드된 뒤에만 활성이므로 email 까지 채운 상태로 렌더한다 + initialState: { _local: { form: { status: formStatus, email: 'seeded@example.com' } } }, + initialData: { user: { data: { id: 7, status: savedStatus } } }, + }); + + await testUtils.render(); + + const buttons = screen.getAllByRole('button'); + fireEvent.click(buttons[0]); + } + + it('상태를 탈퇴로 바꿔 저장하면 확인 다이얼로그가 먼저 열리고 저장 요청은 나가지 않는다', async () => { + await clickSave('withdrawn', 'active'); + + // 존재를 먼저 확정한 뒤에 부재를 단언한다 (부재 단언은 렌더 전에도 통과한다) + await waitFor(() => { + expect(screen.getByTestId('withdraw-confirm-dialog')).toBeInTheDocument(); + }); + + expect(saveRequests()).toHaveLength(0); + }); + + it('확인 다이얼로그의 처리 버튼을 누르면 그때 저장 요청이 나간다', async () => { + await clickSave('withdrawn', 'active'); + + await waitFor(() => { + expect(screen.getByTestId('withdraw-confirm-dialog')).toBeInTheDocument(); + }); + + const dialog = screen.getByTestId('withdraw-confirm-dialog'); + const dialogButtons = Array.from(dialog.querySelectorAll('button')); + + expect(dialogButtons).toHaveLength(2); + + fireEvent.click(dialogButtons[1]); + + await waitFor(() => { + expect(saveRequests()).not.toHaveLength(0); + }); + }); + + it('탈퇴 외 상태 저장은 다이얼로그 없이 즉시 요청한다', async () => { + await clickSave('inactive', 'active'); + + await waitFor(() => { + expect(saveRequests()).not.toHaveLength(0); + }); + + expect(screen.queryByTestId('withdraw-confirm-dialog')).not.toBeInTheDocument(); + }); + + it('이미 탈퇴한 회원을 저장할 때는 다이얼로그를 다시 띄우지 않는다', async () => { + await clickSave('withdrawn', 'withdrawn'); + + await waitFor(() => { + expect(saveRequests()).not.toHaveLength(0); + }); + + expect(screen.queryByTestId('withdraw-confirm-dialog')).not.toBeInTheDocument(); + }); + + it('등록 모드에서는 상태 셀렉트에 탈퇴 옵션이 없다', () => { + const statusSelect = findById(userForm.slots.content, 'field_status'); + const optionsExpr = JSON.stringify(statusSelect); + + // 편집 모드(route.id)에서만 withdrawn 이 포함되는 분기여야 한다 + expect(optionsExpr).toContain('route?.id'); + expect(optionsExpr).toContain('withdrawn'); + + const select = JSON.parse(optionsExpr); + const options = select.children.find((c: Json) => c.name === 'Select')?.props?.options; + + expect(typeof options).toBe('string'); + + const [editBranch, createBranch] = String(options).split(' : '); + + expect(editBranch).toContain("value: 'withdrawn'"); + expect(createBranch).not.toContain("value: 'withdrawn'"); + }); + + it('저장 버튼과 확인 다이얼로그가 완전히 같은 저장 요청을 보낸다', () => { + // 저장 시퀀스는 두 곳에 복제되어 있다(다이얼로그를 거치는 경로 / 거치지 않는 경로). + // 한쪽만 고치면 확인 절차를 거친 저장만 다른 본문을 보내게 되는데, 화면에는 + // 아무 차이도 드러나지 않는다 — 두 요청 정의가 동일함을 구조로 고정한다. + const saveButton = findById(userForm.slots.content, 'footer_save_button'); + const dialog = findById(userForm.slots.content, 'withdraw_confirm_dialog'); + + const collectSaveCalls = (node: unknown): Json[] => { + const found: Json[] = []; + const walk = (n: unknown): void => { + if (Array.isArray(n)) { + n.forEach(walk); + + return; + } + if (n && typeof n === 'object') { + const obj = n as Json; + if (obj.handler === 'apiCall' && String(obj.target ?? '').includes('users')) { + found.push(obj); + } + Object.values(obj).forEach(walk); + } + }; + walk(node); + + return found; + }; + + const fromButton = collectSaveCalls(saveButton); + const fromDialog = collectSaveCalls(dialog); + + expect(fromButton.length).toBeGreaterThan(0); + expect(fromDialog.length).toBeGreaterThan(0); + expect(fromDialog.length).toBe(fromButton.length); + + expect(JSON.stringify(fromDialog)).toBe(JSON.stringify(fromButton)); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-status-result-feedback.test.tsx b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-status-result-feedback.test.tsx new file mode 100644 index 00000000..780e2c6d --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-status-result-feedback.test.tsx @@ -0,0 +1,82 @@ +/** + * @file admin-user-list-bulk-status-result-feedback.test.tsx + * @description 일괄 탈퇴 결과 안내 검증 (공개이슈 #112 후속) + * + * 일괄 탈퇴는 탈퇴 불가 대상(관리자 계정 등)이 섞여 있어도 200 으로 끝난다 — + * 서버는 건별 실패를 failed_count / failed_reasons 로 보고한다. 화면이 그 값을 + * 읽지 않고 성공 안내만 띄우면, 아무 일도 일어나지 않았는데 처리된 것으로 보인다. + * + * @scenario entry_point=admin_bulk_status, target_account=admin, confirm_action=submit + * @effects bulk_status_reports_failed_count, bulk_status_result_is_reflected_in_feedback + */ + +import { describe, it, expect } from 'vitest'; +import userListLayout from '../../layouts/admin_user_list.json'; +import adminKo from '../../lang/partial/ko/admin.json'; +import adminEn from '../../lang/partial/en/admin.json'; + +type Json = Record<string, any>; + +/** 일괄 탈퇴 apiCall 노드를 찾는다. */ +function findWithdrawBulkCall(node: unknown): Json | undefined { + let found: Json | undefined; + + const walk = (n: unknown): void => { + if (found) return; + if (Array.isArray(n)) { + n.forEach(walk); + return; + } + if (n && typeof n === 'object') { + const obj = n as Json; + if ( + obj.handler === 'apiCall' && + String(obj.target ?? '').includes('bulk-status') && + obj.params?.body?.status === 'withdrawn' + ) { + found = obj; + return; + } + Object.values(obj).forEach(walk); + } + }; + + walk(node); + + return found; +} + +describe('회원 목록 일괄 탈퇴 — 결과 안내', () => { + const call = findWithdrawBulkCall(userListLayout); + + it('일괄 탈퇴 요청 노드가 존재한다', () => { + expect(call).toBeDefined(); + }); + + it('성공 후속 안내가 failed_count 를 판정에 사용한다', () => { + const toast = (call!.onSuccess ?? []).find((a: Json) => a.handler === 'toast'); + + expect(toast, '성공 후속에 안내 액션이 없습니다.').toBeDefined(); + + const serialized = JSON.stringify(toast); + + // 무조건 성공 안내로 되돌아가면(회귀) 이 단언이 깨진다 + expect(serialized).toContain('failed_count'); + expect(serialized).toContain('updated_count'); + }); + + it('전건 실패 / 부분 실패 안내 문구가 ko·en 에 정의되어 있다', () => { + for (const dict of [adminKo, adminEn] as unknown as Json[]) { + expect(dict.users.modals.bulk_withdraw_all_failed).toBeTruthy(); + expect(dict.users.modals.bulk_withdraw_partial).toBeTruthy(); + } + }); + + it('안내 문구가 실패 사유 자리를 갖는다', () => { + // 사유가 빠지면 운영자는 "몇 건 실패" 까지만 알고 원인을 알 수 없다 + for (const dict of [adminKo, adminEn] as unknown as Json[]) { + expect(String(dict.users.modals.bulk_withdraw_all_failed)).toContain('{reason}'); + expect(String(dict.users.modals.bulk_withdraw_partial)).toContain('{reason}'); + } + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-withdraw-feedback-expression.test.ts b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-withdraw-feedback-expression.test.ts new file mode 100644 index 00000000..a26b6d2a --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-list-bulk-withdraw-feedback-expression.test.ts @@ -0,0 +1,108 @@ +/** + * @file admin-user-list-bulk-withdraw-feedback-expression.test.ts + * @description 일괄 탈퇴 결과 안내 표현식이 실제로 평가되는지 검증 (공개이슈 #112 후속) + * + * 구조 단언(노드에 failed_count 가 들어 있다)만으로는 표현식이 런타임에 평가된다는 + * 보장이 없다 — 오타 하나면 undefined 가 되어 안내가 통째로 비고, 예외도 남지 않는다. + * 레이아웃에 실제로 적힌 문자열을 엔진에 그대로 넣어 세 갈래를 모두 확인한다. + * + * @scenario entry_point=admin_bulk_status, target_account=admin, confirm_action=submit + * @effects bulk_status_result_is_reflected_in_feedback + */ + +import { describe, it, expect } from 'vitest'; +import { dataBindingEngine } from '@core/template-engine/DataBindingEngine'; +import userListLayout from '../../layouts/admin_user_list.json'; + +type Json = Record<string, any>; + +/** 일괄 탈퇴 성공 후속의 안내 액션을 레이아웃에서 찾는다. */ +function findWithdrawToast(): Json { + let found: Json | undefined; + + const walk = (n: unknown): void => { + if (found) return; + if (Array.isArray(n)) { + n.forEach(walk); + return; + } + if (n && typeof n === 'object') { + const obj = n as Json; + if ( + obj.handler === 'apiCall' && + String(obj.target ?? '').includes('bulk-status') && + obj.params?.body?.status === 'withdrawn' + ) { + found = (obj.onSuccess ?? []).find((a: Json) => a.handler === 'toast'); + return; + } + Object.values(obj).forEach(walk); + } + }; + + walk(userListLayout); + + if (!found) throw new Error('일괄 탈퇴 안내 액션을 찾지 못했습니다.'); + + return found; +} + +const toast = findWithdrawToast(); + +/** + * 레이아웃에 적힌 표현식을 주어진 응답으로 평가한다. + * + * 하네스에는 번역 사전이 없어 엔진이 `$t:` 접두사를 벗기고 키만 돌려준다 — + * 여기서 보는 것은 번역 결과가 아니라 **어느 키·파라미터가 선택되는가** 이므로 + * 접두사 유무는 판정에서 제외한다. + */ +function evaluate(expression: string, data: Json): string { + const resolved = String(dataBindingEngine.resolveBindings(expression, { response: { data } })); + + return resolved.replace(/^\$t:/, ''); +} + +describe('일괄 탈퇴 결과 안내 표현식', () => { + it('전건 성공이면 성공 안내', () => { + const context = { updated_count: 3, failed_count: 0, failed_reasons: [] }; + + expect(evaluate(toast.params.type, context)).toBe('success'); + expect(evaluate(toast.params.message, context)).toBe('admin.users.modals.bulk_withdraw_success'); + }); + + it('전건 실패면 오류 안내에 사유가 실린다', () => { + const context = { + updated_count: 0, + failed_count: 1, + failed_reasons: ['관리자 계정은 탈퇴할 수 없습니다.'], + }; + + expect(evaluate(toast.params.type, context)).toBe('error'); + + const message = evaluate(toast.params.message, context); + + expect(message).toContain('bulk_withdraw_all_failed'); + expect(message).toContain('관리자 계정은 탈퇴할 수 없습니다.'); + }); + + it('부분 실패면 경고 안내에 처리/미처리 인원이 실린다', () => { + const context = { + updated_count: 2, + failed_count: 1, + failed_reasons: ['관리자 계정은 탈퇴할 수 없습니다.'], + }; + + expect(evaluate(toast.params.type, context)).toBe('warning'); + + const message = evaluate(toast.params.message, context); + + expect(message).toContain('bulk_withdraw_partial'); + expect(message).toContain('updated=2'); + expect(message).toContain('failed=1'); + expect(message).toContain('관리자 계정은 탈퇴할 수 없습니다.'); + }); + + it('응답에 필드가 없어도 성공 안내로 떨어진다 (구버전 서버 하위호환)', () => { + expect(evaluate(toast.params.type, {})).toBe('success'); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/dist/css/components.css b/templates/_bundled/sirsoft-admin_basic/dist/css/components.css index 38d052a8..0547bbeb 100644 --- a/templates/_bundled/sirsoft-admin_basic/dist/css/components.css +++ b/templates/_bundled/sirsoft-admin_basic/dist/css/components.css @@ -1 +1 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Pretendard", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 none);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 none);--color-neutral-100:oklch(97% 0 none);--color-neutral-200:oklch(92.2% 0 none);--color-neutral-300:oklch(87% 0 none);--color-neutral-400:oklch(70.8% 0 none);--color-neutral-500:oklch(55.6% 0 none);--color-neutral-600:oklch(43.9% 0 none);--color-neutral-700:oklch(37.1% 0 none);--color-neutral-800:oklch(26.9% 0 none);--color-neutral-900:oklch(20.5% 0 none);--color-neutral-950:oklch(14.5% 0 none);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{color:var(--color-gray-900);font-size:.875rem;line-height:1.5}.dark :is(body){color:var(--color-gray-100)}}@layer components{.admin_layout_root{background-color:var(--color-gray-50);height:100vh;display:flex;overflow:hidden}.dark .admin_layout_root{background-color:var(--color-gray-900)}.mobile_overlay{z-index:40;background-color:#00000080;position:fixed;inset:0}.left_sidebar_area{z-index:50;width:calc(var(--spacing) * 64);background-color:var(--color-white);inset-block:0;flex-direction:column;display:flex;position:relative;left:0}.dark .left_sidebar_area{background-color:var(--color-gray-800)}.left_sidebar_area{transition:width .3s}.left_sidebar_area_collapsed{width:0;overflow:hidden}.left_sidebar_area_portable{z-index:50;width:calc(var(--spacing) * 64);background-color:var(--color-white);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;flex-direction:column;transition-duration:.3s;display:flex;position:fixed;left:0}.dark .left_sidebar_area_portable{background-color:var(--color-gray-800)}.left_sidebar_area_portable{top:4rem;bottom:0;transform:translate(-100%)}.left_sidebar_area_portable_open{transform:translate(0)}.sidebar_header{height:calc(var(--spacing) * 16);padding-inline:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.sidebar_logo{height:calc(var(--spacing) * 10);align-items:center;gap:calc(var(--spacing) * 2);flex:1;min-width:0;display:flex}.logo_image{max-height:calc(var(--spacing) * 10);object-fit:contain;width:auto}.admin_sidebar_menu{padding-top:calc(var(--spacing) * 4);box-shadow:inset 3px 2px 2px var(--tw-shadow-color,#0000000a);flex:1;overflow-y:auto}.admin_sidebar_menu_portable{padding-top:calc(var(--spacing) * 4);flex:1;overflow-y:auto}.admin_sidebar_empty{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 8);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-400)}.dark .admin_sidebar_empty{color:var(--color-gray-500)}.admin_sidebar_empty_icon{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));color:var(--color-gray-300)}.dark .admin_sidebar_empty_icon{color:var(--color-gray-600)}.sidebar_footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100);padding:calc(var(--spacing) * 3);flex-shrink:0}.dark .sidebar_footer{border-color:var(--color-gray-700)}.sidebar_view_site_link{height:calc(var(--spacing) * 9);align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));text-decoration-line:none;display:flex}@media(hover:hover){.sidebar_view_site_link:hover{background-color:var(--color-slate-100);color:var(--color-slate-900)}}.dark .sidebar_view_site_link{color:var(--color-slate-300)}@media(hover:hover){.dark .sidebar_view_site_link:hover{background-color:var(--color-slate-700);color:var(--color-white)}}.sidebar_view_site_icon{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-shrink:0}.sidebar_view_site_label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.right_content_wrapper{flex-direction:column;flex:1;min-width:0;display:flex}.right_content_area{flex:1;min-width:0;min-height:0;overflow:auto}.main_header{z-index:50;height:calc(var(--spacing) * 16);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 8);flex-shrink:0;justify-content:space-between;align-items:center;display:flex;position:relative}.dark .main_header{background-color:var(--color-gray-800)}.main_header{box-shadow:0 1px 3px #00000014}.header_left{flex:1;align-items:center;display:flex}.header_right{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.menu_toggle_btn{margin-left:calc(var(--spacing) * -2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);color:var(--color-slate-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.menu_toggle_btn:hover{background-color:var(--color-slate-100);color:var(--color-slate-900)}}.dark .menu_toggle_btn{color:var(--color-slate-400)}@media(hover:hover){.dark .menu_toggle_btn:hover{background-color:var(--color-slate-700);color:var(--color-white)}}.menu_toggle_btn_icon{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.user_avatar_section{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 3);display:flex;position:relative}.user_avatar{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);background-color:var(--color-gray-200);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex;overflow:hidden}.dark .user_avatar{background-color:var(--color-gray-600)}.user_avatar_img{object-fit:cover;width:100%;height:100%}.user_info{display:none}@media(min-width:48rem){.user_info{display:block}}.mobile_header_portable{z-index:50;height:calc(var(--spacing) * 16);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex;position:relative}.dark .mobile_header_portable{background-color:var(--color-gray-800)}.mobile_header_portable{box-shadow:0 1px 3px #00000014}.mobile_header_left{align-items:center;gap:calc(var(--spacing) * 3);display:flex}.mobile_header_right{align-items:center;gap:calc(var(--spacing) * 2);flex-shrink:0;display:flex}.mobile_logo{align-items:center;gap:calc(var(--spacing) * 2);flex:1;min-width:0;display:flex}.mobile_logo_image{height:calc(var(--spacing) * 8);object-fit:contain;width:auto;max-width:100%}.main_content{min-height:fit-content}.layout_warnings{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2)}.text-primary{color:var(--color-gray-900)}.dark .text-primary{color:var(--color-white)}.text-secondary{color:var(--color-gray-600)}.dark .text-secondary{color:var(--color-gray-400)}.text-muted,.dark .text-muted{color:var(--color-gray-500)}.text-tertiary{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .text-tertiary{color:var(--color-gray-400)}.text-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .text-label{color:var(--color-gray-400)}.text-label-subtle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .text-label-subtle{color:var(--color-gray-400)}.text-heading{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .text-heading{color:var(--color-white)}.section-heading{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-heading{color:var(--color-white)}.section-title{margin-bottom:var(--spacing);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-title{color:var(--color-white)}.section-description{margin-bottom:calc(var(--spacing) * 6);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .section-description{color:var(--color-gray-400)}.card-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .card-title{color:var(--color-white)}.card-title-aside{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);display:flex}.card-description{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .card-description{color:var(--color-gray-400)}.admin-card>.card-title{margin-bottom:calc(var(--spacing) * 4)}.admin-card>.card-title:has(+.card-description){margin-bottom:0}.admin-card>.card-description{margin-bottom:calc(var(--spacing) * 4)}.card-header{align-items:center;gap:calc(var(--spacing) * 2);--tw-leading:1;margin-bottom:0;line-height:1;display:flex}.card-header>*{--tw-leading:1;line-height:1}.text-body{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}.dark .text-body{color:var(--color-gray-300)}.icon-muted{color:var(--color-gray-400)}.dark .icon-muted{color:var(--color-gray-500)}.bg-success{background-color:var(--color-green-100)}.dark .bg-success{background-color:var(--color-green-900)}.bg-success-light{background-color:var(--color-green-50)}.dark .bg-success-light{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .bg-success-light{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.bg-success-solid{background-color:var(--color-green-600)}.dark .bg-success-solid{background-color:var(--color-green-500)}.bg-error{background-color:var(--color-red-100)}.dark .bg-error{background-color:var(--color-red-900)}.bg-error-light{background-color:var(--color-red-50)}.dark .bg-error-light{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .bg-error-light{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.bg-error-solid{background-color:var(--color-red-600)}.dark .bg-error-solid{background-color:var(--color-red-500)}.bg-warning{background-color:var(--color-yellow-100)}.dark .bg-warning{background-color:var(--color-yellow-900)}.bg-warning-light{background-color:var(--color-yellow-50)}.dark .bg-warning-light{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .bg-warning-light{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.bg-warning-solid{background-color:var(--color-yellow-600)}.dark .bg-warning-solid{background-color:var(--color-yellow-500)}.bg-info{background-color:var(--color-blue-100)}.dark .bg-info{background-color:var(--color-blue-900)}.bg-info-light{background-color:var(--color-blue-50)}.dark .bg-info-light{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .bg-info-light{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.bg-info-solid{background-color:var(--color-blue-600)}.dark .bg-info-solid{background-color:var(--color-blue-500)}.bg-ready{background-color:var(--color-cyan-100)}.dark .bg-ready{background-color:var(--color-cyan-900)}.bg-ready-light{background-color:var(--color-cyan-50)}.dark .bg-ready-light{background-color:#104e6433}@supports (color:color-mix(in lab,red,red)){.dark .bg-ready-light{background-color:color-mix(in oklab,var(--color-cyan-900) 20%,transparent)}}.bg-ready-solid{background-color:var(--color-cyan-600)}.dark .bg-ready-solid{background-color:var(--color-cyan-400)}.bg-progress{background-color:var(--color-indigo-100)}.dark .bg-progress{background-color:var(--color-indigo-900)}.bg-progress-light{background-color:var(--color-indigo-50)}.dark .bg-progress-light{background-color:#312c8533}@supports (color:color-mix(in lab,red,red)){.dark .bg-progress-light{background-color:color-mix(in oklab,var(--color-indigo-900) 20%,transparent)}}.bg-progress-solid{background-color:var(--color-indigo-600)}.dark .bg-progress-solid{background-color:var(--color-indigo-400)}.bg-complete{background-color:var(--color-pink-100)}.dark .bg-complete{background-color:var(--color-pink-900)}.bg-complete-light{background-color:var(--color-pink-50)}.dark .bg-complete-light{background-color:#86104333}@supports (color:color-mix(in lab,red,red)){.dark .bg-complete-light{background-color:color-mix(in oklab,var(--color-pink-900) 20%,transparent)}}.bg-complete-solid{background-color:var(--color-pink-600)}.dark .bg-complete-solid{background-color:var(--color-pink-400)}.bg-hold{background-color:var(--color-gray-100)}.dark .bg-hold{background-color:var(--color-gray-800)}.bg-hold-light{background-color:var(--color-gray-50)}.dark .bg-hold-light{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .bg-hold-light{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.bg-hold-solid{background-color:var(--color-gray-500)}.dark .bg-hold-solid{background-color:var(--color-gray-400)}.text-success{color:var(--color-green-600)}.dark .text-success{color:var(--color-green-400)}.text-error{color:var(--color-red-600)}.dark .text-error{color:var(--color-red-400)}.text-warning{color:var(--color-yellow-600)}.dark .text-warning{color:var(--color-yellow-400)}.text-info{color:var(--color-blue-600)}.dark .text-info{color:var(--color-blue-400)}.text-ready{color:var(--color-cyan-600)}.dark .text-ready{color:var(--color-cyan-400)}.text-progress{color:var(--color-indigo-600)}.dark .text-progress{color:var(--color-indigo-400)}.text-complete{color:var(--color-pink-600)}.dark .text-complete{color:var(--color-pink-400)}.text-hold{color:var(--color-gray-500)}.dark .text-hold{color:var(--color-gray-400)}.text-success-light{color:var(--color-white)}.dark .text-success-light{color:var(--color-green-900)}.text-error-light{color:var(--color-white)}.dark .text-error-light{color:var(--color-red-900)}.text-warning-light{color:var(--color-white)}.dark .text-warning-light{color:var(--color-yellow-900)}.text-info-light{color:var(--color-white)}.dark .text-info-light{color:var(--color-blue-900)}.text-ready-light{color:var(--color-white)}.dark .text-ready-light{color:var(--color-cyan-900)}.text-progress-light{color:var(--color-white)}.dark .text-progress-light{color:var(--color-indigo-900)}.text-complete-light{color:var(--color-white)}.dark .text-complete-light{color:var(--color-pink-900)}.text-hold-light{color:var(--color-white)}.dark .text-hold-light{color:var(--color-gray-900)}.border-success{border-color:var(--color-green-200)}.dark .border-success{border-color:var(--color-green-800)}.border-error{border-color:var(--color-red-200)}.dark .border-error{border-color:var(--color-red-800)}.border-warning{border-color:var(--color-yellow-200)}.dark .border-warning{border-color:var(--color-yellow-800)}.border-info{border-color:var(--color-blue-200)}.dark .border-info{border-color:var(--color-blue-800)}.border-ready{border-color:var(--color-cyan-200)}.dark .border-ready{border-color:var(--color-cyan-800)}.border-progress{border-color:var(--color-indigo-200)}.dark .border-progress{border-color:var(--color-indigo-800)}.border-complete{border-color:var(--color-pink-200)}.dark .border-complete{border-color:var(--color-pink-800)}.border-hold{border-color:var(--color-gray-300)}.dark .border-hold{border-color:var(--color-gray-600)}.btn-success{background-color:var(--color-green-600);color:var(--color-white)}@media(hover:hover){.btn-success:hover{background-color:var(--color-green-700)}}.dark .btn-success{background-color:var(--color-green-500)}@media(hover:hover){.dark .btn-success:hover{background-color:var(--color-green-600)}}.btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.btn-danger:hover{background-color:var(--color-red-700)}}.dark .btn-danger{background-color:var(--color-red-500)}@media(hover:hover){.dark .btn-danger:hover{background-color:var(--color-red-600)}}.btn-warning{background-color:var(--color-yellow-600);color:var(--color-white)}@media(hover:hover){.btn-warning:hover{background-color:var(--color-yellow-700)}}.dark .btn-warning{background-color:var(--color-yellow-500)}@media(hover:hover){.dark .btn-warning:hover{background-color:var(--color-yellow-600)}}.btn-info{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.btn-info:hover{background-color:var(--color-blue-700)}}.dark .btn-info{background-color:var(--color-blue-500)}@media(hover:hover){.dark .btn-info:hover{background-color:var(--color-blue-600)}}.btn-neutral{background-color:var(--color-gray-600);color:var(--color-white)}@media(hover:hover){.btn-neutral:hover{background-color:var(--color-gray-700)}}.dark .btn-neutral{background-color:var(--color-gray-500)}@media(hover:hover){.dark .btn-neutral:hover{background-color:var(--color-gray-600)}}.btn-sm{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5)}.button{cursor:pointer;justify-content:center;align-items:center;display:inline-flex}.button:disabled{cursor:not-allowed}.btn{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s;display:inline-flex}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-gray-900);color:var(--color-white)}@media(hover:hover){.btn-primary:hover{background-color:var(--color-gray-800)}}.dark .btn-primary{background-color:var(--color-white);color:var(--color-gray-900)}@media(hover:hover){.dark .btn-primary:hover{background-color:var(--color-gray-100)}}.btn-secondary{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);color:var(--color-gray-700)}@media(hover:hover){.btn-secondary:hover{background-color:var(--color-gray-50)}}.dark .btn-secondary{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-300)}@media(hover:hover){.dark .btn-secondary:hover{background-color:var(--color-gray-700)}}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.btn-outline:hover{background-color:var(--color-gray-50)}}.dark .btn-outline{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .btn-outline:hover{background-color:var(--color-gray-700)}}.btn-ghost{color:var(--color-gray-600);background-color:#0000}@media(hover:hover){.btn-ghost:hover{background-color:var(--color-gray-100)}}.dark .btn-ghost{color:var(--color-gray-400)}@media(hover:hover){.dark .btn-ghost:hover{background-color:var(--color-gray-700)}}.btn-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);color:var(--color-gray-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.btn-icon:hover{background-color:var(--color-gray-100)}}.dark .btn-icon{color:var(--color-gray-400)}@media(hover:hover){.dark .btn-icon:hover{background-color:var(--color-gray-700)}}.btn-preset{background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);border-radius:3.40282e38px}.dark .btn-preset{background-color:var(--color-gray-700);color:var(--color-gray-300)}.btn-date{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .btn-date{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-date.active{border-color:var(--color-gray-900);background-color:var(--color-gray-900);color:var(--color-white)}.dark .btn-date.active{border-color:var(--color-white);background-color:var(--color-white);color:var(--color-gray-900)}.btn-badge{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600);border-radius:3.40282e38px}.dark .btn-badge{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-badge.active{border-color:var(--color-gray-900);color:var(--color-gray-900)}.dark .btn-badge.active{border-color:var(--color-white);color:var(--color-blue-400)}.btn-group{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);display:inline-flex;overflow:hidden}.dark .btn-group{border-color:var(--color-gray-700)}.btn-group-item{border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.btn-group-item:hover{background-color:var(--color-gray-50)}}.dark .btn-group-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}@media(hover:hover){.dark .btn-group-item:hover{background-color:var(--color-gray-700)}}.btn-group-item:last-child{border-right-style:var(--tw-border-style);border-right-width:0}.btn-group-item.active{background-color:var(--color-gray-900);color:var(--color-white)}.dark .btn-group-item.active{background-color:var(--color-white);color:var(--color-gray-900)}.btn-badge-green{border-color:var(--color-green-500);background-color:var(--color-green-50);color:var(--color-green-600)}.dark .btn-badge-green{border-color:var(--color-green-400);background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-green{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .btn-badge-green{color:var(--color-green-400)}.btn-badge-blue{border-color:var(--color-blue-500);background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .btn-badge-blue{border-color:var(--color-blue-400);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-blue{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .btn-badge-blue{color:var(--color-blue-400)}.btn-badge-cyan{border-color:var(--color-cyan-500);background-color:var(--color-cyan-50);color:var(--color-cyan-600)}.dark .btn-badge-cyan{border-color:var(--color-cyan-400);background-color:#104e6433}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-cyan{background-color:color-mix(in oklab,var(--color-cyan-900) 20%,transparent)}}.dark .btn-badge-cyan{color:var(--color-cyan-400)}.btn-badge-indigo{border-color:var(--color-indigo-500);background-color:var(--color-indigo-50);color:var(--color-indigo-600)}.dark .btn-badge-indigo{border-color:var(--color-indigo-400);background-color:#312c8533}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-indigo{background-color:color-mix(in oklab,var(--color-indigo-900) 20%,transparent)}}.dark .btn-badge-indigo{color:var(--color-indigo-400)}.btn-badge-pink{border-color:var(--color-pink-500);background-color:var(--color-pink-50);color:var(--color-pink-600)}.dark .btn-badge-pink{border-color:var(--color-pink-400);background-color:#86104333}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-pink{background-color:color-mix(in oklab,var(--color-pink-900) 20%,transparent)}}.dark .btn-badge-pink{color:var(--color-pink-400)}.btn-badge-red{border-color:var(--color-red-500);background-color:var(--color-red-50);color:var(--color-red-600)}.dark .btn-badge-red{border-color:var(--color-red-400);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-red{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .btn-badge-red{color:var(--color-red-400)}.btn-badge-gray{border-color:var(--color-gray-300);background-color:var(--color-gray-50);color:var(--color-gray-600)}.dark .btn-badge-gray{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-badge-yellow{border-color:var(--color-yellow-500);background-color:var(--color-yellow-50);color:var(--color-yellow-600)}.dark .btn-badge-yellow{border-color:var(--color-yellow-400);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-yellow{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .btn-badge-yellow{color:var(--color-yellow-400)}.btn-badge-orange{border-color:var(--color-orange-500);background-color:var(--color-orange-50);color:var(--color-orange-600)}.dark .btn-badge-orange{border-color:var(--color-orange-400);background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-orange{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .btn-badge-orange{color:var(--color-orange-400)}.input{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input::placeholder{color:var(--color-gray-400)}.input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .input{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .input::placeholder{color:var(--color-gray-500)}.input:disabled{cursor:not-allowed;background-color:var(--color-gray-100);color:var(--color-gray-400)}.dark .input:disabled{background-color:var(--color-gray-800);color:var(--color-gray-500)}.input-tag{border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input-tag:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .input-tag{background-color:var(--color-gray-700);color:var(--color-gray-100)}.input-tag{min-height:2.25rem}.textarea{border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.textarea::placeholder{color:var(--color-gray-400)}.textarea:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .textarea{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .textarea::placeholder{color:var(--color-gray-500)}.textarea:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .textarea:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.input-readonly{cursor:default;color:var(--color-gray-500)}.dark .input-readonly{color:var(--color-gray-400)}.input-w-xs{width:calc(var(--spacing) * 20)}.input-w-sm{width:calc(var(--spacing) * 32)}.input-w-md{width:calc(var(--spacing) * 48)}.input-w-lg{width:calc(var(--spacing) * 64)}.input-w-xl{width:calc(var(--spacing) * 80)}.input-w-auto{width:auto}.input-search-container{border-radius:var(--radius-lg);background-color:var(--color-gray-100);align-items:center;display:flex;overflow:hidden}.dark .input-search-container{background-color:var(--color-gray-700)}.input-search-container .input-search-icon{padding-left:calc(var(--spacing) * 3);color:var(--color-gray-400)}.dark :is(.input-search-container .input-search-icon){color:var(--color-gray-500)}.input-search-container input{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);background-color:#0000;flex:1}.input-search-container input::placeholder{color:var(--color-gray-400)}.input-search-container input:focus{--tw-outline-style:none;outline-style:none}.dark :is(.input-search-container input){color:var(--color-gray-100)}.dark :is(.input-search-container input)::placeholder{color:var(--color-gray-500)}.input-search-container .input-search-dropdown{cursor:pointer;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}@media(hover:hover){.input-search-container .input-search-dropdown:hover{background-color:var(--color-gray-50)}}.dark :is(.input-search-container .input-search-dropdown){border-color:var(--color-gray-700);color:var(--color-gray-400)}@media(hover:hover){.dark :is(.input-search-container .input-search-dropdown):hover{background-color:var(--color-gray-700)}}.select{height:calc(var(--spacing) * 9);cursor:pointer;appearance:none;border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.select:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select{background-color:var(--color-gray-700);color:var(--color-gray-100)}.select:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .select:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}.dark .select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")}.select-composite{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.select-composite:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select-composite{background-color:var(--color-gray-700);color:var(--color-gray-100)}.select-composite:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .select-composite:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.select-composite-sm{height:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.select-w-sm{width:calc(var(--spacing) * 32)}.select-w-md{width:calc(var(--spacing) * 48)}.select-w-lg{width:calc(var(--spacing) * 64)}.input-split{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.input-split input{border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input-split input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark :is(.input-split input){background-color:var(--color-gray-700);color:var(--color-gray-100)}.input-split .input-split-separator{color:var(--color-gray-400)}.dark :is(.input-split .input-split-separator){color:var(--color-gray-500)}.tag-input-container{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.tag{align-items:center;gap:var(--spacing);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);border-radius:3.40282e38px;display:inline-flex}.dark .tag{background-color:var(--color-gray-700);color:var(--color-gray-300)}.tag .tag-remove{cursor:pointer;color:var(--color-gray-400)}@media(hover:hover){.tag .tag-remove:hover{color:var(--color-gray-600)}}.dark :is(.tag .tag-remove){color:var(--color-gray-500)}@media(hover:hover){.dark :is(.tag .tag-remove):hover{color:var(--color-gray-300)}}:where(.form-group>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}.textarea-sm{min-height:5rem}.textarea-md{min-height:8rem}.textarea-lg{min-height:12rem}.input-error{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-500)}.input-error:focus{--tw-ring-color:var(--color-red-500)}.dark .input-error{border-color:var(--color-red-500)}.input-success{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-500)}.input-success:focus{--tw-ring-color:var(--color-green-500)}.dark .input-success{border-color:var(--color-green-500)}.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-500)}:is(.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea):focus{--tw-ring-color:var(--color-red-500)}.dark :is(.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea){border-color:var(--color-red-500)}.form-error{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-500);display:block}.dark .form-error{color:var(--color-red-400)}.form-success{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-600)}.dark .form-success{color:var(--color-green-400)}.checkbox{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-gray-900);border-radius:.25rem}.checkbox:focus{--tw-ring-color:var(--color-gray-500)}.dark .checkbox{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.checkbox:checked{border-color:var(--color-gray-900);background-color:var(--color-gray-900)}.dark .checkbox:checked{border-color:var(--color-white);background-color:var(--color-white)}.radio{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-blue-600);flex-shrink:0}.radio:focus{--tw-ring-color:var(--color-blue-500)}.dark .radio{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.dark .radio:focus{--tw-ring-color:var(--color-blue-400)}.radio:checked{border-color:var(--color-blue-600)}.dark .radio:checked{border-color:var(--color-blue-400)}.toggle{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 11);cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;align-items:center;display:inline-flex;position:relative}.toggle-track{background-color:var(--color-gray-200);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute;inset:0}.dark .toggle-track{background-color:var(--color-gray-700)}.toggle.active .toggle-track{background-color:var(--color-blue-600)}.toggle-handle{left:calc(var(--spacing) * .5);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute}.toggle.active .toggle-handle{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.badge{align-items:center;gap:var(--spacing);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);display:inline-flex}.badge-green{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-200);background-color:var(--color-green-100);color:var(--color-green-700)}.dark .badge-green{border-color:var(--color-green-800);background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-green{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .badge-green{color:var(--color-green-400)}.badge-blue{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-200);background-color:var(--color-blue-100);color:var(--color-blue-700)}.dark .badge-blue{border-color:var(--color-blue-800);background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-blue{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .badge-blue{color:var(--color-blue-400)}.badge-cyan{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-cyan-200);background-color:var(--color-cyan-100);color:var(--color-cyan-700)}.dark .badge-cyan{border-color:var(--color-cyan-800);background-color:#104e644d}@supports (color:color-mix(in lab,red,red)){.dark .badge-cyan{background-color:color-mix(in oklab,var(--color-cyan-900) 30%,transparent)}}.dark .badge-cyan{color:var(--color-cyan-400)}.badge-indigo{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-indigo-200);background-color:var(--color-indigo-100);color:var(--color-indigo-700)}.dark .badge-indigo{border-color:var(--color-indigo-800);background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .badge-indigo{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .badge-indigo{color:var(--color-indigo-400)}.badge-pink{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-pink-200);background-color:var(--color-pink-100);color:var(--color-pink-700)}.dark .badge-pink{border-color:var(--color-pink-800);background-color:#8610434d}@supports (color:color-mix(in lab,red,red)){.dark .badge-pink{background-color:color-mix(in oklab,var(--color-pink-900) 30%,transparent)}}.dark .badge-pink{color:var(--color-pink-400)}.badge-red{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-100);color:var(--color-red-700)}.dark .badge-red{border-color:var(--color-red-800);background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-red{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .badge-red{color:var(--color-red-400)}.badge-gray{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);color:var(--color-gray-600)}.dark .badge-gray{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.badge-yellow{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-100);color:var(--color-yellow-700)}.dark .badge-yellow{border-color:var(--color-yellow-800);background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-yellow{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .badge-yellow{color:var(--color-yellow-400)}.badge-status{align-items:center;gap:calc(var(--spacing) * 1.5);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;display:inline-flex}.badge-status-success{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .badge-status-success{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-success{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .badge-status-success{color:var(--color-green-400)}.badge-status-processing{background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .badge-status-processing{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-processing{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .badge-status-processing{color:var(--color-blue-400)}.badge-status-error{background-color:var(--color-red-50);color:var(--color-red-600)}.dark .badge-status-error{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-error{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .badge-status-error{color:var(--color-red-400)}.badge-status-expired{background-color:var(--color-gray-50);color:var(--color-gray-500)}.dark .badge-status-expired{background-color:var(--color-gray-800);color:var(--color-gray-400)}.search-bar{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media(min-width:48rem){.search-bar{flex-flow:wrap;align-items:center}}.search-bar-select{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.search-bar-select::placeholder{color:var(--color-gray-400)}.search-bar-select:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .search-bar-select{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .search-bar-select::placeholder{color:var(--color-gray-500)}@media(min-width:48rem){.search-bar-select{width:calc(var(--spacing) * 32);flex-shrink:0}}.search-bar-select-custom{width:100%}@media(min-width:48rem){.search-bar-select-custom{width:calc(var(--spacing) * 32);flex-shrink:0}}.search-bar-input{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.search-bar-input::placeholder{color:var(--color-gray-400)}.search-bar-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .search-bar-input{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .search-bar-input::placeholder{color:var(--color-gray-500)}@media(min-width:48rem){.search-bar-input{min-width:calc(var(--spacing) * 64);flex:1}}.search-bar-reset{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);background-color:#0000;transition-duration:.2s;display:inline-flex}@media(hover:hover){.search-bar-reset:hover{background-color:var(--color-gray-50)}}.dark .search-bar-reset{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .search-bar-reset:hover{background-color:var(--color-gray-700)}}.search-bar-reset{gap:calc(var(--spacing) * 1.5);flex-shrink:0;width:100%}@media(min-width:48rem){.search-bar-reset{width:auto}}.filter-grid{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.filter-row{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}@media(min-width:48rem){.filter-row{flex-direction:row;align-items:center}}.filter-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);display:block}.dark .filter-label{color:var(--color-gray-400)}@media(min-width:48rem){.filter-label{width:calc(var(--spacing) * 14);flex-shrink:0;align-items:center;margin-bottom:0;display:flex}}.filter-buttons{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.dropdown{display:inline-block;position:relative}.dropdown-trigger{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.dropdown-trigger:hover{background-color:var(--color-gray-50)}}.dropdown-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dropdown-trigger{color:var(--color-gray-900)}.dark .dropdown-trigger{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-white)}@media(hover:hover){.dark .dropdown-trigger:hover{background-color:var(--color-gray-700)}}.dark .dropdown-trigger:focus{--tw-ring-color:var(--color-blue-400)}.dropdown-trigger-text{cursor:pointer;--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900);align-items:center;display:inline-flex}@media(hover:hover){.dropdown-trigger-text:hover{color:var(--color-blue-600)}}.dark .dropdown-trigger-text{color:var(--color-white)}@media(hover:hover){.dark .dropdown-trigger-text:hover{color:var(--color-blue-400)}}.dropdown-menu{z-index:50;width:calc(var(--spacing) * 56);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .dropdown-menu{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.dropdown-menu-bottom-left{margin-top:calc(var(--spacing) * 2);top:100%;left:0}.dropdown-menu-bottom-right{margin-top:calc(var(--spacing) * 2);top:100%;right:0}.dropdown-menu-top-left{margin-bottom:calc(var(--spacing) * 2);bottom:100%;left:0}.dropdown-menu-top-right{margin-bottom:calc(var(--spacing) * 2);bottom:100%;right:0}.dropdown-item{cursor:pointer;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}@media(hover:hover){.dropdown-item:hover{background-color:var(--color-gray-100)}}.dark .dropdown-item{color:var(--color-gray-300)}@media(hover:hover){.dark .dropdown-item:hover{background-color:var(--color-gray-700)}}.dropdown-item-icon{color:var(--color-gray-400)}.dark .dropdown-item-icon{color:var(--color-gray-500)}.dropdown-item-disabled{cursor:not-allowed;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-400)}.dark .dropdown-item-disabled{color:var(--color-gray-500)}.dropdown-item-focused{background-color:var(--color-gray-100)}.dark .dropdown-item-focused{background-color:var(--color-gray-700)}.dropdown-divider{margin-block:var(--spacing);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100)}.dark .dropdown-divider{border-color:var(--color-gray-700)}.pagination{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.pagination-btn{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);color:var(--color-gray-700);border-radius:.25rem}@media(hover:hover){.pagination-btn:hover{background-color:var(--color-gray-100)}}.pagination-btn:disabled{cursor:not-allowed;opacity:.5}.dark .pagination-btn{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-300)}@media(hover:hover){.dark .pagination-btn:hover{background-color:var(--color-gray-700)}}.pagination-btn-active{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--color-blue-500);background-color:var(--color-blue-500);color:var(--color-white);border-radius:.25rem}.dark .pagination-btn-active{border-color:var(--color-blue-600);background-color:var(--color-blue-600)}.pagination-ellipsis{padding-inline:calc(var(--spacing) * 3);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .pagination-ellipsis{color:var(--color-gray-400)}.dialog-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.dialog-title{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .dialog-title{color:var(--color-white)}.dialog-close{color:var(--color-gray-400)}@media(hover:hover){.dialog-close:hover{color:var(--color-gray-600)}}.dialog-close:focus{--tw-outline-style:none;outline-style:none}.dark .dialog-close{color:var(--color-gray-500)}@media(hover:hover){.dark .dialog-close:hover{color:var(--color-gray-300)}}.dialog-body{margin-bottom:calc(var(--spacing) * 6);color:var(--color-gray-700)}.dark .dialog-body{color:var(--color-gray-300)}.dialog-footer{justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.dialog-btn-primary{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-primary:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-primary:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-primary{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.dialog-btn-primary:hover{background-color:var(--color-blue-700)}}.dialog-btn-primary:focus{--tw-ring-color:var(--color-blue-500)}.dialog-btn-secondary{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-secondary:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-secondary:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-secondary{background-color:var(--color-gray-200);color:var(--color-gray-800)}@media(hover:hover){.dialog-btn-secondary:hover{background-color:var(--color-gray-300)}}.dialog-btn-secondary:focus{--tw-ring-color:var(--color-gray-500)}.dark .dialog-btn-secondary{background-color:var(--color-gray-700);color:var(--color-gray-200)}@media(hover:hover){.dark .dialog-btn-secondary:hover{background-color:var(--color-gray-600)}}.dialog-btn-danger{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-danger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-danger:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.dialog-btn-danger:hover{background-color:var(--color-red-700)}}.dialog-btn-danger:focus{--tw-ring-color:var(--color-red-500)}.dialog-btn-ghost{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-ghost:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-ghost:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-ghost{color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.dialog-btn-ghost:hover{background-color:var(--color-gray-100)}}.dialog-btn-ghost:focus{--tw-ring-color:var(--color-gray-500)}.dark .dialog-btn-ghost{color:var(--color-gray-300)}@media(hover:hover){.dark .dialog-btn-ghost:hover{background-color:var(--color-gray-700)}}.alert-error{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-900);display:flex}.dark .alert-error{border-color:var(--color-red-800);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-error{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .alert-error{color:var(--color-red-100)}.alert-icon-info{margin-top:calc(var(--spacing) * .5);color:var(--color-blue-600);flex-shrink:0}.dark .alert-icon-info{color:var(--color-blue-400)}.alert-icon-success{margin-top:calc(var(--spacing) * .5);color:var(--color-green-600);flex-shrink:0}.dark .alert-icon-success{color:var(--color-green-400)}.alert-icon-warning{margin-top:calc(var(--spacing) * .5);color:var(--color-yellow-600);flex-shrink:0}.dark .alert-icon-warning{color:var(--color-yellow-400)}.alert-icon-error{margin-top:calc(var(--spacing) * .5);color:var(--color-red-600);flex-shrink:0}.dark .alert-icon-error{color:var(--color-red-400)}.alert-text-info{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-800);flex:1}.dark .alert-text-info{color:var(--color-blue-200)}.alert-text-success{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-800);flex:1}.dark .alert-text-success{color:var(--color-green-200)}.alert-text-warning{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-800);flex:1}.dark .alert-text-warning{color:var(--color-yellow-200)}.alert-text-error{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-800);flex:1}.dark .alert-text-error{color:var(--color-red-200)}.alert-dismiss{cursor:pointer;border-style:var(--tw-border-style);background-color:#0000;border-width:0;flex-shrink:0;padding:0}@media(hover:hover){.alert-dismiss:hover{opacity:.7}}.table-container{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);overflow:hidden}.dark .table-container{border-color:var(--color-gray-700)}.table{width:100%}.table thead{background-color:var(--color-gray-50)}.dark :is(.table thead){background-color:var(--color-gray-800)}.table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600)}.dark :is(.table th){border-color:var(--color-gray-700);color:var(--color-gray-400)}.table th.sortable{cursor:pointer}@media(hover:hover){.table th.sortable:hover{background-color:var(--color-gray-100)}.dark :is(.table th.sortable):hover{background-color:var(--color-gray-700)}}.table tbody tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);background-color:var(--color-white)}.dark :is(.table tbody tr){border-color:var(--color-gray-800);background-color:var(--color-gray-900)}.table tbody tr:hover{background-color:var(--color-gray-50)}.dark :is(.table tbody tr:hover){background-color:var(--color-gray-800)}.table tbody tr.selected{background-color:var(--color-blue-50)}.dark :is(.table tbody tr.selected){background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark :is(.table tbody tr.selected){background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.table td{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark :is(.table td){color:var(--color-gray-100)}.table-actions{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);justify-content:space-between;align-items:center;display:flex}.dark .table-actions{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.log-table{width:100%}.log-table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500)}.dark :is(.log-table th){border-color:var(--color-gray-700);color:var(--color-gray-400)}.log-table td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark :is(.log-table td){border-color:var(--color-gray-800);color:var(--color-gray-100)}.log-table tbody tr:last-child td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.log-table-actor{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);align-items:center;display:inline-flex}.dark .log-table-actor{border-color:var(--color-gray-600);background-color:var(--color-gray-700);color:var(--color-gray-300)}.log-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .log-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.log-card-header{margin-bottom:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.log-card-body{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .log-card-body{color:var(--color-gray-100)}.modal-overlay{z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}@supports (color:color-mix(in lab,red,red)){.modal-overlay{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.modal-wrapper{justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.modal-backdrop{background-color:#00000080;position:absolute;inset:0}@supports (color:color-mix(in lab,red,red)){.modal-backdrop{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.modal-container{border-radius:var(--radius-lg);background-color:var(--color-white);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);max-width:calc(100vw - 2rem);max-height:90vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:relative;overflow:hidden}.dark .modal-container{background-color:var(--color-gray-800)}.modal-header-row{padding:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.modal-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .modal-title{color:var(--color-white)}.modal-close-btn{color:var(--color-gray-400);margin-left:auto}@media(hover:hover){.modal-close-btn:hover{color:var(--color-gray-600)}}.modal-close-btn:focus{--tw-outline-style:none;outline-style:none}.dark .modal-close-btn{color:var(--color-gray-500)}@media(hover:hover){.dark .modal-close-btn:hover{color:var(--color-gray-300)}}.modal-body-content{max-height:calc(90vh - 140px);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);overflow-y:auto}.modal{margin-inline:calc(var(--spacing) * 4);width:100%;max-width:var(--container-md);border-radius:var(--radius-xl);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .modal{background-color:var(--color-gray-800)}.modal-lg{max-width:var(--container-lg)}.modal-xl{max-width:var(--container-xl)}.modal-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:flex-start;display:flex}.modal-close{cursor:pointer;border-radius:var(--radius-lg);padding:var(--spacing);color:var(--color-gray-400)}@media(hover:hover){.modal-close:hover{background-color:var(--color-gray-100);color:var(--color-gray-600)}}.dark .modal-close{color:var(--color-gray-500)}@media(hover:hover){.dark .modal-close:hover{background-color:var(--color-gray-700);color:var(--color-gray-300)}}.modal-body{padding-block:calc(var(--spacing) * 4)}.modal-footer{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.danger-confirm-icon{margin-inline:auto;margin-bottom:calc(var(--spacing) * 4);height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);background-color:var(--color-red-100);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.dark .danger-confirm-icon{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .danger-confirm-icon{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.danger-confirm-icon svg,.danger-confirm-icon .icon{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--color-red-600)}.dark :is(.danger-confirm-icon svg,.danger-confirm-icon .icon){color:var(--color-red-400)}.danger-confirm-title{margin-bottom:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .danger-confirm-title{color:var(--color-white)}.danger-confirm-message{margin-bottom:calc(var(--spacing) * 4);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .danger-confirm-message{color:var(--color-gray-400)}.danger-confirm-warning{margin-bottom:calc(var(--spacing) * 4);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-50);padding:calc(var(--spacing) * 4)}.dark .danger-confirm-warning{border-color:var(--color-yellow-700);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .danger-confirm-warning{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.danger-confirm-warning-title{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-yellow-800)}.dark .danger-confirm-warning-title{color:var(--color-yellow-300)}.danger-confirm-warning-list{list-style-type:disc;list-style-position:inside}:where(.danger-confirm-warning-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.danger-confirm-warning-list{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-700)}.dark .danger-confirm-warning-list{color:var(--color-yellow-400)}.danger-confirm-alert{text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-600)}.dark .danger-confirm-alert{color:var(--color-red-400)}.danger-confirm-footer{margin-top:calc(var(--spacing) * 6);justify-content:center;gap:calc(var(--spacing) * 3);display:flex}.card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);width:100%;padding:calc(var(--spacing) * 6)}.dark .card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card{margin-bottom:calc(var(--spacing) * 6);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .admin-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card{flex-direction:column;display:flex}.admin-card-clickable{margin-bottom:calc(var(--spacing) * 2);cursor:pointer;border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5)}@media(hover:hover){.admin-card-clickable:hover{border-color:var(--color-gray-300)}}.dark .admin-card-clickable{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}@media(hover:hover){.dark .admin-card-clickable:hover{border-color:var(--color-gray-600)}}.admin-card-selected{margin-bottom:calc(var(--spacing) * 2);cursor:pointer;border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-500);background-color:var(--color-blue-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5)}.dark .admin-card-selected{border-color:var(--color-blue-500);background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .admin-card-selected{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.card-clickable{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));overflow:hidden}@media(hover:hover){.card-clickable:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .card-clickable{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.card-image{height:calc(var(--spacing) * 48);object-fit:cover;width:100%}.card-content{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:pre-line;color:var(--color-gray-600)}.dark .card-content{color:var(--color-gray-300)}.card-section-title{margin-inline:calc(var(--spacing) * -6);margin-bottom:calc(var(--spacing) * 6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 4);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .card-section-title{border-color:var(--color-gray-700);color:var(--color-white)}.card-account{border-radius:var(--radius-lg);background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4);position:relative}.dark .card-account{background-color:var(--color-gray-900)}.card-account-default{top:calc(var(--spacing) * 3);left:calc(var(--spacing) * 3);position:absolute}.card-account-delete{top:calc(var(--spacing) * 3);right:calc(var(--spacing) * 3);position:absolute}.admin-card-accordion{margin-bottom:calc(var(--spacing) * 6);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .admin-card-accordion{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card-accordion{flex-direction:column;display:flex}.admin-card-accordion>.card-header{cursor:pointer}.admin-card-accordion-icon{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;margin-left:auto;transition-duration:.2s}.dark .admin-card-accordion-icon{color:var(--color-gray-500)}.admin-card-accordion>.row-stack{margin-top:calc(var(--spacing) * 6)}.admin-card-accordion-icon.open{rotate:180deg}.tabs{border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding:var(--spacing);display:inline-flex}.dark .tabs{background-color:var(--color-gray-800)}.tab{cursor:pointer;border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.tab:hover{color:var(--color-gray-700)}}.dark .tab{color:var(--color-gray-400)}@media(hover:hover){.dark .tab:hover{color:var(--color-gray-300)}}.tab.active{background-color:var(--color-white);color:var(--color-gray-900);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .tab.active{background-color:var(--color-gray-700);color:var(--color-white)}.toast{right:calc(var(--spacing) * 4);bottom:calc(var(--spacing) * 4);z-index:50;position:fixed}.toast-item{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);background-color:var(--color-gray-900);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);display:flex}.dark .toast-item{background-color:var(--color-white);color:var(--color-gray-900)}.toast-icon{flex-shrink:0}.drag-item{align-items:center;gap:calc(var(--spacing) * 4);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4);display:flex}@media(hover:hover){.drag-item:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .drag-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.drag-handle{cursor:grab;color:var(--color-gray-400)}.dark .drag-handle{color:var(--color-gray-500)}.drag-handle:active{cursor:grabbing}.table-mobile-cards{display:none}.excel-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .excel-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.excel-card-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-bottom:calc(var(--spacing) * 3);display:flex}.dark .excel-card-header{border-color:var(--color-gray-700)}.excel-card-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .excel-card-title{color:var(--color-white)}:where(.excel-card-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.excel-card-row{padding-block:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));justify-content:space-between;align-items:center;display:flex}.excel-card-label{color:var(--color-gray-500);flex-shrink:0}.dark .excel-card-label{color:var(--color-gray-400)}.excel-card-value{text-align:right;color:var(--color-gray-900)}.dark .excel-card-value{color:var(--color-white)}.excel-card-footer{margin-top:calc(var(--spacing) * 4);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100);padding-top:calc(var(--spacing) * 3);justify-content:space-between;align-items:center;display:flex}.dark .excel-card-footer{border-color:var(--color-gray-700)}.excel-card-creator{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}.dark .excel-card-creator{color:var(--color-gray-300)}.excel-card-creator-label{margin-right:calc(var(--spacing) * 2);color:var(--color-gray-500)}.dark .excel-card-creator-label{color:var(--color-gray-400)}.select-custom{display:inline-block;position:relative}.select-trigger{height:calc(var(--spacing) * 9);border-radius:var(--radius-xl);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:0;display:flex}.select-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select-trigger{background-color:var(--color-gray-700);color:var(--color-gray-200)}.select-trigger:disabled{cursor:not-allowed;opacity:.5}.select-trigger span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.select-trigger-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;flex-shrink:0;transition-duration:.2s}.dark .select-trigger-icon{color:var(--color-gray-500)}.select-trigger-icon.open{rotate:180deg}.select-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute;overflow:hidden}.dark .select-dropdown{border-color:var(--color-gray-600);background-color:var(--color-gray-800)}.select-dropdown.hidden{display:none}.select-dropdown-inner{max-height:calc(var(--spacing) * 60);padding-block:calc(var(--spacing) * 2);overflow:auto}.select-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));align-items:center;display:flex}@media(hover:hover){.select-option:hover{background-color:var(--color-gray-100)}}.dark .select-option{color:var(--color-gray-200)}@media(hover:hover){.dark .select-option:hover{background-color:var(--color-gray-700)}}.select-option.selected{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-blue-600)}.dark .select-option.selected{color:var(--color-blue-400)}.select-option:disabled{cursor:not-allowed;opacity:.5}.select-option-check{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-blue-600);flex-shrink:0;margin-left:auto}.dark .select-option-check{color:var(--color-blue-400)}.multi-select{display:inline-block;position:relative}.multi-select-trigger{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);cursor:pointer;background-color:#0000;display:inline-flex}@media(hover:hover){.multi-select-trigger:hover{background-color:var(--color-gray-50)}}.dark .multi-select-trigger{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .multi-select-trigger:hover{background-color:var(--color-gray-700)}}.multi-select-count{color:var(--color-gray-500)}.dark .multi-select-count{color:var(--color-gray-400)}.multi-select-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);min-width:calc(var(--spacing) * 64);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .multi-select-dropdown{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.multi-select-dropdown.hidden{display:none}.multi-select-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .multi-select-header{border-color:var(--color-gray-700);color:var(--color-white)}.multi-select-hint{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--color-gray-500)}.dark .multi-select-hint{color:var(--color-gray-400)}.multi-select-items{max-height:calc(var(--spacing) * 80);padding-block:calc(var(--spacing) * 2);overflow:auto}.multi-select-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);display:flex}@media(hover:hover){.multi-select-item:hover{background-color:var(--color-gray-50)}}.dark .multi-select-item{color:var(--color-gray-300)}@media(hover:hover){.dark .multi-select-item:hover{background-color:var(--color-gray-700)}}.multi-select-item.disabled{cursor:not-allowed;opacity:.5}.multi-select-item.disabled:hover,.dark .multi-select-item.disabled:hover{background-color:#0000}.select-ghost{display:inline-block;position:relative}.select-ghost-trigger{align-items:center;gap:var(--spacing);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);cursor:pointer;border-radius:var(--radius-lg);color:var(--color-gray-700);background-color:#0000;display:inline-flex}@media(hover:hover){.select-ghost-trigger:hover{background-color:var(--color-gray-100)}}.dark .select-ghost-trigger{color:var(--color-gray-300)}@media(hover:hover){.dark .select-ghost-trigger:hover{background-color:var(--color-gray-700)}}.select-ghost-dropdown{z-index:50;margin-top:var(--spacing);min-width:calc(var(--spacing) * 32);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-block:var(--spacing);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .select-ghost-dropdown{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.select-ghost-dropdown.hidden{display:none}.select-ghost-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);justify-content:space-between;align-items:center;display:flex}@media(hover:hover){.select-ghost-option:hover{background-color:var(--color-gray-50)}}.dark .select-ghost-option{color:var(--color-gray-300)}@media(hover:hover){.dark .select-ghost-option:hover{background-color:var(--color-gray-700)}}.select-ghost-option.selected{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-blue-600)}.dark .select-ghost-option.selected{color:var(--color-blue-400)}.select-ghost-check{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-blue-600)}.dark .select-ghost-check{color:var(--color-blue-400)}.searchable-dropdown{display:inline-block;position:relative}.searchable-dropdown-trigger{border-radius:var(--radius-xl);background-color:var(--color-gray-100);min-width:200px;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:0;display:flex}.searchable-dropdown-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .searchable-dropdown-trigger{background-color:var(--color-gray-700);color:var(--color-gray-200)}.searchable-dropdown-trigger:disabled{cursor:not-allowed;opacity:.5}.searchable-dropdown-trigger-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;flex-shrink:0;transition-duration:.2s}.dark .searchable-dropdown-trigger-icon{color:var(--color-gray-500)}.searchable-dropdown-trigger-icon.open{rotate:180deg}.searchable-dropdown-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:100%;min-width:280px;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute;overflow:hidden}.dark .searchable-dropdown-dropdown{border-color:var(--color-gray-600);background-color:var(--color-gray-800)}.searchable-dropdown-dropdown.hidden{display:none}.searchable-dropdown-search{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding:calc(var(--spacing) * 3)}.dark .searchable-dropdown-search{border-color:var(--color-gray-700)}.searchable-dropdown-search-input{border-radius:var(--radius-lg);background-color:var(--color-gray-50);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.searchable-dropdown-search-input::placeholder{color:var(--color-gray-400)}.searchable-dropdown-search-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;border-color:#0000;outline-style:none}.dark .searchable-dropdown-search-input{border-color:var(--color-gray-600);background-color:var(--color-gray-700);color:var(--color-white)}.dark .searchable-dropdown-search-input::placeholder{color:var(--color-gray-500)}.searchable-dropdown-items{max-height:calc(var(--spacing) * 80);padding-block:calc(var(--spacing) * 2);overflow:auto}.searchable-dropdown-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;align-items:flex-start;gap:calc(var(--spacing) * 3);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media(hover:hover){.searchable-dropdown-option:hover{background-color:var(--color-gray-100)}.dark .searchable-dropdown-option:hover{background-color:var(--color-gray-700)}}.searchable-dropdown-option:disabled{cursor:not-allowed;opacity:.5}.searchable-dropdown-check-area{margin-top:calc(var(--spacing) * .5);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);flex-shrink:0}.searchable-dropdown-check{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-blue-600)}.dark .searchable-dropdown-check{color:var(--color-blue-400)}.searchable-dropdown-option-content{flex:1;min-width:0}.searchable-dropdown-option-label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .searchable-dropdown-option-label{color:var(--color-blue-400)}.searchable-dropdown-option.selected .searchable-dropdown-option-label{color:var(--color-blue-600)}.dark :is(.searchable-dropdown-option.selected .searchable-dropdown-option-label){color:var(--color-blue-400)}.searchable-dropdown-option-description{margin-top:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .searchable-dropdown-option-description{color:var(--color-gray-400)}.searchable-dropdown-message{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 6);text-align:center;color:var(--color-gray-500)}.dark .searchable-dropdown-message{color:var(--color-gray-400)}.stat-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.stat-card:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .stat-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.stats-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 3)}.stats-icon-blue{background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .stats-icon-blue{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-blue{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .stats-icon-blue{color:var(--color-blue-400)}.stats-icon-green{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .stats-icon-green{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-green{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .stats-icon-green{color:var(--color-green-400)}.stats-icon-purple{background-color:var(--color-purple-50);color:var(--color-purple-600)}.dark .stats-icon-purple{background-color:#59168b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-purple{background-color:color-mix(in oklab,var(--color-purple-900) 20%,transparent)}}.dark .stats-icon-purple{color:var(--color-purple-400)}.stats-icon-orange{background-color:var(--color-orange-50);color:var(--color-orange-600)}.dark .stats-icon-orange{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-orange{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .stats-icon-orange{color:var(--color-orange-400)}.stats-change{align-items:center;gap:var(--spacing);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 2);padding-block:var(--spacing);display:flex}.stats-trend-up{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .stats-trend-up{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-trend-up{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .stats-trend-up{color:var(--color-green-400)}.stats-trend-down{background-color:var(--color-red-50);color:var(--color-red-600)}.dark .stats-trend-down{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .stats-trend-down{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .stats-trend-down{color:var(--color-red-400)}.stats-trend-neutral{background-color:var(--color-gray-50);color:var(--color-gray-600)}.dark .stats-trend-neutral{background-color:var(--color-gray-700);color:var(--color-gray-400)}.stats-value-lg{margin-bottom:var(--spacing);font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-gray-900)}.dark .stats-value-lg{color:var(--color-white)}.stats-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .stats-label{color:var(--color-gray-400)}.stats-sublabel{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500);display:flex}.dark .stats-sublabel{color:var(--color-gray-400)}:where(.tree>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}.tree-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);display:flex}@media(hover:hover){.tree-item:hover{background-color:var(--color-gray-50)}.dark .tree-item:hover{background-color:var(--color-gray-800)}}.tree-item.selected{background-color:var(--color-blue-50)}.dark .tree-item.selected{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tree-item.selected{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.tree-item-content{align-items:center;gap:calc(var(--spacing) * 2);flex:1;min-width:0;display:flex}.tree-item-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);flex-shrink:0}.dark .tree-item-icon{color:var(--color-gray-500)}.tree-item-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);overflow:hidden}.dark .tree-item-label{color:var(--color-gray-300)}.tree-expand-btn{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--color-gray-400);border-radius:.25rem;justify-content:center;align-items:center;display:flex}@media(hover:hover){.tree-expand-btn:hover{background-color:var(--color-gray-100)}}.dark .tree-expand-btn{color:var(--color-gray-500)}@media(hover:hover){.dark .tree-expand-btn:hover{background-color:var(--color-gray-700)}}.tree-expand-btn.hidden{visibility:hidden}.tree-children{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-children{border-color:var(--color-gray-700)}.tree-level-1{border-left-style:var(--tw-border-style);border-left-width:0;margin-left:0;padding-left:0}.tree-level-2{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-2{border-color:var(--color-gray-700)}.tree-level-3{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-3{border-color:var(--color-gray-700)}.tree-level-4{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-4{border-color:var(--color-gray-700)}.category-item{cursor:pointer;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);justify-content:space-between;align-items:center;display:flex}.dark .category-item{color:var(--color-white)}@media(hover:hover){.category-item:hover{background-color:var(--color-gray-50)}.dark .category-item:hover{background-color:var(--color-gray-700)}}.category-item[data-selected=true]{background-color:var(--color-blue-600);color:var(--color-white)}.category-item[data-selected=true]:hover{background-color:var(--color-blue-700)}.category-item-mobile{cursor:pointer;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .category-item-mobile{color:var(--color-white)}.category-item-mobile[data-selected=true]{background-color:var(--color-blue-50)}.dark .category-item-mobile[data-selected=true]{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .category-item-mobile[data-selected=true]{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.stats-card{height:calc(var(--spacing) * 36);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-slate-100);background-color:var(--color-white);padding:calc(var(--spacing) * 5);flex-direction:column;justify-content:space-between;display:flex}.dark .stats-card{border-color:var(--color-slate-700);background-color:var(--color-slate-800)}.dashboard-panel{border-radius:var(--radius-3xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-slate-100);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dashboard-panel{border-color:var(--color-slate-700);background-color:var(--color-slate-800)}.stats-value{margin-bottom:calc(var(--spacing) * 2);--tw-leading:1;--tw-font-weight:var(--font-weight-bold);font-size:2rem;line-height:1;font-weight:var(--font-weight-bold);color:var(--color-slate-900)}.dark .stats-value{color:var(--color-white)}.stats-trend{align-items:center;gap:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-emerald-500);display:flex}.status-active{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-green-100);color:var(--color-green-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-active{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .status-active{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .status-active{color:var(--color-green-400)}.status-inactive{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-gray-100);color:var(--color-gray-600);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-inactive{background-color:var(--color-gray-700);color:var(--color-gray-400)}.status-pending{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-yellow-100);color:var(--color-yellow-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-pending{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .status-pending{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .status-pending{color:var(--color-yellow-400)}.status-error{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-red-100);color:var(--color-red-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-error{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .status-error{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .status-error{color:var(--color-red-400)}.list-item{align-items:center;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-block:calc(var(--spacing) * 3);display:flex}.dark .list-item{border-color:var(--color-gray-700)}.list-item:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.list-item-hover{margin-inline:calc(var(--spacing) * -3);align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);display:flex}@media(hover:hover){.list-item-hover:hover{background-color:var(--color-gray-50)}.dark .list-item-hover:hover{background-color:var(--color-gray-800)}}.empty-state{padding-block:calc(var(--spacing) * 12);text-align:center;flex-direction:column;justify-content:center;align-items:center;display:flex}.empty-state-icon{margin-bottom:calc(var(--spacing) * 4);height:calc(var(--spacing) * 16);width:calc(var(--spacing) * 16);color:var(--color-gray-300)}.dark .empty-state-icon{color:var(--color-gray-600)}.empty-state-title{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .empty-state-title{color:var(--color-white)}.empty-state-description{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .empty-state-description{color:var(--color-gray-400)}.page-header{margin-bottom:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.page-header-padded{margin-bottom:calc(var(--spacing) * 6);padding-inline:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.page-header-fullbleed{margin-inline:calc(var(--spacing) * -6);margin-bottom:calc(var(--spacing) * 6);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.dark .page-header-fullbleed{background-color:var(--color-gray-900)}.page-actions{align-items:center;gap:calc(var(--spacing) * 3);display:flex}.section-card{margin-top:calc(var(--spacing) * 6);scroll-margin-top:calc(var(--spacing) * 32);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6)}.dark .section-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.section-header{margin-bottom:calc(var(--spacing) * 6);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-header{color:var(--color-white)}.info-row{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);display:flex}.action-buttons{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.form-label{margin-bottom:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900);display:block}.dark .form-label{color:var(--color-white)}.form-label-muted{margin-bottom:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500);display:block}.dark .form-label-muted{color:var(--color-gray-400)}.form-hint{margin-top:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .form-hint{color:var(--color-gray-400)}.text-value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .text-value{color:var(--color-white)}.text-link{cursor:pointer;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-600)}@media(hover:hover){.text-link:hover{color:var(--color-blue-700)}}.dark .text-link{color:var(--color-blue-400)}@media(hover:hover){.dark .text-link:hover{color:var(--color-blue-300)}}.icon-xs{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3)}.icon-sm{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.icon-md{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.icon-lg{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6)}.icon-xl{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8)}.icon-2xl{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10)}.icon-action{cursor:pointer;color:var(--color-gray-400)}@media(hover:hover){.icon-action:hover{color:var(--color-gray-600)}}.dark .icon-action{color:var(--color-gray-500)}@media(hover:hover){.dark .icon-action:hover{color:var(--color-gray-300)}}.page-container{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 6)}.dark .page-container{background-color:var(--color-gray-900)}.content-area{max-width:var(--container-7xl);margin-inline:auto}.content-panel{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6)}.dark .content-panel{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.flex-row-center{align-items:center;display:flex}.flex-row-between{justify-content:space-between;align-items:center;display:flex}.flex-row-end{justify-content:flex-end;align-items:center;display:flex}.flex-col-start{flex-direction:column;align-items:flex-start;display:flex}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-center{align-items:center;display:flex}.flex-end{justify-content:flex-end;align-items:center;display:flex}.flex-wrap-center{flex-wrap:wrap;align-items:center;display:flex}.inline-flex-center{align-items:center;display:inline-flex}.row-stack{flex-direction:column;display:flex}:where(.row-stack>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--color-gray-200)}:where(.dark .row-stack>:not(:last-child)){border-color:var(--color-gray-700)}.row-stack>*{padding-block:calc(var(--spacing) * 2)}.row-stack>:first-child{padding-top:0}.row-stack>:last-child{padding-bottom:0}.stack-flush{flex-direction:column;gap:0;display:flex}.stack-tight{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.card-stack{margin-top:calc(var(--spacing) * 3);gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.card-stack-item{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .card-stack-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.card-stack-item{flex-direction:column;display:flex}.option-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);overflow:hidden}.dark .option-card{border-color:var(--color-gray-700);background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .option-card{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.option-card>*{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.option-card>*+*{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200)}.dark :is(.option-card>*+*){border-color:var(--color-gray-700)}.alert-info{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-200);background-color:var(--color-blue-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-900);display:flex}.dark .alert-info{border-color:var(--color-blue-800);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .alert-info{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .alert-info{color:var(--color-blue-100)}.alert-warning{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-900);display:flex}.dark .alert-warning{border-color:var(--color-yellow-800);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-warning{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .alert-warning{color:var(--color-yellow-100)}.alert-danger{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-900);display:flex}.dark .alert-danger{border-color:var(--color-red-800);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-danger{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .alert-danger{color:var(--color-red-100)}.alert-success{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-200);background-color:var(--color-green-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-900);display:flex}.dark .alert-success{border-color:var(--color-green-800);background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .alert-success{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .alert-success{color:var(--color-green-100)}.alert-info:has(+.row-stack),.alert-warning:has(+.row-stack),.alert-danger:has(+.row-stack),.alert-success:has(+.row-stack),.alert-error:has(+.row-stack),.alert-info:has(+.toggle-group),.alert-warning:has(+.toggle-group),.alert-danger:has(+.toggle-group),.alert-success:has(+.toggle-group),.alert-error:has(+.toggle-group),.alert-info:has(+.grid-2col-responsive),.alert-warning:has(+.grid-2col-responsive),.alert-danger:has(+.grid-2col-responsive),.alert-success:has(+.grid-2col-responsive),.alert-error:has(+.grid-2col-responsive){margin-bottom:calc(var(--spacing) * 4)}.row-stack>.alert-info,.row-stack>.alert-warning,.row-stack>.alert-danger,.row-stack>.alert-success,.row-stack>.alert-error{padding-top:calc(var(--spacing) * 3);padding-bottom:calc(var(--spacing) * 3)}.row-stack>*+.alert-info,.row-stack>*+.alert-warning,.row-stack>*+.alert-danger,.row-stack>*+.alert-success,.row-stack>*+.alert-error{margin-top:calc(var(--spacing) * 3)}.sub-tab-nav{gap:var(--spacing);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 6);display:flex}@media(min-width:40rem){.sub-tab-nav{padding-inline:calc(var(--spacing) * 8)}}@media(min-width:64rem){.sub-tab-nav{padding-inline:calc(var(--spacing) * 10)}}.dark .sub-tab-nav{background-color:var(--color-gray-800)}.section-header{margin-bottom:calc(var(--spacing) * 6)}.section-header>h3{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark :is(.section-header>h3){color:var(--color-white)}.section-header>p{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark :is(.section-header>p){color:var(--color-gray-400)}.toggle-group{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);overflow:hidden}.dark .toggle-group{border-color:var(--color-gray-700)}.section-header-row{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);background-color:var(--color-gray-50);display:flex}.dark .section-header-row{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .section-header-row{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.section-header-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200)}.dark .section-header-row{border-color:var(--color-gray-700)}.section-header-row{cursor:pointer;-webkit-user-select:none;user-select:none}.list-row-hover{background-color:var(--color-white)}.dark .list-row-hover{background-color:var(--color-gray-800)}@media(hover:hover){.list-row-hover:hover{background-color:var(--color-gray-50)}.dark .list-row-hover:hover{background-color:var(--color-gray-700)}}.list-row-hover{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.text-hint{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-400)}.dark .text-hint{color:var(--color-gray-500)}.text-info-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-blue-700)}.dark .text-info-soft{color:var(--color-blue-300)}.text-warning-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-yellow-700)}.dark .text-warning-soft{color:var(--color-yellow-300)}.text-danger-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-700)}.dark .text-danger-soft{color:var(--color-red-300)}.text-success-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-green-700)}.dark .text-success-soft{color:var(--color-green-300)}.checkbox-aligned{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-gray-900);border-radius:.25rem}.checkbox-aligned:focus{--tw-ring-color:var(--color-gray-500)}.dark .checkbox-aligned{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.checkbox-aligned{margin-top:calc(var(--spacing) * .5);flex-shrink:0}:where(.divider-list>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--color-gray-200)}:where(.dark .divider-list>:not(:last-child)){border-color:var(--color-gray-700)}.modal-footer-buttons{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.spinner-sm{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);animation:var(--animate-spin)}.text-required{margin-left:var(--spacing);color:var(--color-red-500)}.dark .text-required{color:var(--color-red-400)}.form-label-block{margin-bottom:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);display:block}.dark .form-label-block{color:var(--color-gray-300)}.form-sub-label{margin-bottom:var(--spacing);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500);display:block}.dark .form-sub-label{color:var(--color-gray-400)}.text-field-hint{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--color-gray-500)}.dark .text-field-hint{color:var(--color-gray-400)}.section-heading-md{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-heading-md{color:var(--color-white)}.grid-3col-responsive{align-items:flex-start;gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media(min-width:64rem){.grid-3col-responsive{grid-template-columns:repeat(3,minmax(0,1fr))}}.text-error-soft{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-700)}.dark .text-error-soft{color:var(--color-red-300)}.text-error-strong{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-red-800)}.dark .text-error-strong{color:var(--color-red-200)}.row-content-indent{padding-left:calc(var(--spacing) * 6)}.flex-start{align-items:flex-start;display:flex}.inline-clickable{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);display:inline-flex}.grid-2col{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.empty-state-cell{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);text-align:center}.form-error-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-500)}.dark .form-error-xs{color:var(--color-red-400)}.text-page-heading{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-gray-900)}.dark .text-page-heading{color:var(--color-white)}.text-table-header{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-gray-500);text-transform:uppercase}.dark .text-table-header{color:var(--color-gray-400)}.panel-header-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.dark .panel-header-row{border-color:var(--color-gray-700)}.grid-2col-responsive{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media(min-width:48rem){.grid-2col-responsive{grid-template-columns:repeat(2,minmax(0,1fr))}}.panel-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-block:calc(var(--spacing) * 3)}.dark .panel-header{border-color:var(--color-gray-700)}.gap-sm{gap:calc(var(--spacing) * 2)}.gap-md{gap:calc(var(--spacing) * 4)}.gap-lg{gap:calc(var(--spacing) * 6)}.mb-sm{margin-bottom:calc(var(--spacing) * 2)}.mb-md{margin-bottom:calc(var(--spacing) * 4)}.mb-lg{margin-bottom:calc(var(--spacing) * 6)}.surface{background-color:var(--color-white)}.dark .surface{background-color:var(--color-gray-800)}.surface-alt{background-color:var(--color-gray-50)}.dark .surface-alt{background-color:var(--color-gray-900)}.card-border{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.dark .card-border{border-color:var(--color-gray-700)}.card-border-xl{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.dark .card-border-xl{border-color:var(--color-gray-700)}.border-light{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300)}.dark .border-light{border-color:var(--color-gray-600)}.disabled-state:disabled{cursor:not-allowed;opacity:.5}.focus-ring:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dark .focus-ring:focus{--tw-ring-offset-color:var(--color-gray-800)}.focus-ring-inset:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;--tw-ring-inset:inset;outline-style:none}@media(hover:hover){.hover-bg:hover{background-color:var(--color-gray-100)}.dark .hover-bg:hover{background-color:var(--color-gray-700)}.hover-bg-light:hover{background-color:var(--color-gray-50)}.dark .hover-bg-light:hover{background-color:var(--color-gray-800)}}.btn-base{border-radius:var(--radius-lg);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;justify-content:center;align-items:center;transition-duration:.2s;display:inline-flex}.btn-base:disabled{cursor:not-allowed;opacity:.5}.btn-size-sm{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.btn-size-md{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.btn-size-lg{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.badge-base{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-size-sm{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5)}.badge-size-md{padding-inline:calc(var(--spacing) * 2.5);padding-block:var(--spacing)}.shadow-card{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-dropdown{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.shadow-hover:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.page-title{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}@media(min-width:40rem){.page-title{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.dark .page-title{color:var(--color-white)}.page-description{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .page-description{color:var(--color-gray-400)}.page-action-primary{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-blue-600);color:var(--color-white);display:flex}@media(hover:hover){.page-action-primary:hover{background-color:var(--color-blue-700)}}.page-action-secondary{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-gray-200);color:var(--color-gray-900);display:flex}@media(hover:hover){.page-action-secondary:hover{background-color:var(--color-gray-300)}}.dark .page-action-secondary{background-color:var(--color-gray-700);color:var(--color-gray-200)}@media(hover:hover){.dark .page-action-secondary:hover{background-color:var(--color-gray-600)}}.page-action-danger{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-red-600);color:var(--color-white);display:flex}@media(hover:hover){.page-action-danger:hover{background-color:var(--color-red-700)}}.page-action-ghost{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--color-gray-500);display:flex}@media(hover:hover){.page-action-ghost:hover{background-color:var(--color-gray-100);color:var(--color-gray-700)}}.dark .page-action-ghost{color:var(--color-gray-400)}@media(hover:hover){.dark .page-action-ghost:hover{background-color:var(--color-gray-800);color:var(--color-gray-200)}}.tab-container{margin-bottom:calc(var(--spacing) * 6);gap:calc(var(--spacing) * 6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);display:flex}.dark .tab-container{border-color:var(--color-gray-700)}.tab-btn{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:var(--spacing);padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px;margin-bottom:-1px;display:flex}@media(hover:hover){.tab-btn:hover{color:var(--color-gray-900)}}.dark .tab-btn{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn:hover{color:var(--color-gray-200)}}.tab-btn-active{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:var(--spacing);padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-bottom-width:2px;border-color:var(--color-blue-600);color:var(--color-blue-600);margin-bottom:-1px;display:flex}.dark .tab-btn-active{color:var(--color-blue-400)}.tab-badge{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:var(--color-gray-100);color:var(--color-gray-600);border-radius:3.40282e38px}.dark .tab-badge{background-color:var(--color-gray-700);color:var(--color-gray-400)}.tab-badge-active{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:var(--color-blue-100);color:var(--color-blue-600);border-radius:3.40282e38px}.dark .tab-badge-active{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tab-badge-active{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .tab-badge-active{color:var(--color-blue-400)}.sticky-tab-nav{z-index:40;margin-inline:calc(var(--spacing) * -6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 6);position:sticky;top:0}.dark .sticky-tab-nav{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-tab-nav-responsive{z-index:40;margin-inline:calc(var(--spacing) * -4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 4);position:sticky;top:0}@media(min-width:40rem){.sticky-tab-nav-responsive{margin-inline:calc(var(--spacing) * -6);padding-inline:calc(var(--spacing) * 6)}}@media(min-width:64rem){.sticky-tab-nav-responsive{margin-inline:calc(var(--spacing) * -8);padding-inline:calc(var(--spacing) * 8)}}.dark .sticky-tab-nav-responsive{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-section-header{z-index:40;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);position:sticky;top:0}.dark .sticky-section-header{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-footer-buttons{z-index:10;margin-top:calc(var(--spacing) * 6);justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);display:flex;position:sticky;bottom:0}.dark .sticky-footer-buttons{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.sticky-footer-buttons-between{z-index:10;margin-top:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);display:flex;position:sticky;bottom:0}.dark .sticky-footer-buttons-between{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.footer-buttons-between{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex}.dark .footer-buttons-between{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.tab-content{padding-top:calc(var(--spacing) * 6)}.admin-page-content{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 6)}.dark .admin-page-content{background-color:var(--color-gray-900)}.admin-page-content-compact{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 4)}.dark .admin-page-content-compact{background-color:var(--color-gray-900)}.admin-page-content-responsive{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 4)}@media(min-width:40rem){.admin-page-content-responsive{padding:calc(var(--spacing) * 6)}}@media(min-width:64rem){.admin-page-content-responsive{padding:calc(var(--spacing) * 8)}}.dark .admin-page-content-responsive{background-color:var(--color-gray-900)}.admin-page-content-responsive-fluid{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4)}@media(min-width:40rem){.admin-page-content-responsive-fluid{padding:calc(var(--spacing) * 6)}}@media(min-width:64rem){.admin-page-content-responsive-fluid{padding:calc(var(--spacing) * 8)}}.dark .admin-page-content-responsive-fluid{background-color:var(--color-gray-900)}.admin-page-content-fluid{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 6)}.dark .admin-page-content-fluid{background-color:var(--color-gray-900)}.admin-page-content-fluid-compact{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4)}.dark .admin-page-content-fluid-compact{background-color:var(--color-gray-900)}.admin-page-content-viewport{background-color:var(--color-gray-50);height:calc(100vh - 64px);padding:calc(var(--spacing) * 6);overflow:hidden}.dark .admin-page-content-viewport{background-color:var(--color-gray-900)}.admin-page-content-viewport-compact{background-color:var(--color-gray-50);height:calc(100vh - 64px);padding:calc(var(--spacing) * 4);overflow:hidden}.dark .admin-page-content-viewport-compact{background-color:var(--color-gray-900)}.bulk-actions-bar{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);padding:calc(var(--spacing) * 3);display:flex}.dark .bulk-actions-bar{border-color:var(--color-gray-600);background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .bulk-actions-bar{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}:where(.admin-sidebar-menu-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}.admin-sidebar-menu-list{padding-inline:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 4)}.admin-sidebar-item{border-radius:var(--radius-lg);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;display:flex}.admin-sidebar-subitem{border-radius:var(--radius-lg);width:100%;padding-block:6px;padding-right:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-size:13px;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;padding-left:30px;display:flex}.admin-sidebar-item-active{background-color:var(--color-slate-100);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-900)}.dark .admin-sidebar-item-active{background-color:var(--color-slate-700);color:var(--color-white)}.admin-sidebar-item-active-parent{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-900)}.dark .admin-sidebar-item-active-parent{color:var(--color-white)}.admin-sidebar-item-default{color:var(--color-slate-500)}@media(hover:hover){.admin-sidebar-item-default:hover{background-color:var(--color-slate-50);color:var(--color-slate-900)}}.dark .admin-sidebar-item-default{color:var(--color-slate-400)}@media(hover:hover){.dark .admin-sidebar-item-default:hover{background-color:var(--color-slate-800);color:var(--color-white)}}.admin-sidebar-item-disabled{cursor:default;color:var(--color-slate-400)}.dark .admin-sidebar-item-disabled{color:var(--color-slate-500)}.admin-sidebar-icon-box{flex-shrink:0;justify-content:center;align-items:center;width:18px;height:18px;display:flex}.admin-sidebar-chevron-trail{align-items:center;gap:var(--spacing);display:flex}.admin-sidebar-item-content{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.admin-sidebar-module-icon{margin-right:calc(var(--spacing) * 2);height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);color:var(--color-slate-400);opacity:.6}.dark .admin-sidebar-module-icon{color:var(--color-slate-500)}.admin-sidebar-tree-line-vertical{z-index:0;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-slate-300);position:absolute;top:0;bottom:0;left:12px}.dark .admin-sidebar-tree-line-vertical{border-color:var(--color-slate-600)}.admin-sidebar-tree-line-curve{z-index:10;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-left-style:var(--tw-border-style);width:14px;height:50%;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-left-width:1px;border-color:var(--color-slate-300);border-bottom-left-radius:10px;position:absolute;top:0;left:12px}@media(hover:hover){.admin-sidebar-tree-line-curve:is(:where(.group):hover *){border-color:var(--color-blue-400)}}.dark .admin-sidebar-tree-line-curve{border-color:var(--color-slate-600)}@media(hover:hover){.dark .admin-sidebar-tree-line-curve:is(:where(.group):hover *){border-color:var(--color-blue-500)}}.admin-sidebar-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-slate-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.dark .admin-sidebar-header{border-color:var(--color-slate-700)}.admin-sidebar-collapse-btn{border-radius:var(--radius-md);padding:calc(var(--spacing) * 2);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.admin-sidebar-collapse-btn:hover{background-color:var(--color-slate-100)}.dark .admin-sidebar-collapse-btn:hover{background-color:var(--color-slate-700)}}.tab-nav-default{gap:calc(var(--spacing) * 2);display:flex}.tab-nav-underline{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);gap:0;display:flex}.dark .tab-nav-underline{border-color:var(--color-gray-700)}.tab-btn-base{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.tab-btn-default{border-bottom-style:var(--tw-border-style);color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px}@media(hover:hover){.tab-btn-default:hover{background-color:var(--color-gray-50);color:var(--color-gray-900)}}.dark .tab-btn-default{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-default:hover{background-color:var(--color-gray-700);color:var(--color-gray-200)}}.tab-btn-default-active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-blue-600);background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .tab-btn-default-active{border-color:var(--color-blue-400);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tab-btn-default-active{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .tab-btn-default-active{color:var(--color-blue-400)}.tab-btn-underline{border-bottom-style:var(--tw-border-style);color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px}@media(hover:hover){.tab-btn-underline:hover{border-color:var(--color-gray-300);color:var(--color-gray-900)}}.dark .tab-btn-underline{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-underline:hover{border-color:var(--color-gray-600);color:var(--color-gray-200)}}.tab-btn-underline-active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-blue-600);color:var(--color-blue-600)}.dark .tab-btn-underline-active{border-color:var(--color-blue-400);color:var(--color-blue-400)}.tab-btn-pills{border-radius:var(--radius-lg);color:var(--color-gray-600)}@media(hover:hover){.tab-btn-pills:hover{background-color:var(--color-gray-100)}}.dark .tab-btn-pills{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-pills:hover{background-color:var(--color-gray-700)}}.tab-btn-pills-active{border-radius:var(--radius-lg);background-color:var(--color-blue-600);color:var(--color-white)}.dark .tab-btn-pills-active{background-color:var(--color-blue-500)}.tab-btn-disabled{cursor:not-allowed;color:var(--color-gray-400);opacity:.5}.dark .tab-btn-disabled{color:var(--color-gray-600)}.toggle-container{align-items:flex-start;column-gap:calc(var(--spacing) * 2);flex-wrap:wrap;width:auto;display:inline-flex}.toggle-switch-wrapper{flex-shrink:0;align-items:center;display:inline-flex;position:relative}.toggle-switch-track{background-color:var(--color-gray-200);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:relative}.toggle-switch-track:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-300);--tw-outline-style:none;outline-style:none}.toggle-switch-track:is(:where(.peer):checked~*){background-color:var(--color-blue-600)}.dark .toggle-switch-track{background-color:var(--color-gray-700)}.dark .toggle-switch-track:is(:where(.peer):focus~*){--tw-ring-color:var(--color-blue-800)}.dark .toggle-switch-track:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.toggle-switch-track:after{top:calc(var(--spacing) * .5);left:calc(var(--spacing) * .5);--tw-content:"";border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);content:var(--tw-content);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute}.dark .toggle-switch-track:after{content:var(--tw-content);border-color:var(--color-gray-600)}.toggle-switch-track:is(:where(.peer):checked~*):after{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);border-color:var(--color-white)}.toggle-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .toggle-label{color:var(--color-white)}.toggle-description{margin-top:var(--spacing);width:100%;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .toggle-description{color:var(--color-gray-400)}.admin-footer{margin-top:calc(var(--spacing) * 6);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.admin-footer-row{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}@media(min-width:48rem){.admin-footer-row{flex-direction:row}}.admin-footer-group{align-items:center;gap:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);display:flex}.dark .admin-footer-group{color:var(--color-gray-400)}.admin-footer-link-hover{cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.admin-footer-link-hover:hover{color:var(--color-blue-600)}.dark .admin-footer-link-hover:hover{color:var(--color-blue-400)}}.admin-footer-link{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media(hover:hover){.admin-footer-link:hover{color:var(--color-blue-600)}}.dark .admin-footer-link{color:var(--color-gray-400)}@media(hover:hover){.dark .admin-footer-link:hover{color:var(--color-blue-400)}}.responsive-grid{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}.grid-cols-1-lg-3{gap:calc(var(--spacing) * 6);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}.col-span-1-lg,.col-span-2-lg{grid-column:span 1/span 1}.modal-description{margin-top:var(--spacing);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .modal-description{color:var(--color-gray-400)}.toast-message{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.visible\!{visibility:visible!important}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:0}.inset-1{inset:var(--spacing)}.inset-2{inset:calc(var(--spacing) * 2)}.inset-3{inset:calc(var(--spacing) * 3)}.inset-4{inset:calc(var(--spacing) * 4)}.inset-5{inset:calc(var(--spacing) * 5)}.inset-6{inset:calc(var(--spacing) * 6)}.inset-8{inset:calc(var(--spacing) * 8)}.inset-auto{inset:auto}.inset-x-0{inset-inline:0}.inset-x-1{inset-inline:var(--spacing)}.inset-x-2{inset-inline:calc(var(--spacing) * 2)}.inset-x-3{inset-inline:calc(var(--spacing) * 3)}.inset-x-4{inset-inline:calc(var(--spacing) * 4)}.inset-x-auto{inset-inline:auto}.inset-y-0{inset-block:0}.inset-y-1{inset-block:var(--spacing)}.inset-y-2{inset-block:calc(var(--spacing) * 2)}.inset-y-3{inset-block:calc(var(--spacing) * 3)}.inset-y-4{inset-block:calc(var(--spacing) * 4)}.inset-y-auto{inset-block:auto}.-top-1{top:calc(var(--spacing) * -1)}.-top-2{top:calc(var(--spacing) * -2)}.-top-3{top:calc(var(--spacing) * -3)}.-top-4{top:calc(var(--spacing) * -4)}.top-0{top:0}.top-1{top:var(--spacing)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing) * 2)}.top-2\/3{top:66.6667%}.top-3{top:calc(var(--spacing) * 3)}.top-3\/4{top:75%}.top-4{top:calc(var(--spacing) * 4)}.top-5{top:calc(var(--spacing) * 5)}.top-6{top:calc(var(--spacing) * 6)}.top-8{top:calc(var(--spacing) * 8)}.top-10{top:calc(var(--spacing) * 10)}.top-12{top:calc(var(--spacing) * 12)}.top-16{top:calc(var(--spacing) * 16)}.top-auto{top:auto}.top-full{top:100%}.-right-1{right:calc(var(--spacing) * -1)}.-right-2{right:calc(var(--spacing) * -2)}.-right-3{right:calc(var(--spacing) * -3)}.-right-4{right:calc(var(--spacing) * -4)}.right-0{right:0}.right-1{right:var(--spacing)}.right-1\/2{right:50%}.right-2{right:calc(var(--spacing) * 2)}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.right-5{right:calc(var(--spacing) * 5)}.right-6{right:calc(var(--spacing) * 6)}.right-8{right:calc(var(--spacing) * 8)}.right-10{right:calc(var(--spacing) * 10)}.right-12{right:calc(var(--spacing) * 12)}.right-16{right:calc(var(--spacing) * 16)}.right-auto{right:auto}.right-full{right:100%}.-bottom-1{bottom:calc(var(--spacing) * -1)}.-bottom-2{bottom:calc(var(--spacing) * -2)}.-bottom-3{bottom:calc(var(--spacing) * -3)}.-bottom-4{bottom:calc(var(--spacing) * -4)}.bottom-0{bottom:0}.bottom-1{bottom:var(--spacing)}.bottom-1\/2{bottom:50%}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-4{bottom:calc(var(--spacing) * 4)}.bottom-5{bottom:calc(var(--spacing) * 5)}.bottom-6{bottom:calc(var(--spacing) * 6)}.bottom-8{bottom:calc(var(--spacing) * 8)}.bottom-10{bottom:calc(var(--spacing) * 10)}.bottom-12{bottom:calc(var(--spacing) * 12)}.bottom-16{bottom:calc(var(--spacing) * 16)}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.-left-1{left:calc(var(--spacing) * -1)}.-left-2{left:calc(var(--spacing) * -2)}.-left-3{left:calc(var(--spacing) * -3)}.-left-4{left:calc(var(--spacing) * -4)}.left-0{left:0}.left-1{left:var(--spacing)}.left-1\.5{left:calc(var(--spacing) * 1.5)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.left-5{left:calc(var(--spacing) * 5)}.left-6{left:calc(var(--spacing) * 6)}.left-8{left:calc(var(--spacing) * 8)}.left-10{left:calc(var(--spacing) * 10)}.left-12{left:calc(var(--spacing) * 12)}.left-16{left:calc(var(--spacing) * 16)}.left-auto{left:auto}.left-full{left:100%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[999\]{z-index:999}.z-\[9999\]{z-index:9999}.z-auto{z-index:auto}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-5{grid-column:span 5/span 5}.col-span-6{grid-column:span 6/span 6}.col-span-7{grid-column:span 7/span 7}.col-span-8{grid-column:span 8/span 8}.col-span-9{grid-column:span 9/span 9}.col-span-10{grid-column:span 10/span 10}.col-span-11{grid-column:span 11/span 11}.col-span-12{grid-column:span 12/span 12}.col-span-full{grid-column:1/-1}.col-start-1{grid-column-start:1}.col-start-2{grid-column-start:2}.col-start-3{grid-column-start:3}.col-start-4{grid-column-start:4}.col-start-5{grid-column-start:5}.col-start-6{grid-column-start:6}.col-start-7{grid-column-start:7}.col-start-8{grid-column-start:8}.col-start-9{grid-column-start:9}.col-start-10{grid-column-start:10}.col-start-11{grid-column-start:11}.col-start-12{grid-column-start:12}.col-start-13{grid-column-start:13}.col-end-1{grid-column-end:1}.col-end-2{grid-column-end:2}.col-end-3{grid-column-end:3}.col-end-4{grid-column-end:4}.col-end-5{grid-column-end:5}.col-end-6{grid-column-end:6}.col-end-7{grid-column-end:7}.col-end-8{grid-column-end:8}.col-end-9{grid-column-end:9}.col-end-10{grid-column-end:10}.col-end-11{grid-column-end:11}.col-end-12{grid-column-end:12}.col-end-13{grid-column-end:13}.row-span-1{grid-row:span 1/span 1}.row-span-2{grid-row:span 2/span 2}.row-span-3{grid-row:span 3/span 3}.row-span-4{grid-row:span 4/span 4}.row-span-5{grid-row:span 5/span 5}.row-span-6{grid-row:span 6/span 6}.row-span-full{grid-row:1/-1}.row-start-1{grid-row-start:1}.row-start-2{grid-row-start:2}.row-start-3{grid-row-start:3}.row-start-4{grid-row-start:4}.row-start-5{grid-row-start:5}.row-start-6{grid-row-start:6}.row-start-7{grid-row-start:7}.row-end-1{grid-row-end:1}.row-end-2{grid-row-end:2}.row-end-3{grid-row-end:3}.row-end-4{grid-row-end:4}.row-end-5{grid-row-end:5}.row-end-6{grid-row-end:6}.row-end-7{grid-row-end:7}.float-end{float:inline-end}.float-left{float:left}.float-none{float:none}.float-right{float:right}.float-start{float:inline-start}.clear-both{clear:both}.clear-end{clear:inline-end}.clear-left{clear:left}.clear-none{clear:none}.clear-right{clear:right}.clear-start{clear:inline-start}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:0!important}.\!m-1{margin:var(--spacing)!important}.\!m-2{margin:calc(var(--spacing) * 2)!important}.\!m-3{margin:calc(var(--spacing) * 3)!important}.\!m-4{margin:calc(var(--spacing) * 4)!important}.\!m-5{margin:calc(var(--spacing) * 5)!important}.\!m-6{margin:calc(var(--spacing) * 6)!important}.\!m-8{margin:calc(var(--spacing) * 8)!important}.\!m-10{margin:calc(var(--spacing) * 10)!important}.\!m-12{margin:calc(var(--spacing) * 12)!important}.\!m-16{margin:calc(var(--spacing) * 16)!important}.\!m-auto{margin:auto!important}.-m-1{margin:calc(var(--spacing) * -1)}.-m-2{margin:calc(var(--spacing) * -2)}.-m-3{margin:calc(var(--spacing) * -3)}.-m-4{margin:calc(var(--spacing) * -4)}.-m-5{margin:calc(var(--spacing) * -5)}.-m-6{margin:calc(var(--spacing) * -6)}.-m-8{margin:calc(var(--spacing) * -8)}.m-0{margin:0}.m-1{margin:var(--spacing)}.m-2{margin:calc(var(--spacing) * 2)}.m-3{margin:calc(var(--spacing) * 3)}.m-4{margin:calc(var(--spacing) * 4)}.m-5{margin:calc(var(--spacing) * 5)}.m-6{margin:calc(var(--spacing) * 6)}.m-8{margin:calc(var(--spacing) * 8)}.m-10{margin:calc(var(--spacing) * 10)}.m-12{margin:calc(var(--spacing) * 12)}.m-16{margin:calc(var(--spacing) * 16)}.m-auto{margin:auto}.\!mx-0{margin-inline:0!important}.\!mx-1{margin-inline:var(--spacing)!important}.\!mx-2{margin-inline:calc(var(--spacing) * 2)!important}.\!mx-3{margin-inline:calc(var(--spacing) * 3)!important}.\!mx-4{margin-inline:calc(var(--spacing) * 4)!important}.\!mx-5{margin-inline:calc(var(--spacing) * 5)!important}.\!mx-6{margin-inline:calc(var(--spacing) * 6)!important}.\!mx-8{margin-inline:calc(var(--spacing) * 8)!important}.\!mx-10{margin-inline:calc(var(--spacing) * 10)!important}.\!mx-12{margin-inline:calc(var(--spacing) * 12)!important}.\!mx-16{margin-inline:calc(var(--spacing) * 16)!important}.\!mx-auto{margin-inline:auto!important}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.-mx-3{margin-inline:calc(var(--spacing) * -3)}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.-mx-5{margin-inline:calc(var(--spacing) * -5)}.-mx-6{margin-inline:calc(var(--spacing) * -6)}.-mx-8{margin-inline:calc(var(--spacing) * -8)}.mx-0{margin-inline:0}.mx-1{margin-inline:var(--spacing)}.mx-1\.5{margin-inline:calc(var(--spacing) * 1.5)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mx-6{margin-inline:calc(var(--spacing) * 6)}.mx-8{margin-inline:calc(var(--spacing) * 8)}.mx-10{margin-inline:calc(var(--spacing) * 10)}.mx-12{margin-inline:calc(var(--spacing) * 12)}.mx-16{margin-inline:calc(var(--spacing) * 16)}.mx-auto{margin-inline:auto}.\!my-0{margin-block:0!important}.\!my-1{margin-block:var(--spacing)!important}.\!my-2{margin-block:calc(var(--spacing) * 2)!important}.\!my-3{margin-block:calc(var(--spacing) * 3)!important}.\!my-4{margin-block:calc(var(--spacing) * 4)!important}.\!my-5{margin-block:calc(var(--spacing) * 5)!important}.\!my-6{margin-block:calc(var(--spacing) * 6)!important}.\!my-8{margin-block:calc(var(--spacing) * 8)!important}.\!my-10{margin-block:calc(var(--spacing) * 10)!important}.\!my-12{margin-block:calc(var(--spacing) * 12)!important}.\!my-16{margin-block:calc(var(--spacing) * 16)!important}.\!my-auto{margin-block:auto!important}.-my-1{margin-block:calc(var(--spacing) * -1)}.-my-2{margin-block:calc(var(--spacing) * -2)}.-my-3{margin-block:calc(var(--spacing) * -3)}.-my-4{margin-block:calc(var(--spacing) * -4)}.-my-5{margin-block:calc(var(--spacing) * -5)}.-my-6{margin-block:calc(var(--spacing) * -6)}.-my-8{margin-block:calc(var(--spacing) * -8)}.my-0{margin-block:0}.my-1{margin-block:var(--spacing)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-5{margin-block:calc(var(--spacing) * 5)}.my-6{margin-block:calc(var(--spacing) * 6)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-10{margin-block:calc(var(--spacing) * 10)}.my-12{margin-block:calc(var(--spacing) * 12)}.my-16{margin-block:calc(var(--spacing) * 16)}.my-auto{margin-block:auto}.-mt-1{margin-top:calc(var(--spacing) * -1)}.-mt-2{margin-top:calc(var(--spacing) * -2)}.-mt-3{margin-top:calc(var(--spacing) * -3)}.-mt-4{margin-top:calc(var(--spacing) * -4)}.-mt-5{margin-top:calc(var(--spacing) * -5)}.-mt-6{margin-top:calc(var(--spacing) * -6)}.-mt-8{margin-top:calc(var(--spacing) * -8)}.mt-0{margin-top:0}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-auto{margin-top:auto}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.-mr-3{margin-right:calc(var(--spacing) * -3)}.-mr-4{margin-right:calc(var(--spacing) * -4)}.-mr-5{margin-right:calc(var(--spacing) * -5)}.-mr-6{margin-right:calc(var(--spacing) * -6)}.-mr-8{margin-right:calc(var(--spacing) * -8)}.mr-0{margin-right:0}.mr-1{margin-right:var(--spacing)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-3{margin-right:calc(var(--spacing) * 3)}.mr-4{margin-right:calc(var(--spacing) * 4)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mr-6{margin-right:calc(var(--spacing) * 6)}.mr-8{margin-right:calc(var(--spacing) * 8)}.mr-10{margin-right:calc(var(--spacing) * 10)}.mr-12{margin-right:calc(var(--spacing) * 12)}.mr-16{margin-right:calc(var(--spacing) * 16)}.mr-auto{margin-right:auto}.-mb-1{margin-bottom:calc(var(--spacing) * -1)}.-mb-2{margin-bottom:calc(var(--spacing) * -2)}.-mb-3{margin-bottom:calc(var(--spacing) * -3)}.-mb-4{margin-bottom:calc(var(--spacing) * -4)}.-mb-5{margin-bottom:calc(var(--spacing) * -5)}.-mb-6{margin-bottom:calc(var(--spacing) * -6)}.-mb-8{margin-bottom:calc(var(--spacing) * -8)}.-mb-px{margin-bottom:-1px}.mb-0{margin-bottom:0}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.mb-auto{margin-bottom:auto}.-ml-1{margin-left:calc(var(--spacing) * -1)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.-ml-3{margin-left:calc(var(--spacing) * -3)}.-ml-4{margin-left:calc(var(--spacing) * -4)}.-ml-5{margin-left:calc(var(--spacing) * -5)}.-ml-6{margin-left:calc(var(--spacing) * -6)}.-ml-8{margin-left:calc(var(--spacing) * -8)}.ml-0{margin-left:0}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-6{margin-left:calc(var(--spacing) * 6)}.ml-8{margin-left:calc(var(--spacing) * 8)}.ml-10{margin-left:calc(var(--spacing) * 10)}.ml-12{margin-left:calc(var(--spacing) * 12)}.ml-16{margin-left:calc(var(--spacing) * 16)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\!block{display:block!important}.\!flex{display:flex!important}.\!grid{display:grid!important}.\!hidden{display:none!important}.\!inline{display:inline!important}.\!inline-block{display:inline-block!important}.\!inline-flex{display:inline-flex!important}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.\!h-0{height:0!important}.\!h-1{height:var(--spacing)!important}.\!h-2{height:calc(var(--spacing) * 2)!important}.\!h-3{height:calc(var(--spacing) * 3)!important}.\!h-4{height:calc(var(--spacing) * 4)!important}.\!h-5{height:calc(var(--spacing) * 5)!important}.\!h-6{height:calc(var(--spacing) * 6)!important}.\!h-8{height:calc(var(--spacing) * 8)!important}.\!h-10{height:calc(var(--spacing) * 10)!important}.\!h-12{height:calc(var(--spacing) * 12)!important}.\!h-16{height:calc(var(--spacing) * 16)!important}.\!h-20{height:calc(var(--spacing) * 20)!important}.\!h-24{height:calc(var(--spacing) * 24)!important}.\!h-32{height:calc(var(--spacing) * 32)!important}.\!h-40{height:calc(var(--spacing) * 40)!important}.\!h-48{height:calc(var(--spacing) * 48)!important}.\!h-56{height:calc(var(--spacing) * 56)!important}.\!h-64{height:calc(var(--spacing) * 64)!important}.\!h-72{height:calc(var(--spacing) * 72)!important}.\!h-80{height:calc(var(--spacing) * 80)!important}.\!h-96{height:calc(var(--spacing) * 96)!important}.\!h-auto{height:auto!important}.\!h-fit{height:fit-content!important}.\!h-full{height:100%!important}.\!h-max{height:max-content!important}.\!h-min{height:min-content!important}.\!h-px{height:1px!important}.\!h-screen{height:100vh!important}.h-0{height:0}.h-1{height:var(--spacing)}.h-1\/2{height:50%}.h-1\/3{height:33.3333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.6667%}.h-2{height:calc(var(--spacing) * 2)}.h-2\/3{height:66.6667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing) * 4)}.h-4\/5{height:80%}.h-5{height:calc(var(--spacing) * 5)}.h-5\/6{height:83.3333%}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-32{height:calc(var(--spacing) * 32)}.h-40{height:calc(var(--spacing) * 40)}.h-48{height:calc(var(--spacing) * 48)}.h-56{height:calc(var(--spacing) * 56)}.h-64{height:calc(var(--spacing) * 64)}.h-72{height:calc(var(--spacing) * 72)}.h-80{height:calc(var(--spacing) * 80)}.h-96{height:calc(var(--spacing) * 96)}.h-\[42px\]{height:42px}.h-auto{height:auto}.h-fit{height:fit-content}.h-full{height:100%}.h-max{height:max-content}.h-min{height:min-content}.h-px{height:1px}.h-screen{height:100vh}.max-h-0{max-height:0}.max-h-20{max-height:calc(var(--spacing) * 20)}.max-h-32{max-height:calc(var(--spacing) * 32)}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-56{max-height:calc(var(--spacing) * 56)}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[70vh\]{max-height:70vh}.max-h-\[400px\]{max-height:400px}.max-h-\[500px\]{max-height:500px}.max-h-\[calc\(90vh-140px\)\]{max-height:calc(90vh - 140px)}.max-h-\[calc\(100vh-320px\)\]{max-height:calc(100vh - 320px)}.max-h-fit{max-height:fit-content}.max-h-full{max-height:100%}.max-h-max{max-height:max-content}.max-h-min{max-height:min-content}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.min-h-0{min-height:0}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-12{min-height:calc(var(--spacing) * 12)}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-32{min-height:calc(var(--spacing) * 32)}.min-h-40{min-height:calc(var(--spacing) * 40)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-56{min-height:calc(var(--spacing) * 56)}.min-h-64{min-height:calc(var(--spacing) * 64)}.min-h-\[40px\]{min-height:40px}.min-h-\[42px\]{min-height:42px}.min-h-\[60px\]{min-height:60px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[80px\]{min-height:80px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-fit{min-height:fit-content}.min-h-full{min-height:100%}.min-h-max{min-height:max-content}.min-h-min{min-height:min-content}.min-h-screen{min-height:100vh}.\!w-0{width:0!important}.\!w-1{width:var(--spacing)!important}.\!w-1\/2{width:50%!important}.\!w-1\/3{width:33.3333%!important}.\!w-1\/4{width:25%!important}.\!w-1\/5{width:20%!important}.\!w-1\/6{width:16.6667%!important}.\!w-2{width:calc(var(--spacing) * 2)!important}.\!w-2\/3{width:66.6667%!important}.\!w-2\/4{width:50%!important}.\!w-2\/5{width:40%!important}.\!w-3{width:calc(var(--spacing) * 3)!important}.\!w-3\/4{width:75%!important}.\!w-3\/5{width:60%!important}.\!w-4{width:calc(var(--spacing) * 4)!important}.\!w-4\/5{width:80%!important}.\!w-5{width:calc(var(--spacing) * 5)!important}.\!w-5\/6{width:83.3333%!important}.\!w-6{width:calc(var(--spacing) * 6)!important}.\!w-8{width:calc(var(--spacing) * 8)!important}.\!w-10{width:calc(var(--spacing) * 10)!important}.\!w-12{width:calc(var(--spacing) * 12)!important}.\!w-16{width:calc(var(--spacing) * 16)!important}.\!w-20{width:calc(var(--spacing) * 20)!important}.\!w-24{width:calc(var(--spacing) * 24)!important}.\!w-32{width:calc(var(--spacing) * 32)!important}.\!w-40{width:calc(var(--spacing) * 40)!important}.\!w-48{width:calc(var(--spacing) * 48)!important}.\!w-52{width:calc(var(--spacing) * 52)!important}.\!w-56{width:calc(var(--spacing) * 56)!important}.\!w-60{width:calc(var(--spacing) * 60)!important}.\!w-64{width:calc(var(--spacing) * 64)!important}.\!w-72{width:calc(var(--spacing) * 72)!important}.\!w-80{width:calc(var(--spacing) * 80)!important}.\!w-96{width:calc(var(--spacing) * 96)!important}.\!w-auto{width:auto!important}.\!w-fit{width:fit-content!important}.\!w-full{width:100%!important}.\!w-max{width:max-content!important}.\!w-min{width:min-content!important}.\!w-px{width:1px!important}.\!w-screen{width:100vw!important}.w-0{width:0}.w-1{width:var(--spacing)}.w-1\/2{width:50%}.w-1\/3{width:33.3333%}.w-1\/4{width:25%}.w-1\/5{width:20%}.w-1\/6{width:16.6667%}.w-2{width:calc(var(--spacing) * 2)}.w-2\/3{width:66.6667%}.w-2\/4{width:50%}.w-2\/5{width:40%}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4{width:calc(var(--spacing) * 4)}.w-4\/5{width:80%}.w-5{width:calc(var(--spacing) * 5)}.w-5\/6{width:83.3333%}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-32{width:calc(var(--spacing) * 32)}.w-36{width:calc(var(--spacing) * 36)}.w-40{width:calc(var(--spacing) * 40)}.w-48{width:calc(var(--spacing) * 48)}.w-52{width:calc(var(--spacing) * 52)}.w-56{width:calc(var(--spacing) * 56)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-min{width:min-content}.w-px{width:1px}.w-screen{width:100vw}.\!max-w-0{max-width:0!important}.\!max-w-2xl{max-width:var(--container-2xl)!important}.\!max-w-3xl{max-width:var(--container-3xl)!important}.\!max-w-4xl{max-width:var(--container-4xl)!important}.\!max-w-full{max-width:100%!important}.\!max-w-lg{max-width:var(--container-lg)!important}.\!max-w-md{max-width:var(--container-md)!important}.\!max-w-none{max-width:none!important}.\!max-w-sm{max-width:var(--container-sm)!important}.\!max-w-xl{max-width:var(--container-xl)!important}.\!max-w-xs{max-width:var(--container-xs)!important}.max-w-0{max-width:0}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[200px\]{max-width:200px}.max-w-\[calc\(100vw-1rem\)\]{max-width:calc(100vw - 1rem)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-fit{max-width:fit-content}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-max{max-width:max-content}.max-w-md{max-width:var(--container-md)}.max-w-min{max-width:min-content}.max-w-none{max-width:none}.max-w-prose{max-width:65ch}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-screen-lg{max-width:var(--breakpoint-lg)}.max-w-screen-md{max-width:var(--breakpoint-md)}.max-w-screen-sm{max-width:var(--breakpoint-sm)}.max-w-screen-xl{max-width:var(--breakpoint-xl)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.\!min-w-0{min-width:0!important}.\!min-w-4{min-width:calc(var(--spacing) * 4)!important}.\!min-w-8{min-width:calc(var(--spacing) * 8)!important}.\!min-w-12{min-width:calc(var(--spacing) * 12)!important}.\!min-w-16{min-width:calc(var(--spacing) * 16)!important}.\!min-w-20{min-width:calc(var(--spacing) * 20)!important}.\!min-w-24{min-width:calc(var(--spacing) * 24)!important}.\!min-w-32{min-width:calc(var(--spacing) * 32)!important}.\!min-w-40{min-width:calc(var(--spacing) * 40)!important}.\!min-w-48{min-width:calc(var(--spacing) * 48)!important}.\!min-w-56{min-width:calc(var(--spacing) * 56)!important}.\!min-w-64{min-width:calc(var(--spacing) * 64)!important}.\!min-w-fit{min-width:fit-content!important}.\!min-w-full{min-width:100%!important}.\!min-w-max{min-width:max-content!important}.\!min-w-min{min-width:min-content!important}.min-w-0{min-width:0}.min-w-\[20px\]{min-width:20px}.min-w-\[80px\]{min-width:80px}.min-w-\[120px\]{min-width:120px}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.min-w-\[300px\]{min-width:300px}.min-w-fit{min-width:fit-content}.min-w-full{min-width:100%}.min-w-max{min-width:max-content}.min-w-min{min-width:min-content}.\!flex-1{flex:1!important}.\!flex-auto{flex:auto!important}.\!flex-initial{flex:0 auto!important}.\!flex-none{flex:none!important}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-0{flex-basis:0}.basis-1{flex-basis:var(--spacing)}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.3333%}.basis-1\/4{flex-basis:25%}.basis-2{flex-basis:calc(var(--spacing) * 2)}.basis-2\/3{flex-basis:66.6667%}.basis-3\/4{flex-basis:75%}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.caption-bottom{caption-side:bottom}.caption-top{caption-side:top}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-1{--tw-border-spacing-x:var(--spacing);--tw-border-spacing-y:var(--spacing);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-2{--tw-border-spacing-x:calc(var(--spacing) * 2);--tw-border-spacing-y:calc(var(--spacing) * 2);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-3{--tw-border-spacing-x:calc(var(--spacing) * 3);--tw-border-spacing-y:calc(var(--spacing) * 3);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-4{--tw-border-spacing-x:calc(var(--spacing) * 4);--tw-border-spacing-y:calc(var(--spacing) * 4);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-5{--tw-border-spacing-x:calc(var(--spacing) * 5);--tw-border-spacing-y:calc(var(--spacing) * 5);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-6{--tw-border-spacing-x:calc(var(--spacing) * 6);--tw-border-spacing-y:calc(var(--spacing) * 6);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-8{--tw-border-spacing-x:calc(var(--spacing) * 8);--tw-border-spacing-y:calc(var(--spacing) * 8);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-bottom{transform-origin:bottom}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.origin-center{transform-origin:50%}.origin-left{transform-origin:0}.origin-right{transform-origin:100%}.origin-top{transform-origin:top}.origin-top-left{transform-origin:0 0}.origin-top-right{transform-origin:100% 0}.-translate-x-1{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-2{--tw-translate-x:calc(var(--spacing) * -2);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-3{--tw-translate-x:calc(var(--spacing) * -3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-4{--tw-translate-x:calc(var(--spacing) * -4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1{--tw-translate-x:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x: 50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/3{--tw-translate-x:calc(1 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/4{--tw-translate-x: 25% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-2{--tw-translate-x:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-2\/3{--tw-translate-x:calc(2 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-3{--tw-translate-x:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-3\/4{--tw-translate-x: 75% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-6{--tw-translate-x:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-8{--tw-translate-x:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-10{--tw-translate-x:calc(var(--spacing) * 10);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-12{--tw-translate-x:calc(var(--spacing) * 12);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-2{--tw-translate-y:calc(var(--spacing) * -2);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-3{--tw-translate-y:calc(var(--spacing) * -3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-4{--tw-translate-y:calc(var(--spacing) * -4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-full{--tw-translate-y:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y: 50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/3{--tw-translate-y:calc(1 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/4{--tw-translate-y: 25% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2\/3{--tw-translate-y:calc(2 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3\/4{--tw-translate-y: 75% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-4{--tw-translate-y:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-5{--tw-translate-y:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-6{--tw-translate-y:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-10{--tw-translate-y:calc(var(--spacing) * 10);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-12{--tw-translate-y:calc(var(--spacing) * 12);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-full{--tw-translate-y:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-50{--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-150{--tw-scale-x:150%;--tw-scale-y:150%;--tw-scale-z:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-50{--tw-scale-x:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-75{--tw-scale-x:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-90{--tw-scale-x:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-95{--tw-scale-x:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-100{--tw-scale-x:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-105{--tw-scale-x:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-110{--tw-scale-x:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-125{--tw-scale-x:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-150{--tw-scale-x:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-0{--tw-scale-y:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-50{--tw-scale-y:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-75{--tw-scale-y:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-90{--tw-scale-y:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-95{--tw-scale-y:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-100{--tw-scale-y:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-105{--tw-scale-y:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-110{--tw-scale-y:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-125{--tw-scale-y:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-150{--tw-scale-y:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.-rotate-1{rotate:-1deg}.-rotate-2{rotate:-2deg}.-rotate-3{rotate:-3deg}.-rotate-6{rotate:-6deg}.-rotate-12{rotate:-12deg}.-rotate-45{rotate:-45deg}.-rotate-90{rotate:-90deg}.-rotate-180{rotate:-180deg}.rotate-0{rotate:0deg}.rotate-1{rotate:1deg}.rotate-2{rotate:2deg}.rotate-3{rotate:3deg}.rotate-6{rotate:6deg}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.-skew-x-1{--tw-skew-x:skewX( -1deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-2{--tw-skew-x:skewX( -2deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-3{--tw-skew-x:skewX( -3deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-6{--tw-skew-x:skewX( -6deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-12{--tw-skew-x:skewX( -12deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-0{--tw-skew-x:skewX(0deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-1{--tw-skew-x:skewX(1deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-2{--tw-skew-x:skewX(2deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-3{--tw-skew-x:skewX(3deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-6{--tw-skew-x:skewX(6deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-12{--tw-skew-x:skewX(12deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-1{--tw-skew-y:skewY( -1deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-2{--tw-skew-y:skewY( -2deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-3{--tw-skew-y:skewY( -3deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-6{--tw-skew-y:skewY( -6deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-12{--tw-skew-y:skewY( -12deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-0{--tw-skew-y:skewY(0deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-1{--tw-skew-y:skewY(1deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-2{--tw-skew-y:skewY(2deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-3{--tw-skew-y:skewY(3deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-6{--tw-skew-y:skewY(6deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-12{--tw-skew-y:skewY(12deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform,.transform-cpu{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-none{transform:none}.animate-\[loading_1\.5s_ease-in-out_infinite\]{animation:1.5s ease-in-out infinite loading}.animate-bounce{animation:var(--animate-bounce)}.animate-none{animation:none}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-alias{cursor:alias}.cursor-all-scroll{cursor:all-scroll}.cursor-auto{cursor:auto}.cursor-cell{cursor:cell}.cursor-col-resize{cursor:col-resize}.cursor-context-menu{cursor:context-menu}.cursor-copy{cursor:copy}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-e-resize{cursor:e-resize}.cursor-ew-resize{cursor:ew-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-help{cursor:help}.cursor-move{cursor:move}.cursor-n-resize{cursor:n-resize}.cursor-ne-resize{cursor:ne-resize}.cursor-nesw-resize{cursor:nesw-resize}.cursor-no-drop{cursor:no-drop}.cursor-none{cursor:none}.cursor-not-allowed{cursor:not-allowed}.cursor-ns-resize{cursor:ns-resize}.cursor-nw-resize{cursor:nw-resize}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.cursor-progress{cursor:progress}.cursor-row-resize{cursor:row-resize}.cursor-s-resize{cursor:s-resize}.cursor-se-resize{cursor:se-resize}.cursor-sw-resize{cursor:sw-resize}.cursor-text{cursor:text}.cursor-vertical-text{cursor:vertical-text}.cursor-w-resize{cursor:w-resize}.cursor-wait{cursor:wait}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.touch-pan-left{--tw-pan-x:pan-left;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-right{--tw-pan-x:pan-right;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-x{--tw-pan-x:pan-x;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-down{--tw-pan-y:pan-down;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-up{--tw-pan-y:pan-up;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-y{--tw-pan-y:pan-y;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-auto{touch-action:auto}.touch-manipulation{touch-action:manipulation}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.resize-x{resize:horizontal}.resize-y{resize:vertical}.snap-both{scroll-snap-type:both var(--tw-scroll-snap-strictness)}.snap-none{scroll-snap-type:none}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-align-none{scroll-snap-align:none}.snap-center{scroll-snap-align:center}.snap-end{scroll-snap-align:end}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.snap-normal{scroll-snap-stop:normal}.scroll-mt-44{scroll-margin-top:calc(var(--spacing) * 44)}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.appearance-auto{appearance:auto}.appearance-none{appearance:none}.columns-1{columns:1}.columns-2{columns:2}.columns-2xl{columns:var(--container-2xl)}.columns-2xs{columns:var(--container-2xs)}.columns-3{columns:3}.columns-3xl{columns:var(--container-3xl)}.columns-3xs{columns:var(--container-3xs)}.columns-4{columns:4}.columns-4xl{columns:var(--container-4xl)}.columns-5{columns:5}.columns-5xl{columns:var(--container-5xl)}.columns-6{columns:6}.columns-6xl{columns:var(--container-6xl)}.columns-7{columns:7}.columns-7xl{columns:var(--container-7xl)}.columns-8{columns:8}.columns-9{columns:9}.columns-10{columns:10}.columns-11{columns:11}.columns-12{columns:12}.columns-auto{columns:auto}.columns-lg{columns:var(--container-lg)}.columns-md{columns:var(--container-md)}.columns-sm{columns:var(--container-sm)}.columns-xl{columns:var(--container-xl)}.columns-xs{columns:var(--container-xs)}.break-before-all{break-before:all}.break-before-auto{break-before:auto}.break-before-avoid{break-before:avoid}.break-before-avoid-page{break-before:avoid-page}.break-before-column{break-before:column}.break-before-left{break-before:left}.break-before-page{break-before:page}.break-before-right{break-before:right}.break-inside-auto{break-inside:auto}.break-inside-avoid{break-inside:avoid}.break-inside-avoid-column{break-inside:avoid-column}.break-inside-avoid-page{break-inside:avoid-page}.break-after-all{break-after:all}.break-after-auto{break-after:auto}.break-after-avoid{break-after:avoid}.break-after-avoid-page{break-after:avoid-page}.break-after-column{break-after:column}.break-after-left{break-after:left}.break-after-page{break-after:page}.break-after-right{break-after:right}.auto-cols-auto{grid-auto-columns:auto}.auto-cols-fr{grid-auto-columns:minmax(0,1fr)}.auto-cols-max{grid-auto-columns:max-content}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-col{grid-auto-flow:column}.grid-flow-col-dense{grid-auto-flow:column dense}.grid-flow-dense{grid-auto-flow:dense}.grid-flow-row{grid-auto-flow:row}.grid-flow-row-dense{grid-auto-flow:dense}.auto-rows-auto{grid-auto-rows:auto}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.auto-rows-max{grid-auto-rows:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.\!flex-col{flex-direction:column!important}.\!flex-row{flex-direction:row!important}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.\!flex-nowrap{flex-wrap:nowrap!important}.\!flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-around{place-content:space-around}.place-content-baseline{place-content:baseline start}.place-content-between{place-content:space-between}.place-content-center{place-content:center}.place-content-end{place-content:end}.place-content-evenly{place-content:space-evenly}.place-content-start{place-content:start}.place-content-stretch{place-content:stretch}.place-items-baseline{place-items:baseline}.place-items-center{place-items:center}.place-items-end{place-items:end}.place-items-start{place-items:start}.place-items-stretch{place-items:stretch stretch}.content-around{align-content:space-around}.content-between{align-content:space-between}.content-center{align-content:center}.content-end{align-content:flex-end}.content-evenly{align-content:space-evenly}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-evenly{justify-content:space-evenly}.justify-start{justify-content:flex-start}.\!gap-0{gap:0!important}.\!gap-1{gap:var(--spacing)!important}.\!gap-2{gap:calc(var(--spacing) * 2)!important}.\!gap-3{gap:calc(var(--spacing) * 3)!important}.\!gap-4{gap:calc(var(--spacing) * 4)!important}.\!gap-5{gap:calc(var(--spacing) * 5)!important}.\!gap-6{gap:calc(var(--spacing) * 6)!important}.\!gap-8{gap:calc(var(--spacing) * 8)!important}.\!gap-10{gap:calc(var(--spacing) * 10)!important}.\!gap-12{gap:calc(var(--spacing) * 12)!important}.\!gap-16{gap:calc(var(--spacing) * 16)!important}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.-space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * -1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-y-reverse)))}:where(.-space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * -2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-0{column-gap:0}.gap-x-1{column-gap:var(--spacing)}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-x-10{column-gap:calc(var(--spacing) * 10)}.gap-x-12{column-gap:calc(var(--spacing) * 12)}:where(.-space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * -1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-x-reverse)))}:where(.-space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * -2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline:0}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(var(--spacing) * var(--tw-space-x-reverse));margin-inline-end:calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 5) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-0{row-gap:0}.gap-y-1{row-gap:var(--spacing)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.gap-y-5{row-gap:calc(var(--spacing) * 5)}.gap-y-6{row-gap:calc(var(--spacing) * 6)}.gap-y-8{row-gap:calc(var(--spacing) * 8)}.gap-y-10{row-gap:calc(var(--spacing) * 10)}.gap-y-12{row-gap:calc(var(--spacing) * 12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-0>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(0px * var(--tw-divide-x-reverse));border-inline-end-width:calc(0px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-2>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(2px * var(--tw-divide-x-reverse));border-inline-end-width:calc(2px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-4>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(4px * var(--tw-divide-x-reverse));border-inline-end-width:calc(4px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-8>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(8px * var(--tw-divide-x-reverse));border-inline-end-width:calc(8px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-0>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(0px * var(--tw-divide-y-reverse));border-bottom-width:calc(0px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-2>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(2px * var(--tw-divide-y-reverse));border-bottom-width:calc(2px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-4>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(4px * var(--tw-divide-y-reverse));border-bottom-width:calc(4px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-8>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(8px * var(--tw-divide-y-reverse));border-bottom-width:calc(8px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}:where(.divide-dashed>:not(:last-child)){--tw-border-style:dashed;border-style:dashed}:where(.divide-dotted>:not(:last-child)){--tw-border-style:dotted;border-style:dotted}:where(.divide-double>:not(:last-child)){--tw-border-style:double;border-style:double}:where(.divide-none>:not(:last-child)){--tw-border-style:none;border-style:none}:where(.divide-solid>:not(:last-child)){--tw-border-style:solid;border-style:solid}:where(.divide-black>:not(:last-child)){border-color:var(--color-black)}:where(.divide-current>:not(:last-child)){border-color:currentColor}:where(.divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.divide-inherit>:not(:last-child)){border-color:inherit}:where(.divide-purple-100>:not(:last-child)){border-color:var(--color-purple-100)}:where(.divide-slate-50>:not(:last-child)){border-color:var(--color-slate-50)}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}:where(.divide-slate-300>:not(:last-child)){border-color:var(--color-slate-300)}:where(.divide-slate-400>:not(:last-child)){border-color:var(--color-slate-400)}:where(.divide-slate-500>:not(:last-child)){border-color:var(--color-slate-500)}:where(.divide-slate-600>:not(:last-child)){border-color:var(--color-slate-600)}:where(.divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}:where(.divide-slate-800>:not(:last-child)){border-color:var(--color-slate-800)}:where(.divide-slate-900>:not(:last-child)){border-color:var(--color-slate-900)}:where(.divide-transparent>:not(:last-child)){border-color:#0000}:where(.divide-white>:not(:last-child)){border-color:var(--color-white)}.place-self-auto{place-self:auto}.place-self-center{place-self:center}.place-self-end{place-self:end}.place-self-start{place-self:start}.place-self-stretch{place-self:stretch stretch}.self-auto{align-self:auto}.self-baseline{align-self:baseline}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-clip{overflow-x:clip}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-visible{overflow-x:visible}.overflow-y-auto{overflow-y:auto}.overflow-y-clip{overflow-y:clip}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-visible{overflow-y:visible}.overscroll-auto{overscroll-behavior:auto}.overscroll-contain{overscroll-behavior:contain}.overscroll-none{overscroll-behavior:none}.overscroll-x-auto{overscroll-behavior-x:auto}.overscroll-x-contain{overscroll-behavior-x:contain}.overscroll-x-none{overscroll-behavior-x:none}.overscroll-y-auto{overscroll-behavior-y:auto}.overscroll-y-contain{overscroll-behavior-y:contain}.overscroll-y-none{overscroll-behavior-y:none}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t-sm{border-top-left-radius:var(--radius-sm);border-top-right-radius:var(--radius-sm)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-2xl{border-top-left-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-l-3xl{border-top-left-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-l-full{border-top-left-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-sm{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-l-xl{border-top-left-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-2xl{border-top-left-radius:var(--radius-2xl)}.rounded-tl-3xl{border-top-left-radius:var(--radius-3xl)}.rounded-tl-full{border-top-left-radius:3.40282e38px}.rounded-tl-lg{border-top-left-radius:var(--radius-lg)}.rounded-tl-md{border-top-left-radius:var(--radius-md)}.rounded-tl-none{border-top-left-radius:0}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-tl-xl{border-top-left-radius:var(--radius-xl)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-2xl{border-top-right-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl)}.rounded-r-3xl{border-top-right-radius:var(--radius-3xl);border-bottom-right-radius:var(--radius-3xl)}.rounded-r-full{border-top-right-radius:3.40282e38px;border-bottom-right-radius:3.40282e38px}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-sm{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}.rounded-r-xl{border-top-right-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl)}.rounded-tr{border-top-right-radius:.25rem}.rounded-tr-2xl{border-top-right-radius:var(--radius-2xl)}.rounded-tr-3xl{border-top-right-radius:var(--radius-3xl)}.rounded-tr-full{border-top-right-radius:3.40282e38px}.rounded-tr-lg{border-top-right-radius:var(--radius-lg)}.rounded-tr-md{border-top-right-radius:var(--radius-md)}.rounded-tr-none{border-top-right-radius:0}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-b-2xl{border-bottom-right-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-b-3xl{border-bottom-right-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-b-full{border-bottom-right-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-b-md{border-bottom-right-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}.rounded-b-sm{border-bottom-right-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-2xl{border-bottom-right-radius:var(--radius-2xl)}.rounded-br-3xl{border-bottom-right-radius:var(--radius-3xl)}.rounded-br-full{border-bottom-right-radius:3.40282e38px}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-br-none{border-bottom-right-radius:0}.rounded-br-sm{border-bottom-right-radius:var(--radius-sm)}.rounded-br-xl{border-bottom-right-radius:var(--radius-xl)}.rounded-bl{border-bottom-left-radius:.25rem}.rounded-bl-2xl{border-bottom-left-radius:var(--radius-2xl)}.rounded-bl-3xl{border-bottom-left-radius:var(--radius-3xl)}.rounded-bl-full{border-bottom-left-radius:3.40282e38px}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.rounded-bl-none{border-bottom-left-radius:0}.rounded-bl-sm{border-bottom-left-radius:var(--radius-sm)}.rounded-bl-xl{border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-3{border-style:var(--tw-border-style);border-width:3px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-8{border-style:var(--tw-border-style);border-width:8px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.border-x-2{border-inline-style:var(--tw-border-style);border-inline-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-y-0{border-block-style:var(--tw-border-style);border-block-width:0}.border-y-2{border-block-style:var(--tw-border-style);border-block-width:2px}.\!border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-2{border-right-style:var(--tw-border-style);border-right-width:2px}.border-r-4{border-right-style:var(--tw-border-style);border-right-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-4{border-bottom-style:var(--tw-border-style);border-bottom-width:4px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-hidden{--tw-border-style:hidden;border-style:hidden}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-amber-50{border-color:var(--color-amber-50)}.border-amber-100{border-color:var(--color-amber-100)}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-amber-400{border-color:var(--color-amber-400)}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-600{border-color:var(--color-amber-600)}.border-amber-700{border-color:var(--color-amber-700)}.border-amber-800{border-color:var(--color-amber-800)}.border-amber-900{border-color:var(--color-amber-900)}.border-amber-950{border-color:var(--color-amber-950)}.border-black{border-color:var(--color-black)}.border-blue-50{border-color:var(--color-blue-50)}.border-blue-100{border-color:var(--color-blue-100)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-blue-800{border-color:var(--color-blue-800)}.border-blue-900{border-color:var(--color-blue-900)}.border-blue-950{border-color:var(--color-blue-950)}.border-current{border-color:currentColor}.border-cyan-50{border-color:var(--color-cyan-50)}.border-cyan-100{border-color:var(--color-cyan-100)}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-300{border-color:var(--color-cyan-300)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-cyan-500{border-color:var(--color-cyan-500)}.border-cyan-600{border-color:var(--color-cyan-600)}.border-cyan-700{border-color:var(--color-cyan-700)}.border-cyan-800{border-color:var(--color-cyan-800)}.border-cyan-900{border-color:var(--color-cyan-900)}.border-cyan-950{border-color:var(--color-cyan-950)}.border-emerald-50{border-color:var(--color-emerald-50)}.border-emerald-100{border-color:var(--color-emerald-100)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-emerald-400{border-color:var(--color-emerald-400)}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-600{border-color:var(--color-emerald-600)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-emerald-800{border-color:var(--color-emerald-800)}.border-emerald-900{border-color:var(--color-emerald-900)}.border-emerald-950{border-color:var(--color-emerald-950)}.border-fuchsia-50{border-color:var(--color-fuchsia-50)}.border-fuchsia-100{border-color:var(--color-fuchsia-100)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-fuchsia-300{border-color:var(--color-fuchsia-300)}.border-fuchsia-400{border-color:var(--color-fuchsia-400)}.border-fuchsia-500{border-color:var(--color-fuchsia-500)}.border-fuchsia-600{border-color:var(--color-fuchsia-600)}.border-fuchsia-700{border-color:var(--color-fuchsia-700)}.border-fuchsia-800{border-color:var(--color-fuchsia-800)}.border-fuchsia-900{border-color:var(--color-fuchsia-900)}.border-fuchsia-950{border-color:var(--color-fuchsia-950)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-500{border-color:var(--color-gray-500)}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-900{border-color:var(--color-gray-900)}.border-gray-950{border-color:var(--color-gray-950)}.border-green-50{border-color:var(--color-green-50)}.border-green-100{border-color:var(--color-green-100)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-green-400{border-color:var(--color-green-400)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-green-700{border-color:var(--color-green-700)}.border-green-800{border-color:var(--color-green-800)}.border-green-900{border-color:var(--color-green-900)}.border-green-950{border-color:var(--color-green-950)}.border-indigo-50{border-color:var(--color-indigo-50)}.border-indigo-100{border-color:var(--color-indigo-100)}.border-indigo-200{border-color:var(--color-indigo-200)}.border-indigo-300{border-color:var(--color-indigo-300)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-indigo-700{border-color:var(--color-indigo-700)}.border-indigo-800{border-color:var(--color-indigo-800)}.border-indigo-900{border-color:var(--color-indigo-900)}.border-indigo-950{border-color:var(--color-indigo-950)}.border-inherit{border-color:inherit}.border-lime-50{border-color:var(--color-lime-50)}.border-lime-100{border-color:var(--color-lime-100)}.border-lime-200{border-color:var(--color-lime-200)}.border-lime-300{border-color:var(--color-lime-300)}.border-lime-400{border-color:var(--color-lime-400)}.border-lime-500{border-color:var(--color-lime-500)}.border-lime-600{border-color:var(--color-lime-600)}.border-lime-700{border-color:var(--color-lime-700)}.border-lime-800{border-color:var(--color-lime-800)}.border-lime-900{border-color:var(--color-lime-900)}.border-lime-950{border-color:var(--color-lime-950)}.border-neutral-50{border-color:var(--color-neutral-50)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-400{border-color:var(--color-neutral-400)}.border-neutral-500{border-color:var(--color-neutral-500)}.border-neutral-600{border-color:var(--color-neutral-600)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-neutral-950{border-color:var(--color-neutral-950)}.border-orange-50{border-color:var(--color-orange-50)}.border-orange-100{border-color:var(--color-orange-100)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-300{border-color:var(--color-orange-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-orange-500{border-color:var(--color-orange-500)}.border-orange-600{border-color:var(--color-orange-600)}.border-orange-700{border-color:var(--color-orange-700)}.border-orange-800{border-color:var(--color-orange-800)}.border-orange-900{border-color:var(--color-orange-900)}.border-orange-950{border-color:var(--color-orange-950)}.border-pink-50{border-color:var(--color-pink-50)}.border-pink-100{border-color:var(--color-pink-100)}.border-pink-200{border-color:var(--color-pink-200)}.border-pink-300{border-color:var(--color-pink-300)}.border-pink-400{border-color:var(--color-pink-400)}.border-pink-500{border-color:var(--color-pink-500)}.border-pink-600{border-color:var(--color-pink-600)}.border-pink-700{border-color:var(--color-pink-700)}.border-pink-800{border-color:var(--color-pink-800)}.border-pink-900{border-color:var(--color-pink-900)}.border-pink-950{border-color:var(--color-pink-950)}.border-purple-50{border-color:var(--color-purple-50)}.border-purple-100{border-color:var(--color-purple-100)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-400{border-color:var(--color-purple-400)}.border-purple-500{border-color:var(--color-purple-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-purple-700{border-color:var(--color-purple-700)}.border-purple-800{border-color:var(--color-purple-800)}.border-purple-900{border-color:var(--color-purple-900)}.border-purple-950{border-color:var(--color-purple-950)}.border-red-50{border-color:var(--color-red-50)}.border-red-100{border-color:var(--color-red-100)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-red-400{border-color:var(--color-red-400)}.border-red-500{border-color:var(--color-red-500)}.border-red-600{border-color:var(--color-red-600)}.border-red-700{border-color:var(--color-red-700)}.border-red-800{border-color:var(--color-red-800)}.border-red-900{border-color:var(--color-red-900)}.border-red-950{border-color:var(--color-red-950)}.border-rose-50{border-color:var(--color-rose-50)}.border-rose-100{border-color:var(--color-rose-100)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-rose-400{border-color:var(--color-rose-400)}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-600{border-color:var(--color-rose-600)}.border-rose-700{border-color:var(--color-rose-700)}.border-rose-800{border-color:var(--color-rose-800)}.border-rose-900{border-color:var(--color-rose-900)}.border-rose-950{border-color:var(--color-rose-950)}.border-sky-50{border-color:var(--color-sky-50)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-300{border-color:var(--color-sky-300)}.border-sky-400{border-color:var(--color-sky-400)}.border-sky-500{border-color:var(--color-sky-500)}.border-sky-600{border-color:var(--color-sky-600)}.border-sky-700{border-color:var(--color-sky-700)}.border-sky-800{border-color:var(--color-sky-800)}.border-sky-900{border-color:var(--color-sky-900)}.border-sky-950{border-color:var(--color-sky-950)}.border-slate-50{border-color:var(--color-slate-50)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400{border-color:var(--color-slate-400)}.border-slate-500{border-color:var(--color-slate-500)}.border-slate-600{border-color:var(--color-slate-600)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-900{border-color:var(--color-slate-900)}.border-slate-950{border-color:var(--color-slate-950)}.border-stone-50{border-color:var(--color-stone-50)}.border-stone-100{border-color:var(--color-stone-100)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-stone-400{border-color:var(--color-stone-400)}.border-stone-500{border-color:var(--color-stone-500)}.border-stone-600{border-color:var(--color-stone-600)}.border-stone-700{border-color:var(--color-stone-700)}.border-stone-800{border-color:var(--color-stone-800)}.border-stone-900{border-color:var(--color-stone-900)}.border-stone-950{border-color:var(--color-stone-950)}.border-teal-50{border-color:var(--color-teal-50)}.border-teal-100{border-color:var(--color-teal-100)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-400{border-color:var(--color-teal-400)}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-teal-800{border-color:var(--color-teal-800)}.border-teal-900{border-color:var(--color-teal-900)}.border-teal-950{border-color:var(--color-teal-950)}.border-transparent{border-color:#0000}.border-violet-50{border-color:var(--color-violet-50)}.border-violet-100{border-color:var(--color-violet-100)}.border-violet-200{border-color:var(--color-violet-200)}.border-violet-300{border-color:var(--color-violet-300)}.border-violet-400{border-color:var(--color-violet-400)}.border-violet-500{border-color:var(--color-violet-500)}.border-violet-600{border-color:var(--color-violet-600)}.border-violet-700{border-color:var(--color-violet-700)}.border-violet-800{border-color:var(--color-violet-800)}.border-violet-900{border-color:var(--color-violet-900)}.border-violet-950{border-color:var(--color-violet-950)}.border-white{border-color:var(--color-white)}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white) 30%,transparent)}}.border-yellow-50{border-color:var(--color-yellow-50)}.border-yellow-100{border-color:var(--color-yellow-100)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-400{border-color:var(--color-yellow-400)}.border-yellow-500{border-color:var(--color-yellow-500)}.border-yellow-600{border-color:var(--color-yellow-600)}.border-yellow-700{border-color:var(--color-yellow-700)}.border-yellow-800{border-color:var(--color-yellow-800)}.border-yellow-900{border-color:var(--color-yellow-900)}.border-yellow-950{border-color:var(--color-yellow-950)}.border-zinc-50{border-color:var(--color-zinc-50)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-400{border-color:var(--color-zinc-400)}.border-zinc-500{border-color:var(--color-zinc-500)}.border-zinc-600{border-color:var(--color-zinc-600)}.border-zinc-700{border-color:var(--color-zinc-700)}.border-zinc-800{border-color:var(--color-zinc-800)}.border-zinc-900{border-color:var(--color-zinc-900)}.border-zinc-950{border-color:var(--color-zinc-950)}.border-t-current{border-top-color:currentColor}.border-t-white{border-top-color:var(--color-white)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-300{background-color:var(--color-amber-300)}.bg-amber-400{background-color:var(--color-amber-400)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-amber-800{background-color:var(--color-amber-800)}.bg-amber-900{background-color:var(--color-amber-900)}.bg-amber-950{background-color:var(--color-amber-950)}.bg-black{background-color:var(--color-black)}.bg-black\/0{background-color:#0000}@supports (color:color-mix(in lab,red,red)){.bg-black\/0{background-color:color-mix(in oklab,var(--color-black) 0%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/70{background-color:#eff6ffb3}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/70{background-color:color-mix(in oklab,var(--color-blue-50) 70%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-300{background-color:var(--color-blue-300)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-blue-800{background-color:var(--color-blue-800)}.bg-blue-900{background-color:var(--color-blue-900)}.bg-blue-950{background-color:var(--color-blue-950)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-200{background-color:var(--color-cyan-200)}.bg-cyan-300{background-color:var(--color-cyan-300)}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-cyan-600{background-color:var(--color-cyan-600)}.bg-cyan-700{background-color:var(--color-cyan-700)}.bg-cyan-800{background-color:var(--color-cyan-800)}.bg-cyan-900{background-color:var(--color-cyan-900)}.bg-cyan-950{background-color:var(--color-cyan-950)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-300{background-color:var(--color-emerald-300)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-emerald-900{background-color:var(--color-emerald-900)}.bg-emerald-950{background-color:var(--color-emerald-950)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-fuchsia-100{background-color:var(--color-fuchsia-100)}.bg-fuchsia-200{background-color:var(--color-fuchsia-200)}.bg-fuchsia-300{background-color:var(--color-fuchsia-300)}.bg-fuchsia-400{background-color:var(--color-fuchsia-400)}.bg-fuchsia-500{background-color:var(--color-fuchsia-500)}.bg-fuchsia-600{background-color:var(--color-fuchsia-600)}.bg-fuchsia-700{background-color:var(--color-fuchsia-700)}.bg-fuchsia-800{background-color:var(--color-fuchsia-800)}.bg-fuchsia-900{background-color:var(--color-fuchsia-900)}.bg-fuchsia-950{background-color:var(--color-fuchsia-950)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-100\/60{background-color:#f3f4f699}@supports (color:color-mix(in lab,red,red)){.bg-gray-100\/60{background-color:color-mix(in oklab,var(--color-gray-100) 60%,transparent)}}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-green-300{background-color:var(--color-green-300)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-800{background-color:var(--color-green-800)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-950{background-color:var(--color-green-950)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-200{background-color:var(--color-indigo-200)}.bg-indigo-300{background-color:var(--color-indigo-300)}.bg-indigo-400{background-color:var(--color-indigo-400)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-indigo-700{background-color:var(--color-indigo-700)}.bg-indigo-800{background-color:var(--color-indigo-800)}.bg-indigo-900{background-color:var(--color-indigo-900)}.bg-indigo-950{background-color:var(--color-indigo-950)}.bg-inherit{background-color:inherit}.bg-lime-50{background-color:var(--color-lime-50)}.bg-lime-100{background-color:var(--color-lime-100)}.bg-lime-200{background-color:var(--color-lime-200)}.bg-lime-300{background-color:var(--color-lime-300)}.bg-lime-400{background-color:var(--color-lime-400)}.bg-lime-500{background-color:var(--color-lime-500)}.bg-lime-600{background-color:var(--color-lime-600)}.bg-lime-700{background-color:var(--color-lime-700)}.bg-lime-800{background-color:var(--color-lime-800)}.bg-lime-900{background-color:var(--color-lime-900)}.bg-lime-950{background-color:var(--color-lime-950)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-orange-300{background-color:var(--color-orange-300)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-orange-700{background-color:var(--color-orange-700)}.bg-orange-800{background-color:var(--color-orange-800)}.bg-orange-900{background-color:var(--color-orange-900)}.bg-orange-950{background-color:var(--color-orange-950)}.bg-pink-50{background-color:var(--color-pink-50)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-200{background-color:var(--color-pink-200)}.bg-pink-300{background-color:var(--color-pink-300)}.bg-pink-400{background-color:var(--color-pink-400)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-pink-600{background-color:var(--color-pink-600)}.bg-pink-700{background-color:var(--color-pink-700)}.bg-pink-800{background-color:var(--color-pink-800)}.bg-pink-900{background-color:var(--color-pink-900)}.bg-pink-950{background-color:var(--color-pink-950)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-200{background-color:var(--color-purple-200)}.bg-purple-300{background-color:var(--color-purple-300)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-purple-500{background-color:var(--color-purple-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-800{background-color:var(--color-purple-800)}.bg-purple-900{background-color:var(--color-purple-900)}.bg-purple-950{background-color:var(--color-purple-950)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-300{background-color:var(--color-red-300)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/50{background-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/50{background-color:color-mix(in oklab,var(--color-red-500) 50%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-950{background-color:var(--color-red-950)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-100{background-color:var(--color-rose-100)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-rose-700{background-color:var(--color-rose-700)}.bg-rose-800{background-color:var(--color-rose-800)}.bg-rose-900{background-color:var(--color-rose-900)}.bg-rose-950{background-color:var(--color-rose-950)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-100{background-color:var(--color-sky-100)}.bg-sky-200{background-color:var(--color-sky-200)}.bg-sky-300{background-color:var(--color-sky-300)}.bg-sky-400{background-color:var(--color-sky-400)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-sky-950{background-color:var(--color-sky-950)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-600{background-color:var(--color-slate-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-stone-200{background-color:var(--color-stone-200)}.bg-stone-300{background-color:var(--color-stone-300)}.bg-stone-400{background-color:var(--color-stone-400)}.bg-stone-500{background-color:var(--color-stone-500)}.bg-stone-600{background-color:var(--color-stone-600)}.bg-stone-700{background-color:var(--color-stone-700)}.bg-stone-800{background-color:var(--color-stone-800)}.bg-stone-900{background-color:var(--color-stone-900)}.bg-stone-950{background-color:var(--color-stone-950)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-100{background-color:var(--color-teal-100)}.bg-teal-200{background-color:var(--color-teal-200)}.bg-teal-300{background-color:var(--color-teal-300)}.bg-teal-400{background-color:var(--color-teal-400)}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-teal-700{background-color:var(--color-teal-700)}.bg-teal-800{background-color:var(--color-teal-800)}.bg-teal-900{background-color:var(--color-teal-900)}.bg-teal-950{background-color:var(--color-teal-950)}.bg-transparent{background-color:#0000}.bg-violet-50{background-color:var(--color-violet-50)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-200{background-color:var(--color-violet-200)}.bg-violet-300{background-color:var(--color-violet-300)}.bg-violet-400{background-color:var(--color-violet-400)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-violet-600{background-color:var(--color-violet-600)}.bg-violet-700{background-color:var(--color-violet-700)}.bg-violet-800{background-color:var(--color-violet-800)}.bg-violet-900{background-color:var(--color-violet-900)}.bg-violet-950{background-color:var(--color-violet-950)}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white) 90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-200{background-color:var(--color-yellow-200)}.bg-yellow-300{background-color:var(--color-yellow-300)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-700{background-color:var(--color-yellow-700)}.bg-yellow-800{background-color:var(--color-yellow-800)}.bg-yellow-900{background-color:var(--color-yellow-900)}.bg-yellow-950{background-color:var(--color-yellow-950)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-200{background-color:var(--color-zinc-200)}.bg-zinc-300{background-color:var(--color-zinc-300)}.bg-zinc-400{background-color:var(--color-zinc-400)}.bg-zinc-500{background-color:var(--color-zinc-500)}.bg-zinc-600{background-color:var(--color-zinc-600)}.bg-zinc-700{background-color:var(--color-zinc-700)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-950{background-color:var(--color-zinc-950)}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-bl{--tw-gradient-position:to bottom left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tl{--tw-gradient-position:to top left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tr{--tw-gradient-position:to top right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black{--tw-gradient-from:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-50{--tw-gradient-from:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-100{--tw-gradient-from:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-200{--tw-gradient-from:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-300{--tw-gradient-from:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-400{--tw-gradient-from:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-600{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-700{--tw-gradient-from:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-800{--tw-gradient-from:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-current{--tw-gradient-from:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-50{--tw-gradient-from:var(--color-green-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-100{--tw-gradient-from:var(--color-green-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-200{--tw-gradient-from:var(--color-green-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-300{--tw-gradient-from:var(--color-green-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-400{--tw-gradient-from:var(--color-green-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-600{--tw-gradient-from:var(--color-green-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-700{--tw-gradient-from:var(--color-green-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-800{--tw-gradient-from:var(--color-green-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-900{--tw-gradient-from:var(--color-green-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-50{--tw-gradient-from:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-100{--tw-gradient-from:var(--color-indigo-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-200{--tw-gradient-from:var(--color-indigo-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-300{--tw-gradient-from:var(--color-indigo-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-400{--tw-gradient-from:var(--color-indigo-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-500{--tw-gradient-from:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-600{--tw-gradient-from:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-700{--tw-gradient-from:var(--color-indigo-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-800{--tw-gradient-from:var(--color-indigo-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-900{--tw-gradient-from:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-inherit{--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-50{--tw-gradient-from:var(--color-orange-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-100{--tw-gradient-from:var(--color-orange-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-200{--tw-gradient-from:var(--color-orange-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-300{--tw-gradient-from:var(--color-orange-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-400{--tw-gradient-from:var(--color-orange-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-500{--tw-gradient-from:var(--color-orange-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-600{--tw-gradient-from:var(--color-orange-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-700{--tw-gradient-from:var(--color-orange-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-800{--tw-gradient-from:var(--color-orange-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-900{--tw-gradient-from:var(--color-orange-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-50{--tw-gradient-from:var(--color-pink-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-100{--tw-gradient-from:var(--color-pink-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-200{--tw-gradient-from:var(--color-pink-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-300{--tw-gradient-from:var(--color-pink-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-400{--tw-gradient-from:var(--color-pink-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-500{--tw-gradient-from:var(--color-pink-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-600{--tw-gradient-from:var(--color-pink-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-700{--tw-gradient-from:var(--color-pink-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-800{--tw-gradient-from:var(--color-pink-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-900{--tw-gradient-from:var(--color-pink-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-50{--tw-gradient-from:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-100{--tw-gradient-from:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-200{--tw-gradient-from:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-300{--tw-gradient-from:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-400{--tw-gradient-from:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-500{--tw-gradient-from:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-600{--tw-gradient-from:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-700{--tw-gradient-from:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-800{--tw-gradient-from:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-900{--tw-gradient-from:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-50{--tw-gradient-from:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-100{--tw-gradient-from:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-200{--tw-gradient-from:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-300{--tw-gradient-from:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-400{--tw-gradient-from:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-600{--tw-gradient-from:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-700{--tw-gradient-from:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-800{--tw-gradient-from:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-900{--tw-gradient-from:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-50{--tw-gradient-from:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-100{--tw-gradient-from:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-200{--tw-gradient-from:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-300{--tw-gradient-from:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-400{--tw-gradient-from:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-500{--tw-gradient-from:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-600{--tw-gradient-from:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-700{--tw-gradient-from:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-800{--tw-gradient-from:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-900{--tw-gradient-from:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-50{--tw-gradient-from:var(--color-yellow-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-100{--tw-gradient-from:var(--color-yellow-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-200{--tw-gradient-from:var(--color-yellow-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-300{--tw-gradient-from:var(--color-yellow-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-500{--tw-gradient-from:var(--color-yellow-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-600{--tw-gradient-from:var(--color-yellow-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-700{--tw-gradient-from:var(--color-yellow-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-800{--tw-gradient-from:var(--color-yellow-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-900{--tw-gradient-from:var(--color-yellow-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-black{--tw-gradient-via:var(--color-black);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-50{--tw-gradient-via:var(--color-blue-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-100{--tw-gradient-via:var(--color-blue-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-200{--tw-gradient-via:var(--color-blue-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-300{--tw-gradient-via:var(--color-blue-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-400{--tw-gradient-via:var(--color-blue-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-500{--tw-gradient-via:var(--color-blue-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-600{--tw-gradient-via:var(--color-blue-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-700{--tw-gradient-via:var(--color-blue-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-800{--tw-gradient-via:var(--color-blue-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-900{--tw-gradient-via:var(--color-blue-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-current{--tw-gradient-via:currentcolor;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-50{--tw-gradient-via:var(--color-gray-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-100{--tw-gradient-via:var(--color-gray-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-200{--tw-gradient-via:var(--color-gray-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-300{--tw-gradient-via:var(--color-gray-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-400{--tw-gradient-via:var(--color-gray-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-500{--tw-gradient-via:var(--color-gray-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-600{--tw-gradient-via:var(--color-gray-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-700{--tw-gradient-via:var(--color-gray-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-800{--tw-gradient-via:var(--color-gray-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-900{--tw-gradient-via:var(--color-gray-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-inherit{--tw-gradient-via:inherit;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-50{--tw-gradient-via:var(--color-slate-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-100{--tw-gradient-via:var(--color-slate-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-200{--tw-gradient-via:var(--color-slate-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-300{--tw-gradient-via:var(--color-slate-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-400{--tw-gradient-via:var(--color-slate-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-500{--tw-gradient-via:var(--color-slate-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-600{--tw-gradient-via:var(--color-slate-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-700{--tw-gradient-via:var(--color-slate-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-800{--tw-gradient-via:var(--color-slate-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-900{--tw-gradient-via:var(--color-slate-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-black{--tw-gradient-to:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-50{--tw-gradient-to:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-100{--tw-gradient-to:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-200{--tw-gradient-to:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-300{--tw-gradient-to:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-400{--tw-gradient-to:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-700{--tw-gradient-to:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-800{--tw-gradient-to:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-900{--tw-gradient-to:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-current{--tw-gradient-to:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-indigo-50{--tw-gradient-to:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-inherit{--tw-gradient-to:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-50{--tw-gradient-to:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-100{--tw-gradient-to:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-200{--tw-gradient-to:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-300{--tw-gradient-to:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-400{--tw-gradient-to:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-500{--tw-gradient-to:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-600{--tw-gradient-to:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-700{--tw-gradient-to:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-800{--tw-gradient-to:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-900{--tw-gradient-to:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-50{--tw-gradient-to:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-100{--tw-gradient-to:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-200{--tw-gradient-to:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-300{--tw-gradient-to:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-400{--tw-gradient-to:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-500{--tw-gradient-to:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-600{--tw-gradient-to:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-700{--tw-gradient-to:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-800{--tw-gradient-to:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-900{--tw-gradient-to:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-50{--tw-gradient-to:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-100{--tw-gradient-to:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-200{--tw-gradient-to:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-300{--tw-gradient-to:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-400{--tw-gradient-to:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-500{--tw-gradient-to:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-600{--tw-gradient-to:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-700{--tw-gradient-to:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-800{--tw-gradient-to:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-900{--tw-gradient-to:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-none{object-fit:none}.object-scale-down{object-fit:scale-down}.object-bottom{object-position:bottom}.object-center{object-position:center}.object-left{object-position:left}.object-left-bottom{object-position:left bottom}.object-left-top{object-position:left top}.object-right{object-position:right}.object-right-bottom{object-position:right bottom}.object-right-top{object-position:right top}.object-top{object-position:top}.\!p-0{padding:0!important}.\!p-1{padding:var(--spacing)!important}.\!p-2{padding:calc(var(--spacing) * 2)!important}.\!p-3{padding:calc(var(--spacing) * 3)!important}.\!p-4{padding:calc(var(--spacing) * 4)!important}.\!p-5{padding:calc(var(--spacing) * 5)!important}.\!p-6{padding:calc(var(--spacing) * 6)!important}.\!p-8{padding:calc(var(--spacing) * 8)!important}.\!p-10{padding:calc(var(--spacing) * 10)!important}.\!p-12{padding:calc(var(--spacing) * 12)!important}.\!p-16{padding:calc(var(--spacing) * 16)!important}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.p-12{padding:calc(var(--spacing) * 12)}.p-16{padding:calc(var(--spacing) * 16)}.p-20{padding:calc(var(--spacing) * 20)}.p-24{padding:calc(var(--spacing) * 24)}.\!px-0{padding-inline:0!important}.\!px-1{padding-inline:var(--spacing)!important}.\!px-2{padding-inline:calc(var(--spacing) * 2)!important}.\!px-3{padding-inline:calc(var(--spacing) * 3)!important}.\!px-4{padding-inline:calc(var(--spacing) * 4)!important}.\!px-5{padding-inline:calc(var(--spacing) * 5)!important}.\!px-6{padding-inline:calc(var(--spacing) * 6)!important}.\!px-8{padding-inline:calc(var(--spacing) * 8)!important}.\!px-10{padding-inline:calc(var(--spacing) * 10)!important}.\!px-12{padding-inline:calc(var(--spacing) * 12)!important}.\!px-16{padding-inline:calc(var(--spacing) * 16)!important}.px-0{padding-inline:0}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-12{padding-inline:calc(var(--spacing) * 12)}.px-16{padding-inline:calc(var(--spacing) * 16)}.\!py-0{padding-block:0!important}.\!py-1{padding-block:var(--spacing)!important}.\!py-2{padding-block:calc(var(--spacing) * 2)!important}.\!py-3{padding-block:calc(var(--spacing) * 3)!important}.\!py-4{padding-block:calc(var(--spacing) * 4)!important}.\!py-5{padding-block:calc(var(--spacing) * 5)!important}.\!py-6{padding-block:calc(var(--spacing) * 6)!important}.\!py-8{padding-block:calc(var(--spacing) * 8)!important}.\!py-10{padding-block:calc(var(--spacing) * 10)!important}.\!py-12{padding-block:calc(var(--spacing) * 12)!important}.\!py-16{padding-block:calc(var(--spacing) * 16)!important}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:0}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pr-0{padding-right:0}.pr-1{padding-right:var(--spacing)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pr-10{padding-right:calc(var(--spacing) * 10)}.pr-12{padding-right:calc(var(--spacing) * 12)}.pr-16{padding-right:calc(var(--spacing) * 16)}.pb-0{padding-bottom:0}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-0{padding-left:0}.pl-1{padding-left:var(--spacing)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-7{padding-left:calc(var(--spacing) * 7)}.pl-8{padding-left:calc(var(--spacing) * 8)}.pl-10{padding-left:calc(var(--spacing) * 10)}.pl-12{padding-left:calc(var(--spacing) * 12)}.pl-16{padding-left:calc(var(--spacing) * 16)}.\!text-center{text-align:center!important}.\!text-justify{text-align:justify!important}.\!text-left{text-align:left!important}.\!text-right{text-align:right!important}.text-center{text-align:center}.text-end{text-align:end}.text-justify{text-align:justify}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.indent-0{text-indent:0}.indent-1{text-indent:var(--spacing)}.indent-2{text-indent:calc(var(--spacing) * 2)}.indent-4{text-indent:calc(var(--spacing) * 4)}.indent-8{text-indent:calc(var(--spacing) * 8)}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.align-text-bottom{vertical-align:text-bottom}.align-text-top{vertical-align:text-top}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.\!text-2xl{font-size:var(--text-2xl)!important;line-height:var(--tw-leading,var(--text-2xl--line-height))!important}.\!text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.\!text-4xl{font-size:var(--text-4xl)!important;line-height:var(--tw-leading,var(--text-4xl--line-height))!important}.\!text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.\!text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.\!text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.\!text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.\!text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[2rem\]{font-size:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-3{--tw-leading:calc(var(--spacing) * 3);line-height:calc(var(--spacing) * 3)}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-8{--tw-leading:calc(var(--spacing) * 8);line-height:calc(var(--spacing) * 8)}.leading-9{--tw-leading:calc(var(--spacing) * 9);line-height:calc(var(--spacing) * 9)}.leading-10{--tw-leading:calc(var(--spacing) * 10);line-height:calc(var(--spacing) * 10)}.leading-loose{--tw-leading:var(--leading-loose);line-height:var(--leading-loose)}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.\!font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.\!font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.\!font-normal{--tw-font-weight:var(--font-weight-normal)!important;font-weight:var(--font-weight-normal)!important}.\!font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-extralight{--tw-font-weight:var(--font-weight-extralight);font-weight:var(--font-weight-extralight)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-thin{--tw-font-weight:var(--font-weight-thin);font-weight:var(--font-weight-thin)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-50{color:var(--color-amber-50)}.text-amber-100{color:var(--color-amber-100)}.text-amber-200{color:var(--color-amber-200)}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-950{color:var(--color-amber-950)}.text-black{color:var(--color-black)}.text-blue-50{color:var(--color-blue-50)}.text-blue-100{color:var(--color-blue-100)}.text-blue-200{color:var(--color-blue-200)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-blue-950{color:var(--color-blue-950)}.text-current{color:currentColor}.text-cyan-50{color:var(--color-cyan-50)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-200{color:var(--color-cyan-200)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-500{color:var(--color-cyan-500)}.text-cyan-600{color:var(--color-cyan-600)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-800{color:var(--color-cyan-800)}.text-cyan-900{color:var(--color-cyan-900)}.text-cyan-950{color:var(--color-cyan-950)}.text-emerald-50{color:var(--color-emerald-50)}.text-emerald-100{color:var(--color-emerald-100)}.text-emerald-200{color:var(--color-emerald-200)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-fuchsia-50{color:var(--color-fuchsia-50)}.text-fuchsia-100{color:var(--color-fuchsia-100)}.text-fuchsia-200{color:var(--color-fuchsia-200)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-fuchsia-500{color:var(--color-fuchsia-500)}.text-fuchsia-600{color:var(--color-fuchsia-600)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-fuchsia-800{color:var(--color-fuchsia-800)}.text-fuchsia-900{color:var(--color-fuchsia-900)}.text-fuchsia-950{color:var(--color-fuchsia-950)}.text-gray-50{color:var(--color-gray-50)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-gray-950{color:var(--color-gray-950)}.text-green-50{color:var(--color-green-50)}.text-green-100{color:var(--color-green-100)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-green-900{color:var(--color-green-900)}.text-green-950{color:var(--color-green-950)}.text-indigo-50{color:var(--color-indigo-50)}.text-indigo-100{color:var(--color-indigo-100)}.text-indigo-200{color:var(--color-indigo-200)}.text-indigo-300{color:var(--color-indigo-300)}.text-indigo-400{color:var(--color-indigo-400)}.text-indigo-500{color:var(--color-indigo-500)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-indigo-800{color:var(--color-indigo-800)}.text-indigo-900{color:var(--color-indigo-900)}.text-indigo-950{color:var(--color-indigo-950)}.text-inherit{color:inherit}.text-lime-50{color:var(--color-lime-50)}.text-lime-100{color:var(--color-lime-100)}.text-lime-200{color:var(--color-lime-200)}.text-lime-300{color:var(--color-lime-300)}.text-lime-400{color:var(--color-lime-400)}.text-lime-500{color:var(--color-lime-500)}.text-lime-600{color:var(--color-lime-600)}.text-lime-700{color:var(--color-lime-700)}.text-lime-800{color:var(--color-lime-800)}.text-lime-900{color:var(--color-lime-900)}.text-lime-950{color:var(--color-lime-950)}.text-neutral-50{color:var(--color-neutral-50)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-orange-50{color:var(--color-orange-50)}.text-orange-100{color:var(--color-orange-100)}.text-orange-200{color:var(--color-orange-200)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-orange-900{color:var(--color-orange-900)}.text-orange-950{color:var(--color-orange-950)}.text-pink-50{color:var(--color-pink-50)}.text-pink-100{color:var(--color-pink-100)}.text-pink-200{color:var(--color-pink-200)}.text-pink-300{color:var(--color-pink-300)}.text-pink-400{color:var(--color-pink-400)}.text-pink-500{color:var(--color-pink-500)}.text-pink-600{color:var(--color-pink-600)}.text-pink-700{color:var(--color-pink-700)}.text-pink-800{color:var(--color-pink-800)}.text-pink-900{color:var(--color-pink-900)}.text-pink-950{color:var(--color-pink-950)}.text-purple-50{color:var(--color-purple-50)}.text-purple-100{color:var(--color-purple-100)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-purple-900{color:var(--color-purple-900)}.text-purple-950{color:var(--color-purple-950)}.text-red-50{color:var(--color-red-50)}.text-red-100{color:var(--color-red-100)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-50{color:var(--color-rose-50)}.text-rose-100{color:var(--color-rose-100)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-50{color:var(--color-sky-50)}.text-sky-100{color:var(--color-sky-100)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-900{color:var(--color-sky-900)}.text-sky-950{color:var(--color-sky-950)}.text-slate-50{color:var(--color-slate-50)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-stone-50{color:var(--color-stone-50)}.text-stone-100{color:var(--color-stone-100)}.text-stone-200{color:var(--color-stone-200)}.text-stone-300{color:var(--color-stone-300)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.text-stone-800{color:var(--color-stone-800)}.text-stone-900{color:var(--color-stone-900)}.text-stone-950{color:var(--color-stone-950)}.text-teal-50{color:var(--color-teal-50)}.text-teal-100{color:var(--color-teal-100)}.text-teal-200{color:var(--color-teal-200)}.text-teal-300{color:var(--color-teal-300)}.text-teal-400{color:var(--color-teal-400)}.text-teal-500{color:var(--color-teal-500)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-900{color:var(--color-teal-900)}.text-teal-950{color:var(--color-teal-950)}.text-transparent{color:#0000}.text-violet-50{color:var(--color-violet-50)}.text-violet-100{color:var(--color-violet-100)}.text-violet-200{color:var(--color-violet-200)}.text-violet-300{color:var(--color-violet-300)}.text-violet-400{color:var(--color-violet-400)}.text-violet-500{color:var(--color-violet-500)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-violet-900{color:var(--color-violet-900)}.text-violet-950{color:var(--color-violet-950)}.text-white{color:var(--color-white)}.text-yellow-50{color:var(--color-yellow-50)}.text-yellow-100{color:var(--color-yellow-100)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-900{color:var(--color-yellow-900)}.text-yellow-950{color:var(--color-yellow-950)}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.text-zinc-950{color:var(--color-zinc-950)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.placeholder-gray-400::placeholder{color:var(--color-gray-400)}.accent-blue-500{accent-color:var(--color-blue-500)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-15{opacity:.15}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.bg-blend-color{background-blend-mode:color}.bg-blend-color-burn{background-blend-mode:color-burn}.bg-blend-color-dodge{background-blend-mode:color-dodge}.bg-blend-darken{background-blend-mode:darken}.bg-blend-difference{background-blend-mode:difference}.bg-blend-exclusion{background-blend-mode:exclusion}.bg-blend-hard-light{background-blend-mode:hard-light}.bg-blend-hue{background-blend-mode:hue}.bg-blend-lighten{background-blend-mode:lighten}.bg-blend-luminosity{background-blend-mode:luminosity}.bg-blend-multiply{background-blend-mode:multiply}.bg-blend-normal{background-blend-mode:normal}.bg-blend-overlay{background-blend-mode:overlay}.bg-blend-saturation{background-blend-mode:saturation}.bg-blend-screen{background-blend-mode:screen}.bg-blend-soft-light{background-blend-mode:soft-light}.mix-blend-color{mix-blend-mode:color}.mix-blend-color-burn{mix-blend-mode:color-burn}.mix-blend-color-dodge{mix-blend-mode:color-dodge}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-difference{mix-blend-mode:difference}.mix-blend-exclusion{mix-blend-mode:exclusion}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-hue{mix-blend-mode:hue}.mix-blend-lighten{mix-blend-mode:lighten}.mix-blend-luminosity{mix-blend-mode:luminosity}.mix-blend-multiply{mix-blend-mode:multiply}.mix-blend-normal{mix-blend-mode:normal}.mix-blend-overlay{mix-blend-mode:overlay}.mix-blend-plus-darker{mix-blend-mode:plus-darker}.mix-blend-plus-lighter{mix-blend-mode:plus-lighter}.mix-blend-saturation{mix-blend-mode:saturation}.mix-blend-screen{mix-blend-mode:screen}.mix-blend-soft-light{mix-blend-mode:soft-light}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-8{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-purple-200{--tw-shadow-color:oklch(90.2% .063 306.703)}@supports (color:color-mix(in lab,red,red)){.shadow-purple-200{--tw-shadow-color:color-mix(in oklab, var(--color-purple-200) var(--tw-shadow-alpha), transparent)}}.ring-black{--tw-ring-color:var(--color-black)}.ring-blue-50{--tw-ring-color:var(--color-blue-50)}.ring-blue-100{--tw-ring-color:var(--color-blue-100)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-blue-300{--tw-ring-color:var(--color-blue-300)}.ring-blue-400{--tw-ring-color:var(--color-blue-400)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-blue-600{--tw-ring-color:var(--color-blue-600)}.ring-blue-700{--tw-ring-color:var(--color-blue-700)}.ring-blue-800{--tw-ring-color:var(--color-blue-800)}.ring-blue-900{--tw-ring-color:var(--color-blue-900)}.ring-current{--tw-ring-color:currentcolor}.ring-gray-50{--tw-ring-color:var(--color-gray-50)}.ring-gray-100{--tw-ring-color:var(--color-gray-100)}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-gray-400{--tw-ring-color:var(--color-gray-400)}.ring-gray-500{--tw-ring-color:var(--color-gray-500)}.ring-gray-600{--tw-ring-color:var(--color-gray-600)}.ring-gray-700{--tw-ring-color:var(--color-gray-700)}.ring-gray-800{--tw-ring-color:var(--color-gray-800)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-inherit{--tw-ring-color:inherit}.ring-slate-50{--tw-ring-color:var(--color-slate-50)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-slate-300{--tw-ring-color:var(--color-slate-300)}.ring-slate-400{--tw-ring-color:var(--color-slate-400)}.ring-slate-500{--tw-ring-color:var(--color-slate-500)}.ring-slate-600{--tw-ring-color:var(--color-slate-600)}.ring-slate-700{--tw-ring-color:var(--color-slate-700)}.ring-slate-800{--tw-ring-color:var(--color-slate-800)}.ring-slate-900{--tw-ring-color:var(--color-slate-900)}.ring-transparent{--tw-ring-color:transparent}.ring-white{--tw-ring-color:var(--color-white)}.ring-offset-0{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-1{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-4{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-8{--tw-ring-offset-width:8px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-black{--tw-ring-offset-color:var(--color-black)}.ring-offset-current{--tw-ring-offset-color:currentcolor}.ring-offset-inherit{--tw-ring-offset-color:inherit}.ring-offset-transparent{--tw-ring-offset-color:transparent}.ring-offset-white{--tw-ring-offset-color:var(--color-white)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-0{outline-style:var(--tw-outline-style);outline-width:0}.outline-1{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-4{outline-style:var(--tw-outline-style);outline-width:4px}.outline-8{outline-style:var(--tw-outline-style);outline-width:8px}.outline-offset-0{outline-offset:0px}.outline-offset-1{outline-offset:1px}.outline-offset-2{outline-offset:2px}.outline-offset-4{outline-offset:4px}.outline-offset-8{outline-offset:8px}.outline-black{outline-color:var(--color-black)}.outline-current{outline-color:currentColor}.outline-inherit{outline-color:inherit}.outline-transparent{outline-color:#0000}.outline-white{outline-color:var(--color-white)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-lg{--tw-blur:blur(var(--blur-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-md{--tw-blur:blur(var(--blur-md));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-none{--tw-blur: ;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-sm{--tw-blur:blur(var(--blur-sm));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-0{--tw-brightness:brightness(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-50{--tw-brightness:brightness(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-75{--tw-brightness:brightness(75%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-90{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-95{--tw-brightness:brightness(95%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-100{--tw-brightness:brightness(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-105{--tw-brightness:brightness(105%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-125{--tw-brightness:brightness(125%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-150{--tw-brightness:brightness(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-200{--tw-brightness:brightness(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-0{--tw-contrast:contrast(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-50{--tw-contrast:contrast(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-75{--tw-contrast:contrast(75%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-100{--tw-contrast:contrast(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-125{--tw-contrast:contrast(125%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-150{--tw-contrast:contrast(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-200{--tw-contrast:contrast(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size:drop-shadow(0 3px 3px var(--tw-drop-shadow-color,#0000001f));--tw-drop-shadow:drop-shadow(var(--drop-shadow-md));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-sm{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-sm));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-xl{--tw-drop-shadow-size:drop-shadow(0 9px 7px var(--tw-drop-shadow-color,#0000001a));--tw-drop-shadow:drop-shadow(var(--drop-shadow-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-none{--tw-drop-shadow: ;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale-0{--tw-grayscale:grayscale(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-15{--tw-hue-rotate:hue-rotate( -15deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-30{--tw-hue-rotate:hue-rotate( -30deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-60{--tw-hue-rotate:hue-rotate( -60deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-90{--tw-hue-rotate:hue-rotate( -90deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-180{--tw-hue-rotate:hue-rotate( -180deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-0{--tw-hue-rotate:hue-rotate(0deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-15{--tw-hue-rotate:hue-rotate(15deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-30{--tw-hue-rotate:hue-rotate(30deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-60{--tw-hue-rotate:hue-rotate(60deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-90{--tw-hue-rotate:hue-rotate(90deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-180{--tw-hue-rotate:hue-rotate(180deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert-0{--tw-invert:invert(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-0{--tw-saturate:saturate(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-50{--tw-saturate:saturate(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-100{--tw-saturate:saturate(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-150{--tw-saturate:saturate(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-200{--tw-saturate:saturate(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.sepia{--tw-sepia:sepia(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.sepia-0{--tw-sepia:sepia(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(var(--blur-3xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-none{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-0{--tw-backdrop-brightness:brightness(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-50{--tw-backdrop-brightness:brightness(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-75{--tw-backdrop-brightness:brightness(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-90{--tw-backdrop-brightness:brightness(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-95{--tw-backdrop-brightness:brightness(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-100{--tw-backdrop-brightness:brightness(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-105{--tw-backdrop-brightness:brightness(105%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-110{--tw-backdrop-brightness:brightness(110%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-125{--tw-backdrop-brightness:brightness(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-150{--tw-backdrop-brightness:brightness(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-200{--tw-backdrop-brightness:brightness(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-0{--tw-backdrop-contrast:contrast(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-50{--tw-backdrop-contrast:contrast(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-75{--tw-backdrop-contrast:contrast(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-100{--tw-backdrop-contrast:contrast(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-125{--tw-backdrop-contrast:contrast(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-150{--tw-backdrop-contrast:contrast(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-200{--tw-backdrop-contrast:contrast(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale-0{--tw-backdrop-grayscale:grayscale(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-0{--tw-backdrop-hue-rotate:hue-rotate(0deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-15{--tw-backdrop-hue-rotate:hue-rotate(15deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-30{--tw-backdrop-hue-rotate:hue-rotate(30deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-60{--tw-backdrop-hue-rotate:hue-rotate(60deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-90{--tw-backdrop-hue-rotate:hue-rotate(90deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-180{--tw-backdrop-hue-rotate:hue-rotate(180deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert-0{--tw-backdrop-invert:invert(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-0{--tw-backdrop-opacity:opacity(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-5{--tw-backdrop-opacity:opacity(5%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-10{--tw-backdrop-opacity:opacity(10%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-20{--tw-backdrop-opacity:opacity(20%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-25{--tw-backdrop-opacity:opacity(25%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-30{--tw-backdrop-opacity:opacity(30%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-40{--tw-backdrop-opacity:opacity(40%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-50{--tw-backdrop-opacity:opacity(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-60{--tw-backdrop-opacity:opacity(60%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-70{--tw-backdrop-opacity:opacity(70%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-75{--tw-backdrop-opacity:opacity(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-80{--tw-backdrop-opacity:opacity(80%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-90{--tw-backdrop-opacity:opacity(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-95{--tw-backdrop-opacity:opacity(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-100{--tw-backdrop-opacity:opacity(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-0{--tw-backdrop-saturate:saturate(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-50{--tw-backdrop-saturate:saturate(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-100{--tw-backdrop-saturate:saturate(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-150{--tw-backdrop-saturate:saturate(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-200{--tw-backdrop-saturate:saturate(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia-0{--tw-backdrop-sepia:sepia(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.delay-100{transition-delay:.1s}.delay-150{transition-delay:.15s}.delay-200{transition-delay:.2s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.delay-1000{transition-delay:1s}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.duration-1000{--tw-duration:1s;transition-duration:1s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-dashed{--tw-outline-style:dashed;outline-style:dashed}.outline-dotted{--tw-outline-style:dotted;outline-style:dotted}.outline-double{--tw-outline-style:double;outline-style:double}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;user-select:auto}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.ring-inset{--tw-ring-inset:inset}@media(hover:hover){.group-hover\:visible:is(:where(.group):hover *){visibility:visible}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:scale-100:is(:where(.group):hover *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:#0006}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.group-hover\:bg-blue-100:is(:where(.group):hover *){background-color:var(--color-blue-100)}.group-hover\:bg-blue-500:is(:where(.group):hover *){background-color:var(--color-blue-500)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:bg-gray-100:is(:where(.group):hover *){background-color:var(--color-gray-100)}.group-hover\:bg-gray-200:is(:where(.group):hover *){background-color:var(--color-gray-200)}.group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-blue-500:is(:where(.group):hover *){color:var(--color-blue-500)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-blue-700:is(:where(.group):hover *){color:var(--color-blue-700)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:text-gray-800:is(:where(.group):hover *){color:var(--color-gray-800)}.group-hover\:text-gray-900:is(:where(.group):hover *){color:var(--color-gray-900)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.group-focus\:border-blue-500:is(:where(.group):focus *){border-color:var(--color-blue-500)}.group-focus\:ring-2:is(:where(.group):focus *){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-checked\:border-blue-500:is(:where(.peer):checked~*){border-color:var(--color-blue-500)}.peer-checked\:bg-blue-500:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.peer-invalid\:border-red-500:is(:where(.peer):invalid~*){border-color:var(--color-red-500)}.peer-invalid\:text-red-500:is(:where(.peer):invalid~*){color:var(--color-red-500)}.peer-focus\:border-blue-500:is(:where(.peer):focus~*){border-color:var(--color-blue-500)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.first-letter\:text-2xl:first-letter{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.first-letter\:font-bold:first-letter{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.first-letter\:text-blue-600:first-letter{color:var(--color-blue-600)}.first-line\:font-semibold:first-line{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.first-line\:text-gray-700:first-line{color:var(--color-gray-700)}.marker\:text-blue-500 ::marker{color:var(--color-blue-500)}.marker\:text-blue-500::marker{color:var(--color-blue-500)}.marker\:text-blue-500 ::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-blue-500::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-gray-500 ::marker{color:var(--color-gray-500)}.marker\:text-gray-500::marker{color:var(--color-gray-500)}.marker\:text-gray-500 ::-webkit-details-marker{color:var(--color-gray-500)}.marker\:text-gray-500::-webkit-details-marker{color:var(--color-gray-500)}.selection\:bg-blue-200 ::selection{background-color:var(--color-blue-200)}.selection\:bg-blue-200::selection{background-color:var(--color-blue-200)}.selection\:text-blue-900 ::selection{color:var(--color-blue-900)}.selection\:text-blue-900::selection{color:var(--color-blue-900)}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-full::file-selector-button{border-radius:3.40282e38px}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-blue-50::file-selector-button{background-color:var(--color-blue-50)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing) * 4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing) * 2)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-blue-700::file-selector-button{color:var(--color-blue-700)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:0}.before\:block:before{content:var(--tw-content);display:block}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:0}.after\:clear-both:after{content:var(--tw-content);clear:both}.after\:block:after{content:var(--tw-content);display:block}.after\:h-4:after{content:var(--tw-content);height:calc(var(--spacing) * 4)}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing) * 5)}.after\:h-6:after{content:var(--tw-content);height:calc(var(--spacing) * 6)}.after\:w-4:after{content:var(--tw-content);width:calc(var(--spacing) * 4)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing) * 5)}.after\:w-6:after{content:var(--tw-content);width:calc(var(--spacing) * 6)}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:mt-0:first-child{margin-top:0}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.first\:rounded-l:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.first\:rounded-l-lg:first-child{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:pt-0:first-child{padding-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:rounded-r:last-child{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.last\:rounded-r-lg:last-child{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.last\:border-r:last-child{border-right-style:var(--tw-border-style);border-right-width:1px}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:0}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.odd\:bg-gray-100:nth-child(odd){background-color:var(--color-gray-100)}.odd\:bg-white:nth-child(odd){background-color:var(--color-white)}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}.even\:bg-gray-100:nth-child(2n){background-color:var(--color-gray-100)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.checked\:border-blue-500:checked{border-color:var(--color-blue-500)}.checked\:border-blue-600:checked{border-color:var(--color-blue-600)}.checked\:bg-blue-500:checked{background-color:var(--color-blue-500)}.checked\:bg-blue-600:checked{background-color:var(--color-blue-600)}.checked\:bg-green-500:checked{background-color:var(--color-green-500)}.checked\:text-white:checked{color:var(--color-white)}.indeterminate\:border-gray-400:indeterminate{border-color:var(--color-gray-400)}.indeterminate\:bg-gray-300:indeterminate{background-color:var(--color-gray-300)}.placeholder-shown\:border-gray-300:placeholder-shown{border-color:var(--color-gray-300)}.autofill\:bg-yellow-100:autofill{background-color:var(--color-yellow-100)}.autofill\:text-black:autofill{color:var(--color-black)}.valid\:border-green-500:valid{border-color:var(--color-green-500)}.valid\:text-green-600:valid{color:var(--color-green-600)}.valid\:ring-green-500:valid{--tw-ring-color:var(--color-green-500)}.invalid\:border-red-500:invalid{border-color:var(--color-red-500)}.invalid\:text-red-600:invalid{color:var(--color-red-600)}.invalid\:ring-red-500:invalid{--tw-ring-color:var(--color-red-500)}.read-only\:cursor-not-allowed:read-only{cursor:not-allowed}.read-only\:bg-gray-50:read-only{background-color:var(--color-gray-50)}.read-only\:bg-gray-100:read-only{background-color:var(--color-gray-100)}.empty\:hidden:empty{display:none}.focus-within\:border-blue-500:focus-within{border-color:var(--color-blue-500)}.focus-within\:border-gray-500:focus-within{border-color:var(--color-gray-500)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-blue-500:focus-within{--tw-ring-color:var(--color-blue-500)}.focus-within\:ring-gray-500:focus-within{--tw-ring-color:var(--color-gray-500)}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:scale-95:hover{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-100:hover{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black) 10%,transparent)}}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-100:hover{background-color:var(--color-green-100)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-300:hover{background-color:var(--color-green-300)}.hover\:bg-green-400:hover{background-color:var(--color-green-400)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-green-800:hover{background-color:var(--color-green-800)}.hover\:bg-green-900:hover{background-color:var(--color-green-900)}.hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:bg-red-300:hover{background-color:var(--color-red-300)}.hover\:bg-red-400:hover{background-color:var(--color-red-400)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-yellow-50:hover{background-color:var(--color-yellow-50)}.hover\:bg-yellow-100:hover{background-color:var(--color-yellow-100)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.hover\:bg-yellow-900:hover{background-color:var(--color-yellow-900)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-blue-50:hover{color:var(--color-blue-50)}.hover\:text-blue-100:hover{color:var(--color-blue-100)}.hover\:text-blue-200:hover{color:var(--color-blue-200)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-blue-900:hover{color:var(--color-blue-900)}.hover\:text-gray-50:hover{color:var(--color-gray-50)}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-500:hover{color:var(--color-gray-500)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-800:hover{color:var(--color-gray-800)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-50:hover{color:var(--color-green-50)}.hover\:text-green-100:hover{color:var(--color-green-100)}.hover\:text-green-200:hover{color:var(--color-green-200)}.hover\:text-green-300:hover{color:var(--color-green-300)}.hover\:text-green-400:hover{color:var(--color-green-400)}.hover\:text-green-500:hover{color:var(--color-green-500)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-purple-50:hover{color:var(--color-purple-50)}.hover\:text-purple-100:hover{color:var(--color-purple-100)}.hover\:text-purple-200:hover{color:var(--color-purple-200)}.hover\:text-purple-300:hover{color:var(--color-purple-300)}.hover\:text-purple-400:hover{color:var(--color-purple-400)}.hover\:text-purple-500:hover{color:var(--color-purple-500)}.hover\:text-purple-600:hover{color:var(--color-purple-600)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-800:hover{color:var(--color-purple-800)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-50:hover{color:var(--color-red-50)}.hover\:text-red-100:hover{color:var(--color-red-100)}.hover\:text-red-200:hover{color:var(--color-red-200)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:text-slate-50:hover{color:var(--color-slate-50)}.hover\:text-slate-100:hover{color:var(--color-slate-100)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}.hover\:text-slate-300:hover{color:var(--color-slate-300)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-slate-500:hover{color:var(--color-slate-500)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-0:hover{opacity:0}.hover\:opacity-25:hover{opacity:.25}.hover\:opacity-50:hover{opacity:.5}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-100:hover{opacity:1}.hover\:shadow:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-none:hover{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-0:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-1:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-4:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-8:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}.hover\:ring-gray-500:hover{--tw-ring-color:var(--color-gray-500)}.hover\:ring-green-500:hover{--tw-ring-color:var(--color-green-500)}.hover\:ring-red-500:hover{--tw-ring-color:var(--color-red-500)}.file\:hover\:bg-blue-100::file-selector-button:hover{background-color:var(--color-blue-100)}}.focus\:border-blue-50:focus{border-color:var(--color-blue-50)}.focus\:border-blue-100:focus{border-color:var(--color-blue-100)}.focus\:border-blue-200:focus{border-color:var(--color-blue-200)}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.focus\:border-blue-900:focus{border-color:var(--color-blue-900)}.focus\:border-gray-50:focus{border-color:var(--color-gray-50)}.focus\:border-gray-100:focus{border-color:var(--color-gray-100)}.focus\:border-gray-200:focus{border-color:var(--color-gray-200)}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:border-gray-400:focus{border-color:var(--color-gray-400)}.focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.focus\:border-gray-800:focus{border-color:var(--color-gray-800)}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-green-500:focus{border-color:var(--color-green-500)}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:border-red-500:focus{border-color:var(--color-red-500)}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-yellow-500:focus{border-color:var(--color-yellow-500)}.focus\:bg-black:focus{background-color:var(--color-black)}.focus\:bg-blue-50:focus{background-color:var(--color-blue-50)}.focus\:bg-blue-100:focus{background-color:var(--color-blue-100)}.focus\:bg-blue-200:focus{background-color:var(--color-blue-200)}.focus\:bg-blue-300:focus{background-color:var(--color-blue-300)}.focus\:bg-blue-400:focus{background-color:var(--color-blue-400)}.focus\:bg-blue-500:focus{background-color:var(--color-blue-500)}.focus\:bg-blue-600:focus{background-color:var(--color-blue-600)}.focus\:bg-blue-700:focus{background-color:var(--color-blue-700)}.focus\:bg-blue-800:focus{background-color:var(--color-blue-800)}.focus\:bg-blue-900:focus{background-color:var(--color-blue-900)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:bg-gray-100:focus{background-color:var(--color-gray-100)}.focus\:bg-gray-200:focus{background-color:var(--color-gray-200)}.focus\:bg-gray-300:focus{background-color:var(--color-gray-300)}.focus\:bg-gray-400:focus{background-color:var(--color-gray-400)}.focus\:bg-gray-500:focus{background-color:var(--color-gray-500)}.focus\:bg-gray-600:focus{background-color:var(--color-gray-600)}.focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.focus\:bg-transparent:focus{background-color:#0000}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:text-black:focus{color:var(--color-black)}.focus\:text-blue-50:focus{color:var(--color-blue-50)}.focus\:text-blue-100:focus{color:var(--color-blue-100)}.focus\:text-blue-200:focus{color:var(--color-blue-200)}.focus\:text-blue-300:focus{color:var(--color-blue-300)}.focus\:text-blue-400:focus{color:var(--color-blue-400)}.focus\:text-blue-500:focus{color:var(--color-blue-500)}.focus\:text-blue-600:focus{color:var(--color-blue-600)}.focus\:text-blue-700:focus{color:var(--color-blue-700)}.focus\:text-blue-800:focus{color:var(--color-blue-800)}.focus\:text-blue-900:focus{color:var(--color-blue-900)}.focus\:text-gray-50:focus{color:var(--color-gray-50)}.focus\:text-gray-100:focus{color:var(--color-gray-100)}.focus\:text-gray-200:focus{color:var(--color-gray-200)}.focus\:text-gray-300:focus{color:var(--color-gray-300)}.focus\:text-gray-400:focus{color:var(--color-gray-400)}.focus\:text-gray-500:focus{color:var(--color-gray-500)}.focus\:text-gray-600:focus{color:var(--color-gray-600)}.focus\:text-gray-700:focus{color:var(--color-gray-700)}.focus\:text-gray-800:focus{color:var(--color-gray-800)}.focus\:text-gray-900:focus{color:var(--color-gray-900)}.focus\:text-white:focus{color:var(--color-white)}.focus\:shadow:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-md:focus{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-sm:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-xl:focus{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-8:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.focus\:ring-blue-50:focus{--tw-ring-color:var(--color-blue-50)}.focus\:ring-blue-100:focus{--tw-ring-color:var(--color-blue-100)}.focus\:ring-blue-200:focus{--tw-ring-color:var(--color-blue-200)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.focus\:ring-blue-800:focus{--tw-ring-color:var(--color-blue-800)}.focus\:ring-blue-900:focus{--tw-ring-color:var(--color-blue-900)}.focus\:ring-current:focus{--tw-ring-color:currentcolor}.focus\:ring-gray-50:focus{--tw-ring-color:var(--color-gray-50)}.focus\:ring-gray-100:focus{--tw-ring-color:var(--color-gray-100)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:ring-gray-300:focus{--tw-ring-color:var(--color-gray-300)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.focus\:ring-gray-800:focus{--tw-ring-color:var(--color-gray-800)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-slate-400:focus{--tw-ring-color:var(--color-slate-400)}.focus\:ring-transparent:focus{--tw-ring-color:transparent}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-1:focus{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-4:focus{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:var(--color-gray-100)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:ring-offset-white:focus{--tw-ring-offset-color:var(--color-white)}.focus\:outline:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-1:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-4:focus{outline-style:var(--tw-outline-style);outline-width:4px}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-gray-500:focus-visible{--tw-ring-color:var(--color-gray-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-100:active{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.active\:bg-blue-600:active{background-color:var(--color-blue-600)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.active\:bg-blue-800:active{background-color:var(--color-blue-800)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:bg-gray-200:active{background-color:var(--color-gray-200)}.active\:bg-gray-300:active{background-color:var(--color-gray-300)}.active\:bg-gray-700:active{background-color:var(--color-gray-700)}.active\:bg-gray-800:active{background-color:var(--color-gray-800)}.active\:bg-green-600:active{background-color:var(--color-green-600)}.active\:bg-green-700:active{background-color:var(--color-green-700)}.active\:bg-red-600:active{background-color:var(--color-red-600)}.active\:bg-red-700:active{background-color:var(--color-red-700)}.active\:opacity-75:active{opacity:.75}.active\:opacity-100:active{opacity:1}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:border-gray-200:disabled{border-color:var(--color-gray-200)}.disabled\:border-gray-300:disabled{border-color:var(--color-gray-300)}.disabled\:bg-blue-400:disabled{background-color:var(--color-blue-400)}.disabled\:bg-gray-100:disabled{background-color:var(--color-gray-100)}.disabled\:bg-gray-200:disabled{background-color:var(--color-gray-200)}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:bg-gray-400:disabled{background-color:var(--color-gray-400)}.disabled\:text-gray-300:disabled{color:var(--color-gray-300)}.disabled\:text-gray-400:disabled{color:var(--color-gray-400)}.disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.disabled\:no-underline:disabled{text-decoration-line:none}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}@media(hover:hover){.disabled\:hover\:bg-indigo-600:disabled:hover{background-color:var(--color-indigo-600)}.disabled\:hover\:bg-transparent:disabled:hover{background-color:#0000}}@media(min-width:40rem){.sm\:col-span-1{grid-column:span 1/span 1}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-12{grid-column:span 12/span 12}.sm\:col-span-full{grid-column:1/-1}.sm\:m-0{margin:0}.sm\:m-1{margin:var(--spacing)}.sm\:m-2{margin:calc(var(--spacing) * 2)}.sm\:m-3{margin:calc(var(--spacing) * 3)}.sm\:m-4{margin:calc(var(--spacing) * 4)}.sm\:m-auto{margin:auto}.sm\:mx-auto{margin-inline:auto}.sm\:my-auto{margin-block:auto}.sm\:mt-6{margin-top:calc(var(--spacing) * 6)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-block{display:inline-block}.sm\:inline-flex{display:inline-flex}.sm\:w-1\/2{width:50%}.sm\:w-1\/3{width:33.3333%}.sm\:w-1\/4{width:25%}.sm\:w-2\/3{width:66.6667%}.sm\:w-3\/4{width:75%}.sm\:w-40{width:calc(var(--spacing) * 40)}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-center{justify-content:center}.sm\:justify-end{justify-content:flex-end}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:0}.sm\:gap-1{gap:var(--spacing)}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-5{gap:calc(var(--spacing) * 5)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.sm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline:0}:where(.sm\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(var(--spacing) * var(--tw-space-x-reverse));margin-inline-end:calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.sm\:p-0{padding:0}.sm\:p-1{padding:var(--spacing)}.sm\:p-2{padding:calc(var(--spacing) * 2)}.sm\:p-3{padding:calc(var(--spacing) * 3)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-0{padding-inline:0}.sm\:px-1{padding-inline:var(--spacing)}.sm\:px-2{padding-inline:calc(var(--spacing) * 2)}.sm\:px-3{padding-inline:calc(var(--spacing) * 3)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0{padding-block:0}.sm\:py-1{padding-block:var(--spacing)}.sm\:py-2{padding-block:calc(var(--spacing) * 2)}.sm\:py-3{padding-block:calc(var(--spacing) * 3)}.sm\:py-4{padding-block:calc(var(--spacing) * 4)}.sm\:py-5{padding-block:calc(var(--spacing) * 5)}.sm\:py-6{padding-block:calc(var(--spacing) * 6)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:48rem){.md\:absolute{position:absolute}.md\:fixed{position:fixed}.md\:relative{position:relative}.md\:static{position:static}.md\:sticky{position:sticky}.md\:inset-0{inset:0}.md\:top-0{top:0}.md\:right-0{right:0}.md\:bottom-0{bottom:0}.md\:left-0{left:0}.md\:order-1{order:1}.md\:order-2{order:2}.md\:order-3{order:3}.md\:order-first{order:-9999}.md\:order-last{order:9999}.md\:order-none{order:0}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-12{grid-column:span 12/span 12}.md\:col-span-full{grid-column:1/-1}.md\:row-span-1{grid-row:span 1/span 1}.md\:row-span-2{grid-row:span 2/span 2}.md\:row-span-3{grid-row:span 3/span 3}.md\:row-span-4{grid-row:span 4/span 4}.md\:row-span-full{grid-row:1/-1}.md\:m-0{margin:0}.md\:m-1{margin:var(--spacing)}.md\:m-2{margin:calc(var(--spacing) * 2)}.md\:m-3{margin:calc(var(--spacing) * 3)}.md\:m-4{margin:calc(var(--spacing) * 4)}.md\:m-auto{margin:auto}.md\:mx-auto{margin-inline:auto}.md\:my-auto{margin-block:auto}.md\:mt-0{margin-top:0}.md\:mt-1{margin-top:var(--spacing)}.md\:mt-2{margin-top:calc(var(--spacing) * 2)}.md\:mt-3{margin-top:calc(var(--spacing) * 3)}.md\:mt-4{margin-top:calc(var(--spacing) * 4)}.md\:mt-6{margin-top:calc(var(--spacing) * 6)}.md\:mt-8{margin-top:calc(var(--spacing) * 8)}.md\:mr-0{margin-right:0}.md\:mr-1{margin-right:var(--spacing)}.md\:mr-2{margin-right:calc(var(--spacing) * 2)}.md\:mr-3{margin-right:calc(var(--spacing) * 3)}.md\:mr-4{margin-right:calc(var(--spacing) * 4)}.md\:mr-6{margin-right:calc(var(--spacing) * 6)}.md\:mr-8{margin-right:calc(var(--spacing) * 8)}.md\:mr-auto{margin-right:auto}.md\:mb-0{margin-bottom:0}.md\:mb-1{margin-bottom:var(--spacing)}.md\:mb-2{margin-bottom:calc(var(--spacing) * 2)}.md\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.md\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.md\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.md\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.md\:ml-0{margin-left:0}.md\:ml-1{margin-left:var(--spacing)}.md\:ml-2{margin-left:calc(var(--spacing) * 2)}.md\:ml-3{margin-left:calc(var(--spacing) * 3)}.md\:ml-4{margin-left:calc(var(--spacing) * 4)}.md\:ml-6{margin-left:calc(var(--spacing) * 6)}.md\:ml-8{margin-left:calc(var(--spacing) * 8)}.md\:ml-auto{margin-left:auto}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-block{display:inline-block}.md\:inline-flex{display:inline-flex}.md\:h-20{height:calc(var(--spacing) * 20)}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.3333%}.md\:w-1\/4{width:25%}.md\:w-1\/5{width:20%}.md\:w-2\/3{width:66.6667%}.md\:w-2\/5{width:40%}.md\:w-3\/4{width:75%}.md\:w-3\/5{width:60%}.md\:w-4\/5{width:80%}.md\:w-20{width:calc(var(--spacing) * 20)}.md\:w-40{width:calc(var(--spacing) * 40)}.md\:w-48{width:calc(var(--spacing) * 48)}.md\:w-64{width:calc(var(--spacing) * 64)}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:w-80{width:calc(var(--spacing) * 80)}.md\:w-auto{width:auto}.md\:w-full{width:100%}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-4xl{max-width:var(--container-4xl)}.md\:max-w-5xl{max-width:var(--container-5xl)}.md\:max-w-6xl{max-width:var(--container-6xl)}.md\:max-w-7xl{max-width:var(--container-7xl)}.md\:max-w-full{max-width:100%}.md\:max-w-lg{max-width:var(--container-lg)}.md\:max-w-md{max-width:var(--container-md)}.md\:max-w-none{max-width:none}.md\:max-w-sm{max-width:var(--container-sm)}.md\:max-w-xl{max-width:var(--container-xl)}.md\:max-w-xs{max-width:var(--container-xs)}.md\:flex-1{flex:1}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.md\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.md\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.md\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:flex-row{flex-direction:row}.md\:items-baseline{align-items:baseline}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:items-stretch{align-items:stretch}.md\:justify-around{justify-content:space-around}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}.md\:justify-end{justify-content:flex-end}.md\:justify-start{justify-content:flex-start}.md\:gap-0{gap:0}.md\:gap-1{gap:var(--spacing)}.md\:gap-2{gap:calc(var(--spacing) * 2)}.md\:gap-3{gap:calc(var(--spacing) * 3)}.md\:gap-4{gap:calc(var(--spacing) * 4)}.md\:gap-5{gap:calc(var(--spacing) * 5)}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:gap-8{gap:calc(var(--spacing) * 8)}.md\:gap-10{gap:calc(var(--spacing) * 10)}.md\:gap-12{gap:calc(var(--spacing) * 12)}:where(.md\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.md\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline:0}:where(.md\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(var(--spacing) * var(--tw-space-x-reverse));margin-inline-end:calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.md\:rounded-2xl{border-radius:var(--radius-2xl)}.md\:p-0{padding:0}.md\:p-1{padding:var(--spacing)}.md\:p-2{padding:calc(var(--spacing) * 2)}.md\:p-3{padding:calc(var(--spacing) * 3)}.md\:p-4{padding:calc(var(--spacing) * 4)}.md\:p-5{padding:calc(var(--spacing) * 5)}.md\:p-6{padding:calc(var(--spacing) * 6)}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:p-10{padding:calc(var(--spacing) * 10)}.md\:p-12{padding:calc(var(--spacing) * 12)}.md\:px-0{padding-inline:0}.md\:px-1{padding-inline:var(--spacing)}.md\:px-2{padding-inline:calc(var(--spacing) * 2)}.md\:px-3{padding-inline:calc(var(--spacing) * 3)}.md\:px-4{padding-inline:calc(var(--spacing) * 4)}.md\:px-5{padding-inline:calc(var(--spacing) * 5)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}.md\:px-10{padding-inline:calc(var(--spacing) * 10)}.md\:px-12{padding-inline:calc(var(--spacing) * 12)}.md\:py-0{padding-block:0}.md\:py-1{padding-block:var(--spacing)}.md\:py-2{padding-block:calc(var(--spacing) * 2)}.md\:py-3{padding-block:calc(var(--spacing) * 3)}.md\:py-4{padding-block:calc(var(--spacing) * 4)}.md\:py-5{padding-block:calc(var(--spacing) * 5)}.md\:py-6{padding-block:calc(var(--spacing) * 6)}.md\:py-8{padding-block:calc(var(--spacing) * 8)}.md\:py-10{padding-block:calc(var(--spacing) * 10)}.md\:py-12{padding-block:calc(var(--spacing) * 12)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:64rem){.lg\:absolute{position:absolute}.lg\:fixed{position:fixed}.lg\:relative{position:relative}.lg\:static{position:static}.lg\:sticky{position:sticky}.lg\:inset-0{inset:0}.lg\:top-0{top:0}.lg\:right-0{right:0}.lg\:bottom-0{bottom:0}.lg\:left-0{left:0}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-3{order:3}.lg\:order-4{order:4}.lg\:order-first{order:-9999}.lg\:order-last{order:9999}.lg\:order-none{order:0}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:col-span-full{grid-column:1/-1}.lg\:row-span-1{grid-row:span 1/span 1}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-span-3{grid-row:span 3/span 3}.lg\:row-span-4{grid-row:span 4/span 4}.lg\:row-span-full{grid-row:1/-1}.lg\:m-0{margin:0}.lg\:m-1{margin:var(--spacing)}.lg\:m-2{margin:calc(var(--spacing) * 2)}.lg\:m-3{margin:calc(var(--spacing) * 3)}.lg\:m-4{margin:calc(var(--spacing) * 4)}.lg\:m-auto{margin:auto}.lg\:mx-auto{margin-inline:auto}.lg\:my-auto{margin-block:auto}.lg\:mt-0{margin-top:0}.lg\:mt-1{margin-top:var(--spacing)}.lg\:mt-2{margin-top:calc(var(--spacing) * 2)}.lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.lg\:mt-4{margin-top:calc(var(--spacing) * 4)}.lg\:mt-6{margin-top:calc(var(--spacing) * 6)}.lg\:mt-8{margin-top:calc(var(--spacing) * 8)}.lg\:mt-10{margin-top:calc(var(--spacing) * 10)}.lg\:mt-12{margin-top:calc(var(--spacing) * 12)}.lg\:mr-0{margin-right:0}.lg\:mr-1{margin-right:var(--spacing)}.lg\:mr-2{margin-right:calc(var(--spacing) * 2)}.lg\:mr-3{margin-right:calc(var(--spacing) * 3)}.lg\:mr-4{margin-right:calc(var(--spacing) * 4)}.lg\:mr-6{margin-right:calc(var(--spacing) * 6)}.lg\:mr-8{margin-right:calc(var(--spacing) * 8)}.lg\:mr-auto{margin-right:auto}.lg\:mb-0{margin-bottom:0}.lg\:mb-1{margin-bottom:var(--spacing)}.lg\:mb-2{margin-bottom:calc(var(--spacing) * 2)}.lg\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.lg\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.lg\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.lg\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.lg\:mb-10{margin-bottom:calc(var(--spacing) * 10)}.lg\:mb-12{margin-bottom:calc(var(--spacing) * 12)}.lg\:ml-0{margin-left:0}.lg\:ml-1{margin-left:var(--spacing)}.lg\:ml-2{margin-left:calc(var(--spacing) * 2)}.lg\:ml-3{margin-left:calc(var(--spacing) * 3)}.lg\:ml-4{margin-left:calc(var(--spacing) * 4)}.lg\:ml-6{margin-left:calc(var(--spacing) * 6)}.lg\:ml-8{margin-left:calc(var(--spacing) * 8)}.lg\:ml-auto{margin-left:auto}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:inline{display:inline}.lg\:inline-block{display:inline-block}.lg\:inline-flex{display:inline-flex}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.3333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:w-1\/6{width:16.6667%}.lg\:w-2\/3{width:66.6667%}.lg\:w-2\/5{width:40%}.lg\:w-3\/4{width:75%}.lg\:w-3\/5{width:60%}.lg\:w-4\/5{width:80%}.lg\:w-5\/6{width:83.3333%}.lg\:w-auto{width:auto}.lg\:w-full{width:100%}.lg\:max-w-2xl{max-width:var(--container-2xl)}.lg\:max-w-3xl{max-width:var(--container-3xl)}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-5xl{max-width:var(--container-5xl)}.lg\:max-w-6xl{max-width:var(--container-6xl)}.lg\:max-w-7xl{max-width:var(--container-7xl)}.lg\:max-w-full{max-width:100%}.lg\:max-w-lg{max-width:var(--container-lg)}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-none{max-width:none}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:max-w-xl{max-width:var(--container-xl)}.lg\:max-w-xs{max-width:var(--container-xs)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.lg\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.lg\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.lg\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.lg\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.lg\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-baseline{align-items:baseline}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:items-stretch{align-items:stretch}.lg\:justify-around{justify-content:space-around}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-evenly{justify-content:space-evenly}.lg\:justify-start{justify-content:flex-start}.lg\:gap-0{gap:0}.lg\:gap-1{gap:var(--spacing)}.lg\:gap-2{gap:calc(var(--spacing) * 2)}.lg\:gap-3{gap:calc(var(--spacing) * 3)}.lg\:gap-4{gap:calc(var(--spacing) * 4)}.lg\:gap-5{gap:calc(var(--spacing) * 5)}.lg\:gap-6{gap:calc(var(--spacing) * 6)}.lg\:gap-8{gap:calc(var(--spacing) * 8)}.lg\:gap-10{gap:calc(var(--spacing) * 10)}.lg\:gap-12{gap:calc(var(--spacing) * 12)}.lg\:gap-16{gap:calc(var(--spacing) * 16)}:where(.lg\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.lg\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline:0}:where(.lg\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(var(--spacing) * var(--tw-space-x-reverse));margin-inline-end:calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 10) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-x-reverse)))}.lg\:p-0{padding:0}.lg\:p-1{padding:var(--spacing)}.lg\:p-2{padding:calc(var(--spacing) * 2)}.lg\:p-3{padding:calc(var(--spacing) * 3)}.lg\:p-4{padding:calc(var(--spacing) * 4)}.lg\:p-5{padding:calc(var(--spacing) * 5)}.lg\:p-6{padding:calc(var(--spacing) * 6)}.lg\:p-8{padding:calc(var(--spacing) * 8)}.lg\:p-10{padding:calc(var(--spacing) * 10)}.lg\:p-12{padding:calc(var(--spacing) * 12)}.lg\:p-16{padding:calc(var(--spacing) * 16)}.lg\:px-0{padding-inline:0}.lg\:px-1{padding-inline:var(--spacing)}.lg\:px-2{padding-inline:calc(var(--spacing) * 2)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:px-4{padding-inline:calc(var(--spacing) * 4)}.lg\:px-5{padding-inline:calc(var(--spacing) * 5)}.lg\:px-6{padding-inline:calc(var(--spacing) * 6)}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}.lg\:px-10{padding-inline:calc(var(--spacing) * 10)}.lg\:px-12{padding-inline:calc(var(--spacing) * 12)}.lg\:px-16{padding-inline:calc(var(--spacing) * 16)}.lg\:py-0{padding-block:0}.lg\:py-1{padding-block:var(--spacing)}.lg\:py-2{padding-block:calc(var(--spacing) * 2)}.lg\:py-3{padding-block:calc(var(--spacing) * 3)}.lg\:py-4{padding-block:calc(var(--spacing) * 4)}.lg\:py-5{padding-block:calc(var(--spacing) * 5)}.lg\:py-6{padding-block:calc(var(--spacing) * 6)}.lg\:py-8{padding-block:calc(var(--spacing) * 8)}.lg\:py-10{padding-block:calc(var(--spacing) * 10)}.lg\:py-12{padding-block:calc(var(--spacing) * 12)}.lg\:py-16{padding-block:calc(var(--spacing) * 16)}.lg\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.lg\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.lg\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.lg\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.lg\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-3{grid-column:span 3/span 3}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-5{grid-column:span 5/span 5}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-full{grid-column:1/-1}.xl\:row-span-1{grid-row:span 1/span 1}.xl\:row-span-2{grid-row:span 2/span 2}.xl\:row-span-3{grid-row:span 3/span 3}.xl\:row-span-full{grid-row:1/-1}.xl\:m-0{margin:0}.xl\:m-auto{margin:auto}.xl\:mx-auto{margin-inline:auto}.xl\:my-auto{margin-block:auto}.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:inline{display:inline}.xl\:inline-block{display:inline-block}.xl\:inline-flex{display:inline-flex}.xl\:w-1\/2{width:50%}.xl\:w-1\/3{width:33.3333%}.xl\:w-1\/4{width:25%}.xl\:w-1\/5{width:20%}.xl\:w-2\/3{width:66.6667%}.xl\:w-2\/5{width:40%}.xl\:w-3\/4{width:75%}.xl\:w-3\/5{width:60%}.xl\:w-4\/5{width:80%}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-2xl{max-width:var(--container-2xl)}.xl\:max-w-3xl{max-width:var(--container-3xl)}.xl\:max-w-4xl{max-width:var(--container-4xl)}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:max-w-6xl{max-width:var(--container-6xl)}.xl\:max-w-7xl{max-width:var(--container-7xl)}.xl\:max-w-full{max-width:100%}.xl\:max-w-lg{max-width:var(--container-lg)}.xl\:max-w-md{max-width:var(--container-md)}.xl\:max-w-none{max-width:none}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:max-w-xl{max-width:var(--container-xl)}.xl\:max-w-xs{max-width:var(--container-xs)}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.xl\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.xl\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.xl\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.xl\:flex-col{flex-direction:column}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-center{justify-content:center}.xl\:justify-end{justify-content:flex-end}.xl\:justify-start{justify-content:flex-start}.xl\:gap-0{gap:0}.xl\:gap-1{gap:var(--spacing)}.xl\:gap-2{gap:calc(var(--spacing) * 2)}.xl\:gap-3{gap:calc(var(--spacing) * 3)}.xl\:gap-4{gap:calc(var(--spacing) * 4)}.xl\:gap-5{gap:calc(var(--spacing) * 5)}.xl\:gap-6{gap:calc(var(--spacing) * 6)}.xl\:gap-8{gap:calc(var(--spacing) * 8)}.xl\:gap-10{gap:calc(var(--spacing) * 10)}.xl\:gap-12{gap:calc(var(--spacing) * 12)}.xl\:gap-16{gap:calc(var(--spacing) * 16)}:where(.xl\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block:0}:where(.xl\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline:0}:where(.xl\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(var(--spacing) * var(--tw-space-x-reverse));margin-inline-end:calc(var(--spacing) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.xl\:p-0{padding:0}.xl\:p-1{padding:var(--spacing)}.xl\:p-2{padding:calc(var(--spacing) * 2)}.xl\:p-3{padding:calc(var(--spacing) * 3)}.xl\:p-4{padding:calc(var(--spacing) * 4)}.xl\:p-5{padding:calc(var(--spacing) * 5)}.xl\:p-6{padding:calc(var(--spacing) * 6)}.xl\:p-8{padding:calc(var(--spacing) * 8)}.xl\:p-10{padding:calc(var(--spacing) * 10)}.xl\:p-12{padding:calc(var(--spacing) * 12)}.xl\:p-16{padding:calc(var(--spacing) * 16)}.xl\:px-0{padding-inline:0}.xl\:px-1{padding-inline:var(--spacing)}.xl\:px-2{padding-inline:calc(var(--spacing) * 2)}.xl\:px-3{padding-inline:calc(var(--spacing) * 3)}.xl\:px-4{padding-inline:calc(var(--spacing) * 4)}.xl\:px-5{padding-inline:calc(var(--spacing) * 5)}.xl\:px-6{padding-inline:calc(var(--spacing) * 6)}.xl\:px-8{padding-inline:calc(var(--spacing) * 8)}.xl\:px-10{padding-inline:calc(var(--spacing) * 10)}.xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.xl\:px-16{padding-inline:calc(var(--spacing) * 16)}.xl\:py-0{padding-block:0}.xl\:py-1{padding-block:var(--spacing)}.xl\:py-2{padding-block:calc(var(--spacing) * 2)}.xl\:py-3{padding-block:calc(var(--spacing) * 3)}.xl\:py-4{padding-block:calc(var(--spacing) * 4)}.xl\:py-5{padding-block:calc(var(--spacing) * 5)}.xl\:py-6{padding-block:calc(var(--spacing) * 6)}.xl\:py-8{padding-block:calc(var(--spacing) * 8)}.xl\:py-10{padding-block:calc(var(--spacing) * 10)}.xl\:py-12{padding-block:calc(var(--spacing) * 12)}.xl\:py-16{padding-block:calc(var(--spacing) * 16)}.xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.xl\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:96rem){.\32xl\:col-span-1{grid-column:span 1/span 1}.\32xl\:col-span-2{grid-column:span 2/span 2}.\32xl\:col-span-3{grid-column:span 3/span 3}.\32xl\:col-span-4{grid-column:span 4/span 4}.\32xl\:col-span-6{grid-column:span 6/span 6}.\32xl\:col-span-12{grid-column:span 12/span 12}.\32xl\:block{display:block}.\32xl\:flex{display:flex}.\32xl\:hidden{display:none}.\32xl\:w-1\/2{width:50%}.\32xl\:w-1\/3{width:33.3333%}.\32xl\:w-1\/4{width:25%}.\32xl\:w-2\/3{width:66.6667%}.\32xl\:w-3\/4{width:75%}.\32xl\:w-auto{width:auto}.\32xl\:w-full{width:100%}.\32xl\:max-w-2xl{max-width:var(--container-2xl)}.\32xl\:max-w-3xl{max-width:var(--container-3xl)}.\32xl\:max-w-4xl{max-width:var(--container-4xl)}.\32xl\:max-w-5xl{max-width:var(--container-5xl)}.\32xl\:max-w-6xl{max-width:var(--container-6xl)}.\32xl\:max-w-7xl{max-width:var(--container-7xl)}.\32xl\:max-w-full{max-width:100%}.\32xl\:max-w-lg{max-width:var(--container-lg)}.\32xl\:max-w-md{max-width:var(--container-md)}.\32xl\:max-w-sm{max-width:var(--container-sm)}.\32xl\:max-w-xl{max-width:var(--container-xl)}.\32xl\:max-w-xs{max-width:var(--container-xs)}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:gap-0{gap:0}.\32xl\:gap-4{gap:calc(var(--spacing) * 4)}.\32xl\:gap-6{gap:calc(var(--spacing) * 6)}.\32xl\:gap-8{gap:calc(var(--spacing) * 8)}.\32xl\:gap-12{gap:calc(var(--spacing) * 12)}.\32xl\:gap-16{gap:calc(var(--spacing) * 16)}.\32xl\:p-0{padding:0}.\32xl\:p-4{padding:calc(var(--spacing) * 4)}.\32xl\:p-6{padding:calc(var(--spacing) * 6)}.\32xl\:p-8{padding:calc(var(--spacing) * 8)}.\32xl\:p-12{padding:calc(var(--spacing) * 12)}.\32xl\:p-16{padding:calc(var(--spacing) * 16)}.\32xl\:px-0{padding-inline:0}.\32xl\:px-4{padding-inline:calc(var(--spacing) * 4)}.\32xl\:px-6{padding-inline:calc(var(--spacing) * 6)}.\32xl\:px-8{padding-inline:calc(var(--spacing) * 8)}.\32xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.\32xl\:px-16{padding-inline:calc(var(--spacing) * 16)}.\32xl\:py-0{padding-block:0}.\32xl\:py-4{padding-block:calc(var(--spacing) * 4)}.\32xl\:py-6{padding-block:calc(var(--spacing) * 6)}.\32xl\:py-8{padding-block:calc(var(--spacing) * 8)}.\32xl\:py-12{padding-block:calc(var(--spacing) * 12)}.\32xl\:py-16{padding-block:calc(var(--spacing) * 16)}.\32xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.\32xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.\32xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.\32xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.\32xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.\32xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.\32xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\32xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.\32xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.dark .dark\:block{display:block}.dark .dark\:flex{display:flex}.dark .dark\:flex-1{flex:1}.dark .dark\:flex-auto{flex:auto}.dark .dark\:flex-none{flex:none}.dark .dark\:flex-col{flex-direction:column}.dark .dark\:flex-row{flex-direction:row}.dark .dark\:flex-wrap{flex-wrap:wrap}.dark .dark\:items-center{align-items:center}.dark .dark\:items-end{align-items:flex-end}.dark .dark\:items-start{align-items:flex-start}.dark .dark\:items-stretch{align-items:stretch}.dark .dark\:justify-around{justify-content:space-around}.dark .dark\:justify-between{justify-content:space-between}.dark .dark\:justify-center{justify-content:center}.dark .dark\:justify-end{justify-content:flex-end}.dark .dark\:justify-start{justify-content:flex-start}:where(.dark .dark\:divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.dark .dark\:divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.dark .dark\:divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.dark .dark\:divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.dark .dark\:divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.dark .dark\:divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.dark .dark\:divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.dark .dark\:divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.dark .dark\:divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.dark .dark\:divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.dark .dark\:divide-purple-800>:not(:last-child)){border-color:var(--color-purple-800)}:where(.dark .dark\:divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}.dark .dark\:self-center{align-self:center}.dark .dark\:self-end{align-self:flex-end}.dark .dark\:self-start{align-self:flex-start}.dark .dark\:self-stretch{align-self:stretch}.dark .dark\:overflow-auto{overflow:auto}.dark .dark\:overflow-hidden{overflow:hidden}.dark .dark\:overflow-visible{overflow:visible}.dark .dark\:overflow-x-auto{overflow-x:auto}.dark .dark\:overflow-y-auto{overflow-y:auto}.dark .dark\:rounded{border-radius:.25rem}.dark .dark\:rounded-full{border-radius:3.40282e38px}.dark .dark\:rounded-lg{border-radius:var(--radius-lg)}.dark .dark\:rounded-none{border-radius:0}.dark .dark\:rounded-sm{border-radius:var(--radius-sm)}.dark .dark\:rounded-xl{border-radius:var(--radius-xl)}.dark .dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark .dark\:border-0{border-style:var(--tw-border-style);border-width:0}.dark .dark\:border-dashed{--tw-border-style:dashed;border-style:dashed}.dark .dark\:border-dotted{--tw-border-style:dotted;border-style:dotted}.dark .dark\:border-solid{--tw-border-style:solid;border-style:solid}.dark .dark\:border-amber-50{border-color:var(--color-amber-50)}.dark .dark\:border-amber-100{border-color:var(--color-amber-100)}.dark .dark\:border-amber-200{border-color:var(--color-amber-200)}.dark .dark\:border-amber-300{border-color:var(--color-amber-300)}.dark .dark\:border-amber-400{border-color:var(--color-amber-400)}.dark .dark\:border-amber-500{border-color:var(--color-amber-500)}.dark .dark\:border-amber-600{border-color:var(--color-amber-600)}.dark .dark\:border-amber-700{border-color:var(--color-amber-700)}.dark .dark\:border-amber-800{border-color:var(--color-amber-800)}.dark .dark\:border-amber-900{border-color:var(--color-amber-900)}.dark .dark\:border-black{border-color:var(--color-black)}.dark .dark\:border-blue-50{border-color:var(--color-blue-50)}.dark .dark\:border-blue-100{border-color:var(--color-blue-100)}.dark .dark\:border-blue-200{border-color:var(--color-blue-200)}.dark .dark\:border-blue-300{border-color:var(--color-blue-300)}.dark .dark\:border-blue-400{border-color:var(--color-blue-400)}.dark .dark\:border-blue-500{border-color:var(--color-blue-500)}.dark .dark\:border-blue-600{border-color:var(--color-blue-600)}.dark .dark\:border-blue-700{border-color:var(--color-blue-700)}.dark .dark\:border-blue-800{border-color:var(--color-blue-800)}.dark .dark\:border-blue-900{border-color:var(--color-blue-900)}.dark .dark\:border-gray-50{border-color:var(--color-gray-50)}.dark .dark\:border-gray-100{border-color:var(--color-gray-100)}.dark .dark\:border-gray-200{border-color:var(--color-gray-200)}.dark .dark\:border-gray-300{border-color:var(--color-gray-300)}.dark .dark\:border-gray-400{border-color:var(--color-gray-400)}.dark .dark\:border-gray-500{border-color:var(--color-gray-500)}.dark .dark\:border-gray-600{border-color:var(--color-gray-600)}.dark .dark\:border-gray-700{border-color:var(--color-gray-700)}.dark .dark\:border-gray-800{border-color:var(--color-gray-800)}.dark .dark\:border-gray-900{border-color:var(--color-gray-900)}.dark .dark\:border-gray-950{border-color:var(--color-gray-950)}.dark .dark\:border-green-50{border-color:var(--color-green-50)}.dark .dark\:border-green-100{border-color:var(--color-green-100)}.dark .dark\:border-green-200{border-color:var(--color-green-200)}.dark .dark\:border-green-300{border-color:var(--color-green-300)}.dark .dark\:border-green-400{border-color:var(--color-green-400)}.dark .dark\:border-green-500{border-color:var(--color-green-500)}.dark .dark\:border-green-600{border-color:var(--color-green-600)}.dark .dark\:border-green-700{border-color:var(--color-green-700)}.dark .dark\:border-green-800{border-color:var(--color-green-800)}.dark .dark\:border-green-900{border-color:var(--color-green-900)}.dark .dark\:border-indigo-400{border-color:var(--color-indigo-400)}.dark .dark\:border-orange-50{border-color:var(--color-orange-50)}.dark .dark\:border-orange-100{border-color:var(--color-orange-100)}.dark .dark\:border-orange-200{border-color:var(--color-orange-200)}.dark .dark\:border-orange-300{border-color:var(--color-orange-300)}.dark .dark\:border-orange-400{border-color:var(--color-orange-400)}.dark .dark\:border-orange-500{border-color:var(--color-orange-500)}.dark .dark\:border-orange-600{border-color:var(--color-orange-600)}.dark .dark\:border-orange-700{border-color:var(--color-orange-700)}.dark .dark\:border-orange-800{border-color:var(--color-orange-800)}.dark .dark\:border-orange-900{border-color:var(--color-orange-900)}.dark .dark\:border-pink-50{border-color:var(--color-pink-50)}.dark .dark\:border-pink-100{border-color:var(--color-pink-100)}.dark .dark\:border-pink-200{border-color:var(--color-pink-200)}.dark .dark\:border-pink-300{border-color:var(--color-pink-300)}.dark .dark\:border-pink-400{border-color:var(--color-pink-400)}.dark .dark\:border-pink-500{border-color:var(--color-pink-500)}.dark .dark\:border-pink-600{border-color:var(--color-pink-600)}.dark .dark\:border-pink-700{border-color:var(--color-pink-700)}.dark .dark\:border-pink-800{border-color:var(--color-pink-800)}.dark .dark\:border-pink-900{border-color:var(--color-pink-900)}.dark .dark\:border-purple-50{border-color:var(--color-purple-50)}.dark .dark\:border-purple-100{border-color:var(--color-purple-100)}.dark .dark\:border-purple-200{border-color:var(--color-purple-200)}.dark .dark\:border-purple-300{border-color:var(--color-purple-300)}.dark .dark\:border-purple-400{border-color:var(--color-purple-400)}.dark .dark\:border-purple-500{border-color:var(--color-purple-500)}.dark .dark\:border-purple-600{border-color:var(--color-purple-600)}.dark .dark\:border-purple-700{border-color:var(--color-purple-700)}.dark .dark\:border-purple-800{border-color:var(--color-purple-800)}.dark .dark\:border-purple-900{border-color:var(--color-purple-900)}.dark .dark\:border-red-50{border-color:var(--color-red-50)}.dark .dark\:border-red-100{border-color:var(--color-red-100)}.dark .dark\:border-red-200{border-color:var(--color-red-200)}.dark .dark\:border-red-300{border-color:var(--color-red-300)}.dark .dark\:border-red-400{border-color:var(--color-red-400)}.dark .dark\:border-red-500{border-color:var(--color-red-500)}.dark .dark\:border-red-600{border-color:var(--color-red-600)}.dark .dark\:border-red-700{border-color:var(--color-red-700)}.dark .dark\:border-red-800{border-color:var(--color-red-800)}.dark .dark\:border-red-900{border-color:var(--color-red-900)}.dark .dark\:border-slate-50{border-color:var(--color-slate-50)}.dark .dark\:border-slate-100{border-color:var(--color-slate-100)}.dark .dark\:border-slate-200{border-color:var(--color-slate-200)}.dark .dark\:border-slate-300{border-color:var(--color-slate-300)}.dark .dark\:border-slate-400{border-color:var(--color-slate-400)}.dark .dark\:border-slate-500{border-color:var(--color-slate-500)}.dark .dark\:border-slate-600{border-color:var(--color-slate-600)}.dark .dark\:border-slate-700{border-color:var(--color-slate-700)}.dark .dark\:border-slate-800{border-color:var(--color-slate-800)}.dark .dark\:border-slate-900{border-color:var(--color-slate-900)}.dark .dark\:border-slate-950{border-color:var(--color-slate-950)}.dark .dark\:border-transparent{border-color:#0000}.dark .dark\:border-white{border-color:var(--color-white)}.dark .dark\:border-yellow-50{border-color:var(--color-yellow-50)}.dark .dark\:border-yellow-100{border-color:var(--color-yellow-100)}.dark .dark\:border-yellow-200{border-color:var(--color-yellow-200)}.dark .dark\:border-yellow-300{border-color:var(--color-yellow-300)}.dark .dark\:border-yellow-400{border-color:var(--color-yellow-400)}.dark .dark\:border-yellow-500{border-color:var(--color-yellow-500)}.dark .dark\:border-yellow-600{border-color:var(--color-yellow-600)}.dark .dark\:border-yellow-700{border-color:var(--color-yellow-700)}.dark .dark\:border-yellow-800{border-color:var(--color-yellow-800)}.dark .dark\:border-yellow-900{border-color:var(--color-yellow-900)}.dark .dark\:bg-amber-50{background-color:var(--color-amber-50)}.dark .dark\:bg-amber-100{background-color:var(--color-amber-100)}.dark .dark\:bg-amber-200{background-color:var(--color-amber-200)}.dark .dark\:bg-amber-300{background-color:var(--color-amber-300)}.dark .dark\:bg-amber-400{background-color:var(--color-amber-400)}.dark .dark\:bg-amber-500{background-color:var(--color-amber-500)}.dark .dark\:bg-amber-600{background-color:var(--color-amber-600)}.dark .dark\:bg-amber-700{background-color:var(--color-amber-700)}.dark .dark\:bg-amber-800{background-color:var(--color-amber-800)}.dark .dark\:bg-amber-900{background-color:var(--color-amber-900)}.dark .dark\:bg-amber-900\/20{background-color:#7b330633}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/20{background-color:color-mix(in oklab,var(--color-amber-900) 20%,transparent)}}.dark .dark\:bg-amber-900\/30{background-color:#7b33064d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/30{background-color:color-mix(in oklab,var(--color-amber-900) 30%,transparent)}}.dark .dark\:bg-amber-900\/40{background-color:#7b330666}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/40{background-color:color-mix(in oklab,var(--color-amber-900) 40%,transparent)}}.dark .dark\:bg-amber-950{background-color:var(--color-amber-950)}.dark .dark\:bg-black{background-color:var(--color-black)}.dark .dark\:bg-blue-50{background-color:var(--color-blue-50)}.dark .dark\:bg-blue-100{background-color:var(--color-blue-100)}.dark .dark\:bg-blue-200{background-color:var(--color-blue-200)}.dark .dark\:bg-blue-300{background-color:var(--color-blue-300)}.dark .dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark .dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark .dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark .dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark .dark\:bg-blue-800{background-color:var(--color-blue-800)}.dark .dark\:bg-blue-900{background-color:var(--color-blue-900)}.dark .dark\:bg-blue-900\/20{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/20{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .dark\:bg-blue-900\/30{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/30{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .dark\:bg-blue-900\/40{background-color:#1c398e66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/40{background-color:color-mix(in oklab,var(--color-blue-900) 40%,transparent)}}.dark .dark\:bg-blue-900\/50{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/50{background-color:color-mix(in oklab,var(--color-blue-900) 50%,transparent)}}.dark .dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark .dark\:bg-cyan-50{background-color:var(--color-cyan-50)}.dark .dark\:bg-cyan-100{background-color:var(--color-cyan-100)}.dark .dark\:bg-cyan-200{background-color:var(--color-cyan-200)}.dark .dark\:bg-cyan-300{background-color:var(--color-cyan-300)}.dark .dark\:bg-cyan-400{background-color:var(--color-cyan-400)}.dark .dark\:bg-cyan-500{background-color:var(--color-cyan-500)}.dark .dark\:bg-cyan-600{background-color:var(--color-cyan-600)}.dark .dark\:bg-cyan-700{background-color:var(--color-cyan-700)}.dark .dark\:bg-cyan-800{background-color:var(--color-cyan-800)}.dark .dark\:bg-cyan-900{background-color:var(--color-cyan-900)}.dark .dark\:bg-cyan-900\/30{background-color:#104e644d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-cyan-900\/30{background-color:color-mix(in oklab,var(--color-cyan-900) 30%,transparent)}}.dark .dark\:bg-cyan-950{background-color:var(--color-cyan-950)}.dark .dark\:bg-emerald-50{background-color:var(--color-emerald-50)}.dark .dark\:bg-emerald-100{background-color:var(--color-emerald-100)}.dark .dark\:bg-emerald-200{background-color:var(--color-emerald-200)}.dark .dark\:bg-emerald-300{background-color:var(--color-emerald-300)}.dark .dark\:bg-emerald-400{background-color:var(--color-emerald-400)}.dark .dark\:bg-emerald-500{background-color:var(--color-emerald-500)}.dark .dark\:bg-emerald-600{background-color:var(--color-emerald-600)}.dark .dark\:bg-emerald-700{background-color:var(--color-emerald-700)}.dark .dark\:bg-emerald-800{background-color:var(--color-emerald-800)}.dark .dark\:bg-emerald-900{background-color:var(--color-emerald-900)}.dark .dark\:bg-emerald-950{background-color:var(--color-emerald-950)}.dark .dark\:bg-gray-50{background-color:var(--color-gray-50)}.dark .dark\:bg-gray-100{background-color:var(--color-gray-100)}.dark .dark\:bg-gray-200{background-color:var(--color-gray-200)}.dark .dark\:bg-gray-300{background-color:var(--color-gray-300)}.dark .dark\:bg-gray-400{background-color:var(--color-gray-400)}.dark .dark\:bg-gray-500{background-color:var(--color-gray-500)}.dark .dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark .dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark .dark\:bg-gray-700\/30{background-color:#3641534d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/30{background-color:color-mix(in oklab,var(--color-gray-700) 30%,transparent)}}.dark .dark\:bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/50{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.dark .dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark .dark\:bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.dark .dark\:bg-gray-800\/90{background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/90{background-color:color-mix(in oklab,var(--color-gray-800) 90%,transparent)}}.dark .dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark .dark\:bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900) 50%,transparent)}}.dark .dark\:bg-gray-950{background-color:var(--color-gray-950)}.dark .dark\:bg-green-50{background-color:var(--color-green-50)}.dark .dark\:bg-green-100{background-color:var(--color-green-100)}.dark .dark\:bg-green-200{background-color:var(--color-green-200)}.dark .dark\:bg-green-300{background-color:var(--color-green-300)}.dark .dark\:bg-green-400{background-color:var(--color-green-400)}.dark .dark\:bg-green-500{background-color:var(--color-green-500)}.dark .dark\:bg-green-600{background-color:var(--color-green-600)}.dark .dark\:bg-green-700{background-color:var(--color-green-700)}.dark .dark\:bg-green-800{background-color:var(--color-green-800)}.dark .dark\:bg-green-900{background-color:var(--color-green-900)}.dark .dark\:bg-green-900\/20{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/20{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .dark\:bg-green-900\/30{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/30{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .dark\:bg-green-950{background-color:var(--color-green-950)}.dark .dark\:bg-indigo-50{background-color:var(--color-indigo-50)}.dark .dark\:bg-indigo-100{background-color:var(--color-indigo-100)}.dark .dark\:bg-indigo-200{background-color:var(--color-indigo-200)}.dark .dark\:bg-indigo-300{background-color:var(--color-indigo-300)}.dark .dark\:bg-indigo-400{background-color:var(--color-indigo-400)}.dark .dark\:bg-indigo-500{background-color:var(--color-indigo-500)}.dark .dark\:bg-indigo-600{background-color:var(--color-indigo-600)}.dark .dark\:bg-indigo-700{background-color:var(--color-indigo-700)}.dark .dark\:bg-indigo-800{background-color:var(--color-indigo-800)}.dark .dark\:bg-indigo-900{background-color:var(--color-indigo-900)}.dark .dark\:bg-indigo-900\/30{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-indigo-900\/30{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .dark\:bg-indigo-950{background-color:var(--color-indigo-950)}.dark .dark\:bg-orange-50{background-color:var(--color-orange-50)}.dark .dark\:bg-orange-100{background-color:var(--color-orange-100)}.dark .dark\:bg-orange-200{background-color:var(--color-orange-200)}.dark .dark\:bg-orange-300{background-color:var(--color-orange-300)}.dark .dark\:bg-orange-400{background-color:var(--color-orange-400)}.dark .dark\:bg-orange-500{background-color:var(--color-orange-500)}.dark .dark\:bg-orange-600{background-color:var(--color-orange-600)}.dark .dark\:bg-orange-700{background-color:var(--color-orange-700)}.dark .dark\:bg-orange-800{background-color:var(--color-orange-800)}.dark .dark\:bg-orange-900{background-color:var(--color-orange-900)}.dark .dark\:bg-orange-900\/20{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/20{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .dark\:bg-orange-900\/30{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/30{background-color:color-mix(in oklab,var(--color-orange-900) 30%,transparent)}}.dark .dark\:bg-orange-950{background-color:var(--color-orange-950)}.dark .dark\:bg-pink-50{background-color:var(--color-pink-50)}.dark .dark\:bg-pink-100{background-color:var(--color-pink-100)}.dark .dark\:bg-pink-200{background-color:var(--color-pink-200)}.dark .dark\:bg-pink-300{background-color:var(--color-pink-300)}.dark .dark\:bg-pink-400{background-color:var(--color-pink-400)}.dark .dark\:bg-pink-500{background-color:var(--color-pink-500)}.dark .dark\:bg-pink-600{background-color:var(--color-pink-600)}.dark .dark\:bg-pink-700{background-color:var(--color-pink-700)}.dark .dark\:bg-pink-800{background-color:var(--color-pink-800)}.dark .dark\:bg-pink-900{background-color:var(--color-pink-900)}.dark .dark\:bg-pink-950{background-color:var(--color-pink-950)}.dark .dark\:bg-purple-50{background-color:var(--color-purple-50)}.dark .dark\:bg-purple-100{background-color:var(--color-purple-100)}.dark .dark\:bg-purple-200{background-color:var(--color-purple-200)}.dark .dark\:bg-purple-300{background-color:var(--color-purple-300)}.dark .dark\:bg-purple-400{background-color:var(--color-purple-400)}.dark .dark\:bg-purple-500{background-color:var(--color-purple-500)}.dark .dark\:bg-purple-600{background-color:var(--color-purple-600)}.dark .dark\:bg-purple-700{background-color:var(--color-purple-700)}.dark .dark\:bg-purple-800{background-color:var(--color-purple-800)}.dark .dark\:bg-purple-800\/50{background-color:#6e11b080}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-800\/50{background-color:color-mix(in oklab,var(--color-purple-800) 50%,transparent)}}.dark .dark\:bg-purple-900{background-color:var(--color-purple-900)}.dark .dark\:bg-purple-900\/20{background-color:#59168b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/20{background-color:color-mix(in oklab,var(--color-purple-900) 20%,transparent)}}.dark .dark\:bg-purple-900\/30{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/30{background-color:color-mix(in oklab,var(--color-purple-900) 30%,transparent)}}.dark .dark\:bg-purple-900\/50{background-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/50{background-color:color-mix(in oklab,var(--color-purple-900) 50%,transparent)}}.dark .dark\:bg-purple-950{background-color:var(--color-purple-950)}.dark .dark\:bg-red-50{background-color:var(--color-red-50)}.dark .dark\:bg-red-100{background-color:var(--color-red-100)}.dark .dark\:bg-red-200{background-color:var(--color-red-200)}.dark .dark\:bg-red-300{background-color:var(--color-red-300)}.dark .dark\:bg-red-400{background-color:var(--color-red-400)}.dark .dark\:bg-red-500{background-color:var(--color-red-500)}.dark .dark\:bg-red-600{background-color:var(--color-red-600)}.dark .dark\:bg-red-700{background-color:var(--color-red-700)}.dark .dark\:bg-red-800{background-color:var(--color-red-800)}.dark .dark\:bg-red-900{background-color:var(--color-red-900)}.dark .dark\:bg-red-900\/20{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/20{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .dark\:bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .dark\:bg-red-950{background-color:var(--color-red-950)}.dark .dark\:bg-rose-50{background-color:var(--color-rose-50)}.dark .dark\:bg-rose-100{background-color:var(--color-rose-100)}.dark .dark\:bg-rose-200{background-color:var(--color-rose-200)}.dark .dark\:bg-rose-300{background-color:var(--color-rose-300)}.dark .dark\:bg-rose-400{background-color:var(--color-rose-400)}.dark .dark\:bg-rose-500{background-color:var(--color-rose-500)}.dark .dark\:bg-rose-600{background-color:var(--color-rose-600)}.dark .dark\:bg-rose-700{background-color:var(--color-rose-700)}.dark .dark\:bg-rose-800{background-color:var(--color-rose-800)}.dark .dark\:bg-rose-900{background-color:var(--color-rose-900)}.dark .dark\:bg-rose-950{background-color:var(--color-rose-950)}.dark .dark\:bg-slate-50{background-color:var(--color-slate-50)}.dark .dark\:bg-slate-100{background-color:var(--color-slate-100)}.dark .dark\:bg-slate-200{background-color:var(--color-slate-200)}.dark .dark\:bg-slate-300{background-color:var(--color-slate-300)}.dark .dark\:bg-slate-400{background-color:var(--color-slate-400)}.dark .dark\:bg-slate-500{background-color:var(--color-slate-500)}.dark .dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark .dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark .dark\:bg-slate-700\/50{background-color:#31415880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-slate-700\/50{background-color:color-mix(in oklab,var(--color-slate-700) 50%,transparent)}}.dark .dark\:bg-slate-800{background-color:var(--color-slate-800)}.dark .dark\:bg-slate-900{background-color:var(--color-slate-900)}.dark .dark\:bg-slate-950{background-color:var(--color-slate-950)}.dark .dark\:bg-teal-50{background-color:var(--color-teal-50)}.dark .dark\:bg-teal-100{background-color:var(--color-teal-100)}.dark .dark\:bg-teal-200{background-color:var(--color-teal-200)}.dark .dark\:bg-teal-300{background-color:var(--color-teal-300)}.dark .dark\:bg-teal-400{background-color:var(--color-teal-400)}.dark .dark\:bg-teal-500{background-color:var(--color-teal-500)}.dark .dark\:bg-teal-600{background-color:var(--color-teal-600)}.dark .dark\:bg-teal-700{background-color:var(--color-teal-700)}.dark .dark\:bg-teal-800{background-color:var(--color-teal-800)}.dark .dark\:bg-teal-900{background-color:var(--color-teal-900)}.dark .dark\:bg-teal-900\/30{background-color:#0b4f4a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-teal-900\/30{background-color:color-mix(in oklab,var(--color-teal-900) 30%,transparent)}}.dark .dark\:bg-teal-950{background-color:var(--color-teal-950)}.dark .dark\:bg-white{background-color:var(--color-white)}.dark .dark\:bg-yellow-50{background-color:var(--color-yellow-50)}.dark .dark\:bg-yellow-100{background-color:var(--color-yellow-100)}.dark .dark\:bg-yellow-200{background-color:var(--color-yellow-200)}.dark .dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark .dark\:bg-yellow-400{background-color:var(--color-yellow-400)}.dark .dark\:bg-yellow-500{background-color:var(--color-yellow-500)}.dark .dark\:bg-yellow-600{background-color:var(--color-yellow-600)}.dark .dark\:bg-yellow-700{background-color:var(--color-yellow-700)}.dark .dark\:bg-yellow-800{background-color:var(--color-yellow-800)}.dark .dark\:bg-yellow-900{background-color:var(--color-yellow-900)}.dark .dark\:bg-yellow-900\/20{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/20{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .dark\:bg-yellow-900\/30{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/30{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .dark\:bg-yellow-900\/40{background-color:#733e0a66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/40{background-color:color-mix(in oklab,var(--color-yellow-900) 40%,transparent)}}.dark .dark\:bg-yellow-950{background-color:var(--color-yellow-950)}.dark .dark\:from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-indigo-900{--tw-gradient-to:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:text-center{text-align:center}.dark .dark\:text-justify{text-align:justify}.dark .dark\:text-left{text-align:left}.dark .dark\:text-right{text-align:right}.dark .dark\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.dark .dark\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.dark .dark\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.dark .dark\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.dark .dark\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.dark .dark\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.dark .dark\:font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dark .dark\:font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.dark .dark\:font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.dark .dark\:whitespace-normal{white-space:normal}.dark .dark\:whitespace-nowrap{white-space:nowrap}.dark .dark\:whitespace-pre-wrap{white-space:pre-wrap}.dark .dark\:text-amber-50{color:var(--color-amber-50)}.dark .dark\:text-amber-100{color:var(--color-amber-100)}.dark .dark\:text-amber-200{color:var(--color-amber-200)}.dark .dark\:text-amber-300{color:var(--color-amber-300)}.dark .dark\:text-amber-400{color:var(--color-amber-400)}.dark .dark\:text-amber-500{color:var(--color-amber-500)}.dark .dark\:text-amber-600{color:var(--color-amber-600)}.dark .dark\:text-amber-700{color:var(--color-amber-700)}.dark .dark\:text-amber-800{color:var(--color-amber-800)}.dark .dark\:text-amber-900{color:var(--color-amber-900)}.dark .dark\:text-black{color:var(--color-black)}.dark .dark\:text-blue-50{color:var(--color-blue-50)}.dark .dark\:text-blue-100{color:var(--color-blue-100)}.dark .dark\:text-blue-200{color:var(--color-blue-200)}.dark .dark\:text-blue-300{color:var(--color-blue-300)}.dark .dark\:text-blue-400{color:var(--color-blue-400)}.dark .dark\:text-blue-500{color:var(--color-blue-500)}.dark .dark\:text-blue-600{color:var(--color-blue-600)}.dark .dark\:text-blue-700{color:var(--color-blue-700)}.dark .dark\:text-blue-800{color:var(--color-blue-800)}.dark .dark\:text-blue-900{color:var(--color-blue-900)}.dark .dark\:text-cyan-50{color:var(--color-cyan-50)}.dark .dark\:text-cyan-100{color:var(--color-cyan-100)}.dark .dark\:text-cyan-200{color:var(--color-cyan-200)}.dark .dark\:text-cyan-300{color:var(--color-cyan-300)}.dark .dark\:text-cyan-400{color:var(--color-cyan-400)}.dark .dark\:text-cyan-500{color:var(--color-cyan-500)}.dark .dark\:text-cyan-600{color:var(--color-cyan-600)}.dark .dark\:text-cyan-700{color:var(--color-cyan-700)}.dark .dark\:text-cyan-800{color:var(--color-cyan-800)}.dark .dark\:text-cyan-900{color:var(--color-cyan-900)}.dark .dark\:text-emerald-50{color:var(--color-emerald-50)}.dark .dark\:text-emerald-100{color:var(--color-emerald-100)}.dark .dark\:text-emerald-200{color:var(--color-emerald-200)}.dark .dark\:text-emerald-300{color:var(--color-emerald-300)}.dark .dark\:text-emerald-400{color:var(--color-emerald-400)}.dark .dark\:text-emerald-500{color:var(--color-emerald-500)}.dark .dark\:text-emerald-600{color:var(--color-emerald-600)}.dark .dark\:text-emerald-700{color:var(--color-emerald-700)}.dark .dark\:text-emerald-800{color:var(--color-emerald-800)}.dark .dark\:text-emerald-900{color:var(--color-emerald-900)}.dark .dark\:text-gray-50{color:var(--color-gray-50)}.dark .dark\:text-gray-100{color:var(--color-gray-100)}.dark .dark\:text-gray-200{color:var(--color-gray-200)}.dark .dark\:text-gray-300{color:var(--color-gray-300)}.dark .dark\:text-gray-400{color:var(--color-gray-400)}.dark .dark\:text-gray-500{color:var(--color-gray-500)}.dark .dark\:text-gray-600{color:var(--color-gray-600)}.dark .dark\:text-gray-700{color:var(--color-gray-700)}.dark .dark\:text-gray-800{color:var(--color-gray-800)}.dark .dark\:text-gray-900{color:var(--color-gray-900)}.dark .dark\:text-green-50{color:var(--color-green-50)}.dark .dark\:text-green-100{color:var(--color-green-100)}.dark .dark\:text-green-200{color:var(--color-green-200)}.dark .dark\:text-green-300{color:var(--color-green-300)}.dark .dark\:text-green-400{color:var(--color-green-400)}.dark .dark\:text-green-500{color:var(--color-green-500)}.dark .dark\:text-green-600{color:var(--color-green-600)}.dark .dark\:text-green-700{color:var(--color-green-700)}.dark .dark\:text-green-800{color:var(--color-green-800)}.dark .dark\:text-green-900{color:var(--color-green-900)}.dark .dark\:text-indigo-50{color:var(--color-indigo-50)}.dark .dark\:text-indigo-100{color:var(--color-indigo-100)}.dark .dark\:text-indigo-200{color:var(--color-indigo-200)}.dark .dark\:text-indigo-300{color:var(--color-indigo-300)}.dark .dark\:text-indigo-400{color:var(--color-indigo-400)}.dark .dark\:text-indigo-500{color:var(--color-indigo-500)}.dark .dark\:text-indigo-600{color:var(--color-indigo-600)}.dark .dark\:text-indigo-700{color:var(--color-indigo-700)}.dark .dark\:text-indigo-800{color:var(--color-indigo-800)}.dark .dark\:text-indigo-900{color:var(--color-indigo-900)}.dark .dark\:text-orange-50{color:var(--color-orange-50)}.dark .dark\:text-orange-100{color:var(--color-orange-100)}.dark .dark\:text-orange-200{color:var(--color-orange-200)}.dark .dark\:text-orange-300{color:var(--color-orange-300)}.dark .dark\:text-orange-400{color:var(--color-orange-400)}.dark .dark\:text-orange-500{color:var(--color-orange-500)}.dark .dark\:text-orange-600{color:var(--color-orange-600)}.dark .dark\:text-orange-700{color:var(--color-orange-700)}.dark .dark\:text-orange-800{color:var(--color-orange-800)}.dark .dark\:text-orange-900{color:var(--color-orange-900)}.dark .dark\:text-pink-50{color:var(--color-pink-50)}.dark .dark\:text-pink-100{color:var(--color-pink-100)}.dark .dark\:text-pink-200{color:var(--color-pink-200)}.dark .dark\:text-pink-300{color:var(--color-pink-300)}.dark .dark\:text-pink-400{color:var(--color-pink-400)}.dark .dark\:text-pink-500{color:var(--color-pink-500)}.dark .dark\:text-pink-600{color:var(--color-pink-600)}.dark .dark\:text-pink-700{color:var(--color-pink-700)}.dark .dark\:text-pink-800{color:var(--color-pink-800)}.dark .dark\:text-pink-900{color:var(--color-pink-900)}.dark .dark\:text-purple-50{color:var(--color-purple-50)}.dark .dark\:text-purple-100{color:var(--color-purple-100)}.dark .dark\:text-purple-200{color:var(--color-purple-200)}.dark .dark\:text-purple-300{color:var(--color-purple-300)}.dark .dark\:text-purple-400{color:var(--color-purple-400)}.dark .dark\:text-purple-500{color:var(--color-purple-500)}.dark .dark\:text-purple-600{color:var(--color-purple-600)}.dark .dark\:text-purple-700{color:var(--color-purple-700)}.dark .dark\:text-purple-800{color:var(--color-purple-800)}.dark .dark\:text-purple-900{color:var(--color-purple-900)}.dark .dark\:text-red-50{color:var(--color-red-50)}.dark .dark\:text-red-100{color:var(--color-red-100)}.dark .dark\:text-red-200{color:var(--color-red-200)}.dark .dark\:text-red-300{color:var(--color-red-300)}.dark .dark\:text-red-400{color:var(--color-red-400)}.dark .dark\:text-red-500{color:var(--color-red-500)}.dark .dark\:text-red-600{color:var(--color-red-600)}.dark .dark\:text-red-700{color:var(--color-red-700)}.dark .dark\:text-red-800{color:var(--color-red-800)}.dark .dark\:text-red-900{color:var(--color-red-900)}.dark .dark\:text-slate-50{color:var(--color-slate-50)}.dark .dark\:text-slate-100{color:var(--color-slate-100)}.dark .dark\:text-slate-200{color:var(--color-slate-200)}.dark .dark\:text-slate-300{color:var(--color-slate-300)}.dark .dark\:text-slate-400{color:var(--color-slate-400)}.dark .dark\:text-slate-500{color:var(--color-slate-500)}.dark .dark\:text-slate-600{color:var(--color-slate-600)}.dark .dark\:text-slate-700{color:var(--color-slate-700)}.dark .dark\:text-slate-800{color:var(--color-slate-800)}.dark .dark\:text-slate-900{color:var(--color-slate-900)}.dark .dark\:text-teal-50{color:var(--color-teal-50)}.dark .dark\:text-teal-100{color:var(--color-teal-100)}.dark .dark\:text-teal-200{color:var(--color-teal-200)}.dark .dark\:text-teal-300{color:var(--color-teal-300)}.dark .dark\:text-teal-400{color:var(--color-teal-400)}.dark .dark\:text-teal-500{color:var(--color-teal-500)}.dark .dark\:text-teal-600{color:var(--color-teal-600)}.dark .dark\:text-teal-700{color:var(--color-teal-700)}.dark .dark\:text-teal-800{color:var(--color-teal-800)}.dark .dark\:text-teal-900{color:var(--color-teal-900)}.dark .dark\:text-white{color:var(--color-white)}.dark .dark\:text-yellow-50{color:var(--color-yellow-50)}.dark .dark\:text-yellow-100{color:var(--color-yellow-100)}.dark .dark\:text-yellow-200{color:var(--color-yellow-200)}.dark .dark\:text-yellow-300{color:var(--color-yellow-300)}.dark .dark\:text-yellow-400{color:var(--color-yellow-400)}.dark .dark\:text-yellow-500{color:var(--color-yellow-500)}.dark .dark\:text-yellow-600{color:var(--color-yellow-600)}.dark .dark\:text-yellow-700{color:var(--color-yellow-700)}.dark .dark\:text-yellow-800{color:var(--color-yellow-800)}.dark .dark\:text-yellow-900{color:var(--color-yellow-900)}.dark .dark\:italic{font-style:italic}.dark .dark\:underline{text-decoration-line:underline}.dark .dark\:placeholder-gray-400::placeholder{color:var(--color-gray-400)}.dark .dark\:placeholder-gray-500::placeholder{color:var(--color-gray-500)}.dark .dark\:placeholder-gray-600::placeholder{color:var(--color-gray-600)}.dark .dark\:opacity-0{opacity:0}.dark .dark\:opacity-25{opacity:.25}.dark .dark\:opacity-50{opacity:.5}.dark .dark\:opacity-75{opacity:.75}.dark .dark\:opacity-100{opacity:1}.dark .dark\:shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-purple-900\/50{--tw-shadow-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:shadow-purple-900\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-purple-900) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.dark .dark\:ring-blue-50{--tw-ring-color:var(--color-blue-50)}.dark .dark\:ring-blue-100{--tw-ring-color:var(--color-blue-100)}.dark .dark\:ring-blue-200{--tw-ring-color:var(--color-blue-200)}.dark .dark\:ring-blue-300{--tw-ring-color:var(--color-blue-300)}.dark .dark\:ring-blue-400{--tw-ring-color:var(--color-blue-400)}.dark .dark\:ring-blue-500{--tw-ring-color:var(--color-blue-500)}.dark .dark\:ring-blue-600{--tw-ring-color:var(--color-blue-600)}.dark .dark\:ring-blue-700{--tw-ring-color:var(--color-blue-700)}.dark .dark\:ring-blue-800{--tw-ring-color:var(--color-blue-800)}.dark .dark\:ring-blue-900{--tw-ring-color:var(--color-blue-900)}.dark .dark\:ring-gray-50{--tw-ring-color:var(--color-gray-50)}.dark .dark\:ring-gray-100{--tw-ring-color:var(--color-gray-100)}.dark .dark\:ring-gray-200{--tw-ring-color:var(--color-gray-200)}.dark .dark\:ring-gray-300{--tw-ring-color:var(--color-gray-300)}.dark .dark\:ring-gray-400{--tw-ring-color:var(--color-gray-400)}.dark .dark\:ring-gray-500{--tw-ring-color:var(--color-gray-500)}.dark .dark\:ring-gray-600{--tw-ring-color:var(--color-gray-600)}.dark .dark\:ring-gray-700{--tw-ring-color:var(--color-gray-700)}.dark .dark\:ring-gray-800{--tw-ring-color:var(--color-gray-800)}.dark .dark\:ring-gray-900{--tw-ring-color:var(--color-gray-900)}.dark .dark\:ring-offset-gray-800{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:ring-offset-gray-900{--tw-ring-offset-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.dark .dark\:group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.dark .dark\:group-hover\:text-gray-100:is(:where(.group):hover *){color:var(--color-gray-100)}.dark .dark\:group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.dark .dark\:selection\:bg-blue-800 ::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:bg-blue-800::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:text-blue-100 ::selection{color:var(--color-blue-100)}.dark .dark\:selection\:text-blue-100::selection{color:var(--color-blue-100)}.dark .dark\:odd\:bg-gray-800:nth-child(odd){background-color:var(--color-gray-800)}.dark .dark\:odd\:bg-gray-900:nth-child(odd){background-color:var(--color-gray-900)}.dark .dark\:even\:bg-gray-800:nth-child(2n){background-color:var(--color-gray-800)}.dark .dark\:even\:bg-gray-900:nth-child(2n){background-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:hover\:border-amber-50:hover{border-color:var(--color-amber-50)}.dark .dark\:hover\:border-amber-100:hover{border-color:var(--color-amber-100)}.dark .dark\:hover\:border-amber-200:hover{border-color:var(--color-amber-200)}.dark .dark\:hover\:border-amber-300:hover{border-color:var(--color-amber-300)}.dark .dark\:hover\:border-amber-400:hover{border-color:var(--color-amber-400)}.dark .dark\:hover\:border-amber-500:hover{border-color:var(--color-amber-500)}.dark .dark\:hover\:border-amber-600:hover{border-color:var(--color-amber-600)}.dark .dark\:hover\:border-amber-700:hover{border-color:var(--color-amber-700)}.dark .dark\:hover\:border-amber-800:hover{border-color:var(--color-amber-800)}.dark .dark\:hover\:border-amber-900:hover{border-color:var(--color-amber-900)}.dark .dark\:hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.dark .dark\:hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.dark .dark\:hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.dark .dark\:hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.dark .dark\:hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.dark .dark\:hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.dark .dark\:hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.dark .dark\:hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.dark .dark\:hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.dark .dark\:hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.dark .dark\:hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.dark .dark\:hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.dark .dark\:hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.dark .dark\:hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.dark .dark\:hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.dark .dark\:hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.dark .dark\:hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.dark .dark\:hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.dark .dark\:hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.dark .dark\:hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.dark .dark\:hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.dark .dark\:hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.dark .dark\:hover\:bg-amber-200:hover{background-color:var(--color-amber-200)}.dark .dark\:hover\:bg-amber-300:hover{background-color:var(--color-amber-300)}.dark .dark\:hover\:bg-amber-400:hover{background-color:var(--color-amber-400)}.dark .dark\:hover\:bg-amber-500:hover{background-color:var(--color-amber-500)}.dark .dark\:hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.dark .dark\:hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.dark .dark\:hover\:bg-amber-800:hover{background-color:var(--color-amber-800)}.dark .dark\:hover\:bg-amber-900:hover{background-color:var(--color-amber-900)}.dark .dark\:hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.dark .dark\:hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.dark .dark\:hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.dark .dark\:hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.dark .dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark .dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.dark .dark\:hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.dark .dark\:hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.dark .dark\:hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.dark .dark\:hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .dark\:hover\:bg-blue-900\/50:hover{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/50:hover{background-color:color-mix(in oklab,var(--color-blue-900) 50%,transparent)}}.dark .dark\:hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.dark .dark\:hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.dark .dark\:hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.dark .dark\:hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.dark .dark\:hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.dark .dark\:hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.dark .dark\:hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.dark .dark\:hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.dark .dark\:hover\:bg-gray-700\/30:hover{background-color:#3641534d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/30:hover{background-color:color-mix(in oklab,var(--color-gray-700) 30%,transparent)}}.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.dark .dark\:hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.dark .dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark .dark\:hover\:bg-green-50:hover{background-color:var(--color-green-50)}.dark .dark\:hover\:bg-green-100:hover{background-color:var(--color-green-100)}.dark .dark\:hover\:bg-green-200:hover{background-color:var(--color-green-200)}.dark .dark\:hover\:bg-green-300:hover{background-color:var(--color-green-300)}.dark .dark\:hover\:bg-green-400:hover{background-color:var(--color-green-400)}.dark .dark\:hover\:bg-green-500:hover{background-color:var(--color-green-500)}.dark .dark\:hover\:bg-green-600:hover{background-color:var(--color-green-600)}.dark .dark\:hover\:bg-green-700:hover{background-color:var(--color-green-700)}.dark .dark\:hover\:bg-green-800:hover{background-color:var(--color-green-800)}.dark .dark\:hover\:bg-green-900:hover{background-color:var(--color-green-900)}.dark .dark\:hover\:bg-green-900\/30:hover{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-green-900\/30:hover{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .dark\:hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.dark .dark\:hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.dark .dark\:hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.dark .dark\:hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.dark .dark\:hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.dark .dark\:hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.dark .dark\:hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.dark .dark\:hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.dark .dark\:hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.dark .dark\:hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.dark .dark\:hover\:bg-indigo-900\/30:hover{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-indigo-900\/30:hover{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .dark\:hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.dark .dark\:hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.dark .dark\:hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.dark .dark\:hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.dark .dark\:hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.dark .dark\:hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.dark .dark\:hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.dark .dark\:hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.dark .dark\:hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.dark .dark\:hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:color-mix(in oklab,var(--color-orange-900) 30%,transparent)}}.dark .dark\:hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.dark .dark\:hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.dark .dark\:hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.dark .dark\:hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.dark .dark\:hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.dark .dark\:hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.dark .dark\:hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.dark .dark\:hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.dark .dark\:hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.dark .dark\:hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.dark .dark\:hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.dark .dark\:hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.dark .dark\:hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.dark .dark\:hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.dark .dark\:hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.dark .dark\:hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.dark .dark\:hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.dark .dark\:hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.dark .dark\:hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.dark .dark\:hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.dark .dark\:hover\:bg-purple-900\/30:hover{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-purple-900\/30:hover{background-color:color-mix(in oklab,var(--color-purple-900) 30%,transparent)}}.dark .dark\:hover\:bg-red-50:hover{background-color:var(--color-red-50)}.dark .dark\:hover\:bg-red-100:hover{background-color:var(--color-red-100)}.dark .dark\:hover\:bg-red-200:hover{background-color:var(--color-red-200)}.dark .dark\:hover\:bg-red-300:hover{background-color:var(--color-red-300)}.dark .dark\:hover\:bg-red-400:hover{background-color:var(--color-red-400)}.dark .dark\:hover\:bg-red-500:hover{background-color:var(--color-red-500)}.dark .dark\:hover\:bg-red-600:hover{background-color:var(--color-red-600)}.dark .dark\:hover\:bg-red-700:hover{background-color:var(--color-red-700)}.dark .dark\:hover\:bg-red-800:hover{background-color:var(--color-red-800)}.dark .dark\:hover\:bg-red-900:hover{background-color:var(--color-red-900)}.dark .dark\:hover\:bg-red-900\/20:hover{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/20:hover{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .dark\:hover\:bg-red-900\/30:hover{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/30:hover{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .dark\:hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.dark .dark\:hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.dark .dark\:hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.dark .dark\:hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.dark .dark\:hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.dark .dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.dark .dark\:hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.dark .dark\:hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.dark .dark\:hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.dark .dark\:hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.dark .dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.dark .dark\:hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.dark .dark\:hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.dark .dark\:hover\:bg-yellow-900\/30:hover{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-yellow-900\/30:hover{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .dark\:hover\:text-black:hover{color:var(--color-black)}.dark .dark\:hover\:text-blue-50:hover{color:var(--color-blue-50)}.dark .dark\:hover\:text-blue-100:hover{color:var(--color-blue-100)}.dark .dark\:hover\:text-blue-200:hover{color:var(--color-blue-200)}.dark .dark\:hover\:text-blue-300:hover{color:var(--color-blue-300)}.dark .dark\:hover\:text-blue-400:hover{color:var(--color-blue-400)}.dark .dark\:hover\:text-blue-500:hover{color:var(--color-blue-500)}.dark .dark\:hover\:text-blue-600:hover{color:var(--color-blue-600)}.dark .dark\:hover\:text-blue-700:hover{color:var(--color-blue-700)}.dark .dark\:hover\:text-blue-800:hover{color:var(--color-blue-800)}.dark .dark\:hover\:text-blue-900:hover{color:var(--color-blue-900)}.dark .dark\:hover\:text-gray-50:hover{color:var(--color-gray-50)}.dark .dark\:hover\:text-gray-100:hover{color:var(--color-gray-100)}.dark .dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark .dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}.dark .dark\:hover\:text-gray-400:hover{color:var(--color-gray-400)}.dark .dark\:hover\:text-gray-500:hover{color:var(--color-gray-500)}.dark .dark\:hover\:text-gray-600:hover{color:var(--color-gray-600)}.dark .dark\:hover\:text-gray-700:hover{color:var(--color-gray-700)}.dark .dark\:hover\:text-gray-800:hover{color:var(--color-gray-800)}.dark .dark\:hover\:text-gray-900:hover{color:var(--color-gray-900)}.dark .dark\:hover\:text-green-50:hover{color:var(--color-green-50)}.dark .dark\:hover\:text-green-100:hover{color:var(--color-green-100)}.dark .dark\:hover\:text-green-200:hover{color:var(--color-green-200)}.dark .dark\:hover\:text-green-300:hover{color:var(--color-green-300)}.dark .dark\:hover\:text-green-400:hover{color:var(--color-green-400)}.dark .dark\:hover\:text-green-500:hover{color:var(--color-green-500)}.dark .dark\:hover\:text-green-600:hover{color:var(--color-green-600)}.dark .dark\:hover\:text-green-700:hover{color:var(--color-green-700)}.dark .dark\:hover\:text-green-800:hover{color:var(--color-green-800)}.dark .dark\:hover\:text-green-900:hover{color:var(--color-green-900)}.dark .dark\:hover\:text-purple-200:hover{color:var(--color-purple-200)}.dark .dark\:hover\:text-purple-300:hover{color:var(--color-purple-300)}.dark .dark\:hover\:text-red-50:hover{color:var(--color-red-50)}.dark .dark\:hover\:text-red-100:hover{color:var(--color-red-100)}.dark .dark\:hover\:text-red-200:hover{color:var(--color-red-200)}.dark .dark\:hover\:text-red-300:hover{color:var(--color-red-300)}.dark .dark\:hover\:text-red-400:hover{color:var(--color-red-400)}.dark .dark\:hover\:text-red-500:hover{color:var(--color-red-500)}.dark .dark\:hover\:text-red-600:hover{color:var(--color-red-600)}.dark .dark\:hover\:text-red-700:hover{color:var(--color-red-700)}.dark .dark\:hover\:text-red-800:hover{color:var(--color-red-800)}.dark .dark\:hover\:text-red-900:hover{color:var(--color-red-900)}.dark .dark\:hover\:text-slate-800:hover{color:var(--color-slate-800)}.dark .dark\:hover\:text-white:hover{color:var(--color-white)}}.dark .dark\:focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.dark .dark\:focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.dark .dark\:focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.dark .dark\:focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.dark .dark\:focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.dark .dark\:focus\:text-gray-100:focus{color:var(--color-gray-100)}.dark .dark\:focus\:text-gray-200:focus{color:var(--color-gray-200)}.dark .dark\:focus\:text-gray-300:focus{color:var(--color-gray-300)}.dark .dark\:focus\:text-white:focus{color:var(--color-white)}.dark .dark\:focus\:ring-amber-400:focus{--tw-ring-color:var(--color-amber-400)}.dark .dark\:focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.dark .dark\:focus\:ring-amber-600:focus{--tw-ring-color:var(--color-amber-600)}.dark .dark\:focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.dark .dark\:focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.dark .dark\:focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.dark .dark\:focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.dark .dark\:focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.dark .dark\:focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.dark .dark\:focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.dark .dark\:focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.dark .dark\:focus\:ring-indigo-400:focus{--tw-ring-color:var(--color-indigo-400)}.dark .dark\:focus\:ring-red-400:focus{--tw-ring-color:var(--color-red-400)}.dark .dark\:focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.dark .dark\:focus\:ring-slate-500:focus{--tw-ring-color:var(--color-slate-500)}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.dark .dark\:active\:bg-gray-600:active{background-color:var(--color-gray-600)}.dark .dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark .dark\:active\:bg-gray-800:active{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-gray-700:disabled{background-color:var(--color-gray-700)}.dark .dark\:disabled\:bg-gray-800:disabled{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-gray-900:disabled{background-color:var(--color-gray-900)}.dark .dark\:disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.dark .dark\:disabled\:text-gray-600:disabled{color:var(--color-gray-600)}@media(hover:hover){.dark .dark\:disabled\:hover\:bg-indigo-500:disabled:hover{background-color:var(--color-indigo-500)}}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_table\]\:border table{border-style:var(--tw-border-style);border-width:1px}.\[\&_table\]\:border-gray-300 table{border-color:var(--color-gray-300)}.dark .dark\:\[\&_table\]\:border-gray-600 table{border-color:var(--color-gray-600)}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-gray-300 td{border-color:var(--color-gray-300)}.\[\&_td\]\:px-4 td{padding-inline:calc(var(--spacing) * 4)}.\[\&_td\]\:py-2 td{padding-block:calc(var(--spacing) * 2)}.dark .dark\:\[\&_td\]\:border-gray-600 td{border-color:var(--color-gray-600)}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-gray-300 th{border-color:var(--color-gray-300)}.\[\&_th\]\:bg-gray-100 th{background-color:var(--color-gray-100)}.\[\&_th\]\:px-4 th{padding-inline:calc(var(--spacing) * 4)}.\[\&_th\]\:py-2 th{padding-block:calc(var(--spacing) * 2)}.\[\&_th\]\:text-left th{text-align:left}.dark .dark\:\[\&_th\]\:border-gray-600 th{border-color:var(--color-gray-600)}.dark .dark\:\[\&_th\]\:bg-gray-800 th{background-color:var(--color-gray-800)}.\[\&_tr\:hover\]\:bg-gray-50 tr:hover{background-color:var(--color-gray-50)}.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}}.dashboard-content{max-width:80rem;margin:0 auto}.dashboard-stats-grid{gap:var(--spacing-5);margin-bottom:var(--spacing-8);grid-template-columns:1fr;display:grid}@media(min-width:768px){.dashboard-stats-grid{grid-template-columns:repeat(2,1fr)}}@media(min-width:1024px){.dashboard-stats-grid{grid-template-columns:repeat(4,1fr)}}.dashboard-main-grid{gap:var(--spacing-6);grid-template-columns:1fr;display:grid}@media(min-width:1024px){.dashboard-main-grid{grid-template-columns:repeat(5,1fr)}}.dashboard-column-left{gap:var(--spacing-6);flex-direction:column;display:flex}@media(min-width:1024px){.dashboard-column-left{grid-column:span 3}}.dashboard-column-right{gap:var(--spacing-6);flex-direction:column;display:flex}@media(min-width:1024px){.dashboard-column-right{grid-column:span 2}}.progress-bar-container{background-color:var(--color-slate-100);border-radius:var(--radius-full);width:100%;height:.5rem;overflow:hidden}.dark .progress-bar-container{background-color:var(--color-slate-700)}.progress-bar-fill{border-radius:var(--radius-full);height:.5rem;transition:width var(--transition-normal) var(--ease-in-out)}.progress-bar-emerald{background-color:var(--color-emerald-500)}.progress-bar-blue{background-color:var(--color-blue-500)}.progress-bar-slate{background-color:var(--color-slate-600)}.dark .progress-bar-slate{background-color:var(--color-slate-500)}.module-list{gap:var(--spacing-3);flex-direction:column;display:flex}.module-item{padding:var(--spacing-2) 0;justify-content:space-between;align-items:center;display:flex}.module-info{flex-direction:column;display:flex}.module-name{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .module-name{color:#fff}.module-version{font-size:var(--text-xs);color:var(--color-slate-500)}.dark .module-version{color:var(--color-slate-400)}.module-badge-active{padding:var(--spacing-1) var(--spacing-2);font-size:var(--text-xs);font-weight:var(--font-medium);background-color:var(--color-green-100);color:var(--color-green-700);border-radius:var(--radius-full)}.dark .module-badge-active{background-color:var(--color-green-900);color:var(--color-green-400)}.module-badge-inactive{padding:var(--spacing-1) var(--spacing-2);font-size:var(--text-xs);font-weight:var(--font-medium);background-color:var(--color-slate-100);color:var(--color-slate-600);border-radius:var(--radius-full)}.dark .module-badge-inactive{background-color:var(--color-slate-700);color:var(--color-slate-400)}.activity-list{gap:var(--spacing-4);flex-direction:column;display:flex}.activity-item{align-items:flex-start;gap:var(--spacing-3);display:flex}.activity-dot{width:.5rem;height:.5rem;margin-top:var(--spacing-2);border-radius:var(--radius-full);background-color:var(--color-blue-500);flex-shrink:0}.activity-content{flex:1;min-width:0}.activity-title{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .activity-title{color:#fff}.activity-desc{font-size:var(--text-sm);color:var(--color-slate-500)}.dark .activity-desc{color:var(--color-slate-400)}.activity-time{font-size:var(--text-xs);color:var(--color-slate-400);white-space:nowrap}.dark .activity-time{color:var(--color-slate-500)}.alert-list{gap:var(--spacing-3);flex-direction:column;display:flex}.alert-item{align-items:flex-start;gap:var(--spacing-3);padding:var(--spacing-3);border-radius:var(--radius-lg);display:flex}.alert-bg-info{background-color:var(--color-blue-50)}.dark .alert-bg-info{background-color:var(--color-slate-800)}.alert-bg-warning{background-color:var(--color-yellow-50)}.dark .alert-bg-warning{background-color:var(--color-slate-800)}.alert-bg-error{background-color:var(--color-red-50)}.dark .alert-bg-error{background-color:var(--color-slate-800)}.alert-bg-success{background-color:var(--color-green-50)}.dark .alert-bg-success{background-color:var(--color-slate-800)}.alert-dot{width:.5rem;height:.5rem;margin-top:var(--spacing-2);border-radius:var(--radius-full);flex-shrink:0}.alert-dot-info{background-color:var(--color-blue-500)}.alert-dot-warning{background-color:var(--color-yellow-500)}.alert-dot-error{background-color:var(--color-red-500)}.alert-dot-success{background-color:var(--color-green-500)}.alert-content{flex:1}.alert-title{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .alert-title{color:#fff}.alert-time{font-size:var(--text-xs);color:var(--color-slate-500);margin-top:var(--spacing-1)}.dark .alert-time{color:var(--color-slate-400)}.text-subtitle{font-size:var(--text-sm);color:var(--color-slate-500)}.dark .text-subtitle{color:var(--color-slate-400)}.section-margin{margin-bottom:var(--spacing-6)}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-between-mb{margin-bottom:var(--spacing-2);justify-content:space-between;align-items:center;display:flex}.resource-label{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-700)}.dark .resource-label{color:var(--color-slate-300)}.resource-value{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-700)}.dark .resource-value{color:var(--color-slate-300)}.icon-stat{width:1rem;height:1rem}.icon-trend{width:.875rem;height:.875rem}@media(max-width:640px){.input-split{flex-wrap:wrap}.input-split input{flex:1;min-width:0}.page-header{flex-direction:column;align-items:flex-start;gap:1rem}.page-actions{justify-content:flex-start;width:100%}}@media(min-width:640px){.responsive-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.responsive-grid,.grid-cols-1-lg-3{grid-template-columns:repeat(3,minmax(0,1fr))}.col-span-1-lg{grid-column:span 1/span 1}.col-span-2-lg{grid-column:span 2/span 2}}@media(max-width:480px){.responsive-grid{grid-template-columns:1fr!important}}@media(max-width:768px){.table-mobile-cards{display:block}.table-mobile-cards>*+*{margin-top:1rem}.table-desktop{display:none!important}}@media(max-width:640px){.modal{max-width:calc(100% - 2rem);max-height:calc(100vh - 2rem);margin-left:1rem;margin-right:1rem;overflow-y:auto}.modal-overlay{align-items:flex-start;padding:2rem 1rem 1rem}.modal-footer{flex-direction:column;gap:.5rem}.modal-footer .btn,.card-account{width:100%}}.sidebar-scrollbar{scrollbar-width:thin;scrollbar-color:transparent transparent;transition:scrollbar-color .3s}.sidebar-scrollbar:hover{scrollbar-color:#d1d5db transparent}.dark .sidebar-scrollbar:hover{scrollbar-color:#4b5563 transparent}.sidebar-scrollbar::-webkit-scrollbar{width:6px}.sidebar-scrollbar::-webkit-scrollbar-track{background:0 0}.sidebar-scrollbar::-webkit-scrollbar-thumb{background:0 0;border-radius:3px;transition:background .3s}.sidebar-scrollbar:hover::-webkit-scrollbar-thumb{background:#d1d5db}.dark .sidebar-scrollbar:hover::-webkit-scrollbar-thumb{background:#4b5563}.sidebar-scrollbar::-webkit-scrollbar-thumb:hover{background:#9ca3af}.dark .sidebar-scrollbar::-webkit-scrollbar-thumb:hover{background:#6b7280}@property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.yarl__fullsize{width:100%;height:100%}.yarl__relative{position:relative}.yarl__portal{position:fixed;inset:0;opacity:0;overflow:hidden;z-index:var(--yarl__portal_zindex,9999);transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);pointer-events:auto}.yarl__portal_open{opacity:1}.yarl__container{position:absolute;inset:0;overflow:hidden;background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));outline:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;touch-action:var(--yarl__controller_touch_action,none);overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain)}.yarl__carousel{display:flex;flex:0 0 auto;height:100%;align-content:center;justify-content:center;align-items:stretch;width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%));transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));opacity:var(--yarl__pull_opacity,1)}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{display:flex;justify-content:center;align-content:center;align-items:center}.yarl__slide{flex:1;position:relative;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%)}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-width:100%;max-height:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-transform:translateZ(0);-webkit-backface-visibility:hidden;-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{position:absolute;top:50%;left:50%;transform:translate(-50%) translateY(-50%);line-height:0}.yarl__slide_loading{color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));animation:yarl__delayed_fadein 1s linear}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{width:var(--yarl__slide_icon_error_size,48px);height:var(--yarl__slide_icon_error_size,48px);color:var(--yarl__slide_icon_error_color,red)}@media(prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{position:absolute;inset:0 0 auto auto;display:flex;justify-content:flex-end;padding:var(--yarl__toolbar_padding,8px)}[dir=rtl] .yarl__toolbar{inset:0 auto auto 0}.yarl__icon{width:var(--yarl__icon_size,32px);height:var(--yarl__icon_size,32px)}.yarl__button{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);margin:var(--yarl__button_margin,0);outline:none;line-height:0;padding:var(--yarl__button_padding,8px);color:var(--yarl__color_button,hsla(0,0%,100%,.8));filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media(hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{position:absolute;top:50%;transform:translateY(-50%);padding:var(--yarl__navigation_button_padding,24px 16px)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}.yarl__counter{position:var(--yarl__counter_position,absolute);top:var(--yarl__counter_top,0);left:var(--yarl__counter_left,0);bottom:var(--yarl__counter_bottom,unset);right:var(--yarl__counter_right,unset);color:var(--yarl__counter_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__counter_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));margin:var(--yarl__counter_margin,var(--yarl__toolbar_padding,8px));padding:var(--yarl__counter_padding,var(--yarl__button_padding,8px));line-height:var(--yarl__counter_line_height,var(--yarl__icon_size,32px));-webkit-user-select:var(--yarl__counter_user_select,none);-moz-user-select:var(--yarl__counter_user_select,none);user-select:var(--yarl__counter_user_select,none)}[dir=rtl] .yarl__counter{left:var(--yarl__counter_rtl_left,unset);right:var(--yarl__counter_rtl_right,0)}.yarl__thumbnails{display:flex;height:100%}.yarl__thumbnails_bottom,.yarl__thumbnails_end .yarl__thumbnails_track,.yarl__thumbnails_start .yarl__thumbnails_track,.yarl__thumbnails_top{flex-direction:column}.yarl__thumbnails_wrapper{flex:1;position:relative}.yarl__thumbnails_container{flex:0 0 auto;background-color:var(--yarl__thumbnails_container_background_color,var(--yarl__color_backdrop,#000));padding:var(--yarl__thumbnails_container_padding,16px);position:relative;overflow:hidden;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__thumbnails_vignette{position:absolute;pointer-events:none;--yarl__thumbnails_vignette_size:12%}@media(min-width:1200px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:8%}}@media(min-width:2000px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:5%}}.yarl__thumbnails_bottom .yarl__thumbnails_vignette,.yarl__thumbnails_top .yarl__thumbnails_vignette{height:100%;left:0;right:0;background:linear-gradient(to right,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%)}.yarl__thumbnails_end .yarl__thumbnails_vignette,.yarl__thumbnails_start .yarl__thumbnails_vignette{width:100%;top:0;bottom:0;background:linear-gradient(to bottom,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%)}.yarl__thumbnails_track{outline:none;gap:var(--yarl__thumbnails_thumbnail_gap,16px)}.yarl__thumbnails_thumbnail{flex:0 0 auto;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--yarl__thumbnails_thumbnail_background,#000);border-width:var(--yarl__thumbnails_thumbnail_border,1px);border-style:var(--yarl__thumbnails_thumbnail_border_style,solid);border-color:var(--yarl__thumbnails_thumbnail_border_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));border-radius:var(--yarl__thumbnails_thumbnail_border_radius,4px);-webkit-tap-highlight-color:transparent;position:relative;overflow:hidden;padding:var(--yarl__thumbnails_thumbnail_padding,4px);outline:none;width:var(--yarl__thumbnails_thumbnail_width,120px);height:var(--yarl__thumbnails_thumbnail_height,80px);box-sizing:content-box}.yarl__thumbnails_thumbnail_active{border-color:var(--yarl__thumbnails_thumbnail_active_border_color,var(--yarl__color_button_active,#fff))}.yarl__thumbnails_thumbnail_fadein{opacity:0;animation:yarl__thumbnails_thumbnail_fadein var(--yarl__thumbnails_thumbnail_fadein_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadein_delay,0s) forwards}.yarl__thumbnails_thumbnail_fadeout{animation:yarl__thumbnails_thumbnail_fadeout var(--yarl__thumbnails_thumbnail_fadeout_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadeout_delay,0s) forwards;cursor:unset}.yarl__thumbnails_thumbnail_placeholder{visibility:hidden;cursor:unset}.yarl__thumbnails_thumbnail:focus{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail:focus:not(:focus-visible){box-shadow:unset}.yarl__thumbnails_thumbnail:focus-visible{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail_icon{color:var(--yarl__thumbnails_thumbnail_icon_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__thumbnails_thumbnail_icon_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));position:absolute;top:50%;left:50%;transform:translate(-50%) translateY(-50%);width:var(--yarl__thumbnails_thumbnail_icon_size,32px);height:var(--yarl__thumbnails_thumbnail_icon_size,32px)}@keyframes yarl__thumbnails_thumbnail_fadein{0%{opacity:0}to{opacity:1}}@keyframes yarl__thumbnails_thumbnail_fadeout{0%{opacity:1}to{opacity:0}} +/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Pretendard", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--aspect-video:16 / 9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{color:var(--color-gray-900);font-size:.875rem;line-height:1.5}.dark :is(body){color:var(--color-gray-100)}}@layer components{.admin_layout_root{background-color:var(--color-gray-50);height:100vh;display:flex;overflow:hidden}.dark .admin_layout_root{background-color:var(--color-gray-900)}.mobile_overlay{inset:calc(var(--spacing) * 0);z-index:40;background-color:#00000080;position:fixed}.left_sidebar_area{inset-block:calc(var(--spacing) * 0);left:calc(var(--spacing) * 0);z-index:50;width:calc(var(--spacing) * 64);background-color:var(--color-white);flex-direction:column;display:flex;position:relative}.dark .left_sidebar_area{background-color:var(--color-gray-800)}.left_sidebar_area{transition:width .3s}.left_sidebar_area_collapsed{width:calc(var(--spacing) * 0);overflow:hidden}.left_sidebar_area_portable{left:calc(var(--spacing) * 0);z-index:50;width:calc(var(--spacing) * 64);background-color:var(--color-white);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.3s;flex-direction:column;transition-duration:.3s;display:flex;position:fixed}.dark .left_sidebar_area_portable{background-color:var(--color-gray-800)}.left_sidebar_area_portable{top:4rem;bottom:0;transform:translate(-100%)}.left_sidebar_area_portable_open{transform:translate(0)}.sidebar_header{height:calc(var(--spacing) * 16);padding-inline:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.sidebar_logo{height:calc(var(--spacing) * 10);min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);flex:1;display:flex}.logo_image{max-height:calc(var(--spacing) * 10);object-fit:contain;width:auto}.admin_sidebar_menu{padding-top:calc(var(--spacing) * 4);box-shadow:inset 3px 2px 2px var(--tw-shadow-color,#0000000a);flex:1;overflow-y:auto}.admin_sidebar_menu_portable{padding-top:calc(var(--spacing) * 4);flex:1;overflow-y:auto}.admin_sidebar_empty{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 8);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-400)}.dark .admin_sidebar_empty{color:var(--color-gray-500)}.admin_sidebar_empty_icon{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));color:var(--color-gray-300)}.dark .admin_sidebar_empty_icon{color:var(--color-gray-600)}.sidebar_footer{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100);padding:calc(var(--spacing) * 3);flex-shrink:0}.dark .sidebar_footer{border-color:var(--color-gray-700)}.sidebar_view_site_link{height:calc(var(--spacing) * 9);align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));text-decoration-line:none;display:flex}@media(hover:hover){.sidebar_view_site_link:hover{background-color:var(--color-slate-100);color:var(--color-slate-900)}}.dark .sidebar_view_site_link{color:var(--color-slate-300)}@media(hover:hover){.dark .sidebar_view_site_link:hover{background-color:var(--color-slate-700);color:var(--color-white)}}.sidebar_view_site_icon{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));flex-shrink:0}.sidebar_view_site_label{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.right_content_wrapper{min-width:calc(var(--spacing) * 0);flex-direction:column;flex:1;display:flex}.right_content_area{min-height:calc(var(--spacing) * 0);min-width:calc(var(--spacing) * 0);flex:1;overflow:auto}.main_header{z-index:50;height:calc(var(--spacing) * 16);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 8);flex-shrink:0;justify-content:space-between;align-items:center;display:flex;position:relative}.dark .main_header{background-color:var(--color-gray-800)}.main_header{box-shadow:0 1px 3px #00000014}.header_left{flex:1;align-items:center;display:flex}.header_right{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.menu_toggle_btn{margin-left:calc(var(--spacing) * -2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);color:var(--color-slate-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.menu_toggle_btn:hover{background-color:var(--color-slate-100);color:var(--color-slate-900)}}.dark .menu_toggle_btn{color:var(--color-slate-400)}@media(hover:hover){.dark .menu_toggle_btn:hover{background-color:var(--color-slate-700);color:var(--color-white)}}.menu_toggle_btn_icon{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.user_avatar_section{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 3);display:flex;position:relative}.user_avatar{height:calc(var(--spacing) * 9);width:calc(var(--spacing) * 9);background-color:var(--color-gray-200);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex;overflow:hidden}.dark .user_avatar{background-color:var(--color-gray-600)}.user_avatar_img{object-fit:cover;width:100%;height:100%}.user_info{display:none}@media(min-width:48rem){.user_info{display:block}}.mobile_header_portable{z-index:50;height:calc(var(--spacing) * 16);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex;position:relative}.dark .mobile_header_portable{background-color:var(--color-gray-800)}.mobile_header_portable{box-shadow:0 1px 3px #00000014}.mobile_header_left{align-items:center;gap:calc(var(--spacing) * 3);display:flex}.mobile_header_right{align-items:center;gap:calc(var(--spacing) * 2);flex-shrink:0;display:flex}.mobile_logo{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);flex:1;display:flex}.mobile_logo_image{height:calc(var(--spacing) * 8);object-fit:contain;width:auto;max-width:100%}.main_content{min-height:fit-content}.layout_warnings{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2)}.text-primary{color:var(--color-gray-900)}.dark .text-primary{color:var(--color-white)}.text-secondary{color:var(--color-gray-600)}.dark .text-secondary{color:var(--color-gray-400)}.text-muted,.dark .text-muted{color:var(--color-gray-500)}.text-tertiary{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .text-tertiary{color:var(--color-gray-400)}.text-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .text-label{color:var(--color-gray-400)}.text-label-subtle{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .text-label-subtle{color:var(--color-gray-400)}.text-heading{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .text-heading{color:var(--color-white)}.section-heading{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-heading{color:var(--color-white)}.section-title{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-title{color:var(--color-white)}.section-description{margin-bottom:calc(var(--spacing) * 6);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .section-description{color:var(--color-gray-400)}.card-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .card-title{color:var(--color-white)}.card-title-aside{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);display:flex}.card-description{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .card-description{color:var(--color-gray-400)}.admin-card>.card-title{margin-bottom:calc(var(--spacing) * 4)}.admin-card>.card-title:has(+.card-description){margin-bottom:calc(var(--spacing) * 0)}.admin-card>.card-description{margin-bottom:calc(var(--spacing) * 4)}.card-header{margin-bottom:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);--tw-leading:1;line-height:1;display:flex}.card-header>*{--tw-leading:1;line-height:1}.text-body{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}.dark .text-body{color:var(--color-gray-300)}.icon-muted{color:var(--color-gray-400)}.dark .icon-muted{color:var(--color-gray-500)}.bg-success{background-color:var(--color-green-100)}.dark .bg-success{background-color:var(--color-green-900)}.bg-success-light{background-color:var(--color-green-50)}.dark .bg-success-light{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .bg-success-light{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.bg-success-solid{background-color:var(--color-green-600)}.dark .bg-success-solid{background-color:var(--color-green-500)}.bg-error{background-color:var(--color-red-100)}.dark .bg-error{background-color:var(--color-red-900)}.bg-error-light{background-color:var(--color-red-50)}.dark .bg-error-light{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .bg-error-light{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.bg-error-solid{background-color:var(--color-red-600)}.dark .bg-error-solid{background-color:var(--color-red-500)}.bg-warning{background-color:var(--color-yellow-100)}.dark .bg-warning{background-color:var(--color-yellow-900)}.bg-warning-light{background-color:var(--color-yellow-50)}.dark .bg-warning-light{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .bg-warning-light{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.bg-warning-solid{background-color:var(--color-yellow-600)}.dark .bg-warning-solid{background-color:var(--color-yellow-500)}.bg-info{background-color:var(--color-blue-100)}.dark .bg-info{background-color:var(--color-blue-900)}.bg-info-light{background-color:var(--color-blue-50)}.dark .bg-info-light{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .bg-info-light{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.bg-info-solid{background-color:var(--color-blue-600)}.dark .bg-info-solid{background-color:var(--color-blue-500)}.bg-ready{background-color:var(--color-cyan-100)}.dark .bg-ready{background-color:var(--color-cyan-900)}.bg-ready-light{background-color:var(--color-cyan-50)}.dark .bg-ready-light{background-color:#104e6433}@supports (color:color-mix(in lab,red,red)){.dark .bg-ready-light{background-color:color-mix(in oklab,var(--color-cyan-900) 20%,transparent)}}.bg-ready-solid{background-color:var(--color-cyan-600)}.dark .bg-ready-solid{background-color:var(--color-cyan-400)}.bg-progress{background-color:var(--color-indigo-100)}.dark .bg-progress{background-color:var(--color-indigo-900)}.bg-progress-light{background-color:var(--color-indigo-50)}.dark .bg-progress-light{background-color:#312c8533}@supports (color:color-mix(in lab,red,red)){.dark .bg-progress-light{background-color:color-mix(in oklab,var(--color-indigo-900) 20%,transparent)}}.bg-progress-solid{background-color:var(--color-indigo-600)}.dark .bg-progress-solid{background-color:var(--color-indigo-400)}.bg-complete{background-color:var(--color-pink-100)}.dark .bg-complete{background-color:var(--color-pink-900)}.bg-complete-light{background-color:var(--color-pink-50)}.dark .bg-complete-light{background-color:#86104333}@supports (color:color-mix(in lab,red,red)){.dark .bg-complete-light{background-color:color-mix(in oklab,var(--color-pink-900) 20%,transparent)}}.bg-complete-solid{background-color:var(--color-pink-600)}.dark .bg-complete-solid{background-color:var(--color-pink-400)}.bg-hold{background-color:var(--color-gray-100)}.dark .bg-hold{background-color:var(--color-gray-800)}.bg-hold-light{background-color:var(--color-gray-50)}.dark .bg-hold-light{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .bg-hold-light{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.bg-hold-solid{background-color:var(--color-gray-500)}.dark .bg-hold-solid{background-color:var(--color-gray-400)}.text-success{color:var(--color-green-600)}.dark .text-success{color:var(--color-green-400)}.text-error{color:var(--color-red-600)}.dark .text-error{color:var(--color-red-400)}.text-warning{color:var(--color-yellow-600)}.dark .text-warning{color:var(--color-yellow-400)}.text-info{color:var(--color-blue-600)}.dark .text-info{color:var(--color-blue-400)}.text-ready{color:var(--color-cyan-600)}.dark .text-ready{color:var(--color-cyan-400)}.text-progress{color:var(--color-indigo-600)}.dark .text-progress{color:var(--color-indigo-400)}.text-complete{color:var(--color-pink-600)}.dark .text-complete{color:var(--color-pink-400)}.text-hold{color:var(--color-gray-500)}.dark .text-hold{color:var(--color-gray-400)}.text-success-light{color:var(--color-white)}.dark .text-success-light{color:var(--color-green-900)}.text-error-light{color:var(--color-white)}.dark .text-error-light{color:var(--color-red-900)}.text-warning-light{color:var(--color-white)}.dark .text-warning-light{color:var(--color-yellow-900)}.text-info-light{color:var(--color-white)}.dark .text-info-light{color:var(--color-blue-900)}.text-ready-light{color:var(--color-white)}.dark .text-ready-light{color:var(--color-cyan-900)}.text-progress-light{color:var(--color-white)}.dark .text-progress-light{color:var(--color-indigo-900)}.text-complete-light{color:var(--color-white)}.dark .text-complete-light{color:var(--color-pink-900)}.text-hold-light{color:var(--color-white)}.dark .text-hold-light{color:var(--color-gray-900)}.border-success{border-color:var(--color-green-200)}.dark .border-success{border-color:var(--color-green-800)}.border-error{border-color:var(--color-red-200)}.dark .border-error{border-color:var(--color-red-800)}.border-warning{border-color:var(--color-yellow-200)}.dark .border-warning{border-color:var(--color-yellow-800)}.border-info{border-color:var(--color-blue-200)}.dark .border-info{border-color:var(--color-blue-800)}.border-ready{border-color:var(--color-cyan-200)}.dark .border-ready{border-color:var(--color-cyan-800)}.border-progress{border-color:var(--color-indigo-200)}.dark .border-progress{border-color:var(--color-indigo-800)}.border-complete{border-color:var(--color-pink-200)}.dark .border-complete{border-color:var(--color-pink-800)}.border-hold{border-color:var(--color-gray-300)}.dark .border-hold{border-color:var(--color-gray-600)}.btn-success{background-color:var(--color-green-600);color:var(--color-white)}@media(hover:hover){.btn-success:hover{background-color:var(--color-green-700)}}.dark .btn-success{background-color:var(--color-green-500)}@media(hover:hover){.dark .btn-success:hover{background-color:var(--color-green-600)}}.btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.btn-danger:hover{background-color:var(--color-red-700)}}.dark .btn-danger{background-color:var(--color-red-500)}@media(hover:hover){.dark .btn-danger:hover{background-color:var(--color-red-600)}}.btn-warning{background-color:var(--color-yellow-600);color:var(--color-white)}@media(hover:hover){.btn-warning:hover{background-color:var(--color-yellow-700)}}.dark .btn-warning{background-color:var(--color-yellow-500)}@media(hover:hover){.dark .btn-warning:hover{background-color:var(--color-yellow-600)}}.btn-info{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.btn-info:hover{background-color:var(--color-blue-700)}}.dark .btn-info{background-color:var(--color-blue-500)}@media(hover:hover){.dark .btn-info:hover{background-color:var(--color-blue-600)}}.btn-neutral{background-color:var(--color-gray-600);color:var(--color-white)}@media(hover:hover){.btn-neutral:hover{background-color:var(--color-gray-700)}}.dark .btn-neutral{background-color:var(--color-gray-500)}@media(hover:hover){.dark .btn-neutral:hover{background-color:var(--color-gray-600)}}.btn-sm{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5)}.button{cursor:pointer;justify-content:center;align-items:center;display:inline-flex}.button:disabled{cursor:not-allowed}.btn{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;transition-duration:.2s;display:inline-flex}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-gray-900);color:var(--color-white)}@media(hover:hover){.btn-primary:hover{background-color:var(--color-gray-800)}}.dark .btn-primary{background-color:var(--color-white);color:var(--color-gray-900)}@media(hover:hover){.dark .btn-primary:hover{background-color:var(--color-gray-100)}}.btn-secondary{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);color:var(--color-gray-700)}@media(hover:hover){.btn-secondary:hover{background-color:var(--color-gray-50)}}.dark .btn-secondary{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-300)}@media(hover:hover){.dark .btn-secondary:hover{background-color:var(--color-gray-700)}}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.btn-outline:hover{background-color:var(--color-gray-50)}}.dark .btn-outline{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .btn-outline:hover{background-color:var(--color-gray-700)}}.btn-ghost{color:var(--color-gray-600);background-color:#0000}@media(hover:hover){.btn-ghost:hover{background-color:var(--color-gray-100)}}.dark .btn-ghost{color:var(--color-gray-400)}@media(hover:hover){.dark .btn-ghost:hover{background-color:var(--color-gray-700)}}.btn-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);color:var(--color-gray-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.btn-icon:hover{background-color:var(--color-gray-100)}}.dark .btn-icon{color:var(--color-gray-400)}@media(hover:hover){.dark .btn-icon:hover{background-color:var(--color-gray-700)}}.btn-preset{background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);border-radius:3.40282e38px}.dark .btn-preset{background-color:var(--color-gray-700);color:var(--color-gray-300)}.btn-date{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .btn-date{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-date.active{border-color:var(--color-gray-900);background-color:var(--color-gray-900);color:var(--color-white)}.dark .btn-date.active{border-color:var(--color-white);background-color:var(--color-white);color:var(--color-gray-900)}.btn-badge{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600);border-radius:3.40282e38px}.dark .btn-badge{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-badge.active{border-color:var(--color-gray-900);color:var(--color-gray-900)}.dark .btn-badge.active{border-color:var(--color-white);color:var(--color-blue-400)}.btn-group{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);display:inline-flex;overflow:hidden}.dark .btn-group{border-color:var(--color-gray-700)}.btn-group-item{border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.btn-group-item:hover{background-color:var(--color-gray-50)}}.dark .btn-group-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}@media(hover:hover){.dark .btn-group-item:hover{background-color:var(--color-gray-700)}}.btn-group-item:last-child{border-right-style:var(--tw-border-style);border-right-width:0}.btn-group-item.active{background-color:var(--color-gray-900);color:var(--color-white)}.dark .btn-group-item.active{background-color:var(--color-white);color:var(--color-gray-900)}.btn-badge-green{border-color:var(--color-green-500);background-color:var(--color-green-50);color:var(--color-green-600)}.dark .btn-badge-green{border-color:var(--color-green-400);background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-green{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .btn-badge-green{color:var(--color-green-400)}.btn-badge-blue{border-color:var(--color-blue-500);background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .btn-badge-blue{border-color:var(--color-blue-400);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-blue{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .btn-badge-blue{color:var(--color-blue-400)}.btn-badge-cyan{border-color:var(--color-cyan-500);background-color:var(--color-cyan-50);color:var(--color-cyan-600)}.dark .btn-badge-cyan{border-color:var(--color-cyan-400);background-color:#104e6433}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-cyan{background-color:color-mix(in oklab,var(--color-cyan-900) 20%,transparent)}}.dark .btn-badge-cyan{color:var(--color-cyan-400)}.btn-badge-indigo{border-color:var(--color-indigo-500);background-color:var(--color-indigo-50);color:var(--color-indigo-600)}.dark .btn-badge-indigo{border-color:var(--color-indigo-400);background-color:#312c8533}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-indigo{background-color:color-mix(in oklab,var(--color-indigo-900) 20%,transparent)}}.dark .btn-badge-indigo{color:var(--color-indigo-400)}.btn-badge-pink{border-color:var(--color-pink-500);background-color:var(--color-pink-50);color:var(--color-pink-600)}.dark .btn-badge-pink{border-color:var(--color-pink-400);background-color:#86104333}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-pink{background-color:color-mix(in oklab,var(--color-pink-900) 20%,transparent)}}.dark .btn-badge-pink{color:var(--color-pink-400)}.btn-badge-red{border-color:var(--color-red-500);background-color:var(--color-red-50);color:var(--color-red-600)}.dark .btn-badge-red{border-color:var(--color-red-400);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-red{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .btn-badge-red{color:var(--color-red-400)}.btn-badge-gray{border-color:var(--color-gray-300);background-color:var(--color-gray-50);color:var(--color-gray-600)}.dark .btn-badge-gray{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-400)}.btn-badge-yellow{border-color:var(--color-yellow-500);background-color:var(--color-yellow-50);color:var(--color-yellow-600)}.dark .btn-badge-yellow{border-color:var(--color-yellow-400);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-yellow{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .btn-badge-yellow{color:var(--color-yellow-400)}.btn-badge-orange{border-color:var(--color-orange-500);background-color:var(--color-orange-50);color:var(--color-orange-600)}.dark .btn-badge-orange{border-color:var(--color-orange-400);background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .btn-badge-orange{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .btn-badge-orange{color:var(--color-orange-400)}.input{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input::placeholder{color:var(--color-gray-400)}.input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .input{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .input::placeholder{color:var(--color-gray-500)}.input:disabled{cursor:not-allowed;background-color:var(--color-gray-100);color:var(--color-gray-400)}.dark .input:disabled{background-color:var(--color-gray-800);color:var(--color-gray-500)}.input-tag{border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input-tag:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .input-tag{background-color:var(--color-gray-700);color:var(--color-gray-100)}.input-tag{min-height:2.25rem}.textarea{border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.textarea::placeholder{color:var(--color-gray-400)}.textarea:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .textarea{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .textarea::placeholder{color:var(--color-gray-500)}.textarea:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .textarea:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.input-readonly{cursor:default;color:var(--color-gray-500)}.dark .input-readonly{color:var(--color-gray-400)}.input-w-xs{width:calc(var(--spacing) * 20)}.input-w-sm{width:calc(var(--spacing) * 32)}.input-w-md{width:calc(var(--spacing) * 48)}.input-w-lg{width:calc(var(--spacing) * 64)}.input-w-xl{width:calc(var(--spacing) * 80)}.input-w-auto{width:auto}.input-search-container{border-radius:var(--radius-lg);background-color:var(--color-gray-100);align-items:center;display:flex;overflow:hidden}.dark .input-search-container{background-color:var(--color-gray-700)}.input-search-container .input-search-icon{padding-left:calc(var(--spacing) * 3);color:var(--color-gray-400)}.dark :is(.input-search-container .input-search-icon){color:var(--color-gray-500)}.input-search-container input{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);background-color:#0000;flex:1}.input-search-container input::placeholder{color:var(--color-gray-400)}.input-search-container input:focus{--tw-outline-style:none;outline-style:none}.dark :is(.input-search-container input){color:var(--color-gray-100)}.dark :is(.input-search-container input)::placeholder{color:var(--color-gray-500)}.input-search-container .input-search-dropdown{cursor:pointer;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}@media(hover:hover){.input-search-container .input-search-dropdown:hover{background-color:var(--color-gray-50)}}.dark :is(.input-search-container .input-search-dropdown){border-color:var(--color-gray-700);color:var(--color-gray-400)}@media(hover:hover){.dark :is(.input-search-container .input-search-dropdown):hover{background-color:var(--color-gray-700)}}.select{height:calc(var(--spacing) * 9);cursor:pointer;appearance:none;border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.select:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select{background-color:var(--color-gray-700);color:var(--color-gray-100)}.select:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .select:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem}.dark .select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%239ca3af' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")}.select-composite{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.select-composite:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select-composite{background-color:var(--color-gray-700);color:var(--color-gray-100)}.select-composite:disabled{cursor:not-allowed;background-color:var(--color-gray-200);color:var(--color-gray-400)}.dark .select-composite:disabled{background-color:var(--color-gray-700);color:var(--color-gray-500)}.select-composite-sm{height:calc(var(--spacing) * 8);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.select-w-sm{width:calc(var(--spacing) * 32)}.select-w-md{width:calc(var(--spacing) * 48)}.select-w-lg{width:calc(var(--spacing) * 64)}.input-split{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.input-split input{border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.input-split input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark :is(.input-split input){background-color:var(--color-gray-700);color:var(--color-gray-100)}.input-split .input-split-separator{color:var(--color-gray-400)}.dark :is(.input-split .input-split-separator){color:var(--color-gray-500)}.tag-input-container{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.tag{align-items:center;gap:calc(var(--spacing) * 1);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);border-radius:3.40282e38px;display:inline-flex}.dark .tag{background-color:var(--color-gray-700);color:var(--color-gray-300)}.tag .tag-remove{cursor:pointer;color:var(--color-gray-400)}@media(hover:hover){.tag .tag-remove:hover{color:var(--color-gray-600)}}.dark :is(.tag .tag-remove){color:var(--color-gray-500)}@media(hover:hover){.dark :is(.tag .tag-remove):hover{color:var(--color-gray-300)}}:where(.form-group>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}.textarea-sm{min-height:5rem}.textarea-md{min-height:8rem}.textarea-lg{min-height:12rem}.input-error{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-500)}.input-error:focus{--tw-ring-color:var(--color-red-500)}.dark .input-error{border-color:var(--color-red-500)}.input-success{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-500)}.input-success:focus{--tw-ring-color:var(--color-green-500)}.dark .input-success{border-color:var(--color-green-500)}.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-500)}:is(.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea):focus{--tw-ring-color:var(--color-red-500)}.dark :is(.form-field-error .input,.form-field-error input[type],.form-field-error select,.form-field-error textarea){border-color:var(--color-red-500)}.form-error{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-500);display:block}.dark .form-error{color:var(--color-red-400)}.form-success{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-600)}.dark .form-success{color:var(--color-green-400)}.checkbox{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-gray-900);border-radius:.25rem}.checkbox:focus{--tw-ring-color:var(--color-gray-500)}.dark .checkbox{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.checkbox:checked{border-color:var(--color-gray-900);background-color:var(--color-gray-900)}.dark .checkbox:checked{border-color:var(--color-white);background-color:var(--color-white)}.radio{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-blue-600);flex-shrink:0}.radio:focus{--tw-ring-color:var(--color-blue-500)}.dark .radio{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.dark .radio:focus{--tw-ring-color:var(--color-blue-400)}.radio:checked{border-color:var(--color-blue-600)}.dark .radio:checked{border-color:var(--color-blue-400)}.toggle{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 11);cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;align-items:center;display:inline-flex;position:relative}.toggle-track{inset:calc(var(--spacing) * 0);background-color:var(--color-gray-200);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute}.dark .toggle-track{background-color:var(--color-gray-700)}.toggle.active .toggle-track{background-color:var(--color-blue-600)}.toggle-handle{left:calc(var(--spacing) * .5);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute}.toggle.active .toggle-handle{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.badge{align-items:center;gap:calc(var(--spacing) * 1);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);display:inline-flex}.badge-green{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-200);background-color:var(--color-green-100);color:var(--color-green-700)}.dark .badge-green{border-color:var(--color-green-800);background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-green{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .badge-green{color:var(--color-green-400)}.badge-blue{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-200);background-color:var(--color-blue-100);color:var(--color-blue-700)}.dark .badge-blue{border-color:var(--color-blue-800);background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-blue{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .badge-blue{color:var(--color-blue-400)}.badge-cyan{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-cyan-200);background-color:var(--color-cyan-100);color:var(--color-cyan-700)}.dark .badge-cyan{border-color:var(--color-cyan-800);background-color:#104e644d}@supports (color:color-mix(in lab,red,red)){.dark .badge-cyan{background-color:color-mix(in oklab,var(--color-cyan-900) 30%,transparent)}}.dark .badge-cyan{color:var(--color-cyan-400)}.badge-indigo{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-indigo-200);background-color:var(--color-indigo-100);color:var(--color-indigo-700)}.dark .badge-indigo{border-color:var(--color-indigo-800);background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .badge-indigo{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .badge-indigo{color:var(--color-indigo-400)}.badge-pink{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-pink-200);background-color:var(--color-pink-100);color:var(--color-pink-700)}.dark .badge-pink{border-color:var(--color-pink-800);background-color:#8610434d}@supports (color:color-mix(in lab,red,red)){.dark .badge-pink{background-color:color-mix(in oklab,var(--color-pink-900) 30%,transparent)}}.dark .badge-pink{color:var(--color-pink-400)}.badge-red{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-100);color:var(--color-red-700)}.dark .badge-red{border-color:var(--color-red-800);background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-red{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .badge-red{color:var(--color-red-400)}.badge-gray{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);color:var(--color-gray-600)}.dark .badge-gray{border-color:var(--color-gray-700);background-color:var(--color-gray-800);color:var(--color-gray-400)}.badge-yellow{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-100);color:var(--color-yellow-700)}.dark .badge-yellow{border-color:var(--color-yellow-800);background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .badge-yellow{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .badge-yellow{color:var(--color-yellow-400)}.badge-status{align-items:center;gap:calc(var(--spacing) * 1.5);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;display:inline-flex}.badge-status-success{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .badge-status-success{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-success{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .badge-status-success{color:var(--color-green-400)}.badge-status-processing{background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .badge-status-processing{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-processing{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .badge-status-processing{color:var(--color-blue-400)}.badge-status-error{background-color:var(--color-red-50);color:var(--color-red-600)}.dark .badge-status-error{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .badge-status-error{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .badge-status-error{color:var(--color-red-400)}.badge-status-expired{background-color:var(--color-gray-50);color:var(--color-gray-500)}.dark .badge-status-expired{background-color:var(--color-gray-800);color:var(--color-gray-400)}.search-bar{gap:calc(var(--spacing) * 3);flex-direction:column;display:flex}@media(min-width:48rem){.search-bar{flex-flow:wrap;align-items:center}}.search-bar-select{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.search-bar-select::placeholder{color:var(--color-gray-400)}.search-bar-select:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .search-bar-select{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .search-bar-select::placeholder{color:var(--color-gray-500)}@media(min-width:48rem){.search-bar-select{width:calc(var(--spacing) * 32);flex-shrink:0}}.search-bar-select-custom{width:100%}@media(min-width:48rem){.search-bar-select-custom{width:calc(var(--spacing) * 32);flex-shrink:0}}.search-bar-input{height:calc(var(--spacing) * 9);border-radius:var(--radius-lg);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.search-bar-input::placeholder{color:var(--color-gray-400)}.search-bar-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .search-bar-input{background-color:var(--color-gray-700);color:var(--color-gray-100)}.dark .search-bar-input::placeholder{color:var(--color-gray-500)}@media(min-width:48rem){.search-bar-input{min-width:calc(var(--spacing) * 64);flex:1}}.search-bar-reset{cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);background-color:#0000;transition-duration:.2s;display:inline-flex}@media(hover:hover){.search-bar-reset:hover{background-color:var(--color-gray-50)}}.dark .search-bar-reset{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .search-bar-reset:hover{background-color:var(--color-gray-700)}}.search-bar-reset{gap:calc(var(--spacing) * 1.5);flex-shrink:0;width:100%}@media(min-width:48rem){.search-bar-reset{width:auto}}.filter-grid{gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}.filter-row{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}@media(min-width:48rem){.filter-row{flex-direction:row;align-items:center}}.filter-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);display:block}.dark .filter-label{color:var(--color-gray-400)}@media(min-width:48rem){.filter-label{margin-bottom:calc(var(--spacing) * 0);width:calc(var(--spacing) * 14);flex-shrink:0;align-items:center;display:flex}}.filter-buttons{align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.dropdown{display:inline-block;position:relative}.dropdown-trigger{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.dropdown-trigger:hover{background-color:var(--color-gray-50)}}.dropdown-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dropdown-trigger{color:var(--color-gray-900)}.dark .dropdown-trigger{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-white)}@media(hover:hover){.dark .dropdown-trigger:hover{background-color:var(--color-gray-700)}}.dark .dropdown-trigger:focus{--tw-ring-color:var(--color-blue-400)}.dropdown-trigger-text{cursor:pointer;--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900);align-items:center;display:inline-flex}@media(hover:hover){.dropdown-trigger-text:hover{color:var(--color-blue-600)}}.dark .dropdown-trigger-text{color:var(--color-white)}@media(hover:hover){.dark .dropdown-trigger-text:hover{color:var(--color-blue-400)}}.dropdown-menu{z-index:50;width:calc(var(--spacing) * 56);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .dropdown-menu{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.dropdown-menu-bottom-left{top:100%;left:calc(var(--spacing) * 0);margin-top:calc(var(--spacing) * 2)}.dropdown-menu-bottom-right{top:100%;right:calc(var(--spacing) * 0);margin-top:calc(var(--spacing) * 2)}.dropdown-menu-top-left{bottom:100%;left:calc(var(--spacing) * 0);margin-bottom:calc(var(--spacing) * 2)}.dropdown-menu-top-right{right:calc(var(--spacing) * 0);margin-bottom:calc(var(--spacing) * 2);bottom:100%}.dropdown-item{cursor:pointer;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}@media(hover:hover){.dropdown-item:hover{background-color:var(--color-gray-100)}}.dark .dropdown-item{color:var(--color-gray-300)}@media(hover:hover){.dark .dropdown-item:hover{background-color:var(--color-gray-700)}}.dropdown-item-icon{color:var(--color-gray-400)}.dark .dropdown-item-icon{color:var(--color-gray-500)}.dropdown-item-disabled{cursor:not-allowed;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-400)}.dark .dropdown-item-disabled{color:var(--color-gray-500)}.dropdown-item-focused{background-color:var(--color-gray-100)}.dark .dropdown-item-focused{background-color:var(--color-gray-700)}.dropdown-divider{margin-block:calc(var(--spacing) * 1);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100)}.dark .dropdown-divider{border-color:var(--color-gray-700)}.pagination{justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);flex-wrap:wrap;display:flex}.pagination-btn{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);color:var(--color-gray-700);border-radius:.25rem}@media(hover:hover){.pagination-btn:hover{background-color:var(--color-gray-100)}}.pagination-btn:disabled{cursor:not-allowed;opacity:.5}.dark .pagination-btn{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-gray-300)}@media(hover:hover){.dark .pagination-btn:hover{background-color:var(--color-gray-700)}}.pagination-btn-active{border-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));border-width:1px;border-color:var(--color-blue-500);background-color:var(--color-blue-500);color:var(--color-white);border-radius:.25rem}.dark .pagination-btn-active{border-color:var(--color-blue-600);background-color:var(--color-blue-600)}.pagination-ellipsis{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .pagination-ellipsis{color:var(--color-gray-400)}.dialog-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.dialog-title{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .dialog-title{color:var(--color-white)}.dialog-close{color:var(--color-gray-400)}@media(hover:hover){.dialog-close:hover{color:var(--color-gray-600)}}.dialog-close:focus{--tw-outline-style:none;outline-style:none}.dark .dialog-close{color:var(--color-gray-500)}@media(hover:hover){.dark .dialog-close:hover{color:var(--color-gray-300)}}.dialog-body{margin-bottom:calc(var(--spacing) * 6);color:var(--color-gray-700)}.dark .dialog-body{color:var(--color-gray-300)}.dialog-footer{justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.dialog-btn-primary{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-primary:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-primary:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-primary{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.dialog-btn-primary:hover{background-color:var(--color-blue-700)}}.dialog-btn-primary:focus{--tw-ring-color:var(--color-blue-500)}.dialog-btn-secondary{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-secondary:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-secondary:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-secondary{background-color:var(--color-gray-200);color:var(--color-gray-800)}@media(hover:hover){.dialog-btn-secondary:hover{background-color:var(--color-gray-300)}}.dialog-btn-secondary:focus{--tw-ring-color:var(--color-gray-500)}.dark .dialog-btn-secondary{background-color:var(--color-gray-700);color:var(--color-gray-200)}@media(hover:hover){.dark .dialog-btn-secondary:hover{background-color:var(--color-gray-600)}}.dialog-btn-danger{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-danger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-danger:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.dialog-btn-danger:hover{background-color:var(--color-red-700)}}.dialog-btn-danger:focus{--tw-ring-color:var(--color-red-500)}.dialog-btn-ghost{border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.dialog-btn-ghost:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dialog-btn-ghost:disabled{cursor:not-allowed;opacity:.5}.dialog-btn-ghost{color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.dialog-btn-ghost:hover{background-color:var(--color-gray-100)}}.dialog-btn-ghost:focus{--tw-ring-color:var(--color-gray-500)}.dark .dialog-btn-ghost{color:var(--color-gray-300)}@media(hover:hover){.dark .dialog-btn-ghost:hover{background-color:var(--color-gray-700)}}.alert-error{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-900);display:flex}.dark .alert-error{border-color:var(--color-red-800);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-error{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .alert-error{color:var(--color-red-100)}.alert-icon-info{margin-top:calc(var(--spacing) * .5);color:var(--color-blue-600);flex-shrink:0}.dark .alert-icon-info{color:var(--color-blue-400)}.alert-icon-success{margin-top:calc(var(--spacing) * .5);color:var(--color-green-600);flex-shrink:0}.dark .alert-icon-success{color:var(--color-green-400)}.alert-icon-warning{margin-top:calc(var(--spacing) * .5);color:var(--color-yellow-600);flex-shrink:0}.dark .alert-icon-warning{color:var(--color-yellow-400)}.alert-icon-error{margin-top:calc(var(--spacing) * .5);color:var(--color-red-600);flex-shrink:0}.dark .alert-icon-error{color:var(--color-red-400)}.alert-text-info{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-800);flex:1}.dark .alert-text-info{color:var(--color-blue-200)}.alert-text-success{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-800);flex:1}.dark .alert-text-success{color:var(--color-green-200)}.alert-text-warning{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-800);flex:1}.dark .alert-text-warning{color:var(--color-yellow-200)}.alert-text-error{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-800);flex:1}.dark .alert-text-error{color:var(--color-red-200)}.alert-dismiss{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing) * 0);background-color:#0000;border-width:0;flex-shrink:0}@media(hover:hover){.alert-dismiss:hover{opacity:.7}}.table-container{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);overflow:hidden}.dark .table-container{border-color:var(--color-gray-700)}.table{width:100%}.table thead{background-color:var(--color-gray-50)}.dark :is(.table thead){background-color:var(--color-gray-800)}.table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-600)}.dark :is(.table th){border-color:var(--color-gray-700);color:var(--color-gray-400)}.table th.sortable{cursor:pointer}@media(hover:hover){.table th.sortable:hover{background-color:var(--color-gray-100)}.dark :is(.table th.sortable):hover{background-color:var(--color-gray-700)}}.table tbody tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);background-color:var(--color-white)}.dark :is(.table tbody tr){border-color:var(--color-gray-800);background-color:var(--color-gray-900)}.table tbody tr:hover{background-color:var(--color-gray-50)}.dark :is(.table tbody tr:hover){background-color:var(--color-gray-800)}.table tbody tr.selected{background-color:var(--color-blue-50)}.dark :is(.table tbody tr.selected){background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark :is(.table tbody tr.selected){background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.table td{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark :is(.table td){color:var(--color-gray-100)}.table-actions{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);justify-content:space-between;align-items:center;display:flex}.dark .table-actions{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.log-table{width:100%}.log-table th{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500)}.dark :is(.log-table th){border-color:var(--color-gray-700);color:var(--color-gray-400)}.log-table td{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark :is(.log-table td){border-color:var(--color-gray-800);color:var(--color-gray-100)}.log-table tbody tr:last-child td{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.log-table-actor{border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);align-items:center;display:inline-flex}.dark .log-table-actor{border-color:var(--color-gray-600);background-color:var(--color-gray-700);color:var(--color-gray-300)}.log-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .log-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.log-card-header{margin-bottom:calc(var(--spacing) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);display:flex}.log-card-body{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .log-card-body{color:var(--color-gray-100)}.modal-overlay{inset:calc(var(--spacing) * 0);z-index:50;background-color:#00000080;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab,red,red)){.modal-overlay{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.modal-wrapper{inset:calc(var(--spacing) * 0);justify-content:center;align-items:center;display:flex;position:fixed}.modal-backdrop{inset:calc(var(--spacing) * 0);background-color:#00000080;position:absolute}@supports (color:color-mix(in lab,red,red)){.modal-backdrop{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.modal-container{border-radius:var(--radius-lg);background-color:var(--color-white);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);max-width:calc(100vw - 2rem);max-height:90vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:relative;overflow:hidden}.dark .modal-container{background-color:var(--color-gray-800)}.modal-header-row{padding:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.modal-title{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .modal-title{color:var(--color-white)}.modal-close-btn{color:var(--color-gray-400);margin-left:auto}@media(hover:hover){.modal-close-btn:hover{color:var(--color-gray-600)}}.modal-close-btn:focus{--tw-outline-style:none;outline-style:none}.dark .modal-close-btn{color:var(--color-gray-500)}@media(hover:hover){.dark .modal-close-btn:hover{color:var(--color-gray-300)}}.modal-body-content{max-height:calc(90vh - 140px);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4);overflow-y:auto}.modal{margin-inline:calc(var(--spacing) * 4);width:100%;max-width:var(--container-md);border-radius:var(--radius-xl);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .modal{background-color:var(--color-gray-800)}.modal-lg{max-width:var(--container-lg)}.modal-xl{max-width:var(--container-xl)}.modal-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:flex-start;display:flex}.modal-close{cursor:pointer;border-radius:var(--radius-lg);padding:calc(var(--spacing) * 1);color:var(--color-gray-400)}@media(hover:hover){.modal-close:hover{background-color:var(--color-gray-100);color:var(--color-gray-600)}}.dark .modal-close{color:var(--color-gray-500)}@media(hover:hover){.dark .modal-close:hover{background-color:var(--color-gray-700);color:var(--color-gray-300)}}.modal-body{padding-block:calc(var(--spacing) * 4)}.modal-footer{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.danger-confirm-icon{margin-inline:auto;margin-bottom:calc(var(--spacing) * 4);height:calc(var(--spacing) * 12);width:calc(var(--spacing) * 12);background-color:var(--color-red-100);border-radius:3.40282e38px;justify-content:center;align-items:center;display:flex}.dark .danger-confirm-icon{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .danger-confirm-icon{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.danger-confirm-icon svg,.danger-confirm-icon .icon{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--color-red-600)}.dark :is(.danger-confirm-icon svg,.danger-confirm-icon .icon){color:var(--color-red-400)}.danger-confirm-title{margin-bottom:calc(var(--spacing) * 2);text-align:center;font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .danger-confirm-title{color:var(--color-white)}.danger-confirm-message{margin-bottom:calc(var(--spacing) * 4);text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .danger-confirm-message{color:var(--color-gray-400)}.danger-confirm-warning{margin-bottom:calc(var(--spacing) * 4);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-50);padding:calc(var(--spacing) * 4)}.dark .danger-confirm-warning{border-color:var(--color-yellow-700);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .danger-confirm-warning{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.danger-confirm-warning-title{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-yellow-800)}.dark .danger-confirm-warning-title{color:var(--color-yellow-300)}.danger-confirm-warning-list{list-style-type:disc;list-style-position:inside}:where(.danger-confirm-warning-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.danger-confirm-warning-list{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-700)}.dark .danger-confirm-warning-list{color:var(--color-yellow-400)}.danger-confirm-alert{text-align:center;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-600)}.dark .danger-confirm-alert{color:var(--color-red-400)}.danger-confirm-footer{margin-top:calc(var(--spacing) * 6);justify-content:center;gap:calc(var(--spacing) * 3);display:flex}.card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);width:100%;padding:calc(var(--spacing) * 6)}.dark .card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card{margin-bottom:calc(var(--spacing) * 6);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .admin-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card{flex-direction:column;display:flex}.admin-card-clickable{margin-bottom:calc(var(--spacing) * 2);cursor:pointer;border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5)}@media(hover:hover){.admin-card-clickable:hover{border-color:var(--color-gray-300)}}.dark .admin-card-clickable{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}@media(hover:hover){.dark .admin-card-clickable:hover{border-color:var(--color-gray-600)}}.admin-card-selected{margin-bottom:calc(var(--spacing) * 2);cursor:pointer;border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-500);background-color:var(--color-blue-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2.5)}.dark .admin-card-selected{border-color:var(--color-blue-500);background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .admin-card-selected{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.card-clickable{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));overflow:hidden}@media(hover:hover){.card-clickable:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .card-clickable{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.card-image{height:calc(var(--spacing) * 48);object-fit:cover;width:100%}.card-content{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));white-space:pre-line;color:var(--color-gray-600)}.dark .card-content{color:var(--color-gray-300)}.card-section-title{margin-inline:calc(var(--spacing) * -6);margin-bottom:calc(var(--spacing) * 6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 6);padding-bottom:calc(var(--spacing) * 4);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .card-section-title{border-color:var(--color-gray-700);color:var(--color-white)}.card-account{border-radius:var(--radius-lg);background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4);position:relative}.dark .card-account{background-color:var(--color-gray-900)}.card-account-default{top:calc(var(--spacing) * 3);left:calc(var(--spacing) * 3);position:absolute}.card-account-delete{top:calc(var(--spacing) * 3);right:calc(var(--spacing) * 3);position:absolute}.admin-card-accordion{margin-bottom:calc(var(--spacing) * 6);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .admin-card-accordion{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.admin-card-accordion{flex-direction:column;display:flex}.admin-card-accordion>.card-header{cursor:pointer}.admin-card-accordion-icon{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;margin-left:auto;transition-duration:.2s}.dark .admin-card-accordion-icon{color:var(--color-gray-500)}.admin-card-accordion>.row-stack{margin-top:calc(var(--spacing) * 6)}.admin-card-accordion-icon.open{rotate:180deg}.tabs{border-radius:var(--radius-lg);background-color:var(--color-gray-100);padding:calc(var(--spacing) * 1);display:inline-flex}.dark .tabs{background-color:var(--color-gray-800)}.tab{cursor:pointer;border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.tab:hover{color:var(--color-gray-700)}}.dark .tab{color:var(--color-gray-400)}@media(hover:hover){.dark .tab:hover{color:var(--color-gray-300)}}.tab.active{background-color:var(--color-white);color:var(--color-gray-900);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .tab.active{background-color:var(--color-gray-700);color:var(--color-white)}.toast{right:calc(var(--spacing) * 4);bottom:calc(var(--spacing) * 4);z-index:50;position:fixed}.toast-item{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);background-color:var(--color-gray-900);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);display:flex}.dark .toast-item{background-color:var(--color-white);color:var(--color-gray-900)}.toast-icon{flex-shrink:0}.drag-item{align-items:center;gap:calc(var(--spacing) * 4);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4);display:flex}@media(hover:hover){.drag-item:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .drag-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.drag-handle{cursor:grab;color:var(--color-gray-400)}.dark .drag-handle{color:var(--color-gray-500)}.drag-handle:active{cursor:grabbing}.table-mobile-cards{display:none}.excel-card{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .excel-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.excel-card-header{margin-bottom:calc(var(--spacing) * 4);justify-content:space-between;align-items:flex-start;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-bottom:calc(var(--spacing) * 3);display:flex}.dark .excel-card-header{border-color:var(--color-gray-700)}.excel-card-title{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .excel-card-title{color:var(--color-white)}:where(.excel-card-body>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}.excel-card-row{padding-block:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));justify-content:space-between;align-items:center;display:flex}.excel-card-label{color:var(--color-gray-500);flex-shrink:0}.dark .excel-card-label{color:var(--color-gray-400)}.excel-card-value{text-align:right;color:var(--color-gray-900)}.dark .excel-card-value{color:var(--color-white)}.excel-card-footer{margin-top:calc(var(--spacing) * 4);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-100);padding-top:calc(var(--spacing) * 3);justify-content:space-between;align-items:center;display:flex}.dark .excel-card-footer{border-color:var(--color-gray-700)}.excel-card-creator{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700)}.dark .excel-card-creator{color:var(--color-gray-300)}.excel-card-creator-label{margin-right:calc(var(--spacing) * 2);color:var(--color-gray-500)}.dark .excel-card-creator-label{color:var(--color-gray-400)}.select-custom{display:inline-block;position:relative}.select-trigger{height:calc(var(--spacing) * 9);border-radius:var(--radius-xl);background-color:var(--color-gray-100);width:100%;padding-inline:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:0;display:flex}.select-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .select-trigger{background-color:var(--color-gray-700);color:var(--color-gray-200)}.select-trigger:disabled{cursor:not-allowed;opacity:.5}.select-trigger span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.select-trigger-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;flex-shrink:0;transition-duration:.2s}.dark .select-trigger-icon{color:var(--color-gray-500)}.select-trigger-icon.open{rotate:180deg}.select-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:100%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute;overflow:hidden}.dark .select-dropdown{border-color:var(--color-gray-600);background-color:var(--color-gray-800)}.select-dropdown.hidden{display:none}.select-dropdown-inner{max-height:calc(var(--spacing) * 60);padding-block:calc(var(--spacing) * 2);overflow:auto}.select-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));align-items:center;display:flex}@media(hover:hover){.select-option:hover{background-color:var(--color-gray-100)}}.dark .select-option{color:var(--color-gray-200)}@media(hover:hover){.dark .select-option:hover{background-color:var(--color-gray-700)}}.select-option.selected{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-blue-600)}.dark .select-option.selected{color:var(--color-blue-400)}.select-option:disabled{cursor:not-allowed;opacity:.5}.select-option-check{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-blue-600);flex-shrink:0;margin-left:auto}.dark .select-option-check{color:var(--color-blue-400)}.multi-select{display:inline-block;position:relative}.multi-select-trigger{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);color:var(--color-gray-700);cursor:pointer;background-color:#0000;display:inline-flex}@media(hover:hover){.multi-select-trigger:hover{background-color:var(--color-gray-50)}}.dark .multi-select-trigger{border-color:var(--color-gray-600);color:var(--color-gray-300)}@media(hover:hover){.dark .multi-select-trigger:hover{background-color:var(--color-gray-700)}}.multi-select-count{color:var(--color-gray-500)}.dark .multi-select-count{color:var(--color-gray-400)}.multi-select-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);min-width:calc(var(--spacing) * 64);border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .multi-select-dropdown{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.multi-select-dropdown.hidden{display:none}.multi-select-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .multi-select-header{border-color:var(--color-gray-700);color:var(--color-white)}.multi-select-hint{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--color-gray-500)}.dark .multi-select-hint{color:var(--color-gray-400)}.multi-select-items{max-height:calc(var(--spacing) * 80);padding-block:calc(var(--spacing) * 2);overflow:auto}.multi-select-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);display:flex}@media(hover:hover){.multi-select-item:hover{background-color:var(--color-gray-50)}}.dark .multi-select-item{color:var(--color-gray-300)}@media(hover:hover){.dark .multi-select-item:hover{background-color:var(--color-gray-700)}}.multi-select-item.disabled{cursor:not-allowed;opacity:.5}.multi-select-item.disabled:hover,.dark .multi-select-item.disabled:hover{background-color:#0000}.select-ghost{display:inline-block;position:relative}.select-ghost-trigger{align-items:center;gap:calc(var(--spacing) * 1);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);cursor:pointer;border-radius:var(--radius-lg);color:var(--color-gray-700);background-color:#0000;display:inline-flex}@media(hover:hover){.select-ghost-trigger:hover{background-color:var(--color-gray-100)}}.dark .select-ghost-trigger{color:var(--color-gray-300)}@media(hover:hover){.dark .select-ghost-trigger:hover{background-color:var(--color-gray-700)}}.select-ghost-dropdown{z-index:50;margin-top:calc(var(--spacing) * 1);min-width:calc(var(--spacing) * 32);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding-block:calc(var(--spacing) * 1);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute}.dark .select-ghost-dropdown{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.select-ghost-dropdown.hidden{display:none}.select-ghost-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);justify-content:space-between;align-items:center;display:flex}@media(hover:hover){.select-ghost-option:hover{background-color:var(--color-gray-50)}}.dark .select-ghost-option{color:var(--color-gray-300)}@media(hover:hover){.dark .select-ghost-option:hover{background-color:var(--color-gray-700)}}.select-ghost-option.selected{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-blue-600)}.dark .select-ghost-option.selected{color:var(--color-blue-400)}.select-ghost-check{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-blue-600)}.dark .select-ghost-check{color:var(--color-blue-400)}.searchable-dropdown{display:inline-block;position:relative}.searchable-dropdown-trigger{border-radius:var(--radius-xl);background-color:var(--color-gray-100);min-width:200px;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 2);border-style:var(--tw-border-style);border-width:0;display:flex}.searchable-dropdown-trigger:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;outline-style:none}.dark .searchable-dropdown-trigger{background-color:var(--color-gray-700);color:var(--color-gray-200)}.searchable-dropdown-trigger:disabled{cursor:not-allowed;opacity:.5}.searchable-dropdown-trigger-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;flex-shrink:0;transition-duration:.2s}.dark .searchable-dropdown-trigger-icon{color:var(--color-gray-500)}.searchable-dropdown-trigger-icon.open{rotate:180deg}.searchable-dropdown-dropdown{z-index:50;margin-top:calc(var(--spacing) * 2);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);width:100%;min-width:280px;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);position:absolute;overflow:hidden}.dark .searchable-dropdown-dropdown{border-color:var(--color-gray-600);background-color:var(--color-gray-800)}.searchable-dropdown-dropdown.hidden{display:none}.searchable-dropdown-search{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding:calc(var(--spacing) * 3)}.dark .searchable-dropdown-search{border-color:var(--color-gray-700)}.searchable-dropdown-search-input{border-radius:var(--radius-lg);background-color:var(--color-gray-50);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.searchable-dropdown-search-input::placeholder{color:var(--color-gray-400)}.searchable-dropdown-search-input:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;border-color:#0000;outline-style:none}.dark .searchable-dropdown-search-input{border-color:var(--color-gray-600);background-color:var(--color-gray-700);color:var(--color-white)}.dark .searchable-dropdown-search-input::placeholder{color:var(--color-gray-500)}.searchable-dropdown-items{max-height:calc(var(--spacing) * 80);padding-block:calc(var(--spacing) * 2);overflow:auto}.searchable-dropdown-option{cursor:pointer;width:100%;padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);text-align:left;align-items:flex-start;gap:calc(var(--spacing) * 3);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media(hover:hover){.searchable-dropdown-option:hover{background-color:var(--color-gray-100)}.dark .searchable-dropdown-option:hover{background-color:var(--color-gray-700)}}.searchable-dropdown-option:disabled{cursor:not-allowed;opacity:.5}.searchable-dropdown-check-area{margin-top:calc(var(--spacing) * .5);height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);flex-shrink:0}.searchable-dropdown-check{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5);color:var(--color-blue-600)}.dark .searchable-dropdown-check{color:var(--color-blue-400)}.searchable-dropdown-option-content{min-width:calc(var(--spacing) * 0);flex:1}.searchable-dropdown-option-label{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .searchable-dropdown-option-label{color:var(--color-blue-400)}.searchable-dropdown-option.selected .searchable-dropdown-option-label{color:var(--color-blue-600)}.dark :is(.searchable-dropdown-option.selected .searchable-dropdown-option-label){color:var(--color-blue-400)}.searchable-dropdown-option-description{margin-top:calc(var(--spacing) * .5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .searchable-dropdown-option-description{color:var(--color-gray-400)}.searchable-dropdown-message{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 6);text-align:center;color:var(--color-gray-500)}.dark .searchable-dropdown-message{color:var(--color-gray-400)}.stat-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.stat-card:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.dark .stat-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.stats-icon{border-radius:var(--radius-lg);padding:calc(var(--spacing) * 3)}.stats-icon-blue{background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .stats-icon-blue{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-blue{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .stats-icon-blue{color:var(--color-blue-400)}.stats-icon-green{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .stats-icon-green{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-green{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .stats-icon-green{color:var(--color-green-400)}.stats-icon-purple{background-color:var(--color-purple-50);color:var(--color-purple-600)}.dark .stats-icon-purple{background-color:#59168b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-purple{background-color:color-mix(in oklab,var(--color-purple-900) 20%,transparent)}}.dark .stats-icon-purple{color:var(--color-purple-400)}.stats-icon-orange{background-color:var(--color-orange-50);color:var(--color-orange-600)}.dark .stats-icon-orange{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .stats-icon-orange{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .stats-icon-orange{color:var(--color-orange-400)}.stats-change{align-items:center;gap:calc(var(--spacing) * 1);border-radius:var(--radius-md);padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * 1);display:flex}.stats-trend-up{background-color:var(--color-green-50);color:var(--color-green-600)}.dark .stats-trend-up{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .stats-trend-up{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .stats-trend-up{color:var(--color-green-400)}.stats-trend-down{background-color:var(--color-red-50);color:var(--color-red-600)}.dark .stats-trend-down{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .stats-trend-down{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .stats-trend-down{color:var(--color-red-400)}.stats-trend-neutral{background-color:var(--color-gray-50);color:var(--color-gray-600)}.dark .stats-trend-neutral{background-color:var(--color-gray-700);color:var(--color-gray-400)}.stats-value-lg{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-gray-900)}.dark .stats-value-lg{color:var(--color-white)}.stats-label{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .stats-label{color:var(--color-gray-400)}.stats-sublabel{align-items:center;gap:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500);display:flex}.dark .stats-sublabel{color:var(--color-gray-400)}:where(.tree>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}.tree-item{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);display:flex}@media(hover:hover){.tree-item:hover{background-color:var(--color-gray-50)}.dark .tree-item:hover{background-color:var(--color-gray-800)}}.tree-item.selected{background-color:var(--color-blue-50)}.dark .tree-item.selected{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tree-item.selected{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.tree-item-content{min-width:calc(var(--spacing) * 0);align-items:center;gap:calc(var(--spacing) * 2);flex:1;display:flex}.tree-item-icon{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);color:var(--color-gray-400);flex-shrink:0}.dark .tree-item-icon{color:var(--color-gray-500)}.tree-item-label{text-overflow:ellipsis;white-space:nowrap;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-700);overflow:hidden}.dark .tree-item-label{color:var(--color-gray-300)}.tree-expand-btn{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6);color:var(--color-gray-400);border-radius:.25rem;justify-content:center;align-items:center;display:flex}@media(hover:hover){.tree-expand-btn:hover{background-color:var(--color-gray-100)}}.dark .tree-expand-btn{color:var(--color-gray-500)}@media(hover:hover){.dark .tree-expand-btn:hover{background-color:var(--color-gray-700)}}.tree-expand-btn.hidden{visibility:hidden}.tree-children{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-children{border-color:var(--color-gray-700)}.tree-level-1{margin-left:calc(var(--spacing) * 0);border-left-style:var(--tw-border-style);padding-left:calc(var(--spacing) * 0);border-left-width:0}.tree-level-2{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-2{border-color:var(--color-gray-700)}.tree-level-3{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-3{border-color:var(--color-gray-700)}.tree-level-4{margin-left:calc(var(--spacing) * 6);border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-gray-200);padding-left:calc(var(--spacing) * 4)}.dark .tree-level-4{border-color:var(--color-gray-700)}.category-item{cursor:pointer;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900);justify-content:space-between;align-items:center;display:flex}.dark .category-item{color:var(--color-white)}@media(hover:hover){.category-item:hover{background-color:var(--color-gray-50)}.dark .category-item:hover{background-color:var(--color-gray-700)}}.category-item[data-selected=true]{background-color:var(--color-blue-600);color:var(--color-white)}.category-item[data-selected=true]:hover{background-color:var(--color-blue-700)}.category-item-mobile{cursor:pointer;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .category-item-mobile{color:var(--color-white)}.category-item-mobile[data-selected=true]{background-color:var(--color-blue-50)}.dark .category-item-mobile[data-selected=true]{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .category-item-mobile[data-selected=true]{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.stats-card{height:calc(var(--spacing) * 36);border-radius:var(--radius-2xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-slate-100);background-color:var(--color-white);padding:calc(var(--spacing) * 5);flex-direction:column;justify-content:space-between;display:flex}.dark .stats-card{border-color:var(--color-slate-700);background-color:var(--color-slate-800)}.dashboard-panel{border-radius:var(--radius-3xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-slate-100);background-color:var(--color-white);padding:calc(var(--spacing) * 6);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dashboard-panel{border-color:var(--color-slate-700);background-color:var(--color-slate-800)}.stats-value{margin-bottom:calc(var(--spacing) * 2);--tw-leading:1;--tw-font-weight:var(--font-weight-bold);font-size:2rem;line-height:1;font-weight:var(--font-weight-bold);color:var(--color-slate-900)}.dark .stats-value{color:var(--color-white)}.stats-trend{align-items:center;gap:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-emerald-500);display:flex}.status-active{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-green-100);color:var(--color-green-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-active{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .status-active{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .status-active{color:var(--color-green-400)}.status-inactive{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-gray-100);color:var(--color-gray-600);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-inactive{background-color:var(--color-gray-700);color:var(--color-gray-400)}.status-pending{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-yellow-100);color:var(--color-yellow-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-pending{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .status-pending{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .status-pending{color:var(--color-yellow-400)}.status-error{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);background-color:var(--color-red-100);color:var(--color-red-700);border-radius:3.40282e38px;align-items:center;display:inline-flex}.dark .status-error{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .status-error{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .status-error{color:var(--color-red-400)}.list-item{align-items:center;gap:calc(var(--spacing) * 3);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-100);padding-block:calc(var(--spacing) * 3);display:flex}.dark .list-item{border-color:var(--color-gray-700)}.list-item:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.list-item-hover{margin-inline:calc(var(--spacing) * -3);align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);display:flex}@media(hover:hover){.list-item-hover:hover{background-color:var(--color-gray-50)}.dark .list-item-hover:hover{background-color:var(--color-gray-800)}}.empty-state{padding-block:calc(var(--spacing) * 12);text-align:center;flex-direction:column;justify-content:center;align-items:center;display:flex}.empty-state-icon{margin-bottom:calc(var(--spacing) * 4);height:calc(var(--spacing) * 16);width:calc(var(--spacing) * 16);color:var(--color-gray-300)}.dark .empty-state-icon{color:var(--color-gray-600)}.empty-state-title{margin-bottom:calc(var(--spacing) * 2);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .empty-state-title{color:var(--color-white)}.empty-state-description{margin-bottom:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .empty-state-description{color:var(--color-gray-400)}.page-header{margin-bottom:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.page-header-padded{margin-bottom:calc(var(--spacing) * 6);padding-inline:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.page-header-fullbleed{margin-inline:calc(var(--spacing) * -6);margin-bottom:calc(var(--spacing) * 6);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;display:flex}.dark .page-header-fullbleed{background-color:var(--color-gray-900)}.page-actions{align-items:center;gap:calc(var(--spacing) * 3);display:flex}.section-card{margin-top:calc(var(--spacing) * 6);scroll-margin-top:calc(var(--spacing) * 32);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6)}.dark .section-card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.section-header{margin-bottom:calc(var(--spacing) * 6);font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-header{color:var(--color-white)}.info-row{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 2);display:flex}.action-buttons{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.form-label{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900);display:block}.dark .form-label{color:var(--color-white)}.form-label-muted{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-500);display:block}.dark .form-label-muted{color:var(--color-gray-400)}.form-hint{margin-top:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .form-hint{color:var(--color-gray-400)}.text-value{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-900)}.dark .text-value{color:var(--color-white)}.text-link{cursor:pointer;font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-600)}@media(hover:hover){.text-link:hover{color:var(--color-blue-700)}}.dark .text-link{color:var(--color-blue-400)}@media(hover:hover){.dark .text-link:hover{color:var(--color-blue-300)}}.icon-xs{height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3)}.icon-sm{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4)}.icon-md{height:calc(var(--spacing) * 5);width:calc(var(--spacing) * 5)}.icon-lg{height:calc(var(--spacing) * 6);width:calc(var(--spacing) * 6)}.icon-xl{height:calc(var(--spacing) * 8);width:calc(var(--spacing) * 8)}.icon-2xl{height:calc(var(--spacing) * 10);width:calc(var(--spacing) * 10)}.icon-action{cursor:pointer;color:var(--color-gray-400)}@media(hover:hover){.icon-action:hover{color:var(--color-gray-600)}}.dark .icon-action{color:var(--color-gray-500)}@media(hover:hover){.dark .icon-action:hover{color:var(--color-gray-300)}}.page-container{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 6)}.dark .page-container{background-color:var(--color-gray-900)}.content-area{max-width:var(--container-7xl);margin-inline:auto}.content-panel{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 6)}.dark .content-panel{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.flex-row-center{align-items:center;display:flex}.flex-row-between{justify-content:space-between;align-items:center;display:flex}.flex-row-end{justify-content:flex-end;align-items:center;display:flex}.flex-col-start{flex-direction:column;align-items:flex-start;display:flex}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-center{align-items:center;display:flex}.flex-end{justify-content:flex-end;align-items:center;display:flex}.flex-wrap-center{flex-wrap:wrap;align-items:center;display:flex}.inline-flex-center{align-items:center;display:inline-flex}.row-stack{flex-direction:column;display:flex}:where(.row-stack>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--color-gray-200)}:where(.dark .row-stack>:not(:last-child)){border-color:var(--color-gray-700)}.row-stack>*{padding-block:calc(var(--spacing) * 2)}.row-stack>:first-child{padding-top:calc(var(--spacing) * 0)}.row-stack>:last-child{padding-bottom:calc(var(--spacing) * 0)}.stack-flush{gap:calc(var(--spacing) * 0);flex-direction:column;display:flex}.stack-tight{gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.card-stack{margin-top:calc(var(--spacing) * 3);gap:calc(var(--spacing) * 2);flex-direction:column;display:flex}.card-stack-item{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);padding:calc(var(--spacing) * 4)}.dark .card-stack-item{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.card-stack-item{flex-direction:column;display:flex}.option-card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);overflow:hidden}.dark .option-card{border-color:var(--color-gray-700);background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .option-card{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.option-card>*{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3)}.option-card>*+*{border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200)}.dark :is(.option-card>*+*){border-color:var(--color-gray-700)}.alert-info{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-blue-200);background-color:var(--color-blue-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-blue-900);display:flex}.dark .alert-info{border-color:var(--color-blue-800);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .alert-info{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .alert-info{color:var(--color-blue-100)}.alert-warning{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-yellow-200);background-color:var(--color-yellow-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-yellow-900);display:flex}.dark .alert-warning{border-color:var(--color-yellow-800);background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-warning{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .alert-warning{color:var(--color-yellow-100)}.alert-danger{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-red-200);background-color:var(--color-red-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-900);display:flex}.dark .alert-danger{border-color:var(--color-red-800);background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .alert-danger{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .alert-danger{color:var(--color-red-100)}.alert-success{align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-md);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-green-200);background-color:var(--color-green-50);padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 3);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-green-900);display:flex}.dark .alert-success{border-color:var(--color-green-800);background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .alert-success{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .alert-success{color:var(--color-green-100)}.alert-info:has(+.row-stack),.alert-warning:has(+.row-stack),.alert-danger:has(+.row-stack),.alert-success:has(+.row-stack),.alert-error:has(+.row-stack),.alert-info:has(+.toggle-group),.alert-warning:has(+.toggle-group),.alert-danger:has(+.toggle-group),.alert-success:has(+.toggle-group),.alert-error:has(+.toggle-group),.alert-info:has(+.grid-2col-responsive),.alert-warning:has(+.grid-2col-responsive),.alert-danger:has(+.grid-2col-responsive),.alert-success:has(+.grid-2col-responsive),.alert-error:has(+.grid-2col-responsive){margin-bottom:calc(var(--spacing) * 4)}.row-stack>.alert-info,.row-stack>.alert-warning,.row-stack>.alert-danger,.row-stack>.alert-success,.row-stack>.alert-error{padding-top:calc(var(--spacing) * 3);padding-bottom:calc(var(--spacing) * 3)}.row-stack>*+.alert-info,.row-stack>*+.alert-warning,.row-stack>*+.alert-danger,.row-stack>*+.alert-success,.row-stack>*+.alert-error{margin-top:calc(var(--spacing) * 3)}.sub-tab-nav{gap:calc(var(--spacing) * 1);background-color:var(--color-white);padding-inline:calc(var(--spacing) * 6);display:flex}@media(min-width:40rem){.sub-tab-nav{padding-inline:calc(var(--spacing) * 8)}}@media(min-width:64rem){.sub-tab-nav{padding-inline:calc(var(--spacing) * 10)}}.dark .sub-tab-nav{background-color:var(--color-gray-800)}.section-header{margin-bottom:calc(var(--spacing) * 6)}.section-header>h3{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark :is(.section-header>h3){color:var(--color-white)}.section-header>p{margin-top:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark :is(.section-header>p){color:var(--color-gray-400)}.toggle-group{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);overflow:hidden}.dark .toggle-group{border-color:var(--color-gray-700)}.section-header-row{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);background-color:var(--color-gray-50);display:flex}.dark .section-header-row{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .section-header-row{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.section-header-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200)}.dark .section-header-row{border-color:var(--color-gray-700)}.section-header-row{cursor:pointer;-webkit-user-select:none;user-select:none}.list-row-hover{background-color:var(--color-white)}.dark .list-row-hover{background-color:var(--color-gray-800)}@media(hover:hover){.list-row-hover:hover{background-color:var(--color-gray-50)}.dark .list-row-hover:hover{background-color:var(--color-gray-700)}}.list-row-hover{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.text-hint{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-400)}.dark .text-hint{color:var(--color-gray-500)}.text-info-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-blue-700)}.dark .text-info-soft{color:var(--color-blue-300)}.text-warning-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-yellow-700)}.dark .text-warning-soft{color:var(--color-yellow-300)}.text-danger-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-700)}.dark .text-danger-soft{color:var(--color-red-300)}.text-success-soft{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-green-700)}.dark .text-success-soft{color:var(--color-green-300)}.checkbox-aligned{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);cursor:pointer;border-color:var(--color-gray-300);color:var(--color-gray-900);border-radius:.25rem}.checkbox-aligned:focus{--tw-ring-color:var(--color-gray-500)}.dark .checkbox-aligned{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-blue-400)}.checkbox-aligned{margin-top:calc(var(--spacing) * .5);flex-shrink:0}:where(.divider-list>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-color:var(--color-gray-200)}:where(.dark .divider-list>:not(:last-child)){border-color:var(--color-gray-700)}.modal-footer-buttons{margin-top:calc(var(--spacing) * 6);justify-content:flex-end;gap:calc(var(--spacing) * 3);display:flex}.spinner-sm{height:calc(var(--spacing) * 4);width:calc(var(--spacing) * 4);animation:var(--animate-spin)}.text-required{margin-left:calc(var(--spacing) * 1);color:var(--color-red-500)}.dark .text-required{color:var(--color-red-400)}.form-label-block{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-700);display:block}.dark .form-label-block{color:var(--color-gray-300)}.form-sub-label{margin-bottom:calc(var(--spacing) * 1);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500);display:block}.dark .form-sub-label{color:var(--color-gray-400)}.text-field-hint{margin-top:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal);color:var(--color-gray-500)}.dark .text-field-hint{color:var(--color-gray-400)}.section-heading-md{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}.dark .section-heading-md{color:var(--color-white)}.grid-3col-responsive{align-items:flex-start;gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media(min-width:64rem){.grid-3col-responsive{grid-template-columns:repeat(3,minmax(0,1fr))}}.text-error-soft{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-red-700)}.dark .text-error-soft{color:var(--color-red-300)}.text-error-strong{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-red-800)}.dark .text-error-strong{color:var(--color-red-200)}.row-content-indent{padding-left:calc(var(--spacing) * 6)}.flex-start{align-items:flex-start;display:flex}.inline-clickable{cursor:pointer;align-items:center;gap:calc(var(--spacing) * 2);display:inline-flex}.grid-2col{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(2,minmax(0,1fr));display:grid}.empty-state-cell{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);text-align:center}.form-error-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-red-500)}.dark .form-error-xs{color:var(--color-red-400)}.text-page-heading{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height));--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold);color:var(--color-gray-900)}.dark .text-page-heading{color:var(--color-white)}.text-table-header{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider);color:var(--color-gray-500);text-transform:uppercase}.dark .text-table-header{color:var(--color-gray-400)}.panel-header-row{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.dark .panel-header-row{border-color:var(--color-gray-700)}.grid-2col-responsive{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}@media(min-width:48rem){.grid-2col-responsive{grid-template-columns:repeat(2,minmax(0,1fr))}}.panel-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);padding-block:calc(var(--spacing) * 3)}.dark .panel-header{border-color:var(--color-gray-700)}.gap-sm{gap:calc(var(--spacing) * 2)}.gap-md{gap:calc(var(--spacing) * 4)}.gap-lg{gap:calc(var(--spacing) * 6)}.mb-sm{margin-bottom:calc(var(--spacing) * 2)}.mb-md{margin-bottom:calc(var(--spacing) * 4)}.mb-lg{margin-bottom:calc(var(--spacing) * 6)}.surface{background-color:var(--color-white)}.dark .surface{background-color:var(--color-gray-800)}.surface-alt{background-color:var(--color-gray-50)}.dark .surface-alt{background-color:var(--color-gray-900)}.card-border{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.dark .card-border{border-color:var(--color-gray-700)}.card-border-xl{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200)}.dark .card-border-xl{border-color:var(--color-gray-700)}.border-light{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300)}.dark .border-light{border-color:var(--color-gray-600)}.disabled-state:disabled{cursor:not-allowed;opacity:.5}.focus-ring:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.dark .focus-ring:focus{--tw-ring-offset-color:var(--color-gray-800)}.focus-ring-inset:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500);--tw-outline-style:none;--tw-ring-inset:inset;outline-style:none}@media(hover:hover){.hover-bg:hover{background-color:var(--color-gray-100)}.dark .hover-bg:hover{background-color:var(--color-gray-700)}.hover-bg-light:hover{background-color:var(--color-gray-50)}.dark .hover-bg-light:hover{background-color:var(--color-gray-800)}}.btn-base{border-radius:var(--radius-lg);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.2s;justify-content:center;align-items:center;transition-duration:.2s;display:inline-flex}.btn-base:disabled{cursor:not-allowed;opacity:.5}.btn-size-sm{padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 1.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.btn-size-md{padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.btn-size-lg{padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.badge-base{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-size-sm{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5)}.badge-size-md{padding-inline:calc(var(--spacing) * 2.5);padding-block:calc(var(--spacing) * 1)}.shadow-card{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-dropdown{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@media(hover:hover){.shadow-hover:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.page-title{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);color:var(--color-gray-900)}@media(min-width:40rem){.page-title{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}}.dark .page-title{color:var(--color-white)}.page-description{margin-top:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600)}.dark .page-description{color:var(--color-gray-400)}.page-action-primary{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-blue-600);color:var(--color-white);display:flex}@media(hover:hover){.page-action-primary:hover{background-color:var(--color-blue-700)}}.page-action-secondary{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-gray-200);color:var(--color-gray-900);display:flex}@media(hover:hover){.page-action-secondary:hover{background-color:var(--color-gray-300)}}.dark .page-action-secondary{background-color:var(--color-gray-700);color:var(--color-gray-200)}@media(hover:hover){.dark .page-action-secondary:hover{background-color:var(--color-gray-600)}}.page-action-danger{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));background-color:var(--color-red-600);color:var(--color-white);display:flex}@media(hover:hover){.page-action-danger:hover{background-color:var(--color-red-700)}}.page-action-ghost{align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding:calc(var(--spacing) * 2);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--color-gray-500);display:flex}@media(hover:hover){.page-action-ghost:hover{background-color:var(--color-gray-100);color:var(--color-gray-700)}}.dark .page-action-ghost{color:var(--color-gray-400)}@media(hover:hover){.dark .page-action-ghost:hover{background-color:var(--color-gray-800);color:var(--color-gray-200)}}.tab-container{margin-bottom:calc(var(--spacing) * 6);gap:calc(var(--spacing) * 6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);display:flex}.dark .tab-container{border-color:var(--color-gray-700)}.tab-btn{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px;margin-bottom:-1px;display:flex}@media(hover:hover){.tab-btn:hover{color:var(--color-gray-900)}}.dark .tab-btn{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn:hover{color:var(--color-gray-200)}}.tab-btn-active{align-items:center;gap:calc(var(--spacing) * 2);border-bottom-style:var(--tw-border-style);padding-inline:calc(var(--spacing) * 1);padding-block:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-bottom-width:2px;border-color:var(--color-blue-600);color:var(--color-blue-600);margin-bottom:-1px;display:flex}.dark .tab-btn-active{color:var(--color-blue-400)}.tab-badge{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:var(--color-gray-100);color:var(--color-gray-600);border-radius:3.40282e38px}.dark .tab-badge{background-color:var(--color-gray-700);color:var(--color-gray-400)}.tab-badge-active{padding-inline:calc(var(--spacing) * 2);padding-block:calc(var(--spacing) * .5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);background-color:var(--color-blue-100);color:var(--color-blue-600);border-radius:3.40282e38px}.dark .tab-badge-active{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tab-badge-active{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .tab-badge-active{color:var(--color-blue-400)}.sticky-tab-nav{top:calc(var(--spacing) * 0);z-index:40;margin-inline:calc(var(--spacing) * -6);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 6);position:sticky}.dark .sticky-tab-nav{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-tab-nav-responsive{top:calc(var(--spacing) * 0);z-index:40;margin-inline:calc(var(--spacing) * -4);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 4);position:sticky}@media(min-width:40rem){.sticky-tab-nav-responsive{margin-inline:calc(var(--spacing) * -6);padding-inline:calc(var(--spacing) * 6)}}@media(min-width:64rem){.sticky-tab-nav-responsive{margin-inline:calc(var(--spacing) * -8);padding-inline:calc(var(--spacing) * 8)}}.dark .sticky-tab-nav-responsive{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-section-header{top:calc(var(--spacing) * 0);z-index:40;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);position:sticky}.dark .sticky-section-header{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.sticky-footer-buttons{bottom:calc(var(--spacing) * 0);z-index:10;margin-top:calc(var(--spacing) * 6);justify-content:flex-end;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);display:flex;position:sticky}.dark .sticky-footer-buttons{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.sticky-footer-buttons-between{bottom:calc(var(--spacing) * 0);z-index:10;margin-top:calc(var(--spacing) * 6);justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);display:flex;position:sticky}.dark .sticky-footer-buttons-between{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.footer-buttons-between{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 3);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-50);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 3);display:flex}.dark .footer-buttons-between{border-color:var(--color-gray-700);background-color:var(--color-gray-900)}.tab-content{padding-top:calc(var(--spacing) * 6)}.admin-page-content{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 6)}.dark .admin-page-content{background-color:var(--color-gray-900)}.admin-page-content-compact{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 4)}.dark .admin-page-content-compact{background-color:var(--color-gray-900)}.admin-page-content-responsive{background-color:var(--color-gray-50);min-height:100vh;padding:calc(var(--spacing) * 4)}@media(min-width:40rem){.admin-page-content-responsive{padding:calc(var(--spacing) * 6)}}@media(min-width:64rem){.admin-page-content-responsive{padding:calc(var(--spacing) * 8)}}.dark .admin-page-content-responsive{background-color:var(--color-gray-900)}.admin-page-content-responsive-fluid{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4)}@media(min-width:40rem){.admin-page-content-responsive-fluid{padding:calc(var(--spacing) * 6)}}@media(min-width:64rem){.admin-page-content-responsive-fluid{padding:calc(var(--spacing) * 8)}}.dark .admin-page-content-responsive-fluid{background-color:var(--color-gray-900)}.admin-page-content-fluid{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 6)}.dark .admin-page-content-fluid{background-color:var(--color-gray-900)}.admin-page-content-fluid-compact{background-color:var(--color-gray-50);padding:calc(var(--spacing) * 4)}.dark .admin-page-content-fluid-compact{background-color:var(--color-gray-900)}.admin-page-content-viewport{background-color:var(--color-gray-50);height:calc(100vh - 64px);padding:calc(var(--spacing) * 6);overflow:hidden}.dark .admin-page-content-viewport{background-color:var(--color-gray-900)}.admin-page-content-viewport-compact{background-color:var(--color-gray-50);height:calc(100vh - 64px);padding:calc(var(--spacing) * 4);overflow:hidden}.dark .admin-page-content-viewport-compact{background-color:var(--color-gray-900)}.bulk-actions-bar{margin-bottom:calc(var(--spacing) * 4);align-items:center;gap:calc(var(--spacing) * 3);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-gray-100);padding:calc(var(--spacing) * 3);display:flex}.dark .bulk-actions-bar{border-color:var(--color-gray-600);background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .bulk-actions-bar{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}:where(.admin-sidebar-menu-list>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}.admin-sidebar-menu-list{padding-inline:calc(var(--spacing) * 4);padding-bottom:calc(var(--spacing) * 4)}.admin-sidebar-item{border-radius:var(--radius-lg);width:100%;padding-inline:calc(var(--spacing) * 3);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;display:flex}.admin-sidebar-subitem{border-radius:var(--radius-lg);width:100%;padding-block:6px;padding-right:calc(var(--spacing) * 3);--tw-font-weight:var(--font-weight-medium);font-size:13px;font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:space-between;align-items:center;padding-left:30px;display:flex}.admin-sidebar-item-active{background-color:var(--color-slate-100);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-900)}.dark .admin-sidebar-item-active{background-color:var(--color-slate-700);color:var(--color-white)}.admin-sidebar-item-active-parent{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-slate-900)}.dark .admin-sidebar-item-active-parent{color:var(--color-white)}.admin-sidebar-item-default{color:var(--color-slate-500)}@media(hover:hover){.admin-sidebar-item-default:hover{background-color:var(--color-slate-50);color:var(--color-slate-900)}}.dark .admin-sidebar-item-default{color:var(--color-slate-400)}@media(hover:hover){.dark .admin-sidebar-item-default:hover{background-color:var(--color-slate-800);color:var(--color-white)}}.admin-sidebar-item-disabled{cursor:default;color:var(--color-slate-400)}.dark .admin-sidebar-item-disabled{color:var(--color-slate-500)}.admin-sidebar-icon-box{flex-shrink:0;justify-content:center;align-items:center;width:18px;height:18px;display:flex}.admin-sidebar-chevron-trail{align-items:center;gap:calc(var(--spacing) * 1);display:flex}.admin-sidebar-item-content{align-items:center;gap:calc(var(--spacing) * 2);display:flex}.admin-sidebar-module-icon{margin-right:calc(var(--spacing) * 2);height:calc(var(--spacing) * 3);width:calc(var(--spacing) * 3);color:var(--color-slate-400);opacity:.6}.dark .admin-sidebar-module-icon{color:var(--color-slate-500)}.admin-sidebar-tree-line-vertical{top:calc(var(--spacing) * 0);bottom:calc(var(--spacing) * 0);z-index:0;border-left-style:var(--tw-border-style);border-left-width:1px;border-color:var(--color-slate-300);position:absolute;left:12px}.dark .admin-sidebar-tree-line-vertical{border-color:var(--color-slate-600)}.admin-sidebar-tree-line-curve{top:calc(var(--spacing) * 0);z-index:10;border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-left-style:var(--tw-border-style);width:14px;height:50%;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-left-width:1px;border-color:var(--color-slate-300);border-bottom-left-radius:10px;position:absolute;left:12px}@media(hover:hover){.admin-sidebar-tree-line-curve:is(:where(.group):hover *){border-color:var(--color-blue-400)}}.dark .admin-sidebar-tree-line-curve{border-color:var(--color-slate-600)}@media(hover:hover){.dark .admin-sidebar-tree-line-curve:is(:where(.group):hover *){border-color:var(--color-blue-500)}}.admin-sidebar-header{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-slate-200);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 4);justify-content:space-between;align-items:center;display:flex}.dark .admin-sidebar-header{border-color:var(--color-slate-700)}.admin-sidebar-collapse-btn{border-radius:var(--radius-md);padding:calc(var(--spacing) * 2);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.admin-sidebar-collapse-btn:hover{background-color:var(--color-slate-100)}.dark .admin-sidebar-collapse-btn:hover{background-color:var(--color-slate-700)}}.tab-nav-default{gap:calc(var(--spacing) * 2);display:flex}.tab-nav-underline{gap:calc(var(--spacing) * 0);border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--color-gray-200);display:flex}.dark .tab-nav-underline{border-color:var(--color-gray-700)}.tab-btn-base{align-items:center;gap:calc(var(--spacing) * 2);padding-inline:calc(var(--spacing) * 4);padding-block:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}.tab-btn-default{border-bottom-style:var(--tw-border-style);color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px}@media(hover:hover){.tab-btn-default:hover{background-color:var(--color-gray-50);color:var(--color-gray-900)}}.dark .tab-btn-default{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-default:hover{background-color:var(--color-gray-700);color:var(--color-gray-200)}}.tab-btn-default-active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-blue-600);background-color:var(--color-blue-50);color:var(--color-blue-600)}.dark .tab-btn-default-active{border-color:var(--color-blue-400);background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .tab-btn-default-active{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .tab-btn-default-active{color:var(--color-blue-400)}.tab-btn-underline{border-bottom-style:var(--tw-border-style);color:var(--color-gray-600);border-color:#0000;border-bottom-width:2px}@media(hover:hover){.tab-btn-underline:hover{border-color:var(--color-gray-300);color:var(--color-gray-900)}}.dark .tab-btn-underline{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-underline:hover{border-color:var(--color-gray-600);color:var(--color-gray-200)}}.tab-btn-underline-active{border-bottom-style:var(--tw-border-style);border-bottom-width:2px;border-color:var(--color-blue-600);color:var(--color-blue-600)}.dark .tab-btn-underline-active{border-color:var(--color-blue-400);color:var(--color-blue-400)}.tab-btn-pills{border-radius:var(--radius-lg);color:var(--color-gray-600)}@media(hover:hover){.tab-btn-pills:hover{background-color:var(--color-gray-100)}}.dark .tab-btn-pills{color:var(--color-gray-400)}@media(hover:hover){.dark .tab-btn-pills:hover{background-color:var(--color-gray-700)}}.tab-btn-pills-active{border-radius:var(--radius-lg);background-color:var(--color-blue-600);color:var(--color-white)}.dark .tab-btn-pills-active{background-color:var(--color-blue-500)}.tab-btn-disabled{cursor:not-allowed;color:var(--color-gray-400);opacity:.5}.dark .tab-btn-disabled{color:var(--color-gray-600)}.toggle-container{align-items:flex-start;column-gap:calc(var(--spacing) * 2);flex-wrap:wrap;width:auto;display:inline-flex}.toggle-switch-wrapper{flex-shrink:0;align-items:center;display:inline-flex;position:relative}.toggle-switch-track{background-color:var(--color-gray-200);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:relative}.toggle-switch-track:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-300);--tw-outline-style:none;outline-style:none}.toggle-switch-track:is(:where(.peer):checked~*){background-color:var(--color-blue-600)}.dark .toggle-switch-track{background-color:var(--color-gray-700)}.dark .toggle-switch-track:is(:where(.peer):focus~*){--tw-ring-color:var(--color-blue-800)}.dark .toggle-switch-track:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.toggle-switch-track:after{top:calc(var(--spacing) * .5);left:calc(var(--spacing) * .5);--tw-content:"";border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);background-color:var(--color-white);content:var(--tw-content);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));border-radius:3.40282e38px;position:absolute}.dark .toggle-switch-track:after{content:var(--tw-content);border-color:var(--color-gray-600)}.toggle-switch-track:is(:where(.peer):checked~*):after{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y);content:var(--tw-content);border-color:var(--color-white)}.toggle-label{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);color:var(--color-gray-900)}.dark .toggle-label{color:var(--color-white)}.toggle-description{margin-top:calc(var(--spacing) * 1);width:100%;font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));color:var(--color-gray-500)}.dark .toggle-description{color:var(--color-gray-400)}.admin-footer{margin-top:calc(var(--spacing) * 6);padding-inline:calc(var(--spacing) * 6);padding-block:calc(var(--spacing) * 4)}.admin-footer-row{justify-content:space-between;align-items:center;gap:calc(var(--spacing) * 4);flex-direction:column;display:flex}@media(min-width:48rem){.admin-footer-row{flex-direction:row}}.admin-footer-group{align-items:center;gap:calc(var(--spacing) * 4);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);display:flex}.dark .admin-footer-group{color:var(--color-gray-400)}.admin-footer-link-hover{cursor:pointer;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media(hover:hover){.admin-footer-link-hover:hover{color:var(--color-blue-600)}.dark .admin-footer-link-hover:hover{color:var(--color-blue-400)}}.admin-footer-link{align-items:center;gap:calc(var(--spacing) * 2);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-600);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));display:flex}@media(hover:hover){.admin-footer-link:hover{color:var(--color-blue-600)}}.dark .admin-footer-link{color:var(--color-gray-400)}@media(hover:hover){.dark .admin-footer-link:hover{color:var(--color-blue-400)}}.responsive-grid{gap:calc(var(--spacing) * 4);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}.grid-cols-1-lg-3{gap:calc(var(--spacing) * 6);grid-template-columns:repeat(1,minmax(0,1fr));display:grid}.col-span-1-lg,.col-span-2-lg{grid-column:span 1/span 1}.modal-description{margin-top:calc(var(--spacing) * 1);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));color:var(--color-gray-500)}.dark .modal-description{color:var(--color-gray-400)}.toast-message{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.visible\!{visibility:visible!important}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-1{inset:calc(var(--spacing) * 1)}.inset-2{inset:calc(var(--spacing) * 2)}.inset-3{inset:calc(var(--spacing) * 3)}.inset-4{inset:calc(var(--spacing) * 4)}.inset-5{inset:calc(var(--spacing) * 5)}.inset-6{inset:calc(var(--spacing) * 6)}.inset-8{inset:calc(var(--spacing) * 8)}.inset-auto{inset:auto}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.inset-x-1{inset-inline:calc(var(--spacing) * 1)}.inset-x-2{inset-inline:calc(var(--spacing) * 2)}.inset-x-3{inset-inline:calc(var(--spacing) * 3)}.inset-x-4{inset-inline:calc(var(--spacing) * 4)}.inset-x-auto{inset-inline:auto}.inset-y-0{inset-block:calc(var(--spacing) * 0)}.inset-y-1{inset-block:calc(var(--spacing) * 1)}.inset-y-2{inset-block:calc(var(--spacing) * 2)}.inset-y-3{inset-block:calc(var(--spacing) * 3)}.inset-y-4{inset-block:calc(var(--spacing) * 4)}.inset-y-auto{inset-block:auto}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.-top-1{top:calc(var(--spacing) * -1)}.-top-2{top:calc(var(--spacing) * -2)}.-top-3{top:calc(var(--spacing) * -3)}.-top-4{top:calc(var(--spacing) * -4)}.top-0{top:calc(var(--spacing) * 0)}.top-1{top:calc(var(--spacing) * 1)}.top-1\.5{top:calc(var(--spacing) * 1.5)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing) * 2)}.top-2\/3{top:66.6667%}.top-3{top:calc(var(--spacing) * 3)}.top-3\/4{top:75%}.top-4{top:calc(var(--spacing) * 4)}.top-5{top:calc(var(--spacing) * 5)}.top-6{top:calc(var(--spacing) * 6)}.top-8{top:calc(var(--spacing) * 8)}.top-10{top:calc(var(--spacing) * 10)}.top-12{top:calc(var(--spacing) * 12)}.top-16{top:calc(var(--spacing) * 16)}.top-auto{top:auto}.top-full{top:100%}.-right-1{right:calc(var(--spacing) * -1)}.-right-2{right:calc(var(--spacing) * -2)}.-right-3{right:calc(var(--spacing) * -3)}.-right-4{right:calc(var(--spacing) * -4)}.right-0{right:calc(var(--spacing) * 0)}.right-1{right:calc(var(--spacing) * 1)}.right-1\/2{right:50%}.right-2{right:calc(var(--spacing) * 2)}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.right-5{right:calc(var(--spacing) * 5)}.right-6{right:calc(var(--spacing) * 6)}.right-8{right:calc(var(--spacing) * 8)}.right-10{right:calc(var(--spacing) * 10)}.right-12{right:calc(var(--spacing) * 12)}.right-16{right:calc(var(--spacing) * 16)}.right-auto{right:auto}.right-full{right:100%}.-bottom-1{bottom:calc(var(--spacing) * -1)}.-bottom-2{bottom:calc(var(--spacing) * -2)}.-bottom-3{bottom:calc(var(--spacing) * -3)}.-bottom-4{bottom:calc(var(--spacing) * -4)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-1{bottom:calc(var(--spacing) * 1)}.bottom-1\/2{bottom:50%}.bottom-2{bottom:calc(var(--spacing) * 2)}.bottom-3{bottom:calc(var(--spacing) * 3)}.bottom-4{bottom:calc(var(--spacing) * 4)}.bottom-5{bottom:calc(var(--spacing) * 5)}.bottom-6{bottom:calc(var(--spacing) * 6)}.bottom-8{bottom:calc(var(--spacing) * 8)}.bottom-10{bottom:calc(var(--spacing) * 10)}.bottom-12{bottom:calc(var(--spacing) * 12)}.bottom-16{bottom:calc(var(--spacing) * 16)}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.-left-1{left:calc(var(--spacing) * -1)}.-left-2{left:calc(var(--spacing) * -2)}.-left-3{left:calc(var(--spacing) * -3)}.-left-4{left:calc(var(--spacing) * -4)}.left-0{left:calc(var(--spacing) * 0)}.left-1{left:calc(var(--spacing) * 1)}.left-1\.5{left:calc(var(--spacing) * 1.5)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.left-5{left:calc(var(--spacing) * 5)}.left-6{left:calc(var(--spacing) * 6)}.left-8{left:calc(var(--spacing) * 8)}.left-10{left:calc(var(--spacing) * 10)}.left-12{left:calc(var(--spacing) * 12)}.left-16{left:calc(var(--spacing) * 16)}.left-auto{left:auto}.left-full{left:100%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[999\]{z-index:999}.z-\[9999\]{z-index:9999}.z-auto{z-index:auto}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-5{grid-column:span 5/span 5}.col-span-6{grid-column:span 6/span 6}.col-span-7{grid-column:span 7/span 7}.col-span-8{grid-column:span 8/span 8}.col-span-9{grid-column:span 9/span 9}.col-span-10{grid-column:span 10/span 10}.col-span-11{grid-column:span 11/span 11}.col-span-12{grid-column:span 12/span 12}.col-span-full{grid-column:1/-1}.col-start-1{grid-column-start:1}.col-start-2{grid-column-start:2}.col-start-3{grid-column-start:3}.col-start-4{grid-column-start:4}.col-start-5{grid-column-start:5}.col-start-6{grid-column-start:6}.col-start-7{grid-column-start:7}.col-start-8{grid-column-start:8}.col-start-9{grid-column-start:9}.col-start-10{grid-column-start:10}.col-start-11{grid-column-start:11}.col-start-12{grid-column-start:12}.col-start-13{grid-column-start:13}.col-end-1{grid-column-end:1}.col-end-2{grid-column-end:2}.col-end-3{grid-column-end:3}.col-end-4{grid-column-end:4}.col-end-5{grid-column-end:5}.col-end-6{grid-column-end:6}.col-end-7{grid-column-end:7}.col-end-8{grid-column-end:8}.col-end-9{grid-column-end:9}.col-end-10{grid-column-end:10}.col-end-11{grid-column-end:11}.col-end-12{grid-column-end:12}.col-end-13{grid-column-end:13}.row-span-1{grid-row:span 1/span 1}.row-span-2{grid-row:span 2/span 2}.row-span-3{grid-row:span 3/span 3}.row-span-4{grid-row:span 4/span 4}.row-span-5{grid-row:span 5/span 5}.row-span-6{grid-row:span 6/span 6}.row-span-full{grid-row:1/-1}.row-start-1{grid-row-start:1}.row-start-2{grid-row-start:2}.row-start-3{grid-row-start:3}.row-start-4{grid-row-start:4}.row-start-5{grid-row-start:5}.row-start-6{grid-row-start:6}.row-start-7{grid-row-start:7}.row-end-1{grid-row-end:1}.row-end-2{grid-row-end:2}.row-end-3{grid-row-end:3}.row-end-4{grid-row-end:4}.row-end-5{grid-row-end:5}.row-end-6{grid-row-end:6}.row-end-7{grid-row-end:7}.float-end{float:inline-end}.float-left{float:left}.float-none{float:none}.float-right{float:right}.float-start{float:inline-start}.clear-both{clear:both}.clear-end{clear:inline-end}.clear-left{clear:left}.clear-none{clear:none}.clear-right{clear:right}.clear-start{clear:inline-start}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.\!m-0{margin:calc(var(--spacing) * 0)!important}.\!m-1{margin:calc(var(--spacing) * 1)!important}.\!m-2{margin:calc(var(--spacing) * 2)!important}.\!m-3{margin:calc(var(--spacing) * 3)!important}.\!m-4{margin:calc(var(--spacing) * 4)!important}.\!m-5{margin:calc(var(--spacing) * 5)!important}.\!m-6{margin:calc(var(--spacing) * 6)!important}.\!m-8{margin:calc(var(--spacing) * 8)!important}.\!m-10{margin:calc(var(--spacing) * 10)!important}.\!m-12{margin:calc(var(--spacing) * 12)!important}.\!m-16{margin:calc(var(--spacing) * 16)!important}.\!m-auto{margin:auto!important}.-m-1{margin:calc(var(--spacing) * -1)}.-m-2{margin:calc(var(--spacing) * -2)}.-m-3{margin:calc(var(--spacing) * -3)}.-m-4{margin:calc(var(--spacing) * -4)}.-m-5{margin:calc(var(--spacing) * -5)}.-m-6{margin:calc(var(--spacing) * -6)}.-m-8{margin:calc(var(--spacing) * -8)}.m-0{margin:calc(var(--spacing) * 0)}.m-1{margin:calc(var(--spacing) * 1)}.m-2{margin:calc(var(--spacing) * 2)}.m-3{margin:calc(var(--spacing) * 3)}.m-4{margin:calc(var(--spacing) * 4)}.m-5{margin:calc(var(--spacing) * 5)}.m-6{margin:calc(var(--spacing) * 6)}.m-8{margin:calc(var(--spacing) * 8)}.m-10{margin:calc(var(--spacing) * 10)}.m-12{margin:calc(var(--spacing) * 12)}.m-16{margin:calc(var(--spacing) * 16)}.m-auto{margin:auto}.\!mx-0{margin-inline:calc(var(--spacing) * 0)!important}.\!mx-1{margin-inline:calc(var(--spacing) * 1)!important}.\!mx-2{margin-inline:calc(var(--spacing) * 2)!important}.\!mx-3{margin-inline:calc(var(--spacing) * 3)!important}.\!mx-4{margin-inline:calc(var(--spacing) * 4)!important}.\!mx-5{margin-inline:calc(var(--spacing) * 5)!important}.\!mx-6{margin-inline:calc(var(--spacing) * 6)!important}.\!mx-8{margin-inline:calc(var(--spacing) * 8)!important}.\!mx-10{margin-inline:calc(var(--spacing) * 10)!important}.\!mx-12{margin-inline:calc(var(--spacing) * 12)!important}.\!mx-16{margin-inline:calc(var(--spacing) * 16)!important}.\!mx-auto{margin-inline:auto!important}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.-mx-2{margin-inline:calc(var(--spacing) * -2)}.-mx-3{margin-inline:calc(var(--spacing) * -3)}.-mx-4{margin-inline:calc(var(--spacing) * -4)}.-mx-5{margin-inline:calc(var(--spacing) * -5)}.-mx-6{margin-inline:calc(var(--spacing) * -6)}.-mx-8{margin-inline:calc(var(--spacing) * -8)}.mx-0{margin-inline:calc(var(--spacing) * 0)}.mx-1{margin-inline:calc(var(--spacing) * 1)}.mx-1\.5{margin-inline:calc(var(--spacing) * 1.5)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-3{margin-inline:calc(var(--spacing) * 3)}.mx-4{margin-inline:calc(var(--spacing) * 4)}.mx-5{margin-inline:calc(var(--spacing) * 5)}.mx-6{margin-inline:calc(var(--spacing) * 6)}.mx-8{margin-inline:calc(var(--spacing) * 8)}.mx-10{margin-inline:calc(var(--spacing) * 10)}.mx-12{margin-inline:calc(var(--spacing) * 12)}.mx-16{margin-inline:calc(var(--spacing) * 16)}.mx-auto{margin-inline:auto}.\!my-0{margin-block:calc(var(--spacing) * 0)!important}.\!my-1{margin-block:calc(var(--spacing) * 1)!important}.\!my-2{margin-block:calc(var(--spacing) * 2)!important}.\!my-3{margin-block:calc(var(--spacing) * 3)!important}.\!my-4{margin-block:calc(var(--spacing) * 4)!important}.\!my-5{margin-block:calc(var(--spacing) * 5)!important}.\!my-6{margin-block:calc(var(--spacing) * 6)!important}.\!my-8{margin-block:calc(var(--spacing) * 8)!important}.\!my-10{margin-block:calc(var(--spacing) * 10)!important}.\!my-12{margin-block:calc(var(--spacing) * 12)!important}.\!my-16{margin-block:calc(var(--spacing) * 16)!important}.\!my-auto{margin-block:auto!important}.-my-1{margin-block:calc(var(--spacing) * -1)}.-my-2{margin-block:calc(var(--spacing) * -2)}.-my-3{margin-block:calc(var(--spacing) * -3)}.-my-4{margin-block:calc(var(--spacing) * -4)}.-my-5{margin-block:calc(var(--spacing) * -5)}.-my-6{margin-block:calc(var(--spacing) * -6)}.-my-8{margin-block:calc(var(--spacing) * -8)}.my-0{margin-block:calc(var(--spacing) * 0)}.my-1{margin-block:calc(var(--spacing) * 1)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.my-5{margin-block:calc(var(--spacing) * 5)}.my-6{margin-block:calc(var(--spacing) * 6)}.my-8{margin-block:calc(var(--spacing) * 8)}.my-10{margin-block:calc(var(--spacing) * 10)}.my-12{margin-block:calc(var(--spacing) * 12)}.my-16{margin-block:calc(var(--spacing) * 16)}.my-auto{margin-block:auto}.-mt-1{margin-top:calc(var(--spacing) * -1)}.-mt-2{margin-top:calc(var(--spacing) * -2)}.-mt-3{margin-top:calc(var(--spacing) * -3)}.-mt-4{margin-top:calc(var(--spacing) * -4)}.-mt-5{margin-top:calc(var(--spacing) * -5)}.-mt-6{margin-top:calc(var(--spacing) * -6)}.-mt-8{margin-top:calc(var(--spacing) * -8)}.mt-0{margin-top:calc(var(--spacing) * 0)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-auto{margin-top:auto}.-mr-1{margin-right:calc(var(--spacing) * -1)}.-mr-2{margin-right:calc(var(--spacing) * -2)}.-mr-3{margin-right:calc(var(--spacing) * -3)}.-mr-4{margin-right:calc(var(--spacing) * -4)}.-mr-5{margin-right:calc(var(--spacing) * -5)}.-mr-6{margin-right:calc(var(--spacing) * -6)}.-mr-8{margin-right:calc(var(--spacing) * -8)}.mr-0{margin-right:calc(var(--spacing) * 0)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-3{margin-right:calc(var(--spacing) * 3)}.mr-4{margin-right:calc(var(--spacing) * 4)}.mr-5{margin-right:calc(var(--spacing) * 5)}.mr-6{margin-right:calc(var(--spacing) * 6)}.mr-8{margin-right:calc(var(--spacing) * 8)}.mr-10{margin-right:calc(var(--spacing) * 10)}.mr-12{margin-right:calc(var(--spacing) * 12)}.mr-16{margin-right:calc(var(--spacing) * 16)}.mr-auto{margin-right:auto}.-mb-1{margin-bottom:calc(var(--spacing) * -1)}.-mb-2{margin-bottom:calc(var(--spacing) * -2)}.-mb-3{margin-bottom:calc(var(--spacing) * -3)}.-mb-4{margin-bottom:calc(var(--spacing) * -4)}.-mb-5{margin-bottom:calc(var(--spacing) * -5)}.-mb-6{margin-bottom:calc(var(--spacing) * -6)}.-mb-8{margin-bottom:calc(var(--spacing) * -8)}.-mb-px{margin-bottom:-1px}.mb-0{margin-bottom:calc(var(--spacing) * 0)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-10{margin-bottom:calc(var(--spacing) * 10)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.mb-16{margin-bottom:calc(var(--spacing) * 16)}.mb-auto{margin-bottom:auto}.-ml-1{margin-left:calc(var(--spacing) * -1)}.-ml-2{margin-left:calc(var(--spacing) * -2)}.-ml-3{margin-left:calc(var(--spacing) * -3)}.-ml-4{margin-left:calc(var(--spacing) * -4)}.-ml-5{margin-left:calc(var(--spacing) * -5)}.-ml-6{margin-left:calc(var(--spacing) * -6)}.-ml-8{margin-left:calc(var(--spacing) * -8)}.ml-0{margin-left:calc(var(--spacing) * 0)}.ml-0\.5{margin-left:calc(var(--spacing) * .5)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-4{margin-left:calc(var(--spacing) * 4)}.ml-5{margin-left:calc(var(--spacing) * 5)}.ml-6{margin-left:calc(var(--spacing) * 6)}.ml-8{margin-left:calc(var(--spacing) * 8)}.ml-10{margin-left:calc(var(--spacing) * 10)}.ml-12{margin-left:calc(var(--spacing) * 12)}.ml-16{margin-left:calc(var(--spacing) * 16)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\!block{display:block!important}.\!flex{display:flex!important}.\!grid{display:grid!important}.\!hidden{display:none!important}.\!inline{display:inline!important}.\!inline-block{display:inline-block!important}.\!inline-flex{display:inline-flex!important}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.\!h-0{height:calc(var(--spacing) * 0)!important}.\!h-1{height:calc(var(--spacing) * 1)!important}.\!h-2{height:calc(var(--spacing) * 2)!important}.\!h-3{height:calc(var(--spacing) * 3)!important}.\!h-4{height:calc(var(--spacing) * 4)!important}.\!h-5{height:calc(var(--spacing) * 5)!important}.\!h-6{height:calc(var(--spacing) * 6)!important}.\!h-8{height:calc(var(--spacing) * 8)!important}.\!h-10{height:calc(var(--spacing) * 10)!important}.\!h-12{height:calc(var(--spacing) * 12)!important}.\!h-16{height:calc(var(--spacing) * 16)!important}.\!h-20{height:calc(var(--spacing) * 20)!important}.\!h-24{height:calc(var(--spacing) * 24)!important}.\!h-32{height:calc(var(--spacing) * 32)!important}.\!h-40{height:calc(var(--spacing) * 40)!important}.\!h-48{height:calc(var(--spacing) * 48)!important}.\!h-56{height:calc(var(--spacing) * 56)!important}.\!h-64{height:calc(var(--spacing) * 64)!important}.\!h-72{height:calc(var(--spacing) * 72)!important}.\!h-80{height:calc(var(--spacing) * 80)!important}.\!h-96{height:calc(var(--spacing) * 96)!important}.\!h-auto{height:auto!important}.\!h-fit{height:fit-content!important}.\!h-full{height:100%!important}.\!h-max{height:max-content!important}.\!h-min{height:min-content!important}.\!h-px{height:1px!important}.\!h-screen{height:100vh!important}.h-0{height:calc(var(--spacing) * 0)}.h-1{height:calc(var(--spacing) * 1)}.h-1\/2{height:50%}.h-1\/3{height:33.3333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.6667%}.h-2{height:calc(var(--spacing) * 2)}.h-2\/3{height:66.6667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-3{height:calc(var(--spacing) * 3)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing) * 4)}.h-4\/5{height:80%}.h-5{height:calc(var(--spacing) * 5)}.h-5\/6{height:83.3333%}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-32{height:calc(var(--spacing) * 32)}.h-40{height:calc(var(--spacing) * 40)}.h-48{height:calc(var(--spacing) * 48)}.h-56{height:calc(var(--spacing) * 56)}.h-64{height:calc(var(--spacing) * 64)}.h-72{height:calc(var(--spacing) * 72)}.h-80{height:calc(var(--spacing) * 80)}.h-96{height:calc(var(--spacing) * 96)}.h-\[42px\]{height:42px}.h-auto{height:auto}.h-fit{height:fit-content}.h-full{height:100%}.h-max{height:max-content}.h-min{height:min-content}.h-px{height:1px}.h-screen{height:100vh}.max-h-0{max-height:calc(var(--spacing) * 0)}.max-h-20{max-height:calc(var(--spacing) * 20)}.max-h-32{max-height:calc(var(--spacing) * 32)}.max-h-40{max-height:calc(var(--spacing) * 40)}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-56{max-height:calc(var(--spacing) * 56)}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[70vh\]{max-height:70vh}.max-h-\[400px\]{max-height:400px}.max-h-\[500px\]{max-height:500px}.max-h-\[calc\(90vh-140px\)\]{max-height:calc(90vh - 140px)}.max-h-\[calc\(100vh-320px\)\]{max-height:calc(100vh - 320px)}.max-h-fit{max-height:fit-content}.max-h-full{max-height:100%}.max-h-max{max-height:max-content}.max-h-min{max-height:min-content}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-12{min-height:calc(var(--spacing) * 12)}.min-h-16{min-height:calc(var(--spacing) * 16)}.min-h-20{min-height:calc(var(--spacing) * 20)}.min-h-24{min-height:calc(var(--spacing) * 24)}.min-h-32{min-height:calc(var(--spacing) * 32)}.min-h-40{min-height:calc(var(--spacing) * 40)}.min-h-48{min-height:calc(var(--spacing) * 48)}.min-h-56{min-height:calc(var(--spacing) * 56)}.min-h-64{min-height:calc(var(--spacing) * 64)}.min-h-\[40px\]{min-height:40px}.min-h-\[42px\]{min-height:42px}.min-h-\[60px\]{min-height:60px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[80px\]{min-height:80px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-fit{min-height:fit-content}.min-h-full{min-height:100%}.min-h-max{min-height:max-content}.min-h-min{min-height:min-content}.min-h-screen{min-height:100vh}.\!w-0{width:calc(var(--spacing) * 0)!important}.\!w-1{width:calc(var(--spacing) * 1)!important}.\!w-1\/2{width:50%!important}.\!w-1\/3{width:33.3333%!important}.\!w-1\/4{width:25%!important}.\!w-1\/5{width:20%!important}.\!w-1\/6{width:16.6667%!important}.\!w-2{width:calc(var(--spacing) * 2)!important}.\!w-2\/3{width:66.6667%!important}.\!w-2\/4{width:50%!important}.\!w-2\/5{width:40%!important}.\!w-3{width:calc(var(--spacing) * 3)!important}.\!w-3\/4{width:75%!important}.\!w-3\/5{width:60%!important}.\!w-4{width:calc(var(--spacing) * 4)!important}.\!w-4\/5{width:80%!important}.\!w-5{width:calc(var(--spacing) * 5)!important}.\!w-5\/6{width:83.3333%!important}.\!w-6{width:calc(var(--spacing) * 6)!important}.\!w-8{width:calc(var(--spacing) * 8)!important}.\!w-10{width:calc(var(--spacing) * 10)!important}.\!w-12{width:calc(var(--spacing) * 12)!important}.\!w-16{width:calc(var(--spacing) * 16)!important}.\!w-20{width:calc(var(--spacing) * 20)!important}.\!w-24{width:calc(var(--spacing) * 24)!important}.\!w-32{width:calc(var(--spacing) * 32)!important}.\!w-40{width:calc(var(--spacing) * 40)!important}.\!w-48{width:calc(var(--spacing) * 48)!important}.\!w-52{width:calc(var(--spacing) * 52)!important}.\!w-56{width:calc(var(--spacing) * 56)!important}.\!w-60{width:calc(var(--spacing) * 60)!important}.\!w-64{width:calc(var(--spacing) * 64)!important}.\!w-72{width:calc(var(--spacing) * 72)!important}.\!w-80{width:calc(var(--spacing) * 80)!important}.\!w-96{width:calc(var(--spacing) * 96)!important}.\!w-auto{width:auto!important}.\!w-fit{width:fit-content!important}.\!w-full{width:100%!important}.\!w-max{width:max-content!important}.\!w-min{width:min-content!important}.\!w-px{width:1px!important}.\!w-screen{width:100vw!important}.w-0{width:calc(var(--spacing) * 0)}.w-1{width:calc(var(--spacing) * 1)}.w-1\/2{width:50%}.w-1\/3{width:33.3333%}.w-1\/4{width:25%}.w-1\/5{width:20%}.w-1\/6{width:16.6667%}.w-2{width:calc(var(--spacing) * 2)}.w-2\/3{width:66.6667%}.w-2\/4{width:50%}.w-2\/5{width:40%}.w-3{width:calc(var(--spacing) * 3)}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4{width:calc(var(--spacing) * 4)}.w-4\/5{width:80%}.w-5{width:calc(var(--spacing) * 5)}.w-5\/6{width:83.3333%}.w-6{width:calc(var(--spacing) * 6)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-11{width:calc(var(--spacing) * 11)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-28{width:calc(var(--spacing) * 28)}.w-32{width:calc(var(--spacing) * 32)}.w-36{width:calc(var(--spacing) * 36)}.w-40{width:calc(var(--spacing) * 40)}.w-48{width:calc(var(--spacing) * 48)}.w-52{width:calc(var(--spacing) * 52)}.w-56{width:calc(var(--spacing) * 56)}.w-60{width:calc(var(--spacing) * 60)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-min{width:min-content}.w-px{width:1px}.w-screen{width:100vw}.\!max-w-0{max-width:calc(var(--spacing) * 0)!important}.\!max-w-2xl{max-width:var(--container-2xl)!important}.\!max-w-3xl{max-width:var(--container-3xl)!important}.\!max-w-4xl{max-width:var(--container-4xl)!important}.\!max-w-full{max-width:100%!important}.\!max-w-lg{max-width:var(--container-lg)!important}.\!max-w-md{max-width:var(--container-md)!important}.\!max-w-none{max-width:none!important}.\!max-w-sm{max-width:var(--container-sm)!important}.\!max-w-xl{max-width:var(--container-xl)!important}.\!max-w-xs{max-width:var(--container-xs)!important}.max-w-0{max-width:calc(var(--spacing) * 0)}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[200px\]{max-width:200px}.max-w-\[calc\(100vw-1rem\)\]{max-width:calc(100vw - 1rem)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-fit{max-width:fit-content}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-max{max-width:max-content}.max-w-md{max-width:var(--container-md)}.max-w-min{max-width:min-content}.max-w-none{max-width:none}.max-w-prose{max-width:65ch}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-screen-lg{max-width:var(--breakpoint-lg)}.max-w-screen-md{max-width:var(--breakpoint-md)}.max-w-screen-sm{max-width:var(--breakpoint-sm)}.max-w-screen-xl{max-width:var(--breakpoint-xl)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.\!min-w-0{min-width:calc(var(--spacing) * 0)!important}.\!min-w-4{min-width:calc(var(--spacing) * 4)!important}.\!min-w-8{min-width:calc(var(--spacing) * 8)!important}.\!min-w-12{min-width:calc(var(--spacing) * 12)!important}.\!min-w-16{min-width:calc(var(--spacing) * 16)!important}.\!min-w-20{min-width:calc(var(--spacing) * 20)!important}.\!min-w-24{min-width:calc(var(--spacing) * 24)!important}.\!min-w-32{min-width:calc(var(--spacing) * 32)!important}.\!min-w-40{min-width:calc(var(--spacing) * 40)!important}.\!min-w-48{min-width:calc(var(--spacing) * 48)!important}.\!min-w-56{min-width:calc(var(--spacing) * 56)!important}.\!min-w-64{min-width:calc(var(--spacing) * 64)!important}.\!min-w-fit{min-width:fit-content!important}.\!min-w-full{min-width:100%!important}.\!min-w-max{min-width:max-content!important}.\!min-w-min{min-width:min-content!important}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[20px\]{min-width:20px}.min-w-\[80px\]{min-width:80px}.min-w-\[120px\]{min-width:120px}.min-w-\[200px\]{min-width:200px}.min-w-\[280px\]{min-width:280px}.min-w-\[300px\]{min-width:300px}.min-w-fit{min-width:fit-content}.min-w-full{min-width:100%}.min-w-max{min-width:max-content}.min-w-min{min-width:min-content}.\!flex-1{flex:1!important}.\!flex-auto{flex:auto!important}.\!flex-initial{flex:0 auto!important}.\!flex-none{flex:none!important}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-0{flex-basis:calc(var(--spacing) * 0)}.basis-1{flex-basis:calc(var(--spacing) * 1)}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.3333%}.basis-1\/4{flex-basis:25%}.basis-2{flex-basis:calc(var(--spacing) * 2)}.basis-2\/3{flex-basis:66.6667%}.basis-3\/4{flex-basis:75%}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.caption-bottom{caption-side:bottom}.caption-top{caption-side:top}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:calc(var(--spacing) * 0);--tw-border-spacing-y:calc(var(--spacing) * 0);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-1{--tw-border-spacing-x:calc(var(--spacing) * 1);--tw-border-spacing-y:calc(var(--spacing) * 1);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-2{--tw-border-spacing-x:calc(var(--spacing) * 2);--tw-border-spacing-y:calc(var(--spacing) * 2);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-3{--tw-border-spacing-x:calc(var(--spacing) * 3);--tw-border-spacing-y:calc(var(--spacing) * 3);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-4{--tw-border-spacing-x:calc(var(--spacing) * 4);--tw-border-spacing-y:calc(var(--spacing) * 4);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-5{--tw-border-spacing-x:calc(var(--spacing) * 5);--tw-border-spacing-y:calc(var(--spacing) * 5);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-6{--tw-border-spacing-x:calc(var(--spacing) * 6);--tw-border-spacing-y:calc(var(--spacing) * 6);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.border-spacing-8{--tw-border-spacing-x:calc(var(--spacing) * 8);--tw-border-spacing-y:calc(var(--spacing) * 8);border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.origin-bottom{transform-origin:bottom}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.origin-center{transform-origin:50%}.origin-left{transform-origin:0}.origin-right{transform-origin:100%}.origin-top{transform-origin:top}.origin-top-left{transform-origin:0 0}.origin-top-right{transform-origin:100% 0}.-translate-x-1{--tw-translate-x:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-2{--tw-translate-x:calc(var(--spacing) * -2);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-3{--tw-translate-x:calc(var(--spacing) * -3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-4{--tw-translate-x:calc(var(--spacing) * -4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1{--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x: 50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/3{--tw-translate-x:calc(1 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-1\/4{--tw-translate-x: 25% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-2{--tw-translate-x:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-2\/3{--tw-translate-x:calc(2 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-3{--tw-translate-x:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-3\/4{--tw-translate-x: 75% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-6{--tw-translate-x:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-8{--tw-translate-x:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-10{--tw-translate-x:calc(var(--spacing) * 10);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-12{--tw-translate-x:calc(var(--spacing) * 12);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-2{--tw-translate-y:calc(var(--spacing) * -2);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-3{--tw-translate-y:calc(var(--spacing) * -3);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-4{--tw-translate-y:calc(var(--spacing) * -4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-full{--tw-translate-y:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y: 50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/3{--tw-translate-y:calc(1 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1\/4{--tw-translate-y: 25% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing) * 2);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-2\/3{--tw-translate-y:calc(2 / 3 * 100%);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing) * 3);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-3\/4{--tw-translate-y: 75% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-4{--tw-translate-y:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-5{--tw-translate-y:calc(var(--spacing) * 5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-6{--tw-translate-y:calc(var(--spacing) * 6);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing) * 8);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-10{--tw-translate-y:calc(var(--spacing) * 10);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-12{--tw-translate-y:calc(var(--spacing) * 12);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-full{--tw-translate-y:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-50{--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-150{--tw-scale-x:150%;--tw-scale-y:150%;--tw-scale-z:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-50{--tw-scale-x:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-75{--tw-scale-x:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-90{--tw-scale-x:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-95{--tw-scale-x:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-100{--tw-scale-x:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-105{--tw-scale-x:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-110{--tw-scale-x:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-125{--tw-scale-x:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-x-150{--tw-scale-x:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-0{--tw-scale-y:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-50{--tw-scale-y:50%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-75{--tw-scale-y:75%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-90{--tw-scale-y:90%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-95{--tw-scale-y:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-100{--tw-scale-y:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-105{--tw-scale-y:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-110{--tw-scale-y:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-125{--tw-scale-y:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-y-150{--tw-scale-y:150%;scale:var(--tw-scale-x) var(--tw-scale-y)}.-rotate-1{rotate:-1deg}.-rotate-2{rotate:-2deg}.-rotate-3{rotate:-3deg}.-rotate-6{rotate:-6deg}.-rotate-12{rotate:-12deg}.-rotate-45{rotate:-45deg}.-rotate-90{rotate:-90deg}.-rotate-180{rotate:-180deg}.rotate-0{rotate:0deg}.rotate-1{rotate:1deg}.rotate-2{rotate:2deg}.rotate-3{rotate:3deg}.rotate-6{rotate:6deg}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.-skew-x-1{--tw-skew-x:skewX( -1deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-2{--tw-skew-x:skewX( -2deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-3{--tw-skew-x:skewX( -3deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-6{--tw-skew-x:skewX( -6deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-x-12{--tw-skew-x:skewX( -12deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-0{--tw-skew-x:skewX(0deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-1{--tw-skew-x:skewX(1deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-2{--tw-skew-x:skewX(2deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-3{--tw-skew-x:skewX(3deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-6{--tw-skew-x:skewX(6deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-x-12{--tw-skew-x:skewX(12deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-1{--tw-skew-y:skewY( -1deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-2{--tw-skew-y:skewY( -2deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-3{--tw-skew-y:skewY( -3deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-6{--tw-skew-y:skewY( -6deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.-skew-y-12{--tw-skew-y:skewY( -12deg );transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-0{--tw-skew-y:skewY(0deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-1{--tw-skew-y:skewY(1deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-2{--tw-skew-y:skewY(2deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-3{--tw-skew-y:skewY(3deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-6{--tw-skew-y:skewY(6deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.skew-y-12{--tw-skew-y:skewY(12deg);transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform,.transform-cpu{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-none{transform:none}.animate-\[loading_1\.5s_ease-in-out_infinite\]{animation:1.5s ease-in-out infinite loading}.animate-bounce{animation:var(--animate-bounce)}.animate-none{animation:none}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-alias{cursor:alias}.cursor-all-scroll{cursor:all-scroll}.cursor-auto{cursor:auto}.cursor-cell{cursor:cell}.cursor-col-resize{cursor:col-resize}.cursor-context-menu{cursor:context-menu}.cursor-copy{cursor:copy}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-e-resize{cursor:e-resize}.cursor-ew-resize{cursor:ew-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-help{cursor:help}.cursor-move{cursor:move}.cursor-n-resize{cursor:n-resize}.cursor-ne-resize{cursor:ne-resize}.cursor-nesw-resize{cursor:nesw-resize}.cursor-no-drop{cursor:no-drop}.cursor-none{cursor:none}.cursor-not-allowed{cursor:not-allowed}.cursor-ns-resize{cursor:ns-resize}.cursor-nw-resize{cursor:nw-resize}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.cursor-progress{cursor:progress}.cursor-row-resize{cursor:row-resize}.cursor-s-resize{cursor:s-resize}.cursor-se-resize{cursor:se-resize}.cursor-sw-resize{cursor:sw-resize}.cursor-text{cursor:text}.cursor-vertical-text{cursor:vertical-text}.cursor-w-resize{cursor:w-resize}.cursor-wait{cursor:wait}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.touch-pan-left{--tw-pan-x:pan-left;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-right{--tw-pan-x:pan-right;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-x{--tw-pan-x:pan-x;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-down{--tw-pan-y:pan-down;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-up{--tw-pan-y:pan-up;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pan-y{--tw-pan-y:pan-y;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.touch-auto{touch-action:auto}.touch-manipulation{touch-action:manipulation}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.resize-x{resize:horizontal}.resize-y{resize:vertical}.snap-both{scroll-snap-type:both var(--tw-scroll-snap-strictness)}.snap-none{scroll-snap-type:none}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-align-none{scroll-snap-align:none}.snap-center{scroll-snap-align:center}.snap-end{scroll-snap-align:end}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.snap-normal{scroll-snap-stop:normal}.scroll-mt-44{scroll-margin-top:calc(var(--spacing) * 44)}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.appearance-auto{appearance:auto}.appearance-none{appearance:none}.columns-1{columns:1}.columns-2{columns:2}.columns-2xl{columns:var(--container-2xl)}.columns-2xs{columns:var(--container-2xs)}.columns-3{columns:3}.columns-3xl{columns:var(--container-3xl)}.columns-3xs{columns:var(--container-3xs)}.columns-4{columns:4}.columns-4xl{columns:var(--container-4xl)}.columns-5{columns:5}.columns-5xl{columns:var(--container-5xl)}.columns-6{columns:6}.columns-6xl{columns:var(--container-6xl)}.columns-7{columns:7}.columns-7xl{columns:var(--container-7xl)}.columns-8{columns:8}.columns-9{columns:9}.columns-10{columns:10}.columns-11{columns:11}.columns-12{columns:12}.columns-auto{columns:auto}.columns-lg{columns:var(--container-lg)}.columns-md{columns:var(--container-md)}.columns-sm{columns:var(--container-sm)}.columns-xl{columns:var(--container-xl)}.columns-xs{columns:var(--container-xs)}.break-before-all{break-before:all}.break-before-auto{break-before:auto}.break-before-avoid{break-before:avoid}.break-before-avoid-page{break-before:avoid-page}.break-before-column{break-before:column}.break-before-left{break-before:left}.break-before-page{break-before:page}.break-before-right{break-before:right}.break-inside-auto{break-inside:auto}.break-inside-avoid{break-inside:avoid}.break-inside-avoid-column{break-inside:avoid-column}.break-inside-avoid-page{break-inside:avoid-page}.break-after-all{break-after:all}.break-after-auto{break-after:auto}.break-after-avoid{break-after:avoid}.break-after-avoid-page{break-after:avoid-page}.break-after-column{break-after:column}.break-after-left{break-after:left}.break-after-page{break-after:page}.break-after-right{break-after:right}.auto-cols-auto{grid-auto-columns:auto}.auto-cols-fr{grid-auto-columns:minmax(0,1fr)}.auto-cols-max{grid-auto-columns:max-content}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-col{grid-auto-flow:column}.grid-flow-col-dense{grid-auto-flow:column dense}.grid-flow-dense{grid-auto-flow:dense}.grid-flow-row{grid-auto-flow:row}.grid-flow-row-dense{grid-auto-flow:dense}.auto-rows-auto{grid-auto-rows:auto}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.auto-rows-max{grid-auto-rows:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.\!flex-col{flex-direction:column!important}.\!flex-row{flex-direction:row!important}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.\!flex-nowrap{flex-wrap:nowrap!important}.\!flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-around{place-content:space-around}.place-content-baseline{place-content:baseline start}.place-content-between{place-content:space-between}.place-content-center{place-content:center}.place-content-end{place-content:end}.place-content-evenly{place-content:space-evenly}.place-content-start{place-content:start}.place-content-stretch{place-content:stretch}.place-items-baseline{place-items:baseline}.place-items-center{place-items:center}.place-items-end{place-items:end}.place-items-start{place-items:start}.place-items-stretch{place-items:stretch stretch}.content-around{align-content:space-around}.content-between{align-content:space-between}.content-center{align-content:center}.content-end{align-content:flex-end}.content-evenly{align-content:space-evenly}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-evenly{justify-content:space-evenly}.justify-start{justify-content:flex-start}.\!gap-0{gap:calc(var(--spacing) * 0)!important}.\!gap-1{gap:calc(var(--spacing) * 1)!important}.\!gap-2{gap:calc(var(--spacing) * 2)!important}.\!gap-3{gap:calc(var(--spacing) * 3)!important}.\!gap-4{gap:calc(var(--spacing) * 4)!important}.\!gap-5{gap:calc(var(--spacing) * 5)!important}.\!gap-6{gap:calc(var(--spacing) * 6)!important}.\!gap-8{gap:calc(var(--spacing) * 8)!important}.\!gap-10{gap:calc(var(--spacing) * 10)!important}.\!gap-12{gap:calc(var(--spacing) * 12)!important}.\!gap-16{gap:calc(var(--spacing) * 16)!important}.gap-0{gap:calc(var(--spacing) * 0)}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-10{gap:calc(var(--spacing) * 10)}.gap-12{gap:calc(var(--spacing) * 12)}.gap-16{gap:calc(var(--spacing) * 16)}:where(.-space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * -1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-y-reverse)))}:where(.-space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * -2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-0{column-gap:calc(var(--spacing) * 0)}.gap-x-1{column-gap:calc(var(--spacing) * 1)}.gap-x-2{column-gap:calc(var(--spacing) * 2)}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-5{column-gap:calc(var(--spacing) * 5)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-x-8{column-gap:calc(var(--spacing) * 8)}.gap-x-10{column-gap:calc(var(--spacing) * 10)}.gap-x-12{column-gap:calc(var(--spacing) * 12)}:where(.-space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * -1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * -1) * calc(1 - var(--tw-space-x-reverse)))}:where(.-space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * -2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * -2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 0) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 5) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-0{row-gap:calc(var(--spacing) * 0)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.gap-y-3{row-gap:calc(var(--spacing) * 3)}.gap-y-4{row-gap:calc(var(--spacing) * 4)}.gap-y-5{row-gap:calc(var(--spacing) * 5)}.gap-y-6{row-gap:calc(var(--spacing) * 6)}.gap-y-8{row-gap:calc(var(--spacing) * 8)}.gap-y-10{row-gap:calc(var(--spacing) * 10)}.gap-y-12{row-gap:calc(var(--spacing) * 12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-0>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(0px * var(--tw-divide-x-reverse));border-inline-end-width:calc(0px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-2>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(2px * var(--tw-divide-x-reverse));border-inline-end-width:calc(2px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-4>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(4px * var(--tw-divide-x-reverse));border-inline-end-width:calc(4px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-8>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(8px * var(--tw-divide-x-reverse));border-inline-end-width:calc(8px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-0>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(0px * var(--tw-divide-y-reverse));border-bottom-width:calc(0px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-2>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(2px * var(--tw-divide-y-reverse));border-bottom-width:calc(2px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-4>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(4px * var(--tw-divide-y-reverse));border-bottom-width:calc(4px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-8>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(8px * var(--tw-divide-y-reverse));border-bottom-width:calc(8px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}:where(.divide-dashed>:not(:last-child)){--tw-border-style:dashed;border-style:dashed}:where(.divide-dotted>:not(:last-child)){--tw-border-style:dotted;border-style:dotted}:where(.divide-double>:not(:last-child)){--tw-border-style:double;border-style:double}:where(.divide-none>:not(:last-child)){--tw-border-style:none;border-style:none}:where(.divide-solid>:not(:last-child)){--tw-border-style:solid;border-style:solid}:where(.divide-black>:not(:last-child)){border-color:var(--color-black)}:where(.divide-current>:not(:last-child)){border-color:currentColor}:where(.divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.divide-inherit>:not(:last-child)){border-color:inherit}:where(.divide-purple-100>:not(:last-child)){border-color:var(--color-purple-100)}:where(.divide-slate-50>:not(:last-child)){border-color:var(--color-slate-50)}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}:where(.divide-slate-300>:not(:last-child)){border-color:var(--color-slate-300)}:where(.divide-slate-400>:not(:last-child)){border-color:var(--color-slate-400)}:where(.divide-slate-500>:not(:last-child)){border-color:var(--color-slate-500)}:where(.divide-slate-600>:not(:last-child)){border-color:var(--color-slate-600)}:where(.divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}:where(.divide-slate-800>:not(:last-child)){border-color:var(--color-slate-800)}:where(.divide-slate-900>:not(:last-child)){border-color:var(--color-slate-900)}:where(.divide-transparent>:not(:last-child)){border-color:#0000}:where(.divide-white>:not(:last-child)){border-color:var(--color-white)}.place-self-auto{place-self:auto}.place-self-center{place-self:center}.place-self-end{place-self:end}.place-self-start{place-self:start}.place-self-stretch{place-self:stretch stretch}.self-auto{align-self:auto}.self-baseline{align-self:baseline}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-clip{overflow-x:clip}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-visible{overflow-x:visible}.overflow-y-auto{overflow-y:auto}.overflow-y-clip{overflow-y:clip}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-visible{overflow-y:visible}.overscroll-auto{overscroll-behavior:auto}.overscroll-contain{overscroll-behavior:contain}.overscroll-none{overscroll-behavior:none}.overscroll-x-auto{overscroll-behavior-x:auto}.overscroll-x-contain{overscroll-behavior-x:contain}.overscroll-x-none{overscroll-behavior-x:none}.overscroll-y-auto{overscroll-behavior-y:auto}.overscroll-y-contain{overscroll-behavior-y:contain}.overscroll-y-none{overscroll-behavior-y:none}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t-sm{border-top-left-radius:var(--radius-sm);border-top-right-radius:var(--radius-sm)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-2xl{border-top-left-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-l-3xl{border-top-left-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-l-full{border-top-left-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-sm{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-l-xl{border-top-left-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-2xl{border-top-left-radius:var(--radius-2xl)}.rounded-tl-3xl{border-top-left-radius:var(--radius-3xl)}.rounded-tl-full{border-top-left-radius:3.40282e38px}.rounded-tl-lg{border-top-left-radius:var(--radius-lg)}.rounded-tl-md{border-top-left-radius:var(--radius-md)}.rounded-tl-none{border-top-left-radius:0}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-tl-xl{border-top-left-radius:var(--radius-xl)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-2xl{border-top-right-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl)}.rounded-r-3xl{border-top-right-radius:var(--radius-3xl);border-bottom-right-radius:var(--radius-3xl)}.rounded-r-full{border-top-right-radius:3.40282e38px;border-bottom-right-radius:3.40282e38px}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-sm{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}.rounded-r-xl{border-top-right-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl)}.rounded-tr{border-top-right-radius:.25rem}.rounded-tr-2xl{border-top-right-radius:var(--radius-2xl)}.rounded-tr-3xl{border-top-right-radius:var(--radius-3xl)}.rounded-tr-full{border-top-right-radius:3.40282e38px}.rounded-tr-lg{border-top-right-radius:var(--radius-lg)}.rounded-tr-md{border-top-right-radius:var(--radius-md)}.rounded-tr-none{border-top-right-radius:0}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-b-2xl{border-bottom-right-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-b-3xl{border-bottom-right-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-b-full{border-bottom-right-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-b-md{border-bottom-right-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}.rounded-b-sm{border-bottom-right-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-2xl{border-bottom-right-radius:var(--radius-2xl)}.rounded-br-3xl{border-bottom-right-radius:var(--radius-3xl)}.rounded-br-full{border-bottom-right-radius:3.40282e38px}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-br-none{border-bottom-right-radius:0}.rounded-br-sm{border-bottom-right-radius:var(--radius-sm)}.rounded-br-xl{border-bottom-right-radius:var(--radius-xl)}.rounded-bl{border-bottom-left-radius:.25rem}.rounded-bl-2xl{border-bottom-left-radius:var(--radius-2xl)}.rounded-bl-3xl{border-bottom-left-radius:var(--radius-3xl)}.rounded-bl-full{border-bottom-left-radius:3.40282e38px}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.rounded-bl-none{border-bottom-left-radius:0}.rounded-bl-sm{border-bottom-left-radius:var(--radius-sm)}.rounded-bl-xl{border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-3{border-style:var(--tw-border-style);border-width:3px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-8{border-style:var(--tw-border-style);border-width:8px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.border-x-2{border-inline-style:var(--tw-border-style);border-inline-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-y-0{border-block-style:var(--tw-border-style);border-block-width:0}.border-y-2{border-block-style:var(--tw-border-style);border-block-width:2px}.\!border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-2{border-right-style:var(--tw-border-style);border-right-width:2px}.border-r-4{border-right-style:var(--tw-border-style);border-right-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-4{border-bottom-style:var(--tw-border-style);border-bottom-width:4px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-hidden{--tw-border-style:hidden;border-style:hidden}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-amber-50{border-color:var(--color-amber-50)}.border-amber-100{border-color:var(--color-amber-100)}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-amber-400{border-color:var(--color-amber-400)}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-600{border-color:var(--color-amber-600)}.border-amber-700{border-color:var(--color-amber-700)}.border-amber-800{border-color:var(--color-amber-800)}.border-amber-900{border-color:var(--color-amber-900)}.border-amber-950{border-color:var(--color-amber-950)}.border-black{border-color:var(--color-black)}.border-blue-50{border-color:var(--color-blue-50)}.border-blue-100{border-color:var(--color-blue-100)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-blue-800{border-color:var(--color-blue-800)}.border-blue-900{border-color:var(--color-blue-900)}.border-blue-950{border-color:var(--color-blue-950)}.border-current{border-color:currentColor}.border-cyan-50{border-color:var(--color-cyan-50)}.border-cyan-100{border-color:var(--color-cyan-100)}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-300{border-color:var(--color-cyan-300)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-cyan-500{border-color:var(--color-cyan-500)}.border-cyan-600{border-color:var(--color-cyan-600)}.border-cyan-700{border-color:var(--color-cyan-700)}.border-cyan-800{border-color:var(--color-cyan-800)}.border-cyan-900{border-color:var(--color-cyan-900)}.border-cyan-950{border-color:var(--color-cyan-950)}.border-emerald-50{border-color:var(--color-emerald-50)}.border-emerald-100{border-color:var(--color-emerald-100)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-emerald-400{border-color:var(--color-emerald-400)}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-600{border-color:var(--color-emerald-600)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-emerald-800{border-color:var(--color-emerald-800)}.border-emerald-900{border-color:var(--color-emerald-900)}.border-emerald-950{border-color:var(--color-emerald-950)}.border-fuchsia-50{border-color:var(--color-fuchsia-50)}.border-fuchsia-100{border-color:var(--color-fuchsia-100)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-fuchsia-300{border-color:var(--color-fuchsia-300)}.border-fuchsia-400{border-color:var(--color-fuchsia-400)}.border-fuchsia-500{border-color:var(--color-fuchsia-500)}.border-fuchsia-600{border-color:var(--color-fuchsia-600)}.border-fuchsia-700{border-color:var(--color-fuchsia-700)}.border-fuchsia-800{border-color:var(--color-fuchsia-800)}.border-fuchsia-900{border-color:var(--color-fuchsia-900)}.border-fuchsia-950{border-color:var(--color-fuchsia-950)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-500{border-color:var(--color-gray-500)}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-900{border-color:var(--color-gray-900)}.border-gray-950{border-color:var(--color-gray-950)}.border-green-50{border-color:var(--color-green-50)}.border-green-100{border-color:var(--color-green-100)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-green-400{border-color:var(--color-green-400)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-green-700{border-color:var(--color-green-700)}.border-green-800{border-color:var(--color-green-800)}.border-green-900{border-color:var(--color-green-900)}.border-green-950{border-color:var(--color-green-950)}.border-indigo-50{border-color:var(--color-indigo-50)}.border-indigo-100{border-color:var(--color-indigo-100)}.border-indigo-200{border-color:var(--color-indigo-200)}.border-indigo-300{border-color:var(--color-indigo-300)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-indigo-700{border-color:var(--color-indigo-700)}.border-indigo-800{border-color:var(--color-indigo-800)}.border-indigo-900{border-color:var(--color-indigo-900)}.border-indigo-950{border-color:var(--color-indigo-950)}.border-inherit{border-color:inherit}.border-lime-50{border-color:var(--color-lime-50)}.border-lime-100{border-color:var(--color-lime-100)}.border-lime-200{border-color:var(--color-lime-200)}.border-lime-300{border-color:var(--color-lime-300)}.border-lime-400{border-color:var(--color-lime-400)}.border-lime-500{border-color:var(--color-lime-500)}.border-lime-600{border-color:var(--color-lime-600)}.border-lime-700{border-color:var(--color-lime-700)}.border-lime-800{border-color:var(--color-lime-800)}.border-lime-900{border-color:var(--color-lime-900)}.border-lime-950{border-color:var(--color-lime-950)}.border-neutral-50{border-color:var(--color-neutral-50)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-400{border-color:var(--color-neutral-400)}.border-neutral-500{border-color:var(--color-neutral-500)}.border-neutral-600{border-color:var(--color-neutral-600)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-neutral-950{border-color:var(--color-neutral-950)}.border-orange-50{border-color:var(--color-orange-50)}.border-orange-100{border-color:var(--color-orange-100)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-300{border-color:var(--color-orange-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-orange-500{border-color:var(--color-orange-500)}.border-orange-600{border-color:var(--color-orange-600)}.border-orange-700{border-color:var(--color-orange-700)}.border-orange-800{border-color:var(--color-orange-800)}.border-orange-900{border-color:var(--color-orange-900)}.border-orange-950{border-color:var(--color-orange-950)}.border-pink-50{border-color:var(--color-pink-50)}.border-pink-100{border-color:var(--color-pink-100)}.border-pink-200{border-color:var(--color-pink-200)}.border-pink-300{border-color:var(--color-pink-300)}.border-pink-400{border-color:var(--color-pink-400)}.border-pink-500{border-color:var(--color-pink-500)}.border-pink-600{border-color:var(--color-pink-600)}.border-pink-700{border-color:var(--color-pink-700)}.border-pink-800{border-color:var(--color-pink-800)}.border-pink-900{border-color:var(--color-pink-900)}.border-pink-950{border-color:var(--color-pink-950)}.border-purple-50{border-color:var(--color-purple-50)}.border-purple-100{border-color:var(--color-purple-100)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-400{border-color:var(--color-purple-400)}.border-purple-500{border-color:var(--color-purple-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-purple-700{border-color:var(--color-purple-700)}.border-purple-800{border-color:var(--color-purple-800)}.border-purple-900{border-color:var(--color-purple-900)}.border-purple-950{border-color:var(--color-purple-950)}.border-red-50{border-color:var(--color-red-50)}.border-red-100{border-color:var(--color-red-100)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-red-400{border-color:var(--color-red-400)}.border-red-500{border-color:var(--color-red-500)}.border-red-600{border-color:var(--color-red-600)}.border-red-700{border-color:var(--color-red-700)}.border-red-800{border-color:var(--color-red-800)}.border-red-900{border-color:var(--color-red-900)}.border-red-950{border-color:var(--color-red-950)}.border-rose-50{border-color:var(--color-rose-50)}.border-rose-100{border-color:var(--color-rose-100)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-rose-400{border-color:var(--color-rose-400)}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-600{border-color:var(--color-rose-600)}.border-rose-700{border-color:var(--color-rose-700)}.border-rose-800{border-color:var(--color-rose-800)}.border-rose-900{border-color:var(--color-rose-900)}.border-rose-950{border-color:var(--color-rose-950)}.border-sky-50{border-color:var(--color-sky-50)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-300{border-color:var(--color-sky-300)}.border-sky-400{border-color:var(--color-sky-400)}.border-sky-500{border-color:var(--color-sky-500)}.border-sky-600{border-color:var(--color-sky-600)}.border-sky-700{border-color:var(--color-sky-700)}.border-sky-800{border-color:var(--color-sky-800)}.border-sky-900{border-color:var(--color-sky-900)}.border-sky-950{border-color:var(--color-sky-950)}.border-slate-50{border-color:var(--color-slate-50)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400{border-color:var(--color-slate-400)}.border-slate-500{border-color:var(--color-slate-500)}.border-slate-600{border-color:var(--color-slate-600)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-900{border-color:var(--color-slate-900)}.border-slate-950{border-color:var(--color-slate-950)}.border-stone-50{border-color:var(--color-stone-50)}.border-stone-100{border-color:var(--color-stone-100)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-stone-400{border-color:var(--color-stone-400)}.border-stone-500{border-color:var(--color-stone-500)}.border-stone-600{border-color:var(--color-stone-600)}.border-stone-700{border-color:var(--color-stone-700)}.border-stone-800{border-color:var(--color-stone-800)}.border-stone-900{border-color:var(--color-stone-900)}.border-stone-950{border-color:var(--color-stone-950)}.border-teal-50{border-color:var(--color-teal-50)}.border-teal-100{border-color:var(--color-teal-100)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-400{border-color:var(--color-teal-400)}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-teal-800{border-color:var(--color-teal-800)}.border-teal-900{border-color:var(--color-teal-900)}.border-teal-950{border-color:var(--color-teal-950)}.border-transparent{border-color:#0000}.border-violet-50{border-color:var(--color-violet-50)}.border-violet-100{border-color:var(--color-violet-100)}.border-violet-200{border-color:var(--color-violet-200)}.border-violet-300{border-color:var(--color-violet-300)}.border-violet-400{border-color:var(--color-violet-400)}.border-violet-500{border-color:var(--color-violet-500)}.border-violet-600{border-color:var(--color-violet-600)}.border-violet-700{border-color:var(--color-violet-700)}.border-violet-800{border-color:var(--color-violet-800)}.border-violet-900{border-color:var(--color-violet-900)}.border-violet-950{border-color:var(--color-violet-950)}.border-white{border-color:var(--color-white)}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white) 30%,transparent)}}.border-yellow-50{border-color:var(--color-yellow-50)}.border-yellow-100{border-color:var(--color-yellow-100)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-400{border-color:var(--color-yellow-400)}.border-yellow-500{border-color:var(--color-yellow-500)}.border-yellow-600{border-color:var(--color-yellow-600)}.border-yellow-700{border-color:var(--color-yellow-700)}.border-yellow-800{border-color:var(--color-yellow-800)}.border-yellow-900{border-color:var(--color-yellow-900)}.border-yellow-950{border-color:var(--color-yellow-950)}.border-zinc-50{border-color:var(--color-zinc-50)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-400{border-color:var(--color-zinc-400)}.border-zinc-500{border-color:var(--color-zinc-500)}.border-zinc-600{border-color:var(--color-zinc-600)}.border-zinc-700{border-color:var(--color-zinc-700)}.border-zinc-800{border-color:var(--color-zinc-800)}.border-zinc-900{border-color:var(--color-zinc-900)}.border-zinc-950{border-color:var(--color-zinc-950)}.border-t-current{border-top-color:currentColor}.border-t-white{border-top-color:var(--color-white)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-300{background-color:var(--color-amber-300)}.bg-amber-400{background-color:var(--color-amber-400)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-amber-800{background-color:var(--color-amber-800)}.bg-amber-900{background-color:var(--color-amber-900)}.bg-amber-950{background-color:var(--color-amber-950)}.bg-black{background-color:var(--color-black)}.bg-black\/0{background-color:#0000}@supports (color:color-mix(in lab,red,red)){.bg-black\/0{background-color:color-mix(in oklab,var(--color-black) 0%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/70{background-color:#eff6ffb3}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/70{background-color:color-mix(in oklab,var(--color-blue-50) 70%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-300{background-color:var(--color-blue-300)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-blue-800{background-color:var(--color-blue-800)}.bg-blue-900{background-color:var(--color-blue-900)}.bg-blue-950{background-color:var(--color-blue-950)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-200{background-color:var(--color-cyan-200)}.bg-cyan-300{background-color:var(--color-cyan-300)}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-cyan-600{background-color:var(--color-cyan-600)}.bg-cyan-700{background-color:var(--color-cyan-700)}.bg-cyan-800{background-color:var(--color-cyan-800)}.bg-cyan-900{background-color:var(--color-cyan-900)}.bg-cyan-950{background-color:var(--color-cyan-950)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-300{background-color:var(--color-emerald-300)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-emerald-900{background-color:var(--color-emerald-900)}.bg-emerald-950{background-color:var(--color-emerald-950)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-fuchsia-100{background-color:var(--color-fuchsia-100)}.bg-fuchsia-200{background-color:var(--color-fuchsia-200)}.bg-fuchsia-300{background-color:var(--color-fuchsia-300)}.bg-fuchsia-400{background-color:var(--color-fuchsia-400)}.bg-fuchsia-500{background-color:var(--color-fuchsia-500)}.bg-fuchsia-600{background-color:var(--color-fuchsia-600)}.bg-fuchsia-700{background-color:var(--color-fuchsia-700)}.bg-fuchsia-800{background-color:var(--color-fuchsia-800)}.bg-fuchsia-900{background-color:var(--color-fuchsia-900)}.bg-fuchsia-950{background-color:var(--color-fuchsia-950)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-100\/60{background-color:#f3f4f699}@supports (color:color-mix(in lab,red,red)){.bg-gray-100\/60{background-color:color-mix(in oklab,var(--color-gray-100) 60%,transparent)}}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-green-300{background-color:var(--color-green-300)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-800{background-color:var(--color-green-800)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-950{background-color:var(--color-green-950)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-200{background-color:var(--color-indigo-200)}.bg-indigo-300{background-color:var(--color-indigo-300)}.bg-indigo-400{background-color:var(--color-indigo-400)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-indigo-700{background-color:var(--color-indigo-700)}.bg-indigo-800{background-color:var(--color-indigo-800)}.bg-indigo-900{background-color:var(--color-indigo-900)}.bg-indigo-950{background-color:var(--color-indigo-950)}.bg-inherit{background-color:inherit}.bg-lime-50{background-color:var(--color-lime-50)}.bg-lime-100{background-color:var(--color-lime-100)}.bg-lime-200{background-color:var(--color-lime-200)}.bg-lime-300{background-color:var(--color-lime-300)}.bg-lime-400{background-color:var(--color-lime-400)}.bg-lime-500{background-color:var(--color-lime-500)}.bg-lime-600{background-color:var(--color-lime-600)}.bg-lime-700{background-color:var(--color-lime-700)}.bg-lime-800{background-color:var(--color-lime-800)}.bg-lime-900{background-color:var(--color-lime-900)}.bg-lime-950{background-color:var(--color-lime-950)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-orange-300{background-color:var(--color-orange-300)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-orange-700{background-color:var(--color-orange-700)}.bg-orange-800{background-color:var(--color-orange-800)}.bg-orange-900{background-color:var(--color-orange-900)}.bg-orange-950{background-color:var(--color-orange-950)}.bg-pink-50{background-color:var(--color-pink-50)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-200{background-color:var(--color-pink-200)}.bg-pink-300{background-color:var(--color-pink-300)}.bg-pink-400{background-color:var(--color-pink-400)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-pink-600{background-color:var(--color-pink-600)}.bg-pink-700{background-color:var(--color-pink-700)}.bg-pink-800{background-color:var(--color-pink-800)}.bg-pink-900{background-color:var(--color-pink-900)}.bg-pink-950{background-color:var(--color-pink-950)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-200{background-color:var(--color-purple-200)}.bg-purple-300{background-color:var(--color-purple-300)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-purple-500{background-color:var(--color-purple-500)}.bg-purple-600{background-color:var(--color-purple-600)}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-800{background-color:var(--color-purple-800)}.bg-purple-900{background-color:var(--color-purple-900)}.bg-purple-950{background-color:var(--color-purple-950)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-300{background-color:var(--color-red-300)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/50{background-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/50{background-color:color-mix(in oklab,var(--color-red-500) 50%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-950{background-color:var(--color-red-950)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-100{background-color:var(--color-rose-100)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-rose-700{background-color:var(--color-rose-700)}.bg-rose-800{background-color:var(--color-rose-800)}.bg-rose-900{background-color:var(--color-rose-900)}.bg-rose-950{background-color:var(--color-rose-950)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-100{background-color:var(--color-sky-100)}.bg-sky-200{background-color:var(--color-sky-200)}.bg-sky-300{background-color:var(--color-sky-300)}.bg-sky-400{background-color:var(--color-sky-400)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-sky-950{background-color:var(--color-sky-950)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-600{background-color:var(--color-slate-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-stone-200{background-color:var(--color-stone-200)}.bg-stone-300{background-color:var(--color-stone-300)}.bg-stone-400{background-color:var(--color-stone-400)}.bg-stone-500{background-color:var(--color-stone-500)}.bg-stone-600{background-color:var(--color-stone-600)}.bg-stone-700{background-color:var(--color-stone-700)}.bg-stone-800{background-color:var(--color-stone-800)}.bg-stone-900{background-color:var(--color-stone-900)}.bg-stone-950{background-color:var(--color-stone-950)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-100{background-color:var(--color-teal-100)}.bg-teal-200{background-color:var(--color-teal-200)}.bg-teal-300{background-color:var(--color-teal-300)}.bg-teal-400{background-color:var(--color-teal-400)}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-teal-700{background-color:var(--color-teal-700)}.bg-teal-800{background-color:var(--color-teal-800)}.bg-teal-900{background-color:var(--color-teal-900)}.bg-teal-950{background-color:var(--color-teal-950)}.bg-transparent{background-color:#0000}.bg-violet-50{background-color:var(--color-violet-50)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-200{background-color:var(--color-violet-200)}.bg-violet-300{background-color:var(--color-violet-300)}.bg-violet-400{background-color:var(--color-violet-400)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-violet-600{background-color:var(--color-violet-600)}.bg-violet-700{background-color:var(--color-violet-700)}.bg-violet-800{background-color:var(--color-violet-800)}.bg-violet-900{background-color:var(--color-violet-900)}.bg-violet-950{background-color:var(--color-violet-950)}.bg-white{background-color:var(--color-white)}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white) 90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-200{background-color:var(--color-yellow-200)}.bg-yellow-300{background-color:var(--color-yellow-300)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-700{background-color:var(--color-yellow-700)}.bg-yellow-800{background-color:var(--color-yellow-800)}.bg-yellow-900{background-color:var(--color-yellow-900)}.bg-yellow-950{background-color:var(--color-yellow-950)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-200{background-color:var(--color-zinc-200)}.bg-zinc-300{background-color:var(--color-zinc-300)}.bg-zinc-400{background-color:var(--color-zinc-400)}.bg-zinc-500{background-color:var(--color-zinc-500)}.bg-zinc-600{background-color:var(--color-zinc-600)}.bg-zinc-700{background-color:var(--color-zinc-700)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-950{background-color:var(--color-zinc-950)}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-bl{--tw-gradient-position:to bottom left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tl{--tw-gradient-position:to top left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tr{--tw-gradient-position:to top right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black{--tw-gradient-from:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-50{--tw-gradient-from:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-100{--tw-gradient-from:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-200{--tw-gradient-from:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-300{--tw-gradient-from:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-400{--tw-gradient-from:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-600{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-700{--tw-gradient-from:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-800{--tw-gradient-from:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-current{--tw-gradient-from:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-50{--tw-gradient-from:var(--color-green-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-100{--tw-gradient-from:var(--color-green-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-200{--tw-gradient-from:var(--color-green-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-300{--tw-gradient-from:var(--color-green-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-400{--tw-gradient-from:var(--color-green-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-600{--tw-gradient-from:var(--color-green-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-700{--tw-gradient-from:var(--color-green-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-800{--tw-gradient-from:var(--color-green-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-green-900{--tw-gradient-from:var(--color-green-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-50{--tw-gradient-from:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-100{--tw-gradient-from:var(--color-indigo-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-200{--tw-gradient-from:var(--color-indigo-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-300{--tw-gradient-from:var(--color-indigo-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-400{--tw-gradient-from:var(--color-indigo-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-500{--tw-gradient-from:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-600{--tw-gradient-from:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-700{--tw-gradient-from:var(--color-indigo-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-800{--tw-gradient-from:var(--color-indigo-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-indigo-900{--tw-gradient-from:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-inherit{--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-50{--tw-gradient-from:var(--color-orange-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-100{--tw-gradient-from:var(--color-orange-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-200{--tw-gradient-from:var(--color-orange-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-300{--tw-gradient-from:var(--color-orange-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-400{--tw-gradient-from:var(--color-orange-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-500{--tw-gradient-from:var(--color-orange-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-600{--tw-gradient-from:var(--color-orange-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-700{--tw-gradient-from:var(--color-orange-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-800{--tw-gradient-from:var(--color-orange-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-orange-900{--tw-gradient-from:var(--color-orange-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-50{--tw-gradient-from:var(--color-pink-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-100{--tw-gradient-from:var(--color-pink-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-200{--tw-gradient-from:var(--color-pink-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-300{--tw-gradient-from:var(--color-pink-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-400{--tw-gradient-from:var(--color-pink-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-500{--tw-gradient-from:var(--color-pink-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-600{--tw-gradient-from:var(--color-pink-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-700{--tw-gradient-from:var(--color-pink-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-800{--tw-gradient-from:var(--color-pink-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-pink-900{--tw-gradient-from:var(--color-pink-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-50{--tw-gradient-from:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-100{--tw-gradient-from:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-200{--tw-gradient-from:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-300{--tw-gradient-from:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-400{--tw-gradient-from:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-500{--tw-gradient-from:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-600{--tw-gradient-from:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-700{--tw-gradient-from:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-800{--tw-gradient-from:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-purple-900{--tw-gradient-from:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-50{--tw-gradient-from:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-100{--tw-gradient-from:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-200{--tw-gradient-from:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-300{--tw-gradient-from:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-400{--tw-gradient-from:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-600{--tw-gradient-from:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-700{--tw-gradient-from:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-800{--tw-gradient-from:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-red-900{--tw-gradient-from:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-50{--tw-gradient-from:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-100{--tw-gradient-from:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-200{--tw-gradient-from:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-300{--tw-gradient-from:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-400{--tw-gradient-from:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-500{--tw-gradient-from:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-600{--tw-gradient-from:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-700{--tw-gradient-from:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-800{--tw-gradient-from:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-slate-900{--tw-gradient-from:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-50{--tw-gradient-from:var(--color-yellow-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-100{--tw-gradient-from:var(--color-yellow-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-200{--tw-gradient-from:var(--color-yellow-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-300{--tw-gradient-from:var(--color-yellow-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-500{--tw-gradient-from:var(--color-yellow-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-600{--tw-gradient-from:var(--color-yellow-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-700{--tw-gradient-from:var(--color-yellow-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-800{--tw-gradient-from:var(--color-yellow-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.from-yellow-900{--tw-gradient-from:var(--color-yellow-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.via-black{--tw-gradient-via:var(--color-black);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-50{--tw-gradient-via:var(--color-blue-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-100{--tw-gradient-via:var(--color-blue-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-200{--tw-gradient-via:var(--color-blue-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-300{--tw-gradient-via:var(--color-blue-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-400{--tw-gradient-via:var(--color-blue-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-500{--tw-gradient-via:var(--color-blue-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-600{--tw-gradient-via:var(--color-blue-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-700{--tw-gradient-via:var(--color-blue-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-800{--tw-gradient-via:var(--color-blue-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-900{--tw-gradient-via:var(--color-blue-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-current{--tw-gradient-via:currentcolor;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-50{--tw-gradient-via:var(--color-gray-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-100{--tw-gradient-via:var(--color-gray-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-200{--tw-gradient-via:var(--color-gray-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-300{--tw-gradient-via:var(--color-gray-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-400{--tw-gradient-via:var(--color-gray-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-500{--tw-gradient-via:var(--color-gray-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-600{--tw-gradient-via:var(--color-gray-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-700{--tw-gradient-via:var(--color-gray-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-800{--tw-gradient-via:var(--color-gray-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-900{--tw-gradient-via:var(--color-gray-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-inherit{--tw-gradient-via:inherit;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-50{--tw-gradient-via:var(--color-slate-50);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-100{--tw-gradient-via:var(--color-slate-100);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-200{--tw-gradient-via:var(--color-slate-200);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-300{--tw-gradient-via:var(--color-slate-300);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-400{--tw-gradient-via:var(--color-slate-400);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-500{--tw-gradient-via:var(--color-slate-500);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-600{--tw-gradient-via:var(--color-slate-600);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-700{--tw-gradient-via:var(--color-slate-700);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-800{--tw-gradient-via:var(--color-slate-800);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-900{--tw-gradient-via:var(--color-slate-900);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-black{--tw-gradient-to:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-50{--tw-gradient-to:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-100{--tw-gradient-to:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-200{--tw-gradient-to:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-300{--tw-gradient-to:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-400{--tw-gradient-to:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-700{--tw-gradient-to:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-800{--tw-gradient-to:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-blue-900{--tw-gradient-to:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-current{--tw-gradient-to:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-indigo-50{--tw-gradient-to:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-inherit{--tw-gradient-to:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-50{--tw-gradient-to:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-100{--tw-gradient-to:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-200{--tw-gradient-to:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-300{--tw-gradient-to:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-400{--tw-gradient-to:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-500{--tw-gradient-to:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-600{--tw-gradient-to:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-700{--tw-gradient-to:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-800{--tw-gradient-to:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-purple-900{--tw-gradient-to:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-50{--tw-gradient-to:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-100{--tw-gradient-to:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-200{--tw-gradient-to:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-300{--tw-gradient-to:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-400{--tw-gradient-to:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-500{--tw-gradient-to:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-600{--tw-gradient-to:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-700{--tw-gradient-to:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-800{--tw-gradient-to:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-red-900{--tw-gradient-to:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-50{--tw-gradient-to:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-100{--tw-gradient-to:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-200{--tw-gradient-to:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-300{--tw-gradient-to:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-400{--tw-gradient-to:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-500{--tw-gradient-to:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-600{--tw-gradient-to:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-700{--tw-gradient-to:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-800{--tw-gradient-to:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-slate-900{--tw-gradient-to:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-none{object-fit:none}.object-scale-down{object-fit:scale-down}.object-bottom{object-position:bottom}.object-center{object-position:center}.object-left{object-position:left}.object-left-bottom{object-position:left bottom}.object-left-top{object-position:left top}.object-right{object-position:right}.object-right-bottom{object-position:right bottom}.object-right-top{object-position:right top}.object-top{object-position:top}.\!p-0{padding:calc(var(--spacing) * 0)!important}.\!p-1{padding:calc(var(--spacing) * 1)!important}.\!p-2{padding:calc(var(--spacing) * 2)!important}.\!p-3{padding:calc(var(--spacing) * 3)!important}.\!p-4{padding:calc(var(--spacing) * 4)!important}.\!p-5{padding:calc(var(--spacing) * 5)!important}.\!p-6{padding:calc(var(--spacing) * 6)!important}.\!p-8{padding:calc(var(--spacing) * 8)!important}.\!p-10{padding:calc(var(--spacing) * 10)!important}.\!p-12{padding:calc(var(--spacing) * 12)!important}.\!p-16{padding:calc(var(--spacing) * 16)!important}.p-0{padding:calc(var(--spacing) * 0)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.p-10{padding:calc(var(--spacing) * 10)}.p-12{padding:calc(var(--spacing) * 12)}.p-16{padding:calc(var(--spacing) * 16)}.p-20{padding:calc(var(--spacing) * 20)}.p-24{padding:calc(var(--spacing) * 24)}.\!px-0{padding-inline:calc(var(--spacing) * 0)!important}.\!px-1{padding-inline:calc(var(--spacing) * 1)!important}.\!px-2{padding-inline:calc(var(--spacing) * 2)!important}.\!px-3{padding-inline:calc(var(--spacing) * 3)!important}.\!px-4{padding-inline:calc(var(--spacing) * 4)!important}.\!px-5{padding-inline:calc(var(--spacing) * 5)!important}.\!px-6{padding-inline:calc(var(--spacing) * 6)!important}.\!px-8{padding-inline:calc(var(--spacing) * 8)!important}.\!px-10{padding-inline:calc(var(--spacing) * 10)!important}.\!px-12{padding-inline:calc(var(--spacing) * 12)!important}.\!px-16{padding-inline:calc(var(--spacing) * 16)!important}.px-0{padding-inline:calc(var(--spacing) * 0)}.px-0\.5{padding-inline:calc(var(--spacing) * .5)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.px-10{padding-inline:calc(var(--spacing) * 10)}.px-12{padding-inline:calc(var(--spacing) * 12)}.px-16{padding-inline:calc(var(--spacing) * 16)}.\!py-0{padding-block:calc(var(--spacing) * 0)!important}.\!py-1{padding-block:calc(var(--spacing) * 1)!important}.\!py-2{padding-block:calc(var(--spacing) * 2)!important}.\!py-3{padding-block:calc(var(--spacing) * 3)!important}.\!py-4{padding-block:calc(var(--spacing) * 4)!important}.\!py-5{padding-block:calc(var(--spacing) * 5)!important}.\!py-6{padding-block:calc(var(--spacing) * 6)!important}.\!py-8{padding-block:calc(var(--spacing) * 8)!important}.\!py-10{padding-block:calc(var(--spacing) * 10)!important}.\!py-12{padding-block:calc(var(--spacing) * 12)!important}.\!py-16{padding-block:calc(var(--spacing) * 16)!important}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-16{padding-block:calc(var(--spacing) * 16)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pt-0\.5{padding-top:calc(var(--spacing) * .5)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-10{padding-top:calc(var(--spacing) * 10)}.pt-12{padding-top:calc(var(--spacing) * 12)}.pt-16{padding-top:calc(var(--spacing) * 16)}.pr-0{padding-right:calc(var(--spacing) * 0)}.pr-1{padding-right:calc(var(--spacing) * 1)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pr-6{padding-right:calc(var(--spacing) * 6)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pr-10{padding-right:calc(var(--spacing) * 10)}.pr-12{padding-right:calc(var(--spacing) * 12)}.pr-16{padding-right:calc(var(--spacing) * 16)}.pb-0{padding-bottom:calc(var(--spacing) * 0)}.pb-1{padding-bottom:calc(var(--spacing) * 1)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-3{padding-bottom:calc(var(--spacing) * 3)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pb-8{padding-bottom:calc(var(--spacing) * 8)}.pb-10{padding-bottom:calc(var(--spacing) * 10)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-0{padding-left:calc(var(--spacing) * 0)}.pl-1{padding-left:calc(var(--spacing) * 1)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-7{padding-left:calc(var(--spacing) * 7)}.pl-8{padding-left:calc(var(--spacing) * 8)}.pl-10{padding-left:calc(var(--spacing) * 10)}.pl-12{padding-left:calc(var(--spacing) * 12)}.pl-16{padding-left:calc(var(--spacing) * 16)}.\!text-center{text-align:center!important}.\!text-justify{text-align:justify!important}.\!text-left{text-align:left!important}.\!text-right{text-align:right!important}.text-center{text-align:center}.text-end{text-align:end}.text-justify{text-align:justify}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.indent-0{text-indent:calc(var(--spacing) * 0)}.indent-1{text-indent:calc(var(--spacing) * 1)}.indent-2{text-indent:calc(var(--spacing) * 2)}.indent-4{text-indent:calc(var(--spacing) * 4)}.indent-8{text-indent:calc(var(--spacing) * 8)}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.align-text-bottom{vertical-align:text-bottom}.align-text-top{vertical-align:text-top}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.\!text-2xl{font-size:var(--text-2xl)!important;line-height:var(--tw-leading,var(--text-2xl--line-height))!important}.\!text-3xl{font-size:var(--text-3xl)!important;line-height:var(--tw-leading,var(--text-3xl--line-height))!important}.\!text-4xl{font-size:var(--text-4xl)!important;line-height:var(--tw-leading,var(--text-4xl--line-height))!important}.\!text-base{font-size:var(--text-base)!important;line-height:var(--tw-leading,var(--text-base--line-height))!important}.\!text-lg{font-size:var(--text-lg)!important;line-height:var(--tw-leading,var(--text-lg--line-height))!important}.\!text-sm{font-size:var(--text-sm)!important;line-height:var(--tw-leading,var(--text-sm--line-height))!important}.\!text-xl{font-size:var(--text-xl)!important;line-height:var(--tw-leading,var(--text-xl--line-height))!important}.\!text-xs{font-size:var(--text-xs)!important;line-height:var(--tw-leading,var(--text-xs--line-height))!important}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[2rem\]{font-size:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-3{--tw-leading:calc(var(--spacing) * 3);line-height:calc(var(--spacing) * 3)}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.leading-7{--tw-leading:calc(var(--spacing) * 7);line-height:calc(var(--spacing) * 7)}.leading-8{--tw-leading:calc(var(--spacing) * 8);line-height:calc(var(--spacing) * 8)}.leading-9{--tw-leading:calc(var(--spacing) * 9);line-height:calc(var(--spacing) * 9)}.leading-10{--tw-leading:calc(var(--spacing) * 10);line-height:calc(var(--spacing) * 10)}.leading-loose{--tw-leading:var(--leading-loose);line-height:var(--leading-loose)}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.\!font-bold{--tw-font-weight:var(--font-weight-bold)!important;font-weight:var(--font-weight-bold)!important}.\!font-medium{--tw-font-weight:var(--font-weight-medium)!important;font-weight:var(--font-weight-medium)!important}.\!font-normal{--tw-font-weight:var(--font-weight-normal)!important;font-weight:var(--font-weight-normal)!important}.\!font-semibold{--tw-font-weight:var(--font-weight-semibold)!important;font-weight:var(--font-weight-semibold)!important}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-extralight{--tw-font-weight:var(--font-weight-extralight);font-weight:var(--font-weight-extralight)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-thin{--tw-font-weight:var(--font-weight-thin);font-weight:var(--font-weight-thin)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-50{color:var(--color-amber-50)}.text-amber-100{color:var(--color-amber-100)}.text-amber-200{color:var(--color-amber-200)}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-950{color:var(--color-amber-950)}.text-black{color:var(--color-black)}.text-blue-50{color:var(--color-blue-50)}.text-blue-100{color:var(--color-blue-100)}.text-blue-200{color:var(--color-blue-200)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-blue-950{color:var(--color-blue-950)}.text-current{color:currentColor}.text-cyan-50{color:var(--color-cyan-50)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-200{color:var(--color-cyan-200)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-500{color:var(--color-cyan-500)}.text-cyan-600{color:var(--color-cyan-600)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-800{color:var(--color-cyan-800)}.text-cyan-900{color:var(--color-cyan-900)}.text-cyan-950{color:var(--color-cyan-950)}.text-emerald-50{color:var(--color-emerald-50)}.text-emerald-100{color:var(--color-emerald-100)}.text-emerald-200{color:var(--color-emerald-200)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-fuchsia-50{color:var(--color-fuchsia-50)}.text-fuchsia-100{color:var(--color-fuchsia-100)}.text-fuchsia-200{color:var(--color-fuchsia-200)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-fuchsia-500{color:var(--color-fuchsia-500)}.text-fuchsia-600{color:var(--color-fuchsia-600)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-fuchsia-800{color:var(--color-fuchsia-800)}.text-fuchsia-900{color:var(--color-fuchsia-900)}.text-fuchsia-950{color:var(--color-fuchsia-950)}.text-gray-50{color:var(--color-gray-50)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-gray-950{color:var(--color-gray-950)}.text-green-50{color:var(--color-green-50)}.text-green-100{color:var(--color-green-100)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-green-900{color:var(--color-green-900)}.text-green-950{color:var(--color-green-950)}.text-indigo-50{color:var(--color-indigo-50)}.text-indigo-100{color:var(--color-indigo-100)}.text-indigo-200{color:var(--color-indigo-200)}.text-indigo-300{color:var(--color-indigo-300)}.text-indigo-400{color:var(--color-indigo-400)}.text-indigo-500{color:var(--color-indigo-500)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-indigo-800{color:var(--color-indigo-800)}.text-indigo-900{color:var(--color-indigo-900)}.text-indigo-950{color:var(--color-indigo-950)}.text-inherit{color:inherit}.text-lime-50{color:var(--color-lime-50)}.text-lime-100{color:var(--color-lime-100)}.text-lime-200{color:var(--color-lime-200)}.text-lime-300{color:var(--color-lime-300)}.text-lime-400{color:var(--color-lime-400)}.text-lime-500{color:var(--color-lime-500)}.text-lime-600{color:var(--color-lime-600)}.text-lime-700{color:var(--color-lime-700)}.text-lime-800{color:var(--color-lime-800)}.text-lime-900{color:var(--color-lime-900)}.text-lime-950{color:var(--color-lime-950)}.text-neutral-50{color:var(--color-neutral-50)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-orange-50{color:var(--color-orange-50)}.text-orange-100{color:var(--color-orange-100)}.text-orange-200{color:var(--color-orange-200)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-orange-900{color:var(--color-orange-900)}.text-orange-950{color:var(--color-orange-950)}.text-pink-50{color:var(--color-pink-50)}.text-pink-100{color:var(--color-pink-100)}.text-pink-200{color:var(--color-pink-200)}.text-pink-300{color:var(--color-pink-300)}.text-pink-400{color:var(--color-pink-400)}.text-pink-500{color:var(--color-pink-500)}.text-pink-600{color:var(--color-pink-600)}.text-pink-700{color:var(--color-pink-700)}.text-pink-800{color:var(--color-pink-800)}.text-pink-900{color:var(--color-pink-900)}.text-pink-950{color:var(--color-pink-950)}.text-purple-50{color:var(--color-purple-50)}.text-purple-100{color:var(--color-purple-100)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-purple-900{color:var(--color-purple-900)}.text-purple-950{color:var(--color-purple-950)}.text-red-50{color:var(--color-red-50)}.text-red-100{color:var(--color-red-100)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-50{color:var(--color-rose-50)}.text-rose-100{color:var(--color-rose-100)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-50{color:var(--color-sky-50)}.text-sky-100{color:var(--color-sky-100)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-900{color:var(--color-sky-900)}.text-sky-950{color:var(--color-sky-950)}.text-slate-50{color:var(--color-slate-50)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-stone-50{color:var(--color-stone-50)}.text-stone-100{color:var(--color-stone-100)}.text-stone-200{color:var(--color-stone-200)}.text-stone-300{color:var(--color-stone-300)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.text-stone-800{color:var(--color-stone-800)}.text-stone-900{color:var(--color-stone-900)}.text-stone-950{color:var(--color-stone-950)}.text-teal-50{color:var(--color-teal-50)}.text-teal-100{color:var(--color-teal-100)}.text-teal-200{color:var(--color-teal-200)}.text-teal-300{color:var(--color-teal-300)}.text-teal-400{color:var(--color-teal-400)}.text-teal-500{color:var(--color-teal-500)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-900{color:var(--color-teal-900)}.text-teal-950{color:var(--color-teal-950)}.text-transparent{color:#0000}.text-violet-50{color:var(--color-violet-50)}.text-violet-100{color:var(--color-violet-100)}.text-violet-200{color:var(--color-violet-200)}.text-violet-300{color:var(--color-violet-300)}.text-violet-400{color:var(--color-violet-400)}.text-violet-500{color:var(--color-violet-500)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-violet-900{color:var(--color-violet-900)}.text-violet-950{color:var(--color-violet-950)}.text-white{color:var(--color-white)}.text-yellow-50{color:var(--color-yellow-50)}.text-yellow-100{color:var(--color-yellow-100)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-900{color:var(--color-yellow-900)}.text-yellow-950{color:var(--color-yellow-950)}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.text-zinc-950{color:var(--color-zinc-950)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.placeholder-gray-400::placeholder{color:var(--color-gray-400)}.accent-blue-500{accent-color:var(--color-blue-500)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-15{opacity:.15}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.bg-blend-color{background-blend-mode:color}.bg-blend-color-burn{background-blend-mode:color-burn}.bg-blend-color-dodge{background-blend-mode:color-dodge}.bg-blend-darken{background-blend-mode:darken}.bg-blend-difference{background-blend-mode:difference}.bg-blend-exclusion{background-blend-mode:exclusion}.bg-blend-hard-light{background-blend-mode:hard-light}.bg-blend-hue{background-blend-mode:hue}.bg-blend-lighten{background-blend-mode:lighten}.bg-blend-luminosity{background-blend-mode:luminosity}.bg-blend-multiply{background-blend-mode:multiply}.bg-blend-normal{background-blend-mode:normal}.bg-blend-overlay{background-blend-mode:overlay}.bg-blend-saturation{background-blend-mode:saturation}.bg-blend-screen{background-blend-mode:screen}.bg-blend-soft-light{background-blend-mode:soft-light}.mix-blend-color{mix-blend-mode:color}.mix-blend-color-burn{mix-blend-mode:color-burn}.mix-blend-color-dodge{mix-blend-mode:color-dodge}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-difference{mix-blend-mode:difference}.mix-blend-exclusion{mix-blend-mode:exclusion}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-hue{mix-blend-mode:hue}.mix-blend-lighten{mix-blend-mode:lighten}.mix-blend-luminosity{mix-blend-mode:luminosity}.mix-blend-multiply{mix-blend-mode:multiply}.mix-blend-normal{mix-blend-mode:normal}.mix-blend-overlay{mix-blend-mode:overlay}.mix-blend-plus-darker{mix-blend-mode:plus-darker}.mix-blend-plus-lighter{mix-blend-mode:plus-lighter}.mix-blend-saturation{mix-blend-mode:saturation}.mix-blend-screen{mix-blend-mode:screen}.mix-blend-soft-light{mix-blend-mode:soft-light}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-8{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-purple-200{--tw-shadow-color:oklch(90.2% .063 306.703)}@supports (color:color-mix(in lab,red,red)){.shadow-purple-200{--tw-shadow-color:color-mix(in oklab, var(--color-purple-200) var(--tw-shadow-alpha), transparent)}}.ring-black{--tw-ring-color:var(--color-black)}.ring-blue-50{--tw-ring-color:var(--color-blue-50)}.ring-blue-100{--tw-ring-color:var(--color-blue-100)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-blue-300{--tw-ring-color:var(--color-blue-300)}.ring-blue-400{--tw-ring-color:var(--color-blue-400)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-blue-600{--tw-ring-color:var(--color-blue-600)}.ring-blue-700{--tw-ring-color:var(--color-blue-700)}.ring-blue-800{--tw-ring-color:var(--color-blue-800)}.ring-blue-900{--tw-ring-color:var(--color-blue-900)}.ring-current{--tw-ring-color:currentcolor}.ring-gray-50{--tw-ring-color:var(--color-gray-50)}.ring-gray-100{--tw-ring-color:var(--color-gray-100)}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-gray-400{--tw-ring-color:var(--color-gray-400)}.ring-gray-500{--tw-ring-color:var(--color-gray-500)}.ring-gray-600{--tw-ring-color:var(--color-gray-600)}.ring-gray-700{--tw-ring-color:var(--color-gray-700)}.ring-gray-800{--tw-ring-color:var(--color-gray-800)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-inherit{--tw-ring-color:inherit}.ring-slate-50{--tw-ring-color:var(--color-slate-50)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-slate-300{--tw-ring-color:var(--color-slate-300)}.ring-slate-400{--tw-ring-color:var(--color-slate-400)}.ring-slate-500{--tw-ring-color:var(--color-slate-500)}.ring-slate-600{--tw-ring-color:var(--color-slate-600)}.ring-slate-700{--tw-ring-color:var(--color-slate-700)}.ring-slate-800{--tw-ring-color:var(--color-slate-800)}.ring-slate-900{--tw-ring-color:var(--color-slate-900)}.ring-transparent{--tw-ring-color:transparent}.ring-white{--tw-ring-color:var(--color-white)}.ring-offset-0{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-1{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-4{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-8{--tw-ring-offset-width:8px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.ring-offset-black{--tw-ring-offset-color:var(--color-black)}.ring-offset-current{--tw-ring-offset-color:currentcolor}.ring-offset-inherit{--tw-ring-offset-color:inherit}.ring-offset-transparent{--tw-ring-offset-color:transparent}.ring-offset-white{--tw-ring-offset-color:var(--color-white)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-0{outline-style:var(--tw-outline-style);outline-width:0}.outline-1{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-4{outline-style:var(--tw-outline-style);outline-width:4px}.outline-8{outline-style:var(--tw-outline-style);outline-width:8px}.outline-offset-0{outline-offset:0px}.outline-offset-1{outline-offset:1px}.outline-offset-2{outline-offset:2px}.outline-offset-4{outline-offset:4px}.outline-offset-8{outline-offset:8px}.outline-black{outline-color:var(--color-black)}.outline-current{outline-color:currentColor}.outline-inherit{outline-color:inherit}.outline-transparent{outline-color:#0000}.outline-white{outline-color:var(--color-white)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-lg{--tw-blur:blur(var(--blur-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-md{--tw-blur:blur(var(--blur-md));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-none{--tw-blur: ;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-sm{--tw-blur:blur(var(--blur-sm));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-0{--tw-brightness:brightness(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-50{--tw-brightness:brightness(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-75{--tw-brightness:brightness(75%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-90{--tw-brightness:brightness(90%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-95{--tw-brightness:brightness(95%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-100{--tw-brightness:brightness(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-105{--tw-brightness:brightness(105%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-125{--tw-brightness:brightness(125%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-150{--tw-brightness:brightness(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-200{--tw-brightness:brightness(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-0{--tw-contrast:contrast(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-50{--tw-contrast:contrast(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-75{--tw-contrast:contrast(75%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-100{--tw-contrast:contrast(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-125{--tw-contrast:contrast(125%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-150{--tw-contrast:contrast(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.contrast-200{--tw-contrast:contrast(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size:drop-shadow(0 3px 3px var(--tw-drop-shadow-color,#0000001f));--tw-drop-shadow:drop-shadow(var(--drop-shadow-md));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-sm{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-sm));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-xl{--tw-drop-shadow-size:drop-shadow(0 9px 7px var(--tw-drop-shadow-color,#0000001a));--tw-drop-shadow:drop-shadow(var(--drop-shadow-xl));filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-none{--tw-drop-shadow: ;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale-0{--tw-grayscale:grayscale(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-15{--tw-hue-rotate:hue-rotate( -15deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-30{--tw-hue-rotate:hue-rotate( -30deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-60{--tw-hue-rotate:hue-rotate( -60deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-90{--tw-hue-rotate:hue-rotate( -90deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.-hue-rotate-180{--tw-hue-rotate:hue-rotate( -180deg );filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-0{--tw-hue-rotate:hue-rotate(0deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-15{--tw-hue-rotate:hue-rotate(15deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-30{--tw-hue-rotate:hue-rotate(30deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-60{--tw-hue-rotate:hue-rotate(60deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-90{--tw-hue-rotate:hue-rotate(90deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.hue-rotate-180{--tw-hue-rotate:hue-rotate(180deg);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert-0{--tw-invert:invert(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-0{--tw-saturate:saturate(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-50{--tw-saturate:saturate(50%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-100{--tw-saturate:saturate(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-150{--tw-saturate:saturate(150%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.saturate-200{--tw-saturate:saturate(200%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.sepia{--tw-sepia:sepia(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.sepia-0{--tw-sepia:sepia(0%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(var(--blur-3xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-none{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-0{--tw-backdrop-brightness:brightness(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-50{--tw-backdrop-brightness:brightness(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-75{--tw-backdrop-brightness:brightness(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-90{--tw-backdrop-brightness:brightness(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-95{--tw-backdrop-brightness:brightness(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-100{--tw-backdrop-brightness:brightness(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-105{--tw-backdrop-brightness:brightness(105%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-110{--tw-backdrop-brightness:brightness(110%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-125{--tw-backdrop-brightness:brightness(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-150{--tw-backdrop-brightness:brightness(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-brightness-200{--tw-backdrop-brightness:brightness(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-0{--tw-backdrop-contrast:contrast(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-50{--tw-backdrop-contrast:contrast(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-75{--tw-backdrop-contrast:contrast(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-100{--tw-backdrop-contrast:contrast(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-125{--tw-backdrop-contrast:contrast(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-150{--tw-backdrop-contrast:contrast(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-contrast-200{--tw-backdrop-contrast:contrast(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale-0{--tw-backdrop-grayscale:grayscale(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-0{--tw-backdrop-hue-rotate:hue-rotate(0deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-15{--tw-backdrop-hue-rotate:hue-rotate(15deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-30{--tw-backdrop-hue-rotate:hue-rotate(30deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-60{--tw-backdrop-hue-rotate:hue-rotate(60deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-90{--tw-backdrop-hue-rotate:hue-rotate(90deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-180{--tw-backdrop-hue-rotate:hue-rotate(180deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert-0{--tw-backdrop-invert:invert(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-0{--tw-backdrop-opacity:opacity(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-5{--tw-backdrop-opacity:opacity(5%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-10{--tw-backdrop-opacity:opacity(10%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-20{--tw-backdrop-opacity:opacity(20%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-25{--tw-backdrop-opacity:opacity(25%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-30{--tw-backdrop-opacity:opacity(30%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-40{--tw-backdrop-opacity:opacity(40%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-50{--tw-backdrop-opacity:opacity(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-60{--tw-backdrop-opacity:opacity(60%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-70{--tw-backdrop-opacity:opacity(70%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-75{--tw-backdrop-opacity:opacity(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-80{--tw-backdrop-opacity:opacity(80%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-90{--tw-backdrop-opacity:opacity(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-95{--tw-backdrop-opacity:opacity(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-opacity-100{--tw-backdrop-opacity:opacity(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-0{--tw-backdrop-saturate:saturate(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-50{--tw-backdrop-saturate:saturate(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-100{--tw-backdrop-saturate:saturate(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-150{--tw-backdrop-saturate:saturate(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-saturate-200{--tw-backdrop-saturate:saturate(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia-0{--tw-backdrop-sepia:sepia(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.delay-100{transition-delay:.1s}.delay-150{transition-delay:.15s}.delay-200{transition-delay:.2s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.delay-1000{transition-delay:1s}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.duration-1000{--tw-duration:1s;transition-duration:1s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-dashed{--tw-outline-style:dashed;outline-style:dashed}.outline-dotted{--tw-outline-style:dotted;outline-style:dotted}.outline-double{--tw-outline-style:double;outline-style:double}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;user-select:auto}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.ring-inset{--tw-ring-inset:inset}@media(hover:hover){.group-hover\:visible:is(:where(.group):hover *){visibility:visible}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:scale-100:is(:where(.group):hover *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:#0006}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.group-hover\:bg-blue-100:is(:where(.group):hover *){background-color:var(--color-blue-100)}.group-hover\:bg-blue-500:is(:where(.group):hover *){background-color:var(--color-blue-500)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:bg-gray-100:is(:where(.group):hover *){background-color:var(--color-gray-100)}.group-hover\:bg-gray-200:is(:where(.group):hover *){background-color:var(--color-gray-200)}.group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-blue-500:is(:where(.group):hover *){color:var(--color-blue-500)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-blue-700:is(:where(.group):hover *){color:var(--color-blue-700)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:text-gray-800:is(:where(.group):hover *){color:var(--color-gray-800)}.group-hover\:text-gray-900:is(:where(.group):hover *){color:var(--color-gray-900)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.group-focus\:border-blue-500:is(:where(.group):focus *){border-color:var(--color-blue-500)}.group-focus\:ring-2:is(:where(.group):focus *){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-checked\:border-blue-500:is(:where(.peer):checked~*){border-color:var(--color-blue-500)}.peer-checked\:bg-blue-500:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.peer-invalid\:border-red-500:is(:where(.peer):invalid~*){border-color:var(--color-red-500)}.peer-invalid\:text-red-500:is(:where(.peer):invalid~*){color:var(--color-red-500)}.peer-focus\:border-blue-500:is(:where(.peer):focus~*){border-color:var(--color-blue-500)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.first-letter\:text-2xl:first-letter{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.first-letter\:font-bold:first-letter{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.first-letter\:text-blue-600:first-letter{color:var(--color-blue-600)}.first-line\:font-semibold:first-line{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.first-line\:text-gray-700:first-line{color:var(--color-gray-700)}.marker\:text-blue-500 ::marker{color:var(--color-blue-500)}.marker\:text-blue-500::marker{color:var(--color-blue-500)}.marker\:text-blue-500 ::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-blue-500::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-gray-500 ::marker{color:var(--color-gray-500)}.marker\:text-gray-500::marker{color:var(--color-gray-500)}.marker\:text-gray-500 ::-webkit-details-marker{color:var(--color-gray-500)}.marker\:text-gray-500::-webkit-details-marker{color:var(--color-gray-500)}.selection\:bg-blue-200 ::selection{background-color:var(--color-blue-200)}.selection\:bg-blue-200::selection{background-color:var(--color-blue-200)}.selection\:text-blue-900 ::selection{color:var(--color-blue-900)}.selection\:text-blue-900::selection{color:var(--color-blue-900)}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing) * 4)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-full::file-selector-button{border-radius:3.40282e38px}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-blue-50::file-selector-button{background-color:var(--color-blue-50)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing) * 4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing) * 2)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-blue-700::file-selector-button{color:var(--color-blue-700)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:calc(var(--spacing) * 0)}.before\:block:before{content:var(--tw-content);display:block}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing) * 0)}.after\:clear-both:after{content:var(--tw-content);clear:both}.after\:block:after{content:var(--tw-content);display:block}.after\:h-4:after{content:var(--tw-content);height:calc(var(--spacing) * 4)}.after\:h-5:after{content:var(--tw-content);height:calc(var(--spacing) * 5)}.after\:h-6:after{content:var(--tw-content);height:calc(var(--spacing) * 6)}.after\:w-4:after{content:var(--tw-content);width:calc(var(--spacing) * 4)}.after\:w-5:after{content:var(--tw-content);width:calc(var(--spacing) * 5)}.after\:w-6:after{content:var(--tw-content);width:calc(var(--spacing) * 6)}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:mt-0:first-child{margin-top:calc(var(--spacing) * 0)}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.first\:rounded-l:first-child{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.first\:rounded-l-lg:first-child{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:pt-0:first-child{padding-top:calc(var(--spacing) * 0)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing) * 0)}.last\:rounded-r:last-child{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.last\:rounded-r-lg:last-child{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.last\:border-r:last-child{border-right-style:var(--tw-border-style);border-right-width:1px}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing) * 0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.odd\:bg-gray-100:nth-child(odd){background-color:var(--color-gray-100)}.odd\:bg-white:nth-child(odd){background-color:var(--color-white)}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}.even\:bg-gray-100:nth-child(2n){background-color:var(--color-gray-100)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.checked\:border-blue-500:checked{border-color:var(--color-blue-500)}.checked\:border-blue-600:checked{border-color:var(--color-blue-600)}.checked\:bg-blue-500:checked{background-color:var(--color-blue-500)}.checked\:bg-blue-600:checked{background-color:var(--color-blue-600)}.checked\:bg-green-500:checked{background-color:var(--color-green-500)}.checked\:text-white:checked{color:var(--color-white)}.indeterminate\:border-gray-400:indeterminate{border-color:var(--color-gray-400)}.indeterminate\:bg-gray-300:indeterminate{background-color:var(--color-gray-300)}.placeholder-shown\:border-gray-300:placeholder-shown{border-color:var(--color-gray-300)}.autofill\:bg-yellow-100:autofill{background-color:var(--color-yellow-100)}.autofill\:text-black:autofill{color:var(--color-black)}.valid\:border-green-500:valid{border-color:var(--color-green-500)}.valid\:text-green-600:valid{color:var(--color-green-600)}.valid\:ring-green-500:valid{--tw-ring-color:var(--color-green-500)}.invalid\:border-red-500:invalid{border-color:var(--color-red-500)}.invalid\:text-red-600:invalid{color:var(--color-red-600)}.invalid\:ring-red-500:invalid{--tw-ring-color:var(--color-red-500)}.read-only\:cursor-not-allowed:read-only{cursor:not-allowed}.read-only\:bg-gray-50:read-only{background-color:var(--color-gray-50)}.read-only\:bg-gray-100:read-only{background-color:var(--color-gray-100)}.empty\:hidden:empty{display:none}.focus-within\:border-blue-500:focus-within{border-color:var(--color-blue-500)}.focus-within\:border-gray-500:focus-within{border-color:var(--color-gray-500)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-blue-500:focus-within{--tw-ring-color:var(--color-blue-500)}.focus-within\:ring-gray-500:focus-within{--tw-ring-color:var(--color-gray-500)}@media(hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:scale-95:hover{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-100:hover{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black) 10%,transparent)}}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-100:hover{background-color:var(--color-green-100)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-300:hover{background-color:var(--color-green-300)}.hover\:bg-green-400:hover{background-color:var(--color-green-400)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-green-800:hover{background-color:var(--color-green-800)}.hover\:bg-green-900:hover{background-color:var(--color-green-900)}.hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:bg-red-300:hover{background-color:var(--color-red-300)}.hover\:bg-red-400:hover{background-color:var(--color-red-400)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-yellow-50:hover{background-color:var(--color-yellow-50)}.hover\:bg-yellow-100:hover{background-color:var(--color-yellow-100)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.hover\:bg-yellow-900:hover{background-color:var(--color-yellow-900)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-blue-50:hover{color:var(--color-blue-50)}.hover\:text-blue-100:hover{color:var(--color-blue-100)}.hover\:text-blue-200:hover{color:var(--color-blue-200)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-blue-900:hover{color:var(--color-blue-900)}.hover\:text-gray-50:hover{color:var(--color-gray-50)}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-500:hover{color:var(--color-gray-500)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-800:hover{color:var(--color-gray-800)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-50:hover{color:var(--color-green-50)}.hover\:text-green-100:hover{color:var(--color-green-100)}.hover\:text-green-200:hover{color:var(--color-green-200)}.hover\:text-green-300:hover{color:var(--color-green-300)}.hover\:text-green-400:hover{color:var(--color-green-400)}.hover\:text-green-500:hover{color:var(--color-green-500)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-purple-50:hover{color:var(--color-purple-50)}.hover\:text-purple-100:hover{color:var(--color-purple-100)}.hover\:text-purple-200:hover{color:var(--color-purple-200)}.hover\:text-purple-300:hover{color:var(--color-purple-300)}.hover\:text-purple-400:hover{color:var(--color-purple-400)}.hover\:text-purple-500:hover{color:var(--color-purple-500)}.hover\:text-purple-600:hover{color:var(--color-purple-600)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-800:hover{color:var(--color-purple-800)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-50:hover{color:var(--color-red-50)}.hover\:text-red-100:hover{color:var(--color-red-100)}.hover\:text-red-200:hover{color:var(--color-red-200)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:text-slate-50:hover{color:var(--color-slate-50)}.hover\:text-slate-100:hover{color:var(--color-slate-100)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}.hover\:text-slate-300:hover{color:var(--color-slate-300)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-slate-500:hover{color:var(--color-slate-500)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-0:hover{opacity:0}.hover\:opacity-25:hover{opacity:.25}.hover\:opacity-50:hover{opacity:.5}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-100:hover{opacity:1}.hover\:shadow:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-none:hover{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-0:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-1:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-4:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-8:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}.hover\:ring-gray-500:hover{--tw-ring-color:var(--color-gray-500)}.hover\:ring-green-500:hover{--tw-ring-color:var(--color-green-500)}.hover\:ring-red-500:hover{--tw-ring-color:var(--color-red-500)}.file\:hover\:bg-blue-100::file-selector-button:hover{background-color:var(--color-blue-100)}}.focus\:border-blue-50:focus{border-color:var(--color-blue-50)}.focus\:border-blue-100:focus{border-color:var(--color-blue-100)}.focus\:border-blue-200:focus{border-color:var(--color-blue-200)}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.focus\:border-blue-900:focus{border-color:var(--color-blue-900)}.focus\:border-gray-50:focus{border-color:var(--color-gray-50)}.focus\:border-gray-100:focus{border-color:var(--color-gray-100)}.focus\:border-gray-200:focus{border-color:var(--color-gray-200)}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:border-gray-400:focus{border-color:var(--color-gray-400)}.focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.focus\:border-gray-800:focus{border-color:var(--color-gray-800)}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-green-500:focus{border-color:var(--color-green-500)}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:border-red-500:focus{border-color:var(--color-red-500)}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-yellow-500:focus{border-color:var(--color-yellow-500)}.focus\:bg-black:focus{background-color:var(--color-black)}.focus\:bg-blue-50:focus{background-color:var(--color-blue-50)}.focus\:bg-blue-100:focus{background-color:var(--color-blue-100)}.focus\:bg-blue-200:focus{background-color:var(--color-blue-200)}.focus\:bg-blue-300:focus{background-color:var(--color-blue-300)}.focus\:bg-blue-400:focus{background-color:var(--color-blue-400)}.focus\:bg-blue-500:focus{background-color:var(--color-blue-500)}.focus\:bg-blue-600:focus{background-color:var(--color-blue-600)}.focus\:bg-blue-700:focus{background-color:var(--color-blue-700)}.focus\:bg-blue-800:focus{background-color:var(--color-blue-800)}.focus\:bg-blue-900:focus{background-color:var(--color-blue-900)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:bg-gray-100:focus{background-color:var(--color-gray-100)}.focus\:bg-gray-200:focus{background-color:var(--color-gray-200)}.focus\:bg-gray-300:focus{background-color:var(--color-gray-300)}.focus\:bg-gray-400:focus{background-color:var(--color-gray-400)}.focus\:bg-gray-500:focus{background-color:var(--color-gray-500)}.focus\:bg-gray-600:focus{background-color:var(--color-gray-600)}.focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.focus\:bg-transparent:focus{background-color:#0000}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:text-black:focus{color:var(--color-black)}.focus\:text-blue-50:focus{color:var(--color-blue-50)}.focus\:text-blue-100:focus{color:var(--color-blue-100)}.focus\:text-blue-200:focus{color:var(--color-blue-200)}.focus\:text-blue-300:focus{color:var(--color-blue-300)}.focus\:text-blue-400:focus{color:var(--color-blue-400)}.focus\:text-blue-500:focus{color:var(--color-blue-500)}.focus\:text-blue-600:focus{color:var(--color-blue-600)}.focus\:text-blue-700:focus{color:var(--color-blue-700)}.focus\:text-blue-800:focus{color:var(--color-blue-800)}.focus\:text-blue-900:focus{color:var(--color-blue-900)}.focus\:text-gray-50:focus{color:var(--color-gray-50)}.focus\:text-gray-100:focus{color:var(--color-gray-100)}.focus\:text-gray-200:focus{color:var(--color-gray-200)}.focus\:text-gray-300:focus{color:var(--color-gray-300)}.focus\:text-gray-400:focus{color:var(--color-gray-400)}.focus\:text-gray-500:focus{color:var(--color-gray-500)}.focus\:text-gray-600:focus{color:var(--color-gray-600)}.focus\:text-gray-700:focus{color:var(--color-gray-700)}.focus\:text-gray-800:focus{color:var(--color-gray-800)}.focus\:text-gray-900:focus{color:var(--color-gray-900)}.focus\:text-white:focus{color:var(--color-white)}.focus\:shadow:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-md:focus{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-sm:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-xl:focus{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-8:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(8px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.focus\:ring-blue-50:focus{--tw-ring-color:var(--color-blue-50)}.focus\:ring-blue-100:focus{--tw-ring-color:var(--color-blue-100)}.focus\:ring-blue-200:focus{--tw-ring-color:var(--color-blue-200)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.focus\:ring-blue-800:focus{--tw-ring-color:var(--color-blue-800)}.focus\:ring-blue-900:focus{--tw-ring-color:var(--color-blue-900)}.focus\:ring-current:focus{--tw-ring-color:currentcolor}.focus\:ring-gray-50:focus{--tw-ring-color:var(--color-gray-50)}.focus\:ring-gray-100:focus{--tw-ring-color:var(--color-gray-100)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:ring-gray-300:focus{--tw-ring-color:var(--color-gray-300)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.focus\:ring-gray-800:focus{--tw-ring-color:var(--color-gray-800)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-slate-400:focus{--tw-ring-color:var(--color-slate-400)}.focus\:ring-transparent:focus{--tw-ring-color:transparent}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-1:focus{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-4:focus{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:var(--color-gray-100)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:ring-offset-white:focus{--tw-ring-offset-color:var(--color-white)}.focus\:outline:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-1:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-4:focus{outline-style:var(--tw-outline-style);outline-width:4px}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-gray-500:focus-visible{--tw-ring-color:var(--color-gray-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:scale-100:active{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.active\:bg-blue-600:active{background-color:var(--color-blue-600)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.active\:bg-blue-800:active{background-color:var(--color-blue-800)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:bg-gray-200:active{background-color:var(--color-gray-200)}.active\:bg-gray-300:active{background-color:var(--color-gray-300)}.active\:bg-gray-700:active{background-color:var(--color-gray-700)}.active\:bg-gray-800:active{background-color:var(--color-gray-800)}.active\:bg-green-600:active{background-color:var(--color-green-600)}.active\:bg-green-700:active{background-color:var(--color-green-700)}.active\:bg-red-600:active{background-color:var(--color-red-600)}.active\:bg-red-700:active{background-color:var(--color-red-700)}.active\:opacity-75:active{opacity:.75}.active\:opacity-100:active{opacity:1}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:border-gray-200:disabled{border-color:var(--color-gray-200)}.disabled\:border-gray-300:disabled{border-color:var(--color-gray-300)}.disabled\:bg-blue-400:disabled{background-color:var(--color-blue-400)}.disabled\:bg-gray-100:disabled{background-color:var(--color-gray-100)}.disabled\:bg-gray-200:disabled{background-color:var(--color-gray-200)}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:bg-gray-400:disabled{background-color:var(--color-gray-400)}.disabled\:text-gray-300:disabled{color:var(--color-gray-300)}.disabled\:text-gray-400:disabled{color:var(--color-gray-400)}.disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.disabled\:no-underline:disabled{text-decoration-line:none}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}@media(hover:hover){.disabled\:hover\:bg-indigo-600:disabled:hover{background-color:var(--color-indigo-600)}.disabled\:hover\:bg-transparent:disabled:hover{background-color:#0000}}@media(min-width:40rem){.sm\:col-span-1{grid-column:span 1/span 1}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-12{grid-column:span 12/span 12}.sm\:col-span-full{grid-column:1/-1}.sm\:m-0{margin:calc(var(--spacing) * 0)}.sm\:m-1{margin:calc(var(--spacing) * 1)}.sm\:m-2{margin:calc(var(--spacing) * 2)}.sm\:m-3{margin:calc(var(--spacing) * 3)}.sm\:m-4{margin:calc(var(--spacing) * 4)}.sm\:m-auto{margin:auto}.sm\:mx-auto{margin-inline:auto}.sm\:my-auto{margin-block:auto}.sm\:mt-6{margin-top:calc(var(--spacing) * 6)}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-block{display:inline-block}.sm\:inline-flex{display:inline-flex}.sm\:w-1\/2{width:50%}.sm\:w-1\/3{width:33.3333%}.sm\:w-1\/4{width:25%}.sm\:w-2\/3{width:66.6667%}.sm\:w-3\/4{width:75%}.sm\:w-40{width:calc(var(--spacing) * 40)}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-center{justify-content:center}.sm\:justify-end{justify-content:flex-end}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:calc(var(--spacing) * 0)}.sm\:gap-1{gap:calc(var(--spacing) * 1)}.sm\:gap-2{gap:calc(var(--spacing) * 2)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-4{gap:calc(var(--spacing) * 4)}.sm\:gap-5{gap:calc(var(--spacing) * 5)}.sm\:gap-6{gap:calc(var(--spacing) * 6)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 0) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.sm\:p-0{padding:calc(var(--spacing) * 0)}.sm\:p-1{padding:calc(var(--spacing) * 1)}.sm\:p-2{padding:calc(var(--spacing) * 2)}.sm\:p-3{padding:calc(var(--spacing) * 3)}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:p-8{padding:calc(var(--spacing) * 8)}.sm\:px-0{padding-inline:calc(var(--spacing) * 0)}.sm\:px-1{padding-inline:calc(var(--spacing) * 1)}.sm\:px-2{padding-inline:calc(var(--spacing) * 2)}.sm\:px-3{padding-inline:calc(var(--spacing) * 3)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:px-6{padding-inline:calc(var(--spacing) * 6)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0{padding-block:calc(var(--spacing) * 0)}.sm\:py-1{padding-block:calc(var(--spacing) * 1)}.sm\:py-2{padding-block:calc(var(--spacing) * 2)}.sm\:py-3{padding-block:calc(var(--spacing) * 3)}.sm\:py-4{padding-block:calc(var(--spacing) * 4)}.sm\:py-5{padding-block:calc(var(--spacing) * 5)}.sm\:py-6{padding-block:calc(var(--spacing) * 6)}.sm\:py-8{padding-block:calc(var(--spacing) * 8)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:48rem){.md\:absolute{position:absolute}.md\:fixed{position:fixed}.md\:relative{position:relative}.md\:static{position:static}.md\:sticky{position:sticky}.md\:inset-0{inset:calc(var(--spacing) * 0)}.md\:top-0{top:calc(var(--spacing) * 0)}.md\:right-0{right:calc(var(--spacing) * 0)}.md\:bottom-0{bottom:calc(var(--spacing) * 0)}.md\:left-0{left:calc(var(--spacing) * 0)}.md\:order-1{order:1}.md\:order-2{order:2}.md\:order-3{order:3}.md\:order-first{order:-9999}.md\:order-last{order:9999}.md\:order-none{order:0}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-12{grid-column:span 12/span 12}.md\:col-span-full{grid-column:1/-1}.md\:row-span-1{grid-row:span 1/span 1}.md\:row-span-2{grid-row:span 2/span 2}.md\:row-span-3{grid-row:span 3/span 3}.md\:row-span-4{grid-row:span 4/span 4}.md\:row-span-full{grid-row:1/-1}.md\:m-0{margin:calc(var(--spacing) * 0)}.md\:m-1{margin:calc(var(--spacing) * 1)}.md\:m-2{margin:calc(var(--spacing) * 2)}.md\:m-3{margin:calc(var(--spacing) * 3)}.md\:m-4{margin:calc(var(--spacing) * 4)}.md\:m-auto{margin:auto}.md\:mx-auto{margin-inline:auto}.md\:my-auto{margin-block:auto}.md\:mt-0{margin-top:calc(var(--spacing) * 0)}.md\:mt-1{margin-top:calc(var(--spacing) * 1)}.md\:mt-2{margin-top:calc(var(--spacing) * 2)}.md\:mt-3{margin-top:calc(var(--spacing) * 3)}.md\:mt-4{margin-top:calc(var(--spacing) * 4)}.md\:mt-6{margin-top:calc(var(--spacing) * 6)}.md\:mt-8{margin-top:calc(var(--spacing) * 8)}.md\:mr-0{margin-right:calc(var(--spacing) * 0)}.md\:mr-1{margin-right:calc(var(--spacing) * 1)}.md\:mr-2{margin-right:calc(var(--spacing) * 2)}.md\:mr-3{margin-right:calc(var(--spacing) * 3)}.md\:mr-4{margin-right:calc(var(--spacing) * 4)}.md\:mr-6{margin-right:calc(var(--spacing) * 6)}.md\:mr-8{margin-right:calc(var(--spacing) * 8)}.md\:mr-auto{margin-right:auto}.md\:mb-0{margin-bottom:calc(var(--spacing) * 0)}.md\:mb-1{margin-bottom:calc(var(--spacing) * 1)}.md\:mb-2{margin-bottom:calc(var(--spacing) * 2)}.md\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.md\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.md\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.md\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.md\:ml-0{margin-left:calc(var(--spacing) * 0)}.md\:ml-1{margin-left:calc(var(--spacing) * 1)}.md\:ml-2{margin-left:calc(var(--spacing) * 2)}.md\:ml-3{margin-left:calc(var(--spacing) * 3)}.md\:ml-4{margin-left:calc(var(--spacing) * 4)}.md\:ml-6{margin-left:calc(var(--spacing) * 6)}.md\:ml-8{margin-left:calc(var(--spacing) * 8)}.md\:ml-auto{margin-left:auto}.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-block{display:inline-block}.md\:inline-flex{display:inline-flex}.md\:h-20{height:calc(var(--spacing) * 20)}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.3333%}.md\:w-1\/4{width:25%}.md\:w-1\/5{width:20%}.md\:w-2\/3{width:66.6667%}.md\:w-2\/5{width:40%}.md\:w-3\/4{width:75%}.md\:w-3\/5{width:60%}.md\:w-4\/5{width:80%}.md\:w-20{width:calc(var(--spacing) * 20)}.md\:w-40{width:calc(var(--spacing) * 40)}.md\:w-48{width:calc(var(--spacing) * 48)}.md\:w-64{width:calc(var(--spacing) * 64)}.md\:w-72{width:calc(var(--spacing) * 72)}.md\:w-80{width:calc(var(--spacing) * 80)}.md\:w-auto{width:auto}.md\:w-full{width:100%}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-4xl{max-width:var(--container-4xl)}.md\:max-w-5xl{max-width:var(--container-5xl)}.md\:max-w-6xl{max-width:var(--container-6xl)}.md\:max-w-7xl{max-width:var(--container-7xl)}.md\:max-w-full{max-width:100%}.md\:max-w-lg{max-width:var(--container-lg)}.md\:max-w-md{max-width:var(--container-md)}.md\:max-w-none{max-width:none}.md\:max-w-sm{max-width:var(--container-sm)}.md\:max-w-xl{max-width:var(--container-xl)}.md\:max-w-xs{max-width:var(--container-xs)}.md\:flex-1{flex:1}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.md\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.md\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.md\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:flex-row{flex-direction:row}.md\:items-baseline{align-items:baseline}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:items-stretch{align-items:stretch}.md\:justify-around{justify-content:space-around}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}.md\:justify-end{justify-content:flex-end}.md\:justify-start{justify-content:flex-start}.md\:gap-0{gap:calc(var(--spacing) * 0)}.md\:gap-1{gap:calc(var(--spacing) * 1)}.md\:gap-2{gap:calc(var(--spacing) * 2)}.md\:gap-3{gap:calc(var(--spacing) * 3)}.md\:gap-4{gap:calc(var(--spacing) * 4)}.md\:gap-5{gap:calc(var(--spacing) * 5)}.md\:gap-6{gap:calc(var(--spacing) * 6)}.md\:gap-8{gap:calc(var(--spacing) * 8)}.md\:gap-10{gap:calc(var(--spacing) * 10)}.md\:gap-12{gap:calc(var(--spacing) * 12)}:where(.md\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 0) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.md\:rounded-2xl{border-radius:var(--radius-2xl)}.md\:p-0{padding:calc(var(--spacing) * 0)}.md\:p-1{padding:calc(var(--spacing) * 1)}.md\:p-2{padding:calc(var(--spacing) * 2)}.md\:p-3{padding:calc(var(--spacing) * 3)}.md\:p-4{padding:calc(var(--spacing) * 4)}.md\:p-5{padding:calc(var(--spacing) * 5)}.md\:p-6{padding:calc(var(--spacing) * 6)}.md\:p-8{padding:calc(var(--spacing) * 8)}.md\:p-10{padding:calc(var(--spacing) * 10)}.md\:p-12{padding:calc(var(--spacing) * 12)}.md\:px-0{padding-inline:calc(var(--spacing) * 0)}.md\:px-1{padding-inline:calc(var(--spacing) * 1)}.md\:px-2{padding-inline:calc(var(--spacing) * 2)}.md\:px-3{padding-inline:calc(var(--spacing) * 3)}.md\:px-4{padding-inline:calc(var(--spacing) * 4)}.md\:px-5{padding-inline:calc(var(--spacing) * 5)}.md\:px-6{padding-inline:calc(var(--spacing) * 6)}.md\:px-8{padding-inline:calc(var(--spacing) * 8)}.md\:px-10{padding-inline:calc(var(--spacing) * 10)}.md\:px-12{padding-inline:calc(var(--spacing) * 12)}.md\:py-0{padding-block:calc(var(--spacing) * 0)}.md\:py-1{padding-block:calc(var(--spacing) * 1)}.md\:py-2{padding-block:calc(var(--spacing) * 2)}.md\:py-3{padding-block:calc(var(--spacing) * 3)}.md\:py-4{padding-block:calc(var(--spacing) * 4)}.md\:py-5{padding-block:calc(var(--spacing) * 5)}.md\:py-6{padding-block:calc(var(--spacing) * 6)}.md\:py-8{padding-block:calc(var(--spacing) * 8)}.md\:py-10{padding-block:calc(var(--spacing) * 10)}.md\:py-12{padding-block:calc(var(--spacing) * 12)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:64rem){.lg\:absolute{position:absolute}.lg\:fixed{position:fixed}.lg\:relative{position:relative}.lg\:static{position:static}.lg\:sticky{position:sticky}.lg\:inset-0{inset:calc(var(--spacing) * 0)}.lg\:top-0{top:calc(var(--spacing) * 0)}.lg\:right-0{right:calc(var(--spacing) * 0)}.lg\:bottom-0{bottom:calc(var(--spacing) * 0)}.lg\:left-0{left:calc(var(--spacing) * 0)}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-3{order:3}.lg\:order-4{order:4}.lg\:order-first{order:-9999}.lg\:order-last{order:9999}.lg\:order-none{order:0}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:col-span-full{grid-column:1/-1}.lg\:row-span-1{grid-row:span 1/span 1}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-span-3{grid-row:span 3/span 3}.lg\:row-span-4{grid-row:span 4/span 4}.lg\:row-span-full{grid-row:1/-1}.lg\:m-0{margin:calc(var(--spacing) * 0)}.lg\:m-1{margin:calc(var(--spacing) * 1)}.lg\:m-2{margin:calc(var(--spacing) * 2)}.lg\:m-3{margin:calc(var(--spacing) * 3)}.lg\:m-4{margin:calc(var(--spacing) * 4)}.lg\:m-auto{margin:auto}.lg\:mx-auto{margin-inline:auto}.lg\:my-auto{margin-block:auto}.lg\:mt-0{margin-top:calc(var(--spacing) * 0)}.lg\:mt-1{margin-top:calc(var(--spacing) * 1)}.lg\:mt-2{margin-top:calc(var(--spacing) * 2)}.lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.lg\:mt-4{margin-top:calc(var(--spacing) * 4)}.lg\:mt-6{margin-top:calc(var(--spacing) * 6)}.lg\:mt-8{margin-top:calc(var(--spacing) * 8)}.lg\:mt-10{margin-top:calc(var(--spacing) * 10)}.lg\:mt-12{margin-top:calc(var(--spacing) * 12)}.lg\:mr-0{margin-right:calc(var(--spacing) * 0)}.lg\:mr-1{margin-right:calc(var(--spacing) * 1)}.lg\:mr-2{margin-right:calc(var(--spacing) * 2)}.lg\:mr-3{margin-right:calc(var(--spacing) * 3)}.lg\:mr-4{margin-right:calc(var(--spacing) * 4)}.lg\:mr-6{margin-right:calc(var(--spacing) * 6)}.lg\:mr-8{margin-right:calc(var(--spacing) * 8)}.lg\:mr-auto{margin-right:auto}.lg\:mb-0{margin-bottom:calc(var(--spacing) * 0)}.lg\:mb-1{margin-bottom:calc(var(--spacing) * 1)}.lg\:mb-2{margin-bottom:calc(var(--spacing) * 2)}.lg\:mb-3{margin-bottom:calc(var(--spacing) * 3)}.lg\:mb-4{margin-bottom:calc(var(--spacing) * 4)}.lg\:mb-6{margin-bottom:calc(var(--spacing) * 6)}.lg\:mb-8{margin-bottom:calc(var(--spacing) * 8)}.lg\:mb-10{margin-bottom:calc(var(--spacing) * 10)}.lg\:mb-12{margin-bottom:calc(var(--spacing) * 12)}.lg\:ml-0{margin-left:calc(var(--spacing) * 0)}.lg\:ml-1{margin-left:calc(var(--spacing) * 1)}.lg\:ml-2{margin-left:calc(var(--spacing) * 2)}.lg\:ml-3{margin-left:calc(var(--spacing) * 3)}.lg\:ml-4{margin-left:calc(var(--spacing) * 4)}.lg\:ml-6{margin-left:calc(var(--spacing) * 6)}.lg\:ml-8{margin-left:calc(var(--spacing) * 8)}.lg\:ml-auto{margin-left:auto}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:inline{display:inline}.lg\:inline-block{display:inline-block}.lg\:inline-flex{display:inline-flex}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.3333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:w-1\/6{width:16.6667%}.lg\:w-2\/3{width:66.6667%}.lg\:w-2\/5{width:40%}.lg\:w-3\/4{width:75%}.lg\:w-3\/5{width:60%}.lg\:w-4\/5{width:80%}.lg\:w-5\/6{width:83.3333%}.lg\:w-auto{width:auto}.lg\:w-full{width:100%}.lg\:max-w-2xl{max-width:var(--container-2xl)}.lg\:max-w-3xl{max-width:var(--container-3xl)}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-5xl{max-width:var(--container-5xl)}.lg\:max-w-6xl{max-width:var(--container-6xl)}.lg\:max-w-7xl{max-width:var(--container-7xl)}.lg\:max-w-full{max-width:100%}.lg\:max-w-lg{max-width:var(--container-lg)}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-none{max-width:none}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:max-w-xl{max-width:var(--container-xl)}.lg\:max-w-xs{max-width:var(--container-xs)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.lg\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.lg\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.lg\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.lg\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.lg\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-baseline{align-items:baseline}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:items-stretch{align-items:stretch}.lg\:justify-around{justify-content:space-around}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-evenly{justify-content:space-evenly}.lg\:justify-start{justify-content:flex-start}.lg\:gap-0{gap:calc(var(--spacing) * 0)}.lg\:gap-1{gap:calc(var(--spacing) * 1)}.lg\:gap-2{gap:calc(var(--spacing) * 2)}.lg\:gap-3{gap:calc(var(--spacing) * 3)}.lg\:gap-4{gap:calc(var(--spacing) * 4)}.lg\:gap-5{gap:calc(var(--spacing) * 5)}.lg\:gap-6{gap:calc(var(--spacing) * 6)}.lg\:gap-8{gap:calc(var(--spacing) * 8)}.lg\:gap-10{gap:calc(var(--spacing) * 10)}.lg\:gap-12{gap:calc(var(--spacing) * 12)}.lg\:gap-16{gap:calc(var(--spacing) * 16)}:where(.lg\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 10) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 0) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 10) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 10) * calc(1 - var(--tw-space-x-reverse)))}.lg\:p-0{padding:calc(var(--spacing) * 0)}.lg\:p-1{padding:calc(var(--spacing) * 1)}.lg\:p-2{padding:calc(var(--spacing) * 2)}.lg\:p-3{padding:calc(var(--spacing) * 3)}.lg\:p-4{padding:calc(var(--spacing) * 4)}.lg\:p-5{padding:calc(var(--spacing) * 5)}.lg\:p-6{padding:calc(var(--spacing) * 6)}.lg\:p-8{padding:calc(var(--spacing) * 8)}.lg\:p-10{padding:calc(var(--spacing) * 10)}.lg\:p-12{padding:calc(var(--spacing) * 12)}.lg\:p-16{padding:calc(var(--spacing) * 16)}.lg\:px-0{padding-inline:calc(var(--spacing) * 0)}.lg\:px-1{padding-inline:calc(var(--spacing) * 1)}.lg\:px-2{padding-inline:calc(var(--spacing) * 2)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:px-4{padding-inline:calc(var(--spacing) * 4)}.lg\:px-5{padding-inline:calc(var(--spacing) * 5)}.lg\:px-6{padding-inline:calc(var(--spacing) * 6)}.lg\:px-8{padding-inline:calc(var(--spacing) * 8)}.lg\:px-10{padding-inline:calc(var(--spacing) * 10)}.lg\:px-12{padding-inline:calc(var(--spacing) * 12)}.lg\:px-16{padding-inline:calc(var(--spacing) * 16)}.lg\:py-0{padding-block:calc(var(--spacing) * 0)}.lg\:py-1{padding-block:calc(var(--spacing) * 1)}.lg\:py-2{padding-block:calc(var(--spacing) * 2)}.lg\:py-3{padding-block:calc(var(--spacing) * 3)}.lg\:py-4{padding-block:calc(var(--spacing) * 4)}.lg\:py-5{padding-block:calc(var(--spacing) * 5)}.lg\:py-6{padding-block:calc(var(--spacing) * 6)}.lg\:py-8{padding-block:calc(var(--spacing) * 8)}.lg\:py-10{padding-block:calc(var(--spacing) * 10)}.lg\:py-12{padding-block:calc(var(--spacing) * 12)}.lg\:py-16{padding-block:calc(var(--spacing) * 16)}.lg\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.lg\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.lg\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.lg\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.lg\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-3{grid-column:span 3/span 3}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-5{grid-column:span 5/span 5}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-full{grid-column:1/-1}.xl\:row-span-1{grid-row:span 1/span 1}.xl\:row-span-2{grid-row:span 2/span 2}.xl\:row-span-3{grid-row:span 3/span 3}.xl\:row-span-full{grid-row:1/-1}.xl\:m-0{margin:calc(var(--spacing) * 0)}.xl\:m-auto{margin:auto}.xl\:mx-auto{margin-inline:auto}.xl\:my-auto{margin-block:auto}.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:inline{display:inline}.xl\:inline-block{display:inline-block}.xl\:inline-flex{display:inline-flex}.xl\:w-1\/2{width:50%}.xl\:w-1\/3{width:33.3333%}.xl\:w-1\/4{width:25%}.xl\:w-1\/5{width:20%}.xl\:w-2\/3{width:66.6667%}.xl\:w-2\/5{width:40%}.xl\:w-3\/4{width:75%}.xl\:w-3\/5{width:60%}.xl\:w-4\/5{width:80%}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-2xl{max-width:var(--container-2xl)}.xl\:max-w-3xl{max-width:var(--container-3xl)}.xl\:max-w-4xl{max-width:var(--container-4xl)}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:max-w-6xl{max-width:var(--container-6xl)}.xl\:max-w-7xl{max-width:var(--container-7xl)}.xl\:max-w-full{max-width:100%}.xl\:max-w-lg{max-width:var(--container-lg)}.xl\:max-w-md{max-width:var(--container-md)}.xl\:max-w-none{max-width:none}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:max-w-xl{max-width:var(--container-xl)}.xl\:max-w-xs{max-width:var(--container-xs)}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.xl\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.xl\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.xl\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.xl\:flex-col{flex-direction:column}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-center{justify-content:center}.xl\:justify-end{justify-content:flex-end}.xl\:justify-start{justify-content:flex-start}.xl\:gap-0{gap:calc(var(--spacing) * 0)}.xl\:gap-1{gap:calc(var(--spacing) * 1)}.xl\:gap-2{gap:calc(var(--spacing) * 2)}.xl\:gap-3{gap:calc(var(--spacing) * 3)}.xl\:gap-4{gap:calc(var(--spacing) * 4)}.xl\:gap-5{gap:calc(var(--spacing) * 5)}.xl\:gap-6{gap:calc(var(--spacing) * 6)}.xl\:gap-8{gap:calc(var(--spacing) * 8)}.xl\:gap-10{gap:calc(var(--spacing) * 10)}.xl\:gap-12{gap:calc(var(--spacing) * 12)}.xl\:gap-16{gap:calc(var(--spacing) * 16)}:where(.xl\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 0) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 0) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 0) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 1) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 6) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 8) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-x-reverse)))}.xl\:p-0{padding:calc(var(--spacing) * 0)}.xl\:p-1{padding:calc(var(--spacing) * 1)}.xl\:p-2{padding:calc(var(--spacing) * 2)}.xl\:p-3{padding:calc(var(--spacing) * 3)}.xl\:p-4{padding:calc(var(--spacing) * 4)}.xl\:p-5{padding:calc(var(--spacing) * 5)}.xl\:p-6{padding:calc(var(--spacing) * 6)}.xl\:p-8{padding:calc(var(--spacing) * 8)}.xl\:p-10{padding:calc(var(--spacing) * 10)}.xl\:p-12{padding:calc(var(--spacing) * 12)}.xl\:p-16{padding:calc(var(--spacing) * 16)}.xl\:px-0{padding-inline:calc(var(--spacing) * 0)}.xl\:px-1{padding-inline:calc(var(--spacing) * 1)}.xl\:px-2{padding-inline:calc(var(--spacing) * 2)}.xl\:px-3{padding-inline:calc(var(--spacing) * 3)}.xl\:px-4{padding-inline:calc(var(--spacing) * 4)}.xl\:px-5{padding-inline:calc(var(--spacing) * 5)}.xl\:px-6{padding-inline:calc(var(--spacing) * 6)}.xl\:px-8{padding-inline:calc(var(--spacing) * 8)}.xl\:px-10{padding-inline:calc(var(--spacing) * 10)}.xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.xl\:px-16{padding-inline:calc(var(--spacing) * 16)}.xl\:py-0{padding-block:calc(var(--spacing) * 0)}.xl\:py-1{padding-block:calc(var(--spacing) * 1)}.xl\:py-2{padding-block:calc(var(--spacing) * 2)}.xl\:py-3{padding-block:calc(var(--spacing) * 3)}.xl\:py-4{padding-block:calc(var(--spacing) * 4)}.xl\:py-5{padding-block:calc(var(--spacing) * 5)}.xl\:py-6{padding-block:calc(var(--spacing) * 6)}.xl\:py-8{padding-block:calc(var(--spacing) * 8)}.xl\:py-10{padding-block:calc(var(--spacing) * 10)}.xl\:py-12{padding-block:calc(var(--spacing) * 12)}.xl\:py-16{padding-block:calc(var(--spacing) * 16)}.xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.xl\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:96rem){.\32xl\:col-span-1{grid-column:span 1/span 1}.\32xl\:col-span-2{grid-column:span 2/span 2}.\32xl\:col-span-3{grid-column:span 3/span 3}.\32xl\:col-span-4{grid-column:span 4/span 4}.\32xl\:col-span-6{grid-column:span 6/span 6}.\32xl\:col-span-12{grid-column:span 12/span 12}.\32xl\:block{display:block}.\32xl\:flex{display:flex}.\32xl\:hidden{display:none}.\32xl\:w-1\/2{width:50%}.\32xl\:w-1\/3{width:33.3333%}.\32xl\:w-1\/4{width:25%}.\32xl\:w-2\/3{width:66.6667%}.\32xl\:w-3\/4{width:75%}.\32xl\:w-auto{width:auto}.\32xl\:w-full{width:100%}.\32xl\:max-w-2xl{max-width:var(--container-2xl)}.\32xl\:max-w-3xl{max-width:var(--container-3xl)}.\32xl\:max-w-4xl{max-width:var(--container-4xl)}.\32xl\:max-w-5xl{max-width:var(--container-5xl)}.\32xl\:max-w-6xl{max-width:var(--container-6xl)}.\32xl\:max-w-7xl{max-width:var(--container-7xl)}.\32xl\:max-w-full{max-width:100%}.\32xl\:max-w-lg{max-width:var(--container-lg)}.\32xl\:max-w-md{max-width:var(--container-md)}.\32xl\:max-w-sm{max-width:var(--container-sm)}.\32xl\:max-w-xl{max-width:var(--container-xl)}.\32xl\:max-w-xs{max-width:var(--container-xs)}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:gap-0{gap:calc(var(--spacing) * 0)}.\32xl\:gap-4{gap:calc(var(--spacing) * 4)}.\32xl\:gap-6{gap:calc(var(--spacing) * 6)}.\32xl\:gap-8{gap:calc(var(--spacing) * 8)}.\32xl\:gap-12{gap:calc(var(--spacing) * 12)}.\32xl\:gap-16{gap:calc(var(--spacing) * 16)}.\32xl\:p-0{padding:calc(var(--spacing) * 0)}.\32xl\:p-4{padding:calc(var(--spacing) * 4)}.\32xl\:p-6{padding:calc(var(--spacing) * 6)}.\32xl\:p-8{padding:calc(var(--spacing) * 8)}.\32xl\:p-12{padding:calc(var(--spacing) * 12)}.\32xl\:p-16{padding:calc(var(--spacing) * 16)}.\32xl\:px-0{padding-inline:calc(var(--spacing) * 0)}.\32xl\:px-4{padding-inline:calc(var(--spacing) * 4)}.\32xl\:px-6{padding-inline:calc(var(--spacing) * 6)}.\32xl\:px-8{padding-inline:calc(var(--spacing) * 8)}.\32xl\:px-12{padding-inline:calc(var(--spacing) * 12)}.\32xl\:px-16{padding-inline:calc(var(--spacing) * 16)}.\32xl\:py-0{padding-block:calc(var(--spacing) * 0)}.\32xl\:py-4{padding-block:calc(var(--spacing) * 4)}.\32xl\:py-6{padding-block:calc(var(--spacing) * 6)}.\32xl\:py-8{padding-block:calc(var(--spacing) * 8)}.\32xl\:py-12{padding-block:calc(var(--spacing) * 12)}.\32xl\:py-16{padding-block:calc(var(--spacing) * 16)}.\32xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.\32xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.\32xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.\32xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.\32xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.\32xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.\32xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\32xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.\32xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.dark .dark\:block{display:block}.dark .dark\:flex{display:flex}.dark .dark\:flex-1{flex:1}.dark .dark\:flex-auto{flex:auto}.dark .dark\:flex-none{flex:none}.dark .dark\:flex-col{flex-direction:column}.dark .dark\:flex-row{flex-direction:row}.dark .dark\:flex-wrap{flex-wrap:wrap}.dark .dark\:items-center{align-items:center}.dark .dark\:items-end{align-items:flex-end}.dark .dark\:items-start{align-items:flex-start}.dark .dark\:items-stretch{align-items:stretch}.dark .dark\:justify-around{justify-content:space-around}.dark .dark\:justify-between{justify-content:space-between}.dark .dark\:justify-center{justify-content:center}.dark .dark\:justify-end{justify-content:flex-end}.dark .dark\:justify-start{justify-content:flex-start}:where(.dark .dark\:divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.dark .dark\:divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.dark .dark\:divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.dark .dark\:divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.dark .dark\:divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.dark .dark\:divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.dark .dark\:divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.dark .dark\:divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.dark .dark\:divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.dark .dark\:divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.dark .dark\:divide-purple-800>:not(:last-child)){border-color:var(--color-purple-800)}:where(.dark .dark\:divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}.dark .dark\:self-center{align-self:center}.dark .dark\:self-end{align-self:flex-end}.dark .dark\:self-start{align-self:flex-start}.dark .dark\:self-stretch{align-self:stretch}.dark .dark\:overflow-auto{overflow:auto}.dark .dark\:overflow-hidden{overflow:hidden}.dark .dark\:overflow-visible{overflow:visible}.dark .dark\:overflow-x-auto{overflow-x:auto}.dark .dark\:overflow-y-auto{overflow-y:auto}.dark .dark\:rounded{border-radius:.25rem}.dark .dark\:rounded-full{border-radius:3.40282e38px}.dark .dark\:rounded-lg{border-radius:var(--radius-lg)}.dark .dark\:rounded-none{border-radius:0}.dark .dark\:rounded-sm{border-radius:var(--radius-sm)}.dark .dark\:rounded-xl{border-radius:var(--radius-xl)}.dark .dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark .dark\:border-0{border-style:var(--tw-border-style);border-width:0}.dark .dark\:border-dashed{--tw-border-style:dashed;border-style:dashed}.dark .dark\:border-dotted{--tw-border-style:dotted;border-style:dotted}.dark .dark\:border-solid{--tw-border-style:solid;border-style:solid}.dark .dark\:border-amber-50{border-color:var(--color-amber-50)}.dark .dark\:border-amber-100{border-color:var(--color-amber-100)}.dark .dark\:border-amber-200{border-color:var(--color-amber-200)}.dark .dark\:border-amber-300{border-color:var(--color-amber-300)}.dark .dark\:border-amber-400{border-color:var(--color-amber-400)}.dark .dark\:border-amber-500{border-color:var(--color-amber-500)}.dark .dark\:border-amber-600{border-color:var(--color-amber-600)}.dark .dark\:border-amber-700{border-color:var(--color-amber-700)}.dark .dark\:border-amber-800{border-color:var(--color-amber-800)}.dark .dark\:border-amber-900{border-color:var(--color-amber-900)}.dark .dark\:border-black{border-color:var(--color-black)}.dark .dark\:border-blue-50{border-color:var(--color-blue-50)}.dark .dark\:border-blue-100{border-color:var(--color-blue-100)}.dark .dark\:border-blue-200{border-color:var(--color-blue-200)}.dark .dark\:border-blue-300{border-color:var(--color-blue-300)}.dark .dark\:border-blue-400{border-color:var(--color-blue-400)}.dark .dark\:border-blue-500{border-color:var(--color-blue-500)}.dark .dark\:border-blue-600{border-color:var(--color-blue-600)}.dark .dark\:border-blue-700{border-color:var(--color-blue-700)}.dark .dark\:border-blue-800{border-color:var(--color-blue-800)}.dark .dark\:border-blue-900{border-color:var(--color-blue-900)}.dark .dark\:border-gray-50{border-color:var(--color-gray-50)}.dark .dark\:border-gray-100{border-color:var(--color-gray-100)}.dark .dark\:border-gray-200{border-color:var(--color-gray-200)}.dark .dark\:border-gray-300{border-color:var(--color-gray-300)}.dark .dark\:border-gray-400{border-color:var(--color-gray-400)}.dark .dark\:border-gray-500{border-color:var(--color-gray-500)}.dark .dark\:border-gray-600{border-color:var(--color-gray-600)}.dark .dark\:border-gray-700{border-color:var(--color-gray-700)}.dark .dark\:border-gray-800{border-color:var(--color-gray-800)}.dark .dark\:border-gray-900{border-color:var(--color-gray-900)}.dark .dark\:border-gray-950{border-color:var(--color-gray-950)}.dark .dark\:border-green-50{border-color:var(--color-green-50)}.dark .dark\:border-green-100{border-color:var(--color-green-100)}.dark .dark\:border-green-200{border-color:var(--color-green-200)}.dark .dark\:border-green-300{border-color:var(--color-green-300)}.dark .dark\:border-green-400{border-color:var(--color-green-400)}.dark .dark\:border-green-500{border-color:var(--color-green-500)}.dark .dark\:border-green-600{border-color:var(--color-green-600)}.dark .dark\:border-green-700{border-color:var(--color-green-700)}.dark .dark\:border-green-800{border-color:var(--color-green-800)}.dark .dark\:border-green-900{border-color:var(--color-green-900)}.dark .dark\:border-indigo-400{border-color:var(--color-indigo-400)}.dark .dark\:border-orange-50{border-color:var(--color-orange-50)}.dark .dark\:border-orange-100{border-color:var(--color-orange-100)}.dark .dark\:border-orange-200{border-color:var(--color-orange-200)}.dark .dark\:border-orange-300{border-color:var(--color-orange-300)}.dark .dark\:border-orange-400{border-color:var(--color-orange-400)}.dark .dark\:border-orange-500{border-color:var(--color-orange-500)}.dark .dark\:border-orange-600{border-color:var(--color-orange-600)}.dark .dark\:border-orange-700{border-color:var(--color-orange-700)}.dark .dark\:border-orange-800{border-color:var(--color-orange-800)}.dark .dark\:border-orange-900{border-color:var(--color-orange-900)}.dark .dark\:border-pink-50{border-color:var(--color-pink-50)}.dark .dark\:border-pink-100{border-color:var(--color-pink-100)}.dark .dark\:border-pink-200{border-color:var(--color-pink-200)}.dark .dark\:border-pink-300{border-color:var(--color-pink-300)}.dark .dark\:border-pink-400{border-color:var(--color-pink-400)}.dark .dark\:border-pink-500{border-color:var(--color-pink-500)}.dark .dark\:border-pink-600{border-color:var(--color-pink-600)}.dark .dark\:border-pink-700{border-color:var(--color-pink-700)}.dark .dark\:border-pink-800{border-color:var(--color-pink-800)}.dark .dark\:border-pink-900{border-color:var(--color-pink-900)}.dark .dark\:border-purple-50{border-color:var(--color-purple-50)}.dark .dark\:border-purple-100{border-color:var(--color-purple-100)}.dark .dark\:border-purple-200{border-color:var(--color-purple-200)}.dark .dark\:border-purple-300{border-color:var(--color-purple-300)}.dark .dark\:border-purple-400{border-color:var(--color-purple-400)}.dark .dark\:border-purple-500{border-color:var(--color-purple-500)}.dark .dark\:border-purple-600{border-color:var(--color-purple-600)}.dark .dark\:border-purple-700{border-color:var(--color-purple-700)}.dark .dark\:border-purple-800{border-color:var(--color-purple-800)}.dark .dark\:border-purple-900{border-color:var(--color-purple-900)}.dark .dark\:border-red-50{border-color:var(--color-red-50)}.dark .dark\:border-red-100{border-color:var(--color-red-100)}.dark .dark\:border-red-200{border-color:var(--color-red-200)}.dark .dark\:border-red-300{border-color:var(--color-red-300)}.dark .dark\:border-red-400{border-color:var(--color-red-400)}.dark .dark\:border-red-500{border-color:var(--color-red-500)}.dark .dark\:border-red-600{border-color:var(--color-red-600)}.dark .dark\:border-red-700{border-color:var(--color-red-700)}.dark .dark\:border-red-800{border-color:var(--color-red-800)}.dark .dark\:border-red-900{border-color:var(--color-red-900)}.dark .dark\:border-slate-50{border-color:var(--color-slate-50)}.dark .dark\:border-slate-100{border-color:var(--color-slate-100)}.dark .dark\:border-slate-200{border-color:var(--color-slate-200)}.dark .dark\:border-slate-300{border-color:var(--color-slate-300)}.dark .dark\:border-slate-400{border-color:var(--color-slate-400)}.dark .dark\:border-slate-500{border-color:var(--color-slate-500)}.dark .dark\:border-slate-600{border-color:var(--color-slate-600)}.dark .dark\:border-slate-700{border-color:var(--color-slate-700)}.dark .dark\:border-slate-800{border-color:var(--color-slate-800)}.dark .dark\:border-slate-900{border-color:var(--color-slate-900)}.dark .dark\:border-slate-950{border-color:var(--color-slate-950)}.dark .dark\:border-transparent{border-color:#0000}.dark .dark\:border-white{border-color:var(--color-white)}.dark .dark\:border-yellow-50{border-color:var(--color-yellow-50)}.dark .dark\:border-yellow-100{border-color:var(--color-yellow-100)}.dark .dark\:border-yellow-200{border-color:var(--color-yellow-200)}.dark .dark\:border-yellow-300{border-color:var(--color-yellow-300)}.dark .dark\:border-yellow-400{border-color:var(--color-yellow-400)}.dark .dark\:border-yellow-500{border-color:var(--color-yellow-500)}.dark .dark\:border-yellow-600{border-color:var(--color-yellow-600)}.dark .dark\:border-yellow-700{border-color:var(--color-yellow-700)}.dark .dark\:border-yellow-800{border-color:var(--color-yellow-800)}.dark .dark\:border-yellow-900{border-color:var(--color-yellow-900)}.dark .dark\:bg-amber-50{background-color:var(--color-amber-50)}.dark .dark\:bg-amber-100{background-color:var(--color-amber-100)}.dark .dark\:bg-amber-200{background-color:var(--color-amber-200)}.dark .dark\:bg-amber-300{background-color:var(--color-amber-300)}.dark .dark\:bg-amber-400{background-color:var(--color-amber-400)}.dark .dark\:bg-amber-500{background-color:var(--color-amber-500)}.dark .dark\:bg-amber-600{background-color:var(--color-amber-600)}.dark .dark\:bg-amber-700{background-color:var(--color-amber-700)}.dark .dark\:bg-amber-800{background-color:var(--color-amber-800)}.dark .dark\:bg-amber-900{background-color:var(--color-amber-900)}.dark .dark\:bg-amber-900\/20{background-color:#7b330633}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/20{background-color:color-mix(in oklab,var(--color-amber-900) 20%,transparent)}}.dark .dark\:bg-amber-900\/30{background-color:#7b33064d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/30{background-color:color-mix(in oklab,var(--color-amber-900) 30%,transparent)}}.dark .dark\:bg-amber-900\/40{background-color:#7b330666}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/40{background-color:color-mix(in oklab,var(--color-amber-900) 40%,transparent)}}.dark .dark\:bg-amber-950{background-color:var(--color-amber-950)}.dark .dark\:bg-black{background-color:var(--color-black)}.dark .dark\:bg-blue-50{background-color:var(--color-blue-50)}.dark .dark\:bg-blue-100{background-color:var(--color-blue-100)}.dark .dark\:bg-blue-200{background-color:var(--color-blue-200)}.dark .dark\:bg-blue-300{background-color:var(--color-blue-300)}.dark .dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark .dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark .dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark .dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark .dark\:bg-blue-800{background-color:var(--color-blue-800)}.dark .dark\:bg-blue-900{background-color:var(--color-blue-900)}.dark .dark\:bg-blue-900\/20{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/20{background-color:color-mix(in oklab,var(--color-blue-900) 20%,transparent)}}.dark .dark\:bg-blue-900\/30{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/30{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .dark\:bg-blue-900\/40{background-color:#1c398e66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/40{background-color:color-mix(in oklab,var(--color-blue-900) 40%,transparent)}}.dark .dark\:bg-blue-900\/50{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/50{background-color:color-mix(in oklab,var(--color-blue-900) 50%,transparent)}}.dark .dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark .dark\:bg-cyan-50{background-color:var(--color-cyan-50)}.dark .dark\:bg-cyan-100{background-color:var(--color-cyan-100)}.dark .dark\:bg-cyan-200{background-color:var(--color-cyan-200)}.dark .dark\:bg-cyan-300{background-color:var(--color-cyan-300)}.dark .dark\:bg-cyan-400{background-color:var(--color-cyan-400)}.dark .dark\:bg-cyan-500{background-color:var(--color-cyan-500)}.dark .dark\:bg-cyan-600{background-color:var(--color-cyan-600)}.dark .dark\:bg-cyan-700{background-color:var(--color-cyan-700)}.dark .dark\:bg-cyan-800{background-color:var(--color-cyan-800)}.dark .dark\:bg-cyan-900{background-color:var(--color-cyan-900)}.dark .dark\:bg-cyan-900\/30{background-color:#104e644d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-cyan-900\/30{background-color:color-mix(in oklab,var(--color-cyan-900) 30%,transparent)}}.dark .dark\:bg-cyan-950{background-color:var(--color-cyan-950)}.dark .dark\:bg-emerald-50{background-color:var(--color-emerald-50)}.dark .dark\:bg-emerald-100{background-color:var(--color-emerald-100)}.dark .dark\:bg-emerald-200{background-color:var(--color-emerald-200)}.dark .dark\:bg-emerald-300{background-color:var(--color-emerald-300)}.dark .dark\:bg-emerald-400{background-color:var(--color-emerald-400)}.dark .dark\:bg-emerald-500{background-color:var(--color-emerald-500)}.dark .dark\:bg-emerald-600{background-color:var(--color-emerald-600)}.dark .dark\:bg-emerald-700{background-color:var(--color-emerald-700)}.dark .dark\:bg-emerald-800{background-color:var(--color-emerald-800)}.dark .dark\:bg-emerald-900{background-color:var(--color-emerald-900)}.dark .dark\:bg-emerald-950{background-color:var(--color-emerald-950)}.dark .dark\:bg-gray-50{background-color:var(--color-gray-50)}.dark .dark\:bg-gray-100{background-color:var(--color-gray-100)}.dark .dark\:bg-gray-200{background-color:var(--color-gray-200)}.dark .dark\:bg-gray-300{background-color:var(--color-gray-300)}.dark .dark\:bg-gray-400{background-color:var(--color-gray-400)}.dark .dark\:bg-gray-500{background-color:var(--color-gray-500)}.dark .dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark .dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark .dark\:bg-gray-700\/30{background-color:#3641534d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/30{background-color:color-mix(in oklab,var(--color-gray-700) 30%,transparent)}}.dark .dark\:bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/50{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.dark .dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark .dark\:bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.dark .dark\:bg-gray-800\/90{background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/90{background-color:color-mix(in oklab,var(--color-gray-800) 90%,transparent)}}.dark .dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark .dark\:bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900) 50%,transparent)}}.dark .dark\:bg-gray-950{background-color:var(--color-gray-950)}.dark .dark\:bg-green-50{background-color:var(--color-green-50)}.dark .dark\:bg-green-100{background-color:var(--color-green-100)}.dark .dark\:bg-green-200{background-color:var(--color-green-200)}.dark .dark\:bg-green-300{background-color:var(--color-green-300)}.dark .dark\:bg-green-400{background-color:var(--color-green-400)}.dark .dark\:bg-green-500{background-color:var(--color-green-500)}.dark .dark\:bg-green-600{background-color:var(--color-green-600)}.dark .dark\:bg-green-700{background-color:var(--color-green-700)}.dark .dark\:bg-green-800{background-color:var(--color-green-800)}.dark .dark\:bg-green-900{background-color:var(--color-green-900)}.dark .dark\:bg-green-900\/20{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/20{background-color:color-mix(in oklab,var(--color-green-900) 20%,transparent)}}.dark .dark\:bg-green-900\/30{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/30{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .dark\:bg-green-950{background-color:var(--color-green-950)}.dark .dark\:bg-indigo-50{background-color:var(--color-indigo-50)}.dark .dark\:bg-indigo-100{background-color:var(--color-indigo-100)}.dark .dark\:bg-indigo-200{background-color:var(--color-indigo-200)}.dark .dark\:bg-indigo-300{background-color:var(--color-indigo-300)}.dark .dark\:bg-indigo-400{background-color:var(--color-indigo-400)}.dark .dark\:bg-indigo-500{background-color:var(--color-indigo-500)}.dark .dark\:bg-indigo-600{background-color:var(--color-indigo-600)}.dark .dark\:bg-indigo-700{background-color:var(--color-indigo-700)}.dark .dark\:bg-indigo-800{background-color:var(--color-indigo-800)}.dark .dark\:bg-indigo-900{background-color:var(--color-indigo-900)}.dark .dark\:bg-indigo-900\/30{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-indigo-900\/30{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .dark\:bg-indigo-950{background-color:var(--color-indigo-950)}.dark .dark\:bg-orange-50{background-color:var(--color-orange-50)}.dark .dark\:bg-orange-100{background-color:var(--color-orange-100)}.dark .dark\:bg-orange-200{background-color:var(--color-orange-200)}.dark .dark\:bg-orange-300{background-color:var(--color-orange-300)}.dark .dark\:bg-orange-400{background-color:var(--color-orange-400)}.dark .dark\:bg-orange-500{background-color:var(--color-orange-500)}.dark .dark\:bg-orange-600{background-color:var(--color-orange-600)}.dark .dark\:bg-orange-700{background-color:var(--color-orange-700)}.dark .dark\:bg-orange-800{background-color:var(--color-orange-800)}.dark .dark\:bg-orange-900{background-color:var(--color-orange-900)}.dark .dark\:bg-orange-900\/20{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/20{background-color:color-mix(in oklab,var(--color-orange-900) 20%,transparent)}}.dark .dark\:bg-orange-900\/30{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/30{background-color:color-mix(in oklab,var(--color-orange-900) 30%,transparent)}}.dark .dark\:bg-orange-950{background-color:var(--color-orange-950)}.dark .dark\:bg-pink-50{background-color:var(--color-pink-50)}.dark .dark\:bg-pink-100{background-color:var(--color-pink-100)}.dark .dark\:bg-pink-200{background-color:var(--color-pink-200)}.dark .dark\:bg-pink-300{background-color:var(--color-pink-300)}.dark .dark\:bg-pink-400{background-color:var(--color-pink-400)}.dark .dark\:bg-pink-500{background-color:var(--color-pink-500)}.dark .dark\:bg-pink-600{background-color:var(--color-pink-600)}.dark .dark\:bg-pink-700{background-color:var(--color-pink-700)}.dark .dark\:bg-pink-800{background-color:var(--color-pink-800)}.dark .dark\:bg-pink-900{background-color:var(--color-pink-900)}.dark .dark\:bg-pink-950{background-color:var(--color-pink-950)}.dark .dark\:bg-purple-50{background-color:var(--color-purple-50)}.dark .dark\:bg-purple-100{background-color:var(--color-purple-100)}.dark .dark\:bg-purple-200{background-color:var(--color-purple-200)}.dark .dark\:bg-purple-300{background-color:var(--color-purple-300)}.dark .dark\:bg-purple-400{background-color:var(--color-purple-400)}.dark .dark\:bg-purple-500{background-color:var(--color-purple-500)}.dark .dark\:bg-purple-600{background-color:var(--color-purple-600)}.dark .dark\:bg-purple-700{background-color:var(--color-purple-700)}.dark .dark\:bg-purple-800{background-color:var(--color-purple-800)}.dark .dark\:bg-purple-800\/50{background-color:#6e11b080}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-800\/50{background-color:color-mix(in oklab,var(--color-purple-800) 50%,transparent)}}.dark .dark\:bg-purple-900{background-color:var(--color-purple-900)}.dark .dark\:bg-purple-900\/20{background-color:#59168b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/20{background-color:color-mix(in oklab,var(--color-purple-900) 20%,transparent)}}.dark .dark\:bg-purple-900\/30{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/30{background-color:color-mix(in oklab,var(--color-purple-900) 30%,transparent)}}.dark .dark\:bg-purple-900\/50{background-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/50{background-color:color-mix(in oklab,var(--color-purple-900) 50%,transparent)}}.dark .dark\:bg-purple-950{background-color:var(--color-purple-950)}.dark .dark\:bg-red-50{background-color:var(--color-red-50)}.dark .dark\:bg-red-100{background-color:var(--color-red-100)}.dark .dark\:bg-red-200{background-color:var(--color-red-200)}.dark .dark\:bg-red-300{background-color:var(--color-red-300)}.dark .dark\:bg-red-400{background-color:var(--color-red-400)}.dark .dark\:bg-red-500{background-color:var(--color-red-500)}.dark .dark\:bg-red-600{background-color:var(--color-red-600)}.dark .dark\:bg-red-600\/50{background-color:#e4001480}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-600\/50{background-color:color-mix(in oklab,var(--color-red-600) 50%,transparent)}}.dark .dark\:bg-red-700{background-color:var(--color-red-700)}.dark .dark\:bg-red-800{background-color:var(--color-red-800)}.dark .dark\:bg-red-900{background-color:var(--color-red-900)}.dark .dark\:bg-red-900\/20{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/20{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .dark\:bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .dark\:bg-red-950{background-color:var(--color-red-950)}.dark .dark\:bg-rose-50{background-color:var(--color-rose-50)}.dark .dark\:bg-rose-100{background-color:var(--color-rose-100)}.dark .dark\:bg-rose-200{background-color:var(--color-rose-200)}.dark .dark\:bg-rose-300{background-color:var(--color-rose-300)}.dark .dark\:bg-rose-400{background-color:var(--color-rose-400)}.dark .dark\:bg-rose-500{background-color:var(--color-rose-500)}.dark .dark\:bg-rose-600{background-color:var(--color-rose-600)}.dark .dark\:bg-rose-700{background-color:var(--color-rose-700)}.dark .dark\:bg-rose-800{background-color:var(--color-rose-800)}.dark .dark\:bg-rose-900{background-color:var(--color-rose-900)}.dark .dark\:bg-rose-950{background-color:var(--color-rose-950)}.dark .dark\:bg-slate-50{background-color:var(--color-slate-50)}.dark .dark\:bg-slate-100{background-color:var(--color-slate-100)}.dark .dark\:bg-slate-200{background-color:var(--color-slate-200)}.dark .dark\:bg-slate-300{background-color:var(--color-slate-300)}.dark .dark\:bg-slate-400{background-color:var(--color-slate-400)}.dark .dark\:bg-slate-500{background-color:var(--color-slate-500)}.dark .dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark .dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark .dark\:bg-slate-700\/50{background-color:#31415880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-slate-700\/50{background-color:color-mix(in oklab,var(--color-slate-700) 50%,transparent)}}.dark .dark\:bg-slate-800{background-color:var(--color-slate-800)}.dark .dark\:bg-slate-900{background-color:var(--color-slate-900)}.dark .dark\:bg-slate-950{background-color:var(--color-slate-950)}.dark .dark\:bg-teal-50{background-color:var(--color-teal-50)}.dark .dark\:bg-teal-100{background-color:var(--color-teal-100)}.dark .dark\:bg-teal-200{background-color:var(--color-teal-200)}.dark .dark\:bg-teal-300{background-color:var(--color-teal-300)}.dark .dark\:bg-teal-400{background-color:var(--color-teal-400)}.dark .dark\:bg-teal-500{background-color:var(--color-teal-500)}.dark .dark\:bg-teal-600{background-color:var(--color-teal-600)}.dark .dark\:bg-teal-700{background-color:var(--color-teal-700)}.dark .dark\:bg-teal-800{background-color:var(--color-teal-800)}.dark .dark\:bg-teal-900{background-color:var(--color-teal-900)}.dark .dark\:bg-teal-900\/30{background-color:#0b4f4a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-teal-900\/30{background-color:color-mix(in oklab,var(--color-teal-900) 30%,transparent)}}.dark .dark\:bg-teal-950{background-color:var(--color-teal-950)}.dark .dark\:bg-white{background-color:var(--color-white)}.dark .dark\:bg-yellow-50{background-color:var(--color-yellow-50)}.dark .dark\:bg-yellow-100{background-color:var(--color-yellow-100)}.dark .dark\:bg-yellow-200{background-color:var(--color-yellow-200)}.dark .dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark .dark\:bg-yellow-400{background-color:var(--color-yellow-400)}.dark .dark\:bg-yellow-500{background-color:var(--color-yellow-500)}.dark .dark\:bg-yellow-600{background-color:var(--color-yellow-600)}.dark .dark\:bg-yellow-700{background-color:var(--color-yellow-700)}.dark .dark\:bg-yellow-800{background-color:var(--color-yellow-800)}.dark .dark\:bg-yellow-900{background-color:var(--color-yellow-900)}.dark .dark\:bg-yellow-900\/20{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/20{background-color:color-mix(in oklab,var(--color-yellow-900) 20%,transparent)}}.dark .dark\:bg-yellow-900\/30{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/30{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .dark\:bg-yellow-900\/40{background-color:#733e0a66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/40{background-color:color-mix(in oklab,var(--color-yellow-900) 40%,transparent)}}.dark .dark\:bg-yellow-950{background-color:var(--color-yellow-950)}.dark .dark\:from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:to-indigo-900{--tw-gradient-to:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.dark .dark\:text-center{text-align:center}.dark .dark\:text-justify{text-align:justify}.dark .dark\:text-left{text-align:left}.dark .dark\:text-right{text-align:right}.dark .dark\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.dark .dark\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.dark .dark\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.dark .dark\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.dark .dark\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.dark .dark\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.dark .dark\:font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dark .dark\:font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.dark .dark\:font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.dark .dark\:whitespace-normal{white-space:normal}.dark .dark\:whitespace-nowrap{white-space:nowrap}.dark .dark\:whitespace-pre-wrap{white-space:pre-wrap}.dark .dark\:text-amber-50{color:var(--color-amber-50)}.dark .dark\:text-amber-100{color:var(--color-amber-100)}.dark .dark\:text-amber-200{color:var(--color-amber-200)}.dark .dark\:text-amber-300{color:var(--color-amber-300)}.dark .dark\:text-amber-400{color:var(--color-amber-400)}.dark .dark\:text-amber-500{color:var(--color-amber-500)}.dark .dark\:text-amber-600{color:var(--color-amber-600)}.dark .dark\:text-amber-700{color:var(--color-amber-700)}.dark .dark\:text-amber-800{color:var(--color-amber-800)}.dark .dark\:text-amber-900{color:var(--color-amber-900)}.dark .dark\:text-black{color:var(--color-black)}.dark .dark\:text-blue-50{color:var(--color-blue-50)}.dark .dark\:text-blue-100{color:var(--color-blue-100)}.dark .dark\:text-blue-200{color:var(--color-blue-200)}.dark .dark\:text-blue-300{color:var(--color-blue-300)}.dark .dark\:text-blue-400{color:var(--color-blue-400)}.dark .dark\:text-blue-500{color:var(--color-blue-500)}.dark .dark\:text-blue-600{color:var(--color-blue-600)}.dark .dark\:text-blue-700{color:var(--color-blue-700)}.dark .dark\:text-blue-800{color:var(--color-blue-800)}.dark .dark\:text-blue-900{color:var(--color-blue-900)}.dark .dark\:text-cyan-50{color:var(--color-cyan-50)}.dark .dark\:text-cyan-100{color:var(--color-cyan-100)}.dark .dark\:text-cyan-200{color:var(--color-cyan-200)}.dark .dark\:text-cyan-300{color:var(--color-cyan-300)}.dark .dark\:text-cyan-400{color:var(--color-cyan-400)}.dark .dark\:text-cyan-500{color:var(--color-cyan-500)}.dark .dark\:text-cyan-600{color:var(--color-cyan-600)}.dark .dark\:text-cyan-700{color:var(--color-cyan-700)}.dark .dark\:text-cyan-800{color:var(--color-cyan-800)}.dark .dark\:text-cyan-900{color:var(--color-cyan-900)}.dark .dark\:text-emerald-50{color:var(--color-emerald-50)}.dark .dark\:text-emerald-100{color:var(--color-emerald-100)}.dark .dark\:text-emerald-200{color:var(--color-emerald-200)}.dark .dark\:text-emerald-300{color:var(--color-emerald-300)}.dark .dark\:text-emerald-400{color:var(--color-emerald-400)}.dark .dark\:text-emerald-500{color:var(--color-emerald-500)}.dark .dark\:text-emerald-600{color:var(--color-emerald-600)}.dark .dark\:text-emerald-700{color:var(--color-emerald-700)}.dark .dark\:text-emerald-800{color:var(--color-emerald-800)}.dark .dark\:text-emerald-900{color:var(--color-emerald-900)}.dark .dark\:text-gray-50{color:var(--color-gray-50)}.dark .dark\:text-gray-100{color:var(--color-gray-100)}.dark .dark\:text-gray-200{color:var(--color-gray-200)}.dark .dark\:text-gray-300{color:var(--color-gray-300)}.dark .dark\:text-gray-400{color:var(--color-gray-400)}.dark .dark\:text-gray-500{color:var(--color-gray-500)}.dark .dark\:text-gray-600{color:var(--color-gray-600)}.dark .dark\:text-gray-700{color:var(--color-gray-700)}.dark .dark\:text-gray-800{color:var(--color-gray-800)}.dark .dark\:text-gray-900{color:var(--color-gray-900)}.dark .dark\:text-green-50{color:var(--color-green-50)}.dark .dark\:text-green-100{color:var(--color-green-100)}.dark .dark\:text-green-200{color:var(--color-green-200)}.dark .dark\:text-green-300{color:var(--color-green-300)}.dark .dark\:text-green-400{color:var(--color-green-400)}.dark .dark\:text-green-500{color:var(--color-green-500)}.dark .dark\:text-green-600{color:var(--color-green-600)}.dark .dark\:text-green-700{color:var(--color-green-700)}.dark .dark\:text-green-800{color:var(--color-green-800)}.dark .dark\:text-green-900{color:var(--color-green-900)}.dark .dark\:text-indigo-50{color:var(--color-indigo-50)}.dark .dark\:text-indigo-100{color:var(--color-indigo-100)}.dark .dark\:text-indigo-200{color:var(--color-indigo-200)}.dark .dark\:text-indigo-300{color:var(--color-indigo-300)}.dark .dark\:text-indigo-400{color:var(--color-indigo-400)}.dark .dark\:text-indigo-500{color:var(--color-indigo-500)}.dark .dark\:text-indigo-600{color:var(--color-indigo-600)}.dark .dark\:text-indigo-700{color:var(--color-indigo-700)}.dark .dark\:text-indigo-800{color:var(--color-indigo-800)}.dark .dark\:text-indigo-900{color:var(--color-indigo-900)}.dark .dark\:text-orange-50{color:var(--color-orange-50)}.dark .dark\:text-orange-100{color:var(--color-orange-100)}.dark .dark\:text-orange-200{color:var(--color-orange-200)}.dark .dark\:text-orange-300{color:var(--color-orange-300)}.dark .dark\:text-orange-400{color:var(--color-orange-400)}.dark .dark\:text-orange-500{color:var(--color-orange-500)}.dark .dark\:text-orange-600{color:var(--color-orange-600)}.dark .dark\:text-orange-700{color:var(--color-orange-700)}.dark .dark\:text-orange-800{color:var(--color-orange-800)}.dark .dark\:text-orange-900{color:var(--color-orange-900)}.dark .dark\:text-pink-50{color:var(--color-pink-50)}.dark .dark\:text-pink-100{color:var(--color-pink-100)}.dark .dark\:text-pink-200{color:var(--color-pink-200)}.dark .dark\:text-pink-300{color:var(--color-pink-300)}.dark .dark\:text-pink-400{color:var(--color-pink-400)}.dark .dark\:text-pink-500{color:var(--color-pink-500)}.dark .dark\:text-pink-600{color:var(--color-pink-600)}.dark .dark\:text-pink-700{color:var(--color-pink-700)}.dark .dark\:text-pink-800{color:var(--color-pink-800)}.dark .dark\:text-pink-900{color:var(--color-pink-900)}.dark .dark\:text-purple-50{color:var(--color-purple-50)}.dark .dark\:text-purple-100{color:var(--color-purple-100)}.dark .dark\:text-purple-200{color:var(--color-purple-200)}.dark .dark\:text-purple-300{color:var(--color-purple-300)}.dark .dark\:text-purple-400{color:var(--color-purple-400)}.dark .dark\:text-purple-500{color:var(--color-purple-500)}.dark .dark\:text-purple-600{color:var(--color-purple-600)}.dark .dark\:text-purple-700{color:var(--color-purple-700)}.dark .dark\:text-purple-800{color:var(--color-purple-800)}.dark .dark\:text-purple-900{color:var(--color-purple-900)}.dark .dark\:text-red-50{color:var(--color-red-50)}.dark .dark\:text-red-100{color:var(--color-red-100)}.dark .dark\:text-red-200{color:var(--color-red-200)}.dark .dark\:text-red-300{color:var(--color-red-300)}.dark .dark\:text-red-400{color:var(--color-red-400)}.dark .dark\:text-red-500{color:var(--color-red-500)}.dark .dark\:text-red-600{color:var(--color-red-600)}.dark .dark\:text-red-700{color:var(--color-red-700)}.dark .dark\:text-red-800{color:var(--color-red-800)}.dark .dark\:text-red-900{color:var(--color-red-900)}.dark .dark\:text-slate-50{color:var(--color-slate-50)}.dark .dark\:text-slate-100{color:var(--color-slate-100)}.dark .dark\:text-slate-200{color:var(--color-slate-200)}.dark .dark\:text-slate-300{color:var(--color-slate-300)}.dark .dark\:text-slate-400{color:var(--color-slate-400)}.dark .dark\:text-slate-500{color:var(--color-slate-500)}.dark .dark\:text-slate-600{color:var(--color-slate-600)}.dark .dark\:text-slate-700{color:var(--color-slate-700)}.dark .dark\:text-slate-800{color:var(--color-slate-800)}.dark .dark\:text-slate-900{color:var(--color-slate-900)}.dark .dark\:text-teal-50{color:var(--color-teal-50)}.dark .dark\:text-teal-100{color:var(--color-teal-100)}.dark .dark\:text-teal-200{color:var(--color-teal-200)}.dark .dark\:text-teal-300{color:var(--color-teal-300)}.dark .dark\:text-teal-400{color:var(--color-teal-400)}.dark .dark\:text-teal-500{color:var(--color-teal-500)}.dark .dark\:text-teal-600{color:var(--color-teal-600)}.dark .dark\:text-teal-700{color:var(--color-teal-700)}.dark .dark\:text-teal-800{color:var(--color-teal-800)}.dark .dark\:text-teal-900{color:var(--color-teal-900)}.dark .dark\:text-white{color:var(--color-white)}.dark .dark\:text-yellow-50{color:var(--color-yellow-50)}.dark .dark\:text-yellow-100{color:var(--color-yellow-100)}.dark .dark\:text-yellow-200{color:var(--color-yellow-200)}.dark .dark\:text-yellow-300{color:var(--color-yellow-300)}.dark .dark\:text-yellow-400{color:var(--color-yellow-400)}.dark .dark\:text-yellow-500{color:var(--color-yellow-500)}.dark .dark\:text-yellow-600{color:var(--color-yellow-600)}.dark .dark\:text-yellow-700{color:var(--color-yellow-700)}.dark .dark\:text-yellow-800{color:var(--color-yellow-800)}.dark .dark\:text-yellow-900{color:var(--color-yellow-900)}.dark .dark\:italic{font-style:italic}.dark .dark\:underline{text-decoration-line:underline}.dark .dark\:placeholder-gray-400::placeholder{color:var(--color-gray-400)}.dark .dark\:placeholder-gray-500::placeholder{color:var(--color-gray-500)}.dark .dark\:placeholder-gray-600::placeholder{color:var(--color-gray-600)}.dark .dark\:opacity-0{opacity:0}.dark .dark\:opacity-25{opacity:.25}.dark .dark\:opacity-50{opacity:.5}.dark .dark\:opacity-75{opacity:.75}.dark .dark\:opacity-100{opacity:1}.dark .dark\:shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-purple-900\/50{--tw-shadow-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:shadow-purple-900\/50{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-purple-900) 50%, transparent) var(--tw-shadow-alpha), transparent)}}.dark .dark\:ring-blue-50{--tw-ring-color:var(--color-blue-50)}.dark .dark\:ring-blue-100{--tw-ring-color:var(--color-blue-100)}.dark .dark\:ring-blue-200{--tw-ring-color:var(--color-blue-200)}.dark .dark\:ring-blue-300{--tw-ring-color:var(--color-blue-300)}.dark .dark\:ring-blue-400{--tw-ring-color:var(--color-blue-400)}.dark .dark\:ring-blue-500{--tw-ring-color:var(--color-blue-500)}.dark .dark\:ring-blue-600{--tw-ring-color:var(--color-blue-600)}.dark .dark\:ring-blue-700{--tw-ring-color:var(--color-blue-700)}.dark .dark\:ring-blue-800{--tw-ring-color:var(--color-blue-800)}.dark .dark\:ring-blue-900{--tw-ring-color:var(--color-blue-900)}.dark .dark\:ring-gray-50{--tw-ring-color:var(--color-gray-50)}.dark .dark\:ring-gray-100{--tw-ring-color:var(--color-gray-100)}.dark .dark\:ring-gray-200{--tw-ring-color:var(--color-gray-200)}.dark .dark\:ring-gray-300{--tw-ring-color:var(--color-gray-300)}.dark .dark\:ring-gray-400{--tw-ring-color:var(--color-gray-400)}.dark .dark\:ring-gray-500{--tw-ring-color:var(--color-gray-500)}.dark .dark\:ring-gray-600{--tw-ring-color:var(--color-gray-600)}.dark .dark\:ring-gray-700{--tw-ring-color:var(--color-gray-700)}.dark .dark\:ring-gray-800{--tw-ring-color:var(--color-gray-800)}.dark .dark\:ring-gray-900{--tw-ring-color:var(--color-gray-900)}.dark .dark\:ring-offset-gray-800{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:ring-offset-gray-900{--tw-ring-offset-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.dark .dark\:group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.dark .dark\:group-hover\:text-gray-100:is(:where(.group):hover *){color:var(--color-gray-100)}.dark .dark\:group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.dark .dark\:selection\:bg-blue-800 ::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:bg-blue-800::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:text-blue-100 ::selection{color:var(--color-blue-100)}.dark .dark\:selection\:text-blue-100::selection{color:var(--color-blue-100)}.dark .dark\:odd\:bg-gray-800:nth-child(odd){background-color:var(--color-gray-800)}.dark .dark\:odd\:bg-gray-900:nth-child(odd){background-color:var(--color-gray-900)}.dark .dark\:even\:bg-gray-800:nth-child(2n){background-color:var(--color-gray-800)}.dark .dark\:even\:bg-gray-900:nth-child(2n){background-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:hover\:border-amber-50:hover{border-color:var(--color-amber-50)}.dark .dark\:hover\:border-amber-100:hover{border-color:var(--color-amber-100)}.dark .dark\:hover\:border-amber-200:hover{border-color:var(--color-amber-200)}.dark .dark\:hover\:border-amber-300:hover{border-color:var(--color-amber-300)}.dark .dark\:hover\:border-amber-400:hover{border-color:var(--color-amber-400)}.dark .dark\:hover\:border-amber-500:hover{border-color:var(--color-amber-500)}.dark .dark\:hover\:border-amber-600:hover{border-color:var(--color-amber-600)}.dark .dark\:hover\:border-amber-700:hover{border-color:var(--color-amber-700)}.dark .dark\:hover\:border-amber-800:hover{border-color:var(--color-amber-800)}.dark .dark\:hover\:border-amber-900:hover{border-color:var(--color-amber-900)}.dark .dark\:hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.dark .dark\:hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.dark .dark\:hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.dark .dark\:hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.dark .dark\:hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.dark .dark\:hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.dark .dark\:hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.dark .dark\:hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.dark .dark\:hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.dark .dark\:hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.dark .dark\:hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.dark .dark\:hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.dark .dark\:hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.dark .dark\:hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.dark .dark\:hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.dark .dark\:hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.dark .dark\:hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.dark .dark\:hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.dark .dark\:hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.dark .dark\:hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.dark .dark\:hover\:bg-amber-50:hover{background-color:var(--color-amber-50)}.dark .dark\:hover\:bg-amber-100:hover{background-color:var(--color-amber-100)}.dark .dark\:hover\:bg-amber-200:hover{background-color:var(--color-amber-200)}.dark .dark\:hover\:bg-amber-300:hover{background-color:var(--color-amber-300)}.dark .dark\:hover\:bg-amber-400:hover{background-color:var(--color-amber-400)}.dark .dark\:hover\:bg-amber-500:hover{background-color:var(--color-amber-500)}.dark .dark\:hover\:bg-amber-600:hover{background-color:var(--color-amber-600)}.dark .dark\:hover\:bg-amber-700:hover{background-color:var(--color-amber-700)}.dark .dark\:hover\:bg-amber-800:hover{background-color:var(--color-amber-800)}.dark .dark\:hover\:bg-amber-900:hover{background-color:var(--color-amber-900)}.dark .dark\:hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.dark .dark\:hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.dark .dark\:hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.dark .dark\:hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.dark .dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark .dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.dark .dark\:hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.dark .dark\:hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.dark .dark\:hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.dark .dark\:hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:color-mix(in oklab,var(--color-blue-900) 30%,transparent)}}.dark .dark\:hover\:bg-blue-900\/50:hover{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/50:hover{background-color:color-mix(in oklab,var(--color-blue-900) 50%,transparent)}}.dark .dark\:hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.dark .dark\:hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.dark .dark\:hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.dark .dark\:hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.dark .dark\:hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.dark .dark\:hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.dark .dark\:hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.dark .dark\:hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.dark .dark\:hover\:bg-gray-700\/30:hover{background-color:#3641534d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/30:hover{background-color:color-mix(in oklab,var(--color-gray-700) 30%,transparent)}}.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}.dark .dark\:hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800) 50%,transparent)}}.dark .dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark .dark\:hover\:bg-green-50:hover{background-color:var(--color-green-50)}.dark .dark\:hover\:bg-green-100:hover{background-color:var(--color-green-100)}.dark .dark\:hover\:bg-green-200:hover{background-color:var(--color-green-200)}.dark .dark\:hover\:bg-green-300:hover{background-color:var(--color-green-300)}.dark .dark\:hover\:bg-green-400:hover{background-color:var(--color-green-400)}.dark .dark\:hover\:bg-green-500:hover{background-color:var(--color-green-500)}.dark .dark\:hover\:bg-green-600:hover{background-color:var(--color-green-600)}.dark .dark\:hover\:bg-green-700:hover{background-color:var(--color-green-700)}.dark .dark\:hover\:bg-green-800:hover{background-color:var(--color-green-800)}.dark .dark\:hover\:bg-green-900:hover{background-color:var(--color-green-900)}.dark .dark\:hover\:bg-green-900\/30:hover{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-green-900\/30:hover{background-color:color-mix(in oklab,var(--color-green-900) 30%,transparent)}}.dark .dark\:hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.dark .dark\:hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.dark .dark\:hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.dark .dark\:hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.dark .dark\:hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.dark .dark\:hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.dark .dark\:hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.dark .dark\:hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.dark .dark\:hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.dark .dark\:hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.dark .dark\:hover\:bg-indigo-900\/30:hover{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-indigo-900\/30:hover{background-color:color-mix(in oklab,var(--color-indigo-900) 30%,transparent)}}.dark .dark\:hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.dark .dark\:hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.dark .dark\:hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.dark .dark\:hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.dark .dark\:hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.dark .dark\:hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.dark .dark\:hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.dark .dark\:hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.dark .dark\:hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.dark .dark\:hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:color-mix(in oklab,var(--color-orange-900) 30%,transparent)}}.dark .dark\:hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.dark .dark\:hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.dark .dark\:hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.dark .dark\:hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.dark .dark\:hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.dark .dark\:hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.dark .dark\:hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.dark .dark\:hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.dark .dark\:hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.dark .dark\:hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.dark .dark\:hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.dark .dark\:hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.dark .dark\:hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.dark .dark\:hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.dark .dark\:hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.dark .dark\:hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.dark .dark\:hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.dark .dark\:hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.dark .dark\:hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.dark .dark\:hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.dark .dark\:hover\:bg-purple-900\/30:hover{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-purple-900\/30:hover{background-color:color-mix(in oklab,var(--color-purple-900) 30%,transparent)}}.dark .dark\:hover\:bg-red-50:hover{background-color:var(--color-red-50)}.dark .dark\:hover\:bg-red-100:hover{background-color:var(--color-red-100)}.dark .dark\:hover\:bg-red-200:hover{background-color:var(--color-red-200)}.dark .dark\:hover\:bg-red-300:hover{background-color:var(--color-red-300)}.dark .dark\:hover\:bg-red-400:hover{background-color:var(--color-red-400)}.dark .dark\:hover\:bg-red-500:hover{background-color:var(--color-red-500)}.dark .dark\:hover\:bg-red-600:hover{background-color:var(--color-red-600)}.dark .dark\:hover\:bg-red-700:hover{background-color:var(--color-red-700)}.dark .dark\:hover\:bg-red-800:hover{background-color:var(--color-red-800)}.dark .dark\:hover\:bg-red-900:hover{background-color:var(--color-red-900)}.dark .dark\:hover\:bg-red-900\/20:hover{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/20:hover{background-color:color-mix(in oklab,var(--color-red-900) 20%,transparent)}}.dark .dark\:hover\:bg-red-900\/30:hover{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/30:hover{background-color:color-mix(in oklab,var(--color-red-900) 30%,transparent)}}.dark .dark\:hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.dark .dark\:hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.dark .dark\:hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.dark .dark\:hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.dark .dark\:hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.dark .dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.dark .dark\:hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.dark .dark\:hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.dark .dark\:hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.dark .dark\:hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.dark .dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white) 10%,transparent)}}.dark .dark\:hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.dark .dark\:hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.dark .dark\:hover\:bg-yellow-900\/30:hover{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-yellow-900\/30:hover{background-color:color-mix(in oklab,var(--color-yellow-900) 30%,transparent)}}.dark .dark\:hover\:text-black:hover{color:var(--color-black)}.dark .dark\:hover\:text-blue-50:hover{color:var(--color-blue-50)}.dark .dark\:hover\:text-blue-100:hover{color:var(--color-blue-100)}.dark .dark\:hover\:text-blue-200:hover{color:var(--color-blue-200)}.dark .dark\:hover\:text-blue-300:hover{color:var(--color-blue-300)}.dark .dark\:hover\:text-blue-400:hover{color:var(--color-blue-400)}.dark .dark\:hover\:text-blue-500:hover{color:var(--color-blue-500)}.dark .dark\:hover\:text-blue-600:hover{color:var(--color-blue-600)}.dark .dark\:hover\:text-blue-700:hover{color:var(--color-blue-700)}.dark .dark\:hover\:text-blue-800:hover{color:var(--color-blue-800)}.dark .dark\:hover\:text-blue-900:hover{color:var(--color-blue-900)}.dark .dark\:hover\:text-gray-50:hover{color:var(--color-gray-50)}.dark .dark\:hover\:text-gray-100:hover{color:var(--color-gray-100)}.dark .dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark .dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}.dark .dark\:hover\:text-gray-400:hover{color:var(--color-gray-400)}.dark .dark\:hover\:text-gray-500:hover{color:var(--color-gray-500)}.dark .dark\:hover\:text-gray-600:hover{color:var(--color-gray-600)}.dark .dark\:hover\:text-gray-700:hover{color:var(--color-gray-700)}.dark .dark\:hover\:text-gray-800:hover{color:var(--color-gray-800)}.dark .dark\:hover\:text-gray-900:hover{color:var(--color-gray-900)}.dark .dark\:hover\:text-green-50:hover{color:var(--color-green-50)}.dark .dark\:hover\:text-green-100:hover{color:var(--color-green-100)}.dark .dark\:hover\:text-green-200:hover{color:var(--color-green-200)}.dark .dark\:hover\:text-green-300:hover{color:var(--color-green-300)}.dark .dark\:hover\:text-green-400:hover{color:var(--color-green-400)}.dark .dark\:hover\:text-green-500:hover{color:var(--color-green-500)}.dark .dark\:hover\:text-green-600:hover{color:var(--color-green-600)}.dark .dark\:hover\:text-green-700:hover{color:var(--color-green-700)}.dark .dark\:hover\:text-green-800:hover{color:var(--color-green-800)}.dark .dark\:hover\:text-green-900:hover{color:var(--color-green-900)}.dark .dark\:hover\:text-purple-200:hover{color:var(--color-purple-200)}.dark .dark\:hover\:text-purple-300:hover{color:var(--color-purple-300)}.dark .dark\:hover\:text-red-50:hover{color:var(--color-red-50)}.dark .dark\:hover\:text-red-100:hover{color:var(--color-red-100)}.dark .dark\:hover\:text-red-200:hover{color:var(--color-red-200)}.dark .dark\:hover\:text-red-300:hover{color:var(--color-red-300)}.dark .dark\:hover\:text-red-400:hover{color:var(--color-red-400)}.dark .dark\:hover\:text-red-500:hover{color:var(--color-red-500)}.dark .dark\:hover\:text-red-600:hover{color:var(--color-red-600)}.dark .dark\:hover\:text-red-700:hover{color:var(--color-red-700)}.dark .dark\:hover\:text-red-800:hover{color:var(--color-red-800)}.dark .dark\:hover\:text-red-900:hover{color:var(--color-red-900)}.dark .dark\:hover\:text-slate-800:hover{color:var(--color-slate-800)}.dark .dark\:hover\:text-white:hover{color:var(--color-white)}}.dark .dark\:focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.dark .dark\:focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.dark .dark\:focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.dark .dark\:focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.dark .dark\:focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.dark .dark\:focus\:text-gray-100:focus{color:var(--color-gray-100)}.dark .dark\:focus\:text-gray-200:focus{color:var(--color-gray-200)}.dark .dark\:focus\:text-gray-300:focus{color:var(--color-gray-300)}.dark .dark\:focus\:text-white:focus{color:var(--color-white)}.dark .dark\:focus\:ring-amber-400:focus{--tw-ring-color:var(--color-amber-400)}.dark .dark\:focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.dark .dark\:focus\:ring-amber-600:focus{--tw-ring-color:var(--color-amber-600)}.dark .dark\:focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.dark .dark\:focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.dark .dark\:focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.dark .dark\:focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.dark .dark\:focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.dark .dark\:focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.dark .dark\:focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.dark .dark\:focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.dark .dark\:focus\:ring-indigo-400:focus{--tw-ring-color:var(--color-indigo-400)}.dark .dark\:focus\:ring-red-400:focus{--tw-ring-color:var(--color-red-400)}.dark .dark\:focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.dark .dark\:focus\:ring-slate-500:focus{--tw-ring-color:var(--color-slate-500)}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.dark .dark\:active\:bg-gray-600:active{background-color:var(--color-gray-600)}.dark .dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark .dark\:active\:bg-gray-800:active{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-blue-800:disabled{background-color:var(--color-blue-800)}.dark .dark\:disabled\:bg-gray-600:disabled{background-color:var(--color-gray-600)}.dark .dark\:disabled\:bg-gray-700:disabled{background-color:var(--color-gray-700)}.dark .dark\:disabled\:bg-gray-800:disabled{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-gray-900:disabled{background-color:var(--color-gray-900)}.dark .dark\:disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.dark .dark\:disabled\:text-gray-600:disabled{color:var(--color-gray-600)}@media(hover:hover){.dark .dark\:disabled\:hover\:bg-indigo-500:disabled:hover{background-color:var(--color-indigo-500)}}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_table\]\:border table{border-style:var(--tw-border-style);border-width:1px}.\[\&_table\]\:border-gray-300 table{border-color:var(--color-gray-300)}.dark .dark\:\[\&_table\]\:border-gray-600 table{border-color:var(--color-gray-600)}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-gray-300 td{border-color:var(--color-gray-300)}.\[\&_td\]\:px-4 td{padding-inline:calc(var(--spacing) * 4)}.\[\&_td\]\:py-2 td{padding-block:calc(var(--spacing) * 2)}.dark .dark\:\[\&_td\]\:border-gray-600 td{border-color:var(--color-gray-600)}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-gray-300 th{border-color:var(--color-gray-300)}.\[\&_th\]\:bg-gray-100 th{background-color:var(--color-gray-100)}.\[\&_th\]\:px-4 th{padding-inline:calc(var(--spacing) * 4)}.\[\&_th\]\:py-2 th{padding-block:calc(var(--spacing) * 2)}.\[\&_th\]\:text-left th{text-align:left}.dark .dark\:\[\&_th\]\:border-gray-600 th{border-color:var(--color-gray-600)}.dark .dark\:\[\&_th\]\:bg-gray-800 th{background-color:var(--color-gray-800)}.\[\&_tr\:hover\]\:bg-gray-50 tr:hover{background-color:var(--color-gray-50)}.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:color-mix(in oklab,var(--color-gray-700) 50%,transparent)}}}.dashboard-content{max-width:80rem;margin:0 auto}.dashboard-stats-grid{gap:var(--spacing-5);margin-bottom:var(--spacing-8);grid-template-columns:1fr;display:grid}@media(min-width:768px){.dashboard-stats-grid{grid-template-columns:repeat(2,1fr)}}@media(min-width:1024px){.dashboard-stats-grid{grid-template-columns:repeat(4,1fr)}}.dashboard-main-grid{gap:var(--spacing-6);grid-template-columns:1fr;display:grid}@media(min-width:1024px){.dashboard-main-grid{grid-template-columns:repeat(5,1fr)}}.dashboard-column-left{gap:var(--spacing-6);flex-direction:column;display:flex}@media(min-width:1024px){.dashboard-column-left{grid-column:span 3}}.dashboard-column-right{gap:var(--spacing-6);flex-direction:column;display:flex}@media(min-width:1024px){.dashboard-column-right{grid-column:span 2}}.progress-bar-container{background-color:var(--color-slate-100);border-radius:var(--radius-full);width:100%;height:.5rem;overflow:hidden}.dark .progress-bar-container{background-color:var(--color-slate-700)}.progress-bar-fill{border-radius:var(--radius-full);height:.5rem;transition:width var(--transition-normal) var(--ease-in-out)}.progress-bar-emerald{background-color:var(--color-emerald-500)}.progress-bar-blue{background-color:var(--color-blue-500)}.progress-bar-slate{background-color:var(--color-slate-600)}.dark .progress-bar-slate{background-color:var(--color-slate-500)}.module-list{gap:var(--spacing-3);flex-direction:column;display:flex}.module-item{padding:var(--spacing-2) 0;justify-content:space-between;align-items:center;display:flex}.module-info{flex-direction:column;display:flex}.module-name{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .module-name{color:#fff}.module-version{font-size:var(--text-xs);color:var(--color-slate-500)}.dark .module-version{color:var(--color-slate-400)}.module-badge-active{padding:var(--spacing-1) var(--spacing-2);font-size:var(--text-xs);font-weight:var(--font-medium);background-color:var(--color-green-100);color:var(--color-green-700);border-radius:var(--radius-full)}.dark .module-badge-active{background-color:var(--color-green-900);color:var(--color-green-400)}.module-badge-inactive{padding:var(--spacing-1) var(--spacing-2);font-size:var(--text-xs);font-weight:var(--font-medium);background-color:var(--color-slate-100);color:var(--color-slate-600);border-radius:var(--radius-full)}.dark .module-badge-inactive{background-color:var(--color-slate-700);color:var(--color-slate-400)}.activity-list{gap:var(--spacing-4);flex-direction:column;display:flex}.activity-item{align-items:flex-start;gap:var(--spacing-3);display:flex}.activity-dot{width:.5rem;height:.5rem;margin-top:var(--spacing-2);border-radius:var(--radius-full);background-color:var(--color-blue-500);flex-shrink:0}.activity-content{flex:1;min-width:0}.activity-title{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .activity-title{color:#fff}.activity-desc{font-size:var(--text-sm);color:var(--color-slate-500)}.dark .activity-desc{color:var(--color-slate-400)}.activity-time{font-size:var(--text-xs);color:var(--color-slate-400);white-space:nowrap}.dark .activity-time{color:var(--color-slate-500)}.alert-list{gap:var(--spacing-3);flex-direction:column;display:flex}.alert-item{align-items:flex-start;gap:var(--spacing-3);padding:var(--spacing-3);border-radius:var(--radius-lg);display:flex}.alert-bg-info{background-color:var(--color-blue-50)}.dark .alert-bg-info{background-color:var(--color-slate-800)}.alert-bg-warning{background-color:var(--color-yellow-50)}.dark .alert-bg-warning{background-color:var(--color-slate-800)}.alert-bg-error{background-color:var(--color-red-50)}.dark .alert-bg-error{background-color:var(--color-slate-800)}.alert-bg-success{background-color:var(--color-green-50)}.dark .alert-bg-success{background-color:var(--color-slate-800)}.alert-dot{width:.5rem;height:.5rem;margin-top:var(--spacing-2);border-radius:var(--radius-full);flex-shrink:0}.alert-dot-info{background-color:var(--color-blue-500)}.alert-dot-warning{background-color:var(--color-yellow-500)}.alert-dot-error{background-color:var(--color-red-500)}.alert-dot-success{background-color:var(--color-green-500)}.alert-content{flex:1}.alert-title{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-900)}.dark .alert-title{color:#fff}.alert-time{font-size:var(--text-xs);color:var(--color-slate-500);margin-top:var(--spacing-1)}.dark .alert-time{color:var(--color-slate-400)}.text-subtitle{font-size:var(--text-sm);color:var(--color-slate-500)}.dark .text-subtitle{color:var(--color-slate-400)}.section-margin{margin-bottom:var(--spacing-6)}.flex-between{justify-content:space-between;align-items:center;display:flex}.flex-between-mb{margin-bottom:var(--spacing-2);justify-content:space-between;align-items:center;display:flex}.resource-label{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-700)}.dark .resource-label{color:var(--color-slate-300)}.resource-value{font-size:var(--text-sm);font-weight:var(--font-medium);color:var(--color-slate-700)}.dark .resource-value{color:var(--color-slate-300)}.icon-stat{width:1rem;height:1rem}.icon-trend{width:.875rem;height:.875rem}@media(max-width:640px){.input-split{flex-wrap:wrap}.input-split input{flex:1;min-width:0}.page-header{flex-direction:column;align-items:flex-start;gap:1rem}.page-actions{justify-content:flex-start;width:100%}}@media(min-width:640px){.responsive-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.responsive-grid,.grid-cols-1-lg-3{grid-template-columns:repeat(3,minmax(0,1fr))}.col-span-1-lg{grid-column:span 1/span 1}.col-span-2-lg{grid-column:span 2/span 2}}@media(max-width:480px){.responsive-grid{grid-template-columns:1fr!important}}@media(max-width:768px){.table-mobile-cards{display:block}.table-mobile-cards>*+*{margin-top:1rem}.table-desktop{display:none!important}}@media(max-width:640px){.modal{max-width:calc(100% - 2rem);max-height:calc(100vh - 2rem);margin-left:1rem;margin-right:1rem;overflow-y:auto}.modal-overlay{align-items:flex-start;padding:2rem 1rem 1rem}.modal-footer{flex-direction:column;gap:.5rem}.modal-footer .btn,.card-account{width:100%}}.sidebar-scrollbar{scrollbar-width:thin;scrollbar-color:transparent transparent;transition:scrollbar-color .3s}.sidebar-scrollbar:hover{scrollbar-color:#d1d5db transparent}.dark .sidebar-scrollbar:hover{scrollbar-color:#4b5563 transparent}.sidebar-scrollbar::-webkit-scrollbar{width:6px}.sidebar-scrollbar::-webkit-scrollbar-track{background:0 0}.sidebar-scrollbar::-webkit-scrollbar-thumb{background:0 0;border-radius:3px;transition:background .3s}.sidebar-scrollbar:hover::-webkit-scrollbar-thumb{background:#d1d5db}.dark .sidebar-scrollbar:hover::-webkit-scrollbar-thumb{background:#4b5563}.sidebar-scrollbar::-webkit-scrollbar-thumb:hover{background:#9ca3af}.dark .sidebar-scrollbar::-webkit-scrollbar-thumb:hover{background:#6b7280}@property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.yarl__fullsize{height:100%;width:100%}.yarl__relative{position:relative}.yarl__portal{inset:0;opacity:0;overflow:hidden;position:fixed;transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);z-index:var(--yarl__portal_zindex,9999)}.yarl__portal_open{opacity:1}.yarl__container{background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));inset:0;outline:none;overflow:hidden;overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain);position:absolute;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none}.yarl__carousel{align-content:center;align-items:stretch;display:flex;flex:0 0 auto;height:100%;justify-content:center;opacity:var(--yarl__pull_opacity,1);transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%))}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{align-content:center;align-items:center;display:flex;justify-content:center}.yarl__slide{flex:1;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%);position:relative}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-moz-user-select:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{left:50%;line-height:0;position:absolute;top:50%;transform:translate(-50%) translateY(-50%)}.yarl__slide_loading{animation:yarl__delayed_fadein 1s linear;color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)))}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{color:var(--yarl__slide_icon_error_color,red);height:var(--yarl__slide_icon_error_size,48px);width:var(--yarl__slide_icon_error_size,48px)}@media(prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{display:flex;justify-content:flex-end;inset:0 0 auto auto;padding:var(--yarl__toolbar_padding,8px);position:absolute}[dir=rtl] .yarl__toolbar{inset:0 auto auto 0}.yarl__icon{height:var(--yarl__icon_size,32px);width:var(--yarl__icon_size,32px)}.yarl__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);color:var(--yarl__color_button,hsla(0,0%,100%,.8));cursor:pointer;filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));line-height:0;margin:var(--yarl__button_margin,0);outline:none;padding:var(--yarl__button_padding,8px);-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media(hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{padding:var(--yarl__navigation_button_padding,24px 16px);position:absolute;top:50%;transform:translateY(-50%)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}.yarl__counter{bottom:var(--yarl__counter_bottom,unset);color:var(--yarl__counter_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__counter_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));left:var(--yarl__counter_left,0);line-height:var(--yarl__counter_line_height,var(--yarl__icon_size,32px));margin:var(--yarl__counter_margin,var(--yarl__toolbar_padding,8px));padding:var(--yarl__counter_padding,var(--yarl__button_padding,8px));position:var(--yarl__counter_position,absolute);right:var(--yarl__counter_right,unset);top:var(--yarl__counter_top,0);-webkit-user-select:var(--yarl__counter_user_select,none);-moz-user-select:var(--yarl__counter_user_select,none);user-select:var(--yarl__counter_user_select,none)}.yarl__thumbnails{display:flex;height:100%}.yarl__thumbnails_bottom,.yarl__thumbnails_end .yarl__thumbnails_track,.yarl__thumbnails_start .yarl__thumbnails_track,.yarl__thumbnails_top{flex-direction:column}.yarl__thumbnails_wrapper{flex:1;position:relative}.yarl__thumbnails_container{background-color:var(--yarl__thumbnails_container_background_color,var(--yarl__color_backdrop,#000));flex:0 0 auto;overflow:hidden;padding:var(--yarl__thumbnails_container_padding,16px);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__thumbnails_vignette{pointer-events:none;position:absolute;--yarl__thumbnails_vignette_size:12%}@media(min-width:1200px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:8%}}@media(min-width:2000px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:5%}}.yarl__thumbnails_bottom .yarl__thumbnails_vignette,.yarl__thumbnails_top .yarl__thumbnails_vignette{background:linear-gradient(to right,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);height:100%;left:0;right:0}.yarl__thumbnails_end .yarl__thumbnails_vignette,.yarl__thumbnails_start .yarl__thumbnails_vignette{background:linear-gradient(to bottom,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);bottom:0;top:0;width:100%}.yarl__thumbnails_track{gap:var(--yarl__thumbnails_thumbnail_gap,16px);outline:none}.yarl__thumbnails_thumbnail{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--yarl__thumbnails_thumbnail_background,#000);border-color:var(--yarl__thumbnails_thumbnail_border_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));border-radius:var(--yarl__thumbnails_thumbnail_border_radius,4px);border-style:var(--yarl__thumbnails_thumbnail_border_style,solid);border-width:var(--yarl__thumbnails_thumbnail_border,1px);cursor:pointer;flex:0 0 auto;-webkit-tap-highlight-color:transparent;box-sizing:content-box;height:var(--yarl__thumbnails_thumbnail_height,80px);outline:none;overflow:hidden;padding:var(--yarl__thumbnails_thumbnail_padding,4px);position:relative;width:var(--yarl__thumbnails_thumbnail_width,120px)}.yarl__thumbnails_thumbnail_active{border-color:var(--yarl__thumbnails_thumbnail_active_border_color,var(--yarl__color_button_active,#fff))}.yarl__thumbnails_thumbnail_fadein{animation:yarl__thumbnails_thumbnail_fadein var(--yarl__thumbnails_thumbnail_fadein_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadein_delay,0s) forwards;opacity:0}.yarl__thumbnails_thumbnail_fadeout{animation:yarl__thumbnails_thumbnail_fadeout var(--yarl__thumbnails_thumbnail_fadeout_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadeout_delay,0s) forwards;cursor:unset}.yarl__thumbnails_thumbnail_placeholder{cursor:unset;visibility:hidden}.yarl__thumbnails_thumbnail:focus{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail:focus:not(:focus-visible){box-shadow:unset}.yarl__thumbnails_thumbnail:focus-visible{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail_icon{color:var(--yarl__thumbnails_thumbnail_icon_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__thumbnails_thumbnail_icon_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));height:var(--yarl__thumbnails_thumbnail_icon_size,32px);left:50%;position:absolute;top:50%;transform:translate(-50%) translateY(-50%);width:var(--yarl__thumbnails_thumbnail_icon_size,32px)}@keyframes yarl__thumbnails_thumbnail_fadein{0%{opacity:0}to{opacity:1}}@keyframes yarl__thumbnails_thumbnail_fadeout{0%{opacity:1}to{opacity:0}} diff --git a/templates/_bundled/sirsoft-admin_basic/dist/js/components.iife.js b/templates/_bundled/sirsoft-admin_basic/dist/js/components.iife.js index ff41840e..58e90729 100644 --- a/templates/_bundled/sirsoft-admin_basic/dist/js/components.iife.js +++ b/templates/_bundled/sirsoft-admin_basic/dist/js/components.iife.js @@ -1,20 +1,19 @@ -var SirsoftAdminBasic=(function(R,h,v,$n){"use strict";var nA=Object.defineProperty;var rA=(R,h,v)=>h in R?nA(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Pe=(R,h,v)=>rA(R,typeof h!="symbol"?h+"":h,v);var Va,wr;function fb(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const n in e)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}}return t.default=e,Object.freeze(t)}const F=fb(v),hb=()=>window.G7Core,ae=v.forwardRef(({children:e,variant:t="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=hb()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:e})});ae.displayName="Button";const $d=v.forwardRef(({accept:e,maxSize:t,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,M])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=M:p[x]=M});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const M=x.target.files?.[0]||null;if(!M){g(""),n?.({target:{value:null,name:""}});return}if(t&&M.size>t*1024*1024){const C=`File size exceeds ${t}MB limit`;r?.(C),u.current&&(u.current.value=""),g("");return}g(M.name),n?.({target:{value:M,name:M.name}})},[t,n,r,u]),_=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:e,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:_,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});$d.displayName="FileInput";const gb=e=>e==="checkbox"?"checkbox":e==="radio"?"radio":"input",pb=(e,t)=>{const n=gb(e);return t?t.split(/\s+/).filter(Boolean).includes(n)?t:`${n} ${t}`:n},St=v.forwardRef(({label:e,error:t,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=pb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const _=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(E=>{if(m.current=!1,r){const D=E.target.value;b(D);const P=new Event("change",{bubbles:!0});Object.defineProperty(P,"target",{writable:!1,value:{value:D}}),Object.defineProperty(P,"currentTarget",{writable:!1,value:{value:D}}),r(P)}},[r]),M=v.useCallback(E=>{const D=E.target.value;b(D),!m.current&&r&&r(E)},[r]),C=v.useCallback(E=>{m.current||s&&s(E)},[s]),{loadingactions:k,formdata:N,...S}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,...S});const w=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:M,onKeyPress:C,onCompositionStart:_,onCompositionEnd:x,...w,...S})});St.displayName="Input";const ir=({label:e,error:t,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(M=>{if(a.current=!1,r){const C=M.target.value;d(C);const k=new Event("change",{bubbles:!0});Object.defineProperty(k,"target",{writable:!1,value:{value:C}}),Object.defineProperty(k,"currentTarget",{writable:!1,value:{value:C}}),r(k)}},[r]),g=v.useCallback(M=>{const C=M.target.value;d(C),!a.current&&r&&r(M)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},ln=({children:e,className:t="",...n})=>h.jsx("label",{className:t,...n,children:e}),I=v.forwardRef(({children:e,className:t="",...n},r)=>h.jsx("div",{ref:r,className:t,...n,children:e})),Q=({children:e,className:t="",...n})=>h.jsx("span",{className:t,...n,children:e}),vt=({children:e,className:t="",...n})=>h.jsx("p",{className:t,...n,children:e}),_n=({className:e="",alt:t="",...n})=>h.jsx("img",{className:e,alt:t,...n}),Ba=({children:e,className:t="",...n})=>h.jsx("h1",{className:t,...n,children:e}),ar=({children:e,className:t="",...n})=>h.jsx("h2",{className:t,...n,children:e}),Gr=({children:e,className:t="",...n})=>h.jsx("h3",{className:t,...n,children:e}),Fd=({children:e,className:t="",...n})=>h.jsx("h4",{className:t,...n,children:e}),ja=({children:e,className:t="",...n})=>h.jsx("ul",{className:t,...n,children:e}),mb=({children:e,className:t="",...n})=>h.jsx("ol",{className:t,...n,children:e}),Wa=({children:e,className:t="",...n})=>h.jsx("li",{className:t,...n,children:e}),Yr=({children:e,className:t="",...n})=>h.jsx("a",{className:t,...n,children:e}),Ga=({children:e,className:t="",...n})=>h.jsx("form",{className:t,...n,children:e}),Zn=({children:e,className:t="",...n})=>h.jsx("svg",{className:t,...n,children:e}),bb=window.G7Core?.createLogger?.("Comp:Select")??{log:(...e)=>console.log("[Comp:Select]",...e),warn:(...e)=>console.warn("[Comp:Select]",...e),error:(...e)=>console.error("[Comp:Select]",...e)};function yb(e){const t=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[e];return t||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[e]||e}const Xr=({children:e,label:t,error:n,options:r,className:s="",value:o,onChange:i,disabled:a,searchable:l=!1,searchPlaceholder:c,editorAttrs:d,...u})=>{const[f,g]=v.useState(!1),[m,p]=v.useState(""),[y,b]=v.useState(null),_=v.useRef(null),x=v.useRef(null),M=v.useRef(null),C=v.useRef(null),k=v.useMemo(()=>r?Array.isArray(r)?r.length===0?[]:typeof r[0]=="string"?r.map(T=>({value:T,label:yb(T)})):r:(bb.warn("options prop is not an array:",r),null):null,[r]),N=v.useMemo(()=>k&&k.find(z=>String(z.value)===String(o))?.label||"",[k,o]),S=v.useMemo(()=>{if(!k)return null;if(!l||m.trim()==="")return k;const T=m.trim().toLowerCase();return k.filter(z=>z.label.toLowerCase().includes(T)||String(z.value).toLowerCase().includes(T))},[k,l,m]);v.useEffect(()=>{if(f&&l){p("");const T=setTimeout(()=>M.current?.focus(),0);return()=>clearTimeout(T)}f||p("")},[f,l]);const w=v.useCallback(()=>{if(!x.current)return;const T=x.current.getBoundingClientRect(),z=window.innerHeight-T.bottom,j=z<280&&T.top>z;b(j?{bottom:window.innerHeight-T.top+4,left:T.left,width:T.width}:{top:T.bottom+4,left:T.left,width:T.width})},[]);v.useEffect(()=>{if(!f){b(null);return}return w(),window.addEventListener("scroll",w,!0),window.addEventListener("resize",w),()=>{window.removeEventListener("scroll",w,!0),window.removeEventListener("resize",w)}},[f,w]),v.useEffect(()=>{const T=z=>{const H=z.target;_.current&&!_.current.contains(H)&&(!C.current||!C.current.contains(H))&&g(!1)};return f&&document.addEventListener("mousedown",T),()=>{document.removeEventListener("mousedown",T)}},[f]),v.useEffect(()=>{const T=z=>{z.key==="Escape"&&f&&(g(!1),x.current?.focus())};return document.addEventListener("keydown",T),()=>document.removeEventListener("keydown",T)},[f]);const E=()=>{a||g(!f)},D=T=>{i&&i({target:{value:T},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),x.current?.focus()};if(!k){const z=(s??"").split(/\s+/).filter(Boolean).includes("select")?s??"":["select",s].filter(Boolean).join(" ");return h.jsx("select",{className:z,value:o,onChange:i,disabled:a,...d,...u,children:e})}const P=(s??"").split(/\s+/).filter(T=>T&&!T.includes(":")).join(" "),L=P.includes("bg-"),$=/text-(gray|red|blue|green|yellow|white|black|indigo|purple|pink|orange|amber|emerald|teal|cyan|slate)-\d+|text-white|text-black/.test(P),A=L?`${s}${$?"":" text-gray-700 dark:text-gray-200"}`:`w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-sm text-gray-700 dark:text-gray-200 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none ${s}`,O={...d,...u};return h.jsxs(I,{ref:_,className:"relative",...O,children:[h.jsxs(ae,{ref:x,type:"button",onClick:E,disabled:a,className:`${A} flex items-center justify-between gap-2 text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(Q,{className:"truncate",children:N||" "}),h.jsx(Zn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&y&&$n.createPortal(h.jsxs(I,{ref:C,className:"fixed z-[9999] bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",style:{top:y.top,bottom:y.bottom,left:y.left,width:y.width},role:"listbox",children:[l&&h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-600",children:h.jsx(St,{ref:M,type:"text",value:m,onChange:T=>p(T.target.value),placeholder:c??"Search...",className:"w-full px-3 py-2 bg-gray-100 dark:bg-gray-700 border-0 rounded-lg text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:outline-none",role:"searchbox","aria-label":c??"Search"})}),h.jsxs(I,{className:"py-2 max-h-60 overflow-auto",children:[S&&S.length===0&&h.jsx(I,{className:"px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center",children:m?"No results":""}),(S??[]).map(T=>{const z=String(T.value)===String(o);return h.jsxs(ae,{type:"button",onClick:()=>!T.disabled&&D(T.value),disabled:T.disabled,className:`w-full px-4 py-3 text-left text-sm flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${z?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-700 dark:text-gray-200"} ${T.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,role:"option","aria-selected":z,children:[h.jsx(Q,{children:T.label}),z&&h.jsx(Zn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]},T.value)})]})]}),document.body)]})},vb=({children:e,className:t="",...n})=>h.jsx("option",{className:t,...n,children:e}),xb=({children:e,className:t="",...n})=>h.jsx("optgroup",{className:t,...n,children:e}),En=v.forwardRef(({label:e,className:t="",indeterminate:n=!1,...r},s)=>{const o=v.useRef(null),i=a=>{o.current=a,typeof s=="function"?s(a):s&&(s.current=a)};return v.useEffect(()=>{o.current&&(o.current.indeterminate=n)},[n]),h.jsx("input",{ref:i,type:"checkbox",className:t,...r})});En.displayName="Checkbox";const Ya=({children:e,className:t="",...n})=>h.jsx("table",{className:t,...n,children:e}),Xa=({children:e,className:t="",...n})=>h.jsx("thead",{className:t,...n,children:e}),Ka=({children:e,className:t="",...n})=>h.jsx("tbody",{className:t,...n,children:e}),qa=({children:e,className:t="",...n})=>h.jsx("tfoot",{className:t,...n,children:e}),lr=({children:e,className:t="",...n})=>h.jsx("tr",{className:t,...n,children:e}),Kr=({children:e,className:t="",...n})=>h.jsx("th",{className:t,...n,children:e}),en=({children:e,className:t="",...n})=>h.jsx("td",{className:t,...n,children:e}),Yo=({children:e,className:t="",...n})=>h.jsx("nav",{className:t,...n,children:e}),zd=({children:e,className:t="",...n})=>h.jsx("section",{className:t,...n,children:e}),oe=({name:e,iconStyle:t="solid",size:n,color:r,spin:s=!1,pulse:o=!1,fixedWidth:i=!1,ariaLabel:a,className:l="",style:c,...d})=>{const u={solid:"fas",regular:"far",light:"fal",duotone:"fad",brands:"fab"},f={"fa-solid":"solid","fa-regular":"regular","fa-light":"light","fa-duotone":"duotone","fa-brands":"brands"},g=typeof e=="string"?e:String(e);let m=t,p=g;for(const[_,x]of Object.entries(f))if(g.startsWith(_+" ")){m=x,p=g.substring(_.length+1);break}p=p.replace(/^fa-/,"");const y=[u[m],`fa-${p}`,n&&`fa-${n}`,s&&"fa-spin",o&&"fa-pulse",i&&"fa-fw",r,l].filter(Boolean).join(" "),b=a||p.replace(/-/g," ");return h.jsx("i",{className:y,style:c,"aria-label":b,role:"img",...d})},wb={xs:"w-3 h-3",sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6",xl:"w-8 h-8"},Vd={xs:"xs",sm:"sm",md:void 0,lg:"lg",xl:"2x"},kb=({icon:e,size:t="md",className:n="",color:r,ariaLabel:s})=>{const o=e.indexOf(":"),i=e.startsWith("fa-")||e.startsWith("fas ")||e.startsWith("far ")||e.startsWith("fab ")||e.startsWith("fal ")||e.startsWith("fad ");let a,l;i?(a="fa",l=e):o>0?(a=e.substring(0,o),l=e.substring(o+1)):(a="fa",l=e);const c=wb[t],d=r||"";switch(a){case"fa":return h.jsx(oe,{name:l,size:Vd[t],className:n,color:r,ariaLabel:s});case"svg":return h.jsx("img",{src:l,className:`${c} ${d} ${n}`.trim(),alt:s||"","aria-label":s,role:"img"});case"img":return h.jsx("img",{src:l,className:`${c} ${n}`.trim(),alt:s||"",role:"img"});default:return h.jsx(oe,{name:e,size:Vd[t],className:n,color:r,ariaLabel:s})}},Cb=({children:e,className:t="",...n})=>h.jsx("code",{className:t,...n,children:e}),Sb=({children:e,className:t="",...n})=>h.jsx("pre",{className:t,...n,children:e}),_b=({children:e})=>h.jsx(h.Fragment,{children:e}),Eb=({className:e="",...t})=>h.jsx("hr",{className:e,...t}),bt=({className:e="",children:t,style:n,...r})=>h.jsx("i",{className:e,style:n,...r,children:t});var de=(e=>(e.User="fa-user",e.Users="fa-users",e.UserCircle="fa-user-circle",e.UserPlus="fa-user-plus",e.UserMinus="fa-user-minus",e.UserShield="fa-user-shield",e.Search="fa-search",e.Edit="fa-edit",e.Pencil="fa-pencil-alt",e.Trash="fa-trash",e.Save="fa-save",e.Plus="fa-plus",e.Minus="fa-minus",e.Check="fa-check",e.Times="fa-times",e.Download="fa-download",e.Upload="fa-upload",e.Copy="fa-copy",e.ExternalLink="fa-external-link-alt",e.Home="fa-home",e.CaretRight="fa-caret-right",e.CaretDown="fa-caret-down",e.ChevronLeft="fa-chevron-left",e.ChevronRight="fa-chevron-right",e.ChevronUp="fa-chevron-up",e.ChevronDown="fa-chevron-down",e.AngleLeft="fa-angle-left",e.AngleRight="fa-angle-right",e.AngleUp="fa-angle-up",e.AngleDown="fa-angle-down",e.ArrowUp="fa-arrow-up",e.ArrowDown="fa-arrow-down",e.ArrowLeft="fa-arrow-left",e.ArrowRight="fa-arrow-right",e.Bars="fa-bars",e.EllipsisVertical="fa-ellipsis-v",e.EllipsisHorizontal="fa-ellipsis-h",e.CheckCircle="fa-check-circle",e.TimesCircle="fa-times-circle",e.ExclamationCircle="fa-exclamation-circle",e.ExclamationTriangle="fa-exclamation-triangle",e.InfoCircle="fa-info-circle",e.QuestionCircle="fa-question-circle",e.XCircle="fa-times-circle",e.ShoppingCart="fa-shopping-cart",e.ShoppingBag="fa-shopping-bag",e.CreditCard="fa-credit-card",e.Tag="fa-tag",e.Tags="fa-tags",e.File="fa-file",e.FileAlt="fa-file-alt",e.Folder="fa-folder",e.FolderOpen="fa-folder-open",e.Envelope="fa-envelope",e.Phone="fa-phone",e.Comment="fa-comment",e.Comments="fa-comments",e.Cog="fa-cog",e.Cogs="fa-cogs",e.Settings="fa-cog",e.Wrench="fa-wrench",e.Sliders="fa-sliders-h",e.Image="fa-image",e.Images="fa-images",e.Video="fa-video",e.Camera="fa-camera",e.Heart="fa-heart",e.Star="fa-star",e.ThumbsUp="fa-thumbs-up",e.ThumbsDown="fa-thumbs-down",e.Share="fa-share",e.Clock="fa-clock",e.Calendar="fa-calendar",e.CalendarAlt="fa-calendar-alt",e.Lock="fa-lock",e.Unlock="fa-unlock",e.Eye="fa-eye",e.EyeSlash="fa-eye-slash",e.Bell="fa-bell",e.Flag="fa-flag",e.Map="fa-map",e.MapMarker="fa-map-marker-alt",e.Globe="fa-globe",e.Link="fa-link",e.Unlink="fa-unlink",e.Sun="fa-sun",e.Moon="fa-moon",e.ChartBar="fa-chart-bar",e.ChartLine="fa-chart-line",e.ChartPie="fa-chart-pie",e.ChartArea="fa-chart-area",e.Spinner="fa-spinner",e.CircleNotch="fa-circle-notch",e.CheckBadge="fa-badge-check",e.Cube="fa-cube",e.Cubes="fa-cubes",e.Plug="fa-plug",e.PuzzlePiece="fa-puzzle-piece",e.Palette="fa-palette",e))(de||{});const Mb=({title:e,content:t,imageUrl:n,imageAlt:r="",className:s="",onClick:o,style:i,id:a,editorAttrs:l})=>h.jsxs(I,{className:`card-clickable ${s}`,onClick:o,style:i,id:a,...l,children:[n&&h.jsx(I,{className:"w-full",children:h.jsx(_n,{src:n,alt:r,className:"card-image"})}),h.jsxs(I,{className:"p-4",children:[e&&h.jsx(ar,{className:"card-title",children:e}),t&&h.jsx(vt,{className:"card-content",children:t})]})]}),Ls=({items:e,triggerLabel:t="작업",triggerIconName:n=de.EllipsisVertical,position:r="right",className:s="",style:o,children:i,onItemClick:a,id:l,editorAttrs:c})=>{const[d,u]=v.useState(!1),[f,g]=v.useState({top:0,left:0}),m=v.useRef(null),p=v.useRef(null),y=v.useCallback(()=>{if(!m.current)return;const x=m.current.getBoundingClientRect(),M=224;let C=r==="right"?x.right-M:x.left;const k=x.bottom+8;C<8&&(C=8),C+M>window.innerWidth-8&&(C=window.innerWidth-M-8),g({top:k,left:C})},[r]);v.useEffect(()=>{d&&y()},[d,y]),v.useEffect(()=>{if(!d)return;const x=()=>{u(!1)};return window.addEventListener("scroll",x,!0),window.addEventListener("resize",x),()=>{window.removeEventListener("scroll",x,!0),window.removeEventListener("resize",x)}},[d]),v.useEffect(()=>{const x=M=>{const C=M.target,k=m.current?.contains(C),N=p.current?.contains(C);!k&&!N&&u(!1)};return d&&document.addEventListener("mousedown",x),()=>{document.removeEventListener("mousedown",x)}},[d]);const b=x=>{x.disabled||(x.onClick&&x.onClick(),a&&a(x.id,x),u(!1))},_=d?$n.createPortal(h.jsx("div",{ref:p,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:f.top,left:f.left},children:e.filter(x=>x.if!==!1).map(x=>{if(x.divider)return h.jsx("div",{className:"border-t border-gray-200 dark:border-gray-600 my-1"},x.id);const M=x.variant==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",C=x.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer";return h.jsxs("div",{onClick:()=>b(x),className:`flex items-center gap-3 px-4 py-3 transition-colors ${M} ${C}`,children:[x.iconName&&h.jsx(oe,{name:x.iconName,className:"w-4 h-4"}),h.jsx("span",{className:"text-sm font-medium",children:x.label})]},x.id)})}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,id:l,...c,children:[i?h.jsx(I,{ref:m,onClick:()=>u(!d),className:"cursor-pointer",children:i}):h.jsxs(ae,{ref:m,onClick:()=>u(!d),className:"flex items-center gap-2 px-2 py-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[t&&h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:t}),n&&h.jsx(oe,{name:n,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})]}),_]})},qr=(e,t)=>window.G7Core?.t?.(e,t)??e,Xo=({currentPage:e,totalPages:t,onPageChange:n,maxVisiblePages:r=5,showFirstLast:s=!0,className:o="",style:i,prevText:a,nextText:l,id:c,editorAttrs:d})=>{const u=v.useMemo(()=>{const p=[];if(t<=r+2)for(let y=1;y<=t;y++)p.push(y);else{const y=Math.floor(r/2);let b=Math.max(2,e-y),_=Math.min(t-1,e+y);e<=y+1?_=r:e>=t-y&&(b=t-r+1),p.push(1),b>2&&p.push("...");for(let x=b;x<=_;x++)p.push(x);_<t-1&&p.push("..."),t>1&&p.push(t)}return p},[e,t,r]),f=p=>{p<1||p>t||p===e||n(p)},g=()=>{e>1&&n(e-1)},m=()=>{e<t&&n(e+1)};return h.jsxs(I,{className:`pagination ${o}`,style:i,role:"navigation","aria-label":qr("common.pagination"),id:c,...d,children:[s&&h.jsx(ae,{onClick:()=>f(1),disabled:e===1,className:"pagination-btn","aria-label":qr("common.first_page"),children:"«"}),h.jsx(ae,{onClick:g,disabled:e===1,className:"pagination-btn","aria-label":qr("common.prev_page"),children:a||h.jsx(h.Fragment,{children:"‹"})}),h.jsx(I,{className:"pagination",children:u.map((p,y)=>{if(p==="...")return h.jsx(Q,{className:"pagination-ellipsis",children:"..."},`ellipsis-${y}`);const b=p,_=b===e;return h.jsx(ae,{onClick:()=>f(b),className:_?"pagination-btn-active":"pagination-btn","aria-label":qr("common.page_n",{n:b}),"aria-current":_?"page":void 0,children:b},b)})}),h.jsx(ae,{onClick:m,disabled:e===t,className:"pagination-btn","aria-label":qr("common.next_page"),children:l||h.jsx(h.Fragment,{children:"›"})}),s&&h.jsx(ae,{onClick:()=>f(t),disabled:e===t,className:"pagination-btn","aria-label":qr("common.last_page"),children:"»"})]})},Hd=(e,t)=>window.G7Core?.t?.(e,t)??e,Nb=()=>window.G7Core?.locale?.supported?.()??["ko","en"],Tb=()=>window.G7Core?.locale?.current?.()??"ko",Ud=e=>{const t=Hd(`common.language_${e}`);return t===`common.language_${e}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[e]||e.toUpperCase():t},Ob=()=>Nb().map(t=>({code:t,name:Ud(t),nativeName:Ud(t)})),Za=({value:e={},onChange:t,inputType:n="text",layout:r="inline",availableLocales:s,defaultLocale:o,placeholder:i="",required:a=!1,disabled:l=!1,maxLength:c,rows:d=4,className:u="",name:f="multilingual_input",showCodeOnMobile:g=!1,error:m,id:p,editorAttrs:y})=>{const b=!!m,_=v.useMemo(()=>s??Ob(),[s]),x=v.useMemo(()=>o??Tb(),[o]),M=v.useRef(e),[C,k]=v.useState(e),N=v.useRef(JSON.stringify(e));v.useEffect(()=>{const K=JSON.stringify(e);K!==N.current&&(N.current=K,M.current=e,k(e))},[e]);const S=v.useCallback((K,q)=>{const ne={target:{name:f,value:K}};q&&(ne._changedKeys=q),t?.(ne)},[t,f]),[w,E]=v.useState(()=>{if(r==="tabs"||r==="compact"){const ne=_.map(ue=>ue.code);return[x,...ne.filter(ue=>ue!==x)]}const K=Object.keys(e).filter(ne=>e[ne]!==void 0);if(K.length===0)return[x];const q=[];return K.includes(x)&&q.push(x),K.forEach(ne=>{ne!==x&&q.push(ne)}),q}),[D,P]=v.useState(x),L=v.useMemo(()=>_.filter(K=>!w.includes(K.code)),[_,w]),[$,A]=v.useState(!1);v.useEffect(()=>{if(r==="tabs"||r==="compact"){const ue=_.map(le=>le.code),J=[x,...ue.filter(le=>le!==x)],ce=w.join(","),me=J.join(",");ce!==me&&E(J);return}const K=Object.keys(e).filter(ue=>e[ue]!==void 0);if(K.length===0){E([x]);return}const q=[];K.includes(x)&&q.push(x),K.forEach(ue=>{ue!==x&&q.push(ue)});const ne=w.join(","),ee=q.join(",");ne!==ee&&E(q)},[e,x,r,_]);const O=v.useCallback((K,q)=>{const ne={...M.current,[K]:q};M.current=ne,k(ne),S(ne,[K])},[S]),T=v.useCallback(K=>{E(ne=>[...ne,K]),P(K),A(!1);const q={...M.current,[K]:""};M.current=q,k(q),S(q)},[S]),z=v.useCallback(K=>{if(K===x)return;E(ne=>ne.filter(ee=>ee!==K)),D===K&&P(x);const q={...M.current};delete q[K],M.current=q,k(q),S(q)},[D,x,S]),H=v.useCallback(K=>{const q=_.find(ne=>ne.code===K);return q?.nativeName||q?.name||K.toUpperCase()},[_]),j=v.useCallback(K=>!!C[K]?.trim(),[C]),G=v.useCallback(()=>L.length===0?null:h.jsxs(I,{className:"relative",children:[h.jsxs(ae,{type:"button",className:"inline-flex items-center gap-1 px-3 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 rounded-full border border-dashed border-blue-300 dark:border-blue-600 transition-colors",onClick:()=>A(!$),disabled:l,children:[h.jsx(oe,{name:"plus",className:"w-3 h-3"}),h.jsx(Q,{children:Hd("common.add_language")})]}),$&&h.jsx(I,{className:"absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-10 min-w-[120px]",children:L.map(K=>h.jsxs(ae,{type:"button",className:"w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 first:rounded-t-lg last:rounded-b-lg transition-colors",onClick:()=>T(K.code),children:[h.jsx(oe,{name:"globe",className:"w-3.5 h-3.5 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{children:K.nativeName||K.name})]},K.code))})]}),[L,$,l,T]),U=v.useCallback((K,q=!1)=>{const ne=b?"input input-error":"input",ee=b?"textarea input-error":"textarea",ue=K===x,J=`${i}${i?" (":""}${H(K)}${i?")":""}`;return h.jsxs(I,{className:"space-y-2",children:[h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsxs(Q,{className:` +var SirsoftAdminBasic=(function(R,h,v,Dn){"use strict";var VT=Object.defineProperty;var zT=(R,h,v)=>h in R?VT(R,h,{enumerable:!0,configurable:!0,writable:!0,value:v}):R[h]=v;var Pe=(R,h,v)=>zT(R,typeof h!="symbol"?h+"":h,v);var Aa,hr;function Jm(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t){for(const n in t)if(n!=="default"){const r=Object.getOwnPropertyDescriptor(t,n);Object.defineProperty(e,n,r.get?r:{enumerable:!0,get:()=>t[n]})}}return e.default=t,Object.freeze(e)}const $=Jm(v),Rm=()=>window.G7Core,oe=v.forwardRef(({children:t,variant:e="primary",size:n="md",className:r="",...s},o)=>{const i="button",a=Rm()?.style?.mergeClasses?.(i,r)??`${i} ${r}`;return h.jsx("button",{ref:o,className:a,...s,children:t})});oe.displayName="Button";const Nd=v.forwardRef(({accept:t,maxSize:e,onChange:n,onError:r,buttonText:s="Browse",placeholder:o="No file selected",className:i="",disabled:a,...l},c)=>{const d=v.useRef(null),u=c||d,[f,g]=v.useState(""),m={},p={};Object.entries(l).forEach(([x,E])=>{x.startsWith("data-editor-")||x==="id"||x==="onMouseMove"||x==="onMouseLeave"?m[x]=E:p[x]=E});const y=v.useCallback(()=>{!a&&u.current&&u.current.click()},[a,u]),b=v.useCallback(x=>{const E=x.target.files?.[0]||null;if(!E){g(""),n?.({target:{value:null,name:""}});return}if(e&&E.size>e*1024*1024){const S=`File size exceeds ${e}MB limit`;r?.(S),u.current&&(u.current.value=""),g("");return}g(E.name),n?.({target:{value:E,name:E.name}})},[e,n,r,u]),_=v.useCallback(x=>{x.stopPropagation(),u.current&&(u.current.value=""),g(""),n?.({target:{value:null,name:""}})},[n,u]);return h.jsxs("div",{className:`flex items-center gap-2 ${i}`,...m,children:[h.jsx("input",{ref:u,type:"file",accept:t,onChange:b,disabled:a,className:"hidden",...p}),h.jsx("button",{type:"button",onClick:y,disabled:a,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:s}),h.jsx("span",{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:f||o}),f&&!a&&h.jsx("button",{type:"button",onClick:_,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:h.jsx("svg",{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});Nd.displayName="FileInput";const eb=t=>t==="checkbox"?"checkbox":t==="radio"?"radio":"input",tb=(t,e)=>{const n=eb(t);return e?e.split(/\s+/).filter(Boolean).includes(n)?e:`${n} ${e}`:n},wt=v.forwardRef(({label:t,error:e,className:n="",onChange:r,onKeyPress:s,value:o,defaultValue:i,type:a="text",checked:l,defaultChecked:c,...d},u)=>{const f=a==="radio"||a==="checkbox",g=tb(a,n),m=v.useRef(!1),p=v.useRef(null);v.useImperativeHandle(u,()=>p.current);const[y,b]=v.useState(o??i??"");v.useEffect(()=>{if(!f&&!m.current&&o!==void 0){if(p.current&&document.activeElement===p.current)return;b(o)}},[o,f]);const _=v.useCallback(()=>{m.current=!0},[]),x=v.useCallback(M=>{if(m.current=!1,r){const T=M.target.value;b(T);const L=new Event("change",{bubbles:!0});Object.defineProperty(L,"target",{writable:!1,value:{value:T}}),Object.defineProperty(L,"currentTarget",{writable:!1,value:{value:T}}),r(L)}},[r]),E=v.useCallback(M=>{const T=M.target.value;b(T),!m.current&&r&&r(M)},[r]),S=v.useCallback(M=>{m.current||s&&s(M)},[s]),{loadingactions:w,formdata:N,...C}=d;if(f)return h.jsx("input",{ref:p,type:a,className:g,onChange:r,checked:l,defaultChecked:c,value:o,...C});const k=o!==void 0?{value:y}:{defaultValue:i};return h.jsx("input",{ref:p,type:a,className:g,onChange:E,onKeyPress:S,onCompositionStart:_,onCompositionEnd:x,...k,...C})});wt.displayName="Input";const Rn=({label:t,error:e,className:n="",onChange:r,value:s,defaultValue:o,...i})=>{const a=v.useRef(!1),l=v.useRef(null),[c,d]=v.useState(s??o??"");v.useEffect(()=>{!a.current&&s!==void 0&&d(s)},[s]);const u=v.useCallback(()=>{a.current=!0},[]),f=v.useCallback(E=>{if(a.current=!1,r){const S=E.target.value;d(S);const w=new Event("change",{bubbles:!0});Object.defineProperty(w,"target",{writable:!1,value:{value:S}}),Object.defineProperty(w,"currentTarget",{writable:!1,value:{value:S}}),r(w)}},[r]),g=v.useCallback(E=>{const S=E.target.value;d(S),!a.current&&r&&r(E)},[r]),{loadingactions:m,formdata:p,...y}=i,b=s!==void 0?{value:c}:{defaultValue:o},x=(n??"").split(/\s+/).filter(Boolean).includes("textarea")?n??"":["textarea",n].filter(Boolean).join(" ");return h.jsx("textarea",{ref:l,className:x,onChange:g,onCompositionStart:u,onCompositionEnd:f,...b,...y})},on=({children:t,className:e="",...n})=>h.jsx("label",{className:e,...n,children:t}),I=v.forwardRef(({children:t,className:e="",...n},r)=>h.jsx("div",{ref:r,className:e,...n,children:t})),Q=({children:t,className:e="",...n})=>h.jsx("span",{className:e,...n,children:t}),yt=({children:t,className:e="",...n})=>h.jsx("p",{className:e,...n,children:t}),kn=({className:t="",alt:e="",...n})=>h.jsx("img",{className:t,alt:e,...n}),Pa=({children:t,className:e="",...n})=>h.jsx("h1",{className:e,...n,children:t}),er=({children:t,className:e="",...n})=>h.jsx("h2",{className:e,...n,children:t}),$r=({children:t,className:e="",...n})=>h.jsx("h3",{className:e,...n,children:t}),Od=({children:t,className:e="",...n})=>h.jsx("h4",{className:e,...n,children:t}),$a=({children:t,className:e="",...n})=>h.jsx("ul",{className:e,...n,children:t}),nb=({children:t,className:e="",...n})=>h.jsx("ol",{className:e,...n,children:t}),Fa=({children:t,className:e="",...n})=>h.jsx("li",{className:e,...n,children:t}),Fr=({children:t,className:e="",...n})=>h.jsx("a",{className:e,...n,children:t}),Va=({children:t,className:e="",...n})=>h.jsx("form",{className:e,...n,children:t}),Wn=({children:t,className:e="",...n})=>h.jsx("svg",{className:e,...n,children:t}),rb=window.G7Core?.createLogger?.("Comp:Select")??{log:(...t)=>console.log("[Comp:Select]",...t),warn:(...t)=>console.warn("[Comp:Select]",...t),error:(...t)=>console.error("[Comp:Select]",...t)};function sb(t){const e=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[t];return e||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[t]||t}const Vr=({children:t,label:e,error:n,options:r,className:s="",value:o,onChange:i,disabled:a,searchable:l=!1,searchPlaceholder:c,editorAttrs:d,...u})=>{const[f,g]=v.useState(!1),[m,p]=v.useState(""),[y,b]=v.useState(null),_=v.useRef(null),x=v.useRef(null),E=v.useRef(null),S=v.useRef(null),w=v.useMemo(()=>r?Array.isArray(r)?r.length===0?[]:typeof r[0]=="string"?r.map(O=>({value:O,label:sb(O)})):r:(rb.warn("options prop is not an array:",r),null):null,[r]),N=v.useMemo(()=>w&&w.find(V=>String(V.value)===String(o))?.label||"",[w,o]),C=v.useMemo(()=>{if(!w)return null;if(!l||m.trim()==="")return w;const O=m.trim().toLowerCase();return w.filter(V=>V.label.toLowerCase().includes(O)||String(V.value).toLowerCase().includes(O))},[w,l,m]);v.useEffect(()=>{if(f&&l){p("");const O=setTimeout(()=>E.current?.focus(),0);return()=>clearTimeout(O)}f||p("")},[f,l]);const k=v.useCallback(()=>{if(!x.current)return;const O=x.current.getBoundingClientRect(),V=window.innerHeight-O.bottom,W=V<280&&O.top>V;b(W?{bottom:window.innerHeight-O.top+4,left:O.left,width:O.width}:{top:O.bottom+4,left:O.left,width:O.width})},[]);v.useEffect(()=>{if(!f){b(null);return}return k(),window.addEventListener("scroll",k,!0),window.addEventListener("resize",k),()=>{window.removeEventListener("scroll",k,!0),window.removeEventListener("resize",k)}},[f,k]),v.useEffect(()=>{const O=V=>{const H=V.target;_.current&&!_.current.contains(H)&&(!S.current||!S.current.contains(H))&&g(!1)};return f&&document.addEventListener("mousedown",O),()=>{document.removeEventListener("mousedown",O)}},[f]),v.useEffect(()=>{const O=V=>{V.key==="Escape"&&f&&(g(!1),x.current?.focus())};return document.addEventListener("keydown",O),()=>document.removeEventListener("keydown",O)},[f]);const M=()=>{a||g(!f)},T=O=>{i&&i({target:{value:O},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),x.current?.focus()};if(!w){const V=(s??"").split(/\s+/).filter(Boolean).includes("select")?s??"":["select",s].filter(Boolean).join(" ");return h.jsx("select",{className:V,value:o,onChange:i,disabled:a,...d,...u,children:t})}const L=(s??"").split(/\s+/).filter(O=>O&&!O.includes(":")).join(" "),P=L.includes("bg-"),F=/text-(gray|red|blue|green|yellow|white|black|indigo|purple|pink|orange|amber|emerald|teal|cyan|slate)-\d+|text-white|text-black/.test(L),D=P?`${s}${F?"":" text-gray-700 dark:text-gray-200"}`:`w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-sm text-gray-700 dark:text-gray-200 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none ${s}`,A={...d,...u};return h.jsxs(I,{ref:_,className:"relative",...A,children:[h.jsxs(oe,{ref:x,type:"button",onClick:M,disabled:a,className:`${D} flex items-center justify-between gap-2 text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(Q,{className:"truncate",children:N||" "}),h.jsx(Wn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&y&&Dn.createPortal(h.jsxs(I,{ref:S,className:"fixed z-[9999] bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",style:{top:y.top,bottom:y.bottom,left:y.left,width:y.width},role:"listbox",children:[l&&h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-600",children:h.jsx(wt,{ref:E,type:"text",value:m,onChange:O=>p(O.target.value),placeholder:c??"Search...",className:"w-full px-3 py-2 bg-gray-100 dark:bg-gray-700 border-0 rounded-lg text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:outline-none",role:"searchbox","aria-label":c??"Search"})}),h.jsxs(I,{className:"py-2 max-h-60 overflow-auto",children:[C&&C.length===0&&h.jsx(I,{className:"px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center",children:m?"No results":""}),(C??[]).map(O=>{const V=String(O.value)===String(o);return h.jsxs(oe,{type:"button",onClick:()=>!O.disabled&&T(O.value),disabled:O.disabled,className:`w-full px-4 py-3 text-left text-sm flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${V?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-700 dark:text-gray-200"} ${O.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,role:"option","aria-selected":V,children:[h.jsx(Q,{children:O.label}),V&&h.jsx(Wn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]},O.value)})]})]}),document.body)]})},ob=({children:t,className:e="",...n})=>h.jsx("option",{className:e,...n,children:t}),ib=({children:t,className:e="",...n})=>h.jsx("optgroup",{className:e,...n,children:t}),Cn=v.forwardRef(({label:t,className:e="",indeterminate:n=!1,...r},s)=>{const o=v.useRef(null),i=a=>{o.current=a,typeof s=="function"?s(a):s&&(s.current=a)};return v.useEffect(()=>{o.current&&(o.current.indeterminate=n)},[n]),h.jsx("input",{ref:i,type:"checkbox",className:e,...r})});Cn.displayName="Checkbox";const za=({children:t,className:e="",...n})=>h.jsx("table",{className:e,...n,children:t}),Ha=({children:t,className:e="",...n})=>h.jsx("thead",{className:e,...n,children:t}),Ua=({children:t,className:e="",...n})=>h.jsx("tbody",{className:e,...n,children:t}),Ba=({children:t,className:e="",...n})=>h.jsx("tfoot",{className:e,...n,children:t}),tr=({children:t,className:e="",...n})=>h.jsx("tr",{className:e,...n,children:t}),zr=({children:t,className:e="",...n})=>h.jsx("th",{className:e,...n,children:t}),Jt=({children:t,className:e="",...n})=>h.jsx("td",{className:e,...n,children:t}),Po=({children:t,className:e="",...n})=>h.jsx("nav",{className:e,...n,children:t}),Td=({children:t,className:e="",...n})=>h.jsx("section",{className:e,...n,children:t}),re=({name:t,iconStyle:e="solid",size:n,color:r,spin:s=!1,pulse:o=!1,fixedWidth:i=!1,ariaLabel:a,className:l="",style:c,...d})=>{const u={solid:"fas",regular:"far",light:"fal",duotone:"fad",brands:"fab"},f={"fa-solid":"solid","fa-regular":"regular","fa-light":"light","fa-duotone":"duotone","fa-brands":"brands"},g=typeof t=="string"?t:String(t);let m=e,p=g;for(const[_,x]of Object.entries(f))if(g.startsWith(_+" ")){m=x,p=g.substring(_.length+1);break}p=p.replace(/^fa-/,"");const y=[u[m],`fa-${p}`,n&&`fa-${n}`,s&&"fa-spin",o&&"fa-pulse",i&&"fa-fw",r,l].filter(Boolean).join(" "),b=a||p.replace(/-/g," ");return h.jsx("i",{className:y,style:c,"aria-label":b,role:"img",...d})},ab={xs:"w-3 h-3",sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6",xl:"w-8 h-8"},Ad={xs:"xs",sm:"sm",md:void 0,lg:"lg",xl:"2x"},lb=({icon:t,size:e="md",className:n="",color:r,ariaLabel:s})=>{const o=t.indexOf(":"),i=t.startsWith("fa-")||t.startsWith("fas ")||t.startsWith("far ")||t.startsWith("fab ")||t.startsWith("fal ")||t.startsWith("fad ");let a,l;i?(a="fa",l=t):o>0?(a=t.substring(0,o),l=t.substring(o+1)):(a="fa",l=t);const c=ab[e],d=r||"";switch(a){case"fa":return h.jsx(re,{name:l,size:Ad[e],className:n,color:r,ariaLabel:s});case"svg":return h.jsx("img",{src:l,className:`${c} ${d} ${n}`.trim(),alt:s||"","aria-label":s,role:"img"});case"img":return h.jsx("img",{src:l,className:`${c} ${n}`.trim(),alt:s||"",role:"img"});default:return h.jsx(re,{name:t,size:Ad[e],className:n,color:r,ariaLabel:s})}},cb=({children:t,className:e="",...n})=>h.jsx("code",{className:e,...n,children:t}),db=({children:t,className:e="",...n})=>h.jsx("pre",{className:e,...n,children:t}),ub=({children:t})=>h.jsx(h.Fragment,{children:t}),fb=({className:t="",...e})=>h.jsx("hr",{className:t,...e}),gt=({className:t="",children:e,style:n,...r})=>h.jsx("i",{className:t,style:n,...r,children:e});var de=(t=>(t.User="fa-user",t.Users="fa-users",t.UserCircle="fa-user-circle",t.UserPlus="fa-user-plus",t.UserMinus="fa-user-minus",t.UserShield="fa-user-shield",t.Search="fa-search",t.Edit="fa-edit",t.Pencil="fa-pencil-alt",t.Trash="fa-trash",t.Save="fa-save",t.Plus="fa-plus",t.Minus="fa-minus",t.Check="fa-check",t.Times="fa-times",t.Download="fa-download",t.Upload="fa-upload",t.Copy="fa-copy",t.ExternalLink="fa-external-link-alt",t.Home="fa-home",t.CaretRight="fa-caret-right",t.CaretDown="fa-caret-down",t.ChevronLeft="fa-chevron-left",t.ChevronRight="fa-chevron-right",t.ChevronUp="fa-chevron-up",t.ChevronDown="fa-chevron-down",t.AngleLeft="fa-angle-left",t.AngleRight="fa-angle-right",t.AngleUp="fa-angle-up",t.AngleDown="fa-angle-down",t.ArrowUp="fa-arrow-up",t.ArrowDown="fa-arrow-down",t.ArrowLeft="fa-arrow-left",t.ArrowRight="fa-arrow-right",t.Bars="fa-bars",t.EllipsisVertical="fa-ellipsis-v",t.EllipsisHorizontal="fa-ellipsis-h",t.CheckCircle="fa-check-circle",t.TimesCircle="fa-times-circle",t.ExclamationCircle="fa-exclamation-circle",t.ExclamationTriangle="fa-exclamation-triangle",t.InfoCircle="fa-info-circle",t.QuestionCircle="fa-question-circle",t.XCircle="fa-times-circle",t.ShoppingCart="fa-shopping-cart",t.ShoppingBag="fa-shopping-bag",t.CreditCard="fa-credit-card",t.Tag="fa-tag",t.Tags="fa-tags",t.File="fa-file",t.FileAlt="fa-file-alt",t.Folder="fa-folder",t.FolderOpen="fa-folder-open",t.Envelope="fa-envelope",t.Phone="fa-phone",t.Comment="fa-comment",t.Comments="fa-comments",t.Cog="fa-cog",t.Cogs="fa-cogs",t.Settings="fa-cog",t.Wrench="fa-wrench",t.Sliders="fa-sliders-h",t.Image="fa-image",t.Images="fa-images",t.Video="fa-video",t.Camera="fa-camera",t.Heart="fa-heart",t.Star="fa-star",t.ThumbsUp="fa-thumbs-up",t.ThumbsDown="fa-thumbs-down",t.Share="fa-share",t.Clock="fa-clock",t.Calendar="fa-calendar",t.CalendarAlt="fa-calendar-alt",t.Lock="fa-lock",t.Unlock="fa-unlock",t.Eye="fa-eye",t.EyeSlash="fa-eye-slash",t.Bell="fa-bell",t.Flag="fa-flag",t.Map="fa-map",t.MapMarker="fa-map-marker-alt",t.Globe="fa-globe",t.Link="fa-link",t.Unlink="fa-unlink",t.Sun="fa-sun",t.Moon="fa-moon",t.ChartBar="fa-chart-bar",t.ChartLine="fa-chart-line",t.ChartPie="fa-chart-pie",t.ChartArea="fa-chart-area",t.Spinner="fa-spinner",t.CircleNotch="fa-circle-notch",t.CheckBadge="fa-badge-check",t.Cube="fa-cube",t.Cubes="fa-cubes",t.Plug="fa-plug",t.PuzzlePiece="fa-puzzle-piece",t.Palette="fa-palette",t))(de||{});const hb=({title:t,content:e,imageUrl:n,imageAlt:r="",className:s="",onClick:o,style:i,id:a,editorAttrs:l})=>h.jsxs(I,{className:`card-clickable ${s}`,onClick:o,style:i,id:a,...l,children:[n&&h.jsx(I,{className:"w-full",children:h.jsx(kn,{src:n,alt:r,className:"card-image"})}),h.jsxs(I,{className:"p-4",children:[t&&h.jsx(er,{className:"card-title",children:t}),e&&h.jsx(yt,{className:"card-content",children:e})]})]}),Cs=({items:t,triggerLabel:e="작업",triggerIconName:n=de.EllipsisVertical,position:r="right",className:s="",style:o,children:i,onItemClick:a,id:l,editorAttrs:c})=>{const[d,u]=v.useState(!1),[f,g]=v.useState({top:0,left:0}),m=v.useRef(null),p=v.useRef(null),y=v.useCallback(()=>{if(!m.current)return;const x=m.current.getBoundingClientRect(),E=224;let S=r==="right"?x.right-E:x.left;const w=x.bottom+8;S<8&&(S=8),S+E>window.innerWidth-8&&(S=window.innerWidth-E-8),g({top:w,left:S})},[r]);v.useEffect(()=>{d&&y()},[d,y]),v.useEffect(()=>{if(!d)return;const x=()=>{u(!1)};return window.addEventListener("scroll",x,!0),window.addEventListener("resize",x),()=>{window.removeEventListener("scroll",x,!0),window.removeEventListener("resize",x)}},[d]),v.useEffect(()=>{const x=E=>{const S=E.target,w=m.current?.contains(S),N=p.current?.contains(S);!w&&!N&&u(!1)};return d&&document.addEventListener("mousedown",x),()=>{document.removeEventListener("mousedown",x)}},[d]);const b=x=>{x.disabled||(x.onClick&&x.onClick(),a&&a(x.id,x),u(!1))},_=d?Dn.createPortal(h.jsx("div",{ref:p,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:f.top,left:f.left},children:t.filter(x=>x.if!==!1).map(x=>{if(x.divider)return h.jsx("div",{className:"border-t border-gray-200 dark:border-gray-600 my-1"},x.id);const E=x.variant==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",S=x.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer";return h.jsxs("div",{onClick:()=>b(x),className:`flex items-center gap-3 px-4 py-3 transition-colors ${E} ${S}`,children:[x.iconName&&h.jsx(re,{name:x.iconName,className:"w-4 h-4"}),h.jsx("span",{className:"text-sm font-medium",children:x.label})]},x.id)})}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,id:l,...c,children:[i?h.jsx(I,{ref:m,onClick:()=>u(!d),className:"cursor-pointer",children:i}):h.jsxs(oe,{ref:m,onClick:()=>u(!d),className:"flex items-center gap-2 px-2 py-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[e&&h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:e}),n&&h.jsx(re,{name:n,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})]}),_]})},Hr=(t,e)=>window.G7Core?.t?.(t,e)??t,$o=({currentPage:t,totalPages:e,onPageChange:n,maxVisiblePages:r=5,showFirstLast:s=!0,showFirst:o,showLast:i,hasMorePages:a,className:l="",style:c,prevText:d,nextText:u,id:f,editorAttrs:g})=>{const m=e!=null,p=m?e:0,y=m?t<p:a??!1,b=(o??s)&&m,_=(i??s)&&m,x=v.useMemo(()=>{const N=[];if(!m)return N;if(p<=r+2)for(let C=1;C<=p;C++)N.push(C);else{const C=Math.floor(r/2);let k=Math.max(2,t-C),M=Math.min(p-1,t+C);t<=C+1?M=r:t>=p-C&&(k=p-r+1),N.push(1),k>2&&N.push("...");for(let T=k;T<=M;T++)N.push(T);M<p-1&&N.push("..."),p>1&&N.push(p)}return N},[t,p,m,r]),E=N=>{N<1||N===t||m&&N>p||n(N)},S=()=>{t>1&&n(t-1)},w=()=>{y&&n(t+1)};return h.jsxs(I,{className:`pagination ${l}`,style:c,role:"navigation","aria-label":Hr("common.pagination"),id:f,...g,children:[b&&h.jsx(oe,{onClick:()=>E(1),disabled:t===1,className:"pagination-btn","aria-label":Hr("common.first_page"),children:"«"}),h.jsx(oe,{onClick:S,disabled:t===1,className:"pagination-btn","aria-label":Hr("common.prev_page"),children:d||h.jsx(h.Fragment,{children:"‹"})}),h.jsxs(I,{className:"pagination",children:[!m&&h.jsx(Q,{className:"px-3 py-1 text-sm text-gray-700 dark:text-gray-300",children:t}),x.map((N,C)=>{if(N==="...")return h.jsx(Q,{className:"pagination-ellipsis",children:"..."},`ellipsis-${C}`);const k=N,M=k===t;return h.jsx(oe,{onClick:()=>E(k),className:M?"pagination-btn-active":"pagination-btn","aria-label":Hr("common.page_n",{n:k}),"aria-current":M?"page":void 0,children:k},k)})]}),h.jsx(oe,{onClick:w,disabled:!y,className:"pagination-btn","aria-label":Hr("common.next_page"),children:u||h.jsx(h.Fragment,{children:"›"})}),_&&h.jsx(oe,{onClick:()=>E(p),disabled:t===p,className:"pagination-btn","aria-label":Hr("common.last_page"),children:"»"})]})},Dd=(t,e)=>window.G7Core?.t?.(t,e)??t,gb=()=>window.G7Core?.locale?.supported?.()??["ko","en"],pb=()=>window.G7Core?.locale?.current?.()??"ko",Id=t=>{const e=Dd(`common.language_${t}`);return e===`common.language_${t}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[t]||t.toUpperCase():e},mb=()=>gb().map(e=>({code:e,name:Id(e),nativeName:Id(e)})),ja=({value:t={},onChange:e,inputType:n="text",layout:r="inline",availableLocales:s,defaultLocale:o,placeholder:i="",required:a=!1,disabled:l=!1,maxLength:c,rows:d=4,className:u="",name:f="multilingual_input",showCodeOnMobile:g=!1,error:m,id:p,editorAttrs:y})=>{const b=!!m,_=v.useMemo(()=>s??mb(),[s]),x=v.useMemo(()=>o??pb(),[o]),E=v.useRef(t),[S,w]=v.useState(t),N=v.useRef(JSON.stringify(t));v.useEffect(()=>{const Y=JSON.stringify(t);Y!==N.current&&(N.current=Y,E.current=t,w(t))},[t]);const C=v.useCallback((Y,Z)=>{const ee={target:{name:f,value:Y}};Z&&(ee._changedKeys=Z),e?.(ee)},[e,f]),[k,M]=v.useState(()=>{if(r==="tabs"||r==="compact"){const ee=_.map(ce=>ce.code);return[x,...ee.filter(ce=>ce!==x)]}const Y=Object.keys(t).filter(ee=>t[ee]!==void 0);if(Y.length===0)return[x];const Z=[];return Y.includes(x)&&Z.push(x),Y.forEach(ee=>{ee!==x&&Z.push(ee)}),Z}),[T,L]=v.useState(x),P=v.useMemo(()=>_.filter(Y=>!k.includes(Y.code)),[_,k]),[F,D]=v.useState(!1);v.useEffect(()=>{if(r==="tabs"||r==="compact"){const ce=_.map(ae=>ae.code),J=[x,...ce.filter(ae=>ae!==x)],he=k.join(","),le=J.join(",");he!==le&&M(J);return}const Y=Object.keys(t).filter(ce=>t[ce]!==void 0);if(Y.length===0){M([x]);return}const Z=[];Y.includes(x)&&Z.push(x),Y.forEach(ce=>{ce!==x&&Z.push(ce)});const ee=k.join(","),te=Z.join(",");ee!==te&&M(Z)},[t,x,r,_]);const A=v.useCallback((Y,Z)=>{const ee={...E.current,[Y]:Z};E.current=ee,w(ee),C(ee,[Y])},[C]),O=v.useCallback(Y=>{M(ee=>[...ee,Y]),L(Y),D(!1);const Z={...E.current,[Y]:""};E.current=Z,w(Z),C(Z)},[C]),V=v.useCallback(Y=>{if(Y===x)return;M(ee=>ee.filter(te=>te!==Y)),T===Y&&L(x);const Z={...E.current};delete Z[Y],E.current=Z,w(Z),C(Z)},[T,x,C]),H=v.useCallback(Y=>{const Z=_.find(ee=>ee.code===Y);return Z?.nativeName||Z?.name||Y.toUpperCase()},[_]),W=v.useCallback(Y=>!!S[Y]?.trim(),[S]),j=v.useCallback(()=>P.length===0?null:h.jsxs(I,{className:"relative",children:[h.jsxs(oe,{type:"button",className:"inline-flex items-center gap-1 px-3 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 rounded-full border border-dashed border-blue-300 dark:border-blue-600 transition-colors",onClick:()=>D(!F),disabled:l,children:[h.jsx(re,{name:"plus",className:"w-3 h-3"}),h.jsx(Q,{children:Dd("common.add_language")})]}),F&&h.jsx(I,{className:"absolute top-full left-0 mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-10 min-w-[120px]",children:P.map(Y=>h.jsxs(oe,{type:"button",className:"w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 first:rounded-t-lg last:rounded-b-lg transition-colors",onClick:()=>O(Y.code),children:[h.jsx(re,{name:"globe",className:"w-3.5 h-3.5 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{children:Y.nativeName||Y.name})]},Y.code))})]}),[P,F,l,O]),U=v.useCallback((Y,Z=!1)=>{const ee=b?"input input-error":"input",te=b?"textarea input-error":"textarea",ce=Y===x,J=`${i}${i?" (":""}${H(Y)}${i?")":""}`;return h.jsxs(I,{className:"space-y-2",children:[h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsxs(Q,{className:` inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium - ${ue?"bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"} - `,children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),H(K),ue&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),!ue&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",onClick:()=>z(K),disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]}),ue&&q&&G()]}),n==="textarea"?h.jsx(ir,{name:`${f}[${K}]`,value:C[K]||"",onChange:ce=>O(K,ce.target.value),placeholder:J,required:a&&ue,disabled:l,maxLength:c,rows:d,className:`${ee} resize-vertical`}):h.jsx(St,{type:"text",name:`${f}[${K}]`,value:C[K]||"",onChange:ce=>O(K,ce.target.value),placeholder:J,required:a&&ue,disabled:l,maxLength:c,className:ne})]},K)},[f,C,O,i,a,l,c,d,n,H,x,z,G,b]),W=v.useCallback(K=>{const q=b?"input input-error":"input",ne=b?"textarea input-error":"textarea",ee=`${i}${i?" (":""}${H(K)}${i?")":""}`;return h.jsx(I,{children:n==="textarea"?h.jsx(ir,{name:`${f}[${K}]`,value:C[K]||"",onChange:ue=>O(K,ue.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,rows:d,className:`${ne} resize-vertical`}):h.jsx(St,{type:"text",name:`${f}[${K}]`,value:C[K]||"",onChange:ue=>O(K,ue.target.value),placeholder:ee,required:a&&K===x,disabled:l,maxLength:c,className:q})},K)},[f,C,O,i,a,l,c,d,n,x,H,b]),Y=()=>h.jsx(I,{className:u,children:h.jsx(I,{className:"space-y-4",children:w.map((K,q)=>U(K,q===0))})}),V=()=>h.jsxs(I,{className:u,children:[h.jsxs(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:[w.map(K=>h.jsxs(ae,{type:"button",className:` + ${ce?"bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"} + `,children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),H(Y),ce&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),!ce&&h.jsx(oe,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",onClick:()=>V(Y),disabled:l,children:h.jsx(re,{name:"xmark",className:"w-3 h-3"})})]}),ce&&Z&&j()]}),n==="textarea"?h.jsx(Rn,{name:`${f}[${Y}]`,value:S[Y]||"",onChange:he=>A(Y,he.target.value),placeholder:J,required:a&&ce,disabled:l,maxLength:c,rows:d,className:`${te} resize-vertical`}):h.jsx(wt,{type:"text",name:`${f}[${Y}]`,value:S[Y]||"",onChange:he=>A(Y,he.target.value),placeholder:J,required:a&&ce,disabled:l,maxLength:c,className:ee})]},Y)},[f,S,A,i,a,l,c,d,n,H,x,V,j,b]),B=v.useCallback(Y=>{const Z=b?"input input-error":"input",ee=b?"textarea input-error":"textarea",te=`${i}${i?" (":""}${H(Y)}${i?")":""}`;return h.jsx(I,{children:n==="textarea"?h.jsx(Rn,{name:`${f}[${Y}]`,value:S[Y]||"",onChange:ce=>A(Y,ce.target.value),placeholder:te,required:a&&Y===x,disabled:l,maxLength:c,rows:d,className:`${ee} resize-vertical`}):h.jsx(wt,{type:"text",name:`${f}[${Y}]`,value:S[Y]||"",onChange:ce=>A(Y,ce.target.value),placeholder:te,required:a&&Y===x,disabled:l,maxLength:c,className:Z})},Y)},[f,S,A,i,a,l,c,d,n,x,H,b]),G=()=>h.jsx(I,{className:u,children:h.jsx(I,{className:"space-y-4",children:k.map((Y,Z)=>U(Y,Z===0))})}),z=()=>h.jsxs(I,{className:u,children:[h.jsxs(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:[k.map(Y=>h.jsxs(oe,{type:"button",className:` inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-all - ${D===K?K===x?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":K===x?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"} - `,onClick:()=>P(K),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),g?h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:H(K)}),h.jsx(Q,{className:"inline md:hidden",children:K.toUpperCase()})]}):H(K),K===x&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),j(K)&&D!==K&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"}),K!==x&&h.jsx(ae,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors",onClick:q=>{q.stopPropagation(),z(K)},disabled:l,children:h.jsx(oe,{name:"xmark",className:"w-3 h-3"})})]},K)),G()]}),w.map(K=>h.jsx(I,{className:D===K?"block":"hidden",children:W(K)},K))]}),X=()=>{const K=b?"input input-error":"input",q=b?"textarea input-error":"textarea";return h.jsx(I,{className:u,children:h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(I,{className:"flex items-center shrink-0",children:w.map(ne=>h.jsxs(ae,{type:"button",className:` + ${T===Y?Y===x?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":Y===x?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"} + `,onClick:()=>L(Y),children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),g?h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:H(Y)}),h.jsx(Q,{className:"inline md:hidden",children:Y.toUpperCase()})]}):H(Y),Y===x&&h.jsx(Q,{className:"text-red-500 dark:text-red-400",children:"*"}),W(Y)&&T!==Y&&h.jsx(re,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"}),Y!==x&&h.jsx(oe,{type:"button",className:"ml-0.5 p-0.5 rounded-full hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors",onClick:Z=>{Z.stopPropagation(),V(Y)},disabled:l,children:h.jsx(re,{name:"xmark",className:"w-3 h-3"})})]},Y)),j()]}),k.map(Y=>h.jsx(I,{className:T===Y?"block":"hidden",children:B(Y)},Y))]}),X=()=>{const Y=b?"input input-error":"input",Z=b?"textarea input-error":"textarea";return h.jsx(I,{className:u,children:h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(I,{className:"flex items-center shrink-0",children:k.map(ee=>h.jsxs(oe,{type:"button",className:` px-2 py-1.5 text-xs font-medium border-y border-l first:rounded-l last:rounded-r last:border-r transition-colors - ${D===ne?"bg-blue-500 text-white border-blue-500 dark:bg-blue-600 dark:border-blue-600":"bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"} - `,onClick:()=>P(ne),title:H(ne),children:[ne.toUpperCase(),ne===x&&h.jsx(Q,{className:"text-red-400 ml-0.5",children:"*"})]},ne))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(ir,{name:`${f}[${D}]`,value:C[D]||"",onChange:ne=>O(D,ne.target.value),placeholder:i,required:a&&D===x,disabled:l,maxLength:c,rows:d,className:q}):h.jsx(St,{type:"text",name:`${f}[${D}]`,value:C[D]||"",onChange:ne=>O(D,ne.target.value),placeholder:i,required:a&&D===x,disabled:l,maxLength:c,className:K})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?X():r==="tabs"?V():Y(),b&&m&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};Za.displayName="MultilingualInput";const Ab=()=>window.G7Core,Qa=({checked:e,value:t,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>e??t??!1);v.useEffect(()=>{f(e??t??!1)},[e,t]);const g=_=>{const x=_.target.checked;f(x),n&&!r&&n(_)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const _=!u;if(f(_),n){const x=Ab();x?.createChangeEvent&&n(x.createChangeEvent({checked:_,name:l}))}}},b=_=>{r||_.key!==" "&&_.key!=="Enter"&&_.key!=="Spacebar"||(_.preventDefault(),y())};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,role:"switch","aria-checked":u,"aria-disabled":r||void 0,"aria-label":s??l,tabIndex:r?-1:0,onKeyDown:b,onClick:_=>{_.preventDefault(),_.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},Ko=({status:e,label:t,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:de.CheckCircle,warning:de.ExclamationTriangle,error:de.XCircle,info:de.InfoCircle,pending:de.Clock,default:de.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[e],f=t||d[e],g=l[e];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(oe,{name:u,className:"w-4 h-4"}),h.jsx(Q,{children:f})]})},Bd={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},Db={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},jd={sm:"px-1.5 py-0.5 text-[10px]",md:"px-2 py-0.5 text-xs",lg:"px-2.5 py-1 text-sm"},Wd=({color:e,variant:t,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=e||(t?Db[t]:void 0)||"gray",d=Bd[c]||Bd.gray,u=jd[r]||jd.md;return h.jsxs(Q,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};function Gd(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function Ib(e){if(Array.isArray(e))return e}function Lb(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function Pb(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function $b(e,t){return Ib(e)||Lb(e,t)||Fb(e,t)||Pb()}function Fb(e,t){if(e){if(typeof e=="string")return Gd(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?Gd(e,t):void 0}}const Yd=Object.entries,Xd=Object.setPrototypeOf,zb=Object.isFrozen,Vb=Object.getPrototypeOf,Hb=Object.getOwnPropertyDescriptor;let Zt=Object.freeze,hn=Object.seal,Zr=Object.create,Kd=typeof Reflect<"u"&&Reflect,Ja=Kd.apply,Ra=Kd.construct;Zt||(Zt=function(t){return t}),hn||(hn=function(t){return t}),Ja||(Ja=function(t,n){for(var r=arguments.length,s=new Array(r>2?r-2:0),o=2;o<r;o++)s[o-2]=arguments[o];return t.apply(n,s)}),Ra||(Ra=function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return new t(...r)});const kr=_t(Array.prototype.forEach),Ub=_t(Array.prototype.lastIndexOf),qd=_t(Array.prototype.pop),Qr=_t(Array.prototype.push),Bb=_t(Array.prototype.splice),Qt=Array.isArray,Ps=_t(String.prototype.toLowerCase),el=_t(String.prototype.toString),Zd=_t(String.prototype.match),Jr=_t(String.prototype.replace),Qd=_t(String.prototype.indexOf),jb=_t(String.prototype.trim),Wb=_t(Number.prototype.toString),Gb=_t(Boolean.prototype.toString),Jd=typeof BigInt>"u"?null:_t(BigInt.prototype.toString),Rd=typeof Symbol>"u"?null:_t(Symbol.prototype.toString),xt=_t(Object.prototype.hasOwnProperty),$s=_t(Object.prototype.toString),zt=_t(RegExp.prototype.test),Rr=Yb(TypeError);function _t(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return Ja(e,t,r)}}function Yb(e){return function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return Ra(e,n)}}function Ve(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:Ps;if(Xd&&Xd(e,null),!Qt(t))return e;let r=t.length;for(;r--;){let s=t[r];if(typeof s=="string"){const o=n(s);o!==s&&(zb(t)||(t[r]=o),s=o)}e[s]=!0}return e}function Xb(e){for(let t=0;t<e.length;t++)xt(e,t)||(e[t]=null);return e}function Bt(e){const t=Zr(null);for(const r of Yd(e)){var n=$b(r,2);const s=n[0],o=n[1];xt(e,s)&&(Qt(o)?t[s]=Xb(o):o&&typeof o=="object"&&o.constructor===Object?t[s]=Bt(o):t[s]=o)}return t}function Kb(e){switch(typeof e){case"string":return e;case"number":return Wb(e);case"boolean":return Gb(e);case"bigint":return Jd?Jd(e):"0";case"symbol":return Rd?Rd(e):"Symbol()";case"undefined":return $s(e);case"function":case"object":{if(e===null)return $s(e);const t=e,n=Fn(t,"toString");if(typeof n=="function"){const r=n(t);return typeof r=="string"?r:$s(r)}return $s(e)}default:return $s(e)}}function Fn(e,t){for(;e!==null;){const r=Hb(e,t);if(r){if(r.get)return _t(r.get);if(typeof r.value=="function")return _t(r.value)}e=Vb(e)}function n(){return null}return n}function qb(e){try{return zt(e,""),!0}catch{return!1}}const eu=Zt(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),tl=Zt(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),nl=Zt(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Zb=Zt(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),rl=Zt(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Qb=Zt(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),tu=Zt(["#text"]),nu=Zt(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","command","commandfor","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns"]),sl=Zt(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),ru=Zt(["accent","accentunder","align","bevelled","close","columnalign","columnlines","columnspacing","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lquote","lspace","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),qo=Zt(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),Jb=hn(/{{[\w\W]*|^[\w\W]*}}/g),Rb=hn(/<%[\w\W]*|^[\w\W]*%>/g),ey=hn(/\${[\w\W]*/g),ty=hn(/^data-[\-\w.\u00B7-\uFFFF]+$/),ny=hn(/^aria-[\-\w]+$/),su=hn(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),ry=hn(/^(?:\w+script|data):/i),sy=hn(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),oy=hn(/^html$/i),iy=hn(/^[a-z][.\w]*(-[.\w]+)+$/i),zn={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,progressingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},ay=function(){return typeof window>"u"?null:window},ly=function(t,n){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let r=null;const s="data-tt-policy-suffix";n&&n.hasAttribute(s)&&(r=n.getAttribute(s));const o="dompurify"+(r?"#"+r:"");try{return t.createPolicy(o,{createHTML(i){return i},createScriptURL(i){return i}})}catch{return console.warn("TrustedTypes policy "+o+" could not be created."),null}},ou=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function iu(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:ay();const t=se=>iu(se);if(t.version="3.4.8",t.removed=[],!e||!e.document||e.document.nodeType!==zn.document||!e.Element)return t.isSupported=!1,t;let n=e.document;const r=n,s=r.currentScript;e.DocumentFragment;const o=e.HTMLTemplateElement,i=e.Node,a=e.Element,l=e.NodeFilter,c=e.NamedNodeMap;c===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;const d=e.DOMParser,u=e.trustedTypes,f=a.prototype,g=Fn(f,"cloneNode"),m=Fn(f,"remove"),p=Fn(f,"nextSibling"),y=Fn(f,"childNodes"),b=Fn(f,"parentNode"),_=Fn(f,"shadowRoot"),x=Fn(f,"attributes"),M=i&&i.prototype?Fn(i.prototype,"nodeType"):null,C=i&&i.prototype?Fn(i.prototype,"nodeName"):null;if(typeof o=="function"){const se=n.createElement("template");se.content&&se.content.ownerDocument&&(n=se.content.ownerDocument)}let k,N="",S=0;const w=function(B){if(S>0)throw Rr('The configured TRUSTED_TYPES_POLICY.createHTML must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose createHTML wraps DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.');S++;try{return k.createHTML(B)}finally{S--}},E=n,D=E.implementation,P=E.createNodeIterator,L=E.createDocumentFragment,$=E.getElementsByTagName,A=r.importNode;let O=ou();t.isSupported=typeof Yd=="function"&&typeof b=="function"&&D&&D.createHTMLDocument!==void 0;const T=Jb,z=Rb,H=ey,j=ty,G=ny,U=ry,W=sy,Y=iy;let V=su,X=null;const K=Ve({},[...eu,...tl,...nl,...rl,...tu]);let q=null;const ne=Ve({},[...nu,...sl,...ru,...qo]);let ee=Object.seal(Zr(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ue=null,J=null;const ce=Object.seal(Zr(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let me=!0,le=!0,ve=!1,ge=!0,pe=!1,ke=!0,$e=!1,ie=!1,Me=!1,Ae=!1,Ie=!1,Xe=!1,gt=!0,pt=!1;const Be="user-content-";let et=!0,je=!1,re={},fe=null;const be=Ve({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let we=null;const xe=Ve({},["audio","video","img","source","image","track"]);let He=null;const _e=Ve({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Le="http://www.w3.org/1998/Math/MathML",Ce="http://www.w3.org/2000/svg",We="http://www.w3.org/1999/xhtml";let Ze=We,Fe=!1,Ne=null;const ze=Ve({},[Le,Ce,We],el);let Ke=Ve({},["mi","mo","mn","ms","mtext"]),tt=Ve({},["annotation-xml"]);const at=Ve({},["title","style","font","a","script"]);let ct=null;const wn=["application/xhtml+xml","text/html"],yt="text/html";let Ue=null,Ct=null;const Ut=n.createElement("form"),st=function(B){return B instanceof RegExp||B instanceof Function},kn=function(){let B=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(Ct&&Ct===B)return;(!B||typeof B!="object")&&(B={}),B=Bt(B),ct=wn.indexOf(B.PARSER_MEDIA_TYPE)===-1?yt:B.PARSER_MEDIA_TYPE,Ue=ct==="application/xhtml+xml"?el:Ps,X=xt(B,"ALLOWED_TAGS")&&Qt(B.ALLOWED_TAGS)?Ve({},B.ALLOWED_TAGS,Ue):K,q=xt(B,"ALLOWED_ATTR")&&Qt(B.ALLOWED_ATTR)?Ve({},B.ALLOWED_ATTR,Ue):ne,Ne=xt(B,"ALLOWED_NAMESPACES")&&Qt(B.ALLOWED_NAMESPACES)?Ve({},B.ALLOWED_NAMESPACES,el):ze,He=xt(B,"ADD_URI_SAFE_ATTR")&&Qt(B.ADD_URI_SAFE_ATTR)?Ve(Bt(_e),B.ADD_URI_SAFE_ATTR,Ue):_e,we=xt(B,"ADD_DATA_URI_TAGS")&&Qt(B.ADD_DATA_URI_TAGS)?Ve(Bt(xe),B.ADD_DATA_URI_TAGS,Ue):xe,fe=xt(B,"FORBID_CONTENTS")&&Qt(B.FORBID_CONTENTS)?Ve({},B.FORBID_CONTENTS,Ue):be,ue=xt(B,"FORBID_TAGS")&&Qt(B.FORBID_TAGS)?Ve({},B.FORBID_TAGS,Ue):Bt({}),J=xt(B,"FORBID_ATTR")&&Qt(B.FORBID_ATTR)?Ve({},B.FORBID_ATTR,Ue):Bt({}),re=xt(B,"USE_PROFILES")?B.USE_PROFILES&&typeof B.USE_PROFILES=="object"?Bt(B.USE_PROFILES):B.USE_PROFILES:!1,me=B.ALLOW_ARIA_ATTR!==!1,le=B.ALLOW_DATA_ATTR!==!1,ve=B.ALLOW_UNKNOWN_PROTOCOLS||!1,ge=B.ALLOW_SELF_CLOSE_IN_ATTR!==!1,pe=B.SAFE_FOR_TEMPLATES||!1,ke=B.SAFE_FOR_XML!==!1,$e=B.WHOLE_DOCUMENT||!1,Ae=B.RETURN_DOM||!1,Ie=B.RETURN_DOM_FRAGMENT||!1,Xe=B.RETURN_TRUSTED_TYPE||!1,Me=B.FORCE_BODY||!1,gt=B.SANITIZE_DOM!==!1,pt=B.SANITIZE_NAMED_PROPS||!1,et=B.KEEP_CONTENT!==!1,je=B.IN_PLACE||!1,V=qb(B.ALLOWED_URI_REGEXP)?B.ALLOWED_URI_REGEXP:su,Ze=typeof B.NAMESPACE=="string"?B.NAMESPACE:We,Ke=xt(B,"MATHML_TEXT_INTEGRATION_POINTS")&&B.MATHML_TEXT_INTEGRATION_POINTS&&typeof B.MATHML_TEXT_INTEGRATION_POINTS=="object"?Bt(B.MATHML_TEXT_INTEGRATION_POINTS):Ve({},["mi","mo","mn","ms","mtext"]),tt=xt(B,"HTML_INTEGRATION_POINTS")&&B.HTML_INTEGRATION_POINTS&&typeof B.HTML_INTEGRATION_POINTS=="object"?Bt(B.HTML_INTEGRATION_POINTS):Ve({},["annotation-xml"]);const te=xt(B,"CUSTOM_ELEMENT_HANDLING")&&B.CUSTOM_ELEMENT_HANDLING&&typeof B.CUSTOM_ELEMENT_HANDLING=="object"?Bt(B.CUSTOM_ELEMENT_HANDLING):Zr(null);if(ee=Zr(null),xt(te,"tagNameCheck")&&st(te.tagNameCheck)&&(ee.tagNameCheck=te.tagNameCheck),xt(te,"attributeNameCheck")&&st(te.attributeNameCheck)&&(ee.attributeNameCheck=te.attributeNameCheck),xt(te,"allowCustomizedBuiltInElements")&&typeof te.allowCustomizedBuiltInElements=="boolean"&&(ee.allowCustomizedBuiltInElements=te.allowCustomizedBuiltInElements),pe&&(le=!1),Ie&&(Ae=!0),re&&(X=Ve({},tu),q=Zr(null),re.html===!0&&(Ve(X,eu),Ve(q,nu)),re.svg===!0&&(Ve(X,tl),Ve(q,sl),Ve(q,qo)),re.svgFilters===!0&&(Ve(X,nl),Ve(q,sl),Ve(q,qo)),re.mathMl===!0&&(Ve(X,rl),Ve(q,ru),Ve(q,qo))),ce.tagCheck=null,ce.attributeCheck=null,xt(B,"ADD_TAGS")&&(typeof B.ADD_TAGS=="function"?ce.tagCheck=B.ADD_TAGS:Qt(B.ADD_TAGS)&&(X===K&&(X=Bt(X)),Ve(X,B.ADD_TAGS,Ue))),xt(B,"ADD_ATTR")&&(typeof B.ADD_ATTR=="function"?ce.attributeCheck=B.ADD_ATTR:Qt(B.ADD_ATTR)&&(q===ne&&(q=Bt(q)),Ve(q,B.ADD_ATTR,Ue))),xt(B,"ADD_URI_SAFE_ATTR")&&Qt(B.ADD_URI_SAFE_ATTR)&&Ve(He,B.ADD_URI_SAFE_ATTR,Ue),xt(B,"FORBID_CONTENTS")&&Qt(B.FORBID_CONTENTS)&&(fe===be&&(fe=Bt(fe)),Ve(fe,B.FORBID_CONTENTS,Ue)),xt(B,"ADD_FORBID_CONTENTS")&&Qt(B.ADD_FORBID_CONTENTS)&&(fe===be&&(fe=Bt(fe)),Ve(fe,B.ADD_FORBID_CONTENTS,Ue)),et&&(X["#text"]=!0),$e&&Ve(X,["html","head","body"]),X.table&&(Ve(X,["tbody"]),delete ue.tbody),B.TRUSTED_TYPES_POLICY){if(typeof B.TRUSTED_TYPES_POLICY.createHTML!="function")throw Rr('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof B.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Rr('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');const he=k;k=B.TRUSTED_TYPES_POLICY;try{N=w("")}catch(ot){throw k=he,ot}}else k===void 0&&B.TRUSTED_TYPES_POLICY!==null&&(k=ly(u,s)),k&&typeof N=="string"&&(N=w(""));(O.uponSanitizeElement.length>0||O.uponSanitizeAttribute.length>0)&&X===K&&(X=Bt(X)),O.uponSanitizeAttribute.length>0&&q===ne&&(q=Bt(q)),Zt&&Zt(B),Ct=B},qt=Ve({},[...tl,...nl,...Zb]),Ln=Ve({},[...rl,...Qb]),jr=function(B){let te=b(B);(!te||!te.tagName)&&(te={namespaceURI:Ze,tagName:"template"});const he=Ps(B.tagName),ot=Ps(te.tagName);return Ne[B.namespaceURI]?B.namespaceURI===Ce?te.namespaceURI===We?he==="svg":te.namespaceURI===Le?he==="svg"&&(ot==="annotation-xml"||Ke[ot]):!!qt[he]:B.namespaceURI===Le?te.namespaceURI===We?he==="math":te.namespaceURI===Ce?he==="math"&&tt[ot]:!!Ln[he]:B.namespaceURI===We?te.namespaceURI===Ce&&!tt[ot]||te.namespaceURI===Le&&!Ke[ot]?!1:!Ln[he]&&(at[he]||!qt[he]):!!(ct==="application/xhtml+xml"&&Ne[B.namespaceURI]):!1},fn=function(B){Qr(t.removed,{element:B});try{b(B).removeChild(B)}catch{m(B)}},qn=function(B,te){try{Qr(t.removed,{attribute:te.getAttributeNode(B),from:te})}catch{Qr(t.removed,{attribute:null,from:te})}if(te.removeAttribute(B),B==="is")if(Ae||Ie)try{fn(te)}catch{}else try{te.setAttribute(B,"")}catch{}},Wo=function(B){let te=null,he=null;if(Me)B="<remove></remove>"+B;else{const ft=Zd(B,/^[\r\n\t ]+/);he=ft&&ft[0]}ct==="application/xhtml+xml"&&Ze===We&&(B='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+B+"</body></html>");const ot=k?w(B):B;if(Ze===We)try{te=new d().parseFromString(ot,ct)}catch{}if(!te||!te.documentElement){te=D.createDocument(Ze,"template",null);try{te.documentElement.innerHTML=Fe?N:ot}catch{}}const Ge=te.body||te.documentElement;return B&&he&&Ge.insertBefore(n.createTextNode(he),Ge.childNodes[0]||null),Ze===We?$.call(te,$e?"html":"body")[0]:$e?te.documentElement:Ge},Go=function(B){return P.call(B.ownerDocument||B,B,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT|l.SHOW_PROCESSING_INSTRUCTION|l.SHOW_CDATA_SECTION,null)},Is=function(B){var te,he;B.normalize();const ot=P.call(B.ownerDocument||B,B,l.SHOW_TEXT|l.SHOW_COMMENT|l.SHOW_CDATA_SECTION|l.SHOW_PROCESSING_INSTRUCTION,null);let Ge=ot.nextNode();for(;Ge;){let At=Ge.data;kr([T,z,H],Sn=>{At=Jr(At,Sn," ")}),Ge.data=At,Ge=ot.nextNode()}const ft=(te=(he=B.querySelectorAll)===null||he===void 0?void 0:he.call(B,"template"))!==null&&te!==void 0?te:[];kr(Array.from(ft),At=>{or(At.content)&&Is(At.content)})},sr=function(B){const te=C?C(B):null;return typeof te!="string"||Ue(te)!=="form"?!1:typeof B.nodeName!="string"||typeof B.textContent!="string"||typeof B.removeChild!="function"||B.attributes!==x(B)||typeof B.removeAttribute!="function"||typeof B.setAttribute!="function"||typeof B.namespaceURI!="string"||typeof B.insertBefore!="function"||typeof B.hasChildNodes!="function"||B.nodeType!==M(B)||B.childNodes!==y(B)},or=function(B){if(!M||typeof B!="object"||B===null)return!1;try{return M(B)===zn.documentFragment}catch{return!1}},Wr=function(B){if(!M||typeof B!="object"||B===null)return!1;try{return typeof M(B)=="number"}catch{return!1}};function Cn(se,B,te){kr(se,he=>{he.call(t,B,te,Ct)})}const Ha=function(B){let te=null;if(Cn(O.beforeSanitizeElements,B,null),sr(B))return fn(B),!0;const he=Ue(C?C(B):B.nodeName);if(Cn(O.uponSanitizeElement,B,{tagName:he,allowedTags:X}),ke&&B.hasChildNodes()&&!Wr(B.firstElementChild)&&zt(/<[/\w!]/g,B.innerHTML)&&zt(/<[/\w!]/g,B.textContent)||ke&&B.namespaceURI===We&&he==="style"&&Wr(B.firstElementChild)||B.nodeType===zn.progressingInstruction||ke&&B.nodeType===zn.comment&&zt(/<[/\w]/g,B.data))return fn(B),!0;if(ue[he]||!(ce.tagCheck instanceof Function&&ce.tagCheck(he))&&!X[he]){if(!ue[he]&&Z(he)&&(ee.tagNameCheck instanceof RegExp&&zt(ee.tagNameCheck,he)||ee.tagNameCheck instanceof Function&&ee.tagNameCheck(he)))return!1;if(et&&!fe[he]){const Ge=b(B),ft=y(B);if(ft&&Ge){const At=ft.length;for(let Sn=At-1;Sn>=0;--Sn){const Pn=g(ft[Sn],!0);Ge.insertBefore(Pn,p(B))}}}return fn(B),!0}return(M?M(B):B.nodeType)===zn.element&&!jr(B)||(he==="noscript"||he==="noembed"||he==="noframes")&&zt(/<\/no(script|embed|frames)/i,B.innerHTML)?(fn(B),!0):(pe&&B.nodeType===zn.text&&(te=B.textContent,kr([T,z,H],Ge=>{te=Jr(te,Ge," ")}),B.textContent!==te&&(Qr(t.removed,{element:B.cloneNode()}),B.textContent=te)),Cn(O.afterSanitizeElements,B,null),!1)},Ua=function(B,te,he){if(J[te]||gt&&(te==="id"||te==="name")&&(he in n||he in Ut))return!1;const ot=q[te]||ce.attributeCheck instanceof Function&&ce.attributeCheck(te,B);if(!(le&&!J[te]&&zt(j,te))){if(!(me&&zt(G,te))){if(!ot||J[te]){if(!(Z(B)&&(ee.tagNameCheck instanceof RegExp&&zt(ee.tagNameCheck,B)||ee.tagNameCheck instanceof Function&&ee.tagNameCheck(B))&&(ee.attributeNameCheck instanceof RegExp&&zt(ee.attributeNameCheck,te)||ee.attributeNameCheck instanceof Function&&ee.attributeNameCheck(te,B))||te==="is"&&ee.allowCustomizedBuiltInElements&&(ee.tagNameCheck instanceof RegExp&&zt(ee.tagNameCheck,he)||ee.tagNameCheck instanceof Function&&ee.tagNameCheck(he))))return!1}else if(!He[te]){if(!zt(V,Jr(he,W,""))){if(!((te==="src"||te==="xlink:href"||te==="href")&&B!=="script"&&Qd(he,"data:")===0&&we[B])){if(!(ve&&!zt(U,Jr(he,W,"")))){if(he)return!1}}}}}}return!0},Ld=Ve({},["annotation-xml","color-profile","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","missing-glyph"]),Z=function(B){return!Ld[Ps(B)]&&zt(Y,B)},ye=function(B){Cn(O.beforeSanitizeAttributes,B,null);const te=B.attributes;if(!te||sr(B))return;const he={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:q,forceKeepAttr:void 0};let ot=te.length;for(;ot--;){const Ge=te[ot],ft=Ge.name,At=Ge.namespaceURI,Sn=Ge.value,Pn=Ue(ft),Pd=Sn;let Ft=ft==="value"?Pd:jb(Pd);if(he.attrName=Pn,he.attrValue=Ft,he.keepAttr=!0,he.forceKeepAttr=void 0,Cn(O.uponSanitizeAttribute,B,he),Ft=he.attrValue,pt&&(Pn==="id"||Pn==="name")&&Qd(Ft,Be)!==0&&(qn(ft,B),Ft=Be+Ft),ke&&zt(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,Ft)){qn(ft,B);continue}if(Pn==="attributename"&&Zd(Ft,"href")){qn(ft,B);continue}if(he.forceKeepAttr)continue;if(!he.keepAttr){qn(ft,B);continue}if(!ge&&zt(/\/>/i,Ft)){qn(ft,B);continue}pe&&kr([T,z,H],ub=>{Ft=Jr(Ft,ub," ")});const db=Ue(B.nodeName);if(!Ua(db,Pn,Ft)){qn(ft,B);continue}if(k&&typeof u=="object"&&typeof u.getAttributeType=="function"&&!At)switch(u.getAttributeType(db,Pn)){case"TrustedHTML":{Ft=w(Ft);break}case"TrustedScriptURL":{Ft=k.createScriptURL(Ft);break}}if(Ft!==Pd)try{At?B.setAttributeNS(At,ft,Ft):B.setAttribute(ft,Ft),sr(B)?fn(B):qd(t.removed)}catch{qn(ft,B)}}Cn(O.afterSanitizeAttributes,B,null)},De=function(B){let te=null;const he=Go(B);for(Cn(O.beforeSanitizeShadowDOM,B,null);te=he.nextNode();)if(Cn(O.uponSanitizeShadowNode,te,null),Ha(te),ye(te),or(te.content)&&De(te.content),(M?M(te):te.nodeType)===zn.element){const Ge=_?_(te):te.shadowRoot;or(Ge)&&(Qe(Ge),De(Ge))}Cn(O.afterSanitizeShadowDOM,B,null)},Qe=function(B){const te=M?M(B):B.nodeType;if(te===zn.element){const Ge=_?_(B):B.shadowRoot;or(Ge)&&(Qe(Ge),De(Ge))}const he=y?y(B):B.childNodes;if(!he)return;const ot=[];kr(he,Ge=>{Qr(ot,Ge)});for(const Ge of ot)Qe(Ge);if(te===zn.element){const Ge=C?C(B):null;if(typeof Ge=="string"&&Ue(Ge)==="template"){const ft=B.content;or(ft)&&Qe(ft)}}};return t.sanitize=function(se){let B=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},te=null,he=null,ot=null,Ge=null;if(Fe=!se,Fe&&(se="<!-->"),typeof se!="string"&&!Wr(se)&&(se=Kb(se),typeof se!="string"))throw Rr("dirty is not a string, aborting");if(!t.isSupported)return se;if(ie||kn(B),t.removed=[],typeof se=="string"&&(je=!1),je){const Sn=C?C(se):se.nodeName;if(typeof Sn=="string"){const Pn=Ue(Sn);if(!X[Pn]||ue[Pn])throw Rr("root node is forbidden and cannot be sanitized in-place")}if(sr(se))throw Rr("root node is clobbered and cannot be sanitized in-place");Qe(se)}else if(Wr(se))te=Wo("<!---->"),he=te.ownerDocument.importNode(se,!0),he.nodeType===zn.element&&he.nodeName==="BODY"||he.nodeName==="HTML"?te=he:te.appendChild(he),Qe(he);else{if(!Ae&&!pe&&!$e&&se.indexOf("<")===-1)return k&&Xe?w(se):se;if(te=Wo(se),!te)return Ae?null:Xe?N:""}te&&Me&&fn(te.firstChild);const ft=Go(je?se:te);for(;ot=ft.nextNode();)Ha(ot),ye(ot),or(ot.content)&&De(ot.content);if(je)return pe&&Is(se),se;if(Ae){if(pe&&Is(te),Ie)for(Ge=L.call(te.ownerDocument);te.firstChild;)Ge.appendChild(te.firstChild);else Ge=te;return(q.shadowroot||q.shadowrootmode)&&(Ge=A.call(r,Ge,!0)),Ge}let At=$e?te.outerHTML:te.innerHTML;return $e&&X["!doctype"]&&te.ownerDocument&&te.ownerDocument.doctype&&te.ownerDocument.doctype.name&&zt(oy,te.ownerDocument.doctype.name)&&(At="<!DOCTYPE "+te.ownerDocument.doctype.name+`> -`+At),pe&&kr([T,z,H],Sn=>{At=Jr(At,Sn," ")}),k&&Xe?w(At):At},t.setConfig=function(){let se=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};kn(se),ie=!0},t.clearConfig=function(){Ct=null,ie=!1},t.isValidAttribute=function(se,B,te){Ct||kn({});const he=Ue(se),ot=Ue(B);return Ua(he,ot,te)},t.addHook=function(se,B){typeof B=="function"&&Qr(O[se],B)},t.removeHook=function(se,B){if(B!==void 0){const te=Ub(O[se],B);return te===-1?void 0:Bb(O[se],te,1)[0]}return qd(O[se])},t.removeHooks=function(se){O[se]=[]},t.removeAllHooks=function(){O=ou()},t}var cy=iu();const ol=({content:e,text:t,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=t??e??"";if(!a||a.trim()==="")return null;if(!n){const u=` + ${T===ee?"bg-blue-500 text-white border-blue-500 dark:bg-blue-600 dark:border-blue-600":"bg-white text-gray-600 border-gray-200 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700"} + `,onClick:()=>L(ee),title:H(ee),children:[ee.toUpperCase(),ee===x&&h.jsx(Q,{className:"text-red-400 ml-0.5",children:"*"})]},ee))}),h.jsx(I,{className:"flex-1 min-w-0",children:n==="textarea"?h.jsx(Rn,{name:`${f}[${T}]`,value:S[T]||"",onChange:ee=>A(T,ee.target.value),placeholder:i,required:a&&T===x,disabled:l,maxLength:c,rows:d,className:Z}):h.jsx(wt,{type:"text",name:`${f}[${T}]`,value:S[T]||"",onChange:ee=>A(T,ee.target.value),placeholder:i,required:a&&T===x,disabled:l,maxLength:c,className:Y})})]})})};return h.jsxs(I,{className:l?"opacity-50 cursor-not-allowed":void 0,id:p,...y,children:[r==="compact"?X():r==="tabs"?z():G(),b&&m&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:m})]})};ja.displayName="MultilingualInput";const bb=()=>window.G7Core,Wa=({checked:t,value:e,onChange:n,disabled:r=!1,label:s,description:o,size:i="md",className:a="",name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(()=>t??e??!1);v.useEffect(()=>{f(t??e??!1)},[t,e]);const g=_=>{const x=_.target.checked;f(x),n&&!r&&n(_)},p={sm:{track:"w-9 h-5",thumb:"after:h-4 after:w-4"},md:{track:"w-11 h-6",thumb:"after:h-5 after:w-5"},lg:{track:"w-14 h-7",thumb:"after:h-6 after:w-6"}}[i],y=()=>{if(!r){const _=!u;if(f(_),n){const x=bb();x?.createChangeEvent&&n(x.createChangeEvent({checked:_,name:l}))}}},b=_=>{r||_.key!==" "&&_.key!=="Enter"&&_.key!=="Spacebar"||(_.preventDefault(),y())};return h.jsxs("div",{className:`toggle-container ${a}`,id:c,...d,children:[h.jsxs("div",{className:`toggle-switch-wrapper ${r?"cursor-not-allowed opacity-50":"cursor-pointer"}`,role:"switch","aria-checked":u,"aria-disabled":r||void 0,"aria-label":s??l,tabIndex:r?-1:0,onKeyDown:b,onClick:_=>{_.preventDefault(),_.stopPropagation(),y()},children:[h.jsx("input",{type:"checkbox",id:l,name:l,checked:u,onChange:g,disabled:r,className:"sr-only peer",tabIndex:-1}),h.jsx("div",{className:`toggle-switch-track ${p.track} ${p.thumb}`})]}),s&&h.jsx("span",{className:`toggle-label ${r?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:y,children:s}),o&&h.jsx("p",{className:"toggle-description",children:o})]})},Fo=({status:t,label:e,showIcon:n=!0,iconName:r,className:s="",style:o,id:i,editorAttrs:a})=>{const l={success:"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800",warning:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800",error:"bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800",pending:"bg-gray-50 dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600",default:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-600"},c={success:de.CheckCircle,warning:de.ExclamationTriangle,error:de.XCircle,info:de.InfoCircle,pending:de.Clock,default:de.QuestionCircle},d={success:"성공",warning:"경고",error:"오류",info:"정보",pending:"대기 중",default:"기본"},u=r||c[t],f=e||d[t],g=l[t];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium border ${g} ${s}`,style:o,id:i,...a,children:[n&&h.jsx(re,{name:u,className:"w-4 h-4"}),h.jsx(Q,{children:f})]})},Ld={blue:"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300",green:"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300",red:"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300",gray:"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300",yellow:"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300",purple:"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300",orange:"bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300",teal:"bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300",cyan:"bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300"},yb={success:"green",warning:"yellow",danger:"red",error:"red",info:"blue",primary:"blue",secondary:"gray",text:"gray",default:"gray"},Pd={sm:"px-1.5 py-0.5 text-[10px]",md:"px-2 py-0.5 text-xs",lg:"px-2.5 py-1 text-sm"},$d=({color:t,variant:e,text:n,size:r="md",className:s="",style:o,children:i,id:a,editorAttrs:l})=>{const c=t||(e?yb[e]:void 0)||"gray",d=Ld[c]||Ld.gray,u=Pd[r]||Pd.md;return h.jsxs(Q,{className:`inline-flex items-center rounded-full font-medium ${u} ${d} ${s}`,style:o,id:a,...l,children:[n,i]})};/*! @license DOMPurify 3.1.7 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.7/LICENSE */const{entries:Fd,setPrototypeOf:Vd,isFrozen:vb,getPrototypeOf:xb,getOwnPropertyDescriptor:wb}=Object;let{freeze:Xt,seal:un,create:zd}=Object,{apply:Ga,construct:Ya}=typeof Reflect<"u"&&Reflect;Xt||(Xt=function(e){return e}),un||(un=function(e){return e}),Ga||(Ga=function(e,n,r){return e.apply(n,r)}),Ya||(Ya=function(e,n){return new e(...n)});const Vo=an(Array.prototype.forEach),Hd=an(Array.prototype.pop),Ss=an(Array.prototype.push),zo=an(String.prototype.toLowerCase),Xa=an(String.prototype.toString),Ud=an(String.prototype.match),_s=an(String.prototype.replace),kb=an(String.prototype.indexOf),Cb=an(String.prototype.trim),Sn=an(Object.prototype.hasOwnProperty),Kt=an(RegExp.prototype.test),Es=Sb(TypeError);function an(t){return function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return Ga(t,e,r)}}function Sb(t){return function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return Ya(t,n)}}function je(t,e){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:zo;Vd&&Vd(t,null);let r=e.length;for(;r--;){let s=e[r];if(typeof s=="string"){const o=n(s);o!==s&&(vb(e)||(e[r]=o),s=o)}t[s]=!0}return t}function _b(t){for(let e=0;e<t.length;e++)Sn(t,e)||(t[e]=null);return t}function gr(t){const e=zd(null);for(const[n,r]of Fd(t))Sn(t,n)&&(Array.isArray(r)?e[n]=_b(r):r&&typeof r=="object"&&r.constructor===Object?e[n]=gr(r):e[n]=r);return e}function Ms(t,e){for(;t!==null;){const r=wb(t,e);if(r){if(r.get)return an(r.get);if(typeof r.value=="function")return an(r.value)}t=xb(t)}function n(){return null}return n}const Bd=Xt(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),Ka=Xt(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),qa=Xt(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),Eb=Xt(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),Za=Xt(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),Mb=Xt(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),jd=Xt(["#text"]),Wd=Xt(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),Qa=Xt(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),Gd=Xt(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),Ho=Xt(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),Nb=un(/\{\{[\w\W]*|[\w\W]*\}\}/gm),Ob=un(/<%[\w\W]*|[\w\W]*%>/gm),Tb=un(/\${[\w\W]*}/gm),Ab=un(/^data-[\-\w.\u00B7-\uFFFF]/),Db=un(/^aria-[\-\w]+$/),Yd=un(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Ib=un(/^(?:\w+script|data):/i),Lb=un(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Xd=un(/^html$/i),Pb=un(/^[a-z][.\w]*(-[.\w]+)+$/i);var Kd=Object.freeze({__proto__:null,MUSTACHE_EXPR:Nb,ERB_EXPR:Ob,TMPLIT_EXPR:Tb,DATA_ATTR:Ab,ARIA_ATTR:Db,IS_ALLOWED_URI:Yd,IS_SCRIPT_OR_DATA:Ib,ATTR_WHITESPACE:Lb,DOCTYPE_NAME:Xd,CUSTOM_ELEMENT:Pb});const Ns={element:1,text:3,progressingInstruction:7,comment:8,document:9},$b=function(){return typeof window>"u"?null:window},Fb=function(e,n){if(typeof e!="object"||typeof e.createPolicy!="function")return null;let r=null;const s="data-tt-policy-suffix";n&&n.hasAttribute(s)&&(r=n.getAttribute(s));const o="dompurify"+(r?"#"+r:"");try{return e.createPolicy(o,{createHTML(i){return i},createScriptURL(i){return i}})}catch{return console.warn("TrustedTypes policy "+o+" could not be created."),null}};function qd(){let t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:$b();const e=ke=>qd(ke);if(e.version="3.1.7",e.removed=[],!t||!t.document||t.document.nodeType!==Ns.document)return e.isSupported=!1,e;let{document:n}=t;const r=n,s=r.currentScript,{DocumentFragment:o,HTMLTemplateElement:i,Node:a,Element:l,NodeFilter:c,NamedNodeMap:d=t.NamedNodeMap||t.MozNamedAttrMap,HTMLFormElement:u,DOMParser:f,trustedTypes:g}=t,m=l.prototype,p=Ms(m,"cloneNode"),y=Ms(m,"remove"),b=Ms(m,"nextSibling"),_=Ms(m,"childNodes"),x=Ms(m,"parentNode");if(typeof i=="function"){const ke=n.createElement("template");ke.content&&ke.content.ownerDocument&&(n=ke.content.ownerDocument)}let E,S="";const{implementation:w,createNodeIterator:N,createDocumentFragment:C,getElementsByTagName:k}=n,{importNode:M}=r;let T={};e.isSupported=typeof Fd=="function"&&typeof x=="function"&&w&&w.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:L,ERB_EXPR:P,TMPLIT_EXPR:F,DATA_ATTR:D,ARIA_ATTR:A,IS_SCRIPT_OR_DATA:O,ATTR_WHITESPACE:V,CUSTOM_ELEMENT:H}=Kd;let{IS_ALLOWED_URI:W}=Kd,j=null;const U=je({},[...Bd,...Ka,...qa,...Za,...jd]);let B=null;const G=je({},[...Wd,...Qa,...Gd,...Ho]);let z=Object.seal(zd(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),X=null,Y=null,Z=!0,ee=!0,te=!1,ce=!0,J=!1,he=!0,le=!1,ae=!1,Se=!1,ue=!1,pe=!1,we=!1,He=!0,se=!1;const Oe="user-content-";let Le=!0,Ie=!1,Ue={},et=null;const bt=je({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ge=null;const ft=je({},["audio","video","img","source","image","track"]);let Be=null;const ne=je({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),fe="http://www.w3.org/1998/Math/MathML",xe="http://www.w3.org/2000/svg",ge="http://www.w3.org/1999/xhtml";let be=ge,Ve=!1,De=null;const $e=je({},[fe,xe,ge],Xa);let Me=null;const qe=["application/xhtml+xml","text/html"],ot="text/html";let ye=null,Ce=null;const Fe=n.createElement("form"),nt=function(K){return K instanceof RegExp||K instanceof Function},pt=function(){let K=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(Ce&&Ce===K)){if((!K||typeof K!="object")&&(K={}),K=gr(K),Me=qe.indexOf(K.PARSER_MEDIA_TYPE)===-1?ot:K.PARSER_MEDIA_TYPE,ye=Me==="application/xhtml+xml"?Xa:zo,j=Sn(K,"ALLOWED_TAGS")?je({},K.ALLOWED_TAGS,ye):U,B=Sn(K,"ALLOWED_ATTR")?je({},K.ALLOWED_ATTR,ye):G,De=Sn(K,"ALLOWED_NAMESPACES")?je({},K.ALLOWED_NAMESPACES,Xa):$e,Be=Sn(K,"ADD_URI_SAFE_ATTR")?je(gr(ne),K.ADD_URI_SAFE_ATTR,ye):ne,Ge=Sn(K,"ADD_DATA_URI_TAGS")?je(gr(ft),K.ADD_DATA_URI_TAGS,ye):ft,et=Sn(K,"FORBID_CONTENTS")?je({},K.FORBID_CONTENTS,ye):bt,X=Sn(K,"FORBID_TAGS")?je({},K.FORBID_TAGS,ye):{},Y=Sn(K,"FORBID_ATTR")?je({},K.FORBID_ATTR,ye):{},Ue=Sn(K,"USE_PROFILES")?K.USE_PROFILES:!1,Z=K.ALLOW_ARIA_ATTR!==!1,ee=K.ALLOW_DATA_ATTR!==!1,te=K.ALLOW_UNKNOWN_PROTOCOLS||!1,ce=K.ALLOW_SELF_CLOSE_IN_ATTR!==!1,J=K.SAFE_FOR_TEMPLATES||!1,he=K.SAFE_FOR_XML!==!1,le=K.WHOLE_DOCUMENT||!1,ue=K.RETURN_DOM||!1,pe=K.RETURN_DOM_FRAGMENT||!1,we=K.RETURN_TRUSTED_TYPE||!1,Se=K.FORCE_BODY||!1,He=K.SANITIZE_DOM!==!1,se=K.SANITIZE_NAMED_PROPS||!1,Le=K.KEEP_CONTENT!==!1,Ie=K.IN_PLACE||!1,W=K.ALLOWED_URI_REGEXP||Yd,be=K.NAMESPACE||ge,z=K.CUSTOM_ELEMENT_HANDLING||{},K.CUSTOM_ELEMENT_HANDLING&&nt(K.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(z.tagNameCheck=K.CUSTOM_ELEMENT_HANDLING.tagNameCheck),K.CUSTOM_ELEMENT_HANDLING&&nt(K.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(z.attributeNameCheck=K.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),K.CUSTOM_ELEMENT_HANDLING&&typeof K.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(z.allowCustomizedBuiltInElements=K.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),J&&(ee=!1),pe&&(ue=!0),Ue&&(j=je({},jd),B=[],Ue.html===!0&&(je(j,Bd),je(B,Wd)),Ue.svg===!0&&(je(j,Ka),je(B,Qa),je(B,Ho)),Ue.svgFilters===!0&&(je(j,qa),je(B,Qa),je(B,Ho)),Ue.mathMl===!0&&(je(j,Za),je(B,Gd),je(B,Ho))),K.ADD_TAGS&&(j===U&&(j=gr(j)),je(j,K.ADD_TAGS,ye)),K.ADD_ATTR&&(B===G&&(B=gr(B)),je(B,K.ADD_ATTR,ye)),K.ADD_URI_SAFE_ATTR&&je(Be,K.ADD_URI_SAFE_ATTR,ye),K.FORBID_CONTENTS&&(et===bt&&(et=gr(et)),je(et,K.FORBID_CONTENTS,ye)),Le&&(j["#text"]=!0),le&&je(j,["html","head","body"]),j.table&&(je(j,["tbody"]),delete X.tbody),K.TRUSTED_TYPES_POLICY){if(typeof K.TRUSTED_TYPES_POLICY.createHTML!="function")throw Es('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof K.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Es('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');E=K.TRUSTED_TYPES_POLICY,S=E.createHTML("")}else E===void 0&&(E=Fb(g,s)),E!==null&&typeof S=="string"&&(S=E.createHTML(""));Xt&&Xt(K),Ce=K}},it=je({},["mi","mo","mn","ms","mtext"]),rt=je({},["annotation-xml"]),Ot=je({},["title","style","font","a","script"]),ht=je({},[...Ka,...qa,...Eb]),Qt=je({},[...Za,...Mb]),Gt=function(K){let ie=x(K);(!ie||!ie.tagName)&&(ie={namespaceURI:be,tagName:"template"});const ve=zo(K.tagName),st=zo(ie.tagName);return De[K.namespaceURI]?K.namespaceURI===xe?ie.namespaceURI===ge?ve==="svg":ie.namespaceURI===fe?ve==="svg"&&(st==="annotation-xml"||it[st]):!!ht[ve]:K.namespaceURI===fe?ie.namespaceURI===ge?ve==="math":ie.namespaceURI===xe?ve==="math"&&rt[st]:!!Qt[ve]:K.namespaceURI===ge?ie.namespaceURI===xe&&!rt[st]||ie.namespaceURI===fe&&!it[st]?!1:!Qt[ve]&&(Ot[ve]||!ht[ve]):!!(Me==="application/xhtml+xml"&&De[K.namespaceURI]):!1},mt=function(K){Ss(e.removed,{element:K});try{x(K).removeChild(K)}catch{y(K)}},St=function(K,ie){try{Ss(e.removed,{attribute:ie.getAttributeNode(K),from:ie})}catch{Ss(e.removed,{attribute:null,from:ie})}if(ie.removeAttribute(K),K==="is"&&!B[K])if(ue||pe)try{mt(ie)}catch{}else try{ie.setAttribute(K,"")}catch{}},xn=function(K){let ie=null,ve=null;if(Se)K="<remove></remove>"+K;else{const _t=Ud(K,/^[\r\n\t ]+/);ve=_t&&_t[0]}Me==="application/xhtml+xml"&&be===ge&&(K='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+K+"</body></html>");const st=E?E.createHTML(K):K;if(be===ge)try{ie=new f().parseFromString(st,Me)}catch{}if(!ie||!ie.documentElement){ie=w.createDocument(be,"template",null);try{ie.documentElement.innerHTML=Ve?S:st}catch{}}const Tt=ie.body||ie.documentElement;return K&&ve&&Tt.insertBefore(n.createTextNode(ve),Tt.childNodes[0]||null),be===ge?k.call(ie,le?"html":"body")[0]:le?ie.documentElement:Tt},Vt=function(K){return N.call(K.ownerDocument||K,K,c.SHOW_ELEMENT|c.SHOW_COMMENT|c.SHOW_TEXT|c.SHOW_PROCESSING_INSTRUCTION|c.SHOW_CDATA_SECTION,null)},Ze=function(K){return K instanceof u&&(typeof K.nodeName!="string"||typeof K.textContent!="string"||typeof K.removeChild!="function"||!(K.attributes instanceof d)||typeof K.removeAttribute!="function"||typeof K.setAttribute!="function"||typeof K.namespaceURI!="string"||typeof K.insertBefore!="function"||typeof K.hasChildNodes!="function")},jn=function(K){return typeof a=="function"&&K instanceof a},dn=function(K,ie,ve){T[K]&&Vo(T[K],st=>{st.call(e,ie,ve,Ce)})},Io=function(K){let ie=null;if(dn("beforeSanitizeElements",K,null),Ze(K))return mt(K),!0;const ve=ye(K.nodeName);if(dn("uponSanitizeElement",K,{tagName:ve,allowedTags:j}),K.hasChildNodes()&&!jn(K.firstElementChild)&&Kt(/<[/\w]/g,K.innerHTML)&&Kt(/<[/\w]/g,K.textContent)||K.nodeType===Ns.progressingInstruction||he&&K.nodeType===Ns.comment&&Kt(/<[/\w]/g,K.data))return mt(K),!0;if(!j[ve]||X[ve]){if(!X[ve]&&Da(ve)&&(z.tagNameCheck instanceof RegExp&&Kt(z.tagNameCheck,ve)||z.tagNameCheck instanceof Function&&z.tagNameCheck(ve)))return!1;if(Le&&!et[ve]){const st=x(K)||K.parentNode,Tt=_(K)||K.childNodes;if(Tt&&st){const _t=Tt.length;for(let Yt=_t-1;Yt>=0;--Yt){const wn=p(Tt[Yt],!0);wn.__removalCount=(K.__removalCount||0)+1,st.insertBefore(wn,b(K))}}}return mt(K),!0}return K instanceof l&&!Gt(K)||(ve==="noscript"||ve==="noembed"||ve==="noframes")&&Kt(/<\/no(script|embed|frames)/i,K.innerHTML)?(mt(K),!0):(J&&K.nodeType===Ns.text&&(ie=K.textContent,Vo([L,P,F],st=>{ie=_s(ie,st," ")}),K.textContent!==ie&&(Ss(e.removed,{element:K.cloneNode()}),K.textContent=ie)),dn("afterSanitizeElements",K,null),!1)},Lo=function(K,ie,ve){if(He&&(ie==="id"||ie==="name")&&(ve in n||ve in Fe))return!1;if(!(ee&&!Y[ie]&&Kt(D,ie))){if(!(Z&&Kt(A,ie))){if(!B[ie]||Y[ie]){if(!(Da(K)&&(z.tagNameCheck instanceof RegExp&&Kt(z.tagNameCheck,K)||z.tagNameCheck instanceof Function&&z.tagNameCheck(K))&&(z.attributeNameCheck instanceof RegExp&&Kt(z.attributeNameCheck,ie)||z.attributeNameCheck instanceof Function&&z.attributeNameCheck(ie))||ie==="is"&&z.allowCustomizedBuiltInElements&&(z.tagNameCheck instanceof RegExp&&Kt(z.tagNameCheck,ve)||z.tagNameCheck instanceof Function&&z.tagNameCheck(ve))))return!1}else if(!Be[ie]){if(!Kt(W,_s(ve,V,""))){if(!((ie==="src"||ie==="xlink:href"||ie==="href")&&K!=="script"&&kb(ve,"data:")===0&&Ge[K])){if(!(te&&!Kt(O,_s(ve,V,"")))){if(ve)return!1}}}}}}return!0},Da=function(K){return K!=="annotation-xml"&&Ud(K,H)},Ia=function(K){dn("beforeSanitizeAttributes",K,null);const{attributes:ie}=K;if(!ie)return;const ve={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:B};let st=ie.length;for(;st--;){const Tt=ie[st],{name:_t,namespaceURI:Yt,value:wn}=Tt,Pr=ye(_t);let q=_t==="value"?wn:Cb(wn);if(ve.attrName=Pr,ve.attrValue=q,ve.keepAttr=!0,ve.forceKeepAttr=void 0,dn("uponSanitizeAttribute",K,ve),q=ve.attrValue,ve.forceKeepAttr||(St(_t,K),!ve.keepAttr))continue;if(!ce&&Kt(/\/>/i,q)){St(_t,K);continue}J&&Vo([L,P,F],Te=>{q=_s(q,Te," ")});const me=ye(K.nodeName);if(Lo(me,Pr,q)){if(se&&(Pr==="id"||Pr==="name")&&(St(_t,K),q=Oe+q),he&&Kt(/((--!?|])>)|<\/(style|title)/i,q)){St(_t,K);continue}if(E&&typeof g=="object"&&typeof g.getAttributeType=="function"&&!Yt)switch(g.getAttributeType(me,Pr)){case"TrustedHTML":{q=E.createHTML(q);break}case"TrustedScriptURL":{q=E.createScriptURL(q);break}}try{Yt?K.setAttributeNS(Yt,_t,q):K.setAttribute(_t,q),Ze(K)?mt(K):Hd(e.removed)}catch{}}}dn("afterSanitizeAttributes",K,null)},La=function ke(K){let ie=null;const ve=Vt(K);for(dn("beforeSanitizeShadowDOM",K,null);ie=ve.nextNode();)dn("uponSanitizeShadowNode",ie,null),!Io(ie)&&(ie.content instanceof o&&ke(ie.content),Ia(ie));dn("afterSanitizeShadowDOM",K,null)};return e.sanitize=function(ke){let K=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},ie=null,ve=null,st=null,Tt=null;if(Ve=!ke,Ve&&(ke="<!-->"),typeof ke!="string"&&!jn(ke))if(typeof ke.toString=="function"){if(ke=ke.toString(),typeof ke!="string")throw Es("dirty is not a string, aborting")}else throw Es("toString is not a function");if(!e.isSupported)return ke;if(ae||pt(K),e.removed=[],typeof ke=="string"&&(Ie=!1),Ie){if(ke.nodeName){const wn=ye(ke.nodeName);if(!j[wn]||X[wn])throw Es("root node is forbidden and cannot be sanitized in-place")}}else if(ke instanceof a)ie=xn("<!---->"),ve=ie.ownerDocument.importNode(ke,!0),ve.nodeType===Ns.element&&ve.nodeName==="BODY"||ve.nodeName==="HTML"?ie=ve:ie.appendChild(ve);else{if(!ue&&!J&&!le&&ke.indexOf("<")===-1)return E&&we?E.createHTML(ke):ke;if(ie=xn(ke),!ie)return ue?null:we?S:""}ie&&Se&&mt(ie.firstChild);const _t=Vt(Ie?ke:ie);for(;st=_t.nextNode();)Io(st)||(st.content instanceof o&&La(st.content),Ia(st));if(Ie)return ke;if(ue){if(pe)for(Tt=C.call(ie.ownerDocument);ie.firstChild;)Tt.appendChild(ie.firstChild);else Tt=ie;return(B.shadowroot||B.shadowrootmode)&&(Tt=M.call(r,Tt,!0)),Tt}let Yt=le?ie.outerHTML:ie.innerHTML;return le&&j["!doctype"]&&ie.ownerDocument&&ie.ownerDocument.doctype&&ie.ownerDocument.doctype.name&&Kt(Xd,ie.ownerDocument.doctype.name)&&(Yt="<!DOCTYPE "+ie.ownerDocument.doctype.name+`> +`+Yt),J&&Vo([L,P,F],wn=>{Yt=_s(Yt,wn," ")}),E&&we?E.createHTML(Yt):Yt},e.setConfig=function(){let ke=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};pt(ke),ae=!0},e.clearConfig=function(){Ce=null,ae=!1},e.isValidAttribute=function(ke,K,ie){Ce||pt({});const ve=ye(ke),st=ye(K);return Lo(ve,st,ie)},e.addHook=function(ke,K){typeof K=="function"&&(T[ke]=T[ke]||[],Ss(T[ke],K))},e.removeHook=function(ke){if(T[ke])return Hd(T[ke])},e.removeHooks=function(ke){T[ke]&&(T[ke]=[])},e.removeAllHooks=function(){T={}},e}var Vb=qd();const Ja=({content:t,text:e,isHtml:n=!0,className:r="",purifyConfig:s,id:o,editorAttrs:i})=>{const a=e??t??"";if(!a||a.trim()==="")return null;if(!n){const u=` whitespace-pre-wrap text-gray-900 dark:text-gray-100 font-sans ${r} - `.trim().replace(/\s+/g," ");return h.jsx(I,{className:u,id:o,...i,children:a})}const l={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},c=v.useMemo(()=>{const u=s?{...l,...s,FORBID_TAGS:[...l.FORBID_TAGS,...s.FORBID_TAGS||[]],FORBID_ATTR:[...l.FORBID_ATTR,...s.FORBID_ATTR||[]]}:l;return cy.sanitize(a,u).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,m,p,y)=>(p.startsWith("http://")||p.startsWith("https://"))&&!g.includes("rel=")?`<a ${m}href="${p}"${y} rel="noopener noreferrer">`:g)},[a,s]),d=` + `.trim().replace(/\s+/g," ");return h.jsx(I,{className:u,id:o,...i,children:a})}const l={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},c=v.useMemo(()=>{const u=s?{...l,...s,FORBID_TAGS:[...l.FORBID_TAGS,...s.FORBID_TAGS||[]],FORBID_ATTR:[...l.FORBID_ATTR,...s.FORBID_ATTR||[]]}:l;return Vb.sanitize(a,u).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,m,p,y)=>(p.startsWith("http://")||p.startsWith("https://"))&&!g.includes("rel=")?`<a ${m}href="${p}"${y} rel="noopener noreferrer">`:g)},[a,s]),d=` prose dark:prose-invert max-w-none prose-p:my-2 prose-headings:font-bold prose-headings:mt-6 prose-headings:mb-4 @@ -36,67 +35,67 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho [&_tr:hover]:bg-gray-50 dark:[&_tr:hover]:bg-gray-700/50 after:content-[''] after:block after:clear-both ${r} - `.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},Zo=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...e)=>console.log("[Comp:DataGrid]",...e),warn:(...e)=>console.warn("[Comp:DataGrid]",...e),error:(...e)=>console.error("[Comp:DataGrid]",...e)},Fs=(e,t)=>window.G7Core?.t?.(e,t)??e,il={Div:I,Span:Q,Button:ae,Icon:oe,Checkbox:En,P:vt,A:Yr,Label:ln,Input:St,H1:Ba,H2:ar,H3:Gr,H4:Fd,Img:_n,Select:Xr,Textarea:ir,Table:Ya,Thead:Xa,Tbody:Ka,Tfoot:qa,Tr:lr,Th:Kr,Td:en,MultilingualInput:Za,Toggle:Qa,StatusBadge:Ko,Badge:Wd,HtmlContent:ol,ActionMenu:Ls},al=e=>{if(e==null)return"";if(typeof e=="string"||typeof e=="number"||typeof e=="boolean")return String(e);if(Array.isArray(e))return e.map(t=>al(t)).join(", ");if(typeof e=="object"){if(v.isValidElement(e))return e;if(Object.keys(e).length===0)return"";try{return JSON.stringify(e)}catch{return Zo.warn("객체를 문자열로 변환할 수 없습니다:",e),"[Object]"}}return String(e)},au=(e,t)=>{if(!e)return!0;const n=e.match(/^\{\{(.+)\}\}$/);if(!n)return!0;const r=n[1].trim();try{return!!new Function("row",`return ${r}`)(t)}catch(s){return Zo.warn("subRow 조건 평가 실패:",e,s),!0}},zs=(e,t,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:t,value:n,$value:n};return o.renderItemChildren(e,i,il,r,s?{componentContext:s}:void 0)}return Zo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},ll=(e,t,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:t,$value:t};return s.renderItemChildren(e,o,il,n,r?{componentContext:r}:void 0)}return Zo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},dy=({id:e,columns:t,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",showColumnSelector:y=!1,visibleColumns:b,onColumnVisibilityChange:_,responsiveBreakpoint:x=768,cardRenderer:M,rowActions:C,onRowAction:k,alwaysShowPagination:N=!1,serverSidePagination:S=!1,serverCurrentPage:w,serverTotalPages:E,onPageChange:D,prevText:P,nextText:L,columnSelectorText:$,emptyMessage:A,actionsColumnHeader:O,requiredText:T,selectedCountText:z,showFirstLast:H=!0,expandable:j=!1,expandedRowIds:G,onExpandChange:U,expandChildren:W,expandedRowRender:Y,expandColumnWidth:V="48px",expandIconExpanded:X=de.ChevronDown,expandIconCollapsed:K=de.ChevronRight,expandConditionField:q,dynamicColumns:ne,dynamicColumnsInsertAfter:ee,expandContext:ue,__componentContext:J,subRowChildren:ce,subRowCondition:me,subRowClassName:le="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:ve,footerCells:ge,footerClassName:pe="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:ke,editorAttrs:$e})=>{const ie=P||void 0,Me=L||void 0,Ae=$??Fs("common.column_selector"),Ie=A??Fs("common.no_data"),Xe=O??Fs("common.actions"),gt=T??Fs("common.required"),pt=z??Fs("common.selected_count"),Be=window.G7Core,et=Be?.useControllableState,je=Be?.shallowArrayEqual,[re,fe]=et?et(c,null):[c??null,()=>{}],[be,we]=et?et(d,"asc"):[d??"asc",()=>{}],[xe,He]=v.useState(1),[_e,Le]=et?et(G,[],U,{isEqual:je}):[G??[],()=>{}],[Ce,We]=v.useState(!1),Ze=v.useRef(null),Fe=Be?.useResponsive,Ne=Fe?.(),ze=Ne?Ne.width<x:typeof window<"u"&&window.innerWidth<x,Ke=v.useMemo(()=>{if(!ne||!Array.isArray(ne)||ne.length===0)return t;if(ee){const Z=t.findIndex(ye=>ye.field===ee);if(Z!==-1)return[...t.slice(0,Z+1),...ne,...t.slice(Z+1)]}return[...t,...ne]},[t,ne,ee]),[tt,at]=v.useState(()=>b&&b.length>0?b:t.filter(Z=>!Z.hidden).map(Z=>Z.field));v.useEffect(()=>{b&&b.length>0&&at(b)},[b]);const ct=v.useRef("");v.useEffect(()=>{const Z=t.map(ye=>ye.field).join(",");ct.current&&Z!==ct.current&&(!b||b.length===0)&&at(t.filter(ye=>!ye.hidden).map(ye=>ye.field)),ct.current=Z},[t,b]);const wn=v.useRef([]),yt=v.useRef(null);v.useEffect(()=>{if(ne&&ne.length>0){const Z=ne.map(Qe=>Qe.field);if(yt.current===null&&(yt.current=b!==void 0&&b.length>0,yt.current)){wn.current=Z;return}const ye=wn.current,De=Z.filter(Qe=>!ye.includes(Qe));if(De.length>0){const Qe=De.filter(se=>!tt.includes(se));if(Qe.length>0){const se=[...tt,...Qe];at(se),_?.(se)}}wn.current=Z}},[ne]),v.useEffect(()=>{S||He(1)},[n,S]),v.useEffect(()=>{const Z=ye=>{Ze.current&&!Ze.current.contains(ye.target)&&We(!1)};return Ce&&document.addEventListener("mousedown",Z),()=>document.removeEventListener("mousedown",Z)},[Ce]);const Ue=v.useMemo(()=>!y&&(!b||b.length===0)?Ke.filter(Z=>!Z.hidden):Ke.filter(Z=>tt.includes(Z.field)),[Ke,tt,y,b]),Ct=r&&!(S&&!u),Ut=v.useMemo(()=>!re||!r||!n?n||[]:u||S?n:[...n].sort((Z,ye)=>{const De=Z[re],Qe=ye[re];if(De===Qe)return 0;const se=De>Qe?1:-1;return be==="asc"?se:-se}),[n,re,be,r,u,S]),st=v.useMemo(()=>{if(S||!s)return Ut;const Z=(xe-1)*o,ye=Z+o;return Ut.slice(Z,ye)},[Ut,xe,o,s,S]),kn=S?w||1:xe,qt=S?E||1:Math.ceil((Ut?.length||0)/o),Ln=v.useCallback(Z=>{S&&D?D(Z):He(Z)},[S,D]),jr=Z=>{if(!Ct||t.find(Qe=>Qe.field===Z)?.sortable===!1)return;const De=re===Z&&be==="asc"?"desc":"asc";fe(Z),we(De),u?.(Z,De)},fn=v.useCallback(()=>{if(!m||!st)return;const Z=st.map(De=>De[p]);if(Z.every(De=>g.includes(De)))m(g.filter(De=>!Z.includes(De)));else{const De=[...new Set([...g,...Z])];m(De)}},[st,g,m,p]),qn=v.useCallback(Z=>{m&&(g.includes(Z)?m(g.filter(ye=>ye!==Z)):m([...g,Z]))},[g,m]),Wo=v.useCallback(Z=>{if(Ke.find(Qe=>Qe.field===Z)?.required)return;const De=tt.includes(Z)?tt.filter(Qe=>Qe!==Z):[...tt,Z];at(De),_?.(De)},[Ke,tt,_]),Go=v.useCallback((Z,ye)=>{k?.(Z,ye)},[k]),Is=v.useCallback(Z=>C?C.map(ye=>{let De=ye.disabled??!1;if(ye.disabledField&&!De){const Qe=ye.disabledField.split(".");let se=Z;for(const B of Qe)se=se?.[B];se||(De=!0)}return{...ye,disabled:De,onClick:()=>Go(ye.id,Z)}}):[],[C,Go]),sr=v.useCallback(Z=>{if(!q)return!0;const ye=Z[q];return Array.isArray(ye)?ye.length>0:!!ye},[q]),or=v.useCallback(Z=>{const ye=_e.includes(Z)?_e.filter(De=>De!==Z):[..._e,Z];et?Le(ye):U&&U(ye)},[_e,U,et,Le]),Wr=v.useCallback(Z=>_e.includes(Z),[_e]),Cn=v.useCallback(Z=>Y?Y(Z):W&&W.length>0&&Be?.renderExpandContent?Be.renderExpandContent({children:W,row:Z,expandContext:ue,componentContext:J,componentMap:il,keyPrefix:`expand-${Z[p]}`}):null,[Y,W,p,ue,J,J?.state]),Ha=v.useMemo(()=>!st||st.length===0?!1:st.every(Z=>g.includes(Z[p])),[st,g,p]),Ua=v.useMemo(()=>{if(!st||st.length===0)return!1;const Z=st.filter(ye=>g.includes(ye[p])).length;return Z>0&&Z<st.length},[st,g,p]),Ld=(Z,ye)=>{const De=Z[p],Qe=Wr(De);return h.jsxs(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4",children:[h.jsxs(I,{className:"flex items-start justify-between",children:[j&&sr(Z)&&h.jsx(ae,{onClick:()=>or(De),className:"p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors mr-2","aria-label":Qe?"Collapse card":"Expand card",children:h.jsx(oe,{name:Qe?X:K,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})}),f&&h.jsx(En,{checked:g.includes(Z[p]),onChange:()=>qn(Z[p]),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded mr-3 mt-1"}),h.jsx(I,{className:"flex-1",children:h.jsx(I,{className:"grid grid-cols-2 gap-x-4 gap-y-2",children:ye.map(se=>h.jsxs(I,{className:"min-w-0",children:[h.jsx(Q,{className:"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase",children:se.header}),h.jsx(I,{className:"text-sm text-gray-900 dark:text-gray-200 mt-0.5 break-words",children:se.cellChildren&&se.cellChildren.length>0?zs(se.cellChildren,Z,Z[se.field],`card-${Z[p]}-${se.field}`,J):se.render?se.render(Z[se.field],Z):al(Z[se.field])})]},se.field))})}),C&&C.length>0&&h.jsx(I,{className:"ml-2",children:h.jsx(Ls,{items:Is(Z),triggerLabel:"",triggerIconName:de.EllipsisHorizontal})})]}),j&&Qe&&sr(Z)&&h.jsx(I,{className:"mt-4 pt-4 border-t border-gray-200 dark:border-gray-700",children:Cn(Z)}),(ce&&ce.length>0||ve)&&au(me,Z)&&h.jsx(I,{className:le?`mt-3 ${le}`:"mt-3 pt-3 border-t border-gray-100 dark:border-gray-700",children:ve?ve(Z):ll(ce,Z,`card-subrow-${De}`,J)})]})};return ze?h.jsxs(I,{id:e,className:`w-full ${a}`,style:l,...$e,children:[y&&h.jsx(I,{className:"mb-4 flex justify-end",children:h.jsxs(I,{ref:Ze,className:"relative",children:[h.jsxs(ae,{onClick:()=>We(!Ce),className:"flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700",children:[h.jsx(oe,{name:de.Cog,className:"w-4 h-4 text-gray-500 dark:text-gray-400"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:Ae})]}),Ce&&h.jsx(I,{className:"absolute right-0 z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",children:h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:Ke.map(Z=>h.jsxs(I,{className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${Z.required?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:()=>!Z.required&&Wo(Z.field),children:[h.jsx(En,{checked:tt.includes(Z.field),disabled:Z.required,onChange:()=>{},className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"}),h.jsxs(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:[Z.header,Z.required&&h.jsxs(Q,{className:"text-xs text-gray-500 dark:text-gray-400 ml-1",children:["(",gt,")"]})]})]},Z.field))})})]})}),f&&g.length>0&&pt&&h.jsx(I,{className:"mb-4 text-sm text-gray-600 dark:text-gray-400",children:pt.replace("{count}",String(g.length))}),h.jsx(I,{id:e?`${e}__body`:void 0,children:st&&st.length>0?st.map((Z,ye)=>h.jsx(I,{children:M?M(Z,Ue):Ld(Z,Ue)},Z[p]||ye)):h.jsx(I,{className:"text-center py-8 text-gray-500 dark:text-gray-400",children:Ie})}),ge&&ge.length>0&&st&&st.length>0&&h.jsx(I,{className:`rounded-lg p-4 mt-2 ${pe}`,children:ke&&ke.length>0?zs(ke,{},null,"footer-card",J):h.jsx(I,{className:"space-y-1",children:ge.filter(Z=>Z.text||Z.children&&Z.children.length>0).map((Z,ye)=>{const De=Ue.find(Qe=>Qe.field===Z.field);return h.jsxs(I,{className:"flex justify-between text-sm",children:[h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:De?.header||""}),h.jsx(Q,{className:Z.className||"text-gray-900 dark:text-gray-100",children:Z.children&&Z.children.length>0?zs(Z.children,{},null,`footer-mobile-${ye}`,J):Z.text||""})]},`footer-mobile-${ye}`)})})}),s&&(N||qt>1)&&h.jsx(I,{className:"flex justify-center mt-4",children:h.jsx(Xo,{currentPage:kn,totalPages:qt,onPageChange:Ln,showFirstLast:H,maxVisiblePages:5,prevText:ie,nextText:Me})})]}):h.jsxs(I,{id:e,className:`w-full ${a}`,style:l,...$e,children:[y&&h.jsx(I,{className:"mb-4 flex justify-end",children:h.jsxs(I,{ref:Ze,className:"relative",children:[h.jsxs(ae,{onClick:()=>We(!Ce),className:"flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700",children:[h.jsx(oe,{name:de.Cog,className:"w-4 h-4 text-gray-500 dark:text-gray-400"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:Ae})]}),Ce&&h.jsx(I,{className:"absolute right-0 z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",children:h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:Ke.map(Z=>h.jsxs(I,{className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${Z.required?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:()=>!Z.required&&Wo(Z.field),children:[h.jsx(En,{checked:tt.includes(Z.field),disabled:Z.required,onChange:()=>{},className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"}),h.jsxs(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:[Z.header,Z.required&&h.jsxs(Q,{className:"text-xs text-gray-500 dark:text-gray-400 ml-1",children:["(",gt,")"]})]})]},Z.field))})})]})}),f&&g.length>0&&pt&&h.jsx(I,{className:"mb-4 text-sm text-gray-600 dark:text-gray-400",children:pt.replace("{count}",String(g.length))}),h.jsx(I,{id:e?`${e}__body`:void 0,className:"overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg",children:h.jsxs(Ya,{className:"w-full",children:[h.jsx(Xa,{className:"bg-gray-50 dark:bg-gray-700",children:h.jsxs(lr,{children:[j&&h.jsx(Kr,{className:"px-2 py-3",style:{width:V}}),f&&h.jsx(Kr,{className:"px-4 py-3 w-10",children:h.jsx(En,{checked:Ha,ref:Z=>{Z&&(Z.indeterminate=Ua)},onChange:fn,className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"})}),Ue.map(Z=>h.jsxs(Kr,{className:`px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider ${Ct&&Z.sortable!==!1?"cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600":""}`,style:{width:Z.width,minWidth:Z.width},onClick:Ct?()=>jr(Z.field):void 0,children:[Z.header,re===Z.field&&h.jsx(Q,{className:"ml-1",children:be==="asc"?"↑":"↓"})]},Z.field)),C&&C.length>0&&h.jsx(Kr,{className:"px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-24",children:Xe})]})}),h.jsx(Ka,{className:`bg-white dark:bg-gray-800 ${le?"":"divide-y divide-gray-200 dark:divide-gray-700"}`,children:st&&st.length>0?st.map((Z,ye)=>{const De=Z[p]||ye,Qe=Wr(De);return h.jsxs(v.Fragment,{children:[h.jsxs(lr,{className:`${i?"cursor-pointer":""} hover:bg-gray-50 dark:hover:bg-gray-700 ${g.includes(Z[p])?"bg-blue-50 dark:bg-blue-900/20":""} ${le&&ye>0?"border-t border-gray-200 dark:border-gray-700":""}`,onClick:()=>i?.(Z),children:[j&&h.jsx(en,{className:"px-2 py-4 text-center",onClick:se=>se.stopPropagation(),children:sr(Z)?h.jsx(ae,{onClick:()=>or(De),className:"p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors","aria-label":Qe?"Collapse row":"Expand row",children:h.jsx(oe,{name:Qe?X:K,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})}):null}),f&&h.jsx(en,{className:"px-4 py-4",onClick:se=>se.stopPropagation(),children:h.jsx(En,{checked:g.includes(Z[p]),onChange:()=>qn(Z[p]),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"})}),Ue.map(se=>h.jsx(en,{className:"px-6 py-4 align-top whitespace-nowrap text-sm text-gray-900 dark:text-gray-200",style:{width:se.width,minWidth:se.width},children:se.cellChildren&&se.cellChildren.length>0?zs(se.cellChildren,Z,Z[se.field],`${Z[p]}-${se.field}`,J):se.render?se.render(Z[se.field],Z):al(Z[se.field])},se.field)),C&&C.length>0&&h.jsx(en,{className:"px-6 py-4 text-right",onClick:se=>se.stopPropagation(),children:h.jsx(Ls,{items:Is(Z),triggerLabel:"",triggerIconName:de.EllipsisHorizontal,position:"right"})})]}),j&&Qe&&sr(Z)&&h.jsx(lr,{className:"bg-gray-50 dark:bg-gray-900",children:h.jsx(en,{colSpan:Ue.length+1+(f?1:0)+(C&&C.length>0?1:0),className:"px-6 py-4",children:Cn(Z)})}),(ce&&ce.length>0||ve)&&au(me,Z)&&h.jsx(lr,{className:le?"!border-t-0":"",children:h.jsx(en,{colSpan:Ue.length+(j?1:0)+(f?1:0)+(C&&C.length>0?1:0),className:le?"p-2":le||"px-6 py-2 bg-gray-50 dark:bg-gray-900",children:le?h.jsx(I,{className:le,children:ve?ve(Z):ll(ce,Z,`subrow-${De}`,J)}):ve?ve(Z):ll(ce,Z,`subrow-${De}`,J)})})]},De)}):h.jsx(lr,{children:h.jsx(en,{colSpan:Ue.length+(j?1:0)+(f?1:0)+(C&&C.length>0?1:0),className:"px-6 py-8 text-center text-gray-500 dark:text-gray-400",children:Ie})})}),ge&&ge.length>0&&st&&st.length>0&&h.jsx(qa,{children:h.jsxs(lr,{className:pe,children:[j&&h.jsx(en,{className:"px-2 py-4"}),f&&h.jsx(en,{className:"px-4 py-4"}),(()=>{const Z=new Map(ge.filter(ye=>ye.field).map(ye=>[ye.field,ye]));return Ue.map(ye=>{const De=Z.get(ye.field);return De?h.jsx(en,{className:`px-6 py-4 text-sm text-gray-900 dark:text-gray-200 ${De.className||""}`,colSpan:De.colSpan,style:{width:ye.width,minWidth:ye.width},children:De.children&&De.children.length>0?zs(De.children,{},null,`footer-${ye.field}`,J):De.text||""},`footer-${ye.field}`):h.jsx(en,{className:"px-6 py-4",style:{width:ye.width,minWidth:ye.width}},`footer-${ye.field}`)})})(),C&&C.length>0&&h.jsx(en,{className:"px-6 py-4"})]})})]})}),s&&(N||qt>1)&&h.jsx(I,{className:"flex justify-center mt-4",children:h.jsx(Xo,{currentPage:kn,totalPages:qt,onPageChange:Ln,showFirstLast:H,maxVisiblePages:5,prevText:ie,nextText:Me})})]})},cl=window.G7Core?.createLogger?.("Comp:CardGrid")??{log:(...e)=>console.log("[Comp:CardGrid]",...e),warn:(...e)=>console.warn("[Comp:CardGrid]",...e),error:(...e)=>console.error("[Comp:CardGrid]",...e)},uy=(e,t)=>window.G7Core?.t?.(e,t)??e,fy=()=>window.G7Core?.getComponentMap?.()||{},hy=(e,t)=>{if(!e)return!0;const n=e.match(/^\{\{(.+)\}\}$/);if(!n)return!0;const r=n[1].trim();if(cl.log("evaluateCondition:",{condition:e,expr:r,row:t,rowStatus:t?.status}),/[&|<>=!]/.test(r)||r.includes(".length"))try{const o=!!new Function("row",`return ${r}`)(t);return cl.log("evaluateCondition result:",o),o}catch(s){return cl.warn("evaluateCondition: 표현식 평가 실패:",r,s),!1}if(r.startsWith("!row.")){const s=r.slice(5);return!t[s]}if(r.startsWith("row.")){const s=r.slice(4);return!!t[s]}return!0},lu=(e,t,n,r,s)=>{const o=window.G7Core;if(e.name==="ActionMenu"){if(r&&r.length>0&&s)return{...e,props:{...e.props,items:r.map(i=>({...i,onClick:()=>s(i.id,t)}))}};if(e.actions&&Array.isArray(e.actions)){const i=e.props?.items||[],a=new Map(e.actions.map(c=>[c.id,c])),l=i.map(c=>{if(typeof c.onClick=="string"){const d=c.onClick,u=a.get(d);if(u&&o?.ActionDispatcher)return{...c,onClick:()=>{const f={...n};o.ActionDispatcher.dispatch(u,f)}}}return c});return{...e,props:{...e.props,items:l}}}}return e.children&&Array.isArray(e.children)?{...e,children:e.children.map(i=>lu(i,t,n,r,s))}:e},gy=(e,t,n,r="",s,o)=>{const i=window.G7Core;if(i?.renderItemChildren){const a=e.filter(d=>{const u=d.condition||d.if;return u?hy(u,t):!0}),l={row:t,$value:t,index:n},c=a.map(d=>lu(d,t,l,s,o));try{const d=fy();return i.renderItemChildren(c,l,d,r)}catch{return h.jsxs(I,{className:"text-red-600 dark:text-red-400 p-4",children:[h.jsx(vt,{children:"컴포넌트 로드 실패"}),h.jsx(vt,{className:"text-sm",children:"[CardGrid] 데이터를 표시할 수 없습니다."})]})}}return null},dl=(e,t)=>{if(e.name==="Div"&&e.children&&e.children.length>0){const n=e.children.map((r,s)=>dl(r,`${t}-child-${s}`)).filter(Boolean);return h.jsx(I,{className:e.props?.className||"",children:n},t)}return h.jsx(I,{className:"h-4 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse"},t)},cu=(e,t="skeleton")=>!e||e.length===0?null:e.map((n,r)=>{const s=`${t}-${n.id||n.name}-${r}`;return n.condition||n.if?null:(n.iteration,dl(n,s))}).filter(Boolean),py=({data:e,cardColumns:t=[],gridColumns:n=3,gap:r=4,responsiveColumns:s,cardClassName:o="",idField:i="id",pagination:a=!0,pageSize:l=12,serverSidePagination:c=!1,serverCurrentPage:d,serverTotalPages:u,alwaysShowPagination:f=!1,onPageChange:g,paginationInfoText:m,rowActions:p,onRowAction:y,className:b="",style:_,emptyMessage:x,prevText:M,nextText:C,showSkeleton:k=!0,skeletonCount:N,skeletonCellChildren:S,id:w,editorAttrs:E})=>{const D=x??uy("common.no_data"),P=t[0]?.cellChildren||[],[L,$]=v.useState(1);window.G7Core?.useResponsive?.();const O=c?d??1:L,T=c?u??1:Math.ceil((e?.length??0)/l),z=Y=>{c?g?.(Y):$(Y)},H=v.useCallback((Y,V)=>{y?.(Y,V)},[y]),j=v.useMemo(()=>{if(!e)return[];if(!a||c)return e;const Y=(L-1)*l,V=Y+l;return e.slice(Y,V)},[e,a,c,L,l]),G=v.useMemo(()=>{const Y=[],V=s?.sm??1;Y.push(`grid-cols-${V}`),s?.sm&&Y.push(`sm:grid-cols-${s.sm}`);const X=s?.md??Math.min(n,2);Y.push(`md:grid-cols-${X}`);const K=s?.lg??n;return Y.push(`lg:grid-cols-${K}`),s?.xl&&Y.push(`xl:grid-cols-${s.xl}`),Y.join(" ")},[n,s]),U=v.useMemo(()=>k?S&&S.length>0?cu(S,"custom-skeleton"):P&&P.length>0?cu(P,"auto-skeleton"):null:null,[k,S,P]);if(!e){if(!k)return null;const Y=N??n;return h.jsx(I,{className:`grid ${G} gap-${r} ${b}`,style:_,id:w,...E,children:Array.from({length:Y}).map((V,X)=>h.jsx(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 space-y-4",children:U},`skeleton-${X}`))})}if(j.length===0)return h.jsx(I,{className:b,style:_,id:w,...E,children:h.jsx(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-12",children:h.jsx(vt,{className:"text-center text-gray-600 dark:text-gray-400",children:D})})});const W=a&&(f||T>1);return h.jsxs(I,{className:b,style:_,id:w,...E,children:[h.jsx(I,{className:`grid ${G} gap-${r}`,children:j.map((Y,V)=>h.jsx(I,{className:o,children:gy(P,Y,V,`card-${V}`,p,H)},Y[i]||V))}),W&&h.jsxs(I,{className:"mt-6 space-y-4 flex flex-col items-center",children:[h.jsx(Xo,{currentPage:O,totalPages:T,onPageChange:z,showFirstLast:!1,maxVisiblePages:5,prevText:M,nextText:C}),m&&h.jsx(I,{className:"text-sm text-gray-600 dark:text-gray-400 text-center",children:m})]})]})},my=(e,t)=>window.G7Core?.t?.(e,t)??e,Qo=({isOpen:e,onClose:t,title:n,titlePrefix:r,titleSuffix:s,closeOnBackdropClick:o=!0,closeOnOverlayClick:i,showCloseButton:a=!0,closeOnEscape:l=!0,children:c,width:d="500px",className:u="",style:f,__dataContext:g,__translationContext:m})=>{const p=i!==void 0?i:o,y=v.useRef(null),b=v.useRef(null);if(v.useEffect(()=>{const M=C=>{C.key==="Escape"&&e&&l&&t()};if(e&&l)return document.addEventListener("keydown",M),()=>document.removeEventListener("keydown",M)},[e,t,l]),v.useEffect(()=>{if(e&&y.current){b.current=document.activeElement;const M=y.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),C=M[0],k=M[M.length-1];C?.focus();const N=S=>{S.key==="Tab"&&(S.shiftKey?document.activeElement===C&&(S.preventDefault(),k?.focus()):document.activeElement===k&&(S.preventDefault(),C?.focus()))};return document.addEventListener("keydown",N),()=>{document.removeEventListener("keydown",N),b.current?.focus()}}},[e]),v.useEffect(()=>{if(e)return document.body.style.overflow="hidden",()=>{document.body.style.overflow=""}},[e]),!e)return null;const _=f?.zIndex??50,x=(M,C)=>{if(!M||M.length===0)return null;const k=window.G7Core;if(!k)return null;const N=k.getDynamicRenderer?.();return N?M.map((S,w)=>h.jsx(N,{componentDef:S,dataContext:g||{},translationContext:m,registry:k.getComponentRegistry(),bindingEngine:k.getDataBindingEngine(),translationEngine:k.getTranslationEngine(),actionDispatcher:k.getActionDispatcher(),isRootRenderer:!1},`${C}-${w}`)):null};return h.jsxs(I,{className:"modal-wrapper",style:{zIndex:_},onClick:p?t:void 0,children:[h.jsx(I,{className:"modal-backdrop","aria-hidden":"true"}),h.jsxs(I,{ref:y,className:`modal-container ${u}`,style:{width:d,...f?{...f,zIndex:void 0}:{}},onClick:M=>M.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":n?"modal-title":void 0,children:[(n||r||s||a)&&h.jsxs(I,{className:"modal-header-row",children:[h.jsxs(I,{className:"flex-center gap-sm",children:[x(r,"title-prefix"),n&&h.jsx(I,{id:"modal-title",className:"modal-title",children:n}),x(s,"title-suffix")]}),a&&h.jsx(ae,{onClick:t,className:"modal-close-btn","aria-label":my("common.close_modal"),children:h.jsx("svg",{className:"icon-lg",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),h.jsx(I,{className:"modal-body-content",children:c})]})]})},by=({label:e,items:t,onItemClick:n,position:r="bottom-left",variant:s="default",className:o="",style:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(!1),[u,f]=v.useState(-1),g=v.useRef(null),m=v.useRef(null),[p,y]=v.useState(r);v.useEffect(()=>{const x=M=>{g.current&&!g.current.contains(M.target)&&(d(!1),f(-1))};if(c)return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[c]),v.useEffect(()=>{const x=M=>{if(!c)return;const C=t.map((k,N)=>k.disabled?-1:N).filter(k=>k!==-1);switch(M.key){case"ArrowDown":M.preventDefault(),f(k=>{const S=(C.indexOf(k)+1)%C.length;return C[S]});break;case"ArrowUp":M.preventDefault(),f(k=>{const N=C.indexOf(k),S=N<=0?C.length-1:N-1;return C[S]});break;case"Enter":M.preventDefault(),u>=0&&!t[u].disabled&&b(t[u]);break;case"Escape":M.preventDefault(),d(!1),f(-1);break}};if(c)return document.addEventListener("keydown",x),()=>document.removeEventListener("keydown",x)},[c,u,t]),v.useEffect(()=>{if(c&&m.current&&g.current){const x=m.current.getBoundingClientRect(),M=g.current.getBoundingClientRect(),C=window.innerWidth,k=window.innerHeight,N=x.width||224,S=x.height||200;let w=r;r.includes("left")?M.left+N>C&&(w=w.replace("left","right")):M.right-N<0&&(w=w.replace("right","left")),r.includes("top")?M.top-S<0&&(w=w.replace("top","bottom")):M.bottom+S>k&&(w=w.replace("bottom","top")),y(w)}},[c,r]);const b=x=>{x.disabled||(x.onClick?.(),n?.(x.value,x),d(!1),f(-1))},_={"bottom-left":"dropdown-menu-bottom-left","bottom-right":"dropdown-menu-bottom-right","top-left":"dropdown-menu-top-left","top-right":"dropdown-menu-top-right"};return h.jsxs(I,{ref:g,className:`dropdown ${o}`,style:i,id:a,...l,children:[h.jsxs(ae,{onClick:()=>d(!c),className:s==="text"?"dropdown-trigger-text":"dropdown-trigger","aria-haspopup":"true","aria-expanded":c,children:[e,s!=="text"&&h.jsx("svg",{className:`inline-block ml-2 w-4 h-4 transition-transform ${c?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),c&&h.jsx(I,{ref:m,className:`dropdown-menu ${_[p]}`,role:"menu","aria-orientation":"vertical",children:h.jsx(I,{className:"py-1",children:t.map((x,M)=>h.jsx(I,{className:`${x.disabled?"dropdown-item-disabled":"dropdown-item"} ${u===M?"dropdown-item-focused":""}`,onClick:()=>b(x),onMouseEnter:()=>!x.disabled&&f(M),role:"menuitem","aria-disabled":x.disabled,tabIndex:x.disabled?-1:0,children:x.label},x.value))})})]})},yy=({imageUrl:e,imageAlt:t="",title:n,subtitle:r,description:s,price:o,originalPrice:i,discountRate:a,currency:l="₩",buttonText:c="자세히 보기",onButtonClick:d,className:u="",style:f,id:g,editorAttrs:m})=>{const p=a&&a>0,y=(o??0).toLocaleString(),b=i?.toLocaleString();return h.jsxs(I,{className:`rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-xl transition-shadow border border-gray-200 dark:border-gray-700 overflow-hidden ${u}`,style:f,id:g,...m,children:[e&&h.jsxs(I,{className:"relative w-full",children:[h.jsx(_n,{src:e,alt:t||n,className:"w-full h-64 object-cover"}),p&&h.jsx(I,{className:"absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded-md",children:h.jsxs(Q,{className:"text-sm font-bold",children:["-",a,"%"]})})]}),h.jsxs(I,{className:"p-4",children:[r&&h.jsx(Gr,{className:"text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider mb-2",children:r}),h.jsx(ar,{className:"text-xl font-bold text-gray-900 dark:text-white mb-2 line-clamp-2",children:n}),s&&h.jsx(vt,{className:"text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-3",children:s}),h.jsxs(I,{className:"flex items-center gap-2 mb-4",children:[h.jsxs(Q,{className:"text-2xl font-bold text-gray-900 dark:text-white",children:[l,y]}),i&&h.jsxs(Q,{className:"text-sm text-gray-400 dark:text-gray-500 line-through",children:[l,b]})]}),c&&h.jsx(ae,{onClick:d,className:"w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors",children:c})]})]})},vy=({value:e,label:t,change:n,changeLabel:r="전월 대비",iconName:s,trend:o="neutral",className:i="",style:a,id:l,editorAttrs:c})=>{const d=o==="up",u=o==="down",f=n!==void 0?Math.abs(n):0,g=d?"stats-trend-up":u?"stats-trend-down":"stats-trend-neutral",m=d?de.ArrowUp:u?de.ArrowDown:void 0;return h.jsxs(I,{className:`stat-card ${i}`,style:a,id:l,...c,children:[h.jsxs(I,{className:"flex-between mb-md",children:[s&&h.jsx(I,{className:"stats-icon stats-icon-blue",children:h.jsx(oe,{name:s,className:"icon-lg"})}),n!==void 0&&h.jsxs(I,{className:`stats-change ${g}`,children:[m&&h.jsx(oe,{name:m,className:"icon-sm"}),h.jsxs(Q,{className:"text-sm font-semibold",children:[f,"%"]})]})]}),h.jsx(Gr,{className:"stats-value-lg",children:typeof e=="number"?e.toLocaleString():e}),h.jsx(vt,{className:"stats-label",children:t}),n!==void 0&&r&&h.jsx(I,{className:"stats-sublabel",children:h.jsx(Q,{children:r})})]})},du=()=>window.G7Core,uu=(e,t)=>du()?.t?.(e,t)??e,xy=({name:e="search",placeholder:t,value:n,onChange:r,onSubmit:s,showButton:o=!1,suggestions:i=[],onSuggestionClick:a,showSuggestions:l=!1,className:c="",style:d,id:u,editorAttrs:f})=>{const g=t??uu("common.search_placeholder"),[m,p]=v.useState(""),[y,b]=v.useState(!1),_=v.useRef(null),x=v.useRef(""),M=n!==void 0?n:m,C=l&&y&&i.length>0&&M.length>0,k="w-64 pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm",N=du()?.style?.mergeClasses?.(k,c)??`${k} ${c}`;v.useEffect(()=>{n!==void 0&&(x.current=n)},[n]);const S=D=>{const P=D.target.value;p(P),r?.(D),x.current!==""&&P===""&&!D.nativeEvent.inputType&&setTimeout(()=>{const L=_.current?.querySelector("form");L&&L.requestSubmit()},0),x.current=P},w=D=>{p(D.text);const P={target:{value:D.text,name:e}};r?.(P),a?.(D),b(!1)},E=D=>{D.preventDefault(),s?.(D)};return h.jsxs(I,{ref:_,className:"relative",style:d,id:u,...f,children:[h.jsx(Ga,{onSubmit:E,className:"relative",children:h.jsxs(I,{className:`relative flex items-center ${o?"gap-2":""}`,children:[h.jsxs(I,{className:"relative flex-1",children:[h.jsx(I,{className:"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",children:h.jsx(oe,{name:de.Search,className:"w-5 h-5 text-gray-400 dark:text-gray-500"})}),h.jsx(St,{type:"search",name:e,value:M,onChange:S,onFocus:()=>b(!0),onBlur:()=>setTimeout(()=>b(!1),200),placeholder:g,className:N})]}),o&&h.jsx(ae,{type:"submit",className:"px-4 py-2 bg-gray-800 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-500 transition-colors text-sm font-medium cursor-pointer h-[42px]",children:uu("common.search")})]})}),C&&h.jsx(I,{className:"absolute z-10 w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-64 overflow-y-auto",children:i.map(D=>h.jsx(I,{className:"px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 transition-colors",onClick:()=>w(D),children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:de.Search,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:D.text})]})},D.id))})]})},ul=(e,t)=>window.G7Core?.t?.(e,t)??e,wy=({title:e,filters:t,onChange:n,onReset:r,showResetButton:s=!0,className:o="",style:i,id:a,editorAttrs:l})=>{const c=e??ul("common.filter"),d=(f,g)=>{n?.(f,g)},u=(f,g,m)=>{const p=t.find(b=>b.id===f);if(!p)return;const y=Array.isArray(p.value)?p.value:[];if(typeof g=="string"){const b=y.filter(x=>typeof x=="string"),_=m?[...b,g]:b.filter(x=>x!==g);n?.(f,_)}else{const b=y.filter(x=>typeof x=="number"),_=m?[...b,g]:b.filter(x=>x!==g);n?.(f,_)}};return h.jsxs(I,{className:`rounded-lg bg-white dark:bg-gray-800 shadow-md border border-gray-200 dark:border-gray-700 p-4 ${o}`,style:i,id:a,...l,children:[h.jsxs(I,{className:"flex items-center justify-between mb-4 pb-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(Q,{className:"text-lg font-semibold text-gray-900 dark:text-white",children:c}),s&&r&&h.jsx(ae,{onClick:r,className:"text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium",children:ul("common.reset")})]}),h.jsx(I,{className:"space-y-4",children:t.map(f=>h.jsxs(I,{className:"space-y-2",children:[h.jsx(ln,{className:"block text-sm font-medium text-gray-700 dark:text-gray-300",children:f.label}),f.type==="select"&&h.jsxs(Xr,{value:f.value,onChange:g=>d(f.id,g.target.value),className:"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white",children:[h.jsx("option",{value:"",children:ul("common.select_placeholder")}),f.options.map(g=>h.jsx("option",{value:g.value,children:g.label},g.id))]}),f.type==="checkbox"&&h.jsx(I,{className:"space-y-2",children:f.options.map(g=>{const m=Array.isArray(f.value)?f.value.some(p=>p===g.value):!1;return h.jsxs(ln,{className:"flex items-center gap-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 p-2 rounded-md transition-colors",children:[h.jsx(En,{checked:m,onChange:p=>u(f.id,g.value,p.target.checked),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:g.label})]},g.id)})})]},f.id))})]})},ky=({tabs:e,activeTabId:t,onTabChange:n,variant:r="default",className:s="",style:o,mobileBreakpoint:i=768,id:a,editorAttrs:l})=>{const d=window.G7Core?.useResponsive,u=d?.(),f=u?u.width<i:typeof window<"u"&&window.innerWidth<i,g=v.useRef([]),m=k=>{!k.disabled&&k.id!==t&&n?.(k.id)},p=(k,N)=>{const S=k.key==="ArrowRight"?1:k.key==="ArrowLeft"?-1:0;let w=null;if(S!==0)for(let E=1;E<=e.length;E+=1){const D=(N+S*E+e.length*E)%e.length;if(!e[D]?.disabled){w=D;break}}else if(k.key==="Home")w=e.findIndex(E=>!E.disabled);else if(k.key==="End"){for(let E=e.length-1;E>=0;E-=1)if(!e[E]?.disabled){w=E;break}}w===null||w<0||(k.preventDefault(),g.current[w]?.focus(),m(e[w]))},y=k=>{const N=k.target.value,S=Number(N),w=typeof N=="string"&&N!==""&&!Number.isNaN(S)?S:N,E=e.find(D=>String(D.id)===String(w));E&&m(E)};if(f)return h.jsx(I,{className:s,style:o,id:a,...l,children:h.jsx(Xr,{value:t!==void 0?String(t):"",onChange:y,options:e.map(k=>({value:String(k.id),label:k.badge!==void 0?`${k.label} (${k.badge})`:k.label,disabled:k.disabled})),className:"w-full"})});const b=k=>{const N=k.id===t;if(k.disabled)return"tab-btn-base tab-btn-disabled";switch(r){case"pills":return N?"tab-btn-base tab-btn-pills-active":"tab-btn-base tab-btn-pills";case"underline":return N?"tab-btn-base tab-btn-underline-active":"tab-btn-base tab-btn-underline";default:return N?"tab-btn-base tab-btn-default-active":"tab-btn-base tab-btn-default"}},_=r==="underline"?"tab-nav-underline":"tab-nav-default",x=l?.["data-editor-path"],M=e.findIndex(k=>k.id===t),C=M>=0?M:e.findIndex(k=>!k.disabled);return h.jsx(Yo,{className:`${_} ${s}`,style:o,role:"tablist","aria-orientation":"horizontal",id:a,...l,children:e.map((k,N)=>h.jsxs(ae,{ref:S=>{g.current[N]=S},type:"button",role:"tab","aria-selected":k.id===t,tabIndex:N===C?0:-1,onClick:()=>m(k),onKeyDown:S=>p(S,N),disabled:k.disabled,className:b(k),...x?{"data-editor-item-path":`${x}.props.tabs.${N}`}:{},children:[k.iconName&&h.jsx(oe,{name:k.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:k.label}),k.badge!==void 0&&h.jsx(I,{className:"flex items-center justify-center min-w-[20px] h-5 px-1.5 bg-red-500 dark:bg-red-600 text-white dark:text-white text-xs font-bold rounded-full",children:h.jsx(Q,{children:k.badge})})]},k.id))})},fl=window.G7Core?.createLogger?.("Handler:ScrollToSection")??{log:(...e)=>console.log("[Handler:ScrollToSection]",...e),warn:(...e)=>console.warn("[Handler:ScrollToSection]",...e),error:(...e)=>console.error("[Handler:ScrollToSection]",...e)};async function fu(e,t){const n=e.params||{},{targetId:r,offset:s=120,delay:o=100,scrollContainerId:i}=n;if(!r){fl.warn("targetId is required");return}const a=await Cy(r,o);if(!a){fl.warn(`Element not found: ${r}`);return}const l=Sy(a,i),c=_y(a,l,s);Ey(l,c)}async function Cy(e,t){await new Promise(s=>setTimeout(s,t));const n=10,r=50;for(let s=0;s<n;s++){const o=document.getElementById(e);if(o)return await new Promise(i=>setTimeout(i,50)),o;await new Promise(i=>setTimeout(i,r))}return null}function Sy(e,t){if(t){const o=document.getElementById(t);if(o&&hu(o))return o;fl.warn(`Container '${t}' ${o?"has no scroll":"not found"}, trying auto-detect`)}let n=e.parentElement,r=0;const s=10;for(;n&&n!==document.body&&r<s;){if(hu(n))return n;n=n.parentElement,r++}return window}function hu(e){const t=window.getComputedStyle(e);return(t.overflowY==="auto"||t.overflowY==="scroll")&&e.scrollHeight>e.clientHeight}function _y(e,t,n){if(t===window||!("getBoundingClientRect"in t))return e.getBoundingClientRect().top+window.pageYOffset-n;{const r=t.getBoundingClientRect(),s=e.getBoundingClientRect();return t.scrollTop+(s.top-r.top)-n}}function Ey(e,t){e===window||!("scrollTo"in e)?window.scrollTo({top:t,behavior:"smooth"}):e.scrollTo({top:t,behavior:"smooth"})}window.G7Core?.createLogger?.("Comp:TabNavigation");const My=({tabs:e,activeTabId:t,className:n="bg-white dark:bg-gray-800 flex gap-2 border-b border-gray-200 dark:border-gray-700 px-6",style:r,activeClassName:s="tab-btn-base tab-btn-default-active",inactiveClassName:o="tab-btn-base tab-btn-default",sectionIdPrefix:i="tab_content_",scrollOffset:a=120,scrollDelay:l=100,onTabChange:c,enableScrollSpy:d=!1,scrollSpyOffset:u=80,scrollContainerId:f,id:g,editorAttrs:m})=>{const[p,y]=v.useState(t??(e.length>0?e[0].id:"")),_=window.G7Core?.useResponsive,x=_?.(),M=x?x.width<768:typeof window<"u"&&window.innerWidth<768,C=v.useRef(!1),k=v.useRef(null),N=v.useRef([]),S=L=>{if(d&&(C.current=!0,k.current&&clearTimeout(k.current)),L.onClick){if(L.onClick(L.id),!d)return}else if(c){if(c(L.id),!d)return}else y(L.id);const $=L.scrollOffset??a,A=L.scrollDelay??l;fu({params:{targetId:`${i}${L.id}`,offset:$,delay:A,scrollContainerId:f}}),d&&(k.current=setTimeout(()=>{C.current=!1},A+500))},w=(L,$)=>{const A=L.key==="ArrowRight"?1:L.key==="ArrowLeft"?-1:0;let O=null;if(A!==0)for(let T=1;T<=e.length;T+=1){const z=($+A*T+e.length*T)%e.length;if(!e[z]?.disabled){O=z;break}}else if(L.key==="Home")O=e.findIndex(T=>!T.disabled);else if(L.key==="End"){for(let T=e.length-1;T>=0;T-=1)if(!e[T]?.disabled){O=T;break}}O===null||O<0||(L.preventDefault(),N.current[O]?.focus(),S(e[O]))};v.useEffect(()=>{if(!d)return;const L=f?document.getElementById(f):null,$={root:L??null,rootMargin:`-${u}px 0px -30% 0px`,threshold:[0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1]},A=new Map,O=()=>{const j=L,G=j?j.scrollTop:window.pageYOffset||document.documentElement.scrollTop,U=j?j.scrollHeight:document.documentElement.scrollHeight,W=j?j.clientHeight:document.documentElement.clientHeight;return G+W>=U-10},T=j=>{if(C.current)return;j.forEach(X=>{const K=X.target.id;X.isIntersecting?A.set(K,X.intersectionRatio):A.delete(K)});let G=0,U="";A.forEach((X,K)=>{X>G&&(G=X,U=K)});const W=e[e.length-1],Y=`${i}${W?.id}`,V=A.has(Y);if(O()&&V&&W){y(W.id);return}if(U){const X=U.replace(i,""),K=Number(X),q=isNaN(K)?X:K;e.some(ee=>ee.id===q)&&y(q)}},z=()=>{},H=new IntersectionObserver(T,$);return e.forEach(j=>{const G=document.getElementById(`${i}${j.id}`);G&&H.observe(G)}),window.addEventListener("scroll",z,{passive:!0}),()=>{H.disconnect(),window.removeEventListener("scroll",z),k.current&&clearTimeout(k.current)}},[d,e,i,u,f]);const E=L=>{const $=L.target.value,A=Number($),O=isNaN(A)?$:A,T=e.find(z=>z.id===O);T&&S(T)};if(M)return h.jsx(I,{className:"bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3",id:g,...m,children:h.jsx(Xr,{value:String(p),onChange:E,options:e.map(L=>({value:String(L.id),label:L.label,disabled:L.disabled})),className:"w-full"})});const D=e.findIndex(L=>L.id===p),P=D>=0?D:e.findIndex(L=>!L.disabled);return h.jsx(I,{className:n,style:r,role:"tablist","aria-orientation":"horizontal",id:g,...m,children:e.map((L,$)=>h.jsxs(ae,{ref:A=>{N.current[$]=A},type:"button",role:"tab","aria-selected":p===L.id,tabIndex:$===P?0:-1,className:p===L.id?s:o,disabled:L.disabled,onClick:()=>S(L),onKeyDown:A=>w(A,$),children:[L.iconName&&h.jsx(oe,{name:L.iconName,size:"sm"}),h.jsx(Q,{children:L.label})]},L.id))})},Ny=()=>window.G7Core,Ty=({iconName:e,label:t,onClick:n,variant:r="primary",size:s="md",disabled:o=!1,className:i="",style:a,id:l,editorAttrs:c})=>{const d={sm:t?"px-3 py-1.5 text-sm":"w-8 h-8",md:t?"px-4 py-2 text-base":"w-10 h-10",lg:t?"px-5 py-2.5 text-lg":"w-12 h-12"},u={sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6"},f={primary:"bg-blue-600 hover:bg-blue-700 text-white border-transparent",secondary:"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600",danger:"bg-red-600 hover:bg-red-700 text-white border-transparent",ghost:"bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-transparent",outline:"bg-transparent hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600"},g="inline-flex items-center justify-center gap-2 font-medium rounded-lg border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-400",m=o?"opacity-50 cursor-not-allowed":"",p=`${g} ${d[s]} ${f[r]} ${m}`,y=Ny()?.style?.mergeClasses?.(p,i)??`${p} ${i}`;return h.jsxs(ae,{onClick:n,disabled:o,className:y,style:a,id:l,...c,children:[h.jsx(oe,{name:e,className:u[s]}),t]})},Oy=(e,t)=>window.G7Core?.t?.(e,t)??e,gu=({isOpen:e,onClose:t,title:n,content:r,actions:s=[],width:o="500px",closeOnOverlay:i=!0,showCloseButton:a=!0,className:l="",style:c})=>{const d=()=>{i&&t()},u={primary:"dialog-btn-primary",secondary:"dialog-btn-secondary",danger:"dialog-btn-danger",ghost:"dialog-btn-ghost"};return h.jsxs(Qo,{isOpen:e,onClose:d,width:o,className:l,style:c,children:[h.jsxs(I,{className:"dialog-header",children:[h.jsx(ar,{className:"dialog-title",children:n}),a&&h.jsx(ae,{onClick:t,className:"dialog-close","aria-label":Oy("common.close_dialog"),children:h.jsx(oe,{name:de.Times,className:"icon-md"})})]}),h.jsx(I,{className:"dialog-body",children:r}),s.length>0&&h.jsx(I,{className:"dialog-footer",children:s.map((f,g)=>h.jsxs(ae,{onClick:f.onClick,disabled:f.disabled,className:u[f.variant||"primary"],children:[f.iconName&&h.jsx(oe,{name:f.iconName,className:"icon-sm mr-2 inline-block"}),f.label]},g))})]})},hl=(e,t)=>window.G7Core?.t?.(e,t)??e,pu=({isOpen:e,onClose:t,title:n,message:r,confirmText:s,cancelText:o,onConfirm:i,onCancel:a,confirmButtonVariant:l="primary",cancelButtonVariant:c="secondary",width:d="500px",closeOnOverlay:u=!0,isLoading:f=!1,loadingText:g})=>{const m=s??hl("common.confirm"),p=o??hl("common.cancel"),y=g??hl("common.loading"),b=()=>{i()},_=()=>{f||(a?.(),t())},x=()=>{f||t()},M={primary:"bg-blue-600 text-white hover:bg-blue-700",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600",danger:"bg-red-600 text-white hover:bg-red-700",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"};return h.jsxs(Qo,{isOpen:e,onClose:x,title:n,width:d,children:[h.jsx(I,{className:"py-2",children:typeof r=="string"?h.jsx(vt,{className:"text-gray-700 dark:text-gray-300",children:r}):r}),h.jsxs(I,{className:"flex justify-end gap-3 mt-6",children:[h.jsx(ae,{onClick:_,disabled:f,className:"px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",children:h.jsx(Q,{children:p})}),h.jsxs(ae,{onClick:b,disabled:f,className:`flex items-center gap-2 px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed ${M[l]}`,children:[f&&h.jsx(oe,{name:de.Spinner,className:"w-4 h-4 animate-spin"}),h.jsx(Q,{children:f?y:m})]})]})]})},Ay=(e,t)=>window.G7Core?.t?.(e,t)??e,Dy=({isOpen:e,onClose:t,title:n,message:r,confirmText:s,onConfirm:o,confirmButtonVariant:i="primary",width:a="500px",closeOnOverlay:l=!0})=>{const u=[{label:s??Ay("common.confirm"),onClick:()=>{o?.(),t()},variant:i}],f=typeof r=="string"?h.jsx(vt,{className:"text-gray-700 dark:text-gray-300",children:r}):r;return h.jsx(gu,{isOpen:e,onClose:t,title:n,content:f,actions:u,width:a,closeOnOverlay:l})},Iy=(e,t)=>window.G7Core?.t?.(e,t)??e,Ly={success:de.CheckCircle,error:de.XCircle,warning:de.ExclamationTriangle,info:de.InfoCircle},mu={success:{bg:"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800",text:"text-green-800 dark:text-green-300"},error:{bg:"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",text:"text-red-800 dark:text-red-300"},warning:{bg:"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",text:"text-yellow-800 dark:text-yellow-300"},info:{bg:"bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",text:"text-blue-800 dark:text-blue-300"}},Py={"top-left":"top-4 left-4","top-center":"top-4 left-1/2 -translate-x-1/2","top-right":"top-4 right-4","bottom-left":"bottom-4 left-4","bottom-center":"bottom-4 left-1/2 -translate-x-1/2","bottom-right":"bottom-4 right-4"},$y=({toasts:e,position:t="bottom-center",duration:n=3e3,onRemove:r})=>{const[s,o]=v.useState(new Map),i=v.useCallback(a=>{o(l=>{const c=new Map(l);return c.set(a,!1),c}),setTimeout(()=>{if(r)r(a);else{const l=window.G7Core;l?.state?.update&&l.state.update(c=>({toasts:(Array.isArray(c.toasts)?c.toasts:[]).filter(u=>u.id!==a)}))}},300)},[r]);return v.useEffect(()=>{!e||e.length===0||e.forEach(a=>{if(s.has(a.id))return;o(c=>{const d=new Map(c);return d.set(a.id,!0),d});const l=a.duration||n;l>0&&setTimeout(()=>{i(a.id)},l)})},[e,n,i,s]),!e||e.length===0?null:h.jsx(I,{className:`fixed z-[9999] flex flex-col gap-2 ${Py[t]}`,"aria-live":"polite","aria-atomic":"true",children:e.map(a=>h.jsx(Fy,{toast:a,position:t,isVisible:s.get(a.id)??!0,onClose:()=>i(a.id)},a.id))})},Fy=({toast:e,position:t,isVisible:n,onClose:r})=>{const s=mu[e.type]||mu.info,o=e.icon?e.icon:Ly[e.type],i=t.startsWith("bottom")?"translate-y-2":"-translate-y-2";return h.jsxs(I,{className:` + `.trim().replace(/\s+/g," ");return h.jsx(I,{className:d,dangerouslySetInnerHTML:{__html:c},id:o,...i})},Uo=window.G7Core?.createLogger?.("Comp:DataGrid")??{log:(...t)=>console.log("[Comp:DataGrid]",...t),warn:(...t)=>console.warn("[Comp:DataGrid]",...t),error:(...t)=>console.error("[Comp:DataGrid]",...t)},Ur=(t,e)=>window.G7Core?.t?.(t,e)??t,zb={Div:I,Span:Q,Button:oe,Icon:re,Checkbox:Cn,P:yt,A:Fr,Label:on,Input:wt,H1:Pa,H2:er,H3:$r,H4:Od,Img:kn,Select:Vr,Textarea:Rn,Table:za,Thead:Ha,Tbody:Ua,Tfoot:Ba,Tr:tr,Th:zr,Td:Jt,MultilingualInput:ja,Toggle:Wa,StatusBadge:Fo,Badge:$d,HtmlContent:Ja,ActionMenu:Cs},Ra=()=>{const t=window.G7Core?.getComponentMap?.();return t&&Object.keys(t).length>0?t:zb},el=t=>{if(t==null)return"";if(typeof t=="string"||typeof t=="number"||typeof t=="boolean")return String(t);if(Array.isArray(t))return t.map(e=>el(e)).join(", ");if(typeof t=="object"){if(v.isValidElement(t))return t;if(Object.keys(t).length===0)return"";try{return JSON.stringify(t)}catch{return Uo.warn("객체를 문자열로 변환할 수 없습니다:",t),"[Object]"}}return String(t)},Zd=(t,e)=>{if(!t)return!0;const n=window.G7Core;return typeof n?.evaluateCondition=="function"?n.evaluateCondition(t,{row:e,item:e,$item:e}):(Uo.warn("G7Core.evaluateCondition 미노출 — subRow 조건을 평가할 수 없습니다:",t),!0)},Br=(t,e,n,r="",s)=>{const o=window.G7Core;if(o?.renderItemChildren){const i={row:e,value:n,$value:n};return o.renderItemChildren(t,i,Ra(),r,s?{componentContext:s}:void 0)}return Uo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},Qd=(t,e,n)=>t?!t.includes("{{")&&!t.includes("$t:")?t:Br([{type:"basic",name:"Span",text:t}],{},null,e,n):"",tl=(t,e,n="",r)=>{const s=window.G7Core;if(s?.renderItemChildren){const o={row:e,$value:e};return s.renderItemChildren(t,o,Ra(),n,r?{componentContext:r}:void 0)}return Uo.warn("G7Core.renderItemChildren을 사용할 수 없습니다."),null},Hb=({id:t,columns:e,data:n,sortable:r=!0,pagination:s=!1,pageSize:o=10,onRowClick:i,className:a="",style:l,sortField:c,sortDirection:d,onSortChange:u,selectable:f=!1,selectedIds:g=[],onSelectionChange:m,idField:p="id",selectionScope:y="free",showColumnSelector:b=!1,visibleColumns:_,onColumnVisibilityChange:x,responsiveBreakpoint:E=768,cardRenderer:S,rowActions:w,onRowAction:N,alwaysShowPagination:C=!1,serverSidePagination:k=!1,serverCurrentPage:M,serverTotalPages:T,serverHasMorePages:L,onPageChange:P,prevText:F,nextText:D,columnSelectorText:A,emptyMessage:O,actionsColumnHeader:V,requiredText:H,selectedCountText:W,showFirstLast:j=!0,expandable:U=!1,expandedRowIds:B,onExpandChange:G,expandChildren:z,expandedRowRender:X,expandColumnWidth:Y="48px",expandIconExpanded:Z=de.ChevronDown,expandIconCollapsed:ee=de.ChevronRight,expandConditionField:te,dynamicColumns:ce,dynamicColumnsInsertAfter:J,expandContext:he,__componentContext:le,subRowChildren:ae,subRowCondition:Se,subRowClassName:ue="px-6 py-2 bg-gray-50 dark:bg-gray-900",subRowRender:pe,footerCells:we,footerClassName:He="bg-gray-50 dark:bg-gray-800/50 font-medium",footerCardChildren:se,editorAttrs:Oe})=>{const Le=F||void 0,Ie=D||void 0,Ue=A??Ur("common.column_selector"),et=O??Ur("common.no_data"),bt=V??Ur("common.actions"),Ge=H??Ur("common.required"),ft=W??Ur("common.selected_count"),Be=q=>W===void 0?Ur("common.selected_count",{count:q}):W.replace(/\{\{count\}\}/g,String(q)).replace(/\{count\}/g,String(q)),ne=window.G7Core,fe=ne?.useControllableState,xe=ne?.shallowArrayEqual,[ge,be]=fe?fe(c,null):[c??null,()=>{}],[Ve,De]=fe?fe(d,"asc"):[d??"asc",()=>{}],[$e,Me]=v.useState(1),[qe,ot]=fe?fe(B,[],G,{isEqual:xe}):[B??[],()=>{}],[ye,Ce]=v.useState(!1),Fe=v.useRef(null),nt=ne?.useResponsive,pt=nt?.(),it=pt?pt.width<E:typeof window<"u"&&window.innerWidth<E,rt=v.useMemo(()=>{if(!ce||!Array.isArray(ce)||ce.length===0)return e;if(J){const q=e.findIndex(me=>me.field===J);if(q!==-1)return[...e.slice(0,q+1),...ce,...e.slice(q+1)]}return[...e,...ce]},[e,ce,J]),[Ot,ht]=v.useState(()=>_&&_.length>0?_:e.filter(q=>!q.hidden).map(q=>q.field));v.useEffect(()=>{_&&_.length>0&&ht(_)},[_]);const Qt=v.useRef("");v.useEffect(()=>{const q=e.map(me=>me.field).join(",");Qt.current&&q!==Qt.current&&(!_||_.length===0)&&ht(e.filter(me=>!me.hidden).map(me=>me.field)),Qt.current=q},[e,_]);const Gt=v.useRef([]),mt=v.useRef(null);v.useEffect(()=>{if(ce&&ce.length>0){const q=ce.map(ct=>ct.field);if(mt.current===null&&(mt.current=_!==void 0&&_.length>0,mt.current)){Gt.current=q;return}const me=Gt.current,Te=q.filter(ct=>!me.includes(ct));if(Te.length>0){const ct=Te.filter(ze=>!Ot.includes(ze));if(ct.length>0){const ze=[...Ot,...ct];ht(ze),x?.(ze)}}Gt.current=q}},[ce]),v.useEffect(()=>{k||Me(1)},[n,k]),v.useEffect(()=>{const q=me=>{Fe.current&&!Fe.current.contains(me.target)&&Ce(!1)};return ye&&document.addEventListener("mousedown",q),()=>document.removeEventListener("mousedown",q)},[ye]);const St=v.useMemo(()=>!b&&(!_||_.length===0)?rt.filter(q=>!q.hidden):rt.filter(q=>Ot.includes(q.field)),[rt,Ot,b,_]),xn=r&&!(k&&!u),Vt=v.useMemo(()=>!ge||!r||!n?n||[]:u||k?n:[...n].sort((q,me)=>{const Te=q[ge],ct=me[ge];if(Te===ct)return 0;const ze=Te>ct?1:-1;return Ve==="asc"?ze:-ze}),[n,ge,Ve,r,u,k]),Ze=v.useMemo(()=>{if(k||!s)return Vt;const q=($e-1)*o,me=q+o;return Vt.slice(q,me)},[Vt,$e,o,s,k]),jn=k?M||1:$e,dn=k?T??null:Math.ceil((Vt?.length||0)/o),Io=C||(dn??0)>1||L===!0||jn>1,Lo=v.useCallback(q=>{k&&P?P(q):Me(q)},[k,P]),Da=q=>{if(!xn||e.find(ct=>ct.field===q)?.sortable===!1)return;const Te=ge===q&&Ve==="asc"?"desc":"asc";be(q),De(Te),u?.(q,Te)},Ia=v.useCallback(()=>{if(!m||!Ze)return;const q=Ze.map(Te=>Te[p]);if(q.every(Te=>g.includes(Te)))m(g.filter(Te=>!q.includes(Te)));else{const Te=[...new Set([...g,...q])];m(Te)}},[Ze,g,m,p]),La=v.useCallback(q=>{m&&(g.includes(q)?m(g.filter(me=>me!==q)):m([...g,q]))},[g,m]);v.useEffect(()=>{if(y!=="page"||!m||g.length===0)return;const q=new Set((Ze??[]).map(Te=>Te[p])),me=g.filter(Te=>q.has(Te));me.length!==g.length&&m(me)},[y,f,Ze,g,m,p]);const ke=v.useCallback(q=>{if(rt.find(ct=>ct.field===q)?.required)return;const Te=Ot.includes(q)?Ot.filter(ct=>ct!==q):[...Ot,q];ht(Te),x?.(Te)},[rt,Ot,x]),K=v.useCallback((q,me)=>{N?.(q,me)},[N]),ie=v.useCallback(q=>w?w.map(me=>{let Te=me.disabled??!1;if(me.disabledField&&!Te){const ct=me.disabledField.split(".");let ze=q;for(const FT of ct)ze=ze?.[FT];ze||(Te=!0)}return{...me,disabled:Te,onClick:()=>K(me.id,q)}}):[],[w,K]),ve=v.useCallback(q=>{if(!te)return!0;const me=q[te];return Array.isArray(me)?me.length>0:!!me},[te]),st=v.useCallback(q=>{const me=qe.includes(q)?qe.filter(Te=>Te!==q):[...qe,q];fe?ot(me):G&&G(me)},[qe,G,fe,ot]),Tt=v.useCallback(q=>qe.includes(q),[qe]),_t=v.useCallback(q=>X?X(q):z&&z.length>0&&ne?.renderExpandContent?ne.renderExpandContent({children:z,row:q,expandContext:he,componentContext:le,componentMap:Ra(),keyPrefix:`expand-${q[p]}`}):null,[X,z,p,he,le,le?.state]),Yt=v.useMemo(()=>!Ze||Ze.length===0?!1:Ze.every(q=>g.includes(q[p])),[Ze,g,p]),wn=v.useMemo(()=>{if(!Ze||Ze.length===0)return!1;const q=Ze.filter(me=>g.includes(me[p])).length;return q>0&&q<Ze.length},[Ze,g,p]),Pr=(q,me)=>{const Te=q[p],ct=Tt(Te);return h.jsxs(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4",children:[h.jsxs(I,{className:"flex items-start justify-between",children:[U&&ve(q)&&h.jsx(oe,{onClick:()=>st(Te),className:"p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors mr-2","aria-label":ct?"Collapse card":"Expand card",children:h.jsx(re,{name:ct?Z:ee,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})}),f&&h.jsx(Cn,{checked:g.includes(q[p]),onChange:()=>La(q[p]),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded mr-3 mt-1"}),h.jsx(I,{className:"flex-1",children:h.jsx(I,{className:"grid grid-cols-2 gap-x-4 gap-y-2",children:me.map(ze=>h.jsxs(I,{className:"min-w-0",children:[h.jsx(Q,{className:"text-xs font-medium text-gray-500 dark:text-gray-400 uppercase",children:ze.header}),h.jsx(I,{className:"text-sm text-gray-900 dark:text-gray-200 mt-0.5 break-words",children:ze.cellChildren&&ze.cellChildren.length>0?Br(ze.cellChildren,q,q[ze.field],`card-${q[p]}-${ze.field}`,le):ze.render?ze.render(q[ze.field],q):el(q[ze.field])})]},ze.field))})}),w&&w.length>0&&h.jsx(I,{className:"ml-2",children:h.jsx(Cs,{items:ie(q),triggerLabel:"",triggerIconName:de.EllipsisHorizontal})})]}),U&&ct&&ve(q)&&h.jsx(I,{className:"mt-4 pt-4 border-t border-gray-200 dark:border-gray-700",children:_t(q)}),(ae&&ae.length>0||pe)&&Zd(Se,q)&&h.jsx(I,{className:ue?`mt-3 ${ue}`:"mt-3 pt-3 border-t border-gray-100 dark:border-gray-700",children:pe?pe(q):tl(ae,q,`card-subrow-${Te}`,le)})]})};return it?h.jsxs(I,{id:t,className:`w-full ${a}`,style:l,...Oe,children:[b&&h.jsx(I,{className:"mb-4 flex justify-end",children:h.jsxs(I,{ref:Fe,className:"relative",children:[h.jsxs(oe,{onClick:()=>Ce(!ye),className:"flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700",children:[h.jsx(re,{name:de.Cog,className:"w-4 h-4 text-gray-500 dark:text-gray-400"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:Ue})]}),ye&&h.jsx(I,{className:"absolute right-0 z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",children:h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:rt.map(q=>h.jsxs(I,{className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${q.required?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:()=>!q.required&&ke(q.field),children:[h.jsx(Cn,{checked:Ot.includes(q.field),disabled:q.required,onChange:()=>{},className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"}),h.jsxs(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:[q.header,q.required&&h.jsxs(Q,{className:"text-xs text-gray-500 dark:text-gray-400 ml-1",children:["(",Ge,")"]})]})]},q.field))})})]})}),f&&g.length>0&&ft&&h.jsx(I,{className:"mb-4 text-sm text-gray-600 dark:text-gray-400",children:Be(g.length)}),h.jsx(I,{id:t?`${t}__body`:void 0,children:Ze&&Ze.length>0?Ze.map((q,me)=>h.jsx(I,{children:S?S(q,St):Pr(q,St)},q[p]||me)):h.jsx(I,{className:"text-center py-8 text-gray-500 dark:text-gray-400",children:et})}),we&&we.length>0&&Ze&&Ze.length>0&&h.jsx(I,{className:`rounded-lg p-4 mt-2 ${He}`,children:se&&se.length>0?Br(se,{},null,"footer-card",le):h.jsx(I,{className:"space-y-1",children:we.filter(q=>q.text||q.children&&q.children.length>0).map((q,me)=>{const Te=St.find(ct=>ct.field===q.field);return h.jsxs(I,{className:"flex justify-between text-sm",children:[h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:Te?.header||""}),h.jsx(Q,{className:q.className||"text-gray-900 dark:text-gray-100",children:q.children&&q.children.length>0?Br(q.children,{},null,`footer-mobile-${me}`,le):Qd(q.text,`footer-mobile-text-${me}`,le)})]},`footer-mobile-${me}`)})})}),s&&Io&&h.jsx(I,{className:"flex justify-center mt-4",children:h.jsx($o,{currentPage:jn,totalPages:dn,hasMorePages:k?L:void 0,onPageChange:Lo,showFirstLast:j,maxVisiblePages:5,prevText:Le,nextText:Ie})})]}):h.jsxs(I,{id:t,className:`w-full ${a}`,style:l,...Oe,children:[b&&h.jsx(I,{className:"mb-4 flex justify-end",children:h.jsxs(I,{ref:Fe,className:"relative",children:[h.jsxs(oe,{onClick:()=>Ce(!ye),className:"flex items-center gap-2 px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700",children:[h.jsx(re,{name:de.Cog,className:"w-4 h-4 text-gray-500 dark:text-gray-400"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:Ue})]}),ye&&h.jsx(I,{className:"absolute right-0 z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",children:h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:rt.map(q=>h.jsxs(I,{className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${q.required?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,onClick:()=>!q.required&&ke(q.field),children:[h.jsx(Cn,{checked:Ot.includes(q.field),disabled:q.required,onChange:()=>{},className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"}),h.jsxs(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:[q.header,q.required&&h.jsxs(Q,{className:"text-xs text-gray-500 dark:text-gray-400 ml-1",children:["(",Ge,")"]})]})]},q.field))})})]})}),f&&g.length>0&&ft&&h.jsx(I,{className:"mb-4 text-sm text-gray-600 dark:text-gray-400",children:Be(g.length)}),h.jsx(I,{id:t?`${t}__body`:void 0,className:"overflow-x-auto border border-gray-200 dark:border-gray-700 rounded-lg",children:h.jsxs(za,{className:"w-full",children:[h.jsx(Ha,{className:"bg-gray-50 dark:bg-gray-700",children:h.jsxs(tr,{children:[U&&h.jsx(zr,{className:"px-2 py-3",style:{width:Y}}),f&&h.jsx(zr,{className:"px-4 py-3 w-10",children:h.jsx(Cn,{checked:Yt,ref:q=>{q&&(q.indeterminate=wn)},onChange:Ia,className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"})}),St.map(q=>h.jsxs(zr,{className:`px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider ${xn&&q.sortable!==!1?"cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600":""}`,style:{width:q.width,minWidth:q.width},onClick:xn?()=>Da(q.field):void 0,children:[q.header,ge===q.field&&h.jsx(Q,{className:"ml-1",children:Ve==="asc"?"↑":"↓"})]},q.field)),w&&w.length>0&&h.jsx(zr,{className:"px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider w-24",children:bt})]})}),h.jsx(Ua,{className:`bg-white dark:bg-gray-800 ${ue?"":"divide-y divide-gray-200 dark:divide-gray-700"}`,children:Ze&&Ze.length>0?Ze.map((q,me)=>{const Te=q[p]||me,ct=Tt(Te);return h.jsxs(v.Fragment,{children:[h.jsxs(tr,{className:`${i?"cursor-pointer":""} hover:bg-gray-50 dark:hover:bg-gray-700 ${g.includes(q[p])?"bg-blue-50 dark:bg-blue-900/20":""} ${ue&&me>0?"border-t border-gray-200 dark:border-gray-700":""}`,onClick:()=>i?.(q),children:[U&&h.jsx(Jt,{className:"px-2 py-4 text-center",onClick:ze=>ze.stopPropagation(),children:ve(q)?h.jsx(oe,{onClick:()=>st(Te),className:"p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors","aria-label":ct?"Collapse row":"Expand row",children:h.jsx(re,{name:ct?Z:ee,className:"w-4 h-4 text-gray-500 dark:text-gray-400"})}):null}),f&&h.jsx(Jt,{className:"px-4 py-4",onClick:ze=>ze.stopPropagation(),children:h.jsx(Cn,{checked:g.includes(q[p]),onChange:()=>La(q[p]),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded"})}),St.map(ze=>h.jsx(Jt,{className:"px-6 py-4 align-top whitespace-nowrap text-sm text-gray-900 dark:text-gray-200",style:{width:ze.width,minWidth:ze.width},children:ze.cellChildren&&ze.cellChildren.length>0?Br(ze.cellChildren,q,q[ze.field],`${q[p]}-${ze.field}`,le):ze.render?ze.render(q[ze.field],q):el(q[ze.field])},ze.field)),w&&w.length>0&&h.jsx(Jt,{className:"px-6 py-4 text-right",onClick:ze=>ze.stopPropagation(),children:h.jsx(Cs,{items:ie(q),triggerLabel:"",triggerIconName:de.EllipsisHorizontal,position:"right"})})]}),U&&ct&&ve(q)&&h.jsx(tr,{className:"bg-gray-50 dark:bg-gray-900",children:h.jsx(Jt,{colSpan:St.length+1+(f?1:0)+(w&&w.length>0?1:0),className:"px-6 py-4",children:_t(q)})}),(ae&&ae.length>0||pe)&&Zd(Se,q)&&h.jsx(tr,{className:ue?"!border-t-0":"",children:h.jsx(Jt,{colSpan:St.length+(U?1:0)+(f?1:0)+(w&&w.length>0?1:0),className:ue?"p-2":ue||"px-6 py-2 bg-gray-50 dark:bg-gray-900",children:ue?h.jsx(I,{className:ue,children:pe?pe(q):tl(ae,q,`subrow-${Te}`,le)}):pe?pe(q):tl(ae,q,`subrow-${Te}`,le)})})]},Te)}):h.jsx(tr,{children:h.jsx(Jt,{colSpan:St.length+(U?1:0)+(f?1:0)+(w&&w.length>0?1:0),className:"px-6 py-8 text-center text-gray-500 dark:text-gray-400",children:et})})}),we&&we.length>0&&Ze&&Ze.length>0&&h.jsx(Ba,{children:h.jsxs(tr,{className:He,children:[U&&h.jsx(Jt,{className:"px-2 py-4"}),f&&h.jsx(Jt,{className:"px-4 py-4"}),(()=>{const q=new Map(we.filter(me=>me.field).map(me=>[me.field,me]));return St.map(me=>{const Te=q.get(me.field);return Te?h.jsx(Jt,{className:`px-6 py-4 text-sm text-gray-900 dark:text-gray-200 ${Te.className||""}`,colSpan:Te.colSpan,style:{width:me.width,minWidth:me.width},children:Te.children&&Te.children.length>0?Br(Te.children,{},null,`footer-${me.field}`,le):Qd(Te.text,`footer-text-${me.field}`,le)},`footer-${me.field}`):h.jsx(Jt,{className:"px-6 py-4",style:{width:me.width,minWidth:me.width}},`footer-${me.field}`)})})(),w&&w.length>0&&h.jsx(Jt,{className:"px-6 py-4"})]})})]})}),s&&Io&&h.jsx(I,{className:"flex justify-center mt-4",children:h.jsx($o,{currentPage:jn,totalPages:dn,hasMorePages:k?L:void 0,onPageChange:Lo,showFirstLast:j,maxVisiblePages:5,prevText:Le,nextText:Ie})})]})},Ub=window.G7Core?.createLogger?.("Comp:CardGrid")??{log:(...t)=>console.log("[Comp:CardGrid]",...t),warn:(...t)=>console.warn("[Comp:CardGrid]",...t),error:(...t)=>console.error("[Comp:CardGrid]",...t)},Bb=(t,e)=>window.G7Core?.t?.(t,e)??t,jb=()=>window.G7Core?.getComponentMap?.()||{},Wb=(t,e)=>{if(!t)return!0;const n=window.G7Core;return typeof n?.evaluateCondition=="function"?n.evaluateCondition(t,{row:e,item:e,$item:e}):(Ub.warn("G7Core.evaluateCondition 미노출 — 조건을 평가할 수 없습니다:",t),!0)},Jd=(t,e,n,r,s)=>{const o=window.G7Core;if(t.name==="ActionMenu"){if(r&&r.length>0&&s)return{...t,props:{...t.props,items:r.map(i=>({...i,onClick:()=>s(i.id,e)}))}};if(t.actions&&Array.isArray(t.actions)){const i=t.props?.items||[],a=new Map(t.actions.map(c=>[c.id,c])),l=i.map(c=>{if(typeof c.onClick=="string"){const d=c.onClick,u=a.get(d);if(u&&o?.ActionDispatcher)return{...c,onClick:()=>{const f={...n};o.ActionDispatcher.dispatch(u,f)}}}return c});return{...t,props:{...t.props,items:l}}}}return t.children&&Array.isArray(t.children)?{...t,children:t.children.map(i=>Jd(i,e,n,r,s))}:t},Gb=(t,e,n,r="",s,o)=>{const i=window.G7Core;if(i?.renderItemChildren){const a=t.filter(d=>{const u=d.condition||d.if;return u?Wb(u,e):!0}),l={row:e,$value:e,index:n},c=a.map(d=>Jd(d,e,l,s,o));try{const d=jb();return i.renderItemChildren(c,l,d,r)}catch{return h.jsxs(I,{className:"text-red-600 dark:text-red-400 p-4",children:[h.jsx(yt,{children:"컴포넌트 로드 실패"}),h.jsx(yt,{className:"text-sm",children:"[CardGrid] 데이터를 표시할 수 없습니다."})]})}}return null},nl=(t,e)=>{if(t.name==="Div"&&t.children&&t.children.length>0){const n=t.children.map((r,s)=>nl(r,`${e}-child-${s}`)).filter(Boolean);return h.jsx(I,{className:t.props?.className||"",children:n},e)}return h.jsx(I,{className:"h-4 bg-gray-200 dark:bg-gray-700 rounded w-full animate-pulse"},e)},Rd=(t,e="skeleton")=>!t||t.length===0?null:t.map((n,r)=>{const s=`${e}-${n.id||n.name}-${r}`;return n.condition||n.if?null:(n.iteration,nl(n,s))}).filter(Boolean),Yb=({data:t,cardColumns:e=[],gridColumns:n=3,gap:r=4,responsiveColumns:s,cardClassName:o="",idField:i="id",pagination:a=!0,pageSize:l=12,serverSidePagination:c=!1,serverCurrentPage:d,serverTotalPages:u,alwaysShowPagination:f=!1,onPageChange:g,paginationInfoText:m,rowActions:p,onRowAction:y,className:b="",style:_,emptyMessage:x,prevText:E,nextText:S,showSkeleton:w=!0,skeletonCount:N,skeletonCellChildren:C,id:k,editorAttrs:M})=>{const T=x??Bb("common.no_data"),L=e[0]?.cellChildren||[],[P,F]=v.useState(1);window.G7Core?.useResponsive?.();const A=c?d??1:P,O=c?u??1:Math.ceil((t?.length??0)/l),V=G=>{c?g?.(G):F(G)},H=v.useCallback((G,z)=>{y?.(G,z)},[y]),W=v.useMemo(()=>{if(!t)return[];if(!a||c)return t;const G=(P-1)*l,z=G+l;return t.slice(G,z)},[t,a,c,P,l]),j=v.useMemo(()=>{const G=[],z=s?.sm??1;G.push(`grid-cols-${z}`),s?.sm&&G.push(`sm:grid-cols-${s.sm}`);const X=s?.md??Math.min(n,2);G.push(`md:grid-cols-${X}`);const Y=s?.lg??n;return G.push(`lg:grid-cols-${Y}`),s?.xl&&G.push(`xl:grid-cols-${s.xl}`),G.join(" ")},[n,s]),U=v.useMemo(()=>w?C&&C.length>0?Rd(C,"custom-skeleton"):L&&L.length>0?Rd(L,"auto-skeleton"):null:null,[w,C,L]);if(!t){if(!w)return null;const G=N??n;return h.jsx(I,{className:`grid ${j} gap-${r} ${b}`,style:_,id:k,...M,children:Array.from({length:G}).map((z,X)=>h.jsx(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 space-y-4",children:U},`skeleton-${X}`))})}if(W.length===0)return h.jsx(I,{className:b,style:_,id:k,...M,children:h.jsx(I,{className:"bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-12",children:h.jsx(yt,{className:"text-center text-gray-600 dark:text-gray-400",children:T})})});const B=a&&(f||O>1);return h.jsxs(I,{className:b,style:_,id:k,...M,children:[h.jsx(I,{className:`grid ${j} gap-${r}`,children:W.map((G,z)=>h.jsx(I,{className:o,children:Gb(L,G,z,`card-${z}`,p,H)},G[i]||z))}),B&&h.jsxs(I,{className:"mt-6 space-y-4 flex flex-col items-center",children:[h.jsx($o,{currentPage:A,totalPages:O,onPageChange:V,showFirstLast:!1,maxVisiblePages:5,prevText:E,nextText:S}),m&&h.jsx(I,{className:"text-sm text-gray-600 dark:text-gray-400 text-center",children:m})]})]})},Xb=(t,e)=>window.G7Core?.t?.(t,e)??t,Bo=({isOpen:t,onClose:e,title:n,titlePrefix:r,titleSuffix:s,closeOnBackdropClick:o=!0,closeOnOverlayClick:i,showCloseButton:a=!0,closeOnEscape:l=!0,children:c,width:d="500px",className:u="",style:f,__dataContext:g,__translationContext:m})=>{const p=i!==void 0?i:o,y=v.useRef(null),b=v.useRef(null);if(v.useEffect(()=>{const E=S=>{S.key==="Escape"&&t&&l&&e()};if(t&&l)return document.addEventListener("keydown",E),()=>document.removeEventListener("keydown",E)},[t,e,l]),v.useEffect(()=>{if(t&&y.current){b.current=document.activeElement;const E=y.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),S=E[0],w=E[E.length-1];S?.focus();const N=C=>{C.key==="Tab"&&(C.shiftKey?document.activeElement===S&&(C.preventDefault(),w?.focus()):document.activeElement===w&&(C.preventDefault(),S?.focus()))};return document.addEventListener("keydown",N),()=>{document.removeEventListener("keydown",N),b.current?.focus()}}},[t]),v.useEffect(()=>{if(t)return document.body.style.overflow="hidden",()=>{document.body.style.overflow=""}},[t]),!t)return null;const _=f?.zIndex??50,x=(E,S)=>{if(!E||E.length===0)return null;const w=window.G7Core;if(!w)return null;const N=w.getDynamicRenderer?.();return N?E.map((C,k)=>h.jsx(N,{componentDef:C,dataContext:g||{},translationContext:m,registry:w.getComponentRegistry(),bindingEngine:w.getDataBindingEngine(),translationEngine:w.getTranslationEngine(),actionDispatcher:w.getActionDispatcher(),isRootRenderer:!1},`${S}-${k}`)):null};return h.jsxs(I,{className:"modal-wrapper",style:{zIndex:_},onClick:p?e:void 0,children:[h.jsx(I,{className:"modal-backdrop","aria-hidden":"true"}),h.jsxs(I,{ref:y,className:`modal-container ${u}`,style:{width:d,...f?{...f,zIndex:void 0}:{}},onClick:E=>E.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":n?"modal-title":void 0,children:[(n||r||s||a)&&h.jsxs(I,{className:"modal-header-row",children:[h.jsxs(I,{className:"flex-center gap-sm",children:[x(r,"title-prefix"),n&&h.jsx(I,{id:"modal-title",className:"modal-title",children:n}),x(s,"title-suffix")]}),a&&h.jsx(oe,{onClick:e,className:"modal-close-btn","aria-label":Xb("common.close_modal"),children:h.jsx("svg",{className:"icon-lg",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),h.jsx(I,{className:"modal-body-content",children:c})]})]})},Kb=({label:t,items:e,onItemClick:n,position:r="bottom-left",variant:s="default",className:o="",style:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(!1),[u,f]=v.useState(-1),g=v.useRef(null),m=v.useRef(null),[p,y]=v.useState(r);v.useEffect(()=>{const x=E=>{g.current&&!g.current.contains(E.target)&&(d(!1),f(-1))};if(c)return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[c]),v.useEffect(()=>{const x=E=>{if(!c)return;const S=e.map((w,N)=>w.disabled?-1:N).filter(w=>w!==-1);switch(E.key){case"ArrowDown":E.preventDefault(),f(w=>{const C=(S.indexOf(w)+1)%S.length;return S[C]});break;case"ArrowUp":E.preventDefault(),f(w=>{const N=S.indexOf(w),C=N<=0?S.length-1:N-1;return S[C]});break;case"Enter":E.preventDefault(),u>=0&&!e[u].disabled&&b(e[u]);break;case"Escape":E.preventDefault(),d(!1),f(-1);break}};if(c)return document.addEventListener("keydown",x),()=>document.removeEventListener("keydown",x)},[c,u,e]),v.useEffect(()=>{if(c&&m.current&&g.current){const x=m.current.getBoundingClientRect(),E=g.current.getBoundingClientRect(),S=window.innerWidth,w=window.innerHeight,N=x.width||224,C=x.height||200;let k=r;r.includes("left")?E.left+N>S&&(k=k.replace("left","right")):E.right-N<0&&(k=k.replace("right","left")),r.includes("top")?E.top-C<0&&(k=k.replace("top","bottom")):E.bottom+C>w&&(k=k.replace("bottom","top")),y(k)}},[c,r]);const b=x=>{x.disabled||(x.onClick?.(),n?.(x.value,x),d(!1),f(-1))},_={"bottom-left":"dropdown-menu-bottom-left","bottom-right":"dropdown-menu-bottom-right","top-left":"dropdown-menu-top-left","top-right":"dropdown-menu-top-right"};return h.jsxs(I,{ref:g,className:`dropdown ${o}`,style:i,id:a,...l,children:[h.jsxs(oe,{onClick:()=>d(!c),className:s==="text"?"dropdown-trigger-text":"dropdown-trigger","aria-haspopup":"true","aria-expanded":c,children:[t,s!=="text"&&h.jsx("svg",{className:`inline-block ml-2 w-4 h-4 transition-transform ${c?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),c&&h.jsx(I,{ref:m,className:`dropdown-menu ${_[p]}`,role:"menu","aria-orientation":"vertical",children:h.jsx(I,{className:"py-1",children:e.map((x,E)=>h.jsx(I,{className:`${x.disabled?"dropdown-item-disabled":"dropdown-item"} ${u===E?"dropdown-item-focused":""}`,onClick:()=>b(x),onMouseEnter:()=>!x.disabled&&f(E),role:"menuitem","aria-disabled":x.disabled,tabIndex:x.disabled?-1:0,children:x.label},x.value))})})]})},qb=({imageUrl:t,imageAlt:e="",title:n,subtitle:r,description:s,price:o,originalPrice:i,discountRate:a,currency:l="₩",buttonText:c="자세히 보기",onButtonClick:d,className:u="",style:f,id:g,editorAttrs:m})=>{const p=a&&a>0,y=(o??0).toLocaleString(),b=i?.toLocaleString();return h.jsxs(I,{className:`rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-xl transition-shadow border border-gray-200 dark:border-gray-700 overflow-hidden ${u}`,style:f,id:g,...m,children:[t&&h.jsxs(I,{className:"relative w-full",children:[h.jsx(kn,{src:t,alt:e||n,className:"w-full h-64 object-cover"}),p&&h.jsx(I,{className:"absolute top-2 right-2 bg-red-500 text-white px-2 py-1 rounded-md",children:h.jsxs(Q,{className:"text-sm font-bold",children:["-",a,"%"]})})]}),h.jsxs(I,{className:"p-4",children:[r&&h.jsx($r,{className:"text-xs font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider mb-2",children:r}),h.jsx(er,{className:"text-xl font-bold text-gray-900 dark:text-white mb-2 line-clamp-2",children:n}),s&&h.jsx(yt,{className:"text-sm text-gray-600 dark:text-gray-400 mb-4 line-clamp-3",children:s}),h.jsxs(I,{className:"flex items-center gap-2 mb-4",children:[h.jsxs(Q,{className:"text-2xl font-bold text-gray-900 dark:text-white",children:[l,y]}),i&&h.jsxs(Q,{className:"text-sm text-gray-400 dark:text-gray-500 line-through",children:[l,b]})]}),c&&h.jsx(oe,{onClick:d,className:"w-full bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white font-medium py-2 px-4 rounded-lg transition-colors",children:c})]})]})},Zb=({value:t,label:e,change:n,changeLabel:r="전월 대비",iconName:s,trend:o="neutral",className:i="",style:a,id:l,editorAttrs:c})=>{const d=o==="up",u=o==="down",f=n!==void 0?Math.abs(n):0,g=d?"stats-trend-up":u?"stats-trend-down":"stats-trend-neutral",m=d?de.ArrowUp:u?de.ArrowDown:void 0;return h.jsxs(I,{className:`stat-card ${i}`,style:a,id:l,...c,children:[h.jsxs(I,{className:"flex-between mb-md",children:[s&&h.jsx(I,{className:"stats-icon stats-icon-blue",children:h.jsx(re,{name:s,className:"icon-lg"})}),n!==void 0&&h.jsxs(I,{className:`stats-change ${g}`,children:[m&&h.jsx(re,{name:m,className:"icon-sm"}),h.jsxs(Q,{className:"text-sm font-semibold",children:[f,"%"]})]})]}),h.jsx($r,{className:"stats-value-lg",children:typeof t=="number"?t.toLocaleString():t}),h.jsx(yt,{className:"stats-label",children:e}),n!==void 0&&r&&h.jsx(I,{className:"stats-sublabel",children:h.jsx(Q,{children:r})})]})},eu=()=>window.G7Core,tu=(t,e)=>eu()?.t?.(t,e)??t,Qb=({name:t="search",placeholder:e,value:n,onChange:r,onSubmit:s,showButton:o=!1,suggestions:i=[],onSuggestionClick:a,showSuggestions:l=!1,className:c="",style:d,id:u,editorAttrs:f})=>{const g=e??tu("common.search_placeholder"),[m,p]=v.useState(""),[y,b]=v.useState(!1),_=v.useRef(null),x=v.useRef(""),E=n!==void 0?n:m,S=l&&y&&i.length>0&&E.length>0,w="w-64 pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm",N=eu()?.style?.mergeClasses?.(w,c)??`${w} ${c}`;v.useEffect(()=>{n!==void 0&&(x.current=n)},[n]);const C=T=>{const L=T.target.value;p(L),r?.(T),x.current!==""&&L===""&&!T.nativeEvent.inputType&&setTimeout(()=>{const P=_.current?.querySelector("form");P&&P.requestSubmit()},0),x.current=L},k=T=>{p(T.text);const L={target:{value:T.text,name:t}};r?.(L),a?.(T),b(!1)},M=T=>{T.preventDefault(),s?.(T)};return h.jsxs(I,{ref:_,className:"relative",style:d,id:u,...f,children:[h.jsx(Va,{onSubmit:M,className:"relative",children:h.jsxs(I,{className:`relative flex items-center ${o?"gap-2":""}`,children:[h.jsxs(I,{className:"relative flex-1",children:[h.jsx(I,{className:"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",children:h.jsx(re,{name:de.Search,className:"w-5 h-5 text-gray-400 dark:text-gray-500"})}),h.jsx(wt,{type:"search",name:t,value:E,onChange:C,onFocus:()=>b(!0),onBlur:()=>setTimeout(()=>b(!1),200),placeholder:g,className:N})]}),o&&h.jsx(oe,{type:"submit",className:"px-4 py-2 bg-gray-800 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-500 transition-colors text-sm font-medium cursor-pointer h-[42px]",children:tu("common.search")})]})}),S&&h.jsx(I,{className:"absolute z-10 w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-64 overflow-y-auto",children:i.map(T=>h.jsx(I,{className:"px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 transition-colors",onClick:()=>k(T),children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:de.Search,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:T.text})]})},T.id))})]})},rl=(t,e)=>window.G7Core?.t?.(t,e)??t,Jb=({title:t,filters:e,onChange:n,onReset:r,showResetButton:s=!0,className:o="",style:i,id:a,editorAttrs:l})=>{const c=t??rl("common.filter"),d=(f,g)=>{n?.(f,g)},u=(f,g,m)=>{const p=e.find(b=>b.id===f);if(!p)return;const y=Array.isArray(p.value)?p.value:[];if(typeof g=="string"){const b=y.filter(x=>typeof x=="string"),_=m?[...b,g]:b.filter(x=>x!==g);n?.(f,_)}else{const b=y.filter(x=>typeof x=="number"),_=m?[...b,g]:b.filter(x=>x!==g);n?.(f,_)}};return h.jsxs(I,{className:`rounded-lg bg-white dark:bg-gray-800 shadow-md border border-gray-200 dark:border-gray-700 p-4 ${o}`,style:i,id:a,...l,children:[h.jsxs(I,{className:"flex items-center justify-between mb-4 pb-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(Q,{className:"text-lg font-semibold text-gray-900 dark:text-white",children:c}),s&&r&&h.jsx(oe,{onClick:r,className:"text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium",children:rl("common.reset")})]}),h.jsx(I,{className:"space-y-4",children:e.map(f=>h.jsxs(I,{className:"space-y-2",children:[h.jsx(on,{className:"block text-sm font-medium text-gray-700 dark:text-gray-300",children:f.label}),f.type==="select"&&h.jsxs(Vr,{value:f.value,onChange:g=>d(f.id,g.target.value),className:"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-blue-500 dark:focus:border-blue-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white",children:[h.jsx("option",{value:"",children:rl("common.select_placeholder")}),f.options.map(g=>h.jsx("option",{value:g.value,children:g.label},g.id))]}),f.type==="checkbox"&&h.jsx(I,{className:"space-y-2",children:f.options.map(g=>{const m=Array.isArray(f.value)?f.value.some(p=>p===g.value):!1;return h.jsxs(on,{className:"flex items-center gap-2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 p-2 rounded-md transition-colors",children:[h.jsx(Cn,{checked:m,onChange:p=>u(f.id,g.value,p.target.checked),className:"w-4 h-4 text-blue-600 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:g.label})]},g.id)})})]},f.id))})]})},Rb=({tabs:t,activeTabId:e,onTabChange:n,variant:r="default",className:s="",style:o,mobileBreakpoint:i=768,id:a,editorAttrs:l})=>{const d=window.G7Core?.useResponsive,u=d?.(),f=u?u.width<i:typeof window<"u"&&window.innerWidth<i,g=v.useRef([]),m=w=>{!w.disabled&&w.id!==e&&n?.(w.id)},p=(w,N)=>{const C=w.key==="ArrowRight"?1:w.key==="ArrowLeft"?-1:0;let k=null;if(C!==0)for(let M=1;M<=t.length;M+=1){const T=(N+C*M+t.length*M)%t.length;if(!t[T]?.disabled){k=T;break}}else if(w.key==="Home")k=t.findIndex(M=>!M.disabled);else if(w.key==="End"){for(let M=t.length-1;M>=0;M-=1)if(!t[M]?.disabled){k=M;break}}k===null||k<0||(w.preventDefault(),g.current[k]?.focus(),m(t[k]))},y=w=>{const N=w.target.value,C=Number(N),k=typeof N=="string"&&N!==""&&!Number.isNaN(C)?C:N,M=t.find(T=>String(T.id)===String(k));M&&m(M)};if(f)return h.jsx(I,{className:s,style:o,id:a,...l,children:h.jsx(Vr,{value:e!==void 0?String(e):"",onChange:y,options:t.map(w=>({value:String(w.id),label:w.badge!==void 0?`${w.label} (${w.badge})`:w.label,disabled:w.disabled})),className:"w-full"})});const b=w=>{const N=w.id===e;if(w.disabled)return"tab-btn-base tab-btn-disabled";switch(r){case"pills":return N?"tab-btn-base tab-btn-pills-active":"tab-btn-base tab-btn-pills";case"underline":return N?"tab-btn-base tab-btn-underline-active":"tab-btn-base tab-btn-underline";case"default":default:return N?"tab-btn-base tab-btn-default-active":"tab-btn-base tab-btn-default"}},_=r==="underline"?"tab-nav-underline":"tab-nav-default",x=l?.["data-editor-path"],E=t.findIndex(w=>w.id===e),S=E>=0?E:t.findIndex(w=>!w.disabled);return h.jsx(Po,{className:`${_} ${s}`,style:o,role:"tablist","aria-orientation":"horizontal",id:a,...l,children:t.map((w,N)=>h.jsxs(oe,{ref:C=>{g.current[N]=C},type:"button",role:"tab","aria-selected":w.id===e,tabIndex:N===S?0:-1,onClick:()=>m(w),onKeyDown:C=>p(C,N),disabled:w.disabled,className:b(w),...x?{"data-editor-item-path":`${x}.props.tabs.${N}`}:{},children:[w.iconName&&h.jsx(re,{name:w.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:w.label}),w.badge!==void 0&&h.jsx(I,{className:"flex items-center justify-center min-w-[20px] h-5 px-1.5 bg-red-500 dark:bg-red-600 text-white dark:text-white text-xs font-bold rounded-full",children:h.jsx(Q,{children:w.badge})})]},w.id))})},sl=window.G7Core?.createLogger?.("Handler:ScrollToSection")??{log:(...t)=>console.log("[Handler:ScrollToSection]",...t),warn:(...t)=>console.warn("[Handler:ScrollToSection]",...t),error:(...t)=>console.error("[Handler:ScrollToSection]",...t)};async function nu(t,e){const n=t.params||{},{targetId:r,offset:s=120,delay:o=100,scrollContainerId:i}=n;if(!r){sl.warn("targetId is required");return}const a=await ey(r,o);if(!a){sl.warn(`Element not found: ${r}`);return}const l=ty(a,i),c=ny(a,l,s);ry(l,c)}async function ey(t,e){await new Promise(s=>setTimeout(s,e));const n=10,r=50;for(let s=0;s<n;s++){const o=document.getElementById(t);if(o)return await new Promise(i=>setTimeout(i,50)),o;await new Promise(i=>setTimeout(i,r))}return null}function ty(t,e){if(e){const o=document.getElementById(e);if(o&&ru(o))return o;sl.warn(`Container '${e}' ${o?"has no scroll":"not found"}, trying auto-detect`)}let n=t.parentElement,r=0;const s=10;for(;n&&n!==document.body&&r<s;){if(ru(n))return n;n=n.parentElement,r++}return window}function ru(t){const e=window.getComputedStyle(t);return(e.overflowY==="auto"||e.overflowY==="scroll")&&t.scrollHeight>t.clientHeight}function ny(t,e,n){if(e===window||!("getBoundingClientRect"in e))return t.getBoundingClientRect().top+window.pageYOffset-n;{const r=e.getBoundingClientRect(),s=t.getBoundingClientRect();return e.scrollTop+(s.top-r.top)-n}}function ry(t,e){t===window||!("scrollTo"in t)?window.scrollTo({top:e,behavior:"smooth"}):t.scrollTo({top:e,behavior:"smooth"})}window.G7Core?.createLogger?.("Comp:TabNavigation");const sy=({tabs:t,activeTabId:e,className:n="bg-white dark:bg-gray-800 flex gap-2 border-b border-gray-200 dark:border-gray-700 px-6",style:r,activeClassName:s="tab-btn-base tab-btn-default-active",inactiveClassName:o="tab-btn-base tab-btn-default",sectionIdPrefix:i="tab_content_",scrollOffset:a=120,scrollDelay:l=100,onTabChange:c,enableScrollSpy:d=!1,scrollSpyOffset:u=80,scrollContainerId:f,id:g,editorAttrs:m})=>{const[p,y]=v.useState(e??(t.length>0?t[0].id:"")),_=window.G7Core?.useResponsive,x=_?.(),E=x?x.width<768:typeof window<"u"&&window.innerWidth<768,S=v.useRef(!1),w=v.useRef(null),N=v.useRef([]),C=P=>{if(d&&(S.current=!0,w.current&&clearTimeout(w.current)),P.onClick){if(P.onClick(P.id),!d)return}else if(c){if(c(P.id),!d)return}else y(P.id);const F=P.scrollOffset??a,D=P.scrollDelay??l;nu({params:{targetId:`${i}${P.id}`,offset:F,delay:D,scrollContainerId:f}}),d&&(w.current=setTimeout(()=>{S.current=!1},D+500))},k=(P,F)=>{const D=P.key==="ArrowRight"?1:P.key==="ArrowLeft"?-1:0;let A=null;if(D!==0)for(let O=1;O<=t.length;O+=1){const V=(F+D*O+t.length*O)%t.length;if(!t[V]?.disabled){A=V;break}}else if(P.key==="Home")A=t.findIndex(O=>!O.disabled);else if(P.key==="End"){for(let O=t.length-1;O>=0;O-=1)if(!t[O]?.disabled){A=O;break}}A===null||A<0||(P.preventDefault(),N.current[A]?.focus(),C(t[A]))};v.useEffect(()=>{if(!d)return;const P=f?document.getElementById(f):null,F={root:P??null,rootMargin:`-${u}px 0px -30% 0px`,threshold:[0,.1,.2,.3,.4,.5,.6,.7,.8,.9,1]},D=new Map,A=()=>{const W=P,j=W?W.scrollTop:window.pageYOffset||document.documentElement.scrollTop,U=W?W.scrollHeight:document.documentElement.scrollHeight,B=W?W.clientHeight:document.documentElement.clientHeight;return j+B>=U-10},O=W=>{if(S.current)return;W.forEach(X=>{const Y=X.target.id;X.isIntersecting?D.set(Y,X.intersectionRatio):D.delete(Y)});let j=0,U="";D.forEach((X,Y)=>{X>j&&(j=X,U=Y)});const B=t[t.length-1],G=`${i}${B?.id}`,z=D.has(G);if(A()&&z&&B){y(B.id);return}if(U){const X=U.replace(i,""),Y=Number(X),Z=isNaN(Y)?X:Y;t.some(te=>te.id===Z)&&y(Z)}},V=()=>{},H=new IntersectionObserver(O,F);return t.forEach(W=>{const j=document.getElementById(`${i}${W.id}`);j&&H.observe(j)}),window.addEventListener("scroll",V,{passive:!0}),()=>{H.disconnect(),window.removeEventListener("scroll",V),w.current&&clearTimeout(w.current)}},[d,t,i,u,f]);const M=P=>{const F=P.target.value,D=Number(F),A=isNaN(D)?F:D,O=t.find(V=>V.id===A);O&&C(O)};if(E)return h.jsx(I,{className:"bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 py-3",id:g,...m,children:h.jsx(Vr,{value:String(p),onChange:M,options:t.map(P=>({value:String(P.id),label:P.label,disabled:P.disabled})),className:"w-full"})});const T=t.findIndex(P=>P.id===p),L=T>=0?T:t.findIndex(P=>!P.disabled);return h.jsx(I,{className:n,style:r,role:"tablist","aria-orientation":"horizontal",id:g,...m,children:t.map((P,F)=>h.jsxs(oe,{ref:D=>{N.current[F]=D},type:"button",role:"tab","aria-selected":p===P.id,tabIndex:F===L?0:-1,className:p===P.id?s:o,disabled:P.disabled,onClick:()=>C(P),onKeyDown:D=>k(D,F),children:[P.iconName&&h.jsx(re,{name:P.iconName,size:"sm"}),h.jsx(Q,{children:P.label})]},P.id))})},oy=()=>window.G7Core,iy=({iconName:t,label:e,onClick:n,variant:r="primary",size:s="md",disabled:o=!1,className:i="",style:a,id:l,editorAttrs:c})=>{const d={sm:e?"px-3 py-1.5 text-sm":"w-8 h-8",md:e?"px-4 py-2 text-base":"w-10 h-10",lg:e?"px-5 py-2.5 text-lg":"w-12 h-12"},u={sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6"},f={primary:"bg-blue-600 hover:bg-blue-700 text-white border-transparent",secondary:"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600",danger:"bg-red-600 hover:bg-red-700 text-white border-transparent",ghost:"bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-transparent",outline:"bg-transparent hover:bg-gray-50 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600"},g="inline-flex items-center justify-center gap-2 font-medium rounded-lg border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-blue-400",m=o?"opacity-50 cursor-not-allowed":"",p=`${g} ${d[s]} ${f[r]} ${m}`,y=oy()?.style?.mergeClasses?.(p,i)??`${p} ${i}`;return h.jsxs(oe,{onClick:n,disabled:o,className:y,style:a,id:l,...c,children:[h.jsx(re,{name:t,className:u[s]}),e]})},ay=(t,e)=>window.G7Core?.t?.(t,e)??t,su=({isOpen:t,onClose:e,title:n,content:r,actions:s=[],width:o="500px",closeOnOverlay:i=!0,showCloseButton:a=!0,className:l="",style:c})=>{const d=()=>{i&&e()},u={primary:"dialog-btn-primary",secondary:"dialog-btn-secondary",danger:"dialog-btn-danger",ghost:"dialog-btn-ghost"};return h.jsxs(Bo,{isOpen:t,onClose:d,width:o,className:l,style:c,children:[h.jsxs(I,{className:"dialog-header",children:[h.jsx(er,{className:"dialog-title",children:n}),a&&h.jsx(oe,{onClick:e,className:"dialog-close","aria-label":ay("common.close_dialog"),children:h.jsx(re,{name:de.Times,className:"icon-md"})})]}),h.jsx(I,{className:"dialog-body",children:r}),s.length>0&&h.jsx(I,{className:"dialog-footer",children:s.map((f,g)=>h.jsxs(oe,{onClick:f.onClick,disabled:f.disabled,className:u[f.variant||"primary"],children:[f.iconName&&h.jsx(re,{name:f.iconName,className:"icon-sm mr-2 inline-block"}),f.label]},g))})]})},ol=(t,e)=>window.G7Core?.t?.(t,e)??t,ou=({isOpen:t,onClose:e,title:n,message:r,confirmText:s,cancelText:o,onConfirm:i,onCancel:a,confirmButtonVariant:l="primary",cancelButtonVariant:c="secondary",width:d="500px",closeOnOverlay:u=!0,isLoading:f=!1,loadingText:g})=>{const m=s??ol("common.confirm"),p=o??ol("common.cancel"),y=g??ol("common.loading"),b=()=>{i()},_=()=>{f||(a?.(),e())},x=()=>{f||e()},E={primary:"bg-blue-600 text-white hover:bg-blue-700",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600",danger:"bg-red-600 text-white hover:bg-red-700",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"};return h.jsxs(Bo,{isOpen:t,onClose:x,title:n,width:d,children:[h.jsx(I,{className:"py-2",children:typeof r=="string"?h.jsx(yt,{className:"text-gray-700 dark:text-gray-300",children:r}):r}),h.jsxs(I,{className:"flex justify-end gap-3 mt-6",children:[h.jsx(oe,{onClick:_,disabled:f,className:"px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",children:h.jsx(Q,{children:p})}),h.jsxs(oe,{onClick:b,disabled:f,className:`flex items-center gap-2 px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed ${E[l]}`,children:[f&&h.jsx(re,{name:de.Spinner,className:"w-4 h-4 animate-spin"}),h.jsx(Q,{children:f?y:m})]})]})]})},ly=(t,e)=>window.G7Core?.t?.(t,e)??t,cy=({isOpen:t,onClose:e,title:n,message:r,confirmText:s,onConfirm:o,confirmButtonVariant:i="primary",width:a="500px",closeOnOverlay:l=!0})=>{const u=[{label:s??ly("common.confirm"),onClick:()=>{o?.(),e()},variant:i}],f=typeof r=="string"?h.jsx(yt,{className:"text-gray-700 dark:text-gray-300",children:r}):r;return h.jsx(su,{isOpen:t,onClose:e,title:n,content:f,actions:u,width:a,closeOnOverlay:l})},dy=(t,e)=>window.G7Core?.t?.(t,e)??t,uy={success:de.CheckCircle,error:de.XCircle,warning:de.ExclamationTriangle,info:de.InfoCircle},iu={success:{bg:"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800",text:"text-green-800 dark:text-green-300"},error:{bg:"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",text:"text-red-800 dark:text-red-300"},warning:{bg:"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",text:"text-yellow-800 dark:text-yellow-300"},info:{bg:"bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",text:"text-blue-800 dark:text-blue-300"}},fy={"top-left":"top-4 left-4","top-center":"top-4 left-1/2 -translate-x-1/2","top-right":"top-4 right-4","bottom-left":"bottom-4 left-4","bottom-center":"bottom-4 left-1/2 -translate-x-1/2","bottom-right":"bottom-4 right-4"},hy=({toasts:t,position:e="bottom-center",duration:n=3e3,onRemove:r})=>{const[s,o]=v.useState(new Map),i=v.useCallback(a=>{o(l=>{const c=new Map(l);return c.set(a,!1),c}),setTimeout(()=>{if(r)r(a);else{const l=window.G7Core;l?.state?.update&&l.state.update(c=>({toasts:(Array.isArray(c.toasts)?c.toasts:[]).filter(u=>u.id!==a)}))}},300)},[r]);return v.useEffect(()=>{!t||t.length===0||t.forEach(a=>{if(s.has(a.id))return;o(c=>{const d=new Map(c);return d.set(a.id,!0),d});const l=a.duration||n;l>0&&setTimeout(()=>{i(a.id)},l)})},[t,n,i,s]),!t||t.length===0?null:h.jsx(I,{className:`fixed z-[9999] flex flex-col gap-2 ${fy[e]}`,"aria-live":"polite","aria-atomic":"true",children:t.map(a=>h.jsx(gy,{toast:a,position:e,isVisible:s.get(a.id)??!0,onClose:()=>i(a.id)},a.id))})},gy=({toast:t,position:e,isVisible:n,onClose:r})=>{const s=iu[t.type]||iu.info,o=t.icon?t.icon:uy[t.type],i=e.startsWith("bottom")?"translate-y-2":"-translate-y-2";return h.jsxs(I,{className:` flex items-center gap-3 p-4 rounded-lg border shadow-lg min-w-[300px] max-w-md transition-all duration-300 ${s.bg} ${n?"opacity-100 translate-y-0":`opacity-0 ${i}`} - `,role:"alert",children:[h.jsx(oe,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(Q,{className:`flex-1 text-sm font-medium ${s.text}`,children:e.message}),h.jsx(ae,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":Iy("common.close_notification"),children:h.jsx(oe,{name:de.Times,className:"w-4 h-4"})})]})},bu=({items:e,separator:t=h.jsx(oe,{name:de.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...e]:e,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Yo,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:!1}),h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t}),h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(yu,{item:f,isLast:m}),!m&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:t})]})]},g)})})})},yu=({item:e,isLast:t})=>t||!e.href&&!e.onClick?h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white","aria-current":t?"page":void 0,children:e.label}):e.onClick?h.jsx(Yr,{href:"#",onClick:n=>{n.preventDefault(),e.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}):h.jsx(Yr,{href:e.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:e.label}),zy=({title:e,description:t,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(_n,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(oe,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(ar,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:e}),t&&h.jsx(vt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:t}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(ae,{onClick:d.onClick,className:` + `,role:"alert",children:[h.jsx(re,{name:o,className:`w-5 h-5 flex-shrink-0 ${s.text}`}),h.jsx(Q,{className:`flex-1 text-sm font-medium ${s.text}`,children:t.message}),h.jsx(oe,{onClick:r,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${s.text} focus:outline-none`,"aria-label":dy("common.close_notification"),children:h.jsx(re,{name:de.Times,className:"w-4 h-4"})})]})},au=({items:t,separator:e=h.jsx(re,{name:de.ChevronRight,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),showHome:n=!1,homeHref:r="/",maxItems:s,className:o="",id:i,editorAttrs:a})=>{let c=n?[{label:"Home",href:r},...t]:t,d=c,u=!1;if(s&&c.length>s){u=!0;const f=c.slice(0,1),g=c.slice(-(s-2));d=[...f,...g]}return h.jsx(Po,{className:`flex items-center space-x-2 text-sm ${o}`,"aria-label":"Breadcrumb",id:i,...a,children:h.jsx(I,{className:"flex items-center space-x-2",children:d.map((f,g)=>{const m=g===d.length-1,p=g===0;return h.jsxs(v.Fragment,{children:[u&&p&&h.jsxs(h.Fragment,{children:[h.jsx(lu,{item:f,isLast:!1}),h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e}),h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"..."}),d.length>1&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]}),(!u||!p)&&h.jsxs(h.Fragment,{children:[h.jsx(lu,{item:f,isLast:m}),!m&&h.jsx(Q,{className:"text-gray-400 dark:text-gray-500","aria-hidden":"true",children:e})]})]},g)})})})},lu=({item:t,isLast:e})=>e||!t.href&&!t.onClick?h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white","aria-current":e?"page":void 0,children:t.label}):t.onClick?h.jsx(Fr,{href:"#",onClick:n=>{n.preventDefault(),t.onClick?.()},className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}):h.jsx(Fr,{href:t.href,className:"text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors",children:t.label}),py=({title:t,description:e,iconName:n,illustrationSrc:r,illustrationAlt:s="Empty state illustration",actions:o=[],className:i="",id:a,editorAttrs:l})=>{const c={primary:"bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 focus:ring-gray-500",danger:"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:ring-gray-500"};return h.jsxs(I,{className:`flex flex-col items-center justify-center p-8 text-center ${i}`,role:"status","aria-live":"polite",id:a,...l,children:[r?h.jsx(kn,{src:r,alt:s,className:"w-48 h-48 mb-6 object-contain"}):n?h.jsx(I,{className:"mb-6 p-4 rounded-full bg-gray-100 dark:bg-gray-700",children:h.jsx(re,{name:n,className:"w-12 h-12 text-gray-400 dark:text-gray-500"})}):null,h.jsx(er,{className:"text-xl font-semibold text-gray-900 dark:text-white mb-2",children:t}),e&&h.jsx(yt,{className:"text-gray-600 dark:text-gray-400 mb-6 max-w-md",children:e}),o.length>0&&h.jsx(I,{className:"flex flex-wrap gap-3 justify-center",children:o.map((d,u)=>h.jsxs(oe,{onClick:d.onClick,className:` px-4 py-2 rounded-md font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 ${c[d.variant||"primary"]} - `,children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},Vy=(e,t)=>window.G7Core?.t?.(e,t)??e,Hy=({id:e,size:t="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:` - ${a[t]} + `,children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4 mr-2 inline-block"}),d.label]},u))})]})},my=(t,e)=>window.G7Core?.t?.(t,e)??t,by=({id:t,size:e="md",color:n="text-blue-600",fullscreen:r=!1,text:s,className:o="",editorAttrs:i})=>{const a={sm:"w-4 h-4 border-2",md:"w-8 h-8 border-2",lg:"w-12 h-12 border-3",xl:"w-16 h-16 border-4"},l={sm:"text-xs",md:"text-sm",lg:"text-base",xl:"text-lg"},c=h.jsxs(I,{className:`flex flex-col items-center justify-center gap-3 ${o}`,children:[h.jsx(I,{className:` + ${a[e]} border-gray-200 dark:border-gray-700 border-t-current rounded-full animate-spin ${n} - `,role:"status","aria-label":Vy("common.loading")}),s&&h.jsx(Q,{className:`${l[t]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:e,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:e,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},es=()=>window.G7Core,gl=(e,t)=>window.G7Core?.t?.(e,t)??e,Uy=({logo:e,logoAlt:t="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||es()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const w=[],E=D=>{for(const P of D)P.url&&w.push(P.url),P.children&&E(P.children)};return E(n),w},[n]),y=v.useCallback(w=>{let E=null;for(const D of p)(w===D||w.startsWith(D+"/"))&&(!E||D.length>E.length)&&(E=D);return E},[p]);v.useEffect(()=>{const w=es();return w?.state?.subscribe?w.state.subscribe(D=>{const P=D?._global?.locale||w?.locale?.current?.()||"ko";f(P);const L=D?._global?.currentPath||window.location.pathname;m(L)}):void 0},[]),v.useEffect(()=>{const w=()=>{m(window.location.pathname)};return window.addEventListener("popstate",w),()=>window.removeEventListener("popstate",w)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(w,E)=>!w||typeof E!="string"?!1:E===w?!0:E.startsWith(w+"/")?y(E)===w:!1,_=(w,E)=>{const D=new Set,P=L=>{for(const $ of L)if($.children&&$.children.length>0){if(P($.children)||b($.url,E))return D.add($.id),!0}else if(b($.url,E))return!0;return!1};return P(w),D};v.useEffect(()=>{if(n.length>0&&g){const w=_(n,g);w.size>0&&d(E=>new Set([...E,...w]))}},[g,n]);const x=w=>b(w,g),M=w=>w.children?w.children.some(E=>x(E.url)||M(E)):!1,C=w=>{d(E=>{const D=new Set(E);return D.has(w)?D.delete(w):D.add(w),D})},k=w=>typeof w=="string"?w:w[u]||w.en||Object.values(w)[0]||"",N=(w,E)=>{es()?.dispatch?(w.preventDefault(),es().navigation?.onComplete(()=>{es().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),es().dispatch({handler:"navigate",params:{path:E}})):window.location.href=E},S=(w,E=0,D=!1)=>{const P=w.children&&w.children.length>0,L=c.has(w.id),$=k(w.name),A=E>0,O=x(w.url),T=M(w),z=A?"admin-sidebar-subitem":"admin-sidebar-item",H="admin-sidebar-item-active",j="admin-sidebar-item-active-parent",G="admin-sidebar-item-default";return h.jsxs(I,{className:A?"relative group":"",children:[A&&h.jsxs(h.Fragment,{children:[!D&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),P&&w.url?h.jsxs(I,{className:` - ${z} - ${O?H:T?j:G} + `,role:"status","aria-label":my("common.loading")}),s&&h.jsx(Q,{className:`${l[e]} ${n} font-medium`,children:s})]});return r?h.jsx(I,{id:t,className:"fixed inset-0 z-50 flex items-center justify-center bg-white dark:bg-gray-900 bg-opacity-90 dark:bg-opacity-90","aria-live":"polite","aria-busy":"true",...i,children:c}):h.jsx(I,{id:t,className:"flex items-center justify-center p-4","aria-live":"polite","aria-busy":"true",...i,children:c})},jr=()=>window.G7Core,il=(t,e)=>window.G7Core?.t?.(t,e)??t,yy=({logo:t,logoAlt:e="Logo",menu:n=[],collapsed:r=!1,onToggleCollapse:s,className:o="",currentLocale:i,id:a,editorAttrs:l})=>{const[c,d]=v.useState(new Set),[u,f]=v.useState(i||jr()?.locale?.current?.()||"ko"),[g,m]=v.useState(window.location.pathname),p=v.useMemo(()=>{const k=[],M=T=>{for(const L of T)L.url&&k.push(L.url),L.children&&M(L.children)};return M(n),k},[n]),y=v.useCallback(k=>{let M=null;for(const T of p)(k===T||k.startsWith(T+"/"))&&(!M||T.length>M.length)&&(M=T);return M},[p]);v.useEffect(()=>{const k=jr();return k?.state?.subscribe?k.state.subscribe(T=>{const L=T?._global?.locale||k?.locale?.current?.()||"ko";f(L);const P=T?._global?.currentPath||window.location.pathname;m(P)}):void 0},[]),v.useEffect(()=>{const k=()=>{m(window.location.pathname)};return window.addEventListener("popstate",k),()=>window.removeEventListener("popstate",k)},[]),v.useEffect(()=>{i&&f(i)},[i]);const b=(k,M)=>!k||typeof M!="string"?!1:M===k?!0:M.startsWith(k+"/")?y(M)===k:!1,_=(k,M)=>{const T=new Set,L=P=>{for(const F of P)if(F.children&&F.children.length>0){if(L(F.children)||b(F.url,M))return T.add(F.id),!0}else if(b(F.url,M))return!0;return!1};return L(k),T};v.useEffect(()=>{if(n.length>0&&g){const k=_(n,g);k.size>0&&d(M=>new Set([...M,...k]))}},[g,n]);const x=k=>b(k,g),E=k=>k.children?k.children.some(M=>x(M.url)||E(M)):!1,S=k=>{d(M=>{const T=new Set(M);return T.has(k)?T.delete(k):T.add(k),T})},w=k=>typeof k=="string"?k:k[u]||k.en||Object.values(k)[0]||"",N=(k,M)=>{jr()?.dispatch?(k.preventDefault(),jr().navigation?.onComplete(()=>{jr().dispatch({handler:"setState",params:{target:"global",sidebarOpen:!1}})}),jr().dispatch({handler:"navigate",params:{path:M}})):window.location.href=M},C=(k,M=0,T=!1)=>{const L=k.children&&k.children.length>0,P=c.has(k.id),F=w(k.name),D=M>0,A=x(k.url),O=E(k),V=D?"admin-sidebar-subitem":"admin-sidebar-item",H="admin-sidebar-item-active",W="admin-sidebar-item-active-parent",j="admin-sidebar-item-default";return h.jsxs(I,{className:D?"relative group":"",children:[D&&h.jsxs(h.Fragment,{children:[!T&&h.jsx(I,{className:"admin-sidebar-tree-line-vertical"}),h.jsx(I,{className:"admin-sidebar-tree-line-curve"})]}),L&&k.url?h.jsxs(I,{className:` + ${V} + ${A?H:O?W:j} ${r?"justify-center":""} - `,children:[h.jsxs(ae,{onClick:W=>N(W,w.url),className:"flex items-center justify-start flex-1 min-w-0",children:[w.icon&&h.jsx(bt,{className:`${w.icon} ${r?"":"mr-3"} w-5 h-5 flex items-center justify-center`}),!r&&h.jsx(Q,{className:"truncate",children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(ae,{onClick:()=>C(w.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":gl(L?"common.collapse":"common.expand"),children:h.jsx(bt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${L?"rotate-180":""}`})})]})]}):P?h.jsxs(ae,{onClick:()=>C(w.id),className:` - ${z} - ${T?j:G} + `,children:[h.jsxs(oe,{onClick:B=>N(B,k.url),className:"flex items-center justify-start flex-1 min-w-0",children:[k.icon&&h.jsx(gt,{className:`${k.icon} ${r?"":"mr-3"} w-5 h-5 flex items-center justify-center`}),!r&&h.jsx(Q,{className:"truncate",children:F})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[k.module_id&&h.jsx(gt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(oe,{onClick:()=>S(k.id),className:"p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600","aria-label":il(P?"common.collapse":"common.expand"),children:h.jsx(gt,{className:`fas fa-chevron-down w-4 h-4 transition-transform ${P?"rotate-180":""}`})})]})]}):L?h.jsxs(oe,{onClick:()=>S(k.id),className:` + ${V} + ${O?W:j} ${r?"justify-center":""} - `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(bt,{className:`fas fa-chevron-right w-4 h-4 text-slate-400 transition-transform duration-200 ${L?"rotate-90":""}`})]})]}):w.url?h.jsxs(ae,{onClick:W=>N(W,w.url),className:` - ${z} - ${O?H:G} + `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[k.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(gt,{className:k.icon})}),!r&&h.jsx(Q,{children:F})]}),!r&&h.jsxs(I,{className:"admin-sidebar-chevron-trail",children:[k.module_id&&h.jsx(gt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"}),h.jsx(gt,{className:`fas fa-chevron-right w-4 h-4 text-slate-400 transition-transform duration-200 ${P?"rotate-90":""}`})]})]}):k.url?h.jsxs(oe,{onClick:B=>N(B,k.url),className:` + ${V} + ${A?H:j} ${r?"justify-center":""} text-left - `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}):h.jsxs(ae,{className:` - ${z} + `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[k.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(gt,{className:k.icon})}),!r&&h.jsx(Q,{children:F})]}),!r&&k.module_id&&h.jsx(gt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}):h.jsxs(oe,{className:` + ${V} admin-sidebar-item-disabled ${r?"justify-center":""} text-left - `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[w.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(bt,{className:w.icon})}),!r&&h.jsx(Q,{children:$})]}),!r&&w.module_id&&h.jsx(bt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),P&&!r&&L&&h.jsx(I,{className:"relative ml-2",children:w.children.map((W,Y)=>S(W,E+1,Y===w.children.length-1))})]},w.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(e||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[e&&!r&&h.jsx(_n,{src:e,alt:t,className:"h-8"}),s&&h.jsx(ae,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":gl(r?"common.expand_sidebar":"common.collapse_sidebar"),children:h.jsx(bt,{className:`${r?"fas fa-chevron-right":"fas fa-chevron-left"} w-5 h-5 text-slate-600 dark:text-slate-400`})})]}),h.jsx(I,{className:"admin-sidebar-menu-list",children:n.map((w,E)=>S(w,0,E===n.length-1))})]})},Vs=(e,t)=>window.G7Core?.t?.(e,t)??e,By=({user:e={name:"",email:""},notifications:t=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=t.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:` + `,children:[h.jsxs(I,{className:"admin-sidebar-item-content",children:[k.icon&&h.jsx(I,{className:"admin-sidebar-icon-box",children:h.jsx(gt,{className:k.icon})}),!r&&h.jsx(Q,{children:F})]}),!r&&k.module_id&&h.jsx(gt,{className:"fas fa-cube admin-sidebar-module-icon",title:"모듈 메뉴"})]}),L&&!r&&P&&h.jsx(I,{className:"relative ml-2",children:k.children.map((B,G)=>C(B,M+1,G===k.children.length-1))})]},k.id)};return h.jsxs(I,{className:o,id:a,...l,children:[(t||s)&&h.jsxs(I,{className:"admin-sidebar-header",children:[t&&!r&&h.jsx(kn,{src:t,alt:e,className:"h-8"}),s&&h.jsx(oe,{onClick:s,className:"admin-sidebar-collapse-btn","aria-label":il(r?"common.expand_sidebar":"common.collapse_sidebar"),children:h.jsx(gt,{className:`${r?"fas fa-chevron-right":"fas fa-chevron-left"} w-5 h-5 text-slate-600 dark:text-slate-400`})})]}),h.jsx(I,{className:"admin-sidebar-menu-list",children:n.map((k,M)=>C(k,0,M===n.length-1))})]})},Os=(t,e)=>window.G7Core?.t?.(t,e)??t,vy=({user:t={name:"",email:""},notifications:e=[],onNotificationClick:n,onProfileClick:r,onLogoutClick:s,className:o=""})=>{const[i,a]=v.useState(!1),[l,c]=v.useState(!1),d=v.useRef(null),u=v.useRef(null),f=e.filter(g=>!g.read).length;return v.useEffect(()=>{const g=m=>{d.current&&!d.current.contains(m.target)&&a(!1),u.current&&!u.current.contains(m.target)&&c(!1)};return document.addEventListener("mousedown",g),()=>document.removeEventListener("mousedown",g)},[]),h.jsxs(I,{className:` bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-6 py-4 flex items-center justify-end gap-4 ${o} - `,children:[h.jsxs(I,{ref:d,className:"relative",children:[h.jsxs(ae,{onClick:()=>a(!i),className:"relative p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors","aria-label":Vs("common.notifications"),children:[h.jsx(oe,{name:de.Bell,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),f>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full",children:f>9?"9+":f})]}),i&&h.jsxs(I,{className:"absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsx(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:Vs("common.notifications")})}),h.jsx(I,{className:"max-h-96 overflow-y-auto",children:t.length===0?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:Vs("common.no_notifications")})}):t.map(g=>h.jsx(ae,{onClick:()=>{g.onClick?.(),n?.(g.id),a(!1)},className:` + `,children:[h.jsxs(I,{ref:d,className:"relative",children:[h.jsxs(oe,{onClick:()=>a(!i),className:"relative p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-full transition-colors","aria-label":Os("common.notifications"),children:[h.jsx(re,{name:de.Bell,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),f>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full",children:f>9?"9+":f})]}),i&&h.jsxs(I,{className:"absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsx(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:Os("common.notifications")})}),h.jsx(I,{className:"max-h-96 overflow-y-auto",children:e.length===0?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:Os("common.no_notifications")})}):e.map(g=>h.jsx(oe,{onClick:()=>{g.onClick?.(),n?.(g.id),a(!1)},className:` w-full px-4 py-3 text-left hover:bg-gray-50 dark:hover:bg-gray-700 border-b border-gray-100 dark:border-gray-700 transition-colors ${g.read?"":"bg-blue-50 dark:bg-blue-900/20"} - `,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(oe,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(ae,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[e.avatar?h.jsx(_n,{src:e.avatar,alt:e.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:e.name}),e.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:e.role})]}),h.jsx(oe,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:e.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:e.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{children:Vs("common.profile_settings")})]}),h.jsxs(ae,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(oe,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:Vs("common.logout")})]})]})]})]})]})},jy=({notifications:e=[],unreadCount:t=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:_="",id:x,editorAttrs:M})=>{const[C,k]=v.useState(!1),N=v.useRef(null),S=v.useRef(null),w=v.useRef(null),E=v.useRef(null),D=v.useRef(new Set),P=v.useRef(f),L=v.useRef(n),$=v.useRef(r),A=v.useRef(e.length),O=v.useRef(-1);v.useEffect(()=>{P.current=f,L.current=n,$.current=r,A.current=e.length}),v.useEffect(()=>{if(!E.current||!w.current||!C)return;O.current=-1;const j=new IntersectionObserver(([G])=>{G.isIntersecting&&($.current||!L.current||O.current!==A.current&&(O.current=A.current,P.current?.()))},{root:w.current,threshold:.1});return j.observe(E.current),()=>j.disconnect()},[C]),v.useEffect(()=>{if(!w.current||!C)return;const j=new IntersectionObserver(U=>{U.forEach(W=>{const Y=W.target.getAttribute("data-notification-id"),V=W.target.getAttribute("data-unread")==="true";Y&&V&&W.isIntersecting&&D.current.add(Y)})},{root:w.current,threshold:.5});return w.current.querySelectorAll("[data-notification-id]").forEach(U=>j.observe(U)),()=>j.disconnect()},[C,e]);const T=t>0?t:e.filter(j=>!j.read).length,z=v.useCallback(()=>{if(C){if(T>0){const j=Array.from(D.current).filter(G=>{const U=e.find(W=>String(W.id)===String(G));return U&&!U.read});j.length>0&&u?.(j)}D.current.clear(),k(!1)}},[C,u,T,e]);v.useEffect(()=>{if(!C)return;const j=G=>{N.current&&!N.current.contains(G.target)&&z()};return document.addEventListener("mousedown",j),()=>document.removeEventListener("mousedown",j)},[C,z]);const H=v.useCallback(()=>{C?z():k(!0)},[C,z]);return v.useLayoutEffect(()=>{const j=S.current;if(!C||!j)return;j.style.transform="";const G=j.getBoundingClientRect(),U=typeof window<"u"?window.innerWidth:0;if(G.width===0&&G.height===0)return;const W=8;let Y=0;G.right>U-W?Y=-(G.right-(U-W)):G.left<W&&(Y=W-G.left),Y!==0&&(j.style.transform=`translateX(${Y}px)`)},[C,b,e.length]),h.jsxs(I,{ref:N,className:`relative ${_}`,id:x,...M,children:[h.jsxs(ae,{onClick:H,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(oe,{name:de.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),T>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 dark:bg-red-600 rounded-full",children:T>99?"99+":T})]}),C&&h.jsxs(I,{ref:S,className:`absolute ${b==="left"?"left-0":"right-0"} mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between gap-2",children:[h.jsxs(I,{className:"flex items-center gap-3 min-w-0",children:[h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:s}),y&&h.jsxs(I,{className:"flex items-center gap-1.5 cursor-pointer select-none",onClick:()=>y(!c),children:[h.jsx(St,{type:"checkbox",checked:c,onChange:j=>{j.stopPropagation(),y(j.target.checked)},onClick:j=>j.stopPropagation(),className:"w-3.5 h-3.5 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 cursor-pointer"}),h.jsx(Q,{className:"text-xs text-gray-600 dark:text-gray-400",children:l})]})]}),h.jsxs(I,{className:"flex items-center gap-3 flex-shrink-0",children:[g&&T>0&&h.jsx(ae,{onClick:()=>{g(),D.current.clear()},className:"text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline cursor-pointer",children:i}),m&&e.length>0&&h.jsx(ae,{onClick:()=>{m()},className:"text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:underline cursor-pointer",children:a})]})]}),h.jsx(I,{ref:w,className:"max-h-96 overflow-y-auto",children:e.length===0&&!r?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:o})}):h.jsxs(h.Fragment,{children:[e.map(j=>{const G=!j.read;return h.jsxs(I,{"data-notification-id":j.id,"data-unread":G?"true":"false",onClick:()=>{j.onClick?.(),d?.(j),k(!1)},className:`group flex items-start gap-3 px-4 py-3 text-left border-b border-gray-100 dark:border-gray-700 transition-colors cursor-pointer ${G?"bg-blue-50/70 dark:bg-blue-900/20 hover:bg-blue-50 dark:hover:bg-blue-900/30":"bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 opacity-70"}`,children:[j.iconName&&h.jsx(oe,{name:j.iconName,className:`w-5 h-5 mt-0.5 flex-shrink-0 ${G?"text-blue-600 dark:text-blue-400":"text-gray-400 dark:text-gray-500"}`}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`text-sm truncate ${G?"font-semibold text-gray-900 dark:text-white":"font-normal text-gray-600 dark:text-gray-400"}`,children:j.title}),h.jsx(I,{className:`text-sm mt-1 line-clamp-2 ${G?"text-gray-700 dark:text-gray-300":"text-gray-500 dark:text-gray-500"}`,children:j.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1 font-mono",children:j.time})]}),h.jsxs(I,{className:"flex flex-col items-center gap-2 flex-shrink-0",children:[G&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full mt-1.5"}),p&&h.jsx(ae,{onClick:U=>{U.stopPropagation(),p(j)},className:"p-1 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity","aria-label":"Delete notification",children:h.jsx(oe,{name:de.Trash,className:"w-4 h-4"})})]})]},j.id)}),n&&h.jsx(I,{ref:E,className:"px-4 py-3 text-center",children:r?h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."}):h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500"})}),r&&!n&&e.length>0&&h.jsx(I,{className:"px-4 py-2 text-center",children:h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."})})]})})]})]})},vu=window.G7Core?.createLogger?.("Comp:UserProfile")??{log:(...e)=>console.log("[Comp:UserProfile]",...e),warn:(...e)=>console.warn("[Comp:UserProfile]",...e),error:(...e)=>console.error("[Comp:UserProfile]",...e)},Wy=({user:e={name:"",email:""},profileText:t="Profile Settings",logoutText:n="Logout",onProfileClick:r,onLogoutClick:s,className:o="",logoutEndpoint:i="/api/admin/auth/logout",redirectPath:a="/admin/login",showChevron:l=!0,dropdownDirection:c="up",dropdownOnly:d=!1,id:u,editorAttrs:f})=>{const[g,m]=v.useState(!1),[p,y]=v.useState(!1),b=v.useRef(null);v.useEffect(()=>{const C=k=>{b.current&&!b.current.contains(k.target)&&m(!1)};return document.addEventListener("mousedown",C),()=>document.removeEventListener("mousedown",C)},[]);const _=()=>{const k=`; ${document.cookie}`.split("; XSRF-TOKEN=");return k.length===2?decodeURIComponent(k.pop()?.split(";").shift()||""):null},x=()=>localStorage.getItem("auth_token"),M=async()=>{if(!p)try{if(y(!0),m(!1),s){s();return}const C=_(),k=x(),N=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json",...C&&{"X-XSRF-TOKEN":C},...k&&{Authorization:`Bearer ${k}`}},credentials:"include"});N.ok?(localStorage.removeItem("auth_token"),window.location.href=a):(vu.error("로그아웃 실패:",N.statusText),localStorage.removeItem("auth_token"),window.location.href=a)}catch(C){vu.error("로그아웃 오류:",C),localStorage.removeItem("auth_token"),window.location.href=a}finally{y(!1)}};return h.jsxs(I,{ref:b,className:d?`absolute inset-0 ${o}`:`relative ${o}`,id:u,...f,children:[h.jsx(ae,{onClick:()=>m(!g),className:d?"w-full h-full opacity-0 cursor-pointer":"flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors w-full",children:!d&&h.jsxs(h.Fragment,{children:[e.avatar?h.jsx(_n,{src:e.avatar,alt:e.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"flex-1 text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:e.name}),e.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:e.role})]}),l&&h.jsx(oe,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]})}),g&&h.jsxs(I,{className:`absolute ${c==="down"?"top-full mt-2":"bottom-full mb-2"} ${d?"right-0 w-72":"left-0 w-full"} bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:()=>{m(!1),window.G7Core?.dispatch({handler:"navigate",params:{path:`/admin/users/${e.uuid}`}})},children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:e.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:e.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>{m(!1),r?r():window.G7Core?.dispatch({handler:"navigate",params:{path:`/admin/users/${e.uuid}/edit`}})},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center justify-start gap-3 transition-colors",children:[h.jsx(oe,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:t})]}),h.jsxs(ae,{onClick:M,disabled:p,className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center justify-start gap-3 text-red-600 dark:text-red-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",children:[h.jsx(oe,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:p?"Logging out...":n})]})]})]})]})},Jo=window.G7Core?.createLogger?.("Comp:LanguageSelector")??{log:(...e)=>console.log("[Comp:LanguageSelector]",...e),warn:(...e)=>console.warn("[Comp:LanguageSelector]",...e),error:(...e)=>console.error("[Comp:LanguageSelector]",...e)},xu=e=>{const t=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[e];return t||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[e]||e},Gy=({availableLocales:e=["ko","en"],languageText:t="Language",apiEndpoint:n="/api/admin/users/me/language",onLanguageChange:r,className:s="",inline:o=!1,showCode:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(!1),[u,f]=v.useState(!1),[g,m]=v.useState(()=>localStorage.getItem("g7_locale")||"ko"),p=v.useRef(null);v.useEffect(()=>{if(o)return;const x=M=>{p.current&&!p.current.contains(M.target)&&d(!1)};return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[o]);const y=()=>{const M=`; ${document.cookie}`.split("; XSRF-TOKEN=");return M.length===2?decodeURIComponent(M.pop()?.split(";").shift()||""):null},b=()=>localStorage.getItem("auth_token"),_=async x=>{if(!(u||x===g))try{f(!0);const M=y(),C=b();try{const k=await fetch(n,{method:"PATCH",headers:{"Content-Type":"application/json",Accept:"application/json",...M&&{"X-XSRF-TOKEN":M},...C&&{Authorization:`Bearer ${C}`}},credentials:"include",body:JSON.stringify({language:x})});k.ok||Jo.warn("언어 설정 저장 실패 (UI는 변경됨):",k.statusText)}catch(k){Jo.warn("언어 설정 API 호출 실패 (UI는 변경됨):",k)}m(x),d(!1),r?.(x),window.__templateApp?await window.__templateApp.changeLocale(x):(Jo.warn("TemplateApp not found, reloading page"),window.location.reload())}catch(M){Jo.error("언어 변경 오류:",M)}finally{f(!1)}};return o?h.jsxs(I,{className:`${s}`,id:a,...l,children:[h.jsxs(ae,{onClick:()=>d(!c),disabled:u,className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors",children:[h.jsx(oe,{name:de.Globe,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:t}),h.jsx(Q,{className:"px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded",children:g.toUpperCase()}),h.jsx(oe,{name:de.ChevronDown,className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${c?"rotate-180":""}`})]}),c&&h.jsx(I,{className:"px-4 py-2 flex flex-wrap gap-2",children:e.map(x=>h.jsx(ae,{onClick:()=>_(x),disabled:u,className:` + `,children:h.jsxs(I,{className:"flex items-start gap-3",children:[g.iconName&&h.jsx(re,{name:g.iconName,className:"w-5 h-5 text-gray-600 dark:text-gray-400 mt-0.5"}),h.jsxs(I,{className:"flex-1",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:g.title}),h.jsx(I,{className:"text-gray-600 dark:text-gray-400 text-sm mt-1",children:g.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1",children:g.time})]}),!g.read&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 rounded-full mt-1.5"})]})},g.id))})]})]}),h.jsxs(I,{ref:u,className:"relative",children:[h.jsxs(oe,{onClick:()=>c(!l),className:"flex items-center gap-3 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[t.avatar?h.jsx(kn,{src:t.avatar,alt:t.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"hidden md:block text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:t.name}),t.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:t.role})]}),h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]}),l&&h.jsxs(I,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:t.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:t.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>{r?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors text-gray-900 dark:text-white",children:[h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{children:Os("common.profile_settings")})]}),h.jsxs(oe,{onClick:()=>{s?.(),c(!1)},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 text-red-600 dark:text-red-400 transition-colors",children:[h.jsx(re,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:Os("common.logout")})]})]})]})]})]})},xy=({notifications:t=[],unreadCount:e=0,hasMore:n=!1,loading:r=!1,titleText:s="Notifications",emptyText:o="No notifications",markAllReadText:i="Mark all as read",deleteAllText:a="Delete all",unreadOnlyText:l="Unread only",unreadOnly:c=!1,onNotificationClick:d,onClose:u,onLoadMore:f,onMarkAllRead:g,onDeleteAll:m,onDelete:p,onUnreadOnlyToggle:y,dropdownAlign:b="right",className:_="",id:x,editorAttrs:E})=>{const[S,w]=v.useState(!1),N=v.useRef(null),C=v.useRef(null),k=v.useRef(null),M=v.useRef(null),T=v.useRef(new Set),L=v.useRef(f),P=v.useRef(n),F=v.useRef(r),D=v.useRef(t.length),A=v.useRef(-1);v.useEffect(()=>{L.current=f,P.current=n,F.current=r,D.current=t.length}),v.useEffect(()=>{if(!M.current||!k.current||!S)return;A.current=-1;const W=new IntersectionObserver(([j])=>{j.isIntersecting&&(F.current||!P.current||A.current!==D.current&&(A.current=D.current,L.current?.()))},{root:k.current,threshold:.1});return W.observe(M.current),()=>W.disconnect()},[S]),v.useEffect(()=>{if(!k.current||!S)return;const W=new IntersectionObserver(U=>{U.forEach(B=>{const G=B.target.getAttribute("data-notification-id"),z=B.target.getAttribute("data-unread")==="true";G&&z&&B.isIntersecting&&T.current.add(G)})},{root:k.current,threshold:.5});return k.current.querySelectorAll("[data-notification-id]").forEach(U=>W.observe(U)),()=>W.disconnect()},[S,t]);const O=e>0?e:t.filter(W=>!W.read).length,V=v.useCallback(()=>{if(S){if(O>0){const W=Array.from(T.current).filter(j=>{const U=t.find(B=>String(B.id)===String(j));return U&&!U.read});W.length>0&&u?.(W)}T.current.clear(),w(!1)}},[S,u,O,t]);v.useEffect(()=>{if(!S)return;const W=j=>{N.current&&!N.current.contains(j.target)&&V()};return document.addEventListener("mousedown",W),()=>document.removeEventListener("mousedown",W)},[S,V]);const H=v.useCallback(()=>{S?V():w(!0)},[S,V]);return v.useLayoutEffect(()=>{const W=C.current;if(!S||!W)return;W.style.transform="";const j=W.getBoundingClientRect(),U=typeof window<"u"?window.innerWidth:0;if(j.width===0&&j.height===0)return;const B=8;let G=0;j.right>U-B?G=-(j.right-(U-B)):j.left<B&&(G=B-j.left),G!==0&&(W.style.transform=`translateX(${G}px)`)},[S,b,t.length]),h.jsxs(I,{ref:N,className:`relative ${_}`,id:x,...E,children:[h.jsxs(oe,{onClick:H,className:"relative w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors cursor-pointer","aria-label":s,children:[h.jsx(re,{name:de.Bell,className:"text-sm text-slate-500 dark:text-slate-400"}),O>0&&h.jsx(Q,{className:"absolute top-0 right-0 flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 dark:bg-red-600 rounded-full",children:O>99?"99+":O})]}),S&&h.jsxs(I,{ref:C,className:`absolute ${b==="left"?"left-0":"right-0"} mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between gap-2",children:[h.jsxs(I,{className:"flex items-center gap-3 min-w-0",children:[h.jsx(Q,{className:"font-semibold text-gray-900 dark:text-white",children:s}),y&&h.jsxs(I,{className:"flex items-center gap-1.5 cursor-pointer select-none",onClick:()=>y(!c),children:[h.jsx(wt,{type:"checkbox",checked:c,onChange:W=>{W.stopPropagation(),y(W.target.checked)},onClick:W=>W.stopPropagation(),className:"w-3.5 h-3.5 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 cursor-pointer"}),h.jsx(Q,{className:"text-xs text-gray-600 dark:text-gray-400",children:l})]})]}),h.jsxs(I,{className:"flex items-center gap-3 flex-shrink-0",children:[g&&O>0&&h.jsx(oe,{onClick:()=>{g(),T.current.clear()},className:"text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline cursor-pointer",children:i}),m&&t.length>0&&h.jsx(oe,{onClick:()=>{m()},className:"text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:underline cursor-pointer",children:a})]})]}),h.jsx(I,{ref:k,className:"max-h-96 overflow-y-auto",children:t.length===0&&!r?h.jsx(I,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:h.jsx(Q,{children:o})}):h.jsxs(h.Fragment,{children:[t.map(W=>{const j=!W.read;return h.jsxs(I,{"data-notification-id":W.id,"data-unread":j?"true":"false",onClick:()=>{W.onClick?.(),d?.(W),w(!1)},className:`group flex items-start gap-3 px-4 py-3 text-left border-b border-gray-100 dark:border-gray-700 transition-colors cursor-pointer ${j?"bg-blue-50/70 dark:bg-blue-900/20 hover:bg-blue-50 dark:hover:bg-blue-900/30":"bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 opacity-70"}`,children:[W.iconName&&h.jsx(re,{name:W.iconName,className:`w-5 h-5 mt-0.5 flex-shrink-0 ${j?"text-blue-600 dark:text-blue-400":"text-gray-400 dark:text-gray-500"}`}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`text-sm truncate ${j?"font-semibold text-gray-900 dark:text-white":"font-normal text-gray-600 dark:text-gray-400"}`,children:W.title}),h.jsx(I,{className:`text-sm mt-1 line-clamp-2 ${j?"text-gray-700 dark:text-gray-300":"text-gray-500 dark:text-gray-500"}`,children:W.message}),h.jsx(I,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1 font-mono",children:W.time})]}),h.jsxs(I,{className:"flex flex-col items-center gap-2 flex-shrink-0",children:[j&&h.jsx(Q,{className:"w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full mt-1.5"}),p&&h.jsx(oe,{onClick:U=>{U.stopPropagation(),p(W)},className:"p-1 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity","aria-label":"Delete notification",children:h.jsx(re,{name:de.Trash,className:"w-4 h-4"})})]})]},W.id)}),n&&h.jsx(I,{ref:M,className:"px-4 py-3 text-center",children:r?h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."}):h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500"})}),r&&!n&&t.length>0&&h.jsx(I,{className:"px-4 py-2 text-center",children:h.jsx(Q,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."})})]})})]})]})},cu=window.G7Core?.createLogger?.("Comp:UserProfile")??{log:(...t)=>console.log("[Comp:UserProfile]",...t),warn:(...t)=>console.warn("[Comp:UserProfile]",...t),error:(...t)=>console.error("[Comp:UserProfile]",...t)},wy=({user:t={name:"",email:""},profileText:e="Profile Settings",logoutText:n="Logout",onProfileClick:r,onLogoutClick:s,className:o="",logoutEndpoint:i="/api/admin/auth/logout",redirectPath:a="/admin/login",showChevron:l=!0,dropdownDirection:c="up",dropdownOnly:d=!1,id:u,editorAttrs:f})=>{const[g,m]=v.useState(!1),[p,y]=v.useState(!1),b=v.useRef(null);v.useEffect(()=>{const S=w=>{b.current&&!b.current.contains(w.target)&&m(!1)};return document.addEventListener("mousedown",S),()=>document.removeEventListener("mousedown",S)},[]);const _=()=>{const w=`; ${document.cookie}`.split("; XSRF-TOKEN=");return w.length===2?decodeURIComponent(w.pop()?.split(";").shift()||""):null},x=()=>localStorage.getItem("auth_token"),E=async()=>{if(!p)try{if(y(!0),m(!1),s){s();return}const S=_(),w=x(),N=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json",...S&&{"X-XSRF-TOKEN":S},...w&&{Authorization:`Bearer ${w}`}},credentials:"include"});N.ok?(localStorage.removeItem("auth_token"),window.location.href=a):(cu.error("로그아웃 실패:",N.statusText),localStorage.removeItem("auth_token"),window.location.href=a)}catch(S){cu.error("로그아웃 오류:",S),localStorage.removeItem("auth_token"),window.location.href=a}finally{y(!1)}};return h.jsxs(I,{ref:b,className:d?`absolute inset-0 ${o}`:`relative ${o}`,id:u,...f,children:[h.jsx(oe,{onClick:()=>m(!g),className:d?"w-full h-full opacity-0 cursor-pointer":"flex items-center gap-2 px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors w-full",children:!d&&h.jsxs(h.Fragment,{children:[t.avatar?h.jsx(kn,{src:t.avatar,alt:t.name,className:"w-8 h-8 rounded-full object-cover"}):h.jsx(I,{className:"w-8 h-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center",children:h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:"flex-1 text-left",children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white text-sm",children:t.name}),t.role&&h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-xs",children:t.role})]}),l&&h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-600 dark:text-gray-400"})]})}),g&&h.jsxs(I,{className:`absolute ${c==="down"?"top-full mt-2":"bottom-full mb-2"} ${d?"right-0 w-72":"left-0 w-full"} bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[h.jsxs(I,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:()=>{m(!1),window.G7Core?.dispatch({handler:"navigate",params:{path:`/admin/users/${t.uuid}`}})},children:[h.jsx(I,{className:"font-semibold text-gray-900 dark:text-white",children:t.name}),h.jsx(I,{className:"text-gray-500 dark:text-gray-400 text-sm",children:t.email})]}),h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>{m(!1),r?r():window.G7Core?.dispatch({handler:"navigate",params:{path:`/admin/users/${t.uuid}/edit`}})},className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center justify-start gap-3 transition-colors",children:[h.jsx(re,{name:de.User,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:e})]}),h.jsxs(oe,{onClick:E,disabled:p,className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center justify-start gap-3 text-red-600 dark:text-red-400 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",children:[h.jsx(re,{name:de.ArrowRight,className:"w-5 h-5"}),h.jsx(Q,{children:p?"Logging out...":n})]})]})]})]})},jo=window.G7Core?.createLogger?.("Comp:LanguageSelector")??{log:(...t)=>console.log("[Comp:LanguageSelector]",...t),warn:(...t)=>console.warn("[Comp:LanguageSelector]",...t),error:(...t)=>console.error("[Comp:LanguageSelector]",...t)},du=t=>{const e=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[t];return e||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[t]||t},ky=({availableLocales:t=["ko","en"],languageText:e="Language",apiEndpoint:n="/api/admin/users/me/language",onLanguageChange:r,className:s="",inline:o=!1,showCode:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(!1),[u,f]=v.useState(!1),[g,m]=v.useState(()=>localStorage.getItem("g7_locale")||"ko"),p=v.useRef(null);v.useEffect(()=>{if(o)return;const x=E=>{p.current&&!p.current.contains(E.target)&&d(!1)};return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[o]);const y=()=>{const E=`; ${document.cookie}`.split("; XSRF-TOKEN=");return E.length===2?decodeURIComponent(E.pop()?.split(";").shift()||""):null},b=()=>localStorage.getItem("auth_token"),_=async x=>{if(!(u||x===g))try{f(!0);const E=y(),S=b();try{const w=await fetch(n,{method:"PATCH",headers:{"Content-Type":"application/json",Accept:"application/json",...E&&{"X-XSRF-TOKEN":E},...S&&{Authorization:`Bearer ${S}`}},credentials:"include",body:JSON.stringify({language:x})});w.ok||jo.warn("언어 설정 저장 실패 (UI는 변경됨):",w.statusText)}catch(w){jo.warn("언어 설정 API 호출 실패 (UI는 변경됨):",w)}m(x),d(!1),r?.(x),window.__templateApp?await window.__templateApp.changeLocale(x):(jo.warn("TemplateApp not found, reloading page"),window.location.reload())}catch(E){jo.error("언어 변경 오류:",E)}finally{f(!1)}};return o?h.jsxs(I,{className:`${s}`,id:a,...l,children:[h.jsxs(oe,{onClick:()=>d(!c),disabled:u,className:"w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors",children:[h.jsx(re,{name:de.Globe,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:e}),h.jsx(Q,{className:"px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 rounded",children:g.toUpperCase()}),h.jsx(re,{name:de.ChevronDown,className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${c?"rotate-180":""}`})]}),c&&h.jsx(I,{className:"px-4 py-2 flex flex-wrap gap-2",children:t.map(x=>h.jsx(oe,{onClick:()=>_(x),disabled:u,className:` px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${g===x?"bg-blue-600 text-white":"bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"} ${u?"opacity-50 cursor-not-allowed":""} - `,children:xu(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(ae,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":t,children:[h.jsx(oe,{name:de.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(Q,{className:"font-medium uppercase",children:g}),i&&h.jsx(oe,{name:de.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:e.map(x=>h.jsxs(ae,{onClick:()=>_(x),disabled:u,className:` + `,children:du(x)},x))})]}):h.jsxs(I,{ref:p,className:`relative ${s}`,id:a,...l,children:[h.jsxs(oe,{onClick:()=>d(!c),className:i?"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors cursor-pointer":"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors","aria-haspopup":"listbox","aria-expanded":c,"aria-label":e,children:[h.jsx(re,{name:de.Globe,className:i?"w-4 h-4":"w-5 h-5 text-gray-600 dark:text-gray-400"}),i&&h.jsx(Q,{className:"font-medium uppercase",children:g}),i&&h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3"})]}),c&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsx(I,{className:"py-2",children:t.map(x=>h.jsxs(oe,{onClick:()=>_(x),disabled:u,className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${g===x?"bg-gray-50 dark:bg-gray-700":""} ${u?"opacity-50 cursor-not-allowed":""} - `,children:[h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:xu(x)}),g===x&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},Yy=({copyright:e="© 2026 G7",version:t,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(Q,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:e}),t&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:"•"}),h.jsxs(Q,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(oe,{name:de.Tag,className:"w-4 h-4"}),h.jsxs(Q,{children:["v",t]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Yr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(oe,{name:d.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:d.label})]},d.id))})]})})},Xy=({title:e,description:t,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(bu,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Ba,{className:"page-title",children:e}),t&&h.jsx(vt,{className:"page-description",children:t})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(ae,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(oe,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(ae,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},Ky=()=>window.G7Core,qy=({title:e,description:t,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=Ky()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[e&&h.jsx(ar,{className:"text-base font-semibold text-gray-900 dark:text-white",children:e}),t&&h.jsx(vt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:t})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},cr=(e,t)=>window.G7Core?.t?.(e,t)??e,Zy=({image:e,imageAlt:t,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:cr("common.status_active"),inactive:cr("common.status_inactive"),pending:cr("common.status_pending"),error:cr("common.status_error")})[b];return h.jsxs(I,{className:` + `,children:[h.jsx(Q,{className:"text-gray-900 dark:text-white flex-1",children:du(x)}),g===x&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400"})]},x))})})]})},Cy=({copyright:t="© 2026 G7",version:e,quickLinks:n=[],className:r="",changelogModalId:s,copyrightModalId:o,id:i,editorAttrs:a})=>{const l=v.useCallback(()=>{s&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/changelog",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreChangelogContent:"{{response.data.content}}"}},{handler:"openModal",target:s}],onError:[{handler:"openModal",target:s}]}]})},[s]),c=v.useCallback(()=>{o&&window.G7Core?.dispatch?.({handler:"sequence",actions:[{handler:"apiCall",auth_required:!0,target:"/api/admin/license",params:{method:"GET"},onSuccess:[{handler:"setState",params:{target:"global",coreLicenseContent:"{{response.data.content}}"}},{handler:"openModal",target:o}],onError:[{handler:"openModal",target:o}]}]})},[o]);return h.jsx(I,{className:`admin-footer ${r}`,id:i,...a,children:h.jsxs(I,{className:"admin-footer-row",children:[h.jsxs(I,{className:"admin-footer-group",children:[h.jsx(Q,{className:o?"admin-footer-link-hover":"",onClick:o?c:void 0,children:t}),e&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{className:"hidden md:inline",children:"•"}),h.jsxs(Q,{className:`flex-center gap-2 ${s?"admin-footer-link-hover":""}`,onClick:s?l:void 0,children:[h.jsx(re,{name:de.Tag,className:"w-4 h-4"}),h.jsxs(Q,{children:["v",e]})]})]})]}),n.length>0&&h.jsx(I,{className:"flex-center gap-4",children:n.map(d=>h.jsxs(Fr,{href:d.url,className:"admin-footer-link",children:[d.iconName&&h.jsx(re,{name:d.iconName,className:"w-4 h-4"}),h.jsx(Q,{children:d.label})]},d.id))})]})})},Sy=({title:t,description:e,breadcrumbItems:n,tabs:r,onTabChange:s,actions:o,className:i="",children:a,id:l,editorAttrs:c})=>{const d=(u="secondary")=>({primary:"page-action-primary",secondary:"page-action-secondary",danger:"page-action-danger",ghost:"page-action-ghost"})[u];return h.jsxs(I,{className:i,id:l,...c,children:[n&&n.length>0&&h.jsx(I,{className:"mb-md",children:h.jsx(au,{items:n})}),h.jsxs(I,{className:"flex-between mb-lg",children:[h.jsxs(I,{children:[h.jsx(Pa,{className:"page-title",children:t}),e&&h.jsx(yt,{className:"page-description",children:e})]}),a?h.jsx(I,{className:"flex-center page-actions",children:a}):o&&o.length>0?h.jsx(I,{className:"page-actions",children:o.map(u=>h.jsxs(oe,{onClick:u.onClick,disabled:u.disabled,className:d(u.variant),children:[u.iconName&&h.jsx(re,{name:u.iconName,className:"icon-md"}),u.variant!=="ghost"&&u.label&&h.jsx(I,{children:u.label})]},u.id))}):null]}),r&&r.length>0&&h.jsx(I,{className:"tab-container",children:r.map(u=>h.jsxs(oe,{onClick:()=>s?.(u.value),className:u.active?"tab-btn-active":"tab-btn",children:[h.jsx(I,{children:u.label}),u.badge!==void 0&&h.jsx(I,{className:u.active?"tab-badge-active":"tab-badge",children:u.badge})]},u.id))})]})},_y=()=>window.G7Core,Ey=({title:t,description:e,className:n="",style:r,children:s,id:o,editorAttrs:i})=>{const a="flex items-center justify-between",l=_y()?.style?.mergeClasses?.(a,n)??`${a} ${n}`;return h.jsxs(I,{className:l,style:r,id:o,...i,children:[h.jsxs(I,{children:[t&&h.jsx(er,{className:"text-base font-semibold text-gray-900 dark:text-white",children:t}),e&&h.jsx(yt,{className:"mt-1 text-sm text-gray-500 dark:text-gray-400",children:e})]}),s&&h.jsx(I,{className:"flex items-center gap-2",children:s})]})},nr=(t,e)=>window.G7Core?.t?.(t,e)??t,My=({image:t,imageAlt:e,vendor:n,name:r,version:s,status:o,updateAvailable:i=!1,latestVersion:a,dependencies:l=[],showLayoutEditButton:c=!1,actions:d=[],onLayoutEditClick:u,className:f="",id:g,editorAttrs:m})=>{const p=b=>({active:"success",inactive:"default",pending:"pending",error:"error"})[b],y=b=>({active:nr("common.status_active"),inactive:nr("common.status_inactive"),pending:nr("common.status_pending"),error:nr("common.status_error")})[b];return h.jsxs(I,{className:` bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden hover:shadow-lg transition-shadow ${f} - `,id:g,...m,children:[e&&h.jsxs(I,{className:"relative",children:[h.jsx(_n,{src:e,alt:t||cr("common.template_preview",{vendor:n,name:r}),className:"w-full h-48 object-cover"}),h.jsx(I,{className:"absolute top-3 right-3",children:h.jsx(Ko,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(Q,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(oe,{name:de.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(Q,{children:cr("common.update_available")})]})})]}),h.jsxs(I,{className:"p-4",children:[h.jsxs(I,{className:"flex items-start justify-between mb-3",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Gr,{className:"text-lg font-bold text-gray-900 dark:text-white",children:[n,"/",r]}),h.jsxs(I,{className:"flex items-center gap-2 mt-1 text-sm text-gray-600 dark:text-gray-400",children:[h.jsxs(Q,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{children:"→"}),h.jsxs(Q,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!e&&h.jsx(I,{className:"mt-2",children:h.jsx(Ko,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(Ls,{items:d,triggerIconName:de.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(vt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[cr("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,_)=>h.jsx(Q,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},_))})]}),c&&u&&h.jsxs(ae,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(oe,{name:de.Edit,className:"w-4 h-4"}),h.jsx(Q,{children:cr("common.edit_layout")})]})]})]})},wu=({type:e,message:t,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:de.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:de.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:de.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:de.TimesCircle}},l=a[e]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(oe,{name:l.icon,className:l.iconClass,ariaLabel:`${e} icon`}),h.jsx(vt,{className:l.textClass,children:t}),n&&h.jsx(ae,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(oe,{name:de.Times,className:l.iconClass,ariaLabel:"Close"})})]})},Qy=({layoutName:e,onBack:t,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=` + `,id:g,...m,children:[t&&h.jsxs(I,{className:"relative",children:[h.jsx(kn,{src:t,alt:e||nr("common.template_preview",{vendor:n,name:r}),className:"w-full h-48 object-cover"}),h.jsx(I,{className:"absolute top-3 right-3",children:h.jsx(Fo,{status:p(o),label:y(o),showIcon:!0})}),i&&h.jsx(I,{className:"absolute top-3 left-3",children:h.jsxs(Q,{className:"flex items-center gap-1 px-2 py-1 bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800 rounded-md text-xs font-semibold",children:[h.jsx(re,{name:de.ExclamationTriangle,className:"w-3 h-3"}),h.jsx(Q,{children:nr("common.update_available")})]})})]}),h.jsxs(I,{className:"p-4",children:[h.jsxs(I,{className:"flex items-start justify-between mb-3",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs($r,{className:"text-lg font-bold text-gray-900 dark:text-white",children:[n,"/",r]}),h.jsxs(I,{className:"flex items-center gap-2 mt-1 text-sm text-gray-600 dark:text-gray-400",children:[h.jsxs(Q,{children:["v",s]}),i&&a&&h.jsxs(h.Fragment,{children:[h.jsx(Q,{children:"→"}),h.jsxs(Q,{className:"text-yellow-600 dark:text-yellow-400 font-semibold",children:["v",a]})]})]}),!t&&h.jsx(I,{className:"mt-2",children:h.jsx(Fo,{status:p(o),label:y(o),showIcon:!0})})]}),d.length>0&&h.jsx(Cs,{items:d,triggerIconName:de.EllipsisVertical,position:"left"})]}),l.length>0&&h.jsxs(I,{className:"mb-3",children:[h.jsxs(yt,{className:"text-xs text-gray-500 dark:text-gray-400 mb-1",children:[nr("common.dependencies"),":"]}),h.jsx(I,{className:"flex flex-wrap gap-1",children:l.map((b,_)=>h.jsx(Q,{className:"px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded",children:b},_))})]}),c&&u&&h.jsxs(oe,{onClick:u,className:"w-full px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-md hover:bg-blue-700 dark:hover:bg-blue-600 transition-colors flex items-center justify-center gap-2 font-medium",children:[h.jsx(re,{name:de.Edit,className:"w-4 h-4"}),h.jsx(Q,{children:nr("common.edit_layout")})]})]})]})},uu=({type:t,message:e,dismissible:n=!1,onDismiss:r,className:s="",id:o,editorAttrs:i})=>{const a={info:{containerClass:"alert-info",iconClass:"alert-icon-info",textClass:"alert-text-info",icon:de.InfoCircle},success:{containerClass:"alert-success",iconClass:"alert-icon-success",textClass:"alert-text-success",icon:de.CheckCircle},warning:{containerClass:"alert-warning",iconClass:"alert-icon-warning",textClass:"alert-text-warning",icon:de.ExclamationTriangle},error:{containerClass:"alert-error",iconClass:"alert-icon-error",textClass:"alert-text-error",icon:de.TimesCircle}},l=a[t]??a.info;return h.jsxs(I,{className:`${l.containerClass} ${s}`,role:"alert","aria-live":"polite",id:o,...i,children:[h.jsx(re,{name:l.icon,className:l.iconClass,ariaLabel:`${t} icon`}),h.jsx(yt,{className:l.textClass,children:e}),n&&h.jsx(oe,{onClick:r,className:`alert-dismiss ${l.textClass}`,"aria-label":"Close alert",children:h.jsx(re,{name:de.Times,className:l.iconClass,ariaLabel:"Close"})})]})},Ny=({layoutName:t,onBack:e,onPreview:n,onSave:r,isSaving:s=!1,className:o=""})=>{const i=` flex items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 ${o} - `.trim().replace(/\s+/g," ");return h.jsxs(I,{className:i,children:[h.jsxs(I,{className:"flex items-center gap-3",children:[h.jsx(ae,{onClick:t,className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors bg-transparent border-0 cursor-pointer","aria-label":"뒤로가기",children:h.jsx(oe,{name:de.ArrowLeft,className:"text-gray-700 dark:text-gray-300",ariaLabel:"뒤로가기"})}),h.jsx(ar,{className:"text-xl font-semibold text-gray-900 dark:text-gray-100",children:e})]}),h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(ae,{onClick:n,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors border-0 cursor-pointer","aria-label":"미리보기",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:de.Eye,className:"text-gray-700 dark:text-gray-300",ariaLabel:"미리보기 아이콘"}),h.jsx("span",{children:"미리보기"})]})}),h.jsx(ae,{onClick:r,disabled:s,className:` + `.trim().replace(/\s+/g," ");return h.jsxs(I,{className:i,children:[h.jsxs(I,{className:"flex items-center gap-3",children:[h.jsx(oe,{onClick:e,className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors bg-transparent border-0 cursor-pointer","aria-label":"뒤로가기",children:h.jsx(re,{name:de.ArrowLeft,className:"text-gray-700 dark:text-gray-300",ariaLabel:"뒤로가기"})}),h.jsx(er,{className:"text-xl font-semibold text-gray-900 dark:text-gray-100",children:t})]}),h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{onClick:n,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors border-0 cursor-pointer","aria-label":"미리보기",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:de.Eye,className:"text-gray-700 dark:text-gray-300",ariaLabel:"미리보기 아이콘"}),h.jsx("span",{children:"미리보기"})]})}),h.jsx(oe,{onClick:r,disabled:s,className:` px-4 py-2 rounded-lg transition-colors border-0 cursor-pointer ${s?"bg-blue-400 dark:bg-blue-600 cursor-not-allowed":"bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"} text-white - `.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(oe,{name:s?de.Spinner:de.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},Jy=({files:e,selectedId:t,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=` + `.trim().replace(/\s+/g," "),"aria-label":s?"저장 중...":"저장",children:h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(re,{name:s?de.Spinner:de.Save,className:`text-white ${s?"animate-spin":""}`,ariaLabel:s?"저장 중 아이콘":"저장 아이콘"}),h.jsx("span",{children:s?"저장 중...":"저장"})]})})]})]})},Oy=({files:t,selectedId:e,onSelect:n,className:r=""})=>{const s=a=>{try{const l=new Date(a);if(isNaN(l.getTime()))return a;const c=l.getFullYear(),d=String(l.getMonth()+1).padStart(2,"0"),u=String(l.getDate()).padStart(2,"0"),f=String(l.getHours()).padStart(2,"0"),g=String(l.getMinutes()).padStart(2,"0");return`${c}-${d}-${u} ${f}:${g}`}catch{return a}},o=a=>{n(a)},i=` w-full ${r} - `.trim().replace(/\s+/g," ");return h.jsx(I,{className:i,children:e.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"파일이 없습니다."}):h.jsx(ja,{className:"divide-y divide-gray-200 dark:divide-gray-700",role:"listbox","aria-label":"레이아웃 파일 목록",children:e.map(a=>{const l=a.id===t,c=` + `.trim().replace(/\s+/g," ");return h.jsx(I,{className:i,children:t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"파일이 없습니다."}):h.jsx($a,{className:"divide-y divide-gray-200 dark:divide-gray-700",role:"listbox","aria-label":"레이아웃 파일 목록",children:t.map(a=>{const l=a.id===e,c=` flex items-center justify-between p-4 cursor-pointer transition-colors duration-150 ${l?"bg-blue-100 dark:bg-blue-900 border-l-4 border-blue-500":"hover:bg-gray-100 dark:hover:bg-gray-800"} @@ -106,8 +105,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho `.trim().replace(/\s+/g," "),u=` text-sm ${l?"text-blue-700 dark:text-blue-300":"text-gray-500 dark:text-gray-400"} - `.trim().replace(/\s+/g," ");return h.jsx(Wa,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(Q,{className:d,children:a.name}),h.jsx(Q,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function ku(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function Ry(e){if(Array.isArray(e))return e}function ev(e,t,n){return(t=av(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function tv(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function nv(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Cu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Su(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Cu(Object(n),!0).forEach(function(r){ev(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Cu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function rv(e,t){if(e==null)return{};var n,r,s=sv(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}function sv(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function ov(e,t){return Ry(e)||tv(e,t)||lv(e,t)||nv()}function iv(e,t){if(typeof e!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function av(e){var t=iv(e,"string");return typeof t=="symbol"?t:t+""}function lv(e,t){if(e){if(typeof e=="string")return ku(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?ku(e,t):void 0}}function cv(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function _u(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Eu(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?_u(Object(n),!0).forEach(function(r){cv(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):_u(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function dv(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return function(r){return t.reduceRight(function(s,o){return o(s)},r)}}function Hs(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return t.apply(n,[].concat(s,a))}}}function Ro(e){return{}.toString.call(e).includes("Object")}function uv(e){return!Object.keys(e).length}function Us(e){return typeof e=="function"}function fv(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function hv(e,t){return Ro(t)||dr("changeType"),Object.keys(t).some(function(n){return!fv(e,n)})&&dr("changeField"),t}function gv(e){Us(e)||dr("selectorType")}function pv(e){Us(e)||Ro(e)||dr("handlerType"),Ro(e)&&Object.values(e).some(function(t){return!Us(t)})&&dr("handlersType")}function mv(e){e||dr("initialIsRequired"),Ro(e)||dr("initialType"),uv(e)&&dr("initialContent")}function bv(e,t){throw new Error(e[t]||e.default)}var yv={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},dr=Hs(bv)(yv),ei={changes:hv,selector:gv,handler:pv,initial:mv};function vv(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};ei.initial(e),ei.handler(t);var n={current:e},r=Hs(kv)(n,t),s=Hs(wv)(n),o=Hs(ei.changes)(e),i=Hs(xv)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return ei.selector(c),c(n.current)}function l(c){dv(r,s,o,i)(c)}return[a,l]}function xv(e,t){return Us(t)?t(e.current):t}function wv(e,t){return e.current=Eu(Eu({},e.current),t),t}function kv(e,t,n){return Us(t)?t(e.current):Object.keys(n).forEach(function(r){var s;return(s=t[r])===null||s===void 0?void 0:s.call(t,e.current[r])}),n}var Cv={create:vv},Sv={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs"}};function _v(e){return function t(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=e.length?e.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return t.apply(n,[].concat(s,a))}}}function Ev(e){return{}.toString.call(e).includes("Object")}function Mv(e){return e||Nu("configIsRequired"),Ev(e)||Nu("configType"),e.urls?(Nv(),{paths:{vs:e.urls.monacoBase}}):e}function Nv(){console.warn(Mu.deprecation)}function Tv(e,t){throw new Error(e[t]||e.default)}var Mu={configIsRequired:"the configuration object is required",configType:"the configuration object should be an object",default:"an unknown error accured in `@monaco-editor/loader` package",deprecation:`Deprecation warning! + `.trim().replace(/\s+/g," ");return h.jsx(Fa,{className:c,onClick:()=>o(a.id),role:"option","aria-selected":l,tabIndex:0,onKeyDown:f=>{(f.key==="Enter"||f.key===" ")&&(f.preventDefault(),o(a.id))},children:h.jsxs(I,{className:"flex flex-col gap-1",children:[h.jsx(Q,{className:d,children:a.name}),h.jsx(Q,{className:u,children:s(a.updated_at)})]})},a.id)})})})};function fu(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Ty(t){if(Array.isArray(t))return t}function Ay(t,e,n){return(e=Vy(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Dy(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e!==0)for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function Iy(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function hu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function gu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?hu(Object(n),!0).forEach(function(r){Ay(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):hu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Ly(t,e){if(t==null)return{};var n,r,s=Py(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}function Py(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function $y(t,e){return Ty(t)||Dy(t,e)||zy(t,e)||Iy()}function Fy(t,e){if(typeof t!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(typeof r!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Vy(t){var e=Fy(t,"string");return typeof e=="symbol"?e:e+""}function zy(t,e){if(t){if(typeof t=="string")return fu(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?fu(t,e):void 0}}function Hy(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function pu(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function mu(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?pu(Object(n),!0).forEach(function(r){Hy(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):pu(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Uy(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return function(r){return e.reduceRight(function(s,o){return o(s)},r)}}function Ts(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return e.apply(n,[].concat(s,a))}}}function Wo(t){return{}.toString.call(t).includes("Object")}function By(t){return!Object.keys(t).length}function As(t){return typeof t=="function"}function jy(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function Wy(t,e){return Wo(e)||rr("changeType"),Object.keys(e).some(function(n){return!jy(t,n)})&&rr("changeField"),e}function Gy(t){As(t)||rr("selectorType")}function Yy(t){As(t)||Wo(t)||rr("handlerType"),Wo(t)&&Object.values(t).some(function(e){return!As(e)})&&rr("handlersType")}function Xy(t){t||rr("initialIsRequired"),Wo(t)||rr("initialType"),By(t)&&rr("initialContent")}function Ky(t,e){throw new Error(t[e]||t.default)}var qy={initialIsRequired:"initial state is required",initialType:"initial state should be an object",initialContent:"initial state shouldn't be an empty object",handlerType:"handler should be an object or a function",handlersType:"all handlers should be a functions",selectorType:"selector should be a function",changeType:"provided value of changes should be an object",changeField:'it seams you want to change a field in the state which is not specified in the "initial" state',default:"an unknown error accured in `state-local` package"},rr=Ts(Ky)(qy),Go={changes:Wy,selector:Gy,handler:Yy,initial:Xy};function Zy(t){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};Go.initial(t),Go.handler(e);var n={current:t},r=Ts(Ry)(n,e),s=Ts(Jy)(n),o=Ts(Go.changes)(t),i=Ts(Qy)(n);function a(){var c=arguments.length>0&&arguments[0]!==void 0?arguments[0]:function(d){return d};return Go.selector(c),c(n.current)}function l(c){Uy(r,s,o,i)(c)}return[a,l]}function Qy(t,e){return As(e)?e(t.current):e}function Jy(t,e){return t.current=mu(mu({},t.current),e),e}function Ry(t,e,n){return As(e)?e(t.current):Object.keys(n).forEach(function(r){var s;return(s=e[r])===null||s===void 0?void 0:s.call(e,t.current[r])}),n}var ev={create:Zy},tv={paths:{vs:"https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs"}};function nv(t){return function e(){for(var n=this,r=arguments.length,s=new Array(r),o=0;o<r;o++)s[o]=arguments[o];return s.length>=t.length?t.apply(this,s):function(){for(var i=arguments.length,a=new Array(i),l=0;l<i;l++)a[l]=arguments[l];return e.apply(n,[].concat(s,a))}}}function rv(t){return{}.toString.call(t).includes("Object")}function sv(t){return t||yu("configIsRequired"),rv(t)||yu("configType"),t.urls?(ov(),{paths:{vs:t.urls.monacoBase}}):t}function ov(){console.warn(bu.deprecation)}function iv(t,e){throw new Error(t[e]||t.default)}var bu={configIsRequired:"the configuration object is required",configType:"the configuration object should be an object",default:"an unknown error accured in `@monaco-editor/loader` package",deprecation:`Deprecation warning! You are using deprecated way of configuration. Instead of using @@ -116,28 +115,28 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho monaco.config({ paths: { vs: '...' } }) For more please check the link https://github.com/suren-atoyan/monaco-loader#config - `},Nu=_v(Tv)(Mu),Ov={config:Mv},Av=function(){for(var t=arguments.length,n=new Array(t),r=0;r<t;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function Tu(e,t){return Object.keys(t).forEach(function(n){t[n]instanceof Object&&e[n]&&Object.assign(t[n],Tu(e[n],t[n]))}),Su(Su({},e),t)}var Dv={type:"cancelation",msg:"operation is manually canceled"};function pl(e){var t=!1,n=new Promise(function(r,s){e.then(function(o){return t?s(Dv):r(o)}),e.catch(s)});return n.cancel=function(){return t=!0},n}var Iv=["monaco"],Lv=Cv.create({config:Sv,isInitialized:!1,resolve:null,reject:null,monaco:null}),Ou=ov(Lv,2),Bs=Ou[0],ti=Ou[1];function Pv(e){var t=Ov.config(e),n=t.monaco,r=rv(t,Iv);ti(function(s){return{config:Tu(s.config,r),monaco:n}})}function $v(){var e=Bs(function(t){var n=t.monaco,r=t.isInitialized,s=t.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!e.isInitialized){if(ti({isInitialized:!0}),e.monaco)return e.resolve(e.monaco),pl(ml);if(window.monaco&&window.monaco.editor)return Au(window.monaco),e.resolve(window.monaco),pl(ml);Av(Fv,Vv)(Hv)}return pl(ml)}function Fv(e){return document.body.appendChild(e)}function zv(e){var t=document.createElement("script");return e&&(t.src=e),t}function Vv(e){var t=Bs(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=zv("".concat(t.config.paths.vs,"/loader.js"));return n.onload=function(){return e()},n.onerror=t.reject,n}function Hv(){var e=Bs(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),t=window.require;t.config(e.config),t(["vs/editor/editor.main"],function(n){var r=n.m||n;Au(r),e.resolve(r)},function(n){e.reject(n)})}function Au(e){Bs().monaco||ti({monaco:e})}function Uv(){return Bs(function(e){var t=e.monaco;return t})}var ml=new Promise(function(e,t){return ti({resolve:e,reject:t})}),Du={config:Pv,init:$v,__getMonacoInstance:Uv},Bv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},bl=Bv,jv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},Wv=jv;function Gv({children:e}){return v.createElement("div",{style:Wv.container},e)}var Yv=Gv,Xv=Yv;function Kv({width:e,height:t,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...bl.wrapper,width:e,height:t},...i},!n&&v.createElement(Xv,null,r),v.createElement("div",{ref:s,style:{...bl.fullWidth,...!n&&bl.hide},className:o}))}var qv=Kv,Iu=v.memo(qv);function Zv(e){v.useEffect(e,[])}var Lu=Zv;function Qv(e,t,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:e,t)}var cn=Qv;function js(){}function ts(e,t,n,r){return Jv(e,r)||Rv(e,t,n,r)}function Jv(e,t){return e.editor.getModel(Pu(e,t))}function Rv(e,t,n,r){return e.editor.createModel(t,n,r?Pu(e,r):void 0)}function Pu(e,t){return e.Uri.parse(t)}function ex({original:e,modified:t,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=js,onMount:b=js}){let[_,x]=v.useState(!1),[M,C]=v.useState(!0),k=v.useRef(null),N=v.useRef(null),S=v.useRef(null),w=v.useRef(b),E=v.useRef(y),D=v.useRef(!1);Lu(()=>{let A=Du.init();return A.then(O=>(N.current=O)&&C(!1)).catch(O=>O?.type!=="cancelation"&&console.error("Monaco initialization: error:",O)),()=>k.current?$():A.cancel()}),cn(()=>{if(k.current&&N.current){let A=k.current.getOriginalEditor(),O=ts(N.current,e||"",r||n||"text",o||"");O!==A.getModel()&&A.setModel(O)}},[o],_),cn(()=>{if(k.current&&N.current){let A=k.current.getModifiedEditor(),O=ts(N.current,t||"",s||n||"text",i||"");O!==A.getModel()&&A.setModel(O)}},[i],_),cn(()=>{let A=k.current.getModifiedEditor();A.getOption(N.current.editor.EditorOption.readOnly)?A.setValue(t||""):t!==A.getValue()&&(A.executeEdits("",[{range:A.getModel().getFullModelRange(),text:t||"",forceMoveMarkers:!0}]),A.pushUndoStop())},[t],_),cn(()=>{k.current?.getModel()?.original.setValue(e||"")},[e],_),cn(()=>{let{original:A,modified:O}=k.current.getModel();N.current.editor.setModelLanguage(A,r||n||"text"),N.current.editor.setModelLanguage(O,s||n||"text")},[n,r,s],_),cn(()=>{N.current?.editor.setTheme(c)},[c],_),cn(()=>{k.current?.updateOptions(u)},[u],_);let P=v.useCallback(()=>{if(!N.current)return;E.current(N.current);let A=ts(N.current,e||"",r||n||"text",o||""),O=ts(N.current,t||"",s||n||"text",i||"");k.current?.setModel({original:A,modified:O})},[n,t,s,e,r,o,i]),L=v.useCallback(()=>{!D.current&&S.current&&(k.current=N.current.editor.createDiffEditor(S.current,{automaticLayout:!0,...u}),P(),N.current?.editor.setTheme(c),x(!0),D.current=!0)},[u,c,P]);v.useEffect(()=>{_&&w.current(k.current,N.current)},[_]),v.useEffect(()=>{!M&&!_&&L()},[M,_,L]);function $(){let A=k.current?.getModel();a||A?.original?.dispose(),l||A?.modified?.dispose(),k.current?.dispose()}return v.createElement(Iu,{width:g,height:f,isEditorReady:_,loading:d,_ref:S,className:m,wrapperProps:p})}var tx=ex;v.memo(tx);function nx(e){let t=v.useRef();return v.useEffect(()=>{t.current=e},[e]),t.current}var rx=nx,ni=new Map;function sx({defaultValue:e,defaultLanguage:t,defaultPath:n,value:r,language:s,path:o,theme:i="light",line:a,loading:l="Loading...",options:c={},overrideServices:d={},saveViewState:u=!0,keepCurrentModel:f=!1,width:g="100%",height:m="100%",className:p,wrapperProps:y={},beforeMount:b=js,onMount:_=js,onChange:x,onValidate:M=js}){let[C,k]=v.useState(!1),[N,S]=v.useState(!0),w=v.useRef(null),E=v.useRef(null),D=v.useRef(null),P=v.useRef(_),L=v.useRef(b),$=v.useRef(),A=v.useRef(r),O=rx(o),T=v.useRef(!1),z=v.useRef(!1);Lu(()=>{let G=Du.init();return G.then(U=>(w.current=U)&&S(!1)).catch(U=>U?.type!=="cancelation"&&console.error("Monaco initialization: error:",U)),()=>E.current?j():G.cancel()}),cn(()=>{let G=ts(w.current,e||r||"",t||s||"",o||n||"");G!==E.current?.getModel()&&(u&&ni.set(O,E.current?.saveViewState()),E.current?.setModel(G),u&&E.current?.restoreViewState(ni.get(o)))},[o],C),cn(()=>{E.current?.updateOptions(c)},[c],C),cn(()=>{!E.current||r===void 0||(E.current.getOption(w.current.editor.EditorOption.readOnly)?E.current.setValue(r):r!==E.current.getValue()&&(z.current=!0,E.current.executeEdits("",[{range:E.current.getModel().getFullModelRange(),text:r,forceMoveMarkers:!0}]),E.current.pushUndoStop(),z.current=!1))},[r],C),cn(()=>{let G=E.current?.getModel();G&&s&&w.current?.editor.setModelLanguage(G,s)},[s],C),cn(()=>{a!==void 0&&E.current?.revealLine(a)},[a],C),cn(()=>{w.current?.editor.setTheme(i)},[i],C);let H=v.useCallback(()=>{if(!(!D.current||!w.current)&&!T.current){L.current(w.current);let G=o||n,U=ts(w.current,r||e||"",t||s||"",G||"");E.current=w.current?.editor.create(D.current,{model:U,automaticLayout:!0,...c},d),u&&E.current.restoreViewState(ni.get(G)),w.current.editor.setTheme(i),a!==void 0&&E.current.revealLine(a),k(!0),T.current=!0}},[e,t,n,r,s,o,c,d,u,i,a]);v.useEffect(()=>{C&&P.current(E.current,w.current)},[C]),v.useEffect(()=>{!N&&!C&&H()},[N,C,H]),A.current=r,v.useEffect(()=>{C&&x&&($.current?.dispose(),$.current=E.current?.onDidChangeModelContent(G=>{z.current||x(E.current.getValue(),G)}))},[C,x]),v.useEffect(()=>{if(C){let G=w.current.editor.onDidChangeMarkers(U=>{let W=E.current.getModel()?.uri;if(W&&U.find(Y=>Y.path===W.path)){let Y=w.current.editor.getModelMarkers({resource:W});M?.(Y)}});return()=>{G?.dispose()}}return()=>{}},[C,M]);function j(){$.current?.dispose(),f?u&&ni.set(o,E.current.saveViewState()):E.current.getModel()?.dispose(),E.current.dispose()}return v.createElement(Iu,{width:g,height:m,isEditorReady:C,loading:l,_ref:D,className:p,wrapperProps:y})}var ox=sx,ix=v.memo(ox),ax=ix;const lx=({value:e,onChange:t,language:n="json",height:r="100%",readOnly:s=!1,theme:o="vs-dark"})=>{const i=v.useRef(null),a=(c,d)=>{i.current=c,n==="json"&&d.languages.json.jsonDefaults.setDiagnosticsOptions({validate:!0,schemaValidation:"error",allowComments:!1}),c.updateOptions({readOnly:s,minimap:{enabled:!1},scrollBeyondLastLine:!1,fontSize:14,lineNumbers:"on",renderWhitespace:"selection",automaticLayout:!0})},l=c=>{t&&c!==void 0&&t({target:{value:c}})};return v.useEffect(()=>{i.current&&i.current.updateOptions({readOnly:s})},[s]),h.jsx("div",{className:"border border-gray-300 dark:border-gray-700 rounded-lg overflow-hidden",children:h.jsx(ax,{height:r,language:n,value:e,theme:o,onChange:l,onMount:a,options:{readOnly:s,minimap:{enabled:!1},scrollBeyondLastLine:!1,fontSize:14,lineNumbers:"on",renderWhitespace:"selection",automaticLayout:!0}})})},cx=e=>{const t=new Date(e);return new Intl.DateTimeFormat("ko-KR",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}).format(t)},$u=v.memo(({version:e,isSelected:t,onSelect:n})=>{const r=v.useCallback(()=>{n?.(e.id)},[n,e.id]);return h.jsx(Wa,{role:"listitem","aria-selected":t,className:` + `},yu=nv(iv)(bu),av={config:sv},lv=function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];return function(s){return n.reduceRight(function(o,i){return i(o)},s)}};function vu(t,e){return Object.keys(e).forEach(function(n){e[n]instanceof Object&&t[n]&&Object.assign(e[n],vu(t[n],e[n]))}),gu(gu({},t),e)}var cv={type:"cancelation",msg:"operation is manually canceled"};function al(t){var e=!1,n=new Promise(function(r,s){t.then(function(o){return e?s(cv):r(o)}),t.catch(s)});return n.cancel=function(){return e=!0},n}var dv=["monaco"],uv=ev.create({config:tv,isInitialized:!1,resolve:null,reject:null,monaco:null}),xu=$y(uv,2),Ds=xu[0],Yo=xu[1];function fv(t){var e=av.config(t),n=e.monaco,r=Ly(e,dv);Yo(function(s){return{config:vu(s.config,r),monaco:n}})}function hv(){var t=Ds(function(e){var n=e.monaco,r=e.isInitialized,s=e.resolve;return{monaco:n,isInitialized:r,resolve:s}});if(!t.isInitialized){if(Yo({isInitialized:!0}),t.monaco)return t.resolve(t.monaco),al(ll);if(window.monaco&&window.monaco.editor)return wu(window.monaco),t.resolve(window.monaco),al(ll);lv(gv,mv)(bv)}return al(ll)}function gv(t){return document.body.appendChild(t)}function pv(t){var e=document.createElement("script");return t&&(e.src=t),e}function mv(t){var e=Ds(function(r){var s=r.config,o=r.reject;return{config:s,reject:o}}),n=pv("".concat(e.config.paths.vs,"/loader.js"));return n.onload=function(){return t()},n.onerror=e.reject,n}function bv(){var t=Ds(function(n){var r=n.config,s=n.resolve,o=n.reject;return{config:r,resolve:s,reject:o}}),e=window.require;e.config(t.config),e(["vs/editor/editor.main"],function(n){var r=n.m;wu(r),t.resolve(r)},function(n){t.reject(n)})}function wu(t){Ds().monaco||Yo({monaco:t})}function yv(){return Ds(function(t){var e=t.monaco;return e})}var ll=new Promise(function(t,e){return Yo({resolve:t,reject:e})}),ku={config:fv,init:hv,__getMonacoInstance:yv},vv={wrapper:{display:"flex",position:"relative",textAlign:"initial"},fullWidth:{width:"100%"},hide:{display:"none"}},cl=vv,xv={container:{display:"flex",height:"100%",width:"100%",justifyContent:"center",alignItems:"center"}},wv=xv;function kv({children:t}){return v.createElement("div",{style:wv.container},t)}var Cv=kv,Sv=Cv;function _v({width:t,height:e,isEditorReady:n,loading:r,_ref:s,className:o,wrapperProps:i}){return v.createElement("section",{style:{...cl.wrapper,width:t,height:e},...i},!n&&v.createElement(Sv,null,r),v.createElement("div",{ref:s,style:{...cl.fullWidth,...!n&&cl.hide},className:o}))}var Ev=_v,Cu=v.memo(Ev);function Mv(t){v.useEffect(t,[])}var Su=Mv;function Nv(t,e,n=!0){let r=v.useRef(!0);v.useEffect(r.current||!n?()=>{r.current=!1}:t,e)}var ln=Nv;function Is(){}function Wr(t,e,n,r){return Ov(t,r)||Tv(t,e,n,r)}function Ov(t,e){return t.editor.getModel(_u(t,e))}function Tv(t,e,n,r){return t.editor.createModel(e,n,r?_u(t,r):void 0)}function _u(t,e){return t.Uri.parse(e)}function Av({original:t,modified:e,language:n,originalLanguage:r,modifiedLanguage:s,originalModelPath:o,modifiedModelPath:i,keepCurrentOriginalModel:a=!1,keepCurrentModifiedModel:l=!1,theme:c="light",loading:d="Loading...",options:u={},height:f="100%",width:g="100%",className:m,wrapperProps:p={},beforeMount:y=Is,onMount:b=Is}){let[_,x]=v.useState(!1),[E,S]=v.useState(!0),w=v.useRef(null),N=v.useRef(null),C=v.useRef(null),k=v.useRef(b),M=v.useRef(y),T=v.useRef(!1);Su(()=>{let D=ku.init();return D.then(A=>(N.current=A)&&S(!1)).catch(A=>A?.type!=="cancelation"&&console.error("Monaco initialization: error:",A)),()=>w.current?F():D.cancel()}),ln(()=>{if(w.current&&N.current){let D=w.current.getOriginalEditor(),A=Wr(N.current,t||"",r||n||"text",o||"");A!==D.getModel()&&D.setModel(A)}},[o],_),ln(()=>{if(w.current&&N.current){let D=w.current.getModifiedEditor(),A=Wr(N.current,e||"",s||n||"text",i||"");A!==D.getModel()&&D.setModel(A)}},[i],_),ln(()=>{let D=w.current.getModifiedEditor();D.getOption(N.current.editor.EditorOption.readOnly)?D.setValue(e||""):e!==D.getValue()&&(D.executeEdits("",[{range:D.getModel().getFullModelRange(),text:e||"",forceMoveMarkers:!0}]),D.pushUndoStop())},[e],_),ln(()=>{w.current?.getModel()?.original.setValue(t||"")},[t],_),ln(()=>{let{original:D,modified:A}=w.current.getModel();N.current.editor.setModelLanguage(D,r||n||"text"),N.current.editor.setModelLanguage(A,s||n||"text")},[n,r,s],_),ln(()=>{N.current?.editor.setTheme(c)},[c],_),ln(()=>{w.current?.updateOptions(u)},[u],_);let L=v.useCallback(()=>{if(!N.current)return;M.current(N.current);let D=Wr(N.current,t||"",r||n||"text",o||""),A=Wr(N.current,e||"",s||n||"text",i||"");w.current?.setModel({original:D,modified:A})},[n,e,s,t,r,o,i]),P=v.useCallback(()=>{!T.current&&C.current&&(w.current=N.current.editor.createDiffEditor(C.current,{automaticLayout:!0,...u}),L(),N.current?.editor.setTheme(c),x(!0),T.current=!0)},[u,c,L]);v.useEffect(()=>{_&&k.current(w.current,N.current)},[_]),v.useEffect(()=>{!E&&!_&&P()},[E,_,P]);function F(){let D=w.current?.getModel();a||D?.original?.dispose(),l||D?.modified?.dispose(),w.current?.dispose()}return v.createElement(Cu,{width:g,height:f,isEditorReady:_,loading:d,_ref:C,className:m,wrapperProps:p})}var Dv=Av;v.memo(Dv);function Iv(t){let e=v.useRef();return v.useEffect(()=>{e.current=t},[t]),e.current}var Lv=Iv,Xo=new Map;function Pv({defaultValue:t,defaultLanguage:e,defaultPath:n,value:r,language:s,path:o,theme:i="light",line:a,loading:l="Loading...",options:c={},overrideServices:d={},saveViewState:u=!0,keepCurrentModel:f=!1,width:g="100%",height:m="100%",className:p,wrapperProps:y={},beforeMount:b=Is,onMount:_=Is,onChange:x,onValidate:E=Is}){let[S,w]=v.useState(!1),[N,C]=v.useState(!0),k=v.useRef(null),M=v.useRef(null),T=v.useRef(null),L=v.useRef(_),P=v.useRef(b),F=v.useRef(),D=v.useRef(r),A=Lv(o),O=v.useRef(!1),V=v.useRef(!1);Su(()=>{let j=ku.init();return j.then(U=>(k.current=U)&&C(!1)).catch(U=>U?.type!=="cancelation"&&console.error("Monaco initialization: error:",U)),()=>M.current?W():j.cancel()}),ln(()=>{let j=Wr(k.current,t||r||"",e||s||"",o||n||"");j!==M.current?.getModel()&&(u&&Xo.set(A,M.current?.saveViewState()),M.current?.setModel(j),u&&M.current?.restoreViewState(Xo.get(o)))},[o],S),ln(()=>{M.current?.updateOptions(c)},[c],S),ln(()=>{!M.current||r===void 0||(M.current.getOption(k.current.editor.EditorOption.readOnly)?M.current.setValue(r):r!==M.current.getValue()&&(V.current=!0,M.current.executeEdits("",[{range:M.current.getModel().getFullModelRange(),text:r,forceMoveMarkers:!0}]),M.current.pushUndoStop(),V.current=!1))},[r],S),ln(()=>{let j=M.current?.getModel();j&&s&&k.current?.editor.setModelLanguage(j,s)},[s],S),ln(()=>{a!==void 0&&M.current?.revealLine(a)},[a],S),ln(()=>{k.current?.editor.setTheme(i)},[i],S);let H=v.useCallback(()=>{if(!(!T.current||!k.current)&&!O.current){P.current(k.current);let j=o||n,U=Wr(k.current,r||t||"",e||s||"",j||"");M.current=k.current?.editor.create(T.current,{model:U,automaticLayout:!0,...c},d),u&&M.current.restoreViewState(Xo.get(j)),k.current.editor.setTheme(i),a!==void 0&&M.current.revealLine(a),w(!0),O.current=!0}},[t,e,n,r,s,o,c,d,u,i,a]);v.useEffect(()=>{S&&L.current(M.current,k.current)},[S]),v.useEffect(()=>{!N&&!S&&H()},[N,S,H]),D.current=r,v.useEffect(()=>{S&&x&&(F.current?.dispose(),F.current=M.current?.onDidChangeModelContent(j=>{V.current||x(M.current.getValue(),j)}))},[S,x]),v.useEffect(()=>{if(S){let j=k.current.editor.onDidChangeMarkers(U=>{let B=M.current.getModel()?.uri;if(B&&U.find(G=>G.path===B.path)){let G=k.current.editor.getModelMarkers({resource:B});E?.(G)}});return()=>{j?.dispose()}}return()=>{}},[S,E]);function W(){F.current?.dispose(),f?u&&Xo.set(o,M.current.saveViewState()):M.current.getModel()?.dispose(),M.current.dispose()}return v.createElement(Cu,{width:g,height:m,isEditorReady:S,loading:l,_ref:T,className:p,wrapperProps:y})}var $v=Pv,Fv=v.memo($v),Vv=Fv;const zv=({value:t,onChange:e,language:n="json",height:r="100%",readOnly:s=!1,theme:o="vs-dark"})=>{const i=v.useRef(null),a=(c,d)=>{i.current=c,n==="json"&&d.languages.json.jsonDefaults.setDiagnosticsOptions({validate:!0,schemaValidation:"error",allowComments:!1}),c.updateOptions({readOnly:s,minimap:{enabled:!1},scrollBeyondLastLine:!1,fontSize:14,lineNumbers:"on",renderWhitespace:"selection",automaticLayout:!0})},l=c=>{e&&c!==void 0&&e({target:{value:c}})};return v.useEffect(()=>{i.current&&i.current.updateOptions({readOnly:s})},[s]),h.jsx("div",{className:"border border-gray-300 dark:border-gray-700 rounded-lg overflow-hidden",children:h.jsx(Vv,{height:r,language:n,value:t,theme:o,onChange:l,onMount:a,options:{readOnly:s,minimap:{enabled:!1},scrollBeyondLastLine:!1,fontSize:14,lineNumbers:"on",renderWhitespace:"selection",automaticLayout:!0}})})},Hv=t=>{const e=new Date(t);return new Intl.DateTimeFormat("ko-KR",{year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}).format(e)},Eu=v.memo(({version:t,isSelected:e,onSelect:n})=>{const r=v.useCallback(()=>{n?.(t.id)},[n,t.id]);return h.jsx(Fa,{role:"listitem","aria-selected":e,className:` p-3 rounded-lg border cursor-pointer transition-colors - ${t?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"} - `,onClick:r,children:h.jsxs(I,{className:"flex justify-between items-start",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Q,{className:"font-semibold text-gray-900 dark:text-gray-100",children:["버전 ",e.version]}),h.jsx(Q,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:cx(e.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(Q,{className:"text-green-600 dark:text-green-400",children:["+",e.changes_summary.added]}),h.jsxs(Q,{className:"text-red-600 dark:text-red-400",children:["-",e.changes_summary.removed]})]})]})})});$u.displayName="VersionListItem";const Fu=({versions:e,selectedId:t,onSelect:n})=>e.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx(ja,{role:"list",className:"space-y-2",children:e.map(r=>h.jsx($u,{version:r,isSelected:t===r.id,onSelect:n},r.id))}),dx=({layoutId:e,versions:t,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(Fu,{versions:t,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(ae,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},ux=({emailLabel:e="Email",passwordLabel:t="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[_,x]=v.useState(""),[M,C]=v.useState(""),[k,N]=v.useState({}),S=p,w=Object.values(f).some($=>$===!0),E=g||w,D=m||E,P=()=>{const $={},A=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!_||_.trim()===""?$.email=l:A.test(_)||($.email=c),(!M||M.trim()==="")&&($.password=d),N($),Object.keys($).length===0},L=async $=>{if(!P()){$.preventDefault();return}N({}),u?await u($):$.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(Ga,{onSubmit:L,className:"space-y-6",noValidate:!0,children:[S&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(vt,{className:"text-sm text-red-600 dark:text-red-400",children:S})}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:e}),h.jsx(St,{id:"email",name:"email",type:"email",value:_,onChange:$=>x($.target.value),placeholder:s,disabled:D,className:` + ${e?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 hover:border-gray-400 dark:border-gray-600 dark:hover:border-gray-500"} + `,onClick:r,children:h.jsxs(I,{className:"flex justify-between items-start",children:[h.jsxs(I,{className:"flex-1",children:[h.jsxs(Q,{className:"font-semibold text-gray-900 dark:text-gray-100",children:["버전 ",t.version]}),h.jsx(Q,{className:"ml-2 text-sm text-gray-500 dark:text-gray-400",children:Hv(t.created_at)})]}),h.jsxs(I,{className:"flex gap-3 text-sm",children:[h.jsxs(Q,{className:"text-green-600 dark:text-green-400",children:["+",t.changes_summary.added]}),h.jsxs(Q,{className:"text-red-600 dark:text-red-400",children:["-",t.changes_summary.removed]})]})]})})});Eu.displayName="VersionListItem";const Mu=({versions:t,selectedId:e,onSelect:n})=>t.length===0?h.jsx(I,{className:"p-4 text-center text-gray-500 dark:text-gray-400",children:"버전이 없습니다."}):h.jsx($a,{role:"list",className:"space-y-2",children:t.map(r=>h.jsx(Eu,{version:r,isSelected:e===r.id,onSelect:n},r.id))}),Uv=({layoutId:t,versions:e,onRestore:n})=>{const[r,s]=v.useState(),o=l=>{s(l)},i=()=>{r!==void 0&&n(r)},a=r===void 0;return h.jsxs(I,{className:"space-y-4",role:"region","aria-label":"레이아웃 히스토리 패널",children:[h.jsx(I,{className:"border border-gray-300 dark:border-gray-600 rounded-lg p-4",children:h.jsx(Mu,{versions:e,selectedId:r,onSelect:o})}),h.jsx(I,{className:"flex justify-end",children:h.jsx(oe,{variant:"primary",disabled:a,onClick:i,"aria-label":"선택한 버전으로 복원",children:"복원"})})]})},Bv=({emailLabel:t="Email",passwordLabel:e="Password",submitButtonText:n="Sign In",processingText:r="Processing...",emailPlaceholder:s="Email",passwordPlaceholder:o="Password",forgotPasswordText:i="Forgot your password?",forgotPasswordUrl:a="/admin/password/reset",emailRequiredError:l="Please enter your email.",emailInvalidError:c="Please enter a valid email address.",passwordRequiredError:d="Please enter your password.",onSubmit:u,loadingActions:f={},isLoading:g=!1,disabled:m=!1,apiError:p="",className:y="",style:b})=>{const[_,x]=v.useState(""),[E,S]=v.useState(""),[w,N]=v.useState({}),C=p,k=Object.values(f).some(F=>F===!0),M=g||k,T=m||M,L=()=>{const F={},D=/^[^\s@]+@[^\s@]+\.[^\s@]+$/;return!_||_.trim()===""?F.email=l:D.test(_)||(F.email=c),(!E||E.trim()==="")&&(F.password=d),N(F),Object.keys(F).length===0},P=async F=>{if(!L()){F.preventDefault();return}N({}),u?await u(F):F.preventDefault()};return h.jsx(I,{className:`w-full ${y}`,style:b,children:h.jsxs(Va,{onSubmit:P,className:"space-y-6",noValidate:!0,children:[C&&h.jsx(I,{className:"p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800",children:h.jsx(yt,{className:"text-sm text-red-600 dark:text-red-400",children:C})}),h.jsxs(I,{children:[h.jsx(on,{htmlFor:"email",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(wt,{id:"email",name:"email",type:"email",value:_,onChange:F=>x(F.target.value),placeholder:s,disabled:T,className:` w-full px-4 py-3 rounded-lg border - ${k.email?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"} + ${w.email?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"} bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 - `}),k.email&&h.jsx(vt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.email})]}),h.jsxs(I,{children:[h.jsx(ln,{htmlFor:"password",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:t}),h.jsx(St,{id:"password",name:"password",type:"password",value:M,onChange:$=>C($.target.value),placeholder:o,disabled:D,className:` + `}),w.email&&h.jsx(yt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:w.email})]}),h.jsxs(I,{children:[h.jsx(on,{htmlFor:"password",className:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2",children:e}),h.jsx(wt,{id:"password",name:"password",type:"password",value:E,onChange:F=>S(F.target.value),placeholder:o,disabled:T,className:` w-full px-4 py-3 rounded-lg border - ${k.password?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"} + ${w.password?"border-red-500 focus:border-red-500 focus:ring-red-500":"border-gray-300 dark:border-gray-600 focus:border-blue-500 focus:ring-blue-500"} bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 - `}),k.password&&h.jsx(vt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:k.password})]}),a&&h.jsx(I,{className:"flex items-center justify-end",children:h.jsx(Yr,{href:a,className:"text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 transition-colors duration-200",children:i})}),h.jsx(ae,{type:"submit",disabled:D,className:` + `}),w.password&&h.jsx(yt,{className:"mt-2 text-sm text-red-600 dark:text-red-400",children:w.password})]}),a&&h.jsx(I,{className:"flex items-center justify-end",children:h.jsx(Fr,{href:a,className:"text-sm text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 transition-colors duration-200",children:i})}),h.jsx(oe,{type:"submit",disabled:T,className:` w-full px-4 py-3 rounded-lg font-medium bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 @@ -147,16 +146,16 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200 flex items-center justify-center - `,children:E?h.jsxs(h.Fragment,{children:[h.jsxs(Zn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},zu=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,yl=e=>{const t=zu(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},fx=()=>{const e=localStorage.getItem("g7_color_scheme");return e&&["auto","light","dark"].includes(e)?e:"auto"},hx=({onThemeChange:e,className:t="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=fx(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{yl(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{yl("auto")};return p.addEventListener("change",y),()=>p.removeEventListener("change",y)},[l]),v.useEffect(()=>{const p=y=>{f.current&&!f.current.contains(y.target)&&u(!1)};return document.addEventListener("mousedown",p),()=>document.removeEventListener("mousedown",p)},[]);const g=p=>{c(p),localStorage.setItem("g7_color_scheme",p),yl(p),u(!1),e?.(p)},m=()=>zu(l)==="dark"?de.Moon:de.Sun;return h.jsxs(I,{ref:f,className:`relative ${t}`,id:o,...i,children:[h.jsx(ae,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(oe,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(ae,{onClick:()=>g("auto"),className:` + `,children:M?h.jsxs(h.Fragment,{children:[h.jsxs(Wn,{className:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",children:[h.jsx("circle",{className:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor",strokeWidth:"4"}),h.jsx("path",{className:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})]}),r]}):n})]})})},Nu=t=>t==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t,dl=t=>{const e=Nu(t);document.documentElement.setAttribute("data-theme",e),e==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},jv=()=>{const t=localStorage.getItem("g7_color_scheme");return t&&["auto","light","dark"].includes(t)?t:"auto"},Wv=({onThemeChange:t,className:e="",autoText:n="System",lightText:r="Light",darkText:s="Dark",id:o,editorAttrs:i})=>{const a=jv(),[l,c]=v.useState(a),[d,u]=v.useState(!1),f=v.useRef(null);v.useEffect(()=>{dl(l)},[]),v.useEffect(()=>{if(l!=="auto")return;const p=window.matchMedia("(prefers-color-scheme: dark)"),y=()=>{dl("auto")};return p.addEventListener("change",y),()=>p.removeEventListener("change",y)},[l]),v.useEffect(()=>{const p=y=>{f.current&&!f.current.contains(y.target)&&u(!1)};return document.addEventListener("mousedown",p),()=>document.removeEventListener("mousedown",p)},[]);const g=p=>{c(p),localStorage.setItem("g7_color_scheme",p),dl(p),u(!1),t?.(p)},m=()=>Nu(l)==="dark"?de.Moon:de.Sun;return h.jsxs(I,{ref:f,className:`relative ${e}`,id:o,...i,children:[h.jsx(oe,{onClick:()=>u(!d),className:"w-9 h-9 flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors","aria-label":"Toggle theme",children:h.jsx(re,{name:m(),className:"text-sm text-slate-500 dark:text-slate-400"})}),d&&h.jsx(I,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:h.jsxs(I,{className:"py-2",children:[h.jsxs(oe,{onClick:()=>g("auto"),className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${l==="auto"?"bg-gray-50 dark:bg-gray-700":""} - `,children:[h.jsx(oe,{name:de.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),l==="auto"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("light"),className:` + `,children:[h.jsx(re,{name:de.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),l==="auto"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(oe,{onClick:()=>g("light"),className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${l==="light"?"bg-gray-50 dark:bg-gray-700":""} - `,children:[h.jsx(oe,{name:de.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),l==="light"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(ae,{onClick:()=>g("dark"),className:` + `,children:[h.jsx(re,{name:de.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),l==="light"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),h.jsxs(oe,{onClick:()=>g("dark"),className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${l==="dark"?"bg-gray-50 dark:bg-gray-700":""} - `,children:[h.jsx(oe,{name:de.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(oe,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Vu=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...e)=>console.log("[Comp:ColumnSelector]",...e),warn:(...e)=>console.warn("[Comp:ColumnSelector]",...e),error:(...e)=>console.error("[Comp:ColumnSelector]",...e)},Hu=(e,t)=>window.G7Core?.t?.(e,t)??e,gx=()=>window.G7Core,px=({id:e,columns:t,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=de.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const _=s??Hu("common.column_selector"),x=c??Hu("common.required"),M=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return t;if(y){const V=t.findIndex(X=>X.field===y);if(V!==-1)return[...t.slice(0,V+1),...p,...t.slice(V+1)]}return[...t,...p]},[t,p,y]),[C,k]=v.useState(!1),N=v.useRef(null),S=v.useRef(null),[w,E]=v.useState(!1),[D,P]=v.useState(i),L=v.useMemo(()=>{try{const V=window.G7Core?.AuthManager?.getInstance();return V?V.getUser()?.uuid:void 0}catch{return}},[]),$=e?`g7_columns_${e}${L!==void 0?`_${L}`:""}`:null,A=()=>{if(!$)return null;try{const V=localStorage.getItem($);if(V){const X=JSON.parse(V);if(Array.isArray(X)&&X.length>0)return X}}catch(V){Vu.warn("Failed to load from localStorage:",V)}return null},O=V=>{if($)try{localStorage.setItem($,JSON.stringify(V))}catch(X){Vu.warn("Failed to save to localStorage:",X)}},T=()=>{const V=A();return V||(n&&n.length>0?n:f&&f.length>0?f:M.filter(X=>X.visible!==!1).map(X=>X.field))},[z,H]=v.useState(T);v.useEffect(()=>{w||(r&&z.length>0&&r(z),E(!0))},[w,r,z]),v.useEffect(()=>{w&&n&&n.length>0&&(A()||H(n))},[n,w]);const j=v.useRef([]),G=v.useRef(null);v.useEffect(()=>{if(w&&p&&p.length>0){const V=p.map(q=>q.field);if(G.current===null){const q=A();if(G.current=q!==null,q!==null){j.current=V;return}}const X=j.current,K=V.filter(q=>!X.includes(q));if(K.length>0){const q=n&&n.length>0?n:z,ne=K.filter(ee=>!q.includes(ee));if(ne.length>0){const ee=[...q,...ne];H(ee),O(ee),r?.(ee)}}j.current=V}},[p,w]);const U=n&&n.length>0?n:z;v.useEffect(()=>{const V=X=>{N.current&&!N.current.contains(X.target)&&k(!1)};return C&&document.addEventListener("mousedown",V),()=>{document.removeEventListener("mousedown",V)}},[C]),v.useEffect(()=>{if(C&&S.current&&N.current){const V=S.current.getBoundingClientRect(),X=N.current.getBoundingClientRect(),K=window.innerWidth,q=V.width||256;i==="right"?X.right-q<0?P("left"):P("right"):X.left+q>K?P("right"):P("left")}},[C,i]);const W=V=>{if(M.find(q=>q.field===V)?.required)return;const K=U.includes(V)?U.filter(q=>q!==V):[...U,V];H(K),O(K),r?.(K)},Y=D==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:e,...b,children:[h.jsxs(ae,{onClick:()=>k(!C),className:gx()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(oe,{name:o,className:"w-5 h-5"}),_&&h.jsx(Q,{className:`${m||"hidden sm:inline"} text-sm`,children:_})]}),C&&h.jsxs(I,{ref:S,className:`absolute ${Y} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(Q,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:M.map(V=>{const X=U.includes(V.field),K=V.required===!0;return h.jsxs(I,{onClick:()=>!K&&W(V.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${K?"cursor-default":"cursor-pointer"}`,children:[h.jsx(En,{checked:X,disabled:K,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:V.header}),K&&x&&h.jsx(Q,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},V.field)})})]})]})},mx=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...e)=>console.log("[Comp:PageTransition]",...e),warn:(...e)=>console.warn("[Comp:PageTransition]",...e),error:(...e)=>console.error("[Comp:PageTransition]",...e)},bx=({className:e="",style:t})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{const s=window.G7Core?.TransitionManager;if(!s){mx.warn("TransitionManager를 찾을 수 없습니다.");return}return r(s.getIsPending()),s.subscribe(i=>{r(i)})},[]),n?h.jsxs(I,{className:`fixed top-0 left-0 right-0 z-[999] ${e}`,style:t,role:"progressbar","aria-label":"페이지 로딩 중",children:[h.jsx(I,{className:"h-1 bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 animate-pulse",children:h.jsx(I,{className:"h-full bg-blue-700 animate-[loading_1.5s_ease-in-out_infinite]",style:{width:"30%",animation:"loading 1.5s ease-in-out infinite"}})}),h.jsx("style",{children:` + `,children:[h.jsx(re,{name:de.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"flex-1 text-left text-gray-900 dark:text-white",children:s}),l==="dark"&&h.jsx(re,{name:de.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Ou=window.G7Core?.createLogger?.("Comp:ColumnSelector")??{log:(...t)=>console.log("[Comp:ColumnSelector]",...t),warn:(...t)=>console.warn("[Comp:ColumnSelector]",...t),error:(...t)=>console.error("[Comp:ColumnSelector]",...t)},Tu=(t,e)=>window.G7Core?.t?.(t,e)??t,Gv=()=>window.G7Core,Yv=({id:t,columns:e,visibleColumns:n,onColumnVisibilityChange:r,triggerLabel:s,triggerIconName:o=de.Sliders,position:i="right",className:a="",style:l,requiredText:c,menuTitle:d,menuDescription:u,defaultColumns:f,triggerClassName:g,labelClassName:m,dynamicColumns:p,dynamicColumnsInsertAfter:y,editorAttrs:b})=>{const _=s??Tu("common.column_selector"),x=c??Tu("common.required"),E=v.useMemo(()=>{if(!p||!Array.isArray(p)||p.length===0)return e;if(y){const z=e.findIndex(X=>X.field===y);if(z!==-1)return[...e.slice(0,z+1),...p,...e.slice(z+1)]}return[...e,...p]},[e,p,y]),[S,w]=v.useState(!1),N=v.useRef(null),C=v.useRef(null),[k,M]=v.useState(!1),[T,L]=v.useState(i),P=v.useMemo(()=>{try{const z=window.G7Core?.AuthManager?.getInstance();return z?z.getUser()?.uuid:void 0}catch{return}},[]),F=t?`g7_columns_${t}${P!==void 0?`_${P}`:""}`:null,D=()=>{if(!F)return null;try{const z=localStorage.getItem(F);if(z){const X=JSON.parse(z);if(Array.isArray(X)&&X.length>0)return X}}catch(z){Ou.warn("Failed to load from localStorage:",z)}return null},A=z=>{if(F)try{localStorage.setItem(F,JSON.stringify(z))}catch(X){Ou.warn("Failed to save to localStorage:",X)}},O=()=>{const z=D();return z||(n&&n.length>0?n:f&&f.length>0?f:E.filter(X=>X.visible!==!1).map(X=>X.field))},[V,H]=v.useState(O);v.useEffect(()=>{k||(r&&V.length>0&&r(V),M(!0))},[k,r,V]),v.useEffect(()=>{k&&n&&n.length>0&&(D()||H(n))},[n,k]);const W=v.useRef([]),j=v.useRef(null);v.useEffect(()=>{if(k&&p&&p.length>0){const z=p.map(Z=>Z.field);if(j.current===null){const Z=D();if(j.current=Z!==null,Z!==null){W.current=z;return}}const X=W.current,Y=z.filter(Z=>!X.includes(Z));if(Y.length>0){const Z=n&&n.length>0?n:V,ee=Y.filter(te=>!Z.includes(te));if(ee.length>0){const te=[...Z,...ee];H(te),A(te),r?.(te)}}W.current=z}},[p,k]);const U=n&&n.length>0?n:V;v.useEffect(()=>{const z=X=>{N.current&&!N.current.contains(X.target)&&w(!1)};return S&&document.addEventListener("mousedown",z),()=>{document.removeEventListener("mousedown",z)}},[S]),v.useEffect(()=>{if(S&&C.current&&N.current){const z=C.current.getBoundingClientRect(),X=N.current.getBoundingClientRect(),Y=window.innerWidth,Z=z.width||256;i==="right"?X.right-Z<0?L("left"):L("right"):X.left+Z>Y?L("right"):L("left")}},[S,i]);const B=z=>{if(E.find(Z=>Z.field===z)?.required)return;const Y=U.includes(z)?U.filter(Z=>Z!==z):[...U,z];H(Y),A(Y),r?.(Y)},G=T==="left"?"left-0":"right-0";return h.jsxs(I,{ref:N,className:`relative inline-block ${a}`,style:l,id:t,...b,children:[h.jsxs(oe,{onClick:()=>w(!S),className:Gv()?.style?.mergeClasses?.("flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors",g)??`flex items-center gap-2 px-3 py-2 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors ${g??""}`,children:[h.jsx(re,{name:o,className:"w-5 h-5"}),_&&h.jsx(Q,{className:`${m||"hidden sm:inline"} text-sm`,children:_})]}),S&&h.jsxs(I,{ref:C,className:`absolute ${G} z-50 mt-2 w-64 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg`,children:[(d||u)&&h.jsxs(I,{className:"px-4 pt-3 pb-2 border-b border-gray-200 dark:border-gray-700",children:[d&&h.jsx(Q,{className:"block text-sm font-medium text-gray-900 dark:text-white",children:d}),u&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-1",children:u})]}),h.jsx(I,{className:"p-2 max-h-64 overflow-y-auto",children:E.map(z=>{const X=U.includes(z.field),Y=z.required===!0;return h.jsxs(I,{onClick:()=>!Y&&B(z.field),className:`flex items-center gap-2 px-3 py-2 rounded hover:bg-gray-50 dark:hover:bg-gray-700 ${Y?"cursor-default":"cursor-pointer"}`,children:[h.jsx(Cn,{checked:X,disabled:Y,readOnly:!0,className:"h-4 w-4 text-blue-600 rounded border-gray-300 dark:border-gray-600 pointer-events-none"}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300",children:z.header}),Y&&x&&h.jsx(Q,{className:"ml-auto text-xs text-gray-400 dark:text-gray-500",children:x})]},z.field)})})]})]})},Xv=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...t)=>console.log("[Comp:PageTransition]",...t),warn:(...t)=>console.warn("[Comp:PageTransition]",...t),error:(...t)=>console.error("[Comp:PageTransition]",...t)},Kv=({className:t="",style:e})=>{const[n,r]=v.useState(!1);return v.useEffect(()=>{const s=window.G7Core?.TransitionManager;if(!s){Xv.warn("TransitionManager를 찾을 수 없습니다.");return}return r(s.getIsPending()),s.subscribe(i=>{r(i)})},[]),n?h.jsxs(I,{className:`fixed top-0 left-0 right-0 z-[999] ${t}`,style:e,role:"progressbar","aria-label":"페이지 로딩 중",children:[h.jsx(I,{className:"h-1 bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 animate-pulse",children:h.jsx(I,{className:"h-full bg-blue-700 animate-[loading_1.5s_ease-in-out_infinite]",style:{width:"30%",animation:"loading 1.5s ease-in-out infinite"}})}),h.jsx("style",{children:` @keyframes loading { 0% { transform: translateX(-100%); @@ -168,12 +167,19 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho transform: translateX(-100%); } } - `})]}):null},yx=(e,t)=>window.G7Core?.t?.(e,t)??e,vx=({options:e})=>{const t=document.documentElement.classList.contains("dark"),n=t?"rgb(17,24,39)":"rgb(249,250,251)",r=t?"#6b7280":"#9ca3af",s=t?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:e?.text||yx("nav.loading")})]})};function Cr(e){"@babel/helpers - typeof";return Cr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},Cr(e)}function xx(e,t){if(Cr(e)!="object"||!e)return e;var n=e[Symbol.toPrimitive];if(n!==void 0){var r=n.call(e,t);if(Cr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(t==="string"?String:Number)(e)}function Uu(e){var t=xx(e,"string");return Cr(t)=="symbol"?t:t+""}function Ws(e,t,n){return(t=Uu(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function Bu(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(e,s).enumerable})),n.push.apply(n,r)}return n}function Ee(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?arguments[t]:{};t%2?Bu(Object(n),!0).forEach(function(r){Ws(e,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):Bu(Object(n)).forEach(function(r){Object.defineProperty(e,r,Object.getOwnPropertyDescriptor(n,r))})}return e}function wx(e){if(Array.isArray(e))return e}function kx(e,t){var n=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(e)).next,t===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==t);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function vl(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n<t;n++)r[n]=e[n];return r}function ju(e,t){if(e){if(typeof e=="string")return vl(e,t);var n={}.toString.call(e).slice(8,-1);return n==="Object"&&e.constructor&&(n=e.constructor.name),n==="Map"||n==="Set"?Array.from(e):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?vl(e,t):void 0}}function Cx(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Qn(e,t){return wx(e)||kx(e,t)||ju(e,t)||Cx()}function Sx(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}function Vn(e,t){if(e==null)return{};var n,r,s=Sx(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(r=0;r<o.length;r++)n=o[r],t.indexOf(n)===-1&&{}.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var _x=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Wu(e){var t=e.defaultInputValue,n=t===void 0?"":t,r=e.defaultMenuIsOpen,s=r===void 0?!1:r,o=e.defaultValue,i=o===void 0?null:o,a=e.inputValue,l=e.menuIsOpen,c=e.onChange,d=e.onInputChange,u=e.onMenuClose,f=e.onMenuOpen,g=e.value,m=Vn(e,_x),p=v.useState(a!==void 0?a:n),y=Qn(p,2),b=y[0],_=y[1],x=v.useState(l!==void 0?l:s),M=Qn(x,2),C=M[0],k=M[1],N=v.useState(g!==void 0?g:i),S=Qn(N,2),w=S[0],E=S[1],D=v.useCallback(function(z,H){typeof c=="function"&&c(z,H),E(z)},[c]),P=v.useCallback(function(z,H){var j;typeof d=="function"&&(j=d(z,H)),_(j!==void 0?j:z)},[d]),L=v.useCallback(function(){typeof f=="function"&&f(),k(!0)},[f]),$=v.useCallback(function(){typeof u=="function"&&u(),k(!1)},[u]),A=a!==void 0?a:b,O=l!==void 0?l:C,T=g!==void 0?g:w;return Ee(Ee({},m),{},{inputValue:A,menuIsOpen:O,onChange:D,onInputChange:P,onMenuClose:$,onMenuOpen:L,value:T})}function Oe(){return Oe=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)({}).hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},Oe.apply(null,arguments)}function Ex(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Gu(e,t){for(var n=0;n<t.length;n++){var r=t[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(e,Uu(r.key),r)}}function Mx(e,t,n){return t&&Gu(e.prototype,t),n&&Gu(e,n),Object.defineProperty(e,"prototype",{writable:!1}),e}function xl(e,t){return xl=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},xl(e,t)}function Nx(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),Object.defineProperty(e,"prototype",{writable:!1}),t&&xl(e,t)}function ri(e){return ri=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},ri(e)}function Yu(){try{var e=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(Yu=function(){return!!e})()}function Tx(e){if(e===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function Ox(e,t){if(t&&(Cr(t)=="object"||typeof t=="function"))return t;if(t!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return Tx(e)}function Ax(e){var t=Yu();return function(){var n,r=ri(e);if(t){var s=ri(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return Ox(this,n)}}function Dx(e){if(Array.isArray(e))return vl(e)}function Ix(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function Lx(){throw new TypeError(`Invalid attempt to spread non-iterable instance. -In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function ns(e){return Dx(e)||Ix(e)||ju(e)||Lx()}function Px(e){if(e.sheet)return e.sheet;for(var t=0;t<document.styleSheets.length;t++)if(document.styleSheets[t].ownerNode===e)return document.styleSheets[t]}function $x(e){var t=document.createElement("style");return t.setAttribute("data-emotion",e.key),e.nonce!==void 0&&t.setAttribute("nonce",e.nonce),t.appendChild(document.createTextNode("")),t.setAttribute("data-s",""),t}var Fx=(function(){function e(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var t=e.prototype;return t.hydrate=function(r){r.forEach(this._insertTag)},t.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag($x(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=Px(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},t.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},e})(),jt="-ms-",si="-moz-",nt="-webkit-",Xu="comm",wl="rule",kl="decl",zx="@import",Ku="@keyframes",Vx="@layer",Hx=Math.abs,oi=String.fromCharCode,Ux=Object.assign;function Bx(e,t){return Vt(e,0)^45?(((t<<2^Vt(e,0))<<2^Vt(e,1))<<2^Vt(e,2))<<2^Vt(e,3):0}function qu(e){return e.trim()}function jx(e,t){return(e=t.exec(e))?e[0]:e}function rt(e,t,n){return e.replace(t,n)}function Cl(e,t){return e.indexOf(t)}function Vt(e,t){return e.charCodeAt(t)|0}function Gs(e,t,n){return e.slice(t,n)}function Hn(e){return e.length}function Sl(e){return e.length}function ii(e,t){return t.push(e),e}function Wx(e,t){return e.map(t).join("")}var ai=1,rs=1,Zu=0,tn=0,Et=0,ss="";function li(e,t,n,r,s,o,i){return{value:e,root:t,parent:n,type:r,props:s,children:o,line:ai,column:rs,length:i,return:""}}function Ys(e,t){return Ux(li("",null,null,"",null,null,0),e,{length:-e.length},t)}function Gx(){return Et}function Yx(){return Et=tn>0?Vt(ss,--tn):0,rs--,Et===10&&(rs=1,ai--),Et}function dn(){return Et=tn<Zu?Vt(ss,tn++):0,rs++,Et===10&&(rs=1,ai++),Et}function Un(){return Vt(ss,tn)}function ci(){return tn}function Xs(e,t){return Gs(ss,e,t)}function Ks(e){switch(e){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function Qu(e){return ai=rs=1,Zu=Hn(ss=e),tn=0,[]}function Ju(e){return ss="",e}function di(e){return qu(Xs(tn-1,_l(e===91?e+2:e===40?e+1:e)))}function Xx(e){for(;(Et=Un())&&Et<33;)dn();return Ks(e)>2||Ks(Et)>3?"":" "}function Kx(e,t){for(;--t&&dn()&&!(Et<48||Et>102||Et>57&&Et<65||Et>70&&Et<97););return Xs(e,ci()+(t<6&&Un()==32&&dn()==32))}function _l(e){for(;dn();)switch(Et){case e:return tn;case 34:case 39:e!==34&&e!==39&&_l(Et);break;case 40:e===41&&_l(e);break;case 92:dn();break}return tn}function qx(e,t){for(;dn()&&e+Et!==57;)if(e+Et===84&&Un()===47)break;return"/*"+Xs(t,tn-1)+"*"+oi(e===47?e:dn())}function Zx(e){for(;!Ks(Un());)dn();return Xs(e,tn)}function Qx(e){return Ju(ui("",null,null,null,[""],e=Qu(e),0,[0],e))}function ui(e,t,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,_=0,x="",M=s,C=o,k=r,N=x;y;)switch(m=_,_=dn()){case 40:if(m!=108&&Vt(N,u-1)==58){Cl(N+=rt(di(_),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=di(_);break;case 9:case 10:case 13:case 32:N+=Xx(m);break;case 92:N+=Kx(ci()-1,7);continue;case 47:switch(Un()){case 42:case 47:ii(Jx(qx(dn(),ci()),t,n),l);break;default:N+="/"}break;case 123*p:a[c++]=Hn(N)*b;case 125*p:case 59:case 0:switch(_){case 0:case 125:y=0;case 59+d:b==-1&&(N=rt(N,/\f/g,"")),g>0&&Hn(N)-u&&ii(g>32?ef(N+";",r,n,u-1):ef(rt(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(ii(k=Ru(N,t,n,c,d,s,a,x,M=[],C=[],u),o),_===123)if(d===0)ui(N,t,k,k,M,o,u,a,C);else switch(f===99&&Vt(N,3)===110?100:f){case 100:case 108:case 109:case 115:ui(e,k,k,r&&ii(Ru(e,k,k,0,0,s,a,x,s,M=[],u),C),s,C,u,a,r?M:C);break;default:ui(N,k,k,k,[""],C,0,a,C)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+Hn(N),g=m;default:if(p<1){if(_==123)--p;else if(_==125&&p++==0&&Yx()==125)continue}switch(N+=oi(_),_*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(Hn(N)-1)*b,b=1;break;case 64:Un()===45&&(N+=di(dn())),f=Un(),d=u=Hn(x=N+=Zx(ci())),_++;break;case 45:m===45&&Hn(N)==2&&(p=0)}}return o}function Ru(e,t,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=Sl(f),m=0,p=0,y=0;m<r;++m)for(var b=0,_=Gs(e,u+1,u=Hx(p=i[m])),x=e;b<g;++b)(x=qu(p>0?f[b]+" "+_:rt(_,/&\f/g,f[b])))&&(l[y++]=x);return li(e,t,n,s===0?wl:a,l,c,d)}function Jx(e,t,n){return li(e,t,n,Xu,oi(Gx()),Gs(e,2,-2),0)}function ef(e,t,n,r){return li(e,t,n,kl,Gs(e,0,r),Gs(e,r+1,-1),r)}function os(e,t){for(var n="",r=Sl(e),s=0;s<r;s++)n+=t(e[s],s,e,t)||"";return n}function Rx(e,t,n,r){switch(e.type){case Vx:if(e.children.length)break;case zx:case kl:return e.return=e.return||e.value;case Xu:return"";case Ku:return e.return=e.value+"{"+os(e.children,r)+"}";case wl:e.value=e.props.join(",")}return Hn(n=os(e.children,r))?e.return=e.value+"{"+n+"}":""}function ew(e){var t=Sl(e);return function(n,r,s,o){for(var i="",a=0;a<t;a++)i+=e[a](n,r,s,o)||"";return i}}function tw(e){return function(t){t.root||(t=t.return)&&e(t)}}function nw(e){var t=Object.create(null);return function(n){return t[n]===void 0&&(t[n]=e(n)),t[n]}}var rw=function(t,n,r){for(var s=0,o=0;s=o,o=Un(),s===38&&o===12&&(n[r]=1),!Ks(o);)dn();return Xs(t,tn)},sw=function(t,n){var r=-1,s=44;do switch(Ks(s)){case 0:s===38&&Un()===12&&(n[r]=1),t[r]+=rw(tn-1,n,r);break;case 2:t[r]+=di(s);break;case 4:if(s===44){t[++r]=Un()===58?"&\f":"",n[r]=t[r].length;break}default:t[r]+=oi(s)}while(s=dn());return t},ow=function(t,n){return Ju(sw(Qu(t),n))},tf=new WeakMap,iw=function(t){if(!(t.type!=="rule"||!t.parent||t.length<1)){for(var n=t.value,r=t.parent,s=t.column===r.column&&t.line===r.line;r.type!=="rule";)if(r=r.parent,!r)return;if(!(t.props.length===1&&n.charCodeAt(0)!==58&&!tf.get(r))&&!s){tf.set(t,!0);for(var o=[],i=ow(n,o),a=r.props,l=0,c=0;l<i.length;l++)for(var d=0;d<a.length;d++,c++)t.props[c]=o[l]?i[l].replace(/&\f/g,a[d]):a[d]+" "+i[l]}}},aw=function(t){if(t.type==="decl"){var n=t.value;n.charCodeAt(0)===108&&n.charCodeAt(2)===98&&(t.return="",t.value="")}};function nf(e,t){switch(Bx(e,t)){case 5103:return nt+"print-"+e+e;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return nt+e+e;case 5349:case 4246:case 4810:case 6968:case 2756:return nt+e+si+e+jt+e+e;case 6828:case 4268:return nt+e+jt+e+e;case 6165:return nt+e+jt+"flex-"+e+e;case 5187:return nt+e+rt(e,/(\w+).+(:[^]+)/,nt+"box-$1$2"+jt+"flex-$1$2")+e;case 5443:return nt+e+jt+"flex-item-"+rt(e,/flex-|-self/,"")+e;case 4675:return nt+e+jt+"flex-line-pack"+rt(e,/align-content|flex-|-self/,"")+e;case 5548:return nt+e+jt+rt(e,"shrink","negative")+e;case 5292:return nt+e+jt+rt(e,"basis","preferred-size")+e;case 6060:return nt+"box-"+rt(e,"-grow","")+nt+e+jt+rt(e,"grow","positive")+e;case 4554:return nt+rt(e,/([^-])(transform)/g,"$1"+nt+"$2")+e;case 6187:return rt(rt(rt(e,/(zoom-|grab)/,nt+"$1"),/(image-set)/,nt+"$1"),e,"")+e;case 5495:case 3959:return rt(e,/(image-set\([^]*)/,nt+"$1$`$1");case 4968:return rt(rt(e,/(.+:)(flex-)?(.*)/,nt+"box-pack:$3"+jt+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+nt+e+e;case 4095:case 3583:case 4068:case 2532:return rt(e,/(.+)-inline(.+)/,nt+"$1$2")+e;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(Hn(e)-1-t>6)switch(Vt(e,t+1)){case 109:if(Vt(e,t+4)!==45)break;case 102:return rt(e,/(.+:)(.+)-([^]+)/,"$1"+nt+"$2-$3$1"+si+(Vt(e,t+3)==108?"$3":"$2-$3"))+e;case 115:return~Cl(e,"stretch")?nf(rt(e,"stretch","fill-available"),t)+e:e}break;case 4949:if(Vt(e,t+1)!==115)break;case 6444:switch(Vt(e,Hn(e)-3-(~Cl(e,"!important")&&10))){case 107:return rt(e,":",":"+nt)+e;case 101:return rt(e,/(.+:)([^;!]+)(;|!.+)?/,"$1"+nt+(Vt(e,14)===45?"inline-":"")+"box$3$1"+nt+"$2$3$1"+jt+"$2box$3")+e}break;case 5936:switch(Vt(e,t+11)){case 114:return nt+e+jt+rt(e,/[svh]\w+-[tblr]{2}/,"tb")+e;case 108:return nt+e+jt+rt(e,/[svh]\w+-[tblr]{2}/,"tb-rl")+e;case 45:return nt+e+jt+rt(e,/[svh]\w+-[tblr]{2}/,"lr")+e}return nt+e+jt+e+e}return e}var lw=function(t,n,r,s){if(t.length>-1&&!t.return)switch(t.type){case kl:t.return=nf(t.value,t.length);break;case Ku:return os([Ys(t,{value:rt(t.value,"@","@"+nt)})],s);case wl:if(t.length)return Wx(t.props,function(o){switch(jx(o,/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":return os([Ys(t,{props:[rt(o,/:(read-\w+)/,":"+si+"$1")]})],s);case"::placeholder":return os([Ys(t,{props:[rt(o,/:(plac\w+)/,":"+nt+"input-$1")]}),Ys(t,{props:[rt(o,/:(plac\w+)/,":"+si+"$1")]}),Ys(t,{props:[rt(o,/:(plac\w+)/,jt+"input-$1")]})],s)}return""})}},cw=[lw],dw=function(t){var n=t.key;if(n==="css"){var r=document.querySelectorAll("style[data-emotion]:not([data-s])");Array.prototype.forEach.call(r,function(p){var y=p.getAttribute("data-emotion");y.indexOf(" ")!==-1&&(document.head.appendChild(p),p.setAttribute("data-s",""))})}var s=t.stylisPlugins||cw,o={},i,a=[];i=t.container||document.head,Array.prototype.forEach.call(document.querySelectorAll('style[data-emotion^="'+n+' "]'),function(p){for(var y=p.getAttribute("data-emotion").split(" "),b=1;b<y.length;b++)o[y[b]]=!0;a.push(p)});var l,c=[iw,aw];{var d,u=[Rx,tw(function(p){d.insert(p)})],f=ew(c.concat(s,u)),g=function(y){return os(Qx(y),f)};l=function(y,b,_,x){d=_,g(y?y+"{"+b.styles+"}":b.styles),x&&(m.inserted[b.name]=!0)}}var m={key:n,sheet:new Fx({key:n,container:i,nonce:t.nonce,speedy:t.speedy,prepend:t.prepend,insertionPoint:t.insertionPoint}),nonce:t.nonce,inserted:o,registered:{},insert:l};return m.sheet.hydrate(a),m},El={exports:{}},it={};var rf;function uw(){if(rf)return it;rf=1;var e=typeof Symbol=="function"&&Symbol.for,t=e?Symbol.for("react.element"):60103,n=e?Symbol.for("react.portal"):60106,r=e?Symbol.for("react.fragment"):60107,s=e?Symbol.for("react.strict_mode"):60108,o=e?Symbol.for("react.profiler"):60114,i=e?Symbol.for("react.provider"):60109,a=e?Symbol.for("react.context"):60110,l=e?Symbol.for("react.async_mode"):60111,c=e?Symbol.for("react.concurrent_mode"):60111,d=e?Symbol.for("react.forward_ref"):60112,u=e?Symbol.for("react.suspense"):60113,f=e?Symbol.for("react.suspense_list"):60120,g=e?Symbol.for("react.memo"):60115,m=e?Symbol.for("react.lazy"):60116,p=e?Symbol.for("react.block"):60121,y=e?Symbol.for("react.fundamental"):60117,b=e?Symbol.for("react.responder"):60118,_=e?Symbol.for("react.scope"):60119;function x(C){if(typeof C=="object"&&C!==null){var k=C.$$typeof;switch(k){case t:switch(C=C.type,C){case l:case c:case r:case o:case s:case u:return C;default:switch(C=C&&C.$$typeof,C){case a:case d:case m:case g:case i:return C;default:return k}}case n:return k}}}function M(C){return x(C)===c}return it.AsyncMode=l,it.ConcurrentMode=c,it.ContextConsumer=a,it.ContextProvider=i,it.Element=t,it.ForwardRef=d,it.Fragment=r,it.Lazy=m,it.Memo=g,it.Portal=n,it.Profiler=o,it.StrictMode=s,it.Suspense=u,it.isAsyncMode=function(C){return M(C)||x(C)===l},it.isConcurrentMode=M,it.isContextConsumer=function(C){return x(C)===a},it.isContextProvider=function(C){return x(C)===i},it.isElement=function(C){return typeof C=="object"&&C!==null&&C.$$typeof===t},it.isForwardRef=function(C){return x(C)===d},it.isFragment=function(C){return x(C)===r},it.isLazy=function(C){return x(C)===m},it.isMemo=function(C){return x(C)===g},it.isPortal=function(C){return x(C)===n},it.isProfiler=function(C){return x(C)===o},it.isStrictMode=function(C){return x(C)===s},it.isSuspense=function(C){return x(C)===u},it.isValidElementType=function(C){return typeof C=="string"||typeof C=="function"||C===r||C===c||C===o||C===s||C===u||C===f||typeof C=="object"&&C!==null&&(C.$$typeof===m||C.$$typeof===g||C.$$typeof===i||C.$$typeof===a||C.$$typeof===d||C.$$typeof===y||C.$$typeof===b||C.$$typeof===_||C.$$typeof===p)},it.typeOf=x,it}var sf;function fw(){return sf||(sf=1,El.exports=uw()),El.exports}var Ml,of;function hw(){if(of)return Ml;of=1;var e=fw(),t={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},n={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},r={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},o={};o[e.ForwardRef]=r,o[e.Memo]=s;function i(m){return e.isMemo(m)?s:o[m.$$typeof]||t}var a=Object.defineProperty,l=Object.getOwnPropertyNames,c=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,u=Object.getPrototypeOf,f=Object.prototype;function g(m,p,y){if(typeof p!="string"){if(f){var b=u(p);b&&b!==f&&g(m,b,y)}var _=l(p);c&&(_=_.concat(c(p)));for(var x=i(m),M=i(p),C=0;C<_.length;++C){var k=_[C];if(!n[k]&&!(y&&y[k])&&!(M&&M[k])&&!(x&&x[k])){var N=d(p,k);try{a(m,k,N)}catch{}}}}return m}return Ml=g,Ml}hw();var gw=!0;function pw(e,t,n){var r="";return n.split(" ").forEach(function(s){e[s]!==void 0?t.push(e[s]+";"):s&&(r+=s+" ")}),r}var af=function(t,n,r){var s=t.key+"-"+n.name;(r===!1||gw===!1)&&t.registered[s]===void 0&&(t.registered[s]=n.styles)},mw=function(t,n,r){af(t,n,r);var s=t.key+"-"+n.name;if(t.inserted[n.name]===void 0){var o=n;do t.insert(n===o?"."+s:"",o,t.sheet,!0),o=o.next;while(o!==void 0)}};function bw(e){for(var t=0,n,r=0,s=e.length;s>=4;++r,s-=4)n=e.charCodeAt(r)&255|(e.charCodeAt(++r)&255)<<8|(e.charCodeAt(++r)&255)<<16|(e.charCodeAt(++r)&255)<<24,n=(n&65535)*1540483477+((n>>>16)*59797<<16),n^=n>>>24,t=(n&65535)*1540483477+((n>>>16)*59797<<16)^(t&65535)*1540483477+((t>>>16)*59797<<16);switch(s){case 3:t^=(e.charCodeAt(r+2)&255)<<16;case 2:t^=(e.charCodeAt(r+1)&255)<<8;case 1:t^=e.charCodeAt(r)&255,t=(t&65535)*1540483477+((t>>>16)*59797<<16)}return t^=t>>>13,t=(t&65535)*1540483477+((t>>>16)*59797<<16),((t^t>>>15)>>>0).toString(36)}var yw={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,scale:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},vw=/[A-Z]|^ms/g,xw=/_EMO_([^_]+?)_([^]*?)_EMO_/g,lf=function(t){return t.charCodeAt(1)===45},cf=function(t){return t!=null&&typeof t!="boolean"},Nl=nw(function(e){return lf(e)?e:e.replace(vw,"-$&").toLowerCase()}),df=function(t,n){switch(t){case"animation":case"animationName":if(typeof n=="string")return n.replace(xw,function(r,s,o){return Bn={name:s,styles:o,next:Bn},s})}return yw[t]!==1&&!lf(t)&&typeof n=="number"&&n!==0?n+"px":n};function qs(e,t,n){if(n==null)return"";var r=n;if(r.__emotion_styles!==void 0)return r;switch(typeof n){case"boolean":return"";case"object":{var s=n;if(s.anim===1)return Bn={name:s.name,styles:s.styles,next:Bn},s.name;var o=n;if(o.styles!==void 0){var i=o.next;if(i!==void 0)for(;i!==void 0;)Bn={name:i.name,styles:i.styles,next:Bn},i=i.next;var a=o.styles+";";return a}return ww(e,t,n)}case"function":{if(e!==void 0){var l=Bn,c=n(e);return Bn=l,qs(e,t,c)}break}}var d=n;return d}function ww(e,t,n){var r="";if(Array.isArray(n))for(var s=0;s<n.length;s++)r+=qs(e,t,n[s])+";";else for(var o in n){var i=n[o];if(typeof i!="object"){var a=i;cf(a)&&(r+=Nl(o)+":"+df(o,a)+";")}else if(Array.isArray(i)&&typeof i[0]=="string"&&t==null)for(var l=0;l<i.length;l++)cf(i[l])&&(r+=Nl(o)+":"+df(o,i[l])+";");else{var c=qs(e,t,i);switch(o){case"animation":case"animationName":{r+=Nl(o)+":"+c+";";break}default:r+=o+"{"+c+"}"}}}return r}var uf=/label:\s*([^\s;{]+)\s*(;|$)/g,Bn;function ff(e,t,n){if(e.length===1&&typeof e[0]=="object"&&e[0]!==null&&e[0].styles!==void 0)return e[0];var r=!0,s="";Bn=void 0;var o=e[0];if(o==null||o.raw===void 0)r=!1,s+=qs(n,t,o);else{var i=o;s+=i[0]}for(var a=1;a<e.length;a++)if(s+=qs(n,t,e[a]),r){var l=o;s+=l[a]}uf.lastIndex=0;for(var c="",d;(d=uf.exec(s))!==null;)c+="-"+d[1];var u=bw(s)+c;return{name:u,styles:s,next:Bn}}var kw=function(t){return t()},Cw=F.useInsertionEffect?F.useInsertionEffect:!1,Sw=Cw||kw,hf=F.createContext(typeof HTMLElement<"u"?dw({key:"css"}):null);hf.Provider;var _w=function(t){return v.forwardRef(function(n,r){var s=v.useContext(hf);return t(n,s,r)})},Ew=F.createContext({}),Tl={}.hasOwnProperty,Ol="__EMOTION_TYPE_PLEASE_DO_NOT_USE__",Mw=function(t,n){var r={};for(var s in n)Tl.call(n,s)&&(r[s]=n[s]);return r[Ol]=t,r},Nw=function(t){var n=t.cache,r=t.serialized,s=t.isStringTag;return af(n,r,s),Sw(function(){return mw(n,r,s)}),null},Tw=_w(function(e,t,n){var r=e.css;typeof r=="string"&&t.registered[r]!==void 0&&(r=t.registered[r]);var s=e[Ol],o=[r],i="";typeof e.className=="string"?i=pw(t.registered,o,e.className):e.className!=null&&(i=e.className+" ");var a=ff(o,void 0,F.useContext(Ew));i+=t.key+"-"+a.name;var l={};for(var c in e)Tl.call(e,c)&&c!=="css"&&c!==Ol&&(l[c]=e[c]);return l.className=i,n&&(l.ref=n),F.createElement(F.Fragment,null,F.createElement(Nw,{cache:t,serialized:a,isStringTag:typeof s=="string"}),F.createElement(s,l))}),Ow=Tw,Se=function(t,n){var r=arguments;if(n==null||!Tl.call(n,"css"))return F.createElement.apply(void 0,r);var s=r.length,o=new Array(s);o[0]=Ow,o[1]=Mw(t,n);for(var i=2;i<s;i++)o[i]=r[i];return F.createElement.apply(null,o)};(function(e){var t;t||(t=e.JSX||(e.JSX={}))})(Se||(Se={}));function Al(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return ff(t)}function Aw(){var e=Al.apply(void 0,arguments),t="animation-"+e.name;return{name:t,styles:"@keyframes "+t+"{"+e.styles+"}",anim:1,toString:function(){return"_EMO_"+this.name+"_"+this.styles+"_EMO_"}}}function Dw(e,t){return t||(t=e.slice(0)),Object.freeze(Object.defineProperties(e,{raw:{value:Object.freeze(t)}}))}const Iw=Math.min,Lw=Math.max,fi=Math.round,hi=Math.floor,Dl=e=>({x:e,y:e});function Pw(e){const{x:t,y:n,width:r,height:s}=e;return{width:r,height:s,top:n,left:t,right:t+r,bottom:n+s,x:t,y:n}}function gi(){return typeof window<"u"}function gf(e){return mf(e)?(e.nodeName||"").toLowerCase():"#document"}function Sr(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function pf(e){var t;return(t=(mf(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function mf(e){return gi()?e instanceof Node||e instanceof Sr(e).Node:!1}function $w(e){return gi()?e instanceof Element||e instanceof Sr(e).Element:!1}function Il(e){return gi()?e instanceof HTMLElement||e instanceof Sr(e).HTMLElement:!1}function bf(e){return!gi()||typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof Sr(e).ShadowRoot}function yf(e){const{overflow:t,overflowX:n,overflowY:r,display:s}=vf(e);return/auto|scroll|overlay|hidden|clip/.test(t+r+n)&&s!=="inline"&&s!=="contents"}function Fw(e){return/^(html|body|#document)$/.test(gf(e))}function vf(e){return Sr(e).getComputedStyle(e)}function zw(e){if(gf(e)==="html")return e;const t=e.assignedSlot||e.parentNode||bf(e)&&e.host||pf(e);return bf(t)?t.host:t}function xf(e){const t=zw(e);return Fw(t)?(e.ownerDocument||e).body:Il(t)&&yf(t)?t:xf(t)}function pi(e,t,n){var r;t===void 0&&(t=[]),n===void 0&&(n=!0);const s=xf(e),o=s===((r=e.ownerDocument)==null?void 0:r.body),i=Sr(s);if(o){const a=Vw(i);return t.concat(i,i.visualViewport||[],yf(s)?s:[],a&&n?pi(a):[])}else return t.concat(s,pi(s,[],n))}function Vw(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function Hw(e){const t=vf(e);let n=parseFloat(t.width)||0,r=parseFloat(t.height)||0;const s=Il(e),o=s?e.offsetWidth:n,i=s?e.offsetHeight:r,a=fi(n)!==o||fi(r)!==i;return a&&(n=o,r=i),{width:n,height:r,$:a}}function Ll(e){return $w(e)?e:e.contextElement}function Uw(e){const t=Ll(e);if(!Il(t))return Dl(1);const n=t.getBoundingClientRect(),{width:r,height:s,$:o}=Hw(t);let i=(o?fi(n.width):n.width)/r,a=(o?fi(n.height):n.height)/s;return(!i||!Number.isFinite(i))&&(i=1),(!a||!Number.isFinite(a))&&(a=1),{x:i,y:a}}function wf(e,t,n,r){t===void 0&&(t=!1);const s=e.getBoundingClientRect();Ll(e);let o=Dl(1);t&&(o=Uw(e));const i=Dl(0);let a=(s.left+i.x)/o.x,l=(s.top+i.y)/o.y,c=s.width/o.x,d=s.height/o.y;return Pw({width:c,height:d,x:a,y:l})}function kf(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}function Bw(e,t,n){let r=null,s;const o=pf(e);function i(){var d;clearTimeout(s),(d=r)==null||d.disconnect(),r=null}function a(d,u){d===void 0&&(d=!1),u===void 0&&(u=1),i();const f=e.getBoundingClientRect(),{left:g,top:m,width:p,height:y}=f;if(d||t(),!p||!y)return;const b=hi(m),_=hi(o.clientWidth-(g+p)),x=hi(o.clientHeight-(m+y)),M=hi(g),k={rootMargin:-b+"px "+-_+"px "+-x+"px "+-M+"px",threshold:Lw(0,Iw(1,u))||1};let N=!0;function S(w){const E=w[0].intersectionRatio;if(!kf(f,e.getBoundingClientRect()))return a();if(E!==u){if(!N)return a();E?a(!1,E):s=setTimeout(()=>{a(!1,1e-7)},1e3)}N=!1}try{r=new IntersectionObserver(S,{...k,root:o.ownerDocument})}catch{r=new IntersectionObserver(S,k)}r.observe(e)}const l=Sr(e),c=()=>a(n);return l.addEventListener("resize",c),a(!0),()=>{l.removeEventListener("resize",c),i()}}function jw(e,t,n,r){r===void 0&&(r={});const{ancestorScroll:s=!0,ancestorResize:o=!0,elementResize:i=typeof ResizeObserver=="function",layoutShift:a=typeof IntersectionObserver=="function",animationFrame:l=!1}=r,c=Ll(e),d=s||o?[...c?pi(c):[],...t?pi(t):[]]:[];d.forEach(b=>{s&&b.addEventListener("scroll",n),o&&b.addEventListener("resize",n)});const u=c&&a?Bw(c,n,o):null;let f=-1,g=null;i&&(g=new ResizeObserver(b=>{let[_]=b;_&&_.target===c&&g&&t&&(g.unobserve(t),cancelAnimationFrame(f),f=requestAnimationFrame(()=>{var x;(x=g)==null||x.observe(t)})),n()}),c&&!l&&g.observe(c),t&&g.observe(t));let m,p=l?wf(e):null;l&&y();function y(){const b=wf(e);p&&!kf(p,b)&&n(),p=b,m=requestAnimationFrame(y)}return n(),()=>{var b;d.forEach(_=>{s&&_.removeEventListener("scroll",n),o&&_.removeEventListener("resize",n)}),u?.(),(b=g)==null||b.disconnect(),g=null,l&&cancelAnimationFrame(m)}}var Pl=v.useLayoutEffect,Ww=["className","clearValue","cx","getStyles","getClassNames","getValue","hasValue","isMulti","isRtl","options","selectOption","selectProps","setValue","theme"],mi=function(){};function Gw(e,t){return t?t[0]==="-"?e+t:e+"__"+t:e}function Yw(e,t){for(var n=arguments.length,r=new Array(n>2?n-2:0),s=2;s<n;s++)r[s-2]=arguments[s];var o=[].concat(r);if(t&&e)for(var i in t)t.hasOwnProperty(i)&&t[i]&&o.push("".concat(Gw(e,i)));return o.filter(function(a){return a}).map(function(a){return String(a).trim()}).join(" ")}var bi=function(t){return t1(t)?t.filter(Boolean):Cr(t)==="object"&&t!==null?[t]:[]},Cf=function(t){t.className,t.clearValue,t.cx,t.getStyles,t.getClassNames,t.getValue,t.hasValue,t.isMulti,t.isRtl,t.options,t.selectOption,t.selectProps,t.setValue,t.theme;var n=Vn(t,Ww);return Ee({},n)},wt=function(t,n,r){var s=t.cx,o=t.getStyles,i=t.getClassNames,a=t.className;return{css:o(n,t),className:s(r??{},i(n,t),a)}};function yi(e){return[document.documentElement,document.body,window].indexOf(e)>-1}function Xw(e){return yi(e)?window.innerHeight:e.clientHeight}function Sf(e){return yi(e)?window.pageYOffset:e.scrollTop}function vi(e,t){if(yi(e)){window.scrollTo(0,t);return}e.scrollTop=t}function Kw(e){var t=getComputedStyle(e),n=t.position==="absolute",r=/(auto|scroll)/;if(t.position==="fixed")return document.documentElement;for(var s=e;s=s.parentElement;)if(t=getComputedStyle(s),!(n&&t.position==="static")&&r.test(t.overflow+t.overflowY+t.overflowX))return s;return document.documentElement}function qw(e,t,n,r){return n*((e=e/r-1)*e*e+1)+t}function xi(e,t){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:200,r=arguments.length>3&&arguments[3]!==void 0?arguments[3]:mi,s=Sf(e),o=t-s,i=10,a=0;function l(){a+=i;var c=qw(a,s,o,n);vi(e,c),a<n?window.requestAnimationFrame(l):r(e)}l()}function _f(e,t){var n=e.getBoundingClientRect(),r=t.getBoundingClientRect(),s=t.offsetHeight/3;r.bottom+s>n.bottom?vi(e,Math.min(t.offsetTop+t.clientHeight-e.offsetHeight+s,e.scrollHeight)):r.top-s<n.top&&vi(e,Math.max(t.offsetTop-s,0))}function Zw(e){var t=e.getBoundingClientRect();return{bottom:t.bottom,height:t.height,left:t.left,right:t.right,top:t.top,width:t.width}}function Ef(){try{return document.createEvent("TouchEvent"),!0}catch{return!1}}function Qw(){try{return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}catch{return!1}}var Mf=!1,Jw={get passive(){return Mf=!0}},wi=typeof window<"u"?window:{};wi.addEventListener&&wi.removeEventListener&&(wi.addEventListener("p",mi,Jw),wi.removeEventListener("p",mi,!1));var Rw=Mf;function e1(e){return e!=null}function t1(e){return Array.isArray(e)}function Zs(e,t,n){return e?t:n}function sA(e){return e}function oA(e){return e}var n1=function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];var o=Object.entries(t).filter(function(i){var a=Qn(i,1),l=a[0];return!r.includes(l)});return o.reduce(function(i,a){var l=Qn(a,2),c=l[0],d=l[1];return i[c]=d,i},{})},r1=["children","innerProps"],s1=["children","innerProps"];function o1(e){var t=e.maxHeight,n=e.menuEl,r=e.minHeight,s=e.placement,o=e.shouldScroll,i=e.isFixedPosition,a=e.controlHeight,l=Kw(n),c={placement:"bottom",maxHeight:t};if(!n||!n.offsetParent)return c;var d=l.getBoundingClientRect(),u=d.height,f=n.getBoundingClientRect(),g=f.bottom,m=f.height,p=f.top,y=n.offsetParent.getBoundingClientRect(),b=y.top,_=i?window.innerHeight:Xw(l),x=Sf(l),M=parseInt(getComputedStyle(n).marginBottom,10),C=parseInt(getComputedStyle(n).marginTop,10),k=b-C,N=_-p,S=k+x,w=u-x-p,E=g-_+x+M,D=x+p-C,P=160;switch(s){case"auto":case"bottom":if(N>=m)return{placement:"bottom",maxHeight:t};if(w>=m&&!i)return o&&xi(l,E,P),{placement:"bottom",maxHeight:t};if(!i&&w>=r||i&&N>=r){o&&xi(l,E,P);var L=i?N-M:w-M;return{placement:"bottom",maxHeight:L}}if(s==="auto"||i){var $=t,A=i?k:S;return A>=r&&($=Math.min(A-M-a,t)),{placement:"top",maxHeight:$}}if(s==="bottom")return o&&vi(l,E),{placement:"bottom",maxHeight:t};break;case"top":if(k>=m)return{placement:"top",maxHeight:t};if(S>=m&&!i)return o&&xi(l,D,P),{placement:"top",maxHeight:t};if(!i&&S>=r||i&&k>=r){var O=t;return(!i&&S>=r||i&&k>=r)&&(O=i?k-C:S-C),o&&xi(l,D,P),{placement:"top",maxHeight:O}}return{placement:"bottom",maxHeight:t};default:throw new Error('Invalid placement provided "'.concat(s,'".'))}return c}function i1(e){var t={bottom:"top",top:"bottom"};return e?t[e]:"bottom"}var Nf=function(t){return t==="auto"?"bottom":t},a1=function(t,n){var r,s=t.placement,o=t.theme,i=o.borderRadius,a=o.spacing,l=o.colors;return Ee((r={label:"menu"},Ws(r,i1(s),"100%"),Ws(r,"position","absolute"),Ws(r,"width","100%"),Ws(r,"zIndex",1),r),n?{}:{backgroundColor:l.neutral0,borderRadius:i,boxShadow:"0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1)",marginBottom:a.menuGutter,marginTop:a.menuGutter})},Tf=v.createContext(null),l1=function(t){var n=t.children,r=t.minMenuHeight,s=t.maxMenuHeight,o=t.menuPlacement,i=t.menuPosition,a=t.menuShouldScrollIntoView,l=t.theme,c=v.useContext(Tf)||{},d=c.setPortalPlacement,u=v.useRef(null),f=v.useState(s),g=Qn(f,2),m=g[0],p=g[1],y=v.useState(null),b=Qn(y,2),_=b[0],x=b[1],M=l.spacing.controlHeight;return Pl(function(){var C=u.current;if(C){var k=i==="fixed",N=a&&!k,S=o1({maxHeight:s,menuEl:C,minHeight:r,placement:o,shouldScroll:N,isFixedPosition:k,controlHeight:M});p(S.maxHeight),x(S.placement),d?.(S.placement)}},[s,o,i,a,r,d,M]),n({ref:u,placerProps:Ee(Ee({},t),{},{placement:_||Nf(o),maxHeight:m})})},c1=function(t){var n=t.children,r=t.innerRef,s=t.innerProps;return Se("div",Oe({},wt(t,"menu",{menu:!0}),{ref:r},s),n)},d1=c1,u1=function(t,n){var r=t.maxHeight,s=t.theme.spacing.baseUnit;return Ee({maxHeight:r,overflowY:"auto",position:"relative",WebkitOverflowScrolling:"touch"},n?{}:{paddingBottom:s,paddingTop:s})},f1=function(t){var n=t.children,r=t.innerProps,s=t.innerRef,o=t.isMulti;return Se("div",Oe({},wt(t,"menuList",{"menu-list":!0,"menu-list--is-multi":o}),{ref:s},r),n)},Of=function(t,n){var r=t.theme,s=r.spacing.baseUnit,o=r.colors;return Ee({textAlign:"center"},n?{}:{color:o.neutral40,padding:"".concat(s*2,"px ").concat(s*3,"px")})},h1=Of,g1=Of,p1=function(t){var n=t.children,r=n===void 0?"No options":n,s=t.innerProps,o=Vn(t,r1);return Se("div",Oe({},wt(Ee(Ee({},o),{},{children:r,innerProps:s}),"noOptionsMessage",{"menu-notice":!0,"menu-notice--no-options":!0}),s),r)},m1=function(t){var n=t.children,r=n===void 0?"Loading...":n,s=t.innerProps,o=Vn(t,s1);return Se("div",Oe({},wt(Ee(Ee({},o),{},{children:r,innerProps:s}),"loadingMessage",{"menu-notice":!0,"menu-notice--loading":!0}),s),r)},b1=function(t){var n=t.rect,r=t.offset,s=t.position;return{left:n.left,position:s,top:r,width:n.width,zIndex:1}},y1=function(t){var n=t.appendTo,r=t.children,s=t.controlElement,o=t.innerProps,i=t.menuPlacement,a=t.menuPosition,l=v.useRef(null),c=v.useRef(null),d=v.useState(Nf(i)),u=Qn(d,2),f=u[0],g=u[1],m=v.useMemo(function(){return{setPortalPlacement:g}},[]),p=v.useState(null),y=Qn(p,2),b=y[0],_=y[1],x=v.useCallback(function(){if(s){var N=Zw(s),S=a==="fixed"?0:window.pageYOffset,w=N[f]+S;(w!==b?.offset||N.left!==b?.rect.left||N.width!==b?.rect.width)&&_({offset:w,rect:N})}},[s,a,f,b?.offset,b?.rect.left,b?.rect.width]);Pl(function(){x()},[x]);var M=v.useCallback(function(){typeof c.current=="function"&&(c.current(),c.current=null),s&&l.current&&(c.current=jw(s,l.current,x,{elementResize:"ResizeObserver"in window}))},[s,x]);Pl(function(){M()},[M]);var C=v.useCallback(function(N){l.current=N,M()},[M]);if(!n&&a!=="fixed"||!b)return null;var k=Se("div",Oe({ref:C},wt(Ee(Ee({},t),{},{offset:b.offset,position:a,rect:b.rect}),"menuPortal",{"menu-portal":!0}),o),r);return Se(Tf.Provider,{value:m},n?$n.createPortal(k,n):k)},v1=function(t){var n=t.isDisabled,r=t.isRtl;return{label:"container",direction:r?"rtl":void 0,pointerEvents:n?"none":void 0,position:"relative"}},x1=function(t){var n=t.children,r=t.innerProps,s=t.isDisabled,o=t.isRtl;return Se("div",Oe({},wt(t,"container",{"--is-disabled":s,"--is-rtl":o}),r),n)},w1=function(t,n){var r=t.theme.spacing,s=t.isMulti,o=t.hasValue,i=t.selectProps.controlShouldRenderValue;return Ee({alignItems:"center",display:s&&o&&i?"flex":"grid",flex:1,flexWrap:"wrap",WebkitOverflowScrolling:"touch",position:"relative",overflow:"hidden"},n?{}:{padding:"".concat(r.baseUnit/2,"px ").concat(r.baseUnit*2,"px")})},k1=function(t){var n=t.children,r=t.innerProps,s=t.isMulti,o=t.hasValue;return Se("div",Oe({},wt(t,"valueContainer",{"value-container":!0,"value-container--is-multi":s,"value-container--has-value":o}),r),n)},C1=function(){return{alignItems:"center",alignSelf:"stretch",display:"flex",flexShrink:0}},S1=function(t){var n=t.children,r=t.innerProps;return Se("div",Oe({},wt(t,"indicatorsContainer",{indicators:!0}),r),n)},Af,_1=["size"],E1=["innerProps","isRtl","size"],M1={name:"8mmkcg",styles:"display:inline-block;fill:currentColor;line-height:1;stroke:currentColor;stroke-width:0"},Df=function(t){var n=t.size,r=Vn(t,_1);return Se("svg",Oe({height:n,width:n,viewBox:"0 0 20 20","aria-hidden":"true",focusable:"false",css:M1},r))},$l=function(t){return Se(Df,Oe({size:20},t),Se("path",{d:"M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"}))},If=function(t){return Se(Df,Oe({size:20},t),Se("path",{d:"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"}))},Lf=function(t,n){var r=t.isFocused,s=t.theme,o=s.spacing.baseUnit,i=s.colors;return Ee({label:"indicatorContainer",display:"flex",transition:"color 150ms"},n?{}:{color:r?i.neutral60:i.neutral20,padding:o*2,":hover":{color:r?i.neutral80:i.neutral40}})},N1=Lf,T1=function(t){var n=t.children,r=t.innerProps;return Se("div",Oe({},wt(t,"dropdownIndicator",{indicator:!0,"dropdown-indicator":!0}),r),n||Se(If,null))},O1=Lf,A1=function(t){var n=t.children,r=t.innerProps;return Se("div",Oe({},wt(t,"clearIndicator",{indicator:!0,"clear-indicator":!0}),r),n||Se($l,null))},D1=function(t,n){var r=t.isDisabled,s=t.theme,o=s.spacing.baseUnit,i=s.colors;return Ee({label:"indicatorSeparator",alignSelf:"stretch",width:1},n?{}:{backgroundColor:r?i.neutral10:i.neutral20,marginBottom:o*2,marginTop:o*2})},I1=function(t){var n=t.innerProps;return Se("span",Oe({},n,wt(t,"indicatorSeparator",{"indicator-separator":!0})))},L1=Aw(Af||(Af=Dw([` + `})]}):null},qv=(t,e)=>window.G7Core?.t?.(t,e)??t,Zv=({options:t})=>{const e=document.documentElement.classList.contains("dark"),n=e?"rgb(17,24,39)":"rgb(249,250,251)",r=e?"#6b7280":"#9ca3af",s=e?"#9ca3af":"#6b7280";return h.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:n,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[h.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${r}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),h.jsx("span",{style:{color:s,fontSize:"14px"},children:t?.text||qv("nav.loading")})]})};function pr(t){"@babel/helpers - typeof";return pr=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(e){return typeof e}:function(e){return e&&typeof Symbol=="function"&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},pr(t)}function Qv(t,e){if(pr(t)!="object"||!t)return t;var n=t[Symbol.toPrimitive];if(n!==void 0){var r=n.call(t,e);if(pr(r)!="object")return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return(e==="string"?String:Number)(t)}function Au(t){var e=Qv(t,"string");return pr(e)=="symbol"?e:e+""}function Ls(t,e,n){return(e=Au(e))in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function Du(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter(function(s){return Object.getOwnPropertyDescriptor(t,s).enumerable})),n.push.apply(n,r)}return n}function Ee(t){for(var e=1;e<arguments.length;e++){var n=arguments[e]!=null?arguments[e]:{};e%2?Du(Object(n),!0).forEach(function(r){Ls(t,r,n[r])}):Object.getOwnPropertyDescriptors?Object.defineProperties(t,Object.getOwnPropertyDescriptors(n)):Du(Object(n)).forEach(function(r){Object.defineProperty(t,r,Object.getOwnPropertyDescriptor(n,r))})}return t}function Jv(t){if(Array.isArray(t))return t}function Rv(t,e){var n=t==null?null:typeof Symbol<"u"&&t[Symbol.iterator]||t["@@iterator"];if(n!=null){var r,s,o,i,a=[],l=!0,c=!1;try{if(o=(n=n.call(t)).next,e===0){if(Object(n)!==n)return;l=!1}else for(;!(l=(r=o.call(n)).done)&&(a.push(r.value),a.length!==e);l=!0);}catch(d){c=!0,s=d}finally{try{if(!l&&n.return!=null&&(i=n.return(),Object(i)!==i))return}finally{if(c)throw s}}return a}}function ul(t,e){(e==null||e>t.length)&&(e=t.length);for(var n=0,r=Array(e);n<e;n++)r[n]=t[n];return r}function Iu(t,e){if(t){if(typeof t=="string")return ul(t,e);var n={}.toString.call(t).slice(8,-1);return n==="Object"&&t.constructor&&(n=t.constructor.name),n==="Map"||n==="Set"?Array.from(t):n==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?ul(t,e):void 0}}function ex(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Gn(t,e){return Jv(t)||Rv(t,e)||Iu(t,e)||ex()}function tx(t,e){if(t==null)return{};var n={};for(var r in t)if({}.hasOwnProperty.call(t,r)){if(e.indexOf(r)!==-1)continue;n[r]=t[r]}return n}function In(t,e){if(t==null)return{};var n,r,s=tx(t,e);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(t);for(r=0;r<o.length;r++)n=o[r],e.indexOf(n)===-1&&{}.propertyIsEnumerable.call(t,n)&&(s[n]=t[n])}return s}var nx=["defaultInputValue","defaultMenuIsOpen","defaultValue","inputValue","menuIsOpen","onChange","onInputChange","onMenuClose","onMenuOpen","value"];function Lu(t){var e=t.defaultInputValue,n=e===void 0?"":e,r=t.defaultMenuIsOpen,s=r===void 0?!1:r,o=t.defaultValue,i=o===void 0?null:o,a=t.inputValue,l=t.menuIsOpen,c=t.onChange,d=t.onInputChange,u=t.onMenuClose,f=t.onMenuOpen,g=t.value,m=In(t,nx),p=v.useState(a!==void 0?a:n),y=Gn(p,2),b=y[0],_=y[1],x=v.useState(l!==void 0?l:s),E=Gn(x,2),S=E[0],w=E[1],N=v.useState(g!==void 0?g:i),C=Gn(N,2),k=C[0],M=C[1],T=v.useCallback(function(V,H){typeof c=="function"&&c(V,H),M(V)},[c]),L=v.useCallback(function(V,H){var W;typeof d=="function"&&(W=d(V,H)),_(W!==void 0?W:V)},[d]),P=v.useCallback(function(){typeof f=="function"&&f(),w(!0)},[f]),F=v.useCallback(function(){typeof u=="function"&&u(),w(!1)},[u]),D=a!==void 0?a:b,A=l!==void 0?l:S,O=g!==void 0?g:k;return Ee(Ee({},m),{},{inputValue:D,menuIsOpen:A,onChange:T,onInputChange:L,onMenuClose:F,onMenuOpen:P,value:O})}function Ae(){return Ae=Object.assign?Object.assign.bind():function(t){for(var e=1;e<arguments.length;e++){var n=arguments[e];for(var r in n)({}).hasOwnProperty.call(n,r)&&(t[r]=n[r])}return t},Ae.apply(null,arguments)}function rx(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function Pu(t,e){for(var n=0;n<e.length;n++){var r=e[n];r.enumerable=r.enumerable||!1,r.configurable=!0,"value"in r&&(r.writable=!0),Object.defineProperty(t,Au(r.key),r)}}function sx(t,e,n){return e&&Pu(t.prototype,e),n&&Pu(t,n),Object.defineProperty(t,"prototype",{writable:!1}),t}function fl(t,e){return fl=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(n,r){return n.__proto__=r,n},fl(t,e)}function ox(t,e){if(typeof e!="function"&&e!==null)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&fl(t,e)}function Ko(t){return Ko=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(e){return e.__proto__||Object.getPrototypeOf(e)},Ko(t)}function $u(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return($u=function(){return!!t})()}function ix(t){if(t===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function ax(t,e){if(e&&(pr(e)=="object"||typeof e=="function"))return e;if(e!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return ix(t)}function lx(t){var e=$u();return function(){var n,r=Ko(t);if(e){var s=Ko(this).constructor;n=Reflect.construct(r,arguments,s)}else n=r.apply(this,arguments);return ax(this,n)}}function cx(t){if(Array.isArray(t))return ul(t)}function dx(t){if(typeof Symbol<"u"&&t[Symbol.iterator]!=null||t["@@iterator"]!=null)return Array.from(t)}function ux(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Gr(t){return cx(t)||dx(t)||Iu(t)||ux()}function fx(t){if(t.sheet)return t.sheet;for(var e=0;e<document.styleSheets.length;e++)if(document.styleSheets[e].ownerNode===t)return document.styleSheets[e]}function hx(t){var e=document.createElement("style");return e.setAttribute("data-emotion",t.key),t.nonce!==void 0&&e.setAttribute("nonce",t.nonce),e.appendChild(document.createTextNode("")),e.setAttribute("data-s",""),e}var gx=(function(){function t(n){var r=this;this._insertTag=function(s){var o;r.tags.length===0?r.insertionPoint?o=r.insertionPoint.nextSibling:r.prepend?o=r.container.firstChild:o=r.before:o=r.tags[r.tags.length-1].nextSibling,r.container.insertBefore(s,o),r.tags.push(s)},this.isSpeedy=n.speedy===void 0?!0:n.speedy,this.tags=[],this.ctr=0,this.nonce=n.nonce,this.key=n.key,this.container=n.container,this.prepend=n.prepend,this.insertionPoint=n.insertionPoint,this.before=null}var e=t.prototype;return e.hydrate=function(r){r.forEach(this._insertTag)},e.insert=function(r){this.ctr%(this.isSpeedy?65e3:1)===0&&this._insertTag(hx(this));var s=this.tags[this.tags.length-1];if(this.isSpeedy){var o=fx(s);try{o.insertRule(r,o.cssRules.length)}catch{}}else s.appendChild(document.createTextNode(r));this.ctr++},e.flush=function(){this.tags.forEach(function(r){var s;return(s=r.parentNode)==null?void 0:s.removeChild(r)}),this.tags=[],this.ctr=0},t})(),zt="-ms-",qo="-moz-",Qe="-webkit-",Fu="comm",hl="rule",gl="decl",px="@import",Vu="@keyframes",mx="@layer",bx=Math.abs,Zo=String.fromCharCode,yx=Object.assign;function vx(t,e){return $t(t,0)^45?(((e<<2^$t(t,0))<<2^$t(t,1))<<2^$t(t,2))<<2^$t(t,3):0}function zu(t){return t.trim()}function xx(t,e){return(t=e.exec(t))?t[0]:t}function Je(t,e,n){return t.replace(e,n)}function pl(t,e){return t.indexOf(e)}function $t(t,e){return t.charCodeAt(e)|0}function Ps(t,e,n){return t.slice(e,n)}function Ln(t){return t.length}function ml(t){return t.length}function Qo(t,e){return e.push(t),t}function wx(t,e){return t.map(e).join("")}var Jo=1,Yr=1,Hu=0,Rt=0,kt=0,Xr="";function Ro(t,e,n,r,s,o,i){return{value:t,root:e,parent:n,type:r,props:s,children:o,line:Jo,column:Yr,length:i,return:""}}function $s(t,e){return yx(Ro("",null,null,"",null,null,0),t,{length:-t.length},e)}function kx(){return kt}function Cx(){return kt=Rt>0?$t(Xr,--Rt):0,Yr--,kt===10&&(Yr=1,Jo--),kt}function cn(){return kt=Rt<Hu?$t(Xr,Rt++):0,Yr++,kt===10&&(Yr=1,Jo++),kt}function Pn(){return $t(Xr,Rt)}function ei(){return Rt}function Fs(t,e){return Ps(Xr,t,e)}function Vs(t){switch(t){case 0:case 9:case 10:case 13:case 32:return 5;case 33:case 43:case 44:case 47:case 62:case 64:case 126:case 59:case 123:case 125:return 4;case 58:return 3;case 34:case 39:case 40:case 91:return 2;case 41:case 93:return 1}return 0}function Uu(t){return Jo=Yr=1,Hu=Ln(Xr=t),Rt=0,[]}function Bu(t){return Xr="",t}function ti(t){return zu(Fs(Rt-1,bl(t===91?t+2:t===40?t+1:t)))}function Sx(t){for(;(kt=Pn())&&kt<33;)cn();return Vs(t)>2||Vs(kt)>3?"":" "}function _x(t,e){for(;--e&&cn()&&!(kt<48||kt>102||kt>57&&kt<65||kt>70&&kt<97););return Fs(t,ei()+(e<6&&Pn()==32&&cn()==32))}function bl(t){for(;cn();)switch(kt){case t:return Rt;case 34:case 39:t!==34&&t!==39&&bl(kt);break;case 40:t===41&&bl(t);break;case 92:cn();break}return Rt}function Ex(t,e){for(;cn()&&t+kt!==57;)if(t+kt===84&&Pn()===47)break;return"/*"+Fs(e,Rt-1)+"*"+Zo(t===47?t:cn())}function Mx(t){for(;!Vs(Pn());)cn();return Fs(t,Rt)}function Nx(t){return Bu(ni("",null,null,null,[""],t=Uu(t),0,[0],t))}function ni(t,e,n,r,s,o,i,a,l){for(var c=0,d=0,u=i,f=0,g=0,m=0,p=1,y=1,b=1,_=0,x="",E=s,S=o,w=r,N=x;y;)switch(m=_,_=cn()){case 40:if(m!=108&&$t(N,u-1)==58){pl(N+=Je(ti(_),"&","&\f"),"&\f")!=-1&&(b=-1);break}case 34:case 39:case 91:N+=ti(_);break;case 9:case 10:case 13:case 32:N+=Sx(m);break;case 92:N+=_x(ei()-1,7);continue;case 47:switch(Pn()){case 42:case 47:Qo(Ox(Ex(cn(),ei()),e,n),l);break;default:N+="/"}break;case 123*p:a[c++]=Ln(N)*b;case 125*p:case 59:case 0:switch(_){case 0:case 125:y=0;case 59+d:b==-1&&(N=Je(N,/\f/g,"")),g>0&&Ln(N)-u&&Qo(g>32?Wu(N+";",r,n,u-1):Wu(Je(N," ","")+";",r,n,u-2),l);break;case 59:N+=";";default:if(Qo(w=ju(N,e,n,c,d,s,a,x,E=[],S=[],u),o),_===123)if(d===0)ni(N,e,w,w,E,o,u,a,S);else switch(f===99&&$t(N,3)===110?100:f){case 100:case 108:case 109:case 115:ni(t,w,w,r&&Qo(ju(t,w,w,0,0,s,a,x,s,E=[],u),S),s,S,u,a,r?E:S);break;default:ni(N,w,w,w,[""],S,0,a,S)}}c=d=g=0,p=b=1,x=N="",u=i;break;case 58:u=1+Ln(N),g=m;default:if(p<1){if(_==123)--p;else if(_==125&&p++==0&&Cx()==125)continue}switch(N+=Zo(_),_*p){case 38:b=d>0?1:(N+="\f",-1);break;case 44:a[c++]=(Ln(N)-1)*b,b=1;break;case 64:Pn()===45&&(N+=ti(cn())),f=Pn(),d=u=Ln(x=N+=Mx(ei())),_++;break;case 45:m===45&&Ln(N)==2&&(p=0)}}return o}function ju(t,e,n,r,s,o,i,a,l,c,d){for(var u=s-1,f=s===0?o:[""],g=ml(f),m=0,p=0,y=0;m<r;++m)for(var b=0,_=Ps(t,u+1,u=bx(p=i[m])),x=t;b<g;++b)(x=zu(p>0?f[b]+" "+_:Je(_,/&\f/g,f[b])))&&(l[y++]=x);return Ro(t,e,n,s===0?hl:a,l,c,d)}function Ox(t,e,n){return Ro(t,e,n,Fu,Zo(kx()),Ps(t,2,-2),0)}function Wu(t,e,n,r){return Ro(t,e,n,gl,Ps(t,0,r),Ps(t,r+1,-1),r)}function Kr(t,e){for(var n="",r=ml(t),s=0;s<r;s++)n+=e(t[s],s,t,e)||"";return n}function Tx(t,e,n,r){switch(t.type){case mx:if(t.children.length)break;case px:case gl:return t.return=t.return||t.value;case Fu:return"";case Vu:return t.return=t.value+"{"+Kr(t.children,r)+"}";case hl:t.value=t.props.join(",")}return Ln(n=Kr(t.children,r))?t.return=t.value+"{"+n+"}":""}function Ax(t){var e=ml(t);return function(n,r,s,o){for(var i="",a=0;a<e;a++)i+=t[a](n,r,s,o)||"";return i}}function Dx(t){return function(e){e.root||(e=e.return)&&t(e)}}function Ix(t){var e=Object.create(null);return function(n){return e[n]===void 0&&(e[n]=t(n)),e[n]}}var Lx=function(e,n,r){for(var s=0,o=0;s=o,o=Pn(),s===38&&o===12&&(n[r]=1),!Vs(o);)cn();return Fs(e,Rt)},Px=function(e,n){var r=-1,s=44;do switch(Vs(s)){case 0:s===38&&Pn()===12&&(n[r]=1),e[r]+=Lx(Rt-1,n,r);break;case 2:e[r]+=ti(s);break;case 4:if(s===44){e[++r]=Pn()===58?"&\f":"",n[r]=e[r].length;break}default:e[r]+=Zo(s)}while(s=cn());return e},$x=function(e,n){return Bu(Px(Uu(e),n))},Gu=new WeakMap,Fx=function(e){if(!(e.type!=="rule"||!e.parent||e.length<1)){for(var n=e.value,r=e.parent,s=e.column===r.column&&e.line===r.line;r.type!=="rule";)if(r=r.parent,!r)return;if(!(e.props.length===1&&n.charCodeAt(0)!==58&&!Gu.get(r))&&!s){Gu.set(e,!0);for(var o=[],i=$x(n,o),a=r.props,l=0,c=0;l<i.length;l++)for(var d=0;d<a.length;d++,c++)e.props[c]=o[l]?i[l].replace(/&\f/g,a[d]):a[d]+" "+i[l]}}},Vx=function(e){if(e.type==="decl"){var n=e.value;n.charCodeAt(0)===108&&n.charCodeAt(2)===98&&(e.return="",e.value="")}};function Yu(t,e){switch(vx(t,e)){case 5103:return Qe+"print-"+t+t;case 5737:case 4201:case 3177:case 3433:case 1641:case 4457:case 2921:case 5572:case 6356:case 5844:case 3191:case 6645:case 3005:case 6391:case 5879:case 5623:case 6135:case 4599:case 4855:case 4215:case 6389:case 5109:case 5365:case 5621:case 3829:return Qe+t+t;case 5349:case 4246:case 4810:case 6968:case 2756:return Qe+t+qo+t+zt+t+t;case 6828:case 4268:return Qe+t+zt+t+t;case 6165:return Qe+t+zt+"flex-"+t+t;case 5187:return Qe+t+Je(t,/(\w+).+(:[^]+)/,Qe+"box-$1$2"+zt+"flex-$1$2")+t;case 5443:return Qe+t+zt+"flex-item-"+Je(t,/flex-|-self/,"")+t;case 4675:return Qe+t+zt+"flex-line-pack"+Je(t,/align-content|flex-|-self/,"")+t;case 5548:return Qe+t+zt+Je(t,"shrink","negative")+t;case 5292:return Qe+t+zt+Je(t,"basis","preferred-size")+t;case 6060:return Qe+"box-"+Je(t,"-grow","")+Qe+t+zt+Je(t,"grow","positive")+t;case 4554:return Qe+Je(t,/([^-])(transform)/g,"$1"+Qe+"$2")+t;case 6187:return Je(Je(Je(t,/(zoom-|grab)/,Qe+"$1"),/(image-set)/,Qe+"$1"),t,"")+t;case 5495:case 3959:return Je(t,/(image-set\([^]*)/,Qe+"$1$`$1");case 4968:return Je(Je(t,/(.+:)(flex-)?(.*)/,Qe+"box-pack:$3"+zt+"flex-pack:$3"),/s.+-b[^;]+/,"justify")+Qe+t+t;case 4095:case 3583:case 4068:case 2532:return Je(t,/(.+)-inline(.+)/,Qe+"$1$2")+t;case 8116:case 7059:case 5753:case 5535:case 5445:case 5701:case 4933:case 4677:case 5533:case 5789:case 5021:case 4765:if(Ln(t)-1-e>6)switch($t(t,e+1)){case 109:if($t(t,e+4)!==45)break;case 102:return Je(t,/(.+:)(.+)-([^]+)/,"$1"+Qe+"$2-$3$1"+qo+($t(t,e+3)==108?"$3":"$2-$3"))+t;case 115:return~pl(t,"stretch")?Yu(Je(t,"stretch","fill-available"),e)+t:t}break;case 4949:if($t(t,e+1)!==115)break;case 6444:switch($t(t,Ln(t)-3-(~pl(t,"!important")&&10))){case 107:return Je(t,":",":"+Qe)+t;case 101:return Je(t,/(.+:)([^;!]+)(;|!.+)?/,"$1"+Qe+($t(t,14)===45?"inline-":"")+"box$3$1"+Qe+"$2$3$1"+zt+"$2box$3")+t}break;case 5936:switch($t(t,e+11)){case 114:return Qe+t+zt+Je(t,/[svh]\w+-[tblr]{2}/,"tb")+t;case 108:return Qe+t+zt+Je(t,/[svh]\w+-[tblr]{2}/,"tb-rl")+t;case 45:return Qe+t+zt+Je(t,/[svh]\w+-[tblr]{2}/,"lr")+t}return Qe+t+zt+t+t}return t}var zx=function(e,n,r,s){if(e.length>-1&&!e.return)switch(e.type){case gl:e.return=Yu(e.value,e.length);break;case Vu:return Kr([$s(e,{value:Je(e.value,"@","@"+Qe)})],s);case hl:if(e.length)return wx(e.props,function(o){switch(xx(o,/(::plac\w+|:read-\w+)/)){case":read-only":case":read-write":return Kr([$s(e,{props:[Je(o,/:(read-\w+)/,":"+qo+"$1")]})],s);case"::placeholder":return Kr([$s(e,{props:[Je(o,/:(plac\w+)/,":"+Qe+"input-$1")]}),$s(e,{props:[Je(o,/:(plac\w+)/,":"+qo+"$1")]}),$s(e,{props:[Je(o,/:(plac\w+)/,zt+"input-$1")]})],s)}return""})}},Hx=[zx],Ux=function(e){var n=e.key;if(n==="css"){var r=document.querySelectorAll("style[data-emotion]:not([data-s])");Array.prototype.forEach.call(r,function(p){var y=p.getAttribute("data-emotion");y.indexOf(" ")!==-1&&(document.head.appendChild(p),p.setAttribute("data-s",""))})}var s=e.stylisPlugins||Hx,o={},i,a=[];i=e.container||document.head,Array.prototype.forEach.call(document.querySelectorAll('style[data-emotion^="'+n+' "]'),function(p){for(var y=p.getAttribute("data-emotion").split(" "),b=1;b<y.length;b++)o[y[b]]=!0;a.push(p)});var l,c=[Fx,Vx];{var d,u=[Tx,Dx(function(p){d.insert(p)})],f=Ax(c.concat(s,u)),g=function(y){return Kr(Nx(y),f)};l=function(y,b,_,x){d=_,g(y?y+"{"+b.styles+"}":b.styles),x&&(m.inserted[b.name]=!0)}}var m={key:n,sheet:new gx({key:n,container:i,nonce:e.nonce,speedy:e.speedy,prepend:e.prepend,insertionPoint:e.insertionPoint}),nonce:e.nonce,inserted:o,registered:{},insert:l};return m.sheet.hydrate(a),m},yl={exports:{}},Re={};/** @license React v16.13.1 + * react-is.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Xu;function Bx(){if(Xu)return Re;Xu=1;var t=typeof Symbol=="function"&&Symbol.for,e=t?Symbol.for("react.element"):60103,n=t?Symbol.for("react.portal"):60106,r=t?Symbol.for("react.fragment"):60107,s=t?Symbol.for("react.strict_mode"):60108,o=t?Symbol.for("react.profiler"):60114,i=t?Symbol.for("react.provider"):60109,a=t?Symbol.for("react.context"):60110,l=t?Symbol.for("react.async_mode"):60111,c=t?Symbol.for("react.concurrent_mode"):60111,d=t?Symbol.for("react.forward_ref"):60112,u=t?Symbol.for("react.suspense"):60113,f=t?Symbol.for("react.suspense_list"):60120,g=t?Symbol.for("react.memo"):60115,m=t?Symbol.for("react.lazy"):60116,p=t?Symbol.for("react.block"):60121,y=t?Symbol.for("react.fundamental"):60117,b=t?Symbol.for("react.responder"):60118,_=t?Symbol.for("react.scope"):60119;function x(S){if(typeof S=="object"&&S!==null){var w=S.$$typeof;switch(w){case e:switch(S=S.type,S){case l:case c:case r:case o:case s:case u:return S;default:switch(S=S&&S.$$typeof,S){case a:case d:case m:case g:case i:return S;default:return w}}case n:return w}}}function E(S){return x(S)===c}return Re.AsyncMode=l,Re.ConcurrentMode=c,Re.ContextConsumer=a,Re.ContextProvider=i,Re.Element=e,Re.ForwardRef=d,Re.Fragment=r,Re.Lazy=m,Re.Memo=g,Re.Portal=n,Re.Profiler=o,Re.StrictMode=s,Re.Suspense=u,Re.isAsyncMode=function(S){return E(S)||x(S)===l},Re.isConcurrentMode=E,Re.isContextConsumer=function(S){return x(S)===a},Re.isContextProvider=function(S){return x(S)===i},Re.isElement=function(S){return typeof S=="object"&&S!==null&&S.$$typeof===e},Re.isForwardRef=function(S){return x(S)===d},Re.isFragment=function(S){return x(S)===r},Re.isLazy=function(S){return x(S)===m},Re.isMemo=function(S){return x(S)===g},Re.isPortal=function(S){return x(S)===n},Re.isProfiler=function(S){return x(S)===o},Re.isStrictMode=function(S){return x(S)===s},Re.isSuspense=function(S){return x(S)===u},Re.isValidElementType=function(S){return typeof S=="string"||typeof S=="function"||S===r||S===c||S===o||S===s||S===u||S===f||typeof S=="object"&&S!==null&&(S.$$typeof===m||S.$$typeof===g||S.$$typeof===i||S.$$typeof===a||S.$$typeof===d||S.$$typeof===y||S.$$typeof===b||S.$$typeof===_||S.$$typeof===p)},Re.typeOf=x,Re}var Ku;function jx(){return Ku||(Ku=1,yl.exports=Bx()),yl.exports}var vl,qu;function Wx(){if(qu)return vl;qu=1;var t=jx(),e={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},n={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},r={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},s={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},o={};o[t.ForwardRef]=r,o[t.Memo]=s;function i(m){return t.isMemo(m)?s:o[m.$$typeof]||e}var a=Object.defineProperty,l=Object.getOwnPropertyNames,c=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,u=Object.getPrototypeOf,f=Object.prototype;function g(m,p,y){if(typeof p!="string"){if(f){var b=u(p);b&&b!==f&&g(m,b,y)}var _=l(p);c&&(_=_.concat(c(p)));for(var x=i(m),E=i(p),S=0;S<_.length;++S){var w=_[S];if(!n[w]&&!(y&&y[w])&&!(E&&E[w])&&!(x&&x[w])){var N=d(p,w);try{a(m,w,N)}catch{}}}}return m}return vl=g,vl}Wx();var Gx=!0;function Yx(t,e,n){var r="";return n.split(" ").forEach(function(s){t[s]!==void 0?e.push(t[s]+";"):s&&(r+=s+" ")}),r}var Zu=function(e,n,r){var s=e.key+"-"+n.name;(r===!1||Gx===!1)&&e.registered[s]===void 0&&(e.registered[s]=n.styles)},Xx=function(e,n,r){Zu(e,n,r);var s=e.key+"-"+n.name;if(e.inserted[n.name]===void 0){var o=n;do e.insert(n===o?"."+s:"",o,e.sheet,!0),o=o.next;while(o!==void 0)}};function Kx(t){for(var e=0,n,r=0,s=t.length;s>=4;++r,s-=4)n=t.charCodeAt(r)&255|(t.charCodeAt(++r)&255)<<8|(t.charCodeAt(++r)&255)<<16|(t.charCodeAt(++r)&255)<<24,n=(n&65535)*1540483477+((n>>>16)*59797<<16),n^=n>>>24,e=(n&65535)*1540483477+((n>>>16)*59797<<16)^(e&65535)*1540483477+((e>>>16)*59797<<16);switch(s){case 3:e^=(t.charCodeAt(r+2)&255)<<16;case 2:e^=(t.charCodeAt(r+1)&255)<<8;case 1:e^=t.charCodeAt(r)&255,e=(e&65535)*1540483477+((e>>>16)*59797<<16)}return e^=e>>>13,e=(e&65535)*1540483477+((e>>>16)*59797<<16),((e^e>>>15)>>>0).toString(36)}var qx={animationIterationCount:1,aspectRatio:1,borderImageOutset:1,borderImageSlice:1,borderImageWidth:1,boxFlex:1,boxFlexGroup:1,boxOrdinalGroup:1,columnCount:1,columns:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,flexOrder:1,gridRow:1,gridRowEnd:1,gridRowSpan:1,gridRowStart:1,gridColumn:1,gridColumnEnd:1,gridColumnSpan:1,gridColumnStart:1,msGridRow:1,msGridRowSpan:1,msGridColumn:1,msGridColumnSpan:1,fontWeight:1,lineHeight:1,opacity:1,order:1,orphans:1,scale:1,tabSize:1,widows:1,zIndex:1,zoom:1,WebkitLineClamp:1,fillOpacity:1,floodOpacity:1,stopOpacity:1,strokeDasharray:1,strokeDashoffset:1,strokeMiterlimit:1,strokeOpacity:1,strokeWidth:1},Zx=/[A-Z]|^ms/g,Qx=/_EMO_([^_]+?)_([^]*?)_EMO_/g,Qu=function(e){return e.charCodeAt(1)===45},Ju=function(e){return e!=null&&typeof e!="boolean"},xl=Ix(function(t){return Qu(t)?t:t.replace(Zx,"-$&").toLowerCase()}),Ru=function(e,n){switch(e){case"animation":case"animationName":if(typeof n=="string")return n.replace(Qx,function(r,s,o){return $n={name:s,styles:o,next:$n},s})}return qx[e]!==1&&!Qu(e)&&typeof n=="number"&&n!==0?n+"px":n};function zs(t,e,n){if(n==null)return"";var r=n;if(r.__emotion_styles!==void 0)return r;switch(typeof n){case"boolean":return"";case"object":{var s=n;if(s.anim===1)return $n={name:s.name,styles:s.styles,next:$n},s.name;var o=n;if(o.styles!==void 0){var i=o.next;if(i!==void 0)for(;i!==void 0;)$n={name:i.name,styles:i.styles,next:$n},i=i.next;var a=o.styles+";";return a}return Jx(t,e,n)}case"function":{if(t!==void 0){var l=$n,c=n(t);return $n=l,zs(t,e,c)}break}}var d=n;return d}function Jx(t,e,n){var r="";if(Array.isArray(n))for(var s=0;s<n.length;s++)r+=zs(t,e,n[s])+";";else for(var o in n){var i=n[o];if(typeof i!="object"){var a=i;Ju(a)&&(r+=xl(o)+":"+Ru(o,a)+";")}else if(Array.isArray(i)&&typeof i[0]=="string"&&e==null)for(var l=0;l<i.length;l++)Ju(i[l])&&(r+=xl(o)+":"+Ru(o,i[l])+";");else{var c=zs(t,e,i);switch(o){case"animation":case"animationName":{r+=xl(o)+":"+c+";";break}default:r+=o+"{"+c+"}"}}}return r}var ef=/label:\s*([^\s;{]+)\s*(;|$)/g,$n;function tf(t,e,n){if(t.length===1&&typeof t[0]=="object"&&t[0]!==null&&t[0].styles!==void 0)return t[0];var r=!0,s="";$n=void 0;var o=t[0];if(o==null||o.raw===void 0)r=!1,s+=zs(n,e,o);else{var i=o;s+=i[0]}for(var a=1;a<t.length;a++)if(s+=zs(n,e,t[a]),r){var l=o;s+=l[a]}ef.lastIndex=0;for(var c="",d;(d=ef.exec(s))!==null;)c+="-"+d[1];var u=Kx(s)+c;return{name:u,styles:s,next:$n}}var Rx=function(e){return e()},ew=$.useInsertionEffect?$.useInsertionEffect:!1,tw=ew||Rx,nf=$.createContext(typeof HTMLElement<"u"?Ux({key:"css"}):null);nf.Provider;var nw=function(e){return v.forwardRef(function(n,r){var s=v.useContext(nf);return e(n,s,r)})},rw=$.createContext({}),wl={}.hasOwnProperty,kl="__EMOTION_TYPE_PLEASE_DO_NOT_USE__",sw=function(e,n){var r={};for(var s in n)wl.call(n,s)&&(r[s]=n[s]);return r[kl]=e,r},ow=function(e){var n=e.cache,r=e.serialized,s=e.isStringTag;return Zu(n,r,s),tw(function(){return Xx(n,r,s)}),null},iw=nw(function(t,e,n){var r=t.css;typeof r=="string"&&e.registered[r]!==void 0&&(r=e.registered[r]);var s=t[kl],o=[r],i="";typeof t.className=="string"?i=Yx(e.registered,o,t.className):t.className!=null&&(i=t.className+" ");var a=tf(o,void 0,$.useContext(rw));i+=e.key+"-"+a.name;var l={};for(var c in t)wl.call(t,c)&&c!=="css"&&c!==kl&&(l[c]=t[c]);return l.className=i,n&&(l.ref=n),$.createElement($.Fragment,null,$.createElement(ow,{cache:e,serialized:a,isStringTag:typeof s=="string"}),$.createElement(s,l))}),aw=iw,_e=function(e,n){var r=arguments;if(n==null||!wl.call(n,"css"))return $.createElement.apply(void 0,r);var s=r.length,o=new Array(s);o[0]=aw,o[1]=sw(e,n);for(var i=2;i<s;i++)o[i]=r[i];return $.createElement.apply(null,o)};(function(t){var e;e||(e=t.JSX||(t.JSX={}))})(_e||(_e={}));function Cl(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return tf(e)}function lw(){var t=Cl.apply(void 0,arguments),e="animation-"+t.name;return{name:e,styles:"@keyframes "+e+"{"+t.styles+"}",anim:1,toString:function(){return"_EMO_"+this.name+"_"+this.styles+"_EMO_"}}}function cw(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}const dw=Math.min,uw=Math.max,ri=Math.round,si=Math.floor,oi=t=>({x:t,y:t});function fw(t){const{x:e,y:n,width:r,height:s}=t;return{width:r,height:s,top:n,left:e,right:e+r,bottom:n+s,x:e,y:n}}function ii(){return typeof window<"u"}function rf(t){return of(t)?(t.nodeName||"").toLowerCase():"#document"}function Yn(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function sf(t){var e;return(e=(of(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function of(t){return ii()?t instanceof Node||t instanceof Yn(t).Node:!1}function hw(t){return ii()?t instanceof Element||t instanceof Yn(t).Element:!1}function Sl(t){return ii()?t instanceof HTMLElement||t instanceof Yn(t).HTMLElement:!1}function af(t){return!ii()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof Yn(t).ShadowRoot}const gw=new Set(["inline","contents"]);function lf(t){const{overflow:e,overflowX:n,overflowY:r,display:s}=_l(t);return/auto|scroll|overlay|hidden|clip/.test(e+r+n)&&!gw.has(s)}function pw(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}const mw=new Set(["html","body","#document"]);function bw(t){return mw.has(rf(t))}function _l(t){return Yn(t).getComputedStyle(t)}function yw(t){if(rf(t)==="html")return t;const e=t.assignedSlot||t.parentNode||af(t)&&t.host||sf(t);return af(e)?e.host:e}function cf(t){const e=yw(t);return bw(e)?t.ownerDocument?t.ownerDocument.body:t.body:Sl(e)&&lf(e)?e:cf(e)}function ai(t,e,n){var r;e===void 0&&(e=[]),n===void 0&&(n=!0);const s=cf(t),o=s===((r=t.ownerDocument)==null?void 0:r.body),i=Yn(s);if(o){const a=El(i);return e.concat(i,i.visualViewport||[],lf(s)?s:[],a&&n?ai(a):[])}return e.concat(s,ai(s,[],n))}function El(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function vw(t){const e=_l(t);let n=parseFloat(e.width)||0,r=parseFloat(e.height)||0;const s=Sl(t),o=s?t.offsetWidth:n,i=s?t.offsetHeight:r,a=ri(n)!==o||ri(r)!==i;return a&&(n=o,r=i),{width:n,height:r,$:a}}function Ml(t){return hw(t)?t:t.contextElement}function df(t){const e=Ml(t);if(!Sl(e))return oi(1);const n=e.getBoundingClientRect(),{width:r,height:s,$:o}=vw(e);let i=(o?ri(n.width):n.width)/r,a=(o?ri(n.height):n.height)/s;return(!i||!Number.isFinite(i))&&(i=1),(!a||!Number.isFinite(a))&&(a=1),{x:i,y:a}}const xw=oi(0);function ww(t){const e=Yn(t);return!pw()||!e.visualViewport?xw:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function kw(t,e,n){return!1}function uf(t,e,n,r){e===void 0&&(e=!1);const s=t.getBoundingClientRect(),o=Ml(t);let i=oi(1);e&&(i=df(t));const a=kw()?ww(o):oi(0);let l=(s.left+a.x)/i.x,c=(s.top+a.y)/i.y,d=s.width/i.x,u=s.height/i.y;if(o){const f=Yn(o),g=r;let m=f,p=El(m);for(;p&&r&&g!==m;){const y=df(p),b=p.getBoundingClientRect(),_=_l(p),x=b.left+(p.clientLeft+parseFloat(_.paddingLeft))*y.x,E=b.top+(p.clientTop+parseFloat(_.paddingTop))*y.y;l*=y.x,c*=y.y,d*=y.x,u*=y.y,l+=x,c+=E,m=Yn(p),p=El(m)}}return fw({width:d,height:u,x:l,y:c})}function ff(t,e){return t.x===e.x&&t.y===e.y&&t.width===e.width&&t.height===e.height}function Cw(t,e){let n=null,r;const s=sf(t);function o(){var a;clearTimeout(r),(a=n)==null||a.disconnect(),n=null}function i(a,l){a===void 0&&(a=!1),l===void 0&&(l=1),o();const c=t.getBoundingClientRect(),{left:d,top:u,width:f,height:g}=c;if(a||e(),!f||!g)return;const m=si(u),p=si(s.clientWidth-(d+f)),y=si(s.clientHeight-(u+g)),b=si(d),x={rootMargin:-m+"px "+-p+"px "+-y+"px "+-b+"px",threshold:uw(0,dw(1,l))||1};let E=!0;function S(w){const N=w[0].intersectionRatio;if(N!==l){if(!E)return i();N?i(!1,N):r=setTimeout(()=>{i(!1,1e-7)},1e3)}N===1&&!ff(c,t.getBoundingClientRect())&&i(),E=!1}try{n=new IntersectionObserver(S,{...x,root:s.ownerDocument})}catch{n=new IntersectionObserver(S,x)}n.observe(t)}return i(!0),o}function Sw(t,e,n,r){r===void 0&&(r={});const{ancestorScroll:s=!0,ancestorResize:o=!0,elementResize:i=typeof ResizeObserver=="function",layoutShift:a=typeof IntersectionObserver=="function",animationFrame:l=!1}=r,c=Ml(t),d=s||o?[...c?ai(c):[],...ai(e)]:[];d.forEach(b=>{s&&b.addEventListener("scroll",n,{passive:!0}),o&&b.addEventListener("resize",n)});const u=c&&a?Cw(c,n):null;let f=-1,g=null;i&&(g=new ResizeObserver(b=>{let[_]=b;_&&_.target===c&&g&&(g.unobserve(e),cancelAnimationFrame(f),f=requestAnimationFrame(()=>{var x;(x=g)==null||x.observe(e)})),n()}),c&&!l&&g.observe(c),g.observe(e));let m,p=l?uf(t):null;l&&y();function y(){const b=uf(t);p&&!ff(p,b)&&n(),p=b,m=requestAnimationFrame(y)}return n(),()=>{var b;d.forEach(_=>{s&&_.removeEventListener("scroll",n),o&&_.removeEventListener("resize",n)}),u?.(),(b=g)==null||b.disconnect(),g=null,l&&cancelAnimationFrame(m)}}var Nl=v.useLayoutEffect,_w=["className","clearValue","cx","getStyles","getClassNames","getValue","hasValue","isMulti","isRtl","options","selectOption","selectProps","setValue","theme"],li=function(){};function Ew(t,e){return e?e[0]==="-"?t+e:t+"__"+e:t}function Mw(t,e){for(var n=arguments.length,r=new Array(n>2?n-2:0),s=2;s<n;s++)r[s-2]=arguments[s];var o=[].concat(r);if(e&&t)for(var i in e)e.hasOwnProperty(i)&&e[i]&&o.push("".concat(Ew(t,i)));return o.filter(function(a){return a}).map(function(a){return String(a).trim()}).join(" ")}var ci=function(e){return $w(e)?e.filter(Boolean):pr(e)==="object"&&e!==null?[e]:[]},hf=function(e){e.className,e.clearValue,e.cx,e.getStyles,e.getClassNames,e.getValue,e.hasValue,e.isMulti,e.isRtl,e.options,e.selectOption,e.selectProps,e.setValue,e.theme;var n=In(e,_w);return Ee({},n)},vt=function(e,n,r){var s=e.cx,o=e.getStyles,i=e.getClassNames,a=e.className;return{css:o(n,e),className:s(r??{},i(n,e),a)}};function di(t){return[document.documentElement,document.body,window].indexOf(t)>-1}function Nw(t){return di(t)?window.innerHeight:t.clientHeight}function gf(t){return di(t)?window.pageYOffset:t.scrollTop}function ui(t,e){if(di(t)){window.scrollTo(0,e);return}t.scrollTop=e}function Ow(t){var e=getComputedStyle(t),n=e.position==="absolute",r=/(auto|scroll)/;if(e.position==="fixed")return document.documentElement;for(var s=t;s=s.parentElement;)if(e=getComputedStyle(s),!(n&&e.position==="static")&&r.test(e.overflow+e.overflowY+e.overflowX))return s;return document.documentElement}function Tw(t,e,n,r){return n*((t=t/r-1)*t*t+1)+e}function fi(t,e){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:200,r=arguments.length>3&&arguments[3]!==void 0?arguments[3]:li,s=gf(t),o=e-s,i=10,a=0;function l(){a+=i;var c=Tw(a,s,o,n);ui(t,c),a<n?window.requestAnimationFrame(l):r(t)}l()}function pf(t,e){var n=t.getBoundingClientRect(),r=e.getBoundingClientRect(),s=e.offsetHeight/3;r.bottom+s>n.bottom?ui(t,Math.min(e.offsetTop+e.clientHeight-t.offsetHeight+s,t.scrollHeight)):r.top-s<n.top&&ui(t,Math.max(e.offsetTop-s,0))}function Aw(t){var e=t.getBoundingClientRect();return{bottom:e.bottom,height:e.height,left:e.left,right:e.right,top:e.top,width:e.width}}function mf(){try{return document.createEvent("TouchEvent"),!0}catch{return!1}}function Dw(){try{return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)}catch{return!1}}var bf=!1,Iw={get passive(){return bf=!0}},hi=typeof window<"u"?window:{};hi.addEventListener&&hi.removeEventListener&&(hi.addEventListener("p",li,Iw),hi.removeEventListener("p",li,!1));var Lw=bf;function Pw(t){return t!=null}function $w(t){return Array.isArray(t)}function Hs(t,e,n){return t?e:n}function HT(t){return t}function UT(t){return t}var Fw=function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];var o=Object.entries(e).filter(function(i){var a=Gn(i,1),l=a[0];return!r.includes(l)});return o.reduce(function(i,a){var l=Gn(a,2),c=l[0],d=l[1];return i[c]=d,i},{})},Vw=["children","innerProps"],zw=["children","innerProps"];function Hw(t){var e=t.maxHeight,n=t.menuEl,r=t.minHeight,s=t.placement,o=t.shouldScroll,i=t.isFixedPosition,a=t.controlHeight,l=Ow(n),c={placement:"bottom",maxHeight:e};if(!n||!n.offsetParent)return c;var d=l.getBoundingClientRect(),u=d.height,f=n.getBoundingClientRect(),g=f.bottom,m=f.height,p=f.top,y=n.offsetParent.getBoundingClientRect(),b=y.top,_=i?window.innerHeight:Nw(l),x=gf(l),E=parseInt(getComputedStyle(n).marginBottom,10),S=parseInt(getComputedStyle(n).marginTop,10),w=b-S,N=_-p,C=w+x,k=u-x-p,M=g-_+x+E,T=x+p-S,L=160;switch(s){case"auto":case"bottom":if(N>=m)return{placement:"bottom",maxHeight:e};if(k>=m&&!i)return o&&fi(l,M,L),{placement:"bottom",maxHeight:e};if(!i&&k>=r||i&&N>=r){o&&fi(l,M,L);var P=i?N-E:k-E;return{placement:"bottom",maxHeight:P}}if(s==="auto"||i){var F=e,D=i?w:C;return D>=r&&(F=Math.min(D-E-a,e)),{placement:"top",maxHeight:F}}if(s==="bottom")return o&&ui(l,M),{placement:"bottom",maxHeight:e};break;case"top":if(w>=m)return{placement:"top",maxHeight:e};if(C>=m&&!i)return o&&fi(l,T,L),{placement:"top",maxHeight:e};if(!i&&C>=r||i&&w>=r){var A=e;return(!i&&C>=r||i&&w>=r)&&(A=i?w-S:C-S),o&&fi(l,T,L),{placement:"top",maxHeight:A}}return{placement:"bottom",maxHeight:e};default:throw new Error('Invalid placement provided "'.concat(s,'".'))}return c}function Uw(t){var e={bottom:"top",top:"bottom"};return t?e[t]:"bottom"}var yf=function(e){return e==="auto"?"bottom":e},Bw=function(e,n){var r,s=e.placement,o=e.theme,i=o.borderRadius,a=o.spacing,l=o.colors;return Ee((r={label:"menu"},Ls(r,Uw(s),"100%"),Ls(r,"position","absolute"),Ls(r,"width","100%"),Ls(r,"zIndex",1),r),n?{}:{backgroundColor:l.neutral0,borderRadius:i,boxShadow:"0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 11px hsla(0, 0%, 0%, 0.1)",marginBottom:a.menuGutter,marginTop:a.menuGutter})},vf=v.createContext(null),jw=function(e){var n=e.children,r=e.minMenuHeight,s=e.maxMenuHeight,o=e.menuPlacement,i=e.menuPosition,a=e.menuShouldScrollIntoView,l=e.theme,c=v.useContext(vf)||{},d=c.setPortalPlacement,u=v.useRef(null),f=v.useState(s),g=Gn(f,2),m=g[0],p=g[1],y=v.useState(null),b=Gn(y,2),_=b[0],x=b[1],E=l.spacing.controlHeight;return Nl(function(){var S=u.current;if(S){var w=i==="fixed",N=a&&!w,C=Hw({maxHeight:s,menuEl:S,minHeight:r,placement:o,shouldScroll:N,isFixedPosition:w,controlHeight:E});p(C.maxHeight),x(C.placement),d?.(C.placement)}},[s,o,i,a,r,d,E]),n({ref:u,placerProps:Ee(Ee({},e),{},{placement:_||yf(o),maxHeight:m})})},Ww=function(e){var n=e.children,r=e.innerRef,s=e.innerProps;return _e("div",Ae({},vt(e,"menu",{menu:!0}),{ref:r},s),n)},Gw=Ww,Yw=function(e,n){var r=e.maxHeight,s=e.theme.spacing.baseUnit;return Ee({maxHeight:r,overflowY:"auto",position:"relative",WebkitOverflowScrolling:"touch"},n?{}:{paddingBottom:s,paddingTop:s})},Xw=function(e){var n=e.children,r=e.innerProps,s=e.innerRef,o=e.isMulti;return _e("div",Ae({},vt(e,"menuList",{"menu-list":!0,"menu-list--is-multi":o}),{ref:s},r),n)},xf=function(e,n){var r=e.theme,s=r.spacing.baseUnit,o=r.colors;return Ee({textAlign:"center"},n?{}:{color:o.neutral40,padding:"".concat(s*2,"px ").concat(s*3,"px")})},Kw=xf,qw=xf,Zw=function(e){var n=e.children,r=n===void 0?"No options":n,s=e.innerProps,o=In(e,Vw);return _e("div",Ae({},vt(Ee(Ee({},o),{},{children:r,innerProps:s}),"noOptionsMessage",{"menu-notice":!0,"menu-notice--no-options":!0}),s),r)},Qw=function(e){var n=e.children,r=n===void 0?"Loading...":n,s=e.innerProps,o=In(e,zw);return _e("div",Ae({},vt(Ee(Ee({},o),{},{children:r,innerProps:s}),"loadingMessage",{"menu-notice":!0,"menu-notice--loading":!0}),s),r)},Jw=function(e){var n=e.rect,r=e.offset,s=e.position;return{left:n.left,position:s,top:r,width:n.width,zIndex:1}},Rw=function(e){var n=e.appendTo,r=e.children,s=e.controlElement,o=e.innerProps,i=e.menuPlacement,a=e.menuPosition,l=v.useRef(null),c=v.useRef(null),d=v.useState(yf(i)),u=Gn(d,2),f=u[0],g=u[1],m=v.useMemo(function(){return{setPortalPlacement:g}},[]),p=v.useState(null),y=Gn(p,2),b=y[0],_=y[1],x=v.useCallback(function(){if(s){var N=Aw(s),C=a==="fixed"?0:window.pageYOffset,k=N[f]+C;(k!==b?.offset||N.left!==b?.rect.left||N.width!==b?.rect.width)&&_({offset:k,rect:N})}},[s,a,f,b?.offset,b?.rect.left,b?.rect.width]);Nl(function(){x()},[x]);var E=v.useCallback(function(){typeof c.current=="function"&&(c.current(),c.current=null),s&&l.current&&(c.current=Sw(s,l.current,x,{elementResize:"ResizeObserver"in window}))},[s,x]);Nl(function(){E()},[E]);var S=v.useCallback(function(N){l.current=N,E()},[E]);if(!n&&a!=="fixed"||!b)return null;var w=_e("div",Ae({ref:S},vt(Ee(Ee({},e),{},{offset:b.offset,position:a,rect:b.rect}),"menuPortal",{"menu-portal":!0}),o),r);return _e(vf.Provider,{value:m},n?Dn.createPortal(w,n):w)},e1=function(e){var n=e.isDisabled,r=e.isRtl;return{label:"container",direction:r?"rtl":void 0,pointerEvents:n?"none":void 0,position:"relative"}},t1=function(e){var n=e.children,r=e.innerProps,s=e.isDisabled,o=e.isRtl;return _e("div",Ae({},vt(e,"container",{"--is-disabled":s,"--is-rtl":o}),r),n)},n1=function(e,n){var r=e.theme.spacing,s=e.isMulti,o=e.hasValue,i=e.selectProps.controlShouldRenderValue;return Ee({alignItems:"center",display:s&&o&&i?"flex":"grid",flex:1,flexWrap:"wrap",WebkitOverflowScrolling:"touch",position:"relative",overflow:"hidden"},n?{}:{padding:"".concat(r.baseUnit/2,"px ").concat(r.baseUnit*2,"px")})},r1=function(e){var n=e.children,r=e.innerProps,s=e.isMulti,o=e.hasValue;return _e("div",Ae({},vt(e,"valueContainer",{"value-container":!0,"value-container--is-multi":s,"value-container--has-value":o}),r),n)},s1=function(){return{alignItems:"center",alignSelf:"stretch",display:"flex",flexShrink:0}},o1=function(e){var n=e.children,r=e.innerProps;return _e("div",Ae({},vt(e,"indicatorsContainer",{indicators:!0}),r),n)},wf,i1=["size"],a1=["innerProps","isRtl","size"],l1={name:"8mmkcg",styles:"display:inline-block;fill:currentColor;line-height:1;stroke:currentColor;stroke-width:0"},kf=function(e){var n=e.size,r=In(e,i1);return _e("svg",Ae({height:n,width:n,viewBox:"0 0 20 20","aria-hidden":"true",focusable:"false",css:l1},r))},Ol=function(e){return _e(kf,Ae({size:20},e),_e("path",{d:"M14.348 14.849c-0.469 0.469-1.229 0.469-1.697 0l-2.651-3.030-2.651 3.029c-0.469 0.469-1.229 0.469-1.697 0-0.469-0.469-0.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-0.469-0.469-0.469-1.228 0-1.697s1.228-0.469 1.697 0l2.652 3.031 2.651-3.031c0.469-0.469 1.228-0.469 1.697 0s0.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c0.469 0.469 0.469 1.229 0 1.698z"}))},Cf=function(e){return _e(kf,Ae({size:20},e),_e("path",{d:"M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"}))},Sf=function(e,n){var r=e.isFocused,s=e.theme,o=s.spacing.baseUnit,i=s.colors;return Ee({label:"indicatorContainer",display:"flex",transition:"color 150ms"},n?{}:{color:r?i.neutral60:i.neutral20,padding:o*2,":hover":{color:r?i.neutral80:i.neutral40}})},c1=Sf,d1=function(e){var n=e.children,r=e.innerProps;return _e("div",Ae({},vt(e,"dropdownIndicator",{indicator:!0,"dropdown-indicator":!0}),r),n||_e(Cf,null))},u1=Sf,f1=function(e){var n=e.children,r=e.innerProps;return _e("div",Ae({},vt(e,"clearIndicator",{indicator:!0,"clear-indicator":!0}),r),n||_e(Ol,null))},h1=function(e,n){var r=e.isDisabled,s=e.theme,o=s.spacing.baseUnit,i=s.colors;return Ee({label:"indicatorSeparator",alignSelf:"stretch",width:1},n?{}:{backgroundColor:r?i.neutral10:i.neutral20,marginBottom:o*2,marginTop:o*2})},g1=function(e){var n=e.innerProps;return _e("span",Ae({},n,vt(e,"indicatorSeparator",{"indicator-separator":!0})))},p1=lw(wf||(wf=cw([` 0%, 80%, 100% { opacity: 0; } 40% { opacity: 1; } -`]))),P1=function(t,n){var r=t.isFocused,s=t.size,o=t.theme,i=o.colors,a=o.spacing.baseUnit;return Ee({label:"loadingIndicator",display:"flex",transition:"color 150ms",alignSelf:"center",fontSize:s,lineHeight:1,marginRight:s,textAlign:"center",verticalAlign:"middle"},n?{}:{color:r?i.neutral60:i.neutral20,padding:a*2})},Fl=function(t){var n=t.delay,r=t.offset;return Se("span",{css:Al({animation:"".concat(L1," 1s ease-in-out ").concat(n,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:r?"1em":void 0,height:"1em",verticalAlign:"top",width:"1em"},"","")})},$1=function(t){var n=t.innerProps,r=t.isRtl,s=t.size,o=s===void 0?4:s,i=Vn(t,E1);return Se("div",Oe({},wt(Ee(Ee({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),Se(Fl,{delay:0,offset:r}),Se(Fl,{delay:160,offset:!0}),Se(Fl,{delay:320,offset:!r}))},F1=function(t,n){var r=t.isDisabled,s=t.isFocused,o=t.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Ee({label:"control",alignItems:"center",cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms"},n?{}:{backgroundColor:r?i.neutral5:i.neutral0,borderColor:r?i.neutral10:s?i.primary:i.neutral20,borderRadius:a,borderStyle:"solid",borderWidth:1,boxShadow:s?"0 0 0 1px ".concat(i.primary):void 0,"&:hover":{borderColor:s?i.primary:i.neutral30}})},z1=function(t){var n=t.children,r=t.isDisabled,s=t.isFocused,o=t.innerRef,i=t.innerProps,a=t.menuIsOpen;return Se("div",Oe({ref:o},wt(t,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},V1=z1,H1=["data"],U1=function(t,n){var r=t.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},B1=function(t){var n=t.children,r=t.cx,s=t.getStyles,o=t.getClassNames,i=t.Heading,a=t.headingProps,l=t.innerProps,c=t.label,d=t.theme,u=t.selectProps;return Se("div",Oe({},wt(t,"group",{group:!0}),l),Se(i,Oe({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),Se("div",null,n))},j1=function(t,n){var r=t.theme,s=r.colors,o=r.spacing;return Ee({label:"group",cursor:"default",display:"block"},n?{}:{color:s.neutral40,fontSize:"75%",fontWeight:500,marginBottom:"0.25em",paddingLeft:o.baseUnit*3,paddingRight:o.baseUnit*3,textTransform:"uppercase"})},W1=function(t){var n=Cf(t);n.data;var r=Vn(n,H1);return Se("div",Oe({},wt(t,"groupHeading",{"group-heading":!0}),r))},G1=B1,Y1=["innerRef","isDisabled","isHidden","inputClassName"],X1=function(t,n){var r=t.isDisabled,s=t.value,o=t.theme,i=o.spacing,a=o.colors;return Ee(Ee({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},K1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},Pf={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},K1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Ee({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},Pf)},q1=function(t){return Ee({label:"input",color:"inherit",background:0,opacity:t?0:1,width:"100%"},Pf)},Z1=function(t){var n=t.cx,r=t.value,s=Cf(t),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=Vn(s,Y1);return Se("div",Oe({},wt(t,"input",{"input-container":!0}),{"data-value":r||""}),Se("input",Oe({className:n({input:!0},l),ref:o,style:q1(a),disabled:i},c)))},Q1=Z1,J1=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Ee({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},R1=function(t,n){var r=t.theme,s=r.borderRadius,o=r.colors,i=t.cropWithEllipsis;return Ee({overflow:"hidden",textOverflow:i||i===void 0?"ellipsis":void 0,whiteSpace:"nowrap"},n?{}:{borderRadius:s/2,color:o.neutral80,fontSize:"85%",padding:3,paddingLeft:6})},ek=function(t,n){var r=t.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=t.isFocused;return Ee({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},$f=function(t){var n=t.children,r=t.innerProps;return Se("div",r,n)},tk=$f,nk=$f;function rk(e){var t=e.children,n=e.innerProps;return Se("div",Oe({role:"button"},n),t||Se($l,{size:14}))}var sk=function(t){var n=t.children,r=t.components,s=t.data,o=t.innerProps,i=t.isDisabled,a=t.removeProps,l=t.selectProps,c=r.Container,d=r.Label,u=r.Remove;return Se(c,{data:s,innerProps:Ee(Ee({},wt(t,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},Se(d,{data:s,innerProps:Ee({},wt(t,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),Se(u,{data:s,innerProps:Ee(Ee({},wt(t,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),selectProps:l}))},ok=sk,ik=function(t,n){var r=t.isDisabled,s=t.isFocused,o=t.isSelected,i=t.theme,a=i.spacing,l=i.colors;return Ee({label:"option",cursor:"default",display:"block",fontSize:"inherit",width:"100%",userSelect:"none",WebkitTapHighlightColor:"rgba(0, 0, 0, 0)"},n?{}:{backgroundColor:o?l.primary:s?l.primary25:"transparent",color:r?l.neutral20:o?l.neutral0:"inherit",padding:"".concat(a.baseUnit*2,"px ").concat(a.baseUnit*3,"px"),":active":{backgroundColor:r?void 0:o?l.primary:l.primary50}})},ak=function(t){var n=t.children,r=t.isDisabled,s=t.isFocused,o=t.isSelected,i=t.innerRef,a=t.innerProps;return Se("div",Oe({},wt(t,"option",{option:!0,"option--is-disabled":r,"option--is-focused":s,"option--is-selected":o}),{ref:i,"aria-disabled":r},a),n)},lk=ak,ck=function(t,n){var r=t.theme,s=r.spacing,o=r.colors;return Ee({label:"placeholder",gridArea:"1 / 1 / 2 / 3"},n?{}:{color:o.neutral50,marginLeft:s.baseUnit/2,marginRight:s.baseUnit/2})},dk=function(t){var n=t.children,r=t.innerProps;return Se("div",Oe({},wt(t,"placeholder",{placeholder:!0}),r),n)},uk=dk,fk=function(t,n){var r=t.isDisabled,s=t.theme,o=s.spacing,i=s.colors;return Ee({label:"singleValue",gridArea:"1 / 1 / 2 / 3",maxWidth:"100%",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},n?{}:{color:r?i.neutral40:i.neutral80,marginLeft:o.baseUnit/2,marginRight:o.baseUnit/2})},hk=function(t){var n=t.children,r=t.isDisabled,s=t.innerProps;return Se("div",Oe({},wt(t,"singleValue",{"single-value":!0,"single-value--is-disabled":r}),s),n)},gk=hk,ki={ClearIndicator:A1,Control:V1,DropdownIndicator:T1,DownChevron:If,CrossIcon:$l,Group:G1,GroupHeading:W1,IndicatorsContainer:S1,IndicatorSeparator:I1,Input:Q1,LoadingIndicator:$1,Menu:d1,MenuList:f1,MenuPortal:y1,LoadingMessage:m1,NoOptionsMessage:p1,MultiValue:ok,MultiValueContainer:tk,MultiValueLabel:nk,MultiValueRemove:rk,Option:lk,Placeholder:uk,SelectContainer:x1,SingleValue:gk,ValueContainer:k1},pk=function(t){return Ee(Ee({},ki),t.components)},Ff=Number.isNaN||function(t){return typeof t=="number"&&t!==t};function mk(e,t){return!!(e===t||Ff(e)&&Ff(t))}function bk(e,t){if(e.length!==t.length)return!1;for(var n=0;n<e.length;n++)if(!mk(e[n],t[n]))return!1;return!0}function yk(e,t){t===void 0&&(t=bk);var n=null;function r(){for(var s=[],o=0;o<arguments.length;o++)s[o]=arguments[o];if(n&&n.lastThis===this&&t(s,n.lastArgs))return n.lastResult;var i=e.apply(this,s);return n={lastResult:i,lastArgs:s,lastThis:this},i}return r.clear=function(){n=null},r}for(var vk={name:"7pg0cj-a11yText",styles:"label:a11yText;z-index:9999;border:0;clip:rect(1px, 1px, 1px, 1px);height:1px;width:1px;position:absolute;overflow:hidden;padding:0;white-space:nowrap"},xk=function(t){return Se("span",Oe({css:vk},t))},zf=xk,wk={guidance:function(t){var n=t.isSearchable,r=t.isMulti,s=t.tabSelectsValue,o=t.context,i=t.isInitialFocus;switch(o){case"menu":return"Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu".concat(s?", press Tab to select the option and exit the menu":"",".");case"input":return i?"".concat(t["aria-label"]||"Select"," is focused ").concat(n?",type to refine list":"",", press Down to open the menu, ").concat(r?" press left to focus selected values":""):"";case"value":return"Use left and right to toggle between focused values, press Backspace to remove the currently focused value";default:return""}},onChange:function(t){var n=t.action,r=t.label,s=r===void 0?"":r,o=t.labels,i=t.isDisabled;switch(n){case"deselect-option":case"pop-value":case"remove-value":return"option ".concat(s,", deselected.");case"clear":return"All selected options have been cleared.";case"initial-input-focus":return"option".concat(o.length>1?"s":""," ").concat(o.join(","),", selected.");case"select-option":return i?"option ".concat(s," is disabled. Select another option."):"option ".concat(s,", selected.");default:return""}},onFocus:function(t){var n=t.context,r=t.focused,s=t.options,o=t.label,i=o===void 0?"":o,a=t.selectValue,l=t.isDisabled,c=t.isSelected,d=t.isAppleDevice,u=function(p,y){return p&&p.length?"".concat(p.indexOf(y)+1," of ").concat(p.length):""};if(n==="value"&&a)return"value ".concat(i," focused, ").concat(u(a,r),".");if(n==="menu"&&d){var f=l?" disabled":"",g="".concat(c?" selected":"").concat(f);return"".concat(i).concat(g,", ").concat(u(s,r),".")}return""},onFilter:function(t){var n=t.inputValue,r=t.resultsMessage;return"".concat(r).concat(n?" for search term "+n:"",".")}},kk=function(t){var n=t.ariaSelection,r=t.focusedOption,s=t.focusedValue,o=t.focusableOptions,i=t.isFocused,a=t.selectValue,l=t.selectProps,c=t.id,d=t.isAppleDevice,u=l.ariaLiveMessages,f=l.getOptionLabel,g=l.inputValue,m=l.isMulti,p=l.isOptionDisabled,y=l.isSearchable,b=l.menuIsOpen,_=l.options,x=l.screenReaderStatus,M=l.tabSelectsValue,C=l.isLoading,k=l["aria-label"],N=l["aria-live"],S=v.useMemo(function(){return Ee(Ee({},wk),u||{})},[u]),w=v.useMemo(function(){var A="";if(n&&S.onChange){var O=n.option,T=n.options,z=n.removedValue,H=n.removedValues,j=n.value,G=function(q){return Array.isArray(q)?null:q},U=z||O||G(j),W=U?f(U):"",Y=T||H||void 0,V=Y?Y.map(f):[],X=Ee({isDisabled:U&&p(U,a),label:W,labels:V},n);A=S.onChange(X)}return A},[n,S,p,a,f]),E=v.useMemo(function(){var A="",O=r||s,T=!!(r&&a&&a.includes(r));if(O&&S.onFocus){var z={focused:O,label:f(O),isDisabled:p(O,a),isSelected:T,options:o,context:O===r?"menu":"value",selectValue:a,isAppleDevice:d};A=S.onFocus(z)}return A},[r,s,f,p,S,o,a,d]),D=v.useMemo(function(){var A="";if(b&&_.length&&!C&&S.onFilter){var O=x({count:o.length});A=S.onFilter({inputValue:g,resultsMessage:O})}return A},[o,g,b,S,_,x,C]),P=n?.action==="initial-input-focus",L=v.useMemo(function(){var A="";if(S.guidance){var O=s?"value":b?"menu":"input";A=S.guidance({"aria-label":k,context:O,isDisabled:r&&p(r,a),isMulti:m,isSearchable:y,tabSelectsValue:M,isInitialFocus:P})}return A},[k,r,s,m,p,y,b,S,a,M,P]),$=Se(v.Fragment,null,Se("span",{id:"aria-selection"},w),Se("span",{id:"aria-focused"},E),Se("span",{id:"aria-results"},D),Se("span",{id:"aria-guidance"},L));return Se(v.Fragment,null,Se(zf,{id:c},P&&$),Se(zf,{"aria-live":N,"aria-atomic":"false","aria-relevant":"additions text",role:"log"},i&&!P&&$))},Ck=kk,zl=[{base:"A",letters:"AⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ"},{base:"AA",letters:"Ꜳ"},{base:"AE",letters:"ÆǼǢ"},{base:"AO",letters:"Ꜵ"},{base:"AU",letters:"Ꜷ"},{base:"AV",letters:"ꜸꜺ"},{base:"AY",letters:"Ꜽ"},{base:"B",letters:"BⒷBḂḄḆɃƂƁ"},{base:"C",letters:"CⒸCĆĈĊČÇḈƇȻꜾ"},{base:"D",letters:"DⒹDḊĎḌḐḒḎĐƋƊƉꝹ"},{base:"DZ",letters:"DZDŽ"},{base:"Dz",letters:"DzDž"},{base:"E",letters:"EⒺEÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ"},{base:"F",letters:"FⒻFḞƑꝻ"},{base:"G",letters:"GⒼGǴĜḠĞĠǦĢǤƓꞠꝽꝾ"},{base:"H",letters:"HⒽHĤḢḦȞḤḨḪĦⱧⱵꞍ"},{base:"I",letters:"IⒾIÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ"},{base:"J",letters:"JⒿJĴɈ"},{base:"K",letters:"KⓀKḰǨḲĶḴƘⱩꝀꝂꝄꞢ"},{base:"L",letters:"LⓁLĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ"},{base:"LJ",letters:"LJ"},{base:"Lj",letters:"Lj"},{base:"M",letters:"MⓂMḾṀṂⱮƜ"},{base:"N",letters:"NⓃNǸŃÑṄŇṆŅṊṈȠƝꞐꞤ"},{base:"NJ",letters:"NJ"},{base:"Nj",letters:"Nj"},{base:"O",letters:"OⓄOÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ"},{base:"OI",letters:"Ƣ"},{base:"OO",letters:"Ꝏ"},{base:"OU",letters:"Ȣ"},{base:"P",letters:"PⓅPṔṖƤⱣꝐꝒꝔ"},{base:"Q",letters:"QⓆQꝖꝘɊ"},{base:"R",letters:"RⓇRŔṘŘȐȒṚṜŖṞɌⱤꝚꞦꞂ"},{base:"S",letters:"SⓈSẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ"},{base:"T",letters:"TⓉTṪŤṬȚŢṰṮŦƬƮȾꞆ"},{base:"TZ",letters:"Ꜩ"},{base:"U",letters:"UⓊUÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ"},{base:"V",letters:"VⓋVṼṾƲꝞɅ"},{base:"VY",letters:"Ꝡ"},{base:"W",letters:"WⓌWẀẂŴẆẄẈⱲ"},{base:"X",letters:"XⓍXẊẌ"},{base:"Y",letters:"YⓎYỲÝŶỸȲẎŸỶỴƳɎỾ"},{base:"Z",letters:"ZⓏZŹẐŻŽẒẔƵȤⱿⱫꝢ"},{base:"a",letters:"aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ"},{base:"aa",letters:"ꜳ"},{base:"ae",letters:"æǽǣ"},{base:"ao",letters:"ꜵ"},{base:"au",letters:"ꜷ"},{base:"av",letters:"ꜹꜻ"},{base:"ay",letters:"ꜽ"},{base:"b",letters:"bⓑbḃḅḇƀƃɓ"},{base:"c",letters:"cⓒcćĉċčçḉƈȼꜿↄ"},{base:"d",letters:"dⓓdḋďḍḑḓḏđƌɖɗꝺ"},{base:"dz",letters:"dzdž"},{base:"e",letters:"eⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ"},{base:"f",letters:"fⓕfḟƒꝼ"},{base:"g",letters:"gⓖgǵĝḡğġǧģǥɠꞡᵹꝿ"},{base:"h",letters:"hⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ"},{base:"hv",letters:"ƕ"},{base:"i",letters:"iⓘiìíîĩīĭïḯỉǐȉȋịįḭɨı"},{base:"j",letters:"jⓙjĵǰɉ"},{base:"k",letters:"kⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ"},{base:"l",letters:"lⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ"},{base:"lj",letters:"lj"},{base:"m",letters:"mⓜmḿṁṃɱɯ"},{base:"n",letters:"nⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ"},{base:"nj",letters:"nj"},{base:"o",letters:"oⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ"},{base:"oi",letters:"ƣ"},{base:"ou",letters:"ȣ"},{base:"oo",letters:"ꝏ"},{base:"p",letters:"pⓟpṕṗƥᵽꝑꝓꝕ"},{base:"q",letters:"qⓠqɋꝗꝙ"},{base:"r",letters:"rⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ"},{base:"s",letters:"sⓢsßśṥŝṡšṧṣṩșşȿꞩꞅẛ"},{base:"t",letters:"tⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ"},{base:"tz",letters:"ꜩ"},{base:"u",letters:"uⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ"},{base:"v",letters:"vⓥvṽṿʋꝟʌ"},{base:"vy",letters:"ꝡ"},{base:"w",letters:"wⓦwẁẃŵẇẅẘẉⱳ"},{base:"x",letters:"xⓧxẋẍ"},{base:"y",letters:"yⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ"},{base:"z",letters:"zⓩzźẑżžẓẕƶȥɀⱬꝣ"}],Sk=new RegExp("["+zl.map(function(e){return e.letters}).join("")+"]","g"),Vf={},Vl=0;Vl<zl.length;Vl++)for(var Hl=zl[Vl],Ul=0;Ul<Hl.letters.length;Ul++)Vf[Hl.letters[Ul]]=Hl.base;var Hf=function(t){return t.replace(Sk,function(n){return Vf[n]})},_k=yk(Hf),Uf=function(t){return t.replace(/^\s+|\s+$/g,"")},Ek=function(t){return"".concat(t.label," ").concat(t.value)},Mk=function(t){return function(n,r){if(n.data.__isNew__)return!0;var s=Ee({ignoreCase:!0,ignoreAccents:!0,stringify:Ek,trim:!0,matchFrom:"any"},t),o=s.ignoreCase,i=s.ignoreAccents,a=s.stringify,l=s.trim,c=s.matchFrom,d=l?Uf(r):r,u=l?Uf(a(n)):a(n);return o&&(d=d.toLowerCase(),u=u.toLowerCase()),i&&(d=_k(d),u=Hf(u)),c==="start"?u.substr(0,d.length)===d:u.indexOf(d)>-1}},Nk=["innerRef"];function Tk(e){var t=e.innerRef,n=Vn(e,Nk),r=n1(n,"onExited","in","enter","exit","appear");return Se("input",Oe({ref:t},r,{css:Al({label:"dummyInput",background:0,border:0,caretColor:"transparent",fontSize:"inherit",gridArea:"1 / 1 / 2 / 3",outline:0,padding:0,width:1,color:"transparent",left:-100,opacity:0,position:"relative",transform:"scale(.01)"},"","")}))}var Ok=function(t){t.cancelable&&t.preventDefault(),t.stopPropagation()};function Ak(e){var t=e.isEnabled,n=e.onBottomArrive,r=e.onBottomLeave,s=e.onTopArrive,o=e.onTopLeave,i=v.useRef(!1),a=v.useRef(!1),l=v.useRef(0),c=v.useRef(null),d=v.useCallback(function(y,b){if(c.current!==null){var _=c.current,x=_.scrollTop,M=_.scrollHeight,C=_.clientHeight,k=c.current,N=b>0,S=M-C-x,w=!1;S>b&&i.current&&(r&&r(y),i.current=!1),N&&a.current&&(o&&o(y),a.current=!1),N&&b>S?(n&&!i.current&&n(y),k.scrollTop=M,w=!0,i.current=!0):!N&&-b>x&&(s&&!a.current&&s(y),k.scrollTop=0,w=!0,a.current=!0),w&&Ok(y)}},[n,r,s,o]),u=v.useCallback(function(y){d(y,y.deltaY)},[d]),f=v.useCallback(function(y){l.current=y.changedTouches[0].clientY},[]),g=v.useCallback(function(y){var b=l.current-y.changedTouches[0].clientY;d(y,b)},[d]),m=v.useCallback(function(y){if(y){var b=Rw?{passive:!1}:!1;y.addEventListener("wheel",u,b),y.addEventListener("touchstart",f,b),y.addEventListener("touchmove",g,b)}},[g,f,u]),p=v.useCallback(function(y){y&&(y.removeEventListener("wheel",u,!1),y.removeEventListener("touchstart",f,!1),y.removeEventListener("touchmove",g,!1))},[g,f,u]);return v.useEffect(function(){if(t){var y=c.current;return m(y),function(){p(y)}}},[t,m,p]),function(y){c.current=y}}var Bf=["boxSizing","height","overflow","paddingRight","position"],jf={boxSizing:"border-box",overflow:"hidden",position:"relative",height:"100%"};function Wf(e){e.cancelable&&e.preventDefault()}function Gf(e){e.stopPropagation()}function Yf(){var e=this.scrollTop,t=this.scrollHeight,n=e+this.offsetHeight;e===0?this.scrollTop=1:n===t&&(this.scrollTop=e-1)}function Xf(){return"ontouchstart"in window||navigator.maxTouchPoints}var Kf=!!(typeof window<"u"&&window.document&&window.document.createElement),Qs=0,is={capture:!1,passive:!1};function Dk(e){var t=e.isEnabled,n=e.accountForScrollbars,r=n===void 0?!0:n,s=v.useRef({}),o=v.useRef(null),i=v.useCallback(function(l){if(Kf){var c=document.body,d=c&&c.style;if(r&&Bf.forEach(function(m){var p=d&&d[m];s.current[m]=p}),r&&Qs<1){var u=parseInt(s.current.paddingRight,10)||0,f=document.body?document.body.clientWidth:0,g=window.innerWidth-f+u||0;Object.keys(jf).forEach(function(m){var p=jf[m];d&&(d[m]=p)}),d&&(d.paddingRight="".concat(g,"px"))}c&&Xf()&&(c.addEventListener("touchmove",Wf,is),l&&(l.addEventListener("touchstart",Yf,is),l.addEventListener("touchmove",Gf,is))),Qs+=1}},[r]),a=v.useCallback(function(l){if(Kf){var c=document.body,d=c&&c.style;Qs=Math.max(Qs-1,0),r&&Qs<1&&Bf.forEach(function(u){var f=s.current[u];d&&(d[u]=f)}),c&&Xf()&&(c.removeEventListener("touchmove",Wf,is),l&&(l.removeEventListener("touchstart",Yf,is),l.removeEventListener("touchmove",Gf,is)))}},[r]);return v.useEffect(function(){if(t){var l=o.current;return i(l),function(){a(l)}}},[t,i,a]),function(l){o.current=l}}var Ik=function(t){var n=t.target;return n.ownerDocument.activeElement&&n.ownerDocument.activeElement.blur()},Lk={name:"1kfdb0e",styles:"position:fixed;left:0;bottom:0;right:0;top:0"};function Pk(e){var t=e.children,n=e.lockEnabled,r=e.captureEnabled,s=r===void 0?!0:r,o=e.onBottomArrive,i=e.onBottomLeave,a=e.onTopArrive,l=e.onTopLeave,c=Ak({isEnabled:s,onBottomArrive:o,onBottomLeave:i,onTopArrive:a,onTopLeave:l}),d=Dk({isEnabled:n}),u=function(g){c(g),d(g)};return Se(v.Fragment,null,n&&Se("div",{onClick:Ik,css:Lk}),t(u))}var $k={name:"1a0ro4n-requiredInput",styles:"label:requiredInput;opacity:0;pointer-events:none;position:absolute;bottom:0;left:0;right:0;width:100%"},Fk=function(t){var n=t.name,r=t.onFocus;return Se("input",{required:!0,name:n,tabIndex:-1,"aria-hidden":"true",onFocus:r,css:$k,value:"",onChange:function(){}})},zk=Fk;function Bl(e){var t;return typeof window<"u"&&window.navigator!=null?e.test(((t=window.navigator.userAgentData)===null||t===void 0?void 0:t.platform)||window.navigator.platform):!1}function Vk(){return Bl(/^iPhone/i)}function qf(){return Bl(/^Mac/i)}function Hk(){return Bl(/^iPad/i)||qf()&&navigator.maxTouchPoints>1}function Uk(){return Vk()||Hk()}function Bk(){return qf()||Uk()}var jk=function(t){return t.label},Zf=function(t){return t.label},Qf=function(t){return t.value},Wk=function(t){return!!t.isDisabled},Gk={clearIndicator:O1,container:v1,control:F1,dropdownIndicator:N1,group:U1,groupHeading:j1,indicatorsContainer:C1,indicatorSeparator:D1,input:X1,loadingIndicator:P1,loadingMessage:g1,menu:a1,menuList:u1,menuPortal:b1,multiValue:J1,multiValueLabel:R1,multiValueRemove:ek,noOptionsMessage:h1,option:ik,placeholder:ck,singleValue:fk,valueContainer:w1},Yk={primary:"#2684FF",primary75:"#4C9AFF",primary50:"#B2D4FF",primary25:"#DEEBFF",danger:"#DE350B",dangerLight:"#FFBDAD",neutral0:"hsl(0, 0%, 100%)",neutral5:"hsl(0, 0%, 95%)",neutral10:"hsl(0, 0%, 90%)",neutral20:"hsl(0, 0%, 80%)",neutral30:"hsl(0, 0%, 70%)",neutral40:"hsl(0, 0%, 60%)",neutral50:"hsl(0, 0%, 50%)",neutral60:"hsl(0, 0%, 40%)",neutral70:"hsl(0, 0%, 30%)",neutral80:"hsl(0, 0%, 20%)",neutral90:"hsl(0, 0%, 10%)"},Xk=4,Jf=4,Kk=38,qk=Jf*2,Zk={baseUnit:Jf,controlHeight:Kk,menuGutter:qk},jl={borderRadius:Xk,colors:Yk,spacing:Zk},Qk={"aria-live":"polite",backspaceRemovesValue:!0,blurInputOnSelect:Ef(),captureMenuScroll:!Ef(),classNames:{},closeMenuOnSelect:!0,closeMenuOnScroll:!1,components:{},controlShouldRenderValue:!0,escapeClearsValue:!1,filterOption:Mk(),formatGroupLabel:jk,getOptionLabel:Zf,getOptionValue:Qf,isDisabled:!1,isLoading:!1,isMulti:!1,isRtl:!1,isSearchable:!0,isOptionDisabled:Wk,loadingMessage:function(){return"Loading..."},maxMenuHeight:300,minMenuHeight:140,menuIsOpen:!1,menuPlacement:"bottom",menuPosition:"absolute",menuShouldBlockScroll:!1,menuShouldScrollIntoView:!Qw(),noOptionsMessage:function(){return"No options"},openMenuOnFocus:!1,openMenuOnClick:!0,options:[],pageSize:5,placeholder:"Select...",screenReaderStatus:function(t){var n=t.count;return"".concat(n," result").concat(n!==1?"s":""," available")},styles:{},tabIndex:0,tabSelectsValue:!0,unstyled:!1};function Rf(e,t,n,r){var s=sh(e,t,n),o=oh(e,t,n),i=rh(e,t),a=Si(e,t);return{type:"option",data:t,isDisabled:s,isSelected:o,label:i,value:a,index:r}}function Ci(e,t){return e.options.map(function(n,r){if("options"in n){var s=n.options.map(function(i,a){return Rf(e,i,t,a)}).filter(function(i){return nh(e,i)});return s.length>0?{type:"group",data:n,options:s,index:r}:void 0}var o=Rf(e,n,t,r);return nh(e,o)?o:void 0}).filter(e1)}function eh(e){return e.reduce(function(t,n){return n.type==="group"?t.push.apply(t,ns(n.options.map(function(r){return r.data}))):t.push(n.data),t},[])}function th(e,t){return e.reduce(function(n,r){return r.type==="group"?n.push.apply(n,ns(r.options.map(function(s){return{data:s.data,id:"".concat(t,"-").concat(r.index,"-").concat(s.index)}}))):n.push({data:r.data,id:"".concat(t,"-").concat(r.index)}),n},[])}function Jk(e,t){return eh(Ci(e,t))}function nh(e,t){var n=e.inputValue,r=n===void 0?"":n,s=t.data,o=t.isSelected,i=t.label,a=t.value;return(!ah(e)||!o)&&ih(e,{label:i,value:a,data:s},r)}function Rk(e,t){var n=e.focusedValue,r=e.selectValue,s=r.indexOf(n);if(s>-1){var o=t.indexOf(n);if(o>-1)return n;if(s<t.length)return t[s]}return null}function eC(e,t){var n=e.focusedOption;return n&&t.indexOf(n)>-1?n:t[0]}var Wl=function(t,n){var r,s=(r=t.find(function(o){return o.data===n}))===null||r===void 0?void 0:r.id;return s||null},rh=function(t,n){return t.getOptionLabel(n)},Si=function(t,n){return t.getOptionValue(n)};function sh(e,t,n){return typeof e.isOptionDisabled=="function"?e.isOptionDisabled(t,n):!1}function oh(e,t,n){if(n.indexOf(t)>-1)return!0;if(typeof e.isOptionSelected=="function")return e.isOptionSelected(t,n);var r=Si(e,t);return n.some(function(s){return Si(e,s)===r})}function ih(e,t,n){return e.filterOption?e.filterOption(t,n):!0}var ah=function(t){var n=t.hideSelectedOptions,r=t.isMulti;return n===void 0?r:n},tC=1,Gl=(function(e){Nx(n,e);var t=Ax(n);function n(r){var s;if(Ex(this,n),s=t.call(this,r),s.state={ariaSelection:null,focusedOption:null,focusedOptionId:null,focusableOptionsWithIds:[],focusedValue:null,inputIsHidden:!1,isFocused:!1,selectValue:[],clearFocusValueOnUpdate:!1,prevWasFocused:!1,inputIsHiddenAfterUpdate:void 0,prevProps:void 0,instancePrefix:"",isAppleDevice:!1},s.blockOptionHover=!1,s.isComposing=!1,s.commonProps=void 0,s.initialTouchX=0,s.initialTouchY=0,s.openAfterFocus=!1,s.scrollToFocusedOptionOnUpdate=!1,s.userIsDragging=void 0,s.controlRef=null,s.getControlRef=function(l){s.controlRef=l},s.focusedOptionRef=null,s.getFocusedOptionRef=function(l){s.focusedOptionRef=l},s.menuListRef=null,s.getMenuListRef=function(l){s.menuListRef=l},s.inputRef=null,s.getInputRef=function(l){s.inputRef=l},s.focus=s.focusInput,s.blur=s.blurInput,s.onChange=function(l,c){var d=s.props,u=d.onChange,f=d.name;c.name=f,s.ariaOnChange(l,c),u(l,c)},s.setValue=function(l,c,d){var u=s.props,f=u.closeMenuOnSelect,g=u.isMulti,m=u.inputValue;s.onInputChange("",{action:"set-value",prevInputValue:m}),f&&(s.setState({inputIsHiddenAfterUpdate:!g}),s.onMenuClose()),s.setState({clearFocusValueOnUpdate:!0}),s.onChange(l,{action:c,option:d})},s.selectOption=function(l){var c=s.props,d=c.blurInputOnSelect,u=c.isMulti,f=c.name,g=s.state.selectValue,m=u&&s.isOptionSelected(l,g),p=s.isOptionDisabled(l,g);if(m){var y=s.getOptionValue(l);s.setValue(g.filter(function(b){return s.getOptionValue(b)!==y}),"deselect-option",l)}else if(!p)u?s.setValue([].concat(ns(g),[l]),"select-option",l):s.setValue(l,"select-option");else{s.ariaOnChange(l,{action:"select-option",option:l,name:f});return}d&&s.blurInput()},s.removeValue=function(l){var c=s.props.isMulti,d=s.state.selectValue,u=s.getOptionValue(l),f=d.filter(function(m){return s.getOptionValue(m)!==u}),g=Zs(c,f,f[0]||null);s.onChange(g,{action:"remove-value",removedValue:l}),s.focusInput()},s.clearValue=function(){var l=s.state.selectValue;s.onChange(Zs(s.props.isMulti,[],null),{action:"clear",removedValues:l})},s.popValue=function(){var l=s.props.isMulti,c=s.state.selectValue,d=c[c.length-1],u=c.slice(0,c.length-1),f=Zs(l,u,u[0]||null);d&&s.onChange(f,{action:"pop-value",removedValue:d})},s.getFocusedOptionId=function(l){return Wl(s.state.focusableOptionsWithIds,l)},s.getFocusableOptionsWithIds=function(){return th(Ci(s.props,s.state.selectValue),s.getElementId("option"))},s.getValue=function(){return s.state.selectValue},s.cx=function(){for(var l=arguments.length,c=new Array(l),d=0;d<l;d++)c[d]=arguments[d];return Yw.apply(void 0,[s.props.classNamePrefix].concat(c))},s.getOptionLabel=function(l){return rh(s.props,l)},s.getOptionValue=function(l){return Si(s.props,l)},s.getStyles=function(l,c){var d=s.props.unstyled,u=Gk[l](c,d);u.boxSizing="border-box";var f=s.props.styles[l];return f?f(u,c):u},s.getClassNames=function(l,c){var d,u;return(d=(u=s.props.classNames)[l])===null||d===void 0?void 0:d.call(u,c)},s.getElementId=function(l){return"".concat(s.state.instancePrefix,"-").concat(l)},s.getComponents=function(){return pk(s.props)},s.buildCategorizedOptions=function(){return Ci(s.props,s.state.selectValue)},s.getCategorizedOptions=function(){return s.props.menuIsOpen?s.buildCategorizedOptions():[]},s.buildFocusableOptions=function(){return eh(s.buildCategorizedOptions())},s.getFocusableOptions=function(){return s.props.menuIsOpen?s.buildFocusableOptions():[]},s.ariaOnChange=function(l,c){s.setState({ariaSelection:Ee({value:l},c)})},s.onMenuMouseDown=function(l){l.button===0&&(l.stopPropagation(),l.preventDefault(),s.focusInput())},s.onMenuMouseMove=function(l){s.blockOptionHover=!1},s.onControlMouseDown=function(l){if(!l.defaultPrevented){var c=s.props.openMenuOnClick;s.state.isFocused?s.props.menuIsOpen?l.target.tagName!=="INPUT"&&l.target.tagName!=="TEXTAREA"&&s.onMenuClose():c&&s.openMenu("first"):(c&&(s.openAfterFocus=!0),s.focusInput()),l.target.tagName!=="INPUT"&&l.target.tagName!=="TEXTAREA"&&l.preventDefault()}},s.onDropdownIndicatorMouseDown=function(l){if(!(l&&l.type==="mousedown"&&l.button!==0)&&!s.props.isDisabled){var c=s.props,d=c.isMulti,u=c.menuIsOpen;s.focusInput(),u?(s.setState({inputIsHiddenAfterUpdate:!d}),s.onMenuClose()):s.openMenu("first"),l.preventDefault()}},s.onClearIndicatorMouseDown=function(l){l&&l.type==="mousedown"&&l.button!==0||(s.clearValue(),l.preventDefault(),s.openAfterFocus=!1,l.type==="touchend"?s.focusInput():setTimeout(function(){return s.focusInput()}))},s.onScroll=function(l){typeof s.props.closeMenuOnScroll=="boolean"?l.target instanceof HTMLElement&&yi(l.target)&&s.props.onMenuClose():typeof s.props.closeMenuOnScroll=="function"&&s.props.closeMenuOnScroll(l)&&s.props.onMenuClose()},s.onCompositionStart=function(){s.isComposing=!0},s.onCompositionEnd=function(){s.isComposing=!1},s.onTouchStart=function(l){var c=l.touches,d=c&&c.item(0);d&&(s.initialTouchX=d.clientX,s.initialTouchY=d.clientY,s.userIsDragging=!1)},s.onTouchMove=function(l){var c=l.touches,d=c&&c.item(0);if(d){var u=Math.abs(d.clientX-s.initialTouchX),f=Math.abs(d.clientY-s.initialTouchY),g=5;s.userIsDragging=u>g||f>g}},s.onTouchEnd=function(l){s.userIsDragging||(s.controlRef&&!s.controlRef.contains(l.target)&&s.menuListRef&&!s.menuListRef.contains(l.target)&&s.blurInput(),s.initialTouchX=0,s.initialTouchY=0)},s.onControlTouchEnd=function(l){s.userIsDragging||s.onControlMouseDown(l)},s.onClearIndicatorTouchEnd=function(l){s.userIsDragging||s.onClearIndicatorMouseDown(l)},s.onDropdownIndicatorTouchEnd=function(l){s.userIsDragging||s.onDropdownIndicatorMouseDown(l)},s.handleInputChange=function(l){var c=s.props.inputValue,d=l.currentTarget.value;s.setState({inputIsHiddenAfterUpdate:!1}),s.onInputChange(d,{action:"input-change",prevInputValue:c}),s.props.menuIsOpen||s.onMenuOpen()},s.onInputFocus=function(l){s.props.onFocus&&s.props.onFocus(l),s.setState({inputIsHiddenAfterUpdate:!1,isFocused:!0}),(s.openAfterFocus||s.props.openMenuOnFocus)&&s.openMenu("first"),s.openAfterFocus=!1},s.onInputBlur=function(l){var c=s.props.inputValue;if(s.menuListRef&&s.menuListRef.contains(document.activeElement)){s.inputRef.focus();return}s.props.onBlur&&s.props.onBlur(l),s.onInputChange("",{action:"input-blur",prevInputValue:c}),s.onMenuClose(),s.setState({focusedValue:null,isFocused:!1})},s.onOptionHover=function(l){if(!(s.blockOptionHover||s.state.focusedOption===l)){var c=s.getFocusableOptions(),d=c.indexOf(l);s.setState({focusedOption:l,focusedOptionId:d>-1?s.getFocusedOptionId(l):null})}},s.shouldHideSelectedOptions=function(){return ah(s.props)},s.onValueInputFocus=function(l){l.preventDefault(),l.stopPropagation(),s.focus()},s.onKeyDown=function(l){var c=s.props,d=c.isMulti,u=c.backspaceRemovesValue,f=c.escapeClearsValue,g=c.inputValue,m=c.isClearable,p=c.isDisabled,y=c.menuIsOpen,b=c.onKeyDown,_=c.tabSelectsValue,x=c.openMenuOnFocus,M=s.state,C=M.focusedOption,k=M.focusedValue,N=M.selectValue;if(!p&&!(typeof b=="function"&&(b(l),l.defaultPrevented))){switch(s.blockOptionHover=!0,l.key){case"ArrowLeft":if(!d||g)return;s.focusValue("previous");break;case"ArrowRight":if(!d||g)return;s.focusValue("next");break;case"Delete":case"Backspace":if(g)return;if(k)s.removeValue(k);else{if(!u)return;d?s.popValue():m&&s.clearValue()}break;case"Tab":if(s.isComposing||l.shiftKey||!y||!_||!C||x&&s.isOptionSelected(C,N))return;s.selectOption(C);break;case"Enter":if(l.keyCode===229)break;if(y){if(!C||s.isComposing)return;s.selectOption(C);break}return;case"Escape":y?(s.setState({inputIsHiddenAfterUpdate:!1}),s.onInputChange("",{action:"menu-close",prevInputValue:g}),s.onMenuClose()):m&&f&&s.clearValue();break;case" ":if(g)return;if(!y){s.openMenu("first");break}if(!C)return;s.selectOption(C);break;case"ArrowUp":y?s.focusOption("up"):s.openMenu("last");break;case"ArrowDown":y?s.focusOption("down"):s.openMenu("first");break;case"PageUp":if(!y)return;s.focusOption("pageup");break;case"PageDown":if(!y)return;s.focusOption("pagedown");break;case"Home":if(!y)return;s.focusOption("first");break;case"End":if(!y)return;s.focusOption("last");break;default:return}l.preventDefault()}},s.state.instancePrefix="react-select-"+(s.props.instanceId||++tC),s.state.selectValue=bi(r.value),r.menuIsOpen&&s.state.selectValue.length){var o=s.getFocusableOptionsWithIds(),i=s.buildFocusableOptions(),a=i.indexOf(s.state.selectValue[0]);s.state.focusableOptionsWithIds=o,s.state.focusedOption=i[a],s.state.focusedOptionId=Wl(o,i[a])}return s}return Mx(n,[{key:"componentDidMount",value:function(){this.startListeningComposition(),this.startListeningToTouch(),this.props.closeMenuOnScroll&&document&&document.addEventListener&&document.addEventListener("scroll",this.onScroll,!0),this.props.autoFocus&&this.focusInput(),this.props.menuIsOpen&&this.state.focusedOption&&this.menuListRef&&this.focusedOptionRef&&_f(this.menuListRef,this.focusedOptionRef),Bk()&&this.setState({isAppleDevice:!0})}},{key:"componentDidUpdate",value:function(s){var o=this.props,i=o.isDisabled,a=o.menuIsOpen,l=this.state.isFocused;(l&&!i&&s.isDisabled||l&&a&&!s.menuIsOpen)&&this.focusInput(),l&&i&&!s.isDisabled?this.setState({isFocused:!1},this.onMenuClose):!l&&!i&&s.isDisabled&&this.inputRef===document.activeElement&&this.setState({isFocused:!0}),this.menuListRef&&this.focusedOptionRef&&this.scrollToFocusedOptionOnUpdate&&(_f(this.menuListRef,this.focusedOptionRef),this.scrollToFocusedOptionOnUpdate=!1)}},{key:"componentWillUnmount",value:function(){this.stopListeningComposition(),this.stopListeningToTouch(),document.removeEventListener("scroll",this.onScroll,!0)}},{key:"onMenuOpen",value:function(){this.props.onMenuOpen()}},{key:"onMenuClose",value:function(){this.onInputChange("",{action:"menu-close",prevInputValue:this.props.inputValue}),this.props.onMenuClose()}},{key:"onInputChange",value:function(s,o){this.props.onInputChange(s,o)}},{key:"focusInput",value:function(){this.inputRef&&this.inputRef.focus()}},{key:"blurInput",value:function(){this.inputRef&&this.inputRef.blur()}},{key:"openMenu",value:function(s){var o=this,i=this.state,a=i.selectValue,l=i.isFocused,c=this.buildFocusableOptions(),d=s==="first"?0:c.length-1;if(!this.props.isMulti){var u=c.indexOf(a[0]);u>-1&&(d=u)}this.scrollToFocusedOptionOnUpdate=!(l&&this.menuListRef),this.setState({inputIsHiddenAfterUpdate:!1,focusedValue:null,focusedOption:c[d],focusedOptionId:this.getFocusedOptionId(c[d])},function(){return o.onMenuOpen()})}},{key:"focusValue",value:function(s){var o=this.state,i=o.selectValue,a=o.focusedValue;if(this.props.isMulti){this.setState({focusedOption:null});var l=i.indexOf(a);a||(l=-1);var c=i.length-1,d=-1;if(i.length){switch(s){case"previous":l===0?d=0:l===-1?d=c:d=l-1;break;case"next":l>-1&&l<c&&(d=l+1);break}this.setState({inputIsHidden:d!==-1,focusedValue:i[d]})}}}},{key:"focusOption",value:function(){var s=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"first",o=this.props.pageSize,i=this.state.focusedOption,a=this.getFocusableOptions();if(a.length){var l=0,c=a.indexOf(i);i||(c=-1),s==="up"?l=c>0?c-1:a.length-1:s==="down"?l=(c+1)%a.length:s==="pageup"?(l=c-o,l<0&&(l=0)):s==="pagedown"?(l=c+o,l>a.length-1&&(l=a.length-1)):s==="last"&&(l=a.length-1),this.scrollToFocusedOptionOnUpdate=!0,this.setState({focusedOption:a[l],focusedValue:null,focusedOptionId:this.getFocusedOptionId(a[l])})}}},{key:"getTheme",value:(function(){return this.props.theme?typeof this.props.theme=="function"?this.props.theme(jl):Ee(Ee({},jl),this.props.theme):jl})},{key:"getCommonProps",value:function(){var s=this.clearValue,o=this.cx,i=this.getStyles,a=this.getClassNames,l=this.getValue,c=this.selectOption,d=this.setValue,u=this.props,f=u.isMulti,g=u.isRtl,m=u.options,p=this.hasValue();return{clearValue:s,cx:o,getStyles:i,getClassNames:a,getValue:l,hasValue:p,isMulti:f,isRtl:g,options:m,selectOption:c,selectProps:u,setValue:d,theme:this.getTheme()}}},{key:"hasValue",value:function(){var s=this.state.selectValue;return s.length>0}},{key:"hasOptions",value:function(){return!!this.getFocusableOptions().length}},{key:"isClearable",value:function(){var s=this.props,o=s.isClearable,i=s.isMulti;return o===void 0?i:o}},{key:"isOptionDisabled",value:function(s,o){return sh(this.props,s,o)}},{key:"isOptionSelected",value:function(s,o){return oh(this.props,s,o)}},{key:"filterOption",value:function(s,o){return ih(this.props,s,o)}},{key:"formatOptionLabel",value:function(s,o){if(typeof this.props.formatOptionLabel=="function"){var i=this.props.inputValue,a=this.state.selectValue;return this.props.formatOptionLabel(s,{context:o,inputValue:i,selectValue:a})}else return this.getOptionLabel(s)}},{key:"formatGroupLabel",value:function(s){return this.props.formatGroupLabel(s)}},{key:"startListeningComposition",value:(function(){document&&document.addEventListener&&(document.addEventListener("compositionstart",this.onCompositionStart,!1),document.addEventListener("compositionend",this.onCompositionEnd,!1))})},{key:"stopListeningComposition",value:function(){document&&document.removeEventListener&&(document.removeEventListener("compositionstart",this.onCompositionStart),document.removeEventListener("compositionend",this.onCompositionEnd))}},{key:"startListeningToTouch",value:(function(){document&&document.addEventListener&&(document.addEventListener("touchstart",this.onTouchStart,!1),document.addEventListener("touchmove",this.onTouchMove,!1),document.addEventListener("touchend",this.onTouchEnd,!1))})},{key:"stopListeningToTouch",value:function(){document&&document.removeEventListener&&(document.removeEventListener("touchstart",this.onTouchStart),document.removeEventListener("touchmove",this.onTouchMove),document.removeEventListener("touchend",this.onTouchEnd))}},{key:"renderInput",value:(function(){var s=this.props,o=s.isDisabled,i=s.isSearchable,a=s.inputId,l=s.inputValue,c=s.tabIndex,d=s.form,u=s.menuIsOpen,f=s.required,g=this.getComponents(),m=g.Input,p=this.state,y=p.inputIsHidden,b=p.ariaSelection,_=this.commonProps,x=a||this.getElementId("input"),M=Ee(Ee(Ee({"aria-autocomplete":"list","aria-expanded":u,"aria-haspopup":!0,"aria-errormessage":this.props["aria-errormessage"],"aria-invalid":this.props["aria-invalid"],"aria-label":this.props["aria-label"],"aria-labelledby":this.props["aria-labelledby"],"aria-required":f,role:"combobox","aria-activedescendant":this.state.isAppleDevice?void 0:this.state.focusedOptionId||""},u&&{"aria-controls":this.getElementId("listbox")}),!i&&{"aria-readonly":!0}),this.hasValue()?b?.action==="initial-input-focus"&&{"aria-describedby":this.getElementId("live-region")}:{"aria-describedby":this.getElementId("placeholder")});return i?F.createElement(m,Oe({},_,{autoCapitalize:"none",autoComplete:"off",autoCorrect:"off",id:x,innerRef:this.getInputRef,isDisabled:o,isHidden:y,onBlur:this.onInputBlur,onChange:this.handleInputChange,onFocus:this.onInputFocus,spellCheck:"false",tabIndex:c,form:d,type:"text",value:l},M)):F.createElement(Tk,Oe({id:x,innerRef:this.getInputRef,onBlur:this.onInputBlur,onChange:mi,onFocus:this.onInputFocus,disabled:o,tabIndex:c,inputMode:"none",form:d,value:""},M))})},{key:"renderPlaceholderOrValue",value:function(){var s=this,o=this.getComponents(),i=o.MultiValue,a=o.MultiValueContainer,l=o.MultiValueLabel,c=o.MultiValueRemove,d=o.SingleValue,u=o.Placeholder,f=this.commonProps,g=this.props,m=g.controlShouldRenderValue,p=g.isDisabled,y=g.isMulti,b=g.inputValue,_=g.placeholder,x=this.state,M=x.selectValue,C=x.focusedValue,k=x.isFocused;if(!this.hasValue()||!m)return b?null:F.createElement(u,Oe({},f,{key:"placeholder",isDisabled:p,isFocused:k,innerProps:{id:this.getElementId("placeholder")}}),_);if(y)return M.map(function(S,w){var E=S===C,D="".concat(s.getOptionLabel(S),"-").concat(s.getOptionValue(S));return F.createElement(i,Oe({},f,{components:{Container:a,Label:l,Remove:c},isFocused:E,isDisabled:p,key:D,index:w,removeProps:{onClick:function(){return s.removeValue(S)},onTouchEnd:function(){return s.removeValue(S)},onMouseDown:function(L){L.preventDefault()}},data:S}),s.formatOptionLabel(S,"value"))});if(b)return null;var N=M[0];return F.createElement(d,Oe({},f,{data:N,isDisabled:p}),this.formatOptionLabel(N,"value"))}},{key:"renderClearIndicator",value:function(){var s=this.getComponents(),o=s.ClearIndicator,i=this.commonProps,a=this.props,l=a.isDisabled,c=a.isLoading,d=this.state.isFocused;if(!this.isClearable()||!o||l||!this.hasValue()||c)return null;var u={onMouseDown:this.onClearIndicatorMouseDown,onTouchEnd:this.onClearIndicatorTouchEnd,"aria-hidden":"true"};return F.createElement(o,Oe({},i,{innerProps:u,isFocused:d}))}},{key:"renderLoadingIndicator",value:function(){var s=this.getComponents(),o=s.LoadingIndicator,i=this.commonProps,a=this.props,l=a.isDisabled,c=a.isLoading,d=this.state.isFocused;if(!o||!c)return null;var u={"aria-hidden":"true"};return F.createElement(o,Oe({},i,{innerProps:u,isDisabled:l,isFocused:d}))}},{key:"renderIndicatorSeparator",value:function(){var s=this.getComponents(),o=s.DropdownIndicator,i=s.IndicatorSeparator;if(!o||!i)return null;var a=this.commonProps,l=this.props.isDisabled,c=this.state.isFocused;return F.createElement(i,Oe({},a,{isDisabled:l,isFocused:c}))}},{key:"renderDropdownIndicator",value:function(){var s=this.getComponents(),o=s.DropdownIndicator;if(!o)return null;var i=this.commonProps,a=this.props.isDisabled,l=this.state.isFocused,c={onMouseDown:this.onDropdownIndicatorMouseDown,onTouchEnd:this.onDropdownIndicatorTouchEnd,"aria-hidden":"true"};return F.createElement(o,Oe({},i,{innerProps:c,isDisabled:a,isFocused:l}))}},{key:"renderMenu",value:function(){var s=this,o=this.getComponents(),i=o.Group,a=o.GroupHeading,l=o.Menu,c=o.MenuList,d=o.MenuPortal,u=o.LoadingMessage,f=o.NoOptionsMessage,g=o.Option,m=this.commonProps,p=this.state.focusedOption,y=this.props,b=y.captureMenuScroll,_=y.inputValue,x=y.isLoading,M=y.loadingMessage,C=y.minMenuHeight,k=y.maxMenuHeight,N=y.menuIsOpen,S=y.menuPlacement,w=y.menuPosition,E=y.menuPortalTarget,D=y.menuShouldBlockScroll,P=y.menuShouldScrollIntoView,L=y.noOptionsMessage,$=y.onMenuScrollToTop,A=y.onMenuScrollToBottom;if(!N)return null;var O=function(W,Y){var V=W.type,X=W.data,K=W.isDisabled,q=W.isSelected,ne=W.label,ee=W.value,ue=p===X,J=K?void 0:function(){return s.onOptionHover(X)},ce=K?void 0:function(){return s.selectOption(X)},me="".concat(s.getElementId("option"),"-").concat(Y),le={id:me,onClick:ce,onMouseMove:J,onMouseOver:J,tabIndex:-1,role:"option","aria-selected":s.state.isAppleDevice?void 0:q};return F.createElement(g,Oe({},m,{innerProps:le,data:X,isDisabled:K,isSelected:q,key:me,label:ne,type:V,value:ee,isFocused:ue,innerRef:ue?s.getFocusedOptionRef:void 0}),s.formatOptionLabel(W.data,"menu"))},T;if(this.hasOptions())T=this.getCategorizedOptions().map(function(U){if(U.type==="group"){var W=U.data,Y=U.options,V=U.index,X="".concat(s.getElementId("group"),"-").concat(V),K="".concat(X,"-heading");return F.createElement(i,Oe({},m,{key:X,data:W,options:Y,Heading:a,headingProps:{id:K,data:U.data},label:s.formatGroupLabel(U.data)}),U.options.map(function(q){return O(q,"".concat(V,"-").concat(q.index))}))}else if(U.type==="option")return O(U,"".concat(U.index))});else if(x){var z=M({inputValue:_});if(z===null)return null;T=F.createElement(u,m,z)}else{var H=L({inputValue:_});if(H===null)return null;T=F.createElement(f,m,H)}var j={minMenuHeight:C,maxMenuHeight:k,menuPlacement:S,menuPosition:w,menuShouldScrollIntoView:P},G=F.createElement(l1,Oe({},m,j),function(U){var W=U.ref,Y=U.placerProps,V=Y.placement,X=Y.maxHeight;return F.createElement(l,Oe({},m,j,{innerRef:W,innerProps:{onMouseDown:s.onMenuMouseDown,onMouseMove:s.onMenuMouseMove},isLoading:x,placement:V}),F.createElement(Pk,{captureEnabled:b,onTopArrive:$,onBottomArrive:A,lockEnabled:D},function(K){return F.createElement(c,Oe({},m,{innerRef:function(ne){s.getMenuListRef(ne),K(ne)},innerProps:{role:"listbox","aria-multiselectable":m.isMulti,id:s.getElementId("listbox")},isLoading:x,maxHeight:X,focusedOption:p}),T)}))});return E||w==="fixed"?F.createElement(d,Oe({},m,{appendTo:E,controlElement:this.controlRef,menuPlacement:S,menuPosition:w}),G):G}},{key:"renderFormField",value:function(){var s=this,o=this.props,i=o.delimiter,a=o.isDisabled,l=o.isMulti,c=o.name,d=o.required,u=this.state.selectValue;if(d&&!this.hasValue()&&!a)return F.createElement(zk,{name:c,onFocus:this.onValueInputFocus});if(!(!c||a))if(l)if(i){var f=u.map(function(p){return s.getOptionValue(p)}).join(i);return F.createElement("input",{name:c,type:"hidden",value:f})}else{var g=u.length>0?u.map(function(p,y){return F.createElement("input",{key:"i-".concat(y),name:c,type:"hidden",value:s.getOptionValue(p)})}):F.createElement("input",{name:c,type:"hidden",value:""});return F.createElement("div",null,g)}else{var m=u[0]?this.getOptionValue(u[0]):"";return F.createElement("input",{name:c,type:"hidden",value:m})}}},{key:"renderLiveRegion",value:function(){var s=this.commonProps,o=this.state,i=o.ariaSelection,a=o.focusedOption,l=o.focusedValue,c=o.isFocused,d=o.selectValue,u=this.getFocusableOptions();return F.createElement(Ck,Oe({},s,{id:this.getElementId("live-region"),ariaSelection:i,focusedOption:a,focusedValue:l,isFocused:c,selectValue:d,focusableOptions:u,isAppleDevice:this.state.isAppleDevice}))}},{key:"render",value:function(){var s=this.getComponents(),o=s.Control,i=s.IndicatorsContainer,a=s.SelectContainer,l=s.ValueContainer,c=this.props,d=c.className,u=c.id,f=c.isDisabled,g=c.menuIsOpen,m=this.state.isFocused,p=this.commonProps=this.getCommonProps();return F.createElement(a,Oe({},p,{className:d,innerProps:{id:u,onKeyDown:this.onKeyDown},isDisabled:f,isFocused:m}),this.renderLiveRegion(),F.createElement(o,Oe({},p,{innerRef:this.getControlRef,innerProps:{onMouseDown:this.onControlMouseDown,onTouchEnd:this.onControlTouchEnd},isDisabled:f,isFocused:m,menuIsOpen:g}),F.createElement(l,Oe({},p,{isDisabled:f}),this.renderPlaceholderOrValue(),this.renderInput()),F.createElement(i,Oe({},p,{isDisabled:f}),this.renderClearIndicator(),this.renderLoadingIndicator(),this.renderIndicatorSeparator(),this.renderDropdownIndicator())),this.renderMenu(),this.renderFormField())}}],[{key:"getDerivedStateFromProps",value:function(s,o){var i=o.prevProps,a=o.clearFocusValueOnUpdate,l=o.inputIsHiddenAfterUpdate,c=o.ariaSelection,d=o.isFocused,u=o.prevWasFocused,f=o.instancePrefix,g=s.options,m=s.value,p=s.menuIsOpen,y=s.inputValue,b=s.isMulti,_=bi(m),x={};if(i&&(m!==i.value||g!==i.options||p!==i.menuIsOpen||y!==i.inputValue)){var M=p?Jk(s,_):[],C=p?th(Ci(s,_),"".concat(f,"-option")):[],k=a?Rk(o,_):null,N=eC(o,M),S=Wl(C,N);x={selectValue:_,focusedOption:N,focusedOptionId:S,focusableOptionsWithIds:C,focusedValue:k,clearFocusValueOnUpdate:!1}}var w=l!=null&&s!==i?{inputIsHidden:l,inputIsHiddenAfterUpdate:void 0}:{},E=c,D=d&&u;return d&&!D&&(E={value:Zs(b,_,_[0]||null),options:_,action:"initial-input-focus"},D=!u),c?.action==="initial-input-focus"&&(E=null),Ee(Ee(Ee({},x),w),{},{prevProps:s,ariaSelection:E,prevWasFocused:D})}}]),n})(v.Component);Gl.defaultProps=Qk;var nC=v.forwardRef(function(e,t){var n=Wu(e);return F.createElement(Gl,Oe({ref:t},n))}),lh=nC,rC=["allowCreateWhileLoading","createOptionPosition","formatCreateLabel","isValidNewOption","getNewOptionData","onCreateOption","options","onChange"],ch=function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"",n=arguments.length>1?arguments[1]:void 0,r=arguments.length>2?arguments[2]:void 0,s=String(t).toLowerCase(),o=String(r.getOptionValue(n)).toLowerCase(),i=String(r.getOptionLabel(n)).toLowerCase();return o===s||i===s},Yl={formatCreateLabel:function(t){return'Create "'.concat(t,'"')},isValidNewOption:function(t,n,r,s){return!(!t||n.some(function(o){return ch(t,o,s)})||r.some(function(o){return ch(t,o,s)}))},getNewOptionData:function(t,n){return{label:n,value:t,__isNew__:!0}}};function sC(e){var t=e.allowCreateWhileLoading,n=t===void 0?!1:t,r=e.createOptionPosition,s=r===void 0?"last":r,o=e.formatCreateLabel,i=o===void 0?Yl.formatCreateLabel:o,a=e.isValidNewOption,l=a===void 0?Yl.isValidNewOption:a,c=e.getNewOptionData,d=c===void 0?Yl.getNewOptionData:c,u=e.onCreateOption,f=e.options,g=f===void 0?[]:f,m=e.onChange,p=Vn(e,rC),y=p.getOptionValue,b=y===void 0?Qf:y,_=p.getOptionLabel,x=_===void 0?Zf:_,M=p.inputValue,C=p.isLoading,k=p.isMulti,N=p.value,S=p.name,w=v.useMemo(function(){return l(M,bi(N),g,{getOptionValue:b,getOptionLabel:x})?d(M,i(M)):void 0},[i,d,x,b,M,l,g,N]),E=v.useMemo(function(){return(n||!C)&&w?s==="first"?[w].concat(ns(g)):[].concat(ns(g),[w]):g},[n,s,C,w,g]),D=v.useCallback(function(P,L){if(L.action!=="select-option")return m(P,L);var $=Array.isArray(P)?P:[P];if($[$.length-1]===w){if(u)u(M);else{var A=d(M,M),O={action:"create-option",name:S,option:A};m(Zs(k,[].concat(ns(bi(N)),[A]),A),O)}return}m(P,L)},[d,M,k,S,w,u,m,N]);return Ee(Ee({},p),{},{options:E,onChange:D})}var oC=v.forwardRef(function(e,t){var n=Wu(e),r=sC(n);return F.createElement(Gl,Oe({ref:t},r))}),dh=oC;const Ht={control:"input-tag",valueContainer:"gap-1 flex flex-wrap",multiValue:{blue:"bg-blue-100 dark:bg-blue-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",green:"bg-green-100 dark:bg-green-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",amber:"bg-amber-100 dark:bg-amber-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",red:"bg-red-100 dark:bg-red-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",purple:"bg-purple-100 dark:bg-purple-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",gray:"bg-gray-100 dark:bg-gray-700/50 rounded px-1.5 py-0.5 flex items-center gap-1"},multiValueLabel:{blue:"text-blue-700 dark:text-blue-300 text-xs",green:"text-green-700 dark:text-green-300 text-xs font-semibold",amber:"text-amber-700 dark:text-amber-300 text-xs font-semibold",red:"text-red-700 dark:text-red-300 text-xs font-semibold",purple:"text-purple-700 dark:text-purple-300 text-xs font-semibold",gray:"text-gray-700 dark:text-gray-300 text-xs"},multiValueRemove:{blue:"text-blue-700 dark:text-blue-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",green:"text-green-700 dark:text-green-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",amber:"text-amber-700 dark:text-amber-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",red:"text-red-700 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",purple:"text-purple-700 dark:text-purple-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",gray:"text-gray-700 dark:text-gray-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1"},input:"text-gray-900 dark:text-white text-sm",placeholder:"text-gray-400 dark:text-gray-500 text-sm",menu:"mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",menuList:"py-1",option:{base:"px-3 py-2 cursor-pointer text-gray-900 dark:text-white text-[13px]",selected:"bg-blue-100 dark:bg-blue-900/20",focused:"bg-gray-100 dark:bg-gray-700",default:"bg-white dark:bg-gray-800"},noOptionsMessage:"px-3 py-2 text-gray-500 dark:text-gray-400 text-[13px]",checkbox:{base:"w-4 h-4 rounded border flex items-center justify-center text-white text-[10px] shrink-0",selected:"bg-blue-500 border-blue-500",unselected:"border-gray-400 dark:border-gray-500"}},uh=e=>{const{data:t,isSelected:n}=e;return h.jsx(ki.Option,{...e,children:h.jsxs("div",{className:"flex items-center gap-2",children:[h.jsx("span",{className:`${Ht.checkbox.base} ${n?Ht.checkbox.selected:Ht.checkbox.unselected}`,children:n?"✓":""}),h.jsxs("div",{className:"flex-1 min-w-0",children:[h.jsxs("div",{className:"flex items-center gap-1",children:[h.jsx("span",{className:"text-[13px]",children:t.label}),t.count!==void 0&&t.count>0&&h.jsxs("span",{className:"text-gray-500 dark:text-gray-400 text-[10px]",children:["(",t.count,")"]})]}),t.description&&h.jsx("p",{className:"text-xs text-gray-500 dark:text-gray-400 truncate",children:t.description})]})]})})},fh=e=>h.jsx(ki.GroupHeading,{...e,children:h.jsx("div",{className:"px-3 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700",children:e.data.label})}),iC=e=>{const{data:t}=e;return h.jsxs(ki.MultiValueLabel,{...e,children:[h.jsx("span",{children:t.label}),t.count!==void 0&&t.count>0&&h.jsxs("span",{className:"text-xs opacity-60 ml-1",children:["(",t.count,")"]})]})},aC=({value:e,options:t=[],onChange:n,creatable:r=!1,placeholder:s="검색 또는 입력...",noOptionsMessage:o="검색 결과가 없습니다",formatCreateLabel:i,createLabelSuffix:a="추가",maxItems:l,disabled:c=!1,onBeforeRemove:d,onCreateOption:u,className:f="",tagVariants:g,defaultVariant:m="blue",name:p,isMulti:y=!0,isClearable:b=!0,isSearchable:_=!0,delimiters:x=[","],splitOnPaste:M=!0,onInputChange:C,id:k,editorAttrs:N})=>{const S=c===!0||c==="true",w=v.useCallback(J=>J==null?[]:Array.isArray(J)?J:[J],[]),[E,D]=v.useState(()=>w(e)),[P,L]=v.useState(""),$=v.useRef(void 0);v.useEffect(()=>{const J=(me,le)=>!me&&!le?!0:!me||!le||me.length!==le.length?!1:me.every((ve,ge)=>ve===le[ge]),ce=w(e);J(ce,$.current)||(D(ce),$.current=ce)},[e,w]);const A=v.useMemo(()=>i?typeof i=="string"?J=>`${i} "${J}"`:i:J=>h.jsxs("span",{className:"text-[13px]",children:[h.jsx("strong",{children:J})," ",a]}),[i,a]),O=v.useMemo(()=>!E||!Array.isArray(E)?[]:E.map(J=>t.find(me=>me.value===J)||{value:J,label:String(J)}).filter(J=>J!==void 0),[E,t]),T=v.useMemo(()=>{const J=new Set(t.map(ke=>ke.value)),ce=E?E.filter(ke=>!J.has(ke)).map(ke=>({value:ke,label:String(ke)})):[],me=[...t,...ce];if(!me.some(ke=>ke.group))return me;const ve=new Map,ge=[];me.forEach(ke=>{if(ke.group){const $e=ve.get(ke.group)||[];$e.push(ke),ve.set(ke.group,$e)}else ge.push(ke)});const pe=[];return ve.forEach((ke,$e)=>{pe.push({label:$e,options:ke})}),ge.length>0&&pe.push({label:"",options:ge}),pe},[t,E]),z=v.useCallback(J=>({target:{value:J},currentTarget:{value:J},preventDefault:()=>{},stopPropagation:()=>{}}),[]),H=v.useCallback(J=>{if(!x?.length)return[J.trim()].filter(Boolean);const ce=x.map(le=>le.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),me=new RegExp(`[${ce.join("")}]`);return J.split(me).map(le=>le.trim()).filter(Boolean)},[x]),j=v.useCallback((J,ce)=>{if(ce.action!=="input-change"){L(J);return}if(C&&C(z(J)),!x?.some(ge=>J.includes(ge))||!r||!y){L(J);return}const ve=H(J).filter(ge=>ge&&!E.includes(ge));if(ve.length>0){const ge=l?l-E.length:1/0,pe=ve.slice(0,ge);if(pe.length>0){const ke=[...E,...pe];D(ke),n(z(ke))}}L("")},[x,r,y,H,E,l,n,z,C]),G=v.useCallback(J=>{if(!M||!r||!y)return;const ce=J.clipboardData.getData("text"),me=H(ce);if(me.length<=1)return;J.preventDefault();const le=me.filter(ve=>ve&&!E.includes(ve));if(le.length>0){const ve=l?l-E.length:1/0,ge=le.slice(0,ve);if(ge.length>0){const pe=[...E,...ge];D(pe),n(z(pe))}}},[M,r,y,H,E,l,n,z]),U=v.useCallback((J,ce)=>{if((ce.action==="remove-value"||ce.action==="pop-value")&&ce.removedValue&&d){const le=d(ce.removedValue);if(le!==!0){const ve=typeof le=="string"?le:`"${ce.removedValue.label}"은(는) 제거할 수 없습니다.`;alert(ve);return}}if(l&&J.length>l)return;const me=J.map(le=>le.value);D(me),n(z(me))},[n,d,l,z]),W=v.useCallback((J,ce)=>{if(ce.action==="clear"&&E.length>0&&d){const ve=t.find(pe=>pe.value===E[0])||{value:E[0],label:String(E[0])},ge=d(ve);if(ge!==!0){const pe=typeof ge=="string"?ge:`"${ve.label}"은(는) 제거할 수 없습니다.`;alert(pe);return}}const me=J?J.value:null,le=J?[J.value]:[];D(le),n(z(me))},[n,d,E,t,z]),Y=v.useCallback(J=>{u&&u(J);const ce=[...E||[],J];D(ce),n(z(ce))},[n,u,E,z]),V=v.useCallback(J=>!J||!J.trim()?!1:!E?.includes(J.trim()),[E]),X=v.useCallback(J=>{const ce=P.trim();if(J.key==="Tab"&&r&&y&&ce){if(E.includes(ce))J.preventDefault(),L("");else if(!l||E.length<l){J.preventDefault();const me=[...E,ce];D(me),n(z(me)),L("")}return}J.key==="Enter"&&(!ce||E&&E.includes(ce))&&(J.preventDefault(),J.stopPropagation())},[E,P,r,y,l,n,z]),K=v.useCallback(J=>g&&g[J]?g[J]:m,[g,m]),q={control:()=>`${Ht.control} ${f} ${S?"opacity-60 cursor-not-allowed":""}`,valueContainer:()=>Ht.valueContainer,multiValue:J=>{const ce=K(J.data.value);return Ht.multiValue[ce]},multiValueLabel:J=>{const ce=K(J.data.value);return Ht.multiValueLabel[ce]},multiValueRemove:J=>{const ce=K(J.data.value);return`${Ht.multiValueRemove[ce]} ${S?"pointer-events-none":""}`},singleValue:()=>"text-gray-900 dark:text-white text-sm",input:()=>Ht.input,placeholder:()=>Ht.placeholder,menu:()=>Ht.menu,menuList:()=>Ht.menuList,menuPortal:()=>"z-[9999]",option:J=>`${Ht.option.base} ${J.isSelected?Ht.option.selected:J.isFocused?Ht.option.focused:Ht.option.default}`,noOptionsMessage:()=>Ht.noOptionsMessage,clearIndicator:()=>"text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-pointer px-1",dropdownIndicator:()=>"text-gray-400 dark:text-gray-500 px-1"},ne={options:T,onKeyDown:X,placeholder:s,noOptionsMessage:()=>o,isDisabled:S,isSearchable:_,menuIsOpen:void 0,openMenuOnClick:!0,openMenuOnFocus:!0,menuPortalTarget:typeof document<"u"?document.body:null,menuPosition:"fixed",classNamePrefix:"tag-input",unstyled:!0,classNames:q,styles:{menuPortal:J=>({...J,zIndex:9999})},...r&&y||C?{inputValue:P,onInputChange:j}:{}},ee=y?E.join(","):E[0]??"";if(!y){const J={...ne,isMulti:!1,value:O[0]||null,onChange:W,isClearable:b,closeMenuOnSelect:!0,components:{Option:uh,GroupHeading:fh,IndicatorSeparator:()=>null,...b?{}:{ClearIndicator:()=>null}}};return r?h.jsxs(I,{id:k,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:ee}),h.jsx(dh,{...J,onCreateOption:Y,formatCreateLabel:A,isValidNewOption:V,createOptionPosition:"first"})]}):h.jsxs(I,{id:k,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:ee}),h.jsx(lh,{...J})]})}const ue={...ne,isMulti:!0,value:O,onChange:U,closeMenuOnSelect:!1,hideSelectedOptions:!1,backspaceRemovesValue:!1,isClearable:!1,components:{Option:uh,GroupHeading:fh,MultiValueLabel:iC,ClearIndicator:()=>null,DropdownIndicator:()=>null,IndicatorSeparator:()=>null}};return r?h.jsxs(I,{id:k,...N,onPaste:G,children:[p&&h.jsx("input",{type:"hidden",name:p,value:ee}),h.jsx(dh,{...ue,onCreateOption:Y,formatCreateLabel:A,isValidNewOption:V,createOptionPosition:"first"})]}):h.jsxs(I,{id:k,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:ee}),h.jsx(lh,{...ue})]})},as=(e,t)=>window.G7Core?.t?.(e,t)??e,lC=()=>window.G7Core?.locale?.supported?.()??["ko","en"],cC=()=>window.G7Core?.locale?.current?.()??"ko",Xl=e=>{const t=as(`common.language_${e}`);return t===`common.language_${e}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[e]||e.toUpperCase():t},dC=({value:e=[],onChange:t,placeholder:n="입력 후 Enter",disabled:r=!1,creatable:s=!0,delimiters:o=[","],className:i="",name:a,error:l,maxItems:c,modalId:d,statePath:u,__componentContext:f,id:g,editorAttrs:m})=>{const p=v.useMemo(()=>lC(),[]),y=v.useMemo(()=>cC(),[]),[b,_]=v.useState(()=>e||[]),[x,M]=v.useState(""),[C,k]=v.useState(!1),[N,S]=v.useState(null),[w,E]=v.useState({}),D=v.useRef(null),P=v.useRef(void 0);v.useEffect(()=>{const V=(K,q)=>!K&&!q?!0:!K||!q||K.length!==q.length?!1:JSON.stringify(K)===JSON.stringify(q),X=e||[];V(X,P.current)||(_(X),P.current=X)},[e]);const L=v.useCallback(V=>({target:{name:a,value:V},currentTarget:{name:a,value:V},preventDefault:()=>{},stopPropagation:()=>{}}),[a]),$=v.useCallback(V=>{const X=[];for(const K of p){const q=V[K];q?.trim()&&X.push(`${K.toUpperCase()}: ${q}`)}return X.length>0?X.join(" / "):""},[p]),A=v.useCallback(V=>{const X=b.filter((K,q)=>q!==V);_(X),t?.(L(X))},[b,t,L]),O=v.useCallback((V,X,K)=>{const q=window.G7Core;if(!q?.dispatch)return;const ne=p.map(ee=>({code:ee,label:Xl(ee)}));q.dispatch({handler:"setState",params:{target:"global",multilingualTagEdit:{fieldName:a,editingIndex:V,values:X,supportedLocales:ne,defaultLocale:y,statePath:u,currentTags:K}}}),q.dispatch({handler:"openModal",target:d},{componentContext:f})},[a,p,y,d,u,f]),T=v.useCallback(V=>{r||(d?O(V,{...b[V]},b):(S(V),E({...b[V]}),k(!0)))},[r,b,d,O]),z=v.useCallback((V="")=>{const X={};p.forEach(K=>{X[K]=K===y?V:""}),d?(O(null,X,b),M("")):(S(null),E(X),k(!0),M(""))},[p,y,d,O,b]),H=v.useCallback(()=>{if(!Object.values(w).some(K=>K.trim()!=="")){k(!1);return}let X;N!==null?X=b.map((K,q)=>q===N?w:K):X=[...b,w],_(X),t?.(L(X)),k(!1),E({}),S(null)},[w,N,b,t,L]),j=v.useCallback(()=>{k(!1),E({}),S(null)},[]),G=v.useCallback(V=>{const X=V.target.value;if(o.some(q=>X.includes(q))&&s){const q=o.map(ue=>ue.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),ne=new RegExp(`[${q.join("")}]`),ee=X.split(ne).map(ue=>ue.trim()).filter(Boolean);if(ee.length>0){z(ee[0]);return}}M(X)},[o,s,z]),U=v.useCallback(V=>{if(V.key==="Enter"&&x.trim()&&s){if(V.preventDefault(),c&&b.length>=c)return;z(x.trim())}},[x,s,c,b.length,z]),W=v.useCallback((V,X)=>{E(K=>({...K,[V]:X}))},[]),Y=!!l;return h.jsxs(I,{className:i,id:g,...m,children:[h.jsxs(I,{className:`flex flex-wrap gap-1.5 p-2 border rounded-lg bg-white dark:bg-gray-700 min-h-[42px] ${Y?"border-red-500 dark:border-red-400":"border-gray-300 dark:border-gray-600 focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500"} ${r?"opacity-60 bg-gray-100 dark:bg-gray-800 cursor-not-allowed":""}`,children:[b.map((V,X)=>h.jsxs(I,{className:"flex items-center gap-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded px-2 py-0.5 text-sm",children:[h.jsx(Q,{className:`cursor-pointer hover:underline ${r?"cursor-not-allowed":""}`,onClick:()=>T(X),children:$(V)}),!r&&h.jsx(ae,{type:"button",className:"p-0 h-4 w-4 min-w-0 border-0 bg-transparent text-blue-600 dark:text-blue-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-transparent",onClick:K=>{K.stopPropagation(),A(X)},children:h.jsx(oe,{name:"times",className:"text-xs"})})]},X)),s&&!r&&(!c||b.length<c)&&h.jsx("input",{ref:D,type:"text",value:x,onChange:G,onKeyDown:U,placeholder:b.length===0?n:"",disabled:r,className:"flex-1 min-w-[120px] bg-transparent border-none outline-none text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500"})]}),l&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1",children:l}),!d&&h.jsx(Qo,{isOpen:C,onClose:j,title:as(N!==null?"common.edit":"common.add"),width:"400px",children:h.jsxs(I,{className:"space-y-4 p-4",children:[p.map(V=>h.jsxs(I,{className:"space-y-1",children:[h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300 w-16",children:Xl(V)}),h.jsxs(Q,{className:"text-xs text-gray-400 dark:text-gray-500",children:["(",V.toUpperCase(),")"]})]}),h.jsx(St,{type:"text",value:w[V]??"",onChange:X=>W(V,X.target.value),placeholder:`${Xl(V)} 값 입력`,className:"w-full",autoFocus:V===y})]},V)),h.jsxs(I,{className:"flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700",children:[h.jsx(ae,{type:"button",variant:"secondary",onClick:j,children:as("common.cancel")}),h.jsx(ae,{type:"button",variant:"primary",onClick:H,children:as(N!==null?"common.save":"common.add")})]})]})}),a&&h.jsx("input",{type:"hidden",name:a,value:JSON.stringify(b)})]})},hh=(e,t)=>window.G7Core?.t?.(e,t)??e,gh=({type:e,identifier:t,name:n,installedModules:r=[],installedPlugins:s=[],className:o="",style:i,id:a,editorAttrs:l})=>{const c={module:de.Cubes,plugin:de.PuzzlePiece},d={module:hh("common.module"),plugin:hh("common.plugin")},u=v.useMemo(()=>{if(n)return n;if(t){const p=(e==="module"?r:s)?.find(y=>y.identifier===t);if(p)return p.name}},[n,t,e,r,s]),f=c[e],g=u?`${u} ${d[e]}`:d[e];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium text-gray-400 dark:text-gray-500 ${o}`,style:i,id:a,...l,children:[h.jsx(oe,{name:f,className:"w-3.5 h-3.5"}),h.jsx(Q,{children:g})]})},uC=({options:e=[],value:t,onChange:n,multiple:r=!1,searchPlaceholder:s="Search...",onSearch:o,noResultsText:i="No results found",initialText:a="Type to search",triggerLabel:l="Select",triggerIcon:c,disabled:d=!1,className:u="",id:f,editorAttrs:g})=>{const[m,p]=v.useState(!1),[y,b]=v.useState(""),_=v.useRef(null),x=v.useRef(null),M=v.useRef(`searchable-dropdown-input-${Math.random().toString(36).slice(2,9)}`).current,C=v.useMemo(()=>t==null?[]:Array.isArray(t)?t:[t],[t]),k=v.useMemo(()=>{if(!y.trim())return e;const A=y.toLowerCase();return e.filter(O=>O.label.toLowerCase().includes(A)||O.description?.toLowerCase().includes(A))},[e,y]),N=v.useMemo(()=>{if(C.length===0)return l;if(r){const O=C.length;return O===1?e.find(z=>String(z.value)===String(C[0]))?.label||l:`${O} selected`}return e.find(O=>String(O.value)===String(C[0]))?.label||l},[C,e,r,l]);v.useEffect(()=>{const A=O=>{_.current&&!_.current.contains(O.target)&&p(!1)};return m&&document.addEventListener("mousedown",A),()=>{document.removeEventListener("mousedown",A)}},[m]),v.useEffect(()=>{const A=O=>{O.key==="Escape"&&m&&(p(!1),x.current?.focus())};return document.addEventListener("keydown",A),()=>document.removeEventListener("keydown",A)},[m]),v.useEffect(()=>{if(!m){b("");return}const A=setTimeout(()=>{document.getElementById(M)?.focus()},50);return()=>{clearTimeout(A)}},[m,M]);const S=v.useCallback(()=>{d||p(A=>!A)},[d]),w=v.useCallback(A=>{const O=A.target.value;b(O),o?.(O)},[o]),E=v.useCallback(A=>{if(r){const T=C.some(z=>String(z)===String(A))?C.filter(z=>String(z)!==String(A)):[...C,A];n?.(T)}else n?.(A),p(!1),x.current?.focus()},[r,C,n]),D=A=>C.some(O=>String(O)===String(A)),P=y.length===0&&e.length===0,L=y.length>0&&k.length===0,$=k.length>0;return h.jsxs(I,{ref:_,className:`relative inline-block ${u}`,id:f,...g,children:[h.jsxs(ae,{ref:x,type:"button",onClick:S,disabled:d,className:` +`]))),m1=function(e,n){var r=e.isFocused,s=e.size,o=e.theme,i=o.colors,a=o.spacing.baseUnit;return Ee({label:"loadingIndicator",display:"flex",transition:"color 150ms",alignSelf:"center",fontSize:s,lineHeight:1,marginRight:s,textAlign:"center",verticalAlign:"middle"},n?{}:{color:r?i.neutral60:i.neutral20,padding:a*2})},Tl=function(e){var n=e.delay,r=e.offset;return _e("span",{css:Cl({animation:"".concat(p1," 1s ease-in-out ").concat(n,"ms infinite;"),backgroundColor:"currentColor",borderRadius:"1em",display:"inline-block",marginLeft:r?"1em":void 0,height:"1em",verticalAlign:"top",width:"1em"},"","")})},b1=function(e){var n=e.innerProps,r=e.isRtl,s=e.size,o=s===void 0?4:s,i=In(e,a1);return _e("div",Ae({},vt(Ee(Ee({},i),{},{innerProps:n,isRtl:r,size:o}),"loadingIndicator",{indicator:!0,"loading-indicator":!0}),n),_e(Tl,{delay:0,offset:r}),_e(Tl,{delay:160,offset:!0}),_e(Tl,{delay:320,offset:!r}))},y1=function(e,n){var r=e.isDisabled,s=e.isFocused,o=e.theme,i=o.colors,a=o.borderRadius,l=o.spacing;return Ee({label:"control",alignItems:"center",cursor:"default",display:"flex",flexWrap:"wrap",justifyContent:"space-between",minHeight:l.controlHeight,outline:"0 !important",position:"relative",transition:"all 100ms"},n?{}:{backgroundColor:r?i.neutral5:i.neutral0,borderColor:r?i.neutral10:s?i.primary:i.neutral20,borderRadius:a,borderStyle:"solid",borderWidth:1,boxShadow:s?"0 0 0 1px ".concat(i.primary):void 0,"&:hover":{borderColor:s?i.primary:i.neutral30}})},v1=function(e){var n=e.children,r=e.isDisabled,s=e.isFocused,o=e.innerRef,i=e.innerProps,a=e.menuIsOpen;return _e("div",Ae({ref:o},vt(e,"control",{control:!0,"control--is-disabled":r,"control--is-focused":s,"control--menu-is-open":a}),i,{"aria-disabled":r||void 0}),n)},x1=v1,w1=["data"],k1=function(e,n){var r=e.theme.spacing;return n?{}:{paddingBottom:r.baseUnit*2,paddingTop:r.baseUnit*2}},C1=function(e){var n=e.children,r=e.cx,s=e.getStyles,o=e.getClassNames,i=e.Heading,a=e.headingProps,l=e.innerProps,c=e.label,d=e.theme,u=e.selectProps;return _e("div",Ae({},vt(e,"group",{group:!0}),l),_e(i,Ae({},a,{selectProps:u,theme:d,getStyles:s,getClassNames:o,cx:r}),c),_e("div",null,n))},S1=function(e,n){var r=e.theme,s=r.colors,o=r.spacing;return Ee({label:"group",cursor:"default",display:"block"},n?{}:{color:s.neutral40,fontSize:"75%",fontWeight:500,marginBottom:"0.25em",paddingLeft:o.baseUnit*3,paddingRight:o.baseUnit*3,textTransform:"uppercase"})},_1=function(e){var n=hf(e);n.data;var r=In(n,w1);return _e("div",Ae({},vt(e,"groupHeading",{"group-heading":!0}),r))},E1=C1,M1=["innerRef","isDisabled","isHidden","inputClassName"],N1=function(e,n){var r=e.isDisabled,s=e.value,o=e.theme,i=o.spacing,a=o.colors;return Ee(Ee({visibility:r?"hidden":"visible",transform:s?"translateZ(0)":""},O1),n?{}:{margin:i.baseUnit/2,paddingBottom:i.baseUnit/2,paddingTop:i.baseUnit/2,color:a.neutral80})},_f={gridArea:"1 / 2",font:"inherit",minWidth:"2px",border:0,margin:0,outline:0,padding:0},O1={flex:"1 1 auto",display:"inline-grid",gridArea:"1 / 1 / 2 / 3",gridTemplateColumns:"0 min-content","&:after":Ee({content:'attr(data-value) " "',visibility:"hidden",whiteSpace:"pre"},_f)},T1=function(e){return Ee({label:"input",color:"inherit",background:0,opacity:e?0:1,width:"100%"},_f)},A1=function(e){var n=e.cx,r=e.value,s=hf(e),o=s.innerRef,i=s.isDisabled,a=s.isHidden,l=s.inputClassName,c=In(s,M1);return _e("div",Ae({},vt(e,"input",{"input-container":!0}),{"data-value":r||""}),_e("input",Ae({className:n({input:!0},l),ref:o,style:T1(a),disabled:i},c)))},D1=A1,I1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors;return Ee({label:"multiValue",display:"flex",minWidth:0},n?{}:{backgroundColor:i.neutral10,borderRadius:o/2,margin:s.baseUnit/2})},L1=function(e,n){var r=e.theme,s=r.borderRadius,o=r.colors,i=e.cropWithEllipsis;return Ee({overflow:"hidden",textOverflow:i||i===void 0?"ellipsis":void 0,whiteSpace:"nowrap"},n?{}:{borderRadius:s/2,color:o.neutral80,fontSize:"85%",padding:3,paddingLeft:6})},P1=function(e,n){var r=e.theme,s=r.spacing,o=r.borderRadius,i=r.colors,a=e.isFocused;return Ee({alignItems:"center",display:"flex"},n?{}:{borderRadius:o/2,backgroundColor:a?i.dangerLight:void 0,paddingLeft:s.baseUnit,paddingRight:s.baseUnit,":hover":{backgroundColor:i.dangerLight,color:i.danger}})},Ef=function(e){var n=e.children,r=e.innerProps;return _e("div",r,n)},$1=Ef,F1=Ef;function V1(t){var e=t.children,n=t.innerProps;return _e("div",Ae({role:"button"},n),e||_e(Ol,{size:14}))}var z1=function(e){var n=e.children,r=e.components,s=e.data,o=e.innerProps,i=e.isDisabled,a=e.removeProps,l=e.selectProps,c=r.Container,d=r.Label,u=r.Remove;return _e(c,{data:s,innerProps:Ee(Ee({},vt(e,"multiValue",{"multi-value":!0,"multi-value--is-disabled":i})),o),selectProps:l},_e(d,{data:s,innerProps:Ee({},vt(e,"multiValueLabel",{"multi-value__label":!0})),selectProps:l},n),_e(u,{data:s,innerProps:Ee(Ee({},vt(e,"multiValueRemove",{"multi-value__remove":!0})),{},{"aria-label":"Remove ".concat(n||"option")},a),selectProps:l}))},H1=z1,U1=function(e,n){var r=e.isDisabled,s=e.isFocused,o=e.isSelected,i=e.theme,a=i.spacing,l=i.colors;return Ee({label:"option",cursor:"default",display:"block",fontSize:"inherit",width:"100%",userSelect:"none",WebkitTapHighlightColor:"rgba(0, 0, 0, 0)"},n?{}:{backgroundColor:o?l.primary:s?l.primary25:"transparent",color:r?l.neutral20:o?l.neutral0:"inherit",padding:"".concat(a.baseUnit*2,"px ").concat(a.baseUnit*3,"px"),":active":{backgroundColor:r?void 0:o?l.primary:l.primary50}})},B1=function(e){var n=e.children,r=e.isDisabled,s=e.isFocused,o=e.isSelected,i=e.innerRef,a=e.innerProps;return _e("div",Ae({},vt(e,"option",{option:!0,"option--is-disabled":r,"option--is-focused":s,"option--is-selected":o}),{ref:i,"aria-disabled":r},a),n)},j1=B1,W1=function(e,n){var r=e.theme,s=r.spacing,o=r.colors;return Ee({label:"placeholder",gridArea:"1 / 1 / 2 / 3"},n?{}:{color:o.neutral50,marginLeft:s.baseUnit/2,marginRight:s.baseUnit/2})},G1=function(e){var n=e.children,r=e.innerProps;return _e("div",Ae({},vt(e,"placeholder",{placeholder:!0}),r),n)},Y1=G1,X1=function(e,n){var r=e.isDisabled,s=e.theme,o=s.spacing,i=s.colors;return Ee({label:"singleValue",gridArea:"1 / 1 / 2 / 3",maxWidth:"100%",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},n?{}:{color:r?i.neutral40:i.neutral80,marginLeft:o.baseUnit/2,marginRight:o.baseUnit/2})},K1=function(e){var n=e.children,r=e.isDisabled,s=e.innerProps;return _e("div",Ae({},vt(e,"singleValue",{"single-value":!0,"single-value--is-disabled":r}),s),n)},q1=K1,gi={ClearIndicator:f1,Control:x1,DropdownIndicator:d1,DownChevron:Cf,CrossIcon:Ol,Group:E1,GroupHeading:_1,IndicatorsContainer:o1,IndicatorSeparator:g1,Input:D1,LoadingIndicator:b1,Menu:Gw,MenuList:Xw,MenuPortal:Rw,LoadingMessage:Qw,NoOptionsMessage:Zw,MultiValue:H1,MultiValueContainer:$1,MultiValueLabel:F1,MultiValueRemove:V1,Option:j1,Placeholder:Y1,SelectContainer:t1,SingleValue:q1,ValueContainer:r1},Z1=function(e){return Ee(Ee({},gi),e.components)},Mf=Number.isNaN||function(e){return typeof e=="number"&&e!==e};function Q1(t,e){return!!(t===e||Mf(t)&&Mf(e))}function J1(t,e){if(t.length!==e.length)return!1;for(var n=0;n<t.length;n++)if(!Q1(t[n],e[n]))return!1;return!0}function R1(t,e){e===void 0&&(e=J1);var n=null;function r(){for(var s=[],o=0;o<arguments.length;o++)s[o]=arguments[o];if(n&&n.lastThis===this&&e(s,n.lastArgs))return n.lastResult;var i=t.apply(this,s);return n={lastResult:i,lastArgs:s,lastThis:this},i}return r.clear=function(){n=null},r}for(var ek={name:"7pg0cj-a11yText",styles:"label:a11yText;z-index:9999;border:0;clip:rect(1px, 1px, 1px, 1px);height:1px;width:1px;position:absolute;overflow:hidden;padding:0;white-space:nowrap"},tk=function(e){return _e("span",Ae({css:ek},e))},Nf=tk,nk={guidance:function(e){var n=e.isSearchable,r=e.isMulti,s=e.tabSelectsValue,o=e.context,i=e.isInitialFocus;switch(o){case"menu":return"Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu".concat(s?", press Tab to select the option and exit the menu":"",".");case"input":return i?"".concat(e["aria-label"]||"Select"," is focused ").concat(n?",type to refine list":"",", press Down to open the menu, ").concat(r?" press left to focus selected values":""):"";case"value":return"Use left and right to toggle between focused values, press Backspace to remove the currently focused value";default:return""}},onChange:function(e){var n=e.action,r=e.label,s=r===void 0?"":r,o=e.labels,i=e.isDisabled;switch(n){case"deselect-option":case"pop-value":case"remove-value":return"option ".concat(s,", deselected.");case"clear":return"All selected options have been cleared.";case"initial-input-focus":return"option".concat(o.length>1?"s":""," ").concat(o.join(","),", selected.");case"select-option":return i?"option ".concat(s," is disabled. Select another option."):"option ".concat(s,", selected.");default:return""}},onFocus:function(e){var n=e.context,r=e.focused,s=e.options,o=e.label,i=o===void 0?"":o,a=e.selectValue,l=e.isDisabled,c=e.isSelected,d=e.isAppleDevice,u=function(p,y){return p&&p.length?"".concat(p.indexOf(y)+1," of ").concat(p.length):""};if(n==="value"&&a)return"value ".concat(i," focused, ").concat(u(a,r),".");if(n==="menu"&&d){var f=l?" disabled":"",g="".concat(c?" selected":"").concat(f);return"".concat(i).concat(g,", ").concat(u(s,r),".")}return""},onFilter:function(e){var n=e.inputValue,r=e.resultsMessage;return"".concat(r).concat(n?" for search term "+n:"",".")}},rk=function(e){var n=e.ariaSelection,r=e.focusedOption,s=e.focusedValue,o=e.focusableOptions,i=e.isFocused,a=e.selectValue,l=e.selectProps,c=e.id,d=e.isAppleDevice,u=l.ariaLiveMessages,f=l.getOptionLabel,g=l.inputValue,m=l.isMulti,p=l.isOptionDisabled,y=l.isSearchable,b=l.menuIsOpen,_=l.options,x=l.screenReaderStatus,E=l.tabSelectsValue,S=l.isLoading,w=l["aria-label"],N=l["aria-live"],C=v.useMemo(function(){return Ee(Ee({},nk),u||{})},[u]),k=v.useMemo(function(){var D="";if(n&&C.onChange){var A=n.option,O=n.options,V=n.removedValue,H=n.removedValues,W=n.value,j=function(Z){return Array.isArray(Z)?null:Z},U=V||A||j(W),B=U?f(U):"",G=O||H||void 0,z=G?G.map(f):[],X=Ee({isDisabled:U&&p(U,a),label:B,labels:z},n);D=C.onChange(X)}return D},[n,C,p,a,f]),M=v.useMemo(function(){var D="",A=r||s,O=!!(r&&a&&a.includes(r));if(A&&C.onFocus){var V={focused:A,label:f(A),isDisabled:p(A,a),isSelected:O,options:o,context:A===r?"menu":"value",selectValue:a,isAppleDevice:d};D=C.onFocus(V)}return D},[r,s,f,p,C,o,a,d]),T=v.useMemo(function(){var D="";if(b&&_.length&&!S&&C.onFilter){var A=x({count:o.length});D=C.onFilter({inputValue:g,resultsMessage:A})}return D},[o,g,b,C,_,x,S]),L=n?.action==="initial-input-focus",P=v.useMemo(function(){var D="";if(C.guidance){var A=s?"value":b?"menu":"input";D=C.guidance({"aria-label":w,context:A,isDisabled:r&&p(r,a),isMulti:m,isSearchable:y,tabSelectsValue:E,isInitialFocus:L})}return D},[w,r,s,m,p,y,b,C,a,E,L]),F=_e(v.Fragment,null,_e("span",{id:"aria-selection"},k),_e("span",{id:"aria-focused"},M),_e("span",{id:"aria-results"},T),_e("span",{id:"aria-guidance"},P));return _e(v.Fragment,null,_e(Nf,{id:c},L&&F),_e(Nf,{"aria-live":N,"aria-atomic":"false","aria-relevant":"additions text",role:"log"},i&&!L&&F))},sk=rk,Al=[{base:"A",letters:"AⒶAÀÁÂẦẤẪẨÃĀĂẰẮẴẲȦǠÄǞẢÅǺǍȀȂẠẬẶḀĄȺⱯ"},{base:"AA",letters:"Ꜳ"},{base:"AE",letters:"ÆǼǢ"},{base:"AO",letters:"Ꜵ"},{base:"AU",letters:"Ꜷ"},{base:"AV",letters:"ꜸꜺ"},{base:"AY",letters:"Ꜽ"},{base:"B",letters:"BⒷBḂḄḆɃƂƁ"},{base:"C",letters:"CⒸCĆĈĊČÇḈƇȻꜾ"},{base:"D",letters:"DⒹDḊĎḌḐḒḎĐƋƊƉꝹ"},{base:"DZ",letters:"DZDŽ"},{base:"Dz",letters:"DzDž"},{base:"E",letters:"EⒺEÈÉÊỀẾỄỂẼĒḔḖĔĖËẺĚȄȆẸỆȨḜĘḘḚƐƎ"},{base:"F",letters:"FⒻFḞƑꝻ"},{base:"G",letters:"GⒼGǴĜḠĞĠǦĢǤƓꞠꝽꝾ"},{base:"H",letters:"HⒽHĤḢḦȞḤḨḪĦⱧⱵꞍ"},{base:"I",letters:"IⒾIÌÍÎĨĪĬİÏḮỈǏȈȊỊĮḬƗ"},{base:"J",letters:"JⒿJĴɈ"},{base:"K",letters:"KⓀKḰǨḲĶḴƘⱩꝀꝂꝄꞢ"},{base:"L",letters:"LⓁLĿĹĽḶḸĻḼḺŁȽⱢⱠꝈꝆꞀ"},{base:"LJ",letters:"LJ"},{base:"Lj",letters:"Lj"},{base:"M",letters:"MⓂMḾṀṂⱮƜ"},{base:"N",letters:"NⓃNǸŃÑṄŇṆŅṊṈȠƝꞐꞤ"},{base:"NJ",letters:"NJ"},{base:"Nj",letters:"Nj"},{base:"O",letters:"OⓄOÒÓÔỒỐỖỔÕṌȬṎŌṐṒŎȮȰÖȪỎŐǑȌȎƠỜỚỠỞỢỌỘǪǬØǾƆƟꝊꝌ"},{base:"OI",letters:"Ƣ"},{base:"OO",letters:"Ꝏ"},{base:"OU",letters:"Ȣ"},{base:"P",letters:"PⓅPṔṖƤⱣꝐꝒꝔ"},{base:"Q",letters:"QⓆQꝖꝘɊ"},{base:"R",letters:"RⓇRŔṘŘȐȒṚṜŖṞɌⱤꝚꞦꞂ"},{base:"S",letters:"SⓈSẞŚṤŜṠŠṦṢṨȘŞⱾꞨꞄ"},{base:"T",letters:"TⓉTṪŤṬȚŢṰṮŦƬƮȾꞆ"},{base:"TZ",letters:"Ꜩ"},{base:"U",letters:"UⓊUÙÚÛŨṸŪṺŬÜǛǗǕǙỦŮŰǓȔȖƯỪỨỮỬỰỤṲŲṶṴɄ"},{base:"V",letters:"VⓋVṼṾƲꝞɅ"},{base:"VY",letters:"Ꝡ"},{base:"W",letters:"WⓌWẀẂŴẆẄẈⱲ"},{base:"X",letters:"XⓍXẊẌ"},{base:"Y",letters:"YⓎYỲÝŶỸȲẎŸỶỴƳɎỾ"},{base:"Z",letters:"ZⓏZŹẐŻŽẒẔƵȤⱿⱫꝢ"},{base:"a",letters:"aⓐaẚàáâầấẫẩãāăằắẵẳȧǡäǟảåǻǎȁȃạậặḁąⱥɐ"},{base:"aa",letters:"ꜳ"},{base:"ae",letters:"æǽǣ"},{base:"ao",letters:"ꜵ"},{base:"au",letters:"ꜷ"},{base:"av",letters:"ꜹꜻ"},{base:"ay",letters:"ꜽ"},{base:"b",letters:"bⓑbḃḅḇƀƃɓ"},{base:"c",letters:"cⓒcćĉċčçḉƈȼꜿↄ"},{base:"d",letters:"dⓓdḋďḍḑḓḏđƌɖɗꝺ"},{base:"dz",letters:"dzdž"},{base:"e",letters:"eⓔeèéêềếễểẽēḕḗĕėëẻěȅȇẹệȩḝęḙḛɇɛǝ"},{base:"f",letters:"fⓕfḟƒꝼ"},{base:"g",letters:"gⓖgǵĝḡğġǧģǥɠꞡᵹꝿ"},{base:"h",letters:"hⓗhĥḣḧȟḥḩḫẖħⱨⱶɥ"},{base:"hv",letters:"ƕ"},{base:"i",letters:"iⓘiìíîĩīĭïḯỉǐȉȋịįḭɨı"},{base:"j",letters:"jⓙjĵǰɉ"},{base:"k",letters:"kⓚkḱǩḳķḵƙⱪꝁꝃꝅꞣ"},{base:"l",letters:"lⓛlŀĺľḷḹļḽḻſłƚɫⱡꝉꞁꝇ"},{base:"lj",letters:"lj"},{base:"m",letters:"mⓜmḿṁṃɱɯ"},{base:"n",letters:"nⓝnǹńñṅňṇņṋṉƞɲʼnꞑꞥ"},{base:"nj",letters:"nj"},{base:"o",letters:"oⓞoòóôồốỗổõṍȭṏōṑṓŏȯȱöȫỏőǒȍȏơờớỡởợọộǫǭøǿɔꝋꝍɵ"},{base:"oi",letters:"ƣ"},{base:"ou",letters:"ȣ"},{base:"oo",letters:"ꝏ"},{base:"p",letters:"pⓟpṕṗƥᵽꝑꝓꝕ"},{base:"q",letters:"qⓠqɋꝗꝙ"},{base:"r",letters:"rⓡrŕṙřȑȓṛṝŗṟɍɽꝛꞧꞃ"},{base:"s",letters:"sⓢsßśṥŝṡšṧṣṩșşȿꞩꞅẛ"},{base:"t",letters:"tⓣtṫẗťṭțţṱṯŧƭʈⱦꞇ"},{base:"tz",letters:"ꜩ"},{base:"u",letters:"uⓤuùúûũṹūṻŭüǜǘǖǚủůűǔȕȗưừứữửựụṳųṷṵʉ"},{base:"v",letters:"vⓥvṽṿʋꝟʌ"},{base:"vy",letters:"ꝡ"},{base:"w",letters:"wⓦwẁẃŵẇẅẘẉⱳ"},{base:"x",letters:"xⓧxẋẍ"},{base:"y",letters:"yⓨyỳýŷỹȳẏÿỷẙỵƴɏỿ"},{base:"z",letters:"zⓩzźẑżžẓẕƶȥɀⱬꝣ"}],ok=new RegExp("["+Al.map(function(t){return t.letters}).join("")+"]","g"),Of={},Dl=0;Dl<Al.length;Dl++)for(var Il=Al[Dl],Ll=0;Ll<Il.letters.length;Ll++)Of[Il.letters[Ll]]=Il.base;var Tf=function(e){return e.replace(ok,function(n){return Of[n]})},ik=R1(Tf),Af=function(e){return e.replace(/^\s+|\s+$/g,"")},ak=function(e){return"".concat(e.label," ").concat(e.value)},lk=function(e){return function(n,r){if(n.data.__isNew__)return!0;var s=Ee({ignoreCase:!0,ignoreAccents:!0,stringify:ak,trim:!0,matchFrom:"any"},e),o=s.ignoreCase,i=s.ignoreAccents,a=s.stringify,l=s.trim,c=s.matchFrom,d=l?Af(r):r,u=l?Af(a(n)):a(n);return o&&(d=d.toLowerCase(),u=u.toLowerCase()),i&&(d=ik(d),u=Tf(u)),c==="start"?u.substr(0,d.length)===d:u.indexOf(d)>-1}},ck=["innerRef"];function dk(t){var e=t.innerRef,n=In(t,ck),r=Fw(n,"onExited","in","enter","exit","appear");return _e("input",Ae({ref:e},r,{css:Cl({label:"dummyInput",background:0,border:0,caretColor:"transparent",fontSize:"inherit",gridArea:"1 / 1 / 2 / 3",outline:0,padding:0,width:1,color:"transparent",left:-100,opacity:0,position:"relative",transform:"scale(.01)"},"","")}))}var uk=function(e){e.cancelable&&e.preventDefault(),e.stopPropagation()};function fk(t){var e=t.isEnabled,n=t.onBottomArrive,r=t.onBottomLeave,s=t.onTopArrive,o=t.onTopLeave,i=v.useRef(!1),a=v.useRef(!1),l=v.useRef(0),c=v.useRef(null),d=v.useCallback(function(y,b){if(c.current!==null){var _=c.current,x=_.scrollTop,E=_.scrollHeight,S=_.clientHeight,w=c.current,N=b>0,C=E-S-x,k=!1;C>b&&i.current&&(r&&r(y),i.current=!1),N&&a.current&&(o&&o(y),a.current=!1),N&&b>C?(n&&!i.current&&n(y),w.scrollTop=E,k=!0,i.current=!0):!N&&-b>x&&(s&&!a.current&&s(y),w.scrollTop=0,k=!0,a.current=!0),k&&uk(y)}},[n,r,s,o]),u=v.useCallback(function(y){d(y,y.deltaY)},[d]),f=v.useCallback(function(y){l.current=y.changedTouches[0].clientY},[]),g=v.useCallback(function(y){var b=l.current-y.changedTouches[0].clientY;d(y,b)},[d]),m=v.useCallback(function(y){if(y){var b=Lw?{passive:!1}:!1;y.addEventListener("wheel",u,b),y.addEventListener("touchstart",f,b),y.addEventListener("touchmove",g,b)}},[g,f,u]),p=v.useCallback(function(y){y&&(y.removeEventListener("wheel",u,!1),y.removeEventListener("touchstart",f,!1),y.removeEventListener("touchmove",g,!1))},[g,f,u]);return v.useEffect(function(){if(e){var y=c.current;return m(y),function(){p(y)}}},[e,m,p]),function(y){c.current=y}}var Df=["boxSizing","height","overflow","paddingRight","position"],If={boxSizing:"border-box",overflow:"hidden",position:"relative",height:"100%"};function Lf(t){t.cancelable&&t.preventDefault()}function Pf(t){t.stopPropagation()}function $f(){var t=this.scrollTop,e=this.scrollHeight,n=t+this.offsetHeight;t===0?this.scrollTop=1:n===e&&(this.scrollTop=t-1)}function Ff(){return"ontouchstart"in window||navigator.maxTouchPoints}var Vf=!!(typeof window<"u"&&window.document&&window.document.createElement),Us=0,qr={capture:!1,passive:!1};function hk(t){var e=t.isEnabled,n=t.accountForScrollbars,r=n===void 0?!0:n,s=v.useRef({}),o=v.useRef(null),i=v.useCallback(function(l){if(Vf){var c=document.body,d=c&&c.style;if(r&&Df.forEach(function(m){var p=d&&d[m];s.current[m]=p}),r&&Us<1){var u=parseInt(s.current.paddingRight,10)||0,f=document.body?document.body.clientWidth:0,g=window.innerWidth-f+u||0;Object.keys(If).forEach(function(m){var p=If[m];d&&(d[m]=p)}),d&&(d.paddingRight="".concat(g,"px"))}c&&Ff()&&(c.addEventListener("touchmove",Lf,qr),l&&(l.addEventListener("touchstart",$f,qr),l.addEventListener("touchmove",Pf,qr))),Us+=1}},[r]),a=v.useCallback(function(l){if(Vf){var c=document.body,d=c&&c.style;Us=Math.max(Us-1,0),r&&Us<1&&Df.forEach(function(u){var f=s.current[u];d&&(d[u]=f)}),c&&Ff()&&(c.removeEventListener("touchmove",Lf,qr),l&&(l.removeEventListener("touchstart",$f,qr),l.removeEventListener("touchmove",Pf,qr)))}},[r]);return v.useEffect(function(){if(e){var l=o.current;return i(l),function(){a(l)}}},[e,i,a]),function(l){o.current=l}}var gk=function(e){var n=e.target;return n.ownerDocument.activeElement&&n.ownerDocument.activeElement.blur()},pk={name:"1kfdb0e",styles:"position:fixed;left:0;bottom:0;right:0;top:0"};function mk(t){var e=t.children,n=t.lockEnabled,r=t.captureEnabled,s=r===void 0?!0:r,o=t.onBottomArrive,i=t.onBottomLeave,a=t.onTopArrive,l=t.onTopLeave,c=fk({isEnabled:s,onBottomArrive:o,onBottomLeave:i,onTopArrive:a,onTopLeave:l}),d=hk({isEnabled:n}),u=function(g){c(g),d(g)};return _e(v.Fragment,null,n&&_e("div",{onClick:gk,css:pk}),e(u))}var bk={name:"1a0ro4n-requiredInput",styles:"label:requiredInput;opacity:0;pointer-events:none;position:absolute;bottom:0;left:0;right:0;width:100%"},yk=function(e){var n=e.name,r=e.onFocus;return _e("input",{required:!0,name:n,tabIndex:-1,"aria-hidden":"true",onFocus:r,css:bk,value:"",onChange:function(){}})},vk=yk;function Pl(t){var e;return typeof window<"u"&&window.navigator!=null?t.test(((e=window.navigator.userAgentData)===null||e===void 0?void 0:e.platform)||window.navigator.platform):!1}function xk(){return Pl(/^iPhone/i)}function zf(){return Pl(/^Mac/i)}function wk(){return Pl(/^iPad/i)||zf()&&navigator.maxTouchPoints>1}function kk(){return xk()||wk()}function Ck(){return zf()||kk()}var Sk=function(e){return e.label},Hf=function(e){return e.label},Uf=function(e){return e.value},_k=function(e){return!!e.isDisabled},Ek={clearIndicator:u1,container:e1,control:y1,dropdownIndicator:c1,group:k1,groupHeading:S1,indicatorsContainer:s1,indicatorSeparator:h1,input:N1,loadingIndicator:m1,loadingMessage:qw,menu:Bw,menuList:Yw,menuPortal:Jw,multiValue:I1,multiValueLabel:L1,multiValueRemove:P1,noOptionsMessage:Kw,option:U1,placeholder:W1,singleValue:X1,valueContainer:n1},Mk={primary:"#2684FF",primary75:"#4C9AFF",primary50:"#B2D4FF",primary25:"#DEEBFF",danger:"#DE350B",dangerLight:"#FFBDAD",neutral0:"hsl(0, 0%, 100%)",neutral5:"hsl(0, 0%, 95%)",neutral10:"hsl(0, 0%, 90%)",neutral20:"hsl(0, 0%, 80%)",neutral30:"hsl(0, 0%, 70%)",neutral40:"hsl(0, 0%, 60%)",neutral50:"hsl(0, 0%, 50%)",neutral60:"hsl(0, 0%, 40%)",neutral70:"hsl(0, 0%, 30%)",neutral80:"hsl(0, 0%, 20%)",neutral90:"hsl(0, 0%, 10%)"},Nk=4,Bf=4,Ok=38,Tk=Bf*2,Ak={baseUnit:Bf,controlHeight:Ok,menuGutter:Tk},$l={borderRadius:Nk,colors:Mk,spacing:Ak},Dk={"aria-live":"polite",backspaceRemovesValue:!0,blurInputOnSelect:mf(),captureMenuScroll:!mf(),classNames:{},closeMenuOnSelect:!0,closeMenuOnScroll:!1,components:{},controlShouldRenderValue:!0,escapeClearsValue:!1,filterOption:lk(),formatGroupLabel:Sk,getOptionLabel:Hf,getOptionValue:Uf,isDisabled:!1,isLoading:!1,isMulti:!1,isRtl:!1,isSearchable:!0,isOptionDisabled:_k,loadingMessage:function(){return"Loading..."},maxMenuHeight:300,minMenuHeight:140,menuIsOpen:!1,menuPlacement:"bottom",menuPosition:"absolute",menuShouldBlockScroll:!1,menuShouldScrollIntoView:!Dw(),noOptionsMessage:function(){return"No options"},openMenuOnFocus:!1,openMenuOnClick:!0,options:[],pageSize:5,placeholder:"Select...",screenReaderStatus:function(e){var n=e.count;return"".concat(n," result").concat(n!==1?"s":""," available")},styles:{},tabIndex:0,tabSelectsValue:!0,unstyled:!1};function jf(t,e,n,r){var s=Kf(t,e,n),o=qf(t,e,n),i=Xf(t,e),a=mi(t,e);return{type:"option",data:e,isDisabled:s,isSelected:o,label:i,value:a,index:r}}function pi(t,e){return t.options.map(function(n,r){if("options"in n){var s=n.options.map(function(i,a){return jf(t,i,e,a)}).filter(function(i){return Yf(t,i)});return s.length>0?{type:"group",data:n,options:s,index:r}:void 0}var o=jf(t,n,e,r);return Yf(t,o)?o:void 0}).filter(Pw)}function Wf(t){return t.reduce(function(e,n){return n.type==="group"?e.push.apply(e,Gr(n.options.map(function(r){return r.data}))):e.push(n.data),e},[])}function Gf(t,e){return t.reduce(function(n,r){return r.type==="group"?n.push.apply(n,Gr(r.options.map(function(s){return{data:s.data,id:"".concat(e,"-").concat(r.index,"-").concat(s.index)}}))):n.push({data:r.data,id:"".concat(e,"-").concat(r.index)}),n},[])}function Ik(t,e){return Wf(pi(t,e))}function Yf(t,e){var n=t.inputValue,r=n===void 0?"":n,s=e.data,o=e.isSelected,i=e.label,a=e.value;return(!Qf(t)||!o)&&Zf(t,{label:i,value:a,data:s},r)}function Lk(t,e){var n=t.focusedValue,r=t.selectValue,s=r.indexOf(n);if(s>-1){var o=e.indexOf(n);if(o>-1)return n;if(s<e.length)return e[s]}return null}function Pk(t,e){var n=t.focusedOption;return n&&e.indexOf(n)>-1?n:e[0]}var Fl=function(e,n){var r,s=(r=e.find(function(o){return o.data===n}))===null||r===void 0?void 0:r.id;return s||null},Xf=function(e,n){return e.getOptionLabel(n)},mi=function(e,n){return e.getOptionValue(n)};function Kf(t,e,n){return typeof t.isOptionDisabled=="function"?t.isOptionDisabled(e,n):!1}function qf(t,e,n){if(n.indexOf(e)>-1)return!0;if(typeof t.isOptionSelected=="function")return t.isOptionSelected(e,n);var r=mi(t,e);return n.some(function(s){return mi(t,s)===r})}function Zf(t,e,n){return t.filterOption?t.filterOption(e,n):!0}var Qf=function(e){var n=e.hideSelectedOptions,r=e.isMulti;return n===void 0?r:n},$k=1,Vl=(function(t){ox(n,t);var e=lx(n);function n(r){var s;if(rx(this,n),s=e.call(this,r),s.state={ariaSelection:null,focusedOption:null,focusedOptionId:null,focusableOptionsWithIds:[],focusedValue:null,inputIsHidden:!1,isFocused:!1,selectValue:[],clearFocusValueOnUpdate:!1,prevWasFocused:!1,inputIsHiddenAfterUpdate:void 0,prevProps:void 0,instancePrefix:"",isAppleDevice:!1},s.blockOptionHover=!1,s.isComposing=!1,s.commonProps=void 0,s.initialTouchX=0,s.initialTouchY=0,s.openAfterFocus=!1,s.scrollToFocusedOptionOnUpdate=!1,s.userIsDragging=void 0,s.controlRef=null,s.getControlRef=function(l){s.controlRef=l},s.focusedOptionRef=null,s.getFocusedOptionRef=function(l){s.focusedOptionRef=l},s.menuListRef=null,s.getMenuListRef=function(l){s.menuListRef=l},s.inputRef=null,s.getInputRef=function(l){s.inputRef=l},s.focus=s.focusInput,s.blur=s.blurInput,s.onChange=function(l,c){var d=s.props,u=d.onChange,f=d.name;c.name=f,s.ariaOnChange(l,c),u(l,c)},s.setValue=function(l,c,d){var u=s.props,f=u.closeMenuOnSelect,g=u.isMulti,m=u.inputValue;s.onInputChange("",{action:"set-value",prevInputValue:m}),f&&(s.setState({inputIsHiddenAfterUpdate:!g}),s.onMenuClose()),s.setState({clearFocusValueOnUpdate:!0}),s.onChange(l,{action:c,option:d})},s.selectOption=function(l){var c=s.props,d=c.blurInputOnSelect,u=c.isMulti,f=c.name,g=s.state.selectValue,m=u&&s.isOptionSelected(l,g),p=s.isOptionDisabled(l,g);if(m){var y=s.getOptionValue(l);s.setValue(g.filter(function(b){return s.getOptionValue(b)!==y}),"deselect-option",l)}else if(!p)u?s.setValue([].concat(Gr(g),[l]),"select-option",l):s.setValue(l,"select-option");else{s.ariaOnChange(l,{action:"select-option",option:l,name:f});return}d&&s.blurInput()},s.removeValue=function(l){var c=s.props.isMulti,d=s.state.selectValue,u=s.getOptionValue(l),f=d.filter(function(m){return s.getOptionValue(m)!==u}),g=Hs(c,f,f[0]||null);s.onChange(g,{action:"remove-value",removedValue:l}),s.focusInput()},s.clearValue=function(){var l=s.state.selectValue;s.onChange(Hs(s.props.isMulti,[],null),{action:"clear",removedValues:l})},s.popValue=function(){var l=s.props.isMulti,c=s.state.selectValue,d=c[c.length-1],u=c.slice(0,c.length-1),f=Hs(l,u,u[0]||null);d&&s.onChange(f,{action:"pop-value",removedValue:d})},s.getFocusedOptionId=function(l){return Fl(s.state.focusableOptionsWithIds,l)},s.getFocusableOptionsWithIds=function(){return Gf(pi(s.props,s.state.selectValue),s.getElementId("option"))},s.getValue=function(){return s.state.selectValue},s.cx=function(){for(var l=arguments.length,c=new Array(l),d=0;d<l;d++)c[d]=arguments[d];return Mw.apply(void 0,[s.props.classNamePrefix].concat(c))},s.getOptionLabel=function(l){return Xf(s.props,l)},s.getOptionValue=function(l){return mi(s.props,l)},s.getStyles=function(l,c){var d=s.props.unstyled,u=Ek[l](c,d);u.boxSizing="border-box";var f=s.props.styles[l];return f?f(u,c):u},s.getClassNames=function(l,c){var d,u;return(d=(u=s.props.classNames)[l])===null||d===void 0?void 0:d.call(u,c)},s.getElementId=function(l){return"".concat(s.state.instancePrefix,"-").concat(l)},s.getComponents=function(){return Z1(s.props)},s.buildCategorizedOptions=function(){return pi(s.props,s.state.selectValue)},s.getCategorizedOptions=function(){return s.props.menuIsOpen?s.buildCategorizedOptions():[]},s.buildFocusableOptions=function(){return Wf(s.buildCategorizedOptions())},s.getFocusableOptions=function(){return s.props.menuIsOpen?s.buildFocusableOptions():[]},s.ariaOnChange=function(l,c){s.setState({ariaSelection:Ee({value:l},c)})},s.onMenuMouseDown=function(l){l.button===0&&(l.stopPropagation(),l.preventDefault(),s.focusInput())},s.onMenuMouseMove=function(l){s.blockOptionHover=!1},s.onControlMouseDown=function(l){if(!l.defaultPrevented){var c=s.props.openMenuOnClick;s.state.isFocused?s.props.menuIsOpen?l.target.tagName!=="INPUT"&&l.target.tagName!=="TEXTAREA"&&s.onMenuClose():c&&s.openMenu("first"):(c&&(s.openAfterFocus=!0),s.focusInput()),l.target.tagName!=="INPUT"&&l.target.tagName!=="TEXTAREA"&&l.preventDefault()}},s.onDropdownIndicatorMouseDown=function(l){if(!(l&&l.type==="mousedown"&&l.button!==0)&&!s.props.isDisabled){var c=s.props,d=c.isMulti,u=c.menuIsOpen;s.focusInput(),u?(s.setState({inputIsHiddenAfterUpdate:!d}),s.onMenuClose()):s.openMenu("first"),l.preventDefault()}},s.onClearIndicatorMouseDown=function(l){l&&l.type==="mousedown"&&l.button!==0||(s.clearValue(),l.preventDefault(),s.openAfterFocus=!1,l.type==="touchend"?s.focusInput():setTimeout(function(){return s.focusInput()}))},s.onScroll=function(l){typeof s.props.closeMenuOnScroll=="boolean"?l.target instanceof HTMLElement&&di(l.target)&&s.props.onMenuClose():typeof s.props.closeMenuOnScroll=="function"&&s.props.closeMenuOnScroll(l)&&s.props.onMenuClose()},s.onCompositionStart=function(){s.isComposing=!0},s.onCompositionEnd=function(){s.isComposing=!1},s.onTouchStart=function(l){var c=l.touches,d=c&&c.item(0);d&&(s.initialTouchX=d.clientX,s.initialTouchY=d.clientY,s.userIsDragging=!1)},s.onTouchMove=function(l){var c=l.touches,d=c&&c.item(0);if(d){var u=Math.abs(d.clientX-s.initialTouchX),f=Math.abs(d.clientY-s.initialTouchY),g=5;s.userIsDragging=u>g||f>g}},s.onTouchEnd=function(l){s.userIsDragging||(s.controlRef&&!s.controlRef.contains(l.target)&&s.menuListRef&&!s.menuListRef.contains(l.target)&&s.blurInput(),s.initialTouchX=0,s.initialTouchY=0)},s.onControlTouchEnd=function(l){s.userIsDragging||s.onControlMouseDown(l)},s.onClearIndicatorTouchEnd=function(l){s.userIsDragging||s.onClearIndicatorMouseDown(l)},s.onDropdownIndicatorTouchEnd=function(l){s.userIsDragging||s.onDropdownIndicatorMouseDown(l)},s.handleInputChange=function(l){var c=s.props.inputValue,d=l.currentTarget.value;s.setState({inputIsHiddenAfterUpdate:!1}),s.onInputChange(d,{action:"input-change",prevInputValue:c}),s.props.menuIsOpen||s.onMenuOpen()},s.onInputFocus=function(l){s.props.onFocus&&s.props.onFocus(l),s.setState({inputIsHiddenAfterUpdate:!1,isFocused:!0}),(s.openAfterFocus||s.props.openMenuOnFocus)&&s.openMenu("first"),s.openAfterFocus=!1},s.onInputBlur=function(l){var c=s.props.inputValue;if(s.menuListRef&&s.menuListRef.contains(document.activeElement)){s.inputRef.focus();return}s.props.onBlur&&s.props.onBlur(l),s.onInputChange("",{action:"input-blur",prevInputValue:c}),s.onMenuClose(),s.setState({focusedValue:null,isFocused:!1})},s.onOptionHover=function(l){if(!(s.blockOptionHover||s.state.focusedOption===l)){var c=s.getFocusableOptions(),d=c.indexOf(l);s.setState({focusedOption:l,focusedOptionId:d>-1?s.getFocusedOptionId(l):null})}},s.shouldHideSelectedOptions=function(){return Qf(s.props)},s.onValueInputFocus=function(l){l.preventDefault(),l.stopPropagation(),s.focus()},s.onKeyDown=function(l){var c=s.props,d=c.isMulti,u=c.backspaceRemovesValue,f=c.escapeClearsValue,g=c.inputValue,m=c.isClearable,p=c.isDisabled,y=c.menuIsOpen,b=c.onKeyDown,_=c.tabSelectsValue,x=c.openMenuOnFocus,E=s.state,S=E.focusedOption,w=E.focusedValue,N=E.selectValue;if(!p&&!(typeof b=="function"&&(b(l),l.defaultPrevented))){switch(s.blockOptionHover=!0,l.key){case"ArrowLeft":if(!d||g)return;s.focusValue("previous");break;case"ArrowRight":if(!d||g)return;s.focusValue("next");break;case"Delete":case"Backspace":if(g)return;if(w)s.removeValue(w);else{if(!u)return;d?s.popValue():m&&s.clearValue()}break;case"Tab":if(s.isComposing||l.shiftKey||!y||!_||!S||x&&s.isOptionSelected(S,N))return;s.selectOption(S);break;case"Enter":if(l.keyCode===229)break;if(y){if(!S||s.isComposing)return;s.selectOption(S);break}return;case"Escape":y?(s.setState({inputIsHiddenAfterUpdate:!1}),s.onInputChange("",{action:"menu-close",prevInputValue:g}),s.onMenuClose()):m&&f&&s.clearValue();break;case" ":if(g)return;if(!y){s.openMenu("first");break}if(!S)return;s.selectOption(S);break;case"ArrowUp":y?s.focusOption("up"):s.openMenu("last");break;case"ArrowDown":y?s.focusOption("down"):s.openMenu("first");break;case"PageUp":if(!y)return;s.focusOption("pageup");break;case"PageDown":if(!y)return;s.focusOption("pagedown");break;case"Home":if(!y)return;s.focusOption("first");break;case"End":if(!y)return;s.focusOption("last");break;default:return}l.preventDefault()}},s.state.instancePrefix="react-select-"+(s.props.instanceId||++$k),s.state.selectValue=ci(r.value),r.menuIsOpen&&s.state.selectValue.length){var o=s.getFocusableOptionsWithIds(),i=s.buildFocusableOptions(),a=i.indexOf(s.state.selectValue[0]);s.state.focusableOptionsWithIds=o,s.state.focusedOption=i[a],s.state.focusedOptionId=Fl(o,i[a])}return s}return sx(n,[{key:"componentDidMount",value:function(){this.startListeningComposition(),this.startListeningToTouch(),this.props.closeMenuOnScroll&&document&&document.addEventListener&&document.addEventListener("scroll",this.onScroll,!0),this.props.autoFocus&&this.focusInput(),this.props.menuIsOpen&&this.state.focusedOption&&this.menuListRef&&this.focusedOptionRef&&pf(this.menuListRef,this.focusedOptionRef),Ck()&&this.setState({isAppleDevice:!0})}},{key:"componentDidUpdate",value:function(s){var o=this.props,i=o.isDisabled,a=o.menuIsOpen,l=this.state.isFocused;(l&&!i&&s.isDisabled||l&&a&&!s.menuIsOpen)&&this.focusInput(),l&&i&&!s.isDisabled?this.setState({isFocused:!1},this.onMenuClose):!l&&!i&&s.isDisabled&&this.inputRef===document.activeElement&&this.setState({isFocused:!0}),this.menuListRef&&this.focusedOptionRef&&this.scrollToFocusedOptionOnUpdate&&(pf(this.menuListRef,this.focusedOptionRef),this.scrollToFocusedOptionOnUpdate=!1)}},{key:"componentWillUnmount",value:function(){this.stopListeningComposition(),this.stopListeningToTouch(),document.removeEventListener("scroll",this.onScroll,!0)}},{key:"onMenuOpen",value:function(){this.props.onMenuOpen()}},{key:"onMenuClose",value:function(){this.onInputChange("",{action:"menu-close",prevInputValue:this.props.inputValue}),this.props.onMenuClose()}},{key:"onInputChange",value:function(s,o){this.props.onInputChange(s,o)}},{key:"focusInput",value:function(){this.inputRef&&this.inputRef.focus()}},{key:"blurInput",value:function(){this.inputRef&&this.inputRef.blur()}},{key:"openMenu",value:function(s){var o=this,i=this.state,a=i.selectValue,l=i.isFocused,c=this.buildFocusableOptions(),d=s==="first"?0:c.length-1;if(!this.props.isMulti){var u=c.indexOf(a[0]);u>-1&&(d=u)}this.scrollToFocusedOptionOnUpdate=!(l&&this.menuListRef),this.setState({inputIsHiddenAfterUpdate:!1,focusedValue:null,focusedOption:c[d],focusedOptionId:this.getFocusedOptionId(c[d])},function(){return o.onMenuOpen()})}},{key:"focusValue",value:function(s){var o=this.state,i=o.selectValue,a=o.focusedValue;if(this.props.isMulti){this.setState({focusedOption:null});var l=i.indexOf(a);a||(l=-1);var c=i.length-1,d=-1;if(i.length){switch(s){case"previous":l===0?d=0:l===-1?d=c:d=l-1;break;case"next":l>-1&&l<c&&(d=l+1);break}this.setState({inputIsHidden:d!==-1,focusedValue:i[d]})}}}},{key:"focusOption",value:function(){var s=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"first",o=this.props.pageSize,i=this.state.focusedOption,a=this.getFocusableOptions();if(a.length){var l=0,c=a.indexOf(i);i||(c=-1),s==="up"?l=c>0?c-1:a.length-1:s==="down"?l=(c+1)%a.length:s==="pageup"?(l=c-o,l<0&&(l=0)):s==="pagedown"?(l=c+o,l>a.length-1&&(l=a.length-1)):s==="last"&&(l=a.length-1),this.scrollToFocusedOptionOnUpdate=!0,this.setState({focusedOption:a[l],focusedValue:null,focusedOptionId:this.getFocusedOptionId(a[l])})}}},{key:"getTheme",value:(function(){return this.props.theme?typeof this.props.theme=="function"?this.props.theme($l):Ee(Ee({},$l),this.props.theme):$l})},{key:"getCommonProps",value:function(){var s=this.clearValue,o=this.cx,i=this.getStyles,a=this.getClassNames,l=this.getValue,c=this.selectOption,d=this.setValue,u=this.props,f=u.isMulti,g=u.isRtl,m=u.options,p=this.hasValue();return{clearValue:s,cx:o,getStyles:i,getClassNames:a,getValue:l,hasValue:p,isMulti:f,isRtl:g,options:m,selectOption:c,selectProps:u,setValue:d,theme:this.getTheme()}}},{key:"hasValue",value:function(){var s=this.state.selectValue;return s.length>0}},{key:"hasOptions",value:function(){return!!this.getFocusableOptions().length}},{key:"isClearable",value:function(){var s=this.props,o=s.isClearable,i=s.isMulti;return o===void 0?i:o}},{key:"isOptionDisabled",value:function(s,o){return Kf(this.props,s,o)}},{key:"isOptionSelected",value:function(s,o){return qf(this.props,s,o)}},{key:"filterOption",value:function(s,o){return Zf(this.props,s,o)}},{key:"formatOptionLabel",value:function(s,o){if(typeof this.props.formatOptionLabel=="function"){var i=this.props.inputValue,a=this.state.selectValue;return this.props.formatOptionLabel(s,{context:o,inputValue:i,selectValue:a})}else return this.getOptionLabel(s)}},{key:"formatGroupLabel",value:function(s){return this.props.formatGroupLabel(s)}},{key:"startListeningComposition",value:(function(){document&&document.addEventListener&&(document.addEventListener("compositionstart",this.onCompositionStart,!1),document.addEventListener("compositionend",this.onCompositionEnd,!1))})},{key:"stopListeningComposition",value:function(){document&&document.removeEventListener&&(document.removeEventListener("compositionstart",this.onCompositionStart),document.removeEventListener("compositionend",this.onCompositionEnd))}},{key:"startListeningToTouch",value:(function(){document&&document.addEventListener&&(document.addEventListener("touchstart",this.onTouchStart,!1),document.addEventListener("touchmove",this.onTouchMove,!1),document.addEventListener("touchend",this.onTouchEnd,!1))})},{key:"stopListeningToTouch",value:function(){document&&document.removeEventListener&&(document.removeEventListener("touchstart",this.onTouchStart),document.removeEventListener("touchmove",this.onTouchMove),document.removeEventListener("touchend",this.onTouchEnd))}},{key:"renderInput",value:(function(){var s=this.props,o=s.isDisabled,i=s.isSearchable,a=s.inputId,l=s.inputValue,c=s.tabIndex,d=s.form,u=s.menuIsOpen,f=s.required,g=this.getComponents(),m=g.Input,p=this.state,y=p.inputIsHidden,b=p.ariaSelection,_=this.commonProps,x=a||this.getElementId("input"),E=Ee(Ee(Ee({"aria-autocomplete":"list","aria-expanded":u,"aria-haspopup":!0,"aria-errormessage":this.props["aria-errormessage"],"aria-invalid":this.props["aria-invalid"],"aria-label":this.props["aria-label"],"aria-labelledby":this.props["aria-labelledby"],"aria-required":f,role:"combobox","aria-activedescendant":this.state.isAppleDevice?void 0:this.state.focusedOptionId||""},u&&{"aria-controls":this.getElementId("listbox")}),!i&&{"aria-readonly":!0}),this.hasValue()?b?.action==="initial-input-focus"&&{"aria-describedby":this.getElementId("live-region")}:{"aria-describedby":this.getElementId("placeholder")});return i?$.createElement(m,Ae({},_,{autoCapitalize:"none",autoComplete:"off",autoCorrect:"off",id:x,innerRef:this.getInputRef,isDisabled:o,isHidden:y,onBlur:this.onInputBlur,onChange:this.handleInputChange,onFocus:this.onInputFocus,spellCheck:"false",tabIndex:c,form:d,type:"text",value:l},E)):$.createElement(dk,Ae({id:x,innerRef:this.getInputRef,onBlur:this.onInputBlur,onChange:li,onFocus:this.onInputFocus,disabled:o,tabIndex:c,inputMode:"none",form:d,value:""},E))})},{key:"renderPlaceholderOrValue",value:function(){var s=this,o=this.getComponents(),i=o.MultiValue,a=o.MultiValueContainer,l=o.MultiValueLabel,c=o.MultiValueRemove,d=o.SingleValue,u=o.Placeholder,f=this.commonProps,g=this.props,m=g.controlShouldRenderValue,p=g.isDisabled,y=g.isMulti,b=g.inputValue,_=g.placeholder,x=this.state,E=x.selectValue,S=x.focusedValue,w=x.isFocused;if(!this.hasValue()||!m)return b?null:$.createElement(u,Ae({},f,{key:"placeholder",isDisabled:p,isFocused:w,innerProps:{id:this.getElementId("placeholder")}}),_);if(y)return E.map(function(C,k){var M=C===S,T="".concat(s.getOptionLabel(C),"-").concat(s.getOptionValue(C));return $.createElement(i,Ae({},f,{components:{Container:a,Label:l,Remove:c},isFocused:M,isDisabled:p,key:T,index:k,removeProps:{onClick:function(){return s.removeValue(C)},onTouchEnd:function(){return s.removeValue(C)},onMouseDown:function(P){P.preventDefault()}},data:C}),s.formatOptionLabel(C,"value"))});if(b)return null;var N=E[0];return $.createElement(d,Ae({},f,{data:N,isDisabled:p}),this.formatOptionLabel(N,"value"))}},{key:"renderClearIndicator",value:function(){var s=this.getComponents(),o=s.ClearIndicator,i=this.commonProps,a=this.props,l=a.isDisabled,c=a.isLoading,d=this.state.isFocused;if(!this.isClearable()||!o||l||!this.hasValue()||c)return null;var u={onMouseDown:this.onClearIndicatorMouseDown,onTouchEnd:this.onClearIndicatorTouchEnd,"aria-hidden":"true"};return $.createElement(o,Ae({},i,{innerProps:u,isFocused:d}))}},{key:"renderLoadingIndicator",value:function(){var s=this.getComponents(),o=s.LoadingIndicator,i=this.commonProps,a=this.props,l=a.isDisabled,c=a.isLoading,d=this.state.isFocused;if(!o||!c)return null;var u={"aria-hidden":"true"};return $.createElement(o,Ae({},i,{innerProps:u,isDisabled:l,isFocused:d}))}},{key:"renderIndicatorSeparator",value:function(){var s=this.getComponents(),o=s.DropdownIndicator,i=s.IndicatorSeparator;if(!o||!i)return null;var a=this.commonProps,l=this.props.isDisabled,c=this.state.isFocused;return $.createElement(i,Ae({},a,{isDisabled:l,isFocused:c}))}},{key:"renderDropdownIndicator",value:function(){var s=this.getComponents(),o=s.DropdownIndicator;if(!o)return null;var i=this.commonProps,a=this.props.isDisabled,l=this.state.isFocused,c={onMouseDown:this.onDropdownIndicatorMouseDown,onTouchEnd:this.onDropdownIndicatorTouchEnd,"aria-hidden":"true"};return $.createElement(o,Ae({},i,{innerProps:c,isDisabled:a,isFocused:l}))}},{key:"renderMenu",value:function(){var s=this,o=this.getComponents(),i=o.Group,a=o.GroupHeading,l=o.Menu,c=o.MenuList,d=o.MenuPortal,u=o.LoadingMessage,f=o.NoOptionsMessage,g=o.Option,m=this.commonProps,p=this.state.focusedOption,y=this.props,b=y.captureMenuScroll,_=y.inputValue,x=y.isLoading,E=y.loadingMessage,S=y.minMenuHeight,w=y.maxMenuHeight,N=y.menuIsOpen,C=y.menuPlacement,k=y.menuPosition,M=y.menuPortalTarget,T=y.menuShouldBlockScroll,L=y.menuShouldScrollIntoView,P=y.noOptionsMessage,F=y.onMenuScrollToTop,D=y.onMenuScrollToBottom;if(!N)return null;var A=function(B,G){var z=B.type,X=B.data,Y=B.isDisabled,Z=B.isSelected,ee=B.label,te=B.value,ce=p===X,J=Y?void 0:function(){return s.onOptionHover(X)},he=Y?void 0:function(){return s.selectOption(X)},le="".concat(s.getElementId("option"),"-").concat(G),ae={id:le,onClick:he,onMouseMove:J,onMouseOver:J,tabIndex:-1,role:"option","aria-selected":s.state.isAppleDevice?void 0:Z};return $.createElement(g,Ae({},m,{innerProps:ae,data:X,isDisabled:Y,isSelected:Z,key:le,label:ee,type:z,value:te,isFocused:ce,innerRef:ce?s.getFocusedOptionRef:void 0}),s.formatOptionLabel(B.data,"menu"))},O;if(this.hasOptions())O=this.getCategorizedOptions().map(function(U){if(U.type==="group"){var B=U.data,G=U.options,z=U.index,X="".concat(s.getElementId("group"),"-").concat(z),Y="".concat(X,"-heading");return $.createElement(i,Ae({},m,{key:X,data:B,options:G,Heading:a,headingProps:{id:Y,data:U.data},label:s.formatGroupLabel(U.data)}),U.options.map(function(Z){return A(Z,"".concat(z,"-").concat(Z.index))}))}else if(U.type==="option")return A(U,"".concat(U.index))});else if(x){var V=E({inputValue:_});if(V===null)return null;O=$.createElement(u,m,V)}else{var H=P({inputValue:_});if(H===null)return null;O=$.createElement(f,m,H)}var W={minMenuHeight:S,maxMenuHeight:w,menuPlacement:C,menuPosition:k,menuShouldScrollIntoView:L},j=$.createElement(jw,Ae({},m,W),function(U){var B=U.ref,G=U.placerProps,z=G.placement,X=G.maxHeight;return $.createElement(l,Ae({},m,W,{innerRef:B,innerProps:{onMouseDown:s.onMenuMouseDown,onMouseMove:s.onMenuMouseMove},isLoading:x,placement:z}),$.createElement(mk,{captureEnabled:b,onTopArrive:F,onBottomArrive:D,lockEnabled:T},function(Y){return $.createElement(c,Ae({},m,{innerRef:function(ee){s.getMenuListRef(ee),Y(ee)},innerProps:{role:"listbox","aria-multiselectable":m.isMulti,id:s.getElementId("listbox")},isLoading:x,maxHeight:X,focusedOption:p}),O)}))});return M||k==="fixed"?$.createElement(d,Ae({},m,{appendTo:M,controlElement:this.controlRef,menuPlacement:C,menuPosition:k}),j):j}},{key:"renderFormField",value:function(){var s=this,o=this.props,i=o.delimiter,a=o.isDisabled,l=o.isMulti,c=o.name,d=o.required,u=this.state.selectValue;if(d&&!this.hasValue()&&!a)return $.createElement(vk,{name:c,onFocus:this.onValueInputFocus});if(!(!c||a))if(l)if(i){var f=u.map(function(p){return s.getOptionValue(p)}).join(i);return $.createElement("input",{name:c,type:"hidden",value:f})}else{var g=u.length>0?u.map(function(p,y){return $.createElement("input",{key:"i-".concat(y),name:c,type:"hidden",value:s.getOptionValue(p)})}):$.createElement("input",{name:c,type:"hidden",value:""});return $.createElement("div",null,g)}else{var m=u[0]?this.getOptionValue(u[0]):"";return $.createElement("input",{name:c,type:"hidden",value:m})}}},{key:"renderLiveRegion",value:function(){var s=this.commonProps,o=this.state,i=o.ariaSelection,a=o.focusedOption,l=o.focusedValue,c=o.isFocused,d=o.selectValue,u=this.getFocusableOptions();return $.createElement(sk,Ae({},s,{id:this.getElementId("live-region"),ariaSelection:i,focusedOption:a,focusedValue:l,isFocused:c,selectValue:d,focusableOptions:u,isAppleDevice:this.state.isAppleDevice}))}},{key:"render",value:function(){var s=this.getComponents(),o=s.Control,i=s.IndicatorsContainer,a=s.SelectContainer,l=s.ValueContainer,c=this.props,d=c.className,u=c.id,f=c.isDisabled,g=c.menuIsOpen,m=this.state.isFocused,p=this.commonProps=this.getCommonProps();return $.createElement(a,Ae({},p,{className:d,innerProps:{id:u,onKeyDown:this.onKeyDown},isDisabled:f,isFocused:m}),this.renderLiveRegion(),$.createElement(o,Ae({},p,{innerRef:this.getControlRef,innerProps:{onMouseDown:this.onControlMouseDown,onTouchEnd:this.onControlTouchEnd},isDisabled:f,isFocused:m,menuIsOpen:g}),$.createElement(l,Ae({},p,{isDisabled:f}),this.renderPlaceholderOrValue(),this.renderInput()),$.createElement(i,Ae({},p,{isDisabled:f}),this.renderClearIndicator(),this.renderLoadingIndicator(),this.renderIndicatorSeparator(),this.renderDropdownIndicator())),this.renderMenu(),this.renderFormField())}}],[{key:"getDerivedStateFromProps",value:function(s,o){var i=o.prevProps,a=o.clearFocusValueOnUpdate,l=o.inputIsHiddenAfterUpdate,c=o.ariaSelection,d=o.isFocused,u=o.prevWasFocused,f=o.instancePrefix,g=s.options,m=s.value,p=s.menuIsOpen,y=s.inputValue,b=s.isMulti,_=ci(m),x={};if(i&&(m!==i.value||g!==i.options||p!==i.menuIsOpen||y!==i.inputValue)){var E=p?Ik(s,_):[],S=p?Gf(pi(s,_),"".concat(f,"-option")):[],w=a?Lk(o,_):null,N=Pk(o,E),C=Fl(S,N);x={selectValue:_,focusedOption:N,focusedOptionId:C,focusableOptionsWithIds:S,focusedValue:w,clearFocusValueOnUpdate:!1}}var k=l!=null&&s!==i?{inputIsHidden:l,inputIsHiddenAfterUpdate:void 0}:{},M=c,T=d&&u;return d&&!T&&(M={value:Hs(b,_,_[0]||null),options:_,action:"initial-input-focus"},T=!u),c?.action==="initial-input-focus"&&(M=null),Ee(Ee(Ee({},x),k),{},{prevProps:s,ariaSelection:M,prevWasFocused:T})}}]),n})(v.Component);Vl.defaultProps=Dk;var Fk=v.forwardRef(function(t,e){var n=Lu(t);return $.createElement(Vl,Ae({ref:e},n))}),Jf=Fk,Vk=["allowCreateWhileLoading","createOptionPosition","formatCreateLabel","isValidNewOption","getNewOptionData","onCreateOption","options","onChange"],Rf=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:"",n=arguments.length>1?arguments[1]:void 0,r=arguments.length>2?arguments[2]:void 0,s=String(e).toLowerCase(),o=String(r.getOptionValue(n)).toLowerCase(),i=String(r.getOptionLabel(n)).toLowerCase();return o===s||i===s},zl={formatCreateLabel:function(e){return'Create "'.concat(e,'"')},isValidNewOption:function(e,n,r,s){return!(!e||n.some(function(o){return Rf(e,o,s)})||r.some(function(o){return Rf(e,o,s)}))},getNewOptionData:function(e,n){return{label:n,value:e,__isNew__:!0}}};function zk(t){var e=t.allowCreateWhileLoading,n=e===void 0?!1:e,r=t.createOptionPosition,s=r===void 0?"last":r,o=t.formatCreateLabel,i=o===void 0?zl.formatCreateLabel:o,a=t.isValidNewOption,l=a===void 0?zl.isValidNewOption:a,c=t.getNewOptionData,d=c===void 0?zl.getNewOptionData:c,u=t.onCreateOption,f=t.options,g=f===void 0?[]:f,m=t.onChange,p=In(t,Vk),y=p.getOptionValue,b=y===void 0?Uf:y,_=p.getOptionLabel,x=_===void 0?Hf:_,E=p.inputValue,S=p.isLoading,w=p.isMulti,N=p.value,C=p.name,k=v.useMemo(function(){return l(E,ci(N),g,{getOptionValue:b,getOptionLabel:x})?d(E,i(E)):void 0},[i,d,x,b,E,l,g,N]),M=v.useMemo(function(){return(n||!S)&&k?s==="first"?[k].concat(Gr(g)):[].concat(Gr(g),[k]):g},[n,s,S,k,g]),T=v.useCallback(function(L,P){if(P.action!=="select-option")return m(L,P);var F=Array.isArray(L)?L:[L];if(F[F.length-1]===k){if(u)u(E);else{var D=d(E,E),A={action:"create-option",name:C,option:D};m(Hs(w,[].concat(Gr(ci(N)),[D]),D),A)}return}m(L,P)},[d,E,w,C,k,u,m,N]);return Ee(Ee({},p),{},{options:M,onChange:T})}var Hk=v.forwardRef(function(t,e){var n=Lu(t),r=zk(n);return $.createElement(Vl,Ae({ref:e},r))}),eh=Hk;const Ft={control:"input-tag",valueContainer:"gap-1 flex flex-wrap",multiValue:{blue:"bg-blue-100 dark:bg-blue-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",green:"bg-green-100 dark:bg-green-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",amber:"bg-amber-100 dark:bg-amber-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",red:"bg-red-100 dark:bg-red-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",purple:"bg-purple-100 dark:bg-purple-900/20 rounded px-1.5 py-0.5 flex items-center gap-1",gray:"bg-gray-100 dark:bg-gray-700/50 rounded px-1.5 py-0.5 flex items-center gap-1"},multiValueLabel:{blue:"text-blue-700 dark:text-blue-300 text-xs",green:"text-green-700 dark:text-green-300 text-xs font-semibold",amber:"text-amber-700 dark:text-amber-300 text-xs font-semibold",red:"text-red-700 dark:text-red-300 text-xs font-semibold",purple:"text-purple-700 dark:text-purple-300 text-xs font-semibold",gray:"text-gray-700 dark:text-gray-300 text-xs"},multiValueRemove:{blue:"text-blue-700 dark:text-blue-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",green:"text-green-700 dark:text-green-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",amber:"text-amber-700 dark:text-amber-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",red:"text-red-700 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",purple:"text-purple-700 dark:text-purple-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1",gray:"text-gray-700 dark:text-gray-300 hover:bg-red-100 dark:hover:bg-red-900/30 hover:text-red-600 dark:hover:text-red-400 rounded px-1"},input:"text-gray-900 dark:text-white text-sm",placeholder:"text-gray-400 dark:text-gray-500 text-sm",menu:"mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg",menuList:"py-1",option:{base:"px-3 py-2 cursor-pointer text-gray-900 dark:text-white text-[13px]",selected:"bg-blue-100 dark:bg-blue-900/20",focused:"bg-gray-100 dark:bg-gray-700",default:"bg-white dark:bg-gray-800"},noOptionsMessage:"px-3 py-2 text-gray-500 dark:text-gray-400 text-[13px]",checkbox:{base:"w-4 h-4 rounded border flex items-center justify-center text-white text-[10px] shrink-0",selected:"bg-blue-500 border-blue-500",unselected:"border-gray-400 dark:border-gray-500"}},th=t=>{const{data:e,isSelected:n}=t;return h.jsx(gi.Option,{...t,children:h.jsxs("div",{className:"flex items-center gap-2",children:[h.jsx("span",{className:`${Ft.checkbox.base} ${n?Ft.checkbox.selected:Ft.checkbox.unselected}`,children:n?"✓":""}),h.jsxs("div",{className:"flex-1 min-w-0",children:[h.jsxs("div",{className:"flex items-center gap-1",children:[h.jsx("span",{className:"text-[13px]",children:e.label}),e.count!==void 0&&e.count>0&&h.jsxs("span",{className:"text-gray-500 dark:text-gray-400 text-[10px]",children:["(",e.count,")"]})]}),e.description&&h.jsx("p",{className:"text-xs text-gray-500 dark:text-gray-400 truncate",children:e.description})]})]})})},nh=t=>h.jsx(gi.GroupHeading,{...t,children:h.jsx("div",{className:"px-3 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-700",children:t.data.label})}),Uk=t=>{const{data:e}=t;return h.jsxs(gi.MultiValueLabel,{...t,children:[h.jsx("span",{children:e.label}),e.count!==void 0&&e.count>0&&h.jsxs("span",{className:"text-xs opacity-60 ml-1",children:["(",e.count,")"]})]})},Bk=({value:t,options:e=[],onChange:n,creatable:r=!1,placeholder:s="검색 또는 입력...",noOptionsMessage:o="검색 결과가 없습니다",formatCreateLabel:i,createLabelSuffix:a="추가",maxItems:l,disabled:c=!1,onBeforeRemove:d,onCreateOption:u,className:f="",tagVariants:g,defaultVariant:m="blue",name:p,isMulti:y=!0,isClearable:b=!0,isSearchable:_=!0,delimiters:x=[","],splitOnPaste:E=!0,onInputChange:S,id:w,editorAttrs:N})=>{const C=c===!0||c==="true",k=v.useCallback(J=>J==null?[]:Array.isArray(J)?J:[J],[]),[M,T]=v.useState(()=>k(t)),[L,P]=v.useState(""),F=v.useRef(void 0);v.useEffect(()=>{const J=(le,ae)=>!le&&!ae?!0:!le||!ae||le.length!==ae.length?!1:le.every((Se,ue)=>Se===ae[ue]),he=k(t);J(he,F.current)||(T(he),F.current=he)},[t,k]);const D=v.useMemo(()=>i?typeof i=="string"?J=>`${i} "${J}"`:i:J=>h.jsxs("span",{className:"text-[13px]",children:[h.jsx("strong",{children:J})," ",a]}),[i,a]),A=v.useMemo(()=>!M||!Array.isArray(M)?[]:M.map(J=>e.find(le=>le.value===J)||{value:J,label:String(J)}).filter(J=>J!==void 0),[M,e]),O=v.useMemo(()=>{const J=new Set(e.map(we=>we.value)),he=M?M.filter(we=>!J.has(we)).map(we=>({value:we,label:String(we)})):[],le=[...e,...he];if(!le.some(we=>we.group))return le;const Se=new Map,ue=[];le.forEach(we=>{if(we.group){const He=Se.get(we.group)||[];He.push(we),Se.set(we.group,He)}else ue.push(we)});const pe=[];return Se.forEach((we,He)=>{pe.push({label:He,options:we})}),ue.length>0&&pe.push({label:"",options:ue}),pe},[e,M]),V=v.useCallback(J=>({target:{value:J},currentTarget:{value:J},preventDefault:()=>{},stopPropagation:()=>{}}),[]),H=v.useCallback(J=>{if(!x?.length)return[J.trim()].filter(Boolean);const he=x.map(ae=>ae.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),le=new RegExp(`[${he.join("")}]`);return J.split(le).map(ae=>ae.trim()).filter(Boolean)},[x]),W=v.useCallback((J,he)=>{if(he.action!=="input-change"){P(J);return}if(S&&S(V(J)),!x?.some(ue=>J.includes(ue))||!r||!y){P(J);return}const Se=H(J).filter(ue=>ue&&!M.includes(ue));if(Se.length>0){const ue=l?l-M.length:1/0,pe=Se.slice(0,ue);if(pe.length>0){const we=[...M,...pe];T(we),n(V(we))}}P("")},[x,r,y,H,M,l,n,V,S]),j=v.useCallback(J=>{if(!E||!r||!y)return;const he=J.clipboardData.getData("text"),le=H(he);if(le.length<=1)return;J.preventDefault();const ae=le.filter(Se=>Se&&!M.includes(Se));if(ae.length>0){const Se=l?l-M.length:1/0,ue=ae.slice(0,Se);if(ue.length>0){const pe=[...M,...ue];T(pe),n(V(pe))}}},[E,r,y,H,M,l,n,V]),U=v.useCallback((J,he)=>{if((he.action==="remove-value"||he.action==="pop-value")&&he.removedValue&&d){const ae=d(he.removedValue);if(ae!==!0){const Se=typeof ae=="string"?ae:`"${he.removedValue.label}"은(는) 제거할 수 없습니다.`;alert(Se);return}}if(l&&J.length>l)return;const le=J.map(ae=>ae.value);T(le),n(V(le))},[n,d,l,V]),B=v.useCallback((J,he)=>{if(he.action==="clear"&&M.length>0&&d){const Se=e.find(pe=>pe.value===M[0])||{value:M[0],label:String(M[0])},ue=d(Se);if(ue!==!0){const pe=typeof ue=="string"?ue:`"${Se.label}"은(는) 제거할 수 없습니다.`;alert(pe);return}}const le=J?J.value:null,ae=J?[J.value]:[];T(ae),n(V(le))},[n,d,M,e,V]),G=v.useCallback(J=>{u&&u(J);const he=[...M||[],J];T(he),n(V(he))},[n,u,M,V]),z=v.useCallback(J=>!J||!J.trim()?!1:!M?.includes(J.trim()),[M]),X=v.useCallback(J=>{const he=L.trim();if(J.key==="Tab"&&r&&y&&he){if(M.includes(he))J.preventDefault(),P("");else if(!l||M.length<l){J.preventDefault();const le=[...M,he];T(le),n(V(le)),P("")}return}J.key==="Enter"&&(!he||M&&M.includes(he))&&(J.preventDefault(),J.stopPropagation())},[M,L,r,y,l,n,V]),Y=v.useCallback(J=>g&&g[J]?g[J]:m,[g,m]),Z={control:()=>`${Ft.control} ${f} ${C?"opacity-60 cursor-not-allowed":""}`,valueContainer:()=>Ft.valueContainer,multiValue:J=>{const he=Y(J.data.value);return Ft.multiValue[he]},multiValueLabel:J=>{const he=Y(J.data.value);return Ft.multiValueLabel[he]},multiValueRemove:J=>{const he=Y(J.data.value);return`${Ft.multiValueRemove[he]} ${C?"pointer-events-none":""}`},singleValue:()=>"text-gray-900 dark:text-white text-sm",input:()=>Ft.input,placeholder:()=>Ft.placeholder,menu:()=>Ft.menu,menuList:()=>Ft.menuList,menuPortal:()=>"z-[9999]",option:J=>`${Ft.option.base} ${J.isSelected?Ft.option.selected:J.isFocused?Ft.option.focused:Ft.option.default}`,noOptionsMessage:()=>Ft.noOptionsMessage,clearIndicator:()=>"text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 cursor-pointer px-1",dropdownIndicator:()=>"text-gray-400 dark:text-gray-500 px-1"},ee={options:O,onKeyDown:X,placeholder:s,noOptionsMessage:()=>o,isDisabled:C,isSearchable:_,menuIsOpen:void 0,openMenuOnClick:!0,openMenuOnFocus:!0,menuPortalTarget:typeof document<"u"?document.body:null,menuPosition:"fixed",classNamePrefix:"tag-input",unstyled:!0,classNames:Z,styles:{menuPortal:J=>({...J,zIndex:9999})},...r&&y||S?{inputValue:L,onInputChange:W}:{}},te=y?M.join(","):M[0]??"";if(!y){const J={...ee,isMulti:!1,value:A[0]||null,onChange:B,isClearable:b,closeMenuOnSelect:!0,components:{Option:th,GroupHeading:nh,IndicatorSeparator:()=>null,...b?{}:{ClearIndicator:()=>null}}};return r?h.jsxs(I,{id:w,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:te}),h.jsx(eh,{...J,onCreateOption:G,formatCreateLabel:D,isValidNewOption:z,createOptionPosition:"first"})]}):h.jsxs(I,{id:w,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:te}),h.jsx(Jf,{...J})]})}const ce={...ee,isMulti:!0,value:A,onChange:U,closeMenuOnSelect:!1,hideSelectedOptions:!1,backspaceRemovesValue:!1,isClearable:!1,components:{Option:th,GroupHeading:nh,MultiValueLabel:Uk,ClearIndicator:()=>null,DropdownIndicator:()=>null,IndicatorSeparator:()=>null}};return r?h.jsxs(I,{id:w,...N,onPaste:j,children:[p&&h.jsx("input",{type:"hidden",name:p,value:te}),h.jsx(eh,{...ce,onCreateOption:G,formatCreateLabel:D,isValidNewOption:z,createOptionPosition:"first"})]}):h.jsxs(I,{id:w,...N,children:[p&&h.jsx("input",{type:"hidden",name:p,value:te}),h.jsx(Jf,{...ce})]})},Zr=(t,e)=>window.G7Core?.t?.(t,e)??t,jk=()=>window.G7Core?.locale?.supported?.()??["ko","en"],Wk=()=>window.G7Core?.locale?.current?.()??"ko",Hl=t=>{const e=Zr(`common.language_${t}`);return e===`common.language_${t}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[t]||t.toUpperCase():e},Gk=({value:t=[],onChange:e,placeholder:n="입력 후 Enter",disabled:r=!1,creatable:s=!0,delimiters:o=[","],className:i="",name:a,error:l,maxItems:c,modalId:d,statePath:u,__componentContext:f,id:g,editorAttrs:m})=>{const p=v.useMemo(()=>jk(),[]),y=v.useMemo(()=>Wk(),[]),[b,_]=v.useState(()=>t||[]),[x,E]=v.useState(""),[S,w]=v.useState(!1),[N,C]=v.useState(null),[k,M]=v.useState({}),T=v.useRef(null),L=v.useRef(void 0);v.useEffect(()=>{const z=(Y,Z)=>!Y&&!Z?!0:!Y||!Z||Y.length!==Z.length?!1:JSON.stringify(Y)===JSON.stringify(Z),X=t||[];z(X,L.current)||(_(X),L.current=X)},[t]);const P=v.useCallback(z=>({target:{name:a,value:z},currentTarget:{name:a,value:z},preventDefault:()=>{},stopPropagation:()=>{}}),[a]),F=v.useCallback(z=>{const X=[];for(const Y of p){const Z=z[Y];Z?.trim()&&X.push(`${Y.toUpperCase()}: ${Z}`)}return X.length>0?X.join(" / "):""},[p]),D=v.useCallback(z=>{const X=b.filter((Y,Z)=>Z!==z);_(X),e?.(P(X))},[b,e,P]),A=v.useCallback((z,X,Y)=>{const Z=window.G7Core;if(!Z?.dispatch)return;const ee=p.map(te=>({code:te,label:Hl(te)}));Z.dispatch({handler:"setState",params:{target:"global",multilingualTagEdit:{fieldName:a,editingIndex:z,values:X,supportedLocales:ee,defaultLocale:y,statePath:u,currentTags:Y}}}),Z.dispatch({handler:"openModal",target:d},{componentContext:f})},[a,p,y,d,u,f]),O=v.useCallback(z=>{r||(d?A(z,{...b[z]},b):(C(z),M({...b[z]}),w(!0)))},[r,b,d,A]),V=v.useCallback((z="")=>{const X={};p.forEach(Y=>{X[Y]=Y===y?z:""}),d?(A(null,X,b),E("")):(C(null),M(X),w(!0),E(""))},[p,y,d,A,b]),H=v.useCallback(()=>{if(!Object.values(k).some(Y=>Y.trim()!=="")){w(!1);return}let X;N!==null?X=b.map((Y,Z)=>Z===N?k:Y):X=[...b,k],_(X),e?.(P(X)),w(!1),M({}),C(null)},[k,N,b,e,P]),W=v.useCallback(()=>{w(!1),M({}),C(null)},[]),j=v.useCallback(z=>{const X=z.target.value;if(o.some(Z=>X.includes(Z))&&s){const Z=o.map(ce=>ce.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")),ee=new RegExp(`[${Z.join("")}]`),te=X.split(ee).map(ce=>ce.trim()).filter(Boolean);if(te.length>0){V(te[0]);return}}E(X)},[o,s,V]),U=v.useCallback(z=>{if(z.key==="Enter"&&x.trim()&&s){if(z.preventDefault(),c&&b.length>=c)return;V(x.trim())}},[x,s,c,b.length,V]),B=v.useCallback((z,X)=>{M(Y=>({...Y,[z]:X}))},[]),G=!!l;return h.jsxs(I,{className:i,id:g,...m,children:[h.jsxs(I,{className:`flex flex-wrap gap-1.5 p-2 border rounded-lg bg-white dark:bg-gray-700 min-h-[42px] ${G?"border-red-500 dark:border-red-400":"border-gray-300 dark:border-gray-600 focus-within:ring-2 focus-within:ring-blue-500 focus-within:border-blue-500"} ${r?"opacity-60 bg-gray-100 dark:bg-gray-800 cursor-not-allowed":""}`,children:[b.map((z,X)=>h.jsxs(I,{className:"flex items-center gap-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded px-2 py-0.5 text-sm",children:[h.jsx(Q,{className:`cursor-pointer hover:underline ${r?"cursor-not-allowed":""}`,onClick:()=>O(X),children:F(z)}),!r&&h.jsx(oe,{type:"button",className:"p-0 h-4 w-4 min-w-0 border-0 bg-transparent text-blue-600 dark:text-blue-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-transparent",onClick:Y=>{Y.stopPropagation(),D(X)},children:h.jsx(re,{name:"times",className:"text-xs"})})]},X)),s&&!r&&(!c||b.length<c)&&h.jsx("input",{ref:T,type:"text",value:x,onChange:j,onKeyDown:U,placeholder:b.length===0?n:"",disabled:r,className:"flex-1 min-w-[120px] bg-transparent border-none outline-none text-sm text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500"})]}),l&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1",children:l}),!d&&h.jsx(Bo,{isOpen:S,onClose:W,title:Zr(N!==null?"common.edit":"common.add"),width:"400px",children:h.jsxs(I,{className:"space-y-4 p-4",children:[p.map(z=>h.jsxs(I,{className:"space-y-1",children:[h.jsxs(I,{className:"flex items-center gap-2",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300 w-16",children:Hl(z)}),h.jsxs(Q,{className:"text-xs text-gray-400 dark:text-gray-500",children:["(",z.toUpperCase(),")"]})]}),h.jsx(wt,{type:"text",value:k[z]??"",onChange:X=>B(z,X.target.value),placeholder:`${Hl(z)} 값 입력`,className:"w-full",autoFocus:z===y})]},z)),h.jsxs(I,{className:"flex justify-end gap-2 pt-4 border-t border-gray-200 dark:border-gray-700",children:[h.jsx(oe,{type:"button",variant:"secondary",onClick:W,children:Zr("common.cancel")}),h.jsx(oe,{type:"button",variant:"primary",onClick:H,children:Zr(N!==null?"common.save":"common.add")})]})]})}),a&&h.jsx("input",{type:"hidden",name:a,value:JSON.stringify(b)})]})},rh=(t,e)=>window.G7Core?.t?.(t,e)??t,sh=({type:t,identifier:e,name:n,installedModules:r=[],installedPlugins:s=[],className:o="",style:i,id:a,editorAttrs:l})=>{const c={module:de.Cubes,plugin:de.PuzzlePiece},d={module:rh("common.module"),plugin:rh("common.plugin")},u=v.useMemo(()=>{if(n)return n;if(e){const p=(t==="module"?r:s)?.find(y=>y.identifier===e);if(p)return p.name}},[n,e,t,r,s]),f=c[t],g=u?`${u} ${d[t]}`:d[t];return h.jsxs(Q,{className:`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium text-gray-400 dark:text-gray-500 ${o}`,style:i,id:a,...l,children:[h.jsx(re,{name:f,className:"w-3.5 h-3.5"}),h.jsx(Q,{children:g})]})},Yk=({options:t=[],value:e,onChange:n,multiple:r=!1,searchPlaceholder:s="Search...",onSearch:o,noResultsText:i="No results found",initialText:a="Type to search",triggerLabel:l="Select",triggerIcon:c,disabled:d=!1,className:u="",id:f,editorAttrs:g})=>{const[m,p]=v.useState(!1),[y,b]=v.useState(""),_=v.useRef(null),x=v.useRef(null),E=v.useRef(`searchable-dropdown-input-${Math.random().toString(36).slice(2,9)}`).current,S=v.useMemo(()=>e==null?[]:Array.isArray(e)?e:[e],[e]),w=v.useMemo(()=>{if(!y.trim())return t;const D=y.toLowerCase();return t.filter(A=>A.label.toLowerCase().includes(D)||A.description?.toLowerCase().includes(D))},[t,y]),N=v.useMemo(()=>{if(S.length===0)return l;if(r){const A=S.length;return A===1?t.find(V=>String(V.value)===String(S[0]))?.label||l:`${A} selected`}return t.find(A=>String(A.value)===String(S[0]))?.label||l},[S,t,r,l]);v.useEffect(()=>{const D=A=>{_.current&&!_.current.contains(A.target)&&p(!1)};return m&&document.addEventListener("mousedown",D),()=>{document.removeEventListener("mousedown",D)}},[m]),v.useEffect(()=>{const D=A=>{A.key==="Escape"&&m&&(p(!1),x.current?.focus())};return document.addEventListener("keydown",D),()=>document.removeEventListener("keydown",D)},[m]),v.useEffect(()=>{if(!m){b("");return}const D=setTimeout(()=>{document.getElementById(E)?.focus()},50);return()=>{clearTimeout(D)}},[m,E]);const C=v.useCallback(()=>{d||p(D=>!D)},[d]),k=v.useCallback(D=>{const A=D.target.value;b(A),o?.(A)},[o]),M=v.useCallback(D=>{if(r){const O=S.some(V=>String(V)===String(D))?S.filter(V=>String(V)!==String(D)):[...S,D];n?.(O)}else n?.(D),p(!1),x.current?.focus()},[r,S,n]),T=D=>S.some(A=>String(A)===String(D)),L=y.length===0&&t.length===0,P=y.length>0&&w.length===0,F=w.length>0;return h.jsxs(I,{ref:_,className:`relative inline-block ${u}`,id:f,...g,children:[h.jsxs(oe,{ref:x,type:"button",onClick:C,disabled:d,className:` px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-gray-700 dark:text-gray-200 font-medium @@ -182,27 +188,27 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed min-w-[200px] - `,"aria-haspopup":"listbox","aria-expanded":m,children:[h.jsxs(I,{className:"flex items-center gap-2",children:[c,h.jsx(Q,{className:"truncate",children:N})]}),h.jsx(Zn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${m?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),m&&h.jsxs(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden min-w-[280px]",role:"listbox",children:[h.jsx(I,{className:"p-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx("input",{id:M,type:"text",value:y,onChange:w,placeholder:s,className:"w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:outline-none"})}),h.jsxs(I,{className:"max-h-80 overflow-auto",children:[P&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:a}),L&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:i}),$&&h.jsx(I,{className:"py-2",children:k.map(A=>{const O=D(A.value);return h.jsxs(ae,{type:"button",onClick:()=>!A.disabled&&E(A.value),disabled:A.disabled,className:` + `,"aria-haspopup":"listbox","aria-expanded":m,children:[h.jsxs(I,{className:"flex items-center gap-2",children:[c,h.jsx(Q,{className:"truncate",children:N})]}),h.jsx(Wn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${m?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),m&&h.jsxs(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden min-w-[280px]",role:"listbox",children:[h.jsx(I,{className:"p-3 border-b border-gray-200 dark:border-gray-700",children:h.jsx("input",{id:E,type:"text",value:y,onChange:k,placeholder:s,className:"w-full px-3 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-transparent focus:outline-none"})}),h.jsxs(I,{className:"max-h-80 overflow-auto",children:[L&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:a}),P&&h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:i}),F&&h.jsx(I,{className:"py-2",children:w.map(D=>{const A=T(D.value);return h.jsxs(oe,{type:"button",onClick:()=>!D.disabled&&M(D.value),disabled:D.disabled,className:` w-full px-4 py-3 text-left flex items-start gap-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors - ${A.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"} - `,role:"option","aria-selected":O,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:O&&h.jsx(Zn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${O?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children:A.label}),A.description&&h.jsx(I,{className:"text-sm text-gray-500 dark:text-gray-400 mt-0.5",children:A.description})]})]},A.value)})})]})]})]})},Js={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},fC=({options:e=[],value:t=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>e.find(m=>String(m.value)===String(f))?.label||String(f),[e]),c=v.useCallback(f=>{if(s)return;const g=t.filter(m=>String(m)!==String(f));n?.(g)},[t,n,s]),d=v.useMemo(()=>t.map(f=>({value:f,label:l(f)})),[t,l]),u=`${Js.container.base} ${s?Js.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(Q,{className:Js.placeholder,children:r}):d.map(f=>h.jsxs(Q,{className:Js.tag,children:[f.label,!s&&h.jsx(Q,{className:Js.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Zn,{className:"w-3 h-3",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]},f.value))})};function hC(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>r=>{t.forEach(s=>s(r))},t)}const _i=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function ls(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function Kl(e){return"nodeType"in e}function Jt(e){var t,n;return e?ls(e)?e:Kl(e)&&(t=(n=e.ownerDocument)==null?void 0:n.defaultView)!=null?t:window:window}function ql(e){const{Document:t}=Jt(e);return e instanceof t}function Rs(e){return ls(e)?!1:e instanceof Jt(e).HTMLElement}function ph(e){return e instanceof Jt(e).SVGElement}function cs(e){return e?ls(e)?e.document:Kl(e)?ql(e)?e:Rs(e)||ph(e)?e.ownerDocument:document:document:document}const Mn=_i?v.useLayoutEffect:v.useEffect;function Ei(e){const t=v.useRef(e);return Mn(()=>{t.current=e}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return t.current==null?void 0:t.current(...r)},[])}function gC(){const e=v.useRef(null),t=v.useCallback((r,s)=>{e.current=setInterval(r,s)},[]),n=v.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,n]}function eo(e,t){t===void 0&&(t=[e]);const n=v.useRef(e);return Mn(()=>{n.current!==e&&(n.current=e)},t),n}function to(e,t){const n=v.useRef();return v.useMemo(()=>{const r=e(n.current);return n.current=r,r},[...t])}function Mi(e){const t=Ei(e),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&t?.(s,n.current),n.current=s},[]);return[n,r]}function Ni(e){const t=v.useRef();return v.useEffect(()=>{t.current=e},[e]),t.current}let Zl={};function no(e,t){return v.useMemo(()=>{if(t)return t;const n=Zl[e]==null?0:Zl[e]+1;return Zl[e]=n,e+"-"+n},[e,t])}function mh(e){return function(t){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+e*c)}return o},{...t})}}const ds=mh(1),ro=mh(-1);function pC(e){return"clientX"in e&&"clientY"in e}function Ti(e){if(!e)return!1;const{KeyboardEvent:t}=Jt(e.target);return t&&e instanceof t}function mC(e){if(!e)return!1;const{TouchEvent:t}=Jt(e.target);return t&&e instanceof t}function Oi(e){if(mC(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:n}=e.touches[0];return{x:t,y:n}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:n}=e.changedTouches[0];return{x:t,y:n}}}return pC(e)?{x:e.clientX,y:e.clientY}:null}const jn=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:n}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:n}=e;return"scaleX("+t+") scaleY("+n+")"}},Transform:{toString(e){if(e)return[jn.Translate.toString(e),jn.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:n,easing:r}=e;return t+" "+n+"ms "+r}}}),bh="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function bC(e){return e.matches(bh)?e:e.querySelector(bh)}const yC={display:"none"};function vC(e){let{id:t,value:n}=e;return v.createElement("div",{id:t,style:yC},n)}function xC(e){let{id:t,announcement:n,ariaLiveType:r="assertive"}=e;const s={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return v.createElement("div",{id:t,style:s,role:"status","aria-live":r,"aria-atomic":!0},n)}function wC(){const[e,t]=v.useState("");return{announce:v.useCallback(r=>{r!=null&&t(r)},[]),announcement:e}}const yh=v.createContext(null);function kC(e){const t=v.useContext(yh);v.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function CC(){const[e]=v.useState(()=>new Set),t=v.useCallback(r=>(e.add(r),()=>e.delete(r)),[e]);return[v.useCallback(r=>{let{type:s,event:o}=r;e.forEach(i=>{var a;return(a=i[s])==null?void 0:a.call(i,o)})},[e]),t]}const SC={draggable:` + ${D.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"} + `,role:"option","aria-selected":A,children:[h.jsx(I,{className:"w-5 h-5 flex-shrink-0 mt-0.5",children:A&&h.jsx(Wn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsx(I,{className:`font-medium ${A?"text-blue-600 dark:text-blue-400":"text-gray-900 dark:text-white"}`,children:D.label}),D.description&&h.jsx(I,{className:"text-sm text-gray-500 dark:text-gray-400 mt-0.5",children:D.description})]})]},D.value)})})]})]})]})},Bs={container:{base:"tag-select",disabled:"opacity-50 cursor-not-allowed"},tag:"tag",tagRemove:"tag-remove",placeholder:"tag-select-placeholder"},Xk=({options:t=[],value:e=[],onChange:n,placeholder:r="No items selected",disabled:s=!1,className:o="",id:i,editorAttrs:a})=>{const l=v.useCallback(f=>t.find(m=>String(m.value)===String(f))?.label||String(f),[t]),c=v.useCallback(f=>{if(s)return;const g=e.filter(m=>String(m)!==String(f));n?.(g)},[e,n,s]),d=v.useMemo(()=>e.map(f=>({value:f,label:l(f)})),[e,l]),u=`${Bs.container.base} ${s?Bs.container.disabled:""} ${o}`.trim();return h.jsx(I,{className:u,id:i,...a,children:d.length===0?h.jsx(Q,{className:Bs.placeholder,children:r}):d.map(f=>h.jsxs(Q,{className:Bs.tag,children:[f.label,!s&&h.jsx(Q,{className:Bs.tagRemove,onClick:()=>c(f.value),role:"button","aria-label":`Remove ${f.label}`,children:h.jsx(Wn,{className:"w-3 h-3",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]},f.value))})};function Kk(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>r=>{e.forEach(s=>s(r))},e)}const bi=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Qr(t){const e=Object.prototype.toString.call(t);return e==="[object Window]"||e==="[object global]"}function Ul(t){return"nodeType"in t}function qt(t){var e,n;return t?Qr(t)?t:Ul(t)&&(e=(n=t.ownerDocument)==null?void 0:n.defaultView)!=null?e:window:window}function Bl(t){const{Document:e}=qt(t);return t instanceof e}function js(t){return Qr(t)?!1:t instanceof qt(t).HTMLElement}function oh(t){return t instanceof qt(t).SVGElement}function Jr(t){return t?Qr(t)?t.document:Ul(t)?Bl(t)?t:js(t)||oh(t)?t.ownerDocument:document:document:document}const _n=bi?v.useLayoutEffect:v.useEffect;function yi(t){const e=v.useRef(t);return _n(()=>{e.current=t}),v.useCallback(function(){for(var n=arguments.length,r=new Array(n),s=0;s<n;s++)r[s]=arguments[s];return e.current==null?void 0:e.current(...r)},[])}function qk(){const t=v.useRef(null),e=v.useCallback((r,s)=>{t.current=setInterval(r,s)},[]),n=v.useCallback(()=>{t.current!==null&&(clearInterval(t.current),t.current=null)},[]);return[e,n]}function Ws(t,e){e===void 0&&(e=[t]);const n=v.useRef(t);return _n(()=>{n.current!==t&&(n.current=t)},e),n}function Gs(t,e){const n=v.useRef();return v.useMemo(()=>{const r=t(n.current);return n.current=r,r},[...e])}function vi(t){const e=yi(t),n=v.useRef(null),r=v.useCallback(s=>{s!==n.current&&e?.(s,n.current),n.current=s},[]);return[n,r]}function xi(t){const e=v.useRef();return v.useEffect(()=>{e.current=t},[t]),e.current}let jl={};function Ys(t,e){return v.useMemo(()=>{if(e)return e;const n=jl[t]==null?0:jl[t]+1;return jl[t]=n,t+"-"+n},[t,e])}function ih(t){return function(e){for(var n=arguments.length,r=new Array(n>1?n-1:0),s=1;s<n;s++)r[s-1]=arguments[s];return r.reduce((o,i)=>{const a=Object.entries(i);for(const[l,c]of a){const d=o[l];d!=null&&(o[l]=d+t*c)}return o},{...e})}}const Rr=ih(1),Xs=ih(-1);function Zk(t){return"clientX"in t&&"clientY"in t}function wi(t){if(!t)return!1;const{KeyboardEvent:e}=qt(t.target);return e&&t instanceof e}function Qk(t){if(!t)return!1;const{TouchEvent:e}=qt(t.target);return e&&t instanceof e}function ki(t){if(Qk(t)){if(t.touches&&t.touches.length){const{clientX:e,clientY:n}=t.touches[0];return{x:e,y:n}}else if(t.changedTouches&&t.changedTouches.length){const{clientX:e,clientY:n}=t.changedTouches[0];return{x:e,y:n}}}return Zk(t)?{x:t.clientX,y:t.clientY}:null}const Fn=Object.freeze({Translate:{toString(t){if(!t)return;const{x:e,y:n}=t;return"translate3d("+(e?Math.round(e):0)+"px, "+(n?Math.round(n):0)+"px, 0)"}},Scale:{toString(t){if(!t)return;const{scaleX:e,scaleY:n}=t;return"scaleX("+e+") scaleY("+n+")"}},Transform:{toString(t){if(t)return[Fn.Translate.toString(t),Fn.Scale.toString(t)].join(" ")}},Transition:{toString(t){let{property:e,duration:n,easing:r}=t;return e+" "+n+"ms "+r}}}),ah="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function Jk(t){return t.matches(ah)?t:t.querySelector(ah)}const Rk={display:"none"};function eC(t){let{id:e,value:n}=t;return v.createElement("div",{id:e,style:Rk},n)}function tC(t){let{id:e,announcement:n,ariaLiveType:r="assertive"}=t;const s={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return v.createElement("div",{id:e,style:s,role:"status","aria-live":r,"aria-atomic":!0},n)}function nC(){const[t,e]=v.useState("");return{announce:v.useCallback(r=>{r!=null&&e(r)},[]),announcement:t}}const lh=v.createContext(null);function rC(t){const e=v.useContext(lh);v.useEffect(()=>{if(!e)throw new Error("useDndMonitor must be used within a children of <DndContext>");return e(t)},[t,e])}function sC(){const[t]=v.useState(()=>new Set),e=v.useCallback(r=>(t.add(r),()=>t.delete(r)),[t]);return[v.useCallback(r=>{let{type:s,event:o}=r;t.forEach(i=>{var a;return(a=i[s])==null?void 0:a.call(i,o)})},[t]),e]}const oC={draggable:` 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. - `},_C={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 EC(e){let{announcements:t=_C,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=SC}=e;const{announce:o,announcement:i}=wC(),a=no("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),kC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(t.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;t.onDragMove&&o(t.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(t.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(t.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(t.onDragCancel({active:f,over:g}))}}),[o,t])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(vC,{id:r,value:s.draggable}),v.createElement(xC,{id:a,announcement:i}));return n?$n.createPortal(d,n):d}var Nt;(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"})(Nt||(Nt={}));function Ai(){}function us(e,t){return v.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function Ql(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return v.useMemo(()=>[...t].filter(r=>r!=null),[...t])}const Nn=Object.freeze({x:0,y:0});function vh(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function MC(e,t){const n=Oi(e);if(!n)return"0 0";const r={x:(n.x-t.left)/t.width*100,y:(n.y-t.top)/t.height*100};return r.x+"% "+r.y+"%"}function xh(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return n-r}function NC(e,t){let{data:{value:n}}=e,{data:{value:r}}=t;return r-n}function wh(e){let{left:t,top:n,height:r,width:s}=e;return[{x:t,y:n},{x:t+s,y:n},{x:t,y:n+r},{x:t+s,y:n+r}]}function kh(e,t){if(!e||e.length===0)return null;const[n]=e;return n[t]}function Ch(e,t,n){return t===void 0&&(t=e.left),n===void 0&&(n=e.top),{x:t+e.width*.5,y:n+e.height*.5}}const Jl=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=Ch(t,t.left,t.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=vh(Ch(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(xh)},TC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=wh(t),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=wh(l),d=s.reduce((f,g,m)=>f+vh(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(xh)};function OC(e,t){const n=Math.max(t.top,e.top),r=Math.max(t.left,e.left),s=Math.min(t.left+t.width,e.left+e.width),o=Math.min(t.top+t.height,e.top+e.height),i=s-r,a=o-n;if(r<s&&n<o){const l=t.width*t.height,c=e.width*e.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const AC=e=>{let{collisionRect:t,droppableRects:n,droppableContainers:r}=e;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=OC(a,t);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(NC)};function DC(e,t,n){return{...e,scaleX:t&&n?t.width/n.width:1,scaleY:t&&n?t.height/n.height:1}}function Sh(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Nn}function IC(e){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+e*a.y,bottom:i.bottom+e*a.y,left:i.left+e*a.x,right:i.right+e*a.x}),{...n})}}const LC=IC(1);function _h(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 PC(e,t,n){const r=_h(t);if(!r)return e;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=e.left-i-(1-s)*parseFloat(n),c=e.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?e.width/s:e.width,u=o?e.height/o:e.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const $C={ignoreTransform:!1};function fs(e,t){t===void 0&&(t=$C);let n=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:c,transformOrigin:d}=Jt(e).getComputedStyle(e);c&&(n=PC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function Eh(e){return fs(e,{ignoreTransform:!0})}function FC(e){const t=e.innerWidth,n=e.innerHeight;return{top:0,left:0,right:t,bottom:n,width:t,height:n}}function zC(e,t){return t===void 0&&(t=Jt(e).getComputedStyle(e)),t.position==="fixed"}function VC(e,t){t===void 0&&(t=Jt(e).getComputedStyle(e));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(s=>{const o=t[s];return typeof o=="string"?n.test(o):!1})}function Di(e,t){const n=[];function r(s){if(t!=null&&n.length>=t||!s)return n;if(ql(s)&&s.scrollingElement!=null&&!n.includes(s.scrollingElement))return n.push(s.scrollingElement),n;if(!Rs(s)||ph(s)||n.includes(s))return n;const o=Jt(e).getComputedStyle(s);return s!==e&&VC(s,o)&&n.push(s),zC(s,o)?n:r(s.parentNode)}return e?r(e):n}function Mh(e){const[t]=Di(e,1);return t??null}function Rl(e){return!_i||!e?null:ls(e)?e:Kl(e)?ql(e)||e===cs(e).scrollingElement?window:Rs(e)?e:null:null}function Nh(e){return ls(e)?e.scrollX:e.scrollLeft}function Th(e){return ls(e)?e.scrollY:e.scrollTop}function ec(e){return{x:Nh(e),y:Th(e)}}var Dt;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(Dt||(Dt={}));function Oh(e){return!_i||!e?!1:e===document.scrollingElement}function Ah(e){const t={x:0,y:0},n=Oh(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},r={x:e.scrollWidth-n.width,y:e.scrollHeight-n.height},s=e.scrollTop<=t.y,o=e.scrollLeft<=t.x,i=e.scrollTop>=r.y,a=e.scrollLeft>=r.x;return{isTop:s,isLeft:o,isBottom:i,isRight:a,maxScroll:r,minScroll:t}}const HC={x:.2,y:.2};function UC(e,t,n,r,s){let{top:o,left:i,right:a,bottom:l}=n;r===void 0&&(r=10),s===void 0&&(s=HC);const{isTop:c,isBottom:d,isLeft:u,isRight:f}=Ah(e),g={x:0,y:0},m={x:0,y:0},p={height:t.height*s.y,width:t.width*s.x};return!c&&o<=t.top+p.height?(g.y=Dt.Backward,m.y=r*Math.abs((t.top+p.height-o)/p.height)):!d&&l>=t.bottom-p.height&&(g.y=Dt.Forward,m.y=r*Math.abs((t.bottom-p.height-l)/p.height)),!f&&a>=t.right-p.width?(g.x=Dt.Forward,m.x=r*Math.abs((t.right-p.width-a)/p.width)):!u&&i<=t.left+p.width&&(g.x=Dt.Backward,m.x=r*Math.abs((t.left+p.width-i)/p.width)),{direction:g,speed:m}}function BC(e){if(e===document.scrollingElement){const{innerWidth:o,innerHeight:i}=window;return{top:0,left:0,right:o,bottom:i,width:o,height:i}}const{top:t,left:n,right:r,bottom:s}=e.getBoundingClientRect();return{top:t,left:n,right:r,bottom:s,width:e.clientWidth,height:e.clientHeight}}function Dh(e){return e.reduce((t,n)=>ds(t,ec(n)),Nn)}function jC(e){return e.reduce((t,n)=>t+Nh(n),0)}function WC(e){return e.reduce((t,n)=>t+Th(n),0)}function Ih(e,t){if(t===void 0&&(t=fs),!e)return;const{top:n,left:r,bottom:s,right:o}=t(e);Mh(e)&&(s<=0||o<=0||n>=window.innerHeight||r>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const GC=[["x",["left","right"],jC],["y",["top","bottom"],WC]];class tc{constructor(t,n){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const r=Di(n),s=Dh(r);this.rect={...t},this.width=t.width,this.height=t.height;for(const[o,i,a]of GC)for(const l of i)Object.defineProperty(this,l,{get:()=>{const c=a(r),d=s[o]-c;return this.rect[l]+d},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class so{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(n=>{var r;return(r=this.target)==null?void 0:r.removeEventListener(...n)})},this.target=t}add(t,n,r){var s;(s=this.target)==null||s.addEventListener(t,n,r),this.listeners.push([t,n,r])}}function YC(e){const{EventTarget:t}=Jt(e);return e instanceof t?e:cs(e)}function nc(e,t){const n=Math.abs(e.x),r=Math.abs(e.y);return typeof t=="number"?Math.sqrt(n**2+r**2)>t:"x"in t&&"y"in t?n>t.x&&r>t.y:"x"in t?n>t.x:"y"in t?r>t.y:!1}var gn;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(gn||(gn={}));function Lh(e){e.preventDefault()}function XC(e){e.stopPropagation()}var qe;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(qe||(qe={}));const Ph={start:[qe.Space,qe.Enter],cancel:[qe.Esc],end:[qe.Space,qe.Enter,qe.Tab]},KC=(e,t)=>{let{currentCoordinates:n}=t;switch(e.code){case qe.Right:return{...n,x:n.x+25};case qe.Left:return{...n,x:n.x-25};case qe.Down:return{...n,y:n.y+25};case qe.Up:return{...n,y:n.y-25}}};class oo{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:n}}=t;this.props=t,this.listeners=new so(cs(n)),this.windowListeners=new so(Jt(n)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(gn.Resize,this.handleCancel),this.windowListeners.add(gn.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(gn.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:n}=this.props,r=t.node.current;r&&Ih(r),n(Nn)}handleKeyDown(t){if(Ti(t)){const{active:n,context:r,options:s}=this.props,{keyboardCodes:o=Ph,coordinateGetter:i=KC,scrollBehavior:a="smooth"}=s,{code:l}=t;if(o.end.includes(l)){this.handleEnd(t);return}if(o.cancel.includes(l)){this.handleCancel(t);return}const{collisionRect:c}=r.current,d=c?{x:c.left,y:c.top}:Nn;this.referenceCoordinates||(this.referenceCoordinates=d);const u=i(t,{active:n,context:r.current,currentCoordinates:d});if(u){const f=ro(u,d),g={x:0,y:0},{scrollableAncestors:m}=r.current;for(const p of m){const y=t.code,{isTop:b,isRight:_,isLeft:x,isBottom:M,maxScroll:C,minScroll:k}=Ah(p),N=BC(p),S={x:Math.min(y===qe.Right?N.right-N.width/2:N.right,Math.max(y===qe.Right?N.left:N.left+N.width/2,u.x)),y:Math.min(y===qe.Down?N.bottom-N.height/2:N.bottom,Math.max(y===qe.Down?N.top:N.top+N.height/2,u.y))},w=y===qe.Right&&!_||y===qe.Left&&!x,E=y===qe.Down&&!M||y===qe.Up&&!b;if(w&&S.x!==u.x){const D=p.scrollLeft+f.x,P=y===qe.Right&&D<=C.x||y===qe.Left&&D>=k.x;if(P&&!f.y){p.scrollTo({left:D,behavior:a});return}P?g.x=p.scrollLeft-D:g.x=y===qe.Right?p.scrollLeft-C.x:p.scrollLeft-k.x,g.x&&p.scrollBy({left:-g.x,behavior:a});break}else if(E&&S.y!==u.y){const D=p.scrollTop+f.y,P=y===qe.Down&&D<=C.y||y===qe.Up&&D>=k.y;if(P&&!f.x){p.scrollTo({top:D,behavior:a});return}P?g.y=p.scrollTop-D:g.y=y===qe.Down?p.scrollTop-C.y:p.scrollTop-k.y,g.y&&p.scrollBy({top:-g.y,behavior:a});break}}this.handleMove(t,ds(ro(u,this.referenceCoordinates),g))}}}handleMove(t,n){const{onMove:r}=this.props;t.preventDefault(),r(n)}handleEnd(t){const{onEnd:n}=this.props;t.preventDefault(),this.detach(),n()}handleCancel(t){const{onCancel:n}=this.props;t.preventDefault(),this.detach(),n()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}oo.activators=[{eventName:"onKeyDown",handler:(e,t,n)=>{let{keyboardCodes:r=Ph,onActivation:s}=t,{active:o}=n;const{code:i}=e.nativeEvent;if(r.start.includes(i)){const a=o.activatorNode.current;return a&&e.target!==a?!1:(e.preventDefault(),s?.({event:e.nativeEvent}),!0)}return!1}}];function $h(e){return!!(e&&"distance"in e)}function Fh(e){return!!(e&&"delay"in e)}class rc{constructor(t,n,r){var s;r===void 0&&(r=YC(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=n;const{event:o}=t,{target:i}=o;this.props=t,this.events=n,this.document=cs(i),this.documentListeners=new so(this.document),this.listeners=new so(r),this.windowListeners=new so(Jt(i)),this.initialCoordinates=(s=Oi(o))!=null?s:Nn,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:n,bypassActivationConstraint:r}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(gn.Resize,this.handleCancel),this.windowListeners.add(gn.DragStart,Lh),this.windowListeners.add(gn.VisibilityChange,this.handleCancel),this.windowListeners.add(gn.ContextMenu,Lh),this.documentListeners.add(gn.Keydown,this.handleKeydown),n){if(r!=null&&r({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(Fh(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if($h(n)){this.handlePending(n);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,n){const{active:r,onPending:s}=this.props;s(r,t,this.initialCoordinates,n)}handleStart(){const{initialCoordinates:t}=this,{onStart:n}=this.props;t&&(this.activated=!0,this.documentListeners.add(gn.Click,XC,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(gn.SelectionChange,this.removeTextSelection),n(t))}handleMove(t){var n;const{activated:r,initialCoordinates:s,props:o}=this,{onMove:i,options:{activationConstraint:a}}=o;if(!s)return;const l=(n=Oi(t))!=null?n:Nn,c=ro(s,l);if(!r&&a){if($h(a)){if(a.tolerance!=null&&nc(c,a.tolerance))return this.handleCancel();if(nc(c,a.distance))return this.handleStart()}if(Fh(a)&&nc(c,a.tolerance))return this.handleCancel();this.handlePending(a,c);return}t.cancelable&&t.preventDefault(),i(l)}handleEnd(){const{onAbort:t,onEnd:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleCancel(){const{onAbort:t,onCancel:n}=this.props;this.detach(),this.activated||t(this.props.active),n()}handleKeydown(t){t.code===qe.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const qC={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class io extends rc{constructor(t){const{event:n}=t,r=cs(n.target);super(t,qC,r)}}io.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:r}=t;return!n.isPrimary||n.button!==0?!1:(r?.({event:n}),!0)}}];const ZC={move:{name:"mousemove"},end:{name:"mouseup"}};var sc;(function(e){e[e.RightClick=2]="RightClick"})(sc||(sc={}));class QC extends rc{constructor(t){super(t,ZC,cs(t.event.target))}}QC.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:r}=t;return n.button===sc.RightClick?!1:(r?.({event:n}),!0)}}];const oc={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class JC extends rc{constructor(t){super(t,oc)}static setup(){return window.addEventListener(oc.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(oc.move.name,t)};function t(){}}}JC.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:n}=e,{onActivation:r}=t;const{touches:s}=n;return s.length>1?!1:(r?.({event:n}),!0)}}];var ao;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(ao||(ao={}));var Ii;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(Ii||(Ii={}));function RC(e){let{acceleration:t,activator:n=ao.Pointer,canScroll:r,draggingRect:s,enabled:o,interval:i=5,order:a=Ii.TreeOrder,pointerCoordinates:l,scrollableAncestors:c,scrollableAncestorRects:d,delta:u,threshold:f}=e;const g=tS({delta:u,disabled:!o}),[m,p]=gC(),y=v.useRef({x:0,y:0}),b=v.useRef({x:0,y:0}),_=v.useMemo(()=>{switch(n){case ao.Pointer:return l?{top:l.y,bottom:l.y,left:l.x,right:l.x}:null;case ao.DraggableRect:return s}},[n,s,l]),x=v.useRef(null),M=v.useCallback(()=>{const k=x.current;if(!k)return;const N=y.current.x*b.current.x,S=y.current.y*b.current.y;k.scrollBy(N,S)},[]),C=v.useMemo(()=>a===Ii.TreeOrder?[...c].reverse():c,[a,c]);v.useEffect(()=>{if(!o||!c.length||!_){p();return}for(const k of C){if(r?.(k)===!1)continue;const N=c.indexOf(k),S=d[N];if(!S)continue;const{direction:w,speed:E}=UC(k,S,_,t,f);for(const D of["x","y"])g[D][w[D]]||(E[D]=0,w[D]=0);if(E.x>0||E.y>0){p(),x.current=k,m(M,i),y.current=E,b.current=w;return}}y.current={x:0,y:0},b.current={x:0,y:0},p()},[t,M,r,p,o,i,JSON.stringify(_),JSON.stringify(g),m,c,C,d,JSON.stringify(f)])}const eS={x:{[Dt.Backward]:!1,[Dt.Forward]:!1},y:{[Dt.Backward]:!1,[Dt.Forward]:!1}};function tS(e){let{delta:t,disabled:n}=e;const r=Ni(t);return to(s=>{if(n||!r||!s)return eS;const o={x:Math.sign(t.x-r.x),y:Math.sign(t.y-r.y)};return{x:{[Dt.Backward]:s.x[Dt.Backward]||o.x===-1,[Dt.Forward]:s.x[Dt.Forward]||o.x===1},y:{[Dt.Backward]:s.y[Dt.Backward]||o.y===-1,[Dt.Forward]:s.y[Dt.Forward]||o.y===1}}},[n,t,r])}function nS(e,t){const n=t!=null?e.get(t):void 0,r=n?n.node.current:null;return to(s=>{var o;return t==null?null:(o=r??s)!=null?o:null},[r,t])}function rS(e,t){return v.useMemo(()=>e.reduce((n,r)=>{const{sensor:s}=r,o=s.activators.map(i=>({eventName:i.eventName,handler:t(i.handler,r)}));return[...n,...o]},[]),[e,t])}var lo;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(lo||(lo={}));var ic;(function(e){e.Optimized="optimized"})(ic||(ic={}));const zh=new Map;function sS(e,t){let{dragging:n,dependencies:r,config:s}=t;const[o,i]=v.useState(null),{frequency:a,measure:l,strategy:c}=s,d=v.useRef(e),u=y(),f=eo(u),g=v.useCallback(function(b){b===void 0&&(b=[]),!f.current&&i(_=>_===null?b:_.concat(b.filter(x=>!_.includes(x))))},[f]),m=v.useRef(null),p=to(b=>{if(u&&!n)return zh;if(!b||b===zh||d.current!==e||o!=null){const _=new Map;for(let x of e){if(!x)continue;if(o&&o.length>0&&!o.includes(x.id)&&x.rect.current){_.set(x.id,x.rect.current);continue}const M=x.node.current,C=M?new tc(l(M),M):null;x.rect.current=C,C&&_.set(x.id,C)}return _}return b},[e,o,n,u,l]);return v.useEffect(()=>{d.current=e},[e]),v.useEffect(()=>{u||g()},[n,u]),v.useEffect(()=>{o&&o.length>0&&i(null)},[JSON.stringify(o)]),v.useEffect(()=>{u||typeof a!="number"||m.current!==null||(m.current=setTimeout(()=>{g(),m.current=null},a))},[a,u,g,...r]),{droppableRects:p,measureDroppableContainers:g,measuringScheduled:o!=null};function y(){switch(c){case lo.Always:return!1;case lo.BeforeDragging:return n;default:return!n}}}function ac(e,t){return to(n=>e?n||(typeof t=="function"?t(e):e):null,[t,e])}function oS(e,t){return ac(e,t)}function iS(e){let{callback:t,disabled:n}=e;const r=Ei(t),s=v.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:o}=window;return new o(r)},[r,n]);return v.useEffect(()=>()=>s?.disconnect(),[s]),s}function Li(e){let{callback:t,disabled:n}=e;const r=Ei(t),s=v.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:o}=window;return new o(r)},[n]);return v.useEffect(()=>()=>s?.disconnect(),[s]),s}function aS(e){return new tc(fs(e),e)}function Vh(e,t,n){t===void 0&&(t=aS);const[r,s]=v.useState(null);function o(){s(l=>{if(!e)return null;if(e.isConnected===!1){var c;return(c=l??n)!=null?c:null}const d=t(e);return JSON.stringify(l)===JSON.stringify(d)?l:d})}const i=iS({callback(l){if(e)for(const c of l){const{type:d,target:u}=c;if(d==="childList"&&u instanceof HTMLElement&&u.contains(e)){o();break}}}}),a=Li({callback:o});return Mn(()=>{o(),e?(a?.observe(e),i?.observe(document.body,{childList:!0,subtree:!0})):(a?.disconnect(),i?.disconnect())},[e]),r}function lS(e){const t=ac(e);return Sh(e,t)}const Hh=[];function cS(e){const t=v.useRef(e),n=to(r=>e?r&&r!==Hh&&e&&t.current&&e.parentNode===t.current.parentNode?r:Di(e):Hh,[e]);return v.useEffect(()=>{t.current=e},[e]),n}function dS(e){const[t,n]=v.useState(null),r=v.useRef(e),s=v.useCallback(o=>{const i=Rl(o.target);i&&n(a=>a?(a.set(i,ec(i)),new Map(a)):null)},[]);return v.useEffect(()=>{const o=r.current;if(e!==o){i(o);const a=e.map(l=>{const c=Rl(l);return c?(c.addEventListener("scroll",s,{passive:!0}),[c,ec(c)]):null}).filter(l=>l!=null);n(a.length?new Map(a):null),r.current=e}return()=>{i(e),i(o)};function i(a){a.forEach(l=>{const c=Rl(l);c?.removeEventListener("scroll",s)})}},[s,e]),v.useMemo(()=>e.length?t?Array.from(t.values()).reduce((o,i)=>ds(o,i),Nn):Dh(e):Nn,[e,t])}function Uh(e,t){t===void 0&&(t=[]);const n=v.useRef(null);return v.useEffect(()=>{n.current=null},t),v.useEffect(()=>{const r=e!==Nn;r&&!n.current&&(n.current=e),!r&&n.current&&(n.current=null)},[e]),n.current?ro(e,n.current):Nn}function uS(e){v.useEffect(()=>{if(!_i)return;const t=e.map(n=>{let{sensor:r}=n;return r.setup==null?void 0:r.setup()});return()=>{for(const n of t)n?.()}},e.map(t=>{let{sensor:n}=t;return n}))}function fS(e,t){return v.useMemo(()=>e.reduce((n,r)=>{let{eventName:s,handler:o}=r;return n[s]=i=>{o(i,t)},n},{}),[e,t])}function Bh(e){return v.useMemo(()=>e?FC(e):null,[e])}const jh=[];function hS(e,t){t===void 0&&(t=fs);const[n]=e,r=Bh(n?Jt(n):null),[s,o]=v.useState(jh);function i(){o(()=>e.length?e.map(l=>Oh(l)?r:new tc(t(l),l)):jh)}const a=Li({callback:i});return Mn(()=>{a?.disconnect(),i(),e.forEach(l=>a?.observe(l))},[e]),s}function Wh(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return Rs(t)?t:e}function gS(e){let{measure:t}=e;const[n,r]=v.useState(null),s=v.useCallback(c=>{for(const{target:d}of c)if(Rs(d)){r(u=>{const f=t(d);return u?{...u,width:f.width,height:f.height}:f});break}},[t]),o=Li({callback:s}),i=v.useCallback(c=>{const d=Wh(c);o?.disconnect(),d&&o?.observe(d),r(d?t(d):null)},[t,o]),[a,l]=Mi(i);return v.useMemo(()=>({nodeRef:a,rect:n,setRef:l}),[n,a,l])}const pS=[{sensor:io,options:{}},{sensor:oo,options:{}}],mS={current:{}},Pi={draggable:{measure:Eh},droppable:{measure:Eh,strategy:lo.WhileDragging,frequency:ic.Optimized},dragOverlay:{measure:fs}};class co extends Map{get(t){var n;return t!=null&&(n=super.get(t))!=null?n:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:n}=t;return!n})}getNodeFor(t){var n,r;return(n=(r=this.get(t))==null?void 0:r.node.current)!=null?n:void 0}}const bS={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new co,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:Ai},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:Pi,measureDroppableContainers:Ai,windowRect:null,measuringScheduled:!1},Gh={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:Ai,draggableNodes:new Map,over:null,measureDroppableContainers:Ai},uo=v.createContext(Gh),Yh=v.createContext(bS);function yS(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new co}}}function vS(e,t){switch(t.type){case Nt.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case Nt.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case Nt.DragEnd:case Nt.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case Nt.RegisterDroppable:{const{element:n}=t,{id:r}=n,s=new co(e.droppable.containers);return s.set(r,n),{...e,droppable:{...e.droppable,containers:s}}}case Nt.SetDroppableDisabled:{const{id:n,key:r,disabled:s}=t,o=e.droppable.containers.get(n);if(!o||r!==o.key)return e;const i=new co(e.droppable.containers);return i.set(n,{...o,disabled:s}),{...e,droppable:{...e.droppable,containers:i}}}case Nt.UnregisterDroppable:{const{id:n,key:r}=t,s=e.droppable.containers.get(n);if(!s||r!==s.key)return e;const o=new co(e.droppable.containers);return o.delete(n),{...e,droppable:{...e.droppable,containers:o}}}default:return e}}function xS(e){let{disabled:t}=e;const{active:n,activatorEvent:r,draggableNodes:s}=v.useContext(uo),o=Ni(r),i=Ni(n?.id);return v.useEffect(()=>{if(!t&&!r&&o&&i!=null){if(!Ti(o)||document.activeElement===o.target)return;const a=s.get(i);if(!a)return;const{activatorNode:l,node:c}=a;if(!l.current&&!c.current)return;requestAnimationFrame(()=>{for(const d of[l.current,c.current]){if(!d)continue;const u=bC(d);if(u){u.focus();break}}})}},[r,t,s,i,o]),null}function Xh(e,t){let{transform:n,...r}=t;return e!=null&&e.length?e.reduce((s,o)=>o({transform:s,...r}),n):n}function wS(e){return v.useMemo(()=>({draggable:{...Pi.draggable,...e?.draggable},droppable:{...Pi.droppable,...e?.droppable},dragOverlay:{...Pi.dragOverlay,...e?.dragOverlay}}),[e?.draggable,e?.droppable,e?.dragOverlay])}function kS(e){let{activeNode:t,measure:n,initialRect:r,config:s=!0}=e;const o=v.useRef(!1),{x:i,y:a}=typeof s=="boolean"?{x:s,y:s}:s;Mn(()=>{if(!i&&!a||!t){o.current=!1;return}if(o.current||!r)return;const c=t?.node.current;if(!c||c.isConnected===!1)return;const d=n(c),u=Sh(d,r);if(i||(u.x=0),a||(u.y=0),o.current=!0,Math.abs(u.x)>0||Math.abs(u.y)>0){const f=Mh(c);f&&f.scrollBy({top:u.y,left:u.x})}},[t,i,a,r,n])}const $i=v.createContext({...Nn,scaleX:1,scaleY:1});var ur;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(ur||(ur={}));const lc=v.memo(function(t){var n,r,s,o;let{id:i,accessibility:a,autoScroll:l=!0,children:c,sensors:d=pS,collisionDetection:u=AC,measuring:f,modifiers:g,...m}=t;const p=v.useReducer(vS,void 0,yS),[y,b]=p,[_,x]=CC(),[M,C]=v.useState(ur.Uninitialized),k=M===ur.Initialized,{draggable:{active:N,nodes:S,translate:w},droppable:{containers:E}}=y,D=N!=null?S.get(N):null,P=v.useRef({initial:null,translated:null}),L=v.useMemo(()=>{var Fe;return N!=null?{id:N,data:(Fe=D?.data)!=null?Fe:mS,rect:P}:null},[N,D]),$=v.useRef(null),[A,O]=v.useState(null),[T,z]=v.useState(null),H=eo(m,Object.values(m)),j=no("DndDescribedBy",i),G=v.useMemo(()=>E.getEnabled(),[E]),U=wS(f),{droppableRects:W,measureDroppableContainers:Y,measuringScheduled:V}=sS(G,{dragging:k,dependencies:[w.x,w.y],config:U.droppable}),X=nS(S,N),K=v.useMemo(()=>T?Oi(T):null,[T]),q=Ze(),ne=oS(X,U.draggable.measure);kS({activeNode:N!=null?S.get(N):null,config:q.layoutShiftCompensation,initialRect:ne,measure:U.draggable.measure});const ee=Vh(X,U.draggable.measure,ne),ue=Vh(X?X.parentElement:null),J=v.useRef({activatorEvent:null,active:null,activeNode:X,collisionRect:null,collisions:null,droppableRects:W,draggableNodes:S,draggingNode:null,draggingNodeRect:null,droppableContainers:E,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),ce=E.getNodeFor((n=J.current.over)==null?void 0:n.id),me=gS({measure:U.dragOverlay.measure}),le=(r=me.nodeRef.current)!=null?r:X,ve=k?(s=me.rect)!=null?s:ee:null,ge=!!(me.nodeRef.current&&me.rect),pe=lS(ge?null:ee),ke=Bh(le?Jt(le):null),$e=cS(k?ce??X:null),ie=hS($e),Me=Xh(g,{transform:{x:w.x-pe.x,y:w.y-pe.y,scaleX:1,scaleY:1},activatorEvent:T,active:L,activeNodeRect:ee,containerNodeRect:ue,draggingNodeRect:ve,over:J.current.over,overlayNodeRect:me.rect,scrollableAncestors:$e,scrollableAncestorRects:ie,windowRect:ke}),Ae=K?ds(K,w):null,Ie=dS($e),Xe=Uh(Ie),gt=Uh(Ie,[ee]),pt=ds(Me,Xe),Be=ve?LC(ve,Me):null,et=L&&Be?u({active:L,collisionRect:Be,droppableRects:W,droppableContainers:G,pointerCoordinates:Ae}):null,je=kh(et,"id"),[re,fe]=v.useState(null),be=ge?Me:ds(Me,gt),we=DC(be,(o=re?.rect)!=null?o:null,ee),xe=v.useRef(null),He=v.useCallback((Fe,Ne)=>{let{sensor:ze,options:Ke}=Ne;if($.current==null)return;const tt=S.get($.current);if(!tt)return;const at=Fe.nativeEvent,ct=new ze({active:$.current,activeNode:tt,event:at,options:Ke,context:J,onAbort(yt){if(!S.get(yt))return;const{onDragAbort:Ct}=H.current,Ut={id:yt};Ct?.(Ut),_({type:"onDragAbort",event:Ut})},onPending(yt,Ue,Ct,Ut){if(!S.get(yt))return;const{onDragPending:kn}=H.current,qt={id:yt,constraint:Ue,initialCoordinates:Ct,offset:Ut};kn?.(qt),_({type:"onDragPending",event:qt})},onStart(yt){const Ue=$.current;if(Ue==null)return;const Ct=S.get(Ue);if(!Ct)return;const{onDragStart:Ut}=H.current,st={activatorEvent:at,active:{id:Ue,data:Ct.data,rect:P}};$n.unstable_batchedUpdates(()=>{Ut?.(st),C(ur.Initializing),b({type:Nt.DragStart,initialCoordinates:yt,active:Ue}),_({type:"onDragStart",event:st}),O(xe.current),z(at)})},onMove(yt){b({type:Nt.DragMove,coordinates:yt})},onEnd:wn(Nt.DragEnd),onCancel:wn(Nt.DragCancel)});xe.current=ct;function wn(yt){return async function(){const{active:Ct,collisions:Ut,over:st,scrollAdjustedTranslate:kn}=J.current;let qt=null;if(Ct&&kn){const{cancelDrop:Ln}=H.current;qt={activatorEvent:at,active:Ct,collisions:Ut,delta:kn,over:st},yt===Nt.DragEnd&&typeof Ln=="function"&&await Promise.resolve(Ln(qt))&&(yt=Nt.DragCancel)}$.current=null,$n.unstable_batchedUpdates(()=>{b({type:yt}),C(ur.Uninitialized),fe(null),O(null),z(null),xe.current=null;const Ln=yt===Nt.DragEnd?"onDragEnd":"onDragCancel";if(qt){const jr=H.current[Ln];jr?.(qt),_({type:Ln,event:qt})}})}}},[S]),_e=v.useCallback((Fe,Ne)=>(ze,Ke)=>{const tt=ze.nativeEvent,at=S.get(Ke);if($.current!==null||!at||tt.dndKit||tt.defaultPrevented)return;const ct={active:at};Fe(ze,Ne.options,ct)===!0&&(tt.dndKit={capturedBy:Ne.sensor},$.current=Ke,He(ze,Ne))},[S,He]),Le=rS(d,_e);uS(d),Mn(()=>{ee&&M===ur.Initializing&&C(ur.Initialized)},[ee,M]),v.useEffect(()=>{const{onDragMove:Fe}=H.current,{active:Ne,activatorEvent:ze,collisions:Ke,over:tt}=J.current;if(!Ne||!ze)return;const at={active:Ne,activatorEvent:ze,collisions:Ke,delta:{x:pt.x,y:pt.y},over:tt};$n.unstable_batchedUpdates(()=>{Fe?.(at),_({type:"onDragMove",event:at})})},[pt.x,pt.y]),v.useEffect(()=>{const{active:Fe,activatorEvent:Ne,collisions:ze,droppableContainers:Ke,scrollAdjustedTranslate:tt}=J.current;if(!Fe||$.current==null||!Ne||!tt)return;const{onDragOver:at}=H.current,ct=Ke.get(je),wn=ct&&ct.rect.current?{id:ct.id,rect:ct.rect.current,data:ct.data,disabled:ct.disabled}:null,yt={active:Fe,activatorEvent:Ne,collisions:ze,delta:{x:tt.x,y:tt.y},over:wn};$n.unstable_batchedUpdates(()=>{fe(wn),at?.(yt),_({type:"onDragOver",event:yt})})},[je]),Mn(()=>{J.current={activatorEvent:T,active:L,activeNode:X,collisionRect:Be,collisions:et,droppableRects:W,draggableNodes:S,draggingNode:le,draggingNodeRect:ve,droppableContainers:E,over:re,scrollableAncestors:$e,scrollAdjustedTranslate:pt},P.current={initial:ve,translated:Be}},[L,X,et,Be,S,le,ve,W,E,re,$e,pt]),RC({...q,delta:w,draggingRect:Be,pointerCoordinates:Ae,scrollableAncestors:$e,scrollableAncestorRects:ie});const Ce=v.useMemo(()=>({active:L,activeNode:X,activeNodeRect:ee,activatorEvent:T,collisions:et,containerNodeRect:ue,dragOverlay:me,draggableNodes:S,droppableContainers:E,droppableRects:W,over:re,measureDroppableContainers:Y,scrollableAncestors:$e,scrollableAncestorRects:ie,measuringConfiguration:U,measuringScheduled:V,windowRect:ke}),[L,X,ee,T,et,ue,me,S,E,W,re,Y,$e,ie,U,V,ke]),We=v.useMemo(()=>({activatorEvent:T,activators:Le,active:L,activeNodeRect:ee,ariaDescribedById:{draggable:j},dispatch:b,draggableNodes:S,over:re,measureDroppableContainers:Y}),[T,Le,L,ee,b,j,S,re,Y]);return v.createElement(yh.Provider,{value:x},v.createElement(uo.Provider,{value:We},v.createElement(Yh.Provider,{value:Ce},v.createElement($i.Provider,{value:we},c)),v.createElement(xS,{disabled:a?.restoreFocus===!1})),v.createElement(EC,{...a,hiddenTextDescribedById:j}));function Ze(){const Fe=A?.autoScrollEnabled===!1,Ne=typeof l=="object"?l.enabled===!1:l===!1,ze=k&&!Fe&&!Ne;return typeof l=="object"?{...l,enabled:ze}:{enabled:ze}}}),CS=v.createContext(null),Kh="button",SS="Draggable";function _S(e){let{id:t,data:n,disabled:r=!1,attributes:s}=e;const o=no(SS),{activators:i,activatorEvent:a,active:l,activeNodeRect:c,ariaDescribedById:d,draggableNodes:u,over:f}=v.useContext(uo),{role:g=Kh,roleDescription:m="draggable",tabIndex:p=0}=s??{},y=l?.id===t,b=v.useContext(y?$i:CS),[_,x]=Mi(),[M,C]=Mi(),k=fS(i,t),N=eo(n);Mn(()=>(u.set(t,{id:t,key:o,node:_,activatorNode:M,data:N}),()=>{const w=u.get(t);w&&w.key===o&&u.delete(t)}),[u,t]);const S=v.useMemo(()=>({role:g,tabIndex:p,"aria-disabled":r,"aria-pressed":y&&g===Kh?!0:void 0,"aria-roledescription":m,"aria-describedby":d.draggable}),[r,g,p,y,m,d.draggable]);return{active:l,activatorEvent:a,activeNodeRect:c,attributes:S,isDragging:y,listeners:r?void 0:k,node:_,over:f,setNodeRef:x,setActivatorNodeRef:C,transform:b}}function qh(){return v.useContext(Yh)}const ES="Droppable",MS={timeout:25};function NS(e){let{data:t,disabled:n=!1,id:r,resizeObserverConfig:s}=e;const o=no(ES),{active:i,dispatch:a,over:l,measureDroppableContainers:c}=v.useContext(uo),d=v.useRef({disabled:n}),u=v.useRef(!1),f=v.useRef(null),g=v.useRef(null),{disabled:m,updateMeasurementsFor:p,timeout:y}={...MS,...s},b=eo(p??r),_=v.useCallback(()=>{if(!u.current){u.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{c(Array.isArray(b.current)?b.current:[b.current]),g.current=null},y)},[y]),x=Li({callback:_,disabled:m||!i}),M=v.useCallback((S,w)=>{x&&(w&&(x.unobserve(w),u.current=!1),S&&x.observe(S))},[x]),[C,k]=Mi(M),N=eo(t);return v.useEffect(()=>{!x||!C.current||(x.disconnect(),u.current=!1,x.observe(C.current))},[C,x]),v.useEffect(()=>(a({type:Nt.RegisterDroppable,element:{id:r,key:o,disabled:n,node:C,rect:f,data:N}}),()=>a({type:Nt.UnregisterDroppable,key:o,id:r})),[r]),v.useEffect(()=>{n!==d.current.disabled&&(a({type:Nt.SetDroppableDisabled,id:r,key:o,disabled:n}),d.current.disabled=n)},[r,o,n,a]),{active:i,rect:f,isOver:l?.id===r,node:C,over:l,setNodeRef:k}}function TS(e){let{animation:t,children:n}=e;const[r,s]=v.useState(null),[o,i]=v.useState(null),a=Ni(n);return!n&&!r&&a&&s(a),Mn(()=>{if(!o)return;const l=r?.key,c=r?.props.id;if(l==null||c==null){s(null);return}Promise.resolve(t(c,o)).then(()=>{s(null)})},[t,r,o]),v.createElement(v.Fragment,null,n,r?v.cloneElement(r,{ref:i}):null)}const OS={x:0,y:0,scaleX:1,scaleY:1};function AS(e){let{children:t}=e;return v.createElement(uo.Provider,{value:Gh},v.createElement($i.Provider,{value:OS},t))}const DS={position:"fixed",touchAction:"none"},IS=e=>Ti(e)?"transform 250ms ease":void 0,LS=v.forwardRef((e,t)=>{let{as:n,activatorEvent:r,adjustScale:s,children:o,className:i,rect:a,style:l,transform:c,transition:d=IS}=e;if(!a)return null;const u=s?c:{...c,scaleX:1,scaleY:1},f={...DS,width:a.width,height:a.height,top:a.top,left:a.left,transform:jn.Transform.toString(u),transformOrigin:s&&r?MC(r,a):void 0,transition:typeof d=="function"?d(r):d,...l};return v.createElement(n,{className:i,style:f,ref:t},o)}),PS={duration:250,easing:"ease",keyframes:e=>{let{transform:{initial:t,final:n}}=e;return[{transform:jn.Transform.toString(t)},{transform:jn.Transform.toString(n)}]},sideEffects:(e=>t=>{let{active:n,dragOverlay:r}=t;const s={},{styles:o,className:i}=e;if(o!=null&&o.active)for(const[a,l]of Object.entries(o.active))l!==void 0&&(s[a]=n.node.style.getPropertyValue(a),n.node.style.setProperty(a,l));if(o!=null&&o.dragOverlay)for(const[a,l]of Object.entries(o.dragOverlay))l!==void 0&&r.node.style.setProperty(a,l);return i!=null&&i.active&&n.node.classList.add(i.active),i!=null&&i.dragOverlay&&r.node.classList.add(i.dragOverlay),function(){for(const[l,c]of Object.entries(s))n.node.style.setProperty(l,c);i!=null&&i.active&&n.node.classList.remove(i.active)}})({styles:{active:{opacity:"0"}}})};function $S(e){let{config:t,draggableNodes:n,droppableContainers:r,measuringConfiguration:s}=e;return Ei((o,i)=>{if(t===null)return;const a=n.get(o);if(!a)return;const l=a.node.current;if(!l)return;const c=Wh(i);if(!c)return;const{transform:d}=Jt(i).getComputedStyle(i),u=_h(d);if(!u)return;const f=typeof t=="function"?t:FS(t);return Ih(l,s.draggable.measure),f({active:{id:o,data:a.data,node:l,rect:s.draggable.measure(l)},draggableNodes:n,dragOverlay:{node:i,rect:s.dragOverlay.measure(c)},droppableContainers:r,measuringConfiguration:s,transform:u})})}function FS(e){const{duration:t,easing:n,sideEffects:r,keyframes:s}={...PS,...e};return o=>{let{active:i,dragOverlay:a,transform:l,...c}=o;if(!t)return;const d={x:a.rect.left-i.rect.left,y:a.rect.top-i.rect.top},u={scaleX:l.scaleX!==1?i.rect.width*l.scaleX/a.rect.width:1,scaleY:l.scaleY!==1?i.rect.height*l.scaleY/a.rect.height:1},f={x:l.x-d.x,y:l.y-d.y,...u},g=s({...c,active:i,dragOverlay:a,transform:{initial:l,final:f}}),[m]=g,p=g[g.length-1];if(JSON.stringify(m)===JSON.stringify(p))return;const y=r?.({active:i,dragOverlay:a,...c}),b=a.node.animate(g,{duration:t,easing:n,fill:"forwards"});return new Promise(_=>{b.onfinish=()=>{y?.(),_()}})}}let Zh=0;function zS(e){return v.useMemo(()=>{if(e!=null)return Zh++,Zh},[e])}const VS=v.memo(e=>{let{adjustScale:t=!1,children:n,dropAnimation:r,style:s,transition:o,modifiers:i,wrapperElement:a="div",className:l,zIndex:c=999}=e;const{activatorEvent:d,active:u,activeNodeRect:f,containerNodeRect:g,draggableNodes:m,droppableContainers:p,dragOverlay:y,over:b,measuringConfiguration:_,scrollableAncestors:x,scrollableAncestorRects:M,windowRect:C}=qh(),k=v.useContext($i),N=zS(u?.id),S=Xh(i,{activatorEvent:d,active:u,activeNodeRect:f,containerNodeRect:g,draggingNodeRect:y.rect,over:b,overlayNodeRect:y.rect,scrollableAncestors:x,scrollableAncestorRects:M,transform:k,windowRect:C}),w=ac(f),E=$S({config:r,draggableNodes:m,droppableContainers:p,measuringConfiguration:_}),D=w?y.setRef:void 0;return v.createElement(AS,null,v.createElement(TS,{animation:E},u&&N?v.createElement(LS,{key:N,id:u.id,ref:D,as:a,activatorEvent:d,adjustScale:t,className:l,transition:o,rect:w,style:{zIndex:c,...s},transform:S},n):null))});function hs(e,t,n){const r=e.slice();return r.splice(n<0?r.length+n:n,0,r.splice(t,1)[0]),r}function HS(e,t){return e.reduce((n,r,s)=>{const o=t.get(r);return o&&(n[s]=o),n},Array(e.length))}function Fi(e){return e!==null&&e>=0}function US(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function BS(e){return typeof e=="boolean"?{draggable:e,droppable:e}:e}const zi=e=>{let{rects:t,activeIndex:n,overIndex:r,index:s}=e;const o=hs(t,r,n),i=t[s],a=o[s];return!a||!i?null:{x:a.left-i.left,y:a.top-i.top,scaleX:a.width/i.width,scaleY:a.height/i.height}},Vi={scaleX:1,scaleY:1},cc=e=>{var t;let{activeIndex:n,activeNodeRect:r,index:s,rects:o,overIndex:i}=e;const a=(t=o[n])!=null?t:r;if(!a)return null;if(s===n){const c=o[i];return c?{x:0,y:n<i?c.top+c.height-(a.top+a.height):c.top-a.top,...Vi}:null}const l=jS(o,s,n);return s>n&&s<=i?{x:0,y:-a.height-l,...Vi}:s<n&&s>=i?{x:0,y:a.height+l,...Vi}:{x:0,y:0,...Vi}};function jS(e,t,n){const r=e[t],s=e[t-1],o=e[t+1];return r?n<t?s?r.top-(s.top+s.height):o?o.top-(r.top+r.height):0:o?o.top-(r.top+r.height):s?r.top-(s.top+s.height):0:0}const Qh="Sortable",Jh=v.createContext({activeIndex:-1,containerId:Qh,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:zi,disabled:{draggable:!1,droppable:!1}});function Hi(e){let{children:t,id:n,items:r,strategy:s=zi,disabled:o=!1}=e;const{active:i,dragOverlay:a,droppableRects:l,over:c,measureDroppableContainers:d}=qh(),u=no(Qh,n),f=a.rect!==null,g=v.useMemo(()=>r.map(k=>typeof k=="object"&&"id"in k?k.id:k),[r]),m=i!=null,p=i?g.indexOf(i.id):-1,y=c?g.indexOf(c.id):-1,b=v.useRef(g),_=!US(g,b.current),x=y!==-1&&p===-1||_,M=BS(o);Mn(()=>{_&&m&&d(g)},[_,g,m,d]),v.useEffect(()=>{b.current=g},[g]);const C=v.useMemo(()=>({activeIndex:p,containerId:u,disabled:M,disableTransforms:x,items:g,overIndex:y,useDragOverlay:f,sortedRects:HS(g,l),strategy:s}),[p,u,M.draggable,M.droppable,x,g,y,l,f,s]);return v.createElement(Jh.Provider,{value:C},t)}const Rh=e=>{let{id:t,items:n,activeIndex:r,overIndex:s}=e;return hs(n,r,s).indexOf(t)},eg=e=>{let{containerId:t,isSorting:n,wasDragging:r,index:s,items:o,newIndex:i,previousItems:a,previousContainerId:l,transition:c}=e;return!c||!r||a!==o&&s===i?!1:n?!0:i!==s&&t===l},WS={duration:200,easing:"ease"},tg="transform",GS=jn.Transition.toString({property:tg,duration:0,easing:"linear"}),YS={roleDescription:"sortable"};function XS(e){let{disabled:t,index:n,node:r,rect:s}=e;const[o,i]=v.useState(null),a=v.useRef(n);return Mn(()=>{if(!t&&n!==a.current&&r.current){const l=s.current;if(l){const c=fs(r.current,{ignoreTransform:!0}),d={x:l.left-c.left,y:l.top-c.top,scaleX:l.width/c.width,scaleY:l.height/c.height};(d.x||d.y)&&i(d)}}n!==a.current&&(a.current=n)},[t,n,r,s]),v.useEffect(()=>{o&&i(null)},[o]),o}function Ui(e){let{animateLayoutChanges:t=eg,attributes:n,disabled:r,data:s,getNewIndex:o=Rh,id:i,strategy:a,resizeObserverConfig:l,transition:c=WS}=e;const{items:d,containerId:u,activeIndex:f,disabled:g,disableTransforms:m,sortedRects:p,overIndex:y,useDragOverlay:b,strategy:_}=v.useContext(Jh),x=KS(r,g),M=d.indexOf(i),C=v.useMemo(()=>({sortable:{containerId:u,index:M,items:d},...s}),[u,s,M,d]),k=v.useMemo(()=>d.slice(d.indexOf(i)),[d,i]),{rect:N,node:S,isOver:w,setNodeRef:E}=NS({id:i,data:C,disabled:x.droppable,resizeObserverConfig:{updateMeasurementsFor:k,...l}}),{active:D,activatorEvent:P,activeNodeRect:L,attributes:$,setNodeRef:A,listeners:O,isDragging:T,over:z,setActivatorNodeRef:H,transform:j}=_S({id:i,data:C,attributes:{...YS,...n},disabled:x.draggable}),G=hC(E,A),U=!!D,W=U&&!m&&Fi(f)&&Fi(y),Y=!b&&T,V=Y&&W?j:null,K=W?V??(a??_)({rects:p,activeNodeRect:L,activeIndex:f,overIndex:y,index:M}):null,q=Fi(f)&&Fi(y)?o({id:i,items:d,activeIndex:f,overIndex:y}):M,ne=D?.id,ee=v.useRef({activeId:ne,items:d,newIndex:q,containerId:u}),ue=d!==ee.current.items,J=t({active:D,containerId:u,isDragging:T,isSorting:U,id:i,index:M,items:d,newIndex:ee.current.newIndex,previousItems:ee.current.items,previousContainerId:ee.current.containerId,transition:c,wasDragging:ee.current.activeId!=null}),ce=XS({disabled:!J,index:M,node:S,rect:N});return v.useEffect(()=>{U&&ee.current.newIndex!==q&&(ee.current.newIndex=q),u!==ee.current.containerId&&(ee.current.containerId=u),d!==ee.current.items&&(ee.current.items=d)},[U,q,u,d]),v.useEffect(()=>{if(ne===ee.current.activeId)return;if(ne!=null&&ee.current.activeId==null){ee.current.activeId=ne;return}const le=setTimeout(()=>{ee.current.activeId=ne},50);return()=>clearTimeout(le)},[ne]),{active:D,activeIndex:f,attributes:$,data:C,rect:N,index:M,newIndex:q,items:d,isOver:w,isSorting:U,isDragging:T,listeners:O,node:S,overIndex:y,over:z,setNodeRef:G,setActivatorNodeRef:H,setDroppableNodeRef:E,setDraggableNodeRef:A,transform:ce??K,transition:me()};function me(){if(ce||ue&&ee.current.newIndex===M)return GS;if(!(Y&&!Ti(P)||!c)&&(U||J))return jn.Transition.toString({...c,property:tg})}}function KS(e,t){var n,r;return typeof e=="boolean"?{draggable:e,droppable:!1}:{draggable:(n=e?.draggable)!=null?n:t.draggable,droppable:(r=e?.droppable)!=null?r:t.droppable}}function fo(e){if(!e)return!1;const t=e.data.current;return!!(t&&"sortable"in t&&typeof t.sortable=="object"&&"containerId"in t.sortable&&"items"in t.sortable&&"index"in t.sortable)}const qS=[qe.Down,qe.Right,qe.Up,qe.Left],Bi=(e,t)=>{let{context:{active:n,collisionRect:r,droppableRects:s,droppableContainers:o,over:i,scrollableAncestors:a}}=t;if(qS.includes(e.code)){if(e.preventDefault(),!n||!r)return;const l=[];o.getEnabled().forEach(u=>{if(!u||u!=null&&u.disabled)return;const f=s.get(u.id);if(f)switch(e.code){case qe.Down:r.top<f.top&&l.push(u);break;case qe.Up:r.top>f.top&&l.push(u);break;case qe.Left:r.left>f.left&&l.push(u);break;case qe.Right:r.left<f.left&&l.push(u);break}});const c=TC({collisionRect:r,droppableRects:s,droppableContainers:l});let d=kh(c,"id");if(d===i?.id&&c.length>1&&(d=c[1].id),d!=null){const u=o.get(n.id),f=o.get(d),g=f?s.get(f.id):null,m=f?.node.current;if(m&&g&&u&&f){const y=Di(m).some((k,N)=>a[N]!==k),b=ng(u,f),_=ZS(u,f),x=y||!b?{x:0,y:0}:{x:_?r.width-g.width:0,y:_?r.height-g.height:0},M={x:g.left,y:g.top};return x.x&&x.y?M:ro(M,x)}}}};function ng(e,t){return!fo(e)||!fo(t)?!1:e.data.current.sortable.containerId===t.data.current.sortable.containerId}function ZS(e,t){return!fo(e)||!fo(t)||!ng(e,t)?!1:e.data.current.sortable.index<t.data.current.sortable.index}const QS=Object.freeze(Object.defineProperty({__proto__:null,SortableContext:Hi,arrayMove:hs,defaultAnimateLayoutChanges:eg,defaultNewIndexGetter:Rh,hasSortableData:fo,rectSortingStrategy:zi,sortableKeyboardCoordinates:Bi,useSortable:Ui,verticalListSortingStrategy:cc},Symbol.toStringTag,{value:"Module"})),JS=()=>window.G7Core,rg=({item:e,isSelected:t=!1,isExpanded:n=!1,level:r=0,onClick:s,onToggle:o,onExpandToggle:i,className:a="",selectedClassName:l="bg-blue-50 dark:bg-blue-900/30 border-blue-500",defaultClassName:c="bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600",dragHandleClassName:d="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300",iconContainerClassName:u="bg-gray-100 dark:bg-gray-700",contentClassName:f="",enableDrag:g=!0,toggleDisabled:m=!1})=>{const{attributes:p,listeners:y,setNodeRef:b,transform:_,transition:x,isDragging:M}=Ui({id:e.id}),C={transform:jn.Transform.toString(_),transition:x,opacity:M?.5:1},k=r*24+12,S=`flex items-center gap-2 cursor-pointer transition-colors ${t?l:c}`,w=JS()?.style?.mergeClasses?.(S,a)??`${S} ${a}`.trim();return h.jsxs(I,{ref:b,style:C,className:w,onClick:s,children:[g&&h.jsx(I,{...p,...y,className:`flex-shrink-0 cursor-grab active:cursor-grabbing p-1 ${d}`,style:{paddingLeft:`${k}px`},children:h.jsx(oe,{name:"grip-vertical",className:"w-4 h-4"})}),!g&&r>0&&h.jsx(I,{style:{paddingLeft:`${k}px`}}),g&&h.jsx(h.Fragment,{children:e.hasChildren?h.jsx(ae,{type:"button",onClick:E=>{E.stopPropagation(),i?.()},className:`flex-shrink-0 p-1 ${d}`,children:h.jsx(oe,{name:n?"chevron-down":"chevron-right",className:"w-4 h-4"})}):h.jsx(I,{className:"flex-shrink-0 w-6"})}),h.jsx(I,{className:`flex-shrink-0 w-8 h-8 flex items-center justify-center rounded ${u}`,children:h.jsx(oe,{name:e.icon||"file",className:"w-4 h-4 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:`flex-1 min-w-0 ${f}`,children:[h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-900 dark:text-white truncate",children:e.name}),e.isModuleMenu&&h.jsx(oe,{name:"cube",className:"w-2 h-2 text-gray-300 dark:text-gray-500 ml-2",title:e.moduleName})]}),h.jsx(Q,{className:"text-xs text-gray-500 dark:text-gray-400 truncate block",children:e.url})]}),h.jsx(I,{className:"flex-shrink-0",onClick:E=>E.stopPropagation(),children:h.jsx(Qa,{checked:e.isActive,onChange:()=>o?.(new MouseEvent("click")),size:"sm",disabled:m})})]})},RS=()=>window.G7Core;function sg(e,t,n=0,r=null){const s=[];for(const o of e)s.push({item:o,depth:n,parentId:r}),o.children?.length>0&&t.has(o.id)&&s.push(...sg(o.children,t,n+1,o.id));return s}function dc(e,t){for(const n of e){if(n.id===t)return n;if(n.children){const r=dc(n.children,t);if(r)return r}}return null}function og(e){const t=[];if(e.children)for(const n of e.children)t.push(n.id),t.push(...og(n));return t}function ig(e,t){return e.filter(n=>n.id!==t).map(n=>n.children?.length>0?{...n,children:ig(n.children,t)}:n)}function ag(e,t,n,r){if(n===null){const s=[...e];return s.splice(r,0,t),s}return e.map(s=>{if(s.id===n){const o=[...s.children||[]];return o.splice(r,0,t),{...s,children:o}}return s.children?.length>0?{...s,children:ag(s.children,t,n,r)}:s})}function lg(e,t,n,r){if(t===null){const s=e.findIndex(i=>i.id===n),o=e.findIndex(i=>i.id===r);return s===-1||o===-1?e:hs(e,s,o)}return e.map(s=>{if(s.id===t&&s.children){const o=s.children.findIndex(a=>a.id===n),i=s.children.findIndex(a=>a.id===r);return o===-1||i===-1?s:{...s,children:hs(s.children,o,i)}}return s.children?.length>0?{...s,children:lg(s.children,t,n,r)}:s})}function cg(e){const t=e.map((s,o)=>({id:s.id,order:o+1})),n={},r=s=>{s.forEach(o=>{o.children?.length>0&&(n[o.id]=o.children.map((i,a)=>({id:i.id,order:a+1})),r(o.children))})};return r(e),{parent_menus:t,child_menus:n}}const e_=({items:e=[],selectedId:t,onSelect:n,onOrderChange:r,onToggleStatus:s,title:o,className:i="",listClassName:a="",childrenContainerClassName:l="bg-gray-50 dark:bg-gray-900/50",itemClassName:c="py-2.5 px-3 mx-4 my-2 rounded-lg border",itemSelectedClassName:d,itemDefaultClassName:u,titleClassName:f="px-4 py-3 border-b border-gray-200 dark:border-gray-700",enableDrag:g=!0,toggleDisabled:m=!1,id:p,editorAttrs:y})=>{const[b,_]=v.useState(new Set),[x,M]=v.useState(null),[C,k]=v.useState(e),N=v.useRef(!1),S=v.useRef(""),w=v.useRef(""),E=v.useCallback(W=>JSON.stringify(W),[]),D=v.useCallback(W=>W.map(Y=>{const V=Y.children?.map(X=>X.id).join(",")||"";return`${Y.id}:${V}`}).join("|"),[]);v.useEffect(()=>{const W=D(e),Y=E(e);if(N.current){const V=D(C);(W===V||W!==S.current)&&(N.current=!1,S.current=W,w.current=Y,(W!==V||Y!==E(C))&&k(e));return}Y!==w.current&&(S.current=W,w.current=Y,k(e))},[e,D,E,C]);const P=Ql(us(io,{activationConstraint:{distance:8}}),us(oo,{coordinateGetter:Bi})),L=v.useCallback(W=>{if(typeof W=="string")return W;const V=RS()?.locale?.current?.()||"ko";return W[V]||W.en||Object.values(W)[0]||""},[]),$=v.useCallback(W=>{_(Y=>{const V=new Set(Y);return V.has(W)?V.delete(W):V.add(W),V})},[]),A=v.useCallback(W=>{n?.(W)},[n]),O=v.useCallback((W,Y,V)=>{V.stopPropagation(),s?.(W,Y)},[s]),T=W=>{M(W.active.id)},z=W=>{const{active:Y,over:V}=W;if(M(null),!V||Y.id===V.id)return;const X=j.find(q=>q.item.id===Number(Y.id)),K=j.find(q=>q.item.id===Number(V.id));if(!(!X||!K))if(X.parentId===K.parentId){const q=lg(C,X.parentId,Number(Y.id),Number(V.id));k(q),N.current=!0,r?.(cg(q))}else{const q=X.item;if(og(q).includes(Number(V.id)))return;let ee=ig(C,q.id);const ue=j.findIndex(pe=>pe.item.id===Number(Y.id)),J=j.findIndex(pe=>pe.item.id===Number(V.id)),ce=ue>J,me=K.parentId;let le;if(me===null)le=ee.findIndex(pe=>pe.id===Number(V.id)),le===-1?le=ee.length:ce||(le+=1);else{const ke=dc(ee,me)?.children||[];le=ke.findIndex($e=>$e.id===Number(V.id)),le===-1?le=ke.length:ce||(le+=1)}const ve={...q,parent_id:me,children:q.children||[]};ee=ag(ee,ve,me,le),k(ee),N.current=!0;const ge=cg(ee);ge.moved_items=[{id:q.id,new_parent_id:me}],r?.(ge)}},H=W=>({id:W.id,name:L(W.name),slug:W.slug,url:W.url,icon:W.icon,isActive:W.is_active,isModuleMenu:!!W.module_id,moduleName:W.module?L(W.module.name):void 0,hasChildren:W.children&&W.children.length>0}),j=v.useMemo(()=>sg(C,b),[C,b]),G=v.useMemo(()=>j.map(W=>W.item.id),[j]),U=x?dc(C,Number(x)):null;return h.jsxs(I,{className:i,id:p,...y,children:[o&&h.jsx(I,{className:f,children:h.jsx(Gr,{className:"text-sm font-medium text-gray-900 dark:text-white",children:o})}),h.jsxs(lc,{sensors:P,collisionDetection:Jl,onDragStart:T,onDragEnd:z,children:[h.jsx(Hi,{items:G,strategy:cc,children:h.jsx(I,{className:a,children:j.map(W=>h.jsx(I,{className:W.depth>0?l:"",children:h.jsx(rg,{item:H(W.item),isSelected:t===W.item.id,isExpanded:b.has(W.item.id),level:W.depth,onClick:()=>A(W.item),onToggle:Y=>O(W.item.id,W.item.is_active,Y),onExpandToggle:()=>$(W.item.id),className:c,selectedClassName:d,defaultClassName:u,enableDrag:g,toggleDisabled:m})},W.item.id))})}),h.jsx(VS,{children:U?h.jsx(I,{className:"bg-white dark:bg-gray-800 shadow-lg rounded-lg border border-blue-500 p-3",children:h.jsx(I,{className:"flex items-center gap-2",children:h.jsx(I,{className:"text-sm font-medium text-gray-900 dark:text-white",children:L(U.name)})})}):null})]})]})},t_=[{value:"LayoutDashboard",label:"LayoutDashboard",faIcon:"tachometer-alt"},{value:"Settings",label:"Settings",faIcon:"cog"},{value:"Menu",label:"Menu",faIcon:"bars"},{value:"Users",label:"Users",faIcon:"users"},{value:"Package",label:"Package",faIcon:"cube"},{value:"Puzzle",label:"Puzzle",faIcon:"puzzle-piece"},{value:"FileText",label:"FileText",faIcon:"file-alt"},{value:"ShoppingCart",label:"ShoppingCart",faIcon:"shopping-cart"},{value:"MessageSquare",label:"MessageSquare",faIcon:"comment"},{value:"Home",label:"Home",faIcon:"home"},{value:"User",label:"User",faIcon:"user"},{value:"Search",label:"Search",faIcon:"search"},{value:"Bell",label:"Bell",faIcon:"bell"},{value:"Mail",label:"Mail",faIcon:"envelope"},{value:"Calendar",label:"Calendar",faIcon:"calendar"},{value:"Chart",label:"Chart",faIcon:"chart-bar"},{value:"Database",label:"Database",faIcon:"database"},{value:"Lock",label:"Lock",faIcon:"lock"},{value:"Globe",label:"Globe",faIcon:"globe"},{value:"Image",label:"Image",faIcon:"image"}],n_=({value:e,onChange:t,options:n=t_,placeholder:r,searchPlaceholder:s,noResultsText:o,className:i="",disabled:a=!1,name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),[g,m]=v.useState(""),p=v.useRef(null),y=n.find(x=>{if(!e)return!1;if(e.includes("fa-")){const M=e.replace(/^(fas|far|fab|fal|fad)\s+fa-/,"");return x.faIcon===M||x.faIcon===e}return x.value===e||x.faIcon===e}),b=n.filter(x=>x.label.toLowerCase().includes(g.toLowerCase()));v.useEffect(()=>{const x=M=>{p.current&&!p.current.contains(M.target)&&(f(!1),m(""))};if(u)return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[u]);const _=x=>{const M=`fas fa-${x.faIcon}`;t?.(M),f(!1),m("")};return h.jsxs(I,{ref:p,className:`relative ${i}`,id:c,...d,children:[h.jsxs(ae,{type:"button",onClick:()=>!a&&f(!u),disabled:a,className:` + `},iC={onDragStart(t){let{active:e}=t;return"Picked up draggable item "+e.id+"."},onDragOver(t){let{active:e,over:n}=t;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(t){let{active:e,over:n}=t;return n?"Draggable item "+e.id+" was dropped over droppable area "+n.id:"Draggable item "+e.id+" was dropped."},onDragCancel(t){let{active:e}=t;return"Dragging was cancelled. Draggable item "+e.id+" was dropped."}};function aC(t){let{announcements:e=iC,container:n,hiddenTextDescribedById:r,screenReaderInstructions:s=oC}=t;const{announce:o,announcement:i}=nC(),a=Ys("DndLiveRegion"),[l,c]=v.useState(!1);if(v.useEffect(()=>{c(!0)},[]),rC(v.useMemo(()=>({onDragStart(u){let{active:f}=u;o(e.onDragStart({active:f}))},onDragMove(u){let{active:f,over:g}=u;e.onDragMove&&o(e.onDragMove({active:f,over:g}))},onDragOver(u){let{active:f,over:g}=u;o(e.onDragOver({active:f,over:g}))},onDragEnd(u){let{active:f,over:g}=u;o(e.onDragEnd({active:f,over:g}))},onDragCancel(u){let{active:f,over:g}=u;o(e.onDragCancel({active:f,over:g}))}}),[o,e])),!l)return null;const d=v.createElement(v.Fragment,null,v.createElement(eC,{id:r,value:s.draggable}),v.createElement(tC,{id:a,announcement:i}));return n?Dn.createPortal(d,n):d}var Et;(function(t){t.DragStart="dragStart",t.DragMove="dragMove",t.DragEnd="dragEnd",t.DragCancel="dragCancel",t.DragOver="dragOver",t.RegisterDroppable="registerDroppable",t.SetDroppableDisabled="setDroppableDisabled",t.UnregisterDroppable="unregisterDroppable"})(Et||(Et={}));function Ci(){}function es(t,e){return v.useMemo(()=>({sensor:t,options:e??{}}),[t,e])}function Wl(){for(var t=arguments.length,e=new Array(t),n=0;n<t;n++)e[n]=arguments[n];return v.useMemo(()=>[...e].filter(r=>r!=null),[...e])}const En=Object.freeze({x:0,y:0});function ch(t,e){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}function lC(t,e){const n=ki(t);if(!n)return"0 0";const r={x:(n.x-e.left)/e.width*100,y:(n.y-e.top)/e.height*100};return r.x+"% "+r.y+"%"}function dh(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return n-r}function cC(t,e){let{data:{value:n}}=t,{data:{value:r}}=e;return r-n}function uh(t){let{left:e,top:n,height:r,width:s}=t;return[{x:e,y:n},{x:e+s,y:n},{x:e,y:n+r},{x:e+s,y:n+r}]}function fh(t,e){if(!t||t.length===0)return null;const[n]=t;return n[e]}function hh(t,e,n){return e===void 0&&(e=t.left),n===void 0&&(n=t.top),{x:e+t.width*.5,y:n+t.height*.5}}const Gl=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=hh(e,e.left,e.top),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=ch(hh(l),s);o.push({id:a,data:{droppableContainer:i,value:c}})}}return o.sort(dh)},dC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=uh(e),o=[];for(const i of r){const{id:a}=i,l=n.get(a);if(l){const c=uh(l),d=s.reduce((f,g,m)=>f+ch(c[m],g),0),u=Number((d/4).toFixed(4));o.push({id:a,data:{droppableContainer:i,value:u}})}}return o.sort(dh)};function uC(t,e){const n=Math.max(e.top,t.top),r=Math.max(e.left,t.left),s=Math.min(e.left+e.width,t.left+t.width),o=Math.min(e.top+e.height,t.top+t.height),i=s-r,a=o-n;if(r<s&&n<o){const l=e.width*e.height,c=t.width*t.height,d=i*a,u=d/(l+c-d);return Number(u.toFixed(4))}return 0}const fC=t=>{let{collisionRect:e,droppableRects:n,droppableContainers:r}=t;const s=[];for(const o of r){const{id:i}=o,a=n.get(i);if(a){const l=uC(a,e);l>0&&s.push({id:i,data:{droppableContainer:o,value:l}})}}return s.sort(cC)};function hC(t,e,n){return{...t,scaleX:e&&n?e.width/n.width:1,scaleY:e&&n?e.height/n.height:1}}function gh(t,e){return t&&e?{x:t.left-e.left,y:t.top-e.top}:En}function gC(t){return function(n){for(var r=arguments.length,s=new Array(r>1?r-1:0),o=1;o<r;o++)s[o-1]=arguments[o];return s.reduce((i,a)=>({...i,top:i.top+t*a.y,bottom:i.bottom+t*a.y,left:i.left+t*a.x,right:i.right+t*a.x}),{...n})}}const pC=gC(1);function ph(t){if(t.startsWith("matrix3d(")){const e=t.slice(9,-1).split(/, /);return{x:+e[12],y:+e[13],scaleX:+e[0],scaleY:+e[5]}}else if(t.startsWith("matrix(")){const e=t.slice(7,-1).split(/, /);return{x:+e[4],y:+e[5],scaleX:+e[0],scaleY:+e[3]}}return null}function mC(t,e,n){const r=ph(e);if(!r)return t;const{scaleX:s,scaleY:o,x:i,y:a}=r,l=t.left-i-(1-s)*parseFloat(n),c=t.top-a-(1-o)*parseFloat(n.slice(n.indexOf(" ")+1)),d=s?t.width/s:t.width,u=o?t.height/o:t.height;return{width:d,height:u,top:c,right:l+d,bottom:c+u,left:l}}const bC={ignoreTransform:!1};function ts(t,e){e===void 0&&(e=bC);let n=t.getBoundingClientRect();if(e.ignoreTransform){const{transform:c,transformOrigin:d}=qt(t).getComputedStyle(t);c&&(n=mC(n,c,d))}const{top:r,left:s,width:o,height:i,bottom:a,right:l}=n;return{top:r,left:s,width:o,height:i,bottom:a,right:l}}function mh(t){return ts(t,{ignoreTransform:!0})}function yC(t){const e=t.innerWidth,n=t.innerHeight;return{top:0,left:0,right:e,bottom:n,width:e,height:n}}function vC(t,e){return e===void 0&&(e=qt(t).getComputedStyle(t)),e.position==="fixed"}function xC(t,e){e===void 0&&(e=qt(t).getComputedStyle(t));const n=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(s=>{const o=e[s];return typeof o=="string"?n.test(o):!1})}function Si(t,e){const n=[];function r(s){if(e!=null&&n.length>=e||!s)return n;if(Bl(s)&&s.scrollingElement!=null&&!n.includes(s.scrollingElement))return n.push(s.scrollingElement),n;if(!js(s)||oh(s)||n.includes(s))return n;const o=qt(t).getComputedStyle(s);return s!==t&&xC(s,o)&&n.push(s),vC(s,o)?n:r(s.parentNode)}return t?r(t):n}function bh(t){const[e]=Si(t,1);return e??null}function Yl(t){return!bi||!t?null:Qr(t)?t:Ul(t)?Bl(t)||t===Jr(t).scrollingElement?window:js(t)?t:null:null}function yh(t){return Qr(t)?t.scrollX:t.scrollLeft}function vh(t){return Qr(t)?t.scrollY:t.scrollTop}function Xl(t){return{x:yh(t),y:vh(t)}}var At;(function(t){t[t.Forward=1]="Forward",t[t.Backward=-1]="Backward"})(At||(At={}));function xh(t){return!bi||!t?!1:t===document.scrollingElement}function wh(t){const e={x:0,y:0},n=xh(t)?{height:window.innerHeight,width:window.innerWidth}:{height:t.clientHeight,width:t.clientWidth},r={x:t.scrollWidth-n.width,y:t.scrollHeight-n.height},s=t.scrollTop<=e.y,o=t.scrollLeft<=e.x,i=t.scrollTop>=r.y,a=t.scrollLeft>=r.x;return{isTop:s,isLeft:o,isBottom:i,isRight:a,maxScroll:r,minScroll:e}}const wC={x:.2,y:.2};function kC(t,e,n,r,s){let{top:o,left:i,right:a,bottom:l}=n;r===void 0&&(r=10),s===void 0&&(s=wC);const{isTop:c,isBottom:d,isLeft:u,isRight:f}=wh(t),g={x:0,y:0},m={x:0,y:0},p={height:e.height*s.y,width:e.width*s.x};return!c&&o<=e.top+p.height?(g.y=At.Backward,m.y=r*Math.abs((e.top+p.height-o)/p.height)):!d&&l>=e.bottom-p.height&&(g.y=At.Forward,m.y=r*Math.abs((e.bottom-p.height-l)/p.height)),!f&&a>=e.right-p.width?(g.x=At.Forward,m.x=r*Math.abs((e.right-p.width-a)/p.width)):!u&&i<=e.left+p.width&&(g.x=At.Backward,m.x=r*Math.abs((e.left+p.width-i)/p.width)),{direction:g,speed:m}}function CC(t){if(t===document.scrollingElement){const{innerWidth:o,innerHeight:i}=window;return{top:0,left:0,right:o,bottom:i,width:o,height:i}}const{top:e,left:n,right:r,bottom:s}=t.getBoundingClientRect();return{top:e,left:n,right:r,bottom:s,width:t.clientWidth,height:t.clientHeight}}function kh(t){return t.reduce((e,n)=>Rr(e,Xl(n)),En)}function SC(t){return t.reduce((e,n)=>e+yh(n),0)}function _C(t){return t.reduce((e,n)=>e+vh(n),0)}function Ch(t,e){if(e===void 0&&(e=ts),!t)return;const{top:n,left:r,bottom:s,right:o}=e(t);bh(t)&&(s<=0||o<=0||n>=window.innerHeight||r>=window.innerWidth)&&t.scrollIntoView({block:"center",inline:"center"})}const EC=[["x",["left","right"],SC],["y",["top","bottom"],_C]];class Kl{constructor(e,n){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const r=Si(n),s=kh(r);this.rect={...e},this.width=e.width,this.height=e.height;for(const[o,i,a]of EC)for(const l of i)Object.defineProperty(this,l,{get:()=>{const c=a(r),d=s[o]-c;return this.rect[l]+d},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class Ks{constructor(e){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(n=>{var r;return(r=this.target)==null?void 0:r.removeEventListener(...n)})},this.target=e}add(e,n,r){var s;(s=this.target)==null||s.addEventListener(e,n,r),this.listeners.push([e,n,r])}}function MC(t){const{EventTarget:e}=qt(t);return t instanceof e?t:Jr(t)}function ql(t,e){const n=Math.abs(t.x),r=Math.abs(t.y);return typeof e=="number"?Math.sqrt(n**2+r**2)>e:"x"in e&&"y"in e?n>e.x&&r>e.y:"x"in e?n>e.x:"y"in e?r>e.y:!1}var fn;(function(t){t.Click="click",t.DragStart="dragstart",t.Keydown="keydown",t.ContextMenu="contextmenu",t.Resize="resize",t.SelectionChange="selectionchange",t.VisibilityChange="visibilitychange"})(fn||(fn={}));function Sh(t){t.preventDefault()}function NC(t){t.stopPropagation()}var Ye;(function(t){t.Space="Space",t.Down="ArrowDown",t.Right="ArrowRight",t.Left="ArrowLeft",t.Up="ArrowUp",t.Esc="Escape",t.Enter="Enter",t.Tab="Tab"})(Ye||(Ye={}));const _h={start:[Ye.Space,Ye.Enter],cancel:[Ye.Esc],end:[Ye.Space,Ye.Enter,Ye.Tab]},OC=(t,e)=>{let{currentCoordinates:n}=e;switch(t.code){case Ye.Right:return{...n,x:n.x+25};case Ye.Left:return{...n,x:n.x-25};case Ye.Down:return{...n,y:n.y+25};case Ye.Up:return{...n,y:n.y-25}}};class qs{constructor(e){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=e;const{event:{target:n}}=e;this.props=e,this.listeners=new Ks(Jr(n)),this.windowListeners=new Ks(qt(n)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(fn.Resize,this.handleCancel),this.windowListeners.add(fn.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(fn.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:e,onStart:n}=this.props,r=e.node.current;r&&Ch(r),n(En)}handleKeyDown(e){if(wi(e)){const{active:n,context:r,options:s}=this.props,{keyboardCodes:o=_h,coordinateGetter:i=OC,scrollBehavior:a="smooth"}=s,{code:l}=e;if(o.end.includes(l)){this.handleEnd(e);return}if(o.cancel.includes(l)){this.handleCancel(e);return}const{collisionRect:c}=r.current,d=c?{x:c.left,y:c.top}:En;this.referenceCoordinates||(this.referenceCoordinates=d);const u=i(e,{active:n,context:r.current,currentCoordinates:d});if(u){const f=Xs(u,d),g={x:0,y:0},{scrollableAncestors:m}=r.current;for(const p of m){const y=e.code,{isTop:b,isRight:_,isLeft:x,isBottom:E,maxScroll:S,minScroll:w}=wh(p),N=CC(p),C={x:Math.min(y===Ye.Right?N.right-N.width/2:N.right,Math.max(y===Ye.Right?N.left:N.left+N.width/2,u.x)),y:Math.min(y===Ye.Down?N.bottom-N.height/2:N.bottom,Math.max(y===Ye.Down?N.top:N.top+N.height/2,u.y))},k=y===Ye.Right&&!_||y===Ye.Left&&!x,M=y===Ye.Down&&!E||y===Ye.Up&&!b;if(k&&C.x!==u.x){const T=p.scrollLeft+f.x,L=y===Ye.Right&&T<=S.x||y===Ye.Left&&T>=w.x;if(L&&!f.y){p.scrollTo({left:T,behavior:a});return}L?g.x=p.scrollLeft-T:g.x=y===Ye.Right?p.scrollLeft-S.x:p.scrollLeft-w.x,g.x&&p.scrollBy({left:-g.x,behavior:a});break}else if(M&&C.y!==u.y){const T=p.scrollTop+f.y,L=y===Ye.Down&&T<=S.y||y===Ye.Up&&T>=w.y;if(L&&!f.x){p.scrollTo({top:T,behavior:a});return}L?g.y=p.scrollTop-T:g.y=y===Ye.Down?p.scrollTop-S.y:p.scrollTop-w.y,g.y&&p.scrollBy({top:-g.y,behavior:a});break}}this.handleMove(e,Rr(Xs(u,this.referenceCoordinates),g))}}}handleMove(e,n){const{onMove:r}=this.props;e.preventDefault(),r(n)}handleEnd(e){const{onEnd:n}=this.props;e.preventDefault(),this.detach(),n()}handleCancel(e){const{onCancel:n}=this.props;e.preventDefault(),this.detach(),n()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}qs.activators=[{eventName:"onKeyDown",handler:(t,e,n)=>{let{keyboardCodes:r=_h,onActivation:s}=e,{active:o}=n;const{code:i}=t.nativeEvent;if(r.start.includes(i)){const a=o.activatorNode.current;return a&&t.target!==a?!1:(t.preventDefault(),s?.({event:t.nativeEvent}),!0)}return!1}}];function Eh(t){return!!(t&&"distance"in t)}function Mh(t){return!!(t&&"delay"in t)}class Zl{constructor(e,n,r){var s;r===void 0&&(r=MC(e.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=e,this.events=n;const{event:o}=e,{target:i}=o;this.props=e,this.events=n,this.document=Jr(i),this.documentListeners=new Ks(this.document),this.listeners=new Ks(r),this.windowListeners=new Ks(qt(i)),this.initialCoordinates=(s=ki(o))!=null?s:En,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:e,props:{options:{activationConstraint:n,bypassActivationConstraint:r}}}=this;if(this.listeners.add(e.move.name,this.handleMove,{passive:!1}),this.listeners.add(e.end.name,this.handleEnd),e.cancel&&this.listeners.add(e.cancel.name,this.handleCancel),this.windowListeners.add(fn.Resize,this.handleCancel),this.windowListeners.add(fn.DragStart,Sh),this.windowListeners.add(fn.VisibilityChange,this.handleCancel),this.windowListeners.add(fn.ContextMenu,Sh),this.documentListeners.add(fn.Keydown,this.handleKeydown),n){if(r!=null&&r({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(Mh(n)){this.timeoutId=setTimeout(this.handleStart,n.delay),this.handlePending(n);return}if(Eh(n)){this.handlePending(n);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(e,n){const{active:r,onPending:s}=this.props;s(r,e,this.initialCoordinates,n)}handleStart(){const{initialCoordinates:e}=this,{onStart:n}=this.props;e&&(this.activated=!0,this.documentListeners.add(fn.Click,NC,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(fn.SelectionChange,this.removeTextSelection),n(e))}handleMove(e){var n;const{activated:r,initialCoordinates:s,props:o}=this,{onMove:i,options:{activationConstraint:a}}=o;if(!s)return;const l=(n=ki(e))!=null?n:En,c=Xs(s,l);if(!r&&a){if(Eh(a)){if(a.tolerance!=null&&ql(c,a.tolerance))return this.handleCancel();if(ql(c,a.distance))return this.handleStart()}if(Mh(a)&&ql(c,a.tolerance))return this.handleCancel();this.handlePending(a,c);return}e.cancelable&&e.preventDefault(),i(l)}handleEnd(){const{onAbort:e,onEnd:n}=this.props;this.detach(),this.activated||e(this.props.active),n()}handleCancel(){const{onAbort:e,onCancel:n}=this.props;this.detach(),this.activated||e(this.props.active),n()}handleKeydown(e){e.code===Ye.Esc&&this.handleCancel()}removeTextSelection(){var e;(e=this.document.getSelection())==null||e.removeAllRanges()}}const TC={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class Zs extends Zl{constructor(e){const{event:n}=e,r=Jr(n.target);super(e,TC,r)}}Zs.activators=[{eventName:"onPointerDown",handler:(t,e)=>{let{nativeEvent:n}=t,{onActivation:r}=e;return!n.isPrimary||n.button!==0?!1:(r?.({event:n}),!0)}}];const AC={move:{name:"mousemove"},end:{name:"mouseup"}};var Ql;(function(t){t[t.RightClick=2]="RightClick"})(Ql||(Ql={}));class DC extends Zl{constructor(e){super(e,AC,Jr(e.event.target))}}DC.activators=[{eventName:"onMouseDown",handler:(t,e)=>{let{nativeEvent:n}=t,{onActivation:r}=e;return n.button===Ql.RightClick?!1:(r?.({event:n}),!0)}}];const Jl={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class IC extends Zl{constructor(e){super(e,Jl)}static setup(){return window.addEventListener(Jl.move.name,e,{capture:!1,passive:!1}),function(){window.removeEventListener(Jl.move.name,e)};function e(){}}}IC.activators=[{eventName:"onTouchStart",handler:(t,e)=>{let{nativeEvent:n}=t,{onActivation:r}=e;const{touches:s}=n;return s.length>1?!1:(r?.({event:n}),!0)}}];var Qs;(function(t){t[t.Pointer=0]="Pointer",t[t.DraggableRect=1]="DraggableRect"})(Qs||(Qs={}));var _i;(function(t){t[t.TreeOrder=0]="TreeOrder",t[t.ReversedTreeOrder=1]="ReversedTreeOrder"})(_i||(_i={}));function LC(t){let{acceleration:e,activator:n=Qs.Pointer,canScroll:r,draggingRect:s,enabled:o,interval:i=5,order:a=_i.TreeOrder,pointerCoordinates:l,scrollableAncestors:c,scrollableAncestorRects:d,delta:u,threshold:f}=t;const g=$C({delta:u,disabled:!o}),[m,p]=qk(),y=v.useRef({x:0,y:0}),b=v.useRef({x:0,y:0}),_=v.useMemo(()=>{switch(n){case Qs.Pointer:return l?{top:l.y,bottom:l.y,left:l.x,right:l.x}:null;case Qs.DraggableRect:return s}},[n,s,l]),x=v.useRef(null),E=v.useCallback(()=>{const w=x.current;if(!w)return;const N=y.current.x*b.current.x,C=y.current.y*b.current.y;w.scrollBy(N,C)},[]),S=v.useMemo(()=>a===_i.TreeOrder?[...c].reverse():c,[a,c]);v.useEffect(()=>{if(!o||!c.length||!_){p();return}for(const w of S){if(r?.(w)===!1)continue;const N=c.indexOf(w),C=d[N];if(!C)continue;const{direction:k,speed:M}=kC(w,C,_,e,f);for(const T of["x","y"])g[T][k[T]]||(M[T]=0,k[T]=0);if(M.x>0||M.y>0){p(),x.current=w,m(E,i),y.current=M,b.current=k;return}}y.current={x:0,y:0},b.current={x:0,y:0},p()},[e,E,r,p,o,i,JSON.stringify(_),JSON.stringify(g),m,c,S,d,JSON.stringify(f)])}const PC={x:{[At.Backward]:!1,[At.Forward]:!1},y:{[At.Backward]:!1,[At.Forward]:!1}};function $C(t){let{delta:e,disabled:n}=t;const r=xi(e);return Gs(s=>{if(n||!r||!s)return PC;const o={x:Math.sign(e.x-r.x),y:Math.sign(e.y-r.y)};return{x:{[At.Backward]:s.x[At.Backward]||o.x===-1,[At.Forward]:s.x[At.Forward]||o.x===1},y:{[At.Backward]:s.y[At.Backward]||o.y===-1,[At.Forward]:s.y[At.Forward]||o.y===1}}},[n,e,r])}function FC(t,e){const n=e!=null?t.get(e):void 0,r=n?n.node.current:null;return Gs(s=>{var o;return e==null?null:(o=r??s)!=null?o:null},[r,e])}function VC(t,e){return v.useMemo(()=>t.reduce((n,r)=>{const{sensor:s}=r,o=s.activators.map(i=>({eventName:i.eventName,handler:e(i.handler,r)}));return[...n,...o]},[]),[t,e])}var Js;(function(t){t[t.Always=0]="Always",t[t.BeforeDragging=1]="BeforeDragging",t[t.WhileDragging=2]="WhileDragging"})(Js||(Js={}));var Rl;(function(t){t.Optimized="optimized"})(Rl||(Rl={}));const Nh=new Map;function zC(t,e){let{dragging:n,dependencies:r,config:s}=e;const[o,i]=v.useState(null),{frequency:a,measure:l,strategy:c}=s,d=v.useRef(t),u=y(),f=Ws(u),g=v.useCallback(function(b){b===void 0&&(b=[]),!f.current&&i(_=>_===null?b:_.concat(b.filter(x=>!_.includes(x))))},[f]),m=v.useRef(null),p=Gs(b=>{if(u&&!n)return Nh;if(!b||b===Nh||d.current!==t||o!=null){const _=new Map;for(let x of t){if(!x)continue;if(o&&o.length>0&&!o.includes(x.id)&&x.rect.current){_.set(x.id,x.rect.current);continue}const E=x.node.current,S=E?new Kl(l(E),E):null;x.rect.current=S,S&&_.set(x.id,S)}return _}return b},[t,o,n,u,l]);return v.useEffect(()=>{d.current=t},[t]),v.useEffect(()=>{u||g()},[n,u]),v.useEffect(()=>{o&&o.length>0&&i(null)},[JSON.stringify(o)]),v.useEffect(()=>{u||typeof a!="number"||m.current!==null||(m.current=setTimeout(()=>{g(),m.current=null},a))},[a,u,g,...r]),{droppableRects:p,measureDroppableContainers:g,measuringScheduled:o!=null};function y(){switch(c){case Js.Always:return!1;case Js.BeforeDragging:return n;default:return!n}}}function ec(t,e){return Gs(n=>t?n||(typeof e=="function"?e(t):t):null,[e,t])}function HC(t,e){return ec(t,e)}function UC(t){let{callback:e,disabled:n}=t;const r=yi(e),s=v.useMemo(()=>{if(n||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:o}=window;return new o(r)},[r,n]);return v.useEffect(()=>()=>s?.disconnect(),[s]),s}function Ei(t){let{callback:e,disabled:n}=t;const r=yi(e),s=v.useMemo(()=>{if(n||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:o}=window;return new o(r)},[n]);return v.useEffect(()=>()=>s?.disconnect(),[s]),s}function BC(t){return new Kl(ts(t),t)}function Oh(t,e,n){e===void 0&&(e=BC);const[r,s]=v.useState(null);function o(){s(l=>{if(!t)return null;if(t.isConnected===!1){var c;return(c=l??n)!=null?c:null}const d=e(t);return JSON.stringify(l)===JSON.stringify(d)?l:d})}const i=UC({callback(l){if(t)for(const c of l){const{type:d,target:u}=c;if(d==="childList"&&u instanceof HTMLElement&&u.contains(t)){o();break}}}}),a=Ei({callback:o});return _n(()=>{o(),t?(a?.observe(t),i?.observe(document.body,{childList:!0,subtree:!0})):(a?.disconnect(),i?.disconnect())},[t]),r}function jC(t){const e=ec(t);return gh(t,e)}const Th=[];function WC(t){const e=v.useRef(t),n=Gs(r=>t?r&&r!==Th&&t&&e.current&&t.parentNode===e.current.parentNode?r:Si(t):Th,[t]);return v.useEffect(()=>{e.current=t},[t]),n}function GC(t){const[e,n]=v.useState(null),r=v.useRef(t),s=v.useCallback(o=>{const i=Yl(o.target);i&&n(a=>a?(a.set(i,Xl(i)),new Map(a)):null)},[]);return v.useEffect(()=>{const o=r.current;if(t!==o){i(o);const a=t.map(l=>{const c=Yl(l);return c?(c.addEventListener("scroll",s,{passive:!0}),[c,Xl(c)]):null}).filter(l=>l!=null);n(a.length?new Map(a):null),r.current=t}return()=>{i(t),i(o)};function i(a){a.forEach(l=>{const c=Yl(l);c?.removeEventListener("scroll",s)})}},[s,t]),v.useMemo(()=>t.length?e?Array.from(e.values()).reduce((o,i)=>Rr(o,i),En):kh(t):En,[t,e])}function Ah(t,e){e===void 0&&(e=[]);const n=v.useRef(null);return v.useEffect(()=>{n.current=null},e),v.useEffect(()=>{const r=t!==En;r&&!n.current&&(n.current=t),!r&&n.current&&(n.current=null)},[t]),n.current?Xs(t,n.current):En}function YC(t){v.useEffect(()=>{if(!bi)return;const e=t.map(n=>{let{sensor:r}=n;return r.setup==null?void 0:r.setup()});return()=>{for(const n of e)n?.()}},t.map(e=>{let{sensor:n}=e;return n}))}function XC(t,e){return v.useMemo(()=>t.reduce((n,r)=>{let{eventName:s,handler:o}=r;return n[s]=i=>{o(i,e)},n},{}),[t,e])}function Dh(t){return v.useMemo(()=>t?yC(t):null,[t])}const Ih=[];function KC(t,e){e===void 0&&(e=ts);const[n]=t,r=Dh(n?qt(n):null),[s,o]=v.useState(Ih);function i(){o(()=>t.length?t.map(l=>xh(l)?r:new Kl(e(l),l)):Ih)}const a=Ei({callback:i});return _n(()=>{a?.disconnect(),i(),t.forEach(l=>a?.observe(l))},[t]),s}function Lh(t){if(!t)return null;if(t.children.length>1)return t;const e=t.children[0];return js(e)?e:t}function qC(t){let{measure:e}=t;const[n,r]=v.useState(null),s=v.useCallback(c=>{for(const{target:d}of c)if(js(d)){r(u=>{const f=e(d);return u?{...u,width:f.width,height:f.height}:f});break}},[e]),o=Ei({callback:s}),i=v.useCallback(c=>{const d=Lh(c);o?.disconnect(),d&&o?.observe(d),r(d?e(d):null)},[e,o]),[a,l]=vi(i);return v.useMemo(()=>({nodeRef:a,rect:n,setRef:l}),[n,a,l])}const ZC=[{sensor:Zs,options:{}},{sensor:qs,options:{}}],QC={current:{}},Mi={draggable:{measure:mh},droppable:{measure:mh,strategy:Js.WhileDragging,frequency:Rl.Optimized},dragOverlay:{measure:ts}};class Rs extends Map{get(e){var n;return e!=null&&(n=super.get(e))!=null?n:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(e=>{let{disabled:n}=e;return!n})}getNodeFor(e){var n,r;return(n=(r=this.get(e))==null?void 0:r.node.current)!=null?n:void 0}}const JC={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new Rs,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:Ci},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:Mi,measureDroppableContainers:Ci,windowRect:null,measuringScheduled:!1},Ph={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:Ci,draggableNodes:new Map,over:null,measureDroppableContainers:Ci},eo=v.createContext(Ph),$h=v.createContext(JC);function RC(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new Rs}}}function eS(t,e){switch(e.type){case Et.DragStart:return{...t,draggable:{...t.draggable,initialCoordinates:e.initialCoordinates,active:e.active}};case Et.DragMove:return t.draggable.active==null?t:{...t,draggable:{...t.draggable,translate:{x:e.coordinates.x-t.draggable.initialCoordinates.x,y:e.coordinates.y-t.draggable.initialCoordinates.y}}};case Et.DragEnd:case Et.DragCancel:return{...t,draggable:{...t.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case Et.RegisterDroppable:{const{element:n}=e,{id:r}=n,s=new Rs(t.droppable.containers);return s.set(r,n),{...t,droppable:{...t.droppable,containers:s}}}case Et.SetDroppableDisabled:{const{id:n,key:r,disabled:s}=e,o=t.droppable.containers.get(n);if(!o||r!==o.key)return t;const i=new Rs(t.droppable.containers);return i.set(n,{...o,disabled:s}),{...t,droppable:{...t.droppable,containers:i}}}case Et.UnregisterDroppable:{const{id:n,key:r}=e,s=t.droppable.containers.get(n);if(!s||r!==s.key)return t;const o=new Rs(t.droppable.containers);return o.delete(n),{...t,droppable:{...t.droppable,containers:o}}}default:return t}}function tS(t){let{disabled:e}=t;const{active:n,activatorEvent:r,draggableNodes:s}=v.useContext(eo),o=xi(r),i=xi(n?.id);return v.useEffect(()=>{if(!e&&!r&&o&&i!=null){if(!wi(o)||document.activeElement===o.target)return;const a=s.get(i);if(!a)return;const{activatorNode:l,node:c}=a;if(!l.current&&!c.current)return;requestAnimationFrame(()=>{for(const d of[l.current,c.current]){if(!d)continue;const u=Jk(d);if(u){u.focus();break}}})}},[r,e,s,i,o]),null}function Fh(t,e){let{transform:n,...r}=e;return t!=null&&t.length?t.reduce((s,o)=>o({transform:s,...r}),n):n}function nS(t){return v.useMemo(()=>({draggable:{...Mi.draggable,...t?.draggable},droppable:{...Mi.droppable,...t?.droppable},dragOverlay:{...Mi.dragOverlay,...t?.dragOverlay}}),[t?.draggable,t?.droppable,t?.dragOverlay])}function rS(t){let{activeNode:e,measure:n,initialRect:r,config:s=!0}=t;const o=v.useRef(!1),{x:i,y:a}=typeof s=="boolean"?{x:s,y:s}:s;_n(()=>{if(!i&&!a||!e){o.current=!1;return}if(o.current||!r)return;const c=e?.node.current;if(!c||c.isConnected===!1)return;const d=n(c),u=gh(d,r);if(i||(u.x=0),a||(u.y=0),o.current=!0,Math.abs(u.x)>0||Math.abs(u.y)>0){const f=bh(c);f&&f.scrollBy({top:u.y,left:u.x})}},[e,i,a,r,n])}const Ni=v.createContext({...En,scaleX:1,scaleY:1});var sr;(function(t){t[t.Uninitialized=0]="Uninitialized",t[t.Initializing=1]="Initializing",t[t.Initialized=2]="Initialized"})(sr||(sr={}));const tc=v.memo(function(e){var n,r,s,o;let{id:i,accessibility:a,autoScroll:l=!0,children:c,sensors:d=ZC,collisionDetection:u=fC,measuring:f,modifiers:g,...m}=e;const p=v.useReducer(eS,void 0,RC),[y,b]=p,[_,x]=sC(),[E,S]=v.useState(sr.Uninitialized),w=E===sr.Initialized,{draggable:{active:N,nodes:C,translate:k},droppable:{containers:M}}=y,T=N!=null?C.get(N):null,L=v.useRef({initial:null,translated:null}),P=v.useMemo(()=>{var ye;return N!=null?{id:N,data:(ye=T?.data)!=null?ye:QC,rect:L}:null},[N,T]),F=v.useRef(null),[D,A]=v.useState(null),[O,V]=v.useState(null),H=Ws(m,Object.values(m)),W=Ys("DndDescribedBy",i),j=v.useMemo(()=>M.getEnabled(),[M]),U=nS(f),{droppableRects:B,measureDroppableContainers:G,measuringScheduled:z}=zC(j,{dragging:w,dependencies:[k.x,k.y],config:U.droppable}),X=FC(C,N),Y=v.useMemo(()=>O?ki(O):null,[O]),Z=ot(),ee=HC(X,U.draggable.measure);rS({activeNode:N!=null?C.get(N):null,config:Z.layoutShiftCompensation,initialRect:ee,measure:U.draggable.measure});const te=Oh(X,U.draggable.measure,ee),ce=Oh(X?X.parentElement:null),J=v.useRef({activatorEvent:null,active:null,activeNode:X,collisionRect:null,collisions:null,droppableRects:B,draggableNodes:C,draggingNode:null,draggingNodeRect:null,droppableContainers:M,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),he=M.getNodeFor((n=J.current.over)==null?void 0:n.id),le=qC({measure:U.dragOverlay.measure}),ae=(r=le.nodeRef.current)!=null?r:X,Se=w?(s=le.rect)!=null?s:te:null,ue=!!(le.nodeRef.current&&le.rect),pe=jC(ue?null:te),we=Dh(ae?qt(ae):null),He=WC(w?he??X:null),se=KC(He),Oe=Fh(g,{transform:{x:k.x-pe.x,y:k.y-pe.y,scaleX:1,scaleY:1},activatorEvent:O,active:P,activeNodeRect:te,containerNodeRect:ce,draggingNodeRect:Se,over:J.current.over,overlayNodeRect:le.rect,scrollableAncestors:He,scrollableAncestorRects:se,windowRect:we}),Le=Y?Rr(Y,k):null,Ie=GC(He),Ue=Ah(Ie),et=Ah(Ie,[te]),bt=Rr(Oe,Ue),Ge=Se?pC(Se,Oe):null,ft=P&&Ge?u({active:P,collisionRect:Ge,droppableRects:B,droppableContainers:j,pointerCoordinates:Le}):null,Be=fh(ft,"id"),[ne,fe]=v.useState(null),xe=ue?Oe:Rr(Oe,et),ge=hC(xe,(o=ne?.rect)!=null?o:null,te),be=v.useRef(null),Ve=v.useCallback((ye,Ce)=>{let{sensor:Fe,options:nt}=Ce;if(F.current==null)return;const pt=C.get(F.current);if(!pt)return;const it=ye.nativeEvent,rt=new Fe({active:F.current,activeNode:pt,event:it,options:nt,context:J,onAbort(ht){if(!C.get(ht))return;const{onDragAbort:Gt}=H.current,mt={id:ht};Gt?.(mt),_({type:"onDragAbort",event:mt})},onPending(ht,Qt,Gt,mt){if(!C.get(ht))return;const{onDragPending:xn}=H.current,Vt={id:ht,constraint:Qt,initialCoordinates:Gt,offset:mt};xn?.(Vt),_({type:"onDragPending",event:Vt})},onStart(ht){const Qt=F.current;if(Qt==null)return;const Gt=C.get(Qt);if(!Gt)return;const{onDragStart:mt}=H.current,St={activatorEvent:it,active:{id:Qt,data:Gt.data,rect:L}};Dn.unstable_batchedUpdates(()=>{mt?.(St),S(sr.Initializing),b({type:Et.DragStart,initialCoordinates:ht,active:Qt}),_({type:"onDragStart",event:St}),A(be.current),V(it)})},onMove(ht){b({type:Et.DragMove,coordinates:ht})},onEnd:Ot(Et.DragEnd),onCancel:Ot(Et.DragCancel)});be.current=rt;function Ot(ht){return async function(){const{active:Gt,collisions:mt,over:St,scrollAdjustedTranslate:xn}=J.current;let Vt=null;if(Gt&&xn){const{cancelDrop:Ze}=H.current;Vt={activatorEvent:it,active:Gt,collisions:mt,delta:xn,over:St},ht===Et.DragEnd&&typeof Ze=="function"&&await Promise.resolve(Ze(Vt))&&(ht=Et.DragCancel)}F.current=null,Dn.unstable_batchedUpdates(()=>{b({type:ht}),S(sr.Uninitialized),fe(null),A(null),V(null),be.current=null;const Ze=ht===Et.DragEnd?"onDragEnd":"onDragCancel";if(Vt){const jn=H.current[Ze];jn?.(Vt),_({type:Ze,event:Vt})}})}}},[C]),De=v.useCallback((ye,Ce)=>(Fe,nt)=>{const pt=Fe.nativeEvent,it=C.get(nt);if(F.current!==null||!it||pt.dndKit||pt.defaultPrevented)return;const rt={active:it};ye(Fe,Ce.options,rt)===!0&&(pt.dndKit={capturedBy:Ce.sensor},F.current=nt,Ve(Fe,Ce))},[C,Ve]),$e=VC(d,De);YC(d),_n(()=>{te&&E===sr.Initializing&&S(sr.Initialized)},[te,E]),v.useEffect(()=>{const{onDragMove:ye}=H.current,{active:Ce,activatorEvent:Fe,collisions:nt,over:pt}=J.current;if(!Ce||!Fe)return;const it={active:Ce,activatorEvent:Fe,collisions:nt,delta:{x:bt.x,y:bt.y},over:pt};Dn.unstable_batchedUpdates(()=>{ye?.(it),_({type:"onDragMove",event:it})})},[bt.x,bt.y]),v.useEffect(()=>{const{active:ye,activatorEvent:Ce,collisions:Fe,droppableContainers:nt,scrollAdjustedTranslate:pt}=J.current;if(!ye||F.current==null||!Ce||!pt)return;const{onDragOver:it}=H.current,rt=nt.get(Be),Ot=rt&&rt.rect.current?{id:rt.id,rect:rt.rect.current,data:rt.data,disabled:rt.disabled}:null,ht={active:ye,activatorEvent:Ce,collisions:Fe,delta:{x:pt.x,y:pt.y},over:Ot};Dn.unstable_batchedUpdates(()=>{fe(Ot),it?.(ht),_({type:"onDragOver",event:ht})})},[Be]),_n(()=>{J.current={activatorEvent:O,active:P,activeNode:X,collisionRect:Ge,collisions:ft,droppableRects:B,draggableNodes:C,draggingNode:ae,draggingNodeRect:Se,droppableContainers:M,over:ne,scrollableAncestors:He,scrollAdjustedTranslate:bt},L.current={initial:Se,translated:Ge}},[P,X,ft,Ge,C,ae,Se,B,M,ne,He,bt]),LC({...Z,delta:k,draggingRect:Ge,pointerCoordinates:Le,scrollableAncestors:He,scrollableAncestorRects:se});const Me=v.useMemo(()=>({active:P,activeNode:X,activeNodeRect:te,activatorEvent:O,collisions:ft,containerNodeRect:ce,dragOverlay:le,draggableNodes:C,droppableContainers:M,droppableRects:B,over:ne,measureDroppableContainers:G,scrollableAncestors:He,scrollableAncestorRects:se,measuringConfiguration:U,measuringScheduled:z,windowRect:we}),[P,X,te,O,ft,ce,le,C,M,B,ne,G,He,se,U,z,we]),qe=v.useMemo(()=>({activatorEvent:O,activators:$e,active:P,activeNodeRect:te,ariaDescribedById:{draggable:W},dispatch:b,draggableNodes:C,over:ne,measureDroppableContainers:G}),[O,$e,P,te,b,W,C,ne,G]);return v.createElement(lh.Provider,{value:x},v.createElement(eo.Provider,{value:qe},v.createElement($h.Provider,{value:Me},v.createElement(Ni.Provider,{value:ge},c)),v.createElement(tS,{disabled:a?.restoreFocus===!1})),v.createElement(aC,{...a,hiddenTextDescribedById:W}));function ot(){const ye=D?.autoScrollEnabled===!1,Ce=typeof l=="object"?l.enabled===!1:l===!1,Fe=w&&!ye&&!Ce;return typeof l=="object"?{...l,enabled:Fe}:{enabled:Fe}}}),sS=v.createContext(null),Vh="button",oS="Draggable";function iS(t){let{id:e,data:n,disabled:r=!1,attributes:s}=t;const o=Ys(oS),{activators:i,activatorEvent:a,active:l,activeNodeRect:c,ariaDescribedById:d,draggableNodes:u,over:f}=v.useContext(eo),{role:g=Vh,roleDescription:m="draggable",tabIndex:p=0}=s??{},y=l?.id===e,b=v.useContext(y?Ni:sS),[_,x]=vi(),[E,S]=vi(),w=XC(i,e),N=Ws(n);_n(()=>(u.set(e,{id:e,key:o,node:_,activatorNode:E,data:N}),()=>{const k=u.get(e);k&&k.key===o&&u.delete(e)}),[u,e]);const C=v.useMemo(()=>({role:g,tabIndex:p,"aria-disabled":r,"aria-pressed":y&&g===Vh?!0:void 0,"aria-roledescription":m,"aria-describedby":d.draggable}),[r,g,p,y,m,d.draggable]);return{active:l,activatorEvent:a,activeNodeRect:c,attributes:C,isDragging:y,listeners:r?void 0:w,node:_,over:f,setNodeRef:x,setActivatorNodeRef:S,transform:b}}function zh(){return v.useContext($h)}const aS="Droppable",lS={timeout:25};function cS(t){let{data:e,disabled:n=!1,id:r,resizeObserverConfig:s}=t;const o=Ys(aS),{active:i,dispatch:a,over:l,measureDroppableContainers:c}=v.useContext(eo),d=v.useRef({disabled:n}),u=v.useRef(!1),f=v.useRef(null),g=v.useRef(null),{disabled:m,updateMeasurementsFor:p,timeout:y}={...lS,...s},b=Ws(p??r),_=v.useCallback(()=>{if(!u.current){u.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{c(Array.isArray(b.current)?b.current:[b.current]),g.current=null},y)},[y]),x=Ei({callback:_,disabled:m||!i}),E=v.useCallback((C,k)=>{x&&(k&&(x.unobserve(k),u.current=!1),C&&x.observe(C))},[x]),[S,w]=vi(E),N=Ws(e);return v.useEffect(()=>{!x||!S.current||(x.disconnect(),u.current=!1,x.observe(S.current))},[S,x]),v.useEffect(()=>(a({type:Et.RegisterDroppable,element:{id:r,key:o,disabled:n,node:S,rect:f,data:N}}),()=>a({type:Et.UnregisterDroppable,key:o,id:r})),[r]),v.useEffect(()=>{n!==d.current.disabled&&(a({type:Et.SetDroppableDisabled,id:r,key:o,disabled:n}),d.current.disabled=n)},[r,o,n,a]),{active:i,rect:f,isOver:l?.id===r,node:S,over:l,setNodeRef:w}}function dS(t){let{animation:e,children:n}=t;const[r,s]=v.useState(null),[o,i]=v.useState(null),a=xi(n);return!n&&!r&&a&&s(a),_n(()=>{if(!o)return;const l=r?.key,c=r?.props.id;if(l==null||c==null){s(null);return}Promise.resolve(e(c,o)).then(()=>{s(null)})},[e,r,o]),v.createElement(v.Fragment,null,n,r?v.cloneElement(r,{ref:i}):null)}const uS={x:0,y:0,scaleX:1,scaleY:1};function fS(t){let{children:e}=t;return v.createElement(eo.Provider,{value:Ph},v.createElement(Ni.Provider,{value:uS},e))}const hS={position:"fixed",touchAction:"none"},gS=t=>wi(t)?"transform 250ms ease":void 0,pS=v.forwardRef((t,e)=>{let{as:n,activatorEvent:r,adjustScale:s,children:o,className:i,rect:a,style:l,transform:c,transition:d=gS}=t;if(!a)return null;const u=s?c:{...c,scaleX:1,scaleY:1},f={...hS,width:a.width,height:a.height,top:a.top,left:a.left,transform:Fn.Transform.toString(u),transformOrigin:s&&r?lC(r,a):void 0,transition:typeof d=="function"?d(r):d,...l};return v.createElement(n,{className:i,style:f,ref:e},o)}),mS={duration:250,easing:"ease",keyframes:t=>{let{transform:{initial:e,final:n}}=t;return[{transform:Fn.Transform.toString(e)},{transform:Fn.Transform.toString(n)}]},sideEffects:(t=>e=>{let{active:n,dragOverlay:r}=e;const s={},{styles:o,className:i}=t;if(o!=null&&o.active)for(const[a,l]of Object.entries(o.active))l!==void 0&&(s[a]=n.node.style.getPropertyValue(a),n.node.style.setProperty(a,l));if(o!=null&&o.dragOverlay)for(const[a,l]of Object.entries(o.dragOverlay))l!==void 0&&r.node.style.setProperty(a,l);return i!=null&&i.active&&n.node.classList.add(i.active),i!=null&&i.dragOverlay&&r.node.classList.add(i.dragOverlay),function(){for(const[l,c]of Object.entries(s))n.node.style.setProperty(l,c);i!=null&&i.active&&n.node.classList.remove(i.active)}})({styles:{active:{opacity:"0"}}})};function bS(t){let{config:e,draggableNodes:n,droppableContainers:r,measuringConfiguration:s}=t;return yi((o,i)=>{if(e===null)return;const a=n.get(o);if(!a)return;const l=a.node.current;if(!l)return;const c=Lh(i);if(!c)return;const{transform:d}=qt(i).getComputedStyle(i),u=ph(d);if(!u)return;const f=typeof e=="function"?e:yS(e);return Ch(l,s.draggable.measure),f({active:{id:o,data:a.data,node:l,rect:s.draggable.measure(l)},draggableNodes:n,dragOverlay:{node:i,rect:s.dragOverlay.measure(c)},droppableContainers:r,measuringConfiguration:s,transform:u})})}function yS(t){const{duration:e,easing:n,sideEffects:r,keyframes:s}={...mS,...t};return o=>{let{active:i,dragOverlay:a,transform:l,...c}=o;if(!e)return;const d={x:a.rect.left-i.rect.left,y:a.rect.top-i.rect.top},u={scaleX:l.scaleX!==1?i.rect.width*l.scaleX/a.rect.width:1,scaleY:l.scaleY!==1?i.rect.height*l.scaleY/a.rect.height:1},f={x:l.x-d.x,y:l.y-d.y,...u},g=s({...c,active:i,dragOverlay:a,transform:{initial:l,final:f}}),[m]=g,p=g[g.length-1];if(JSON.stringify(m)===JSON.stringify(p))return;const y=r?.({active:i,dragOverlay:a,...c}),b=a.node.animate(g,{duration:e,easing:n,fill:"forwards"});return new Promise(_=>{b.onfinish=()=>{y?.(),_()}})}}let Hh=0;function vS(t){return v.useMemo(()=>{if(t!=null)return Hh++,Hh},[t])}const xS=v.memo(t=>{let{adjustScale:e=!1,children:n,dropAnimation:r,style:s,transition:o,modifiers:i,wrapperElement:a="div",className:l,zIndex:c=999}=t;const{activatorEvent:d,active:u,activeNodeRect:f,containerNodeRect:g,draggableNodes:m,droppableContainers:p,dragOverlay:y,over:b,measuringConfiguration:_,scrollableAncestors:x,scrollableAncestorRects:E,windowRect:S}=zh(),w=v.useContext(Ni),N=vS(u?.id),C=Fh(i,{activatorEvent:d,active:u,activeNodeRect:f,containerNodeRect:g,draggingNodeRect:y.rect,over:b,overlayNodeRect:y.rect,scrollableAncestors:x,scrollableAncestorRects:E,transform:w,windowRect:S}),k=ec(f),M=bS({config:r,draggableNodes:m,droppableContainers:p,measuringConfiguration:_}),T=k?y.setRef:void 0;return v.createElement(fS,null,v.createElement(dS,{animation:M},u&&N?v.createElement(pS,{key:N,id:u.id,ref:T,as:a,activatorEvent:d,adjustScale:e,className:l,transition:o,rect:k,style:{zIndex:c,...s},transform:C},n):null))});function ns(t,e,n){const r=t.slice();return r.splice(n<0?r.length+n:n,0,r.splice(e,1)[0]),r}function wS(t,e){return t.reduce((n,r,s)=>{const o=e.get(r);return o&&(n[s]=o),n},Array(t.length))}function Oi(t){return t!==null&&t>=0}function kS(t,e){if(t===e)return!0;if(t.length!==e.length)return!1;for(let n=0;n<t.length;n++)if(t[n]!==e[n])return!1;return!0}function CS(t){return typeof t=="boolean"?{draggable:t,droppable:t}:t}const Ti=t=>{let{rects:e,activeIndex:n,overIndex:r,index:s}=t;const o=ns(e,r,n),i=e[s],a=o[s];return!a||!i?null:{x:a.left-i.left,y:a.top-i.top,scaleX:a.width/i.width,scaleY:a.height/i.height}},Ai={scaleX:1,scaleY:1},nc=t=>{var e;let{activeIndex:n,activeNodeRect:r,index:s,rects:o,overIndex:i}=t;const a=(e=o[n])!=null?e:r;if(!a)return null;if(s===n){const c=o[i];return c?{x:0,y:n<i?c.top+c.height-(a.top+a.height):c.top-a.top,...Ai}:null}const l=SS(o,s,n);return s>n&&s<=i?{x:0,y:-a.height-l,...Ai}:s<n&&s>=i?{x:0,y:a.height+l,...Ai}:{x:0,y:0,...Ai}};function SS(t,e,n){const r=t[e],s=t[e-1],o=t[e+1];return r?n<e?s?r.top-(s.top+s.height):o?o.top-(r.top+r.height):0:o?o.top-(r.top+r.height):s?r.top-(s.top+s.height):0:0}const Uh="Sortable",Bh=v.createContext({activeIndex:-1,containerId:Uh,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:Ti,disabled:{draggable:!1,droppable:!1}});function Di(t){let{children:e,id:n,items:r,strategy:s=Ti,disabled:o=!1}=t;const{active:i,dragOverlay:a,droppableRects:l,over:c,measureDroppableContainers:d}=zh(),u=Ys(Uh,n),f=a.rect!==null,g=v.useMemo(()=>r.map(w=>typeof w=="object"&&"id"in w?w.id:w),[r]),m=i!=null,p=i?g.indexOf(i.id):-1,y=c?g.indexOf(c.id):-1,b=v.useRef(g),_=!kS(g,b.current),x=y!==-1&&p===-1||_,E=CS(o);_n(()=>{_&&m&&d(g)},[_,g,m,d]),v.useEffect(()=>{b.current=g},[g]);const S=v.useMemo(()=>({activeIndex:p,containerId:u,disabled:E,disableTransforms:x,items:g,overIndex:y,useDragOverlay:f,sortedRects:wS(g,l),strategy:s}),[p,u,E.draggable,E.droppable,x,g,y,l,f,s]);return v.createElement(Bh.Provider,{value:S},e)}const jh=t=>{let{id:e,items:n,activeIndex:r,overIndex:s}=t;return ns(n,r,s).indexOf(e)},Wh=t=>{let{containerId:e,isSorting:n,wasDragging:r,index:s,items:o,newIndex:i,previousItems:a,previousContainerId:l,transition:c}=t;return!c||!r||a!==o&&s===i?!1:n?!0:i!==s&&e===l},_S={duration:200,easing:"ease"},Gh="transform",ES=Fn.Transition.toString({property:Gh,duration:0,easing:"linear"}),MS={roleDescription:"sortable"};function NS(t){let{disabled:e,index:n,node:r,rect:s}=t;const[o,i]=v.useState(null),a=v.useRef(n);return _n(()=>{if(!e&&n!==a.current&&r.current){const l=s.current;if(l){const c=ts(r.current,{ignoreTransform:!0}),d={x:l.left-c.left,y:l.top-c.top,scaleX:l.width/c.width,scaleY:l.height/c.height};(d.x||d.y)&&i(d)}}n!==a.current&&(a.current=n)},[e,n,r,s]),v.useEffect(()=>{o&&i(null)},[o]),o}function Ii(t){let{animateLayoutChanges:e=Wh,attributes:n,disabled:r,data:s,getNewIndex:o=jh,id:i,strategy:a,resizeObserverConfig:l,transition:c=_S}=t;const{items:d,containerId:u,activeIndex:f,disabled:g,disableTransforms:m,sortedRects:p,overIndex:y,useDragOverlay:b,strategy:_}=v.useContext(Bh),x=OS(r,g),E=d.indexOf(i),S=v.useMemo(()=>({sortable:{containerId:u,index:E,items:d},...s}),[u,s,E,d]),w=v.useMemo(()=>d.slice(d.indexOf(i)),[d,i]),{rect:N,node:C,isOver:k,setNodeRef:M}=cS({id:i,data:S,disabled:x.droppable,resizeObserverConfig:{updateMeasurementsFor:w,...l}}),{active:T,activatorEvent:L,activeNodeRect:P,attributes:F,setNodeRef:D,listeners:A,isDragging:O,over:V,setActivatorNodeRef:H,transform:W}=iS({id:i,data:S,attributes:{...MS,...n},disabled:x.draggable}),j=Kk(M,D),U=!!T,B=U&&!m&&Oi(f)&&Oi(y),G=!b&&O,z=G&&B?W:null,Y=B?z??(a??_)({rects:p,activeNodeRect:P,activeIndex:f,overIndex:y,index:E}):null,Z=Oi(f)&&Oi(y)?o({id:i,items:d,activeIndex:f,overIndex:y}):E,ee=T?.id,te=v.useRef({activeId:ee,items:d,newIndex:Z,containerId:u}),ce=d!==te.current.items,J=e({active:T,containerId:u,isDragging:O,isSorting:U,id:i,index:E,items:d,newIndex:te.current.newIndex,previousItems:te.current.items,previousContainerId:te.current.containerId,transition:c,wasDragging:te.current.activeId!=null}),he=NS({disabled:!J,index:E,node:C,rect:N});return v.useEffect(()=>{U&&te.current.newIndex!==Z&&(te.current.newIndex=Z),u!==te.current.containerId&&(te.current.containerId=u),d!==te.current.items&&(te.current.items=d)},[U,Z,u,d]),v.useEffect(()=>{if(ee===te.current.activeId)return;if(ee!=null&&te.current.activeId==null){te.current.activeId=ee;return}const ae=setTimeout(()=>{te.current.activeId=ee},50);return()=>clearTimeout(ae)},[ee]),{active:T,activeIndex:f,attributes:F,data:S,rect:N,index:E,newIndex:Z,items:d,isOver:k,isSorting:U,isDragging:O,listeners:A,node:C,overIndex:y,over:V,setNodeRef:j,setActivatorNodeRef:H,setDroppableNodeRef:M,setDraggableNodeRef:D,transform:he??Y,transition:le()};function le(){if(he||ce&&te.current.newIndex===E)return ES;if(!(G&&!wi(L)||!c)&&(U||J))return Fn.Transition.toString({...c,property:Gh})}}function OS(t,e){var n,r;return typeof t=="boolean"?{draggable:t,droppable:!1}:{draggable:(n=t?.draggable)!=null?n:e.draggable,droppable:(r=t?.droppable)!=null?r:e.droppable}}function to(t){if(!t)return!1;const e=t.data.current;return!!(e&&"sortable"in e&&typeof e.sortable=="object"&&"containerId"in e.sortable&&"items"in e.sortable&&"index"in e.sortable)}const TS=[Ye.Down,Ye.Right,Ye.Up,Ye.Left],Li=(t,e)=>{let{context:{active:n,collisionRect:r,droppableRects:s,droppableContainers:o,over:i,scrollableAncestors:a}}=e;if(TS.includes(t.code)){if(t.preventDefault(),!n||!r)return;const l=[];o.getEnabled().forEach(u=>{if(!u||u!=null&&u.disabled)return;const f=s.get(u.id);if(f)switch(t.code){case Ye.Down:r.top<f.top&&l.push(u);break;case Ye.Up:r.top>f.top&&l.push(u);break;case Ye.Left:r.left>f.left&&l.push(u);break;case Ye.Right:r.left<f.left&&l.push(u);break}});const c=dC({collisionRect:r,droppableRects:s,droppableContainers:l});let d=fh(c,"id");if(d===i?.id&&c.length>1&&(d=c[1].id),d!=null){const u=o.get(n.id),f=o.get(d),g=f?s.get(f.id):null,m=f?.node.current;if(m&&g&&u&&f){const y=Si(m).some((w,N)=>a[N]!==w),b=Yh(u,f),_=AS(u,f),x=y||!b?{x:0,y:0}:{x:_?r.width-g.width:0,y:_?r.height-g.height:0},E={x:g.left,y:g.top};return x.x&&x.y?E:Xs(E,x)}}}};function Yh(t,e){return!to(t)||!to(e)?!1:t.data.current.sortable.containerId===e.data.current.sortable.containerId}function AS(t,e){return!to(t)||!to(e)||!Yh(t,e)?!1:t.data.current.sortable.index<e.data.current.sortable.index}const DS=Object.freeze(Object.defineProperty({__proto__:null,SortableContext:Di,arrayMove:ns,defaultAnimateLayoutChanges:Wh,defaultNewIndexGetter:jh,hasSortableData:to,rectSortingStrategy:Ti,sortableKeyboardCoordinates:Li,useSortable:Ii,verticalListSortingStrategy:nc},Symbol.toStringTag,{value:"Module"})),IS=()=>window.G7Core,Xh=({item:t,isSelected:e=!1,isExpanded:n=!1,level:r=0,onClick:s,onToggle:o,onExpandToggle:i,className:a="",selectedClassName:l="bg-blue-50 dark:bg-blue-900/30 border-blue-500",defaultClassName:c="bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600",dragHandleClassName:d="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300",iconContainerClassName:u="bg-gray-100 dark:bg-gray-700",contentClassName:f="",enableDrag:g=!0,toggleDisabled:m=!1})=>{const{attributes:p,listeners:y,setNodeRef:b,transform:_,transition:x,isDragging:E}=Ii({id:t.id}),S={transform:Fn.Transform.toString(_),transition:x,opacity:E?.5:1},w=r*24+12,C=`flex items-center gap-2 cursor-pointer transition-colors ${e?l:c}`,k=IS()?.style?.mergeClasses?.(C,a)??`${C} ${a}`.trim();return h.jsxs(I,{ref:b,style:S,className:k,onClick:s,children:[g&&h.jsx(I,{...p,...y,className:`flex-shrink-0 cursor-grab active:cursor-grabbing p-1 ${d}`,style:{paddingLeft:`${w}px`},children:h.jsx(re,{name:"grip-vertical",className:"w-4 h-4"})}),!g&&r>0&&h.jsx(I,{style:{paddingLeft:`${w}px`}}),g&&h.jsx(h.Fragment,{children:t.hasChildren?h.jsx(oe,{type:"button",onClick:M=>{M.stopPropagation(),i?.()},className:`flex-shrink-0 p-1 ${d}`,children:h.jsx(re,{name:n?"chevron-down":"chevron-right",className:"w-4 h-4"})}):h.jsx(I,{className:"flex-shrink-0 w-6"})}),h.jsx(I,{className:`flex-shrink-0 w-8 h-8 flex items-center justify-center rounded ${u}`,children:h.jsx(re,{name:t.icon||"file",className:"w-4 h-4 text-gray-600 dark:text-gray-400"})}),h.jsxs(I,{className:`flex-1 min-w-0 ${f}`,children:[h.jsxs(I,{className:"flex items-center gap-1",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-900 dark:text-white truncate",children:t.name}),t.isModuleMenu&&h.jsx(re,{name:"cube",className:"w-2 h-2 text-gray-300 dark:text-gray-500 ml-2",title:t.moduleName})]}),h.jsx(Q,{className:"text-xs text-gray-500 dark:text-gray-400 truncate block",children:t.url})]}),h.jsx(I,{className:"flex-shrink-0",onClick:M=>M.stopPropagation(),children:h.jsx(Wa,{checked:t.isActive,onChange:()=>o?.(new MouseEvent("click")),size:"sm",disabled:m})})]})},LS=()=>window.G7Core;function Kh(t,e,n=0,r=null){const s=[];for(const o of t)s.push({item:o,depth:n,parentId:r}),o.children?.length>0&&e.has(o.id)&&s.push(...Kh(o.children,e,n+1,o.id));return s}function rc(t,e){for(const n of t){if(n.id===e)return n;if(n.children){const r=rc(n.children,e);if(r)return r}}return null}function qh(t){const e=[];if(t.children)for(const n of t.children)e.push(n.id),e.push(...qh(n));return e}function Zh(t,e){return t.filter(n=>n.id!==e).map(n=>n.children?.length>0?{...n,children:Zh(n.children,e)}:n)}function Qh(t,e,n,r){if(n===null){const s=[...t];return s.splice(r,0,e),s}return t.map(s=>{if(s.id===n){const o=[...s.children||[]];return o.splice(r,0,e),{...s,children:o}}return s.children?.length>0?{...s,children:Qh(s.children,e,n,r)}:s})}function Jh(t,e,n,r){if(e===null){const s=t.findIndex(i=>i.id===n),o=t.findIndex(i=>i.id===r);return s===-1||o===-1?t:ns(t,s,o)}return t.map(s=>{if(s.id===e&&s.children){const o=s.children.findIndex(a=>a.id===n),i=s.children.findIndex(a=>a.id===r);return o===-1||i===-1?s:{...s,children:ns(s.children,o,i)}}return s.children?.length>0?{...s,children:Jh(s.children,e,n,r)}:s})}function Rh(t){const e=t.map((s,o)=>({id:s.id,order:o+1})),n={},r=s=>{s.forEach(o=>{o.children?.length>0&&(n[o.id]=o.children.map((i,a)=>({id:i.id,order:a+1})),r(o.children))})};return r(t),{parent_menus:e,child_menus:n}}const PS=({items:t=[],selectedId:e,onSelect:n,onOrderChange:r,onToggleStatus:s,title:o,className:i="",listClassName:a="",childrenContainerClassName:l="bg-gray-50 dark:bg-gray-900/50",itemClassName:c="py-2.5 px-3 mx-4 my-2 rounded-lg border",itemSelectedClassName:d,itemDefaultClassName:u,titleClassName:f="px-4 py-3 border-b border-gray-200 dark:border-gray-700",enableDrag:g=!0,toggleDisabled:m=!1,id:p,editorAttrs:y})=>{const[b,_]=v.useState(new Set),[x,E]=v.useState(null),[S,w]=v.useState(t),N=v.useRef(!1),C=v.useRef(""),k=v.useRef(""),M=v.useCallback(B=>JSON.stringify(B),[]),T=v.useCallback(B=>B.map(G=>{const z=G.children?.map(X=>X.id).join(",")||"";return`${G.id}:${z}`}).join("|"),[]);v.useEffect(()=>{const B=T(t),G=M(t);if(N.current){const z=T(S);(B===z||B!==C.current)&&(N.current=!1,C.current=B,k.current=G,(B!==z||G!==M(S))&&w(t));return}G!==k.current&&(C.current=B,k.current=G,w(t))},[t,T,M,S]);const L=Wl(es(Zs,{activationConstraint:{distance:8}}),es(qs,{coordinateGetter:Li})),P=v.useCallback(B=>{if(typeof B=="string")return B;const z=LS()?.locale?.current?.()||"ko";return B[z]||B.en||Object.values(B)[0]||""},[]),F=v.useCallback(B=>{_(G=>{const z=new Set(G);return z.has(B)?z.delete(B):z.add(B),z})},[]),D=v.useCallback(B=>{n?.(B)},[n]),A=v.useCallback((B,G,z)=>{z.stopPropagation(),s?.(B,G)},[s]),O=B=>{E(B.active.id)},V=B=>{const{active:G,over:z}=B;if(E(null),!z||G.id===z.id)return;const X=W.find(Z=>Z.item.id===Number(G.id)),Y=W.find(Z=>Z.item.id===Number(z.id));if(!(!X||!Y))if(X.parentId===Y.parentId){const Z=Jh(S,X.parentId,Number(G.id),Number(z.id));w(Z),N.current=!0,r?.(Rh(Z))}else{const Z=X.item;if(qh(Z).includes(Number(z.id)))return;let te=Zh(S,Z.id);const ce=W.findIndex(pe=>pe.item.id===Number(G.id)),J=W.findIndex(pe=>pe.item.id===Number(z.id)),he=ce>J,le=Y.parentId;let ae;if(le===null)ae=te.findIndex(pe=>pe.id===Number(z.id)),ae===-1?ae=te.length:he||(ae+=1);else{const we=rc(te,le)?.children||[];ae=we.findIndex(He=>He.id===Number(z.id)),ae===-1?ae=we.length:he||(ae+=1)}const Se={...Z,parent_id:le,children:Z.children||[]};te=Qh(te,Se,le,ae),w(te),N.current=!0;const ue=Rh(te);ue.moved_items=[{id:Z.id,new_parent_id:le}],r?.(ue)}},H=B=>({id:B.id,name:P(B.name),slug:B.slug,url:B.url,icon:B.icon,isActive:B.is_active,isModuleMenu:!!B.module_id,moduleName:B.module?P(B.module.name):void 0,hasChildren:B.children&&B.children.length>0}),W=v.useMemo(()=>Kh(S,b),[S,b]),j=v.useMemo(()=>W.map(B=>B.item.id),[W]),U=x?rc(S,Number(x)):null;return h.jsxs(I,{className:i,id:p,...y,children:[o&&h.jsx(I,{className:f,children:h.jsx($r,{className:"text-sm font-medium text-gray-900 dark:text-white",children:o})}),h.jsxs(tc,{sensors:L,collisionDetection:Gl,onDragStart:O,onDragEnd:V,children:[h.jsx(Di,{items:j,strategy:nc,children:h.jsx(I,{className:a,children:W.map(B=>h.jsx(I,{className:B.depth>0?l:"",children:h.jsx(Xh,{item:H(B.item),isSelected:e===B.item.id,isExpanded:b.has(B.item.id),level:B.depth,onClick:()=>D(B.item),onToggle:G=>A(B.item.id,B.item.is_active,G),onExpandToggle:()=>F(B.item.id),className:c,selectedClassName:d,defaultClassName:u,enableDrag:g,toggleDisabled:m})},B.item.id))})}),h.jsx(xS,{children:U?h.jsx(I,{className:"bg-white dark:bg-gray-800 shadow-lg rounded-lg border border-blue-500 p-3",children:h.jsx(I,{className:"flex items-center gap-2",children:h.jsx(I,{className:"text-sm font-medium text-gray-900 dark:text-white",children:P(U.name)})})}):null})]})]})},$S=[{value:"LayoutDashboard",label:"LayoutDashboard",faIcon:"tachometer-alt"},{value:"Settings",label:"Settings",faIcon:"cog"},{value:"Menu",label:"Menu",faIcon:"bars"},{value:"Users",label:"Users",faIcon:"users"},{value:"Package",label:"Package",faIcon:"cube"},{value:"Puzzle",label:"Puzzle",faIcon:"puzzle-piece"},{value:"FileText",label:"FileText",faIcon:"file-alt"},{value:"ShoppingCart",label:"ShoppingCart",faIcon:"shopping-cart"},{value:"MessageSquare",label:"MessageSquare",faIcon:"comment"},{value:"Home",label:"Home",faIcon:"home"},{value:"User",label:"User",faIcon:"user"},{value:"Search",label:"Search",faIcon:"search"},{value:"Bell",label:"Bell",faIcon:"bell"},{value:"Mail",label:"Mail",faIcon:"envelope"},{value:"Calendar",label:"Calendar",faIcon:"calendar"},{value:"Chart",label:"Chart",faIcon:"chart-bar"},{value:"Database",label:"Database",faIcon:"database"},{value:"Lock",label:"Lock",faIcon:"lock"},{value:"Globe",label:"Globe",faIcon:"globe"},{value:"Image",label:"Image",faIcon:"image"}],FS=({value:t,onChange:e,options:n=$S,placeholder:r,searchPlaceholder:s,noResultsText:o,className:i="",disabled:a=!1,name:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),[g,m]=v.useState(""),p=v.useRef(null),y=n.find(x=>{if(!t)return!1;if(t.includes("fa-")){const E=t.replace(/^(fas|far|fab|fal|fad)\s+fa-/,"");return x.faIcon===E||x.faIcon===t}return x.value===t||x.faIcon===t}),b=n.filter(x=>x.label.toLowerCase().includes(g.toLowerCase()));v.useEffect(()=>{const x=E=>{p.current&&!p.current.contains(E.target)&&(f(!1),m(""))};if(u)return document.addEventListener("mousedown",x),()=>document.removeEventListener("mousedown",x)},[u]);const _=x=>{const E=`fas fa-${x.faIcon}`;e?.(E),f(!1),m("")};return h.jsxs(I,{ref:p,className:`relative ${i}`,id:c,...d,children:[h.jsxs(oe,{type:"button",onClick:()=>!a&&f(!u),disabled:a,className:` w-full flex items-center justify-between px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg text-left text-sm ${a?"opacity-50 cursor-not-allowed":"hover:border-gray-400 dark:hover:border-gray-500"} focus:outline-none focus:ring-2 focus:ring-blue-500 - `,children:[h.jsx(I,{className:"flex items-center gap-2",children:y?h.jsxs(h.Fragment,{children:[h.jsx(oe,{name:y.faIcon,className:"w-4 h-4 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:y.label})]}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:r})}),h.jsx(oe,{name:"chevron-down",className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${u?"rotate-180":""}`})]}),l&&h.jsx("input",{type:"hidden",name:l,value:e||""}),u&&h.jsxs(I,{className:"absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-72 overflow-hidden",children:[h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-700",children:h.jsx(St,{type:"text",value:g,onChange:x=>m(x.target.value),placeholder:s,className:"w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white",autoFocus:!0})}),h.jsx(I,{className:"max-h-56 overflow-y-auto",children:b.length>0?b.map(x=>h.jsxs(ae,{type:"button",onClick:()=>_(x),className:` + `,children:[h.jsx(I,{className:"flex items-center gap-2",children:y?h.jsxs(h.Fragment,{children:[h.jsx(re,{name:y.faIcon,className:"w-4 h-4 text-gray-600 dark:text-gray-400"}),h.jsx(Q,{className:"text-gray-900 dark:text-white",children:y.label})]}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:r})}),h.jsx(re,{name:"chevron-down",className:`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform ${u?"rotate-180":""}`})]}),l&&h.jsx("input",{type:"hidden",name:l,value:t||""}),u&&h.jsxs(I,{className:"absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-72 overflow-hidden",children:[h.jsx(I,{className:"p-2 border-b border-gray-200 dark:border-gray-700",children:h.jsx(wt,{type:"text",value:g,onChange:x=>m(x.target.value),placeholder:s,className:"w-full px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 text-gray-900 dark:text-white",autoFocus:!0})}),h.jsx(I,{className:"max-h-56 overflow-y-auto",children:b.length>0?b.map(x=>h.jsxs(oe,{type:"button",onClick:()=>_(x),className:` w-full flex items-center gap-3 px-3 py-2 text-left text-sm ${y?.faIcon===x.faIcon?"bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"} - `,children:[h.jsx(oe,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(Q,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(oe,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},uc=({defaultOpen:e=!1,isOpen:t,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(e),u=t!==void 0,f=u?t:c,g=v.useRef(null),[m,p]=v.useState(e?void 0:0),y=()=>{if(i)return;const _=!f;u||d(_),n?.(_)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const _=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(_)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,_=>{if(v.isValidElement(_)){const x=_.props?.slot;x==="trigger"?b.trigger=_:x==="content"&&(b.content=_)}}),!b.trigger&&!b.content){const _=v.Children.toArray(o);_.length>=2?(b.trigger=_[0],b.content=_.slice(1)):_.length===1&&(b.content=_[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(ae,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},ho=window.G7Core,dg=ho?.useControllableState,ug=(e,t)=>window.G7Core?.t?.(e,t)??e,r_={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},s_=({options:e,value:t,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=e.find(u=>u.value===t)??e[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(Q,{children:c?.label??""}),h.jsx(oe,{name:de.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:e.map(u=>{const f=u.value??"",g=r_[f],m=g?ug(g):"",p=u.value===t;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(Q,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(oe,{name:de.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},o_=(e,t)=>{if(e.length!==t.length)return!1;const n=[...e].sort((s,o)=>s.id-o.id),r=[...t].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},i_=({data:e=[],value:t=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,installedModules:a=[],installedPlugins:l=[],id:c,editorAttrs:d})=>{const u=v.useCallback(E=>{if(n){const D=ho?.createChangeEvent?ho.createChangeEvent({value:E}):{target:{value:E}};n(D)}},[n]),[f,g]=dg?dg(t,[],u,{isEqual:o_}):(()=>{const[E,D]=v.useState(t),P=v.useRef(n);P.current=n;const L=v.useRef(!1);v.useEffect(()=>{if(L.current){L.current=!1;return}D(t)},[t]);const $=v.useCallback(A=>{L.current=!0,D(O=>{const T=typeof A=="function"?A(O):A;if(P.current){const z=ho?.createChangeEvent?ho.createChangeEvent({value:T}):{target:{value:T}};P.current(z)}return T})},[]);return[E,$]})(),m=v.useMemo(()=>new Set(f.map(E=>E.id)),[f]),p=v.useCallback(E=>E.is_assignable?[E.id]:E.children.flatMap(D=>p(D)),[]),y=v.useCallback(E=>{const D=p(E);return D.length>0&&D.every(P=>m.has(P))},[m,p]),b=v.useCallback(E=>{const D=p(E),P=D.filter(L=>m.has(L)).length;return P>0&&P<D.length},[m,p]),_=v.useCallback(E=>p(E).filter(P=>m.has(P)).length,[m,p]),x=v.useCallback((E,D)=>{o||g(P=>D?[...P,{id:E,scope_type:null}]:P.filter(L=>L.id!==E))},[o,g]),M=v.useCallback((E,D)=>{if(o)return;const P=p(E),L=new Set(P);g($=>{if(D){const A=new Set($.map(T=>T.id)),O=P.filter(T=>!A.has(T)).map(T=>({id:T,scope_type:null}));return[...$,...O]}else return $.filter(A=>!L.has(A.id))})},[o,p,g]),C=v.useCallback((E,D)=>{o||g(P=>P.map(L=>L.id===E?{...L,scope_type:D}:L))},[o,g]),k=E=>{const D=m.has(E.id),P=!!E.resource_route_key,L=D&&P&&r.length>0,$=f.find(A=>A.id===E.id)?.scope_type??null;return h.jsxs(ln,{className:`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${D?"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20":"border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"} ${o?"opacity-50 cursor-not-allowed":""}`,children:[h.jsx(St,{type:"checkbox",checked:D,onChange:A=>{A.stopPropagation(),x(E.id,A.target.checked)},disabled:o,className:"w-4 h-4 mt-0.5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsxs(I,{className:"flex items-center justify-between gap-2",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-900 dark:text-white",children:E.name}),L&&h.jsx(s_,{options:r,value:$,onChange:A=>C(E.id,A),disabled:o})]}),E.description&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:E.description})]})]},E.id)},N=(E,D)=>{const P=y(E),L=b(E),$=_(E),A=E.children.filter(z=>z.is_assignable);if(A.length===0&&E.children.length===0)return null;const O=E.children.some(z=>!z.is_assignable),T=i===1?"grid-cols-1":i===3?"grid-cols-1 md:grid-cols-3":"grid-cols-1 md:grid-cols-2";return h.jsxs(uc,{defaultOpen:!1,className:"border-0 rounded-none",children:[h.jsxs(I,{slot:"trigger",className:"flex items-center gap-3 w-full py-2",children:[h.jsx("input",{type:"checkbox",checked:P,ref:z=>{z&&(z.indeterminate=L)},onChange:z=>{z.stopPropagation(),M(E,z.target.checked)},onClick:z=>z.stopPropagation(),onMouseDown:z=>z.stopPropagation(),disabled:o,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(oe,{name:de.Folder,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:E.name}),h.jsxs(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 ml-1",children:[$,"/",E.leaf_count]}),h.jsx(oe,{name:de.ChevronDown,className:"w-4 h-4 text-gray-400 dark:text-gray-500 ml-auto"})]}),h.jsx(I,{slot:"content",className:`pl-8 grid ${T} gap-2 py-2`,children:O?E.children.map(z=>z.is_assignable?k(z):N(z)):A.map(z=>k(z))})]},E.id)},S=E=>{if(E.extension_type==="core")return"core";if(E.extension_type==="module")return"module";if(E.extension_type==="plugin")return"plugin";const D=E.extension_identifier??E.module??"";return D==="core"||!D?"core":l?.some(L=>L.identifier===D)?"plugin":"module"},w=E=>{const D=y(E),P=b(E),L=_(E),$=S(E);return h.jsxs(uc,{defaultOpen:!1,className:"mb-2",children:[h.jsxs(I,{slot:"trigger",className:"flex items-center gap-3 w-full py-2",children:[h.jsx("input",{type:"checkbox",checked:D,ref:A=>{A&&(A.indeterminate=P)},onChange:A=>{A.stopPropagation(),M(E,A.target.checked)},onClick:A=>A.stopPropagation(),onMouseDown:A=>A.stopPropagation(),disabled:o,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(oe,{name:$==="core"?de.Cog:de.Cube,className:"w-5 h-5 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white",children:E.name}),$!=="core"&&h.jsx(gh,{type:$,identifier:E.extension_identifier??E.module,installedModules:a,installedPlugins:l}),h.jsxs(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 ml-2",children:[L,"/",E.leaf_count]}),h.jsx(oe,{name:de.ChevronDown,className:"w-4 h-4 text-gray-400 dark:text-gray-500 ml-auto"})]}),h.jsx(I,{slot:"content",className:"pl-8 space-y-2 pb-2",children:E.children.map(A=>A.is_assignable?k(A):N(A))})]},E.id)};return e.length===0?h.jsx(I,{className:`text-sm text-gray-400 dark:text-gray-500 py-4 text-center ${s}`,id:c,...d,children:ug("common.no_data")??"데이터가 없습니다."}):h.jsx(I,{className:`space-y-2 ${s}`,id:c,...d,children:e.map(E=>w(E))})};function a_({text:e,keyword:t}){if(!t||!e)return h.jsx(h.Fragment,{children:e});const n=t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=e.split(new RegExp(`(${n})`,"gi"));return h.jsx(h.Fragment,{children:r.map((s,o)=>s.toLowerCase()===t.toLowerCase()?h.jsx(Q,{className:"bg-yellow-200 dark:bg-yellow-700 rounded px-0.5",children:s},o):s)})}const l_=({data:e=[],expandedIds:t=[],selectedIds:n=[],searchKeyword:r="",showProductCount:s=!1,selectable:o=!1,className:i="",onToggle:a,onSelectionChange:l,id:c,editorAttrs:d})=>{const u=v.useCallback(y=>{if(!a)return;const b=t.includes(y)?t.filter(_=>_!==y):[...t,y];a(b)},[t,a]),f=v.useCallback((y,b)=>{if(!l)return;const _=b?[...n,y]:n.filter(x=>x!==y);l(_)},[n,l]),g=v.useMemo(()=>{if(!r)return null;const y=new Set,b=_=>{for(const x of _)(x.localized_name||x.name).toLowerCase().includes(r.toLowerCase())&&y.add(x.id),x.children&&b(x.children)};return b(e),y},[e,r]),m=v.useCallback(y=>!g||g.has(y.id)?!0:(y.children??[]).some(b=>m(b)),[g]),p=(y,b=0)=>{const _=(y.children??[]).length>0,x=t.includes(y.id),M=n.includes(y.id),C=y.localized_name||y.name;return r&&!m(y)?null:h.jsxs(I,{"data-testid":`category-node-${y.id}`,children:[h.jsxs(I,{className:`flex items-center gap-2 py-1.5 px-2 rounded cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${b>0?"ml-"+Math.min(b*5,20):""}`,style:b>0?{marginLeft:`${b*20}px`}:void 0,children:[_?h.jsx(I,{className:"flex-shrink-0 w-5 h-5 flex items-center justify-center text-gray-400 dark:text-gray-500",onClick:()=>u(y.id),"data-testid":`toggle-${y.id}`,children:h.jsx(oe,{name:x?de.CaretDown:de.CaretRight,className:"text-xs"})}):h.jsx(I,{className:"flex-shrink-0 w-5 h-5"}),o&&h.jsx(En,{checked:M,onChange:k=>{k.stopPropagation(),f(y.id,k.target.checked)},className:"flex-shrink-0","data-testid":`checkbox-${y.id}`}),h.jsx(oe,{name:de.Folder,className:`flex-shrink-0 text-sm ${_?"text-yellow-500 dark:text-yellow-400":"text-gray-400 dark:text-gray-500"}`}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300 select-none",onClick:()=>_&&u(y.id),children:h.jsx(a_,{text:C,keyword:r})}),s&&y.products_count!==void 0&&h.jsx(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400 ml-1",children:y.products_count})]}),_&&x&&h.jsx(I,{"data-testid":`children-${y.id}`,children:(y.children??[]).map(k=>p(k,b+1))})]},y.id)};return e.length===0?h.jsx(I,{className:`text-sm text-gray-400 dark:text-gray-500 py-4 text-center ${i}`,id:c,...d,children:"데이터가 없습니다."}):h.jsx(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg p-2 max-h-80 overflow-y-auto ${i}`,id:c,...d,children:e.map(y=>p(y,0))})},fg="carousel",go="controller",c_="navigation",d_="no-scroll",fc="portal",u_="root",hg="toolbar",f_="counter",po="fullscreen",gg="slideshow",mo="thumbnails",pg="zoom",gs="loading",hc="playing",bo="error",yo="complete",h_="placeholder",vo=e=>`active-slide-${e}`,g_=vo(gs),p_=vo(hc),m_=vo(bo),b_=vo(yo),mg="fullsize",ps="flex_center",y_="no_scroll",bg="no_scroll_padding",gc="slide",yg="slide_wrapper",v_="slide_wrapper_interactive",fr="prev",hr="next",pc="swipe",ms="close",vg="onPointerDown",xg="onPointerMove",wg="onPointerUp",kg="onPointerLeave",Cg="onPointerCancel",mc="onKeyDown",x_="onKeyUp",bc="onWheel",w_="Escape",k_="ArrowLeft",C_="ArrowRight",S_="button",ji="icon",Sg="contain",_g="cover",Eg="Unknown action type",Mg="yarl__";function Wt(...e){return e.filter(Boolean).join(" ")}function Te(e){return`${Mg}${e}`}function ht(e){return`--${Mg}${e}`}function _r(e,t){return`${e}${t?`_${t}`:""}`}function Er(e){return t=>_r(e,t)}function Mr(e,t){var n;return(n=e?.[t])!==null&&n!==void 0?n:t}function Ng(e,t,n){return Mr(e,"{index} of {total}").replace(/\{index}/g,`${wc(n,t.length)+1}`).replace(/\{total}/g,`${t.length}`)}function bs(...e){return()=>{e.forEach(t=>{t()})}}function un(e,t,n){return()=>{const r=F.useContext(n);if(!r)throw new Error(`${e} must be used within a ${t}.Provider`);return r}}function yc(){return typeof window<"u"}function vc(e,t=0){const n=10**t;return Math.round((e+Number.EPSILON)*n)/n}function gr(e){return e.type===void 0||e.type==="image"}function xc(e,t){return e.imageFit===_g||e.imageFit!==Sg&&t===_g}function Wi(e){return typeof e=="string"?Number.parseInt(e,10):e}function Gi(e){if(typeof e=="number")return{pixel:e};if(typeof e=="string"){const t=Wi(e);return e.endsWith("%")?{percent:t}:{pixel:t}}return{pixel:0}}function __(e,t){const n=Gi(t),r=n.percent!==void 0?e.width/100*n.percent:n.pixel;return{width:Math.max(e.width-2*r,0),height:Math.max(e.height-2*r,0)}}function E_(){return(yc()?window?.devicePixelRatio:void 0)||1}function wc(e,t){return t>0?(e%t+t)%t:0}function kc(e){return e.length>0}function Cc(e,t){return e[wc(t,e.length)]}function Sc(e,t){return kc(e)?Cc(e,t):void 0}function Tg(e){return gr(e)?e.src:void 0}function Yi(e,t,n){if(!n)return e;const{buttons:r,...s}=e,o=r.findIndex(a=>a===t),i=F.isValidElement(n)?F.cloneElement(n,{key:t},null):n;if(o>=0){const a=[...r];return a.splice(o,1,i),{buttons:a,...s}}return{buttons:[i,...r],...s}}function Og(e,t,n=0){return Math.min(e.preload,Math.max(e.finite?t.length-1:Math.floor(t.length/2),n))}const M_=Number(F.version.split(".")[0])>=19;function N_(e){return{inert:M_?e:e?"":void 0}}function T_(e){e.scrollTop}const _c={open:!1,close:()=>{},index:0,slides:[],render:{},plugins:[],toolbar:{buttons:[ms]},labels:{},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},carousel:{finite:!1,preload:2,padding:"16px",spacing:"30%",imageFit:Sg,imageProps:{}},controller:{ref:null,focus:!0,aria:!1,touchAction:"none",closeOnPullUp:!1,closeOnPullDown:!1,closeOnBackdropClick:!1,closeOnEscape:!0,preventDefaultWheelX:!0,preventDefaultWheelY:!1,disableSwipeNavigation:!1},portal:{},noScroll:{disabled:!1},on:{},styles:{},className:""};function pn(e,t){return{name:e,component:t}}function It(e,t){return{module:e,children:t}}function Ag(e,t,n){return e.module.name===t?n(e):e.children?[It(e.module,e.children.flatMap(r=>{var s;return(s=Ag(r,t,n))!==null&&s!==void 0?s:[]}))]:[e]}function ys(e,t,n){return e.flatMap(r=>{var s;return(s=Ag(r,t,n))!==null&&s!==void 0?s:[]})}function O_(e,t=[],n=[]){let r=e;const s=g=>{const m=[...r];for(;m.length>0;){const p=m.pop();if(p?.module.name===g)return!0;p?.children&&m.push(...p.children)}return!1},o=(g,m)=>{if(g===""){r=[It(m,r)];return}r=ys(r,g,p=>[It(m,[p])])},i=(g,m)=>{r=ys(r,g,p=>[It(p.module,[It(m,p.children)])])},a=(g,m,p)=>{r=ys(r,g,y=>{var b;return[It(y.module,[...p?[It(m)]:[],...(b=y.children)!==null&&b!==void 0?b:[],...p?[]:[It(m)]])]})},l=(g,m,p)=>{r=ys(r,g,y=>[...p?[It(m)]:[],y,...p?[]:[It(m)]])},c=g=>{i(go,g)},d=(g,m)=>{r=ys(r,g,p=>[It(m,p.children)])},u=g=>{r=ys(r,g,m=>m.children)},f=g=>{n.push(g)};return t.forEach(g=>{g({contains:s,addParent:o,append:i,addChild:a,addSibling:l,addModule:c,replace:d,remove:u,augment:f})}),{config:r,augmentation:g=>n.reduce((m,p)=>p(m),g)}}const Dg=F.createContext(null),Ec=un("useA11yContext","A11yContext",Dg);function A_({children:e}){const[t,n]=F.useState(!1),[r,s]=F.useState(!1),o=F.useMemo(()=>({focusWithin:t,trackFocusWithin:(a,l)=>{const c=d=>u=>{var f;u.currentTarget.contains(u.relatedTarget)||n(d),(f=d?a:l)===null||f===void 0||f(u)};return{onFocus:c(!0),onBlur:c(!1)}},autoPlaying:r,setAutoPlaying:s}),[t,r]);return F.createElement(Dg.Provider,{value:o},e)}const Ig=F.createContext(null),vs=un("useDocument","DocumentContext",Ig);function D_({nodeRef:e,children:t}){const n=F.useMemo(()=>{const r=o=>{var i;return((i=o||e.current)===null||i===void 0?void 0:i.ownerDocument)||document};return{getOwnerDocument:r,getOwnerWindow:o=>{var i;return((i=r(o))===null||i===void 0?void 0:i.defaultView)||window}}},[e]);return F.createElement(Ig.Provider,{value:n},t)}const Lg=F.createContext(null),xs=un("useEvents","EventsContext",Lg);function I_({children:e}){const[t]=F.useState({});F.useEffect(()=>()=>{Object.keys(t).forEach(r=>delete t[r])},[t]);const n=F.useMemo(()=>{const r=(i,a)=>{var l;(l=t[i])===null||l===void 0||l.splice(0,t[i].length,...t[i].filter(c=>c!==a))};return{publish:(...[i,a])=>{var l;(l=t[i])===null||l===void 0||l.forEach(c=>c(a))},subscribe:(i,a)=>(t[i]||(t[i]=[]),t[i].push(a),()=>r(i,a)),unsubscribe:r}},[t]);return F.createElement(Lg.Provider,{value:n},e)}const Pg=F.createContext(null),Lt=un("useLightboxProps","LightboxPropsContext",Pg);function $g({children:e,...t}){return F.createElement(Pg.Provider,{value:t},e)}const Fg=F.createContext(null),mn=un("useLightboxState","LightboxStateContext",Fg),zg=F.createContext(null),L_=un("useLightboxDispatch","LightboxDispatchContext",zg);function P_(e,t){switch(t.type){case"swipe":{const{slides:n}=e,r=t?.increment||0,s=e.globalIndex+r,o=wc(s,n.length),i=Sc(n,o),a=r||t.duration!==void 0?{increment:r,duration:t.duration,easing:t.easing}:void 0;return{slides:n,currentIndex:o,globalIndex:s,currentSlide:i,animation:a}}case"update":return t.slides!==e.slides||t.index!==e.currentIndex?{slides:t.slides,currentIndex:t.index,globalIndex:t.index,currentSlide:Sc(t.slides,t.index)}:e;default:throw new Error(Eg)}}function $_({slides:e,index:t,children:n}){const[r,s]=F.useReducer(P_,{slides:e,currentIndex:t,globalIndex:t,currentSlide:Sc(e,t)}),[o,i]=F.useState(e),[a,l]=F.useState(t);(e!==o||t!==a)&&(i(e),l(t),s({type:"update",slides:e,index:t}));const c=F.useMemo(()=>({...r,state:r,dispatch:s}),[r,s]);return F.createElement(zg.Provider,{value:s},F.createElement(Fg.Provider,{value:c},n))}const Vg=F.createContext(null),F_=un("useRTLContext","RTLContext",Vg);function z_({isRTL:e,children:t}){const n=F.useMemo(()=>({isRTL:e}),[e]);return F.createElement(Vg.Provider,{value:n},t)}const Hg=F.createContext(null),xo=un("useTimeouts","TimeoutsContext",Hg);function V_({children:e}){const[t]=F.useState([]);F.useEffect(()=>()=>{t.forEach(r=>window.clearTimeout(r)),t.splice(0,t.length)},[t]);const n=F.useMemo(()=>{const r=i=>{t.splice(0,t.length,...t.filter(a=>a!==i))};return{setTimeout:(i,a)=>{const l=window.setTimeout(()=>{r(l),i()},a);return t.push(l),l},clearTimeout:i=>{i!==void 0&&(r(i),window.clearTimeout(i))}}},[t]);return F.createElement(Hg.Provider,{value:n},e)}const ws=F.forwardRef(function({label:t,className:n,icon:r,renderIcon:s,onClick:o,style:i,...a},l){const{styles:c,labels:d}=Lt(),u=Mr(d,t);return F.createElement("button",{ref:l,type:"button",title:u,"aria-label":u,className:Wt(Te(S_),n),onClick:o,style:{...i,...c.button},...a},s?s():F.createElement(r,{className:Te(ji),style:c.icon}))});function Ug(e,t){const n=r=>F.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24","aria-hidden":"true",focusable:"false",...r},t);return n.displayName=e,n}function nn(e,t){return Ug(e,F.createElement("g",{fill:"currentColor"},F.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t))}function H_(e,t){return Ug(e,F.createElement(F.Fragment,null,F.createElement("defs",null,F.createElement("mask",{id:"strike"},F.createElement("path",{d:"M0 0h24v24H0z",fill:"white"}),F.createElement("path",{d:"M0 0L24 24",stroke:"black",strokeWidth:4}))),F.createElement("path",{d:"M0.70707 2.121320L21.878680 23.292883",stroke:"currentColor",strokeWidth:2}),F.createElement("g",{fill:"currentColor",mask:"url(#strike)"},F.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t)))}const U_=nn("Close",F.createElement("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),B_=nn("Previous",F.createElement("path",{d:"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"})),j_=nn("Next",F.createElement("path",{d:"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"})),W_=nn("Loading",F.createElement(F.Fragment,null,Array.from({length:8}).map((e,t,n)=>F.createElement("line",{key:t,x1:"12",y1:"6.5",x2:"12",y2:"1.8",strokeLinecap:"round",strokeWidth:"2.6",stroke:"currentColor",strokeOpacity:1/n.length*(t+1),transform:`rotate(${360/n.length*t}, 12, 12)`})))),G_=nn("Error",F.createElement("path",{d:"M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z"})),Tn=yc()?F.useLayoutEffect:F.useEffect;function Mc(){const[e,t]=F.useState(!1);return F.useEffect(()=>{var n,r;const s=(n=window.matchMedia)===null||n===void 0?void 0:n.call(window,"(prefers-reduced-motion: reduce)");t(s?.matches);const o=i=>t(i.matches);return(r=s?.addEventListener)===null||r===void 0||r.call(s,"change",o),()=>{var i;return(i=s?.removeEventListener)===null||i===void 0?void 0:i.call(s,"change",o)}},[]),e}function Y_(e){let t=0,n=0,r=0;const o=window.getComputedStyle(e).transform.match(/matrix.*\((.+)\)/);if(o){const i=o[1].split(",").map(Wi);i.length===6?(t=i[4],n=i[5]):i.length===16&&(t=i[12],n=i[13],r=i[14])}return{x:t,y:n,z:r}}function Nc(e,t){const n=F.useRef(void 0),r=F.useRef(void 0),s=Mc();return Tn(()=>{var o,i,a;if(e.current&&n.current!==void 0&&!s){const{keyframes:l,duration:c,easing:d,onfinish:u}=t(n.current,e.current.getBoundingClientRect(),Y_(e.current))||{};if(l&&c){(o=r.current)===null||o===void 0||o.cancel(),r.current=void 0;try{r.current=(a=(i=e.current).animate)===null||a===void 0?void 0:a.call(i,l,{duration:c,easing:d})}catch(f){console.error(f)}r.current&&(r.current.onfinish=()=>{r.current=void 0,u?.()})}}n.current=void 0}),{prepareAnimation:o=>{n.current=o},isAnimationPlaying:()=>{var o;return((o=r.current)===null||o===void 0?void 0:o.playState)==="running"}}}function Bg(){const e=F.useRef(null),t=F.useRef(void 0),[n,r]=F.useState();return{setContainerRef:F.useCallback(o=>{e.current=o,t.current&&(t.current.disconnect(),t.current=void 0);const i=()=>{if(o){const a=window.getComputedStyle(o),l=c=>parseFloat(c)||0;r({width:Math.round(o.clientWidth-l(a.paddingLeft)-l(a.paddingRight)),height:Math.round(o.clientHeight-l(a.paddingTop)-l(a.paddingBottom))})}else r(void 0)};i(),o&&typeof ResizeObserver<"u"&&(t.current=new ResizeObserver(i),t.current.observe(o))},[]),containerRef:e,containerRect:n}}function Xi(){const e=F.useRef(void 0),{setTimeout:t,clearTimeout:n}=xo();return F.useCallback((r,s)=>{n(e.current),e.current=t(r,s>0?s:0)},[t,n])}function Ye(e){const t=F.useRef(e);return Tn(()=>{t.current=e}),F.useCallback((...n)=>{var r;return(r=t.current)===null||r===void 0?void 0:r.call(t,...n)},[])}function jg(e,t){typeof e=="function"?e(t):e&&(e.current=t)}function Tc(e,t){return F.useMemo(()=>e==null&&t==null?null:n=>{jg(e,n),jg(t,n)},[e,t])}function Wg(e,t=!1){const n=F.useRef(!1);Tn(()=>{t&&n.current&&(n.current=!1,e())},[t,e]);const r=F.useCallback(()=>{n.current=!0},[]),s=F.useCallback(()=>{n.current=!1},[]);return{onFocus:r,onBlur:s}}function Ki(){return F_().isRTL}function Gg(){const[e]=F.useState({}),t=F.useCallback((s,o)=>{var i;(i=e[s])===null||i===void 0||i.forEach(a=>{o.isPropagationStopped()||a(o)})},[e]),n=F.useMemo(()=>({onPointerDown:s=>t(vg,s),onPointerMove:s=>t(xg,s),onPointerUp:s=>t(wg,s),onPointerLeave:s=>t(kg,s),onPointerCancel:s=>t(Cg,s),onKeyDown:s=>t(mc,s),onKeyUp:s=>t(x_,s),onWheel:s=>t(bc,s)}),[t]),r=F.useCallback((s,o)=>(e[s]||(e[s]=[]),e[s].unshift(o),()=>{const i=e[s];i&&i.splice(0,i.length,...i.filter(a=>a!==o))}),[e]);return{registerSensors:n,subscribeSensors:r}}function Yg(e,t){const n=F.useRef(0),r=Xi(),s=Ye((...o)=>{n.current=Date.now(),e(...o)});return F.useCallback((...o)=>{r(()=>{s(...o)},t-(Date.now()-n.current))},[t,s,r])}const Oc=Er("slide"),Ac=Er("slide_image");function Nr({slide:e,offset:t,render:n,rect:r,imageFit:s,imageProps:o,onClick:i,onLoad:a,onError:l,style:c}){var d,u,f,g,m,p,y,b;const[_,x]=F.useState(gs),{publish:M}=xs(),{setTimeout:C}=xo(),k=F.useRef(null);F.useEffect(()=>{t===0&&M(vo(_))},[t,_,M]);const N=Ye(U=>{("decode"in U?U.decode():Promise.resolve()).catch(()=>{}).then(()=>{U.parentNode&&(x(yo),C(()=>{a?.(U)},0))})}),S=F.useCallback(U=>{k.current=U,U?.complete&&N(U)},[N]),w=F.useCallback(U=>{N(U.currentTarget)},[N]),E=Ye(()=>{x(bo),l?.()}),D=xc(e,s),P=(U,W)=>Number.isFinite(U)?U:W,L=P(Math.max(...((u=(d=e.srcSet)===null||d===void 0?void 0:d.map(U=>U.width))!==null&&u!==void 0?u:[]).concat(e.width?[e.width]:[]).filter(Boolean)),((f=k.current)===null||f===void 0?void 0:f.naturalWidth)||0),$=P(Math.max(...((m=(g=e.srcSet)===null||g===void 0?void 0:g.map(U=>U.height))!==null&&m!==void 0?m:[]).concat(e.height?[e.height]:[]).filter(Boolean)),((p=k.current)===null||p===void 0?void 0:p.naturalHeight)||0),A=L&&$?{maxWidth:`min(${L}px, 100%)`,maxHeight:`min(${$}px, 100%)`}:{maxWidth:"100%",maxHeight:"100%"},O=(y=e.srcSet)===null||y===void 0?void 0:y.slice().sort((U,W)=>U.width-W.width).map(U=>`${U.src} ${U.width}w`).join(", "),T=()=>r&&!D&&e.width&&e.height?r.height/e.height*e.width:Number.MAX_VALUE,z=O&&r&&yc()?`${Math.round(Math.min(T(),r.width))}px`:void 0,{style:H,className:j,...G}=(typeof o=="function"?o(e):o)||{};return F.createElement(F.Fragment,null,F.createElement("img",{ref:S,onLoad:w,onError:E,onClick:i,draggable:!1,className:Wt(Te(Ac()),D&&Te(Ac("cover")),_!==yo&&Te(Ac("loading")),j),style:{...A,...c,...H},...G,alt:(b=e.alt)!==null&&b!==void 0?b:"",sizes:z,srcSet:O,src:e.src}),_!==yo&&F.createElement("div",{className:Te(Oc(h_))},_===gs&&(n?.iconLoading?n.iconLoading():F.createElement(W_,{className:Wt(Te(ji),Te(Oc(gs)))})),_===bo&&(n?.iconError?n.iconError():F.createElement(G_,{className:Wt(Te(ji),Te(Oc(bo)))}))))}const X_=F.forwardRef(function({className:t,children:n,onFocus:r,onBlur:s,...o},i){const a=F.useRef(null),[l,c]=F.useState(!1),{trackFocusWithin:d}=Ec(),u=Ye(()=>{if(a.current){const f=window.getComputedStyle(a.current).direction==="rtl";f!==l&&c(f)}});return F.useEffect(u),F.createElement(D_,{nodeRef:a},F.createElement(z_,{isRTL:l},F.createElement("div",{ref:Tc(i,a),className:Wt(Te("root"),t),...d(r,s),...o},n)))});var Gt;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL",e[e.ANIMATION=3]="ANIMATION"})(Gt||(Gt={}));function Xg(e,t,n,r,s){F.useEffect(()=>s?()=>{}:bs(e(vg,t),e(xg,n),e(wg,r),e(kg,r),e(Cg,r)),[e,t,n,r,s])}var On;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL"})(On||(On={}));const Dc=30;function K_({disableSwipeNavigation:e,closeOnBackdropClick:t},n,r,s,o,i,a,l,c,d,u,f,g,m,p,y){const b=F.useRef(0),_=F.useRef([]),x=F.useRef(void 0),M=F.useRef(0),C=F.useRef(On.NONE),k=F.useCallback(L=>{x.current===L.pointerId&&(x.current=void 0,C.current=On.NONE);const $=_.current;$.splice(0,$.length,...$.filter(A=>A.pointerId!==L.pointerId))},[]),N=F.useCallback(L=>{k(L),L.persist(),_.current.push(L)},[k]),S=F.useCallback(L=>_.current.find(({pointerId:$})=>L.pointerId===$),[]),w=Ye(L=>{N(L)}),E=(L,$)=>u&&L>$||d&&L<-$,D=Ye(L=>{const $=S(L);if($)if(x.current===L.pointerId){const A=Date.now()-M.current,O=b.current;C.current===On.SWIPE?Math.abs(O)>.3*s||Math.abs(O)>5&&A<o?l(O,A):c(O):C.current===On.PULL&&(E(O,2*Dc)?m(O,A):p(O)),b.current=0,C.current=On.NONE}else{const{target:A}=L;t&&A instanceof HTMLElement&&A===$.target&&(A.classList.contains(Te(gc))||A.classList.contains(Te(yg)))&&y()}k(L)}),P=Ye(L=>{const $=S(L);if($){const A=x.current===L.pointerId;if(L.buttons===0){A&&b.current!==0?D(L):k($);return}const O=L.clientX-$.clientX,T=L.clientY-$.clientY;if(x.current===void 0){const z=H=>{N(L),x.current=L.pointerId,M.current=Date.now(),C.current=H};Math.abs(O)>Math.abs(T)&&Math.abs(O)>Dc&&r(O)?e||(z(On.SWIPE),i()):Math.abs(T)>Math.abs(O)&&E(T,Dc)&&(z(On.PULL),f())}else A&&(C.current===On.SWIPE?(b.current=O,a(O)):C.current===On.PULL&&(b.current=T,g(T)))}});Xg(n,w,P,D)}function q_({preventDefaultWheelX:e,preventDefaultWheelY:t}){const n=F.useRef(null),r=Ye(s=>{const o=Math.abs(s.deltaX)>Math.abs(s.deltaY);(o&&e||!o&&t||s.ctrlKey)&&s.preventDefault()});return F.useCallback(s=>{var o;s?s.addEventListener("wheel",r,{passive:!1}):(o=n.current)===null||o===void 0||o.removeEventListener("wheel",r),n.current=s},[r])}function Z_(e,t,n,r,s,o,i,a,l){const c=F.useRef(0),d=F.useRef(0),u=F.useRef(void 0),f=F.useRef(void 0),g=F.useRef(0),m=F.useRef(void 0),p=F.useRef(0),{setTimeout:y,clearTimeout:b}=xo(),_=F.useCallback(()=>{u.current&&(b(u.current),u.current=void 0)},[b]),x=F.useCallback(()=>{f.current&&(b(f.current),f.current=void 0)},[b]),M=Ye(()=>{e!==Gt.SWIPE&&(c.current=0,p.current=0,_(),x())});F.useEffect(M,[e,M]);const C=Ye(N=>{f.current=void 0,c.current===N&&l(c.current)}),k=Ye(N=>{if(N.ctrlKey||Math.abs(N.deltaY)>Math.abs(N.deltaX))return;const S=w=>{g.current=w,b(m.current),m.current=w>0?y(()=>{g.current=0,m.current=void 0},300):void 0};if(e===Gt.NONE){if(Math.abs(N.deltaX)<=1.2*Math.abs(g.current)){S(N.deltaX);return}if(!n(-N.deltaX))return;if(d.current+=N.deltaX,_(),Math.abs(d.current)>30)d.current=0,S(0),p.current=Date.now(),o();else{const w=d.current;u.current=y(()=>{u.current=void 0,w===d.current&&(d.current=0)},s)}}else if(e===Gt.SWIPE){let w=c.current-N.deltaX;if(w=Math.min(Math.abs(w),r)*Math.sign(w),c.current=w,i(w),x(),Math.abs(w)>.2*r){S(N.deltaX),a(w,Date.now()-p.current);return}f.current=y(()=>C(w),2*s)}else S(N.deltaX)});F.useEffect(()=>t(bc,k),[t,k])}const Kg=Er("container"),qg=F.createContext(null),An=un("useController","ControllerContext",qg);function Q_({children:e,...t}){var n;const{carousel:r,animation:s,controller:o,on:i,styles:a,render:l}=t,{closeOnPullUp:c,closeOnPullDown:d,preventDefaultWheelX:u,preventDefaultWheelY:f}=o,[g,m]=F.useState(),p=mn(),y=L_(),[b,_]=F.useState(Gt.NONE),x=F.useRef(0),M=F.useRef(0),C=F.useRef(1),{registerSensors:k,subscribeSensors:N}=Gg(),{subscribe:S,publish:w}=xs(),E=Xi(),D=Xi(),P=Xi(),{containerRef:L,setContainerRef:$,containerRect:A}=Bg(),O=Tc(q_({preventDefaultWheelX:u,preventDefaultWheelY:f}),$),T=F.useRef(null),z=Tc(T,void 0),{getOwnerDocument:H}=vs(),j=Ki(),G=ie=>(j?-1:1)*(typeof ie=="number"?ie:1),U=Ye(()=>{var ie;return(ie=L.current)===null||ie===void 0?void 0:ie.focus()}),W=Ye(()=>t),Y=Ye(()=>p),V=F.useCallback(ie=>w(fr,ie),[w]),X=F.useCallback(ie=>w(hr,ie),[w]),K=F.useCallback(()=>w(ms),[w]),q=ie=>!(r.finite&&(G(ie)>0&&p.currentIndex===0||G(ie)<0&&p.currentIndex===p.slides.length-1)),ne=ie=>{var Me;x.current=ie,(Me=L.current)===null||Me===void 0||Me.style.setProperty(ht("swipe_offset"),`${Math.round(ie)}px`)},ee=ie=>{var Me,Ae;M.current=ie,C.current=(()=>{const gt=d&&ie>0?ie:c&&ie<0?-ie:0;return Math.min(Math.max(vc(1-gt/60*(1-.5),2),.5),1)})(),(Me=L.current)===null||Me===void 0||Me.style.setProperty(ht("pull_offset"),`${Math.round(ie)}px`),(Ae=L.current)===null||Ae===void 0||Ae.style.setProperty(ht("pull_opacity"),`${C.current}`)},{prepareAnimation:ue}=Nc(T,(ie,Me,Ae)=>{if(T.current&&A)return{keyframes:[{transform:`translate(0, ${ie.rect.y-Me.y+Ae.y}px)`,opacity:ie.opacity},{transform:"translate(0, 0)",opacity:1}],duration:ie.duration,easing:s.easing.fade}}),J=(ie,Me)=>{if(c||d){ee(ie);let Ae=0;T.current&&(Ae=s.fade*(Me?2:1),ue({rect:T.current.getBoundingClientRect(),opacity:C.current,duration:Ae})),P(()=>{ee(0),_(Gt.NONE)},Ae),_(Gt.ANIMATION),Me||K()}},{prepareAnimation:ce,isAnimationPlaying:me}=Nc(T,(ie,Me,Ae)=>{var Ie;if(T.current&&A&&(!((Ie=p.animation)===null||Ie===void 0)&&Ie.duration)){const Xe=Gi(r.spacing),gt=(Xe.percent?Xe.percent*A.width/100:Xe.pixel)||0;return{keyframes:[{transform:`translate(${G(p.globalIndex-ie.index)*(A.width+gt)+ie.rect.x-Me.x+Ae.x}px, 0)`},{transform:"translate(0, 0)"}],duration:p.animation.duration,easing:p.animation.easing}}}),le=Ye(ie=>{var Me,Ae;const Ie=ie.offset||0,Xe=Ie?s.swipe:(Me=s.navigation)!==null&&Me!==void 0?Me:s.swipe,gt=!Ie&&!me()?s.easing.navigation:s.easing.swipe;let{direction:pt}=ie;const Be=(Ae=ie.count)!==null&&Ae!==void 0?Ae:1;let et=Gt.ANIMATION,je=Xe*Be;if(!pt){const fe=A?.width,be=ie.duration||0,we=fe?Xe/fe*Math.abs(Ie):Xe;Be!==0?(be<we?je=je/we*Math.max(be,we/5):fe&&(je=Xe/fe*(fe-Math.abs(Ie))),pt=G(Ie)>0?fr:hr):je=Xe/2}let re=0;pt===fr?q(G(1))?re=-Be:(et=Gt.NONE,je=Xe):pt===hr&&(q(G(-1))?re=Be:(et=Gt.NONE,je=Xe)),je=Math.round(je),D(()=>{ne(0),_(Gt.NONE)},je),T.current&&ce({rect:T.current.getBoundingClientRect(),index:p.globalIndex}),_(et),w(pc,{type:"swipe",increment:re,duration:je,easing:gt})});F.useEffect(()=>{var ie,Me;!((ie=p.animation)===null||ie===void 0)&&ie.increment&&(!((Me=p.animation)===null||Me===void 0)&&Me.duration)&&E(()=>y({type:"swipe",increment:0}),p.animation.duration)},[p.animation,y,E]);const ve=[N,q,A?.width||0,s.swipe,()=>_(Gt.SWIPE),ie=>ne(ie),(ie,Me)=>le({offset:ie,duration:Me,count:1}),ie=>le({offset:ie,count:0})],ge=[()=>{d&&_(Gt.PULL)},ie=>ee(ie),ie=>J(ie),ie=>J(ie,!0)];K_(o,...ve,c,d,...ge,K),Z_(b,...ve);const pe=Ye(()=>{o.focus&&H().querySelector(`.${Te(fc)} .${Te(Kg())}`)&&U()});F.useEffect(pe,[pe]);const ke=Ye(()=>{var ie;(ie=i.view)===null||ie===void 0||ie.call(i,{index:p.currentIndex})});F.useEffect(ke,[p.globalIndex,ke]),F.useEffect(()=>bs(S(fr,ie=>le({direction:fr,...ie})),S(hr,ie=>le({direction:hr,...ie})),S(pc,ie=>y(ie))),[S,le,y]);const $e=F.useMemo(()=>({prev:V,next:X,close:K,focus:U,slideRect:A?__(A,r.padding):{width:0,height:0},containerRect:A||{width:0,height:0},subscribeSensors:N,containerRef:L,setCarouselRef:z,toolbarWidth:g,setToolbarWidth:m}),[V,X,K,U,N,A,L,z,g,m,r.padding]);return F.useImperativeHandle(o.ref,()=>({prev:V,next:X,close:K,focus:U,getLightboxProps:W,getLightboxState:Y}),[V,X,K,U,W,Y]),F.createElement("div",{ref:O,className:Wt(Te(Kg()),Te(ps)),style:{...b===Gt.SWIPE?{[ht("swipe_offset")]:`${Math.round(x.current)}px`}:null,...b===Gt.PULL?{[ht("pull_offset")]:`${Math.round(M.current)}px`,[ht("pull_opacity")]:`${C.current}`}:null,...o.touchAction!=="none"?{[ht("controller_touch_action")]:o.touchAction}:null,...a.container},tabIndex:-1,...k},A&&F.createElement(qg.Provider,{value:$e},e,(n=l.controls)===null||n===void 0?void 0:n.call(l)))}const J_=pn(go,Q_);function Tr(e){return _r(fg,e)}function Zg(e){return _r(gc,e)}function R_({slide:e,offset:t}){const n=F.useRef(null),{currentIndex:r,slides:s}=mn(),{slideRect:o,focus:i}=An(),{render:a,carousel:{imageFit:l,imageProps:c},on:{click:d},styles:{slide:u},labels:f}=Lt(),{getOwnerDocument:g}=vs(),m=t!==0;F.useEffect(()=>{var y;m&&(!((y=n.current)===null||y===void 0)&&y.contains(g().activeElement))&&i()},[m,i,g]);const p=()=>{var y,b,_,x;let M=(y=a.slide)===null||y===void 0?void 0:y.call(a,{slide:e,offset:t,rect:o});return!M&&gr(e)&&(M=F.createElement(Nr,{slide:e,offset:t,render:a,rect:o,imageFit:l,imageProps:c,onClick:m?void 0:()=>d?.({index:r})})),M?F.createElement(F.Fragment,null,(b=a.slideHeader)===null||b===void 0?void 0:b.call(a,{slide:e}),((_=a.slideContainer)!==null&&_!==void 0?_:(({children:C})=>C))({slide:e,children:M}),(x=a.slideFooter)===null||x===void 0?void 0:x.call(a,{slide:e})):null};return F.createElement("div",{ref:n,className:Wt(Te(Zg()),!m&&Te(Zg("current")),Te(ps)),...N_(m),style:u,role:"group","aria-roledescription":Mr(f,"Slide"),"aria-label":Ng(f,s,r+t)},p())}function e2(){const e=Lt().styles.slide;return F.createElement("div",{className:Te(gc),style:e})}function t2({carousel:e,labels:t}){const{slides:n,currentIndex:r,globalIndex:s}=mn(),{setCarouselRef:o}=An(),{autoPlaying:i,focusWithin:a}=Ec(),l=Gi(e.spacing),c=Gi(e.padding),d=Og(e,n,1),u=[];if(kc(n))for(let f=r-d;f<=r+d;f+=1){const g=Cc(n,f),m=s-r+f,p=e.finite&&(f<0||f>n.length-1);u.push(p?{key:m}:{key:[`${m}`,Tg(g)].filter(Boolean).join("|"),offset:f-r,slide:g})}return F.createElement("div",{ref:o,className:Wt(Te(Tr()),u.length>0&&Te(Tr("with_slides"))),style:{[`${ht(Tr("slides_count"))}`]:u.length,[`${ht(Tr("spacing_px"))}`]:l.pixel||0,[`${ht(Tr("spacing_percent"))}`]:l.percent||0,[`${ht(Tr("padding_px"))}`]:c.pixel||0,[`${ht(Tr("padding_percent"))}`]:c.percent||0},role:"region","aria-live":i&&!a?"off":"polite","aria-roledescription":Mr(t,"Carousel"),"aria-label":Mr(t,"Photo gallery")},u.map(({key:f,slide:g,offset:m})=>g?F.createElement(R_,{key:f,slide:g,offset:m}):F.createElement(e2,{key:f})))}const n2=pn(fg,t2);function Qg(){const{carousel:e}=Lt(),{slides:t,currentIndex:n}=mn(),r=t.length===0||e.finite&&n===0,s=t.length===0||e.finite&&n===t.length-1;return{prevDisabled:r,nextDisabled:s}}function Jg(e){var t;const n=Ki(),{publish:r}=xs(),{animation:s,controller:o}=Lt(),{prevDisabled:i,nextDisabled:a}=Qg(),l=((t=s.navigation)!==null&&t!==void 0?t:s.swipe)/2,c=Yg(()=>r(fr),l),d=Yg(()=>r(hr),l),u=Ye(f=>{switch(f.key){case w_:if(!o.closeOnEscape)return;r(ms);break;case k_:(n?a:i)||(n?d:c)();break;case C_:(n?i:a)||(n?c:d)();break;default:return}f.stopPropagation()});F.useEffect(()=>e(mc,u),[e,u])}function Rg({label:e,icon:t,renderIcon:n,action:r,onClick:s,disabled:o,style:i}){return F.createElement(ws,{label:e,icon:t,renderIcon:n,className:Te(`navigation_${r}`),disabled:o,onClick:s,style:i,...Wg(An().focus,o)})}function r2({render:{buttonPrev:e,buttonNext:t,iconPrev:n,iconNext:r},styles:s}){const{prev:o,next:i,subscribeSensors:a}=An(),{prevDisabled:l,nextDisabled:c}=Qg();return Jg(a),F.createElement(F.Fragment,null,e?e():F.createElement(Rg,{label:"Previous",action:fr,icon:B_,renderIcon:n,style:s.navigationPrev,disabled:l,onClick:o}),t?t():F.createElement(Rg,{label:"Next",action:hr,icon:j_,renderIcon:r,style:s.navigationNext,disabled:c,onClick:i}))}const s2=pn(c_,r2),ep=Te(y_),o2=Te(bg);function i2(e){return"style"in e}function tp(e,t,n){const r=window.getComputedStyle(e),s=n?"padding-left":"padding-right",o=n?r.paddingLeft:r.paddingRight,i=e.style.getPropertyValue(s);return e.style.setProperty(s,`${(Wi(o)||0)+t}px`),()=>{i?e.style.setProperty(s,i):e.style.removeProperty(s)}}function a2({noScroll:{disabled:e},children:t}){const n=Ki(),{getOwnerDocument:r,getOwnerWindow:s}=vs();return F.useEffect(()=>{if(e)return()=>{};const o=[],i=s(),{body:a,documentElement:l}=r(),c=Math.round(i.innerWidth-l.clientWidth);if(c>0){o.push(tp(a,c,n));const d=a.getElementsByTagName("*");for(let u=0;u<d.length;u+=1){const f=d[u];i2(f)&&i.getComputedStyle(f).getPropertyValue("position")==="fixed"&&!f.classList.contains(o2)&&o.push(tp(f,c,n))}}return a.classList.add(ep),()=>{a.classList.remove(ep),o.forEach(d=>d())}},[n,e,r,s]),F.createElement(F.Fragment,null,t)}const l2=pn(d_,a2);function np(e){return _r(fc,e)}function rp(e,t,n){const r=e.getAttribute(t);return e.setAttribute(t,n),()=>{r?e.setAttribute(t,r):e.removeAttribute(t)}}function c2({portal:{root:e,container:{className:t,style:n,...r}={}},animation:s,styles:o,className:i,on:a,close:l,labels:c,children:d}){const[u,f]=F.useState(!1),[g,m]=F.useState(!1),p=F.useRef([]),y=F.useRef(null),{setTimeout:b}=xo(),{subscribe:_}=xs(),M=Mc()?0:s.fade;F.useEffect(()=>(f(!0),()=>{f(!1),m(!1)}),[]);const C=Ye(()=>{p.current.forEach(w=>w()),p.current=[]}),k=Ye(()=>{var w;m(!1),C(),(w=a.exiting)===null||w===void 0||w.call(a),b(()=>{var E;(E=a.exited)===null||E===void 0||E.call(a),l()},M)});F.useEffect(()=>_(ms,k),[_,k]);const N=Ye(w=>{var E,D,P;T_(w),m(!0),(E=a.entering)===null||E===void 0||E.call(a);const L=(P=(D=w.parentNode)===null||D===void 0?void 0:D.children)!==null&&P!==void 0?P:[];for(let $=0;$<L.length;$+=1){const A=L[$];["TEMPLATE","SCRIPT","STYLE"].indexOf(A.tagName)===-1&&A!==w&&(p.current.push(rp(A,"inert","")),p.current.push(rp(A,"aria-hidden","true")))}p.current.push(()=>{var $,A;(A=($=y.current)===null||$===void 0?void 0:$.focus)===null||A===void 0||A.call($)}),b(()=>{var $;($=a.entered)===null||$===void 0||$.call(a)},M)}),S=F.useCallback(w=>{w?N(w):C()},[N,C]);return u?$n.createPortal(F.createElement(X_,{ref:S,className:Wt(i,t,Te(np()),Te(bg),g&&Te(np("open"))),"aria-modal":!0,role:"dialog","aria-label":Mr(c,"Lightbox"),style:{...s.fade!==_c.animation.fade?{[ht("fade_animation_duration")]:`${M}ms`}:null,...s.easing.fade!==_c.animation.easing.fade?{[ht("fade_animation_timing_function")]:s.easing.fade}:null,...o.root,...n},onFocus:w=>{y.current||(y.current=w.relatedTarget)},...r},d),(typeof e=="function"?e():e)||document.body):null}const d2=pn(fc,c2);function u2({children:e}){return F.createElement(F.Fragment,null,e)}const f2=pn(u_,u2);function h2(e){return _r(hg,e)}function g2({toolbar:{buttons:e},render:{buttonClose:t,iconClose:n},styles:r}){const{close:s,setToolbarWidth:o}=An(),{setContainerRef:i,containerRect:a}=Bg();Tn(()=>{o(a?.width)},[o,a?.width]);const l=()=>t?t():F.createElement(ws,{key:ms,label:"Close",icon:U_,renderIcon:n,onClick:s});return F.createElement("div",{ref:i,style:r.toolbar,className:Te(h2())},e?.map(c=>c===ms?l():c))}const p2=pn(hg,g2);function sp(e,t){var n;return F.createElement(e.module.component,{key:e.module.name,...t},(n=e.children)===null||n===void 0?void 0:n.map(r=>sp(r,t)))}function m2(e,t={}){const{easing:n,...r}=e,{easing:s,...o}=t;return{easing:{...n,...s},...r,...o}}function b2({carousel:e,animation:t,render:n,toolbar:r,controller:s,noScroll:o,on:i,plugins:a,slides:l,index:c,...d}){const{animation:u,carousel:f,render:g,toolbar:m,controller:p,noScroll:y,on:b,slides:_,index:x,plugins:M,...C}=_c,{config:k,augmentation:N}=O_([It(d2,[It(l2,[It(J_,[It(n2),It(p2),It(s2)])])])],a||M),S=N({animation:m2(u,t),carousel:{...f,...e},render:{...g,...n},toolbar:{...m,...r},controller:{...p,...s},noScroll:{...y,...o},on:{...b,...i},...C,...d});return S.open?F.createElement($g,{...S},F.createElement($_,{slides:l||_,index:Wi(c||x)},F.createElement(V_,null,F.createElement(I_,null,F.createElement(A_,null,sp(It(f2,k),S)))))):null}const op={minZoom:1,maxZoomPixelRatio:1,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:500,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,pinchZoomV4:!1,scrollToZoom:!1,maxZoom:8};function y2(e){return Math.min(Math.max(e,Number.EPSILON),1)}function ip(e){const{minZoom:t,...n}={...op,...e};return{minZoom:y2(t),...n}}function v2(e,t,n,r){const s=F.useRef(void 0),o=F.useRef(void 0),{zoom:i}=Lt().animation,a=Mc(),l=Ye(()=>{var c,d,u;if((c=s.current)===null||c===void 0||c.cancel(),s.current=void 0,o.current&&r?.current){try{s.current=(u=(d=r.current).animate)===null||u===void 0?void 0:u.call(d,[{transform:o.current},{transform:`scale(${e}) translateX(${t}px) translateY(${n}px)`}],{duration:a?0:i??500,easing:s.current?"ease-out":"ease-in-out"})}catch(f){console.error(f)}o.current=void 0,s.current&&(s.current.onfinish=()=>{s.current=void 0})}});return Tn(l,[e,t,n,l]),F.useCallback(()=>{o.current=r?.current?window.getComputedStyle(r.current).transform:void 0},[r])}function x2(e,t){const{on:n}=Lt(),r=Ye(()=>{var s;t||(s=n.zoom)===null||s===void 0||s.call(n,{zoom:e})});F.useEffect(r,[e,r])}function qi(){const{zoom:e}=Lt();return ip(e)}function w2(e,t){var n;const r=typeof e=="function"?(n=e(t))!==null&&n!==void 0?n:op.maxZoom:e;return Math.max(r,1)}function k2(e,t){var n,r;let s={width:0,height:0},o={width:0,height:0};const{currentSlide:i}=mn(),{imageFit:a}=Lt().carousel,{maxZoomPixelRatio:l,maxZoom:c}=qi();if(e&&i){const u={...i,...t};if(gr(u)){const f=xc(u,a),g=Math.max(...(((n=u.srcSet)===null||n===void 0?void 0:n.map(p=>p.width))||[]).concat(u.width?[u.width]:[])),m=Math.max(...(((r=u.srcSet)===null||r===void 0?void 0:r.map(p=>p.height))||[]).concat(u.height?[u.height]:[]));g>0&&m>0&&e.width>0&&e.height>0&&(o=f?{width:Math.round(Math.min(g,e.width/e.height*m)),height:Math.round(Math.min(m,e.height/e.width*g))}:{width:g,height:m},o={width:o.width*l,height:o.height*l},s=f?{width:Math.min(e.width,o.width,g),height:Math.min(e.height,o.height,m)}:{width:Math.round(Math.min(e.width,e.height/m*g,g)),height:Math.round(Math.min(e.height,e.width/g*m,m))})}else e.width>0&&e.height>0&&(t&&t.width>0&&t.height>0?s={width:Math.min(e.width,t.width),height:Math.min(e.height,t.height)}:s={width:e.width,height:e.height})}const d=i&&s.width?gr(i)?Math.max(vc(o.width/s.width,5),1):w2(c,i):1;return{imageRect:s,maxZoom:d}}function ap(e,t){return Math.hypot(e.clientX-t.clientX,e.clientY-t.clientY)}function lp(e,t,n=100,r=2){return e*Math.min(1+Math.abs(t/n),r)**Math.sign(t)}function C2(e,t,n,r,s,o,i,a,l){const c=F.useRef([]),d=F.useRef(0),u=F.useRef(void 0),{globalIndex:f}=mn(),{getOwnerWindow:g}=vs(),{containerRef:m,subscribeSensors:p}=An(),{keyboardMoveDistance:y,zoomInMultiplier:b,wheelZoomDistanceFactor:_,scrollToZoom:x,doubleTapDelay:M,doubleClickDelay:C,doubleClickMaxStops:k,pinchZoomDistanceFactor:N,pinchZoomV4:S}=qi(),w=F.useCallback(z=>{if(m.current){const{pageX:H,pageY:j}=z,{scrollX:G,scrollY:U}=g(),{left:W,top:Y,width:V,height:X}=m.current.getBoundingClientRect();return[H-W-G-V/2,j-Y-U-X/2]}return[]},[m,g]),E=Ye(z=>{const{key:H,metaKey:j,ctrlKey:G}=z,U=j||G,W=()=>{z.preventDefault(),z.stopPropagation()};if(e>1){const Y=(V,X)=>{W(),a(V,X)};H==="ArrowDown"?Y(0,y):H==="ArrowUp"?Y(0,-y):H==="ArrowLeft"?Y(-y,0):H==="ArrowRight"&&Y(y,0)}H==="+"||U&&H==="="?(W(),s()):H==="-"||U&&H==="_"?(W(),o()):U&&H==="0"&&(W(),i(1))}),D=Ye(z=>{if((z.ctrlKey||x)&&Math.abs(z.deltaY)>Math.abs(z.deltaX)){z.stopPropagation(),i(lp(e,-z.deltaY,_),!0,...w(z));return}e>1&&(z.stopPropagation(),x||a(z.deltaX,z.deltaY))}),P=F.useCallback(z=>{const H=c.current;H.splice(0,H.length,...H.filter(j=>j.pointerId!==z.pointerId))},[]),L=F.useCallback(z=>{P(z),z.persist(),c.current.push(z)},[P]),$=Ye(z=>{var H;const j=c.current;if(z.pointerType==="mouse"&&z.buttons>1||!(!((H=l?.current)===null||H===void 0)&&H.contains(z.target)))return;e>1&&z.stopPropagation();const{timeStamp:G}=z;if(j.length===0&&G-d.current<(z.pointerType==="touch"?M:C)){d.current=0;const U=e>=1?e!==n?e*Math.max(n**(1/k),b):1:e!==t?e/Math.max(t**(-1/k),b):1;i(U,!1,...w(z))}else d.current=G;if(L(z),j.length===2){const U=ap(j[0],j[1]);u.current={previousDistance:U,initialDistance:Math.max(U,1),initialZoom:e}}}),A=Ye(z=>{const H=c.current,j=H.find(G=>G.pointerId===z.pointerId);if(H.length===2&&u.current){z.stopPropagation(),L(z);const G=ap(H[0],H[1]),U=S?u.current.initialZoom/u.current.initialDistance*G:lp(e,G-u.current.previousDistance,N);i(U,!0,...H.map(W=>w(W)).reduce((W,Y)=>Y.map((V,X)=>W[X]+V/2))),u.current.previousDistance=G;return}e>1&&(z.stopPropagation(),j&&(H.length===1&&a((j.clientX-z.clientX)/e,(j.clientY-z.clientY)/e),L(z)))}),O=F.useCallback(z=>{const H=c.current;H.length===2&&H.find(j=>j.pointerId===z.pointerId)&&(u.current=void 0),P(z)},[P]),T=F.useCallback(()=>{const z=c.current;z.splice(0,z.length),d.current=0,u.current=void 0},[]);Xg(p,$,A,O,r),F.useEffect(T,[f,T]),F.useEffect(()=>r?()=>{}:bs(T,p(mc,E),p(bc,D)),[r,p,T,E,D])}function S2(e,t,n){const[r,s]=F.useState(1),[o,i]=F.useState(0),[a,l]=F.useState(0),c=v2(r,o,a,n),{currentSlide:d,globalIndex:u}=mn(),{containerRect:f,slideRect:g}=An(),{minZoom:m,zoomInMultiplier:p}=qi(),y=d&&gr(d)?d.src:void 0,b=!n?.current;Tn(()=>{s(1),i(0),l(0)},[u,y]);const _=F.useCallback((N,S,w)=>{const E=w||r,D=o-(N||0),P=a-(S||0),L=(e.width*E-g.width)/2/E,$=(e.height*E-g.height)/2/E;i(Math.min(Math.abs(D),Math.max(L,0))*Math.sign(D)),l(Math.min(Math.abs(P),Math.max($,0))*Math.sign(P))},[r,o,a,g,e.width,e.height]),x=F.useCallback((N,S,w,E)=>{const D=vc(N+.01<t?N-.01>m?N:m:t,5);S||c(),_(w?w*(1/r-1/D):0,E?E*(1/r-1/D):0,D),s(D)},[r,m,t,_,c]),M=Ye(()=>{r>1&&(r>t&&x(t,!0),_())});Tn(M,[f.width,f.height,M]);const C=F.useCallback(()=>{const N=r*p;x(r<1&&N>1?1:N)},[r,p,x]),k=F.useCallback(()=>{const N=r/p;x(r>1&&N<1?1:N)},[r,p,x]);return{zoom:r,offsetX:o,offsetY:a,disabled:b,changeOffsets:_,changeZoom:x,zoomIn:C,zoomOut:k}}const cp=F.createContext(null),Ic=un("useZoom","ZoomControllerContext",cp);function _2({children:e}){const[t,n]=F.useState(),{slideRect:r}=An(),{ref:s,minZoom:o}=qi(),{imageRect:i,maxZoom:a}=k2(r,t?.imageDimensions),{zoom:l,offsetX:c,offsetY:d,disabled:u,changeZoom:f,changeOffsets:g,zoomIn:m,zoomOut:p}=S2(i,a,t?.zoomWrapperRef);x2(l,u),C2(l,o,a,u,m,p,f,g,t?.zoomWrapperRef);const y=F.useMemo(()=>({zoom:l,minZoom:o,maxZoom:a,offsetX:c,offsetY:d,disabled:u,zoomIn:m,zoomOut:p,changeZoom:f}),[l,o,a,c,d,u,m,p,f]);F.useImperativeHandle(s,()=>y,[y]);const b=F.useMemo(()=>({...y,setZoomWrapper:n}),[y,n]);return F.createElement(cp.Provider,{value:b},e)}const E2=nn("ZoomIn",F.createElement(F.Fragment,null,F.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"}),F.createElement("path",{d:"M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"}))),M2=nn("ZoomOut",F.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"})),dp=F.forwardRef(function({zoomIn:t,onLoseFocus:n},r){const s=F.useRef(!1),o=F.useRef(!1),{zoom:i,minZoom:a,maxZoom:l,zoomIn:c,zoomOut:d,disabled:u}=Ic(),{render:f}=Lt(),g=u||(t?i>=l:i<=a);return F.useEffect(()=>{g&&s.current&&o.current&&n(),g||(s.current=!0)},[g,n]),F.createElement(ws,{ref:r,disabled:g,label:t?"Zoom in":"Zoom out",icon:t?E2:M2,renderIcon:t?f.iconZoomIn:f.iconZoomOut,onClick:t?c:d,onFocus:()=>{o.current=!0},onBlur:()=>{o.current=!1}})});function N2(){const e=F.useRef(null),t=F.useRef(null),{focus:n}=An(),r=F.useCallback(i=>{var a,l;!((a=i.current)===null||a===void 0)&&a.disabled?n():(l=i.current)===null||l===void 0||l.focus()},[n]),s=F.useCallback(()=>r(e),[r]),o=F.useCallback(()=>r(t),[r]);return F.createElement(F.Fragment,null,F.createElement(dp,{zoomIn:!0,ref:e,onLoseFocus:o}),F.createElement(dp,{ref:t,onLoseFocus:s}))}function T2(){const{render:e}=Lt(),t=Ic();return e.buttonZoom?F.createElement(F.Fragment,null,e.buttonZoom(t)):F.createElement(N2,null)}function O2(e){var t;return(((t=e.srcSet)===null||t===void 0?void 0:t.length)||0)>0}function A2({current:e,preload:t},{type:n,source:r}){switch(n){case"fetch":return e?{current:e,preload:r}:{current:r};case"done":return r===t?{current:r}:{current:e,preload:t};default:throw new Error(Eg)}}function D2(e){var t,n;const[{current:r,preload:s},o]=F.useReducer(A2,{}),{slide:i,rect:a,imageFit:l,render:c,interactive:d}=e,u=i.srcSet.sort((C,k)=>C.width-k.width),f=(t=i.width)!==null&&t!==void 0?t:u[u.length-1].width,g=(n=i.height)!==null&&n!==void 0?n:u[u.length-1].height,m=xc(i,l),p=Math.max(...u.map(C=>C.width)),y=Math.min((m?Math.max:Math.min)(a.width,f*(a.height/g)),p),b=E_(),_=Ye(()=>{var C;const k=(C=u.find(N=>N.width>=y*b))!==null&&C!==void 0?C:u[u.length-1];(!r||u.findIndex(N=>N.src===r)<u.findIndex(N=>N===k))&&o({type:"fetch",source:k.src})});Tn(_,[a.width,a.height,b,_]);const x=Ye(C=>o({type:"done",source:C})),M={WebkitTransform:d?"initial":"translateZ(0)"};return m||Object.assign(M,a.width/a.height<f/g?{width:"100%",height:"auto"}:{width:"auto",height:"100%"}),F.createElement(F.Fragment,null,s&&s!==r&&F.createElement(Nr,{key:"preload",...e,offset:void 0,slide:{...i,src:s,srcSet:void 0},style:{position:"absolute",visibility:"hidden",...M},onLoad:()=>x(s),render:{...c,iconLoading:()=>null,iconError:()=>null}}),r&&F.createElement(Nr,{key:"current",...e,slide:{...i,src:r,srcSet:void 0},style:M}))}function I2({render:e,slide:t,offset:n,rect:r}){var s;const[o,i]=F.useState(),a=F.useRef(null),l=gr(t),{zoom:c,maxZoom:d,offsetX:u,offsetY:f,setZoomWrapper:g}=Ic(),m=c>1,{carousel:p,on:y}=Lt(),{currentIndex:b}=mn();Tn(()=>{if(n!==0||l||!a.current)return()=>{};const x=()=>{const C=a.current;if(!C)return;let k=0,N=0;for(const S of C.children)S instanceof HTMLElement&&(k=Math.max(k,S.offsetWidth),N=Math.max(N,S.offsetHeight));i(S=>S&&S.width===k&&S.height===N?S:{width:k,height:N})};if(x(),typeof ResizeObserver>"u")return()=>{};const M=new ResizeObserver(x);for(const C of a.current.children)M.observe(C);return()=>M.disconnect()},[n,l,r]),Tn(()=>n===0?(g({zoomWrapperRef:a,imageDimensions:o}),()=>g(void 0)):()=>{},[n,o,g]);let _=(s=e.slide)===null||s===void 0?void 0:s.call(e,{slide:t,offset:n,rect:r,zoom:c,maxZoom:d});if(!_&&l){const x={slide:t,offset:n,rect:r,render:e,imageFit:p.imageFit,imageProps:p.imageProps,onClick:n===0?()=>{var M;return(M=y.click)===null||M===void 0?void 0:M.call(y,{index:b})}:void 0};_=O2(t)?F.createElement(D2,{...x,slide:t,interactive:m,rect:n===0?{width:r.width*c,height:r.height*c}:r}):F.createElement(Nr,{onLoad:M=>i({width:M.naturalWidth,height:M.naturalHeight}),...x})}return _?F.createElement("div",{ref:a,className:Wt(Te(mg),Te(ps),Te(yg),m&&Te(v_)),style:n===0?{transform:`scale(${c}) translateX(${u}px) translateY(${f}px)`}:void 0},_):null}const L2=({augment:e,addModule:t})=>{e(({zoom:n,toolbar:r,render:s,controller:o,...i})=>{const a=ip(n);return{zoom:a,toolbar:Yi(r,pg,F.createElement(T2,null)),render:{...s,slide:l=>{var c,d;return gr(l.slide)||l.slide.type!=null&&(!((c=a.supports)===null||c===void 0)&&c.includes(l.slide.type))?F.createElement(I2,{render:s,...l}):(d=s.slide)===null||d===void 0?void 0:d.call(s,l)}},controller:{...o,preventDefaultWheelY:a.scrollToZoom},...i}}),t(pn(pg,_2))},P2={separator:"/",container:{}},up=e=>({...P2,...e});function $2({counter:e}){const{slides:t,currentIndex:n}=mn(),{separator:r,container:{className:s,...o},className:i,...a}=up(e);return t.length===0?null:F.createElement("div",{className:Wt(Te("counter"),s||i),...a,...o,"aria-hidden":!0},n+1," ",r," ",t.length)}function F2({augment:e,addChild:t}){e(({counter:n,...r})=>({counter:up(n),...r})),t(go,pn(f_,$2))}const z2={autoplay:!1,delay:3e3,ref:null},fp=e=>({...z2,...e}),hp=F.createContext(null),V2=un("useSlideshow","SlideshowContext",hp);function H2({slideshow:e,carousel:{finite:t},on:n,children:r}){const{autoplay:s,delay:o,ref:i}=fp(e),a=F.useRef(s),[l,c]=F.useState(s),d=F.useRef(void 0),u=F.useRef(void 0),{slides:f,currentIndex:g}=mn(),{setTimeout:m,clearTimeout:p}=xo(),{subscribe:y}=xs(),{next:b}=An(),{setAutoPlaying:_}=Ec();F.useEffect(()=>_(l),[l,_]);const x=f.length===0||t&&g===f.length-1,M=F.useCallback(()=>{!l&&!x&&c(!0)},[l,x]),C=F.useCallback(()=>{l&&c(!1)},[l]),k=F.useCallback(()=>{p(d.current),d.current=void 0},[p]),N=Ye(()=>{k(),!(!l||x||u.current===gs||u.current===hc)&&(d.current=m(()=>{l&&(u.current=void 0,b())},o))});F.useEffect(N,[g,l,N]),F.useEffect(()=>{l&&x&&c(!1)},[g,l,x]);const S=Ye(()=>{var D;return(D=n.slideshowStart)===null||D===void 0?void 0:D.call(n)}),w=Ye(()=>{var D;return(D=n.slideshowStop)===null||D===void 0?void 0:D.call(n)});F.useEffect(()=>{l?S():a.current&&w(),a.current=l},[l,S,w]),F.useEffect(()=>bs(k,y(g_,()=>{u.current=gs,k()}),y(p_,()=>{u.current=hc,k()}),y(m_,()=>{u.current=bo,N()}),y(b_,()=>{u.current=yo,N()})),[y,k,N]);const E=F.useMemo(()=>({playing:l,disabled:x,play:M,pause:C}),[l,x,M,C]);return F.useImperativeHandle(i,()=>E,[E]),F.createElement(hp.Provider,{value:E},r)}const U2=nn("Play",F.createElement("path",{d:"M8 5v14l11-7z"})),B2=nn("Pause",F.createElement("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"}));function j2(){const{playing:e,disabled:t,play:n,pause:r}=V2(),{render:s}=Lt(),o=Wg(An().focus,t);return s.buttonSlideshow?F.createElement(F.Fragment,null,s.buttonSlideshow({playing:e,disabled:t,play:n,pause:r})):F.createElement(ws,{label:e?"Pause":"Play",icon:e?B2:U2,renderIcon:e?s.iconSlideshowPause:s.iconSlideshowPlay,onClick:e?r:n,disabled:t,...o})}function W2({augment:e,addModule:t}){e(({slideshow:n,toolbar:r,...s})=>({toolbar:Yi(r,gg,F.createElement(j2,null)),slideshow:fp(n),...s})),t(pn(gg,H2))}const G2={auto:!1,ref:null},gp=e=>({...G2,...e}),pp=F.createContext(null),Y2=un("useFullscreen","FullscreenContext",pp);function X2({fullscreen:e,on:t,children:n}){const{auto:r,ref:s}=gp(e),o=F.useRef(null),[i,a]=F.useState(),[l,c]=F.useState(!1),d=F.useRef(!1),{getOwnerDocument:u}=vs();Tn(()=>{var x,M,C,k;const N=u();a(!((k=(C=(M=(x=N.fullscreenEnabled)!==null&&x!==void 0?x:N.webkitFullscreenEnabled)!==null&&M!==void 0?M:N.mozFullScreenEnabled)!==null&&C!==void 0?C:N.msFullscreenEnabled)!==null&&k!==void 0&&k))},[u]);const f=F.useCallback(()=>{var x;const M=u(),C=M.fullscreenElement||M.webkitFullscreenElement||M.mozFullScreenElement||M.msFullscreenElement;return((x=C?.shadowRoot)===null||x===void 0?void 0:x.fullscreenElement)||C},[u]),g=F.useCallback(()=>{const x=o.current;try{x.requestFullscreen?x.requestFullscreen().catch(()=>{}):x.webkitRequestFullscreen?x.webkitRequestFullscreen():x.mozRequestFullScreen?x.mozRequestFullScreen():x.msRequestFullscreen&&x.msRequestFullscreen()}catch{}},[]),m=F.useCallback(()=>{if(!f())return;const x=u();try{x.exitFullscreen?x.exitFullscreen().catch(()=>{}):x.webkitExitFullscreen?x.webkitExitFullscreen():x.mozCancelFullScreen?x.mozCancelFullScreen():x.msExitFullscreen&&x.msExitFullscreen()}catch{}},[f,u]);F.useEffect(()=>{const x=u(),M=()=>{c(f()===o.current)};return bs(...["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange"].map(C=>(x.addEventListener(C,M),()=>x.removeEventListener(C,M))))},[f,u]);const p=Ye(()=>{var x;return(x=t.enterFullscreen)===null||x===void 0?void 0:x.call(t)}),y=Ye(()=>{var x;return(x=t.exitFullscreen)===null||x===void 0?void 0:x.call(t)});F.useEffect(()=>{l&&(d.current=!0),d.current&&(l?p:y)()},[l,p,y]);const b=Ye(()=>{var x;return(x=r?g:null)===null||x===void 0||x(),m});F.useEffect(b,[b]);const _=F.useMemo(()=>({fullscreen:l,disabled:i,enter:g,exit:m}),[l,i,g,m]);return F.useImperativeHandle(s,()=>_,[_]),F.createElement("div",{ref:o,className:Wt(Te(po),Te(mg))},F.createElement(pp.Provider,{value:_},n))}const K2=nn("EnterFullscreen",F.createElement("path",{d:"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"})),q2=nn("ExitFullscreen",F.createElement("path",{d:"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"}));function Z2(){var e;const{fullscreen:t,disabled:n,enter:r,exit:s}=Y2(),{render:o}=Lt();return n?null:o.buttonFullscreen?F.createElement(F.Fragment,null,(e=o.buttonFullscreen)===null||e===void 0?void 0:e.call(o,{fullscreen:t,disabled:n,enter:r,exit:s})):F.createElement(ws,{disabled:n,label:t?"Exit Fullscreen":"Enter Fullscreen",icon:t?q2:K2,renderIcon:t?o.iconExitFullscreen:o.iconEnterFullscreen,onClick:t?s:r})}function Q2({augment:e,contains:t,addParent:n}){e(({fullscreen:r,toolbar:s,...o})=>({toolbar:Yi(s,po,F.createElement(Z2,null)),fullscreen:gp(r),...o})),n(t(mo)?mo:go,pn(po,X2))}const Or={ref:null,position:"bottom",width:120,height:80,border:1,borderRadius:4,padding:4,gap:16,imageFit:"contain",vignette:!0,hidden:!1,showToggle:!1},Lc=e=>({...Or,...e});function mp(){const{thumbnails:e}=Lt();return Lc(e)}const Ar=e=>_r(mo,e),Pt=e=>Ar(_r("thumbnail",e)),J2=nn("VideoThumbnail",F.createElement("path",{d:"M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"})),R2=nn("UnknownThumbnail",F.createElement("path",{d:"M23 18V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zM8.5 12.5l2.5 3.01L14.5 11l4.5 6H5l3.5-4.5z"}));function eE({slide:e,render:t,rect:n,imageFit:r}){var s;const o=(s=t.thumbnail)===null||s===void 0?void 0:s.call(t,{slide:e,render:t,rect:n,imageFit:r});if(o)return o;const i={render:t,rect:n,imageFit:r};if(e.thumbnail)return F.createElement(Nr,{slide:{src:e.thumbnail},...i});if(gr(e))return F.createElement(Nr,{slide:e,...i});const a=Te(Pt(ji));return e.type==="video"?F.createElement(F.Fragment,null,e.poster&&F.createElement(Nr,{slide:{src:e.poster},...i}),F.createElement(J2,{className:a})):F.createElement(R2,{className:a})}const tE=Er("active"),Pc=Er("fadein"),$c=Er("fadeout"),nE=Er("placeholder"),bp="delay",yp="duration";function rE({slide:e,index:t,onClick:n,fadeIn:r,fadeOut:s,placeholder:o,onLoseFocus:i}){const a=F.useRef(null),{render:l,styles:c,labels:d}=Lt(),{slides:u,globalIndex:f}=mn(),{getOwnerDocument:g}=vs(),{width:m,height:p,imageFit:y}=mp(),b={width:m,height:p},_=t===f,x=Ye(i);return F.useEffect(()=>{s&&g().activeElement===a.current&&x()},[s,x,g]),F.createElement("button",{ref:a,type:"button",className:Wt(Te(ps),Te(Pt()),_&&Te(Pt(tE())),r&&Te(Pt(Pc())),s&&Te(Pt($c())),o&&Te(Pt(nE()))),style:{...r?{[ht(Pt(Pc(yp)))]:`${r.duration}ms`,[ht(Pt(Pc(bp)))]:`${r.delay}ms`}:null,...s?{[ht(Pt($c(yp)))]:`${s.duration}ms`,[ht(Pt($c(bp)))]:`${s.delay}ms`}:null,...c.thumbnail},onClick:n,"aria-current":_?!0:void 0,"aria-label":Ng(d,u,t)},e&&eE({slide:e,render:l,rect:b,imageFit:y}))}function vp(e){return["top","bottom"].includes(e)}function xp(e,t){return t+2*(e.border+e.padding)+e.gap}function sE(e){const{thumbnail:t,poster:n}=e||{thumbnail:"placeholder"};return typeof t=="string"&&t||typeof n=="string"&&n||e&&Tg(e)||void 0}function wp({visible:e,containerRef:t}){const n=F.useRef(null),r=Ki(),{publish:s,subscribe:o}=xs(),{carousel:i,styles:a,labels:l}=Lt(),{slides:c,globalIndex:d,animation:u}=mn(),{registerSensors:f,subscribeSensors:g}=Gg();Jg(g);const m=mp(),{position:p,width:y,height:b,border:_,borderStyle:x,borderColor:M,borderRadius:C,padding:k,gap:N,vignette:S}=m,w=u?.duration!==void 0&&u?.increment||0,E=u?.duration||0,{prepareAnimation:D}=Nc(n,O=>({keyframes:vp(p)?[{transform:`translateX(${(r?-1:1)*xp(m,y)*w+O}px)`},{transform:"translateX(0)"}]:[{transform:`translateY(${xp(m,b)*w+O}px)`},{transform:"translateY(0)"}],duration:E,easing:u?.easing})),P=Ye(()=>{let O=0;if(t.current&&n.current){const T=t.current.getBoundingClientRect(),z=n.current.getBoundingClientRect();O=vp(p)?z.left-T.left-(T.width-z.width)/2:z.top-T.top-(T.height-z.height)/2}D(O)});F.useEffect(()=>bs(o(pc,P)),[o,P]);const L=Og(i,c),$=[];if(kc(c))for(let O=d-L-Math.abs(w);O<=d+L+Math.abs(w);O+=1){const z=i.finite&&(O<0||O>c.length-1)||w<0&&O<d-L||w>0&&O>d+L?null:Cc(c,O),H=[`${O}`,sE(z)].filter(Boolean).join("|");$.push({key:H,index:O,slide:z})}const A=O=>()=>{O>d?s(hr,{count:O-d}):O<d&&s(fr,{count:d-O})};return F.createElement("div",{className:Wt(Te(Ar("container")),Te(ps)),style:{...e?null:{display:"none"},...y!==Or.width?{[ht(Pt("width"))]:`${y}px`}:null,...b!==Or.height?{[ht(Pt("height"))]:`${b}px`}:null,..._!==Or.border?{[ht(Pt("border"))]:`${_}px`}:null,...x?{[ht(Pt("border_style"))]:x}:null,...M?{[ht(Pt("border_color"))]:M}:null,...C!==Or.borderRadius?{[ht(Pt("border_radius"))]:`${C}px`}:null,...k!==Or.padding?{[ht(Pt("padding"))]:`${k}px`}:null,...N!==Or.gap?{[ht(Pt("gap"))]:`${N}px`}:null,...a.thumbnailsContainer}},F.createElement("nav",{ref:n,style:a.thumbnailsTrack,className:Wt(Te(Ar("track")),Te(ps)),"aria-label":Mr(l,"Thumbnails"),tabIndex:-1,...f},$.map(({key:O,index:T,slide:z})=>{const H=E/Math.abs(w||1),j=w>0&&T>d+L-w&&T<=d+L||w<0&&T<d-L-w&&T>=d-L?{duration:H,delay:((w>0?T-(d+L-w):d-L-w-T)-1)*H}:void 0,G=w>0&&T<d-L||w<0&&T>d+L?{duration:H,delay:(w>0?w-(d-L-T):-w-(T-(d+L)))*H}:void 0;return F.createElement(rE,{key:O,index:T,slide:z,fadeIn:j,fadeOut:G,placeholder:!z,onClick:A(T),onLoseFocus:()=>{var U;return(U=n.current)===null||U===void 0?void 0:U.focus()}})})),S&&F.createElement("div",{className:Te(Ar("vignette"))}))}const kp=F.createContext(null),oE=un("useThumbnails","ThumbnailsContext",kp);function iE({children:e,...t}){const{ref:n,position:r,hidden:s}=Lc(t.thumbnails),[o,i]=F.useState(!s),a=F.useRef(null),l=F.useMemo(()=>({visible:o,show:()=>i(!0),hide:()=>i(!1)}),[o]);return F.useImperativeHandle(n,()=>l,[l]),F.createElement($g,{...t},F.createElement(kp.Provider,{value:l},F.createElement("div",{ref:a,className:Wt(Te(Ar()),Te(Ar(`${r}`)))},["start","top"].includes(r)&&F.createElement(wp,{containerRef:a,visible:o}),F.createElement("div",{className:Te(Ar("wrapper"))},e),["end","bottom"].includes(r)&&F.createElement(wp,{containerRef:a,visible:o}))))}const Cp=()=>F.createElement(F.Fragment,null,F.createElement("path",{strokeWidth:2,stroke:"currentColor",strokeLinejoin:"round",fill:"none",d:"M3 5l18 0l0 14l-18 0l0-14z"}),F.createElement("path",{d:"M5 14h4v3h-4zM10 14h4v3h-4zM15 14h4v3h-4z"})),aE=nn("ThumbnailsVisible",Cp()),lE=H_("ThumbnailsHidden",Cp());function cE(){const{visible:e,show:t,hide:n}=oE(),{render:r}=Lt();return r.buttonThumbnails?F.createElement(F.Fragment,null,r.buttonThumbnails({visible:e,show:t,hide:n})):F.createElement(ws,{label:e?"Hide thumbnails":"Show thumbnails",icon:e?aE:lE,renderIcon:e?r.iconThumbnailsVisible:r.iconThumbnailsHidden,onClick:e?n:t})}function dE({augment:e,contains:t,append:n,addParent:r}){e(({thumbnails:o,toolbar:i,...a})=>{const l=Lc(o);return{toolbar:Yi(i,mo,l.showToggle?F.createElement(cE,null):null),thumbnails:l,...a}});const s=pn(mo,iE);t(po)?n(po,s):r(go,s)}const uE=window.G7Core?.createLogger?.("Comp:ImageGallery")??{log:(...e)=>console.log("[Comp:ImageGallery]",...e),warn:(...e)=>console.warn("[Comp:ImageGallery]",...e),error:(...e)=>console.error("[Comp:ImageGallery]",...e)},Fc=window.G7Core,Zi=(e,t)=>Fc?.t?.(e,t)??e,fE=async(e,t)=>{try{const n=await Fc.api.get(e,{responseType:"blob"});if(n){const r=URL.createObjectURL(n),s=document.createElement("a");s.href=r,s.download=t,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(r)}}catch(n){uE.error("Failed to download file:",n),Fc?.toast?.error?.(Zi("common.download_failed"))}},hE=(e,t)=>{const n=document.createElement("a");n.href=e,n.download=t,n.target="_blank",document.body.appendChild(n),n.click(),document.body.removeChild(n)},zc=async e=>{const t=e.downloadUrl||e.src,n=e.filename||e.title||"image";e.downloadRequiresAuth?await fE(t,n):hE(t,n)},gE=({image:e,index:t,onDownload:n})=>{const[r,s]=v.useState(!1),o=async i=>{if(i.stopPropagation(),n){n(e,t);return}s(!0);try{await zc(e)}finally{s(!1)}};return h.jsx(ae,{type:"button",onClick:o,disabled:r,className:"yarl__button flex items-center justify-center","aria-label":Zi("common.download"),title:Zi("common.download"),children:r?h.jsx(bt,{className:"fa-solid fa-spinner fa-spin text-white"}):h.jsx(bt,{className:"fa-solid fa-download text-white"})})},Qi=({images:e,isOpen:t,onClose:n,startIndex:r=0,enableZoom:s=!0,enableSlideshow:o=!1,enableFullscreen:i=!0,showCounter:a=!0,showDownload:l=!0,showThumbnails:c=!0,onDownload:d,editorAttrs:u,id:f})=>{const[g,m]=v.useState(r),p=v.useRef(r),y=e.map(x=>({src:x.src,title:x.title,description:x.description})),b=[];s&&b.push(L2),o&&b.push(W2),i&&b.push(Q2),a&&b.push(F2),c&&b.push(dE);const _=e[g];if(v.useEffect(()=>{p.current=g},[g]),u){const x=(e??[]).slice(0,4);return h.jsx(I,{id:f,className:"grid grid-cols-2 gap-2 p-3 rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800",...u,children:x.length>0?x.map((M,C)=>h.jsx(_n,{src:M.thumbnail||M.src,alt:M.title||"",className:"w-full aspect-square object-cover rounded bg-gray-200 dark:bg-gray-700"},C)):h.jsxs(I,{className:"col-span-2 flex items-center justify-center py-6 text-sm text-gray-400 dark:text-gray-500",children:[h.jsx(bt,{className:"fa-regular fa-images mr-2"}),Zi("editor.component.image_gallery")]})})}return h.jsx(b2,{open:t,close:n,slides:y,index:g,plugins:b,on:{view:({index:x})=>{x!==p.current&&m(x)}},zoom:{maxZoomPixelRatio:3,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:300,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,scrollToZoom:!0},carousel:{finite:!0,preload:2,padding:"16px",spacing:"30%"},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},controller:{closeOnBackdropClick:!0,closeOnPullDown:!0,closeOnPullUp:!0},thumbnails:{position:"bottom",width:120,height:80,border:2,borderRadius:4,padding:4,gap:16,showToggle:!1,vignette:!0},toolbar:{buttons:[l&&_&&h.jsx(gE,{image:_,index:g,onDownload:d},"download"),"close"].filter(Boolean)},styles:{container:{backgroundColor:"rgba(0, 0, 0, 0.9)"}}})},Sp=()=>{const[e,t]=v.useState(!1),[n,r]=v.useState([]),[s,o]=v.useState(0),i=v.useCallback((l,c=0)=>{r(l),o(c),t(!0)},[]),a=v.useCallback(()=>{t(!1)},[]);return{isOpen:e,openGallery:i,closeGallery:a,galleryProps:{images:n,isOpen:e,onClose:a,startIndex:s}}};Qi.displayName="ImageGallery";const pE=Object.freeze(Object.defineProperty({__proto__:null,ImageGallery:Qi,executeImageDownload:zc,useImageGallery:Sp},Symbol.toStringTag,{value:"Module"})),mE=v.createContext(null),bE=({value:e,children:t})=>h.jsx(mE.Provider,{value:e,children:t});function yE(e,t){return t.forEach((function(n){n&&typeof n!="string"&&!Array.isArray(n)&&Object.keys(n).forEach((function(r){if(r!=="default"&&!(r in e)){var s=Object.getOwnPropertyDescriptor(n,r);Object.defineProperty(e,r,s.get?s:{enumerable:!0,get:function(){return n[r]}})}}))})),Object.freeze(e)}function _p(e,t){return new Promise((function(n,r){let s;return vE(e).then((function(o){try{return s=o,n(new Blob([t.slice(0,2),s,t.slice(2)],{type:"image/jpeg"}))}catch(i){return r(i)}}),r)}))}const vE=e=>new Promise(((t,n)=>{const r=new FileReader;r.addEventListener("load",(({target:{result:s}})=>{const o=new DataView(s);let i=0;if(o.getUint16(i)!==65496)return n("not a valid JPEG");for(i+=2;;){const a=o.getUint16(i);if(a===65498)break;const l=o.getUint16(i+2);if(a===65505&&o.getUint32(i+4)===1165519206){const c=i+10;let d;switch(o.getUint16(c)){case 18761:d=!0;break;case 19789:d=!1;break;default:return n("TIFF header contains invalid endian")}if(o.getUint16(c+2,d)!==42)return n("TIFF header contains invalid version");const u=o.getUint32(c+4,d),f=c+u+2+12*o.getUint16(c+u,d);for(let g=c+u+2;g<f;g+=12)if(o.getUint16(g,d)==274){if(o.getUint16(g+2,d)!==3)return n("Orientation data type is invalid");if(o.getUint32(g+4,d)!==1)return n("Orientation data count is invalid");o.setUint16(g+8,1,d);break}return t(s.slice(i,i+2+l))}i+=2+l}return t(new Blob)})),r.readAsArrayBuffer(e)}));var Ji={},xE={get exports(){return Ji},set exports(e){Ji=e}};(function(e){var t,n,r={};xE.exports=r,r.parse=function(s,o){for(var i=r.bin.readUshort,a=r.bin.readUint,l=0,c={},d=new Uint8Array(s),u=d.length-4;a(d,u)!=101010256;)u--;l=u,l+=4;var f=i(d,l+=4);i(d,l+=2);var g=a(d,l+=2),m=a(d,l+=4);l+=4,l=m;for(var p=0;p<f;p++){a(d,l),l+=4,l+=4,l+=4,a(d,l+=4),g=a(d,l+=4);var y=a(d,l+=4),b=i(d,l+=4),_=i(d,l+2),x=i(d,l+4);l+=6;var M=a(d,l+=8);l+=4,l+=b+_+x,r._readLocal(d,M,c,g,y,o)}return c},r._readLocal=function(s,o,i,a,l,c){var d=r.bin.readUshort,u=r.bin.readUint;u(s,o),d(s,o+=4),d(s,o+=2);var f=d(s,o+=2);u(s,o+=2),u(s,o+=4),o+=4;var g=d(s,o+=8),m=d(s,o+=2);o+=2;var p=r.bin.readUTF8(s,o,g);if(o+=g,o+=m,c)i[p]={size:l,csize:a};else{var y=new Uint8Array(s.buffer,o);if(f==0)i[p]=new Uint8Array(y.buffer.slice(o,o+a));else{if(f!=8)throw"unknown compression method: "+f;var b=new Uint8Array(l);r.inflateRaw(y,b),i[p]=b}}},r.inflateRaw=function(s,o){return r.F.inflate(s,o)},r.inflate=function(s,o){return s[0],s[1],r.inflateRaw(new Uint8Array(s.buffer,s.byteOffset+2,s.length-6),o)},r.deflate=function(s,o){o==null&&(o={level:6});var i=0,a=new Uint8Array(50+Math.floor(1.1*s.length));a[i]=120,a[i+1]=156,i+=2,i=r.F.deflateRaw(s,a,i,o.level);var l=r.adler(s,0,s.length);return a[i+0]=l>>>24&255,a[i+1]=l>>>16&255,a[i+2]=l>>>8&255,a[i+3]=l>>>0&255,new Uint8Array(a.buffer,0,i+4)},r.deflateRaw=function(s,o){o==null&&(o={level:6});var i=new Uint8Array(50+Math.floor(1.1*s.length)),a=r.F.deflateRaw(s,i,a,o.level);return new Uint8Array(i.buffer,0,a)},r.encode=function(s,o){o==null&&(o=!1);var i=0,a=r.bin.writeUint,l=r.bin.writeUshort,c={};for(var d in s){var u=!r._noNeed(d)&&!o,f=s[d],g=r.crc.crc(f,0,f.length);c[d]={cpr:u,usize:f.length,crc:g,file:u?r.deflateRaw(f):f}}for(var d in c)i+=c[d].file.length+30+46+2*r.bin.sizeUTF8(d);i+=22;var m=new Uint8Array(i),p=0,y=[];for(var d in c){var b=c[d];y.push(p),p=r._writeHeader(m,p,d,b,0)}var _=0,x=p;for(var d in c)b=c[d],y.push(p),p=r._writeHeader(m,p,d,b,1,y[_++]);var M=p-x;return a(m,p,101010256),p+=4,l(m,p+=4,_),l(m,p+=2,_),a(m,p+=2,M),a(m,p+=4,x),p+=4,p+=2,m.buffer},r._noNeed=function(s){var o=s.split(".").pop().toLowerCase();return"png,jpg,jpeg,zip".indexOf(o)!=-1},r._writeHeader=function(s,o,i,a,l,c){var d=r.bin.writeUint,u=r.bin.writeUshort,f=a.file;return d(s,o,l==0?67324752:33639248),o+=4,l==1&&(o+=2),u(s,o,20),u(s,o+=2,0),u(s,o+=2,a.cpr?8:0),d(s,o+=2,0),d(s,o+=4,a.crc),d(s,o+=4,f.length),d(s,o+=4,a.usize),u(s,o+=4,r.bin.sizeUTF8(i)),u(s,o+=2,0),o+=2,l==1&&(o+=2,o+=2,d(s,o+=6,c),o+=4),o+=r.bin.writeUTF8(s,o,i),l==0&&(s.set(f,o),o+=f.length),o},r.crc={table:(function(){for(var s=new Uint32Array(256),o=0;o<256;o++){for(var i=o,a=0;a<8;a++)1&i?i=3988292384^i>>>1:i>>>=1;s[o]=i}return s})(),update:function(s,o,i,a){for(var l=0;l<a;l++)s=r.crc.table[255&(s^o[i+l])]^s>>>8;return s},crc:function(s,o,i){return 4294967295^r.crc.update(4294967295,s,o,i)}},r.adler=function(s,o,i){for(var a=1,l=0,c=o,d=o+i;c<d;){for(var u=Math.min(c+5552,d);c<u;)l+=a+=s[c++];a%=65521,l%=65521}return l<<16|a},r.bin={readUshort:function(s,o){return s[o]|s[o+1]<<8},writeUshort:function(s,o,i){s[o]=255&i,s[o+1]=i>>8&255},readUint:function(s,o){return 16777216*s[o+3]+(s[o+2]<<16|s[o+1]<<8|s[o])},writeUint:function(s,o,i){s[o]=255&i,s[o+1]=i>>8&255,s[o+2]=i>>16&255,s[o+3]=i>>24&255},readASCII:function(s,o,i){for(var a="",l=0;l<i;l++)a+=String.fromCharCode(s[o+l]);return a},writeASCII:function(s,o,i){for(var a=0;a<i.length;a++)s[o+a]=i.charCodeAt(a)},pad:function(s){return s.length<2?"0"+s:s},readUTF8:function(s,o,i){for(var a,l="",c=0;c<i;c++)l+="%"+r.bin.pad(s[o+c].toString(16));try{a=decodeURIComponent(l)}catch{return r.bin.readASCII(s,o,i)}return a},writeUTF8:function(s,o,i){for(var a=i.length,l=0,c=0;c<a;c++){var d=i.charCodeAt(c);if((4294967168&d)==0)s[o+l]=d,l++;else if((4294965248&d)==0)s[o+l]=192|d>>6,s[o+l+1]=128|d>>0&63,l+=2;else if((4294901760&d)==0)s[o+l]=224|d>>12,s[o+l+1]=128|d>>6&63,s[o+l+2]=128|d>>0&63,l+=3;else{if((4292870144&d)!=0)throw"e";s[o+l]=240|d>>18,s[o+l+1]=128|d>>12&63,s[o+l+2]=128|d>>6&63,s[o+l+3]=128|d>>0&63,l+=4}}return l},sizeUTF8:function(s){for(var o=s.length,i=0,a=0;a<o;a++){var l=s.charCodeAt(a);if((4294967168&l)==0)i++;else if((4294965248&l)==0)i+=2;else if((4294901760&l)==0)i+=3;else{if((4292870144&l)!=0)throw"e";i+=4}}return i}},r.F={},r.F.deflateRaw=function(s,o,i,a){var l=[[0,0,0,0,0],[4,4,8,4,0],[4,5,16,8,0],[4,6,16,16,0],[4,10,16,32,0],[8,16,32,32,0],[8,16,128,128,0],[8,32,128,256,0],[32,128,258,1024,1],[32,258,258,4096,1]][a],c=r.F.U,d=r.F._goodIndex;r.F._hash;var u=r.F._putsE,f=0,g=i<<3,m=0,p=s.length;if(a==0){for(;f<p;)u(o,g,f+(D=Math.min(65535,p-f))==p?1:0),g=r.F._copyExact(s,f,D,o,g+8),f+=D;return g>>>3}var y=c.lits,b=c.strt,_=c.prev,x=0,M=0,C=0,k=0,N=0,S=0;for(p>2&&(b[S=r.F._hash(s,0)]=0),f=0;f<p;f++){if(N=S,f+1<p-2){S=r.F._hash(s,f+1);var w=f+1&32767;_[w]=b[S],b[S]=w}if(m<=f){(x>14e3||M>26697)&&p-f>100&&(m<f&&(y[x]=f-m,x+=2,m=f),g=r.F._writeBlock(f==p-1||m==p?1:0,y,x,k,s,C,f-C,o,g),x=M=k=0,C=f);var E=0;f<p-2&&(E=r.F._bestMatch(s,f,_,N,Math.min(l[2],p-f),l[3]));var D=E>>>16,P=65535&E;if(E!=0){P=65535&E;var L=d(D=E>>>16,c.of0);c.lhst[257+L]++;var $=d(P,c.df0);c.dhst[$]++,k+=c.exb[L]+c.dxb[$],y[x]=D<<23|f-m,y[x+1]=P<<16|L<<8|$,x+=2,m=f+D}else c.lhst[s[f]]++;M++}}for(C==f&&s.length!=0||(m<f&&(y[x]=f-m,x+=2,m=f),g=r.F._writeBlock(1,y,x,k,s,C,f-C,o,g),x=0,M=0,x=M=k=0,C=f);(7&g)!=0;)g++;return g>>>3},r.F._bestMatch=function(s,o,i,a,l,c){var d=32767&o,u=i[d],f=d-u+32768&32767;if(u==d||a!=r.F._hash(s,o-f))return 0;for(var g=0,m=0,p=Math.min(32767,o);f<=p&&--c!=0&&u!=d;){if(g==0||s[o+g]==s[o+g-f]){var y=r.F._howLong(s,o,f);if(y>g){if(m=f,(g=y)>=l)break;f+2<y&&(y=f+2);for(var b=0,_=0;_<y-2;_++){var x=o-f+_+32768&32767,M=x-i[x]+32768&32767;M>b&&(b=M,u=x)}}}f+=(d=u)-(u=i[d])+32768&32767}return g<<16|m},r.F._howLong=function(s,o,i){if(s[o]!=s[o-i]||s[o+1]!=s[o+1-i]||s[o+2]!=s[o+2-i])return 0;var a=o,l=Math.min(s.length,o+258);for(o+=3;o<l&&s[o]==s[o-i];)o++;return o-a},r.F._hash=function(s,o){return(s[o]<<8|s[o+1])+(s[o+2]<<4)&65535},r.saved=0,r.F._writeBlock=function(s,o,i,a,l,c,d,u,f){var g,m,p,y,b,_,x,M,C,k=r.F.U,N=r.F._putsF,S=r.F._putsE;k.lhst[256]++,m=(g=r.F.getTrees())[0],p=g[1],y=g[2],b=g[3],_=g[4],x=g[5],M=g[6],C=g[7];var w=32+((f+3&7)==0?0:8-(f+3&7))+(d<<3),E=a+r.F.contSize(k.fltree,k.lhst)+r.F.contSize(k.fdtree,k.dhst),D=a+r.F.contSize(k.ltree,k.lhst)+r.F.contSize(k.dtree,k.dhst);D+=14+3*x+r.F.contSize(k.itree,k.ihst)+(2*k.ihst[16]+3*k.ihst[17]+7*k.ihst[18]);for(var P=0;P<286;P++)k.lhst[P]=0;for(P=0;P<30;P++)k.dhst[P]=0;for(P=0;P<19;P++)k.ihst[P]=0;var L=w<E&&w<D?0:E<D?1:2;if(N(u,f,s),N(u,f+1,L),f+=3,L==0){for(;(7&f)!=0;)f++;f=r.F._copyExact(l,c,d,u,f)}else{var $,A;if(L==1&&($=k.fltree,A=k.fdtree),L==2){r.F.makeCodes(k.ltree,m),r.F.revCodes(k.ltree,m),r.F.makeCodes(k.dtree,p),r.F.revCodes(k.dtree,p),r.F.makeCodes(k.itree,y),r.F.revCodes(k.itree,y),$=k.ltree,A=k.dtree,S(u,f,b-257),S(u,f+=5,_-1),S(u,f+=5,x-4),f+=4;for(var O=0;O<x;O++)S(u,f+3*O,k.itree[1+(k.ordr[O]<<1)]);f+=3*x,f=r.F._codeTiny(M,k.itree,u,f),f=r.F._codeTiny(C,k.itree,u,f)}for(var T=c,z=0;z<i;z+=2){for(var H=o[z],j=H>>>23,G=T+(8388607&H);T<G;)f=r.F._writeLit(l[T++],$,u,f);if(j!=0){var U=o[z+1],W=U>>16,Y=U>>8&255,V=255&U;S(u,f=r.F._writeLit(257+Y,$,u,f),j-k.of0[Y]),f+=k.exb[Y],N(u,f=r.F._writeLit(V,A,u,f),W-k.df0[V]),f+=k.dxb[V],T+=j}}f=r.F._writeLit(256,$,u,f)}return f},r.F._copyExact=function(s,o,i,a,l){var c=l>>>3;return a[c]=i,a[c+1]=i>>>8,a[c+2]=255-a[c],a[c+3]=255-a[c+1],c+=4,a.set(new Uint8Array(s.buffer,o,i),c),l+(i+4<<3)},r.F.getTrees=function(){for(var s=r.F.U,o=r.F._hufTree(s.lhst,s.ltree,15),i=r.F._hufTree(s.dhst,s.dtree,15),a=[],l=r.F._lenCodes(s.ltree,a),c=[],d=r.F._lenCodes(s.dtree,c),u=0;u<a.length;u+=2)s.ihst[a[u]]++;for(u=0;u<c.length;u+=2)s.ihst[c[u]]++;for(var f=r.F._hufTree(s.ihst,s.itree,7),g=19;g>4&&s.itree[1+(s.ordr[g-1]<<1)]==0;)g--;return[o,i,f,l,d,g,a,c]},r.F.getSecond=function(s){for(var o=[],i=0;i<s.length;i+=2)o.push(s[i+1]);return o},r.F.nonZero=function(s){for(var o="",i=0;i<s.length;i+=2)s[i+1]!=0&&(o+=(i>>1)+",");return o},r.F.contSize=function(s,o){for(var i=0,a=0;a<o.length;a++)i+=o[a]*s[1+(a<<1)];return i},r.F._codeTiny=function(s,o,i,a){for(var l=0;l<s.length;l+=2){var c=s[l],d=s[l+1];a=r.F._writeLit(c,o,i,a);var u=c==16?2:c==17?3:7;c>15&&(r.F._putsE(i,a,d,u),a+=u)}return a},r.F._lenCodes=function(s,o){for(var i=s.length;i!=2&&s[i-1]==0;)i-=2;for(var a=0;a<i;a+=2){var l=s[a+1],c=a+3<i?s[a+3]:-1,d=a+5<i?s[a+5]:-1,u=a==0?-1:s[a-1];if(l==0&&c==l&&d==l){for(var f=a+5;f+2<i&&s[f+2]==l;)f+=2;(g=Math.min(f+1-a>>>1,138))<11?o.push(17,g-3):o.push(18,g-11),a+=2*g-2}else if(l==u&&c==l&&d==l){for(f=a+5;f+2<i&&s[f+2]==l;)f+=2;var g=Math.min(f+1-a>>>1,6);o.push(16,g-3),a+=2*g-2}else o.push(l,0)}return i>>>1},r.F._hufTree=function(s,o,i){var a=[],l=s.length,c=o.length,d=0;for(d=0;d<c;d+=2)o[d]=0,o[d+1]=0;for(d=0;d<l;d++)s[d]!=0&&a.push({lit:d,f:s[d]});var u=a.length,f=a.slice(0);if(u==0)return 0;if(u==1){var g=a[0].lit;return f=g==0?1:0,o[1+(g<<1)]=1,o[1+(f<<1)]=1,1}a.sort((function(M,C){return M.f-C.f}));var m=a[0],p=a[1],y=0,b=1,_=2;for(a[0]={lit:-1,f:m.f+p.f,l:m,r:p,d:0};b!=u-1;)m=y!=b&&(_==u||a[y].f<a[_].f)?a[y++]:a[_++],p=y!=b&&(_==u||a[y].f<a[_].f)?a[y++]:a[_++],a[b++]={lit:-1,f:m.f+p.f,l:m,r:p};var x=r.F.setDepth(a[b-1],0);for(x>i&&(r.F.restrictDepth(f,i,x),x=i),d=0;d<u;d++)o[1+(f[d].lit<<1)]=f[d].d;return x},r.F.setDepth=function(s,o){return s.lit!=-1?(s.d=o,o):Math.max(r.F.setDepth(s.l,o+1),r.F.setDepth(s.r,o+1))},r.F.restrictDepth=function(s,o,i){var a=0,l=1<<i-o,c=0;for(s.sort((function(u,f){return f.d==u.d?u.f-f.f:f.d-u.d})),a=0;a<s.length&&s[a].d>o;a++){var d=s[a].d;s[a].d=o,c+=l-(1<<i-d)}for(c>>>=i-o;c>0;)(d=s[a].d)<o?(s[a].d++,c-=1<<o-d-1):a++;for(;a>=0;a--)s[a].d==o&&c<0&&(s[a].d--,c++);c!=0&&console.log("debt left")},r.F._goodIndex=function(s,o){var i=0;return o[16|i]<=s&&(i|=16),o[8|i]<=s&&(i|=8),o[4|i]<=s&&(i|=4),o[2|i]<=s&&(i|=2),o[1|i]<=s&&(i|=1),i},r.F._writeLit=function(s,o,i,a){return r.F._putsF(i,a,o[s<<1]),a+o[1+(s<<1)]},r.F.inflate=function(s,o){var i=Uint8Array;if(s[0]==3&&s[1]==0)return o||new i(0);var a=r.F,l=a._bitsF,c=a._bitsE,d=a._decodeTiny,u=a.makeCodes,f=a.codes2map,g=a._get17,m=a.U,p=o==null;p&&(o=new i(s.length>>>2<<3));for(var y,b,_=0,x=0,M=0,C=0,k=0,N=0,S=0,w=0,E=0;_==0;)if(_=l(s,E,1),x=l(s,E+1,2),E+=3,x!=0){if(p&&(o=r.F._check(o,w+131072)),x==1&&(y=m.flmap,b=m.fdmap,N=511,S=31),x==2){M=c(s,E,5)+257,C=c(s,E+5,5)+1,k=c(s,E+10,4)+4,E+=14;for(var D=0;D<38;D+=2)m.itree[D]=0,m.itree[D+1]=0;var P=1;for(D=0;D<k;D++){var L=c(s,E+3*D,3);m.itree[1+(m.ordr[D]<<1)]=L,L>P&&(P=L)}E+=3*k,u(m.itree,P),f(m.itree,P,m.imap),y=m.lmap,b=m.dmap,E=d(m.imap,(1<<P)-1,M+C,s,E,m.ttree);var $=a._copyOut(m.ttree,0,M,m.ltree);N=(1<<$)-1;var A=a._copyOut(m.ttree,M,C,m.dtree);S=(1<<A)-1,u(m.ltree,$),f(m.ltree,$,y),u(m.dtree,A),f(m.dtree,A,b)}for(;;){var O=y[g(s,E)&N];E+=15&O;var T=O>>>4;if(!(T>>>8))o[w++]=T;else{if(T==256)break;var z=w+T-254;if(T>264){var H=m.ldef[T-257];z=w+(H>>>3)+c(s,E,7&H),E+=7&H}var j=b[g(s,E)&S];E+=15&j;var G=j>>>4,U=m.ddef[G],W=(U>>>4)+l(s,E,15&U);for(E+=15&U,p&&(o=r.F._check(o,w+131072));w<z;)o[w]=o[w++-W],o[w]=o[w++-W],o[w]=o[w++-W],o[w]=o[w++-W];w=z}}}else{(7&E)!=0&&(E+=8-(7&E));var Y=4+(E>>>3),V=s[Y-4]|s[Y-3]<<8;p&&(o=r.F._check(o,w+V)),o.set(new i(s.buffer,s.byteOffset+Y,V),w),E=Y+V<<3,w+=V}return o.length==w?o:o.slice(0,w)},r.F._check=function(s,o){var i=s.length;if(o<=i)return s;var a=new Uint8Array(Math.max(i<<1,o));return a.set(s,0),a},r.F._decodeTiny=function(s,o,i,a,l,c){for(var d=r.F._bitsE,u=r.F._get17,f=0;f<i;){var g=s[u(a,l)&o];l+=15&g;var m=g>>>4;if(m<=15)c[f]=m,f++;else{var p=0,y=0;m==16?(y=3+d(a,l,2),l+=2,p=c[f-1]):m==17?(y=3+d(a,l,3),l+=3):m==18&&(y=11+d(a,l,7),l+=7);for(var b=f+y;f<b;)c[f]=p,f++}}return l},r.F._copyOut=function(s,o,i,a){for(var l=0,c=0,d=a.length>>>1;c<i;){var u=s[c+o];a[c<<1]=0,a[1+(c<<1)]=u,u>l&&(l=u),c++}for(;c<d;)a[c<<1]=0,a[1+(c<<1)]=0,c++;return l},r.F.makeCodes=function(s,o){for(var i,a,l,c,d=r.F.U,u=s.length,f=d.bl_count,g=0;g<=o;g++)f[g]=0;for(g=1;g<u;g+=2)f[s[g]]++;var m=d.next_code;for(i=0,f[0]=0,a=1;a<=o;a++)i=i+f[a-1]<<1,m[a]=i;for(l=0;l<u;l+=2)(c=s[l+1])!=0&&(s[l]=m[c],m[c]++)},r.F.codes2map=function(s,o,i){for(var a=s.length,l=r.F.U.rev15,c=0;c<a;c+=2)if(s[c+1]!=0)for(var d=c>>1,u=s[c+1],f=d<<4|u,g=o-u,m=s[c]<<g,p=m+(1<<g);m!=p;)i[l[m]>>>15-o]=f,m++},r.F.revCodes=function(s,o){for(var i=r.F.U.rev15,a=15-o,l=0;l<s.length;l+=2){var c=s[l]<<o-s[l+1];s[l]=i[c]>>>a}},r.F._putsE=function(s,o,i){i<<=7&o;var a=o>>>3;s[a]|=i,s[a+1]|=i>>>8},r.F._putsF=function(s,o,i){i<<=7&o;var a=o>>>3;s[a]|=i,s[a+1]|=i>>>8,s[a+2]|=i>>>16},r.F._bitsE=function(s,o,i){return(s[o>>>3]|s[1+(o>>>3)]<<8)>>>(7&o)&(1<<i)-1},r.F._bitsF=function(s,o,i){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16)>>>(7&o)&(1<<i)-1},r.F._get17=function(s,o){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16)>>>(7&o)},r.F._get25=function(s,o){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16|s[3+(o>>>3)]<<24)>>>(7&o)},r.F.U=(t=Uint16Array,n=Uint32Array,{next_code:new t(16),bl_count:new t(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new t(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new n(32),flmap:new t(512),fltree:[],fdmap:new t(32),fdtree:[],lmap:new t(32768),ltree:[],ttree:[],dmap:new t(32768),dtree:[],imap:new t(512),itree:[],rev15:new t(32768),lhst:new n(286),dhst:new n(30),ihst:new n(19),lits:new n(15e3),strt:new t(65536),prev:new t(32768)}),(function(){for(var s=r.F.U,o=0;o<32768;o++){var i=o;i=(4278255360&(i=(4042322160&(i=(3435973836&(i=(2863311530&i)>>>1|(1431655765&i)<<1))>>>2|(858993459&i)<<2))>>>4|(252645135&i)<<4))>>>8|(16711935&i)<<8,s.rev15[o]=(i>>>16|i<<16)>>>17}function a(l,c,d){for(;c--!=0;)l.push(0,d)}for(o=0;o<32;o++)s.ldef[o]=s.of0[o]<<3|s.exb[o],s.ddef[o]=s.df0[o]<<4|s.dxb[o];a(s.fltree,144,8),a(s.fltree,112,9),a(s.fltree,24,7),a(s.fltree,8,8),r.F.makeCodes(s.fltree,9),r.F.codes2map(s.fltree,9,s.flmap),r.F.revCodes(s.fltree,9),a(s.fdtree,32,5),r.F.makeCodes(s.fdtree,5),r.F.codes2map(s.fdtree,5,s.fdmap),r.F.revCodes(s.fdtree,5),a(s.itree,19,0),a(s.ltree,286,0),a(s.dtree,30,0),a(s.ttree,320,0)})()})();var wE=yE({__proto__:null,default:Ji},[Ji]);const Dn=(function(){var e={nextZero(d,u){for(;d[u]!=0;)u++;return u},readUshort:(d,u)=>d[u]<<8|d[u+1],writeUshort(d,u,f){d[u]=f>>8&255,d[u+1]=255&f},readUint:(d,u)=>16777216*d[u]+(d[u+1]<<16|d[u+2]<<8|d[u+3]),writeUint(d,u,f){d[u]=f>>24&255,d[u+1]=f>>16&255,d[u+2]=f>>8&255,d[u+3]=255&f},readASCII(d,u,f){let g="";for(let m=0;m<f;m++)g+=String.fromCharCode(d[u+m]);return g},writeASCII(d,u,f){for(let g=0;g<f.length;g++)d[u+g]=f.charCodeAt(g)},readBytes(d,u,f){const g=[];for(let m=0;m<f;m++)g.push(d[u+m]);return g},pad:d=>d.length<2?`0${d}`:d,readUTF8(d,u,f){let g,m="";for(let p=0;p<f;p++)m+=`%${e.pad(d[u+p].toString(16))}`;try{g=decodeURIComponent(m)}catch{return e.readASCII(d,u,f)}return g}};function t(d,u,f,g){const m=u*f,p=o(g),y=Math.ceil(u*p/8),b=new Uint8Array(4*m),_=new Uint32Array(b.buffer),{ctype:x}=g,{depth:M}=g,C=e.readUshort;if(x==6){const H=m<<2;if(M==8)for(var k=0;k<H;k+=4)b[k]=d[k],b[k+1]=d[k+1],b[k+2]=d[k+2],b[k+3]=d[k+3];if(M==16)for(k=0;k<H;k++)b[k]=d[k<<1]}else if(x==2){const H=g.tabs.tRNS;if(H==null){if(M==8)for(k=0;k<m;k++){var N=3*k;_[k]=255<<24|d[N+2]<<16|d[N+1]<<8|d[N]}if(M==16)for(k=0;k<m;k++)N=6*k,_[k]=255<<24|d[N+4]<<16|d[N+2]<<8|d[N]}else{var S=H[0];const j=H[1],G=H[2];if(M==8)for(k=0;k<m;k++){var w=k<<2;N=3*k,_[k]=255<<24|d[N+2]<<16|d[N+1]<<8|d[N],d[N]==S&&d[N+1]==j&&d[N+2]==G&&(b[w+3]=0)}if(M==16)for(k=0;k<m;k++)w=k<<2,N=6*k,_[k]=255<<24|d[N+4]<<16|d[N+2]<<8|d[N],C(d,N)==S&&C(d,N+2)==j&&C(d,N+4)==G&&(b[w+3]=0)}}else if(x==3){const H=g.tabs.PLTE,j=g.tabs.tRNS,G=j?j.length:0;if(M==1)for(var E=0;E<f;E++){var D=E*y,P=E*u;for(k=0;k<u;k++){w=P+k<<2;var L=3*($=d[D+(k>>3)]>>7-((7&k)<<0)&1);b[w]=H[L],b[w+1]=H[L+1],b[w+2]=H[L+2],b[w+3]=$<G?j[$]:255}}if(M==2)for(E=0;E<f;E++)for(D=E*y,P=E*u,k=0;k<u;k++)w=P+k<<2,L=3*($=d[D+(k>>2)]>>6-((3&k)<<1)&3),b[w]=H[L],b[w+1]=H[L+1],b[w+2]=H[L+2],b[w+3]=$<G?j[$]:255;if(M==4)for(E=0;E<f;E++)for(D=E*y,P=E*u,k=0;k<u;k++)w=P+k<<2,L=3*($=d[D+(k>>1)]>>4-((1&k)<<2)&15),b[w]=H[L],b[w+1]=H[L+1],b[w+2]=H[L+2],b[w+3]=$<G?j[$]:255;if(M==8)for(k=0;k<m;k++){var $;w=k<<2,L=3*($=d[k]),b[w]=H[L],b[w+1]=H[L+1],b[w+2]=H[L+2],b[w+3]=$<G?j[$]:255}}else if(x==4){if(M==8)for(k=0;k<m;k++){w=k<<2;var A=d[O=k<<1];b[w]=A,b[w+1]=A,b[w+2]=A,b[w+3]=d[O+1]}if(M==16)for(k=0;k<m;k++){var O;w=k<<2,A=d[O=k<<2],b[w]=A,b[w+1]=A,b[w+2]=A,b[w+3]=d[O+2]}}else if(x==0)for(S=g.tabs.tRNS?g.tabs.tRNS:-1,E=0;E<f;E++){const H=E*y,j=E*u;if(M==1)for(var T=0;T<u;T++){var z=(A=255*(d[H+(T>>>3)]>>>7-(7&T)&1))==255*S?0:255;_[j+T]=z<<24|A<<16|A<<8|A}else if(M==2)for(T=0;T<u;T++)z=(A=85*(d[H+(T>>>2)]>>>6-((3&T)<<1)&3))==85*S?0:255,_[j+T]=z<<24|A<<16|A<<8|A;else if(M==4)for(T=0;T<u;T++)z=(A=17*(d[H+(T>>>1)]>>>4-((1&T)<<2)&15))==17*S?0:255,_[j+T]=z<<24|A<<16|A<<8|A;else if(M==8)for(T=0;T<u;T++)z=(A=d[H+T])==S?0:255,_[j+T]=z<<24|A<<16|A<<8|A;else if(M==16)for(T=0;T<u;T++)A=d[H+(T<<1)],z=C(d,H+(T<<1))==S?0:255,_[j+T]=z<<24|A<<16|A<<8|A}return b}function n(d,u,f,g){const m=o(d),p=Math.ceil(f*m/8),y=new Uint8Array((p+1+d.interlace)*g);return u=d.tabs.CgBI?s(u,y):r(u,y),d.interlace==0?u=i(u,d,0,f,g):d.interlace==1&&(u=(function(_,x){const M=x.width,C=x.height,k=o(x),N=k>>3,S=Math.ceil(M*k/8),w=new Uint8Array(C*S);let E=0;const D=[0,0,4,0,2,0,1],P=[0,4,0,2,0,1,0],L=[8,8,8,4,4,2,2],$=[8,8,4,4,2,2,1];let A=0;for(;A<7;){const T=L[A],z=$[A];let H=0,j=0,G=D[A];for(;G<C;)G+=T,j++;let U=P[A];for(;U<M;)U+=z,H++;const W=Math.ceil(H*k/8);i(_,x,E,H,j);let Y=0,V=D[A];for(;V<C;){let X=P[A],K=E+Y*W<<3;for(;X<M;){var O;if(k==1&&(O=(O=_[K>>3])>>7-(7&K)&1,w[V*S+(X>>3)]|=O<<7-((7&X)<<0)),k==2&&(O=(O=_[K>>3])>>6-(7&K)&3,w[V*S+(X>>2)]|=O<<6-((3&X)<<1)),k==4&&(O=(O=_[K>>3])>>4-(7&K)&15,w[V*S+(X>>1)]|=O<<4-((1&X)<<2)),k>=8){const q=V*S+X*N;for(let ne=0;ne<N;ne++)w[q+ne]=_[(K>>3)+ne]}K+=k,X+=z}Y++,V+=T}H*j!=0&&(E+=j*(1+W)),A+=1}return w})(u,d)),u}function r(d,u){return s(new Uint8Array(d.buffer,2,d.length-6),u)}var s=(function(){const d={H:{}};return d.H.N=function(u,f){const g=Uint8Array;let m,p,y=0,b=0,_=0,x=0,M=0,C=0,k=0,N=0,S=0;if(u[0]==3&&u[1]==0)return f||new g(0);const w=d.H,E=w.b,D=w.e,P=w.R,L=w.n,$=w.A,A=w.Z,O=w.m,T=f==null;for(T&&(f=new g(u.length>>>2<<5));y==0;)if(y=E(u,S,1),b=E(u,S+1,2),S+=3,b!=0){if(T&&(f=d.H.W(f,N+(1<<17))),b==1&&(m=O.J,p=O.h,C=511,k=31),b==2){_=D(u,S,5)+257,x=D(u,S+5,5)+1,M=D(u,S+10,4)+4,S+=14;let H=1;for(var z=0;z<38;z+=2)O.Q[z]=0,O.Q[z+1]=0;for(z=0;z<M;z++){const U=D(u,S+3*z,3);O.Q[1+(O.X[z]<<1)]=U,U>H&&(H=U)}S+=3*M,L(O.Q,H),$(O.Q,H,O.u),m=O.w,p=O.d,S=P(O.u,(1<<H)-1,_+x,u,S,O.v);const j=w.V(O.v,0,_,O.C);C=(1<<j)-1;const G=w.V(O.v,_,x,O.D);k=(1<<G)-1,L(O.C,j),$(O.C,j,m),L(O.D,G),$(O.D,G,p)}for(;;){const H=m[A(u,S)&C];S+=15&H;const j=H>>>4;if(!(j>>>8))f[N++]=j;else{if(j==256)break;{let G=N+j-254;if(j>264){const X=O.q[j-257];G=N+(X>>>3)+D(u,S,7&X),S+=7&X}const U=p[A(u,S)&k];S+=15&U;const W=U>>>4,Y=O.c[W],V=(Y>>>4)+E(u,S,15&Y);for(S+=15&Y;N<G;)f[N]=f[N++-V],f[N]=f[N++-V],f[N]=f[N++-V],f[N]=f[N++-V];N=G}}}}else{(7&S)!=0&&(S+=8-(7&S));const H=4+(S>>>3),j=u[H-4]|u[H-3]<<8;T&&(f=d.H.W(f,N+j)),f.set(new g(u.buffer,u.byteOffset+H,j),N),S=H+j<<3,N+=j}return f.length==N?f:f.slice(0,N)},d.H.W=function(u,f){const g=u.length;if(f<=g)return u;const m=new Uint8Array(g<<1);return m.set(u,0),m},d.H.R=function(u,f,g,m,p,y){const b=d.H.e,_=d.H.Z;let x=0;for(;x<g;){const M=u[_(m,p)&f];p+=15&M;const C=M>>>4;if(C<=15)y[x]=C,x++;else{let k=0,N=0;C==16?(N=3+b(m,p,2),p+=2,k=y[x-1]):C==17?(N=3+b(m,p,3),p+=3):C==18&&(N=11+b(m,p,7),p+=7);const S=x+N;for(;x<S;)y[x]=k,x++}}return p},d.H.V=function(u,f,g,m){let p=0,y=0;const b=m.length>>>1;for(;y<g;){const _=u[y+f];m[y<<1]=0,m[1+(y<<1)]=_,_>p&&(p=_),y++}for(;y<b;)m[y<<1]=0,m[1+(y<<1)]=0,y++;return p},d.H.n=function(u,f){const g=d.H.m,m=u.length;let p,y,b,_;const x=g.j;for(var M=0;M<=f;M++)x[M]=0;for(M=1;M<m;M+=2)x[u[M]]++;const C=g.K;for(p=0,x[0]=0,y=1;y<=f;y++)p=p+x[y-1]<<1,C[y]=p;for(b=0;b<m;b+=2)_=u[b+1],_!=0&&(u[b]=C[_],C[_]++)},d.H.A=function(u,f,g){const m=u.length,p=d.H.m.r;for(let y=0;y<m;y+=2)if(u[y+1]!=0){const b=y>>1,_=u[y+1],x=b<<4|_,M=f-_;let C=u[y]<<M;const k=C+(1<<M);for(;C!=k;)g[p[C]>>>15-f]=x,C++}},d.H.l=function(u,f){const g=d.H.m.r,m=15-f;for(let p=0;p<u.length;p+=2){const y=u[p]<<f-u[p+1];u[p]=g[y]>>>m}},d.H.M=function(u,f,g){g<<=7&f;const m=f>>>3;u[m]|=g,u[m+1]|=g>>>8},d.H.I=function(u,f,g){g<<=7&f;const m=f>>>3;u[m]|=g,u[m+1]|=g>>>8,u[m+2]|=g>>>16},d.H.e=function(u,f,g){return(u[f>>>3]|u[1+(f>>>3)]<<8)>>>(7&f)&(1<<g)-1},d.H.b=function(u,f,g){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16)>>>(7&f)&(1<<g)-1},d.H.Z=function(u,f){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16)>>>(7&f)},d.H.i=function(u,f){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16|u[3+(f>>>3)]<<24)>>>(7&f)},d.H.m=(function(){const u=Uint16Array,f=Uint32Array;return{K:new u(16),j:new u(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new u(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new f(32),J:new u(512),_:[],h:new u(32),$:[],w:new u(32768),C:[],v:[],d:new u(32768),D:[],u:new u(512),Q:[],r:new u(32768),s:new f(286),Y:new f(30),a:new f(19),t:new f(15e3),k:new u(65536),g:new u(32768)}})(),(function(){const u=d.H.m;for(var f=0;f<32768;f++){let m=f;m=(2863311530&m)>>>1|(1431655765&m)<<1,m=(3435973836&m)>>>2|(858993459&m)<<2,m=(4042322160&m)>>>4|(252645135&m)<<4,m=(4278255360&m)>>>8|(16711935&m)<<8,u.r[f]=(m>>>16|m<<16)>>>17}function g(m,p,y){for(;p--!=0;)m.push(0,y)}for(f=0;f<32;f++)u.q[f]=u.S[f]<<3|u.T[f],u.c[f]=u.p[f]<<4|u.z[f];g(u._,144,8),g(u._,112,9),g(u._,24,7),g(u._,8,8),d.H.n(u._,9),d.H.A(u._,9,u.J),d.H.l(u._,9),g(u.$,32,5),d.H.n(u.$,5),d.H.A(u.$,5,u.h),d.H.l(u.$,5),g(u.Q,19,0),g(u.C,286,0),g(u.D,30,0),g(u.v,320,0)})(),d.H.N})();function o(d){return[1,null,3,1,2,null,4][d.ctype]*d.depth}function i(d,u,f,g,m){let p=o(u);const y=Math.ceil(g*p/8);let b,_;p=Math.ceil(p/8);let x=d[f],M=0;if(x>1&&(d[f]=[0,0,1][x-2]),x==3)for(M=p;M<y;M++)d[M+1]=d[M+1]+(d[M+1-p]>>>1)&255;for(let C=0;C<m;C++)if(b=f+C*y,_=b+C+1,x=d[_-1],M=0,x==0)for(;M<y;M++)d[b+M]=d[_+M];else if(x==1){for(;M<p;M++)d[b+M]=d[_+M];for(;M<y;M++)d[b+M]=d[_+M]+d[b+M-p]}else if(x==2)for(;M<y;M++)d[b+M]=d[_+M]+d[b+M-y];else if(x==3){for(;M<p;M++)d[b+M]=d[_+M]+(d[b+M-y]>>>1);for(;M<y;M++)d[b+M]=d[_+M]+(d[b+M-y]+d[b+M-p]>>>1)}else{for(;M<p;M++)d[b+M]=d[_+M]+a(0,d[b+M-y],0);for(;M<y;M++)d[b+M]=d[_+M]+a(d[b+M-p],d[b+M-y],d[b+M-p-y])}return d}function a(d,u,f){const g=d+u-f,m=g-d,p=g-u,y=g-f;return m*m<=p*p&&m*m<=y*y?d:p*p<=y*y?u:f}function l(d,u,f){f.width=e.readUint(d,u),u+=4,f.height=e.readUint(d,u),u+=4,f.depth=d[u],u++,f.ctype=d[u],u++,f.compress=d[u],u++,f.filter=d[u],u++,f.interlace=d[u],u++}function c(d,u,f,g,m,p,y,b,_){const x=Math.min(u,m),M=Math.min(f,p);let C=0,k=0;for(let A=0;A<M;A++)for(let O=0;O<x;O++)if(y>=0&&b>=0?(C=A*u+O<<2,k=(b+A)*m+y+O<<2):(C=(-b+A)*u-y+O<<2,k=A*m+O<<2),_==0)g[k]=d[C],g[k+1]=d[C+1],g[k+2]=d[C+2],g[k+3]=d[C+3];else if(_==1){var N=d[C+3]*.00392156862745098,S=d[C]*N,w=d[C+1]*N,E=d[C+2]*N,D=g[k+3]*(1/255),P=g[k]*D,L=g[k+1]*D,$=g[k+2]*D;const T=1-N,z=N+D*T,H=z==0?0:1/z;g[k+3]=255*z,g[k+0]=(S+P*T)*H,g[k+1]=(w+L*T)*H,g[k+2]=(E+$*T)*H}else if(_==2)N=d[C+3],S=d[C],w=d[C+1],E=d[C+2],D=g[k+3],P=g[k],L=g[k+1],$=g[k+2],N==D&&S==P&&w==L&&E==$?(g[k]=0,g[k+1]=0,g[k+2]=0,g[k+3]=0):(g[k]=S,g[k+1]=w,g[k+2]=E,g[k+3]=N);else if(_==3){if(N=d[C+3],S=d[C],w=d[C+1],E=d[C+2],D=g[k+3],P=g[k],L=g[k+1],$=g[k+2],N==D&&S==P&&w==L&&E==$)continue;if(N<220&&D>20)return!1}return!0}return{decode:function(u){const f=new Uint8Array(u);let g=8;const m=e,p=m.readUshort,y=m.readUint,b={tabs:{},frames:[]},_=new Uint8Array(f.length);let x,M=0,C=0;const k=[137,80,78,71,13,10,26,10];for(var N=0;N<8;N++)if(f[N]!=k[N])throw"The input is not a PNG file!";for(;g<f.length;){const A=m.readUint(f,g);g+=4;const O=m.readASCII(f,g,4);if(g+=4,O=="IHDR")l(f,g,b);else if(O=="iCCP"){for(var S=g;f[S]!=0;)S++;m.readASCII(f,g,S-g),f[S+1];const T=f.slice(S+2,g+A);let z=null;try{z=r(T)}catch{z=s(T)}b.tabs[O]=z}else if(O=="CgBI")b.tabs[O]=f.slice(g,g+4);else if(O=="IDAT"){for(N=0;N<A;N++)_[M+N]=f[g+N];M+=A}else if(O=="acTL")b.tabs[O]={num_frames:y(f,g),num_plays:y(f,g+4)},x=new Uint8Array(f.length);else if(O=="fcTL"){C!=0&&(($=b.frames[b.frames.length-1]).data=n(b,x.slice(0,C),$.rect.width,$.rect.height),C=0);const T={x:y(f,g+12),y:y(f,g+16),width:y(f,g+4),height:y(f,g+8)};let z=p(f,g+22);z=p(f,g+20)/(z==0?100:z);const H={rect:T,delay:Math.round(1e3*z),dispose:f[g+24],blend:f[g+25]};b.frames.push(H)}else if(O=="fdAT"){for(N=0;N<A-4;N++)x[C+N]=f[g+N+4];C+=A-4}else if(O=="pHYs")b.tabs[O]=[m.readUint(f,g),m.readUint(f,g+4),f[g+8]];else if(O=="cHRM")for(b.tabs[O]=[],N=0;N<8;N++)b.tabs[O].push(m.readUint(f,g+4*N));else if(O=="tEXt"||O=="zTXt"){b.tabs[O]==null&&(b.tabs[O]={});var w=m.nextZero(f,g),E=m.readASCII(f,g,w-g),D=g+A-w-1;if(O=="tEXt")L=m.readASCII(f,w+1,D);else{var P=r(f.slice(w+2,w+2+D));L=m.readUTF8(P,0,P.length)}b.tabs[O][E]=L}else if(O=="iTXt"){b.tabs[O]==null&&(b.tabs[O]={}),w=0,S=g,w=m.nextZero(f,S),E=m.readASCII(f,S,w-S);const T=f[S=w+1];var L;f[S+1],S+=2,w=m.nextZero(f,S),m.readASCII(f,S,w-S),S=w+1,w=m.nextZero(f,S),m.readUTF8(f,S,w-S),D=A-((S=w+1)-g),T==0?L=m.readUTF8(f,S,D):(P=r(f.slice(S,S+D)),L=m.readUTF8(P,0,P.length)),b.tabs[O][E]=L}else if(O=="PLTE")b.tabs[O]=m.readBytes(f,g,A);else if(O=="hIST"){const T=b.tabs.PLTE.length/3;for(b.tabs[O]=[],N=0;N<T;N++)b.tabs[O].push(p(f,g+2*N))}else if(O=="tRNS")b.ctype==3?b.tabs[O]=m.readBytes(f,g,A):b.ctype==0?b.tabs[O]=p(f,g):b.ctype==2&&(b.tabs[O]=[p(f,g),p(f,g+2),p(f,g+4)]);else if(O=="gAMA")b.tabs[O]=m.readUint(f,g)/1e5;else if(O=="sRGB")b.tabs[O]=f[g];else if(O=="bKGD")b.ctype==0||b.ctype==4?b.tabs[O]=[p(f,g)]:b.ctype==2||b.ctype==6?b.tabs[O]=[p(f,g),p(f,g+2),p(f,g+4)]:b.ctype==3&&(b.tabs[O]=f[g]);else if(O=="IEND")break;g+=A,m.readUint(f,g),g+=4}var $;return C!=0&&(($=b.frames[b.frames.length-1]).data=n(b,x.slice(0,C),$.rect.width,$.rect.height)),b.data=n(b,_,b.width,b.height),delete b.compress,delete b.interlace,delete b.filter,b},toRGBA8:function(u){const f=u.width,g=u.height;if(u.tabs.acTL==null)return[t(u.data,f,g,u).buffer];const m=[];u.frames[0].data==null&&(u.frames[0].data=u.data);const p=f*g*4,y=new Uint8Array(p),b=new Uint8Array(p),_=new Uint8Array(p);for(let M=0;M<u.frames.length;M++){const C=u.frames[M],k=C.rect.x,N=C.rect.y,S=C.rect.width,w=C.rect.height,E=t(C.data,S,w,u);if(M!=0)for(var x=0;x<p;x++)_[x]=y[x];if(C.blend==0?c(E,S,w,y,f,g,k,N,0):C.blend==1&&c(E,S,w,y,f,g,k,N,1),m.push(y.buffer.slice(0)),C.dispose!=0){if(C.dispose==1)c(b,S,w,y,f,g,k,N,0);else if(C.dispose==2)for(x=0;x<p;x++)y[x]=_[x]}}return m},_paeth:a,_copyTile:c,_bin:e}})();(function(){const{_copyTile:e}=Dn,{_bin:t}=Dn,n=Dn._paeth;var r={table:(function(){const S=new Uint32Array(256);for(let w=0;w<256;w++){let E=w;for(let D=0;D<8;D++)1&E?E=3988292384^E>>>1:E>>>=1;S[w]=E}return S})(),update(S,w,E,D){for(let P=0;P<D;P++)S=r.table[255&(S^w[E+P])]^S>>>8;return S},crc:(S,w,E)=>4294967295^r.update(4294967295,S,w,E)};function s(S,w,E,D){w[E]+=S[0]*D>>4,w[E+1]+=S[1]*D>>4,w[E+2]+=S[2]*D>>4,w[E+3]+=S[3]*D>>4}function o(S){return Math.max(0,Math.min(255,S))}function i(S,w){const E=S[0]-w[0],D=S[1]-w[1],P=S[2]-w[2],L=S[3]-w[3];return E*E+D*D+P*P+L*L}function a(S,w,E,D,P,L,$){$==null&&($=1);const A=D.length,O=[];for(var T=0;T<A;T++){const V=D[T];O.push([V>>>0&255,V>>>8&255,V>>>16&255,V>>>24&255])}for(T=0;T<A;T++){let V=4294967295;for(var z=0,H=0;H<A;H++){var j=i(O[T],O[H]);H!=T&&j<V&&(V=j,z=H)}}const G=new Uint32Array(P.buffer),U=new Int16Array(w*E*4),W=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5];for(T=0;T<W.length;T++)W[T]=255*((W[T]+.5)/16-.5);for(let V=0;V<E;V++)for(let X=0;X<w;X++){var Y;T=4*(V*w+X),$!=2?Y=[o(S[T]+U[T]),o(S[T+1]+U[T+1]),o(S[T+2]+U[T+2]),o(S[T+3]+U[T+3])]:(j=W[4*(3&V)+(3&X)],Y=[o(S[T]+j),o(S[T+1]+j),o(S[T+2]+j),o(S[T+3]+j)]),z=0;let K=16777215;for(H=0;H<A;H++){const ee=i(Y,O[H]);ee<K&&(K=ee,z=H)}const q=O[z],ne=[Y[0]-q[0],Y[1]-q[1],Y[2]-q[2],Y[3]-q[3]];$==1&&(X!=w-1&&s(ne,U,T+4,7),V!=E-1&&(X!=0&&s(ne,U,T+4*w-4,3),s(ne,U,T+4*w,5),X!=w-1&&s(ne,U,T+4*w+4,1))),L[T>>2]=z,G[T>>2]=D[z]}}function l(S,w,E,D,P){P==null&&(P={});const{crc:L}=r,$=t.writeUint,A=t.writeUshort,O=t.writeASCII;let T=8;const z=S.frames.length>1;let H,j=!1,G=33+(z?20:0);if(P.sRGB!=null&&(G+=13),P.pHYs!=null&&(G+=21),P.iCCP!=null&&(H=pako.deflate(P.iCCP),G+=21+H.length+4),S.ctype==3){for(var U=S.plte.length,W=0;W<U;W++)S.plte[W]>>>24!=255&&(j=!0);G+=8+3*U+4+(j?8+1*U+4:0)}for(var Y=0;Y<S.frames.length;Y++)z&&(G+=38),G+=(q=S.frames[Y]).cimg.length+12,Y!=0&&(G+=4);G+=12;const V=new Uint8Array(G),X=[137,80,78,71,13,10,26,10];for(W=0;W<8;W++)V[W]=X[W];if($(V,T,13),T+=4,O(V,T,"IHDR"),T+=4,$(V,T,w),T+=4,$(V,T,E),T+=4,V[T]=S.depth,T++,V[T]=S.ctype,T++,V[T]=0,T++,V[T]=0,T++,V[T]=0,T++,$(V,T,L(V,T-17,17)),T+=4,P.sRGB!=null&&($(V,T,1),T+=4,O(V,T,"sRGB"),T+=4,V[T]=P.sRGB,T++,$(V,T,L(V,T-5,5)),T+=4),P.iCCP!=null){const ne=13+H.length;$(V,T,ne),T+=4,O(V,T,"iCCP"),T+=4,O(V,T,"ICC profile"),T+=11,T+=2,V.set(H,T),T+=H.length,$(V,T,L(V,T-(ne+4),ne+4)),T+=4}if(P.pHYs!=null&&($(V,T,9),T+=4,O(V,T,"pHYs"),T+=4,$(V,T,P.pHYs[0]),T+=4,$(V,T,P.pHYs[1]),T+=4,V[T]=P.pHYs[2],T++,$(V,T,L(V,T-13,13)),T+=4),z&&($(V,T,8),T+=4,O(V,T,"acTL"),T+=4,$(V,T,S.frames.length),T+=4,$(V,T,P.loop!=null?P.loop:0),T+=4,$(V,T,L(V,T-12,12)),T+=4),S.ctype==3){for($(V,T,3*(U=S.plte.length)),T+=4,O(V,T,"PLTE"),T+=4,W=0;W<U;W++){const ne=3*W,ee=S.plte[W],ue=255&ee,J=ee>>>8&255,ce=ee>>>16&255;V[T+ne+0]=ue,V[T+ne+1]=J,V[T+ne+2]=ce}if(T+=3*U,$(V,T,L(V,T-3*U-4,3*U+4)),T+=4,j){for($(V,T,U),T+=4,O(V,T,"tRNS"),T+=4,W=0;W<U;W++)V[T+W]=S.plte[W]>>>24&255;T+=U,$(V,T,L(V,T-U-4,U+4)),T+=4}}let K=0;for(Y=0;Y<S.frames.length;Y++){var q=S.frames[Y];z&&($(V,T,26),T+=4,O(V,T,"fcTL"),T+=4,$(V,T,K++),T+=4,$(V,T,q.rect.width),T+=4,$(V,T,q.rect.height),T+=4,$(V,T,q.rect.x),T+=4,$(V,T,q.rect.y),T+=4,A(V,T,D[Y]),T+=2,A(V,T,1e3),T+=2,V[T]=q.dispose,T++,V[T]=q.blend,T++,$(V,T,L(V,T-30,30)),T+=4);const ne=q.cimg;$(V,T,(U=ne.length)+(Y==0?0:4)),T+=4;const ee=T;O(V,T,Y==0?"IDAT":"fdAT"),T+=4,Y!=0&&($(V,T,K++),T+=4),V.set(ne,T),T+=U,$(V,T,L(V,ee,T-ee)),T+=4}return $(V,T,0),T+=4,O(V,T,"IEND"),T+=4,$(V,T,L(V,T-4,4)),T+=4,V.buffer}function c(S,w,E){for(let D=0;D<S.frames.length;D++){const P=S.frames[D];P.rect.width;const L=P.rect.height,$=new Uint8Array(L*P.bpl+L);P.cimg=g(P.img,L,P.bpp,P.bpl,$,w,E)}}function d(S,w,E,D,P){const L=P[0],$=P[1],A=P[2],O=P[3],T=P[4],z=P[5];let H=6,j=8,G=255;for(var U=0;U<S.length;U++){const ve=new Uint8Array(S[U]);for(var W=ve.length,Y=0;Y<W;Y+=4)G&=ve[Y+3]}const V=G!=255,X=(function(ge,pe,ke,$e,ie,Me){const Ae=[];for(var Ie=0;Ie<ge.length;Ie++){const je=new Uint8Array(ge[Ie]),re=new Uint32Array(je.buffer);var Xe;let fe=0,be=0,we=pe,xe=ke,He=$e?1:0;if(Ie!=0){const _e=Me||$e||Ie==1||Ae[Ie-2].dispose!=0?1:2;let Le=0,Ce=1e9;for(let We=0;We<_e;We++){var gt=new Uint8Array(ge[Ie-1-We]);const Ze=new Uint32Array(ge[Ie-1-We]);let Fe=pe,Ne=ke,ze=-1,Ke=-1;for(let at=0;at<ke;at++)for(let ct=0;ct<pe;ct++)re[Be=at*pe+ct]!=Ze[Be]&&(ct<Fe&&(Fe=ct),ct>ze&&(ze=ct),at<Ne&&(Ne=at),at>Ke&&(Ke=at));ze==-1&&(Fe=Ne=ze=Ke=0),ie&&((1&Fe)==1&&Fe--,(1&Ne)==1&&Ne--);const tt=(ze-Fe+1)*(Ke-Ne+1);tt<Ce&&(Ce=tt,Le=We,fe=Fe,be=Ne,we=ze-Fe+1,xe=Ke-Ne+1)}gt=new Uint8Array(ge[Ie-1-Le]),Le==1&&(Ae[Ie-1].dispose=2),Xe=new Uint8Array(we*xe*4),e(gt,pe,ke,Xe,we,xe,-fe,-be,0),He=e(je,pe,ke,Xe,we,xe,-fe,-be,3)?1:0,He==1?f(je,pe,ke,Xe,{x:fe,y:be,width:we,height:xe}):e(je,pe,ke,Xe,we,xe,-fe,-be,0)}else Xe=je.slice(0);Ae.push({rect:{x:fe,y:be,width:we,height:xe},img:Xe,blend:He,dispose:0})}if($e)for(Ie=0;Ie<Ae.length;Ie++){if((et=Ae[Ie]).blend==1)continue;const je=et.rect,re=Ae[Ie-1].rect,fe=Math.min(je.x,re.x),be=Math.min(je.y,re.y),we={x:fe,y:be,width:Math.max(je.x+je.width,re.x+re.width)-fe,height:Math.max(je.y+je.height,re.y+re.height)-be};Ae[Ie-1].dispose=1,Ie-1!=0&&u(ge,pe,ke,Ae,Ie-1,we,ie),u(ge,pe,ke,Ae,Ie,we,ie)}let pt=0;if(ge.length!=1)for(var Be=0;Be<Ae.length;Be++){var et;pt+=(et=Ae[Be]).rect.width*et.rect.height}return Ae})(S,w,E,L,$,A),K={},q=[],ne=[];if(D!=0){const ve=[];for(Y=0;Y<X.length;Y++)ve.push(X[Y].img.buffer);const ge=(function(ie){let Me=0;for(var Ae=0;Ae<ie.length;Ae++)Me+=ie[Ae].byteLength;const Ie=new Uint8Array(Me);let Xe=0;for(Ae=0;Ae<ie.length;Ae++){const gt=new Uint8Array(ie[Ae]),pt=gt.length;for(let Be=0;Be<pt;Be+=4){let et=gt[Be],je=gt[Be+1],re=gt[Be+2];const fe=gt[Be+3];fe==0&&(et=je=re=0),Ie[Xe+Be]=et,Ie[Xe+Be+1]=je,Ie[Xe+Be+2]=re,Ie[Xe+Be+3]=fe}Xe+=pt}return Ie.buffer})(ve),pe=p(ge,D);for(Y=0;Y<pe.plte.length;Y++)q.push(pe.plte[Y].est.rgba);let ke=0;for(Y=0;Y<X.length;Y++){const $e=(ue=X[Y]).img.length;var ee=new Uint8Array(pe.inds.buffer,ke>>2,$e>>2);ne.push(ee);const ie=new Uint8Array(pe.abuf,ke,$e);z&&a(ue.img,ue.rect.width,ue.rect.height,q,ie,ee),ue.img.set(ie),ke+=$e}}else for(U=0;U<X.length;U++){var ue=X[U];const ve=new Uint32Array(ue.img.buffer);var J=ue.rect.width;for(W=ve.length,ee=new Uint8Array(W),ne.push(ee),Y=0;Y<W;Y++){const ge=ve[Y];if(Y!=0&&ge==ve[Y-1])ee[Y]=ee[Y-1];else if(Y>J&&ge==ve[Y-J])ee[Y]=ee[Y-J];else{let pe=K[ge];if(pe==null&&(K[ge]=pe=q.length,q.push(ge),q.length>=300))break;ee[Y]=pe}}}const ce=q.length;for(ce<=256&&T==0&&(j=ce<=2?1:ce<=4?2:ce<=16?4:8,j=Math.max(j,O)),U=0;U<X.length;U++){(ue=X[U]).rect.x,ue.rect.y,J=ue.rect.width;const ve=ue.rect.height;let ge=ue.img;new Uint32Array(ge.buffer);let pe=4*J,ke=4;if(ce<=256&&T==0){pe=Math.ceil(j*J/8);var me=new Uint8Array(pe*ve);const $e=ne[U];for(let ie=0;ie<ve;ie++){Y=ie*pe;const Me=ie*J;if(j==8)for(var le=0;le<J;le++)me[Y+le]=$e[Me+le];else if(j==4)for(le=0;le<J;le++)me[Y+(le>>1)]|=$e[Me+le]<<4-4*(1&le);else if(j==2)for(le=0;le<J;le++)me[Y+(le>>2)]|=$e[Me+le]<<6-2*(3&le);else if(j==1)for(le=0;le<J;le++)me[Y+(le>>3)]|=$e[Me+le]<<7-1*(7&le)}ge=me,H=3,ke=1}else if(V==0&&X.length==1){me=new Uint8Array(J*ve*3);const $e=J*ve;for(Y=0;Y<$e;Y++){const ie=3*Y,Me=4*Y;me[ie]=ge[Me],me[ie+1]=ge[Me+1],me[ie+2]=ge[Me+2]}ge=me,H=2,ke=3,pe=3*J}ue.img=ge,ue.bpl=pe,ue.bpp=ke}return{ctype:H,depth:j,plte:q,frames:X}}function u(S,w,E,D,P,L,$){const A=Uint8Array,O=Uint32Array,T=new A(S[P-1]),z=new O(S[P-1]),H=P+1<S.length?new A(S[P+1]):null,j=new A(S[P]),G=new O(j.buffer);let U=w,W=E,Y=-1,V=-1;for(let K=0;K<L.height;K++)for(let q=0;q<L.width;q++){const ne=L.x+q,ee=L.y+K,ue=ee*w+ne,J=G[ue];J==0||D[P-1].dispose==0&&z[ue]==J&&(H==null||H[4*ue+3]!=0)||(ne<U&&(U=ne),ne>Y&&(Y=ne),ee<W&&(W=ee),ee>V&&(V=ee))}Y==-1&&(U=W=Y=V=0),$&&((1&U)==1&&U--,(1&W)==1&&W--),L={x:U,y:W,width:Y-U+1,height:V-W+1};const X=D[P];X.rect=L,X.blend=1,X.img=new Uint8Array(L.width*L.height*4),D[P-1].dispose==0?(e(T,w,E,X.img,L.width,L.height,-L.x,-L.y,0),f(j,w,E,X.img,L)):e(j,w,E,X.img,L.width,L.height,-L.x,-L.y,0)}function f(S,w,E,D,P){e(S,w,E,D,P.width,P.height,-P.x,-P.y,2)}function g(S,w,E,D,P,L,$){const A=[];let O,T=[0,1,2,3,4];L!=-1?T=[L]:(w*D>5e5||E==1)&&(T=[0]),$&&(O={level:0});const z=wE;for(var H=0;H<T.length;H++){for(let U=0;U<w;U++)m(P,S,U,D,E,T[H]);A.push(z.deflate(P,O))}let j,G=1e9;for(H=0;H<A.length;H++)A[H].length<G&&(j=H,G=A[H].length);return A[j]}function m(S,w,E,D,P,L){const $=E*D;let A=$+E;if(S[A]=L,A++,L==0)if(D<500)for(var O=0;O<D;O++)S[A+O]=w[$+O];else S.set(new Uint8Array(w.buffer,$,D),A);else if(L==1){for(O=0;O<P;O++)S[A+O]=w[$+O];for(O=P;O<D;O++)S[A+O]=w[$+O]-w[$+O-P]+256&255}else if(E==0){for(O=0;O<P;O++)S[A+O]=w[$+O];if(L==2)for(O=P;O<D;O++)S[A+O]=w[$+O];if(L==3)for(O=P;O<D;O++)S[A+O]=w[$+O]-(w[$+O-P]>>1)+256&255;if(L==4)for(O=P;O<D;O++)S[A+O]=w[$+O]-n(w[$+O-P],0,0)+256&255}else{if(L==2)for(O=0;O<D;O++)S[A+O]=w[$+O]+256-w[$+O-D]&255;if(L==3){for(O=0;O<P;O++)S[A+O]=w[$+O]+256-(w[$+O-D]>>1)&255;for(O=P;O<D;O++)S[A+O]=w[$+O]+256-(w[$+O-D]+w[$+O-P]>>1)&255}if(L==4){for(O=0;O<P;O++)S[A+O]=w[$+O]+256-n(0,w[$+O-D],0)&255;for(O=P;O<D;O++)S[A+O]=w[$+O]+256-n(w[$+O-P],w[$+O-D],w[$+O-P-D])&255}}}function p(S,w){const E=new Uint8Array(S),D=E.slice(0),P=new Uint32Array(D.buffer),L=y(D,w),$=L[0],A=L[1],O=E.length,T=new Uint8Array(O>>2);let z;if(E.length<2e7)for(var H=0;H<O;H+=4)z=b($,j=E[H]*.00392156862745098,G=E[H+1]*.00392156862745098,U=E[H+2]*.00392156862745098,W=E[H+3]*.00392156862745098),T[H>>2]=z.ind,P[H>>2]=z.est.rgba;else for(H=0;H<O;H+=4){var j=E[H]*.00392156862745098,G=E[H+1]*(1/255),U=E[H+2]*(1/255),W=E[H+3]*(1/255);for(z=$;z.left;)z=_(z.est,j,G,U,W)<=0?z.left:z.right;T[H>>2]=z.ind,P[H>>2]=z.est.rgba}return{abuf:D.buffer,inds:T,plte:A}}function y(S,w,E){E==null&&(E=1e-4);const D=new Uint32Array(S.buffer),P={i0:0,i1:S.length,bst:null,est:null,tdst:0,left:null,right:null};P.bst=C(S,P.i0,P.i1),P.est=k(P.bst);const L=[P];for(;L.length<w;){let A=0,O=0;for(var $=0;$<L.length;$++)L[$].est.L>A&&(A=L[$].est.L,O=$);if(A<E)break;const T=L[O],z=x(S,D,T.i0,T.i1,T.est.e,T.est.eMq255);if(T.i0>=z||T.i1<=z){T.est.L=0;continue}const H={i0:T.i0,i1:z,bst:null,est:null,tdst:0,left:null,right:null};H.bst=C(S,H.i0,H.i1),H.est=k(H.bst);const j={i0:z,i1:T.i1,bst:null,est:null,tdst:0,left:null,right:null};for(j.bst={R:[],m:[],N:T.bst.N-H.bst.N},$=0;$<16;$++)j.bst.R[$]=T.bst.R[$]-H.bst.R[$];for($=0;$<4;$++)j.bst.m[$]=T.bst.m[$]-H.bst.m[$];j.est=k(j.bst),T.left=H,T.right=j,L[O]=H,L.push(j)}for(L.sort(((A,O)=>O.bst.N-A.bst.N)),$=0;$<L.length;$++)L[$].ind=$;return[P,L]}function b(S,w,E,D,P){if(S.left==null)return S.tdst=(function(H,j,G,U,W){const Y=j-H[0],V=G-H[1],X=U-H[2],K=W-H[3];return Y*Y+V*V+X*X+K*K})(S.est.q,w,E,D,P),S;const L=_(S.est,w,E,D,P);let $=S.left,A=S.right;L>0&&($=S.right,A=S.left);const O=b($,w,E,D,P);if(O.tdst<=L*L)return O;const T=b(A,w,E,D,P);return T.tdst<O.tdst?T:O}function _(S,w,E,D,P){const{e:L}=S;return L[0]*w+L[1]*E+L[2]*D+L[3]*P-S.eMq}function x(S,w,E,D,P,L){for(D-=4;E<D;){for(;M(S,E,P)<=L;)E+=4;for(;M(S,D,P)>L;)D-=4;if(E>=D)break;const $=w[E>>2];w[E>>2]=w[D>>2],w[D>>2]=$,E+=4,D-=4}for(;M(S,E,P)>L;)E-=4;return E+4}function M(S,w,E){return S[w]*E[0]+S[w+1]*E[1]+S[w+2]*E[2]+S[w+3]*E[3]}function C(S,w,E){const D=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],P=[0,0,0,0],L=E-w>>2;for(let $=w;$<E;$+=4){const A=S[$]*.00392156862745098,O=S[$+1]*(1/255),T=S[$+2]*(1/255),z=S[$+3]*(1/255);P[0]+=A,P[1]+=O,P[2]+=T,P[3]+=z,D[0]+=A*A,D[1]+=A*O,D[2]+=A*T,D[3]+=A*z,D[5]+=O*O,D[6]+=O*T,D[7]+=O*z,D[10]+=T*T,D[11]+=T*z,D[15]+=z*z}return D[4]=D[1],D[8]=D[2],D[9]=D[6],D[12]=D[3],D[13]=D[7],D[14]=D[11],{R:D,m:P,N:L}}function k(S){const{R:w}=S,{m:E}=S,{N:D}=S,P=E[0],L=E[1],$=E[2],A=E[3],O=D==0?0:1/D,T=[w[0]-P*P*O,w[1]-P*L*O,w[2]-P*$*O,w[3]-P*A*O,w[4]-L*P*O,w[5]-L*L*O,w[6]-L*$*O,w[7]-L*A*O,w[8]-$*P*O,w[9]-$*L*O,w[10]-$*$*O,w[11]-$*A*O,w[12]-A*P*O,w[13]-A*L*O,w[14]-A*$*O,w[15]-A*A*O],z=T,H=N;let j=[Math.random(),Math.random(),Math.random(),Math.random()],G=0,U=0;if(D!=0)for(let Y=0;Y<16&&(j=H.multVec(z,j),U=Math.sqrt(H.dot(j,j)),j=H.sml(1/U,j),!(Y!=0&&Math.abs(U-G)<1e-9));Y++)G=U;const W=[P*O,L*O,$*O,A*O];return{Cov:T,q:W,e:j,L:G,eMq255:H.dot(H.sml(255,W),j),eMq:H.dot(j,W),rgba:(Math.round(255*W[3])<<24|Math.round(255*W[2])<<16|Math.round(255*W[1])<<8|Math.round(255*W[0])<<0)>>>0}}var N={multVec:(S,w)=>[S[0]*w[0]+S[1]*w[1]+S[2]*w[2]+S[3]*w[3],S[4]*w[0]+S[5]*w[1]+S[6]*w[2]+S[7]*w[3],S[8]*w[0]+S[9]*w[1]+S[10]*w[2]+S[11]*w[3],S[12]*w[0]+S[13]*w[1]+S[14]*w[2]+S[15]*w[3]],dot:(S,w)=>S[0]*w[0]+S[1]*w[1]+S[2]*w[2]+S[3]*w[3],sml:(S,w)=>[S*w[0],S*w[1],S*w[2],S*w[3]]};Dn.encode=function(w,E,D,P,L,$,A){P==null&&(P=0),A==null&&(A=!1);const O=d(w,E,D,P,[!1,!1,!1,0,A,!1]);return c(O,-1),l(O,E,D,L,$)},Dn.encodeLL=function(w,E,D,P,L,$,A,O){const T={ctype:0+(P==1?0:2)+(L==0?0:4),depth:$,frames:[]},z=(P+L)*$,H=z*E;for(let j=0;j<w.length;j++)T.frames.push({rect:{x:0,y:0,width:E,height:D},img:new Uint8Array(w[j]),blend:0,dispose:1,bpp:Math.ceil(z/8),bpl:Math.ceil(H/8)});return c(T,0,!0),l(T,E,D,A,O)},Dn.encode.compress=d,Dn.encode.dither=a,Dn.quantize=p,Dn.quantize.getKDtree=y,Dn.quantize.getNearest=b})();const Ep={toArrayBuffer(e,t){const n=e.width,r=e.height,s=n<<2,o=e.getContext("2d").getImageData(0,0,n,r),i=new Uint32Array(o.data.buffer),a=(32*n+31)/32<<2,l=a*r,c=122+l,d=new ArrayBuffer(c),u=new DataView(d),f=1<<20;let g,m,p,y,b=f,_=0,x=0,M=0;function C(S){u.setUint16(x,S,!0),x+=2}function k(S){u.setUint32(x,S,!0),x+=4}function N(S){x+=S}C(19778),k(c),N(4),k(122),k(108),k(n),k(-r>>>0),C(1),C(32),k(3),k(l),k(2835),k(2835),N(8),k(16711680),k(65280),k(255),k(4278190080),k(1466527264),(function S(){for(;_<r&&b>0;){for(y=122+_*a,g=0;g<s;)b--,m=i[M++],p=m>>>24,u.setUint32(y+g,m<<8|p),g+=4;_++}M<i.length?(b=f,setTimeout(S,Ep._dly)):t(d)})()},toBlob(e,t){this.toArrayBuffer(e,(n=>{t(new Blob([n],{type:"image/bmp"}))}))},_dly:9};var rn={CHROME:"CHROME",FIREFOX:"FIREFOX",DESKTOP_SAFARI:"DESKTOP_SAFARI",IE:"IE",IOS:"IOS",ETC:"ETC"},kE={[rn.CHROME]:16384,[rn.FIREFOX]:11180,[rn.DESKTOP_SAFARI]:16384,[rn.IE]:8192,[rn.IOS]:4096,[rn.ETC]:8192};const Vc=typeof window<"u",Mp=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,Ri=Vc&&window.cordova&&window.cordova.require&&window.cordova.require("cordova/modulemapper"),CE=(Vc||Mp)&&(Ri&&Ri.getOriginalSymbol(window,"File")||typeof File<"u"&&File),Np=(Vc||Mp)&&(Ri&&Ri.getOriginalSymbol(window,"FileReader")||typeof FileReader<"u"&&FileReader);function Hc(e,t,n=Date.now()){return new Promise((r=>{const s=e.split(","),o=s[0].match(/:(.*?);/)[1],i=globalThis.atob(s[1]);let a=i.length;const l=new Uint8Array(a);for(;a--;)l[a]=i.charCodeAt(a);const c=new Blob([l],{type:o});c.name=t,c.lastModified=n,r(c)}))}function Tp(e){return new Promise(((t,n)=>{const r=new Np;r.onload=()=>t(r.result),r.onerror=s=>n(s),r.readAsDataURL(e)}))}function Op(e){return new Promise(((t,n)=>{const r=new Image;r.onload=()=>t(r),r.onerror=s=>n(s),r.src=e}))}function Dr(){if(Dr.cachedResult!==void 0)return Dr.cachedResult;let e=rn.ETC;const{userAgent:t}=navigator;return/Chrom(e|ium)/i.test(t)?e=rn.CHROME:/iP(ad|od|hone)/i.test(t)&&/WebKit/i.test(t)?e=rn.IOS:/Safari/i.test(t)?e=rn.DESKTOP_SAFARI:/Firefox/i.test(t)?e=rn.FIREFOX:(/MSIE/i.test(t)||document.documentMode)&&(e=rn.IE),Dr.cachedResult=e,Dr.cachedResult}function Ap(e,t){const n=Dr(),r=kE[n];let s=e,o=t,i=s*o;const a=s>o?o/s:s/o;for(;i>r*r;){const l=(r+s)/2,c=(r+o)/2;l<c?(o=c,s=c*a):(o=l*a,s=l),i=s*o}return{width:s,height:o}}function ea(e,t){let n,r;try{if(n=new OffscreenCanvas(e,t),r=n.getContext("2d"),r===null)throw new Error("getContext of OffscreenCanvas returns null")}catch{n=document.createElement("canvas"),r=n.getContext("2d")}return n.width=e,n.height=t,[n,r]}function Dp(e,t){const{width:n,height:r}=Ap(e.width,e.height),[s,o]=ea(n,r);return t&&/jpe?g/.test(t)&&(o.fillStyle="white",o.fillRect(0,0,s.width,s.height)),o.drawImage(e,0,0,s.width,s.height),s}function ta(){return ta.cachedResult!==void 0||(ta.cachedResult=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&typeof document<"u"&&"ontouchend"in document),ta.cachedResult}function na(e,t={}){return new Promise((function(n,r){let s,o;var i=function(){try{return o=Dp(s,t.fileType||e.type),n([s,o])}catch(l){return r(l)}},a=function(l){try{var c=function(d){try{throw d}catch(u){return r(u)}};try{let d;return Tp(e).then((function(u){try{return d=u,Op(d).then((function(f){try{return s=f,(function(){try{return i()}catch(g){return r(g)}})()}catch(g){return c(g)}}),c)}catch(f){return c(f)}}),c)}catch(d){c(d)}}catch(d){return r(d)}};try{if(ta()||[rn.DESKTOP_SAFARI,rn.MOBILE_SAFARI].includes(Dr()))throw new Error("Skip createImageBitmap on IOS and Safari");return createImageBitmap(e).then((function(l){try{return s=l,i()}catch{return a()}}),a)}catch{a()}}))}function ra(e,t,n,r,s=1){return new Promise((function(o,i){let a;if(t==="image/png"){let c,d,u;return c=e.getContext("2d"),{data:d}=c.getImageData(0,0,e.width,e.height),u=Dn.encode([d.buffer],e.width,e.height,4096*s),a=new Blob([u],{type:t}),a.name=n,a.lastModified=r,l.call(this)}{let c=function(){return l.call(this)};if(t==="image/bmp")return new Promise((d=>Ep.toBlob(e,d))).then(function(d){try{return a=d,a.name=n,a.lastModified=r,c.call(this)}catch(u){return i(u)}}.bind(this),i);{let d=function(){return c.call(this)};if(typeof OffscreenCanvas=="function"&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:s}).then(function(u){try{return a=u,a.name=n,a.lastModified=r,d.call(this)}catch(f){return i(f)}}.bind(this),i);{let u;return u=e.toDataURL(t,s),Hc(u,n,r).then(function(f){try{return a=f,d.call(this)}catch(g){return i(g)}}.bind(this),i)}}}function l(){return o(a)}}))}function Wn(e){e.width=0,e.height=0}function ks(){return new Promise((function(e,t){let n,r,s,o;return ks.cachedResult!==void 0?e(ks.cachedResult):Hc("data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/xABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==","test.jpg",Date.now()).then((function(i){try{return n=i,na(n).then((function(a){try{return r=a[1],ra(r,n.type,n.name,n.lastModified).then((function(l){try{return s=l,Wn(r),na(s).then((function(c){try{return o=c[0],ks.cachedResult=o.width===1&&o.height===2,e(ks.cachedResult)}catch(d){return t(d)}}),t)}catch(c){return t(c)}}),t)}catch(l){return t(l)}}),t)}catch(a){return t(a)}}),t)}))}function Ip(e){return new Promise(((t,n)=>{const r=new Np;r.onload=s=>{const o=new DataView(s.target.result);if(o.getUint16(0,!1)!=65496)return t(-2);const i=o.byteLength;let a=2;for(;a<i;){if(o.getUint16(a+2,!1)<=8)return t(-1);const l=o.getUint16(a,!1);if(a+=2,l==65505){if(o.getUint32(a+=2,!1)!=1165519206)return t(-1);const c=o.getUint16(a+=6,!1)==18761;a+=o.getUint32(a+4,c);const d=o.getUint16(a,c);a+=2;for(let u=0;u<d;u++)if(o.getUint16(a+12*u,c)==274)return t(o.getUint16(a+12*u+8,c))}else{if((65280&l)!=65280)break;a+=o.getUint16(a,!1)}}return t(-1)},r.onerror=s=>n(s),r.readAsArrayBuffer(e)}))}function Lp(e,t){const{width:n}=e,{height:r}=e,{maxWidthOrHeight:s}=t;let o,i=e;return isFinite(s)&&(n>s||r>s)&&([i,o]=ea(n,r),n>r?(i.width=s,i.height=r/n*s):(i.width=n/r*s,i.height=s),o.drawImage(e,0,0,i.width,i.height),Wn(e)),i}function Pp(e,t){const{width:n}=e,{height:r}=e,[s,o]=ea(n,r);switch(t>4&&t<9?(s.width=r,s.height=n):(s.width=n,s.height=r),t){case 2:o.transform(-1,0,0,1,n,0);break;case 3:o.transform(-1,0,0,-1,n,r);break;case 4:o.transform(1,0,0,-1,0,r);break;case 5:o.transform(0,1,1,0,0,0);break;case 6:o.transform(0,1,-1,0,r,0);break;case 7:o.transform(0,-1,-1,0,r,n);break;case 8:o.transform(0,-1,1,0,0,n)}return o.drawImage(e,0,0,n,r),Wn(e),s}function $p(e,t,n=0){return new Promise((function(r,s){let o,i,a,l,c,d,u,f,g,m,p,y,b,_,x,M,C,k,N,S;function w(D=5){if(t.signal&&t.signal.aborted)throw t.signal.reason;o+=D,t.onProgress(Math.min(o,100))}function E(D){if(t.signal&&t.signal.aborted)throw t.signal.reason;o=Math.min(Math.max(D,o),100),t.onProgress(o)}return o=n,i=t.maxIteration||10,a=1024*t.maxSizeMB*1024,w(),na(e,t).then(function(D){try{return[,l]=D,w(),c=Lp(l,t),w(),new Promise((function(P,L){var $;if(!($=t.exifOrientation))return Ip(e).then(function(O){try{return $=O,A.call(this)}catch(T){return L(T)}}.bind(this),L);function A(){return P($)}return A.call(this)})).then(function(P){try{return d=P,w(),ks().then(function(L){try{return u=L?c:Pp(c,d),w(),f=t.initialQuality||1,g=t.fileType||e.type,ra(u,g,e.name,e.lastModified,f).then(function($){try{{let O=function(){if(i--&&(x>a||x>b)){let z,H;return z=S?.95*N.width:N.width,H=S?.95*N.height:N.height,[C,k]=ea(z,H),k.drawImage(N,0,0,z,H),f*=g==="image/png"?.85:.95,ra(C,g,e.name,e.lastModified,f).then((function(j){try{return M=j,Wn(N),N=C,x=M.size,E(Math.min(99,Math.floor((_-x)/(_-a)*100))),O}catch(G){return s(G)}}),s)}return[1]},T=function(){return Wn(N),Wn(C),Wn(c),Wn(u),Wn(l),E(100),r(M)};if(m=$,w(),p=m.size>a,y=m.size>e.size,!p&&!y)return E(100),r(m);var A;return b=e.size,_=m.size,x=_,N=u,S=!t.alwaysKeepResolution&&p,(A=function(z){for(;z;){if(z.then)return void z.then(A,s);try{if(z.pop){if(z.length)return z.pop()?T.call(this):z;z=O}else z=z.call(this)}catch(H){return s(H)}}}.bind(this))(O)}}catch(O){return s(O)}}.bind(this),s)}catch($){return s($)}}.bind(this),s)}catch(L){return s(L)}}.bind(this),s)}catch(P){return s(P)}}.bind(this),s)}))}const SE=` + `,children:[h.jsx(re,{name:x.faIcon,className:"w-4 h-4"}),h.jsx(Q,{children:x.label}),y?.faIcon===x.faIcon&&h.jsx(re,{name:"check",className:"w-4 h-4 ml-auto text-blue-600"})]},x.value)):h.jsx(I,{className:"px-3 py-4 text-sm text-gray-500 dark:text-gray-400 text-center",children:o})})]})]})},sc=({defaultOpen:t=!1,isOpen:e,onToggle:n,className:r="",style:s,children:o,disabled:i=!1,id:a,editorAttrs:l})=>{const[c,d]=v.useState(t),u=e!==void 0,f=u?e:c,g=v.useRef(null),[m,p]=v.useState(t?void 0:0),y=()=>{if(i)return;const _=!f;u||d(_),n?.(_)};v.useEffect(()=>{if(g.current)if(f){p(g.current.scrollHeight);const _=setTimeout(()=>{p(void 0)},300);return()=>clearTimeout(_)}else p(g.current.scrollHeight),requestAnimationFrame(()=>{p(0)})},[f]);const b={};if(v.Children.forEach(o,_=>{if(v.isValidElement(_)){const x=_.props?.slot;x==="trigger"?b.trigger=_:x==="content"&&(b.content=_)}}),!b.trigger&&!b.content){const _=v.Children.toArray(o);_.length>=2?(b.trigger=_[0],b.content=_.slice(1)):_.length===1&&(b.content=_[0])}return h.jsxs(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden ${r}`,style:s,id:a,...l,children:[h.jsxs(oe,{type:"button",onClick:y,disabled:i,className:`w-full flex items-center justify-between p-3 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${i?"opacity-50 cursor-not-allowed":""} ${f?"border-b border-gray-200 dark:border-gray-700":""}`,"aria-expanded":f,children:[h.jsx(I,{className:"flex-1",children:b.trigger}),!b.trigger&&h.jsx("svg",{className:`w-5 h-5 text-gray-500 dark:text-gray-400 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),h.jsx(I,{ref:g,className:"overflow-hidden transition-all duration-300 ease-in-out",style:{height:m!==void 0?`${m}px`:"auto"},children:h.jsx(I,{className:"bg-white dark:bg-gray-800",children:b.content})})]})},no=window.G7Core,eg=no?.useControllableState,tg=(t,e)=>window.G7Core?.t?.(t,e)??t,VS={"":"common.scope_type_all_desc",self:"common.scope_type_self_desc",role:"common.scope_type_role_desc"},zS=({options:t,value:e,onChange:n,disabled:r=!1})=>{const[s,o]=v.useState(!1),[i,a]=v.useState({}),l=v.useRef(null),c=t.find(u=>u.value===e)??t[0];v.useEffect(()=>{if(!s)return;const u=g=>{l.current&&!l.current.contains(g.target)&&o(!1)},f=()=>{if(!l.current)return;const g=l.current.getBoundingClientRect();a({position:"fixed",top:g.bottom+4,right:window.innerWidth-g.right,zIndex:9999})};return document.addEventListener("mousedown",u),window.addEventListener("scroll",f,!0),window.addEventListener("resize",f),()=>{document.removeEventListener("mousedown",u),window.removeEventListener("scroll",f,!0),window.removeEventListener("resize",f)}},[s]);const d=v.useCallback(()=>{if(r||!l.current)return;const u=l.current.getBoundingClientRect();a({position:"fixed",top:u.bottom+4,right:window.innerWidth-u.right,zIndex:9999}),o(f=>!f)},[r]);return h.jsxs(I,{ref:l,className:"relative flex-shrink-0",children:[h.jsxs(I,{role:"button",tabIndex:0,onClick:u=>{u.stopPropagation(),u.preventDefault(),d()},onMouseDown:u=>u.stopPropagation(),onKeyDown:u=>{(u.key==="Enter"||u.key===" ")&&(u.stopPropagation(),u.preventDefault(),d()),u.key==="Escape"&&o(!1)},className:`flex items-center gap-1 text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer select-none ${r?"opacity-50 cursor-not-allowed":"hover:border-blue-400 dark:hover:border-blue-500"}`,children:[h.jsx(Q,{children:c?.label??""}),h.jsx(re,{name:de.ChevronDown,className:"w-3 h-3 text-gray-400 dark:text-gray-500"})]}),s&&h.jsx(I,{style:i,className:"w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1",children:t.map(u=>{const f=u.value??"",g=VS[f],m=g?tg(g):"",p=u.value===e;return h.jsxs(I,{role:"option","aria-selected":p,onClick:y=>{y.stopPropagation(),y.preventDefault(),n(u.value),o(!1)},onMouseDown:y=>y.stopPropagation(),className:`px-3 py-2 cursor-pointer transition-colors ${p?"bg-blue-50 dark:bg-blue-900/30":"hover:bg-gray-50 dark:hover:bg-gray-700/50"}`,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[h.jsx(Q,{className:`text-xs font-medium ${p?"text-blue-700 dark:text-blue-300":"text-gray-900 dark:text-white"}`,children:u.label}),p&&h.jsx(re,{name:de.Check,className:"w-3.5 h-3.5 text-blue-600 dark:text-blue-400"})]}),m&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:m})]},f||"__null__")})})]})},HS=(t,e)=>{if(t.length!==e.length)return!1;const n=[...t].sort((s,o)=>s.id-o.id),r=[...e].sort((s,o)=>s.id-o.id);return n.every((s,o)=>s.id===r[o].id&&s.scope_type===r[o].scope_type)},US=({data:t=[],value:e=[],onChange:n,scopeOptions:r=[],className:s="",disabled:o=!1,desktopColumns:i=2,installedModules:a=[],installedPlugins:l=[],id:c,editorAttrs:d})=>{const u=v.useCallback(M=>{if(n){const T=no?.createChangeEvent?no.createChangeEvent({value:M}):{target:{value:M}};n(T)}},[n]),[f,g]=eg?eg(e,[],u,{isEqual:HS}):(()=>{const[M,T]=v.useState(e),L=v.useRef(n);L.current=n;const P=v.useRef(!1);v.useEffect(()=>{if(P.current){P.current=!1;return}T(e)},[e]);const F=v.useCallback(D=>{P.current=!0,T(A=>{const O=typeof D=="function"?D(A):D;if(L.current){const V=no?.createChangeEvent?no.createChangeEvent({value:O}):{target:{value:O}};L.current(V)}return O})},[]);return[M,F]})(),m=v.useMemo(()=>new Set(f.map(M=>M.id)),[f]),p=v.useCallback(M=>M.is_assignable?[M.id]:M.children.flatMap(T=>p(T)),[]),y=v.useCallback(M=>{const T=p(M);return T.length>0&&T.every(L=>m.has(L))},[m,p]),b=v.useCallback(M=>{const T=p(M),L=T.filter(P=>m.has(P)).length;return L>0&&L<T.length},[m,p]),_=v.useCallback(M=>p(M).filter(L=>m.has(L)).length,[m,p]),x=v.useCallback((M,T)=>{o||g(L=>T?[...L,{id:M,scope_type:null}]:L.filter(P=>P.id!==M))},[o,g]),E=v.useCallback((M,T)=>{if(o)return;const L=p(M),P=new Set(L);g(F=>{if(T){const D=new Set(F.map(O=>O.id)),A=L.filter(O=>!D.has(O)).map(O=>({id:O,scope_type:null}));return[...F,...A]}else return F.filter(D=>!P.has(D.id))})},[o,p,g]),S=v.useCallback((M,T)=>{o||g(L=>L.map(P=>P.id===M?{...P,scope_type:T}:P))},[o,g]),w=M=>{const T=m.has(M.id),L=!!M.resource_route_key,P=T&&L&&r.length>0,F=f.find(D=>D.id===M.id)?.scope_type??null;return h.jsxs(on,{className:`flex items-start gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${T?"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20":"border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50"} ${o?"opacity-50 cursor-not-allowed":""}`,children:[h.jsx(wt,{type:"checkbox",checked:T,onChange:D=>{D.stopPropagation(),x(M.id,D.target.checked)},disabled:o,className:"w-4 h-4 mt-0.5 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsxs(I,{className:"flex-1 min-w-0",children:[h.jsxs(I,{className:"flex items-center justify-between gap-2",children:[h.jsx(Q,{className:"text-sm font-medium text-gray-900 dark:text-white",children:M.name}),P&&h.jsx(zS,{options:r,value:F,onChange:D=>S(M.id,D),disabled:o})]}),M.description&&h.jsx(Q,{className:"block text-xs text-gray-500 dark:text-gray-400 mt-0.5",children:M.description})]})]},M.id)},N=(M,T)=>{const L=y(M),P=b(M),F=_(M),D=M.children.filter(V=>V.is_assignable);if(D.length===0&&M.children.length===0)return null;const A=M.children.some(V=>!V.is_assignable),O=i===1?"grid-cols-1":i===3?"grid-cols-1 md:grid-cols-3":"grid-cols-1 md:grid-cols-2";return h.jsxs(sc,{defaultOpen:!1,className:"border-0 rounded-none",children:[h.jsxs(I,{slot:"trigger",className:"flex items-center gap-3 w-full py-2",children:[h.jsx("input",{type:"checkbox",checked:L,ref:V=>{V&&(V.indeterminate=P)},onChange:V=>{V.stopPropagation(),E(M,V.target.checked)},onClick:V=>V.stopPropagation(),onMouseDown:V=>V.stopPropagation(),disabled:o,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(re,{name:de.Folder,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:M.name}),h.jsxs(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 ml-1",children:[F,"/",M.leaf_count]}),h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-400 dark:text-gray-500 ml-auto"})]}),h.jsx(I,{slot:"content",className:`pl-8 grid ${O} gap-2 py-2`,children:A?M.children.map(V=>V.is_assignable?w(V):N(V)):D.map(V=>w(V))})]},M.id)},C=M=>{if(M.extension_type==="core")return"core";if(M.extension_type==="module")return"module";if(M.extension_type==="plugin")return"plugin";const T=M.extension_identifier??M.module??"";return T==="core"||!T?"core":l?.some(P=>P.identifier===T)?"plugin":"module"},k=M=>{const T=y(M),L=b(M),P=_(M),F=C(M);return h.jsxs(sc,{defaultOpen:!1,className:"mb-2",children:[h.jsxs(I,{slot:"trigger",className:"flex items-center gap-3 w-full py-2",children:[h.jsx("input",{type:"checkbox",checked:T,ref:D=>{D&&(D.indeterminate=L)},onChange:D=>{D.stopPropagation(),E(M,D.target.checked)},onClick:D=>D.stopPropagation(),onMouseDown:D=>D.stopPropagation(),disabled:o,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(re,{name:F==="core"?de.Cog:de.Cube,className:"w-5 h-5 text-gray-400 dark:text-gray-500"}),h.jsx(Q,{className:"font-medium text-gray-900 dark:text-white",children:M.name}),F!=="core"&&h.jsx(sh,{type:F,identifier:M.extension_identifier??M.module,installedModules:a,installedPlugins:l}),h.jsxs(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 ml-2",children:[P,"/",M.leaf_count]}),h.jsx(re,{name:de.ChevronDown,className:"w-4 h-4 text-gray-400 dark:text-gray-500 ml-auto"})]}),h.jsx(I,{slot:"content",className:"pl-8 space-y-2 pb-2",children:M.children.map(D=>D.is_assignable?w(D):N(D))})]},M.id)};return t.length===0?h.jsx(I,{className:`text-sm text-gray-400 dark:text-gray-500 py-4 text-center ${s}`,id:c,...d,children:tg("common.no_data")??"데이터가 없습니다."}):h.jsx(I,{className:`space-y-2 ${s}`,id:c,...d,children:t.map(M=>k(M))})};function BS({text:t,keyword:e}){if(!e||!t)return h.jsx(h.Fragment,{children:t});const n=e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),r=t.split(new RegExp(`(${n})`,"gi"));return h.jsx(h.Fragment,{children:r.map((s,o)=>s.toLowerCase()===e.toLowerCase()?h.jsx(Q,{className:"bg-yellow-200 dark:bg-yellow-700 rounded px-0.5",children:s},o):s)})}const jS=({data:t=[],expandedIds:e=[],selectedIds:n=[],searchKeyword:r="",showProductCount:s=!1,selectable:o=!1,className:i="",onToggle:a,onSelectionChange:l,id:c,editorAttrs:d})=>{const u=v.useCallback(y=>{if(!a)return;const b=e.includes(y)?e.filter(_=>_!==y):[...e,y];a(b)},[e,a]),f=v.useCallback((y,b)=>{if(!l)return;const _=b?[...n,y]:n.filter(x=>x!==y);l(_)},[n,l]),g=v.useMemo(()=>{if(!r)return null;const y=new Set,b=_=>{for(const x of _)(x.localized_name||x.name).toLowerCase().includes(r.toLowerCase())&&y.add(x.id),x.children&&b(x.children)};return b(t),y},[t,r]),m=v.useCallback(y=>!g||g.has(y.id)?!0:(y.children??[]).some(b=>m(b)),[g]),p=(y,b=0)=>{const _=(y.children??[]).length>0,x=e.includes(y.id),E=n.includes(y.id),S=y.localized_name||y.name;return r&&!m(y)?null:h.jsxs(I,{"data-testid":`category-node-${y.id}`,children:[h.jsxs(I,{className:`flex items-center gap-2 py-1.5 px-2 rounded cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors ${b>0?"ml-"+Math.min(b*5,20):""}`,style:b>0?{marginLeft:`${b*20}px`}:void 0,children:[_?h.jsx(I,{className:"flex-shrink-0 w-5 h-5 flex items-center justify-center text-gray-400 dark:text-gray-500",onClick:()=>u(y.id),"data-testid":`toggle-${y.id}`,children:h.jsx(re,{name:x?de.CaretDown:de.CaretRight,className:"text-xs"})}):h.jsx(I,{className:"flex-shrink-0 w-5 h-5"}),o&&h.jsx(Cn,{checked:E,onChange:w=>{w.stopPropagation(),f(y.id,w.target.checked)},className:"flex-shrink-0","data-testid":`checkbox-${y.id}`}),h.jsx(re,{name:de.Folder,className:`flex-shrink-0 text-sm ${_?"text-yellow-500 dark:text-yellow-400":"text-gray-400 dark:text-gray-500"}`}),h.jsx(Q,{className:"text-sm text-gray-700 dark:text-gray-300 select-none",onClick:()=>_&&u(y.id),children:h.jsx(BS,{text:S,keyword:r})}),s&&y.products_count!==void 0&&h.jsx(Q,{className:"inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400 ml-1",children:y.products_count})]}),_&&x&&h.jsx(I,{"data-testid":`children-${y.id}`,children:(y.children??[]).map(w=>p(w,b+1))})]},y.id)};return t.length===0?h.jsx(I,{className:`text-sm text-gray-400 dark:text-gray-500 py-4 text-center ${i}`,id:c,...d,children:"데이터가 없습니다."}):h.jsx(I,{className:`border border-gray-200 dark:border-gray-700 rounded-lg p-2 max-h-80 overflow-y-auto ${i}`,id:c,...d,children:t.map(y=>p(y,0))})},ng="carousel",ro="controller",WS="navigation",GS="no-scroll",oc="portal",YS="root",rg="toolbar",XS="counter",so="fullscreen",sg="slideshow",oo="thumbnails",og="zoom",rs="loading",ic="playing",io="error",ao="complete",KS="placeholder",lo=t=>`active-slide-${t}`,qS=lo(rs),ZS=lo(ic),QS=lo(io),JS=lo(ao),ig="fullsize",ss="flex_center",RS="no_scroll",ag="no_scroll_padding",ac="slide",lg="slide_wrapper",e_="slide_wrapper_interactive",or="prev",ir="next",lc="swipe",os="close",cg="onPointerDown",dg="onPointerMove",ug="onPointerUp",fg="onPointerLeave",hg="onPointerCancel",cc="onKeyDown",t_="onKeyUp",dc="onWheel",n_="Escape",r_="ArrowLeft",s_="ArrowRight",o_="button",Pi="icon",gg="contain",pg="cover",mg="Unknown action type",bg="yarl__";function Ht(...t){return[...t].filter(Boolean).join(" ")}function Ne(t){return`${bg}${t}`}function dt(t){return`--${bg}${t}`}function mr(t,e){return`${t}${e?`_${e}`:""}`}function br(t){return e=>mr(t,e)}function yr(t,e){var n;return(n=t?.[e])!==null&&n!==void 0?n:e}function yg(t,e,n){return yr(t,"{index} of {total}").replace(/\{index}/g,`${gc(n,e.length)+1}`).replace(/\{total}/g,`${e.length}`)}function is(...t){return()=>{t.forEach(e=>{e()})}}function hn(t,e,n){return()=>{const r=$.useContext(n);if(!r)throw new Error(`${t} must be used within a ${e}.Provider`);return r}}function uc(){return typeof window<"u"}function fc(t,e=0){const n=10**e;return Math.round((t+Number.EPSILON)*n)/n}function vr(t){return t.type===void 0||t.type==="image"}function hc(t,e){return t.imageFit===pg||t.imageFit!==gg&&e===pg}function $i(t){return typeof t=="string"?Number.parseInt(t,10):t}function Fi(t){if(typeof t=="number")return{pixel:t};if(typeof t=="string"){const e=$i(t);return t.endsWith("%")?{percent:e}:{pixel:e}}return{pixel:0}}function i_(t,e){const n=Fi(e),r=n.percent!==void 0?t.width/100*n.percent:n.pixel;return{width:Math.max(t.width-2*r,0),height:Math.max(t.height-2*r,0)}}function a_(){return(uc()?window?.devicePixelRatio:void 0)||1}function gc(t,e){return e>0?(t%e+e)%e:0}function pc(t){return t.length>0}function mc(t,e){return t[gc(e,t.length)]}function bc(t,e){return pc(t)?mc(t,e):void 0}function vg(t){return vr(t)?t.src:void 0}function Vi(t,e,n){if(!n)return t;const{buttons:r,...s}=t,o=r.findIndex(a=>a===e),i=$.isValidElement(n)?$.cloneElement(n,{key:e},null):n;if(o>=0){const a=[...r];return a.splice(o,1,i),{buttons:a,...s}}return{buttons:[i,...r],...s}}function xg(t,e,n=0){return Math.min(t.preload,Math.max(t.finite?e.length-1:Math.floor(e.length/2),n))}const l_=Number($.version.split(".")[0])>=19;function c_(t){return{inert:l_?t:t?"":void 0}}function d_(t){t.scrollTop}const yc={open:!1,close:()=>{},index:0,slides:[],render:{},plugins:[],toolbar:{buttons:[os]},labels:{},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},carousel:{finite:!1,preload:2,padding:"16px",spacing:"30%",imageFit:gg,imageProps:{}},controller:{ref:null,focus:!0,aria:!1,touchAction:"none",closeOnPullUp:!1,closeOnPullDown:!1,closeOnBackdropClick:!1,preventDefaultWheelX:!0,preventDefaultWheelY:!1,disableSwipeNavigation:!1},portal:{},noScroll:{disabled:!1},on:{},styles:{},className:""};function gn(t,e){return{name:t,component:e}}function Dt(t,e){return{module:t,children:e}}function wg(t,e,n){return t.module.name===e?n(t):t.children?[Dt(t.module,t.children.flatMap(r=>{var s;return(s=wg(r,e,n))!==null&&s!==void 0?s:[]}))]:[t]}function as(t,e,n){return t.flatMap(r=>{var s;return(s=wg(r,e,n))!==null&&s!==void 0?s:[]})}function u_(t,e=[],n=[]){let r=t;const s=g=>{const m=[...r];for(;m.length>0;){const p=m.pop();if(p?.module.name===g)return!0;p?.children&&m.push(...p.children)}return!1},o=(g,m)=>{if(g===""){r=[Dt(m,r)];return}r=as(r,g,p=>[Dt(m,[p])])},i=(g,m)=>{r=as(r,g,p=>[Dt(p.module,[Dt(m,p.children)])])},a=(g,m,p)=>{r=as(r,g,y=>{var b;return[Dt(y.module,[...p?[Dt(m)]:[],...(b=y.children)!==null&&b!==void 0?b:[],...p?[]:[Dt(m)]])]})},l=(g,m,p)=>{r=as(r,g,y=>[...p?[Dt(m)]:[],y,...p?[]:[Dt(m)]])},c=g=>{i(ro,g)},d=(g,m)=>{r=as(r,g,p=>[Dt(m,p.children)])},u=g=>{r=as(r,g,m=>m.children)},f=g=>{n.push(g)};return e.forEach(g=>{g({contains:s,addParent:o,append:i,addChild:a,addSibling:l,addModule:c,replace:d,remove:u,augment:f})}),{config:r,augmentation:g=>n.reduce((m,p)=>p(m),g)}}const kg=$.createContext(null),vc=hn("useA11yContext","A11yContext",kg);function f_({children:t}){const[e,n]=$.useState(!1),[r,s]=$.useState(!1),o=$.useMemo(()=>({focusWithin:e,trackFocusWithin:(a,l)=>{const c=d=>u=>{var f;u.currentTarget.contains(u.relatedTarget)||n(d),(f=d?a:l)===null||f===void 0||f(u)};return{onFocus:c(!0),onBlur:c(!1)}},autoPlaying:r,setAutoPlaying:s}),[e,r]);return $.createElement(kg.Provider,{value:o},t)}const Cg=$.createContext(null),ls=hn("useDocument","DocumentContext",Cg);function h_({nodeRef:t,children:e}){const n=$.useMemo(()=>{const r=o=>{var i;return((i=o||t.current)===null||i===void 0?void 0:i.ownerDocument)||document};return{getOwnerDocument:r,getOwnerWindow:o=>{var i;return((i=r(o))===null||i===void 0?void 0:i.defaultView)||window}}},[t]);return $.createElement(Cg.Provider,{value:n},e)}const Sg=$.createContext(null),cs=hn("useEvents","EventsContext",Sg);function g_({children:t}){const[e]=$.useState({});$.useEffect(()=>()=>{Object.keys(e).forEach(r=>delete e[r])},[e]);const n=$.useMemo(()=>{const r=(i,a)=>{var l;(l=e[i])===null||l===void 0||l.splice(0,e[i].length,...e[i].filter(c=>c!==a))};return{publish:(...[i,a])=>{var l;(l=e[i])===null||l===void 0||l.forEach(c=>c(a))},subscribe:(i,a)=>(e[i]||(e[i]=[]),e[i].push(a),()=>r(i,a)),unsubscribe:r}},[e]);return $.createElement(Sg.Provider,{value:n},t)}const _g=$.createContext(null),It=hn("useLightboxProps","LightboxPropsContext",_g);function Eg({children:t,...e}){return $.createElement(_g.Provider,{value:e},t)}const Mg=$.createContext(null),pn=hn("useLightboxState","LightboxStateContext",Mg),Ng=$.createContext(null),p_=hn("useLightboxDispatch","LightboxDispatchContext",Ng);function m_(t,e){switch(e.type){case"swipe":{const{slides:n}=t,r=e?.increment||0,s=t.globalIndex+r,o=gc(s,n.length),i=bc(n,o),a=r||e.duration!==void 0?{increment:r,duration:e.duration,easing:e.easing}:void 0;return{slides:n,currentIndex:o,globalIndex:s,currentSlide:i,animation:a}}case"update":return e.slides!==t.slides||e.index!==t.currentIndex?{slides:e.slides,currentIndex:e.index,globalIndex:e.index,currentSlide:bc(e.slides,e.index)}:t;default:throw new Error(mg)}}function b_({slides:t,index:e,children:n}){const[r,s]=$.useReducer(m_,{slides:t,currentIndex:e,globalIndex:e,currentSlide:bc(t,e)});$.useEffect(()=>{s({type:"update",slides:t,index:e})},[t,e]);const o=$.useMemo(()=>({...r,state:r,dispatch:s}),[r,s]);return $.createElement(Ng.Provider,{value:s},$.createElement(Mg.Provider,{value:o},n))}const Og=$.createContext(null),co=hn("useTimeouts","TimeoutsContext",Og);function y_({children:t}){const[e]=$.useState([]);$.useEffect(()=>()=>{e.forEach(r=>window.clearTimeout(r)),e.splice(0,e.length)},[e]);const n=$.useMemo(()=>{const r=i=>{e.splice(0,e.length,...e.filter(a=>a!==i))};return{setTimeout:(i,a)=>{const l=window.setTimeout(()=>{r(l),i()},a);return e.push(l),l},clearTimeout:i=>{i!==void 0&&(r(i),window.clearTimeout(i))}}},[e]);return $.createElement(Og.Provider,{value:n},t)}const ds=$.forwardRef(function({label:e,className:n,icon:r,renderIcon:s,onClick:o,style:i,...a},l){const{styles:c,labels:d}=It(),u=yr(d,e);return $.createElement("button",{ref:l,type:"button",title:u,"aria-label":u,className:Ht(Ne(o_),n),onClick:o,style:{...i,...c.button},...a},s?s():$.createElement(r,{className:Ne(Pi),style:c.icon}))});function Tg(t,e){const n=r=>$.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24","aria-hidden":"true",focusable:"false",...r},e);return n.displayName=t,n}function en(t,e){return Tg(t,$.createElement("g",{fill:"currentColor"},$.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),e))}function v_(t,e){return Tg(t,$.createElement($.Fragment,null,$.createElement("defs",null,$.createElement("mask",{id:"strike"},$.createElement("path",{d:"M0 0h24v24H0z",fill:"white"}),$.createElement("path",{d:"M0 0L24 24",stroke:"black",strokeWidth:4}))),$.createElement("path",{d:"M0.70707 2.121320L21.878680 23.292883",stroke:"currentColor",strokeWidth:2}),$.createElement("g",{fill:"currentColor",mask:"url(#strike)"},$.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),e)))}const x_=en("Close",$.createElement("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),w_=en("Previous",$.createElement("path",{d:"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"})),k_=en("Next",$.createElement("path",{d:"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"})),C_=en("Loading",$.createElement($.Fragment,null,Array.from({length:8}).map((t,e,n)=>$.createElement("line",{key:e,x1:"12",y1:"6.5",x2:"12",y2:"1.8",strokeLinecap:"round",strokeWidth:"2.6",stroke:"currentColor",strokeOpacity:1/n.length*(e+1),transform:`rotate(${360/n.length*e}, 12, 12)`})))),S_=en("Error",$.createElement("path",{d:"M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z"})),Mn=uc()?$.useLayoutEffect:$.useEffect;function xc(){const[t,e]=$.useState(!1);return $.useEffect(()=>{var n,r;const s=(n=window.matchMedia)===null||n===void 0?void 0:n.call(window,"(prefers-reduced-motion: reduce)");e(s?.matches);const o=i=>e(i.matches);return(r=s?.addEventListener)===null||r===void 0||r.call(s,"change",o),()=>{var i;return(i=s?.removeEventListener)===null||i===void 0?void 0:i.call(s,"change",o)}},[]),t}function __(t){let e=0,n=0,r=0;const o=window.getComputedStyle(t).transform.match(/matrix.*\((.+)\)/);if(o){const i=o[1].split(",").map($i);i.length===6?(e=i[4],n=i[5]):i.length===16&&(e=i[12],n=i[13],r=i[14])}return{x:e,y:n,z:r}}function wc(t,e){const n=$.useRef(void 0),r=$.useRef(void 0),s=xc();return Mn(()=>{var o,i,a;if(t.current&&n.current!==void 0&&!s){const{keyframes:l,duration:c,easing:d,onfinish:u}=e(n.current,t.current.getBoundingClientRect(),__(t.current))||{};if(l&&c){(o=r.current)===null||o===void 0||o.cancel(),r.current=void 0;try{r.current=(a=(i=t.current).animate)===null||a===void 0?void 0:a.call(i,l,{duration:c,easing:d})}catch(f){console.error(f)}r.current&&(r.current.onfinish=()=>{r.current=void 0,u?.()})}}n.current=void 0}),{prepareAnimation:o=>{n.current=o},isAnimationPlaying:()=>{var o;return((o=r.current)===null||o===void 0?void 0:o.playState)==="running"}}}function Ag(){const t=$.useRef(null),e=$.useRef(void 0),[n,r]=$.useState();return{setContainerRef:$.useCallback(o=>{t.current=o,e.current&&(e.current.disconnect(),e.current=void 0);const i=()=>{if(o){const a=window.getComputedStyle(o),l=c=>parseFloat(c)||0;r({width:Math.round(o.clientWidth-l(a.paddingLeft)-l(a.paddingRight)),height:Math.round(o.clientHeight-l(a.paddingTop)-l(a.paddingBottom))})}else r(void 0)};i(),o&&typeof ResizeObserver<"u"&&(e.current=new ResizeObserver(i),e.current.observe(o))},[]),containerRef:t,containerRect:n}}function zi(){const t=$.useRef(void 0),{setTimeout:e,clearTimeout:n}=co();return $.useCallback((r,s)=>{n(t.current),t.current=e(r,s>0?s:0)},[e,n])}function We(t){const e=$.useRef(t);return Mn(()=>{e.current=t}),$.useCallback((...n)=>{var r;return(r=e.current)===null||r===void 0?void 0:r.call(e,...n)},[])}function Dg(t,e){typeof t=="function"?t(e):t&&(t.current=e)}function kc(t,e){return $.useMemo(()=>t==null&&e==null?null:n=>{Dg(t,n),Dg(e,n)},[t,e])}function Ig(t,e=!1){const n=$.useRef(!1);Mn(()=>{e&&n.current&&(n.current=!1,t())},[e,t]);const r=$.useCallback(()=>{n.current=!0},[]),s=$.useCallback(()=>{n.current=!1},[]);return{onFocus:r,onBlur:s}}function Hi(){const[t,e]=$.useState(!1);return Mn(()=>{e(window.getComputedStyle(window.document.documentElement).direction==="rtl")},[]),t}function Lg(){const[t]=$.useState({}),e=$.useCallback((s,o)=>{var i;(i=t[s])===null||i===void 0||i.forEach(a=>{o.isPropagationStopped()||a(o)})},[t]),n=$.useMemo(()=>({onPointerDown:s=>e(cg,s),onPointerMove:s=>e(dg,s),onPointerUp:s=>e(ug,s),onPointerLeave:s=>e(fg,s),onPointerCancel:s=>e(hg,s),onKeyDown:s=>e(cc,s),onKeyUp:s=>e(t_,s),onWheel:s=>e(dc,s)}),[e]),r=$.useCallback((s,o)=>(t[s]||(t[s]=[]),t[s].unshift(o),()=>{const i=t[s];i&&i.splice(0,i.length,...i.filter(a=>a!==o))}),[t]);return{registerSensors:n,subscribeSensors:r}}function Pg(t,e){const n=$.useRef(0),r=zi(),s=We((...o)=>{n.current=Date.now(),t(o)});return $.useCallback((...o)=>{r(()=>{s(o)},e-(Date.now()-n.current))},[e,s,r])}const Cc=br("slide"),Sc=br("slide_image");function xr({slide:t,offset:e,render:n,rect:r,imageFit:s,imageProps:o,onClick:i,onLoad:a,onError:l,style:c}){var d,u,f,g,m,p,y,b;const[_,x]=$.useState(rs),{publish:E}=cs(),{setTimeout:S}=co(),w=$.useRef(null);$.useEffect(()=>{e===0&&E(lo(_))},[e,_,E]);const N=We(U=>{("decode"in U?U.decode():Promise.resolve()).catch(()=>{}).then(()=>{U.parentNode&&(x(ao),S(()=>{a?.(U)},0))})}),C=$.useCallback(U=>{w.current=U,U?.complete&&N(U)},[N]),k=$.useCallback(U=>{N(U.currentTarget)},[N]),M=We(()=>{x(io),l?.()}),T=hc(t,s),L=(U,B)=>Number.isFinite(U)?U:B,P=L(Math.max(...((u=(d=t.srcSet)===null||d===void 0?void 0:d.map(U=>U.width))!==null&&u!==void 0?u:[]).concat(t.width?[t.width]:[]).filter(Boolean)),((f=w.current)===null||f===void 0?void 0:f.naturalWidth)||0),F=L(Math.max(...((m=(g=t.srcSet)===null||g===void 0?void 0:g.map(U=>U.height))!==null&&m!==void 0?m:[]).concat(t.height?[t.height]:[]).filter(Boolean)),((p=w.current)===null||p===void 0?void 0:p.naturalHeight)||0),D=P&&F?{maxWidth:`min(${P}px, 100%)`,maxHeight:`min(${F}px, 100%)`}:{maxWidth:"100%",maxHeight:"100%"},A=(y=t.srcSet)===null||y===void 0?void 0:y.sort((U,B)=>U.width-B.width).map(U=>`${U.src} ${U.width}w`).join(", "),O=()=>r&&!T&&t.width&&t.height?r.height/t.height*t.width:Number.MAX_VALUE,V=A&&r&&uc()?`${Math.round(Math.min(O(),r.width))}px`:void 0,{style:H,className:W,...j}=o||{};return $.createElement($.Fragment,null,$.createElement("img",{ref:C,onLoad:k,onError:M,onClick:i,draggable:!1,className:Ht(Ne(Sc()),T&&Ne(Sc("cover")),_!==ao&&Ne(Sc("loading")),W),style:{...D,...c,...H},...j,alt:(b=t.alt)!==null&&b!==void 0?b:"",sizes:V,srcSet:A,src:t.src}),_!==ao&&$.createElement("div",{className:Ne(Cc(KS))},_===rs&&(n?.iconLoading?n.iconLoading():$.createElement(C_,{className:Ht(Ne(Pi),Ne(Cc(rs)))})),_===io&&(n?.iconError?n.iconError():$.createElement(S_,{className:Ht(Ne(Pi),Ne(Cc(io)))}))))}const E_=$.forwardRef(function({className:e,children:n,onFocus:r,onBlur:s,...o},i){const a=$.useRef(null),{trackFocusWithin:l}=vc();return $.createElement(h_,{nodeRef:a},$.createElement("div",{ref:kc(i,a),className:Ht(Ne("root"),e),...l(r,s),...o},n))});var Ut;(function(t){t[t.NONE=0]="NONE",t[t.SWIPE=1]="SWIPE",t[t.PULL=2]="PULL",t[t.ANIMATION=3]="ANIMATION"})(Ut||(Ut={}));function $g(t,e,n,r,s){$.useEffect(()=>s?()=>{}:is(t(cg,e),t(dg,n),t(ug,r),t(fg,r),t(hg,r)),[t,e,n,r,s])}var Nn;(function(t){t[t.NONE=0]="NONE",t[t.SWIPE=1]="SWIPE",t[t.PULL=2]="PULL"})(Nn||(Nn={}));const _c=30;function M_({disableSwipeNavigation:t,closeOnBackdropClick:e},n,r,s,o,i,a,l,c,d,u,f,g,m,p,y){const b=$.useRef(0),_=$.useRef([]),x=$.useRef(void 0),E=$.useRef(0),S=$.useRef(Nn.NONE),w=$.useCallback(P=>{x.current===P.pointerId&&(x.current=void 0,S.current=Nn.NONE);const F=_.current;F.splice(0,F.length,...F.filter(D=>D.pointerId!==P.pointerId))},[]),N=$.useCallback(P=>{w(P),P.persist(),_.current.push(P)},[w]),C=$.useCallback(P=>_.current.find(({pointerId:F})=>P.pointerId===F),[]),k=We(P=>{N(P)}),M=(P,F)=>u&&P>F||d&&P<-F,T=We(P=>{const F=C(P);if(F)if(x.current===P.pointerId){const D=Date.now()-E.current,A=b.current;S.current===Nn.SWIPE?Math.abs(A)>.3*s||Math.abs(A)>5&&D<o?l(A,D):c(A):S.current===Nn.PULL&&(M(A,2*_c)?m(A,D):p(A)),b.current=0,S.current=Nn.NONE}else{const{target:D}=P;e&&D instanceof HTMLElement&&D===F.target&&(D.classList.contains(Ne(ac))||D.classList.contains(Ne(lg)))&&y()}w(P)}),L=We(P=>{const F=C(P);if(F){const D=x.current===P.pointerId;if(P.buttons===0){D&&b.current!==0?T(P):w(F);return}const A=P.clientX-F.clientX,O=P.clientY-F.clientY;if(x.current===void 0){const V=H=>{N(P),x.current=P.pointerId,E.current=Date.now(),S.current=H};Math.abs(A)>Math.abs(O)&&Math.abs(A)>_c&&r(A)?t||(V(Nn.SWIPE),i()):Math.abs(O)>Math.abs(A)&&M(O,_c)&&(V(Nn.PULL),f())}else D&&(S.current===Nn.SWIPE?(b.current=A,a(A)):S.current===Nn.PULL&&(b.current=O,g(O)))}});$g(n,k,L,T)}function N_({preventDefaultWheelX:t,preventDefaultWheelY:e}){const n=$.useRef(null),r=We(s=>{const o=Math.abs(s.deltaX)>Math.abs(s.deltaY);(o&&t||!o&&e||s.ctrlKey)&&s.preventDefault()});return $.useCallback(s=>{var o;s?s.addEventListener("wheel",r,{passive:!1}):(o=n.current)===null||o===void 0||o.removeEventListener("wheel",r),n.current=s},[r])}function O_(t,e,n,r,s,o,i,a,l){const c=$.useRef(0),d=$.useRef(0),u=$.useRef(void 0),f=$.useRef(void 0),g=$.useRef(0),m=$.useRef(void 0),p=$.useRef(0),{setTimeout:y,clearTimeout:b}=co(),_=$.useCallback(()=>{u.current&&(b(u.current),u.current=void 0)},[b]),x=$.useCallback(()=>{f.current&&(b(f.current),f.current=void 0)},[b]),E=We(()=>{t!==Ut.SWIPE&&(c.current=0,p.current=0,_(),x())});$.useEffect(E,[t,E]);const S=We(N=>{f.current=void 0,c.current===N&&l(c.current)}),w=We(N=>{if(N.ctrlKey||Math.abs(N.deltaY)>Math.abs(N.deltaX))return;const C=k=>{g.current=k,b(m.current),m.current=k>0?y(()=>{g.current=0,m.current=void 0},300):void 0};if(t===Ut.NONE){if(Math.abs(N.deltaX)<=1.2*Math.abs(g.current)){C(N.deltaX);return}if(!n(-N.deltaX))return;if(d.current+=N.deltaX,_(),Math.abs(d.current)>30)d.current=0,C(0),p.current=Date.now(),o();else{const k=d.current;u.current=y(()=>{u.current=void 0,k===d.current&&(d.current=0)},s)}}else if(t===Ut.SWIPE){let k=c.current-N.deltaX;if(k=Math.min(Math.abs(k),r)*Math.sign(k),c.current=k,i(k),x(),Math.abs(k)>.2*r){C(N.deltaX),a(k,Date.now()-p.current);return}f.current=y(()=>S(k),2*s)}else C(N.deltaX)});$.useEffect(()=>e(dc,w),[e,w])}const Fg=br("container"),Vg=$.createContext(null),On=hn("useController","ControllerContext",Vg);function T_({children:t,...e}){var n;const{carousel:r,animation:s,controller:o,on:i,styles:a,render:l}=e,{closeOnPullUp:c,closeOnPullDown:d,preventDefaultWheelX:u,preventDefaultWheelY:f}=o,[g,m]=$.useState(),p=pn(),y=p_(),[b,_]=$.useState(Ut.NONE),x=$.useRef(0),E=$.useRef(0),S=$.useRef(1),{registerSensors:w,subscribeSensors:N}=Lg(),{subscribe:C,publish:k}=cs(),M=zi(),T=zi(),L=zi(),{containerRef:P,setContainerRef:F,containerRect:D}=Ag(),A=kc(N_({preventDefaultWheelX:u,preventDefaultWheelY:f}),F),O=$.useRef(null),V=kc(O,void 0),{getOwnerDocument:H}=ls(),W=Hi(),j=se=>(W?-1:1)*(typeof se=="number"?se:1),U=We(()=>{var se;return(se=P.current)===null||se===void 0?void 0:se.focus()}),B=We(()=>e),G=We(()=>p),z=$.useCallback(se=>k(or,se),[k]),X=$.useCallback(se=>k(ir,se),[k]),Y=$.useCallback(()=>k(os),[k]),Z=se=>!(r.finite&&(j(se)>0&&p.currentIndex===0||j(se)<0&&p.currentIndex===p.slides.length-1)),ee=se=>{var Oe;x.current=se,(Oe=P.current)===null||Oe===void 0||Oe.style.setProperty(dt("swipe_offset"),`${Math.round(se)}px`)},te=se=>{var Oe,Le;E.current=se,S.current=(()=>{const et=d&&se>0?se:c&&se<0?-se:0;return Math.min(Math.max(fc(1-et/60*(1-.5),2),.5),1)})(),(Oe=P.current)===null||Oe===void 0||Oe.style.setProperty(dt("pull_offset"),`${Math.round(se)}px`),(Le=P.current)===null||Le===void 0||Le.style.setProperty(dt("pull_opacity"),`${S.current}`)},{prepareAnimation:ce}=wc(O,(se,Oe,Le)=>{if(O.current&&D)return{keyframes:[{transform:`translate(0, ${se.rect.y-Oe.y+Le.y}px)`,opacity:se.opacity},{transform:"translate(0, 0)",opacity:1}],duration:se.duration,easing:s.easing.fade}}),J=(se,Oe)=>{if(c||d){te(se);let Le=0;O.current&&(Le=s.fade*(Oe?2:1),ce({rect:O.current.getBoundingClientRect(),opacity:S.current,duration:Le})),L(()=>{te(0),_(Ut.NONE)},Le),_(Ut.ANIMATION),Oe||Y()}},{prepareAnimation:he,isAnimationPlaying:le}=wc(O,(se,Oe,Le)=>{var Ie;if(O.current&&D&&(!((Ie=p.animation)===null||Ie===void 0)&&Ie.duration)){const Ue=Fi(r.spacing),et=(Ue.percent?Ue.percent*D.width/100:Ue.pixel)||0;return{keyframes:[{transform:`translate(${j(p.globalIndex-se.index)*(D.width+et)+se.rect.x-Oe.x+Le.x}px, 0)`},{transform:"translate(0, 0)"}],duration:p.animation.duration,easing:p.animation.easing}}}),ae=We(se=>{var Oe,Le;const Ie=se.offset||0,Ue=Ie?s.swipe:(Oe=s.navigation)!==null&&Oe!==void 0?Oe:s.swipe,et=!Ie&&!le()?s.easing.navigation:s.easing.swipe;let{direction:bt}=se;const Ge=(Le=se.count)!==null&&Le!==void 0?Le:1;let ft=Ut.ANIMATION,Be=Ue*Ge;if(!bt){const fe=D?.width,xe=se.duration||0,ge=fe?Ue/fe*Math.abs(Ie):Ue;Ge!==0?(xe<ge?Be=Be/ge*Math.max(xe,ge/5):fe&&(Be=Ue/fe*(fe-Math.abs(Ie))),bt=j(Ie)>0?or:ir):Be=Ue/2}let ne=0;bt===or?Z(j(1))?ne=-Ge:(ft=Ut.NONE,Be=Ue):bt===ir&&(Z(j(-1))?ne=Ge:(ft=Ut.NONE,Be=Ue)),Be=Math.round(Be),T(()=>{ee(0),_(Ut.NONE)},Be),O.current&&he({rect:O.current.getBoundingClientRect(),index:p.globalIndex}),_(ft),k(lc,{type:"swipe",increment:ne,duration:Be,easing:et})});$.useEffect(()=>{var se,Oe;!((se=p.animation)===null||se===void 0)&&se.increment&&(!((Oe=p.animation)===null||Oe===void 0)&&Oe.duration)&&M(()=>y({type:"swipe",increment:0}),p.animation.duration)},[p.animation,y,M]);const Se=[N,Z,D?.width||0,s.swipe,()=>_(Ut.SWIPE),se=>ee(se),(se,Oe)=>ae({offset:se,duration:Oe,count:1}),se=>ae({offset:se,count:0})],ue=[()=>{d&&_(Ut.PULL)},se=>te(se),se=>J(se),se=>J(se,!0)];M_(o,...Se,c,d,...ue,Y),O_(b,...Se);const pe=We(()=>{o.focus&&H().querySelector(`.${Ne(oc)} .${Ne(Fg())}`)&&U()});$.useEffect(pe,[pe]);const we=We(()=>{var se;(se=i.view)===null||se===void 0||se.call(i,{index:p.currentIndex})});$.useEffect(we,[p.globalIndex,we]),$.useEffect(()=>is(C(or,se=>ae({direction:or,...se})),C(ir,se=>ae({direction:ir,...se})),C(lc,se=>y(se))),[C,ae,y]);const He=$.useMemo(()=>({prev:z,next:X,close:Y,focus:U,slideRect:D?i_(D,r.padding):{width:0,height:0},containerRect:D||{width:0,height:0},subscribeSensors:N,containerRef:P,setCarouselRef:V,toolbarWidth:g,setToolbarWidth:m}),[z,X,Y,U,N,D,P,V,g,m,r.padding]);return $.useImperativeHandle(o.ref,()=>({prev:z,next:X,close:Y,focus:U,getLightboxProps:B,getLightboxState:G}),[z,X,Y,U,B,G]),$.createElement("div",{ref:A,className:Ht(Ne(Fg()),Ne(ss)),style:{...b===Ut.SWIPE?{[dt("swipe_offset")]:`${Math.round(x.current)}px`}:null,...b===Ut.PULL?{[dt("pull_offset")]:`${Math.round(E.current)}px`,[dt("pull_opacity")]:`${S.current}`}:null,...o.touchAction!=="none"?{[dt("controller_touch_action")]:o.touchAction}:null,...a.container},tabIndex:-1,...w},D&&$.createElement(Vg.Provider,{value:He},t,(n=l.controls)===null||n===void 0?void 0:n.call(l)))}const A_=gn(ro,T_);function wr(t){return mr(ng,t)}function zg(t){return mr(ac,t)}function D_({slide:t,offset:e}){const n=$.useRef(null),{currentIndex:r,slides:s}=pn(),{slideRect:o,focus:i}=On(),{render:a,carousel:{imageFit:l,imageProps:c},on:{click:d},styles:{slide:u},labels:f}=It(),{getOwnerDocument:g}=ls(),m=e!==0;$.useEffect(()=>{var y;m&&(!((y=n.current)===null||y===void 0)&&y.contains(g().activeElement))&&i()},[m,i,g]);const p=()=>{var y,b,_,x;let E=(y=a.slide)===null||y===void 0?void 0:y.call(a,{slide:t,offset:e,rect:o});return!E&&vr(t)&&(E=$.createElement(xr,{slide:t,offset:e,render:a,rect:o,imageFit:l,imageProps:c,onClick:m?void 0:()=>d?.({index:r})})),E?$.createElement($.Fragment,null,(b=a.slideHeader)===null||b===void 0?void 0:b.call(a,{slide:t}),((_=a.slideContainer)!==null&&_!==void 0?_:(({children:S})=>S))({slide:t,children:E}),(x=a.slideFooter)===null||x===void 0?void 0:x.call(a,{slide:t})):null};return $.createElement("div",{ref:n,className:Ht(Ne(zg()),!m&&Ne(zg("current")),Ne(ss)),...c_(m),style:u,role:"group","aria-roledescription":yr(f,"Slide"),"aria-label":yg(f,s,r+e)},p())}function I_(){const t=It().styles.slide;return $.createElement("div",{className:Ne(ac),style:t})}function L_({carousel:t,labels:e}){const{slides:n,currentIndex:r,globalIndex:s}=pn(),{setCarouselRef:o}=On(),{autoPlaying:i,focusWithin:a}=vc(),l=Fi(t.spacing),c=Fi(t.padding),d=xg(t,n,1),u=[];if(pc(n))for(let f=r-d;f<=r+d;f+=1){const g=mc(n,f),m=s-r+f,p=t.finite&&(f<0||f>n.length-1);u.push(p?{key:m}:{key:[`${m}`,vg(g)].filter(Boolean).join("|"),offset:f-r,slide:g})}return $.createElement("div",{ref:o,className:Ht(Ne(wr()),u.length>0&&Ne(wr("with_slides"))),style:{[`${dt(wr("slides_count"))}`]:u.length,[`${dt(wr("spacing_px"))}`]:l.pixel||0,[`${dt(wr("spacing_percent"))}`]:l.percent||0,[`${dt(wr("padding_px"))}`]:c.pixel||0,[`${dt(wr("padding_percent"))}`]:c.percent||0},role:"region","aria-live":i&&!a?"off":"polite","aria-roledescription":yr(e,"Carousel"),"aria-label":yr(e,"Photo gallery")},u.map(({key:f,slide:g,offset:m})=>g?$.createElement(D_,{key:f,slide:g,offset:m}):$.createElement(I_,{key:f})))}const P_=gn(ng,L_);function Hg(){const{carousel:t}=It(),{slides:e,currentIndex:n}=pn(),r=e.length===0||t.finite&&n===0,s=e.length===0||t.finite&&n===e.length-1;return{prevDisabled:r,nextDisabled:s}}function Ug(t){var e;const n=Hi(),{publish:r}=cs(),{animation:s}=It(),{prevDisabled:o,nextDisabled:i}=Hg(),a=((e=s.navigation)!==null&&e!==void 0?e:s.swipe)/2,l=Pg(()=>r(or),a),c=Pg(()=>r(ir),a),d=We(u=>{switch(u.key){case n_:r(os);break;case r_:(n?i:o)||(n?c:l)();break;case s_:(n?o:i)||(n?l:c)();break}});$.useEffect(()=>t(cc,d),[t,d])}function Bg({label:t,icon:e,renderIcon:n,action:r,onClick:s,disabled:o,style:i}){return $.createElement(ds,{label:t,icon:e,renderIcon:n,className:Ne(`navigation_${r}`),disabled:o,onClick:s,style:i,...Ig(On().focus,o)})}function $_({render:{buttonPrev:t,buttonNext:e,iconPrev:n,iconNext:r},styles:s}){const{prev:o,next:i,subscribeSensors:a}=On(),{prevDisabled:l,nextDisabled:c}=Hg();return Ug(a),$.createElement($.Fragment,null,t?t():$.createElement(Bg,{label:"Previous",action:or,icon:w_,renderIcon:n,style:s.navigationPrev,disabled:l,onClick:o}),e?e():$.createElement(Bg,{label:"Next",action:ir,icon:k_,renderIcon:r,style:s.navigationNext,disabled:c,onClick:i}))}const F_=gn(WS,$_),jg=Ne(RS),V_=Ne(ag);function z_(t){return"style"in t}function Wg(t,e,n){const r=window.getComputedStyle(t),s=n?"padding-left":"padding-right",o=n?r.paddingLeft:r.paddingRight,i=t.style.getPropertyValue(s);return t.style.setProperty(s,`${($i(o)||0)+e}px`),()=>{i?t.style.setProperty(s,i):t.style.removeProperty(s)}}function H_({noScroll:{disabled:t},children:e}){const n=Hi(),{getOwnerDocument:r,getOwnerWindow:s}=ls();return $.useEffect(()=>{if(t)return()=>{};const o=[],i=s(),{body:a,documentElement:l}=r(),c=Math.round(i.innerWidth-l.clientWidth);if(c>0){o.push(Wg(a,c,n));const d=a.getElementsByTagName("*");for(let u=0;u<d.length;u+=1){const f=d[u];z_(f)&&i.getComputedStyle(f).getPropertyValue("position")==="fixed"&&!f.classList.contains(V_)&&o.push(Wg(f,c,n))}}return a.classList.add(jg),()=>{a.classList.remove(jg),o.forEach(d=>d())}},[n,t,r,s]),$.createElement($.Fragment,null,e)}const U_=gn(GS,H_);function Gg(t){return mr(oc,t)}function Yg(t,e,n){const r=t.getAttribute(e);return t.setAttribute(e,n),()=>{r?t.setAttribute(e,r):t.removeAttribute(e)}}function B_({children:t,animation:e,styles:n,className:r,on:s,portal:o,close:i,labels:a}){const[l,c]=$.useState(!1),[d,u]=$.useState(!1),f=$.useRef([]),g=$.useRef(null),{setTimeout:m}=co(),{subscribe:p}=cs(),b=xc()?0:e.fade;$.useEffect(()=>(c(!0),()=>{c(!1),u(!1)}),[]);const _=We(()=>{f.current.forEach(w=>w()),f.current=[]}),x=We(()=>{var w;u(!1),_(),(w=s.exiting)===null||w===void 0||w.call(s),m(()=>{var N;(N=s.exited)===null||N===void 0||N.call(s),i()},b)});$.useEffect(()=>p(os,x),[p,x]);const E=We(w=>{var N,C,k;d_(w),u(!0),(N=s.entering)===null||N===void 0||N.call(s);const M=(k=(C=w.parentNode)===null||C===void 0?void 0:C.children)!==null&&k!==void 0?k:[];for(let T=0;T<M.length;T+=1){const L=M[T];["TEMPLATE","SCRIPT","STYLE"].indexOf(L.tagName)===-1&&L!==w&&(f.current.push(Yg(L,"inert","")),f.current.push(Yg(L,"aria-hidden","true")))}f.current.push(()=>{var T,L;(L=(T=g.current)===null||T===void 0?void 0:T.focus)===null||L===void 0||L.call(T)}),m(()=>{var T;(T=s.entered)===null||T===void 0||T.call(s)},b)}),S=$.useCallback(w=>{w?E(w):_()},[E,_]);return l?Dn.createPortal($.createElement(E_,{ref:S,className:Ht(r,Ne(Gg()),Ne(ag),d&&Ne(Gg("open"))),"aria-modal":!0,role:"dialog","aria-label":yr(a,"Lightbox"),style:{...e.fade!==yc.animation.fade?{[dt("fade_animation_duration")]:`${b}ms`}:null,...e.easing.fade!==yc.animation.easing.fade?{[dt("fade_animation_timing_function")]:e.easing.fade}:null,...n.root},onFocus:w=>{g.current||(g.current=w.relatedTarget)}},t),o.root||document.body):null}const j_=gn(oc,B_);function W_({children:t}){return $.createElement($.Fragment,null,t)}const G_=gn(YS,W_);function Y_(t){return mr(rg,t)}function X_({toolbar:{buttons:t},render:{buttonClose:e,iconClose:n},styles:r}){const{close:s,setToolbarWidth:o}=On(),{setContainerRef:i,containerRect:a}=Ag();Mn(()=>{o(a?.width)},[o,a?.width]);const l=()=>e?e():$.createElement(ds,{key:os,label:"Close",icon:x_,renderIcon:n,onClick:s});return $.createElement("div",{ref:i,style:r.toolbar,className:Ne(Y_())},t?.map(c=>c===os?l():c))}const K_=gn(rg,X_);function Xg(t,e){var n;return $.createElement(t.module.component,{key:t.module.name,...e},(n=t.children)===null||n===void 0?void 0:n.map(r=>Xg(r,e)))}function q_(t,e={}){const{easing:n,...r}=t,{easing:s,...o}=e;return{easing:{...n,...s},...r,...o}}function Z_({carousel:t,animation:e,render:n,toolbar:r,controller:s,noScroll:o,on:i,plugins:a,slides:l,index:c,...d}){const{animation:u,carousel:f,render:g,toolbar:m,controller:p,noScroll:y,on:b,slides:_,index:x,plugins:E,...S}=yc,{config:w,augmentation:N}=u_([Dt(j_,[Dt(U_,[Dt(A_,[Dt(P_),Dt(K_),Dt(F_)])])])],a||E),C=N({animation:q_(u,e),carousel:{...f,...t},render:{...g,...n},toolbar:{...m,...r},controller:{...p,...s},noScroll:{...y,...o},on:{...b,...i},...S,...d});return C.open?$.createElement(Eg,{...C},$.createElement(b_,{slides:l||_,index:$i(c||x)},$.createElement(y_,null,$.createElement(g_,null,$.createElement(f_,null,Xg(Dt(G_,w),C)))))):null}const Q_={minZoom:1,maxZoomPixelRatio:1,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:500,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,pinchZoomV4:!1,scrollToZoom:!1};function J_(t){return Math.min(Math.max(t,Number.EPSILON),1)}function Kg(t){const{minZoom:e,...n}={...Q_,...t};return{minZoom:J_(e),...n}}function R_(t,e,n,r){const s=$.useRef(void 0),o=$.useRef(void 0),{zoom:i}=It().animation,a=xc(),l=We(()=>{var c,d,u;if((c=s.current)===null||c===void 0||c.cancel(),s.current=void 0,o.current&&r?.current){try{s.current=(u=(d=r.current).animate)===null||u===void 0?void 0:u.call(d,[{transform:o.current},{transform:`scale(${t}) translateX(${e}px) translateY(${n}px)`}],{duration:a?0:i??500,easing:s.current?"ease-out":"ease-in-out"})}catch(f){console.error(f)}o.current=void 0,s.current&&(s.current.onfinish=()=>{s.current=void 0})}});return Mn(l,[t,e,n,l]),$.useCallback(()=>{o.current=r?.current?window.getComputedStyle(r.current).transform:void 0},[r])}function e2(t,e){const{on:n}=It(),r=We(()=>{var s;e||(s=n.zoom)===null||s===void 0||s.call(n,{zoom:t})});$.useEffect(r,[t,r])}function Ui(){const{zoom:t}=It();return Kg(t)}function t2(t,e){var n,r;let s={width:0,height:0},o={width:0,height:0};const{currentSlide:i}=pn(),{imageFit:a}=It().carousel,{maxZoomPixelRatio:l}=Ui();if(t&&i){const d={...i,...e};if(vr(d)){const u=hc(d,a),f=Math.max(...(((n=d.srcSet)===null||n===void 0?void 0:n.map(m=>m.width))||[]).concat(d.width?[d.width]:[])),g=Math.max(...(((r=d.srcSet)===null||r===void 0?void 0:r.map(m=>m.height))||[]).concat(d.height?[d.height]:[]));f>0&&g>0&&t.width>0&&t.height>0&&(o=u?{width:Math.round(Math.min(f,t.width/t.height*g)),height:Math.round(Math.min(g,t.height/t.width*f))}:{width:f,height:g},o={width:o.width*l,height:o.height*l},s=u?{width:Math.min(t.width,o.width,f),height:Math.min(t.height,o.height,g)}:{width:Math.round(Math.min(t.width,t.height/g*f,f)),height:Math.round(Math.min(t.height,t.width/f*g,g))})}}const c=s.width?Math.max(fc(o.width/s.width,5),1):1;return{imageRect:s,maxZoom:c}}function qg(t,e){return Math.hypot(t.clientX-e.clientX,t.clientY-e.clientY)}function Zg(t,e,n=100,r=2){return t*Math.min(1+Math.abs(e/n),r)**Math.sign(e)}function n2(t,e,n,r,s,o,i,a,l){const c=$.useRef([]),d=$.useRef(0),u=$.useRef(void 0),{globalIndex:f}=pn(),{getOwnerWindow:g}=ls(),{containerRef:m,subscribeSensors:p}=On(),{keyboardMoveDistance:y,zoomInMultiplier:b,wheelZoomDistanceFactor:_,scrollToZoom:x,doubleTapDelay:E,doubleClickDelay:S,doubleClickMaxStops:w,pinchZoomDistanceFactor:N,pinchZoomV4:C}=Ui(),k=$.useCallback(V=>{if(m.current){const{pageX:H,pageY:W}=V,{scrollX:j,scrollY:U}=g(),{left:B,top:G,width:z,height:X}=m.current.getBoundingClientRect();return[H-B-j-z/2,W-G-U-X/2]}return[]},[m,g]),M=We(V=>{const{key:H,metaKey:W,ctrlKey:j}=V,U=W||j,B=()=>{V.preventDefault(),V.stopPropagation()};if(t>1){const G=(z,X)=>{B(),a(z,X)};H==="ArrowDown"?G(0,y):H==="ArrowUp"?G(0,-y):H==="ArrowLeft"?G(-y,0):H==="ArrowRight"&&G(y,0)}H==="+"||U&&H==="="?(B(),s()):H==="-"||U&&H==="_"?(B(),o()):U&&H==="0"&&(B(),i(1))}),T=We(V=>{if((V.ctrlKey||x)&&Math.abs(V.deltaY)>Math.abs(V.deltaX)){V.stopPropagation(),i(Zg(t,-V.deltaY,_),!0,...k(V));return}t>1&&(V.stopPropagation(),x||a(V.deltaX,V.deltaY))}),L=$.useCallback(V=>{const H=c.current;H.splice(0,H.length,...H.filter(W=>W.pointerId!==V.pointerId))},[]),P=$.useCallback(V=>{L(V),V.persist(),c.current.push(V)},[L]),F=We(V=>{var H;const W=c.current;if(V.pointerType==="mouse"&&V.buttons>1||!(!((H=l?.current)===null||H===void 0)&&H.contains(V.target)))return;t>1&&V.stopPropagation();const{timeStamp:j}=V;if(W.length===0&&j-d.current<(V.pointerType==="touch"?E:S)){d.current=0;const U=t>=1?t!==n?t*Math.max(n**(1/w),b):1:t!==e?t/Math.max(e**(-1/w),b):1;i(U,!1,...k(V))}else d.current=j;if(P(V),W.length===2){const U=qg(W[0],W[1]);u.current={previousDistance:U,initialDistance:Math.max(U,1),initialZoom:t}}}),D=We(V=>{const H=c.current,W=H.find(j=>j.pointerId===V.pointerId);if(H.length===2&&u.current){V.stopPropagation(),P(V);const j=qg(H[0],H[1]),U=C?u.current.initialZoom/u.current.initialDistance*j:Zg(t,j-u.current.previousDistance,N);i(U,!0,...H.map(B=>k(B)).reduce((B,G)=>G.map((z,X)=>B[X]+z/2))),u.current.previousDistance=j;return}t>1&&(V.stopPropagation(),W&&(H.length===1&&a((W.clientX-V.clientX)/t,(W.clientY-V.clientY)/t),P(V)))}),A=$.useCallback(V=>{const H=c.current;H.length===2&&H.find(W=>W.pointerId===V.pointerId)&&(u.current=void 0),L(V)},[L]),O=$.useCallback(()=>{const V=c.current;V.splice(0,V.length),d.current=0,u.current=void 0},[]);$g(p,F,D,A,r),$.useEffect(O,[f,O]),$.useEffect(()=>r?()=>{}:is(O,p(cc,M),p(dc,T)),[r,p,O,M,T])}function r2(t,e,n){const[r,s]=$.useState(1),[o,i]=$.useState(0),[a,l]=$.useState(0),c=R_(r,o,a,n),{currentSlide:d,globalIndex:u}=pn(),{containerRect:f,slideRect:g}=On(),{minZoom:m,zoomInMultiplier:p}=Ui(),y=d&&vr(d)?d.src:void 0,b=!y||!n?.current;Mn(()=>{s(1),i(0),l(0)},[u,y]);const _=$.useCallback((N,C,k)=>{const M=k||r,T=o-(N||0),L=a-(C||0),P=(t.width*M-g.width)/2/M,F=(t.height*M-g.height)/2/M;i(Math.min(Math.abs(T),Math.max(P,0))*Math.sign(T)),l(Math.min(Math.abs(L),Math.max(F,0))*Math.sign(L))},[r,o,a,g,t.width,t.height]),x=$.useCallback((N,C,k,M)=>{const T=fc(N+.01<e?N-.01>m?N:m:e,5);C||c(),_(k?k*(1/r-1/T):0,M?M*(1/r-1/T):0,T),s(T)},[r,m,e,_,c]),E=We(()=>{r>1&&(r>e&&x(e,!0),_())});Mn(E,[f.width,f.height,E]);const S=$.useCallback(()=>{const N=r*p;x(r<1&&N>1?1:N)},[r,p,x]),w=$.useCallback(()=>{const N=r/p;x(r>1&&N<1?1:N)},[r,p,x]);return{zoom:r,offsetX:o,offsetY:a,disabled:b,changeOffsets:_,changeZoom:x,zoomIn:S,zoomOut:w}}const Qg=$.createContext(null),Ec=hn("useZoom","ZoomControllerContext",Qg);function s2({children:t}){const[e,n]=$.useState(),{slideRect:r}=On(),{ref:s,minZoom:o}=Ui(),{imageRect:i,maxZoom:a}=t2(r,e?.imageDimensions),{zoom:l,offsetX:c,offsetY:d,disabled:u,changeZoom:f,changeOffsets:g,zoomIn:m,zoomOut:p}=r2(i,a,e?.zoomWrapperRef);e2(l,u),n2(l,o,a,u,m,p,f,g,e?.zoomWrapperRef);const y=$.useMemo(()=>({zoom:l,minZoom:o,maxZoom:a,offsetX:c,offsetY:d,disabled:u,zoomIn:m,zoomOut:p,changeZoom:f}),[l,o,a,c,d,u,m,p,f]);$.useImperativeHandle(s,()=>y,[y]);const b=$.useMemo(()=>({...y,setZoomWrapper:n}),[y,n]);return $.createElement(Qg.Provider,{value:b},t)}const o2=en("ZoomIn",$.createElement($.Fragment,null,$.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"}),$.createElement("path",{d:"M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"}))),i2=en("ZoomOut",$.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"})),Jg=$.forwardRef(function({zoomIn:e,onLoseFocus:n},r){const s=$.useRef(!1),o=$.useRef(!1),{zoom:i,minZoom:a,maxZoom:l,zoomIn:c,zoomOut:d,disabled:u}=Ec(),{render:f}=It(),g=u||(e?i>=l:i<=a);return $.useEffect(()=>{g&&s.current&&o.current&&n(),g||(s.current=!0)},[g,n]),$.createElement(ds,{ref:r,disabled:g,label:e?"Zoom in":"Zoom out",icon:e?o2:i2,renderIcon:e?f.iconZoomIn:f.iconZoomOut,onClick:e?c:d,onFocus:()=>{o.current=!0},onBlur:()=>{o.current=!1}})});function a2(){const t=$.useRef(null),e=$.useRef(null),{focus:n}=On(),r=$.useCallback(i=>{var a,l;!((a=i.current)===null||a===void 0)&&a.disabled?n():(l=i.current)===null||l===void 0||l.focus()},[n]),s=$.useCallback(()=>r(t),[r]),o=$.useCallback(()=>r(e),[r]);return $.createElement($.Fragment,null,$.createElement(Jg,{zoomIn:!0,ref:t,onLoseFocus:o}),$.createElement(Jg,{ref:e,onLoseFocus:s}))}function l2(){const{render:t}=It(),e=Ec();return t.buttonZoom?$.createElement($.Fragment,null,t.buttonZoom(e)):$.createElement(a2,null)}function c2(t){var e;return(((e=t.srcSet)===null||e===void 0?void 0:e.length)||0)>0}function d2({current:t,preload:e},{type:n,source:r}){switch(n){case"fetch":return t?{current:t,preload:r}:{current:r};case"done":return r===e?{current:r}:{current:t,preload:e};default:throw new Error(mg)}}function u2(t){var e,n;const[{current:r,preload:s},o]=$.useReducer(d2,{}),{slide:i,rect:a,imageFit:l,render:c,interactive:d}=t,u=i.srcSet.sort((S,w)=>S.width-w.width),f=(e=i.width)!==null&&e!==void 0?e:u[u.length-1].width,g=(n=i.height)!==null&&n!==void 0?n:u[u.length-1].height,m=hc(i,l),p=Math.max(...u.map(S=>S.width)),y=Math.min((m?Math.max:Math.min)(a.width,f*(a.height/g)),p),b=a_(),_=We(()=>{var S;const w=(S=u.find(N=>N.width>=y*b))!==null&&S!==void 0?S:u[u.length-1];(!r||u.findIndex(N=>N.src===r)<u.findIndex(N=>N===w))&&o({type:"fetch",source:w.src})});Mn(_,[a.width,a.height,b,_]);const x=We(S=>o({type:"done",source:S})),E={WebkitTransform:d?"initial":"translateZ(0)"};return m||Object.assign(E,a.width/a.height<f/g?{width:"100%",height:"auto"}:{width:"auto",height:"100%"}),$.createElement($.Fragment,null,s&&s!==r&&$.createElement(xr,{key:"preload",...t,offset:void 0,slide:{...i,src:s,srcSet:void 0},style:{position:"absolute",visibility:"hidden",...E},onLoad:()=>x(s),render:{...c,iconLoading:()=>null,iconError:()=>null}}),r&&$.createElement(xr,{key:"current",...t,slide:{...i,src:r,srcSet:void 0},style:E}))}function f2({render:t,slide:e,offset:n,rect:r}){var s;const[o,i]=$.useState(),a=$.useRef(null),{zoom:l,maxZoom:c,offsetX:d,offsetY:u,setZoomWrapper:f}=Ec(),g=l>1,{carousel:m,on:p}=It(),{currentIndex:y}=pn();Mn(()=>n===0?(f({zoomWrapperRef:a,imageDimensions:o}),()=>f(void 0)):()=>{},[n,o,f]);let b=(s=t.slide)===null||s===void 0?void 0:s.call(t,{slide:e,offset:n,rect:r,zoom:l,maxZoom:c});if(!b&&vr(e)){const _={slide:e,offset:n,rect:r,render:t,imageFit:m.imageFit,imageProps:m.imageProps,onClick:n===0?()=>{var x;return(x=p.click)===null||x===void 0?void 0:x.call(p,{index:y})}:void 0};b=c2(e)?$.createElement(u2,{..._,slide:e,interactive:g,rect:n===0?{width:r.width*l,height:r.height*l}:r}):$.createElement(xr,{onLoad:x=>i({width:x.naturalWidth,height:x.naturalHeight}),..._})}return b?$.createElement("div",{ref:a,className:Ht(Ne(ig),Ne(ss),Ne(lg),g&&Ne(e_)),style:n===0?{transform:`scale(${l}) translateX(${d}px) translateY(${u}px)`}:void 0},b):null}const h2=({augment:t,addModule:e})=>{t(({zoom:n,toolbar:r,render:s,controller:o,...i})=>{const a=Kg(n);return{zoom:a,toolbar:Vi(r,og,$.createElement(l2,null)),render:{...s,slide:l=>{var c;return vr(l.slide)?$.createElement(f2,{render:s,...l}):(c=s.slide)===null||c===void 0?void 0:c.call(s,l)}},controller:{...o,preventDefaultWheelY:a.scrollToZoom},...i}}),e(gn(og,s2))},g2={separator:"/",container:{}},Rg=t=>({...g2,...t});function p2({counter:t}){const{slides:e,currentIndex:n}=pn(),{separator:r,container:{className:s,...o},className:i,...a}=Rg(t);return e.length===0?null:$.createElement("div",{className:Ht(Ne("counter"),s||i),...a,...o,"aria-hidden":!0},n+1," ",r," ",e.length)}function m2({augment:t,addChild:e}){t(({counter:n,...r})=>({counter:Rg(n),...r})),e(ro,gn(XS,p2))}const b2={autoplay:!1,delay:3e3,ref:null},ep=t=>({...b2,...t}),tp=$.createContext(null),y2=hn("useSlideshow","SlideshowContext",tp);function v2({slideshow:t,carousel:{finite:e},on:n,children:r}){const{autoplay:s,delay:o,ref:i}=ep(t),a=$.useRef(s),[l,c]=$.useState(s),d=$.useRef(void 0),u=$.useRef(void 0),{slides:f,currentIndex:g}=pn(),{setTimeout:m,clearTimeout:p}=co(),{subscribe:y}=cs(),{next:b}=On(),{setAutoPlaying:_}=vc();$.useEffect(()=>_(l),[l,_]);const x=f.length===0||e&&g===f.length-1,E=$.useCallback(()=>{!l&&!x&&c(!0)},[l,x]),S=$.useCallback(()=>{l&&c(!1)},[l]),w=$.useCallback(()=>{p(d.current),d.current=void 0},[p]),N=We(()=>{w(),!(!l||x||u.current===rs||u.current===ic)&&(d.current=m(()=>{l&&(u.current=void 0,b())},o))});$.useEffect(N,[g,l,N]),$.useEffect(()=>{l&&x&&c(!1)},[g,l,x]);const C=We(()=>{var T;return(T=n.slideshowStart)===null||T===void 0?void 0:T.call(n)}),k=We(()=>{var T;return(T=n.slideshowStop)===null||T===void 0?void 0:T.call(n)});$.useEffect(()=>{l?C():a.current&&k(),a.current=l},[l,C,k]),$.useEffect(()=>is(w,y(qS,()=>{u.current=rs,w()}),y(ZS,()=>{u.current=ic,w()}),y(QS,()=>{u.current=io,N()}),y(JS,()=>{u.current=ao,N()})),[y,w,N]);const M=$.useMemo(()=>({playing:l,disabled:x,play:E,pause:S}),[l,x,E,S]);return $.useImperativeHandle(i,()=>M,[M]),$.createElement(tp.Provider,{value:M},r)}const x2=en("Play",$.createElement("path",{d:"M8 5v14l11-7z"})),w2=en("Pause",$.createElement("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"}));function k2(){const{playing:t,disabled:e,play:n,pause:r}=y2(),{render:s}=It(),o=Ig(On().focus,e);return s.buttonSlideshow?$.createElement($.Fragment,null,s.buttonSlideshow({playing:t,disabled:e,play:n,pause:r})):$.createElement(ds,{label:t?"Pause":"Play",icon:t?w2:x2,renderIcon:t?s.iconSlideshowPause:s.iconSlideshowPlay,onClick:t?r:n,disabled:e,...o})}function C2({augment:t,addModule:e}){t(({slideshow:n,toolbar:r,...s})=>({toolbar:Vi(r,sg,$.createElement(k2,null)),slideshow:ep(n),...s})),e(gn(sg,v2))}const S2={auto:!1,ref:null},np=t=>({...S2,...t}),rp=$.createContext(null),_2=hn("useFullscreen","FullscreenContext",rp);function E2({fullscreen:t,on:e,children:n}){const{auto:r,ref:s}=np(t),o=$.useRef(null),[i,a]=$.useState(),[l,c]=$.useState(!1),d=$.useRef(!1),{getOwnerDocument:u}=ls();Mn(()=>{var x,E,S,w;const N=u();a(!((w=(S=(E=(x=N.fullscreenEnabled)!==null&&x!==void 0?x:N.webkitFullscreenEnabled)!==null&&E!==void 0?E:N.mozFullScreenEnabled)!==null&&S!==void 0?S:N.msFullscreenEnabled)!==null&&w!==void 0&&w))},[u]);const f=$.useCallback(()=>{var x;const E=u(),S=E.fullscreenElement||E.webkitFullscreenElement||E.mozFullScreenElement||E.msFullscreenElement;return((x=S?.shadowRoot)===null||x===void 0?void 0:x.fullscreenElement)||S},[u]),g=$.useCallback(()=>{const x=o.current;try{x.requestFullscreen?x.requestFullscreen().catch(()=>{}):x.webkitRequestFullscreen?x.webkitRequestFullscreen():x.mozRequestFullScreen?x.mozRequestFullScreen():x.msRequestFullscreen&&x.msRequestFullscreen()}catch{}},[]),m=$.useCallback(()=>{if(!f())return;const x=u();try{x.exitFullscreen?x.exitFullscreen().catch(()=>{}):x.webkitExitFullscreen?x.webkitExitFullscreen():x.mozCancelFullScreen?x.mozCancelFullScreen():x.msExitFullscreen&&x.msExitFullscreen()}catch{}},[f,u]);$.useEffect(()=>{const x=u(),E=()=>{c(f()===o.current)};return is(...["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange"].map(S=>(x.addEventListener(S,E),()=>x.removeEventListener(S,E))))},[f,u]);const p=We(()=>{var x;return(x=e.enterFullscreen)===null||x===void 0?void 0:x.call(e)}),y=We(()=>{var x;return(x=e.exitFullscreen)===null||x===void 0?void 0:x.call(e)});$.useEffect(()=>{l&&(d.current=!0),d.current&&(l?p:y)()},[l,p,y]);const b=We(()=>{var x;return(x=r?g:null)===null||x===void 0||x(),m});$.useEffect(b,[b]);const _=$.useMemo(()=>({fullscreen:l,disabled:i,enter:g,exit:m}),[l,i,g,m]);return $.useImperativeHandle(s,()=>_,[_]),$.createElement("div",{ref:o,className:Ht(Ne(so),Ne(ig))},$.createElement(rp.Provider,{value:_},n))}const M2=en("EnterFullscreen",$.createElement("path",{d:"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"})),N2=en("ExitFullscreen",$.createElement("path",{d:"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"}));function O2(){var t;const{fullscreen:e,disabled:n,enter:r,exit:s}=_2(),{render:o}=It();return n?null:o.buttonFullscreen?$.createElement($.Fragment,null,(t=o.buttonFullscreen)===null||t===void 0?void 0:t.call(o,{fullscreen:e,disabled:n,enter:r,exit:s})):$.createElement(ds,{disabled:n,label:e?"Exit Fullscreen":"Enter Fullscreen",icon:e?N2:M2,renderIcon:e?o.iconExitFullscreen:o.iconEnterFullscreen,onClick:e?s:r})}function T2({augment:t,contains:e,addParent:n}){t(({fullscreen:r,toolbar:s,...o})=>({toolbar:Vi(s,so,$.createElement(O2,null)),fullscreen:np(r),...o})),n(e(oo)?oo:ro,gn(so,E2))}const kr={ref:null,position:"bottom",width:120,height:80,border:1,borderRadius:4,padding:4,gap:16,imageFit:"contain",vignette:!0,hidden:!1,showToggle:!1},Mc=t=>({...kr,...t});function sp(){const{thumbnails:t}=It();return Mc(t)}const Cr=t=>mr(oo,t),Lt=t=>Cr(mr("thumbnail",t)),A2=en("VideoThumbnail",$.createElement("path",{d:"M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"})),D2=en("UnknownThumbnail",$.createElement("path",{d:"M23 18V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zM8.5 12.5l2.5 3.01L14.5 11l4.5 6H5l3.5-4.5z"}));function I2({slide:t,render:e,rect:n,imageFit:r}){var s;const o=(s=e.thumbnail)===null||s===void 0?void 0:s.call(e,{slide:t,render:e,rect:n,imageFit:r});if(o)return o;const i={render:e,rect:n,imageFit:r};if(t.thumbnail)return $.createElement(xr,{slide:{src:t.thumbnail},...i});if(vr(t))return $.createElement(xr,{slide:t,...i});const a=Ne(Lt(Pi));return t.type==="video"?$.createElement($.Fragment,null,t.poster&&$.createElement(xr,{slide:{src:t.poster},...i}),$.createElement(A2,{className:a})):$.createElement(D2,{className:a})}const L2=br("active"),Nc=br("fadein"),Oc=br("fadeout"),P2=br("placeholder"),op="delay",ip="duration";function $2({slide:t,index:e,onClick:n,fadeIn:r,fadeOut:s,placeholder:o,onLoseFocus:i}){const a=$.useRef(null),{render:l,styles:c,labels:d}=It(),{slides:u,globalIndex:f}=pn(),{getOwnerDocument:g}=ls(),{width:m,height:p,imageFit:y}=sp(),b={width:m,height:p},_=e===f,x=We(i);return $.useEffect(()=>{s&&g().activeElement===a.current&&x()},[s,x,g]),$.createElement("button",{ref:a,type:"button",className:Ht(Ne(ss),Ne(Lt()),_&&Ne(Lt(L2())),r&&Ne(Lt(Nc())),s&&Ne(Lt(Oc())),o&&Ne(Lt(P2()))),style:{...r?{[dt(Lt(Nc(ip)))]:`${r.duration}ms`,[dt(Lt(Nc(op)))]:`${r.delay}ms`}:null,...s?{[dt(Lt(Oc(ip)))]:`${s.duration}ms`,[dt(Lt(Oc(op)))]:`${s.delay}ms`}:null,...c.thumbnail},onClick:n,"aria-current":_?!0:void 0,"aria-label":yg(d,u,e)},t&&I2({slide:t,render:l,rect:b,imageFit:y}))}function ap(t){return["top","bottom"].includes(t)}function lp(t,e){return e+2*(t.border+t.padding)+t.gap}function F2(t){const{thumbnail:e,poster:n}=t||{thumbnail:"placeholder"};return typeof e=="string"&&e||typeof n=="string"&&n||t&&vg(t)||void 0}function cp({visible:t,containerRef:e}){const n=$.useRef(null),r=Hi(),{publish:s,subscribe:o}=cs(),{carousel:i,styles:a,labels:l}=It(),{slides:c,globalIndex:d,animation:u}=pn(),{registerSensors:f,subscribeSensors:g}=Lg();Ug(g);const m=sp(),{position:p,width:y,height:b,border:_,borderStyle:x,borderColor:E,borderRadius:S,padding:w,gap:N,vignette:C}=m,k=u?.duration!==void 0&&u?.increment||0,M=u?.duration||0,{prepareAnimation:T}=wc(n,A=>({keyframes:ap(p)?[{transform:`translateX(${(r?-1:1)*lp(m,y)*k+A}px)`},{transform:"translateX(0)"}]:[{transform:`translateY(${lp(m,b)*k+A}px)`},{transform:"translateY(0)"}],duration:M,easing:u?.easing})),L=We(()=>{let A=0;if(e.current&&n.current){const O=e.current.getBoundingClientRect(),V=n.current.getBoundingClientRect();A=ap(p)?V.left-O.left-(O.width-V.width)/2:V.top-O.top-(O.height-V.height)/2}T(A)});$.useEffect(()=>is(o(lc,L)),[o,L]);const P=xg(i,c),F=[];if(pc(c))for(let A=d-P-Math.abs(k);A<=d+P+Math.abs(k);A+=1){const V=i.finite&&(A<0||A>c.length-1)||k<0&&A<d-P||k>0&&A>d+P?null:mc(c,A),H=[`${A}`,F2(V)].filter(Boolean).join("|");F.push({key:H,index:A,slide:V})}const D=A=>()=>{A>d?s(ir,{count:A-d}):A<d&&s(or,{count:d-A})};return $.createElement("div",{className:Ht(Ne(Cr("container")),Ne(ss)),style:{...t?null:{display:"none"},...y!==kr.width?{[dt(Lt("width"))]:`${y}px`}:null,...b!==kr.height?{[dt(Lt("height"))]:`${b}px`}:null,..._!==kr.border?{[dt(Lt("border"))]:`${_}px`}:null,...x?{[dt(Lt("border_style"))]:x}:null,...E?{[dt(Lt("border_color"))]:E}:null,...S!==kr.borderRadius?{[dt(Lt("border_radius"))]:`${S}px`}:null,...w!==kr.padding?{[dt(Lt("padding"))]:`${w}px`}:null,...N!==kr.gap?{[dt(Lt("gap"))]:`${N}px`}:null,...a.thumbnailsContainer}},$.createElement("nav",{ref:n,style:a.thumbnailsTrack,className:Ht(Ne(Cr("track")),Ne(ss)),"aria-label":yr(l,"Thumbnails"),tabIndex:-1,...f},F.map(({key:A,index:O,slide:V})=>{const H=M/Math.abs(k||1),W=k>0&&O>d+P-k&&O<=d+P||k<0&&O<d-P-k&&O>=d-P?{duration:H,delay:((k>0?O-(d+P-k):d-P-k-O)-1)*H}:void 0,j=k>0&&O<d-P||k<0&&O>d+P?{duration:H,delay:(k>0?k-(d-P-O):-k-(O-(d+P)))*H}:void 0;return $.createElement($2,{key:A,index:O,slide:V,fadeIn:W,fadeOut:j,placeholder:!V,onClick:D(O),onLoseFocus:()=>{var U;return(U=n.current)===null||U===void 0?void 0:U.focus()}})})),C&&$.createElement("div",{className:Ne(Cr("vignette"))}))}const dp=$.createContext(null),V2=hn("useThumbnails","ThumbnailsContext",dp);function z2({children:t,...e}){const{ref:n,position:r,hidden:s}=Mc(e.thumbnails),[o,i]=$.useState(!s),a=$.useRef(null),l=$.useMemo(()=>({visible:o,show:()=>i(!0),hide:()=>i(!1)}),[o]);return $.useImperativeHandle(n,()=>l,[l]),$.createElement(Eg,{...e},$.createElement(dp.Provider,{value:l},$.createElement("div",{ref:a,className:Ht(Ne(Cr()),Ne(Cr(`${r}`)))},["start","top"].includes(r)&&$.createElement(cp,{containerRef:a,visible:o}),$.createElement("div",{className:Ne(Cr("wrapper"))},t),["end","bottom"].includes(r)&&$.createElement(cp,{containerRef:a,visible:o}))))}const up=()=>$.createElement($.Fragment,null,$.createElement("path",{strokeWidth:2,stroke:"currentColor",strokeLinejoin:"round",fill:"none",d:"M3 5l18 0l0 14l-18 0l0-14z"}),$.createElement("path",{d:"M5 14h4v3h-4zM10 14h4v3h-4zM15 14h4v3h-4z"})),H2=en("ThumbnailsVisible",up()),U2=v_("ThumbnailsHidden",up());function B2(){const{visible:t,show:e,hide:n}=V2(),{render:r}=It();return r.buttonThumbnails?$.createElement($.Fragment,null,r.buttonThumbnails({visible:t,show:e,hide:n})):$.createElement(ds,{label:t?"Hide thumbnails":"Show thumbnails",icon:t?H2:U2,renderIcon:t?r.iconThumbnailsVisible:r.iconThumbnailsHidden,onClick:t?n:e})}function j2({augment:t,contains:e,append:n,addParent:r}){t(({thumbnails:o,toolbar:i,...a})=>{const l=Mc(o);return{toolbar:Vi(i,oo,l.showToggle?$.createElement(B2,null):null),thumbnails:l,...a}});const s=gn(oo,z2);e(so)?n(so,s):r(ro,s)}const Tc=t=>{if(!t||typeof window>"u"||!window.location||!/^(https?:)?\/\//i.test(t))return!1;try{return new URL(t,window.location.href).origin!==window.location.origin}catch{return!1}},W2=window.G7Core?.createLogger?.("Comp:ImageGallery")??{log:(...t)=>console.log("[Comp:ImageGallery]",...t),warn:(...t)=>console.warn("[Comp:ImageGallery]",...t),error:(...t)=>console.error("[Comp:ImageGallery]",...t)},Ac=window.G7Core,Bi=(t,e)=>Ac?.t?.(t,e)??t,G2=async(t,e)=>{try{const n=await Ac.api.get(t,{responseType:"blob"});if(n){const r=URL.createObjectURL(n),s=document.createElement("a");s.href=r,s.download=e,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(r)}}catch(n){W2.error("Failed to download file:",n),Ac?.toast?.error?.(Bi("common.download_failed"))}},Y2=(t,e)=>{const n=document.createElement("a");n.href=t,n.download=e,n.target="_blank",document.body.appendChild(n),n.click(),document.body.removeChild(n)},Dc=async t=>{const e=t.downloadUrl||t.src,n=t.filename||t.title||"image";t.downloadRequiresAuth&&!Tc(e)?await G2(e,n):Y2(e,n)},X2=({image:t,index:e,onDownload:n})=>{const[r,s]=v.useState(!1),o=async i=>{if(i.stopPropagation(),n){n(t,e);return}s(!0);try{await Dc(t)}finally{s(!1)}};return h.jsx(oe,{type:"button",onClick:o,disabled:r,className:"yarl__button flex items-center justify-center","aria-label":Bi("common.download"),title:Bi("common.download"),children:r?h.jsx(gt,{className:"fa-solid fa-spinner fa-spin text-white"}):h.jsx(gt,{className:"fa-solid fa-download text-white"})})},ji=({images:t,isOpen:e,onClose:n,startIndex:r=0,enableZoom:s=!0,enableSlideshow:o=!1,enableFullscreen:i=!0,showCounter:a=!0,showDownload:l=!0,showThumbnails:c=!0,onDownload:d,editorAttrs:u,id:f})=>{const[g,m]=v.useState(r),p=v.useRef(r),y=t.map(x=>({src:x.src,title:x.title,description:x.description})),b=[];s&&b.push(h2),o&&b.push(C2),i&&b.push(T2),a&&b.push(m2),c&&b.push(j2);const _=t[g];if(v.useEffect(()=>{p.current=g},[g]),u){const x=(t??[]).slice(0,4);return h.jsx(I,{id:f,className:"grid grid-cols-2 gap-2 p-3 rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800",...u,children:x.length>0?x.map((E,S)=>h.jsx(kn,{src:E.thumbnail||E.src,alt:E.title||"",className:"w-full aspect-square object-cover rounded bg-gray-200 dark:bg-gray-700"},S)):h.jsxs(I,{className:"col-span-2 flex items-center justify-center py-6 text-sm text-gray-400 dark:text-gray-500",children:[h.jsx(gt,{className:"fa-regular fa-images mr-2"}),Bi("editor.component.image_gallery")]})})}return h.jsx(Z_,{open:e,close:n,slides:y,index:g,plugins:b,on:{view:({index:x})=>{x!==p.current&&m(x)}},zoom:{maxZoomPixelRatio:3,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:300,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,scrollToZoom:!0},carousel:{finite:!0,preload:2,padding:"16px",spacing:"30%"},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},controller:{closeOnBackdropClick:!0,closeOnPullDown:!0,closeOnPullUp:!0},thumbnails:{position:"bottom",width:120,height:80,border:2,borderRadius:4,padding:4,gap:16,showToggle:!1,vignette:!0},toolbar:{buttons:[l&&_&&h.jsx(X2,{image:_,index:g,onDownload:d},"download"),"close"].filter(Boolean)},styles:{container:{backgroundColor:"rgba(0, 0, 0, 0.9)"}}})},fp=()=>{const[t,e]=v.useState(!1),[n,r]=v.useState([]),[s,o]=v.useState(0),i=v.useCallback((l,c=0)=>{r(l),o(c),e(!0)},[]),a=v.useCallback(()=>{e(!1)},[]);return{isOpen:t,openGallery:i,closeGallery:a,galleryProps:{images:n,isOpen:t,onClose:a,startIndex:s}}};ji.displayName="ImageGallery";const K2=Object.freeze(Object.defineProperty({__proto__:null,ImageGallery:ji,executeImageDownload:Dc,useImageGallery:fp},Symbol.toStringTag,{value:"Module"})),q2=v.createContext(null),Z2=({value:t,children:e})=>h.jsx(q2.Provider,{value:t,children:e});function Q2(t,e){return e.forEach((function(n){n&&typeof n!="string"&&!Array.isArray(n)&&Object.keys(n).forEach((function(r){if(r!=="default"&&!(r in t)){var s=Object.getOwnPropertyDescriptor(n,r);Object.defineProperty(t,r,s.get?s:{enumerable:!0,get:function(){return n[r]}})}}))})),Object.freeze(t)}function hp(t,e){return new Promise((function(n,r){let s;return J2(t).then((function(o){try{return s=o,n(new Blob([e.slice(0,2),s,e.slice(2)],{type:"image/jpeg"}))}catch(i){return r(i)}}),r)}))}const J2=t=>new Promise(((e,n)=>{const r=new FileReader;r.addEventListener("load",(({target:{result:s}})=>{const o=new DataView(s);let i=0;if(o.getUint16(i)!==65496)return n("not a valid JPEG");for(i+=2;;){const a=o.getUint16(i);if(a===65498)break;const l=o.getUint16(i+2);if(a===65505&&o.getUint32(i+4)===1165519206){const c=i+10;let d;switch(o.getUint16(c)){case 18761:d=!0;break;case 19789:d=!1;break;default:return n("TIFF header contains invalid endian")}if(o.getUint16(c+2,d)!==42)return n("TIFF header contains invalid version");const u=o.getUint32(c+4,d),f=c+u+2+12*o.getUint16(c+u,d);for(let g=c+u+2;g<f;g+=12)if(o.getUint16(g,d)==274){if(o.getUint16(g+2,d)!==3)return n("Orientation data type is invalid");if(o.getUint32(g+4,d)!==1)return n("Orientation data count is invalid");o.setUint16(g+8,1,d);break}return e(s.slice(i,i+2+l))}i+=2+l}return e(new Blob)})),r.readAsArrayBuffer(t)}));var Wi={},R2={get exports(){return Wi},set exports(t){Wi=t}};(function(t){var e,n,r={};R2.exports=r,r.parse=function(s,o){for(var i=r.bin.readUshort,a=r.bin.readUint,l=0,c={},d=new Uint8Array(s),u=d.length-4;a(d,u)!=101010256;)u--;l=u,l+=4;var f=i(d,l+=4);i(d,l+=2);var g=a(d,l+=2),m=a(d,l+=4);l+=4,l=m;for(var p=0;p<f;p++){a(d,l),l+=4,l+=4,l+=4,a(d,l+=4),g=a(d,l+=4);var y=a(d,l+=4),b=i(d,l+=4),_=i(d,l+2),x=i(d,l+4);l+=6;var E=a(d,l+=8);l+=4,l+=b+_+x,r._readLocal(d,E,c,g,y,o)}return c},r._readLocal=function(s,o,i,a,l,c){var d=r.bin.readUshort,u=r.bin.readUint;u(s,o),d(s,o+=4),d(s,o+=2);var f=d(s,o+=2);u(s,o+=2),u(s,o+=4),o+=4;var g=d(s,o+=8),m=d(s,o+=2);o+=2;var p=r.bin.readUTF8(s,o,g);if(o+=g,o+=m,c)i[p]={size:l,csize:a};else{var y=new Uint8Array(s.buffer,o);if(f==0)i[p]=new Uint8Array(y.buffer.slice(o,o+a));else{if(f!=8)throw"unknown compression method: "+f;var b=new Uint8Array(l);r.inflateRaw(y,b),i[p]=b}}},r.inflateRaw=function(s,o){return r.F.inflate(s,o)},r.inflate=function(s,o){return s[0],s[1],r.inflateRaw(new Uint8Array(s.buffer,s.byteOffset+2,s.length-6),o)},r.deflate=function(s,o){o==null&&(o={level:6});var i=0,a=new Uint8Array(50+Math.floor(1.1*s.length));a[i]=120,a[i+1]=156,i+=2,i=r.F.deflateRaw(s,a,i,o.level);var l=r.adler(s,0,s.length);return a[i+0]=l>>>24&255,a[i+1]=l>>>16&255,a[i+2]=l>>>8&255,a[i+3]=l>>>0&255,new Uint8Array(a.buffer,0,i+4)},r.deflateRaw=function(s,o){o==null&&(o={level:6});var i=new Uint8Array(50+Math.floor(1.1*s.length)),a=r.F.deflateRaw(s,i,a,o.level);return new Uint8Array(i.buffer,0,a)},r.encode=function(s,o){o==null&&(o=!1);var i=0,a=r.bin.writeUint,l=r.bin.writeUshort,c={};for(var d in s){var u=!r._noNeed(d)&&!o,f=s[d],g=r.crc.crc(f,0,f.length);c[d]={cpr:u,usize:f.length,crc:g,file:u?r.deflateRaw(f):f}}for(var d in c)i+=c[d].file.length+30+46+2*r.bin.sizeUTF8(d);i+=22;var m=new Uint8Array(i),p=0,y=[];for(var d in c){var b=c[d];y.push(p),p=r._writeHeader(m,p,d,b,0)}var _=0,x=p;for(var d in c)b=c[d],y.push(p),p=r._writeHeader(m,p,d,b,1,y[_++]);var E=p-x;return a(m,p,101010256),p+=4,l(m,p+=4,_),l(m,p+=2,_),a(m,p+=2,E),a(m,p+=4,x),p+=4,p+=2,m.buffer},r._noNeed=function(s){var o=s.split(".").pop().toLowerCase();return"png,jpg,jpeg,zip".indexOf(o)!=-1},r._writeHeader=function(s,o,i,a,l,c){var d=r.bin.writeUint,u=r.bin.writeUshort,f=a.file;return d(s,o,l==0?67324752:33639248),o+=4,l==1&&(o+=2),u(s,o,20),u(s,o+=2,0),u(s,o+=2,a.cpr?8:0),d(s,o+=2,0),d(s,o+=4,a.crc),d(s,o+=4,f.length),d(s,o+=4,a.usize),u(s,o+=4,r.bin.sizeUTF8(i)),u(s,o+=2,0),o+=2,l==1&&(o+=2,o+=2,d(s,o+=6,c),o+=4),o+=r.bin.writeUTF8(s,o,i),l==0&&(s.set(f,o),o+=f.length),o},r.crc={table:(function(){for(var s=new Uint32Array(256),o=0;o<256;o++){for(var i=o,a=0;a<8;a++)1&i?i=3988292384^i>>>1:i>>>=1;s[o]=i}return s})(),update:function(s,o,i,a){for(var l=0;l<a;l++)s=r.crc.table[255&(s^o[i+l])]^s>>>8;return s},crc:function(s,o,i){return 4294967295^r.crc.update(4294967295,s,o,i)}},r.adler=function(s,o,i){for(var a=1,l=0,c=o,d=o+i;c<d;){for(var u=Math.min(c+5552,d);c<u;)l+=a+=s[c++];a%=65521,l%=65521}return l<<16|a},r.bin={readUshort:function(s,o){return s[o]|s[o+1]<<8},writeUshort:function(s,o,i){s[o]=255&i,s[o+1]=i>>8&255},readUint:function(s,o){return 16777216*s[o+3]+(s[o+2]<<16|s[o+1]<<8|s[o])},writeUint:function(s,o,i){s[o]=255&i,s[o+1]=i>>8&255,s[o+2]=i>>16&255,s[o+3]=i>>24&255},readASCII:function(s,o,i){for(var a="",l=0;l<i;l++)a+=String.fromCharCode(s[o+l]);return a},writeASCII:function(s,o,i){for(var a=0;a<i.length;a++)s[o+a]=i.charCodeAt(a)},pad:function(s){return s.length<2?"0"+s:s},readUTF8:function(s,o,i){for(var a,l="",c=0;c<i;c++)l+="%"+r.bin.pad(s[o+c].toString(16));try{a=decodeURIComponent(l)}catch{return r.bin.readASCII(s,o,i)}return a},writeUTF8:function(s,o,i){for(var a=i.length,l=0,c=0;c<a;c++){var d=i.charCodeAt(c);if((4294967168&d)==0)s[o+l]=d,l++;else if((4294965248&d)==0)s[o+l]=192|d>>6,s[o+l+1]=128|d>>0&63,l+=2;else if((4294901760&d)==0)s[o+l]=224|d>>12,s[o+l+1]=128|d>>6&63,s[o+l+2]=128|d>>0&63,l+=3;else{if((4292870144&d)!=0)throw"e";s[o+l]=240|d>>18,s[o+l+1]=128|d>>12&63,s[o+l+2]=128|d>>6&63,s[o+l+3]=128|d>>0&63,l+=4}}return l},sizeUTF8:function(s){for(var o=s.length,i=0,a=0;a<o;a++){var l=s.charCodeAt(a);if((4294967168&l)==0)i++;else if((4294965248&l)==0)i+=2;else if((4294901760&l)==0)i+=3;else{if((4292870144&l)!=0)throw"e";i+=4}}return i}},r.F={},r.F.deflateRaw=function(s,o,i,a){var l=[[0,0,0,0,0],[4,4,8,4,0],[4,5,16,8,0],[4,6,16,16,0],[4,10,16,32,0],[8,16,32,32,0],[8,16,128,128,0],[8,32,128,256,0],[32,128,258,1024,1],[32,258,258,4096,1]][a],c=r.F.U,d=r.F._goodIndex;r.F._hash;var u=r.F._putsE,f=0,g=i<<3,m=0,p=s.length;if(a==0){for(;f<p;)u(o,g,f+(T=Math.min(65535,p-f))==p?1:0),g=r.F._copyExact(s,f,T,o,g+8),f+=T;return g>>>3}var y=c.lits,b=c.strt,_=c.prev,x=0,E=0,S=0,w=0,N=0,C=0;for(p>2&&(b[C=r.F._hash(s,0)]=0),f=0;f<p;f++){if(N=C,f+1<p-2){C=r.F._hash(s,f+1);var k=f+1&32767;_[k]=b[C],b[C]=k}if(m<=f){(x>14e3||E>26697)&&p-f>100&&(m<f&&(y[x]=f-m,x+=2,m=f),g=r.F._writeBlock(f==p-1||m==p?1:0,y,x,w,s,S,f-S,o,g),x=E=w=0,S=f);var M=0;f<p-2&&(M=r.F._bestMatch(s,f,_,N,Math.min(l[2],p-f),l[3]));var T=M>>>16,L=65535&M;if(M!=0){L=65535&M;var P=d(T=M>>>16,c.of0);c.lhst[257+P]++;var F=d(L,c.df0);c.dhst[F]++,w+=c.exb[P]+c.dxb[F],y[x]=T<<23|f-m,y[x+1]=L<<16|P<<8|F,x+=2,m=f+T}else c.lhst[s[f]]++;E++}}for(S==f&&s.length!=0||(m<f&&(y[x]=f-m,x+=2,m=f),g=r.F._writeBlock(1,y,x,w,s,S,f-S,o,g),x=0,E=0,x=E=w=0,S=f);(7&g)!=0;)g++;return g>>>3},r.F._bestMatch=function(s,o,i,a,l,c){var d=32767&o,u=i[d],f=d-u+32768&32767;if(u==d||a!=r.F._hash(s,o-f))return 0;for(var g=0,m=0,p=Math.min(32767,o);f<=p&&--c!=0&&u!=d;){if(g==0||s[o+g]==s[o+g-f]){var y=r.F._howLong(s,o,f);if(y>g){if(m=f,(g=y)>=l)break;f+2<y&&(y=f+2);for(var b=0,_=0;_<y-2;_++){var x=o-f+_+32768&32767,E=x-i[x]+32768&32767;E>b&&(b=E,u=x)}}}f+=(d=u)-(u=i[d])+32768&32767}return g<<16|m},r.F._howLong=function(s,o,i){if(s[o]!=s[o-i]||s[o+1]!=s[o+1-i]||s[o+2]!=s[o+2-i])return 0;var a=o,l=Math.min(s.length,o+258);for(o+=3;o<l&&s[o]==s[o-i];)o++;return o-a},r.F._hash=function(s,o){return(s[o]<<8|s[o+1])+(s[o+2]<<4)&65535},r.saved=0,r.F._writeBlock=function(s,o,i,a,l,c,d,u,f){var g,m,p,y,b,_,x,E,S,w=r.F.U,N=r.F._putsF,C=r.F._putsE;w.lhst[256]++,m=(g=r.F.getTrees())[0],p=g[1],y=g[2],b=g[3],_=g[4],x=g[5],E=g[6],S=g[7];var k=32+((f+3&7)==0?0:8-(f+3&7))+(d<<3),M=a+r.F.contSize(w.fltree,w.lhst)+r.F.contSize(w.fdtree,w.dhst),T=a+r.F.contSize(w.ltree,w.lhst)+r.F.contSize(w.dtree,w.dhst);T+=14+3*x+r.F.contSize(w.itree,w.ihst)+(2*w.ihst[16]+3*w.ihst[17]+7*w.ihst[18]);for(var L=0;L<286;L++)w.lhst[L]=0;for(L=0;L<30;L++)w.dhst[L]=0;for(L=0;L<19;L++)w.ihst[L]=0;var P=k<M&&k<T?0:M<T?1:2;if(N(u,f,s),N(u,f+1,P),f+=3,P==0){for(;(7&f)!=0;)f++;f=r.F._copyExact(l,c,d,u,f)}else{var F,D;if(P==1&&(F=w.fltree,D=w.fdtree),P==2){r.F.makeCodes(w.ltree,m),r.F.revCodes(w.ltree,m),r.F.makeCodes(w.dtree,p),r.F.revCodes(w.dtree,p),r.F.makeCodes(w.itree,y),r.F.revCodes(w.itree,y),F=w.ltree,D=w.dtree,C(u,f,b-257),C(u,f+=5,_-1),C(u,f+=5,x-4),f+=4;for(var A=0;A<x;A++)C(u,f+3*A,w.itree[1+(w.ordr[A]<<1)]);f+=3*x,f=r.F._codeTiny(E,w.itree,u,f),f=r.F._codeTiny(S,w.itree,u,f)}for(var O=c,V=0;V<i;V+=2){for(var H=o[V],W=H>>>23,j=O+(8388607&H);O<j;)f=r.F._writeLit(l[O++],F,u,f);if(W!=0){var U=o[V+1],B=U>>16,G=U>>8&255,z=255&U;C(u,f=r.F._writeLit(257+G,F,u,f),W-w.of0[G]),f+=w.exb[G],N(u,f=r.F._writeLit(z,D,u,f),B-w.df0[z]),f+=w.dxb[z],O+=W}}f=r.F._writeLit(256,F,u,f)}return f},r.F._copyExact=function(s,o,i,a,l){var c=l>>>3;return a[c]=i,a[c+1]=i>>>8,a[c+2]=255-a[c],a[c+3]=255-a[c+1],c+=4,a.set(new Uint8Array(s.buffer,o,i),c),l+(i+4<<3)},r.F.getTrees=function(){for(var s=r.F.U,o=r.F._hufTree(s.lhst,s.ltree,15),i=r.F._hufTree(s.dhst,s.dtree,15),a=[],l=r.F._lenCodes(s.ltree,a),c=[],d=r.F._lenCodes(s.dtree,c),u=0;u<a.length;u+=2)s.ihst[a[u]]++;for(u=0;u<c.length;u+=2)s.ihst[c[u]]++;for(var f=r.F._hufTree(s.ihst,s.itree,7),g=19;g>4&&s.itree[1+(s.ordr[g-1]<<1)]==0;)g--;return[o,i,f,l,d,g,a,c]},r.F.getSecond=function(s){for(var o=[],i=0;i<s.length;i+=2)o.push(s[i+1]);return o},r.F.nonZero=function(s){for(var o="",i=0;i<s.length;i+=2)s[i+1]!=0&&(o+=(i>>1)+",");return o},r.F.contSize=function(s,o){for(var i=0,a=0;a<o.length;a++)i+=o[a]*s[1+(a<<1)];return i},r.F._codeTiny=function(s,o,i,a){for(var l=0;l<s.length;l+=2){var c=s[l],d=s[l+1];a=r.F._writeLit(c,o,i,a);var u=c==16?2:c==17?3:7;c>15&&(r.F._putsE(i,a,d,u),a+=u)}return a},r.F._lenCodes=function(s,o){for(var i=s.length;i!=2&&s[i-1]==0;)i-=2;for(var a=0;a<i;a+=2){var l=s[a+1],c=a+3<i?s[a+3]:-1,d=a+5<i?s[a+5]:-1,u=a==0?-1:s[a-1];if(l==0&&c==l&&d==l){for(var f=a+5;f+2<i&&s[f+2]==l;)f+=2;(g=Math.min(f+1-a>>>1,138))<11?o.push(17,g-3):o.push(18,g-11),a+=2*g-2}else if(l==u&&c==l&&d==l){for(f=a+5;f+2<i&&s[f+2]==l;)f+=2;var g=Math.min(f+1-a>>>1,6);o.push(16,g-3),a+=2*g-2}else o.push(l,0)}return i>>>1},r.F._hufTree=function(s,o,i){var a=[],l=s.length,c=o.length,d=0;for(d=0;d<c;d+=2)o[d]=0,o[d+1]=0;for(d=0;d<l;d++)s[d]!=0&&a.push({lit:d,f:s[d]});var u=a.length,f=a.slice(0);if(u==0)return 0;if(u==1){var g=a[0].lit;return f=g==0?1:0,o[1+(g<<1)]=1,o[1+(f<<1)]=1,1}a.sort((function(E,S){return E.f-S.f}));var m=a[0],p=a[1],y=0,b=1,_=2;for(a[0]={lit:-1,f:m.f+p.f,l:m,r:p,d:0};b!=u-1;)m=y!=b&&(_==u||a[y].f<a[_].f)?a[y++]:a[_++],p=y!=b&&(_==u||a[y].f<a[_].f)?a[y++]:a[_++],a[b++]={lit:-1,f:m.f+p.f,l:m,r:p};var x=r.F.setDepth(a[b-1],0);for(x>i&&(r.F.restrictDepth(f,i,x),x=i),d=0;d<u;d++)o[1+(f[d].lit<<1)]=f[d].d;return x},r.F.setDepth=function(s,o){return s.lit!=-1?(s.d=o,o):Math.max(r.F.setDepth(s.l,o+1),r.F.setDepth(s.r,o+1))},r.F.restrictDepth=function(s,o,i){var a=0,l=1<<i-o,c=0;for(s.sort((function(u,f){return f.d==u.d?u.f-f.f:f.d-u.d})),a=0;a<s.length&&s[a].d>o;a++){var d=s[a].d;s[a].d=o,c+=l-(1<<i-d)}for(c>>>=i-o;c>0;)(d=s[a].d)<o?(s[a].d++,c-=1<<o-d-1):a++;for(;a>=0;a--)s[a].d==o&&c<0&&(s[a].d--,c++);c!=0&&console.log("debt left")},r.F._goodIndex=function(s,o){var i=0;return o[16|i]<=s&&(i|=16),o[8|i]<=s&&(i|=8),o[4|i]<=s&&(i|=4),o[2|i]<=s&&(i|=2),o[1|i]<=s&&(i|=1),i},r.F._writeLit=function(s,o,i,a){return r.F._putsF(i,a,o[s<<1]),a+o[1+(s<<1)]},r.F.inflate=function(s,o){var i=Uint8Array;if(s[0]==3&&s[1]==0)return o||new i(0);var a=r.F,l=a._bitsF,c=a._bitsE,d=a._decodeTiny,u=a.makeCodes,f=a.codes2map,g=a._get17,m=a.U,p=o==null;p&&(o=new i(s.length>>>2<<3));for(var y,b,_=0,x=0,E=0,S=0,w=0,N=0,C=0,k=0,M=0;_==0;)if(_=l(s,M,1),x=l(s,M+1,2),M+=3,x!=0){if(p&&(o=r.F._check(o,k+131072)),x==1&&(y=m.flmap,b=m.fdmap,N=511,C=31),x==2){E=c(s,M,5)+257,S=c(s,M+5,5)+1,w=c(s,M+10,4)+4,M+=14;for(var T=0;T<38;T+=2)m.itree[T]=0,m.itree[T+1]=0;var L=1;for(T=0;T<w;T++){var P=c(s,M+3*T,3);m.itree[1+(m.ordr[T]<<1)]=P,P>L&&(L=P)}M+=3*w,u(m.itree,L),f(m.itree,L,m.imap),y=m.lmap,b=m.dmap,M=d(m.imap,(1<<L)-1,E+S,s,M,m.ttree);var F=a._copyOut(m.ttree,0,E,m.ltree);N=(1<<F)-1;var D=a._copyOut(m.ttree,E,S,m.dtree);C=(1<<D)-1,u(m.ltree,F),f(m.ltree,F,y),u(m.dtree,D),f(m.dtree,D,b)}for(;;){var A=y[g(s,M)&N];M+=15&A;var O=A>>>4;if(!(O>>>8))o[k++]=O;else{if(O==256)break;var V=k+O-254;if(O>264){var H=m.ldef[O-257];V=k+(H>>>3)+c(s,M,7&H),M+=7&H}var W=b[g(s,M)&C];M+=15&W;var j=W>>>4,U=m.ddef[j],B=(U>>>4)+l(s,M,15&U);for(M+=15&U,p&&(o=r.F._check(o,k+131072));k<V;)o[k]=o[k++-B],o[k]=o[k++-B],o[k]=o[k++-B],o[k]=o[k++-B];k=V}}}else{(7&M)!=0&&(M+=8-(7&M));var G=4+(M>>>3),z=s[G-4]|s[G-3]<<8;p&&(o=r.F._check(o,k+z)),o.set(new i(s.buffer,s.byteOffset+G,z),k),M=G+z<<3,k+=z}return o.length==k?o:o.slice(0,k)},r.F._check=function(s,o){var i=s.length;if(o<=i)return s;var a=new Uint8Array(Math.max(i<<1,o));return a.set(s,0),a},r.F._decodeTiny=function(s,o,i,a,l,c){for(var d=r.F._bitsE,u=r.F._get17,f=0;f<i;){var g=s[u(a,l)&o];l+=15&g;var m=g>>>4;if(m<=15)c[f]=m,f++;else{var p=0,y=0;m==16?(y=3+d(a,l,2),l+=2,p=c[f-1]):m==17?(y=3+d(a,l,3),l+=3):m==18&&(y=11+d(a,l,7),l+=7);for(var b=f+y;f<b;)c[f]=p,f++}}return l},r.F._copyOut=function(s,o,i,a){for(var l=0,c=0,d=a.length>>>1;c<i;){var u=s[c+o];a[c<<1]=0,a[1+(c<<1)]=u,u>l&&(l=u),c++}for(;c<d;)a[c<<1]=0,a[1+(c<<1)]=0,c++;return l},r.F.makeCodes=function(s,o){for(var i,a,l,c,d=r.F.U,u=s.length,f=d.bl_count,g=0;g<=o;g++)f[g]=0;for(g=1;g<u;g+=2)f[s[g]]++;var m=d.next_code;for(i=0,f[0]=0,a=1;a<=o;a++)i=i+f[a-1]<<1,m[a]=i;for(l=0;l<u;l+=2)(c=s[l+1])!=0&&(s[l]=m[c],m[c]++)},r.F.codes2map=function(s,o,i){for(var a=s.length,l=r.F.U.rev15,c=0;c<a;c+=2)if(s[c+1]!=0)for(var d=c>>1,u=s[c+1],f=d<<4|u,g=o-u,m=s[c]<<g,p=m+(1<<g);m!=p;)i[l[m]>>>15-o]=f,m++},r.F.revCodes=function(s,o){for(var i=r.F.U.rev15,a=15-o,l=0;l<s.length;l+=2){var c=s[l]<<o-s[l+1];s[l]=i[c]>>>a}},r.F._putsE=function(s,o,i){i<<=7&o;var a=o>>>3;s[a]|=i,s[a+1]|=i>>>8},r.F._putsF=function(s,o,i){i<<=7&o;var a=o>>>3;s[a]|=i,s[a+1]|=i>>>8,s[a+2]|=i>>>16},r.F._bitsE=function(s,o,i){return(s[o>>>3]|s[1+(o>>>3)]<<8)>>>(7&o)&(1<<i)-1},r.F._bitsF=function(s,o,i){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16)>>>(7&o)&(1<<i)-1},r.F._get17=function(s,o){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16)>>>(7&o)},r.F._get25=function(s,o){return(s[o>>>3]|s[1+(o>>>3)]<<8|s[2+(o>>>3)]<<16|s[3+(o>>>3)]<<24)>>>(7&o)},r.F.U=(e=Uint16Array,n=Uint32Array,{next_code:new e(16),bl_count:new e(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new e(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new n(32),flmap:new e(512),fltree:[],fdmap:new e(32),fdtree:[],lmap:new e(32768),ltree:[],ttree:[],dmap:new e(32768),dtree:[],imap:new e(512),itree:[],rev15:new e(32768),lhst:new n(286),dhst:new n(30),ihst:new n(19),lits:new n(15e3),strt:new e(65536),prev:new e(32768)}),(function(){for(var s=r.F.U,o=0;o<32768;o++){var i=o;i=(4278255360&(i=(4042322160&(i=(3435973836&(i=(2863311530&i)>>>1|(1431655765&i)<<1))>>>2|(858993459&i)<<2))>>>4|(252645135&i)<<4))>>>8|(16711935&i)<<8,s.rev15[o]=(i>>>16|i<<16)>>>17}function a(l,c,d){for(;c--!=0;)l.push(0,d)}for(o=0;o<32;o++)s.ldef[o]=s.of0[o]<<3|s.exb[o],s.ddef[o]=s.df0[o]<<4|s.dxb[o];a(s.fltree,144,8),a(s.fltree,112,9),a(s.fltree,24,7),a(s.fltree,8,8),r.F.makeCodes(s.fltree,9),r.F.codes2map(s.fltree,9,s.flmap),r.F.revCodes(s.fltree,9),a(s.fdtree,32,5),r.F.makeCodes(s.fdtree,5),r.F.codes2map(s.fdtree,5,s.fdmap),r.F.revCodes(s.fdtree,5),a(s.itree,19,0),a(s.ltree,286,0),a(s.dtree,30,0),a(s.ttree,320,0)})()})();var eE=Q2({__proto__:null,default:Wi},[Wi]);const Tn=(function(){var t={nextZero(d,u){for(;d[u]!=0;)u++;return u},readUshort:(d,u)=>d[u]<<8|d[u+1],writeUshort(d,u,f){d[u]=f>>8&255,d[u+1]=255&f},readUint:(d,u)=>16777216*d[u]+(d[u+1]<<16|d[u+2]<<8|d[u+3]),writeUint(d,u,f){d[u]=f>>24&255,d[u+1]=f>>16&255,d[u+2]=f>>8&255,d[u+3]=255&f},readASCII(d,u,f){let g="";for(let m=0;m<f;m++)g+=String.fromCharCode(d[u+m]);return g},writeASCII(d,u,f){for(let g=0;g<f.length;g++)d[u+g]=f.charCodeAt(g)},readBytes(d,u,f){const g=[];for(let m=0;m<f;m++)g.push(d[u+m]);return g},pad:d=>d.length<2?`0${d}`:d,readUTF8(d,u,f){let g,m="";for(let p=0;p<f;p++)m+=`%${t.pad(d[u+p].toString(16))}`;try{g=decodeURIComponent(m)}catch{return t.readASCII(d,u,f)}return g}};function e(d,u,f,g){const m=u*f,p=o(g),y=Math.ceil(u*p/8),b=new Uint8Array(4*m),_=new Uint32Array(b.buffer),{ctype:x}=g,{depth:E}=g,S=t.readUshort;if(x==6){const H=m<<2;if(E==8)for(var w=0;w<H;w+=4)b[w]=d[w],b[w+1]=d[w+1],b[w+2]=d[w+2],b[w+3]=d[w+3];if(E==16)for(w=0;w<H;w++)b[w]=d[w<<1]}else if(x==2){const H=g.tabs.tRNS;if(H==null){if(E==8)for(w=0;w<m;w++){var N=3*w;_[w]=255<<24|d[N+2]<<16|d[N+1]<<8|d[N]}if(E==16)for(w=0;w<m;w++)N=6*w,_[w]=255<<24|d[N+4]<<16|d[N+2]<<8|d[N]}else{var C=H[0];const W=H[1],j=H[2];if(E==8)for(w=0;w<m;w++){var k=w<<2;N=3*w,_[w]=255<<24|d[N+2]<<16|d[N+1]<<8|d[N],d[N]==C&&d[N+1]==W&&d[N+2]==j&&(b[k+3]=0)}if(E==16)for(w=0;w<m;w++)k=w<<2,N=6*w,_[w]=255<<24|d[N+4]<<16|d[N+2]<<8|d[N],S(d,N)==C&&S(d,N+2)==W&&S(d,N+4)==j&&(b[k+3]=0)}}else if(x==3){const H=g.tabs.PLTE,W=g.tabs.tRNS,j=W?W.length:0;if(E==1)for(var M=0;M<f;M++){var T=M*y,L=M*u;for(w=0;w<u;w++){k=L+w<<2;var P=3*(F=d[T+(w>>3)]>>7-((7&w)<<0)&1);b[k]=H[P],b[k+1]=H[P+1],b[k+2]=H[P+2],b[k+3]=F<j?W[F]:255}}if(E==2)for(M=0;M<f;M++)for(T=M*y,L=M*u,w=0;w<u;w++)k=L+w<<2,P=3*(F=d[T+(w>>2)]>>6-((3&w)<<1)&3),b[k]=H[P],b[k+1]=H[P+1],b[k+2]=H[P+2],b[k+3]=F<j?W[F]:255;if(E==4)for(M=0;M<f;M++)for(T=M*y,L=M*u,w=0;w<u;w++)k=L+w<<2,P=3*(F=d[T+(w>>1)]>>4-((1&w)<<2)&15),b[k]=H[P],b[k+1]=H[P+1],b[k+2]=H[P+2],b[k+3]=F<j?W[F]:255;if(E==8)for(w=0;w<m;w++){var F;k=w<<2,P=3*(F=d[w]),b[k]=H[P],b[k+1]=H[P+1],b[k+2]=H[P+2],b[k+3]=F<j?W[F]:255}}else if(x==4){if(E==8)for(w=0;w<m;w++){k=w<<2;var D=d[A=w<<1];b[k]=D,b[k+1]=D,b[k+2]=D,b[k+3]=d[A+1]}if(E==16)for(w=0;w<m;w++){var A;k=w<<2,D=d[A=w<<2],b[k]=D,b[k+1]=D,b[k+2]=D,b[k+3]=d[A+2]}}else if(x==0)for(C=g.tabs.tRNS?g.tabs.tRNS:-1,M=0;M<f;M++){const H=M*y,W=M*u;if(E==1)for(var O=0;O<u;O++){var V=(D=255*(d[H+(O>>>3)]>>>7-(7&O)&1))==255*C?0:255;_[W+O]=V<<24|D<<16|D<<8|D}else if(E==2)for(O=0;O<u;O++)V=(D=85*(d[H+(O>>>2)]>>>6-((3&O)<<1)&3))==85*C?0:255,_[W+O]=V<<24|D<<16|D<<8|D;else if(E==4)for(O=0;O<u;O++)V=(D=17*(d[H+(O>>>1)]>>>4-((1&O)<<2)&15))==17*C?0:255,_[W+O]=V<<24|D<<16|D<<8|D;else if(E==8)for(O=0;O<u;O++)V=(D=d[H+O])==C?0:255,_[W+O]=V<<24|D<<16|D<<8|D;else if(E==16)for(O=0;O<u;O++)D=d[H+(O<<1)],V=S(d,H+(O<<1))==C?0:255,_[W+O]=V<<24|D<<16|D<<8|D}return b}function n(d,u,f,g){const m=o(d),p=Math.ceil(f*m/8),y=new Uint8Array((p+1+d.interlace)*g);return u=d.tabs.CgBI?s(u,y):r(u,y),d.interlace==0?u=i(u,d,0,f,g):d.interlace==1&&(u=(function(_,x){const E=x.width,S=x.height,w=o(x),N=w>>3,C=Math.ceil(E*w/8),k=new Uint8Array(S*C);let M=0;const T=[0,0,4,0,2,0,1],L=[0,4,0,2,0,1,0],P=[8,8,8,4,4,2,2],F=[8,8,4,4,2,2,1];let D=0;for(;D<7;){const O=P[D],V=F[D];let H=0,W=0,j=T[D];for(;j<S;)j+=O,W++;let U=L[D];for(;U<E;)U+=V,H++;const B=Math.ceil(H*w/8);i(_,x,M,H,W);let G=0,z=T[D];for(;z<S;){let X=L[D],Y=M+G*B<<3;for(;X<E;){var A;if(w==1&&(A=(A=_[Y>>3])>>7-(7&Y)&1,k[z*C+(X>>3)]|=A<<7-((7&X)<<0)),w==2&&(A=(A=_[Y>>3])>>6-(7&Y)&3,k[z*C+(X>>2)]|=A<<6-((3&X)<<1)),w==4&&(A=(A=_[Y>>3])>>4-(7&Y)&15,k[z*C+(X>>1)]|=A<<4-((1&X)<<2)),w>=8){const Z=z*C+X*N;for(let ee=0;ee<N;ee++)k[Z+ee]=_[(Y>>3)+ee]}Y+=w,X+=V}G++,z+=O}H*W!=0&&(M+=W*(1+B)),D+=1}return k})(u,d)),u}function r(d,u){return s(new Uint8Array(d.buffer,2,d.length-6),u)}var s=(function(){const d={H:{}};return d.H.N=function(u,f){const g=Uint8Array;let m,p,y=0,b=0,_=0,x=0,E=0,S=0,w=0,N=0,C=0;if(u[0]==3&&u[1]==0)return f||new g(0);const k=d.H,M=k.b,T=k.e,L=k.R,P=k.n,F=k.A,D=k.Z,A=k.m,O=f==null;for(O&&(f=new g(u.length>>>2<<5));y==0;)if(y=M(u,C,1),b=M(u,C+1,2),C+=3,b!=0){if(O&&(f=d.H.W(f,N+(1<<17))),b==1&&(m=A.J,p=A.h,S=511,w=31),b==2){_=T(u,C,5)+257,x=T(u,C+5,5)+1,E=T(u,C+10,4)+4,C+=14;let H=1;for(var V=0;V<38;V+=2)A.Q[V]=0,A.Q[V+1]=0;for(V=0;V<E;V++){const U=T(u,C+3*V,3);A.Q[1+(A.X[V]<<1)]=U,U>H&&(H=U)}C+=3*E,P(A.Q,H),F(A.Q,H,A.u),m=A.w,p=A.d,C=L(A.u,(1<<H)-1,_+x,u,C,A.v);const W=k.V(A.v,0,_,A.C);S=(1<<W)-1;const j=k.V(A.v,_,x,A.D);w=(1<<j)-1,P(A.C,W),F(A.C,W,m),P(A.D,j),F(A.D,j,p)}for(;;){const H=m[D(u,C)&S];C+=15&H;const W=H>>>4;if(!(W>>>8))f[N++]=W;else{if(W==256)break;{let j=N+W-254;if(W>264){const X=A.q[W-257];j=N+(X>>>3)+T(u,C,7&X),C+=7&X}const U=p[D(u,C)&w];C+=15&U;const B=U>>>4,G=A.c[B],z=(G>>>4)+M(u,C,15&G);for(C+=15&G;N<j;)f[N]=f[N++-z],f[N]=f[N++-z],f[N]=f[N++-z],f[N]=f[N++-z];N=j}}}}else{(7&C)!=0&&(C+=8-(7&C));const H=4+(C>>>3),W=u[H-4]|u[H-3]<<8;O&&(f=d.H.W(f,N+W)),f.set(new g(u.buffer,u.byteOffset+H,W),N),C=H+W<<3,N+=W}return f.length==N?f:f.slice(0,N)},d.H.W=function(u,f){const g=u.length;if(f<=g)return u;const m=new Uint8Array(g<<1);return m.set(u,0),m},d.H.R=function(u,f,g,m,p,y){const b=d.H.e,_=d.H.Z;let x=0;for(;x<g;){const E=u[_(m,p)&f];p+=15&E;const S=E>>>4;if(S<=15)y[x]=S,x++;else{let w=0,N=0;S==16?(N=3+b(m,p,2),p+=2,w=y[x-1]):S==17?(N=3+b(m,p,3),p+=3):S==18&&(N=11+b(m,p,7),p+=7);const C=x+N;for(;x<C;)y[x]=w,x++}}return p},d.H.V=function(u,f,g,m){let p=0,y=0;const b=m.length>>>1;for(;y<g;){const _=u[y+f];m[y<<1]=0,m[1+(y<<1)]=_,_>p&&(p=_),y++}for(;y<b;)m[y<<1]=0,m[1+(y<<1)]=0,y++;return p},d.H.n=function(u,f){const g=d.H.m,m=u.length;let p,y,b,_;const x=g.j;for(var E=0;E<=f;E++)x[E]=0;for(E=1;E<m;E+=2)x[u[E]]++;const S=g.K;for(p=0,x[0]=0,y=1;y<=f;y++)p=p+x[y-1]<<1,S[y]=p;for(b=0;b<m;b+=2)_=u[b+1],_!=0&&(u[b]=S[_],S[_]++)},d.H.A=function(u,f,g){const m=u.length,p=d.H.m.r;for(let y=0;y<m;y+=2)if(u[y+1]!=0){const b=y>>1,_=u[y+1],x=b<<4|_,E=f-_;let S=u[y]<<E;const w=S+(1<<E);for(;S!=w;)g[p[S]>>>15-f]=x,S++}},d.H.l=function(u,f){const g=d.H.m.r,m=15-f;for(let p=0;p<u.length;p+=2){const y=u[p]<<f-u[p+1];u[p]=g[y]>>>m}},d.H.M=function(u,f,g){g<<=7&f;const m=f>>>3;u[m]|=g,u[m+1]|=g>>>8},d.H.I=function(u,f,g){g<<=7&f;const m=f>>>3;u[m]|=g,u[m+1]|=g>>>8,u[m+2]|=g>>>16},d.H.e=function(u,f,g){return(u[f>>>3]|u[1+(f>>>3)]<<8)>>>(7&f)&(1<<g)-1},d.H.b=function(u,f,g){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16)>>>(7&f)&(1<<g)-1},d.H.Z=function(u,f){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16)>>>(7&f)},d.H.i=function(u,f){return(u[f>>>3]|u[1+(f>>>3)]<<8|u[2+(f>>>3)]<<16|u[3+(f>>>3)]<<24)>>>(7&f)},d.H.m=(function(){const u=Uint16Array,f=Uint32Array;return{K:new u(16),j:new u(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new u(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new f(32),J:new u(512),_:[],h:new u(32),$:[],w:new u(32768),C:[],v:[],d:new u(32768),D:[],u:new u(512),Q:[],r:new u(32768),s:new f(286),Y:new f(30),a:new f(19),t:new f(15e3),k:new u(65536),g:new u(32768)}})(),(function(){const u=d.H.m;for(var f=0;f<32768;f++){let m=f;m=(2863311530&m)>>>1|(1431655765&m)<<1,m=(3435973836&m)>>>2|(858993459&m)<<2,m=(4042322160&m)>>>4|(252645135&m)<<4,m=(4278255360&m)>>>8|(16711935&m)<<8,u.r[f]=(m>>>16|m<<16)>>>17}function g(m,p,y){for(;p--!=0;)m.push(0,y)}for(f=0;f<32;f++)u.q[f]=u.S[f]<<3|u.T[f],u.c[f]=u.p[f]<<4|u.z[f];g(u._,144,8),g(u._,112,9),g(u._,24,7),g(u._,8,8),d.H.n(u._,9),d.H.A(u._,9,u.J),d.H.l(u._,9),g(u.$,32,5),d.H.n(u.$,5),d.H.A(u.$,5,u.h),d.H.l(u.$,5),g(u.Q,19,0),g(u.C,286,0),g(u.D,30,0),g(u.v,320,0)})(),d.H.N})();function o(d){return[1,null,3,1,2,null,4][d.ctype]*d.depth}function i(d,u,f,g,m){let p=o(u);const y=Math.ceil(g*p/8);let b,_;p=Math.ceil(p/8);let x=d[f],E=0;if(x>1&&(d[f]=[0,0,1][x-2]),x==3)for(E=p;E<y;E++)d[E+1]=d[E+1]+(d[E+1-p]>>>1)&255;for(let S=0;S<m;S++)if(b=f+S*y,_=b+S+1,x=d[_-1],E=0,x==0)for(;E<y;E++)d[b+E]=d[_+E];else if(x==1){for(;E<p;E++)d[b+E]=d[_+E];for(;E<y;E++)d[b+E]=d[_+E]+d[b+E-p]}else if(x==2)for(;E<y;E++)d[b+E]=d[_+E]+d[b+E-y];else if(x==3){for(;E<p;E++)d[b+E]=d[_+E]+(d[b+E-y]>>>1);for(;E<y;E++)d[b+E]=d[_+E]+(d[b+E-y]+d[b+E-p]>>>1)}else{for(;E<p;E++)d[b+E]=d[_+E]+a(0,d[b+E-y],0);for(;E<y;E++)d[b+E]=d[_+E]+a(d[b+E-p],d[b+E-y],d[b+E-p-y])}return d}function a(d,u,f){const g=d+u-f,m=g-d,p=g-u,y=g-f;return m*m<=p*p&&m*m<=y*y?d:p*p<=y*y?u:f}function l(d,u,f){f.width=t.readUint(d,u),u+=4,f.height=t.readUint(d,u),u+=4,f.depth=d[u],u++,f.ctype=d[u],u++,f.compress=d[u],u++,f.filter=d[u],u++,f.interlace=d[u],u++}function c(d,u,f,g,m,p,y,b,_){const x=Math.min(u,m),E=Math.min(f,p);let S=0,w=0;for(let D=0;D<E;D++)for(let A=0;A<x;A++)if(y>=0&&b>=0?(S=D*u+A<<2,w=(b+D)*m+y+A<<2):(S=(-b+D)*u-y+A<<2,w=D*m+A<<2),_==0)g[w]=d[S],g[w+1]=d[S+1],g[w+2]=d[S+2],g[w+3]=d[S+3];else if(_==1){var N=d[S+3]*.00392156862745098,C=d[S]*N,k=d[S+1]*N,M=d[S+2]*N,T=g[w+3]*(1/255),L=g[w]*T,P=g[w+1]*T,F=g[w+2]*T;const O=1-N,V=N+T*O,H=V==0?0:1/V;g[w+3]=255*V,g[w+0]=(C+L*O)*H,g[w+1]=(k+P*O)*H,g[w+2]=(M+F*O)*H}else if(_==2)N=d[S+3],C=d[S],k=d[S+1],M=d[S+2],T=g[w+3],L=g[w],P=g[w+1],F=g[w+2],N==T&&C==L&&k==P&&M==F?(g[w]=0,g[w+1]=0,g[w+2]=0,g[w+3]=0):(g[w]=C,g[w+1]=k,g[w+2]=M,g[w+3]=N);else if(_==3){if(N=d[S+3],C=d[S],k=d[S+1],M=d[S+2],T=g[w+3],L=g[w],P=g[w+1],F=g[w+2],N==T&&C==L&&k==P&&M==F)continue;if(N<220&&T>20)return!1}return!0}return{decode:function(u){const f=new Uint8Array(u);let g=8;const m=t,p=m.readUshort,y=m.readUint,b={tabs:{},frames:[]},_=new Uint8Array(f.length);let x,E=0,S=0;const w=[137,80,78,71,13,10,26,10];for(var N=0;N<8;N++)if(f[N]!=w[N])throw"The input is not a PNG file!";for(;g<f.length;){const D=m.readUint(f,g);g+=4;const A=m.readASCII(f,g,4);if(g+=4,A=="IHDR")l(f,g,b);else if(A=="iCCP"){for(var C=g;f[C]!=0;)C++;m.readASCII(f,g,C-g),f[C+1];const O=f.slice(C+2,g+D);let V=null;try{V=r(O)}catch{V=s(O)}b.tabs[A]=V}else if(A=="CgBI")b.tabs[A]=f.slice(g,g+4);else if(A=="IDAT"){for(N=0;N<D;N++)_[E+N]=f[g+N];E+=D}else if(A=="acTL")b.tabs[A]={num_frames:y(f,g),num_plays:y(f,g+4)},x=new Uint8Array(f.length);else if(A=="fcTL"){S!=0&&((F=b.frames[b.frames.length-1]).data=n(b,x.slice(0,S),F.rect.width,F.rect.height),S=0);const O={x:y(f,g+12),y:y(f,g+16),width:y(f,g+4),height:y(f,g+8)};let V=p(f,g+22);V=p(f,g+20)/(V==0?100:V);const H={rect:O,delay:Math.round(1e3*V),dispose:f[g+24],blend:f[g+25]};b.frames.push(H)}else if(A=="fdAT"){for(N=0;N<D-4;N++)x[S+N]=f[g+N+4];S+=D-4}else if(A=="pHYs")b.tabs[A]=[m.readUint(f,g),m.readUint(f,g+4),f[g+8]];else if(A=="cHRM")for(b.tabs[A]=[],N=0;N<8;N++)b.tabs[A].push(m.readUint(f,g+4*N));else if(A=="tEXt"||A=="zTXt"){b.tabs[A]==null&&(b.tabs[A]={});var k=m.nextZero(f,g),M=m.readASCII(f,g,k-g),T=g+D-k-1;if(A=="tEXt")P=m.readASCII(f,k+1,T);else{var L=r(f.slice(k+2,k+2+T));P=m.readUTF8(L,0,L.length)}b.tabs[A][M]=P}else if(A=="iTXt"){b.tabs[A]==null&&(b.tabs[A]={}),k=0,C=g,k=m.nextZero(f,C),M=m.readASCII(f,C,k-C);const O=f[C=k+1];var P;f[C+1],C+=2,k=m.nextZero(f,C),m.readASCII(f,C,k-C),C=k+1,k=m.nextZero(f,C),m.readUTF8(f,C,k-C),T=D-((C=k+1)-g),O==0?P=m.readUTF8(f,C,T):(L=r(f.slice(C,C+T)),P=m.readUTF8(L,0,L.length)),b.tabs[A][M]=P}else if(A=="PLTE")b.tabs[A]=m.readBytes(f,g,D);else if(A=="hIST"){const O=b.tabs.PLTE.length/3;for(b.tabs[A]=[],N=0;N<O;N++)b.tabs[A].push(p(f,g+2*N))}else if(A=="tRNS")b.ctype==3?b.tabs[A]=m.readBytes(f,g,D):b.ctype==0?b.tabs[A]=p(f,g):b.ctype==2&&(b.tabs[A]=[p(f,g),p(f,g+2),p(f,g+4)]);else if(A=="gAMA")b.tabs[A]=m.readUint(f,g)/1e5;else if(A=="sRGB")b.tabs[A]=f[g];else if(A=="bKGD")b.ctype==0||b.ctype==4?b.tabs[A]=[p(f,g)]:b.ctype==2||b.ctype==6?b.tabs[A]=[p(f,g),p(f,g+2),p(f,g+4)]:b.ctype==3&&(b.tabs[A]=f[g]);else if(A=="IEND")break;g+=D,m.readUint(f,g),g+=4}var F;return S!=0&&((F=b.frames[b.frames.length-1]).data=n(b,x.slice(0,S),F.rect.width,F.rect.height)),b.data=n(b,_,b.width,b.height),delete b.compress,delete b.interlace,delete b.filter,b},toRGBA8:function(u){const f=u.width,g=u.height;if(u.tabs.acTL==null)return[e(u.data,f,g,u).buffer];const m=[];u.frames[0].data==null&&(u.frames[0].data=u.data);const p=f*g*4,y=new Uint8Array(p),b=new Uint8Array(p),_=new Uint8Array(p);for(let E=0;E<u.frames.length;E++){const S=u.frames[E],w=S.rect.x,N=S.rect.y,C=S.rect.width,k=S.rect.height,M=e(S.data,C,k,u);if(E!=0)for(var x=0;x<p;x++)_[x]=y[x];if(S.blend==0?c(M,C,k,y,f,g,w,N,0):S.blend==1&&c(M,C,k,y,f,g,w,N,1),m.push(y.buffer.slice(0)),S.dispose!=0){if(S.dispose==1)c(b,C,k,y,f,g,w,N,0);else if(S.dispose==2)for(x=0;x<p;x++)y[x]=_[x]}}return m},_paeth:a,_copyTile:c,_bin:t}})();(function(){const{_copyTile:t}=Tn,{_bin:e}=Tn,n=Tn._paeth;var r={table:(function(){const C=new Uint32Array(256);for(let k=0;k<256;k++){let M=k;for(let T=0;T<8;T++)1&M?M=3988292384^M>>>1:M>>>=1;C[k]=M}return C})(),update(C,k,M,T){for(let L=0;L<T;L++)C=r.table[255&(C^k[M+L])]^C>>>8;return C},crc:(C,k,M)=>4294967295^r.update(4294967295,C,k,M)};function s(C,k,M,T){k[M]+=C[0]*T>>4,k[M+1]+=C[1]*T>>4,k[M+2]+=C[2]*T>>4,k[M+3]+=C[3]*T>>4}function o(C){return Math.max(0,Math.min(255,C))}function i(C,k){const M=C[0]-k[0],T=C[1]-k[1],L=C[2]-k[2],P=C[3]-k[3];return M*M+T*T+L*L+P*P}function a(C,k,M,T,L,P,F){F==null&&(F=1);const D=T.length,A=[];for(var O=0;O<D;O++){const z=T[O];A.push([z>>>0&255,z>>>8&255,z>>>16&255,z>>>24&255])}for(O=0;O<D;O++){let z=4294967295;for(var V=0,H=0;H<D;H++){var W=i(A[O],A[H]);H!=O&&W<z&&(z=W,V=H)}}const j=new Uint32Array(L.buffer),U=new Int16Array(k*M*4),B=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5];for(O=0;O<B.length;O++)B[O]=255*((B[O]+.5)/16-.5);for(let z=0;z<M;z++)for(let X=0;X<k;X++){var G;O=4*(z*k+X),F!=2?G=[o(C[O]+U[O]),o(C[O+1]+U[O+1]),o(C[O+2]+U[O+2]),o(C[O+3]+U[O+3])]:(W=B[4*(3&z)+(3&X)],G=[o(C[O]+W),o(C[O+1]+W),o(C[O+2]+W),o(C[O+3]+W)]),V=0;let Y=16777215;for(H=0;H<D;H++){const te=i(G,A[H]);te<Y&&(Y=te,V=H)}const Z=A[V],ee=[G[0]-Z[0],G[1]-Z[1],G[2]-Z[2],G[3]-Z[3]];F==1&&(X!=k-1&&s(ee,U,O+4,7),z!=M-1&&(X!=0&&s(ee,U,O+4*k-4,3),s(ee,U,O+4*k,5),X!=k-1&&s(ee,U,O+4*k+4,1))),P[O>>2]=V,j[O>>2]=T[V]}}function l(C,k,M,T,L){L==null&&(L={});const{crc:P}=r,F=e.writeUint,D=e.writeUshort,A=e.writeASCII;let O=8;const V=C.frames.length>1;let H,W=!1,j=33+(V?20:0);if(L.sRGB!=null&&(j+=13),L.pHYs!=null&&(j+=21),L.iCCP!=null&&(H=pako.deflate(L.iCCP),j+=21+H.length+4),C.ctype==3){for(var U=C.plte.length,B=0;B<U;B++)C.plte[B]>>>24!=255&&(W=!0);j+=8+3*U+4+(W?8+1*U+4:0)}for(var G=0;G<C.frames.length;G++)V&&(j+=38),j+=(Z=C.frames[G]).cimg.length+12,G!=0&&(j+=4);j+=12;const z=new Uint8Array(j),X=[137,80,78,71,13,10,26,10];for(B=0;B<8;B++)z[B]=X[B];if(F(z,O,13),O+=4,A(z,O,"IHDR"),O+=4,F(z,O,k),O+=4,F(z,O,M),O+=4,z[O]=C.depth,O++,z[O]=C.ctype,O++,z[O]=0,O++,z[O]=0,O++,z[O]=0,O++,F(z,O,P(z,O-17,17)),O+=4,L.sRGB!=null&&(F(z,O,1),O+=4,A(z,O,"sRGB"),O+=4,z[O]=L.sRGB,O++,F(z,O,P(z,O-5,5)),O+=4),L.iCCP!=null){const ee=13+H.length;F(z,O,ee),O+=4,A(z,O,"iCCP"),O+=4,A(z,O,"ICC profile"),O+=11,O+=2,z.set(H,O),O+=H.length,F(z,O,P(z,O-(ee+4),ee+4)),O+=4}if(L.pHYs!=null&&(F(z,O,9),O+=4,A(z,O,"pHYs"),O+=4,F(z,O,L.pHYs[0]),O+=4,F(z,O,L.pHYs[1]),O+=4,z[O]=L.pHYs[2],O++,F(z,O,P(z,O-13,13)),O+=4),V&&(F(z,O,8),O+=4,A(z,O,"acTL"),O+=4,F(z,O,C.frames.length),O+=4,F(z,O,L.loop!=null?L.loop:0),O+=4,F(z,O,P(z,O-12,12)),O+=4),C.ctype==3){for(F(z,O,3*(U=C.plte.length)),O+=4,A(z,O,"PLTE"),O+=4,B=0;B<U;B++){const ee=3*B,te=C.plte[B],ce=255&te,J=te>>>8&255,he=te>>>16&255;z[O+ee+0]=ce,z[O+ee+1]=J,z[O+ee+2]=he}if(O+=3*U,F(z,O,P(z,O-3*U-4,3*U+4)),O+=4,W){for(F(z,O,U),O+=4,A(z,O,"tRNS"),O+=4,B=0;B<U;B++)z[O+B]=C.plte[B]>>>24&255;O+=U,F(z,O,P(z,O-U-4,U+4)),O+=4}}let Y=0;for(G=0;G<C.frames.length;G++){var Z=C.frames[G];V&&(F(z,O,26),O+=4,A(z,O,"fcTL"),O+=4,F(z,O,Y++),O+=4,F(z,O,Z.rect.width),O+=4,F(z,O,Z.rect.height),O+=4,F(z,O,Z.rect.x),O+=4,F(z,O,Z.rect.y),O+=4,D(z,O,T[G]),O+=2,D(z,O,1e3),O+=2,z[O]=Z.dispose,O++,z[O]=Z.blend,O++,F(z,O,P(z,O-30,30)),O+=4);const ee=Z.cimg;F(z,O,(U=ee.length)+(G==0?0:4)),O+=4;const te=O;A(z,O,G==0?"IDAT":"fdAT"),O+=4,G!=0&&(F(z,O,Y++),O+=4),z.set(ee,O),O+=U,F(z,O,P(z,te,O-te)),O+=4}return F(z,O,0),O+=4,A(z,O,"IEND"),O+=4,F(z,O,P(z,O-4,4)),O+=4,z.buffer}function c(C,k,M){for(let T=0;T<C.frames.length;T++){const L=C.frames[T];L.rect.width;const P=L.rect.height,F=new Uint8Array(P*L.bpl+P);L.cimg=g(L.img,P,L.bpp,L.bpl,F,k,M)}}function d(C,k,M,T,L){const P=L[0],F=L[1],D=L[2],A=L[3],O=L[4],V=L[5];let H=6,W=8,j=255;for(var U=0;U<C.length;U++){const Se=new Uint8Array(C[U]);for(var B=Se.length,G=0;G<B;G+=4)j&=Se[G+3]}const z=j!=255,X=(function(ue,pe,we,He,se,Oe){const Le=[];for(var Ie=0;Ie<ue.length;Ie++){const Be=new Uint8Array(ue[Ie]),ne=new Uint32Array(Be.buffer);var Ue;let fe=0,xe=0,ge=pe,be=we,Ve=He?1:0;if(Ie!=0){const De=Oe||He||Ie==1||Le[Ie-2].dispose!=0?1:2;let $e=0,Me=1e9;for(let qe=0;qe<De;qe++){var et=new Uint8Array(ue[Ie-1-qe]);const ot=new Uint32Array(ue[Ie-1-qe]);let ye=pe,Ce=we,Fe=-1,nt=-1;for(let it=0;it<we;it++)for(let rt=0;rt<pe;rt++)ne[Ge=it*pe+rt]!=ot[Ge]&&(rt<ye&&(ye=rt),rt>Fe&&(Fe=rt),it<Ce&&(Ce=it),it>nt&&(nt=it));Fe==-1&&(ye=Ce=Fe=nt=0),se&&((1&ye)==1&&ye--,(1&Ce)==1&&Ce--);const pt=(Fe-ye+1)*(nt-Ce+1);pt<Me&&(Me=pt,$e=qe,fe=ye,xe=Ce,ge=Fe-ye+1,be=nt-Ce+1)}et=new Uint8Array(ue[Ie-1-$e]),$e==1&&(Le[Ie-1].dispose=2),Ue=new Uint8Array(ge*be*4),t(et,pe,we,Ue,ge,be,-fe,-xe,0),Ve=t(Be,pe,we,Ue,ge,be,-fe,-xe,3)?1:0,Ve==1?f(Be,pe,we,Ue,{x:fe,y:xe,width:ge,height:be}):t(Be,pe,we,Ue,ge,be,-fe,-xe,0)}else Ue=Be.slice(0);Le.push({rect:{x:fe,y:xe,width:ge,height:be},img:Ue,blend:Ve,dispose:0})}if(He)for(Ie=0;Ie<Le.length;Ie++){if((ft=Le[Ie]).blend==1)continue;const Be=ft.rect,ne=Le[Ie-1].rect,fe=Math.min(Be.x,ne.x),xe=Math.min(Be.y,ne.y),ge={x:fe,y:xe,width:Math.max(Be.x+Be.width,ne.x+ne.width)-fe,height:Math.max(Be.y+Be.height,ne.y+ne.height)-xe};Le[Ie-1].dispose=1,Ie-1!=0&&u(ue,pe,we,Le,Ie-1,ge,se),u(ue,pe,we,Le,Ie,ge,se)}let bt=0;if(ue.length!=1)for(var Ge=0;Ge<Le.length;Ge++){var ft;bt+=(ft=Le[Ge]).rect.width*ft.rect.height}return Le})(C,k,M,P,F,D),Y={},Z=[],ee=[];if(T!=0){const Se=[];for(G=0;G<X.length;G++)Se.push(X[G].img.buffer);const ue=(function(se){let Oe=0;for(var Le=0;Le<se.length;Le++)Oe+=se[Le].byteLength;const Ie=new Uint8Array(Oe);let Ue=0;for(Le=0;Le<se.length;Le++){const et=new Uint8Array(se[Le]),bt=et.length;for(let Ge=0;Ge<bt;Ge+=4){let ft=et[Ge],Be=et[Ge+1],ne=et[Ge+2];const fe=et[Ge+3];fe==0&&(ft=Be=ne=0),Ie[Ue+Ge]=ft,Ie[Ue+Ge+1]=Be,Ie[Ue+Ge+2]=ne,Ie[Ue+Ge+3]=fe}Ue+=bt}return Ie.buffer})(Se),pe=p(ue,T);for(G=0;G<pe.plte.length;G++)Z.push(pe.plte[G].est.rgba);let we=0;for(G=0;G<X.length;G++){const He=(ce=X[G]).img.length;var te=new Uint8Array(pe.inds.buffer,we>>2,He>>2);ee.push(te);const se=new Uint8Array(pe.abuf,we,He);V&&a(ce.img,ce.rect.width,ce.rect.height,Z,se,te),ce.img.set(se),we+=He}}else for(U=0;U<X.length;U++){var ce=X[U];const Se=new Uint32Array(ce.img.buffer);var J=ce.rect.width;for(B=Se.length,te=new Uint8Array(B),ee.push(te),G=0;G<B;G++){const ue=Se[G];if(G!=0&&ue==Se[G-1])te[G]=te[G-1];else if(G>J&&ue==Se[G-J])te[G]=te[G-J];else{let pe=Y[ue];if(pe==null&&(Y[ue]=pe=Z.length,Z.push(ue),Z.length>=300))break;te[G]=pe}}}const he=Z.length;for(he<=256&&O==0&&(W=he<=2?1:he<=4?2:he<=16?4:8,W=Math.max(W,A)),U=0;U<X.length;U++){(ce=X[U]).rect.x,ce.rect.y,J=ce.rect.width;const Se=ce.rect.height;let ue=ce.img;new Uint32Array(ue.buffer);let pe=4*J,we=4;if(he<=256&&O==0){pe=Math.ceil(W*J/8);var le=new Uint8Array(pe*Se);const He=ee[U];for(let se=0;se<Se;se++){G=se*pe;const Oe=se*J;if(W==8)for(var ae=0;ae<J;ae++)le[G+ae]=He[Oe+ae];else if(W==4)for(ae=0;ae<J;ae++)le[G+(ae>>1)]|=He[Oe+ae]<<4-4*(1&ae);else if(W==2)for(ae=0;ae<J;ae++)le[G+(ae>>2)]|=He[Oe+ae]<<6-2*(3&ae);else if(W==1)for(ae=0;ae<J;ae++)le[G+(ae>>3)]|=He[Oe+ae]<<7-1*(7&ae)}ue=le,H=3,we=1}else if(z==0&&X.length==1){le=new Uint8Array(J*Se*3);const He=J*Se;for(G=0;G<He;G++){const se=3*G,Oe=4*G;le[se]=ue[Oe],le[se+1]=ue[Oe+1],le[se+2]=ue[Oe+2]}ue=le,H=2,we=3,pe=3*J}ce.img=ue,ce.bpl=pe,ce.bpp=we}return{ctype:H,depth:W,plte:Z,frames:X}}function u(C,k,M,T,L,P,F){const D=Uint8Array,A=Uint32Array,O=new D(C[L-1]),V=new A(C[L-1]),H=L+1<C.length?new D(C[L+1]):null,W=new D(C[L]),j=new A(W.buffer);let U=k,B=M,G=-1,z=-1;for(let Y=0;Y<P.height;Y++)for(let Z=0;Z<P.width;Z++){const ee=P.x+Z,te=P.y+Y,ce=te*k+ee,J=j[ce];J==0||T[L-1].dispose==0&&V[ce]==J&&(H==null||H[4*ce+3]!=0)||(ee<U&&(U=ee),ee>G&&(G=ee),te<B&&(B=te),te>z&&(z=te))}G==-1&&(U=B=G=z=0),F&&((1&U)==1&&U--,(1&B)==1&&B--),P={x:U,y:B,width:G-U+1,height:z-B+1};const X=T[L];X.rect=P,X.blend=1,X.img=new Uint8Array(P.width*P.height*4),T[L-1].dispose==0?(t(O,k,M,X.img,P.width,P.height,-P.x,-P.y,0),f(W,k,M,X.img,P)):t(W,k,M,X.img,P.width,P.height,-P.x,-P.y,0)}function f(C,k,M,T,L){t(C,k,M,T,L.width,L.height,-L.x,-L.y,2)}function g(C,k,M,T,L,P,F){const D=[];let A,O=[0,1,2,3,4];P!=-1?O=[P]:(k*T>5e5||M==1)&&(O=[0]),F&&(A={level:0});const V=eE;for(var H=0;H<O.length;H++){for(let U=0;U<k;U++)m(L,C,U,T,M,O[H]);D.push(V.deflate(L,A))}let W,j=1e9;for(H=0;H<D.length;H++)D[H].length<j&&(W=H,j=D[H].length);return D[W]}function m(C,k,M,T,L,P){const F=M*T;let D=F+M;if(C[D]=P,D++,P==0)if(T<500)for(var A=0;A<T;A++)C[D+A]=k[F+A];else C.set(new Uint8Array(k.buffer,F,T),D);else if(P==1){for(A=0;A<L;A++)C[D+A]=k[F+A];for(A=L;A<T;A++)C[D+A]=k[F+A]-k[F+A-L]+256&255}else if(M==0){for(A=0;A<L;A++)C[D+A]=k[F+A];if(P==2)for(A=L;A<T;A++)C[D+A]=k[F+A];if(P==3)for(A=L;A<T;A++)C[D+A]=k[F+A]-(k[F+A-L]>>1)+256&255;if(P==4)for(A=L;A<T;A++)C[D+A]=k[F+A]-n(k[F+A-L],0,0)+256&255}else{if(P==2)for(A=0;A<T;A++)C[D+A]=k[F+A]+256-k[F+A-T]&255;if(P==3){for(A=0;A<L;A++)C[D+A]=k[F+A]+256-(k[F+A-T]>>1)&255;for(A=L;A<T;A++)C[D+A]=k[F+A]+256-(k[F+A-T]+k[F+A-L]>>1)&255}if(P==4){for(A=0;A<L;A++)C[D+A]=k[F+A]+256-n(0,k[F+A-T],0)&255;for(A=L;A<T;A++)C[D+A]=k[F+A]+256-n(k[F+A-L],k[F+A-T],k[F+A-L-T])&255}}}function p(C,k){const M=new Uint8Array(C),T=M.slice(0),L=new Uint32Array(T.buffer),P=y(T,k),F=P[0],D=P[1],A=M.length,O=new Uint8Array(A>>2);let V;if(M.length<2e7)for(var H=0;H<A;H+=4)V=b(F,W=M[H]*.00392156862745098,j=M[H+1]*.00392156862745098,U=M[H+2]*.00392156862745098,B=M[H+3]*.00392156862745098),O[H>>2]=V.ind,L[H>>2]=V.est.rgba;else for(H=0;H<A;H+=4){var W=M[H]*.00392156862745098,j=M[H+1]*(1/255),U=M[H+2]*(1/255),B=M[H+3]*(1/255);for(V=F;V.left;)V=_(V.est,W,j,U,B)<=0?V.left:V.right;O[H>>2]=V.ind,L[H>>2]=V.est.rgba}return{abuf:T.buffer,inds:O,plte:D}}function y(C,k,M){M==null&&(M=1e-4);const T=new Uint32Array(C.buffer),L={i0:0,i1:C.length,bst:null,est:null,tdst:0,left:null,right:null};L.bst=S(C,L.i0,L.i1),L.est=w(L.bst);const P=[L];for(;P.length<k;){let D=0,A=0;for(var F=0;F<P.length;F++)P[F].est.L>D&&(D=P[F].est.L,A=F);if(D<M)break;const O=P[A],V=x(C,T,O.i0,O.i1,O.est.e,O.est.eMq255);if(O.i0>=V||O.i1<=V){O.est.L=0;continue}const H={i0:O.i0,i1:V,bst:null,est:null,tdst:0,left:null,right:null};H.bst=S(C,H.i0,H.i1),H.est=w(H.bst);const W={i0:V,i1:O.i1,bst:null,est:null,tdst:0,left:null,right:null};for(W.bst={R:[],m:[],N:O.bst.N-H.bst.N},F=0;F<16;F++)W.bst.R[F]=O.bst.R[F]-H.bst.R[F];for(F=0;F<4;F++)W.bst.m[F]=O.bst.m[F]-H.bst.m[F];W.est=w(W.bst),O.left=H,O.right=W,P[A]=H,P.push(W)}for(P.sort(((D,A)=>A.bst.N-D.bst.N)),F=0;F<P.length;F++)P[F].ind=F;return[L,P]}function b(C,k,M,T,L){if(C.left==null)return C.tdst=(function(H,W,j,U,B){const G=W-H[0],z=j-H[1],X=U-H[2],Y=B-H[3];return G*G+z*z+X*X+Y*Y})(C.est.q,k,M,T,L),C;const P=_(C.est,k,M,T,L);let F=C.left,D=C.right;P>0&&(F=C.right,D=C.left);const A=b(F,k,M,T,L);if(A.tdst<=P*P)return A;const O=b(D,k,M,T,L);return O.tdst<A.tdst?O:A}function _(C,k,M,T,L){const{e:P}=C;return P[0]*k+P[1]*M+P[2]*T+P[3]*L-C.eMq}function x(C,k,M,T,L,P){for(T-=4;M<T;){for(;E(C,M,L)<=P;)M+=4;for(;E(C,T,L)>P;)T-=4;if(M>=T)break;const F=k[M>>2];k[M>>2]=k[T>>2],k[T>>2]=F,M+=4,T-=4}for(;E(C,M,L)>P;)M-=4;return M+4}function E(C,k,M){return C[k]*M[0]+C[k+1]*M[1]+C[k+2]*M[2]+C[k+3]*M[3]}function S(C,k,M){const T=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],L=[0,0,0,0],P=M-k>>2;for(let F=k;F<M;F+=4){const D=C[F]*.00392156862745098,A=C[F+1]*(1/255),O=C[F+2]*(1/255),V=C[F+3]*(1/255);L[0]+=D,L[1]+=A,L[2]+=O,L[3]+=V,T[0]+=D*D,T[1]+=D*A,T[2]+=D*O,T[3]+=D*V,T[5]+=A*A,T[6]+=A*O,T[7]+=A*V,T[10]+=O*O,T[11]+=O*V,T[15]+=V*V}return T[4]=T[1],T[8]=T[2],T[9]=T[6],T[12]=T[3],T[13]=T[7],T[14]=T[11],{R:T,m:L,N:P}}function w(C){const{R:k}=C,{m:M}=C,{N:T}=C,L=M[0],P=M[1],F=M[2],D=M[3],A=T==0?0:1/T,O=[k[0]-L*L*A,k[1]-L*P*A,k[2]-L*F*A,k[3]-L*D*A,k[4]-P*L*A,k[5]-P*P*A,k[6]-P*F*A,k[7]-P*D*A,k[8]-F*L*A,k[9]-F*P*A,k[10]-F*F*A,k[11]-F*D*A,k[12]-D*L*A,k[13]-D*P*A,k[14]-D*F*A,k[15]-D*D*A],V=O,H=N;let W=[Math.random(),Math.random(),Math.random(),Math.random()],j=0,U=0;if(T!=0)for(let G=0;G<16&&(W=H.multVec(V,W),U=Math.sqrt(H.dot(W,W)),W=H.sml(1/U,W),!(G!=0&&Math.abs(U-j)<1e-9));G++)j=U;const B=[L*A,P*A,F*A,D*A];return{Cov:O,q:B,e:W,L:j,eMq255:H.dot(H.sml(255,B),W),eMq:H.dot(W,B),rgba:(Math.round(255*B[3])<<24|Math.round(255*B[2])<<16|Math.round(255*B[1])<<8|Math.round(255*B[0])<<0)>>>0}}var N={multVec:(C,k)=>[C[0]*k[0]+C[1]*k[1]+C[2]*k[2]+C[3]*k[3],C[4]*k[0]+C[5]*k[1]+C[6]*k[2]+C[7]*k[3],C[8]*k[0]+C[9]*k[1]+C[10]*k[2]+C[11]*k[3],C[12]*k[0]+C[13]*k[1]+C[14]*k[2]+C[15]*k[3]],dot:(C,k)=>C[0]*k[0]+C[1]*k[1]+C[2]*k[2]+C[3]*k[3],sml:(C,k)=>[C*k[0],C*k[1],C*k[2],C*k[3]]};Tn.encode=function(k,M,T,L,P,F,D){L==null&&(L=0),D==null&&(D=!1);const A=d(k,M,T,L,[!1,!1,!1,0,D,!1]);return c(A,-1),l(A,M,T,P,F)},Tn.encodeLL=function(k,M,T,L,P,F,D,A){const O={ctype:0+(L==1?0:2)+(P==0?0:4),depth:F,frames:[]},V=(L+P)*F,H=V*M;for(let W=0;W<k.length;W++)O.frames.push({rect:{x:0,y:0,width:M,height:T},img:new Uint8Array(k[W]),blend:0,dispose:1,bpp:Math.ceil(V/8),bpl:Math.ceil(H/8)});return c(O,0,!0),l(O,M,T,D,A)},Tn.encode.compress=d,Tn.encode.dither=a,Tn.quantize=p,Tn.quantize.getKDtree=y,Tn.quantize.getNearest=b})();const gp={toArrayBuffer(t,e){const n=t.width,r=t.height,s=n<<2,o=t.getContext("2d").getImageData(0,0,n,r),i=new Uint32Array(o.data.buffer),a=(32*n+31)/32<<2,l=a*r,c=122+l,d=new ArrayBuffer(c),u=new DataView(d),f=1<<20;let g,m,p,y,b=f,_=0,x=0,E=0;function S(C){u.setUint16(x,C,!0),x+=2}function w(C){u.setUint32(x,C,!0),x+=4}function N(C){x+=C}S(19778),w(c),N(4),w(122),w(108),w(n),w(-r>>>0),S(1),S(32),w(3),w(l),w(2835),w(2835),N(8),w(16711680),w(65280),w(255),w(4278190080),w(1466527264),(function C(){for(;_<r&&b>0;){for(y=122+_*a,g=0;g<s;)b--,m=i[E++],p=m>>>24,u.setUint32(y+g,m<<8|p),g+=4;_++}E<i.length?(b=f,setTimeout(C,gp._dly)):e(d)})()},toBlob(t,e){this.toArrayBuffer(t,(n=>{e(new Blob([n],{type:"image/bmp"}))}))},_dly:9};var tn={CHROME:"CHROME",FIREFOX:"FIREFOX",DESKTOP_SAFARI:"DESKTOP_SAFARI",IE:"IE",IOS:"IOS",ETC:"ETC"},tE={[tn.CHROME]:16384,[tn.FIREFOX]:11180,[tn.DESKTOP_SAFARI]:16384,[tn.IE]:8192,[tn.IOS]:4096,[tn.ETC]:8192};const Ic=typeof window<"u",pp=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,Gi=Ic&&window.cordova&&window.cordova.require&&window.cordova.require("cordova/modulemapper"),nE=(Ic||pp)&&(Gi&&Gi.getOriginalSymbol(window,"File")||typeof File<"u"&&File),mp=(Ic||pp)&&(Gi&&Gi.getOriginalSymbol(window,"FileReader")||typeof FileReader<"u"&&FileReader);function Lc(t,e,n=Date.now()){return new Promise((r=>{const s=t.split(","),o=s[0].match(/:(.*?);/)[1],i=globalThis.atob(s[1]);let a=i.length;const l=new Uint8Array(a);for(;a--;)l[a]=i.charCodeAt(a);const c=new Blob([l],{type:o});c.name=e,c.lastModified=n,r(c)}))}function bp(t){return new Promise(((e,n)=>{const r=new mp;r.onload=()=>e(r.result),r.onerror=s=>n(s),r.readAsDataURL(t)}))}function yp(t){return new Promise(((e,n)=>{const r=new Image;r.onload=()=>e(r),r.onerror=s=>n(s),r.src=t}))}function Sr(){if(Sr.cachedResult!==void 0)return Sr.cachedResult;let t=tn.ETC;const{userAgent:e}=navigator;return/Chrom(e|ium)/i.test(e)?t=tn.CHROME:/iP(ad|od|hone)/i.test(e)&&/WebKit/i.test(e)?t=tn.IOS:/Safari/i.test(e)?t=tn.DESKTOP_SAFARI:/Firefox/i.test(e)?t=tn.FIREFOX:(/MSIE/i.test(e)||document.documentMode)&&(t=tn.IE),Sr.cachedResult=t,Sr.cachedResult}function vp(t,e){const n=Sr(),r=tE[n];let s=t,o=e,i=s*o;const a=s>o?o/s:s/o;for(;i>r*r;){const l=(r+s)/2,c=(r+o)/2;l<c?(o=c,s=c*a):(o=l*a,s=l),i=s*o}return{width:s,height:o}}function Yi(t,e){let n,r;try{if(n=new OffscreenCanvas(t,e),r=n.getContext("2d"),r===null)throw new Error("getContext of OffscreenCanvas returns null")}catch{n=document.createElement("canvas"),r=n.getContext("2d")}return n.width=t,n.height=e,[n,r]}function xp(t,e){const{width:n,height:r}=vp(t.width,t.height),[s,o]=Yi(n,r);return e&&/jpe?g/.test(e)&&(o.fillStyle="white",o.fillRect(0,0,s.width,s.height)),o.drawImage(t,0,0,s.width,s.height),s}function Xi(){return Xi.cachedResult!==void 0||(Xi.cachedResult=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&typeof document<"u"&&"ontouchend"in document),Xi.cachedResult}function Ki(t,e={}){return new Promise((function(n,r){let s,o;var i=function(){try{return o=xp(s,e.fileType||t.type),n([s,o])}catch(l){return r(l)}},a=function(l){try{var c=function(d){try{throw d}catch(u){return r(u)}};try{let d;return bp(t).then((function(u){try{return d=u,yp(d).then((function(f){try{return s=f,(function(){try{return i()}catch(g){return r(g)}})()}catch(g){return c(g)}}),c)}catch(f){return c(f)}}),c)}catch(d){c(d)}}catch(d){return r(d)}};try{if(Xi()||[tn.DESKTOP_SAFARI,tn.MOBILE_SAFARI].includes(Sr()))throw new Error("Skip createImageBitmap on IOS and Safari");return createImageBitmap(t).then((function(l){try{return s=l,i()}catch{return a()}}),a)}catch{a()}}))}function qi(t,e,n,r,s=1){return new Promise((function(o,i){let a;if(e==="image/png"){let c,d,u;return c=t.getContext("2d"),{data:d}=c.getImageData(0,0,t.width,t.height),u=Tn.encode([d.buffer],t.width,t.height,4096*s),a=new Blob([u],{type:e}),a.name=n,a.lastModified=r,l.call(this)}{let c=function(){return l.call(this)};if(e==="image/bmp")return new Promise((d=>gp.toBlob(t,d))).then(function(d){try{return a=d,a.name=n,a.lastModified=r,c.call(this)}catch(u){return i(u)}}.bind(this),i);{let d=function(){return c.call(this)};if(typeof OffscreenCanvas=="function"&&t instanceof OffscreenCanvas)return t.convertToBlob({type:e,quality:s}).then(function(u){try{return a=u,a.name=n,a.lastModified=r,d.call(this)}catch(f){return i(f)}}.bind(this),i);{let u;return u=t.toDataURL(e,s),Lc(u,n,r).then(function(f){try{return a=f,d.call(this)}catch(g){return i(g)}}.bind(this),i)}}}function l(){return o(a)}}))}function Vn(t){t.width=0,t.height=0}function us(){return new Promise((function(t,e){let n,r,s,o;return us.cachedResult!==void 0?t(us.cachedResult):Lc("data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/xABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==","test.jpg",Date.now()).then((function(i){try{return n=i,Ki(n).then((function(a){try{return r=a[1],qi(r,n.type,n.name,n.lastModified).then((function(l){try{return s=l,Vn(r),Ki(s).then((function(c){try{return o=c[0],us.cachedResult=o.width===1&&o.height===2,t(us.cachedResult)}catch(d){return e(d)}}),e)}catch(c){return e(c)}}),e)}catch(l){return e(l)}}),e)}catch(a){return e(a)}}),e)}))}function wp(t){return new Promise(((e,n)=>{const r=new mp;r.onload=s=>{const o=new DataView(s.target.result);if(o.getUint16(0,!1)!=65496)return e(-2);const i=o.byteLength;let a=2;for(;a<i;){if(o.getUint16(a+2,!1)<=8)return e(-1);const l=o.getUint16(a,!1);if(a+=2,l==65505){if(o.getUint32(a+=2,!1)!=1165519206)return e(-1);const c=o.getUint16(a+=6,!1)==18761;a+=o.getUint32(a+4,c);const d=o.getUint16(a,c);a+=2;for(let u=0;u<d;u++)if(o.getUint16(a+12*u,c)==274)return e(o.getUint16(a+12*u+8,c))}else{if((65280&l)!=65280)break;a+=o.getUint16(a,!1)}}return e(-1)},r.onerror=s=>n(s),r.readAsArrayBuffer(t)}))}function kp(t,e){const{width:n}=t,{height:r}=t,{maxWidthOrHeight:s}=e;let o,i=t;return isFinite(s)&&(n>s||r>s)&&([i,o]=Yi(n,r),n>r?(i.width=s,i.height=r/n*s):(i.width=n/r*s,i.height=s),o.drawImage(t,0,0,i.width,i.height),Vn(t)),i}function Cp(t,e){const{width:n}=t,{height:r}=t,[s,o]=Yi(n,r);switch(e>4&&e<9?(s.width=r,s.height=n):(s.width=n,s.height=r),e){case 2:o.transform(-1,0,0,1,n,0);break;case 3:o.transform(-1,0,0,-1,n,r);break;case 4:o.transform(1,0,0,-1,0,r);break;case 5:o.transform(0,1,1,0,0,0);break;case 6:o.transform(0,1,-1,0,r,0);break;case 7:o.transform(0,-1,-1,0,r,n);break;case 8:o.transform(0,-1,1,0,0,n)}return o.drawImage(t,0,0,n,r),Vn(t),s}function Sp(t,e,n=0){return new Promise((function(r,s){let o,i,a,l,c,d,u,f,g,m,p,y,b,_,x,E,S,w,N,C;function k(T=5){if(e.signal&&e.signal.aborted)throw e.signal.reason;o+=T,e.onProgress(Math.min(o,100))}function M(T){if(e.signal&&e.signal.aborted)throw e.signal.reason;o=Math.min(Math.max(T,o),100),e.onProgress(o)}return o=n,i=e.maxIteration||10,a=1024*e.maxSizeMB*1024,k(),Ki(t,e).then(function(T){try{return[,l]=T,k(),c=kp(l,e),k(),new Promise((function(L,P){var F;if(!(F=e.exifOrientation))return wp(t).then(function(A){try{return F=A,D.call(this)}catch(O){return P(O)}}.bind(this),P);function D(){return L(F)}return D.call(this)})).then(function(L){try{return d=L,k(),us().then(function(P){try{return u=P?c:Cp(c,d),k(),f=e.initialQuality||1,g=e.fileType||t.type,qi(u,g,t.name,t.lastModified,f).then(function(F){try{{let A=function(){if(i--&&(x>a||x>b)){let V,H;return V=C?.95*N.width:N.width,H=C?.95*N.height:N.height,[S,w]=Yi(V,H),w.drawImage(N,0,0,V,H),f*=g==="image/png"?.85:.95,qi(S,g,t.name,t.lastModified,f).then((function(W){try{return E=W,Vn(N),N=S,x=E.size,M(Math.min(99,Math.floor((_-x)/(_-a)*100))),A}catch(j){return s(j)}}),s)}return[1]},O=function(){return Vn(N),Vn(S),Vn(c),Vn(u),Vn(l),M(100),r(E)};if(m=F,k(),p=m.size>a,y=m.size>t.size,!p&&!y)return M(100),r(m);var D;return b=t.size,_=m.size,x=_,N=u,C=!e.alwaysKeepResolution&&p,(D=function(V){for(;V;){if(V.then)return void V.then(D,s);try{if(V.pop){if(V.length)return V.pop()?O.call(this):V;V=A}else V=V.call(this)}catch(H){return s(H)}}}.bind(this))(A)}}catch(A){return s(A)}}.bind(this),s)}catch(F){return s(F)}}.bind(this),s)}catch(P){return s(P)}}.bind(this),s)}catch(L){return s(L)}}.bind(this),s)}))}const rE=` let scriptImported = false self.addEventListener('message', async (e) => { const { file, id, imageCompressionLibUrl, options } = e.data @@ -221,11 +227,11 @@ self.addEventListener('message', async (e) => { self.postMessage({ error: e.message + '\\n' + e.stack, id }) } }) -`;let Uc;function _E(e,t){return new Promise(((n,r)=>{Uc||(Uc=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(SE));const s=new Worker(Uc);s.addEventListener("message",(function(i){if(t.signal&&t.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else t.onProgress(i.data.progress)})),s.addEventListener("error",r),t.signal&&t.signal.addEventListener("abort",(()=>{r(t.signal.reason),s.terminate()})),s.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}})}))}function Yt(e,t){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...t},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(e instanceof Blob||e instanceof CE))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return $p(e,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return $p(e,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",_E(e,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=e.name,o.lastModified=e.lastModified}catch{}try{s.preserveExif&&e.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===e.type)&&(o=_p(e,o))}catch{}return n(o)}}))}Yt.getDataUrlFromFile=Tp,Yt.getFilefromDataUrl=Hc,Yt.loadImage=Op,Yt.drawImageInCanvas=Dp,Yt.drawFileInCanvas=na,Yt.canvasToFile=ra,Yt.getExifOrientation=Ip,Yt.handleMaxWidthOrHeight=Lp,Yt.followExifOrientation=Pp,Yt.cleanupCanvasMemory=Wn,Yt.isAutoOrientationInBrowser=ks,Yt.approximateBelowMaximumCanvasSizeOfBrowser=Ap,Yt.copyExifWithoutOrientation=_p,Yt.getBrowserName=Dr,Yt.version="2.0.2";const EE=window.G7Core,Bc=e=>{const t=["B","KB","MB","GB"];let n=0,r=e;for(;r>=1024&&n<t.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${t[n]}`},ME=e=>{if(e&&typeof e=="object"){const t=e;if(t.response?.data?.message)return t.response.data.message;if(t.response?.data?.errors){const n=Object.values(t.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(t.message)return t.message}return e instanceof Error?e.message:"업로드 실패"},NE=e=>e?e.startsWith("image/")?"fa-file-image":e.startsWith("video/")?"fa-file-video":e.startsWith("audio/")?"fa-file-audio":e==="application/pdf"?"fa-file-pdf":e.includes("word")||e.includes("document")?"fa-file-word":e.includes("excel")||e.includes("spreadsheet")?"fa-file-excel":e.includes("zip")||e.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",$t=(e,t)=>EE?.t?.(e,t)??e,Fp=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...e)=>console.log("[Comp:FileUploader]",...e),warn:(...e)=>console.warn("[Comp:FileUploader]",...e),error:(...e)=>console.error("[Comp:FileUploader]",...e)},Ir=window.G7Core;function TE(e){const{attachmentableType:t,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:_,confirmBeforeRemove:x,uploadParams:M}=e,[C,k]=v.useState(u),[N,S]=v.useState([]),[w,E]=v.useState(!1),D=v.useRef(null),P=v.useRef(0),L=v.useRef(new Set),$=v.useRef(new Map),A=v.useRef(null),[O,T]=v.useState(!1),[z,H]=v.useState(null),[j,G]=v.useState(!1),U=v.useRef(!1),[W,Y]=v.useState([]),[V,X]=v.useState(!1),[K,q]=v.useState(0),ne=v.useRef(0),[ee,ue]=v.useState(new Map),J=v.useRef(new Map),ce=v.useRef(g);ce.current=g,v.useEffect(()=>{ce.current?.(N)},[N]),v.useEffect(()=>{if(U.current)return;const re=u.filter(we=>!L.current.has(we.id)&&(!we.hash||!L.current.has(we.hash))),fe=new Set(re.map(we=>we.id)),be=Array.from($.current.values()).filter(we=>!fe.has(we.id)&&!L.current.has(we.id));k([...re,...be])},[u]);const me=v.useCallback(async re=>{const fe={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await Yt(re,fe)},[l]),le=v.useCallback(async re=>{const fe=C.length+N.length,be=Math.max(0,s-fe),we=Array.from(re).slice(0,be);if(re.length>be){const xe=Array.from(re).slice(be),He=$t("attachment.upload_limit_exceeded",{max:s});p?.(He,xe[0]??re[0])}for(const xe of we){if(xe.size>o*1024*1024){p?.(`[${xe.name}] 파일 크기(${Bc(xe.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,xe);continue}if(a){const _e="."+xe.name.split(".").pop()?.toLowerCase();if(!a.split(",").map(Ce=>Ce.trim().toLowerCase()).includes(_e)){p?.(`[${xe.name}] 허용되지 않는 파일 형식입니다. (허용: ${a})`,xe);continue}}const He={id:`pending-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:xe,original_filename:xe.name,mime_type:xe.type,size:xe.size,size_formatted:Bc(xe.size),status:"pending",progress:0};if(xe.type.startsWith("image/")&&(He.preview=URL.createObjectURL(xe)),S(_e=>[..._e,He]),xe.type.startsWith("image/")&&l){S(_e=>_e.map(Le=>Le.id===He.id?{...Le,status:"compressing"}:Le));try{const _e=await me(xe);S(Le=>Le.map(Ce=>Ce.id===He.id?{...Ce,compressedBlob:_e,size:_e.size,size_formatted:Bc(_e.size),status:"pending",progress:0}:Ce))}catch{S(_e=>_e.map(Le=>Le.id===He.id?{...Le,status:"error",error:"이미지 압축 실패"}:Le))}}}},[C.length,N.length,s,o,a,l,me,p]),ve=v.useCallback(async re=>{const fe=new FormData,be=re.compressedBlob?new File([re.compressedBlob],re.original_filename,{type:re.mime_type}):re.file;fe.append("file",be),t&&fe.append("attachmentable_type",t),n&&fe.append("attachmentable_id",String(n)),fe.append("collection",r),c.forEach(we=>{fe.append("role_ids[]",String(we))}),M&&Object.entries(M).forEach(([we,xe])=>{fe.append(we,xe)});try{const we=await Ir.api.post(f.upload,fe,{headers:{"Content-Type":"multipart/form-data"},onUploadProgress:He=>{const _e=Math.round(He.loaded*100/(He.total??1));S(Le=>Le.map(Ce=>Ce.id===re.id?{...Ce,progress:_e}:Ce))}});return we?.data?.data??we?.data}catch(we){const xe=ME(we);throw new Error(xe)}},[t,n,r,c,f.upload]),ge=v.useCallback(async()=>{const re=N.filter(_e=>_e.status==="pending"||_e.status==="error");if(re.length===0)return A.current=C,[];const fe=[],be=new Map,we=[...re],xe=async()=>{for(;we.length>0&&P.current<i;){const _e=we.shift();P.current++,S(Le=>Le.map(Ce=>Ce.id===_e.id?{...Ce,status:"uploading",progress:0}:Ce));try{const Le=await ve(_e);if(Le)fe.push(Le),be.set(String(_e.id),String(Le.hash)),$.current.set(Le.id,Le),S(Ce=>Ce.filter(We=>We.id!==_e.id)),k(Ce=>[...Ce,Le]),Y(Ce=>{if(Ce.length===0)return Ce;const We=String(_e.id);return Ce.some(Fe=>String(Fe)===We)?Ce.map(Fe=>String(Fe)===We?String(Le.hash):Fe):Ce}),_e.preview&&URL.revokeObjectURL(_e.preview);else{const Ce=$t("attachment.upload_response_invalid");S(We=>We.map(Ze=>Ze.id===_e.id?{...Ze,status:"error",error:Ce}:Ze)),p?.(`[${_e.original_filename}] ${Ce}`,_e.file)}}catch(Le){const Ce=Le instanceof Error?Le.message:"업로드 실패";S(We=>We.map(Ze=>Ze.id===_e.id?{...Ze,status:"error",error:Ce}:Ze)),p?.(`[${_e.original_filename}] ${Ce}`,_e.file)}finally{P.current--}}},He=Array(Math.min(i,re.length)).fill(null).map(()=>xe());if(await Promise.all(He),fe.length>0){m?.(fe);const _e=[...C,...fe];if(W.length>0){const Le=W.map(Ze=>be.get(String(Ze))??Ze),Ce=new Map(Le.map((Ze,Fe)=>[String(Ze),Fe])),We=[..._e].sort((Ze,Fe)=>{const Ne=Ce.get(String(Ze.hash))??_e.length,ze=Ce.get(String(Fe.hash))??_e.length;return Ne-ze}).map((Ze,Fe)=>({...Ze,sort_order:Fe,order:Fe+1}));A.current=We,f.reorder&&b&&b(We)}else A.current=_e.map((Le,Ce)=>({...Le,sort_order:Ce,order:Ce+1}))}else A.current=C;return fe},[N,i,ve,m,p,W,C,b,f.reorder]);v.useEffect(()=>{if(!_)return;const re=Ir.componentEvent.on(_,async()=>{const fe=await ge(),be=A.current??[...C,...fe];return{uploadedAttachments:fe,existingFiles:C,allFiles:be}});return()=>{re()}},[_,ge,C]),v.useEffect(()=>{d&&N.some(re=>re.status==="pending")&&ge()},[d,N,ge]);const pe=v.useCallback(async re=>{G(!0);try{if(re.id&&f.delete){const be=f.delete.replace(":id",String(re.id));await Ir.api.delete(be)}const fe=re.hash||re.id;re.id&&(L.current.add(re.id),$.current.delete(re.id)),re.hash&&L.current.add(re.hash),k(be=>be.filter(we=>(we.hash||we.id)!==fe)),y?.(re.hash||re.id),T(!1),H(null)}catch{Fp.error("첨부파일 삭제 실패")}finally{G(!1)}},[y,f.delete]),ke=v.useCallback(async re=>{"hash"in re?x?(H(re),T(!0)):await pe(re):(re.preview&&URL.revokeObjectURL(re.preview),S(fe=>fe.filter(be=>be.id!==re.id)))},[x,pe]),$e=v.useCallback(re=>{S(fe=>fe.map(be=>be.id===re.id?{...be,status:"pending",error:void 0}:be))},[]),ie=v.useCallback(async re=>{const{arrayMove:fe}=await Promise.resolve().then(()=>QS),{active:be,over:we}=re;if(!we||be.id===we.id)return;const xe=[...C,...N];let He;if(W.length===0)He=xe;else{const Ne=new Map(W.map((ze,Ke)=>[String(ze),Ke]));He=[...xe].sort((ze,Ke)=>{const tt="hash"in ze?String(ze.hash):String(ze.id),at="hash"in Ke?String(Ke.hash):String(Ke.id);return(Ne.get(tt)??xe.length)-(Ne.get(at)??xe.length)})}const _e=He.findIndex(Ne=>"hash"in Ne?Ne.hash===be.id:Ne.id===be.id),Le=He.findIndex(Ne=>"hash"in Ne?Ne.hash===we.id:Ne.id===we.id),Ce=fe(He,_e,Le);Y(Ce.map(Ne=>"hash"in Ne?Ne.hash:Ne.id));const We=Ce.filter(Ne=>"hash"in Ne),Ze=Ce.filter(Ne=>!("hash"in Ne));U.current=!0,k(We),S(Ze);const Fe=We.map((Ne,ze)=>({...Ne,sort_order:ze,order:ze+1}));if(We.length>0&&f.reorder){const Ne=We.map((ze,Ke)=>({id:ze.id,order:Ke+1}));try{const ze=await Ir.api.patch(f.reorder,{order:Ne});Ir.toast.success(ze.message)}catch(ze){const Ke=ze instanceof Error?ze.message:$t("attachment.reorder_failed");Ir.toast.error(Ke)}}b?.(Fe)},[C,N,W,f.reorder,b]),Me=v.useMemo(()=>[...C,...N].filter(re=>"is_image"in re?re.is_image:re.mime_type?.startsWith("image/")??!1),[C,N]),Ae=v.useCallback(re=>{const fe=Me.findIndex(be=>"hash"in re&&"hash"in be?be.hash===re.hash:"id"in re&&"id"in be&&!("hash"in re)&&!("hash"in be)?be.id===re.id:!1);fe!==-1&&(ne.current+=1,q(fe),X(!0))},[Me]);v.useEffect(()=>{let re=!1;return(async()=>{const be=C.filter(xe=>xe.is_image),we=new Set(be.map(xe=>xe.id));for(const[xe,He]of J.current)we.has(xe)||(URL.revokeObjectURL(He),J.current.delete(xe));for(const xe of be){if(re)break;if(!J.current.has(xe.id))try{const He=await Ir.api.get(xe.download_url,{responseType:"blob"});if(!re&&He){const _e=URL.createObjectURL(He);J.current.set(xe.id,_e),ue(new Map(J.current))}}catch(He){Fp.error("Failed to load authenticated image:",He)}}})(),()=>{re=!0}},[C]),v.useEffect(()=>()=>{J.current.forEach(re=>URL.revokeObjectURL(re)),J.current.clear()},[]);const Ie=v.useCallback(async re=>{const{executeImageDownload:fe}=await Promise.resolve().then(()=>pE);await fe({src:re.download_url,downloadUrl:re.download_url,filename:re.original_filename,downloadRequiresAuth:!0})},[]),Xe=v.useMemo(()=>{const re=[...C,...N];if(W.length===0)return re;const fe=new Map(W.map((be,we)=>[String(be),we]));return[...re].sort((be,we)=>{const xe="hash"in be?String(be.hash):String(be.id),He="hash"in we?String(we.hash):String(we.id),_e=fe.get(xe)??re.length,Le=fe.get(He)??re.length;return _e-Le})},[C,N,W]),gt=Xe.length,pt=gt>0,Be=gt<s,et=v.useCallback(()=>S([]),[]),je=v.useCallback(()=>N,[N]);return{existingFiles:C,pendingFiles:N,isDragOver:w,setIsDragOver:E,isDeleting:j,totalCount:gt,hasFiles:pt,canAddMore:Be,allItems:Xe,imageFiles:Me,handleFiles:le,handleRemove:ke,handleRetry:$e,handleUploadAll:ge,handleOpenGallery:Ae,handleDownload:Ie,handleDragEnd:ie,inputRef:D,galleryOpen:V,setGalleryOpen:X,galleryStartIndex:K,galleryKeyRef:ne,confirmDialogOpen:O,setConfirmDialogOpen:T,itemToDelete:z,executeRemoveAttachment:pe,authenticatedImageUrls:ee,clear:et,getPendingFiles:je}}const OE=({isDragOver:e,canAddMore:t,onFiles:n,setIsDragOver:r,inputRef:s,accept:o,maxFiles:i,maxSize:a,disabled:l,children:c})=>{const d=t&&!l;return h.jsxs(I,{className:` +`;let Pc;function sE(t,e){return new Promise(((n,r)=>{Pc||(Pc=(function(i){const a=[];return a.push(i),URL.createObjectURL(new Blob(a))})(rE));const s=new Worker(Pc);s.addEventListener("message",(function(i){if(e.signal&&e.signal.aborted)s.terminate();else if(i.data.progress===void 0){if(i.data.error)return r(new Error(i.data.error)),void s.terminate();n(i.data.file),s.terminate()}else e.onProgress(i.data.progress)})),s.addEventListener("error",r),e.signal&&e.signal.addEventListener("abort",(()=>{r(e.signal.reason),s.terminate()})),s.postMessage({file:t,imageCompressionLibUrl:e.libURL,options:{...e,onProgress:void 0,signal:void 0}})}))}function Bt(t,e){return new Promise((function(n,r){let s,o,i,a,l,c;if(s={...e},i=0,{onProgress:a}=s,s.maxSizeMB=s.maxSizeMB||Number.POSITIVE_INFINITY,l=typeof s.useWebWorker!="boolean"||s.useWebWorker,delete s.useWebWorker,s.onProgress=g=>{i=g,typeof a=="function"&&a(i)},!(t instanceof Blob||t instanceof nE))return r(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(t.type))return r(new Error("The file given is not an image"));if(c=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!l||typeof Worker!="function"||c)return Sp(t,s).then(function(g){try{return o=g,f.call(this)}catch(m){return r(m)}}.bind(this),r);var d=function(){try{return f.call(this)}catch(g){return r(g)}}.bind(this),u=function(g){try{return Sp(t,s).then((function(m){try{return o=m,d()}catch(p){return r(p)}}),r)}catch(m){return r(m)}};try{return s.libURL=s.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",sE(t,s).then((function(g){try{return o=g,d()}catch{return u()}}),u)}catch{u()}function f(){try{o.name=t.name,o.lastModified=t.lastModified}catch{}try{s.preserveExif&&t.type==="image/jpeg"&&(!s.fileType||s.fileType&&s.fileType===t.type)&&(o=hp(t,o))}catch{}return n(o)}}))}Bt.getDataUrlFromFile=bp,Bt.getFilefromDataUrl=Lc,Bt.loadImage=yp,Bt.drawImageInCanvas=xp,Bt.drawFileInCanvas=Ki,Bt.canvasToFile=qi,Bt.getExifOrientation=wp,Bt.handleMaxWidthOrHeight=kp,Bt.followExifOrientation=Cp,Bt.cleanupCanvasMemory=Vn,Bt.isAutoOrientationInBrowser=us,Bt.approximateBelowMaximumCanvasSizeOfBrowser=vp,Bt.copyExifWithoutOrientation=hp,Bt.getBrowserName=Sr,Bt.version="2.0.2";const oE=window.G7Core,$c=t=>{const e=["B","KB","MB","GB"];let n=0,r=t;for(;r>=1024&&n<e.length-1;)r/=1024,n++;return`${r.toFixed(2)} ${e[n]}`},iE=t=>{if(t&&typeof t=="object"){const e=t;if(e.response?.data?.message)return e.response.data.message;if(e.response?.data?.errors){const n=Object.values(e.response.data.errors)[0];if(Array.isArray(n)&&n[0])return n[0]}if(e.message)return e.message}return t instanceof Error?t.message:"업로드 실패"},aE=t=>t?t.startsWith("image/")?"fa-file-image":t.startsWith("video/")?"fa-file-video":t.startsWith("audio/")?"fa-file-audio":t==="application/pdf"?"fa-file-pdf":t.includes("word")||t.includes("document")?"fa-file-word":t.includes("excel")||t.includes("spreadsheet")?"fa-file-excel":t.includes("zip")||t.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",Pt=(t,e)=>oE?.t?.(t,e)??t,_p=window.G7Core?.createLogger?.("Comp:FileUploader")??{log:(...t)=>console.log("[Comp:FileUploader]",...t),warn:(...t)=>console.warn("[Comp:FileUploader]",...t),error:(...t)=>console.error("[Comp:FileUploader]",...t)},_r=window.G7Core;function lE(t){const{attachmentableType:e,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:u,endpoints:f,onFilesChange:g,onUploadComplete:m,onUploadError:p,onRemove:y,onReorder:b,uploadTriggerEvent:_,confirmBeforeRemove:x,uploadParams:E}=t,[S,w]=v.useState(u),[N,C]=v.useState([]),[k,M]=v.useState(!1),T=v.useRef(null),L=v.useRef(0),P=v.useRef(new Set),F=v.useRef(new Map),D=v.useRef(null),[A,O]=v.useState(!1),[V,H]=v.useState(null),[W,j]=v.useState(!1),U=v.useRef(!1),[B,G]=v.useState([]),[z,X]=v.useState(!1),[Y,Z]=v.useState(0),ee=v.useRef(0),[te,ce]=v.useState(new Map),J=v.useRef(new Map),he=v.useRef(g);he.current=g,v.useEffect(()=>{he.current?.(N)},[N]),v.useEffect(()=>{if(U.current)return;const ne=u.filter(ge=>!P.current.has(ge.id)&&(!ge.hash||!P.current.has(ge.hash))),fe=new Set(ne.map(ge=>ge.id)),xe=Array.from(F.current.values()).filter(ge=>!fe.has(ge.id)&&!P.current.has(ge.id));w([...ne,...xe])},[u]);const le=v.useCallback(async ne=>{const fe={maxSizeMB:l?.maxSizeMB??1,maxWidthOrHeight:l?.maxWidthOrHeight??1920,useWebWorker:!0};return await Bt(ne,fe)},[l]),ae=v.useCallback(async ne=>{const fe=S.length+N.length,xe=Math.max(0,s-fe),ge=Array.from(ne).slice(0,xe);if(ne.length>xe){const be=Array.from(ne).slice(xe),Ve=Pt("attachment.upload_limit_exceeded",{max:s});p?.(Ve,be[0]??ne[0])}for(const be of ge){if(be.size>o*1024*1024){p?.(`[${be.name}] 파일 크기(${$c(be.size)})가 최대 허용 크기(${o}MB)를 초과합니다.`,be);continue}if(a){const De="."+be.name.split(".").pop()?.toLowerCase();if(!a.split(",").map(Me=>Me.trim().toLowerCase()).includes(De)){p?.(`[${be.name}] 허용되지 않는 파일 형식입니다. (허용: ${a})`,be);continue}}const Ve={id:`pending-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:be,original_filename:be.name,mime_type:be.type,size:be.size,size_formatted:$c(be.size),status:"pending",progress:0};if(be.type.startsWith("image/")&&(Ve.preview=URL.createObjectURL(be)),C(De=>[...De,Ve]),be.type.startsWith("image/")&&l){C(De=>De.map($e=>$e.id===Ve.id?{...$e,status:"compressing"}:$e));try{const De=await le(be);C($e=>$e.map(Me=>Me.id===Ve.id?{...Me,compressedBlob:De,size:De.size,size_formatted:$c(De.size),status:"pending",progress:0}:Me))}catch{C(De=>De.map($e=>$e.id===Ve.id?{...$e,status:"error",error:"이미지 압축 실패"}:$e))}}}},[S.length,N.length,s,o,a,l,le,p]),Se=v.useCallback(async ne=>{const fe=new FormData,xe=ne.compressedBlob?new File([ne.compressedBlob],ne.original_filename,{type:ne.mime_type}):ne.file;fe.append("file",xe),e&&fe.append("attachmentable_type",e),n&&fe.append("attachmentable_id",String(n)),fe.append("collection",r),c.forEach(ge=>{fe.append("role_ids[]",String(ge))}),E&&Object.entries(E).forEach(([ge,be])=>{fe.append(ge,be)});try{const ge=await _r.api.post(f.upload,fe,{headers:{"Content-Type":"multipart/form-data"},onUploadProgress:Ve=>{const De=Math.round(Ve.loaded*100/(Ve.total??1));C($e=>$e.map(Me=>Me.id===ne.id?{...Me,progress:De}:Me))}});return ge?.data?.data??ge?.data}catch(ge){const be=iE(ge);throw new Error(be)}},[e,n,r,c,f.upload]),ue=v.useCallback(async()=>{const ne=N.filter(De=>De.status==="pending"||De.status==="error");if(ne.length===0)return D.current=S,[];const fe=[],xe=new Map,ge=[...ne],be=async()=>{for(;ge.length>0&&L.current<i;){const De=ge.shift();L.current++,C($e=>$e.map(Me=>Me.id===De.id?{...Me,status:"uploading",progress:0}:Me));try{const $e=await Se(De);if($e)fe.push($e),xe.set(String(De.id),String($e.hash)),F.current.set($e.id,$e),C(Me=>Me.filter(qe=>qe.id!==De.id)),w(Me=>[...Me,$e]),G(Me=>{if(Me.length===0)return Me;const qe=String(De.id);return Me.some(ye=>String(ye)===qe)?Me.map(ye=>String(ye)===qe?String($e.hash):ye):Me}),De.preview&&URL.revokeObjectURL(De.preview);else{const Me=Pt("attachment.upload_response_invalid");C(qe=>qe.map(ot=>ot.id===De.id?{...ot,status:"error",error:Me}:ot)),p?.(`[${De.original_filename}] ${Me}`,De.file)}}catch($e){const Me=$e instanceof Error?$e.message:"업로드 실패";C(qe=>qe.map(ot=>ot.id===De.id?{...ot,status:"error",error:Me}:ot)),p?.(`[${De.original_filename}] ${Me}`,De.file)}finally{L.current--}}},Ve=Array(Math.min(i,ne.length)).fill(null).map(()=>be());if(await Promise.all(Ve),fe.length>0){m?.(fe);const De=[...S,...fe];if(B.length>0){const $e=B.map(ot=>xe.get(String(ot))??ot),Me=new Map($e.map((ot,ye)=>[String(ot),ye])),qe=[...De].sort((ot,ye)=>{const Ce=Me.get(String(ot.hash))??De.length,Fe=Me.get(String(ye.hash))??De.length;return Ce-Fe}).map((ot,ye)=>({...ot,sort_order:ye,order:ye+1}));D.current=qe,f.reorder&&b&&b(qe)}else D.current=De.map(($e,Me)=>({...$e,sort_order:Me,order:Me+1}))}else D.current=S;return fe},[N,i,Se,m,p,B,S,b,f.reorder]);v.useEffect(()=>{if(!_)return;const ne=_r.componentEvent.on(_,async()=>{const fe=await ue(),xe=D.current??[...S,...fe];return{uploadedAttachments:fe,existingFiles:S,allFiles:xe}});return()=>{ne()}},[_,ue,S]),v.useEffect(()=>{d&&N.some(ne=>ne.status==="pending")&&ue()},[d,N,ue]);const pe=v.useCallback(async ne=>{j(!0);try{if(ne.id&&f.delete){const xe=f.delete.replace(":id",String(ne.id));await _r.api.delete(xe)}const fe=ne.hash||ne.id;ne.id&&(P.current.add(ne.id),F.current.delete(ne.id)),ne.hash&&P.current.add(ne.hash),w(xe=>xe.filter(ge=>(ge.hash||ge.id)!==fe)),y?.(ne.hash||ne.id),O(!1),H(null)}catch{_p.error("첨부파일 삭제 실패")}finally{j(!1)}},[y,f.delete]),we=v.useCallback(async ne=>{"hash"in ne?x?(H(ne),O(!0)):await pe(ne):(ne.preview&&URL.revokeObjectURL(ne.preview),C(fe=>fe.filter(xe=>xe.id!==ne.id)))},[x,pe]),He=v.useCallback(ne=>{C(fe=>fe.map(xe=>xe.id===ne.id?{...xe,status:"pending",error:void 0}:xe))},[]),se=v.useCallback(async ne=>{const{arrayMove:fe}=await Promise.resolve().then(()=>DS),{active:xe,over:ge}=ne;if(!ge||xe.id===ge.id)return;const be=[...S,...N];let Ve;if(B.length===0)Ve=be;else{const Ce=new Map(B.map((Fe,nt)=>[String(Fe),nt]));Ve=[...be].sort((Fe,nt)=>{const pt="hash"in Fe?String(Fe.hash):String(Fe.id),it="hash"in nt?String(nt.hash):String(nt.id);return(Ce.get(pt)??be.length)-(Ce.get(it)??be.length)})}const De=Ve.findIndex(Ce=>"hash"in Ce?Ce.hash===xe.id:Ce.id===xe.id),$e=Ve.findIndex(Ce=>"hash"in Ce?Ce.hash===ge.id:Ce.id===ge.id),Me=fe(Ve,De,$e);G(Me.map(Ce=>"hash"in Ce?Ce.hash:Ce.id));const qe=Me.filter(Ce=>"hash"in Ce),ot=Me.filter(Ce=>!("hash"in Ce));U.current=!0,w(qe),C(ot);const ye=qe.map((Ce,Fe)=>({...Ce,sort_order:Fe,order:Fe+1}));if(qe.length>0&&f.reorder){const Ce=qe.map((Fe,nt)=>({id:Fe.id,order:nt+1}));try{const Fe=await _r.api.patch(f.reorder,{order:Ce});_r.toast.success(Fe.message)}catch(Fe){const nt=Fe instanceof Error?Fe.message:Pt("attachment.reorder_failed");_r.toast.error(nt)}}b?.(ye)},[S,N,B,f.reorder,b]),Oe=v.useMemo(()=>[...S,...N].filter(ne=>"is_image"in ne?ne.is_image:ne.mime_type?.startsWith("image/")??!1),[S,N]),Le=v.useCallback(ne=>{const fe=Oe.findIndex(xe=>"hash"in ne&&"hash"in xe?xe.hash===ne.hash:"id"in ne&&"id"in xe&&!("hash"in ne)&&!("hash"in xe)?xe.id===ne.id:!1);fe!==-1&&(ee.current+=1,Z(fe),X(!0))},[Oe]);v.useEffect(()=>{let ne=!1;return(async()=>{const xe=S.filter(be=>be.is_image),ge=new Set(xe.map(be=>be.id));for(const[be,Ve]of J.current)ge.has(be)||(URL.revokeObjectURL(Ve),J.current.delete(be));for(const be of xe){if(ne)break;if(!J.current.has(be.id)){if(Tc(be.download_url)){J.current.set(be.id,be.download_url),ce(new Map(J.current));continue}try{const Ve=await _r.api.get(be.download_url,{responseType:"blob"});if(!ne&&Ve){const De=URL.createObjectURL(Ve);J.current.set(be.id,De),ce(new Map(J.current))}}catch(Ve){_p.error("Failed to load authenticated image:",Ve)}}}})(),()=>{ne=!0}},[S]),v.useEffect(()=>()=>{J.current.forEach(ne=>URL.revokeObjectURL(ne)),J.current.clear()},[]);const Ie=v.useCallback(async ne=>{const{executeImageDownload:fe}=await Promise.resolve().then(()=>K2);await fe({src:ne.download_url,downloadUrl:ne.download_url,filename:ne.original_filename,downloadRequiresAuth:!0})},[]),Ue=v.useMemo(()=>{const ne=[...S,...N];if(B.length===0)return ne;const fe=new Map(B.map((xe,ge)=>[String(xe),ge]));return[...ne].sort((xe,ge)=>{const be="hash"in xe?String(xe.hash):String(xe.id),Ve="hash"in ge?String(ge.hash):String(ge.id),De=fe.get(be)??ne.length,$e=fe.get(Ve)??ne.length;return De-$e})},[S,N,B]),et=Ue.length,bt=et>0,Ge=et<s,ft=v.useCallback(()=>C([]),[]),Be=v.useCallback(()=>N,[N]);return{existingFiles:S,pendingFiles:N,isDragOver:k,setIsDragOver:M,isDeleting:W,totalCount:et,hasFiles:bt,canAddMore:Ge,allItems:Ue,imageFiles:Oe,handleFiles:ae,handleRemove:we,handleRetry:He,handleUploadAll:ue,handleOpenGallery:Le,handleDownload:Ie,handleDragEnd:se,inputRef:T,galleryOpen:z,setGalleryOpen:X,galleryStartIndex:Y,galleryKeyRef:ee,confirmDialogOpen:A,setConfirmDialogOpen:O,itemToDelete:V,executeRemoveAttachment:pe,authenticatedImageUrls:te,clear:ft,getPendingFiles:Be}}const cE=({isDragOver:t,canAddMore:e,onFiles:n,setIsDragOver:r,inputRef:s,accept:o,maxFiles:i,maxSize:a,disabled:l,children:c})=>{const d=e&&!l;return h.jsxs(I,{className:` relative border-2 border-dashed rounded-lg transition-colors ${l?"opacity-60 cursor-not-allowed":""} - ${e&&!l?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 dark:border-gray-600"} - `,onDragOver:u=>{u.preventDefault(),d&&r(!0)},onDragLeave:()=>r(!1),onDrop:u=>{u.preventDefault(),r(!1),d&&n(u.dataTransfer.files)},children:[h.jsx("input",{ref:s,type:"file",multiple:!0,accept:o,onChange:u=>u.target.files&&!l&&n(u.target.files),className:"hidden",disabled:l}),!c&&h.jsxs(I,{className:`p-8 text-center transition-colors ${l?"cursor-not-allowed":"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,onClick:()=>!l&&s.current?.click(),children:[h.jsx(bt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(vt,{className:"text-sm text-gray-600 dark:text-gray-400",children:$t("attachment.drop_or_click")}),h.jsxs(vt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[$t("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},AE=window.G7Core,DE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...e)=>console.log("[Comp:SortableThumbnail]",...e),warn:(...e)=>console.warn("[Comp:SortableThumbnail]",...e),error:(...e)=>console.error("[Comp:SortableThumbnail]",...e)},IE=({file:e,onRemove:t,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Ui({id:"hash"in e?e.hash:e.id}),m={transform:jn.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in e&&e.status!==void 0,y=p?e.status:"done",b=y==="uploading",_=y==="compressing",x=y==="error",M=p?e.progress:100,C=p?e.preview:void 0,k=p?e.mime_type?.startsWith("image/")??!1:e.is_image,[N,S]=v.useState(void 0),w=!p&&k?e.download_url:void 0;v.useEffect(()=>{if(!w){S(void 0);return}let D=!0,P;return(async()=>{try{const $=await AE.api.get(w,{responseType:"blob"});D&&$&&(P=URL.createObjectURL($),S(P))}catch($){DE.error("Failed to load authenticated image:",$)}})(),()=>{D=!1,P&&URL.revokeObjectURL(P)}},[w]);const E=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${k||!p?"cursor-pointer":""}`,onClick:()=>{b||_||(k&&r?r():!p&&s&&s())},children:[C||E?h.jsx(_n,{src:C||E,alt:e.original_filename,className:"w-full h-full object-cover"}):h.jsx(bt,{className:`fa-solid ${NE(e.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||_)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 transition-all",style:{width:`${M}%`}})}),h.jsxs(Q,{className:"text-white text-xs mt-1",children:[M,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:h.jsx(bt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),i()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${o?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:$t(o?"attachment.primary_image":"attachment.set_as_primary"),children:h.jsx(bt,{className:"fa-solid fa-star text-xs"})}),h.jsxs(I,{className:`absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100 ${a?"hidden":""}`,children:[h.jsx(ae,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:$t("attachment.drag_to_reorder"),onClick:D=>D.stopPropagation(),children:h.jsx(bt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:$t("common.download"),children:h.jsx(bt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:$t("common.retry"),children:h.jsx(bt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(ae,{type:"button",onClick:D=>{D.stopPropagation(),t()},disabled:b||_,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:$t("common.delete"),children:h.jsx(bt,{className:"fa-solid fa-times text-sm"})})]})]}),h.jsxs(I,{className:`p-2 ${!p&&s?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&s&&s()},children:[h.jsx(vt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:e.original_filename,children:e.original_filename}),h.jsx(vt,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.size_formatted})]})]})},LE=({allItems:e,canAddMore:t,totalCount:n,maxFiles:r,maxSize:s,accept:o,onRemove:i,onRetry:a,onImageClick:l,onDownload:c,onDragEnd:d,onAddClick:u,enablePrimarySelection:f,primaryFileId:g,onPrimaryChange:m,disabled:p})=>{const y=Ql(us(io,{activationConstraint:{distance:8}}),us(oo,{coordinateGetter:Bi}));return h.jsxs(I,{className:"p-3",children:[h.jsx(lc,{sensors:y,collisionDetection:Jl,onDragEnd:d,children:h.jsx(Hi,{items:e.map(b=>"hash"in b?b.hash:b.id),strategy:zi,children:h.jsxs(I,{className:"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3",children:[e.map(b=>{const _="hash"in b,x=_?b.is_image:b.mime_type?.startsWith("image/")??!1;return h.jsx(IE,{file:b,onRemove:()=>i(b),onRetry:"status"in b&&b.status==="error"?()=>a(b):void 0,onImageClick:x?()=>l(b):void 0,onDownload:_?()=>c(b):void 0,isPrimary:f&&_&&(b.hash||b.id)===g,onPrimaryClick:f&&_?()=>m?.(b.hash||b.id):void 0,disabled:p},_?b.hash:b.id)}),t&&!p&&h.jsxs(I,{className:"aspect-square flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:u,children:[h.jsx(bt,{className:"fa-solid fa-plus text-2xl text-gray-400 dark:text-gray-500 mb-1"}),h.jsx(Q,{className:"text-xs text-gray-500 dark:text-gray-400",children:$t("common.add")})]})]})})}),h.jsxs(I,{className:"mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center text-xs text-gray-500 dark:text-gray-400",children:[h.jsx(Q,{children:$t("attachment.attached_count",{count:n,max:r})}),h.jsxs(Q,{children:[$t("attachment.max_size",{size:s}),o&&` (${o})`]})]})]})},zp=v.forwardRef(({uploadTriggerEvent:e,attachmentableType:t,attachmentableId:n,collection:r="default",maxFiles:s=10,maxSize:o=10,maxConcurrentUploads:i=3,accept:a,imageCompression:l,roleIds:c=[],autoUpload:d=!0,onFilesChange:u,onUploadComplete:f,onUploadError:g,onRemove:m,onReorder:p,initialFiles:y=[],disabled:b=!1,className:_="",confirmBeforeRemove:x=!1,confirmRemoveTitle:M,confirmRemoveMessage:C,apiEndpoints:k,uploadParams:N,enablePrimarySelection:S=!1,primaryFileId:w,onPrimaryChange:E,id:D,editorAttrs:P},L)=>{const $={upload:k?.upload??"/api/admin/attachments",delete:k?.delete??"/api/admin/attachments/:id",reorder:k?.reorder??"/api/admin/attachments/reorder"},A=TE({attachmentableType:t,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:y,endpoints:$,onFilesChange:u,onUploadComplete:f,onUploadError:g,onRemove:m,onReorder:p,uploadTriggerEvent:e,confirmBeforeRemove:x,uploadParams:N});v.useImperativeHandle(L,()=>({uploadAll:A.handleUploadAll,clear:A.clear,getPendingFiles:A.getPendingFiles}));const O=v.useMemo(()=>A.imageFiles.map(z=>{const H=!("hash"in z),j=z,G=z;return H?{src:j.preview||"",filename:j.original_filename,title:j.original_filename,downloadRequiresAuth:!1}:{src:A.authenticatedImageUrls.get(G.id)||G.download_url,downloadUrl:G.download_url,filename:G.original_filename,title:G.original_filename,downloadRequiresAuth:!0}}),[A.imageFiles,A.authenticatedImageUrls]),T=v.useMemo(()=>({existingFiles:A.existingFiles,pendingFiles:A.pendingFiles,isDragOver:A.isDragOver,isDeleting:A.isDeleting,maxFiles:s,maxSize:o,accept:a,endpoints:$,confirmBeforeRemove:x,totalCount:A.totalCount,hasFiles:A.hasFiles,canAddMore:A.canAddMore,allItems:A.allItems,imageFiles:A.imageFiles,handleFiles:A.handleFiles,handleRemove:A.handleRemove,handleRetry:A.handleRetry,handleUploadAll:A.handleUploadAll,handleOpenGallery:A.handleOpenGallery,handleDownload:A.handleDownload,handleDragEnd:A.handleDragEnd,setIsDragOver:A.setIsDragOver,inputRef:A.inputRef,galleryOpen:A.galleryOpen,setGalleryOpen:A.setGalleryOpen,galleryStartIndex:A.galleryStartIndex,galleryKeyRef:A.galleryKeyRef,confirmDialogOpen:A.confirmDialogOpen,setConfirmDialogOpen:A.setConfirmDialogOpen,itemToDelete:A.itemToDelete,executeRemoveAttachment:A.executeRemoveAttachment,authenticatedImageUrls:A.authenticatedImageUrls}),[A,s,o,a,$,x]);return h.jsx(bE,{value:T,children:h.jsxs(I,{id:D,className:_,...P,children:[h.jsx(OE,{isDragOver:A.isDragOver,canAddMore:A.canAddMore,onFiles:A.handleFiles,setIsDragOver:A.setIsDragOver,inputRef:A.inputRef,accept:a,maxFiles:s,maxSize:o,disabled:b,children:A.hasFiles&&h.jsx(LE,{allItems:A.allItems,canAddMore:A.canAddMore,totalCount:A.totalCount,maxFiles:s,maxSize:o,accept:a,onRemove:A.handleRemove,onRetry:A.handleRetry,onImageClick:A.handleOpenGallery,onDownload:A.handleDownload,onDragEnd:A.handleDragEnd,onAddClick:()=>A.inputRef.current?.click(),enablePrimarySelection:S,primaryFileId:w,onPrimaryChange:E,disabled:b})}),x&&h.jsx(pu,{isOpen:A.confirmDialogOpen,onClose:()=>{A.isDeleting||A.setConfirmDialogOpen(!1)},title:M??$t("attachment.delete_confirm_title"),message:C??$t("attachment.delete_confirm_message"),confirmText:$t("common.delete"),confirmButtonVariant:"danger",onConfirm:()=>{A.itemToDelete&&A.executeRemoveAttachment(A.itemToDelete)},isLoading:A.isDeleting,loadingText:$t("attachment.deleting")}),O.length>0&&A.galleryOpen&&h.jsx(Qi,{images:O,isOpen:!0,onClose:()=>A.setGalleryOpen(!1),startIndex:A.galleryStartIndex,showDownload:!0},`gallery-${A.galleryKeyRef.current}`)]})})});zp.displayName="FileUploader";const PE=({name:e,value:t,options:n,onChange:r,disabled:s=!1,inline:o=!1,className:i="",size:a="md",label:l,error:c,id:d,editorAttrs:u})=>{const[f,g]=v.useState(()=>t??"");v.useEffect(()=>{g(t??"")},[t]);const m=_=>{const x=_.target.value;g(x),r&&!s&&r(_)},y={sm:{radio:"w-3.5 h-3.5",label:"text-xs",gap:"gap-1.5"},md:{radio:"w-4 h-4",label:"text-sm",gap:"gap-2"},lg:{radio:"w-5 h-5",label:"text-base",gap:"gap-2.5"}}[a],b=o?"flex flex-wrap items-center gap-4":"flex flex-col gap-2";return h.jsxs(I,{className:i,id:d,...u,children:[l&&h.jsx(Q,{className:`block mb-2 font-medium text-gray-700 dark:text-gray-300 ${y.label}`,children:l}),h.jsx(I,{className:b,children:n.map(_=>{const x=String(_.value)===String(f),M=s||_.disabled;return h.jsxs(ln,{className:`inline-flex items-center ${y.gap} ${M?"cursor-not-allowed opacity-50":"cursor-pointer"}`,children:[h.jsx("input",{type:"radio",name:e,value:_.value,checked:x,onChange:m,disabled:M,className:` + ${t&&!l?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 dark:border-gray-600"} + `,onDragOver:u=>{u.preventDefault(),d&&r(!0)},onDragLeave:()=>r(!1),onDrop:u=>{u.preventDefault(),r(!1),d&&n(u.dataTransfer.files)},children:[h.jsx("input",{ref:s,type:"file",multiple:!0,accept:o,onChange:u=>u.target.files&&!l&&n(u.target.files),className:"hidden",disabled:l}),!c&&h.jsxs(I,{className:`p-8 text-center transition-colors ${l?"cursor-not-allowed":"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50"}`,onClick:()=>!l&&s.current?.click(),children:[h.jsx(gt,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),h.jsx(yt,{className:"text-sm text-gray-600 dark:text-gray-400",children:Pt("attachment.drop_or_click")}),h.jsxs(yt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[Pt("attachment.upload_limit",{maxFiles:i,maxSize:a}),o&&` (${o})`]})]}),c]})},dE=window.G7Core,uE=window.G7Core?.createLogger?.("Comp:SortableThumbnail")??{log:(...t)=>console.log("[Comp:SortableThumbnail]",...t),warn:(...t)=>console.warn("[Comp:SortableThumbnail]",...t),error:(...t)=>console.error("[Comp:SortableThumbnail]",...t)},fE=({file:t,onRemove:e,onRetry:n,onImageClick:r,onDownload:s,isPrimary:o,onPrimaryClick:i,disabled:a})=>{const{attributes:l,listeners:c,setNodeRef:d,transform:u,transition:f,isDragging:g}=Ii({id:"hash"in t?t.hash:t.id}),m={transform:Fn.Transform.toString(u),transition:f,opacity:g?.5:1},p="status"in t&&t.status!==void 0,y=p?t.status:"done",b=y==="uploading",_=y==="compressing",x=y==="error",E=p?t.progress:100,S=p?t.preview:void 0,w=p?t.mime_type?.startsWith("image/")??!1:t.is_image,[N,C]=v.useState(void 0),k=!p&&w?t.download_url:void 0;v.useEffect(()=>{if(!k){C(void 0);return}if(Tc(k)){C(k);return}let T=!0,L;return(async()=>{try{const F=await dE.api.get(k,{responseType:"blob"});T&&F&&(L=URL.createObjectURL(F),C(L))}catch(F){uE.error("Failed to load authenticated image:",F)}})(),()=>{T=!1,L&&URL.revokeObjectURL(L)}},[k]);const M=N;return h.jsxs(I,{ref:d,style:m,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[h.jsxs(I,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${w||!p?"cursor-pointer":""}`,onClick:()=>{b||_||(w&&r?r():!p&&s&&s())},children:[S||M?h.jsx(kn,{src:S||M,alt:t.original_filename,className:"w-full h-full object-cover"}):h.jsx(gt,{className:`fa-solid ${aE(t.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(b||_)&&h.jsxs(I,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[h.jsx(I,{className:"w-3/4 h-2 bg-gray-700 dark:bg-gray-600 rounded-full overflow-hidden",children:h.jsx(I,{className:"h-full bg-blue-500 dark:bg-blue-400 transition-all",style:{width:`${E}%`}})}),h.jsxs(Q,{className:"text-white text-xs mt-1",children:[E,"%"]})]}),x&&h.jsx(I,{className:"absolute inset-0 bg-red-500/50 dark:bg-red-600/50 flex items-center justify-center",children:h.jsx(gt,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),i&&h.jsx(oe,{type:"button",onClick:T=>{T.stopPropagation(),i()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${o?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:Pt(o?"attachment.primary_image":"attachment.set_as_primary"),children:h.jsx(gt,{className:"fa-solid fa-star text-xs"})}),h.jsxs(I,{className:`absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100 ${a?"hidden":""}`,children:[h.jsx(oe,{type:"button",...l,...c,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:Pt("attachment.drag_to_reorder"),onClick:T=>T.stopPropagation(),children:h.jsx(gt,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&s&&h.jsx(oe,{type:"button",onClick:T=>{T.stopPropagation(),s()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:Pt("common.download"),children:h.jsx(gt,{className:"fa-solid fa-download text-sm"})}),x&&n&&h.jsx(oe,{type:"button",onClick:T=>{T.stopPropagation(),n()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:Pt("common.retry"),children:h.jsx(gt,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),h.jsx(oe,{type:"button",onClick:T=>{T.stopPropagation(),e()},disabled:b||_,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:Pt("common.delete"),children:h.jsx(gt,{className:"fa-solid fa-times text-sm"})})]})]}),h.jsxs(I,{className:`p-2 ${!p&&s?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&s&&s()},children:[h.jsx(yt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:t.original_filename,children:t.original_filename}),h.jsx(yt,{className:"text-xs text-gray-500 dark:text-gray-400",children:t.size_formatted})]})]})},hE=({allItems:t,canAddMore:e,totalCount:n,maxFiles:r,maxSize:s,accept:o,onRemove:i,onRetry:a,onImageClick:l,onDownload:c,onDragEnd:d,onAddClick:u,enablePrimarySelection:f,primaryFileId:g,onPrimaryChange:m,disabled:p})=>{const y=Wl(es(Zs,{activationConstraint:{distance:8}}),es(qs,{coordinateGetter:Li}));return h.jsxs(I,{className:"p-3",children:[h.jsx(tc,{sensors:y,collisionDetection:Gl,onDragEnd:d,children:h.jsx(Di,{items:t.map(b=>"hash"in b?b.hash:b.id),strategy:Ti,children:h.jsxs(I,{className:"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3",children:[t.map(b=>{const _="hash"in b,x=_?b.is_image:b.mime_type?.startsWith("image/")??!1;return h.jsx(fE,{file:b,onRemove:()=>i(b),onRetry:"status"in b&&b.status==="error"?()=>a(b):void 0,onImageClick:x?()=>l(b):void 0,onDownload:_?()=>c(b):void 0,isPrimary:f&&_&&(b.hash||b.id)===g,onPrimaryClick:f&&_?()=>m?.(b.hash||b.id):void 0,disabled:p},_?b.hash:b.id)}),e&&!p&&h.jsxs(I,{className:"aspect-square flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:u,children:[h.jsx(gt,{className:"fa-solid fa-plus text-2xl text-gray-400 dark:text-gray-500 mb-1"}),h.jsx(Q,{className:"text-xs text-gray-500 dark:text-gray-400",children:Pt("common.add")})]})]})})}),h.jsxs(I,{className:"mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center text-xs text-gray-500 dark:text-gray-400",children:[h.jsx(Q,{children:Pt("attachment.attached_count",{count:n,max:r})}),h.jsxs(Q,{children:[Pt("attachment.max_size",{size:s}),o&&` (${o})`]})]})]})},Ep=v.forwardRef(({uploadTriggerEvent:t,attachmentableType:e,attachmentableId:n,collection:r="default",maxFiles:s=10,maxSize:o=10,maxConcurrentUploads:i=3,accept:a,imageCompression:l,roleIds:c=[],autoUpload:d=!0,onFilesChange:u,onUploadComplete:f,onUploadError:g,onRemove:m,onReorder:p,initialFiles:y=[],disabled:b=!1,className:_="",confirmBeforeRemove:x=!1,confirmRemoveTitle:E,confirmRemoveMessage:S,apiEndpoints:w,uploadParams:N,enablePrimarySelection:C=!1,primaryFileId:k,onPrimaryChange:M,id:T,editorAttrs:L},P)=>{const F={upload:w?.upload??"/api/admin/attachments",delete:w?.delete??"/api/admin/attachments/:id",reorder:w?.reorder??"/api/admin/attachments/reorder"},D=lE({attachmentableType:e,attachmentableId:n,collection:r,maxFiles:s,maxSize:o,maxConcurrentUploads:i,accept:a,imageCompression:l,roleIds:c,autoUpload:d,initialFiles:y,endpoints:F,onFilesChange:u,onUploadComplete:f,onUploadError:g,onRemove:m,onReorder:p,uploadTriggerEvent:t,confirmBeforeRemove:x,uploadParams:N});v.useImperativeHandle(P,()=>({uploadAll:D.handleUploadAll,clear:D.clear,getPendingFiles:D.getPendingFiles}));const A=v.useMemo(()=>D.imageFiles.map(V=>{const H=!("hash"in V),W=V,j=V;return H?{src:W.preview||"",filename:W.original_filename,title:W.original_filename,downloadRequiresAuth:!1}:{src:D.authenticatedImageUrls.get(j.id)||j.download_url,downloadUrl:j.download_url,filename:j.original_filename,title:j.original_filename,downloadRequiresAuth:!0}}),[D.imageFiles,D.authenticatedImageUrls]),O=v.useMemo(()=>({existingFiles:D.existingFiles,pendingFiles:D.pendingFiles,isDragOver:D.isDragOver,isDeleting:D.isDeleting,maxFiles:s,maxSize:o,accept:a,endpoints:F,confirmBeforeRemove:x,totalCount:D.totalCount,hasFiles:D.hasFiles,canAddMore:D.canAddMore,allItems:D.allItems,imageFiles:D.imageFiles,handleFiles:D.handleFiles,handleRemove:D.handleRemove,handleRetry:D.handleRetry,handleUploadAll:D.handleUploadAll,handleOpenGallery:D.handleOpenGallery,handleDownload:D.handleDownload,handleDragEnd:D.handleDragEnd,setIsDragOver:D.setIsDragOver,inputRef:D.inputRef,galleryOpen:D.galleryOpen,setGalleryOpen:D.setGalleryOpen,galleryStartIndex:D.galleryStartIndex,galleryKeyRef:D.galleryKeyRef,confirmDialogOpen:D.confirmDialogOpen,setConfirmDialogOpen:D.setConfirmDialogOpen,itemToDelete:D.itemToDelete,executeRemoveAttachment:D.executeRemoveAttachment,authenticatedImageUrls:D.authenticatedImageUrls}),[D,s,o,a,F,x]);return h.jsx(Z2,{value:O,children:h.jsxs(I,{id:T,className:_,...L,children:[h.jsx(cE,{isDragOver:D.isDragOver,canAddMore:D.canAddMore,onFiles:D.handleFiles,setIsDragOver:D.setIsDragOver,inputRef:D.inputRef,accept:a,maxFiles:s,maxSize:o,disabled:b,children:D.hasFiles&&h.jsx(hE,{allItems:D.allItems,canAddMore:D.canAddMore,totalCount:D.totalCount,maxFiles:s,maxSize:o,accept:a,onRemove:D.handleRemove,onRetry:D.handleRetry,onImageClick:D.handleOpenGallery,onDownload:D.handleDownload,onDragEnd:D.handleDragEnd,onAddClick:()=>D.inputRef.current?.click(),enablePrimarySelection:C,primaryFileId:k,onPrimaryChange:M,disabled:b})}),x&&h.jsx(ou,{isOpen:D.confirmDialogOpen,onClose:()=>{D.isDeleting||D.setConfirmDialogOpen(!1)},title:E??Pt("attachment.delete_confirm_title"),message:S??Pt("attachment.delete_confirm_message"),confirmText:Pt("common.delete"),confirmButtonVariant:"danger",onConfirm:()=>{D.itemToDelete&&D.executeRemoveAttachment(D.itemToDelete)},isLoading:D.isDeleting,loadingText:Pt("attachment.deleting")}),A.length>0&&D.galleryOpen&&h.jsx(ji,{images:A,isOpen:!0,onClose:()=>D.setGalleryOpen(!1),startIndex:D.galleryStartIndex,showDownload:!0},`gallery-${D.galleryKeyRef.current}`)]})})});Ep.displayName="FileUploader";const gE=({name:t,value:e,options:n,onChange:r,disabled:s=!1,inline:o=!1,className:i="",size:a="md",label:l,error:c,id:d,editorAttrs:u})=>{const[f,g]=v.useState(()=>e??"");v.useEffect(()=>{g(e??"")},[e]);const m=_=>{const x=_.target.value;g(x),r&&!s&&r(_)},y={sm:{radio:"w-3.5 h-3.5",label:"text-xs",gap:"gap-1.5"},md:{radio:"w-4 h-4",label:"text-sm",gap:"gap-2"},lg:{radio:"w-5 h-5",label:"text-base",gap:"gap-2.5"}}[a],b=o?"flex flex-wrap items-center gap-4":"flex flex-col gap-2";return h.jsxs(I,{className:i,id:d,...u,children:[l&&h.jsx(Q,{className:`block mb-2 font-medium text-gray-700 dark:text-gray-300 ${y.label}`,children:l}),h.jsx(I,{className:b,children:n.map(_=>{const x=String(_.value)===String(f),E=s||_.disabled;return h.jsxs(on,{className:`inline-flex items-center ${y.gap} ${E?"cursor-not-allowed opacity-50":"cursor-pointer"}`,children:[h.jsx("input",{type:"radio",name:t,value:_.value,checked:x,onChange:m,disabled:E,className:` ${y.radio} text-blue-600 bg-gray-100 dark:bg-gray-700 @@ -233,15 +239,15 @@ self.addEventListener('message', async (e) => { focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2 dark:ring-offset-gray-800 - ${M?"cursor-not-allowed":"cursor-pointer"} + ${E?"cursor-not-allowed":"cursor-pointer"} `}),h.jsx(Q,{className:` ${y.label} font-medium text-gray-900 dark:text-gray-300 - `,children:_.label})]},_.value)})}),c&&h.jsx(Q,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},Cs=()=>window.G7Core,sa=(e,t)=>Cs()?.t?.(e,t)??e,$E=()=>Cs()?.locale?.supported?.()??["ko","en"],FE=()=>Cs()?.locale?.current?.()??"ko",Vp=e=>{const t=sa(`common.language_${e}`);return t===`common.language_${e}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[e]||e.toUpperCase():t},zE=({content:e="",value:t,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const _=v.useMemo(()=>$E(),[]),x=v.useMemo(()=>FE(),[]),[M,C]=v.useState(x),[k,N]=v.useState(s),[S,w]=v.useState(e),[E,D]=v.useState(t??{}),[P,L]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||w(e)},[e,r]),v.useEffect(()=>{r&&t&&D(t)},[t,r]);const $=v.useCallback(G=>{const U=G.target.value;if(w(U),n){const W=Cs()?.createChangeEvent?.({value:U,name:g,type:"textarea"})??{target:{name:g,value:U}};n(W)}},[n,g]),A=v.useCallback((G,U)=>{const W={...E,[G]:U};if(D(W),n){const Y=Cs()?.createChangeEvent?.({value:W,name:g,type:"multilingual"})??{target:{name:g,value:W}};n(Y)}},[n,g,E]),O=v.useCallback(G=>{const U=G.target.checked;if(N(U),U||L(!1),o){const W=Cs()?.createChangeEvent?.({checked:U,name:m,type:"checkbox"})??{target:{name:m,checked:U}};o(W)}},[o,m]),T=v.useCallback(()=>{L(G=>!G)},[]),z=r?E[M]??"":S,H=v.useCallback(G=>!!E[G]?.trim(),[E]),j=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:_.map(G=>{const U=G===x,W=M===G;return h.jsxs(ae,{type:"button",className:` + `,children:_.label})]},_.value)})}),c&&h.jsx(Q,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:c})]})},fs=()=>window.G7Core,Zi=(t,e)=>fs()?.t?.(t,e)??t,pE=()=>fs()?.locale?.supported?.()??["ko","en"],mE=()=>fs()?.locale?.current?.()??"ko",Mp=t=>{const e=Zi(`common.language_${t}`);return e===`common.language_${t}`?{ko:"한국어",en:"English",ja:"日本語",zh:"中文"}[t]||t.toUpperCase():e},bE=({content:t="",value:e,onChange:n,multilingual:r=!1,isHtml:s=!1,onIsHtmlChange:o,rows:i=15,placeholder:a="",label:l,showHtmlModeToggle:c=!0,contentClassName:d="",purifyConfig:u,className:f="",name:g="content",htmlFieldName:m="content_mode",readOnly:p=!1,id:y,editorAttrs:b})=>{const _=v.useMemo(()=>pE(),[]),x=v.useMemo(()=>mE(),[]),[E,S]=v.useState(x),[w,N]=v.useState(s),[C,k]=v.useState(t),[M,T]=v.useState(e??{}),[L,P]=v.useState(!1);v.useEffect(()=>{N(s)},[s]),v.useEffect(()=>{r||k(t)},[t,r]),v.useEffect(()=>{r&&e&&T(e)},[e,r]);const F=v.useCallback(j=>{const U=j.target.value;if(k(U),n){const B=fs()?.createChangeEvent?.({value:U,name:g,type:"textarea"})??{target:{name:g,value:U}};n(B)}},[n,g]),D=v.useCallback((j,U)=>{const B={...M,[j]:U};if(T(B),n){const G=fs()?.createChangeEvent?.({value:B,name:g,type:"multilingual"})??{target:{name:g,value:B}};n(G)}},[n,g,M]),A=v.useCallback(j=>{const U=j.target.checked;if(N(U),U||P(!1),o){const B=fs()?.createChangeEvent?.({checked:U,name:m,type:"checkbox"})??{target:{name:m,checked:U}};o(B)}},[o,m]),O=v.useCallback(()=>{P(j=>!j)},[]),V=r?M[E]??"":C,H=v.useCallback(j=>!!M[j]?.trim(),[M]),W=()=>h.jsx(I,{className:"flex flex-wrap items-center gap-2 mb-3",children:_.map(j=>{const U=j===x,B=E===j;return h.jsxs(oe,{type:"button",className:` inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-semibold transition-all - ${W?U?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":U?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"} - `,onClick:()=>C(G),children:[h.jsx(oe,{name:"globe",className:"w-3 h-3"}),h.jsx(Q,{children:Vp(G)}),U&&h.jsx(Q,{className:"text-red-500",children:"*"}),H(G)&&!W&&h.jsx(oe,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},G)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(ln,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[k&&!p&&h.jsx(ae,{type:"button",onClick:T,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${P?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:sa(P?"common.preview_off":"common.preview")}),c&&h.jsxs(ln,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(St,{type:"checkbox",name:m,checked:k,onChange:O,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:sa("common.html_mode")})]})]})]}),r&&j(),!P&&(r?_.map(G=>h.jsx(I,{className:M===G?"block":"hidden",children:h.jsx(ir,{name:`${g}[${G}]`,value:E[G]??"",onChange:U=>A(G,U.target.value),placeholder:`${a} (${Vp(G)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},G)):h.jsx(ir,{name:g,value:S,onChange:$,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${k?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),P&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(ol,{content:z,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Hp="g7_dismissed_warnings";function Up(){try{const e=sessionStorage.getItem(Hp);return e?JSON.parse(e):[]}catch{return[]}}function VE(e){try{const t=Up();t.includes(e)||(t.push(e),sessionStorage.setItem(Hp,JSON.stringify(t)))}catch{}}function HE(e){switch(e){case"error":return"error";case"warning":return"warning";default:return"info"}}const UE=({warnings:e,className:t=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Up())},[]);const s=v.useCallback(i=>{VE(i),r(a=>[...a,i])},[]);if(!e||e.length===0)return null;const o=e.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${t}`.trim(),children:o.map(i=>h.jsx(wu,{type:HE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Bp=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...e)=>console.log("[Comp:FilterVisibility]",...e),warn:(...e)=>console.warn("[Comp:FilterVisibility]",...e),error:(...e)=>console.error("[Comp:FilterVisibility]",...e)},BE=({id:e,visibleFilters:t,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${e}${a!==void 0?`_${a}`:""}`,[e,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Bp.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Bp.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(t&&t.length>0?t:n)},[c,t,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&t&&t.length>0){if(i.current){i.current=!1;return}d(t)}},[s,t,d]),null},jE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...e)=>console.log("[Comp:SlotContainer]",...e),warn:(...e)=>console.warn("[Comp:SlotContainer]",...e),error:(...e)=>console.error("[Comp:SlotContainer]",...e)};function WE(e,t){return t&&e&&typeof e.id=="string"&&e.id.length>0?{...e,id:`${e.id}__${t}`}:e}const GE=({slotId:e,className:t="",emptyContent:n,style:r,id:s,children:o})=>{const[i,a]=v.useState([]),[,l]=v.useState({}),c=v.useCallback(()=>{const m=window.__slotContextValue;if(m)return m;const p=window.G7Core;return p&&p.getSlotContext?.()||null},[]),d=v.useCallback(()=>{const m=c();m&&m.isEnabled&&a(m.getSlotComponents(e))},[e,c]);v.useEffect(()=>{const m=c();return!m||!m.isEnabled?void 0:(d(),m.subscribeToSlot(e,()=>{d(),l({})}))},[e,c,d]);const u=v.useCallback(()=>{const m=window.G7Core;return m?m.getDynamicRenderer?.():null},[]);if(i.length===0)return n?h.jsx(I,{className:t,style:r,id:s,children:n}):o?h.jsx(I,{className:t,style:r,id:s,children:o}):null;const f=u(),g=window.G7Core;return!f||!g?(jE.error("G7Core or DynamicRenderer not available"),null):h.jsxs(I,{className:t,style:r,id:s,children:[i.map(m=>{const{componentDef:p,dataContext:y,parentFormContext:b,getParentComponentContext:_,translationContext:x}=m,M=WE(p,s);return h.jsx(f,{componentDef:M,dataContext:y,translationContext:x,registry:g.getComponentRegistry(),bindingEngine:g.getDataBindingEngine(),translationEngine:g.getTranslationEngine(),actionDispatcher:g.getActionDispatcher(),parentComponentContext:_?.(),parentFormContextProp:b,isRootRenderer:!1},M.id||`slot-${e}-${m.order}`)}),o]})},jp=(e,t)=>window.G7Core?.t?.(e,t)??e,Wp=(e,t,n)=>window.G7Core?.renderComponentLayout?.(e,t,n)??null,YE=({options:e=[],value:t,onChange:n,placeholder:r,disabled:s=!1,className:o="",maxHeight:i="320px",children:a,selectedChildren:l,__componentLayoutDefs:c,id:d,editorAttrs:u})=>{const[f,g]=v.useState(!1),m=v.useRef(null),p=v.useRef(null),y=v.useMemo(()=>t==null?null:e.find(k=>String(k.value)===String(t))||null,[e,t]);v.useEffect(()=>{const k=N=>{m.current&&!m.current.contains(N.target)&&g(!1)};return f&&document.addEventListener("mousedown",k),()=>{document.removeEventListener("mousedown",k)}},[f]),v.useEffect(()=>{const k=N=>{N.key==="Escape"&&f&&(g(!1),p.current?.focus())};return document.addEventListener("keydown",k),()=>document.removeEventListener("keydown",k)},[f]);const b=v.useCallback(()=>{s||g(k=>!k)},[s]),_=v.useCallback(k=>{n&&n({target:{value:k},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),p.current?.focus()},[n]),x=r??jp("common.select"),M=()=>{if(!y)return h.jsx(Q,{className:"text-gray-400 dark:text-gray-500",children:x});if(c?.selected){const k=Wp(c.selected,{item:y,isSelected:!0},"selected");if(k)return k}return l||h.jsx(Q,{className:"truncate",children:y.label})},C=(k,N)=>h.jsxs(I,{className:"flex items-center justify-between w-full",children:[h.jsx(Q,{className:N?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-900 dark:text-white",children:k.label}),N&&h.jsx(Zn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]});return h.jsxs(I,{ref:m,className:`relative ${o}`,id:d,...u,children:[h.jsxs(ae,{ref:p,type:"button",onClick:b,disabled:s,className:` + ${B?U?"bg-blue-500 text-white dark:bg-blue-600 dark:text-white shadow-md scale-105":"bg-gray-500 text-white dark:bg-gray-600 dark:text-white shadow-md scale-105":U?"bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/50":"bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"} + `,onClick:()=>S(j),children:[h.jsx(re,{name:"globe",className:"w-3 h-3"}),h.jsx(Q,{children:Mp(j)}),U&&h.jsx(Q,{className:"text-red-500",children:"*"}),H(j)&&!B&&h.jsx(re,{name:"check",className:"w-3 h-3 text-green-500 dark:text-green-400"})]},j)})});return h.jsxs(I,{className:`space-y-2 ${f}`,id:y,...b,children:[h.jsxs(I,{className:"flex items-center justify-between",children:[l&&h.jsx(on,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:l}),h.jsxs(I,{className:"flex items-center gap-3",children:[w&&!p&&h.jsx(oe,{type:"button",onClick:O,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${L?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:Zi(L?"common.preview_off":"common.preview")}),c&&h.jsxs(on,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[h.jsx(wt,{type:"checkbox",name:m,checked:w,onChange:A,disabled:p,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),h.jsx(I,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:Zi("common.html_mode")})]})]})]}),r&&W(),!L&&(r?_.map(j=>h.jsx(I,{className:E===j?"block":"hidden",children:h.jsx(Rn,{name:`${g}[${j}]`,value:M[j]??"",onChange:U=>D(j,U.target.value),placeholder:`${a} (${Mp(j)})`,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${w?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})},j)):h.jsx(Rn,{name:g,value:C,onChange:F,placeholder:a,rows:i,readOnly:p,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${w?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`})),L&&h.jsx(I,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:h.jsx(Ja,{content:V,isHtml:!0,className:d||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:u})})]})},Np="g7_dismissed_warnings";function Op(){try{const t=sessionStorage.getItem(Np);return t?JSON.parse(t):[]}catch{return[]}}function yE(t){try{const e=Op();e.includes(t)||(e.push(t),sessionStorage.setItem(Np,JSON.stringify(e)))}catch{}}function vE(t){switch(t){case"error":return"error";case"warning":return"warning";case"info":default:return"info"}}const xE=({warnings:t,className:e=""})=>{const[n,r]=v.useState([]);v.useEffect(()=>{r(Op())},[]);const s=v.useCallback(i=>{yE(i),r(a=>[...a,i])},[]);if(!t||t.length===0)return null;const o=t.filter(i=>!n.includes(i.id));return o.length===0?null:h.jsx(I,{className:`space-y-2 ${e}`.trim(),children:o.map(i=>h.jsx(uu,{type:vE(i.level),message:i.message,dismissible:!0,onDismiss:()=>s(i.id)},i.id))})},Tp=window.G7Core?.createLogger?.("Comp:FilterVisibility")??{log:(...t)=>console.log("[Comp:FilterVisibility]",...t),warn:(...t)=>console.warn("[Comp:FilterVisibility]",...t),error:(...t)=>console.error("[Comp:FilterVisibility]",...t)},wE=({id:t,visibleFilters:e,defaultFilters:n=[],onFilterVisibilityChange:r})=>{const[s,o]=v.useState(!1),i=v.useRef(!0),a=v.useMemo(()=>{try{const f=window.G7Core?.AuthManager?.getInstance();return f?f.getUser()?.uuid:void 0}catch{return}},[]),l=v.useMemo(()=>`g7_filters_${t}${a!==void 0?`_${a}`:""}`,[t,a]),c=v.useCallback(()=>{try{const f=localStorage.getItem(l);if(f){const g=JSON.parse(f);if(Array.isArray(g))return g}}catch(f){Tp.warn("Failed to load from localStorage:",f)}return null},[l]),d=v.useCallback(f=>{try{localStorage.setItem(l,JSON.stringify(f))}catch(g){Tp.warn("Failed to save to localStorage:",g)}},[l]),u=v.useCallback(()=>{const f=c();return f||(e&&e.length>0?e:n)},[c,e,n]);return v.useEffect(()=>{if(!s){const f=u();r&&f.length>0&&r(f),o(!0)}},[s,r,u]),v.useEffect(()=>{if(s&&e&&e.length>0){if(i.current){i.current=!1;return}d(e)}},[s,e,d]),null},kE=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...t)=>console.log("[Comp:SlotContainer]",...t),warn:(...t)=>console.warn("[Comp:SlotContainer]",...t),error:(...t)=>console.error("[Comp:SlotContainer]",...t)};function CE(t,e){return e&&t&&typeof t.id=="string"&&t.id.length>0?{...t,id:`${t.id}__${e}`}:t}const SE=({slotId:t,className:e="",emptyContent:n,style:r,id:s,children:o})=>{const[i,a]=v.useState([]),[,l]=v.useState({}),c=v.useCallback(()=>{const m=window.__slotContextValue;if(m)return m;const p=window.G7Core;return p&&p.getSlotContext?.()||null},[]),d=v.useCallback(()=>{const m=c();m&&m.isEnabled&&a(m.getSlotComponents(t))},[t,c]);v.useEffect(()=>{const m=c();return!m||!m.isEnabled?void 0:(d(),m.subscribeToSlot(t,()=>{d(),l({})}))},[t,c,d]);const u=v.useCallback(()=>{const m=window.G7Core;return m?m.getDynamicRenderer?.():null},[]);if(i.length===0)return n?h.jsx(I,{className:e,style:r,id:s,children:n}):o?h.jsx(I,{className:e,style:r,id:s,children:o}):null;const f=u(),g=window.G7Core;return!f||!g?(kE.error("G7Core or DynamicRenderer not available"),null):h.jsxs(I,{className:e,style:r,id:s,children:[i.map(m=>{const{componentDef:p,dataContext:y,parentFormContext:b,getParentComponentContext:_,translationContext:x}=m,E=CE(p,s);return h.jsx(f,{componentDef:E,dataContext:y,translationContext:x,registry:g.getComponentRegistry(),bindingEngine:g.getDataBindingEngine(),translationEngine:g.getTranslationEngine(),actionDispatcher:g.getActionDispatcher(),parentComponentContext:_?.(),parentFormContextProp:b,isRootRenderer:!1},E.id||`slot-${t}-${m.order}`)}),o]})},Ap=(t,e)=>window.G7Core?.t?.(t,e)??t,Dp=(t,e,n)=>window.G7Core?.renderComponentLayout?.(t,e,n)??null,_E=({options:t=[],value:e,onChange:n,placeholder:r,disabled:s=!1,className:o="",maxHeight:i="320px",children:a,selectedChildren:l,__componentLayoutDefs:c,id:d,editorAttrs:u})=>{const[f,g]=v.useState(!1),m=v.useRef(null),p=v.useRef(null),y=v.useMemo(()=>e==null?null:t.find(w=>String(w.value)===String(e))||null,[t,e]);v.useEffect(()=>{const w=N=>{m.current&&!m.current.contains(N.target)&&g(!1)};return f&&document.addEventListener("mousedown",w),()=>{document.removeEventListener("mousedown",w)}},[f]),v.useEffect(()=>{const w=N=>{N.key==="Escape"&&f&&(g(!1),p.current?.focus())};return document.addEventListener("keydown",w),()=>document.removeEventListener("keydown",w)},[f]);const b=v.useCallback(()=>{s||g(w=>!w)},[s]),_=v.useCallback(w=>{n&&n({target:{value:w},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),p.current?.focus()},[n]),x=r??Ap("common.select"),E=()=>{if(!y)return h.jsx(Q,{className:"text-gray-400 dark:text-gray-500",children:x});if(c?.selected){const w=Dp(c.selected,{item:y,isSelected:!0},"selected");if(w)return w}return l||h.jsx(Q,{className:"truncate",children:y.label})},S=(w,N)=>h.jsxs(I,{className:"flex items-center justify-between w-full",children:[h.jsx(Q,{className:N?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-900 dark:text-white",children:w.label}),N&&h.jsx(Wn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]});return h.jsxs(I,{ref:m,className:`relative ${o}`,id:d,...u,children:[h.jsxs(oe,{ref:p,type:"button",onClick:b,disabled:s,className:` w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl @@ -250,13 +256,27 @@ self.addEventListener('message', async (e) => { flex items-center justify-between gap-2 text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed - `,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(I,{className:"flex-1 min-w-0",children:M()}),h.jsx(Zn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:e.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:jp("common.no_options")}):e.map((k,N)=>{const S=String(k.value)===String(t);return h.jsx(ae,{type:"button",onClick:()=>!k.disabled&&_(k.value),disabled:k.disabled,className:` + `,"aria-haspopup":"listbox","aria-expanded":f,children:[h.jsx(I,{className:"flex-1 min-w-0",children:E()}),h.jsx(Wn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${f?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:h.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),f&&h.jsx(I,{className:"absolute z-50 w-full mt-2 bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",role:"listbox",children:h.jsx(I,{className:"overflow-auto",style:{maxHeight:i},children:t.length===0?h.jsx(I,{className:"px-4 py-6 text-center text-gray-500 dark:text-gray-400",children:Ap("common.no_options")}):t.map((w,N)=>{const C=String(w.value)===String(e);return h.jsx(oe,{type:"button",onClick:()=>!w.disabled&&_(w.value),disabled:w.disabled,className:` w-full px-4 py-3 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors - ${S?"bg-blue-50 dark:bg-blue-900/20":""} - ${k.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"} - `,role:"option","aria-selected":S,"data-item-value":k.value,"data-item-index":N,"data-item-selected":S,children:c?.item?Wp(c.item,{item:k,index:N,isSelected:S},`item-${N}`):a||C(k,S)},k.value)})})})]})};function XE(e){const t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}const KE=({value:e,checked:t=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=XE(o),_=document.documentElement.classList.contains("dark");let x;return t?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?_?.25:.12:_?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${_?.3:.2})`}:{},h.jsxs(ln,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(St,{type:"checkbox",value:e,checked:t,onChange:M=>{l?.(M)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=t?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(ln,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(St,{type:"checkbox",value:e,checked:t,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(oe,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},qE=({label:e,icon:t,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const _=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:_,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,_=u.current?.contains(b),x=f.current?.contains(b);!_&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?$n.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(ae,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"}),e&&h.jsx(Q,{className:"text-sm font-medium",children:e}),n==="right"&&t&&h.jsx(oe,{name:t,className:"w-4 h-4"})]}),m]})},ZE=({label:e,icon:t,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3 transition-colors ${l} ${c} ${o}`,style:i,children:[t&&h.jsx(oe,{name:t,className:"w-4 h-4"}),e&&h.jsx(Q,{className:"text-sm font-medium",children:e})]})},QE=()=>window.G7Core,jc=v.createContext(null),Gp=()=>{const e=v.useContext(jc);if(!e){const t=QE(),n=t?.locale?.current?.()||"ko",r=t?.locale?.supported?.()||["ko","en"];return{activeLocale:n,getActiveLocale:()=>t?.locale?.current?.()||"ko",supportedLocales:r,isInsideMultilingualTabPanel:!1}}return{...e,isInsideMultilingualTabPanel:!0}},JE=()=>window.G7Core,RE=({value:e,onChange:t,placeholder:n,readOnly:r,disabled:s,hasError:o})=>{const{activeLocale:i,getActiveLocale:a}=Gp(),l=typeof e=="string"?{[i]:e}:e||{},c=v.useCallback(u=>{const f=a();t({...l,[f]:u.target.value})},[l,a,t]),d=o?"border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500":"border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500";return h.jsx(St,{type:"text",value:l[i]||"",onChange:c,placeholder:n,readOnly:r,disabled:s,className:`w-full px-3 py-2 text-sm border ${d} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2`})},eM=(e,t)=>{if(!t)return!1;const n=t.startsWith("!"),r=n?t.slice(1):t,s=!!e[r];return n?!s:s},tM=({column:e,item:t,index:n,onValueChange:r,readOnly:s,error:o})=>{const i=t[e.key],a=s||e.readOnly||eM(t,e.readOnlyCondition)||e.disabled,l=!!o,c=l?"border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500":"border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500",d=v.useCallback(f=>{r(e.key,f)},[e.key,r]),u=f=>h.jsxs(I,{className:"w-full",children:[f,l&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:o})]});switch(e.type){case"input":return u(h.jsx(St,{type:"text",value:i||"",onChange:f=>d(f.target.value),placeholder:e.placeholder,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${e.className||""}`}));case"number":return u(h.jsx(St,{type:"number",value:i??"",onChange:f=>d(f.target.value?Number(f.target.value):null),placeholder:e.placeholder,min:e.min,max:e.max,step:e.step,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${e.className||""}`}));case"select":return u(h.jsxs(Xr,{value:i??"",onChange:f=>d(f.target.value),disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${e.className||""}`,children:[h.jsx("option",{value:"",children:"선택하세요"}),e.options?.map(f=>h.jsx("option",{value:f.value,disabled:f.disabled,children:f.label},f.value))]}));case"textarea":return u(h.jsx(ir,{value:i||"",onChange:f=>d(f.target.value),placeholder:e.placeholder,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 resize-none ${e.className||""}`,rows:2}));case"multilingual":return u(h.jsx(RE,{value:i,onChange:f=>d(f),placeholder:e.placeholder,readOnly:a,disabled:a,hasError:l}));case"custom":return e.render?h.jsx(h.Fragment,{children:e.render(t,n,d)}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"-"});default:return h.jsx(Q,{className:"text-gray-900 dark:text-white text-sm",children:String(i??"")})}},nM=(e,t,n)=>{if(!e)return;if(n==="multilingual"){const o=JE()?.locale?.current?.()||"ko",i=`${t}.${o}`;return e[i]?.[0]}return e[t]?.[0]},rM=({item:e,index:t,id:n,columns:r,enableDrag:s=!0,showIndex:o=!0,canRemove:i=!0,rowActions:a,readOnly:l=!1,onValueChange:c,onRemove:d,className:u="",level:f=0,errors:g})=>{const{attributes:m,listeners:p,setNodeRef:y,transform:b,transition:_,isDragging:x}=Ui({id:n,disabled:!s||l}),M={transform:jn.Transform.toString(b),transition:_,opacity:x?.5:1},C=v.useCallback((S,w)=>{c?.(S,w)},[c]),k=v.useCallback(S=>{S.onClick?.(e,t)},[e,t]),N=f>0?{paddingLeft:`${f*24}px`}:{};return h.jsxs(I,{ref:y,style:M,className:`flex items-center gap-2 py-2 px-2 border-b border-gray-100 dark:border-gray-700 last:border-b-0 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors ${x?"bg-blue-50 dark:bg-blue-900/20 shadow-lg rounded-lg z-10":""} ${u}`,children:[s&&!l&&h.jsx(I,{...m,...p,className:"flex-shrink-0 w-6 h-6 flex items-center justify-center cursor-grab active:cursor-grabbing text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400",style:N,children:h.jsx(oe,{name:"grip-vertical",className:"w-4 h-4"})}),o&&h.jsx(I,{className:"flex-shrink-0 w-8 text-center text-sm text-gray-500 dark:text-gray-400 font-medium",children:t+1}),r.map(S=>{const w=nM(g,S.key,S.type);return h.jsx(I,{className:`flex-1 min-w-0 ${S.width||""}`,style:S.width?{flex:"none",width:S.width}:{},children:h.jsx(tM,{column:S,item:e,index:t,onValueChange:C,readOnly:l,error:w})},S.key)}),a&&a.length>0&&!l&&h.jsx(I,{className:"flex-shrink-0 flex items-center gap-1",children:a.map(S=>{const w=typeof S.disabled=="function"?S.disabled(e,t):S.disabled;return h.jsx(ae,{type:"button",onClick:()=>k(S),disabled:w,className:`p-1.5 rounded transition-colors ${S.danger?"text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600":"text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300"} disabled:opacity-50 disabled:cursor-not-allowed`,title:S.label,children:S.icon?h.jsx(oe,{name:S.icon,className:"w-4 h-4"}):h.jsx(Q,{className:"text-xs",children:S.label})},S.key)})}),i&&!a&&!l&&h.jsx(I,{className:"flex-shrink-0",children:h.jsx(ae,{type:"button",onClick:d,className:"p-1.5 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded transition-colors",title:"삭제",children:h.jsx(oe,{name:"minus",className:"w-4 h-4"})})})]})},Yp=()=>window.G7Core,Wc=()=>`item_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,sM=e=>{const t={_id:Wc()};return e.forEach(n=>{if(n.type==="multilingual"){const s=Yp()?.locale?.all?.()||["ko","en"],o={};s.forEach(i=>{o[i]=""}),t[n.key]=o}else n.type==="number"?t[n.key]=null:(n.type,t[n.key]="")}),t},oM=(e,t)=>{if(!e)return{};const n=`fields.${t}.`,r={};return Object.keys(e).forEach(s=>{if(s.startsWith(n)){const o=s.substring(n.length);r[o]=e[s]}}),r},iM=({items:e=[],columns:t,onChange:n,onAddItem:r,onRemoveItem:s,onReorder:o,addLabel:i="항목 추가",enableDrag:a=!0,showIndex:l=!0,minItems:c=0,maxItems:d,rowActions:u,emptyMessage:f="항목이 없습니다.",className:g="",headerClassName:m="",rowClassName:p="",name:y,readOnly:b=!1,createDefaultItem:_,childrenKey:x,itemIdKey:M="_id",errors:C,id:k,editorAttrs:N})=>{const S=v.useRef(null),w=v.useRef([]),E=v.useRef(null),D=v.useMemo(()=>{let G=!1;for(const U of e)if(U[M]==null){G=!0;break}return G?e.map(U=>U[M]!=null?U:{...U,[M]:Wc()}):e},[e,M]);w.current=D,v.useEffect(()=>{E.current=null},[D]);const P=v.useCallback(G=>String(G[M]??""),[M]);v.useEffect(()=>{if(y){const G=Yp();G?.form&&(S.current=G.form)}},[y]);const L=Ql(us(io,{activationConstraint:{distance:8}}),us(oo,{coordinateGetter:Bi})),$=v.useMemo(()=>D.map(G=>P(G)),[D,P]),A=v.useCallback(G=>{const{active:U,over:W}=G;if(W&&U.id!==W.id){const Y=E.current??w.current,V=Y.map(q=>String(q[M]??"")),X=V.indexOf(String(U.id)),K=V.indexOf(String(W.id));if(X!==-1&&K!==-1){const q=hs([...Y],X,K);E.current=q,n?.(q),o?.(q)}}},[n,o,M]),O=v.useCallback((G,U,W)=>{const V=[...E.current??w.current];V[G]={...V[G],[U]:W},E.current=V,n?.(V)},[n]),T=v.useCallback(()=>{const G=E.current??w.current;if(d&&G.length>=d)return;if(r){r();return}const U=_?_():sM(t);U[M]||(U[M]=Wc());const W=[...G,U];E.current=W,n?.(W)},[d,r,_,t,M,n]),z=v.useCallback(G=>{const U=E.current??w.current;if(U.length<=c)return;const W=U[G];if(s){s(G,W);return}const Y=U.filter((V,X)=>X!==G);E.current=Y,n?.(Y)},[c,s,n]),H=D.length>c,j=!d||D.length<d;return h.jsxs(I,{className:`dynamic-field-list ${g}`,id:k,...N,children:[h.jsxs(I,{className:`flex items-center gap-2 py-2 px-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-t-lg ${m}`,children:[a&&!b&&h.jsx(I,{className:"flex-shrink-0 w-6"}),l&&h.jsx(I,{className:"flex-shrink-0 w-8 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase",children:"#"}),t.map(G=>h.jsxs(I,{className:`flex-1 min-w-0 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase ${G.width||""}`,style:G.width?{flex:"none",width:G.width}:{},children:[G.label,G.required&&h.jsx(Q,{className:"text-red-500 dark:text-red-400 ml-0.5",children:"*"})]},G.key)),(u||!b&&H)&&h.jsx(I,{className:"flex-shrink-0 w-8"})]}),D.length>0?h.jsx(lc,{sensors:L,collisionDetection:Jl,onDragEnd:A,children:h.jsx(Hi,{items:$,strategy:cc,children:h.jsx(I,{className:"border-x border-gray-200 dark:border-gray-700",children:D.map((G,U)=>{const W=P(G),Y=oM(C,U);return h.jsx(rM,{id:W,item:G,index:U,columns:t,enableDrag:a,showIndex:l,canRemove:H,rowActions:u,readOnly:b,onValueChange:(V,X)=>O(U,V,X),onRemove:()=>z(U),className:p,errors:Y},W)})})})}):h.jsx(I,{className:"border-x border-gray-200 dark:border-gray-700 py-8 text-center text-gray-500 dark:text-gray-400 text-sm",children:f}),!b&&j&&h.jsx(I,{className:"relative border border-gray-200 dark:border-gray-700 border-t-0 rounded-b-lg",children:h.jsx(I,{className:"flex items-center justify-center py-2",children:h.jsxs(ae,{type:"button",onClick:T,className:"flex items-center gap-1.5 px-4 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[h.jsx(oe,{name:"plus",className:"w-4 h-4"}),h.jsx(Q,{children:i})]})})}),(b||!j)&&D.length>0&&h.jsx(I,{className:"border-x border-b border-gray-200 dark:border-gray-700 rounded-b-lg h-1"})]})},Xp=()=>window.G7Core,aM=()=>Xp()?.locale?.supported?.()??["ko","en"],lM=()=>Xp()?.locale?.current?.()??"ko",cM={ko:"한국어",en:"English",ja:"日本語",zh:"中文"},dM=({children:e,className:t="",style:n,variant:r="default",defaultLocale:s,onLocaleChange:o,locales:i})=>{const a=v.useMemo(()=>i&&i.length>0?i:aM(),[i]),[l,c]=v.useState(s||lM());v.useEffect(()=>{if(a.length>0&&!a.includes(l)){const p=a[0];c(p),o?.(p)}},[a,l,o]);const d=v.useRef(l);d.current=l;const u=v.useCallback(()=>d.current,[]),f=v.useCallback(p=>{c(p),o?.(p)},[o]),g=v.useMemo(()=>({activeLocale:l,getActiveLocale:u,supportedLocales:a}),[l,u,a]),m=p=>{const y=p===l,b="px-4 py-2 text-sm font-medium transition-all";return r==="pills"?y?`${b} bg-blue-600 text-white rounded-lg`:`${b} text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg`:r==="underline"?y?`${b} text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400`:`${b} text-gray-600 dark:text-gray-400 border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-500`:y?`${b} text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border-b-2 border-blue-600`:`${b} text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 border-b-2 border-transparent`};return h.jsxs(I,{className:t,style:n,children:[h.jsx(Yo,{className:"flex gap-1 border-b border-gray-200 dark:border-gray-700 mb-4",children:a.map(p=>h.jsx(ae,{type:"button",onClick:()=>f(p),className:m(p),children:h.jsx(Q,{children:cM[p]||p})},p))}),h.jsx(jc.Provider,{value:g,children:e})]})},uM=({label:e,required:t=!1,error:n,helperText:r,className:s="",labelClassName:o="",children:i,horizontal:a=!1,labelWidth:l="w-1/3",id:c,editorAttrs:d})=>{const u=a?`flex items-start gap-4 ${s}`:`space-y-1 ${s}`,f=a?`${l} flex-shrink-0 pt-2`:"",g=a?"flex-1":"";return h.jsxs(I,{className:u,id:c,...d,children:[e&&h.jsx(I,{className:f,children:h.jsxs(ln,{className:`block text-sm font-medium text-gray-700 dark:text-gray-300 ${o}`,children:[e,t&&h.jsx("span",{className:"ml-1 text-red-500 dark:text-red-400",children:"*"})]})}),h.jsxs(I,{className:`${g} ${n?"form-field-error":""}`.trim(),children:[i,r&&!n&&h.jsx(I,{className:"mt-1 text-xs text-gray-500 dark:text-gray-400",children:r}),n&&h.jsx(I,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:n})]})]})};function wo(e){return e+.5|0}const pr=(e,t,n)=>Math.max(Math.min(e,n),t);function ko(e){return pr(wo(e*2.55),0,255)}function mr(e){return pr(wo(e*255),0,255)}function Jn(e){return pr(wo(e/2.55)/100,0,1)}function Kp(e){return pr(wo(e*100),0,100)}const bn={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Gc=[..."0123456789ABCDEF"],fM=e=>Gc[e&15],hM=e=>Gc[(e&240)>>4]+Gc[e&15],oa=e=>(e&240)>>4===(e&15),gM=e=>oa(e.r)&&oa(e.g)&&oa(e.b)&&oa(e.a);function pM(e){var t=e.length,n;return e[0]==="#"&&(t===4||t===5?n={r:255&bn[e[1]]*17,g:255&bn[e[2]]*17,b:255&bn[e[3]]*17,a:t===5?bn[e[4]]*17:255}:(t===7||t===9)&&(n={r:bn[e[1]]<<4|bn[e[2]],g:bn[e[3]]<<4|bn[e[4]],b:bn[e[5]]<<4|bn[e[6]],a:t===9?bn[e[7]]<<4|bn[e[8]]:255})),n}const mM=(e,t)=>e<255?t(e):"";function bM(e){var t=gM(e)?fM:hM;return e?"#"+t(e.r)+t(e.g)+t(e.b)+mM(e.a,t):void 0}const yM=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function qp(e,t,n){const r=t*Math.min(n,1-n),s=(o,i=(o+e/30)%12)=>n-r*Math.max(Math.min(i-3,9-i,1),-1);return[s(0),s(8),s(4)]}function vM(e,t,n){const r=(s,o=(s+e/60)%6)=>n-n*t*Math.max(Math.min(o,4-o,1),0);return[r(5),r(3),r(1)]}function xM(e,t,n){const r=qp(e,1,.5);let s;for(t+n>1&&(s=1/(t+n),t*=s,n*=s),s=0;s<3;s++)r[s]*=1-t-n,r[s]+=t;return r}function wM(e,t,n,r,s){return e===s?(t-n)/r+(t<n?6:0):t===s?(n-e)/r+2:(e-t)/r+4}function Yc(e){const n=e.r/255,r=e.g/255,s=e.b/255,o=Math.max(n,r,s),i=Math.min(n,r,s),a=(o+i)/2;let l,c,d;return o!==i&&(d=o-i,c=a>.5?d/(2-o-i):d/(o+i),l=wM(n,r,s,d,o),l=l*60+.5),[l|0,c||0,a]}function Xc(e,t,n,r){return(Array.isArray(t)?e(t[0],t[1],t[2]):e(t,n,r)).map(mr)}function Kc(e,t,n){return Xc(qp,e,t,n)}function kM(e,t,n){return Xc(xM,e,t,n)}function CM(e,t,n){return Xc(vM,e,t,n)}function Zp(e){return(e%360+360)%360}function SM(e){const t=yM.exec(e);let n=255,r;if(!t)return;t[5]!==r&&(n=t[6]?ko(+t[5]):mr(+t[5]));const s=Zp(+t[2]),o=+t[3]/100,i=+t[4]/100;return t[1]==="hwb"?r=kM(s,o,i):t[1]==="hsv"?r=CM(s,o,i):r=Kc(s,o,i),{r:r[0],g:r[1],b:r[2],a:n}}function _M(e,t){var n=Yc(e);n[0]=Zp(n[0]+t),n=Kc(n),e.r=n[0],e.g=n[1],e.b=n[2]}function EM(e){if(!e)return;const t=Yc(e),n=t[0],r=Kp(t[1]),s=Kp(t[2]);return e.a<255?`hsla(${n}, ${r}%, ${s}%, ${Jn(e.a)})`:`hsl(${n}, ${r}%, ${s}%)`}const Qp={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Jp={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function MM(){const e={},t=Object.keys(Jp),n=Object.keys(Qp);let r,s,o,i,a;for(r=0;r<t.length;r++){for(i=a=t[r],s=0;s<n.length;s++)o=n[s],a=a.replace(o,Qp[o]);o=parseInt(Jp[i],16),e[a]=[o>>16&255,o>>8&255,o&255]}return e}let ia;function NM(e){ia||(ia=MM(),ia.transparent=[0,0,0,0]);const t=ia[e.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}const TM=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function OM(e){const t=TM.exec(e);let n=255,r,s,o;if(t){if(t[7]!==r){const i=+t[7];n=t[8]?ko(i):pr(i*255,0,255)}return r=+t[1],s=+t[3],o=+t[5],r=255&(t[2]?ko(r):pr(r,0,255)),s=255&(t[4]?ko(s):pr(s,0,255)),o=255&(t[6]?ko(o):pr(o,0,255)),{r,g:s,b:o,a:n}}}function AM(e){return e&&(e.a<255?`rgba(${e.r}, ${e.g}, ${e.b}, ${Jn(e.a)})`:`rgb(${e.r}, ${e.g}, ${e.b})`)}const qc=e=>e<=.0031308?e*12.92:Math.pow(e,1/2.4)*1.055-.055,Ss=e=>e<=.04045?e/12.92:Math.pow((e+.055)/1.055,2.4);function DM(e,t,n){const r=Ss(Jn(e.r)),s=Ss(Jn(e.g)),o=Ss(Jn(e.b));return{r:mr(qc(r+n*(Ss(Jn(t.r))-r))),g:mr(qc(s+n*(Ss(Jn(t.g))-s))),b:mr(qc(o+n*(Ss(Jn(t.b))-o))),a:e.a+n*(t.a-e.a)}}function aa(e,t,n){if(e){let r=Yc(e);r[t]=Math.max(0,Math.min(r[t]+r[t]*n,t===0?360:1)),r=Kc(r),e.r=r[0],e.g=r[1],e.b=r[2]}}function Rp(e,t){return e&&Object.assign(t||{},e)}function e0(e){var t={r:0,g:0,b:0,a:255};return Array.isArray(e)?e.length>=3&&(t={r:e[0],g:e[1],b:e[2],a:255},e.length>3&&(t.a=mr(e[3]))):(t=Rp(e,{r:0,g:0,b:0,a:1}),t.a=mr(t.a)),t}function IM(e){return e.charAt(0)==="r"?OM(e):SM(e)}class Co{constructor(t){if(t instanceof Co)return t;const n=typeof t;let r;n==="object"?r=e0(t):n==="string"&&(r=pM(t)||NM(t)||IM(t)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var t=Rp(this._rgb);return t&&(t.a=Jn(t.a)),t}set rgb(t){this._rgb=e0(t)}rgbString(){return this._valid?AM(this._rgb):void 0}hexString(){return this._valid?bM(this._rgb):void 0}hslString(){return this._valid?EM(this._rgb):void 0}mix(t,n){if(t){const r=this.rgb,s=t.rgb;let o;const i=n===o?.5:n,a=2*i-1,l=r.a-s.a,c=((a*l===-1?a:(a+l)/(1+a*l))+1)/2;o=1-c,r.r=255&c*r.r+o*s.r+.5,r.g=255&c*r.g+o*s.g+.5,r.b=255&c*r.b+o*s.b+.5,r.a=i*r.a+(1-i)*s.a,this.rgb=r}return this}interpolate(t,n){return t&&(this._rgb=DM(this._rgb,t._rgb,n)),this}clone(){return new Co(this.rgb)}alpha(t){return this._rgb.a=mr(t),this}clearer(t){const n=this._rgb;return n.a*=1-t,this}greyscale(){const t=this._rgb,n=wo(t.r*.3+t.g*.59+t.b*.11);return t.r=t.g=t.b=n,this}opaquer(t){const n=this._rgb;return n.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return aa(this._rgb,2,t),this}darken(t){return aa(this._rgb,2,-t),this}saturate(t){return aa(this._rgb,1,t),this}desaturate(t){return aa(this._rgb,1,-t),this}rotate(t){return _M(this._rgb,t),this}}function Rn(){}const LM=(()=>{let e=0;return()=>e++})();function dt(e){return e==null}function Tt(e){if(Array.isArray&&Array.isArray(e))return!0;const t=Object.prototype.toString.call(e);return t.slice(0,7)==="[object"&&t.slice(-6)==="Array]"}function Je(e){return e!==null&&Object.prototype.toString.call(e)==="[object Object]"}function yn(e){return(typeof e=="number"||e instanceof Number)&&isFinite(+e)}function Gn(e,t){return yn(e)?e:t}function Re(e,t){return typeof e>"u"?t:e}const PM=(e,t)=>typeof e=="string"&&e.endsWith("%")?parseFloat(e)/100:+e/t,t0=(e,t)=>typeof e=="string"&&e.endsWith("%")?parseFloat(e)/100*t:+e;function mt(e,t,n){if(e&&typeof e.call=="function")return e.apply(n,t)}function lt(e,t,n,r){let s,o,i;if(Tt(e))for(o=e.length,s=0;s<o;s++)t.call(n,e[s],s);else if(Je(e))for(i=Object.keys(e),o=i.length,s=0;s<o;s++)t.call(n,e[i[s]],i[s])}function la(e,t){let n,r,s,o;if(!e||!t||e.length!==t.length)return!1;for(n=0,r=e.length;n<r;++n)if(s=e[n],o=t[n],s.datasetIndex!==o.datasetIndex||s.index!==o.index)return!1;return!0}function ca(e){if(Tt(e))return e.map(ca);if(Je(e)){const t=Object.create(null),n=Object.keys(e),r=n.length;let s=0;for(;s<r;++s)t[n[s]]=ca(e[n[s]]);return t}return e}function n0(e){return["__proto__","prototype","constructor"].indexOf(e)===-1}function $M(e,t,n,r){if(!n0(e))return;const s=t[e],o=n[e];Je(s)&&Je(o)?So(s,o,r):t[e]=ca(o)}function So(e,t,n){const r=Tt(t)?t:[t],s=r.length;if(!Je(e))return e;n=n||{};const o=n.merger||$M;let i;for(let a=0;a<s;++a){if(i=r[a],!Je(i))continue;const l=Object.keys(i);for(let c=0,d=l.length;c<d;++c)o(l[c],e,i,n)}return e}function _o(e,t){return So(e,t,{merger:FM})}function FM(e,t,n){if(!n0(e))return;const r=t[e],s=n[e];Je(r)&&Je(s)?_o(r,s):Object.prototype.hasOwnProperty.call(t,e)||(t[e]=ca(s))}const r0={"":e=>e,x:e=>e.x,y:e=>e.y};function zM(e){const t=e.split("."),n=[];let r="";for(const s of t)r+=s,r.endsWith("\\")?r=r.slice(0,-1)+".":(n.push(r),r="");return n}function VM(e){const t=zM(e);return n=>{for(const r of t){if(r==="")break;n=n&&n[r]}return n}}function Lr(e,t){return(r0[t]||(r0[t]=VM(t)))(e)}function Zc(e){return e.charAt(0).toUpperCase()+e.slice(1)}const Eo=e=>typeof e<"u",br=e=>typeof e=="function",s0=(e,t)=>{if(e.size!==t.size)return!1;for(const n of e)if(!t.has(n))return!1;return!0};function HM(e){return e.type==="mouseup"||e.type==="click"||e.type==="contextmenu"}const ut=Math.PI,Mt=2*ut,da=Number.POSITIVE_INFINITY,UM=ut/180,Ot=ut/2,Pr=ut/4,o0=ut*2/3,i0=Math.log10,yr=Math.sign;function ua(e,t,n){return Math.abs(e-t)<n}function a0(e){const t=Math.round(e);e=ua(e,t,e/1e3)?t:e;const n=Math.pow(10,Math.floor(i0(e))),r=e/n;return(r<=1?1:r<=2?2:r<=5?5:10)*n}function BM(e){const t=[],n=Math.sqrt(e);let r;for(r=1;r<n;r++)e%r===0&&(t.push(r),t.push(e/r));return n===(n|0)&&t.push(n),t.sort((s,o)=>s-o).pop(),t}function jM(e){return typeof e=="symbol"||typeof e=="object"&&e!==null&&!(Symbol.toPrimitive in e||"toString"in e||"valueOf"in e)}function fa(e){return!jM(e)&&!isNaN(parseFloat(e))&&isFinite(e)}function WM(e,t){const n=Math.round(e);return n-t<=e&&n+t>=e}function GM(e,t,n){let r,s,o;for(r=0,s=e.length;r<s;r++)o=e[r][n],isNaN(o)||(t.min=Math.min(t.min,o),t.max=Math.max(t.max,o))}function er(e){return e*(ut/180)}function YM(e){return e*(180/ut)}function l0(e){if(!yn(e))return;let t=1,n=0;for(;Math.round(e*t)/t!==e;)t*=10,n++;return n}function c0(e,t){const n=t.x-e.x,r=t.y-e.y,s=Math.sqrt(n*n+r*r);let o=Math.atan2(r,n);return o<-.5*ut&&(o+=Mt),{angle:o,distance:s}}function XM(e,t){return Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2))}function Yn(e){return(e%Mt+Mt)%Mt}function ha(e,t,n,r){const s=Yn(e),o=Yn(t),i=Yn(n),a=Yn(o-s),l=Yn(i-s),c=Yn(s-o),d=Yn(s-i);return s===o||s===i||r&&o===i||a>l&&c<d}function sn(e,t,n){return Math.max(t,Math.min(n,e))}function KM(e){return sn(e,-32768,32767)}function $r(e,t,n,r=1e-6){return e>=Math.min(t,n)-r&&e<=Math.max(t,n)+r}function Qc(e,t,n){n=n||(i=>e[i]<t);let r=e.length-1,s=0,o;for(;r-s>1;)o=s+r>>1,n(o)?s=o:r=o;return{lo:s,hi:r}}const Jc=(e,t,n,r)=>Qc(e,n,r?s=>{const o=e[s][t];return o<n||o===n&&e[s+1][t]===n}:s=>e[s][t]<n),qM=(e,t,n)=>Qc(e,n,r=>e[r][t]>=n);function ZM(e,t,n){let r=0,s=e.length;for(;r<s&&e[r]<t;)r++;for(;s>r&&e[s-1]>n;)s--;return r>0||s<e.length?e.slice(r,s):e}const d0=["push","pop","shift","splice","unshift"];function QM(e,t){if(e._chartjs){e._chartjs.listeners.push(t);return}Object.defineProperty(e,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[t]}}),d0.forEach(n=>{const r="_onData"+Zc(n),s=e[n];Object.defineProperty(e,n,{configurable:!0,enumerable:!1,value(...o){const i=s.apply(this,o);return e._chartjs.listeners.forEach(a=>{typeof a[r]=="function"&&a[r](...o)}),i}})})}function u0(e,t){const n=e._chartjs;if(!n)return;const r=n.listeners,s=r.indexOf(t);s!==-1&&r.splice(s,1),!(r.length>0)&&(d0.forEach(o=>{delete e[o]}),delete e._chartjs)}function f0(e){const t=new Set(e);return t.size===e.length?e:Array.from(t)}const h0=(function(){return typeof window>"u"?function(e){return e()}:window.requestAnimationFrame})();function g0(e,t){let n=[],r=!1;return function(...s){n=s,r||(r=!0,h0.call(window,()=>{r=!1,e.apply(t,n)}))}}function JM(e,t){let n;return function(...r){return t?(clearTimeout(n),n=setTimeout(e,t,r)):e.apply(this,r),t}}const Rc=e=>e==="start"?"left":e==="end"?"right":"center",Xt=(e,t,n)=>e==="start"?t:e==="end"?n:(t+n)/2,RM=(e,t,n,r)=>e===(r?"left":"right")?n:e==="center"?(t+n)/2:t,ga=e=>e===0||e===1,p0=(e,t,n)=>-(Math.pow(2,10*(e-=1))*Math.sin((e-t)*Mt/n)),m0=(e,t,n)=>Math.pow(2,-10*e)*Math.sin((e-t)*Mt/n)+1,Mo={linear:e=>e,easeInQuad:e=>e*e,easeOutQuad:e=>-e*(e-2),easeInOutQuad:e=>(e/=.5)<1?.5*e*e:-.5*(--e*(e-2)-1),easeInCubic:e=>e*e*e,easeOutCubic:e=>(e-=1)*e*e+1,easeInOutCubic:e=>(e/=.5)<1?.5*e*e*e:.5*((e-=2)*e*e+2),easeInQuart:e=>e*e*e*e,easeOutQuart:e=>-((e-=1)*e*e*e-1),easeInOutQuart:e=>(e/=.5)<1?.5*e*e*e*e:-.5*((e-=2)*e*e*e-2),easeInQuint:e=>e*e*e*e*e,easeOutQuint:e=>(e-=1)*e*e*e*e+1,easeInOutQuint:e=>(e/=.5)<1?.5*e*e*e*e*e:.5*((e-=2)*e*e*e*e+2),easeInSine:e=>-Math.cos(e*Ot)+1,easeOutSine:e=>Math.sin(e*Ot),easeInOutSine:e=>-.5*(Math.cos(ut*e)-1),easeInExpo:e=>e===0?0:Math.pow(2,10*(e-1)),easeOutExpo:e=>e===1?1:-Math.pow(2,-10*e)+1,easeInOutExpo:e=>ga(e)?e:e<.5?.5*Math.pow(2,10*(e*2-1)):.5*(-Math.pow(2,-10*(e*2-1))+2),easeInCirc:e=>e>=1?e:-(Math.sqrt(1-e*e)-1),easeOutCirc:e=>Math.sqrt(1-(e-=1)*e),easeInOutCirc:e=>(e/=.5)<1?-.5*(Math.sqrt(1-e*e)-1):.5*(Math.sqrt(1-(e-=2)*e)+1),easeInElastic:e=>ga(e)?e:p0(e,.075,.3),easeOutElastic:e=>ga(e)?e:m0(e,.075,.3),easeInOutElastic(e){return ga(e)?e:e<.5?.5*p0(e*2,.1125,.45):.5+.5*m0(e*2-1,.1125,.45)},easeInBack(e){return e*e*((1.70158+1)*e-1.70158)},easeOutBack(e){return(e-=1)*e*((1.70158+1)*e+1.70158)+1},easeInOutBack(e){let t=1.70158;return(e/=.5)<1?.5*(e*e*(((t*=1.525)+1)*e-t)):.5*((e-=2)*e*(((t*=1.525)+1)*e+t)+2)},easeInBounce:e=>1-Mo.easeOutBounce(1-e),easeOutBounce(e){return e<1/2.75?7.5625*e*e:e<2/2.75?7.5625*(e-=1.5/2.75)*e+.75:e<2.5/2.75?7.5625*(e-=2.25/2.75)*e+.9375:7.5625*(e-=2.625/2.75)*e+.984375},easeInOutBounce:e=>e<.5?Mo.easeInBounce(e*2)*.5:Mo.easeOutBounce(e*2-1)*.5+.5};function b0(e){if(e&&typeof e=="object"){const t=e.toString();return t==="[object CanvasPattern]"||t==="[object CanvasGradient]"}return!1}function y0(e){return b0(e)?e:new Co(e)}function ed(e){return b0(e)?e:new Co(e).saturate(.5).darken(.1).hexString()}const eN=["x","y","borderWidth","radius","tension"],tN=["color","borderColor","backgroundColor"];function nN(e){e.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),e.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>t!=="onProgress"&&t!=="onComplete"&&t!=="fn"}),e.set("animations",{colors:{type:"color",properties:tN},numbers:{type:"number",properties:eN}}),e.describe("animations",{_fallback:"animation"}),e.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>t|0}}}})}function rN(e){e.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}const v0=new Map;function sN(e,t){t=t||{};const n=e+JSON.stringify(t);let r=v0.get(n);return r||(r=new Intl.NumberFormat(e,t),v0.set(n,r)),r}function td(e,t,n){return sN(t,n).format(e)}const oN={values(e){return Tt(e)?e:""+e},numeric(e,t,n){if(e===0)return"0";const r=this.chart.options.locale;let s,o=e;if(n.length>1){const c=Math.max(Math.abs(n[0].value),Math.abs(n[n.length-1].value));(c<1e-4||c>1e15)&&(s="scientific"),o=iN(e,n)}const i=i0(Math.abs(o)),a=isNaN(i)?1:Math.max(Math.min(-1*Math.floor(i),20),0),l={notation:s,minimumFractionDigits:a,maximumFractionDigits:a};return Object.assign(l,this.options.ticks.format),td(e,r,l)}};function iN(e,t){let n=t.length>3?t[2].value-t[1].value:t[1].value-t[0].value;return Math.abs(n)>=1&&e!==Math.floor(e)&&(n=e-Math.floor(e)),n}var x0={formatters:oN};function aN(e){e.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,n)=>n.lineWidth,tickColor:(t,n)=>n.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:x0.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),e.route("scale.ticks","color","","color"),e.route("scale.grid","color","","borderColor"),e.route("scale.border","color","","borderColor"),e.route("scale.title","color","","color"),e.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&t!=="callback"&&t!=="parser",_indexable:t=>t!=="borderDash"&&t!=="tickBorderDash"&&t!=="dash"}),e.describe("scales",{_fallback:"scale"}),e.describe("scale.ticks",{_scriptable:t=>t!=="backdropPadding"&&t!=="callback",_indexable:t=>t!=="backdropPadding"})}const Fr=Object.create(null),nd=Object.create(null);function No(e,t){if(!t)return e;const n=t.split(".");for(let r=0,s=n.length;r<s;++r){const o=n[r];e=e[o]||(e[o]=Object.create(null))}return e}function rd(e,t,n){return typeof t=="string"?So(No(e,t),n):So(No(e,""),t)}class lN{constructor(t,n){this.animation=void 0,this.backgroundColor="rgba(0,0,0,0.1)",this.borderColor="rgba(0,0,0,0.1)",this.color="#666",this.datasets={},this.devicePixelRatio=r=>r.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(r,s)=>ed(s.backgroundColor),this.hoverBorderColor=(r,s)=>ed(s.borderColor),this.hoverColor=(r,s)=>ed(s.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(n)}set(t,n){return rd(this,t,n)}get(t){return No(this,t)}describe(t,n){return rd(nd,t,n)}override(t,n){return rd(Fr,t,n)}route(t,n,r,s){const o=No(this,t),i=No(this,r),a="_"+n;Object.defineProperties(o,{[a]:{value:o[n],writable:!0},[n]:{enumerable:!0,get(){const l=this[a],c=i[s];return Je(l)?Object.assign({},c,l):Re(l,c)},set(l){this[a]=l}}})}apply(t){t.forEach(n=>n(this))}}var kt=new lN({_scriptable:e=>!e.startsWith("on"),_indexable:e=>e!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[nN,rN,aN]);function cN(e){return!e||dt(e.size)||dt(e.family)?null:(e.style?e.style+" ":"")+(e.weight?e.weight+" ":"")+e.size+"px "+e.family}function w0(e,t,n,r,s){let o=t[s];return o||(o=t[s]=e.measureText(s).width,n.push(s)),o>r&&(r=o),r}function zr(e,t,n){const r=e.currentDevicePixelRatio,s=n!==0?Math.max(n/2,.5):0;return Math.round((t-s)*r)/r+s}function k0(e,t){!t&&!e||(t=t||e.getContext("2d"),t.save(),t.resetTransform(),t.clearRect(0,0,e.width,e.height),t.restore())}function C0(e,t,n,r){S0(e,t,n,r,null)}function S0(e,t,n,r,s){let o,i,a,l,c,d,u,f;const g=t.pointStyle,m=t.rotation,p=t.radius;let y=(m||0)*UM;if(g&&typeof g=="object"&&(o=g.toString(),o==="[object HTMLImageElement]"||o==="[object HTMLCanvasElement]")){e.save(),e.translate(n,r),e.rotate(y),e.drawImage(g,-g.width/2,-g.height/2,g.width,g.height),e.restore();return}if(!(isNaN(p)||p<=0)){switch(e.beginPath(),g){default:s?e.ellipse(n,r,s/2,p,0,0,Mt):e.arc(n,r,p,0,Mt),e.closePath();break;case"triangle":d=s?s/2:p,e.moveTo(n+Math.sin(y)*d,r-Math.cos(y)*p),y+=o0,e.lineTo(n+Math.sin(y)*d,r-Math.cos(y)*p),y+=o0,e.lineTo(n+Math.sin(y)*d,r-Math.cos(y)*p),e.closePath();break;case"rectRounded":c=p*.516,l=p-c,i=Math.cos(y+Pr)*l,u=Math.cos(y+Pr)*(s?s/2-c:l),a=Math.sin(y+Pr)*l,f=Math.sin(y+Pr)*(s?s/2-c:l),e.arc(n-u,r-a,c,y-ut,y-Ot),e.arc(n+f,r-i,c,y-Ot,y),e.arc(n+u,r+a,c,y,y+Ot),e.arc(n-f,r+i,c,y+Ot,y+ut),e.closePath();break;case"rect":if(!m){l=Math.SQRT1_2*p,d=s?s/2:l,e.rect(n-d,r-l,2*d,2*l);break}y+=Pr;case"rectRot":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),e.moveTo(n-u,r-a),e.lineTo(n+f,r-i),e.lineTo(n+u,r+a),e.lineTo(n-f,r+i),e.closePath();break;case"crossRot":y+=Pr;case"cross":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),e.moveTo(n-u,r-a),e.lineTo(n+u,r+a),e.moveTo(n+f,r-i),e.lineTo(n-f,r+i);break;case"star":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),e.moveTo(n-u,r-a),e.lineTo(n+u,r+a),e.moveTo(n+f,r-i),e.lineTo(n-f,r+i),y+=Pr,u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),e.moveTo(n-u,r-a),e.lineTo(n+u,r+a),e.moveTo(n+f,r-i),e.lineTo(n-f,r+i);break;case"line":i=s?s/2:Math.cos(y)*p,a=Math.sin(y)*p,e.moveTo(n-i,r-a),e.lineTo(n+i,r+a);break;case"dash":e.moveTo(n,r),e.lineTo(n+Math.cos(y)*(s?s/2:p),r+Math.sin(y)*p);break;case!1:e.closePath();break}e.fill(),t.borderWidth>0&&e.stroke()}}function _0(e,t,n){return n=n||.5,!t||e&&e.x>t.left-n&&e.x<t.right+n&&e.y>t.top-n&&e.y<t.bottom+n}function sd(e,t){e.save(),e.beginPath(),e.rect(t.left,t.top,t.right-t.left,t.bottom-t.top),e.clip()}function od(e){e.restore()}function dN(e,t){t.translation&&e.translate(t.translation[0],t.translation[1]),dt(t.rotation)||e.rotate(t.rotation),t.color&&(e.fillStyle=t.color),t.textAlign&&(e.textAlign=t.textAlign),t.textBaseline&&(e.textBaseline=t.textBaseline)}function uN(e,t,n,r,s){if(s.strikethrough||s.underline){const o=e.measureText(r),i=t-o.actualBoundingBoxLeft,a=t+o.actualBoundingBoxRight,l=n-o.actualBoundingBoxAscent,c=n+o.actualBoundingBoxDescent,d=s.strikethrough?(l+c)/2:c;e.strokeStyle=e.fillStyle,e.beginPath(),e.lineWidth=s.decorationWidth||2,e.moveTo(i,d),e.lineTo(a,d),e.stroke()}}function fN(e,t){const n=e.fillStyle;e.fillStyle=t.color,e.fillRect(t.left,t.top,t.width,t.height),e.fillStyle=n}function To(e,t,n,r,s,o={}){const i=Tt(t)?t:[t],a=o.strokeWidth>0&&o.strokeColor!=="";let l,c;for(e.save(),e.font=s.string,dN(e,o),l=0;l<i.length;++l)c=i[l],o.backdrop&&fN(e,o.backdrop),a&&(o.strokeColor&&(e.strokeStyle=o.strokeColor),dt(o.strokeWidth)||(e.lineWidth=o.strokeWidth),e.strokeText(c,n,r,o.maxWidth)),e.fillText(c,n,r,o.maxWidth),uN(e,n,r,c,o),r+=Number(s.lineHeight);e.restore()}function pa(e,t){const{x:n,y:r,w:s,h:o,radius:i}=t;e.arc(n+i.topLeft,r+i.topLeft,i.topLeft,1.5*ut,ut,!0),e.lineTo(n,r+o-i.bottomLeft),e.arc(n+i.bottomLeft,r+o-i.bottomLeft,i.bottomLeft,ut,Ot,!0),e.lineTo(n+s-i.bottomRight,r+o),e.arc(n+s-i.bottomRight,r+o-i.bottomRight,i.bottomRight,Ot,0,!0),e.lineTo(n+s,r+i.topRight),e.arc(n+s-i.topRight,r+i.topRight,i.topRight,0,-Ot,!0),e.lineTo(n+i.topLeft,r)}const hN=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,gN=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function pN(e,t){const n=(""+e).match(hN);if(!n||n[1]==="normal")return t*1.2;switch(e=+n[2],n[3]){case"px":return e;case"%":e/=100;break}return t*e}const mN=e=>+e||0;function id(e,t){const n={},r=Je(t),s=r?Object.keys(t):t,o=Je(e)?r?i=>Re(e[i],e[t[i]]):i=>e[i]:()=>e;for(const i of s)n[i]=mN(o(i));return n}function E0(e){return id(e,{top:"y",right:"x",bottom:"y",left:"x"})}function _s(e){return id(e,["topLeft","topRight","bottomLeft","bottomRight"])}function vn(e){const t=E0(e);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function Kt(e,t){e=e||{},t=t||kt.font;let n=Re(e.size,t.size);typeof n=="string"&&(n=parseInt(n,10));let r=Re(e.style,t.style);r&&!(""+r).match(gN)&&(console.warn('Invalid font style specified: "'+r+'"'),r=void 0);const s={family:Re(e.family,t.family),lineHeight:pN(Re(e.lineHeight,t.lineHeight),n),size:n,style:r,weight:Re(e.weight,t.weight),string:""};return s.string=cN(s),s}function ma(e,t,n,r){let s,o,i;for(s=0,o=e.length;s<o;++s)if(i=e[s],i!==void 0&&i!==void 0)return i}function bN(e,t,n){const{min:r,max:s}=e,o=t0(t,(s-r)/2),i=(a,l)=>n&&a===0?0:a+l;return{min:i(r,-Math.abs(o)),max:i(s,o)}}function Es(e,t){return Object.assign(Object.create(e),t)}function ad(e,t=[""],n,r,s=()=>e[0]){const o=n||e;typeof r>"u"&&(r=A0("_fallback",e));const i={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:e,_rootScopes:o,_fallback:r,_getTarget:s,override:a=>ad([a,...e],t,o,r)};return new Proxy(i,{deleteProperty(a,l){return delete a[l],delete a._keys,delete e[0][l],!0},get(a,l){return N0(a,l,()=>_N(l,t,e,a))},getOwnPropertyDescriptor(a,l){return Reflect.getOwnPropertyDescriptor(a._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(e[0])},has(a,l){return D0(a).includes(l)},ownKeys(a){return D0(a)},set(a,l,c){const d=a._storage||(a._storage=s());return a[l]=d[l]=c,delete a._keys,!0}})}function Ms(e,t,n,r){const s={_cacheable:!1,_proxy:e,_context:t,_subProxy:n,_stack:new Set,_descriptors:M0(e,r),setContext:o=>Ms(e,o,n,r),override:o=>Ms(e.override(o),t,n,r)};return new Proxy(s,{deleteProperty(o,i){return delete o[i],delete e[i],!0},get(o,i,a){return N0(o,i,()=>vN(o,i,a))},getOwnPropertyDescriptor(o,i){return o._descriptors.allKeys?Reflect.has(e,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(e,i)},getPrototypeOf(){return Reflect.getPrototypeOf(e)},has(o,i){return Reflect.has(e,i)},ownKeys(){return Reflect.ownKeys(e)},set(o,i,a){return e[i]=a,delete o[i],!0}})}function M0(e,t={scriptable:!0,indexable:!0}){const{_scriptable:n=t.scriptable,_indexable:r=t.indexable,_allKeys:s=t.allKeys}=e;return{allKeys:s,scriptable:n,indexable:r,isScriptable:br(n)?n:()=>n,isIndexable:br(r)?r:()=>r}}const yN=(e,t)=>e?e+Zc(t):t,ld=(e,t)=>Je(t)&&e!=="adapters"&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function N0(e,t,n){if(Object.prototype.hasOwnProperty.call(e,t)||t==="constructor")return e[t];const r=n();return e[t]=r,r}function vN(e,t,n){const{_proxy:r,_context:s,_subProxy:o,_descriptors:i}=e;let a=r[t];return br(a)&&i.isScriptable(t)&&(a=xN(t,a,e,n)),Tt(a)&&a.length&&(a=wN(t,a,e,i.isIndexable)),ld(t,a)&&(a=Ms(a,s,o&&o[t],i)),a}function xN(e,t,n,r){const{_proxy:s,_context:o,_subProxy:i,_stack:a}=n;if(a.has(e))throw new Error("Recursion detected: "+Array.from(a).join("->")+"->"+e);a.add(e);let l=t(o,i||r);return a.delete(e),ld(e,l)&&(l=cd(s._scopes,s,e,l)),l}function wN(e,t,n,r){const{_proxy:s,_context:o,_subProxy:i,_descriptors:a}=n;if(typeof o.index<"u"&&r(e))return t[o.index%t.length];if(Je(t[0])){const l=t,c=s._scopes.filter(d=>d!==l);t=[];for(const d of l){const u=cd(c,s,e,d);t.push(Ms(u,o,i&&i[e],a))}}return t}function T0(e,t,n){return br(e)?e(t,n):e}const kN=(e,t)=>e===!0?t:typeof e=="string"?Lr(t,e):void 0;function CN(e,t,n,r,s){for(const o of t){const i=kN(n,o);if(i){e.add(i);const a=T0(i._fallback,n,s);if(typeof a<"u"&&a!==n&&a!==r)return a}else if(i===!1&&typeof r<"u"&&n!==r)return null}return!1}function cd(e,t,n,r){const s=t._rootScopes,o=T0(t._fallback,n,r),i=[...e,...s],a=new Set;a.add(r);let l=O0(a,i,n,o||n,r);return l===null||typeof o<"u"&&o!==n&&(l=O0(a,i,o,l,r),l===null)?!1:ad(Array.from(a),[""],s,o,()=>SN(t,n,r))}function O0(e,t,n,r,s){for(;n;)n=CN(e,t,n,r,s);return n}function SN(e,t,n){const r=e._getTarget();t in r||(r[t]={});const s=r[t];return Tt(s)&&Je(n)?n:s||{}}function _N(e,t,n,r){let s;for(const o of t)if(s=A0(yN(o,e),n),typeof s<"u")return ld(e,s)?cd(n,r,e,s):s}function A0(e,t){for(const n of t){if(!n)continue;const r=n[e];if(typeof r<"u")return r}}function D0(e){let t=e._keys;return t||(t=e._keys=EN(e._scopes)),t}function EN(e){const t=new Set;for(const n of e)for(const r of Object.keys(n).filter(s=>!s.startsWith("_")))t.add(r);return Array.from(t)}function dd(){return typeof window<"u"&&typeof document<"u"}function ud(e){let t=e.parentNode;return t&&t.toString()==="[object ShadowRoot]"&&(t=t.host),t}function ba(e,t,n){let r;return typeof e=="string"?(r=parseInt(e,10),e.indexOf("%")!==-1&&(r=r/100*t.parentNode[n])):r=e,r}const ya=e=>e.ownerDocument.defaultView.getComputedStyle(e,null);function MN(e,t){return ya(e).getPropertyValue(t)}const NN=["top","right","bottom","left"];function Vr(e,t,n){const r={};n=n?"-"+n:"";for(let s=0;s<4;s++){const o=NN[s];r[o]=parseFloat(e[t+"-"+o+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}const TN=(e,t,n)=>(e>0||t>0)&&(!n||!n.shadowRoot);function ON(e,t){const n=e.touches,r=n&&n.length?n[0]:e,{offsetX:s,offsetY:o}=r;let i=!1,a,l;if(TN(s,o,e.target))a=s,l=o;else{const c=t.getBoundingClientRect();a=r.clientX-c.left,l=r.clientY-c.top,i=!0}return{x:a,y:l,box:i}}function Hr(e,t){if("native"in e)return e;const{canvas:n,currentDevicePixelRatio:r}=t,s=ya(n),o=s.boxSizing==="border-box",i=Vr(s,"padding"),a=Vr(s,"border","width"),{x:l,y:c,box:d}=ON(e,n),u=i.left+(d&&a.left),f=i.top+(d&&a.top);let{width:g,height:m}=t;return o&&(g-=i.width+a.width,m-=i.height+a.height),{x:Math.round((l-u)/g*n.width/r),y:Math.round((c-f)/m*n.height/r)}}function AN(e,t,n){let r,s;if(t===void 0||n===void 0){const o=e&&ud(e);if(!o)t=e.clientWidth,n=e.clientHeight;else{const i=o.getBoundingClientRect(),a=ya(o),l=Vr(a,"border","width"),c=Vr(a,"padding");t=i.width-c.width-l.width,n=i.height-c.height-l.height,r=ba(a.maxWidth,o,"clientWidth"),s=ba(a.maxHeight,o,"clientHeight")}}return{width:t,height:n,maxWidth:r||da,maxHeight:s||da}}const vr=e=>Math.round(e*10)/10;function DN(e,t,n,r){const s=ya(e),o=Vr(s,"margin"),i=ba(s.maxWidth,e,"clientWidth")||da,a=ba(s.maxHeight,e,"clientHeight")||da,l=AN(e,t,n);let{width:c,height:d}=l;if(s.boxSizing==="content-box"){const f=Vr(s,"border","width"),g=Vr(s,"padding");c-=g.width+f.width,d-=g.height+f.height}return c=Math.max(0,c-o.width),d=Math.max(0,r?c/r:d-o.height),c=vr(Math.min(c,i,l.maxWidth)),d=vr(Math.min(d,a,l.maxHeight)),c&&!d&&(d=vr(c/2)),(t!==void 0||n!==void 0)&&r&&l.height&&d>l.height&&(d=l.height,c=vr(Math.floor(d*r))),{width:c,height:d}}function I0(e,t,n){const r=t||1,s=vr(e.height*r),o=vr(e.width*r);e.height=vr(e.height),e.width=vr(e.width);const i=e.canvas;return i.style&&(n||!i.style.height&&!i.style.width)&&(i.style.height=`${e.height}px`,i.style.width=`${e.width}px`),e.currentDevicePixelRatio!==r||i.height!==s||i.width!==o?(e.currentDevicePixelRatio=r,i.height=s,i.width=o,e.ctx.setTransform(r,0,0,r,0,0),!0):!1}const IN=(function(){let e=!1;try{const t={get passive(){return e=!0,!1}};dd()&&(window.addEventListener("test",null,t),window.removeEventListener("test",null,t))}catch{}return e})();function L0(e,t){const n=MN(e,t),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}const LN=function(e,t){return{x(n){return e+e+t-n},setWidth(n){t=n},textAlign(n){return n==="center"?n:n==="right"?"left":"right"},xPlus(n,r){return n-r},leftForLtr(n,r){return n-r}}},PN=function(){return{x(e){return e},setWidth(e){},textAlign(e){return e},xPlus(e,t){return e+t},leftForLtr(e,t){return e}}};function Ns(e,t,n){return e?LN(t,n):PN()}function P0(e,t){let n,r;(t==="ltr"||t==="rtl")&&(n=e.canvas.style,r=[n.getPropertyValue("direction"),n.getPropertyPriority("direction")],n.setProperty("direction",t,"important"),e.prevTextDirection=r)}function $0(e,t){t!==void 0&&(delete e.prevTextDirection,e.canvas.style.setProperty("direction",t[0],t[1]))}function va(e,t,n){return e.options.clip?e[n]:t[n]}function $N(e,t){const{xScale:n,yScale:r}=e;return n&&r?{left:va(n,t,"left"),right:va(n,t,"right"),top:va(r,t,"top"),bottom:va(r,t,"bottom")}:t}function FN(e,t){const n=t._clip;if(n.disabled)return!1;const r=$N(t,e.chartArea);return{left:n.left===!1?0:r.left-(n.left===!0?0:n.left),right:n.right===!1?e.width:r.right+(n.right===!0?0:n.right),top:n.top===!1?0:r.top-(n.top===!0?0:n.top),bottom:n.bottom===!1?e.height:r.bottom+(n.bottom===!0?0:n.bottom)}}class zN{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,n,r,s){const o=n.listeners[s],i=n.duration;o.forEach(a=>a({chart:t,initial:n.initial,numSteps:i,currentStep:Math.min(r-n.start,i)}))}_refresh(){this._request||(this._running=!0,this._request=h0.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(t=Date.now()){let n=0;this._charts.forEach((r,s)=>{if(!r.running||!r.items.length)return;const o=r.items;let i=o.length-1,a=!1,l;for(;i>=0;--i)l=o[i],l._active?(l._total>r.duration&&(r.duration=l._total),l.tick(t),a=!0):(o[i]=o[o.length-1],o.pop());a&&(s.draw(),this._notify(s,r,t,"progress")),o.length||(r.running=!1,this._notify(s,r,t,"complete"),r.initial=!1),n+=o.length}),this._lastDate=t,n===0&&(this._running=!1)}_getAnims(t){const n=this._charts;let r=n.get(t);return r||(r={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},n.set(t,r)),r}listen(t,n,r){this._getAnims(t).listeners[n].push(r)}add(t,n){!n||!n.length||this._getAnims(t).items.push(...n)}has(t){return this._getAnims(t).items.length>0}start(t){const n=this._charts.get(t);n&&(n.running=!0,n.start=Date.now(),n.duration=n.items.reduce((r,s)=>Math.max(r,s._duration),0),this._refresh())}running(t){if(!this._running)return!1;const n=this._charts.get(t);return!(!n||!n.running||!n.items.length)}stop(t){const n=this._charts.get(t);if(!n||!n.items.length)return;const r=n.items;let s=r.length-1;for(;s>=0;--s)r[s].cancel();n.items=[],this._notify(t,n,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}var tr=new zN;const F0="transparent",VN={boolean(e,t,n){return n>.5?t:e},color(e,t,n){const r=y0(e||F0),s=r.valid&&y0(t||F0);return s&&s.valid?s.mix(r,n).hexString():t},number(e,t,n){return e+(t-e)*n}};class HN{constructor(t,n,r,s){const o=n[r];s=ma([t.to,s,o,t.from]);const i=ma([t.from,o,s]);this._active=!0,this._fn=t.fn||VN[t.type||typeof i],this._easing=Mo[t.easing]||Mo.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=n,this._prop=r,this._from=i,this._to=s,this._promises=void 0}active(){return this._active}update(t,n,r){if(this._active){this._notify(!1);const s=this._target[this._prop],o=r-this._start,i=this._duration-o;this._start=r,this._duration=Math.floor(Math.max(i,t.duration)),this._total+=o,this._loop=!!t.loop,this._to=ma([t.to,n,s,t.from]),this._from=ma([t.from,s,n])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){const n=t-this._start,r=this._duration,s=this._prop,o=this._from,i=this._loop,a=this._to;let l;if(this._active=o!==a&&(i||n<r),!this._active){this._target[s]=a,this._notify(!0);return}if(n<0){this._target[s]=o;return}l=n/r%2,l=i&&l>1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[s]=this._fn(o,a,l)}wait(){const t=this._promises||(this._promises=[]);return new Promise((n,r)=>{t.push({res:n,rej:r})})}_notify(t){const n=t?"res":"rej",r=this._promises||[];for(let s=0;s<r.length;s++)r[s][n]()}}class z0{constructor(t,n){this._chart=t,this._properties=new Map,this.configure(n)}configure(t){if(!Je(t))return;const n=Object.keys(kt.animation),r=this._properties;Object.getOwnPropertyNames(t).forEach(s=>{const o=t[s];if(!Je(o))return;const i={};for(const a of n)i[a]=o[a];(Tt(o.properties)&&o.properties||[s]).forEach(a=>{(a===s||!r.has(a))&&r.set(a,i)})})}_animateOptions(t,n){const r=n.options,s=BN(t,r);if(!s)return[];const o=this._createAnimations(s,r);return r.$shared&&UN(t.options.$animations,r).then(()=>{t.options=r},()=>{}),o}_createAnimations(t,n){const r=this._properties,s=[],o=t.$animations||(t.$animations={}),i=Object.keys(n),a=Date.now();let l;for(l=i.length-1;l>=0;--l){const c=i[l];if(c.charAt(0)==="$")continue;if(c==="options"){s.push(...this._animateOptions(t,n));continue}const d=n[c];let u=o[c];const f=r.get(c);if(u)if(f&&u.active()){u.update(f,d,a);continue}else u.cancel();if(!f||!f.duration){t[c]=d;continue}o[c]=u=new HN(f,t,c,d),s.push(u)}return s}update(t,n){if(this._properties.size===0){Object.assign(t,n);return}const r=this._createAnimations(t,n);if(r.length)return tr.add(this._chart,r),!0}}function UN(e,t){const n=[],r=Object.keys(t);for(let s=0;s<r.length;s++){const o=e[r[s]];o&&o.active()&&n.push(o.wait())}return Promise.all(n)}function BN(e,t){if(!t)return;let n=e.options;if(!n){e.options=t;return}return n.$shared&&(e.options=n=Object.assign({},n,{$shared:!1,$animations:{}})),n}function V0(e,t){const n=e&&e.options||{},r=n.reverse,s=n.min===void 0?t:0,o=n.max===void 0?t:0;return{start:r?o:s,end:r?s:o}}function jN(e,t,n){if(n===!1)return!1;const r=V0(e,n),s=V0(t,n);return{top:s.end,right:r.end,bottom:s.start,left:r.start}}function WN(e){let t,n,r,s;return Je(e)?(t=e.top,n=e.right,r=e.bottom,s=e.left):t=n=r=s=e,{top:t,right:n,bottom:r,left:s,disabled:e===!1}}function H0(e,t){const n=[],r=e._getSortedDatasetMetas(t);let s,o;for(s=0,o=r.length;s<o;++s)n.push(r[s].index);return n}function U0(e,t,n,r={}){const s=e.keys,o=r.mode==="single";let i,a,l,c;if(t===null)return;let d=!1;for(i=0,a=s.length;i<a;++i){if(l=+s[i],l===n){if(d=!0,r.all)continue;break}c=e.values[l],yn(c)&&(o||t===0||yr(t)===yr(c))&&(t+=c)}return!d&&!r.all?0:t}function GN(e,t){const{iScale:n,vScale:r}=t,s=n.axis==="x"?"x":"y",o=r.axis==="x"?"x":"y",i=Object.keys(e),a=new Array(i.length);let l,c,d;for(l=0,c=i.length;l<c;++l)d=i[l],a[l]={[s]:d,[o]:e[d]};return a}function fd(e,t){const n=e&&e.options.stacked;return n||n===void 0&&t.stack!==void 0}function YN(e,t,n){return`${e.id}.${t.id}.${n.stack||n.type}`}function XN(e){const{min:t,max:n,minDefined:r,maxDefined:s}=e.getUserBounds();return{min:r?t:Number.NEGATIVE_INFINITY,max:s?n:Number.POSITIVE_INFINITY}}function KN(e,t,n){const r=e[t]||(e[t]={});return r[n]||(r[n]={})}function B0(e,t,n,r){for(const s of t.getMatchingVisibleMetas(r).reverse()){const o=e[s.index];if(n&&o>0||!n&&o<0)return s.index}return null}function j0(e,t){const{chart:n,_cachedMeta:r}=e,s=n._stacks||(n._stacks={}),{iScale:o,vScale:i,index:a}=r,l=o.axis,c=i.axis,d=YN(o,i,r),u=t.length;let f;for(let g=0;g<u;++g){const m=t[g],{[l]:p,[c]:y}=m,b=m._stacks||(m._stacks={});f=b[c]=KN(s,d,p),f[a]=y,f._top=B0(f,i,!0,r.type),f._bottom=B0(f,i,!1,r.type);const _=f._visualValues||(f._visualValues={});_[a]=y}}function hd(e,t){const n=e.scales;return Object.keys(n).filter(r=>n[r].axis===t).shift()}function qN(e,t){return Es(e,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:"default",type:"dataset"})}function ZN(e,t,n){return Es(e,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:n,index:t,mode:"default",type:"data"})}function Oo(e,t){const n=e.controller.index,r=e.vScale&&e.vScale.axis;if(r){t=t||e._parsed;for(const s of t){const o=s._stacks;if(!o||o[r]===void 0||o[r][n]===void 0)return;delete o[r][n],o[r]._visualValues!==void 0&&o[r]._visualValues[n]!==void 0&&delete o[r]._visualValues[n]}}}const gd=e=>e==="reset"||e==="none",W0=(e,t)=>t?e:Object.assign({},e),QN=(e,t,n)=>e&&!t.hidden&&t._stacked&&{keys:H0(n,!0),values:null};class Ts{constructor(t,n){this.chart=t,this._ctx=t.ctx,this.index=n,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=fd(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Oo(this._cachedMeta),this.index=t}linkScales(){const t=this.chart,n=this._cachedMeta,r=this.getDataset(),s=(u,f,g,m)=>u==="x"?f:u==="r"?m:g,o=n.xAxisID=Re(r.xAxisID,hd(t,"x")),i=n.yAxisID=Re(r.yAxisID,hd(t,"y")),a=n.rAxisID=Re(r.rAxisID,hd(t,"r")),l=n.indexAxis,c=n.iAxisID=s(l,o,i,a),d=n.vAxisID=s(l,i,o,a);n.xScale=this.getScaleForId(o),n.yScale=this.getScaleForId(i),n.rScale=this.getScaleForId(a),n.iScale=this.getScaleForId(c),n.vScale=this.getScaleForId(d)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const n=this._cachedMeta;return t===n.iScale?n.vScale:n.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&u0(this._data,this),t._stacked&&Oo(t)}_dataCheck(){const t=this.getDataset(),n=t.data||(t.data=[]),r=this._data;if(Je(n)){const s=this._cachedMeta;this._data=GN(n,s)}else if(r!==n){if(r){u0(r,this);const s=this._cachedMeta;Oo(s),s._parsed=[]}n&&Object.isExtensible(n)&&QM(n,this),this._syncList=[],this._data=n}}addElements(){const t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){const n=this._cachedMeta,r=this.getDataset();let s=!1;this._dataCheck();const o=n._stacked;n._stacked=fd(n.vScale,n),n.stack!==r.stack&&(s=!0,Oo(n),n.stack=r.stack),this._resyncElements(t),(s||o!==n._stacked)&&(j0(this,n._parsed),n._stacked=fd(n.vScale,n))}configure(){const t=this.chart.config,n=t.datasetScopeKeys(this._type),r=t.getOptionScopes(this.getDataset(),n,!0);this.options=t.createResolver(r,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,n){const{_cachedMeta:r,_data:s}=this,{iScale:o,_stacked:i}=r,a=o.axis;let l=t===0&&n===s.length?!0:r._sorted,c=t>0&&r._parsed[t-1],d,u,f;if(this._parsing===!1)r._parsed=s,r._sorted=!0,f=s;else{Tt(s[t])?f=this.parseArrayData(r,s,t,n):Je(s[t])?f=this.parseObjectData(r,s,t,n):f=this.parsePrimitiveData(r,s,t,n);const g=()=>u[a]===null||c&&u[a]<c[a];for(d=0;d<n;++d)r._parsed[d+t]=u=f[d],l&&(g()&&(l=!1),c=u);r._sorted=l}i&&j0(this,f)}parsePrimitiveData(t,n,r,s){const{iScale:o,vScale:i}=t,a=o.axis,l=i.axis,c=o.getLabels(),d=o===i,u=new Array(s);let f,g,m;for(f=0,g=s;f<g;++f)m=f+r,u[f]={[a]:d||o.parse(c[m],m),[l]:i.parse(n[m],m)};return u}parseArrayData(t,n,r,s){const{xScale:o,yScale:i}=t,a=new Array(s);let l,c,d,u;for(l=0,c=s;l<c;++l)d=l+r,u=n[d],a[l]={x:o.parse(u[0],d),y:i.parse(u[1],d)};return a}parseObjectData(t,n,r,s){const{xScale:o,yScale:i}=t,{xAxisKey:a="x",yAxisKey:l="y"}=this._parsing,c=new Array(s);let d,u,f,g;for(d=0,u=s;d<u;++d)f=d+r,g=n[f],c[d]={x:o.parse(Lr(g,a),f),y:i.parse(Lr(g,l),f)};return c}getParsed(t){return this._cachedMeta._parsed[t]}getDataElement(t){return this._cachedMeta.data[t]}applyStack(t,n,r){const s=this.chart,o=this._cachedMeta,i=n[t.axis],a={keys:H0(s,!0),values:n._stacks[t.axis]._visualValues};return U0(a,i,o.index,{mode:r})}updateRangeFromParsed(t,n,r,s){const o=r[n.axis];let i=o===null?NaN:o;const a=s&&r._stacks[n.axis];s&&a&&(s.values=a,i=U0(s,o,this._cachedMeta.index)),t.min=Math.min(t.min,i),t.max=Math.max(t.max,i)}getMinMax(t,n){const r=this._cachedMeta,s=r._parsed,o=r._sorted&&t===r.iScale,i=s.length,a=this._getOtherScale(t),l=QN(n,r,this.chart),c={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:d,max:u}=XN(a);let f,g;function m(){g=s[f];const p=g[a.axis];return!yn(g[t.axis])||d>p||u<p}for(f=0;f<i&&!(!m()&&(this.updateRangeFromParsed(c,t,g,l),o));++f);if(o){for(f=i-1;f>=0;--f)if(!m()){this.updateRangeFromParsed(c,t,g,l);break}}return c}getAllParsedValues(t){const n=this._cachedMeta._parsed,r=[];let s,o,i;for(s=0,o=n.length;s<o;++s)i=n[s][t.axis],yn(i)&&r.push(i);return r}getMaxOverflow(){return!1}getLabelAndValue(t){const n=this._cachedMeta,r=n.iScale,s=n.vScale,o=this.getParsed(t);return{label:r?""+r.getLabelForValue(o[r.axis]):"",value:s?""+s.getLabelForValue(o[s.axis]):""}}_update(t){const n=this._cachedMeta;this.update(t||"default"),n._clip=WN(Re(this.options.clip,jN(n.xScale,n.yScale,this.getMaxOverflow())))}update(t){}draw(){const t=this._ctx,n=this.chart,r=this._cachedMeta,s=r.data||[],o=n.chartArea,i=[],a=this._drawStart||0,l=this._drawCount||s.length-a,c=this.options.drawActiveElementsOnTop;let d;for(r.dataset&&r.dataset.draw(t,o,a,l),d=a;d<a+l;++d){const u=s[d];u.hidden||(u.active&&c?i.push(u):u.draw(t,o))}for(d=0;d<i.length;++d)i[d].draw(t,o)}getStyle(t,n){const r=n?"active":"default";return t===void 0&&this._cachedMeta.dataset?this.resolveDatasetElementOptions(r):this.resolveDataElementOptions(t||0,r)}getContext(t,n,r){const s=this.getDataset();let o;if(t>=0&&t<this._cachedMeta.data.length){const i=this._cachedMeta.data[t];o=i.$context||(i.$context=ZN(this.getContext(),t,i)),o.parsed=this.getParsed(t),o.raw=s.data[t],o.index=o.dataIndex=t}else o=this.$context||(this.$context=qN(this.chart.getContext(),this.index)),o.dataset=s,o.index=o.datasetIndex=this.index;return o.active=!!n,o.mode=r,o}resolveDatasetElementOptions(t){return this._resolveElementOptions(this.datasetElementType.id,t)}resolveDataElementOptions(t,n){return this._resolveElementOptions(this.dataElementType.id,n,t)}_resolveElementOptions(t,n="default",r){const s=n==="active",o=this._cachedDataOpts,i=t+"-"+n,a=o[i],l=this.enableOptionSharing&&Eo(r);if(a)return W0(a,l);const c=this.chart.config,d=c.datasetElementScopeKeys(this._type,t),u=s?[`${t}Hover`,"hover",t,""]:[t,""],f=c.getOptionScopes(this.getDataset(),d),g=Object.keys(kt.elements[t]),m=()=>this.getContext(r,s,n),p=c.resolveNamedOptions(f,g,m,u);return p.$shared&&(p.$shared=l,o[i]=Object.freeze(W0(p,l))),p}_resolveAnimations(t,n,r){const s=this.chart,o=this._cachedDataOpts,i=`animation-${n}`,a=o[i];if(a)return a;let l;if(s.options.animation!==!1){const d=this.chart.config,u=d.datasetAnimationScopeKeys(this._type,n),f=d.getOptionScopes(this.getDataset(),u);l=d.createResolver(f,this.getContext(t,r,n))}const c=new z0(s,l&&l.animations);return l&&l._cacheable&&(o[i]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,n){return!n||gd(t)||this.chart._animationsDisabled}_getSharedOptions(t,n){const r=this.resolveDataElementOptions(t,n),s=this._sharedOptions,o=this.getSharedOptions(r),i=this.includeOptions(n,o)||o!==s;return this.updateSharedOptions(o,n,r),{sharedOptions:o,includeOptions:i}}updateElement(t,n,r,s){gd(s)?Object.assign(t,r):this._resolveAnimations(n,s).update(t,r)}updateSharedOptions(t,n,r){t&&!gd(n)&&this._resolveAnimations(void 0,n).update(t,r)}_setStyle(t,n,r,s){t.active=s;const o=this.getStyle(n,s);this._resolveAnimations(n,r,s).update(t,{options:!s&&this.getSharedOptions(o)||o})}removeHoverStyle(t,n,r){this._setStyle(t,r,"active",!1)}setHoverStyle(t,n,r){this._setStyle(t,r,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const n=this._data,r=this._cachedMeta.data;for(const[a,l,c]of this._syncList)this[a](l,c);this._syncList=[];const s=r.length,o=n.length,i=Math.min(o,s);i&&this.parse(0,i),o>s?this._insertElements(s,o-s,t):o<s&&this._removeElements(o,s-o)}_insertElements(t,n,r=!0){const s=this._cachedMeta,o=s.data,i=t+n;let a;const l=c=>{for(c.length+=n,a=c.length-1;a>=i;a--)c[a]=c[a-n]};for(l(o),a=t;a<i;++a)o[a]=new this.dataElementType;this._parsing&&l(s._parsed),this.parse(t,n),r&&this.updateElements(o,t,n,"reset")}updateElements(t,n,r,s){}_removeElements(t,n){const r=this._cachedMeta;if(this._parsing){const s=r._parsed.splice(t,n);r._stacked&&Oo(r,s)}r.data.splice(t,n)}_sync(t){if(this._parsing)this._syncList.push(t);else{const[n,r,s]=t;this[n](r,s)}this.chart._dataChanges.push([this.index,...t])}_onDataPush(){const t=arguments.length;this._sync(["_insertElements",this.getDataset().data.length-t,t])}_onDataPop(){this._sync(["_removeElements",this._cachedMeta.data.length-1,1])}_onDataShift(){this._sync(["_removeElements",0,1])}_onDataSplice(t,n){n&&this._sync(["_removeElements",t,n]);const r=arguments.length-2;r&&this._sync(["_insertElements",t,r])}_onDataUnshift(){this._sync(["_insertElements",0,arguments.length])}}Pe(Ts,"defaults",{}),Pe(Ts,"datasetElementType",null),Pe(Ts,"dataElementType",null);function JN(e,t){if(!e._cache.$bar){const n=e.getMatchingVisibleMetas(t);let r=[];for(let s=0,o=n.length;s<o;s++)r=r.concat(n[s].controller.getAllParsedValues(e));e._cache.$bar=f0(r.sort((s,o)=>s-o))}return e._cache.$bar}function RN(e){const t=e.iScale,n=JN(t,e.type);let r=t._length,s,o,i,a;const l=()=>{i===32767||i===-32768||(Eo(a)&&(r=Math.min(r,Math.abs(i-a)||r)),a=i)};for(s=0,o=n.length;s<o;++s)i=t.getPixelForValue(n[s]),l();for(a=void 0,s=0,o=t.ticks.length;s<o;++s)i=t.getPixelForTick(s),l();return r}function eT(e,t,n,r){const s=n.barThickness;let o,i;return dt(s)?(o=t.min*n.categoryPercentage,i=n.barPercentage):(o=s*r,i=1),{chunk:o/r,ratio:i,start:t.pixels[e]-o/2}}function tT(e,t,n,r){const s=t.pixels,o=s[e];let i=e>0?s[e-1]:null,a=e<s.length-1?s[e+1]:null;const l=n.categoryPercentage;i===null&&(i=o-(a===null?t.end-t.start:a-o)),a===null&&(a=o+o-i);const c=o-(o-Math.min(i,a))/2*l;return{chunk:Math.abs(a-i)/2*l/r,ratio:n.barPercentage,start:c}}function nT(e,t,n,r){const s=n.parse(e[0],r),o=n.parse(e[1],r),i=Math.min(s,o),a=Math.max(s,o);let l=i,c=a;Math.abs(i)>Math.abs(a)&&(l=a,c=i),t[n.axis]=c,t._custom={barStart:l,barEnd:c,start:s,end:o,min:i,max:a}}function G0(e,t,n,r){return Tt(e)?nT(e,t,n,r):t[n.axis]=n.parse(e,r),t}function Y0(e,t,n,r){const s=e.iScale,o=e.vScale,i=s.getLabels(),a=s===o,l=[];let c,d,u,f;for(c=n,d=n+r;c<d;++c)f=t[c],u={},u[s.axis]=a||s.parse(i[c],c),l.push(G0(f,u,o,c));return l}function pd(e){return e&&e.barStart!==void 0&&e.barEnd!==void 0}function rT(e,t,n){return e!==0?yr(e):(t.isHorizontal()?1:-1)*(t.min>=n?1:-1)}function sT(e){let t,n,r,s,o;return e.horizontal?(t=e.base>e.x,n="left",r="right"):(t=e.base<e.y,n="bottom",r="top"),t?(s="end",o="start"):(s="start",o="end"),{start:n,end:r,reverse:t,top:s,bottom:o}}function oT(e,t,n,r){let s=t.borderSkipped;const o={};if(!s){e.borderSkipped=o;return}if(s===!0){e.borderSkipped={top:!0,right:!0,bottom:!0,left:!0};return}const{start:i,end:a,reverse:l,top:c,bottom:d}=sT(e);s==="middle"&&n&&(e.enableBorderRadius=!0,(n._top||0)===r?s=c:(n._bottom||0)===r?s=d:(o[X0(d,i,a,l)]=!0,s=c)),o[X0(s,i,a,l)]=!0,e.borderSkipped=o}function X0(e,t,n,r){return r?(e=iT(e,t,n),e=K0(e,n,t)):e=K0(e,t,n),e}function iT(e,t,n){return e===t?n:e===n?t:e}function K0(e,t,n){return e==="start"?t:e==="end"?n:e}function aT(e,{inflateAmount:t},n){e.inflateAmount=t==="auto"?n===1?.33:0:t}class xa extends Ts{parsePrimitiveData(t,n,r,s){return Y0(t,n,r,s)}parseArrayData(t,n,r,s){return Y0(t,n,r,s)}parseObjectData(t,n,r,s){const{iScale:o,vScale:i}=t,{xAxisKey:a="x",yAxisKey:l="y"}=this._parsing,c=o.axis==="x"?a:l,d=i.axis==="x"?a:l,u=[];let f,g,m,p;for(f=r,g=r+s;f<g;++f)p=n[f],m={},m[o.axis]=o.parse(Lr(p,c),f),u.push(G0(Lr(p,d),m,i,f));return u}updateRangeFromParsed(t,n,r,s){super.updateRangeFromParsed(t,n,r,s);const o=r._custom;o&&n===this._cachedMeta.vScale&&(t.min=Math.min(t.min,o.min),t.max=Math.max(t.max,o.max))}getMaxOverflow(){return 0}getLabelAndValue(t){const n=this._cachedMeta,{iScale:r,vScale:s}=n,o=this.getParsed(t),i=o._custom,a=pd(i)?"["+i.start+", "+i.end+"]":""+s.getLabelForValue(o[s.axis]);return{label:""+r.getLabelForValue(o[r.axis]),value:a}}initialize(){this.enableOptionSharing=!0,super.initialize();const t=this._cachedMeta;t.stack=this.getDataset().stack}update(t){const n=this._cachedMeta;this.updateElements(n.data,0,n.data.length,t)}updateElements(t,n,r,s){const o=s==="reset",{index:i,_cachedMeta:{vScale:a}}=this,l=a.getBasePixel(),c=a.isHorizontal(),d=this._getRuler(),{sharedOptions:u,includeOptions:f}=this._getSharedOptions(n,s);for(let g=n;g<n+r;g++){const m=this.getParsed(g),p=o||dt(m[a.axis])?{base:l,head:l}:this._calculateBarValuePixels(g),y=this._calculateBarIndexPixels(g,d),b=(m._stacks||{})[a.axis],_={horizontal:c,base:p.base,enableBorderRadius:!b||pd(m._custom)||i===b._top||i===b._bottom,x:c?p.head:y.center,y:c?y.center:p.head,height:c?y.size:Math.abs(p.size),width:c?Math.abs(p.size):y.size};f&&(_.options=u||this.resolveDataElementOptions(g,t[g].active?"active":s));const x=_.options||t[g].options;oT(_,x,b,i),aT(_,x,d.ratio),this.updateElement(t[g],g,_,s)}}_getStacks(t,n){const{iScale:r}=this._cachedMeta,s=r.getMatchingVisibleMetas(this._type).filter(d=>d.controller.options.grouped),o=r.options.stacked,i=[],a=this._cachedMeta.controller.getParsed(n),l=a&&a[r.axis],c=d=>{const u=d._parsed.find(g=>g[r.axis]===l),f=u&&u[d.vScale.axis];if(dt(f)||isNaN(f))return!0};for(const d of s)if(!(n!==void 0&&c(d))&&((o===!1||i.indexOf(d.stack)===-1||o===void 0&&d.stack===void 0)&&i.push(d.stack),d.index===t))break;return i.length||i.push(void 0),i}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const t=this.chart.scales,n=this.chart.options.indexAxis;return Object.keys(t).filter(r=>t[r].axis===n).shift()}_getAxis(){const t={},n=this.getFirstScaleIdForIndexAxis();for(const r of this.chart.data.datasets)t[Re(this.chart.options.indexAxis==="x"?r.xAxisID:r.yAxisID,n)]=!0;return Object.keys(t)}_getStackIndex(t,n,r){const s=this._getStacks(t,r),o=n!==void 0?s.indexOf(n):-1;return o===-1?s.length-1:o}_getRuler(){const t=this.options,n=this._cachedMeta,r=n.iScale,s=[];let o,i;for(o=0,i=n.data.length;o<i;++o)s.push(r.getPixelForValue(this.getParsed(o)[r.axis],o));const a=t.barThickness;return{min:a||RN(n),pixels:s,start:r._startPixel,end:r._endPixel,stackCount:this._getStackCount(),scale:r,grouped:t.grouped,ratio:a?1:t.categoryPercentage*t.barPercentage}}_calculateBarValuePixels(t){const{_cachedMeta:{vScale:n,_stacked:r,index:s},options:{base:o,minBarLength:i}}=this,a=o||0,l=this.getParsed(t),c=l._custom,d=pd(c);let u=l[n.axis],f=0,g=r?this.applyStack(n,l,r):u,m,p;g!==u&&(f=g-u,g=u),d&&(u=c.barStart,g=c.barEnd-c.barStart,u!==0&&yr(u)!==yr(c.barEnd)&&(f=0),f+=u);const y=!dt(o)&&!d?o:f;let b=n.getPixelForValue(y);if(this.chart.getDataVisibility(t)?m=n.getPixelForValue(f+g):m=b,p=m-b,Math.abs(p)<i){p=rT(p,n,a)*i,u===a&&(b-=p/2);const _=n.getPixelForDecimal(0),x=n.getPixelForDecimal(1),M=Math.min(_,x),C=Math.max(_,x);b=Math.max(Math.min(b,C),M),m=b+p,r&&!d&&(l._stacks[n.axis]._visualValues[s]=n.getValueForPixel(m)-n.getValueForPixel(b))}if(b===n.getPixelForValue(a)){const _=yr(p)*n.getLineWidthForValue(a)/2;b+=_,p-=_}return{size:p,base:b,head:m,center:m+p/2}}_calculateBarIndexPixels(t,n){const r=n.scale,s=this.options,o=s.skipNull,i=Re(s.maxBarThickness,1/0);let a,l;const c=this._getAxisCount();if(n.grouped){const d=o?this._getStackCount(t):n.stackCount,u=s.barThickness==="flex"?tT(t,n,s,d*c):eT(t,n,s,d*c),f=this.chart.options.indexAxis==="x"?this.getDataset().xAxisID:this.getDataset().yAxisID,g=this._getAxis().indexOf(Re(f,this.getFirstScaleIdForIndexAxis())),m=this._getStackIndex(this.index,this._cachedMeta.stack,o?t:void 0)+g;a=u.start+u.chunk*m+u.chunk/2,l=Math.min(i,u.chunk*u.ratio)}else a=r.getPixelForValue(this.getParsed(t)[r.axis],t),l=Math.min(i,n.min*n.ratio);return{base:a-l/2,head:a+l/2,center:a,size:l}}draw(){const t=this._cachedMeta,n=t.vScale,r=t.data,s=r.length;let o=0;for(;o<s;++o)this.getParsed(o)[n.axis]!==null&&!r[o].hidden&&r[o].draw(this._ctx)}}Pe(xa,"id","bar"),Pe(xa,"defaults",{datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}}),Pe(xa,"overrides",{scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}});function lT(e,t,n){let r=1,s=1,o=0,i=0;if(t<Mt){const a=e,l=a+t,c=Math.cos(a),d=Math.sin(a),u=Math.cos(l),f=Math.sin(l),g=(x,M,C)=>ha(x,a,l,!0)?1:Math.max(M,M*n,C,C*n),m=(x,M,C)=>ha(x,a,l,!0)?-1:Math.min(M,M*n,C,C*n),p=g(0,c,u),y=g(Ot,d,f),b=m(ut,c,u),_=m(ut+Ot,d,f);r=(p-b)/2,s=(y-_)/2,o=-(p+b)/2,i=-(y+_)/2}return{ratioX:r,ratioY:s,offsetX:o,offsetY:i}}class Ao extends Ts{constructor(t,n){super(t,n),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,n){const r=this.getDataset().data,s=this._cachedMeta;if(this._parsing===!1)s._parsed=r;else{let o=l=>+r[l];if(Je(r[t])){const{key:l="value"}=this._parsing;o=c=>+Lr(r[c],l)}let i,a;for(i=t,a=t+n;i<a;++i)s._parsed[i]=o(i)}}_getRotation(){return er(this.options.rotation-90)}_getCircumference(){return er(this.options.circumference)}_getRotationExtents(){let t=Mt,n=-Mt;for(let r=0;r<this.chart.data.datasets.length;++r)if(this.chart.isDatasetVisible(r)&&this.chart.getDatasetMeta(r).type===this._type){const s=this.chart.getDatasetMeta(r).controller,o=s._getRotation(),i=s._getCircumference();t=Math.min(t,o),n=Math.max(n,o+i)}return{rotation:t,circumference:n-t}}update(t){const n=this.chart,{chartArea:r}=n,s=this._cachedMeta,o=s.data,i=this.getMaxBorderWidth()+this.getMaxOffset(o)+this.options.spacing,a=Math.max((Math.min(r.width,r.height)-i)/2,0),l=Math.min(PM(this.options.cutout,a),1),c=this._getRingWeight(this.index),{circumference:d,rotation:u}=this._getRotationExtents(),{ratioX:f,ratioY:g,offsetX:m,offsetY:p}=lT(u,d,l),y=(r.width-i)/f,b=(r.height-i)/g,_=Math.max(Math.min(y,b)/2,0),x=t0(this.options.radius,_),M=Math.max(x*l,0),C=(x-M)/this._getVisibleDatasetWeightTotal();this.offsetX=m*x,this.offsetY=p*x,s.total=this.calculateTotal(),this.outerRadius=x-C*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-C*c,0),this.updateElements(o,0,o.length,t)}_circumference(t,n){const r=this.options,s=this._cachedMeta,o=this._getCircumference();return n&&r.animation.animateRotate||!this.chart.getDataVisibility(t)||s._parsed[t]===null||s.data[t].hidden?0:this.calculateCircumference(s._parsed[t]*o/Mt)}updateElements(t,n,r,s){const o=s==="reset",i=this.chart,a=i.chartArea,c=i.options.animation,d=(a.left+a.right)/2,u=(a.top+a.bottom)/2,f=o&&c.animateScale,g=f?0:this.innerRadius,m=f?0:this.outerRadius,{sharedOptions:p,includeOptions:y}=this._getSharedOptions(n,s);let b=this._getRotation(),_;for(_=0;_<n;++_)b+=this._circumference(_,o);for(_=n;_<n+r;++_){const x=this._circumference(_,o),M=t[_],C={x:d+this.offsetX,y:u+this.offsetY,startAngle:b,endAngle:b+x,circumference:x,outerRadius:m,innerRadius:g};y&&(C.options=p||this.resolveDataElementOptions(_,M.active?"active":s)),b+=x,this.updateElement(M,_,C,s)}}calculateTotal(){const t=this._cachedMeta,n=t.data;let r=0,s;for(s=0;s<n.length;s++){const o=t._parsed[s];o!==null&&!isNaN(o)&&this.chart.getDataVisibility(s)&&!n[s].hidden&&(r+=Math.abs(o))}return r}calculateCircumference(t){const n=this._cachedMeta.total;return n>0&&!isNaN(t)?Mt*(Math.abs(t)/n):0}getLabelAndValue(t){const n=this._cachedMeta,r=this.chart,s=r.data.labels||[],o=td(n._parsed[t],r.options.locale);return{label:s[t]||"",value:o}}getMaxBorderWidth(t){let n=0;const r=this.chart;let s,o,i,a,l;if(!t){for(s=0,o=r.data.datasets.length;s<o;++s)if(r.isDatasetVisible(s)){i=r.getDatasetMeta(s),t=i.data,a=i.controller;break}}if(!t)return 0;for(s=0,o=t.length;s<o;++s)l=a.resolveDataElementOptions(s),l.borderAlign!=="inner"&&(n=Math.max(n,l.borderWidth||0,l.hoverBorderWidth||0));return n}getMaxOffset(t){let n=0;for(let r=0,s=t.length;r<s;++r){const o=this.resolveDataElementOptions(r);n=Math.max(n,o.offset||0,o.hoverOffset||0)}return n}_getRingWeightOffset(t){let n=0;for(let r=0;r<t;++r)this.chart.isDatasetVisible(r)&&(n+=this._getRingWeight(r));return n}_getRingWeight(t){return Math.max(Re(this.chart.data.datasets[t].weight,1),0)}_getVisibleDatasetWeightTotal(){return this._getRingWeightOffset(this.chart.data.datasets.length)||1}}Pe(Ao,"id","doughnut"),Pe(Ao,"defaults",{datasetElementType:!1,dataElementType:"arc",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:"number",properties:["circumference","endAngle","innerRadius","outerRadius","startAngle","x","y","offset","borderWidth","spacing"]}},cutout:"50%",rotation:0,circumference:360,radius:"100%",spacing:0,indexAxis:"r"}),Pe(Ao,"descriptors",{_scriptable:t=>t!=="spacing",_indexable:t=>t!=="spacing"&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")}),Pe(Ao,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const n=t.data,{labels:{pointStyle:r,textAlign:s,color:o,useBorderRadius:i,borderRadius:a}}=t.legend.options;return n.labels.length&&n.datasets.length?n.labels.map((l,c)=>{const u=t.getDatasetMeta(0).controller.getStyle(c);return{text:l,fillStyle:u.backgroundColor,fontColor:o,hidden:!t.getDataVisibility(c),lineDash:u.borderDash,lineDashOffset:u.borderDashOffset,lineJoin:u.borderJoinStyle,lineWidth:u.borderWidth,strokeStyle:u.borderColor,textAlign:s,pointStyle:r,borderRadius:i&&(a||u.borderRadius),index:c}}):[]}},onClick(t,n,r){r.chart.toggleDataVisibility(n.index),r.chart.update()}}}});function Ur(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class md{constructor(t){Pe(this,"options");this.options=t||{}}static override(t){Object.assign(md.prototype,t)}init(){}formats(){return Ur()}parse(){return Ur()}format(){return Ur()}add(){return Ur()}diff(){return Ur()}startOf(){return Ur()}endOf(){return Ur()}}var cT={_date:md};function dT(e,t,n,r){const{controller:s,data:o,_sorted:i}=e,a=s._cachedMeta.iScale,l=e.dataset&&e.dataset.options?e.dataset.options.spanGaps:null;if(a&&t===a.axis&&t!=="r"&&i&&o.length){const c=a._reversePixels?qM:Jc;if(r){if(s._sharedOptions){const d=o[0],u=typeof d.getRange=="function"&&d.getRange(t);if(u){const f=c(o,t,n-u),g=c(o,t,n+u);return{lo:f.lo,hi:g.hi}}}}else{const d=c(o,t,n);if(l){const{vScale:u}=s._cachedMeta,{_parsed:f}=e,g=f.slice(0,d.lo+1).reverse().findIndex(p=>!dt(p[u.axis]));d.lo-=Math.max(0,g);const m=f.slice(d.hi).findIndex(p=>!dt(p[u.axis]));d.hi+=Math.max(0,m)}return d}}return{lo:0,hi:o.length-1}}function wa(e,t,n,r,s){const o=e.getSortedVisibleDatasetMetas(),i=n[t];for(let a=0,l=o.length;a<l;++a){const{index:c,data:d}=o[a],{lo:u,hi:f}=dT(o[a],t,i,s);for(let g=u;g<=f;++g){const m=d[g];m.skip||r(m,c,g)}}}function uT(e){const t=e.indexOf("x")!==-1,n=e.indexOf("y")!==-1;return function(r,s){const o=t?Math.abs(r.x-s.x):0,i=n?Math.abs(r.y-s.y):0;return Math.sqrt(Math.pow(o,2)+Math.pow(i,2))}}function bd(e,t,n,r,s){const o=[];return!s&&!e.isPointInArea(t)||wa(e,n,t,function(a,l,c){!s&&!_0(a,e.chartArea,0)||a.inRange(t.x,t.y,r)&&o.push({element:a,datasetIndex:l,index:c})},!0),o}function fT(e,t,n,r){let s=[];function o(i,a,l){const{startAngle:c,endAngle:d}=i.getProps(["startAngle","endAngle"],r),{angle:u}=c0(i,{x:t.x,y:t.y});ha(u,c,d)&&s.push({element:i,datasetIndex:a,index:l})}return wa(e,n,t,o),s}function hT(e,t,n,r,s,o){let i=[];const a=uT(n);let l=Number.POSITIVE_INFINITY;function c(d,u,f){const g=d.inRange(t.x,t.y,s);if(r&&!g)return;const m=d.getCenterPoint(s);if(!(!!o||e.isPointInArea(m))&&!g)return;const y=a(t,m);y<l?(i=[{element:d,datasetIndex:u,index:f}],l=y):y===l&&i.push({element:d,datasetIndex:u,index:f})}return wa(e,n,t,c),i}function yd(e,t,n,r,s,o){return!o&&!e.isPointInArea(t)?[]:n==="r"&&!r?fT(e,t,n,s):hT(e,t,n,r,s,o)}function q0(e,t,n,r,s){const o=[],i=n==="x"?"inXRange":"inYRange";let a=!1;return wa(e,n,t,(l,c,d)=>{l[i]&&l[i](t[n],s)&&(o.push({element:l,datasetIndex:c,index:d}),a=a||l.inRange(t.x,t.y,s))}),r&&!a?[]:o}var gT={modes:{index(e,t,n,r){const s=Hr(t,e),o=n.axis||"x",i=n.includeInvisible||!1,a=n.intersect?bd(e,s,o,r,i):yd(e,s,o,!1,r,i),l=[];return a.length?(e.getSortedVisibleDatasetMetas().forEach(c=>{const d=a[0].index,u=c.data[d];u&&!u.skip&&l.push({element:u,datasetIndex:c.index,index:d})}),l):[]},dataset(e,t,n,r){const s=Hr(t,e),o=n.axis||"xy",i=n.includeInvisible||!1;let a=n.intersect?bd(e,s,o,r,i):yd(e,s,o,!1,r,i);if(a.length>0){const l=a[0].datasetIndex,c=e.getDatasetMeta(l).data;a=[];for(let d=0;d<c.length;++d)a.push({element:c[d],datasetIndex:l,index:d})}return a},point(e,t,n,r){const s=Hr(t,e),o=n.axis||"xy",i=n.includeInvisible||!1;return bd(e,s,o,r,i)},nearest(e,t,n,r){const s=Hr(t,e),o=n.axis||"xy",i=n.includeInvisible||!1;return yd(e,s,o,n.intersect,r,i)},x(e,t,n,r){const s=Hr(t,e);return q0(e,s,"x",n.intersect,r)},y(e,t,n,r){const s=Hr(t,e);return q0(e,s,"y",n.intersect,r)}}};const Z0=["left","top","right","bottom"];function Do(e,t){return e.filter(n=>n.pos===t)}function Q0(e,t){return e.filter(n=>Z0.indexOf(n.pos)===-1&&n.box.axis===t)}function Io(e,t){return e.sort((n,r)=>{const s=t?r:n,o=t?n:r;return s.weight===o.weight?s.index-o.index:s.weight-o.weight})}function pT(e){const t=[];let n,r,s,o,i,a;for(n=0,r=(e||[]).length;n<r;++n)s=e[n],{position:o,options:{stack:i,stackWeight:a=1}}=s,t.push({index:n,box:s,pos:o,horizontal:s.isHorizontal(),weight:s.weight,stack:i&&o+i,stackWeight:a});return t}function mT(e){const t={};for(const n of e){const{stack:r,pos:s,stackWeight:o}=n;if(!r||!Z0.includes(s))continue;const i=t[r]||(t[r]={count:0,placed:0,weight:0,size:0});i.count++,i.weight+=o}return t}function bT(e,t){const n=mT(e),{vBoxMaxWidth:r,hBoxMaxHeight:s}=t;let o,i,a;for(o=0,i=e.length;o<i;++o){a=e[o];const{fullSize:l}=a.box,c=n[a.stack],d=c&&a.stackWeight/c.weight;a.horizontal?(a.width=d?d*r:l&&t.availableWidth,a.height=s):(a.width=r,a.height=d?d*s:l&&t.availableHeight)}return n}function yT(e){const t=pT(e),n=Io(t.filter(c=>c.box.fullSize),!0),r=Io(Do(t,"left"),!0),s=Io(Do(t,"right")),o=Io(Do(t,"top"),!0),i=Io(Do(t,"bottom")),a=Q0(t,"x"),l=Q0(t,"y");return{fullSize:n,leftAndTop:r.concat(o),rightAndBottom:s.concat(l).concat(i).concat(a),chartArea:Do(t,"chartArea"),vertical:r.concat(s).concat(l),horizontal:o.concat(i).concat(a)}}function J0(e,t,n,r){return Math.max(e[n],t[n])+Math.max(e[r],t[r])}function R0(e,t){e.top=Math.max(e.top,t.top),e.left=Math.max(e.left,t.left),e.bottom=Math.max(e.bottom,t.bottom),e.right=Math.max(e.right,t.right)}function vT(e,t,n,r){const{pos:s,box:o}=n,i=e.maxPadding;if(!Je(s)){n.size&&(e[s]-=n.size);const u=r[n.stack]||{size:0,count:1};u.size=Math.max(u.size,n.horizontal?o.height:o.width),n.size=u.size/u.count,e[s]+=n.size}o.getPadding&&R0(i,o.getPadding());const a=Math.max(0,t.outerWidth-J0(i,e,"left","right")),l=Math.max(0,t.outerHeight-J0(i,e,"top","bottom")),c=a!==e.w,d=l!==e.h;return e.w=a,e.h=l,n.horizontal?{same:c,other:d}:{same:d,other:c}}function xT(e){const t=e.maxPadding;function n(r){const s=Math.max(t[r]-e[r],0);return e[r]+=s,s}e.y+=n("top"),e.x+=n("left"),n("right"),n("bottom")}function wT(e,t){const n=t.maxPadding;function r(s){const o={left:0,top:0,right:0,bottom:0};return s.forEach(i=>{o[i]=Math.max(t[i],n[i])}),o}return r(e?["left","right"]:["top","bottom"])}function Lo(e,t,n,r){const s=[];let o,i,a,l,c,d;for(o=0,i=e.length,c=0;o<i;++o){a=e[o],l=a.box,l.update(a.width||t.w,a.height||t.h,wT(a.horizontal,t));const{same:u,other:f}=vT(t,n,a,r);c|=u&&s.length,d=d||f,l.fullSize||s.push(a)}return c&&Lo(s,t,n,r)||d}function ka(e,t,n,r,s){e.top=n,e.left=t,e.right=t+r,e.bottom=n+s,e.width=r,e.height=s}function em(e,t,n,r){const s=n.padding;let{x:o,y:i}=t;for(const a of e){const l=a.box,c=r[a.stack]||{placed:0,weight:1},d=a.stackWeight/c.weight||1;if(a.horizontal){const u=t.w*d,f=c.size||l.height;Eo(c.start)&&(i=c.start),l.fullSize?ka(l,s.left,i,n.outerWidth-s.right-s.left,f):ka(l,t.left+c.placed,i,u,f),c.start=i,c.placed+=u,i=l.bottom}else{const u=t.h*d,f=c.size||l.width;Eo(c.start)&&(o=c.start),l.fullSize?ka(l,o,s.top,f,n.outerHeight-s.bottom-s.top):ka(l,o,t.top+c.placed,f,u),c.start=o,c.placed+=u,o=l.right}}t.x=o,t.y=i}var xn={addBox(e,t){e.boxes||(e.boxes=[]),t.fullSize=t.fullSize||!1,t.position=t.position||"top",t.weight=t.weight||0,t._layers=t._layers||function(){return[{z:0,draw(n){t.draw(n)}}]},e.boxes.push(t)},removeBox(e,t){const n=e.boxes?e.boxes.indexOf(t):-1;n!==-1&&e.boxes.splice(n,1)},configure(e,t,n){t.fullSize=n.fullSize,t.position=n.position,t.weight=n.weight},update(e,t,n,r){if(!e)return;const s=vn(e.options.layout.padding),o=Math.max(t-s.width,0),i=Math.max(n-s.height,0),a=yT(e.boxes),l=a.vertical,c=a.horizontal;lt(e.boxes,p=>{typeof p.beforeLayout=="function"&&p.beforeLayout()});const d=l.reduce((p,y)=>y.box.options&&y.box.options.display===!1?p:p+1,0)||1,u=Object.freeze({outerWidth:t,outerHeight:n,padding:s,availableWidth:o,availableHeight:i,vBoxMaxWidth:o/2/d,hBoxMaxHeight:i/2}),f=Object.assign({},s);R0(f,vn(r));const g=Object.assign({maxPadding:f,w:o,h:i,x:s.left,y:s.top},s),m=bT(l.concat(c),u);Lo(a.fullSize,g,u,m),Lo(l,g,u,m),Lo(c,g,u,m)&&Lo(l,g,u,m),xT(g),em(a.leftAndTop,g,u,m),g.x+=g.w,g.y+=g.h,em(a.rightAndBottom,g,u,m),e.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},lt(a.chartArea,p=>{const y=p.box;Object.assign(y,e.chartArea),y.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})})}};class tm{acquireContext(t,n){}releaseContext(t){return!1}addEventListener(t,n,r){}removeEventListener(t,n,r){}getDevicePixelRatio(){return 1}getMaximumSize(t,n,r,s){return n=Math.max(0,n||t.width),r=r||t.height,{width:n,height:Math.max(0,s?Math.floor(n/s):r)}}isAttached(t){return!0}updateConfig(t){}}class kT extends tm{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}}const Ca="$chartjs",CT={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},nm=e=>e===null||e==="";function ST(e,t){const n=e.style,r=e.getAttribute("height"),s=e.getAttribute("width");if(e[Ca]={initial:{height:r,width:s,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||"block",n.boxSizing=n.boxSizing||"border-box",nm(s)){const o=L0(e,"width");o!==void 0&&(e.width=o)}if(nm(r))if(e.style.height==="")e.height=e.width/(t||2);else{const o=L0(e,"height");o!==void 0&&(e.height=o)}return e}const rm=IN?{passive:!0}:!1;function _T(e,t,n){e&&e.addEventListener(t,n,rm)}function ET(e,t,n){e&&e.canvas&&e.canvas.removeEventListener(t,n,rm)}function MT(e,t){const n=CT[e.type]||e.type,{x:r,y:s}=Hr(e,t);return{type:n,chart:t,native:e,x:r!==void 0?r:null,y:s!==void 0?s:null}}function Sa(e,t){for(const n of e)if(n===t||n.contains(t))return!0}function NT(e,t,n){const r=e.canvas,s=new MutationObserver(o=>{let i=!1;for(const a of o)i=i||Sa(a.addedNodes,r),i=i&&!Sa(a.removedNodes,r);i&&n()});return s.observe(document,{childList:!0,subtree:!0}),s}function TT(e,t,n){const r=e.canvas,s=new MutationObserver(o=>{let i=!1;for(const a of o)i=i||Sa(a.removedNodes,r),i=i&&!Sa(a.addedNodes,r);i&&n()});return s.observe(document,{childList:!0,subtree:!0}),s}const Po=new Map;let sm=0;function om(){const e=window.devicePixelRatio;e!==sm&&(sm=e,Po.forEach((t,n)=>{n.currentDevicePixelRatio!==e&&t()}))}function OT(e,t){Po.size||window.addEventListener("resize",om),Po.set(e,t)}function AT(e){Po.delete(e),Po.size||window.removeEventListener("resize",om)}function DT(e,t,n){const r=e.canvas,s=r&&ud(r);if(!s)return;const o=g0((a,l)=>{const c=s.clientWidth;n(a,l),c<s.clientWidth&&n()},window),i=new ResizeObserver(a=>{const l=a[0],c=l.contentRect.width,d=l.contentRect.height;c===0&&d===0||o(c,d)});return i.observe(s),OT(e,o),i}function vd(e,t,n){n&&n.disconnect(),t==="resize"&&AT(e)}function IT(e,t,n){const r=e.canvas,s=g0(o=>{e.ctx!==null&&n(MT(o,e))},e);return _T(r,t,s),s}class LT extends tm{acquireContext(t,n){const r=t&&t.getContext&&t.getContext("2d");return r&&r.canvas===t?(ST(t,n),r):null}releaseContext(t){const n=t.canvas;if(!n[Ca])return!1;const r=n[Ca].initial;["height","width"].forEach(o=>{const i=r[o];dt(i)?n.removeAttribute(o):n.setAttribute(o,i)});const s=r.style||{};return Object.keys(s).forEach(o=>{n.style[o]=s[o]}),n.width=n.width,delete n[Ca],!0}addEventListener(t,n,r){this.removeEventListener(t,n);const s=t.$proxies||(t.$proxies={}),i={attach:NT,detach:TT,resize:DT}[n]||IT;s[n]=i(t,n,r)}removeEventListener(t,n){const r=t.$proxies||(t.$proxies={}),s=r[n];if(!s)return;({attach:vd,detach:vd,resize:vd}[n]||ET)(t,n,s),r[n]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,n,r,s){return DN(t,n,r,s)}isAttached(t){const n=t&&ud(t);return!!(n&&n.isConnected)}}function PT(e){return!dd()||typeof OffscreenCanvas<"u"&&e instanceof OffscreenCanvas?kT:LT}let Br=(Va=class{constructor(){Pe(this,"x");Pe(this,"y");Pe(this,"active",!1);Pe(this,"options");Pe(this,"$animations")}tooltipPosition(t){const{x:n,y:r}=this.getProps(["x","y"],t);return{x:n,y:r}}hasValue(){return fa(this.x)&&fa(this.y)}getProps(t,n){const r=this.$animations;if(!n||!r)return this;const s={};return t.forEach(o=>{s[o]=r[o]&&r[o].active()?r[o]._to:this[o]}),s}},Pe(Va,"defaults",{}),Pe(Va,"defaultRoutes"),Va);function $T(e,t){const n=e.options.ticks,r=FT(e),s=Math.min(n.maxTicksLimit||r,r),o=n.major.enabled?VT(t):[],i=o.length,a=o[0],l=o[i-1],c=[];if(i>s)return HT(t,c,o,i/s),c;const d=zT(o,t,s);if(i>0){let u,f;const g=i>1?Math.round((l-a)/(i-1)):null;for(_a(t,c,d,dt(g)?0:a-g,a),u=0,f=i-1;u<f;u++)_a(t,c,d,o[u],o[u+1]);return _a(t,c,d,l,dt(g)?t.length:l+g),c}return _a(t,c,d),c}function FT(e){const t=e.options.offset,n=e._tickSize(),r=e._length/n+(t?0:1),s=e._maxLength/n;return Math.floor(Math.min(r,s))}function zT(e,t,n){const r=UT(e),s=t.length/n;if(!r)return Math.max(s,1);const o=BM(r);for(let i=0,a=o.length-1;i<a;i++){const l=o[i];if(l>s)return l}return Math.max(s,1)}function VT(e){const t=[];let n,r;for(n=0,r=e.length;n<r;n++)e[n].major&&t.push(n);return t}function HT(e,t,n,r){let s=0,o=n[0],i;for(r=Math.ceil(r),i=0;i<e.length;i++)i===o&&(t.push(e[i]),s++,o=n[s*r])}function _a(e,t,n,r,s){const o=Re(r,0),i=Math.min(Re(s,e.length),e.length);let a=0,l,c,d;for(n=Math.ceil(n),s&&(l=s-r,n=l/Math.floor(l/n)),d=o;d<0;)a++,d=Math.round(o+a*n);for(c=Math.max(o,0);c<i;c++)c===d&&(t.push(e[c]),a++,d=Math.round(o+a*n))}function UT(e){const t=e.length;let n,r;if(t<2)return!1;for(r=e[0],n=1;n<t;++n)if(e[n]-e[n-1]!==r)return!1;return r}const BT=e=>e==="left"?"right":e==="right"?"left":e,im=(e,t,n)=>t==="top"||t==="left"?e[t]+n:e[t]-n,am=(e,t)=>Math.min(t||e,e);function lm(e,t){const n=[],r=e.length/t,s=e.length;let o=0;for(;o<s;o+=r)n.push(e[Math.floor(o)]);return n}function jT(e,t,n){const r=e.ticks.length,s=Math.min(t,r-1),o=e._startPixel,i=e._endPixel,a=1e-6;let l=e.getPixelForTick(s),c;if(!(n&&(r===1?c=Math.max(l-o,i-l):t===0?c=(e.getPixelForTick(1)-l)/2:c=(l-e.getPixelForTick(s-1))/2,l+=s<t?c:-c,l<o-a||l>i+a)))return l}function WT(e,t){lt(e,n=>{const r=n.gc,s=r.length/2;let o;if(s>t){for(o=0;o<s;++o)delete n.data[r[o]];r.splice(0,s)}})}function $o(e){return e.drawTicks?e.tickLength:0}function cm(e,t){if(!e.display)return 0;const n=Kt(e.font,t),r=vn(e.padding);return(Tt(e.text)?e.text.length:1)*n.lineHeight+r.height}function GT(e,t){return Es(e,{scale:t,type:"scale"})}function YT(e,t,n){return Es(e,{tick:n,index:t,type:"tick"})}function XT(e,t,n){let r=Rc(e);return(n&&t!=="right"||!n&&t==="right")&&(r=BT(r)),r}function KT(e,t,n,r){const{top:s,left:o,bottom:i,right:a,chart:l}=e,{chartArea:c,scales:d}=l;let u=0,f,g,m;const p=i-s,y=a-o;if(e.isHorizontal()){if(g=Xt(r,o,a),Je(n)){const b=Object.keys(n)[0],_=n[b];m=d[b].getPixelForValue(_)+p-t}else n==="center"?m=(c.bottom+c.top)/2+p-t:m=im(e,n,t);f=a-o}else{if(Je(n)){const b=Object.keys(n)[0],_=n[b];g=d[b].getPixelForValue(_)-y+t}else n==="center"?g=(c.left+c.right)/2-y+t:g=im(e,n,t);m=Xt(r,i,s),u=n==="left"?-Ot:Ot}return{titleX:g,titleY:m,maxWidth:f,rotation:u}}class Os extends Br{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){this.options=t.setContext(this.getContext()),this.axis=t.axis,this._userMin=this.parse(t.min),this._userMax=this.parse(t.max),this._suggestedMin=this.parse(t.suggestedMin),this._suggestedMax=this.parse(t.suggestedMax)}parse(t,n){return t}getUserBounds(){let{_userMin:t,_userMax:n,_suggestedMin:r,_suggestedMax:s}=this;return t=Gn(t,Number.POSITIVE_INFINITY),n=Gn(n,Number.NEGATIVE_INFINITY),r=Gn(r,Number.POSITIVE_INFINITY),s=Gn(s,Number.NEGATIVE_INFINITY),{min:Gn(t,r),max:Gn(n,s),minDefined:yn(t),maxDefined:yn(n)}}getMinMax(t){let{min:n,max:r,minDefined:s,maxDefined:o}=this.getUserBounds(),i;if(s&&o)return{min:n,max:r};const a=this.getMatchingVisibleMetas();for(let l=0,c=a.length;l<c;++l)i=a[l].controller.getMinMax(this,t),s||(n=Math.min(n,i.min)),o||(r=Math.max(r,i.max));return n=o&&n>r?r:n,r=s&&n>r?n:r,{min:Gn(n,Gn(r,n)),max:Gn(r,Gn(n,r))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){mt(this.options.beforeUpdate,[this])}update(t,n,r){const{beginAtZero:s,grace:o,ticks:i}=this.options,a=i.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=n,this._margins=r=Object.assign({left:0,right:0,top:0,bottom:0},r),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+r.left+r.right:this.height+r.top+r.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=bN(this,o,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const l=a<this.ticks.length;this._convertTicksToLabels(l?lm(this.ticks,a):this.ticks),this.configure(),this.beforeCalculateLabelRotation(),this.calculateLabelRotation(),this.afterCalculateLabelRotation(),i.display&&(i.autoSkip||i.source==="auto")&&(this.ticks=$T(this,this.ticks),this._labelSizes=null,this.afterAutoSkip()),l&&this._convertTicksToLabels(this.ticks),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate()}configure(){let t=this.options.reverse,n,r;this.isHorizontal()?(n=this.left,r=this.right):(n=this.top,r=this.bottom,t=!t),this._startPixel=n,this._endPixel=r,this._reversePixels=t,this._length=r-n,this._alignToPixels=this.options.alignToPixels}afterUpdate(){mt(this.options.afterUpdate,[this])}beforeSetDimensions(){mt(this.options.beforeSetDimensions,[this])}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0}afterSetDimensions(){mt(this.options.afterSetDimensions,[this])}_callHooks(t){this.chart.notifyPlugins(t,this.getContext()),mt(this.options[t],[this])}beforeDataLimits(){this._callHooks("beforeDataLimits")}determineDataLimits(){}afterDataLimits(){this._callHooks("afterDataLimits")}beforeBuildTicks(){this._callHooks("beforeBuildTicks")}buildTicks(){return[]}afterBuildTicks(){this._callHooks("afterBuildTicks")}beforeTickToLabelConversion(){mt(this.options.beforeTickToLabelConversion,[this])}generateTickLabels(t){const n=this.options.ticks;let r,s,o;for(r=0,s=t.length;r<s;r++)o=t[r],o.label=mt(n.callback,[o.value,r,t],this)}afterTickToLabelConversion(){mt(this.options.afterTickToLabelConversion,[this])}beforeCalculateLabelRotation(){mt(this.options.beforeCalculateLabelRotation,[this])}calculateLabelRotation(){const t=this.options,n=t.ticks,r=am(this.ticks.length,t.ticks.maxTicksLimit),s=n.minRotation||0,o=n.maxRotation;let i=s,a,l,c;if(!this._isVisible()||!n.display||s>=o||r<=1||!this.isHorizontal()){this.labelRotation=s;return}const d=this._getLabelSizes(),u=d.widest.width,f=d.highest.height,g=sn(this.chart.width-u,0,this.maxWidth);a=t.offset?this.maxWidth/r:g/(r-1),u+6>a&&(a=g/(r-(t.offset?.5:1)),l=this.maxHeight-$o(t.grid)-n.padding-cm(t.title,this.chart.options.font),c=Math.sqrt(u*u+f*f),i=YM(Math.min(Math.asin(sn((d.highest.height+6)/a,-1,1)),Math.asin(sn(l/c,-1,1))-Math.asin(sn(f/c,-1,1)))),i=Math.max(s,Math.min(o,i))),this.labelRotation=i}afterCalculateLabelRotation(){mt(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){mt(this.options.beforeFit,[this])}fit(){const t={width:0,height:0},{chart:n,options:{ticks:r,title:s,grid:o}}=this,i=this._isVisible(),a=this.isHorizontal();if(i){const l=cm(s,n.options.font);if(a?(t.width=this.maxWidth,t.height=$o(o)+l):(t.height=this.maxHeight,t.width=$o(o)+l),r.display&&this.ticks.length){const{first:c,last:d,widest:u,highest:f}=this._getLabelSizes(),g=r.padding*2,m=er(this.labelRotation),p=Math.cos(m),y=Math.sin(m);if(a){const b=r.mirror?0:y*u.width+p*f.height;t.height=Math.min(this.maxHeight,t.height+b+g)}else{const b=r.mirror?0:p*u.width+y*f.height;t.width=Math.min(this.maxWidth,t.width+b+g)}this._calculatePadding(c,d,y,p)}}this._handleMargins(),a?(this.width=this._length=n.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=n.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,n,r,s){const{ticks:{align:o,padding:i},position:a}=this.options,l=this.labelRotation!==0,c=a!=="top"&&this.axis==="x";if(this.isHorizontal()){const d=this.getPixelForTick(0)-this.left,u=this.right-this.getPixelForTick(this.ticks.length-1);let f=0,g=0;l?c?(f=s*t.width,g=r*n.height):(f=r*t.height,g=s*n.width):o==="start"?g=n.width:o==="end"?f=t.width:o!=="inner"&&(f=t.width/2,g=n.width/2),this.paddingLeft=Math.max((f-d+i)*this.width/(this.width-d),0),this.paddingRight=Math.max((g-u+i)*this.width/(this.width-u),0)}else{let d=n.height/2,u=t.height/2;o==="start"?(d=0,u=t.height):o==="end"&&(d=n.height,u=0),this.paddingTop=d+i,this.paddingBottom=u+i}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){mt(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:n}=this.options;return n==="top"||n==="bottom"||t==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){this.beforeTickToLabelConversion(),this.generateTickLabels(t);let n,r;for(n=0,r=t.length;n<r;n++)dt(t[n].label)&&(t.splice(n,1),r--,n--);this.afterTickToLabelConversion()}_getLabelSizes(){let t=this._labelSizes;if(!t){const n=this.options.ticks.sampleSize;let r=this.ticks;n<r.length&&(r=lm(r,n)),this._labelSizes=t=this._computeLabelSizes(r,r.length,this.options.ticks.maxTicksLimit)}return t}_computeLabelSizes(t,n,r){const{ctx:s,_longestTextCache:o}=this,i=[],a=[],l=Math.floor(n/am(n,r));let c=0,d=0,u,f,g,m,p,y,b,_,x,M,C;for(u=0;u<n;u+=l){if(m=t[u].label,p=this._resolveTickFontOptions(u),s.font=y=p.string,b=o[y]=o[y]||{data:{},gc:[]},_=p.lineHeight,x=M=0,!dt(m)&&!Tt(m))x=w0(s,b.data,b.gc,x,m),M=_;else if(Tt(m))for(f=0,g=m.length;f<g;++f)C=m[f],!dt(C)&&!Tt(C)&&(x=w0(s,b.data,b.gc,x,C),M+=_);i.push(x),a.push(M),c=Math.max(x,c),d=Math.max(M,d)}WT(o,n);const k=i.indexOf(c),N=a.indexOf(d),S=w=>({width:i[w]||0,height:a[w]||0});return{first:S(0),last:S(n-1),widest:S(k),highest:S(N),widths:i,heights:a}}getLabelForValue(t){return t}getPixelForValue(t,n){return NaN}getValueForPixel(t){}getPixelForTick(t){const n=this.ticks;return t<0||t>n.length-1?null:this.getPixelForValue(n[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);const n=this._startPixel+t*this._length;return KM(this._alignToPixels?zr(this.chart,n,0):n)}getDecimalForPixel(t){const n=(t-this._startPixel)/this._length;return this._reversePixels?1-n:n}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:n}=this;return t<0&&n<0?n:t>0&&n>0?t:0}getContext(t){const n=this.ticks||[];if(t>=0&&t<n.length){const r=n[t];return r.$context||(r.$context=YT(this.getContext(),t,r))}return this.$context||(this.$context=GT(this.chart.getContext(),this))}_tickSize(){const t=this.options.ticks,n=er(this.labelRotation),r=Math.abs(Math.cos(n)),s=Math.abs(Math.sin(n)),o=this._getLabelSizes(),i=t.autoSkipPadding||0,a=o?o.widest.width+i:0,l=o?o.highest.height+i:0;return this.isHorizontal()?l*r>a*s?a/r:l/s:l*s<a*r?l/r:a/s}_isVisible(){const t=this.options.display;return t!=="auto"?!!t:this.getMatchingVisibleMetas().length>0}_computeGridLineItems(t){const n=this.axis,r=this.chart,s=this.options,{grid:o,position:i,border:a}=s,l=o.offset,c=this.isHorizontal(),u=this.ticks.length+(l?1:0),f=$o(o),g=[],m=a.setContext(this.getContext()),p=m.display?m.width:0,y=p/2,b=function(O){return zr(r,O,p)};let _,x,M,C,k,N,S,w,E,D,P,L;if(i==="top")_=b(this.bottom),N=this.bottom-f,w=_-y,D=b(t.top)+y,L=t.bottom;else if(i==="bottom")_=b(this.top),D=t.top,L=b(t.bottom)-y,N=_+y,w=this.top+f;else if(i==="left")_=b(this.right),k=this.right-f,S=_-y,E=b(t.left)+y,P=t.right;else if(i==="right")_=b(this.left),E=t.left,P=b(t.right)-y,k=_+y,S=this.left+f;else if(n==="x"){if(i==="center")_=b((t.top+t.bottom)/2+.5);else if(Je(i)){const O=Object.keys(i)[0],T=i[O];_=b(this.chart.scales[O].getPixelForValue(T))}D=t.top,L=t.bottom,N=_+y,w=N+f}else if(n==="y"){if(i==="center")_=b((t.left+t.right)/2);else if(Je(i)){const O=Object.keys(i)[0],T=i[O];_=b(this.chart.scales[O].getPixelForValue(T))}k=_-y,S=k-f,E=t.left,P=t.right}const $=Re(s.ticks.maxTicksLimit,u),A=Math.max(1,Math.ceil(u/$));for(x=0;x<u;x+=A){const O=this.getContext(x),T=o.setContext(O),z=a.setContext(O),H=T.lineWidth,j=T.color,G=z.dash||[],U=z.dashOffset,W=T.tickWidth,Y=T.tickColor,V=T.tickBorderDash||[],X=T.tickBorderDashOffset;M=jT(this,x,l),M!==void 0&&(C=zr(r,M,H),c?k=S=E=P=C:N=w=D=L=C,g.push({tx1:k,ty1:N,tx2:S,ty2:w,x1:E,y1:D,x2:P,y2:L,width:H,color:j,borderDash:G,borderDashOffset:U,tickWidth:W,tickColor:Y,tickBorderDash:V,tickBorderDashOffset:X}))}return this._ticksLength=u,this._borderValue=_,g}_computeLabelItems(t){const n=this.axis,r=this.options,{position:s,ticks:o}=r,i=this.isHorizontal(),a=this.ticks,{align:l,crossAlign:c,padding:d,mirror:u}=o,f=$o(r.grid),g=f+d,m=u?-d:g,p=-er(this.labelRotation),y=[];let b,_,x,M,C,k,N,S,w,E,D,P,L="middle";if(s==="top")k=this.bottom-m,N=this._getXAxisLabelAlignment();else if(s==="bottom")k=this.top+m,N=this._getXAxisLabelAlignment();else if(s==="left"){const A=this._getYAxisLabelAlignment(f);N=A.textAlign,C=A.x}else if(s==="right"){const A=this._getYAxisLabelAlignment(f);N=A.textAlign,C=A.x}else if(n==="x"){if(s==="center")k=(t.top+t.bottom)/2+g;else if(Je(s)){const A=Object.keys(s)[0],O=s[A];k=this.chart.scales[A].getPixelForValue(O)+g}N=this._getXAxisLabelAlignment()}else if(n==="y"){if(s==="center")C=(t.left+t.right)/2-g;else if(Je(s)){const A=Object.keys(s)[0],O=s[A];C=this.chart.scales[A].getPixelForValue(O)}N=this._getYAxisLabelAlignment(f).textAlign}n==="y"&&(l==="start"?L="top":l==="end"&&(L="bottom"));const $=this._getLabelSizes();for(b=0,_=a.length;b<_;++b){x=a[b],M=x.label;const A=o.setContext(this.getContext(b));S=this.getPixelForTick(b)+o.labelOffset,w=this._resolveTickFontOptions(b),E=w.lineHeight,D=Tt(M)?M.length:1;const O=D/2,T=A.color,z=A.textStrokeColor,H=A.textStrokeWidth;let j=N;i?(C=S,N==="inner"&&(b===_-1?j=this.options.reverse?"left":"right":b===0?j=this.options.reverse?"right":"left":j="center"),s==="top"?c==="near"||p!==0?P=-D*E+E/2:c==="center"?P=-$.highest.height/2-O*E+E:P=-$.highest.height+E/2:c==="near"||p!==0?P=E/2:c==="center"?P=$.highest.height/2-O*E:P=$.highest.height-D*E,u&&(P*=-1),p!==0&&!A.showLabelBackdrop&&(C+=E/2*Math.sin(p))):(k=S,P=(1-D)*E/2);let G;if(A.showLabelBackdrop){const U=vn(A.backdropPadding),W=$.heights[b],Y=$.widths[b];let V=P-U.top,X=0-U.left;switch(L){case"middle":V-=W/2;break;case"bottom":V-=W;break}switch(N){case"center":X-=Y/2;break;case"right":X-=Y;break;case"inner":b===_-1?X-=Y:b>0&&(X-=Y/2);break}G={left:X,top:V,width:Y+U.width,height:W+U.height,color:A.backdropColor}}y.push({label:M,font:w,textOffset:P,options:{rotation:p,color:T,strokeColor:z,strokeWidth:H,textAlign:j,textBaseline:L,translation:[C,k],backdrop:G}})}return y}_getXAxisLabelAlignment(){const{position:t,ticks:n}=this.options;if(-er(this.labelRotation))return t==="top"?"left":"right";let s="center";return n.align==="start"?s="left":n.align==="end"?s="right":n.align==="inner"&&(s="inner"),s}_getYAxisLabelAlignment(t){const{position:n,ticks:{crossAlign:r,mirror:s,padding:o}}=this.options,i=this._getLabelSizes(),a=t+o,l=i.widest.width;let c,d;return n==="left"?s?(d=this.right+o,r==="near"?c="left":r==="center"?(c="center",d+=l/2):(c="right",d+=l)):(d=this.right-a,r==="near"?c="right":r==="center"?(c="center",d-=l/2):(c="left",d=this.left)):n==="right"?s?(d=this.left+o,r==="near"?c="right":r==="center"?(c="center",d-=l/2):(c="left",d-=l)):(d=this.left+a,r==="near"?c="left":r==="center"?(c="center",d+=l/2):(c="right",d=this.right)):c="right",{textAlign:c,x:d}}_computeLabelArea(){if(this.options.ticks.mirror)return;const t=this.chart,n=this.options.position;if(n==="left"||n==="right")return{top:0,left:this.left,bottom:t.height,right:this.right};if(n==="top"||n==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:t.width}}drawBackground(){const{ctx:t,options:{backgroundColor:n},left:r,top:s,width:o,height:i}=this;n&&(t.save(),t.fillStyle=n,t.fillRect(r,s,o,i),t.restore())}getLineWidthForValue(t){const n=this.options.grid;if(!this._isVisible()||!n.display)return 0;const s=this.ticks.findIndex(o=>o.value===t);return s>=0?n.setContext(this.getContext(s)).lineWidth:0}drawGrid(t){const n=this.options.grid,r=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t));let o,i;const a=(l,c,d)=>{!d.width||!d.color||(r.save(),r.lineWidth=d.width,r.strokeStyle=d.color,r.setLineDash(d.borderDash||[]),r.lineDashOffset=d.borderDashOffset,r.beginPath(),r.moveTo(l.x,l.y),r.lineTo(c.x,c.y),r.stroke(),r.restore())};if(n.display)for(o=0,i=s.length;o<i;++o){const l=s[o];n.drawOnChartArea&&a({x:l.x1,y:l.y1},{x:l.x2,y:l.y2},l),n.drawTicks&&a({x:l.tx1,y:l.ty1},{x:l.tx2,y:l.ty2},{color:l.tickColor,width:l.tickWidth,borderDash:l.tickBorderDash,borderDashOffset:l.tickBorderDashOffset})}}drawBorder(){const{chart:t,ctx:n,options:{border:r,grid:s}}=this,o=r.setContext(this.getContext()),i=r.display?o.width:0;if(!i)return;const a=s.setContext(this.getContext(0)).lineWidth,l=this._borderValue;let c,d,u,f;this.isHorizontal()?(c=zr(t,this.left,i)-i/2,d=zr(t,this.right,a)+a/2,u=f=l):(u=zr(t,this.top,i)-i/2,f=zr(t,this.bottom,a)+a/2,c=d=l),n.save(),n.lineWidth=o.width,n.strokeStyle=o.color,n.beginPath(),n.moveTo(c,u),n.lineTo(d,f),n.stroke(),n.restore()}drawLabels(t){if(!this.options.ticks.display)return;const r=this.ctx,s=this._computeLabelArea();s&&sd(r,s);const o=this.getLabelItems(t);for(const i of o){const a=i.options,l=i.font,c=i.label,d=i.textOffset;To(r,c,0,d,l,a)}s&&od(r)}drawTitle(){const{ctx:t,options:{position:n,title:r,reverse:s}}=this;if(!r.display)return;const o=Kt(r.font),i=vn(r.padding),a=r.align;let l=o.lineHeight/2;n==="bottom"||n==="center"||Je(n)?(l+=i.bottom,Tt(r.text)&&(l+=o.lineHeight*(r.text.length-1))):l+=i.top;const{titleX:c,titleY:d,maxWidth:u,rotation:f}=KT(this,l,n,a);To(t,r.text,0,0,o,{color:r.color,maxWidth:u,rotation:f,textAlign:XT(a,n,s),textBaseline:"middle",translation:[c,d]})}draw(t){this._isVisible()&&(this.drawBackground(),this.drawGrid(t),this.drawBorder(),this.drawTitle(),this.drawLabels(t))}_layers(){const t=this.options,n=t.ticks&&t.ticks.z||0,r=Re(t.grid&&t.grid.z,-1),s=Re(t.border&&t.border.z,0);return!this._isVisible()||this.draw!==Os.prototype.draw?[{z:n,draw:o=>{this.draw(o)}}]:[{z:r,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:n,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(t){const n=this.chart.getSortedVisibleDatasetMetas(),r=this.axis+"AxisID",s=[];let o,i;for(o=0,i=n.length;o<i;++o){const a=n[o];a[r]===this.id&&(!t||a.type===t)&&s.push(a)}return s}_resolveTickFontOptions(t){const n=this.options.ticks.setContext(this.getContext(t));return Kt(n.font)}_maxDigits(){const t=this._resolveTickFontOptions(0).lineHeight;return(this.isHorizontal()?this.width:this.height)/t}}class Ea{constructor(t,n,r){this.type=t,this.scope=n,this.override=r,this.items=Object.create(null)}isForType(t){return Object.prototype.isPrototypeOf.call(this.type.prototype,t.prototype)}register(t){const n=Object.getPrototypeOf(t);let r;QT(n)&&(r=this.register(n));const s=this.items,o=t.id,i=this.scope+"."+o;if(!o)throw new Error("class does not have id: "+t);return o in s||(s[o]=t,qT(t,i,r),this.override&&kt.override(t.id,t.overrides)),i}get(t){return this.items[t]}unregister(t){const n=this.items,r=t.id,s=this.scope;r in n&&delete n[r],s&&r in kt[s]&&(delete kt[s][r],this.override&&delete Fr[r])}}function qT(e,t,n){const r=So(Object.create(null),[n?kt.get(n):{},kt.get(t),e.defaults]);kt.set(t,r),e.defaultRoutes&&ZT(t,e.defaultRoutes),e.descriptors&&kt.describe(t,e.descriptors)}function ZT(e,t){Object.keys(t).forEach(n=>{const r=n.split("."),s=r.pop(),o=[e].concat(r).join("."),i=t[n].split("."),a=i.pop(),l=i.join(".");kt.route(o,s,l,a)})}function QT(e){return"id"in e&&"defaults"in e}class JT{constructor(){this.controllers=new Ea(Ts,"datasets",!0),this.elements=new Ea(Br,"elements"),this.plugins=new Ea(Object,"plugins"),this.scales=new Ea(Os,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,n,r){[...n].forEach(s=>{const o=r||this._getRegistryForType(s);r||o.isForType(s)||o===this.plugins&&s.id?this._exec(t,o,s):lt(s,i=>{const a=r||this._getRegistryForType(i);this._exec(t,a,i)})})}_exec(t,n,r){const s=Zc(t);mt(r["before"+s],[],r),n[t](r),mt(r["after"+s],[],r)}_getRegistryForType(t){for(let n=0;n<this._typedRegistries.length;n++){const r=this._typedRegistries[n];if(r.isForType(t))return r}return this.plugins}_get(t,n,r){const s=n.get(t);if(s===void 0)throw new Error('"'+t+'" is not a registered '+r+".");return s}}var Xn=new JT;class RT{constructor(){this._init=void 0}notify(t,n,r,s){if(n==="beforeInit"&&(this._init=this._createDescriptors(t,!0),this._notify(this._init,t,"install")),this._init===void 0)return;const o=s?this._descriptors(t).filter(s):this._descriptors(t),i=this._notify(o,t,n,r);return n==="afterDestroy"&&(this._notify(o,t,"stop"),this._notify(this._init,t,"uninstall"),this._init=void 0),i}_notify(t,n,r,s){s=s||{};for(const o of t){const i=o.plugin,a=i[r],l=[n,s,o.options];if(mt(a,l,i)===!1&&s.cancelable)return!1}return!0}invalidate(){dt(this._cache)||(this._oldCache=this._cache,this._cache=void 0)}_descriptors(t){if(this._cache)return this._cache;const n=this._cache=this._createDescriptors(t);return this._notifyStateChanges(t),n}_createDescriptors(t,n){const r=t&&t.config,s=Re(r.options&&r.options.plugins,{}),o=eO(r);return s===!1&&!n?[]:nO(t,o,s,n)}_notifyStateChanges(t){const n=this._oldCache||[],r=this._cache,s=(o,i)=>o.filter(a=>!i.some(l=>a.plugin.id===l.plugin.id));this._notify(s(n,r),t,"stop"),this._notify(s(r,n),t,"start")}}function eO(e){const t={},n=[],r=Object.keys(Xn.plugins.items);for(let o=0;o<r.length;o++)n.push(Xn.getPlugin(r[o]));const s=e.plugins||[];for(let o=0;o<s.length;o++){const i=s[o];n.indexOf(i)===-1&&(n.push(i),t[i.id]=!0)}return{plugins:n,localIds:t}}function tO(e,t){return!t&&e===!1?null:e===!0?{}:e}function nO(e,{plugins:t,localIds:n},r,s){const o=[],i=e.getContext();for(const a of t){const l=a.id,c=tO(r[l],s);c!==null&&o.push({plugin:a,options:rO(e.config,{plugin:a,local:n[l]},c,i)})}return o}function rO(e,{plugin:t,local:n},r,s){const o=e.pluginScopeKeys(t),i=e.getOptionScopes(r,o);return n&&t.defaults&&i.push(t.defaults),e.createResolver(i,s,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function xd(e,t){const n=kt.datasets[e]||{};return((t.datasets||{})[e]||{}).indexAxis||t.indexAxis||n.indexAxis||"x"}function sO(e,t){let n=e;return e==="_index_"?n=t:e==="_value_"&&(n=t==="x"?"y":"x"),n}function oO(e,t){return e===t?"_index_":"_value_"}function dm(e){if(e==="x"||e==="y"||e==="r")return e}function iO(e){if(e==="top"||e==="bottom")return"x";if(e==="left"||e==="right")return"y"}function wd(e,...t){if(dm(e))return e;for(const n of t){const r=n.axis||iO(n.position)||e.length>1&&dm(e[0].toLowerCase());if(r)return r}throw new Error(`Cannot determine type of '${e}' axis. Please provide 'axis' or 'position' option.`)}function um(e,t,n){if(n[t+"AxisID"]===e)return{axis:t}}function aO(e,t){if(t.data&&t.data.datasets){const n=t.data.datasets.filter(r=>r.xAxisID===e||r.yAxisID===e);if(n.length)return um(e,"x",n[0])||um(e,"y",n[0])}return{}}function lO(e,t){const n=Fr[e.type]||{scales:{}},r=t.scales||{},s=xd(e.type,t),o=Object.create(null);return Object.keys(r).forEach(i=>{const a=r[i];if(!Je(a))return console.error(`Invalid scale configuration for scale: ${i}`);if(a._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${i}`);const l=wd(i,a,aO(i,e),kt.scales[a.type]),c=oO(l,s),d=n.scales||{};o[i]=_o(Object.create(null),[{axis:l},a,d[l],d[c]])}),e.data.datasets.forEach(i=>{const a=i.type||e.type,l=i.indexAxis||xd(a,t),d=(Fr[a]||{}).scales||{};Object.keys(d).forEach(u=>{const f=sO(u,l),g=i[f+"AxisID"]||f;o[g]=o[g]||Object.create(null),_o(o[g],[{axis:f},r[g],d[u]])})}),Object.keys(o).forEach(i=>{const a=o[i];_o(a,[kt.scales[a.type],kt.scale])}),o}function fm(e){const t=e.options||(e.options={});t.plugins=Re(t.plugins,{}),t.scales=lO(e,t)}function hm(e){return e=e||{},e.datasets=e.datasets||[],e.labels=e.labels||[],e}function cO(e){return e=e||{},e.data=hm(e.data),fm(e),e}const gm=new Map,pm=new Set;function Ma(e,t){let n=gm.get(e);return n||(n=t(),gm.set(e,n),pm.add(n)),n}const Fo=(e,t,n)=>{const r=Lr(t,n);r!==void 0&&e.add(r)};class dO{constructor(t){this._config=cO(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=hm(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),fm(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Ma(t,()=>[[`datasets.${t}`,""]])}datasetAnimationScopeKeys(t,n){return Ma(`${t}.transition.${n}`,()=>[[`datasets.${t}.transitions.${n}`,`transitions.${n}`],[`datasets.${t}`,""]])}datasetElementScopeKeys(t,n){return Ma(`${t}-${n}`,()=>[[`datasets.${t}.elements.${n}`,`datasets.${t}`,`elements.${n}`,""]])}pluginScopeKeys(t){const n=t.id,r=this.type;return Ma(`${r}-plugin-${n}`,()=>[[`plugins.${n}`,...t.additionalOptionScopes||[]]])}_cachedScopes(t,n){const r=this._scopeCache;let s=r.get(t);return(!s||n)&&(s=new Map,r.set(t,s)),s}getOptionScopes(t,n,r){const{options:s,type:o}=this,i=this._cachedScopes(t,r),a=i.get(n);if(a)return a;const l=new Set;n.forEach(d=>{t&&(l.add(t),d.forEach(u=>Fo(l,t,u))),d.forEach(u=>Fo(l,s,u)),d.forEach(u=>Fo(l,Fr[o]||{},u)),d.forEach(u=>Fo(l,kt,u)),d.forEach(u=>Fo(l,nd,u))});const c=Array.from(l);return c.length===0&&c.push(Object.create(null)),pm.has(n)&&i.set(n,c),c}chartOptionScopes(){const{options:t,type:n}=this;return[t,Fr[n]||{},kt.datasets[n]||{},{type:n},kt,nd]}resolveNamedOptions(t,n,r,s=[""]){const o={$shared:!0},{resolver:i,subPrefixes:a}=mm(this._resolverCache,t,s);let l=i;if(fO(i,n)){o.$shared=!1,r=br(r)?r():r;const c=this.createResolver(t,r,a);l=Ms(i,r,c)}for(const c of n)o[c]=l[c];return o}createResolver(t,n,r=[""],s){const{resolver:o}=mm(this._resolverCache,t,r);return Je(n)?Ms(o,n,void 0,s):o}}function mm(e,t,n){let r=e.get(t);r||(r=new Map,e.set(t,r));const s=n.join();let o=r.get(s);return o||(o={resolver:ad(t,n),subPrefixes:n.filter(a=>!a.toLowerCase().includes("hover"))},r.set(s,o)),o}const uO=e=>Je(e)&&Object.getOwnPropertyNames(e).some(t=>br(e[t]));function fO(e,t){const{isScriptable:n,isIndexable:r}=M0(e);for(const s of t){const o=n(s),i=r(s),a=(i||o)&&e[s];if(o&&(br(a)||uO(a))||i&&Tt(a))return!0}return!1}var hO="4.5.1";const gO=["top","bottom","left","right","chartArea"];function bm(e,t){return e==="top"||e==="bottom"||gO.indexOf(e)===-1&&t==="x"}function ym(e,t){return function(n,r){return n[e]===r[e]?n[t]-r[t]:n[e]-r[e]}}function vm(e){const t=e.chart,n=t.options.animation;t.notifyPlugins("afterRender"),mt(n&&n.onComplete,[e],t)}function pO(e){const t=e.chart,n=t.options.animation;mt(n&&n.onProgress,[e],t)}function xm(e){return dd()&&typeof e=="string"?e=document.getElementById(e):e&&e.length&&(e=e[0]),e&&e.canvas&&(e=e.canvas),e}const Na={},wm=e=>{const t=xm(e);return Object.values(Na).filter(n=>n.canvas===t).pop()};function mO(e,t,n){const r=Object.keys(e);for(const s of r){const o=+s;if(o>=t){const i=e[s];delete e[s],(n>0||o>t)&&(e[o+n]=i)}}}function bO(e,t,n,r){return!n||e.type==="mouseout"?null:r?t:e}let zo=(wr=class{static register(...t){Xn.add(...t),km()}static unregister(...t){Xn.remove(...t),km()}constructor(t,n){const r=this.config=new dO(n),s=xm(t),o=wm(s);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const i=r.createResolver(r.chartOptionScopes(),this.getContext());this.platform=new(r.platform||PT(s)),this.platform.updateConfig(r);const a=this.platform.acquireContext(s,i.aspectRatio),l=a&&a.canvas,c=l&&l.height,d=l&&l.width;if(this.id=LM(),this.ctx=a,this.canvas=l,this.width=d,this.height=c,this._options=i,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new RT,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=JM(u=>this.update(u),i.resizeDelay||0),this._dataChanges=[],Na[this.id]=this,!a||!l){console.error("Failed to create chart: can't acquire context from the given item");return}tr.listen(this,"complete",vm),tr.listen(this,"progress",pO),this._initialize(),this.attached&&this.update()}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:n},width:r,height:s,_aspectRatio:o}=this;return dt(t)?n&&o?o:s?r/s:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return Xn}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():I0(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return k0(this.canvas,this.ctx),this}stop(){return tr.stop(this),this}resize(t,n){tr.running(this)?this._resizeBeforeDraw={width:t,height:n}:this._resize(t,n)}_resize(t,n){const r=this.options,s=this.canvas,o=r.maintainAspectRatio&&this.aspectRatio,i=this.platform.getMaximumSize(s,t,n,o),a=r.devicePixelRatio||this.platform.getDevicePixelRatio(),l=this.width?"resize":"attach";this.width=i.width,this.height=i.height,this._aspectRatio=this.aspectRatio,I0(this,a,!0)&&(this.notifyPlugins("resize",{size:i}),mt(r.onResize,[this,i],this),this.attached&&this._doResize(l)&&this.render())}ensureScalesHaveIDs(){const n=this.options.scales||{};lt(n,(r,s)=>{r.id=s})}buildOrUpdateScales(){const t=this.options,n=t.scales,r=this.scales,s=Object.keys(r).reduce((i,a)=>(i[a]=!1,i),{});let o=[];n&&(o=o.concat(Object.keys(n).map(i=>{const a=n[i],l=wd(i,a),c=l==="r",d=l==="x";return{options:a,dposition:c?"chartArea":d?"bottom":"left",dtype:c?"radialLinear":d?"category":"linear"}}))),lt(o,i=>{const a=i.options,l=a.id,c=wd(l,a),d=Re(a.type,i.dtype);(a.position===void 0||bm(a.position,c)!==bm(i.dposition))&&(a.position=i.dposition),s[l]=!0;let u=null;if(l in r&&r[l].type===d)u=r[l];else{const f=Xn.getScale(d);u=new f({id:l,type:d,ctx:this.ctx,chart:this}),r[u.id]=u}u.init(a,t)}),lt(s,(i,a)=>{i||delete r[a]}),lt(r,i=>{xn.configure(this,i,i.options),xn.addBox(this,i)})}_updateMetasets(){const t=this._metasets,n=this.data.datasets.length,r=t.length;if(t.sort((s,o)=>s.index-o.index),r>n){for(let s=n;s<r;++s)this._destroyDatasetMeta(s);t.splice(n,r-n)}this._sortedMetasets=t.slice(0).sort(ym("order","index"))}_removeUnreferencedMetasets(){const{_metasets:t,data:{datasets:n}}=this;t.length>n.length&&delete this._stacks,t.forEach((r,s)=>{n.filter(o=>o===r._dataset).length===0&&this._destroyDatasetMeta(s)})}buildOrUpdateControllers(){const t=[],n=this.data.datasets;let r,s;for(this._removeUnreferencedMetasets(),r=0,s=n.length;r<s;r++){const o=n[r];let i=this.getDatasetMeta(r);const a=o.type||this.config.type;if(i.type&&i.type!==a&&(this._destroyDatasetMeta(r),i=this.getDatasetMeta(r)),i.type=a,i.indexAxis=o.indexAxis||xd(a,this.options),i.order=o.order||0,i.index=r,i.label=""+o.label,i.visible=this.isDatasetVisible(r),i.controller)i.controller.updateIndex(r),i.controller.linkScales();else{const l=Xn.getController(a),{datasetElementType:c,dataElementType:d}=kt.datasets[a];Object.assign(l,{dataElementType:Xn.getElement(d),datasetElementType:c&&Xn.getElement(c)}),i.controller=new l(this,r),t.push(i.controller)}}return this._updateMetasets(),t}_resetElements(){lt(this.data.datasets,(t,n)=>{this.getDatasetMeta(n).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const n=this.config;n.update();const r=this._options=n.createResolver(n.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!r.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0})===!1)return;const o=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let i=0;for(let c=0,d=this.data.datasets.length;c<d;c++){const{controller:u}=this.getDatasetMeta(c),f=!s&&o.indexOf(u)===-1;u.buildOrUpdateElements(f),i=Math.max(+u.getMaxOverflow(),i)}i=this._minPadding=r.layout.autoPadding?i:0,this._updateLayout(i),s||lt(o,c=>{c.reset()}),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(ym("z","_idx"));const{_active:a,_lastEvent:l}=this;l?this._eventHandler(l,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){lt(this.scales,t=>{xn.removeBox(this,t)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const t=this.options,n=new Set(Object.keys(this._listeners)),r=new Set(t.events);(!s0(n,r)||!!this._responsiveListeners!==t.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:t}=this,n=this._getUniformDataChanges()||[];for(const{method:r,start:s,count:o}of n){const i=r==="_removeElements"?-o:o;mO(t,s,i)}}_getUniformDataChanges(){const t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];const n=this.data.datasets.length,r=o=>new Set(t.filter(i=>i[0]===o).map((i,a)=>a+","+i.splice(1).join(","))),s=r(0);for(let o=1;o<n;o++)if(!s0(s,r(o)))return;return Array.from(s).map(o=>o.split(",")).map(o=>({method:o[1],start:+o[2],count:+o[3]}))}_updateLayout(t){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;xn.update(this,this.width,this.height,t);const n=this.chartArea,r=n.width<=0||n.height<=0;this._layers=[],lt(this.boxes,s=>{r&&s.position==="chartArea"||(s.configure&&s.configure(),this._layers.push(...s._layers()))},this),this._layers.forEach((s,o)=>{s._idx=o}),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})!==!1){for(let n=0,r=this.data.datasets.length;n<r;++n)this.getDatasetMeta(n).controller.configure();for(let n=0,r=this.data.datasets.length;n<r;++n)this._updateDataset(n,br(t)?t({datasetIndex:n}):t);this.notifyPlugins("afterDatasetsUpdate",{mode:t})}}_updateDataset(t,n){const r=this.getDatasetMeta(t),s={meta:r,index:t,mode:n,cancelable:!0};this.notifyPlugins("beforeDatasetUpdate",s)!==!1&&(r.controller._update(n),s.cancelable=!1,this.notifyPlugins("afterDatasetUpdate",s))}render(){this.notifyPlugins("beforeRender",{cancelable:!0})!==!1&&(tr.has(this)?this.attached&&!tr.running(this)&&tr.start(this):(this.draw(),vm({chart:this})))}draw(){let t;if(this._resizeBeforeDraw){const{width:r,height:s}=this._resizeBeforeDraw;this._resizeBeforeDraw=null,this._resize(r,s)}if(this.clear(),this.width<=0||this.height<=0||this.notifyPlugins("beforeDraw",{cancelable:!0})===!1)return;const n=this._layers;for(t=0;t<n.length&&n[t].z<=0;++t)n[t].draw(this.chartArea);for(this._drawDatasets();t<n.length;++t)n[t].draw(this.chartArea);this.notifyPlugins("afterDraw")}_getSortedDatasetMetas(t){const n=this._sortedMetasets,r=[];let s,o;for(s=0,o=n.length;s<o;++s){const i=n[s];(!t||i.visible)&&r.push(i)}return r}getSortedVisibleDatasetMetas(){return this._getSortedDatasetMetas(!0)}_drawDatasets(){if(this.notifyPlugins("beforeDatasetsDraw",{cancelable:!0})===!1)return;const t=this.getSortedVisibleDatasetMetas();for(let n=t.length-1;n>=0;--n)this._drawDataset(t[n]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const n=this.ctx,r={meta:t,index:t.index,cancelable:!0},s=FN(this,t);this.notifyPlugins("beforeDatasetDraw",r)!==!1&&(s&&sd(n,s),t.controller.draw(),s&&od(n),r.cancelable=!1,this.notifyPlugins("afterDatasetDraw",r))}isPointInArea(t){return _0(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,n,r,s){const o=gT.modes[n];return typeof o=="function"?o(this,t,r,s):[]}getDatasetMeta(t){const n=this.data.datasets[t],r=this._metasets;let s=r.filter(o=>o&&o._dataset===n).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:n&&n.order||0,index:t,_dataset:n,_parsed:[],_sorted:!1},r.push(s)),s}getContext(){return this.$context||(this.$context=Es(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const n=this.data.datasets[t];if(!n)return!1;const r=this.getDatasetMeta(t);return typeof r.hidden=="boolean"?!r.hidden:!n.hidden}setDatasetVisibility(t,n){const r=this.getDatasetMeta(t);r.hidden=!n}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,n,r){const s=r?"show":"hide",o=this.getDatasetMeta(t),i=o.controller._resolveAnimations(void 0,s);Eo(n)?(o.data[n].hidden=!r,this.update()):(this.setDatasetVisibility(t,r),i.update(o,{visible:r}),this.update(a=>a.datasetIndex===t?s:void 0))}hide(t,n){this._updateVisibility(t,n,!1)}show(t,n){this._updateVisibility(t,n,!0)}_destroyDatasetMeta(t){const n=this._metasets[t];n&&n.controller&&n.controller._destroy(),delete this._metasets[t]}_stop(){let t,n;for(this.stop(),tr.remove(this),t=0,n=this.data.datasets.length;t<n;++t)this._destroyDatasetMeta(t)}destroy(){this.notifyPlugins("beforeDestroy");const{canvas:t,ctx:n}=this;this._stop(),this.config.clearCache(),t&&(this.unbindEvents(),k0(t,n),this.platform.releaseContext(n),this.canvas=null,this.ctx=null),delete Na[this.id],this.notifyPlugins("afterDestroy")}toBase64Image(...t){return this.canvas.toDataURL(...t)}bindEvents(){this.bindUserEvents(),this.options.responsive?this.bindResponsiveEvents():this.attached=!0}bindUserEvents(){const t=this._listeners,n=this.platform,r=(o,i)=>{n.addEventListener(this,o,i),t[o]=i},s=(o,i,a)=>{o.offsetX=i,o.offsetY=a,this._eventHandler(o)};lt(this.options.events,o=>r(o,s))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const t=this._responsiveListeners,n=this.platform,r=(l,c)=>{n.addEventListener(this,l,c),t[l]=c},s=(l,c)=>{t[l]&&(n.removeEventListener(this,l,c),delete t[l])},o=(l,c)=>{this.canvas&&this.resize(l,c)};let i;const a=()=>{s("attach",a),this.attached=!0,this.resize(),r("resize",o),r("detach",i)};i=()=>{this.attached=!1,s("resize",o),this._stop(),this._resize(0,0),r("attach",a)},n.isAttached(this.canvas)?a():i()}unbindEvents(){lt(this._listeners,(t,n)=>{this.platform.removeEventListener(this,n,t)}),this._listeners={},lt(this._responsiveListeners,(t,n)=>{this.platform.removeEventListener(this,n,t)}),this._responsiveListeners=void 0}updateHoverStyle(t,n,r){const s=r?"set":"remove";let o,i,a,l;for(n==="dataset"&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+s+"DatasetHoverStyle"]()),a=0,l=t.length;a<l;++a){i=t[a];const c=i&&this.getDatasetMeta(i.datasetIndex).controller;c&&c[s+"HoverStyle"](i.element,i.datasetIndex,i.index)}}getActiveElements(){return this._active||[]}setActiveElements(t){const n=this._active||[],r=t.map(({datasetIndex:o,index:i})=>{const a=this.getDatasetMeta(o);if(!a)throw new Error("No dataset found at index "+o);return{datasetIndex:o,element:a.data[i],index:i}});!la(r,n)&&(this._active=r,this._lastEvent=null,this._updateHoverStyles(r,n))}notifyPlugins(t,n,r){return this._plugins.notify(this,t,n,r)}isPluginEnabled(t){return this._plugins._cache.filter(n=>n.plugin.id===t).length===1}_updateHoverStyles(t,n,r){const s=this.options.hover,o=(l,c)=>l.filter(d=>!c.some(u=>d.datasetIndex===u.datasetIndex&&d.index===u.index)),i=o(n,t),a=r?t:o(t,n);i.length&&this.updateHoverStyle(i,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(t,n){const r={event:t,replay:n,cancelable:!0,inChartArea:this.isPointInArea(t)},s=i=>(i.options.events||this.options.events).includes(t.native.type);if(this.notifyPlugins("beforeEvent",r,s)===!1)return;const o=this._handleEvent(t,n,r.inChartArea);return r.cancelable=!1,this.notifyPlugins("afterEvent",r,s),(o||r.changed)&&this.render(),this}_handleEvent(t,n,r){const{_active:s=[],options:o}=this,i=n,a=this._getActiveElements(t,s,r,i),l=HM(t),c=bO(t,this._lastEvent,r,l);r&&(this._lastEvent=null,mt(o.onHover,[t,a,this],this),l&&mt(o.onClick,[t,a,this],this));const d=!la(a,s);return(d||n)&&(this._active=a,this._updateHoverStyles(a,s,n)),this._lastEvent=c,d}_getActiveElements(t,n,r,s){if(t.type==="mouseout")return[];if(!r)return n;const o=this.options.hover;return this.getElementsAtEventForMode(t,o.mode,o,s)}},Pe(wr,"defaults",kt),Pe(wr,"instances",Na),Pe(wr,"overrides",Fr),Pe(wr,"registry",Xn),Pe(wr,"version",hO),Pe(wr,"getChart",wm),wr);function km(){return lt(zo.instances,e=>e._plugins.invalidate())}function yO(e,t,n){const{startAngle:r,x:s,y:o,outerRadius:i,innerRadius:a,options:l}=t,{borderWidth:c,borderJoinStyle:d}=l,u=Math.min(c/i,Yn(r-n));if(e.beginPath(),e.arc(s,o,i-c/2,r+u/2,n-u/2),a>0){const f=Math.min(c/a,Yn(r-n));e.arc(s,o,a+c/2,n-f/2,r+f/2,!0)}else{const f=Math.min(c/2,i*Yn(r-n));if(d==="round")e.arc(s,o,f,n-ut/2,r+ut/2,!0);else if(d==="bevel"){const g=2*f*f,m=-g*Math.cos(n+ut/2)+s,p=-g*Math.sin(n+ut/2)+o,y=g*Math.cos(r+ut/2)+s,b=g*Math.sin(r+ut/2)+o;e.lineTo(m,p),e.lineTo(y,b)}}e.closePath(),e.moveTo(0,0),e.rect(0,0,e.canvas.width,e.canvas.height),e.clip("evenodd")}function vO(e,t,n){const{startAngle:r,pixelMargin:s,x:o,y:i,outerRadius:a,innerRadius:l}=t;let c=s/a;e.beginPath(),e.arc(o,i,a,r-c,n+c),l>s?(c=s/l,e.arc(o,i,l,n+c,r-c,!0)):e.arc(o,i,s,n+Ot,r-Ot),e.closePath(),e.clip()}function xO(e){return id(e,["outerStart","outerEnd","innerStart","innerEnd"])}function wO(e,t,n,r){const s=xO(e.options.borderRadius),o=(n-t)/2,i=Math.min(o,r*t/2),a=l=>{const c=(n-Math.min(o,l))*r/2;return sn(l,0,Math.min(o,c))};return{outerStart:a(s.outerStart),outerEnd:a(s.outerEnd),innerStart:sn(s.innerStart,0,i),innerEnd:sn(s.innerEnd,0,i)}}function As(e,t,n,r){return{x:n+e*Math.cos(t),y:r+e*Math.sin(t)}}function Ta(e,t,n,r,s,o){const{x:i,y:a,startAngle:l,pixelMargin:c,innerRadius:d}=t,u=Math.max(t.outerRadius+r+n-c,0),f=d>0?d+r+n+c:0;let g=0;const m=s-l;if(r){const A=d>0?d-r:0,O=u>0?u-r:0,T=(A+O)/2,z=T!==0?m*T/(T+r):m;g=(m-z)/2}const p=Math.max(.001,m*u-n/ut)/u,y=(m-p)/2,b=l+y+g,_=s-y-g,{outerStart:x,outerEnd:M,innerStart:C,innerEnd:k}=wO(t,f,u,_-b),N=u-x,S=u-M,w=b+x/N,E=_-M/S,D=f+C,P=f+k,L=b+C/D,$=_-k/P;if(e.beginPath(),o){const A=(w+E)/2;if(e.arc(i,a,u,w,A),e.arc(i,a,u,A,E),M>0){const H=As(S,E,i,a);e.arc(H.x,H.y,M,E,_+Ot)}const O=As(P,_,i,a);if(e.lineTo(O.x,O.y),k>0){const H=As(P,$,i,a);e.arc(H.x,H.y,k,_+Ot,$+Math.PI)}const T=(_-k/f+(b+C/f))/2;if(e.arc(i,a,f,_-k/f,T,!0),e.arc(i,a,f,T,b+C/f,!0),C>0){const H=As(D,L,i,a);e.arc(H.x,H.y,C,L+Math.PI,b-Ot)}const z=As(N,b,i,a);if(e.lineTo(z.x,z.y),x>0){const H=As(N,w,i,a);e.arc(H.x,H.y,x,b-Ot,w)}}else{e.moveTo(i,a);const A=Math.cos(w)*u+i,O=Math.sin(w)*u+a;e.lineTo(A,O);const T=Math.cos(E)*u+i,z=Math.sin(E)*u+a;e.lineTo(T,z)}e.closePath()}function kO(e,t,n,r,s){const{fullCircles:o,startAngle:i,circumference:a}=t;let l=t.endAngle;if(o){Ta(e,t,n,r,l,s);for(let c=0;c<o;++c)e.fill();isNaN(a)||(l=i+(a%Mt||Mt))}return Ta(e,t,n,r,l,s),e.fill(),l}function CO(e,t,n,r,s){const{fullCircles:o,startAngle:i,circumference:a,options:l}=t,{borderWidth:c,borderJoinStyle:d,borderDash:u,borderDashOffset:f,borderRadius:g}=l,m=l.borderAlign==="inner";if(!c)return;e.setLineDash(u||[]),e.lineDashOffset=f,m?(e.lineWidth=c*2,e.lineJoin=d||"round"):(e.lineWidth=c,e.lineJoin=d||"bevel");let p=t.endAngle;if(o){Ta(e,t,n,r,p,s);for(let y=0;y<o;++y)e.stroke();isNaN(a)||(p=i+(a%Mt||Mt))}m&&vO(e,t,p),l.selfJoin&&p-i>=ut&&g===0&&d!=="miter"&&yO(e,t,p),o||(Ta(e,t,n,r,p,s),e.stroke())}class Vo extends Br{constructor(n){super();Pe(this,"circumference");Pe(this,"endAngle");Pe(this,"fullCircles");Pe(this,"innerRadius");Pe(this,"outerRadius");Pe(this,"pixelMargin");Pe(this,"startAngle");this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,n&&Object.assign(this,n)}inRange(n,r,s){const o=this.getProps(["x","y"],s),{angle:i,distance:a}=c0(o,{x:n,y:r}),{startAngle:l,endAngle:c,innerRadius:d,outerRadius:u,circumference:f}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],s),g=(this.options.spacing+this.options.borderWidth)/2,m=Re(f,c-l),p=ha(i,l,c)&&l!==c,y=m>=Mt||p,b=$r(a,d+g,u+g);return y&&b}getCenterPoint(n){const{x:r,y:s,startAngle:o,endAngle:i,innerRadius:a,outerRadius:l}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],n),{offset:c,spacing:d}=this.options,u=(o+i)/2,f=(a+l+d+c)/2;return{x:r+Math.cos(u)*f,y:s+Math.sin(u)*f}}tooltipPosition(n){return this.getCenterPoint(n)}draw(n){const{options:r,circumference:s}=this,o=(r.offset||0)/4,i=(r.spacing||0)/2,a=r.circular;if(this.pixelMargin=r.borderAlign==="inner"?.33:0,this.fullCircles=s>Mt?Math.floor(s/Mt):0,s===0||this.innerRadius<0||this.outerRadius<0)return;n.save();const l=(this.startAngle+this.endAngle)/2;n.translate(Math.cos(l)*o,Math.sin(l)*o);const c=1-Math.sin(Math.min(ut,s||0)),d=o*c;n.fillStyle=r.backgroundColor,n.strokeStyle=r.borderColor,kO(n,this,d,i,a),CO(n,this,d,i,a),n.restore()}}Pe(Vo,"id","arc"),Pe(Vo,"defaults",{borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),Pe(Vo,"defaultRoutes",{backgroundColor:"backgroundColor"}),Pe(Vo,"descriptors",{_scriptable:!0,_indexable:n=>n!=="borderDash"});function Cm(e,t){const{x:n,y:r,base:s,width:o,height:i}=e.getProps(["x","y","base","width","height"],t);let a,l,c,d,u;return e.horizontal?(u=i/2,a=Math.min(n,s),l=Math.max(n,s),c=r-u,d=r+u):(u=o/2,a=n-u,l=n+u,c=Math.min(r,s),d=Math.max(r,s)),{left:a,top:c,right:l,bottom:d}}function xr(e,t,n,r){return e?0:sn(t,n,r)}function SO(e,t,n){const r=e.options.borderWidth,s=e.borderSkipped,o=E0(r);return{t:xr(s.top,o.top,0,n),r:xr(s.right,o.right,0,t),b:xr(s.bottom,o.bottom,0,n),l:xr(s.left,o.left,0,t)}}function _O(e,t,n){const{enableBorderRadius:r}=e.getProps(["enableBorderRadius"]),s=e.options.borderRadius,o=_s(s),i=Math.min(t,n),a=e.borderSkipped,l=r||Je(s);return{topLeft:xr(!l||a.top||a.left,o.topLeft,0,i),topRight:xr(!l||a.top||a.right,o.topRight,0,i),bottomLeft:xr(!l||a.bottom||a.left,o.bottomLeft,0,i),bottomRight:xr(!l||a.bottom||a.right,o.bottomRight,0,i)}}function EO(e){const t=Cm(e),n=t.right-t.left,r=t.bottom-t.top,s=SO(e,n/2,r/2),o=_O(e,n/2,r/2);return{outer:{x:t.left,y:t.top,w:n,h:r,radius:o},inner:{x:t.left+s.l,y:t.top+s.t,w:n-s.l-s.r,h:r-s.t-s.b,radius:{topLeft:Math.max(0,o.topLeft-Math.max(s.t,s.l)),topRight:Math.max(0,o.topRight-Math.max(s.t,s.r)),bottomLeft:Math.max(0,o.bottomLeft-Math.max(s.b,s.l)),bottomRight:Math.max(0,o.bottomRight-Math.max(s.b,s.r))}}}}function kd(e,t,n,r){const s=t===null,o=n===null,a=e&&!(s&&o)&&Cm(e,r);return a&&(s||$r(t,a.left,a.right))&&(o||$r(n,a.top,a.bottom))}function MO(e){return e.topLeft||e.topRight||e.bottomLeft||e.bottomRight}function NO(e,t){e.rect(t.x,t.y,t.w,t.h)}function Cd(e,t,n={}){const r=e.x!==n.x?-t:0,s=e.y!==n.y?-t:0,o=(e.x+e.w!==n.x+n.w?t:0)-r,i=(e.y+e.h!==n.y+n.h?t:0)-s;return{x:e.x+r,y:e.y+s,w:e.w+o,h:e.h+i,radius:e.radius}}class Oa extends Br{constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,this.inflateAmount=void 0,t&&Object.assign(this,t)}draw(t){const{inflateAmount:n,options:{borderColor:r,backgroundColor:s}}=this,{inner:o,outer:i}=EO(this),a=MO(i.radius)?pa:NO;t.save(),(i.w!==o.w||i.h!==o.h)&&(t.beginPath(),a(t,Cd(i,n,o)),t.clip(),a(t,Cd(o,-n,i)),t.fillStyle=r,t.fill("evenodd")),t.beginPath(),a(t,Cd(o,n)),t.fillStyle=s,t.fill(),t.restore()}inRange(t,n,r){return kd(this,t,n,r)}inXRange(t,n){return kd(this,t,null,n)}inYRange(t,n){return kd(this,null,t,n)}getCenterPoint(t){const{x:n,y:r,base:s,horizontal:o}=this.getProps(["x","y","base","horizontal"],t);return{x:o?(n+s)/2:n,y:o?r:(r+s)/2}}getRange(t){return t==="x"?this.width/2:this.height/2}}Pe(Oa,"id","bar"),Pe(Oa,"defaults",{borderSkipped:"start",borderWidth:0,borderRadius:0,inflateAmount:"auto",pointStyle:void 0}),Pe(Oa,"defaultRoutes",{backgroundColor:"backgroundColor",borderColor:"borderColor"});const Sm=(e,t)=>{let{boxHeight:n=t,boxWidth:r=t}=e;return e.usePointStyle&&(n=Math.min(n,t),r=e.pointStyleWidth||Math.min(r,t)),{boxWidth:r,boxHeight:n,itemHeight:Math.max(t,n)}},TO=(e,t)=>e!==null&&t!==null&&e.datasetIndex===t.datasetIndex&&e.index===t.index;class _m extends Br{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,n,r){this.maxWidth=t,this.maxHeight=n,this._margins=r,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const t=this.options.labels||{};let n=mt(t.generateLabels,[this.chart],this)||[];t.filter&&(n=n.filter(r=>t.filter(r,this.chart.data))),t.sort&&(n=n.sort((r,s)=>t.sort(r,s,this.chart.data))),this.options.reverse&&n.reverse(),this.legendItems=n}fit(){const{options:t,ctx:n}=this;if(!t.display){this.width=this.height=0;return}const r=t.labels,s=Kt(r.font),o=s.size,i=this._computeTitleHeight(),{boxWidth:a,itemHeight:l}=Sm(r,o);let c,d;n.font=s.string,this.isHorizontal()?(c=this.maxWidth,d=this._fitRows(i,o,a,l)+10):(d=this.maxHeight,c=this._fitCols(i,s,a,l)+10),this.width=Math.min(c,t.maxWidth||this.maxWidth),this.height=Math.min(d,t.maxHeight||this.maxHeight)}_fitRows(t,n,r,s){const{ctx:o,maxWidth:i,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],d=s+a;let u=t;o.textAlign="left",o.textBaseline="middle";let f=-1,g=-d;return this.legendItems.forEach((m,p)=>{const y=r+n/2+o.measureText(m.text).width;(p===0||c[c.length-1]+y+2*a>i)&&(u+=d,c[c.length-(p>0?0:1)]=0,g+=d,f++),l[p]={left:0,top:g,row:f,width:y,height:s},c[c.length-1]+=y+a}),u}_fitCols(t,n,r,s){const{ctx:o,maxHeight:i,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],d=i-t;let u=a,f=0,g=0,m=0,p=0;return this.legendItems.forEach((y,b)=>{const{itemWidth:_,itemHeight:x}=OO(r,n,o,y,s);b>0&&g+x+2*a>d&&(u+=f+a,c.push({width:f,height:g}),m+=f+a,p++,f=g=0),l[b]={left:m,top:g,col:p,width:_,height:x},f=Math.max(f,_),g+=x+a}),u+=f,c.push({width:f,height:g}),u}adjustHitBoxes(){if(!this.options.display)return;const t=this._computeTitleHeight(),{legendHitBoxes:n,options:{align:r,labels:{padding:s},rtl:o}}=this,i=Ns(o,this.left,this.width);if(this.isHorizontal()){let a=0,l=Xt(r,this.left+s,this.right-this.lineWidths[a]);for(const c of n)a!==c.row&&(a=c.row,l=Xt(r,this.left+s,this.right-this.lineWidths[a])),c.top+=this.top+t+s,c.left=i.leftForLtr(i.x(l),c.width),l+=c.width+s}else{let a=0,l=Xt(r,this.top+t+s,this.bottom-this.columnSizes[a].height);for(const c of n)c.col!==a&&(a=c.col,l=Xt(r,this.top+t+s,this.bottom-this.columnSizes[a].height)),c.top=l,c.left+=this.left+s,c.left=i.leftForLtr(i.x(c.left),c.width),l+=c.height+s}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){const t=this.ctx;sd(t,this),this._draw(),od(t)}}_draw(){const{options:t,columnSizes:n,lineWidths:r,ctx:s}=this,{align:o,labels:i}=t,a=kt.color,l=Ns(t.rtl,this.left,this.width),c=Kt(i.font),{padding:d}=i,u=c.size,f=u/2;let g;this.drawTitle(),s.textAlign=l.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=c.string;const{boxWidth:m,boxHeight:p,itemHeight:y}=Sm(i,u),b=function(k,N,S){if(isNaN(m)||m<=0||isNaN(p)||p<0)return;s.save();const w=Re(S.lineWidth,1);if(s.fillStyle=Re(S.fillStyle,a),s.lineCap=Re(S.lineCap,"butt"),s.lineDashOffset=Re(S.lineDashOffset,0),s.lineJoin=Re(S.lineJoin,"miter"),s.lineWidth=w,s.strokeStyle=Re(S.strokeStyle,a),s.setLineDash(Re(S.lineDash,[])),i.usePointStyle){const E={radius:p*Math.SQRT2/2,pointStyle:S.pointStyle,rotation:S.rotation,borderWidth:w},D=l.xPlus(k,m/2),P=N+f;S0(s,E,D,P,i.pointStyleWidth&&m)}else{const E=N+Math.max((u-p)/2,0),D=l.leftForLtr(k,m),P=_s(S.borderRadius);s.beginPath(),Object.values(P).some(L=>L!==0)?pa(s,{x:D,y:E,w:m,h:p,radius:P}):s.rect(D,E,m,p),s.fill(),w!==0&&s.stroke()}s.restore()},_=function(k,N,S){To(s,S.text,k,N+y/2,c,{strikethrough:S.hidden,textAlign:l.textAlign(S.textAlign)})},x=this.isHorizontal(),M=this._computeTitleHeight();x?g={x:Xt(o,this.left+d,this.right-r[0]),y:this.top+d+M,line:0}:g={x:this.left+d,y:Xt(o,this.top+M+d,this.bottom-n[0].height),line:0},P0(this.ctx,t.textDirection);const C=y+d;this.legendItems.forEach((k,N)=>{s.strokeStyle=k.fontColor,s.fillStyle=k.fontColor;const S=s.measureText(k.text).width,w=l.textAlign(k.textAlign||(k.textAlign=i.textAlign)),E=m+f+S;let D=g.x,P=g.y;l.setWidth(this.width),x?N>0&&D+E+d>this.right&&(P=g.y+=C,g.line++,D=g.x=Xt(o,this.left+d,this.right-r[g.line])):N>0&&P+C>this.bottom&&(D=g.x=D+n[g.line].width+d,g.line++,P=g.y=Xt(o,this.top+M+d,this.bottom-n[g.line].height));const L=l.x(D);if(b(L,P,k),D=RM(w,D+m+f,x?D+E:this.right,t.rtl),_(l.x(D),P,k),x)g.x+=E+d;else if(typeof k.text!="string"){const $=c.lineHeight;g.y+=Em(k,$)+d}else g.y+=C}),$0(this.ctx,t.textDirection)}drawTitle(){const t=this.options,n=t.title,r=Kt(n.font),s=vn(n.padding);if(!n.display)return;const o=Ns(t.rtl,this.left,this.width),i=this.ctx,a=n.position,l=r.size/2,c=s.top+l;let d,u=this.left,f=this.width;if(this.isHorizontal())f=Math.max(...this.lineWidths),d=this.top+c,u=Xt(t.align,u,this.right-f);else{const m=this.columnSizes.reduce((p,y)=>Math.max(p,y.height),0);d=c+Xt(t.align,this.top,this.bottom-m-t.labels.padding-this._computeTitleHeight())}const g=Xt(a,u,u+f);i.textAlign=o.textAlign(Rc(a)),i.textBaseline="middle",i.strokeStyle=n.color,i.fillStyle=n.color,i.font=r.string,To(i,n.text,g,d,r)}_computeTitleHeight(){const t=this.options.title,n=Kt(t.font),r=vn(t.padding);return t.display?n.lineHeight+r.height:0}_getLegendItemAt(t,n){let r,s,o;if($r(t,this.left,this.right)&&$r(n,this.top,this.bottom)){for(o=this.legendHitBoxes,r=0;r<o.length;++r)if(s=o[r],$r(t,s.left,s.left+s.width)&&$r(n,s.top,s.top+s.height))return this.legendItems[r]}return null}handleEvent(t){const n=this.options;if(!IO(t.type,n))return;const r=this._getLegendItemAt(t.x,t.y);if(t.type==="mousemove"||t.type==="mouseout"){const s=this._hoveredItem,o=TO(s,r);s&&!o&&mt(n.onLeave,[t,s,this],this),this._hoveredItem=r,r&&!o&&mt(n.onHover,[t,r,this],this)}else r&&mt(n.onClick,[t,r,this],this)}}function OO(e,t,n,r,s){const o=AO(r,e,t,n),i=DO(s,r,t.lineHeight);return{itemWidth:o,itemHeight:i}}function AO(e,t,n,r){let s=e.text;return s&&typeof s!="string"&&(s=s.reduce((o,i)=>o.length>i.length?o:i)),t+n.size/2+r.measureText(s).width}function DO(e,t,n){let r=e;return typeof t.text!="string"&&(r=Em(t,n)),r}function Em(e,t){const n=e.text?e.text.length:0;return t*n}function IO(e,t){return!!((e==="mousemove"||e==="mouseout")&&(t.onHover||t.onLeave)||t.onClick&&(e==="click"||e==="mouseup"))}var Mm={id:"legend",_element:_m,start(e,t,n){const r=e.legend=new _m({ctx:e.ctx,options:n,chart:e});xn.configure(e,r,n),xn.addBox(e,r)},stop(e){xn.removeBox(e,e.legend),delete e.legend},beforeUpdate(e,t,n){const r=e.legend;xn.configure(e,r,n),r.options=n},afterUpdate(e){const t=e.legend;t.buildLabels(),t.adjustHitBoxes()},afterEvent(e,t){t.replay||e.legend.handleEvent(t.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(e,t,n){const r=t.datasetIndex,s=n.chart;s.isDatasetVisible(r)?(s.hide(r),t.hidden=!0):(s.show(r),t.hidden=!1)},onHover:null,onLeave:null,labels:{color:e=>e.chart.options.color,boxWidth:40,padding:10,generateLabels(e){const t=e.data.datasets,{labels:{usePointStyle:n,pointStyle:r,textAlign:s,color:o,useBorderRadius:i,borderRadius:a}}=e.legend.options;return e._getSortedDatasetMetas().map(l=>{const c=l.controller.getStyle(n?0:void 0),d=vn(c.borderWidth);return{text:t[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(d.width+d.height)/4,strokeStyle:c.borderColor,pointStyle:r||c.pointStyle,rotation:c.rotation,textAlign:s||c.textAlign,borderRadius:i&&(a||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:e=>e.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:e=>!e.startsWith("on"),labels:{_scriptable:e=>!["generateLabels","filter","sort"].includes(e)}}};class Nm extends Br{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,n){const r=this.options;if(this.left=0,this.top=0,!r.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=t,this.height=this.bottom=n;const s=Tt(r.text)?r.text.length:1;this._padding=vn(r.padding);const o=s*Kt(r.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const t=this.options.position;return t==="top"||t==="bottom"}_drawArgs(t){const{top:n,left:r,bottom:s,right:o,options:i}=this,a=i.align;let l=0,c,d,u;return this.isHorizontal()?(d=Xt(a,r,o),u=n+t,c=o-r):(i.position==="left"?(d=r+t,u=Xt(a,s,n),l=ut*-.5):(d=o-t,u=Xt(a,n,s),l=ut*.5),c=s-n),{titleX:d,titleY:u,maxWidth:c,rotation:l}}draw(){const t=this.ctx,n=this.options;if(!n.display)return;const r=Kt(n.font),o=r.lineHeight/2+this._padding.top,{titleX:i,titleY:a,maxWidth:l,rotation:c}=this._drawArgs(o);To(t,n.text,0,0,r,{color:n.color,maxWidth:l,rotation:c,textAlign:Rc(n.align),textBaseline:"middle",translation:[i,a]})}}function LO(e,t){const n=new Nm({ctx:e.ctx,options:t,chart:e});xn.configure(e,n,t),xn.addBox(e,n),e.titleBlock=n}var PO={id:"title",_element:Nm,start(e,t,n){LO(e,n)},stop(e){const t=e.titleBlock;xn.removeBox(e,t),delete e.titleBlock},beforeUpdate(e,t,n){const r=e.titleBlock;xn.configure(e,r,n),r.options=n},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const Ho={average(e){if(!e.length)return!1;let t,n,r=new Set,s=0,o=0;for(t=0,n=e.length;t<n;++t){const a=e[t].element;if(a&&a.hasValue()){const l=a.tooltipPosition();r.add(l.x),s+=l.y,++o}}return o===0||r.size===0?!1:{x:[...r].reduce((a,l)=>a+l)/r.size,y:s/o}},nearest(e,t){if(!e.length)return!1;let n=t.x,r=t.y,s=Number.POSITIVE_INFINITY,o,i,a;for(o=0,i=e.length;o<i;++o){const l=e[o].element;if(l&&l.hasValue()){const c=l.getCenterPoint(),d=XM(t,c);d<s&&(s=d,a=l)}}if(a){const l=a.tooltipPosition();n=l.x,r=l.y}return{x:n,y:r}}};function Kn(e,t){return t&&(Tt(t)?Array.prototype.push.apply(e,t):e.push(t)),e}function nr(e){return(typeof e=="string"||e instanceof String)&&e.indexOf(` -`)>-1?e.split(` -`):e}function $O(e,t){const{element:n,datasetIndex:r,index:s}=t,o=e.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:e,label:i,parsed:o.getParsed(s),raw:e.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function Tm(e,t){const n=e.chart.ctx,{body:r,footer:s,title:o}=e,{boxWidth:i,boxHeight:a}=t,l=Kt(t.bodyFont),c=Kt(t.titleFont),d=Kt(t.footerFont),u=o.length,f=s.length,g=r.length,m=vn(t.padding);let p=m.height,y=0,b=r.reduce((M,C)=>M+C.before.length+C.lines.length+C.after.length,0);if(b+=e.beforeBody.length+e.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*t.titleSpacing+t.titleMarginBottom),b){const M=t.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*M+(b-g)*l.lineHeight+(b-1)*t.bodySpacing}f&&(p+=t.footerMarginTop+f*d.lineHeight+(f-1)*t.footerSpacing);let _=0;const x=function(M){y=Math.max(y,n.measureText(M).width+_)};return n.save(),n.font=c.string,lt(e.title,x),n.font=l.string,lt(e.beforeBody.concat(e.afterBody),x),_=t.displayColors?i+2+t.boxPadding:0,lt(r,M=>{lt(M.before,x),lt(M.lines,x),lt(M.after,x)}),_=0,n.font=d.string,lt(e.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function FO(e,t){const{y:n,height:r}=t;return n<r/2?"top":n>e.height-r/2?"bottom":"center"}function zO(e,t,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(e==="left"&&s+o+i>t.width||e==="right"&&s-o-i<0)return!0}function VO(e,t,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=e;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),zO(c,e,t,n)&&(c="center"),c}function Om(e,t,n){const r=n.yAlign||t.yAlign||FO(e,n);return{xAlign:n.xAlign||t.xAlign||VO(e,t,n,r),yAlign:r}}function HO(e,t){let{x:n,width:r}=e;return t==="right"?n-=r:t==="center"&&(n-=r/2),n}function UO(e,t,n){let{y:r,height:s}=e;return t==="top"?r+=n:t==="bottom"?r-=s+n:r-=s/2,r}function Am(e,t,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=e,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=_s(i);let m=HO(t,a);const p=UO(t,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:sn(m,0,r.width-t.width),y:sn(p,0,r.height-t.height)}}function Aa(e,t,n){const r=vn(n.padding);return t==="center"?e.x+e.width/2:t==="right"?e.x+e.width-r.right:e.x+r.left}function Dm(e){return Kn([],nr(e))}function BO(e,t,n){return Es(e,{tooltip:t,tooltipItems:n,type:"tooltip"})}function Im(e,t){const n=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return n?e.override(n):e}const Lm={beforeTitle:Rn,title(e){if(e.length>0){const t=e[0],n=t.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(r>0&&t.dataIndex<r)return n[t.dataIndex]}return""},afterTitle:Rn,beforeBody:Rn,beforeLabel:Rn,label(e){if(this&&this.options&&this.options.mode==="dataset")return e.label+": "+e.formattedValue||e.formattedValue;let t=e.dataset.label||"";t&&(t+=": ");const n=e.formattedValue;return dt(n)||(t+=n),t},labelColor(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(e){const n=e.chart.getDatasetMeta(e.datasetIndex).controller.getStyle(e.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Rn,afterBody:Rn,beforeFooter:Rn,footer:Rn,afterFooter:Rn};function on(e,t,n,r){const s=e[t].call(n,r);return typeof s>"u"?Lm[t].call(n,r):s}class Sd extends Br{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this._cachedAnimations;if(t)return t;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new z0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=BO(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,n){const{callbacks:r}=n,s=on(r,"beforeTitle",this,t),o=on(r,"title",this,t),i=on(r,"afterTitle",this,t);let a=[];return a=Kn(a,nr(s)),a=Kn(a,nr(o)),a=Kn(a,nr(i)),a}getBeforeBody(t,n){return Dm(on(n.callbacks,"beforeBody",this,t))}getBody(t,n){const{callbacks:r}=n,s=[];return lt(t,o=>{const i={before:[],lines:[],after:[]},a=Im(r,o);Kn(i.before,nr(on(a,"beforeLabel",this,o))),Kn(i.lines,on(a,"label",this,o)),Kn(i.after,nr(on(a,"afterLabel",this,o))),s.push(i)}),s}getAfterBody(t,n){return Dm(on(n.callbacks,"afterBody",this,t))}getFooter(t,n){const{callbacks:r}=n,s=on(r,"beforeFooter",this,t),o=on(r,"footer",this,t),i=on(r,"afterFooter",this,t);let a=[];return a=Kn(a,nr(s)),a=Kn(a,nr(o)),a=Kn(a,nr(i)),a}_createItems(t){const n=this._active,r=this.chart.data,s=[],o=[],i=[];let a=[],l,c;for(l=0,c=n.length;l<c;++l)a.push($O(this.chart,n[l]));return t.filter&&(a=a.filter((d,u,f)=>t.filter(d,u,f,r))),t.itemSort&&(a=a.sort((d,u)=>t.itemSort(d,u,r))),lt(a,d=>{const u=Im(t.callbacks,d);s.push(on(u,"labelColor",this,d)),o.push(on(u,"labelPointStyle",this,d)),i.push(on(u,"labelTextColor",this,d))}),this.labelColors=s,this.labelPointStyles=o,this.labelTextColors=i,this.dataPoints=a,a}update(t,n){const r=this.options.setContext(this.getContext()),s=this._active;let o,i=[];if(!s.length)this.opacity!==0&&(o={opacity:0});else{const a=Ho[r.position].call(this,s,this._eventPosition);i=this._createItems(r),this.title=this.getTitle(i,r),this.beforeBody=this.getBeforeBody(i,r),this.body=this.getBody(i,r),this.afterBody=this.getAfterBody(i,r),this.footer=this.getFooter(i,r);const l=this._size=Tm(this,r),c=Object.assign({},a,l),d=Om(this.chart,r,c),u=Am(r,c,d,this.chart);this.xAlign=d.xAlign,this.yAlign=d.yAlign,o={opacity:1,x:u.x,y:u.y,width:l.width,height:l.height,caretX:a.x,caretY:a.y}}this._tooltipItems=i,this.$context=void 0,o&&this._resolveAnimations().update(this,o),t&&r.external&&r.external.call(this,{chart:this.chart,tooltip:this,replay:n})}drawCaret(t,n,r,s){const o=this.getCaretPosition(t,r,s);n.lineTo(o.x1,o.y1),n.lineTo(o.x2,o.y2),n.lineTo(o.x3,o.y3)}getCaretPosition(t,n,r){const{xAlign:s,yAlign:o}=this,{caretSize:i,cornerRadius:a}=r,{topLeft:l,topRight:c,bottomLeft:d,bottomRight:u}=_s(a),{x:f,y:g}=t,{width:m,height:p}=n;let y,b,_,x,M,C;return o==="center"?(M=g+p/2,s==="left"?(y=f,b=y-i,x=M+i,C=M-i):(y=f+m,b=y+i,x=M-i,C=M+i),_=y):(s==="left"?b=f+Math.max(l,d)+i:s==="right"?b=f+m-Math.max(c,u)-i:b=this.caretX,o==="top"?(x=g,M=x-i,y=b-i,_=b+i):(x=g+p,M=x+i,y=b+i,_=b-i),C=x),{x1:y,x2:b,x3:_,y1:x,y2:M,y3:C}}drawTitle(t,n,r){const s=this.title,o=s.length;let i,a,l;if(o){const c=Ns(r.rtl,this.x,this.width);for(t.x=Aa(this,r.titleAlign,r),n.textAlign=c.textAlign(r.titleAlign),n.textBaseline="middle",i=Kt(r.titleFont),a=r.titleSpacing,n.fillStyle=r.titleColor,n.font=i.string,l=0;l<o;++l)n.fillText(s[l],c.x(t.x),t.y+i.lineHeight/2),t.y+=i.lineHeight+a,l+1===o&&(t.y+=r.titleMarginBottom-a)}}_drawColorBox(t,n,r,s,o){const i=this.labelColors[r],a=this.labelPointStyles[r],{boxHeight:l,boxWidth:c}=o,d=Kt(o.bodyFont),u=Aa(this,"left",o),f=s.x(u),g=l<d.lineHeight?(d.lineHeight-l)/2:0,m=n.y+g;if(o.usePointStyle){const p={radius:Math.min(c,l)/2,pointStyle:a.pointStyle,rotation:a.rotation,borderWidth:1},y=s.leftForLtr(f,c)+c/2,b=m+l/2;t.strokeStyle=o.multiKeyBackground,t.fillStyle=o.multiKeyBackground,C0(t,p,y,b),t.strokeStyle=i.borderColor,t.fillStyle=i.backgroundColor,C0(t,p,y,b)}else{t.lineWidth=Je(i.borderWidth)?Math.max(...Object.values(i.borderWidth)):i.borderWidth||1,t.strokeStyle=i.borderColor,t.setLineDash(i.borderDash||[]),t.lineDashOffset=i.borderDashOffset||0;const p=s.leftForLtr(f,c),y=s.leftForLtr(s.xPlus(f,1),c-2),b=_s(i.borderRadius);Object.values(b).some(_=>_!==0)?(t.beginPath(),t.fillStyle=o.multiKeyBackground,pa(t,{x:p,y:m,w:c,h:l,radius:b}),t.fill(),t.stroke(),t.fillStyle=i.backgroundColor,t.beginPath(),pa(t,{x:y,y:m+1,w:c-2,h:l-2,radius:b}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(p,m,c,l),t.strokeRect(p,m,c,l),t.fillStyle=i.backgroundColor,t.fillRect(y,m+1,c-2,l-2))}t.fillStyle=this.labelTextColors[r]}drawBody(t,n,r){const{body:s}=this,{bodySpacing:o,bodyAlign:i,displayColors:a,boxHeight:l,boxWidth:c,boxPadding:d}=r,u=Kt(r.bodyFont);let f=u.lineHeight,g=0;const m=Ns(r.rtl,this.x,this.width),p=function(S){n.fillText(S,m.x(t.x+g),t.y+f/2),t.y+=f+o},y=m.textAlign(i);let b,_,x,M,C,k,N;for(n.textAlign=i,n.textBaseline="middle",n.font=u.string,t.x=Aa(this,y,r),n.fillStyle=r.bodyColor,lt(this.beforeBody,p),g=a&&y!=="right"?i==="center"?c/2+d:c+2+d:0,M=0,k=s.length;M<k;++M){for(b=s[M],_=this.labelTextColors[M],n.fillStyle=_,lt(b.before,p),x=b.lines,a&&x.length&&(this._drawColorBox(n,t,M,m,r),f=Math.max(u.lineHeight,l)),C=0,N=x.length;C<N;++C)p(x[C]),f=u.lineHeight;lt(b.after,p)}g=0,f=u.lineHeight,lt(this.afterBody,p),t.y-=o}drawFooter(t,n,r){const s=this.footer,o=s.length;let i,a;if(o){const l=Ns(r.rtl,this.x,this.width);for(t.x=Aa(this,r.footerAlign,r),t.y+=r.footerMarginTop,n.textAlign=l.textAlign(r.footerAlign),n.textBaseline="middle",i=Kt(r.footerFont),n.fillStyle=r.footerColor,n.font=i.string,a=0;a<o;++a)n.fillText(s[a],l.x(t.x),t.y+i.lineHeight/2),t.y+=i.lineHeight+r.footerSpacing}}drawBackground(t,n,r,s){const{xAlign:o,yAlign:i}=this,{x:a,y:l}=t,{width:c,height:d}=r,{topLeft:u,topRight:f,bottomLeft:g,bottomRight:m}=_s(s.cornerRadius);n.fillStyle=s.backgroundColor,n.strokeStyle=s.borderColor,n.lineWidth=s.borderWidth,n.beginPath(),n.moveTo(a+u,l),i==="top"&&this.drawCaret(t,n,r,s),n.lineTo(a+c-f,l),n.quadraticCurveTo(a+c,l,a+c,l+f),i==="center"&&o==="right"&&this.drawCaret(t,n,r,s),n.lineTo(a+c,l+d-m),n.quadraticCurveTo(a+c,l+d,a+c-m,l+d),i==="bottom"&&this.drawCaret(t,n,r,s),n.lineTo(a+g,l+d),n.quadraticCurveTo(a,l+d,a,l+d-g),i==="center"&&o==="left"&&this.drawCaret(t,n,r,s),n.lineTo(a,l+u),n.quadraticCurveTo(a,l,a+u,l),n.closePath(),n.fill(),s.borderWidth>0&&n.stroke()}_updateAnimationTarget(t){const n=this.chart,r=this.$animations,s=r&&r.x,o=r&&r.y;if(s||o){const i=Ho[t.position].call(this,this._active,this._eventPosition);if(!i)return;const a=this._size=Tm(this,t),l=Object.assign({},i,this._size),c=Om(n,t,l),d=Am(t,l,c,n);(s._to!==d.x||o._to!==d.y)&&(this.xAlign=c.xAlign,this.yAlign=c.yAlign,this.width=a.width,this.height=a.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,d))}}_willRender(){return!!this.opacity}draw(t){const n=this.options.setContext(this.getContext());let r=this.opacity;if(!r)return;this._updateAnimationTarget(n);const s={width:this.width,height:this.height},o={x:this.x,y:this.y};r=Math.abs(r)<.001?0:r;const i=vn(n.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;n.enabled&&a&&(t.save(),t.globalAlpha=r,this.drawBackground(o,t,s,n),P0(t,n.textDirection),o.y+=i.top,this.drawTitle(o,t,n),this.drawBody(o,t,n),this.drawFooter(o,t,n),$0(t,n.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,n){const r=this._active,s=t.map(({datasetIndex:a,index:l})=>{const c=this.chart.getDatasetMeta(a);if(!c)throw new Error("Cannot find a dataset at index "+a);return{datasetIndex:a,element:c.data[l],index:l}}),o=!la(r,s),i=this._positionChanged(s,n);(o||i)&&(this._active=s,this._eventPosition=n,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,n,r=!0){if(n&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,o=this._active||[],i=this._getActiveElements(t,o,n,r),a=this._positionChanged(i,t),l=n||!la(i,o)||a;return l&&(this._active=i,(s.enabled||s.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,n))),l}_getActiveElements(t,n,r,s){const o=this.options;if(t.type==="mouseout")return[];if(!s)return n.filter(a=>this.chart.data.datasets[a.datasetIndex]&&this.chart.getDatasetMeta(a.datasetIndex).controller.getParsed(a.index)!==void 0);const i=this.chart.getElementsAtEventForMode(t,o.mode,o,r);return o.reverse&&i.reverse(),i}_positionChanged(t,n){const{caretX:r,caretY:s,options:o}=this,i=Ho[o.position].call(this,t,n);return i!==!1&&(r!==i.x||s!==i.y)}}Pe(Sd,"positioners",Ho);var Pm={id:"tooltip",_element:Sd,positioners:Ho,afterInit(e,t,n){n&&(e.tooltip=new Sd({chart:e,options:n}))},beforeUpdate(e,t,n){e.tooltip&&e.tooltip.initialize(n)},reset(e,t,n){e.tooltip&&e.tooltip.initialize(n)},afterDraw(e){const t=e.tooltip;if(t&&t._willRender()){const n={tooltip:t};if(e.notifyPlugins("beforeTooltipDraw",{...n,cancelable:!0})===!1)return;t.draw(e.ctx),e.notifyPlugins("afterTooltipDraw",n)}},afterEvent(e,t){if(e.tooltip){const n=t.replay;e.tooltip.handleEvent(t.event,n,t.inChartArea)&&(t.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(e,t)=>t.bodyFont.size,boxWidth:(e,t)=>t.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Lm},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:e=>e!=="filter"&&e!=="itemSort"&&e!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};const jO=(e,t,n,r)=>(typeof t=="string"?(n=e.push(t)-1,r.unshift({index:n,label:t})):isNaN(t)&&(n=null),n);function WO(e,t,n,r){const s=e.indexOf(t);if(s===-1)return jO(e,t,n,r);const o=e.lastIndexOf(t);return s!==o?n:s}const GO=(e,t)=>e===null?null:sn(Math.round(e),0,t);function $m(e){const t=this.getLabels();return e>=0&&e<t.length?t[e]:e}class _d extends Os{constructor(t){super(t),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(t){const n=this._addedLabels;if(n.length){const r=this.getLabels();for(const{index:s,label:o}of n)r[s]===o&&r.splice(s,1);this._addedLabels=[]}super.init(t)}parse(t,n){if(dt(t))return null;const r=this.getLabels();return n=isFinite(n)&&r[n]===t?n:WO(r,t,Re(n,t),this._addedLabels),GO(n,r.length-1)}determineDataLimits(){const{minDefined:t,maxDefined:n}=this.getUserBounds();let{min:r,max:s}=this.getMinMax(!0);this.options.bounds==="ticks"&&(t||(r=0),n||(s=this.getLabels().length-1)),this.min=r,this.max=s}buildTicks(){const t=this.min,n=this.max,r=this.options.offset,s=[];let o=this.getLabels();o=t===0&&n===o.length-1?o:o.slice(t,n+1),this._valueRange=Math.max(o.length-(r?0:1),1),this._startValue=this.min-(r?.5:0);for(let i=t;i<=n;i++)s.push({value:i});return s}getLabelForValue(t){return $m.call(this,t)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(t){return typeof t!="number"&&(t=this.parse(t)),t===null?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getPixelForTick(t){const n=this.ticks;return t<0||t>n.length-1?null:this.getPixelForValue(n[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}}Pe(_d,"id","category"),Pe(_d,"defaults",{ticks:{callback:$m}});function YO(e,t){const n=[],{bounds:s,step:o,min:i,max:a,precision:l,count:c,maxTicks:d,maxDigits:u,includeBounds:f}=e,g=o||1,m=d-1,{min:p,max:y}=t,b=!dt(i),_=!dt(a),x=!dt(c),M=(y-p)/(u+1);let C=a0((y-p)/m/g)*g,k,N,S,w;if(C<1e-14&&!b&&!_)return[{value:p},{value:y}];w=Math.ceil(y/C)-Math.floor(p/C),w>m&&(C=a0(w*C/m/g)*g),dt(l)||(k=Math.pow(10,l),C=Math.ceil(C*k)/k),s==="ticks"?(N=Math.floor(p/C)*C,S=Math.ceil(y/C)*C):(N=p,S=y),b&&_&&o&&WM((a-i)/o,C/1e3)?(w=Math.round(Math.min((a-i)/C,d)),C=(a-i)/w,N=i,S=a):x?(N=b?i:N,S=_?a:S,w=c-1,C=(S-N)/w):(w=(S-N)/C,ua(w,Math.round(w),C/1e3)?w=Math.round(w):w=Math.ceil(w));const E=Math.max(l0(C),l0(N));k=Math.pow(10,dt(l)?E:l),N=Math.round(N*k)/k,S=Math.round(S*k)/k;let D=0;for(b&&(f&&N!==i?(n.push({value:i}),N<i&&D++,ua(Math.round((N+D*C)*k)/k,i,Fm(i,M,e))&&D++):N<i&&D++);D<w;++D){const P=Math.round((N+D*C)*k)/k;if(_&&P>a)break;n.push({value:P})}return _&&f&&S!==a?n.length&&ua(n[n.length-1].value,a,Fm(a,M,e))?n[n.length-1].value=a:n.push({value:a}):(!_||S===a)&&n.push({value:S}),n}function Fm(e,t,{horizontal:n,minRotation:r}){const s=er(r),o=(n?Math.sin(s):Math.cos(s))||.001,i=.75*t*(""+e).length;return Math.min(t/o,i)}class XO extends Os{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,n){return dt(t)||(typeof t=="number"||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const{beginAtZero:t}=this.options,{minDefined:n,maxDefined:r}=this.getUserBounds();let{min:s,max:o}=this;const i=l=>s=n?s:l,a=l=>o=r?o:l;if(t){const l=yr(s),c=yr(o);l<0&&c<0?a(0):l>0&&c>0&&i(0)}if(s===o){let l=o===0?1:Math.abs(o*.05);a(o+l),t||i(s-l)}this.min=s,this.max=o}getTickLimit(){const t=this.options.ticks;let{maxTicksLimit:n,stepSize:r}=t,s;return r?(s=Math.ceil(this.max/r)-Math.floor(this.min/r)+1,s>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${r} would result generating up to ${s} ticks. Limiting to 1000.`),s=1e3)):(s=this.computeTickLimit(),n=n||11),n&&(s=Math.min(n,s)),s}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this.options,n=t.ticks;let r=this.getTickLimit();r=Math.max(2,r);const s={maxTicks:r,bounds:t.bounds,min:t.min,max:t.max,precision:n.precision,step:n.stepSize,count:n.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:n.minRotation||0,includeBounds:n.includeBounds!==!1},o=this._range||this,i=YO(s,o);return t.bounds==="ticks"&&GM(i,this,"value"),t.reverse?(i.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),i}configure(){const t=this.ticks;let n=this.min,r=this.max;if(super.configure(),this.options.offset&&t.length){const s=(r-n)/Math.max(t.length-1,1)/2;n-=s,r+=s}this._startValue=n,this._endValue=r,this._valueRange=r-n}getLabelForValue(t){return td(t,this.chart.options.locale,this.options.ticks.format)}}class Ed extends XO{determineDataLimits(){const{min:t,max:n}=this.getMinMax(!0);this.min=yn(t)?t:0,this.max=yn(n)?n:1,this.handleTickRangeOptions()}computeTickLimit(){const t=this.isHorizontal(),n=t?this.width:this.height,r=er(this.options.ticks.minRotation),s=(t?Math.sin(r):Math.cos(r))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(n/Math.min(40,o.lineHeight/s))}getPixelForValue(t){return t===null?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}}Pe(Ed,"id","linear"),Pe(Ed,"defaults",{ticks:{callback:x0.formatters.numeric}});const Da={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},an=Object.keys(Da);function zm(e,t){return e-t}function Vm(e,t){if(dt(t))return null;const n=e._adapter,{parser:r,round:s,isoWeekday:o}=e._parseOpts;let i=t;return typeof r=="function"&&(i=r(i)),yn(i)||(i=typeof r=="string"?n.parse(i,r):n.parse(i)),i===null?null:(s&&(i=s==="week"&&(fa(o)||o===!0)?n.startOf(i,"isoWeek",o):n.startOf(i,s)),+i)}function Hm(e,t,n,r){const s=an.length;for(let o=an.indexOf(e);o<s-1;++o){const i=Da[an[o]],a=i.steps?i.steps:Number.MAX_SAFE_INTEGER;if(i.common&&Math.ceil((n-t)/(a*i.size))<=r)return an[o]}return an[s-1]}function KO(e,t,n,r,s){for(let o=an.length-1;o>=an.indexOf(n);o--){const i=an[o];if(Da[i].common&&e._adapter.diff(s,r,i)>=t-1)return i}return an[n?an.indexOf(n):0]}function qO(e){for(let t=an.indexOf(e)+1,n=an.length;t<n;++t)if(Da[an[t]].common)return an[t]}function Um(e,t,n){if(!n)e[t]=!0;else if(n.length){const{lo:r,hi:s}=Qc(n,t),o=n[r]>=t?n[r]:n[s];e[o]=!0}}function ZO(e,t,n,r){const s=e._adapter,o=+s.startOf(t[0].value,r),i=t[t.length-1].value;let a,l;for(a=o;a<=i;a=+s.add(a,1,r))l=n[a],l>=0&&(t[l].major=!0);return t}function Bm(e,t,n){const r=[],s={},o=t.length;let i,a;for(i=0;i<o;++i)a=t[i],s[a]=i,r.push({value:a,major:!1});return o===0||!n?r:ZO(e,r,s,n)}class Ia extends Os{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,n={}){const r=t.time||(t.time={}),s=this._adapter=new cT._date(t.adapters.date);s.init(n),_o(r.displayFormats,s.formats()),this._parseOpts={parser:r.parser,round:r.round,isoWeekday:r.isoWeekday},super.init(t),this._normalized=n.normalized}parse(t,n){return t===void 0?null:Vm(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this.options,n=this._adapter,r=t.time.unit||"day";let{min:s,max:o,minDefined:i,maxDefined:a}=this.getUserBounds();function l(c){!i&&!isNaN(c.min)&&(s=Math.min(s,c.min)),!a&&!isNaN(c.max)&&(o=Math.max(o,c.max))}(!i||!a)&&(l(this._getLabelBounds()),(t.bounds!=="ticks"||t.ticks.source!=="labels")&&l(this.getMinMax(!1))),s=yn(s)&&!isNaN(s)?s:+n.startOf(Date.now(),r),o=yn(o)&&!isNaN(o)?o:+n.endOf(Date.now(),r)+1,this.min=Math.min(s,o-1),this.max=Math.max(s+1,o)}_getLabelBounds(){const t=this.getLabelTimestamps();let n=Number.POSITIVE_INFINITY,r=Number.NEGATIVE_INFINITY;return t.length&&(n=t[0],r=t[t.length-1]),{min:n,max:r}}buildTicks(){const t=this.options,n=t.time,r=t.ticks,s=r.source==="labels"?this.getLabelTimestamps():this._generate();t.bounds==="ticks"&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const o=this.min,i=this.max,a=ZM(s,o,i);return this._unit=n.unit||(r.autoSkip?Hm(n.minUnit,this.min,this.max,this._getLabelCapacity(o)):KO(this,a.length,n.minUnit,this.min,this.max)),this._majorUnit=!r.major.enabled||this._unit==="year"?void 0:qO(this._unit),this.initOffsets(s),t.reverse&&a.reverse(),Bm(this,a,this._majorUnit)}afterAutoSkip(){this.options.offsetAfterAutoskip&&this.initOffsets(this.ticks.map(t=>+t.value))}initOffsets(t=[]){let n=0,r=0,s,o;this.options.offset&&t.length&&(s=this.getDecimalForValue(t[0]),t.length===1?n=1-s:n=(this.getDecimalForValue(t[1])-s)/2,o=this.getDecimalForValue(t[t.length-1]),t.length===1?r=o:r=(o-this.getDecimalForValue(t[t.length-2]))/2);const i=t.length<3?.5:.25;n=sn(n,0,i),r=sn(r,0,i),this._offsets={start:n,end:r,factor:1/(n+1+r)}}_generate(){const t=this._adapter,n=this.min,r=this.max,s=this.options,o=s.time,i=o.unit||Hm(o.minUnit,n,r,this._getLabelCapacity(n)),a=Re(s.ticks.stepSize,1),l=i==="week"?o.isoWeekday:!1,c=fa(l)||l===!0,d={};let u=n,f,g;if(c&&(u=+t.startOf(u,"isoWeek",l)),u=+t.startOf(u,c?"day":i),t.diff(r,n,i)>1e5*a)throw new Error(n+" and "+r+" are too far apart with stepSize of "+a+" "+i);const m=s.ticks.source==="data"&&this.getDataTimestamps();for(f=u,g=0;f<r;f=+t.add(f,a,i),g++)Um(d,f,m);return(f===r||s.bounds==="ticks"||g===1)&&Um(d,f,m),Object.keys(d).sort(zm).map(p=>+p)}getLabelForValue(t){const n=this._adapter,r=this.options.time;return r.tooltipFormat?n.format(t,r.tooltipFormat):n.format(t,r.displayFormats.datetime)}format(t,n){const s=this.options.time.displayFormats,o=this._unit,i=n||s[o];return this._adapter.format(t,i)}_tickFormatFunction(t,n,r,s){const o=this.options,i=o.ticks.callback;if(i)return mt(i,[t,n,r],this);const a=o.time.displayFormats,l=this._unit,c=this._majorUnit,d=l&&a[l],u=c&&a[c],f=r[n],g=c&&u&&f&&f.major;return this._adapter.format(t,s||(g?u:d))}generateTickLabels(t){let n,r,s;for(n=0,r=t.length;n<r;++n)s=t[n],s.label=this._tickFormatFunction(s.value,n,t)}getDecimalForValue(t){return t===null?NaN:(t-this.min)/(this.max-this.min)}getPixelForValue(t){const n=this._offsets,r=this.getDecimalForValue(t);return this.getPixelForDecimal((n.start+r)*n.factor)}getValueForPixel(t){const n=this._offsets,r=this.getDecimalForPixel(t)/n.factor-n.end;return this.min+r*(this.max-this.min)}_getLabelSize(t){const n=this.options.ticks,r=this.ctx.measureText(t).width,s=er(this.isHorizontal()?n.maxRotation:n.minRotation),o=Math.cos(s),i=Math.sin(s),a=this._resolveTickFontOptions(0).size;return{w:r*o+a*i,h:r*i+a*o}}_getLabelCapacity(t){const n=this.options.time,r=n.displayFormats,s=r[n.unit]||r.millisecond,o=this._tickFormatFunction(t,0,Bm(this,[t],this._majorUnit),s),i=this._getLabelSize(o),a=Math.floor(this.isHorizontal()?this.width/i.w:this.height/i.h)-1;return a>0?a:1}getDataTimestamps(){let t=this._cache.data||[],n,r;if(t.length)return t;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(n=0,r=s.length;n<r;++n)t=t.concat(s[n].controller.getAllParsedValues(this));return this._cache.data=this.normalize(t)}getLabelTimestamps(){const t=this._cache.labels||[];let n,r;if(t.length)return t;const s=this.getLabels();for(n=0,r=s.length;n<r;++n)t.push(Vm(this,s[n]));return this._cache.labels=this._normalized?t:this.normalize(t)}normalize(t){return f0(t.sort(zm))}}Pe(Ia,"id","time"),Pe(Ia,"defaults",{bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}});function La(e,t,n){let r=0,s=e.length-1,o,i,a,l;n?(t>=e[r].pos&&t<=e[s].pos&&({lo:r,hi:s}=Jc(e,"pos",t)),{pos:o,time:a}=e[r],{pos:i,time:l}=e[s]):(t>=e[r].time&&t<=e[s].time&&({lo:r,hi:s}=Jc(e,"time",t)),{time:o,pos:a}=e[r],{time:i,pos:l}=e[s]);const c=i-o;return c?a+(l-a)*(t-o)/c:a}class jm extends Ia{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const t=this._getTimestampsForTable(),n=this._table=this.buildLookupTable(t);this._minPos=La(n,this.min),this._tableRange=La(n,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){const{min:n,max:r}=this,s=[],o=[];let i,a,l,c,d;for(i=0,a=t.length;i<a;++i)c=t[i],c>=n&&c<=r&&s.push(c);if(s.length<2)return[{time:n,pos:0},{time:r,pos:1}];for(i=0,a=s.length;i<a;++i)d=s[i+1],l=s[i-1],c=s[i],Math.round((d+l)/2)!==c&&o.push({time:c,pos:i/(a-1)});return o}_generate(){const t=this.min,n=this.max;let r=super.getDataTimestamps();return(!r.includes(t)||!r.length)&&r.splice(0,0,t),(!r.includes(n)||r.length===1)&&r.push(n),r.sort((s,o)=>s-o)}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;const n=this.getDataTimestamps(),r=this.getLabelTimestamps();return n.length&&r.length?t=this.normalize(n.concat(r)):t=n.length?n:r,t=this._cache.all=t,t}getDecimalForValue(t){return(La(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){const n=this._offsets,r=this.getDecimalForPixel(t)/n.factor-n.end;return La(this._table,r*this._tableRange+this._minPos,!0)}}Pe(jm,"id","timeseries"),Pe(jm,"defaults",Ia.defaults);const Wm="label";function Gm(e,t){typeof e=="function"?e(t):e&&(e.current=t)}function QO(e,t){const n=e.options;n&&t&&Object.assign(n,t)}function Ym(e,t){e.labels=t}function Xm(e,t,n=Wm){const r=[];e.datasets=t.map(s=>{const o=e.datasets.find(i=>i[n]===s[n]);return!o||!s.data||r.includes(o)?{...s}:(r.push(o),Object.assign(o,s),o)})}function JO(e,t=Wm){const n={labels:[],datasets:[]};return Ym(n,e.labels),Xm(n,e.datasets,t),n}function RO(e,t){const{height:n=150,width:r=300,redraw:s=!1,datasetIdKey:o,type:i,data:a,options:l,plugins:c=[],fallbackContent:d,updateMode:u,...f}=e,g=v.useRef(null),m=v.useRef(null),p=()=>{g.current&&(m.current=new zo(g.current,{type:i,data:JO(a,o),options:l&&{...l},plugins:c}),Gm(t,m.current))},y=()=>{Gm(t,null),m.current&&(m.current.destroy(),m.current=null)};return v.useEffect(()=>{!s&&m.current&&l&&QO(m.current,l)},[s,l]),v.useEffect(()=>{!s&&m.current&&Ym(m.current.config.data,a.labels)},[s,a.labels]),v.useEffect(()=>{!s&&m.current&&a.datasets&&Xm(m.current.config.data,a.datasets,o)},[s,a.datasets]),v.useEffect(()=>{m.current&&(s?(y(),setTimeout(p)):m.current.update(u))},[s,l,a.labels,a.datasets,u]),v.useEffect(()=>{m.current&&(y(),setTimeout(p))},[i]),v.useEffect(()=>(p(),()=>y()),[]),h.jsx("canvas",{ref:g,role:"img",height:n,width:r,...f,children:d})}const e5=v.forwardRef(RO);function Km(e,t){return zo.register(t),v.forwardRef((n,r)=>h.jsx(e5,{...n,ref:r,type:e}))}const t5=Km("bar",xa),n5=Km("doughnut",Ao);zo.register(_d,Ed,Oa,PO,Pm,Mm);const r5=({labels:e,datasets:t,height:n=200,showLegend:r=!1,showGrid:s=!1,showYGrid:o=!1,showYAxis:i=!1,className:a="",id:l,editorAttrs:c})=>{const d=v.useMemo(()=>({labels:e??[],datasets:(t??[]).map((m,p)=>({label:m.label,data:m.data,backgroundColor:m.backgroundColor||(p===0?"#7C3AED":"#DDD6FE"),borderRadius:m.borderRadius??4,barThickness:16,maxBarThickness:20,yAxisID:m.yAxisID??"y"}))}),[e,t]),u=v.useMemo(()=>(t??[]).some(m=>m.yAxisID==="y1"),[t]),f=v.useMemo(()=>({responsive:!0,maintainAspectRatio:!1,animation:{duration:750,easing:"easeOutQuart"},animations:{x:{duration:0},y:{from:m=>{if(m.type==="data")return m.chart.scales.y.getPixelForValue(0)}}},transitions:{active:{animation:{duration:200}},resize:{animation:{duration:0}}},plugins:{legend:{display:r,position:"top"},tooltip:{backgroundColor:"#1e293b",titleColor:"#fff",bodyColor:"#fff",padding:12,cornerRadius:8}},scales:{x:{grid:{display:s},border:{display:!1},ticks:{color:"#94a3b8",font:{size:11}}},y:{type:"linear",position:"left",grid:{display:o,color:"#e2e8f0"},border:{display:!1},ticks:{display:i,color:"#94a3b8",font:{size:11},padding:8},beginAtZero:!0,grace:"5%"},...u?{y1:{type:"linear",position:"right",grid:{display:!1},border:{display:!1},ticks:{display:i,color:"#94a3b8",font:{size:11},padding:8},beginAtZero:!0,grace:"5%"}}:{}}}),[r,s,o,i,u]),g=typeof n=="number"?n:/^\d+$/.test(String(n))?`${n}px`:n;return h.jsx("div",{className:`bar-chart-container ${a}`,style:{height:g},id:l,...c,children:h.jsx(t5,{data:d,options:f})})},s5=v.memo(r5,(e,t)=>JSON.stringify(e.labels)===JSON.stringify(t.labels)&&JSON.stringify(e.datasets)===JSON.stringify(t.datasets)&&e.height===t.height&&e.showLegend===t.showLegend&&e.showGrid===t.showGrid&&e.showYGrid===t.showYGrid&&e.showYAxis===t.showYAxis&&e.className===t.className&&e.id===t.id&&e.editorAttrs===t.editorAttrs);zo.register(Vo,Pm,Mm);const o5=({data:e,centerLabel:t,centerValue:n,size:r=200,cutout:s="70%",showLegend:o=!1,className:i="",id:a,editorAttrs:l})=>{const c=v.useMemo(()=>({labels:(e??[]).map(f=>f.name),datasets:[{data:(e??[]).map(f=>f.value),backgroundColor:(e??[]).map(f=>f.color),borderWidth:0,hoverOffset:4}]}),[e]),d=v.useMemo(()=>({responsive:!0,maintainAspectRatio:!1,cutout:s,plugins:{legend:{display:o},tooltip:{backgroundColor:"#1e293b",titleColor:"#fff",bodyColor:"#fff",padding:12,cornerRadius:8,callbacks:{label:f=>` $${f.parsed.toLocaleString()}`}}}}),[s,o]),u=typeof r=="number"?r:/^\d+$/.test(String(r))?`${r}px`:r;return h.jsx("div",{className:`donut-chart-wrapper ${i}`,id:a,...l,children:h.jsxs("div",{className:"relative",style:{width:u,height:u},children:[h.jsx(n5,{data:c,options:d}),(t||n)&&h.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center pointer-events-none",children:[t&&h.jsx("span",{className:"text-xs text-slate-500 dark:text-slate-400",children:t}),n&&h.jsx("span",{className:"text-lg font-bold text-slate-900 dark:text-white",children:n})]})]})})},i5=({id:e,className:t,style:n,children:r,editorAttrs:s})=>h.jsx("div",{id:e,className:t,style:n,...s,children:r}),a5=({children:e,cols:t=1,responsive:n,gap:r,rowGap:s,colGap:o,autoRows:i,autoCols:a,flow:l="row",className:c="",style:d,onClick:u,id:f,editorAttrs:g})=>{const m=["grid"];if(t&&m.push(`grid-cols-${t}`),n&&(n.sm&&m.push(`sm:grid-cols-${n.sm}`),n.md&&m.push(`md:grid-cols-${n.md}`),n.lg&&m.push(`lg:grid-cols-${n.lg}`),n.xl&&m.push(`xl:grid-cols-${n.xl}`),n["2xl"]&&m.push(`2xl:grid-cols-${n["2xl"]}`)),r!==void 0&&r>0&&m.push(`gap-${r}`),s!==void 0&&s>0&&m.push(`gap-y-${s}`),o!==void 0&&o>0&&m.push(`gap-x-${o}`),i){const y={auto:"auto-rows-auto",min:"auto-rows-min",max:"auto-rows-max",fr:"auto-rows-fr"};m.push(y[i])}if(a){const y={auto:"auto-cols-auto",min:"auto-cols-min",max:"auto-cols-max",fr:"auto-cols-fr"};m.push(y[a])}const p={row:"grid-flow-row",col:"grid-flow-col",dense:"grid-flow-dense","row-dense":"grid-flow-row-dense","col-dense":"grid-flow-col-dense"};return m.push(p[l]),c&&m.push(c),h.jsx(I,{className:m.join(" "),style:d,onClick:u,id:f,...g,children:e})},l5=({children:e,direction:t="row",justify:n="start",align:r="stretch",wrap:s="nowrap",gap:o=0,grow:i=!1,shrink:a=!0,className:l="",style:c,onClick:d,id:u,editorAttrs:f})=>{const g=["flex"],m={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};g.push(m[t]);const p={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};g.push(p[s]);const y={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};g.push(y[n]);const b={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};return g.push(b[r]),o>0&&g.push(`gap-${o}`),i===!0?g.push("flex-grow"):typeof i=="number"&&i>0&&g.push(`flex-grow-${i}`),a===!1?g.push("shrink-0"):a===!0?g.push("flex-shrink"):typeof a=="number"&&a>0&&g.push(`flex-shrink-${a}`),l&&g.push(l),h.jsx(I,{className:g.join(" "),style:c,onClick:d,id:u,...f,children:e})},c5=({title:e,subtitle:t,children:n,padding:r="none",background:s="none",maxWidth:o,centered:i=!1,border:a=!1,shadow:l="none",rounded:c=!1,className:d="",style:u,onClick:f,id:g,editorAttrs:m})=>{const p=[],y={none:"",sm:"p-2",md:"p-4",lg:"p-6",xl:"p-8"};y[r]&&p.push(y[r]);const b={none:"",white:"bg-white dark:bg-gray-700",gray:"bg-gray-50 dark:bg-gray-800",primary:"bg-blue-50 dark:bg-blue-900/20",secondary:"bg-gray-50 dark:bg-gray-800"};if(s!=="none"&&p.push(b[s]),o){const x={sm:"max-w-sm",md:"max-w-md",lg:"max-w-lg",xl:"max-w-xl","2xl":"max-w-2xl","4xl":"max-w-4xl","6xl":"max-w-6xl","7xl":"max-w-7xl",full:"max-w-full"};p.push(x[o])}i&&p.push("mx-auto"),a&&p.push("border","border-gray-200","dark:border-gray-700");const _={none:"",sm:"shadow-sm",md:"shadow-md",lg:"shadow-lg",xl:"shadow-xl"};return l!=="none"&&p.push(_[l]),c&&p.push("rounded-lg"),d&&p.push(d),h.jsxs(zd,{className:p.join(" "),style:u,onClick:f,id:g,...m,children:[(e||t)&&h.jsxs("div",{className:"mb-4",children:[e&&h.jsx("h2",{className:"text-2xl font-bold text-gray-900 dark:text-white",children:e}),t&&h.jsx("p",{className:"mt-1 text-sm text-gray-600 dark:text-gray-400",children:t})]}),n]})},d5=({leftWidth:e="250px",rightWidth:t="300px",leftSlot:n,centerSlot:r,rightSlot:s,className:o="",style:i,id:a,editorAttrs:l})=>{const c=`flex flex-row w-full h-full ${o}`.trim(),d={width:e,flexShrink:0},u={flex:1,minWidth:0},f={width:t,flexShrink:0};return h.jsxs(I,{className:c,style:i,id:a,...l,children:[h.jsx(I,{className:"flex flex-col",style:d,children:n}),h.jsx(I,{className:"flex flex-col",style:u,children:r}),h.jsx(I,{className:"flex flex-col",style:f,children:s})]})},u5={language:"json",theme:"vs-dark",options:{automaticLayout:!0,minimap:{enabled:!0},scrollBeyondLastLine:!1,fontSize:14,tabSize:2,formatOnPaste:!0,formatOnType:!0,wordWrap:"on",folding:!0,lineNumbers:"on",renderWhitespace:"selection"}},qm=[{uri:`${window.location.origin}/schemas/layout.json`,fileMatch:["*layout*.json"],schema:{type:"object",properties:{version:{type:"string"},endpoint:{type:"string"},components:{type:"array"}},required:["version","endpoint","components"]}}],f5={"vs/nls":{availableLanguages:{"*":"ko"}}};function h5(e){e.languages.json.jsonDefaults.setDiagnosticsOptions({validate:!0,schemas:qm,enableSchemaRequest:!0,schemaValidation:"error",schemaRequest:"error"})}const g5={identifier:"sirsoft-admin_basic",vendor:"sirsoft",name:{ko:"Admin Basic",en:"Admin Basic"},version:"1.0.4",license:"MIT",description:{ko:"그누보드7 기본 관리자 템플릿",en:"Gnuboard7 default admin template"},type:"admin",locales:["ko","en"],author:{name:"sirsoft",email:"contact@sirsoft.com",url:"https://sirsoft.com"},release_date:"2026-05-15",g7_version:">=7.0.0",dependencies:{modules:{},plugins:{}},assets:{css:["assets/css/main.css","assets/css/ui-system.css"],js:["dist/components.js"],fonts:["assets/fonts/"],images:["assets/images/"]},components:{basic:["a","button","checkbox","div","form","h1","h2","h3","hr","icon","img","input","label","li","nav","p","section","select","span","svg","table","tbody","td","textarea","th","thead","tr","ul"],composite:["action-menu","admin-footer","admin-header","admin-sidebar","alert-dialog","breadcrumb","card","confirm-dialog","data-grid","dialog","dropdown","empty-state","filter-group","icon-button","loading-spinner","modal","page-header","pagination","product-card","search-bar","stat-card","status-badge","tab-navigation","template-card","toast"],layout:["container","flex","grid","section"]},preview:{thumbnail:"preview/thumbnail.png",screenshots:["preview/screenshot1.png","preview/screenshot2.png"]},error_config:{layouts:{401:"errors/401",403:"errors/403",404:"errors/404",500:"errors/500",503:"errors/503",maintenance:"errors/maintenance"}},github_url:"https://github.com/gnuboard/g7-template-sirsoft-admin_basic",github_changelog_url:"https://github.com/gnuboard/g7-template-sirsoft-admin_basic/blob/main/CHANGELOG.md",externals:[{id:"fontawesome",type:"style",url:"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",preconnect:"https://cdnjs.cloudflare.com"},{id:"pretendard",type:"webfont",url:"https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css",preconnect:"https://cdn.jsdelivr.net",crossorigin:"anonymous"},{id:"flag-icons",type:"style",url:"https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"}]},rr=window.G7Core?.createLogger?.("Handler:SetTheme")??{log:(...e)=>console.log("[Handler:SetTheme]",...e),warn:(...e)=>console.warn("[Handler:SetTheme]",...e),error:(...e)=>console.error("[Handler:SetTheme]",...e)},Md=["auto","light","dark"],Zm="g7_color_scheme",p5=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,Pa=e=>{const t=p5(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")};function m5(){try{const e=localStorage.getItem(Zm),t=e&&Md.includes(e)?e:"auto";Pa(t);try{window.G7Core?.state?.setGlobal?.({theme:t})}catch(n){rr.warn("Failed to set global theme state:",n)}rr.log("Initial theme applied:",t)}catch(e){rr.warn("Failed to load initial theme:",e),Pa("auto")}}async function b5(e,t){const n=e?.target;if(n&&typeof n=="string"&&Md.includes(n)){Pa(n);try{window.G7Core?.state?.setGlobal?.({theme:n})}catch(r){rr.warn("Failed to set global theme state:",r)}return}m5()}async function y5(e,t){let n;if(e?.target&&typeof e.target=="string"&&!e.target.includes("{{")&&(n=e.target),!n||typeof n!="string"){rr.warn("Invalid theme:",n);return}if(!Md.includes(n)){rr.warn("Unsupported theme:",n);return}const r=n;try{localStorage.setItem(Zm,r)}catch(s){rr.error("Failed to save theme to localStorage:",s);return}Pa(r);try{window.G7Core?.state?.setGlobal?.({theme:r})}catch(s){rr.warn("Failed to set global theme state:",s)}rr.log("Theme changed to:",r)}const Uo=window.G7Core?.createLogger?.("Handler:InitMenu")??{log:(...e)=>console.log("[Handler:InitMenu]",...e),warn:(...e)=>console.warn("[Handler:InitMenu]",...e),error:(...e)=>console.error("[Handler:InitMenu]",...e)};function Qm(e,t){for(const n of e){if(n.slug===t)return n;if(n.children&&n.children.length>0){const r=Qm(n.children,t);if(r)return r}}return null}function Jm(e){return new URLSearchParams(window.location.search).get(e)}function v5(e){return window.G7Core?.state?.getDataSource?.(e)}async function x5(e,t=30,n=100){for(let r=0;r<t;r++){const o=v5(e)?.data;if(Array.isArray(o)&&o.length>0)return o;await new Promise(i=>setTimeout(i,n))}return null}async function w5(e,t){const n=window.G7Core,r=Jm("menu"),s=Jm("mode");if(!r){Uo.log("[initMenuFromUrl] No menu parameter in URL");return}if(!n?.state?.set){Uo.warn("[initMenuFromUrl] G7Core.state.set not available");return}const o=await x5("menus");if(!o){Uo.warn("[initMenuFromUrl] menus data not available after waiting");return}const i=Qm(o,r);if(!i){Uo.warn("[initMenuFromUrl] Menu not found with slug:",r);return}const a=s==="edit"?"edit":"view";n.state.set({selectedMenuId:i.id,selectedMenu:i,panelMode:a}),a==="edit"&&n.state.set({formData:{name:i.name,slug:i.slug,url:i.url,icon:i.icon,parent_id:i.parent_id,module_id:i.module_id,is_active:i.is_active}}),Uo.log("[initMenuFromUrl] Menu initialized from URL:",{slug:r,mode:a,menuId:i.id})}const Rt=window.G7Core?.createLogger?.("Handler:FilterVisibility")??{log:(...e)=>console.log("[Handler:FilterVisibility]",...e),warn:(...e)=>console.warn("[Handler:FilterVisibility]",...e),error:(...e)=>console.error("[Handler:FilterVisibility]",...e)},$a="g7_filter_visibility_";async function k5(e,t){const{storageKey:n,defaultFilters:r=[],stateKey:s="visibleFilters"}=e?.params||{};if(!n){Rt.warn("[initFilterVisibility] storageKey is required");return}const o=`${$a}${n}`;try{const i=localStorage.getItem(o);let a;if(i){const l=JSON.parse(i);Array.isArray(l)?(a=l,Rt.log(`[initFilterVisibility] Loaded from localStorage: ${a.join(", ")}`)):(a=r,Rt.warn("[initFilterVisibility] Invalid saved data format, using defaults"))}else a=r,Rt.log(`[initFilterVisibility] No saved data, using defaults: ${r.join(", ")}`);t?.setState&&t.setState({[s]:a})}catch(i){Rt.error("[initFilterVisibility] Failed to load filter visibility:",i),t?.setState&&t.setState({[s]:r})}}async function C5(e,t){const{storageKey:n,filters:r}=e?.params||{};if(!n){Rt.warn("[saveFilterVisibility] storageKey is required");return}if(!Array.isArray(r)){Rt.warn("[saveFilterVisibility] filters must be an array");return}const s=`${$a}${n}`;try{localStorage.setItem(s,JSON.stringify(r)),Rt.log(`[saveFilterVisibility] Saved to localStorage: ${r.join(", ")}`)}catch(o){Rt.error("[saveFilterVisibility] Failed to save filter visibility:",o)}}async function S5(e,t){const{storageKey:n,filterId:r,stateKey:s="visibleFilters"}=e?.params||{};if(!n||!r){Rt.warn("[toggleFilterVisibility] storageKey and filterId are required");return}const o=t?.state?.[s]||[];let i;o.includes(r)?i=o.filter(l=>l!==r):i=[...o,r],t?.setState&&t.setState({[s]:i});const a=`${$a}${n}`;try{localStorage.setItem(a,JSON.stringify(i)),Rt.log(`[toggleFilterVisibility] Toggled ${r}: ${i.join(", ")}`)}catch(l){Rt.error("[toggleFilterVisibility] Failed to save:",l)}}async function _5(e,t){const{storageKey:n,defaultFilters:r=[],stateKey:s="visibleFilters"}=e?.params||{};if(!n){Rt.warn("[resetFilterVisibility] storageKey is required");return}const o=`${$a}${n}`;try{localStorage.setItem(o,JSON.stringify(r)),t?.setState&&t.setState({[s]:r}),Rt.log(`[resetFilterVisibility] Reset to defaults: ${r.join(", ")}`)}catch(i){Rt.error("[resetFilterVisibility] Failed to reset:",i)}}const Ds=window.G7Core?.createLogger?.("Handler:MultilingualTag")??{log:(...e)=>console.log("[Handler:MultilingualTag]",...e),warn:(...e)=>console.warn("[Handler:MultilingualTag]",...e),error:(...e)=>console.error("[Handler:MultilingualTag]",...e)};async function E5(e,t){const n=window.G7Core;if(!n){Ds.error("G7Core not available");return}const r=n.state?.getGlobal?.()?.multilingualTagEdit;if(!r){Ds.warn("No multilingualTagEdit state found"),n.modal?.close?.();return}const{fieldName:s,editingIndex:o,values:i,statePath:a,currentTags:l}=r;if(!Object.values(i||{}).some(g=>typeof g=="string"&&g.trim()!=="")){Ds.warn("No values to save"),n.modal?.close?.(),Nd(n);return}const d=Array.isArray(l)?l:[];let u;o==null?u=[...d,i]:u=d.map((g,m)=>m===o?i:g);const f=a||(s?.startsWith("form.")?s:`form.${s}`);n.state?.setParentLocal?.({[f]:u}),n.modal?.close?.(),Nd(n)}async function M5(e,t){const n=window.G7Core;if(!n){Ds.error("G7Core not available");return}n.modal?.close?.(),Nd(n)}async function N5(e,t){const n=window.G7Core;if(!n){Ds.error("G7Core not available");return}const r=e?.params?.locale;if(!r){Ds.warn("No locale specified");return}const s=t?.event;let o="";s?.target?.value!==void 0?o=s.target.value:s?.value!==void 0&&(o=s.value);const a=(n.state?.getGlobal?.()||{}).multilingualTagEdit||{},l={...a.values||{},[r]:o};n.dispatch?.({handler:"setState",params:{target:"global",multilingualTagEdit:{...a,values:l}}})}function Nd(e){e.dispatch?.({handler:"setState",params:{target:"global",multilingualTagEdit:null}})}function Rm(e){const t=e.getFullYear(),n=String(e.getMonth()+1).padStart(2,"0"),r=String(e.getDate()).padStart(2,"0"),s=String(e.getHours()).padStart(2,"0"),o=String(e.getMinutes()).padStart(2,"0"),i=String(e.getSeconds()).padStart(2,"0");return`${t}-${n}-${r}T${s}:${o}:${i}`}function T5(e){const t=new Date,n=new Date(t.getFullYear(),t.getMonth(),t.getDate(),23,59,59),r=Rm(n),s=new Date(t.getFullYear(),t.getMonth(),t.getDate(),0,0,0);switch(e){case"today":break;case"week":s.setDate(s.getDate()-6);break;case"month":s.setMonth(s.getMonth()-1);break;case"3months":s.setMonth(s.getMonth()-3);break;case"6months":s.setMonth(s.getMonth()-6);break;case"1year":s.setFullYear(s.getFullYear()-1);break}return{startDate:Rm(s),endDate:r}}function O5(e){const{preset:t}=e.params||{preset:"today"},{startDate:n,endDate:r}=T5(t);return{startDate:n,endDate:r,preset:t}}const Bo=window.G7Core?.createLogger?.("Handler:Sidebar")??{log:(...e)=>console.log("[Handler:Sidebar]",...e),warn:(...e)=>console.warn("[Handler:Sidebar]",...e),error:(...e)=>console.error("[Handler:Sidebar]",...e)},eb="g7_admin_sidebar_collapsed";function tb(){try{return localStorage.getItem(eb)==="1"}catch{return!1}}function A5(e){try{localStorage.setItem(eb,e?"1":"0")}catch(t){Bo.error("Failed to save sidebar state to localStorage:",t)}try{window.G7Core?.state?.setGlobal?.({sidebarCollapsed:e})}catch(t){Bo.warn("Failed to set global sidebar state:",t)}}function nb(){const e=tb();try{window.G7Core?.state?.setGlobal?.({sidebarCollapsed:e})}catch(t){Bo.warn("Failed to restore sidebar state:",t)}Bo.log("Initial sidebar collapsed state restored:",e)}async function D5(e,t){nb()}async function I5(e,t){const r=!(()=>{try{return!!window.G7Core?.state?.getGlobal?.()?.sidebarCollapsed}catch{return tb()}})();A5(r),Bo.log("Sidebar collapsed toggled to:",r)}const Td=window.G7Core?.createLogger?.("Handler:DownloadAttachment")??{log:(...e)=>console.log("[Handler:DownloadAttachment]",...e),warn:(...e)=>console.warn("[Handler:DownloadAttachment]",...e),error:(...e)=>console.error("[Handler:DownloadAttachment]",...e)};async function L5(e,t){const n=window.G7Core,{url:r,filename:s}=e?.params||{};if(!r){Td.warn("다운로드 URL 이 없어 요청을 건너뜁니다.");return}if(!n?.api?.get){Td.error("G7Core.api.get 이 초기화되지 않아 다운로드를 실행할 수 없습니다.");return}try{const o=await n.api.get(r,{responseType:"blob"});if(o){const i=URL.createObjectURL(o),a=document.createElement("a");a.href=i,a.download=s||"",document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}}catch(o){Td.error("첨부파일 다운로드 실패",o),n?.toast?.error?.(n?.t?.("common.download_failed")??"Download failed")}}const Fa=window.G7Core?.createLogger?.("Handler:DetectAssetUrlMode")??{log:(...e)=>console.log("[Handler:DetectAssetUrlMode]",...e),warn:(...e)=>console.warn("[Handler:DetectAssetUrlMode]",...e),error:(...e)=>console.error("[Handler:DetectAssetUrlMode]",...e)},P5="G7_ASSET_PROBE_OK";async function rb(e){try{const t=await fetch(e,{cache:"no-store",credentials:"omit"});if(!t.ok)return!1;const n=t.headers.get("content-type")??"";return/javascript|ecmascript/i.test(n)?(await t.text()).includes(P5):!1}catch{return!1}}async function sb(){const[e,t]=await Promise.all([rb("/api/system/asset-probe.js"),rb("/api/system/asset-probe")]);return e&&t?"extension":!e&&t?"extensionless":"unavailable"}function $5(){return window.G7Config?.settings?.general?.asset_url_mode??"extension"}async function F5(e){try{const t=await sb();if(t==="unavailable")return;const n=$5();if(t===n)return;window.G7Core?.state?.setGlobal?.({assetUrlModeDrift:{detected:t,stored:n}})}catch(t){Fa.warn("자산 URL 방식 대조 실패 (대시보드 동작에는 영향 없음):",t)}}async function z5(e,t){const n=window.G7Core,r=t?.setState??n?.state?.setLocal,s=o=>{typeof r=="function"?r({asset_url_mode_detect_status:o}):Fa.warn("setState 를 찾지 못해 감지 상태를 폼에 반영하지 못했습니다")};try{s("detecting");const o=await sb();if(o==="unavailable"){s("unavailable");return}typeof r=="function"?r({"general.asset_url_mode":o,asset_url_mode_detect_status:o}):Fa.warn("setState 를 찾지 못해 감지 결과를 폼에 반영하지 못했습니다")}catch(o){Fa.error("자산 URL 방식 감지 실패:",o),s("unavailable")}}const za={detectAssetUrlMode:z5,checkAssetUrlModeDrift:F5,setTheme:y5,initTheme:b5,scrollToSection:fu,initMenuFromUrl:w5,initFilterVisibility:k5,saveFilterVisibility:C5,toggleFilterVisibility:S5,resetFilterVisibility:_5,saveMultilingualTag:E5,cancelMultilingualTag:M5,updateMultilingualTagValue:N5,setDateRange:O5,toggleSidebar:I5,initSidebar:D5,downloadAttachment:L5},jo=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic:IdentityLauncher")??{log:(...e)=>console.log("[Template:sirsoft-admin_basic:IdentityLauncher]",...e),warn:(...e)=>console.warn("[Template:sirsoft-admin_basic:IdentityLauncher]",...e),error:(...e)=>console.error("[Template:sirsoft-admin_basic:IdentityLauncher]",...e)};function V5(){const t=window.G7Core?.api?.getToken?.()??null;return t?{Authorization:`Bearer ${t}`}:{}}function H5(){try{const e=typeof window<"u"?window.localStorage?.getItem("g7_locale"):null;return e?{"Accept-Language":e}:{}}catch{return{}}}async function U5(e,t=null){const n={purpose:e.purpose};e.provider_id&&(n.provider_id=e.provider_id),t&&(n.target=t);const r=e.challenge_start_url||"/api/identity/challenges",s=await fetch(r,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...H5(),...V5()},body:JSON.stringify(n)}),o=await s.json().catch(()=>null);if(!s.ok||!o?.success){const a=o?.message??`Challenge 시작 실패 (HTTP ${s.status})`;throw new Error(a)}const i=o.data??o;return{id:i.id,expires_at:i.expires_at,render_hint:i.render_hint??e.render_hint??"text_code",public_payload:i.public_payload,redirect_url:i.redirect_url,max_attempts:typeof i.max_attempts=="number"?i.max_attempts:void 0}}async function B5(e){const t=window.G7Core,n=t?.identity;if(!t?.dispatch||!t?.state||!n)return jo.error("G7Core 또는 G7Core.identity 가 초기화되지 않아 launcher 를 실행할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};if(e.render_hint==="external_redirect"||e.redirect_url)return n.redirectExternally(e);const r=e.target&&(e.target.email||e.target.phone)?e.target:null;let s;try{s=await U5(e,r)}catch(m){jo.error("Challenge 시작 실패:",m);try{await t.dispatch({handler:"toast",params:{type:"error",message:m instanceof Error?m.message:"본인인증 시작에 실패했습니다."}})}catch{}return{status:"failed",failureCode:"CHALLENGE_START_FAILED",reason:m instanceof Error?m.message:String(m)}}if(s.redirect_url)return n.redirectExternally({...e,render_hint:"external_redirect",redirect_url:s.redirect_url});const o=s.expires_at?new Date(s.expires_at).getTime():0,i=o>0?Math.max(0,Math.floor((o-Date.now())/1e3)):0,a=typeof s.max_attempts=="number"?s.max_attempts:0;t.state.set({identityChallenge:{policy_key:e.policy_key,purpose:e.purpose,provider_id:e.provider_id??null,render_hint:s.render_hint,challenge_id:s.id,expires_at:s.expires_at,public_payload:s.public_payload??{},target:r,code:"",error:null,attempts:0,maxAttempts:a,remainingSeconds:i,resendCooldown:0}});let l=o,c=0;const d=window.setInterval(()=>{const m=l>0?Math.max(0,Math.floor((l-Date.now())/1e3)):0;c=Math.max(0,c-1),t.state.set({identityChallenge:{remainingSeconds:m,resendCooldown:c}})},1e3);let u=null;try{u=t.state.subscribe?.(m=>{const p=m?.identityChallenge?.expires_at;if(typeof p=="string"){const b=new Date(p).getTime();Number.isFinite(b)&&b!==l&&(l=b)}const y=m?.identityChallenge?.resendCooldown;typeof y=="number"&&y>c&&(c=y)})??null}catch{}const f=n.createDeferred();try{await t.dispatch({handler:"openModal",target:"identity-challenge-modal"})}catch(m){return jo.error("openModal 실패 — 풀페이지로 폴백:",m),window.clearInterval(d),u?.(),n.redirectExternally({...e,render_hint:"external_redirect",redirect_url:`/admin/identity/challenge?challenge_id=${encodeURIComponent(s.id)}&return=${encodeURIComponent(window.location.href)}`})}const g=await f;return window.clearInterval(d),u?.(),g}function j5(){const e=window.G7Core?.identity;if(!e?.setLauncher){jo.warn("G7Core.identity 가 아직 초기화되지 않아 launcher 등록을 건너뜁니다. 코어 부트스트랩 순서를 확인하세요.");return}e.setLauncher(B5),jo.log("IDV launcher registered (sirsoft-admin_basic)")}function W5(e){const t=e.icons;return Array.isArray(t)?t.filter(n=>!!n&&typeof n=="object"&&typeof n.value=="string"):[]}function G5(e,t){const n=e.label;return typeof n=="string"?n.startsWith("$t:")?t(n.slice(3)):n:e.value}function Y5(e,t,n){if(n==="")return!0;const r=n.toLowerCase();return e.value.toLowerCase().includes(r)||t.toLowerCase().includes(r)?!0:(e.keywords??[]).some(s=>s.toLowerCase().includes(r))}function X5({entry:e}){const t=e.preview;return t?.html?h.jsx("span",{className:"g7le-icon-preview",dangerouslySetInnerHTML:{__html:t.html},style:Ad}):t?.className?h.jsx("span",{className:`g7le-icon-preview ${t.className}`,style:Ad}):h.jsx("i",{className:`fas fa-${e.value}`,style:Ad,"aria-hidden":"true"})}function K5({control:e,value:t,onChange:n,t:r}){const s=W5(e),o=t==null?"":String(t),i=typeof e.iconColumns=="number"?e.iconColumns:6,a=e.iconSearchPlaceholder,l=typeof a=="string"&&a.startsWith("$t:")?r(a.slice(3)):r("layout_editor.icon_picker.search_placeholder"),[c,d]=v.useState("");if(s.length===0)return h.jsx("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:ob,children:h.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-free","data-testid":"g7le-widget-icon-picker-free",value:o,placeholder:r("layout_editor.icon_picker.free_input_hint"),onChange:g=>n(g.target.value===""?void 0:g.target.value),style:ib})});const u=g=>G5(g,r),f=s.filter(g=>Y5(g,u(g),c));return h.jsxs("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:ob,children:[h.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-search","data-testid":"g7le-widget-icon-picker-search",value:c,placeholder:l,onChange:g=>d(g.target.value),style:ib}),h.jsxs("div",{className:"g7le-icon-grid","data-testid":"g7le-widget-icon-picker-grid",style:{...q5,gridTemplateColumns:`repeat(${i}, minmax(0, 1fr))`},children:[h.jsx("button",{type:"button","data-testid":"g7le-icon-cell-none",title:r("layout_editor.icon_picker.none"),onClick:()=>n(void 0),"aria-pressed":o==="",style:o===""?ab:Od,children:h.jsx("span",{style:Z5,children:"—"})}),f.map(g=>{const m=g.value===o;return h.jsx("button",{type:"button","data-testid":`g7le-icon-cell-${g.value}`,title:u(g),"aria-pressed":m,onClick:()=>n(g.value),style:m?ab:Od,children:h.jsx(X5,{entry:g})},g.value)})]}),f.length===0&&h.jsx("div",{"data-testid":"g7le-widget-icon-picker-empty",style:Q5,children:r("layout_editor.icon_picker.no_results")})]})}const ob={display:"flex",flexDirection:"column",gap:6,width:"100%"},ib={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%"},q5={display:"grid",gap:4,maxHeight:220,overflowY:"auto",padding:2},Od={display:"flex",alignItems:"center",justifyContent:"center",minHeight:34,padding:4,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",cursor:"pointer"},ab={...Od,border:"2px solid #2563eb",background:"#eff6ff"},Ad={display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:16,lineHeight:1},Z5={fontSize:10,color:"#475569",fontFamily:"monospace",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"100%"},Q5={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"4px 2px"},lb=130,In=20;function J5(e){const t=e?.arrayProp;return typeof t=="string"&&t?t:"tabs"}function R5(e){let t=0;for(const n of e){const r=typeof n.id=="number"?n.id:Number(n.id);Number.isFinite(r)&&r>t&&(t=r)}return t+1}function eA({node:e,params:t,cellBoxes:n,t:r,onPatchNode:s}){const o=J5(t),i=v.useMemo(()=>{const g=(e.props??{})[o];return typeof g=="string"||!Array.isArray(g)?null:g},[e,o]),a=v.useMemo(()=>{const g=new Map,m=`props.${o}.`;for(const p of n??[]){if(!p.path.startsWith(m))continue;const y=Number(p.path.slice(m.length));Number.isInteger(y)&&g.set(y,{top:p.top,left:p.left,width:p.width,height:p.height})}return g},[n,o]),l=v.useCallback(g=>{const m=e.props??{};s({...e,props:{...m,[o]:g}})},[e,o,s]),c=v.useCallback(g=>{if(!i)return;const p={id:R5(i),label:r("editor.tabnav_inplace.new_tab")},y=i.slice();y.splice(g,0,p),l(y)},[i,l,r]),d=v.useCallback(g=>{if(!i)return;const m=i.slice();m.splice(g,1),l(m)},[i,l]),u=v.useCallback((g,m)=>{if(!i)return;const p=g+m;if(p<0||p>=i.length)return;const y=i.slice(),[b]=y.splice(g,1);y.splice(p,0,b),l(y)},[i,l]);if(!i||a.size===0)return null;const f={position:"absolute",width:In,height:In,display:"flex",alignItems:"center",justifyContent:"center",fontSize:12,lineHeight:1,border:"1px solid rgba(0,0,0,0.15)",borderRadius:4,background:"#ffffff",color:"#1f2937",cursor:"pointer",boxShadow:"0 1px 2px rgba(0,0,0,0.18)",pointerEvents:"auto",padding:0,zIndex:lb};return h.jsx("div",{className:"g7le-tabnav-inplace","data-testid":"g7le-tabnav-inplace",style:{position:"absolute",inset:0,pointerEvents:"none",zIndex:lb},children:i.map((g,m)=>{const p=a.get(m);if(!p)return null;const y=p.left+p.width;return h.jsxs("div",{"data-testid":`g7le-tabnav-aff-${m}`,children:[h.jsx("button",{type:"button","data-testid":`g7le-tabnav-remove-${m}`,title:r("editor.tabnav_inplace.remove"),"aria-label":r("editor.tabnav_inplace.remove"),style:{...f,left:y-In/2,top:p.top-In/2,color:"#b91c1c"},onClick:b=>{b.preventDefault(),b.stopPropagation(),d(m)},children:"✕"}),m>0&&h.jsx("button",{type:"button","data-testid":`g7le-tabnav-move-left-${m}`,title:r("editor.tabnav_inplace.move_left"),"aria-label":r("editor.tabnav_inplace.move_left"),style:{...f,left:p.left,top:p.top+p.height-In/2},onClick:b=>{b.preventDefault(),b.stopPropagation(),u(m,-1)},children:"◀"}),m<i.length-1&&h.jsx("button",{type:"button","data-testid":`g7le-tabnav-move-right-${m}`,title:r("editor.tabnav_inplace.move_right"),"aria-label":r("editor.tabnav_inplace.move_right"),style:{...f,left:y-In,top:p.top+p.height-In/2},onClick:b=>{b.preventDefault(),b.stopPropagation(),u(m,1)},children:"▶"}),m===0&&h.jsx("button",{type:"button","data-testid":"g7le-tabnav-add-0",title:r("editor.tabnav_inplace.add"),"aria-label":r("editor.tabnav_inplace.add"),style:{...f,left:p.left-In,top:p.top+p.height/2-In/2,color:"#15803d"},onClick:b=>{b.preventDefault(),b.stopPropagation(),c(0)},children:"+"}),h.jsx("button",{type:"button","data-testid":`g7le-tabnav-add-${m+1}`,title:r("editor.tabnav_inplace.add"),"aria-label":r("editor.tabnav_inplace.add"),style:{...f,left:y-In/2,top:p.top+p.height/2-In/2,color:"#15803d"},onClick:b=>{b.preventDefault(),b.stopPropagation(),c(m+1)},children:"+"})]},`tab-aff-${g.id}-${m}`)})})}const Dd=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic:EditorWidgets")??{log:(...e)=>console.log("[Template:sirsoft-admin_basic:EditorWidgets]",...e),warn:(...e)=>console.warn("[Template:sirsoft-admin_basic:EditorWidgets]",...e)};function tA(){if(typeof window>"u")return;const e=window.G7Core?.layoutEditor;if(!e?.registerWidget){Dd.warn("G7Core.layoutEditor 미노출 — icon-picker 위젯 등록 보류");return}e.registerWidget("icon-picker",K5),Dd.log("편집기 위젯 'icon-picker' 등록됨(Font Awesome 검색 그리드)"),e.registerCanvasOverlay&&(e.registerCanvasOverlay("tabnav",eA),Dd.log("편집기 캔버스 오버레이 'tabnav' 등록됨(TabNavigation 탭 인플레이스)"))}const Id=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic")??{log:(...e)=>console.log("[Template:sirsoft-admin_basic]",...e),warn:(...e)=>console.warn("[Template:sirsoft-admin_basic]",...e),error:(...e)=>console.error("[Template:sirsoft-admin_basic]",...e)};typeof window<"u"&&(window.G7TemplateHandlers=za);function cb(){if(typeof window<"u"){let e=0;const t=50,n=()=>{const r=window.G7Core?.getActionDispatcher?.();r?(Object.entries(za).forEach(([s,o])=>{r.registerHandler(s,o)}),Id.log(`${Object.keys(za).length} custom handler(s) registered:`,Object.keys(za)),j5(),nb()):(e++,e<=t?(Id.warn(`ActionDispatcher not found, retrying... (${e}/${t})`),setTimeout(n,100)):Id.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};document.readyState==="complete"?n():window.addEventListener("load",n)}}return tA(),cb(),R.A=Yr,R.Accordion=uc,R.ActionMenu=Ls,R.AdminFooter=Yy,R.AdminHeader=By,R.AdminSidebar=Uy,R.Alert=wu,R.AlertDialog=Dy,R.Badge=Wd,R.BarChart=s5,R.Breadcrumb=bu,R.Button=ae,R.Card=Mb,R.CardGrid=py,R.CategoryTree=l_,R.Checkbox=En,R.ChipCheckbox=KE,R.Code=Cb,R.CodeEditor=lx,R.ColumnSelector=px,R.ConfirmDialog=pu,R.Container=i5,R.DataGrid=dy,R.Dialog=gu,R.Div=I,R.DonutChart=o5,R.Dropdown=by,R.DropdownButton=qE,R.DropdownMenuItem=ZE,R.DynamicFieldList=iM,R.EmptyState=zy,R.ExtensionBadge=gh,R.FileInput=$d,R.FileUploader=zp,R.FilterGroup=wy,R.FilterVisibilitySelector=BE,R.Flex=l5,R.Form=Ga,R.FormField=uM,R.Fragment=_b,R.Grid=a5,R.H1=Ba,R.H2=ar,R.H3=Gr,R.H4=Fd,R.Hr=Eb,R.HtmlContent=ol,R.HtmlEditor=zE,R.I=bt,R.Icon=oe,R.IconButton=Ty,R.IconName=de,R.IconSelect=n_,R.ImageGallery=Qi,R.Img=_n,R.Input=St,R.Label=ln,R.LanguageSelector=Gy,R.LayoutEditorHeader=Qy,R.LayoutFileList=Jy,R.LayoutHistoryPanel=dx,R.LayoutWarnings=UE,R.Li=Wa,R.LoadingSpinner=Hy,R.LoginForm=ux,R.Modal=Qo,R.MultilingualInput=Za,R.MultilingualLocaleContext=jc,R.MultilingualTabPanel=dM,R.MultilingualTagInput=dC,R.Nav=Yo,R.NotificationCenter=jy,R.Ol=mb,R.Optgroup=xb,R.Option=vb,R.P=vt,R.PageHeader=Xy,R.PageLoading=vx,R.PageTransitionIndicator=bx,R.Pagination=Xo,R.PermissionTree=i_,R.Pre=Sb,R.ProductCard=yy,R.RadioGroup=PE,R.RichSelect=YE,R.SearchBar=xy,R.SearchableDropdown=uC,R.Section=zd,R.SectionHeader=qy,R.SectionLayout=c5,R.Select=Xr,R.SlotContainer=GE,R.SortableMenuItem=rg,R.SortableMenuList=e_,R.Span=Q,R.StatCard=vy,R.StatusBadge=Ko,R.Svg=Zn,R.TabNavigation=ky,R.TabNavigationScroll=My,R.Table=Ya,R.TagInput=aC,R.TagSelect=fC,R.Tbody=Ka,R.Td=en,R.TemplateCard=Zy,R.Textarea=ir,R.Tfoot=qa,R.Th=Kr,R.Thead=Xa,R.ThemeToggle=hx,R.ThreeColumnLayout=d5,R.Toast=$y,R.Toggle=Qa,R.Tr=lr,R.Ul=ja,R.UnifiedIcon=kb,R.UserProfile=Wy,R.VersionList=Fu,R.configureJSONLanguageService=h5,R.defaultJSONSchemas=qm,R.executeImageDownload=zc,R.initTemplate=cb,R.jsonEditorDefaults=u5,R.monacoInitOptions=f5,R.templateMetadata=g5,R.useCurrentMultilingualLocale=Gp,R.useImageGallery=Sp,Object.defineProperty(R,Symbol.toStringTag,{value:"Module"}),R})({},ReactJSXRuntime,React,ReactDOM); -//# sourceMappingURL=components.iife.js.map + ${C?"bg-blue-50 dark:bg-blue-900/20":""} + ${w.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"} + `,role:"option","aria-selected":C,"data-item-value":w.value,"data-item-index":N,"data-item-selected":C,children:c?.item?Dp(c.item,{item:w,index:N,isSelected:C},`item-${N}`):a||S(w,C)},w.value)})})})]})};function EE(t){const e=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(t);return e?{r:parseInt(e[1],16),g:parseInt(e[2],16),b:parseInt(e[3],16)}:null}const ME=({value:t,checked:e=!1,icon:n,label:r,variant:s="blue",customColor:o,className:i="",style:a,onChange:l,id:c,editorAttrs:d})=>{const[u,f]=v.useState(!1),g="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border cursor-pointer transition-all";if(o){const b=EE(o),_=document.documentElement.classList.contains("dark");let x;return e?x={backgroundColor:o,color:"#ffffff",borderColor:o,opacity:u?.85:1}:x=b?{backgroundColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${u?_?.25:.12:_?.15:.06})`,color:o,borderColor:`rgba(${b.r}, ${b.g}, ${b.b}, ${_?.3:.2})`}:{},h.jsxs(on,{className:`${g} ${i}`,style:{...x,...a},onMouseEnter:()=>f(!0),onMouseLeave:()=>f(!1),id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:E=>{l?.(E)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})}const y=e?{green:"bg-green-600 dark:bg-green-600 text-white dark:text-white border-green-700 dark:border-green-500 hover:bg-green-700 dark:hover:bg-green-500",orange:"bg-orange-600 dark:bg-orange-600 text-white dark:text-white border-orange-700 dark:border-orange-500 hover:bg-orange-700 dark:hover:bg-orange-500",yellow:"bg-yellow-600 dark:bg-yellow-600 text-white dark:text-white border-yellow-700 dark:border-yellow-500 hover:bg-yellow-700 dark:hover:bg-yellow-500",gray:"bg-gray-600 dark:bg-gray-500 text-white dark:text-white border-gray-700 dark:border-gray-400 hover:bg-gray-700 dark:hover:bg-gray-400",blue:"bg-blue-600 dark:bg-blue-600 text-white dark:text-white border-blue-700 dark:border-blue-500 hover:bg-blue-700 dark:hover:bg-blue-500"}[s]:{green:"bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30",orange:"bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800 hover:bg-orange-100 dark:hover:bg-orange-900/30",yellow:"bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30",gray:"bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-600",blue:"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30"}[s];return h.jsxs(on,{className:`${g} ${y} ${i}`,style:a,id:c,...d,children:[h.jsx(wt,{type:"checkbox",value:t,checked:e,onChange:b=>{l?.(b)},className:"hidden"}),n&&h.jsx(re,{name:n,className:"w-4 h-4"}),h.jsx(I,{children:r})]})},NE=({label:t,icon:e,iconPosition:n="left",position:r="right",className:s="",style:o,children:i})=>{const[a,l]=v.useState(!1),[c,d]=v.useState({top:0,left:0}),u=v.useRef(null),f=v.useRef(null),g=v.useCallback(()=>{if(!u.current)return;const p=u.current.getBoundingClientRect(),y=224;let b=r==="right"?p.right-y:p.left;const _=p.bottom+8;b<8&&(b=8),b+y>window.innerWidth-8&&(b=window.innerWidth-y-8),d({top:_,left:b})},[r]);v.useEffect(()=>{a&&g()},[a,g]),v.useEffect(()=>{if(!a)return;const p=()=>{l(!1)};return window.addEventListener("scroll",p,!0),window.addEventListener("resize",p),()=>{window.removeEventListener("scroll",p,!0),window.removeEventListener("resize",p)}},[a]),v.useEffect(()=>{const p=y=>{const b=y.target,_=u.current?.contains(b),x=f.current?.contains(b);!_&&!x&&l(!1)};return a&&document.addEventListener("mousedown",p),()=>{document.removeEventListener("mousedown",p)}},[a]);const m=a?Dn.createPortal(h.jsx("div",{ref:f,className:"fixed z-[9999] w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg overflow-hidden",style:{top:c.top,left:c.left},onClick:()=>l(!1),children:i}),document.body):null;return h.jsxs(I,{className:`relative inline-block ${s}`,style:o,children:[h.jsxs(oe,{ref:u,onClick:()=>l(!a),className:"flex items-center gap-2",children:[n==="left"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"}),t&&h.jsx(Q,{className:"text-sm font-medium",children:t}),n==="right"&&e&&h.jsx(re,{name:e,className:"w-4 h-4"})]}),m]})},OE=({label:t,icon:e,variant:n="default",disabled:r=!1,divider:s=!1,className:o="",style:i,onClick:a})=>{if(s)return h.jsx(I,{className:"h-px bg-gray-200 dark:bg-gray-700 my-1",style:i});const l=n==="danger"?"text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20":"text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700",c=r?"opacity-50 cursor-not-allowed":"cursor-pointer",d=()=>{!r&&a&&a()};return h.jsxs(I,{onClick:d,className:`flex items-center gap-3 px-4 py-3 transition-colors ${l} ${c} ${o}`,style:i,children:[e&&h.jsx(re,{name:e,className:"w-4 h-4"}),t&&h.jsx(Q,{className:"text-sm font-medium",children:t})]})},TE=()=>window.G7Core,Fc=v.createContext(null),Ip=()=>{const t=v.useContext(Fc);if(!t){const e=TE(),n=e?.locale?.current?.()||"ko",r=e?.locale?.supported?.()||["ko","en"];return{activeLocale:n,getActiveLocale:()=>e?.locale?.current?.()||"ko",supportedLocales:r,isInsideMultilingualTabPanel:!1}}return{...t,isInsideMultilingualTabPanel:!0}},AE=()=>window.G7Core,DE=({value:t,onChange:e,placeholder:n,readOnly:r,disabled:s,hasError:o})=>{const{activeLocale:i,getActiveLocale:a}=Ip(),l=typeof t=="string"?{[i]:t}:t||{},c=v.useCallback(u=>{const f=a();e({...l,[f]:u.target.value})},[l,a,e]),d=o?"border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500":"border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500";return h.jsx(wt,{type:"text",value:l[i]||"",onChange:c,placeholder:n,readOnly:r,disabled:s,className:`w-full px-3 py-2 text-sm border ${d} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2`})},IE=(t,e)=>{if(!e)return!1;const n=e.startsWith("!"),r=n?e.slice(1):e,s=!!t[r];return n?!s:s},LE=({column:t,item:e,index:n,onValueChange:r,readOnly:s,error:o})=>{const i=e[t.key],a=s||t.readOnly||IE(e,t.readOnlyCondition)||t.disabled,l=!!o,c=l?"border-red-500 dark:border-red-500 focus:ring-red-500 focus:border-red-500":"border-gray-300 dark:border-gray-600 focus:ring-blue-500 focus:border-blue-500",d=v.useCallback(f=>{r(t.key,f)},[t.key,r]),u=f=>h.jsxs(I,{className:"w-full",children:[f,l&&h.jsx(Q,{className:"text-xs text-red-500 dark:text-red-400 mt-1 block",children:o})]});switch(t.type){case"input":return u(h.jsx(wt,{type:"text",value:i||"",onChange:f=>d(f.target.value),placeholder:t.placeholder,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${t.className||""}`}));case"number":return u(h.jsx(wt,{type:"number",value:i??"",onChange:f=>d(f.target.value?Number(f.target.value):null),placeholder:t.placeholder,min:t.min,max:t.max,step:t.step,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${t.className||""}`}));case"select":return u(h.jsxs(Vr,{value:i??"",onChange:f=>d(f.target.value),disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 ${t.className||""}`,children:[h.jsx("option",{value:"",children:"선택하세요"}),t.options?.map(f=>h.jsx("option",{value:f.value,disabled:f.disabled,children:f.label},f.value))]}));case"textarea":return u(h.jsx(Rn,{value:i||"",onChange:f=>d(f.target.value),placeholder:t.placeholder,readOnly:a,disabled:a,className:`w-full px-3 py-2 text-sm border ${c} rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 resize-none ${t.className||""}`,rows:2}));case"multilingual":return u(h.jsx(DE,{value:i,onChange:f=>d(f),placeholder:t.placeholder,readOnly:a,disabled:a,hasError:l}));case"custom":return t.render?h.jsx(h.Fragment,{children:t.render(e,n,d)}):h.jsx(Q,{className:"text-gray-500 dark:text-gray-400",children:"-"});default:return h.jsx(Q,{className:"text-gray-900 dark:text-white text-sm",children:String(i??"")})}},PE=(t,e,n)=>{if(!t)return;if(n==="multilingual"){const o=AE()?.locale?.current?.()||"ko",i=`${e}.${o}`;return t[i]?.[0]}return t[e]?.[0]},$E=({item:t,index:e,id:n,columns:r,enableDrag:s=!0,showIndex:o=!0,canRemove:i=!0,rowActions:a,readOnly:l=!1,onValueChange:c,onRemove:d,className:u="",level:f=0,errors:g})=>{const{attributes:m,listeners:p,setNodeRef:y,transform:b,transition:_,isDragging:x}=Ii({id:n,disabled:!s||l}),E={transform:Fn.Transform.toString(b),transition:_,opacity:x?.5:1},S=v.useCallback((C,k)=>{c?.(C,k)},[c]),w=v.useCallback(C=>{C.onClick?.(t,e)},[t,e]),N=f>0?{paddingLeft:`${f*24}px`}:{};return h.jsxs(I,{ref:y,style:E,className:`flex items-center gap-2 py-2 px-2 border-b border-gray-100 dark:border-gray-700 last:border-b-0 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors ${x?"bg-blue-50 dark:bg-blue-900/20 shadow-lg rounded-lg z-10":""} ${u}`,children:[s&&!l&&h.jsx(I,{...m,...p,className:"flex-shrink-0 w-6 h-6 flex items-center justify-center cursor-grab active:cursor-grabbing text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400",style:N,children:h.jsx(re,{name:"grip-vertical",className:"w-4 h-4"})}),o&&h.jsx(I,{className:"flex-shrink-0 w-8 text-center text-sm text-gray-500 dark:text-gray-400 font-medium",children:e+1}),r.map(C=>{const k=PE(g,C.key,C.type);return h.jsx(I,{className:`flex-1 min-w-0 ${C.width||""}`,style:C.width?{flex:"none",width:C.width}:{},children:h.jsx(LE,{column:C,item:t,index:e,onValueChange:S,readOnly:l,error:k})},C.key)}),a&&a.length>0&&!l&&h.jsx(I,{className:"flex-shrink-0 flex items-center gap-1",children:a.map(C=>{const k=typeof C.disabled=="function"?C.disabled(t,e):C.disabled;return h.jsx(oe,{type:"button",onClick:()=>w(C),disabled:k,className:`p-1.5 rounded transition-colors ${C.danger?"text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 hover:text-red-600":"text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300"} disabled:opacity-50 disabled:cursor-not-allowed`,title:C.label,children:C.icon?h.jsx(re,{name:C.icon,className:"w-4 h-4"}):h.jsx(Q,{className:"text-xs",children:C.label})},C.key)})}),i&&!a&&!l&&h.jsx(I,{className:"flex-shrink-0",children:h.jsx(oe,{type:"button",onClick:d,className:"p-1.5 text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300 rounded transition-colors",title:"삭제",children:h.jsx(re,{name:"minus",className:"w-4 h-4"})})})]})},Lp=()=>window.G7Core,Vc=()=>`item_${Date.now()}_${Math.random().toString(36).substring(2,9)}`,FE=t=>{const e={_id:Vc()};return t.forEach(n=>{if(n.type==="multilingual"){const s=Lp()?.locale?.all?.()||["ko","en"],o={};s.forEach(i=>{o[i]=""}),e[n.key]=o}else n.type==="number"?e[n.key]=null:(n.type,e[n.key]="")}),e},VE=(t,e)=>{if(!t)return{};const n=`fields.${e}.`,r={};return Object.keys(t).forEach(s=>{if(s.startsWith(n)){const o=s.substring(n.length);r[o]=t[s]}}),r},zE=({items:t=[],columns:e,onChange:n,onAddItem:r,onRemoveItem:s,onReorder:o,addLabel:i="항목 추가",enableDrag:a=!0,showIndex:l=!0,minItems:c=0,maxItems:d,rowActions:u,emptyMessage:f="항목이 없습니다.",className:g="",headerClassName:m="",rowClassName:p="",name:y,readOnly:b=!1,createDefaultItem:_,childrenKey:x,itemIdKey:E="_id",errors:S,id:w,editorAttrs:N})=>{const C=v.useRef(null),k=v.useRef([]),M=v.useRef(null),T=v.useMemo(()=>{let j=!1;for(const U of t)if(U[E]==null){j=!0;break}return j?t.map(U=>U[E]!=null?U:{...U,[E]:Vc()}):t},[t,E]);k.current=T,v.useEffect(()=>{M.current=null},[T]);const L=v.useCallback(j=>String(j[E]??""),[E]);v.useEffect(()=>{if(y){const j=Lp();j?.form&&(C.current=j.form)}},[y]);const P=Wl(es(Zs,{activationConstraint:{distance:8}}),es(qs,{coordinateGetter:Li})),F=v.useMemo(()=>T.map(j=>L(j)),[T,L]),D=v.useCallback(j=>{const{active:U,over:B}=j;if(B&&U.id!==B.id){const G=M.current??k.current,z=G.map(Z=>String(Z[E]??"")),X=z.indexOf(String(U.id)),Y=z.indexOf(String(B.id));if(X!==-1&&Y!==-1){const Z=ns([...G],X,Y);M.current=Z,n?.(Z),o?.(Z)}}},[n,o,E]),A=v.useCallback((j,U,B)=>{const z=[...M.current??k.current];z[j]={...z[j],[U]:B},M.current=z,n?.(z)},[n]),O=v.useCallback(()=>{const j=M.current??k.current;if(d&&j.length>=d)return;if(r){r();return}const U=_?_():FE(e);U[E]||(U[E]=Vc());const B=[...j,U];M.current=B,n?.(B)},[d,r,_,e,E,n]),V=v.useCallback(j=>{const U=M.current??k.current;if(U.length<=c)return;const B=U[j];if(s){s(j,B);return}const G=U.filter((z,X)=>X!==j);M.current=G,n?.(G)},[c,s,n]),H=T.length>c,W=!d||T.length<d;return h.jsxs(I,{className:`dynamic-field-list ${g}`,id:w,...N,children:[h.jsxs(I,{className:`flex items-center gap-2 py-2 px-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-t-lg ${m}`,children:[a&&!b&&h.jsx(I,{className:"flex-shrink-0 w-6"}),l&&h.jsx(I,{className:"flex-shrink-0 w-8 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase",children:"#"}),e.map(j=>h.jsxs(I,{className:`flex-1 min-w-0 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase ${j.width||""}`,style:j.width?{flex:"none",width:j.width}:{},children:[j.label,j.required&&h.jsx(Q,{className:"text-red-500 dark:text-red-400 ml-0.5",children:"*"})]},j.key)),(u||!b&&H)&&h.jsx(I,{className:"flex-shrink-0 w-8"})]}),T.length>0?h.jsx(tc,{sensors:P,collisionDetection:Gl,onDragEnd:D,children:h.jsx(Di,{items:F,strategy:nc,children:h.jsx(I,{className:"border-x border-gray-200 dark:border-gray-700",children:T.map((j,U)=>{const B=L(j),G=VE(S,U);return h.jsx($E,{id:B,item:j,index:U,columns:e,enableDrag:a,showIndex:l,canRemove:H,rowActions:u,readOnly:b,onValueChange:(z,X)=>A(U,z,X),onRemove:()=>V(U),className:p,errors:G},B)})})})}):h.jsx(I,{className:"border-x border-gray-200 dark:border-gray-700 py-8 text-center text-gray-500 dark:text-gray-400 text-sm",children:f}),!b&&W&&h.jsx(I,{className:"relative border border-gray-200 dark:border-gray-700 border-t-0 rounded-b-lg",children:h.jsx(I,{className:"flex items-center justify-center py-2",children:h.jsxs(oe,{type:"button",onClick:O,className:"flex items-center gap-1.5 px-4 py-1.5 text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors",children:[h.jsx(re,{name:"plus",className:"w-4 h-4"}),h.jsx(Q,{children:i})]})})}),(b||!W)&&T.length>0&&h.jsx(I,{className:"border-x border-b border-gray-200 dark:border-gray-700 rounded-b-lg h-1"})]})},Pp=()=>window.G7Core,HE=()=>Pp()?.locale?.supported?.()??["ko","en"],UE=()=>Pp()?.locale?.current?.()??"ko",BE={ko:"한국어",en:"English",ja:"日本語",zh:"中文"},jE=({children:t,className:e="",style:n,variant:r="default",defaultLocale:s,onLocaleChange:o,locales:i})=>{const a=v.useMemo(()=>i&&i.length>0?i:HE(),[i]),[l,c]=v.useState(s||UE());v.useEffect(()=>{if(a.length>0&&!a.includes(l)){const p=a[0];c(p),o?.(p)}},[a,l,o]);const d=v.useRef(l);d.current=l;const u=v.useCallback(()=>d.current,[]),f=v.useCallback(p=>{c(p),o?.(p)},[o]),g=v.useMemo(()=>({activeLocale:l,getActiveLocale:u,supportedLocales:a}),[l,u,a]),m=p=>{const y=p===l,b="px-4 py-2 text-sm font-medium transition-all";return r==="pills"?y?`${b} bg-blue-600 text-white rounded-lg`:`${b} text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg`:r==="underline"?y?`${b} text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400`:`${b} text-gray-600 dark:text-gray-400 border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-500`:y?`${b} text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border-b-2 border-blue-600`:`${b} text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 border-b-2 border-transparent`};return h.jsxs(I,{className:e,style:n,children:[h.jsx(Po,{className:"flex gap-1 border-b border-gray-200 dark:border-gray-700 mb-4",children:a.map(p=>h.jsx(oe,{type:"button",onClick:()=>f(p),className:m(p),children:h.jsx(Q,{children:BE[p]||p})},p))}),h.jsx(Fc.Provider,{value:g,children:t})]})},WE=({label:t,required:e=!1,error:n,helperText:r,className:s="",labelClassName:o="",children:i,horizontal:a=!1,labelWidth:l="w-1/3",id:c,editorAttrs:d})=>{const u=a?`flex items-start gap-4 ${s}`:`space-y-1 ${s}`,f=a?`${l} flex-shrink-0 pt-2`:"",g=a?"flex-1":"";return h.jsxs(I,{className:u,id:c,...d,children:[t&&h.jsx(I,{className:f,children:h.jsxs(on,{className:`block text-sm font-medium text-gray-700 dark:text-gray-300 ${o}`,children:[t,e&&h.jsx("span",{className:"ml-1 text-red-500 dark:text-red-400",children:"*"})]})}),h.jsxs(I,{className:`${g} ${n?"form-field-error":""}`.trim(),children:[i,r&&!n&&h.jsx(I,{className:"mt-1 text-xs text-gray-500 dark:text-gray-400",children:r}),n&&h.jsx(I,{className:"mt-1 text-xs text-red-600 dark:text-red-400",children:n})]})]})};/*! + * @kurkle/color v0.3.4 + * https://github.com/kurkle/color#readme + * (c) 2024 Jukka Kurkela + * Released under the MIT License + */function uo(t){return t+.5|0}const ar=(t,e,n)=>Math.max(Math.min(t,n),e);function fo(t){return ar(uo(t*2.55),0,255)}function lr(t){return ar(uo(t*255),0,255)}function Xn(t){return ar(uo(t/2.55)/100,0,1)}function $p(t){return ar(uo(t*100),0,100)}const mn={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},zc=[..."0123456789ABCDEF"],GE=t=>zc[t&15],YE=t=>zc[(t&240)>>4]+zc[t&15],Qi=t=>(t&240)>>4===(t&15),XE=t=>Qi(t.r)&&Qi(t.g)&&Qi(t.b)&&Qi(t.a);function KE(t){var e=t.length,n;return t[0]==="#"&&(e===4||e===5?n={r:255&mn[t[1]]*17,g:255&mn[t[2]]*17,b:255&mn[t[3]]*17,a:e===5?mn[t[4]]*17:255}:(e===7||e===9)&&(n={r:mn[t[1]]<<4|mn[t[2]],g:mn[t[3]]<<4|mn[t[4]],b:mn[t[5]]<<4|mn[t[6]],a:e===9?mn[t[7]]<<4|mn[t[8]]:255})),n}const qE=(t,e)=>t<255?e(t):"";function ZE(t){var e=XE(t)?GE:YE;return t?"#"+e(t.r)+e(t.g)+e(t.b)+qE(t.a,e):void 0}const QE=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Fp(t,e,n){const r=e*Math.min(n,1-n),s=(o,i=(o+t/30)%12)=>n-r*Math.max(Math.min(i-3,9-i,1),-1);return[s(0),s(8),s(4)]}function JE(t,e,n){const r=(s,o=(s+t/60)%6)=>n-n*e*Math.max(Math.min(o,4-o,1),0);return[r(5),r(3),r(1)]}function RE(t,e,n){const r=Fp(t,1,.5);let s;for(e+n>1&&(s=1/(e+n),e*=s,n*=s),s=0;s<3;s++)r[s]*=1-e-n,r[s]+=e;return r}function eM(t,e,n,r,s){return t===s?(e-n)/r+(e<n?6:0):e===s?(n-t)/r+2:(t-e)/r+4}function Hc(t){const n=t.r/255,r=t.g/255,s=t.b/255,o=Math.max(n,r,s),i=Math.min(n,r,s),a=(o+i)/2;let l,c,d;return o!==i&&(d=o-i,c=a>.5?d/(2-o-i):d/(o+i),l=eM(n,r,s,d,o),l=l*60+.5),[l|0,c||0,a]}function Uc(t,e,n,r){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,n,r)).map(lr)}function Bc(t,e,n){return Uc(Fp,t,e,n)}function tM(t,e,n){return Uc(RE,t,e,n)}function nM(t,e,n){return Uc(JE,t,e,n)}function Vp(t){return(t%360+360)%360}function rM(t){const e=QE.exec(t);let n=255,r;if(!e)return;e[5]!==r&&(n=e[6]?fo(+e[5]):lr(+e[5]));const s=Vp(+e[2]),o=+e[3]/100,i=+e[4]/100;return e[1]==="hwb"?r=tM(s,o,i):e[1]==="hsv"?r=nM(s,o,i):r=Bc(s,o,i),{r:r[0],g:r[1],b:r[2],a:n}}function sM(t,e){var n=Hc(t);n[0]=Vp(n[0]+e),n=Bc(n),t.r=n[0],t.g=n[1],t.b=n[2]}function oM(t){if(!t)return;const e=Hc(t),n=e[0],r=$p(e[1]),s=$p(e[2]);return t.a<255?`hsla(${n}, ${r}%, ${s}%, ${Xn(t.a)})`:`hsl(${n}, ${r}%, ${s}%)`}const zp={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Hp={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function iM(){const t={},e=Object.keys(Hp),n=Object.keys(zp);let r,s,o,i,a;for(r=0;r<e.length;r++){for(i=a=e[r],s=0;s<n.length;s++)o=n[s],a=a.replace(o,zp[o]);o=parseInt(Hp[i],16),t[a]=[o>>16&255,o>>8&255,o&255]}return t}let Ji;function aM(t){Ji||(Ji=iM(),Ji.transparent=[0,0,0,0]);const e=Ji[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:e.length===4?e[3]:255}}const lM=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function cM(t){const e=lM.exec(t);let n=255,r,s,o;if(e){if(e[7]!==r){const i=+e[7];n=e[8]?fo(i):ar(i*255,0,255)}return r=+e[1],s=+e[3],o=+e[5],r=255&(e[2]?fo(r):ar(r,0,255)),s=255&(e[4]?fo(s):ar(s,0,255)),o=255&(e[6]?fo(o):ar(o,0,255)),{r,g:s,b:o,a:n}}}function dM(t){return t&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${Xn(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`)}const jc=t=>t<=.0031308?t*12.92:Math.pow(t,1/2.4)*1.055-.055,hs=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function uM(t,e,n){const r=hs(Xn(t.r)),s=hs(Xn(t.g)),o=hs(Xn(t.b));return{r:lr(jc(r+n*(hs(Xn(e.r))-r))),g:lr(jc(s+n*(hs(Xn(e.g))-s))),b:lr(jc(o+n*(hs(Xn(e.b))-o))),a:t.a+n*(e.a-t.a)}}function Ri(t,e,n){if(t){let r=Hc(t);r[e]=Math.max(0,Math.min(r[e]+r[e]*n,e===0?360:1)),r=Bc(r),t.r=r[0],t.g=r[1],t.b=r[2]}}function Up(t,e){return t&&Object.assign(e||{},t)}function Bp(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=lr(t[3]))):(e=Up(t,{r:0,g:0,b:0,a:1}),e.a=lr(e.a)),e}function fM(t){return t.charAt(0)==="r"?cM(t):rM(t)}class ho{constructor(e){if(e instanceof ho)return e;const n=typeof e;let r;n==="object"?r=Bp(e):n==="string"&&(r=KE(e)||aM(e)||fM(e)),this._rgb=r,this._valid=!!r}get valid(){return this._valid}get rgb(){var e=Up(this._rgb);return e&&(e.a=Xn(e.a)),e}set rgb(e){this._rgb=Bp(e)}rgbString(){return this._valid?dM(this._rgb):void 0}hexString(){return this._valid?ZE(this._rgb):void 0}hslString(){return this._valid?oM(this._rgb):void 0}mix(e,n){if(e){const r=this.rgb,s=e.rgb;let o;const i=n===o?.5:n,a=2*i-1,l=r.a-s.a,c=((a*l===-1?a:(a+l)/(1+a*l))+1)/2;o=1-c,r.r=255&c*r.r+o*s.r+.5,r.g=255&c*r.g+o*s.g+.5,r.b=255&c*r.b+o*s.b+.5,r.a=i*r.a+(1-i)*s.a,this.rgb=r}return this}interpolate(e,n){return e&&(this._rgb=uM(this._rgb,e._rgb,n)),this}clone(){return new ho(this.rgb)}alpha(e){return this._rgb.a=lr(e),this}clearer(e){const n=this._rgb;return n.a*=1-e,this}greyscale(){const e=this._rgb,n=uo(e.r*.3+e.g*.59+e.b*.11);return e.r=e.g=e.b=n,this}opaquer(e){const n=this._rgb;return n.a*=1+e,this}negate(){const e=this._rgb;return e.r=255-e.r,e.g=255-e.g,e.b=255-e.b,this}lighten(e){return Ri(this._rgb,2,e),this}darken(e){return Ri(this._rgb,2,-e),this}saturate(e){return Ri(this._rgb,1,e),this}desaturate(e){return Ri(this._rgb,1,-e),this}rotate(e){return sM(this._rgb,e),this}}/*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + */function Kn(){}const hM=(()=>{let t=0;return()=>t++})();function at(t){return t==null}function Mt(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return e.slice(0,7)==="[object"&&e.slice(-6)==="Array]"}function Xe(t){return t!==null&&Object.prototype.toString.call(t)==="[object Object]"}function bn(t){return(typeof t=="number"||t instanceof Number)&&isFinite(+t)}function zn(t,e){return bn(t)?t:e}function Ke(t,e){return typeof t>"u"?e:t}const gM=(t,e)=>typeof t=="string"&&t.endsWith("%")?parseFloat(t)/100:+t/e,jp=(t,e)=>typeof t=="string"&&t.endsWith("%")?parseFloat(t)/100*e:+t;function ut(t,e,n){if(t&&typeof t.call=="function")return t.apply(n,e)}function tt(t,e,n,r){let s,o,i;if(Mt(t))for(o=t.length,s=0;s<o;s++)e.call(n,t[s],s);else if(Xe(t))for(i=Object.keys(t),o=i.length,s=0;s<o;s++)e.call(n,t[i[s]],i[s])}function ea(t,e){let n,r,s,o;if(!t||!e||t.length!==e.length)return!1;for(n=0,r=t.length;n<r;++n)if(s=t[n],o=e[n],s.datasetIndex!==o.datasetIndex||s.index!==o.index)return!1;return!0}function ta(t){if(Mt(t))return t.map(ta);if(Xe(t)){const e=Object.create(null),n=Object.keys(t),r=n.length;let s=0;for(;s<r;++s)e[n[s]]=ta(t[n[s]]);return e}return t}function Wp(t){return["__proto__","prototype","constructor"].indexOf(t)===-1}function pM(t,e,n,r){if(!Wp(t))return;const s=e[t],o=n[t];Xe(s)&&Xe(o)?go(s,o,r):e[t]=ta(o)}function go(t,e,n){const r=Mt(e)?e:[e],s=r.length;if(!Xe(t))return t;n=n||{};const o=n.merger||pM;let i;for(let a=0;a<s;++a){if(i=r[a],!Xe(i))continue;const l=Object.keys(i);for(let c=0,d=l.length;c<d;++c)o(l[c],t,i,n)}return t}function po(t,e){return go(t,e,{merger:mM})}function mM(t,e,n){if(!Wp(t))return;const r=e[t],s=n[t];Xe(r)&&Xe(s)?po(r,s):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=ta(s))}const Gp={"":t=>t,x:t=>t.x,y:t=>t.y};function bM(t){const e=t.split("."),n=[];let r="";for(const s of e)r+=s,r.endsWith("\\")?r=r.slice(0,-1)+".":(n.push(r),r="");return n}function yM(t){const e=bM(t);return n=>{for(const r of e){if(r==="")break;n=n&&n[r]}return n}}function Er(t,e){return(Gp[e]||(Gp[e]=yM(e)))(t)}function Wc(t){return t.charAt(0).toUpperCase()+t.slice(1)}const mo=t=>typeof t<"u",cr=t=>typeof t=="function",Yp=(t,e)=>{if(t.size!==e.size)return!1;for(const n of t)if(!e.has(n))return!1;return!0};function vM(t){return t.type==="mouseup"||t.type==="click"||t.type==="contextmenu"}const lt=Math.PI,Ct=2*lt,na=Number.POSITIVE_INFINITY,xM=lt/180,Nt=lt/2,Mr=lt/4,Xp=lt*2/3,Kp=Math.log10,dr=Math.sign;function ra(t,e,n){return Math.abs(t-e)<n}function qp(t){const e=Math.round(t);t=ra(t,e,t/1e3)?e:t;const n=Math.pow(10,Math.floor(Kp(t))),r=t/n;return(r<=1?1:r<=2?2:r<=5?5:10)*n}function wM(t){const e=[],n=Math.sqrt(t);let r;for(r=1;r<n;r++)t%r===0&&(e.push(r),e.push(t/r));return n===(n|0)&&e.push(n),e.sort((s,o)=>s-o).pop(),e}function kM(t){return typeof t=="symbol"||typeof t=="object"&&t!==null&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}function sa(t){return!kM(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function CM(t,e){const n=Math.round(t);return n-e<=t&&n+e>=t}function SM(t,e,n){let r,s,o;for(r=0,s=t.length;r<s;r++)o=t[r][n],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function qn(t){return t*(lt/180)}function _M(t){return t*(180/lt)}function Zp(t){if(!bn(t))return;let e=1,n=0;for(;Math.round(t*e)/e!==t;)e*=10,n++;return n}function Qp(t,e){const n=e.x-t.x,r=e.y-t.y,s=Math.sqrt(n*n+r*r);let o=Math.atan2(r,n);return o<-.5*lt&&(o+=Ct),{angle:o,distance:s}}function EM(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function Hn(t){return(t%Ct+Ct)%Ct}function oa(t,e,n,r){const s=Hn(t),o=Hn(e),i=Hn(n),a=Hn(o-s),l=Hn(i-s),c=Hn(s-o),d=Hn(s-i);return s===o||s===i||r&&o===i||a>l&&c<d}function nn(t,e,n){return Math.max(e,Math.min(n,t))}function MM(t){return nn(t,-32768,32767)}function Nr(t,e,n,r=1e-6){return t>=Math.min(e,n)-r&&t<=Math.max(e,n)+r}function Gc(t,e,n){n=n||(i=>t[i]<e);let r=t.length-1,s=0,o;for(;r-s>1;)o=s+r>>1,n(o)?s=o:r=o;return{lo:s,hi:r}}const Yc=(t,e,n,r)=>Gc(t,n,r?s=>{const o=t[s][e];return o<n||o===n&&t[s+1][e]===n}:s=>t[s][e]<n),NM=(t,e,n)=>Gc(t,n,r=>t[r][e]>=n);function OM(t,e,n){let r=0,s=t.length;for(;r<s&&t[r]<e;)r++;for(;s>r&&t[s-1]>n;)s--;return r>0||s<t.length?t.slice(r,s):t}const Jp=["push","pop","shift","splice","unshift"];function TM(t,e){if(t._chartjs){t._chartjs.listeners.push(e);return}Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),Jp.forEach(n=>{const r="_onData"+Wc(n),s=t[n];Object.defineProperty(t,n,{configurable:!0,enumerable:!1,value(...o){const i=s.apply(this,o);return t._chartjs.listeners.forEach(a=>{typeof a[r]=="function"&&a[r](...o)}),i}})})}function Rp(t,e){const n=t._chartjs;if(!n)return;const r=n.listeners,s=r.indexOf(e);s!==-1&&r.splice(s,1),!(r.length>0)&&(Jp.forEach(o=>{delete t[o]}),delete t._chartjs)}function e0(t){const e=new Set(t);return e.size===t.length?t:Array.from(e)}const t0=(function(){return typeof window>"u"?function(t){return t()}:window.requestAnimationFrame})();function n0(t,e){let n=[],r=!1;return function(...s){n=s,r||(r=!0,t0.call(window,()=>{r=!1,t.apply(e,n)}))}}function AM(t,e){let n;return function(...r){return e?(clearTimeout(n),n=setTimeout(t,e,r)):t.apply(this,r),e}}const Xc=t=>t==="start"?"left":t==="end"?"right":"center",jt=(t,e,n)=>t==="start"?e:t==="end"?n:(e+n)/2,DM=(t,e,n,r)=>t===(r?"left":"right")?n:t==="center"?(e+n)/2:e,ia=t=>t===0||t===1,r0=(t,e,n)=>-(Math.pow(2,10*(t-=1))*Math.sin((t-e)*Ct/n)),s0=(t,e,n)=>Math.pow(2,-10*t)*Math.sin((t-e)*Ct/n)+1,bo={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>-Math.cos(t*Nt)+1,easeOutSine:t=>Math.sin(t*Nt),easeInOutSine:t=>-.5*(Math.cos(lt*t)-1),easeInExpo:t=>t===0?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>t===1?1:-Math.pow(2,-10*t)+1,easeInOutExpo:t=>ia(t)?t:t<.5?.5*Math.pow(2,10*(t*2-1)):.5*(-Math.pow(2,-10*(t*2-1))+2),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>ia(t)?t:r0(t,.075,.3),easeOutElastic:t=>ia(t)?t:s0(t,.075,.3),easeInOutElastic(t){return ia(t)?t:t<.5?.5*r0(t*2,.1125,.45):.5+.5*s0(t*2-1,.1125,.45)},easeInBack(t){return t*t*((1.70158+1)*t-1.70158)},easeOutBack(t){return(t-=1)*t*((1.70158+1)*t+1.70158)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?.5*(t*t*(((e*=1.525)+1)*t-e)):.5*((t-=2)*t*(((e*=1.525)+1)*t+e)+2)},easeInBounce:t=>1-bo.easeOutBounce(1-t),easeOutBounce(t){return t<1/2.75?7.5625*t*t:t<2/2.75?7.5625*(t-=1.5/2.75)*t+.75:t<2.5/2.75?7.5625*(t-=2.25/2.75)*t+.9375:7.5625*(t-=2.625/2.75)*t+.984375},easeInOutBounce:t=>t<.5?bo.easeInBounce(t*2)*.5:bo.easeOutBounce(t*2-1)*.5+.5};function o0(t){if(t&&typeof t=="object"){const e=t.toString();return e==="[object CanvasPattern]"||e==="[object CanvasGradient]"}return!1}function i0(t){return o0(t)?t:new ho(t)}function Kc(t){return o0(t)?t:new ho(t).saturate(.5).darken(.1).hexString()}const IM=["x","y","borderWidth","radius","tension"],LM=["color","borderColor","backgroundColor"];function PM(t){t.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),t.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:e=>e!=="onProgress"&&e!=="onComplete"&&e!=="fn"}),t.set("animations",{colors:{type:"color",properties:LM},numbers:{type:"number",properties:IM}}),t.describe("animations",{_fallback:"animation"}),t.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:e=>e|0}}}})}function $M(t){t.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}const a0=new Map;function FM(t,e){e=e||{};const n=t+JSON.stringify(e);let r=a0.get(n);return r||(r=new Intl.NumberFormat(t,e),a0.set(n,r)),r}function qc(t,e,n){return FM(e,n).format(t)}const VM={values(t){return Mt(t)?t:""+t},numeric(t,e,n){if(t===0)return"0";const r=this.chart.options.locale;let s,o=t;if(n.length>1){const c=Math.max(Math.abs(n[0].value),Math.abs(n[n.length-1].value));(c<1e-4||c>1e15)&&(s="scientific"),o=zM(t,n)}const i=Kp(Math.abs(o)),a=isNaN(i)?1:Math.max(Math.min(-1*Math.floor(i),20),0),l={notation:s,minimumFractionDigits:a,maximumFractionDigits:a};return Object.assign(l,this.options.ticks.format),qc(t,r,l)}};function zM(t,e){let n=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;return Math.abs(n)>=1&&t!==Math.floor(t)&&(n=t-Math.floor(t)),n}var l0={formatters:VM};function HM(t){t.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(e,n)=>n.lineWidth,tickColor:(e,n)=>n.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:l0.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),t.route("scale.ticks","color","","color"),t.route("scale.grid","color","","borderColor"),t.route("scale.border","color","","borderColor"),t.route("scale.title","color","","color"),t.describe("scale",{_fallback:!1,_scriptable:e=>!e.startsWith("before")&&!e.startsWith("after")&&e!=="callback"&&e!=="parser",_indexable:e=>e!=="borderDash"&&e!=="tickBorderDash"&&e!=="dash"}),t.describe("scales",{_fallback:"scale"}),t.describe("scale.ticks",{_scriptable:e=>e!=="backdropPadding"&&e!=="callback",_indexable:e=>e!=="backdropPadding"})}const Or=Object.create(null),Zc=Object.create(null);function yo(t,e){if(!e)return t;const n=e.split(".");for(let r=0,s=n.length;r<s;++r){const o=n[r];t=t[o]||(t[o]=Object.create(null))}return t}function Qc(t,e,n){return typeof e=="string"?go(yo(t,e),n):go(yo(t,""),e)}class UM{constructor(e,n){this.animation=void 0,this.backgroundColor="rgba(0,0,0,0.1)",this.borderColor="rgba(0,0,0,0.1)",this.color="#666",this.datasets={},this.devicePixelRatio=r=>r.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(r,s)=>Kc(s.backgroundColor),this.hoverBorderColor=(r,s)=>Kc(s.borderColor),this.hoverColor=(r,s)=>Kc(s.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(e),this.apply(n)}set(e,n){return Qc(this,e,n)}get(e){return yo(this,e)}describe(e,n){return Qc(Zc,e,n)}override(e,n){return Qc(Or,e,n)}route(e,n,r,s){const o=yo(this,e),i=yo(this,r),a="_"+n;Object.defineProperties(o,{[a]:{value:o[n],writable:!0},[n]:{enumerable:!0,get(){const l=this[a],c=i[s];return Xe(l)?Object.assign({},c,l):Ke(l,c)},set(l){this[a]=l}}})}apply(e){e.forEach(n=>n(this))}}var xt=new UM({_scriptable:t=>!t.startsWith("on"),_indexable:t=>t!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[PM,$M,HM]);function BM(t){return!t||at(t.size)||at(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family}function c0(t,e,n,r,s){let o=e[s];return o||(o=e[s]=t.measureText(s).width,n.push(s)),o>r&&(r=o),r}function Tr(t,e,n){const r=t.currentDevicePixelRatio,s=n!==0?Math.max(n/2,.5):0;return Math.round((e-s)*r)/r+s}function d0(t,e){!e&&!t||(e=e||t.getContext("2d"),e.save(),e.resetTransform(),e.clearRect(0,0,t.width,t.height),e.restore())}function u0(t,e,n,r){f0(t,e,n,r,null)}function f0(t,e,n,r,s){let o,i,a,l,c,d,u,f;const g=e.pointStyle,m=e.rotation,p=e.radius;let y=(m||0)*xM;if(g&&typeof g=="object"&&(o=g.toString(),o==="[object HTMLImageElement]"||o==="[object HTMLCanvasElement]")){t.save(),t.translate(n,r),t.rotate(y),t.drawImage(g,-g.width/2,-g.height/2,g.width,g.height),t.restore();return}if(!(isNaN(p)||p<=0)){switch(t.beginPath(),g){default:s?t.ellipse(n,r,s/2,p,0,0,Ct):t.arc(n,r,p,0,Ct),t.closePath();break;case"triangle":d=s?s/2:p,t.moveTo(n+Math.sin(y)*d,r-Math.cos(y)*p),y+=Xp,t.lineTo(n+Math.sin(y)*d,r-Math.cos(y)*p),y+=Xp,t.lineTo(n+Math.sin(y)*d,r-Math.cos(y)*p),t.closePath();break;case"rectRounded":c=p*.516,l=p-c,i=Math.cos(y+Mr)*l,u=Math.cos(y+Mr)*(s?s/2-c:l),a=Math.sin(y+Mr)*l,f=Math.sin(y+Mr)*(s?s/2-c:l),t.arc(n-u,r-a,c,y-lt,y-Nt),t.arc(n+f,r-i,c,y-Nt,y),t.arc(n+u,r+a,c,y,y+Nt),t.arc(n-f,r+i,c,y+Nt,y+lt),t.closePath();break;case"rect":if(!m){l=Math.SQRT1_2*p,d=s?s/2:l,t.rect(n-d,r-l,2*d,2*l);break}y+=Mr;case"rectRot":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),t.moveTo(n-u,r-a),t.lineTo(n+f,r-i),t.lineTo(n+u,r+a),t.lineTo(n-f,r+i),t.closePath();break;case"crossRot":y+=Mr;case"cross":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),t.moveTo(n-u,r-a),t.lineTo(n+u,r+a),t.moveTo(n+f,r-i),t.lineTo(n-f,r+i);break;case"star":u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),t.moveTo(n-u,r-a),t.lineTo(n+u,r+a),t.moveTo(n+f,r-i),t.lineTo(n-f,r+i),y+=Mr,u=Math.cos(y)*(s?s/2:p),i=Math.cos(y)*p,a=Math.sin(y)*p,f=Math.sin(y)*(s?s/2:p),t.moveTo(n-u,r-a),t.lineTo(n+u,r+a),t.moveTo(n+f,r-i),t.lineTo(n-f,r+i);break;case"line":i=s?s/2:Math.cos(y)*p,a=Math.sin(y)*p,t.moveTo(n-i,r-a),t.lineTo(n+i,r+a);break;case"dash":t.moveTo(n,r),t.lineTo(n+Math.cos(y)*(s?s/2:p),r+Math.sin(y)*p);break;case!1:t.closePath();break}t.fill(),e.borderWidth>0&&t.stroke()}}function h0(t,e,n){return n=n||.5,!e||t&&t.x>e.left-n&&t.x<e.right+n&&t.y>e.top-n&&t.y<e.bottom+n}function Jc(t,e){t.save(),t.beginPath(),t.rect(e.left,e.top,e.right-e.left,e.bottom-e.top),t.clip()}function Rc(t){t.restore()}function jM(t,e){e.translation&&t.translate(e.translation[0],e.translation[1]),at(e.rotation)||t.rotate(e.rotation),e.color&&(t.fillStyle=e.color),e.textAlign&&(t.textAlign=e.textAlign),e.textBaseline&&(t.textBaseline=e.textBaseline)}function WM(t,e,n,r,s){if(s.strikethrough||s.underline){const o=t.measureText(r),i=e-o.actualBoundingBoxLeft,a=e+o.actualBoundingBoxRight,l=n-o.actualBoundingBoxAscent,c=n+o.actualBoundingBoxDescent,d=s.strikethrough?(l+c)/2:c;t.strokeStyle=t.fillStyle,t.beginPath(),t.lineWidth=s.decorationWidth||2,t.moveTo(i,d),t.lineTo(a,d),t.stroke()}}function GM(t,e){const n=t.fillStyle;t.fillStyle=e.color,t.fillRect(e.left,e.top,e.width,e.height),t.fillStyle=n}function vo(t,e,n,r,s,o={}){const i=Mt(e)?e:[e],a=o.strokeWidth>0&&o.strokeColor!=="";let l,c;for(t.save(),t.font=s.string,jM(t,o),l=0;l<i.length;++l)c=i[l],o.backdrop&&GM(t,o.backdrop),a&&(o.strokeColor&&(t.strokeStyle=o.strokeColor),at(o.strokeWidth)||(t.lineWidth=o.strokeWidth),t.strokeText(c,n,r,o.maxWidth)),t.fillText(c,n,r,o.maxWidth),WM(t,n,r,c,o),r+=Number(s.lineHeight);t.restore()}function aa(t,e){const{x:n,y:r,w:s,h:o,radius:i}=e;t.arc(n+i.topLeft,r+i.topLeft,i.topLeft,1.5*lt,lt,!0),t.lineTo(n,r+o-i.bottomLeft),t.arc(n+i.bottomLeft,r+o-i.bottomLeft,i.bottomLeft,lt,Nt,!0),t.lineTo(n+s-i.bottomRight,r+o),t.arc(n+s-i.bottomRight,r+o-i.bottomRight,i.bottomRight,Nt,0,!0),t.lineTo(n+s,r+i.topRight),t.arc(n+s-i.topRight,r+i.topRight,i.topRight,0,-Nt,!0),t.lineTo(n+i.topLeft,r)}const YM=/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/,XM=/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/;function KM(t,e){const n=(""+t).match(YM);if(!n||n[1]==="normal")return e*1.2;switch(t=+n[2],n[3]){case"px":return t;case"%":t/=100;break}return e*t}const qM=t=>+t||0;function ed(t,e){const n={},r=Xe(e),s=r?Object.keys(e):e,o=Xe(t)?r?i=>Ke(t[i],t[e[i]]):i=>t[i]:()=>t;for(const i of s)n[i]=qM(o(i));return n}function g0(t){return ed(t,{top:"y",right:"x",bottom:"y",left:"x"})}function gs(t){return ed(t,["topLeft","topRight","bottomLeft","bottomRight"])}function yn(t){const e=g0(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Wt(t,e){t=t||{},e=e||xt.font;let n=Ke(t.size,e.size);typeof n=="string"&&(n=parseInt(n,10));let r=Ke(t.style,e.style);r&&!(""+r).match(XM)&&(console.warn('Invalid font style specified: "'+r+'"'),r=void 0);const s={family:Ke(t.family,e.family),lineHeight:KM(Ke(t.lineHeight,e.lineHeight),n),size:n,style:r,weight:Ke(t.weight,e.weight),string:""};return s.string=BM(s),s}function la(t,e,n,r){let s,o,i;for(s=0,o=t.length;s<o;++s)if(i=t[s],i!==void 0&&i!==void 0)return i}function ZM(t,e,n){const{min:r,max:s}=t,o=jp(e,(s-r)/2),i=(a,l)=>n&&a===0?0:a+l;return{min:i(r,-Math.abs(o)),max:i(s,o)}}function ps(t,e){return Object.assign(Object.create(t),e)}function td(t,e=[""],n,r,s=()=>t[0]){const o=n||t;typeof r>"u"&&(r=v0("_fallback",t));const i={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:o,_fallback:r,_getTarget:s,override:a=>td([a,...t],e,o,r)};return new Proxy(i,{deleteProperty(a,l){return delete a[l],delete a._keys,delete t[0][l],!0},get(a,l){return m0(a,l,()=>sN(l,e,t,a))},getOwnPropertyDescriptor(a,l){return Reflect.getOwnPropertyDescriptor(a._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(t[0])},has(a,l){return x0(a).includes(l)},ownKeys(a){return x0(a)},set(a,l,c){const d=a._storage||(a._storage=s());return a[l]=d[l]=c,delete a._keys,!0}})}function ms(t,e,n,r){const s={_cacheable:!1,_proxy:t,_context:e,_subProxy:n,_stack:new Set,_descriptors:p0(t,r),setContext:o=>ms(t,o,n,r),override:o=>ms(t.override(o),e,n,r)};return new Proxy(s,{deleteProperty(o,i){return delete o[i],delete t[i],!0},get(o,i,a){return m0(o,i,()=>JM(o,i,a))},getOwnPropertyDescriptor(o,i){return o._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i)},getPrototypeOf(){return Reflect.getPrototypeOf(t)},has(o,i){return Reflect.has(t,i)},ownKeys(){return Reflect.ownKeys(t)},set(o,i,a){return t[i]=a,delete o[i],!0}})}function p0(t,e={scriptable:!0,indexable:!0}){const{_scriptable:n=e.scriptable,_indexable:r=e.indexable,_allKeys:s=e.allKeys}=t;return{allKeys:s,scriptable:n,indexable:r,isScriptable:cr(n)?n:()=>n,isIndexable:cr(r)?r:()=>r}}const QM=(t,e)=>t?t+Wc(e):e,nd=(t,e)=>Xe(e)&&t!=="adapters"&&(Object.getPrototypeOf(e)===null||e.constructor===Object);function m0(t,e,n){if(Object.prototype.hasOwnProperty.call(t,e)||e==="constructor")return t[e];const r=n();return t[e]=r,r}function JM(t,e,n){const{_proxy:r,_context:s,_subProxy:o,_descriptors:i}=t;let a=r[e];return cr(a)&&i.isScriptable(e)&&(a=RM(e,a,t,n)),Mt(a)&&a.length&&(a=eN(e,a,t,i.isIndexable)),nd(e,a)&&(a=ms(a,s,o&&o[e],i)),a}function RM(t,e,n,r){const{_proxy:s,_context:o,_subProxy:i,_stack:a}=n;if(a.has(t))throw new Error("Recursion detected: "+Array.from(a).join("->")+"->"+t);a.add(t);let l=e(o,i||r);return a.delete(t),nd(t,l)&&(l=rd(s._scopes,s,t,l)),l}function eN(t,e,n,r){const{_proxy:s,_context:o,_subProxy:i,_descriptors:a}=n;if(typeof o.index<"u"&&r(t))return e[o.index%e.length];if(Xe(e[0])){const l=e,c=s._scopes.filter(d=>d!==l);e=[];for(const d of l){const u=rd(c,s,t,d);e.push(ms(u,o,i&&i[t],a))}}return e}function b0(t,e,n){return cr(t)?t(e,n):t}const tN=(t,e)=>t===!0?e:typeof t=="string"?Er(e,t):void 0;function nN(t,e,n,r,s){for(const o of e){const i=tN(n,o);if(i){t.add(i);const a=b0(i._fallback,n,s);if(typeof a<"u"&&a!==n&&a!==r)return a}else if(i===!1&&typeof r<"u"&&n!==r)return null}return!1}function rd(t,e,n,r){const s=e._rootScopes,o=b0(e._fallback,n,r),i=[...t,...s],a=new Set;a.add(r);let l=y0(a,i,n,o||n,r);return l===null||typeof o<"u"&&o!==n&&(l=y0(a,i,o,l,r),l===null)?!1:td(Array.from(a),[""],s,o,()=>rN(e,n,r))}function y0(t,e,n,r,s){for(;n;)n=nN(t,e,n,r,s);return n}function rN(t,e,n){const r=t._getTarget();e in r||(r[e]={});const s=r[e];return Mt(s)&&Xe(n)?n:s||{}}function sN(t,e,n,r){let s;for(const o of e)if(s=v0(QM(o,t),n),typeof s<"u")return nd(t,s)?rd(n,r,t,s):s}function v0(t,e){for(const n of e){if(!n)continue;const r=n[t];if(typeof r<"u")return r}}function x0(t){let e=t._keys;return e||(e=t._keys=oN(t._scopes)),e}function oN(t){const e=new Set;for(const n of t)for(const r of Object.keys(n).filter(s=>!s.startsWith("_")))e.add(r);return Array.from(e)}function sd(){return typeof window<"u"&&typeof document<"u"}function od(t){let e=t.parentNode;return e&&e.toString()==="[object ShadowRoot]"&&(e=e.host),e}function ca(t,e,n){let r;return typeof t=="string"?(r=parseInt(t,10),t.indexOf("%")!==-1&&(r=r/100*e.parentNode[n])):r=t,r}const da=t=>t.ownerDocument.defaultView.getComputedStyle(t,null);function iN(t,e){return da(t).getPropertyValue(e)}const aN=["top","right","bottom","left"];function Ar(t,e,n){const r={};n=n?"-"+n:"";for(let s=0;s<4;s++){const o=aN[s];r[o]=parseFloat(t[e+"-"+o+n])||0}return r.width=r.left+r.right,r.height=r.top+r.bottom,r}const lN=(t,e,n)=>(t>0||e>0)&&(!n||!n.shadowRoot);function cN(t,e){const n=t.touches,r=n&&n.length?n[0]:t,{offsetX:s,offsetY:o}=r;let i=!1,a,l;if(lN(s,o,t.target))a=s,l=o;else{const c=e.getBoundingClientRect();a=r.clientX-c.left,l=r.clientY-c.top,i=!0}return{x:a,y:l,box:i}}function Dr(t,e){if("native"in t)return t;const{canvas:n,currentDevicePixelRatio:r}=e,s=da(n),o=s.boxSizing==="border-box",i=Ar(s,"padding"),a=Ar(s,"border","width"),{x:l,y:c,box:d}=cN(t,n),u=i.left+(d&&a.left),f=i.top+(d&&a.top);let{width:g,height:m}=e;return o&&(g-=i.width+a.width,m-=i.height+a.height),{x:Math.round((l-u)/g*n.width/r),y:Math.round((c-f)/m*n.height/r)}}function dN(t,e,n){let r,s;if(e===void 0||n===void 0){const o=t&&od(t);if(!o)e=t.clientWidth,n=t.clientHeight;else{const i=o.getBoundingClientRect(),a=da(o),l=Ar(a,"border","width"),c=Ar(a,"padding");e=i.width-c.width-l.width,n=i.height-c.height-l.height,r=ca(a.maxWidth,o,"clientWidth"),s=ca(a.maxHeight,o,"clientHeight")}}return{width:e,height:n,maxWidth:r||na,maxHeight:s||na}}const ur=t=>Math.round(t*10)/10;function uN(t,e,n,r){const s=da(t),o=Ar(s,"margin"),i=ca(s.maxWidth,t,"clientWidth")||na,a=ca(s.maxHeight,t,"clientHeight")||na,l=dN(t,e,n);let{width:c,height:d}=l;if(s.boxSizing==="content-box"){const f=Ar(s,"border","width"),g=Ar(s,"padding");c-=g.width+f.width,d-=g.height+f.height}return c=Math.max(0,c-o.width),d=Math.max(0,r?c/r:d-o.height),c=ur(Math.min(c,i,l.maxWidth)),d=ur(Math.min(d,a,l.maxHeight)),c&&!d&&(d=ur(c/2)),(e!==void 0||n!==void 0)&&r&&l.height&&d>l.height&&(d=l.height,c=ur(Math.floor(d*r))),{width:c,height:d}}function w0(t,e,n){const r=e||1,s=ur(t.height*r),o=ur(t.width*r);t.height=ur(t.height),t.width=ur(t.width);const i=t.canvas;return i.style&&(n||!i.style.height&&!i.style.width)&&(i.style.height=`${t.height}px`,i.style.width=`${t.width}px`),t.currentDevicePixelRatio!==r||i.height!==s||i.width!==o?(t.currentDevicePixelRatio=r,i.height=s,i.width=o,t.ctx.setTransform(r,0,0,r,0,0),!0):!1}const fN=(function(){let t=!1;try{const e={get passive(){return t=!0,!1}};sd()&&(window.addEventListener("test",null,e),window.removeEventListener("test",null,e))}catch{}return t})();function k0(t,e){const n=iN(t,e),r=n&&n.match(/^(\d+)(\.\d+)?px$/);return r?+r[1]:void 0}const hN=function(t,e){return{x(n){return t+t+e-n},setWidth(n){e=n},textAlign(n){return n==="center"?n:n==="right"?"left":"right"},xPlus(n,r){return n-r},leftForLtr(n,r){return n-r}}},gN=function(){return{x(t){return t},setWidth(t){},textAlign(t){return t},xPlus(t,e){return t+e},leftForLtr(t,e){return t}}};function bs(t,e,n){return t?hN(e,n):gN()}function C0(t,e){let n,r;(e==="ltr"||e==="rtl")&&(n=t.canvas.style,r=[n.getPropertyValue("direction"),n.getPropertyPriority("direction")],n.setProperty("direction",e,"important"),t.prevTextDirection=r)}function S0(t,e){e!==void 0&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function ua(t,e,n){return t.options.clip?t[n]:e[n]}function pN(t,e){const{xScale:n,yScale:r}=t;return n&&r?{left:ua(n,e,"left"),right:ua(n,e,"right"),top:ua(r,e,"top"),bottom:ua(r,e,"bottom")}:e}function mN(t,e){const n=e._clip;if(n.disabled)return!1;const r=pN(e,t.chartArea);return{left:n.left===!1?0:r.left-(n.left===!0?0:n.left),right:n.right===!1?t.width:r.right+(n.right===!0?0:n.right),top:n.top===!1?0:r.top-(n.top===!0?0:n.top),bottom:n.bottom===!1?t.height:r.bottom+(n.bottom===!0?0:n.bottom)}}/*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + */class bN{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(e,n,r,s){const o=n.listeners[s],i=n.duration;o.forEach(a=>a({chart:e,initial:n.initial,numSteps:i,currentStep:Math.min(r-n.start,i)}))}_refresh(){this._request||(this._running=!0,this._request=t0.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(e=Date.now()){let n=0;this._charts.forEach((r,s)=>{if(!r.running||!r.items.length)return;const o=r.items;let i=o.length-1,a=!1,l;for(;i>=0;--i)l=o[i],l._active?(l._total>r.duration&&(r.duration=l._total),l.tick(e),a=!0):(o[i]=o[o.length-1],o.pop());a&&(s.draw(),this._notify(s,r,e,"progress")),o.length||(r.running=!1,this._notify(s,r,e,"complete"),r.initial=!1),n+=o.length}),this._lastDate=e,n===0&&(this._running=!1)}_getAnims(e){const n=this._charts;let r=n.get(e);return r||(r={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},n.set(e,r)),r}listen(e,n,r){this._getAnims(e).listeners[n].push(r)}add(e,n){!n||!n.length||this._getAnims(e).items.push(...n)}has(e){return this._getAnims(e).items.length>0}start(e){const n=this._charts.get(e);n&&(n.running=!0,n.start=Date.now(),n.duration=n.items.reduce((r,s)=>Math.max(r,s._duration),0),this._refresh())}running(e){if(!this._running)return!1;const n=this._charts.get(e);return!(!n||!n.running||!n.items.length)}stop(e){const n=this._charts.get(e);if(!n||!n.items.length)return;const r=n.items;let s=r.length-1;for(;s>=0;--s)r[s].cancel();n.items=[],this._notify(e,n,Date.now(),"complete")}remove(e){return this._charts.delete(e)}}var Zn=new bN;const _0="transparent",yN={boolean(t,e,n){return n>.5?e:t},color(t,e,n){const r=i0(t||_0),s=r.valid&&i0(e||_0);return s&&s.valid?s.mix(r,n).hexString():e},number(t,e,n){return t+(e-t)*n}};class vN{constructor(e,n,r,s){const o=n[r];s=la([e.to,s,o,e.from]);const i=la([e.from,o,s]);this._active=!0,this._fn=e.fn||yN[e.type||typeof i],this._easing=bo[e.easing]||bo.linear,this._start=Math.floor(Date.now()+(e.delay||0)),this._duration=this._total=Math.floor(e.duration),this._loop=!!e.loop,this._target=n,this._prop=r,this._from=i,this._to=s,this._promises=void 0}active(){return this._active}update(e,n,r){if(this._active){this._notify(!1);const s=this._target[this._prop],o=r-this._start,i=this._duration-o;this._start=r,this._duration=Math.floor(Math.max(i,e.duration)),this._total+=o,this._loop=!!e.loop,this._to=la([e.to,n,s,e.from]),this._from=la([e.from,s,n])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(e){const n=e-this._start,r=this._duration,s=this._prop,o=this._from,i=this._loop,a=this._to;let l;if(this._active=o!==a&&(i||n<r),!this._active){this._target[s]=a,this._notify(!0);return}if(n<0){this._target[s]=o;return}l=n/r%2,l=i&&l>1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[s]=this._fn(o,a,l)}wait(){const e=this._promises||(this._promises=[]);return new Promise((n,r)=>{e.push({res:n,rej:r})})}_notify(e){const n=e?"res":"rej",r=this._promises||[];for(let s=0;s<r.length;s++)r[s][n]()}}class E0{constructor(e,n){this._chart=e,this._properties=new Map,this.configure(n)}configure(e){if(!Xe(e))return;const n=Object.keys(xt.animation),r=this._properties;Object.getOwnPropertyNames(e).forEach(s=>{const o=e[s];if(!Xe(o))return;const i={};for(const a of n)i[a]=o[a];(Mt(o.properties)&&o.properties||[s]).forEach(a=>{(a===s||!r.has(a))&&r.set(a,i)})})}_animateOptions(e,n){const r=n.options,s=wN(e,r);if(!s)return[];const o=this._createAnimations(s,r);return r.$shared&&xN(e.options.$animations,r).then(()=>{e.options=r},()=>{}),o}_createAnimations(e,n){const r=this._properties,s=[],o=e.$animations||(e.$animations={}),i=Object.keys(n),a=Date.now();let l;for(l=i.length-1;l>=0;--l){const c=i[l];if(c.charAt(0)==="$")continue;if(c==="options"){s.push(...this._animateOptions(e,n));continue}const d=n[c];let u=o[c];const f=r.get(c);if(u)if(f&&u.active()){u.update(f,d,a);continue}else u.cancel();if(!f||!f.duration){e[c]=d;continue}o[c]=u=new vN(f,e,c,d),s.push(u)}return s}update(e,n){if(this._properties.size===0){Object.assign(e,n);return}const r=this._createAnimations(e,n);if(r.length)return Zn.add(this._chart,r),!0}}function xN(t,e){const n=[],r=Object.keys(e);for(let s=0;s<r.length;s++){const o=t[r[s]];o&&o.active()&&n.push(o.wait())}return Promise.all(n)}function wN(t,e){if(!e)return;let n=t.options;if(!n){t.options=e;return}return n.$shared&&(t.options=n=Object.assign({},n,{$shared:!1,$animations:{}})),n}function M0(t,e){const n=t&&t.options||{},r=n.reverse,s=n.min===void 0?e:0,o=n.max===void 0?e:0;return{start:r?o:s,end:r?s:o}}function kN(t,e,n){if(n===!1)return!1;const r=M0(t,n),s=M0(e,n);return{top:s.end,right:r.end,bottom:s.start,left:r.start}}function CN(t){let e,n,r,s;return Xe(t)?(e=t.top,n=t.right,r=t.bottom,s=t.left):e=n=r=s=t,{top:e,right:n,bottom:r,left:s,disabled:t===!1}}function N0(t,e){const n=[],r=t._getSortedDatasetMetas(e);let s,o;for(s=0,o=r.length;s<o;++s)n.push(r[s].index);return n}function O0(t,e,n,r={}){const s=t.keys,o=r.mode==="single";let i,a,l,c;if(e===null)return;let d=!1;for(i=0,a=s.length;i<a;++i){if(l=+s[i],l===n){if(d=!0,r.all)continue;break}c=t.values[l],bn(c)&&(o||e===0||dr(e)===dr(c))&&(e+=c)}return!d&&!r.all?0:e}function SN(t,e){const{iScale:n,vScale:r}=e,s=n.axis==="x"?"x":"y",o=r.axis==="x"?"x":"y",i=Object.keys(t),a=new Array(i.length);let l,c,d;for(l=0,c=i.length;l<c;++l)d=i[l],a[l]={[s]:d,[o]:t[d]};return a}function id(t,e){const n=t&&t.options.stacked;return n||n===void 0&&e.stack!==void 0}function _N(t,e,n){return`${t.id}.${e.id}.${n.stack||n.type}`}function EN(t){const{min:e,max:n,minDefined:r,maxDefined:s}=t.getUserBounds();return{min:r?e:Number.NEGATIVE_INFINITY,max:s?n:Number.POSITIVE_INFINITY}}function MN(t,e,n){const r=t[e]||(t[e]={});return r[n]||(r[n]={})}function T0(t,e,n,r){for(const s of e.getMatchingVisibleMetas(r).reverse()){const o=t[s.index];if(n&&o>0||!n&&o<0)return s.index}return null}function A0(t,e){const{chart:n,_cachedMeta:r}=t,s=n._stacks||(n._stacks={}),{iScale:o,vScale:i,index:a}=r,l=o.axis,c=i.axis,d=_N(o,i,r),u=e.length;let f;for(let g=0;g<u;++g){const m=e[g],{[l]:p,[c]:y}=m,b=m._stacks||(m._stacks={});f=b[c]=MN(s,d,p),f[a]=y,f._top=T0(f,i,!0,r.type),f._bottom=T0(f,i,!1,r.type);const _=f._visualValues||(f._visualValues={});_[a]=y}}function ad(t,e){const n=t.scales;return Object.keys(n).filter(r=>n[r].axis===e).shift()}function NN(t,e){return ps(t,{active:!1,dataset:void 0,datasetIndex:e,index:e,mode:"default",type:"dataset"})}function ON(t,e,n){return ps(t,{active:!1,dataIndex:e,parsed:void 0,raw:void 0,element:n,index:e,mode:"default",type:"data"})}function xo(t,e){const n=t.controller.index,r=t.vScale&&t.vScale.axis;if(r){e=e||t._parsed;for(const s of e){const o=s._stacks;if(!o||o[r]===void 0||o[r][n]===void 0)return;delete o[r][n],o[r]._visualValues!==void 0&&o[r]._visualValues[n]!==void 0&&delete o[r]._visualValues[n]}}}const ld=t=>t==="reset"||t==="none",D0=(t,e)=>e?t:Object.assign({},t),TN=(t,e,n)=>t&&!e.hidden&&e._stacked&&{keys:N0(n,!0),values:null};class ys{constructor(e,n){this.chart=e,this._ctx=e.ctx,this.index=n,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){const e=this._cachedMeta;this.configure(),this.linkScales(),e._stacked=id(e.vScale,e),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(e){this.index!==e&&xo(this._cachedMeta),this.index=e}linkScales(){const e=this.chart,n=this._cachedMeta,r=this.getDataset(),s=(u,f,g,m)=>u==="x"?f:u==="r"?m:g,o=n.xAxisID=Ke(r.xAxisID,ad(e,"x")),i=n.yAxisID=Ke(r.yAxisID,ad(e,"y")),a=n.rAxisID=Ke(r.rAxisID,ad(e,"r")),l=n.indexAxis,c=n.iAxisID=s(l,o,i,a),d=n.vAxisID=s(l,i,o,a);n.xScale=this.getScaleForId(o),n.yScale=this.getScaleForId(i),n.rScale=this.getScaleForId(a),n.iScale=this.getScaleForId(c),n.vScale=this.getScaleForId(d)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(e){return this.chart.scales[e]}_getOtherScale(e){const n=this._cachedMeta;return e===n.iScale?n.vScale:n.iScale}reset(){this._update("reset")}_destroy(){const e=this._cachedMeta;this._data&&Rp(this._data,this),e._stacked&&xo(e)}_dataCheck(){const e=this.getDataset(),n=e.data||(e.data=[]),r=this._data;if(Xe(n)){const s=this._cachedMeta;this._data=SN(n,s)}else if(r!==n){if(r){Rp(r,this);const s=this._cachedMeta;xo(s),s._parsed=[]}n&&Object.isExtensible(n)&&TM(n,this),this._syncList=[],this._data=n}}addElements(){const e=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(e.dataset=new this.datasetElementType)}buildOrUpdateElements(e){const n=this._cachedMeta,r=this.getDataset();let s=!1;this._dataCheck();const o=n._stacked;n._stacked=id(n.vScale,n),n.stack!==r.stack&&(s=!0,xo(n),n.stack=r.stack),this._resyncElements(e),(s||o!==n._stacked)&&(A0(this,n._parsed),n._stacked=id(n.vScale,n))}configure(){const e=this.chart.config,n=e.datasetScopeKeys(this._type),r=e.getOptionScopes(this.getDataset(),n,!0);this.options=e.createResolver(r,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(e,n){const{_cachedMeta:r,_data:s}=this,{iScale:o,_stacked:i}=r,a=o.axis;let l=e===0&&n===s.length?!0:r._sorted,c=e>0&&r._parsed[e-1],d,u,f;if(this._parsing===!1)r._parsed=s,r._sorted=!0,f=s;else{Mt(s[e])?f=this.parseArrayData(r,s,e,n):Xe(s[e])?f=this.parseObjectData(r,s,e,n):f=this.parsePrimitiveData(r,s,e,n);const g=()=>u[a]===null||c&&u[a]<c[a];for(d=0;d<n;++d)r._parsed[d+e]=u=f[d],l&&(g()&&(l=!1),c=u);r._sorted=l}i&&A0(this,f)}parsePrimitiveData(e,n,r,s){const{iScale:o,vScale:i}=e,a=o.axis,l=i.axis,c=o.getLabels(),d=o===i,u=new Array(s);let f,g,m;for(f=0,g=s;f<g;++f)m=f+r,u[f]={[a]:d||o.parse(c[m],m),[l]:i.parse(n[m],m)};return u}parseArrayData(e,n,r,s){const{xScale:o,yScale:i}=e,a=new Array(s);let l,c,d,u;for(l=0,c=s;l<c;++l)d=l+r,u=n[d],a[l]={x:o.parse(u[0],d),y:i.parse(u[1],d)};return a}parseObjectData(e,n,r,s){const{xScale:o,yScale:i}=e,{xAxisKey:a="x",yAxisKey:l="y"}=this._parsing,c=new Array(s);let d,u,f,g;for(d=0,u=s;d<u;++d)f=d+r,g=n[f],c[d]={x:o.parse(Er(g,a),f),y:i.parse(Er(g,l),f)};return c}getParsed(e){return this._cachedMeta._parsed[e]}getDataElement(e){return this._cachedMeta.data[e]}applyStack(e,n,r){const s=this.chart,o=this._cachedMeta,i=n[e.axis],a={keys:N0(s,!0),values:n._stacks[e.axis]._visualValues};return O0(a,i,o.index,{mode:r})}updateRangeFromParsed(e,n,r,s){const o=r[n.axis];let i=o===null?NaN:o;const a=s&&r._stacks[n.axis];s&&a&&(s.values=a,i=O0(s,o,this._cachedMeta.index)),e.min=Math.min(e.min,i),e.max=Math.max(e.max,i)}getMinMax(e,n){const r=this._cachedMeta,s=r._parsed,o=r._sorted&&e===r.iScale,i=s.length,a=this._getOtherScale(e),l=TN(n,r,this.chart),c={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY},{min:d,max:u}=EN(a);let f,g;function m(){g=s[f];const p=g[a.axis];return!bn(g[e.axis])||d>p||u<p}for(f=0;f<i&&!(!m()&&(this.updateRangeFromParsed(c,e,g,l),o));++f);if(o){for(f=i-1;f>=0;--f)if(!m()){this.updateRangeFromParsed(c,e,g,l);break}}return c}getAllParsedValues(e){const n=this._cachedMeta._parsed,r=[];let s,o,i;for(s=0,o=n.length;s<o;++s)i=n[s][e.axis],bn(i)&&r.push(i);return r}getMaxOverflow(){return!1}getLabelAndValue(e){const n=this._cachedMeta,r=n.iScale,s=n.vScale,o=this.getParsed(e);return{label:r?""+r.getLabelForValue(o[r.axis]):"",value:s?""+s.getLabelForValue(o[s.axis]):""}}_update(e){const n=this._cachedMeta;this.update(e||"default"),n._clip=CN(Ke(this.options.clip,kN(n.xScale,n.yScale,this.getMaxOverflow())))}update(e){}draw(){const e=this._ctx,n=this.chart,r=this._cachedMeta,s=r.data||[],o=n.chartArea,i=[],a=this._drawStart||0,l=this._drawCount||s.length-a,c=this.options.drawActiveElementsOnTop;let d;for(r.dataset&&r.dataset.draw(e,o,a,l),d=a;d<a+l;++d){const u=s[d];u.hidden||(u.active&&c?i.push(u):u.draw(e,o))}for(d=0;d<i.length;++d)i[d].draw(e,o)}getStyle(e,n){const r=n?"active":"default";return e===void 0&&this._cachedMeta.dataset?this.resolveDatasetElementOptions(r):this.resolveDataElementOptions(e||0,r)}getContext(e,n,r){const s=this.getDataset();let o;if(e>=0&&e<this._cachedMeta.data.length){const i=this._cachedMeta.data[e];o=i.$context||(i.$context=ON(this.getContext(),e,i)),o.parsed=this.getParsed(e),o.raw=s.data[e],o.index=o.dataIndex=e}else o=this.$context||(this.$context=NN(this.chart.getContext(),this.index)),o.dataset=s,o.index=o.datasetIndex=this.index;return o.active=!!n,o.mode=r,o}resolveDatasetElementOptions(e){return this._resolveElementOptions(this.datasetElementType.id,e)}resolveDataElementOptions(e,n){return this._resolveElementOptions(this.dataElementType.id,n,e)}_resolveElementOptions(e,n="default",r){const s=n==="active",o=this._cachedDataOpts,i=e+"-"+n,a=o[i],l=this.enableOptionSharing&&mo(r);if(a)return D0(a,l);const c=this.chart.config,d=c.datasetElementScopeKeys(this._type,e),u=s?[`${e}Hover`,"hover",e,""]:[e,""],f=c.getOptionScopes(this.getDataset(),d),g=Object.keys(xt.elements[e]),m=()=>this.getContext(r,s,n),p=c.resolveNamedOptions(f,g,m,u);return p.$shared&&(p.$shared=l,o[i]=Object.freeze(D0(p,l))),p}_resolveAnimations(e,n,r){const s=this.chart,o=this._cachedDataOpts,i=`animation-${n}`,a=o[i];if(a)return a;let l;if(s.options.animation!==!1){const d=this.chart.config,u=d.datasetAnimationScopeKeys(this._type,n),f=d.getOptionScopes(this.getDataset(),u);l=d.createResolver(f,this.getContext(e,r,n))}const c=new E0(s,l&&l.animations);return l&&l._cacheable&&(o[i]=Object.freeze(c)),c}getSharedOptions(e){if(e.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},e))}includeOptions(e,n){return!n||ld(e)||this.chart._animationsDisabled}_getSharedOptions(e,n){const r=this.resolveDataElementOptions(e,n),s=this._sharedOptions,o=this.getSharedOptions(r),i=this.includeOptions(n,o)||o!==s;return this.updateSharedOptions(o,n,r),{sharedOptions:o,includeOptions:i}}updateElement(e,n,r,s){ld(s)?Object.assign(e,r):this._resolveAnimations(n,s).update(e,r)}updateSharedOptions(e,n,r){e&&!ld(n)&&this._resolveAnimations(void 0,n).update(e,r)}_setStyle(e,n,r,s){e.active=s;const o=this.getStyle(n,s);this._resolveAnimations(n,r,s).update(e,{options:!s&&this.getSharedOptions(o)||o})}removeHoverStyle(e,n,r){this._setStyle(e,r,"active",!1)}setHoverStyle(e,n,r){this._setStyle(e,r,"active",!0)}_removeDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!1)}_setDatasetHoverStyle(){const e=this._cachedMeta.dataset;e&&this._setStyle(e,void 0,"active",!0)}_resyncElements(e){const n=this._data,r=this._cachedMeta.data;for(const[a,l,c]of this._syncList)this[a](l,c);this._syncList=[];const s=r.length,o=n.length,i=Math.min(o,s);i&&this.parse(0,i),o>s?this._insertElements(s,o-s,e):o<s&&this._removeElements(o,s-o)}_insertElements(e,n,r=!0){const s=this._cachedMeta,o=s.data,i=e+n;let a;const l=c=>{for(c.length+=n,a=c.length-1;a>=i;a--)c[a]=c[a-n]};for(l(o),a=e;a<i;++a)o[a]=new this.dataElementType;this._parsing&&l(s._parsed),this.parse(e,n),r&&this.updateElements(o,e,n,"reset")}updateElements(e,n,r,s){}_removeElements(e,n){const r=this._cachedMeta;if(this._parsing){const s=r._parsed.splice(e,n);r._stacked&&xo(r,s)}r.data.splice(e,n)}_sync(e){if(this._parsing)this._syncList.push(e);else{const[n,r,s]=e;this[n](r,s)}this.chart._dataChanges.push([this.index,...e])}_onDataPush(){const e=arguments.length;this._sync(["_insertElements",this.getDataset().data.length-e,e])}_onDataPop(){this._sync(["_removeElements",this._cachedMeta.data.length-1,1])}_onDataShift(){this._sync(["_removeElements",0,1])}_onDataSplice(e,n){n&&this._sync(["_removeElements",e,n]);const r=arguments.length-2;r&&this._sync(["_insertElements",e,r])}_onDataUnshift(){this._sync(["_insertElements",0,arguments.length])}}Pe(ys,"defaults",{}),Pe(ys,"datasetElementType",null),Pe(ys,"dataElementType",null);function AN(t,e){if(!t._cache.$bar){const n=t.getMatchingVisibleMetas(e);let r=[];for(let s=0,o=n.length;s<o;s++)r=r.concat(n[s].controller.getAllParsedValues(t));t._cache.$bar=e0(r.sort((s,o)=>s-o))}return t._cache.$bar}function DN(t){const e=t.iScale,n=AN(e,t.type);let r=e._length,s,o,i,a;const l=()=>{i===32767||i===-32768||(mo(a)&&(r=Math.min(r,Math.abs(i-a)||r)),a=i)};for(s=0,o=n.length;s<o;++s)i=e.getPixelForValue(n[s]),l();for(a=void 0,s=0,o=e.ticks.length;s<o;++s)i=e.getPixelForTick(s),l();return r}function IN(t,e,n,r){const s=n.barThickness;let o,i;return at(s)?(o=e.min*n.categoryPercentage,i=n.barPercentage):(o=s*r,i=1),{chunk:o/r,ratio:i,start:e.pixels[t]-o/2}}function LN(t,e,n,r){const s=e.pixels,o=s[t];let i=t>0?s[t-1]:null,a=t<s.length-1?s[t+1]:null;const l=n.categoryPercentage;i===null&&(i=o-(a===null?e.end-e.start:a-o)),a===null&&(a=o+o-i);const c=o-(o-Math.min(i,a))/2*l;return{chunk:Math.abs(a-i)/2*l/r,ratio:n.barPercentage,start:c}}function PN(t,e,n,r){const s=n.parse(t[0],r),o=n.parse(t[1],r),i=Math.min(s,o),a=Math.max(s,o);let l=i,c=a;Math.abs(i)>Math.abs(a)&&(l=a,c=i),e[n.axis]=c,e._custom={barStart:l,barEnd:c,start:s,end:o,min:i,max:a}}function I0(t,e,n,r){return Mt(t)?PN(t,e,n,r):e[n.axis]=n.parse(t,r),e}function L0(t,e,n,r){const s=t.iScale,o=t.vScale,i=s.getLabels(),a=s===o,l=[];let c,d,u,f;for(c=n,d=n+r;c<d;++c)f=e[c],u={},u[s.axis]=a||s.parse(i[c],c),l.push(I0(f,u,o,c));return l}function cd(t){return t&&t.barStart!==void 0&&t.barEnd!==void 0}function $N(t,e,n){return t!==0?dr(t):(e.isHorizontal()?1:-1)*(e.min>=n?1:-1)}function FN(t){let e,n,r,s,o;return t.horizontal?(e=t.base>t.x,n="left",r="right"):(e=t.base<t.y,n="bottom",r="top"),e?(s="end",o="start"):(s="start",o="end"),{start:n,end:r,reverse:e,top:s,bottom:o}}function VN(t,e,n,r){let s=e.borderSkipped;const o={};if(!s){t.borderSkipped=o;return}if(s===!0){t.borderSkipped={top:!0,right:!0,bottom:!0,left:!0};return}const{start:i,end:a,reverse:l,top:c,bottom:d}=FN(t);s==="middle"&&n&&(t.enableBorderRadius=!0,(n._top||0)===r?s=c:(n._bottom||0)===r?s=d:(o[P0(d,i,a,l)]=!0,s=c)),o[P0(s,i,a,l)]=!0,t.borderSkipped=o}function P0(t,e,n,r){return r?(t=zN(t,e,n),t=$0(t,n,e)):t=$0(t,e,n),t}function zN(t,e,n){return t===e?n:t===n?e:t}function $0(t,e,n){return t==="start"?e:t==="end"?n:t}function HN(t,{inflateAmount:e},n){t.inflateAmount=e==="auto"?n===1?.33:0:e}class fa extends ys{parsePrimitiveData(e,n,r,s){return L0(e,n,r,s)}parseArrayData(e,n,r,s){return L0(e,n,r,s)}parseObjectData(e,n,r,s){const{iScale:o,vScale:i}=e,{xAxisKey:a="x",yAxisKey:l="y"}=this._parsing,c=o.axis==="x"?a:l,d=i.axis==="x"?a:l,u=[];let f,g,m,p;for(f=r,g=r+s;f<g;++f)p=n[f],m={},m[o.axis]=o.parse(Er(p,c),f),u.push(I0(Er(p,d),m,i,f));return u}updateRangeFromParsed(e,n,r,s){super.updateRangeFromParsed(e,n,r,s);const o=r._custom;o&&n===this._cachedMeta.vScale&&(e.min=Math.min(e.min,o.min),e.max=Math.max(e.max,o.max))}getMaxOverflow(){return 0}getLabelAndValue(e){const n=this._cachedMeta,{iScale:r,vScale:s}=n,o=this.getParsed(e),i=o._custom,a=cd(i)?"["+i.start+", "+i.end+"]":""+s.getLabelForValue(o[s.axis]);return{label:""+r.getLabelForValue(o[r.axis]),value:a}}initialize(){this.enableOptionSharing=!0,super.initialize();const e=this._cachedMeta;e.stack=this.getDataset().stack}update(e){const n=this._cachedMeta;this.updateElements(n.data,0,n.data.length,e)}updateElements(e,n,r,s){const o=s==="reset",{index:i,_cachedMeta:{vScale:a}}=this,l=a.getBasePixel(),c=a.isHorizontal(),d=this._getRuler(),{sharedOptions:u,includeOptions:f}=this._getSharedOptions(n,s);for(let g=n;g<n+r;g++){const m=this.getParsed(g),p=o||at(m[a.axis])?{base:l,head:l}:this._calculateBarValuePixels(g),y=this._calculateBarIndexPixels(g,d),b=(m._stacks||{})[a.axis],_={horizontal:c,base:p.base,enableBorderRadius:!b||cd(m._custom)||i===b._top||i===b._bottom,x:c?p.head:y.center,y:c?y.center:p.head,height:c?y.size:Math.abs(p.size),width:c?Math.abs(p.size):y.size};f&&(_.options=u||this.resolveDataElementOptions(g,e[g].active?"active":s));const x=_.options||e[g].options;VN(_,x,b,i),HN(_,x,d.ratio),this.updateElement(e[g],g,_,s)}}_getStacks(e,n){const{iScale:r}=this._cachedMeta,s=r.getMatchingVisibleMetas(this._type).filter(d=>d.controller.options.grouped),o=r.options.stacked,i=[],a=this._cachedMeta.controller.getParsed(n),l=a&&a[r.axis],c=d=>{const u=d._parsed.find(g=>g[r.axis]===l),f=u&&u[d.vScale.axis];if(at(f)||isNaN(f))return!0};for(const d of s)if(!(n!==void 0&&c(d))&&((o===!1||i.indexOf(d.stack)===-1||o===void 0&&d.stack===void 0)&&i.push(d.stack),d.index===e))break;return i.length||i.push(void 0),i}_getStackCount(e){return this._getStacks(void 0,e).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){const e=this.chart.scales,n=this.chart.options.indexAxis;return Object.keys(e).filter(r=>e[r].axis===n).shift()}_getAxis(){const e={},n=this.getFirstScaleIdForIndexAxis();for(const r of this.chart.data.datasets)e[Ke(this.chart.options.indexAxis==="x"?r.xAxisID:r.yAxisID,n)]=!0;return Object.keys(e)}_getStackIndex(e,n,r){const s=this._getStacks(e,r),o=n!==void 0?s.indexOf(n):-1;return o===-1?s.length-1:o}_getRuler(){const e=this.options,n=this._cachedMeta,r=n.iScale,s=[];let o,i;for(o=0,i=n.data.length;o<i;++o)s.push(r.getPixelForValue(this.getParsed(o)[r.axis],o));const a=e.barThickness;return{min:a||DN(n),pixels:s,start:r._startPixel,end:r._endPixel,stackCount:this._getStackCount(),scale:r,grouped:e.grouped,ratio:a?1:e.categoryPercentage*e.barPercentage}}_calculateBarValuePixels(e){const{_cachedMeta:{vScale:n,_stacked:r,index:s},options:{base:o,minBarLength:i}}=this,a=o||0,l=this.getParsed(e),c=l._custom,d=cd(c);let u=l[n.axis],f=0,g=r?this.applyStack(n,l,r):u,m,p;g!==u&&(f=g-u,g=u),d&&(u=c.barStart,g=c.barEnd-c.barStart,u!==0&&dr(u)!==dr(c.barEnd)&&(f=0),f+=u);const y=!at(o)&&!d?o:f;let b=n.getPixelForValue(y);if(this.chart.getDataVisibility(e)?m=n.getPixelForValue(f+g):m=b,p=m-b,Math.abs(p)<i){p=$N(p,n,a)*i,u===a&&(b-=p/2);const _=n.getPixelForDecimal(0),x=n.getPixelForDecimal(1),E=Math.min(_,x),S=Math.max(_,x);b=Math.max(Math.min(b,S),E),m=b+p,r&&!d&&(l._stacks[n.axis]._visualValues[s]=n.getValueForPixel(m)-n.getValueForPixel(b))}if(b===n.getPixelForValue(a)){const _=dr(p)*n.getLineWidthForValue(a)/2;b+=_,p-=_}return{size:p,base:b,head:m,center:m+p/2}}_calculateBarIndexPixels(e,n){const r=n.scale,s=this.options,o=s.skipNull,i=Ke(s.maxBarThickness,1/0);let a,l;const c=this._getAxisCount();if(n.grouped){const d=o?this._getStackCount(e):n.stackCount,u=s.barThickness==="flex"?LN(e,n,s,d*c):IN(e,n,s,d*c),f=this.chart.options.indexAxis==="x"?this.getDataset().xAxisID:this.getDataset().yAxisID,g=this._getAxis().indexOf(Ke(f,this.getFirstScaleIdForIndexAxis())),m=this._getStackIndex(this.index,this._cachedMeta.stack,o?e:void 0)+g;a=u.start+u.chunk*m+u.chunk/2,l=Math.min(i,u.chunk*u.ratio)}else a=r.getPixelForValue(this.getParsed(e)[r.axis],e),l=Math.min(i,n.min*n.ratio);return{base:a-l/2,head:a+l/2,center:a,size:l}}draw(){const e=this._cachedMeta,n=e.vScale,r=e.data,s=r.length;let o=0;for(;o<s;++o)this.getParsed(o)[n.axis]!==null&&!r[o].hidden&&r[o].draw(this._ctx)}}Pe(fa,"id","bar"),Pe(fa,"defaults",{datasetElementType:!1,dataElementType:"bar",categoryPercentage:.8,barPercentage:.9,grouped:!0,animations:{numbers:{type:"number",properties:["x","y","base","width","height"]}}}),Pe(fa,"overrides",{scales:{_index_:{type:"category",offset:!0,grid:{offset:!0}},_value_:{type:"linear",beginAtZero:!0}}});function UN(t,e,n){let r=1,s=1,o=0,i=0;if(e<Ct){const a=t,l=a+e,c=Math.cos(a),d=Math.sin(a),u=Math.cos(l),f=Math.sin(l),g=(x,E,S)=>oa(x,a,l,!0)?1:Math.max(E,E*n,S,S*n),m=(x,E,S)=>oa(x,a,l,!0)?-1:Math.min(E,E*n,S,S*n),p=g(0,c,u),y=g(Nt,d,f),b=m(lt,c,u),_=m(lt+Nt,d,f);r=(p-b)/2,s=(y-_)/2,o=-(p+b)/2,i=-(y+_)/2}return{ratioX:r,ratioY:s,offsetX:o,offsetY:i}}class wo extends ys{constructor(e,n){super(e,n),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(e,n){const r=this.getDataset().data,s=this._cachedMeta;if(this._parsing===!1)s._parsed=r;else{let o=l=>+r[l];if(Xe(r[e])){const{key:l="value"}=this._parsing;o=c=>+Er(r[c],l)}let i,a;for(i=e,a=e+n;i<a;++i)s._parsed[i]=o(i)}}_getRotation(){return qn(this.options.rotation-90)}_getCircumference(){return qn(this.options.circumference)}_getRotationExtents(){let e=Ct,n=-Ct;for(let r=0;r<this.chart.data.datasets.length;++r)if(this.chart.isDatasetVisible(r)&&this.chart.getDatasetMeta(r).type===this._type){const s=this.chart.getDatasetMeta(r).controller,o=s._getRotation(),i=s._getCircumference();e=Math.min(e,o),n=Math.max(n,o+i)}return{rotation:e,circumference:n-e}}update(e){const n=this.chart,{chartArea:r}=n,s=this._cachedMeta,o=s.data,i=this.getMaxBorderWidth()+this.getMaxOffset(o)+this.options.spacing,a=Math.max((Math.min(r.width,r.height)-i)/2,0),l=Math.min(gM(this.options.cutout,a),1),c=this._getRingWeight(this.index),{circumference:d,rotation:u}=this._getRotationExtents(),{ratioX:f,ratioY:g,offsetX:m,offsetY:p}=UN(u,d,l),y=(r.width-i)/f,b=(r.height-i)/g,_=Math.max(Math.min(y,b)/2,0),x=jp(this.options.radius,_),E=Math.max(x*l,0),S=(x-E)/this._getVisibleDatasetWeightTotal();this.offsetX=m*x,this.offsetY=p*x,s.total=this.calculateTotal(),this.outerRadius=x-S*this._getRingWeightOffset(this.index),this.innerRadius=Math.max(this.outerRadius-S*c,0),this.updateElements(o,0,o.length,e)}_circumference(e,n){const r=this.options,s=this._cachedMeta,o=this._getCircumference();return n&&r.animation.animateRotate||!this.chart.getDataVisibility(e)||s._parsed[e]===null||s.data[e].hidden?0:this.calculateCircumference(s._parsed[e]*o/Ct)}updateElements(e,n,r,s){const o=s==="reset",i=this.chart,a=i.chartArea,c=i.options.animation,d=(a.left+a.right)/2,u=(a.top+a.bottom)/2,f=o&&c.animateScale,g=f?0:this.innerRadius,m=f?0:this.outerRadius,{sharedOptions:p,includeOptions:y}=this._getSharedOptions(n,s);let b=this._getRotation(),_;for(_=0;_<n;++_)b+=this._circumference(_,o);for(_=n;_<n+r;++_){const x=this._circumference(_,o),E=e[_],S={x:d+this.offsetX,y:u+this.offsetY,startAngle:b,endAngle:b+x,circumference:x,outerRadius:m,innerRadius:g};y&&(S.options=p||this.resolveDataElementOptions(_,E.active?"active":s)),b+=x,this.updateElement(E,_,S,s)}}calculateTotal(){const e=this._cachedMeta,n=e.data;let r=0,s;for(s=0;s<n.length;s++){const o=e._parsed[s];o!==null&&!isNaN(o)&&this.chart.getDataVisibility(s)&&!n[s].hidden&&(r+=Math.abs(o))}return r}calculateCircumference(e){const n=this._cachedMeta.total;return n>0&&!isNaN(e)?Ct*(Math.abs(e)/n):0}getLabelAndValue(e){const n=this._cachedMeta,r=this.chart,s=r.data.labels||[],o=qc(n._parsed[e],r.options.locale);return{label:s[e]||"",value:o}}getMaxBorderWidth(e){let n=0;const r=this.chart;let s,o,i,a,l;if(!e){for(s=0,o=r.data.datasets.length;s<o;++s)if(r.isDatasetVisible(s)){i=r.getDatasetMeta(s),e=i.data,a=i.controller;break}}if(!e)return 0;for(s=0,o=e.length;s<o;++s)l=a.resolveDataElementOptions(s),l.borderAlign!=="inner"&&(n=Math.max(n,l.borderWidth||0,l.hoverBorderWidth||0));return n}getMaxOffset(e){let n=0;for(let r=0,s=e.length;r<s;++r){const o=this.resolveDataElementOptions(r);n=Math.max(n,o.offset||0,o.hoverOffset||0)}return n}_getRingWeightOffset(e){let n=0;for(let r=0;r<e;++r)this.chart.isDatasetVisible(r)&&(n+=this._getRingWeight(r));return n}_getRingWeight(e){return Math.max(Ke(this.chart.data.datasets[e].weight,1),0)}_getVisibleDatasetWeightTotal(){return this._getRingWeightOffset(this.chart.data.datasets.length)||1}}Pe(wo,"id","doughnut"),Pe(wo,"defaults",{datasetElementType:!1,dataElementType:"arc",animation:{animateRotate:!0,animateScale:!1},animations:{numbers:{type:"number",properties:["circumference","endAngle","innerRadius","outerRadius","startAngle","x","y","offset","borderWidth","spacing"]}},cutout:"50%",rotation:0,circumference:360,radius:"100%",spacing:0,indexAxis:"r"}),Pe(wo,"descriptors",{_scriptable:e=>e!=="spacing",_indexable:e=>e!=="spacing"&&!e.startsWith("borderDash")&&!e.startsWith("hoverBorderDash")}),Pe(wo,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(e){const n=e.data,{labels:{pointStyle:r,textAlign:s,color:o,useBorderRadius:i,borderRadius:a}}=e.legend.options;return n.labels.length&&n.datasets.length?n.labels.map((l,c)=>{const u=e.getDatasetMeta(0).controller.getStyle(c);return{text:l,fillStyle:u.backgroundColor,fontColor:o,hidden:!e.getDataVisibility(c),lineDash:u.borderDash,lineDashOffset:u.borderDashOffset,lineJoin:u.borderJoinStyle,lineWidth:u.borderWidth,strokeStyle:u.borderColor,textAlign:s,pointStyle:r,borderRadius:i&&(a||u.borderRadius),index:c}}):[]}},onClick(e,n,r){r.chart.toggleDataVisibility(n.index),r.chart.update()}}}});function Ir(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}class dd{constructor(e){Pe(this,"options");this.options=e||{}}static override(e){Object.assign(dd.prototype,e)}init(){}formats(){return Ir()}parse(){return Ir()}format(){return Ir()}add(){return Ir()}diff(){return Ir()}startOf(){return Ir()}endOf(){return Ir()}}var BN={_date:dd};function jN(t,e,n,r){const{controller:s,data:o,_sorted:i}=t,a=s._cachedMeta.iScale,l=t.dataset&&t.dataset.options?t.dataset.options.spanGaps:null;if(a&&e===a.axis&&e!=="r"&&i&&o.length){const c=a._reversePixels?NM:Yc;if(r){if(s._sharedOptions){const d=o[0],u=typeof d.getRange=="function"&&d.getRange(e);if(u){const f=c(o,e,n-u),g=c(o,e,n+u);return{lo:f.lo,hi:g.hi}}}}else{const d=c(o,e,n);if(l){const{vScale:u}=s._cachedMeta,{_parsed:f}=t,g=f.slice(0,d.lo+1).reverse().findIndex(p=>!at(p[u.axis]));d.lo-=Math.max(0,g);const m=f.slice(d.hi).findIndex(p=>!at(p[u.axis]));d.hi+=Math.max(0,m)}return d}}return{lo:0,hi:o.length-1}}function ha(t,e,n,r,s){const o=t.getSortedVisibleDatasetMetas(),i=n[e];for(let a=0,l=o.length;a<l;++a){const{index:c,data:d}=o[a],{lo:u,hi:f}=jN(o[a],e,i,s);for(let g=u;g<=f;++g){const m=d[g];m.skip||r(m,c,g)}}}function WN(t){const e=t.indexOf("x")!==-1,n=t.indexOf("y")!==-1;return function(r,s){const o=e?Math.abs(r.x-s.x):0,i=n?Math.abs(r.y-s.y):0;return Math.sqrt(Math.pow(o,2)+Math.pow(i,2))}}function ud(t,e,n,r,s){const o=[];return!s&&!t.isPointInArea(e)||ha(t,n,e,function(a,l,c){!s&&!h0(a,t.chartArea,0)||a.inRange(e.x,e.y,r)&&o.push({element:a,datasetIndex:l,index:c})},!0),o}function GN(t,e,n,r){let s=[];function o(i,a,l){const{startAngle:c,endAngle:d}=i.getProps(["startAngle","endAngle"],r),{angle:u}=Qp(i,{x:e.x,y:e.y});oa(u,c,d)&&s.push({element:i,datasetIndex:a,index:l})}return ha(t,n,e,o),s}function YN(t,e,n,r,s,o){let i=[];const a=WN(n);let l=Number.POSITIVE_INFINITY;function c(d,u,f){const g=d.inRange(e.x,e.y,s);if(r&&!g)return;const m=d.getCenterPoint(s);if(!(!!o||t.isPointInArea(m))&&!g)return;const y=a(e,m);y<l?(i=[{element:d,datasetIndex:u,index:f}],l=y):y===l&&i.push({element:d,datasetIndex:u,index:f})}return ha(t,n,e,c),i}function fd(t,e,n,r,s,o){return!o&&!t.isPointInArea(e)?[]:n==="r"&&!r?GN(t,e,n,s):YN(t,e,n,r,s,o)}function F0(t,e,n,r,s){const o=[],i=n==="x"?"inXRange":"inYRange";let a=!1;return ha(t,n,e,(l,c,d)=>{l[i]&&l[i](e[n],s)&&(o.push({element:l,datasetIndex:c,index:d}),a=a||l.inRange(e.x,e.y,s))}),r&&!a?[]:o}var XN={modes:{index(t,e,n,r){const s=Dr(e,t),o=n.axis||"x",i=n.includeInvisible||!1,a=n.intersect?ud(t,s,o,r,i):fd(t,s,o,!1,r,i),l=[];return a.length?(t.getSortedVisibleDatasetMetas().forEach(c=>{const d=a[0].index,u=c.data[d];u&&!u.skip&&l.push({element:u,datasetIndex:c.index,index:d})}),l):[]},dataset(t,e,n,r){const s=Dr(e,t),o=n.axis||"xy",i=n.includeInvisible||!1;let a=n.intersect?ud(t,s,o,r,i):fd(t,s,o,!1,r,i);if(a.length>0){const l=a[0].datasetIndex,c=t.getDatasetMeta(l).data;a=[];for(let d=0;d<c.length;++d)a.push({element:c[d],datasetIndex:l,index:d})}return a},point(t,e,n,r){const s=Dr(e,t),o=n.axis||"xy",i=n.includeInvisible||!1;return ud(t,s,o,r,i)},nearest(t,e,n,r){const s=Dr(e,t),o=n.axis||"xy",i=n.includeInvisible||!1;return fd(t,s,o,n.intersect,r,i)},x(t,e,n,r){const s=Dr(e,t);return F0(t,s,"x",n.intersect,r)},y(t,e,n,r){const s=Dr(e,t);return F0(t,s,"y",n.intersect,r)}}};const V0=["left","top","right","bottom"];function ko(t,e){return t.filter(n=>n.pos===e)}function z0(t,e){return t.filter(n=>V0.indexOf(n.pos)===-1&&n.box.axis===e)}function Co(t,e){return t.sort((n,r)=>{const s=e?r:n,o=e?n:r;return s.weight===o.weight?s.index-o.index:s.weight-o.weight})}function KN(t){const e=[];let n,r,s,o,i,a;for(n=0,r=(t||[]).length;n<r;++n)s=t[n],{position:o,options:{stack:i,stackWeight:a=1}}=s,e.push({index:n,box:s,pos:o,horizontal:s.isHorizontal(),weight:s.weight,stack:i&&o+i,stackWeight:a});return e}function qN(t){const e={};for(const n of t){const{stack:r,pos:s,stackWeight:o}=n;if(!r||!V0.includes(s))continue;const i=e[r]||(e[r]={count:0,placed:0,weight:0,size:0});i.count++,i.weight+=o}return e}function ZN(t,e){const n=qN(t),{vBoxMaxWidth:r,hBoxMaxHeight:s}=e;let o,i,a;for(o=0,i=t.length;o<i;++o){a=t[o];const{fullSize:l}=a.box,c=n[a.stack],d=c&&a.stackWeight/c.weight;a.horizontal?(a.width=d?d*r:l&&e.availableWidth,a.height=s):(a.width=r,a.height=d?d*s:l&&e.availableHeight)}return n}function QN(t){const e=KN(t),n=Co(e.filter(c=>c.box.fullSize),!0),r=Co(ko(e,"left"),!0),s=Co(ko(e,"right")),o=Co(ko(e,"top"),!0),i=Co(ko(e,"bottom")),a=z0(e,"x"),l=z0(e,"y");return{fullSize:n,leftAndTop:r.concat(o),rightAndBottom:s.concat(l).concat(i).concat(a),chartArea:ko(e,"chartArea"),vertical:r.concat(s).concat(l),horizontal:o.concat(i).concat(a)}}function H0(t,e,n,r){return Math.max(t[n],e[n])+Math.max(t[r],e[r])}function U0(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function JN(t,e,n,r){const{pos:s,box:o}=n,i=t.maxPadding;if(!Xe(s)){n.size&&(t[s]-=n.size);const u=r[n.stack]||{size:0,count:1};u.size=Math.max(u.size,n.horizontal?o.height:o.width),n.size=u.size/u.count,t[s]+=n.size}o.getPadding&&U0(i,o.getPadding());const a=Math.max(0,e.outerWidth-H0(i,t,"left","right")),l=Math.max(0,e.outerHeight-H0(i,t,"top","bottom")),c=a!==t.w,d=l!==t.h;return t.w=a,t.h=l,n.horizontal?{same:c,other:d}:{same:d,other:c}}function RN(t){const e=t.maxPadding;function n(r){const s=Math.max(e[r]-t[r],0);return t[r]+=s,s}t.y+=n("top"),t.x+=n("left"),n("right"),n("bottom")}function eO(t,e){const n=e.maxPadding;function r(s){const o={left:0,top:0,right:0,bottom:0};return s.forEach(i=>{o[i]=Math.max(e[i],n[i])}),o}return r(t?["left","right"]:["top","bottom"])}function So(t,e,n,r){const s=[];let o,i,a,l,c,d;for(o=0,i=t.length,c=0;o<i;++o){a=t[o],l=a.box,l.update(a.width||e.w,a.height||e.h,eO(a.horizontal,e));const{same:u,other:f}=JN(e,n,a,r);c|=u&&s.length,d=d||f,l.fullSize||s.push(a)}return c&&So(s,e,n,r)||d}function ga(t,e,n,r,s){t.top=n,t.left=e,t.right=e+r,t.bottom=n+s,t.width=r,t.height=s}function B0(t,e,n,r){const s=n.padding;let{x:o,y:i}=e;for(const a of t){const l=a.box,c=r[a.stack]||{placed:0,weight:1},d=a.stackWeight/c.weight||1;if(a.horizontal){const u=e.w*d,f=c.size||l.height;mo(c.start)&&(i=c.start),l.fullSize?ga(l,s.left,i,n.outerWidth-s.right-s.left,f):ga(l,e.left+c.placed,i,u,f),c.start=i,c.placed+=u,i=l.bottom}else{const u=e.h*d,f=c.size||l.width;mo(c.start)&&(o=c.start),l.fullSize?ga(l,o,s.top,f,n.outerHeight-s.bottom-s.top):ga(l,o,e.top+c.placed,f,u),c.start=o,c.placed+=u,o=l.right}}e.x=o,e.y=i}var vn={addBox(t,e){t.boxes||(t.boxes=[]),e.fullSize=e.fullSize||!1,e.position=e.position||"top",e.weight=e.weight||0,e._layers=e._layers||function(){return[{z:0,draw(n){e.draw(n)}}]},t.boxes.push(e)},removeBox(t,e){const n=t.boxes?t.boxes.indexOf(e):-1;n!==-1&&t.boxes.splice(n,1)},configure(t,e,n){e.fullSize=n.fullSize,e.position=n.position,e.weight=n.weight},update(t,e,n,r){if(!t)return;const s=yn(t.options.layout.padding),o=Math.max(e-s.width,0),i=Math.max(n-s.height,0),a=QN(t.boxes),l=a.vertical,c=a.horizontal;tt(t.boxes,p=>{typeof p.beforeLayout=="function"&&p.beforeLayout()});const d=l.reduce((p,y)=>y.box.options&&y.box.options.display===!1?p:p+1,0)||1,u=Object.freeze({outerWidth:e,outerHeight:n,padding:s,availableWidth:o,availableHeight:i,vBoxMaxWidth:o/2/d,hBoxMaxHeight:i/2}),f=Object.assign({},s);U0(f,yn(r));const g=Object.assign({maxPadding:f,w:o,h:i,x:s.left,y:s.top},s),m=ZN(l.concat(c),u);So(a.fullSize,g,u,m),So(l,g,u,m),So(c,g,u,m)&&So(l,g,u,m),RN(g),B0(a.leftAndTop,g,u,m),g.x+=g.w,g.y+=g.h,B0(a.rightAndBottom,g,u,m),t.chartArea={left:g.left,top:g.top,right:g.left+g.w,bottom:g.top+g.h,height:g.h,width:g.w},tt(a.chartArea,p=>{const y=p.box;Object.assign(y,t.chartArea),y.update(g.w,g.h,{left:0,top:0,right:0,bottom:0})})}};class j0{acquireContext(e,n){}releaseContext(e){return!1}addEventListener(e,n,r){}removeEventListener(e,n,r){}getDevicePixelRatio(){return 1}getMaximumSize(e,n,r,s){return n=Math.max(0,n||e.width),r=r||e.height,{width:n,height:Math.max(0,s?Math.floor(n/s):r)}}isAttached(e){return!0}updateConfig(e){}}class tO extends j0{acquireContext(e){return e&&e.getContext&&e.getContext("2d")||null}updateConfig(e){e.options.animation=!1}}const pa="$chartjs",nO={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},W0=t=>t===null||t==="";function rO(t,e){const n=t.style,r=t.getAttribute("height"),s=t.getAttribute("width");if(t[pa]={initial:{height:r,width:s,style:{display:n.display,height:n.height,width:n.width}}},n.display=n.display||"block",n.boxSizing=n.boxSizing||"border-box",W0(s)){const o=k0(t,"width");o!==void 0&&(t.width=o)}if(W0(r))if(t.style.height==="")t.height=t.width/(e||2);else{const o=k0(t,"height");o!==void 0&&(t.height=o)}return t}const G0=fN?{passive:!0}:!1;function sO(t,e,n){t&&t.addEventListener(e,n,G0)}function oO(t,e,n){t&&t.canvas&&t.canvas.removeEventListener(e,n,G0)}function iO(t,e){const n=nO[t.type]||t.type,{x:r,y:s}=Dr(t,e);return{type:n,chart:e,native:t,x:r!==void 0?r:null,y:s!==void 0?s:null}}function ma(t,e){for(const n of t)if(n===e||n.contains(e))return!0}function aO(t,e,n){const r=t.canvas,s=new MutationObserver(o=>{let i=!1;for(const a of o)i=i||ma(a.addedNodes,r),i=i&&!ma(a.removedNodes,r);i&&n()});return s.observe(document,{childList:!0,subtree:!0}),s}function lO(t,e,n){const r=t.canvas,s=new MutationObserver(o=>{let i=!1;for(const a of o)i=i||ma(a.removedNodes,r),i=i&&!ma(a.addedNodes,r);i&&n()});return s.observe(document,{childList:!0,subtree:!0}),s}const _o=new Map;let Y0=0;function X0(){const t=window.devicePixelRatio;t!==Y0&&(Y0=t,_o.forEach((e,n)=>{n.currentDevicePixelRatio!==t&&e()}))}function cO(t,e){_o.size||window.addEventListener("resize",X0),_o.set(t,e)}function dO(t){_o.delete(t),_o.size||window.removeEventListener("resize",X0)}function uO(t,e,n){const r=t.canvas,s=r&&od(r);if(!s)return;const o=n0((a,l)=>{const c=s.clientWidth;n(a,l),c<s.clientWidth&&n()},window),i=new ResizeObserver(a=>{const l=a[0],c=l.contentRect.width,d=l.contentRect.height;c===0&&d===0||o(c,d)});return i.observe(s),cO(t,o),i}function hd(t,e,n){n&&n.disconnect(),e==="resize"&&dO(t)}function fO(t,e,n){const r=t.canvas,s=n0(o=>{t.ctx!==null&&n(iO(o,t))},t);return sO(r,e,s),s}class hO extends j0{acquireContext(e,n){const r=e&&e.getContext&&e.getContext("2d");return r&&r.canvas===e?(rO(e,n),r):null}releaseContext(e){const n=e.canvas;if(!n[pa])return!1;const r=n[pa].initial;["height","width"].forEach(o=>{const i=r[o];at(i)?n.removeAttribute(o):n.setAttribute(o,i)});const s=r.style||{};return Object.keys(s).forEach(o=>{n.style[o]=s[o]}),n.width=n.width,delete n[pa],!0}addEventListener(e,n,r){this.removeEventListener(e,n);const s=e.$proxies||(e.$proxies={}),i={attach:aO,detach:lO,resize:uO}[n]||fO;s[n]=i(e,n,r)}removeEventListener(e,n){const r=e.$proxies||(e.$proxies={}),s=r[n];if(!s)return;({attach:hd,detach:hd,resize:hd}[n]||oO)(e,n,s),r[n]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(e,n,r,s){return uN(e,n,r,s)}isAttached(e){const n=e&&od(e);return!!(n&&n.isConnected)}}function gO(t){return!sd()||typeof OffscreenCanvas<"u"&&t instanceof OffscreenCanvas?tO:hO}let Lr=(Aa=class{constructor(){Pe(this,"x");Pe(this,"y");Pe(this,"active",!1);Pe(this,"options");Pe(this,"$animations")}tooltipPosition(e){const{x:n,y:r}=this.getProps(["x","y"],e);return{x:n,y:r}}hasValue(){return sa(this.x)&&sa(this.y)}getProps(e,n){const r=this.$animations;if(!n||!r)return this;const s={};return e.forEach(o=>{s[o]=r[o]&&r[o].active()?r[o]._to:this[o]}),s}},Pe(Aa,"defaults",{}),Pe(Aa,"defaultRoutes"),Aa);function pO(t,e){const n=t.options.ticks,r=mO(t),s=Math.min(n.maxTicksLimit||r,r),o=n.major.enabled?yO(e):[],i=o.length,a=o[0],l=o[i-1],c=[];if(i>s)return vO(e,c,o,i/s),c;const d=bO(o,e,s);if(i>0){let u,f;const g=i>1?Math.round((l-a)/(i-1)):null;for(ba(e,c,d,at(g)?0:a-g,a),u=0,f=i-1;u<f;u++)ba(e,c,d,o[u],o[u+1]);return ba(e,c,d,l,at(g)?e.length:l+g),c}return ba(e,c,d),c}function mO(t){const e=t.options.offset,n=t._tickSize(),r=t._length/n+(e?0:1),s=t._maxLength/n;return Math.floor(Math.min(r,s))}function bO(t,e,n){const r=xO(t),s=e.length/n;if(!r)return Math.max(s,1);const o=wM(r);for(let i=0,a=o.length-1;i<a;i++){const l=o[i];if(l>s)return l}return Math.max(s,1)}function yO(t){const e=[];let n,r;for(n=0,r=t.length;n<r;n++)t[n].major&&e.push(n);return e}function vO(t,e,n,r){let s=0,o=n[0],i;for(r=Math.ceil(r),i=0;i<t.length;i++)i===o&&(e.push(t[i]),s++,o=n[s*r])}function ba(t,e,n,r,s){const o=Ke(r,0),i=Math.min(Ke(s,t.length),t.length);let a=0,l,c,d;for(n=Math.ceil(n),s&&(l=s-r,n=l/Math.floor(l/n)),d=o;d<0;)a++,d=Math.round(o+a*n);for(c=Math.max(o,0);c<i;c++)c===d&&(e.push(t[c]),a++,d=Math.round(o+a*n))}function xO(t){const e=t.length;let n,r;if(e<2)return!1;for(r=t[0],n=1;n<e;++n)if(t[n]-t[n-1]!==r)return!1;return r}const wO=t=>t==="left"?"right":t==="right"?"left":t,K0=(t,e,n)=>e==="top"||e==="left"?t[e]+n:t[e]-n,q0=(t,e)=>Math.min(e||t,t);function Z0(t,e){const n=[],r=t.length/e,s=t.length;let o=0;for(;o<s;o+=r)n.push(t[Math.floor(o)]);return n}function kO(t,e,n){const r=t.ticks.length,s=Math.min(e,r-1),o=t._startPixel,i=t._endPixel,a=1e-6;let l=t.getPixelForTick(s),c;if(!(n&&(r===1?c=Math.max(l-o,i-l):e===0?c=(t.getPixelForTick(1)-l)/2:c=(l-t.getPixelForTick(s-1))/2,l+=s<e?c:-c,l<o-a||l>i+a)))return l}function CO(t,e){tt(t,n=>{const r=n.gc,s=r.length/2;let o;if(s>e){for(o=0;o<s;++o)delete n.data[r[o]];r.splice(0,s)}})}function Eo(t){return t.drawTicks?t.tickLength:0}function Q0(t,e){if(!t.display)return 0;const n=Wt(t.font,e),r=yn(t.padding);return(Mt(t.text)?t.text.length:1)*n.lineHeight+r.height}function SO(t,e){return ps(t,{scale:e,type:"scale"})}function _O(t,e,n){return ps(t,{tick:n,index:e,type:"tick"})}function EO(t,e,n){let r=Xc(t);return(n&&e!=="right"||!n&&e==="right")&&(r=wO(r)),r}function MO(t,e,n,r){const{top:s,left:o,bottom:i,right:a,chart:l}=t,{chartArea:c,scales:d}=l;let u=0,f,g,m;const p=i-s,y=a-o;if(t.isHorizontal()){if(g=jt(r,o,a),Xe(n)){const b=Object.keys(n)[0],_=n[b];m=d[b].getPixelForValue(_)+p-e}else n==="center"?m=(c.bottom+c.top)/2+p-e:m=K0(t,n,e);f=a-o}else{if(Xe(n)){const b=Object.keys(n)[0],_=n[b];g=d[b].getPixelForValue(_)-y+e}else n==="center"?g=(c.left+c.right)/2-y+e:g=K0(t,n,e);m=jt(r,i,s),u=n==="left"?-Nt:Nt}return{titleX:g,titleY:m,maxWidth:f,rotation:u}}class vs extends Lr{constructor(e){super(),this.id=e.id,this.type=e.type,this.options=void 0,this.ctx=e.ctx,this.chart=e.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(e){this.options=e.setContext(this.getContext()),this.axis=e.axis,this._userMin=this.parse(e.min),this._userMax=this.parse(e.max),this._suggestedMin=this.parse(e.suggestedMin),this._suggestedMax=this.parse(e.suggestedMax)}parse(e,n){return e}getUserBounds(){let{_userMin:e,_userMax:n,_suggestedMin:r,_suggestedMax:s}=this;return e=zn(e,Number.POSITIVE_INFINITY),n=zn(n,Number.NEGATIVE_INFINITY),r=zn(r,Number.POSITIVE_INFINITY),s=zn(s,Number.NEGATIVE_INFINITY),{min:zn(e,r),max:zn(n,s),minDefined:bn(e),maxDefined:bn(n)}}getMinMax(e){let{min:n,max:r,minDefined:s,maxDefined:o}=this.getUserBounds(),i;if(s&&o)return{min:n,max:r};const a=this.getMatchingVisibleMetas();for(let l=0,c=a.length;l<c;++l)i=a[l].controller.getMinMax(this,e),s||(n=Math.min(n,i.min)),o||(r=Math.max(r,i.max));return n=o&&n>r?r:n,r=s&&n>r?n:r,{min:zn(n,zn(r,n)),max:zn(r,zn(n,r))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){const e=this.chart.data;return this.options.labels||(this.isHorizontal()?e.xLabels:e.yLabels)||e.labels||[]}getLabelItems(e=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(e))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){ut(this.options.beforeUpdate,[this])}update(e,n,r){const{beginAtZero:s,grace:o,ticks:i}=this.options,a=i.sampleSize;this.beforeUpdate(),this.maxWidth=e,this.maxHeight=n,this._margins=r=Object.assign({left:0,right:0,top:0,bottom:0},r),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+r.left+r.right:this.height+r.top+r.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=ZM(this,o,s),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();const l=a<this.ticks.length;this._convertTicksToLabels(l?Z0(this.ticks,a):this.ticks),this.configure(),this.beforeCalculateLabelRotation(),this.calculateLabelRotation(),this.afterCalculateLabelRotation(),i.display&&(i.autoSkip||i.source==="auto")&&(this.ticks=pO(this,this.ticks),this._labelSizes=null,this.afterAutoSkip()),l&&this._convertTicksToLabels(this.ticks),this.beforeFit(),this.fit(),this.afterFit(),this.afterUpdate()}configure(){let e=this.options.reverse,n,r;this.isHorizontal()?(n=this.left,r=this.right):(n=this.top,r=this.bottom,e=!e),this._startPixel=n,this._endPixel=r,this._reversePixels=e,this._length=r-n,this._alignToPixels=this.options.alignToPixels}afterUpdate(){ut(this.options.afterUpdate,[this])}beforeSetDimensions(){ut(this.options.beforeSetDimensions,[this])}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=0,this.right=this.width):(this.height=this.maxHeight,this.top=0,this.bottom=this.height),this.paddingLeft=0,this.paddingTop=0,this.paddingRight=0,this.paddingBottom=0}afterSetDimensions(){ut(this.options.afterSetDimensions,[this])}_callHooks(e){this.chart.notifyPlugins(e,this.getContext()),ut(this.options[e],[this])}beforeDataLimits(){this._callHooks("beforeDataLimits")}determineDataLimits(){}afterDataLimits(){this._callHooks("afterDataLimits")}beforeBuildTicks(){this._callHooks("beforeBuildTicks")}buildTicks(){return[]}afterBuildTicks(){this._callHooks("afterBuildTicks")}beforeTickToLabelConversion(){ut(this.options.beforeTickToLabelConversion,[this])}generateTickLabels(e){const n=this.options.ticks;let r,s,o;for(r=0,s=e.length;r<s;r++)o=e[r],o.label=ut(n.callback,[o.value,r,e],this)}afterTickToLabelConversion(){ut(this.options.afterTickToLabelConversion,[this])}beforeCalculateLabelRotation(){ut(this.options.beforeCalculateLabelRotation,[this])}calculateLabelRotation(){const e=this.options,n=e.ticks,r=q0(this.ticks.length,e.ticks.maxTicksLimit),s=n.minRotation||0,o=n.maxRotation;let i=s,a,l,c;if(!this._isVisible()||!n.display||s>=o||r<=1||!this.isHorizontal()){this.labelRotation=s;return}const d=this._getLabelSizes(),u=d.widest.width,f=d.highest.height,g=nn(this.chart.width-u,0,this.maxWidth);a=e.offset?this.maxWidth/r:g/(r-1),u+6>a&&(a=g/(r-(e.offset?.5:1)),l=this.maxHeight-Eo(e.grid)-n.padding-Q0(e.title,this.chart.options.font),c=Math.sqrt(u*u+f*f),i=_M(Math.min(Math.asin(nn((d.highest.height+6)/a,-1,1)),Math.asin(nn(l/c,-1,1))-Math.asin(nn(f/c,-1,1)))),i=Math.max(s,Math.min(o,i))),this.labelRotation=i}afterCalculateLabelRotation(){ut(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){ut(this.options.beforeFit,[this])}fit(){const e={width:0,height:0},{chart:n,options:{ticks:r,title:s,grid:o}}=this,i=this._isVisible(),a=this.isHorizontal();if(i){const l=Q0(s,n.options.font);if(a?(e.width=this.maxWidth,e.height=Eo(o)+l):(e.height=this.maxHeight,e.width=Eo(o)+l),r.display&&this.ticks.length){const{first:c,last:d,widest:u,highest:f}=this._getLabelSizes(),g=r.padding*2,m=qn(this.labelRotation),p=Math.cos(m),y=Math.sin(m);if(a){const b=r.mirror?0:y*u.width+p*f.height;e.height=Math.min(this.maxHeight,e.height+b+g)}else{const b=r.mirror?0:p*u.width+y*f.height;e.width=Math.min(this.maxWidth,e.width+b+g)}this._calculatePadding(c,d,y,p)}}this._handleMargins(),a?(this.width=this._length=n.width-this._margins.left-this._margins.right,this.height=e.height):(this.width=e.width,this.height=this._length=n.height-this._margins.top-this._margins.bottom)}_calculatePadding(e,n,r,s){const{ticks:{align:o,padding:i},position:a}=this.options,l=this.labelRotation!==0,c=a!=="top"&&this.axis==="x";if(this.isHorizontal()){const d=this.getPixelForTick(0)-this.left,u=this.right-this.getPixelForTick(this.ticks.length-1);let f=0,g=0;l?c?(f=s*e.width,g=r*n.height):(f=r*e.height,g=s*n.width):o==="start"?g=n.width:o==="end"?f=e.width:o!=="inner"&&(f=e.width/2,g=n.width/2),this.paddingLeft=Math.max((f-d+i)*this.width/(this.width-d),0),this.paddingRight=Math.max((g-u+i)*this.width/(this.width-u),0)}else{let d=n.height/2,u=e.height/2;o==="start"?(d=0,u=e.height):o==="end"&&(d=n.height,u=0),this.paddingTop=d+i,this.paddingBottom=u+i}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){ut(this.options.afterFit,[this])}isHorizontal(){const{axis:e,position:n}=this.options;return n==="top"||n==="bottom"||e==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(e){this.beforeTickToLabelConversion(),this.generateTickLabels(e);let n,r;for(n=0,r=e.length;n<r;n++)at(e[n].label)&&(e.splice(n,1),r--,n--);this.afterTickToLabelConversion()}_getLabelSizes(){let e=this._labelSizes;if(!e){const n=this.options.ticks.sampleSize;let r=this.ticks;n<r.length&&(r=Z0(r,n)),this._labelSizes=e=this._computeLabelSizes(r,r.length,this.options.ticks.maxTicksLimit)}return e}_computeLabelSizes(e,n,r){const{ctx:s,_longestTextCache:o}=this,i=[],a=[],l=Math.floor(n/q0(n,r));let c=0,d=0,u,f,g,m,p,y,b,_,x,E,S;for(u=0;u<n;u+=l){if(m=e[u].label,p=this._resolveTickFontOptions(u),s.font=y=p.string,b=o[y]=o[y]||{data:{},gc:[]},_=p.lineHeight,x=E=0,!at(m)&&!Mt(m))x=c0(s,b.data,b.gc,x,m),E=_;else if(Mt(m))for(f=0,g=m.length;f<g;++f)S=m[f],!at(S)&&!Mt(S)&&(x=c0(s,b.data,b.gc,x,S),E+=_);i.push(x),a.push(E),c=Math.max(x,c),d=Math.max(E,d)}CO(o,n);const w=i.indexOf(c),N=a.indexOf(d),C=k=>({width:i[k]||0,height:a[k]||0});return{first:C(0),last:C(n-1),widest:C(w),highest:C(N),widths:i,heights:a}}getLabelForValue(e){return e}getPixelForValue(e,n){return NaN}getValueForPixel(e){}getPixelForTick(e){const n=this.ticks;return e<0||e>n.length-1?null:this.getPixelForValue(n[e].value)}getPixelForDecimal(e){this._reversePixels&&(e=1-e);const n=this._startPixel+e*this._length;return MM(this._alignToPixels?Tr(this.chart,n,0):n)}getDecimalForPixel(e){const n=(e-this._startPixel)/this._length;return this._reversePixels?1-n:n}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:e,max:n}=this;return e<0&&n<0?n:e>0&&n>0?e:0}getContext(e){const n=this.ticks||[];if(e>=0&&e<n.length){const r=n[e];return r.$context||(r.$context=_O(this.getContext(),e,r))}return this.$context||(this.$context=SO(this.chart.getContext(),this))}_tickSize(){const e=this.options.ticks,n=qn(this.labelRotation),r=Math.abs(Math.cos(n)),s=Math.abs(Math.sin(n)),o=this._getLabelSizes(),i=e.autoSkipPadding||0,a=o?o.widest.width+i:0,l=o?o.highest.height+i:0;return this.isHorizontal()?l*r>a*s?a/r:l/s:l*s<a*r?l/r:a/s}_isVisible(){const e=this.options.display;return e!=="auto"?!!e:this.getMatchingVisibleMetas().length>0}_computeGridLineItems(e){const n=this.axis,r=this.chart,s=this.options,{grid:o,position:i,border:a}=s,l=o.offset,c=this.isHorizontal(),u=this.ticks.length+(l?1:0),f=Eo(o),g=[],m=a.setContext(this.getContext()),p=m.display?m.width:0,y=p/2,b=function(A){return Tr(r,A,p)};let _,x,E,S,w,N,C,k,M,T,L,P;if(i==="top")_=b(this.bottom),N=this.bottom-f,k=_-y,T=b(e.top)+y,P=e.bottom;else if(i==="bottom")_=b(this.top),T=e.top,P=b(e.bottom)-y,N=_+y,k=this.top+f;else if(i==="left")_=b(this.right),w=this.right-f,C=_-y,M=b(e.left)+y,L=e.right;else if(i==="right")_=b(this.left),M=e.left,L=b(e.right)-y,w=_+y,C=this.left+f;else if(n==="x"){if(i==="center")_=b((e.top+e.bottom)/2+.5);else if(Xe(i)){const A=Object.keys(i)[0],O=i[A];_=b(this.chart.scales[A].getPixelForValue(O))}T=e.top,P=e.bottom,N=_+y,k=N+f}else if(n==="y"){if(i==="center")_=b((e.left+e.right)/2);else if(Xe(i)){const A=Object.keys(i)[0],O=i[A];_=b(this.chart.scales[A].getPixelForValue(O))}w=_-y,C=w-f,M=e.left,L=e.right}const F=Ke(s.ticks.maxTicksLimit,u),D=Math.max(1,Math.ceil(u/F));for(x=0;x<u;x+=D){const A=this.getContext(x),O=o.setContext(A),V=a.setContext(A),H=O.lineWidth,W=O.color,j=V.dash||[],U=V.dashOffset,B=O.tickWidth,G=O.tickColor,z=O.tickBorderDash||[],X=O.tickBorderDashOffset;E=kO(this,x,l),E!==void 0&&(S=Tr(r,E,H),c?w=C=M=L=S:N=k=T=P=S,g.push({tx1:w,ty1:N,tx2:C,ty2:k,x1:M,y1:T,x2:L,y2:P,width:H,color:W,borderDash:j,borderDashOffset:U,tickWidth:B,tickColor:G,tickBorderDash:z,tickBorderDashOffset:X}))}return this._ticksLength=u,this._borderValue=_,g}_computeLabelItems(e){const n=this.axis,r=this.options,{position:s,ticks:o}=r,i=this.isHorizontal(),a=this.ticks,{align:l,crossAlign:c,padding:d,mirror:u}=o,f=Eo(r.grid),g=f+d,m=u?-d:g,p=-qn(this.labelRotation),y=[];let b,_,x,E,S,w,N,C,k,M,T,L,P="middle";if(s==="top")w=this.bottom-m,N=this._getXAxisLabelAlignment();else if(s==="bottom")w=this.top+m,N=this._getXAxisLabelAlignment();else if(s==="left"){const D=this._getYAxisLabelAlignment(f);N=D.textAlign,S=D.x}else if(s==="right"){const D=this._getYAxisLabelAlignment(f);N=D.textAlign,S=D.x}else if(n==="x"){if(s==="center")w=(e.top+e.bottom)/2+g;else if(Xe(s)){const D=Object.keys(s)[0],A=s[D];w=this.chart.scales[D].getPixelForValue(A)+g}N=this._getXAxisLabelAlignment()}else if(n==="y"){if(s==="center")S=(e.left+e.right)/2-g;else if(Xe(s)){const D=Object.keys(s)[0],A=s[D];S=this.chart.scales[D].getPixelForValue(A)}N=this._getYAxisLabelAlignment(f).textAlign}n==="y"&&(l==="start"?P="top":l==="end"&&(P="bottom"));const F=this._getLabelSizes();for(b=0,_=a.length;b<_;++b){x=a[b],E=x.label;const D=o.setContext(this.getContext(b));C=this.getPixelForTick(b)+o.labelOffset,k=this._resolveTickFontOptions(b),M=k.lineHeight,T=Mt(E)?E.length:1;const A=T/2,O=D.color,V=D.textStrokeColor,H=D.textStrokeWidth;let W=N;i?(S=C,N==="inner"&&(b===_-1?W=this.options.reverse?"left":"right":b===0?W=this.options.reverse?"right":"left":W="center"),s==="top"?c==="near"||p!==0?L=-T*M+M/2:c==="center"?L=-F.highest.height/2-A*M+M:L=-F.highest.height+M/2:c==="near"||p!==0?L=M/2:c==="center"?L=F.highest.height/2-A*M:L=F.highest.height-T*M,u&&(L*=-1),p!==0&&!D.showLabelBackdrop&&(S+=M/2*Math.sin(p))):(w=C,L=(1-T)*M/2);let j;if(D.showLabelBackdrop){const U=yn(D.backdropPadding),B=F.heights[b],G=F.widths[b];let z=L-U.top,X=0-U.left;switch(P){case"middle":z-=B/2;break;case"bottom":z-=B;break}switch(N){case"center":X-=G/2;break;case"right":X-=G;break;case"inner":b===_-1?X-=G:b>0&&(X-=G/2);break}j={left:X,top:z,width:G+U.width,height:B+U.height,color:D.backdropColor}}y.push({label:E,font:k,textOffset:L,options:{rotation:p,color:O,strokeColor:V,strokeWidth:H,textAlign:W,textBaseline:P,translation:[S,w],backdrop:j}})}return y}_getXAxisLabelAlignment(){const{position:e,ticks:n}=this.options;if(-qn(this.labelRotation))return e==="top"?"left":"right";let s="center";return n.align==="start"?s="left":n.align==="end"?s="right":n.align==="inner"&&(s="inner"),s}_getYAxisLabelAlignment(e){const{position:n,ticks:{crossAlign:r,mirror:s,padding:o}}=this.options,i=this._getLabelSizes(),a=e+o,l=i.widest.width;let c,d;return n==="left"?s?(d=this.right+o,r==="near"?c="left":r==="center"?(c="center",d+=l/2):(c="right",d+=l)):(d=this.right-a,r==="near"?c="right":r==="center"?(c="center",d-=l/2):(c="left",d=this.left)):n==="right"?s?(d=this.left+o,r==="near"?c="right":r==="center"?(c="center",d-=l/2):(c="left",d-=l)):(d=this.left+a,r==="near"?c="left":r==="center"?(c="center",d+=l/2):(c="right",d=this.right)):c="right",{textAlign:c,x:d}}_computeLabelArea(){if(this.options.ticks.mirror)return;const e=this.chart,n=this.options.position;if(n==="left"||n==="right")return{top:0,left:this.left,bottom:e.height,right:this.right};if(n==="top"||n==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:e.width}}drawBackground(){const{ctx:e,options:{backgroundColor:n},left:r,top:s,width:o,height:i}=this;n&&(e.save(),e.fillStyle=n,e.fillRect(r,s,o,i),e.restore())}getLineWidthForValue(e){const n=this.options.grid;if(!this._isVisible()||!n.display)return 0;const s=this.ticks.findIndex(o=>o.value===e);return s>=0?n.setContext(this.getContext(s)).lineWidth:0}drawGrid(e){const n=this.options.grid,r=this.ctx,s=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(e));let o,i;const a=(l,c,d)=>{!d.width||!d.color||(r.save(),r.lineWidth=d.width,r.strokeStyle=d.color,r.setLineDash(d.borderDash||[]),r.lineDashOffset=d.borderDashOffset,r.beginPath(),r.moveTo(l.x,l.y),r.lineTo(c.x,c.y),r.stroke(),r.restore())};if(n.display)for(o=0,i=s.length;o<i;++o){const l=s[o];n.drawOnChartArea&&a({x:l.x1,y:l.y1},{x:l.x2,y:l.y2},l),n.drawTicks&&a({x:l.tx1,y:l.ty1},{x:l.tx2,y:l.ty2},{color:l.tickColor,width:l.tickWidth,borderDash:l.tickBorderDash,borderDashOffset:l.tickBorderDashOffset})}}drawBorder(){const{chart:e,ctx:n,options:{border:r,grid:s}}=this,o=r.setContext(this.getContext()),i=r.display?o.width:0;if(!i)return;const a=s.setContext(this.getContext(0)).lineWidth,l=this._borderValue;let c,d,u,f;this.isHorizontal()?(c=Tr(e,this.left,i)-i/2,d=Tr(e,this.right,a)+a/2,u=f=l):(u=Tr(e,this.top,i)-i/2,f=Tr(e,this.bottom,a)+a/2,c=d=l),n.save(),n.lineWidth=o.width,n.strokeStyle=o.color,n.beginPath(),n.moveTo(c,u),n.lineTo(d,f),n.stroke(),n.restore()}drawLabels(e){if(!this.options.ticks.display)return;const r=this.ctx,s=this._computeLabelArea();s&&Jc(r,s);const o=this.getLabelItems(e);for(const i of o){const a=i.options,l=i.font,c=i.label,d=i.textOffset;vo(r,c,0,d,l,a)}s&&Rc(r)}drawTitle(){const{ctx:e,options:{position:n,title:r,reverse:s}}=this;if(!r.display)return;const o=Wt(r.font),i=yn(r.padding),a=r.align;let l=o.lineHeight/2;n==="bottom"||n==="center"||Xe(n)?(l+=i.bottom,Mt(r.text)&&(l+=o.lineHeight*(r.text.length-1))):l+=i.top;const{titleX:c,titleY:d,maxWidth:u,rotation:f}=MO(this,l,n,a);vo(e,r.text,0,0,o,{color:r.color,maxWidth:u,rotation:f,textAlign:EO(a,n,s),textBaseline:"middle",translation:[c,d]})}draw(e){this._isVisible()&&(this.drawBackground(),this.drawGrid(e),this.drawBorder(),this.drawTitle(),this.drawLabels(e))}_layers(){const e=this.options,n=e.ticks&&e.ticks.z||0,r=Ke(e.grid&&e.grid.z,-1),s=Ke(e.border&&e.border.z,0);return!this._isVisible()||this.draw!==vs.prototype.draw?[{z:n,draw:o=>{this.draw(o)}}]:[{z:r,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:s,draw:()=>{this.drawBorder()}},{z:n,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(e){const n=this.chart.getSortedVisibleDatasetMetas(),r=this.axis+"AxisID",s=[];let o,i;for(o=0,i=n.length;o<i;++o){const a=n[o];a[r]===this.id&&(!e||a.type===e)&&s.push(a)}return s}_resolveTickFontOptions(e){const n=this.options.ticks.setContext(this.getContext(e));return Wt(n.font)}_maxDigits(){const e=this._resolveTickFontOptions(0).lineHeight;return(this.isHorizontal()?this.width:this.height)/e}}class ya{constructor(e,n,r){this.type=e,this.scope=n,this.override=r,this.items=Object.create(null)}isForType(e){return Object.prototype.isPrototypeOf.call(this.type.prototype,e.prototype)}register(e){const n=Object.getPrototypeOf(e);let r;TO(n)&&(r=this.register(n));const s=this.items,o=e.id,i=this.scope+"."+o;if(!o)throw new Error("class does not have id: "+e);return o in s||(s[o]=e,NO(e,i,r),this.override&&xt.override(e.id,e.overrides)),i}get(e){return this.items[e]}unregister(e){const n=this.items,r=e.id,s=this.scope;r in n&&delete n[r],s&&r in xt[s]&&(delete xt[s][r],this.override&&delete Or[r])}}function NO(t,e,n){const r=go(Object.create(null),[n?xt.get(n):{},xt.get(e),t.defaults]);xt.set(e,r),t.defaultRoutes&&OO(e,t.defaultRoutes),t.descriptors&&xt.describe(e,t.descriptors)}function OO(t,e){Object.keys(e).forEach(n=>{const r=n.split("."),s=r.pop(),o=[t].concat(r).join("."),i=e[n].split("."),a=i.pop(),l=i.join(".");xt.route(o,s,l,a)})}function TO(t){return"id"in t&&"defaults"in t}class AO{constructor(){this.controllers=new ya(ys,"datasets",!0),this.elements=new ya(Lr,"elements"),this.plugins=new ya(Object,"plugins"),this.scales=new ya(vs,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...e){this._each("register",e)}remove(...e){this._each("unregister",e)}addControllers(...e){this._each("register",e,this.controllers)}addElements(...e){this._each("register",e,this.elements)}addPlugins(...e){this._each("register",e,this.plugins)}addScales(...e){this._each("register",e,this.scales)}getController(e){return this._get(e,this.controllers,"controller")}getElement(e){return this._get(e,this.elements,"element")}getPlugin(e){return this._get(e,this.plugins,"plugin")}getScale(e){return this._get(e,this.scales,"scale")}removeControllers(...e){this._each("unregister",e,this.controllers)}removeElements(...e){this._each("unregister",e,this.elements)}removePlugins(...e){this._each("unregister",e,this.plugins)}removeScales(...e){this._each("unregister",e,this.scales)}_each(e,n,r){[...n].forEach(s=>{const o=r||this._getRegistryForType(s);r||o.isForType(s)||o===this.plugins&&s.id?this._exec(e,o,s):tt(s,i=>{const a=r||this._getRegistryForType(i);this._exec(e,a,i)})})}_exec(e,n,r){const s=Wc(e);ut(r["before"+s],[],r),n[e](r),ut(r["after"+s],[],r)}_getRegistryForType(e){for(let n=0;n<this._typedRegistries.length;n++){const r=this._typedRegistries[n];if(r.isForType(e))return r}return this.plugins}_get(e,n,r){const s=n.get(e);if(s===void 0)throw new Error('"'+e+'" is not a registered '+r+".");return s}}var Un=new AO;class DO{constructor(){this._init=void 0}notify(e,n,r,s){if(n==="beforeInit"&&(this._init=this._createDescriptors(e,!0),this._notify(this._init,e,"install")),this._init===void 0)return;const o=s?this._descriptors(e).filter(s):this._descriptors(e),i=this._notify(o,e,n,r);return n==="afterDestroy"&&(this._notify(o,e,"stop"),this._notify(this._init,e,"uninstall"),this._init=void 0),i}_notify(e,n,r,s){s=s||{};for(const o of e){const i=o.plugin,a=i[r],l=[n,s,o.options];if(ut(a,l,i)===!1&&s.cancelable)return!1}return!0}invalidate(){at(this._cache)||(this._oldCache=this._cache,this._cache=void 0)}_descriptors(e){if(this._cache)return this._cache;const n=this._cache=this._createDescriptors(e);return this._notifyStateChanges(e),n}_createDescriptors(e,n){const r=e&&e.config,s=Ke(r.options&&r.options.plugins,{}),o=IO(r);return s===!1&&!n?[]:PO(e,o,s,n)}_notifyStateChanges(e){const n=this._oldCache||[],r=this._cache,s=(o,i)=>o.filter(a=>!i.some(l=>a.plugin.id===l.plugin.id));this._notify(s(n,r),e,"stop"),this._notify(s(r,n),e,"start")}}function IO(t){const e={},n=[],r=Object.keys(Un.plugins.items);for(let o=0;o<r.length;o++)n.push(Un.getPlugin(r[o]));const s=t.plugins||[];for(let o=0;o<s.length;o++){const i=s[o];n.indexOf(i)===-1&&(n.push(i),e[i.id]=!0)}return{plugins:n,localIds:e}}function LO(t,e){return!e&&t===!1?null:t===!0?{}:t}function PO(t,{plugins:e,localIds:n},r,s){const o=[],i=t.getContext();for(const a of e){const l=a.id,c=LO(r[l],s);c!==null&&o.push({plugin:a,options:$O(t.config,{plugin:a,local:n[l]},c,i)})}return o}function $O(t,{plugin:e,local:n},r,s){const o=t.pluginScopeKeys(e),i=t.getOptionScopes(r,o);return n&&e.defaults&&i.push(e.defaults),t.createResolver(i,s,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function gd(t,e){const n=xt.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||n.indexAxis||"x"}function FO(t,e){let n=t;return t==="_index_"?n=e:t==="_value_"&&(n=e==="x"?"y":"x"),n}function VO(t,e){return t===e?"_index_":"_value_"}function J0(t){if(t==="x"||t==="y"||t==="r")return t}function zO(t){if(t==="top"||t==="bottom")return"x";if(t==="left"||t==="right")return"y"}function pd(t,...e){if(J0(t))return t;for(const n of e){const r=n.axis||zO(n.position)||t.length>1&&J0(t[0].toLowerCase());if(r)return r}throw new Error(`Cannot determine type of '${t}' axis. Please provide 'axis' or 'position' option.`)}function R0(t,e,n){if(n[e+"AxisID"]===t)return{axis:e}}function HO(t,e){if(e.data&&e.data.datasets){const n=e.data.datasets.filter(r=>r.xAxisID===t||r.yAxisID===t);if(n.length)return R0(t,"x",n[0])||R0(t,"y",n[0])}return{}}function UO(t,e){const n=Or[t.type]||{scales:{}},r=e.scales||{},s=gd(t.type,e),o=Object.create(null);return Object.keys(r).forEach(i=>{const a=r[i];if(!Xe(a))return console.error(`Invalid scale configuration for scale: ${i}`);if(a._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${i}`);const l=pd(i,a,HO(i,t),xt.scales[a.type]),c=VO(l,s),d=n.scales||{};o[i]=po(Object.create(null),[{axis:l},a,d[l],d[c]])}),t.data.datasets.forEach(i=>{const a=i.type||t.type,l=i.indexAxis||gd(a,e),d=(Or[a]||{}).scales||{};Object.keys(d).forEach(u=>{const f=FO(u,l),g=i[f+"AxisID"]||f;o[g]=o[g]||Object.create(null),po(o[g],[{axis:f},r[g],d[u]])})}),Object.keys(o).forEach(i=>{const a=o[i];po(a,[xt.scales[a.type],xt.scale])}),o}function em(t){const e=t.options||(t.options={});e.plugins=Ke(e.plugins,{}),e.scales=UO(t,e)}function tm(t){return t=t||{},t.datasets=t.datasets||[],t.labels=t.labels||[],t}function BO(t){return t=t||{},t.data=tm(t.data),em(t),t}const nm=new Map,rm=new Set;function va(t,e){let n=nm.get(t);return n||(n=e(),nm.set(t,n),rm.add(n)),n}const Mo=(t,e,n)=>{const r=Er(e,n);r!==void 0&&t.add(r)};class jO{constructor(e){this._config=BO(e),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(e){this._config.type=e}get data(){return this._config.data}set data(e){this._config.data=tm(e)}get options(){return this._config.options}set options(e){this._config.options=e}get plugins(){return this._config.plugins}update(){const e=this._config;this.clearCache(),em(e)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(e){return va(e,()=>[[`datasets.${e}`,""]])}datasetAnimationScopeKeys(e,n){return va(`${e}.transition.${n}`,()=>[[`datasets.${e}.transitions.${n}`,`transitions.${n}`],[`datasets.${e}`,""]])}datasetElementScopeKeys(e,n){return va(`${e}-${n}`,()=>[[`datasets.${e}.elements.${n}`,`datasets.${e}`,`elements.${n}`,""]])}pluginScopeKeys(e){const n=e.id,r=this.type;return va(`${r}-plugin-${n}`,()=>[[`plugins.${n}`,...e.additionalOptionScopes||[]]])}_cachedScopes(e,n){const r=this._scopeCache;let s=r.get(e);return(!s||n)&&(s=new Map,r.set(e,s)),s}getOptionScopes(e,n,r){const{options:s,type:o}=this,i=this._cachedScopes(e,r),a=i.get(n);if(a)return a;const l=new Set;n.forEach(d=>{e&&(l.add(e),d.forEach(u=>Mo(l,e,u))),d.forEach(u=>Mo(l,s,u)),d.forEach(u=>Mo(l,Or[o]||{},u)),d.forEach(u=>Mo(l,xt,u)),d.forEach(u=>Mo(l,Zc,u))});const c=Array.from(l);return c.length===0&&c.push(Object.create(null)),rm.has(n)&&i.set(n,c),c}chartOptionScopes(){const{options:e,type:n}=this;return[e,Or[n]||{},xt.datasets[n]||{},{type:n},xt,Zc]}resolveNamedOptions(e,n,r,s=[""]){const o={$shared:!0},{resolver:i,subPrefixes:a}=sm(this._resolverCache,e,s);let l=i;if(GO(i,n)){o.$shared=!1,r=cr(r)?r():r;const c=this.createResolver(e,r,a);l=ms(i,r,c)}for(const c of n)o[c]=l[c];return o}createResolver(e,n,r=[""],s){const{resolver:o}=sm(this._resolverCache,e,r);return Xe(n)?ms(o,n,void 0,s):o}}function sm(t,e,n){let r=t.get(e);r||(r=new Map,t.set(e,r));const s=n.join();let o=r.get(s);return o||(o={resolver:td(e,n),subPrefixes:n.filter(a=>!a.toLowerCase().includes("hover"))},r.set(s,o)),o}const WO=t=>Xe(t)&&Object.getOwnPropertyNames(t).some(e=>cr(t[e]));function GO(t,e){const{isScriptable:n,isIndexable:r}=p0(t);for(const s of e){const o=n(s),i=r(s),a=(i||o)&&t[s];if(o&&(cr(a)||WO(a))||i&&Mt(a))return!0}return!1}var YO="4.5.1";const XO=["top","bottom","left","right","chartArea"];function om(t,e){return t==="top"||t==="bottom"||XO.indexOf(t)===-1&&e==="x"}function im(t,e){return function(n,r){return n[t]===r[t]?n[e]-r[e]:n[t]-r[t]}}function am(t){const e=t.chart,n=e.options.animation;e.notifyPlugins("afterRender"),ut(n&&n.onComplete,[t],e)}function KO(t){const e=t.chart,n=e.options.animation;ut(n&&n.onProgress,[t],e)}function lm(t){return sd()&&typeof t=="string"?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const xa={},cm=t=>{const e=lm(t);return Object.values(xa).filter(n=>n.canvas===e).pop()};function qO(t,e,n){const r=Object.keys(t);for(const s of r){const o=+s;if(o>=e){const i=t[s];delete t[s],(n>0||o>e)&&(t[o+n]=i)}}}function ZO(t,e,n,r){return!n||t.type==="mouseout"?null:r?e:t}let No=(hr=class{static register(...e){Un.add(...e),dm()}static unregister(...e){Un.remove(...e),dm()}constructor(e,n){const r=this.config=new jO(n),s=lm(e),o=cm(s);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");const i=r.createResolver(r.chartOptionScopes(),this.getContext());this.platform=new(r.platform||gO(s)),this.platform.updateConfig(r);const a=this.platform.acquireContext(s,i.aspectRatio),l=a&&a.canvas,c=l&&l.height,d=l&&l.width;if(this.id=hM(),this.ctx=a,this.canvas=l,this.width=d,this.height=c,this._options=i,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new DO,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=AM(u=>this.update(u),i.resizeDelay||0),this._dataChanges=[],xa[this.id]=this,!a||!l){console.error("Failed to create chart: can't acquire context from the given item");return}Zn.listen(this,"complete",am),Zn.listen(this,"progress",KO),this._initialize(),this.attached&&this.update()}get aspectRatio(){const{options:{aspectRatio:e,maintainAspectRatio:n},width:r,height:s,_aspectRatio:o}=this;return at(e)?n&&o?o:s?r/s:null:e}get data(){return this.config.data}set data(e){this.config.data=e}get options(){return this._options}set options(e){this.config.options=e}get registry(){return Un}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():w0(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return d0(this.canvas,this.ctx),this}stop(){return Zn.stop(this),this}resize(e,n){Zn.running(this)?this._resizeBeforeDraw={width:e,height:n}:this._resize(e,n)}_resize(e,n){const r=this.options,s=this.canvas,o=r.maintainAspectRatio&&this.aspectRatio,i=this.platform.getMaximumSize(s,e,n,o),a=r.devicePixelRatio||this.platform.getDevicePixelRatio(),l=this.width?"resize":"attach";this.width=i.width,this.height=i.height,this._aspectRatio=this.aspectRatio,w0(this,a,!0)&&(this.notifyPlugins("resize",{size:i}),ut(r.onResize,[this,i],this),this.attached&&this._doResize(l)&&this.render())}ensureScalesHaveIDs(){const n=this.options.scales||{};tt(n,(r,s)=>{r.id=s})}buildOrUpdateScales(){const e=this.options,n=e.scales,r=this.scales,s=Object.keys(r).reduce((i,a)=>(i[a]=!1,i),{});let o=[];n&&(o=o.concat(Object.keys(n).map(i=>{const a=n[i],l=pd(i,a),c=l==="r",d=l==="x";return{options:a,dposition:c?"chartArea":d?"bottom":"left",dtype:c?"radialLinear":d?"category":"linear"}}))),tt(o,i=>{const a=i.options,l=a.id,c=pd(l,a),d=Ke(a.type,i.dtype);(a.position===void 0||om(a.position,c)!==om(i.dposition))&&(a.position=i.dposition),s[l]=!0;let u=null;if(l in r&&r[l].type===d)u=r[l];else{const f=Un.getScale(d);u=new f({id:l,type:d,ctx:this.ctx,chart:this}),r[u.id]=u}u.init(a,e)}),tt(s,(i,a)=>{i||delete r[a]}),tt(r,i=>{vn.configure(this,i,i.options),vn.addBox(this,i)})}_updateMetasets(){const e=this._metasets,n=this.data.datasets.length,r=e.length;if(e.sort((s,o)=>s.index-o.index),r>n){for(let s=n;s<r;++s)this._destroyDatasetMeta(s);e.splice(n,r-n)}this._sortedMetasets=e.slice(0).sort(im("order","index"))}_removeUnreferencedMetasets(){const{_metasets:e,data:{datasets:n}}=this;e.length>n.length&&delete this._stacks,e.forEach((r,s)=>{n.filter(o=>o===r._dataset).length===0&&this._destroyDatasetMeta(s)})}buildOrUpdateControllers(){const e=[],n=this.data.datasets;let r,s;for(this._removeUnreferencedMetasets(),r=0,s=n.length;r<s;r++){const o=n[r];let i=this.getDatasetMeta(r);const a=o.type||this.config.type;if(i.type&&i.type!==a&&(this._destroyDatasetMeta(r),i=this.getDatasetMeta(r)),i.type=a,i.indexAxis=o.indexAxis||gd(a,this.options),i.order=o.order||0,i.index=r,i.label=""+o.label,i.visible=this.isDatasetVisible(r),i.controller)i.controller.updateIndex(r),i.controller.linkScales();else{const l=Un.getController(a),{datasetElementType:c,dataElementType:d}=xt.datasets[a];Object.assign(l,{dataElementType:Un.getElement(d),datasetElementType:c&&Un.getElement(c)}),i.controller=new l(this,r),e.push(i.controller)}}return this._updateMetasets(),e}_resetElements(){tt(this.data.datasets,(e,n)=>{this.getDatasetMeta(n).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(e){const n=this.config;n.update();const r=this._options=n.createResolver(n.chartOptionScopes(),this.getContext()),s=this._animationsDisabled=!r.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:e,cancelable:!0})===!1)return;const o=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let i=0;for(let c=0,d=this.data.datasets.length;c<d;c++){const{controller:u}=this.getDatasetMeta(c),f=!s&&o.indexOf(u)===-1;u.buildOrUpdateElements(f),i=Math.max(+u.getMaxOverflow(),i)}i=this._minPadding=r.layout.autoPadding?i:0,this._updateLayout(i),s||tt(o,c=>{c.reset()}),this._updateDatasets(e),this.notifyPlugins("afterUpdate",{mode:e}),this._layers.sort(im("z","_idx"));const{_active:a,_lastEvent:l}=this;l?this._eventHandler(l,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){tt(this.scales,e=>{vn.removeBox(this,e)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){const e=this.options,n=new Set(Object.keys(this._listeners)),r=new Set(e.events);(!Yp(n,r)||!!this._responsiveListeners!==e.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){const{_hiddenIndices:e}=this,n=this._getUniformDataChanges()||[];for(const{method:r,start:s,count:o}of n){const i=r==="_removeElements"?-o:o;qO(e,s,i)}}_getUniformDataChanges(){const e=this._dataChanges;if(!e||!e.length)return;this._dataChanges=[];const n=this.data.datasets.length,r=o=>new Set(e.filter(i=>i[0]===o).map((i,a)=>a+","+i.splice(1).join(","))),s=r(0);for(let o=1;o<n;o++)if(!Yp(s,r(o)))return;return Array.from(s).map(o=>o.split(",")).map(o=>({method:o[1],start:+o[2],count:+o[3]}))}_updateLayout(e){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;vn.update(this,this.width,this.height,e);const n=this.chartArea,r=n.width<=0||n.height<=0;this._layers=[],tt(this.boxes,s=>{r&&s.position==="chartArea"||(s.configure&&s.configure(),this._layers.push(...s._layers()))},this),this._layers.forEach((s,o)=>{s._idx=o}),this.notifyPlugins("afterLayout")}_updateDatasets(e){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:e,cancelable:!0})!==!1){for(let n=0,r=this.data.datasets.length;n<r;++n)this.getDatasetMeta(n).controller.configure();for(let n=0,r=this.data.datasets.length;n<r;++n)this._updateDataset(n,cr(e)?e({datasetIndex:n}):e);this.notifyPlugins("afterDatasetsUpdate",{mode:e})}}_updateDataset(e,n){const r=this.getDatasetMeta(e),s={meta:r,index:e,mode:n,cancelable:!0};this.notifyPlugins("beforeDatasetUpdate",s)!==!1&&(r.controller._update(n),s.cancelable=!1,this.notifyPlugins("afterDatasetUpdate",s))}render(){this.notifyPlugins("beforeRender",{cancelable:!0})!==!1&&(Zn.has(this)?this.attached&&!Zn.running(this)&&Zn.start(this):(this.draw(),am({chart:this})))}draw(){let e;if(this._resizeBeforeDraw){const{width:r,height:s}=this._resizeBeforeDraw;this._resizeBeforeDraw=null,this._resize(r,s)}if(this.clear(),this.width<=0||this.height<=0||this.notifyPlugins("beforeDraw",{cancelable:!0})===!1)return;const n=this._layers;for(e=0;e<n.length&&n[e].z<=0;++e)n[e].draw(this.chartArea);for(this._drawDatasets();e<n.length;++e)n[e].draw(this.chartArea);this.notifyPlugins("afterDraw")}_getSortedDatasetMetas(e){const n=this._sortedMetasets,r=[];let s,o;for(s=0,o=n.length;s<o;++s){const i=n[s];(!e||i.visible)&&r.push(i)}return r}getSortedVisibleDatasetMetas(){return this._getSortedDatasetMetas(!0)}_drawDatasets(){if(this.notifyPlugins("beforeDatasetsDraw",{cancelable:!0})===!1)return;const e=this.getSortedVisibleDatasetMetas();for(let n=e.length-1;n>=0;--n)this._drawDataset(e[n]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(e){const n=this.ctx,r={meta:e,index:e.index,cancelable:!0},s=mN(this,e);this.notifyPlugins("beforeDatasetDraw",r)!==!1&&(s&&Jc(n,s),e.controller.draw(),s&&Rc(n),r.cancelable=!1,this.notifyPlugins("afterDatasetDraw",r))}isPointInArea(e){return h0(e,this.chartArea,this._minPadding)}getElementsAtEventForMode(e,n,r,s){const o=XN.modes[n];return typeof o=="function"?o(this,e,r,s):[]}getDatasetMeta(e){const n=this.data.datasets[e],r=this._metasets;let s=r.filter(o=>o&&o._dataset===n).pop();return s||(s={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:n&&n.order||0,index:e,_dataset:n,_parsed:[],_sorted:!1},r.push(s)),s}getContext(){return this.$context||(this.$context=ps(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(e){const n=this.data.datasets[e];if(!n)return!1;const r=this.getDatasetMeta(e);return typeof r.hidden=="boolean"?!r.hidden:!n.hidden}setDatasetVisibility(e,n){const r=this.getDatasetMeta(e);r.hidden=!n}toggleDataVisibility(e){this._hiddenIndices[e]=!this._hiddenIndices[e]}getDataVisibility(e){return!this._hiddenIndices[e]}_updateVisibility(e,n,r){const s=r?"show":"hide",o=this.getDatasetMeta(e),i=o.controller._resolveAnimations(void 0,s);mo(n)?(o.data[n].hidden=!r,this.update()):(this.setDatasetVisibility(e,r),i.update(o,{visible:r}),this.update(a=>a.datasetIndex===e?s:void 0))}hide(e,n){this._updateVisibility(e,n,!1)}show(e,n){this._updateVisibility(e,n,!0)}_destroyDatasetMeta(e){const n=this._metasets[e];n&&n.controller&&n.controller._destroy(),delete this._metasets[e]}_stop(){let e,n;for(this.stop(),Zn.remove(this),e=0,n=this.data.datasets.length;e<n;++e)this._destroyDatasetMeta(e)}destroy(){this.notifyPlugins("beforeDestroy");const{canvas:e,ctx:n}=this;this._stop(),this.config.clearCache(),e&&(this.unbindEvents(),d0(e,n),this.platform.releaseContext(n),this.canvas=null,this.ctx=null),delete xa[this.id],this.notifyPlugins("afterDestroy")}toBase64Image(...e){return this.canvas.toDataURL(...e)}bindEvents(){this.bindUserEvents(),this.options.responsive?this.bindResponsiveEvents():this.attached=!0}bindUserEvents(){const e=this._listeners,n=this.platform,r=(o,i)=>{n.addEventListener(this,o,i),e[o]=i},s=(o,i,a)=>{o.offsetX=i,o.offsetY=a,this._eventHandler(o)};tt(this.options.events,o=>r(o,s))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});const e=this._responsiveListeners,n=this.platform,r=(l,c)=>{n.addEventListener(this,l,c),e[l]=c},s=(l,c)=>{e[l]&&(n.removeEventListener(this,l,c),delete e[l])},o=(l,c)=>{this.canvas&&this.resize(l,c)};let i;const a=()=>{s("attach",a),this.attached=!0,this.resize(),r("resize",o),r("detach",i)};i=()=>{this.attached=!1,s("resize",o),this._stop(),this._resize(0,0),r("attach",a)},n.isAttached(this.canvas)?a():i()}unbindEvents(){tt(this._listeners,(e,n)=>{this.platform.removeEventListener(this,n,e)}),this._listeners={},tt(this._responsiveListeners,(e,n)=>{this.platform.removeEventListener(this,n,e)}),this._responsiveListeners=void 0}updateHoverStyle(e,n,r){const s=r?"set":"remove";let o,i,a,l;for(n==="dataset"&&(o=this.getDatasetMeta(e[0].datasetIndex),o.controller["_"+s+"DatasetHoverStyle"]()),a=0,l=e.length;a<l;++a){i=e[a];const c=i&&this.getDatasetMeta(i.datasetIndex).controller;c&&c[s+"HoverStyle"](i.element,i.datasetIndex,i.index)}}getActiveElements(){return this._active||[]}setActiveElements(e){const n=this._active||[],r=e.map(({datasetIndex:o,index:i})=>{const a=this.getDatasetMeta(o);if(!a)throw new Error("No dataset found at index "+o);return{datasetIndex:o,element:a.data[i],index:i}});!ea(r,n)&&(this._active=r,this._lastEvent=null,this._updateHoverStyles(r,n))}notifyPlugins(e,n,r){return this._plugins.notify(this,e,n,r)}isPluginEnabled(e){return this._plugins._cache.filter(n=>n.plugin.id===e).length===1}_updateHoverStyles(e,n,r){const s=this.options.hover,o=(l,c)=>l.filter(d=>!c.some(u=>d.datasetIndex===u.datasetIndex&&d.index===u.index)),i=o(n,e),a=r?e:o(e,n);i.length&&this.updateHoverStyle(i,s.mode,!1),a.length&&s.mode&&this.updateHoverStyle(a,s.mode,!0)}_eventHandler(e,n){const r={event:e,replay:n,cancelable:!0,inChartArea:this.isPointInArea(e)},s=i=>(i.options.events||this.options.events).includes(e.native.type);if(this.notifyPlugins("beforeEvent",r,s)===!1)return;const o=this._handleEvent(e,n,r.inChartArea);return r.cancelable=!1,this.notifyPlugins("afterEvent",r,s),(o||r.changed)&&this.render(),this}_handleEvent(e,n,r){const{_active:s=[],options:o}=this,i=n,a=this._getActiveElements(e,s,r,i),l=vM(e),c=ZO(e,this._lastEvent,r,l);r&&(this._lastEvent=null,ut(o.onHover,[e,a,this],this),l&&ut(o.onClick,[e,a,this],this));const d=!ea(a,s);return(d||n)&&(this._active=a,this._updateHoverStyles(a,s,n)),this._lastEvent=c,d}_getActiveElements(e,n,r,s){if(e.type==="mouseout")return[];if(!r)return n;const o=this.options.hover;return this.getElementsAtEventForMode(e,o.mode,o,s)}},Pe(hr,"defaults",xt),Pe(hr,"instances",xa),Pe(hr,"overrides",Or),Pe(hr,"registry",Un),Pe(hr,"version",YO),Pe(hr,"getChart",cm),hr);function dm(){return tt(No.instances,t=>t._plugins.invalidate())}function QO(t,e,n){const{startAngle:r,x:s,y:o,outerRadius:i,innerRadius:a,options:l}=e,{borderWidth:c,borderJoinStyle:d}=l,u=Math.min(c/i,Hn(r-n));if(t.beginPath(),t.arc(s,o,i-c/2,r+u/2,n-u/2),a>0){const f=Math.min(c/a,Hn(r-n));t.arc(s,o,a+c/2,n-f/2,r+f/2,!0)}else{const f=Math.min(c/2,i*Hn(r-n));if(d==="round")t.arc(s,o,f,n-lt/2,r+lt/2,!0);else if(d==="bevel"){const g=2*f*f,m=-g*Math.cos(n+lt/2)+s,p=-g*Math.sin(n+lt/2)+o,y=g*Math.cos(r+lt/2)+s,b=g*Math.sin(r+lt/2)+o;t.lineTo(m,p),t.lineTo(y,b)}}t.closePath(),t.moveTo(0,0),t.rect(0,0,t.canvas.width,t.canvas.height),t.clip("evenodd")}function JO(t,e,n){const{startAngle:r,pixelMargin:s,x:o,y:i,outerRadius:a,innerRadius:l}=e;let c=s/a;t.beginPath(),t.arc(o,i,a,r-c,n+c),l>s?(c=s/l,t.arc(o,i,l,n+c,r-c,!0)):t.arc(o,i,s,n+Nt,r-Nt),t.closePath(),t.clip()}function RO(t){return ed(t,["outerStart","outerEnd","innerStart","innerEnd"])}function e5(t,e,n,r){const s=RO(t.options.borderRadius),o=(n-e)/2,i=Math.min(o,r*e/2),a=l=>{const c=(n-Math.min(o,l))*r/2;return nn(l,0,Math.min(o,c))};return{outerStart:a(s.outerStart),outerEnd:a(s.outerEnd),innerStart:nn(s.innerStart,0,i),innerEnd:nn(s.innerEnd,0,i)}}function xs(t,e,n,r){return{x:n+t*Math.cos(e),y:r+t*Math.sin(e)}}function wa(t,e,n,r,s,o){const{x:i,y:a,startAngle:l,pixelMargin:c,innerRadius:d}=e,u=Math.max(e.outerRadius+r+n-c,0),f=d>0?d+r+n+c:0;let g=0;const m=s-l;if(r){const D=d>0?d-r:0,A=u>0?u-r:0,O=(D+A)/2,V=O!==0?m*O/(O+r):m;g=(m-V)/2}const p=Math.max(.001,m*u-n/lt)/u,y=(m-p)/2,b=l+y+g,_=s-y-g,{outerStart:x,outerEnd:E,innerStart:S,innerEnd:w}=e5(e,f,u,_-b),N=u-x,C=u-E,k=b+x/N,M=_-E/C,T=f+S,L=f+w,P=b+S/T,F=_-w/L;if(t.beginPath(),o){const D=(k+M)/2;if(t.arc(i,a,u,k,D),t.arc(i,a,u,D,M),E>0){const H=xs(C,M,i,a);t.arc(H.x,H.y,E,M,_+Nt)}const A=xs(L,_,i,a);if(t.lineTo(A.x,A.y),w>0){const H=xs(L,F,i,a);t.arc(H.x,H.y,w,_+Nt,F+Math.PI)}const O=(_-w/f+(b+S/f))/2;if(t.arc(i,a,f,_-w/f,O,!0),t.arc(i,a,f,O,b+S/f,!0),S>0){const H=xs(T,P,i,a);t.arc(H.x,H.y,S,P+Math.PI,b-Nt)}const V=xs(N,b,i,a);if(t.lineTo(V.x,V.y),x>0){const H=xs(N,k,i,a);t.arc(H.x,H.y,x,b-Nt,k)}}else{t.moveTo(i,a);const D=Math.cos(k)*u+i,A=Math.sin(k)*u+a;t.lineTo(D,A);const O=Math.cos(M)*u+i,V=Math.sin(M)*u+a;t.lineTo(O,V)}t.closePath()}function t5(t,e,n,r,s){const{fullCircles:o,startAngle:i,circumference:a}=e;let l=e.endAngle;if(o){wa(t,e,n,r,l,s);for(let c=0;c<o;++c)t.fill();isNaN(a)||(l=i+(a%Ct||Ct))}return wa(t,e,n,r,l,s),t.fill(),l}function n5(t,e,n,r,s){const{fullCircles:o,startAngle:i,circumference:a,options:l}=e,{borderWidth:c,borderJoinStyle:d,borderDash:u,borderDashOffset:f,borderRadius:g}=l,m=l.borderAlign==="inner";if(!c)return;t.setLineDash(u||[]),t.lineDashOffset=f,m?(t.lineWidth=c*2,t.lineJoin=d||"round"):(t.lineWidth=c,t.lineJoin=d||"bevel");let p=e.endAngle;if(o){wa(t,e,n,r,p,s);for(let y=0;y<o;++y)t.stroke();isNaN(a)||(p=i+(a%Ct||Ct))}m&&JO(t,e,p),l.selfJoin&&p-i>=lt&&g===0&&d!=="miter"&&QO(t,e,p),o||(wa(t,e,n,r,p,s),t.stroke())}class Oo extends Lr{constructor(n){super();Pe(this,"circumference");Pe(this,"endAngle");Pe(this,"fullCircles");Pe(this,"innerRadius");Pe(this,"outerRadius");Pe(this,"pixelMargin");Pe(this,"startAngle");this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,n&&Object.assign(this,n)}inRange(n,r,s){const o=this.getProps(["x","y"],s),{angle:i,distance:a}=Qp(o,{x:n,y:r}),{startAngle:l,endAngle:c,innerRadius:d,outerRadius:u,circumference:f}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],s),g=(this.options.spacing+this.options.borderWidth)/2,m=Ke(f,c-l),p=oa(i,l,c)&&l!==c,y=m>=Ct||p,b=Nr(a,d+g,u+g);return y&&b}getCenterPoint(n){const{x:r,y:s,startAngle:o,endAngle:i,innerRadius:a,outerRadius:l}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],n),{offset:c,spacing:d}=this.options,u=(o+i)/2,f=(a+l+d+c)/2;return{x:r+Math.cos(u)*f,y:s+Math.sin(u)*f}}tooltipPosition(n){return this.getCenterPoint(n)}draw(n){const{options:r,circumference:s}=this,o=(r.offset||0)/4,i=(r.spacing||0)/2,a=r.circular;if(this.pixelMargin=r.borderAlign==="inner"?.33:0,this.fullCircles=s>Ct?Math.floor(s/Ct):0,s===0||this.innerRadius<0||this.outerRadius<0)return;n.save();const l=(this.startAngle+this.endAngle)/2;n.translate(Math.cos(l)*o,Math.sin(l)*o);const c=1-Math.sin(Math.min(lt,s||0)),d=o*c;n.fillStyle=r.backgroundColor,n.strokeStyle=r.borderColor,t5(n,this,d,i,a),n5(n,this,d,i,a),n.restore()}}Pe(Oo,"id","arc"),Pe(Oo,"defaults",{borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),Pe(Oo,"defaultRoutes",{backgroundColor:"backgroundColor"}),Pe(Oo,"descriptors",{_scriptable:!0,_indexable:n=>n!=="borderDash"});function um(t,e){const{x:n,y:r,base:s,width:o,height:i}=t.getProps(["x","y","base","width","height"],e);let a,l,c,d,u;return t.horizontal?(u=i/2,a=Math.min(n,s),l=Math.max(n,s),c=r-u,d=r+u):(u=o/2,a=n-u,l=n+u,c=Math.min(r,s),d=Math.max(r,s)),{left:a,top:c,right:l,bottom:d}}function fr(t,e,n,r){return t?0:nn(e,n,r)}function r5(t,e,n){const r=t.options.borderWidth,s=t.borderSkipped,o=g0(r);return{t:fr(s.top,o.top,0,n),r:fr(s.right,o.right,0,e),b:fr(s.bottom,o.bottom,0,n),l:fr(s.left,o.left,0,e)}}function s5(t,e,n){const{enableBorderRadius:r}=t.getProps(["enableBorderRadius"]),s=t.options.borderRadius,o=gs(s),i=Math.min(e,n),a=t.borderSkipped,l=r||Xe(s);return{topLeft:fr(!l||a.top||a.left,o.topLeft,0,i),topRight:fr(!l||a.top||a.right,o.topRight,0,i),bottomLeft:fr(!l||a.bottom||a.left,o.bottomLeft,0,i),bottomRight:fr(!l||a.bottom||a.right,o.bottomRight,0,i)}}function o5(t){const e=um(t),n=e.right-e.left,r=e.bottom-e.top,s=r5(t,n/2,r/2),o=s5(t,n/2,r/2);return{outer:{x:e.left,y:e.top,w:n,h:r,radius:o},inner:{x:e.left+s.l,y:e.top+s.t,w:n-s.l-s.r,h:r-s.t-s.b,radius:{topLeft:Math.max(0,o.topLeft-Math.max(s.t,s.l)),topRight:Math.max(0,o.topRight-Math.max(s.t,s.r)),bottomLeft:Math.max(0,o.bottomLeft-Math.max(s.b,s.l)),bottomRight:Math.max(0,o.bottomRight-Math.max(s.b,s.r))}}}}function md(t,e,n,r){const s=e===null,o=n===null,a=t&&!(s&&o)&&um(t,r);return a&&(s||Nr(e,a.left,a.right))&&(o||Nr(n,a.top,a.bottom))}function i5(t){return t.topLeft||t.topRight||t.bottomLeft||t.bottomRight}function a5(t,e){t.rect(e.x,e.y,e.w,e.h)}function bd(t,e,n={}){const r=t.x!==n.x?-e:0,s=t.y!==n.y?-e:0,o=(t.x+t.w!==n.x+n.w?e:0)-r,i=(t.y+t.h!==n.y+n.h?e:0)-s;return{x:t.x+r,y:t.y+s,w:t.w+o,h:t.h+i,radius:t.radius}}class ka extends Lr{constructor(e){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,this.inflateAmount=void 0,e&&Object.assign(this,e)}draw(e){const{inflateAmount:n,options:{borderColor:r,backgroundColor:s}}=this,{inner:o,outer:i}=o5(this),a=i5(i.radius)?aa:a5;e.save(),(i.w!==o.w||i.h!==o.h)&&(e.beginPath(),a(e,bd(i,n,o)),e.clip(),a(e,bd(o,-n,i)),e.fillStyle=r,e.fill("evenodd")),e.beginPath(),a(e,bd(o,n)),e.fillStyle=s,e.fill(),e.restore()}inRange(e,n,r){return md(this,e,n,r)}inXRange(e,n){return md(this,e,null,n)}inYRange(e,n){return md(this,null,e,n)}getCenterPoint(e){const{x:n,y:r,base:s,horizontal:o}=this.getProps(["x","y","base","horizontal"],e);return{x:o?(n+s)/2:n,y:o?r:(r+s)/2}}getRange(e){return e==="x"?this.width/2:this.height/2}}Pe(ka,"id","bar"),Pe(ka,"defaults",{borderSkipped:"start",borderWidth:0,borderRadius:0,inflateAmount:"auto",pointStyle:void 0}),Pe(ka,"defaultRoutes",{backgroundColor:"backgroundColor",borderColor:"borderColor"});const fm=(t,e)=>{let{boxHeight:n=e,boxWidth:r=e}=t;return t.usePointStyle&&(n=Math.min(n,e),r=t.pointStyleWidth||Math.min(r,e)),{boxWidth:r,boxHeight:n,itemHeight:Math.max(e,n)}},l5=(t,e)=>t!==null&&e!==null&&t.datasetIndex===e.datasetIndex&&t.index===e.index;class hm extends Lr{constructor(e){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=e.chart,this.options=e.options,this.ctx=e.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(e,n,r){this.maxWidth=e,this.maxHeight=n,this._margins=r,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){const e=this.options.labels||{};let n=ut(e.generateLabels,[this.chart],this)||[];e.filter&&(n=n.filter(r=>e.filter(r,this.chart.data))),e.sort&&(n=n.sort((r,s)=>e.sort(r,s,this.chart.data))),this.options.reverse&&n.reverse(),this.legendItems=n}fit(){const{options:e,ctx:n}=this;if(!e.display){this.width=this.height=0;return}const r=e.labels,s=Wt(r.font),o=s.size,i=this._computeTitleHeight(),{boxWidth:a,itemHeight:l}=fm(r,o);let c,d;n.font=s.string,this.isHorizontal()?(c=this.maxWidth,d=this._fitRows(i,o,a,l)+10):(d=this.maxHeight,c=this._fitCols(i,s,a,l)+10),this.width=Math.min(c,e.maxWidth||this.maxWidth),this.height=Math.min(d,e.maxHeight||this.maxHeight)}_fitRows(e,n,r,s){const{ctx:o,maxWidth:i,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],d=s+a;let u=e;o.textAlign="left",o.textBaseline="middle";let f=-1,g=-d;return this.legendItems.forEach((m,p)=>{const y=r+n/2+o.measureText(m.text).width;(p===0||c[c.length-1]+y+2*a>i)&&(u+=d,c[c.length-(p>0?0:1)]=0,g+=d,f++),l[p]={left:0,top:g,row:f,width:y,height:s},c[c.length-1]+=y+a}),u}_fitCols(e,n,r,s){const{ctx:o,maxHeight:i,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],d=i-e;let u=a,f=0,g=0,m=0,p=0;return this.legendItems.forEach((y,b)=>{const{itemWidth:_,itemHeight:x}=c5(r,n,o,y,s);b>0&&g+x+2*a>d&&(u+=f+a,c.push({width:f,height:g}),m+=f+a,p++,f=g=0),l[b]={left:m,top:g,col:p,width:_,height:x},f=Math.max(f,_),g+=x+a}),u+=f,c.push({width:f,height:g}),u}adjustHitBoxes(){if(!this.options.display)return;const e=this._computeTitleHeight(),{legendHitBoxes:n,options:{align:r,labels:{padding:s},rtl:o}}=this,i=bs(o,this.left,this.width);if(this.isHorizontal()){let a=0,l=jt(r,this.left+s,this.right-this.lineWidths[a]);for(const c of n)a!==c.row&&(a=c.row,l=jt(r,this.left+s,this.right-this.lineWidths[a])),c.top+=this.top+e+s,c.left=i.leftForLtr(i.x(l),c.width),l+=c.width+s}else{let a=0,l=jt(r,this.top+e+s,this.bottom-this.columnSizes[a].height);for(const c of n)c.col!==a&&(a=c.col,l=jt(r,this.top+e+s,this.bottom-this.columnSizes[a].height)),c.top=l,c.left+=this.left+s,c.left=i.leftForLtr(i.x(c.left),c.width),l+=c.height+s}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){const e=this.ctx;Jc(e,this),this._draw(),Rc(e)}}_draw(){const{options:e,columnSizes:n,lineWidths:r,ctx:s}=this,{align:o,labels:i}=e,a=xt.color,l=bs(e.rtl,this.left,this.width),c=Wt(i.font),{padding:d}=i,u=c.size,f=u/2;let g;this.drawTitle(),s.textAlign=l.textAlign("left"),s.textBaseline="middle",s.lineWidth=.5,s.font=c.string;const{boxWidth:m,boxHeight:p,itemHeight:y}=fm(i,u),b=function(w,N,C){if(isNaN(m)||m<=0||isNaN(p)||p<0)return;s.save();const k=Ke(C.lineWidth,1);if(s.fillStyle=Ke(C.fillStyle,a),s.lineCap=Ke(C.lineCap,"butt"),s.lineDashOffset=Ke(C.lineDashOffset,0),s.lineJoin=Ke(C.lineJoin,"miter"),s.lineWidth=k,s.strokeStyle=Ke(C.strokeStyle,a),s.setLineDash(Ke(C.lineDash,[])),i.usePointStyle){const M={radius:p*Math.SQRT2/2,pointStyle:C.pointStyle,rotation:C.rotation,borderWidth:k},T=l.xPlus(w,m/2),L=N+f;f0(s,M,T,L,i.pointStyleWidth&&m)}else{const M=N+Math.max((u-p)/2,0),T=l.leftForLtr(w,m),L=gs(C.borderRadius);s.beginPath(),Object.values(L).some(P=>P!==0)?aa(s,{x:T,y:M,w:m,h:p,radius:L}):s.rect(T,M,m,p),s.fill(),k!==0&&s.stroke()}s.restore()},_=function(w,N,C){vo(s,C.text,w,N+y/2,c,{strikethrough:C.hidden,textAlign:l.textAlign(C.textAlign)})},x=this.isHorizontal(),E=this._computeTitleHeight();x?g={x:jt(o,this.left+d,this.right-r[0]),y:this.top+d+E,line:0}:g={x:this.left+d,y:jt(o,this.top+E+d,this.bottom-n[0].height),line:0},C0(this.ctx,e.textDirection);const S=y+d;this.legendItems.forEach((w,N)=>{s.strokeStyle=w.fontColor,s.fillStyle=w.fontColor;const C=s.measureText(w.text).width,k=l.textAlign(w.textAlign||(w.textAlign=i.textAlign)),M=m+f+C;let T=g.x,L=g.y;l.setWidth(this.width),x?N>0&&T+M+d>this.right&&(L=g.y+=S,g.line++,T=g.x=jt(o,this.left+d,this.right-r[g.line])):N>0&&L+S>this.bottom&&(T=g.x=T+n[g.line].width+d,g.line++,L=g.y=jt(o,this.top+E+d,this.bottom-n[g.line].height));const P=l.x(T);if(b(P,L,w),T=DM(k,T+m+f,x?T+M:this.right,e.rtl),_(l.x(T),L,w),x)g.x+=M+d;else if(typeof w.text!="string"){const F=c.lineHeight;g.y+=gm(w,F)+d}else g.y+=S}),S0(this.ctx,e.textDirection)}drawTitle(){const e=this.options,n=e.title,r=Wt(n.font),s=yn(n.padding);if(!n.display)return;const o=bs(e.rtl,this.left,this.width),i=this.ctx,a=n.position,l=r.size/2,c=s.top+l;let d,u=this.left,f=this.width;if(this.isHorizontal())f=Math.max(...this.lineWidths),d=this.top+c,u=jt(e.align,u,this.right-f);else{const m=this.columnSizes.reduce((p,y)=>Math.max(p,y.height),0);d=c+jt(e.align,this.top,this.bottom-m-e.labels.padding-this._computeTitleHeight())}const g=jt(a,u,u+f);i.textAlign=o.textAlign(Xc(a)),i.textBaseline="middle",i.strokeStyle=n.color,i.fillStyle=n.color,i.font=r.string,vo(i,n.text,g,d,r)}_computeTitleHeight(){const e=this.options.title,n=Wt(e.font),r=yn(e.padding);return e.display?n.lineHeight+r.height:0}_getLegendItemAt(e,n){let r,s,o;if(Nr(e,this.left,this.right)&&Nr(n,this.top,this.bottom)){for(o=this.legendHitBoxes,r=0;r<o.length;++r)if(s=o[r],Nr(e,s.left,s.left+s.width)&&Nr(n,s.top,s.top+s.height))return this.legendItems[r]}return null}handleEvent(e){const n=this.options;if(!f5(e.type,n))return;const r=this._getLegendItemAt(e.x,e.y);if(e.type==="mousemove"||e.type==="mouseout"){const s=this._hoveredItem,o=l5(s,r);s&&!o&&ut(n.onLeave,[e,s,this],this),this._hoveredItem=r,r&&!o&&ut(n.onHover,[e,r,this],this)}else r&&ut(n.onClick,[e,r,this],this)}}function c5(t,e,n,r,s){const o=d5(r,t,e,n),i=u5(s,r,e.lineHeight);return{itemWidth:o,itemHeight:i}}function d5(t,e,n,r){let s=t.text;return s&&typeof s!="string"&&(s=s.reduce((o,i)=>o.length>i.length?o:i)),e+n.size/2+r.measureText(s).width}function u5(t,e,n){let r=t;return typeof e.text!="string"&&(r=gm(e,n)),r}function gm(t,e){const n=t.text?t.text.length:0;return e*n}function f5(t,e){return!!((t==="mousemove"||t==="mouseout")&&(e.onHover||e.onLeave)||e.onClick&&(t==="click"||t==="mouseup"))}var pm={id:"legend",_element:hm,start(t,e,n){const r=t.legend=new hm({ctx:t.ctx,options:n,chart:t});vn.configure(t,r,n),vn.addBox(t,r)},stop(t){vn.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,n){const r=t.legend;vn.configure(t,r,n),r.options=n},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,n){const r=e.datasetIndex,s=n.chart;s.isDatasetVisible(r)?(s.hide(r),e.hidden=!0):(s.show(r),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:n,pointStyle:r,textAlign:s,color:o,useBorderRadius:i,borderRadius:a}}=t.legend.options;return t._getSortedDatasetMetas().map(l=>{const c=l.controller.getStyle(n?0:void 0),d=yn(c.borderWidth);return{text:e[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(d.width+d.height)/4,strokeStyle:c.borderColor,pointStyle:r||c.pointStyle,rotation:c.rotation,textAlign:s||c.textAlign,borderRadius:i&&(a||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class mm extends Lr{constructor(e){super(),this.chart=e.chart,this.options=e.options,this.ctx=e.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(e,n){const r=this.options;if(this.left=0,this.top=0,!r.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=e,this.height=this.bottom=n;const s=Mt(r.text)?r.text.length:1;this._padding=yn(r.padding);const o=s*Wt(r.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){const e=this.options.position;return e==="top"||e==="bottom"}_drawArgs(e){const{top:n,left:r,bottom:s,right:o,options:i}=this,a=i.align;let l=0,c,d,u;return this.isHorizontal()?(d=jt(a,r,o),u=n+e,c=o-r):(i.position==="left"?(d=r+e,u=jt(a,s,n),l=lt*-.5):(d=o-e,u=jt(a,n,s),l=lt*.5),c=s-n),{titleX:d,titleY:u,maxWidth:c,rotation:l}}draw(){const e=this.ctx,n=this.options;if(!n.display)return;const r=Wt(n.font),o=r.lineHeight/2+this._padding.top,{titleX:i,titleY:a,maxWidth:l,rotation:c}=this._drawArgs(o);vo(e,n.text,0,0,r,{color:n.color,maxWidth:l,rotation:c,textAlign:Xc(n.align),textBaseline:"middle",translation:[i,a]})}}function h5(t,e){const n=new mm({ctx:t.ctx,options:e,chart:t});vn.configure(t,n,e),vn.addBox(t,n),t.titleBlock=n}var g5={id:"title",_element:mm,start(t,e,n){h5(t,n)},stop(t){const e=t.titleBlock;vn.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,n){const r=t.titleBlock;vn.configure(t,r,n),r.options=n},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const To={average(t){if(!t.length)return!1;let e,n,r=new Set,s=0,o=0;for(e=0,n=t.length;e<n;++e){const a=t[e].element;if(a&&a.hasValue()){const l=a.tooltipPosition();r.add(l.x),s+=l.y,++o}}return o===0||r.size===0?!1:{x:[...r].reduce((a,l)=>a+l)/r.size,y:s/o}},nearest(t,e){if(!t.length)return!1;let n=e.x,r=e.y,s=Number.POSITIVE_INFINITY,o,i,a;for(o=0,i=t.length;o<i;++o){const l=t[o].element;if(l&&l.hasValue()){const c=l.getCenterPoint(),d=EM(e,c);d<s&&(s=d,a=l)}}if(a){const l=a.tooltipPosition();n=l.x,r=l.y}return{x:n,y:r}}};function Bn(t,e){return e&&(Mt(e)?Array.prototype.push.apply(t,e):t.push(e)),t}function Qn(t){return(typeof t=="string"||t instanceof String)&&t.indexOf(` +`)>-1?t.split(` +`):t}function p5(t,e){const{element:n,datasetIndex:r,index:s}=e,o=t.getDatasetMeta(r).controller,{label:i,value:a}=o.getLabelAndValue(s);return{chart:t,label:i,parsed:o.getParsed(s),raw:t.data.datasets[r].data[s],formattedValue:a,dataset:o.getDataset(),dataIndex:s,datasetIndex:r,element:n}}function bm(t,e){const n=t.chart.ctx,{body:r,footer:s,title:o}=t,{boxWidth:i,boxHeight:a}=e,l=Wt(e.bodyFont),c=Wt(e.titleFont),d=Wt(e.footerFont),u=o.length,f=s.length,g=r.length,m=yn(e.padding);let p=m.height,y=0,b=r.reduce((E,S)=>E+S.before.length+S.lines.length+S.after.length,0);if(b+=t.beforeBody.length+t.afterBody.length,u&&(p+=u*c.lineHeight+(u-1)*e.titleSpacing+e.titleMarginBottom),b){const E=e.displayColors?Math.max(a,l.lineHeight):l.lineHeight;p+=g*E+(b-g)*l.lineHeight+(b-1)*e.bodySpacing}f&&(p+=e.footerMarginTop+f*d.lineHeight+(f-1)*e.footerSpacing);let _=0;const x=function(E){y=Math.max(y,n.measureText(E).width+_)};return n.save(),n.font=c.string,tt(t.title,x),n.font=l.string,tt(t.beforeBody.concat(t.afterBody),x),_=e.displayColors?i+2+e.boxPadding:0,tt(r,E=>{tt(E.before,x),tt(E.lines,x),tt(E.after,x)}),_=0,n.font=d.string,tt(t.footer,x),n.restore(),y+=m.width,{width:y,height:p}}function m5(t,e){const{y:n,height:r}=e;return n<r/2?"top":n>t.height-r/2?"bottom":"center"}function b5(t,e,n,r){const{x:s,width:o}=r,i=n.caretSize+n.caretPadding;if(t==="left"&&s+o+i>e.width||t==="right"&&s-o-i<0)return!0}function y5(t,e,n,r){const{x:s,width:o}=n,{width:i,chartArea:{left:a,right:l}}=t;let c="center";return r==="center"?c=s<=(a+l)/2?"left":"right":s<=o/2?c="left":s>=i-o/2&&(c="right"),b5(c,t,e,n)&&(c="center"),c}function ym(t,e,n){const r=n.yAlign||e.yAlign||m5(t,n);return{xAlign:n.xAlign||e.xAlign||y5(t,e,n,r),yAlign:r}}function v5(t,e){let{x:n,width:r}=t;return e==="right"?n-=r:e==="center"&&(n-=r/2),n}function x5(t,e,n){let{y:r,height:s}=t;return e==="top"?r+=n:e==="bottom"?r-=s+n:r-=s/2,r}function vm(t,e,n,r){const{caretSize:s,caretPadding:o,cornerRadius:i}=t,{xAlign:a,yAlign:l}=n,c=s+o,{topLeft:d,topRight:u,bottomLeft:f,bottomRight:g}=gs(i);let m=v5(e,a);const p=x5(e,l,c);return l==="center"?a==="left"?m+=c:a==="right"&&(m-=c):a==="left"?m-=Math.max(d,f)+s:a==="right"&&(m+=Math.max(u,g)+s),{x:nn(m,0,r.width-e.width),y:nn(p,0,r.height-e.height)}}function Ca(t,e,n){const r=yn(n.padding);return e==="center"?t.x+t.width/2:e==="right"?t.x+t.width-r.right:t.x+r.left}function xm(t){return Bn([],Qn(t))}function w5(t,e,n){return ps(t,{tooltip:e,tooltipItems:n,type:"tooltip"})}function wm(t,e){const n=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return n?t.override(n):t}const km={beforeTitle:Kn,title(t){if(t.length>0){const e=t[0],n=e.chart.data.labels,r=n?n.length:0;if(this&&this.options&&this.options.mode==="dataset")return e.dataset.label||"";if(e.label)return e.label;if(r>0&&e.dataIndex<r)return n[e.dataIndex]}return""},afterTitle:Kn,beforeBody:Kn,beforeLabel:Kn,label(t){if(this&&this.options&&this.options.mode==="dataset")return t.label+": "+t.formattedValue||t.formattedValue;let e=t.dataset.label||"";e&&(e+=": ");const n=t.formattedValue;return at(n)||(e+=n),e},labelColor(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{borderColor:n.borderColor,backgroundColor:n.backgroundColor,borderWidth:n.borderWidth,borderDash:n.borderDash,borderDashOffset:n.borderDashOffset,borderRadius:0}},labelTextColor(){return this.options.bodyColor},labelPointStyle(t){const n=t.chart.getDatasetMeta(t.datasetIndex).controller.getStyle(t.dataIndex);return{pointStyle:n.pointStyle,rotation:n.rotation}},afterLabel:Kn,afterBody:Kn,beforeFooter:Kn,footer:Kn,afterFooter:Kn};function rn(t,e,n,r){const s=t[e].call(n,r);return typeof s>"u"?km[e].call(n,r):s}class yd extends Lr{constructor(e){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=e.chart,this.options=e.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(e){this.options=e,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const e=this._cachedAnimations;if(e)return e;const n=this.chart,r=this.options.setContext(this.getContext()),s=r.enabled&&n.options.animation&&r.animations,o=new E0(this.chart,s);return s._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=w5(this.chart.getContext(),this,this._tooltipItems))}getTitle(e,n){const{callbacks:r}=n,s=rn(r,"beforeTitle",this,e),o=rn(r,"title",this,e),i=rn(r,"afterTitle",this,e);let a=[];return a=Bn(a,Qn(s)),a=Bn(a,Qn(o)),a=Bn(a,Qn(i)),a}getBeforeBody(e,n){return xm(rn(n.callbacks,"beforeBody",this,e))}getBody(e,n){const{callbacks:r}=n,s=[];return tt(e,o=>{const i={before:[],lines:[],after:[]},a=wm(r,o);Bn(i.before,Qn(rn(a,"beforeLabel",this,o))),Bn(i.lines,rn(a,"label",this,o)),Bn(i.after,Qn(rn(a,"afterLabel",this,o))),s.push(i)}),s}getAfterBody(e,n){return xm(rn(n.callbacks,"afterBody",this,e))}getFooter(e,n){const{callbacks:r}=n,s=rn(r,"beforeFooter",this,e),o=rn(r,"footer",this,e),i=rn(r,"afterFooter",this,e);let a=[];return a=Bn(a,Qn(s)),a=Bn(a,Qn(o)),a=Bn(a,Qn(i)),a}_createItems(e){const n=this._active,r=this.chart.data,s=[],o=[],i=[];let a=[],l,c;for(l=0,c=n.length;l<c;++l)a.push(p5(this.chart,n[l]));return e.filter&&(a=a.filter((d,u,f)=>e.filter(d,u,f,r))),e.itemSort&&(a=a.sort((d,u)=>e.itemSort(d,u,r))),tt(a,d=>{const u=wm(e.callbacks,d);s.push(rn(u,"labelColor",this,d)),o.push(rn(u,"labelPointStyle",this,d)),i.push(rn(u,"labelTextColor",this,d))}),this.labelColors=s,this.labelPointStyles=o,this.labelTextColors=i,this.dataPoints=a,a}update(e,n){const r=this.options.setContext(this.getContext()),s=this._active;let o,i=[];if(!s.length)this.opacity!==0&&(o={opacity:0});else{const a=To[r.position].call(this,s,this._eventPosition);i=this._createItems(r),this.title=this.getTitle(i,r),this.beforeBody=this.getBeforeBody(i,r),this.body=this.getBody(i,r),this.afterBody=this.getAfterBody(i,r),this.footer=this.getFooter(i,r);const l=this._size=bm(this,r),c=Object.assign({},a,l),d=ym(this.chart,r,c),u=vm(r,c,d,this.chart);this.xAlign=d.xAlign,this.yAlign=d.yAlign,o={opacity:1,x:u.x,y:u.y,width:l.width,height:l.height,caretX:a.x,caretY:a.y}}this._tooltipItems=i,this.$context=void 0,o&&this._resolveAnimations().update(this,o),e&&r.external&&r.external.call(this,{chart:this.chart,tooltip:this,replay:n})}drawCaret(e,n,r,s){const o=this.getCaretPosition(e,r,s);n.lineTo(o.x1,o.y1),n.lineTo(o.x2,o.y2),n.lineTo(o.x3,o.y3)}getCaretPosition(e,n,r){const{xAlign:s,yAlign:o}=this,{caretSize:i,cornerRadius:a}=r,{topLeft:l,topRight:c,bottomLeft:d,bottomRight:u}=gs(a),{x:f,y:g}=e,{width:m,height:p}=n;let y,b,_,x,E,S;return o==="center"?(E=g+p/2,s==="left"?(y=f,b=y-i,x=E+i,S=E-i):(y=f+m,b=y+i,x=E-i,S=E+i),_=y):(s==="left"?b=f+Math.max(l,d)+i:s==="right"?b=f+m-Math.max(c,u)-i:b=this.caretX,o==="top"?(x=g,E=x-i,y=b-i,_=b+i):(x=g+p,E=x+i,y=b+i,_=b-i),S=x),{x1:y,x2:b,x3:_,y1:x,y2:E,y3:S}}drawTitle(e,n,r){const s=this.title,o=s.length;let i,a,l;if(o){const c=bs(r.rtl,this.x,this.width);for(e.x=Ca(this,r.titleAlign,r),n.textAlign=c.textAlign(r.titleAlign),n.textBaseline="middle",i=Wt(r.titleFont),a=r.titleSpacing,n.fillStyle=r.titleColor,n.font=i.string,l=0;l<o;++l)n.fillText(s[l],c.x(e.x),e.y+i.lineHeight/2),e.y+=i.lineHeight+a,l+1===o&&(e.y+=r.titleMarginBottom-a)}}_drawColorBox(e,n,r,s,o){const i=this.labelColors[r],a=this.labelPointStyles[r],{boxHeight:l,boxWidth:c}=o,d=Wt(o.bodyFont),u=Ca(this,"left",o),f=s.x(u),g=l<d.lineHeight?(d.lineHeight-l)/2:0,m=n.y+g;if(o.usePointStyle){const p={radius:Math.min(c,l)/2,pointStyle:a.pointStyle,rotation:a.rotation,borderWidth:1},y=s.leftForLtr(f,c)+c/2,b=m+l/2;e.strokeStyle=o.multiKeyBackground,e.fillStyle=o.multiKeyBackground,u0(e,p,y,b),e.strokeStyle=i.borderColor,e.fillStyle=i.backgroundColor,u0(e,p,y,b)}else{e.lineWidth=Xe(i.borderWidth)?Math.max(...Object.values(i.borderWidth)):i.borderWidth||1,e.strokeStyle=i.borderColor,e.setLineDash(i.borderDash||[]),e.lineDashOffset=i.borderDashOffset||0;const p=s.leftForLtr(f,c),y=s.leftForLtr(s.xPlus(f,1),c-2),b=gs(i.borderRadius);Object.values(b).some(_=>_!==0)?(e.beginPath(),e.fillStyle=o.multiKeyBackground,aa(e,{x:p,y:m,w:c,h:l,radius:b}),e.fill(),e.stroke(),e.fillStyle=i.backgroundColor,e.beginPath(),aa(e,{x:y,y:m+1,w:c-2,h:l-2,radius:b}),e.fill()):(e.fillStyle=o.multiKeyBackground,e.fillRect(p,m,c,l),e.strokeRect(p,m,c,l),e.fillStyle=i.backgroundColor,e.fillRect(y,m+1,c-2,l-2))}e.fillStyle=this.labelTextColors[r]}drawBody(e,n,r){const{body:s}=this,{bodySpacing:o,bodyAlign:i,displayColors:a,boxHeight:l,boxWidth:c,boxPadding:d}=r,u=Wt(r.bodyFont);let f=u.lineHeight,g=0;const m=bs(r.rtl,this.x,this.width),p=function(C){n.fillText(C,m.x(e.x+g),e.y+f/2),e.y+=f+o},y=m.textAlign(i);let b,_,x,E,S,w,N;for(n.textAlign=i,n.textBaseline="middle",n.font=u.string,e.x=Ca(this,y,r),n.fillStyle=r.bodyColor,tt(this.beforeBody,p),g=a&&y!=="right"?i==="center"?c/2+d:c+2+d:0,E=0,w=s.length;E<w;++E){for(b=s[E],_=this.labelTextColors[E],n.fillStyle=_,tt(b.before,p),x=b.lines,a&&x.length&&(this._drawColorBox(n,e,E,m,r),f=Math.max(u.lineHeight,l)),S=0,N=x.length;S<N;++S)p(x[S]),f=u.lineHeight;tt(b.after,p)}g=0,f=u.lineHeight,tt(this.afterBody,p),e.y-=o}drawFooter(e,n,r){const s=this.footer,o=s.length;let i,a;if(o){const l=bs(r.rtl,this.x,this.width);for(e.x=Ca(this,r.footerAlign,r),e.y+=r.footerMarginTop,n.textAlign=l.textAlign(r.footerAlign),n.textBaseline="middle",i=Wt(r.footerFont),n.fillStyle=r.footerColor,n.font=i.string,a=0;a<o;++a)n.fillText(s[a],l.x(e.x),e.y+i.lineHeight/2),e.y+=i.lineHeight+r.footerSpacing}}drawBackground(e,n,r,s){const{xAlign:o,yAlign:i}=this,{x:a,y:l}=e,{width:c,height:d}=r,{topLeft:u,topRight:f,bottomLeft:g,bottomRight:m}=gs(s.cornerRadius);n.fillStyle=s.backgroundColor,n.strokeStyle=s.borderColor,n.lineWidth=s.borderWidth,n.beginPath(),n.moveTo(a+u,l),i==="top"&&this.drawCaret(e,n,r,s),n.lineTo(a+c-f,l),n.quadraticCurveTo(a+c,l,a+c,l+f),i==="center"&&o==="right"&&this.drawCaret(e,n,r,s),n.lineTo(a+c,l+d-m),n.quadraticCurveTo(a+c,l+d,a+c-m,l+d),i==="bottom"&&this.drawCaret(e,n,r,s),n.lineTo(a+g,l+d),n.quadraticCurveTo(a,l+d,a,l+d-g),i==="center"&&o==="left"&&this.drawCaret(e,n,r,s),n.lineTo(a,l+u),n.quadraticCurveTo(a,l,a+u,l),n.closePath(),n.fill(),s.borderWidth>0&&n.stroke()}_updateAnimationTarget(e){const n=this.chart,r=this.$animations,s=r&&r.x,o=r&&r.y;if(s||o){const i=To[e.position].call(this,this._active,this._eventPosition);if(!i)return;const a=this._size=bm(this,e),l=Object.assign({},i,this._size),c=ym(n,e,l),d=vm(e,l,c,n);(s._to!==d.x||o._to!==d.y)&&(this.xAlign=c.xAlign,this.yAlign=c.yAlign,this.width=a.width,this.height=a.height,this.caretX=i.x,this.caretY=i.y,this._resolveAnimations().update(this,d))}}_willRender(){return!!this.opacity}draw(e){const n=this.options.setContext(this.getContext());let r=this.opacity;if(!r)return;this._updateAnimationTarget(n);const s={width:this.width,height:this.height},o={x:this.x,y:this.y};r=Math.abs(r)<.001?0:r;const i=yn(n.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;n.enabled&&a&&(e.save(),e.globalAlpha=r,this.drawBackground(o,e,s,n),C0(e,n.textDirection),o.y+=i.top,this.drawTitle(o,e,n),this.drawBody(o,e,n),this.drawFooter(o,e,n),S0(e,n.textDirection),e.restore())}getActiveElements(){return this._active||[]}setActiveElements(e,n){const r=this._active,s=e.map(({datasetIndex:a,index:l})=>{const c=this.chart.getDatasetMeta(a);if(!c)throw new Error("Cannot find a dataset at index "+a);return{datasetIndex:a,element:c.data[l],index:l}}),o=!ea(r,s),i=this._positionChanged(s,n);(o||i)&&(this._active=s,this._eventPosition=n,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(e,n,r=!0){if(n&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;const s=this.options,o=this._active||[],i=this._getActiveElements(e,o,n,r),a=this._positionChanged(i,e),l=n||!ea(i,o)||a;return l&&(this._active=i,(s.enabled||s.external)&&(this._eventPosition={x:e.x,y:e.y},this.update(!0,n))),l}_getActiveElements(e,n,r,s){const o=this.options;if(e.type==="mouseout")return[];if(!s)return n.filter(a=>this.chart.data.datasets[a.datasetIndex]&&this.chart.getDatasetMeta(a.datasetIndex).controller.getParsed(a.index)!==void 0);const i=this.chart.getElementsAtEventForMode(e,o.mode,o,r);return o.reverse&&i.reverse(),i}_positionChanged(e,n){const{caretX:r,caretY:s,options:o}=this,i=To[o.position].call(this,e,n);return i!==!1&&(r!==i.x||s!==i.y)}}Pe(yd,"positioners",To);var Cm={id:"tooltip",_element:yd,positioners:To,afterInit(t,e,n){n&&(t.tooltip=new yd({chart:t,options:n}))},beforeUpdate(t,e,n){t.tooltip&&t.tooltip.initialize(n)},reset(t,e,n){t.tooltip&&t.tooltip.initialize(n)},afterDraw(t){const e=t.tooltip;if(e&&e._willRender()){const n={tooltip:e};if(t.notifyPlugins("beforeTooltipDraw",{...n,cancelable:!0})===!1)return;e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",n)}},afterEvent(t,e){if(t.tooltip){const n=e.replay;t.tooltip.handleEvent(e.event,n,e.inChartArea)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:km},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:t=>t!=="filter"&&t!=="itemSort"&&t!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]};const k5=(t,e,n,r)=>(typeof e=="string"?(n=t.push(e)-1,r.unshift({index:n,label:e})):isNaN(e)&&(n=null),n);function C5(t,e,n,r){const s=t.indexOf(e);if(s===-1)return k5(t,e,n,r);const o=t.lastIndexOf(e);return s!==o?n:s}const S5=(t,e)=>t===null?null:nn(Math.round(t),0,e);function Sm(t){const e=this.getLabels();return t>=0&&t<e.length?e[t]:t}class vd extends vs{constructor(e){super(e),this._startValue=void 0,this._valueRange=0,this._addedLabels=[]}init(e){const n=this._addedLabels;if(n.length){const r=this.getLabels();for(const{index:s,label:o}of n)r[s]===o&&r.splice(s,1);this._addedLabels=[]}super.init(e)}parse(e,n){if(at(e))return null;const r=this.getLabels();return n=isFinite(n)&&r[n]===e?n:C5(r,e,Ke(n,e),this._addedLabels),S5(n,r.length-1)}determineDataLimits(){const{minDefined:e,maxDefined:n}=this.getUserBounds();let{min:r,max:s}=this.getMinMax(!0);this.options.bounds==="ticks"&&(e||(r=0),n||(s=this.getLabels().length-1)),this.min=r,this.max=s}buildTicks(){const e=this.min,n=this.max,r=this.options.offset,s=[];let o=this.getLabels();o=e===0&&n===o.length-1?o:o.slice(e,n+1),this._valueRange=Math.max(o.length-(r?0:1),1),this._startValue=this.min-(r?.5:0);for(let i=e;i<=n;i++)s.push({value:i});return s}getLabelForValue(e){return Sm.call(this,e)}configure(){super.configure(),this.isHorizontal()||(this._reversePixels=!this._reversePixels)}getPixelForValue(e){return typeof e!="number"&&(e=this.parse(e)),e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getPixelForTick(e){const n=this.ticks;return e<0||e>n.length-1?null:this.getPixelForValue(n[e].value)}getValueForPixel(e){return Math.round(this._startValue+this.getDecimalForPixel(e)*this._valueRange)}getBasePixel(){return this.bottom}}Pe(vd,"id","category"),Pe(vd,"defaults",{ticks:{callback:Sm}});function _5(t,e){const n=[],{bounds:s,step:o,min:i,max:a,precision:l,count:c,maxTicks:d,maxDigits:u,includeBounds:f}=t,g=o||1,m=d-1,{min:p,max:y}=e,b=!at(i),_=!at(a),x=!at(c),E=(y-p)/(u+1);let S=qp((y-p)/m/g)*g,w,N,C,k;if(S<1e-14&&!b&&!_)return[{value:p},{value:y}];k=Math.ceil(y/S)-Math.floor(p/S),k>m&&(S=qp(k*S/m/g)*g),at(l)||(w=Math.pow(10,l),S=Math.ceil(S*w)/w),s==="ticks"?(N=Math.floor(p/S)*S,C=Math.ceil(y/S)*S):(N=p,C=y),b&&_&&o&&CM((a-i)/o,S/1e3)?(k=Math.round(Math.min((a-i)/S,d)),S=(a-i)/k,N=i,C=a):x?(N=b?i:N,C=_?a:C,k=c-1,S=(C-N)/k):(k=(C-N)/S,ra(k,Math.round(k),S/1e3)?k=Math.round(k):k=Math.ceil(k));const M=Math.max(Zp(S),Zp(N));w=Math.pow(10,at(l)?M:l),N=Math.round(N*w)/w,C=Math.round(C*w)/w;let T=0;for(b&&(f&&N!==i?(n.push({value:i}),N<i&&T++,ra(Math.round((N+T*S)*w)/w,i,_m(i,E,t))&&T++):N<i&&T++);T<k;++T){const L=Math.round((N+T*S)*w)/w;if(_&&L>a)break;n.push({value:L})}return _&&f&&C!==a?n.length&&ra(n[n.length-1].value,a,_m(a,E,t))?n[n.length-1].value=a:n.push({value:a}):(!_||C===a)&&n.push({value:C}),n}function _m(t,e,{horizontal:n,minRotation:r}){const s=qn(r),o=(n?Math.sin(s):Math.cos(s))||.001,i=.75*e*(""+t).length;return Math.min(e/o,i)}class E5 extends vs{constructor(e){super(e),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(e,n){return at(e)||(typeof e=="number"||e instanceof Number)&&!isFinite(+e)?null:+e}handleTickRangeOptions(){const{beginAtZero:e}=this.options,{minDefined:n,maxDefined:r}=this.getUserBounds();let{min:s,max:o}=this;const i=l=>s=n?s:l,a=l=>o=r?o:l;if(e){const l=dr(s),c=dr(o);l<0&&c<0?a(0):l>0&&c>0&&i(0)}if(s===o){let l=o===0?1:Math.abs(o*.05);a(o+l),e||i(s-l)}this.min=s,this.max=o}getTickLimit(){const e=this.options.ticks;let{maxTicksLimit:n,stepSize:r}=e,s;return r?(s=Math.ceil(this.max/r)-Math.floor(this.min/r)+1,s>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${r} would result generating up to ${s} ticks. Limiting to 1000.`),s=1e3)):(s=this.computeTickLimit(),n=n||11),n&&(s=Math.min(n,s)),s}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const e=this.options,n=e.ticks;let r=this.getTickLimit();r=Math.max(2,r);const s={maxTicks:r,bounds:e.bounds,min:e.min,max:e.max,precision:n.precision,step:n.stepSize,count:n.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:n.minRotation||0,includeBounds:n.includeBounds!==!1},o=this._range||this,i=_5(s,o);return e.bounds==="ticks"&&SM(i,this,"value"),e.reverse?(i.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),i}configure(){const e=this.ticks;let n=this.min,r=this.max;if(super.configure(),this.options.offset&&e.length){const s=(r-n)/Math.max(e.length-1,1)/2;n-=s,r+=s}this._startValue=n,this._endValue=r,this._valueRange=r-n}getLabelForValue(e){return qc(e,this.chart.options.locale,this.options.ticks.format)}}class xd extends E5{determineDataLimits(){const{min:e,max:n}=this.getMinMax(!0);this.min=bn(e)?e:0,this.max=bn(n)?n:1,this.handleTickRangeOptions()}computeTickLimit(){const e=this.isHorizontal(),n=e?this.width:this.height,r=qn(this.options.ticks.minRotation),s=(e?Math.sin(r):Math.cos(r))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(n/Math.min(40,o.lineHeight/s))}getPixelForValue(e){return e===null?NaN:this.getPixelForDecimal((e-this._startValue)/this._valueRange)}getValueForPixel(e){return this._startValue+this.getDecimalForPixel(e)*this._valueRange}}Pe(xd,"id","linear"),Pe(xd,"defaults",{ticks:{callback:l0.formatters.numeric}});const Sa={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},sn=Object.keys(Sa);function Em(t,e){return t-e}function Mm(t,e){if(at(e))return null;const n=t._adapter,{parser:r,round:s,isoWeekday:o}=t._parseOpts;let i=e;return typeof r=="function"&&(i=r(i)),bn(i)||(i=typeof r=="string"?n.parse(i,r):n.parse(i)),i===null?null:(s&&(i=s==="week"&&(sa(o)||o===!0)?n.startOf(i,"isoWeek",o):n.startOf(i,s)),+i)}function Nm(t,e,n,r){const s=sn.length;for(let o=sn.indexOf(t);o<s-1;++o){const i=Sa[sn[o]],a=i.steps?i.steps:Number.MAX_SAFE_INTEGER;if(i.common&&Math.ceil((n-e)/(a*i.size))<=r)return sn[o]}return sn[s-1]}function M5(t,e,n,r,s){for(let o=sn.length-1;o>=sn.indexOf(n);o--){const i=sn[o];if(Sa[i].common&&t._adapter.diff(s,r,i)>=e-1)return i}return sn[n?sn.indexOf(n):0]}function N5(t){for(let e=sn.indexOf(t)+1,n=sn.length;e<n;++e)if(Sa[sn[e]].common)return sn[e]}function Om(t,e,n){if(!n)t[e]=!0;else if(n.length){const{lo:r,hi:s}=Gc(n,e),o=n[r]>=e?n[r]:n[s];t[o]=!0}}function O5(t,e,n,r){const s=t._adapter,o=+s.startOf(e[0].value,r),i=e[e.length-1].value;let a,l;for(a=o;a<=i;a=+s.add(a,1,r))l=n[a],l>=0&&(e[l].major=!0);return e}function Tm(t,e,n){const r=[],s={},o=e.length;let i,a;for(i=0;i<o;++i)a=e[i],s[a]=i,r.push({value:a,major:!1});return o===0||!n?r:O5(t,r,s,n)}class _a extends vs{constructor(e){super(e),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(e,n={}){const r=e.time||(e.time={}),s=this._adapter=new BN._date(e.adapters.date);s.init(n),po(r.displayFormats,s.formats()),this._parseOpts={parser:r.parser,round:r.round,isoWeekday:r.isoWeekday},super.init(e),this._normalized=n.normalized}parse(e,n){return e===void 0?null:Mm(this,e)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const e=this.options,n=this._adapter,r=e.time.unit||"day";let{min:s,max:o,minDefined:i,maxDefined:a}=this.getUserBounds();function l(c){!i&&!isNaN(c.min)&&(s=Math.min(s,c.min)),!a&&!isNaN(c.max)&&(o=Math.max(o,c.max))}(!i||!a)&&(l(this._getLabelBounds()),(e.bounds!=="ticks"||e.ticks.source!=="labels")&&l(this.getMinMax(!1))),s=bn(s)&&!isNaN(s)?s:+n.startOf(Date.now(),r),o=bn(o)&&!isNaN(o)?o:+n.endOf(Date.now(),r)+1,this.min=Math.min(s,o-1),this.max=Math.max(s+1,o)}_getLabelBounds(){const e=this.getLabelTimestamps();let n=Number.POSITIVE_INFINITY,r=Number.NEGATIVE_INFINITY;return e.length&&(n=e[0],r=e[e.length-1]),{min:n,max:r}}buildTicks(){const e=this.options,n=e.time,r=e.ticks,s=r.source==="labels"?this.getLabelTimestamps():this._generate();e.bounds==="ticks"&&s.length&&(this.min=this._userMin||s[0],this.max=this._userMax||s[s.length-1]);const o=this.min,i=this.max,a=OM(s,o,i);return this._unit=n.unit||(r.autoSkip?Nm(n.minUnit,this.min,this.max,this._getLabelCapacity(o)):M5(this,a.length,n.minUnit,this.min,this.max)),this._majorUnit=!r.major.enabled||this._unit==="year"?void 0:N5(this._unit),this.initOffsets(s),e.reverse&&a.reverse(),Tm(this,a,this._majorUnit)}afterAutoSkip(){this.options.offsetAfterAutoskip&&this.initOffsets(this.ticks.map(e=>+e.value))}initOffsets(e=[]){let n=0,r=0,s,o;this.options.offset&&e.length&&(s=this.getDecimalForValue(e[0]),e.length===1?n=1-s:n=(this.getDecimalForValue(e[1])-s)/2,o=this.getDecimalForValue(e[e.length-1]),e.length===1?r=o:r=(o-this.getDecimalForValue(e[e.length-2]))/2);const i=e.length<3?.5:.25;n=nn(n,0,i),r=nn(r,0,i),this._offsets={start:n,end:r,factor:1/(n+1+r)}}_generate(){const e=this._adapter,n=this.min,r=this.max,s=this.options,o=s.time,i=o.unit||Nm(o.minUnit,n,r,this._getLabelCapacity(n)),a=Ke(s.ticks.stepSize,1),l=i==="week"?o.isoWeekday:!1,c=sa(l)||l===!0,d={};let u=n,f,g;if(c&&(u=+e.startOf(u,"isoWeek",l)),u=+e.startOf(u,c?"day":i),e.diff(r,n,i)>1e5*a)throw new Error(n+" and "+r+" are too far apart with stepSize of "+a+" "+i);const m=s.ticks.source==="data"&&this.getDataTimestamps();for(f=u,g=0;f<r;f=+e.add(f,a,i),g++)Om(d,f,m);return(f===r||s.bounds==="ticks"||g===1)&&Om(d,f,m),Object.keys(d).sort(Em).map(p=>+p)}getLabelForValue(e){const n=this._adapter,r=this.options.time;return r.tooltipFormat?n.format(e,r.tooltipFormat):n.format(e,r.displayFormats.datetime)}format(e,n){const s=this.options.time.displayFormats,o=this._unit,i=n||s[o];return this._adapter.format(e,i)}_tickFormatFunction(e,n,r,s){const o=this.options,i=o.ticks.callback;if(i)return ut(i,[e,n,r],this);const a=o.time.displayFormats,l=this._unit,c=this._majorUnit,d=l&&a[l],u=c&&a[c],f=r[n],g=c&&u&&f&&f.major;return this._adapter.format(e,s||(g?u:d))}generateTickLabels(e){let n,r,s;for(n=0,r=e.length;n<r;++n)s=e[n],s.label=this._tickFormatFunction(s.value,n,e)}getDecimalForValue(e){return e===null?NaN:(e-this.min)/(this.max-this.min)}getPixelForValue(e){const n=this._offsets,r=this.getDecimalForValue(e);return this.getPixelForDecimal((n.start+r)*n.factor)}getValueForPixel(e){const n=this._offsets,r=this.getDecimalForPixel(e)/n.factor-n.end;return this.min+r*(this.max-this.min)}_getLabelSize(e){const n=this.options.ticks,r=this.ctx.measureText(e).width,s=qn(this.isHorizontal()?n.maxRotation:n.minRotation),o=Math.cos(s),i=Math.sin(s),a=this._resolveTickFontOptions(0).size;return{w:r*o+a*i,h:r*i+a*o}}_getLabelCapacity(e){const n=this.options.time,r=n.displayFormats,s=r[n.unit]||r.millisecond,o=this._tickFormatFunction(e,0,Tm(this,[e],this._majorUnit),s),i=this._getLabelSize(o),a=Math.floor(this.isHorizontal()?this.width/i.w:this.height/i.h)-1;return a>0?a:1}getDataTimestamps(){let e=this._cache.data||[],n,r;if(e.length)return e;const s=this.getMatchingVisibleMetas();if(this._normalized&&s.length)return this._cache.data=s[0].controller.getAllParsedValues(this);for(n=0,r=s.length;n<r;++n)e=e.concat(s[n].controller.getAllParsedValues(this));return this._cache.data=this.normalize(e)}getLabelTimestamps(){const e=this._cache.labels||[];let n,r;if(e.length)return e;const s=this.getLabels();for(n=0,r=s.length;n<r;++n)e.push(Mm(this,s[n]));return this._cache.labels=this._normalized?e:this.normalize(e)}normalize(e){return e0(e.sort(Em))}}Pe(_a,"id","time"),Pe(_a,"defaults",{bounds:"data",adapters:{},time:{parser:!1,unit:!1,round:!1,isoWeekday:!1,minUnit:"millisecond",displayFormats:{}},ticks:{source:"auto",callback:!1,major:{enabled:!1}}});function Ea(t,e,n){let r=0,s=t.length-1,o,i,a,l;n?(e>=t[r].pos&&e<=t[s].pos&&({lo:r,hi:s}=Yc(t,"pos",e)),{pos:o,time:a}=t[r],{pos:i,time:l}=t[s]):(e>=t[r].time&&e<=t[s].time&&({lo:r,hi:s}=Yc(t,"time",e)),{time:o,pos:a}=t[r],{time:i,pos:l}=t[s]);const c=i-o;return c?a+(l-a)*(e-o)/c:a}class Am extends _a{constructor(e){super(e),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){const e=this._getTimestampsForTable(),n=this._table=this.buildLookupTable(e);this._minPos=Ea(n,this.min),this._tableRange=Ea(n,this.max)-this._minPos,super.initOffsets(e)}buildLookupTable(e){const{min:n,max:r}=this,s=[],o=[];let i,a,l,c,d;for(i=0,a=e.length;i<a;++i)c=e[i],c>=n&&c<=r&&s.push(c);if(s.length<2)return[{time:n,pos:0},{time:r,pos:1}];for(i=0,a=s.length;i<a;++i)d=s[i+1],l=s[i-1],c=s[i],Math.round((d+l)/2)!==c&&o.push({time:c,pos:i/(a-1)});return o}_generate(){const e=this.min,n=this.max;let r=super.getDataTimestamps();return(!r.includes(e)||!r.length)&&r.splice(0,0,e),(!r.includes(n)||r.length===1)&&r.push(n),r.sort((s,o)=>s-o)}_getTimestampsForTable(){let e=this._cache.all||[];if(e.length)return e;const n=this.getDataTimestamps(),r=this.getLabelTimestamps();return n.length&&r.length?e=this.normalize(n.concat(r)):e=n.length?n:r,e=this._cache.all=e,e}getDecimalForValue(e){return(Ea(this._table,e)-this._minPos)/this._tableRange}getValueForPixel(e){const n=this._offsets,r=this.getDecimalForPixel(e)/n.factor-n.end;return Ea(this._table,r*this._tableRange+this._minPos,!0)}}Pe(Am,"id","timeseries"),Pe(Am,"defaults",_a.defaults);const Dm="label";function Im(t,e){typeof t=="function"?t(e):t&&(t.current=e)}function T5(t,e){const n=t.options;n&&e&&Object.assign(n,e)}function Lm(t,e){t.labels=e}function Pm(t,e,n=Dm){const r=[];t.datasets=e.map(s=>{const o=t.datasets.find(i=>i[n]===s[n]);return!o||!s.data||r.includes(o)?{...s}:(r.push(o),Object.assign(o,s),o)})}function A5(t,e=Dm){const n={labels:[],datasets:[]};return Lm(n,t.labels),Pm(n,t.datasets,e),n}function D5(t,e){const{height:n=150,width:r=300,redraw:s=!1,datasetIdKey:o,type:i,data:a,options:l,plugins:c=[],fallbackContent:d,updateMode:u,...f}=t,g=v.useRef(null),m=v.useRef(null),p=()=>{g.current&&(m.current=new No(g.current,{type:i,data:A5(a,o),options:l&&{...l},plugins:c}),Im(e,m.current))},y=()=>{Im(e,null),m.current&&(m.current.destroy(),m.current=null)};return v.useEffect(()=>{!s&&m.current&&l&&T5(m.current,l)},[s,l]),v.useEffect(()=>{!s&&m.current&&Lm(m.current.config.data,a.labels)},[s,a.labels]),v.useEffect(()=>{!s&&m.current&&a.datasets&&Pm(m.current.config.data,a.datasets,o)},[s,a.datasets]),v.useEffect(()=>{m.current&&(s?(y(),setTimeout(p)):m.current.update(u))},[s,l,a.labels,a.datasets,u]),v.useEffect(()=>{m.current&&(y(),setTimeout(p))},[i]),v.useEffect(()=>(p(),()=>y()),[]),h.jsx("canvas",{ref:g,role:"img",height:n,width:r,...f,children:d})}const I5=v.forwardRef(D5);function $m(t,e){return No.register(e),v.forwardRef((n,r)=>h.jsx(I5,{...n,ref:r,type:t}))}const L5=$m("bar",fa),P5=$m("doughnut",wo);No.register(vd,xd,ka,g5,Cm,pm);const $5=({labels:t,datasets:e,height:n=200,showLegend:r=!1,showGrid:s=!1,showYGrid:o=!1,showYAxis:i=!1,className:a="",id:l,editorAttrs:c})=>{const d=v.useMemo(()=>({labels:t??[],datasets:(e??[]).map((m,p)=>({label:m.label,data:m.data,backgroundColor:m.backgroundColor||(p===0?"#7C3AED":"#DDD6FE"),borderRadius:m.borderRadius??4,barThickness:16,maxBarThickness:20,yAxisID:m.yAxisID??"y"}))}),[t,e]),u=v.useMemo(()=>(e??[]).some(m=>m.yAxisID==="y1"),[e]),f=v.useMemo(()=>({responsive:!0,maintainAspectRatio:!1,animation:{duration:750,easing:"easeOutQuart"},animations:{x:{duration:0},y:{from:m=>{if(m.type==="data")return m.chart.scales.y.getPixelForValue(0)}}},transitions:{active:{animation:{duration:200}},resize:{animation:{duration:0}}},plugins:{legend:{display:r,position:"top"},tooltip:{backgroundColor:"#1e293b",titleColor:"#fff",bodyColor:"#fff",padding:12,cornerRadius:8}},scales:{x:{grid:{display:s},border:{display:!1},ticks:{color:"#94a3b8",font:{size:11}}},y:{type:"linear",position:"left",grid:{display:o,color:"#e2e8f0"},border:{display:!1},ticks:{display:i,color:"#94a3b8",font:{size:11},padding:8},beginAtZero:!0,grace:"5%"},...u?{y1:{type:"linear",position:"right",grid:{display:!1},border:{display:!1},ticks:{display:i,color:"#94a3b8",font:{size:11},padding:8},beginAtZero:!0,grace:"5%"}}:{}}}),[r,s,o,i,u]),g=typeof n=="number"?n:/^\d+$/.test(String(n))?`${n}px`:n;return h.jsx("div",{className:`bar-chart-container ${a}`,style:{height:g},id:l,...c,children:h.jsx(L5,{data:d,options:f})})},F5=v.memo($5,(t,e)=>JSON.stringify(t.labels)===JSON.stringify(e.labels)&&JSON.stringify(t.datasets)===JSON.stringify(e.datasets)&&t.height===e.height&&t.showLegend===e.showLegend&&t.showGrid===e.showGrid&&t.showYGrid===e.showYGrid&&t.showYAxis===e.showYAxis&&t.className===e.className&&t.id===e.id&&t.editorAttrs===e.editorAttrs);No.register(Oo,Cm,pm);const V5=({data:t,centerLabel:e,centerValue:n,size:r=200,cutout:s="70%",showLegend:o=!1,className:i="",id:a,editorAttrs:l})=>{const c=v.useMemo(()=>({labels:(t??[]).map(f=>f.name),datasets:[{data:(t??[]).map(f=>f.value),backgroundColor:(t??[]).map(f=>f.color),borderWidth:0,hoverOffset:4}]}),[t]),d=v.useMemo(()=>({responsive:!0,maintainAspectRatio:!1,cutout:s,plugins:{legend:{display:o},tooltip:{backgroundColor:"#1e293b",titleColor:"#fff",bodyColor:"#fff",padding:12,cornerRadius:8,callbacks:{label:f=>` $${f.parsed.toLocaleString()}`}}}}),[s,o]),u=typeof r=="number"?r:/^\d+$/.test(String(r))?`${r}px`:r;return h.jsx("div",{className:`donut-chart-wrapper ${i}`,id:a,...l,children:h.jsxs("div",{className:"relative",style:{width:u,height:u},children:[h.jsx(P5,{data:c,options:d}),(e||n)&&h.jsxs("div",{className:"absolute inset-0 flex flex-col items-center justify-center pointer-events-none",children:[e&&h.jsx("span",{className:"text-xs text-slate-500 dark:text-slate-400",children:e}),n&&h.jsx("span",{className:"text-lg font-bold text-slate-900 dark:text-white",children:n})]})]})})},z5=({id:t,className:e,style:n,children:r,editorAttrs:s})=>h.jsx("div",{id:t,className:e,style:n,...s,children:r}),H5=({children:t,cols:e=1,responsive:n,gap:r,rowGap:s,colGap:o,autoRows:i,autoCols:a,flow:l="row",className:c="",style:d,onClick:u,id:f,editorAttrs:g})=>{const m=["grid"];if(e&&m.push(`grid-cols-${e}`),n&&(n.sm&&m.push(`sm:grid-cols-${n.sm}`),n.md&&m.push(`md:grid-cols-${n.md}`),n.lg&&m.push(`lg:grid-cols-${n.lg}`),n.xl&&m.push(`xl:grid-cols-${n.xl}`),n["2xl"]&&m.push(`2xl:grid-cols-${n["2xl"]}`)),r!==void 0&&r>0&&m.push(`gap-${r}`),s!==void 0&&s>0&&m.push(`gap-y-${s}`),o!==void 0&&o>0&&m.push(`gap-x-${o}`),i){const y={auto:"auto-rows-auto",min:"auto-rows-min",max:"auto-rows-max",fr:"auto-rows-fr"};m.push(y[i])}if(a){const y={auto:"auto-cols-auto",min:"auto-cols-min",max:"auto-cols-max",fr:"auto-cols-fr"};m.push(y[a])}const p={row:"grid-flow-row",col:"grid-flow-col",dense:"grid-flow-dense","row-dense":"grid-flow-row-dense","col-dense":"grid-flow-col-dense"};return m.push(p[l]),c&&m.push(c),h.jsx(I,{className:m.join(" "),style:d,onClick:u,id:f,...g,children:t})},U5=({children:t,direction:e="row",justify:n="start",align:r="stretch",wrap:s="nowrap",gap:o=0,grow:i=!1,shrink:a=!0,className:l="",style:c,onClick:d,id:u,editorAttrs:f})=>{const g=["flex"],m={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};g.push(m[e]);const p={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};g.push(p[s]);const y={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};g.push(y[n]);const b={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};return g.push(b[r]),o>0&&g.push(`gap-${o}`),i===!0?g.push("flex-grow"):typeof i=="number"&&i>0&&g.push(`flex-grow-${i}`),a===!1?g.push("shrink-0"):a===!0?g.push("flex-shrink"):typeof a=="number"&&a>0&&g.push(`flex-shrink-${a}`),l&&g.push(l),h.jsx(I,{className:g.join(" "),style:c,onClick:d,id:u,...f,children:t})},B5=({title:t,subtitle:e,children:n,padding:r="none",background:s="none",maxWidth:o,centered:i=!1,border:a=!1,shadow:l="none",rounded:c=!1,className:d="",style:u,onClick:f,id:g,editorAttrs:m})=>{const p=[],y={none:"",sm:"p-2",md:"p-4",lg:"p-6",xl:"p-8"};y[r]&&p.push(y[r]);const b={none:"",white:"bg-white dark:bg-gray-700",gray:"bg-gray-50 dark:bg-gray-800",primary:"bg-blue-50 dark:bg-blue-900/20",secondary:"bg-gray-50 dark:bg-gray-800"};if(s!=="none"&&p.push(b[s]),o){const x={sm:"max-w-sm",md:"max-w-md",lg:"max-w-lg",xl:"max-w-xl","2xl":"max-w-2xl","4xl":"max-w-4xl","6xl":"max-w-6xl","7xl":"max-w-7xl",full:"max-w-full"};p.push(x[o])}i&&p.push("mx-auto"),a&&p.push("border","border-gray-200","dark:border-gray-700");const _={none:"",sm:"shadow-sm",md:"shadow-md",lg:"shadow-lg",xl:"shadow-xl"};return l!=="none"&&p.push(_[l]),c&&p.push("rounded-lg"),d&&p.push(d),h.jsxs(Td,{className:p.join(" "),style:u,onClick:f,id:g,...m,children:[(t||e)&&h.jsxs("div",{className:"mb-4",children:[t&&h.jsx("h2",{className:"text-2xl font-bold text-gray-900 dark:text-white",children:t}),e&&h.jsx("p",{className:"mt-1 text-sm text-gray-600 dark:text-gray-400",children:e})]}),n]})},j5=({leftWidth:t="250px",rightWidth:e="300px",leftSlot:n,centerSlot:r,rightSlot:s,className:o="",style:i,id:a,editorAttrs:l})=>{const c=`flex flex-row w-full h-full ${o}`.trim(),d={width:t,flexShrink:0},u={flex:1,minWidth:0},f={width:e,flexShrink:0};return h.jsxs(I,{className:c,style:i,id:a,...l,children:[h.jsx(I,{className:"flex flex-col",style:d,children:n}),h.jsx(I,{className:"flex flex-col",style:u,children:r}),h.jsx(I,{className:"flex flex-col",style:f,children:s})]})},W5={language:"json",theme:"vs-dark",options:{automaticLayout:!0,minimap:{enabled:!0},scrollBeyondLastLine:!1,fontSize:14,tabSize:2,formatOnPaste:!0,formatOnType:!0,wordWrap:"on",folding:!0,lineNumbers:"on",renderWhitespace:"selection"}},Fm=[{uri:`${window.location.origin}/schemas/layout.json`,fileMatch:["*layout*.json"],schema:{type:"object",properties:{version:{type:"string"},endpoint:{type:"string"},components:{type:"array"}},required:["version","endpoint","components"]}}],G5={"vs/nls":{availableLanguages:{"*":"ko"}}};function Y5(t){t.languages.json.jsonDefaults.setDiagnosticsOptions({validate:!0,schemas:Fm,enableSchemaRequest:!0,schemaValidation:"error",schemaRequest:"error"})}const X5={identifier:"sirsoft-admin_basic",vendor:"sirsoft",name:{ko:"Admin Basic",en:"Admin Basic"},version:"1.0.5",license:"MIT",description:{ko:"그누보드7 기본 관리자 템플릿",en:"Gnuboard7 default admin template"},type:"admin",locales:["ko","en"],author:{name:"sirsoft",email:"contact@sirsoft.com",url:"https://sirsoft.com"},release_date:"2026-05-15",g7_version:">=7.0.7",dependencies:{modules:{},plugins:{}},assets:{css:["assets/css/main.css","assets/css/ui-system.css"],js:["dist/components.js"],fonts:["assets/fonts/"],images:["assets/images/"]},components:{basic:["a","button","checkbox","div","form","h1","h2","h3","hr","icon","img","input","label","li","nav","p","section","select","span","svg","table","tbody","td","textarea","th","thead","tr","ul"],composite:["action-menu","admin-footer","admin-header","admin-sidebar","alert-dialog","breadcrumb","card","confirm-dialog","data-grid","dialog","dropdown","empty-state","filter-group","icon-button","loading-spinner","modal","page-header","pagination","product-card","search-bar","stat-card","status-badge","tab-navigation","template-card","toast"],layout:["container","flex","grid","section"]},preview:{thumbnail:"preview/thumbnail.png",screenshots:["preview/screenshot1.png","preview/screenshot2.png"]},error_config:{layouts:{401:"errors/401",403:"errors/403",404:"errors/404",500:"errors/500",503:"errors/503",maintenance:"errors/maintenance"}},github_url:"https://github.com/gnuboard/g7-template-sirsoft-admin_basic",github_changelog_url:"https://github.com/gnuboard/g7-template-sirsoft-admin_basic/blob/main/CHANGELOG.md",externals:[{id:"fontawesome",type:"style",url:"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",preconnect:"https://cdnjs.cloudflare.com"},{id:"pretendard",type:"webfont",url:"https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css",preconnect:"https://cdn.jsdelivr.net",crossorigin:"anonymous"},{id:"flag-icons",type:"style",url:"https://cdn.jsdelivr.net/npm/flag-icons@7.2.3/css/flag-icons.min.css"}]},Jn=window.G7Core?.createLogger?.("Handler:SetTheme")??{log:(...t)=>console.log("[Handler:SetTheme]",...t),warn:(...t)=>console.warn("[Handler:SetTheme]",...t),error:(...t)=>console.error("[Handler:SetTheme]",...t)},wd=["auto","light","dark"],Vm="g7_color_scheme",K5=t=>t==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":t,Ma=t=>{const e=K5(t);document.documentElement.setAttribute("data-theme",e),e==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")};function q5(){try{const t=localStorage.getItem(Vm),e=t&&wd.includes(t)?t:"auto";Ma(e);try{window.G7Core?.state?.setGlobal?.({theme:e})}catch(n){Jn.warn("Failed to set global theme state:",n)}Jn.log("Initial theme applied:",e)}catch(t){Jn.warn("Failed to load initial theme:",t),Ma("auto")}}async function Z5(t,e){const n=t?.target;if(n&&typeof n=="string"&&wd.includes(n)){Ma(n);try{window.G7Core?.state?.setGlobal?.({theme:n})}catch(r){Jn.warn("Failed to set global theme state:",r)}return}q5()}async function Q5(t,e){let n;if(t?.target&&typeof t.target=="string"&&!t.target.includes("{{")&&(n=t.target),!n||typeof n!="string"){Jn.warn("Invalid theme:",n);return}if(!wd.includes(n)){Jn.warn("Unsupported theme:",n);return}const r=n;try{localStorage.setItem(Vm,r)}catch(s){Jn.error("Failed to save theme to localStorage:",s);return}Ma(r);try{window.G7Core?.state?.setGlobal?.({theme:r})}catch(s){Jn.warn("Failed to set global theme state:",s)}Jn.log("Theme changed to:",r)}const ws=window.G7Core?.createLogger?.("Handler:InitMenu")??{log:(...t)=>console.log("[Handler:InitMenu]",...t),warn:(...t)=>console.warn("[Handler:InitMenu]",...t),error:(...t)=>console.error("[Handler:InitMenu]",...t)};function zm(t,e){for(const n of t){if(n.slug===e)return n;if(n.children&&n.children.length>0){const r=zm(n.children,e);if(r)return r}}return null}function Hm(t){return new URLSearchParams(window.location.search).get(t)}function J5(t){return window.G7Core?.state?.getDataSource?.(t)}function R5(t){const e=[t,t?.data,t?.data?.data];for(const n of e)if(Array.isArray(n)&&n.length>0)return n;return null}async function eT(t,e=30,n=100){for(let r=0;r<e;r++){const s=R5(J5(t));if(s)return s;await new Promise(o=>setTimeout(o,n))}return null}async function tT(t){const e=window.G7Core;try{const r=(await e?.api?.get?.(`/api/admin/menus/${t.id}`))?.data;if(r&&typeof r=="object"&&r.id===t.id)return r}catch(n){ws.warn("[initMenuFromUrl] Failed to hydrate menu detail:",n)}return t}async function nT(t,e){const n=window.G7Core,r=Hm("menu"),s=Hm("mode");if(!r){ws.log("[initMenuFromUrl] No menu parameter in URL");return}if(!n?.state?.set){ws.warn("[initMenuFromUrl] G7Core.state.set not available");return}const o=await eT("menus");if(!o){ws.warn("[initMenuFromUrl] menus data not available after waiting");return}const i=zm(o,r);if(!i){ws.warn("[initMenuFromUrl] Menu not found with slug:",r);return}const a=await tT(i),l=s==="edit"?"edit":"view";n.state.set({selectedMenuId:a.id,selectedMenu:a,panelMode:l}),l==="edit"&&n.state.set({formData:{name:a.name,slug:a.slug,url:a.url,icon:a.icon,parent_id:a.parent_id,extension_type:a.extension_type,extension_identifier:a.extension_identifier,is_active:a.is_active,roles:(a.roles??[]).map(c=>c.id)}}),ws.log("[initMenuFromUrl] Menu initialized from URL:",{slug:r,mode:l,menuId:i.id})}const Zt=window.G7Core?.createLogger?.("Handler:FilterVisibility")??{log:(...t)=>console.log("[Handler:FilterVisibility]",...t),warn:(...t)=>console.warn("[Handler:FilterVisibility]",...t),error:(...t)=>console.error("[Handler:FilterVisibility]",...t)},Na="g7_filter_visibility_";async function rT(t,e){const{storageKey:n,defaultFilters:r=[],stateKey:s="visibleFilters"}=t?.params||{};if(!n){Zt.warn("[initFilterVisibility] storageKey is required");return}const o=`${Na}${n}`;try{const i=localStorage.getItem(o);let a;if(i){const l=JSON.parse(i);Array.isArray(l)?(a=l,Zt.log(`[initFilterVisibility] Loaded from localStorage: ${a.join(", ")}`)):(a=r,Zt.warn("[initFilterVisibility] Invalid saved data format, using defaults"))}else a=r,Zt.log(`[initFilterVisibility] No saved data, using defaults: ${r.join(", ")}`);e?.setState&&e.setState({[s]:a})}catch(i){Zt.error("[initFilterVisibility] Failed to load filter visibility:",i),e?.setState&&e.setState({[s]:r})}}async function sT(t,e){const{storageKey:n,filters:r}=t?.params||{};if(!n){Zt.warn("[saveFilterVisibility] storageKey is required");return}if(!Array.isArray(r)){Zt.warn("[saveFilterVisibility] filters must be an array");return}const s=`${Na}${n}`;try{localStorage.setItem(s,JSON.stringify(r)),Zt.log(`[saveFilterVisibility] Saved to localStorage: ${r.join(", ")}`)}catch(o){Zt.error("[saveFilterVisibility] Failed to save filter visibility:",o)}}async function oT(t,e){const{storageKey:n,filterId:r,stateKey:s="visibleFilters"}=t?.params||{};if(!n||!r){Zt.warn("[toggleFilterVisibility] storageKey and filterId are required");return}const o=e?.state?.[s]||[];let i;o.includes(r)?i=o.filter(l=>l!==r):i=[...o,r],e?.setState&&e.setState({[s]:i});const a=`${Na}${n}`;try{localStorage.setItem(a,JSON.stringify(i)),Zt.log(`[toggleFilterVisibility] Toggled ${r}: ${i.join(", ")}`)}catch(l){Zt.error("[toggleFilterVisibility] Failed to save:",l)}}async function iT(t,e){const{storageKey:n,defaultFilters:r=[],stateKey:s="visibleFilters"}=t?.params||{};if(!n){Zt.warn("[resetFilterVisibility] storageKey is required");return}const o=`${Na}${n}`;try{localStorage.setItem(o,JSON.stringify(r)),e?.setState&&e.setState({[s]:r}),Zt.log(`[resetFilterVisibility] Reset to defaults: ${r.join(", ")}`)}catch(i){Zt.error("[resetFilterVisibility] Failed to reset:",i)}}const ks=window.G7Core?.createLogger?.("Handler:MultilingualTag")??{log:(...t)=>console.log("[Handler:MultilingualTag]",...t),warn:(...t)=>console.warn("[Handler:MultilingualTag]",...t),error:(...t)=>console.error("[Handler:MultilingualTag]",...t)};async function aT(t,e){const n=window.G7Core;if(!n){ks.error("G7Core not available");return}const r=n.state?.getGlobal?.()?.multilingualTagEdit;if(!r){ks.warn("No multilingualTagEdit state found"),n.modal?.close?.();return}const{fieldName:s,editingIndex:o,values:i,statePath:a,currentTags:l}=r;if(!Object.values(i||{}).some(g=>typeof g=="string"&&g.trim()!=="")){ks.warn("No values to save"),n.modal?.close?.(),kd(n);return}const d=Array.isArray(l)?l:[];let u;o==null?u=[...d,i]:u=d.map((g,m)=>m===o?i:g);const f=a||(s?.startsWith("form.")?s:`form.${s}`);n.state?.setParentLocal?.({[f]:u}),n.modal?.close?.(),kd(n)}async function lT(t,e){const n=window.G7Core;if(!n){ks.error("G7Core not available");return}n.modal?.close?.(),kd(n)}async function cT(t,e){const n=window.G7Core;if(!n){ks.error("G7Core not available");return}const r=t?.params?.locale;if(!r){ks.warn("No locale specified");return}const s=e?.event;let o="";s?.target?.value!==void 0?o=s.target.value:s?.value!==void 0&&(o=s.value);const a=(n.state?.getGlobal?.()||{}).multilingualTagEdit||{},l={...a.values||{},[r]:o};n.dispatch?.({handler:"setState",params:{target:"global",multilingualTagEdit:{...a,values:l}}})}function kd(t){t.dispatch?.({handler:"setState",params:{target:"global",multilingualTagEdit:null}})}function Um(t){const e=t.getFullYear(),n=String(t.getMonth()+1).padStart(2,"0"),r=String(t.getDate()).padStart(2,"0"),s=String(t.getHours()).padStart(2,"0"),o=String(t.getMinutes()).padStart(2,"0"),i=String(t.getSeconds()).padStart(2,"0");return`${e}-${n}-${r}T${s}:${o}:${i}`}function dT(t){const e=new Date,n=new Date(e.getFullYear(),e.getMonth(),e.getDate(),23,59,59),r=Um(n),s=new Date(e.getFullYear(),e.getMonth(),e.getDate(),0,0,0);switch(t){case"today":break;case"week":s.setDate(s.getDate()-6);break;case"month":s.setMonth(s.getMonth()-1);break;case"3months":s.setMonth(s.getMonth()-3);break;case"6months":s.setMonth(s.getMonth()-6);break;case"1year":s.setFullYear(s.getFullYear()-1);break}return{startDate:Um(s),endDate:r}}function uT(t){const{preset:e}=t.params||{preset:"today"},{startDate:n,endDate:r}=dT(e);return{startDate:n,endDate:r,preset:e}}const Ao=window.G7Core?.createLogger?.("Handler:Sidebar")??{log:(...t)=>console.log("[Handler:Sidebar]",...t),warn:(...t)=>console.warn("[Handler:Sidebar]",...t),error:(...t)=>console.error("[Handler:Sidebar]",...t)},Bm="g7_admin_sidebar_collapsed";function jm(){try{return localStorage.getItem(Bm)==="1"}catch{return!1}}function fT(t){try{localStorage.setItem(Bm,t?"1":"0")}catch(e){Ao.error("Failed to save sidebar state to localStorage:",e)}try{window.G7Core?.state?.setGlobal?.({sidebarCollapsed:t})}catch(e){Ao.warn("Failed to set global sidebar state:",e)}}function Wm(){const t=jm();try{window.G7Core?.state?.setGlobal?.({sidebarCollapsed:t})}catch(e){Ao.warn("Failed to restore sidebar state:",e)}Ao.log("Initial sidebar collapsed state restored:",t)}async function hT(t,e){Wm()}async function gT(t,e){const r=!(()=>{try{return!!window.G7Core?.state?.getGlobal?.()?.sidebarCollapsed}catch{return jm()}})();fT(r),Ao.log("Sidebar collapsed toggled to:",r)}const Cd=window.G7Core?.createLogger?.("Handler:DownloadAttachment")??{log:(...t)=>console.log("[Handler:DownloadAttachment]",...t),warn:(...t)=>console.warn("[Handler:DownloadAttachment]",...t),error:(...t)=>console.error("[Handler:DownloadAttachment]",...t)};async function pT(t,e){const n=window.G7Core,{url:r,filename:s}=t?.params||{};if(!r){Cd.warn("다운로드 URL 이 없어 요청을 건너뜁니다.");return}if(!n?.api?.get){Cd.error("G7Core.api.get 이 초기화되지 않아 다운로드를 실행할 수 없습니다.");return}try{const o=await n.api.get(r,{responseType:"blob"});if(o){const i=URL.createObjectURL(o),a=document.createElement("a");a.href=i,a.download=s||"",document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(i)}}catch(o){Cd.error("첨부파일 다운로드 실패",o),n?.toast?.error?.(n?.t?.("common.download_failed")??"Download failed")}}const Oa=window.G7Core?.createLogger?.("Handler:DetectAssetUrlMode")??{log:(...t)=>console.log("[Handler:DetectAssetUrlMode]",...t),warn:(...t)=>console.warn("[Handler:DetectAssetUrlMode]",...t),error:(...t)=>console.error("[Handler:DetectAssetUrlMode]",...t)},mT="G7_ASSET_PROBE_OK";async function Gm(t){try{const e=await fetch(t,{cache:"no-store",credentials:"omit"});if(!e.ok)return!1;const n=e.headers.get("content-type")??"";return/javascript|ecmascript/i.test(n)?(await e.text()).includes(mT):!1}catch{return!1}}async function Ym(){const[t,e]=await Promise.all([Gm("/api/system/asset-probe.js"),Gm("/api/system/asset-probe")]);return t&&e?"extension":!t&&e?"extensionless":"unavailable"}function bT(){return window.G7Config?.settings?.general?.asset_url_mode??"extension"}async function yT(t){try{const e=await Ym();if(e==="unavailable")return;const n=bT();if(e===n)return;window.G7Core?.state?.setGlobal?.({assetUrlModeDrift:{detected:e,stored:n}})}catch(e){Oa.warn("자산 URL 방식 대조 실패 (대시보드 동작에는 영향 없음):",e)}}async function vT(t,e){const n=window.G7Core,r=e?.setState??n?.state?.setLocal,s=o=>{typeof r=="function"?r({asset_url_mode_detect_status:o}):Oa.warn("setState 를 찾지 못해 감지 상태를 폼에 반영하지 못했습니다")};try{s("detecting");const o=await Ym();if(o==="unavailable"){s("unavailable");return}typeof r=="function"?r({"general.asset_url_mode":o,asset_url_mode_detect_status:o}):Oa.warn("setState 를 찾지 못해 감지 결과를 폼에 반영하지 못했습니다")}catch(o){Oa.error("자산 URL 방식 감지 실패:",o),s("unavailable")}}const Ta={detectAssetUrlMode:vT,checkAssetUrlModeDrift:yT,setTheme:Q5,initTheme:Z5,scrollToSection:nu,initMenuFromUrl:nT,initFilterVisibility:rT,saveFilterVisibility:sT,toggleFilterVisibility:oT,resetFilterVisibility:iT,saveMultilingualTag:aT,cancelMultilingualTag:lT,updateMultilingualTagValue:cT,setDateRange:uT,toggleSidebar:gT,initSidebar:hT,downloadAttachment:pT},Do=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic:IdentityLauncher")??{log:(...t)=>console.log("[Template:sirsoft-admin_basic:IdentityLauncher]",...t),warn:(...t)=>console.warn("[Template:sirsoft-admin_basic:IdentityLauncher]",...t),error:(...t)=>console.error("[Template:sirsoft-admin_basic:IdentityLauncher]",...t)};function xT(){const e=window.G7Core?.api?.getToken?.()??null;return e?{Authorization:`Bearer ${e}`}:{}}function wT(){try{const t=typeof window<"u"?window.localStorage?.getItem("g7_locale"):null;return t?{"Accept-Language":t}:{}}catch{return{}}}async function kT(t,e=null){const n={purpose:t.purpose};t.provider_id&&(n.provider_id=t.provider_id),e&&(n.target=e);const r=t.challenge_start_url||"/api/identity/challenges",s=await fetch(r,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...wT(),...xT()},body:JSON.stringify(n)}),o=await s.json().catch(()=>null);if(!s.ok||!o?.success){const a=o?.message??`Challenge 시작 실패 (HTTP ${s.status})`;throw new Error(a)}const i=o.data??o;return{id:i.id,expires_at:i.expires_at,render_hint:i.render_hint??t.render_hint??"text_code",public_payload:i.public_payload,redirect_url:i.redirect_url,max_attempts:typeof i.max_attempts=="number"?i.max_attempts:void 0}}async function CT(t){const e=window.G7Core,n=e?.identity;if(!e?.dispatch||!e?.state||!n)return Do.error("G7Core 또는 G7Core.identity 가 초기화되지 않아 launcher 를 실행할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};if(t.render_hint==="external_redirect"||t.redirect_url)return n.redirectExternally(t);const r=t.target&&(t.target.email||t.target.phone)?t.target:null;let s;try{s=await kT(t,r)}catch(m){Do.error("Challenge 시작 실패:",m);try{await e.dispatch({handler:"toast",params:{type:"error",message:m instanceof Error?m.message:"본인인증 시작에 실패했습니다."}})}catch{}return{status:"failed",failureCode:"CHALLENGE_START_FAILED",reason:m instanceof Error?m.message:String(m)}}if(s.redirect_url)return n.redirectExternally({...t,render_hint:"external_redirect",redirect_url:s.redirect_url});const o=s.expires_at?new Date(s.expires_at).getTime():0,i=o>0?Math.max(0,Math.floor((o-Date.now())/1e3)):0,a=typeof s.max_attempts=="number"?s.max_attempts:0;e.state.set({identityChallenge:{policy_key:t.policy_key,purpose:t.purpose,provider_id:t.provider_id??null,render_hint:s.render_hint,challenge_id:s.id,expires_at:s.expires_at,public_payload:s.public_payload??{},target:r,code:"",error:null,attempts:0,maxAttempts:a,remainingSeconds:i,resendCooldown:0}});let l=o,c=0;const d=window.setInterval(()=>{const m=l>0?Math.max(0,Math.floor((l-Date.now())/1e3)):0;c=Math.max(0,c-1),e.state.set({identityChallenge:{remainingSeconds:m,resendCooldown:c}})},1e3);let u=null;try{u=e.state.subscribe?.(m=>{const p=m?.identityChallenge?.expires_at;if(typeof p=="string"){const b=new Date(p).getTime();Number.isFinite(b)&&b!==l&&(l=b)}const y=m?.identityChallenge?.resendCooldown;typeof y=="number"&&y>c&&(c=y)})??null}catch{}const f=n.createDeferred();try{await e.dispatch({handler:"openModal",target:"identity-challenge-modal"})}catch(m){return Do.error("openModal 실패 — 풀페이지로 폴백:",m),window.clearInterval(d),u?.(),n.redirectExternally({...t,render_hint:"external_redirect",redirect_url:`/admin/identity/challenge?challenge_id=${encodeURIComponent(s.id)}&return=${encodeURIComponent(window.location.href)}`})}const g=await f;return window.clearInterval(d),u?.(),g}function ST(){const t=window.G7Core?.identity;if(!t?.setLauncher){Do.warn("G7Core.identity 가 아직 초기화되지 않아 launcher 등록을 건너뜁니다. 코어 부트스트랩 순서를 확인하세요.");return}t.setLauncher(CT),Do.log("IDV launcher registered (sirsoft-admin_basic)")}function _T(t){const e=t.icons;return Array.isArray(e)?e.filter(n=>!!n&&typeof n=="object"&&typeof n.value=="string"):[]}function ET(t,e){const n=t.label;return typeof n=="string"?n.startsWith("$t:")?e(n.slice(3)):n:t.value}function MT(t,e,n){if(n==="")return!0;const r=n.toLowerCase();return t.value.toLowerCase().includes(r)||e.toLowerCase().includes(r)?!0:(t.keywords??[]).some(s=>s.toLowerCase().includes(r))}function NT({entry:t}){const e=t.preview;return e?.html?h.jsx("span",{className:"g7le-icon-preview",dangerouslySetInnerHTML:{__html:e.html},style:_d}):e?.className?h.jsx("span",{className:`g7le-icon-preview ${e.className}`,style:_d}):h.jsx("i",{className:`fas fa-${t.value}`,style:_d,"aria-hidden":"true"})}function OT({control:t,value:e,onChange:n,t:r}){const s=_T(t),o=e==null?"":String(e),i=typeof t.iconColumns=="number"?t.iconColumns:6,a=t.iconSearchPlaceholder,l=typeof a=="string"&&a.startsWith("$t:")?r(a.slice(3)):r("layout_editor.icon_picker.search_placeholder"),[c,d]=v.useState("");if(s.length===0)return h.jsx("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:Xm,children:h.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-free","data-testid":"g7le-widget-icon-picker-free",value:o,placeholder:r("layout_editor.icon_picker.free_input_hint"),onChange:g=>n(g.target.value===""?void 0:g.target.value),style:Km})});const u=g=>ET(g,r),f=s.filter(g=>MT(g,u(g),c));return h.jsxs("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:Xm,children:[h.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-search","data-testid":"g7le-widget-icon-picker-search",value:c,placeholder:l,onChange:g=>d(g.target.value),style:Km}),h.jsxs("div",{className:"g7le-icon-grid","data-testid":"g7le-widget-icon-picker-grid",style:{...TT,gridTemplateColumns:`repeat(${i}, minmax(0, 1fr))`},children:[h.jsx("button",{type:"button","data-testid":"g7le-icon-cell-none",title:r("layout_editor.icon_picker.none"),onClick:()=>n(void 0),"aria-pressed":o==="",style:o===""?qm:Sd,children:h.jsx("span",{style:AT,children:"—"})}),f.map(g=>{const m=g.value===o;return h.jsx("button",{type:"button","data-testid":`g7le-icon-cell-${g.value}`,title:u(g),"aria-pressed":m,onClick:()=>n(g.value),style:m?qm:Sd,children:h.jsx(NT,{entry:g})},g.value)})]}),f.length===0&&h.jsx("div",{"data-testid":"g7le-widget-icon-picker-empty",style:DT,children:r("layout_editor.icon_picker.no_results")})]})}const Xm={display:"flex",flexDirection:"column",gap:6,width:"100%"},Km={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%"},TT={display:"grid",gap:4,maxHeight:220,overflowY:"auto",padding:2},Sd={display:"flex",alignItems:"center",justifyContent:"center",minHeight:34,padding:4,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",cursor:"pointer"},qm={...Sd,border:"2px solid #2563eb",background:"#eff6ff"},_d={display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:16,lineHeight:1},AT={fontSize:10,color:"#475569",fontFamily:"monospace",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"100%"},DT={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"4px 2px"},Zm=130,An=20;function IT(t){const e=t?.arrayProp;return typeof e=="string"&&e?e:"tabs"}function LT(t){let e=0;for(const n of t){const r=typeof n.id=="number"?n.id:Number(n.id);Number.isFinite(r)&&r>e&&(e=r)}return e+1}function PT({node:t,params:e,cellBoxes:n,t:r,onPatchNode:s}){const o=IT(e),i=v.useMemo(()=>{const g=(t.props??{})[o];return typeof g=="string"||!Array.isArray(g)?null:g},[t,o]),a=v.useMemo(()=>{const g=new Map,m=`props.${o}.`;for(const p of n??[]){if(!p.path.startsWith(m))continue;const y=Number(p.path.slice(m.length));Number.isInteger(y)&&g.set(y,{top:p.top,left:p.left,width:p.width,height:p.height})}return g},[n,o]),l=v.useCallback(g=>{const m=t.props??{};s({...t,props:{...m,[o]:g}})},[t,o,s]),c=v.useCallback(g=>{if(!i)return;const p={id:LT(i),label:r("editor.tabnav_inplace.new_tab")},y=i.slice();y.splice(g,0,p),l(y)},[i,l,r]),d=v.useCallback(g=>{if(!i)return;const m=i.slice();m.splice(g,1),l(m)},[i,l]),u=v.useCallback((g,m)=>{if(!i)return;const p=g+m;if(p<0||p>=i.length)return;const y=i.slice(),[b]=y.splice(g,1);y.splice(p,0,b),l(y)},[i,l]);if(!i||a.size===0)return null;const f={position:"absolute",width:An,height:An,display:"flex",alignItems:"center",justifyContent:"center",fontSize:12,lineHeight:1,border:"1px solid rgba(0,0,0,0.15)",borderRadius:4,background:"#ffffff",color:"#1f2937",cursor:"pointer",boxShadow:"0 1px 2px rgba(0,0,0,0.18)",pointerEvents:"auto",padding:0,zIndex:Zm};return h.jsx("div",{className:"g7le-tabnav-inplace","data-testid":"g7le-tabnav-inplace",style:{position:"absolute",inset:0,pointerEvents:"none",zIndex:Zm},children:i.map((g,m)=>{const p=a.get(m);if(!p)return null;const y=p.left+p.width;return h.jsxs("div",{"data-testid":`g7le-tabnav-aff-${m}`,children:[h.jsx("button",{type:"button","data-testid":`g7le-tabnav-remove-${m}`,title:r("editor.tabnav_inplace.remove"),"aria-label":r("editor.tabnav_inplace.remove"),style:{...f,left:y-An/2,top:p.top-An/2,color:"#b91c1c"},onClick:b=>{b.preventDefault(),b.stopPropagation(),d(m)},children:"✕"}),m>0&&h.jsx("button",{type:"button","data-testid":`g7le-tabnav-move-left-${m}`,title:r("editor.tabnav_inplace.move_left"),"aria-label":r("editor.tabnav_inplace.move_left"),style:{...f,left:p.left,top:p.top+p.height-An/2},onClick:b=>{b.preventDefault(),b.stopPropagation(),u(m,-1)},children:"◀"}),m<i.length-1&&h.jsx("button",{type:"button","data-testid":`g7le-tabnav-move-right-${m}`,title:r("editor.tabnav_inplace.move_right"),"aria-label":r("editor.tabnav_inplace.move_right"),style:{...f,left:y-An,top:p.top+p.height-An/2},onClick:b=>{b.preventDefault(),b.stopPropagation(),u(m,1)},children:"▶"}),m===0&&h.jsx("button",{type:"button","data-testid":"g7le-tabnav-add-0",title:r("editor.tabnav_inplace.add"),"aria-label":r("editor.tabnav_inplace.add"),style:{...f,left:p.left-An,top:p.top+p.height/2-An/2,color:"#15803d"},onClick:b=>{b.preventDefault(),b.stopPropagation(),c(0)},children:"+"}),h.jsx("button",{type:"button","data-testid":`g7le-tabnav-add-${m+1}`,title:r("editor.tabnav_inplace.add"),"aria-label":r("editor.tabnav_inplace.add"),style:{...f,left:y-An/2,top:p.top+p.height/2-An/2,color:"#15803d"},onClick:b=>{b.preventDefault(),b.stopPropagation(),c(m+1)},children:"+"})]},`tab-aff-${g.id}-${m}`)})})}const Ed=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic:EditorWidgets")??{log:(...t)=>console.log("[Template:sirsoft-admin_basic:EditorWidgets]",...t),warn:(...t)=>console.warn("[Template:sirsoft-admin_basic:EditorWidgets]",...t)};function $T(){if(typeof window>"u")return;const t=window.G7Core?.layoutEditor;if(!t?.registerWidget){Ed.warn("G7Core.layoutEditor 미노출 — icon-picker 위젯 등록 보류");return}t.registerWidget("icon-picker",OT),Ed.log("편집기 위젯 'icon-picker' 등록됨(Font Awesome 검색 그리드)"),t.registerCanvasOverlay&&(t.registerCanvasOverlay("tabnav",PT),Ed.log("편집기 캔버스 오버레이 'tabnav' 등록됨(TabNavigation 탭 인플레이스)"))}const Md=window.G7Core?.createLogger?.("Template:sirsoft-admin_basic")??{log:(...t)=>console.log("[Template:sirsoft-admin_basic]",...t),warn:(...t)=>console.warn("[Template:sirsoft-admin_basic]",...t),error:(...t)=>console.error("[Template:sirsoft-admin_basic]",...t)};typeof window<"u"&&(window.G7TemplateHandlers=Ta);function Qm(){if(typeof window<"u"){let t=0;const e=50,n=()=>{const r=window.G7Core?.getActionDispatcher?.();r?(Object.entries(Ta).forEach(([s,o])=>{r.registerHandler(s,o)}),Md.log(`${Object.keys(Ta).length} custom handler(s) registered:`,Object.keys(Ta)),ST(),Wm()):(t++,t<=e?(Md.warn(`ActionDispatcher not found, retrying... (${t}/${e})`),setTimeout(n,100)):Md.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};document.readyState==="complete"?n():window.addEventListener("load",n)}}return $T(),Qm(),R.A=Fr,R.Accordion=sc,R.ActionMenu=Cs,R.AdminFooter=Cy,R.AdminHeader=vy,R.AdminSidebar=yy,R.Alert=uu,R.AlertDialog=cy,R.Badge=$d,R.BarChart=F5,R.Breadcrumb=au,R.Button=oe,R.Card=hb,R.CardGrid=Yb,R.CategoryTree=jS,R.Checkbox=Cn,R.ChipCheckbox=ME,R.Code=cb,R.CodeEditor=zv,R.ColumnSelector=Yv,R.ConfirmDialog=ou,R.Container=z5,R.DataGrid=Hb,R.Dialog=su,R.Div=I,R.DonutChart=V5,R.Dropdown=Kb,R.DropdownButton=NE,R.DropdownMenuItem=OE,R.DynamicFieldList=zE,R.EmptyState=py,R.ExtensionBadge=sh,R.FileInput=Nd,R.FileUploader=Ep,R.FilterGroup=Jb,R.FilterVisibilitySelector=wE,R.Flex=U5,R.Form=Va,R.FormField=WE,R.Fragment=ub,R.Grid=H5,R.H1=Pa,R.H2=er,R.H3=$r,R.H4=Od,R.Hr=fb,R.HtmlContent=Ja,R.HtmlEditor=bE,R.I=gt,R.Icon=re,R.IconButton=iy,R.IconName=de,R.IconSelect=FS,R.ImageGallery=ji,R.Img=kn,R.Input=wt,R.Label=on,R.LanguageSelector=ky,R.LayoutEditorHeader=Ny,R.LayoutFileList=Oy,R.LayoutHistoryPanel=Uv,R.LayoutWarnings=xE,R.Li=Fa,R.LoadingSpinner=by,R.LoginForm=Bv,R.Modal=Bo,R.MultilingualInput=ja,R.MultilingualLocaleContext=Fc,R.MultilingualTabPanel=jE,R.MultilingualTagInput=Gk,R.Nav=Po,R.NotificationCenter=xy,R.Ol=nb,R.Optgroup=ib,R.Option=ob,R.P=yt,R.PageHeader=Sy,R.PageLoading=Zv,R.PageTransitionIndicator=Kv,R.Pagination=$o,R.PermissionTree=US,R.Pre=db,R.ProductCard=qb,R.RadioGroup=gE,R.RichSelect=_E,R.SearchBar=Qb,R.SearchableDropdown=Yk,R.Section=Td,R.SectionHeader=Ey,R.SectionLayout=B5,R.Select=Vr,R.SlotContainer=SE,R.SortableMenuItem=Xh,R.SortableMenuList=PS,R.Span=Q,R.StatCard=Zb,R.StatusBadge=Fo,R.Svg=Wn,R.TabNavigation=Rb,R.TabNavigationScroll=sy,R.Table=za,R.TagInput=Bk,R.TagSelect=Xk,R.Tbody=Ua,R.Td=Jt,R.TemplateCard=My,R.Textarea=Rn,R.Tfoot=Ba,R.Th=zr,R.Thead=Ha,R.ThemeToggle=Wv,R.ThreeColumnLayout=j5,R.Toast=hy,R.Toggle=Wa,R.Tr=tr,R.Ul=$a,R.UnifiedIcon=lb,R.UserProfile=wy,R.VersionList=Mu,R.configureJSONLanguageService=Y5,R.defaultJSONSchemas=Fm,R.executeImageDownload=Dc,R.initTemplate=Qm,R.jsonEditorDefaults=W5,R.monacoInitOptions=G5,R.templateMetadata=X5,R.useCurrentMultilingualLocale=Ip,R.useImageGallery=fp,Object.defineProperty(R,Symbol.toStringTag,{value:"Module"}),R})({},ReactJSXRuntime,React,ReactDOM); diff --git a/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/DataGrid.d.ts b/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/DataGrid.d.ts index b563f03a..af2aced6 100644 --- a/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/DataGrid.d.ts +++ b/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/DataGrid.d.ts @@ -104,6 +104,18 @@ export interface DataGridProps { selectedIds?: (string | number)[]; onSelectionChange?: (ids: (string | number)[]) => void; idField?: string; + /** + * 선택의 유효 범위. + * + * - `'page'`: 선택은 지금 그려진 행에서만 유효하다. 검색·필터·페이지 이동으로 + * 행이 목록에서 빠지면 그 행의 선택도 함께 버린다. 일괄 처리 버튼이 달린 + * 목록 화면은 이 값을 쓴다 — 그래야 "보이지 않는 행이 일괄 처리 대상" 이 + * 되는 상태가 만들어지지 않는다. + * - `'free'`(기본): 선택을 컴포넌트가 건드리지 않는다. 선택 자체가 저장 대상인 + * 폼(쿠폰 적용 대상 고르기 등)처럼, 목록에 안 보이는 항목도 선택으로 남아야 + * 하는 화면용. + */ + selectionScope?: 'page' | 'free'; showColumnSelector?: boolean; visibleColumns?: string[]; onColumnVisibilityChange?: (fields: string[]) => void; diff --git a/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/assetOrigin.d.ts b/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/assetOrigin.d.ts new file mode 100644 index 00000000..08a4a4e9 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/dist/src/components/composite/assetOrigin.d.ts @@ -0,0 +1,23 @@ +/** + * 자산 URL 출처 판정 유틸리티 + * + * 공개 자산 스토리지(S3/CDN)를 켜면 첨부·이미지의 `download_url` 이 외부 origin + * 절대 URL 이 된다. 이런 URL 은 인증이 필요 없는 공개 자산이므로 XHR(Blob) 로 + * 가져오면 안 된다 — 교차 출처 XHR 은 CDN 이 CORS 헤더를 주지 않는 한 응답을 + * 읽지 못해 이미지가 통째로 실패하고, 응답을 읽을 수 있는 CDN 이라면 이번에는 + * 세션 토큰이 제3자 origin 으로 나간다. 교차 출처 URL 은 `<img src>` / 링크로 + * 직접 사용한다. + * + * @module composite/assetOrigin + */ +/** + * URL 이 현재 문서와 다른 출처인지 판정합니다. + * + * 상대 경로는 항상 동일 출처입니다. 절대 URL(`https://…`)과 프로토콜 상대 + * URL(`//host/…`)만 출처를 비교하며, 파싱 불가하거나 브라우저 밖이면 + * 동일 출처로 간주합니다(기존 인증 경로 유지). + * + * @param url 판정할 URL + * @returns 교차 출처 여부 + */ +export declare const isCrossOriginAssetUrl: (url?: string | null) => boolean; diff --git a/templates/_bundled/sirsoft-admin_basic/lang/partial/en/admin.json b/templates/_bundled/sirsoft-admin_basic/lang/partial/en/admin.json index 4a3c16aa..345b8e39 100644 --- a/templates/_bundled/sirsoft-admin_basic/lang/partial/en/admin.json +++ b/templates/_bundled/sirsoft-admin_basic/lang/partial/en/admin.json @@ -205,7 +205,16 @@ "bulk_block_success": "Users have been blocked.", "bulk_withdraw_title": "Withdraw Users", "bulk_withdraw_confirm": "Withdraw {{count}} user(s). This action may be difficult to reverse.", - "bulk_withdraw_success": "Users have been withdrawn." + "bulk_withdraw_success": "Users have been withdrawn.", + "bulk_withdraw_partial": "{updated} user(s) withdrawn. {failed} could not be processed — {reason}", + "bulk_withdraw_all_failed": "No users were withdrawn — {reason}", + "withdraw_confirm_title": "Confirm Withdrawal", + "withdraw_confirm_lead": "Saving the status as 'Withdrawn' will actually withdraw this user. This cannot be undone.", + "withdraw_confirm_effects_title": "What happens as well", + "withdraw_confirm_effect_anonymize": "Email, name and nickname are anonymized.", + "withdraw_confirm_effect_sessions": "All login sessions for this user are revoked.", + "withdraw_confirm_effect_linked": "Consent history and profile image are deleted, and linked data such as mileage and identity verification records is cleaned up.", + "withdraw_confirm_submit": "Withdraw User" }, "form": { "page_title_create": "Create User", @@ -1379,7 +1388,13 @@ "image_max_width": "Max Width", "image_max_height": "Max Height", "image_quality": "Image Quality", - "image_quality_hint": "Enter a value between 1-100. Higher values mean better quality." + "image_quality_hint": "Enter a value between 1-100. Higher values mean better quality.", + "orphan_cleanup": "Orphan Attachment Cleanup", + "orphan_cleanup_desc": "Clean up attachments left without an owner because a form was abandoned before saving.", + "orphan_cleanup_enabled": "Auto cleanup orphan attachments", + "orphan_cleanup_enabled_hint": "Off by default — the operator must turn it on. When on, files and records of orphan attachments past the retention period are deleted daily. Attachments currently in use, such as the site logo, are never deleted.", + "orphan_retention_days": "Orphan attachment retention (days)", + "orphan_retention_days_hint": "Only orphan attachments older than this period are eligible for cleanup. Allow generous headroom for forms left open. (1 ~ 3650 days)" }, "seo": { "meta_settings": "Meta Tag Settings", @@ -1615,7 +1630,9 @@ }, "drivers": { "common": { - "optional": "Optional" + "optional": "Optional", + "unavailable_saved_value_prefix": "The saved value ", + "unavailable_saved_value_suffix": " is no longer available. Choose an available option and save." }, "storage": { "title": "File Storage", @@ -1627,17 +1644,21 @@ }, "s3_bucket": "S3 Bucket", "s3_region": "S3 Region", - "regions": { - "ap_northeast_2": "Asia Pacific (Seoul)", - "ap_northeast_1": "Asia Pacific (Tokyo)", - "us_east_1": "US East (N. Virginia)", - "us_west_2": "US West (Oregon)", - "eu_west_1": "Europe (Ireland)" - }, + "s3_region_desc": "Enter an AWS region code or the region value of your S3-compatible storage. (e.g. ap-northeast-2, or auto for Cloudflare R2)", "s3_access_key": "Access Key", "s3_secret_key": "Secret Key", - "s3_url": "S3 URL", - "s3_url_desc": "Only enter if using a custom endpoint or CDN URL." + "s3_endpoint": "Endpoint URL", + "s3_endpoint_desc": "API endpoint of S3-compatible storage (Cloudflare R2, MinIO, etc.). Leave empty for AWS S3.", + "s3_use_path_style": "Use path-style addressing", + "s3_use_path_style_desc": "Enable for storages that only support path-style addressing, such as MinIO.", + "s3_url": "S3 URL (Public URL)", + "s3_url_desc": "CDN or custom domain used to build public file URLs. Enter the API address in the Endpoint URL field instead." + }, + "public_asset": { + "title": "Public Asset Storage", + "desc": "Configure the disk for serving fully public assets (product, category, review, and editor images, etc.) via direct URLs. When disabled, the existing streaming method is used.", + "disk": "Disk", + "s3_help": "When selecting S3, the S3 URL (CDN domain) in the File Storage card above must be configured. Without it, streaming is used." }, "cache": { "title": "Cache", @@ -1730,9 +1751,6 @@ "desc": "Configure the search engine driver for integrated and admin search.", "driver": "Search Engine Driver", "driver_desc": "The built-in MySQL FULLTEXT (ngram) engine supports Korean search without additional configuration.", - "options": { - "mysql_fulltext": "MySQL FULLTEXT (ngram)" - }, "plugin_notice": "Additional drivers will appear when search engine plugins (Meilisearch, Elasticsearch, etc.) are installed." }, "test_connection": "Test Connection", diff --git a/templates/_bundled/sirsoft-admin_basic/lang/partial/ko/admin.json b/templates/_bundled/sirsoft-admin_basic/lang/partial/ko/admin.json index d58cf921..318a030e 100644 --- a/templates/_bundled/sirsoft-admin_basic/lang/partial/ko/admin.json +++ b/templates/_bundled/sirsoft-admin_basic/lang/partial/ko/admin.json @@ -209,7 +209,16 @@ "bulk_block_success": "사용자가 차단되었습니다.", "bulk_withdraw_title": "사용자 탈퇴 처리", "bulk_withdraw_confirm": "{{count}}명의 사용자를 탈퇴 처리합니다. 이 작업은 되돌리기 어려울 수 있습니다.", - "bulk_withdraw_success": "사용자가 탈퇴 처리되었습니다." + "bulk_withdraw_success": "사용자가 탈퇴 처리되었습니다.", + "bulk_withdraw_partial": "{updated}명을 탈퇴 처리했습니다. {failed}명은 처리하지 못했습니다 — {reason}", + "bulk_withdraw_all_failed": "탈퇴 처리하지 못했습니다 — {reason}", + "withdraw_confirm_title": "탈퇴 처리 확인", + "withdraw_confirm_lead": "상태를 '탈퇴'로 저장하면 이 회원은 실제로 탈퇴 처리됩니다. 되돌릴 수 없습니다.", + "withdraw_confirm_effects_title": "함께 처리되는 항목", + "withdraw_confirm_effect_anonymize": "이메일·이름·닉네임이 익명화됩니다.", + "withdraw_confirm_effect_sessions": "이 회원의 모든 로그인 세션이 해제됩니다.", + "withdraw_confirm_effect_linked": "약관 동의 이력·프로필 이미지가 삭제되고, 마일리지·본인인증 기록 등 연계 데이터가 정리됩니다.", + "withdraw_confirm_submit": "탈퇴 처리" }, "form": { "page_title_create": "사용자 등록", @@ -1383,7 +1392,13 @@ "image_max_width": "최대 너비", "image_max_height": "최대 높이", "image_quality": "이미지 품질", - "image_quality_hint": "1~100 사이의 값을 입력하세요. 높을수록 품질이 좋습니다." + "image_quality_hint": "1~100 사이의 값을 입력하세요. 높을수록 품질이 좋습니다.", + "orphan_cleanup": "고아 첨부 정리", + "orphan_cleanup_desc": "폼을 저장하지 않고 나가서 소유자 없이 남은 첨부파일을 정리합니다.", + "orphan_cleanup_enabled": "고아 첨부 자동 정리", + "orphan_cleanup_enabled_hint": "기본 꺼짐 — 운영자가 직접 켜야 동작합니다. 켜면 보존기간이 지난 고아 첨부의 파일과 기록을 매일 삭제합니다. 사이트 로고처럼 현재 사용 중인 첨부는 삭제하지 않습니다.", + "orphan_retention_days": "고아 첨부 보존기간 (일)", + "orphan_retention_days_hint": "업로드 후 이 기간이 지난 고아 첨부만 정리 대상이 됩니다. 폼 작성 중 이탈을 감안해 넉넉히 잡으세요. (1 ~ 3650 일)" }, "seo": { "meta_settings": "메타 태그 설정", @@ -1619,7 +1634,9 @@ }, "drivers": { "common": { - "optional": "선택사항" + "optional": "선택사항", + "unavailable_saved_value_prefix": "저장된 값 ", + "unavailable_saved_value_suffix": " 은(는) 현재 사용할 수 없습니다. 사용 가능한 값을 선택해 저장하세요." }, "storage": { "title": "파일 스토리지", @@ -1631,17 +1648,21 @@ }, "s3_bucket": "S3 버킷", "s3_region": "S3 리전", - "regions": { - "ap_northeast_2": "아시아 태평양 (서울)", - "ap_northeast_1": "아시아 태평양 (도쿄)", - "us_east_1": "미국 동부 (버지니아)", - "us_west_2": "미국 서부 (오레곤)", - "eu_west_1": "유럽 (아일랜드)" - }, + "s3_region_desc": "AWS 리전 코드 또는 S3 호환 스토리지의 리전 값을 입력하세요. (예: ap-northeast-2, Cloudflare R2 는 auto)", "s3_access_key": "Access Key", "s3_secret_key": "Secret Key", - "s3_url": "S3 URL", - "s3_url_desc": "커스텀 엔드포인트 또는 CDN URL을 사용하는 경우에만 입력하세요." + "s3_endpoint": "엔드포인트 URL", + "s3_endpoint_desc": "S3 호환 스토리지(Cloudflare R2, MinIO 등)의 API 주소입니다. AWS S3 는 비워 둡니다.", + "s3_use_path_style": "Path-style 주소 사용", + "s3_use_path_style_desc": "MinIO 등 path-style 주소 전용 스토리지에서 켭니다.", + "s3_url": "S3 URL (공개 URL)", + "s3_url_desc": "파일 공개 URL 생성에 쓰는 CDN/커스텀 도메인입니다. API 요청 주소는 엔드포인트 URL에 입력하세요." + }, + "public_asset": { + "title": "공개 자산 스토리지", + "desc": "완전 공개 자산(상품·카테고리·리뷰·에디터 이미지 등)의 직접 URL 서빙 디스크를 설정합니다. 사용 안 함이면 기존 스트리밍 방식으로 동작합니다.", + "disk": "디스크", + "s3_help": "S3 선택 시 위 파일 스토리지 카드의 S3 URL(CDN 도메인) 설정이 필요합니다. 미설정 시 스트리밍으로 동작합니다." }, "cache": { "title": "캐시", @@ -1734,9 +1755,6 @@ "desc": "통합검색 및 관리자 검색에 사용할 검색엔진 드라이버를 설정합니다.", "driver": "검색엔진 드라이버", "driver_desc": "기본 제공되는 MySQL FULLTEXT(ngram) 엔진은 별도 설정 없이 한국어 검색을 지원합니다.", - "options": { - "mysql_fulltext": "MySQL FULLTEXT (ngram)" - }, "plugin_notice": "검색엔진 플러그인(Meilisearch, Elasticsearch 등)을 설치하면 추가 드라이버가 표시됩니다." }, "test_connection": "연결 테스트", diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_form.json index 62ee3dac..7ae83107 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_form.json @@ -336,7 +336,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:common.validation_error" + "message": "{{error.message ?? '$t:common.validation_error'}}" } } ] diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_list.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_list.json index dbe6f348..10289191 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_list.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_role_list.json @@ -430,7 +430,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.roles.toggle_status_failed" + "message": "{{error.message ?? '$t:admin.roles.toggle_status_failed'}}" } } ] diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_schedule_list.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_schedule_list.json index 17efebb8..e90724a8 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_schedule_list.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_schedule_list.json @@ -147,7 +147,14 @@ "params": { "target": "global", "selectedSchedule": null, - "isEditMode": false + "isEditMode": false, + "scheduleForm": { + "type": "artisan", + "frequency": "daily", + "without_overlapping": true, + "run_in_maintenance": false, + "is_active": true + } } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_settings.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_settings.json index df2553c3..336473a6 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_settings.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_settings.json @@ -694,7 +694,7 @@ "type": "basic", "name": "Button", "props": { - "disabled": "{{!_local.hasChanges || _local.isSaving || ((_global.activeSettingsTab || query.tab) === 'drivers' && ((_local.form?.drivers?.storage_driver === 's3' && !_local.driverTestResults?.s3?.success && ((_local.form?.drivers?.storage_driver || '') !== (_local.originalDrivers?.storage_driver || '') || (_local.form?.drivers?.s3_bucket || '') !== (_local.originalDrivers?.s3_bucket || '') || (_local.form?.drivers?.s3_region || '') !== (_local.originalDrivers?.s3_region || '') || (_local.form?.drivers?.s3_access_key || '') !== (_local.originalDrivers?.s3_access_key || '') || (_local.form?.drivers?.s3_secret_key || '') !== (_local.originalDrivers?.s3_secret_key || ''))) || (_local.form?.drivers?.cache_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.cache_driver === 'memcached' && !_local.driverTestResults?.memcached?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.memcached_host || '') !== (_local.originalDrivers?.memcached_host || '') || String(_local.form?.drivers?.memcached_port ?? '') !== String(_local.originalDrivers?.memcached_port ?? ''))) || (_local.form?.drivers?.session_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.session_driver || '') !== (_local.originalDrivers?.session_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.queue_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.queue_driver || '') !== (_local.originalDrivers?.queue_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.websocket_enabled && !_local.driverTestResults?.websocket?.success && (Boolean(_local.form?.drivers?.websocket_enabled) !== Boolean(_local.originalDrivers?.websocket_enabled) || (_local.form?.drivers?.websocket_host || '') !== (_local.originalDrivers?.websocket_host || '') || String(_local.form?.drivers?.websocket_port ?? '') !== String(_local.originalDrivers?.websocket_port ?? '') || (_local.form?.drivers?.websocket_scheme || '') !== (_local.originalDrivers?.websocket_scheme || '')))))}}", + "disabled": "{{!_local.hasChanges || _local.isSaving || ((_global.activeSettingsTab || query.tab) === 'drivers' && (((_local.form?.drivers?.storage_driver === 's3' || _local.form?.drivers?.public_asset_disk === 's3') && !_local.driverTestResults?.s3?.success && ((_local.form?.drivers?.storage_driver || '') !== (_local.originalDrivers?.storage_driver || '') || (_local.form?.drivers?.s3_bucket || '') !== (_local.originalDrivers?.s3_bucket || '') || (_local.form?.drivers?.s3_region || '') !== (_local.originalDrivers?.s3_region || '') || (_local.form?.drivers?.s3_access_key || '') !== (_local.originalDrivers?.s3_access_key || '') || (_local.form?.drivers?.s3_secret_key || '') !== (_local.originalDrivers?.s3_secret_key || '') || (_local.form?.drivers?.s3_endpoint || '') !== (_local.originalDrivers?.s3_endpoint || '') || Boolean(_local.form?.drivers?.s3_use_path_style) !== Boolean(_local.originalDrivers?.s3_use_path_style))) || (_local.form?.drivers?.cache_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.cache_driver === 'memcached' && !_local.driverTestResults?.memcached?.success && ((_local.form?.drivers?.cache_driver || '') !== (_local.originalDrivers?.cache_driver || '') || (_local.form?.drivers?.memcached_host || '') !== (_local.originalDrivers?.memcached_host || '') || String(_local.form?.drivers?.memcached_port ?? '') !== String(_local.originalDrivers?.memcached_port ?? ''))) || (_local.form?.drivers?.session_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.session_driver || '') !== (_local.originalDrivers?.session_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.queue_driver === 'redis' && !_local.driverTestResults?.redis?.success && ((_local.form?.drivers?.queue_driver || '') !== (_local.originalDrivers?.queue_driver || '') || (_local.form?.drivers?.redis_host || '') !== (_local.originalDrivers?.redis_host || '') || String(_local.form?.drivers?.redis_port ?? '') !== String(_local.originalDrivers?.redis_port ?? '') || (_local.form?.drivers?.redis_password || '') !== (_local.originalDrivers?.redis_password || ''))) || (_local.form?.drivers?.websocket_enabled && !_local.driverTestResults?.websocket?.success && (Boolean(_local.form?.drivers?.websocket_enabled) !== Boolean(_local.originalDrivers?.websocket_enabled) || (_local.form?.drivers?.websocket_host || '') !== (_local.originalDrivers?.websocket_host || '') || String(_local.form?.drivers?.websocket_port ?? '') !== String(_local.originalDrivers?.websocket_port ?? '') || (_local.form?.drivers?.websocket_scheme || '') !== (_local.originalDrivers?.websocket_scheme || '')))))}}", "className": "btn btn-primary flex-center gap-1.5" }, "actions": [ @@ -769,7 +769,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.save_error" + "message": "{{error.message ?? '$t:admin.settings.save_error'}}" } } ] @@ -877,7 +877,7 @@ } ], "initLocal": { - "driverTestResults": [], + "driverTestResults": {}, "editingDefinition": null, "editingTemplate": null, "editLang": "ko", diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_template_layout_edit.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_template_layout_edit.json index 12459d06..0850b7b6 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_template_layout_edit.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_template_layout_edit.json @@ -215,7 +215,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.templates.layout_edit.preview_error" + "message": "{{error.message ?? '$t:admin.templates.layout_edit.preview_error'}}" } } ] @@ -286,7 +286,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.templates.layout_edit.preview_error" + "message": "{{error.message ?? '$t:admin.templates.layout_edit.preview_error'}}" } } ] diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_form.json index 046e0cd5..ce6fde82 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_form.json @@ -80,7 +80,7 @@ "handler": "toast", "params": { "type": "error", - "message": "{{error.message}}" + "message": "{{error.message ?? '$t:common.error_occurred'}}" } } }, @@ -851,31 +851,25 @@ "text": "$t:admin.users.form.fields.status" }, { + "comment": "커스텀 Select 는 value-제어 전용 — defaultValue 는 렌더되지 않아 create 모드 표시가 비었다. 미전송 시 서버(CreateUserRequest)가 active 로 보정하므로 표시 기본값과 서버 기본값이 일치한다.", "type": "composite", "name": "Select", "props": { "name": "status", "className": "w-full", - "defaultValue": "active", - "options": [ - { - "label": "$t:admin.users.form.status_options.active", - "value": "active" - }, - { - "label": "$t:admin.users.form.status_options.inactive", - "value": "inactive" - }, - { - "label": "$t:admin.users.form.status_options.blocked", - "value": "blocked" - }, - { - "label": "$t:admin.users.form.status_options.withdrawn", - "value": "withdrawn" + "value": "{{_local.form?.status ?? 'active'}}", + "options": "{{route?.id ? [{label: '$t:admin.users.form.status_options.active', value: 'active'}, {label: '$t:admin.users.form.status_options.inactive', value: 'inactive'}, {label: '$t:admin.users.form.status_options.blocked', value: 'blocked'}, {label: '$t:admin.users.form.status_options.withdrawn', value: 'withdrawn'}] : [{label: '$t:admin.users.form.status_options.active', value: 'active'}, {label: '$t:admin.users.form.status_options.inactive', value: 'inactive'}, {label: '$t:admin.users.form.status_options.blocked', value: 'blocked'}] ?? []}}" + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "form.status": "{{$event.target.value}}" } - ] - } + } + ] } ] } @@ -1463,14 +1457,340 @@ "type": "basic", "name": "Button", "props": { - "disabled": "{{_global.isSaving}}", + "disabled": "{{_global.isSaving || (route?.id && !_local.form?.email)}}", "className": "flex-center gap-2 px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" }, + "actions": [ + { + "type": "click", + "handler": "conditions", + "conditions": [ + { + "comment": "상태를 탈퇴로 바꾸는 저장은 비가역 파기라 확인을 먼저 받는다 (공개이슈 #112).", + "if": "{{route?.id && _local.form?.status === 'withdrawn' && user?.data?.status !== 'withdrawn'}}", + "then": { + "handler": "setState", + "params": { + "target": "local", + "showWithdrawConfirm": true + } + } + }, + { + "then": { + "handler": "sequence", + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isSaving": true + } + }, + { + "handler": "apiCall", + "auth_required": true, + "target": "{{route?.id ? '/api/admin/users/' + route?.id : '/api/admin/users'}}", + "params": { + "method": "{{route?.id ? 'PUT' : 'POST'}}", + "body": "{{_local.form}}" + }, + "errorHandling": { + "403": { + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isSaving": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "$t:auth.permission_denied" + } + } + ] + } + }, + "500": { + "handler": "sequence", + "params": { + "actions": [ + { + "handler": "setState", + "params": { + "target": "global", + "isSaving": false + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "$t:errors.server_error" + } + } + ] + } + } + }, + "onSuccess": [ + { + "handler": "setState", + "params": { + "target": "global", + "isSaving": false + } + }, + { + "comment": "회원 결제 통화 통합 저장 (D9 — 이커머스 설치 시에만. 회원정보 저장 성공 후 같은 흐름에서 통화도 영속 저장). 게이트=편집 모드 + 폼에 통화 필드가 있을 때만. 코어 users 미변경 → 모듈 엔드포인트로 별도 PATCH.", + "if": "{{route?.id && _local.form?.ecommerce_preferred_currency}}", + "handler": "apiCall", + "auth_required": true, + "target": "{{'/api/modules/sirsoft-ecommerce/admin/users/' + route?.id + '/currency'}}", + "params": { + "method": "PATCH", + "body": "{{ { currency: _local.form.ecommerce_preferred_currency } }}" + }, + "onError": [ + { + "handler": "toast", + "params": { + "type": "error", + "message": "{{error.message ?? '$t:common.error_occurred'}}" + } + } + ] + }, + { + "comment": "회원 배송국가 통합 저장 (D-admin — 이커머스 설치 + 해외배송 ON 시에만. 통화와 동형: 코어 users 가 미명시 모듈 키를 drop 하므로 전용 모듈 엔드포인트로 별도 PATCH). 게이트=편집 모드 + 폼에 배송국가 필드가 있을 때만.", + "if": "{{route?.id && _local.form?.ecommerce_preferred_shipping_country}}", + "handler": "apiCall", + "auth_required": true, + "target": "{{'/api/modules/sirsoft-ecommerce/admin/users/' + route?.id + '/shipping-country'}}", + "params": { + "method": "PATCH", + "body": "{{ { shipping_country: _local.form.ecommerce_preferred_shipping_country } }}" + }, + "onError": [ + { + "handler": "toast", + "params": { + "type": "error", + "message": "{{error.message ?? '$t:common.error_occurred'}}" + } + } + ] + }, + { + "handler": "toast", + "params": { + "type": "success", + "message": "{{route?.id ? '$t:admin.users.form.update_success' : '$t:admin.users.form.create_success'}}" + } + }, + { + "handler": "navigate", + "params": { + "path": "/admin/users", + "mergeQuery": true, + "query": {} + } + } + ], + "onError": [ + { + "handler": "setState", + "params": { + "target": "global", + "isSaving": false + } + }, + { + "handler": "setState", + "params": { + "target": "local", + "errors": "{{error.errors ?? null}}" + } + }, + { + "handler": "toast", + "params": { + "type": "error", + "message": "{{error.message ?? '$t:admin.users.form.validation_error'}}" + } + } + ] + } + ] + } + } + ] + } + ], + "children": [ + { + "type": "basic", + "name": "Icon", + "if": "{{!_global.isSaving}}", + "props": { + "name": "save", + "size": "sm" + } + }, + { + "type": "basic", + "name": "Icon", + "if": "{{_global.isSaving}}", + "props": { + "name": "spinner", + "size": "sm", + "spin": true + } + }, + { + "type": "basic", + "name": "Span", + "text": "{{_global.isSaving ? $t:common.saving : $t:common.save}}" + } + ], + "comment": "수정 모드에서는 회원 정보가 폼에 채워지기 전까지 저장을 막는다 — 시드 전에 누르면 빈 본문이 나가 화면에는 값이 보이는데 \"이름은 필수입니다\" 로 거절된다." + } + ] + } + ] + }, + { + "id": "withdraw_confirm_dialog", + "type": "composite", + "name": "Modal", + "comment": "modals 섹션이 아닌 페이지 트리에 두는 이유: modals 는 별도 컨텍스트라 _local.form 이 해석되지 않는다.", + "props": { + "isOpen": true, + "title": "$t:admin.users.modals.withdraw_confirm_title", + "width": "520px", + "closeOnEscape": false, + "closeOnOverlayClick": false, + "showCloseButton": false + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "py-2 space-y-3" + }, + "children": [ + { + "type": "basic", + "name": "P", + "props": { + "className": "text-gray-700 dark:text-gray-300" + }, + "text": "$t:admin.users.modals.withdraw_confirm_lead" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-sm font-medium text-gray-900 dark:text-gray-100" + }, + "text": "$t:admin.users.modals.withdraw_confirm_effects_title" + }, + { + "type": "basic", + "name": "Ul", + "props": { + "className": "text-sm text-gray-600 dark:text-gray-400 space-y-1" + }, + "children": [ + { + "type": "basic", + "name": "Li", + "props": { + "className": "list-disc list-inside" + }, + "text": "$t:admin.users.modals.withdraw_confirm_effect_anonymize" + }, + { + "type": "basic", + "name": "Li", + "props": { + "className": "list-disc list-inside" + }, + "text": "$t:admin.users.modals.withdraw_confirm_effect_sessions" + }, + { + "type": "basic", + "name": "Li", + "props": { + "className": "list-disc list-inside" + }, + "text": "$t:admin.users.modals.withdraw_confirm_effect_linked" + } + ] + } + ] + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "modal-footer-buttons" + }, + "children": [ + { + "id": "withdraw_confirm_cancel_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "className": "btn btn-secondary" + }, + "actions": [ + { + "type": "click", + "handler": "setState", + "params": { + "target": "local", + "showWithdrawConfirm": false + } + } + ], + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:common.cancel" + } + ] + }, + { + "id": "withdraw_confirm_submit_button", + "type": "basic", + "name": "Button", + "props": { + "type": "button", + "disabled": "{{_global.isSaving}}", + "className": "flex-center gap-2 px-4 py-2 bg-red-600 dark:bg-red-500 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + }, "actions": [ { "type": "click", "handler": "sequence", "actions": [ + { + "handler": "setState", + "params": { + "target": "local", + "showWithdrawConfirm": false + } + }, { "handler": "setState", "params": { @@ -1553,7 +1873,7 @@ "handler": "toast", "params": { "type": "error", - "message": "{{error.message}}" + "message": "{{error.message ?? '$t:common.error_occurred'}}" } } ] @@ -1573,7 +1893,7 @@ "handler": "toast", "params": { "type": "error", - "message": "{{error.message}}" + "message": "{{error.message ?? '$t:common.error_occurred'}}" } } ] @@ -1613,7 +1933,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.users.form.validation_error" + "message": "{{error.message ?? '$t:admin.users.form.validation_error'}}" } } ] @@ -1622,35 +1942,17 @@ } ], "children": [ - { - "type": "basic", - "name": "Icon", - "if": "{{!_global.isSaving}}", - "props": { - "name": "save", - "size": "sm" - } - }, - { - "type": "basic", - "name": "Icon", - "if": "{{_global.isSaving}}", - "props": { - "name": "spinner", - "size": "sm", - "spin": true - } - }, { "type": "basic", "name": "Span", - "text": "{{_global.isSaving ? $t:common.saving : $t:common.save}}" + "text": "$t:admin.users.modals.withdraw_confirm_submit" } ] } ] } - ] + ], + "if": "{{_local.showWithdrawConfirm === true}}" } ] } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_list.json b/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_list.json index 0a5fbb5f..c6572bd7 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_list.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/admin_user_list.json @@ -984,6 +984,7 @@ "serverTotalPages": "{{users?.data?.pagination?.last_page ?? null}}", "serverHasMorePages": "{{users?.data?.pagination?.has_more_pages ?? false}}", "selectable": "{{users?.data?.abilities?.can_update === true}}", + "selectionScope": "page", "selectedIds": "{{_global.selectedIds_admin_users || []}}", "idField": "uuid", "showColumnSelector": false, @@ -1887,10 +1888,11 @@ } }, { + "comment": "탈퇴 불가 대상(관리자 계정 등)은 200 응답 안에서 건별로 실패한다 — failed_count 를 읽지 않으면 아무 일도 없었는데 성공으로 안내된다.", "handler": "toast", "params": { - "type": "success", - "message": "$t:admin.users.modals.bulk_withdraw_success" + "type": "{{(response.data?.failed_count ?? 0) === 0 ? 'success' : ((response.data?.updated_count ?? 0) === 0 ? 'error' : 'warning')}}", + "message": "{{(response.data?.failed_count ?? 0) === 0 ? '$t:admin.users.modals.bulk_withdraw_success' : ((response.data?.updated_count ?? 0) === 0 ? '$t:admin.users.modals.bulk_withdraw_all_failed|reason=' + (response.data?.failed_reasons ?? []).join(' / ') : '$t:admin.users.modals.bulk_withdraw_partial|updated=' + (response.data?.updated_count ?? 0) + '|failed=' + (response.data?.failed_count ?? 0) + '|reason=' + (response.data?.failed_reasons ?? []).join(' / '))}}" } } ], diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_activity_log_list/_partial_datagrid.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_activity_log_list/_partial_datagrid.json index c1fa576a..3d6ca64c 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_activity_log_list/_partial_datagrid.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_activity_log_list/_partial_datagrid.json @@ -17,6 +17,7 @@ "alwaysShowPagination": true, "idField": "id", "selectable": true, + "selectionScope": "page", "selectedIds": "{{_global.selectedIds_admin_activity_logs || []}}", "selectedCountText": "", "loading": "{{activityLogs?.loading}}", diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_menu_list/_panel_menu_list.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_menu_list/_panel_menu_list.json index 5c55d2ec..b21de57f 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_menu_list/_panel_menu_list.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_menu_list/_panel_menu_list.json @@ -133,7 +133,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.menus.messages.order_update_failed" + "message": "{{error.message ?? '$t:admin.menus.messages.order_update_failed'}}" } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_module_list/_modal_reactivate_language_packs.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_module_list/_modal_reactivate_language_packs.json index 4b4f86e8..aa0a7e10 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_module_list/_modal_reactivate_language_packs.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_module_list/_modal_reactivate_language_packs.json @@ -49,7 +49,7 @@ "name": "Icon", "props": { "name": "language", - "className": "w-4 h-4 text-gray-500 dark:text-gray-400" + "className": "text-base text-gray-500 dark:text-gray-400" } }, { @@ -174,7 +174,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.modules.modals.reactivate_language_packs_failed", + "message": "{{error.message ?? '$t:admin.modules.modals.reactivate_language_packs_failed'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_notification_log_list/_partial_datagrid.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_notification_log_list/_partial_datagrid.json index b80ae897..bc6af44c 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_notification_log_list/_partial_datagrid.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_notification_log_list/_partial_datagrid.json @@ -17,6 +17,7 @@ "alwaysShowPagination": true, "idField": "id", "selectable": "{{notificationLogs?.data?.abilities?.can_delete === true}}", + "selectionScope": "page", "selectedIds": "{{_global.selectedIds_admin_notification_logs || []}}", "selectedCountText": "", "loading": "{{notificationLogs?.loading}}", diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_plugin_list/_modal_reactivate_language_packs.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_plugin_list/_modal_reactivate_language_packs.json index 598fa03e..bb666881 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_plugin_list/_modal_reactivate_language_packs.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_plugin_list/_modal_reactivate_language_packs.json @@ -49,7 +49,7 @@ "name": "Icon", "props": { "name": "language", - "className": "w-4 h-4 text-gray-500 dark:text-gray-400" + "className": "text-base text-gray-500 dark:text-gray-400" } }, { @@ -174,7 +174,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.plugins.modals.reactivate_language_packs_failed", + "message": "{{error.message ?? '$t:admin.plugins.modals.reactivate_language_packs_failed'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_modal_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_modal_form.json index 04c7e5cc..7698fb37 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_modal_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_modal_form.json @@ -16,6 +16,7 @@ "type": "basic", "name": "Form", "props": { + "id": "schedule_form_modal", "className": "row-stack" }, "children": [ @@ -69,13 +70,23 @@ "props": { "name": "type", "className": "w-full", - "defaultValue": "{{_global.selectedSchedule?.type ?? 'artisan'}}", + "value": "{{_global.scheduleForm?.type ?? 'artisan'}}", "options": [ { "label": "$t:admin.schedules.type.artisan", "value": "artisan" }, { "label": "$t:admin.schedules.type.shell", "value": "shell" }, { "label": "$t:admin.schedules.type.url", "value": "url" } ] - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "global", + "scheduleForm.type": "{{$event.target.value}}" + } + } + ] } ] } @@ -160,7 +171,7 @@ "props": { "name": "frequency", "className": "w-full", - "defaultValue": "{{_global.selectedSchedule?.frequency ?? 'daily'}}", + "value": "{{_global.scheduleForm?.frequency ?? 'daily'}}", "options": [ { "label": "$t:admin.schedules.frequency.everyMinute", "value": "everyMinute" }, { "label": "$t:admin.schedules.frequency.hourly", "value": "hourly" }, @@ -176,7 +187,7 @@ "handler": "setState", "params": { "target": "global", - "scheduleFrequency": "{{$event.target.value}}" + "scheduleForm.frequency": "{{$event.target.value}}" } } ] @@ -186,7 +197,7 @@ { "type": "basic", "name": "Div", - "if": "{{(_global.scheduleFrequency ?? _global.selectedSchedule?.frequency) === 'custom'}}", + "if": "{{(_global.scheduleForm?.frequency ?? 'daily') === 'custom'}}", "children": [ { "type": "basic", @@ -288,9 +299,19 @@ "name": "Toggle", "props": { "name": "without_overlapping", - "checked": "{{_global.selectedSchedule?.without_overlapping ?? true}}", + "checked": "{{_global.scheduleForm?.without_overlapping ?? true}}", "size": "sm" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "global", + "scheduleForm.without_overlapping": "{{$event.target.checked}}" + } + } + ] } ] }, @@ -322,9 +343,19 @@ "name": "Toggle", "props": { "name": "run_in_maintenance", - "checked": "{{_global.selectedSchedule?.run_in_maintenance ?? false}}", + "checked": "{{_global.scheduleForm?.run_in_maintenance ?? false}}", "size": "sm" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "global", + "scheduleForm.run_in_maintenance": "{{$event.target.checked}}" + } + } + ] } ] }, @@ -356,9 +387,19 @@ "name": "Toggle", "props": { "name": "is_active", - "checked": "{{_global.selectedSchedule?.is_active ?? true}}", + "checked": "{{_global.scheduleForm?.is_active ?? true}}", "size": "sm" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "global", + "scheduleForm.is_active": "{{$event.target.checked}}" + } + } + ] } ] } @@ -386,14 +427,14 @@ "body": { "name": "{{form.name}}", "description": "{{form.description}}", - "type": "{{form.type}}", + "type": "{{_global.scheduleForm?.type ?? 'artisan'}}", "command": "{{form.command}}", - "frequency": "{{form.frequency}}", - "expression": "{{form.expression}}", + "frequency": "{{_global.scheduleForm?.frequency ?? 'daily'}}", + "expression": "{{(_global.scheduleForm?.frequency ?? 'daily') === 'custom' ? form.expression : ((_global.scheduleForm?.frequency ?? 'daily') === 'everyMinute' ? '* * * * *' : ((_global.scheduleForm?.frequency ?? 'daily') === 'hourly' ? '0 * * * *' : ((_global.scheduleForm?.frequency ?? 'daily') === 'weekly' ? '0 0 * * 0' : ((_global.scheduleForm?.frequency ?? 'daily') === 'monthly' ? '0 0 1 * *' : '0 0 * * *'))))}}", "timeout": "{{form.timeout}}", - "without_overlapping": "{{form.without_overlapping}}", - "run_in_maintenance": "{{form.run_in_maintenance}}", - "is_active": "{{form.is_active}}" + "without_overlapping": "{{_global.scheduleForm?.without_overlapping ?? true}}", + "run_in_maintenance": "{{_global.scheduleForm?.run_in_maintenance ?? false}}", + "is_active": "{{_global.scheduleForm?.is_active ?? true}}" } }, "onSuccess": [ diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_tab_schedules.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_tab_schedules.json index 6367c85d..b8894b15 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_tab_schedules.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_schedule_list/_tab_schedules.json @@ -1647,6 +1647,7 @@ "alwaysShowPagination": true, "idField": "id", "selectable": true, + "selectionScope": "page", "showColumnSelector": true, "responsiveBreakpoint": 768, "rowActions": [ @@ -1752,7 +1753,14 @@ "params": { "target": "global", "selectedSchedule": "{{$args[1]}}", - "isEditMode": true + "isEditMode": true, + "scheduleForm": { + "type": "{{$args[1].type ?? 'artisan'}}", + "frequency": "{{$args[1].frequency ?? 'daily'}}", + "without_overlapping": "{{!!$args[1].without_overlapping}}", + "run_in_maintenance": "{{!!$args[1].run_in_maintenance}}", + "is_active": "{{!!$args[1].is_active}}" + } } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_definition_add.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_definition_add.json index cdb4908a..ae158d60 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_definition_add.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_definition_add.json @@ -584,7 +584,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.identity.messages.add_modal.save_failed", + "message": "{{error.message ?? '$t:admin.settings.identity.messages.add_modal.save_failed'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_template_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_template_form.json index 4c802556..e68bb5fd 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_template_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_identity_message_template_form.json @@ -317,7 +317,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.identity.messages.modal.preview_failed", + "message": "{{error.message ?? '$t:admin.settings.identity.messages.modal.preview_failed'}}", "type": "error" } } @@ -376,7 +376,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.identity.messages.modal.reset_failed", + "message": "{{error.message ?? '$t:admin.settings.identity.messages.modal.reset_failed'}}", "type": "error" } } @@ -490,7 +490,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.identity.messages.modal.save_failed", + "message": "{{error.message ?? '$t:admin.settings.identity.messages.modal.save_failed'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_mail_template_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_mail_template_form.json index d0be1af9..bac8bca0 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_mail_template_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_mail_template_form.json @@ -591,7 +591,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.mail_templates.save_error", + "message": "{{error.message ?? '$t:admin.settings.mail_templates.save_error'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_notification_template_form.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_notification_template_form.json index 0df07620..fb44af34 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_notification_template_form.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_modal_notification_template_form.json @@ -983,7 +983,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.notification_definitions.save_error", + "message": "{{error.message ?? '$t:admin.settings.notification_definitions.save_error'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_drivers.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_drivers.json index 73ed5cba..f6a7338d 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_drivers.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_drivers.json @@ -81,6 +81,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.storage_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_storage", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.storage_driver && !(_local.form?.available_drivers?.storage ?? []).some(d => d.id === _local.form?.drivers?.storage_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-storage" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.storage_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] } @@ -90,7 +120,8 @@ "id": "s3_settings", "type": "basic", "name": "Div", - "if": "{{_local.form?.drivers?.storage_driver === 's3'}}", + "comment": "공개 자산 스토리지만 s3 인 구성(파일 스토리지는 로컬)에서도 접속 정보를 입력해야 하므로 두 축 중 하나라도 s3 면 노출한다", + "if": "{{_local.form?.drivers?.storage_driver === 's3' || _local.form?.drivers?.public_asset_disk === 's3'}}", "props": { "className": "ml-4 pl-4 border-l-2 border-blue-200 dark:border-blue-800 space-y-4" }, @@ -149,53 +180,31 @@ }, { "type": "basic", - "name": "Div", + "name": "Input", "props": { - "className": "w-full md:w-64" + "type": "text", + "name": "drivers.s3_region", + "placeholder": "ap-northeast-2", + "disabled": "{{_computed.isReadOnly}}", + "className": "{{_local.errors?.['drivers.s3_region'] ? 'input-error md:w-64 font-mono' : 'md:w-64 font-mono'}}" + } + }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.['drivers.s3_region']}}", + "props": { + "className": "form-error" }, - "children": [ - { - "type": "composite", - "name": "Select", - "props": { - "name": "drivers.s3_region", - "disabled": "{{_computed.isReadOnly}}", - "className": "w-full", - "error": "{{_local.errors?.['drivers.s3_region']?.[0] ?? ''}}", - "options": [ - { - "value": "ap-northeast-2", - "label": "$t:admin.settings.drivers.storage.regions.ap_northeast_2" - }, - { - "value": "ap-northeast-1", - "label": "$t:admin.settings.drivers.storage.regions.ap_northeast_1" - }, - { - "value": "us-east-1", - "label": "$t:admin.settings.drivers.storage.regions.us_east_1" - }, - { - "value": "us-west-2", - "label": "$t:admin.settings.drivers.storage.regions.us_west_2" - }, - { - "value": "eu-west-1", - "label": "$t:admin.settings.drivers.storage.regions.eu_west_1" - } - ] - } - }, - { - "type": "basic", - "name": "Span", - "if": "{{_local.errors?.['drivers.s3_region']}}", - "props": { - "className": "form-error" - }, - "text": "{{_local.errors?.['drivers.s3_region']?.[0] ?? ''}}" - } - ] + "text": "{{_local.errors?.['drivers.s3_region']?.[0] ?? ''}}" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-hint mt-1" + }, + "text": "$t:admin.settings.drivers.storage.s3_region_desc" } ] }, @@ -273,6 +282,102 @@ } ] }, + { + "id": "s3_endpoint_field", + "type": "basic", + "name": "Div", + "props": { + "className": "py-3" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:admin.settings.drivers.storage.s3_endpoint" + }, + { + "type": "basic", + "name": "Input", + "props": { + "type": "text", + "name": "drivers.s3_endpoint", + "placeholder": "https://<account-id>.r2.cloudflarestorage.com", + "disabled": "{{_computed.isReadOnly}}", + "className": "{{_local.errors?.['drivers.s3_endpoint'] ? 'input-error' : ''}}" + } + }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.['drivers.s3_endpoint']}}", + "props": { + "className": "form-error" + }, + "text": "{{_local.errors?.['drivers.s3_endpoint']?.[0] ?? ''}}" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-hint mt-1" + }, + "text": "$t:admin.settings.drivers.storage.s3_endpoint_desc" + } + ] + }, + { + "id": "s3_use_path_style_field", + "type": "basic", + "name": "Div", + "props": { + "className": "py-3" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { + "className": "flex-between" + }, + "children": [ + { + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Span", + "props": { + "className": "text-heading" + }, + "text": "$t:admin.settings.drivers.storage.s3_use_path_style" + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "text-label-subtle" + }, + "text": "$t:admin.settings.drivers.storage.s3_use_path_style_desc" + } + ] + }, + { + "type": "composite", + "name": "Toggle", + "props": { + "name": "drivers.s3_use_path_style", + "size": "md", + "disabled": "{{_computed.isReadOnly}}" + } + } + ] + } + ] + }, { "type": "basic", "name": "Div", @@ -332,7 +437,7 @@ "props": { "type": "button", "disabled": "{{_computed.isReadOnly || (_local.testingDriver === 's3')}}", - "className": "px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" + "className": "px-4 py-2 bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 disabled:bg-blue-400 dark:disabled:bg-blue-800 text-white dark:text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" }, "children": [ { @@ -382,6 +487,7 @@ "handler": "apiCall", "target": "/api/admin/settings/test-driver", "auth_required": true, + "comment": "s3_url 은 공개 URL(CDN) base 라 연결 테스트와 무관 — 의도적 미전송", "params": { "method": "POST", "body": { @@ -389,7 +495,9 @@ "s3_bucket": "{{_local.form.drivers?.s3_bucket}}", "s3_region": "{{_local.form.drivers?.s3_region}}", "s3_access_key": "{{_local.form.drivers?.s3_access_key}}", - "s3_secret_key": "{{_local.form.drivers?.s3_secret_key}}" + "s3_secret_key": "{{_local.form.drivers?.s3_secret_key}}", + "s3_endpoint": "{{_local.form.drivers?.s3_endpoint}}", + "s3_use_path_style": "{{!!_local.form.drivers?.s3_use_path_style}}" } }, "onSuccess": [ @@ -422,7 +530,7 @@ "target": "local", "driverTestResults.s3": { "success": false, - "message": "Connection test failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } } }, @@ -430,7 +538,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.drivers.test_failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } }, { @@ -480,6 +588,128 @@ } ] }, + { + "id": "card_public_asset_settings", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card" + }, + "children": [ + { + "type": "basic", + "name": "H3", + "props": { + "className": "card-title" + }, + "text": "$t:admin.settings.drivers.public_asset.title" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "card-description" + }, + "text": "$t:admin.settings.drivers.public_asset.desc" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "row-stack" + }, + "children": [ + { + "id": "field_public_asset_disk", + "type": "basic", + "name": "Div", + "props": { + "className": "" + }, + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:admin.settings.drivers.public_asset.disk" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "w-full md:w-64" + }, + "children": [ + { + "type": "composite", + "name": "Select", + "props": { + "name": "drivers.public_asset_disk", + "disabled": "{{_computed.isReadOnly}}", + "className": "w-full", + "error": "{{_local.errors?.['drivers.public_asset_disk']?.[0] ?? ''}}", + "options": "{{(_local.form?.available_drivers?.public_asset ?? []).map(d => ({value: d.id, label: $localized(d.label)}))}}" + } + }, + { + "type": "basic", + "name": "Span", + "if": "{{_local.errors?.['drivers.public_asset_disk']}}", + "props": { + "className": "form-error" + }, + "text": "{{_local.errors?.['drivers.public_asset_disk']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_public_asset", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.public_asset_disk && !(_local.form?.available_drivers?.public_asset ?? []).some(d => d.id === _local.form?.drivers?.public_asset_disk)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-public_asset" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.public_asset_disk ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] + } + ] + } + ] + }, + { + "id": "public_asset_s3_help", + "type": "basic", + "name": "Div", + "if": "{{_local.form?.drivers?.public_asset_disk === 's3'}}", + "props": { + "className": "text-sm text-gray-500 dark:text-gray-400 mt-2" + }, + "text": "$t:admin.settings.drivers.public_asset.s3_help" + } + ] + } + ] + }, { "id": "card_cache_settings", "type": "basic", @@ -553,6 +783,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.cache_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_cache", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.cache_driver && !(_local.form?.available_drivers?.cache ?? []).some(d => d.id === _local.form?.drivers?.cache_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-cache" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.cache_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] } @@ -751,7 +1011,7 @@ "props": { "type": "button", "disabled": "{{_computed.isReadOnly || (_local.testingDriver === 'redis')}}", - "className": "px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" + "className": "px-4 py-2 bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 disabled:bg-blue-400 dark:disabled:bg-blue-800 text-white dark:text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" }, "children": [ { @@ -841,7 +1101,7 @@ "target": "local", "driverTestResults.redis": { "success": false, - "message": "Connection test failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } } }, @@ -849,7 +1109,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.drivers.test_failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } }, { @@ -1003,7 +1263,7 @@ "props": { "type": "button", "disabled": "{{_computed.isReadOnly || (_local.testingDriver === 'memcached')}}", - "className": "px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" + "className": "px-4 py-2 bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 disabled:bg-blue-400 dark:disabled:bg-blue-800 text-white dark:text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" }, "children": [ { @@ -1091,7 +1351,7 @@ "target": "local", "driverTestResults.memcached": { "success": false, - "message": "Connection test failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } } }, @@ -1099,7 +1359,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.drivers.test_failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } }, { @@ -1211,12 +1471,7 @@ "disabled": "{{_computed.isReadOnly}}", "className": "w-full", "error": "{{_local.errors?.['drivers.search_engine_driver']?.[0] ?? ''}}", - "options": [ - { - "value": "mysql-fulltext", - "label": "$t:admin.settings.drivers.search_engine.options.mysql_fulltext" - } - ] + "options": "{{(_local.form?.available_drivers?.search ?? []).map(d => ({value: d.id, label: $localized(d.label)}))}}" } }, { @@ -1227,6 +1482,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.search_engine_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_search", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.search_engine_driver && !(_local.form?.available_drivers?.search ?? []).some(d => d.id === _local.form?.drivers?.search_engine_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-search" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.search_engine_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] }, @@ -1342,6 +1627,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.session_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_session", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.session_driver && !(_local.form?.available_drivers?.session ?? []).some(d => d.id === _local.form?.drivers?.session_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-session" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.session_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] }, @@ -1501,6 +1816,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.queue_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_queue", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.queue_driver && !(_local.form?.available_drivers?.queue ?? []).some(d => d.id === _local.form?.drivers?.queue_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-queue" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.queue_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] }, @@ -1655,6 +2000,36 @@ "className": "form-error" }, "text": "{{_local.errors?.['drivers.log_driver']?.[0] ?? ''}}" + }, + { + "id": "unavailable_driver_notice_log", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.drivers?.log_driver && !(_local.form?.available_drivers?.log ?? []).some(d => d.id === _local.form?.drivers?.log_driver)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-log" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.drivers?.log_driver ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] } @@ -2489,7 +2864,7 @@ "props": { "type": "button", "disabled": "{{_computed.isReadOnly || (_local.testingDriver === 'websocket')}}", - "className": "px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" + "className": "px-4 py-2 bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 disabled:bg-blue-400 dark:disabled:bg-blue-800 text-white dark:text-white text-sm font-medium rounded-lg flex-center gap-2 transition-colors" }, "children": [ { @@ -2545,7 +2920,10 @@ "websocket_enabled": true, "websocket_host": "{{_local.form.drivers?.websocket_host}}", "websocket_port": "{{_local.form.drivers?.websocket_port}}", - "websocket_scheme": "{{_local.form.drivers?.websocket_scheme}}" + "websocket_scheme": "{{_local.form.drivers?.websocket_scheme}}", + "websocket_server_host": "{{_local.form.drivers?.websocket_server_host}}", + "websocket_server_port": "{{_local.form.drivers?.websocket_server_port}}", + "websocket_server_scheme": "{{_local.form.drivers?.websocket_server_scheme}}" } }, "onSuccess": [ @@ -2578,7 +2956,7 @@ "target": "local", "driverTestResults.websocket": { "success": false, - "message": "Connection test failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } } }, @@ -2586,7 +2964,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.drivers.test_failed" + "message": "{{error.message ?? '$t:admin.settings.drivers.test_failed'}}" } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json index ef0b95d2..29916465 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_general.json @@ -307,7 +307,7 @@ "params": { "target": "local", "hasChanges": true, - "form.general.site_logo": "{{(_local.form?.general?.site_logo ?? []).filter(item => item.id !== $args[0])}}" + "form.general.site_logo": "{{(_local.form?.general?.site_logo ?? []).filter(item => (item.hash || item.id) !== $args[0])}}" } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_messages.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_messages.json index 91a5d58d..edfd214d 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_messages.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_messages.json @@ -422,7 +422,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.settings.identity.messages.toggle_failed", + "message": "{{error.message ?? '$t:admin.settings.identity.messages.toggle_failed'}}", "variant": "error" } }, diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json index e27471f3..dd8d36c1 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_identity_policies.json @@ -619,7 +619,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? '$t:admin.identity.policies.toggle_failed'}}" } }, { @@ -956,7 +956,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.identity.policies.toggle_failed" + "message": "{{error.message ?? '$t:admin.identity.policies.toggle_failed'}}" } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_info.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_info.json index 90917078..90eb08c8 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_info.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_info.json @@ -354,7 +354,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.settings.info.changelog_load_failed" + "message": "{{error.message ?? '$t:admin.settings.info.changelog_load_failed'}}" } } ] diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_mail.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_mail.json index 48283c49..056eea90 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_mail.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_mail.json @@ -85,6 +85,36 @@ "options": "{{(_local.form?.available_drivers?.mail ?? []).map(d => ({value: d.id, label: $localized(d.label)}))}}", "error": "{{_local.errors?.['mail.mailer']?.[0] ?? ''}}" } + }, + { + "id": "unavailable_driver_notice_mail", + "type": "basic", + "name": "P", + "if": "{{!!_local.form?.mail?.mailer && !(_local.form?.available_drivers?.mail ?? []).some(d => d.id === _local.form?.mail?.mailer)}}", + "props": { + "className": "text-warning-soft mt-2", + "data-testid": "driver-unavailable-mail" + }, + "children": [ + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_prefix" + }, + { + "type": "basic", + "name": "Span", + "props": { + "className": "font-mono" + }, + "text": "{{_local.form?.mail?.mailer ?? ''}}" + }, + { + "type": "basic", + "name": "Span", + "text": "$t:admin.settings.drivers.common.unavailable_saved_value_suffix" + } + ] } ] } @@ -813,7 +843,7 @@ "name": "Button", "props": { "disabled": "{{_computed.isReadOnly || _local.isSendingTest}}", - "className": "flex-center justify-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" + "className": "flex-center justify-center gap-2 px-4 py-2 bg-blue-600 dark:bg-blue-500 text-white dark:text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-600 font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap" }, "actions": [ { @@ -915,7 +945,7 @@ "if": "{{!_local.isSendingTest}}", "props": { "name": "paper-plane", - "className": "w-4 h-4" + "className": "text-base" } }, { diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_upload.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_upload.json index e34f32e6..596459a0 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_upload.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_settings/_tab_upload.json @@ -347,6 +347,104 @@ ] } ] + }, + { + "id": "card_orphan_cleanup", + "type": "basic", + "name": "Div", + "props": { + "className": "admin-card" + }, + "children": [ + { + "type": "basic", + "name": "H3", + "props": { + "className": "card-title" + }, + "text": "$t:admin.settings.upload.orphan_cleanup" + }, + { + "type": "basic", + "name": "Div", + "props": { + "className": "card-description" + }, + "text": "$t:admin.settings.upload.orphan_cleanup_desc" + }, + { + "type": "basic", + "name": "Div", + "children": [ + { + "id": "field_orphan_cleanup_enabled", + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:admin.settings.upload.orphan_cleanup_enabled" + }, + { + "type": "composite", + "name": "Toggle", + "props": { + "name": "upload.orphan_cleanup_enabled", + "disabled": "{{_computed.isReadOnly}}" + } + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "form-hint" + }, + "text": "$t:admin.settings.upload.orphan_cleanup_enabled_hint" + } + ] + }, + { + "id": "field_orphan_retention_days", + "type": "basic", + "name": "Div", + "children": [ + { + "type": "basic", + "name": "Label", + "props": { + "className": "form-label" + }, + "text": "$t:admin.settings.upload.orphan_retention_days" + }, + { + "type": "basic", + "name": "Input", + "props": { + "type": "number", + "name": "upload.orphan_retention_days", + "min": "{{_local?.form?._meta?.limits?.upload_orphan_retention_days_min ?? 1}}", + "max": "{{_local?.form?._meta?.limits?.upload_orphan_retention_days_max ?? 3650}}", + "disabled": "{{_computed.isReadOnly}}", + "className": "input" + } + }, + { + "type": "basic", + "name": "P", + "props": { + "className": "form-hint" + }, + "text": "$t:admin.settings.upload.orphan_retention_days_hint" + } + ] + } + ] + } + ] } ], "props": {} diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_layout_edit/_modal_extension_preview_layout.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_layout_edit/_modal_extension_preview_layout.json index 39269c8f..1fc8f9d0 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_layout_edit/_modal_extension_preview_layout.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_layout_edit/_modal_extension_preview_layout.json @@ -101,7 +101,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:admin.templates.layout_edit.preview_error" + "message": "{{error.message ?? '$t:admin.templates.layout_edit.preview_error'}}" } } ] diff --git a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_list/_modal_reactivate_language_packs.json b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_list/_modal_reactivate_language_packs.json index d5c4d172..fd5b71ad 100644 --- a/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_list/_modal_reactivate_language_packs.json +++ b/templates/_bundled/sirsoft-admin_basic/layouts/partials/admin_template_list/_modal_reactivate_language_packs.json @@ -49,7 +49,7 @@ "name": "Icon", "props": { "name": "language", - "className": "w-4 h-4 text-gray-500 dark:text-gray-400" + "className": "text-base text-gray-500 dark:text-gray-400" } }, { @@ -174,7 +174,7 @@ { "handler": "toast", "params": { - "message": "$t:admin.templates.modals.reactivate_language_packs_failed", + "message": "{{error.message ?? '$t:admin.templates.modals.reactivate_language_packs_failed'}}", "type": "error" } } diff --git a/templates/_bundled/sirsoft-admin_basic/package-lock.json b/templates/_bundled/sirsoft-admin_basic/package-lock.json index fa20a761..04fc26af 100644 --- a/templates/_bundled/sirsoft-admin_basic/package-lock.json +++ b/templates/_bundled/sirsoft-admin_basic/package-lock.json @@ -1,12 +1,12 @@ { "name": "sirsoft-admin_basic", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sirsoft-admin_basic", - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/templates/_bundled/sirsoft-admin_basic/package.json b/templates/_bundled/sirsoft-admin_basic/package.json index 9895f6fe..745366a6 100644 --- a/templates/_bundled/sirsoft-admin_basic/package.json +++ b/templates/_bundled/sirsoft-admin_basic/package.json @@ -1,6 +1,6 @@ { "name": "sirsoft-admin_basic", - "version": "1.0.4", + "version": "1.0.5", "description": "Gnuboard7 Basic Admin Template Components", "type": "module", "main": "dist/components.js", diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/basic/IconTypes.ts b/templates/_bundled/sirsoft-admin_basic/src/components/basic/IconTypes.ts index 403e30a0..4de3927a 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/basic/IconTypes.ts +++ b/templates/_bundled/sirsoft-admin_basic/src/components/basic/IconTypes.ts @@ -1,3 +1,4 @@ +// audit:allow extension-dist-source-literal-sync 타입/미소비 상수 중심 모듈 — 타입 소거·tree-shake 로 일부 리터럴이 번들에 남지 않는다 (fresh 빌드로 실측 — stale 아님) /** * Font Awesome 아이콘 스타일 */ diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/DataGrid.tsx b/templates/_bundled/sirsoft-admin_basic/src/components/composite/DataGrid.tsx index 71b8fb24..93583096 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/composite/DataGrid.tsx +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/DataGrid.tsx @@ -216,6 +216,18 @@ export interface DataGridProps { selectedIds?: (string | number)[]; onSelectionChange?: (ids: (string | number)[]) => void; idField?: string; + /** + * 선택의 유효 범위. + * + * - `'page'`: 선택은 지금 그려진 행에서만 유효하다. 검색·필터·페이지 이동으로 + * 행이 목록에서 빠지면 그 행의 선택도 함께 버린다. 일괄 처리 버튼이 달린 + * 목록 화면은 이 값을 쓴다 — 그래야 "보이지 않는 행이 일괄 처리 대상" 이 + * 되는 상태가 만들어지지 않는다. + * - `'free'`(기본): 선택을 컴포넌트가 건드리지 않는다. 선택 자체가 저장 대상인 + * 폼(쿠폰 적용 대상 고르기 등)처럼, 목록에 안 보이는 항목도 선택으로 남아야 + * 하는 화면용. + */ + selectionScope?: 'page' | 'free'; // 컬럼 표시/숨김 기능 showColumnSelector?: boolean; @@ -648,6 +660,7 @@ export const DataGrid: React.FC<DataGridProps> = ({ selectedIds = [], onSelectionChange, idField = 'id', + selectionScope = 'free', // 컬럼 표시/숨김 showColumnSelector = false, visibleColumns: initialVisibleColumns, @@ -713,6 +726,27 @@ export const DataGrid: React.FC<DataGridProps> = ({ const resolvedRequiredText = requiredText ?? t('common.required'); const resolvedSelectedCountText = selectedCountText ?? t('common.selected_count'); + /** + * 선택 건수 문구의 파라미터 자리를 채웁니다. + * + * 치환은 번역 엔진에 맡깁니다 — G7 의 파라미터 표준은 `{{count}}` 이고, 엔진은 + * `{{count}}` 를 먼저, `{count}` 를 그다음에 치환합니다. 컴포넌트가 자체 치환을 + * 두면 표준 표기가 `{1}개 선택됨` 처럼 바깥 중괄호를 남긴 채 렌더됩니다 + * (예외·경고 없이 화면에만 드러남). + * + * `selectedCountText` prop 으로 넘어온 문자열은 엔진을 거치지 않으므로 이때만 + * 같은 순서로 직접 채웁니다. + */ + const formatSelectedCount = (count: number): string => { + if (selectedCountText === undefined) { + return t('common.selected_count', { count }); + } + + return selectedCountText + .replace(/\{\{count\}\}/g, String(count)) + .replace(/\{count\}/g, String(count)); + }; + // G7Core.useControllableState 훅 참조 (Phase 2-2) const G7Core = (window as any).G7Core; const useControllableState = G7Core?.useControllableState as @@ -1008,6 +1042,28 @@ export const DataGrid: React.FC<DataGridProps> = ({ [selectedIds, onSelectionChange] ); + // 목록에서 빠진 행의 선택 정리 + // + // 선택 상태는 목록 화면 밖(전역/로컬 상태)에 있으므로 검색·필터·페이지 이동으로 + // 행이 화면에서 사라져도 그대로 남는다. 그 상태에서 일괄 처리를 누르면 운영자가 + // 보고 있지도, 체크하지도 않은 행이 대상이 된다 — 확인 모달은 건수만 말하므로 + // 실행 전에 알아챌 방법이 없다. `selectionScope: 'page'` 인 목록은 지금 그려진 + // 행에 없는 선택을 버려서, 대상이 언제나 "화면에 보이고 체크된 행" 이 되게 한다. + useEffect(() => { + // `selectable` 여부로 가르지 않는다 — 체크박스가 꺼진 화면에 남은 선택은 + // 아예 눈에 띄지 않으므로 오히려 더 위험하다. + if (selectionScope !== 'page' || !onSelectionChange) return; + if (selectedIds.length === 0) return; + + const visibleIds = new Set((paginatedData ?? []).map((row) => row[idField])); + const survivors = selectedIds.filter((id) => visibleIds.has(id)); + + // 길이가 같으면 버릴 것이 없다 — 여기서 끊지 않으면 setState 가 매 렌더 반복된다. + if (survivors.length === selectedIds.length) return; + + onSelectionChange(survivors); + }, [selectionScope, selectable, paginatedData, selectedIds, onSelectionChange, idField]); + // 컬럼 표시/숨김 토글 const handleColumnToggle = useCallback( (field: string) => { @@ -1270,7 +1326,7 @@ export const DataGrid: React.FC<DataGridProps> = ({ {/* 선택된 항목 수 표시 */} {selectable && selectedIds.length > 0 && resolvedSelectedCountText && ( <Div className="mb-4 text-sm text-gray-600 dark:text-gray-400"> - {resolvedSelectedCountText.replace('{count}', String(selectedIds.length))} + {formatSelectedCount(selectedIds.length)} </Div> )} @@ -1390,7 +1446,7 @@ export const DataGrid: React.FC<DataGridProps> = ({ {/* 선택된 항목 수 표시 */} {selectable && selectedIds.length > 0 && resolvedSelectedCountText && ( <Div className="mb-4 text-sm text-gray-600 dark:text-gray-400"> - {resolvedSelectedCountText.replace('{count}', String(selectedIds.length))} + {formatSelectedCount(selectedIds.length)} </Div> )} diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx b/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx index cb2805e2..766ae162 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx @@ -19,6 +19,7 @@ import { Img } from '../../basic/Img'; import type { Attachment, PendingFile } from './types'; import { getFileIcon, t } from './utils'; +import { isCrossOriginAssetUrl } from '../assetOrigin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const G7Core = (window as any).G7Core; @@ -89,6 +90,13 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ return; } + // 공개 자산 디스크(S3/CDN)가 준 교차 출처 URL 은 인증이 필요 없는 공개 자산이다. + // XHR 로 가져오면 CORS 미설정 CDN 에서 실패하므로 URL 을 그대로 사용한다. + if (isCrossOriginAssetUrl(downloadUrl)) { + setAuthenticatedImageUrl(downloadUrl); + return; + } + let isMounted = true; let objectUrl: string | undefined; @@ -158,9 +166,9 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ {/* 업로드/압축 진행률 오버레이 */} {(isUploading || isCompressing) && ( <Div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center"> - <Div className="w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden"> + <Div className="w-3/4 h-2 bg-gray-700 dark:bg-gray-600 rounded-full overflow-hidden"> <Div - className="h-full bg-blue-500 transition-all" + className="h-full bg-blue-500 dark:bg-blue-400 transition-all" style={{ width: `${progress}%` }} /> </Div> @@ -170,7 +178,7 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ {/* 에러 오버레이 */} {hasError && ( - <Div className="absolute inset-0 bg-red-500/50 flex items-center justify-center"> + <Div className="absolute inset-0 bg-red-500/50 dark:bg-red-600/50 flex items-center justify-center"> <I className="fa-solid fa-exclamation-triangle text-2xl text-white" /> </Div> )} diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/useFileUploader.ts b/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/useFileUploader.ts index 95098c22..6caf56f9 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/useFileUploader.ts +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/FileUploader/useFileUploader.ts @@ -22,6 +22,7 @@ import imageCompression from 'browser-image-compression'; import type { Attachment, PendingFile, FileUploaderProps, ApiEndpoints } from './types'; import { formatFileSize, extractErrorMessage, t } from './utils'; +import { isCrossOriginAssetUrl } from '../assetOrigin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const G7Core = (window as any).G7Core; @@ -710,6 +711,14 @@ export function useFileUploader(options: UseFileUploaderOptions): UseFileUploade // ref 기반 has 체크 (stale closure 방지) if (authenticatedImageUrlsRef.current.has(file.id)) continue; + // 공개 자산 디스크(S3/CDN)의 교차 출처 URL 은 공개 자산이므로 XHR 없이 직접 사용한다. + // (CORS 미설정 CDN 에서의 실패 방지 + 제3자 origin 으로의 토큰 노출 차단) + if (isCrossOriginAssetUrl(file.download_url)) { + authenticatedImageUrlsRef.current.set(file.id, file.download_url); + setAuthenticatedImageUrls(new Map(authenticatedImageUrlsRef.current)); + continue; + } + try { const blob = await G7Core.api.get(file.download_url, { responseType: 'blob', diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/ImageGallery.tsx b/templates/_bundled/sirsoft-admin_basic/src/components/composite/ImageGallery.tsx index 30d09296..7774b150 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/composite/ImageGallery.tsx +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/ImageGallery.tsx @@ -15,6 +15,7 @@ import 'yet-another-react-lightbox/plugins/thumbnails.css'; import { Button } from '../basic/Button'; import { I } from '../basic/I'; +import { isCrossOriginAssetUrl } from './assetOrigin'; import { Div } from '../basic/Div'; import { Img } from '../basic/Img'; import type { EditorAttrs } from '../../types'; @@ -137,7 +138,9 @@ export const executeImageDownload = async (image: GalleryImage): Promise<void> = const downloadUrl = image.downloadUrl || image.src; const filename = image.filename || image.title || 'image'; - if (image.downloadRequiresAuth) { + // 공개 자산 디스크(S3/CDN)의 교차 출처 URL 은 인증이 필요 없는 공개 자산이다. + // 인증 XHR 로 가져오면 CORS 미설정 CDN 에서 실패하므로 일반 링크 다운로드를 쓴다. + if (image.downloadRequiresAuth && !isCrossOriginAssetUrl(downloadUrl)) { await downloadAuthenticatedFile(downloadUrl, filename); } else { downloadFile(downloadUrl, filename); diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx b/templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx index b46f3430..4820801f 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx @@ -1219,6 +1219,8 @@ describe('DataGrid', () => { * `renderCellChildren` 으로 넘기고 `cell.text` 는 원문 그대로 출력해 왔다. * 그 결과 `{{order.data?.total_quantity ?? 0}}개` 같은 값이 화면에 **원본 문자열로 노출**된다. * (브라우저 실측: `/admin/ecommerce/orders/:orderNumber` 합계 행 3셀) + * + * @effects datagrid_footer_cell_text_binding_resolved */ describe('푸터 행 text 바인딩 해석', () => { const footerColumns: DataGridColumn[] = [ @@ -1320,4 +1322,292 @@ describe('DataGrid', () => { expect(Object.keys(seen[0])).toContain('Pre'); }); }); + + describe('선택 건수 문구', () => { + /** + * G7 파라미터 표준은 `{{count}}` 다. 컴포넌트가 자체 치환을 두고 단일 중괄호만 + * 처리하면 표준 표기가 `{1}개 선택됨` 처럼 바깥 중괄호를 남긴 채 렌더된다 — + * 예외도 경고도 없이 화면에만 드러난다 (실제 제보 경로). + */ + it('표준 표기의 번역 문구가 그대로 렌더되지 않고 치환된다', async () => { + (window as any).G7Core = { + ...(window as any).G7Core, + t: (key: string, params?: Record<string, string | number>) => { + if (key !== 'common.selected_count') return key; + + // 런타임 TranslationEngine 과 같은 순서: 이중 먼저, 단일 다음 + let text = '{{count}}개 선택됨'; + for (const [k, v] of Object.entries(params ?? {})) { + text = text + .replace(new RegExp(`\\{\\{${k}\\}\\}`, 'g'), String(v)) + .replace(new RegExp(`\\{${k}\\}`, 'g'), String(v)); + } + + return text; + }, + }; + + render( + <DataGrid + columns={[{ field: 'name', header: '이름' }]} + data={[{ id: 1, name: 'A' }]} + selectable + selectedIds={[1]} + idField="id" + /> + ); + + expect(await screen.findByText('1개 선택됨')).toBeInTheDocument(); + expect(screen.queryByText('{1}개 선택됨')).not.toBeInTheDocument(); + }); + + /** + * prop 으로 직접 넘긴 문구는 번역 엔진을 거치지 않는다 — 두 표기 모두 채워야 한다. + */ + it.each([ + ['{{count}}개 선택', '2개 선택'], + ['{count}개 선택', '2개 선택'], + ])('prop 문구 %s 를 %s 로 채운다', async (template, expected) => { + render( + <DataGrid + columns={[{ field: 'name', header: '이름' }]} + data={[ + { id: 1, name: 'A' }, + { id: 2, name: 'B' }, + ]} + selectable + selectedIds={[1, 2]} + idField="id" + selectedCountText={template} + /> + ); + + expect(await screen.findByText(expected)).toBeInTheDocument(); + }); + }); + + describe('선택의 유효 범위 (selectionScope)', () => { + const columns: DataGridColumn[] = [{ field: 'name', header: '이름' }]; + + /** + * 선택 상태는 목록 화면 밖(전역/로컬 상태)에 있어서 검색·필터·페이지 이동으로 + * 행이 화면에서 빠져도 남는다. 그 상태로 일괄 처리를 누르면 운영자가 보지도, + * 체크하지도 않은 행이 대상이 된다 — 확인 모달은 건수만 말하므로 실행 전에 + * 알아챌 방법이 없다. + * + * @scenario scope=page, transition=search_filter_change + * @effects hidden_row_selection_dropped + */ + it('page 범위에서 검색으로 사라진 행의 선택을 버린다', async () => { + const onSelectionChange = vi.fn(); + + const { rerender } = render( + <DataGrid + columns={columns} + data={[ + { id: 'a', name: '관리자C' }, + { id: 'b', name: '임영철' }, + ]} + selectable + selectedIds={['a']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + expect(onSelectionChange).not.toHaveBeenCalled(); + + // 검색 결과: 임영철만 남는다 (관리자C 는 화면에서 사라짐) + rerender( + <DataGrid + columns={columns} + data={[{ id: 'b', name: '임영철' }]} + selectable + selectedIds={['a']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + await waitFor(() => expect(onSelectionChange).toHaveBeenCalledWith([])); + }); + + /** + * @scenario scope=page, transition=search_filter_change + * @effects visible_row_selection_survives + */ + it('page 범위에서 화면에 남은 선택은 유지한다', async () => { + const onSelectionChange = vi.fn(); + + const { rerender } = render( + <DataGrid + columns={columns} + data={[ + { id: 'a', name: '관리자C' }, + { id: 'b', name: '임영철' }, + ]} + selectable + selectedIds={['a', 'b']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + rerender( + <DataGrid + columns={columns} + data={[{ id: 'b', name: '임영철' }]} + selectable + selectedIds={['a', 'b']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + await waitFor(() => expect(onSelectionChange).toHaveBeenCalledWith(['b'])); + }); + + /** + * 서버 페이지네이션 목록은 페이지를 넘기면 data 가 그 페이지 행으로 통째로 바뀐다 — + * 앞 페이지에서 고른 행은 화면에 없으므로 대상에서 빠져야 한다. + * + * @scenario scope=page, transition=page_change + * @effects hidden_row_selection_dropped + */ + it('page 범위에서 페이지를 넘기면 앞 페이지 선택을 버린다', async () => { + const onSelectionChange = vi.fn(); + const pageOne = [ + { id: 'p1a', name: '1페이지 A' }, + { id: 'p1b', name: '1페이지 B' }, + ]; + const pageTwo = [ + { id: 'p2a', name: '2페이지 A' }, + { id: 'p2b', name: '2페이지 B' }, + ]; + + const { rerender } = render( + <DataGrid + columns={columns} + data={pageOne} + selectable + selectedIds={['p1a', 'p1b']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + serverSidePagination + /> + ); + + expect(onSelectionChange).not.toHaveBeenCalled(); + + rerender( + <DataGrid + columns={columns} + data={pageTwo} + selectable + selectedIds={['p1a', 'p1b']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + serverSidePagination + /> + ); + + await waitFor(() => expect(onSelectionChange).toHaveBeenCalledWith([])); + }); + + /** + * 체크박스가 꺼진 화면(권한 없음)에 남은 선택은 아예 눈에 띄지 않는다 — + * `selectable` 로 가르면 그 상태가 그대로 통과한다. + * + * @scenario scope=page, transition=initial_mount + * @effects selection_cleaned_even_when_selectable_off + */ + it('page 범위에서는 selectable 이 꺼져 있어도 정리한다', async () => { + const onSelectionChange = vi.fn(); + + render( + <DataGrid + columns={columns} + data={[{ id: 'b', name: '임영철' }]} + selectable={false} + selectedIds={['a']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + await waitFor(() => expect(onSelectionChange).toHaveBeenCalledWith([])); + }); + + /** + * 선택 자체가 저장 대상인 폼(쿠폰 적용 대상 고르기 등)은 목록에 안 보이는 항목도 + * 선택으로 남아야 한다 — 기본값이 그 화면들을 건드리면 안 된다. + * + * @scenario scope=free, transition=search_filter_change + * @effects free_scope_leaves_selection_untouched + */ + it('기본값(free)에서는 선택을 건드리지 않는다', async () => { + const onSelectionChange = vi.fn(); + + const { rerender } = render( + <DataGrid + columns={columns} + data={[{ id: 'a', name: '상품A' }]} + selectable + selectedIds={['a']} + onSelectionChange={onSelectionChange} + idField="id" + /> + ); + + rerender( + <DataGrid + columns={columns} + data={[{ id: 'z', name: '상품Z' }]} + selectable + selectedIds={['a']} + onSelectionChange={onSelectionChange} + idField="id" + /> + ); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(onSelectionChange).not.toHaveBeenCalled(); + }); + + /** + * 정리할 것이 없는데도 콜백이 나가면 상위가 setState → 리렌더 → 다시 콜백으로 + * 무한 루프가 된다. + * + * @scenario scope=page, transition=initial_mount + * @effects no_callback_when_nothing_to_drop + */ + it('버릴 선택이 없으면 콜백을 부르지 않는다', async () => { + const onSelectionChange = vi.fn(); + + render( + <DataGrid + columns={columns} + data={[ + { id: 'a', name: 'A' }, + { id: 'b', name: 'B' }, + ]} + selectable + selectedIds={['a', 'b']} + onSelectionChange={onSelectionChange} + idField="id" + selectionScope="page" + /> + ); + + await new Promise((resolve) => setTimeout(resolve, 20)); + expect(onSelectionChange).not.toHaveBeenCalled(); + }); + }); }); diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/composite/assetOrigin.ts b/templates/_bundled/sirsoft-admin_basic/src/components/composite/assetOrigin.ts new file mode 100644 index 00000000..62679c79 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/src/components/composite/assetOrigin.ts @@ -0,0 +1,38 @@ +/** + * 자산 URL 출처 판정 유틸리티 + * + * 공개 자산 스토리지(S3/CDN)를 켜면 첨부·이미지의 `download_url` 이 외부 origin + * 절대 URL 이 된다. 이런 URL 은 인증이 필요 없는 공개 자산이므로 XHR(Blob) 로 + * 가져오면 안 된다 — 교차 출처 XHR 은 CDN 이 CORS 헤더를 주지 않는 한 응답을 + * 읽지 못해 이미지가 통째로 실패하고, 응답을 읽을 수 있는 CDN 이라면 이번에는 + * 세션 토큰이 제3자 origin 으로 나간다. 교차 출처 URL 은 `<img src>` / 링크로 + * 직접 사용한다. + * + * @module composite/assetOrigin + */ + +/** + * URL 이 현재 문서와 다른 출처인지 판정합니다. + * + * 상대 경로는 항상 동일 출처입니다. 절대 URL(`https://…`)과 프로토콜 상대 + * URL(`//host/…`)만 출처를 비교하며, 파싱 불가하거나 브라우저 밖이면 + * 동일 출처로 간주합니다(기존 인증 경로 유지). + * + * @param url 판정할 URL + * @returns 교차 출처 여부 + */ +export const isCrossOriginAssetUrl = (url?: string | null): boolean => { + if (!url || typeof window === 'undefined' || !window.location) { + return false; + } + + if (!/^(https?:)?\/\//i.test(url)) { + return false; + } + + try { + return new URL(url, window.location.href).origin !== window.location.origin; + } catch { + return false; + } +}; diff --git a/templates/_bundled/sirsoft-admin_basic/src/components/modals/ConfirmModal.tsx b/templates/_bundled/sirsoft-admin_basic/src/components/modals/ConfirmModal.tsx index ca4a659d..7898208f 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/components/modals/ConfirmModal.tsx +++ b/templates/_bundled/sirsoft-admin_basic/src/components/modals/ConfirmModal.tsx @@ -1,3 +1,4 @@ +// audit:allow extension-dist-source-literal-sync 미등록(components.json 부재)·미참조 컴포넌트 — 배럴(modals/index.ts)만 재수출할 뿐 소비처가 없어 tree-shake 된다 import React, { useEffect, useRef } from 'react'; import { Div } from '../basic/Div'; import { Button } from '../basic/Button'; diff --git a/templates/_bundled/sirsoft-admin_basic/src/styles/main.css b/templates/_bundled/sirsoft-admin_basic/src/styles/main.css index 5603792d..134ac1bc 100644 --- a/templates/_bundled/sirsoft-admin_basic/src/styles/main.css +++ b/templates/_bundled/sirsoft-admin_basic/src/styles/main.css @@ -16,7 +16,7 @@ */ @source "../../layouts/**/*.json"; /* SAFELIST-START */ -@source inline("bg-blue-600 bg-gray-100 bg-gray-50 bg-gray-800 bg-gray-900 bg-red-600 bg-white block border border-0 border-blue-600 border-dashed border-dotted border-gray-300 border-gray-500 border-gray-700 border-gray-900 border-green-600 border-red-600 border-solid border-white dark:bg-amber-500 dark:bg-amber-900 dark:bg-amber-900/20 dark:bg-amber-900/30 dark:bg-amber-900/40 dark:bg-blue-400 dark:bg-blue-500 dark:bg-blue-600 dark:bg-blue-800 dark:bg-blue-900 dark:bg-blue-900/20 dark:bg-blue-900/30 dark:bg-blue-900/40 dark:bg-blue-900/50 dark:bg-emerald-400 dark:bg-gray-100 dark:bg-gray-50 dark:bg-gray-600 dark:bg-gray-700 dark:bg-gray-700/30 dark:bg-gray-700/50 dark:bg-gray-800 dark:bg-gray-800/50 dark:bg-gray-900 dark:bg-gray-900/50 dark:bg-gray-950 dark:bg-green-900 dark:bg-green-900/20 dark:bg-green-900/30 dark:bg-indigo-500 dark:bg-indigo-900 dark:bg-indigo-900/30 dark:bg-orange-900 dark:bg-purple-800/50 dark:bg-purple-900 dark:bg-purple-900/20 dark:bg-purple-900/30 dark:bg-purple-900/50 dark:bg-red-500 dark:bg-red-600 dark:bg-red-700 dark:bg-red-900 dark:bg-red-900/20 dark:bg-red-900/30 dark:bg-rose-400 dark:bg-slate-300 dark:bg-slate-700 dark:bg-slate-800 dark:bg-white dark:bg-yellow-700 dark:bg-yellow-900 dark:bg-yellow-900/30 dark:bg-yellow-900/40 dark:block dark:border dark:border-0 dark:border-amber-400 dark:border-amber-600 dark:border-amber-700 dark:border-amber-800 dark:border-blue-600 dark:border-blue-700 dark:border-blue-800 dark:border-dashed dark:border-dotted dark:border-gray-300 dark:border-gray-500 dark:border-gray-600 dark:border-gray-700 dark:border-gray-900 dark:border-green-600 dark:border-green-700 dark:border-green-800 dark:border-indigo-400 dark:border-orange-600 dark:border-purple-600 dark:border-purple-700 dark:border-red-500 dark:border-red-600 dark:border-red-700 dark:border-red-800 dark:border-solid dark:border-white dark:border-yellow-600 dark:disabled:bg-gray-600 dark:disabled:hover:bg-indigo-500 dark:disabled:text-gray-600 dark:divide-purple-800 dark:flex dark:flex-1 dark:flex-auto dark:flex-col dark:flex-none dark:flex-row dark:flex-wrap dark:focus:ring-amber-400 dark:focus:ring-blue-400 dark:focus:ring-indigo-400 dark:focus:ring-offset-gray-900 dark:focus:ring-slate-500 dark:font-bold dark:font-medium dark:font-normal dark:from-blue-900 dark:hover:bg-amber-600 dark:hover:bg-blue-600 dark:hover:bg-gray-100 dark:hover:bg-gray-200 dark:hover:bg-gray-500 dark:hover:bg-gray-600 dark:hover:bg-gray-700 dark:hover:bg-gray-700/30 dark:hover:bg-indigo-600 dark:hover:bg-indigo-900/30 dark:hover:bg-orange-900/30 dark:hover:bg-purple-900/30 dark:hover:bg-red-600 dark:hover:bg-red-800 dark:hover:bg-red-900/20 dark:hover:bg-red-900/30 dark:hover:bg-slate-200 dark:hover:bg-slate-600 dark:hover:bg-yellow-800 dark:hover:text-blue-300 dark:hover:text-blue-400 dark:hover:text-gray-200 dark:hover:text-gray-300 dark:hover:text-purple-200 dark:hover:text-purple-300 dark:hover:text-red-400 dark:hover:text-slate-800 dark:hover:text-white dark:italic dark:items-center dark:items-end dark:items-start dark:items-stretch dark:justify-around dark:justify-between dark:justify-center dark:justify-end dark:justify-start dark:opacity-0 dark:opacity-100 dark:opacity-25 dark:opacity-50 dark:opacity-75 dark:overflow-auto dark:overflow-hidden dark:overflow-visible dark:overflow-x-auto dark:overflow-y-auto dark:placeholder-gray-500 dark:prose-invert dark:rounded dark:rounded-full dark:rounded-lg dark:rounded-none dark:rounded-sm dark:rounded-xl dark:self-center dark:self-end dark:self-start dark:self-stretch dark:shadow dark:shadow-2xl dark:shadow-lg dark:shadow-none dark:shadow-purple-900/50 dark:shadow-sm dark:shadow-xl dark:text-2xl dark:text-amber-200 dark:text-amber-300 dark:text-amber-400 dark:text-base dark:text-blue-100 dark:text-blue-200 dark:text-blue-300 dark:text-blue-400 dark:text-blue-600 dark:text-center dark:text-gray-100 dark:text-gray-200 dark:text-gray-300 dark:text-gray-400 dark:text-gray-500 dark:text-gray-600 dark:text-gray-700 dark:text-gray-900 dark:text-green-100 dark:text-green-200 dark:text-green-300 dark:text-green-400 dark:text-green-600 dark:text-indigo-200 dark:text-indigo-300 dark:text-indigo-400 dark:text-justify dark:text-left dark:text-lg dark:text-orange-200 dark:text-orange-400 dark:text-orange-500 dark:text-purple-100 dark:text-purple-200 dark:text-purple-300 dark:text-purple-400 dark:text-red-200 dark:text-red-300 dark:text-red-400 dark:text-red-500 dark:text-red-600 dark:text-right dark:text-slate-300 dark:text-slate-400 dark:text-sm dark:text-white dark:text-xl dark:text-xs dark:text-yellow-100 dark:text-yellow-200 dark:text-yellow-300 dark:text-yellow-400 dark:text-yellow-500 dark:to-indigo-900 dark:underline dark:whitespace-normal dark:whitespace-nowrap dark:whitespace-pre-wrap disabled:bg-blue-400 disabled:bg-gray-100 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-indigo-600 disabled:hover:bg-transparent disabled:no-underline disabled:opacity-50 disabled:text-gray-400 flex flex-1 flex-auto flex-col flex-none flex-row flex-wrap focus:border-blue-500 focus:border-indigo-500 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-blue-500 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-red-500 focus:ring-slate-400 font-bold font-medium font-normal hover:-translate-y-0.5 hover:bg-amber-50 hover:bg-amber-700 hover:bg-blue-700 hover:bg-gray-100 hover:bg-gray-200 hover:bg-gray-300 hover:bg-gray-50 hover:bg-gray-800 hover:bg-gray-900 hover:bg-green-700 hover:bg-indigo-50 hover:bg-indigo-700 hover:bg-orange-50 hover:bg-orange-700 hover:bg-purple-50 hover:bg-purple-700 hover:bg-red-100 hover:bg-red-50 hover:bg-red-700 hover:bg-slate-50 hover:bg-slate-700 hover:bg-yellow-700 hover:no-underline hover:text-blue-500 hover:text-blue-600 hover:text-gray-600 hover:text-gray-700 hover:text-gray-900 hover:text-purple-800 hover:text-purple-900 hover:text-red-500 hover:text-white hover:underline italic items-center items-end items-start items-stretch justify-around justify-between justify-center justify-end justify-start lg:grid-cols-2 lg:grid-cols-3 lg:mb-8 lg:p-8 lg:px-8 max-h-[400px] max-h-[500px] max-h-[70vh] max-w-[200px] md: md:col-span-2 md:flex-1 md:flex-row md:gap-2 md:gap-3 md:grid-cols-2 md:grid-cols-3 md:grid-cols-4 md:h-20 md:items-center md:justify-between md:mb-6 md:p-6 md:rounded-2xl md:space-y-0 md:text-xl md:text-xs md:w-20 md:w-40 md:w-64 md:w-72 md:w-80 md:w-auto min-h-[200px] min-h-[60vh] min-w-[200px] min-w-[80px] opacity-0 opacity-100 opacity-25 opacity-50 opacity-75 overflow-auto overflow-hidden overflow-visible overflow-x-auto overflow-y-auto rounded rounded-full rounded-lg rounded-none rounded-sm rounded-xl self-center self-end self-start self-stretch shadow shadow-2xl shadow-lg shadow-none shadow-sm shadow-xl sm:flex-row sm:gap-6 sm:grid-cols-2 sm:inline sm:items-start sm:justify-between sm:mt-6 sm:p-6 sm:px-6 sm:text-2xl sm:text-3xl sm:w-40 text-2xl text-[10px] text-[11px] text-base text-blue-600 text-center text-gray-500 text-gray-700 text-gray-900 text-green-600 text-justify text-left text-lg text-red-600 text-right text-sm text-white text-xl text-xs underline whitespace-normal whitespace-nowrap whitespace-pre-wrap"); +@source inline("bg-blue-600 bg-gray-100 bg-gray-50 bg-gray-800 bg-gray-900 bg-red-600 bg-white block border border-0 border-blue-600 border-dashed border-dotted border-gray-300 border-gray-500 border-gray-700 border-gray-900 border-green-600 border-red-600 border-solid border-white dark:bg-amber-500 dark:bg-amber-900 dark:bg-amber-900/20 dark:bg-amber-900/30 dark:bg-amber-900/40 dark:bg-blue-400 dark:bg-blue-500 dark:bg-blue-600 dark:bg-blue-800 dark:bg-blue-900 dark:bg-blue-900/20 dark:bg-blue-900/30 dark:bg-blue-900/40 dark:bg-blue-900/50 dark:bg-emerald-400 dark:bg-gray-100 dark:bg-gray-50 dark:bg-gray-600 dark:bg-gray-700 dark:bg-gray-700/30 dark:bg-gray-700/50 dark:bg-gray-800 dark:bg-gray-800/50 dark:bg-gray-900 dark:bg-gray-900/50 dark:bg-gray-950 dark:bg-green-900 dark:bg-green-900/20 dark:bg-green-900/30 dark:bg-indigo-500 dark:bg-indigo-900 dark:bg-indigo-900/30 dark:bg-orange-900 dark:bg-purple-800/50 dark:bg-purple-900 dark:bg-purple-900/20 dark:bg-purple-900/30 dark:bg-purple-900/50 dark:bg-red-500 dark:bg-red-600 dark:bg-red-700 dark:bg-red-900 dark:bg-red-900/20 dark:bg-red-900/30 dark:bg-rose-400 dark:bg-slate-300 dark:bg-slate-700 dark:bg-slate-800 dark:bg-white dark:bg-yellow-700 dark:bg-yellow-900 dark:bg-yellow-900/30 dark:bg-yellow-900/40 dark:block dark:border dark:border-0 dark:border-amber-400 dark:border-amber-600 dark:border-amber-700 dark:border-amber-800 dark:border-blue-600 dark:border-blue-700 dark:border-blue-800 dark:border-dashed dark:border-dotted dark:border-gray-300 dark:border-gray-500 dark:border-gray-600 dark:border-gray-700 dark:border-gray-900 dark:border-green-600 dark:border-green-700 dark:border-green-800 dark:border-indigo-400 dark:border-orange-600 dark:border-purple-600 dark:border-purple-700 dark:border-red-500 dark:border-red-600 dark:border-red-700 dark:border-red-800 dark:border-solid dark:border-white dark:border-yellow-600 dark:disabled:bg-blue-800 dark:disabled:bg-gray-600 dark:disabled:hover:bg-indigo-500 dark:disabled:text-gray-600 dark:divide-purple-800 dark:flex dark:flex-1 dark:flex-auto dark:flex-col dark:flex-none dark:flex-row dark:flex-wrap dark:focus:ring-amber-400 dark:focus:ring-blue-400 dark:focus:ring-indigo-400 dark:focus:ring-offset-gray-900 dark:focus:ring-slate-500 dark:font-bold dark:font-medium dark:font-normal dark:from-blue-900 dark:hover:bg-amber-600 dark:hover:bg-blue-600 dark:hover:bg-gray-100 dark:hover:bg-gray-200 dark:hover:bg-gray-500 dark:hover:bg-gray-600 dark:hover:bg-gray-700 dark:hover:bg-gray-700/30 dark:hover:bg-indigo-600 dark:hover:bg-indigo-900/30 dark:hover:bg-orange-900/30 dark:hover:bg-purple-900/30 dark:hover:bg-red-600 dark:hover:bg-red-800 dark:hover:bg-red-900/20 dark:hover:bg-red-900/30 dark:hover:bg-slate-200 dark:hover:bg-slate-600 dark:hover:bg-yellow-800 dark:hover:text-blue-300 dark:hover:text-blue-400 dark:hover:text-gray-200 dark:hover:text-gray-300 dark:hover:text-purple-200 dark:hover:text-purple-300 dark:hover:text-red-400 dark:hover:text-slate-800 dark:hover:text-white dark:italic dark:items-center dark:items-end dark:items-start dark:items-stretch dark:justify-around dark:justify-between dark:justify-center dark:justify-end dark:justify-start dark:opacity-0 dark:opacity-100 dark:opacity-25 dark:opacity-50 dark:opacity-75 dark:overflow-auto dark:overflow-hidden dark:overflow-visible dark:overflow-x-auto dark:overflow-y-auto dark:placeholder-gray-500 dark:prose-invert dark:rounded dark:rounded-full dark:rounded-lg dark:rounded-none dark:rounded-sm dark:rounded-xl dark:self-center dark:self-end dark:self-start dark:self-stretch dark:shadow dark:shadow-2xl dark:shadow-lg dark:shadow-none dark:shadow-purple-900/50 dark:shadow-sm dark:shadow-xl dark:text-2xl dark:text-amber-200 dark:text-amber-300 dark:text-amber-400 dark:text-base dark:text-blue-100 dark:text-blue-200 dark:text-blue-300 dark:text-blue-400 dark:text-blue-600 dark:text-center dark:text-gray-100 dark:text-gray-200 dark:text-gray-300 dark:text-gray-400 dark:text-gray-500 dark:text-gray-600 dark:text-gray-700 dark:text-gray-900 dark:text-green-100 dark:text-green-200 dark:text-green-300 dark:text-green-400 dark:text-green-600 dark:text-indigo-200 dark:text-indigo-300 dark:text-indigo-400 dark:text-justify dark:text-left dark:text-lg dark:text-orange-200 dark:text-orange-400 dark:text-orange-500 dark:text-purple-100 dark:text-purple-200 dark:text-purple-300 dark:text-purple-400 dark:text-red-200 dark:text-red-300 dark:text-red-400 dark:text-red-500 dark:text-red-600 dark:text-right dark:text-slate-300 dark:text-slate-400 dark:text-sm dark:text-white dark:text-xl dark:text-xs dark:text-yellow-100 dark:text-yellow-200 dark:text-yellow-300 dark:text-yellow-400 dark:text-yellow-500 dark:to-indigo-900 dark:underline dark:whitespace-normal dark:whitespace-nowrap dark:whitespace-pre-wrap disabled:bg-blue-400 disabled:bg-gray-100 disabled:bg-gray-400 disabled:cursor-not-allowed disabled:hover:bg-indigo-600 disabled:hover:bg-transparent disabled:no-underline disabled:opacity-50 disabled:text-gray-400 flex flex-1 flex-auto flex-col flex-none flex-row flex-wrap focus:border-blue-500 focus:border-indigo-500 focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-blue-500 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-red-500 focus:ring-slate-400 font-bold font-medium font-normal hover:-translate-y-0.5 hover:bg-amber-50 hover:bg-amber-700 hover:bg-blue-700 hover:bg-gray-100 hover:bg-gray-200 hover:bg-gray-300 hover:bg-gray-50 hover:bg-gray-800 hover:bg-gray-900 hover:bg-green-700 hover:bg-indigo-50 hover:bg-indigo-700 hover:bg-orange-50 hover:bg-orange-700 hover:bg-purple-50 hover:bg-purple-700 hover:bg-red-100 hover:bg-red-50 hover:bg-red-700 hover:bg-slate-50 hover:bg-slate-700 hover:bg-yellow-700 hover:no-underline hover:text-blue-500 hover:text-blue-600 hover:text-gray-600 hover:text-gray-700 hover:text-gray-900 hover:text-purple-800 hover:text-purple-900 hover:text-red-500 hover:text-white hover:underline italic items-center items-end items-start items-stretch justify-around justify-between justify-center justify-end justify-start lg:grid-cols-2 lg:grid-cols-3 lg:mb-8 lg:p-8 lg:px-8 max-h-[400px] max-h-[500px] max-h-[70vh] max-w-[200px] md: md:col-span-2 md:flex-1 md:flex-row md:gap-2 md:gap-3 md:grid-cols-2 md:grid-cols-3 md:grid-cols-4 md:h-20 md:items-center md:justify-between md:mb-6 md:p-6 md:rounded-2xl md:space-y-0 md:text-xl md:text-xs md:w-20 md:w-40 md:w-64 md:w-72 md:w-80 md:w-auto min-h-[200px] min-h-[60vh] min-w-[200px] min-w-[80px] opacity-0 opacity-100 opacity-25 opacity-50 opacity-75 overflow-auto overflow-hidden overflow-visible overflow-x-auto overflow-y-auto rounded rounded-full rounded-lg rounded-none rounded-sm rounded-xl self-center self-end self-start self-stretch shadow shadow-2xl shadow-lg shadow-none shadow-sm shadow-xl sm:flex-row sm:gap-6 sm:grid-cols-2 sm:inline sm:items-start sm:justify-between sm:mt-6 sm:p-6 sm:px-6 sm:text-2xl sm:text-3xl sm:w-40 text-2xl text-[10px] text-[11px] text-base text-blue-600 text-center text-gray-500 text-gray-700 text-gray-900 text-green-600 text-justify text-left text-lg text-red-600 text-right text-sm text-white text-xl text-xs underline whitespace-normal whitespace-nowrap whitespace-pre-wrap"); /* SAFELIST-END */ /** diff --git a/templates/_bundled/sirsoft-admin_basic/template.json b/templates/_bundled/sirsoft-admin_basic/template.json index 1c76082f..70f7689b 100644 --- a/templates/_bundled/sirsoft-admin_basic/template.json +++ b/templates/_bundled/sirsoft-admin_basic/template.json @@ -5,7 +5,7 @@ "ko": "Admin Basic", "en": "Admin Basic" }, - "version": "1.0.4", + "version": "1.0.5", "license": "MIT", "description": { "ko": "그누보드7 기본 관리자 템플릿", @@ -22,7 +22,7 @@ "url": "https://sirsoft.com" }, "release_date": "2026-05-15", - "g7_version": ">=7.0.6", + "g7_version": ">=7.0.7", "dependencies": { "modules": {}, "plugins": {} diff --git a/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/admin/schedule-form-modal-save.spec.ts b/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/admin/schedule-form-modal-save.spec.ts new file mode 100644 index 00000000..4f10bcb4 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/admin/schedule-form-modal-save.spec.ts @@ -0,0 +1,74 @@ +/** + * sirsoft-admin_basic — 스케줄 등록/수정 모달 저장 회귀. + * + * 결함: footer 저장 버튼이 `form="schedule_form_modal"` 로 폼과 연결되는데 Form 에 id 가 + * 없어 클릭이 무반응이었고(예외·콘솔 오류 없음), 커스텀 Select 는 defaultValue 를 + * 렌더하지 않아 작업 유형·실행 주기가 빈 값으로 표시되며 저장 요청에 실리지 않았다. + * Toggle 자동바인딩은 "on" 문자열을 보내 boolean 검증에서 422 가 됐다 — 세 결함이 + * 겹쳐 이 모달로는 스케줄을 한 건도 등록할 수 없었다. 전부 브라우저에서만 드러나는 + * 결함이라 단위 테스트(JSON 구조 단언)와 별개로 실제 저장 왕복을 고정한다. + * + * @scenario schedule-form-modal-save + * @axes leg=create + * @effects schedule_modal_save_creates_row + */ +import { test, expect } from '@playwright/test'; +import { issueToken, authenticatePage } from '../../fixtures/admin-template-auth'; + +test.describe('@sirsoft-admin_basic 스케줄 폼 모달 저장', () => { + // @scenario leg=create + // @effects schedule_modal_save_creates_row + test('등록 모달에서 저장하면 예약이 생성되어 목록에 나타난다 (원복 포함)', async ({ page }) => { + const token = issueToken( + 'core.schedules.read', + 'core.schedules.create', + 'core.schedules.update', + 'core.schedules.delete' + ); + await authenticatePage(page, token); + await page.goto('/admin/schedules'); + await page.waitForLoadState('networkidle', { timeout: 30_000 }); + + const name = `E2E 스케줄 저장 회귀 ${Date.now()}`; + + await page.getByRole('button', { name: /스케줄 등록/ }).click(); + const dialog = page.getByRole('dialog'); + await expect(dialog).toBeVisible({ timeout: 10_000 }); + + // 커스텀 Select 가 value 로 제어되어 기본값이 표시되어야 한다 (빈 표시 회귀). + await expect(dialog.getByRole('button', { name: 'Artisan' })).toBeVisible(); + await expect(dialog.getByRole('button', { name: '매일' })).toBeVisible(); + + await dialog.locator('input[name="name"]').fill(name); + await dialog.locator('input[name="command"]').fill('cache:clear'); + + const createResponse = page.waitForResponse( + (res) => res.url().includes('/api/admin/schedules') && res.request().method() === 'POST', + { timeout: 30_000 } + ); + await dialog.getByRole('button', { name: '저장' }).click(); + + // 저장 버튼이 폼과 연결되지 않으면 POST 자체가 발생하지 않는다 (무반응 회귀). + const response = await createResponse; + expect(response.status(), '스케줄 생성 요청이 거절되었다').toBe(201); + + const created = (await response.json()) as { data: { id: number } }; + + await expect(dialog).toBeHidden({ timeout: 10_000 }); + await expect(page.getByText(name)).toBeVisible({ timeout: 10_000 }); + + // 원복 — 검증용 예약 삭제 (페이지 인증 컨텍스트의 실제 API 경로) + const deleteStatus = await page.evaluate(async (id) => { + const res = await fetch(`/api/admin/schedules/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }, + credentials: 'include', + }); + return res.status; + }, created.data.id); + expect(deleteStatus, '검증용 예약 원복(삭제)이 실패했다').toBe(200); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/public-asset-storage.spec.ts b/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/public-asset-storage.spec.ts new file mode 100644 index 00000000..dc012053 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/public-asset-storage.spec.ts @@ -0,0 +1,231 @@ +/** + * E2E: 공개 자산 스토리지 설정 — 코어 드라이버 탭 카드 (공개#100) + * + * 시나리오 매니페스트: `tests/scenarios/public-asset-cdn.yaml` + * (케이스 마킹은 각 test 의 라인 주석에 있다) + * + * @effects settings_catalog_includes_plugin_registered_disks, + * invalid_disk_rejected_with_422 + * + * 드라이버 탭의 "공개 자산 스토리지" 카드가 카탈로그(none/public/s3)를 그리고, + * 선택-저장-재진입 왕복에서 값이 유지되는지 검증한다. 함께 있는 CKEditor5 축은 + * 코어 PluginSettingsController 의 카탈로그 부착을 제한 권한에서 검증하는 것이라 + * 코어/템플릿 표면에 둔다 — 이커머스 모듈이 소유한 오버라이드 필드는 모듈 spec + * (`modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/`) 이 담당한다. + * + * composite Select 는 네이티브 select 가 아니라 커스텀 드롭다운(root div 에 name 속성, + * Button 토글, portal listbox role=option)이다 — 상호작용은 그 구조를 따른다. + * 저장 후에는 반드시 원값으로 되돌려 사이트 설정을 바꾸지 않는다. + */ +import { test, expect, authenticatePage, issueToken } from '../fixtures/admin-template-auth'; +import type { Page } from '@playwright/test'; + +/** 코어 설정 읽기/쓰기 권한 토큰 (테스트 파일 단위 1회 발급) */ +let settingsToken: string; + +test.beforeAll(() => { + settingsToken = issueToken('core.settings.read', 'core.settings.update'); +}); + +const DRIVERS_TAB = '/admin/settings?tab=drivers'; +const CKEDITOR5_SETTINGS = '/admin/plugins/sirsoft-ckeditor5/settings'; + +const CORE_SELECT_ROOT = '[name="drivers.public_asset_disk"]'; +const PLUGIN_SELECT_ROOT = '[name="public_asset_disk"]'; + +/** + * 드라이버 탭 진입 — 폼 데이터 적재를 엔진 상태로 확인한다. + * + * DOM attach 만 기다리면 바인딩 전 빈 Select 를 카탈로그 누락으로 오인한다. + */ +async function gotoDriversTab(page: Page): Promise<void> { + await page.goto(DRIVERS_TAB); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect + .poll( + async () => + await page.evaluate(() => { + const local = (window as any).G7Core?.state?.getLocal?.(); + return Array.isArray(local?.form?.available_drivers?.public_asset); + }), + { timeout: 20_000 }, + ) + .toBe(true); +} + +/** + * 커스텀 드롭다운을 열고 옵션 라벨 목록을 읽는다 (읽은 뒤 드롭다운은 닫지 않음). + * + * @param page Playwright 페이지 + * @param rootSelector Select 루트(name 속성) 선택자 + * @returns 옵션 라벨 배열 + */ +async function openAndReadOptions(page: Page, rootSelector: string): Promise<string[]> { + await page.locator(`${rootSelector} button`).first().click(); + const listbox = page.locator('[role="listbox"]'); + await expect(listbox).toBeVisible({ timeout: 10_000 }); + + return listbox.locator('[role="option"]').allInnerTexts(); +} + +/** + * 엔진 로컬 상태에서 폼 값을 읽는다. + * + * @param page Playwright 페이지 + * @param path form 하위 도트 경로 (예: 'drivers.public_asset_disk') + * @returns 현재 폼 값 + */ +async function readFormValue(page: Page, path: string): Promise<unknown> { + return page.evaluate((p) => { + const form = (window as any).G7Core?.state?.getLocal?.()?.form; + return p.split('.').reduce((acc: any, key: string) => acc?.[key], form); + }, path); +} + +test.describe('공개 자산 스토리지 설정', () => { + // @scenario consumer=product, disk_setting=none, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=legacy_local_row + // @effects settings_catalog_includes_plugin_registered_disks + test('드라이버 탭 카드가 코어 카탈로그(none/public/s3)를 그린다', async ({ page }) => { + await authenticatePage(page, settingsToken); + await gotoDriversTab(page); + + await expect(page.locator(CORE_SELECT_ROOT)).toBeVisible(); + + const labels = await openAndReadOptions(page, CORE_SELECT_ROOT); + expect(labels.join('|')).toContain('사용 안 함'); + expect(labels.join('|')).toContain('Public'); + expect(labels.join('|')).toContain('Amazon S3'); + + await page.keyboard.press('Escape'); + }); + + // @scenario consumer=product, disk_setting=public, e2e=save_roundtrip, hook=unregistered, override=module_override, row_state=new_remote_row + // @effects invalid_disk_rejected_with_422 + test('선택-저장-재진입 왕복에서 값이 유지되고 원복된다', async ({ page }) => { + test.setTimeout(120_000); + await authenticatePage(page, settingsToken); + await gotoDriversTab(page); + + const original = (await readFormValue(page, 'drivers.public_asset_disk')) as string; + + // Public 디스크 선택 → 저장 → 성공 토스트 + await openAndReadOptions(page, CORE_SELECT_ROOT); + await page.locator('[role="listbox"] [role="option"]', { hasText: 'Public' }).first().click(); + await expect.poll(() => readFormValue(page, 'drivers.public_asset_disk')).toBe('public'); + + await page.locator('#save_button button, button#save_button').first().click(); + await expect(page.getByText('설정이 저장되었습니다')).toBeVisible({ timeout: 20_000 }); + + // 재진입 — 서버 저장값이 폼으로 복원되는지 확인 (refetchOnMount) + await gotoDriversTab(page); + await expect.poll(() => readFormValue(page, 'drivers.public_asset_disk'), { timeout: 20_000 }).toBe('public'); + + // 원복 (사이트 설정 보존) — 원값 라벨('none' → '사용 안 함')로 되돌린다 + const restoreLabel = original === 'public' ? 'Public' : original === 's3' ? 'Amazon S3' : '사용 안 함'; + await openAndReadOptions(page, CORE_SELECT_ROOT); + await page.locator('[role="listbox"] [role="option"]', { hasText: restoreLabel }).first().click(); + await page.locator('#save_button button, button#save_button').first().click(); + await expect(page.getByText('설정이 저장되었습니다')).toBeVisible({ timeout: 20_000 }); + + await gotoDriversTab(page); + await expect + .poll(() => readFormValue(page, 'drivers.public_asset_disk'), { timeout: 20_000 }) + .toBe(original || 'none'); + }); + + // @scenario consumer=editor, disk_setting=none, e2e=ecommerce_settings_field, hook=unregistered, override=module_override, row_state=legacy_local_row + // @effects settings_catalog_includes_plugin_registered_disks + test('CKEditor5 설정 필드가 코어 설정 권한 없이도 카탈로그를 그린다', async ({ page }) => { + // core.settings.read 를 의도적으로 제외 — 카탈로그는 플러그인 설정 응답에 서버 + // 부착되므로, 코어 환경설정 API 권한이 없는 커스텀 역할에서도 선택지가 채워져야 + // 한다 (화면 권한과 카탈로그 권한의 표면 단일화 검증) + const pluginOnlyToken = issueToken('core.plugins.read', 'core.plugins.update'); + await authenticatePage(page, pluginOnlyToken); + await page.goto(CKEDITOR5_SETTINGS); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect + .poll( + async () => + await page.evaluate(() => { + const local = (window as any).G7Core?.state?.getLocal?.(); + return Array.isArray(local?.form?.available_public_asset_disks); + }), + { timeout: 20_000 }, + ) + .toBe(true); + + await expect(page.locator(PLUGIN_SELECT_ROOT)).toBeVisible({ timeout: 20_000 }); + + const labels = await openAndReadOptions(page, PLUGIN_SELECT_ROOT); + expect(labels[0]).toContain('코어 설정 따름'); + expect(labels.join('|')).toContain('사용 안 함'); + expect(labels.join('|')).toContain('Amazon S3'); + + await page.keyboard.press('Escape'); + }); + + // @scenario consumer=product, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row + // @effects cross_origin_asset_request_omits_session_token + test('교차 출처 공개 자산 요청에는 세션 토큰을 보내지 않는다', async ({ page }) => { + await authenticatePage(page, settingsToken); + await gotoDriversTab(page); + + // 외부 CDN origin 을 가로채 요청 헤더를 실측한다 (CORS 헤더는 주지 않는다 — + // 기본 설정 S3 버킷과 동일한 조건) + const crossOriginUrl = 'https://cdn.example.invalid/bucket/products/probe.png'; + const capturedHeaders: Record<string, string>[] = []; + await page.route(crossOriginUrl, async (route) => { + capturedHeaders.push(route.request().headers()); + await route.fulfill({ status: 200, contentType: 'image/png', body: Buffer.from([]) }); + }); + + const sameOriginHeaders: Record<string, string>[] = []; + await page.route('**/api/admin/settings?probe=1', async (route) => { + sameOriginHeaders.push(route.request().headers()); + await route.fulfill({ status: 200, contentType: 'application/json', body: '{"success":true}' }); + }); + + await page.evaluate(async (url) => { + const api = (window as any).G7Core?.api; + await api.get(url, { responseType: 'blob' }).catch(() => undefined); + await api.get('/api/admin/settings?probe=1').catch(() => undefined); + }, crossOriginUrl); + + await expect.poll(() => capturedHeaders.length, { timeout: 20_000 }).toBeGreaterThan(0); + await expect.poll(() => sameOriginHeaders.length, { timeout: 20_000 }).toBeGreaterThan(0); + + // 동일 출처는 종전대로 토큰을 실어야 한다 (대조군 — 판정식이 살아 있음을 보장) + expect(sameOriginHeaders[0]['authorization']).toContain('Bearer'); + // 교차 출처는 토큰이 없어야 한다 (제3자 origin 노출 + CORS preflight 유발 차단) + expect(capturedHeaders[0]['authorization']).toBeUndefined(); + }); + + // @scenario consumer=product, disk_setting=s3_without_url, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row + // @effects s3_credentials_visible_when_public_asset_disk_is_s3 + test('파일 스토리지가 로컬이어도 공개 자산이 S3 면 접속 필드가 노출된다', async ({ page }) => { + await authenticatePage(page, settingsToken); + await gotoDriversTab(page); + + const storageSelect = '[name="drivers.storage_driver"]'; + + // 파일 스토리지를 로컬로 두고(저장하지 않는다 — 폼 상태만 변경) + await openAndReadOptions(page, storageSelect); + await page.locator('[role="listbox"] [role="option"]', { hasText: '로컬' }).first().click(); + + // 이 상태에서 s3 접속 필드는 없어야 한다 (존재 확정 전 부재 단언 회피 — + // 아래에서 같은 선택자로 존재를 확정한다) + const bucketInput = page.locator('input[name="drivers.s3_bucket"]'); + await expect(bucketInput).toHaveCount(0); + + // 공개 자산 디스크만 S3 로 바꾸면 접속 필드가 나타나야 한다 + await openAndReadOptions(page, CORE_SELECT_ROOT); + await page.locator('[role="listbox"] [role="option"]', { hasText: 'Amazon S3' }).first().click(); + + await expect(bucketInput).toBeVisible({ timeout: 20_000 }); + await expect(page.locator('input[name="drivers.s3_url"]')).toBeVisible(); + await expect(page.locator('input[name="drivers.s3_endpoint"]')).toBeVisible(); + + // 저장하지 않고 이탈 — 사이트 설정을 바꾸지 않는다 + await gotoDriversTab(page); + }); +}); diff --git a/templates/_bundled/sirsoft-admin_basic/tests/scenarios/settings-driver-catalog-binding.yaml b/templates/_bundled/sirsoft-admin_basic/tests/scenarios/settings-driver-catalog-binding.yaml new file mode 100644 index 00000000..af12e9a2 --- /dev/null +++ b/templates/_bundled/sirsoft-admin_basic/tests/scenarios/settings-driver-catalog-binding.yaml @@ -0,0 +1,34 @@ +feature: 드라이버 셀렉트의 카탈로그 바인딩과 죽은 저장값 안내 (A5b / E4) + +description: | + 환경설정의 드라이버 셀렉트는 8곳이다 — 파일 스토리지 · 공개 자산 스토리지 · 캐시 · + 검색엔진 · 세션 · 큐 · 로그 · 메일. 이 목록은 서버가 카탈로그(`available_drivers`)로 + 내려주는데, 검색엔진만 레이아웃에 옵션이 리터럴로 박혀 있었다. 플러그인이 엔진을 등록해도 + 화면 셀렉트에는 뜨지 않아 운영자가 고를 방법이 없었다. + + 두 번째 문제는 그 반대 방향이다. 운영자가 고른 드라이버를 제공하던 확장이 사라지면 + 저장값만 남는다. 셀렉트는 카탈로그에서 옵션을 만드므로 그 값에 해당하는 option 이 없고, + 브라우저는 첫 옵션을 선택된 것처럼 그린다 — 화면은 "정상" 으로 보이고, 실제로 동작 중인 + 드라이버는 폴백된 기본값이다. 무엇을 골랐었는지도, 그것이 지금 없다는 사실도 화면에 + 나타나지 않는다. + + 그래서 저장값이 카탈로그에 없을 때만 셀렉트 아래에 안내를 띄우고 **저장된 값 문자열 + 자체를 드러낸다**. 값을 감추면 운영자가 무엇을 복구해야 하는지 알 수 없다. + +axis_notes: + saved_value_state: | + 카탈로그 8곳은 축이 아니라 effects 로 판정한다 — 여덟 셀렉트가 같은 규칙을 공유하는 + 것이 이 기능의 요구사항이라, 한 곳만 검사하면 나머지 일곱은 조용히 빠질 수 있다. + `every_catalog_bound_select_has_dead_value_notice` 가 레이아웃 파일을 순회해 전수를 센다. + +axes: + saved_value_state: [live, dead] + +effects: + - driver_select_options_built_from_catalog # 리터럴 옵션 금지 — 플러그인 등록분이 화면에 뜬다 + - live_saved_value_shows_no_notice # 카탈로그에 있는 값에는 안내를 띄우지 않는다 + - dead_saved_value_notice_reveals_the_value # 안내가 저장된 값 문자열을 드러낸다 + - every_catalog_bound_select_has_dead_value_notice # 카탈로그 바인딩 셀렉트 8곳 전수 + +test_files: + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-catalog-binding.test.tsx diff --git a/templates/_bundled/sirsoft-basic/CHANGELOG.md b/templates/_bundled/sirsoft-basic/CHANGELOG.md index 6d07a9ff..986c462b 100644 --- a/templates/_bundled/sirsoft-basic/CHANGELOG.md +++ b/templates/_bundled/sirsoft-basic/CHANGELOG.md @@ -4,6 +4,24 @@ 형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)를 따르며, [Semantic Versioning](https://semver.org/lang/ko/)을 준수합니다. +## [1.1.1] - 2026-08-19 + +### Added + +- 통합검색에서 검색이 실패했을 때 "검색 결과가 없습니다" 와 구분되는 "검색 중 오류" 안내를 표시합니다. 실패한 항목만 오류로 표시되고 나머지 항목의 결과는 정상 표시됩니다. (#103 @Tuwasduliebst 님께서 제보해주셨습니다.) +- 공개 자산 스토리지(직접 URL/CDN)를 사용하는 사이트에서도 첨부 이미지 미리보기와 다운로드가 그 주소를 그대로 사용해 정상 동작합니다. + +### Fixed + +- 파일 업로더 썸네일의 업로드 진행률 막대와 오류 표시가 다크 모드에서 대비가 낮아 잘 보이지 않던 부분을 보완했습니다. +- 장바구니를 처음 열었을 때 "전체 선택" 옆 개수가 `(/27)` 처럼 선택 개수 없이 표시되던 문제를 수정했습니다. 이제 진입·새로고침·다른 화면에서 돌아온 경우 모두 `(27/27)` 로 바로 표시되며, 체크박스 상태 및 주문 요약의 "선택 상품 N / M개" 와 항상 같은 값을 가리킵니다. (#92, #93 @koojunho 님께서 제보하고 수정해주셨습니다.) +- 장바구니 옵션변경 모달의 재고 안내와 마이페이지 프로필의 입력 오류 메시지가, 표시할 값이 아직 준비되지 않은 순간에도 빈칸으로 남지 않도록 보완했습니다. 값이 준비된 뒤의 표시는 종전과 동일합니다. +- 마이페이지 게시판 알림 수신 설정(댓글·답글 등 4개 항목)의 체크 상태가 저장값과 어긋날 수 있던 가능성을 차단했습니다. 저장된 값이 어떤 형태든 화면의 체크 표시와 저장 결과가 항상 일치합니다. +- 장바구니 "전체 선택" 체크박스가 다크 모드에서 테두리 색을 따르지 않던 문제를 수정했습니다. +- 빌드 산출물이 존재하지 않는 소스맵 파일을 참조해 브라우저 개발자 도구 사용 시 불필요한 404 요청이 발생하던 문제를 수정했습니다. +- 쇼핑(주문서 할인·재주문·배송지 저장·상품 문의)과 마이페이지 화면에서 요청이 실패했을 때 실패 사유 대신 일반 안내 문구만 표시되던 문제를 수정했습니다. 비밀번호 변경 실패 안내가 항상 일반 문구로만 나오던 문제도 함께 바로잡았습니다. 이제 서버가 알려준 사유가 그대로 안내됩니다. +- 로그인한 회원이 통합검색을 해도 비회원과 같은 범위로만 검색되던 문제를 수정했습니다. 검색 화면이 로그인 정보를 함께 보내지 않아, 회원 전용 게시판처럼 로그인해야 볼 수 있는 글이 검색 결과에서 빠졌습니다. 비회원의 검색 범위는 종전과 동일합니다. + ## [1.1.0] - 2026-08-10 ### Added diff --git a/templates/_bundled/sirsoft-basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx new file mode 100644 index 00000000..0c17a31b --- /dev/null +++ b/templates/_bundled/sirsoft-basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx @@ -0,0 +1,102 @@ +/** + * 공개 자산(S3/CDN) 교차 출처 URL 의 인증 요청 차단 회귀 테스트 + * + * 공개 자산 스토리지를 켜면 첨부·이미지의 `download_url` 이 외부 origin 절대 URL 이 된다. + * 그 URL 을 인증 XHR(Blob)로 가져오면 + * ① CORS 헤더를 주지 않는 CDN(기본 설정 S3 버킷 등)에서 이미지가 통째로 실패하고 + * ② 응답을 읽을 수 있는 CDN 이라면 세션 토큰이 제3자 origin 으로 전송된다. + * 두 경로 모두 브라우저 실측으로 확인된 결함이므로, 교차 출처 URL 은 + * 인증 요청 없이 그대로 사용해야 한다. + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { isCrossOriginAssetUrl } from '../../src/components/composite/assetOrigin'; +import { SortableThumbnailItem } from '../../src/components/composite/FileUploader/SortableThumbnailItem'; + +const apiGet = vi.fn(); + +beforeEach(() => { + apiGet.mockReset(); + apiGet.mockResolvedValue(new Blob(['x'], { type: 'image/png' })); + // 전역 G7Core 객체는 setup 이 만든 것을 그대로 쓰고 api 만 주입한다 + // (컴포넌트가 모듈 로드 시점에 같은 객체 참조를 캡처하므로 교체하면 안 된다) + (window as any).G7Core.api = { get: apiGet }; + (window as any).URL.createObjectURL = vi.fn(() => 'blob:mock-object-url'); + (window as any).URL.revokeObjectURL = vi.fn(); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('isCrossOriginAssetUrl', () => { + it('상대 경로(API 스트리밍)는 동일 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('/api/modules/sirsoft-ecommerce/product-image/abc')).toBe(false); + }); + + it('동일 출처 절대 URL 은 동일 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl(`${window.location.origin}/storage/a.png`)).toBe(false); + }); + + it('외부 CDN 절대 URL 은 교차 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('https://bucket.s3.ap-southeast-2.amazonaws.com/a.png')).toBe(true); + }); + + it('프로토콜 상대 URL 도 호스트가 다르면 교차 출처로 판정한다', () => { + expect(isCrossOriginAssetUrl('//cdn.example.com/a.png')).toBe(true); + }); + + it('빈 값은 동일 출처로 간주해 기존 경로를 유지한다', () => { + expect(isCrossOriginAssetUrl(undefined)).toBe(false); + expect(isCrossOriginAssetUrl('')).toBe(false); + }); +}); + +/** + * 썸네일 항목 렌더용 첨부 픽스처를 만듭니다. + * + * @param downloadUrl 첨부의 download_url + * @returns 첨부 객체 + */ +const makeAttachment = (downloadUrl: string) => ({ + id: 1, + hash: 'h1', + original_filename: 'probe.png', + size: 100, + mime_type: 'image/png', + is_image: true, + download_url: downloadUrl, +}); + +// @scenario consumer=product, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=legacy_local_row +// @effects cross_origin_asset_request_omits_session_token +describe('SortableThumbnailItem 의 공개 자산 URL 처리', () => { + it('동일 출처 URL 은 종전대로 인증 요청으로 이미지를 로드한다', async () => { + render( + <SortableThumbnailItem + file={makeAttachment('/api/modules/sirsoft-ecommerce/product-image/abc') as any} + onRemove={vi.fn()} + /> + ); + + await waitFor(() => expect(apiGet).toHaveBeenCalledTimes(1)); + expect(apiGet).toHaveBeenCalledWith( + '/api/modules/sirsoft-ecommerce/product-image/abc', + { responseType: 'blob' } + ); + }); + + it('교차 출처 공개 자산 URL 은 인증 요청 없이 그대로 사용한다', async () => { + const cdnUrl = 'https://bucket.s3.ap-southeast-2.amazonaws.com/images/products/a.png'; + + render(<SortableThumbnailItem file={makeAttachment(cdnUrl) as any} onRemove={vi.fn()} />); + + await waitFor(() => { + expect(screen.getByRole('img')).toHaveAttribute('src', cdnUrl); + }); + expect(apiGet).not.toHaveBeenCalled(); + }); +}); diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/board-secret-post-login-redirect.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/layouts/board-secret-post-login-redirect.test.tsx index c0fdcebf..8427282f 100644 --- a/templates/_bundled/sirsoft-basic/__tests__/layouts/board-secret-post-login-redirect.test.tsx +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/board-secret-post-login-redirect.test.tsx @@ -10,7 +10,7 @@ * 회귀 차단: 버튼 액션이 (a) redirectToLoginWithReturn 핸들러이고 (b) 깨진 * navigate+{{_global.currentPath}} 패턴이 잔존하지 않는지 고정한다. * - * @scenario entry_point:secret_post_login_button + * @scenario entry_point=secret_post_login_button * @effects secret_button_redirect_not_empty,login_returns_to_original_path */ diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/identity-challenge-modal.test.tsx b/templates/_bundled/sirsoft-basic/__tests__/layouts/identity-challenge-modal.test.tsx index 8b2023ea..55ebf4d9 100644 --- a/templates/_bundled/sirsoft-basic/__tests__/layouts/identity-challenge-modal.test.tsx +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/identity-challenge-modal.test.tsx @@ -248,6 +248,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () => expect(cancelChain).toBeDefined(); }); + /** @effects modal_cancel_button_calls_server_cancel_api_first, modal_cancel_apiCall_uses_global_identityChallenge_challenge_id */ it('cancel sequence 의 첫 액션이 서버 cancel API 호출 — audit trail 정합', () => { const seq = findCancelSequence(); expect(seq).toBeDefined(); @@ -262,6 +263,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () => expect(apiCall!.target!).toContain('_global.identityChallenge.challenge_id'); }); + /** @effects modal_cancel_apiCall_skipped_when_challenge_id_absent */ it('cancel apiCall 이 challenge 발급된 경우에만 호출 (if 가드)', () => { const seq = findCancelSequence(); const inner = (seq!.params!.actions ?? []) as Action[]; @@ -269,6 +271,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () => expect((apiCall as any).if).toContain('_global.identityChallenge?.challenge_id'); }); + /** @effects modal_cancel_apiCall_failure_does_not_block_modal_close */ it('cancel apiCall 실패해도 모달 닫기는 진행 (onError suppress)', () => { const seq = findCancelSequence(); const inner = (seq!.params!.actions ?? []) as Action[]; @@ -278,6 +281,7 @@ describe('본인인증 공통 모달 (sirsoft-basic) — engine-v1.46.0', () => expect(onError[0].handler).toBe('suppress'); }); + /** @effects modal_cancel_sequence_order_is_apiCall_resolve_close */ it('cancel sequence 가 [apiCall, resolveIdentityChallenge, closeModal] 순서', () => { const seq = findCancelSequence(); const inner = (seq!.params!.actions ?? []) as Action[]; diff --git a/templates/_bundled/sirsoft-basic/__tests__/layouts/search-url-state-contract.test.ts b/templates/_bundled/sirsoft-basic/__tests__/layouts/search-url-state-contract.test.ts index 22a90701..2eb348d2 100644 --- a/templates/_bundled/sirsoft-basic/__tests__/layouts/search-url-state-contract.test.ts +++ b/templates/_bundled/sirsoft-basic/__tests__/layouts/search-url-state-contract.test.ts @@ -84,6 +84,18 @@ describe('통합검색 — 화면 상태의 SSoT 는 URL 쿼리', () => { expect(source.endpoint, '엔드포인트에 남은 전역 상태').not.toMatch(/_global\.search/); }); + it('검색 데이터소스는 로그인 회원의 토큰을 실어 보낸다 (auth_mode=optional)', () => { + // 검색 라우트는 optional.sanctum 으로 로그인 회원의 열람 범위(회원 전용 게시판 포함)를 + // 해석한다. 데이터소스 auth_mode 기본값은 'none' 이라 미선언이면 토큰이 실리지 않아, + // 로그인 회원도 비회원 범위로 검색되고 서버측 권한 해석이 통째로 무력화된다 + // (7.0.7 사전점검 브라우저 실측 발견 — 서버는 Bearer 를 해석하도록 고쳐졌지만 + // 화면이 보내지 않아 계약의 반대편 끝이 끊겨 있었다). + const layout = JSON.parse(read(indexPath)); + const source = layout.data_sources?.find((ds: any) => ds.id === 'searchResults'); + + expect(source?.auth_mode, 'searchResults auth_mode').toBe('optional'); + }); + it('검색 화면 어디에도 전역 검색 상태가 남아 있지 않다', () => { const leftovers = searchFiles.filter((path) => /_global\??\.search(ActiveTab|SortBy|BoardFilter|Page|Cursor)|"search(ActiveTab|SortBy|BoardFilter|Page|Cursor)"/.test(read(path)) diff --git a/templates/_bundled/sirsoft-basic/dist/css/components.css b/templates/_bundled/sirsoft-basic/dist/css/components.css index 974fca39..72268d63 100644 --- a/templates/_bundled/sirsoft-basic/dist/css/components.css +++ b/templates/_bundled/sirsoft-basic/dist/css/components.css @@ -1 +1 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4,0,1,1);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary-50:#eff6ff;--color-primary-100:#dbeafe;--color-primary-400:#60a5fa;--color-primary-500:#3b82f6;--color-primary-600:#2563eb;--color-primary-700:#1d4ed8;--color-primary-900:#1e3a8a}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:255 255 255;--foreground:17 24 39}.dark{--background:17 24 39;--foreground:255 255 255}html{scroll-behavior:smooth}body{background-color:var(--color-white);color:var(--color-gray-900)}.dark :is(body){background-color:var(--color-gray-900);color:var(--color-white)}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}a{color:var(--color-blue-600)}@media(hover:hover){a:hover{color:var(--color-blue-700)}}.dark :is(a){color:var(--color-blue-400)}@media(hover:hover){.dark :is(a):hover{color:var(--color-blue-300)}}a{text-decoration:none}:focus-visible{outline-style:var(--tw-outline-style);outline-offset:2px;outline-width:2px;outline-color:var(--color-blue-500)}}@layer components{.flex-between{justify-content:space-between;align-items:center;display:flex}.btn{border-radius:var(--radius-lg);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:center;align-items:center;display:inline-flex}.btn:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.btn-primary:hover{background-color:var(--color-blue-700)}}.btn-primary:focus{--tw-ring-color:var(--color-blue-500)}.dark .btn-primary{background-color:var(--color-blue-500)}@media(hover:hover){.dark .btn-primary:hover{background-color:var(--color-blue-600)}}.dark .btn-primary:focus{--tw-ring-color:var(--color-blue-400)}.btn-secondary{background-color:var(--color-gray-100);color:var(--color-gray-900)}@media(hover:hover){.btn-secondary:hover{background-color:var(--color-gray-200)}}.btn-secondary:focus{--tw-ring-color:var(--color-gray-500)}.dark .btn-secondary{background-color:var(--color-gray-700);color:var(--color-white)}@media(hover:hover){.dark .btn-secondary:hover{background-color:var(--color-gray-600)}}.dark .btn-secondary:focus{--tw-ring-color:var(--color-gray-400)}.btn-ghost{color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.btn-ghost:hover{background-color:var(--color-gray-100)}}.btn-ghost:focus{--tw-ring-color:var(--color-gray-500)}.dark .btn-ghost{color:var(--color-gray-300)}@media(hover:hover){.dark .btn-ghost:hover{background-color:var(--color-gray-800)}}.dark .btn-ghost:focus{--tw-ring-color:var(--color-gray-400)}.btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.btn-danger:hover{background-color:var(--color-red-700)}}.btn-danger:focus{--tw-ring-color:var(--color-red-500)}.dark .btn-danger{background-color:var(--color-red-500)}@media(hover:hover){.dark .btn-danger:hover{background-color:var(--color-red-600)}}.dark .btn-danger:focus{--tw-ring-color:var(--color-red-400)}.input{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);width:100%;padding-inline:calc(var(--spacing)*4);padding-block:calc(var(--spacing)*2);background-color:var(--color-white);color:var(--color-gray-900)}.input:focus{border-color:var(--color-blue-500);--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500)}.dark .input{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-white)}.input::placeholder{color:var(--color-gray-400)}.dark .input::placeholder{color:var(--color-gray-500)}.input-error{border-color:var(--color-red-500)}.input-error:focus{border-color:var(--color-red-500);--tw-ring-color:var(--color-red-500)}.card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.badge{padding-inline:calc(var(--spacing)*2.5);padding-block:calc(var(--spacing)*.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-primary{background-color:var(--color-blue-100);color:var(--color-blue-800)}.dark .badge-primary{background-color:var(--color-blue-900);color:var(--color-blue-200)}.badge-success{background-color:var(--color-green-100);color:var(--color-green-800)}.dark .badge-success{background-color:var(--color-green-900);color:var(--color-green-200)}.badge-warning{background-color:var(--color-yellow-100);color:var(--color-yellow-800)}.dark .badge-warning{background-color:var(--color-yellow-900);color:var(--color-yellow-200)}.badge-danger{background-color:var(--color-red-100);color:var(--color-red-800)}.dark .badge-danger{background-color:var(--color-red-900);color:var(--color-red-200)}}@layer utilities{.\@container{container-type:inline-size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-1{inset:calc(var(--spacing)*1)}.inset-2{inset:calc(var(--spacing)*2)}.inset-3{inset:calc(var(--spacing)*3)}.inset-4{inset:calc(var(--spacing)*4)}.inset-5{inset:calc(var(--spacing)*5)}.inset-6{inset:calc(var(--spacing)*6)}.inset-8{inset:calc(var(--spacing)*8)}.inset-auto{inset:auto}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-x-1{inset-inline:calc(var(--spacing)*1)}.inset-x-2{inset-inline:calc(var(--spacing)*2)}.inset-x-3{inset-inline:calc(var(--spacing)*3)}.inset-x-4{inset-inline:calc(var(--spacing)*4)}.inset-x-auto{inset-inline:auto}.inset-y-0{inset-block:calc(var(--spacing)*0)}.inset-y-1{inset-block:calc(var(--spacing)*1)}.inset-y-2{inset-block:calc(var(--spacing)*2)}.inset-y-3{inset-block:calc(var(--spacing)*3)}.inset-y-4{inset-block:calc(var(--spacing)*4)}.inset-y-auto{inset-block:auto}.-top-1{top:calc(var(--spacing)*-1)}.-top-2{top:calc(var(--spacing)*-2)}.-top-3{top:calc(var(--spacing)*-3)}.-top-4{top:calc(var(--spacing)*-4)}.top-0{top:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-1\.5{top:calc(var(--spacing)*1.5)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing)*2)}.top-2\/3{top:66.6667%}.top-3{top:calc(var(--spacing)*3)}.top-3\/4{top:75%}.top-4{top:calc(var(--spacing)*4)}.top-5{top:calc(var(--spacing)*5)}.top-6{top:calc(var(--spacing)*6)}.top-8{top:calc(var(--spacing)*8)}.top-10{top:calc(var(--spacing)*10)}.top-12{top:calc(var(--spacing)*12)}.top-16{top:calc(var(--spacing)*16)}.top-24{top:calc(var(--spacing)*24)}.top-auto{top:auto}.top-full{top:100%}.-right-1{right:calc(var(--spacing)*-1)}.-right-2{right:calc(var(--spacing)*-2)}.-right-3{right:calc(var(--spacing)*-3)}.-right-4{right:calc(var(--spacing)*-4)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-1\/2{right:50%}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.right-5{right:calc(var(--spacing)*5)}.right-6{right:calc(var(--spacing)*6)}.right-8{right:calc(var(--spacing)*8)}.right-10{right:calc(var(--spacing)*10)}.right-12{right:calc(var(--spacing)*12)}.right-16{right:calc(var(--spacing)*16)}.right-auto{right:auto}.right-full{right:100%}.-bottom-1{bottom:calc(var(--spacing)*-1)}.-bottom-2{bottom:calc(var(--spacing)*-2)}.-bottom-3{bottom:calc(var(--spacing)*-3)}.-bottom-4{bottom:calc(var(--spacing)*-4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-1{bottom:calc(var(--spacing)*1)}.bottom-1\/2{bottom:50%}.bottom-2{bottom:calc(var(--spacing)*2)}.bottom-3{bottom:calc(var(--spacing)*3)}.bottom-4{bottom:calc(var(--spacing)*4)}.bottom-5{bottom:calc(var(--spacing)*5)}.bottom-6{bottom:calc(var(--spacing)*6)}.bottom-8{bottom:calc(var(--spacing)*8)}.bottom-10{bottom:calc(var(--spacing)*10)}.bottom-12{bottom:calc(var(--spacing)*12)}.bottom-16{bottom:calc(var(--spacing)*16)}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.-left-1{left:calc(var(--spacing)*-1)}.-left-2{left:calc(var(--spacing)*-2)}.-left-3{left:calc(var(--spacing)*-3)}.-left-4{left:calc(var(--spacing)*-4)}.left-0{left:calc(var(--spacing)*0)}.left-1{left:calc(var(--spacing)*1)}.left-1\.5{left:calc(var(--spacing)*1.5)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.left-3{left:calc(var(--spacing)*3)}.left-4{left:calc(var(--spacing)*4)}.left-5{left:calc(var(--spacing)*5)}.left-6{left:calc(var(--spacing)*6)}.left-8{left:calc(var(--spacing)*8)}.left-10{left:calc(var(--spacing)*10)}.left-12{left:calc(var(--spacing)*12)}.left-16{left:calc(var(--spacing)*16)}.left-auto{left:auto}.left-full{left:100%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[9999\]{z-index:9999}.z-auto{z-index:auto}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-5{grid-column:span 5/span 5}.col-span-6{grid-column:span 6/span 6}.col-span-7{grid-column:span 7/span 7}.col-span-8{grid-column:span 8/span 8}.col-span-9{grid-column:span 9/span 9}.col-span-10{grid-column:span 10/span 10}.col-span-11{grid-column:span 11/span 11}.col-span-12{grid-column:span 12/span 12}.col-span-full{grid-column:1/-1}.col-start-1{grid-column-start:1}.col-start-2{grid-column-start:2}.col-start-3{grid-column-start:3}.col-start-4{grid-column-start:4}.col-start-5{grid-column-start:5}.col-start-6{grid-column-start:6}.col-start-7{grid-column-start:7}.col-start-8{grid-column-start:8}.col-start-9{grid-column-start:9}.col-start-10{grid-column-start:10}.col-start-11{grid-column-start:11}.col-start-12{grid-column-start:12}.col-start-13{grid-column-start:13}.col-end-1{grid-column-end:1}.col-end-2{grid-column-end:2}.col-end-3{grid-column-end:3}.col-end-4{grid-column-end:4}.col-end-5{grid-column-end:5}.col-end-6{grid-column-end:6}.col-end-7{grid-column-end:7}.col-end-8{grid-column-end:8}.col-end-9{grid-column-end:9}.col-end-10{grid-column-end:10}.col-end-11{grid-column-end:11}.col-end-12{grid-column-end:12}.col-end-13{grid-column-end:13}.row-span-1{grid-row:span 1/span 1}.row-span-2{grid-row:span 2/span 2}.row-span-3{grid-row:span 3/span 3}.row-span-4{grid-row:span 4/span 4}.row-span-5{grid-row:span 5/span 5}.row-span-6{grid-row:span 6/span 6}.row-span-full{grid-row:1/-1}.row-start-1{grid-row-start:1}.row-start-2{grid-row-start:2}.row-start-3{grid-row-start:3}.row-start-4{grid-row-start:4}.row-start-5{grid-row-start:5}.row-start-6{grid-row-start:6}.row-start-7{grid-row-start:7}.row-end-1{grid-row-end:1}.row-end-2{grid-row-end:2}.row-end-3{grid-row-end:3}.row-end-4{grid-row-end:4}.row-end-5{grid-row-end:5}.row-end-6{grid-row-end:6}.row-end-7{grid-row-end:7}.float-end{float:inline-end}.float-left{float:left}.float-none{float:none}.float-right{float:right}.float-start{float:inline-start}.clear-both{clear:both}.clear-end{clear:inline-end}.clear-left{clear:left}.clear-none{clear:none}.clear-right{clear:right}.clear-start{clear:inline-start}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-m-1{margin:calc(var(--spacing)*-1)}.-m-2{margin:calc(var(--spacing)*-2)}.-m-3{margin:calc(var(--spacing)*-3)}.-m-4{margin:calc(var(--spacing)*-4)}.-m-5{margin:calc(var(--spacing)*-5)}.-m-6{margin:calc(var(--spacing)*-6)}.-m-8{margin:calc(var(--spacing)*-8)}.m-0{margin:calc(var(--spacing)*0)}.m-1{margin:calc(var(--spacing)*1)}.m-2{margin:calc(var(--spacing)*2)}.m-3{margin:calc(var(--spacing)*3)}.m-4{margin:calc(var(--spacing)*4)}.m-5{margin:calc(var(--spacing)*5)}.m-6{margin:calc(var(--spacing)*6)}.m-8{margin:calc(var(--spacing)*8)}.m-10{margin:calc(var(--spacing)*10)}.m-12{margin:calc(var(--spacing)*12)}.m-16{margin:calc(var(--spacing)*16)}.m-auto{margin:auto}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.-mx-2{margin-inline:calc(var(--spacing)*-2)}.-mx-3{margin-inline:calc(var(--spacing)*-3)}.-mx-4{margin-inline:calc(var(--spacing)*-4)}.-mx-5{margin-inline:calc(var(--spacing)*-5)}.-mx-6{margin-inline:calc(var(--spacing)*-6)}.-mx-8{margin-inline:calc(var(--spacing)*-8)}.mx-0{margin-inline:calc(var(--spacing)*0)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-5{margin-inline:calc(var(--spacing)*5)}.mx-6{margin-inline:calc(var(--spacing)*6)}.mx-8{margin-inline:calc(var(--spacing)*8)}.mx-10{margin-inline:calc(var(--spacing)*10)}.mx-12{margin-inline:calc(var(--spacing)*12)}.mx-16{margin-inline:calc(var(--spacing)*16)}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.-my-2{margin-block:calc(var(--spacing)*-2)}.-my-3{margin-block:calc(var(--spacing)*-3)}.-my-4{margin-block:calc(var(--spacing)*-4)}.-my-5{margin-block:calc(var(--spacing)*-5)}.-my-6{margin-block:calc(var(--spacing)*-6)}.-my-8{margin-block:calc(var(--spacing)*-8)}.my-0{margin-block:calc(var(--spacing)*0)}.my-1{margin-block:calc(var(--spacing)*1)}.my-2{margin-block:calc(var(--spacing)*2)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.my-5{margin-block:calc(var(--spacing)*5)}.my-6{margin-block:calc(var(--spacing)*6)}.my-8{margin-block:calc(var(--spacing)*8)}.my-10{margin-block:calc(var(--spacing)*10)}.my-12{margin-block:calc(var(--spacing)*12)}.my-16{margin-block:calc(var(--spacing)*16)}.my-auto{margin-block:auto}.-mt-1{margin-top:calc(var(--spacing)*-1)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.-mt-3{margin-top:calc(var(--spacing)*-3)}.-mt-4{margin-top:calc(var(--spacing)*-4)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.-mt-6{margin-top:calc(var(--spacing)*-6)}.-mt-8{margin-top:calc(var(--spacing)*-8)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-10{margin-top:calc(var(--spacing)*10)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-16{margin-top:calc(var(--spacing)*16)}.mt-auto{margin-top:auto}.-mr-1{margin-right:calc(var(--spacing)*-1)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.-mr-3{margin-right:calc(var(--spacing)*-3)}.-mr-4{margin-right:calc(var(--spacing)*-4)}.-mr-5{margin-right:calc(var(--spacing)*-5)}.-mr-6{margin-right:calc(var(--spacing)*-6)}.-mr-8{margin-right:calc(var(--spacing)*-8)}.mr-0{margin-right:calc(var(--spacing)*0)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-1\.5{margin-right:calc(var(--spacing)*1.5)}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-3{margin-right:calc(var(--spacing)*3)}.mr-4{margin-right:calc(var(--spacing)*4)}.mr-5{margin-right:calc(var(--spacing)*5)}.mr-6{margin-right:calc(var(--spacing)*6)}.mr-8{margin-right:calc(var(--spacing)*8)}.mr-10{margin-right:calc(var(--spacing)*10)}.mr-12{margin-right:calc(var(--spacing)*12)}.mr-16{margin-right:calc(var(--spacing)*16)}.mr-auto{margin-right:auto}.-mb-1{margin-bottom:calc(var(--spacing)*-1)}.-mb-2{margin-bottom:calc(var(--spacing)*-2)}.-mb-3{margin-bottom:calc(var(--spacing)*-3)}.-mb-4{margin-bottom:calc(var(--spacing)*-4)}.-mb-5{margin-bottom:calc(var(--spacing)*-5)}.-mb-6{margin-bottom:calc(var(--spacing)*-6)}.-mb-8{margin-bottom:calc(var(--spacing)*-8)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-10{margin-bottom:calc(var(--spacing)*10)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-16{margin-bottom:calc(var(--spacing)*16)}.mb-auto{margin-bottom:auto}.-ml-1{margin-left:calc(var(--spacing)*-1)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.-ml-3{margin-left:calc(var(--spacing)*-3)}.-ml-4{margin-left:calc(var(--spacing)*-4)}.-ml-5{margin-left:calc(var(--spacing)*-5)}.-ml-6{margin-left:calc(var(--spacing)*-6)}.-ml-8{margin-left:calc(var(--spacing)*-8)}.ml-0{margin-left:calc(var(--spacing)*0)}.ml-0\.5{margin-left:calc(var(--spacing)*.5)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-1\.5{margin-left:calc(var(--spacing)*1.5)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-5{margin-left:calc(var(--spacing)*5)}.ml-6{margin-left:calc(var(--spacing)*6)}.ml-8{margin-left:calc(var(--spacing)*8)}.ml-10{margin-left:calc(var(--spacing)*10)}.ml-12{margin-left:calc(var(--spacing)*12)}.ml-14{margin-left:calc(var(--spacing)*14)}.ml-16{margin-left:calc(var(--spacing)*16)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-0{height:calc(var(--spacing)*0)}.h-1{height:calc(var(--spacing)*1)}.h-1\/2{height:50%}.h-1\/3{height:33.3333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.6667%}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-2\/3{height:66.6667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing)*4)}.h-4\/5{height:80%}.h-5{height:calc(var(--spacing)*5)}.h-5\/6{height:83.3333%}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-20{height:calc(var(--spacing)*20)}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-40{height:calc(var(--spacing)*40)}.h-48{height:calc(var(--spacing)*48)}.h-56{height:calc(var(--spacing)*56)}.h-64{height:calc(var(--spacing)*64)}.h-72{height:calc(var(--spacing)*72)}.h-80{height:calc(var(--spacing)*80)}.h-96{height:calc(var(--spacing)*96)}.h-\[42px\]{height:42px}.h-auto{height:auto}.h-fit{height:fit-content}.h-full{height:100%}.h-max{height:max-content}.h-min{height:min-content}.h-px{height:1px}.h-screen{height:100vh}.max-h-0{max-height:calc(var(--spacing)*0)}.max-h-60{max-height:calc(var(--spacing)*60)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-80{max-height:calc(var(--spacing)*80)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[55vh\]{max-height:55vh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[75vh\]{max-height:75vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[calc\(90vh-140px\)\]{max-height:calc(90vh - 140px)}.max-h-\[calc\(100vh-200px\)\]{max-height:calc(100vh - 200px)}.max-h-fit{max-height:fit-content}.max-h-full{max-height:100%}.max-h-max{max-height:max-content}.max-h-min{max-height:min-content}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-20{min-height:calc(var(--spacing)*20)}.min-h-\[40px\]{min-height:40px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[80px\]{min-height:80px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-fit{min-height:fit-content}.min-h-full{min-height:100%}.min-h-max{min-height:max-content}.min-h-min{min-height:min-content}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing)*0)}.w-1{width:calc(var(--spacing)*1)}.w-1\/2{width:50%}.w-1\/3{width:33.3333%}.w-1\/4{width:25%}.w-1\/5{width:20%}.w-1\/6{width:16.6667%}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-2\/3{width:66.6667%}.w-2\/4{width:50%}.w-2\/5{width:40%}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4{width:calc(var(--spacing)*4)}.w-4\/5{width:80%}.w-5{width:calc(var(--spacing)*5)}.w-5\/6{width:83.3333%}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-14{width:calc(var(--spacing)*14)}.w-16{width:calc(var(--spacing)*16)}.w-20{width:calc(var(--spacing)*20)}.w-24{width:calc(var(--spacing)*24)}.w-28{width:calc(var(--spacing)*28)}.w-32{width:calc(var(--spacing)*32)}.w-40{width:calc(var(--spacing)*40)}.w-44{width:calc(var(--spacing)*44)}.w-48{width:calc(var(--spacing)*48)}.w-52{width:calc(var(--spacing)*52)}.w-56{width:calc(var(--spacing)*56)}.w-60{width:calc(var(--spacing)*60)}.w-64{width:calc(var(--spacing)*64)}.w-72{width:calc(var(--spacing)*72)}.w-80{width:calc(var(--spacing)*80)}.w-96{width:calc(var(--spacing)*96)}.w-\[220px\]{width:220px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-min{width:min-content}.w-px{width:1px}.w-screen{width:100vw}.max-w-0{max-width:calc(var(--spacing)*0)}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[60\%\]{max-width:60%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[calc\(100vw-1rem\)\]{max-width:calc(100vw - 1rem)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-fit{max-width:fit-content}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-max{max-width:max-content}.max-w-md{max-width:var(--container-md)}.max-w-min{max-width:min-content}.max-w-none{max-width:none}.max-w-prose{max-width:65ch}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-screen-lg{max-width:var(--breakpoint-lg)}.max-w-screen-md{max-width:var(--breakpoint-md)}.max-w-screen-sm{max-width:var(--breakpoint-sm)}.max-w-screen-xl{max-width:var(--breakpoint-xl)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-\[100px\]{min-width:100px}.min-w-\[120px\]{min-width:120px}.min-w-\[300px\]{min-width:300px}.min-w-fit{min-width:fit-content}.min-w-full{min-width:100%}.min-w-max{min-width:max-content}.min-w-min{min-width:min-content}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-0{flex-basis:calc(var(--spacing)*0)}.basis-1{flex-basis:calc(var(--spacing)*1)}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.3333%}.basis-1\/4{flex-basis:25%}.basis-2{flex-basis:calc(var(--spacing)*2)}.basis-2\/3{flex-basis:66.6667%}.basis-3\/4{flex-basis:75%}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.caption-bottom{caption-side:bottom}.caption-top{caption-side:top}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:calc(var(--spacing)*0);--tw-border-spacing-y:calc(var(--spacing)*0);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-1{--tw-border-spacing-x:calc(var(--spacing)*1);--tw-border-spacing-y:calc(var(--spacing)*1);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-2{--tw-border-spacing-x:calc(var(--spacing)*2);--tw-border-spacing-y:calc(var(--spacing)*2);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-3{--tw-border-spacing-x:calc(var(--spacing)*3);--tw-border-spacing-y:calc(var(--spacing)*3);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-4{--tw-border-spacing-x:calc(var(--spacing)*4);--tw-border-spacing-y:calc(var(--spacing)*4);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-5{--tw-border-spacing-x:calc(var(--spacing)*5);--tw-border-spacing-y:calc(var(--spacing)*5);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-6{--tw-border-spacing-x:calc(var(--spacing)*6);--tw-border-spacing-y:calc(var(--spacing)*6);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-8{--tw-border-spacing-x:calc(var(--spacing)*8);--tw-border-spacing-y:calc(var(--spacing)*8);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.origin-bottom{transform-origin:bottom}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.origin-center{transform-origin:50%}.origin-left{transform-origin:0}.origin-right{transform-origin:100%}.origin-top{transform-origin:top}.origin-top-left{transform-origin:0 0}.origin-top-right{transform-origin:100% 0}.-translate-x-1{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-2{--tw-translate-x:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-3{--tw-translate-x:calc(var(--spacing)*-3);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-4{--tw-translate-x:calc(var(--spacing)*-4);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x: 50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/3{--tw-translate-x:calc(1/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/4{--tw-translate-x: 25% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-2{--tw-translate-x:calc(var(--spacing)*2);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-2\/3{--tw-translate-x:calc(2/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-3{--tw-translate-x:calc(var(--spacing)*3);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-3\/4{--tw-translate-x: 75% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-6{--tw-translate-x:calc(var(--spacing)*6);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-8{--tw-translate-x:calc(var(--spacing)*8);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-10{--tw-translate-x:calc(var(--spacing)*10);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-12{--tw-translate-x:calc(var(--spacing)*12);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-2{--tw-translate-y:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-3{--tw-translate-y:calc(var(--spacing)*-3);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-4{--tw-translate-y:calc(var(--spacing)*-4);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-full{--tw-translate-y:-100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y: 50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/3{--tw-translate-y:calc(1/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/4{--tw-translate-y: 25% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing)*2);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-2\/3{--tw-translate-y:calc(2/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing)*3);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-3\/4{--tw-translate-y: 75% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-4{--tw-translate-y:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-5{--tw-translate-y:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-6{--tw-translate-y:calc(var(--spacing)*6);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing)*8);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-10{--tw-translate-y:calc(var(--spacing)*10);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-12{--tw-translate-y:calc(var(--spacing)*12);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-full{--tw-translate-y:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-50{--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-150{--tw-scale-x:150%;--tw-scale-y:150%;--tw-scale-z:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-50{--tw-scale-x:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-75{--tw-scale-x:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-90{--tw-scale-x:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-95{--tw-scale-x:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-100{--tw-scale-x:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-105{--tw-scale-x:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-110{--tw-scale-x:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-125{--tw-scale-x:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-150{--tw-scale-x:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-0{--tw-scale-y:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-50{--tw-scale-y:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-75{--tw-scale-y:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-90{--tw-scale-y:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-95{--tw-scale-y:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-100{--tw-scale-y:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-105{--tw-scale-y:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-110{--tw-scale-y:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-125{--tw-scale-y:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-150{--tw-scale-y:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.-rotate-1{rotate:-1deg}.-rotate-2{rotate:-2deg}.-rotate-3{rotate:-3deg}.-rotate-6{rotate:-6deg}.-rotate-12{rotate:-12deg}.-rotate-45{rotate:-45deg}.-rotate-90{rotate:-90deg}.-rotate-180{rotate:-180deg}.rotate-0{rotate:none}.rotate-1{rotate:1deg}.rotate-2{rotate:2deg}.rotate-3{rotate:3deg}.rotate-6{rotate:6deg}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.-skew-x-1{--tw-skew-x:skewX( -1deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-2{--tw-skew-x:skewX( -2deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-3{--tw-skew-x:skewX( -3deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-6{--tw-skew-x:skewX( -6deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-12{--tw-skew-x:skewX( -12deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-0{--tw-skew-x:skewX(0deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-1{--tw-skew-x:skewX(1deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-2{--tw-skew-x:skewX(2deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-3{--tw-skew-x:skewX(3deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-6{--tw-skew-x:skewX(6deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-12{--tw-skew-x:skewX(12deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-1{--tw-skew-y:skewY( -1deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-2{--tw-skew-y:skewY( -2deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-3{--tw-skew-y:skewY( -3deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-6{--tw-skew-y:skewY( -6deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-12{--tw-skew-y:skewY( -12deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-0{--tw-skew-y:skewY(0deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-1{--tw-skew-y:skewY(1deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-2{--tw-skew-y:skewY(2deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-3{--tw-skew-y:skewY(3deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-6{--tw-skew-y:skewY(6deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-12{--tw-skew-y:skewY(12deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform,.transform-cpu{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0)var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform-none{transform:none}.animate-\[loading_1\.5s_ease-in-out_infinite\]{animation:1.5s ease-in-out infinite loading}.animate-bounce{animation:var(--animate-bounce)}.animate-none{animation:none}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-alias{cursor:alias}.cursor-all-scroll{cursor:all-scroll}.cursor-auto{cursor:auto}.cursor-cell{cursor:cell}.cursor-col-resize{cursor:col-resize}.cursor-context-menu{cursor:context-menu}.cursor-copy{cursor:copy}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-e-resize{cursor:e-resize}.cursor-ew-resize{cursor:ew-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-help{cursor:help}.cursor-move{cursor:move}.cursor-n-resize{cursor:n-resize}.cursor-ne-resize{cursor:ne-resize}.cursor-nesw-resize{cursor:nesw-resize}.cursor-no-drop{cursor:no-drop}.cursor-none{cursor:none}.cursor-not-allowed{cursor:not-allowed}.cursor-ns-resize{cursor:ns-resize}.cursor-nw-resize{cursor:nw-resize}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.cursor-progress{cursor:progress}.cursor-row-resize{cursor:row-resize}.cursor-s-resize{cursor:s-resize}.cursor-se-resize{cursor:se-resize}.cursor-sw-resize{cursor:sw-resize}.cursor-text{cursor:text}.cursor-vertical-text{cursor:vertical-text}.cursor-w-resize{cursor:w-resize}.cursor-wait{cursor:wait}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.touch-pan-left{--tw-pan-x:pan-left;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-right{--tw-pan-x:pan-right;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-x{--tw-pan-x:pan-x;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-down{--tw-pan-y:pan-down;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-up{--tw-pan-y:pan-up;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-y{--tw-pan-y:pan-y;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-auto{touch-action:auto}.touch-manipulation{touch-action:manipulation}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.resize-x{resize:horizontal}.resize-y{resize:vertical}.snap-both{scroll-snap-type:both var(--tw-scroll-snap-strictness)}.snap-none{scroll-snap-type:none}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-align-none{scroll-snap-align:none}.snap-center{scroll-snap-align:center}.snap-end{scroll-snap-align:end}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.snap-normal{scroll-snap-stop:normal}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.appearance-auto{appearance:auto}.appearance-none{appearance:none}.columns-1{columns:1}.columns-2{columns:2}.columns-2xl{columns:var(--container-2xl)}.columns-2xs{columns:var(--container-2xs)}.columns-3{columns:3}.columns-3xl{columns:var(--container-3xl)}.columns-3xs{columns:var(--container-3xs)}.columns-4{columns:4}.columns-4xl{columns:var(--container-4xl)}.columns-5{columns:5}.columns-5xl{columns:var(--container-5xl)}.columns-6{columns:6}.columns-6xl{columns:var(--container-6xl)}.columns-7{columns:7}.columns-7xl{columns:var(--container-7xl)}.columns-8{columns:8}.columns-9{columns:9}.columns-10{columns:10}.columns-11{columns:11}.columns-12{columns:12}.columns-auto{columns:auto}.columns-lg{columns:var(--container-lg)}.columns-md{columns:var(--container-md)}.columns-sm{columns:var(--container-sm)}.columns-xl{columns:var(--container-xl)}.columns-xs{columns:var(--container-xs)}.break-before-all{break-before:all}.break-before-auto{break-before:auto}.break-before-avoid{break-before:avoid}.break-before-avoid-page{break-before:avoid-page}.break-before-column{break-before:column}.break-before-left{break-before:left}.break-before-page{break-before:page}.break-before-right{break-before:right}.break-inside-auto{break-inside:auto}.break-inside-avoid{break-inside:avoid}.break-inside-avoid-column{break-inside:avoid-column}.break-inside-avoid-page{break-inside:avoid-page}.break-after-all{break-after:all}.break-after-auto{break-after:auto}.break-after-avoid{break-after:avoid}.break-after-avoid-page{break-after:avoid-page}.break-after-column{break-after:column}.break-after-left{break-after:left}.break-after-page{break-after:page}.break-after-right{break-after:right}.auto-cols-\[calc\(25\%_-_12px\)\]{grid-auto-columns:calc(25% - 12px)}.auto-cols-\[calc\(33\.333\%_-_11px\)\]{grid-auto-columns:calc(33.333% - 11px)}.auto-cols-\[calc\(50\%_-_8px\)\]{grid-auto-columns:calc(50% - 8px)}.auto-cols-auto{grid-auto-columns:auto}.auto-cols-fr{grid-auto-columns:minmax(0,1fr)}.auto-cols-max{grid-auto-columns:max-content}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-col{grid-auto-flow:column}.grid-flow-col-dense{grid-auto-flow:column dense}.grid-flow-dense{grid-auto-flow:dense}.grid-flow-row{grid-auto-flow:row}.grid-flow-row-dense{grid-auto-flow:dense}.auto-rows-auto{grid-auto-rows:auto}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.auto-rows-max{grid-auto-rows:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-\[\.\.\.\]{grid-template-columns:...}.grid-cols-\[60px_1fr\]{grid-template-columns:60px 1fr}.grid-cols-\[60px_1fr_160px\]{grid-template-columns:60px 1fr 160px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px\]{grid-template-columns:60px minmax(300px,1fr) 160px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px_100px\]{grid-template-columns:60px minmax(300px,1fr) 160px 100px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px_100px_70px\]{grid-template-columns:60px minmax(300px,1fr) 160px 100px 70px}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-around{place-content:space-around}.place-content-baseline{place-content:baseline start}.place-content-between{place-content:space-between}.place-content-center{place-content:center}.place-content-end{place-content:end}.place-content-evenly{place-content:space-evenly}.place-content-start{place-content:start}.place-content-stretch{place-content:stretch}.place-items-baseline{place-items:baseline}.place-items-center{place-items:center}.place-items-end{place-items:end}.place-items-start{place-items:start}.place-items-stretch{place-items:stretch stretch}.content-around{align-content:space-around}.content-between{align-content:space-between}.content-center{align-content:center}.content-end{align-content:flex-end}.content-evenly{align-content:space-evenly}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-evenly{justify-content:space-evenly}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing)*0)}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-10{gap:calc(var(--spacing)*10)}.gap-12{gap:calc(var(--spacing)*12)}.gap-16{gap:calc(var(--spacing)*16)}:where(.-space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*-1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*-1)*calc(1 - var(--tw-space-y-reverse)))}:where(.-space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*-2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*-2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-0{column-gap:calc(var(--spacing)*0)}.gap-x-1{column-gap:calc(var(--spacing)*1)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-x-3{column-gap:calc(var(--spacing)*3)}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-x-5{column-gap:calc(var(--spacing)*5)}.gap-x-6{column-gap:calc(var(--spacing)*6)}.gap-x-8{column-gap:calc(var(--spacing)*8)}.gap-x-10{column-gap:calc(var(--spacing)*10)}.gap-x-12{column-gap:calc(var(--spacing)*12)}:where(.-space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*-1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*-1)*calc(1 - var(--tw-space-x-reverse)))}:where(.-space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*-2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*-2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*5)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.gap-y-0{row-gap:calc(var(--spacing)*0)}.gap-y-1{row-gap:calc(var(--spacing)*1)}.gap-y-2{row-gap:calc(var(--spacing)*2)}.gap-y-3{row-gap:calc(var(--spacing)*3)}.gap-y-4{row-gap:calc(var(--spacing)*4)}.gap-y-5{row-gap:calc(var(--spacing)*5)}.gap-y-6{row-gap:calc(var(--spacing)*6)}.gap-y-8{row-gap:calc(var(--spacing)*8)}.gap-y-10{row-gap:calc(var(--spacing)*10)}.gap-y-12{row-gap:calc(var(--spacing)*12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-0>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(0px*var(--tw-divide-x-reverse));border-inline-end-width:calc(0px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-2>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(2px*var(--tw-divide-x-reverse));border-inline-end-width:calc(2px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-4>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(4px*var(--tw-divide-x-reverse));border-inline-end-width:calc(4px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-8>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(8px*var(--tw-divide-x-reverse));border-inline-end-width:calc(8px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-0>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(0px*var(--tw-divide-y-reverse));border-bottom-width:calc(0px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-2>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(2px*var(--tw-divide-y-reverse));border-bottom-width:calc(2px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-4>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(4px*var(--tw-divide-y-reverse));border-bottom-width:calc(4px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-8>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(8px*var(--tw-divide-y-reverse));border-bottom-width:calc(8px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}:where(.divide-dashed>:not(:last-child)){--tw-border-style:dashed;border-style:dashed}:where(.divide-dotted>:not(:last-child)){--tw-border-style:dotted;border-style:dotted}:where(.divide-double>:not(:last-child)){--tw-border-style:double;border-style:double}:where(.divide-none>:not(:last-child)){--tw-border-style:none;border-style:none}:where(.divide-solid>:not(:last-child)){--tw-border-style:solid;border-style:solid}:where(.divide-black>:not(:last-child)){border-color:var(--color-black)}:where(.divide-current>:not(:last-child)){border-color:currentColor}:where(.divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.divide-inherit>:not(:last-child)){border-color:inherit}:where(.divide-slate-50>:not(:last-child)){border-color:var(--color-slate-50)}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}:where(.divide-slate-300>:not(:last-child)){border-color:var(--color-slate-300)}:where(.divide-slate-400>:not(:last-child)){border-color:var(--color-slate-400)}:where(.divide-slate-500>:not(:last-child)){border-color:var(--color-slate-500)}:where(.divide-slate-600>:not(:last-child)){border-color:var(--color-slate-600)}:where(.divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}:where(.divide-slate-800>:not(:last-child)){border-color:var(--color-slate-800)}:where(.divide-slate-900>:not(:last-child)){border-color:var(--color-slate-900)}:where(.divide-transparent>:not(:last-child)){border-color:#0000}:where(.divide-white>:not(:last-child)){border-color:var(--color-white)}.place-self-auto{place-self:auto}.place-self-center{place-self:center}.place-self-end{place-self:end}.place-self-start{place-self:start}.place-self-stretch{place-self:stretch stretch}.self-auto{align-self:auto}.self-baseline{align-self:baseline}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-clip{overflow-x:clip}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-visible{overflow-x:visible}.overflow-y-auto{overflow-y:auto}.overflow-y-clip{overflow-y:clip}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-visible{overflow-y:visible}.overscroll-auto{overscroll-behavior:auto}.overscroll-contain{overscroll-behavior:contain}.overscroll-none{overscroll-behavior:none}.overscroll-x-auto{overscroll-behavior-x:auto}.overscroll-x-contain{overscroll-behavior-x:contain}.overscroll-x-none{overscroll-behavior-x:none}.overscroll-y-auto{overscroll-behavior-y:auto}.overscroll-y-contain{overscroll-behavior-y:contain}.overscroll-y-none{overscroll-behavior-y:none}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t-sm{border-top-left-radius:var(--radius-sm);border-top-right-radius:var(--radius-sm)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-2xl{border-top-left-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-l-3xl{border-top-left-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-l-full{border-top-left-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-sm{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-l-xl{border-top-left-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-2xl{border-top-left-radius:var(--radius-2xl)}.rounded-tl-3xl{border-top-left-radius:var(--radius-3xl)}.rounded-tl-full{border-top-left-radius:3.40282e38px}.rounded-tl-lg{border-top-left-radius:var(--radius-lg)}.rounded-tl-md{border-top-left-radius:var(--radius-md)}.rounded-tl-none{border-top-left-radius:0}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-tl-xl{border-top-left-radius:var(--radius-xl)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-2xl{border-top-right-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl)}.rounded-r-3xl{border-top-right-radius:var(--radius-3xl);border-bottom-right-radius:var(--radius-3xl)}.rounded-r-full{border-top-right-radius:3.40282e38px;border-bottom-right-radius:3.40282e38px}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-sm{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}.rounded-r-xl{border-top-right-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl)}.rounded-tr{border-top-right-radius:.25rem}.rounded-tr-2xl{border-top-right-radius:var(--radius-2xl)}.rounded-tr-3xl{border-top-right-radius:var(--radius-3xl)}.rounded-tr-full{border-top-right-radius:3.40282e38px}.rounded-tr-lg{border-top-right-radius:var(--radius-lg)}.rounded-tr-md{border-top-right-radius:var(--radius-md)}.rounded-tr-none{border-top-right-radius:0}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-b-2xl{border-bottom-right-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-b-3xl{border-bottom-right-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-b-full{border-bottom-right-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-b-md{border-bottom-right-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}.rounded-b-sm{border-bottom-right-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-2xl{border-bottom-right-radius:var(--radius-2xl)}.rounded-br-3xl{border-bottom-right-radius:var(--radius-3xl)}.rounded-br-full{border-bottom-right-radius:3.40282e38px}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-br-none{border-bottom-right-radius:0}.rounded-br-sm{border-bottom-right-radius:var(--radius-sm)}.rounded-br-xl{border-bottom-right-radius:var(--radius-xl)}.rounded-bl{border-bottom-left-radius:.25rem}.rounded-bl-2xl{border-bottom-left-radius:var(--radius-2xl)}.rounded-bl-3xl{border-bottom-left-radius:var(--radius-3xl)}.rounded-bl-full{border-bottom-left-radius:3.40282e38px}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.rounded-bl-none{border-bottom-left-radius:0}.rounded-bl-sm{border-bottom-left-radius:var(--radius-sm)}.rounded-bl-xl{border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-8{border-style:var(--tw-border-style);border-width:8px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.border-x-2{border-inline-style:var(--tw-border-style);border-inline-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-y-0{border-block-style:var(--tw-border-style);border-block-width:0}.border-y-2{border-block-style:var(--tw-border-style);border-block-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-2{border-right-style:var(--tw-border-style);border-right-width:2px}.border-r-4{border-right-style:var(--tw-border-style);border-right-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-4{border-bottom-style:var(--tw-border-style);border-bottom-width:4px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-hidden{--tw-border-style:hidden;border-style:hidden}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-amber-50{border-color:var(--color-amber-50)}.border-amber-100{border-color:var(--color-amber-100)}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-amber-400{border-color:var(--color-amber-400)}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-600{border-color:var(--color-amber-600)}.border-amber-700{border-color:var(--color-amber-700)}.border-amber-800{border-color:var(--color-amber-800)}.border-amber-900{border-color:var(--color-amber-900)}.border-amber-950{border-color:var(--color-amber-950)}.border-black{border-color:var(--color-black)}.border-blue-50{border-color:var(--color-blue-50)}.border-blue-100{border-color:var(--color-blue-100)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-blue-800{border-color:var(--color-blue-800)}.border-blue-900{border-color:var(--color-blue-900)}.border-blue-950{border-color:var(--color-blue-950)}.border-current{border-color:currentColor}.border-cyan-50{border-color:var(--color-cyan-50)}.border-cyan-100{border-color:var(--color-cyan-100)}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-300{border-color:var(--color-cyan-300)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-cyan-500{border-color:var(--color-cyan-500)}.border-cyan-600{border-color:var(--color-cyan-600)}.border-cyan-700{border-color:var(--color-cyan-700)}.border-cyan-800{border-color:var(--color-cyan-800)}.border-cyan-900{border-color:var(--color-cyan-900)}.border-cyan-950{border-color:var(--color-cyan-950)}.border-emerald-50{border-color:var(--color-emerald-50)}.border-emerald-100{border-color:var(--color-emerald-100)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-emerald-400{border-color:var(--color-emerald-400)}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-600{border-color:var(--color-emerald-600)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-emerald-800{border-color:var(--color-emerald-800)}.border-emerald-900{border-color:var(--color-emerald-900)}.border-emerald-950{border-color:var(--color-emerald-950)}.border-fuchsia-50{border-color:var(--color-fuchsia-50)}.border-fuchsia-100{border-color:var(--color-fuchsia-100)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-fuchsia-300{border-color:var(--color-fuchsia-300)}.border-fuchsia-400{border-color:var(--color-fuchsia-400)}.border-fuchsia-500{border-color:var(--color-fuchsia-500)}.border-fuchsia-600{border-color:var(--color-fuchsia-600)}.border-fuchsia-700{border-color:var(--color-fuchsia-700)}.border-fuchsia-800{border-color:var(--color-fuchsia-800)}.border-fuchsia-900{border-color:var(--color-fuchsia-900)}.border-fuchsia-950{border-color:var(--color-fuchsia-950)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-500{border-color:var(--color-gray-500)}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-900{border-color:var(--color-gray-900)}.border-gray-950{border-color:var(--color-gray-950)}.border-green-50{border-color:var(--color-green-50)}.border-green-100{border-color:var(--color-green-100)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-green-400{border-color:var(--color-green-400)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-green-700{border-color:var(--color-green-700)}.border-green-800{border-color:var(--color-green-800)}.border-green-900{border-color:var(--color-green-900)}.border-green-950{border-color:var(--color-green-950)}.border-indigo-50{border-color:var(--color-indigo-50)}.border-indigo-100{border-color:var(--color-indigo-100)}.border-indigo-200{border-color:var(--color-indigo-200)}.border-indigo-300{border-color:var(--color-indigo-300)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-indigo-700{border-color:var(--color-indigo-700)}.border-indigo-800{border-color:var(--color-indigo-800)}.border-indigo-900{border-color:var(--color-indigo-900)}.border-indigo-950{border-color:var(--color-indigo-950)}.border-inherit{border-color:inherit}.border-lime-50{border-color:var(--color-lime-50)}.border-lime-100{border-color:var(--color-lime-100)}.border-lime-200{border-color:var(--color-lime-200)}.border-lime-300{border-color:var(--color-lime-300)}.border-lime-400{border-color:var(--color-lime-400)}.border-lime-500{border-color:var(--color-lime-500)}.border-lime-600{border-color:var(--color-lime-600)}.border-lime-700{border-color:var(--color-lime-700)}.border-lime-800{border-color:var(--color-lime-800)}.border-lime-900{border-color:var(--color-lime-900)}.border-lime-950{border-color:var(--color-lime-950)}.border-neutral-50{border-color:var(--color-neutral-50)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-400{border-color:var(--color-neutral-400)}.border-neutral-500{border-color:var(--color-neutral-500)}.border-neutral-600{border-color:var(--color-neutral-600)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-neutral-950{border-color:var(--color-neutral-950)}.border-orange-50{border-color:var(--color-orange-50)}.border-orange-100{border-color:var(--color-orange-100)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-300{border-color:var(--color-orange-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-orange-500{border-color:var(--color-orange-500)}.border-orange-600{border-color:var(--color-orange-600)}.border-orange-700{border-color:var(--color-orange-700)}.border-orange-800{border-color:var(--color-orange-800)}.border-orange-900{border-color:var(--color-orange-900)}.border-orange-950{border-color:var(--color-orange-950)}.border-pink-50{border-color:var(--color-pink-50)}.border-pink-100{border-color:var(--color-pink-100)}.border-pink-200{border-color:var(--color-pink-200)}.border-pink-300{border-color:var(--color-pink-300)}.border-pink-400{border-color:var(--color-pink-400)}.border-pink-500{border-color:var(--color-pink-500)}.border-pink-600{border-color:var(--color-pink-600)}.border-pink-700{border-color:var(--color-pink-700)}.border-pink-800{border-color:var(--color-pink-800)}.border-pink-900{border-color:var(--color-pink-900)}.border-pink-950{border-color:var(--color-pink-950)}.border-primary-500{border-color:var(--color-primary-500)}.border-purple-50{border-color:var(--color-purple-50)}.border-purple-100{border-color:var(--color-purple-100)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-400{border-color:var(--color-purple-400)}.border-purple-500{border-color:var(--color-purple-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-purple-700{border-color:var(--color-purple-700)}.border-purple-800{border-color:var(--color-purple-800)}.border-purple-900{border-color:var(--color-purple-900)}.border-purple-950{border-color:var(--color-purple-950)}.border-red-50{border-color:var(--color-red-50)}.border-red-100{border-color:var(--color-red-100)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-red-400{border-color:var(--color-red-400)}.border-red-500{border-color:var(--color-red-500)}.border-red-600{border-color:var(--color-red-600)}.border-red-700{border-color:var(--color-red-700)}.border-red-800{border-color:var(--color-red-800)}.border-red-900{border-color:var(--color-red-900)}.border-red-950{border-color:var(--color-red-950)}.border-rose-50{border-color:var(--color-rose-50)}.border-rose-100{border-color:var(--color-rose-100)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-rose-400{border-color:var(--color-rose-400)}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-600{border-color:var(--color-rose-600)}.border-rose-700{border-color:var(--color-rose-700)}.border-rose-800{border-color:var(--color-rose-800)}.border-rose-900{border-color:var(--color-rose-900)}.border-rose-950{border-color:var(--color-rose-950)}.border-sky-50{border-color:var(--color-sky-50)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-300{border-color:var(--color-sky-300)}.border-sky-400{border-color:var(--color-sky-400)}.border-sky-500{border-color:var(--color-sky-500)}.border-sky-600{border-color:var(--color-sky-600)}.border-sky-700{border-color:var(--color-sky-700)}.border-sky-800{border-color:var(--color-sky-800)}.border-sky-900{border-color:var(--color-sky-900)}.border-sky-950{border-color:var(--color-sky-950)}.border-slate-50{border-color:var(--color-slate-50)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400{border-color:var(--color-slate-400)}.border-slate-500{border-color:var(--color-slate-500)}.border-slate-600{border-color:var(--color-slate-600)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-900{border-color:var(--color-slate-900)}.border-slate-950{border-color:var(--color-slate-950)}.border-stone-50{border-color:var(--color-stone-50)}.border-stone-100{border-color:var(--color-stone-100)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-stone-400{border-color:var(--color-stone-400)}.border-stone-500{border-color:var(--color-stone-500)}.border-stone-600{border-color:var(--color-stone-600)}.border-stone-700{border-color:var(--color-stone-700)}.border-stone-800{border-color:var(--color-stone-800)}.border-stone-900{border-color:var(--color-stone-900)}.border-stone-950{border-color:var(--color-stone-950)}.border-teal-50{border-color:var(--color-teal-50)}.border-teal-100{border-color:var(--color-teal-100)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-400{border-color:var(--color-teal-400)}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-teal-800{border-color:var(--color-teal-800)}.border-teal-900{border-color:var(--color-teal-900)}.border-teal-950{border-color:var(--color-teal-950)}.border-transparent{border-color:#0000}.border-violet-50{border-color:var(--color-violet-50)}.border-violet-100{border-color:var(--color-violet-100)}.border-violet-200{border-color:var(--color-violet-200)}.border-violet-300{border-color:var(--color-violet-300)}.border-violet-400{border-color:var(--color-violet-400)}.border-violet-500{border-color:var(--color-violet-500)}.border-violet-600{border-color:var(--color-violet-600)}.border-violet-700{border-color:var(--color-violet-700)}.border-violet-800{border-color:var(--color-violet-800)}.border-violet-900{border-color:var(--color-violet-900)}.border-violet-950{border-color:var(--color-violet-950)}.border-white{border-color:var(--color-white)}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-yellow-50{border-color:var(--color-yellow-50)}.border-yellow-100{border-color:var(--color-yellow-100)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-400{border-color:var(--color-yellow-400)}.border-yellow-500{border-color:var(--color-yellow-500)}.border-yellow-600{border-color:var(--color-yellow-600)}.border-yellow-700{border-color:var(--color-yellow-700)}.border-yellow-800{border-color:var(--color-yellow-800)}.border-yellow-900{border-color:var(--color-yellow-900)}.border-yellow-950{border-color:var(--color-yellow-950)}.border-zinc-50{border-color:var(--color-zinc-50)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-400{border-color:var(--color-zinc-400)}.border-zinc-500{border-color:var(--color-zinc-500)}.border-zinc-600{border-color:var(--color-zinc-600)}.border-zinc-700{border-color:var(--color-zinc-700)}.border-zinc-800{border-color:var(--color-zinc-800)}.border-zinc-900{border-color:var(--color-zinc-900)}.border-zinc-950{border-color:var(--color-zinc-950)}.border-t-blue-500{border-top-color:var(--color-blue-500)}.border-t-gray-900{border-top-color:var(--color-gray-900)}.border-t-white{border-top-color:var(--color-white)}.bg-\[\#03C75A\]{background-color:#03c75a}.bg-\[\#1877F2\]{background-color:#1877f2}.bg-\[\#FEE500\]{background-color:#fee500}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-300{background-color:var(--color-amber-300)}.bg-amber-400{background-color:var(--color-amber-400)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-amber-800{background-color:var(--color-amber-800)}.bg-amber-900{background-color:var(--color-amber-900)}.bg-amber-950{background-color:var(--color-amber-950)}.bg-black{background-color:var(--color-black)}.bg-black\/0{background-color:#0000}@supports (color:color-mix(in lab,red,red)){.bg-black\/0{background-color:color-mix(in oklab,var(--color-black)0%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/70{background-color:#eff6ffb3}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/70{background-color:color-mix(in oklab,var(--color-blue-50)70%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-300{background-color:var(--color-blue-300)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-blue-800{background-color:var(--color-blue-800)}.bg-blue-900{background-color:var(--color-blue-900)}.bg-blue-950{background-color:var(--color-blue-950)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-200{background-color:var(--color-cyan-200)}.bg-cyan-300{background-color:var(--color-cyan-300)}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-cyan-600{background-color:var(--color-cyan-600)}.bg-cyan-700{background-color:var(--color-cyan-700)}.bg-cyan-800{background-color:var(--color-cyan-800)}.bg-cyan-900{background-color:var(--color-cyan-900)}.bg-cyan-950{background-color:var(--color-cyan-950)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-300{background-color:var(--color-emerald-300)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-emerald-900{background-color:var(--color-emerald-900)}.bg-emerald-950{background-color:var(--color-emerald-950)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-fuchsia-100{background-color:var(--color-fuchsia-100)}.bg-fuchsia-200{background-color:var(--color-fuchsia-200)}.bg-fuchsia-300{background-color:var(--color-fuchsia-300)}.bg-fuchsia-400{background-color:var(--color-fuchsia-400)}.bg-fuchsia-500{background-color:var(--color-fuchsia-500)}.bg-fuchsia-600{background-color:var(--color-fuchsia-600)}.bg-fuchsia-700{background-color:var(--color-fuchsia-700)}.bg-fuchsia-800{background-color:var(--color-fuchsia-800)}.bg-fuchsia-900{background-color:var(--color-fuchsia-900)}.bg-fuchsia-950{background-color:var(--color-fuchsia-950)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-100\/70{background-color:#f3f4f6b3}@supports (color:color-mix(in lab,red,red)){.bg-gray-100\/70{background-color:color-mix(in oklab,var(--color-gray-100)70%,transparent)}}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-900\/40{background-color:#10182866}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/40{background-color:color-mix(in oklab,var(--color-gray-900)40%,transparent)}}.bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-green-300{background-color:var(--color-green-300)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-800{background-color:var(--color-green-800)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-950{background-color:var(--color-green-950)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-200{background-color:var(--color-indigo-200)}.bg-indigo-300{background-color:var(--color-indigo-300)}.bg-indigo-400{background-color:var(--color-indigo-400)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-indigo-700{background-color:var(--color-indigo-700)}.bg-indigo-800{background-color:var(--color-indigo-800)}.bg-indigo-900{background-color:var(--color-indigo-900)}.bg-indigo-950{background-color:var(--color-indigo-950)}.bg-inherit{background-color:inherit}.bg-lime-50{background-color:var(--color-lime-50)}.bg-lime-100{background-color:var(--color-lime-100)}.bg-lime-200{background-color:var(--color-lime-200)}.bg-lime-300{background-color:var(--color-lime-300)}.bg-lime-400{background-color:var(--color-lime-400)}.bg-lime-500{background-color:var(--color-lime-500)}.bg-lime-600{background-color:var(--color-lime-600)}.bg-lime-700{background-color:var(--color-lime-700)}.bg-lime-800{background-color:var(--color-lime-800)}.bg-lime-900{background-color:var(--color-lime-900)}.bg-lime-950{background-color:var(--color-lime-950)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-orange-300{background-color:var(--color-orange-300)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-orange-700{background-color:var(--color-orange-700)}.bg-orange-800{background-color:var(--color-orange-800)}.bg-orange-900{background-color:var(--color-orange-900)}.bg-orange-950{background-color:var(--color-orange-950)}.bg-pink-50{background-color:var(--color-pink-50)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-200{background-color:var(--color-pink-200)}.bg-pink-300{background-color:var(--color-pink-300)}.bg-pink-400{background-color:var(--color-pink-400)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-pink-600{background-color:var(--color-pink-600)}.bg-pink-700{background-color:var(--color-pink-700)}.bg-pink-800{background-color:var(--color-pink-800)}.bg-pink-900{background-color:var(--color-pink-900)}.bg-pink-950{background-color:var(--color-pink-950)}.bg-primary-50{background-color:var(--color-primary-50)}.bg-primary-100{background-color:var(--color-primary-100)}.bg-primary-600{background-color:var(--color-primary-600)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-200{background-color:var(--color-purple-200)}.bg-purple-300{background-color:var(--color-purple-300)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-purple-500{background-color:var(--color-purple-500)}.bg-purple-500\/80{background-color:#ac4bffcc}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/80{background-color:color-mix(in oklab,var(--color-purple-500)80%,transparent)}}.bg-purple-600{background-color:var(--color-purple-600)}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-800{background-color:var(--color-purple-800)}.bg-purple-900{background-color:var(--color-purple-900)}.bg-purple-950{background-color:var(--color-purple-950)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-50\/30{background-color:#fef2f24d}@supports (color:color-mix(in lab,red,red)){.bg-red-50\/30{background-color:color-mix(in oklab,var(--color-red-50)30%,transparent)}}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-300{background-color:var(--color-red-300)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/50{background-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/50{background-color:color-mix(in oklab,var(--color-red-500)50%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-950{background-color:var(--color-red-950)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-100{background-color:var(--color-rose-100)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-rose-700{background-color:var(--color-rose-700)}.bg-rose-800{background-color:var(--color-rose-800)}.bg-rose-900{background-color:var(--color-rose-900)}.bg-rose-950{background-color:var(--color-rose-950)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-100{background-color:var(--color-sky-100)}.bg-sky-200{background-color:var(--color-sky-200)}.bg-sky-300{background-color:var(--color-sky-300)}.bg-sky-400{background-color:var(--color-sky-400)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-sky-950{background-color:var(--color-sky-950)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-600{background-color:var(--color-slate-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-stone-200{background-color:var(--color-stone-200)}.bg-stone-300{background-color:var(--color-stone-300)}.bg-stone-400{background-color:var(--color-stone-400)}.bg-stone-500{background-color:var(--color-stone-500)}.bg-stone-600{background-color:var(--color-stone-600)}.bg-stone-700{background-color:var(--color-stone-700)}.bg-stone-800{background-color:var(--color-stone-800)}.bg-stone-900{background-color:var(--color-stone-900)}.bg-stone-950{background-color:var(--color-stone-950)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-100{background-color:var(--color-teal-100)}.bg-teal-200{background-color:var(--color-teal-200)}.bg-teal-300{background-color:var(--color-teal-300)}.bg-teal-400{background-color:var(--color-teal-400)}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-teal-700{background-color:var(--color-teal-700)}.bg-teal-800{background-color:var(--color-teal-800)}.bg-teal-900{background-color:var(--color-teal-900)}.bg-teal-950{background-color:var(--color-teal-950)}.bg-transparent{background-color:#0000}.bg-violet-50{background-color:var(--color-violet-50)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-200{background-color:var(--color-violet-200)}.bg-violet-300{background-color:var(--color-violet-300)}.bg-violet-400{background-color:var(--color-violet-400)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-violet-600{background-color:var(--color-violet-600)}.bg-violet-700{background-color:var(--color-violet-700)}.bg-violet-800{background-color:var(--color-violet-800)}.bg-violet-900{background-color:var(--color-violet-900)}.bg-violet-950{background-color:var(--color-violet-950)}.bg-white{background-color:var(--color-white)}.bg-white\/20{background-color:#fff3}@supports (color:color-mix(in lab,red,red)){.bg-white\/20{background-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-white\/30{background-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.bg-white\/30{background-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-200{background-color:var(--color-yellow-200)}.bg-yellow-300{background-color:var(--color-yellow-300)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-700{background-color:var(--color-yellow-700)}.bg-yellow-800{background-color:var(--color-yellow-800)}.bg-yellow-900{background-color:var(--color-yellow-900)}.bg-yellow-950{background-color:var(--color-yellow-950)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-200{background-color:var(--color-zinc-200)}.bg-zinc-300{background-color:var(--color-zinc-300)}.bg-zinc-400{background-color:var(--color-zinc-400)}.bg-zinc-500{background-color:var(--color-zinc-500)}.bg-zinc-600{background-color:var(--color-zinc-600)}.bg-zinc-700{background-color:var(--color-zinc-700)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-950{background-color:var(--color-zinc-950)}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-bl{--tw-gradient-position:to bottom left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tl{--tw-gradient-position:to top left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tr{--tw-gradient-position:to top right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black{--tw-gradient-from:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/90{--tw-gradient-from:#000000e6}@supports (color:color-mix(in lab,red,red)){.from-black\/90{--tw-gradient-from:color-mix(in oklab,var(--color-black)90%,transparent)}}.from-black\/90{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-50{--tw-gradient-from:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-100{--tw-gradient-from:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-200{--tw-gradient-from:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-300{--tw-gradient-from:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-400{--tw-gradient-from:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-600{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-700{--tw-gradient-from:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-800{--tw-gradient-from:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-current{--tw-gradient-from:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-emerald-500{--tw-gradient-from:var(--color-emerald-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-50{--tw-gradient-from:var(--color-green-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-100{--tw-gradient-from:var(--color-green-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-200{--tw-gradient-from:var(--color-green-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-300{--tw-gradient-from:var(--color-green-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-400{--tw-gradient-from:var(--color-green-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-600{--tw-gradient-from:var(--color-green-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-700{--tw-gradient-from:var(--color-green-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-800{--tw-gradient-from:var(--color-green-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-900{--tw-gradient-from:var(--color-green-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-50{--tw-gradient-from:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-100{--tw-gradient-from:var(--color-indigo-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-200{--tw-gradient-from:var(--color-indigo-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-300{--tw-gradient-from:var(--color-indigo-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-400{--tw-gradient-from:var(--color-indigo-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-500{--tw-gradient-from:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-600{--tw-gradient-from:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-700{--tw-gradient-from:var(--color-indigo-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-800{--tw-gradient-from:var(--color-indigo-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-900{--tw-gradient-from:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-inherit{--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-50{--tw-gradient-from:var(--color-orange-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-100{--tw-gradient-from:var(--color-orange-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-200{--tw-gradient-from:var(--color-orange-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-300{--tw-gradient-from:var(--color-orange-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-400{--tw-gradient-from:var(--color-orange-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-500{--tw-gradient-from:var(--color-orange-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-600{--tw-gradient-from:var(--color-orange-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-700{--tw-gradient-from:var(--color-orange-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-800{--tw-gradient-from:var(--color-orange-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-900{--tw-gradient-from:var(--color-orange-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-50{--tw-gradient-from:var(--color-pink-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-100{--tw-gradient-from:var(--color-pink-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-200{--tw-gradient-from:var(--color-pink-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-300{--tw-gradient-from:var(--color-pink-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-400{--tw-gradient-from:var(--color-pink-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-500{--tw-gradient-from:var(--color-pink-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-600{--tw-gradient-from:var(--color-pink-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-700{--tw-gradient-from:var(--color-pink-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-800{--tw-gradient-from:var(--color-pink-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-900{--tw-gradient-from:var(--color-pink-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-50{--tw-gradient-from:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-100{--tw-gradient-from:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-200{--tw-gradient-from:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-300{--tw-gradient-from:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-400{--tw-gradient-from:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-500{--tw-gradient-from:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-600{--tw-gradient-from:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-700{--tw-gradient-from:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-800{--tw-gradient-from:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-900{--tw-gradient-from:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-50{--tw-gradient-from:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-100{--tw-gradient-from:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-200{--tw-gradient-from:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-300{--tw-gradient-from:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-400{--tw-gradient-from:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-600{--tw-gradient-from:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-700{--tw-gradient-from:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-800{--tw-gradient-from:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-900{--tw-gradient-from:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-50{--tw-gradient-from:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-100{--tw-gradient-from:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-200{--tw-gradient-from:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-300{--tw-gradient-from:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-400{--tw-gradient-from:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-500{--tw-gradient-from:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-600{--tw-gradient-from:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-700{--tw-gradient-from:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-800{--tw-gradient-from:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-900{--tw-gradient-from:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-50{--tw-gradient-from:var(--color-yellow-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-100{--tw-gradient-from:var(--color-yellow-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-200{--tw-gradient-from:var(--color-yellow-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-300{--tw-gradient-from:var(--color-yellow-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-500{--tw-gradient-from:var(--color-yellow-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-600{--tw-gradient-from:var(--color-yellow-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-700{--tw-gradient-from:var(--color-yellow-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-800{--tw-gradient-from:var(--color-yellow-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-900{--tw-gradient-from:var(--color-yellow-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-black{--tw-gradient-via:var(--color-black);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-black\/60{--tw-gradient-via:#0009}@supports (color:color-mix(in lab,red,red)){.via-black\/60{--tw-gradient-via:color-mix(in oklab,var(--color-black)60%,transparent)}}.via-black\/60{--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-50{--tw-gradient-via:var(--color-blue-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-100{--tw-gradient-via:var(--color-blue-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-200{--tw-gradient-via:var(--color-blue-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-300{--tw-gradient-via:var(--color-blue-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-400{--tw-gradient-via:var(--color-blue-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-500{--tw-gradient-via:var(--color-blue-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-600{--tw-gradient-via:var(--color-blue-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-700{--tw-gradient-via:var(--color-blue-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-800{--tw-gradient-via:var(--color-blue-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-900{--tw-gradient-via:var(--color-blue-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-current{--tw-gradient-via:currentcolor;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-50{--tw-gradient-via:var(--color-gray-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-100{--tw-gradient-via:var(--color-gray-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-200{--tw-gradient-via:var(--color-gray-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-300{--tw-gradient-via:var(--color-gray-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-400{--tw-gradient-via:var(--color-gray-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-500{--tw-gradient-via:var(--color-gray-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-600{--tw-gradient-via:var(--color-gray-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-700{--tw-gradient-via:var(--color-gray-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-800{--tw-gradient-via:var(--color-gray-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-900{--tw-gradient-via:var(--color-gray-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-inherit{--tw-gradient-via:inherit;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-50{--tw-gradient-via:var(--color-slate-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-100{--tw-gradient-via:var(--color-slate-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-200{--tw-gradient-via:var(--color-slate-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-300{--tw-gradient-via:var(--color-slate-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-400{--tw-gradient-via:var(--color-slate-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-500{--tw-gradient-via:var(--color-slate-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-600{--tw-gradient-via:var(--color-slate-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-700{--tw-gradient-via:var(--color-slate-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-800{--tw-gradient-via:var(--color-slate-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-900{--tw-gradient-via:var(--color-slate-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-black{--tw-gradient-to:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-50{--tw-gradient-to:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-100{--tw-gradient-to:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-200{--tw-gradient-to:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-300{--tw-gradient-to:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-400{--tw-gradient-to:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-700{--tw-gradient-to:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-800{--tw-gradient-to:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-900{--tw-gradient-to:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-current{--tw-gradient-to:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-inherit{--tw-gradient-to:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-50{--tw-gradient-to:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-100{--tw-gradient-to:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-200{--tw-gradient-to:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-300{--tw-gradient-to:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-400{--tw-gradient-to:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-500{--tw-gradient-to:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-600{--tw-gradient-to:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-700{--tw-gradient-to:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-800{--tw-gradient-to:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-900{--tw-gradient-to:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-50{--tw-gradient-to:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-100{--tw-gradient-to:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-200{--tw-gradient-to:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-300{--tw-gradient-to:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-400{--tw-gradient-to:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-500{--tw-gradient-to:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-600{--tw-gradient-to:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-700{--tw-gradient-to:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-800{--tw-gradient-to:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-900{--tw-gradient-to:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-50{--tw-gradient-to:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-100{--tw-gradient-to:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-200{--tw-gradient-to:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-300{--tw-gradient-to:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-400{--tw-gradient-to:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-500{--tw-gradient-to:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-600{--tw-gradient-to:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-700{--tw-gradient-to:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-800{--tw-gradient-to:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-900{--tw-gradient-to:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-teal-600{--tw-gradient-to:var(--color-teal-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-none{object-fit:none}.object-scale-down{object-fit:scale-down}.object-bottom{object-position:bottom}.object-center{object-position:center}.object-left{object-position:left}.object-left-bottom{object-position:left bottom}.object-left-top{object-position:left top}.object-right{object-position:right}.object-right-bottom{object-position:right bottom}.object-right-top{object-position:right top}.object-top{object-position:top}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-10{padding:calc(var(--spacing)*10)}.p-12{padding:calc(var(--spacing)*12)}.p-16{padding:calc(var(--spacing)*16)}.p-20{padding:calc(var(--spacing)*20)}.p-24{padding:calc(var(--spacing)*24)}.px-0{padding-inline:calc(var(--spacing)*0)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.px-10{padding-inline:calc(var(--spacing)*10)}.px-12{padding-inline:calc(var(--spacing)*12)}.px-16{padding-inline:calc(var(--spacing)*16)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-3\.5{padding-block:calc(var(--spacing)*3.5)}.py-4{padding-block:calc(var(--spacing)*4)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-5{padding-top:calc(var(--spacing)*5)}.pt-6{padding-top:calc(var(--spacing)*6)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-10{padding-top:calc(var(--spacing)*10)}.pt-12{padding-top:calc(var(--spacing)*12)}.pt-16{padding-top:calc(var(--spacing)*16)}.pr-0{padding-right:calc(var(--spacing)*0)}.pr-1{padding-right:calc(var(--spacing)*1)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-5{padding-right:calc(var(--spacing)*5)}.pr-6{padding-right:calc(var(--spacing)*6)}.pr-8{padding-right:calc(var(--spacing)*8)}.pr-10{padding-right:calc(var(--spacing)*10)}.pr-12{padding-right:calc(var(--spacing)*12)}.pr-16{padding-right:calc(var(--spacing)*16)}.pb-0{padding-bottom:calc(var(--spacing)*0)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-5{padding-bottom:calc(var(--spacing)*5)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.pb-10{padding-bottom:calc(var(--spacing)*10)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pl-0{padding-left:calc(var(--spacing)*0)}.pl-1{padding-left:calc(var(--spacing)*1)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-5{padding-left:calc(var(--spacing)*5)}.pl-6{padding-left:calc(var(--spacing)*6)}.pl-8{padding-left:calc(var(--spacing)*8)}.pl-10{padding-left:calc(var(--spacing)*10)}.pl-12{padding-left:calc(var(--spacing)*12)}.pl-16{padding-left:calc(var(--spacing)*16)}.text-center{text-align:center}.text-end{text-align:end}.text-justify{text-align:justify}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.indent-0{text-indent:calc(var(--spacing)*0)}.indent-1{text-indent:calc(var(--spacing)*1)}.indent-2{text-indent:calc(var(--spacing)*2)}.indent-4{text-indent:calc(var(--spacing)*4)}.indent-8{text-indent:calc(var(--spacing)*8)}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.align-text-bottom{vertical-align:text-bottom}.align-text-top{vertical-align:text-top}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-3{--tw-leading:calc(var(--spacing)*3);line-height:calc(var(--spacing)*3)}.leading-4{--tw-leading:calc(var(--spacing)*4);line-height:calc(var(--spacing)*4)}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-7{--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.leading-8{--tw-leading:calc(var(--spacing)*8);line-height:calc(var(--spacing)*8)}.leading-9{--tw-leading:calc(var(--spacing)*9);line-height:calc(var(--spacing)*9)}.leading-10{--tw-leading:calc(var(--spacing)*10);line-height:calc(var(--spacing)*10)}.leading-loose{--tw-leading:var(--leading-loose);line-height:var(--leading-loose)}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-extralight{--tw-font-weight:var(--font-weight-extralight);font-weight:var(--font-weight-extralight)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-thin{--tw-font-weight:var(--font-weight-thin);font-weight:var(--font-weight-thin)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.\!whitespace-normal{white-space:normal!important}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#000000\]\/85{color:oklab(0% none none/.85)}.text-amber-50{color:var(--color-amber-50)}.text-amber-100{color:var(--color-amber-100)}.text-amber-200{color:var(--color-amber-200)}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-950{color:var(--color-amber-950)}.text-black{color:var(--color-black)}.text-blue-50{color:var(--color-blue-50)}.text-blue-100{color:var(--color-blue-100)}.text-blue-200{color:var(--color-blue-200)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-blue-950{color:var(--color-blue-950)}.text-current{color:currentColor}.text-cyan-50{color:var(--color-cyan-50)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-200{color:var(--color-cyan-200)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-500{color:var(--color-cyan-500)}.text-cyan-600{color:var(--color-cyan-600)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-800{color:var(--color-cyan-800)}.text-cyan-900{color:var(--color-cyan-900)}.text-cyan-950{color:var(--color-cyan-950)}.text-emerald-50{color:var(--color-emerald-50)}.text-emerald-100{color:var(--color-emerald-100)}.text-emerald-200{color:var(--color-emerald-200)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-fuchsia-50{color:var(--color-fuchsia-50)}.text-fuchsia-100{color:var(--color-fuchsia-100)}.text-fuchsia-200{color:var(--color-fuchsia-200)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-fuchsia-500{color:var(--color-fuchsia-500)}.text-fuchsia-600{color:var(--color-fuchsia-600)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-fuchsia-800{color:var(--color-fuchsia-800)}.text-fuchsia-900{color:var(--color-fuchsia-900)}.text-fuchsia-950{color:var(--color-fuchsia-950)}.text-gray-50{color:var(--color-gray-50)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-gray-950{color:var(--color-gray-950)}.text-green-50{color:var(--color-green-50)}.text-green-100{color:var(--color-green-100)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-green-900{color:var(--color-green-900)}.text-green-950{color:var(--color-green-950)}.text-indigo-50{color:var(--color-indigo-50)}.text-indigo-100{color:var(--color-indigo-100)}.text-indigo-200{color:var(--color-indigo-200)}.text-indigo-300{color:var(--color-indigo-300)}.text-indigo-400{color:var(--color-indigo-400)}.text-indigo-500{color:var(--color-indigo-500)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-indigo-800{color:var(--color-indigo-800)}.text-indigo-900{color:var(--color-indigo-900)}.text-indigo-950{color:var(--color-indigo-950)}.text-inherit{color:inherit}.text-lime-50{color:var(--color-lime-50)}.text-lime-100{color:var(--color-lime-100)}.text-lime-200{color:var(--color-lime-200)}.text-lime-300{color:var(--color-lime-300)}.text-lime-400{color:var(--color-lime-400)}.text-lime-500{color:var(--color-lime-500)}.text-lime-600{color:var(--color-lime-600)}.text-lime-700{color:var(--color-lime-700)}.text-lime-800{color:var(--color-lime-800)}.text-lime-900{color:var(--color-lime-900)}.text-lime-950{color:var(--color-lime-950)}.text-neutral-50{color:var(--color-neutral-50)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-orange-50{color:var(--color-orange-50)}.text-orange-100{color:var(--color-orange-100)}.text-orange-200{color:var(--color-orange-200)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-orange-900{color:var(--color-orange-900)}.text-orange-950{color:var(--color-orange-950)}.text-pink-50{color:var(--color-pink-50)}.text-pink-100{color:var(--color-pink-100)}.text-pink-200{color:var(--color-pink-200)}.text-pink-300{color:var(--color-pink-300)}.text-pink-400{color:var(--color-pink-400)}.text-pink-500{color:var(--color-pink-500)}.text-pink-600{color:var(--color-pink-600)}.text-pink-700{color:var(--color-pink-700)}.text-pink-800{color:var(--color-pink-800)}.text-pink-900{color:var(--color-pink-900)}.text-pink-950{color:var(--color-pink-950)}.text-primary-400{color:var(--color-primary-400)}.text-primary-600{color:var(--color-primary-600)}.text-purple-50{color:var(--color-purple-50)}.text-purple-100{color:var(--color-purple-100)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-purple-900{color:var(--color-purple-900)}.text-purple-950{color:var(--color-purple-950)}.text-red-50{color:var(--color-red-50)}.text-red-100{color:var(--color-red-100)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-50{color:var(--color-rose-50)}.text-rose-100{color:var(--color-rose-100)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-50{color:var(--color-sky-50)}.text-sky-100{color:var(--color-sky-100)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-900{color:var(--color-sky-900)}.text-sky-950{color:var(--color-sky-950)}.text-slate-50{color:var(--color-slate-50)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-stone-50{color:var(--color-stone-50)}.text-stone-100{color:var(--color-stone-100)}.text-stone-200{color:var(--color-stone-200)}.text-stone-300{color:var(--color-stone-300)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.text-stone-800{color:var(--color-stone-800)}.text-stone-900{color:var(--color-stone-900)}.text-stone-950{color:var(--color-stone-950)}.text-teal-50{color:var(--color-teal-50)}.text-teal-100{color:var(--color-teal-100)}.text-teal-200{color:var(--color-teal-200)}.text-teal-300{color:var(--color-teal-300)}.text-teal-400{color:var(--color-teal-400)}.text-teal-500{color:var(--color-teal-500)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-900{color:var(--color-teal-900)}.text-teal-950{color:var(--color-teal-950)}.text-transparent{color:#0000}.text-violet-50{color:var(--color-violet-50)}.text-violet-100{color:var(--color-violet-100)}.text-violet-200{color:var(--color-violet-200)}.text-violet-300{color:var(--color-violet-300)}.text-violet-400{color:var(--color-violet-400)}.text-violet-500{color:var(--color-violet-500)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-violet-900{color:var(--color-violet-900)}.text-violet-950{color:var(--color-violet-950)}.text-white{color:var(--color-white)}.text-yellow-50{color:var(--color-yellow-50)}.text-yellow-100{color:var(--color-yellow-100)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-900{color:var(--color-yellow-900)}.text-yellow-950{color:var(--color-yellow-950)}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.text-zinc-950{color:var(--color-zinc-950)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.placeholder-gray-400::placeholder{color:var(--color-gray-400)}.placeholder-gray-500::placeholder{color:var(--color-gray-500)}.accent-black{accent-color:var(--color-black)}.accent-blue-600{accent-color:var(--color-blue-600)}.accent-gray-900{accent-color:var(--color-gray-900)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-15{opacity:.15}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.bg-blend-color{background-blend-mode:color}.bg-blend-color-burn{background-blend-mode:color-burn}.bg-blend-color-dodge{background-blend-mode:color-dodge}.bg-blend-darken{background-blend-mode:darken}.bg-blend-difference{background-blend-mode:difference}.bg-blend-exclusion{background-blend-mode:exclusion}.bg-blend-hard-light{background-blend-mode:hard-light}.bg-blend-hue{background-blend-mode:hue}.bg-blend-lighten{background-blend-mode:lighten}.bg-blend-luminosity{background-blend-mode:luminosity}.bg-blend-multiply{background-blend-mode:multiply}.bg-blend-normal{background-blend-mode:normal}.bg-blend-overlay{background-blend-mode:overlay}.bg-blend-saturation{background-blend-mode:saturation}.bg-blend-screen{background-blend-mode:screen}.bg-blend-soft-light{background-blend-mode:soft-light}.mix-blend-color{mix-blend-mode:color}.mix-blend-color-burn{mix-blend-mode:color-burn}.mix-blend-color-dodge{mix-blend-mode:color-dodge}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-difference{mix-blend-mode:difference}.mix-blend-exclusion{mix-blend-mode:exclusion}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-hue{mix-blend-mode:hue}.mix-blend-lighten{mix-blend-mode:lighten}.mix-blend-luminosity{mix-blend-mode:luminosity}.mix-blend-multiply{mix-blend-mode:multiply}.mix-blend-normal{mix-blend-mode:normal}.mix-blend-overlay{mix-blend-mode:overlay}.mix-blend-plus-darker{mix-blend-mode:plus-darker}.mix-blend-plus-lighter{mix-blend-mode:plus-lighter}.mix-blend-saturation{mix-blend-mode:saturation}.mix-blend-screen{mix-blend-mode:screen}.mix-blend-soft-light{mix-blend-mode:soft-light}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-8{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-black{--tw-ring-color:var(--color-black)}.ring-blue-50{--tw-ring-color:var(--color-blue-50)}.ring-blue-100{--tw-ring-color:var(--color-blue-100)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-blue-300{--tw-ring-color:var(--color-blue-300)}.ring-blue-400{--tw-ring-color:var(--color-blue-400)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-blue-600{--tw-ring-color:var(--color-blue-600)}.ring-blue-700{--tw-ring-color:var(--color-blue-700)}.ring-blue-800{--tw-ring-color:var(--color-blue-800)}.ring-blue-900{--tw-ring-color:var(--color-blue-900)}.ring-current{--tw-ring-color:currentcolor}.ring-gray-50{--tw-ring-color:var(--color-gray-50)}.ring-gray-100{--tw-ring-color:var(--color-gray-100)}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-gray-400{--tw-ring-color:var(--color-gray-400)}.ring-gray-500{--tw-ring-color:var(--color-gray-500)}.ring-gray-600{--tw-ring-color:var(--color-gray-600)}.ring-gray-700{--tw-ring-color:var(--color-gray-700)}.ring-gray-800{--tw-ring-color:var(--color-gray-800)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-inherit{--tw-ring-color:inherit}.ring-red-500{--tw-ring-color:var(--color-red-500)}.ring-slate-50{--tw-ring-color:var(--color-slate-50)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-slate-300{--tw-ring-color:var(--color-slate-300)}.ring-slate-400{--tw-ring-color:var(--color-slate-400)}.ring-slate-500{--tw-ring-color:var(--color-slate-500)}.ring-slate-600{--tw-ring-color:var(--color-slate-600)}.ring-slate-700{--tw-ring-color:var(--color-slate-700)}.ring-slate-800{--tw-ring-color:var(--color-slate-800)}.ring-slate-900{--tw-ring-color:var(--color-slate-900)}.ring-transparent{--tw-ring-color:transparent}.ring-white{--tw-ring-color:var(--color-white)}.ring-offset-0{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-1{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-4{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-8{--tw-ring-offset-width:8px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-black{--tw-ring-offset-color:var(--color-black)}.ring-offset-current{--tw-ring-offset-color:currentcolor}.ring-offset-inherit{--tw-ring-offset-color:inherit}.ring-offset-transparent{--tw-ring-offset-color:transparent}.ring-offset-white{--tw-ring-offset-color:var(--color-white)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-0{outline-style:var(--tw-outline-style);outline-width:0}.outline-1{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-4{outline-style:var(--tw-outline-style);outline-width:4px}.outline-8{outline-style:var(--tw-outline-style);outline-width:8px}.outline-offset-0{outline-offset:0px}.outline-offset-1{outline-offset:1px}.outline-offset-2{outline-offset:2px}.outline-offset-4{outline-offset:4px}.outline-offset-8{outline-offset:8px}.outline-black{outline-color:var(--color-black)}.outline-current{outline-color:currentColor}.outline-inherit{outline-color:inherit}.outline-transparent{outline-color:#0000}.outline-white{outline-color:var(--color-white)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-lg{--tw-blur:blur(var(--blur-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-md{--tw-blur:blur(var(--blur-md));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-none{--tw-blur: ;filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-sm{--tw-blur:blur(var(--blur-sm));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-0{--tw-brightness:brightness(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-50{--tw-brightness:brightness(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-75{--tw-brightness:brightness(75%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-90{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-95{--tw-brightness:brightness(95%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-100{--tw-brightness:brightness(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-105{--tw-brightness:brightness(105%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-125{--tw-brightness:brightness(125%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-150{--tw-brightness:brightness(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-200{--tw-brightness:brightness(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-0{--tw-contrast:contrast(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-50{--tw-contrast:contrast(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-75{--tw-contrast:contrast(75%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-100{--tw-contrast:contrast(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-125{--tw-contrast:contrast(125%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-150{--tw-contrast:contrast(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-200{--tw-contrast:contrast(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a))drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a)drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size:drop-shadow(0 3px 3px var(--tw-drop-shadow-color,#0000001f));--tw-drop-shadow:drop-shadow(var(--drop-shadow-md));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-sm{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-sm));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-xl{--tw-drop-shadow-size:drop-shadow(0 9px 7px var(--tw-drop-shadow-color,#0000001a));--tw-drop-shadow:drop-shadow(var(--drop-shadow-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-none{--tw-drop-shadow: ;filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.grayscale-0{--tw-grayscale:grayscale(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-15{--tw-hue-rotate:hue-rotate( -15deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-30{--tw-hue-rotate:hue-rotate( -30deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-60{--tw-hue-rotate:hue-rotate( -60deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-90{--tw-hue-rotate:hue-rotate( -90deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-180{--tw-hue-rotate:hue-rotate( -180deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-0{--tw-hue-rotate:hue-rotate(0deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-15{--tw-hue-rotate:hue-rotate(15deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-30{--tw-hue-rotate:hue-rotate(30deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-60{--tw-hue-rotate:hue-rotate(60deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-90{--tw-hue-rotate:hue-rotate(90deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-180{--tw-hue-rotate:hue-rotate(180deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert-0{--tw-invert:invert(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-0{--tw-saturate:saturate(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-50{--tw-saturate:saturate(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-100{--tw-saturate:saturate(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-150{--tw-saturate:saturate(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-200{--tw-saturate:saturate(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.sepia{--tw-sepia:sepia(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.sepia-0{--tw-sepia:sepia(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(var(--blur-3xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-none{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-0{--tw-backdrop-brightness:brightness(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-50{--tw-backdrop-brightness:brightness(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-75{--tw-backdrop-brightness:brightness(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-90{--tw-backdrop-brightness:brightness(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-95{--tw-backdrop-brightness:brightness(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-100{--tw-backdrop-brightness:brightness(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-105{--tw-backdrop-brightness:brightness(105%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-110{--tw-backdrop-brightness:brightness(110%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-125{--tw-backdrop-brightness:brightness(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-150{--tw-backdrop-brightness:brightness(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-200{--tw-backdrop-brightness:brightness(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-0{--tw-backdrop-contrast:contrast(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-50{--tw-backdrop-contrast:contrast(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-75{--tw-backdrop-contrast:contrast(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-100{--tw-backdrop-contrast:contrast(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-125{--tw-backdrop-contrast:contrast(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-150{--tw-backdrop-contrast:contrast(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-200{--tw-backdrop-contrast:contrast(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-grayscale-0{--tw-backdrop-grayscale:grayscale(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-0{--tw-backdrop-hue-rotate:hue-rotate(0deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-15{--tw-backdrop-hue-rotate:hue-rotate(15deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-30{--tw-backdrop-hue-rotate:hue-rotate(30deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-60{--tw-backdrop-hue-rotate:hue-rotate(60deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-90{--tw-backdrop-hue-rotate:hue-rotate(90deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-180{--tw-backdrop-hue-rotate:hue-rotate(180deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-invert-0{--tw-backdrop-invert:invert(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-0{--tw-backdrop-opacity:opacity(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-5{--tw-backdrop-opacity:opacity(5%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-10{--tw-backdrop-opacity:opacity(10%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-20{--tw-backdrop-opacity:opacity(20%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-25{--tw-backdrop-opacity:opacity(25%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-30{--tw-backdrop-opacity:opacity(30%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-40{--tw-backdrop-opacity:opacity(40%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-50{--tw-backdrop-opacity:opacity(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-60{--tw-backdrop-opacity:opacity(60%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-70{--tw-backdrop-opacity:opacity(70%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-75{--tw-backdrop-opacity:opacity(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-80{--tw-backdrop-opacity:opacity(80%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-90{--tw-backdrop-opacity:opacity(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-95{--tw-backdrop-opacity:opacity(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-100{--tw-backdrop-opacity:opacity(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-0{--tw-backdrop-saturate:saturate(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-50{--tw-backdrop-saturate:saturate(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-100{--tw-backdrop-saturate:saturate(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-150{--tw-backdrop-saturate:saturate(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-200{--tw-backdrop-saturate:saturate(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-sepia-0{--tw-backdrop-sepia:sepia(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[max-height\]{transition-property:max-height;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.delay-100{transition-delay:.1s}.delay-150{transition-delay:.15s}.delay-200{transition-delay:.2s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.delay-1000{transition-delay:1s}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.duration-1000{--tw-duration:1s;transition-duration:1s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-dashed{--tw-outline-style:dashed;outline-style:dashed}.outline-dotted{--tw-outline-style:dotted;outline-style:dotted}.outline-double{--tw-outline-style:double;outline-style:double}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;user-select:auto}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.ring-inset{--tw-ring-inset:inset}@media(hover:hover){.group-hover\:visible:is(:where(.group):hover *){visibility:visible}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-100:is(:where(.group):hover *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:#0006}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.group-hover\:bg-blue-100:is(:where(.group):hover *){background-color:var(--color-blue-100)}.group-hover\:bg-blue-500:is(:where(.group):hover *){background-color:var(--color-blue-500)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:bg-gray-100:is(:where(.group):hover *){background-color:var(--color-gray-100)}.group-hover\:bg-gray-200:is(:where(.group):hover *){background-color:var(--color-gray-200)}.group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-blue-500:is(:where(.group):hover *){color:var(--color-blue-500)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-blue-700:is(:where(.group):hover *){color:var(--color-blue-700)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:text-gray-800:is(:where(.group):hover *){color:var(--color-gray-800)}.group-hover\:text-gray-900:is(:where(.group):hover *){color:var(--color-gray-900)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.group-focus\:border-blue-500:is(:where(.group):focus *){border-color:var(--color-blue-500)}.group-focus\:ring-2:is(:where(.group):focus *){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-checked\:border-blue-500:is(:where(.peer):checked~*){border-color:var(--color-blue-500)}.peer-checked\:bg-blue-500:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.peer-invalid\:border-red-500:is(:where(.peer):invalid~*){border-color:var(--color-red-500)}.peer-invalid\:text-red-500:is(:where(.peer):invalid~*){color:var(--color-red-500)}.peer-focus\:border-blue-500:is(:where(.peer):focus~*){border-color:var(--color-blue-500)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.first-letter\:text-2xl:first-letter{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.first-letter\:font-bold:first-letter{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.first-letter\:text-blue-600:first-letter{color:var(--color-blue-600)}.first-line\:font-semibold:first-line{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.first-line\:text-gray-700:first-line{color:var(--color-gray-700)}.marker\:text-blue-500 ::marker{color:var(--color-blue-500)}.marker\:text-blue-500::marker{color:var(--color-blue-500)}.marker\:text-blue-500 ::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-blue-500::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-gray-500 ::marker{color:var(--color-gray-500)}.marker\:text-gray-500::marker{color:var(--color-gray-500)}.marker\:text-gray-500 ::-webkit-details-marker{color:var(--color-gray-500)}.marker\:text-gray-500::-webkit-details-marker{color:var(--color-gray-500)}.selection\:bg-blue-200 ::selection{background-color:var(--color-blue-200)}.selection\:bg-blue-200::selection{background-color:var(--color-blue-200)}.selection\:text-blue-900 ::selection{color:var(--color-blue-900)}.selection\:text-blue-900::selection{color:var(--color-blue-900)}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing)*4)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-full::file-selector-button{border-radius:3.40282e38px}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-blue-50::file-selector-button{background-color:var(--color-blue-50)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing)*4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing)*2)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-blue-700::file-selector-button{color:var(--color-blue-700)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}.before\:block:before{content:var(--tw-content);display:block}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:clear-both:after{content:var(--tw-content);clear:both}.after\:block:after{content:var(--tw-content);display:block}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:mt-0:first-child{margin-top:calc(var(--spacing)*0)}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.first\:rounded-l-lg:first-child{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:pt-0:first-child{padding-top:calc(var(--spacing)*0)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}.last\:rounded-r-lg:last-child{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing)*0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.odd\:bg-gray-100:nth-child(odd){background-color:var(--color-gray-100)}.odd\:bg-white:nth-child(odd){background-color:var(--color-white)}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}.even\:bg-gray-100:nth-child(2n){background-color:var(--color-gray-100)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.checked\:border-blue-500:checked{border-color:var(--color-blue-500)}.checked\:border-blue-600:checked{border-color:var(--color-blue-600)}.checked\:bg-blue-500:checked{background-color:var(--color-blue-500)}.checked\:bg-blue-600:checked{background-color:var(--color-blue-600)}.checked\:bg-green-500:checked{background-color:var(--color-green-500)}.checked\:text-white:checked{color:var(--color-white)}.indeterminate\:border-gray-400:indeterminate{border-color:var(--color-gray-400)}.indeterminate\:bg-gray-300:indeterminate{background-color:var(--color-gray-300)}.placeholder-shown\:border-gray-300:placeholder-shown{border-color:var(--color-gray-300)}.autofill\:bg-yellow-100:autofill{background-color:var(--color-yellow-100)}.autofill\:text-black:autofill{color:var(--color-black)}.valid\:border-green-500:valid{border-color:var(--color-green-500)}.valid\:text-green-600:valid{color:var(--color-green-600)}.valid\:ring-green-500:valid{--tw-ring-color:var(--color-green-500)}.invalid\:border-red-500:invalid{border-color:var(--color-red-500)}.invalid\:text-red-600:invalid{color:var(--color-red-600)}.invalid\:ring-red-500:invalid{--tw-ring-color:var(--color-red-500)}.read-only\:cursor-not-allowed:read-only{cursor:not-allowed}.read-only\:bg-gray-50:read-only{background-color:var(--color-gray-50)}.read-only\:bg-gray-100:read-only{background-color:var(--color-gray-100)}.empty\:hidden:empty{display:none}.focus-within\:border-blue-500:focus-within{border-color:var(--color-blue-500)}.focus-within\:border-gray-500:focus-within{border-color:var(--color-gray-500)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-blue-500:focus-within{--tw-ring-color:var(--color-blue-500)}.focus-within\:ring-gray-500:focus-within{--tw-ring-color:var(--color-gray-500)}@media(hover:hover){.hover\:scale-95:hover{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-100:hover{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.hover\:bg-emerald-600:hover{background-color:var(--color-emerald-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-100:hover{background-color:var(--color-green-100)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-300:hover{background-color:var(--color-green-300)}.hover\:bg-green-400:hover{background-color:var(--color-green-400)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-green-800:hover{background-color:var(--color-green-800)}.hover\:bg-green-900:hover{background-color:var(--color-green-900)}.hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.hover\:bg-primary-700:hover{background-color:var(--color-primary-700)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:bg-red-300:hover{background-color:var(--color-red-300)}.hover\:bg-red-400:hover{background-color:var(--color-red-400)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-white\/30:hover{background-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/30:hover{background-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.hover\:bg-yellow-50:hover{background-color:var(--color-yellow-50)}.hover\:bg-yellow-100:hover{background-color:var(--color-yellow-100)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.hover\:bg-yellow-900:hover{background-color:var(--color-yellow-900)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-blue-50:hover{color:var(--color-blue-50)}.hover\:text-blue-100:hover{color:var(--color-blue-100)}.hover\:text-blue-200:hover{color:var(--color-blue-200)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-blue-900:hover{color:var(--color-blue-900)}.hover\:text-gray-50:hover{color:var(--color-gray-50)}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-500:hover{color:var(--color-gray-500)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-800:hover{color:var(--color-gray-800)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-50:hover{color:var(--color-green-50)}.hover\:text-green-100:hover{color:var(--color-green-100)}.hover\:text-green-200:hover{color:var(--color-green-200)}.hover\:text-green-300:hover{color:var(--color-green-300)}.hover\:text-green-400:hover{color:var(--color-green-400)}.hover\:text-green-500:hover{color:var(--color-green-500)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-purple-50:hover{color:var(--color-purple-50)}.hover\:text-purple-100:hover{color:var(--color-purple-100)}.hover\:text-purple-200:hover{color:var(--color-purple-200)}.hover\:text-purple-300:hover{color:var(--color-purple-300)}.hover\:text-purple-400:hover{color:var(--color-purple-400)}.hover\:text-purple-500:hover{color:var(--color-purple-500)}.hover\:text-purple-600:hover{color:var(--color-purple-600)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-800:hover{color:var(--color-purple-800)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-50:hover{color:var(--color-red-50)}.hover\:text-red-100:hover{color:var(--color-red-100)}.hover\:text-red-200:hover{color:var(--color-red-200)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:text-slate-50:hover{color:var(--color-slate-50)}.hover\:text-slate-100:hover{color:var(--color-slate-100)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}.hover\:text-slate-300:hover{color:var(--color-slate-300)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-slate-500:hover{color:var(--color-slate-500)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-0:hover{opacity:0}.hover\:opacity-25:hover{opacity:.25}.hover\:opacity-50:hover{opacity:.5}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}.hover\:shadow:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-none:hover{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-0:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-1:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-4:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-8:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}.hover\:ring-gray-500:hover{--tw-ring-color:var(--color-gray-500)}.hover\:ring-green-500:hover{--tw-ring-color:var(--color-green-500)}.hover\:ring-red-500:hover{--tw-ring-color:var(--color-red-500)}.file\:hover\:bg-blue-100::file-selector-button:hover{background-color:var(--color-blue-100)}}.focus\:border-blue-50:focus{border-color:var(--color-blue-50)}.focus\:border-blue-100:focus{border-color:var(--color-blue-100)}.focus\:border-blue-200:focus{border-color:var(--color-blue-200)}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.focus\:border-blue-900:focus{border-color:var(--color-blue-900)}.focus\:border-gray-50:focus{border-color:var(--color-gray-50)}.focus\:border-gray-100:focus{border-color:var(--color-gray-100)}.focus\:border-gray-200:focus{border-color:var(--color-gray-200)}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:border-gray-400:focus{border-color:var(--color-gray-400)}.focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.focus\:border-gray-800:focus{border-color:var(--color-gray-800)}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-green-500:focus{border-color:var(--color-green-500)}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:border-red-500:focus{border-color:var(--color-red-500)}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-yellow-500:focus{border-color:var(--color-yellow-500)}.focus\:bg-black:focus{background-color:var(--color-black)}.focus\:bg-blue-50:focus{background-color:var(--color-blue-50)}.focus\:bg-blue-100:focus{background-color:var(--color-blue-100)}.focus\:bg-blue-200:focus{background-color:var(--color-blue-200)}.focus\:bg-blue-300:focus{background-color:var(--color-blue-300)}.focus\:bg-blue-400:focus{background-color:var(--color-blue-400)}.focus\:bg-blue-500:focus{background-color:var(--color-blue-500)}.focus\:bg-blue-600:focus{background-color:var(--color-blue-600)}.focus\:bg-blue-700:focus{background-color:var(--color-blue-700)}.focus\:bg-blue-800:focus{background-color:var(--color-blue-800)}.focus\:bg-blue-900:focus{background-color:var(--color-blue-900)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:bg-gray-100:focus{background-color:var(--color-gray-100)}.focus\:bg-gray-200:focus{background-color:var(--color-gray-200)}.focus\:bg-gray-300:focus{background-color:var(--color-gray-300)}.focus\:bg-gray-400:focus{background-color:var(--color-gray-400)}.focus\:bg-gray-500:focus{background-color:var(--color-gray-500)}.focus\:bg-gray-600:focus{background-color:var(--color-gray-600)}.focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.focus\:bg-transparent:focus{background-color:#0000}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:text-black:focus{color:var(--color-black)}.focus\:text-blue-50:focus{color:var(--color-blue-50)}.focus\:text-blue-100:focus{color:var(--color-blue-100)}.focus\:text-blue-200:focus{color:var(--color-blue-200)}.focus\:text-blue-300:focus{color:var(--color-blue-300)}.focus\:text-blue-400:focus{color:var(--color-blue-400)}.focus\:text-blue-500:focus{color:var(--color-blue-500)}.focus\:text-blue-600:focus{color:var(--color-blue-600)}.focus\:text-blue-700:focus{color:var(--color-blue-700)}.focus\:text-blue-800:focus{color:var(--color-blue-800)}.focus\:text-blue-900:focus{color:var(--color-blue-900)}.focus\:text-gray-50:focus{color:var(--color-gray-50)}.focus\:text-gray-100:focus{color:var(--color-gray-100)}.focus\:text-gray-200:focus{color:var(--color-gray-200)}.focus\:text-gray-300:focus{color:var(--color-gray-300)}.focus\:text-gray-400:focus{color:var(--color-gray-400)}.focus\:text-gray-500:focus{color:var(--color-gray-500)}.focus\:text-gray-600:focus{color:var(--color-gray-600)}.focus\:text-gray-700:focus{color:var(--color-gray-700)}.focus\:text-gray-800:focus{color:var(--color-gray-800)}.focus\:text-gray-900:focus{color:var(--color-gray-900)}.focus\:text-white:focus{color:var(--color-white)}.focus\:shadow:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-md:focus{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-sm:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-xl:focus{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-8:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.focus\:ring-blue-50:focus{--tw-ring-color:var(--color-blue-50)}.focus\:ring-blue-100:focus{--tw-ring-color:var(--color-blue-100)}.focus\:ring-blue-200:focus{--tw-ring-color:var(--color-blue-200)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.focus\:ring-blue-800:focus{--tw-ring-color:var(--color-blue-800)}.focus\:ring-blue-900:focus{--tw-ring-color:var(--color-blue-900)}.focus\:ring-current:focus{--tw-ring-color:currentcolor}.focus\:ring-gray-50:focus{--tw-ring-color:var(--color-gray-50)}.focus\:ring-gray-100:focus{--tw-ring-color:var(--color-gray-100)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:ring-gray-300:focus{--tw-ring-color:var(--color-gray-300)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.focus\:ring-gray-800:focus{--tw-ring-color:var(--color-gray-800)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-primary-500:focus{--tw-ring-color:var(--color-primary-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-transparent:focus{--tw-ring-color:transparent}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-1:focus{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-4:focus{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:var(--color-gray-100)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:ring-offset-white:focus{--tw-ring-offset-color:var(--color-white)}.focus\:outline:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-1:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-4:focus{outline-style:var(--tw-outline-style);outline-width:4px}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-gray-500:focus-visible{--tw-ring-color:var(--color-gray-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:scale-100:active{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.active\:bg-blue-600:active{background-color:var(--color-blue-600)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.active\:bg-blue-800:active{background-color:var(--color-blue-800)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:bg-gray-200:active{background-color:var(--color-gray-200)}.active\:bg-gray-300:active{background-color:var(--color-gray-300)}.active\:bg-gray-700:active{background-color:var(--color-gray-700)}.active\:bg-gray-800:active{background-color:var(--color-gray-800)}.active\:bg-green-600:active{background-color:var(--color-green-600)}.active\:bg-green-700:active{background-color:var(--color-green-700)}.active\:bg-red-600:active{background-color:var(--color-red-600)}.active\:bg-red-700:active{background-color:var(--color-red-700)}.active\:opacity-75:active{opacity:.75}.active\:opacity-100:active{opacity:1}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:border-gray-200:disabled{border-color:var(--color-gray-200)}.disabled\:border-gray-300:disabled{border-color:var(--color-gray-300)}.disabled\:bg-gray-100:disabled{background-color:var(--color-gray-100)}.disabled\:bg-gray-200:disabled{background-color:var(--color-gray-200)}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:text-gray-300:disabled{color:var(--color-gray-300)}.disabled\:text-gray-400:disabled{color:var(--color-gray-400)}.disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.disabled\:no-underline:disabled{text-decoration-line:none}.disabled\:opacity-20:disabled{opacity:.2}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}@media(min-width:40rem){.sm\:col-span-1{grid-column:span 1/span 1}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-12{grid-column:span 12/span 12}.sm\:col-span-full{grid-column:1/-1}.sm\:m-0{margin:calc(var(--spacing)*0)}.sm\:m-1{margin:calc(var(--spacing)*1)}.sm\:m-2{margin:calc(var(--spacing)*2)}.sm\:m-3{margin:calc(var(--spacing)*3)}.sm\:m-4{margin:calc(var(--spacing)*4)}.sm\:m-auto{margin:auto}.sm\:mx-auto{margin-inline:auto}.sm\:my-auto{margin-block:auto}.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-block{display:inline-block}.sm\:inline-flex{display:inline-flex}.sm\:w-1\/2{width:50%}.sm\:w-1\/3{width:33.3333%}.sm\:w-1\/4{width:25%}.sm\:w-2\/3{width:66.6667%}.sm\:w-3\/4{width:75%}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:min-w-48{min-width:calc(var(--spacing)*48)}.sm\:auto-cols-\[calc\(33\.333\%_-_11px\)\]{grid-auto-columns:calc(33.333% - 11px)}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-center{justify-content:center}.sm\:justify-end{justify-content:flex-end}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:calc(var(--spacing)*0)}.sm\:gap-1{gap:calc(var(--spacing)*1)}.sm\:gap-2{gap:calc(var(--spacing)*2)}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:gap-4{gap:calc(var(--spacing)*4)}.sm\:gap-5{gap:calc(var(--spacing)*5)}.sm\:gap-6{gap:calc(var(--spacing)*6)}.sm\:gap-8{gap:calc(var(--spacing)*8)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}.sm\:p-0{padding:calc(var(--spacing)*0)}.sm\:p-1{padding:calc(var(--spacing)*1)}.sm\:p-2{padding:calc(var(--spacing)*2)}.sm\:p-3{padding:calc(var(--spacing)*3)}.sm\:p-4{padding:calc(var(--spacing)*4)}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:p-8{padding:calc(var(--spacing)*8)}.sm\:px-0{padding-inline:calc(var(--spacing)*0)}.sm\:px-1{padding-inline:calc(var(--spacing)*1)}.sm\:px-2{padding-inline:calc(var(--spacing)*2)}.sm\:px-3{padding-inline:calc(var(--spacing)*3)}.sm\:px-4{padding-inline:calc(var(--spacing)*4)}.sm\:px-5{padding-inline:calc(var(--spacing)*5)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:py-0{padding-block:calc(var(--spacing)*0)}.sm\:py-1{padding-block:calc(var(--spacing)*1)}.sm\:py-2{padding-block:calc(var(--spacing)*2)}.sm\:py-3{padding-block:calc(var(--spacing)*3)}.sm\:py-4{padding-block:calc(var(--spacing)*4)}.sm\:py-5{padding-block:calc(var(--spacing)*5)}.sm\:py-6{padding-block:calc(var(--spacing)*6)}.sm\:py-8{padding-block:calc(var(--spacing)*8)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:48rem){.md\:absolute{position:absolute}.md\:fixed{position:fixed}.md\:relative{position:relative}.md\:static{position:static}.md\:sticky{position:sticky}.md\:inset-0{inset:calc(var(--spacing)*0)}.md\:top-0{top:calc(var(--spacing)*0)}.md\:right-0{right:calc(var(--spacing)*0)}.md\:bottom-0{bottom:calc(var(--spacing)*0)}.md\:left-0{left:calc(var(--spacing)*0)}.md\:order-1{order:1}.md\:order-2{order:2}.md\:order-3{order:3}.md\:order-first{order:-9999}.md\:order-last{order:9999}.md\:order-none{order:0}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-12{grid-column:span 12/span 12}.md\:col-span-full{grid-column:1/-1}.md\:row-span-1{grid-row:span 1/span 1}.md\:row-span-2{grid-row:span 2/span 2}.md\:row-span-3{grid-row:span 3/span 3}.md\:row-span-4{grid-row:span 4/span 4}.md\:row-span-full{grid-row:1/-1}.md\:m-0{margin:calc(var(--spacing)*0)}.md\:m-1{margin:calc(var(--spacing)*1)}.md\:m-2{margin:calc(var(--spacing)*2)}.md\:m-3{margin:calc(var(--spacing)*3)}.md\:m-4{margin:calc(var(--spacing)*4)}.md\:m-auto{margin:auto}.md\:mx-auto{margin-inline:auto}.md\:my-auto{margin-block:auto}.md\:mt-0{margin-top:calc(var(--spacing)*0)}.md\:mt-1{margin-top:calc(var(--spacing)*1)}.md\:mt-2{margin-top:calc(var(--spacing)*2)}.md\:mt-3{margin-top:calc(var(--spacing)*3)}.md\:mt-4{margin-top:calc(var(--spacing)*4)}.md\:mt-6{margin-top:calc(var(--spacing)*6)}.md\:mt-8{margin-top:calc(var(--spacing)*8)}.md\:mr-0{margin-right:calc(var(--spacing)*0)}.md\:mr-1{margin-right:calc(var(--spacing)*1)}.md\:mr-2{margin-right:calc(var(--spacing)*2)}.md\:mr-3{margin-right:calc(var(--spacing)*3)}.md\:mr-4{margin-right:calc(var(--spacing)*4)}.md\:mr-6{margin-right:calc(var(--spacing)*6)}.md\:mr-8{margin-right:calc(var(--spacing)*8)}.md\:mr-auto{margin-right:auto}.md\:mb-0{margin-bottom:calc(var(--spacing)*0)}.md\:mb-1{margin-bottom:calc(var(--spacing)*1)}.md\:mb-2{margin-bottom:calc(var(--spacing)*2)}.md\:mb-3{margin-bottom:calc(var(--spacing)*3)}.md\:mb-4{margin-bottom:calc(var(--spacing)*4)}.md\:mb-6{margin-bottom:calc(var(--spacing)*6)}.md\:mb-8{margin-bottom:calc(var(--spacing)*8)}.md\:ml-0{margin-left:calc(var(--spacing)*0)}.md\:ml-1{margin-left:calc(var(--spacing)*1)}.md\:ml-2{margin-left:calc(var(--spacing)*2)}.md\:ml-3{margin-left:calc(var(--spacing)*3)}.md\:ml-4{margin-left:calc(var(--spacing)*4)}.md\:ml-6{margin-left:calc(var(--spacing)*6)}.md\:ml-8{margin-left:calc(var(--spacing)*8)}.md\:ml-auto{margin-left:auto}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-block{display:inline-block}.md\:inline-flex{display:inline-flex}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.3333%}.md\:w-1\/4{width:25%}.md\:w-1\/5{width:20%}.md\:w-2\/3{width:66.6667%}.md\:w-2\/5{width:40%}.md\:w-3\/4{width:75%}.md\:w-3\/5{width:60%}.md\:w-4\/5{width:80%}.md\:w-auto{width:auto}.md\:w-full{width:100%}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-4xl{max-width:var(--container-4xl)}.md\:max-w-5xl{max-width:var(--container-5xl)}.md\:max-w-6xl{max-width:var(--container-6xl)}.md\:max-w-7xl{max-width:var(--container-7xl)}.md\:max-w-full{max-width:100%}.md\:max-w-lg{max-width:var(--container-lg)}.md\:max-w-md{max-width:var(--container-md)}.md\:max-w-none{max-width:none}.md\:max-w-sm{max-width:var(--container-sm)}.md\:max-w-xl{max-width:var(--container-xl)}.md\:max-w-xs{max-width:var(--container-xs)}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.md\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.md\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.md\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:flex-row{flex-direction:row}.md\:items-baseline{align-items:baseline}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:items-stretch{align-items:stretch}.md\:justify-around{justify-content:space-around}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}.md\:justify-end{justify-content:flex-end}.md\:justify-start{justify-content:flex-start}.md\:gap-0{gap:calc(var(--spacing)*0)}.md\:gap-1{gap:calc(var(--spacing)*1)}.md\:gap-2{gap:calc(var(--spacing)*2)}.md\:gap-3{gap:calc(var(--spacing)*3)}.md\:gap-4{gap:calc(var(--spacing)*4)}.md\:gap-5{gap:calc(var(--spacing)*5)}.md\:gap-6{gap:calc(var(--spacing)*6)}.md\:gap-8{gap:calc(var(--spacing)*8)}.md\:gap-10{gap:calc(var(--spacing)*10)}.md\:gap-12{gap:calc(var(--spacing)*12)}:where(.md\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.md\:p-0{padding:calc(var(--spacing)*0)}.md\:p-1{padding:calc(var(--spacing)*1)}.md\:p-2{padding:calc(var(--spacing)*2)}.md\:p-3{padding:calc(var(--spacing)*3)}.md\:p-4{padding:calc(var(--spacing)*4)}.md\:p-5{padding:calc(var(--spacing)*5)}.md\:p-6{padding:calc(var(--spacing)*6)}.md\:p-8{padding:calc(var(--spacing)*8)}.md\:p-10{padding:calc(var(--spacing)*10)}.md\:p-12{padding:calc(var(--spacing)*12)}.md\:px-0{padding-inline:calc(var(--spacing)*0)}.md\:px-1{padding-inline:calc(var(--spacing)*1)}.md\:px-2{padding-inline:calc(var(--spacing)*2)}.md\:px-3{padding-inline:calc(var(--spacing)*3)}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:px-5{padding-inline:calc(var(--spacing)*5)}.md\:px-6{padding-inline:calc(var(--spacing)*6)}.md\:px-8{padding-inline:calc(var(--spacing)*8)}.md\:px-10{padding-inline:calc(var(--spacing)*10)}.md\:px-12{padding-inline:calc(var(--spacing)*12)}.md\:py-0{padding-block:calc(var(--spacing)*0)}.md\:py-1{padding-block:calc(var(--spacing)*1)}.md\:py-2{padding-block:calc(var(--spacing)*2)}.md\:py-3{padding-block:calc(var(--spacing)*3)}.md\:py-4{padding-block:calc(var(--spacing)*4)}.md\:py-5{padding-block:calc(var(--spacing)*5)}.md\:py-6{padding-block:calc(var(--spacing)*6)}.md\:py-8{padding-block:calc(var(--spacing)*8)}.md\:py-10{padding-block:calc(var(--spacing)*10)}.md\:py-12{padding-block:calc(var(--spacing)*12)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:64rem){.lg\:absolute{position:absolute}.lg\:fixed{position:fixed}.lg\:relative{position:relative}.lg\:static{position:static}.lg\:sticky{position:sticky}.lg\:inset-0{inset:calc(var(--spacing)*0)}.lg\:top-0{top:calc(var(--spacing)*0)}.lg\:right-0{right:calc(var(--spacing)*0)}.lg\:bottom-0{bottom:calc(var(--spacing)*0)}.lg\:left-0{left:calc(var(--spacing)*0)}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-3{order:3}.lg\:order-4{order:4}.lg\:order-first{order:-9999}.lg\:order-last{order:9999}.lg\:order-none{order:0}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:col-span-full{grid-column:1/-1}.lg\:row-span-1{grid-row:span 1/span 1}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-span-3{grid-row:span 3/span 3}.lg\:row-span-4{grid-row:span 4/span 4}.lg\:row-span-full{grid-row:1/-1}.lg\:m-0{margin:calc(var(--spacing)*0)}.lg\:m-1{margin:calc(var(--spacing)*1)}.lg\:m-2{margin:calc(var(--spacing)*2)}.lg\:m-3{margin:calc(var(--spacing)*3)}.lg\:m-4{margin:calc(var(--spacing)*4)}.lg\:m-auto{margin:auto}.lg\:mx-auto{margin-inline:auto}.lg\:my-auto{margin-block:auto}.lg\:mt-0{margin-top:calc(var(--spacing)*0)}.lg\:mt-1{margin-top:calc(var(--spacing)*1)}.lg\:mt-2{margin-top:calc(var(--spacing)*2)}.lg\:mt-3{margin-top:calc(var(--spacing)*3)}.lg\:mt-4{margin-top:calc(var(--spacing)*4)}.lg\:mt-6{margin-top:calc(var(--spacing)*6)}.lg\:mt-8{margin-top:calc(var(--spacing)*8)}.lg\:mt-10{margin-top:calc(var(--spacing)*10)}.lg\:mt-12{margin-top:calc(var(--spacing)*12)}.lg\:mr-0{margin-right:calc(var(--spacing)*0)}.lg\:mr-1{margin-right:calc(var(--spacing)*1)}.lg\:mr-2{margin-right:calc(var(--spacing)*2)}.lg\:mr-3{margin-right:calc(var(--spacing)*3)}.lg\:mr-4{margin-right:calc(var(--spacing)*4)}.lg\:mr-6{margin-right:calc(var(--spacing)*6)}.lg\:mr-8{margin-right:calc(var(--spacing)*8)}.lg\:mr-auto{margin-right:auto}.lg\:mb-0{margin-bottom:calc(var(--spacing)*0)}.lg\:mb-1{margin-bottom:calc(var(--spacing)*1)}.lg\:mb-2{margin-bottom:calc(var(--spacing)*2)}.lg\:mb-3{margin-bottom:calc(var(--spacing)*3)}.lg\:mb-4{margin-bottom:calc(var(--spacing)*4)}.lg\:mb-6{margin-bottom:calc(var(--spacing)*6)}.lg\:mb-8{margin-bottom:calc(var(--spacing)*8)}.lg\:mb-10{margin-bottom:calc(var(--spacing)*10)}.lg\:mb-12{margin-bottom:calc(var(--spacing)*12)}.lg\:ml-0{margin-left:calc(var(--spacing)*0)}.lg\:ml-1{margin-left:calc(var(--spacing)*1)}.lg\:ml-2{margin-left:calc(var(--spacing)*2)}.lg\:ml-3{margin-left:calc(var(--spacing)*3)}.lg\:ml-4{margin-left:calc(var(--spacing)*4)}.lg\:ml-6{margin-left:calc(var(--spacing)*6)}.lg\:ml-8{margin-left:calc(var(--spacing)*8)}.lg\:ml-auto{margin-left:auto}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:inline{display:inline}.lg\:inline-block{display:inline-block}.lg\:inline-flex{display:inline-flex}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.3333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:w-1\/6{width:16.6667%}.lg\:w-2\/3{width:66.6667%}.lg\:w-2\/5{width:40%}.lg\:w-3\/4{width:75%}.lg\:w-3\/5{width:60%}.lg\:w-4\/5{width:80%}.lg\:w-5\/6{width:83.3333%}.lg\:w-auto{width:auto}.lg\:w-full{width:100%}.lg\:max-w-2xl{max-width:var(--container-2xl)}.lg\:max-w-3xl{max-width:var(--container-3xl)}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-5xl{max-width:var(--container-5xl)}.lg\:max-w-6xl{max-width:var(--container-6xl)}.lg\:max-w-7xl{max-width:var(--container-7xl)}.lg\:max-w-full{max-width:100%}.lg\:max-w-lg{max-width:var(--container-lg)}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-none{max-width:none}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:max-w-xl{max-width:var(--container-xl)}.lg\:max-w-xs{max-width:var(--container-xs)}.lg\:auto-cols-\[calc\(25\%_-_12px\)\]{grid-auto-columns:calc(25% - 12px)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.lg\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.lg\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.lg\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.lg\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.lg\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-baseline{align-items:baseline}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:items-stretch{align-items:stretch}.lg\:justify-around{justify-content:space-around}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-evenly{justify-content:space-evenly}.lg\:justify-start{justify-content:flex-start}.lg\:gap-0{gap:calc(var(--spacing)*0)}.lg\:gap-1{gap:calc(var(--spacing)*1)}.lg\:gap-2{gap:calc(var(--spacing)*2)}.lg\:gap-3{gap:calc(var(--spacing)*3)}.lg\:gap-4{gap:calc(var(--spacing)*4)}.lg\:gap-5{gap:calc(var(--spacing)*5)}.lg\:gap-6{gap:calc(var(--spacing)*6)}.lg\:gap-8{gap:calc(var(--spacing)*8)}.lg\:gap-10{gap:calc(var(--spacing)*10)}.lg\:gap-12{gap:calc(var(--spacing)*12)}.lg\:gap-16{gap:calc(var(--spacing)*16)}:where(.lg\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*10)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*10)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-x-reverse)))}.lg\:p-0{padding:calc(var(--spacing)*0)}.lg\:p-1{padding:calc(var(--spacing)*1)}.lg\:p-2{padding:calc(var(--spacing)*2)}.lg\:p-3{padding:calc(var(--spacing)*3)}.lg\:p-4{padding:calc(var(--spacing)*4)}.lg\:p-5{padding:calc(var(--spacing)*5)}.lg\:p-6{padding:calc(var(--spacing)*6)}.lg\:p-8{padding:calc(var(--spacing)*8)}.lg\:p-10{padding:calc(var(--spacing)*10)}.lg\:p-12{padding:calc(var(--spacing)*12)}.lg\:p-16{padding:calc(var(--spacing)*16)}.lg\:px-0{padding-inline:calc(var(--spacing)*0)}.lg\:px-1{padding-inline:calc(var(--spacing)*1)}.lg\:px-2{padding-inline:calc(var(--spacing)*2)}.lg\:px-3{padding-inline:calc(var(--spacing)*3)}.lg\:px-4{padding-inline:calc(var(--spacing)*4)}.lg\:px-5{padding-inline:calc(var(--spacing)*5)}.lg\:px-6{padding-inline:calc(var(--spacing)*6)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:px-10{padding-inline:calc(var(--spacing)*10)}.lg\:px-12{padding-inline:calc(var(--spacing)*12)}.lg\:px-16{padding-inline:calc(var(--spacing)*16)}.lg\:py-0{padding-block:calc(var(--spacing)*0)}.lg\:py-1{padding-block:calc(var(--spacing)*1)}.lg\:py-2{padding-block:calc(var(--spacing)*2)}.lg\:py-3{padding-block:calc(var(--spacing)*3)}.lg\:py-4{padding-block:calc(var(--spacing)*4)}.lg\:py-5{padding-block:calc(var(--spacing)*5)}.lg\:py-6{padding-block:calc(var(--spacing)*6)}.lg\:py-8{padding-block:calc(var(--spacing)*8)}.lg\:py-10{padding-block:calc(var(--spacing)*10)}.lg\:py-12{padding-block:calc(var(--spacing)*12)}.lg\:py-16{padding-block:calc(var(--spacing)*16)}.lg\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.lg\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.lg\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.lg\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.lg\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-3{grid-column:span 3/span 3}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-5{grid-column:span 5/span 5}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-full{grid-column:1/-1}.xl\:row-span-1{grid-row:span 1/span 1}.xl\:row-span-2{grid-row:span 2/span 2}.xl\:row-span-3{grid-row:span 3/span 3}.xl\:row-span-full{grid-row:1/-1}.xl\:m-0{margin:calc(var(--spacing)*0)}.xl\:m-auto{margin:auto}.xl\:mx-auto{margin-inline:auto}.xl\:my-auto{margin-block:auto}.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:inline{display:inline}.xl\:inline-block{display:inline-block}.xl\:inline-flex{display:inline-flex}.xl\:w-1\/2{width:50%}.xl\:w-1\/3{width:33.3333%}.xl\:w-1\/4{width:25%}.xl\:w-1\/5{width:20%}.xl\:w-2\/3{width:66.6667%}.xl\:w-2\/5{width:40%}.xl\:w-3\/4{width:75%}.xl\:w-3\/5{width:60%}.xl\:w-4\/5{width:80%}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-2xl{max-width:var(--container-2xl)}.xl\:max-w-3xl{max-width:var(--container-3xl)}.xl\:max-w-4xl{max-width:var(--container-4xl)}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:max-w-6xl{max-width:var(--container-6xl)}.xl\:max-w-7xl{max-width:var(--container-7xl)}.xl\:max-w-full{max-width:100%}.xl\:max-w-lg{max-width:var(--container-lg)}.xl\:max-w-md{max-width:var(--container-md)}.xl\:max-w-none{max-width:none}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:max-w-xl{max-width:var(--container-xl)}.xl\:max-w-xs{max-width:var(--container-xs)}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.xl\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.xl\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.xl\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.xl\:flex-col{flex-direction:column}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-center{justify-content:center}.xl\:justify-end{justify-content:flex-end}.xl\:justify-start{justify-content:flex-start}.xl\:gap-0{gap:calc(var(--spacing)*0)}.xl\:gap-1{gap:calc(var(--spacing)*1)}.xl\:gap-2{gap:calc(var(--spacing)*2)}.xl\:gap-3{gap:calc(var(--spacing)*3)}.xl\:gap-4{gap:calc(var(--spacing)*4)}.xl\:gap-5{gap:calc(var(--spacing)*5)}.xl\:gap-6{gap:calc(var(--spacing)*6)}.xl\:gap-8{gap:calc(var(--spacing)*8)}.xl\:gap-10{gap:calc(var(--spacing)*10)}.xl\:gap-12{gap:calc(var(--spacing)*12)}.xl\:gap-16{gap:calc(var(--spacing)*16)}:where(.xl\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.xl\:p-0{padding:calc(var(--spacing)*0)}.xl\:p-1{padding:calc(var(--spacing)*1)}.xl\:p-2{padding:calc(var(--spacing)*2)}.xl\:p-3{padding:calc(var(--spacing)*3)}.xl\:p-4{padding:calc(var(--spacing)*4)}.xl\:p-5{padding:calc(var(--spacing)*5)}.xl\:p-6{padding:calc(var(--spacing)*6)}.xl\:p-8{padding:calc(var(--spacing)*8)}.xl\:p-10{padding:calc(var(--spacing)*10)}.xl\:p-12{padding:calc(var(--spacing)*12)}.xl\:p-16{padding:calc(var(--spacing)*16)}.xl\:px-0{padding-inline:calc(var(--spacing)*0)}.xl\:px-1{padding-inline:calc(var(--spacing)*1)}.xl\:px-2{padding-inline:calc(var(--spacing)*2)}.xl\:px-3{padding-inline:calc(var(--spacing)*3)}.xl\:px-4{padding-inline:calc(var(--spacing)*4)}.xl\:px-5{padding-inline:calc(var(--spacing)*5)}.xl\:px-6{padding-inline:calc(var(--spacing)*6)}.xl\:px-8{padding-inline:calc(var(--spacing)*8)}.xl\:px-10{padding-inline:calc(var(--spacing)*10)}.xl\:px-12{padding-inline:calc(var(--spacing)*12)}.xl\:px-16{padding-inline:calc(var(--spacing)*16)}.xl\:py-0{padding-block:calc(var(--spacing)*0)}.xl\:py-1{padding-block:calc(var(--spacing)*1)}.xl\:py-2{padding-block:calc(var(--spacing)*2)}.xl\:py-3{padding-block:calc(var(--spacing)*3)}.xl\:py-4{padding-block:calc(var(--spacing)*4)}.xl\:py-5{padding-block:calc(var(--spacing)*5)}.xl\:py-6{padding-block:calc(var(--spacing)*6)}.xl\:py-8{padding-block:calc(var(--spacing)*8)}.xl\:py-10{padding-block:calc(var(--spacing)*10)}.xl\:py-12{padding-block:calc(var(--spacing)*12)}.xl\:py-16{padding-block:calc(var(--spacing)*16)}.xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.xl\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:96rem){.\32xl\:col-span-1{grid-column:span 1/span 1}.\32xl\:col-span-2{grid-column:span 2/span 2}.\32xl\:col-span-3{grid-column:span 3/span 3}.\32xl\:col-span-4{grid-column:span 4/span 4}.\32xl\:col-span-6{grid-column:span 6/span 6}.\32xl\:col-span-12{grid-column:span 12/span 12}.\32xl\:block{display:block}.\32xl\:flex{display:flex}.\32xl\:hidden{display:none}.\32xl\:w-1\/2{width:50%}.\32xl\:w-1\/3{width:33.3333%}.\32xl\:w-1\/4{width:25%}.\32xl\:w-2\/3{width:66.6667%}.\32xl\:w-3\/4{width:75%}.\32xl\:w-auto{width:auto}.\32xl\:w-full{width:100%}.\32xl\:max-w-2xl{max-width:var(--container-2xl)}.\32xl\:max-w-3xl{max-width:var(--container-3xl)}.\32xl\:max-w-4xl{max-width:var(--container-4xl)}.\32xl\:max-w-5xl{max-width:var(--container-5xl)}.\32xl\:max-w-6xl{max-width:var(--container-6xl)}.\32xl\:max-w-7xl{max-width:var(--container-7xl)}.\32xl\:max-w-full{max-width:100%}.\32xl\:max-w-lg{max-width:var(--container-lg)}.\32xl\:max-w-md{max-width:var(--container-md)}.\32xl\:max-w-sm{max-width:var(--container-sm)}.\32xl\:max-w-xl{max-width:var(--container-xl)}.\32xl\:max-w-xs{max-width:var(--container-xs)}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:gap-0{gap:calc(var(--spacing)*0)}.\32xl\:gap-4{gap:calc(var(--spacing)*4)}.\32xl\:gap-6{gap:calc(var(--spacing)*6)}.\32xl\:gap-8{gap:calc(var(--spacing)*8)}.\32xl\:gap-12{gap:calc(var(--spacing)*12)}.\32xl\:gap-16{gap:calc(var(--spacing)*16)}.\32xl\:p-0{padding:calc(var(--spacing)*0)}.\32xl\:p-4{padding:calc(var(--spacing)*4)}.\32xl\:p-6{padding:calc(var(--spacing)*6)}.\32xl\:p-8{padding:calc(var(--spacing)*8)}.\32xl\:p-12{padding:calc(var(--spacing)*12)}.\32xl\:p-16{padding:calc(var(--spacing)*16)}.\32xl\:px-0{padding-inline:calc(var(--spacing)*0)}.\32xl\:px-4{padding-inline:calc(var(--spacing)*4)}.\32xl\:px-6{padding-inline:calc(var(--spacing)*6)}.\32xl\:px-8{padding-inline:calc(var(--spacing)*8)}.\32xl\:px-12{padding-inline:calc(var(--spacing)*12)}.\32xl\:px-16{padding-inline:calc(var(--spacing)*16)}.\32xl\:py-0{padding-block:calc(var(--spacing)*0)}.\32xl\:py-4{padding-block:calc(var(--spacing)*4)}.\32xl\:py-6{padding-block:calc(var(--spacing)*6)}.\32xl\:py-8{padding-block:calc(var(--spacing)*8)}.\32xl\:py-12{padding-block:calc(var(--spacing)*12)}.\32xl\:py-16{padding-block:calc(var(--spacing)*16)}.\32xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.\32xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.\32xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.\32xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.\32xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.\32xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.\32xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\32xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.\32xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.dark .dark\:block{display:block}.dark .dark\:flex{display:flex}.dark .dark\:flex-1{flex:1}.dark .dark\:flex-auto{flex:auto}.dark .dark\:flex-none{flex:none}.dark .dark\:flex-col{flex-direction:column}.dark .dark\:flex-row{flex-direction:row}.dark .dark\:flex-wrap{flex-wrap:wrap}.dark .dark\:items-center{align-items:center}.dark .dark\:items-end{align-items:flex-end}.dark .dark\:items-start{align-items:flex-start}.dark .dark\:items-stretch{align-items:stretch}.dark .dark\:justify-around{justify-content:space-around}.dark .dark\:justify-between{justify-content:space-between}.dark .dark\:justify-center{justify-content:center}.dark .dark\:justify-end{justify-content:flex-end}.dark .dark\:justify-start{justify-content:flex-start}:where(.dark .dark\:divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.dark .dark\:divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.dark .dark\:divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.dark .dark\:divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.dark .dark\:divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.dark .dark\:divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.dark .dark\:divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.dark .dark\:divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.dark .dark\:divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.dark .dark\:divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}.dark .dark\:self-center{align-self:center}.dark .dark\:self-end{align-self:flex-end}.dark .dark\:self-start{align-self:flex-start}.dark .dark\:self-stretch{align-self:stretch}.dark .dark\:overflow-auto{overflow:auto}.dark .dark\:overflow-hidden{overflow:hidden}.dark .dark\:overflow-visible{overflow:visible}.dark .dark\:overflow-x-auto{overflow-x:auto}.dark .dark\:overflow-y-auto{overflow-y:auto}.dark .dark\:rounded{border-radius:.25rem}.dark .dark\:rounded-full{border-radius:3.40282e38px}.dark .dark\:rounded-lg{border-radius:var(--radius-lg)}.dark .dark\:rounded-none{border-radius:0}.dark .dark\:rounded-sm{border-radius:var(--radius-sm)}.dark .dark\:rounded-xl{border-radius:var(--radius-xl)}.dark .dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark .dark\:border-0{border-style:var(--tw-border-style);border-width:0}.dark .dark\:border-dashed{--tw-border-style:dashed;border-style:dashed}.dark .dark\:border-dotted{--tw-border-style:dotted;border-style:dotted}.dark .dark\:border-solid{--tw-border-style:solid;border-style:solid}.dark .dark\:border-black{border-color:var(--color-black)}.dark .dark\:border-blue-50{border-color:var(--color-blue-50)}.dark .dark\:border-blue-100{border-color:var(--color-blue-100)}.dark .dark\:border-blue-200{border-color:var(--color-blue-200)}.dark .dark\:border-blue-300{border-color:var(--color-blue-300)}.dark .dark\:border-blue-400{border-color:var(--color-blue-400)}.dark .dark\:border-blue-500{border-color:var(--color-blue-500)}.dark .dark\:border-blue-600{border-color:var(--color-blue-600)}.dark .dark\:border-blue-700{border-color:var(--color-blue-700)}.dark .dark\:border-blue-800{border-color:var(--color-blue-800)}.dark .dark\:border-blue-900{border-color:var(--color-blue-900)}.dark .dark\:border-gray-50{border-color:var(--color-gray-50)}.dark .dark\:border-gray-100{border-color:var(--color-gray-100)}.dark .dark\:border-gray-200{border-color:var(--color-gray-200)}.dark .dark\:border-gray-300{border-color:var(--color-gray-300)}.dark .dark\:border-gray-400{border-color:var(--color-gray-400)}.dark .dark\:border-gray-500{border-color:var(--color-gray-500)}.dark .dark\:border-gray-600{border-color:var(--color-gray-600)}.dark .dark\:border-gray-700{border-color:var(--color-gray-700)}.dark .dark\:border-gray-700\/50{border-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-gray-700\/50{border-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:border-gray-800{border-color:var(--color-gray-800)}.dark .dark\:border-gray-900{border-color:var(--color-gray-900)}.dark .dark\:border-gray-900\/30{border-color:#1018284d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-gray-900\/30{border-color:color-mix(in oklab,var(--color-gray-900)30%,transparent)}}.dark .dark\:border-gray-950{border-color:var(--color-gray-950)}.dark .dark\:border-green-600{border-color:var(--color-green-600)}.dark .dark\:border-green-700{border-color:var(--color-green-700)}.dark .dark\:border-green-800{border-color:var(--color-green-800)}.dark .dark\:border-orange-600{border-color:var(--color-orange-600)}.dark .dark\:border-orange-800{border-color:var(--color-orange-800)}.dark .dark\:border-red-50{border-color:var(--color-red-50)}.dark .dark\:border-red-100{border-color:var(--color-red-100)}.dark .dark\:border-red-200{border-color:var(--color-red-200)}.dark .dark\:border-red-300{border-color:var(--color-red-300)}.dark .dark\:border-red-400{border-color:var(--color-red-400)}.dark .dark\:border-red-500{border-color:var(--color-red-500)}.dark .dark\:border-red-600{border-color:var(--color-red-600)}.dark .dark\:border-red-700{border-color:var(--color-red-700)}.dark .dark\:border-red-800{border-color:var(--color-red-800)}.dark .dark\:border-red-900{border-color:var(--color-red-900)}.dark .dark\:border-red-900\/50{border-color:#82181a80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-red-900\/50{border-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark .dark\:border-slate-50{border-color:var(--color-slate-50)}.dark .dark\:border-slate-100{border-color:var(--color-slate-100)}.dark .dark\:border-slate-200{border-color:var(--color-slate-200)}.dark .dark\:border-slate-300{border-color:var(--color-slate-300)}.dark .dark\:border-slate-400{border-color:var(--color-slate-400)}.dark .dark\:border-slate-500{border-color:var(--color-slate-500)}.dark .dark\:border-slate-600{border-color:var(--color-slate-600)}.dark .dark\:border-slate-700{border-color:var(--color-slate-700)}.dark .dark\:border-slate-800{border-color:var(--color-slate-800)}.dark .dark\:border-slate-900{border-color:var(--color-slate-900)}.dark .dark\:border-slate-950{border-color:var(--color-slate-950)}.dark .dark\:border-transparent{border-color:#0000}.dark .dark\:border-white{border-color:var(--color-white)}.dark .dark\:border-yellow-800{border-color:var(--color-yellow-800)}.dark .dark\:border-t-gray-900{border-top-color:var(--color-gray-900)}.dark .dark\:border-t-white{border-top-color:var(--color-white)}.dark .dark\:bg-amber-900\/30{background-color:#7b33064d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/30{background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark .dark\:bg-black{background-color:var(--color-black)}.dark .dark\:bg-blue-50{background-color:var(--color-blue-50)}.dark .dark\:bg-blue-100{background-color:var(--color-blue-100)}.dark .dark\:bg-blue-200{background-color:var(--color-blue-200)}.dark .dark\:bg-blue-300{background-color:var(--color-blue-300)}.dark .dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark .dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark .dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark .dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark .dark\:bg-blue-800{background-color:var(--color-blue-800)}.dark .dark\:bg-blue-900{background-color:var(--color-blue-900)}.dark .dark\:bg-blue-900\/10{background-color:#1c398e1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/10{background-color:color-mix(in oklab,var(--color-blue-900)10%,transparent)}}.dark .dark\:bg-blue-900\/20{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/20{background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark .dark\:bg-blue-900\/30{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/30{background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark .dark\:bg-blue-900\/40{background-color:#1c398e66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/40{background-color:color-mix(in oklab,var(--color-blue-900)40%,transparent)}}.dark .dark\:bg-blue-900\/50{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/50{background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark .dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark .dark\:bg-blue-950\/50{background-color:#16245680}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-950\/50{background-color:color-mix(in oklab,var(--color-blue-950)50%,transparent)}}.dark .dark\:bg-emerald-900{background-color:var(--color-emerald-900)}.dark .dark\:bg-gray-50{background-color:var(--color-gray-50)}.dark .dark\:bg-gray-100{background-color:var(--color-gray-100)}.dark .dark\:bg-gray-200{background-color:var(--color-gray-200)}.dark .dark\:bg-gray-300{background-color:var(--color-gray-300)}.dark .dark\:bg-gray-400{background-color:var(--color-gray-400)}.dark .dark\:bg-gray-500{background-color:var(--color-gray-500)}.dark .dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark .dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark .dark\:bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/50{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark .dark\:bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.dark .dark\:bg-gray-800\/80{background-color:#1e2939cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/80{background-color:color-mix(in oklab,var(--color-gray-800)80%,transparent)}}.dark .dark\:bg-gray-800\/90{background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/90{background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)}}.dark .dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark .dark\:bg-gray-900\/30{background-color:#1018284d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/30{background-color:color-mix(in oklab,var(--color-gray-900)30%,transparent)}}.dark .dark\:bg-gray-900\/40{background-color:#10182866}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/40{background-color:color-mix(in oklab,var(--color-gray-900)40%,transparent)}}.dark .dark\:bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.dark .dark\:bg-gray-950{background-color:var(--color-gray-950)}.dark .dark\:bg-green-50{background-color:var(--color-green-50)}.dark .dark\:bg-green-100{background-color:var(--color-green-100)}.dark .dark\:bg-green-200{background-color:var(--color-green-200)}.dark .dark\:bg-green-300{background-color:var(--color-green-300)}.dark .dark\:bg-green-400{background-color:var(--color-green-400)}.dark .dark\:bg-green-500{background-color:var(--color-green-500)}.dark .dark\:bg-green-600{background-color:var(--color-green-600)}.dark .dark\:bg-green-700{background-color:var(--color-green-700)}.dark .dark\:bg-green-800{background-color:var(--color-green-800)}.dark .dark\:bg-green-900{background-color:var(--color-green-900)}.dark .dark\:bg-green-900\/20{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/20{background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark .dark\:bg-green-900\/30{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/30{background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark .dark\:bg-green-900\/50{background-color:#0d542b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/50{background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark .dark\:bg-green-950{background-color:var(--color-green-950)}.dark .dark\:bg-indigo-50{background-color:var(--color-indigo-50)}.dark .dark\:bg-indigo-100{background-color:var(--color-indigo-100)}.dark .dark\:bg-indigo-200{background-color:var(--color-indigo-200)}.dark .dark\:bg-indigo-300{background-color:var(--color-indigo-300)}.dark .dark\:bg-indigo-400{background-color:var(--color-indigo-400)}.dark .dark\:bg-indigo-500{background-color:var(--color-indigo-500)}.dark .dark\:bg-indigo-600{background-color:var(--color-indigo-600)}.dark .dark\:bg-indigo-700{background-color:var(--color-indigo-700)}.dark .dark\:bg-indigo-800{background-color:var(--color-indigo-800)}.dark .dark\:bg-indigo-900{background-color:var(--color-indigo-900)}.dark .dark\:bg-indigo-900\/30{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-indigo-900\/30{background-color:color-mix(in oklab,var(--color-indigo-900)30%,transparent)}}.dark .dark\:bg-indigo-950{background-color:var(--color-indigo-950)}.dark .dark\:bg-orange-50{background-color:var(--color-orange-50)}.dark .dark\:bg-orange-100{background-color:var(--color-orange-100)}.dark .dark\:bg-orange-200{background-color:var(--color-orange-200)}.dark .dark\:bg-orange-300{background-color:var(--color-orange-300)}.dark .dark\:bg-orange-400{background-color:var(--color-orange-400)}.dark .dark\:bg-orange-500{background-color:var(--color-orange-500)}.dark .dark\:bg-orange-600{background-color:var(--color-orange-600)}.dark .dark\:bg-orange-700{background-color:var(--color-orange-700)}.dark .dark\:bg-orange-800{background-color:var(--color-orange-800)}.dark .dark\:bg-orange-900{background-color:var(--color-orange-900)}.dark .dark\:bg-orange-900\/20{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/20{background-color:color-mix(in oklab,var(--color-orange-900)20%,transparent)}}.dark .dark\:bg-orange-900\/30{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/30{background-color:color-mix(in oklab,var(--color-orange-900)30%,transparent)}}.dark .dark\:bg-orange-900\/50{background-color:#7e2a0c80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/50{background-color:color-mix(in oklab,var(--color-orange-900)50%,transparent)}}.dark .dark\:bg-orange-950{background-color:var(--color-orange-950)}.dark .dark\:bg-pink-50{background-color:var(--color-pink-50)}.dark .dark\:bg-pink-100{background-color:var(--color-pink-100)}.dark .dark\:bg-pink-200{background-color:var(--color-pink-200)}.dark .dark\:bg-pink-300{background-color:var(--color-pink-300)}.dark .dark\:bg-pink-400{background-color:var(--color-pink-400)}.dark .dark\:bg-pink-500{background-color:var(--color-pink-500)}.dark .dark\:bg-pink-600{background-color:var(--color-pink-600)}.dark .dark\:bg-pink-700{background-color:var(--color-pink-700)}.dark .dark\:bg-pink-800{background-color:var(--color-pink-800)}.dark .dark\:bg-pink-900{background-color:var(--color-pink-900)}.dark .dark\:bg-pink-950{background-color:var(--color-pink-950)}.dark .dark\:bg-primary-900\/30{background-color:#1e3a8a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-primary-900\/30{background-color:color-mix(in oklab,var(--color-primary-900)30%,transparent)}}.dark .dark\:bg-purple-50{background-color:var(--color-purple-50)}.dark .dark\:bg-purple-100{background-color:var(--color-purple-100)}.dark .dark\:bg-purple-200{background-color:var(--color-purple-200)}.dark .dark\:bg-purple-300{background-color:var(--color-purple-300)}.dark .dark\:bg-purple-400{background-color:var(--color-purple-400)}.dark .dark\:bg-purple-500{background-color:var(--color-purple-500)}.dark .dark\:bg-purple-500\/80{background-color:#ac4bffcc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-500\/80{background-color:color-mix(in oklab,var(--color-purple-500)80%,transparent)}}.dark .dark\:bg-purple-600{background-color:var(--color-purple-600)}.dark .dark\:bg-purple-700{background-color:var(--color-purple-700)}.dark .dark\:bg-purple-800{background-color:var(--color-purple-800)}.dark .dark\:bg-purple-900{background-color:var(--color-purple-900)}.dark .dark\:bg-purple-900\/30{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/30{background-color:color-mix(in oklab,var(--color-purple-900)30%,transparent)}}.dark .dark\:bg-purple-900\/50{background-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/50{background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark .dark\:bg-purple-950{background-color:var(--color-purple-950)}.dark .dark\:bg-red-50{background-color:var(--color-red-50)}.dark .dark\:bg-red-100{background-color:var(--color-red-100)}.dark .dark\:bg-red-200{background-color:var(--color-red-200)}.dark .dark\:bg-red-300{background-color:var(--color-red-300)}.dark .dark\:bg-red-400{background-color:var(--color-red-400)}.dark .dark\:bg-red-500{background-color:var(--color-red-500)}.dark .dark\:bg-red-600{background-color:var(--color-red-600)}.dark .dark\:bg-red-700{background-color:var(--color-red-700)}.dark .dark\:bg-red-800{background-color:var(--color-red-800)}.dark .dark\:bg-red-900{background-color:var(--color-red-900)}.dark .dark\:bg-red-900\/10{background-color:#82181a1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/10{background-color:color-mix(in oklab,var(--color-red-900)10%,transparent)}}.dark .dark\:bg-red-900\/20{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/20{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark .dark\:bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark .dark\:bg-red-950{background-color:var(--color-red-950)}.dark .dark\:bg-red-950\/50{background-color:#46080980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-950\/50{background-color:color-mix(in oklab,var(--color-red-950)50%,transparent)}}.dark .dark\:bg-red-950\/80{background-color:#460809cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-950\/80{background-color:color-mix(in oklab,var(--color-red-950)80%,transparent)}}.dark .dark\:bg-slate-50{background-color:var(--color-slate-50)}.dark .dark\:bg-slate-100{background-color:var(--color-slate-100)}.dark .dark\:bg-slate-200{background-color:var(--color-slate-200)}.dark .dark\:bg-slate-300{background-color:var(--color-slate-300)}.dark .dark\:bg-slate-400{background-color:var(--color-slate-400)}.dark .dark\:bg-slate-500{background-color:var(--color-slate-500)}.dark .dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark .dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark .dark\:bg-slate-800{background-color:var(--color-slate-800)}.dark .dark\:bg-slate-900{background-color:var(--color-slate-900)}.dark .dark\:bg-slate-950{background-color:var(--color-slate-950)}.dark .dark\:bg-white{background-color:var(--color-white)}.dark .dark\:bg-yellow-50{background-color:var(--color-yellow-50)}.dark .dark\:bg-yellow-100{background-color:var(--color-yellow-100)}.dark .dark\:bg-yellow-200{background-color:var(--color-yellow-200)}.dark .dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark .dark\:bg-yellow-400{background-color:var(--color-yellow-400)}.dark .dark\:bg-yellow-500{background-color:var(--color-yellow-500)}.dark .dark\:bg-yellow-600{background-color:var(--color-yellow-600)}.dark .dark\:bg-yellow-700{background-color:var(--color-yellow-700)}.dark .dark\:bg-yellow-800{background-color:var(--color-yellow-800)}.dark .dark\:bg-yellow-900{background-color:var(--color-yellow-900)}.dark .dark\:bg-yellow-900\/20{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/20{background-color:color-mix(in oklab,var(--color-yellow-900)20%,transparent)}}.dark .dark\:bg-yellow-900\/30{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/30{background-color:color-mix(in oklab,var(--color-yellow-900)30%,transparent)}}.dark .dark\:bg-yellow-950{background-color:var(--color-yellow-950)}.dark .dark\:from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:text-center{text-align:center}.dark .dark\:text-justify{text-align:justify}.dark .dark\:text-left{text-align:left}.dark .dark\:text-right{text-align:right}.dark .dark\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.dark .dark\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.dark .dark\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.dark .dark\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.dark .dark\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.dark .dark\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.dark .dark\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.dark .dark\:leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.dark .dark\:leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.dark .dark\:leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.dark .dark\:font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dark .dark\:font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.dark .dark\:font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dark .dark\:tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.dark .dark\:tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.dark .dark\:tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.dark .dark\:whitespace-normal{white-space:normal}.dark .dark\:whitespace-nowrap{white-space:nowrap}.dark .dark\:whitespace-pre-wrap{white-space:pre-wrap}.dark .dark\:text-amber-400{color:var(--color-amber-400)}.dark .dark\:text-black{color:var(--color-black)}.dark .dark\:text-blue-50{color:var(--color-blue-50)}.dark .dark\:text-blue-100{color:var(--color-blue-100)}.dark .dark\:text-blue-200{color:var(--color-blue-200)}.dark .dark\:text-blue-300{color:var(--color-blue-300)}.dark .dark\:text-blue-400{color:var(--color-blue-400)}.dark .dark\:text-blue-500{color:var(--color-blue-500)}.dark .dark\:text-blue-600{color:var(--color-blue-600)}.dark .dark\:text-blue-700{color:var(--color-blue-700)}.dark .dark\:text-blue-800{color:var(--color-blue-800)}.dark .dark\:text-blue-900{color:var(--color-blue-900)}.dark .dark\:text-emerald-400{color:var(--color-emerald-400)}.dark .dark\:text-gray-50{color:var(--color-gray-50)}.dark .dark\:text-gray-100{color:var(--color-gray-100)}.dark .dark\:text-gray-200{color:var(--color-gray-200)}.dark .dark\:text-gray-300{color:var(--color-gray-300)}.dark .dark\:text-gray-400{color:var(--color-gray-400)}.dark .dark\:text-gray-500{color:var(--color-gray-500)}.dark .dark\:text-gray-500\/80{color:#6a7282cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:text-gray-500\/80{color:color-mix(in oklab,var(--color-gray-500)80%,transparent)}}.dark .dark\:text-gray-600{color:var(--color-gray-600)}.dark .dark\:text-gray-700{color:var(--color-gray-700)}.dark .dark\:text-gray-800{color:var(--color-gray-800)}.dark .dark\:text-gray-900{color:var(--color-gray-900)}.dark .dark\:text-green-50{color:var(--color-green-50)}.dark .dark\:text-green-100{color:var(--color-green-100)}.dark .dark\:text-green-200{color:var(--color-green-200)}.dark .dark\:text-green-300{color:var(--color-green-300)}.dark .dark\:text-green-400{color:var(--color-green-400)}.dark .dark\:text-green-500{color:var(--color-green-500)}.dark .dark\:text-green-600{color:var(--color-green-600)}.dark .dark\:text-green-700{color:var(--color-green-700)}.dark .dark\:text-green-800{color:var(--color-green-800)}.dark .dark\:text-green-900{color:var(--color-green-900)}.dark .dark\:text-indigo-400{color:var(--color-indigo-400)}.dark .dark\:text-orange-50{color:var(--color-orange-50)}.dark .dark\:text-orange-100{color:var(--color-orange-100)}.dark .dark\:text-orange-200{color:var(--color-orange-200)}.dark .dark\:text-orange-300{color:var(--color-orange-300)}.dark .dark\:text-orange-400{color:var(--color-orange-400)}.dark .dark\:text-orange-500{color:var(--color-orange-500)}.dark .dark\:text-orange-600{color:var(--color-orange-600)}.dark .dark\:text-orange-700{color:var(--color-orange-700)}.dark .dark\:text-orange-800{color:var(--color-orange-800)}.dark .dark\:text-orange-900{color:var(--color-orange-900)}.dark .dark\:text-pink-50{color:var(--color-pink-50)}.dark .dark\:text-pink-100{color:var(--color-pink-100)}.dark .dark\:text-pink-200{color:var(--color-pink-200)}.dark .dark\:text-pink-300{color:var(--color-pink-300)}.dark .dark\:text-pink-400{color:var(--color-pink-400)}.dark .dark\:text-pink-500{color:var(--color-pink-500)}.dark .dark\:text-pink-600{color:var(--color-pink-600)}.dark .dark\:text-pink-700{color:var(--color-pink-700)}.dark .dark\:text-pink-800{color:var(--color-pink-800)}.dark .dark\:text-pink-900{color:var(--color-pink-900)}.dark .dark\:text-primary-400{color:var(--color-primary-400)}.dark .dark\:text-primary-500{color:var(--color-primary-500)}.dark .dark\:text-purple-50{color:var(--color-purple-50)}.dark .dark\:text-purple-100{color:var(--color-purple-100)}.dark .dark\:text-purple-200{color:var(--color-purple-200)}.dark .dark\:text-purple-300{color:var(--color-purple-300)}.dark .dark\:text-purple-400{color:var(--color-purple-400)}.dark .dark\:text-purple-500{color:var(--color-purple-500)}.dark .dark\:text-purple-600{color:var(--color-purple-600)}.dark .dark\:text-purple-700{color:var(--color-purple-700)}.dark .dark\:text-purple-800{color:var(--color-purple-800)}.dark .dark\:text-purple-900{color:var(--color-purple-900)}.dark .dark\:text-red-50{color:var(--color-red-50)}.dark .dark\:text-red-100{color:var(--color-red-100)}.dark .dark\:text-red-200{color:var(--color-red-200)}.dark .dark\:text-red-300{color:var(--color-red-300)}.dark .dark\:text-red-400{color:var(--color-red-400)}.dark .dark\:text-red-500{color:var(--color-red-500)}.dark .dark\:text-red-600{color:var(--color-red-600)}.dark .dark\:text-red-700{color:var(--color-red-700)}.dark .dark\:text-red-800{color:var(--color-red-800)}.dark .dark\:text-red-900{color:var(--color-red-900)}.dark .dark\:text-slate-50{color:var(--color-slate-50)}.dark .dark\:text-slate-100{color:var(--color-slate-100)}.dark .dark\:text-slate-200{color:var(--color-slate-200)}.dark .dark\:text-slate-300{color:var(--color-slate-300)}.dark .dark\:text-slate-400{color:var(--color-slate-400)}.dark .dark\:text-slate-500{color:var(--color-slate-500)}.dark .dark\:text-slate-600{color:var(--color-slate-600)}.dark .dark\:text-slate-700{color:var(--color-slate-700)}.dark .dark\:text-slate-800{color:var(--color-slate-800)}.dark .dark\:text-slate-900{color:var(--color-slate-900)}.dark .dark\:text-white{color:var(--color-white)}.dark .dark\:text-yellow-50{color:var(--color-yellow-50)}.dark .dark\:text-yellow-100{color:var(--color-yellow-100)}.dark .dark\:text-yellow-200{color:var(--color-yellow-200)}.dark .dark\:text-yellow-300{color:var(--color-yellow-300)}.dark .dark\:text-yellow-400{color:var(--color-yellow-400)}.dark .dark\:text-yellow-500{color:var(--color-yellow-500)}.dark .dark\:text-yellow-600{color:var(--color-yellow-600)}.dark .dark\:text-yellow-700{color:var(--color-yellow-700)}.dark .dark\:text-yellow-800{color:var(--color-yellow-800)}.dark .dark\:text-yellow-900{color:var(--color-yellow-900)}.dark .dark\:italic{font-style:italic}.dark .dark\:underline{text-decoration-line:underline}.dark .dark\:placeholder-gray-400::placeholder{color:var(--color-gray-400)}.dark .dark\:placeholder-gray-500::placeholder{color:var(--color-gray-500)}.dark .dark\:opacity-0{opacity:0}.dark .dark\:opacity-25{opacity:.25}.dark .dark\:opacity-50{opacity:.5}.dark .dark\:opacity-75{opacity:.75}.dark .dark\:opacity-100{opacity:1}.dark .dark\:shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:ring-blue-50{--tw-ring-color:var(--color-blue-50)}.dark .dark\:ring-blue-100{--tw-ring-color:var(--color-blue-100)}.dark .dark\:ring-blue-200{--tw-ring-color:var(--color-blue-200)}.dark .dark\:ring-blue-300{--tw-ring-color:var(--color-blue-300)}.dark .dark\:ring-blue-400{--tw-ring-color:var(--color-blue-400)}.dark .dark\:ring-blue-500{--tw-ring-color:var(--color-blue-500)}.dark .dark\:ring-blue-600{--tw-ring-color:var(--color-blue-600)}.dark .dark\:ring-blue-700{--tw-ring-color:var(--color-blue-700)}.dark .dark\:ring-blue-800{--tw-ring-color:var(--color-blue-800)}.dark .dark\:ring-blue-900{--tw-ring-color:var(--color-blue-900)}.dark .dark\:ring-gray-50{--tw-ring-color:var(--color-gray-50)}.dark .dark\:ring-gray-100{--tw-ring-color:var(--color-gray-100)}.dark .dark\:ring-gray-200{--tw-ring-color:var(--color-gray-200)}.dark .dark\:ring-gray-300{--tw-ring-color:var(--color-gray-300)}.dark .dark\:ring-gray-400{--tw-ring-color:var(--color-gray-400)}.dark .dark\:ring-gray-500{--tw-ring-color:var(--color-gray-500)}.dark .dark\:ring-gray-600{--tw-ring-color:var(--color-gray-600)}.dark .dark\:ring-gray-700{--tw-ring-color:var(--color-gray-700)}.dark .dark\:ring-gray-800{--tw-ring-color:var(--color-gray-800)}.dark .dark\:ring-gray-900{--tw-ring-color:var(--color-gray-900)}.dark .dark\:ring-white{--tw-ring-color:var(--color-white)}.dark .dark\:ring-offset-gray-800{--tw-ring-offset-color:var(--color-gray-800)}@media(hover:hover){.dark .dark\:group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.dark .dark\:group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.dark .dark\:group-hover\:text-blue-400:is(:where(.group):hover *){color:var(--color-blue-400)}.dark .dark\:group-hover\:text-gray-100:is(:where(.group):hover *){color:var(--color-gray-100)}.dark .dark\:group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.dark .dark\:selection\:bg-blue-800 ::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:bg-blue-800::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:text-blue-100 ::selection{color:var(--color-blue-100)}.dark .dark\:selection\:text-blue-100::selection{color:var(--color-blue-100)}.dark .dark\:odd\:bg-gray-800:nth-child(odd){background-color:var(--color-gray-800)}.dark .dark\:odd\:bg-gray-900:nth-child(odd){background-color:var(--color-gray-900)}.dark .dark\:even\:bg-gray-800:nth-child(2n){background-color:var(--color-gray-800)}.dark .dark\:even\:bg-gray-900:nth-child(2n){background-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.dark .dark\:hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.dark .dark\:hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.dark .dark\:hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.dark .dark\:hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.dark .dark\:hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.dark .dark\:hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.dark .dark\:hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.dark .dark\:hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.dark .dark\:hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.dark .dark\:hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.dark .dark\:hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.dark .dark\:hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.dark .dark\:hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.dark .dark\:hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.dark .dark\:hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.dark .dark\:hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.dark .dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark .dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.dark .dark\:hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.dark .dark\:hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.dark .dark\:hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.dark .dark\:hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.dark .dark\:hover\:bg-blue-900\/20:hover{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/20:hover{background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark .dark\:hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.dark .dark\:hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.dark .dark\:hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.dark .dark\:hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.dark .dark\:hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.dark .dark\:hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.dark .dark\:hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.dark .dark\:hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.dark .dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark .dark\:hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.dark .dark\:hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.dark .dark\:hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.dark .dark\:hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.dark .dark\:hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.dark .dark\:hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.dark .dark\:hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.dark .dark\:hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.dark .dark\:hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.dark .dark\:hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.dark .dark\:hover\:bg-orange-900\/20:hover{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/20:hover{background-color:color-mix(in oklab,var(--color-orange-900)20%,transparent)}}.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:color-mix(in oklab,var(--color-orange-900)30%,transparent)}}.dark .dark\:hover\:bg-red-600:hover{background-color:var(--color-red-600)}.dark .dark\:hover\:bg-red-900\/20:hover{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/20:hover{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark .dark\:hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.dark .dark\:hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.dark .dark\:hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.dark .dark\:hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.dark .dark\:hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.dark .dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.dark .dark\:hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.dark .dark\:hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.dark .dark\:hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.dark .dark\:hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.dark .dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark .dark\:hover\:text-black:hover{color:var(--color-black)}.dark .dark\:hover\:text-blue-50:hover{color:var(--color-blue-50)}.dark .dark\:hover\:text-blue-100:hover{color:var(--color-blue-100)}.dark .dark\:hover\:text-blue-200:hover{color:var(--color-blue-200)}.dark .dark\:hover\:text-blue-300:hover{color:var(--color-blue-300)}.dark .dark\:hover\:text-blue-400:hover{color:var(--color-blue-400)}.dark .dark\:hover\:text-blue-500:hover{color:var(--color-blue-500)}.dark .dark\:hover\:text-blue-600:hover{color:var(--color-blue-600)}.dark .dark\:hover\:text-blue-700:hover{color:var(--color-blue-700)}.dark .dark\:hover\:text-blue-800:hover{color:var(--color-blue-800)}.dark .dark\:hover\:text-blue-900:hover{color:var(--color-blue-900)}.dark .dark\:hover\:text-gray-50:hover{color:var(--color-gray-50)}.dark .dark\:hover\:text-gray-100:hover{color:var(--color-gray-100)}.dark .dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark .dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}.dark .dark\:hover\:text-gray-400:hover{color:var(--color-gray-400)}.dark .dark\:hover\:text-gray-500:hover{color:var(--color-gray-500)}.dark .dark\:hover\:text-gray-600:hover{color:var(--color-gray-600)}.dark .dark\:hover\:text-gray-700:hover{color:var(--color-gray-700)}.dark .dark\:hover\:text-gray-800:hover{color:var(--color-gray-800)}.dark .dark\:hover\:text-gray-900:hover{color:var(--color-gray-900)}.dark .dark\:hover\:text-green-50:hover{color:var(--color-green-50)}.dark .dark\:hover\:text-green-100:hover{color:var(--color-green-100)}.dark .dark\:hover\:text-green-200:hover{color:var(--color-green-200)}.dark .dark\:hover\:text-green-300:hover{color:var(--color-green-300)}.dark .dark\:hover\:text-green-400:hover{color:var(--color-green-400)}.dark .dark\:hover\:text-green-500:hover{color:var(--color-green-500)}.dark .dark\:hover\:text-green-600:hover{color:var(--color-green-600)}.dark .dark\:hover\:text-green-700:hover{color:var(--color-green-700)}.dark .dark\:hover\:text-green-800:hover{color:var(--color-green-800)}.dark .dark\:hover\:text-green-900:hover{color:var(--color-green-900)}.dark .dark\:hover\:text-red-50:hover{color:var(--color-red-50)}.dark .dark\:hover\:text-red-100:hover{color:var(--color-red-100)}.dark .dark\:hover\:text-red-200:hover{color:var(--color-red-200)}.dark .dark\:hover\:text-red-300:hover{color:var(--color-red-300)}.dark .dark\:hover\:text-red-400:hover{color:var(--color-red-400)}.dark .dark\:hover\:text-red-500:hover{color:var(--color-red-500)}.dark .dark\:hover\:text-red-600:hover{color:var(--color-red-600)}.dark .dark\:hover\:text-red-700:hover{color:var(--color-red-700)}.dark .dark\:hover\:text-red-800:hover{color:var(--color-red-800)}.dark .dark\:hover\:text-red-900:hover{color:var(--color-red-900)}.dark .dark\:hover\:text-white:hover{color:var(--color-white)}}.dark .dark\:focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.dark .dark\:focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.dark .dark\:focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.dark .dark\:focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.dark .dark\:focus\:text-gray-100:focus{color:var(--color-gray-100)}.dark .dark\:focus\:text-gray-200:focus{color:var(--color-gray-200)}.dark .dark\:focus\:text-gray-300:focus{color:var(--color-gray-300)}.dark .dark\:focus\:text-white:focus{color:var(--color-white)}.dark .dark\:focus\:ring-amber-600:focus{--tw-ring-color:var(--color-amber-600)}.dark .dark\:focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.dark .dark\:focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.dark .dark\:focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.dark .dark\:focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.dark .dark\:focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.dark .dark\:focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.dark .dark\:focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.dark .dark\:focus\:ring-indigo-400:focus{--tw-ring-color:var(--color-indigo-400)}.dark .dark\:focus\:ring-white:focus{--tw-ring-color:var(--color-white)}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.dark .dark\:active\:bg-gray-200:active{background-color:var(--color-gray-200)}.dark .dark\:active\:bg-gray-600:active{background-color:var(--color-gray-600)}.dark .dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark .dark\:active\:bg-gray-800:active{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-gray-700:disabled{background-color:var(--color-gray-700)}.dark .dark\:disabled\:bg-gray-800:disabled{background-color:var(--color-gray-800)}.dark .dark\:disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.dark .dark\:disabled\:text-gray-600:disabled{color:var(--color-gray-600)}.dark .dark\:disabled\:opacity-50:disabled{opacity:.5}.\[\&_img\]\:hidden img{display:none}.\[\&_mark\]\:rounded mark{border-radius:.25rem}.\[\&_mark\]\:bg-yellow-200 mark{background-color:var(--color-yellow-200)}.\[\&_mark\]\:px-0\.5 mark{padding-inline:calc(var(--spacing)*.5)}.dark :is(.\[\&_mark\]\:dark\:bg-yellow-500\/40 mark){background-color:#edb20066}@supports (color:color-mix(in lab,red,red)){.dark :is(.\[\&_mark\]\:dark\:bg-yellow-500\/40 mark){background-color:color-mix(in oklab,var(--color-yellow-500)40%,transparent)}}.\[\&_table\]\:hidden table{display:none}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_table\]\:border table{border-style:var(--tw-border-style);border-width:1px}.\[\&_table\]\:border-gray-300 table{border-color:var(--color-gray-300)}.dark .dark\:\[\&_table\]\:border-gray-600 table{border-color:var(--color-gray-600)}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-gray-300 td{border-color:var(--color-gray-300)}.\[\&_td\]\:px-4 td{padding-inline:calc(var(--spacing)*4)}.\[\&_td\]\:py-2 td{padding-block:calc(var(--spacing)*2)}.dark .dark\:\[\&_td\]\:border-gray-600 td{border-color:var(--color-gray-600)}.\[\&_textarea\]\:\!border-red-500 textarea{border-color:var(--color-red-500)!important}.\[\&_textarea\]\:focus\:\!ring-red-500 textarea:focus{--tw-ring-color:var(--color-red-500)!important}.dark :is(.\[\&_textarea\]\:dark\:\!border-red-500 textarea){border-color:var(--color-red-500)!important}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-gray-300 th{border-color:var(--color-gray-300)}.\[\&_th\]\:bg-gray-100 th{background-color:var(--color-gray-100)}.\[\&_th\]\:px-4 th{padding-inline:calc(var(--spacing)*4)}.\[\&_th\]\:py-2 th{padding-block:calc(var(--spacing)*2)}.\[\&_th\]\:text-left th{text-align:left}.dark .dark\:\[\&_th\]\:border-gray-600 th{border-color:var(--color-gray-600)}.dark .dark\:\[\&_th\]\:bg-gray-800 th{background-color:var(--color-gray-800)}.\[\&_tr\:hover\]\:bg-gray-50 tr:hover{background-color:var(--color-gray-50)}.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-hide::-webkit-scrollbar{display:none}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}}@property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.yarl__fullsize{height:100%;width:100%}.yarl__relative{position:relative}.yarl__portal{inset:0;opacity:0;overflow:hidden;position:fixed;transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);z-index:var(--yarl__portal_zindex,9999)}.yarl__portal_open{opacity:1}.yarl__container{background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));inset:0;outline:none;overflow:hidden;overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain);position:absolute;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none}.yarl__carousel{align-content:center;align-items:stretch;display:flex;flex:0 0 auto;height:100%;justify-content:center;opacity:var(--yarl__pull_opacity,1);transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%))}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{align-content:center;align-items:center;display:flex;justify-content:center}.yarl__slide{flex:1;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%);position:relative}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-moz-user-select:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{left:50%;line-height:0;position:absolute;top:50%;transform:translate(-50%) translateY(-50%)}.yarl__slide_loading{animation:yarl__delayed_fadein 1s linear;color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)))}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{color:var(--yarl__slide_icon_error_color,red);height:var(--yarl__slide_icon_error_size,48px);width:var(--yarl__slide_icon_error_size,48px)}@media(prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{display:flex;justify-content:flex-end;inset:0 0 auto auto;padding:var(--yarl__toolbar_padding,8px);position:absolute}[dir=rtl] .yarl__toolbar{inset:0 auto auto 0}.yarl__icon{height:var(--yarl__icon_size,32px);width:var(--yarl__icon_size,32px)}.yarl__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);color:var(--yarl__color_button,hsla(0,0%,100%,.8));cursor:pointer;filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));line-height:0;margin:var(--yarl__button_margin,0);outline:none;padding:var(--yarl__button_padding,8px);-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media(hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{padding:var(--yarl__navigation_button_padding,24px 16px);position:absolute;top:50%;transform:translateY(-50%)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}.yarl__counter{bottom:var(--yarl__counter_bottom,unset);color:var(--yarl__counter_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__counter_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));left:var(--yarl__counter_left,0);line-height:var(--yarl__counter_line_height,var(--yarl__icon_size,32px));margin:var(--yarl__counter_margin,var(--yarl__toolbar_padding,8px));padding:var(--yarl__counter_padding,var(--yarl__button_padding,8px));position:var(--yarl__counter_position,absolute);right:var(--yarl__counter_right,unset);top:var(--yarl__counter_top,0);-webkit-user-select:var(--yarl__counter_user_select,none);-moz-user-select:var(--yarl__counter_user_select,none);user-select:var(--yarl__counter_user_select,none)}.yarl__thumbnails{display:flex;height:100%}.yarl__thumbnails_bottom,.yarl__thumbnails_end .yarl__thumbnails_track,.yarl__thumbnails_start .yarl__thumbnails_track,.yarl__thumbnails_top{flex-direction:column}.yarl__thumbnails_wrapper{flex:1;position:relative}.yarl__thumbnails_container{background-color:var(--yarl__thumbnails_container_background_color,var(--yarl__color_backdrop,#000));flex:0 0 auto;overflow:hidden;padding:var(--yarl__thumbnails_container_padding,16px);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__thumbnails_vignette{pointer-events:none;position:absolute;--yarl__thumbnails_vignette_size:12%}@media(min-width:1200px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:8%}}@media(min-width:2000px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:5%}}.yarl__thumbnails_bottom .yarl__thumbnails_vignette,.yarl__thumbnails_top .yarl__thumbnails_vignette{background:linear-gradient(to right,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);height:100%;left:0;right:0}.yarl__thumbnails_end .yarl__thumbnails_vignette,.yarl__thumbnails_start .yarl__thumbnails_vignette{background:linear-gradient(to bottom,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);bottom:0;top:0;width:100%}.yarl__thumbnails_track{gap:var(--yarl__thumbnails_thumbnail_gap,16px);outline:none}.yarl__thumbnails_thumbnail{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--yarl__thumbnails_thumbnail_background,#000);border-color:var(--yarl__thumbnails_thumbnail_border_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));border-radius:var(--yarl__thumbnails_thumbnail_border_radius,4px);border-style:var(--yarl__thumbnails_thumbnail_border_style,solid);border-width:var(--yarl__thumbnails_thumbnail_border,1px);cursor:pointer;flex:0 0 auto;-webkit-tap-highlight-color:transparent;box-sizing:content-box;height:var(--yarl__thumbnails_thumbnail_height,80px);outline:none;overflow:hidden;padding:var(--yarl__thumbnails_thumbnail_padding,4px);position:relative;width:var(--yarl__thumbnails_thumbnail_width,120px)}.yarl__thumbnails_thumbnail_active{border-color:var(--yarl__thumbnails_thumbnail_active_border_color,var(--yarl__color_button_active,#fff))}.yarl__thumbnails_thumbnail_fadein{animation:yarl__thumbnails_thumbnail_fadein var(--yarl__thumbnails_thumbnail_fadein_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadein_delay,0s) forwards;opacity:0}.yarl__thumbnails_thumbnail_fadeout{animation:yarl__thumbnails_thumbnail_fadeout var(--yarl__thumbnails_thumbnail_fadeout_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadeout_delay,0s) forwards;cursor:unset}.yarl__thumbnails_thumbnail_placeholder{cursor:unset;visibility:hidden}.yarl__thumbnails_thumbnail:focus{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail:focus:not(:focus-visible){box-shadow:unset}.yarl__thumbnails_thumbnail:focus-visible{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail_icon{color:var(--yarl__thumbnails_thumbnail_icon_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__thumbnails_thumbnail_icon_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));height:var(--yarl__thumbnails_thumbnail_icon_size,32px);left:50%;position:absolute;top:50%;transform:translate(-50%) translateY(-50%);width:var(--yarl__thumbnails_thumbnail_icon_size,32px)}@keyframes yarl__thumbnails_thumbnail_fadein{0%{opacity:0}to{opacity:1}}@keyframes yarl__thumbnails_thumbnail_fadeout{0%{opacity:1}to{opacity:0}} +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-100:oklch(93.6% .032 17.717);--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-orange-50:oklch(98% .016 73.684);--color-orange-100:oklch(95.4% .038 75.164);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-500:oklch(70.5% .213 47.604);--color-orange-600:oklch(64.6% .222 41.116);--color-orange-700:oklch(55.3% .195 38.402);--color-orange-800:oklch(47% .157 37.304);--color-orange-900:oklch(40.8% .123 38.172);--color-orange-950:oklch(26.6% .079 36.259);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-700:oklch(55.5% .163 48.998);--color-amber-800:oklch(47.3% .137 46.201);--color-amber-900:oklch(41.4% .112 45.904);--color-amber-950:oklch(27.9% .077 45.635);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-100:oklch(97.3% .071 103.193);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-900:oklch(42.1% .095 57.708);--color-yellow-950:oklch(28.6% .066 53.813);--color-lime-50:oklch(98.6% .031 120.757);--color-lime-100:oklch(96.7% .067 122.328);--color-lime-200:oklch(93.8% .127 124.321);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-400:oklch(84.1% .238 128.85);--color-lime-500:oklch(76.8% .233 130.85);--color-lime-600:oklch(64.8% .2 131.684);--color-lime-700:oklch(53.2% .157 131.589);--color-lime-800:oklch(45.3% .124 130.933);--color-lime-900:oklch(40.5% .101 131.063);--color-lime-950:oklch(27.4% .072 132.109);--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-100:oklch(95% .052 163.051);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-emerald-700:oklch(50.8% .118 165.612);--color-emerald-800:oklch(43.2% .095 166.913);--color-emerald-900:oklch(37.8% .077 168.94);--color-emerald-950:oklch(26.2% .051 172.552);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-100:oklch(95.3% .051 180.801);--color-teal-200:oklch(91% .096 180.426);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-400:oklch(77.7% .152 181.912);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-teal-800:oklch(43.7% .078 188.216);--color-teal-900:oklch(38.6% .063 188.416);--color-teal-950:oklch(27.7% .046 192.524);--color-cyan-50:oklch(98.4% .019 200.873);--color-cyan-100:oklch(95.6% .045 203.388);--color-cyan-200:oklch(91.7% .08 205.041);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-700:oklch(52% .105 223.128);--color-cyan-800:oklch(45% .085 224.283);--color-cyan-900:oklch(39.8% .07 227.392);--color-cyan-950:oklch(30.2% .056 229.695);--color-sky-50:oklch(97.7% .013 236.62);--color-sky-100:oklch(95.1% .026 236.824);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-sky-700:oklch(50% .134 242.749);--color-sky-800:oklch(44.3% .11 240.79);--color-sky-900:oklch(39.1% .09 240.876);--color-sky-950:oklch(29.3% .066 243.157);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-blue-950:oklch(28.2% .091 267.935);--color-indigo-50:oklch(96.2% .018 272.314);--color-indigo-100:oklch(93% .034 272.788);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-indigo-600:oklch(51.1% .262 276.966);--color-indigo-700:oklch(45.7% .24 277.023);--color-indigo-800:oklch(39.8% .195 277.366);--color-indigo-900:oklch(35.9% .144 278.697);--color-indigo-950:oklch(25.7% .09 281.288);--color-violet-50:oklch(96.9% .016 293.756);--color-violet-100:oklch(94.3% .029 294.588);--color-violet-200:oklch(89.4% .057 293.283);--color-violet-300:oklch(81.1% .111 293.571);--color-violet-400:oklch(70.2% .183 293.541);--color-violet-500:oklch(60.6% .25 292.717);--color-violet-600:oklch(54.1% .281 293.009);--color-violet-700:oklch(49.1% .27 292.581);--color-violet-800:oklch(43.2% .232 292.759);--color-violet-900:oklch(38% .189 293.745);--color-violet-950:oklch(28.3% .141 291.089);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-600:oklch(55.8% .288 302.321);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-800:oklch(43.8% .218 303.724);--color-purple-900:oklch(38.1% .176 304.987);--color-purple-950:oklch(29.1% .149 302.717);--color-fuchsia-50:oklch(97.7% .017 320.058);--color-fuchsia-100:oklch(95.2% .037 318.852);--color-fuchsia-200:oklch(90.3% .076 319.62);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-400:oklch(74% .238 322.16);--color-fuchsia-500:oklch(66.7% .295 322.15);--color-fuchsia-600:oklch(59.1% .293 322.896);--color-fuchsia-700:oklch(51.8% .253 323.949);--color-fuchsia-800:oklch(45.2% .211 324.591);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-fuchsia-950:oklch(29.3% .136 325.661);--color-pink-50:oklch(97.1% .014 343.198);--color-pink-100:oklch(94.8% .028 342.258);--color-pink-200:oklch(89.9% .061 343.231);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-400:oklch(71.8% .202 349.761);--color-pink-500:oklch(65.6% .241 354.308);--color-pink-600:oklch(59.2% .249 .584);--color-pink-700:oklch(52.5% .223 3.958);--color-pink-800:oklch(45.9% .187 3.815);--color-pink-900:oklch(40.8% .153 2.432);--color-pink-950:oklch(28.4% .109 3.907);--color-rose-50:oklch(96.9% .015 12.422);--color-rose-100:oklch(94.1% .03 12.58);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-400:oklch(71.2% .194 13.428);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-600:oklch(58.6% .253 17.585);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-800:oklch(45.5% .188 13.697);--color-rose-900:oklch(41% .159 10.272);--color-rose-950:oklch(27.1% .105 12.094);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-slate-950:oklch(12.9% .042 264.695);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-gray-950:oklch(13% .028 261.692);--color-zinc-50:oklch(98.5% 0 0);--color-zinc-100:oklch(96.7% .001 286.375);--color-zinc-200:oklch(92% .004 286.32);--color-zinc-300:oklch(87.1% .006 286.286);--color-zinc-400:oklch(70.5% .015 286.067);--color-zinc-500:oklch(55.2% .016 285.938);--color-zinc-600:oklch(44.2% .017 285.786);--color-zinc-700:oklch(37% .013 285.805);--color-zinc-800:oklch(27.4% .006 286.033);--color-zinc-900:oklch(21% .006 285.885);--color-zinc-950:oklch(14.1% .005 285.823);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-300:oklch(87% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-600:oklch(43.9% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-800:oklch(26.9% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-neutral-950:oklch(14.5% 0 0);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-stone-800:oklch(26.8% .007 34.298);--color-stone-900:oklch(21.6% .006 56.043);--color-stone-950:oklch(14.7% .004 49.25);--color-black:#000;--color-white:#fff;--spacing:.25rem;--breakpoint-sm:40rem;--breakpoint-md:48rem;--breakpoint-lg:64rem;--breakpoint-xl:80rem;--breakpoint-2xl:96rem;--container-3xs:16rem;--container-2xs:18rem;--container-xs:20rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-3xl:48rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5/2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-thin:100;--font-weight-extralight:200;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tighter:-.05em;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--radius-3xl:1.5rem;--drop-shadow-sm:0 1px 2px #00000026;--drop-shadow-md:0 3px 3px #0000001f;--drop-shadow-lg:0 4px 4px #00000026;--drop-shadow-xl:0 9px 7px #0000001a;--drop-shadow-2xl:0 25px 25px #00000026;--ease-in:cubic-bezier(.4,0,1,1);--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0,0,.2,1)infinite;--animate-pulse:pulse 2s cubic-bezier(.4,0,.6,1)infinite;--animate-bounce:bounce 1s infinite;--blur-sm:8px;--blur-md:12px;--blur-lg:16px;--blur-xl:24px;--blur-2xl:40px;--blur-3xl:64px;--aspect-video:16/9;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary-50:#eff6ff;--color-primary-100:#dbeafe;--color-primary-400:#60a5fa;--color-primary-500:#3b82f6;--color-primary-600:#2563eb;--color-primary-700:#1d4ed8;--color-primary-900:#1e3a8a}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:255 255 255;--foreground:17 24 39}.dark{--background:17 24 39;--foreground:255 255 255}html{scroll-behavior:smooth}body{background-color:var(--color-white);color:var(--color-gray-900)}.dark :is(body){background-color:var(--color-gray-900);color:var(--color-white)}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}a{color:var(--color-blue-600)}@media(hover:hover){a:hover{color:var(--color-blue-700)}}.dark :is(a){color:var(--color-blue-400)}@media(hover:hover){.dark :is(a):hover{color:var(--color-blue-300)}}a{text-decoration:none}:focus-visible{outline-style:var(--tw-outline-style);outline-offset:2px;outline-width:2px;outline-color:var(--color-blue-500)}}@layer components{.flex-between{justify-content:space-between;align-items:center;display:flex}.btn{border-radius:var(--radius-lg);--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));justify-content:center;align-items:center;display:inline-flex}.btn:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color);--tw-outline-style:none;outline-style:none}.btn:disabled{cursor:not-allowed;opacity:.5}.btn-primary{background-color:var(--color-blue-600);color:var(--color-white)}@media(hover:hover){.btn-primary:hover{background-color:var(--color-blue-700)}}.btn-primary:focus{--tw-ring-color:var(--color-blue-500)}.dark .btn-primary{background-color:var(--color-blue-500)}@media(hover:hover){.dark .btn-primary:hover{background-color:var(--color-blue-600)}}.dark .btn-primary:focus{--tw-ring-color:var(--color-blue-400)}.btn-secondary{background-color:var(--color-gray-100);color:var(--color-gray-900)}@media(hover:hover){.btn-secondary:hover{background-color:var(--color-gray-200)}}.btn-secondary:focus{--tw-ring-color:var(--color-gray-500)}.dark .btn-secondary{background-color:var(--color-gray-700);color:var(--color-white)}@media(hover:hover){.dark .btn-secondary:hover{background-color:var(--color-gray-600)}}.dark .btn-secondary:focus{--tw-ring-color:var(--color-gray-400)}.btn-ghost{color:var(--color-gray-700);background-color:#0000}@media(hover:hover){.btn-ghost:hover{background-color:var(--color-gray-100)}}.btn-ghost:focus{--tw-ring-color:var(--color-gray-500)}.dark .btn-ghost{color:var(--color-gray-300)}@media(hover:hover){.dark .btn-ghost:hover{background-color:var(--color-gray-800)}}.dark .btn-ghost:focus{--tw-ring-color:var(--color-gray-400)}.btn-danger{background-color:var(--color-red-600);color:var(--color-white)}@media(hover:hover){.btn-danger:hover{background-color:var(--color-red-700)}}.btn-danger:focus{--tw-ring-color:var(--color-red-500)}.dark .btn-danger{background-color:var(--color-red-500)}@media(hover:hover){.dark .btn-danger:hover{background-color:var(--color-red-600)}}.dark .btn-danger:focus{--tw-ring-color:var(--color-red-400)}.input{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-300);width:100%;padding-inline:calc(var(--spacing)*4);padding-block:calc(var(--spacing)*2);background-color:var(--color-white);color:var(--color-gray-900)}.input:focus{border-color:var(--color-blue-500);--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:var(--color-blue-500)}.dark .input{border-color:var(--color-gray-600);background-color:var(--color-gray-800);color:var(--color-white)}.input::placeholder{color:var(--color-gray-400)}.dark .input::placeholder{color:var(--color-gray-500)}.input-error{border-color:var(--color-red-500)}.input-error:focus{border-color:var(--color-red-500);--tw-ring-color:var(--color-red-500)}.card{border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-gray-200);background-color:var(--color-white);--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .card{border-color:var(--color-gray-700);background-color:var(--color-gray-800)}.badge{padding-inline:calc(var(--spacing)*2.5);padding-block:calc(var(--spacing)*.5);font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);border-radius:3.40282e38px;align-items:center;display:inline-flex}.badge-primary{background-color:var(--color-blue-100);color:var(--color-blue-800)}.dark .badge-primary{background-color:var(--color-blue-900);color:var(--color-blue-200)}.badge-success{background-color:var(--color-green-100);color:var(--color-green-800)}.dark .badge-success{background-color:var(--color-green-900);color:var(--color-green-200)}.badge-warning{background-color:var(--color-yellow-100);color:var(--color-yellow-800)}.dark .badge-warning{background-color:var(--color-yellow-900);color:var(--color-yellow-200)}.badge-danger{background-color:var(--color-red-100);color:var(--color-red-800)}.dark .badge-danger{background-color:var(--color-red-900);color:var(--color-red-200)}}@layer utilities{.\@container{container-type:inline-size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing)*0)}.inset-1{inset:calc(var(--spacing)*1)}.inset-2{inset:calc(var(--spacing)*2)}.inset-3{inset:calc(var(--spacing)*3)}.inset-4{inset:calc(var(--spacing)*4)}.inset-5{inset:calc(var(--spacing)*5)}.inset-6{inset:calc(var(--spacing)*6)}.inset-8{inset:calc(var(--spacing)*8)}.inset-auto{inset:auto}.inset-x-0{inset-inline:calc(var(--spacing)*0)}.inset-x-1{inset-inline:calc(var(--spacing)*1)}.inset-x-2{inset-inline:calc(var(--spacing)*2)}.inset-x-3{inset-inline:calc(var(--spacing)*3)}.inset-x-4{inset-inline:calc(var(--spacing)*4)}.inset-x-auto{inset-inline:auto}.inset-y-0{inset-block:calc(var(--spacing)*0)}.inset-y-1{inset-block:calc(var(--spacing)*1)}.inset-y-2{inset-block:calc(var(--spacing)*2)}.inset-y-3{inset-block:calc(var(--spacing)*3)}.inset-y-4{inset-block:calc(var(--spacing)*4)}.inset-y-auto{inset-block:auto}.-top-1{top:calc(var(--spacing)*-1)}.-top-2{top:calc(var(--spacing)*-2)}.-top-3{top:calc(var(--spacing)*-3)}.-top-4{top:calc(var(--spacing)*-4)}.top-0{top:calc(var(--spacing)*0)}.top-1{top:calc(var(--spacing)*1)}.top-1\.5{top:calc(var(--spacing)*1.5)}.top-1\/2{top:50%}.top-1\/3{top:33.3333%}.top-1\/4{top:25%}.top-2{top:calc(var(--spacing)*2)}.top-2\/3{top:66.6667%}.top-3{top:calc(var(--spacing)*3)}.top-3\/4{top:75%}.top-4{top:calc(var(--spacing)*4)}.top-5{top:calc(var(--spacing)*5)}.top-6{top:calc(var(--spacing)*6)}.top-8{top:calc(var(--spacing)*8)}.top-10{top:calc(var(--spacing)*10)}.top-12{top:calc(var(--spacing)*12)}.top-16{top:calc(var(--spacing)*16)}.top-24{top:calc(var(--spacing)*24)}.top-auto{top:auto}.top-full{top:100%}.-right-1{right:calc(var(--spacing)*-1)}.-right-2{right:calc(var(--spacing)*-2)}.-right-3{right:calc(var(--spacing)*-3)}.-right-4{right:calc(var(--spacing)*-4)}.right-0{right:calc(var(--spacing)*0)}.right-1{right:calc(var(--spacing)*1)}.right-1\/2{right:50%}.right-2{right:calc(var(--spacing)*2)}.right-3{right:calc(var(--spacing)*3)}.right-4{right:calc(var(--spacing)*4)}.right-5{right:calc(var(--spacing)*5)}.right-6{right:calc(var(--spacing)*6)}.right-8{right:calc(var(--spacing)*8)}.right-10{right:calc(var(--spacing)*10)}.right-12{right:calc(var(--spacing)*12)}.right-16{right:calc(var(--spacing)*16)}.right-auto{right:auto}.right-full{right:100%}.-bottom-1{bottom:calc(var(--spacing)*-1)}.-bottom-2{bottom:calc(var(--spacing)*-2)}.-bottom-3{bottom:calc(var(--spacing)*-3)}.-bottom-4{bottom:calc(var(--spacing)*-4)}.bottom-0{bottom:calc(var(--spacing)*0)}.bottom-1{bottom:calc(var(--spacing)*1)}.bottom-1\/2{bottom:50%}.bottom-2{bottom:calc(var(--spacing)*2)}.bottom-3{bottom:calc(var(--spacing)*3)}.bottom-4{bottom:calc(var(--spacing)*4)}.bottom-5{bottom:calc(var(--spacing)*5)}.bottom-6{bottom:calc(var(--spacing)*6)}.bottom-8{bottom:calc(var(--spacing)*8)}.bottom-10{bottom:calc(var(--spacing)*10)}.bottom-12{bottom:calc(var(--spacing)*12)}.bottom-16{bottom:calc(var(--spacing)*16)}.bottom-auto{bottom:auto}.bottom-full{bottom:100%}.-left-1{left:calc(var(--spacing)*-1)}.-left-2{left:calc(var(--spacing)*-2)}.-left-3{left:calc(var(--spacing)*-3)}.-left-4{left:calc(var(--spacing)*-4)}.left-0{left:calc(var(--spacing)*0)}.left-1{left:calc(var(--spacing)*1)}.left-1\.5{left:calc(var(--spacing)*1.5)}.left-1\/2{left:50%}.left-2{left:calc(var(--spacing)*2)}.left-3{left:calc(var(--spacing)*3)}.left-4{left:calc(var(--spacing)*4)}.left-5{left:calc(var(--spacing)*5)}.left-6{left:calc(var(--spacing)*6)}.left-8{left:calc(var(--spacing)*8)}.left-10{left:calc(var(--spacing)*10)}.left-12{left:calc(var(--spacing)*12)}.left-16{left:calc(var(--spacing)*16)}.left-auto{left:auto}.left-full{left:100%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-0{z-index:0}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[9999\]{z-index:9999}.z-auto{z-index:auto}.order-1{order:1}.order-2{order:2}.order-3{order:3}.order-4{order:4}.order-5{order:5}.order-6{order:6}.order-first{order:-9999}.order-last{order:9999}.order-none{order:0}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-4{grid-column:span 4/span 4}.col-span-5{grid-column:span 5/span 5}.col-span-6{grid-column:span 6/span 6}.col-span-7{grid-column:span 7/span 7}.col-span-8{grid-column:span 8/span 8}.col-span-9{grid-column:span 9/span 9}.col-span-10{grid-column:span 10/span 10}.col-span-11{grid-column:span 11/span 11}.col-span-12{grid-column:span 12/span 12}.col-span-full{grid-column:1/-1}.col-start-1{grid-column-start:1}.col-start-2{grid-column-start:2}.col-start-3{grid-column-start:3}.col-start-4{grid-column-start:4}.col-start-5{grid-column-start:5}.col-start-6{grid-column-start:6}.col-start-7{grid-column-start:7}.col-start-8{grid-column-start:8}.col-start-9{grid-column-start:9}.col-start-10{grid-column-start:10}.col-start-11{grid-column-start:11}.col-start-12{grid-column-start:12}.col-start-13{grid-column-start:13}.col-end-1{grid-column-end:1}.col-end-2{grid-column-end:2}.col-end-3{grid-column-end:3}.col-end-4{grid-column-end:4}.col-end-5{grid-column-end:5}.col-end-6{grid-column-end:6}.col-end-7{grid-column-end:7}.col-end-8{grid-column-end:8}.col-end-9{grid-column-end:9}.col-end-10{grid-column-end:10}.col-end-11{grid-column-end:11}.col-end-12{grid-column-end:12}.col-end-13{grid-column-end:13}.row-span-1{grid-row:span 1/span 1}.row-span-2{grid-row:span 2/span 2}.row-span-3{grid-row:span 3/span 3}.row-span-4{grid-row:span 4/span 4}.row-span-5{grid-row:span 5/span 5}.row-span-6{grid-row:span 6/span 6}.row-span-full{grid-row:1/-1}.row-start-1{grid-row-start:1}.row-start-2{grid-row-start:2}.row-start-3{grid-row-start:3}.row-start-4{grid-row-start:4}.row-start-5{grid-row-start:5}.row-start-6{grid-row-start:6}.row-start-7{grid-row-start:7}.row-end-1{grid-row-end:1}.row-end-2{grid-row-end:2}.row-end-3{grid-row-end:3}.row-end-4{grid-row-end:4}.row-end-5{grid-row-end:5}.row-end-6{grid-row-end:6}.row-end-7{grid-row-end:7}.float-end{float:inline-end}.float-left{float:left}.float-none{float:none}.float-right{float:right}.float-start{float:inline-start}.clear-both{clear:both}.clear-end{clear:inline-end}.clear-left{clear:left}.clear-none{clear:none}.clear-right{clear:right}.clear-start{clear:inline-start}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.-m-1{margin:calc(var(--spacing)*-1)}.-m-2{margin:calc(var(--spacing)*-2)}.-m-3{margin:calc(var(--spacing)*-3)}.-m-4{margin:calc(var(--spacing)*-4)}.-m-5{margin:calc(var(--spacing)*-5)}.-m-6{margin:calc(var(--spacing)*-6)}.-m-8{margin:calc(var(--spacing)*-8)}.m-0{margin:calc(var(--spacing)*0)}.m-1{margin:calc(var(--spacing)*1)}.m-2{margin:calc(var(--spacing)*2)}.m-3{margin:calc(var(--spacing)*3)}.m-4{margin:calc(var(--spacing)*4)}.m-5{margin:calc(var(--spacing)*5)}.m-6{margin:calc(var(--spacing)*6)}.m-8{margin:calc(var(--spacing)*8)}.m-10{margin:calc(var(--spacing)*10)}.m-12{margin:calc(var(--spacing)*12)}.m-16{margin:calc(var(--spacing)*16)}.m-auto{margin:auto}.-mx-1{margin-inline:calc(var(--spacing)*-1)}.-mx-2{margin-inline:calc(var(--spacing)*-2)}.-mx-3{margin-inline:calc(var(--spacing)*-3)}.-mx-4{margin-inline:calc(var(--spacing)*-4)}.-mx-5{margin-inline:calc(var(--spacing)*-5)}.-mx-6{margin-inline:calc(var(--spacing)*-6)}.-mx-8{margin-inline:calc(var(--spacing)*-8)}.mx-0{margin-inline:calc(var(--spacing)*0)}.mx-1{margin-inline:calc(var(--spacing)*1)}.mx-2{margin-inline:calc(var(--spacing)*2)}.mx-3{margin-inline:calc(var(--spacing)*3)}.mx-4{margin-inline:calc(var(--spacing)*4)}.mx-5{margin-inline:calc(var(--spacing)*5)}.mx-6{margin-inline:calc(var(--spacing)*6)}.mx-8{margin-inline:calc(var(--spacing)*8)}.mx-10{margin-inline:calc(var(--spacing)*10)}.mx-12{margin-inline:calc(var(--spacing)*12)}.mx-16{margin-inline:calc(var(--spacing)*16)}.mx-auto{margin-inline:auto}.-my-1{margin-block:calc(var(--spacing)*-1)}.-my-2{margin-block:calc(var(--spacing)*-2)}.-my-3{margin-block:calc(var(--spacing)*-3)}.-my-4{margin-block:calc(var(--spacing)*-4)}.-my-5{margin-block:calc(var(--spacing)*-5)}.-my-6{margin-block:calc(var(--spacing)*-6)}.-my-8{margin-block:calc(var(--spacing)*-8)}.my-0{margin-block:calc(var(--spacing)*0)}.my-1{margin-block:calc(var(--spacing)*1)}.my-2{margin-block:calc(var(--spacing)*2)}.my-3{margin-block:calc(var(--spacing)*3)}.my-4{margin-block:calc(var(--spacing)*4)}.my-5{margin-block:calc(var(--spacing)*5)}.my-6{margin-block:calc(var(--spacing)*6)}.my-8{margin-block:calc(var(--spacing)*8)}.my-10{margin-block:calc(var(--spacing)*10)}.my-12{margin-block:calc(var(--spacing)*12)}.my-16{margin-block:calc(var(--spacing)*16)}.my-auto{margin-block:auto}.-mt-1{margin-top:calc(var(--spacing)*-1)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.-mt-3{margin-top:calc(var(--spacing)*-3)}.-mt-4{margin-top:calc(var(--spacing)*-4)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.-mt-6{margin-top:calc(var(--spacing)*-6)}.-mt-8{margin-top:calc(var(--spacing)*-8)}.mt-0{margin-top:calc(var(--spacing)*0)}.mt-0\.5{margin-top:calc(var(--spacing)*.5)}.mt-1{margin-top:calc(var(--spacing)*1)}.mt-1\.5{margin-top:calc(var(--spacing)*1.5)}.mt-2{margin-top:calc(var(--spacing)*2)}.mt-3{margin-top:calc(var(--spacing)*3)}.mt-4{margin-top:calc(var(--spacing)*4)}.mt-5{margin-top:calc(var(--spacing)*5)}.mt-6{margin-top:calc(var(--spacing)*6)}.mt-8{margin-top:calc(var(--spacing)*8)}.mt-10{margin-top:calc(var(--spacing)*10)}.mt-12{margin-top:calc(var(--spacing)*12)}.mt-16{margin-top:calc(var(--spacing)*16)}.mt-auto{margin-top:auto}.-mr-1{margin-right:calc(var(--spacing)*-1)}.-mr-2{margin-right:calc(var(--spacing)*-2)}.-mr-3{margin-right:calc(var(--spacing)*-3)}.-mr-4{margin-right:calc(var(--spacing)*-4)}.-mr-5{margin-right:calc(var(--spacing)*-5)}.-mr-6{margin-right:calc(var(--spacing)*-6)}.-mr-8{margin-right:calc(var(--spacing)*-8)}.mr-0{margin-right:calc(var(--spacing)*0)}.mr-1{margin-right:calc(var(--spacing)*1)}.mr-1\.5{margin-right:calc(var(--spacing)*1.5)}.mr-2{margin-right:calc(var(--spacing)*2)}.mr-3{margin-right:calc(var(--spacing)*3)}.mr-4{margin-right:calc(var(--spacing)*4)}.mr-5{margin-right:calc(var(--spacing)*5)}.mr-6{margin-right:calc(var(--spacing)*6)}.mr-8{margin-right:calc(var(--spacing)*8)}.mr-10{margin-right:calc(var(--spacing)*10)}.mr-12{margin-right:calc(var(--spacing)*12)}.mr-16{margin-right:calc(var(--spacing)*16)}.mr-auto{margin-right:auto}.-mb-1{margin-bottom:calc(var(--spacing)*-1)}.-mb-2{margin-bottom:calc(var(--spacing)*-2)}.-mb-3{margin-bottom:calc(var(--spacing)*-3)}.-mb-4{margin-bottom:calc(var(--spacing)*-4)}.-mb-5{margin-bottom:calc(var(--spacing)*-5)}.-mb-6{margin-bottom:calc(var(--spacing)*-6)}.-mb-8{margin-bottom:calc(var(--spacing)*-8)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-0\.5{margin-bottom:calc(var(--spacing)*.5)}.mb-1{margin-bottom:calc(var(--spacing)*1)}.mb-1\.5{margin-bottom:calc(var(--spacing)*1.5)}.mb-2{margin-bottom:calc(var(--spacing)*2)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.mb-5{margin-bottom:calc(var(--spacing)*5)}.mb-6{margin-bottom:calc(var(--spacing)*6)}.mb-8{margin-bottom:calc(var(--spacing)*8)}.mb-10{margin-bottom:calc(var(--spacing)*10)}.mb-12{margin-bottom:calc(var(--spacing)*12)}.mb-16{margin-bottom:calc(var(--spacing)*16)}.mb-auto{margin-bottom:auto}.-ml-1{margin-left:calc(var(--spacing)*-1)}.-ml-2{margin-left:calc(var(--spacing)*-2)}.-ml-3{margin-left:calc(var(--spacing)*-3)}.-ml-4{margin-left:calc(var(--spacing)*-4)}.-ml-5{margin-left:calc(var(--spacing)*-5)}.-ml-6{margin-left:calc(var(--spacing)*-6)}.-ml-8{margin-left:calc(var(--spacing)*-8)}.ml-0{margin-left:calc(var(--spacing)*0)}.ml-0\.5{margin-left:calc(var(--spacing)*.5)}.ml-1{margin-left:calc(var(--spacing)*1)}.ml-1\.5{margin-left:calc(var(--spacing)*1.5)}.ml-2{margin-left:calc(var(--spacing)*2)}.ml-3{margin-left:calc(var(--spacing)*3)}.ml-4{margin-left:calc(var(--spacing)*4)}.ml-5{margin-left:calc(var(--spacing)*5)}.ml-6{margin-left:calc(var(--spacing)*6)}.ml-8{margin-left:calc(var(--spacing)*8)}.ml-10{margin-left:calc(var(--spacing)*10)}.ml-12{margin-left:calc(var(--spacing)*12)}.ml-14{margin-left:calc(var(--spacing)*14)}.ml-16{margin-left:calc(var(--spacing)*16)}.ml-auto{margin-left:auto}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.aspect-\[4\/3\]{aspect-ratio:4/3}.aspect-auto{aspect-ratio:auto}.aspect-square{aspect-ratio:1}.aspect-video{aspect-ratio:var(--aspect-video)}.h-0{height:calc(var(--spacing)*0)}.h-1{height:calc(var(--spacing)*1)}.h-1\/2{height:50%}.h-1\/3{height:33.3333%}.h-1\/4{height:25%}.h-1\/5{height:20%}.h-1\/6{height:16.6667%}.h-2{height:calc(var(--spacing)*2)}.h-2\.5{height:calc(var(--spacing)*2.5)}.h-2\/3{height:66.6667%}.h-2\/4{height:50%}.h-2\/5{height:40%}.h-3{height:calc(var(--spacing)*3)}.h-3\.5{height:calc(var(--spacing)*3.5)}.h-3\/4{height:75%}.h-3\/5{height:60%}.h-4{height:calc(var(--spacing)*4)}.h-4\/5{height:80%}.h-5{height:calc(var(--spacing)*5)}.h-5\/6{height:83.3333%}.h-6{height:calc(var(--spacing)*6)}.h-7{height:calc(var(--spacing)*7)}.h-8{height:calc(var(--spacing)*8)}.h-9{height:calc(var(--spacing)*9)}.h-10{height:calc(var(--spacing)*10)}.h-12{height:calc(var(--spacing)*12)}.h-14{height:calc(var(--spacing)*14)}.h-16{height:calc(var(--spacing)*16)}.h-20{height:calc(var(--spacing)*20)}.h-24{height:calc(var(--spacing)*24)}.h-32{height:calc(var(--spacing)*32)}.h-40{height:calc(var(--spacing)*40)}.h-48{height:calc(var(--spacing)*48)}.h-56{height:calc(var(--spacing)*56)}.h-64{height:calc(var(--spacing)*64)}.h-72{height:calc(var(--spacing)*72)}.h-80{height:calc(var(--spacing)*80)}.h-96{height:calc(var(--spacing)*96)}.h-\[42px\]{height:42px}.h-auto{height:auto}.h-fit{height:fit-content}.h-full{height:100%}.h-max{height:max-content}.h-min{height:min-content}.h-px{height:1px}.h-screen{height:100vh}.max-h-0{max-height:calc(var(--spacing)*0)}.max-h-60{max-height:calc(var(--spacing)*60)}.max-h-64{max-height:calc(var(--spacing)*64)}.max-h-80{max-height:calc(var(--spacing)*80)}.max-h-96{max-height:calc(var(--spacing)*96)}.max-h-\[55vh\]{max-height:55vh}.max-h-\[70vh\]{max-height:70vh}.max-h-\[75vh\]{max-height:75vh}.max-h-\[90vh\]{max-height:90vh}.max-h-\[calc\(90vh-140px\)\]{max-height:calc(90vh - 140px)}.max-h-\[calc\(100vh-200px\)\]{max-height:calc(100vh - 200px)}.max-h-fit{max-height:fit-content}.max-h-full{max-height:100%}.max-h-max{max-height:max-content}.max-h-min{max-height:min-content}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-20{min-height:calc(var(--spacing)*20)}.min-h-\[40px\]{min-height:40px}.min-h-\[60vh\]{min-height:60vh}.min-h-\[80px\]{min-height:80px}.min-h-\[200px\]{min-height:200px}.min-h-\[400px\]{min-height:400px}.min-h-fit{min-height:fit-content}.min-h-full{min-height:100%}.min-h-max{min-height:max-content}.min-h-min{min-height:min-content}.min-h-screen{min-height:100vh}.w-0{width:calc(var(--spacing)*0)}.w-1{width:calc(var(--spacing)*1)}.w-1\/2{width:50%}.w-1\/3{width:33.3333%}.w-1\/4{width:25%}.w-1\/5{width:20%}.w-1\/6{width:16.6667%}.w-2{width:calc(var(--spacing)*2)}.w-2\.5{width:calc(var(--spacing)*2.5)}.w-2\/3{width:66.6667%}.w-2\/4{width:50%}.w-2\/5{width:40%}.w-3{width:calc(var(--spacing)*3)}.w-3\.5{width:calc(var(--spacing)*3.5)}.w-3\/4{width:75%}.w-3\/5{width:60%}.w-4{width:calc(var(--spacing)*4)}.w-4\/5{width:80%}.w-5{width:calc(var(--spacing)*5)}.w-5\/6{width:83.3333%}.w-6{width:calc(var(--spacing)*6)}.w-8{width:calc(var(--spacing)*8)}.w-9{width:calc(var(--spacing)*9)}.w-10{width:calc(var(--spacing)*10)}.w-12{width:calc(var(--spacing)*12)}.w-14{width:calc(var(--spacing)*14)}.w-16{width:calc(var(--spacing)*16)}.w-20{width:calc(var(--spacing)*20)}.w-24{width:calc(var(--spacing)*24)}.w-28{width:calc(var(--spacing)*28)}.w-32{width:calc(var(--spacing)*32)}.w-40{width:calc(var(--spacing)*40)}.w-44{width:calc(var(--spacing)*44)}.w-48{width:calc(var(--spacing)*48)}.w-52{width:calc(var(--spacing)*52)}.w-56{width:calc(var(--spacing)*56)}.w-60{width:calc(var(--spacing)*60)}.w-64{width:calc(var(--spacing)*64)}.w-72{width:calc(var(--spacing)*72)}.w-80{width:calc(var(--spacing)*80)}.w-96{width:calc(var(--spacing)*96)}.w-\[220px\]{width:220px}.w-auto{width:auto}.w-fit{width:fit-content}.w-full{width:100%}.w-max{width:max-content}.w-min{width:min-content}.w-px{width:1px}.w-screen{width:100vw}.max-w-0{max-width:calc(var(--spacing)*0)}.max-w-2xl{max-width:var(--container-2xl)}.max-w-3xl{max-width:var(--container-3xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[60\%\]{max-width:60%}.max-w-\[85vw\]{max-width:85vw}.max-w-\[calc\(100vw-1rem\)\]{max-width:calc(100vw - 1rem)}.max-w-\[calc\(100vw-2rem\)\]{max-width:calc(100vw - 2rem)}.max-w-fit{max-width:fit-content}.max-w-full{max-width:100%}.max-w-lg{max-width:var(--container-lg)}.max-w-max{max-width:max-content}.max-w-md{max-width:var(--container-md)}.max-w-min{max-width:min-content}.max-w-none{max-width:none}.max-w-prose{max-width:65ch}.max-w-screen-2xl{max-width:var(--breakpoint-2xl)}.max-w-screen-lg{max-width:var(--breakpoint-lg)}.max-w-screen-md{max-width:var(--breakpoint-md)}.max-w-screen-sm{max-width:var(--breakpoint-sm)}.max-w-screen-xl{max-width:var(--breakpoint-xl)}.max-w-sm{max-width:var(--container-sm)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing)*0)}.min-w-5{min-width:calc(var(--spacing)*5)}.min-w-24{min-width:calc(var(--spacing)*24)}.min-w-\[100px\]{min-width:100px}.min-w-\[120px\]{min-width:120px}.min-w-\[300px\]{min-width:300px}.min-w-fit{min-width:fit-content}.min-w-full{min-width:100%}.min-w-max{min-width:max-content}.min-w-min{min-width:min-content}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow,.grow{flex-grow:1}.grow-0{flex-grow:0}.basis-0{flex-basis:calc(var(--spacing)*0)}.basis-1{flex-basis:calc(var(--spacing)*1)}.basis-1\/2{flex-basis:50%}.basis-1\/3{flex-basis:33.3333%}.basis-1\/4{flex-basis:25%}.basis-2{flex-basis:calc(var(--spacing)*2)}.basis-2\/3{flex-basis:66.6667%}.basis-3\/4{flex-basis:75%}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.caption-bottom{caption-side:bottom}.caption-top{caption-side:top}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:calc(var(--spacing)*0);--tw-border-spacing-y:calc(var(--spacing)*0);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-1{--tw-border-spacing-x:calc(var(--spacing)*1);--tw-border-spacing-y:calc(var(--spacing)*1);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-2{--tw-border-spacing-x:calc(var(--spacing)*2);--tw-border-spacing-y:calc(var(--spacing)*2);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-3{--tw-border-spacing-x:calc(var(--spacing)*3);--tw-border-spacing-y:calc(var(--spacing)*3);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-4{--tw-border-spacing-x:calc(var(--spacing)*4);--tw-border-spacing-y:calc(var(--spacing)*4);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-5{--tw-border-spacing-x:calc(var(--spacing)*5);--tw-border-spacing-y:calc(var(--spacing)*5);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-6{--tw-border-spacing-x:calc(var(--spacing)*6);--tw-border-spacing-y:calc(var(--spacing)*6);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.border-spacing-8{--tw-border-spacing-x:calc(var(--spacing)*8);--tw-border-spacing-y:calc(var(--spacing)*8);border-spacing:var(--tw-border-spacing-x)var(--tw-border-spacing-y)}.origin-bottom{transform-origin:bottom}.origin-bottom-left{transform-origin:0 100%}.origin-bottom-right{transform-origin:100% 100%}.origin-center{transform-origin:50%}.origin-left{transform-origin:0}.origin-right{transform-origin:100%}.origin-top{transform-origin:top}.origin-top-left{transform-origin:0 0}.origin-top-right{transform-origin:100% 0}.-translate-x-1{--tw-translate-x:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-2{--tw-translate-x:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-3{--tw-translate-x:calc(var(--spacing)*-3);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-4{--tw-translate-x:calc(var(--spacing)*-4);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-x-full{--tw-translate-x:-100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-0{--tw-translate-x:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1{--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/2{--tw-translate-x: 50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/3{--tw-translate-x:calc(1/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-1\/4{--tw-translate-x: 25% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-2{--tw-translate-x:calc(var(--spacing)*2);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-2\/3{--tw-translate-x:calc(2/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-3{--tw-translate-x:calc(var(--spacing)*3);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-3\/4{--tw-translate-x: 75% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-5{--tw-translate-x:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-6{--tw-translate-x:calc(var(--spacing)*6);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-8{--tw-translate-x:calc(var(--spacing)*8);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-10{--tw-translate-x:calc(var(--spacing)*10);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-12{--tw-translate-x:calc(var(--spacing)*12);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-x-full{--tw-translate-x:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1{--tw-translate-y:calc(var(--spacing)*-1);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-2{--tw-translate-y:calc(var(--spacing)*-2);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-3{--tw-translate-y:calc(var(--spacing)*-3);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-4{--tw-translate-y:calc(var(--spacing)*-4);translate:var(--tw-translate-x)var(--tw-translate-y)}.-translate-y-full{--tw-translate-y:-100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-0{--tw-translate-y:calc(var(--spacing)*0);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/2{--tw-translate-y: 50% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/3{--tw-translate-y:calc(1/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-1\/4{--tw-translate-y: 25% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-2{--tw-translate-y:calc(var(--spacing)*2);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-2\/3{--tw-translate-y:calc(2/3*100%);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-3{--tw-translate-y:calc(var(--spacing)*3);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-3\/4{--tw-translate-y: 75% ;translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-4{--tw-translate-y:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-5{--tw-translate-y:calc(var(--spacing)*5);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-6{--tw-translate-y:calc(var(--spacing)*6);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-8{--tw-translate-y:calc(var(--spacing)*8);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-10{--tw-translate-y:calc(var(--spacing)*10);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-12{--tw-translate-y:calc(var(--spacing)*12);translate:var(--tw-translate-x)var(--tw-translate-y)}.translate-y-full{--tw-translate-y:100%;translate:var(--tw-translate-x)var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-50{--tw-scale-x:50%;--tw-scale-y:50%;--tw-scale-z:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-75{--tw-scale-x:75%;--tw-scale-y:75%;--tw-scale-z:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-90{--tw-scale-x:90%;--tw-scale-y:90%;--tw-scale-z:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-95{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-105{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-150{--tw-scale-x:150%;--tw-scale-y:150%;--tw-scale-z:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-0{--tw-scale-x:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-50{--tw-scale-x:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-75{--tw-scale-x:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-90{--tw-scale-x:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-95{--tw-scale-x:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-100{--tw-scale-x:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-105{--tw-scale-x:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-110{--tw-scale-x:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-125{--tw-scale-x:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-x-150{--tw-scale-x:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-0{--tw-scale-y:0%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-50{--tw-scale-y:50%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-75{--tw-scale-y:75%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-90{--tw-scale-y:90%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-95{--tw-scale-y:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-100{--tw-scale-y:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-105{--tw-scale-y:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-110{--tw-scale-y:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-125{--tw-scale-y:125%;scale:var(--tw-scale-x)var(--tw-scale-y)}.scale-y-150{--tw-scale-y:150%;scale:var(--tw-scale-x)var(--tw-scale-y)}.-rotate-1{rotate:-1deg}.-rotate-2{rotate:-2deg}.-rotate-3{rotate:-3deg}.-rotate-6{rotate:-6deg}.-rotate-12{rotate:-12deg}.-rotate-45{rotate:-45deg}.-rotate-90{rotate:-90deg}.-rotate-180{rotate:-180deg}.rotate-0{rotate:none}.rotate-1{rotate:1deg}.rotate-2{rotate:2deg}.rotate-3{rotate:3deg}.rotate-6{rotate:6deg}.rotate-12{rotate:12deg}.rotate-45{rotate:45deg}.rotate-90{rotate:90deg}.rotate-180{rotate:180deg}.-skew-x-1{--tw-skew-x:skewX( -1deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-2{--tw-skew-x:skewX( -2deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-3{--tw-skew-x:skewX( -3deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-6{--tw-skew-x:skewX( -6deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-x-12{--tw-skew-x:skewX( -12deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-0{--tw-skew-x:skewX(0deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-1{--tw-skew-x:skewX(1deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-2{--tw-skew-x:skewX(2deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-3{--tw-skew-x:skewX(3deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-6{--tw-skew-x:skewX(6deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-x-12{--tw-skew-x:skewX(12deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-1{--tw-skew-y:skewY( -1deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-2{--tw-skew-y:skewY( -2deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-3{--tw-skew-y:skewY( -3deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-6{--tw-skew-y:skewY( -6deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.-skew-y-12{--tw-skew-y:skewY( -12deg );transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-0{--tw-skew-y:skewY(0deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-1{--tw-skew-y:skewY(1deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-2{--tw-skew-y:skewY(2deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-3{--tw-skew-y:skewY(3deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-6{--tw-skew-y:skewY(6deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.skew-y-12{--tw-skew-y:skewY(12deg);transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform,.transform-cpu{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0)var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.transform-none{transform:none}.animate-\[loading_1\.5s_ease-in-out_infinite\]{animation:1.5s ease-in-out infinite loading}.animate-bounce{animation:var(--animate-bounce)}.animate-none{animation:none}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-alias{cursor:alias}.cursor-all-scroll{cursor:all-scroll}.cursor-auto{cursor:auto}.cursor-cell{cursor:cell}.cursor-col-resize{cursor:col-resize}.cursor-context-menu{cursor:context-menu}.cursor-copy{cursor:copy}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-e-resize{cursor:e-resize}.cursor-ew-resize{cursor:ew-resize}.cursor-grab{cursor:grab}.cursor-grabbing{cursor:grabbing}.cursor-help{cursor:help}.cursor-move{cursor:move}.cursor-n-resize{cursor:n-resize}.cursor-ne-resize{cursor:ne-resize}.cursor-nesw-resize{cursor:nesw-resize}.cursor-no-drop{cursor:no-drop}.cursor-none{cursor:none}.cursor-not-allowed{cursor:not-allowed}.cursor-ns-resize{cursor:ns-resize}.cursor-nw-resize{cursor:nw-resize}.cursor-nwse-resize{cursor:nwse-resize}.cursor-pointer{cursor:pointer}.cursor-progress{cursor:progress}.cursor-row-resize{cursor:row-resize}.cursor-s-resize{cursor:s-resize}.cursor-se-resize{cursor:se-resize}.cursor-sw-resize{cursor:sw-resize}.cursor-text{cursor:text}.cursor-vertical-text{cursor:vertical-text}.cursor-w-resize{cursor:w-resize}.cursor-wait{cursor:wait}.cursor-zoom-in{cursor:zoom-in}.cursor-zoom-out{cursor:zoom-out}.touch-pan-left{--tw-pan-x:pan-left;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-right{--tw-pan-x:pan-right;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-x{--tw-pan-x:pan-x;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-down{--tw-pan-y:pan-down;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-up{--tw-pan-y:pan-up;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pan-y{--tw-pan-y:pan-y;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,)var(--tw-pan-y,)var(--tw-pinch-zoom,)}.touch-auto{touch-action:auto}.touch-manipulation{touch-action:manipulation}.touch-none{touch-action:none}.resize{resize:both}.resize-none{resize:none}.resize-x{resize:horizontal}.resize-y{resize:vertical}.snap-both{scroll-snap-type:both var(--tw-scroll-snap-strictness)}.snap-none{scroll-snap-type:none}.snap-x{scroll-snap-type:x var(--tw-scroll-snap-strictness)}.snap-y{scroll-snap-type:y var(--tw-scroll-snap-strictness)}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-align-none{scroll-snap-align:none}.snap-center{scroll-snap-align:center}.snap-end{scroll-snap-align:end}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.snap-normal{scroll-snap-stop:normal}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.\[appearance\:textfield\]{appearance:textfield}.appearance-auto{appearance:auto}.appearance-none{appearance:none}.columns-1{columns:1}.columns-2{columns:2}.columns-2xl{columns:var(--container-2xl)}.columns-2xs{columns:var(--container-2xs)}.columns-3{columns:3}.columns-3xl{columns:var(--container-3xl)}.columns-3xs{columns:var(--container-3xs)}.columns-4{columns:4}.columns-4xl{columns:var(--container-4xl)}.columns-5{columns:5}.columns-5xl{columns:var(--container-5xl)}.columns-6{columns:6}.columns-6xl{columns:var(--container-6xl)}.columns-7{columns:7}.columns-7xl{columns:var(--container-7xl)}.columns-8{columns:8}.columns-9{columns:9}.columns-10{columns:10}.columns-11{columns:11}.columns-12{columns:12}.columns-auto{columns:auto}.columns-lg{columns:var(--container-lg)}.columns-md{columns:var(--container-md)}.columns-sm{columns:var(--container-sm)}.columns-xl{columns:var(--container-xl)}.columns-xs{columns:var(--container-xs)}.break-before-all{break-before:all}.break-before-auto{break-before:auto}.break-before-avoid{break-before:avoid}.break-before-avoid-page{break-before:avoid-page}.break-before-column{break-before:column}.break-before-left{break-before:left}.break-before-page{break-before:page}.break-before-right{break-before:right}.break-inside-auto{break-inside:auto}.break-inside-avoid{break-inside:avoid}.break-inside-avoid-column{break-inside:avoid-column}.break-inside-avoid-page{break-inside:avoid-page}.break-after-all{break-after:all}.break-after-auto{break-after:auto}.break-after-avoid{break-after:avoid}.break-after-avoid-page{break-after:avoid-page}.break-after-column{break-after:column}.break-after-left{break-after:left}.break-after-page{break-after:page}.break-after-right{break-after:right}.auto-cols-\[calc\(25\%_-_12px\)\]{grid-auto-columns:calc(25% - 12px)}.auto-cols-\[calc\(33\.333\%_-_11px\)\]{grid-auto-columns:calc(33.333% - 11px)}.auto-cols-\[calc\(50\%_-_8px\)\]{grid-auto-columns:calc(50% - 8px)}.auto-cols-auto{grid-auto-columns:auto}.auto-cols-fr{grid-auto-columns:minmax(0,1fr)}.auto-cols-max{grid-auto-columns:max-content}.auto-cols-min{grid-auto-columns:min-content}.grid-flow-col{grid-auto-flow:column}.grid-flow-col-dense{grid-auto-flow:column dense}.grid-flow-dense{grid-auto-flow:dense}.grid-flow-row{grid-auto-flow:row}.grid-flow-row-dense{grid-auto-flow:dense}.auto-rows-auto{grid-auto-rows:auto}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.auto-rows-max{grid-auto-rows:max-content}.auto-rows-min{grid-auto-rows:min-content}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.grid-cols-10{grid-template-columns:repeat(10,minmax(0,1fr))}.grid-cols-11{grid-template-columns:repeat(11,minmax(0,1fr))}.grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.grid-cols-\[\.\.\.\]{grid-template-columns:...}.grid-cols-\[60px_1fr\]{grid-template-columns:60px 1fr}.grid-cols-\[60px_1fr_160px\]{grid-template-columns:60px 1fr 160px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px\]{grid-template-columns:60px minmax(300px,1fr) 160px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px_100px\]{grid-template-columns:60px minmax(300px,1fr) 160px 100px}.grid-cols-\[60px_minmax\(300px\,1fr\)_160px_100px_70px\]{grid-template-columns:60px minmax(300px,1fr) 160px 100px 70px}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-around{place-content:space-around}.place-content-baseline{place-content:baseline start}.place-content-between{place-content:space-between}.place-content-center{place-content:center}.place-content-end{place-content:end}.place-content-evenly{place-content:space-evenly}.place-content-start{place-content:start}.place-content-stretch{place-content:stretch}.place-items-baseline{place-items:baseline}.place-items-center{place-items:center}.place-items-end{place-items:end}.place-items-start{place-items:start}.place-items-stretch{place-items:stretch stretch}.content-around{align-content:space-around}.content-between{align-content:space-between}.content-center{align-content:center}.content-end{align-content:flex-end}.content-evenly{align-content:space-evenly}.content-start{align-content:flex-start}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.justify-evenly{justify-content:space-evenly}.justify-start{justify-content:flex-start}.gap-0{gap:calc(var(--spacing)*0)}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-5{gap:calc(var(--spacing)*5)}.gap-6{gap:calc(var(--spacing)*6)}.gap-8{gap:calc(var(--spacing)*8)}.gap-10{gap:calc(var(--spacing)*10)}.gap-12{gap:calc(var(--spacing)*12)}.gap-16{gap:calc(var(--spacing)*16)}:where(.-space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*-1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*-1)*calc(1 - var(--tw-space-y-reverse)))}:where(.-space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*-2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*-2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1.5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1.5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*5)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}.gap-x-0{column-gap:calc(var(--spacing)*0)}.gap-x-1{column-gap:calc(var(--spacing)*1)}.gap-x-2{column-gap:calc(var(--spacing)*2)}.gap-x-3{column-gap:calc(var(--spacing)*3)}.gap-x-4{column-gap:calc(var(--spacing)*4)}.gap-x-5{column-gap:calc(var(--spacing)*5)}.gap-x-6{column-gap:calc(var(--spacing)*6)}.gap-x-8{column-gap:calc(var(--spacing)*8)}.gap-x-10{column-gap:calc(var(--spacing)*10)}.gap-x-12{column-gap:calc(var(--spacing)*12)}:where(.-space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*-1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*-1)*calc(1 - var(--tw-space-x-reverse)))}:where(.-space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*-2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*-2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-5>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*5)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*5)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.gap-y-0{row-gap:calc(var(--spacing)*0)}.gap-y-1{row-gap:calc(var(--spacing)*1)}.gap-y-2{row-gap:calc(var(--spacing)*2)}.gap-y-3{row-gap:calc(var(--spacing)*3)}.gap-y-4{row-gap:calc(var(--spacing)*4)}.gap-y-5{row-gap:calc(var(--spacing)*5)}.gap-y-6{row-gap:calc(var(--spacing)*6)}.gap-y-8{row-gap:calc(var(--spacing)*8)}.gap-y-10{row-gap:calc(var(--spacing)*10)}.gap-y-12{row-gap:calc(var(--spacing)*12)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px*var(--tw-divide-x-reverse));border-inline-end-width:calc(1px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-0>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(0px*var(--tw-divide-x-reverse));border-inline-end-width:calc(0px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-2>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(2px*var(--tw-divide-x-reverse));border-inline-end-width:calc(2px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-4>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(4px*var(--tw-divide-x-reverse));border-inline-end-width:calc(4px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-x-8>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(8px*var(--tw-divide-x-reverse));border-inline-end-width:calc(8px*calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px*var(--tw-divide-y-reverse));border-bottom-width:calc(1px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-0>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(0px*var(--tw-divide-y-reverse));border-bottom-width:calc(0px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-2>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(2px*var(--tw-divide-y-reverse));border-bottom-width:calc(2px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-4>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(4px*var(--tw-divide-y-reverse));border-bottom-width:calc(4px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-8>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(8px*var(--tw-divide-y-reverse));border-bottom-width:calc(8px*calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}:where(.divide-dashed>:not(:last-child)){--tw-border-style:dashed;border-style:dashed}:where(.divide-dotted>:not(:last-child)){--tw-border-style:dotted;border-style:dotted}:where(.divide-double>:not(:last-child)){--tw-border-style:double;border-style:double}:where(.divide-none>:not(:last-child)){--tw-border-style:none;border-style:none}:where(.divide-solid>:not(:last-child)){--tw-border-style:solid;border-style:solid}:where(.divide-black>:not(:last-child)){border-color:var(--color-black)}:where(.divide-current>:not(:last-child)){border-color:currentColor}:where(.divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}:where(.divide-inherit>:not(:last-child)){border-color:inherit}:where(.divide-slate-50>:not(:last-child)){border-color:var(--color-slate-50)}:where(.divide-slate-100>:not(:last-child)){border-color:var(--color-slate-100)}:where(.divide-slate-200>:not(:last-child)){border-color:var(--color-slate-200)}:where(.divide-slate-300>:not(:last-child)){border-color:var(--color-slate-300)}:where(.divide-slate-400>:not(:last-child)){border-color:var(--color-slate-400)}:where(.divide-slate-500>:not(:last-child)){border-color:var(--color-slate-500)}:where(.divide-slate-600>:not(:last-child)){border-color:var(--color-slate-600)}:where(.divide-slate-700>:not(:last-child)){border-color:var(--color-slate-700)}:where(.divide-slate-800>:not(:last-child)){border-color:var(--color-slate-800)}:where(.divide-slate-900>:not(:last-child)){border-color:var(--color-slate-900)}:where(.divide-transparent>:not(:last-child)){border-color:#0000}:where(.divide-white>:not(:last-child)){border-color:var(--color-white)}.place-self-auto{place-self:auto}.place-self-center{place-self:center}.place-self-end{place-self:end}.place-self-start{place-self:start}.place-self-stretch{place-self:stretch stretch}.self-auto{align-self:auto}.self-baseline{align-self:baseline}.self-center{align-self:center}.self-end{align-self:flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-clip{overflow:clip}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-visible{overflow:visible}.overflow-x-auto{overflow-x:auto}.overflow-x-clip{overflow-x:clip}.overflow-x-hidden{overflow-x:hidden}.overflow-x-scroll{overflow-x:scroll}.overflow-x-visible{overflow-x:visible}.overflow-y-auto{overflow-y:auto}.overflow-y-clip{overflow-y:clip}.overflow-y-hidden{overflow-y:hidden}.overflow-y-scroll{overflow-y:scroll}.overflow-y-visible{overflow-y:visible}.overscroll-auto{overscroll-behavior:auto}.overscroll-contain{overscroll-behavior:contain}.overscroll-none{overscroll-behavior:none}.overscroll-x-auto{overscroll-behavior-x:auto}.overscroll-x-contain{overscroll-behavior-x:contain}.overscroll-x-none{overscroll-behavior-x:none}.overscroll-y-auto{overscroll-behavior-y:auto}.overscroll-y-contain{overscroll-behavior-y:contain}.overscroll-y-none{overscroll-behavior-y:none}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-3xl{border-radius:var(--radius-3xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-none{border-radius:0}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-2xl{border-top-left-radius:var(--radius-2xl);border-top-right-radius:var(--radius-2xl)}.rounded-t-3xl{border-top-left-radius:var(--radius-3xl);border-top-right-radius:var(--radius-3xl)}.rounded-t-full{border-top-left-radius:3.40282e38px;border-top-right-radius:3.40282e38px}.rounded-t-lg{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.rounded-t-md{border-top-left-radius:var(--radius-md);border-top-right-radius:var(--radius-md)}.rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.rounded-t-sm{border-top-left-radius:var(--radius-sm);border-top-right-radius:var(--radius-sm)}.rounded-t-xl{border-top-left-radius:var(--radius-xl);border-top-right-radius:var(--radius-xl)}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-l-2xl{border-top-left-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-l-3xl{border-top-left-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-l-full{border-top-left-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-l-lg{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-l-md{border-top-left-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-l-none{border-top-left-radius:0;border-bottom-left-radius:0}.rounded-l-sm{border-top-left-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-l-xl{border-top-left-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-tl{border-top-left-radius:.25rem}.rounded-tl-2xl{border-top-left-radius:var(--radius-2xl)}.rounded-tl-3xl{border-top-left-radius:var(--radius-3xl)}.rounded-tl-full{border-top-left-radius:3.40282e38px}.rounded-tl-lg{border-top-left-radius:var(--radius-lg)}.rounded-tl-md{border-top-left-radius:var(--radius-md)}.rounded-tl-none{border-top-left-radius:0}.rounded-tl-sm{border-top-left-radius:var(--radius-sm)}.rounded-tl-xl{border-top-left-radius:var(--radius-xl)}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-r-2xl{border-top-right-radius:var(--radius-2xl);border-bottom-right-radius:var(--radius-2xl)}.rounded-r-3xl{border-top-right-radius:var(--radius-3xl);border-bottom-right-radius:var(--radius-3xl)}.rounded-r-full{border-top-right-radius:3.40282e38px;border-bottom-right-radius:3.40282e38px}.rounded-r-lg{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.rounded-r-none{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-r-sm{border-top-right-radius:var(--radius-sm);border-bottom-right-radius:var(--radius-sm)}.rounded-r-xl{border-top-right-radius:var(--radius-xl);border-bottom-right-radius:var(--radius-xl)}.rounded-tr{border-top-right-radius:.25rem}.rounded-tr-2xl{border-top-right-radius:var(--radius-2xl)}.rounded-tr-3xl{border-top-right-radius:var(--radius-3xl)}.rounded-tr-full{border-top-right-radius:3.40282e38px}.rounded-tr-lg{border-top-right-radius:var(--radius-lg)}.rounded-tr-md{border-top-right-radius:var(--radius-md)}.rounded-tr-none{border-top-right-radius:0}.rounded-tr-sm{border-top-right-radius:var(--radius-sm)}.rounded-tr-xl{border-top-right-radius:var(--radius-xl)}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-b-2xl{border-bottom-right-radius:var(--radius-2xl);border-bottom-left-radius:var(--radius-2xl)}.rounded-b-3xl{border-bottom-right-radius:var(--radius-3xl);border-bottom-left-radius:var(--radius-3xl)}.rounded-b-full{border-bottom-right-radius:3.40282e38px;border-bottom-left-radius:3.40282e38px}.rounded-b-lg{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.rounded-b-md{border-bottom-right-radius:var(--radius-md);border-bottom-left-radius:var(--radius-md)}.rounded-b-none{border-bottom-right-radius:0;border-bottom-left-radius:0}.rounded-b-sm{border-bottom-right-radius:var(--radius-sm);border-bottom-left-radius:var(--radius-sm)}.rounded-b-xl{border-bottom-right-radius:var(--radius-xl);border-bottom-left-radius:var(--radius-xl)}.rounded-br{border-bottom-right-radius:.25rem}.rounded-br-2xl{border-bottom-right-radius:var(--radius-2xl)}.rounded-br-3xl{border-bottom-right-radius:var(--radius-3xl)}.rounded-br-full{border-bottom-right-radius:3.40282e38px}.rounded-br-lg{border-bottom-right-radius:var(--radius-lg)}.rounded-br-md{border-bottom-right-radius:var(--radius-md)}.rounded-br-none{border-bottom-right-radius:0}.rounded-br-sm{border-bottom-right-radius:var(--radius-sm)}.rounded-br-xl{border-bottom-right-radius:var(--radius-xl)}.rounded-bl{border-bottom-left-radius:.25rem}.rounded-bl-2xl{border-bottom-left-radius:var(--radius-2xl)}.rounded-bl-3xl{border-bottom-left-radius:var(--radius-3xl)}.rounded-bl-full{border-bottom-left-radius:3.40282e38px}.rounded-bl-lg{border-bottom-left-radius:var(--radius-lg)}.rounded-bl-md{border-bottom-left-radius:var(--radius-md)}.rounded-bl-none{border-bottom-left-radius:0}.rounded-bl-sm{border-bottom-left-radius:var(--radius-sm)}.rounded-bl-xl{border-bottom-left-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-8{border-style:var(--tw-border-style);border-width:8px}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-x-0{border-inline-style:var(--tw-border-style);border-inline-width:0}.border-x-2{border-inline-style:var(--tw-border-style);border-inline-width:2px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-y-0{border-block-style:var(--tw-border-style);border-block-width:0}.border-y-2{border-block-style:var(--tw-border-style);border-block-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-t-0{border-top-style:var(--tw-border-style);border-top-width:0}.border-t-2{border-top-style:var(--tw-border-style);border-top-width:2px}.border-t-4{border-top-style:var(--tw-border-style);border-top-width:4px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-r-0{border-right-style:var(--tw-border-style);border-right-width:0}.border-r-2{border-right-style:var(--tw-border-style);border-right-width:2px}.border-r-4{border-right-style:var(--tw-border-style);border-right-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-0{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-b-4{border-bottom-style:var(--tw-border-style);border-bottom-width:4px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-0{border-left-style:var(--tw-border-style);border-left-width:0}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-l-4{border-left-style:var(--tw-border-style);border-left-width:4px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-hidden{--tw-border-style:hidden;border-style:hidden}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-amber-50{border-color:var(--color-amber-50)}.border-amber-100{border-color:var(--color-amber-100)}.border-amber-200{border-color:var(--color-amber-200)}.border-amber-300{border-color:var(--color-amber-300)}.border-amber-400{border-color:var(--color-amber-400)}.border-amber-500{border-color:var(--color-amber-500)}.border-amber-600{border-color:var(--color-amber-600)}.border-amber-700{border-color:var(--color-amber-700)}.border-amber-800{border-color:var(--color-amber-800)}.border-amber-900{border-color:var(--color-amber-900)}.border-amber-950{border-color:var(--color-amber-950)}.border-black{border-color:var(--color-black)}.border-blue-50{border-color:var(--color-blue-50)}.border-blue-100{border-color:var(--color-blue-100)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-300{border-color:var(--color-blue-300)}.border-blue-400{border-color:var(--color-blue-400)}.border-blue-500{border-color:var(--color-blue-500)}.border-blue-600{border-color:var(--color-blue-600)}.border-blue-700{border-color:var(--color-blue-700)}.border-blue-800{border-color:var(--color-blue-800)}.border-blue-900{border-color:var(--color-blue-900)}.border-blue-950{border-color:var(--color-blue-950)}.border-current{border-color:currentColor}.border-cyan-50{border-color:var(--color-cyan-50)}.border-cyan-100{border-color:var(--color-cyan-100)}.border-cyan-200{border-color:var(--color-cyan-200)}.border-cyan-300{border-color:var(--color-cyan-300)}.border-cyan-400{border-color:var(--color-cyan-400)}.border-cyan-500{border-color:var(--color-cyan-500)}.border-cyan-600{border-color:var(--color-cyan-600)}.border-cyan-700{border-color:var(--color-cyan-700)}.border-cyan-800{border-color:var(--color-cyan-800)}.border-cyan-900{border-color:var(--color-cyan-900)}.border-cyan-950{border-color:var(--color-cyan-950)}.border-emerald-50{border-color:var(--color-emerald-50)}.border-emerald-100{border-color:var(--color-emerald-100)}.border-emerald-200{border-color:var(--color-emerald-200)}.border-emerald-300{border-color:var(--color-emerald-300)}.border-emerald-400{border-color:var(--color-emerald-400)}.border-emerald-500{border-color:var(--color-emerald-500)}.border-emerald-600{border-color:var(--color-emerald-600)}.border-emerald-700{border-color:var(--color-emerald-700)}.border-emerald-800{border-color:var(--color-emerald-800)}.border-emerald-900{border-color:var(--color-emerald-900)}.border-emerald-950{border-color:var(--color-emerald-950)}.border-fuchsia-50{border-color:var(--color-fuchsia-50)}.border-fuchsia-100{border-color:var(--color-fuchsia-100)}.border-fuchsia-200{border-color:var(--color-fuchsia-200)}.border-fuchsia-300{border-color:var(--color-fuchsia-300)}.border-fuchsia-400{border-color:var(--color-fuchsia-400)}.border-fuchsia-500{border-color:var(--color-fuchsia-500)}.border-fuchsia-600{border-color:var(--color-fuchsia-600)}.border-fuchsia-700{border-color:var(--color-fuchsia-700)}.border-fuchsia-800{border-color:var(--color-fuchsia-800)}.border-fuchsia-900{border-color:var(--color-fuchsia-900)}.border-fuchsia-950{border-color:var(--color-fuchsia-950)}.border-gray-50{border-color:var(--color-gray-50)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-400{border-color:var(--color-gray-400)}.border-gray-500{border-color:var(--color-gray-500)}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-900{border-color:var(--color-gray-900)}.border-gray-950{border-color:var(--color-gray-950)}.border-green-50{border-color:var(--color-green-50)}.border-green-100{border-color:var(--color-green-100)}.border-green-200{border-color:var(--color-green-200)}.border-green-300{border-color:var(--color-green-300)}.border-green-400{border-color:var(--color-green-400)}.border-green-500{border-color:var(--color-green-500)}.border-green-600{border-color:var(--color-green-600)}.border-green-700{border-color:var(--color-green-700)}.border-green-800{border-color:var(--color-green-800)}.border-green-900{border-color:var(--color-green-900)}.border-green-950{border-color:var(--color-green-950)}.border-indigo-50{border-color:var(--color-indigo-50)}.border-indigo-100{border-color:var(--color-indigo-100)}.border-indigo-200{border-color:var(--color-indigo-200)}.border-indigo-300{border-color:var(--color-indigo-300)}.border-indigo-400{border-color:var(--color-indigo-400)}.border-indigo-500{border-color:var(--color-indigo-500)}.border-indigo-600{border-color:var(--color-indigo-600)}.border-indigo-700{border-color:var(--color-indigo-700)}.border-indigo-800{border-color:var(--color-indigo-800)}.border-indigo-900{border-color:var(--color-indigo-900)}.border-indigo-950{border-color:var(--color-indigo-950)}.border-inherit{border-color:inherit}.border-lime-50{border-color:var(--color-lime-50)}.border-lime-100{border-color:var(--color-lime-100)}.border-lime-200{border-color:var(--color-lime-200)}.border-lime-300{border-color:var(--color-lime-300)}.border-lime-400{border-color:var(--color-lime-400)}.border-lime-500{border-color:var(--color-lime-500)}.border-lime-600{border-color:var(--color-lime-600)}.border-lime-700{border-color:var(--color-lime-700)}.border-lime-800{border-color:var(--color-lime-800)}.border-lime-900{border-color:var(--color-lime-900)}.border-lime-950{border-color:var(--color-lime-950)}.border-neutral-50{border-color:var(--color-neutral-50)}.border-neutral-100{border-color:var(--color-neutral-100)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-neutral-300{border-color:var(--color-neutral-300)}.border-neutral-400{border-color:var(--color-neutral-400)}.border-neutral-500{border-color:var(--color-neutral-500)}.border-neutral-600{border-color:var(--color-neutral-600)}.border-neutral-700{border-color:var(--color-neutral-700)}.border-neutral-800{border-color:var(--color-neutral-800)}.border-neutral-900{border-color:var(--color-neutral-900)}.border-neutral-950{border-color:var(--color-neutral-950)}.border-orange-50{border-color:var(--color-orange-50)}.border-orange-100{border-color:var(--color-orange-100)}.border-orange-200{border-color:var(--color-orange-200)}.border-orange-300{border-color:var(--color-orange-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-orange-500{border-color:var(--color-orange-500)}.border-orange-600{border-color:var(--color-orange-600)}.border-orange-700{border-color:var(--color-orange-700)}.border-orange-800{border-color:var(--color-orange-800)}.border-orange-900{border-color:var(--color-orange-900)}.border-orange-950{border-color:var(--color-orange-950)}.border-pink-50{border-color:var(--color-pink-50)}.border-pink-100{border-color:var(--color-pink-100)}.border-pink-200{border-color:var(--color-pink-200)}.border-pink-300{border-color:var(--color-pink-300)}.border-pink-400{border-color:var(--color-pink-400)}.border-pink-500{border-color:var(--color-pink-500)}.border-pink-600{border-color:var(--color-pink-600)}.border-pink-700{border-color:var(--color-pink-700)}.border-pink-800{border-color:var(--color-pink-800)}.border-pink-900{border-color:var(--color-pink-900)}.border-pink-950{border-color:var(--color-pink-950)}.border-primary-500{border-color:var(--color-primary-500)}.border-purple-50{border-color:var(--color-purple-50)}.border-purple-100{border-color:var(--color-purple-100)}.border-purple-200{border-color:var(--color-purple-200)}.border-purple-300{border-color:var(--color-purple-300)}.border-purple-400{border-color:var(--color-purple-400)}.border-purple-500{border-color:var(--color-purple-500)}.border-purple-600{border-color:var(--color-purple-600)}.border-purple-700{border-color:var(--color-purple-700)}.border-purple-800{border-color:var(--color-purple-800)}.border-purple-900{border-color:var(--color-purple-900)}.border-purple-950{border-color:var(--color-purple-950)}.border-red-50{border-color:var(--color-red-50)}.border-red-100{border-color:var(--color-red-100)}.border-red-200{border-color:var(--color-red-200)}.border-red-300{border-color:var(--color-red-300)}.border-red-400{border-color:var(--color-red-400)}.border-red-500{border-color:var(--color-red-500)}.border-red-600{border-color:var(--color-red-600)}.border-red-700{border-color:var(--color-red-700)}.border-red-800{border-color:var(--color-red-800)}.border-red-900{border-color:var(--color-red-900)}.border-red-950{border-color:var(--color-red-950)}.border-rose-50{border-color:var(--color-rose-50)}.border-rose-100{border-color:var(--color-rose-100)}.border-rose-200{border-color:var(--color-rose-200)}.border-rose-300{border-color:var(--color-rose-300)}.border-rose-400{border-color:var(--color-rose-400)}.border-rose-500{border-color:var(--color-rose-500)}.border-rose-600{border-color:var(--color-rose-600)}.border-rose-700{border-color:var(--color-rose-700)}.border-rose-800{border-color:var(--color-rose-800)}.border-rose-900{border-color:var(--color-rose-900)}.border-rose-950{border-color:var(--color-rose-950)}.border-sky-50{border-color:var(--color-sky-50)}.border-sky-100{border-color:var(--color-sky-100)}.border-sky-200{border-color:var(--color-sky-200)}.border-sky-300{border-color:var(--color-sky-300)}.border-sky-400{border-color:var(--color-sky-400)}.border-sky-500{border-color:var(--color-sky-500)}.border-sky-600{border-color:var(--color-sky-600)}.border-sky-700{border-color:var(--color-sky-700)}.border-sky-800{border-color:var(--color-sky-800)}.border-sky-900{border-color:var(--color-sky-900)}.border-sky-950{border-color:var(--color-sky-950)}.border-slate-50{border-color:var(--color-slate-50)}.border-slate-100{border-color:var(--color-slate-100)}.border-slate-200{border-color:var(--color-slate-200)}.border-slate-300{border-color:var(--color-slate-300)}.border-slate-400{border-color:var(--color-slate-400)}.border-slate-500{border-color:var(--color-slate-500)}.border-slate-600{border-color:var(--color-slate-600)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.border-slate-900{border-color:var(--color-slate-900)}.border-slate-950{border-color:var(--color-slate-950)}.border-stone-50{border-color:var(--color-stone-50)}.border-stone-100{border-color:var(--color-stone-100)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-stone-400{border-color:var(--color-stone-400)}.border-stone-500{border-color:var(--color-stone-500)}.border-stone-600{border-color:var(--color-stone-600)}.border-stone-700{border-color:var(--color-stone-700)}.border-stone-800{border-color:var(--color-stone-800)}.border-stone-900{border-color:var(--color-stone-900)}.border-stone-950{border-color:var(--color-stone-950)}.border-teal-50{border-color:var(--color-teal-50)}.border-teal-100{border-color:var(--color-teal-100)}.border-teal-200{border-color:var(--color-teal-200)}.border-teal-300{border-color:var(--color-teal-300)}.border-teal-400{border-color:var(--color-teal-400)}.border-teal-500{border-color:var(--color-teal-500)}.border-teal-600{border-color:var(--color-teal-600)}.border-teal-700{border-color:var(--color-teal-700)}.border-teal-800{border-color:var(--color-teal-800)}.border-teal-900{border-color:var(--color-teal-900)}.border-teal-950{border-color:var(--color-teal-950)}.border-transparent{border-color:#0000}.border-violet-50{border-color:var(--color-violet-50)}.border-violet-100{border-color:var(--color-violet-100)}.border-violet-200{border-color:var(--color-violet-200)}.border-violet-300{border-color:var(--color-violet-300)}.border-violet-400{border-color:var(--color-violet-400)}.border-violet-500{border-color:var(--color-violet-500)}.border-violet-600{border-color:var(--color-violet-600)}.border-violet-700{border-color:var(--color-violet-700)}.border-violet-800{border-color:var(--color-violet-800)}.border-violet-900{border-color:var(--color-violet-900)}.border-violet-950{border-color:var(--color-violet-950)}.border-white{border-color:var(--color-white)}.border-white\/30{border-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.border-white\/30{border-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.border-yellow-50{border-color:var(--color-yellow-50)}.border-yellow-100{border-color:var(--color-yellow-100)}.border-yellow-200{border-color:var(--color-yellow-200)}.border-yellow-300{border-color:var(--color-yellow-300)}.border-yellow-400{border-color:var(--color-yellow-400)}.border-yellow-500{border-color:var(--color-yellow-500)}.border-yellow-600{border-color:var(--color-yellow-600)}.border-yellow-700{border-color:var(--color-yellow-700)}.border-yellow-800{border-color:var(--color-yellow-800)}.border-yellow-900{border-color:var(--color-yellow-900)}.border-yellow-950{border-color:var(--color-yellow-950)}.border-zinc-50{border-color:var(--color-zinc-50)}.border-zinc-100{border-color:var(--color-zinc-100)}.border-zinc-200{border-color:var(--color-zinc-200)}.border-zinc-300{border-color:var(--color-zinc-300)}.border-zinc-400{border-color:var(--color-zinc-400)}.border-zinc-500{border-color:var(--color-zinc-500)}.border-zinc-600{border-color:var(--color-zinc-600)}.border-zinc-700{border-color:var(--color-zinc-700)}.border-zinc-800{border-color:var(--color-zinc-800)}.border-zinc-900{border-color:var(--color-zinc-900)}.border-zinc-950{border-color:var(--color-zinc-950)}.border-t-blue-500{border-top-color:var(--color-blue-500)}.border-t-gray-900{border-top-color:var(--color-gray-900)}.border-t-white{border-top-color:var(--color-white)}.bg-\[\#03C75A\]{background-color:#03c75a}.bg-\[\#1877F2\]{background-color:#1877f2}.bg-\[\#FEE500\]{background-color:#fee500}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-200{background-color:var(--color-amber-200)}.bg-amber-300{background-color:var(--color-amber-300)}.bg-amber-400{background-color:var(--color-amber-400)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-700{background-color:var(--color-amber-700)}.bg-amber-800{background-color:var(--color-amber-800)}.bg-amber-900{background-color:var(--color-amber-900)}.bg-amber-950{background-color:var(--color-amber-950)}.bg-black{background-color:var(--color-black)}.bg-black\/0{background-color:#0000}@supports (color:color-mix(in lab,red,red)){.bg-black\/0{background-color:color-mix(in oklab,var(--color-black)0%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black)50%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/70{background-color:#eff6ffb3}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/70{background-color:color-mix(in oklab,var(--color-blue-50)70%,transparent)}}.bg-blue-100{background-color:var(--color-blue-100)}.bg-blue-200{background-color:var(--color-blue-200)}.bg-blue-300{background-color:var(--color-blue-300)}.bg-blue-400{background-color:var(--color-blue-400)}.bg-blue-500{background-color:var(--color-blue-500)}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab,red,red)){.bg-blue-500\/20{background-color:color-mix(in oklab,var(--color-blue-500)20%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-blue-800{background-color:var(--color-blue-800)}.bg-blue-900{background-color:var(--color-blue-900)}.bg-blue-950{background-color:var(--color-blue-950)}.bg-current{background-color:currentColor}.bg-cyan-50{background-color:var(--color-cyan-50)}.bg-cyan-100{background-color:var(--color-cyan-100)}.bg-cyan-200{background-color:var(--color-cyan-200)}.bg-cyan-300{background-color:var(--color-cyan-300)}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-cyan-500{background-color:var(--color-cyan-500)}.bg-cyan-600{background-color:var(--color-cyan-600)}.bg-cyan-700{background-color:var(--color-cyan-700)}.bg-cyan-800{background-color:var(--color-cyan-800)}.bg-cyan-900{background-color:var(--color-cyan-900)}.bg-cyan-950{background-color:var(--color-cyan-950)}.bg-emerald-50{background-color:var(--color-emerald-50)}.bg-emerald-100{background-color:var(--color-emerald-100)}.bg-emerald-200{background-color:var(--color-emerald-200)}.bg-emerald-300{background-color:var(--color-emerald-300)}.bg-emerald-400{background-color:var(--color-emerald-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-emerald-600{background-color:var(--color-emerald-600)}.bg-emerald-700{background-color:var(--color-emerald-700)}.bg-emerald-800{background-color:var(--color-emerald-800)}.bg-emerald-900{background-color:var(--color-emerald-900)}.bg-emerald-950{background-color:var(--color-emerald-950)}.bg-fuchsia-50{background-color:var(--color-fuchsia-50)}.bg-fuchsia-100{background-color:var(--color-fuchsia-100)}.bg-fuchsia-200{background-color:var(--color-fuchsia-200)}.bg-fuchsia-300{background-color:var(--color-fuchsia-300)}.bg-fuchsia-400{background-color:var(--color-fuchsia-400)}.bg-fuchsia-500{background-color:var(--color-fuchsia-500)}.bg-fuchsia-600{background-color:var(--color-fuchsia-600)}.bg-fuchsia-700{background-color:var(--color-fuchsia-700)}.bg-fuchsia-800{background-color:var(--color-fuchsia-800)}.bg-fuchsia-900{background-color:var(--color-fuchsia-900)}.bg-fuchsia-950{background-color:var(--color-fuchsia-950)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-100\/70{background-color:#f3f4f6b3}@supports (color:color-mix(in lab,red,red)){.bg-gray-100\/70{background-color:color-mix(in oklab,var(--color-gray-100)70%,transparent)}}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-400{background-color:var(--color-gray-400)}.bg-gray-500{background-color:var(--color-gray-500)}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.bg-gray-900{background-color:var(--color-gray-900)}.bg-gray-900\/40{background-color:#10182866}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/40{background-color:color-mix(in oklab,var(--color-gray-900)40%,transparent)}}.bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.bg-gray-950{background-color:var(--color-gray-950)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-green-200{background-color:var(--color-green-200)}.bg-green-300{background-color:var(--color-green-300)}.bg-green-400{background-color:var(--color-green-400)}.bg-green-500{background-color:var(--color-green-500)}.bg-green-600{background-color:var(--color-green-600)}.bg-green-700{background-color:var(--color-green-700)}.bg-green-800{background-color:var(--color-green-800)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-950{background-color:var(--color-green-950)}.bg-indigo-50{background-color:var(--color-indigo-50)}.bg-indigo-100{background-color:var(--color-indigo-100)}.bg-indigo-200{background-color:var(--color-indigo-200)}.bg-indigo-300{background-color:var(--color-indigo-300)}.bg-indigo-400{background-color:var(--color-indigo-400)}.bg-indigo-500{background-color:var(--color-indigo-500)}.bg-indigo-600{background-color:var(--color-indigo-600)}.bg-indigo-700{background-color:var(--color-indigo-700)}.bg-indigo-800{background-color:var(--color-indigo-800)}.bg-indigo-900{background-color:var(--color-indigo-900)}.bg-indigo-950{background-color:var(--color-indigo-950)}.bg-inherit{background-color:inherit}.bg-lime-50{background-color:var(--color-lime-50)}.bg-lime-100{background-color:var(--color-lime-100)}.bg-lime-200{background-color:var(--color-lime-200)}.bg-lime-300{background-color:var(--color-lime-300)}.bg-lime-400{background-color:var(--color-lime-400)}.bg-lime-500{background-color:var(--color-lime-500)}.bg-lime-600{background-color:var(--color-lime-600)}.bg-lime-700{background-color:var(--color-lime-700)}.bg-lime-800{background-color:var(--color-lime-800)}.bg-lime-900{background-color:var(--color-lime-900)}.bg-lime-950{background-color:var(--color-lime-950)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-neutral-500{background-color:var(--color-neutral-500)}.bg-neutral-600{background-color:var(--color-neutral-600)}.bg-neutral-700{background-color:var(--color-neutral-700)}.bg-neutral-800{background-color:var(--color-neutral-800)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-neutral-950{background-color:var(--color-neutral-950)}.bg-orange-50{background-color:var(--color-orange-50)}.bg-orange-100{background-color:var(--color-orange-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-orange-300{background-color:var(--color-orange-300)}.bg-orange-400{background-color:var(--color-orange-400)}.bg-orange-500{background-color:var(--color-orange-500)}.bg-orange-600{background-color:var(--color-orange-600)}.bg-orange-700{background-color:var(--color-orange-700)}.bg-orange-800{background-color:var(--color-orange-800)}.bg-orange-900{background-color:var(--color-orange-900)}.bg-orange-950{background-color:var(--color-orange-950)}.bg-pink-50{background-color:var(--color-pink-50)}.bg-pink-100{background-color:var(--color-pink-100)}.bg-pink-200{background-color:var(--color-pink-200)}.bg-pink-300{background-color:var(--color-pink-300)}.bg-pink-400{background-color:var(--color-pink-400)}.bg-pink-500{background-color:var(--color-pink-500)}.bg-pink-600{background-color:var(--color-pink-600)}.bg-pink-700{background-color:var(--color-pink-700)}.bg-pink-800{background-color:var(--color-pink-800)}.bg-pink-900{background-color:var(--color-pink-900)}.bg-pink-950{background-color:var(--color-pink-950)}.bg-primary-50{background-color:var(--color-primary-50)}.bg-primary-100{background-color:var(--color-primary-100)}.bg-primary-600{background-color:var(--color-primary-600)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-purple-100{background-color:var(--color-purple-100)}.bg-purple-200{background-color:var(--color-purple-200)}.bg-purple-300{background-color:var(--color-purple-300)}.bg-purple-400{background-color:var(--color-purple-400)}.bg-purple-500{background-color:var(--color-purple-500)}.bg-purple-500\/80{background-color:#ac4bffcc}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/80{background-color:color-mix(in oklab,var(--color-purple-500)80%,transparent)}}.bg-purple-600{background-color:var(--color-purple-600)}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-800{background-color:var(--color-purple-800)}.bg-purple-900{background-color:var(--color-purple-900)}.bg-purple-950{background-color:var(--color-purple-950)}.bg-red-50{background-color:var(--color-red-50)}.bg-red-50\/30{background-color:#fef2f24d}@supports (color:color-mix(in lab,red,red)){.bg-red-50\/30{background-color:color-mix(in oklab,var(--color-red-50)30%,transparent)}}.bg-red-100{background-color:var(--color-red-100)}.bg-red-200{background-color:var(--color-red-200)}.bg-red-300{background-color:var(--color-red-300)}.bg-red-400{background-color:var(--color-red-400)}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/50{background-color:#fb2c3680}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/50{background-color:color-mix(in oklab,var(--color-red-500)50%,transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-700{background-color:var(--color-red-700)}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-950{background-color:var(--color-red-950)}.bg-rose-50{background-color:var(--color-rose-50)}.bg-rose-100{background-color:var(--color-rose-100)}.bg-rose-200{background-color:var(--color-rose-200)}.bg-rose-300{background-color:var(--color-rose-300)}.bg-rose-400{background-color:var(--color-rose-400)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-600{background-color:var(--color-rose-600)}.bg-rose-700{background-color:var(--color-rose-700)}.bg-rose-800{background-color:var(--color-rose-800)}.bg-rose-900{background-color:var(--color-rose-900)}.bg-rose-950{background-color:var(--color-rose-950)}.bg-sky-50{background-color:var(--color-sky-50)}.bg-sky-100{background-color:var(--color-sky-100)}.bg-sky-200{background-color:var(--color-sky-200)}.bg-sky-300{background-color:var(--color-sky-300)}.bg-sky-400{background-color:var(--color-sky-400)}.bg-sky-500{background-color:var(--color-sky-500)}.bg-sky-600{background-color:var(--color-sky-600)}.bg-sky-700{background-color:var(--color-sky-700)}.bg-sky-800{background-color:var(--color-sky-800)}.bg-sky-900{background-color:var(--color-sky-900)}.bg-sky-950{background-color:var(--color-sky-950)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-100{background-color:var(--color-slate-100)}.bg-slate-200{background-color:var(--color-slate-200)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-slate-400{background-color:var(--color-slate-400)}.bg-slate-500{background-color:var(--color-slate-500)}.bg-slate-600{background-color:var(--color-slate-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-slate-800{background-color:var(--color-slate-800)}.bg-slate-900{background-color:var(--color-slate-900)}.bg-slate-950{background-color:var(--color-slate-950)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-stone-200{background-color:var(--color-stone-200)}.bg-stone-300{background-color:var(--color-stone-300)}.bg-stone-400{background-color:var(--color-stone-400)}.bg-stone-500{background-color:var(--color-stone-500)}.bg-stone-600{background-color:var(--color-stone-600)}.bg-stone-700{background-color:var(--color-stone-700)}.bg-stone-800{background-color:var(--color-stone-800)}.bg-stone-900{background-color:var(--color-stone-900)}.bg-stone-950{background-color:var(--color-stone-950)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-100{background-color:var(--color-teal-100)}.bg-teal-200{background-color:var(--color-teal-200)}.bg-teal-300{background-color:var(--color-teal-300)}.bg-teal-400{background-color:var(--color-teal-400)}.bg-teal-500{background-color:var(--color-teal-500)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-teal-700{background-color:var(--color-teal-700)}.bg-teal-800{background-color:var(--color-teal-800)}.bg-teal-900{background-color:var(--color-teal-900)}.bg-teal-950{background-color:var(--color-teal-950)}.bg-transparent{background-color:#0000}.bg-violet-50{background-color:var(--color-violet-50)}.bg-violet-100{background-color:var(--color-violet-100)}.bg-violet-200{background-color:var(--color-violet-200)}.bg-violet-300{background-color:var(--color-violet-300)}.bg-violet-400{background-color:var(--color-violet-400)}.bg-violet-500{background-color:var(--color-violet-500)}.bg-violet-600{background-color:var(--color-violet-600)}.bg-violet-700{background-color:var(--color-violet-700)}.bg-violet-800{background-color:var(--color-violet-800)}.bg-violet-900{background-color:var(--color-violet-900)}.bg-violet-950{background-color:var(--color-violet-950)}.bg-white{background-color:var(--color-white)}.bg-white\/20{background-color:#fff3}@supports (color:color-mix(in lab,red,red)){.bg-white\/20{background-color:color-mix(in oklab,var(--color-white)20%,transparent)}}.bg-white\/30{background-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.bg-white\/30{background-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.bg-white\/50{background-color:#ffffff80}@supports (color:color-mix(in lab,red,red)){.bg-white\/50{background-color:color-mix(in oklab,var(--color-white)50%,transparent)}}.bg-white\/90{background-color:#ffffffe6}@supports (color:color-mix(in lab,red,red)){.bg-white\/90{background-color:color-mix(in oklab,var(--color-white)90%,transparent)}}.bg-yellow-50{background-color:var(--color-yellow-50)}.bg-yellow-100{background-color:var(--color-yellow-100)}.bg-yellow-200{background-color:var(--color-yellow-200)}.bg-yellow-300{background-color:var(--color-yellow-300)}.bg-yellow-400{background-color:var(--color-yellow-400)}.bg-yellow-500{background-color:var(--color-yellow-500)}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-700{background-color:var(--color-yellow-700)}.bg-yellow-800{background-color:var(--color-yellow-800)}.bg-yellow-900{background-color:var(--color-yellow-900)}.bg-yellow-950{background-color:var(--color-yellow-950)}.bg-zinc-50{background-color:var(--color-zinc-50)}.bg-zinc-100{background-color:var(--color-zinc-100)}.bg-zinc-200{background-color:var(--color-zinc-200)}.bg-zinc-300{background-color:var(--color-zinc-300)}.bg-zinc-400{background-color:var(--color-zinc-400)}.bg-zinc-500{background-color:var(--color-zinc-500)}.bg-zinc-600{background-color:var(--color-zinc-600)}.bg-zinc-700{background-color:var(--color-zinc-700)}.bg-zinc-800{background-color:var(--color-zinc-800)}.bg-zinc-900{background-color:var(--color-zinc-900)}.bg-zinc-950{background-color:var(--color-zinc-950)}.bg-gradient-to-b{--tw-gradient-position:to bottom in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-bl{--tw-gradient-position:to bottom left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-l{--tw-gradient-position:to left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-r{--tw-gradient-position:to right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-t{--tw-gradient-position:to top in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tl{--tw-gradient-position:to top left in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-gradient-to-tr{--tw-gradient-position:to top right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.from-black{--tw-gradient-from:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-black\/90{--tw-gradient-from:#000000e6}@supports (color:color-mix(in lab,red,red)){.from-black\/90{--tw-gradient-from:color-mix(in oklab,var(--color-black)90%,transparent)}}.from-black\/90{--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-50{--tw-gradient-from:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-100{--tw-gradient-from:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-200{--tw-gradient-from:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-300{--tw-gradient-from:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-400{--tw-gradient-from:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-500{--tw-gradient-from:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-600{--tw-gradient-from:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-700{--tw-gradient-from:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-800{--tw-gradient-from:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-blue-900{--tw-gradient-from:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-current{--tw-gradient-from:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-emerald-500{--tw-gradient-from:var(--color-emerald-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-50{--tw-gradient-from:var(--color-green-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-100{--tw-gradient-from:var(--color-green-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-200{--tw-gradient-from:var(--color-green-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-300{--tw-gradient-from:var(--color-green-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-400{--tw-gradient-from:var(--color-green-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-500{--tw-gradient-from:var(--color-green-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-600{--tw-gradient-from:var(--color-green-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-700{--tw-gradient-from:var(--color-green-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-800{--tw-gradient-from:var(--color-green-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-green-900{--tw-gradient-from:var(--color-green-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-50{--tw-gradient-from:var(--color-indigo-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-100{--tw-gradient-from:var(--color-indigo-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-200{--tw-gradient-from:var(--color-indigo-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-300{--tw-gradient-from:var(--color-indigo-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-400{--tw-gradient-from:var(--color-indigo-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-500{--tw-gradient-from:var(--color-indigo-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-600{--tw-gradient-from:var(--color-indigo-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-700{--tw-gradient-from:var(--color-indigo-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-800{--tw-gradient-from:var(--color-indigo-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-indigo-900{--tw-gradient-from:var(--color-indigo-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-inherit{--tw-gradient-from:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-50{--tw-gradient-from:var(--color-orange-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-100{--tw-gradient-from:var(--color-orange-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-200{--tw-gradient-from:var(--color-orange-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-300{--tw-gradient-from:var(--color-orange-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-400{--tw-gradient-from:var(--color-orange-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-500{--tw-gradient-from:var(--color-orange-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-600{--tw-gradient-from:var(--color-orange-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-700{--tw-gradient-from:var(--color-orange-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-800{--tw-gradient-from:var(--color-orange-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-orange-900{--tw-gradient-from:var(--color-orange-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-50{--tw-gradient-from:var(--color-pink-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-100{--tw-gradient-from:var(--color-pink-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-200{--tw-gradient-from:var(--color-pink-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-300{--tw-gradient-from:var(--color-pink-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-400{--tw-gradient-from:var(--color-pink-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-500{--tw-gradient-from:var(--color-pink-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-600{--tw-gradient-from:var(--color-pink-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-700{--tw-gradient-from:var(--color-pink-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-800{--tw-gradient-from:var(--color-pink-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-pink-900{--tw-gradient-from:var(--color-pink-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-50{--tw-gradient-from:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-100{--tw-gradient-from:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-200{--tw-gradient-from:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-300{--tw-gradient-from:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-400{--tw-gradient-from:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-500{--tw-gradient-from:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-600{--tw-gradient-from:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-700{--tw-gradient-from:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-800{--tw-gradient-from:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-purple-900{--tw-gradient-from:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-50{--tw-gradient-from:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-100{--tw-gradient-from:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-200{--tw-gradient-from:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-300{--tw-gradient-from:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-400{--tw-gradient-from:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-500{--tw-gradient-from:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-600{--tw-gradient-from:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-700{--tw-gradient-from:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-800{--tw-gradient-from:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-red-900{--tw-gradient-from:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-50{--tw-gradient-from:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-100{--tw-gradient-from:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-200{--tw-gradient-from:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-300{--tw-gradient-from:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-400{--tw-gradient-from:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-500{--tw-gradient-from:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-600{--tw-gradient-from:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-700{--tw-gradient-from:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-800{--tw-gradient-from:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-slate-900{--tw-gradient-from:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-transparent{--tw-gradient-from:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-white{--tw-gradient-from:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-50{--tw-gradient-from:var(--color-yellow-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-100{--tw-gradient-from:var(--color-yellow-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-200{--tw-gradient-from:var(--color-yellow-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-300{--tw-gradient-from:var(--color-yellow-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-400{--tw-gradient-from:var(--color-yellow-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-500{--tw-gradient-from:var(--color-yellow-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-600{--tw-gradient-from:var(--color-yellow-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-700{--tw-gradient-from:var(--color-yellow-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-800{--tw-gradient-from:var(--color-yellow-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.from-yellow-900{--tw-gradient-from:var(--color-yellow-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.via-black{--tw-gradient-via:var(--color-black);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-black\/60{--tw-gradient-via:#0009}@supports (color:color-mix(in lab,red,red)){.via-black\/60{--tw-gradient-via:color-mix(in oklab,var(--color-black)60%,transparent)}}.via-black\/60{--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-50{--tw-gradient-via:var(--color-blue-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-100{--tw-gradient-via:var(--color-blue-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-200{--tw-gradient-via:var(--color-blue-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-300{--tw-gradient-via:var(--color-blue-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-400{--tw-gradient-via:var(--color-blue-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-500{--tw-gradient-via:var(--color-blue-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-600{--tw-gradient-via:var(--color-blue-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-700{--tw-gradient-via:var(--color-blue-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-800{--tw-gradient-via:var(--color-blue-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-blue-900{--tw-gradient-via:var(--color-blue-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-current{--tw-gradient-via:currentcolor;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-50{--tw-gradient-via:var(--color-gray-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-100{--tw-gradient-via:var(--color-gray-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-200{--tw-gradient-via:var(--color-gray-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-300{--tw-gradient-via:var(--color-gray-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-400{--tw-gradient-via:var(--color-gray-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-500{--tw-gradient-via:var(--color-gray-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-600{--tw-gradient-via:var(--color-gray-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-700{--tw-gradient-via:var(--color-gray-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-800{--tw-gradient-via:var(--color-gray-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-gray-900{--tw-gradient-via:var(--color-gray-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-inherit{--tw-gradient-via:inherit;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-50{--tw-gradient-via:var(--color-slate-50);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-100{--tw-gradient-via:var(--color-slate-100);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-200{--tw-gradient-via:var(--color-slate-200);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-300{--tw-gradient-via:var(--color-slate-300);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-400{--tw-gradient-via:var(--color-slate-400);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-500{--tw-gradient-via:var(--color-slate-500);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-600{--tw-gradient-via:var(--color-slate-600);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-700{--tw-gradient-via:var(--color-slate-700);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-800{--tw-gradient-via:var(--color-slate-800);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-slate-900{--tw-gradient-via:var(--color-slate-900);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-transparent{--tw-gradient-via:transparent;--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.via-white{--tw-gradient-via:var(--color-white);--tw-gradient-via-stops:var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-via)var(--tw-gradient-via-position),var(--tw-gradient-to)var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-via-stops)}.to-black{--tw-gradient-to:var(--color-black);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-50{--tw-gradient-to:var(--color-blue-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-100{--tw-gradient-to:var(--color-blue-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-200{--tw-gradient-to:var(--color-blue-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-300{--tw-gradient-to:var(--color-blue-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-400{--tw-gradient-to:var(--color-blue-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-500{--tw-gradient-to:var(--color-blue-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-600{--tw-gradient-to:var(--color-blue-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-700{--tw-gradient-to:var(--color-blue-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-800{--tw-gradient-to:var(--color-blue-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-blue-900{--tw-gradient-to:var(--color-blue-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-current{--tw-gradient-to:currentcolor;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-inherit{--tw-gradient-to:inherit;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-50{--tw-gradient-to:var(--color-purple-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-100{--tw-gradient-to:var(--color-purple-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-200{--tw-gradient-to:var(--color-purple-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-300{--tw-gradient-to:var(--color-purple-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-400{--tw-gradient-to:var(--color-purple-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-500{--tw-gradient-to:var(--color-purple-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-600{--tw-gradient-to:var(--color-purple-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-700{--tw-gradient-to:var(--color-purple-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-800{--tw-gradient-to:var(--color-purple-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-purple-900{--tw-gradient-to:var(--color-purple-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-50{--tw-gradient-to:var(--color-red-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-100{--tw-gradient-to:var(--color-red-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-200{--tw-gradient-to:var(--color-red-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-300{--tw-gradient-to:var(--color-red-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-400{--tw-gradient-to:var(--color-red-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-500{--tw-gradient-to:var(--color-red-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-600{--tw-gradient-to:var(--color-red-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-700{--tw-gradient-to:var(--color-red-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-800{--tw-gradient-to:var(--color-red-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-red-900{--tw-gradient-to:var(--color-red-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-50{--tw-gradient-to:var(--color-slate-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-100{--tw-gradient-to:var(--color-slate-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-200{--tw-gradient-to:var(--color-slate-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-300{--tw-gradient-to:var(--color-slate-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-400{--tw-gradient-to:var(--color-slate-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-500{--tw-gradient-to:var(--color-slate-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-600{--tw-gradient-to:var(--color-slate-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-700{--tw-gradient-to:var(--color-slate-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-800{--tw-gradient-to:var(--color-slate-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-slate-900{--tw-gradient-to:var(--color-slate-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-teal-600{--tw-gradient-to:var(--color-teal-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-transparent{--tw-gradient-to:transparent;--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.to-white{--tw-gradient-to:var(--color-white);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-none{object-fit:none}.object-scale-down{object-fit:scale-down}.object-bottom{object-position:bottom}.object-center{object-position:center}.object-left{object-position:left}.object-left-bottom{object-position:left bottom}.object-left-top{object-position:left top}.object-right{object-position:right}.object-right-bottom{object-position:right bottom}.object-right-top{object-position:right top}.object-top{object-position:top}.p-0{padding:calc(var(--spacing)*0)}.p-1{padding:calc(var(--spacing)*1)}.p-1\.5{padding:calc(var(--spacing)*1.5)}.p-2{padding:calc(var(--spacing)*2)}.p-3{padding:calc(var(--spacing)*3)}.p-4{padding:calc(var(--spacing)*4)}.p-5{padding:calc(var(--spacing)*5)}.p-6{padding:calc(var(--spacing)*6)}.p-8{padding:calc(var(--spacing)*8)}.p-10{padding:calc(var(--spacing)*10)}.p-12{padding:calc(var(--spacing)*12)}.p-16{padding:calc(var(--spacing)*16)}.p-20{padding:calc(var(--spacing)*20)}.p-24{padding:calc(var(--spacing)*24)}.px-0{padding-inline:calc(var(--spacing)*0)}.px-1{padding-inline:calc(var(--spacing)*1)}.px-1\.5{padding-inline:calc(var(--spacing)*1.5)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-2\.5{padding-inline:calc(var(--spacing)*2.5)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-4{padding-inline:calc(var(--spacing)*4)}.px-5{padding-inline:calc(var(--spacing)*5)}.px-6{padding-inline:calc(var(--spacing)*6)}.px-8{padding-inline:calc(var(--spacing)*8)}.px-10{padding-inline:calc(var(--spacing)*10)}.px-12{padding-inline:calc(var(--spacing)*12)}.px-16{padding-inline:calc(var(--spacing)*16)}.py-0{padding-block:calc(var(--spacing)*0)}.py-0\.5{padding-block:calc(var(--spacing)*.5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-1\.5{padding-block:calc(var(--spacing)*1.5)}.py-2{padding-block:calc(var(--spacing)*2)}.py-2\.5{padding-block:calc(var(--spacing)*2.5)}.py-3{padding-block:calc(var(--spacing)*3)}.py-3\.5{padding-block:calc(var(--spacing)*3.5)}.py-4{padding-block:calc(var(--spacing)*4)}.py-5{padding-block:calc(var(--spacing)*5)}.py-6{padding-block:calc(var(--spacing)*6)}.py-8{padding-block:calc(var(--spacing)*8)}.py-10{padding-block:calc(var(--spacing)*10)}.py-12{padding-block:calc(var(--spacing)*12)}.py-16{padding-block:calc(var(--spacing)*16)}.py-20{padding-block:calc(var(--spacing)*20)}.pt-0{padding-top:calc(var(--spacing)*0)}.pt-1{padding-top:calc(var(--spacing)*1)}.pt-2{padding-top:calc(var(--spacing)*2)}.pt-3{padding-top:calc(var(--spacing)*3)}.pt-4{padding-top:calc(var(--spacing)*4)}.pt-5{padding-top:calc(var(--spacing)*5)}.pt-6{padding-top:calc(var(--spacing)*6)}.pt-8{padding-top:calc(var(--spacing)*8)}.pt-10{padding-top:calc(var(--spacing)*10)}.pt-12{padding-top:calc(var(--spacing)*12)}.pt-16{padding-top:calc(var(--spacing)*16)}.pr-0{padding-right:calc(var(--spacing)*0)}.pr-1{padding-right:calc(var(--spacing)*1)}.pr-2{padding-right:calc(var(--spacing)*2)}.pr-3{padding-right:calc(var(--spacing)*3)}.pr-4{padding-right:calc(var(--spacing)*4)}.pr-5{padding-right:calc(var(--spacing)*5)}.pr-6{padding-right:calc(var(--spacing)*6)}.pr-8{padding-right:calc(var(--spacing)*8)}.pr-10{padding-right:calc(var(--spacing)*10)}.pr-12{padding-right:calc(var(--spacing)*12)}.pr-16{padding-right:calc(var(--spacing)*16)}.pb-0{padding-bottom:calc(var(--spacing)*0)}.pb-1{padding-bottom:calc(var(--spacing)*1)}.pb-2{padding-bottom:calc(var(--spacing)*2)}.pb-3{padding-bottom:calc(var(--spacing)*3)}.pb-4{padding-bottom:calc(var(--spacing)*4)}.pb-5{padding-bottom:calc(var(--spacing)*5)}.pb-6{padding-bottom:calc(var(--spacing)*6)}.pb-8{padding-bottom:calc(var(--spacing)*8)}.pb-10{padding-bottom:calc(var(--spacing)*10)}.pb-12{padding-bottom:calc(var(--spacing)*12)}.pb-16{padding-bottom:calc(var(--spacing)*16)}.pl-0{padding-left:calc(var(--spacing)*0)}.pl-1{padding-left:calc(var(--spacing)*1)}.pl-2{padding-left:calc(var(--spacing)*2)}.pl-3{padding-left:calc(var(--spacing)*3)}.pl-4{padding-left:calc(var(--spacing)*4)}.pl-5{padding-left:calc(var(--spacing)*5)}.pl-6{padding-left:calc(var(--spacing)*6)}.pl-8{padding-left:calc(var(--spacing)*8)}.pl-10{padding-left:calc(var(--spacing)*10)}.pl-12{padding-left:calc(var(--spacing)*12)}.pl-16{padding-left:calc(var(--spacing)*16)}.text-center{text-align:center}.text-end{text-align:end}.text-justify{text-align:justify}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.indent-0{text-indent:calc(var(--spacing)*0)}.indent-1{text-indent:calc(var(--spacing)*1)}.indent-2{text-indent:calc(var(--spacing)*2)}.indent-4{text-indent:calc(var(--spacing)*4)}.indent-8{text-indent:calc(var(--spacing)*8)}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.align-text-bottom{vertical-align:text-bottom}.align-text-top{vertical-align:text-top}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}.text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-3{--tw-leading:calc(var(--spacing)*3);line-height:calc(var(--spacing)*3)}.leading-4{--tw-leading:calc(var(--spacing)*4);line-height:calc(var(--spacing)*4)}.leading-5{--tw-leading:calc(var(--spacing)*5);line-height:calc(var(--spacing)*5)}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.leading-7{--tw-leading:calc(var(--spacing)*7);line-height:calc(var(--spacing)*7)}.leading-8{--tw-leading:calc(var(--spacing)*8);line-height:calc(var(--spacing)*8)}.leading-9{--tw-leading:calc(var(--spacing)*9);line-height:calc(var(--spacing)*9)}.leading-10{--tw-leading:calc(var(--spacing)*10);line-height:calc(var(--spacing)*10)}.leading-loose{--tw-leading:var(--leading-loose);line-height:var(--leading-loose)}.leading-none{--tw-leading:1;line-height:1}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-black{--tw-font-weight:var(--font-weight-black);font-weight:var(--font-weight-black)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold);font-weight:var(--font-weight-extrabold)}.font-extralight{--tw-font-weight:var(--font-weight-extralight);font-weight:var(--font-weight-extralight)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.font-thin{--tw-font-weight:var(--font-weight-thin);font-weight:var(--font-weight-thin)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-tighter{--tw-tracking:var(--tracking-tighter);letter-spacing:var(--tracking-tighter)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.\!whitespace-normal{white-space:normal!important}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#000000\]\/85{color:oklab(0% none none/.85)}.text-amber-50{color:var(--color-amber-50)}.text-amber-100{color:var(--color-amber-100)}.text-amber-200{color:var(--color-amber-200)}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-amber-500{color:var(--color-amber-500)}.text-amber-600{color:var(--color-amber-600)}.text-amber-700{color:var(--color-amber-700)}.text-amber-800{color:var(--color-amber-800)}.text-amber-900{color:var(--color-amber-900)}.text-amber-950{color:var(--color-amber-950)}.text-black{color:var(--color-black)}.text-blue-50{color:var(--color-blue-50)}.text-blue-100{color:var(--color-blue-100)}.text-blue-200{color:var(--color-blue-200)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-blue-500{color:var(--color-blue-500)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-blue-950{color:var(--color-blue-950)}.text-current{color:currentColor}.text-cyan-50{color:var(--color-cyan-50)}.text-cyan-100{color:var(--color-cyan-100)}.text-cyan-200{color:var(--color-cyan-200)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-cyan-500{color:var(--color-cyan-500)}.text-cyan-600{color:var(--color-cyan-600)}.text-cyan-700{color:var(--color-cyan-700)}.text-cyan-800{color:var(--color-cyan-800)}.text-cyan-900{color:var(--color-cyan-900)}.text-cyan-950{color:var(--color-cyan-950)}.text-emerald-50{color:var(--color-emerald-50)}.text-emerald-100{color:var(--color-emerald-100)}.text-emerald-200{color:var(--color-emerald-200)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-emerald-500{color:var(--color-emerald-500)}.text-emerald-600{color:var(--color-emerald-600)}.text-emerald-700{color:var(--color-emerald-700)}.text-emerald-800{color:var(--color-emerald-800)}.text-emerald-900{color:var(--color-emerald-900)}.text-emerald-950{color:var(--color-emerald-950)}.text-fuchsia-50{color:var(--color-fuchsia-50)}.text-fuchsia-100{color:var(--color-fuchsia-100)}.text-fuchsia-200{color:var(--color-fuchsia-200)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-fuchsia-400{color:var(--color-fuchsia-400)}.text-fuchsia-500{color:var(--color-fuchsia-500)}.text-fuchsia-600{color:var(--color-fuchsia-600)}.text-fuchsia-700{color:var(--color-fuchsia-700)}.text-fuchsia-800{color:var(--color-fuchsia-800)}.text-fuchsia-900{color:var(--color-fuchsia-900)}.text-fuchsia-950{color:var(--color-fuchsia-950)}.text-gray-50{color:var(--color-gray-50)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-gray-950{color:var(--color-gray-950)}.text-green-50{color:var(--color-green-50)}.text-green-100{color:var(--color-green-100)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-green-500{color:var(--color-green-500)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.text-green-800{color:var(--color-green-800)}.text-green-900{color:var(--color-green-900)}.text-green-950{color:var(--color-green-950)}.text-indigo-50{color:var(--color-indigo-50)}.text-indigo-100{color:var(--color-indigo-100)}.text-indigo-200{color:var(--color-indigo-200)}.text-indigo-300{color:var(--color-indigo-300)}.text-indigo-400{color:var(--color-indigo-400)}.text-indigo-500{color:var(--color-indigo-500)}.text-indigo-600{color:var(--color-indigo-600)}.text-indigo-700{color:var(--color-indigo-700)}.text-indigo-800{color:var(--color-indigo-800)}.text-indigo-900{color:var(--color-indigo-900)}.text-indigo-950{color:var(--color-indigo-950)}.text-inherit{color:inherit}.text-lime-50{color:var(--color-lime-50)}.text-lime-100{color:var(--color-lime-100)}.text-lime-200{color:var(--color-lime-200)}.text-lime-300{color:var(--color-lime-300)}.text-lime-400{color:var(--color-lime-400)}.text-lime-500{color:var(--color-lime-500)}.text-lime-600{color:var(--color-lime-600)}.text-lime-700{color:var(--color-lime-700)}.text-lime-800{color:var(--color-lime-800)}.text-lime-900{color:var(--color-lime-900)}.text-lime-950{color:var(--color-lime-950)}.text-neutral-50{color:var(--color-neutral-50)}.text-neutral-100{color:var(--color-neutral-100)}.text-neutral-200{color:var(--color-neutral-200)}.text-neutral-300{color:var(--color-neutral-300)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.text-neutral-900{color:var(--color-neutral-900)}.text-neutral-950{color:var(--color-neutral-950)}.text-orange-50{color:var(--color-orange-50)}.text-orange-100{color:var(--color-orange-100)}.text-orange-200{color:var(--color-orange-200)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-orange-500{color:var(--color-orange-500)}.text-orange-600{color:var(--color-orange-600)}.text-orange-700{color:var(--color-orange-700)}.text-orange-800{color:var(--color-orange-800)}.text-orange-900{color:var(--color-orange-900)}.text-orange-950{color:var(--color-orange-950)}.text-pink-50{color:var(--color-pink-50)}.text-pink-100{color:var(--color-pink-100)}.text-pink-200{color:var(--color-pink-200)}.text-pink-300{color:var(--color-pink-300)}.text-pink-400{color:var(--color-pink-400)}.text-pink-500{color:var(--color-pink-500)}.text-pink-600{color:var(--color-pink-600)}.text-pink-700{color:var(--color-pink-700)}.text-pink-800{color:var(--color-pink-800)}.text-pink-900{color:var(--color-pink-900)}.text-pink-950{color:var(--color-pink-950)}.text-primary-400{color:var(--color-primary-400)}.text-primary-600{color:var(--color-primary-600)}.text-purple-50{color:var(--color-purple-50)}.text-purple-100{color:var(--color-purple-100)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-purple-500{color:var(--color-purple-500)}.text-purple-600{color:var(--color-purple-600)}.text-purple-700{color:var(--color-purple-700)}.text-purple-800{color:var(--color-purple-800)}.text-purple-900{color:var(--color-purple-900)}.text-purple-950{color:var(--color-purple-950)}.text-red-50{color:var(--color-red-50)}.text-red-100{color:var(--color-red-100)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-red-500{color:var(--color-red-500)}.text-red-600{color:var(--color-red-600)}.text-red-700{color:var(--color-red-700)}.text-red-800{color:var(--color-red-800)}.text-red-900{color:var(--color-red-900)}.text-red-950{color:var(--color-red-950)}.text-rose-50{color:var(--color-rose-50)}.text-rose-100{color:var(--color-rose-100)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-rose-400{color:var(--color-rose-400)}.text-rose-500{color:var(--color-rose-500)}.text-rose-600{color:var(--color-rose-600)}.text-rose-700{color:var(--color-rose-700)}.text-rose-800{color:var(--color-rose-800)}.text-rose-900{color:var(--color-rose-900)}.text-rose-950{color:var(--color-rose-950)}.text-sky-50{color:var(--color-sky-50)}.text-sky-100{color:var(--color-sky-100)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-sky-500{color:var(--color-sky-500)}.text-sky-600{color:var(--color-sky-600)}.text-sky-700{color:var(--color-sky-700)}.text-sky-800{color:var(--color-sky-800)}.text-sky-900{color:var(--color-sky-900)}.text-sky-950{color:var(--color-sky-950)}.text-slate-50{color:var(--color-slate-50)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-slate-600{color:var(--color-slate-600)}.text-slate-700{color:var(--color-slate-700)}.text-slate-800{color:var(--color-slate-800)}.text-slate-900{color:var(--color-slate-900)}.text-slate-950{color:var(--color-slate-950)}.text-stone-50{color:var(--color-stone-50)}.text-stone-100{color:var(--color-stone-100)}.text-stone-200{color:var(--color-stone-200)}.text-stone-300{color:var(--color-stone-300)}.text-stone-400{color:var(--color-stone-400)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.text-stone-800{color:var(--color-stone-800)}.text-stone-900{color:var(--color-stone-900)}.text-stone-950{color:var(--color-stone-950)}.text-teal-50{color:var(--color-teal-50)}.text-teal-100{color:var(--color-teal-100)}.text-teal-200{color:var(--color-teal-200)}.text-teal-300{color:var(--color-teal-300)}.text-teal-400{color:var(--color-teal-400)}.text-teal-500{color:var(--color-teal-500)}.text-teal-600{color:var(--color-teal-600)}.text-teal-700{color:var(--color-teal-700)}.text-teal-800{color:var(--color-teal-800)}.text-teal-900{color:var(--color-teal-900)}.text-teal-950{color:var(--color-teal-950)}.text-transparent{color:#0000}.text-violet-50{color:var(--color-violet-50)}.text-violet-100{color:var(--color-violet-100)}.text-violet-200{color:var(--color-violet-200)}.text-violet-300{color:var(--color-violet-300)}.text-violet-400{color:var(--color-violet-400)}.text-violet-500{color:var(--color-violet-500)}.text-violet-600{color:var(--color-violet-600)}.text-violet-700{color:var(--color-violet-700)}.text-violet-800{color:var(--color-violet-800)}.text-violet-900{color:var(--color-violet-900)}.text-violet-950{color:var(--color-violet-950)}.text-white{color:var(--color-white)}.text-yellow-50{color:var(--color-yellow-50)}.text-yellow-100{color:var(--color-yellow-100)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.text-yellow-500{color:var(--color-yellow-500)}.text-yellow-600{color:var(--color-yellow-600)}.text-yellow-700{color:var(--color-yellow-700)}.text-yellow-800{color:var(--color-yellow-800)}.text-yellow-900{color:var(--color-yellow-900)}.text-yellow-950{color:var(--color-yellow-950)}.text-zinc-50{color:var(--color-zinc-50)}.text-zinc-100{color:var(--color-zinc-100)}.text-zinc-200{color:var(--color-zinc-200)}.text-zinc-300{color:var(--color-zinc-300)}.text-zinc-400{color:var(--color-zinc-400)}.text-zinc-500{color:var(--color-zinc-500)}.text-zinc-600{color:var(--color-zinc-600)}.text-zinc-700{color:var(--color-zinc-700)}.text-zinc-800{color:var(--color-zinc-800)}.text-zinc-900{color:var(--color-zinc-900)}.text-zinc-950{color:var(--color-zinc-950)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.placeholder-gray-400::placeholder{color:var(--color-gray-400)}.placeholder-gray-500::placeholder{color:var(--color-gray-500)}.accent-black{accent-color:var(--color-black)}.accent-blue-600{accent-color:var(--color-blue-600)}.accent-gray-900{accent-color:var(--color-gray-900)}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-10{opacity:.1}.opacity-15{opacity:.15}.opacity-20{opacity:.2}.opacity-25{opacity:.25}.opacity-30{opacity:.3}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-45{opacity:.45}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-65{opacity:.65}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.opacity-80{opacity:.8}.opacity-85{opacity:.85}.opacity-90{opacity:.9}.opacity-95{opacity:.95}.opacity-100{opacity:1}.bg-blend-color{background-blend-mode:color}.bg-blend-color-burn{background-blend-mode:color-burn}.bg-blend-color-dodge{background-blend-mode:color-dodge}.bg-blend-darken{background-blend-mode:darken}.bg-blend-difference{background-blend-mode:difference}.bg-blend-exclusion{background-blend-mode:exclusion}.bg-blend-hard-light{background-blend-mode:hard-light}.bg-blend-hue{background-blend-mode:hue}.bg-blend-lighten{background-blend-mode:lighten}.bg-blend-luminosity{background-blend-mode:luminosity}.bg-blend-multiply{background-blend-mode:multiply}.bg-blend-normal{background-blend-mode:normal}.bg-blend-overlay{background-blend-mode:overlay}.bg-blend-saturation{background-blend-mode:saturation}.bg-blend-screen{background-blend-mode:screen}.bg-blend-soft-light{background-blend-mode:soft-light}.mix-blend-color{mix-blend-mode:color}.mix-blend-color-burn{mix-blend-mode:color-burn}.mix-blend-color-dodge{mix-blend-mode:color-dodge}.mix-blend-darken{mix-blend-mode:darken}.mix-blend-difference{mix-blend-mode:difference}.mix-blend-exclusion{mix-blend-mode:exclusion}.mix-blend-hard-light{mix-blend-mode:hard-light}.mix-blend-hue{mix-blend-mode:hue}.mix-blend-lighten{mix-blend-mode:lighten}.mix-blend-luminosity{mix-blend-mode:luminosity}.mix-blend-multiply{mix-blend-mode:multiply}.mix-blend-normal{mix-blend-mode:normal}.mix-blend-overlay{mix-blend-mode:overlay}.mix-blend-plus-darker{mix-blend-mode:plus-darker}.mix-blend-plus-lighter{mix-blend-mode:plus-lighter}.mix-blend-saturation{mix-blend-mode:saturation}.mix-blend-screen{mix-blend-mode:screen}.mix-blend-soft-light{mix-blend-mode:soft-light}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-0{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-4{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-8{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-black{--tw-ring-color:var(--color-black)}.ring-blue-50{--tw-ring-color:var(--color-blue-50)}.ring-blue-100{--tw-ring-color:var(--color-blue-100)}.ring-blue-200{--tw-ring-color:var(--color-blue-200)}.ring-blue-300{--tw-ring-color:var(--color-blue-300)}.ring-blue-400{--tw-ring-color:var(--color-blue-400)}.ring-blue-500{--tw-ring-color:var(--color-blue-500)}.ring-blue-600{--tw-ring-color:var(--color-blue-600)}.ring-blue-700{--tw-ring-color:var(--color-blue-700)}.ring-blue-800{--tw-ring-color:var(--color-blue-800)}.ring-blue-900{--tw-ring-color:var(--color-blue-900)}.ring-current{--tw-ring-color:currentcolor}.ring-gray-50{--tw-ring-color:var(--color-gray-50)}.ring-gray-100{--tw-ring-color:var(--color-gray-100)}.ring-gray-200{--tw-ring-color:var(--color-gray-200)}.ring-gray-300{--tw-ring-color:var(--color-gray-300)}.ring-gray-400{--tw-ring-color:var(--color-gray-400)}.ring-gray-500{--tw-ring-color:var(--color-gray-500)}.ring-gray-600{--tw-ring-color:var(--color-gray-600)}.ring-gray-700{--tw-ring-color:var(--color-gray-700)}.ring-gray-800{--tw-ring-color:var(--color-gray-800)}.ring-gray-900{--tw-ring-color:var(--color-gray-900)}.ring-inherit{--tw-ring-color:inherit}.ring-red-500{--tw-ring-color:var(--color-red-500)}.ring-slate-50{--tw-ring-color:var(--color-slate-50)}.ring-slate-100{--tw-ring-color:var(--color-slate-100)}.ring-slate-200{--tw-ring-color:var(--color-slate-200)}.ring-slate-300{--tw-ring-color:var(--color-slate-300)}.ring-slate-400{--tw-ring-color:var(--color-slate-400)}.ring-slate-500{--tw-ring-color:var(--color-slate-500)}.ring-slate-600{--tw-ring-color:var(--color-slate-600)}.ring-slate-700{--tw-ring-color:var(--color-slate-700)}.ring-slate-800{--tw-ring-color:var(--color-slate-800)}.ring-slate-900{--tw-ring-color:var(--color-slate-900)}.ring-transparent{--tw-ring-color:transparent}.ring-white{--tw-ring-color:var(--color-white)}.ring-offset-0{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-1{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-2{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-4{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-8{--tw-ring-offset-width:8px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.ring-offset-black{--tw-ring-offset-color:var(--color-black)}.ring-offset-current{--tw-ring-offset-color:currentcolor}.ring-offset-inherit{--tw-ring-offset-color:inherit}.ring-offset-transparent{--tw-ring-offset-color:transparent}.ring-offset-white{--tw-ring-offset-color:var(--color-white)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.outline-0{outline-style:var(--tw-outline-style);outline-width:0}.outline-1{outline-style:var(--tw-outline-style);outline-width:1px}.outline-2{outline-style:var(--tw-outline-style);outline-width:2px}.outline-4{outline-style:var(--tw-outline-style);outline-width:4px}.outline-8{outline-style:var(--tw-outline-style);outline-width:8px}.outline-offset-0{outline-offset:0px}.outline-offset-1{outline-offset:1px}.outline-offset-2{outline-offset:2px}.outline-offset-4{outline-offset:4px}.outline-offset-8{outline-offset:8px}.outline-black{outline-color:var(--color-black)}.outline-current{outline-color:currentColor}.outline-inherit{outline-color:inherit}.outline-transparent{outline-color:#0000}.outline-white{outline-color:var(--color-white)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-2xl{--tw-blur:blur(var(--blur-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-3xl{--tw-blur:blur(var(--blur-3xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-lg{--tw-blur:blur(var(--blur-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-md{--tw-blur:blur(var(--blur-md));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-none{--tw-blur: ;filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-sm{--tw-blur:blur(var(--blur-sm));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.blur-xl{--tw-blur:blur(var(--blur-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-0{--tw-brightness:brightness(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-50{--tw-brightness:brightness(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-75{--tw-brightness:brightness(75%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-90{--tw-brightness:brightness(90%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-95{--tw-brightness:brightness(95%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-100{--tw-brightness:brightness(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-105{--tw-brightness:brightness(105%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-125{--tw-brightness:brightness(125%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-150{--tw-brightness:brightness(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.brightness-200{--tw-brightness:brightness(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-0{--tw-contrast:contrast(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-50{--tw-contrast:contrast(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-75{--tw-contrast:contrast(75%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-100{--tw-contrast:contrast(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-125{--tw-contrast:contrast(125%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-150{--tw-contrast:contrast(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.contrast-200{--tw-contrast:contrast(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a))drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a)drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-2xl{--tw-drop-shadow-size:drop-shadow(0 25px 25px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-2xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-lg{--tw-drop-shadow-size:drop-shadow(0 4px 4px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-lg));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-md{--tw-drop-shadow-size:drop-shadow(0 3px 3px var(--tw-drop-shadow-color,#0000001f));--tw-drop-shadow:drop-shadow(var(--drop-shadow-md));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-sm{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#00000026));--tw-drop-shadow:drop-shadow(var(--drop-shadow-sm));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-xl{--tw-drop-shadow-size:drop-shadow(0 9px 7px var(--tw-drop-shadow-color,#0000001a));--tw-drop-shadow:drop-shadow(var(--drop-shadow-xl));filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.drop-shadow-none{--tw-drop-shadow: ;filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.grayscale-0{--tw-grayscale:grayscale(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-15{--tw-hue-rotate:hue-rotate( -15deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-30{--tw-hue-rotate:hue-rotate( -30deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-60{--tw-hue-rotate:hue-rotate( -60deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-90{--tw-hue-rotate:hue-rotate( -90deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.-hue-rotate-180{--tw-hue-rotate:hue-rotate( -180deg );filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-0{--tw-hue-rotate:hue-rotate(0deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-15{--tw-hue-rotate:hue-rotate(15deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-30{--tw-hue-rotate:hue-rotate(30deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-60{--tw-hue-rotate:hue-rotate(60deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-90{--tw-hue-rotate:hue-rotate(90deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.hue-rotate-180{--tw-hue-rotate:hue-rotate(180deg);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.invert-0{--tw-invert:invert(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-0{--tw-saturate:saturate(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-50{--tw-saturate:saturate(50%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-100{--tw-saturate:saturate(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-150{--tw-saturate:saturate(150%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.saturate-200{--tw-saturate:saturate(200%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.sepia{--tw-sepia:sepia(100%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.sepia-0{--tw-sepia:sepia(0%);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-2xl{--tw-backdrop-blur:blur(var(--blur-2xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-3xl{--tw-backdrop-blur:blur(var(--blur-3xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-none{--tw-backdrop-blur: ;-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-0{--tw-backdrop-brightness:brightness(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-50{--tw-backdrop-brightness:brightness(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-75{--tw-backdrop-brightness:brightness(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-90{--tw-backdrop-brightness:brightness(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-95{--tw-backdrop-brightness:brightness(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-100{--tw-backdrop-brightness:brightness(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-105{--tw-backdrop-brightness:brightness(105%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-110{--tw-backdrop-brightness:brightness(110%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-125{--tw-backdrop-brightness:brightness(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-150{--tw-backdrop-brightness:brightness(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-brightness-200{--tw-backdrop-brightness:brightness(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-0{--tw-backdrop-contrast:contrast(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-50{--tw-backdrop-contrast:contrast(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-75{--tw-backdrop-contrast:contrast(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-100{--tw-backdrop-contrast:contrast(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-125{--tw-backdrop-contrast:contrast(125%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-150{--tw-backdrop-contrast:contrast(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-contrast-200{--tw-backdrop-contrast:contrast(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-grayscale-0{--tw-backdrop-grayscale:grayscale(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-0{--tw-backdrop-hue-rotate:hue-rotate(0deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-15{--tw-backdrop-hue-rotate:hue-rotate(15deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-30{--tw-backdrop-hue-rotate:hue-rotate(30deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-60{--tw-backdrop-hue-rotate:hue-rotate(60deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-90{--tw-backdrop-hue-rotate:hue-rotate(90deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-hue-rotate-180{--tw-backdrop-hue-rotate:hue-rotate(180deg);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-invert-0{--tw-backdrop-invert:invert(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-0{--tw-backdrop-opacity:opacity(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-5{--tw-backdrop-opacity:opacity(5%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-10{--tw-backdrop-opacity:opacity(10%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-20{--tw-backdrop-opacity:opacity(20%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-25{--tw-backdrop-opacity:opacity(25%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-30{--tw-backdrop-opacity:opacity(30%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-40{--tw-backdrop-opacity:opacity(40%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-50{--tw-backdrop-opacity:opacity(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-60{--tw-backdrop-opacity:opacity(60%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-70{--tw-backdrop-opacity:opacity(70%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-75{--tw-backdrop-opacity:opacity(75%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-80{--tw-backdrop-opacity:opacity(80%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-90{--tw-backdrop-opacity:opacity(90%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-95{--tw-backdrop-opacity:opacity(95%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-opacity-100{--tw-backdrop-opacity:opacity(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-0{--tw-backdrop-saturate:saturate(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-50{--tw-backdrop-saturate:saturate(50%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-100{--tw-backdrop-saturate:saturate(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-150{--tw-backdrop-saturate:saturate(150%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-saturate-200{--tw-backdrop-saturate:saturate(200%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.backdrop-sepia-0{--tw-backdrop-sepia:sepia(0%);-webkit-backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,)var(--tw-backdrop-brightness,)var(--tw-backdrop-contrast,)var(--tw-backdrop-grayscale,)var(--tw-backdrop-hue-rotate,)var(--tw-backdrop-invert,)var(--tw-backdrop-opacity,)var(--tw-backdrop-saturate,)var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-\[max-height\]{transition-property:max-height;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-shadow{transition-property:box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-none{transition-property:none}.delay-75{transition-delay:75ms}.delay-100{transition-delay:.1s}.delay-150{transition-delay:.15s}.delay-200{transition-delay:.2s}.delay-300{transition-delay:.3s}.delay-500{transition-delay:.5s}.delay-700{transition-delay:.7s}.delay-1000{transition-delay:1s}.duration-75{--tw-duration:75ms;transition-duration:75ms}.duration-100{--tw-duration:.1s;transition-duration:.1s}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.duration-1000{--tw-duration:1s;transition-duration:1s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-linear{--tw-ease:linear;transition-timing-function:linear}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-dashed{--tw-outline-style:dashed;outline-style:dashed}.outline-dotted{--tw-outline-style:dotted;outline-style:dotted}.outline-double{--tw-outline-style:double;outline-style:double}.outline-none{--tw-outline-style:none;outline-style:none}.select-all{-webkit-user-select:all;user-select:all}.select-auto{-webkit-user-select:auto;user-select:auto}.select-none{-webkit-user-select:none;user-select:none}.select-text{-webkit-user-select:text;user-select:text}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.ring-inset{--tw-ring-inset:inset}@media(hover:hover){.group-hover\:visible:is(:where(.group):hover *){visibility:visible}.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:translate-y-1:is(:where(.group):hover *){--tw-translate-y:calc(var(--spacing)*1);translate:var(--tw-translate-x)var(--tw-translate-y)}.group-hover\:scale-100:is(:where(.group):hover *){--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:scale-105:is(:where(.group):hover *){--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:#0006}@supports (color:color-mix(in lab,red,red)){.group-hover\:bg-black\/40:is(:where(.group):hover *){background-color:color-mix(in oklab,var(--color-black)40%,transparent)}}.group-hover\:bg-blue-100:is(:where(.group):hover *){background-color:var(--color-blue-100)}.group-hover\:bg-blue-500:is(:where(.group):hover *){background-color:var(--color-blue-500)}.group-hover\:bg-blue-600:is(:where(.group):hover *){background-color:var(--color-blue-600)}.group-hover\:bg-gray-100:is(:where(.group):hover *){background-color:var(--color-gray-100)}.group-hover\:bg-gray-200:is(:where(.group):hover *){background-color:var(--color-gray-200)}.group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.group-hover\:text-black:is(:where(.group):hover *){color:var(--color-black)}.group-hover\:text-blue-500:is(:where(.group):hover *){color:var(--color-blue-500)}.group-hover\:text-blue-600:is(:where(.group):hover *){color:var(--color-blue-600)}.group-hover\:text-blue-700:is(:where(.group):hover *){color:var(--color-blue-700)}.group-hover\:text-gray-700:is(:where(.group):hover *){color:var(--color-gray-700)}.group-hover\:text-gray-800:is(:where(.group):hover *){color:var(--color-gray-800)}.group-hover\:text-gray-900:is(:where(.group):hover *){color:var(--color-gray-900)}.group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.group-focus\:border-blue-500:is(:where(.group):focus *){border-color:var(--color-blue-500)}.group-focus\:ring-2:is(:where(.group):focus *){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-checked\:border-blue-500:is(:where(.peer):checked~*){border-color:var(--color-blue-500)}.peer-checked\:bg-blue-500:is(:where(.peer):checked~*){background-color:var(--color-blue-500)}.peer-invalid\:border-red-500:is(:where(.peer):invalid~*){border-color:var(--color-red-500)}.peer-invalid\:text-red-500:is(:where(.peer):invalid~*){color:var(--color-red-500)}.peer-focus\:border-blue-500:is(:where(.peer):focus~*){border-color:var(--color-blue-500)}.peer-focus\:ring-2:is(:where(.peer):focus~*){--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.peer-disabled\:cursor-not-allowed:is(:where(.peer):disabled~*){cursor:not-allowed}.peer-disabled\:opacity-50:is(:where(.peer):disabled~*){opacity:.5}.first-letter\:text-2xl:first-letter{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.first-letter\:font-bold:first-letter{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.first-letter\:text-blue-600:first-letter{color:var(--color-blue-600)}.first-line\:font-semibold:first-line{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.first-line\:text-gray-700:first-line{color:var(--color-gray-700)}.marker\:text-blue-500 ::marker{color:var(--color-blue-500)}.marker\:text-blue-500::marker{color:var(--color-blue-500)}.marker\:text-blue-500 ::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-blue-500::-webkit-details-marker{color:var(--color-blue-500)}.marker\:text-gray-500 ::marker{color:var(--color-gray-500)}.marker\:text-gray-500::marker{color:var(--color-gray-500)}.marker\:text-gray-500 ::-webkit-details-marker{color:var(--color-gray-500)}.marker\:text-gray-500::-webkit-details-marker{color:var(--color-gray-500)}.selection\:bg-blue-200 ::selection{background-color:var(--color-blue-200)}.selection\:bg-blue-200::selection{background-color:var(--color-blue-200)}.selection\:text-blue-900 ::selection{color:var(--color-blue-900)}.selection\:text-blue-900::selection{color:var(--color-blue-900)}.file\:mr-4::file-selector-button{margin-right:calc(var(--spacing)*4)}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-full::file-selector-button{border-radius:3.40282e38px}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-blue-50::file-selector-button{background-color:var(--color-blue-50)}.file\:px-4::file-selector-button{padding-inline:calc(var(--spacing)*4)}.file\:py-2::file-selector-button{padding-block:calc(var(--spacing)*2)}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-semibold::file-selector-button{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.file\:text-blue-700::file-selector-button{color:var(--color-blue-700)}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:inset-0:before{content:var(--tw-content);inset:calc(var(--spacing)*0)}.before\:block:before{content:var(--tw-content);display:block}.after\:absolute:after{content:var(--tw-content);position:absolute}.after\:inset-0:after{content:var(--tw-content);inset:calc(var(--spacing)*0)}.after\:clear-both:after{content:var(--tw-content);clear:both}.after\:block:after{content:var(--tw-content);display:block}.after\:content-\[\'\'\]:after{--tw-content:"";content:var(--tw-content)}.first\:mt-0:first-child{margin-top:calc(var(--spacing)*0)}.first\:rounded-t-lg:first-child{border-top-left-radius:var(--radius-lg);border-top-right-radius:var(--radius-lg)}.first\:rounded-l-lg:first-child{border-top-left-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:pt-0:first-child{padding-top:calc(var(--spacing)*0)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}.last\:rounded-r-lg:last-child{border-top-right-radius:var(--radius-lg);border-bottom-right-radius:var(--radius-lg)}.last\:rounded-b-lg:last-child{border-bottom-right-radius:var(--radius-lg);border-bottom-left-radius:var(--radius-lg)}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.last\:pb-0:last-child{padding-bottom:calc(var(--spacing)*0)}.odd\:bg-gray-50:nth-child(odd){background-color:var(--color-gray-50)}.odd\:bg-gray-100:nth-child(odd){background-color:var(--color-gray-100)}.odd\:bg-white:nth-child(odd){background-color:var(--color-white)}.even\:bg-gray-50:nth-child(2n){background-color:var(--color-gray-50)}.even\:bg-gray-100:nth-child(2n){background-color:var(--color-gray-100)}.even\:bg-white:nth-child(2n){background-color:var(--color-white)}.checked\:border-blue-500:checked{border-color:var(--color-blue-500)}.checked\:border-blue-600:checked{border-color:var(--color-blue-600)}.checked\:bg-blue-500:checked{background-color:var(--color-blue-500)}.checked\:bg-blue-600:checked{background-color:var(--color-blue-600)}.checked\:bg-green-500:checked{background-color:var(--color-green-500)}.checked\:text-white:checked{color:var(--color-white)}.indeterminate\:border-gray-400:indeterminate{border-color:var(--color-gray-400)}.indeterminate\:bg-gray-300:indeterminate{background-color:var(--color-gray-300)}.placeholder-shown\:border-gray-300:placeholder-shown{border-color:var(--color-gray-300)}.autofill\:bg-yellow-100:autofill{background-color:var(--color-yellow-100)}.autofill\:text-black:autofill{color:var(--color-black)}.valid\:border-green-500:valid{border-color:var(--color-green-500)}.valid\:text-green-600:valid{color:var(--color-green-600)}.valid\:ring-green-500:valid{--tw-ring-color:var(--color-green-500)}.invalid\:border-red-500:invalid{border-color:var(--color-red-500)}.invalid\:text-red-600:invalid{color:var(--color-red-600)}.invalid\:ring-red-500:invalid{--tw-ring-color:var(--color-red-500)}.read-only\:cursor-not-allowed:read-only{cursor:not-allowed}.read-only\:bg-gray-50:read-only{background-color:var(--color-gray-50)}.read-only\:bg-gray-100:read-only{background-color:var(--color-gray-100)}.empty\:hidden:empty{display:none}.focus-within\:border-blue-500:focus-within{border-color:var(--color-blue-500)}.focus-within\:border-gray-500:focus-within{border-color:var(--color-gray-500)}.focus-within\:ring-2:focus-within{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-within\:ring-blue-500:focus-within{--tw-ring-color:var(--color-blue-500)}.focus-within\:ring-gray-500:focus-within{--tw-ring-color:var(--color-gray-500)}@media(hover:hover){.hover\:scale-95:hover{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-100:hover{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-105:hover{--tw-scale-x:105%;--tw-scale-y:105%;--tw-scale-z:105%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x)var(--tw-scale-y)}.hover\:border-blue-50:hover{border-color:var(--color-blue-50)}.hover\:border-blue-100:hover{border-color:var(--color-blue-100)}.hover\:border-blue-200:hover{border-color:var(--color-blue-200)}.hover\:border-blue-300:hover{border-color:var(--color-blue-300)}.hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.hover\:border-blue-500:hover{border-color:var(--color-blue-500)}.hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.hover\:border-blue-900:hover{border-color:var(--color-blue-900)}.hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.hover\:bg-black:hover{background-color:var(--color-black)}.hover\:bg-black\/10:hover{background-color:#0000001a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/10:hover{background-color:color-mix(in oklab,var(--color-black)10%,transparent)}}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.hover\:bg-emerald-600:hover{background-color:var(--color-emerald-600)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.hover\:bg-green-50:hover{background-color:var(--color-green-50)}.hover\:bg-green-100:hover{background-color:var(--color-green-100)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}.hover\:bg-green-300:hover{background-color:var(--color-green-300)}.hover\:bg-green-400:hover{background-color:var(--color-green-400)}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-600:hover{background-color:var(--color-green-600)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-green-800:hover{background-color:var(--color-green-800)}.hover\:bg-green-900:hover{background-color:var(--color-green-900)}.hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.hover\:bg-orange-50:hover{background-color:var(--color-orange-50)}.hover\:bg-orange-100:hover{background-color:var(--color-orange-100)}.hover\:bg-orange-200:hover{background-color:var(--color-orange-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-orange-400:hover{background-color:var(--color-orange-400)}.hover\:bg-orange-500:hover{background-color:var(--color-orange-500)}.hover\:bg-orange-600:hover{background-color:var(--color-orange-600)}.hover\:bg-orange-700:hover{background-color:var(--color-orange-700)}.hover\:bg-orange-800:hover{background-color:var(--color-orange-800)}.hover\:bg-orange-900:hover{background-color:var(--color-orange-900)}.hover\:bg-pink-50:hover{background-color:var(--color-pink-50)}.hover\:bg-pink-100:hover{background-color:var(--color-pink-100)}.hover\:bg-pink-200:hover{background-color:var(--color-pink-200)}.hover\:bg-pink-300:hover{background-color:var(--color-pink-300)}.hover\:bg-pink-400:hover{background-color:var(--color-pink-400)}.hover\:bg-pink-500:hover{background-color:var(--color-pink-500)}.hover\:bg-pink-600:hover{background-color:var(--color-pink-600)}.hover\:bg-pink-700:hover{background-color:var(--color-pink-700)}.hover\:bg-pink-800:hover{background-color:var(--color-pink-800)}.hover\:bg-pink-900:hover{background-color:var(--color-pink-900)}.hover\:bg-primary-700:hover{background-color:var(--color-primary-700)}.hover\:bg-purple-50:hover{background-color:var(--color-purple-50)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-purple-200:hover{background-color:var(--color-purple-200)}.hover\:bg-purple-300:hover{background-color:var(--color-purple-300)}.hover\:bg-purple-400:hover{background-color:var(--color-purple-400)}.hover\:bg-purple-500:hover{background-color:var(--color-purple-500)}.hover\:bg-purple-600:hover{background-color:var(--color-purple-600)}.hover\:bg-purple-700:hover{background-color:var(--color-purple-700)}.hover\:bg-purple-800:hover{background-color:var(--color-purple-800)}.hover\:bg-purple-900:hover{background-color:var(--color-purple-900)}.hover\:bg-red-50:hover{background-color:var(--color-red-50)}.hover\:bg-red-100:hover{background-color:var(--color-red-100)}.hover\:bg-red-200:hover{background-color:var(--color-red-200)}.hover\:bg-red-300:hover{background-color:var(--color-red-300)}.hover\:bg-red-400:hover{background-color:var(--color-red-400)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-600:hover{background-color:var(--color-red-600)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-red-800:hover{background-color:var(--color-red-800)}.hover\:bg-red-900:hover{background-color:var(--color-red-900)}.hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.hover\:bg-transparent:hover{background-color:#0000}.hover\:bg-white:hover{background-color:var(--color-white)}.hover\:bg-white\/30:hover{background-color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/30:hover{background-color:color-mix(in oklab,var(--color-white)30%,transparent)}}.hover\:bg-yellow-50:hover{background-color:var(--color-yellow-50)}.hover\:bg-yellow-100:hover{background-color:var(--color-yellow-100)}.hover\:bg-yellow-200:hover{background-color:var(--color-yellow-200)}.hover\:bg-yellow-300:hover{background-color:var(--color-yellow-300)}.hover\:bg-yellow-400:hover{background-color:var(--color-yellow-400)}.hover\:bg-yellow-500:hover{background-color:var(--color-yellow-500)}.hover\:bg-yellow-600:hover{background-color:var(--color-yellow-600)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:bg-yellow-800:hover{background-color:var(--color-yellow-800)}.hover\:bg-yellow-900:hover{background-color:var(--color-yellow-900)}.hover\:text-black:hover{color:var(--color-black)}.hover\:text-blue-50:hover{color:var(--color-blue-50)}.hover\:text-blue-100:hover{color:var(--color-blue-100)}.hover\:text-blue-200:hover{color:var(--color-blue-200)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-blue-400:hover{color:var(--color-blue-400)}.hover\:text-blue-500:hover{color:var(--color-blue-500)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-blue-700:hover{color:var(--color-blue-700)}.hover\:text-blue-800:hover{color:var(--color-blue-800)}.hover\:text-blue-900:hover{color:var(--color-blue-900)}.hover\:text-gray-50:hover{color:var(--color-gray-50)}.hover\:text-gray-100:hover{color:var(--color-gray-100)}.hover\:text-gray-200:hover{color:var(--color-gray-200)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-gray-400:hover{color:var(--color-gray-400)}.hover\:text-gray-500:hover{color:var(--color-gray-500)}.hover\:text-gray-600:hover{color:var(--color-gray-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-gray-800:hover{color:var(--color-gray-800)}.hover\:text-gray-900:hover{color:var(--color-gray-900)}.hover\:text-green-50:hover{color:var(--color-green-50)}.hover\:text-green-100:hover{color:var(--color-green-100)}.hover\:text-green-200:hover{color:var(--color-green-200)}.hover\:text-green-300:hover{color:var(--color-green-300)}.hover\:text-green-400:hover{color:var(--color-green-400)}.hover\:text-green-500:hover{color:var(--color-green-500)}.hover\:text-green-600:hover{color:var(--color-green-600)}.hover\:text-green-700:hover{color:var(--color-green-700)}.hover\:text-green-800:hover{color:var(--color-green-800)}.hover\:text-green-900:hover{color:var(--color-green-900)}.hover\:text-purple-50:hover{color:var(--color-purple-50)}.hover\:text-purple-100:hover{color:var(--color-purple-100)}.hover\:text-purple-200:hover{color:var(--color-purple-200)}.hover\:text-purple-300:hover{color:var(--color-purple-300)}.hover\:text-purple-400:hover{color:var(--color-purple-400)}.hover\:text-purple-500:hover{color:var(--color-purple-500)}.hover\:text-purple-600:hover{color:var(--color-purple-600)}.hover\:text-purple-700:hover{color:var(--color-purple-700)}.hover\:text-purple-800:hover{color:var(--color-purple-800)}.hover\:text-purple-900:hover{color:var(--color-purple-900)}.hover\:text-red-50:hover{color:var(--color-red-50)}.hover\:text-red-100:hover{color:var(--color-red-100)}.hover\:text-red-200:hover{color:var(--color-red-200)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-red-500:hover{color:var(--color-red-500)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-red-700:hover{color:var(--color-red-700)}.hover\:text-red-800:hover{color:var(--color-red-800)}.hover\:text-red-900:hover{color:var(--color-red-900)}.hover\:text-slate-50:hover{color:var(--color-slate-50)}.hover\:text-slate-100:hover{color:var(--color-slate-100)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}.hover\:text-slate-300:hover{color:var(--color-slate-300)}.hover\:text-slate-400:hover{color:var(--color-slate-400)}.hover\:text-slate-500:hover{color:var(--color-slate-500)}.hover\:text-slate-600:hover{color:var(--color-slate-600)}.hover\:text-slate-700:hover{color:var(--color-slate-700)}.hover\:text-slate-800:hover{color:var(--color-slate-800)}.hover\:text-slate-900:hover{color:var(--color-slate-900)}.hover\:text-white:hover{color:var(--color-white)}.hover\:line-through:hover{text-decoration-line:line-through}.hover\:no-underline:hover{text-decoration-line:none}.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-0:hover{opacity:0}.hover\:opacity-25:hover{opacity:.25}.hover\:opacity-50:hover{opacity:.5}.hover\:opacity-75:hover{opacity:.75}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-90:hover{opacity:.9}.hover\:opacity-100:hover{opacity:1}.hover\:shadow:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-2xl:hover{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-md:hover{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-none:hover{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-xl:hover{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-0:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-1:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-4:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-8:hover{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}.hover\:ring-gray-500:hover{--tw-ring-color:var(--color-gray-500)}.hover\:ring-green-500:hover{--tw-ring-color:var(--color-green-500)}.hover\:ring-red-500:hover{--tw-ring-color:var(--color-red-500)}.file\:hover\:bg-blue-100::file-selector-button:hover{background-color:var(--color-blue-100)}}.focus\:border-blue-50:focus{border-color:var(--color-blue-50)}.focus\:border-blue-100:focus{border-color:var(--color-blue-100)}.focus\:border-blue-200:focus{border-color:var(--color-blue-200)}.focus\:border-blue-300:focus{border-color:var(--color-blue-300)}.focus\:border-blue-400:focus{border-color:var(--color-blue-400)}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-blue-600:focus{border-color:var(--color-blue-600)}.focus\:border-blue-700:focus{border-color:var(--color-blue-700)}.focus\:border-blue-800:focus{border-color:var(--color-blue-800)}.focus\:border-blue-900:focus{border-color:var(--color-blue-900)}.focus\:border-gray-50:focus{border-color:var(--color-gray-50)}.focus\:border-gray-100:focus{border-color:var(--color-gray-100)}.focus\:border-gray-200:focus{border-color:var(--color-gray-200)}.focus\:border-gray-300:focus{border-color:var(--color-gray-300)}.focus\:border-gray-400:focus{border-color:var(--color-gray-400)}.focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.focus\:border-gray-800:focus{border-color:var(--color-gray-800)}.focus\:border-gray-900:focus{border-color:var(--color-gray-900)}.focus\:border-green-500:focus{border-color:var(--color-green-500)}.focus\:border-indigo-500:focus{border-color:var(--color-indigo-500)}.focus\:border-purple-500:focus{border-color:var(--color-purple-500)}.focus\:border-red-500:focus{border-color:var(--color-red-500)}.focus\:border-transparent:focus{border-color:#0000}.focus\:border-yellow-500:focus{border-color:var(--color-yellow-500)}.focus\:bg-black:focus{background-color:var(--color-black)}.focus\:bg-blue-50:focus{background-color:var(--color-blue-50)}.focus\:bg-blue-100:focus{background-color:var(--color-blue-100)}.focus\:bg-blue-200:focus{background-color:var(--color-blue-200)}.focus\:bg-blue-300:focus{background-color:var(--color-blue-300)}.focus\:bg-blue-400:focus{background-color:var(--color-blue-400)}.focus\:bg-blue-500:focus{background-color:var(--color-blue-500)}.focus\:bg-blue-600:focus{background-color:var(--color-blue-600)}.focus\:bg-blue-700:focus{background-color:var(--color-blue-700)}.focus\:bg-blue-800:focus{background-color:var(--color-blue-800)}.focus\:bg-blue-900:focus{background-color:var(--color-blue-900)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:bg-gray-100:focus{background-color:var(--color-gray-100)}.focus\:bg-gray-200:focus{background-color:var(--color-gray-200)}.focus\:bg-gray-300:focus{background-color:var(--color-gray-300)}.focus\:bg-gray-400:focus{background-color:var(--color-gray-400)}.focus\:bg-gray-500:focus{background-color:var(--color-gray-500)}.focus\:bg-gray-600:focus{background-color:var(--color-gray-600)}.focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.focus\:bg-transparent:focus{background-color:#0000}.focus\:bg-white:focus{background-color:var(--color-white)}.focus\:text-black:focus{color:var(--color-black)}.focus\:text-blue-50:focus{color:var(--color-blue-50)}.focus\:text-blue-100:focus{color:var(--color-blue-100)}.focus\:text-blue-200:focus{color:var(--color-blue-200)}.focus\:text-blue-300:focus{color:var(--color-blue-300)}.focus\:text-blue-400:focus{color:var(--color-blue-400)}.focus\:text-blue-500:focus{color:var(--color-blue-500)}.focus\:text-blue-600:focus{color:var(--color-blue-600)}.focus\:text-blue-700:focus{color:var(--color-blue-700)}.focus\:text-blue-800:focus{color:var(--color-blue-800)}.focus\:text-blue-900:focus{color:var(--color-blue-900)}.focus\:text-gray-50:focus{color:var(--color-gray-50)}.focus\:text-gray-100:focus{color:var(--color-gray-100)}.focus\:text-gray-200:focus{color:var(--color-gray-200)}.focus\:text-gray-300:focus{color:var(--color-gray-300)}.focus\:text-gray-400:focus{color:var(--color-gray-400)}.focus\:text-gray-500:focus{color:var(--color-gray-500)}.focus\:text-gray-600:focus{color:var(--color-gray-600)}.focus\:text-gray-700:focus{color:var(--color-gray-700)}.focus\:text-gray-800:focus{color:var(--color-gray-800)}.focus\:text-gray-900:focus{color:var(--color-gray-900)}.focus\:text-white:focus{color:var(--color-white)}.focus\:shadow:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-lg:focus{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-md:focus{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-none:focus{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-sm:focus{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:shadow-xl:focus{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-0:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(0px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(1px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-4:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(4px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-8:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(8px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-amber-500:focus{--tw-ring-color:var(--color-amber-500)}.focus\:ring-blue-50:focus{--tw-ring-color:var(--color-blue-50)}.focus\:ring-blue-100:focus{--tw-ring-color:var(--color-blue-100)}.focus\:ring-blue-200:focus{--tw-ring-color:var(--color-blue-200)}.focus\:ring-blue-300:focus{--tw-ring-color:var(--color-blue-300)}.focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.focus\:ring-blue-800:focus{--tw-ring-color:var(--color-blue-800)}.focus\:ring-blue-900:focus{--tw-ring-color:var(--color-blue-900)}.focus\:ring-current:focus{--tw-ring-color:currentcolor}.focus\:ring-gray-50:focus{--tw-ring-color:var(--color-gray-50)}.focus\:ring-gray-100:focus{--tw-ring-color:var(--color-gray-100)}.focus\:ring-gray-200:focus{--tw-ring-color:var(--color-gray-200)}.focus\:ring-gray-300:focus{--tw-ring-color:var(--color-gray-300)}.focus\:ring-gray-400:focus{--tw-ring-color:var(--color-gray-400)}.focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.focus\:ring-gray-800:focus{--tw-ring-color:var(--color-gray-800)}.focus\:ring-gray-900:focus{--tw-ring-color:var(--color-gray-900)}.focus\:ring-green-500:focus{--tw-ring-color:var(--color-green-500)}.focus\:ring-indigo-500:focus{--tw-ring-color:var(--color-indigo-500)}.focus\:ring-primary-500:focus{--tw-ring-color:var(--color-primary-500)}.focus\:ring-purple-500:focus{--tw-ring-color:var(--color-purple-500)}.focus\:ring-red-500:focus{--tw-ring-color:var(--color-red-500)}.focus\:ring-transparent:focus{--tw-ring-color:transparent}.focus\:ring-yellow-500:focus{--tw-ring-color:var(--color-yellow-500)}.focus\:ring-offset-0:focus{--tw-ring-offset-width:0px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-1:focus{--tw-ring-offset-width:1px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-4:focus{--tw-ring-offset-width:4px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus\:ring-offset-gray-100:focus{--tw-ring-offset-color:var(--color-gray-100)}.focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.focus\:ring-offset-white:focus{--tw-ring-offset-color:var(--color-white)}.focus\:outline:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-0:focus{outline-style:var(--tw-outline-style);outline-width:0}.focus\:outline-1:focus{outline-style:var(--tw-outline-style);outline-width:1px}.focus\:outline-2:focus{outline-style:var(--tw-outline-style);outline-width:2px}.focus\:outline-4:focus{outline-style:var(--tw-outline-style);outline-width:4px}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus\:ring-inset:focus{--tw-ring-inset:inset}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-blue-500:focus-visible{--tw-ring-color:var(--color-blue-500)}.focus-visible\:ring-gray-500:focus-visible{--tw-ring-color:var(--color-gray-500)}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,)0 0 0 var(--tw-ring-offset-width)var(--tw-ring-offset-color)}.focus-visible\:outline:focus-visible{outline-style:var(--tw-outline-style);outline-width:1px}.focus-visible\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\:outline-offset-2:focus-visible{outline-offset:2px}.active\:scale-95:active{--tw-scale-x:95%;--tw-scale-y:95%;--tw-scale-z:95%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:scale-100:active{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x)var(--tw-scale-y)}.active\:cursor-grabbing:active{cursor:grabbing}.active\:bg-blue-600:active{background-color:var(--color-blue-600)}.active\:bg-blue-700:active{background-color:var(--color-blue-700)}.active\:bg-blue-800:active{background-color:var(--color-blue-800)}.active\:bg-gray-100:active{background-color:var(--color-gray-100)}.active\:bg-gray-200:active{background-color:var(--color-gray-200)}.active\:bg-gray-300:active{background-color:var(--color-gray-300)}.active\:bg-gray-700:active{background-color:var(--color-gray-700)}.active\:bg-gray-800:active{background-color:var(--color-gray-800)}.active\:bg-green-600:active{background-color:var(--color-green-600)}.active\:bg-green-700:active{background-color:var(--color-green-700)}.active\:bg-red-600:active{background-color:var(--color-red-600)}.active\:bg-red-700:active{background-color:var(--color-red-700)}.active\:opacity-75:active{opacity:.75}.active\:opacity-100:active{opacity:1}.disabled\:cursor-default:disabled{cursor:default}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:border-gray-200:disabled{border-color:var(--color-gray-200)}.disabled\:border-gray-300:disabled{border-color:var(--color-gray-300)}.disabled\:bg-gray-100:disabled{background-color:var(--color-gray-100)}.disabled\:bg-gray-200:disabled{background-color:var(--color-gray-200)}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:text-gray-300:disabled{color:var(--color-gray-300)}.disabled\:text-gray-400:disabled{color:var(--color-gray-400)}.disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.disabled\:no-underline:disabled{text-decoration-line:none}.disabled\:opacity-20:disabled{opacity:.2}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-50:disabled{opacity:.5}.disabled\:opacity-75:disabled{opacity:.75}@media(min-width:40rem){.sm\:col-span-1{grid-column:span 1/span 1}.sm\:col-span-2{grid-column:span 2/span 2}.sm\:col-span-3{grid-column:span 3/span 3}.sm\:col-span-4{grid-column:span 4/span 4}.sm\:col-span-6{grid-column:span 6/span 6}.sm\:col-span-12{grid-column:span 12/span 12}.sm\:col-span-full{grid-column:1/-1}.sm\:m-0{margin:calc(var(--spacing)*0)}.sm\:m-1{margin:calc(var(--spacing)*1)}.sm\:m-2{margin:calc(var(--spacing)*2)}.sm\:m-3{margin:calc(var(--spacing)*3)}.sm\:m-4{margin:calc(var(--spacing)*4)}.sm\:m-auto{margin:auto}.sm\:mx-auto{margin-inline:auto}.sm\:my-auto{margin-block:auto}.sm\:ml-auto{margin-left:auto}.sm\:block{display:block}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:inline{display:inline}.sm\:inline-block{display:inline-block}.sm\:inline-flex{display:inline-flex}.sm\:w-1\/2{width:50%}.sm\:w-1\/3{width:33.3333%}.sm\:w-1\/4{width:25%}.sm\:w-2\/3{width:66.6667%}.sm\:w-3\/4{width:75%}.sm\:w-64{width:calc(var(--spacing)*64)}.sm\:w-auto{width:auto}.sm\:w-full{width:100%}.sm\:min-w-48{min-width:calc(var(--spacing)*48)}.sm\:auto-cols-\[calc\(33\.333\%_-_11px\)\]{grid-auto-columns:calc(33.333% - 11px)}.sm\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.sm\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.sm\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.sm\:flex-col{flex-direction:column}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:items-start{align-items:flex-start}.sm\:justify-between{justify-content:space-between}.sm\:justify-center{justify-content:center}.sm\:justify-end{justify-content:flex-end}.sm\:justify-start{justify-content:flex-start}.sm\:gap-0{gap:calc(var(--spacing)*0)}.sm\:gap-1{gap:calc(var(--spacing)*1)}.sm\:gap-2{gap:calc(var(--spacing)*2)}.sm\:gap-3{gap:calc(var(--spacing)*3)}.sm\:gap-4{gap:calc(var(--spacing)*4)}.sm\:gap-5{gap:calc(var(--spacing)*5)}.sm\:gap-6{gap:calc(var(--spacing)*6)}.sm\:gap-8{gap:calc(var(--spacing)*8)}:where(.sm\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.sm\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.sm\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}.sm\:p-0{padding:calc(var(--spacing)*0)}.sm\:p-1{padding:calc(var(--spacing)*1)}.sm\:p-2{padding:calc(var(--spacing)*2)}.sm\:p-3{padding:calc(var(--spacing)*3)}.sm\:p-4{padding:calc(var(--spacing)*4)}.sm\:p-5{padding:calc(var(--spacing)*5)}.sm\:p-6{padding:calc(var(--spacing)*6)}.sm\:p-8{padding:calc(var(--spacing)*8)}.sm\:px-0{padding-inline:calc(var(--spacing)*0)}.sm\:px-1{padding-inline:calc(var(--spacing)*1)}.sm\:px-2{padding-inline:calc(var(--spacing)*2)}.sm\:px-3{padding-inline:calc(var(--spacing)*3)}.sm\:px-4{padding-inline:calc(var(--spacing)*4)}.sm\:px-5{padding-inline:calc(var(--spacing)*5)}.sm\:px-6{padding-inline:calc(var(--spacing)*6)}.sm\:px-8{padding-inline:calc(var(--spacing)*8)}.sm\:py-0{padding-block:calc(var(--spacing)*0)}.sm\:py-1{padding-block:calc(var(--spacing)*1)}.sm\:py-2{padding-block:calc(var(--spacing)*2)}.sm\:py-3{padding-block:calc(var(--spacing)*3)}.sm\:py-4{padding-block:calc(var(--spacing)*4)}.sm\:py-5{padding-block:calc(var(--spacing)*5)}.sm\:py-6{padding-block:calc(var(--spacing)*6)}.sm\:py-8{padding-block:calc(var(--spacing)*8)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.sm\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.sm\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:48rem){.md\:absolute{position:absolute}.md\:fixed{position:fixed}.md\:relative{position:relative}.md\:static{position:static}.md\:sticky{position:sticky}.md\:inset-0{inset:calc(var(--spacing)*0)}.md\:top-0{top:calc(var(--spacing)*0)}.md\:right-0{right:calc(var(--spacing)*0)}.md\:bottom-0{bottom:calc(var(--spacing)*0)}.md\:left-0{left:calc(var(--spacing)*0)}.md\:order-1{order:1}.md\:order-2{order:2}.md\:order-3{order:3}.md\:order-first{order:-9999}.md\:order-last{order:9999}.md\:order-none{order:0}.md\:col-span-1{grid-column:span 1/span 1}.md\:col-span-2{grid-column:span 2/span 2}.md\:col-span-3{grid-column:span 3/span 3}.md\:col-span-4{grid-column:span 4/span 4}.md\:col-span-5{grid-column:span 5/span 5}.md\:col-span-6{grid-column:span 6/span 6}.md\:col-span-12{grid-column:span 12/span 12}.md\:col-span-full{grid-column:1/-1}.md\:row-span-1{grid-row:span 1/span 1}.md\:row-span-2{grid-row:span 2/span 2}.md\:row-span-3{grid-row:span 3/span 3}.md\:row-span-4{grid-row:span 4/span 4}.md\:row-span-full{grid-row:1/-1}.md\:m-0{margin:calc(var(--spacing)*0)}.md\:m-1{margin:calc(var(--spacing)*1)}.md\:m-2{margin:calc(var(--spacing)*2)}.md\:m-3{margin:calc(var(--spacing)*3)}.md\:m-4{margin:calc(var(--spacing)*4)}.md\:m-auto{margin:auto}.md\:mx-auto{margin-inline:auto}.md\:my-auto{margin-block:auto}.md\:mt-0{margin-top:calc(var(--spacing)*0)}.md\:mt-1{margin-top:calc(var(--spacing)*1)}.md\:mt-2{margin-top:calc(var(--spacing)*2)}.md\:mt-3{margin-top:calc(var(--spacing)*3)}.md\:mt-4{margin-top:calc(var(--spacing)*4)}.md\:mt-6{margin-top:calc(var(--spacing)*6)}.md\:mt-8{margin-top:calc(var(--spacing)*8)}.md\:mr-0{margin-right:calc(var(--spacing)*0)}.md\:mr-1{margin-right:calc(var(--spacing)*1)}.md\:mr-2{margin-right:calc(var(--spacing)*2)}.md\:mr-3{margin-right:calc(var(--spacing)*3)}.md\:mr-4{margin-right:calc(var(--spacing)*4)}.md\:mr-6{margin-right:calc(var(--spacing)*6)}.md\:mr-8{margin-right:calc(var(--spacing)*8)}.md\:mr-auto{margin-right:auto}.md\:mb-0{margin-bottom:calc(var(--spacing)*0)}.md\:mb-1{margin-bottom:calc(var(--spacing)*1)}.md\:mb-2{margin-bottom:calc(var(--spacing)*2)}.md\:mb-3{margin-bottom:calc(var(--spacing)*3)}.md\:mb-4{margin-bottom:calc(var(--spacing)*4)}.md\:mb-6{margin-bottom:calc(var(--spacing)*6)}.md\:mb-8{margin-bottom:calc(var(--spacing)*8)}.md\:ml-0{margin-left:calc(var(--spacing)*0)}.md\:ml-1{margin-left:calc(var(--spacing)*1)}.md\:ml-2{margin-left:calc(var(--spacing)*2)}.md\:ml-3{margin-left:calc(var(--spacing)*3)}.md\:ml-4{margin-left:calc(var(--spacing)*4)}.md\:ml-6{margin-left:calc(var(--spacing)*6)}.md\:ml-8{margin-left:calc(var(--spacing)*8)}.md\:ml-auto{margin-left:auto}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:inline{display:inline}.md\:inline-block{display:inline-block}.md\:inline-flex{display:inline-flex}.md\:w-1\/2{width:50%}.md\:w-1\/3{width:33.3333%}.md\:w-1\/4{width:25%}.md\:w-1\/5{width:20%}.md\:w-2\/3{width:66.6667%}.md\:w-2\/5{width:40%}.md\:w-3\/4{width:75%}.md\:w-3\/5{width:60%}.md\:w-4\/5{width:80%}.md\:w-auto{width:auto}.md\:w-full{width:100%}.md\:max-w-2xl{max-width:var(--container-2xl)}.md\:max-w-3xl{max-width:var(--container-3xl)}.md\:max-w-4xl{max-width:var(--container-4xl)}.md\:max-w-5xl{max-width:var(--container-5xl)}.md\:max-w-6xl{max-width:var(--container-6xl)}.md\:max-w-7xl{max-width:var(--container-7xl)}.md\:max-w-full{max-width:100%}.md\:max-w-lg{max-width:var(--container-lg)}.md\:max-w-md{max-width:var(--container-md)}.md\:max-w-none{max-width:none}.md\:max-w-sm{max-width:var(--container-sm)}.md\:max-w-xl{max-width:var(--container-xl)}.md\:max-w-xs{max-width:var(--container-xs)}.md\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.md\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.md\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.md\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.md\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.md\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.md\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.md\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.md\:flex-col{flex-direction:column}.md\:flex-row{flex-direction:row}.md\:items-baseline{align-items:baseline}.md\:items-center{align-items:center}.md\:items-end{align-items:flex-end}.md\:items-start{align-items:flex-start}.md\:items-stretch{align-items:stretch}.md\:justify-around{justify-content:space-around}.md\:justify-between{justify-content:space-between}.md\:justify-center{justify-content:center}.md\:justify-end{justify-content:flex-end}.md\:justify-start{justify-content:flex-start}.md\:gap-0{gap:calc(var(--spacing)*0)}.md\:gap-1{gap:calc(var(--spacing)*1)}.md\:gap-2{gap:calc(var(--spacing)*2)}.md\:gap-3{gap:calc(var(--spacing)*3)}.md\:gap-4{gap:calc(var(--spacing)*4)}.md\:gap-5{gap:calc(var(--spacing)*5)}.md\:gap-6{gap:calc(var(--spacing)*6)}.md\:gap-8{gap:calc(var(--spacing)*8)}.md\:gap-10{gap:calc(var(--spacing)*10)}.md\:gap-12{gap:calc(var(--spacing)*12)}:where(.md\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.md\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.md\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.md\:p-0{padding:calc(var(--spacing)*0)}.md\:p-1{padding:calc(var(--spacing)*1)}.md\:p-2{padding:calc(var(--spacing)*2)}.md\:p-3{padding:calc(var(--spacing)*3)}.md\:p-4{padding:calc(var(--spacing)*4)}.md\:p-5{padding:calc(var(--spacing)*5)}.md\:p-6{padding:calc(var(--spacing)*6)}.md\:p-8{padding:calc(var(--spacing)*8)}.md\:p-10{padding:calc(var(--spacing)*10)}.md\:p-12{padding:calc(var(--spacing)*12)}.md\:px-0{padding-inline:calc(var(--spacing)*0)}.md\:px-1{padding-inline:calc(var(--spacing)*1)}.md\:px-2{padding-inline:calc(var(--spacing)*2)}.md\:px-3{padding-inline:calc(var(--spacing)*3)}.md\:px-4{padding-inline:calc(var(--spacing)*4)}.md\:px-5{padding-inline:calc(var(--spacing)*5)}.md\:px-6{padding-inline:calc(var(--spacing)*6)}.md\:px-8{padding-inline:calc(var(--spacing)*8)}.md\:px-10{padding-inline:calc(var(--spacing)*10)}.md\:px-12{padding-inline:calc(var(--spacing)*12)}.md\:py-0{padding-block:calc(var(--spacing)*0)}.md\:py-1{padding-block:calc(var(--spacing)*1)}.md\:py-2{padding-block:calc(var(--spacing)*2)}.md\:py-3{padding-block:calc(var(--spacing)*3)}.md\:py-4{padding-block:calc(var(--spacing)*4)}.md\:py-5{padding-block:calc(var(--spacing)*5)}.md\:py-6{padding-block:calc(var(--spacing)*6)}.md\:py-8{padding-block:calc(var(--spacing)*8)}.md\:py-10{padding-block:calc(var(--spacing)*10)}.md\:py-12{padding-block:calc(var(--spacing)*12)}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.md\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.md\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.md\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.md\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.md\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:64rem){.lg\:absolute{position:absolute}.lg\:fixed{position:fixed}.lg\:relative{position:relative}.lg\:static{position:static}.lg\:sticky{position:sticky}.lg\:inset-0{inset:calc(var(--spacing)*0)}.lg\:top-0{top:calc(var(--spacing)*0)}.lg\:right-0{right:calc(var(--spacing)*0)}.lg\:bottom-0{bottom:calc(var(--spacing)*0)}.lg\:left-0{left:calc(var(--spacing)*0)}.lg\:order-1{order:1}.lg\:order-2{order:2}.lg\:order-3{order:3}.lg\:order-4{order:4}.lg\:order-first{order:-9999}.lg\:order-last{order:9999}.lg\:order-none{order:0}.lg\:col-span-1{grid-column:span 1/span 1}.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:col-span-4{grid-column:span 4/span 4}.lg\:col-span-5{grid-column:span 5/span 5}.lg\:col-span-6{grid-column:span 6/span 6}.lg\:col-span-7{grid-column:span 7/span 7}.lg\:col-span-8{grid-column:span 8/span 8}.lg\:col-span-12{grid-column:span 12/span 12}.lg\:col-span-full{grid-column:1/-1}.lg\:row-span-1{grid-row:span 1/span 1}.lg\:row-span-2{grid-row:span 2/span 2}.lg\:row-span-3{grid-row:span 3/span 3}.lg\:row-span-4{grid-row:span 4/span 4}.lg\:row-span-full{grid-row:1/-1}.lg\:m-0{margin:calc(var(--spacing)*0)}.lg\:m-1{margin:calc(var(--spacing)*1)}.lg\:m-2{margin:calc(var(--spacing)*2)}.lg\:m-3{margin:calc(var(--spacing)*3)}.lg\:m-4{margin:calc(var(--spacing)*4)}.lg\:m-auto{margin:auto}.lg\:mx-auto{margin-inline:auto}.lg\:my-auto{margin-block:auto}.lg\:mt-0{margin-top:calc(var(--spacing)*0)}.lg\:mt-1{margin-top:calc(var(--spacing)*1)}.lg\:mt-2{margin-top:calc(var(--spacing)*2)}.lg\:mt-3{margin-top:calc(var(--spacing)*3)}.lg\:mt-4{margin-top:calc(var(--spacing)*4)}.lg\:mt-6{margin-top:calc(var(--spacing)*6)}.lg\:mt-8{margin-top:calc(var(--spacing)*8)}.lg\:mt-10{margin-top:calc(var(--spacing)*10)}.lg\:mt-12{margin-top:calc(var(--spacing)*12)}.lg\:mr-0{margin-right:calc(var(--spacing)*0)}.lg\:mr-1{margin-right:calc(var(--spacing)*1)}.lg\:mr-2{margin-right:calc(var(--spacing)*2)}.lg\:mr-3{margin-right:calc(var(--spacing)*3)}.lg\:mr-4{margin-right:calc(var(--spacing)*4)}.lg\:mr-6{margin-right:calc(var(--spacing)*6)}.lg\:mr-8{margin-right:calc(var(--spacing)*8)}.lg\:mr-auto{margin-right:auto}.lg\:mb-0{margin-bottom:calc(var(--spacing)*0)}.lg\:mb-1{margin-bottom:calc(var(--spacing)*1)}.lg\:mb-2{margin-bottom:calc(var(--spacing)*2)}.lg\:mb-3{margin-bottom:calc(var(--spacing)*3)}.lg\:mb-4{margin-bottom:calc(var(--spacing)*4)}.lg\:mb-6{margin-bottom:calc(var(--spacing)*6)}.lg\:mb-8{margin-bottom:calc(var(--spacing)*8)}.lg\:mb-10{margin-bottom:calc(var(--spacing)*10)}.lg\:mb-12{margin-bottom:calc(var(--spacing)*12)}.lg\:ml-0{margin-left:calc(var(--spacing)*0)}.lg\:ml-1{margin-left:calc(var(--spacing)*1)}.lg\:ml-2{margin-left:calc(var(--spacing)*2)}.lg\:ml-3{margin-left:calc(var(--spacing)*3)}.lg\:ml-4{margin-left:calc(var(--spacing)*4)}.lg\:ml-6{margin-left:calc(var(--spacing)*6)}.lg\:ml-8{margin-left:calc(var(--spacing)*8)}.lg\:ml-auto{margin-left:auto}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:grid{display:grid}.lg\:hidden{display:none}.lg\:inline{display:inline}.lg\:inline-block{display:inline-block}.lg\:inline-flex{display:inline-flex}.lg\:w-1\/2{width:50%}.lg\:w-1\/3{width:33.3333%}.lg\:w-1\/4{width:25%}.lg\:w-1\/5{width:20%}.lg\:w-1\/6{width:16.6667%}.lg\:w-2\/3{width:66.6667%}.lg\:w-2\/5{width:40%}.lg\:w-3\/4{width:75%}.lg\:w-3\/5{width:60%}.lg\:w-4\/5{width:80%}.lg\:w-5\/6{width:83.3333%}.lg\:w-auto{width:auto}.lg\:w-full{width:100%}.lg\:max-w-2xl{max-width:var(--container-2xl)}.lg\:max-w-3xl{max-width:var(--container-3xl)}.lg\:max-w-4xl{max-width:var(--container-4xl)}.lg\:max-w-5xl{max-width:var(--container-5xl)}.lg\:max-w-6xl{max-width:var(--container-6xl)}.lg\:max-w-7xl{max-width:var(--container-7xl)}.lg\:max-w-full{max-width:100%}.lg\:max-w-lg{max-width:var(--container-lg)}.lg\:max-w-md{max-width:var(--container-md)}.lg\:max-w-none{max-width:none}.lg\:max-w-sm{max-width:var(--container-sm)}.lg\:max-w-xl{max-width:var(--container-xl)}.lg\:max-w-xs{max-width:var(--container-xs)}.lg\:auto-cols-\[calc\(25\%_-_12px\)\]{grid-auto-columns:calc(25% - 12px)}.lg\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.lg\:grid-cols-8{grid-template-columns:repeat(8,minmax(0,1fr))}.lg\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.lg\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.lg\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.lg\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.lg\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.lg\:grid-rows-5{grid-template-rows:repeat(5,minmax(0,1fr))}.lg\:grid-rows-6{grid-template-rows:repeat(6,minmax(0,1fr))}.lg\:flex-col{flex-direction:column}.lg\:flex-row{flex-direction:row}.lg\:items-baseline{align-items:baseline}.lg\:items-center{align-items:center}.lg\:items-end{align-items:flex-end}.lg\:items-start{align-items:flex-start}.lg\:items-stretch{align-items:stretch}.lg\:justify-around{justify-content:space-around}.lg\:justify-between{justify-content:space-between}.lg\:justify-center{justify-content:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-evenly{justify-content:space-evenly}.lg\:justify-start{justify-content:flex-start}.lg\:gap-0{gap:calc(var(--spacing)*0)}.lg\:gap-1{gap:calc(var(--spacing)*1)}.lg\:gap-2{gap:calc(var(--spacing)*2)}.lg\:gap-3{gap:calc(var(--spacing)*3)}.lg\:gap-4{gap:calc(var(--spacing)*4)}.lg\:gap-5{gap:calc(var(--spacing)*5)}.lg\:gap-6{gap:calc(var(--spacing)*6)}.lg\:gap-8{gap:calc(var(--spacing)*8)}.lg\:gap-10{gap:calc(var(--spacing)*10)}.lg\:gap-12{gap:calc(var(--spacing)*12)}.lg\:gap-16{gap:calc(var(--spacing)*16)}:where(.lg\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-y-10>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*10)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-y-reverse)))}:where(.lg\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}:where(.lg\:space-x-10>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*10)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*10)*calc(1 - var(--tw-space-x-reverse)))}.lg\:p-0{padding:calc(var(--spacing)*0)}.lg\:p-1{padding:calc(var(--spacing)*1)}.lg\:p-2{padding:calc(var(--spacing)*2)}.lg\:p-3{padding:calc(var(--spacing)*3)}.lg\:p-4{padding:calc(var(--spacing)*4)}.lg\:p-5{padding:calc(var(--spacing)*5)}.lg\:p-6{padding:calc(var(--spacing)*6)}.lg\:p-8{padding:calc(var(--spacing)*8)}.lg\:p-10{padding:calc(var(--spacing)*10)}.lg\:p-12{padding:calc(var(--spacing)*12)}.lg\:p-16{padding:calc(var(--spacing)*16)}.lg\:px-0{padding-inline:calc(var(--spacing)*0)}.lg\:px-1{padding-inline:calc(var(--spacing)*1)}.lg\:px-2{padding-inline:calc(var(--spacing)*2)}.lg\:px-3{padding-inline:calc(var(--spacing)*3)}.lg\:px-4{padding-inline:calc(var(--spacing)*4)}.lg\:px-5{padding-inline:calc(var(--spacing)*5)}.lg\:px-6{padding-inline:calc(var(--spacing)*6)}.lg\:px-8{padding-inline:calc(var(--spacing)*8)}.lg\:px-10{padding-inline:calc(var(--spacing)*10)}.lg\:px-12{padding-inline:calc(var(--spacing)*12)}.lg\:px-16{padding-inline:calc(var(--spacing)*16)}.lg\:py-0{padding-block:calc(var(--spacing)*0)}.lg\:py-1{padding-block:calc(var(--spacing)*1)}.lg\:py-2{padding-block:calc(var(--spacing)*2)}.lg\:py-3{padding-block:calc(var(--spacing)*3)}.lg\:py-4{padding-block:calc(var(--spacing)*4)}.lg\:py-5{padding-block:calc(var(--spacing)*5)}.lg\:py-6{padding-block:calc(var(--spacing)*6)}.lg\:py-8{padding-block:calc(var(--spacing)*8)}.lg\:py-10{padding-block:calc(var(--spacing)*10)}.lg\:py-12{padding-block:calc(var(--spacing)*12)}.lg\:py-16{padding-block:calc(var(--spacing)*16)}.lg\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.lg\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.lg\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.lg\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.lg\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.lg\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.lg\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.lg\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.lg\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:80rem){.xl\:col-span-1{grid-column:span 1/span 1}.xl\:col-span-2{grid-column:span 2/span 2}.xl\:col-span-3{grid-column:span 3/span 3}.xl\:col-span-4{grid-column:span 4/span 4}.xl\:col-span-5{grid-column:span 5/span 5}.xl\:col-span-6{grid-column:span 6/span 6}.xl\:col-span-12{grid-column:span 12/span 12}.xl\:col-span-full{grid-column:1/-1}.xl\:row-span-1{grid-row:span 1/span 1}.xl\:row-span-2{grid-row:span 2/span 2}.xl\:row-span-3{grid-row:span 3/span 3}.xl\:row-span-full{grid-row:1/-1}.xl\:m-0{margin:calc(var(--spacing)*0)}.xl\:m-auto{margin:auto}.xl\:mx-auto{margin-inline:auto}.xl\:my-auto{margin-block:auto}.xl\:block{display:block}.xl\:flex{display:flex}.xl\:hidden{display:none}.xl\:inline{display:inline}.xl\:inline-block{display:inline-block}.xl\:inline-flex{display:inline-flex}.xl\:w-1\/2{width:50%}.xl\:w-1\/3{width:33.3333%}.xl\:w-1\/4{width:25%}.xl\:w-1\/5{width:20%}.xl\:w-2\/3{width:66.6667%}.xl\:w-2\/5{width:40%}.xl\:w-3\/4{width:75%}.xl\:w-3\/5{width:60%}.xl\:w-4\/5{width:80%}.xl\:w-auto{width:auto}.xl\:w-full{width:100%}.xl\:max-w-2xl{max-width:var(--container-2xl)}.xl\:max-w-3xl{max-width:var(--container-3xl)}.xl\:max-w-4xl{max-width:var(--container-4xl)}.xl\:max-w-5xl{max-width:var(--container-5xl)}.xl\:max-w-6xl{max-width:var(--container-6xl)}.xl\:max-w-7xl{max-width:var(--container-7xl)}.xl\:max-w-full{max-width:100%}.xl\:max-w-lg{max-width:var(--container-lg)}.xl\:max-w-md{max-width:var(--container-md)}.xl\:max-w-none{max-width:none}.xl\:max-w-sm{max-width:var(--container-sm)}.xl\:max-w-xl{max-width:var(--container-xl)}.xl\:max-w-xs{max-width:var(--container-xs)}.xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.xl\:grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.xl\:grid-rows-2{grid-template-rows:repeat(2,minmax(0,1fr))}.xl\:grid-rows-3{grid-template-rows:repeat(3,minmax(0,1fr))}.xl\:grid-rows-4{grid-template-rows:repeat(4,minmax(0,1fr))}.xl\:flex-col{flex-direction:column}.xl\:flex-row{flex-direction:row}.xl\:items-center{align-items:center}.xl\:items-end{align-items:flex-end}.xl\:items-start{align-items:flex-start}.xl\:justify-between{justify-content:space-between}.xl\:justify-center{justify-content:center}.xl\:justify-end{justify-content:flex-end}.xl\:justify-start{justify-content:flex-start}.xl\:gap-0{gap:calc(var(--spacing)*0)}.xl\:gap-1{gap:calc(var(--spacing)*1)}.xl\:gap-2{gap:calc(var(--spacing)*2)}.xl\:gap-3{gap:calc(var(--spacing)*3)}.xl\:gap-4{gap:calc(var(--spacing)*4)}.xl\:gap-5{gap:calc(var(--spacing)*5)}.xl\:gap-6{gap:calc(var(--spacing)*6)}.xl\:gap-8{gap:calc(var(--spacing)*8)}.xl\:gap-10{gap:calc(var(--spacing)*10)}.xl\:gap-12{gap:calc(var(--spacing)*12)}.xl\:gap-16{gap:calc(var(--spacing)*16)}:where(.xl\:space-y-0>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*0)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*1)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*2)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*3)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*4)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*8)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-y-reverse)))}:where(.xl\:space-x-0>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*0)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*0)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-1>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*1)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*1)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*2)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*2)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*3)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*3)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*4)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*4)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-6>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*6)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-x-reverse)))}:where(.xl\:space-x-8>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing)*8)*var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing)*8)*calc(1 - var(--tw-space-x-reverse)))}.xl\:p-0{padding:calc(var(--spacing)*0)}.xl\:p-1{padding:calc(var(--spacing)*1)}.xl\:p-2{padding:calc(var(--spacing)*2)}.xl\:p-3{padding:calc(var(--spacing)*3)}.xl\:p-4{padding:calc(var(--spacing)*4)}.xl\:p-5{padding:calc(var(--spacing)*5)}.xl\:p-6{padding:calc(var(--spacing)*6)}.xl\:p-8{padding:calc(var(--spacing)*8)}.xl\:p-10{padding:calc(var(--spacing)*10)}.xl\:p-12{padding:calc(var(--spacing)*12)}.xl\:p-16{padding:calc(var(--spacing)*16)}.xl\:px-0{padding-inline:calc(var(--spacing)*0)}.xl\:px-1{padding-inline:calc(var(--spacing)*1)}.xl\:px-2{padding-inline:calc(var(--spacing)*2)}.xl\:px-3{padding-inline:calc(var(--spacing)*3)}.xl\:px-4{padding-inline:calc(var(--spacing)*4)}.xl\:px-5{padding-inline:calc(var(--spacing)*5)}.xl\:px-6{padding-inline:calc(var(--spacing)*6)}.xl\:px-8{padding-inline:calc(var(--spacing)*8)}.xl\:px-10{padding-inline:calc(var(--spacing)*10)}.xl\:px-12{padding-inline:calc(var(--spacing)*12)}.xl\:px-16{padding-inline:calc(var(--spacing)*16)}.xl\:py-0{padding-block:calc(var(--spacing)*0)}.xl\:py-1{padding-block:calc(var(--spacing)*1)}.xl\:py-2{padding-block:calc(var(--spacing)*2)}.xl\:py-3{padding-block:calc(var(--spacing)*3)}.xl\:py-4{padding-block:calc(var(--spacing)*4)}.xl\:py-5{padding-block:calc(var(--spacing)*5)}.xl\:py-6{padding-block:calc(var(--spacing)*6)}.xl\:py-8{padding-block:calc(var(--spacing)*8)}.xl\:py-10{padding-block:calc(var(--spacing)*10)}.xl\:py-12{padding-block:calc(var(--spacing)*12)}.xl\:py-16{padding-block:calc(var(--spacing)*16)}.xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.xl\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}@media(min-width:96rem){.\32xl\:col-span-1{grid-column:span 1/span 1}.\32xl\:col-span-2{grid-column:span 2/span 2}.\32xl\:col-span-3{grid-column:span 3/span 3}.\32xl\:col-span-4{grid-column:span 4/span 4}.\32xl\:col-span-6{grid-column:span 6/span 6}.\32xl\:col-span-12{grid-column:span 12/span 12}.\32xl\:block{display:block}.\32xl\:flex{display:flex}.\32xl\:hidden{display:none}.\32xl\:w-1\/2{width:50%}.\32xl\:w-1\/3{width:33.3333%}.\32xl\:w-1\/4{width:25%}.\32xl\:w-2\/3{width:66.6667%}.\32xl\:w-3\/4{width:75%}.\32xl\:w-auto{width:auto}.\32xl\:w-full{width:100%}.\32xl\:max-w-2xl{max-width:var(--container-2xl)}.\32xl\:max-w-3xl{max-width:var(--container-3xl)}.\32xl\:max-w-4xl{max-width:var(--container-4xl)}.\32xl\:max-w-5xl{max-width:var(--container-5xl)}.\32xl\:max-w-6xl{max-width:var(--container-6xl)}.\32xl\:max-w-7xl{max-width:var(--container-7xl)}.\32xl\:max-w-full{max-width:100%}.\32xl\:max-w-lg{max-width:var(--container-lg)}.\32xl\:max-w-md{max-width:var(--container-md)}.\32xl\:max-w-sm{max-width:var(--container-sm)}.\32xl\:max-w-xl{max-width:var(--container-xl)}.\32xl\:max-w-xs{max-width:var(--container-xs)}.\32xl\:grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.\32xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.\32xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.\32xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.\32xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.\32xl\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.\32xl\:grid-cols-12{grid-template-columns:repeat(12,minmax(0,1fr))}.\32xl\:gap-0{gap:calc(var(--spacing)*0)}.\32xl\:gap-4{gap:calc(var(--spacing)*4)}.\32xl\:gap-6{gap:calc(var(--spacing)*6)}.\32xl\:gap-8{gap:calc(var(--spacing)*8)}.\32xl\:gap-12{gap:calc(var(--spacing)*12)}.\32xl\:gap-16{gap:calc(var(--spacing)*16)}.\32xl\:p-0{padding:calc(var(--spacing)*0)}.\32xl\:p-4{padding:calc(var(--spacing)*4)}.\32xl\:p-6{padding:calc(var(--spacing)*6)}.\32xl\:p-8{padding:calc(var(--spacing)*8)}.\32xl\:p-12{padding:calc(var(--spacing)*12)}.\32xl\:p-16{padding:calc(var(--spacing)*16)}.\32xl\:px-0{padding-inline:calc(var(--spacing)*0)}.\32xl\:px-4{padding-inline:calc(var(--spacing)*4)}.\32xl\:px-6{padding-inline:calc(var(--spacing)*6)}.\32xl\:px-8{padding-inline:calc(var(--spacing)*8)}.\32xl\:px-12{padding-inline:calc(var(--spacing)*12)}.\32xl\:px-16{padding-inline:calc(var(--spacing)*16)}.\32xl\:py-0{padding-block:calc(var(--spacing)*0)}.\32xl\:py-4{padding-block:calc(var(--spacing)*4)}.\32xl\:py-6{padding-block:calc(var(--spacing)*6)}.\32xl\:py-8{padding-block:calc(var(--spacing)*8)}.\32xl\:py-12{padding-block:calc(var(--spacing)*12)}.\32xl\:py-16{padding-block:calc(var(--spacing)*16)}.\32xl\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.\32xl\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.\32xl\:text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.\32xl\:text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.\32xl\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.\32xl\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.\32xl\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.\32xl\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.\32xl\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}}.dark .dark\:block{display:block}.dark .dark\:flex{display:flex}.dark .dark\:flex-1{flex:1}.dark .dark\:flex-auto{flex:auto}.dark .dark\:flex-none{flex:none}.dark .dark\:flex-col{flex-direction:column}.dark .dark\:flex-row{flex-direction:row}.dark .dark\:flex-wrap{flex-wrap:wrap}.dark .dark\:items-center{align-items:center}.dark .dark\:items-end{align-items:flex-end}.dark .dark\:items-start{align-items:flex-start}.dark .dark\:items-stretch{align-items:stretch}.dark .dark\:justify-around{justify-content:space-around}.dark .dark\:justify-between{justify-content:space-between}.dark .dark\:justify-center{justify-content:center}.dark .dark\:justify-end{justify-content:flex-end}.dark .dark\:justify-start{justify-content:flex-start}:where(.dark .dark\:divide-gray-50>:not(:last-child)){border-color:var(--color-gray-50)}:where(.dark .dark\:divide-gray-100>:not(:last-child)){border-color:var(--color-gray-100)}:where(.dark .dark\:divide-gray-200>:not(:last-child)){border-color:var(--color-gray-200)}:where(.dark .dark\:divide-gray-300>:not(:last-child)){border-color:var(--color-gray-300)}:where(.dark .dark\:divide-gray-400>:not(:last-child)){border-color:var(--color-gray-400)}:where(.dark .dark\:divide-gray-500>:not(:last-child)){border-color:var(--color-gray-500)}:where(.dark .dark\:divide-gray-600>:not(:last-child)){border-color:var(--color-gray-600)}:where(.dark .dark\:divide-gray-700>:not(:last-child)){border-color:var(--color-gray-700)}:where(.dark .dark\:divide-gray-800>:not(:last-child)){border-color:var(--color-gray-800)}:where(.dark .dark\:divide-gray-900>:not(:last-child)){border-color:var(--color-gray-900)}.dark .dark\:self-center{align-self:center}.dark .dark\:self-end{align-self:flex-end}.dark .dark\:self-start{align-self:flex-start}.dark .dark\:self-stretch{align-self:stretch}.dark .dark\:overflow-auto{overflow:auto}.dark .dark\:overflow-hidden{overflow:hidden}.dark .dark\:overflow-visible{overflow:visible}.dark .dark\:overflow-x-auto{overflow-x:auto}.dark .dark\:overflow-y-auto{overflow-y:auto}.dark .dark\:rounded{border-radius:.25rem}.dark .dark\:rounded-full{border-radius:3.40282e38px}.dark .dark\:rounded-lg{border-radius:var(--radius-lg)}.dark .dark\:rounded-none{border-radius:0}.dark .dark\:rounded-sm{border-radius:var(--radius-sm)}.dark .dark\:rounded-xl{border-radius:var(--radius-xl)}.dark .dark\:border{border-style:var(--tw-border-style);border-width:1px}.dark .dark\:border-0{border-style:var(--tw-border-style);border-width:0}.dark .dark\:border-dashed{--tw-border-style:dashed;border-style:dashed}.dark .dark\:border-dotted{--tw-border-style:dotted;border-style:dotted}.dark .dark\:border-solid{--tw-border-style:solid;border-style:solid}.dark .dark\:border-black{border-color:var(--color-black)}.dark .dark\:border-blue-50{border-color:var(--color-blue-50)}.dark .dark\:border-blue-100{border-color:var(--color-blue-100)}.dark .dark\:border-blue-200{border-color:var(--color-blue-200)}.dark .dark\:border-blue-300{border-color:var(--color-blue-300)}.dark .dark\:border-blue-400{border-color:var(--color-blue-400)}.dark .dark\:border-blue-500{border-color:var(--color-blue-500)}.dark .dark\:border-blue-600{border-color:var(--color-blue-600)}.dark .dark\:border-blue-700{border-color:var(--color-blue-700)}.dark .dark\:border-blue-800{border-color:var(--color-blue-800)}.dark .dark\:border-blue-900{border-color:var(--color-blue-900)}.dark .dark\:border-gray-50{border-color:var(--color-gray-50)}.dark .dark\:border-gray-100{border-color:var(--color-gray-100)}.dark .dark\:border-gray-200{border-color:var(--color-gray-200)}.dark .dark\:border-gray-300{border-color:var(--color-gray-300)}.dark .dark\:border-gray-400{border-color:var(--color-gray-400)}.dark .dark\:border-gray-500{border-color:var(--color-gray-500)}.dark .dark\:border-gray-600{border-color:var(--color-gray-600)}.dark .dark\:border-gray-700{border-color:var(--color-gray-700)}.dark .dark\:border-gray-700\/50{border-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-gray-700\/50{border-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:border-gray-800{border-color:var(--color-gray-800)}.dark .dark\:border-gray-900{border-color:var(--color-gray-900)}.dark .dark\:border-gray-900\/30{border-color:#1018284d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-gray-900\/30{border-color:color-mix(in oklab,var(--color-gray-900)30%,transparent)}}.dark .dark\:border-gray-950{border-color:var(--color-gray-950)}.dark .dark\:border-green-600{border-color:var(--color-green-600)}.dark .dark\:border-green-700{border-color:var(--color-green-700)}.dark .dark\:border-green-800{border-color:var(--color-green-800)}.dark .dark\:border-orange-600{border-color:var(--color-orange-600)}.dark .dark\:border-orange-800{border-color:var(--color-orange-800)}.dark .dark\:border-red-50{border-color:var(--color-red-50)}.dark .dark\:border-red-100{border-color:var(--color-red-100)}.dark .dark\:border-red-200{border-color:var(--color-red-200)}.dark .dark\:border-red-300{border-color:var(--color-red-300)}.dark .dark\:border-red-400{border-color:var(--color-red-400)}.dark .dark\:border-red-500{border-color:var(--color-red-500)}.dark .dark\:border-red-600{border-color:var(--color-red-600)}.dark .dark\:border-red-700{border-color:var(--color-red-700)}.dark .dark\:border-red-800{border-color:var(--color-red-800)}.dark .dark\:border-red-900{border-color:var(--color-red-900)}.dark .dark\:border-red-900\/50{border-color:#82181a80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:border-red-900\/50{border-color:color-mix(in oklab,var(--color-red-900)50%,transparent)}}.dark .dark\:border-slate-50{border-color:var(--color-slate-50)}.dark .dark\:border-slate-100{border-color:var(--color-slate-100)}.dark .dark\:border-slate-200{border-color:var(--color-slate-200)}.dark .dark\:border-slate-300{border-color:var(--color-slate-300)}.dark .dark\:border-slate-400{border-color:var(--color-slate-400)}.dark .dark\:border-slate-500{border-color:var(--color-slate-500)}.dark .dark\:border-slate-600{border-color:var(--color-slate-600)}.dark .dark\:border-slate-700{border-color:var(--color-slate-700)}.dark .dark\:border-slate-800{border-color:var(--color-slate-800)}.dark .dark\:border-slate-900{border-color:var(--color-slate-900)}.dark .dark\:border-slate-950{border-color:var(--color-slate-950)}.dark .dark\:border-transparent{border-color:#0000}.dark .dark\:border-white{border-color:var(--color-white)}.dark .dark\:border-yellow-800{border-color:var(--color-yellow-800)}.dark .dark\:border-t-gray-900{border-top-color:var(--color-gray-900)}.dark .dark\:border-t-white{border-top-color:var(--color-white)}.dark .dark\:bg-amber-900\/30{background-color:#7b33064d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-amber-900\/30{background-color:color-mix(in oklab,var(--color-amber-900)30%,transparent)}}.dark .dark\:bg-black{background-color:var(--color-black)}.dark .dark\:bg-blue-50{background-color:var(--color-blue-50)}.dark .dark\:bg-blue-100{background-color:var(--color-blue-100)}.dark .dark\:bg-blue-200{background-color:var(--color-blue-200)}.dark .dark\:bg-blue-300{background-color:var(--color-blue-300)}.dark .dark\:bg-blue-400{background-color:var(--color-blue-400)}.dark .dark\:bg-blue-500{background-color:var(--color-blue-500)}.dark .dark\:bg-blue-600{background-color:var(--color-blue-600)}.dark .dark\:bg-blue-700{background-color:var(--color-blue-700)}.dark .dark\:bg-blue-800{background-color:var(--color-blue-800)}.dark .dark\:bg-blue-900{background-color:var(--color-blue-900)}.dark .dark\:bg-blue-900\/10{background-color:#1c398e1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/10{background-color:color-mix(in oklab,var(--color-blue-900)10%,transparent)}}.dark .dark\:bg-blue-900\/20{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/20{background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark .dark\:bg-blue-900\/30{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/30{background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark .dark\:bg-blue-900\/40{background-color:#1c398e66}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/40{background-color:color-mix(in oklab,var(--color-blue-900)40%,transparent)}}.dark .dark\:bg-blue-900\/50{background-color:#1c398e80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-900\/50{background-color:color-mix(in oklab,var(--color-blue-900)50%,transparent)}}.dark .dark\:bg-blue-950{background-color:var(--color-blue-950)}.dark .dark\:bg-blue-950\/50{background-color:#16245680}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-blue-950\/50{background-color:color-mix(in oklab,var(--color-blue-950)50%,transparent)}}.dark .dark\:bg-emerald-900{background-color:var(--color-emerald-900)}.dark .dark\:bg-gray-50{background-color:var(--color-gray-50)}.dark .dark\:bg-gray-100{background-color:var(--color-gray-100)}.dark .dark\:bg-gray-200{background-color:var(--color-gray-200)}.dark .dark\:bg-gray-300{background-color:var(--color-gray-300)}.dark .dark\:bg-gray-400{background-color:var(--color-gray-400)}.dark .dark\:bg-gray-500{background-color:var(--color-gray-500)}.dark .dark\:bg-gray-600{background-color:var(--color-gray-600)}.dark .dark\:bg-gray-700{background-color:var(--color-gray-700)}.dark .dark\:bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-700\/50{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:bg-gray-800{background-color:var(--color-gray-800)}.dark .dark\:bg-gray-800\/50{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/50{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.dark .dark\:bg-gray-800\/80{background-color:#1e2939cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/80{background-color:color-mix(in oklab,var(--color-gray-800)80%,transparent)}}.dark .dark\:bg-gray-800\/90{background-color:#1e2939e6}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-800\/90{background-color:color-mix(in oklab,var(--color-gray-800)90%,transparent)}}.dark .dark\:bg-gray-900{background-color:var(--color-gray-900)}.dark .dark\:bg-gray-900\/30{background-color:#1018284d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/30{background-color:color-mix(in oklab,var(--color-gray-900)30%,transparent)}}.dark .dark\:bg-gray-900\/40{background-color:#10182866}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/40{background-color:color-mix(in oklab,var(--color-gray-900)40%,transparent)}}.dark .dark\:bg-gray-900\/50{background-color:#10182880}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-gray-900\/50{background-color:color-mix(in oklab,var(--color-gray-900)50%,transparent)}}.dark .dark\:bg-gray-950{background-color:var(--color-gray-950)}.dark .dark\:bg-green-50{background-color:var(--color-green-50)}.dark .dark\:bg-green-100{background-color:var(--color-green-100)}.dark .dark\:bg-green-200{background-color:var(--color-green-200)}.dark .dark\:bg-green-300{background-color:var(--color-green-300)}.dark .dark\:bg-green-400{background-color:var(--color-green-400)}.dark .dark\:bg-green-500{background-color:var(--color-green-500)}.dark .dark\:bg-green-600{background-color:var(--color-green-600)}.dark .dark\:bg-green-700{background-color:var(--color-green-700)}.dark .dark\:bg-green-800{background-color:var(--color-green-800)}.dark .dark\:bg-green-900{background-color:var(--color-green-900)}.dark .dark\:bg-green-900\/20{background-color:#0d542b33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/20{background-color:color-mix(in oklab,var(--color-green-900)20%,transparent)}}.dark .dark\:bg-green-900\/30{background-color:#0d542b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/30{background-color:color-mix(in oklab,var(--color-green-900)30%,transparent)}}.dark .dark\:bg-green-900\/50{background-color:#0d542b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-green-900\/50{background-color:color-mix(in oklab,var(--color-green-900)50%,transparent)}}.dark .dark\:bg-green-950{background-color:var(--color-green-950)}.dark .dark\:bg-indigo-50{background-color:var(--color-indigo-50)}.dark .dark\:bg-indigo-100{background-color:var(--color-indigo-100)}.dark .dark\:bg-indigo-200{background-color:var(--color-indigo-200)}.dark .dark\:bg-indigo-300{background-color:var(--color-indigo-300)}.dark .dark\:bg-indigo-400{background-color:var(--color-indigo-400)}.dark .dark\:bg-indigo-500{background-color:var(--color-indigo-500)}.dark .dark\:bg-indigo-600{background-color:var(--color-indigo-600)}.dark .dark\:bg-indigo-700{background-color:var(--color-indigo-700)}.dark .dark\:bg-indigo-800{background-color:var(--color-indigo-800)}.dark .dark\:bg-indigo-900{background-color:var(--color-indigo-900)}.dark .dark\:bg-indigo-900\/30{background-color:#312c854d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-indigo-900\/30{background-color:color-mix(in oklab,var(--color-indigo-900)30%,transparent)}}.dark .dark\:bg-indigo-950{background-color:var(--color-indigo-950)}.dark .dark\:bg-orange-50{background-color:var(--color-orange-50)}.dark .dark\:bg-orange-100{background-color:var(--color-orange-100)}.dark .dark\:bg-orange-200{background-color:var(--color-orange-200)}.dark .dark\:bg-orange-300{background-color:var(--color-orange-300)}.dark .dark\:bg-orange-400{background-color:var(--color-orange-400)}.dark .dark\:bg-orange-500{background-color:var(--color-orange-500)}.dark .dark\:bg-orange-600{background-color:var(--color-orange-600)}.dark .dark\:bg-orange-700{background-color:var(--color-orange-700)}.dark .dark\:bg-orange-800{background-color:var(--color-orange-800)}.dark .dark\:bg-orange-900{background-color:var(--color-orange-900)}.dark .dark\:bg-orange-900\/20{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/20{background-color:color-mix(in oklab,var(--color-orange-900)20%,transparent)}}.dark .dark\:bg-orange-900\/30{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/30{background-color:color-mix(in oklab,var(--color-orange-900)30%,transparent)}}.dark .dark\:bg-orange-900\/50{background-color:#7e2a0c80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-orange-900\/50{background-color:color-mix(in oklab,var(--color-orange-900)50%,transparent)}}.dark .dark\:bg-orange-950{background-color:var(--color-orange-950)}.dark .dark\:bg-pink-50{background-color:var(--color-pink-50)}.dark .dark\:bg-pink-100{background-color:var(--color-pink-100)}.dark .dark\:bg-pink-200{background-color:var(--color-pink-200)}.dark .dark\:bg-pink-300{background-color:var(--color-pink-300)}.dark .dark\:bg-pink-400{background-color:var(--color-pink-400)}.dark .dark\:bg-pink-500{background-color:var(--color-pink-500)}.dark .dark\:bg-pink-600{background-color:var(--color-pink-600)}.dark .dark\:bg-pink-700{background-color:var(--color-pink-700)}.dark .dark\:bg-pink-800{background-color:var(--color-pink-800)}.dark .dark\:bg-pink-900{background-color:var(--color-pink-900)}.dark .dark\:bg-pink-950{background-color:var(--color-pink-950)}.dark .dark\:bg-primary-900\/30{background-color:#1e3a8a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-primary-900\/30{background-color:color-mix(in oklab,var(--color-primary-900)30%,transparent)}}.dark .dark\:bg-purple-50{background-color:var(--color-purple-50)}.dark .dark\:bg-purple-100{background-color:var(--color-purple-100)}.dark .dark\:bg-purple-200{background-color:var(--color-purple-200)}.dark .dark\:bg-purple-300{background-color:var(--color-purple-300)}.dark .dark\:bg-purple-400{background-color:var(--color-purple-400)}.dark .dark\:bg-purple-500{background-color:var(--color-purple-500)}.dark .dark\:bg-purple-500\/80{background-color:#ac4bffcc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-500\/80{background-color:color-mix(in oklab,var(--color-purple-500)80%,transparent)}}.dark .dark\:bg-purple-600{background-color:var(--color-purple-600)}.dark .dark\:bg-purple-700{background-color:var(--color-purple-700)}.dark .dark\:bg-purple-800{background-color:var(--color-purple-800)}.dark .dark\:bg-purple-900{background-color:var(--color-purple-900)}.dark .dark\:bg-purple-900\/30{background-color:#59168b4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/30{background-color:color-mix(in oklab,var(--color-purple-900)30%,transparent)}}.dark .dark\:bg-purple-900\/50{background-color:#59168b80}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-purple-900\/50{background-color:color-mix(in oklab,var(--color-purple-900)50%,transparent)}}.dark .dark\:bg-purple-950{background-color:var(--color-purple-950)}.dark .dark\:bg-red-50{background-color:var(--color-red-50)}.dark .dark\:bg-red-100{background-color:var(--color-red-100)}.dark .dark\:bg-red-200{background-color:var(--color-red-200)}.dark .dark\:bg-red-300{background-color:var(--color-red-300)}.dark .dark\:bg-red-400{background-color:var(--color-red-400)}.dark .dark\:bg-red-500{background-color:var(--color-red-500)}.dark .dark\:bg-red-600{background-color:var(--color-red-600)}.dark .dark\:bg-red-600\/50{background-color:#e4001480}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-600\/50{background-color:color-mix(in oklab,var(--color-red-600)50%,transparent)}}.dark .dark\:bg-red-700{background-color:var(--color-red-700)}.dark .dark\:bg-red-800{background-color:var(--color-red-800)}.dark .dark\:bg-red-900{background-color:var(--color-red-900)}.dark .dark\:bg-red-900\/10{background-color:#82181a1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/10{background-color:color-mix(in oklab,var(--color-red-900)10%,transparent)}}.dark .dark\:bg-red-900\/20{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/20{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark .dark\:bg-red-900\/30{background-color:#82181a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-900\/30{background-color:color-mix(in oklab,var(--color-red-900)30%,transparent)}}.dark .dark\:bg-red-950{background-color:var(--color-red-950)}.dark .dark\:bg-red-950\/50{background-color:#46080980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-950\/50{background-color:color-mix(in oklab,var(--color-red-950)50%,transparent)}}.dark .dark\:bg-red-950\/80{background-color:#460809cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-red-950\/80{background-color:color-mix(in oklab,var(--color-red-950)80%,transparent)}}.dark .dark\:bg-slate-50{background-color:var(--color-slate-50)}.dark .dark\:bg-slate-100{background-color:var(--color-slate-100)}.dark .dark\:bg-slate-200{background-color:var(--color-slate-200)}.dark .dark\:bg-slate-300{background-color:var(--color-slate-300)}.dark .dark\:bg-slate-400{background-color:var(--color-slate-400)}.dark .dark\:bg-slate-500{background-color:var(--color-slate-500)}.dark .dark\:bg-slate-600{background-color:var(--color-slate-600)}.dark .dark\:bg-slate-700{background-color:var(--color-slate-700)}.dark .dark\:bg-slate-800{background-color:var(--color-slate-800)}.dark .dark\:bg-slate-900{background-color:var(--color-slate-900)}.dark .dark\:bg-slate-950{background-color:var(--color-slate-950)}.dark .dark\:bg-white{background-color:var(--color-white)}.dark .dark\:bg-yellow-50{background-color:var(--color-yellow-50)}.dark .dark\:bg-yellow-100{background-color:var(--color-yellow-100)}.dark .dark\:bg-yellow-200{background-color:var(--color-yellow-200)}.dark .dark\:bg-yellow-300{background-color:var(--color-yellow-300)}.dark .dark\:bg-yellow-400{background-color:var(--color-yellow-400)}.dark .dark\:bg-yellow-500{background-color:var(--color-yellow-500)}.dark .dark\:bg-yellow-600{background-color:var(--color-yellow-600)}.dark .dark\:bg-yellow-700{background-color:var(--color-yellow-700)}.dark .dark\:bg-yellow-800{background-color:var(--color-yellow-800)}.dark .dark\:bg-yellow-900{background-color:var(--color-yellow-900)}.dark .dark\:bg-yellow-900\/20{background-color:#733e0a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/20{background-color:color-mix(in oklab,var(--color-yellow-900)20%,transparent)}}.dark .dark\:bg-yellow-900\/30{background-color:#733e0a4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:bg-yellow-900\/30{background-color:color-mix(in oklab,var(--color-yellow-900)30%,transparent)}}.dark .dark\:bg-yellow-950{background-color:var(--color-yellow-950)}.dark .dark\:from-gray-50{--tw-gradient-from:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-100{--tw-gradient-from:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-200{--tw-gradient-from:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-300{--tw-gradient-from:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-400{--tw-gradient-from:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-500{--tw-gradient-from:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-600{--tw-gradient-from:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-700{--tw-gradient-from:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-800{--tw-gradient-from:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:from-gray-900{--tw-gradient-from:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-50{--tw-gradient-to:var(--color-gray-50);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-100{--tw-gradient-to:var(--color-gray-100);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-200{--tw-gradient-to:var(--color-gray-200);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-300{--tw-gradient-to:var(--color-gray-300);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-400{--tw-gradient-to:var(--color-gray-400);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-500{--tw-gradient-to:var(--color-gray-500);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-600{--tw-gradient-to:var(--color-gray-600);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-700{--tw-gradient-to:var(--color-gray-700);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-800{--tw-gradient-to:var(--color-gray-800);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:to-gray-900{--tw-gradient-to:var(--color-gray-900);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position),var(--tw-gradient-from)var(--tw-gradient-from-position),var(--tw-gradient-to)var(--tw-gradient-to-position))}.dark .dark\:text-center{text-align:center}.dark .dark\:text-justify{text-align:justify}.dark .dark\:text-left{text-align:left}.dark .dark\:text-right{text-align:right}.dark .dark\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.dark .dark\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.dark .dark\:text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.dark .dark\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.dark .dark\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.dark .dark\:text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.dark .dark\:text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.dark .dark\:leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.dark .dark\:leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.dark .dark\:leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.dark .dark\:font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.dark .dark\:font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.dark .dark\:font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.dark .dark\:tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.dark .dark\:tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.dark .dark\:tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.dark .dark\:whitespace-normal{white-space:normal}.dark .dark\:whitespace-nowrap{white-space:nowrap}.dark .dark\:whitespace-pre-wrap{white-space:pre-wrap}.dark .dark\:text-amber-400{color:var(--color-amber-400)}.dark .dark\:text-black{color:var(--color-black)}.dark .dark\:text-blue-50{color:var(--color-blue-50)}.dark .dark\:text-blue-100{color:var(--color-blue-100)}.dark .dark\:text-blue-200{color:var(--color-blue-200)}.dark .dark\:text-blue-300{color:var(--color-blue-300)}.dark .dark\:text-blue-400{color:var(--color-blue-400)}.dark .dark\:text-blue-500{color:var(--color-blue-500)}.dark .dark\:text-blue-600{color:var(--color-blue-600)}.dark .dark\:text-blue-700{color:var(--color-blue-700)}.dark .dark\:text-blue-800{color:var(--color-blue-800)}.dark .dark\:text-blue-900{color:var(--color-blue-900)}.dark .dark\:text-emerald-400{color:var(--color-emerald-400)}.dark .dark\:text-gray-50{color:var(--color-gray-50)}.dark .dark\:text-gray-100{color:var(--color-gray-100)}.dark .dark\:text-gray-200{color:var(--color-gray-200)}.dark .dark\:text-gray-300{color:var(--color-gray-300)}.dark .dark\:text-gray-400{color:var(--color-gray-400)}.dark .dark\:text-gray-500{color:var(--color-gray-500)}.dark .dark\:text-gray-500\/80{color:#6a7282cc}@supports (color:color-mix(in lab,red,red)){.dark .dark\:text-gray-500\/80{color:color-mix(in oklab,var(--color-gray-500)80%,transparent)}}.dark .dark\:text-gray-600{color:var(--color-gray-600)}.dark .dark\:text-gray-700{color:var(--color-gray-700)}.dark .dark\:text-gray-800{color:var(--color-gray-800)}.dark .dark\:text-gray-900{color:var(--color-gray-900)}.dark .dark\:text-green-50{color:var(--color-green-50)}.dark .dark\:text-green-100{color:var(--color-green-100)}.dark .dark\:text-green-200{color:var(--color-green-200)}.dark .dark\:text-green-300{color:var(--color-green-300)}.dark .dark\:text-green-400{color:var(--color-green-400)}.dark .dark\:text-green-500{color:var(--color-green-500)}.dark .dark\:text-green-600{color:var(--color-green-600)}.dark .dark\:text-green-700{color:var(--color-green-700)}.dark .dark\:text-green-800{color:var(--color-green-800)}.dark .dark\:text-green-900{color:var(--color-green-900)}.dark .dark\:text-indigo-400{color:var(--color-indigo-400)}.dark .dark\:text-orange-50{color:var(--color-orange-50)}.dark .dark\:text-orange-100{color:var(--color-orange-100)}.dark .dark\:text-orange-200{color:var(--color-orange-200)}.dark .dark\:text-orange-300{color:var(--color-orange-300)}.dark .dark\:text-orange-400{color:var(--color-orange-400)}.dark .dark\:text-orange-500{color:var(--color-orange-500)}.dark .dark\:text-orange-600{color:var(--color-orange-600)}.dark .dark\:text-orange-700{color:var(--color-orange-700)}.dark .dark\:text-orange-800{color:var(--color-orange-800)}.dark .dark\:text-orange-900{color:var(--color-orange-900)}.dark .dark\:text-pink-50{color:var(--color-pink-50)}.dark .dark\:text-pink-100{color:var(--color-pink-100)}.dark .dark\:text-pink-200{color:var(--color-pink-200)}.dark .dark\:text-pink-300{color:var(--color-pink-300)}.dark .dark\:text-pink-400{color:var(--color-pink-400)}.dark .dark\:text-pink-500{color:var(--color-pink-500)}.dark .dark\:text-pink-600{color:var(--color-pink-600)}.dark .dark\:text-pink-700{color:var(--color-pink-700)}.dark .dark\:text-pink-800{color:var(--color-pink-800)}.dark .dark\:text-pink-900{color:var(--color-pink-900)}.dark .dark\:text-primary-400{color:var(--color-primary-400)}.dark .dark\:text-primary-500{color:var(--color-primary-500)}.dark .dark\:text-purple-50{color:var(--color-purple-50)}.dark .dark\:text-purple-100{color:var(--color-purple-100)}.dark .dark\:text-purple-200{color:var(--color-purple-200)}.dark .dark\:text-purple-300{color:var(--color-purple-300)}.dark .dark\:text-purple-400{color:var(--color-purple-400)}.dark .dark\:text-purple-500{color:var(--color-purple-500)}.dark .dark\:text-purple-600{color:var(--color-purple-600)}.dark .dark\:text-purple-700{color:var(--color-purple-700)}.dark .dark\:text-purple-800{color:var(--color-purple-800)}.dark .dark\:text-purple-900{color:var(--color-purple-900)}.dark .dark\:text-red-50{color:var(--color-red-50)}.dark .dark\:text-red-100{color:var(--color-red-100)}.dark .dark\:text-red-200{color:var(--color-red-200)}.dark .dark\:text-red-300{color:var(--color-red-300)}.dark .dark\:text-red-400{color:var(--color-red-400)}.dark .dark\:text-red-500{color:var(--color-red-500)}.dark .dark\:text-red-500\/60{color:#fb2c3699}@supports (color:color-mix(in lab,red,red)){.dark .dark\:text-red-500\/60{color:color-mix(in oklab,var(--color-red-500)60%,transparent)}}.dark .dark\:text-red-600{color:var(--color-red-600)}.dark .dark\:text-red-700{color:var(--color-red-700)}.dark .dark\:text-red-800{color:var(--color-red-800)}.dark .dark\:text-red-900{color:var(--color-red-900)}.dark .dark\:text-slate-50{color:var(--color-slate-50)}.dark .dark\:text-slate-100{color:var(--color-slate-100)}.dark .dark\:text-slate-200{color:var(--color-slate-200)}.dark .dark\:text-slate-300{color:var(--color-slate-300)}.dark .dark\:text-slate-400{color:var(--color-slate-400)}.dark .dark\:text-slate-500{color:var(--color-slate-500)}.dark .dark\:text-slate-600{color:var(--color-slate-600)}.dark .dark\:text-slate-700{color:var(--color-slate-700)}.dark .dark\:text-slate-800{color:var(--color-slate-800)}.dark .dark\:text-slate-900{color:var(--color-slate-900)}.dark .dark\:text-white{color:var(--color-white)}.dark .dark\:text-yellow-50{color:var(--color-yellow-50)}.dark .dark\:text-yellow-100{color:var(--color-yellow-100)}.dark .dark\:text-yellow-200{color:var(--color-yellow-200)}.dark .dark\:text-yellow-300{color:var(--color-yellow-300)}.dark .dark\:text-yellow-400{color:var(--color-yellow-400)}.dark .dark\:text-yellow-500{color:var(--color-yellow-500)}.dark .dark\:text-yellow-600{color:var(--color-yellow-600)}.dark .dark\:text-yellow-700{color:var(--color-yellow-700)}.dark .dark\:text-yellow-800{color:var(--color-yellow-800)}.dark .dark\:text-yellow-900{color:var(--color-yellow-900)}.dark .dark\:italic{font-style:italic}.dark .dark\:underline{text-decoration-line:underline}.dark .dark\:placeholder-gray-400::placeholder{color:var(--color-gray-400)}.dark .dark\:placeholder-gray-500::placeholder{color:var(--color-gray-500)}.dark .dark\:opacity-0{opacity:0}.dark .dark\:opacity-25{opacity:.25}.dark .dark\:opacity-50{opacity:.5}.dark .dark\:opacity-75{opacity:.75}.dark .dark\:opacity-100{opacity:1}.dark .dark\:shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-inner{--tw-shadow:inset 0 2px 4px 0 var(--tw-shadow-color,#0000000d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a),0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a),0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-none{--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a),0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark .dark\:ring-blue-50{--tw-ring-color:var(--color-blue-50)}.dark .dark\:ring-blue-100{--tw-ring-color:var(--color-blue-100)}.dark .dark\:ring-blue-200{--tw-ring-color:var(--color-blue-200)}.dark .dark\:ring-blue-300{--tw-ring-color:var(--color-blue-300)}.dark .dark\:ring-blue-400{--tw-ring-color:var(--color-blue-400)}.dark .dark\:ring-blue-500{--tw-ring-color:var(--color-blue-500)}.dark .dark\:ring-blue-600{--tw-ring-color:var(--color-blue-600)}.dark .dark\:ring-blue-700{--tw-ring-color:var(--color-blue-700)}.dark .dark\:ring-blue-800{--tw-ring-color:var(--color-blue-800)}.dark .dark\:ring-blue-900{--tw-ring-color:var(--color-blue-900)}.dark .dark\:ring-gray-50{--tw-ring-color:var(--color-gray-50)}.dark .dark\:ring-gray-100{--tw-ring-color:var(--color-gray-100)}.dark .dark\:ring-gray-200{--tw-ring-color:var(--color-gray-200)}.dark .dark\:ring-gray-300{--tw-ring-color:var(--color-gray-300)}.dark .dark\:ring-gray-400{--tw-ring-color:var(--color-gray-400)}.dark .dark\:ring-gray-500{--tw-ring-color:var(--color-gray-500)}.dark .dark\:ring-gray-600{--tw-ring-color:var(--color-gray-600)}.dark .dark\:ring-gray-700{--tw-ring-color:var(--color-gray-700)}.dark .dark\:ring-gray-800{--tw-ring-color:var(--color-gray-800)}.dark .dark\:ring-gray-900{--tw-ring-color:var(--color-gray-900)}.dark .dark\:ring-white{--tw-ring-color:var(--color-white)}.dark .dark\:ring-offset-gray-800{--tw-ring-offset-color:var(--color-gray-800)}@media(hover:hover){.dark .dark\:group-hover\:bg-gray-700:is(:where(.group):hover *){background-color:var(--color-gray-700)}.dark .dark\:group-hover\:bg-gray-800:is(:where(.group):hover *){background-color:var(--color-gray-800)}.dark .dark\:group-hover\:text-blue-400:is(:where(.group):hover *){color:var(--color-blue-400)}.dark .dark\:group-hover\:text-gray-100:is(:where(.group):hover *){color:var(--color-gray-100)}.dark .dark\:group-hover\:text-white:is(:where(.group):hover *){color:var(--color-white)}}.dark .dark\:selection\:bg-blue-800 ::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:bg-blue-800::selection{background-color:var(--color-blue-800)}.dark .dark\:selection\:text-blue-100 ::selection{color:var(--color-blue-100)}.dark .dark\:selection\:text-blue-100::selection{color:var(--color-blue-100)}.dark .dark\:odd\:bg-gray-800:nth-child(odd){background-color:var(--color-gray-800)}.dark .dark\:odd\:bg-gray-900:nth-child(odd){background-color:var(--color-gray-900)}.dark .dark\:even\:bg-gray-800:nth-child(2n){background-color:var(--color-gray-800)}.dark .dark\:even\:bg-gray-900:nth-child(2n){background-color:var(--color-gray-900)}@media(hover:hover){.dark .dark\:hover\:border-blue-600:hover{border-color:var(--color-blue-600)}.dark .dark\:hover\:border-blue-700:hover{border-color:var(--color-blue-700)}.dark .dark\:hover\:border-blue-800:hover{border-color:var(--color-blue-800)}.dark .dark\:hover\:border-gray-50:hover{border-color:var(--color-gray-50)}.dark .dark\:hover\:border-gray-100:hover{border-color:var(--color-gray-100)}.dark .dark\:hover\:border-gray-200:hover{border-color:var(--color-gray-200)}.dark .dark\:hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.dark .dark\:hover\:border-gray-400:hover{border-color:var(--color-gray-400)}.dark .dark\:hover\:border-gray-500:hover{border-color:var(--color-gray-500)}.dark .dark\:hover\:border-gray-600:hover{border-color:var(--color-gray-600)}.dark .dark\:hover\:border-gray-700:hover{border-color:var(--color-gray-700)}.dark .dark\:hover\:border-gray-800:hover{border-color:var(--color-gray-800)}.dark .dark\:hover\:border-gray-900:hover{border-color:var(--color-gray-900)}.dark .dark\:hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.dark .dark\:hover\:bg-blue-100:hover{background-color:var(--color-blue-100)}.dark .dark\:hover\:bg-blue-200:hover{background-color:var(--color-blue-200)}.dark .dark\:hover\:bg-blue-300:hover{background-color:var(--color-blue-300)}.dark .dark\:hover\:bg-blue-400:hover{background-color:var(--color-blue-400)}.dark .dark\:hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.dark .dark\:hover\:bg-blue-600:hover{background-color:var(--color-blue-600)}.dark .dark\:hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.dark .dark\:hover\:bg-blue-800:hover{background-color:var(--color-blue-800)}.dark .dark\:hover\:bg-blue-900:hover{background-color:var(--color-blue-900)}.dark .dark\:hover\:bg-blue-900\/20:hover{background-color:#1c398e33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/20:hover{background-color:color-mix(in oklab,var(--color-blue-900)20%,transparent)}}.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:#1c398e4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-blue-900\/30:hover{background-color:color-mix(in oklab,var(--color-blue-900)30%,transparent)}}.dark .dark\:hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.dark .dark\:hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.dark .dark\:hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.dark .dark\:hover\:bg-gray-300:hover{background-color:var(--color-gray-300)}.dark .dark\:hover\:bg-gray-400:hover{background-color:var(--color-gray-400)}.dark .dark\:hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.dark .dark\:hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.dark .dark\:hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-700\/50:hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.dark .dark\:hover\:bg-gray-800:hover{background-color:var(--color-gray-800)}.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:#1e293980}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-gray-800\/50:hover{background-color:color-mix(in oklab,var(--color-gray-800)50%,transparent)}}.dark .dark\:hover\:bg-gray-900:hover{background-color:var(--color-gray-900)}.dark .dark\:hover\:bg-indigo-50:hover{background-color:var(--color-indigo-50)}.dark .dark\:hover\:bg-indigo-100:hover{background-color:var(--color-indigo-100)}.dark .dark\:hover\:bg-indigo-200:hover{background-color:var(--color-indigo-200)}.dark .dark\:hover\:bg-indigo-300:hover{background-color:var(--color-indigo-300)}.dark .dark\:hover\:bg-indigo-400:hover{background-color:var(--color-indigo-400)}.dark .dark\:hover\:bg-indigo-500:hover{background-color:var(--color-indigo-500)}.dark .dark\:hover\:bg-indigo-600:hover{background-color:var(--color-indigo-600)}.dark .dark\:hover\:bg-indigo-700:hover{background-color:var(--color-indigo-700)}.dark .dark\:hover\:bg-indigo-800:hover{background-color:var(--color-indigo-800)}.dark .dark\:hover\:bg-indigo-900:hover{background-color:var(--color-indigo-900)}.dark .dark\:hover\:bg-orange-900\/20:hover{background-color:#7e2a0c33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/20:hover{background-color:color-mix(in oklab,var(--color-orange-900)20%,transparent)}}.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:#7e2a0c4d}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-orange-900\/30:hover{background-color:color-mix(in oklab,var(--color-orange-900)30%,transparent)}}.dark .dark\:hover\:bg-red-600:hover{background-color:var(--color-red-600)}.dark .dark\:hover\:bg-red-900\/20:hover{background-color:#82181a33}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-red-900\/20:hover{background-color:color-mix(in oklab,var(--color-red-900)20%,transparent)}}.dark .dark\:hover\:bg-slate-50:hover{background-color:var(--color-slate-50)}.dark .dark\:hover\:bg-slate-100:hover{background-color:var(--color-slate-100)}.dark .dark\:hover\:bg-slate-200:hover{background-color:var(--color-slate-200)}.dark .dark\:hover\:bg-slate-300:hover{background-color:var(--color-slate-300)}.dark .dark\:hover\:bg-slate-400:hover{background-color:var(--color-slate-400)}.dark .dark\:hover\:bg-slate-500:hover{background-color:var(--color-slate-500)}.dark .dark\:hover\:bg-slate-600:hover{background-color:var(--color-slate-600)}.dark .dark\:hover\:bg-slate-700:hover{background-color:var(--color-slate-700)}.dark .dark\:hover\:bg-slate-800:hover{background-color:var(--color-slate-800)}.dark .dark\:hover\:bg-slate-900:hover{background-color:var(--color-slate-900)}.dark .dark\:hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.dark .dark\:hover\:bg-white\/10:hover{background-color:color-mix(in oklab,var(--color-white)10%,transparent)}}.dark .dark\:hover\:text-black:hover{color:var(--color-black)}.dark .dark\:hover\:text-blue-50:hover{color:var(--color-blue-50)}.dark .dark\:hover\:text-blue-100:hover{color:var(--color-blue-100)}.dark .dark\:hover\:text-blue-200:hover{color:var(--color-blue-200)}.dark .dark\:hover\:text-blue-300:hover{color:var(--color-blue-300)}.dark .dark\:hover\:text-blue-400:hover{color:var(--color-blue-400)}.dark .dark\:hover\:text-blue-500:hover{color:var(--color-blue-500)}.dark .dark\:hover\:text-blue-600:hover{color:var(--color-blue-600)}.dark .dark\:hover\:text-blue-700:hover{color:var(--color-blue-700)}.dark .dark\:hover\:text-blue-800:hover{color:var(--color-blue-800)}.dark .dark\:hover\:text-blue-900:hover{color:var(--color-blue-900)}.dark .dark\:hover\:text-gray-50:hover{color:var(--color-gray-50)}.dark .dark\:hover\:text-gray-100:hover{color:var(--color-gray-100)}.dark .dark\:hover\:text-gray-200:hover{color:var(--color-gray-200)}.dark .dark\:hover\:text-gray-300:hover{color:var(--color-gray-300)}.dark .dark\:hover\:text-gray-400:hover{color:var(--color-gray-400)}.dark .dark\:hover\:text-gray-500:hover{color:var(--color-gray-500)}.dark .dark\:hover\:text-gray-600:hover{color:var(--color-gray-600)}.dark .dark\:hover\:text-gray-700:hover{color:var(--color-gray-700)}.dark .dark\:hover\:text-gray-800:hover{color:var(--color-gray-800)}.dark .dark\:hover\:text-gray-900:hover{color:var(--color-gray-900)}.dark .dark\:hover\:text-green-50:hover{color:var(--color-green-50)}.dark .dark\:hover\:text-green-100:hover{color:var(--color-green-100)}.dark .dark\:hover\:text-green-200:hover{color:var(--color-green-200)}.dark .dark\:hover\:text-green-300:hover{color:var(--color-green-300)}.dark .dark\:hover\:text-green-400:hover{color:var(--color-green-400)}.dark .dark\:hover\:text-green-500:hover{color:var(--color-green-500)}.dark .dark\:hover\:text-green-600:hover{color:var(--color-green-600)}.dark .dark\:hover\:text-green-700:hover{color:var(--color-green-700)}.dark .dark\:hover\:text-green-800:hover{color:var(--color-green-800)}.dark .dark\:hover\:text-green-900:hover{color:var(--color-green-900)}.dark .dark\:hover\:text-red-50:hover{color:var(--color-red-50)}.dark .dark\:hover\:text-red-100:hover{color:var(--color-red-100)}.dark .dark\:hover\:text-red-200:hover{color:var(--color-red-200)}.dark .dark\:hover\:text-red-300:hover{color:var(--color-red-300)}.dark .dark\:hover\:text-red-400:hover{color:var(--color-red-400)}.dark .dark\:hover\:text-red-500:hover{color:var(--color-red-500)}.dark .dark\:hover\:text-red-600:hover{color:var(--color-red-600)}.dark .dark\:hover\:text-red-700:hover{color:var(--color-red-700)}.dark .dark\:hover\:text-red-800:hover{color:var(--color-red-800)}.dark .dark\:hover\:text-red-900:hover{color:var(--color-red-900)}.dark .dark\:hover\:text-white:hover{color:var(--color-white)}}.dark .dark\:focus\:border-gray-500:focus{border-color:var(--color-gray-500)}.dark .dark\:focus\:border-gray-600:focus{border-color:var(--color-gray-600)}.dark .dark\:focus\:border-gray-700:focus{border-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-700:focus{background-color:var(--color-gray-700)}.dark .dark\:focus\:bg-gray-800:focus{background-color:var(--color-gray-800)}.dark .dark\:focus\:bg-gray-900:focus{background-color:var(--color-gray-900)}.dark .dark\:focus\:text-gray-100:focus{color:var(--color-gray-100)}.dark .dark\:focus\:text-gray-200:focus{color:var(--color-gray-200)}.dark .dark\:focus\:text-gray-300:focus{color:var(--color-gray-300)}.dark .dark\:focus\:text-white:focus{color:var(--color-white)}.dark .dark\:focus\:ring-amber-600:focus{--tw-ring-color:var(--color-amber-600)}.dark .dark\:focus\:ring-blue-400:focus{--tw-ring-color:var(--color-blue-400)}.dark .dark\:focus\:ring-blue-500:focus{--tw-ring-color:var(--color-blue-500)}.dark .dark\:focus\:ring-blue-600:focus{--tw-ring-color:var(--color-blue-600)}.dark .dark\:focus\:ring-blue-700:focus{--tw-ring-color:var(--color-blue-700)}.dark .dark\:focus\:ring-gray-500:focus{--tw-ring-color:var(--color-gray-500)}.dark .dark\:focus\:ring-gray-600:focus{--tw-ring-color:var(--color-gray-600)}.dark .dark\:focus\:ring-gray-700:focus{--tw-ring-color:var(--color-gray-700)}.dark .dark\:focus\:ring-indigo-400:focus{--tw-ring-color:var(--color-indigo-400)}.dark .dark\:focus\:ring-white:focus{--tw-ring-color:var(--color-white)}.dark .dark\:focus\:ring-offset-gray-800:focus{--tw-ring-offset-color:var(--color-gray-800)}.dark .dark\:focus\:ring-offset-gray-900:focus{--tw-ring-offset-color:var(--color-gray-900)}.dark .dark\:active\:bg-gray-200:active{background-color:var(--color-gray-200)}.dark .dark\:active\:bg-gray-600:active{background-color:var(--color-gray-600)}.dark .dark\:active\:bg-gray-700:active{background-color:var(--color-gray-700)}.dark .dark\:active\:bg-gray-800:active{background-color:var(--color-gray-800)}.dark .dark\:disabled\:bg-gray-700:disabled{background-color:var(--color-gray-700)}.dark .dark\:disabled\:bg-gray-800:disabled{background-color:var(--color-gray-800)}.dark .dark\:disabled\:text-gray-500:disabled{color:var(--color-gray-500)}.dark .dark\:disabled\:text-gray-600:disabled{color:var(--color-gray-600)}.dark .dark\:disabled\:opacity-50:disabled{opacity:.5}.\[\&_img\]\:hidden img{display:none}.\[\&_mark\]\:rounded mark{border-radius:.25rem}.\[\&_mark\]\:bg-yellow-200 mark{background-color:var(--color-yellow-200)}.\[\&_mark\]\:px-0\.5 mark{padding-inline:calc(var(--spacing)*.5)}.dark :is(.\[\&_mark\]\:dark\:bg-yellow-500\/40 mark){background-color:#edb20066}@supports (color:color-mix(in lab,red,red)){.dark :is(.\[\&_mark\]\:dark\:bg-yellow-500\/40 mark){background-color:color-mix(in oklab,var(--color-yellow-500)40%,transparent)}}.\[\&_table\]\:hidden table{display:none}.\[\&_table\]\:w-full table{width:100%}.\[\&_table\]\:border-collapse table{border-collapse:collapse}.\[\&_table\]\:border table{border-style:var(--tw-border-style);border-width:1px}.\[\&_table\]\:border-gray-300 table{border-color:var(--color-gray-300)}.dark .dark\:\[\&_table\]\:border-gray-600 table{border-color:var(--color-gray-600)}.\[\&_td\]\:border td{border-style:var(--tw-border-style);border-width:1px}.\[\&_td\]\:border-gray-300 td{border-color:var(--color-gray-300)}.\[\&_td\]\:px-4 td{padding-inline:calc(var(--spacing)*4)}.\[\&_td\]\:py-2 td{padding-block:calc(var(--spacing)*2)}.dark .dark\:\[\&_td\]\:border-gray-600 td{border-color:var(--color-gray-600)}.\[\&_textarea\]\:\!border-red-500 textarea{border-color:var(--color-red-500)!important}.\[\&_textarea\]\:focus\:\!ring-red-500 textarea:focus{--tw-ring-color:var(--color-red-500)!important}.dark :is(.\[\&_textarea\]\:dark\:\!border-red-500 textarea){border-color:var(--color-red-500)!important}.\[\&_th\]\:border th{border-style:var(--tw-border-style);border-width:1px}.\[\&_th\]\:border-gray-300 th{border-color:var(--color-gray-300)}.\[\&_th\]\:bg-gray-100 th{background-color:var(--color-gray-100)}.\[\&_th\]\:px-4 th{padding-inline:calc(var(--spacing)*4)}.\[\&_th\]\:py-2 th{padding-block:calc(var(--spacing)*2)}.\[\&_th\]\:text-left th{text-align:left}.dark .dark\:\[\&_th\]\:border-gray-600 th{border-color:var(--color-gray-600)}.dark .dark\:\[\&_th\]\:bg-gray-800 th{background-color:var(--color-gray-800)}.\[\&_tr\:hover\]\:bg-gray-50 tr:hover{background-color:var(--color-gray-50)}.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:#36415380}@supports (color:color-mix(in lab,red,red)){.dark .dark\:\[\&_tr\:hover\]\:bg-gray-700\/50 tr:hover{background-color:color-mix(in oklab,var(--color-gray-700)50%,transparent)}}.\[\&\:\:-webkit-inner-spin-button\]\:appearance-none::-webkit-inner-spin-button{appearance:none}.\[\&\:\:-webkit-outer-spin-button\]\:appearance-none::-webkit-outer-spin-button{appearance:none}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.scrollbar-hide{-ms-overflow-style:none;scrollbar-width:none}.scrollbar-hide::-webkit-scrollbar{display:none}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}}@property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"<color>";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"<length-percentage>";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"<length-percentage>";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"<length-percentage>";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}@keyframes bounce{0%,to{animation-timing-function:cubic-bezier(.8,0,1,1);transform:translateY(-25%)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:none}}.yarl__fullsize{height:100%;width:100%}.yarl__relative{position:relative}.yarl__portal{inset:0;opacity:0;overflow:hidden;position:fixed;transition:opacity var(--yarl__fade_animation_duration,.25s) var(--yarl__fade_animation_timing_function,ease);z-index:var(--yarl__portal_zindex,9999)}.yarl__portal_open{opacity:1}.yarl__container{background-color:var(--yarl__container_background_color,var(--yarl__color_backdrop,#000));inset:0;outline:none;overflow:hidden;overscroll-behavior:var(--yarl__controller_overscroll_behavior,contain);position:absolute;touch-action:var(--yarl__controller_touch_action,none);-webkit-user-select:none;-moz-user-select:none;user-select:none}.yarl__carousel{align-content:center;align-items:stretch;display:flex;flex:0 0 auto;height:100%;justify-content:center;opacity:var(--yarl__pull_opacity,1);transform:translate(var(--yarl__swipe_offset,0),var(--yarl__pull_offset,0));width:calc(100% + (var(--yarl__carousel_slides_count) - 1)*(100% + var(--yarl__carousel_spacing_px, 0)*1px + var(--yarl__carousel_spacing_percent, 0)*1%))}.yarl__carousel_with_slides{-moz-column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%);column-gap:calc(var(--yarl__carousel_spacing_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_spacing_percent, 0)*1%)}.yarl__flex_center{align-content:center;align-items:center;display:flex;justify-content:center}.yarl__slide{flex:1;overflow:hidden;padding:calc(var(--yarl__carousel_padding_px, 0)*1px + 100/(var(--yarl__carousel_slides_count)*100 + (var(--yarl__carousel_slides_count) - 1)*var(--yarl__carousel_spacing_percent, 0))*var(--yarl__carousel_padding_percent, 0)*1%);position:relative}[dir=rtl] .yarl__slide{--yarl__direction:-1}.yarl__slide_image{max-height:100%;max-width:100%;-o-object-fit:contain;object-fit:contain;touch-action:var(--yarl__controller_touch_action,none);-moz-user-select:none;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}.yarl__slide_image_cover{height:100%;-o-object-fit:cover;object-fit:cover;width:100%}.yarl__slide_image_loading{opacity:0}@media screen and (min-width:800px){.yarl__slide_wrapper:not(.yarl__slide_wrapper_interactive) .yarl__slide_image{-webkit-backface-visibility:hidden;-webkit-transform:translateZ(0);-webkit-transform-style:preserve-3d}}.yarl__slide_placeholder{left:50%;line-height:0;position:absolute;top:50%;transform:translate(-50%) translateY(-50%)}.yarl__slide_loading{animation:yarl__delayed_fadein 1s linear;color:var(--yarl__slide_icon_loading_color,var(--yarl__color_button,hsla(0,0%,100%,.8)))}.yarl__slide_loading line{animation:yarl__stroke_opacity 1s linear infinite}.yarl__slide_loading line:first-of-type{animation-delay:-1.875s}.yarl__slide_loading line:nth-of-type(2){animation-delay:-1.75s}.yarl__slide_loading line:nth-of-type(3){animation-delay:-1.625s}.yarl__slide_loading line:nth-of-type(4){animation-delay:-1.5s}.yarl__slide_loading line:nth-of-type(5){animation-delay:-1.375s}.yarl__slide_loading line:nth-of-type(6){animation-delay:-1.25s}.yarl__slide_loading line:nth-of-type(7){animation-delay:-1.125s}.yarl__slide_loading line:nth-of-type(8){animation-delay:-1s}.yarl__slide_error{color:var(--yarl__slide_icon_error_color,red);height:var(--yarl__slide_icon_error_size,48px);width:var(--yarl__slide_icon_error_size,48px)}@media(prefers-reduced-motion){.yarl__portal,.yarl__slide{transition:unset}.yarl__slide_loading,.yarl__slide_loading line{animation:unset}}.yarl__toolbar{display:flex;justify-content:flex-end;inset:0 0 auto auto;padding:var(--yarl__toolbar_padding,8px);position:absolute}[dir=rtl] .yarl__toolbar{inset:0 auto auto 0}.yarl__icon{height:var(--yarl__icon_size,32px);width:var(--yarl__icon_size,32px)}.yarl__button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--yarl__button_background_color,transparent);border:var(--yarl__button_border,0);color:var(--yarl__color_button,hsla(0,0%,100%,.8));cursor:pointer;filter:var(--yarl__button_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));line-height:0;margin:var(--yarl__button_margin,0);outline:none;padding:var(--yarl__button_padding,8px);-webkit-tap-highlight-color:transparent}.yarl__button:focus{color:var(--yarl__color_button_active,#fff)}.yarl__button:focus:not(:focus-visible){color:var(--yarl__color_button,hsla(0,0%,100%,.8))}.yarl__button:focus-visible{color:var(--yarl__color_button_active,#fff)}@media(hover:hover){.yarl__button:focus-visible:hover,.yarl__button:focus:hover,.yarl__button:hover{color:var(--yarl__color_button_active,#fff)}}.yarl__button:disabled{color:var(--yarl__color_button_disabled,hsla(0,0%,100%,.4));cursor:default}.yarl__navigation_next,.yarl__navigation_prev{padding:var(--yarl__navigation_button_padding,24px 16px);position:absolute;top:50%;transform:translateY(-50%)}.yarl__navigation_prev{left:0}[dir=rtl] .yarl__navigation_prev{left:unset;right:0;transform:translateY(-50%) rotate(180deg)}.yarl__navigation_next{right:0}[dir=rtl] .yarl__navigation_next{left:0;right:unset;transform:translateY(-50%) rotate(180deg)}.yarl__no_scroll{height:100%;overflow:hidden;overscroll-behavior:none}@keyframes yarl__delayed_fadein{0%{opacity:0}80%{opacity:0}to{opacity:1}}@keyframes yarl__stroke_opacity{0%{stroke-opacity:1}to{stroke-opacity:.125}}.yarl__counter{bottom:var(--yarl__counter_bottom,unset);color:var(--yarl__counter_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__counter_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));left:var(--yarl__counter_left,0);line-height:var(--yarl__counter_line_height,var(--yarl__icon_size,32px));margin:var(--yarl__counter_margin,var(--yarl__toolbar_padding,8px));padding:var(--yarl__counter_padding,var(--yarl__button_padding,8px));position:var(--yarl__counter_position,absolute);right:var(--yarl__counter_right,unset);top:var(--yarl__counter_top,0);-webkit-user-select:var(--yarl__counter_user_select,none);-moz-user-select:var(--yarl__counter_user_select,none);user-select:var(--yarl__counter_user_select,none)}.yarl__thumbnails{display:flex;height:100%}.yarl__thumbnails_bottom,.yarl__thumbnails_end .yarl__thumbnails_track,.yarl__thumbnails_start .yarl__thumbnails_track,.yarl__thumbnails_top{flex-direction:column}.yarl__thumbnails_wrapper{flex:1;position:relative}.yarl__thumbnails_container{background-color:var(--yarl__thumbnails_container_background_color,var(--yarl__color_backdrop,#000));flex:0 0 auto;overflow:hidden;padding:var(--yarl__thumbnails_container_padding,16px);position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-touch-callout:none}.yarl__thumbnails_vignette{pointer-events:none;position:absolute;--yarl__thumbnails_vignette_size:12%}@media(min-width:1200px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:8%}}@media(min-width:2000px){.yarl__thumbnails_vignette{--yarl__thumbnails_vignette_size:5%}}.yarl__thumbnails_bottom .yarl__thumbnails_vignette,.yarl__thumbnails_top .yarl__thumbnails_vignette{background:linear-gradient(to right,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);height:100%;left:0;right:0}.yarl__thumbnails_end .yarl__thumbnails_vignette,.yarl__thumbnails_start .yarl__thumbnails_vignette{background:linear-gradient(to bottom,var(--yarl__color_backdrop,#000) 0,transparent var(--yarl__thumbnails_vignette_size,12%) calc(100% - var(--yarl__thumbnails_vignette_size, 12%)),var(--yarl__color_backdrop,#000) 100%);bottom:0;top:0;width:100%}.yarl__thumbnails_track{gap:var(--yarl__thumbnails_thumbnail_gap,16px);outline:none}.yarl__thumbnails_thumbnail{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:var(--yarl__thumbnails_thumbnail_background,#000);border-color:var(--yarl__thumbnails_thumbnail_border_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));border-radius:var(--yarl__thumbnails_thumbnail_border_radius,4px);border-style:var(--yarl__thumbnails_thumbnail_border_style,solid);border-width:var(--yarl__thumbnails_thumbnail_border,1px);cursor:pointer;flex:0 0 auto;-webkit-tap-highlight-color:transparent;box-sizing:content-box;height:var(--yarl__thumbnails_thumbnail_height,80px);outline:none;overflow:hidden;padding:var(--yarl__thumbnails_thumbnail_padding,4px);position:relative;width:var(--yarl__thumbnails_thumbnail_width,120px)}.yarl__thumbnails_thumbnail_active{border-color:var(--yarl__thumbnails_thumbnail_active_border_color,var(--yarl__color_button_active,#fff))}.yarl__thumbnails_thumbnail_fadein{animation:yarl__thumbnails_thumbnail_fadein var(--yarl__thumbnails_thumbnail_fadein_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadein_delay,0s) forwards;opacity:0}.yarl__thumbnails_thumbnail_fadeout{animation:yarl__thumbnails_thumbnail_fadeout var(--yarl__thumbnails_thumbnail_fadeout_duration,.5s) ease-in-out var(--yarl__thumbnails_thumbnail_fadeout_delay,0s) forwards;cursor:unset}.yarl__thumbnails_thumbnail_placeholder{cursor:unset;visibility:hidden}.yarl__thumbnails_thumbnail:focus{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail:focus:not(:focus-visible){box-shadow:unset}.yarl__thumbnails_thumbnail:focus-visible{box-shadow:var(--yarl__thumbnails_thumbnail_focus_box_shadow,#000 0 0 0 2px,var(--yarl__color_button,hsla(0,0%,100%,.8)) 0 0 0 4px)}.yarl__thumbnails_thumbnail_icon{color:var(--yarl__thumbnails_thumbnail_icon_color,var(--yarl__color_button,hsla(0,0%,100%,.8)));filter:var(--yarl__thumbnails_thumbnail_icon_filter,drop-shadow(2px 2px 2px rgba(0,0,0,.8)));height:var(--yarl__thumbnails_thumbnail_icon_size,32px);left:50%;position:absolute;top:50%;transform:translate(-50%) translateY(-50%);width:var(--yarl__thumbnails_thumbnail_icon_size,32px)}@keyframes yarl__thumbnails_thumbnail_fadein{0%{opacity:0}to{opacity:1}}@keyframes yarl__thumbnails_thumbnail_fadeout{0%{opacity:1}to{opacity:0}} diff --git a/templates/_bundled/sirsoft-basic/dist/js/components.iife.js b/templates/_bundled/sirsoft-basic/dist/js/components.iife.js index 8215c084..f4d04476 100644 --- a/templates/_bundled/sirsoft-basic/dist/js/components.iife.js +++ b/templates/_bundled/sirsoft-basic/dist/js/components.iife.js @@ -1,4 +1,4 @@ -var SirsoftBasic=(function(J,d,k,Jt){"use strict";function Dc(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const r in e)if(r!=="default"){const n=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,n.get?n:{enumerable:!0,get:()=>e[r]})}}return t.default=e,Object.freeze(t)}const N=Dc(k),Lc=()=>window.G7Core,Q=k.forwardRef(({children:e,variant:t="primary",size:r="md",className:n="",...o},a)=>{const s="inline-flex items-center justify-center",l=Lc()?.style?.mergeClasses?.(s,n)??`${s} ${n}`;return d.jsx("button",{ref:a,className:l,...o,children:e})});Q.displayName="Button";const W=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("div",{ref:n,className:t,...r,children:e})),R=({children:e,className:t="",...r})=>d.jsx("span",{className:t,...r,children:e}),fn=({children:e,className:t="",...r})=>d.jsx("svg",{className:t,...r,children:e}),xt=k.forwardRef(({label:e,error:t,className:r="",onChange:n,onKeyPress:o,value:a,defaultValue:s,type:l="text",checked:f,defaultChecked:h,...c},i)=>{const u=l==="radio"||l==="checkbox",g=k.useRef(!1),p=k.useRef(null);k.useImperativeHandle(i,()=>p.current);const[m,w]=k.useState(a??s??"");k.useEffect(()=>{!u&&!g.current&&a!==void 0&&w(a)},[a,u]);const C=k.useCallback(()=>{g.current=!0},[]),O=k.useCallback(y=>{if(g.current=!1,n){const T=y.target.value;w(T);const A=new Event("change",{bubbles:!0});Object.defineProperty(A,"target",{writable:!1,value:{value:T}}),Object.defineProperty(A,"currentTarget",{writable:!1,value:{value:T}}),n(A)}},[n]),v=k.useCallback(y=>{const T=y.target.value;w(T),!g.current&&n&&n(y)},[n]),E=k.useCallback(y=>{g.current||o&&o(y)},[o]),{loadingactions:I,formdata:_,...M}=c;if(u)return d.jsx("input",{ref:p,type:l,className:r,onChange:n,checked:f,defaultChecked:h,value:a,...M});const x=a!==void 0?{value:m}:{defaultValue:s};return d.jsx("input",{ref:p,type:l,className:r,onChange:v,onKeyPress:E,onCompositionStart:C,onCompositionEnd:O,...x,...M})});xt.displayName="Input";const ja=k.forwardRef(({accept:e,maxSize:t,onChange:r,onError:n,buttonText:o="Browse",placeholder:a="No file selected",className:s="",disabled:l,...f},h)=>{const c=k.useRef(null),i=h||c,[u,g]=k.useState(""),p={},m={};Object.entries(f).forEach(([v,E])=>{v.startsWith("data-editor-")||v==="id"||v==="onMouseMove"||v==="onMouseLeave"?p[v]=E:m[v]=E});const w=k.useCallback(()=>{!l&&i.current&&i.current.click()},[l,i]),C=k.useCallback(v=>{const E=v.target.files?.[0]||null;if(!E){g(""),r?.(null);return}if(t&&E.size>t*1024*1024){const I=`File size exceeds ${t}MB limit`;n?.(I),i.current&&(i.current.value=""),g("");return}g(E.name),r?.(E)},[t,r,n,i]),O=k.useCallback(v=>{v.stopPropagation(),i.current&&(i.current.value=""),g(""),r?.(null)},[r,i]);return d.jsxs(W,{className:`flex items-center gap-2 ${s}`,...p,children:[d.jsx(xt,{ref:i,type:"file",accept:e,onChange:C,disabled:l,className:"hidden",...m}),d.jsx(Q,{type:"button",onClick:w,disabled:l,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:o}),d.jsx(R,{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:u||a}),u&&!l&&d.jsx(Q,{type:"button",onClick:O,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:d.jsx(fn,{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});ja.displayName="FileInput";var Je=(e=>(e.User="fa-user",e.Users="fa-users",e.UserCircle="fa-user-circle",e.UserPlus="fa-user-plus",e.UserMinus="fa-user-minus",e.UserShield="fa-user-shield",e.Search="fa-search",e.Edit="fa-edit",e.Pencil="fa-pencil-alt",e.Trash="fa-trash",e.Save="fa-save",e.Plus="fa-plus",e.Minus="fa-minus",e.Check="fa-check",e.Times="fa-times",e.Download="fa-download",e.Upload="fa-upload",e.Copy="fa-copy",e.ExternalLink="fa-external-link-alt",e.Home="fa-home",e.ChevronLeft="fa-chevron-left",e.ChevronRight="fa-chevron-right",e.ChevronUp="fa-chevron-up",e.ChevronDown="fa-chevron-down",e.AngleLeft="fa-angle-left",e.AngleRight="fa-angle-right",e.AngleUp="fa-angle-up",e.AngleDown="fa-angle-down",e.ArrowUp="fa-arrow-up",e.ArrowDown="fa-arrow-down",e.ArrowLeft="fa-arrow-left",e.ArrowRight="fa-arrow-right",e.Bars="fa-bars",e.EllipsisVertical="fa-ellipsis-v",e.EllipsisHorizontal="fa-ellipsis-h",e.CheckCircle="fa-check-circle",e.TimesCircle="fa-times-circle",e.ExclamationCircle="fa-exclamation-circle",e.ExclamationTriangle="fa-exclamation-triangle",e.InfoCircle="fa-info-circle",e.QuestionCircle="fa-question-circle",e.XCircle="fa-times-circle",e.ShoppingCart="fa-shopping-cart",e.ShoppingBag="fa-shopping-bag",e.CreditCard="fa-credit-card",e.Tag="fa-tag",e.Tags="fa-tags",e.File="fa-file",e.FileAlt="fa-file-alt",e.Folder="fa-folder",e.FolderOpen="fa-folder-open",e.Envelope="fa-envelope",e.Phone="fa-phone",e.Comment="fa-comment",e.Comments="fa-comments",e.Cog="fa-cog",e.Cogs="fa-cogs",e.Settings="fa-cog",e.Wrench="fa-wrench",e.Sliders="fa-sliders-h",e.Image="fa-image",e.Images="fa-images",e.Video="fa-video",e.Camera="fa-camera",e.Heart="fa-heart",e.Star="fa-star",e.ThumbsUp="fa-thumbs-up",e.ThumbsDown="fa-thumbs-down",e.Share="fa-share",e.Clock="fa-clock",e.Calendar="fa-calendar",e.CalendarAlt="fa-calendar-alt",e.Lock="fa-lock",e.Unlock="fa-unlock",e.Eye="fa-eye",e.EyeSlash="fa-eye-slash",e.Bell="fa-bell",e.Flag="fa-flag",e.Map="fa-map",e.MapMarker="fa-map-marker-alt",e.Globe="fa-globe",e.Link="fa-link",e.Unlink="fa-unlink",e.Sun="fa-sun",e.Moon="fa-moon",e.ChartBar="fa-chart-bar",e.ChartLine="fa-chart-line",e.ChartPie="fa-chart-pie",e.ChartArea="fa-chart-area",e.Spinner="fa-spinner",e.CircleNotch="fa-circle-notch",e.CheckBadge="fa-badge-check",e.BellSlash="fa-bell-slash",e.BookOpen="fa-book-open",e.Building="fa-building",e.FileCircleCheck="fa-file-circle-check",e.WandMagicSparkles="fa-wand-magic-sparkles",e.UserXmark="fa-user-xmark",e.Cube="fa-cube",e.Cubes="fa-cubes",e.Plug="fa-plug",e.PuzzlePiece="fa-puzzle-piece",e.Fire="fa-fire",e.CommentDots="fa-comment-dots",e.DollarSign="fa-dollar-sign",e.Bars2="fa-bars",e.X="fa-times",e.List="fa-list",e.ThLarge="fa-th-large",e.Inbox="fa-inbox",e.Box="fa-box",e.PaperPlane="fa-paper-plane",e.FileText="fa-file-alt",e.Paperclip="fa-paperclip",e.Truck="fa-truck",e.Sync="fa-sync",e.RotateLeft="fa-rotate-left",e.MapPin="fa-map-marker-alt",e.HelpCircle="fa-question-circle",e))(Je||{});const Pc={user:"fa-user",users:"fa-users","user-circle":"fa-user-circle","user-plus":"fa-user-plus","user-minus":"fa-user-minus","user-shield":"fa-user-shield",search:"fa-search",edit:"fa-edit",pencil:"fa-pencil-alt",trash:"fa-trash",save:"fa-save",plus:"fa-plus",minus:"fa-minus",check:"fa-check",times:"fa-times",download:"fa-download",upload:"fa-upload",copy:"fa-copy","external-link":"fa-external-link-alt",home:"fa-home","chevron-left":"fa-chevron-left","chevron-right":"fa-chevron-right","chevron-up":"fa-chevron-up","chevron-down":"fa-chevron-down","angle-left":"fa-angle-left","angle-right":"fa-angle-right","angle-up":"fa-angle-up","angle-down":"fa-angle-down","arrow-up":"fa-arrow-up","arrow-down":"fa-arrow-down","arrow-left":"fa-arrow-left","arrow-right":"fa-arrow-right",bars:"fa-bars","ellipsis-vertical":"fa-ellipsis-v","ellipsis-v":"fa-ellipsis-v","ellipsis-horizontal":"fa-ellipsis-h","ellipsis-h":"fa-ellipsis-h","check-circle":"fa-check-circle","times-circle":"fa-times-circle","x-circle":"fa-times-circle","exclamation-circle":"fa-exclamation-circle","exclamation-triangle":"fa-exclamation-triangle","info-circle":"fa-info-circle","question-circle":"fa-question-circle","shopping-cart":"fa-shopping-cart","shopping-bag":"fa-shopping-bag","credit-card":"fa-credit-card",tag:"fa-tag",tags:"fa-tags",file:"fa-file","file-alt":"fa-file-alt",folder:"fa-folder","folder-open":"fa-folder-open",envelope:"fa-envelope",phone:"fa-phone",comment:"fa-comment",comments:"fa-comments",cog:"fa-cog",cogs:"fa-cogs",settings:"fa-cog",wrench:"fa-wrench",sliders:"fa-sliders-h",image:"fa-image",images:"fa-images",video:"fa-video",camera:"fa-camera",heart:"fa-heart",star:"fa-star","thumbs-up":"fa-thumbs-up","thumbs-down":"fa-thumbs-down",share:"fa-share",clock:"fa-clock",calendar:"fa-calendar","calendar-alt":"fa-calendar-alt",lock:"fa-lock",unlock:"fa-unlock",eye:"fa-eye","eye-slash":"fa-eye-slash",bell:"fa-bell",flag:"fa-flag",map:"fa-map","map-marker":"fa-map-marker-alt",globe:"fa-globe",link:"fa-link",unlink:"fa-unlink",sun:"fa-sun",moon:"fa-moon","chart-bar":"fa-chart-bar","chart-line":"fa-chart-line","chart-pie":"fa-chart-pie","chart-area":"fa-chart-area",spinner:"fa-spinner","circle-notch":"fa-circle-notch","check-badge":"fa-badge-check","badge-check":"fa-badge-check",cube:"fa-cube",cubes:"fa-cubes",plug:"fa-plug","puzzle-piece":"fa-puzzle-piece",flame:"fa-fire",fire:"fa-fire","message-square":"fa-comment-dots","dollar-sign":"fa-dollar-sign",menu:"fa-bars",x:"fa-times",list:"fa-list","layout-grid":"fa-th-large","th-large":"fa-th-large",grid:"fa-th-large",inbox:"fa-inbox",package:"fa-box",box:"fa-box",send:"fa-paper-plane","paper-plane":"fa-paper-plane","file-text":"fa-file-alt",paperclip:"fa-paperclip",truck:"fa-truck","refresh-cw":"fa-sync",sync:"fa-sync","undo-2":"fa-rotate-left","rotate-left":"fa-rotate-left","map-pin":"fa-map-marker-alt","help-circle":"fa-question-circle","alert-triangle":"fa-exclamation-triangle","bell-off":"fa-bell-slash","book-open":"fa-book-open",building:"fa-building","edit-2":"fa-edit","edit-3":"fa-edit","file-check":"fa-file-circle-check",mail:"fa-envelope",sparkles:"fa-wand-magic-sparkles","trash-2":"fa-trash","user-x":"fa-user-xmark"},de=({name:e,iconStyle:t="solid",size:r,color:n,spin:o=!1,pulse:a=!1,fixedWidth:s=!1,ariaLabel:l,className:f="",style:h,...c})=>{const i={solid:"fas",regular:"far",light:"fal",duotone:"fad",brands:"fab"},u=typeof e=="string"?e:String(e),g=Pc[u],p=g?String(g).replace(/^fa-/,""):u.replace(/^fa-/,""),m=[i[t],`fa-${p}`,r&&`fa-${r}`,o&&"fa-spin",a&&"fa-pulse",s&&"fa-fw",n,f].filter(Boolean).join(" "),w=l||p.replace(/-/g," ");return d.jsx("i",{className:m,style:h,"aria-label":w,role:"img",...c})},br=(e,t)=>window.G7Core?.t?.(e,t)??e,ro={minLength:{key:"minLength",labelKey:"auth.password_input.rules.minLength",defaultParam:6,validate:(e,t=6)=>e.length>=t},maxLength:{key:"maxLength",labelKey:"auth.password_input.rules.maxLength",defaultParam:20,validate:(e,t=20)=>e.length<=t},hasUppercase:{key:"hasUppercase",labelKey:"auth.password_input.rules.hasUppercase",validate:e=>/[A-Z]/.test(e)},hasLowercase:{key:"hasLowercase",labelKey:"auth.password_input.rules.hasLowercase",validate:e=>/[a-z]/.test(e)},hasNumber:{key:"hasNumber",labelKey:"auth.password_input.rules.hasNumber",validate:e=>/[0-9]/.test(e)},hasSpecial:{key:"hasSpecial",labelKey:"auth.password_input.rules.hasSpecial",validate:e=>/[!@#$%^&*(),.?":{}|<>]/.test(e)},noSpaces:{key:"noSpaces",labelKey:"auth.password_input.rules.noSpaces",validate:e=>!/\s/.test(e)},minTypes:{key:"minTypes",labelKey:"auth.password_input.rules.minTypes",defaultParam:3,validate:(e,t=3)=>{let r=0;return/[A-Z]/.test(e)&&r++,/[a-z]/.test(e)&&r++,/[0-9]/.test(e)&&r++,/[!@#$%^&*(),.?":{}|<>]/.test(e)&&r++,r>=t}}},Ba=[ro.minLength];function $c(e){const[t,r]=e.split(":"),n=ro[t];if(!n)return null;const o=r?parseInt(r,10):n.defaultParam;return{rule:n,param:isNaN(o)?n.defaultParam:o}}const Wa=k.forwardRef(({label:e,error:t,showToggle:r=!0,showValidation:n=!1,isConfirmField:o=!1,confirmTarget:a,onMatchChange:s,onValidityChange:l,rules:f,enableRules:h,showRules:c,wrapperClassName:i="",validationClassName:u="",className:g="",onChange:p,value:m,defaultValue:w,disabled:C,...O},v)=>{const E=k.useRef(null);k.useImperativeHandle(v,()=>E.current);const[I,_]=k.useState(!1),[M,x]=k.useState(!1),[y,T]=k.useState(m??w??"");k.useEffect(()=>{m!==void 0&&T(m)},[m]);const A=k.useMemo(()=>f&&f.length>0?f.map(j=>({rule:j,param:j.defaultParam})):h&&h.length>0?h.map(j=>$c(j)).filter(j=>j!==null):Ba.map(j=>({rule:j,param:j.defaultParam})),[f,h]),P=k.useMemo(()=>!c||c.length===0?A:A.filter(j=>c.includes(j.rule.key)),[A,c]),D=k.useMemo(()=>!n||o?{}:P.reduce((j,{rule:H,param:q})=>(j[H.key]=H.validate(y,q),j),{}),[y,P,n,o]),L=k.useMemo(()=>!o||a===void 0?!0:y===a&&y.length>0,[o,y,a]),U=k.useMemo(()=>n?Object.values(D).every(j=>j):!0,[D,n]);k.useEffect(()=>{if(l&&n&&!o){const j=Object.entries(D).filter(([,H])=>!H).map(([H])=>H);l(U,j)}},[U,D,l,n,o]),k.useEffect(()=>{s&&o&&s(L)},[L,s,o]);const S=k.useCallback(()=>{_(j=>!j)},[]),b=k.useCallback(j=>{const H=j.target.value;T(H),p&&p(j)},[p]),F=k.useCallback(j=>{x(j.getModifierState("CapsLock"))},[]),{loadingactions:$,formdata:G,...Y}=O,B={},K={};for(const[j,H]of Object.entries(Y))j.startsWith("data-editor-")||j==="onMouseMove"||j==="onMouseLeave"?B[j]=H:K[j]=H;return d.jsxs("div",{className:`relative ${i}`,...B,children:[d.jsxs("div",{className:"relative",children:[d.jsx("input",{ref:E,type:I?"text":"password",className:`${g} ${r?"pr-10":""}`,value:m!==void 0?m:void 0,defaultValue:m===void 0?w:void 0,onChange:b,onKeyDown:F,onKeyUp:F,disabled:C,...K}),r&&d.jsx("button",{type:"button",onClick:S,disabled:C,className:"absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed dark:disabled:opacity-50","aria-label":br(I?"auth.password_input.hide":"auth.password_input.show"),children:d.jsx(de,{name:I?"eye-slash":"eye",className:"w-5 h-5"})})]}),M&&!I&&d.jsxs("div",{className:"mt-2 flex items-center gap-1.5 text-sm text-yellow-600 dark:text-yellow-400",children:[d.jsx(de,{name:"triangle-exclamation",className:"w-4 h-4"}),d.jsx("span",{children:br("auth.password_input.caps_lock_on")})]}),o&&a!==void 0&&y.length>0&&d.jsxs("div",{className:`mt-2 flex items-center gap-1.5 text-sm ${L?"text-green-600 dark:text-green-400":"text-red-600 dark:text-red-400"}`,children:[d.jsx(de,{name:L?"check-circle":"xmark-circle",className:"w-4 h-4"}),d.jsx("span",{children:br(L?"auth.password_input.match":"auth.password_input.mismatch")})]}),n&&!o&&y.length>0&&d.jsx("div",{className:`mt-2 space-y-1 ${u}`,children:P.map(({rule:j,param:H})=>{const q=D[j.key],ue=H!==void 0?{count:H}:void 0;return d.jsxs("div",{className:`flex items-center gap-1.5 text-sm ${q?"text-green-600 dark:text-green-400":"text-gray-500 dark:text-gray-400"}`,children:[d.jsx(de,{name:q?"check-circle":"circle",className:"w-4 h-4"}),d.jsx("span",{children:br(j.labelKey,ue)})]},j.key)})}),t&&d.jsx("p",{className:"mt-1 text-sm text-red-600 dark:text-red-400",children:t})]})});Wa.displayName="PasswordInput";const no=({label:e,error:t,className:r="",onChange:n,value:o,defaultValue:a,...s})=>{const l=k.useRef(!1),f=k.useRef(null),[h,c]=k.useState(o??a??"");k.useEffect(()=>{!l.current&&o!==void 0&&c(o)},[o]);const i=k.useCallback(()=>{l.current=!0},[]),u=k.useCallback(O=>{if(l.current=!1,n){const v=O.target.value;c(v);const E=new Event("change",{bubbles:!0});Object.defineProperty(E,"target",{writable:!1,value:{value:v}}),Object.defineProperty(E,"currentTarget",{writable:!1,value:{value:v}}),n(E)}},[n]),g=k.useCallback(O=>{const v=O.target.value;c(v),!l.current&&n&&n(O)},[n]),{loadingactions:p,formdata:m,...w}=s,C=o!==void 0?{value:h}:{defaultValue:a};return d.jsx("textarea",{ref:f,className:r,onChange:g,onCompositionStart:i,onCompositionEnd:u,...C,...w})},yr=({children:e,className:t="",...r})=>d.jsx("label",{className:t,...r,children:e}),Dt=({children:e,className:t="",...r})=>d.jsx("p",{className:t,...r,children:e}),Lt=({className:e="",alt:t="",...r})=>d.jsx("img",{className:e,alt:t,...r}),Fc=({children:e,className:t="",...r})=>d.jsx("h1",{className:t,...r,children:e}),Va=({children:e,className:t="",...r})=>d.jsx("h2",{className:t,...r,children:e}),gn=({children:e,className:t="",...r})=>d.jsx("h3",{className:t,...r,children:e}),Ka=({children:e,className:t="",...r})=>d.jsx("h4",{className:t,...r,children:e}),oo=({children:e,className:t="",...r})=>d.jsx("ul",{className:t,...r,children:e}),Uc=({children:e,className:t="",...r})=>d.jsx("ol",{className:t,...r,children:e}),Xt=({children:e,className:t="",...r})=>d.jsx("li",{className:t,...r,children:e}),ao=({children:e,className:t="",...r})=>d.jsx("a",{className:t,...r,children:e}),so=({children:e,className:t="",...r})=>d.jsx("form",{className:t,...r,children:e}),zc=window.G7Core?.createLogger?.("Comp:Select")??{log:(...e)=>console.log("[Comp:Select]",...e),warn:(...e)=>console.warn("[Comp:Select]",...e),error:(...e)=>console.error("[Comp:Select]",...e)};function Hc(e){const t=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[e];return t||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[e]||e}const Ya=({children:e,label:t,error:r,options:n,className:o="",value:a,onChange:s,disabled:l,searchable:f=!1,searchPlaceholder:h,editorAttrs:c,...i})=>{const[u,g]=k.useState(!1),[p,m]=k.useState(""),[w,C]=k.useState(null),O=k.useRef(null),v=k.useRef(null),E=k.useRef(null),I=k.useRef(null),_=k.useMemo(()=>n?Array.isArray(n)?n.length===0?[]:typeof n[0]=="string"?n.map(S=>({value:S,label:Hc(S)})):n:(zc.warn("options prop is not an array:",n),null):null,[n]),M=k.useMemo(()=>_&&_.find(b=>String(b.value)===String(a))?.label||"",[_,a]),x=k.useMemo(()=>{if(!_)return null;if(!f||p.trim()==="")return _;const S=p.trim().toLowerCase();return _.filter(b=>b.label.toLowerCase().includes(S)||String(b.value).toLowerCase().includes(S))},[_,f,p]);k.useEffect(()=>{if(u&&f){m("");const S=setTimeout(()=>E.current?.focus(),0);return()=>clearTimeout(S)}u||m("")},[u,f]);const y=k.useCallback(()=>{if(!v.current)return;const S=v.current.getBoundingClientRect(),b=window.innerHeight-S.bottom,$=b<280&&S.top>b;C($?{bottom:window.innerHeight-S.top+4,left:S.left,width:S.width}:{top:S.bottom+4,left:S.left,width:S.width})},[]);k.useEffect(()=>{if(!u){C(null);return}return y(),window.addEventListener("scroll",y,!0),window.addEventListener("resize",y),()=>{window.removeEventListener("scroll",y,!0),window.removeEventListener("resize",y)}},[u,y]),k.useEffect(()=>{const S=b=>{const F=b.target;O.current&&!O.current.contains(F)&&(!I.current||!I.current.contains(F))&&g(!1)};return u&&document.addEventListener("mousedown",S),()=>{document.removeEventListener("mousedown",S)}},[u]),k.useEffect(()=>{const S=b=>{b.key==="Escape"&&u&&(g(!1),v.current?.focus())};return document.addEventListener("keydown",S),()=>document.removeEventListener("keydown",S)},[u]);const T=()=>{l||g(!u)},A=S=>{s&&s({target:{value:S},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),v.current?.focus()};if(!_)return d.jsx("select",{className:o,value:a,onChange:s,disabled:l,...c,...i,children:e});const P=o&&o.includes("bg-"),D=o&&/text-(gray|red|blue|green|yellow|white|black|indigo|purple|pink|orange|amber|emerald|teal|cyan|slate)-\d+|text-white|text-black/.test(o),L=P?`${o}${D?"":" text-gray-700 dark:text-gray-200"}`:`w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-gray-700 dark:text-gray-200 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none ${o}`,U={...c,...i};return d.jsxs(W,{ref:O,className:"relative",...U,children:[d.jsxs(Q,{ref:v,type:"button",onClick:T,disabled:l,className:`${L} flex items-center justify-between gap-2 text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`,"aria-haspopup":"listbox","aria-expanded":u,children:[d.jsx(R,{className:"truncate",children:M||" "}),d.jsx(fn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${u?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),u&&w&&Jt.createPortal(d.jsxs(W,{ref:I,className:"fixed z-[9999] bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",style:{top:w.top,bottom:w.bottom,left:w.left,width:w.width},role:"listbox",children:[f&&d.jsx(W,{className:"p-2 border-b border-gray-200 dark:border-gray-600",children:d.jsx(xt,{ref:E,type:"text",value:p,onChange:S=>m(S.target.value),placeholder:h??"Search...",className:"w-full px-3 py-2 bg-gray-100 dark:bg-gray-700 border-0 rounded-lg text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:outline-none",role:"searchbox","aria-label":h??"Search"})}),d.jsxs(W,{className:"py-2 max-h-60 overflow-auto",children:[x&&x.length===0&&d.jsx(W,{className:"px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center",children:p?"No results":""}),(x??[]).map(S=>{const b=String(S.value)===String(a);return d.jsxs(Q,{type:"button",onClick:()=>!S.disabled&&A(S.value),disabled:S.disabled,className:`w-full px-4 py-3 text-left flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${b?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-700 dark:text-gray-200"} ${S.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,role:"option","aria-selected":b,children:[d.jsx(R,{children:S.label}),b&&d.jsx(fn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]},S.value)})]})]}),document.body)]})},Gc=({children:e,className:t="",...r})=>d.jsx("option",{className:t,...r,children:e}),jc=({children:e,className:t="",...r})=>d.jsx("optgroup",{className:t,...r,children:e}),Xa=k.forwardRef(({label:e,className:t="",...r},n)=>d.jsx("input",{ref:n,type:"checkbox",className:t,...r}));Xa.displayName="Checkbox";const Bc=({children:e,className:t="",...r})=>d.jsx("table",{className:t,...r,children:e}),Wc=({children:e,className:t="",...r})=>d.jsx("thead",{className:t,...r,children:e}),Vc=({children:e,className:t="",...r})=>d.jsx("tbody",{className:t,...r,children:e}),Kc=({children:e,className:t="",...r})=>d.jsx("tr",{className:t,...r,children:e}),Yc=({children:e,className:t="",...r})=>d.jsx("th",{className:t,...r,children:e}),Xc=({children:e,className:t="",...r})=>d.jsx("td",{className:t,...r,children:e}),Fr=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("nav",{ref:n,className:t,...r,children:e}));Fr.displayName="Nav";const qa=({children:e,className:t="",...r})=>d.jsx("section",{className:t,...r,children:e}),qc=({children:e,className:t="",...r})=>d.jsx("code",{className:t,...r,children:e}),lo=k.forwardRef(({className:e="",...t},r)=>d.jsx("hr",{ref:r,className:e,...t})),Zc=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("header",{ref:n,className:t,...r,children:e})),Za=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,io=e=>{const t=Za(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},Qc=()=>{const e=localStorage.getItem("g7_color_scheme");return e&&["auto","light","dark"].includes(e)?e:"auto"},hn=({onThemeChange:e,className:t="",autoText:r="System",lightText:n="Light",darkText:o="Dark",id:a,editorAttrs:s})=>{const l=Qc(),[f,h]=k.useState(l),[c,i]=k.useState(!1),u=k.useRef(null);k.useEffect(()=>{io(f)},[]),k.useEffect(()=>{if(f!=="auto")return;const m=window.matchMedia("(prefers-color-scheme: dark)"),w=()=>{io("auto")};return m.addEventListener("change",w),()=>m.removeEventListener("change",w)},[f]),k.useEffect(()=>{const m=w=>{u.current&&!u.current.contains(w.target)&&i(!1)};return document.addEventListener("mousedown",m),()=>document.removeEventListener("mousedown",m)},[]);const g=m=>{h(m),localStorage.setItem("g7_color_scheme",m),io(m),i(!1),e?.(m)},p=()=>Za(f)==="dark"?Je.Moon:Je.Sun;return d.jsxs(W,{ref:u,className:`relative ${t}`,id:a,...s,children:[d.jsx(Q,{onClick:()=>i(!c),className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-gray-600 dark:text-gray-400","aria-label":"Toggle theme",children:d.jsx(de,{name:p(),className:"w-5 h-5"})}),c&&d.jsx(W,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:d.jsxs(W,{className:"py-2",children:[d.jsxs(Q,{onClick:()=>g("auto"),className:` +var SirsoftBasic=(function(J,d,k,Jt){"use strict";function Lc(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e){for(const r in e)if(r!=="default"){const n=Object.getOwnPropertyDescriptor(e,r);Object.defineProperty(t,r,n.get?n:{enumerable:!0,get:()=>e[r]})}}return t.default=e,Object.freeze(t)}const N=Lc(k),Pc=()=>window.G7Core,Q=k.forwardRef(({children:e,variant:t="primary",size:r="md",className:n="",...o},a)=>{const s="inline-flex items-center justify-center",l=Pc()?.style?.mergeClasses?.(s,n)??`${s} ${n}`;return d.jsx("button",{ref:a,className:l,...o,children:e})});Q.displayName="Button";const W=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("div",{ref:n,className:t,...r,children:e})),R=({children:e,className:t="",...r})=>d.jsx("span",{className:t,...r,children:e}),fn=({children:e,className:t="",...r})=>d.jsx("svg",{className:t,...r,children:e}),xt=k.forwardRef(({label:e,error:t,className:r="",onChange:n,onKeyPress:o,value:a,defaultValue:s,type:l="text",checked:f,defaultChecked:h,...c},i)=>{const u=l==="radio"||l==="checkbox",g=k.useRef(!1),p=k.useRef(null);k.useImperativeHandle(i,()=>p.current);const[m,x]=k.useState(a??s??"");k.useEffect(()=>{!u&&!g.current&&a!==void 0&&x(a)},[a,u]);const C=k.useCallback(()=>{g.current=!0},[]),O=k.useCallback(y=>{if(g.current=!1,n){const T=y.target.value;x(T);const A=new Event("change",{bubbles:!0});Object.defineProperty(A,"target",{writable:!1,value:{value:T}}),Object.defineProperty(A,"currentTarget",{writable:!1,value:{value:T}}),n(A)}},[n]),v=k.useCallback(y=>{const T=y.target.value;x(T),!g.current&&n&&n(y)},[n]),E=k.useCallback(y=>{g.current||o&&o(y)},[o]),{loadingactions:I,formdata:_,...M}=c;if(u)return d.jsx("input",{ref:p,type:l,className:r,onChange:n,checked:f,defaultChecked:h,value:a,...M});const w=a!==void 0?{value:m}:{defaultValue:s};return d.jsx("input",{ref:p,type:l,className:r,onChange:v,onKeyPress:E,onCompositionStart:C,onCompositionEnd:O,...w,...M})});xt.displayName="Input";const Ba=k.forwardRef(({accept:e,maxSize:t,onChange:r,onError:n,buttonText:o="Browse",placeholder:a="No file selected",className:s="",disabled:l,...f},h)=>{const c=k.useRef(null),i=h||c,[u,g]=k.useState(""),p={},m={};Object.entries(f).forEach(([v,E])=>{v.startsWith("data-editor-")||v==="id"||v==="onMouseMove"||v==="onMouseLeave"?p[v]=E:m[v]=E});const x=k.useCallback(()=>{!l&&i.current&&i.current.click()},[l,i]),C=k.useCallback(v=>{const E=v.target.files?.[0]||null;if(!E){g(""),r?.(null);return}if(t&&E.size>t*1024*1024){const I=`File size exceeds ${t}MB limit`;n?.(I),i.current&&(i.current.value=""),g("");return}g(E.name),r?.(E)},[t,r,n,i]),O=k.useCallback(v=>{v.stopPropagation(),i.current&&(i.current.value=""),g(""),r?.(null)},[r,i]);return d.jsxs(W,{className:`flex items-center gap-2 ${s}`,...p,children:[d.jsx(xt,{ref:i,type:"file",accept:e,onChange:C,disabled:l,className:"hidden",...m}),d.jsx(Q,{type:"button",onClick:x,disabled:l,className:"px-4 py-2 bg-gray-100 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md text-sm font-medium text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed",children:o}),d.jsx(R,{className:"flex-1 text-sm text-gray-600 dark:text-gray-400 truncate",children:u||a}),u&&!l&&d.jsx(Q,{type:"button",onClick:O,className:"p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200","aria-label":"Clear file",children:d.jsx(fn,{className:"w-4 h-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]})});Ba.displayName="FileInput";var Je=(e=>(e.User="fa-user",e.Users="fa-users",e.UserCircle="fa-user-circle",e.UserPlus="fa-user-plus",e.UserMinus="fa-user-minus",e.UserShield="fa-user-shield",e.Search="fa-search",e.Edit="fa-edit",e.Pencil="fa-pencil-alt",e.Trash="fa-trash",e.Save="fa-save",e.Plus="fa-plus",e.Minus="fa-minus",e.Check="fa-check",e.Times="fa-times",e.Download="fa-download",e.Upload="fa-upload",e.Copy="fa-copy",e.ExternalLink="fa-external-link-alt",e.Home="fa-home",e.ChevronLeft="fa-chevron-left",e.ChevronRight="fa-chevron-right",e.ChevronUp="fa-chevron-up",e.ChevronDown="fa-chevron-down",e.AngleLeft="fa-angle-left",e.AngleRight="fa-angle-right",e.AngleUp="fa-angle-up",e.AngleDown="fa-angle-down",e.ArrowUp="fa-arrow-up",e.ArrowDown="fa-arrow-down",e.ArrowLeft="fa-arrow-left",e.ArrowRight="fa-arrow-right",e.Bars="fa-bars",e.EllipsisVertical="fa-ellipsis-v",e.EllipsisHorizontal="fa-ellipsis-h",e.CheckCircle="fa-check-circle",e.TimesCircle="fa-times-circle",e.ExclamationCircle="fa-exclamation-circle",e.ExclamationTriangle="fa-exclamation-triangle",e.InfoCircle="fa-info-circle",e.QuestionCircle="fa-question-circle",e.XCircle="fa-times-circle",e.ShoppingCart="fa-shopping-cart",e.ShoppingBag="fa-shopping-bag",e.CreditCard="fa-credit-card",e.Tag="fa-tag",e.Tags="fa-tags",e.File="fa-file",e.FileAlt="fa-file-alt",e.Folder="fa-folder",e.FolderOpen="fa-folder-open",e.Envelope="fa-envelope",e.Phone="fa-phone",e.Comment="fa-comment",e.Comments="fa-comments",e.Cog="fa-cog",e.Cogs="fa-cogs",e.Settings="fa-cog",e.Wrench="fa-wrench",e.Sliders="fa-sliders-h",e.Image="fa-image",e.Images="fa-images",e.Video="fa-video",e.Camera="fa-camera",e.Heart="fa-heart",e.Star="fa-star",e.ThumbsUp="fa-thumbs-up",e.ThumbsDown="fa-thumbs-down",e.Share="fa-share",e.Clock="fa-clock",e.Calendar="fa-calendar",e.CalendarAlt="fa-calendar-alt",e.Lock="fa-lock",e.Unlock="fa-unlock",e.Eye="fa-eye",e.EyeSlash="fa-eye-slash",e.Bell="fa-bell",e.Flag="fa-flag",e.Map="fa-map",e.MapMarker="fa-map-marker-alt",e.Globe="fa-globe",e.Link="fa-link",e.Unlink="fa-unlink",e.Sun="fa-sun",e.Moon="fa-moon",e.ChartBar="fa-chart-bar",e.ChartLine="fa-chart-line",e.ChartPie="fa-chart-pie",e.ChartArea="fa-chart-area",e.Spinner="fa-spinner",e.CircleNotch="fa-circle-notch",e.CheckBadge="fa-badge-check",e.BellSlash="fa-bell-slash",e.BookOpen="fa-book-open",e.Building="fa-building",e.FileCircleCheck="fa-file-circle-check",e.WandMagicSparkles="fa-wand-magic-sparkles",e.UserXmark="fa-user-xmark",e.Cube="fa-cube",e.Cubes="fa-cubes",e.Plug="fa-plug",e.PuzzlePiece="fa-puzzle-piece",e.Fire="fa-fire",e.CommentDots="fa-comment-dots",e.DollarSign="fa-dollar-sign",e.Bars2="fa-bars",e.X="fa-times",e.List="fa-list",e.ThLarge="fa-th-large",e.Inbox="fa-inbox",e.Box="fa-box",e.PaperPlane="fa-paper-plane",e.FileText="fa-file-alt",e.Paperclip="fa-paperclip",e.Truck="fa-truck",e.Sync="fa-sync",e.RotateLeft="fa-rotate-left",e.MapPin="fa-map-marker-alt",e.HelpCircle="fa-question-circle",e))(Je||{});const $c={user:"fa-user",users:"fa-users","user-circle":"fa-user-circle","user-plus":"fa-user-plus","user-minus":"fa-user-minus","user-shield":"fa-user-shield",search:"fa-search",edit:"fa-edit",pencil:"fa-pencil-alt",trash:"fa-trash",save:"fa-save",plus:"fa-plus",minus:"fa-minus",check:"fa-check",times:"fa-times",download:"fa-download",upload:"fa-upload",copy:"fa-copy","external-link":"fa-external-link-alt",home:"fa-home","chevron-left":"fa-chevron-left","chevron-right":"fa-chevron-right","chevron-up":"fa-chevron-up","chevron-down":"fa-chevron-down","angle-left":"fa-angle-left","angle-right":"fa-angle-right","angle-up":"fa-angle-up","angle-down":"fa-angle-down","arrow-up":"fa-arrow-up","arrow-down":"fa-arrow-down","arrow-left":"fa-arrow-left","arrow-right":"fa-arrow-right",bars:"fa-bars","ellipsis-vertical":"fa-ellipsis-v","ellipsis-v":"fa-ellipsis-v","ellipsis-horizontal":"fa-ellipsis-h","ellipsis-h":"fa-ellipsis-h","check-circle":"fa-check-circle","times-circle":"fa-times-circle","x-circle":"fa-times-circle","exclamation-circle":"fa-exclamation-circle","exclamation-triangle":"fa-exclamation-triangle","info-circle":"fa-info-circle","question-circle":"fa-question-circle","shopping-cart":"fa-shopping-cart","shopping-bag":"fa-shopping-bag","credit-card":"fa-credit-card",tag:"fa-tag",tags:"fa-tags",file:"fa-file","file-alt":"fa-file-alt",folder:"fa-folder","folder-open":"fa-folder-open",envelope:"fa-envelope",phone:"fa-phone",comment:"fa-comment",comments:"fa-comments",cog:"fa-cog",cogs:"fa-cogs",settings:"fa-cog",wrench:"fa-wrench",sliders:"fa-sliders-h",image:"fa-image",images:"fa-images",video:"fa-video",camera:"fa-camera",heart:"fa-heart",star:"fa-star","thumbs-up":"fa-thumbs-up","thumbs-down":"fa-thumbs-down",share:"fa-share",clock:"fa-clock",calendar:"fa-calendar","calendar-alt":"fa-calendar-alt",lock:"fa-lock",unlock:"fa-unlock",eye:"fa-eye","eye-slash":"fa-eye-slash",bell:"fa-bell",flag:"fa-flag",map:"fa-map","map-marker":"fa-map-marker-alt",globe:"fa-globe",link:"fa-link",unlink:"fa-unlink",sun:"fa-sun",moon:"fa-moon","chart-bar":"fa-chart-bar","chart-line":"fa-chart-line","chart-pie":"fa-chart-pie","chart-area":"fa-chart-area",spinner:"fa-spinner","circle-notch":"fa-circle-notch","check-badge":"fa-badge-check","badge-check":"fa-badge-check",cube:"fa-cube",cubes:"fa-cubes",plug:"fa-plug","puzzle-piece":"fa-puzzle-piece",flame:"fa-fire",fire:"fa-fire","message-square":"fa-comment-dots","dollar-sign":"fa-dollar-sign",menu:"fa-bars",x:"fa-times",list:"fa-list","layout-grid":"fa-th-large","th-large":"fa-th-large",grid:"fa-th-large",inbox:"fa-inbox",package:"fa-box",box:"fa-box",send:"fa-paper-plane","paper-plane":"fa-paper-plane","file-text":"fa-file-alt",paperclip:"fa-paperclip",truck:"fa-truck","refresh-cw":"fa-sync",sync:"fa-sync","undo-2":"fa-rotate-left","rotate-left":"fa-rotate-left","map-pin":"fa-map-marker-alt","help-circle":"fa-question-circle","alert-triangle":"fa-exclamation-triangle","bell-off":"fa-bell-slash","book-open":"fa-book-open",building:"fa-building","edit-2":"fa-edit","edit-3":"fa-edit","file-check":"fa-file-circle-check",mail:"fa-envelope",sparkles:"fa-wand-magic-sparkles","trash-2":"fa-trash","user-x":"fa-user-xmark"},de=({name:e,iconStyle:t="solid",size:r,color:n,spin:o=!1,pulse:a=!1,fixedWidth:s=!1,ariaLabel:l,className:f="",style:h,...c})=>{const i={solid:"fas",regular:"far",light:"fal",duotone:"fad",brands:"fab"},u=typeof e=="string"?e:String(e),g=$c[u],p=g?String(g).replace(/^fa-/,""):u.replace(/^fa-/,""),m=[i[t],`fa-${p}`,r&&`fa-${r}`,o&&"fa-spin",a&&"fa-pulse",s&&"fa-fw",n,f].filter(Boolean).join(" "),x=l||p.replace(/-/g," ");return d.jsx("i",{className:m,style:h,"aria-label":x,role:"img",...c})},br=(e,t)=>window.G7Core?.t?.(e,t)??e,ro={minLength:{key:"minLength",labelKey:"auth.password_input.rules.minLength",defaultParam:6,validate:(e,t=6)=>e.length>=t},maxLength:{key:"maxLength",labelKey:"auth.password_input.rules.maxLength",defaultParam:20,validate:(e,t=20)=>e.length<=t},hasUppercase:{key:"hasUppercase",labelKey:"auth.password_input.rules.hasUppercase",validate:e=>/[A-Z]/.test(e)},hasLowercase:{key:"hasLowercase",labelKey:"auth.password_input.rules.hasLowercase",validate:e=>/[a-z]/.test(e)},hasNumber:{key:"hasNumber",labelKey:"auth.password_input.rules.hasNumber",validate:e=>/[0-9]/.test(e)},hasSpecial:{key:"hasSpecial",labelKey:"auth.password_input.rules.hasSpecial",validate:e=>/[!@#$%^&*(),.?":{}|<>]/.test(e)},noSpaces:{key:"noSpaces",labelKey:"auth.password_input.rules.noSpaces",validate:e=>!/\s/.test(e)},minTypes:{key:"minTypes",labelKey:"auth.password_input.rules.minTypes",defaultParam:3,validate:(e,t=3)=>{let r=0;return/[A-Z]/.test(e)&&r++,/[a-z]/.test(e)&&r++,/[0-9]/.test(e)&&r++,/[!@#$%^&*(),.?":{}|<>]/.test(e)&&r++,r>=t}}},Wa=[ro.minLength];function Fc(e){const[t,r]=e.split(":"),n=ro[t];if(!n)return null;const o=r?parseInt(r,10):n.defaultParam;return{rule:n,param:isNaN(o)?n.defaultParam:o}}const Va=k.forwardRef(({label:e,error:t,showToggle:r=!0,showValidation:n=!1,isConfirmField:o=!1,confirmTarget:a,onMatchChange:s,onValidityChange:l,rules:f,enableRules:h,showRules:c,wrapperClassName:i="",validationClassName:u="",className:g="",onChange:p,value:m,defaultValue:x,disabled:C,...O},v)=>{const E=k.useRef(null);k.useImperativeHandle(v,()=>E.current);const[I,_]=k.useState(!1),[M,w]=k.useState(!1),[y,T]=k.useState(m??x??"");k.useEffect(()=>{m!==void 0&&T(m)},[m]);const A=k.useMemo(()=>f&&f.length>0?f.map(j=>({rule:j,param:j.defaultParam})):h&&h.length>0?h.map(j=>Fc(j)).filter(j=>j!==null):Wa.map(j=>({rule:j,param:j.defaultParam})),[f,h]),P=k.useMemo(()=>!c||c.length===0?A:A.filter(j=>c.includes(j.rule.key)),[A,c]),D=k.useMemo(()=>!n||o?{}:P.reduce((j,{rule:H,param:q})=>(j[H.key]=H.validate(y,q),j),{}),[y,P,n,o]),L=k.useMemo(()=>!o||a===void 0?!0:y===a&&y.length>0,[o,y,a]),U=k.useMemo(()=>n?Object.values(D).every(j=>j):!0,[D,n]);k.useEffect(()=>{if(l&&n&&!o){const j=Object.entries(D).filter(([,H])=>!H).map(([H])=>H);l(U,j)}},[U,D,l,n,o]),k.useEffect(()=>{s&&o&&s(L)},[L,s,o]);const S=k.useCallback(()=>{_(j=>!j)},[]),b=k.useCallback(j=>{const H=j.target.value;T(H),p&&p(j)},[p]),F=k.useCallback(j=>{w(j.getModifierState("CapsLock"))},[]),{loadingactions:$,formdata:G,...Y}=O,B={},K={};for(const[j,H]of Object.entries(Y))j.startsWith("data-editor-")||j==="onMouseMove"||j==="onMouseLeave"?B[j]=H:K[j]=H;return d.jsxs("div",{className:`relative ${i}`,...B,children:[d.jsxs("div",{className:"relative",children:[d.jsx("input",{ref:E,type:I?"text":"password",className:`${g} ${r?"pr-10":""}`,value:m!==void 0?m:void 0,defaultValue:m===void 0?x:void 0,onChange:b,onKeyDown:F,onKeyUp:F,disabled:C,...K}),r&&d.jsx("button",{type:"button",onClick:S,disabled:C,className:"absolute right-2 top-1/2 -translate-y-1/2 p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 disabled:opacity-50 disabled:cursor-not-allowed dark:disabled:opacity-50","aria-label":br(I?"auth.password_input.hide":"auth.password_input.show"),children:d.jsx(de,{name:I?"eye-slash":"eye",className:"w-5 h-5"})})]}),M&&!I&&d.jsxs("div",{className:"mt-2 flex items-center gap-1.5 text-sm text-yellow-600 dark:text-yellow-400",children:[d.jsx(de,{name:"triangle-exclamation",className:"w-4 h-4"}),d.jsx("span",{children:br("auth.password_input.caps_lock_on")})]}),o&&a!==void 0&&y.length>0&&d.jsxs("div",{className:`mt-2 flex items-center gap-1.5 text-sm ${L?"text-green-600 dark:text-green-400":"text-red-600 dark:text-red-400"}`,children:[d.jsx(de,{name:L?"check-circle":"xmark-circle",className:"w-4 h-4"}),d.jsx("span",{children:br(L?"auth.password_input.match":"auth.password_input.mismatch")})]}),n&&!o&&y.length>0&&d.jsx("div",{className:`mt-2 space-y-1 ${u}`,children:P.map(({rule:j,param:H})=>{const q=D[j.key],ue=H!==void 0?{count:H}:void 0;return d.jsxs("div",{className:`flex items-center gap-1.5 text-sm ${q?"text-green-600 dark:text-green-400":"text-gray-500 dark:text-gray-400"}`,children:[d.jsx(de,{name:q?"check-circle":"circle",className:"w-4 h-4"}),d.jsx("span",{children:br(j.labelKey,ue)})]},j.key)})}),t&&d.jsx("p",{className:"mt-1 text-sm text-red-600 dark:text-red-400",children:t})]})});Va.displayName="PasswordInput";const no=({label:e,error:t,className:r="",onChange:n,value:o,defaultValue:a,...s})=>{const l=k.useRef(!1),f=k.useRef(null),[h,c]=k.useState(o??a??"");k.useEffect(()=>{!l.current&&o!==void 0&&c(o)},[o]);const i=k.useCallback(()=>{l.current=!0},[]),u=k.useCallback(O=>{if(l.current=!1,n){const v=O.target.value;c(v);const E=new Event("change",{bubbles:!0});Object.defineProperty(E,"target",{writable:!1,value:{value:v}}),Object.defineProperty(E,"currentTarget",{writable:!1,value:{value:v}}),n(E)}},[n]),g=k.useCallback(O=>{const v=O.target.value;c(v),!l.current&&n&&n(O)},[n]),{loadingactions:p,formdata:m,...x}=s,C=o!==void 0?{value:h}:{defaultValue:a};return d.jsx("textarea",{ref:f,className:r,onChange:g,onCompositionStart:i,onCompositionEnd:u,...C,...x})},yr=({children:e,className:t="",...r})=>d.jsx("label",{className:t,...r,children:e}),Dt=({children:e,className:t="",...r})=>d.jsx("p",{className:t,...r,children:e}),Lt=({className:e="",alt:t="",...r})=>d.jsx("img",{className:e,alt:t,...r}),Uc=({children:e,className:t="",...r})=>d.jsx("h1",{className:t,...r,children:e}),Ka=({children:e,className:t="",...r})=>d.jsx("h2",{className:t,...r,children:e}),gn=({children:e,className:t="",...r})=>d.jsx("h3",{className:t,...r,children:e}),Ya=({children:e,className:t="",...r})=>d.jsx("h4",{className:t,...r,children:e}),oo=({children:e,className:t="",...r})=>d.jsx("ul",{className:t,...r,children:e}),zc=({children:e,className:t="",...r})=>d.jsx("ol",{className:t,...r,children:e}),Xt=({children:e,className:t="",...r})=>d.jsx("li",{className:t,...r,children:e}),ao=({children:e,className:t="",...r})=>d.jsx("a",{className:t,...r,children:e}),so=({children:e,className:t="",...r})=>d.jsx("form",{className:t,...r,children:e}),Hc=window.G7Core?.createLogger?.("Comp:Select")??{log:(...e)=>console.log("[Comp:Select]",...e),warn:(...e)=>console.warn("[Comp:Select]",...e),error:(...e)=>console.error("[Comp:Select]",...e)};function Gc(e){const t=window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[e];return t||{ko:"한국어",en:"English",ja:"日本語",zh:"中文",es:"Español",fr:"Français",de:"Deutsch"}[e]||e}const Xa=({children:e,label:t,error:r,options:n,className:o="",value:a,onChange:s,disabled:l,searchable:f=!1,searchPlaceholder:h,editorAttrs:c,...i})=>{const[u,g]=k.useState(!1),[p,m]=k.useState(""),[x,C]=k.useState(null),O=k.useRef(null),v=k.useRef(null),E=k.useRef(null),I=k.useRef(null),_=k.useMemo(()=>n?Array.isArray(n)?n.length===0?[]:typeof n[0]=="string"?n.map(S=>({value:S,label:Gc(S)})):n:(Hc.warn("options prop is not an array:",n),null):null,[n]),M=k.useMemo(()=>_&&_.find(b=>String(b.value)===String(a))?.label||"",[_,a]),w=k.useMemo(()=>{if(!_)return null;if(!f||p.trim()==="")return _;const S=p.trim().toLowerCase();return _.filter(b=>b.label.toLowerCase().includes(S)||String(b.value).toLowerCase().includes(S))},[_,f,p]);k.useEffect(()=>{if(u&&f){m("");const S=setTimeout(()=>E.current?.focus(),0);return()=>clearTimeout(S)}u||m("")},[u,f]);const y=k.useCallback(()=>{if(!v.current)return;const S=v.current.getBoundingClientRect(),b=window.innerHeight-S.bottom,$=b<280&&S.top>b;C($?{bottom:window.innerHeight-S.top+4,left:S.left,width:S.width}:{top:S.bottom+4,left:S.left,width:S.width})},[]);k.useEffect(()=>{if(!u){C(null);return}return y(),window.addEventListener("scroll",y,!0),window.addEventListener("resize",y),()=>{window.removeEventListener("scroll",y,!0),window.removeEventListener("resize",y)}},[u,y]),k.useEffect(()=>{const S=b=>{const F=b.target;O.current&&!O.current.contains(F)&&(!I.current||!I.current.contains(F))&&g(!1)};return u&&document.addEventListener("mousedown",S),()=>{document.removeEventListener("mousedown",S)}},[u]),k.useEffect(()=>{const S=b=>{b.key==="Escape"&&u&&(g(!1),v.current?.focus())};return document.addEventListener("keydown",S),()=>document.removeEventListener("keydown",S)},[u]);const T=()=>{l||g(!u)},A=S=>{s&&s({target:{value:S},preventDefault:()=>{},stopPropagation:()=>{},type:"change"}),g(!1),v.current?.focus()};if(!_)return d.jsx("select",{className:o,value:a,onChange:s,disabled:l,...c,...i,children:e});const P=o&&o.includes("bg-"),D=o&&/text-(gray|red|blue|green|yellow|white|black|indigo|purple|pink|orange|amber|emerald|teal|cyan|slate)-\d+|text-white|text-black/.test(o),L=P?`${o}${D?"":" text-gray-700 dark:text-gray-200"}`:`w-full px-4 py-2.5 bg-gray-100 dark:bg-gray-700 border-0 rounded-xl text-gray-700 dark:text-gray-200 font-medium focus:ring-2 focus:ring-blue-500 focus:outline-none ${o}`,U={...c,...i};return d.jsxs(W,{ref:O,className:"relative",...U,children:[d.jsxs(Q,{ref:v,type:"button",onClick:T,disabled:l,className:`${L} flex items-center justify-between gap-2 text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed`,"aria-haspopup":"listbox","aria-expanded":u,children:[d.jsx(R,{className:"truncate",children:M||" "}),d.jsx(fn,{className:`w-4 h-4 flex-shrink-0 transition-transform duration-200 ${u?"rotate-180":""}`,fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M19 9l-7 7-7-7"})})]}),u&&x&&Jt.createPortal(d.jsxs(W,{ref:I,className:"fixed z-[9999] bg-white dark:bg-gray-800 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-600 overflow-hidden",style:{top:x.top,bottom:x.bottom,left:x.left,width:x.width},role:"listbox",children:[f&&d.jsx(W,{className:"p-2 border-b border-gray-200 dark:border-gray-600",children:d.jsx(xt,{ref:E,type:"text",value:p,onChange:S=>m(S.target.value),placeholder:h??"Search...",className:"w-full px-3 py-2 bg-gray-100 dark:bg-gray-700 border-0 rounded-lg text-sm text-gray-700 dark:text-gray-200 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:outline-none",role:"searchbox","aria-label":h??"Search"})}),d.jsxs(W,{className:"py-2 max-h-60 overflow-auto",children:[w&&w.length===0&&d.jsx(W,{className:"px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center",children:p?"No results":""}),(w??[]).map(S=>{const b=String(S.value)===String(a);return d.jsxs(Q,{type:"button",onClick:()=>!S.disabled&&A(S.value),disabled:S.disabled,className:`w-full px-4 py-3 text-left flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors ${b?"text-blue-600 dark:text-blue-400 font-medium":"text-gray-700 dark:text-gray-200"} ${S.disabled?"opacity-50 cursor-not-allowed":"cursor-pointer"}`,role:"option","aria-selected":b,children:[d.jsx(R,{children:S.label}),b&&d.jsx(fn,{className:"w-5 h-5 text-blue-600 dark:text-blue-400",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M5 13l4 4L19 7"})})]},S.value)})]})]}),document.body)]})},jc=({children:e,className:t="",...r})=>d.jsx("option",{className:t,...r,children:e}),Bc=({children:e,className:t="",...r})=>d.jsx("optgroup",{className:t,...r,children:e}),qa=k.forwardRef(({label:e,className:t="",...r},n)=>d.jsx("input",{ref:n,type:"checkbox",className:t,...r}));qa.displayName="Checkbox";const Wc=({children:e,className:t="",...r})=>d.jsx("table",{className:t,...r,children:e}),Vc=({children:e,className:t="",...r})=>d.jsx("thead",{className:t,...r,children:e}),Kc=({children:e,className:t="",...r})=>d.jsx("tbody",{className:t,...r,children:e}),Yc=({children:e,className:t="",...r})=>d.jsx("tr",{className:t,...r,children:e}),Xc=({children:e,className:t="",...r})=>d.jsx("th",{className:t,...r,children:e}),qc=({children:e,className:t="",...r})=>d.jsx("td",{className:t,...r,children:e}),Fr=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("nav",{ref:n,className:t,...r,children:e}));Fr.displayName="Nav";const Za=({children:e,className:t="",...r})=>d.jsx("section",{className:t,...r,children:e}),Zc=({children:e,className:t="",...r})=>d.jsx("code",{className:t,...r,children:e}),lo=k.forwardRef(({className:e="",...t},r)=>d.jsx("hr",{ref:r,className:e,...t})),Qc=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("header",{ref:n,className:t,...r,children:e})),Qa=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,io=e=>{const t=Qa(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")},Jc=()=>{const e=localStorage.getItem("g7_color_scheme");return e&&["auto","light","dark"].includes(e)?e:"auto"},hn=({onThemeChange:e,className:t="",autoText:r="System",lightText:n="Light",darkText:o="Dark",id:a,editorAttrs:s})=>{const l=Jc(),[f,h]=k.useState(l),[c,i]=k.useState(!1),u=k.useRef(null);k.useEffect(()=>{io(f)},[]),k.useEffect(()=>{if(f!=="auto")return;const m=window.matchMedia("(prefers-color-scheme: dark)"),x=()=>{io("auto")};return m.addEventListener("change",x),()=>m.removeEventListener("change",x)},[f]),k.useEffect(()=>{const m=x=>{u.current&&!u.current.contains(x.target)&&i(!1)};return document.addEventListener("mousedown",m),()=>document.removeEventListener("mousedown",m)},[]);const g=m=>{h(m),localStorage.setItem("g7_color_scheme",m),io(m),i(!1),e?.(m)},p=()=>Qa(f)==="dark"?Je.Moon:Je.Sun;return d.jsxs(W,{ref:u,className:`relative ${t}`,id:a,...s,children:[d.jsx(Q,{onClick:()=>i(!c),className:"p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors text-gray-600 dark:text-gray-400","aria-label":"Toggle theme",children:d.jsx(de,{name:p(),className:"w-5 h-5"})}),c&&d.jsx(W,{className:"absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50",children:d.jsxs(W,{className:"py-2",children:[d.jsxs(Q,{onClick:()=>g("auto"),className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${f==="auto"?"bg-gray-50 dark:bg-gray-700":""} `,children:[d.jsx(de,{name:Je.Settings,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),d.jsx(R,{className:"flex-1 text-left text-gray-900 dark:text-white",children:r}),f==="auto"&&d.jsx(de,{name:Je.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),d.jsxs(Q,{onClick:()=>g("light"),className:` @@ -7,17 +7,17 @@ var SirsoftBasic=(function(J,d,k,Jt){"use strict";function Dc(e){const t=Object. `,children:[d.jsx(de,{name:Je.Sun,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),d.jsx(R,{className:"flex-1 text-left text-gray-900 dark:text-white",children:n}),f==="light"&&d.jsx(de,{name:Je.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]}),d.jsxs(Q,{onClick:()=>g("dark"),className:` w-full px-4 py-2 text-left hover:bg-gray-50 dark:hover:bg-gray-700 flex items-center gap-3 transition-colors ${f==="dark"?"bg-gray-50 dark:bg-gray-700":""} - `,children:[d.jsx(de,{name:Je.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),d.jsx(R,{className:"flex-1 text-left text-gray-900 dark:text-white",children:o}),f==="dark"&&d.jsx(de,{name:Je.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Jc=Object.freeze(Object.defineProperty({__proto__:null,ThemeToggle:hn,default:hn},Symbol.toStringTag,{value:"Module"})),Qa={xs:"w-6 h-6 text-xs",sm:"w-8 h-8 text-sm",md:"w-10 h-10 text-base",lg:"w-12 h-12 text-lg",xl:"w-16 h-16 text-xl","2xl":"w-24 h-24 text-2xl","3xl":"w-32 h-32 text-3xl","4xl":"w-40 h-40 text-4xl","5xl":"w-48 h-48 text-5xl"},Rc="rounded-full overflow-hidden flex-shrink-0 border border-gray-200 dark:border-gray-700",ed="grayscale opacity-50",co={normal:"bg-gradient-to-br from-blue-500 to-purple-600",withdrawn:"bg-gray-300 dark:bg-gray-600",guest:"bg-gray-200 dark:bg-gray-700"},Ur=({author:e,name:t,avatar:r,size:n="md",className:o="",text:a,isWithdrawn:s=!1,isGuest:l=!1,id:f,editorAttrs:h})=>{const c=r??e?.avatar,i=a??t??e?.name??"?",u=s||e?.status==="withdrawn",g=l||e?.is_guest||!1,p=k.useMemo(()=>[Qa[n]||Qa.md,Rc,u?ed:"",o].filter(Boolean).join(" "),[n,u,o]),m=g?co.guest:u?co.withdrawn:co.normal,w=()=>g?d.jsx(W,{className:`w-full h-full flex items-center justify-center ${m}`,children:d.jsx(de,{name:"user",className:"text-gray-400 dark:text-gray-500"})}):u?d.jsx(W,{className:`w-full h-full flex items-center justify-center ${m}`,children:d.jsx(de,{name:"user",className:"text-gray-500 dark:text-gray-400"})}):d.jsx(W,{className:`w-full h-full flex items-center justify-center text-white font-semibold ${m}`,children:i.charAt(0).toUpperCase()});return d.jsx(W,{className:p,id:f,...h,children:c&&!g&&!u?d.jsx(Lt,{src:c,alt:i,className:"w-full h-full object-cover"}):w()})},td=Object.freeze(Object.defineProperty({__proto__:null,Avatar:Ur},Symbol.toStringTag,{value:"Module"})),rd=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...e)=>console.log("[Comp:SlotContainer]",...e),warn:(...e)=>console.warn("[Comp:SlotContainer]",...e),error:(...e)=>console.error("[Comp:SlotContainer]",...e)};function Ja(e,t){return t&&e&&typeof e.id=="string"&&e.id.length>0?{...e,id:`${e.id}__${t}`}:e}const pn=({slotId:e,className:t="",emptyContent:r,style:n,id:o,children:a})=>{const[s,l]=k.useState([]),[,f]=k.useState({}),h=k.useCallback(()=>{const p=window.__slotContextValue;if(p)return p;const m=window.G7Core;return m&&m.getSlotContext?.()||null},[]),c=k.useCallback(()=>{const p=h();p&&p.isEnabled&&l(p.getSlotComponents(e))},[e,h]);k.useEffect(()=>{const p=h();return!p||!p.isEnabled?void 0:(c(),p.subscribeToSlot(e,()=>{c(),f({})}))},[e,h,c]);const i=k.useCallback(()=>{const p=window.G7Core;return p?p.getDynamicRenderer?.():null},[]);if(s.length===0)return r?d.jsx(W,{className:t,style:n,id:o,children:r}):a?d.jsx(W,{className:t,style:n,id:o,children:a}):null;const u=i(),g=window.G7Core;return!u||!g?(rd.error("G7Core or DynamicRenderer not available"),null):d.jsxs(W,{className:t,style:n,id:o,children:[s.map(p=>{const{componentDef:m,dataContext:w,parentFormContext:C,getParentComponentContext:O,translationContext:v}=p,E=Ja(m,o);return d.jsx(u,{componentDef:E,dataContext:w,translationContext:v,registry:g.getComponentRegistry(),bindingEngine:g.getDataBindingEngine(),translationEngine:g.getTranslationEngine(),actionDispatcher:g.getActionDispatcher(),parentComponentContext:O?.(),parentFormContextProp:C,isRootRenderer:!1},E.id||`slot-${e}-${p.order}`)}),a]})},nd=Object.freeze(Object.defineProperty({__proto__:null,SlotContainer:pn,default:pn,scopeSlotChildDef:Ja},Symbol.toStringTag,{value:"Module"})),mn=({notifications:e=[],unreadCount:t=0,hasMore:r=!1,loading:n=!1,titleText:o="알림",emptyText:a="알림이 없습니다.",markAllReadText:s="모두 읽음",deleteAllText:l="모두 삭제",unreadOnlyText:f="안 읽은 알림만",unreadOnly:h=!1,onNotificationClick:c,onClose:i,onLoadMore:u,onMarkAllRead:g,onDeleteAll:p,onDelete:m,onUnreadOnlyToggle:w,dropdownAlign:C="right",className:O="",id:v,editorAttrs:E})=>{const[I,_]=k.useState(!1),M=k.useRef(null),x=k.useRef(null),y=k.useRef(null),T=k.useRef(null),A=k.useRef(new Set),P=k.useRef(u),D=k.useRef(r),L=k.useRef(n),U=k.useRef(e.length),S=k.useRef(-1);k.useEffect(()=>{P.current=u,D.current=r,L.current=n,U.current=e.length}),k.useEffect(()=>{if(!T.current||!y.current||!I)return;S.current=-1;const G=new IntersectionObserver(([Y])=>{Y.isIntersecting&&(L.current||!D.current||S.current!==U.current&&(S.current=U.current,P.current?.()))},{root:y.current,threshold:.1});return G.observe(T.current),()=>G.disconnect()},[I]),k.useEffect(()=>{if(!y.current||!I)return;const G=new IntersectionObserver(B=>{B.forEach(K=>{const j=K.target.getAttribute("data-notification-id"),H=K.target.getAttribute("data-unread")==="true";j&&H&&K.isIntersecting&&A.current.add(j)})},{root:y.current,threshold:.5});return y.current.querySelectorAll("[data-notification-id]").forEach(B=>G.observe(B)),()=>G.disconnect()},[I,e]);const b=t>0?t:e.filter(G=>!G.read).length,F=k.useCallback(()=>{if(I){if(b>0){const G=Array.from(A.current).filter(Y=>{const B=e.find(K=>String(K.id)===String(Y));return B&&!B.read});G.length>0&&i?.(G)}A.current.clear(),_(!1)}},[I,i,b,e]);k.useEffect(()=>{if(!I)return;const G=Y=>{M.current&&!M.current.contains(Y.target)&&F()};return document.addEventListener("mousedown",G),()=>document.removeEventListener("mousedown",G)},[I,F]);const $=k.useCallback(()=>{I?F():_(!0)},[I,F]);return k.useLayoutEffect(()=>{const G=x.current;if(!I||!G)return;G.style.transform="";const Y=G.getBoundingClientRect(),B=typeof window<"u"?window.innerWidth:0;if(Y.width===0&&Y.height===0)return;const K=8;let j=0;Y.right>B-K?j=-(Y.right-(B-K)):Y.left<K&&(j=K-Y.left),j!==0&&(G.style.transform=`translateX(${j}px)`)},[I,C,e.length]),d.jsxs(W,{ref:M,className:`relative ${O}`,id:v,...E,children:[d.jsxs(Q,{onClick:$,className:"relative p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors cursor-pointer","aria-label":o,children:[d.jsx(de,{name:Je.Bell,className:"w-5 h-5"}),b>0&&d.jsx(R,{className:"absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center text-xs font-bold text-white bg-red-500 dark:bg-red-600 rounded-full",children:b>99?"99+":b})]}),I&&d.jsxs(W,{ref:x,className:`absolute ${C==="left"?"left-0":"right-0"} mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[d.jsxs(W,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between gap-2",children:[d.jsxs(W,{className:"flex items-center gap-3 min-w-0",children:[d.jsx(R,{className:"font-semibold text-gray-900 dark:text-white",children:o}),w&&d.jsxs(W,{className:"flex items-center gap-1.5 cursor-pointer select-none",onClick:()=>w(!h),children:[d.jsx(xt,{type:"checkbox",checked:h,onChange:G=>{G.stopPropagation(),w(G.target.checked)},onClick:G=>G.stopPropagation(),className:"w-3.5 h-3.5 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 cursor-pointer"}),d.jsx(R,{className:"text-xs text-gray-600 dark:text-gray-400",children:f})]})]}),d.jsxs(W,{className:"flex items-center gap-3 flex-shrink-0",children:[g&&b>0&&d.jsx(Q,{onClick:()=>{g(),A.current.clear()},className:"text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline cursor-pointer",children:s}),p&&e.length>0&&d.jsx(Q,{onClick:()=>{p()},className:"text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:underline cursor-pointer",children:l})]})]}),d.jsx(W,{ref:y,className:"max-h-96 overflow-y-auto",children:e.length===0&&!n?d.jsx(W,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:d.jsx(R,{children:a})}):d.jsxs(d.Fragment,{children:[e.map(G=>{const Y=!G.read;return d.jsxs(W,{"data-notification-id":G.id,"data-unread":Y?"true":"false",onClick:()=>{G.onClick?.(),c?.(G),_(!1)},className:`group flex items-start gap-3 px-4 py-3 text-left border-b border-gray-100 dark:border-gray-700 transition-colors cursor-pointer ${Y?"bg-blue-50/70 dark:bg-blue-900/20 hover:bg-blue-50 dark:hover:bg-blue-900/30":"bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 opacity-70"}`,children:[G.iconName&&d.jsx(de,{name:G.iconName,className:`w-5 h-5 mt-0.5 flex-shrink-0 ${Y?"text-blue-600 dark:text-blue-400":"text-gray-400 dark:text-gray-500"}`}),d.jsxs(W,{className:"flex-1 min-w-0",children:[d.jsx(W,{className:`text-sm truncate ${Y?"font-semibold text-gray-900 dark:text-white":"font-normal text-gray-600 dark:text-gray-400"}`,children:G.title}),d.jsx(W,{className:`text-sm mt-1 line-clamp-2 ${Y?"text-gray-700 dark:text-gray-300":"text-gray-500 dark:text-gray-500"}`,children:G.message}),d.jsx(W,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1 font-mono",children:G.time})]}),d.jsxs(W,{className:"flex flex-col items-center gap-2 flex-shrink-0",children:[Y&&d.jsx(R,{className:"w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full mt-1.5"}),m&&d.jsx(Q,{onClick:B=>{B.stopPropagation(),m(G)},className:"p-1 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity","aria-label":"Delete notification",children:d.jsx(de,{name:Je.Trash,className:"w-4 h-4"})})]})]},G.id)}),r&&d.jsx(W,{ref:T,className:"px-4 py-3 text-center",children:n?d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."}):d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500"})}),n&&!r&&e.length>0&&d.jsx(W,{className:"px-4 py-2 text-center",children:d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."})})]})})]})]})},od=Object.freeze(Object.defineProperty({__proto__:null,NotificationCenter:mn,default:mn},Symbol.toStringTag,{value:"Module"})),Xe=(e,t)=>window.G7Core?.t?.(e,t)??e,pt=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},Ra=({logo:e,siteName:t="그누보드7",user:r,cartCount:n=0,notificationCount:o=0,boards:a=[],maxVisibleBoards:s=5,onMobileMenuOpen:l,availableLocales:f=[],currentLocale:h="ko",shopBase:c="/shop",className:i="",editorAttrs:u,notifications:g=[],notificationHasMore:p=!1,notificationLoading:m=!1,notificationUnreadOnly:w=!1,notificationTitleText:C,notificationEmptyText:O,notificationMarkAllReadText:v,notificationDeleteAllText:E,notificationUnreadOnlyText:I,onNotificationClose:_,onNotificationClick:M,onNotificationLoadMore:x,onNotificationMarkAllRead:y,onNotificationDeleteAll:T,onNotificationDelete:A,onNotificationUnreadOnlyToggle:P})=>{const[D,L]=k.useState(""),[U,S]=k.useState(!1),[b,F]=k.useState(!1),[$,G]=k.useState(!1),[Y,B]=k.useState({top:0,left:0}),[K,j]=k.useState(window.location.pathname),H=k.useRef(null),q=k.useRef(null),ue=k.useRef(null),fe=window.G7Core?.useResponsive,ee=fe?.(),ke=ee?ee.width<768:typeof window<"u"&&window.innerWidth<768;k.useEffect(()=>{const V=()=>j(window.location.pathname);window.addEventListener("popstate",V);const re=history.pushState;return history.pushState=function(...oe){re.apply(this,oe),j(window.location.pathname)},()=>{window.removeEventListener("popstate",V),history.pushState=re}},[]);const me=(V,re=!1)=>re?K===V:K===V||K.startsWith(V+"/"),Ge=()=>{const V=c==="/"?"":c;return K.startsWith(`${V}/products`)||K.startsWith(`${V}/cart`)||K.startsWith(`${V}/checkout`)||K.startsWith(`${V}/category`)},De=V=>{const re="px-3 py-2 text-sm font-medium whitespace-nowrap cursor-pointer rounded-lg transition-colors";return V?`${re} bg-gray-900 text-white dark:bg-white dark:text-gray-900`:`${re} text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800`};k.useEffect(()=>{const V=re=>{H.current&&!H.current.contains(re.target)&&S(!1),q.current&&!q.current.contains(re.target)&&F(!1),ue.current&&!ue.current.contains(re.target)&&G(!1)};return document.addEventListener("mousedown",V),()=>document.removeEventListener("mousedown",V)},[]);const ye=a.slice(0,s),Me=a.slice(s),Ce=V=>{V.preventDefault(),D.trim()&&pt(`/search?q=${encodeURIComponent(D)}`)},we=async()=>{window.G7Core?.dispatch?.({handler:"logout"})},Fe=V=>{window.G7Core?.dispatch?.({handler:"setLocale",target:V}),G(!1)},Le=V=>window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[V]??(V==="ko"?"한국어":V==="en"?"English":V==="ja"?"日本語":V==="zh"?"中文":V==="es"?"Español":V==="fr"?"Français":V==="de"?"Deutsch":V.toUpperCase());return d.jsxs(Zc,{...u??{},className:`sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 ${i}`,children:[d.jsx(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs(W,{className:"flex items-center justify-between h-16",children:[d.jsx(Q,{onClick:()=>pt("/"),className:"flex items-center gap-2 flex-shrink-0 cursor-pointer",children:e?d.jsx(Lt,{src:e,alt:t,className:"h-8"}):d.jsx(R,{className:"text-xl font-bold text-gray-900 dark:text-white",children:t})}),!ke&&d.jsx(so,{onSubmit:Ce,className:"flex flex-1 max-w-lg mx-8",children:d.jsxs(W,{className:"relative w-full",children:[d.jsx(xt,{type:"text",value:D,onChange:V=>L(V.target.value),placeholder:Xe("common.search_placeholder"),className:"w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"}),d.jsx(de,{name:"search",className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500"})]})}),d.jsxs(W,{className:"flex items-center gap-2",children:[d.jsx(hn,{autoText:Xe("common.theme.auto"),lightText:Xe("common.theme.light"),darkText:Xe("common.theme.dark")}),r?.uuid&&d.jsx(mn,{notifications:g,unreadCount:o,hasMore:p,loading:m,unreadOnly:w,titleText:C??Xe("mypage.notifications.title"),emptyText:O??Xe("mypage.notifications.empty"),markAllReadText:v??Xe("mypage.notifications.mark_all_read"),deleteAllText:E??Xe("mypage.notifications.delete_all"),unreadOnlyText:I??Xe("mypage.notifications.unread_only"),onClose:_,onNotificationClick:M,onLoadMore:x,onMarkAllRead:y,onDeleteAll:T,onDelete:A,onUnreadOnlyToggle:P,dropdownAlign:"right"}),d.jsxs(Q,{onClick:()=>pt(`${c}/cart`),className:"relative p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:[d.jsx(de,{name:"shopping-cart",className:"w-5 h-5"}),n>0&&d.jsx(R,{className:"absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center text-xs bg-blue-500 text-white rounded-full",children:n>99?"99+":n})]}),f&&f.length>1&&d.jsxs(W,{ref:ue,className:"relative",children:[d.jsxs(Q,{onClick:()=>G(!$),className:"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg cursor-pointer transition-colors","aria-haspopup":"listbox","aria-expanded":$,"aria-label":Xe("common.language"),children:[d.jsx(de,{name:"globe",className:"w-4 h-4"}),d.jsx(R,{className:"font-medium uppercase",children:h}),d.jsx(de,{name:"chevron-down",className:"w-3 h-3"})]}),$&&d.jsx(W,{role:"listbox",className:"absolute right-0 mt-2 w-44 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50 overflow-hidden",children:f.map(V=>{const re=V===h;return d.jsxs(Q,{role:"option","aria-selected":re,onClick:()=>Fe(V),className:`w-full px-4 py-2.5 text-left text-sm flex items-center gap-3 cursor-pointer transition-colors ${re?"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400":"text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"}`,children:[d.jsx(de,{name:"globe",className:"w-4 h-4"}),d.jsx(R,{className:"flex-1 font-medium",children:Le(V)}),re&&d.jsx(de,{name:"check",className:"w-4 h-4"})]},V)})})]}),d.jsx(pn,{slotId:"header_currency",id:"header_currency_slot_desktop",className:"flex items-center"}),r?.uuid?d.jsxs(W,{ref:H,className:"relative",children:[d.jsxs(Q,{onClick:()=>S(!U),className:"flex items-center gap-2 p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",children:[d.jsx(Ur,{avatar:r.avatar,name:r.name,size:"sm"}),d.jsx(R,{className:"hidden sm:inline text-sm font-medium",children:r.name}),d.jsx(de,{name:"chevron-down",className:"w-4 h-4"})]}),U&&d.jsxs(W,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50",children:[d.jsx(W,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:d.jsxs(W,{className:"flex items-center gap-3",children:[d.jsx(Ur,{avatar:r.avatar,name:r.name,size:"md"}),d.jsxs(W,{children:[d.jsx(W,{className:"text-sm font-medium text-gray-900 dark:text-white",children:r.name}),d.jsx(W,{className:"text-xs text-gray-500 dark:text-gray-400",children:Xe("common.member")})]})]})}),d.jsxs(W,{className:"py-1",children:[r.is_admin&&d.jsxs(d.Fragment,{children:[d.jsxs(ao,{href:"/admin",className:"block w-full text-left px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer font-medium",children:[d.jsx(de,{name:"settings",className:"inline w-4 h-4 mr-2"}),Xe("common.admin_menu")]}),d.jsx(lo,{className:"my-1 border-gray-200 dark:border-gray-700"})]}),d.jsxs(Q,{onClick:()=>{pt("/mypage"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"user",className:"inline w-4 h-4 mr-2"}),Xe("common.mypage")]}),d.jsxs(Q,{onClick:()=>{pt("/mypage/orders"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"shopping-bag",className:"inline w-4 h-4 mr-2"}),Xe("mypage.tabs.orders")]}),d.jsxs(Q,{onClick:()=>{pt("/mypage/wishlist"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"heart",className:"inline w-4 h-4 mr-2"}),Xe("mypage.tabs.wishlist")]})]}),d.jsx(lo,{className:"my-1 border-gray-200 dark:border-gray-700"}),d.jsxs(Q,{onClick:we,className:"block w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"log-out",className:"inline w-4 h-4 mr-2"}),Xe("auth.logout")]})]})]}):d.jsxs(W,{className:"flex items-center gap-1.5",children:[d.jsx(Q,{onClick:()=>pt("/login?redirect=/mypage/orders"),className:"px-2.5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:Xe("shop.guest_order_form.nav_link")}),d.jsx(Q,{onClick:()=>pt("/login"),className:"px-2.5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:Xe("auth.login")}),d.jsx(Q,{onClick:()=>pt("/register"),className:"ml-1 px-3 py-2 text-sm font-medium text-white bg-emerald-500 rounded-lg hover:bg-emerald-600 cursor-pointer",children:Xe("auth.register_link")})]}),ke&&d.jsx(Q,{onClick:l,className:"p-2 text-gray-600 dark:text-gray-400",children:d.jsx(de,{name:"menu",className:"w-6 h-6"})})]})]})}),!ke&&d.jsx(Fr,{className:"border-t border-gray-200 dark:border-gray-800",children:d.jsx(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs(W,{className:"flex items-center gap-1 h-12 overflow-x-auto",children:[d.jsx(Q,{onClick:()=>pt("/"),className:De(me("/",!0)),"data-testid":"nav-home",children:Xe("nav.home")}),d.jsxs(Q,{onClick:()=>pt("/boards/popular"),className:`flex items-center gap-1 ${De(me("/boards/popular"))}`,"data-testid":"nav-popular",children:[d.jsx(R,{className:"text-orange-500",children:"🔥"}),Xe("nav.popular")]}),d.jsxs(Q,{onClick:()=>pt(`${c==="/"?"":c}/products`),className:`flex items-center gap-1 ${De(Ge())}`,"data-testid":"nav-shop",children:[d.jsx(R,{children:"🛒"}),Xe("nav.shop")]}),ye.map(V=>d.jsxs(Q,{onClick:()=>pt(`/board/${V.slug}`),className:De(me(`/board/${V.slug}`)),children:[V.icon&&d.jsx(R,{className:"mr-1",children:V.icon}),V.name]},V.id)),Me.length>0&&d.jsxs(W,{ref:q,className:"relative",children:[d.jsxs(Q,{onClick:()=>{if(!b&&q.current){const V=q.current.getBoundingClientRect();B({top:V.bottom+4,left:V.left})}F(!b)},className:"flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white",children:[Xe("nav.more"),d.jsx(de,{name:"chevron-down",className:"w-4 h-4"})]}),b&&d.jsx(W,{className:"fixed w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50",style:{top:`${Y.top}px`,left:`${Y.left}px`},children:Me.map(V=>{const re=me(`/board/${V.slug}`);return d.jsxs(Q,{onClick:()=>{pt(`/board/${V.slug}`),F(!1)},className:`block w-full text-left px-4 py-2 text-sm cursor-pointer ${re?"bg-gray-900 text-white dark:bg-white dark:text-gray-900":"text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"}`,children:[V.icon&&d.jsx(R,{className:"mr-1",children:V.icon}),V.name]},V.id)})})]})]})})})]})},ad=Object.freeze(Object.defineProperty({__proto__:null,default:Ra},Symbol.toStringTag,{value:"Module"})),sd=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("footer",{ref:n,className:t,...r,children:e})),kt=(e,t)=>window.G7Core?.t?.(e,t)??e,ld=768,es=1024,id=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},ts=({siteName:e="그누보드7",siteDescription:t,copyrightText:r,socialLinks:n={},linkGroups:o,className:a="",editorAttrs:s})=>{const l=new Date().getFullYear(),h=window.G7Core?.useResponsive,i=h?.()?.width??(typeof window<"u"?window.innerWidth:es),u=i<ld,g=i>=es,p=u?1:g?5:2,m=[{title:kt("footer.community"),links:[{label:kt("nav.home"),href:"/"},{label:kt("nav.popular"),href:"/boards/popular"},{label:kt("footer.all_boards"),href:"/boards"}]},{title:kt("footer.info"),links:[{label:kt("footer.about"),href:"/page/about"},{label:kt("footer.faq"),href:"/page/faq"},{label:kt("footer.contact"),href:"/page/contact"}]},{title:kt("footer.policy"),links:[{label:kt("footer.terms"),href:"/page/terms"},{label:kt("footer.privacy"),href:"/page/privacy"},{label:kt("footer.refund"),href:"/page/refund"}]}],w=o||m,C={github:"github",twitter:"twitter",discord:"discord",facebook:"facebook",instagram:"instagram"};return d.jsx(sd,{...s??{},className:`bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 ${a}`,children:d.jsxs(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12",children:[d.jsxs(W,{className:"grid gap-8",style:{gridTemplateColumns:`repeat(${p}, minmax(0, 1fr))`},children:[d.jsxs(W,{style:g?{gridColumn:"span 2 / span 2"}:void 0,children:[d.jsx(gn,{className:"text-lg font-bold text-gray-900 dark:text-white",children:e}),t&&d.jsx(Dt,{className:"mt-2 text-sm text-gray-600 dark:text-gray-400",children:t}),d.jsx(W,{className:"mt-4 flex items-center gap-4",children:Object.entries(n).map(([O,v])=>v?d.jsx(ao,{href:v,target:"_blank",rel:"noopener noreferrer",className:"text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200","aria-label":O,children:d.jsx(de,{name:C[O],className:"w-5 h-5"})},O):null)})]}),w.map((O,v)=>d.jsxs(W,{children:[d.jsx(Ka,{className:"text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider",children:O.title}),d.jsx(oo,{className:"mt-4 space-y-2",children:O.links.map((E,I)=>d.jsx(Xt,{children:d.jsx(Q,{onClick:()=>id(E.href),className:"text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:E.label})},I))})]},v))]}),d.jsxs(W,{className:"mt-8 pt-8 border-t border-gray-200 dark:border-gray-800 flex justify-between items-center gap-4",style:{flexDirection:u?"column":"row"},children:[d.jsx(Dt,{className:"text-sm text-gray-500 dark:text-gray-400",children:r||`© ${l} ${e}. All rights reserved.`}),d.jsx(Dt,{className:"text-sm text-gray-500 dark:text-gray-400",children:kt("footer.powered_by")})]})]})})},cd=Object.freeze(Object.defineProperty({__proto__:null,default:ts},Symbol.toStringTag,{value:"Module"})),Pt=(e,t)=>window.G7Core?.t?.(e,t)??e,Tt=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},rs=({isOpen:e,onClose:t,logo:r,siteName:n="그누보드7",user:o,boards:a=[],cartCount:s=0})=>{const l=k.useRef(null),h=window.G7Core?.useResponsive,c=h?.(),i=c?c.width<1024:typeof window<"u"&&window.innerWidth<1024;k.useEffect(()=>{const g=p=>{p.key==="Escape"&&t()};return e&&(document.addEventListener("keydown",g),document.body.style.overflow="hidden"),()=>{document.removeEventListener("keydown",g),document.body.style.overflow=""}},[e,t]);const u=g=>{g.target===g.currentTarget&&t()};return!e||!i?null:d.jsxs(W,{className:"fixed inset-0 z-50",onClick:u,children:[d.jsx(W,{className:"fixed inset-0 bg-black/50 transition-opacity"}),d.jsxs(W,{ref:l,className:`fixed inset-y-0 left-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-xl transform transition-transform duration-300 ${e?"translate-x-0":"-translate-x-full"}`,children:[d.jsxs(W,{className:"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/")},className:"flex items-center gap-2 cursor-pointer",children:r?d.jsx(Lt,{src:r,alt:n,className:"h-8"}):d.jsx(R,{className:"text-xl font-bold text-gray-900 dark:text-white",children:n})}),d.jsx(Q,{onClick:t,className:"p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200","aria-label":"메뉴 닫기",children:d.jsx(de,{name:"x",className:"w-6 h-6"})})]}),o?d.jsx(W,{className:"p-4 border-b border-gray-200 dark:border-gray-800",children:d.jsxs(W,{className:"flex items-center gap-3",children:[o.avatar?d.jsx(Lt,{src:o.avatar,alt:o.name,className:"w-12 h-12 rounded-full"}):d.jsx(W,{className:"w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center text-white text-lg font-medium",children:(o.name||"U").charAt(0).toUpperCase()}),d.jsxs(W,{children:[d.jsx(Dt,{className:"font-medium text-gray-900 dark:text-white",children:o.name}),d.jsx(Q,{onClick:()=>{t(),Tt("/mypage")},className:"text-sm text-blue-600 dark:text-blue-400 hover:underline cursor-pointer",children:Pt("common.mypage")})]})]})}):d.jsx(W,{className:"p-4 border-b border-gray-200 dark:border-gray-800",children:d.jsxs(W,{className:"flex gap-2",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/login")},className:"flex-1 py-2 text-center text-sm font-medium text-white bg-gray-900 dark:bg-white dark:text-gray-900 rounded-lg cursor-pointer",children:Pt("auth.login")}),d.jsx(Q,{onClick:()=>{t(),Tt("/register")},className:"flex-1 py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer",children:Pt("auth.register")})]})}),d.jsx(Fr,{className:"p-4 overflow-y-auto max-h-[calc(100vh-200px)]",children:d.jsxs(oo,{className:"space-y-1",children:[d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(de,{name:"home",className:"w-5 h-5"}),Pt("nav.home")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/popular")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(R,{className:"text-orange-500",children:"🔥"}),Pt("nav.popular")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/shop")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(R,{children:"🛒"}),Pt("nav.shop")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/cart")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(de,{name:"shopping-cart",className:"w-5 h-5"}),Pt("nav.cart"),s>0&&d.jsx(R,{className:"ml-auto px-2 py-0.5 text-xs bg-blue-500 text-white rounded-full",children:s})]})}),d.jsx(Xt,{className:"my-4 border-t border-gray-200 dark:border-gray-800"}),d.jsx(Xt,{className:"px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider",children:Pt("nav.boards")}),a.map(g=>d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt(`/board/${g.slug}`)},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[g.icon&&d.jsx(R,{children:g.icon}),g.name]})},g.id))]})}),d.jsx(W,{className:"absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900",children:d.jsxs(W,{className:"flex items-center justify-between text-sm text-gray-500 dark:text-gray-400",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/about")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.about")}),d.jsx(Q,{onClick:()=>{t(),Tt("/terms")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.terms")}),d.jsx(Q,{onClick:()=>{t(),Tt("/privacy")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.privacy")})]})})]})]})},dd=Object.freeze(Object.defineProperty({__proto__:null,default:rs},Symbol.toStringTag,{value:"Module"}));const{entries:ns,setPrototypeOf:os,isFrozen:ud,getPrototypeOf:fd,getOwnPropertyDescriptor:gd}=Object;let{freeze:ft,seal:Nt,create:uo}=Object,{apply:fo,construct:go}=typeof Reflect<"u"&&Reflect;ft||(ft=function(t){return t}),Nt||(Nt=function(t){return t}),fo||(fo=function(t,r){for(var n=arguments.length,o=new Array(n>2?n-2:0),a=2;a<n;a++)o[a-2]=arguments[a];return t.apply(r,o)}),go||(go=function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return new t(...n)});const bn=ht(Array.prototype.forEach),hd=ht(Array.prototype.lastIndexOf),as=ht(Array.prototype.pop),zr=ht(Array.prototype.push),pd=ht(Array.prototype.splice),yn=ht(String.prototype.toLowerCase),ho=ht(String.prototype.toString),po=ht(String.prototype.match),Hr=ht(String.prototype.replace),md=ht(String.prototype.indexOf),bd=ht(String.prototype.trim),$t=ht(Object.prototype.hasOwnProperty),gt=ht(RegExp.prototype.test),Gr=yd(TypeError);function ht(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return fo(e,t,n)}}function yd(e){return function(){for(var t=arguments.length,r=new Array(t),n=0;n<t;n++)r[n]=arguments[n];return go(e,r)}}function Ae(e,t){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:yn;os&&os(e,null);let n=t.length;for(;n--;){let o=t[n];if(typeof o=="string"){const a=r(o);a!==o&&(ud(t)||(t[n]=a),o=a)}e[o]=!0}return e}function vd(e){for(let t=0;t<e.length;t++)$t(e,t)||(e[t]=null);return e}function Vt(e){const t=uo(null);for(const[r,n]of ns(e))$t(e,r)&&(Array.isArray(n)?t[r]=vd(n):n&&typeof n=="object"&&n.constructor===Object?t[r]=Vt(n):t[r]=n);return t}function jr(e,t){for(;e!==null;){const n=gd(e,t);if(n){if(n.get)return ht(n.get);if(typeof n.value=="function")return ht(n.value)}e=fd(e)}function r(){return null}return r}const ss=ft(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),mo=ft(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),bo=ft(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),wd=ft(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),yo=ft(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),xd=ft(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),ls=ft(["#text"]),is=ft(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),vo=ft(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),cs=ft(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),vn=ft(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),kd=Nt(/\{\{[\w\W]*|[\w\W]*\}\}/gm),Cd=Nt(/<%[\w\W]*|[\w\W]*%>/gm),Sd=Nt(/\$\{[\w\W]*/gm),_d=Nt(/^data-[\-\w.\u00B7-\uFFFF]+$/),Ed=Nt(/^aria-[\-\w]+$/),ds=Nt(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Td=Nt(/^(?:\w+script|data):/i),Nd=Nt(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),us=Nt(/^html$/i),Ad=Nt(/^[a-z][.\w]*(-[.\w]+)+$/i);var fs=Object.freeze({__proto__:null,ARIA_ATTR:Ed,ATTR_WHITESPACE:Nd,CUSTOM_ELEMENT:Ad,DATA_ATTR:_d,DOCTYPE_NAME:us,ERB_EXPR:Cd,IS_ALLOWED_URI:ds,IS_SCRIPT_OR_DATA:Td,MUSTACHE_EXPR:kd,TMPLIT_EXPR:Sd});const Br={element:1,text:3,progressingInstruction:7,comment:8,document:9},Md=function(){return typeof window>"u"?null:window},Od=function(t,r){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let n=null;const o="data-tt-policy-suffix";r&&r.hasAttribute(o)&&(n=r.getAttribute(o));const a="dompurify"+(n?"#"+n:"");try{return t.createPolicy(a,{createHTML(s){return s},createScriptURL(s){return s}})}catch{return console.warn("TrustedTypes policy "+a+" could not be created."),null}},gs=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function hs(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Md();const t=ge=>hs(ge);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==Br.document||!e.Element)return t.isSupported=!1,t;let{document:r}=e;const n=r,o=n.currentScript,{DocumentFragment:a,HTMLTemplateElement:s,Node:l,Element:f,NodeFilter:h,NamedNodeMap:c=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:i,DOMParser:u,trustedTypes:g}=e,p=f.prototype,m=jr(p,"cloneNode"),w=jr(p,"remove"),C=jr(p,"nextSibling"),O=jr(p,"childNodes"),v=jr(p,"parentNode");if(typeof s=="function"){const ge=r.createElement("template");ge.content&&ge.content.ownerDocument&&(r=ge.content.ownerDocument)}let E,I="";const{implementation:_,createNodeIterator:M,createDocumentFragment:x,getElementsByTagName:y}=r,{importNode:T}=n;let A=gs();t.isSupported=typeof ns=="function"&&typeof v=="function"&&_&&_.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:P,ERB_EXPR:D,TMPLIT_EXPR:L,DATA_ATTR:U,ARIA_ATTR:S,IS_SCRIPT_OR_DATA:b,ATTR_WHITESPACE:F,CUSTOM_ELEMENT:$}=fs;let{IS_ALLOWED_URI:G}=fs,Y=null;const B=Ae({},[...ss,...mo,...bo,...yo,...ls]);let K=null;const j=Ae({},[...is,...vo,...cs,...vn]);let H=Object.seal(uo(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),q=null,ue=null;const te=Object.seal(uo(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let fe=!0,ee=!0,ke=!1,me=!0,Ge=!1,De=!0,ye=!1,Me=!1,Ce=!1,we=!1,Fe=!1,Le=!1,V=!0,re=!1;const oe="user-content-";let ae=!0,be=!1,xe={},He=null;const Pe=Ae({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Oe=Ae({},["audio","video","img","source","image","track"]);let X=null;const se=Ae({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ie="http://www.w3.org/1998/Math/MathML",le="http://www.w3.org/2000/svg",ne="http://www.w3.org/1999/xhtml";let Te=ne,ve=!1,Se=null;const _e=Ae({},[ie,le,ne],ho);let Ue=Ae({},["mi","mo","mn","ms","mtext"]),je=Ae({},["annotation-xml"]);const Ee=Ae({},["title","style","font","a","script"]);let he=null;const Ne=["application/xhtml+xml","text/html"],Ke="text/html";let $e=null,Be=null;const Ze=r.createElement("form"),qt=function(z){return z instanceof RegExp||z instanceof Function},qe=function(){let z=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(Be&&Be===z)){if((!z||typeof z!="object")&&(z={}),z=Vt(z),he=Ne.indexOf(z.PARSER_MEDIA_TYPE)===-1?Ke:z.PARSER_MEDIA_TYPE,$e=he==="application/xhtml+xml"?ho:yn,Y=$t(z,"ALLOWED_TAGS")?Ae({},z.ALLOWED_TAGS,$e):B,K=$t(z,"ALLOWED_ATTR")?Ae({},z.ALLOWED_ATTR,$e):j,Se=$t(z,"ALLOWED_NAMESPACES")?Ae({},z.ALLOWED_NAMESPACES,ho):_e,X=$t(z,"ADD_URI_SAFE_ATTR")?Ae(Vt(se),z.ADD_URI_SAFE_ATTR,$e):se,Ve=$t(z,"ADD_DATA_URI_TAGS")?Ae(Vt(Oe),z.ADD_DATA_URI_TAGS,$e):Oe,He=$t(z,"FORBID_CONTENTS")?Ae({},z.FORBID_CONTENTS,$e):Pe,q=$t(z,"FORBID_TAGS")?Ae({},z.FORBID_TAGS,$e):Vt({}),ue=$t(z,"FORBID_ATTR")?Ae({},z.FORBID_ATTR,$e):Vt({}),xe=$t(z,"USE_PROFILES")?z.USE_PROFILES:!1,fe=z.ALLOW_ARIA_ATTR!==!1,ee=z.ALLOW_DATA_ATTR!==!1,ke=z.ALLOW_UNKNOWN_PROTOCOLS||!1,me=z.ALLOW_SELF_CLOSE_IN_ATTR!==!1,Ge=z.SAFE_FOR_TEMPLATES||!1,De=z.SAFE_FOR_XML!==!1,ye=z.WHOLE_DOCUMENT||!1,we=z.RETURN_DOM||!1,Fe=z.RETURN_DOM_FRAGMENT||!1,Le=z.RETURN_TRUSTED_TYPE||!1,Ce=z.FORCE_BODY||!1,V=z.SANITIZE_DOM!==!1,re=z.SANITIZE_NAMED_PROPS||!1,ae=z.KEEP_CONTENT!==!1,be=z.IN_PLACE||!1,G=z.ALLOWED_URI_REGEXP||ds,Te=z.NAMESPACE||ne,Ue=z.MATHML_TEXT_INTEGRATION_POINTS||Ue,je=z.HTML_INTEGRATION_POINTS||je,H=z.CUSTOM_ELEMENT_HANDLING||{},z.CUSTOM_ELEMENT_HANDLING&&qt(z.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(H.tagNameCheck=z.CUSTOM_ELEMENT_HANDLING.tagNameCheck),z.CUSTOM_ELEMENT_HANDLING&&qt(z.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(H.attributeNameCheck=z.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),z.CUSTOM_ELEMENT_HANDLING&&typeof z.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(H.allowCustomizedBuiltInElements=z.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ge&&(ee=!1),Fe&&(we=!0),xe&&(Y=Ae({},ls),K=[],xe.html===!0&&(Ae(Y,ss),Ae(K,is)),xe.svg===!0&&(Ae(Y,mo),Ae(K,vo),Ae(K,vn)),xe.svgFilters===!0&&(Ae(Y,bo),Ae(K,vo),Ae(K,vn)),xe.mathMl===!0&&(Ae(Y,yo),Ae(K,cs),Ae(K,vn))),z.ADD_TAGS&&(typeof z.ADD_TAGS=="function"?te.tagCheck=z.ADD_TAGS:(Y===B&&(Y=Vt(Y)),Ae(Y,z.ADD_TAGS,$e))),z.ADD_ATTR&&(typeof z.ADD_ATTR=="function"?te.attributeCheck=z.ADD_ATTR:(K===j&&(K=Vt(K)),Ae(K,z.ADD_ATTR,$e))),z.ADD_URI_SAFE_ATTR&&Ae(X,z.ADD_URI_SAFE_ATTR,$e),z.FORBID_CONTENTS&&(He===Pe&&(He=Vt(He)),Ae(He,z.FORBID_CONTENTS,$e)),z.ADD_FORBID_CONTENTS&&(He===Pe&&(He=Vt(He)),Ae(He,z.ADD_FORBID_CONTENTS,$e)),ae&&(Y["#text"]=!0),ye&&Ae(Y,["html","head","body"]),Y.table&&(Ae(Y,["tbody"]),delete q.tbody),z.TRUSTED_TYPES_POLICY){if(typeof z.TRUSTED_TYPES_POLICY.createHTML!="function")throw Gr('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof z.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Gr('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');E=z.TRUSTED_TYPES_POLICY,I=E.createHTML("")}else E===void 0&&(E=Od(g,o)),E!==null&&typeof I=="string"&&(I=E.createHTML(""));ft&&ft(z),Be=z}},jt=Ae({},[...mo,...bo,...wd]),_t=Ae({},[...yo,...xd]),Bt=function(z){let Z=v(z);(!Z||!Z.tagName)&&(Z={namespaceURI:Te,tagName:"template"});const ce=yn(z.tagName),Ye=yn(Z.tagName);return Se[z.namespaceURI]?z.namespaceURI===le?Z.namespaceURI===ne?ce==="svg":Z.namespaceURI===ie?ce==="svg"&&(Ye==="annotation-xml"||Ue[Ye]):!!jt[ce]:z.namespaceURI===ie?Z.namespaceURI===ne?ce==="math":Z.namespaceURI===le?ce==="math"&&je[Ye]:!!_t[ce]:z.namespaceURI===ne?Z.namespaceURI===le&&!je[Ye]||Z.namespaceURI===ie&&!Ue[Ye]?!1:!_t[ce]&&(Ee[ce]||!jt[ce]):!!(he==="application/xhtml+xml"&&Se[z.namespaceURI]):!1},dt=function(z){zr(t.removed,{element:z});try{v(z).removeChild(z)}catch{w(z)}},Et=function(z,Z){try{zr(t.removed,{attribute:Z.getAttributeNode(z),from:Z})}catch{zr(t.removed,{attribute:null,from:Z})}if(Z.removeAttribute(z),z==="is")if(we||Fe)try{dt(Z)}catch{}else try{Z.setAttribute(z,"")}catch{}},Wt=function(z){let Z=null,ce=null;if(Ce)z="<remove></remove>"+z;else{const Qe=po(z,/^[\r\n\t ]+/);ce=Qe&&Qe[0]}he==="application/xhtml+xml"&&Te===ne&&(z='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+z+"</body></html>");const Ye=E?E.createHTML(z):z;if(Te===ne)try{Z=new u().parseFromString(Ye,he)}catch{}if(!Z||!Z.documentElement){Z=_.createDocument(Te,"template",null);try{Z.documentElement.innerHTML=ve?I:Ye}catch{}}const ut=Z.body||Z.documentElement;return z&&ce&&ut.insertBefore(r.createTextNode(ce),ut.childNodes[0]||null),Te===ne?y.call(Z,ye?"html":"body")[0]:ye?Z.documentElement:ut},nr=function(z){return M.call(z.ownerDocument||z,z,h.SHOW_ELEMENT|h.SHOW_COMMENT|h.SHOW_TEXT|h.SHOW_PROCESSING_INSTRUCTION|h.SHOW_CDATA_SECTION,null)},mr=function(z){return z instanceof i&&(typeof z.nodeName!="string"||typeof z.textContent!="string"||typeof z.removeChild!="function"||!(z.attributes instanceof c)||typeof z.removeAttribute!="function"||typeof z.setAttribute!="function"||typeof z.namespaceURI!="string"||typeof z.insertBefore!="function"||typeof z.hasChildNodes!="function")},Ec=function(z){return typeof l=="function"&&z instanceof l};function Zt(ge,z,Z){bn(ge,ce=>{ce.call(t,z,Z,Be)})}const Tc=function(z){let Z=null;if(Zt(A.beforeSanitizeElements,z,null),mr(z))return dt(z),!0;const ce=$e(z.nodeName);if(Zt(A.uponSanitizeElement,z,{tagName:ce,allowedTags:Y}),De&&z.hasChildNodes()&&!Ec(z.firstElementChild)&>(/<[/\w!]/g,z.innerHTML)&>(/<[/\w!]/g,z.textContent)||z.nodeType===Br.progressingInstruction||De&&z.nodeType===Br.comment&>(/<[/\w]/g,z.data))return dt(z),!0;if(!(te.tagCheck instanceof Function&&te.tagCheck(ce))&&(!Y[ce]||q[ce])){if(!q[ce]&&Ac(ce)&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,ce)||H.tagNameCheck instanceof Function&&H.tagNameCheck(ce)))return!1;if(ae&&!He[ce]){const Ye=v(z)||z.parentNode,ut=O(z)||z.childNodes;if(ut&&Ye){const Qe=ut.length;for(let wt=Qe-1;wt>=0;--wt){const Qt=m(ut[wt],!0);Qt.__removalCount=(z.__removalCount||0)+1,Ye.insertBefore(Qt,C(z))}}}return dt(z),!0}return z instanceof f&&!Bt(z)||(ce==="noscript"||ce==="noembed"||ce==="noframes")&>(/<\/no(script|embed|frames)/i,z.innerHTML)?(dt(z),!0):(Ge&&z.nodeType===Br.text&&(Z=z.textContent,bn([P,D,L],Ye=>{Z=Hr(Z,Ye," ")}),z.textContent!==Z&&(zr(t.removed,{element:z.cloneNode()}),z.textContent=Z)),Zt(A.afterSanitizeElements,z,null),!1)},Nc=function(z,Z,ce){if(V&&(Z==="id"||Z==="name")&&(ce in r||ce in Ze))return!1;if(!(ee&&!ue[Z]&>(U,Z))){if(!(fe&>(S,Z))){if(!(te.attributeCheck instanceof Function&&te.attributeCheck(Z,z))){if(!K[Z]||ue[Z]){if(!(Ac(z)&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,z)||H.tagNameCheck instanceof Function&&H.tagNameCheck(z))&&(H.attributeNameCheck instanceof RegExp&>(H.attributeNameCheck,Z)||H.attributeNameCheck instanceof Function&&H.attributeNameCheck(Z,z))||Z==="is"&&H.allowCustomizedBuiltInElements&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,ce)||H.tagNameCheck instanceof Function&&H.tagNameCheck(ce))))return!1}else if(!X[Z]){if(!gt(G,Hr(ce,F,""))){if(!((Z==="src"||Z==="xlink:href"||Z==="href")&&z!=="script"&&md(ce,"data:")===0&&Ve[z])){if(!(ke&&!gt(b,Hr(ce,F,"")))){if(ce)return!1}}}}}}}return!0},Ac=function(z){return z!=="annotation-xml"&&po(z,$)},Mc=function(z){Zt(A.beforeSanitizeAttributes,z,null);const{attributes:Z}=z;if(!Z||mr(z))return;const ce={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:K,forceKeepAttr:void 0};let Ye=Z.length;for(;Ye--;){const ut=Z[Ye],{name:Qe,namespaceURI:wt,value:Qt}=ut,$r=$e(Qe),Ga=Qt;let st=Qe==="value"?Ga:bd(Ga);if(ce.attrName=$r,ce.attrValue=st,ce.keepAttr=!0,ce.forceKeepAttr=void 0,Zt(A.uponSanitizeAttribute,z,ce),st=ce.attrValue,re&&($r==="id"||$r==="name")&&(Et(Qe,z),st=oe+st),De&>(/((--!?|])>)|<\/(style|title|textarea)/i,st)){Et(Qe,z);continue}if($r==="attributename"&&po(st,"href")){Et(Qe,z);continue}if(ce.forceKeepAttr)continue;if(!ce.keepAttr){Et(Qe,z);continue}if(!me&>(/\/>/i,st)){Et(Qe,z);continue}Ge&&bn([P,D,L],Ic=>{st=Hr(st,Ic," ")});const Oc=$e(z.nodeName);if(!Nc(Oc,$r,st)){Et(Qe,z);continue}if(E&&typeof g=="object"&&typeof g.getAttributeType=="function"&&!wt)switch(g.getAttributeType(Oc,$r)){case"TrustedHTML":{st=E.createHTML(st);break}case"TrustedScriptURL":{st=E.createScriptURL(st);break}}if(st!==Ga)try{wt?z.setAttributeNS(wt,Qe,st):z.setAttribute(Qe,st),mr(z)?dt(z):as(t.removed)}catch{Et(Qe,z)}}Zt(A.afterSanitizeAttributes,z,null)},Vp=function ge(z){let Z=null;const ce=nr(z);for(Zt(A.beforeSanitizeShadowDOM,z,null);Z=ce.nextNode();)Zt(A.uponSanitizeShadowNode,Z,null),Tc(Z),Mc(Z),Z.content instanceof a&&ge(Z.content);Zt(A.afterSanitizeShadowDOM,z,null)};return t.sanitize=function(ge){let z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},Z=null,ce=null,Ye=null,ut=null;if(ve=!ge,ve&&(ge="<!-->"),typeof ge!="string"&&!Ec(ge))if(typeof ge.toString=="function"){if(ge=ge.toString(),typeof ge!="string")throw Gr("dirty is not a string, aborting")}else throw Gr("toString is not a function");if(!t.isSupported)return ge;if(Me||qe(z),t.removed=[],typeof ge=="string"&&(be=!1),be){if(ge.nodeName){const Qt=$e(ge.nodeName);if(!Y[Qt]||q[Qt])throw Gr("root node is forbidden and cannot be sanitized in-place")}}else if(ge instanceof l)Z=Wt("<!---->"),ce=Z.ownerDocument.importNode(ge,!0),ce.nodeType===Br.element&&ce.nodeName==="BODY"||ce.nodeName==="HTML"?Z=ce:Z.appendChild(ce);else{if(!we&&!Ge&&!ye&&ge.indexOf("<")===-1)return E&&Le?E.createHTML(ge):ge;if(Z=Wt(ge),!Z)return we?null:Le?I:""}Z&&Ce&&dt(Z.firstChild);const Qe=nr(be?ge:Z);for(;Ye=Qe.nextNode();)Tc(Ye),Mc(Ye),Ye.content instanceof a&&Vp(Ye.content);if(be)return ge;if(we){if(Fe)for(ut=x.call(Z.ownerDocument);Z.firstChild;)ut.appendChild(Z.firstChild);else ut=Z;return(K.shadowroot||K.shadowrootmode)&&(ut=T.call(n,ut,!0)),ut}let wt=ye?Z.outerHTML:Z.innerHTML;return ye&&Y["!doctype"]&&Z.ownerDocument&&Z.ownerDocument.doctype&&Z.ownerDocument.doctype.name&>(us,Z.ownerDocument.doctype.name)&&(wt="<!DOCTYPE "+Z.ownerDocument.doctype.name+`> -`+wt),Ge&&bn([P,D,L],Qt=>{wt=Hr(wt,Qt," ")}),E&&Le?E.createHTML(wt):wt},t.setConfig=function(){let ge=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};qe(ge),Me=!0},t.clearConfig=function(){Be=null,Me=!1},t.isValidAttribute=function(ge,z,Z){Be||qe({});const ce=$e(ge),Ye=$e(z);return Nc(ce,Ye,Z)},t.addHook=function(ge,z){typeof z=="function"&&zr(A[ge],z)},t.removeHook=function(ge,z){if(z!==void 0){const Z=hd(A[ge],z);return Z===-1?void 0:pd(A[ge],Z,1)[0]}return as(A[ge])},t.removeHooks=function(ge){A[ge]=[]},t.removeAllHooks=function(){A=gs()},t}var wo=hs();const Id=()=>{const e=()=>{const n=window.G7Core?.state?.get?.();return n&&(n.preferredCurrency||n.defaultCurrency)||""},[t,r]=k.useState(e);return k.useEffect(()=>{const n=window.G7Core?.state?.subscribe;if(r(e()),typeof n!="function")return;const o=n(a=>{r(a&&(a.preferredCurrency||a.defaultCurrency)||"")});return typeof o=="function"?o:void 0},[]),t},Dd=(e,t=!1)=>{window.G7Core?.dispatch?.({handler:"navigate",params:t?{path:e,mergeQuery:!0,query:{}}:{path:e}})},Ld=e=>{const t=Math.round(e*2)/2;return[1,2,3,4,5].map(r=>t>=r?"full":t>=r-.5?"half":"empty")},ps=({product:e,onClick:t,shopBase:r="/shop",preserveListQuery:n=!1,className:o="",id:a,editorAttrs:s})=>{const l=Id(),f=v=>{const E=`multi_currency_${v}`,I=e[E];if(I&&I[l])return I[l].formatted;const _=`${v}_formatted`;return e[_]},h=e.name_localized??(typeof e.name=="string"?e.name:"")??"",c=f("selling_price"),i=f("list_price"),u=e.discount_rate&&e.discount_rate>0,g=e.sales_status&&e.sales_status!=="on_sale",p=e.labels??[],m=v=>wo.sanitize(v,{ALLOWED_TAGS:["mark"],ALLOWED_ATTR:[]}),w=e.name_highlighted,C=e.description_highlighted??e.short_description,O=()=>{if(t)t(e.id);else{const v=r==="/"?"":r,E=e.product_code??e.id;Dd(`${v}/products/${E}`,n)}};return d.jsxs(Q,{onClick:O,className:`block w-full text-left group bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-lg transition-shadow cursor-pointer ${o}`,id:a,...s,children:[d.jsxs(W,{className:"relative aspect-square overflow-hidden bg-gray-100 dark:bg-gray-700",children:[d.jsx(Lt,{src:e.thumbnail_url,alt:h,className:"w-full h-full object-cover group-hover:scale-105 transition-transform duration-200",loading:"lazy"}),g&&d.jsx(W,{className:"absolute inset-0 bg-black/50 flex items-center justify-center",children:d.jsx(R,{className:`text-lg font-bold ${e.sales_status==="sold_out"?"text-red-300":e.sales_status==="suspended"?"text-amber-300":"text-blue-300"}`,style:{textShadow:"0 1px 4px rgba(0,0,0,0.6)"},children:e.sales_status_label??e.sales_status})}),u&&e.sales_status!=="sold_out"&&e.sales_status!=="suspended"&&d.jsxs(R,{className:"absolute top-2 left-2 px-2 py-1 bg-red-500 text-white text-xs font-bold rounded",children:[e.discount_rate,"%"]}),p.length>0&&d.jsx(W,{className:"absolute bottom-2 right-2 flex gap-1 flex-wrap justify-end",children:p.map((v,E)=>d.jsx(R,{className:"text-xs text-white font-medium px-1.5 py-0.5 rounded",style:{backgroundColor:v.color||"#6b7280"},children:v.name},E))})]}),d.jsxs(W,{className:"p-4",children:[d.jsxs(W,{className:"flex items-center gap-1.5 flex-wrap",children:[e.primary_category&&d.jsx(R,{className:"inline-block text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 px-2 py-0.5 rounded",children:e.primary_category}),e.brand_name&&d.jsx(R,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.brand_name})]}),w?d.jsx(W,{className:"mt-1 font-medium text-gray-900 dark:text-white line-clamp-1 [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-500/40 [&_mark]:px-0.5 [&_mark]:rounded",dangerouslySetInnerHTML:{__html:m(w)}}):d.jsx(gn,{className:"mt-1 font-medium text-gray-900 dark:text-white line-clamp-1",children:h}),C&&d.jsx(W,{className:"mt-1 text-sm text-gray-600 dark:text-gray-400 line-clamp-2 [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-500/40 [&_mark]:px-0.5 [&_mark]:rounded",dangerouslySetInnerHTML:{__html:m(C)}}),d.jsxs(W,{className:"mt-1.5 flex items-center gap-1",children:[Ld(e.rating_avg??0).map((v,E)=>d.jsxs(R,{className:`text-xs ${v==="empty"?"text-gray-300 dark:text-gray-600":"text-yellow-400"}`,children:[v==="full"&&d.jsx("i",{className:"fa-solid fa-star"}),v==="half"&&d.jsx("i",{className:"fa-solid fa-star-half-stroke"}),v==="empty"&&d.jsx("i",{className:"fa-regular fa-star"})]},E)),d.jsx(R,{className:"text-xs text-gray-600 dark:text-gray-400 ml-0.5",children:(e.rating_avg??0).toFixed(1)}),d.jsxs(R,{className:"text-xs text-gray-400 dark:text-gray-500",children:["(",e.review_count??0,")"]})]}),d.jsxs(W,{className:"mt-2 flex items-baseline gap-2",children:[d.jsx(R,{className:"font-bold text-gray-900 dark:text-white",children:c}),i&&u&&d.jsx(R,{className:"text-sm text-gray-500 dark:text-gray-400 line-through",children:i})]})]})]})},Pd=Object.freeze(Object.defineProperty({__proto__:null,default:ps},Symbol.toStringTag,{value:"Module"})),$d={ALLOWED_TAGS:["svg","g","defs","title","desc","rect","circle","ellipse","line","polyline","polygon","path","text","tspan","linearGradient","radialGradient","stop","clipPath"],ALLOWED_ATTR:["xmlns","width","height","viewBox","role","aria-label","aria-hidden","fill","fill-opacity","fill-rule","stroke","stroke-width","stroke-linecap","stroke-linejoin","opacity","transform","x","y","x1","y1","x2","y2","cx","cy","r","rx","ry","d","points","width","height","text-anchor","dominant-baseline","font-family","font-size","font-weight","offset","stop-color","stop-opacity","gradientUnits","id","clip-path"],FORBID_TAGS:["script","foreignObject","image","use","a","style"],FORBID_ATTR:["onload","onerror","onclick","onmouseover","onfocus","onblur","href","xlink:href"]},xo=({brandMark:e,size:t=32,className:r="",id:n,editorAttrs:o})=>{const a=e?.svg,s=e?.text,l=k.useMemo(()=>!a||a.trim()===""?"":wo.sanitize(a,$d),[a]),f={width:`${t}px`,height:`${t}px`,flex:`0 0 ${t}px`};if(l)return d.jsx(W,{id:n,"aria-hidden":"true",className:`inline-flex items-center justify-center ${r}`.trim(),style:f,dangerouslySetInnerHTML:{__html:l},...o});if(s&&s.trim()!==""){const h=e?.class??"bg-gray-500 text-white";return d.jsx(R,{id:n,"aria-hidden":"true",className:`inline-flex items-center justify-center rounded-lg text-xs font-bold ${h} ${r}`.trim(),style:f,...o,children:s})}return null},Fd=Object.freeze(Object.defineProperty({__proto__:null,BrandMark:xo,default:xo},Symbol.toStringTag,{value:"Module"})),ms="carousel",Wr="controller",Ud="navigation",zd="no-scroll",ko="portal",Hd="root",bs="toolbar",Gd="counter",Vr="fullscreen",ys="slideshow",Kr="thumbnails",vs="zoom",vr="loading",Co="playing",Yr="error",Xr="complete",jd="placeholder",qr=e=>`active-slide-${e}`,Bd=qr(vr),Wd=qr(Co),Vd=qr(Yr),Kd=qr(Xr),ws="fullsize",wr="flex_center",Yd="no_scroll",xs="no_scroll_padding",So="slide",ks="slide_wrapper",Xd="slide_wrapper_interactive",Rt="prev",er="next",_o="swipe",xr="close",Cs="onPointerDown",Ss="onPointerMove",_s="onPointerUp",Es="onPointerLeave",Ts="onPointerCancel",Eo="onKeyDown",qd="onKeyUp",To="onWheel",Zd="Escape",Qd="ArrowLeft",Jd="ArrowRight",Rd="button",wn="icon",Ns="contain",As="cover",Ms="Unknown action type",Os="yarl__";function lt(...e){return[...e].filter(Boolean).join(" ")}function pe(e){return`${Os}${e}`}function We(e){return`--${Os}${e}`}function or(e,t){return`${e}${t?`_${t}`:""}`}function ar(e){return t=>or(e,t)}function sr(e,t){var r;return(r=e?.[t])!==null&&r!==void 0?r:t}function Is(e,t,r){return sr(e,"{index} of {total}").replace(/\{index}/g,`${Oo(r,t.length)+1}`).replace(/\{total}/g,`${t.length}`)}function kr(...e){return()=>{e.forEach(t=>{t()})}}function At(e,t,r){return()=>{const n=N.useContext(r);if(!n)throw new Error(`${e} must be used within a ${t}.Provider`);return n}}function No(){return typeof window<"u"}function Ao(e,t=0){const r=10**t;return Math.round((e+Number.EPSILON)*r)/r}function lr(e){return e.type===void 0||e.type==="image"}function Mo(e,t){return e.imageFit===As||e.imageFit!==Ns&&t===As}function xn(e){return typeof e=="string"?Number.parseInt(e,10):e}function kn(e){if(typeof e=="number")return{pixel:e};if(typeof e=="string"){const t=xn(e);return e.endsWith("%")?{percent:t}:{pixel:t}}return{pixel:0}}function eu(e,t){const r=kn(t),n=r.percent!==void 0?e.width/100*r.percent:r.pixel;return{width:Math.max(e.width-2*n,0),height:Math.max(e.height-2*n,0)}}function tu(){return(No()?window?.devicePixelRatio:void 0)||1}function Oo(e,t){return t>0?(e%t+t)%t:0}function Io(e){return e.length>0}function Do(e,t){return e[Oo(t,e.length)]}function Lo(e,t){return Io(e)?Do(e,t):void 0}function Ds(e){return lr(e)?e.src:void 0}function Cn(e,t,r){if(!r)return e;const{buttons:n,...o}=e,a=n.findIndex(l=>l===t),s=N.isValidElement(r)?N.cloneElement(r,{key:t},null):r;if(a>=0){const l=[...n];return l.splice(a,1,s),{buttons:l,...o}}return{buttons:[s,...n],...o}}function Ls(e,t,r=0){return Math.min(e.preload,Math.max(e.finite?t.length-1:Math.floor(t.length/2),r))}const ru=Number(N.version.split(".")[0])>=19;function nu(e){return{inert:ru?e:e?"":void 0}}function ou(e){e.scrollTop}const Po={open:!1,close:()=>{},index:0,slides:[],render:{},plugins:[],toolbar:{buttons:[xr]},labels:{},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},carousel:{finite:!1,preload:2,padding:"16px",spacing:"30%",imageFit:Ns,imageProps:{}},controller:{ref:null,focus:!0,aria:!1,touchAction:"none",closeOnPullUp:!1,closeOnPullDown:!1,closeOnBackdropClick:!1,preventDefaultWheelX:!0,preventDefaultWheelY:!1,disableSwipeNavigation:!1},portal:{},noScroll:{disabled:!1},on:{},styles:{},className:""};function Mt(e,t){return{name:e,component:t}}function tt(e,t){return{module:e,children:t}}function Ps(e,t,r){return e.module.name===t?r(e):e.children?[tt(e.module,e.children.flatMap(n=>{var o;return(o=Ps(n,t,r))!==null&&o!==void 0?o:[]}))]:[e]}function Cr(e,t,r){return e.flatMap(n=>{var o;return(o=Ps(n,t,r))!==null&&o!==void 0?o:[]})}function au(e,t=[],r=[]){let n=e;const o=g=>{const p=[...n];for(;p.length>0;){const m=p.pop();if(m?.module.name===g)return!0;m?.children&&p.push(...m.children)}return!1},a=(g,p)=>{if(g===""){n=[tt(p,n)];return}n=Cr(n,g,m=>[tt(p,[m])])},s=(g,p)=>{n=Cr(n,g,m=>[tt(m.module,[tt(p,m.children)])])},l=(g,p,m)=>{n=Cr(n,g,w=>{var C;return[tt(w.module,[...m?[tt(p)]:[],...(C=w.children)!==null&&C!==void 0?C:[],...m?[]:[tt(p)]])]})},f=(g,p,m)=>{n=Cr(n,g,w=>[...m?[tt(p)]:[],w,...m?[]:[tt(p)]])},h=g=>{s(Wr,g)},c=(g,p)=>{n=Cr(n,g,m=>[tt(p,m.children)])},i=g=>{n=Cr(n,g,p=>p.children)},u=g=>{r.push(g)};return t.forEach(g=>{g({contains:o,addParent:a,append:s,addChild:l,addSibling:f,addModule:h,replace:c,remove:i,augment:u})}),{config:n,augmentation:g=>r.reduce((p,m)=>m(p),g)}}const $s=N.createContext(null),$o=At("useA11yContext","A11yContext",$s);function su({children:e}){const[t,r]=N.useState(!1),[n,o]=N.useState(!1),a=N.useMemo(()=>({focusWithin:t,trackFocusWithin:(l,f)=>{const h=c=>i=>{var u;i.currentTarget.contains(i.relatedTarget)||r(c),(u=c?l:f)===null||u===void 0||u(i)};return{onFocus:h(!0),onBlur:h(!1)}},autoPlaying:n,setAutoPlaying:o}),[t,n]);return N.createElement($s.Provider,{value:a},e)}const Fs=N.createContext(null),Sr=At("useDocument","DocumentContext",Fs);function lu({nodeRef:e,children:t}){const r=N.useMemo(()=>{const n=a=>{var s;return((s=a||e.current)===null||s===void 0?void 0:s.ownerDocument)||document};return{getOwnerDocument:n,getOwnerWindow:a=>{var s;return((s=n(a))===null||s===void 0?void 0:s.defaultView)||window}}},[e]);return N.createElement(Fs.Provider,{value:r},t)}const Us=N.createContext(null),_r=At("useEvents","EventsContext",Us);function iu({children:e}){const[t]=N.useState({});N.useEffect(()=>()=>{Object.keys(t).forEach(n=>delete t[n])},[t]);const r=N.useMemo(()=>{const n=(s,l)=>{var f;(f=t[s])===null||f===void 0||f.splice(0,t[s].length,...t[s].filter(h=>h!==l))};return{publish:(...[s,l])=>{var f;(f=t[s])===null||f===void 0||f.forEach(h=>h(l))},subscribe:(s,l)=>(t[s]||(t[s]=[]),t[s].push(l),()=>n(s,l)),unsubscribe:n}},[t]);return N.createElement(Us.Provider,{value:r},e)}const zs=N.createContext(null),rt=At("useLightboxProps","LightboxPropsContext",zs);function Hs({children:e,...t}){return N.createElement(zs.Provider,{value:t},e)}const Gs=N.createContext(null),Ot=At("useLightboxState","LightboxStateContext",Gs),js=N.createContext(null),cu=At("useLightboxDispatch","LightboxDispatchContext",js);function du(e,t){switch(t.type){case"swipe":{const{slides:r}=e,n=t?.increment||0,o=e.globalIndex+n,a=Oo(o,r.length),s=Lo(r,a),l=n||t.duration!==void 0?{increment:n,duration:t.duration,easing:t.easing}:void 0;return{slides:r,currentIndex:a,globalIndex:o,currentSlide:s,animation:l}}case"update":return t.slides!==e.slides||t.index!==e.currentIndex?{slides:t.slides,currentIndex:t.index,globalIndex:t.index,currentSlide:Lo(t.slides,t.index)}:e;default:throw new Error(Ms)}}function uu({slides:e,index:t,children:r}){const[n,o]=N.useReducer(du,{slides:e,currentIndex:t,globalIndex:t,currentSlide:Lo(e,t)});N.useEffect(()=>{o({type:"update",slides:e,index:t})},[e,t]);const a=N.useMemo(()=>({...n,state:n,dispatch:o}),[n,o]);return N.createElement(js.Provider,{value:o},N.createElement(Gs.Provider,{value:a},r))}const Bs=N.createContext(null),Zr=At("useTimeouts","TimeoutsContext",Bs);function fu({children:e}){const[t]=N.useState([]);N.useEffect(()=>()=>{t.forEach(n=>window.clearTimeout(n)),t.splice(0,t.length)},[t]);const r=N.useMemo(()=>{const n=s=>{t.splice(0,t.length,...t.filter(l=>l!==s))};return{setTimeout:(s,l)=>{const f=window.setTimeout(()=>{n(f),s()},l);return t.push(f),f},clearTimeout:s=>{s!==void 0&&(n(s),window.clearTimeout(s))}}},[t]);return N.createElement(Bs.Provider,{value:r},e)}const Er=N.forwardRef(function({label:t,className:r,icon:n,renderIcon:o,onClick:a,style:s,...l},f){const{styles:h,labels:c}=rt(),i=sr(c,t);return N.createElement("button",{ref:f,type:"button",title:i,"aria-label":i,className:lt(pe(Rd),r),onClick:a,style:{...s,...h.button},...l},o?o():N.createElement(n,{className:pe(wn),style:h.icon}))});function Ws(e,t){const r=n=>N.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24","aria-hidden":"true",focusable:"false",...n},t);return r.displayName=e,r}function mt(e,t){return Ws(e,N.createElement("g",{fill:"currentColor"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t))}function gu(e,t){return Ws(e,N.createElement(N.Fragment,null,N.createElement("defs",null,N.createElement("mask",{id:"strike"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"white"}),N.createElement("path",{d:"M0 0L24 24",stroke:"black",strokeWidth:4}))),N.createElement("path",{d:"M0.70707 2.121320L21.878680 23.292883",stroke:"currentColor",strokeWidth:2}),N.createElement("g",{fill:"currentColor",mask:"url(#strike)"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t)))}const hu=mt("Close",N.createElement("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),pu=mt("Previous",N.createElement("path",{d:"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"})),mu=mt("Next",N.createElement("path",{d:"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"})),bu=mt("Loading",N.createElement(N.Fragment,null,Array.from({length:8}).map((e,t,r)=>N.createElement("line",{key:t,x1:"12",y1:"6.5",x2:"12",y2:"1.8",strokeLinecap:"round",strokeWidth:"2.6",stroke:"currentColor",strokeOpacity:1/r.length*(t+1),transform:`rotate(${360/r.length*t}, 12, 12)`})))),yu=mt("Error",N.createElement("path",{d:"M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z"})),Ft=No()?N.useLayoutEffect:N.useEffect;function Fo(){const[e,t]=N.useState(!1);return N.useEffect(()=>{var r,n;const o=(r=window.matchMedia)===null||r===void 0?void 0:r.call(window,"(prefers-reduced-motion: reduce)");t(o?.matches);const a=s=>t(s.matches);return(n=o?.addEventListener)===null||n===void 0||n.call(o,"change",a),()=>{var s;return(s=o?.removeEventListener)===null||s===void 0?void 0:s.call(o,"change",a)}},[]),e}function vu(e){let t=0,r=0,n=0;const a=window.getComputedStyle(e).transform.match(/matrix.*\((.+)\)/);if(a){const s=a[1].split(",").map(xn);s.length===6?(t=s[4],r=s[5]):s.length===16&&(t=s[12],r=s[13],n=s[14])}return{x:t,y:r,z:n}}function Uo(e,t){const r=N.useRef(void 0),n=N.useRef(void 0),o=Fo();return Ft(()=>{var a,s,l;if(e.current&&r.current!==void 0&&!o){const{keyframes:f,duration:h,easing:c,onfinish:i}=t(r.current,e.current.getBoundingClientRect(),vu(e.current))||{};if(f&&h){(a=n.current)===null||a===void 0||a.cancel(),n.current=void 0;try{n.current=(l=(s=e.current).animate)===null||l===void 0?void 0:l.call(s,f,{duration:h,easing:c})}catch(u){console.error(u)}n.current&&(n.current.onfinish=()=>{n.current=void 0,i?.()})}}r.current=void 0}),{prepareAnimation:a=>{r.current=a},isAnimationPlaying:()=>{var a;return((a=n.current)===null||a===void 0?void 0:a.playState)==="running"}}}function Vs(){const e=N.useRef(null),t=N.useRef(void 0),[r,n]=N.useState();return{setContainerRef:N.useCallback(a=>{e.current=a,t.current&&(t.current.disconnect(),t.current=void 0);const s=()=>{if(a){const l=window.getComputedStyle(a),f=h=>parseFloat(h)||0;n({width:Math.round(a.clientWidth-f(l.paddingLeft)-f(l.paddingRight)),height:Math.round(a.clientHeight-f(l.paddingTop)-f(l.paddingBottom))})}else n(void 0)};s(),a&&typeof ResizeObserver<"u"&&(t.current=new ResizeObserver(s),t.current.observe(a))},[]),containerRef:e,containerRect:r}}function Sn(){const e=N.useRef(void 0),{setTimeout:t,clearTimeout:r}=Zr();return N.useCallback((n,o)=>{r(e.current),e.current=t(n,o>0?o:0)},[t,r])}function Ie(e){const t=N.useRef(e);return Ft(()=>{t.current=e}),N.useCallback((...r)=>{var n;return(n=t.current)===null||n===void 0?void 0:n.call(t,...r)},[])}function Ks(e,t){typeof e=="function"?e(t):e&&(e.current=t)}function zo(e,t){return N.useMemo(()=>e==null&&t==null?null:r=>{Ks(e,r),Ks(t,r)},[e,t])}function Ys(e,t=!1){const r=N.useRef(!1);Ft(()=>{t&&r.current&&(r.current=!1,e())},[t,e]);const n=N.useCallback(()=>{r.current=!0},[]),o=N.useCallback(()=>{r.current=!1},[]);return{onFocus:n,onBlur:o}}function _n(){const[e,t]=N.useState(!1);return Ft(()=>{t(window.getComputedStyle(window.document.documentElement).direction==="rtl")},[]),e}function Xs(){const[e]=N.useState({}),t=N.useCallback((o,a)=>{var s;(s=e[o])===null||s===void 0||s.forEach(l=>{a.isPropagationStopped()||l(a)})},[e]),r=N.useMemo(()=>({onPointerDown:o=>t(Cs,o),onPointerMove:o=>t(Ss,o),onPointerUp:o=>t(_s,o),onPointerLeave:o=>t(Es,o),onPointerCancel:o=>t(Ts,o),onKeyDown:o=>t(Eo,o),onKeyUp:o=>t(qd,o),onWheel:o=>t(To,o)}),[t]),n=N.useCallback((o,a)=>(e[o]||(e[o]=[]),e[o].unshift(a),()=>{const s=e[o];s&&s.splice(0,s.length,...s.filter(l=>l!==a))}),[e]);return{registerSensors:r,subscribeSensors:n}}function qs(e,t){const r=N.useRef(0),n=Sn(),o=Ie((...a)=>{r.current=Date.now(),e(a)});return N.useCallback((...a)=>{n(()=>{o(a)},t-(Date.now()-r.current))},[t,o,n])}const Ho=ar("slide"),Go=ar("slide_image");function ir({slide:e,offset:t,render:r,rect:n,imageFit:o,imageProps:a,onClick:s,onLoad:l,onError:f,style:h}){var c,i,u,g,p,m,w,C;const[O,v]=N.useState(vr),{publish:E}=_r(),{setTimeout:I}=Zr(),_=N.useRef(null);N.useEffect(()=>{t===0&&E(qr(O))},[t,O,E]);const M=Ie(B=>{("decode"in B?B.decode():Promise.resolve()).catch(()=>{}).then(()=>{B.parentNode&&(v(Xr),I(()=>{l?.(B)},0))})}),x=N.useCallback(B=>{_.current=B,B?.complete&&M(B)},[M]),y=N.useCallback(B=>{M(B.currentTarget)},[M]),T=Ie(()=>{v(Yr),f?.()}),A=Mo(e,o),P=(B,K)=>Number.isFinite(B)?B:K,D=P(Math.max(...((i=(c=e.srcSet)===null||c===void 0?void 0:c.map(B=>B.width))!==null&&i!==void 0?i:[]).concat(e.width?[e.width]:[]).filter(Boolean)),((u=_.current)===null||u===void 0?void 0:u.naturalWidth)||0),L=P(Math.max(...((p=(g=e.srcSet)===null||g===void 0?void 0:g.map(B=>B.height))!==null&&p!==void 0?p:[]).concat(e.height?[e.height]:[]).filter(Boolean)),((m=_.current)===null||m===void 0?void 0:m.naturalHeight)||0),U=D&&L?{maxWidth:`min(${D}px, 100%)`,maxHeight:`min(${L}px, 100%)`}:{maxWidth:"100%",maxHeight:"100%"},S=(w=e.srcSet)===null||w===void 0?void 0:w.sort((B,K)=>B.width-K.width).map(B=>`${B.src} ${B.width}w`).join(", "),b=()=>n&&!A&&e.width&&e.height?n.height/e.height*e.width:Number.MAX_VALUE,F=S&&n&&No()?`${Math.round(Math.min(b(),n.width))}px`:void 0,{style:$,className:G,...Y}=a||{};return N.createElement(N.Fragment,null,N.createElement("img",{ref:x,onLoad:y,onError:T,onClick:s,draggable:!1,className:lt(pe(Go()),A&&pe(Go("cover")),O!==Xr&&pe(Go("loading")),G),style:{...U,...h,...$},...Y,alt:(C=e.alt)!==null&&C!==void 0?C:"",sizes:F,srcSet:S,src:e.src}),O!==Xr&&N.createElement("div",{className:pe(Ho(jd))},O===vr&&(r?.iconLoading?r.iconLoading():N.createElement(bu,{className:lt(pe(wn),pe(Ho(vr)))})),O===Yr&&(r?.iconError?r.iconError():N.createElement(yu,{className:lt(pe(wn),pe(Ho(Yr)))}))))}const wu=N.forwardRef(function({className:t,children:r,onFocus:n,onBlur:o,...a},s){const l=N.useRef(null),{trackFocusWithin:f}=$o();return N.createElement(lu,{nodeRef:l},N.createElement("div",{ref:zo(s,l),className:lt(pe("root"),t),...f(n,o),...a},r))});var it;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL",e[e.ANIMATION=3]="ANIMATION"})(it||(it={}));function Zs(e,t,r,n,o){N.useEffect(()=>o?()=>{}:kr(e(Cs,t),e(Ss,r),e(_s,n),e(Es,n),e(Ts,n)),[e,t,r,n,o])}var Ut;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL"})(Ut||(Ut={}));const jo=30;function xu({disableSwipeNavigation:e,closeOnBackdropClick:t},r,n,o,a,s,l,f,h,c,i,u,g,p,m,w){const C=N.useRef(0),O=N.useRef([]),v=N.useRef(void 0),E=N.useRef(0),I=N.useRef(Ut.NONE),_=N.useCallback(D=>{v.current===D.pointerId&&(v.current=void 0,I.current=Ut.NONE);const L=O.current;L.splice(0,L.length,...L.filter(U=>U.pointerId!==D.pointerId))},[]),M=N.useCallback(D=>{_(D),D.persist(),O.current.push(D)},[_]),x=N.useCallback(D=>O.current.find(({pointerId:L})=>D.pointerId===L),[]),y=Ie(D=>{M(D)}),T=(D,L)=>i&&D>L||c&&D<-L,A=Ie(D=>{const L=x(D);if(L)if(v.current===D.pointerId){const U=Date.now()-E.current,S=C.current;I.current===Ut.SWIPE?Math.abs(S)>.3*o||Math.abs(S)>5&&U<a?f(S,U):h(S):I.current===Ut.PULL&&(T(S,2*jo)?p(S,U):m(S)),C.current=0,I.current=Ut.NONE}else{const{target:U}=D;t&&U instanceof HTMLElement&&U===L.target&&(U.classList.contains(pe(So))||U.classList.contains(pe(ks)))&&w()}_(D)}),P=Ie(D=>{const L=x(D);if(L){const U=v.current===D.pointerId;if(D.buttons===0){U&&C.current!==0?A(D):_(L);return}const S=D.clientX-L.clientX,b=D.clientY-L.clientY;if(v.current===void 0){const F=$=>{M(D),v.current=D.pointerId,E.current=Date.now(),I.current=$};Math.abs(S)>Math.abs(b)&&Math.abs(S)>jo&&n(S)?e||(F(Ut.SWIPE),s()):Math.abs(b)>Math.abs(S)&&T(b,jo)&&(F(Ut.PULL),u())}else U&&(I.current===Ut.SWIPE?(C.current=S,l(S)):I.current===Ut.PULL&&(C.current=b,g(b)))}});Zs(r,y,P,A)}function ku({preventDefaultWheelX:e,preventDefaultWheelY:t}){const r=N.useRef(null),n=Ie(o=>{const a=Math.abs(o.deltaX)>Math.abs(o.deltaY);(a&&e||!a&&t||o.ctrlKey)&&o.preventDefault()});return N.useCallback(o=>{var a;o?o.addEventListener("wheel",n,{passive:!1}):(a=r.current)===null||a===void 0||a.removeEventListener("wheel",n),r.current=o},[n])}function Cu(e,t,r,n,o,a,s,l,f){const h=N.useRef(0),c=N.useRef(0),i=N.useRef(void 0),u=N.useRef(void 0),g=N.useRef(0),p=N.useRef(void 0),m=N.useRef(0),{setTimeout:w,clearTimeout:C}=Zr(),O=N.useCallback(()=>{i.current&&(C(i.current),i.current=void 0)},[C]),v=N.useCallback(()=>{u.current&&(C(u.current),u.current=void 0)},[C]),E=Ie(()=>{e!==it.SWIPE&&(h.current=0,m.current=0,O(),v())});N.useEffect(E,[e,E]);const I=Ie(M=>{u.current=void 0,h.current===M&&f(h.current)}),_=Ie(M=>{if(M.ctrlKey||Math.abs(M.deltaY)>Math.abs(M.deltaX))return;const x=y=>{g.current=y,C(p.current),p.current=y>0?w(()=>{g.current=0,p.current=void 0},300):void 0};if(e===it.NONE){if(Math.abs(M.deltaX)<=1.2*Math.abs(g.current)){x(M.deltaX);return}if(!r(-M.deltaX))return;if(c.current+=M.deltaX,O(),Math.abs(c.current)>30)c.current=0,x(0),m.current=Date.now(),a();else{const y=c.current;i.current=w(()=>{i.current=void 0,y===c.current&&(c.current=0)},o)}}else if(e===it.SWIPE){let y=h.current-M.deltaX;if(y=Math.min(Math.abs(y),n)*Math.sign(y),h.current=y,s(y),v(),Math.abs(y)>.2*n){x(M.deltaX),l(y,Date.now()-m.current);return}u.current=w(()=>I(y),2*o)}else x(M.deltaX)});N.useEffect(()=>t(To,_),[t,_])}const Qs=ar("container"),Js=N.createContext(null),zt=At("useController","ControllerContext",Js);function Su({children:e,...t}){var r;const{carousel:n,animation:o,controller:a,on:s,styles:l,render:f}=t,{closeOnPullUp:h,closeOnPullDown:c,preventDefaultWheelX:i,preventDefaultWheelY:u}=a,[g,p]=N.useState(),m=Ot(),w=cu(),[C,O]=N.useState(it.NONE),v=N.useRef(0),E=N.useRef(0),I=N.useRef(1),{registerSensors:_,subscribeSensors:M}=Xs(),{subscribe:x,publish:y}=_r(),T=Sn(),A=Sn(),P=Sn(),{containerRef:D,setContainerRef:L,containerRect:U}=Vs(),S=zo(ku({preventDefaultWheelX:i,preventDefaultWheelY:u}),L),b=N.useRef(null),F=zo(b,void 0),{getOwnerDocument:$}=Sr(),G=_n(),Y=V=>(G?-1:1)*(typeof V=="number"?V:1),B=Ie(()=>{var V;return(V=D.current)===null||V===void 0?void 0:V.focus()}),K=Ie(()=>t),j=Ie(()=>m),H=N.useCallback(V=>y(Rt,V),[y]),q=N.useCallback(V=>y(er,V),[y]),ue=N.useCallback(()=>y(xr),[y]),te=V=>!(n.finite&&(Y(V)>0&&m.currentIndex===0||Y(V)<0&&m.currentIndex===m.slides.length-1)),fe=V=>{var re;v.current=V,(re=D.current)===null||re===void 0||re.style.setProperty(We("swipe_offset"),`${Math.round(V)}px`)},ee=V=>{var re,oe;E.current=V,I.current=(()=>{const xe=c&&V>0?V:h&&V<0?-V:0;return Math.min(Math.max(Ao(1-xe/60*(1-.5),2),.5),1)})(),(re=D.current)===null||re===void 0||re.style.setProperty(We("pull_offset"),`${Math.round(V)}px`),(oe=D.current)===null||oe===void 0||oe.style.setProperty(We("pull_opacity"),`${I.current}`)},{prepareAnimation:ke}=Uo(b,(V,re,oe)=>{if(b.current&&U)return{keyframes:[{transform:`translate(0, ${V.rect.y-re.y+oe.y}px)`,opacity:V.opacity},{transform:"translate(0, 0)",opacity:1}],duration:V.duration,easing:o.easing.fade}}),me=(V,re)=>{if(h||c){ee(V);let oe=0;b.current&&(oe=o.fade*(re?2:1),ke({rect:b.current.getBoundingClientRect(),opacity:I.current,duration:oe})),P(()=>{ee(0),O(it.NONE)},oe),O(it.ANIMATION),re||ue()}},{prepareAnimation:Ge,isAnimationPlaying:De}=Uo(b,(V,re,oe)=>{var ae;if(b.current&&U&&(!((ae=m.animation)===null||ae===void 0)&&ae.duration)){const be=kn(n.spacing),xe=(be.percent?be.percent*U.width/100:be.pixel)||0;return{keyframes:[{transform:`translate(${Y(m.globalIndex-V.index)*(U.width+xe)+V.rect.x-re.x+oe.x}px, 0)`},{transform:"translate(0, 0)"}],duration:m.animation.duration,easing:m.animation.easing}}}),ye=Ie(V=>{var re,oe;const ae=V.offset||0,be=ae?o.swipe:(re=o.navigation)!==null&&re!==void 0?re:o.swipe,xe=!ae&&!De()?o.easing.navigation:o.easing.swipe;let{direction:He}=V;const Pe=(oe=V.count)!==null&&oe!==void 0?oe:1;let Ve=it.ANIMATION,Oe=be*Pe;if(!He){const se=U?.width,ie=V.duration||0,le=se?be/se*Math.abs(ae):be;Pe!==0?(ie<le?Oe=Oe/le*Math.max(ie,le/5):se&&(Oe=be/se*(se-Math.abs(ae))),He=Y(ae)>0?Rt:er):Oe=be/2}let X=0;He===Rt?te(Y(1))?X=-Pe:(Ve=it.NONE,Oe=be):He===er&&(te(Y(-1))?X=Pe:(Ve=it.NONE,Oe=be)),Oe=Math.round(Oe),A(()=>{fe(0),O(it.NONE)},Oe),b.current&&Ge({rect:b.current.getBoundingClientRect(),index:m.globalIndex}),O(Ve),y(_o,{type:"swipe",increment:X,duration:Oe,easing:xe})});N.useEffect(()=>{var V,re;!((V=m.animation)===null||V===void 0)&&V.increment&&(!((re=m.animation)===null||re===void 0)&&re.duration)&&T(()=>w({type:"swipe",increment:0}),m.animation.duration)},[m.animation,w,T]);const Me=[M,te,U?.width||0,o.swipe,()=>O(it.SWIPE),V=>fe(V),(V,re)=>ye({offset:V,duration:re,count:1}),V=>ye({offset:V,count:0})],Ce=[()=>{c&&O(it.PULL)},V=>ee(V),V=>me(V),V=>me(V,!0)];xu(a,...Me,h,c,...Ce,ue),Cu(C,...Me);const we=Ie(()=>{a.focus&&$().querySelector(`.${pe(ko)} .${pe(Qs())}`)&&B()});N.useEffect(we,[we]);const Fe=Ie(()=>{var V;(V=s.view)===null||V===void 0||V.call(s,{index:m.currentIndex})});N.useEffect(Fe,[m.globalIndex,Fe]),N.useEffect(()=>kr(x(Rt,V=>ye({direction:Rt,...V})),x(er,V=>ye({direction:er,...V})),x(_o,V=>w(V))),[x,ye,w]);const Le=N.useMemo(()=>({prev:H,next:q,close:ue,focus:B,slideRect:U?eu(U,n.padding):{width:0,height:0},containerRect:U||{width:0,height:0},subscribeSensors:M,containerRef:D,setCarouselRef:F,toolbarWidth:g,setToolbarWidth:p}),[H,q,ue,B,M,U,D,F,g,p,n.padding]);return N.useImperativeHandle(a.ref,()=>({prev:H,next:q,close:ue,focus:B,getLightboxProps:K,getLightboxState:j}),[H,q,ue,B,K,j]),N.createElement("div",{ref:S,className:lt(pe(Qs()),pe(wr)),style:{...C===it.SWIPE?{[We("swipe_offset")]:`${Math.round(v.current)}px`}:null,...C===it.PULL?{[We("pull_offset")]:`${Math.round(E.current)}px`,[We("pull_opacity")]:`${I.current}`}:null,...a.touchAction!=="none"?{[We("controller_touch_action")]:a.touchAction}:null,...l.container},tabIndex:-1,..._},U&&N.createElement(Js.Provider,{value:Le},e,(r=f.controls)===null||r===void 0?void 0:r.call(f)))}const _u=Mt(Wr,Su);function cr(e){return or(ms,e)}function Rs(e){return or(So,e)}function Eu({slide:e,offset:t}){const r=N.useRef(null),{currentIndex:n,slides:o}=Ot(),{slideRect:a,focus:s}=zt(),{render:l,carousel:{imageFit:f,imageProps:h},on:{click:c},styles:{slide:i},labels:u}=rt(),{getOwnerDocument:g}=Sr(),p=t!==0;N.useEffect(()=>{var w;p&&(!((w=r.current)===null||w===void 0)&&w.contains(g().activeElement))&&s()},[p,s,g]);const m=()=>{var w,C,O,v;let E=(w=l.slide)===null||w===void 0?void 0:w.call(l,{slide:e,offset:t,rect:a});return!E&&lr(e)&&(E=N.createElement(ir,{slide:e,offset:t,render:l,rect:a,imageFit:f,imageProps:h,onClick:p?void 0:()=>c?.({index:n})})),E?N.createElement(N.Fragment,null,(C=l.slideHeader)===null||C===void 0?void 0:C.call(l,{slide:e}),((O=l.slideContainer)!==null&&O!==void 0?O:(({children:I})=>I))({slide:e,children:E}),(v=l.slideFooter)===null||v===void 0?void 0:v.call(l,{slide:e})):null};return N.createElement("div",{ref:r,className:lt(pe(Rs()),!p&&pe(Rs("current")),pe(wr)),...nu(p),style:i,role:"group","aria-roledescription":sr(u,"Slide"),"aria-label":Is(u,o,n+t)},m())}function Tu(){const e=rt().styles.slide;return N.createElement("div",{className:pe(So),style:e})}function Nu({carousel:e,labels:t}){const{slides:r,currentIndex:n,globalIndex:o}=Ot(),{setCarouselRef:a}=zt(),{autoPlaying:s,focusWithin:l}=$o(),f=kn(e.spacing),h=kn(e.padding),c=Ls(e,r,1),i=[];if(Io(r))for(let u=n-c;u<=n+c;u+=1){const g=Do(r,u),p=o-n+u,m=e.finite&&(u<0||u>r.length-1);i.push(m?{key:p}:{key:[`${p}`,Ds(g)].filter(Boolean).join("|"),offset:u-n,slide:g})}return N.createElement("div",{ref:a,className:lt(pe(cr()),i.length>0&&pe(cr("with_slides"))),style:{[`${We(cr("slides_count"))}`]:i.length,[`${We(cr("spacing_px"))}`]:f.pixel||0,[`${We(cr("spacing_percent"))}`]:f.percent||0,[`${We(cr("padding_px"))}`]:h.pixel||0,[`${We(cr("padding_percent"))}`]:h.percent||0},role:"region","aria-live":s&&!l?"off":"polite","aria-roledescription":sr(t,"Carousel"),"aria-label":sr(t,"Photo gallery")},i.map(({key:u,slide:g,offset:p})=>g?N.createElement(Eu,{key:u,slide:g,offset:p}):N.createElement(Tu,{key:u})))}const Au=Mt(ms,Nu);function el(){const{carousel:e}=rt(),{slides:t,currentIndex:r}=Ot(),n=t.length===0||e.finite&&r===0,o=t.length===0||e.finite&&r===t.length-1;return{prevDisabled:n,nextDisabled:o}}function tl(e){var t;const r=_n(),{publish:n}=_r(),{animation:o}=rt(),{prevDisabled:a,nextDisabled:s}=el(),l=((t=o.navigation)!==null&&t!==void 0?t:o.swipe)/2,f=qs(()=>n(Rt),l),h=qs(()=>n(er),l),c=Ie(i=>{switch(i.key){case Zd:n(xr);break;case Qd:(r?s:a)||(r?h:f)();break;case Jd:(r?a:s)||(r?f:h)();break}});N.useEffect(()=>e(Eo,c),[e,c])}function rl({label:e,icon:t,renderIcon:r,action:n,onClick:o,disabled:a,style:s}){return N.createElement(Er,{label:e,icon:t,renderIcon:r,className:pe(`navigation_${n}`),disabled:a,onClick:o,style:s,...Ys(zt().focus,a)})}function Mu({render:{buttonPrev:e,buttonNext:t,iconPrev:r,iconNext:n},styles:o}){const{prev:a,next:s,subscribeSensors:l}=zt(),{prevDisabled:f,nextDisabled:h}=el();return tl(l),N.createElement(N.Fragment,null,e?e():N.createElement(rl,{label:"Previous",action:Rt,icon:pu,renderIcon:r,style:o.navigationPrev,disabled:f,onClick:a}),t?t():N.createElement(rl,{label:"Next",action:er,icon:mu,renderIcon:n,style:o.navigationNext,disabled:h,onClick:s}))}const Ou=Mt(Ud,Mu),nl=pe(Yd),Iu=pe(xs);function Du(e){return"style"in e}function ol(e,t,r){const n=window.getComputedStyle(e),o=r?"padding-left":"padding-right",a=r?n.paddingLeft:n.paddingRight,s=e.style.getPropertyValue(o);return e.style.setProperty(o,`${(xn(a)||0)+t}px`),()=>{s?e.style.setProperty(o,s):e.style.removeProperty(o)}}function Lu({noScroll:{disabled:e},children:t}){const r=_n(),{getOwnerDocument:n,getOwnerWindow:o}=Sr();return N.useEffect(()=>{if(e)return()=>{};const a=[],s=o(),{body:l,documentElement:f}=n(),h=Math.round(s.innerWidth-f.clientWidth);if(h>0){a.push(ol(l,h,r));const c=l.getElementsByTagName("*");for(let i=0;i<c.length;i+=1){const u=c[i];Du(u)&&s.getComputedStyle(u).getPropertyValue("position")==="fixed"&&!u.classList.contains(Iu)&&a.push(ol(u,h,r))}}return l.classList.add(nl),()=>{l.classList.remove(nl),a.forEach(c=>c())}},[r,e,n,o]),N.createElement(N.Fragment,null,t)}const Pu=Mt(zd,Lu);function al(e){return or(ko,e)}function sl(e,t,r){const n=e.getAttribute(t);return e.setAttribute(t,r),()=>{n?e.setAttribute(t,n):e.removeAttribute(t)}}function $u({children:e,animation:t,styles:r,className:n,on:o,portal:a,close:s,labels:l}){const[f,h]=N.useState(!1),[c,i]=N.useState(!1),u=N.useRef([]),g=N.useRef(null),{setTimeout:p}=Zr(),{subscribe:m}=_r(),C=Fo()?0:t.fade;N.useEffect(()=>(h(!0),()=>{h(!1),i(!1)}),[]);const O=Ie(()=>{u.current.forEach(_=>_()),u.current=[]}),v=Ie(()=>{var _;i(!1),O(),(_=o.exiting)===null||_===void 0||_.call(o),p(()=>{var M;(M=o.exited)===null||M===void 0||M.call(o),s()},C)});N.useEffect(()=>m(xr,v),[m,v]);const E=Ie(_=>{var M,x,y;ou(_),i(!0),(M=o.entering)===null||M===void 0||M.call(o);const T=(y=(x=_.parentNode)===null||x===void 0?void 0:x.children)!==null&&y!==void 0?y:[];for(let A=0;A<T.length;A+=1){const P=T[A];["TEMPLATE","SCRIPT","STYLE"].indexOf(P.tagName)===-1&&P!==_&&(u.current.push(sl(P,"inert","")),u.current.push(sl(P,"aria-hidden","true")))}u.current.push(()=>{var A,P;(P=(A=g.current)===null||A===void 0?void 0:A.focus)===null||P===void 0||P.call(A)}),p(()=>{var A;(A=o.entered)===null||A===void 0||A.call(o)},C)}),I=N.useCallback(_=>{_?E(_):O()},[E,O]);return f?Jt.createPortal(N.createElement(wu,{ref:I,className:lt(n,pe(al()),pe(xs),c&&pe(al("open"))),"aria-modal":!0,role:"dialog","aria-label":sr(l,"Lightbox"),style:{...t.fade!==Po.animation.fade?{[We("fade_animation_duration")]:`${C}ms`}:null,...t.easing.fade!==Po.animation.easing.fade?{[We("fade_animation_timing_function")]:t.easing.fade}:null,...r.root},onFocus:_=>{g.current||(g.current=_.relatedTarget)}},e),a.root||document.body):null}const Fu=Mt(ko,$u);function Uu({children:e}){return N.createElement(N.Fragment,null,e)}const zu=Mt(Hd,Uu);function Hu(e){return or(bs,e)}function Gu({toolbar:{buttons:e},render:{buttonClose:t,iconClose:r},styles:n}){const{close:o,setToolbarWidth:a}=zt(),{setContainerRef:s,containerRect:l}=Vs();Ft(()=>{a(l?.width)},[a,l?.width]);const f=()=>t?t():N.createElement(Er,{key:xr,label:"Close",icon:hu,renderIcon:r,onClick:o});return N.createElement("div",{ref:s,style:n.toolbar,className:pe(Hu())},e?.map(h=>h===xr?f():h))}const ju=Mt(bs,Gu);function ll(e,t){var r;return N.createElement(e.module.component,{key:e.module.name,...t},(r=e.children)===null||r===void 0?void 0:r.map(n=>ll(n,t)))}function Bu(e,t={}){const{easing:r,...n}=e,{easing:o,...a}=t;return{easing:{...r,...o},...n,...a}}function Wu({carousel:e,animation:t,render:r,toolbar:n,controller:o,noScroll:a,on:s,plugins:l,slides:f,index:h,...c}){const{animation:i,carousel:u,render:g,toolbar:p,controller:m,noScroll:w,on:C,slides:O,index:v,plugins:E,...I}=Po,{config:_,augmentation:M}=au([tt(Fu,[tt(Pu,[tt(_u,[tt(Au),tt(ju),tt(Ou)])])])],l||E),x=M({animation:Bu(i,t),carousel:{...u,...e},render:{...g,...r},toolbar:{...p,...n},controller:{...m,...o},noScroll:{...w,...a},on:{...C,...s},...I,...c});return x.open?N.createElement(Hs,{...x},N.createElement(uu,{slides:f||O,index:xn(h||v)},N.createElement(fu,null,N.createElement(iu,null,N.createElement(su,null,ll(tt(zu,_),x)))))):null}const Vu={minZoom:1,maxZoomPixelRatio:1,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:500,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,pinchZoomV4:!1,scrollToZoom:!1};function Ku(e){return Math.min(Math.max(e,Number.EPSILON),1)}function il(e){const{minZoom:t,...r}={...Vu,...e};return{minZoom:Ku(t),...r}}function Yu(e,t,r,n){const o=N.useRef(void 0),a=N.useRef(void 0),{zoom:s}=rt().animation,l=Fo(),f=Ie(()=>{var h,c,i;if((h=o.current)===null||h===void 0||h.cancel(),o.current=void 0,a.current&&n?.current){try{o.current=(i=(c=n.current).animate)===null||i===void 0?void 0:i.call(c,[{transform:a.current},{transform:`scale(${e}) translateX(${t}px) translateY(${r}px)`}],{duration:l?0:s??500,easing:o.current?"ease-out":"ease-in-out"})}catch(u){console.error(u)}a.current=void 0,o.current&&(o.current.onfinish=()=>{o.current=void 0})}});return Ft(f,[e,t,r,f]),N.useCallback(()=>{a.current=n?.current?window.getComputedStyle(n.current).transform:void 0},[n])}function Xu(e,t){const{on:r}=rt(),n=Ie(()=>{var o;t||(o=r.zoom)===null||o===void 0||o.call(r,{zoom:e})});N.useEffect(n,[e,n])}function En(){const{zoom:e}=rt();return il(e)}function qu(e,t){var r,n;let o={width:0,height:0},a={width:0,height:0};const{currentSlide:s}=Ot(),{imageFit:l}=rt().carousel,{maxZoomPixelRatio:f}=En();if(e&&s){const c={...s,...t};if(lr(c)){const i=Mo(c,l),u=Math.max(...(((r=c.srcSet)===null||r===void 0?void 0:r.map(p=>p.width))||[]).concat(c.width?[c.width]:[])),g=Math.max(...(((n=c.srcSet)===null||n===void 0?void 0:n.map(p=>p.height))||[]).concat(c.height?[c.height]:[]));u>0&&g>0&&e.width>0&&e.height>0&&(a=i?{width:Math.round(Math.min(u,e.width/e.height*g)),height:Math.round(Math.min(g,e.height/e.width*u))}:{width:u,height:g},a={width:a.width*f,height:a.height*f},o=i?{width:Math.min(e.width,a.width,u),height:Math.min(e.height,a.height,g)}:{width:Math.round(Math.min(e.width,e.height/g*u,u)),height:Math.round(Math.min(e.height,e.width/u*g,g))})}}const h=o.width?Math.max(Ao(a.width/o.width,5),1):1;return{imageRect:o,maxZoom:h}}function cl(e,t){return Math.hypot(e.clientX-t.clientX,e.clientY-t.clientY)}function dl(e,t,r=100,n=2){return e*Math.min(1+Math.abs(t/r),n)**Math.sign(t)}function Zu(e,t,r,n,o,a,s,l,f){const h=N.useRef([]),c=N.useRef(0),i=N.useRef(void 0),{globalIndex:u}=Ot(),{getOwnerWindow:g}=Sr(),{containerRef:p,subscribeSensors:m}=zt(),{keyboardMoveDistance:w,zoomInMultiplier:C,wheelZoomDistanceFactor:O,scrollToZoom:v,doubleTapDelay:E,doubleClickDelay:I,doubleClickMaxStops:_,pinchZoomDistanceFactor:M,pinchZoomV4:x}=En(),y=N.useCallback(F=>{if(p.current){const{pageX:$,pageY:G}=F,{scrollX:Y,scrollY:B}=g(),{left:K,top:j,width:H,height:q}=p.current.getBoundingClientRect();return[$-K-Y-H/2,G-j-B-q/2]}return[]},[p,g]),T=Ie(F=>{const{key:$,metaKey:G,ctrlKey:Y}=F,B=G||Y,K=()=>{F.preventDefault(),F.stopPropagation()};if(e>1){const j=(H,q)=>{K(),l(H,q)};$==="ArrowDown"?j(0,w):$==="ArrowUp"?j(0,-w):$==="ArrowLeft"?j(-w,0):$==="ArrowRight"&&j(w,0)}$==="+"||B&&$==="="?(K(),o()):$==="-"||B&&$==="_"?(K(),a()):B&&$==="0"&&(K(),s(1))}),A=Ie(F=>{if((F.ctrlKey||v)&&Math.abs(F.deltaY)>Math.abs(F.deltaX)){F.stopPropagation(),s(dl(e,-F.deltaY,O),!0,...y(F));return}e>1&&(F.stopPropagation(),v||l(F.deltaX,F.deltaY))}),P=N.useCallback(F=>{const $=h.current;$.splice(0,$.length,...$.filter(G=>G.pointerId!==F.pointerId))},[]),D=N.useCallback(F=>{P(F),F.persist(),h.current.push(F)},[P]),L=Ie(F=>{var $;const G=h.current;if(F.pointerType==="mouse"&&F.buttons>1||!(!(($=f?.current)===null||$===void 0)&&$.contains(F.target)))return;e>1&&F.stopPropagation();const{timeStamp:Y}=F;if(G.length===0&&Y-c.current<(F.pointerType==="touch"?E:I)){c.current=0;const B=e>=1?e!==r?e*Math.max(r**(1/_),C):1:e!==t?e/Math.max(t**(-1/_),C):1;s(B,!1,...y(F))}else c.current=Y;if(D(F),G.length===2){const B=cl(G[0],G[1]);i.current={previousDistance:B,initialDistance:Math.max(B,1),initialZoom:e}}}),U=Ie(F=>{const $=h.current,G=$.find(Y=>Y.pointerId===F.pointerId);if($.length===2&&i.current){F.stopPropagation(),D(F);const Y=cl($[0],$[1]),B=x?i.current.initialZoom/i.current.initialDistance*Y:dl(e,Y-i.current.previousDistance,M);s(B,!0,...$.map(K=>y(K)).reduce((K,j)=>j.map((H,q)=>K[q]+H/2))),i.current.previousDistance=Y;return}e>1&&(F.stopPropagation(),G&&($.length===1&&l((G.clientX-F.clientX)/e,(G.clientY-F.clientY)/e),D(F)))}),S=N.useCallback(F=>{const $=h.current;$.length===2&&$.find(G=>G.pointerId===F.pointerId)&&(i.current=void 0),P(F)},[P]),b=N.useCallback(()=>{const F=h.current;F.splice(0,F.length),c.current=0,i.current=void 0},[]);Zs(m,L,U,S,n),N.useEffect(b,[u,b]),N.useEffect(()=>n?()=>{}:kr(b,m(Eo,T),m(To,A)),[n,m,b,T,A])}function Qu(e,t,r){const[n,o]=N.useState(1),[a,s]=N.useState(0),[l,f]=N.useState(0),h=Yu(n,a,l,r),{currentSlide:c,globalIndex:i}=Ot(),{containerRect:u,slideRect:g}=zt(),{minZoom:p,zoomInMultiplier:m}=En(),w=c&&lr(c)?c.src:void 0,C=!w||!r?.current;Ft(()=>{o(1),s(0),f(0)},[i,w]);const O=N.useCallback((M,x,y)=>{const T=y||n,A=a-(M||0),P=l-(x||0),D=(e.width*T-g.width)/2/T,L=(e.height*T-g.height)/2/T;s(Math.min(Math.abs(A),Math.max(D,0))*Math.sign(A)),f(Math.min(Math.abs(P),Math.max(L,0))*Math.sign(P))},[n,a,l,g,e.width,e.height]),v=N.useCallback((M,x,y,T)=>{const A=Ao(M+.01<t?M-.01>p?M:p:t,5);x||h(),O(y?y*(1/n-1/A):0,T?T*(1/n-1/A):0,A),o(A)},[n,p,t,O,h]),E=Ie(()=>{n>1&&(n>t&&v(t,!0),O())});Ft(E,[u.width,u.height,E]);const I=N.useCallback(()=>{const M=n*m;v(n<1&&M>1?1:M)},[n,m,v]),_=N.useCallback(()=>{const M=n/m;v(n>1&&M<1?1:M)},[n,m,v]);return{zoom:n,offsetX:a,offsetY:l,disabled:C,changeOffsets:O,changeZoom:v,zoomIn:I,zoomOut:_}}const ul=N.createContext(null),Bo=At("useZoom","ZoomControllerContext",ul);function Ju({children:e}){const[t,r]=N.useState(),{slideRect:n}=zt(),{ref:o,minZoom:a}=En(),{imageRect:s,maxZoom:l}=qu(n,t?.imageDimensions),{zoom:f,offsetX:h,offsetY:c,disabled:i,changeZoom:u,changeOffsets:g,zoomIn:p,zoomOut:m}=Qu(s,l,t?.zoomWrapperRef);Xu(f,i),Zu(f,a,l,i,p,m,u,g,t?.zoomWrapperRef);const w=N.useMemo(()=>({zoom:f,minZoom:a,maxZoom:l,offsetX:h,offsetY:c,disabled:i,zoomIn:p,zoomOut:m,changeZoom:u}),[f,a,l,h,c,i,p,m,u]);N.useImperativeHandle(o,()=>w,[w]);const C=N.useMemo(()=>({...w,setZoomWrapper:r}),[w,r]);return N.createElement(ul.Provider,{value:C},e)}const Ru=mt("ZoomIn",N.createElement(N.Fragment,null,N.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"}),N.createElement("path",{d:"M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"}))),ef=mt("ZoomOut",N.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"})),fl=N.forwardRef(function({zoomIn:t,onLoseFocus:r},n){const o=N.useRef(!1),a=N.useRef(!1),{zoom:s,minZoom:l,maxZoom:f,zoomIn:h,zoomOut:c,disabled:i}=Bo(),{render:u}=rt(),g=i||(t?s>=f:s<=l);return N.useEffect(()=>{g&&o.current&&a.current&&r(),g||(o.current=!0)},[g,r]),N.createElement(Er,{ref:n,disabled:g,label:t?"Zoom in":"Zoom out",icon:t?Ru:ef,renderIcon:t?u.iconZoomIn:u.iconZoomOut,onClick:t?h:c,onFocus:()=>{a.current=!0},onBlur:()=>{a.current=!1}})});function tf(){const e=N.useRef(null),t=N.useRef(null),{focus:r}=zt(),n=N.useCallback(s=>{var l,f;!((l=s.current)===null||l===void 0)&&l.disabled?r():(f=s.current)===null||f===void 0||f.focus()},[r]),o=N.useCallback(()=>n(e),[n]),a=N.useCallback(()=>n(t),[n]);return N.createElement(N.Fragment,null,N.createElement(fl,{zoomIn:!0,ref:e,onLoseFocus:a}),N.createElement(fl,{ref:t,onLoseFocus:o}))}function rf(){const{render:e}=rt(),t=Bo();return e.buttonZoom?N.createElement(N.Fragment,null,e.buttonZoom(t)):N.createElement(tf,null)}function nf(e){var t;return(((t=e.srcSet)===null||t===void 0?void 0:t.length)||0)>0}function of({current:e,preload:t},{type:r,source:n}){switch(r){case"fetch":return e?{current:e,preload:n}:{current:n};case"done":return n===t?{current:n}:{current:e,preload:t};default:throw new Error(Ms)}}function af(e){var t,r;const[{current:n,preload:o},a]=N.useReducer(of,{}),{slide:s,rect:l,imageFit:f,render:h,interactive:c}=e,i=s.srcSet.sort((I,_)=>I.width-_.width),u=(t=s.width)!==null&&t!==void 0?t:i[i.length-1].width,g=(r=s.height)!==null&&r!==void 0?r:i[i.length-1].height,p=Mo(s,f),m=Math.max(...i.map(I=>I.width)),w=Math.min((p?Math.max:Math.min)(l.width,u*(l.height/g)),m),C=tu(),O=Ie(()=>{var I;const _=(I=i.find(M=>M.width>=w*C))!==null&&I!==void 0?I:i[i.length-1];(!n||i.findIndex(M=>M.src===n)<i.findIndex(M=>M===_))&&a({type:"fetch",source:_.src})});Ft(O,[l.width,l.height,C,O]);const v=Ie(I=>a({type:"done",source:I})),E={WebkitTransform:c?"initial":"translateZ(0)"};return p||Object.assign(E,l.width/l.height<u/g?{width:"100%",height:"auto"}:{width:"auto",height:"100%"}),N.createElement(N.Fragment,null,o&&o!==n&&N.createElement(ir,{key:"preload",...e,offset:void 0,slide:{...s,src:o,srcSet:void 0},style:{position:"absolute",visibility:"hidden",...E},onLoad:()=>v(o),render:{...h,iconLoading:()=>null,iconError:()=>null}}),n&&N.createElement(ir,{key:"current",...e,slide:{...s,src:n,srcSet:void 0},style:E}))}function sf({render:e,slide:t,offset:r,rect:n}){var o;const[a,s]=N.useState(),l=N.useRef(null),{zoom:f,maxZoom:h,offsetX:c,offsetY:i,setZoomWrapper:u}=Bo(),g=f>1,{carousel:p,on:m}=rt(),{currentIndex:w}=Ot();Ft(()=>r===0?(u({zoomWrapperRef:l,imageDimensions:a}),()=>u(void 0)):()=>{},[r,a,u]);let C=(o=e.slide)===null||o===void 0?void 0:o.call(e,{slide:t,offset:r,rect:n,zoom:f,maxZoom:h});if(!C&&lr(t)){const O={slide:t,offset:r,rect:n,render:e,imageFit:p.imageFit,imageProps:p.imageProps,onClick:r===0?()=>{var v;return(v=m.click)===null||v===void 0?void 0:v.call(m,{index:w})}:void 0};C=nf(t)?N.createElement(af,{...O,slide:t,interactive:g,rect:r===0?{width:n.width*f,height:n.height*f}:n}):N.createElement(ir,{onLoad:v=>s({width:v.naturalWidth,height:v.naturalHeight}),...O})}return C?N.createElement("div",{ref:l,className:lt(pe(ws),pe(wr),pe(ks),g&&pe(Xd)),style:r===0?{transform:`scale(${f}) translateX(${c}px) translateY(${i}px)`}:void 0},C):null}const lf=({augment:e,addModule:t})=>{e(({zoom:r,toolbar:n,render:o,controller:a,...s})=>{const l=il(r);return{zoom:l,toolbar:Cn(n,vs,N.createElement(rf,null)),render:{...o,slide:f=>{var h;return lr(f.slide)?N.createElement(sf,{render:o,...f}):(h=o.slide)===null||h===void 0?void 0:h.call(o,f)}},controller:{...a,preventDefaultWheelY:l.scrollToZoom},...s}}),t(Mt(vs,Ju))},cf={separator:"/",container:{}},gl=e=>({...cf,...e});function df({counter:e}){const{slides:t,currentIndex:r}=Ot(),{separator:n,container:{className:o,...a},className:s,...l}=gl(e);return t.length===0?null:N.createElement("div",{className:lt(pe("counter"),o||s),...l,...a,"aria-hidden":!0},r+1," ",n," ",t.length)}function uf({augment:e,addChild:t}){e(({counter:r,...n})=>({counter:gl(r),...n})),t(Wr,Mt(Gd,df))}const ff={autoplay:!1,delay:3e3,ref:null},hl=e=>({...ff,...e}),pl=N.createContext(null),gf=At("useSlideshow","SlideshowContext",pl);function hf({slideshow:e,carousel:{finite:t},on:r,children:n}){const{autoplay:o,delay:a,ref:s}=hl(e),l=N.useRef(o),[f,h]=N.useState(o),c=N.useRef(void 0),i=N.useRef(void 0),{slides:u,currentIndex:g}=Ot(),{setTimeout:p,clearTimeout:m}=Zr(),{subscribe:w}=_r(),{next:C}=zt(),{setAutoPlaying:O}=$o();N.useEffect(()=>O(f),[f,O]);const v=u.length===0||t&&g===u.length-1,E=N.useCallback(()=>{!f&&!v&&h(!0)},[f,v]),I=N.useCallback(()=>{f&&h(!1)},[f]),_=N.useCallback(()=>{m(c.current),c.current=void 0},[m]),M=Ie(()=>{_(),!(!f||v||i.current===vr||i.current===Co)&&(c.current=p(()=>{f&&(i.current=void 0,C())},a))});N.useEffect(M,[g,f,M]),N.useEffect(()=>{f&&v&&h(!1)},[g,f,v]);const x=Ie(()=>{var A;return(A=r.slideshowStart)===null||A===void 0?void 0:A.call(r)}),y=Ie(()=>{var A;return(A=r.slideshowStop)===null||A===void 0?void 0:A.call(r)});N.useEffect(()=>{f?x():l.current&&y(),l.current=f},[f,x,y]),N.useEffect(()=>kr(_,w(Bd,()=>{i.current=vr,_()}),w(Wd,()=>{i.current=Co,_()}),w(Vd,()=>{i.current=Yr,M()}),w(Kd,()=>{i.current=Xr,M()})),[w,_,M]);const T=N.useMemo(()=>({playing:f,disabled:v,play:E,pause:I}),[f,v,E,I]);return N.useImperativeHandle(s,()=>T,[T]),N.createElement(pl.Provider,{value:T},n)}const pf=mt("Play",N.createElement("path",{d:"M8 5v14l11-7z"})),mf=mt("Pause",N.createElement("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"}));function bf(){const{playing:e,disabled:t,play:r,pause:n}=gf(),{render:o}=rt(),a=Ys(zt().focus,t);return o.buttonSlideshow?N.createElement(N.Fragment,null,o.buttonSlideshow({playing:e,disabled:t,play:r,pause:n})):N.createElement(Er,{label:e?"Pause":"Play",icon:e?mf:pf,renderIcon:e?o.iconSlideshowPause:o.iconSlideshowPlay,onClick:e?n:r,disabled:t,...a})}function yf({augment:e,addModule:t}){e(({slideshow:r,toolbar:n,...o})=>({toolbar:Cn(n,ys,N.createElement(bf,null)),slideshow:hl(r),...o})),t(Mt(ys,hf))}const vf={auto:!1,ref:null},ml=e=>({...vf,...e}),bl=N.createContext(null),wf=At("useFullscreen","FullscreenContext",bl);function xf({fullscreen:e,on:t,children:r}){const{auto:n,ref:o}=ml(e),a=N.useRef(null),[s,l]=N.useState(),[f,h]=N.useState(!1),c=N.useRef(!1),{getOwnerDocument:i}=Sr();Ft(()=>{var v,E,I,_;const M=i();l(!((_=(I=(E=(v=M.fullscreenEnabled)!==null&&v!==void 0?v:M.webkitFullscreenEnabled)!==null&&E!==void 0?E:M.mozFullScreenEnabled)!==null&&I!==void 0?I:M.msFullscreenEnabled)!==null&&_!==void 0&&_))},[i]);const u=N.useCallback(()=>{var v;const E=i(),I=E.fullscreenElement||E.webkitFullscreenElement||E.mozFullScreenElement||E.msFullscreenElement;return((v=I?.shadowRoot)===null||v===void 0?void 0:v.fullscreenElement)||I},[i]),g=N.useCallback(()=>{const v=a.current;try{v.requestFullscreen?v.requestFullscreen().catch(()=>{}):v.webkitRequestFullscreen?v.webkitRequestFullscreen():v.mozRequestFullScreen?v.mozRequestFullScreen():v.msRequestFullscreen&&v.msRequestFullscreen()}catch{}},[]),p=N.useCallback(()=>{if(!u())return;const v=i();try{v.exitFullscreen?v.exitFullscreen().catch(()=>{}):v.webkitExitFullscreen?v.webkitExitFullscreen():v.mozCancelFullScreen?v.mozCancelFullScreen():v.msExitFullscreen&&v.msExitFullscreen()}catch{}},[u,i]);N.useEffect(()=>{const v=i(),E=()=>{h(u()===a.current)};return kr(...["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange"].map(I=>(v.addEventListener(I,E),()=>v.removeEventListener(I,E))))},[u,i]);const m=Ie(()=>{var v;return(v=t.enterFullscreen)===null||v===void 0?void 0:v.call(t)}),w=Ie(()=>{var v;return(v=t.exitFullscreen)===null||v===void 0?void 0:v.call(t)});N.useEffect(()=>{f&&(c.current=!0),c.current&&(f?m:w)()},[f,m,w]);const C=Ie(()=>{var v;return(v=n?g:null)===null||v===void 0||v(),p});N.useEffect(C,[C]);const O=N.useMemo(()=>({fullscreen:f,disabled:s,enter:g,exit:p}),[f,s,g,p]);return N.useImperativeHandle(o,()=>O,[O]),N.createElement("div",{ref:a,className:lt(pe(Vr),pe(ws))},N.createElement(bl.Provider,{value:O},r))}const kf=mt("EnterFullscreen",N.createElement("path",{d:"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"})),Cf=mt("ExitFullscreen",N.createElement("path",{d:"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"}));function Sf(){var e;const{fullscreen:t,disabled:r,enter:n,exit:o}=wf(),{render:a}=rt();return r?null:a.buttonFullscreen?N.createElement(N.Fragment,null,(e=a.buttonFullscreen)===null||e===void 0?void 0:e.call(a,{fullscreen:t,disabled:r,enter:n,exit:o})):N.createElement(Er,{disabled:r,label:t?"Exit Fullscreen":"Enter Fullscreen",icon:t?Cf:kf,renderIcon:t?a.iconExitFullscreen:a.iconEnterFullscreen,onClick:t?o:n})}function _f({augment:e,contains:t,addParent:r}){e(({fullscreen:n,toolbar:o,...a})=>({toolbar:Cn(o,Vr,N.createElement(Sf,null)),fullscreen:ml(n),...a})),r(t(Kr)?Kr:Wr,Mt(Vr,xf))}const dr={ref:null,position:"bottom",width:120,height:80,border:1,borderRadius:4,padding:4,gap:16,imageFit:"contain",vignette:!0,hidden:!1,showToggle:!1},Wo=e=>({...dr,...e});function yl(){const{thumbnails:e}=rt();return Wo(e)}const ur=e=>or(Kr,e),nt=e=>ur(or("thumbnail",e)),Ef=mt("VideoThumbnail",N.createElement("path",{d:"M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"})),Tf=mt("UnknownThumbnail",N.createElement("path",{d:"M23 18V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zM8.5 12.5l2.5 3.01L14.5 11l4.5 6H5l3.5-4.5z"}));function Nf({slide:e,render:t,rect:r,imageFit:n}){var o;const a=(o=t.thumbnail)===null||o===void 0?void 0:o.call(t,{slide:e,render:t,rect:r,imageFit:n});if(a)return a;const s={render:t,rect:r,imageFit:n};if(e.thumbnail)return N.createElement(ir,{slide:{src:e.thumbnail},...s});if(lr(e))return N.createElement(ir,{slide:e,...s});const l=pe(nt(wn));return e.type==="video"?N.createElement(N.Fragment,null,e.poster&&N.createElement(ir,{slide:{src:e.poster},...s}),N.createElement(Ef,{className:l})):N.createElement(Tf,{className:l})}const Af=ar("active"),Vo=ar("fadein"),Ko=ar("fadeout"),Mf=ar("placeholder"),vl="delay",wl="duration";function Of({slide:e,index:t,onClick:r,fadeIn:n,fadeOut:o,placeholder:a,onLoseFocus:s}){const l=N.useRef(null),{render:f,styles:h,labels:c}=rt(),{slides:i,globalIndex:u}=Ot(),{getOwnerDocument:g}=Sr(),{width:p,height:m,imageFit:w}=yl(),C={width:p,height:m},O=t===u,v=Ie(s);return N.useEffect(()=>{o&&g().activeElement===l.current&&v()},[o,v,g]),N.createElement("button",{ref:l,type:"button",className:lt(pe(wr),pe(nt()),O&&pe(nt(Af())),n&&pe(nt(Vo())),o&&pe(nt(Ko())),a&&pe(nt(Mf()))),style:{...n?{[We(nt(Vo(wl)))]:`${n.duration}ms`,[We(nt(Vo(vl)))]:`${n.delay}ms`}:null,...o?{[We(nt(Ko(wl)))]:`${o.duration}ms`,[We(nt(Ko(vl)))]:`${o.delay}ms`}:null,...h.thumbnail},onClick:r,"aria-current":O?!0:void 0,"aria-label":Is(c,i,t)},e&&Nf({slide:e,render:f,rect:C,imageFit:w}))}function xl(e){return["top","bottom"].includes(e)}function kl(e,t){return t+2*(e.border+e.padding)+e.gap}function If(e){const{thumbnail:t,poster:r}=e||{thumbnail:"placeholder"};return typeof t=="string"&&t||typeof r=="string"&&r||e&&Ds(e)||void 0}function Cl({visible:e,containerRef:t}){const r=N.useRef(null),n=_n(),{publish:o,subscribe:a}=_r(),{carousel:s,styles:l,labels:f}=rt(),{slides:h,globalIndex:c,animation:i}=Ot(),{registerSensors:u,subscribeSensors:g}=Xs();tl(g);const p=yl(),{position:m,width:w,height:C,border:O,borderStyle:v,borderColor:E,borderRadius:I,padding:_,gap:M,vignette:x}=p,y=i?.duration!==void 0&&i?.increment||0,T=i?.duration||0,{prepareAnimation:A}=Uo(r,S=>({keyframes:xl(m)?[{transform:`translateX(${(n?-1:1)*kl(p,w)*y+S}px)`},{transform:"translateX(0)"}]:[{transform:`translateY(${kl(p,C)*y+S}px)`},{transform:"translateY(0)"}],duration:T,easing:i?.easing})),P=Ie(()=>{let S=0;if(t.current&&r.current){const b=t.current.getBoundingClientRect(),F=r.current.getBoundingClientRect();S=xl(m)?F.left-b.left-(b.width-F.width)/2:F.top-b.top-(b.height-F.height)/2}A(S)});N.useEffect(()=>kr(a(_o,P)),[a,P]);const D=Ls(s,h),L=[];if(Io(h))for(let S=c-D-Math.abs(y);S<=c+D+Math.abs(y);S+=1){const F=s.finite&&(S<0||S>h.length-1)||y<0&&S<c-D||y>0&&S>c+D?null:Do(h,S),$=[`${S}`,If(F)].filter(Boolean).join("|");L.push({key:$,index:S,slide:F})}const U=S=>()=>{S>c?o(er,{count:S-c}):S<c&&o(Rt,{count:c-S})};return N.createElement("div",{className:lt(pe(ur("container")),pe(wr)),style:{...e?null:{display:"none"},...w!==dr.width?{[We(nt("width"))]:`${w}px`}:null,...C!==dr.height?{[We(nt("height"))]:`${C}px`}:null,...O!==dr.border?{[We(nt("border"))]:`${O}px`}:null,...v?{[We(nt("border_style"))]:v}:null,...E?{[We(nt("border_color"))]:E}:null,...I!==dr.borderRadius?{[We(nt("border_radius"))]:`${I}px`}:null,..._!==dr.padding?{[We(nt("padding"))]:`${_}px`}:null,...M!==dr.gap?{[We(nt("gap"))]:`${M}px`}:null,...l.thumbnailsContainer}},N.createElement("nav",{ref:r,style:l.thumbnailsTrack,className:lt(pe(ur("track")),pe(wr)),"aria-label":sr(f,"Thumbnails"),tabIndex:-1,...u},L.map(({key:S,index:b,slide:F})=>{const $=T/Math.abs(y||1),G=y>0&&b>c+D-y&&b<=c+D||y<0&&b<c-D-y&&b>=c-D?{duration:$,delay:((y>0?b-(c+D-y):c-D-y-b)-1)*$}:void 0,Y=y>0&&b<c-D||y<0&&b>c+D?{duration:$,delay:(y>0?y-(c-D-b):-y-(b-(c+D)))*$}:void 0;return N.createElement(Of,{key:S,index:b,slide:F,fadeIn:G,fadeOut:Y,placeholder:!F,onClick:U(b),onLoseFocus:()=>{var B;return(B=r.current)===null||B===void 0?void 0:B.focus()}})})),x&&N.createElement("div",{className:pe(ur("vignette"))}))}const Sl=N.createContext(null),Df=At("useThumbnails","ThumbnailsContext",Sl);function Lf({children:e,...t}){const{ref:r,position:n,hidden:o}=Wo(t.thumbnails),[a,s]=N.useState(!o),l=N.useRef(null),f=N.useMemo(()=>({visible:a,show:()=>s(!0),hide:()=>s(!1)}),[a]);return N.useImperativeHandle(r,()=>f,[f]),N.createElement(Hs,{...t},N.createElement(Sl.Provider,{value:f},N.createElement("div",{ref:l,className:lt(pe(ur()),pe(ur(`${n}`)))},["start","top"].includes(n)&&N.createElement(Cl,{containerRef:l,visible:a}),N.createElement("div",{className:pe(ur("wrapper"))},e),["end","bottom"].includes(n)&&N.createElement(Cl,{containerRef:l,visible:a}))))}const _l=()=>N.createElement(N.Fragment,null,N.createElement("path",{strokeWidth:2,stroke:"currentColor",strokeLinejoin:"round",fill:"none",d:"M3 5l18 0l0 14l-18 0l0-14z"}),N.createElement("path",{d:"M5 14h4v3h-4zM10 14h4v3h-4zM15 14h4v3h-4z"})),Pf=mt("ThumbnailsVisible",_l()),$f=gu("ThumbnailsHidden",_l());function Ff(){const{visible:e,show:t,hide:r}=Df(),{render:n}=rt();return n.buttonThumbnails?N.createElement(N.Fragment,null,n.buttonThumbnails({visible:e,show:t,hide:r})):N.createElement(Er,{label:e?"Hide thumbnails":"Show thumbnails",icon:e?Pf:$f,renderIcon:e?n.iconThumbnailsVisible:n.iconThumbnailsHidden,onClick:e?r:t})}function Uf({augment:e,contains:t,append:r,addParent:n}){e(({thumbnails:a,toolbar:s,...l})=>{const f=Wo(a);return{toolbar:Cn(s,Kr,f.showToggle?N.createElement(Ff,null):null),thumbnails:f,...l}});const o=Mt(Kr,Lf);t(Vr)?r(Vr,o):n(Wr,o)}const Ct=({className:e="",children:t,style:r,...n})=>d.jsx("i",{className:e,style:r,...n,children:t}),Yo=window.G7Core,Tn=(e,t)=>Yo?.t?.(e,t)??e,zf=async(e,t)=>{try{const r=await Yo.api.get(e,{responseType:"blob"});if(r){const n=URL.createObjectURL(r),o=document.createElement("a");o.href=n,o.download=t,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(n)}}catch(r){console.error("Failed to download file:",r),Yo?.toast?.error?.(Tn("common.download_failed"))}},Hf=(e,t)=>{const r=document.createElement("a");r.href=e,r.download=t,r.target="_blank",document.body.appendChild(r),r.click(),document.body.removeChild(r)},El=async e=>{const t=e.downloadUrl||e.src,r=e.filename||e.title||"image";e.downloadRequiresAuth?await zf(t,r):Hf(t,r)},Gf=({image:e,index:t,onDownload:r})=>{const[n,o]=k.useState(!1),a=async s=>{if(s.stopPropagation(),r){r(e,t);return}o(!0);try{await El(e)}finally{o(!1)}};return d.jsx(Q,{type:"button",onClick:a,disabled:n,className:"yarl__button flex items-center justify-center","aria-label":Tn("common.download"),title:Tn("common.download"),children:n?d.jsx(Ct,{className:"fa-solid fa-spinner fa-spin text-white"}):d.jsx(Ct,{className:"fa-solid fa-download text-white"})})},Tr=({images:e,isOpen:t,onClose:r,startIndex:n=0,enableZoom:o=!0,enableSlideshow:a=!1,enableFullscreen:s=!0,showCounter:l=!0,showDownload:f=!0,showThumbnails:h=!0,onDownload:c,editorAttrs:i,id:u})=>{const[g,p]=k.useState(n),m=k.useRef(n),w=e.map(v=>({src:v.src,title:v.title,description:v.description})),C=[];o&&C.push(lf),a&&C.push(yf),s&&C.push(_f),l&&C.push(uf),h&&C.push(Uf);const O=e[g];if(k.useEffect(()=>{m.current=g},[g]),i){const v=(e??[]).slice(0,4);return d.jsx(W,{id:u,className:"grid grid-cols-2 gap-2 p-3 rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800",...i,children:v.length>0?v.map((E,I)=>d.jsx(Lt,{src:E.thumbnail||E.src,alt:E.title||"",className:"w-full aspect-square object-cover rounded bg-gray-200 dark:bg-gray-700"},I)):d.jsxs(W,{className:"col-span-2 flex items-center justify-center py-6 text-sm text-gray-400 dark:text-gray-500",children:[d.jsx(Ct,{className:"fa-regular fa-images mr-2"}),Tn("editor.component.image_gallery")]})})}return d.jsx(Wu,{open:t,close:r,slides:w,index:g,plugins:C,on:{view:({index:v})=>{v!==m.current&&p(v)}},zoom:{maxZoomPixelRatio:3,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:300,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,scrollToZoom:!0},carousel:{finite:!0,preload:2,padding:"16px",spacing:"30%"},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},controller:{closeOnBackdropClick:!0,closeOnPullDown:!0,closeOnPullUp:!0},thumbnails:{position:"bottom",width:120,height:80,border:2,borderRadius:4,padding:4,gap:16,showToggle:!1,vignette:!0},toolbar:{buttons:[f&&O&&d.jsx(Gf,{image:O,index:g,onDownload:c},"download"),"close"].filter(Boolean)},styles:{container:{backgroundColor:"rgba(0, 0, 0, 0.9)"}}})},Tl=()=>{const[e,t]=k.useState(!1),[r,n]=k.useState([]),[o,a]=k.useState(0),s=k.useCallback((f,h=0)=>{n(f),a(h),t(!0)},[]),l=k.useCallback(()=>{t(!1)},[]);return{isOpen:e,openGallery:s,closeGallery:l,galleryProps:{images:r,isOpen:e,onClose:l,startIndex:o}}};Tr.displayName="ImageGallery";const Nl=Object.freeze(Object.defineProperty({__proto__:null,ImageGallery:Tr,default:Tr,executeImageDownload:El,useImageGallery:Tl},Symbol.toStringTag,{value:"Module"})),jf=window.G7Core,Xo=(e,t)=>jf?.t?.(e,t)??e,Nn=e=>e.url??e.download_url,An=({images:e=[],className:t="",id:r,editorAttrs:n})=>{const[o,a]=k.useState(0),{openGallery:s,galleryProps:l}=Tl(),f=k.useMemo(()=>e.map(c=>({src:Nn(c),title:c.alt_text_current??"",thumbnail:Nn(c),downloadUrl:c.download_url,filename:c.alt_text_current??`image-${c.id}`})),[e]);if(!e||e.length===0)return d.jsx(W,{className:`flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-lg aspect-square ${t}`,id:r,...n,children:d.jsxs(W,{className:"text-center text-gray-400 dark:text-gray-500",children:[d.jsx(de,{name:"image",size:"3x",className:"mb-3 opacity-50"}),d.jsx(W,{className:"text-sm",children:Xo("shop.no_image")})]})});const h=e[o]??e[0];return d.jsxs(W,{id:r,className:t,...n,children:[d.jsx(W,{className:"relative overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-700 aspect-square mb-3",children:d.jsx(Q,{type:"button",className:"w-full h-full cursor-zoom-in block",onClick:()=>s(f,o),"aria-label":Xo("shop.view_image"),children:d.jsx(Lt,{src:Nn(h),alt:h.alt_text_current??"",className:"w-full h-full object-contain"})})}),e.length>1&&d.jsx(W,{className:"flex gap-2 overflow-x-auto pb-1",children:e.map((c,i)=>d.jsx(Q,{type:"button",className:`flex-shrink-0 w-16 h-16 rounded-md overflow-hidden border-2 transition-colors cursor-pointer ${i===o?"border-gray-900 dark:border-white":"border-gray-200 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-400"}`,onClick:()=>a(i),"aria-label":c.alt_text_current??`${Xo("shop.image")} ${i+1}`,children:d.jsx(Lt,{src:Nn(c),alt:c.alt_text_current??"",className:"w-full h-full object-cover"})},c.id))}),d.jsx(Tr,{...l})]})};An.displayName="ProductImageViewer";const Bf=Object.freeze(Object.defineProperty({__proto__:null,ProductImageViewer:An,default:An},Symbol.toStringTag,{value:"Module"})),Al=({value:e,min:t=1,max:r=999,onChange:n,size:o="md",disabled:a=!1,className:s="",id:l,editorAttrs:f})=>{const c={sm:{container:"h-8",button:"w-8 h-8 text-sm",input:"w-10 text-sm"},md:{container:"h-10",button:"w-10 h-10",input:"w-14"},lg:{container:"h-12",button:"w-12 h-12 text-lg",input:"w-16 text-lg"}}[o],i=()=>{e>t&&!a&&n(e-1)},u=()=>{e<r&&!a&&n(e+1)},g=C=>{const O=parseInt(C.target.value,10);if(!isNaN(O)){const v=Math.max(t,Math.min(r,O));n(v)}},p=C=>{const O=parseInt(C.target.value,10);isNaN(O)||O<t?n(t):O>r&&n(r)},m=a||e<=t,w=a||e>=r;return d.jsxs(W,{className:`inline-flex items-center border border-gray-300 dark:border-gray-600 rounded-lg ${c.container} ${s}`,id:l,...f,children:[d.jsx(Q,{type:"button",onClick:i,disabled:m,className:`${c.button} flex items-center justify-center text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-l-lg transition-colors`,"aria-label":"수량 감소",children:d.jsx(de,{name:"minus",className:"w-4 h-4"})}),d.jsx(xt,{type:"number",value:e,onChange:g,onBlur:p,min:t,max:r,disabled:a,className:`${c.input} text-center border-x border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`,"aria-label":"수량"}),d.jsx(Q,{type:"button",onClick:u,disabled:w,className:`${c.button} flex items-center justify-center text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-r-lg transition-colors`,"aria-label":"수량 증가",children:d.jsx(de,{name:"plus",className:"w-4 h-4"})})]})},Wf=Object.freeze(Object.defineProperty({__proto__:null,default:Al},Symbol.toStringTag,{value:"Module"})),Vf=window.G7Core?.createLogger?.("Comp:PostReactions")??{log:(...e)=>console.log("[Comp:PostReactions]",...e),warn:(...e)=>console.warn("[Comp:PostReactions]",...e),error:(...e)=>console.error("[Comp:PostReactions]",...e)},Ml=(e,t)=>window.G7Core?.t?.(e,t)??e,Kf=[{type:"like",emoji:"👍",labelKey:"board.reactions.like",activeColor:"bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"},{type:"funny",emoji:"😂",labelKey:"board.reactions.funny",activeColor:"bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"},{type:"agree",emoji:"👌",labelKey:"board.reactions.agree",activeColor:"bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"},{type:"thanks",emoji:"🙏",labelKey:"board.reactions.thanks",activeColor:"bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"},{type:"wow",emoji:"😮",labelKey:"board.reactions.wow",activeColor:"bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"}],Ol=({reactions:e,userReaction:t,postId:r,onReact:n,size:o="md",className:a="",id:s,editorAttrs:l})=>{const f={sm:"px-2 py-1 text-xs gap-1",md:"px-3 py-1.5 text-sm gap-1.5",lg:"px-4 py-2 text-base gap-2"},h={sm:"text-sm",md:"text-base",lg:"text-lg"},c=u=>e.find(p=>p.type===u)?.count??0,i=async u=>{if(n){n(u);return}try{if(!(await fetch(`/api/posts/${r}/reactions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:u}),credentials:"include"})).ok)throw new Error("리액션 처리 실패");window.location.reload()}catch(g){Vf.error("리액션 오류:",g)}};return d.jsx(W,{className:`flex flex-wrap gap-2 ${a}`,id:s,...l,children:Kf.map(u=>{const g=c(u.type),p=t===u.type;return d.jsxs(Q,{onClick:()=>i(u.type),className:` + `,children:[d.jsx(de,{name:Je.Moon,className:"w-5 h-5 text-gray-600 dark:text-gray-400"}),d.jsx(R,{className:"flex-1 text-left text-gray-900 dark:text-white",children:o}),f==="dark"&&d.jsx(de,{name:Je.Check,className:"w-4 h-4 text-blue-600 dark:text-blue-400 ml-auto"})]})]})})]})},Rc=Object.freeze(Object.defineProperty({__proto__:null,ThemeToggle:hn,default:hn},Symbol.toStringTag,{value:"Module"})),Ja={xs:"w-6 h-6 text-xs",sm:"w-8 h-8 text-sm",md:"w-10 h-10 text-base",lg:"w-12 h-12 text-lg",xl:"w-16 h-16 text-xl","2xl":"w-24 h-24 text-2xl","3xl":"w-32 h-32 text-3xl","4xl":"w-40 h-40 text-4xl","5xl":"w-48 h-48 text-5xl"},ed="rounded-full overflow-hidden flex-shrink-0 border border-gray-200 dark:border-gray-700",td="grayscale opacity-50",co={normal:"bg-gradient-to-br from-blue-500 to-purple-600",withdrawn:"bg-gray-300 dark:bg-gray-600",guest:"bg-gray-200 dark:bg-gray-700"},Ur=({author:e,name:t,avatar:r,size:n="md",className:o="",text:a,isWithdrawn:s=!1,isGuest:l=!1,id:f,editorAttrs:h})=>{const c=r??e?.avatar,i=a??t??e?.name??"?",u=s||e?.status==="withdrawn",g=l||e?.is_guest||!1,p=k.useMemo(()=>[Ja[n]||Ja.md,ed,u?td:"",o].filter(Boolean).join(" "),[n,u,o]),m=g?co.guest:u?co.withdrawn:co.normal,x=()=>g?d.jsx(W,{className:`w-full h-full flex items-center justify-center ${m}`,children:d.jsx(de,{name:"user",className:"text-gray-400 dark:text-gray-500"})}):u?d.jsx(W,{className:`w-full h-full flex items-center justify-center ${m}`,children:d.jsx(de,{name:"user",className:"text-gray-500 dark:text-gray-400"})}):d.jsx(W,{className:`w-full h-full flex items-center justify-center text-white font-semibold ${m}`,children:i.charAt(0).toUpperCase()});return d.jsx(W,{className:p,id:f,...h,children:c&&!g&&!u?d.jsx(Lt,{src:c,alt:i,className:"w-full h-full object-cover"}):x()})},rd=Object.freeze(Object.defineProperty({__proto__:null,Avatar:Ur},Symbol.toStringTag,{value:"Module"})),nd=window.G7Core?.createLogger?.("Comp:SlotContainer")??{log:(...e)=>console.log("[Comp:SlotContainer]",...e),warn:(...e)=>console.warn("[Comp:SlotContainer]",...e),error:(...e)=>console.error("[Comp:SlotContainer]",...e)};function Ra(e,t){return t&&e&&typeof e.id=="string"&&e.id.length>0?{...e,id:`${e.id}__${t}`}:e}const pn=({slotId:e,className:t="",emptyContent:r,style:n,id:o,children:a})=>{const[s,l]=k.useState([]),[,f]=k.useState({}),h=k.useCallback(()=>{const p=window.__slotContextValue;if(p)return p;const m=window.G7Core;return m&&m.getSlotContext?.()||null},[]),c=k.useCallback(()=>{const p=h();p&&p.isEnabled&&l(p.getSlotComponents(e))},[e,h]);k.useEffect(()=>{const p=h();return!p||!p.isEnabled?void 0:(c(),p.subscribeToSlot(e,()=>{c(),f({})}))},[e,h,c]);const i=k.useCallback(()=>{const p=window.G7Core;return p?p.getDynamicRenderer?.():null},[]);if(s.length===0)return r?d.jsx(W,{className:t,style:n,id:o,children:r}):a?d.jsx(W,{className:t,style:n,id:o,children:a}):null;const u=i(),g=window.G7Core;return!u||!g?(nd.error("G7Core or DynamicRenderer not available"),null):d.jsxs(W,{className:t,style:n,id:o,children:[s.map(p=>{const{componentDef:m,dataContext:x,parentFormContext:C,getParentComponentContext:O,translationContext:v}=p,E=Ra(m,o);return d.jsx(u,{componentDef:E,dataContext:x,translationContext:v,registry:g.getComponentRegistry(),bindingEngine:g.getDataBindingEngine(),translationEngine:g.getTranslationEngine(),actionDispatcher:g.getActionDispatcher(),parentComponentContext:O?.(),parentFormContextProp:C,isRootRenderer:!1},E.id||`slot-${e}-${p.order}`)}),a]})},od=Object.freeze(Object.defineProperty({__proto__:null,SlotContainer:pn,default:pn,scopeSlotChildDef:Ra},Symbol.toStringTag,{value:"Module"})),mn=({notifications:e=[],unreadCount:t=0,hasMore:r=!1,loading:n=!1,titleText:o="알림",emptyText:a="알림이 없습니다.",markAllReadText:s="모두 읽음",deleteAllText:l="모두 삭제",unreadOnlyText:f="안 읽은 알림만",unreadOnly:h=!1,onNotificationClick:c,onClose:i,onLoadMore:u,onMarkAllRead:g,onDeleteAll:p,onDelete:m,onUnreadOnlyToggle:x,dropdownAlign:C="right",className:O="",id:v,editorAttrs:E})=>{const[I,_]=k.useState(!1),M=k.useRef(null),w=k.useRef(null),y=k.useRef(null),T=k.useRef(null),A=k.useRef(new Set),P=k.useRef(u),D=k.useRef(r),L=k.useRef(n),U=k.useRef(e.length),S=k.useRef(-1);k.useEffect(()=>{P.current=u,D.current=r,L.current=n,U.current=e.length}),k.useEffect(()=>{if(!T.current||!y.current||!I)return;S.current=-1;const G=new IntersectionObserver(([Y])=>{Y.isIntersecting&&(L.current||!D.current||S.current!==U.current&&(S.current=U.current,P.current?.()))},{root:y.current,threshold:.1});return G.observe(T.current),()=>G.disconnect()},[I]),k.useEffect(()=>{if(!y.current||!I)return;const G=new IntersectionObserver(B=>{B.forEach(K=>{const j=K.target.getAttribute("data-notification-id"),H=K.target.getAttribute("data-unread")==="true";j&&H&&K.isIntersecting&&A.current.add(j)})},{root:y.current,threshold:.5});return y.current.querySelectorAll("[data-notification-id]").forEach(B=>G.observe(B)),()=>G.disconnect()},[I,e]);const b=t>0?t:e.filter(G=>!G.read).length,F=k.useCallback(()=>{if(I){if(b>0){const G=Array.from(A.current).filter(Y=>{const B=e.find(K=>String(K.id)===String(Y));return B&&!B.read});G.length>0&&i?.(G)}A.current.clear(),_(!1)}},[I,i,b,e]);k.useEffect(()=>{if(!I)return;const G=Y=>{M.current&&!M.current.contains(Y.target)&&F()};return document.addEventListener("mousedown",G),()=>document.removeEventListener("mousedown",G)},[I,F]);const $=k.useCallback(()=>{I?F():_(!0)},[I,F]);return k.useLayoutEffect(()=>{const G=w.current;if(!I||!G)return;G.style.transform="";const Y=G.getBoundingClientRect(),B=typeof window<"u"?window.innerWidth:0;if(Y.width===0&&Y.height===0)return;const K=8;let j=0;Y.right>B-K?j=-(Y.right-(B-K)):Y.left<K&&(j=K-Y.left),j!==0&&(G.style.transform=`translateX(${j}px)`)},[I,C,e.length]),d.jsxs(W,{ref:M,className:`relative ${O}`,id:v,...E,children:[d.jsxs(Q,{onClick:$,className:"relative p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors cursor-pointer","aria-label":o,children:[d.jsx(de,{name:Je.Bell,className:"w-5 h-5"}),b>0&&d.jsx(R,{className:"absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center text-xs font-bold text-white bg-red-500 dark:bg-red-600 rounded-full",children:b>99?"99+":b})]}),I&&d.jsxs(W,{ref:w,className:`absolute ${C==="left"?"left-0":"right-0"} mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50`,children:[d.jsxs(W,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between gap-2",children:[d.jsxs(W,{className:"flex items-center gap-3 min-w-0",children:[d.jsx(R,{className:"font-semibold text-gray-900 dark:text-white",children:o}),x&&d.jsxs(W,{className:"flex items-center gap-1.5 cursor-pointer select-none",onClick:()=>x(!h),children:[d.jsx(xt,{type:"checkbox",checked:h,onChange:G=>{G.stopPropagation(),x(G.target.checked)},onClick:G=>G.stopPropagation(),className:"w-3.5 h-3.5 rounded border-gray-300 dark:border-gray-600 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 cursor-pointer"}),d.jsx(R,{className:"text-xs text-gray-600 dark:text-gray-400",children:f})]})]}),d.jsxs(W,{className:"flex items-center gap-3 flex-shrink-0",children:[g&&b>0&&d.jsx(Q,{onClick:()=>{g(),A.current.clear()},className:"text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 hover:underline cursor-pointer",children:s}),p&&e.length>0&&d.jsx(Q,{onClick:()=>{p()},className:"text-xs text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:underline cursor-pointer",children:l})]})]}),d.jsx(W,{ref:y,className:"max-h-96 overflow-y-auto",children:e.length===0&&!n?d.jsx(W,{className:"px-4 py-8 text-center text-gray-500 dark:text-gray-400",children:d.jsx(R,{children:a})}):d.jsxs(d.Fragment,{children:[e.map(G=>{const Y=!G.read;return d.jsxs(W,{"data-notification-id":G.id,"data-unread":Y?"true":"false",onClick:()=>{G.onClick?.(),c?.(G),_(!1)},className:`group flex items-start gap-3 px-4 py-3 text-left border-b border-gray-100 dark:border-gray-700 transition-colors cursor-pointer ${Y?"bg-blue-50/70 dark:bg-blue-900/20 hover:bg-blue-50 dark:hover:bg-blue-900/30":"bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50 opacity-70"}`,children:[G.iconName&&d.jsx(de,{name:G.iconName,className:`w-5 h-5 mt-0.5 flex-shrink-0 ${Y?"text-blue-600 dark:text-blue-400":"text-gray-400 dark:text-gray-500"}`}),d.jsxs(W,{className:"flex-1 min-w-0",children:[d.jsx(W,{className:`text-sm truncate ${Y?"font-semibold text-gray-900 dark:text-white":"font-normal text-gray-600 dark:text-gray-400"}`,children:G.title}),d.jsx(W,{className:`text-sm mt-1 line-clamp-2 ${Y?"text-gray-700 dark:text-gray-300":"text-gray-500 dark:text-gray-500"}`,children:G.message}),d.jsx(W,{className:"text-gray-400 dark:text-gray-500 text-xs mt-1 font-mono",children:G.time})]}),d.jsxs(W,{className:"flex flex-col items-center gap-2 flex-shrink-0",children:[Y&&d.jsx(R,{className:"w-2 h-2 bg-blue-500 dark:bg-blue-400 rounded-full mt-1.5"}),m&&d.jsx(Q,{onClick:B=>{B.stopPropagation(),m(G)},className:"p-1 text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 rounded hover:bg-red-50 dark:hover:bg-red-900/20 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity","aria-label":"Delete notification",children:d.jsx(de,{name:Je.Trash,className:"w-4 h-4"})})]})]},G.id)}),r&&d.jsx(W,{ref:T,className:"px-4 py-3 text-center",children:n?d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."}):d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500"})}),n&&!r&&e.length>0&&d.jsx(W,{className:"px-4 py-2 text-center",children:d.jsx(R,{className:"text-sm text-gray-400 dark:text-gray-500",children:"..."})})]})})]})]})},ad=Object.freeze(Object.defineProperty({__proto__:null,NotificationCenter:mn,default:mn},Symbol.toStringTag,{value:"Module"})),Xe=(e,t)=>window.G7Core?.t?.(e,t)??e,pt=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},es=({logo:e,siteName:t="그누보드7",user:r,cartCount:n=0,notificationCount:o=0,boards:a=[],maxVisibleBoards:s=5,onMobileMenuOpen:l,availableLocales:f=[],currentLocale:h="ko",shopBase:c="/shop",className:i="",editorAttrs:u,notifications:g=[],notificationHasMore:p=!1,notificationLoading:m=!1,notificationUnreadOnly:x=!1,notificationTitleText:C,notificationEmptyText:O,notificationMarkAllReadText:v,notificationDeleteAllText:E,notificationUnreadOnlyText:I,onNotificationClose:_,onNotificationClick:M,onNotificationLoadMore:w,onNotificationMarkAllRead:y,onNotificationDeleteAll:T,onNotificationDelete:A,onNotificationUnreadOnlyToggle:P})=>{const[D,L]=k.useState(""),[U,S]=k.useState(!1),[b,F]=k.useState(!1),[$,G]=k.useState(!1),[Y,B]=k.useState({top:0,left:0}),[K,j]=k.useState(window.location.pathname),H=k.useRef(null),q=k.useRef(null),ue=k.useRef(null),fe=window.G7Core?.useResponsive,ee=fe?.(),xe=ee?ee.width<768:typeof window<"u"&&window.innerWidth<768;k.useEffect(()=>{const V=()=>j(window.location.pathname);window.addEventListener("popstate",V);const re=history.pushState;return history.pushState=function(...oe){re.apply(this,oe),j(window.location.pathname)},()=>{window.removeEventListener("popstate",V),history.pushState=re}},[]);const he=(V,re=!1)=>re?K===V:K===V||K.startsWith(V+"/"),Ge=()=>{const V=c==="/"?"":c;return K.startsWith(`${V}/products`)||K.startsWith(`${V}/cart`)||K.startsWith(`${V}/checkout`)||K.startsWith(`${V}/category`)},De=V=>{const re="px-3 py-2 text-sm font-medium whitespace-nowrap cursor-pointer rounded-lg transition-colors";return V?`${re} bg-gray-900 text-white dark:bg-white dark:text-gray-900`:`${re} text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-800`};k.useEffect(()=>{const V=re=>{H.current&&!H.current.contains(re.target)&&S(!1),q.current&&!q.current.contains(re.target)&&F(!1),ue.current&&!ue.current.contains(re.target)&&G(!1)};return document.addEventListener("mousedown",V),()=>document.removeEventListener("mousedown",V)},[]);const ye=a.slice(0,s),Me=a.slice(s),Ce=V=>{V.preventDefault(),D.trim()&&pt(`/search?q=${encodeURIComponent(D)}`)},we=async()=>{window.G7Core?.dispatch?.({handler:"logout"})},Fe=V=>{window.G7Core?.dispatch?.({handler:"setLocale",target:V}),G(!1)},Le=V=>window.G7Core?.state?.get?.("_global.appConfig.localeNames")?.[V]??(V==="ko"?"한국어":V==="en"?"English":V==="ja"?"日本語":V==="zh"?"中文":V==="es"?"Español":V==="fr"?"Français":V==="de"?"Deutsch":V.toUpperCase());return d.jsxs(Qc,{...u??{},className:`sticky top-0 z-50 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 ${i}`,children:[d.jsx(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs(W,{className:"flex items-center justify-between h-16",children:[d.jsx(Q,{onClick:()=>pt("/"),className:"flex items-center gap-2 flex-shrink-0 cursor-pointer",children:e?d.jsx(Lt,{src:e,alt:t,className:"h-8"}):d.jsx(R,{className:"text-xl font-bold text-gray-900 dark:text-white",children:t})}),!xe&&d.jsx(so,{onSubmit:Ce,className:"flex flex-1 max-w-lg mx-8",children:d.jsxs(W,{className:"relative w-full",children:[d.jsx(xt,{type:"text",value:D,onChange:V=>L(V.target.value),placeholder:Xe("common.search_placeholder"),className:"w-full px-4 py-2 pl-10 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"}),d.jsx(de,{name:"search",className:"absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500"})]})}),d.jsxs(W,{className:"flex items-center gap-2",children:[d.jsx(hn,{autoText:Xe("common.theme.auto"),lightText:Xe("common.theme.light"),darkText:Xe("common.theme.dark")}),r?.uuid&&d.jsx(mn,{notifications:g,unreadCount:o,hasMore:p,loading:m,unreadOnly:x,titleText:C??Xe("mypage.notifications.title"),emptyText:O??Xe("mypage.notifications.empty"),markAllReadText:v??Xe("mypage.notifications.mark_all_read"),deleteAllText:E??Xe("mypage.notifications.delete_all"),unreadOnlyText:I??Xe("mypage.notifications.unread_only"),onClose:_,onNotificationClick:M,onLoadMore:w,onMarkAllRead:y,onDeleteAll:T,onDelete:A,onUnreadOnlyToggle:P,dropdownAlign:"right"}),d.jsxs(Q,{onClick:()=>pt(`${c}/cart`),className:"relative p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:[d.jsx(de,{name:"shopping-cart",className:"w-5 h-5"}),n>0&&d.jsx(R,{className:"absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center text-xs bg-blue-500 text-white rounded-full",children:n>99?"99+":n})]}),f&&f.length>1&&d.jsxs(W,{ref:ue,className:"relative",children:[d.jsxs(Q,{onClick:()=>G(!$),className:"flex items-center gap-1.5 px-2.5 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg cursor-pointer transition-colors","aria-haspopup":"listbox","aria-expanded":$,"aria-label":Xe("common.language"),children:[d.jsx(de,{name:"globe",className:"w-4 h-4"}),d.jsx(R,{className:"font-medium uppercase",children:h}),d.jsx(de,{name:"chevron-down",className:"w-3 h-3"})]}),$&&d.jsx(W,{role:"listbox",className:"absolute right-0 mt-2 w-44 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50 overflow-hidden",children:f.map(V=>{const re=V===h;return d.jsxs(Q,{role:"option","aria-selected":re,onClick:()=>Fe(V),className:`w-full px-4 py-2.5 text-left text-sm flex items-center gap-3 cursor-pointer transition-colors ${re?"bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400":"text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"}`,children:[d.jsx(de,{name:"globe",className:"w-4 h-4"}),d.jsx(R,{className:"flex-1 font-medium",children:Le(V)}),re&&d.jsx(de,{name:"check",className:"w-4 h-4"})]},V)})})]}),d.jsx(pn,{slotId:"header_currency",id:"header_currency_slot_desktop",className:"flex items-center"}),r?.uuid?d.jsxs(W,{ref:H,className:"relative",children:[d.jsxs(Q,{onClick:()=>S(!U),className:"flex items-center gap-2 p-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white",children:[d.jsx(Ur,{avatar:r.avatar,name:r.name,size:"sm"}),d.jsx(R,{className:"hidden sm:inline text-sm font-medium",children:r.name}),d.jsx(de,{name:"chevron-down",className:"w-4 h-4"})]}),U&&d.jsxs(W,{className:"absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50",children:[d.jsx(W,{className:"px-4 py-3 border-b border-gray-200 dark:border-gray-700",children:d.jsxs(W,{className:"flex items-center gap-3",children:[d.jsx(Ur,{avatar:r.avatar,name:r.name,size:"md"}),d.jsxs(W,{children:[d.jsx(W,{className:"text-sm font-medium text-gray-900 dark:text-white",children:r.name}),d.jsx(W,{className:"text-xs text-gray-500 dark:text-gray-400",children:Xe("common.member")})]})]})}),d.jsxs(W,{className:"py-1",children:[r.is_admin&&d.jsxs(d.Fragment,{children:[d.jsxs(ao,{href:"/admin",className:"block w-full text-left px-4 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer font-medium",children:[d.jsx(de,{name:"settings",className:"inline w-4 h-4 mr-2"}),Xe("common.admin_menu")]}),d.jsx(lo,{className:"my-1 border-gray-200 dark:border-gray-700"})]}),d.jsxs(Q,{onClick:()=>{pt("/mypage"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"user",className:"inline w-4 h-4 mr-2"}),Xe("common.mypage")]}),d.jsxs(Q,{onClick:()=>{pt("/mypage/orders"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"shopping-bag",className:"inline w-4 h-4 mr-2"}),Xe("mypage.tabs.orders")]}),d.jsxs(Q,{onClick:()=>{pt("/mypage/wishlist"),S(!1)},className:"block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"heart",className:"inline w-4 h-4 mr-2"}),Xe("mypage.tabs.wishlist")]})]}),d.jsx(lo,{className:"my-1 border-gray-200 dark:border-gray-700"}),d.jsxs(Q,{onClick:we,className:"block w-full text-left px-4 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer",children:[d.jsx(de,{name:"log-out",className:"inline w-4 h-4 mr-2"}),Xe("auth.logout")]})]})]}):d.jsxs(W,{className:"flex items-center gap-1.5",children:[d.jsx(Q,{onClick:()=>pt("/login?redirect=/mypage/orders"),className:"px-2.5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:Xe("shop.guest_order_form.nav_link")}),d.jsx(Q,{onClick:()=>pt("/login"),className:"px-2.5 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:Xe("auth.login")}),d.jsx(Q,{onClick:()=>pt("/register"),className:"ml-1 px-3 py-2 text-sm font-medium text-white bg-emerald-500 rounded-lg hover:bg-emerald-600 cursor-pointer",children:Xe("auth.register_link")})]}),xe&&d.jsx(Q,{onClick:l,className:"p-2 text-gray-600 dark:text-gray-400",children:d.jsx(de,{name:"menu",className:"w-6 h-6"})})]})]})}),!xe&&d.jsx(Fr,{className:"border-t border-gray-200 dark:border-gray-800",children:d.jsx(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs(W,{className:"flex items-center gap-1 h-12 overflow-x-auto",children:[d.jsx(Q,{onClick:()=>pt("/"),className:De(he("/",!0)),"data-testid":"nav-home",children:Xe("nav.home")}),d.jsxs(Q,{onClick:()=>pt("/boards/popular"),className:`flex items-center gap-1 ${De(he("/boards/popular"))}`,"data-testid":"nav-popular",children:[d.jsx(R,{className:"text-orange-500",children:"🔥"}),Xe("nav.popular")]}),d.jsxs(Q,{onClick:()=>pt(`${c==="/"?"":c}/products`),className:`flex items-center gap-1 ${De(Ge())}`,"data-testid":"nav-shop",children:[d.jsx(R,{children:"🛒"}),Xe("nav.shop")]}),ye.map(V=>d.jsxs(Q,{onClick:()=>pt(`/board/${V.slug}`),className:De(he(`/board/${V.slug}`)),children:[V.icon&&d.jsx(R,{className:"mr-1",children:V.icon}),V.name]},V.id)),Me.length>0&&d.jsxs(W,{ref:q,className:"relative",children:[d.jsxs(Q,{onClick:()=>{if(!b&&q.current){const V=q.current.getBoundingClientRect();B({top:V.bottom+4,left:V.left})}F(!b)},className:"flex items-center gap-1 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white",children:[Xe("nav.more"),d.jsx(de,{name:"chevron-down",className:"w-4 h-4"})]}),b&&d.jsx(W,{className:"fixed w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-50",style:{top:`${Y.top}px`,left:`${Y.left}px`},children:Me.map(V=>{const re=he(`/board/${V.slug}`);return d.jsxs(Q,{onClick:()=>{pt(`/board/${V.slug}`),F(!1)},className:`block w-full text-left px-4 py-2 text-sm cursor-pointer ${re?"bg-gray-900 text-white dark:bg-white dark:text-gray-900":"text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"}`,children:[V.icon&&d.jsx(R,{className:"mr-1",children:V.icon}),V.name]},V.id)})})]})]})})})]})},sd=Object.freeze(Object.defineProperty({__proto__:null,default:es},Symbol.toStringTag,{value:"Module"})),ld=k.forwardRef(({children:e,className:t="",...r},n)=>d.jsx("footer",{ref:n,className:t,...r,children:e})),kt=(e,t)=>window.G7Core?.t?.(e,t)??e,id=768,ts=1024,cd=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},rs=({siteName:e="그누보드7",siteDescription:t,copyrightText:r,socialLinks:n={},linkGroups:o,className:a="",editorAttrs:s})=>{const l=new Date().getFullYear(),h=window.G7Core?.useResponsive,i=h?.()?.width??(typeof window<"u"?window.innerWidth:ts),u=i<id,g=i>=ts,p=u?1:g?5:2,m=[{title:kt("footer.community"),links:[{label:kt("nav.home"),href:"/"},{label:kt("nav.popular"),href:"/boards/popular"},{label:kt("footer.all_boards"),href:"/boards"}]},{title:kt("footer.info"),links:[{label:kt("footer.about"),href:"/page/about"},{label:kt("footer.faq"),href:"/page/faq"},{label:kt("footer.contact"),href:"/page/contact"}]},{title:kt("footer.policy"),links:[{label:kt("footer.terms"),href:"/page/terms"},{label:kt("footer.privacy"),href:"/page/privacy"},{label:kt("footer.refund"),href:"/page/refund"}]}],x=o||m,C={github:"github",twitter:"twitter",discord:"discord",facebook:"facebook",instagram:"instagram"};return d.jsx(ld,{...s??{},className:`bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800 ${a}`,children:d.jsxs(W,{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12",children:[d.jsxs(W,{className:"grid gap-8",style:{gridTemplateColumns:`repeat(${p}, minmax(0, 1fr))`},children:[d.jsxs(W,{style:g?{gridColumn:"span 2 / span 2"}:void 0,children:[d.jsx(gn,{className:"text-lg font-bold text-gray-900 dark:text-white",children:e}),t&&d.jsx(Dt,{className:"mt-2 text-sm text-gray-600 dark:text-gray-400",children:t}),d.jsx(W,{className:"mt-4 flex items-center gap-4",children:Object.entries(n).map(([O,v])=>v?d.jsx(ao,{href:v,target:"_blank",rel:"noopener noreferrer",className:"text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200","aria-label":O,children:d.jsx(de,{name:C[O],className:"w-5 h-5"})},O):null)})]}),x.map((O,v)=>d.jsxs(W,{children:[d.jsx(Ya,{className:"text-sm font-semibold text-gray-900 dark:text-white uppercase tracking-wider",children:O.title}),d.jsx(oo,{className:"mt-4 space-y-2",children:O.links.map((E,I)=>d.jsx(Xt,{children:d.jsx(Q,{onClick:()=>cd(E.href),className:"text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white cursor-pointer",children:E.label})},I))})]},v))]}),d.jsxs(W,{className:"mt-8 pt-8 border-t border-gray-200 dark:border-gray-800 flex justify-between items-center gap-4",style:{flexDirection:u?"column":"row"},children:[d.jsx(Dt,{className:"text-sm text-gray-500 dark:text-gray-400",children:r||`© ${l} ${e}. All rights reserved.`}),d.jsx(Dt,{className:"text-sm text-gray-500 dark:text-gray-400",children:kt("footer.powered_by")})]})]})})},dd=Object.freeze(Object.defineProperty({__proto__:null,default:rs},Symbol.toStringTag,{value:"Module"})),Pt=(e,t)=>window.G7Core?.t?.(e,t)??e,Tt=e=>{window.G7Core?.dispatch?.({handler:"navigate",params:{path:e}})},ns=({isOpen:e,onClose:t,logo:r,siteName:n="그누보드7",user:o,boards:a=[],cartCount:s=0})=>{const l=k.useRef(null),h=window.G7Core?.useResponsive,c=h?.(),i=c?c.width<1024:typeof window<"u"&&window.innerWidth<1024;k.useEffect(()=>{const g=p=>{p.key==="Escape"&&t()};return e&&(document.addEventListener("keydown",g),document.body.style.overflow="hidden"),()=>{document.removeEventListener("keydown",g),document.body.style.overflow=""}},[e,t]);const u=g=>{g.target===g.currentTarget&&t()};return!e||!i?null:d.jsxs(W,{className:"fixed inset-0 z-50",onClick:u,children:[d.jsx(W,{className:"fixed inset-0 bg-black/50 transition-opacity"}),d.jsxs(W,{ref:l,className:`fixed inset-y-0 left-0 w-80 max-w-[85vw] bg-white dark:bg-gray-900 shadow-xl transform transition-transform duration-300 ${e?"translate-x-0":"-translate-x-full"}`,children:[d.jsxs(W,{className:"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-800",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/")},className:"flex items-center gap-2 cursor-pointer",children:r?d.jsx(Lt,{src:r,alt:n,className:"h-8"}):d.jsx(R,{className:"text-xl font-bold text-gray-900 dark:text-white",children:n})}),d.jsx(Q,{onClick:t,className:"p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200","aria-label":"메뉴 닫기",children:d.jsx(de,{name:"x",className:"w-6 h-6"})})]}),o?d.jsx(W,{className:"p-4 border-b border-gray-200 dark:border-gray-800",children:d.jsxs(W,{className:"flex items-center gap-3",children:[o.avatar?d.jsx(Lt,{src:o.avatar,alt:o.name,className:"w-12 h-12 rounded-full"}):d.jsx(W,{className:"w-12 h-12 rounded-full bg-blue-500 flex items-center justify-center text-white text-lg font-medium",children:(o.name||"U").charAt(0).toUpperCase()}),d.jsxs(W,{children:[d.jsx(Dt,{className:"font-medium text-gray-900 dark:text-white",children:o.name}),d.jsx(Q,{onClick:()=>{t(),Tt("/mypage")},className:"text-sm text-blue-600 dark:text-blue-400 hover:underline cursor-pointer",children:Pt("common.mypage")})]})]})}):d.jsx(W,{className:"p-4 border-b border-gray-200 dark:border-gray-800",children:d.jsxs(W,{className:"flex gap-2",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/login")},className:"flex-1 py-2 text-center text-sm font-medium text-white bg-gray-900 dark:bg-white dark:text-gray-900 rounded-lg cursor-pointer",children:Pt("auth.login")}),d.jsx(Q,{onClick:()=>{t(),Tt("/register")},className:"flex-1 py-2 text-center text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer",children:Pt("auth.register")})]})}),d.jsx(Fr,{className:"p-4 overflow-y-auto max-h-[calc(100vh-200px)]",children:d.jsxs(oo,{className:"space-y-1",children:[d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(de,{name:"home",className:"w-5 h-5"}),Pt("nav.home")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/popular")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(R,{className:"text-orange-500",children:"🔥"}),Pt("nav.popular")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/shop")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(R,{children:"🛒"}),Pt("nav.shop")]})}),d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt("/cart")},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[d.jsx(de,{name:"shopping-cart",className:"w-5 h-5"}),Pt("nav.cart"),s>0&&d.jsx(R,{className:"ml-auto px-2 py-0.5 text-xs bg-blue-500 text-white rounded-full",children:s})]})}),d.jsx(Xt,{className:"my-4 border-t border-gray-200 dark:border-gray-800"}),d.jsx(Xt,{className:"px-3 py-2 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider",children:Pt("nav.boards")}),a.map(g=>d.jsx(Xt,{children:d.jsxs(Q,{onClick:()=>{t(),Tt(`/board/${g.slug}`)},className:"flex items-center gap-3 px-3 py-2 w-full text-left text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer",children:[g.icon&&d.jsx(R,{children:g.icon}),g.name]})},g.id))]})}),d.jsx(W,{className:"absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900",children:d.jsxs(W,{className:"flex items-center justify-between text-sm text-gray-500 dark:text-gray-400",children:[d.jsx(Q,{onClick:()=>{t(),Tt("/about")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.about")}),d.jsx(Q,{onClick:()=>{t(),Tt("/terms")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.terms")}),d.jsx(Q,{onClick:()=>{t(),Tt("/privacy")},className:"hover:text-gray-700 dark:hover:text-gray-200 cursor-pointer",children:Pt("footer.privacy")})]})})]})]})},ud=Object.freeze(Object.defineProperty({__proto__:null,default:ns},Symbol.toStringTag,{value:"Module"}));const{entries:os,setPrototypeOf:as,isFrozen:fd,getPrototypeOf:gd,getOwnPropertyDescriptor:hd}=Object;let{freeze:ft,seal:Nt,create:uo}=Object,{apply:fo,construct:go}=typeof Reflect<"u"&&Reflect;ft||(ft=function(t){return t}),Nt||(Nt=function(t){return t}),fo||(fo=function(t,r){for(var n=arguments.length,o=new Array(n>2?n-2:0),a=2;a<n;a++)o[a-2]=arguments[a];return t.apply(r,o)}),go||(go=function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return new t(...n)});const bn=ht(Array.prototype.forEach),pd=ht(Array.prototype.lastIndexOf),ss=ht(Array.prototype.pop),zr=ht(Array.prototype.push),md=ht(Array.prototype.splice),yn=ht(String.prototype.toLowerCase),ho=ht(String.prototype.toString),po=ht(String.prototype.match),Hr=ht(String.prototype.replace),bd=ht(String.prototype.indexOf),yd=ht(String.prototype.trim),$t=ht(Object.prototype.hasOwnProperty),gt=ht(RegExp.prototype.test),Gr=vd(TypeError);function ht(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return fo(e,t,n)}}function vd(e){return function(){for(var t=arguments.length,r=new Array(t),n=0;n<t;n++)r[n]=arguments[n];return go(e,r)}}function Ae(e,t){let r=arguments.length>2&&arguments[2]!==void 0?arguments[2]:yn;as&&as(e,null);let n=t.length;for(;n--;){let o=t[n];if(typeof o=="string"){const a=r(o);a!==o&&(fd(t)||(t[n]=a),o=a)}e[o]=!0}return e}function wd(e){for(let t=0;t<e.length;t++)$t(e,t)||(e[t]=null);return e}function Vt(e){const t=uo(null);for(const[r,n]of os(e))$t(e,r)&&(Array.isArray(n)?t[r]=wd(n):n&&typeof n=="object"&&n.constructor===Object?t[r]=Vt(n):t[r]=n);return t}function jr(e,t){for(;e!==null;){const n=hd(e,t);if(n){if(n.get)return ht(n.get);if(typeof n.value=="function")return ht(n.value)}e=gd(e)}function r(){return null}return r}const ls=ft(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","search","section","select","shadow","slot","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),mo=ft(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","enterkeyhint","exportparts","filter","font","g","glyph","glyphref","hkern","image","inputmode","line","lineargradient","marker","mask","metadata","mpath","part","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),bo=ft(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),xd=ft(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),yo=ft(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),kd=ft(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),is=ft(["#text"]),cs=ft(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","exportparts","face","for","headers","height","hidden","high","href","hreflang","id","inert","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","part","pattern","placeholder","playsinline","popover","popovertarget","popovertargetaction","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","slot","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),vo=ft(["accent-height","accumulate","additive","alignment-baseline","amplitude","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","exponent","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","mask-type","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","slope","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","tablevalues","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),ds=ft(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),vn=ft(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),Cd=Nt(/\{\{[\w\W]*|[\w\W]*\}\}/gm),Sd=Nt(/<%[\w\W]*|[\w\W]*%>/gm),_d=Nt(/\$\{[\w\W]*/gm),Ed=Nt(/^data-[\-\w.\u00B7-\uFFFF]+$/),Td=Nt(/^aria-[\-\w]+$/),us=Nt(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Nd=Nt(/^(?:\w+script|data):/i),Ad=Nt(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),fs=Nt(/^html$/i),Md=Nt(/^[a-z][.\w]*(-[.\w]+)+$/i);var gs=Object.freeze({__proto__:null,ARIA_ATTR:Td,ATTR_WHITESPACE:Ad,CUSTOM_ELEMENT:Md,DATA_ATTR:Ed,DOCTYPE_NAME:fs,ERB_EXPR:Sd,IS_ALLOWED_URI:us,IS_SCRIPT_OR_DATA:Nd,MUSTACHE_EXPR:Cd,TMPLIT_EXPR:_d});const Br={element:1,text:3,progressingInstruction:7,comment:8,document:9},Od=function(){return typeof window>"u"?null:window},Id=function(t,r){if(typeof t!="object"||typeof t.createPolicy!="function")return null;let n=null;const o="data-tt-policy-suffix";r&&r.hasAttribute(o)&&(n=r.getAttribute(o));const a="dompurify"+(n?"#"+n:"");try{return t.createPolicy(a,{createHTML(s){return s},createScriptURL(s){return s}})}catch{return console.warn("TrustedTypes policy "+a+" could not be created."),null}},hs=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}};function ps(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:Od();const t=ge=>ps(ge);if(t.version="3.3.1",t.removed=[],!e||!e.document||e.document.nodeType!==Br.document||!e.Element)return t.isSupported=!1,t;let{document:r}=e;const n=r,o=n.currentScript,{DocumentFragment:a,HTMLTemplateElement:s,Node:l,Element:f,NodeFilter:h,NamedNodeMap:c=e.NamedNodeMap||e.MozNamedAttrMap,HTMLFormElement:i,DOMParser:u,trustedTypes:g}=e,p=f.prototype,m=jr(p,"cloneNode"),x=jr(p,"remove"),C=jr(p,"nextSibling"),O=jr(p,"childNodes"),v=jr(p,"parentNode");if(typeof s=="function"){const ge=r.createElement("template");ge.content&&ge.content.ownerDocument&&(r=ge.content.ownerDocument)}let E,I="";const{implementation:_,createNodeIterator:M,createDocumentFragment:w,getElementsByTagName:y}=r,{importNode:T}=n;let A=hs();t.isSupported=typeof os=="function"&&typeof v=="function"&&_&&_.createHTMLDocument!==void 0;const{MUSTACHE_EXPR:P,ERB_EXPR:D,TMPLIT_EXPR:L,DATA_ATTR:U,ARIA_ATTR:S,IS_SCRIPT_OR_DATA:b,ATTR_WHITESPACE:F,CUSTOM_ELEMENT:$}=gs;let{IS_ALLOWED_URI:G}=gs,Y=null;const B=Ae({},[...ls,...mo,...bo,...yo,...is]);let K=null;const j=Ae({},[...cs,...vo,...ds,...vn]);let H=Object.seal(uo(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),q=null,ue=null;const te=Object.seal(uo(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}}));let fe=!0,ee=!0,xe=!1,he=!0,Ge=!1,De=!0,ye=!1,Me=!1,Ce=!1,we=!1,Fe=!1,Le=!1,V=!0,re=!1;const oe="user-content-";let ae=!0,be=!1,ke={},He=null;const Pe=Ae({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Oe=Ae({},["audio","video","img","source","image","track"]);let X=null;const se=Ae({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ie="http://www.w3.org/1998/Math/MathML",le="http://www.w3.org/2000/svg",ne="http://www.w3.org/1999/xhtml";let Te=ne,ve=!1,Se=null;const _e=Ae({},[ie,le,ne],ho);let Ue=Ae({},["mi","mo","mn","ms","mtext"]),je=Ae({},["annotation-xml"]);const Ee=Ae({},["title","style","font","a","script"]);let pe=null;const Ne=["application/xhtml+xml","text/html"],Ke="text/html";let $e=null,Be=null;const Ze=r.createElement("form"),qt=function(z){return z instanceof RegExp||z instanceof Function},qe=function(){let z=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!(Be&&Be===z)){if((!z||typeof z!="object")&&(z={}),z=Vt(z),pe=Ne.indexOf(z.PARSER_MEDIA_TYPE)===-1?Ke:z.PARSER_MEDIA_TYPE,$e=pe==="application/xhtml+xml"?ho:yn,Y=$t(z,"ALLOWED_TAGS")?Ae({},z.ALLOWED_TAGS,$e):B,K=$t(z,"ALLOWED_ATTR")?Ae({},z.ALLOWED_ATTR,$e):j,Se=$t(z,"ALLOWED_NAMESPACES")?Ae({},z.ALLOWED_NAMESPACES,ho):_e,X=$t(z,"ADD_URI_SAFE_ATTR")?Ae(Vt(se),z.ADD_URI_SAFE_ATTR,$e):se,Ve=$t(z,"ADD_DATA_URI_TAGS")?Ae(Vt(Oe),z.ADD_DATA_URI_TAGS,$e):Oe,He=$t(z,"FORBID_CONTENTS")?Ae({},z.FORBID_CONTENTS,$e):Pe,q=$t(z,"FORBID_TAGS")?Ae({},z.FORBID_TAGS,$e):Vt({}),ue=$t(z,"FORBID_ATTR")?Ae({},z.FORBID_ATTR,$e):Vt({}),ke=$t(z,"USE_PROFILES")?z.USE_PROFILES:!1,fe=z.ALLOW_ARIA_ATTR!==!1,ee=z.ALLOW_DATA_ATTR!==!1,xe=z.ALLOW_UNKNOWN_PROTOCOLS||!1,he=z.ALLOW_SELF_CLOSE_IN_ATTR!==!1,Ge=z.SAFE_FOR_TEMPLATES||!1,De=z.SAFE_FOR_XML!==!1,ye=z.WHOLE_DOCUMENT||!1,we=z.RETURN_DOM||!1,Fe=z.RETURN_DOM_FRAGMENT||!1,Le=z.RETURN_TRUSTED_TYPE||!1,Ce=z.FORCE_BODY||!1,V=z.SANITIZE_DOM!==!1,re=z.SANITIZE_NAMED_PROPS||!1,ae=z.KEEP_CONTENT!==!1,be=z.IN_PLACE||!1,G=z.ALLOWED_URI_REGEXP||us,Te=z.NAMESPACE||ne,Ue=z.MATHML_TEXT_INTEGRATION_POINTS||Ue,je=z.HTML_INTEGRATION_POINTS||je,H=z.CUSTOM_ELEMENT_HANDLING||{},z.CUSTOM_ELEMENT_HANDLING&&qt(z.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(H.tagNameCheck=z.CUSTOM_ELEMENT_HANDLING.tagNameCheck),z.CUSTOM_ELEMENT_HANDLING&&qt(z.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(H.attributeNameCheck=z.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),z.CUSTOM_ELEMENT_HANDLING&&typeof z.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements=="boolean"&&(H.allowCustomizedBuiltInElements=z.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Ge&&(ee=!1),Fe&&(we=!0),ke&&(Y=Ae({},is),K=[],ke.html===!0&&(Ae(Y,ls),Ae(K,cs)),ke.svg===!0&&(Ae(Y,mo),Ae(K,vo),Ae(K,vn)),ke.svgFilters===!0&&(Ae(Y,bo),Ae(K,vo),Ae(K,vn)),ke.mathMl===!0&&(Ae(Y,yo),Ae(K,ds),Ae(K,vn))),z.ADD_TAGS&&(typeof z.ADD_TAGS=="function"?te.tagCheck=z.ADD_TAGS:(Y===B&&(Y=Vt(Y)),Ae(Y,z.ADD_TAGS,$e))),z.ADD_ATTR&&(typeof z.ADD_ATTR=="function"?te.attributeCheck=z.ADD_ATTR:(K===j&&(K=Vt(K)),Ae(K,z.ADD_ATTR,$e))),z.ADD_URI_SAFE_ATTR&&Ae(X,z.ADD_URI_SAFE_ATTR,$e),z.FORBID_CONTENTS&&(He===Pe&&(He=Vt(He)),Ae(He,z.FORBID_CONTENTS,$e)),z.ADD_FORBID_CONTENTS&&(He===Pe&&(He=Vt(He)),Ae(He,z.ADD_FORBID_CONTENTS,$e)),ae&&(Y["#text"]=!0),ye&&Ae(Y,["html","head","body"]),Y.table&&(Ae(Y,["tbody"]),delete q.tbody),z.TRUSTED_TYPES_POLICY){if(typeof z.TRUSTED_TYPES_POLICY.createHTML!="function")throw Gr('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if(typeof z.TRUSTED_TYPES_POLICY.createScriptURL!="function")throw Gr('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');E=z.TRUSTED_TYPES_POLICY,I=E.createHTML("")}else E===void 0&&(E=Id(g,o)),E!==null&&typeof I=="string"&&(I=E.createHTML(""));ft&&ft(z),Be=z}},jt=Ae({},[...mo,...bo,...xd]),_t=Ae({},[...yo,...kd]),Bt=function(z){let Z=v(z);(!Z||!Z.tagName)&&(Z={namespaceURI:Te,tagName:"template"});const ce=yn(z.tagName),Ye=yn(Z.tagName);return Se[z.namespaceURI]?z.namespaceURI===le?Z.namespaceURI===ne?ce==="svg":Z.namespaceURI===ie?ce==="svg"&&(Ye==="annotation-xml"||Ue[Ye]):!!jt[ce]:z.namespaceURI===ie?Z.namespaceURI===ne?ce==="math":Z.namespaceURI===le?ce==="math"&&je[Ye]:!!_t[ce]:z.namespaceURI===ne?Z.namespaceURI===le&&!je[Ye]||Z.namespaceURI===ie&&!Ue[Ye]?!1:!_t[ce]&&(Ee[ce]||!jt[ce]):!!(pe==="application/xhtml+xml"&&Se[z.namespaceURI]):!1},dt=function(z){zr(t.removed,{element:z});try{v(z).removeChild(z)}catch{x(z)}},Et=function(z,Z){try{zr(t.removed,{attribute:Z.getAttributeNode(z),from:Z})}catch{zr(t.removed,{attribute:null,from:Z})}if(Z.removeAttribute(z),z==="is")if(we||Fe)try{dt(Z)}catch{}else try{Z.setAttribute(z,"")}catch{}},Wt=function(z){let Z=null,ce=null;if(Ce)z="<remove></remove>"+z;else{const Qe=po(z,/^[\r\n\t ]+/);ce=Qe&&Qe[0]}pe==="application/xhtml+xml"&&Te===ne&&(z='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+z+"</body></html>");const Ye=E?E.createHTML(z):z;if(Te===ne)try{Z=new u().parseFromString(Ye,pe)}catch{}if(!Z||!Z.documentElement){Z=_.createDocument(Te,"template",null);try{Z.documentElement.innerHTML=ve?I:Ye}catch{}}const ut=Z.body||Z.documentElement;return z&&ce&&ut.insertBefore(r.createTextNode(ce),ut.childNodes[0]||null),Te===ne?y.call(Z,ye?"html":"body")[0]:ye?Z.documentElement:ut},nr=function(z){return M.call(z.ownerDocument||z,z,h.SHOW_ELEMENT|h.SHOW_COMMENT|h.SHOW_TEXT|h.SHOW_PROCESSING_INSTRUCTION|h.SHOW_CDATA_SECTION,null)},mr=function(z){return z instanceof i&&(typeof z.nodeName!="string"||typeof z.textContent!="string"||typeof z.removeChild!="function"||!(z.attributes instanceof c)||typeof z.removeAttribute!="function"||typeof z.setAttribute!="function"||typeof z.namespaceURI!="string"||typeof z.insertBefore!="function"||typeof z.hasChildNodes!="function")},Tc=function(z){return typeof l=="function"&&z instanceof l};function Zt(ge,z,Z){bn(ge,ce=>{ce.call(t,z,Z,Be)})}const Nc=function(z){let Z=null;if(Zt(A.beforeSanitizeElements,z,null),mr(z))return dt(z),!0;const ce=$e(z.nodeName);if(Zt(A.uponSanitizeElement,z,{tagName:ce,allowedTags:Y}),De&&z.hasChildNodes()&&!Tc(z.firstElementChild)&>(/<[/\w!]/g,z.innerHTML)&>(/<[/\w!]/g,z.textContent)||z.nodeType===Br.progressingInstruction||De&&z.nodeType===Br.comment&>(/<[/\w]/g,z.data))return dt(z),!0;if(!(te.tagCheck instanceof Function&&te.tagCheck(ce))&&(!Y[ce]||q[ce])){if(!q[ce]&&Mc(ce)&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,ce)||H.tagNameCheck instanceof Function&&H.tagNameCheck(ce)))return!1;if(ae&&!He[ce]){const Ye=v(z)||z.parentNode,ut=O(z)||z.childNodes;if(ut&&Ye){const Qe=ut.length;for(let wt=Qe-1;wt>=0;--wt){const Qt=m(ut[wt],!0);Qt.__removalCount=(z.__removalCount||0)+1,Ye.insertBefore(Qt,C(z))}}}return dt(z),!0}return z instanceof f&&!Bt(z)||(ce==="noscript"||ce==="noembed"||ce==="noframes")&>(/<\/no(script|embed|frames)/i,z.innerHTML)?(dt(z),!0):(Ge&&z.nodeType===Br.text&&(Z=z.textContent,bn([P,D,L],Ye=>{Z=Hr(Z,Ye," ")}),z.textContent!==Z&&(zr(t.removed,{element:z.cloneNode()}),z.textContent=Z)),Zt(A.afterSanitizeElements,z,null),!1)},Ac=function(z,Z,ce){if(V&&(Z==="id"||Z==="name")&&(ce in r||ce in Ze))return!1;if(!(ee&&!ue[Z]&>(U,Z))){if(!(fe&>(S,Z))){if(!(te.attributeCheck instanceof Function&&te.attributeCheck(Z,z))){if(!K[Z]||ue[Z]){if(!(Mc(z)&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,z)||H.tagNameCheck instanceof Function&&H.tagNameCheck(z))&&(H.attributeNameCheck instanceof RegExp&>(H.attributeNameCheck,Z)||H.attributeNameCheck instanceof Function&&H.attributeNameCheck(Z,z))||Z==="is"&&H.allowCustomizedBuiltInElements&&(H.tagNameCheck instanceof RegExp&>(H.tagNameCheck,ce)||H.tagNameCheck instanceof Function&&H.tagNameCheck(ce))))return!1}else if(!X[Z]){if(!gt(G,Hr(ce,F,""))){if(!((Z==="src"||Z==="xlink:href"||Z==="href")&&z!=="script"&&bd(ce,"data:")===0&&Ve[z])){if(!(xe&&!gt(b,Hr(ce,F,"")))){if(ce)return!1}}}}}}}return!0},Mc=function(z){return z!=="annotation-xml"&&po(z,$)},Oc=function(z){Zt(A.beforeSanitizeAttributes,z,null);const{attributes:Z}=z;if(!Z||mr(z))return;const ce={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:K,forceKeepAttr:void 0};let Ye=Z.length;for(;Ye--;){const ut=Z[Ye],{name:Qe,namespaceURI:wt,value:Qt}=ut,$r=$e(Qe),ja=Qt;let st=Qe==="value"?ja:yd(ja);if(ce.attrName=$r,ce.attrValue=st,ce.keepAttr=!0,ce.forceKeepAttr=void 0,Zt(A.uponSanitizeAttribute,z,ce),st=ce.attrValue,re&&($r==="id"||$r==="name")&&(Et(Qe,z),st=oe+st),De&>(/((--!?|])>)|<\/(style|title|textarea)/i,st)){Et(Qe,z);continue}if($r==="attributename"&&po(st,"href")){Et(Qe,z);continue}if(ce.forceKeepAttr)continue;if(!ce.keepAttr){Et(Qe,z);continue}if(!he&>(/\/>/i,st)){Et(Qe,z);continue}Ge&&bn([P,D,L],Dc=>{st=Hr(st,Dc," ")});const Ic=$e(z.nodeName);if(!Ac(Ic,$r,st)){Et(Qe,z);continue}if(E&&typeof g=="object"&&typeof g.getAttributeType=="function"&&!wt)switch(g.getAttributeType(Ic,$r)){case"TrustedHTML":{st=E.createHTML(st);break}case"TrustedScriptURL":{st=E.createScriptURL(st);break}}if(st!==ja)try{wt?z.setAttributeNS(wt,Qe,st):z.setAttribute(Qe,st),mr(z)?dt(z):ss(t.removed)}catch{Et(Qe,z)}}Zt(A.afterSanitizeAttributes,z,null)},Kp=function ge(z){let Z=null;const ce=nr(z);for(Zt(A.beforeSanitizeShadowDOM,z,null);Z=ce.nextNode();)Zt(A.uponSanitizeShadowNode,Z,null),Nc(Z),Oc(Z),Z.content instanceof a&&ge(Z.content);Zt(A.afterSanitizeShadowDOM,z,null)};return t.sanitize=function(ge){let z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},Z=null,ce=null,Ye=null,ut=null;if(ve=!ge,ve&&(ge="<!-->"),typeof ge!="string"&&!Tc(ge))if(typeof ge.toString=="function"){if(ge=ge.toString(),typeof ge!="string")throw Gr("dirty is not a string, aborting")}else throw Gr("toString is not a function");if(!t.isSupported)return ge;if(Me||qe(z),t.removed=[],typeof ge=="string"&&(be=!1),be){if(ge.nodeName){const Qt=$e(ge.nodeName);if(!Y[Qt]||q[Qt])throw Gr("root node is forbidden and cannot be sanitized in-place")}}else if(ge instanceof l)Z=Wt("<!---->"),ce=Z.ownerDocument.importNode(ge,!0),ce.nodeType===Br.element&&ce.nodeName==="BODY"||ce.nodeName==="HTML"?Z=ce:Z.appendChild(ce);else{if(!we&&!Ge&&!ye&&ge.indexOf("<")===-1)return E&&Le?E.createHTML(ge):ge;if(Z=Wt(ge),!Z)return we?null:Le?I:""}Z&&Ce&&dt(Z.firstChild);const Qe=nr(be?ge:Z);for(;Ye=Qe.nextNode();)Nc(Ye),Oc(Ye),Ye.content instanceof a&&Kp(Ye.content);if(be)return ge;if(we){if(Fe)for(ut=w.call(Z.ownerDocument);Z.firstChild;)ut.appendChild(Z.firstChild);else ut=Z;return(K.shadowroot||K.shadowrootmode)&&(ut=T.call(n,ut,!0)),ut}let wt=ye?Z.outerHTML:Z.innerHTML;return ye&&Y["!doctype"]&&Z.ownerDocument&&Z.ownerDocument.doctype&&Z.ownerDocument.doctype.name&>(fs,Z.ownerDocument.doctype.name)&&(wt="<!DOCTYPE "+Z.ownerDocument.doctype.name+`> +`+wt),Ge&&bn([P,D,L],Qt=>{wt=Hr(wt,Qt," ")}),E&&Le?E.createHTML(wt):wt},t.setConfig=function(){let ge=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};qe(ge),Me=!0},t.clearConfig=function(){Be=null,Me=!1},t.isValidAttribute=function(ge,z,Z){Be||qe({});const ce=$e(ge),Ye=$e(z);return Ac(ce,Ye,Z)},t.addHook=function(ge,z){typeof z=="function"&&zr(A[ge],z)},t.removeHook=function(ge,z){if(z!==void 0){const Z=pd(A[ge],z);return Z===-1?void 0:md(A[ge],Z,1)[0]}return ss(A[ge])},t.removeHooks=function(ge){A[ge]=[]},t.removeAllHooks=function(){A=hs()},t}var wo=ps();const Dd=()=>{const e=()=>{const n=window.G7Core?.state?.get?.();return n&&(n.preferredCurrency||n.defaultCurrency)||""},[t,r]=k.useState(e);return k.useEffect(()=>{const n=window.G7Core?.state?.subscribe;if(r(e()),typeof n!="function")return;const o=n(a=>{r(a&&(a.preferredCurrency||a.defaultCurrency)||"")});return typeof o=="function"?o:void 0},[]),t},Ld=(e,t=!1)=>{window.G7Core?.dispatch?.({handler:"navigate",params:t?{path:e,mergeQuery:!0,query:{}}:{path:e}})},Pd=e=>{const t=Math.round(e*2)/2;return[1,2,3,4,5].map(r=>t>=r?"full":t>=r-.5?"half":"empty")},ms=({product:e,onClick:t,shopBase:r="/shop",preserveListQuery:n=!1,className:o="",id:a,editorAttrs:s})=>{const l=Dd(),f=v=>{const E=`multi_currency_${v}`,I=e[E];if(I&&I[l])return I[l].formatted;const _=`${v}_formatted`;return e[_]},h=e.name_localized??(typeof e.name=="string"?e.name:"")??"",c=f("selling_price"),i=f("list_price"),u=e.discount_rate&&e.discount_rate>0,g=e.sales_status&&e.sales_status!=="on_sale",p=e.labels??[],m=v=>wo.sanitize(v,{ALLOWED_TAGS:["mark"],ALLOWED_ATTR:[]}),x=e.name_highlighted,C=e.description_highlighted??e.short_description,O=()=>{if(t)t(e.id);else{const v=r==="/"?"":r,E=e.product_code??e.id;Ld(`${v}/products/${E}`,n)}};return d.jsxs(Q,{onClick:O,className:`block w-full text-left group bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-lg transition-shadow cursor-pointer ${o}`,id:a,...s,children:[d.jsxs(W,{className:"relative aspect-square overflow-hidden bg-gray-100 dark:bg-gray-700",children:[d.jsx(Lt,{src:e.thumbnail_url,alt:h,className:"w-full h-full object-cover group-hover:scale-105 transition-transform duration-200",loading:"lazy"}),g&&d.jsx(W,{className:"absolute inset-0 bg-black/50 flex items-center justify-center",children:d.jsx(R,{className:`text-lg font-bold ${e.sales_status==="sold_out"?"text-red-300":e.sales_status==="suspended"?"text-amber-300":"text-blue-300"}`,style:{textShadow:"0 1px 4px rgba(0,0,0,0.6)"},children:e.sales_status_label??e.sales_status})}),u&&e.sales_status!=="sold_out"&&e.sales_status!=="suspended"&&d.jsxs(R,{className:"absolute top-2 left-2 px-2 py-1 bg-red-500 text-white text-xs font-bold rounded",children:[e.discount_rate,"%"]}),p.length>0&&d.jsx(W,{className:"absolute bottom-2 right-2 flex gap-1 flex-wrap justify-end",children:p.map((v,E)=>d.jsx(R,{className:"text-xs text-white font-medium px-1.5 py-0.5 rounded",style:{backgroundColor:v.color||"#6b7280"},children:v.name},E))})]}),d.jsxs(W,{className:"p-4",children:[d.jsxs(W,{className:"flex items-center gap-1.5 flex-wrap",children:[e.primary_category&&d.jsx(R,{className:"inline-block text-xs font-medium text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/30 px-2 py-0.5 rounded",children:e.primary_category}),e.brand_name&&d.jsx(R,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.brand_name})]}),x?d.jsx(W,{className:"mt-1 font-medium text-gray-900 dark:text-white line-clamp-1 [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-500/40 [&_mark]:px-0.5 [&_mark]:rounded",dangerouslySetInnerHTML:{__html:m(x)}}):d.jsx(gn,{className:"mt-1 font-medium text-gray-900 dark:text-white line-clamp-1",children:h}),C&&d.jsx(W,{className:"mt-1 text-sm text-gray-600 dark:text-gray-400 line-clamp-2 [&_mark]:bg-yellow-200 [&_mark]:dark:bg-yellow-500/40 [&_mark]:px-0.5 [&_mark]:rounded",dangerouslySetInnerHTML:{__html:m(C)}}),d.jsxs(W,{className:"mt-1.5 flex items-center gap-1",children:[Pd(e.rating_avg??0).map((v,E)=>d.jsxs(R,{className:`text-xs ${v==="empty"?"text-gray-300 dark:text-gray-600":"text-yellow-400"}`,children:[v==="full"&&d.jsx("i",{className:"fa-solid fa-star"}),v==="half"&&d.jsx("i",{className:"fa-solid fa-star-half-stroke"}),v==="empty"&&d.jsx("i",{className:"fa-regular fa-star"})]},E)),d.jsx(R,{className:"text-xs text-gray-600 dark:text-gray-400 ml-0.5",children:(e.rating_avg??0).toFixed(1)}),d.jsxs(R,{className:"text-xs text-gray-400 dark:text-gray-500",children:["(",e.review_count??0,")"]})]}),d.jsxs(W,{className:"mt-2 flex items-baseline gap-2",children:[d.jsx(R,{className:"font-bold text-gray-900 dark:text-white",children:c}),i&&u&&d.jsx(R,{className:"text-sm text-gray-500 dark:text-gray-400 line-through",children:i})]})]})]})},$d=Object.freeze(Object.defineProperty({__proto__:null,default:ms},Symbol.toStringTag,{value:"Module"})),Fd={ALLOWED_TAGS:["svg","g","defs","title","desc","rect","circle","ellipse","line","polyline","polygon","path","text","tspan","linearGradient","radialGradient","stop","clipPath"],ALLOWED_ATTR:["xmlns","width","height","viewBox","role","aria-label","aria-hidden","fill","fill-opacity","fill-rule","stroke","stroke-width","stroke-linecap","stroke-linejoin","opacity","transform","x","y","x1","y1","x2","y2","cx","cy","r","rx","ry","d","points","width","height","text-anchor","dominant-baseline","font-family","font-size","font-weight","offset","stop-color","stop-opacity","gradientUnits","id","clip-path"],FORBID_TAGS:["script","foreignObject","image","use","a","style"],FORBID_ATTR:["onload","onerror","onclick","onmouseover","onfocus","onblur","href","xlink:href"]},xo=({brandMark:e,size:t=32,className:r="",id:n,editorAttrs:o})=>{const a=e?.svg,s=e?.text,l=k.useMemo(()=>!a||a.trim()===""?"":wo.sanitize(a,Fd),[a]),f={width:`${t}px`,height:`${t}px`,flex:`0 0 ${t}px`};if(l)return d.jsx(W,{id:n,"aria-hidden":"true",className:`inline-flex items-center justify-center ${r}`.trim(),style:f,dangerouslySetInnerHTML:{__html:l},...o});if(s&&s.trim()!==""){const h=e?.class??"bg-gray-500 text-white";return d.jsx(R,{id:n,"aria-hidden":"true",className:`inline-flex items-center justify-center rounded-lg text-xs font-bold ${h} ${r}`.trim(),style:f,...o,children:s})}return null},Ud=Object.freeze(Object.defineProperty({__proto__:null,BrandMark:xo,default:xo},Symbol.toStringTag,{value:"Module"})),bs="carousel",Wr="controller",zd="navigation",Hd="no-scroll",ko="portal",Gd="root",ys="toolbar",jd="counter",Vr="fullscreen",vs="slideshow",Kr="thumbnails",ws="zoom",vr="loading",Co="playing",Yr="error",Xr="complete",Bd="placeholder",qr=e=>`active-slide-${e}`,Wd=qr(vr),Vd=qr(Co),Kd=qr(Yr),Yd=qr(Xr),xs="fullsize",wr="flex_center",Xd="no_scroll",ks="no_scroll_padding",So="slide",Cs="slide_wrapper",qd="slide_wrapper_interactive",Rt="prev",er="next",_o="swipe",xr="close",Ss="onPointerDown",_s="onPointerMove",Es="onPointerUp",Ts="onPointerLeave",Ns="onPointerCancel",Eo="onKeyDown",Zd="onKeyUp",To="onWheel",Qd="Escape",Jd="ArrowLeft",Rd="ArrowRight",eu="button",wn="icon",As="contain",Ms="cover",Os="Unknown action type",Is="yarl__";function lt(...e){return[...e].filter(Boolean).join(" ")}function me(e){return`${Is}${e}`}function We(e){return`--${Is}${e}`}function or(e,t){return`${e}${t?`_${t}`:""}`}function ar(e){return t=>or(e,t)}function sr(e,t){var r;return(r=e?.[t])!==null&&r!==void 0?r:t}function Ds(e,t,r){return sr(e,"{index} of {total}").replace(/\{index}/g,`${Oo(r,t.length)+1}`).replace(/\{total}/g,`${t.length}`)}function kr(...e){return()=>{e.forEach(t=>{t()})}}function At(e,t,r){return()=>{const n=N.useContext(r);if(!n)throw new Error(`${e} must be used within a ${t}.Provider`);return n}}function No(){return typeof window<"u"}function Ao(e,t=0){const r=10**t;return Math.round((e+Number.EPSILON)*r)/r}function lr(e){return e.type===void 0||e.type==="image"}function Mo(e,t){return e.imageFit===Ms||e.imageFit!==As&&t===Ms}function xn(e){return typeof e=="string"?Number.parseInt(e,10):e}function kn(e){if(typeof e=="number")return{pixel:e};if(typeof e=="string"){const t=xn(e);return e.endsWith("%")?{percent:t}:{pixel:t}}return{pixel:0}}function tu(e,t){const r=kn(t),n=r.percent!==void 0?e.width/100*r.percent:r.pixel;return{width:Math.max(e.width-2*n,0),height:Math.max(e.height-2*n,0)}}function ru(){return(No()?window?.devicePixelRatio:void 0)||1}function Oo(e,t){return t>0?(e%t+t)%t:0}function Io(e){return e.length>0}function Do(e,t){return e[Oo(t,e.length)]}function Lo(e,t){return Io(e)?Do(e,t):void 0}function Ls(e){return lr(e)?e.src:void 0}function Cn(e,t,r){if(!r)return e;const{buttons:n,...o}=e,a=n.findIndex(l=>l===t),s=N.isValidElement(r)?N.cloneElement(r,{key:t},null):r;if(a>=0){const l=[...n];return l.splice(a,1,s),{buttons:l,...o}}return{buttons:[s,...n],...o}}function Ps(e,t,r=0){return Math.min(e.preload,Math.max(e.finite?t.length-1:Math.floor(t.length/2),r))}const nu=Number(N.version.split(".")[0])>=19;function ou(e){return{inert:nu?e:e?"":void 0}}function au(e){e.scrollTop}const Po={open:!1,close:()=>{},index:0,slides:[],render:{},plugins:[],toolbar:{buttons:[xr]},labels:{},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},carousel:{finite:!1,preload:2,padding:"16px",spacing:"30%",imageFit:As,imageProps:{}},controller:{ref:null,focus:!0,aria:!1,touchAction:"none",closeOnPullUp:!1,closeOnPullDown:!1,closeOnBackdropClick:!1,preventDefaultWheelX:!0,preventDefaultWheelY:!1,disableSwipeNavigation:!1},portal:{},noScroll:{disabled:!1},on:{},styles:{},className:""};function Mt(e,t){return{name:e,component:t}}function tt(e,t){return{module:e,children:t}}function $s(e,t,r){return e.module.name===t?r(e):e.children?[tt(e.module,e.children.flatMap(n=>{var o;return(o=$s(n,t,r))!==null&&o!==void 0?o:[]}))]:[e]}function Cr(e,t,r){return e.flatMap(n=>{var o;return(o=$s(n,t,r))!==null&&o!==void 0?o:[]})}function su(e,t=[],r=[]){let n=e;const o=g=>{const p=[...n];for(;p.length>0;){const m=p.pop();if(m?.module.name===g)return!0;m?.children&&p.push(...m.children)}return!1},a=(g,p)=>{if(g===""){n=[tt(p,n)];return}n=Cr(n,g,m=>[tt(p,[m])])},s=(g,p)=>{n=Cr(n,g,m=>[tt(m.module,[tt(p,m.children)])])},l=(g,p,m)=>{n=Cr(n,g,x=>{var C;return[tt(x.module,[...m?[tt(p)]:[],...(C=x.children)!==null&&C!==void 0?C:[],...m?[]:[tt(p)]])]})},f=(g,p,m)=>{n=Cr(n,g,x=>[...m?[tt(p)]:[],x,...m?[]:[tt(p)]])},h=g=>{s(Wr,g)},c=(g,p)=>{n=Cr(n,g,m=>[tt(p,m.children)])},i=g=>{n=Cr(n,g,p=>p.children)},u=g=>{r.push(g)};return t.forEach(g=>{g({contains:o,addParent:a,append:s,addChild:l,addSibling:f,addModule:h,replace:c,remove:i,augment:u})}),{config:n,augmentation:g=>r.reduce((p,m)=>m(p),g)}}const Fs=N.createContext(null),$o=At("useA11yContext","A11yContext",Fs);function lu({children:e}){const[t,r]=N.useState(!1),[n,o]=N.useState(!1),a=N.useMemo(()=>({focusWithin:t,trackFocusWithin:(l,f)=>{const h=c=>i=>{var u;i.currentTarget.contains(i.relatedTarget)||r(c),(u=c?l:f)===null||u===void 0||u(i)};return{onFocus:h(!0),onBlur:h(!1)}},autoPlaying:n,setAutoPlaying:o}),[t,n]);return N.createElement(Fs.Provider,{value:a},e)}const Us=N.createContext(null),Sr=At("useDocument","DocumentContext",Us);function iu({nodeRef:e,children:t}){const r=N.useMemo(()=>{const n=a=>{var s;return((s=a||e.current)===null||s===void 0?void 0:s.ownerDocument)||document};return{getOwnerDocument:n,getOwnerWindow:a=>{var s;return((s=n(a))===null||s===void 0?void 0:s.defaultView)||window}}},[e]);return N.createElement(Us.Provider,{value:r},t)}const zs=N.createContext(null),_r=At("useEvents","EventsContext",zs);function cu({children:e}){const[t]=N.useState({});N.useEffect(()=>()=>{Object.keys(t).forEach(n=>delete t[n])},[t]);const r=N.useMemo(()=>{const n=(s,l)=>{var f;(f=t[s])===null||f===void 0||f.splice(0,t[s].length,...t[s].filter(h=>h!==l))};return{publish:(...[s,l])=>{var f;(f=t[s])===null||f===void 0||f.forEach(h=>h(l))},subscribe:(s,l)=>(t[s]||(t[s]=[]),t[s].push(l),()=>n(s,l)),unsubscribe:n}},[t]);return N.createElement(zs.Provider,{value:r},e)}const Hs=N.createContext(null),rt=At("useLightboxProps","LightboxPropsContext",Hs);function Gs({children:e,...t}){return N.createElement(Hs.Provider,{value:t},e)}const js=N.createContext(null),Ot=At("useLightboxState","LightboxStateContext",js),Bs=N.createContext(null),du=At("useLightboxDispatch","LightboxDispatchContext",Bs);function uu(e,t){switch(t.type){case"swipe":{const{slides:r}=e,n=t?.increment||0,o=e.globalIndex+n,a=Oo(o,r.length),s=Lo(r,a),l=n||t.duration!==void 0?{increment:n,duration:t.duration,easing:t.easing}:void 0;return{slides:r,currentIndex:a,globalIndex:o,currentSlide:s,animation:l}}case"update":return t.slides!==e.slides||t.index!==e.currentIndex?{slides:t.slides,currentIndex:t.index,globalIndex:t.index,currentSlide:Lo(t.slides,t.index)}:e;default:throw new Error(Os)}}function fu({slides:e,index:t,children:r}){const[n,o]=N.useReducer(uu,{slides:e,currentIndex:t,globalIndex:t,currentSlide:Lo(e,t)});N.useEffect(()=>{o({type:"update",slides:e,index:t})},[e,t]);const a=N.useMemo(()=>({...n,state:n,dispatch:o}),[n,o]);return N.createElement(Bs.Provider,{value:o},N.createElement(js.Provider,{value:a},r))}const Ws=N.createContext(null),Zr=At("useTimeouts","TimeoutsContext",Ws);function gu({children:e}){const[t]=N.useState([]);N.useEffect(()=>()=>{t.forEach(n=>window.clearTimeout(n)),t.splice(0,t.length)},[t]);const r=N.useMemo(()=>{const n=s=>{t.splice(0,t.length,...t.filter(l=>l!==s))};return{setTimeout:(s,l)=>{const f=window.setTimeout(()=>{n(f),s()},l);return t.push(f),f},clearTimeout:s=>{s!==void 0&&(n(s),window.clearTimeout(s))}}},[t]);return N.createElement(Ws.Provider,{value:r},e)}const Er=N.forwardRef(function({label:t,className:r,icon:n,renderIcon:o,onClick:a,style:s,...l},f){const{styles:h,labels:c}=rt(),i=sr(c,t);return N.createElement("button",{ref:f,type:"button",title:i,"aria-label":i,className:lt(me(eu),r),onClick:a,style:{...s,...h.button},...l},o?o():N.createElement(n,{className:me(wn),style:h.icon}))});function Vs(e,t){const r=n=>N.createElement("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24","aria-hidden":"true",focusable:"false",...n},t);return r.displayName=e,r}function mt(e,t){return Vs(e,N.createElement("g",{fill:"currentColor"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t))}function hu(e,t){return Vs(e,N.createElement(N.Fragment,null,N.createElement("defs",null,N.createElement("mask",{id:"strike"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"white"}),N.createElement("path",{d:"M0 0L24 24",stroke:"black",strokeWidth:4}))),N.createElement("path",{d:"M0.70707 2.121320L21.878680 23.292883",stroke:"currentColor",strokeWidth:2}),N.createElement("g",{fill:"currentColor",mask:"url(#strike)"},N.createElement("path",{d:"M0 0h24v24H0z",fill:"none"}),t)))}const pu=mt("Close",N.createElement("path",{d:"M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"})),mu=mt("Previous",N.createElement("path",{d:"M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"})),bu=mt("Next",N.createElement("path",{d:"M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"})),yu=mt("Loading",N.createElement(N.Fragment,null,Array.from({length:8}).map((e,t,r)=>N.createElement("line",{key:t,x1:"12",y1:"6.5",x2:"12",y2:"1.8",strokeLinecap:"round",strokeWidth:"2.6",stroke:"currentColor",strokeOpacity:1/r.length*(t+1),transform:`rotate(${360/r.length*t}, 12, 12)`})))),vu=mt("Error",N.createElement("path",{d:"M21.9,21.9l-8.49-8.49l0,0L3.59,3.59l0,0L2.1,2.1L0.69,3.51L3,5.83V19c0,1.1,0.9,2,2,2h13.17l2.31,2.31L21.9,21.9z M5,18 l3.5-4.5l2.5,3.01L12.17,15l3,3H5z M21,18.17L5.83,3H19c1.1,0,2,0.9,2,2V18.17z"})),Ft=No()?N.useLayoutEffect:N.useEffect;function Fo(){const[e,t]=N.useState(!1);return N.useEffect(()=>{var r,n;const o=(r=window.matchMedia)===null||r===void 0?void 0:r.call(window,"(prefers-reduced-motion: reduce)");t(o?.matches);const a=s=>t(s.matches);return(n=o?.addEventListener)===null||n===void 0||n.call(o,"change",a),()=>{var s;return(s=o?.removeEventListener)===null||s===void 0?void 0:s.call(o,"change",a)}},[]),e}function wu(e){let t=0,r=0,n=0;const a=window.getComputedStyle(e).transform.match(/matrix.*\((.+)\)/);if(a){const s=a[1].split(",").map(xn);s.length===6?(t=s[4],r=s[5]):s.length===16&&(t=s[12],r=s[13],n=s[14])}return{x:t,y:r,z:n}}function Uo(e,t){const r=N.useRef(void 0),n=N.useRef(void 0),o=Fo();return Ft(()=>{var a,s,l;if(e.current&&r.current!==void 0&&!o){const{keyframes:f,duration:h,easing:c,onfinish:i}=t(r.current,e.current.getBoundingClientRect(),wu(e.current))||{};if(f&&h){(a=n.current)===null||a===void 0||a.cancel(),n.current=void 0;try{n.current=(l=(s=e.current).animate)===null||l===void 0?void 0:l.call(s,f,{duration:h,easing:c})}catch(u){console.error(u)}n.current&&(n.current.onfinish=()=>{n.current=void 0,i?.()})}}r.current=void 0}),{prepareAnimation:a=>{r.current=a},isAnimationPlaying:()=>{var a;return((a=n.current)===null||a===void 0?void 0:a.playState)==="running"}}}function Ks(){const e=N.useRef(null),t=N.useRef(void 0),[r,n]=N.useState();return{setContainerRef:N.useCallback(a=>{e.current=a,t.current&&(t.current.disconnect(),t.current=void 0);const s=()=>{if(a){const l=window.getComputedStyle(a),f=h=>parseFloat(h)||0;n({width:Math.round(a.clientWidth-f(l.paddingLeft)-f(l.paddingRight)),height:Math.round(a.clientHeight-f(l.paddingTop)-f(l.paddingBottom))})}else n(void 0)};s(),a&&typeof ResizeObserver<"u"&&(t.current=new ResizeObserver(s),t.current.observe(a))},[]),containerRef:e,containerRect:r}}function Sn(){const e=N.useRef(void 0),{setTimeout:t,clearTimeout:r}=Zr();return N.useCallback((n,o)=>{r(e.current),e.current=t(n,o>0?o:0)},[t,r])}function Ie(e){const t=N.useRef(e);return Ft(()=>{t.current=e}),N.useCallback((...r)=>{var n;return(n=t.current)===null||n===void 0?void 0:n.call(t,...r)},[])}function Ys(e,t){typeof e=="function"?e(t):e&&(e.current=t)}function zo(e,t){return N.useMemo(()=>e==null&&t==null?null:r=>{Ys(e,r),Ys(t,r)},[e,t])}function Xs(e,t=!1){const r=N.useRef(!1);Ft(()=>{t&&r.current&&(r.current=!1,e())},[t,e]);const n=N.useCallback(()=>{r.current=!0},[]),o=N.useCallback(()=>{r.current=!1},[]);return{onFocus:n,onBlur:o}}function _n(){const[e,t]=N.useState(!1);return Ft(()=>{t(window.getComputedStyle(window.document.documentElement).direction==="rtl")},[]),e}function qs(){const[e]=N.useState({}),t=N.useCallback((o,a)=>{var s;(s=e[o])===null||s===void 0||s.forEach(l=>{a.isPropagationStopped()||l(a)})},[e]),r=N.useMemo(()=>({onPointerDown:o=>t(Ss,o),onPointerMove:o=>t(_s,o),onPointerUp:o=>t(Es,o),onPointerLeave:o=>t(Ts,o),onPointerCancel:o=>t(Ns,o),onKeyDown:o=>t(Eo,o),onKeyUp:o=>t(Zd,o),onWheel:o=>t(To,o)}),[t]),n=N.useCallback((o,a)=>(e[o]||(e[o]=[]),e[o].unshift(a),()=>{const s=e[o];s&&s.splice(0,s.length,...s.filter(l=>l!==a))}),[e]);return{registerSensors:r,subscribeSensors:n}}function Zs(e,t){const r=N.useRef(0),n=Sn(),o=Ie((...a)=>{r.current=Date.now(),e(a)});return N.useCallback((...a)=>{n(()=>{o(a)},t-(Date.now()-r.current))},[t,o,n])}const Ho=ar("slide"),Go=ar("slide_image");function ir({slide:e,offset:t,render:r,rect:n,imageFit:o,imageProps:a,onClick:s,onLoad:l,onError:f,style:h}){var c,i,u,g,p,m,x,C;const[O,v]=N.useState(vr),{publish:E}=_r(),{setTimeout:I}=Zr(),_=N.useRef(null);N.useEffect(()=>{t===0&&E(qr(O))},[t,O,E]);const M=Ie(B=>{("decode"in B?B.decode():Promise.resolve()).catch(()=>{}).then(()=>{B.parentNode&&(v(Xr),I(()=>{l?.(B)},0))})}),w=N.useCallback(B=>{_.current=B,B?.complete&&M(B)},[M]),y=N.useCallback(B=>{M(B.currentTarget)},[M]),T=Ie(()=>{v(Yr),f?.()}),A=Mo(e,o),P=(B,K)=>Number.isFinite(B)?B:K,D=P(Math.max(...((i=(c=e.srcSet)===null||c===void 0?void 0:c.map(B=>B.width))!==null&&i!==void 0?i:[]).concat(e.width?[e.width]:[]).filter(Boolean)),((u=_.current)===null||u===void 0?void 0:u.naturalWidth)||0),L=P(Math.max(...((p=(g=e.srcSet)===null||g===void 0?void 0:g.map(B=>B.height))!==null&&p!==void 0?p:[]).concat(e.height?[e.height]:[]).filter(Boolean)),((m=_.current)===null||m===void 0?void 0:m.naturalHeight)||0),U=D&&L?{maxWidth:`min(${D}px, 100%)`,maxHeight:`min(${L}px, 100%)`}:{maxWidth:"100%",maxHeight:"100%"},S=(x=e.srcSet)===null||x===void 0?void 0:x.sort((B,K)=>B.width-K.width).map(B=>`${B.src} ${B.width}w`).join(", "),b=()=>n&&!A&&e.width&&e.height?n.height/e.height*e.width:Number.MAX_VALUE,F=S&&n&&No()?`${Math.round(Math.min(b(),n.width))}px`:void 0,{style:$,className:G,...Y}=a||{};return N.createElement(N.Fragment,null,N.createElement("img",{ref:w,onLoad:y,onError:T,onClick:s,draggable:!1,className:lt(me(Go()),A&&me(Go("cover")),O!==Xr&&me(Go("loading")),G),style:{...U,...h,...$},...Y,alt:(C=e.alt)!==null&&C!==void 0?C:"",sizes:F,srcSet:S,src:e.src}),O!==Xr&&N.createElement("div",{className:me(Ho(Bd))},O===vr&&(r?.iconLoading?r.iconLoading():N.createElement(yu,{className:lt(me(wn),me(Ho(vr)))})),O===Yr&&(r?.iconError?r.iconError():N.createElement(vu,{className:lt(me(wn),me(Ho(Yr)))}))))}const xu=N.forwardRef(function({className:t,children:r,onFocus:n,onBlur:o,...a},s){const l=N.useRef(null),{trackFocusWithin:f}=$o();return N.createElement(iu,{nodeRef:l},N.createElement("div",{ref:zo(s,l),className:lt(me("root"),t),...f(n,o),...a},r))});var it;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL",e[e.ANIMATION=3]="ANIMATION"})(it||(it={}));function Qs(e,t,r,n,o){N.useEffect(()=>o?()=>{}:kr(e(Ss,t),e(_s,r),e(Es,n),e(Ts,n),e(Ns,n)),[e,t,r,n,o])}var Ut;(function(e){e[e.NONE=0]="NONE",e[e.SWIPE=1]="SWIPE",e[e.PULL=2]="PULL"})(Ut||(Ut={}));const jo=30;function ku({disableSwipeNavigation:e,closeOnBackdropClick:t},r,n,o,a,s,l,f,h,c,i,u,g,p,m,x){const C=N.useRef(0),O=N.useRef([]),v=N.useRef(void 0),E=N.useRef(0),I=N.useRef(Ut.NONE),_=N.useCallback(D=>{v.current===D.pointerId&&(v.current=void 0,I.current=Ut.NONE);const L=O.current;L.splice(0,L.length,...L.filter(U=>U.pointerId!==D.pointerId))},[]),M=N.useCallback(D=>{_(D),D.persist(),O.current.push(D)},[_]),w=N.useCallback(D=>O.current.find(({pointerId:L})=>D.pointerId===L),[]),y=Ie(D=>{M(D)}),T=(D,L)=>i&&D>L||c&&D<-L,A=Ie(D=>{const L=w(D);if(L)if(v.current===D.pointerId){const U=Date.now()-E.current,S=C.current;I.current===Ut.SWIPE?Math.abs(S)>.3*o||Math.abs(S)>5&&U<a?f(S,U):h(S):I.current===Ut.PULL&&(T(S,2*jo)?p(S,U):m(S)),C.current=0,I.current=Ut.NONE}else{const{target:U}=D;t&&U instanceof HTMLElement&&U===L.target&&(U.classList.contains(me(So))||U.classList.contains(me(Cs)))&&x()}_(D)}),P=Ie(D=>{const L=w(D);if(L){const U=v.current===D.pointerId;if(D.buttons===0){U&&C.current!==0?A(D):_(L);return}const S=D.clientX-L.clientX,b=D.clientY-L.clientY;if(v.current===void 0){const F=$=>{M(D),v.current=D.pointerId,E.current=Date.now(),I.current=$};Math.abs(S)>Math.abs(b)&&Math.abs(S)>jo&&n(S)?e||(F(Ut.SWIPE),s()):Math.abs(b)>Math.abs(S)&&T(b,jo)&&(F(Ut.PULL),u())}else U&&(I.current===Ut.SWIPE?(C.current=S,l(S)):I.current===Ut.PULL&&(C.current=b,g(b)))}});Qs(r,y,P,A)}function Cu({preventDefaultWheelX:e,preventDefaultWheelY:t}){const r=N.useRef(null),n=Ie(o=>{const a=Math.abs(o.deltaX)>Math.abs(o.deltaY);(a&&e||!a&&t||o.ctrlKey)&&o.preventDefault()});return N.useCallback(o=>{var a;o?o.addEventListener("wheel",n,{passive:!1}):(a=r.current)===null||a===void 0||a.removeEventListener("wheel",n),r.current=o},[n])}function Su(e,t,r,n,o,a,s,l,f){const h=N.useRef(0),c=N.useRef(0),i=N.useRef(void 0),u=N.useRef(void 0),g=N.useRef(0),p=N.useRef(void 0),m=N.useRef(0),{setTimeout:x,clearTimeout:C}=Zr(),O=N.useCallback(()=>{i.current&&(C(i.current),i.current=void 0)},[C]),v=N.useCallback(()=>{u.current&&(C(u.current),u.current=void 0)},[C]),E=Ie(()=>{e!==it.SWIPE&&(h.current=0,m.current=0,O(),v())});N.useEffect(E,[e,E]);const I=Ie(M=>{u.current=void 0,h.current===M&&f(h.current)}),_=Ie(M=>{if(M.ctrlKey||Math.abs(M.deltaY)>Math.abs(M.deltaX))return;const w=y=>{g.current=y,C(p.current),p.current=y>0?x(()=>{g.current=0,p.current=void 0},300):void 0};if(e===it.NONE){if(Math.abs(M.deltaX)<=1.2*Math.abs(g.current)){w(M.deltaX);return}if(!r(-M.deltaX))return;if(c.current+=M.deltaX,O(),Math.abs(c.current)>30)c.current=0,w(0),m.current=Date.now(),a();else{const y=c.current;i.current=x(()=>{i.current=void 0,y===c.current&&(c.current=0)},o)}}else if(e===it.SWIPE){let y=h.current-M.deltaX;if(y=Math.min(Math.abs(y),n)*Math.sign(y),h.current=y,s(y),v(),Math.abs(y)>.2*n){w(M.deltaX),l(y,Date.now()-m.current);return}u.current=x(()=>I(y),2*o)}else w(M.deltaX)});N.useEffect(()=>t(To,_),[t,_])}const Js=ar("container"),Rs=N.createContext(null),zt=At("useController","ControllerContext",Rs);function _u({children:e,...t}){var r;const{carousel:n,animation:o,controller:a,on:s,styles:l,render:f}=t,{closeOnPullUp:h,closeOnPullDown:c,preventDefaultWheelX:i,preventDefaultWheelY:u}=a,[g,p]=N.useState(),m=Ot(),x=du(),[C,O]=N.useState(it.NONE),v=N.useRef(0),E=N.useRef(0),I=N.useRef(1),{registerSensors:_,subscribeSensors:M}=qs(),{subscribe:w,publish:y}=_r(),T=Sn(),A=Sn(),P=Sn(),{containerRef:D,setContainerRef:L,containerRect:U}=Ks(),S=zo(Cu({preventDefaultWheelX:i,preventDefaultWheelY:u}),L),b=N.useRef(null),F=zo(b,void 0),{getOwnerDocument:$}=Sr(),G=_n(),Y=V=>(G?-1:1)*(typeof V=="number"?V:1),B=Ie(()=>{var V;return(V=D.current)===null||V===void 0?void 0:V.focus()}),K=Ie(()=>t),j=Ie(()=>m),H=N.useCallback(V=>y(Rt,V),[y]),q=N.useCallback(V=>y(er,V),[y]),ue=N.useCallback(()=>y(xr),[y]),te=V=>!(n.finite&&(Y(V)>0&&m.currentIndex===0||Y(V)<0&&m.currentIndex===m.slides.length-1)),fe=V=>{var re;v.current=V,(re=D.current)===null||re===void 0||re.style.setProperty(We("swipe_offset"),`${Math.round(V)}px`)},ee=V=>{var re,oe;E.current=V,I.current=(()=>{const ke=c&&V>0?V:h&&V<0?-V:0;return Math.min(Math.max(Ao(1-ke/60*(1-.5),2),.5),1)})(),(re=D.current)===null||re===void 0||re.style.setProperty(We("pull_offset"),`${Math.round(V)}px`),(oe=D.current)===null||oe===void 0||oe.style.setProperty(We("pull_opacity"),`${I.current}`)},{prepareAnimation:xe}=Uo(b,(V,re,oe)=>{if(b.current&&U)return{keyframes:[{transform:`translate(0, ${V.rect.y-re.y+oe.y}px)`,opacity:V.opacity},{transform:"translate(0, 0)",opacity:1}],duration:V.duration,easing:o.easing.fade}}),he=(V,re)=>{if(h||c){ee(V);let oe=0;b.current&&(oe=o.fade*(re?2:1),xe({rect:b.current.getBoundingClientRect(),opacity:I.current,duration:oe})),P(()=>{ee(0),O(it.NONE)},oe),O(it.ANIMATION),re||ue()}},{prepareAnimation:Ge,isAnimationPlaying:De}=Uo(b,(V,re,oe)=>{var ae;if(b.current&&U&&(!((ae=m.animation)===null||ae===void 0)&&ae.duration)){const be=kn(n.spacing),ke=(be.percent?be.percent*U.width/100:be.pixel)||0;return{keyframes:[{transform:`translate(${Y(m.globalIndex-V.index)*(U.width+ke)+V.rect.x-re.x+oe.x}px, 0)`},{transform:"translate(0, 0)"}],duration:m.animation.duration,easing:m.animation.easing}}}),ye=Ie(V=>{var re,oe;const ae=V.offset||0,be=ae?o.swipe:(re=o.navigation)!==null&&re!==void 0?re:o.swipe,ke=!ae&&!De()?o.easing.navigation:o.easing.swipe;let{direction:He}=V;const Pe=(oe=V.count)!==null&&oe!==void 0?oe:1;let Ve=it.ANIMATION,Oe=be*Pe;if(!He){const se=U?.width,ie=V.duration||0,le=se?be/se*Math.abs(ae):be;Pe!==0?(ie<le?Oe=Oe/le*Math.max(ie,le/5):se&&(Oe=be/se*(se-Math.abs(ae))),He=Y(ae)>0?Rt:er):Oe=be/2}let X=0;He===Rt?te(Y(1))?X=-Pe:(Ve=it.NONE,Oe=be):He===er&&(te(Y(-1))?X=Pe:(Ve=it.NONE,Oe=be)),Oe=Math.round(Oe),A(()=>{fe(0),O(it.NONE)},Oe),b.current&&Ge({rect:b.current.getBoundingClientRect(),index:m.globalIndex}),O(Ve),y(_o,{type:"swipe",increment:X,duration:Oe,easing:ke})});N.useEffect(()=>{var V,re;!((V=m.animation)===null||V===void 0)&&V.increment&&(!((re=m.animation)===null||re===void 0)&&re.duration)&&T(()=>x({type:"swipe",increment:0}),m.animation.duration)},[m.animation,x,T]);const Me=[M,te,U?.width||0,o.swipe,()=>O(it.SWIPE),V=>fe(V),(V,re)=>ye({offset:V,duration:re,count:1}),V=>ye({offset:V,count:0})],Ce=[()=>{c&&O(it.PULL)},V=>ee(V),V=>he(V),V=>he(V,!0)];ku(a,...Me,h,c,...Ce,ue),Su(C,...Me);const we=Ie(()=>{a.focus&&$().querySelector(`.${me(ko)} .${me(Js())}`)&&B()});N.useEffect(we,[we]);const Fe=Ie(()=>{var V;(V=s.view)===null||V===void 0||V.call(s,{index:m.currentIndex})});N.useEffect(Fe,[m.globalIndex,Fe]),N.useEffect(()=>kr(w(Rt,V=>ye({direction:Rt,...V})),w(er,V=>ye({direction:er,...V})),w(_o,V=>x(V))),[w,ye,x]);const Le=N.useMemo(()=>({prev:H,next:q,close:ue,focus:B,slideRect:U?tu(U,n.padding):{width:0,height:0},containerRect:U||{width:0,height:0},subscribeSensors:M,containerRef:D,setCarouselRef:F,toolbarWidth:g,setToolbarWidth:p}),[H,q,ue,B,M,U,D,F,g,p,n.padding]);return N.useImperativeHandle(a.ref,()=>({prev:H,next:q,close:ue,focus:B,getLightboxProps:K,getLightboxState:j}),[H,q,ue,B,K,j]),N.createElement("div",{ref:S,className:lt(me(Js()),me(wr)),style:{...C===it.SWIPE?{[We("swipe_offset")]:`${Math.round(v.current)}px`}:null,...C===it.PULL?{[We("pull_offset")]:`${Math.round(E.current)}px`,[We("pull_opacity")]:`${I.current}`}:null,...a.touchAction!=="none"?{[We("controller_touch_action")]:a.touchAction}:null,...l.container},tabIndex:-1,..._},U&&N.createElement(Rs.Provider,{value:Le},e,(r=f.controls)===null||r===void 0?void 0:r.call(f)))}const Eu=Mt(Wr,_u);function cr(e){return or(bs,e)}function el(e){return or(So,e)}function Tu({slide:e,offset:t}){const r=N.useRef(null),{currentIndex:n,slides:o}=Ot(),{slideRect:a,focus:s}=zt(),{render:l,carousel:{imageFit:f,imageProps:h},on:{click:c},styles:{slide:i},labels:u}=rt(),{getOwnerDocument:g}=Sr(),p=t!==0;N.useEffect(()=>{var x;p&&(!((x=r.current)===null||x===void 0)&&x.contains(g().activeElement))&&s()},[p,s,g]);const m=()=>{var x,C,O,v;let E=(x=l.slide)===null||x===void 0?void 0:x.call(l,{slide:e,offset:t,rect:a});return!E&&lr(e)&&(E=N.createElement(ir,{slide:e,offset:t,render:l,rect:a,imageFit:f,imageProps:h,onClick:p?void 0:()=>c?.({index:n})})),E?N.createElement(N.Fragment,null,(C=l.slideHeader)===null||C===void 0?void 0:C.call(l,{slide:e}),((O=l.slideContainer)!==null&&O!==void 0?O:(({children:I})=>I))({slide:e,children:E}),(v=l.slideFooter)===null||v===void 0?void 0:v.call(l,{slide:e})):null};return N.createElement("div",{ref:r,className:lt(me(el()),!p&&me(el("current")),me(wr)),...ou(p),style:i,role:"group","aria-roledescription":sr(u,"Slide"),"aria-label":Ds(u,o,n+t)},m())}function Nu(){const e=rt().styles.slide;return N.createElement("div",{className:me(So),style:e})}function Au({carousel:e,labels:t}){const{slides:r,currentIndex:n,globalIndex:o}=Ot(),{setCarouselRef:a}=zt(),{autoPlaying:s,focusWithin:l}=$o(),f=kn(e.spacing),h=kn(e.padding),c=Ps(e,r,1),i=[];if(Io(r))for(let u=n-c;u<=n+c;u+=1){const g=Do(r,u),p=o-n+u,m=e.finite&&(u<0||u>r.length-1);i.push(m?{key:p}:{key:[`${p}`,Ls(g)].filter(Boolean).join("|"),offset:u-n,slide:g})}return N.createElement("div",{ref:a,className:lt(me(cr()),i.length>0&&me(cr("with_slides"))),style:{[`${We(cr("slides_count"))}`]:i.length,[`${We(cr("spacing_px"))}`]:f.pixel||0,[`${We(cr("spacing_percent"))}`]:f.percent||0,[`${We(cr("padding_px"))}`]:h.pixel||0,[`${We(cr("padding_percent"))}`]:h.percent||0},role:"region","aria-live":s&&!l?"off":"polite","aria-roledescription":sr(t,"Carousel"),"aria-label":sr(t,"Photo gallery")},i.map(({key:u,slide:g,offset:p})=>g?N.createElement(Tu,{key:u,slide:g,offset:p}):N.createElement(Nu,{key:u})))}const Mu=Mt(bs,Au);function tl(){const{carousel:e}=rt(),{slides:t,currentIndex:r}=Ot(),n=t.length===0||e.finite&&r===0,o=t.length===0||e.finite&&r===t.length-1;return{prevDisabled:n,nextDisabled:o}}function rl(e){var t;const r=_n(),{publish:n}=_r(),{animation:o}=rt(),{prevDisabled:a,nextDisabled:s}=tl(),l=((t=o.navigation)!==null&&t!==void 0?t:o.swipe)/2,f=Zs(()=>n(Rt),l),h=Zs(()=>n(er),l),c=Ie(i=>{switch(i.key){case Qd:n(xr);break;case Jd:(r?s:a)||(r?h:f)();break;case Rd:(r?a:s)||(r?f:h)();break}});N.useEffect(()=>e(Eo,c),[e,c])}function nl({label:e,icon:t,renderIcon:r,action:n,onClick:o,disabled:a,style:s}){return N.createElement(Er,{label:e,icon:t,renderIcon:r,className:me(`navigation_${n}`),disabled:a,onClick:o,style:s,...Xs(zt().focus,a)})}function Ou({render:{buttonPrev:e,buttonNext:t,iconPrev:r,iconNext:n},styles:o}){const{prev:a,next:s,subscribeSensors:l}=zt(),{prevDisabled:f,nextDisabled:h}=tl();return rl(l),N.createElement(N.Fragment,null,e?e():N.createElement(nl,{label:"Previous",action:Rt,icon:mu,renderIcon:r,style:o.navigationPrev,disabled:f,onClick:a}),t?t():N.createElement(nl,{label:"Next",action:er,icon:bu,renderIcon:n,style:o.navigationNext,disabled:h,onClick:s}))}const Iu=Mt(zd,Ou),ol=me(Xd),Du=me(ks);function Lu(e){return"style"in e}function al(e,t,r){const n=window.getComputedStyle(e),o=r?"padding-left":"padding-right",a=r?n.paddingLeft:n.paddingRight,s=e.style.getPropertyValue(o);return e.style.setProperty(o,`${(xn(a)||0)+t}px`),()=>{s?e.style.setProperty(o,s):e.style.removeProperty(o)}}function Pu({noScroll:{disabled:e},children:t}){const r=_n(),{getOwnerDocument:n,getOwnerWindow:o}=Sr();return N.useEffect(()=>{if(e)return()=>{};const a=[],s=o(),{body:l,documentElement:f}=n(),h=Math.round(s.innerWidth-f.clientWidth);if(h>0){a.push(al(l,h,r));const c=l.getElementsByTagName("*");for(let i=0;i<c.length;i+=1){const u=c[i];Lu(u)&&s.getComputedStyle(u).getPropertyValue("position")==="fixed"&&!u.classList.contains(Du)&&a.push(al(u,h,r))}}return l.classList.add(ol),()=>{l.classList.remove(ol),a.forEach(c=>c())}},[r,e,n,o]),N.createElement(N.Fragment,null,t)}const $u=Mt(Hd,Pu);function sl(e){return or(ko,e)}function ll(e,t,r){const n=e.getAttribute(t);return e.setAttribute(t,r),()=>{n?e.setAttribute(t,n):e.removeAttribute(t)}}function Fu({children:e,animation:t,styles:r,className:n,on:o,portal:a,close:s,labels:l}){const[f,h]=N.useState(!1),[c,i]=N.useState(!1),u=N.useRef([]),g=N.useRef(null),{setTimeout:p}=Zr(),{subscribe:m}=_r(),C=Fo()?0:t.fade;N.useEffect(()=>(h(!0),()=>{h(!1),i(!1)}),[]);const O=Ie(()=>{u.current.forEach(_=>_()),u.current=[]}),v=Ie(()=>{var _;i(!1),O(),(_=o.exiting)===null||_===void 0||_.call(o),p(()=>{var M;(M=o.exited)===null||M===void 0||M.call(o),s()},C)});N.useEffect(()=>m(xr,v),[m,v]);const E=Ie(_=>{var M,w,y;au(_),i(!0),(M=o.entering)===null||M===void 0||M.call(o);const T=(y=(w=_.parentNode)===null||w===void 0?void 0:w.children)!==null&&y!==void 0?y:[];for(let A=0;A<T.length;A+=1){const P=T[A];["TEMPLATE","SCRIPT","STYLE"].indexOf(P.tagName)===-1&&P!==_&&(u.current.push(ll(P,"inert","")),u.current.push(ll(P,"aria-hidden","true")))}u.current.push(()=>{var A,P;(P=(A=g.current)===null||A===void 0?void 0:A.focus)===null||P===void 0||P.call(A)}),p(()=>{var A;(A=o.entered)===null||A===void 0||A.call(o)},C)}),I=N.useCallback(_=>{_?E(_):O()},[E,O]);return f?Jt.createPortal(N.createElement(xu,{ref:I,className:lt(n,me(sl()),me(ks),c&&me(sl("open"))),"aria-modal":!0,role:"dialog","aria-label":sr(l,"Lightbox"),style:{...t.fade!==Po.animation.fade?{[We("fade_animation_duration")]:`${C}ms`}:null,...t.easing.fade!==Po.animation.easing.fade?{[We("fade_animation_timing_function")]:t.easing.fade}:null,...r.root},onFocus:_=>{g.current||(g.current=_.relatedTarget)}},e),a.root||document.body):null}const Uu=Mt(ko,Fu);function zu({children:e}){return N.createElement(N.Fragment,null,e)}const Hu=Mt(Gd,zu);function Gu(e){return or(ys,e)}function ju({toolbar:{buttons:e},render:{buttonClose:t,iconClose:r},styles:n}){const{close:o,setToolbarWidth:a}=zt(),{setContainerRef:s,containerRect:l}=Ks();Ft(()=>{a(l?.width)},[a,l?.width]);const f=()=>t?t():N.createElement(Er,{key:xr,label:"Close",icon:pu,renderIcon:r,onClick:o});return N.createElement("div",{ref:s,style:n.toolbar,className:me(Gu())},e?.map(h=>h===xr?f():h))}const Bu=Mt(ys,ju);function il(e,t){var r;return N.createElement(e.module.component,{key:e.module.name,...t},(r=e.children)===null||r===void 0?void 0:r.map(n=>il(n,t)))}function Wu(e,t={}){const{easing:r,...n}=e,{easing:o,...a}=t;return{easing:{...r,...o},...n,...a}}function Vu({carousel:e,animation:t,render:r,toolbar:n,controller:o,noScroll:a,on:s,plugins:l,slides:f,index:h,...c}){const{animation:i,carousel:u,render:g,toolbar:p,controller:m,noScroll:x,on:C,slides:O,index:v,plugins:E,...I}=Po,{config:_,augmentation:M}=su([tt(Uu,[tt($u,[tt(Eu,[tt(Mu),tt(Bu),tt(Iu)])])])],l||E),w=M({animation:Wu(i,t),carousel:{...u,...e},render:{...g,...r},toolbar:{...p,...n},controller:{...m,...o},noScroll:{...x,...a},on:{...C,...s},...I,...c});return w.open?N.createElement(Gs,{...w},N.createElement(fu,{slides:f||O,index:xn(h||v)},N.createElement(gu,null,N.createElement(cu,null,N.createElement(lu,null,il(tt(Hu,_),w)))))):null}const Ku={minZoom:1,maxZoomPixelRatio:1,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:500,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,pinchZoomV4:!1,scrollToZoom:!1};function Yu(e){return Math.min(Math.max(e,Number.EPSILON),1)}function cl(e){const{minZoom:t,...r}={...Ku,...e};return{minZoom:Yu(t),...r}}function Xu(e,t,r,n){const o=N.useRef(void 0),a=N.useRef(void 0),{zoom:s}=rt().animation,l=Fo(),f=Ie(()=>{var h,c,i;if((h=o.current)===null||h===void 0||h.cancel(),o.current=void 0,a.current&&n?.current){try{o.current=(i=(c=n.current).animate)===null||i===void 0?void 0:i.call(c,[{transform:a.current},{transform:`scale(${e}) translateX(${t}px) translateY(${r}px)`}],{duration:l?0:s??500,easing:o.current?"ease-out":"ease-in-out"})}catch(u){console.error(u)}a.current=void 0,o.current&&(o.current.onfinish=()=>{o.current=void 0})}});return Ft(f,[e,t,r,f]),N.useCallback(()=>{a.current=n?.current?window.getComputedStyle(n.current).transform:void 0},[n])}function qu(e,t){const{on:r}=rt(),n=Ie(()=>{var o;t||(o=r.zoom)===null||o===void 0||o.call(r,{zoom:e})});N.useEffect(n,[e,n])}function En(){const{zoom:e}=rt();return cl(e)}function Zu(e,t){var r,n;let o={width:0,height:0},a={width:0,height:0};const{currentSlide:s}=Ot(),{imageFit:l}=rt().carousel,{maxZoomPixelRatio:f}=En();if(e&&s){const c={...s,...t};if(lr(c)){const i=Mo(c,l),u=Math.max(...(((r=c.srcSet)===null||r===void 0?void 0:r.map(p=>p.width))||[]).concat(c.width?[c.width]:[])),g=Math.max(...(((n=c.srcSet)===null||n===void 0?void 0:n.map(p=>p.height))||[]).concat(c.height?[c.height]:[]));u>0&&g>0&&e.width>0&&e.height>0&&(a=i?{width:Math.round(Math.min(u,e.width/e.height*g)),height:Math.round(Math.min(g,e.height/e.width*u))}:{width:u,height:g},a={width:a.width*f,height:a.height*f},o=i?{width:Math.min(e.width,a.width,u),height:Math.min(e.height,a.height,g)}:{width:Math.round(Math.min(e.width,e.height/g*u,u)),height:Math.round(Math.min(e.height,e.width/u*g,g))})}}const h=o.width?Math.max(Ao(a.width/o.width,5),1):1;return{imageRect:o,maxZoom:h}}function dl(e,t){return Math.hypot(e.clientX-t.clientX,e.clientY-t.clientY)}function ul(e,t,r=100,n=2){return e*Math.min(1+Math.abs(t/r),n)**Math.sign(t)}function Qu(e,t,r,n,o,a,s,l,f){const h=N.useRef([]),c=N.useRef(0),i=N.useRef(void 0),{globalIndex:u}=Ot(),{getOwnerWindow:g}=Sr(),{containerRef:p,subscribeSensors:m}=zt(),{keyboardMoveDistance:x,zoomInMultiplier:C,wheelZoomDistanceFactor:O,scrollToZoom:v,doubleTapDelay:E,doubleClickDelay:I,doubleClickMaxStops:_,pinchZoomDistanceFactor:M,pinchZoomV4:w}=En(),y=N.useCallback(F=>{if(p.current){const{pageX:$,pageY:G}=F,{scrollX:Y,scrollY:B}=g(),{left:K,top:j,width:H,height:q}=p.current.getBoundingClientRect();return[$-K-Y-H/2,G-j-B-q/2]}return[]},[p,g]),T=Ie(F=>{const{key:$,metaKey:G,ctrlKey:Y}=F,B=G||Y,K=()=>{F.preventDefault(),F.stopPropagation()};if(e>1){const j=(H,q)=>{K(),l(H,q)};$==="ArrowDown"?j(0,x):$==="ArrowUp"?j(0,-x):$==="ArrowLeft"?j(-x,0):$==="ArrowRight"&&j(x,0)}$==="+"||B&&$==="="?(K(),o()):$==="-"||B&&$==="_"?(K(),a()):B&&$==="0"&&(K(),s(1))}),A=Ie(F=>{if((F.ctrlKey||v)&&Math.abs(F.deltaY)>Math.abs(F.deltaX)){F.stopPropagation(),s(ul(e,-F.deltaY,O),!0,...y(F));return}e>1&&(F.stopPropagation(),v||l(F.deltaX,F.deltaY))}),P=N.useCallback(F=>{const $=h.current;$.splice(0,$.length,...$.filter(G=>G.pointerId!==F.pointerId))},[]),D=N.useCallback(F=>{P(F),F.persist(),h.current.push(F)},[P]),L=Ie(F=>{var $;const G=h.current;if(F.pointerType==="mouse"&&F.buttons>1||!(!(($=f?.current)===null||$===void 0)&&$.contains(F.target)))return;e>1&&F.stopPropagation();const{timeStamp:Y}=F;if(G.length===0&&Y-c.current<(F.pointerType==="touch"?E:I)){c.current=0;const B=e>=1?e!==r?e*Math.max(r**(1/_),C):1:e!==t?e/Math.max(t**(-1/_),C):1;s(B,!1,...y(F))}else c.current=Y;if(D(F),G.length===2){const B=dl(G[0],G[1]);i.current={previousDistance:B,initialDistance:Math.max(B,1),initialZoom:e}}}),U=Ie(F=>{const $=h.current,G=$.find(Y=>Y.pointerId===F.pointerId);if($.length===2&&i.current){F.stopPropagation(),D(F);const Y=dl($[0],$[1]),B=w?i.current.initialZoom/i.current.initialDistance*Y:ul(e,Y-i.current.previousDistance,M);s(B,!0,...$.map(K=>y(K)).reduce((K,j)=>j.map((H,q)=>K[q]+H/2))),i.current.previousDistance=Y;return}e>1&&(F.stopPropagation(),G&&($.length===1&&l((G.clientX-F.clientX)/e,(G.clientY-F.clientY)/e),D(F)))}),S=N.useCallback(F=>{const $=h.current;$.length===2&&$.find(G=>G.pointerId===F.pointerId)&&(i.current=void 0),P(F)},[P]),b=N.useCallback(()=>{const F=h.current;F.splice(0,F.length),c.current=0,i.current=void 0},[]);Qs(m,L,U,S,n),N.useEffect(b,[u,b]),N.useEffect(()=>n?()=>{}:kr(b,m(Eo,T),m(To,A)),[n,m,b,T,A])}function Ju(e,t,r){const[n,o]=N.useState(1),[a,s]=N.useState(0),[l,f]=N.useState(0),h=Xu(n,a,l,r),{currentSlide:c,globalIndex:i}=Ot(),{containerRect:u,slideRect:g}=zt(),{minZoom:p,zoomInMultiplier:m}=En(),x=c&&lr(c)?c.src:void 0,C=!x||!r?.current;Ft(()=>{o(1),s(0),f(0)},[i,x]);const O=N.useCallback((M,w,y)=>{const T=y||n,A=a-(M||0),P=l-(w||0),D=(e.width*T-g.width)/2/T,L=(e.height*T-g.height)/2/T;s(Math.min(Math.abs(A),Math.max(D,0))*Math.sign(A)),f(Math.min(Math.abs(P),Math.max(L,0))*Math.sign(P))},[n,a,l,g,e.width,e.height]),v=N.useCallback((M,w,y,T)=>{const A=Ao(M+.01<t?M-.01>p?M:p:t,5);w||h(),O(y?y*(1/n-1/A):0,T?T*(1/n-1/A):0,A),o(A)},[n,p,t,O,h]),E=Ie(()=>{n>1&&(n>t&&v(t,!0),O())});Ft(E,[u.width,u.height,E]);const I=N.useCallback(()=>{const M=n*m;v(n<1&&M>1?1:M)},[n,m,v]),_=N.useCallback(()=>{const M=n/m;v(n>1&&M<1?1:M)},[n,m,v]);return{zoom:n,offsetX:a,offsetY:l,disabled:C,changeOffsets:O,changeZoom:v,zoomIn:I,zoomOut:_}}const fl=N.createContext(null),Bo=At("useZoom","ZoomControllerContext",fl);function Ru({children:e}){const[t,r]=N.useState(),{slideRect:n}=zt(),{ref:o,minZoom:a}=En(),{imageRect:s,maxZoom:l}=Zu(n,t?.imageDimensions),{zoom:f,offsetX:h,offsetY:c,disabled:i,changeZoom:u,changeOffsets:g,zoomIn:p,zoomOut:m}=Ju(s,l,t?.zoomWrapperRef);qu(f,i),Qu(f,a,l,i,p,m,u,g,t?.zoomWrapperRef);const x=N.useMemo(()=>({zoom:f,minZoom:a,maxZoom:l,offsetX:h,offsetY:c,disabled:i,zoomIn:p,zoomOut:m,changeZoom:u}),[f,a,l,h,c,i,p,m,u]);N.useImperativeHandle(o,()=>x,[x]);const C=N.useMemo(()=>({...x,setZoomWrapper:r}),[x,r]);return N.createElement(fl.Provider,{value:C},e)}const ef=mt("ZoomIn",N.createElement(N.Fragment,null,N.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"}),N.createElement("path",{d:"M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"}))),tf=mt("ZoomOut",N.createElement("path",{d:"M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14zM7 9h5v1H7z"})),gl=N.forwardRef(function({zoomIn:t,onLoseFocus:r},n){const o=N.useRef(!1),a=N.useRef(!1),{zoom:s,minZoom:l,maxZoom:f,zoomIn:h,zoomOut:c,disabled:i}=Bo(),{render:u}=rt(),g=i||(t?s>=f:s<=l);return N.useEffect(()=>{g&&o.current&&a.current&&r(),g||(o.current=!0)},[g,r]),N.createElement(Er,{ref:n,disabled:g,label:t?"Zoom in":"Zoom out",icon:t?ef:tf,renderIcon:t?u.iconZoomIn:u.iconZoomOut,onClick:t?h:c,onFocus:()=>{a.current=!0},onBlur:()=>{a.current=!1}})});function rf(){const e=N.useRef(null),t=N.useRef(null),{focus:r}=zt(),n=N.useCallback(s=>{var l,f;!((l=s.current)===null||l===void 0)&&l.disabled?r():(f=s.current)===null||f===void 0||f.focus()},[r]),o=N.useCallback(()=>n(e),[n]),a=N.useCallback(()=>n(t),[n]);return N.createElement(N.Fragment,null,N.createElement(gl,{zoomIn:!0,ref:e,onLoseFocus:a}),N.createElement(gl,{ref:t,onLoseFocus:o}))}function nf(){const{render:e}=rt(),t=Bo();return e.buttonZoom?N.createElement(N.Fragment,null,e.buttonZoom(t)):N.createElement(rf,null)}function of(e){var t;return(((t=e.srcSet)===null||t===void 0?void 0:t.length)||0)>0}function af({current:e,preload:t},{type:r,source:n}){switch(r){case"fetch":return e?{current:e,preload:n}:{current:n};case"done":return n===t?{current:n}:{current:e,preload:t};default:throw new Error(Os)}}function sf(e){var t,r;const[{current:n,preload:o},a]=N.useReducer(af,{}),{slide:s,rect:l,imageFit:f,render:h,interactive:c}=e,i=s.srcSet.sort((I,_)=>I.width-_.width),u=(t=s.width)!==null&&t!==void 0?t:i[i.length-1].width,g=(r=s.height)!==null&&r!==void 0?r:i[i.length-1].height,p=Mo(s,f),m=Math.max(...i.map(I=>I.width)),x=Math.min((p?Math.max:Math.min)(l.width,u*(l.height/g)),m),C=ru(),O=Ie(()=>{var I;const _=(I=i.find(M=>M.width>=x*C))!==null&&I!==void 0?I:i[i.length-1];(!n||i.findIndex(M=>M.src===n)<i.findIndex(M=>M===_))&&a({type:"fetch",source:_.src})});Ft(O,[l.width,l.height,C,O]);const v=Ie(I=>a({type:"done",source:I})),E={WebkitTransform:c?"initial":"translateZ(0)"};return p||Object.assign(E,l.width/l.height<u/g?{width:"100%",height:"auto"}:{width:"auto",height:"100%"}),N.createElement(N.Fragment,null,o&&o!==n&&N.createElement(ir,{key:"preload",...e,offset:void 0,slide:{...s,src:o,srcSet:void 0},style:{position:"absolute",visibility:"hidden",...E},onLoad:()=>v(o),render:{...h,iconLoading:()=>null,iconError:()=>null}}),n&&N.createElement(ir,{key:"current",...e,slide:{...s,src:n,srcSet:void 0},style:E}))}function lf({render:e,slide:t,offset:r,rect:n}){var o;const[a,s]=N.useState(),l=N.useRef(null),{zoom:f,maxZoom:h,offsetX:c,offsetY:i,setZoomWrapper:u}=Bo(),g=f>1,{carousel:p,on:m}=rt(),{currentIndex:x}=Ot();Ft(()=>r===0?(u({zoomWrapperRef:l,imageDimensions:a}),()=>u(void 0)):()=>{},[r,a,u]);let C=(o=e.slide)===null||o===void 0?void 0:o.call(e,{slide:t,offset:r,rect:n,zoom:f,maxZoom:h});if(!C&&lr(t)){const O={slide:t,offset:r,rect:n,render:e,imageFit:p.imageFit,imageProps:p.imageProps,onClick:r===0?()=>{var v;return(v=m.click)===null||v===void 0?void 0:v.call(m,{index:x})}:void 0};C=of(t)?N.createElement(sf,{...O,slide:t,interactive:g,rect:r===0?{width:n.width*f,height:n.height*f}:n}):N.createElement(ir,{onLoad:v=>s({width:v.naturalWidth,height:v.naturalHeight}),...O})}return C?N.createElement("div",{ref:l,className:lt(me(xs),me(wr),me(Cs),g&&me(qd)),style:r===0?{transform:`scale(${f}) translateX(${c}px) translateY(${i}px)`}:void 0},C):null}const cf=({augment:e,addModule:t})=>{e(({zoom:r,toolbar:n,render:o,controller:a,...s})=>{const l=cl(r);return{zoom:l,toolbar:Cn(n,ws,N.createElement(nf,null)),render:{...o,slide:f=>{var h;return lr(f.slide)?N.createElement(lf,{render:o,...f}):(h=o.slide)===null||h===void 0?void 0:h.call(o,f)}},controller:{...a,preventDefaultWheelY:l.scrollToZoom},...s}}),t(Mt(ws,Ru))},df={separator:"/",container:{}},hl=e=>({...df,...e});function uf({counter:e}){const{slides:t,currentIndex:r}=Ot(),{separator:n,container:{className:o,...a},className:s,...l}=hl(e);return t.length===0?null:N.createElement("div",{className:lt(me("counter"),o||s),...l,...a,"aria-hidden":!0},r+1," ",n," ",t.length)}function ff({augment:e,addChild:t}){e(({counter:r,...n})=>({counter:hl(r),...n})),t(Wr,Mt(jd,uf))}const gf={autoplay:!1,delay:3e3,ref:null},pl=e=>({...gf,...e}),ml=N.createContext(null),hf=At("useSlideshow","SlideshowContext",ml);function pf({slideshow:e,carousel:{finite:t},on:r,children:n}){const{autoplay:o,delay:a,ref:s}=pl(e),l=N.useRef(o),[f,h]=N.useState(o),c=N.useRef(void 0),i=N.useRef(void 0),{slides:u,currentIndex:g}=Ot(),{setTimeout:p,clearTimeout:m}=Zr(),{subscribe:x}=_r(),{next:C}=zt(),{setAutoPlaying:O}=$o();N.useEffect(()=>O(f),[f,O]);const v=u.length===0||t&&g===u.length-1,E=N.useCallback(()=>{!f&&!v&&h(!0)},[f,v]),I=N.useCallback(()=>{f&&h(!1)},[f]),_=N.useCallback(()=>{m(c.current),c.current=void 0},[m]),M=Ie(()=>{_(),!(!f||v||i.current===vr||i.current===Co)&&(c.current=p(()=>{f&&(i.current=void 0,C())},a))});N.useEffect(M,[g,f,M]),N.useEffect(()=>{f&&v&&h(!1)},[g,f,v]);const w=Ie(()=>{var A;return(A=r.slideshowStart)===null||A===void 0?void 0:A.call(r)}),y=Ie(()=>{var A;return(A=r.slideshowStop)===null||A===void 0?void 0:A.call(r)});N.useEffect(()=>{f?w():l.current&&y(),l.current=f},[f,w,y]),N.useEffect(()=>kr(_,x(Wd,()=>{i.current=vr,_()}),x(Vd,()=>{i.current=Co,_()}),x(Kd,()=>{i.current=Yr,M()}),x(Yd,()=>{i.current=Xr,M()})),[x,_,M]);const T=N.useMemo(()=>({playing:f,disabled:v,play:E,pause:I}),[f,v,E,I]);return N.useImperativeHandle(s,()=>T,[T]),N.createElement(ml.Provider,{value:T},n)}const mf=mt("Play",N.createElement("path",{d:"M8 5v14l11-7z"})),bf=mt("Pause",N.createElement("path",{d:"M6 19h4V5H6v14zm8-14v14h4V5h-4z"}));function yf(){const{playing:e,disabled:t,play:r,pause:n}=hf(),{render:o}=rt(),a=Xs(zt().focus,t);return o.buttonSlideshow?N.createElement(N.Fragment,null,o.buttonSlideshow({playing:e,disabled:t,play:r,pause:n})):N.createElement(Er,{label:e?"Pause":"Play",icon:e?bf:mf,renderIcon:e?o.iconSlideshowPause:o.iconSlideshowPlay,onClick:e?n:r,disabled:t,...a})}function vf({augment:e,addModule:t}){e(({slideshow:r,toolbar:n,...o})=>({toolbar:Cn(n,vs,N.createElement(yf,null)),slideshow:pl(r),...o})),t(Mt(vs,pf))}const wf={auto:!1,ref:null},bl=e=>({...wf,...e}),yl=N.createContext(null),xf=At("useFullscreen","FullscreenContext",yl);function kf({fullscreen:e,on:t,children:r}){const{auto:n,ref:o}=bl(e),a=N.useRef(null),[s,l]=N.useState(),[f,h]=N.useState(!1),c=N.useRef(!1),{getOwnerDocument:i}=Sr();Ft(()=>{var v,E,I,_;const M=i();l(!((_=(I=(E=(v=M.fullscreenEnabled)!==null&&v!==void 0?v:M.webkitFullscreenEnabled)!==null&&E!==void 0?E:M.mozFullScreenEnabled)!==null&&I!==void 0?I:M.msFullscreenEnabled)!==null&&_!==void 0&&_))},[i]);const u=N.useCallback(()=>{var v;const E=i(),I=E.fullscreenElement||E.webkitFullscreenElement||E.mozFullScreenElement||E.msFullscreenElement;return((v=I?.shadowRoot)===null||v===void 0?void 0:v.fullscreenElement)||I},[i]),g=N.useCallback(()=>{const v=a.current;try{v.requestFullscreen?v.requestFullscreen().catch(()=>{}):v.webkitRequestFullscreen?v.webkitRequestFullscreen():v.mozRequestFullScreen?v.mozRequestFullScreen():v.msRequestFullscreen&&v.msRequestFullscreen()}catch{}},[]),p=N.useCallback(()=>{if(!u())return;const v=i();try{v.exitFullscreen?v.exitFullscreen().catch(()=>{}):v.webkitExitFullscreen?v.webkitExitFullscreen():v.mozCancelFullScreen?v.mozCancelFullScreen():v.msExitFullscreen&&v.msExitFullscreen()}catch{}},[u,i]);N.useEffect(()=>{const v=i(),E=()=>{h(u()===a.current)};return kr(...["fullscreenchange","webkitfullscreenchange","mozfullscreenchange","MSFullscreenChange"].map(I=>(v.addEventListener(I,E),()=>v.removeEventListener(I,E))))},[u,i]);const m=Ie(()=>{var v;return(v=t.enterFullscreen)===null||v===void 0?void 0:v.call(t)}),x=Ie(()=>{var v;return(v=t.exitFullscreen)===null||v===void 0?void 0:v.call(t)});N.useEffect(()=>{f&&(c.current=!0),c.current&&(f?m:x)()},[f,m,x]);const C=Ie(()=>{var v;return(v=n?g:null)===null||v===void 0||v(),p});N.useEffect(C,[C]);const O=N.useMemo(()=>({fullscreen:f,disabled:s,enter:g,exit:p}),[f,s,g,p]);return N.useImperativeHandle(o,()=>O,[O]),N.createElement("div",{ref:a,className:lt(me(Vr),me(xs))},N.createElement(yl.Provider,{value:O},r))}const Cf=mt("EnterFullscreen",N.createElement("path",{d:"M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"})),Sf=mt("ExitFullscreen",N.createElement("path",{d:"M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"}));function _f(){var e;const{fullscreen:t,disabled:r,enter:n,exit:o}=xf(),{render:a}=rt();return r?null:a.buttonFullscreen?N.createElement(N.Fragment,null,(e=a.buttonFullscreen)===null||e===void 0?void 0:e.call(a,{fullscreen:t,disabled:r,enter:n,exit:o})):N.createElement(Er,{disabled:r,label:t?"Exit Fullscreen":"Enter Fullscreen",icon:t?Sf:Cf,renderIcon:t?a.iconExitFullscreen:a.iconEnterFullscreen,onClick:t?o:n})}function Ef({augment:e,contains:t,addParent:r}){e(({fullscreen:n,toolbar:o,...a})=>({toolbar:Cn(o,Vr,N.createElement(_f,null)),fullscreen:bl(n),...a})),r(t(Kr)?Kr:Wr,Mt(Vr,kf))}const dr={ref:null,position:"bottom",width:120,height:80,border:1,borderRadius:4,padding:4,gap:16,imageFit:"contain",vignette:!0,hidden:!1,showToggle:!1},Wo=e=>({...dr,...e});function vl(){const{thumbnails:e}=rt();return Wo(e)}const ur=e=>or(Kr,e),nt=e=>ur(or("thumbnail",e)),Tf=mt("VideoThumbnail",N.createElement("path",{d:"M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"})),Nf=mt("UnknownThumbnail",N.createElement("path",{d:"M23 18V6c0-1.1-.9-2-2-2H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zM8.5 12.5l2.5 3.01L14.5 11l4.5 6H5l3.5-4.5z"}));function Af({slide:e,render:t,rect:r,imageFit:n}){var o;const a=(o=t.thumbnail)===null||o===void 0?void 0:o.call(t,{slide:e,render:t,rect:r,imageFit:n});if(a)return a;const s={render:t,rect:r,imageFit:n};if(e.thumbnail)return N.createElement(ir,{slide:{src:e.thumbnail},...s});if(lr(e))return N.createElement(ir,{slide:e,...s});const l=me(nt(wn));return e.type==="video"?N.createElement(N.Fragment,null,e.poster&&N.createElement(ir,{slide:{src:e.poster},...s}),N.createElement(Tf,{className:l})):N.createElement(Nf,{className:l})}const Mf=ar("active"),Vo=ar("fadein"),Ko=ar("fadeout"),Of=ar("placeholder"),wl="delay",xl="duration";function If({slide:e,index:t,onClick:r,fadeIn:n,fadeOut:o,placeholder:a,onLoseFocus:s}){const l=N.useRef(null),{render:f,styles:h,labels:c}=rt(),{slides:i,globalIndex:u}=Ot(),{getOwnerDocument:g}=Sr(),{width:p,height:m,imageFit:x}=vl(),C={width:p,height:m},O=t===u,v=Ie(s);return N.useEffect(()=>{o&&g().activeElement===l.current&&v()},[o,v,g]),N.createElement("button",{ref:l,type:"button",className:lt(me(wr),me(nt()),O&&me(nt(Mf())),n&&me(nt(Vo())),o&&me(nt(Ko())),a&&me(nt(Of()))),style:{...n?{[We(nt(Vo(xl)))]:`${n.duration}ms`,[We(nt(Vo(wl)))]:`${n.delay}ms`}:null,...o?{[We(nt(Ko(xl)))]:`${o.duration}ms`,[We(nt(Ko(wl)))]:`${o.delay}ms`}:null,...h.thumbnail},onClick:r,"aria-current":O?!0:void 0,"aria-label":Ds(c,i,t)},e&&Af({slide:e,render:f,rect:C,imageFit:x}))}function kl(e){return["top","bottom"].includes(e)}function Cl(e,t){return t+2*(e.border+e.padding)+e.gap}function Df(e){const{thumbnail:t,poster:r}=e||{thumbnail:"placeholder"};return typeof t=="string"&&t||typeof r=="string"&&r||e&&Ls(e)||void 0}function Sl({visible:e,containerRef:t}){const r=N.useRef(null),n=_n(),{publish:o,subscribe:a}=_r(),{carousel:s,styles:l,labels:f}=rt(),{slides:h,globalIndex:c,animation:i}=Ot(),{registerSensors:u,subscribeSensors:g}=qs();rl(g);const p=vl(),{position:m,width:x,height:C,border:O,borderStyle:v,borderColor:E,borderRadius:I,padding:_,gap:M,vignette:w}=p,y=i?.duration!==void 0&&i?.increment||0,T=i?.duration||0,{prepareAnimation:A}=Uo(r,S=>({keyframes:kl(m)?[{transform:`translateX(${(n?-1:1)*Cl(p,x)*y+S}px)`},{transform:"translateX(0)"}]:[{transform:`translateY(${Cl(p,C)*y+S}px)`},{transform:"translateY(0)"}],duration:T,easing:i?.easing})),P=Ie(()=>{let S=0;if(t.current&&r.current){const b=t.current.getBoundingClientRect(),F=r.current.getBoundingClientRect();S=kl(m)?F.left-b.left-(b.width-F.width)/2:F.top-b.top-(b.height-F.height)/2}A(S)});N.useEffect(()=>kr(a(_o,P)),[a,P]);const D=Ps(s,h),L=[];if(Io(h))for(let S=c-D-Math.abs(y);S<=c+D+Math.abs(y);S+=1){const F=s.finite&&(S<0||S>h.length-1)||y<0&&S<c-D||y>0&&S>c+D?null:Do(h,S),$=[`${S}`,Df(F)].filter(Boolean).join("|");L.push({key:$,index:S,slide:F})}const U=S=>()=>{S>c?o(er,{count:S-c}):S<c&&o(Rt,{count:c-S})};return N.createElement("div",{className:lt(me(ur("container")),me(wr)),style:{...e?null:{display:"none"},...x!==dr.width?{[We(nt("width"))]:`${x}px`}:null,...C!==dr.height?{[We(nt("height"))]:`${C}px`}:null,...O!==dr.border?{[We(nt("border"))]:`${O}px`}:null,...v?{[We(nt("border_style"))]:v}:null,...E?{[We(nt("border_color"))]:E}:null,...I!==dr.borderRadius?{[We(nt("border_radius"))]:`${I}px`}:null,..._!==dr.padding?{[We(nt("padding"))]:`${_}px`}:null,...M!==dr.gap?{[We(nt("gap"))]:`${M}px`}:null,...l.thumbnailsContainer}},N.createElement("nav",{ref:r,style:l.thumbnailsTrack,className:lt(me(ur("track")),me(wr)),"aria-label":sr(f,"Thumbnails"),tabIndex:-1,...u},L.map(({key:S,index:b,slide:F})=>{const $=T/Math.abs(y||1),G=y>0&&b>c+D-y&&b<=c+D||y<0&&b<c-D-y&&b>=c-D?{duration:$,delay:((y>0?b-(c+D-y):c-D-y-b)-1)*$}:void 0,Y=y>0&&b<c-D||y<0&&b>c+D?{duration:$,delay:(y>0?y-(c-D-b):-y-(b-(c+D)))*$}:void 0;return N.createElement(If,{key:S,index:b,slide:F,fadeIn:G,fadeOut:Y,placeholder:!F,onClick:U(b),onLoseFocus:()=>{var B;return(B=r.current)===null||B===void 0?void 0:B.focus()}})})),w&&N.createElement("div",{className:me(ur("vignette"))}))}const _l=N.createContext(null),Lf=At("useThumbnails","ThumbnailsContext",_l);function Pf({children:e,...t}){const{ref:r,position:n,hidden:o}=Wo(t.thumbnails),[a,s]=N.useState(!o),l=N.useRef(null),f=N.useMemo(()=>({visible:a,show:()=>s(!0),hide:()=>s(!1)}),[a]);return N.useImperativeHandle(r,()=>f,[f]),N.createElement(Gs,{...t},N.createElement(_l.Provider,{value:f},N.createElement("div",{ref:l,className:lt(me(ur()),me(ur(`${n}`)))},["start","top"].includes(n)&&N.createElement(Sl,{containerRef:l,visible:a}),N.createElement("div",{className:me(ur("wrapper"))},e),["end","bottom"].includes(n)&&N.createElement(Sl,{containerRef:l,visible:a}))))}const El=()=>N.createElement(N.Fragment,null,N.createElement("path",{strokeWidth:2,stroke:"currentColor",strokeLinejoin:"round",fill:"none",d:"M3 5l18 0l0 14l-18 0l0-14z"}),N.createElement("path",{d:"M5 14h4v3h-4zM10 14h4v3h-4zM15 14h4v3h-4z"})),$f=mt("ThumbnailsVisible",El()),Ff=hu("ThumbnailsHidden",El());function Uf(){const{visible:e,show:t,hide:r}=Lf(),{render:n}=rt();return n.buttonThumbnails?N.createElement(N.Fragment,null,n.buttonThumbnails({visible:e,show:t,hide:r})):N.createElement(Er,{label:e?"Hide thumbnails":"Show thumbnails",icon:e?$f:Ff,renderIcon:e?n.iconThumbnailsVisible:n.iconThumbnailsHidden,onClick:e?r:t})}function zf({augment:e,contains:t,append:r,addParent:n}){e(({thumbnails:a,toolbar:s,...l})=>{const f=Wo(a);return{toolbar:Cn(s,Kr,f.showToggle?N.createElement(Uf,null):null),thumbnails:f,...l}});const o=Mt(Kr,Pf);t(Vr)?r(Vr,o):n(Wr,o)}const Ct=({className:e="",children:t,style:r,...n})=>d.jsx("i",{className:e,style:r,...n,children:t}),Yo=e=>{if(!e||typeof window>"u"||!window.location||!/^(https?:)?\/\//i.test(e))return!1;try{return new URL(e,window.location.href).origin!==window.location.origin}catch{return!1}},Xo=window.G7Core,Tn=(e,t)=>Xo?.t?.(e,t)??e,Hf=async(e,t)=>{try{const r=await Xo.api.get(e,{responseType:"blob"});if(r){const n=URL.createObjectURL(r),o=document.createElement("a");o.href=n,o.download=t,document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(n)}}catch(r){console.error("Failed to download file:",r),Xo?.toast?.error?.(Tn("common.download_failed"))}},Gf=(e,t)=>{const r=document.createElement("a");r.href=e,r.download=t,r.target="_blank",document.body.appendChild(r),r.click(),document.body.removeChild(r)},Tl=async e=>{const t=e.downloadUrl||e.src,r=e.filename||e.title||"image";e.downloadRequiresAuth&&!Yo(t)?await Hf(t,r):Gf(t,r)},jf=({image:e,index:t,onDownload:r})=>{const[n,o]=k.useState(!1),a=async s=>{if(s.stopPropagation(),r){r(e,t);return}o(!0);try{await Tl(e)}finally{o(!1)}};return d.jsx(Q,{type:"button",onClick:a,disabled:n,className:"yarl__button flex items-center justify-center","aria-label":Tn("common.download"),title:Tn("common.download"),children:n?d.jsx(Ct,{className:"fa-solid fa-spinner fa-spin text-white"}):d.jsx(Ct,{className:"fa-solid fa-download text-white"})})},Tr=({images:e,isOpen:t,onClose:r,startIndex:n=0,enableZoom:o=!0,enableSlideshow:a=!1,enableFullscreen:s=!0,showCounter:l=!0,showDownload:f=!0,showThumbnails:h=!0,onDownload:c,editorAttrs:i,id:u})=>{const[g,p]=k.useState(n),m=k.useRef(n),x=e.map(v=>({src:v.src,title:v.title,description:v.description})),C=[];o&&C.push(cf),a&&C.push(vf),s&&C.push(Ef),l&&C.push(ff),h&&C.push(zf);const O=e[g];if(k.useEffect(()=>{m.current=g},[g]),i){const v=(e??[]).slice(0,4);return d.jsx(W,{id:u,className:"grid grid-cols-2 gap-2 p-3 rounded-lg border border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800",...i,children:v.length>0?v.map((E,I)=>d.jsx(Lt,{src:E.thumbnail||E.src,alt:E.title||"",className:"w-full aspect-square object-cover rounded bg-gray-200 dark:bg-gray-700"},I)):d.jsxs(W,{className:"col-span-2 flex items-center justify-center py-6 text-sm text-gray-400 dark:text-gray-500",children:[d.jsx(Ct,{className:"fa-regular fa-images mr-2"}),Tn("editor.component.image_gallery")]})})}return d.jsx(Vu,{open:t,close:r,slides:x,index:g,plugins:C,on:{view:({index:v})=>{v!==m.current&&p(v)}},zoom:{maxZoomPixelRatio:3,zoomInMultiplier:2,doubleTapDelay:300,doubleClickDelay:300,doubleClickMaxStops:2,keyboardMoveDistance:50,wheelZoomDistanceFactor:100,pinchZoomDistanceFactor:100,scrollToZoom:!0},carousel:{finite:!0,preload:2,padding:"16px",spacing:"30%"},animation:{fade:250,swipe:500,easing:{fade:"ease",swipe:"ease-out",navigation:"ease-in-out"}},controller:{closeOnBackdropClick:!0,closeOnPullDown:!0,closeOnPullUp:!0},thumbnails:{position:"bottom",width:120,height:80,border:2,borderRadius:4,padding:4,gap:16,showToggle:!1,vignette:!0},toolbar:{buttons:[f&&O&&d.jsx(jf,{image:O,index:g,onDownload:c},"download"),"close"].filter(Boolean)},styles:{container:{backgroundColor:"rgba(0, 0, 0, 0.9)"}}})},Nl=()=>{const[e,t]=k.useState(!1),[r,n]=k.useState([]),[o,a]=k.useState(0),s=k.useCallback((f,h=0)=>{n(f),a(h),t(!0)},[]),l=k.useCallback(()=>{t(!1)},[]);return{isOpen:e,openGallery:s,closeGallery:l,galleryProps:{images:r,isOpen:e,onClose:l,startIndex:o}}};Tr.displayName="ImageGallery";const Al=Object.freeze(Object.defineProperty({__proto__:null,ImageGallery:Tr,default:Tr,executeImageDownload:Tl,useImageGallery:Nl},Symbol.toStringTag,{value:"Module"})),Bf=window.G7Core,qo=(e,t)=>Bf?.t?.(e,t)??e,Nn=e=>e.url??e.download_url,An=({images:e=[],className:t="",id:r,editorAttrs:n})=>{const[o,a]=k.useState(0),{openGallery:s,galleryProps:l}=Nl(),f=k.useMemo(()=>e.map(c=>({src:Nn(c),title:c.alt_text_current??"",thumbnail:Nn(c),downloadUrl:c.download_url,filename:c.alt_text_current??`image-${c.id}`})),[e]);if(!e||e.length===0)return d.jsx(W,{className:`flex items-center justify-center bg-gray-100 dark:bg-gray-700 rounded-lg aspect-square ${t}`,id:r,...n,children:d.jsxs(W,{className:"text-center text-gray-400 dark:text-gray-500",children:[d.jsx(de,{name:"image",size:"3x",className:"mb-3 opacity-50"}),d.jsx(W,{className:"text-sm",children:qo("shop.no_image")})]})});const h=e[o]??e[0];return d.jsxs(W,{id:r,className:t,...n,children:[d.jsx(W,{className:"relative overflow-hidden rounded-lg bg-gray-100 dark:bg-gray-700 aspect-square mb-3",children:d.jsx(Q,{type:"button",className:"w-full h-full cursor-zoom-in block",onClick:()=>s(f,o),"aria-label":qo("shop.view_image"),children:d.jsx(Lt,{src:Nn(h),alt:h.alt_text_current??"",className:"w-full h-full object-contain"})})}),e.length>1&&d.jsx(W,{className:"flex gap-2 overflow-x-auto pb-1",children:e.map((c,i)=>d.jsx(Q,{type:"button",className:`flex-shrink-0 w-16 h-16 rounded-md overflow-hidden border-2 transition-colors cursor-pointer ${i===o?"border-gray-900 dark:border-white":"border-gray-200 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-400"}`,onClick:()=>a(i),"aria-label":c.alt_text_current??`${qo("shop.image")} ${i+1}`,children:d.jsx(Lt,{src:Nn(c),alt:c.alt_text_current??"",className:"w-full h-full object-cover"})},c.id))}),d.jsx(Tr,{...l})]})};An.displayName="ProductImageViewer";const Wf=Object.freeze(Object.defineProperty({__proto__:null,ProductImageViewer:An,default:An},Symbol.toStringTag,{value:"Module"})),Ml=({value:e,min:t=1,max:r=999,onChange:n,size:o="md",disabled:a=!1,className:s="",id:l,editorAttrs:f})=>{const c={sm:{container:"h-8",button:"w-8 h-8 text-sm",input:"w-10 text-sm"},md:{container:"h-10",button:"w-10 h-10",input:"w-14"},lg:{container:"h-12",button:"w-12 h-12 text-lg",input:"w-16 text-lg"}}[o],i=()=>{e>t&&!a&&n(e-1)},u=()=>{e<r&&!a&&n(e+1)},g=C=>{const O=parseInt(C.target.value,10);if(!isNaN(O)){const v=Math.max(t,Math.min(r,O));n(v)}},p=C=>{const O=parseInt(C.target.value,10);isNaN(O)||O<t?n(t):O>r&&n(r)},m=a||e<=t,x=a||e>=r;return d.jsxs(W,{className:`inline-flex items-center border border-gray-300 dark:border-gray-600 rounded-lg ${c.container} ${s}`,id:l,...f,children:[d.jsx(Q,{type:"button",onClick:i,disabled:m,className:`${c.button} flex items-center justify-center text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-l-lg transition-colors`,"aria-label":"수량 감소",children:d.jsx(de,{name:"minus",className:"w-4 h-4"})}),d.jsx(xt,{type:"number",value:e,onChange:g,onBlur:p,min:t,max:r,disabled:a,className:`${c.input} text-center border-x border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none`,"aria-label":"수량"}),d.jsx(Q,{type:"button",onClick:u,disabled:x,className:`${c.button} flex items-center justify-center text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-r-lg transition-colors`,"aria-label":"수량 증가",children:d.jsx(de,{name:"plus",className:"w-4 h-4"})})]})},Vf=Object.freeze(Object.defineProperty({__proto__:null,default:Ml},Symbol.toStringTag,{value:"Module"})),Kf=window.G7Core?.createLogger?.("Comp:PostReactions")??{log:(...e)=>console.log("[Comp:PostReactions]",...e),warn:(...e)=>console.warn("[Comp:PostReactions]",...e),error:(...e)=>console.error("[Comp:PostReactions]",...e)},Ol=(e,t)=>window.G7Core?.t?.(e,t)??e,Yf=[{type:"like",emoji:"👍",labelKey:"board.reactions.like",activeColor:"bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400"},{type:"funny",emoji:"😂",labelKey:"board.reactions.funny",activeColor:"bg-yellow-100 dark:bg-yellow-900/30 text-yellow-600 dark:text-yellow-400"},{type:"agree",emoji:"👌",labelKey:"board.reactions.agree",activeColor:"bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400"},{type:"thanks",emoji:"🙏",labelKey:"board.reactions.thanks",activeColor:"bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400"},{type:"wow",emoji:"😮",labelKey:"board.reactions.wow",activeColor:"bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400"}],Il=({reactions:e,userReaction:t,postId:r,onReact:n,size:o="md",className:a="",id:s,editorAttrs:l})=>{const f={sm:"px-2 py-1 text-xs gap-1",md:"px-3 py-1.5 text-sm gap-1.5",lg:"px-4 py-2 text-base gap-2"},h={sm:"text-sm",md:"text-base",lg:"text-lg"},c=u=>e.find(p=>p.type===u)?.count??0,i=async u=>{if(n){n(u);return}try{if(!(await fetch(`/api/posts/${r}/reactions`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:u}),credentials:"include"})).ok)throw new Error("리액션 처리 실패");window.location.reload()}catch(g){Kf.error("리액션 오류:",g)}};return d.jsx(W,{className:`flex flex-wrap gap-2 ${a}`,id:s,...l,children:Yf.map(u=>{const g=c(u.type),p=t===u.type;return d.jsxs(Q,{onClick:()=>i(u.type),className:` inline-flex items-center rounded-full border transition-colors ${f[o]} ${p?`${u.activeColor} border-transparent`:"border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"} - `,title:Ml(u.labelKey),children:[d.jsx(R,{className:h[o],children:u.emoji}),d.jsx(R,{children:Ml(u.labelKey)}),g>0&&d.jsx(R,{className:"font-medium",children:g})]},u.type)})})},Yf=Object.freeze(Object.defineProperty({__proto__:null,default:Ol},Symbol.toStringTag,{value:"Module"})),Mn=window.G7Core?.createLogger?.("Comp:RichTextEditor")??{log:(...e)=>console.log("[Comp:RichTextEditor]",...e),warn:(...e)=>console.warn("[Comp:RichTextEditor]",...e),error:(...e)=>console.error("[Comp:RichTextEditor]",...e)},St=(e,t)=>window.G7Core?.t?.(e,t)??e,Il=({name:e,initialValue:t="",placeholder:r,onChange:n,imageUploadUrl:o="/api/upload/image",minHeight:a="300px",disabled:s=!1,className:l="",id:f,editorAttrs:h})=>{const c=k.useRef(null),[i,u]=k.useState(t),[g,p]=k.useState(!1),[m,w]=k.useState(!1),[C,O]=k.useState("");k.useEffect(()=>{c.current&&!c.current.innerHTML&&(c.current.innerHTML=t)},[t]),k.useEffect(()=>{n&&n(i)},[i,n]);const v=T=>{Mn.log("Apply format:",T)},E=async T=>{const A=new FormData;A.append("file",T);try{const P=await fetch(o,{method:"POST",body:A,credentials:"include"});if(!P.ok)throw new Error("업로드 실패");const D=await P.json();Mn.log("Image uploaded:",D.url)}catch(P){Mn.error("이미지 업로드 오류:",P)}},I=T=>{const A=T.target.files?.[0];A&&E(A)},_=()=>{C&&(Mn.log("Insert link:",C),w(!1),O(""))},M=[{format:"bold",icon:"B",title:St("editor.toolbar.bold")},{format:"italic",icon:"I",title:St("editor.toolbar.italic")},{format:"underline",icon:"U",title:St("editor.toolbar.underline")},{format:"strike",icon:"S",title:St("editor.toolbar.strike")},{format:"code",icon:"</>",title:St("editor.toolbar.code")}],x=[{format:"h1",label:"H1"},{format:"h2",label:"H2"},{format:"h3",label:"H3"}],y=[{format:"bulletList",icon:"•",title:St("editor.toolbar.bullet_list")},{format:"orderedList",icon:"1.",title:St("editor.toolbar.ordered_list")},{format:"blockquote",icon:"❝",title:St("editor.toolbar.blockquote")}];return d.jsxs(W,{className:`border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden ${l}`,id:f,...h,children:[d.jsxs(W,{className:"flex flex-wrap items-center gap-1 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700",children:[d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:M.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"w-8 h-8 flex items-center justify-center text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:T.title,children:T.icon},T.format))}),d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:x.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"px-2 h-8 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",children:T.label},T.format))}),d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:y.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"w-8 h-8 flex items-center justify-center text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:T.title,children:T.icon},T.format))}),d.jsxs(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:[d.jsx(Q,{type:"button",onClick:()=>w(!0),disabled:s,className:"w-8 h-8 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:St("editor.toolbar.link"),children:"🔗"}),d.jsxs(yr,{className:"w-8 h-8 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer",children:["🖼",d.jsx(xt,{type:"file",accept:"image/*",onChange:I,disabled:s,className:"hidden"})]})]}),d.jsxs(Q,{type:"button",onClick:()=>p(!g),disabled:s,className:`px-2 h-8 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50 ${g?"bg-gray-200 dark:bg-gray-700":""}`,children:["</>",St("editor.toolbar.code_view")]})]}),g?d.jsx(no,{value:i,onChange:T=>u(T.target.value),placeholder:r,disabled:s,className:"w-full p-4 bg-gray-900 text-gray-100 font-mono text-sm focus:outline-none resize-none",style:{minHeight:a}}):d.jsx(W,{ref:c,contentEditable:!s,onInput:T=>u(T.currentTarget.innerHTML),className:"p-4 bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none prose dark:prose-invert max-w-none",style:{minHeight:a},"data-placeholder":r}),d.jsx(xt,{type:"hidden",name:e,value:i}),m&&d.jsx(W,{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:d.jsxs(W,{className:"bg-white dark:bg-gray-800 rounded-lg p-6 w-96",children:[d.jsx(gn,{className:"text-lg font-medium text-gray-900 dark:text-white mb-4",children:St("editor.link_modal.title")}),d.jsx(xt,{type:"url",value:C,onChange:T=>O(T.target.value),placeholder:"https://example.com",className:"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"}),d.jsxs(W,{className:"flex justify-end gap-2",children:[d.jsx(Q,{type:"button",onClick:()=>{w(!1),O("")},className:"px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg",children:St("common.cancel")}),d.jsx(Q,{type:"button",onClick:_,className:"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700",children:St("editor.link_modal.insert")})]})]})})]})},Xf=Object.freeze(Object.defineProperty({__proto__:null,default:Il},Symbol.toStringTag,{value:"Module"})),qo=({content:e,text:t,isHtml:r=!0,className:n="",purifyConfig:o,id:a,editorAttrs:s})=>{const l=t??e??"";if(!l||l.trim()==="")return null;if(!r){const i=` + `,title:Ol(u.labelKey),children:[d.jsx(R,{className:h[o],children:u.emoji}),d.jsx(R,{children:Ol(u.labelKey)}),g>0&&d.jsx(R,{className:"font-medium",children:g})]},u.type)})})},Xf=Object.freeze(Object.defineProperty({__proto__:null,default:Il},Symbol.toStringTag,{value:"Module"})),Mn=window.G7Core?.createLogger?.("Comp:RichTextEditor")??{log:(...e)=>console.log("[Comp:RichTextEditor]",...e),warn:(...e)=>console.warn("[Comp:RichTextEditor]",...e),error:(...e)=>console.error("[Comp:RichTextEditor]",...e)},St=(e,t)=>window.G7Core?.t?.(e,t)??e,Dl=({name:e,initialValue:t="",placeholder:r,onChange:n,imageUploadUrl:o="/api/upload/image",minHeight:a="300px",disabled:s=!1,className:l="",id:f,editorAttrs:h})=>{const c=k.useRef(null),[i,u]=k.useState(t),[g,p]=k.useState(!1),[m,x]=k.useState(!1),[C,O]=k.useState("");k.useEffect(()=>{c.current&&!c.current.innerHTML&&(c.current.innerHTML=t)},[t]),k.useEffect(()=>{n&&n(i)},[i,n]);const v=T=>{Mn.log("Apply format:",T)},E=async T=>{const A=new FormData;A.append("file",T);try{const P=await fetch(o,{method:"POST",body:A,credentials:"include"});if(!P.ok)throw new Error("업로드 실패");const D=await P.json();Mn.log("Image uploaded:",D.url)}catch(P){Mn.error("이미지 업로드 오류:",P)}},I=T=>{const A=T.target.files?.[0];A&&E(A)},_=()=>{C&&(Mn.log("Insert link:",C),x(!1),O(""))},M=[{format:"bold",icon:"B",title:St("editor.toolbar.bold")},{format:"italic",icon:"I",title:St("editor.toolbar.italic")},{format:"underline",icon:"U",title:St("editor.toolbar.underline")},{format:"strike",icon:"S",title:St("editor.toolbar.strike")},{format:"code",icon:"</>",title:St("editor.toolbar.code")}],w=[{format:"h1",label:"H1"},{format:"h2",label:"H2"},{format:"h3",label:"H3"}],y=[{format:"bulletList",icon:"•",title:St("editor.toolbar.bullet_list")},{format:"orderedList",icon:"1.",title:St("editor.toolbar.ordered_list")},{format:"blockquote",icon:"❝",title:St("editor.toolbar.blockquote")}];return d.jsxs(W,{className:`border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden ${l}`,id:f,...h,children:[d.jsxs(W,{className:"flex flex-wrap items-center gap-1 p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700",children:[d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:M.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"w-8 h-8 flex items-center justify-center text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:T.title,children:T.icon},T.format))}),d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:w.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"px-2 h-8 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",children:T.label},T.format))}),d.jsx(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:y.map(T=>d.jsx(Q,{type:"button",onClick:()=>v(T.format),disabled:s,className:"w-8 h-8 flex items-center justify-center text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:T.title,children:T.icon},T.format))}),d.jsxs(W,{className:"flex items-center gap-1 pr-2 border-r border-gray-200 dark:border-gray-700",children:[d.jsx(Q,{type:"button",onClick:()=>x(!0),disabled:s,className:"w-8 h-8 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50",title:St("editor.toolbar.link"),children:"🔗"}),d.jsxs(yr,{className:"w-8 h-8 flex items-center justify-center text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded cursor-pointer",children:["🖼",d.jsx(xt,{type:"file",accept:"image/*",onChange:I,disabled:s,className:"hidden"})]})]}),d.jsxs(Q,{type:"button",onClick:()=>p(!g),disabled:s,className:`px-2 h-8 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded disabled:opacity-50 ${g?"bg-gray-200 dark:bg-gray-700":""}`,children:["</>",St("editor.toolbar.code_view")]})]}),g?d.jsx(no,{value:i,onChange:T=>u(T.target.value),placeholder:r,disabled:s,className:"w-full p-4 bg-gray-900 text-gray-100 font-mono text-sm focus:outline-none resize-none",style:{minHeight:a}}):d.jsx(W,{ref:c,contentEditable:!s,onInput:T=>u(T.currentTarget.innerHTML),className:"p-4 bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:outline-none prose dark:prose-invert max-w-none",style:{minHeight:a},"data-placeholder":r}),d.jsx(xt,{type:"hidden",name:e,value:i}),m&&d.jsx(W,{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/50",children:d.jsxs(W,{className:"bg-white dark:bg-gray-800 rounded-lg p-6 w-96",children:[d.jsx(gn,{className:"text-lg font-medium text-gray-900 dark:text-white mb-4",children:St("editor.link_modal.title")}),d.jsx(xt,{type:"url",value:C,onChange:T=>O(T.target.value),placeholder:"https://example.com",className:"w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white mb-4"}),d.jsxs(W,{className:"flex justify-end gap-2",children:[d.jsx(Q,{type:"button",onClick:()=>{x(!1),O("")},className:"px-4 py-2 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg",children:St("common.cancel")}),d.jsx(Q,{type:"button",onClick:_,className:"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700",children:St("editor.link_modal.insert")})]})]})})]})},qf=Object.freeze(Object.defineProperty({__proto__:null,default:Dl},Symbol.toStringTag,{value:"Module"})),Zo=({content:e,text:t,isHtml:r=!0,className:n="",purifyConfig:o,id:a,editorAttrs:s})=>{const l=t??e??"";if(!l||l.trim()==="")return null;if(!r){const i=` whitespace-pre-wrap text-gray-900 dark:text-gray-100 font-sans ${n} - `.trim().replace(/\s+/g," ");return d.jsx(W,{className:i,id:a,...s,children:l})}const f={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},h=k.useMemo(()=>{const i=o?{...f,...o,FORBID_TAGS:[...f.FORBID_TAGS,...o.FORBID_TAGS||[]],FORBID_ATTR:[...f.FORBID_ATTR,...o.FORBID_ATTR||[]]}:f;return wo.sanitize(l,i).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,p,m,w)=>(m.startsWith("http://")||m.startsWith("https://"))&&!g.includes("rel=")?`<a ${p}href="${m}"${w} rel="noopener noreferrer">`:g)},[l,o]),c=` + `.trim().replace(/\s+/g," ");return d.jsx(W,{className:i,id:a,...s,children:l})}const f={FORBID_TAGS:["script","noscript","iframe","frame","frameset","object","embed","applet","portal","form","input","textarea","select","button","style","link","meta","base","body","head","html","title","svg","math","audio","video","source","track","canvas","details","dialog","plaintext","xmp","listing","marquee","noframes","noembed","template","slot"],FORBID_ATTR:["onerror","onload","onclick","onmouseover","onfocus","onblur","ontoggle","oncanplay","onloadstart","formaction","xlink:href","action"],ALLOW_DATA_ATTR:!0,ADD_ATTR:["target"]},h=k.useMemo(()=>{const i=o?{...f,...o,FORBID_TAGS:[...f.FORBID_TAGS,...o.FORBID_TAGS||[]],FORBID_ATTR:[...f.FORBID_ATTR,...o.FORBID_ATTR||[]]}:f;return wo.sanitize(l,i).replace(/<a\s+([^>]*?)href=["']([^"']+)["']([^>]*?)>/gi,(g,p,m,x)=>(m.startsWith("http://")||m.startsWith("https://"))&&!g.includes("rel=")?`<a ${p}href="${m}"${x} rel="noopener noreferrer">`:g)},[l,o]),c=` prose dark:prose-invert max-w-none prose-p:my-2 prose-headings:font-bold prose-headings:mt-6 prose-headings:mb-4 @@ -39,7 +39,7 @@ var SirsoftBasic=(function(J,d,k,Jt){"use strict";function Dc(e){const t=Object. [&_tr:hover]:bg-gray-50 dark:[&_tr:hover]:bg-gray-700/50 after:content-[''] after:block after:clear-both ${n} - `.trim().replace(/\s+/g," ");return d.jsx(W,{id:a,className:c,dangerouslySetInnerHTML:{__html:h},...s})},qf=Object.freeze(Object.defineProperty({__proto__:null,HtmlContent:qo},Symbol.toStringTag,{value:"Module"})),Zo=()=>window.G7Core,Qo=(e,t)=>Zo()?.t?.(e,t)??e,Dl=({content:e="",onChange:t,isHtml:r=!1,onIsHtmlChange:n,rows:o=15,placeholder:a="",label:s,showHtmlModeToggle:l=!0,contentClassName:f="",purifyConfig:h,className:c="",name:i="content",htmlFieldName:u="content_mode",readOnly:g=!1,id:p,editorAttrs:m})=>{const[w,C]=k.useState(r),[O,v]=k.useState(e),[E,I]=k.useState(!1);k.useEffect(()=>{C(r)},[r]),k.useEffect(()=>{v(e)},[e]);const _=k.useCallback(y=>{const T=y.target.value;if(v(T),t){const A=Zo()?.createChangeEvent?.({value:T,name:i,type:"textarea"})??{target:{name:i,value:T}};t(A)}},[t,i]),M=k.useCallback(y=>{const T=y.target.checked;if(C(T),T||I(!1),n){const A=Zo()?.createChangeEvent?.({checked:T,name:u,type:"checkbox"})??{target:{name:u,checked:T}};n(A)}},[n,u]),x=k.useCallback(()=>{I(y=>!y)},[]);return d.jsxs(W,{className:`space-y-2 ${c}`,id:p,...m,children:[d.jsxs(W,{className:"flex items-center justify-between",children:[s&&d.jsx(yr,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:s}),d.jsxs(W,{className:"flex items-center gap-3",children:[w&&!g&&d.jsx(Q,{type:"button",onClick:x,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${E?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:Qo(E?"common.preview_off":"common.preview")}),l&&d.jsxs(yr,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[d.jsx(xt,{type:"checkbox",name:u,checked:w,onChange:M,disabled:g,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),d.jsx(W,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:Qo("common.html_mode")})]})]})]}),!E&&d.jsx(no,{name:i,value:O,onChange:_,placeholder:a,rows:o,readOnly:g,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${w?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`}),E&&d.jsx(W,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:d.jsx(qo,{content:O,isHtml:!0,className:f||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:h})})]})},Zf=Object.freeze(Object.defineProperty({__proto__:null,HtmlEditor:Dl},Symbol.toStringTag,{value:"Module"})),Ll=e=>window.G7Core?.t?.(e)??e,Jo=({maxHeight:e=500,expandText:t,collapseText:r,className:n="",children:o,id:a,editorAttrs:s})=>{const[l,f]=k.useState(!1),[h,c]=k.useState(!1),i=k.useRef(null),u=k.useCallback(()=>{if(i.current){const w=i.current.scrollHeight;c(w>e)}},[e]);k.useEffect(()=>{u();const w=i.current;if(!w)return;const C=w.querySelectorAll("img");C.forEach(v=>{v.complete||v.addEventListener("load",u)});let O=null;return typeof ResizeObserver<"u"&&(O=new ResizeObserver(u),O.observe(w)),()=>{C.forEach(v=>{v.removeEventListener("load",u)}),O?.disconnect()}},[u,o]);const g=()=>{f(w=>!w)},p=t||Ll("sirsoft-ecommerce.shop.product.expand_detail"),m=r||Ll("sirsoft-ecommerce.shop.product.collapse_detail");return d.jsxs(W,{className:n,id:a,...s,children:[d.jsxs(W,{className:"relative",children:[d.jsx(W,{ref:i,className:!l&&h?"overflow-hidden transition-[max-height] duration-300 ease-in-out":"transition-[max-height] duration-300 ease-in-out",style:!l&&h?{maxHeight:`${e}px`}:void 0,children:o}),!l&&h&&d.jsx(W,{className:"absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-white dark:from-gray-900 to-transparent pointer-events-none"})]}),h&&d.jsxs(Q,{type:"button",onClick:g,className:"flex items-center justify-center w-full py-3.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer gap-1.5",children:[d.jsx(R,{className:"text-sm font-semibold text-gray-600 dark:text-gray-400",children:l?m:p}),d.jsx(de,{name:l?"chevron-up":"chevron-down",size:"sm"})]})]})},Qf=Object.freeze(Object.defineProperty({__proto__:null,ExpandableContent:Jo,default:Jo},Symbol.toStringTag,{value:"Module"})),Jf=(e,t)=>window.G7Core?.t?.(e,t)??e,Ro=({isOpen:e,onClose:t,title:r,icon:n,iconClassName:o="text-gray-500 dark:text-gray-400",closeOnBackdropClick:a=!0,closeOnOverlayClick:s,showCloseButton:l=!0,closeOnEscape:f=!0,children:h,width:c="500px",className:i="",style:u})=>{const g=s!==void 0?s:a,p=k.useRef(null),m=k.useRef(null);if(k.useEffect(()=>{const C=O=>{O.key==="Escape"&&e&&f&&t()};if(e&&f)return document.addEventListener("keydown",C),()=>document.removeEventListener("keydown",C)},[e,t,f]),k.useEffect(()=>{if(e&&p.current){m.current=document.activeElement;const C=p.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),O=C[0],v=C[C.length-1];O?.focus();const E=I=>{I.key==="Tab"&&(I.shiftKey?document.activeElement===O&&(I.preventDefault(),v?.focus()):document.activeElement===v&&(I.preventDefault(),O?.focus()))};return document.addEventListener("keydown",E),()=>{document.removeEventListener("keydown",E),m.current?.focus()}}},[e]),k.useEffect(()=>{if(e)return document.body.style.overflow="hidden",()=>{document.body.style.overflow=""}},[e]),!e)return null;const w=u?.zIndex??50;return d.jsxs(W,{className:"fixed inset-0 flex items-center justify-center",style:{zIndex:w},onClick:g?t:void 0,children:[d.jsx(W,{className:"absolute inset-0 bg-black/50","aria-hidden":"true"}),d.jsxs(W,{ref:p,className:`relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-h-[90vh] max-w-[calc(100vw-2rem)] overflow-hidden ${i}`,style:{width:c,...u?{...u,zIndex:void 0}:{}},onClick:C=>C.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":r?"modal-title":void 0,children:[(r||n||l)&&d.jsxs(W,{className:"flex items-center justify-between p-4",children:[(r||n)&&d.jsxs(W,{id:"modal-title",className:"flex items-center gap-3 text-lg font-semibold text-gray-900 dark:text-white",children:[n&&d.jsx(de,{name:n,size:"md",className:o}),r]}),l&&d.jsx(Q,{onClick:t,className:"ml-auto text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none","aria-label":Jf("common.close_modal"),children:d.jsx("svg",{className:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),d.jsx(W,{className:"px-6 py-4 overflow-y-auto max-h-[calc(90vh-140px)]",children:h})]})]})},Rf=Object.freeze(Object.defineProperty({__proto__:null,Modal:Ro},Symbol.toStringTag,{value:"Module"})),ea=(e,t)=>window.G7Core?.t?.(e,t)??e,ta=({isOpen:e,onClose:t,title:r,message:n,confirmText:o,cancelText:a,onConfirm:s,onCancel:l,confirmButtonVariant:f="primary",cancelButtonVariant:h="secondary",width:c="500px",closeOnOverlay:i=!0,isLoading:u=!1,loadingText:g})=>{const p=o??ea("common.confirm"),m=a??ea("common.cancel"),w=g??ea("common.loading"),C=()=>{s()},O=()=>{u||(l?.(),t())},v=()=>{u||t()},E={primary:"bg-blue-600 text-white hover:bg-blue-700",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600",danger:"bg-red-600 text-white hover:bg-red-700",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"};return d.jsxs(Ro,{isOpen:e,onClose:v,title:r,width:c,children:[d.jsx(W,{className:"py-2",children:typeof n=="string"?d.jsx(Dt,{className:"text-gray-700 dark:text-gray-300",children:n}):n}),d.jsxs(W,{className:"flex justify-end gap-3 mt-6",children:[d.jsx(Q,{onClick:O,disabled:u,className:"px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",children:d.jsx(R,{children:m})}),d.jsxs(Q,{onClick:C,disabled:u,className:`flex items-center gap-2 px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed ${E[f]}`,children:[u&&d.jsx(Ct,{className:"fa-solid fa-spinner w-4 h-4 animate-spin"}),d.jsx(R,{children:u?w:p})]})]})]})},eg=Object.freeze(Object.defineProperty({__proto__:null,ConfirmDialog:ta},Symbol.toStringTag,{value:"Module"})),Pl=k.createContext(null),$l=({value:e,children:t})=>d.jsx(Pl.Provider,{value:e,children:t}),tg=()=>{const e=k.useContext(Pl);if(!e)throw new Error("useFileUploaderContext must be used within FileUploaderProvider");return e};function rg(e,t){return t.forEach((function(r){r&&typeof r!="string"&&!Array.isArray(r)&&Object.keys(r).forEach((function(n){if(n!=="default"&&!(n in e)){var o=Object.getOwnPropertyDescriptor(r,n);Object.defineProperty(e,n,o.get?o:{enumerable:!0,get:function(){return r[n]}})}}))})),Object.freeze(e)}function Fl(e,t){return new Promise((function(r,n){let o;return ng(e).then((function(a){try{return o=a,r(new Blob([t.slice(0,2),o,t.slice(2)],{type:"image/jpeg"}))}catch(s){return n(s)}}),n)}))}const ng=e=>new Promise(((t,r)=>{const n=new FileReader;n.addEventListener("load",(({target:{result:o}})=>{const a=new DataView(o);let s=0;if(a.getUint16(s)!==65496)return r("not a valid JPEG");for(s+=2;;){const l=a.getUint16(s);if(l===65498)break;const f=a.getUint16(s+2);if(l===65505&&a.getUint32(s+4)===1165519206){const h=s+10;let c;switch(a.getUint16(h)){case 18761:c=!0;break;case 19789:c=!1;break;default:return r("TIFF header contains invalid endian")}if(a.getUint16(h+2,c)!==42)return r("TIFF header contains invalid version");const i=a.getUint32(h+4,c),u=h+i+2+12*a.getUint16(h+i,c);for(let g=h+i+2;g<u;g+=12)if(a.getUint16(g,c)==274){if(a.getUint16(g+2,c)!==3)return r("Orientation data type is invalid");if(a.getUint32(g+4,c)!==1)return r("Orientation data count is invalid");a.setUint16(g+8,1,c);break}return t(o.slice(s,s+2+f))}s+=2+f}return t(new Blob)})),n.readAsArrayBuffer(e)}));var On={},og={get exports(){return On},set exports(e){On=e}};(function(e){var t,r,n={};og.exports=n,n.parse=function(o,a){for(var s=n.bin.readUshort,l=n.bin.readUint,f=0,h={},c=new Uint8Array(o),i=c.length-4;l(c,i)!=101010256;)i--;f=i,f+=4;var u=s(c,f+=4);s(c,f+=2);var g=l(c,f+=2),p=l(c,f+=4);f+=4,f=p;for(var m=0;m<u;m++){l(c,f),f+=4,f+=4,f+=4,l(c,f+=4),g=l(c,f+=4);var w=l(c,f+=4),C=s(c,f+=4),O=s(c,f+2),v=s(c,f+4);f+=6;var E=l(c,f+=8);f+=4,f+=C+O+v,n._readLocal(c,E,h,g,w,a)}return h},n._readLocal=function(o,a,s,l,f,h){var c=n.bin.readUshort,i=n.bin.readUint;i(o,a),c(o,a+=4),c(o,a+=2);var u=c(o,a+=2);i(o,a+=2),i(o,a+=4),a+=4;var g=c(o,a+=8),p=c(o,a+=2);a+=2;var m=n.bin.readUTF8(o,a,g);if(a+=g,a+=p,h)s[m]={size:f,csize:l};else{var w=new Uint8Array(o.buffer,a);if(u==0)s[m]=new Uint8Array(w.buffer.slice(a,a+l));else{if(u!=8)throw"unknown compression method: "+u;var C=new Uint8Array(f);n.inflateRaw(w,C),s[m]=C}}},n.inflateRaw=function(o,a){return n.F.inflate(o,a)},n.inflate=function(o,a){return o[0],o[1],n.inflateRaw(new Uint8Array(o.buffer,o.byteOffset+2,o.length-6),a)},n.deflate=function(o,a){a==null&&(a={level:6});var s=0,l=new Uint8Array(50+Math.floor(1.1*o.length));l[s]=120,l[s+1]=156,s+=2,s=n.F.deflateRaw(o,l,s,a.level);var f=n.adler(o,0,o.length);return l[s+0]=f>>>24&255,l[s+1]=f>>>16&255,l[s+2]=f>>>8&255,l[s+3]=f>>>0&255,new Uint8Array(l.buffer,0,s+4)},n.deflateRaw=function(o,a){a==null&&(a={level:6});var s=new Uint8Array(50+Math.floor(1.1*o.length)),l=n.F.deflateRaw(o,s,l,a.level);return new Uint8Array(s.buffer,0,l)},n.encode=function(o,a){a==null&&(a=!1);var s=0,l=n.bin.writeUint,f=n.bin.writeUshort,h={};for(var c in o){var i=!n._noNeed(c)&&!a,u=o[c],g=n.crc.crc(u,0,u.length);h[c]={cpr:i,usize:u.length,crc:g,file:i?n.deflateRaw(u):u}}for(var c in h)s+=h[c].file.length+30+46+2*n.bin.sizeUTF8(c);s+=22;var p=new Uint8Array(s),m=0,w=[];for(var c in h){var C=h[c];w.push(m),m=n._writeHeader(p,m,c,C,0)}var O=0,v=m;for(var c in h)C=h[c],w.push(m),m=n._writeHeader(p,m,c,C,1,w[O++]);var E=m-v;return l(p,m,101010256),m+=4,f(p,m+=4,O),f(p,m+=2,O),l(p,m+=2,E),l(p,m+=4,v),m+=4,m+=2,p.buffer},n._noNeed=function(o){var a=o.split(".").pop().toLowerCase();return"png,jpg,jpeg,zip".indexOf(a)!=-1},n._writeHeader=function(o,a,s,l,f,h){var c=n.bin.writeUint,i=n.bin.writeUshort,u=l.file;return c(o,a,f==0?67324752:33639248),a+=4,f==1&&(a+=2),i(o,a,20),i(o,a+=2,0),i(o,a+=2,l.cpr?8:0),c(o,a+=2,0),c(o,a+=4,l.crc),c(o,a+=4,u.length),c(o,a+=4,l.usize),i(o,a+=4,n.bin.sizeUTF8(s)),i(o,a+=2,0),a+=2,f==1&&(a+=2,a+=2,c(o,a+=6,h),a+=4),a+=n.bin.writeUTF8(o,a,s),f==0&&(o.set(u,a),a+=u.length),a},n.crc={table:(function(){for(var o=new Uint32Array(256),a=0;a<256;a++){for(var s=a,l=0;l<8;l++)1&s?s=3988292384^s>>>1:s>>>=1;o[a]=s}return o})(),update:function(o,a,s,l){for(var f=0;f<l;f++)o=n.crc.table[255&(o^a[s+f])]^o>>>8;return o},crc:function(o,a,s){return 4294967295^n.crc.update(4294967295,o,a,s)}},n.adler=function(o,a,s){for(var l=1,f=0,h=a,c=a+s;h<c;){for(var i=Math.min(h+5552,c);h<i;)f+=l+=o[h++];l%=65521,f%=65521}return f<<16|l},n.bin={readUshort:function(o,a){return o[a]|o[a+1]<<8},writeUshort:function(o,a,s){o[a]=255&s,o[a+1]=s>>8&255},readUint:function(o,a){return 16777216*o[a+3]+(o[a+2]<<16|o[a+1]<<8|o[a])},writeUint:function(o,a,s){o[a]=255&s,o[a+1]=s>>8&255,o[a+2]=s>>16&255,o[a+3]=s>>24&255},readASCII:function(o,a,s){for(var l="",f=0;f<s;f++)l+=String.fromCharCode(o[a+f]);return l},writeASCII:function(o,a,s){for(var l=0;l<s.length;l++)o[a+l]=s.charCodeAt(l)},pad:function(o){return o.length<2?"0"+o:o},readUTF8:function(o,a,s){for(var l,f="",h=0;h<s;h++)f+="%"+n.bin.pad(o[a+h].toString(16));try{l=decodeURIComponent(f)}catch{return n.bin.readASCII(o,a,s)}return l},writeUTF8:function(o,a,s){for(var l=s.length,f=0,h=0;h<l;h++){var c=s.charCodeAt(h);if((4294967168&c)==0)o[a+f]=c,f++;else if((4294965248&c)==0)o[a+f]=192|c>>6,o[a+f+1]=128|c>>0&63,f+=2;else if((4294901760&c)==0)o[a+f]=224|c>>12,o[a+f+1]=128|c>>6&63,o[a+f+2]=128|c>>0&63,f+=3;else{if((4292870144&c)!=0)throw"e";o[a+f]=240|c>>18,o[a+f+1]=128|c>>12&63,o[a+f+2]=128|c>>6&63,o[a+f+3]=128|c>>0&63,f+=4}}return f},sizeUTF8:function(o){for(var a=o.length,s=0,l=0;l<a;l++){var f=o.charCodeAt(l);if((4294967168&f)==0)s++;else if((4294965248&f)==0)s+=2;else if((4294901760&f)==0)s+=3;else{if((4292870144&f)!=0)throw"e";s+=4}}return s}},n.F={},n.F.deflateRaw=function(o,a,s,l){var f=[[0,0,0,0,0],[4,4,8,4,0],[4,5,16,8,0],[4,6,16,16,0],[4,10,16,32,0],[8,16,32,32,0],[8,16,128,128,0],[8,32,128,256,0],[32,128,258,1024,1],[32,258,258,4096,1]][l],h=n.F.U,c=n.F._goodIndex;n.F._hash;var i=n.F._putsE,u=0,g=s<<3,p=0,m=o.length;if(l==0){for(;u<m;)i(a,g,u+(A=Math.min(65535,m-u))==m?1:0),g=n.F._copyExact(o,u,A,a,g+8),u+=A;return g>>>3}var w=h.lits,C=h.strt,O=h.prev,v=0,E=0,I=0,_=0,M=0,x=0;for(m>2&&(C[x=n.F._hash(o,0)]=0),u=0;u<m;u++){if(M=x,u+1<m-2){x=n.F._hash(o,u+1);var y=u+1&32767;O[y]=C[x],C[x]=y}if(p<=u){(v>14e3||E>26697)&&m-u>100&&(p<u&&(w[v]=u-p,v+=2,p=u),g=n.F._writeBlock(u==m-1||p==m?1:0,w,v,_,o,I,u-I,a,g),v=E=_=0,I=u);var T=0;u<m-2&&(T=n.F._bestMatch(o,u,O,M,Math.min(f[2],m-u),f[3]));var A=T>>>16,P=65535&T;if(T!=0){P=65535&T;var D=c(A=T>>>16,h.of0);h.lhst[257+D]++;var L=c(P,h.df0);h.dhst[L]++,_+=h.exb[D]+h.dxb[L],w[v]=A<<23|u-p,w[v+1]=P<<16|D<<8|L,v+=2,p=u+A}else h.lhst[o[u]]++;E++}}for(I==u&&o.length!=0||(p<u&&(w[v]=u-p,v+=2,p=u),g=n.F._writeBlock(1,w,v,_,o,I,u-I,a,g),v=0,E=0,v=E=_=0,I=u);(7&g)!=0;)g++;return g>>>3},n.F._bestMatch=function(o,a,s,l,f,h){var c=32767&a,i=s[c],u=c-i+32768&32767;if(i==c||l!=n.F._hash(o,a-u))return 0;for(var g=0,p=0,m=Math.min(32767,a);u<=m&&--h!=0&&i!=c;){if(g==0||o[a+g]==o[a+g-u]){var w=n.F._howLong(o,a,u);if(w>g){if(p=u,(g=w)>=f)break;u+2<w&&(w=u+2);for(var C=0,O=0;O<w-2;O++){var v=a-u+O+32768&32767,E=v-s[v]+32768&32767;E>C&&(C=E,i=v)}}}u+=(c=i)-(i=s[c])+32768&32767}return g<<16|p},n.F._howLong=function(o,a,s){if(o[a]!=o[a-s]||o[a+1]!=o[a+1-s]||o[a+2]!=o[a+2-s])return 0;var l=a,f=Math.min(o.length,a+258);for(a+=3;a<f&&o[a]==o[a-s];)a++;return a-l},n.F._hash=function(o,a){return(o[a]<<8|o[a+1])+(o[a+2]<<4)&65535},n.saved=0,n.F._writeBlock=function(o,a,s,l,f,h,c,i,u){var g,p,m,w,C,O,v,E,I,_=n.F.U,M=n.F._putsF,x=n.F._putsE;_.lhst[256]++,p=(g=n.F.getTrees())[0],m=g[1],w=g[2],C=g[3],O=g[4],v=g[5],E=g[6],I=g[7];var y=32+((u+3&7)==0?0:8-(u+3&7))+(c<<3),T=l+n.F.contSize(_.fltree,_.lhst)+n.F.contSize(_.fdtree,_.dhst),A=l+n.F.contSize(_.ltree,_.lhst)+n.F.contSize(_.dtree,_.dhst);A+=14+3*v+n.F.contSize(_.itree,_.ihst)+(2*_.ihst[16]+3*_.ihst[17]+7*_.ihst[18]);for(var P=0;P<286;P++)_.lhst[P]=0;for(P=0;P<30;P++)_.dhst[P]=0;for(P=0;P<19;P++)_.ihst[P]=0;var D=y<T&&y<A?0:T<A?1:2;if(M(i,u,o),M(i,u+1,D),u+=3,D==0){for(;(7&u)!=0;)u++;u=n.F._copyExact(f,h,c,i,u)}else{var L,U;if(D==1&&(L=_.fltree,U=_.fdtree),D==2){n.F.makeCodes(_.ltree,p),n.F.revCodes(_.ltree,p),n.F.makeCodes(_.dtree,m),n.F.revCodes(_.dtree,m),n.F.makeCodes(_.itree,w),n.F.revCodes(_.itree,w),L=_.ltree,U=_.dtree,x(i,u,C-257),x(i,u+=5,O-1),x(i,u+=5,v-4),u+=4;for(var S=0;S<v;S++)x(i,u+3*S,_.itree[1+(_.ordr[S]<<1)]);u+=3*v,u=n.F._codeTiny(E,_.itree,i,u),u=n.F._codeTiny(I,_.itree,i,u)}for(var b=h,F=0;F<s;F+=2){for(var $=a[F],G=$>>>23,Y=b+(8388607&$);b<Y;)u=n.F._writeLit(f[b++],L,i,u);if(G!=0){var B=a[F+1],K=B>>16,j=B>>8&255,H=255&B;x(i,u=n.F._writeLit(257+j,L,i,u),G-_.of0[j]),u+=_.exb[j],M(i,u=n.F._writeLit(H,U,i,u),K-_.df0[H]),u+=_.dxb[H],b+=G}}u=n.F._writeLit(256,L,i,u)}return u},n.F._copyExact=function(o,a,s,l,f){var h=f>>>3;return l[h]=s,l[h+1]=s>>>8,l[h+2]=255-l[h],l[h+3]=255-l[h+1],h+=4,l.set(new Uint8Array(o.buffer,a,s),h),f+(s+4<<3)},n.F.getTrees=function(){for(var o=n.F.U,a=n.F._hufTree(o.lhst,o.ltree,15),s=n.F._hufTree(o.dhst,o.dtree,15),l=[],f=n.F._lenCodes(o.ltree,l),h=[],c=n.F._lenCodes(o.dtree,h),i=0;i<l.length;i+=2)o.ihst[l[i]]++;for(i=0;i<h.length;i+=2)o.ihst[h[i]]++;for(var u=n.F._hufTree(o.ihst,o.itree,7),g=19;g>4&&o.itree[1+(o.ordr[g-1]<<1)]==0;)g--;return[a,s,u,f,c,g,l,h]},n.F.getSecond=function(o){for(var a=[],s=0;s<o.length;s+=2)a.push(o[s+1]);return a},n.F.nonZero=function(o){for(var a="",s=0;s<o.length;s+=2)o[s+1]!=0&&(a+=(s>>1)+",");return a},n.F.contSize=function(o,a){for(var s=0,l=0;l<a.length;l++)s+=a[l]*o[1+(l<<1)];return s},n.F._codeTiny=function(o,a,s,l){for(var f=0;f<o.length;f+=2){var h=o[f],c=o[f+1];l=n.F._writeLit(h,a,s,l);var i=h==16?2:h==17?3:7;h>15&&(n.F._putsE(s,l,c,i),l+=i)}return l},n.F._lenCodes=function(o,a){for(var s=o.length;s!=2&&o[s-1]==0;)s-=2;for(var l=0;l<s;l+=2){var f=o[l+1],h=l+3<s?o[l+3]:-1,c=l+5<s?o[l+5]:-1,i=l==0?-1:o[l-1];if(f==0&&h==f&&c==f){for(var u=l+5;u+2<s&&o[u+2]==f;)u+=2;(g=Math.min(u+1-l>>>1,138))<11?a.push(17,g-3):a.push(18,g-11),l+=2*g-2}else if(f==i&&h==f&&c==f){for(u=l+5;u+2<s&&o[u+2]==f;)u+=2;var g=Math.min(u+1-l>>>1,6);a.push(16,g-3),l+=2*g-2}else a.push(f,0)}return s>>>1},n.F._hufTree=function(o,a,s){var l=[],f=o.length,h=a.length,c=0;for(c=0;c<h;c+=2)a[c]=0,a[c+1]=0;for(c=0;c<f;c++)o[c]!=0&&l.push({lit:c,f:o[c]});var i=l.length,u=l.slice(0);if(i==0)return 0;if(i==1){var g=l[0].lit;return u=g==0?1:0,a[1+(g<<1)]=1,a[1+(u<<1)]=1,1}l.sort((function(E,I){return E.f-I.f}));var p=l[0],m=l[1],w=0,C=1,O=2;for(l[0]={lit:-1,f:p.f+m.f,l:p,r:m,d:0};C!=i-1;)p=w!=C&&(O==i||l[w].f<l[O].f)?l[w++]:l[O++],m=w!=C&&(O==i||l[w].f<l[O].f)?l[w++]:l[O++],l[C++]={lit:-1,f:p.f+m.f,l:p,r:m};var v=n.F.setDepth(l[C-1],0);for(v>s&&(n.F.restrictDepth(u,s,v),v=s),c=0;c<i;c++)a[1+(u[c].lit<<1)]=u[c].d;return v},n.F.setDepth=function(o,a){return o.lit!=-1?(o.d=a,a):Math.max(n.F.setDepth(o.l,a+1),n.F.setDepth(o.r,a+1))},n.F.restrictDepth=function(o,a,s){var l=0,f=1<<s-a,h=0;for(o.sort((function(i,u){return u.d==i.d?i.f-u.f:u.d-i.d})),l=0;l<o.length&&o[l].d>a;l++){var c=o[l].d;o[l].d=a,h+=f-(1<<s-c)}for(h>>>=s-a;h>0;)(c=o[l].d)<a?(o[l].d++,h-=1<<a-c-1):l++;for(;l>=0;l--)o[l].d==a&&h<0&&(o[l].d--,h++);h!=0&&console.log("debt left")},n.F._goodIndex=function(o,a){var s=0;return a[16|s]<=o&&(s|=16),a[8|s]<=o&&(s|=8),a[4|s]<=o&&(s|=4),a[2|s]<=o&&(s|=2),a[1|s]<=o&&(s|=1),s},n.F._writeLit=function(o,a,s,l){return n.F._putsF(s,l,a[o<<1]),l+a[1+(o<<1)]},n.F.inflate=function(o,a){var s=Uint8Array;if(o[0]==3&&o[1]==0)return a||new s(0);var l=n.F,f=l._bitsF,h=l._bitsE,c=l._decodeTiny,i=l.makeCodes,u=l.codes2map,g=l._get17,p=l.U,m=a==null;m&&(a=new s(o.length>>>2<<3));for(var w,C,O=0,v=0,E=0,I=0,_=0,M=0,x=0,y=0,T=0;O==0;)if(O=f(o,T,1),v=f(o,T+1,2),T+=3,v!=0){if(m&&(a=n.F._check(a,y+131072)),v==1&&(w=p.flmap,C=p.fdmap,M=511,x=31),v==2){E=h(o,T,5)+257,I=h(o,T+5,5)+1,_=h(o,T+10,4)+4,T+=14;for(var A=0;A<38;A+=2)p.itree[A]=0,p.itree[A+1]=0;var P=1;for(A=0;A<_;A++){var D=h(o,T+3*A,3);p.itree[1+(p.ordr[A]<<1)]=D,D>P&&(P=D)}T+=3*_,i(p.itree,P),u(p.itree,P,p.imap),w=p.lmap,C=p.dmap,T=c(p.imap,(1<<P)-1,E+I,o,T,p.ttree);var L=l._copyOut(p.ttree,0,E,p.ltree);M=(1<<L)-1;var U=l._copyOut(p.ttree,E,I,p.dtree);x=(1<<U)-1,i(p.ltree,L),u(p.ltree,L,w),i(p.dtree,U),u(p.dtree,U,C)}for(;;){var S=w[g(o,T)&M];T+=15&S;var b=S>>>4;if(!(b>>>8))a[y++]=b;else{if(b==256)break;var F=y+b-254;if(b>264){var $=p.ldef[b-257];F=y+($>>>3)+h(o,T,7&$),T+=7&$}var G=C[g(o,T)&x];T+=15&G;var Y=G>>>4,B=p.ddef[Y],K=(B>>>4)+f(o,T,15&B);for(T+=15&B,m&&(a=n.F._check(a,y+131072));y<F;)a[y]=a[y++-K],a[y]=a[y++-K],a[y]=a[y++-K],a[y]=a[y++-K];y=F}}}else{(7&T)!=0&&(T+=8-(7&T));var j=4+(T>>>3),H=o[j-4]|o[j-3]<<8;m&&(a=n.F._check(a,y+H)),a.set(new s(o.buffer,o.byteOffset+j,H),y),T=j+H<<3,y+=H}return a.length==y?a:a.slice(0,y)},n.F._check=function(o,a){var s=o.length;if(a<=s)return o;var l=new Uint8Array(Math.max(s<<1,a));return l.set(o,0),l},n.F._decodeTiny=function(o,a,s,l,f,h){for(var c=n.F._bitsE,i=n.F._get17,u=0;u<s;){var g=o[i(l,f)&a];f+=15&g;var p=g>>>4;if(p<=15)h[u]=p,u++;else{var m=0,w=0;p==16?(w=3+c(l,f,2),f+=2,m=h[u-1]):p==17?(w=3+c(l,f,3),f+=3):p==18&&(w=11+c(l,f,7),f+=7);for(var C=u+w;u<C;)h[u]=m,u++}}return f},n.F._copyOut=function(o,a,s,l){for(var f=0,h=0,c=l.length>>>1;h<s;){var i=o[h+a];l[h<<1]=0,l[1+(h<<1)]=i,i>f&&(f=i),h++}for(;h<c;)l[h<<1]=0,l[1+(h<<1)]=0,h++;return f},n.F.makeCodes=function(o,a){for(var s,l,f,h,c=n.F.U,i=o.length,u=c.bl_count,g=0;g<=a;g++)u[g]=0;for(g=1;g<i;g+=2)u[o[g]]++;var p=c.next_code;for(s=0,u[0]=0,l=1;l<=a;l++)s=s+u[l-1]<<1,p[l]=s;for(f=0;f<i;f+=2)(h=o[f+1])!=0&&(o[f]=p[h],p[h]++)},n.F.codes2map=function(o,a,s){for(var l=o.length,f=n.F.U.rev15,h=0;h<l;h+=2)if(o[h+1]!=0)for(var c=h>>1,i=o[h+1],u=c<<4|i,g=a-i,p=o[h]<<g,m=p+(1<<g);p!=m;)s[f[p]>>>15-a]=u,p++},n.F.revCodes=function(o,a){for(var s=n.F.U.rev15,l=15-a,f=0;f<o.length;f+=2){var h=o[f]<<a-o[f+1];o[f]=s[h]>>>l}},n.F._putsE=function(o,a,s){s<<=7&a;var l=a>>>3;o[l]|=s,o[l+1]|=s>>>8},n.F._putsF=function(o,a,s){s<<=7&a;var l=a>>>3;o[l]|=s,o[l+1]|=s>>>8,o[l+2]|=s>>>16},n.F._bitsE=function(o,a,s){return(o[a>>>3]|o[1+(a>>>3)]<<8)>>>(7&a)&(1<<s)-1},n.F._bitsF=function(o,a,s){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16)>>>(7&a)&(1<<s)-1},n.F._get17=function(o,a){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16)>>>(7&a)},n.F._get25=function(o,a){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16|o[3+(a>>>3)]<<24)>>>(7&a)},n.F.U=(t=Uint16Array,r=Uint32Array,{next_code:new t(16),bl_count:new t(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new t(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new r(32),flmap:new t(512),fltree:[],fdmap:new t(32),fdtree:[],lmap:new t(32768),ltree:[],ttree:[],dmap:new t(32768),dtree:[],imap:new t(512),itree:[],rev15:new t(32768),lhst:new r(286),dhst:new r(30),ihst:new r(19),lits:new r(15e3),strt:new t(65536),prev:new t(32768)}),(function(){for(var o=n.F.U,a=0;a<32768;a++){var s=a;s=(4278255360&(s=(4042322160&(s=(3435973836&(s=(2863311530&s)>>>1|(1431655765&s)<<1))>>>2|(858993459&s)<<2))>>>4|(252645135&s)<<4))>>>8|(16711935&s)<<8,o.rev15[a]=(s>>>16|s<<16)>>>17}function l(f,h,c){for(;h--!=0;)f.push(0,c)}for(a=0;a<32;a++)o.ldef[a]=o.of0[a]<<3|o.exb[a],o.ddef[a]=o.df0[a]<<4|o.dxb[a];l(o.fltree,144,8),l(o.fltree,112,9),l(o.fltree,24,7),l(o.fltree,8,8),n.F.makeCodes(o.fltree,9),n.F.codes2map(o.fltree,9,o.flmap),n.F.revCodes(o.fltree,9),l(o.fdtree,32,5),n.F.makeCodes(o.fdtree,5),n.F.codes2map(o.fdtree,5,o.fdmap),n.F.revCodes(o.fdtree,5),l(o.itree,19,0),l(o.ltree,286,0),l(o.dtree,30,0),l(o.ttree,320,0)})()})();var ag=rg({__proto__:null,default:On},[On]);const Ht=(function(){var e={nextZero(c,i){for(;c[i]!=0;)i++;return i},readUshort:(c,i)=>c[i]<<8|c[i+1],writeUshort(c,i,u){c[i]=u>>8&255,c[i+1]=255&u},readUint:(c,i)=>16777216*c[i]+(c[i+1]<<16|c[i+2]<<8|c[i+3]),writeUint(c,i,u){c[i]=u>>24&255,c[i+1]=u>>16&255,c[i+2]=u>>8&255,c[i+3]=255&u},readASCII(c,i,u){let g="";for(let p=0;p<u;p++)g+=String.fromCharCode(c[i+p]);return g},writeASCII(c,i,u){for(let g=0;g<u.length;g++)c[i+g]=u.charCodeAt(g)},readBytes(c,i,u){const g=[];for(let p=0;p<u;p++)g.push(c[i+p]);return g},pad:c=>c.length<2?`0${c}`:c,readUTF8(c,i,u){let g,p="";for(let m=0;m<u;m++)p+=`%${e.pad(c[i+m].toString(16))}`;try{g=decodeURIComponent(p)}catch{return e.readASCII(c,i,u)}return g}};function t(c,i,u,g){const p=i*u,m=a(g),w=Math.ceil(i*m/8),C=new Uint8Array(4*p),O=new Uint32Array(C.buffer),{ctype:v}=g,{depth:E}=g,I=e.readUshort;if(v==6){const $=p<<2;if(E==8)for(var _=0;_<$;_+=4)C[_]=c[_],C[_+1]=c[_+1],C[_+2]=c[_+2],C[_+3]=c[_+3];if(E==16)for(_=0;_<$;_++)C[_]=c[_<<1]}else if(v==2){const $=g.tabs.tRNS;if($==null){if(E==8)for(_=0;_<p;_++){var M=3*_;O[_]=255<<24|c[M+2]<<16|c[M+1]<<8|c[M]}if(E==16)for(_=0;_<p;_++)M=6*_,O[_]=255<<24|c[M+4]<<16|c[M+2]<<8|c[M]}else{var x=$[0];const G=$[1],Y=$[2];if(E==8)for(_=0;_<p;_++){var y=_<<2;M=3*_,O[_]=255<<24|c[M+2]<<16|c[M+1]<<8|c[M],c[M]==x&&c[M+1]==G&&c[M+2]==Y&&(C[y+3]=0)}if(E==16)for(_=0;_<p;_++)y=_<<2,M=6*_,O[_]=255<<24|c[M+4]<<16|c[M+2]<<8|c[M],I(c,M)==x&&I(c,M+2)==G&&I(c,M+4)==Y&&(C[y+3]=0)}}else if(v==3){const $=g.tabs.PLTE,G=g.tabs.tRNS,Y=G?G.length:0;if(E==1)for(var T=0;T<u;T++){var A=T*w,P=T*i;for(_=0;_<i;_++){y=P+_<<2;var D=3*(L=c[A+(_>>3)]>>7-((7&_)<<0)&1);C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255}}if(E==2)for(T=0;T<u;T++)for(A=T*w,P=T*i,_=0;_<i;_++)y=P+_<<2,D=3*(L=c[A+(_>>2)]>>6-((3&_)<<1)&3),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255;if(E==4)for(T=0;T<u;T++)for(A=T*w,P=T*i,_=0;_<i;_++)y=P+_<<2,D=3*(L=c[A+(_>>1)]>>4-((1&_)<<2)&15),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255;if(E==8)for(_=0;_<p;_++){var L;y=_<<2,D=3*(L=c[_]),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255}}else if(v==4){if(E==8)for(_=0;_<p;_++){y=_<<2;var U=c[S=_<<1];C[y]=U,C[y+1]=U,C[y+2]=U,C[y+3]=c[S+1]}if(E==16)for(_=0;_<p;_++){var S;y=_<<2,U=c[S=_<<2],C[y]=U,C[y+1]=U,C[y+2]=U,C[y+3]=c[S+2]}}else if(v==0)for(x=g.tabs.tRNS?g.tabs.tRNS:-1,T=0;T<u;T++){const $=T*w,G=T*i;if(E==1)for(var b=0;b<i;b++){var F=(U=255*(c[$+(b>>>3)]>>>7-(7&b)&1))==255*x?0:255;O[G+b]=F<<24|U<<16|U<<8|U}else if(E==2)for(b=0;b<i;b++)F=(U=85*(c[$+(b>>>2)]>>>6-((3&b)<<1)&3))==85*x?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==4)for(b=0;b<i;b++)F=(U=17*(c[$+(b>>>1)]>>>4-((1&b)<<2)&15))==17*x?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==8)for(b=0;b<i;b++)F=(U=c[$+b])==x?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==16)for(b=0;b<i;b++)U=c[$+(b<<1)],F=I(c,$+(b<<1))==x?0:255,O[G+b]=F<<24|U<<16|U<<8|U}return C}function r(c,i,u,g){const p=a(c),m=Math.ceil(u*p/8),w=new Uint8Array((m+1+c.interlace)*g);return i=c.tabs.CgBI?o(i,w):n(i,w),c.interlace==0?i=s(i,c,0,u,g):c.interlace==1&&(i=(function(O,v){const E=v.width,I=v.height,_=a(v),M=_>>3,x=Math.ceil(E*_/8),y=new Uint8Array(I*x);let T=0;const A=[0,0,4,0,2,0,1],P=[0,4,0,2,0,1,0],D=[8,8,8,4,4,2,2],L=[8,8,4,4,2,2,1];let U=0;for(;U<7;){const b=D[U],F=L[U];let $=0,G=0,Y=A[U];for(;Y<I;)Y+=b,G++;let B=P[U];for(;B<E;)B+=F,$++;const K=Math.ceil($*_/8);s(O,v,T,$,G);let j=0,H=A[U];for(;H<I;){let q=P[U],ue=T+j*K<<3;for(;q<E;){var S;if(_==1&&(S=(S=O[ue>>3])>>7-(7&ue)&1,y[H*x+(q>>3)]|=S<<7-((7&q)<<0)),_==2&&(S=(S=O[ue>>3])>>6-(7&ue)&3,y[H*x+(q>>2)]|=S<<6-((3&q)<<1)),_==4&&(S=(S=O[ue>>3])>>4-(7&ue)&15,y[H*x+(q>>1)]|=S<<4-((1&q)<<2)),_>=8){const te=H*x+q*M;for(let fe=0;fe<M;fe++)y[te+fe]=O[(ue>>3)+fe]}ue+=_,q+=F}j++,H+=b}$*G!=0&&(T+=G*(1+K)),U+=1}return y})(i,c)),i}function n(c,i){return o(new Uint8Array(c.buffer,2,c.length-6),i)}var o=(function(){const c={H:{}};return c.H.N=function(i,u){const g=Uint8Array;let p,m,w=0,C=0,O=0,v=0,E=0,I=0,_=0,M=0,x=0;if(i[0]==3&&i[1]==0)return u||new g(0);const y=c.H,T=y.b,A=y.e,P=y.R,D=y.n,L=y.A,U=y.Z,S=y.m,b=u==null;for(b&&(u=new g(i.length>>>2<<5));w==0;)if(w=T(i,x,1),C=T(i,x+1,2),x+=3,C!=0){if(b&&(u=c.H.W(u,M+(1<<17))),C==1&&(p=S.J,m=S.h,I=511,_=31),C==2){O=A(i,x,5)+257,v=A(i,x+5,5)+1,E=A(i,x+10,4)+4,x+=14;let $=1;for(var F=0;F<38;F+=2)S.Q[F]=0,S.Q[F+1]=0;for(F=0;F<E;F++){const B=A(i,x+3*F,3);S.Q[1+(S.X[F]<<1)]=B,B>$&&($=B)}x+=3*E,D(S.Q,$),L(S.Q,$,S.u),p=S.w,m=S.d,x=P(S.u,(1<<$)-1,O+v,i,x,S.v);const G=y.V(S.v,0,O,S.C);I=(1<<G)-1;const Y=y.V(S.v,O,v,S.D);_=(1<<Y)-1,D(S.C,G),L(S.C,G,p),D(S.D,Y),L(S.D,Y,m)}for(;;){const $=p[U(i,x)&I];x+=15&$;const G=$>>>4;if(!(G>>>8))u[M++]=G;else{if(G==256)break;{let Y=M+G-254;if(G>264){const q=S.q[G-257];Y=M+(q>>>3)+A(i,x,7&q),x+=7&q}const B=m[U(i,x)&_];x+=15&B;const K=B>>>4,j=S.c[K],H=(j>>>4)+T(i,x,15&j);for(x+=15&j;M<Y;)u[M]=u[M++-H],u[M]=u[M++-H],u[M]=u[M++-H],u[M]=u[M++-H];M=Y}}}}else{(7&x)!=0&&(x+=8-(7&x));const $=4+(x>>>3),G=i[$-4]|i[$-3]<<8;b&&(u=c.H.W(u,M+G)),u.set(new g(i.buffer,i.byteOffset+$,G),M),x=$+G<<3,M+=G}return u.length==M?u:u.slice(0,M)},c.H.W=function(i,u){const g=i.length;if(u<=g)return i;const p=new Uint8Array(g<<1);return p.set(i,0),p},c.H.R=function(i,u,g,p,m,w){const C=c.H.e,O=c.H.Z;let v=0;for(;v<g;){const E=i[O(p,m)&u];m+=15&E;const I=E>>>4;if(I<=15)w[v]=I,v++;else{let _=0,M=0;I==16?(M=3+C(p,m,2),m+=2,_=w[v-1]):I==17?(M=3+C(p,m,3),m+=3):I==18&&(M=11+C(p,m,7),m+=7);const x=v+M;for(;v<x;)w[v]=_,v++}}return m},c.H.V=function(i,u,g,p){let m=0,w=0;const C=p.length>>>1;for(;w<g;){const O=i[w+u];p[w<<1]=0,p[1+(w<<1)]=O,O>m&&(m=O),w++}for(;w<C;)p[w<<1]=0,p[1+(w<<1)]=0,w++;return m},c.H.n=function(i,u){const g=c.H.m,p=i.length;let m,w,C,O;const v=g.j;for(var E=0;E<=u;E++)v[E]=0;for(E=1;E<p;E+=2)v[i[E]]++;const I=g.K;for(m=0,v[0]=0,w=1;w<=u;w++)m=m+v[w-1]<<1,I[w]=m;for(C=0;C<p;C+=2)O=i[C+1],O!=0&&(i[C]=I[O],I[O]++)},c.H.A=function(i,u,g){const p=i.length,m=c.H.m.r;for(let w=0;w<p;w+=2)if(i[w+1]!=0){const C=w>>1,O=i[w+1],v=C<<4|O,E=u-O;let I=i[w]<<E;const _=I+(1<<E);for(;I!=_;)g[m[I]>>>15-u]=v,I++}},c.H.l=function(i,u){const g=c.H.m.r,p=15-u;for(let m=0;m<i.length;m+=2){const w=i[m]<<u-i[m+1];i[m]=g[w]>>>p}},c.H.M=function(i,u,g){g<<=7&u;const p=u>>>3;i[p]|=g,i[p+1]|=g>>>8},c.H.I=function(i,u,g){g<<=7&u;const p=u>>>3;i[p]|=g,i[p+1]|=g>>>8,i[p+2]|=g>>>16},c.H.e=function(i,u,g){return(i[u>>>3]|i[1+(u>>>3)]<<8)>>>(7&u)&(1<<g)-1},c.H.b=function(i,u,g){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16)>>>(7&u)&(1<<g)-1},c.H.Z=function(i,u){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16)>>>(7&u)},c.H.i=function(i,u){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16|i[3+(u>>>3)]<<24)>>>(7&u)},c.H.m=(function(){const i=Uint16Array,u=Uint32Array;return{K:new i(16),j:new i(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new i(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new u(32),J:new i(512),_:[],h:new i(32),$:[],w:new i(32768),C:[],v:[],d:new i(32768),D:[],u:new i(512),Q:[],r:new i(32768),s:new u(286),Y:new u(30),a:new u(19),t:new u(15e3),k:new i(65536),g:new i(32768)}})(),(function(){const i=c.H.m;for(var u=0;u<32768;u++){let p=u;p=(2863311530&p)>>>1|(1431655765&p)<<1,p=(3435973836&p)>>>2|(858993459&p)<<2,p=(4042322160&p)>>>4|(252645135&p)<<4,p=(4278255360&p)>>>8|(16711935&p)<<8,i.r[u]=(p>>>16|p<<16)>>>17}function g(p,m,w){for(;m--!=0;)p.push(0,w)}for(u=0;u<32;u++)i.q[u]=i.S[u]<<3|i.T[u],i.c[u]=i.p[u]<<4|i.z[u];g(i._,144,8),g(i._,112,9),g(i._,24,7),g(i._,8,8),c.H.n(i._,9),c.H.A(i._,9,i.J),c.H.l(i._,9),g(i.$,32,5),c.H.n(i.$,5),c.H.A(i.$,5,i.h),c.H.l(i.$,5),g(i.Q,19,0),g(i.C,286,0),g(i.D,30,0),g(i.v,320,0)})(),c.H.N})();function a(c){return[1,null,3,1,2,null,4][c.ctype]*c.depth}function s(c,i,u,g,p){let m=a(i);const w=Math.ceil(g*m/8);let C,O;m=Math.ceil(m/8);let v=c[u],E=0;if(v>1&&(c[u]=[0,0,1][v-2]),v==3)for(E=m;E<w;E++)c[E+1]=c[E+1]+(c[E+1-m]>>>1)&255;for(let I=0;I<p;I++)if(C=u+I*w,O=C+I+1,v=c[O-1],E=0,v==0)for(;E<w;E++)c[C+E]=c[O+E];else if(v==1){for(;E<m;E++)c[C+E]=c[O+E];for(;E<w;E++)c[C+E]=c[O+E]+c[C+E-m]}else if(v==2)for(;E<w;E++)c[C+E]=c[O+E]+c[C+E-w];else if(v==3){for(;E<m;E++)c[C+E]=c[O+E]+(c[C+E-w]>>>1);for(;E<w;E++)c[C+E]=c[O+E]+(c[C+E-w]+c[C+E-m]>>>1)}else{for(;E<m;E++)c[C+E]=c[O+E]+l(0,c[C+E-w],0);for(;E<w;E++)c[C+E]=c[O+E]+l(c[C+E-m],c[C+E-w],c[C+E-m-w])}return c}function l(c,i,u){const g=c+i-u,p=g-c,m=g-i,w=g-u;return p*p<=m*m&&p*p<=w*w?c:m*m<=w*w?i:u}function f(c,i,u){u.width=e.readUint(c,i),i+=4,u.height=e.readUint(c,i),i+=4,u.depth=c[i],i++,u.ctype=c[i],i++,u.compress=c[i],i++,u.filter=c[i],i++,u.interlace=c[i],i++}function h(c,i,u,g,p,m,w,C,O){const v=Math.min(i,p),E=Math.min(u,m);let I=0,_=0;for(let U=0;U<E;U++)for(let S=0;S<v;S++)if(w>=0&&C>=0?(I=U*i+S<<2,_=(C+U)*p+w+S<<2):(I=(-C+U)*i-w+S<<2,_=U*p+S<<2),O==0)g[_]=c[I],g[_+1]=c[I+1],g[_+2]=c[I+2],g[_+3]=c[I+3];else if(O==1){var M=c[I+3]*.00392156862745098,x=c[I]*M,y=c[I+1]*M,T=c[I+2]*M,A=g[_+3]*(1/255),P=g[_]*A,D=g[_+1]*A,L=g[_+2]*A;const b=1-M,F=M+A*b,$=F==0?0:1/F;g[_+3]=255*F,g[_+0]=(x+P*b)*$,g[_+1]=(y+D*b)*$,g[_+2]=(T+L*b)*$}else if(O==2)M=c[I+3],x=c[I],y=c[I+1],T=c[I+2],A=g[_+3],P=g[_],D=g[_+1],L=g[_+2],M==A&&x==P&&y==D&&T==L?(g[_]=0,g[_+1]=0,g[_+2]=0,g[_+3]=0):(g[_]=x,g[_+1]=y,g[_+2]=T,g[_+3]=M);else if(O==3){if(M=c[I+3],x=c[I],y=c[I+1],T=c[I+2],A=g[_+3],P=g[_],D=g[_+1],L=g[_+2],M==A&&x==P&&y==D&&T==L)continue;if(M<220&&A>20)return!1}return!0}return{decode:function(i){const u=new Uint8Array(i);let g=8;const p=e,m=p.readUshort,w=p.readUint,C={tabs:{},frames:[]},O=new Uint8Array(u.length);let v,E=0,I=0;const _=[137,80,78,71,13,10,26,10];for(var M=0;M<8;M++)if(u[M]!=_[M])throw"The input is not a PNG file!";for(;g<u.length;){const U=p.readUint(u,g);g+=4;const S=p.readASCII(u,g,4);if(g+=4,S=="IHDR")f(u,g,C);else if(S=="iCCP"){for(var x=g;u[x]!=0;)x++;p.readASCII(u,g,x-g),u[x+1];const b=u.slice(x+2,g+U);let F=null;try{F=n(b)}catch{F=o(b)}C.tabs[S]=F}else if(S=="CgBI")C.tabs[S]=u.slice(g,g+4);else if(S=="IDAT"){for(M=0;M<U;M++)O[E+M]=u[g+M];E+=U}else if(S=="acTL")C.tabs[S]={num_frames:w(u,g),num_plays:w(u,g+4)},v=new Uint8Array(u.length);else if(S=="fcTL"){I!=0&&((L=C.frames[C.frames.length-1]).data=r(C,v.slice(0,I),L.rect.width,L.rect.height),I=0);const b={x:w(u,g+12),y:w(u,g+16),width:w(u,g+4),height:w(u,g+8)};let F=m(u,g+22);F=m(u,g+20)/(F==0?100:F);const $={rect:b,delay:Math.round(1e3*F),dispose:u[g+24],blend:u[g+25]};C.frames.push($)}else if(S=="fdAT"){for(M=0;M<U-4;M++)v[I+M]=u[g+M+4];I+=U-4}else if(S=="pHYs")C.tabs[S]=[p.readUint(u,g),p.readUint(u,g+4),u[g+8]];else if(S=="cHRM")for(C.tabs[S]=[],M=0;M<8;M++)C.tabs[S].push(p.readUint(u,g+4*M));else if(S=="tEXt"||S=="zTXt"){C.tabs[S]==null&&(C.tabs[S]={});var y=p.nextZero(u,g),T=p.readASCII(u,g,y-g),A=g+U-y-1;if(S=="tEXt")D=p.readASCII(u,y+1,A);else{var P=n(u.slice(y+2,y+2+A));D=p.readUTF8(P,0,P.length)}C.tabs[S][T]=D}else if(S=="iTXt"){C.tabs[S]==null&&(C.tabs[S]={}),y=0,x=g,y=p.nextZero(u,x),T=p.readASCII(u,x,y-x);const b=u[x=y+1];var D;u[x+1],x+=2,y=p.nextZero(u,x),p.readASCII(u,x,y-x),x=y+1,y=p.nextZero(u,x),p.readUTF8(u,x,y-x),A=U-((x=y+1)-g),b==0?D=p.readUTF8(u,x,A):(P=n(u.slice(x,x+A)),D=p.readUTF8(P,0,P.length)),C.tabs[S][T]=D}else if(S=="PLTE")C.tabs[S]=p.readBytes(u,g,U);else if(S=="hIST"){const b=C.tabs.PLTE.length/3;for(C.tabs[S]=[],M=0;M<b;M++)C.tabs[S].push(m(u,g+2*M))}else if(S=="tRNS")C.ctype==3?C.tabs[S]=p.readBytes(u,g,U):C.ctype==0?C.tabs[S]=m(u,g):C.ctype==2&&(C.tabs[S]=[m(u,g),m(u,g+2),m(u,g+4)]);else if(S=="gAMA")C.tabs[S]=p.readUint(u,g)/1e5;else if(S=="sRGB")C.tabs[S]=u[g];else if(S=="bKGD")C.ctype==0||C.ctype==4?C.tabs[S]=[m(u,g)]:C.ctype==2||C.ctype==6?C.tabs[S]=[m(u,g),m(u,g+2),m(u,g+4)]:C.ctype==3&&(C.tabs[S]=u[g]);else if(S=="IEND")break;g+=U,p.readUint(u,g),g+=4}var L;return I!=0&&((L=C.frames[C.frames.length-1]).data=r(C,v.slice(0,I),L.rect.width,L.rect.height)),C.data=r(C,O,C.width,C.height),delete C.compress,delete C.interlace,delete C.filter,C},toRGBA8:function(i){const u=i.width,g=i.height;if(i.tabs.acTL==null)return[t(i.data,u,g,i).buffer];const p=[];i.frames[0].data==null&&(i.frames[0].data=i.data);const m=u*g*4,w=new Uint8Array(m),C=new Uint8Array(m),O=new Uint8Array(m);for(let E=0;E<i.frames.length;E++){const I=i.frames[E],_=I.rect.x,M=I.rect.y,x=I.rect.width,y=I.rect.height,T=t(I.data,x,y,i);if(E!=0)for(var v=0;v<m;v++)O[v]=w[v];if(I.blend==0?h(T,x,y,w,u,g,_,M,0):I.blend==1&&h(T,x,y,w,u,g,_,M,1),p.push(w.buffer.slice(0)),I.dispose!=0){if(I.dispose==1)h(C,x,y,w,u,g,_,M,0);else if(I.dispose==2)for(v=0;v<m;v++)w[v]=O[v]}}return p},_paeth:l,_copyTile:h,_bin:e}})();(function(){const{_copyTile:e}=Ht,{_bin:t}=Ht,r=Ht._paeth;var n={table:(function(){const x=new Uint32Array(256);for(let y=0;y<256;y++){let T=y;for(let A=0;A<8;A++)1&T?T=3988292384^T>>>1:T>>>=1;x[y]=T}return x})(),update(x,y,T,A){for(let P=0;P<A;P++)x=n.table[255&(x^y[T+P])]^x>>>8;return x},crc:(x,y,T)=>4294967295^n.update(4294967295,x,y,T)};function o(x,y,T,A){y[T]+=x[0]*A>>4,y[T+1]+=x[1]*A>>4,y[T+2]+=x[2]*A>>4,y[T+3]+=x[3]*A>>4}function a(x){return Math.max(0,Math.min(255,x))}function s(x,y){const T=x[0]-y[0],A=x[1]-y[1],P=x[2]-y[2],D=x[3]-y[3];return T*T+A*A+P*P+D*D}function l(x,y,T,A,P,D,L){L==null&&(L=1);const U=A.length,S=[];for(var b=0;b<U;b++){const H=A[b];S.push([H>>>0&255,H>>>8&255,H>>>16&255,H>>>24&255])}for(b=0;b<U;b++){let H=4294967295;for(var F=0,$=0;$<U;$++){var G=s(S[b],S[$]);$!=b&&G<H&&(H=G,F=$)}}const Y=new Uint32Array(P.buffer),B=new Int16Array(y*T*4),K=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5];for(b=0;b<K.length;b++)K[b]=255*((K[b]+.5)/16-.5);for(let H=0;H<T;H++)for(let q=0;q<y;q++){var j;b=4*(H*y+q),L!=2?j=[a(x[b]+B[b]),a(x[b+1]+B[b+1]),a(x[b+2]+B[b+2]),a(x[b+3]+B[b+3])]:(G=K[4*(3&H)+(3&q)],j=[a(x[b]+G),a(x[b+1]+G),a(x[b+2]+G),a(x[b+3]+G)]),F=0;let ue=16777215;for($=0;$<U;$++){const ee=s(j,S[$]);ee<ue&&(ue=ee,F=$)}const te=S[F],fe=[j[0]-te[0],j[1]-te[1],j[2]-te[2],j[3]-te[3]];L==1&&(q!=y-1&&o(fe,B,b+4,7),H!=T-1&&(q!=0&&o(fe,B,b+4*y-4,3),o(fe,B,b+4*y,5),q!=y-1&&o(fe,B,b+4*y+4,1))),D[b>>2]=F,Y[b>>2]=A[F]}}function f(x,y,T,A,P){P==null&&(P={});const{crc:D}=n,L=t.writeUint,U=t.writeUshort,S=t.writeASCII;let b=8;const F=x.frames.length>1;let $,G=!1,Y=33+(F?20:0);if(P.sRGB!=null&&(Y+=13),P.pHYs!=null&&(Y+=21),P.iCCP!=null&&($=pako.deflate(P.iCCP),Y+=21+$.length+4),x.ctype==3){for(var B=x.plte.length,K=0;K<B;K++)x.plte[K]>>>24!=255&&(G=!0);Y+=8+3*B+4+(G?8+1*B+4:0)}for(var j=0;j<x.frames.length;j++)F&&(Y+=38),Y+=(te=x.frames[j]).cimg.length+12,j!=0&&(Y+=4);Y+=12;const H=new Uint8Array(Y),q=[137,80,78,71,13,10,26,10];for(K=0;K<8;K++)H[K]=q[K];if(L(H,b,13),b+=4,S(H,b,"IHDR"),b+=4,L(H,b,y),b+=4,L(H,b,T),b+=4,H[b]=x.depth,b++,H[b]=x.ctype,b++,H[b]=0,b++,H[b]=0,b++,H[b]=0,b++,L(H,b,D(H,b-17,17)),b+=4,P.sRGB!=null&&(L(H,b,1),b+=4,S(H,b,"sRGB"),b+=4,H[b]=P.sRGB,b++,L(H,b,D(H,b-5,5)),b+=4),P.iCCP!=null){const fe=13+$.length;L(H,b,fe),b+=4,S(H,b,"iCCP"),b+=4,S(H,b,"ICC profile"),b+=11,b+=2,H.set($,b),b+=$.length,L(H,b,D(H,b-(fe+4),fe+4)),b+=4}if(P.pHYs!=null&&(L(H,b,9),b+=4,S(H,b,"pHYs"),b+=4,L(H,b,P.pHYs[0]),b+=4,L(H,b,P.pHYs[1]),b+=4,H[b]=P.pHYs[2],b++,L(H,b,D(H,b-13,13)),b+=4),F&&(L(H,b,8),b+=4,S(H,b,"acTL"),b+=4,L(H,b,x.frames.length),b+=4,L(H,b,P.loop!=null?P.loop:0),b+=4,L(H,b,D(H,b-12,12)),b+=4),x.ctype==3){for(L(H,b,3*(B=x.plte.length)),b+=4,S(H,b,"PLTE"),b+=4,K=0;K<B;K++){const fe=3*K,ee=x.plte[K],ke=255&ee,me=ee>>>8&255,Ge=ee>>>16&255;H[b+fe+0]=ke,H[b+fe+1]=me,H[b+fe+2]=Ge}if(b+=3*B,L(H,b,D(H,b-3*B-4,3*B+4)),b+=4,G){for(L(H,b,B),b+=4,S(H,b,"tRNS"),b+=4,K=0;K<B;K++)H[b+K]=x.plte[K]>>>24&255;b+=B,L(H,b,D(H,b-B-4,B+4)),b+=4}}let ue=0;for(j=0;j<x.frames.length;j++){var te=x.frames[j];F&&(L(H,b,26),b+=4,S(H,b,"fcTL"),b+=4,L(H,b,ue++),b+=4,L(H,b,te.rect.width),b+=4,L(H,b,te.rect.height),b+=4,L(H,b,te.rect.x),b+=4,L(H,b,te.rect.y),b+=4,U(H,b,A[j]),b+=2,U(H,b,1e3),b+=2,H[b]=te.dispose,b++,H[b]=te.blend,b++,L(H,b,D(H,b-30,30)),b+=4);const fe=te.cimg;L(H,b,(B=fe.length)+(j==0?0:4)),b+=4;const ee=b;S(H,b,j==0?"IDAT":"fdAT"),b+=4,j!=0&&(L(H,b,ue++),b+=4),H.set(fe,b),b+=B,L(H,b,D(H,ee,b-ee)),b+=4}return L(H,b,0),b+=4,S(H,b,"IEND"),b+=4,L(H,b,D(H,b-4,4)),b+=4,H.buffer}function h(x,y,T){for(let A=0;A<x.frames.length;A++){const P=x.frames[A];P.rect.width;const D=P.rect.height,L=new Uint8Array(D*P.bpl+D);P.cimg=g(P.img,D,P.bpp,P.bpl,L,y,T)}}function c(x,y,T,A,P){const D=P[0],L=P[1],U=P[2],S=P[3],b=P[4],F=P[5];let $=6,G=8,Y=255;for(var B=0;B<x.length;B++){const Me=new Uint8Array(x[B]);for(var K=Me.length,j=0;j<K;j+=4)Y&=Me[j+3]}const H=Y!=255,q=(function(Ce,we,Fe,Le,V,re){const oe=[];for(var ae=0;ae<Ce.length;ae++){const Oe=new Uint8Array(Ce[ae]),X=new Uint32Array(Oe.buffer);var be;let se=0,ie=0,le=we,ne=Fe,Te=Le?1:0;if(ae!=0){const ve=re||Le||ae==1||oe[ae-2].dispose!=0?1:2;let Se=0,_e=1e9;for(let Ue=0;Ue<ve;Ue++){var xe=new Uint8Array(Ce[ae-1-Ue]);const je=new Uint32Array(Ce[ae-1-Ue]);let Ee=we,he=Fe,Ne=-1,Ke=-1;for(let Be=0;Be<Fe;Be++)for(let Ze=0;Ze<we;Ze++)X[Pe=Be*we+Ze]!=je[Pe]&&(Ze<Ee&&(Ee=Ze),Ze>Ne&&(Ne=Ze),Be<he&&(he=Be),Be>Ke&&(Ke=Be));Ne==-1&&(Ee=he=Ne=Ke=0),V&&((1&Ee)==1&&Ee--,(1&he)==1&&he--);const $e=(Ne-Ee+1)*(Ke-he+1);$e<_e&&(_e=$e,Se=Ue,se=Ee,ie=he,le=Ne-Ee+1,ne=Ke-he+1)}xe=new Uint8Array(Ce[ae-1-Se]),Se==1&&(oe[ae-1].dispose=2),be=new Uint8Array(le*ne*4),e(xe,we,Fe,be,le,ne,-se,-ie,0),Te=e(Oe,we,Fe,be,le,ne,-se,-ie,3)?1:0,Te==1?u(Oe,we,Fe,be,{x:se,y:ie,width:le,height:ne}):e(Oe,we,Fe,be,le,ne,-se,-ie,0)}else be=Oe.slice(0);oe.push({rect:{x:se,y:ie,width:le,height:ne},img:be,blend:Te,dispose:0})}if(Le)for(ae=0;ae<oe.length;ae++){if((Ve=oe[ae]).blend==1)continue;const Oe=Ve.rect,X=oe[ae-1].rect,se=Math.min(Oe.x,X.x),ie=Math.min(Oe.y,X.y),le={x:se,y:ie,width:Math.max(Oe.x+Oe.width,X.x+X.width)-se,height:Math.max(Oe.y+Oe.height,X.y+X.height)-ie};oe[ae-1].dispose=1,ae-1!=0&&i(Ce,we,Fe,oe,ae-1,le,V),i(Ce,we,Fe,oe,ae,le,V)}let He=0;if(Ce.length!=1)for(var Pe=0;Pe<oe.length;Pe++){var Ve;He+=(Ve=oe[Pe]).rect.width*Ve.rect.height}return oe})(x,y,T,D,L,U),ue={},te=[],fe=[];if(A!=0){const Me=[];for(j=0;j<q.length;j++)Me.push(q[j].img.buffer);const Ce=(function(V){let re=0;for(var oe=0;oe<V.length;oe++)re+=V[oe].byteLength;const ae=new Uint8Array(re);let be=0;for(oe=0;oe<V.length;oe++){const xe=new Uint8Array(V[oe]),He=xe.length;for(let Pe=0;Pe<He;Pe+=4){let Ve=xe[Pe],Oe=xe[Pe+1],X=xe[Pe+2];const se=xe[Pe+3];se==0&&(Ve=Oe=X=0),ae[be+Pe]=Ve,ae[be+Pe+1]=Oe,ae[be+Pe+2]=X,ae[be+Pe+3]=se}be+=He}return ae.buffer})(Me),we=m(Ce,A);for(j=0;j<we.plte.length;j++)te.push(we.plte[j].est.rgba);let Fe=0;for(j=0;j<q.length;j++){const Le=(ke=q[j]).img.length;var ee=new Uint8Array(we.inds.buffer,Fe>>2,Le>>2);fe.push(ee);const V=new Uint8Array(we.abuf,Fe,Le);F&&l(ke.img,ke.rect.width,ke.rect.height,te,V,ee),ke.img.set(V),Fe+=Le}}else for(B=0;B<q.length;B++){var ke=q[B];const Me=new Uint32Array(ke.img.buffer);var me=ke.rect.width;for(K=Me.length,ee=new Uint8Array(K),fe.push(ee),j=0;j<K;j++){const Ce=Me[j];if(j!=0&&Ce==Me[j-1])ee[j]=ee[j-1];else if(j>me&&Ce==Me[j-me])ee[j]=ee[j-me];else{let we=ue[Ce];if(we==null&&(ue[Ce]=we=te.length,te.push(Ce),te.length>=300))break;ee[j]=we}}}const Ge=te.length;for(Ge<=256&&b==0&&(G=Ge<=2?1:Ge<=4?2:Ge<=16?4:8,G=Math.max(G,S)),B=0;B<q.length;B++){(ke=q[B]).rect.x,ke.rect.y,me=ke.rect.width;const Me=ke.rect.height;let Ce=ke.img;new Uint32Array(Ce.buffer);let we=4*me,Fe=4;if(Ge<=256&&b==0){we=Math.ceil(G*me/8);var De=new Uint8Array(we*Me);const Le=fe[B];for(let V=0;V<Me;V++){j=V*we;const re=V*me;if(G==8)for(var ye=0;ye<me;ye++)De[j+ye]=Le[re+ye];else if(G==4)for(ye=0;ye<me;ye++)De[j+(ye>>1)]|=Le[re+ye]<<4-4*(1&ye);else if(G==2)for(ye=0;ye<me;ye++)De[j+(ye>>2)]|=Le[re+ye]<<6-2*(3&ye);else if(G==1)for(ye=0;ye<me;ye++)De[j+(ye>>3)]|=Le[re+ye]<<7-1*(7&ye)}Ce=De,$=3,Fe=1}else if(H==0&&q.length==1){De=new Uint8Array(me*Me*3);const Le=me*Me;for(j=0;j<Le;j++){const V=3*j,re=4*j;De[V]=Ce[re],De[V+1]=Ce[re+1],De[V+2]=Ce[re+2]}Ce=De,$=2,Fe=3,we=3*me}ke.img=Ce,ke.bpl=we,ke.bpp=Fe}return{ctype:$,depth:G,plte:te,frames:q}}function i(x,y,T,A,P,D,L){const U=Uint8Array,S=Uint32Array,b=new U(x[P-1]),F=new S(x[P-1]),$=P+1<x.length?new U(x[P+1]):null,G=new U(x[P]),Y=new S(G.buffer);let B=y,K=T,j=-1,H=-1;for(let ue=0;ue<D.height;ue++)for(let te=0;te<D.width;te++){const fe=D.x+te,ee=D.y+ue,ke=ee*y+fe,me=Y[ke];me==0||A[P-1].dispose==0&&F[ke]==me&&($==null||$[4*ke+3]!=0)||(fe<B&&(B=fe),fe>j&&(j=fe),ee<K&&(K=ee),ee>H&&(H=ee))}j==-1&&(B=K=j=H=0),L&&((1&B)==1&&B--,(1&K)==1&&K--),D={x:B,y:K,width:j-B+1,height:H-K+1};const q=A[P];q.rect=D,q.blend=1,q.img=new Uint8Array(D.width*D.height*4),A[P-1].dispose==0?(e(b,y,T,q.img,D.width,D.height,-D.x,-D.y,0),u(G,y,T,q.img,D)):e(G,y,T,q.img,D.width,D.height,-D.x,-D.y,0)}function u(x,y,T,A,P){e(x,y,T,A,P.width,P.height,-P.x,-P.y,2)}function g(x,y,T,A,P,D,L){const U=[];let S,b=[0,1,2,3,4];D!=-1?b=[D]:(y*A>5e5||T==1)&&(b=[0]),L&&(S={level:0});const F=ag;for(var $=0;$<b.length;$++){for(let B=0;B<y;B++)p(P,x,B,A,T,b[$]);U.push(F.deflate(P,S))}let G,Y=1e9;for($=0;$<U.length;$++)U[$].length<Y&&(G=$,Y=U[$].length);return U[G]}function p(x,y,T,A,P,D){const L=T*A;let U=L+T;if(x[U]=D,U++,D==0)if(A<500)for(var S=0;S<A;S++)x[U+S]=y[L+S];else x.set(new Uint8Array(y.buffer,L,A),U);else if(D==1){for(S=0;S<P;S++)x[U+S]=y[L+S];for(S=P;S<A;S++)x[U+S]=y[L+S]-y[L+S-P]+256&255}else if(T==0){for(S=0;S<P;S++)x[U+S]=y[L+S];if(D==2)for(S=P;S<A;S++)x[U+S]=y[L+S];if(D==3)for(S=P;S<A;S++)x[U+S]=y[L+S]-(y[L+S-P]>>1)+256&255;if(D==4)for(S=P;S<A;S++)x[U+S]=y[L+S]-r(y[L+S-P],0,0)+256&255}else{if(D==2)for(S=0;S<A;S++)x[U+S]=y[L+S]+256-y[L+S-A]&255;if(D==3){for(S=0;S<P;S++)x[U+S]=y[L+S]+256-(y[L+S-A]>>1)&255;for(S=P;S<A;S++)x[U+S]=y[L+S]+256-(y[L+S-A]+y[L+S-P]>>1)&255}if(D==4){for(S=0;S<P;S++)x[U+S]=y[L+S]+256-r(0,y[L+S-A],0)&255;for(S=P;S<A;S++)x[U+S]=y[L+S]+256-r(y[L+S-P],y[L+S-A],y[L+S-P-A])&255}}}function m(x,y){const T=new Uint8Array(x),A=T.slice(0),P=new Uint32Array(A.buffer),D=w(A,y),L=D[0],U=D[1],S=T.length,b=new Uint8Array(S>>2);let F;if(T.length<2e7)for(var $=0;$<S;$+=4)F=C(L,G=T[$]*.00392156862745098,Y=T[$+1]*.00392156862745098,B=T[$+2]*.00392156862745098,K=T[$+3]*.00392156862745098),b[$>>2]=F.ind,P[$>>2]=F.est.rgba;else for($=0;$<S;$+=4){var G=T[$]*.00392156862745098,Y=T[$+1]*(1/255),B=T[$+2]*(1/255),K=T[$+3]*(1/255);for(F=L;F.left;)F=O(F.est,G,Y,B,K)<=0?F.left:F.right;b[$>>2]=F.ind,P[$>>2]=F.est.rgba}return{abuf:A.buffer,inds:b,plte:U}}function w(x,y,T){T==null&&(T=1e-4);const A=new Uint32Array(x.buffer),P={i0:0,i1:x.length,bst:null,est:null,tdst:0,left:null,right:null};P.bst=I(x,P.i0,P.i1),P.est=_(P.bst);const D=[P];for(;D.length<y;){let U=0,S=0;for(var L=0;L<D.length;L++)D[L].est.L>U&&(U=D[L].est.L,S=L);if(U<T)break;const b=D[S],F=v(x,A,b.i0,b.i1,b.est.e,b.est.eMq255);if(b.i0>=F||b.i1<=F){b.est.L=0;continue}const $={i0:b.i0,i1:F,bst:null,est:null,tdst:0,left:null,right:null};$.bst=I(x,$.i0,$.i1),$.est=_($.bst);const G={i0:F,i1:b.i1,bst:null,est:null,tdst:0,left:null,right:null};for(G.bst={R:[],m:[],N:b.bst.N-$.bst.N},L=0;L<16;L++)G.bst.R[L]=b.bst.R[L]-$.bst.R[L];for(L=0;L<4;L++)G.bst.m[L]=b.bst.m[L]-$.bst.m[L];G.est=_(G.bst),b.left=$,b.right=G,D[S]=$,D.push(G)}for(D.sort(((U,S)=>S.bst.N-U.bst.N)),L=0;L<D.length;L++)D[L].ind=L;return[P,D]}function C(x,y,T,A,P){if(x.left==null)return x.tdst=(function($,G,Y,B,K){const j=G-$[0],H=Y-$[1],q=B-$[2],ue=K-$[3];return j*j+H*H+q*q+ue*ue})(x.est.q,y,T,A,P),x;const D=O(x.est,y,T,A,P);let L=x.left,U=x.right;D>0&&(L=x.right,U=x.left);const S=C(L,y,T,A,P);if(S.tdst<=D*D)return S;const b=C(U,y,T,A,P);return b.tdst<S.tdst?b:S}function O(x,y,T,A,P){const{e:D}=x;return D[0]*y+D[1]*T+D[2]*A+D[3]*P-x.eMq}function v(x,y,T,A,P,D){for(A-=4;T<A;){for(;E(x,T,P)<=D;)T+=4;for(;E(x,A,P)>D;)A-=4;if(T>=A)break;const L=y[T>>2];y[T>>2]=y[A>>2],y[A>>2]=L,T+=4,A-=4}for(;E(x,T,P)>D;)T-=4;return T+4}function E(x,y,T){return x[y]*T[0]+x[y+1]*T[1]+x[y+2]*T[2]+x[y+3]*T[3]}function I(x,y,T){const A=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],P=[0,0,0,0],D=T-y>>2;for(let L=y;L<T;L+=4){const U=x[L]*.00392156862745098,S=x[L+1]*(1/255),b=x[L+2]*(1/255),F=x[L+3]*(1/255);P[0]+=U,P[1]+=S,P[2]+=b,P[3]+=F,A[0]+=U*U,A[1]+=U*S,A[2]+=U*b,A[3]+=U*F,A[5]+=S*S,A[6]+=S*b,A[7]+=S*F,A[10]+=b*b,A[11]+=b*F,A[15]+=F*F}return A[4]=A[1],A[8]=A[2],A[9]=A[6],A[12]=A[3],A[13]=A[7],A[14]=A[11],{R:A,m:P,N:D}}function _(x){const{R:y}=x,{m:T}=x,{N:A}=x,P=T[0],D=T[1],L=T[2],U=T[3],S=A==0?0:1/A,b=[y[0]-P*P*S,y[1]-P*D*S,y[2]-P*L*S,y[3]-P*U*S,y[4]-D*P*S,y[5]-D*D*S,y[6]-D*L*S,y[7]-D*U*S,y[8]-L*P*S,y[9]-L*D*S,y[10]-L*L*S,y[11]-L*U*S,y[12]-U*P*S,y[13]-U*D*S,y[14]-U*L*S,y[15]-U*U*S],F=b,$=M;let G=[Math.random(),Math.random(),Math.random(),Math.random()],Y=0,B=0;if(A!=0)for(let j=0;j<16&&(G=$.multVec(F,G),B=Math.sqrt($.dot(G,G)),G=$.sml(1/B,G),!(j!=0&&Math.abs(B-Y)<1e-9));j++)Y=B;const K=[P*S,D*S,L*S,U*S];return{Cov:b,q:K,e:G,L:Y,eMq255:$.dot($.sml(255,K),G),eMq:$.dot(G,K),rgba:(Math.round(255*K[3])<<24|Math.round(255*K[2])<<16|Math.round(255*K[1])<<8|Math.round(255*K[0])<<0)>>>0}}var M={multVec:(x,y)=>[x[0]*y[0]+x[1]*y[1]+x[2]*y[2]+x[3]*y[3],x[4]*y[0]+x[5]*y[1]+x[6]*y[2]+x[7]*y[3],x[8]*y[0]+x[9]*y[1]+x[10]*y[2]+x[11]*y[3],x[12]*y[0]+x[13]*y[1]+x[14]*y[2]+x[15]*y[3]],dot:(x,y)=>x[0]*y[0]+x[1]*y[1]+x[2]*y[2]+x[3]*y[3],sml:(x,y)=>[x*y[0],x*y[1],x*y[2],x*y[3]]};Ht.encode=function(y,T,A,P,D,L,U){P==null&&(P=0),U==null&&(U=!1);const S=c(y,T,A,P,[!1,!1,!1,0,U,!1]);return h(S,-1),f(S,T,A,D,L)},Ht.encodeLL=function(y,T,A,P,D,L,U,S){const b={ctype:0+(P==1?0:2)+(D==0?0:4),depth:L,frames:[]},F=(P+D)*L,$=F*T;for(let G=0;G<y.length;G++)b.frames.push({rect:{x:0,y:0,width:T,height:A},img:new Uint8Array(y[G]),blend:0,dispose:1,bpp:Math.ceil(F/8),bpl:Math.ceil($/8)});return h(b,0,!0),f(b,T,A,U,S)},Ht.encode.compress=c,Ht.encode.dither=l,Ht.quantize=m,Ht.quantize.getKDtree=w,Ht.quantize.getNearest=C})();const Ul={toArrayBuffer(e,t){const r=e.width,n=e.height,o=r<<2,a=e.getContext("2d").getImageData(0,0,r,n),s=new Uint32Array(a.data.buffer),l=(32*r+31)/32<<2,f=l*n,h=122+f,c=new ArrayBuffer(h),i=new DataView(c),u=1<<20;let g,p,m,w,C=u,O=0,v=0,E=0;function I(x){i.setUint16(v,x,!0),v+=2}function _(x){i.setUint32(v,x,!0),v+=4}function M(x){v+=x}I(19778),_(h),M(4),_(122),_(108),_(r),_(-n>>>0),I(1),I(32),_(3),_(f),_(2835),_(2835),M(8),_(16711680),_(65280),_(255),_(4278190080),_(1466527264),(function x(){for(;O<n&&C>0;){for(w=122+O*l,g=0;g<o;)C--,p=s[E++],m=p>>>24,i.setUint32(w+g,p<<8|m),g+=4;O++}E<s.length?(C=u,setTimeout(x,Ul._dly)):t(c)})()},toBlob(e,t){this.toArrayBuffer(e,(r=>{t(new Blob([r],{type:"image/bmp"}))}))},_dly:9};var bt={CHROME:"CHROME",FIREFOX:"FIREFOX",DESKTOP_SAFARI:"DESKTOP_SAFARI",IE:"IE",IOS:"IOS",ETC:"ETC"},sg={[bt.CHROME]:16384,[bt.FIREFOX]:11180,[bt.DESKTOP_SAFARI]:16384,[bt.IE]:8192,[bt.IOS]:4096,[bt.ETC]:8192};const ra=typeof window<"u",zl=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,In=ra&&window.cordova&&window.cordova.require&&window.cordova.require("cordova/modulemapper"),lg=(ra||zl)&&(In&&In.getOriginalSymbol(window,"File")||typeof File<"u"&&File),Hl=(ra||zl)&&(In&&In.getOriginalSymbol(window,"FileReader")||typeof FileReader<"u"&&FileReader);function na(e,t,r=Date.now()){return new Promise((n=>{const o=e.split(","),a=o[0].match(/:(.*?);/)[1],s=globalThis.atob(o[1]);let l=s.length;const f=new Uint8Array(l);for(;l--;)f[l]=s.charCodeAt(l);const h=new Blob([f],{type:a});h.name=t,h.lastModified=r,n(h)}))}function Gl(e){return new Promise(((t,r)=>{const n=new Hl;n.onload=()=>t(n.result),n.onerror=o=>r(o),n.readAsDataURL(e)}))}function jl(e){return new Promise(((t,r)=>{const n=new Image;n.onload=()=>t(n),n.onerror=o=>r(o),n.src=e}))}function fr(){if(fr.cachedResult!==void 0)return fr.cachedResult;let e=bt.ETC;const{userAgent:t}=navigator;return/Chrom(e|ium)/i.test(t)?e=bt.CHROME:/iP(ad|od|hone)/i.test(t)&&/WebKit/i.test(t)?e=bt.IOS:/Safari/i.test(t)?e=bt.DESKTOP_SAFARI:/Firefox/i.test(t)?e=bt.FIREFOX:(/MSIE/i.test(t)||document.documentMode)&&(e=bt.IE),fr.cachedResult=e,fr.cachedResult}function Bl(e,t){const r=fr(),n=sg[r];let o=e,a=t,s=o*a;const l=o>a?a/o:o/a;for(;s>n*n;){const f=(n+o)/2,h=(n+a)/2;f<h?(a=h,o=h*l):(a=f*l,o=f),s=o*a}return{width:o,height:a}}function Dn(e,t){let r,n;try{if(r=new OffscreenCanvas(e,t),n=r.getContext("2d"),n===null)throw new Error("getContext of OffscreenCanvas returns null")}catch{r=document.createElement("canvas"),n=r.getContext("2d")}return r.width=e,r.height=t,[r,n]}function Wl(e,t){const{width:r,height:n}=Bl(e.width,e.height),[o,a]=Dn(r,n);return t&&/jpe?g/.test(t)&&(a.fillStyle="white",a.fillRect(0,0,o.width,o.height)),a.drawImage(e,0,0,o.width,o.height),o}function Ln(){return Ln.cachedResult!==void 0||(Ln.cachedResult=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&typeof document<"u"&&"ontouchend"in document),Ln.cachedResult}function Pn(e,t={}){return new Promise((function(r,n){let o,a;var s=function(){try{return a=Wl(o,t.fileType||e.type),r([o,a])}catch(f){return n(f)}},l=function(f){try{var h=function(c){try{throw c}catch(i){return n(i)}};try{let c;return Gl(e).then((function(i){try{return c=i,jl(c).then((function(u){try{return o=u,(function(){try{return s()}catch(g){return n(g)}})()}catch(g){return h(g)}}),h)}catch(u){return h(u)}}),h)}catch(c){h(c)}}catch(c){return n(c)}};try{if(Ln()||[bt.DESKTOP_SAFARI,bt.MOBILE_SAFARI].includes(fr()))throw new Error("Skip createImageBitmap on IOS and Safari");return createImageBitmap(e).then((function(f){try{return o=f,s()}catch{return l()}}),l)}catch{l()}}))}function $n(e,t,r,n,o=1){return new Promise((function(a,s){let l;if(t==="image/png"){let h,c,i;return h=e.getContext("2d"),{data:c}=h.getImageData(0,0,e.width,e.height),i=Ht.encode([c.buffer],e.width,e.height,4096*o),l=new Blob([i],{type:t}),l.name=r,l.lastModified=n,f.call(this)}{let h=function(){return f.call(this)};if(t==="image/bmp")return new Promise((c=>Ul.toBlob(e,c))).then(function(c){try{return l=c,l.name=r,l.lastModified=n,h.call(this)}catch(i){return s(i)}}.bind(this),s);{let c=function(){return h.call(this)};if(typeof OffscreenCanvas=="function"&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:o}).then(function(i){try{return l=i,l.name=r,l.lastModified=n,c.call(this)}catch(u){return s(u)}}.bind(this),s);{let i;return i=e.toDataURL(t,o),na(i,r,n).then(function(u){try{return l=u,c.call(this)}catch(g){return s(g)}}.bind(this),s)}}}function f(){return a(l)}}))}function Kt(e){e.width=0,e.height=0}function Nr(){return new Promise((function(e,t){let r,n,o,a;return Nr.cachedResult!==void 0?e(Nr.cachedResult):na("data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/xABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==","test.jpg",Date.now()).then((function(s){try{return r=s,Pn(r).then((function(l){try{return n=l[1],$n(n,r.type,r.name,r.lastModified).then((function(f){try{return o=f,Kt(n),Pn(o).then((function(h){try{return a=h[0],Nr.cachedResult=a.width===1&&a.height===2,e(Nr.cachedResult)}catch(c){return t(c)}}),t)}catch(h){return t(h)}}),t)}catch(f){return t(f)}}),t)}catch(l){return t(l)}}),t)}))}function Vl(e){return new Promise(((t,r)=>{const n=new Hl;n.onload=o=>{const a=new DataView(o.target.result);if(a.getUint16(0,!1)!=65496)return t(-2);const s=a.byteLength;let l=2;for(;l<s;){if(a.getUint16(l+2,!1)<=8)return t(-1);const f=a.getUint16(l,!1);if(l+=2,f==65505){if(a.getUint32(l+=2,!1)!=1165519206)return t(-1);const h=a.getUint16(l+=6,!1)==18761;l+=a.getUint32(l+4,h);const c=a.getUint16(l,h);l+=2;for(let i=0;i<c;i++)if(a.getUint16(l+12*i,h)==274)return t(a.getUint16(l+12*i+8,h))}else{if((65280&f)!=65280)break;l+=a.getUint16(l,!1)}}return t(-1)},n.onerror=o=>r(o),n.readAsArrayBuffer(e)}))}function Kl(e,t){const{width:r}=e,{height:n}=e,{maxWidthOrHeight:o}=t;let a,s=e;return isFinite(o)&&(r>o||n>o)&&([s,a]=Dn(r,n),r>n?(s.width=o,s.height=n/r*o):(s.width=r/n*o,s.height=o),a.drawImage(e,0,0,s.width,s.height),Kt(e)),s}function Yl(e,t){const{width:r}=e,{height:n}=e,[o,a]=Dn(r,n);switch(t>4&&t<9?(o.width=n,o.height=r):(o.width=r,o.height=n),t){case 2:a.transform(-1,0,0,1,r,0);break;case 3:a.transform(-1,0,0,-1,r,n);break;case 4:a.transform(1,0,0,-1,0,n);break;case 5:a.transform(0,1,1,0,0,0);break;case 6:a.transform(0,1,-1,0,n,0);break;case 7:a.transform(0,-1,-1,0,n,r);break;case 8:a.transform(0,-1,1,0,0,r)}return a.drawImage(e,0,0,r,n),Kt(e),o}function Xl(e,t,r=0){return new Promise((function(n,o){let a,s,l,f,h,c,i,u,g,p,m,w,C,O,v,E,I,_,M,x;function y(A=5){if(t.signal&&t.signal.aborted)throw t.signal.reason;a+=A,t.onProgress(Math.min(a,100))}function T(A){if(t.signal&&t.signal.aborted)throw t.signal.reason;a=Math.min(Math.max(A,a),100),t.onProgress(a)}return a=r,s=t.maxIteration||10,l=1024*t.maxSizeMB*1024,y(),Pn(e,t).then(function(A){try{return[,f]=A,y(),h=Kl(f,t),y(),new Promise((function(P,D){var L;if(!(L=t.exifOrientation))return Vl(e).then(function(S){try{return L=S,U.call(this)}catch(b){return D(b)}}.bind(this),D);function U(){return P(L)}return U.call(this)})).then(function(P){try{return c=P,y(),Nr().then(function(D){try{return i=D?h:Yl(h,c),y(),u=t.initialQuality||1,g=t.fileType||e.type,$n(i,g,e.name,e.lastModified,u).then(function(L){try{{let S=function(){if(s--&&(v>l||v>C)){let F,$;return F=x?.95*M.width:M.width,$=x?.95*M.height:M.height,[I,_]=Dn(F,$),_.drawImage(M,0,0,F,$),u*=g==="image/png"?.85:.95,$n(I,g,e.name,e.lastModified,u).then((function(G){try{return E=G,Kt(M),M=I,v=E.size,T(Math.min(99,Math.floor((O-v)/(O-l)*100))),S}catch(Y){return o(Y)}}),o)}return[1]},b=function(){return Kt(M),Kt(I),Kt(h),Kt(i),Kt(f),T(100),n(E)};if(p=L,y(),m=p.size>l,w=p.size>e.size,!m&&!w)return T(100),n(p);var U;return C=e.size,O=p.size,v=O,M=i,x=!t.alwaysKeepResolution&&m,(U=function(F){for(;F;){if(F.then)return void F.then(U,o);try{if(F.pop){if(F.length)return F.pop()?b.call(this):F;F=S}else F=F.call(this)}catch($){return o($)}}}.bind(this))(S)}}catch(S){return o(S)}}.bind(this),o)}catch(L){return o(L)}}.bind(this),o)}catch(D){return o(D)}}.bind(this),o)}catch(P){return o(P)}}.bind(this),o)}))}const ig=` + `.trim().replace(/\s+/g," ");return d.jsx(W,{id:a,className:c,dangerouslySetInnerHTML:{__html:h},...s})},Zf=Object.freeze(Object.defineProperty({__proto__:null,HtmlContent:Zo},Symbol.toStringTag,{value:"Module"})),Qo=()=>window.G7Core,Jo=(e,t)=>Qo()?.t?.(e,t)??e,Ll=({content:e="",onChange:t,isHtml:r=!1,onIsHtmlChange:n,rows:o=15,placeholder:a="",label:s,showHtmlModeToggle:l=!0,contentClassName:f="",purifyConfig:h,className:c="",name:i="content",htmlFieldName:u="content_mode",readOnly:g=!1,id:p,editorAttrs:m})=>{const[x,C]=k.useState(r),[O,v]=k.useState(e),[E,I]=k.useState(!1);k.useEffect(()=>{C(r)},[r]),k.useEffect(()=>{v(e)},[e]);const _=k.useCallback(y=>{const T=y.target.value;if(v(T),t){const A=Qo()?.createChangeEvent?.({value:T,name:i,type:"textarea"})??{target:{name:i,value:T}};t(A)}},[t,i]),M=k.useCallback(y=>{const T=y.target.checked;if(C(T),T||I(!1),n){const A=Qo()?.createChangeEvent?.({checked:T,name:u,type:"checkbox"})??{target:{name:u,checked:T}};n(A)}},[n,u]),w=k.useCallback(()=>{I(y=>!y)},[]);return d.jsxs(W,{className:`space-y-2 ${c}`,id:p,...m,children:[d.jsxs(W,{className:"flex items-center justify-between",children:[s&&d.jsx(yr,{className:"block text-sm font-medium text-gray-500 dark:text-gray-400",children:s}),d.jsxs(W,{className:"flex items-center gap-3",children:[x&&!g&&d.jsx(Q,{type:"button",onClick:w,className:`px-3 py-1.5 text-xs font-bold rounded-lg focus:outline-none focus:ring-2 ${E?"text-gray-700 dark:text-gray-200 bg-gray-200 dark:bg-gray-600 border border-gray-300 dark:border-gray-500 hover:bg-gray-300 dark:hover:bg-gray-500 focus:ring-gray-400 dark:focus:ring-gray-500":"text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-700 border border-blue-300 dark:border-blue-600 hover:bg-blue-50 dark:hover:bg-gray-600 focus:ring-blue-500 dark:focus:ring-blue-600"}`,children:Jo(E?"common.preview_off":"common.preview")}),l&&d.jsxs(yr,{className:"flex items-center gap-2 p-2 cursor-pointer",children:[d.jsx(xt,{type:"checkbox",name:u,checked:x,onChange:M,disabled:g,className:"w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}),d.jsx(W,{className:"text-sm font-medium text-gray-700 dark:text-gray-300",children:Jo("common.html_mode")})]})]})]}),!E&&d.jsx(no,{name:i,value:O,onChange:_,placeholder:a,rows:o,readOnly:g,className:`block w-full rounded-lg border px-3 py-2 text-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed ${x?"font-mono bg-white dark:bg-gray-800 border-blue-300 dark:border-blue-600 text-gray-800 dark:text-gray-200 focus:border-blue-500 focus:ring-blue-500":"bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-900 dark:text-white focus:border-blue-500 focus:ring-blue-500"}`}),E&&d.jsx(W,{className:"p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 min-h-[400px]",children:d.jsx(Zo,{content:O,isHtml:!0,className:f||"prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100",purifyConfig:h})})]})},Qf=Object.freeze(Object.defineProperty({__proto__:null,HtmlEditor:Ll},Symbol.toStringTag,{value:"Module"})),Pl=e=>window.G7Core?.t?.(e)??e,Ro=({maxHeight:e=500,expandText:t,collapseText:r,className:n="",children:o,id:a,editorAttrs:s})=>{const[l,f]=k.useState(!1),[h,c]=k.useState(!1),i=k.useRef(null),u=k.useCallback(()=>{if(i.current){const x=i.current.scrollHeight;c(x>e)}},[e]);k.useEffect(()=>{u();const x=i.current;if(!x)return;const C=x.querySelectorAll("img");C.forEach(v=>{v.complete||v.addEventListener("load",u)});let O=null;return typeof ResizeObserver<"u"&&(O=new ResizeObserver(u),O.observe(x)),()=>{C.forEach(v=>{v.removeEventListener("load",u)}),O?.disconnect()}},[u,o]);const g=()=>{f(x=>!x)},p=t||Pl("sirsoft-ecommerce.shop.product.expand_detail"),m=r||Pl("sirsoft-ecommerce.shop.product.collapse_detail");return d.jsxs(W,{className:n,id:a,...s,children:[d.jsxs(W,{className:"relative",children:[d.jsx(W,{ref:i,className:!l&&h?"overflow-hidden transition-[max-height] duration-300 ease-in-out":"transition-[max-height] duration-300 ease-in-out",style:!l&&h?{maxHeight:`${e}px`}:void 0,children:o}),!l&&h&&d.jsx(W,{className:"absolute bottom-0 left-0 right-0 h-24 bg-gradient-to-t from-white dark:from-gray-900 to-transparent pointer-events-none"})]}),h&&d.jsxs(Q,{type:"button",onClick:g,className:"flex items-center justify-center w-full py-3.5 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer gap-1.5",children:[d.jsx(R,{className:"text-sm font-semibold text-gray-600 dark:text-gray-400",children:l?m:p}),d.jsx(de,{name:l?"chevron-up":"chevron-down",size:"sm"})]})]})},Jf=Object.freeze(Object.defineProperty({__proto__:null,ExpandableContent:Ro,default:Ro},Symbol.toStringTag,{value:"Module"})),Rf=(e,t)=>window.G7Core?.t?.(e,t)??e,ea=({isOpen:e,onClose:t,title:r,icon:n,iconClassName:o="text-gray-500 dark:text-gray-400",closeOnBackdropClick:a=!0,closeOnOverlayClick:s,showCloseButton:l=!0,closeOnEscape:f=!0,children:h,width:c="500px",className:i="",style:u})=>{const g=s!==void 0?s:a,p=k.useRef(null),m=k.useRef(null);if(k.useEffect(()=>{const C=O=>{O.key==="Escape"&&e&&f&&t()};if(e&&f)return document.addEventListener("keydown",C),()=>document.removeEventListener("keydown",C)},[e,t,f]),k.useEffect(()=>{if(e&&p.current){m.current=document.activeElement;const C=p.current.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),O=C[0],v=C[C.length-1];O?.focus();const E=I=>{I.key==="Tab"&&(I.shiftKey?document.activeElement===O&&(I.preventDefault(),v?.focus()):document.activeElement===v&&(I.preventDefault(),O?.focus()))};return document.addEventListener("keydown",E),()=>{document.removeEventListener("keydown",E),m.current?.focus()}}},[e]),k.useEffect(()=>{if(e)return document.body.style.overflow="hidden",()=>{document.body.style.overflow=""}},[e]),!e)return null;const x=u?.zIndex??50;return d.jsxs(W,{className:"fixed inset-0 flex items-center justify-center",style:{zIndex:x},onClick:g?t:void 0,children:[d.jsx(W,{className:"absolute inset-0 bg-black/50","aria-hidden":"true"}),d.jsxs(W,{ref:p,className:`relative bg-white dark:bg-gray-800 rounded-lg shadow-xl max-h-[90vh] max-w-[calc(100vw-2rem)] overflow-hidden ${i}`,style:{width:c,...u?{...u,zIndex:void 0}:{}},onClick:C=>C.stopPropagation(),role:"dialog","aria-modal":"true","aria-labelledby":r?"modal-title":void 0,children:[(r||n||l)&&d.jsxs(W,{className:"flex items-center justify-between p-4",children:[(r||n)&&d.jsxs(W,{id:"modal-title",className:"flex items-center gap-3 text-lg font-semibold text-gray-900 dark:text-white",children:[n&&d.jsx(de,{name:n,size:"md",className:o}),r]}),l&&d.jsx(Q,{onClick:t,className:"ml-auto text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 focus:outline-none","aria-label":Rf("common.close_modal"),children:d.jsx("svg",{className:"w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",children:d.jsx("path",{strokeLinecap:"round",strokeLinejoin:"round",strokeWidth:2,d:"M6 18L18 6M6 6l12 12"})})})]}),d.jsx(W,{className:"px-6 py-4 overflow-y-auto max-h-[calc(90vh-140px)]",children:h})]})]})},eg=Object.freeze(Object.defineProperty({__proto__:null,Modal:ea},Symbol.toStringTag,{value:"Module"})),ta=(e,t)=>window.G7Core?.t?.(e,t)??e,ra=({isOpen:e,onClose:t,title:r,message:n,confirmText:o,cancelText:a,onConfirm:s,onCancel:l,confirmButtonVariant:f="primary",cancelButtonVariant:h="secondary",width:c="500px",closeOnOverlay:i=!0,isLoading:u=!1,loadingText:g})=>{const p=o??ta("common.confirm"),m=a??ta("common.cancel"),x=g??ta("common.loading"),C=()=>{s()},O=()=>{u||(l?.(),t())},v=()=>{u||t()},E={primary:"bg-blue-600 text-white hover:bg-blue-700",secondary:"bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600",danger:"bg-red-600 text-white hover:bg-red-700",ghost:"bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"};return d.jsxs(ea,{isOpen:e,onClose:v,title:r,width:c,children:[d.jsx(W,{className:"py-2",children:typeof n=="string"?d.jsx(Dt,{className:"text-gray-700 dark:text-gray-300",children:n}):n}),d.jsxs(W,{className:"flex justify-end gap-3 mt-6",children:[d.jsx(Q,{onClick:O,disabled:u,className:"px-4 py-2 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed",children:d.jsx(R,{children:m})}),d.jsxs(Q,{onClick:C,disabled:u,className:`flex items-center gap-2 px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed ${E[f]}`,children:[u&&d.jsx(Ct,{className:"fa-solid fa-spinner w-4 h-4 animate-spin"}),d.jsx(R,{children:u?x:p})]})]})]})},tg=Object.freeze(Object.defineProperty({__proto__:null,ConfirmDialog:ra},Symbol.toStringTag,{value:"Module"})),$l=k.createContext(null),Fl=({value:e,children:t})=>d.jsx($l.Provider,{value:e,children:t}),rg=()=>{const e=k.useContext($l);if(!e)throw new Error("useFileUploaderContext must be used within FileUploaderProvider");return e};function ng(e,t){return t.forEach((function(r){r&&typeof r!="string"&&!Array.isArray(r)&&Object.keys(r).forEach((function(n){if(n!=="default"&&!(n in e)){var o=Object.getOwnPropertyDescriptor(r,n);Object.defineProperty(e,n,o.get?o:{enumerable:!0,get:function(){return r[n]}})}}))})),Object.freeze(e)}function Ul(e,t){return new Promise((function(r,n){let o;return og(e).then((function(a){try{return o=a,r(new Blob([t.slice(0,2),o,t.slice(2)],{type:"image/jpeg"}))}catch(s){return n(s)}}),n)}))}const og=e=>new Promise(((t,r)=>{const n=new FileReader;n.addEventListener("load",(({target:{result:o}})=>{const a=new DataView(o);let s=0;if(a.getUint16(s)!==65496)return r("not a valid JPEG");for(s+=2;;){const l=a.getUint16(s);if(l===65498)break;const f=a.getUint16(s+2);if(l===65505&&a.getUint32(s+4)===1165519206){const h=s+10;let c;switch(a.getUint16(h)){case 18761:c=!0;break;case 19789:c=!1;break;default:return r("TIFF header contains invalid endian")}if(a.getUint16(h+2,c)!==42)return r("TIFF header contains invalid version");const i=a.getUint32(h+4,c),u=h+i+2+12*a.getUint16(h+i,c);for(let g=h+i+2;g<u;g+=12)if(a.getUint16(g,c)==274){if(a.getUint16(g+2,c)!==3)return r("Orientation data type is invalid");if(a.getUint32(g+4,c)!==1)return r("Orientation data count is invalid");a.setUint16(g+8,1,c);break}return t(o.slice(s,s+2+f))}s+=2+f}return t(new Blob)})),n.readAsArrayBuffer(e)}));var On={},ag={get exports(){return On},set exports(e){On=e}};(function(e){var t,r,n={};ag.exports=n,n.parse=function(o,a){for(var s=n.bin.readUshort,l=n.bin.readUint,f=0,h={},c=new Uint8Array(o),i=c.length-4;l(c,i)!=101010256;)i--;f=i,f+=4;var u=s(c,f+=4);s(c,f+=2);var g=l(c,f+=2),p=l(c,f+=4);f+=4,f=p;for(var m=0;m<u;m++){l(c,f),f+=4,f+=4,f+=4,l(c,f+=4),g=l(c,f+=4);var x=l(c,f+=4),C=s(c,f+=4),O=s(c,f+2),v=s(c,f+4);f+=6;var E=l(c,f+=8);f+=4,f+=C+O+v,n._readLocal(c,E,h,g,x,a)}return h},n._readLocal=function(o,a,s,l,f,h){var c=n.bin.readUshort,i=n.bin.readUint;i(o,a),c(o,a+=4),c(o,a+=2);var u=c(o,a+=2);i(o,a+=2),i(o,a+=4),a+=4;var g=c(o,a+=8),p=c(o,a+=2);a+=2;var m=n.bin.readUTF8(o,a,g);if(a+=g,a+=p,h)s[m]={size:f,csize:l};else{var x=new Uint8Array(o.buffer,a);if(u==0)s[m]=new Uint8Array(x.buffer.slice(a,a+l));else{if(u!=8)throw"unknown compression method: "+u;var C=new Uint8Array(f);n.inflateRaw(x,C),s[m]=C}}},n.inflateRaw=function(o,a){return n.F.inflate(o,a)},n.inflate=function(o,a){return o[0],o[1],n.inflateRaw(new Uint8Array(o.buffer,o.byteOffset+2,o.length-6),a)},n.deflate=function(o,a){a==null&&(a={level:6});var s=0,l=new Uint8Array(50+Math.floor(1.1*o.length));l[s]=120,l[s+1]=156,s+=2,s=n.F.deflateRaw(o,l,s,a.level);var f=n.adler(o,0,o.length);return l[s+0]=f>>>24&255,l[s+1]=f>>>16&255,l[s+2]=f>>>8&255,l[s+3]=f>>>0&255,new Uint8Array(l.buffer,0,s+4)},n.deflateRaw=function(o,a){a==null&&(a={level:6});var s=new Uint8Array(50+Math.floor(1.1*o.length)),l=n.F.deflateRaw(o,s,l,a.level);return new Uint8Array(s.buffer,0,l)},n.encode=function(o,a){a==null&&(a=!1);var s=0,l=n.bin.writeUint,f=n.bin.writeUshort,h={};for(var c in o){var i=!n._noNeed(c)&&!a,u=o[c],g=n.crc.crc(u,0,u.length);h[c]={cpr:i,usize:u.length,crc:g,file:i?n.deflateRaw(u):u}}for(var c in h)s+=h[c].file.length+30+46+2*n.bin.sizeUTF8(c);s+=22;var p=new Uint8Array(s),m=0,x=[];for(var c in h){var C=h[c];x.push(m),m=n._writeHeader(p,m,c,C,0)}var O=0,v=m;for(var c in h)C=h[c],x.push(m),m=n._writeHeader(p,m,c,C,1,x[O++]);var E=m-v;return l(p,m,101010256),m+=4,f(p,m+=4,O),f(p,m+=2,O),l(p,m+=2,E),l(p,m+=4,v),m+=4,m+=2,p.buffer},n._noNeed=function(o){var a=o.split(".").pop().toLowerCase();return"png,jpg,jpeg,zip".indexOf(a)!=-1},n._writeHeader=function(o,a,s,l,f,h){var c=n.bin.writeUint,i=n.bin.writeUshort,u=l.file;return c(o,a,f==0?67324752:33639248),a+=4,f==1&&(a+=2),i(o,a,20),i(o,a+=2,0),i(o,a+=2,l.cpr?8:0),c(o,a+=2,0),c(o,a+=4,l.crc),c(o,a+=4,u.length),c(o,a+=4,l.usize),i(o,a+=4,n.bin.sizeUTF8(s)),i(o,a+=2,0),a+=2,f==1&&(a+=2,a+=2,c(o,a+=6,h),a+=4),a+=n.bin.writeUTF8(o,a,s),f==0&&(o.set(u,a),a+=u.length),a},n.crc={table:(function(){for(var o=new Uint32Array(256),a=0;a<256;a++){for(var s=a,l=0;l<8;l++)1&s?s=3988292384^s>>>1:s>>>=1;o[a]=s}return o})(),update:function(o,a,s,l){for(var f=0;f<l;f++)o=n.crc.table[255&(o^a[s+f])]^o>>>8;return o},crc:function(o,a,s){return 4294967295^n.crc.update(4294967295,o,a,s)}},n.adler=function(o,a,s){for(var l=1,f=0,h=a,c=a+s;h<c;){for(var i=Math.min(h+5552,c);h<i;)f+=l+=o[h++];l%=65521,f%=65521}return f<<16|l},n.bin={readUshort:function(o,a){return o[a]|o[a+1]<<8},writeUshort:function(o,a,s){o[a]=255&s,o[a+1]=s>>8&255},readUint:function(o,a){return 16777216*o[a+3]+(o[a+2]<<16|o[a+1]<<8|o[a])},writeUint:function(o,a,s){o[a]=255&s,o[a+1]=s>>8&255,o[a+2]=s>>16&255,o[a+3]=s>>24&255},readASCII:function(o,a,s){for(var l="",f=0;f<s;f++)l+=String.fromCharCode(o[a+f]);return l},writeASCII:function(o,a,s){for(var l=0;l<s.length;l++)o[a+l]=s.charCodeAt(l)},pad:function(o){return o.length<2?"0"+o:o},readUTF8:function(o,a,s){for(var l,f="",h=0;h<s;h++)f+="%"+n.bin.pad(o[a+h].toString(16));try{l=decodeURIComponent(f)}catch{return n.bin.readASCII(o,a,s)}return l},writeUTF8:function(o,a,s){for(var l=s.length,f=0,h=0;h<l;h++){var c=s.charCodeAt(h);if((4294967168&c)==0)o[a+f]=c,f++;else if((4294965248&c)==0)o[a+f]=192|c>>6,o[a+f+1]=128|c>>0&63,f+=2;else if((4294901760&c)==0)o[a+f]=224|c>>12,o[a+f+1]=128|c>>6&63,o[a+f+2]=128|c>>0&63,f+=3;else{if((4292870144&c)!=0)throw"e";o[a+f]=240|c>>18,o[a+f+1]=128|c>>12&63,o[a+f+2]=128|c>>6&63,o[a+f+3]=128|c>>0&63,f+=4}}return f},sizeUTF8:function(o){for(var a=o.length,s=0,l=0;l<a;l++){var f=o.charCodeAt(l);if((4294967168&f)==0)s++;else if((4294965248&f)==0)s+=2;else if((4294901760&f)==0)s+=3;else{if((4292870144&f)!=0)throw"e";s+=4}}return s}},n.F={},n.F.deflateRaw=function(o,a,s,l){var f=[[0,0,0,0,0],[4,4,8,4,0],[4,5,16,8,0],[4,6,16,16,0],[4,10,16,32,0],[8,16,32,32,0],[8,16,128,128,0],[8,32,128,256,0],[32,128,258,1024,1],[32,258,258,4096,1]][l],h=n.F.U,c=n.F._goodIndex;n.F._hash;var i=n.F._putsE,u=0,g=s<<3,p=0,m=o.length;if(l==0){for(;u<m;)i(a,g,u+(A=Math.min(65535,m-u))==m?1:0),g=n.F._copyExact(o,u,A,a,g+8),u+=A;return g>>>3}var x=h.lits,C=h.strt,O=h.prev,v=0,E=0,I=0,_=0,M=0,w=0;for(m>2&&(C[w=n.F._hash(o,0)]=0),u=0;u<m;u++){if(M=w,u+1<m-2){w=n.F._hash(o,u+1);var y=u+1&32767;O[y]=C[w],C[w]=y}if(p<=u){(v>14e3||E>26697)&&m-u>100&&(p<u&&(x[v]=u-p,v+=2,p=u),g=n.F._writeBlock(u==m-1||p==m?1:0,x,v,_,o,I,u-I,a,g),v=E=_=0,I=u);var T=0;u<m-2&&(T=n.F._bestMatch(o,u,O,M,Math.min(f[2],m-u),f[3]));var A=T>>>16,P=65535&T;if(T!=0){P=65535&T;var D=c(A=T>>>16,h.of0);h.lhst[257+D]++;var L=c(P,h.df0);h.dhst[L]++,_+=h.exb[D]+h.dxb[L],x[v]=A<<23|u-p,x[v+1]=P<<16|D<<8|L,v+=2,p=u+A}else h.lhst[o[u]]++;E++}}for(I==u&&o.length!=0||(p<u&&(x[v]=u-p,v+=2,p=u),g=n.F._writeBlock(1,x,v,_,o,I,u-I,a,g),v=0,E=0,v=E=_=0,I=u);(7&g)!=0;)g++;return g>>>3},n.F._bestMatch=function(o,a,s,l,f,h){var c=32767&a,i=s[c],u=c-i+32768&32767;if(i==c||l!=n.F._hash(o,a-u))return 0;for(var g=0,p=0,m=Math.min(32767,a);u<=m&&--h!=0&&i!=c;){if(g==0||o[a+g]==o[a+g-u]){var x=n.F._howLong(o,a,u);if(x>g){if(p=u,(g=x)>=f)break;u+2<x&&(x=u+2);for(var C=0,O=0;O<x-2;O++){var v=a-u+O+32768&32767,E=v-s[v]+32768&32767;E>C&&(C=E,i=v)}}}u+=(c=i)-(i=s[c])+32768&32767}return g<<16|p},n.F._howLong=function(o,a,s){if(o[a]!=o[a-s]||o[a+1]!=o[a+1-s]||o[a+2]!=o[a+2-s])return 0;var l=a,f=Math.min(o.length,a+258);for(a+=3;a<f&&o[a]==o[a-s];)a++;return a-l},n.F._hash=function(o,a){return(o[a]<<8|o[a+1])+(o[a+2]<<4)&65535},n.saved=0,n.F._writeBlock=function(o,a,s,l,f,h,c,i,u){var g,p,m,x,C,O,v,E,I,_=n.F.U,M=n.F._putsF,w=n.F._putsE;_.lhst[256]++,p=(g=n.F.getTrees())[0],m=g[1],x=g[2],C=g[3],O=g[4],v=g[5],E=g[6],I=g[7];var y=32+((u+3&7)==0?0:8-(u+3&7))+(c<<3),T=l+n.F.contSize(_.fltree,_.lhst)+n.F.contSize(_.fdtree,_.dhst),A=l+n.F.contSize(_.ltree,_.lhst)+n.F.contSize(_.dtree,_.dhst);A+=14+3*v+n.F.contSize(_.itree,_.ihst)+(2*_.ihst[16]+3*_.ihst[17]+7*_.ihst[18]);for(var P=0;P<286;P++)_.lhst[P]=0;for(P=0;P<30;P++)_.dhst[P]=0;for(P=0;P<19;P++)_.ihst[P]=0;var D=y<T&&y<A?0:T<A?1:2;if(M(i,u,o),M(i,u+1,D),u+=3,D==0){for(;(7&u)!=0;)u++;u=n.F._copyExact(f,h,c,i,u)}else{var L,U;if(D==1&&(L=_.fltree,U=_.fdtree),D==2){n.F.makeCodes(_.ltree,p),n.F.revCodes(_.ltree,p),n.F.makeCodes(_.dtree,m),n.F.revCodes(_.dtree,m),n.F.makeCodes(_.itree,x),n.F.revCodes(_.itree,x),L=_.ltree,U=_.dtree,w(i,u,C-257),w(i,u+=5,O-1),w(i,u+=5,v-4),u+=4;for(var S=0;S<v;S++)w(i,u+3*S,_.itree[1+(_.ordr[S]<<1)]);u+=3*v,u=n.F._codeTiny(E,_.itree,i,u),u=n.F._codeTiny(I,_.itree,i,u)}for(var b=h,F=0;F<s;F+=2){for(var $=a[F],G=$>>>23,Y=b+(8388607&$);b<Y;)u=n.F._writeLit(f[b++],L,i,u);if(G!=0){var B=a[F+1],K=B>>16,j=B>>8&255,H=255&B;w(i,u=n.F._writeLit(257+j,L,i,u),G-_.of0[j]),u+=_.exb[j],M(i,u=n.F._writeLit(H,U,i,u),K-_.df0[H]),u+=_.dxb[H],b+=G}}u=n.F._writeLit(256,L,i,u)}return u},n.F._copyExact=function(o,a,s,l,f){var h=f>>>3;return l[h]=s,l[h+1]=s>>>8,l[h+2]=255-l[h],l[h+3]=255-l[h+1],h+=4,l.set(new Uint8Array(o.buffer,a,s),h),f+(s+4<<3)},n.F.getTrees=function(){for(var o=n.F.U,a=n.F._hufTree(o.lhst,o.ltree,15),s=n.F._hufTree(o.dhst,o.dtree,15),l=[],f=n.F._lenCodes(o.ltree,l),h=[],c=n.F._lenCodes(o.dtree,h),i=0;i<l.length;i+=2)o.ihst[l[i]]++;for(i=0;i<h.length;i+=2)o.ihst[h[i]]++;for(var u=n.F._hufTree(o.ihst,o.itree,7),g=19;g>4&&o.itree[1+(o.ordr[g-1]<<1)]==0;)g--;return[a,s,u,f,c,g,l,h]},n.F.getSecond=function(o){for(var a=[],s=0;s<o.length;s+=2)a.push(o[s+1]);return a},n.F.nonZero=function(o){for(var a="",s=0;s<o.length;s+=2)o[s+1]!=0&&(a+=(s>>1)+",");return a},n.F.contSize=function(o,a){for(var s=0,l=0;l<a.length;l++)s+=a[l]*o[1+(l<<1)];return s},n.F._codeTiny=function(o,a,s,l){for(var f=0;f<o.length;f+=2){var h=o[f],c=o[f+1];l=n.F._writeLit(h,a,s,l);var i=h==16?2:h==17?3:7;h>15&&(n.F._putsE(s,l,c,i),l+=i)}return l},n.F._lenCodes=function(o,a){for(var s=o.length;s!=2&&o[s-1]==0;)s-=2;for(var l=0;l<s;l+=2){var f=o[l+1],h=l+3<s?o[l+3]:-1,c=l+5<s?o[l+5]:-1,i=l==0?-1:o[l-1];if(f==0&&h==f&&c==f){for(var u=l+5;u+2<s&&o[u+2]==f;)u+=2;(g=Math.min(u+1-l>>>1,138))<11?a.push(17,g-3):a.push(18,g-11),l+=2*g-2}else if(f==i&&h==f&&c==f){for(u=l+5;u+2<s&&o[u+2]==f;)u+=2;var g=Math.min(u+1-l>>>1,6);a.push(16,g-3),l+=2*g-2}else a.push(f,0)}return s>>>1},n.F._hufTree=function(o,a,s){var l=[],f=o.length,h=a.length,c=0;for(c=0;c<h;c+=2)a[c]=0,a[c+1]=0;for(c=0;c<f;c++)o[c]!=0&&l.push({lit:c,f:o[c]});var i=l.length,u=l.slice(0);if(i==0)return 0;if(i==1){var g=l[0].lit;return u=g==0?1:0,a[1+(g<<1)]=1,a[1+(u<<1)]=1,1}l.sort((function(E,I){return E.f-I.f}));var p=l[0],m=l[1],x=0,C=1,O=2;for(l[0]={lit:-1,f:p.f+m.f,l:p,r:m,d:0};C!=i-1;)p=x!=C&&(O==i||l[x].f<l[O].f)?l[x++]:l[O++],m=x!=C&&(O==i||l[x].f<l[O].f)?l[x++]:l[O++],l[C++]={lit:-1,f:p.f+m.f,l:p,r:m};var v=n.F.setDepth(l[C-1],0);for(v>s&&(n.F.restrictDepth(u,s,v),v=s),c=0;c<i;c++)a[1+(u[c].lit<<1)]=u[c].d;return v},n.F.setDepth=function(o,a){return o.lit!=-1?(o.d=a,a):Math.max(n.F.setDepth(o.l,a+1),n.F.setDepth(o.r,a+1))},n.F.restrictDepth=function(o,a,s){var l=0,f=1<<s-a,h=0;for(o.sort((function(i,u){return u.d==i.d?i.f-u.f:u.d-i.d})),l=0;l<o.length&&o[l].d>a;l++){var c=o[l].d;o[l].d=a,h+=f-(1<<s-c)}for(h>>>=s-a;h>0;)(c=o[l].d)<a?(o[l].d++,h-=1<<a-c-1):l++;for(;l>=0;l--)o[l].d==a&&h<0&&(o[l].d--,h++);h!=0&&console.log("debt left")},n.F._goodIndex=function(o,a){var s=0;return a[16|s]<=o&&(s|=16),a[8|s]<=o&&(s|=8),a[4|s]<=o&&(s|=4),a[2|s]<=o&&(s|=2),a[1|s]<=o&&(s|=1),s},n.F._writeLit=function(o,a,s,l){return n.F._putsF(s,l,a[o<<1]),l+a[1+(o<<1)]},n.F.inflate=function(o,a){var s=Uint8Array;if(o[0]==3&&o[1]==0)return a||new s(0);var l=n.F,f=l._bitsF,h=l._bitsE,c=l._decodeTiny,i=l.makeCodes,u=l.codes2map,g=l._get17,p=l.U,m=a==null;m&&(a=new s(o.length>>>2<<3));for(var x,C,O=0,v=0,E=0,I=0,_=0,M=0,w=0,y=0,T=0;O==0;)if(O=f(o,T,1),v=f(o,T+1,2),T+=3,v!=0){if(m&&(a=n.F._check(a,y+131072)),v==1&&(x=p.flmap,C=p.fdmap,M=511,w=31),v==2){E=h(o,T,5)+257,I=h(o,T+5,5)+1,_=h(o,T+10,4)+4,T+=14;for(var A=0;A<38;A+=2)p.itree[A]=0,p.itree[A+1]=0;var P=1;for(A=0;A<_;A++){var D=h(o,T+3*A,3);p.itree[1+(p.ordr[A]<<1)]=D,D>P&&(P=D)}T+=3*_,i(p.itree,P),u(p.itree,P,p.imap),x=p.lmap,C=p.dmap,T=c(p.imap,(1<<P)-1,E+I,o,T,p.ttree);var L=l._copyOut(p.ttree,0,E,p.ltree);M=(1<<L)-1;var U=l._copyOut(p.ttree,E,I,p.dtree);w=(1<<U)-1,i(p.ltree,L),u(p.ltree,L,x),i(p.dtree,U),u(p.dtree,U,C)}for(;;){var S=x[g(o,T)&M];T+=15&S;var b=S>>>4;if(!(b>>>8))a[y++]=b;else{if(b==256)break;var F=y+b-254;if(b>264){var $=p.ldef[b-257];F=y+($>>>3)+h(o,T,7&$),T+=7&$}var G=C[g(o,T)&w];T+=15&G;var Y=G>>>4,B=p.ddef[Y],K=(B>>>4)+f(o,T,15&B);for(T+=15&B,m&&(a=n.F._check(a,y+131072));y<F;)a[y]=a[y++-K],a[y]=a[y++-K],a[y]=a[y++-K],a[y]=a[y++-K];y=F}}}else{(7&T)!=0&&(T+=8-(7&T));var j=4+(T>>>3),H=o[j-4]|o[j-3]<<8;m&&(a=n.F._check(a,y+H)),a.set(new s(o.buffer,o.byteOffset+j,H),y),T=j+H<<3,y+=H}return a.length==y?a:a.slice(0,y)},n.F._check=function(o,a){var s=o.length;if(a<=s)return o;var l=new Uint8Array(Math.max(s<<1,a));return l.set(o,0),l},n.F._decodeTiny=function(o,a,s,l,f,h){for(var c=n.F._bitsE,i=n.F._get17,u=0;u<s;){var g=o[i(l,f)&a];f+=15&g;var p=g>>>4;if(p<=15)h[u]=p,u++;else{var m=0,x=0;p==16?(x=3+c(l,f,2),f+=2,m=h[u-1]):p==17?(x=3+c(l,f,3),f+=3):p==18&&(x=11+c(l,f,7),f+=7);for(var C=u+x;u<C;)h[u]=m,u++}}return f},n.F._copyOut=function(o,a,s,l){for(var f=0,h=0,c=l.length>>>1;h<s;){var i=o[h+a];l[h<<1]=0,l[1+(h<<1)]=i,i>f&&(f=i),h++}for(;h<c;)l[h<<1]=0,l[1+(h<<1)]=0,h++;return f},n.F.makeCodes=function(o,a){for(var s,l,f,h,c=n.F.U,i=o.length,u=c.bl_count,g=0;g<=a;g++)u[g]=0;for(g=1;g<i;g+=2)u[o[g]]++;var p=c.next_code;for(s=0,u[0]=0,l=1;l<=a;l++)s=s+u[l-1]<<1,p[l]=s;for(f=0;f<i;f+=2)(h=o[f+1])!=0&&(o[f]=p[h],p[h]++)},n.F.codes2map=function(o,a,s){for(var l=o.length,f=n.F.U.rev15,h=0;h<l;h+=2)if(o[h+1]!=0)for(var c=h>>1,i=o[h+1],u=c<<4|i,g=a-i,p=o[h]<<g,m=p+(1<<g);p!=m;)s[f[p]>>>15-a]=u,p++},n.F.revCodes=function(o,a){for(var s=n.F.U.rev15,l=15-a,f=0;f<o.length;f+=2){var h=o[f]<<a-o[f+1];o[f]=s[h]>>>l}},n.F._putsE=function(o,a,s){s<<=7&a;var l=a>>>3;o[l]|=s,o[l+1]|=s>>>8},n.F._putsF=function(o,a,s){s<<=7&a;var l=a>>>3;o[l]|=s,o[l+1]|=s>>>8,o[l+2]|=s>>>16},n.F._bitsE=function(o,a,s){return(o[a>>>3]|o[1+(a>>>3)]<<8)>>>(7&a)&(1<<s)-1},n.F._bitsF=function(o,a,s){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16)>>>(7&a)&(1<<s)-1},n.F._get17=function(o,a){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16)>>>(7&a)},n.F._get25=function(o,a){return(o[a>>>3]|o[1+(a>>>3)]<<8|o[2+(a>>>3)]<<16|o[3+(a>>>3)]<<24)>>>(7&a)},n.F.U=(t=Uint16Array,r=Uint32Array,{next_code:new t(16),bl_count:new t(16),ordr:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],of0:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],exb:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],ldef:new t(32),df0:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],dxb:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],ddef:new r(32),flmap:new t(512),fltree:[],fdmap:new t(32),fdtree:[],lmap:new t(32768),ltree:[],ttree:[],dmap:new t(32768),dtree:[],imap:new t(512),itree:[],rev15:new t(32768),lhst:new r(286),dhst:new r(30),ihst:new r(19),lits:new r(15e3),strt:new t(65536),prev:new t(32768)}),(function(){for(var o=n.F.U,a=0;a<32768;a++){var s=a;s=(4278255360&(s=(4042322160&(s=(3435973836&(s=(2863311530&s)>>>1|(1431655765&s)<<1))>>>2|(858993459&s)<<2))>>>4|(252645135&s)<<4))>>>8|(16711935&s)<<8,o.rev15[a]=(s>>>16|s<<16)>>>17}function l(f,h,c){for(;h--!=0;)f.push(0,c)}for(a=0;a<32;a++)o.ldef[a]=o.of0[a]<<3|o.exb[a],o.ddef[a]=o.df0[a]<<4|o.dxb[a];l(o.fltree,144,8),l(o.fltree,112,9),l(o.fltree,24,7),l(o.fltree,8,8),n.F.makeCodes(o.fltree,9),n.F.codes2map(o.fltree,9,o.flmap),n.F.revCodes(o.fltree,9),l(o.fdtree,32,5),n.F.makeCodes(o.fdtree,5),n.F.codes2map(o.fdtree,5,o.fdmap),n.F.revCodes(o.fdtree,5),l(o.itree,19,0),l(o.ltree,286,0),l(o.dtree,30,0),l(o.ttree,320,0)})()})();var sg=ng({__proto__:null,default:On},[On]);const Ht=(function(){var e={nextZero(c,i){for(;c[i]!=0;)i++;return i},readUshort:(c,i)=>c[i]<<8|c[i+1],writeUshort(c,i,u){c[i]=u>>8&255,c[i+1]=255&u},readUint:(c,i)=>16777216*c[i]+(c[i+1]<<16|c[i+2]<<8|c[i+3]),writeUint(c,i,u){c[i]=u>>24&255,c[i+1]=u>>16&255,c[i+2]=u>>8&255,c[i+3]=255&u},readASCII(c,i,u){let g="";for(let p=0;p<u;p++)g+=String.fromCharCode(c[i+p]);return g},writeASCII(c,i,u){for(let g=0;g<u.length;g++)c[i+g]=u.charCodeAt(g)},readBytes(c,i,u){const g=[];for(let p=0;p<u;p++)g.push(c[i+p]);return g},pad:c=>c.length<2?`0${c}`:c,readUTF8(c,i,u){let g,p="";for(let m=0;m<u;m++)p+=`%${e.pad(c[i+m].toString(16))}`;try{g=decodeURIComponent(p)}catch{return e.readASCII(c,i,u)}return g}};function t(c,i,u,g){const p=i*u,m=a(g),x=Math.ceil(i*m/8),C=new Uint8Array(4*p),O=new Uint32Array(C.buffer),{ctype:v}=g,{depth:E}=g,I=e.readUshort;if(v==6){const $=p<<2;if(E==8)for(var _=0;_<$;_+=4)C[_]=c[_],C[_+1]=c[_+1],C[_+2]=c[_+2],C[_+3]=c[_+3];if(E==16)for(_=0;_<$;_++)C[_]=c[_<<1]}else if(v==2){const $=g.tabs.tRNS;if($==null){if(E==8)for(_=0;_<p;_++){var M=3*_;O[_]=255<<24|c[M+2]<<16|c[M+1]<<8|c[M]}if(E==16)for(_=0;_<p;_++)M=6*_,O[_]=255<<24|c[M+4]<<16|c[M+2]<<8|c[M]}else{var w=$[0];const G=$[1],Y=$[2];if(E==8)for(_=0;_<p;_++){var y=_<<2;M=3*_,O[_]=255<<24|c[M+2]<<16|c[M+1]<<8|c[M],c[M]==w&&c[M+1]==G&&c[M+2]==Y&&(C[y+3]=0)}if(E==16)for(_=0;_<p;_++)y=_<<2,M=6*_,O[_]=255<<24|c[M+4]<<16|c[M+2]<<8|c[M],I(c,M)==w&&I(c,M+2)==G&&I(c,M+4)==Y&&(C[y+3]=0)}}else if(v==3){const $=g.tabs.PLTE,G=g.tabs.tRNS,Y=G?G.length:0;if(E==1)for(var T=0;T<u;T++){var A=T*x,P=T*i;for(_=0;_<i;_++){y=P+_<<2;var D=3*(L=c[A+(_>>3)]>>7-((7&_)<<0)&1);C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255}}if(E==2)for(T=0;T<u;T++)for(A=T*x,P=T*i,_=0;_<i;_++)y=P+_<<2,D=3*(L=c[A+(_>>2)]>>6-((3&_)<<1)&3),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255;if(E==4)for(T=0;T<u;T++)for(A=T*x,P=T*i,_=0;_<i;_++)y=P+_<<2,D=3*(L=c[A+(_>>1)]>>4-((1&_)<<2)&15),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255;if(E==8)for(_=0;_<p;_++){var L;y=_<<2,D=3*(L=c[_]),C[y]=$[D],C[y+1]=$[D+1],C[y+2]=$[D+2],C[y+3]=L<Y?G[L]:255}}else if(v==4){if(E==8)for(_=0;_<p;_++){y=_<<2;var U=c[S=_<<1];C[y]=U,C[y+1]=U,C[y+2]=U,C[y+3]=c[S+1]}if(E==16)for(_=0;_<p;_++){var S;y=_<<2,U=c[S=_<<2],C[y]=U,C[y+1]=U,C[y+2]=U,C[y+3]=c[S+2]}}else if(v==0)for(w=g.tabs.tRNS?g.tabs.tRNS:-1,T=0;T<u;T++){const $=T*x,G=T*i;if(E==1)for(var b=0;b<i;b++){var F=(U=255*(c[$+(b>>>3)]>>>7-(7&b)&1))==255*w?0:255;O[G+b]=F<<24|U<<16|U<<8|U}else if(E==2)for(b=0;b<i;b++)F=(U=85*(c[$+(b>>>2)]>>>6-((3&b)<<1)&3))==85*w?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==4)for(b=0;b<i;b++)F=(U=17*(c[$+(b>>>1)]>>>4-((1&b)<<2)&15))==17*w?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==8)for(b=0;b<i;b++)F=(U=c[$+b])==w?0:255,O[G+b]=F<<24|U<<16|U<<8|U;else if(E==16)for(b=0;b<i;b++)U=c[$+(b<<1)],F=I(c,$+(b<<1))==w?0:255,O[G+b]=F<<24|U<<16|U<<8|U}return C}function r(c,i,u,g){const p=a(c),m=Math.ceil(u*p/8),x=new Uint8Array((m+1+c.interlace)*g);return i=c.tabs.CgBI?o(i,x):n(i,x),c.interlace==0?i=s(i,c,0,u,g):c.interlace==1&&(i=(function(O,v){const E=v.width,I=v.height,_=a(v),M=_>>3,w=Math.ceil(E*_/8),y=new Uint8Array(I*w);let T=0;const A=[0,0,4,0,2,0,1],P=[0,4,0,2,0,1,0],D=[8,8,8,4,4,2,2],L=[8,8,4,4,2,2,1];let U=0;for(;U<7;){const b=D[U],F=L[U];let $=0,G=0,Y=A[U];for(;Y<I;)Y+=b,G++;let B=P[U];for(;B<E;)B+=F,$++;const K=Math.ceil($*_/8);s(O,v,T,$,G);let j=0,H=A[U];for(;H<I;){let q=P[U],ue=T+j*K<<3;for(;q<E;){var S;if(_==1&&(S=(S=O[ue>>3])>>7-(7&ue)&1,y[H*w+(q>>3)]|=S<<7-((7&q)<<0)),_==2&&(S=(S=O[ue>>3])>>6-(7&ue)&3,y[H*w+(q>>2)]|=S<<6-((3&q)<<1)),_==4&&(S=(S=O[ue>>3])>>4-(7&ue)&15,y[H*w+(q>>1)]|=S<<4-((1&q)<<2)),_>=8){const te=H*w+q*M;for(let fe=0;fe<M;fe++)y[te+fe]=O[(ue>>3)+fe]}ue+=_,q+=F}j++,H+=b}$*G!=0&&(T+=G*(1+K)),U+=1}return y})(i,c)),i}function n(c,i){return o(new Uint8Array(c.buffer,2,c.length-6),i)}var o=(function(){const c={H:{}};return c.H.N=function(i,u){const g=Uint8Array;let p,m,x=0,C=0,O=0,v=0,E=0,I=0,_=0,M=0,w=0;if(i[0]==3&&i[1]==0)return u||new g(0);const y=c.H,T=y.b,A=y.e,P=y.R,D=y.n,L=y.A,U=y.Z,S=y.m,b=u==null;for(b&&(u=new g(i.length>>>2<<5));x==0;)if(x=T(i,w,1),C=T(i,w+1,2),w+=3,C!=0){if(b&&(u=c.H.W(u,M+(1<<17))),C==1&&(p=S.J,m=S.h,I=511,_=31),C==2){O=A(i,w,5)+257,v=A(i,w+5,5)+1,E=A(i,w+10,4)+4,w+=14;let $=1;for(var F=0;F<38;F+=2)S.Q[F]=0,S.Q[F+1]=0;for(F=0;F<E;F++){const B=A(i,w+3*F,3);S.Q[1+(S.X[F]<<1)]=B,B>$&&($=B)}w+=3*E,D(S.Q,$),L(S.Q,$,S.u),p=S.w,m=S.d,w=P(S.u,(1<<$)-1,O+v,i,w,S.v);const G=y.V(S.v,0,O,S.C);I=(1<<G)-1;const Y=y.V(S.v,O,v,S.D);_=(1<<Y)-1,D(S.C,G),L(S.C,G,p),D(S.D,Y),L(S.D,Y,m)}for(;;){const $=p[U(i,w)&I];w+=15&$;const G=$>>>4;if(!(G>>>8))u[M++]=G;else{if(G==256)break;{let Y=M+G-254;if(G>264){const q=S.q[G-257];Y=M+(q>>>3)+A(i,w,7&q),w+=7&q}const B=m[U(i,w)&_];w+=15&B;const K=B>>>4,j=S.c[K],H=(j>>>4)+T(i,w,15&j);for(w+=15&j;M<Y;)u[M]=u[M++-H],u[M]=u[M++-H],u[M]=u[M++-H],u[M]=u[M++-H];M=Y}}}}else{(7&w)!=0&&(w+=8-(7&w));const $=4+(w>>>3),G=i[$-4]|i[$-3]<<8;b&&(u=c.H.W(u,M+G)),u.set(new g(i.buffer,i.byteOffset+$,G),M),w=$+G<<3,M+=G}return u.length==M?u:u.slice(0,M)},c.H.W=function(i,u){const g=i.length;if(u<=g)return i;const p=new Uint8Array(g<<1);return p.set(i,0),p},c.H.R=function(i,u,g,p,m,x){const C=c.H.e,O=c.H.Z;let v=0;for(;v<g;){const E=i[O(p,m)&u];m+=15&E;const I=E>>>4;if(I<=15)x[v]=I,v++;else{let _=0,M=0;I==16?(M=3+C(p,m,2),m+=2,_=x[v-1]):I==17?(M=3+C(p,m,3),m+=3):I==18&&(M=11+C(p,m,7),m+=7);const w=v+M;for(;v<w;)x[v]=_,v++}}return m},c.H.V=function(i,u,g,p){let m=0,x=0;const C=p.length>>>1;for(;x<g;){const O=i[x+u];p[x<<1]=0,p[1+(x<<1)]=O,O>m&&(m=O),x++}for(;x<C;)p[x<<1]=0,p[1+(x<<1)]=0,x++;return m},c.H.n=function(i,u){const g=c.H.m,p=i.length;let m,x,C,O;const v=g.j;for(var E=0;E<=u;E++)v[E]=0;for(E=1;E<p;E+=2)v[i[E]]++;const I=g.K;for(m=0,v[0]=0,x=1;x<=u;x++)m=m+v[x-1]<<1,I[x]=m;for(C=0;C<p;C+=2)O=i[C+1],O!=0&&(i[C]=I[O],I[O]++)},c.H.A=function(i,u,g){const p=i.length,m=c.H.m.r;for(let x=0;x<p;x+=2)if(i[x+1]!=0){const C=x>>1,O=i[x+1],v=C<<4|O,E=u-O;let I=i[x]<<E;const _=I+(1<<E);for(;I!=_;)g[m[I]>>>15-u]=v,I++}},c.H.l=function(i,u){const g=c.H.m.r,p=15-u;for(let m=0;m<i.length;m+=2){const x=i[m]<<u-i[m+1];i[m]=g[x]>>>p}},c.H.M=function(i,u,g){g<<=7&u;const p=u>>>3;i[p]|=g,i[p+1]|=g>>>8},c.H.I=function(i,u,g){g<<=7&u;const p=u>>>3;i[p]|=g,i[p+1]|=g>>>8,i[p+2]|=g>>>16},c.H.e=function(i,u,g){return(i[u>>>3]|i[1+(u>>>3)]<<8)>>>(7&u)&(1<<g)-1},c.H.b=function(i,u,g){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16)>>>(7&u)&(1<<g)-1},c.H.Z=function(i,u){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16)>>>(7&u)},c.H.i=function(i,u){return(i[u>>>3]|i[1+(u>>>3)]<<8|i[2+(u>>>3)]<<16|i[3+(u>>>3)]<<24)>>>(7&u)},c.H.m=(function(){const i=Uint16Array,u=Uint32Array;return{K:new i(16),j:new i(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new i(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new u(32),J:new i(512),_:[],h:new i(32),$:[],w:new i(32768),C:[],v:[],d:new i(32768),D:[],u:new i(512),Q:[],r:new i(32768),s:new u(286),Y:new u(30),a:new u(19),t:new u(15e3),k:new i(65536),g:new i(32768)}})(),(function(){const i=c.H.m;for(var u=0;u<32768;u++){let p=u;p=(2863311530&p)>>>1|(1431655765&p)<<1,p=(3435973836&p)>>>2|(858993459&p)<<2,p=(4042322160&p)>>>4|(252645135&p)<<4,p=(4278255360&p)>>>8|(16711935&p)<<8,i.r[u]=(p>>>16|p<<16)>>>17}function g(p,m,x){for(;m--!=0;)p.push(0,x)}for(u=0;u<32;u++)i.q[u]=i.S[u]<<3|i.T[u],i.c[u]=i.p[u]<<4|i.z[u];g(i._,144,8),g(i._,112,9),g(i._,24,7),g(i._,8,8),c.H.n(i._,9),c.H.A(i._,9,i.J),c.H.l(i._,9),g(i.$,32,5),c.H.n(i.$,5),c.H.A(i.$,5,i.h),c.H.l(i.$,5),g(i.Q,19,0),g(i.C,286,0),g(i.D,30,0),g(i.v,320,0)})(),c.H.N})();function a(c){return[1,null,3,1,2,null,4][c.ctype]*c.depth}function s(c,i,u,g,p){let m=a(i);const x=Math.ceil(g*m/8);let C,O;m=Math.ceil(m/8);let v=c[u],E=0;if(v>1&&(c[u]=[0,0,1][v-2]),v==3)for(E=m;E<x;E++)c[E+1]=c[E+1]+(c[E+1-m]>>>1)&255;for(let I=0;I<p;I++)if(C=u+I*x,O=C+I+1,v=c[O-1],E=0,v==0)for(;E<x;E++)c[C+E]=c[O+E];else if(v==1){for(;E<m;E++)c[C+E]=c[O+E];for(;E<x;E++)c[C+E]=c[O+E]+c[C+E-m]}else if(v==2)for(;E<x;E++)c[C+E]=c[O+E]+c[C+E-x];else if(v==3){for(;E<m;E++)c[C+E]=c[O+E]+(c[C+E-x]>>>1);for(;E<x;E++)c[C+E]=c[O+E]+(c[C+E-x]+c[C+E-m]>>>1)}else{for(;E<m;E++)c[C+E]=c[O+E]+l(0,c[C+E-x],0);for(;E<x;E++)c[C+E]=c[O+E]+l(c[C+E-m],c[C+E-x],c[C+E-m-x])}return c}function l(c,i,u){const g=c+i-u,p=g-c,m=g-i,x=g-u;return p*p<=m*m&&p*p<=x*x?c:m*m<=x*x?i:u}function f(c,i,u){u.width=e.readUint(c,i),i+=4,u.height=e.readUint(c,i),i+=4,u.depth=c[i],i++,u.ctype=c[i],i++,u.compress=c[i],i++,u.filter=c[i],i++,u.interlace=c[i],i++}function h(c,i,u,g,p,m,x,C,O){const v=Math.min(i,p),E=Math.min(u,m);let I=0,_=0;for(let U=0;U<E;U++)for(let S=0;S<v;S++)if(x>=0&&C>=0?(I=U*i+S<<2,_=(C+U)*p+x+S<<2):(I=(-C+U)*i-x+S<<2,_=U*p+S<<2),O==0)g[_]=c[I],g[_+1]=c[I+1],g[_+2]=c[I+2],g[_+3]=c[I+3];else if(O==1){var M=c[I+3]*.00392156862745098,w=c[I]*M,y=c[I+1]*M,T=c[I+2]*M,A=g[_+3]*(1/255),P=g[_]*A,D=g[_+1]*A,L=g[_+2]*A;const b=1-M,F=M+A*b,$=F==0?0:1/F;g[_+3]=255*F,g[_+0]=(w+P*b)*$,g[_+1]=(y+D*b)*$,g[_+2]=(T+L*b)*$}else if(O==2)M=c[I+3],w=c[I],y=c[I+1],T=c[I+2],A=g[_+3],P=g[_],D=g[_+1],L=g[_+2],M==A&&w==P&&y==D&&T==L?(g[_]=0,g[_+1]=0,g[_+2]=0,g[_+3]=0):(g[_]=w,g[_+1]=y,g[_+2]=T,g[_+3]=M);else if(O==3){if(M=c[I+3],w=c[I],y=c[I+1],T=c[I+2],A=g[_+3],P=g[_],D=g[_+1],L=g[_+2],M==A&&w==P&&y==D&&T==L)continue;if(M<220&&A>20)return!1}return!0}return{decode:function(i){const u=new Uint8Array(i);let g=8;const p=e,m=p.readUshort,x=p.readUint,C={tabs:{},frames:[]},O=new Uint8Array(u.length);let v,E=0,I=0;const _=[137,80,78,71,13,10,26,10];for(var M=0;M<8;M++)if(u[M]!=_[M])throw"The input is not a PNG file!";for(;g<u.length;){const U=p.readUint(u,g);g+=4;const S=p.readASCII(u,g,4);if(g+=4,S=="IHDR")f(u,g,C);else if(S=="iCCP"){for(var w=g;u[w]!=0;)w++;p.readASCII(u,g,w-g),u[w+1];const b=u.slice(w+2,g+U);let F=null;try{F=n(b)}catch{F=o(b)}C.tabs[S]=F}else if(S=="CgBI")C.tabs[S]=u.slice(g,g+4);else if(S=="IDAT"){for(M=0;M<U;M++)O[E+M]=u[g+M];E+=U}else if(S=="acTL")C.tabs[S]={num_frames:x(u,g),num_plays:x(u,g+4)},v=new Uint8Array(u.length);else if(S=="fcTL"){I!=0&&((L=C.frames[C.frames.length-1]).data=r(C,v.slice(0,I),L.rect.width,L.rect.height),I=0);const b={x:x(u,g+12),y:x(u,g+16),width:x(u,g+4),height:x(u,g+8)};let F=m(u,g+22);F=m(u,g+20)/(F==0?100:F);const $={rect:b,delay:Math.round(1e3*F),dispose:u[g+24],blend:u[g+25]};C.frames.push($)}else if(S=="fdAT"){for(M=0;M<U-4;M++)v[I+M]=u[g+M+4];I+=U-4}else if(S=="pHYs")C.tabs[S]=[p.readUint(u,g),p.readUint(u,g+4),u[g+8]];else if(S=="cHRM")for(C.tabs[S]=[],M=0;M<8;M++)C.tabs[S].push(p.readUint(u,g+4*M));else if(S=="tEXt"||S=="zTXt"){C.tabs[S]==null&&(C.tabs[S]={});var y=p.nextZero(u,g),T=p.readASCII(u,g,y-g),A=g+U-y-1;if(S=="tEXt")D=p.readASCII(u,y+1,A);else{var P=n(u.slice(y+2,y+2+A));D=p.readUTF8(P,0,P.length)}C.tabs[S][T]=D}else if(S=="iTXt"){C.tabs[S]==null&&(C.tabs[S]={}),y=0,w=g,y=p.nextZero(u,w),T=p.readASCII(u,w,y-w);const b=u[w=y+1];var D;u[w+1],w+=2,y=p.nextZero(u,w),p.readASCII(u,w,y-w),w=y+1,y=p.nextZero(u,w),p.readUTF8(u,w,y-w),A=U-((w=y+1)-g),b==0?D=p.readUTF8(u,w,A):(P=n(u.slice(w,w+A)),D=p.readUTF8(P,0,P.length)),C.tabs[S][T]=D}else if(S=="PLTE")C.tabs[S]=p.readBytes(u,g,U);else if(S=="hIST"){const b=C.tabs.PLTE.length/3;for(C.tabs[S]=[],M=0;M<b;M++)C.tabs[S].push(m(u,g+2*M))}else if(S=="tRNS")C.ctype==3?C.tabs[S]=p.readBytes(u,g,U):C.ctype==0?C.tabs[S]=m(u,g):C.ctype==2&&(C.tabs[S]=[m(u,g),m(u,g+2),m(u,g+4)]);else if(S=="gAMA")C.tabs[S]=p.readUint(u,g)/1e5;else if(S=="sRGB")C.tabs[S]=u[g];else if(S=="bKGD")C.ctype==0||C.ctype==4?C.tabs[S]=[m(u,g)]:C.ctype==2||C.ctype==6?C.tabs[S]=[m(u,g),m(u,g+2),m(u,g+4)]:C.ctype==3&&(C.tabs[S]=u[g]);else if(S=="IEND")break;g+=U,p.readUint(u,g),g+=4}var L;return I!=0&&((L=C.frames[C.frames.length-1]).data=r(C,v.slice(0,I),L.rect.width,L.rect.height)),C.data=r(C,O,C.width,C.height),delete C.compress,delete C.interlace,delete C.filter,C},toRGBA8:function(i){const u=i.width,g=i.height;if(i.tabs.acTL==null)return[t(i.data,u,g,i).buffer];const p=[];i.frames[0].data==null&&(i.frames[0].data=i.data);const m=u*g*4,x=new Uint8Array(m),C=new Uint8Array(m),O=new Uint8Array(m);for(let E=0;E<i.frames.length;E++){const I=i.frames[E],_=I.rect.x,M=I.rect.y,w=I.rect.width,y=I.rect.height,T=t(I.data,w,y,i);if(E!=0)for(var v=0;v<m;v++)O[v]=x[v];if(I.blend==0?h(T,w,y,x,u,g,_,M,0):I.blend==1&&h(T,w,y,x,u,g,_,M,1),p.push(x.buffer.slice(0)),I.dispose!=0){if(I.dispose==1)h(C,w,y,x,u,g,_,M,0);else if(I.dispose==2)for(v=0;v<m;v++)x[v]=O[v]}}return p},_paeth:l,_copyTile:h,_bin:e}})();(function(){const{_copyTile:e}=Ht,{_bin:t}=Ht,r=Ht._paeth;var n={table:(function(){const w=new Uint32Array(256);for(let y=0;y<256;y++){let T=y;for(let A=0;A<8;A++)1&T?T=3988292384^T>>>1:T>>>=1;w[y]=T}return w})(),update(w,y,T,A){for(let P=0;P<A;P++)w=n.table[255&(w^y[T+P])]^w>>>8;return w},crc:(w,y,T)=>4294967295^n.update(4294967295,w,y,T)};function o(w,y,T,A){y[T]+=w[0]*A>>4,y[T+1]+=w[1]*A>>4,y[T+2]+=w[2]*A>>4,y[T+3]+=w[3]*A>>4}function a(w){return Math.max(0,Math.min(255,w))}function s(w,y){const T=w[0]-y[0],A=w[1]-y[1],P=w[2]-y[2],D=w[3]-y[3];return T*T+A*A+P*P+D*D}function l(w,y,T,A,P,D,L){L==null&&(L=1);const U=A.length,S=[];for(var b=0;b<U;b++){const H=A[b];S.push([H>>>0&255,H>>>8&255,H>>>16&255,H>>>24&255])}for(b=0;b<U;b++){let H=4294967295;for(var F=0,$=0;$<U;$++){var G=s(S[b],S[$]);$!=b&&G<H&&(H=G,F=$)}}const Y=new Uint32Array(P.buffer),B=new Int16Array(y*T*4),K=[0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5];for(b=0;b<K.length;b++)K[b]=255*((K[b]+.5)/16-.5);for(let H=0;H<T;H++)for(let q=0;q<y;q++){var j;b=4*(H*y+q),L!=2?j=[a(w[b]+B[b]),a(w[b+1]+B[b+1]),a(w[b+2]+B[b+2]),a(w[b+3]+B[b+3])]:(G=K[4*(3&H)+(3&q)],j=[a(w[b]+G),a(w[b+1]+G),a(w[b+2]+G),a(w[b+3]+G)]),F=0;let ue=16777215;for($=0;$<U;$++){const ee=s(j,S[$]);ee<ue&&(ue=ee,F=$)}const te=S[F],fe=[j[0]-te[0],j[1]-te[1],j[2]-te[2],j[3]-te[3]];L==1&&(q!=y-1&&o(fe,B,b+4,7),H!=T-1&&(q!=0&&o(fe,B,b+4*y-4,3),o(fe,B,b+4*y,5),q!=y-1&&o(fe,B,b+4*y+4,1))),D[b>>2]=F,Y[b>>2]=A[F]}}function f(w,y,T,A,P){P==null&&(P={});const{crc:D}=n,L=t.writeUint,U=t.writeUshort,S=t.writeASCII;let b=8;const F=w.frames.length>1;let $,G=!1,Y=33+(F?20:0);if(P.sRGB!=null&&(Y+=13),P.pHYs!=null&&(Y+=21),P.iCCP!=null&&($=pako.deflate(P.iCCP),Y+=21+$.length+4),w.ctype==3){for(var B=w.plte.length,K=0;K<B;K++)w.plte[K]>>>24!=255&&(G=!0);Y+=8+3*B+4+(G?8+1*B+4:0)}for(var j=0;j<w.frames.length;j++)F&&(Y+=38),Y+=(te=w.frames[j]).cimg.length+12,j!=0&&(Y+=4);Y+=12;const H=new Uint8Array(Y),q=[137,80,78,71,13,10,26,10];for(K=0;K<8;K++)H[K]=q[K];if(L(H,b,13),b+=4,S(H,b,"IHDR"),b+=4,L(H,b,y),b+=4,L(H,b,T),b+=4,H[b]=w.depth,b++,H[b]=w.ctype,b++,H[b]=0,b++,H[b]=0,b++,H[b]=0,b++,L(H,b,D(H,b-17,17)),b+=4,P.sRGB!=null&&(L(H,b,1),b+=4,S(H,b,"sRGB"),b+=4,H[b]=P.sRGB,b++,L(H,b,D(H,b-5,5)),b+=4),P.iCCP!=null){const fe=13+$.length;L(H,b,fe),b+=4,S(H,b,"iCCP"),b+=4,S(H,b,"ICC profile"),b+=11,b+=2,H.set($,b),b+=$.length,L(H,b,D(H,b-(fe+4),fe+4)),b+=4}if(P.pHYs!=null&&(L(H,b,9),b+=4,S(H,b,"pHYs"),b+=4,L(H,b,P.pHYs[0]),b+=4,L(H,b,P.pHYs[1]),b+=4,H[b]=P.pHYs[2],b++,L(H,b,D(H,b-13,13)),b+=4),F&&(L(H,b,8),b+=4,S(H,b,"acTL"),b+=4,L(H,b,w.frames.length),b+=4,L(H,b,P.loop!=null?P.loop:0),b+=4,L(H,b,D(H,b-12,12)),b+=4),w.ctype==3){for(L(H,b,3*(B=w.plte.length)),b+=4,S(H,b,"PLTE"),b+=4,K=0;K<B;K++){const fe=3*K,ee=w.plte[K],xe=255&ee,he=ee>>>8&255,Ge=ee>>>16&255;H[b+fe+0]=xe,H[b+fe+1]=he,H[b+fe+2]=Ge}if(b+=3*B,L(H,b,D(H,b-3*B-4,3*B+4)),b+=4,G){for(L(H,b,B),b+=4,S(H,b,"tRNS"),b+=4,K=0;K<B;K++)H[b+K]=w.plte[K]>>>24&255;b+=B,L(H,b,D(H,b-B-4,B+4)),b+=4}}let ue=0;for(j=0;j<w.frames.length;j++){var te=w.frames[j];F&&(L(H,b,26),b+=4,S(H,b,"fcTL"),b+=4,L(H,b,ue++),b+=4,L(H,b,te.rect.width),b+=4,L(H,b,te.rect.height),b+=4,L(H,b,te.rect.x),b+=4,L(H,b,te.rect.y),b+=4,U(H,b,A[j]),b+=2,U(H,b,1e3),b+=2,H[b]=te.dispose,b++,H[b]=te.blend,b++,L(H,b,D(H,b-30,30)),b+=4);const fe=te.cimg;L(H,b,(B=fe.length)+(j==0?0:4)),b+=4;const ee=b;S(H,b,j==0?"IDAT":"fdAT"),b+=4,j!=0&&(L(H,b,ue++),b+=4),H.set(fe,b),b+=B,L(H,b,D(H,ee,b-ee)),b+=4}return L(H,b,0),b+=4,S(H,b,"IEND"),b+=4,L(H,b,D(H,b-4,4)),b+=4,H.buffer}function h(w,y,T){for(let A=0;A<w.frames.length;A++){const P=w.frames[A];P.rect.width;const D=P.rect.height,L=new Uint8Array(D*P.bpl+D);P.cimg=g(P.img,D,P.bpp,P.bpl,L,y,T)}}function c(w,y,T,A,P){const D=P[0],L=P[1],U=P[2],S=P[3],b=P[4],F=P[5];let $=6,G=8,Y=255;for(var B=0;B<w.length;B++){const Me=new Uint8Array(w[B]);for(var K=Me.length,j=0;j<K;j+=4)Y&=Me[j+3]}const H=Y!=255,q=(function(Ce,we,Fe,Le,V,re){const oe=[];for(var ae=0;ae<Ce.length;ae++){const Oe=new Uint8Array(Ce[ae]),X=new Uint32Array(Oe.buffer);var be;let se=0,ie=0,le=we,ne=Fe,Te=Le?1:0;if(ae!=0){const ve=re||Le||ae==1||oe[ae-2].dispose!=0?1:2;let Se=0,_e=1e9;for(let Ue=0;Ue<ve;Ue++){var ke=new Uint8Array(Ce[ae-1-Ue]);const je=new Uint32Array(Ce[ae-1-Ue]);let Ee=we,pe=Fe,Ne=-1,Ke=-1;for(let Be=0;Be<Fe;Be++)for(let Ze=0;Ze<we;Ze++)X[Pe=Be*we+Ze]!=je[Pe]&&(Ze<Ee&&(Ee=Ze),Ze>Ne&&(Ne=Ze),Be<pe&&(pe=Be),Be>Ke&&(Ke=Be));Ne==-1&&(Ee=pe=Ne=Ke=0),V&&((1&Ee)==1&&Ee--,(1&pe)==1&&pe--);const $e=(Ne-Ee+1)*(Ke-pe+1);$e<_e&&(_e=$e,Se=Ue,se=Ee,ie=pe,le=Ne-Ee+1,ne=Ke-pe+1)}ke=new Uint8Array(Ce[ae-1-Se]),Se==1&&(oe[ae-1].dispose=2),be=new Uint8Array(le*ne*4),e(ke,we,Fe,be,le,ne,-se,-ie,0),Te=e(Oe,we,Fe,be,le,ne,-se,-ie,3)?1:0,Te==1?u(Oe,we,Fe,be,{x:se,y:ie,width:le,height:ne}):e(Oe,we,Fe,be,le,ne,-se,-ie,0)}else be=Oe.slice(0);oe.push({rect:{x:se,y:ie,width:le,height:ne},img:be,blend:Te,dispose:0})}if(Le)for(ae=0;ae<oe.length;ae++){if((Ve=oe[ae]).blend==1)continue;const Oe=Ve.rect,X=oe[ae-1].rect,se=Math.min(Oe.x,X.x),ie=Math.min(Oe.y,X.y),le={x:se,y:ie,width:Math.max(Oe.x+Oe.width,X.x+X.width)-se,height:Math.max(Oe.y+Oe.height,X.y+X.height)-ie};oe[ae-1].dispose=1,ae-1!=0&&i(Ce,we,Fe,oe,ae-1,le,V),i(Ce,we,Fe,oe,ae,le,V)}let He=0;if(Ce.length!=1)for(var Pe=0;Pe<oe.length;Pe++){var Ve;He+=(Ve=oe[Pe]).rect.width*Ve.rect.height}return oe})(w,y,T,D,L,U),ue={},te=[],fe=[];if(A!=0){const Me=[];for(j=0;j<q.length;j++)Me.push(q[j].img.buffer);const Ce=(function(V){let re=0;for(var oe=0;oe<V.length;oe++)re+=V[oe].byteLength;const ae=new Uint8Array(re);let be=0;for(oe=0;oe<V.length;oe++){const ke=new Uint8Array(V[oe]),He=ke.length;for(let Pe=0;Pe<He;Pe+=4){let Ve=ke[Pe],Oe=ke[Pe+1],X=ke[Pe+2];const se=ke[Pe+3];se==0&&(Ve=Oe=X=0),ae[be+Pe]=Ve,ae[be+Pe+1]=Oe,ae[be+Pe+2]=X,ae[be+Pe+3]=se}be+=He}return ae.buffer})(Me),we=m(Ce,A);for(j=0;j<we.plte.length;j++)te.push(we.plte[j].est.rgba);let Fe=0;for(j=0;j<q.length;j++){const Le=(xe=q[j]).img.length;var ee=new Uint8Array(we.inds.buffer,Fe>>2,Le>>2);fe.push(ee);const V=new Uint8Array(we.abuf,Fe,Le);F&&l(xe.img,xe.rect.width,xe.rect.height,te,V,ee),xe.img.set(V),Fe+=Le}}else for(B=0;B<q.length;B++){var xe=q[B];const Me=new Uint32Array(xe.img.buffer);var he=xe.rect.width;for(K=Me.length,ee=new Uint8Array(K),fe.push(ee),j=0;j<K;j++){const Ce=Me[j];if(j!=0&&Ce==Me[j-1])ee[j]=ee[j-1];else if(j>he&&Ce==Me[j-he])ee[j]=ee[j-he];else{let we=ue[Ce];if(we==null&&(ue[Ce]=we=te.length,te.push(Ce),te.length>=300))break;ee[j]=we}}}const Ge=te.length;for(Ge<=256&&b==0&&(G=Ge<=2?1:Ge<=4?2:Ge<=16?4:8,G=Math.max(G,S)),B=0;B<q.length;B++){(xe=q[B]).rect.x,xe.rect.y,he=xe.rect.width;const Me=xe.rect.height;let Ce=xe.img;new Uint32Array(Ce.buffer);let we=4*he,Fe=4;if(Ge<=256&&b==0){we=Math.ceil(G*he/8);var De=new Uint8Array(we*Me);const Le=fe[B];for(let V=0;V<Me;V++){j=V*we;const re=V*he;if(G==8)for(var ye=0;ye<he;ye++)De[j+ye]=Le[re+ye];else if(G==4)for(ye=0;ye<he;ye++)De[j+(ye>>1)]|=Le[re+ye]<<4-4*(1&ye);else if(G==2)for(ye=0;ye<he;ye++)De[j+(ye>>2)]|=Le[re+ye]<<6-2*(3&ye);else if(G==1)for(ye=0;ye<he;ye++)De[j+(ye>>3)]|=Le[re+ye]<<7-1*(7&ye)}Ce=De,$=3,Fe=1}else if(H==0&&q.length==1){De=new Uint8Array(he*Me*3);const Le=he*Me;for(j=0;j<Le;j++){const V=3*j,re=4*j;De[V]=Ce[re],De[V+1]=Ce[re+1],De[V+2]=Ce[re+2]}Ce=De,$=2,Fe=3,we=3*he}xe.img=Ce,xe.bpl=we,xe.bpp=Fe}return{ctype:$,depth:G,plte:te,frames:q}}function i(w,y,T,A,P,D,L){const U=Uint8Array,S=Uint32Array,b=new U(w[P-1]),F=new S(w[P-1]),$=P+1<w.length?new U(w[P+1]):null,G=new U(w[P]),Y=new S(G.buffer);let B=y,K=T,j=-1,H=-1;for(let ue=0;ue<D.height;ue++)for(let te=0;te<D.width;te++){const fe=D.x+te,ee=D.y+ue,xe=ee*y+fe,he=Y[xe];he==0||A[P-1].dispose==0&&F[xe]==he&&($==null||$[4*xe+3]!=0)||(fe<B&&(B=fe),fe>j&&(j=fe),ee<K&&(K=ee),ee>H&&(H=ee))}j==-1&&(B=K=j=H=0),L&&((1&B)==1&&B--,(1&K)==1&&K--),D={x:B,y:K,width:j-B+1,height:H-K+1};const q=A[P];q.rect=D,q.blend=1,q.img=new Uint8Array(D.width*D.height*4),A[P-1].dispose==0?(e(b,y,T,q.img,D.width,D.height,-D.x,-D.y,0),u(G,y,T,q.img,D)):e(G,y,T,q.img,D.width,D.height,-D.x,-D.y,0)}function u(w,y,T,A,P){e(w,y,T,A,P.width,P.height,-P.x,-P.y,2)}function g(w,y,T,A,P,D,L){const U=[];let S,b=[0,1,2,3,4];D!=-1?b=[D]:(y*A>5e5||T==1)&&(b=[0]),L&&(S={level:0});const F=sg;for(var $=0;$<b.length;$++){for(let B=0;B<y;B++)p(P,w,B,A,T,b[$]);U.push(F.deflate(P,S))}let G,Y=1e9;for($=0;$<U.length;$++)U[$].length<Y&&(G=$,Y=U[$].length);return U[G]}function p(w,y,T,A,P,D){const L=T*A;let U=L+T;if(w[U]=D,U++,D==0)if(A<500)for(var S=0;S<A;S++)w[U+S]=y[L+S];else w.set(new Uint8Array(y.buffer,L,A),U);else if(D==1){for(S=0;S<P;S++)w[U+S]=y[L+S];for(S=P;S<A;S++)w[U+S]=y[L+S]-y[L+S-P]+256&255}else if(T==0){for(S=0;S<P;S++)w[U+S]=y[L+S];if(D==2)for(S=P;S<A;S++)w[U+S]=y[L+S];if(D==3)for(S=P;S<A;S++)w[U+S]=y[L+S]-(y[L+S-P]>>1)+256&255;if(D==4)for(S=P;S<A;S++)w[U+S]=y[L+S]-r(y[L+S-P],0,0)+256&255}else{if(D==2)for(S=0;S<A;S++)w[U+S]=y[L+S]+256-y[L+S-A]&255;if(D==3){for(S=0;S<P;S++)w[U+S]=y[L+S]+256-(y[L+S-A]>>1)&255;for(S=P;S<A;S++)w[U+S]=y[L+S]+256-(y[L+S-A]+y[L+S-P]>>1)&255}if(D==4){for(S=0;S<P;S++)w[U+S]=y[L+S]+256-r(0,y[L+S-A],0)&255;for(S=P;S<A;S++)w[U+S]=y[L+S]+256-r(y[L+S-P],y[L+S-A],y[L+S-P-A])&255}}}function m(w,y){const T=new Uint8Array(w),A=T.slice(0),P=new Uint32Array(A.buffer),D=x(A,y),L=D[0],U=D[1],S=T.length,b=new Uint8Array(S>>2);let F;if(T.length<2e7)for(var $=0;$<S;$+=4)F=C(L,G=T[$]*.00392156862745098,Y=T[$+1]*.00392156862745098,B=T[$+2]*.00392156862745098,K=T[$+3]*.00392156862745098),b[$>>2]=F.ind,P[$>>2]=F.est.rgba;else for($=0;$<S;$+=4){var G=T[$]*.00392156862745098,Y=T[$+1]*(1/255),B=T[$+2]*(1/255),K=T[$+3]*(1/255);for(F=L;F.left;)F=O(F.est,G,Y,B,K)<=0?F.left:F.right;b[$>>2]=F.ind,P[$>>2]=F.est.rgba}return{abuf:A.buffer,inds:b,plte:U}}function x(w,y,T){T==null&&(T=1e-4);const A=new Uint32Array(w.buffer),P={i0:0,i1:w.length,bst:null,est:null,tdst:0,left:null,right:null};P.bst=I(w,P.i0,P.i1),P.est=_(P.bst);const D=[P];for(;D.length<y;){let U=0,S=0;for(var L=0;L<D.length;L++)D[L].est.L>U&&(U=D[L].est.L,S=L);if(U<T)break;const b=D[S],F=v(w,A,b.i0,b.i1,b.est.e,b.est.eMq255);if(b.i0>=F||b.i1<=F){b.est.L=0;continue}const $={i0:b.i0,i1:F,bst:null,est:null,tdst:0,left:null,right:null};$.bst=I(w,$.i0,$.i1),$.est=_($.bst);const G={i0:F,i1:b.i1,bst:null,est:null,tdst:0,left:null,right:null};for(G.bst={R:[],m:[],N:b.bst.N-$.bst.N},L=0;L<16;L++)G.bst.R[L]=b.bst.R[L]-$.bst.R[L];for(L=0;L<4;L++)G.bst.m[L]=b.bst.m[L]-$.bst.m[L];G.est=_(G.bst),b.left=$,b.right=G,D[S]=$,D.push(G)}for(D.sort(((U,S)=>S.bst.N-U.bst.N)),L=0;L<D.length;L++)D[L].ind=L;return[P,D]}function C(w,y,T,A,P){if(w.left==null)return w.tdst=(function($,G,Y,B,K){const j=G-$[0],H=Y-$[1],q=B-$[2],ue=K-$[3];return j*j+H*H+q*q+ue*ue})(w.est.q,y,T,A,P),w;const D=O(w.est,y,T,A,P);let L=w.left,U=w.right;D>0&&(L=w.right,U=w.left);const S=C(L,y,T,A,P);if(S.tdst<=D*D)return S;const b=C(U,y,T,A,P);return b.tdst<S.tdst?b:S}function O(w,y,T,A,P){const{e:D}=w;return D[0]*y+D[1]*T+D[2]*A+D[3]*P-w.eMq}function v(w,y,T,A,P,D){for(A-=4;T<A;){for(;E(w,T,P)<=D;)T+=4;for(;E(w,A,P)>D;)A-=4;if(T>=A)break;const L=y[T>>2];y[T>>2]=y[A>>2],y[A>>2]=L,T+=4,A-=4}for(;E(w,T,P)>D;)T-=4;return T+4}function E(w,y,T){return w[y]*T[0]+w[y+1]*T[1]+w[y+2]*T[2]+w[y+3]*T[3]}function I(w,y,T){const A=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],P=[0,0,0,0],D=T-y>>2;for(let L=y;L<T;L+=4){const U=w[L]*.00392156862745098,S=w[L+1]*(1/255),b=w[L+2]*(1/255),F=w[L+3]*(1/255);P[0]+=U,P[1]+=S,P[2]+=b,P[3]+=F,A[0]+=U*U,A[1]+=U*S,A[2]+=U*b,A[3]+=U*F,A[5]+=S*S,A[6]+=S*b,A[7]+=S*F,A[10]+=b*b,A[11]+=b*F,A[15]+=F*F}return A[4]=A[1],A[8]=A[2],A[9]=A[6],A[12]=A[3],A[13]=A[7],A[14]=A[11],{R:A,m:P,N:D}}function _(w){const{R:y}=w,{m:T}=w,{N:A}=w,P=T[0],D=T[1],L=T[2],U=T[3],S=A==0?0:1/A,b=[y[0]-P*P*S,y[1]-P*D*S,y[2]-P*L*S,y[3]-P*U*S,y[4]-D*P*S,y[5]-D*D*S,y[6]-D*L*S,y[7]-D*U*S,y[8]-L*P*S,y[9]-L*D*S,y[10]-L*L*S,y[11]-L*U*S,y[12]-U*P*S,y[13]-U*D*S,y[14]-U*L*S,y[15]-U*U*S],F=b,$=M;let G=[Math.random(),Math.random(),Math.random(),Math.random()],Y=0,B=0;if(A!=0)for(let j=0;j<16&&(G=$.multVec(F,G),B=Math.sqrt($.dot(G,G)),G=$.sml(1/B,G),!(j!=0&&Math.abs(B-Y)<1e-9));j++)Y=B;const K=[P*S,D*S,L*S,U*S];return{Cov:b,q:K,e:G,L:Y,eMq255:$.dot($.sml(255,K),G),eMq:$.dot(G,K),rgba:(Math.round(255*K[3])<<24|Math.round(255*K[2])<<16|Math.round(255*K[1])<<8|Math.round(255*K[0])<<0)>>>0}}var M={multVec:(w,y)=>[w[0]*y[0]+w[1]*y[1]+w[2]*y[2]+w[3]*y[3],w[4]*y[0]+w[5]*y[1]+w[6]*y[2]+w[7]*y[3],w[8]*y[0]+w[9]*y[1]+w[10]*y[2]+w[11]*y[3],w[12]*y[0]+w[13]*y[1]+w[14]*y[2]+w[15]*y[3]],dot:(w,y)=>w[0]*y[0]+w[1]*y[1]+w[2]*y[2]+w[3]*y[3],sml:(w,y)=>[w*y[0],w*y[1],w*y[2],w*y[3]]};Ht.encode=function(y,T,A,P,D,L,U){P==null&&(P=0),U==null&&(U=!1);const S=c(y,T,A,P,[!1,!1,!1,0,U,!1]);return h(S,-1),f(S,T,A,D,L)},Ht.encodeLL=function(y,T,A,P,D,L,U,S){const b={ctype:0+(P==1?0:2)+(D==0?0:4),depth:L,frames:[]},F=(P+D)*L,$=F*T;for(let G=0;G<y.length;G++)b.frames.push({rect:{x:0,y:0,width:T,height:A},img:new Uint8Array(y[G]),blend:0,dispose:1,bpp:Math.ceil(F/8),bpl:Math.ceil($/8)});return h(b,0,!0),f(b,T,A,U,S)},Ht.encode.compress=c,Ht.encode.dither=l,Ht.quantize=m,Ht.quantize.getKDtree=x,Ht.quantize.getNearest=C})();const zl={toArrayBuffer(e,t){const r=e.width,n=e.height,o=r<<2,a=e.getContext("2d").getImageData(0,0,r,n),s=new Uint32Array(a.data.buffer),l=(32*r+31)/32<<2,f=l*n,h=122+f,c=new ArrayBuffer(h),i=new DataView(c),u=1<<20;let g,p,m,x,C=u,O=0,v=0,E=0;function I(w){i.setUint16(v,w,!0),v+=2}function _(w){i.setUint32(v,w,!0),v+=4}function M(w){v+=w}I(19778),_(h),M(4),_(122),_(108),_(r),_(-n>>>0),I(1),I(32),_(3),_(f),_(2835),_(2835),M(8),_(16711680),_(65280),_(255),_(4278190080),_(1466527264),(function w(){for(;O<n&&C>0;){for(x=122+O*l,g=0;g<o;)C--,p=s[E++],m=p>>>24,i.setUint32(x+g,p<<8|m),g+=4;O++}E<s.length?(C=u,setTimeout(w,zl._dly)):t(c)})()},toBlob(e,t){this.toArrayBuffer(e,(r=>{t(new Blob([r],{type:"image/bmp"}))}))},_dly:9};var bt={CHROME:"CHROME",FIREFOX:"FIREFOX",DESKTOP_SAFARI:"DESKTOP_SAFARI",IE:"IE",IOS:"IOS",ETC:"ETC"},lg={[bt.CHROME]:16384,[bt.FIREFOX]:11180,[bt.DESKTOP_SAFARI]:16384,[bt.IE]:8192,[bt.IOS]:4096,[bt.ETC]:8192};const na=typeof window<"u",Hl=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,In=na&&window.cordova&&window.cordova.require&&window.cordova.require("cordova/modulemapper"),ig=(na||Hl)&&(In&&In.getOriginalSymbol(window,"File")||typeof File<"u"&&File),Gl=(na||Hl)&&(In&&In.getOriginalSymbol(window,"FileReader")||typeof FileReader<"u"&&FileReader);function oa(e,t,r=Date.now()){return new Promise((n=>{const o=e.split(","),a=o[0].match(/:(.*?);/)[1],s=globalThis.atob(o[1]);let l=s.length;const f=new Uint8Array(l);for(;l--;)f[l]=s.charCodeAt(l);const h=new Blob([f],{type:a});h.name=t,h.lastModified=r,n(h)}))}function jl(e){return new Promise(((t,r)=>{const n=new Gl;n.onload=()=>t(n.result),n.onerror=o=>r(o),n.readAsDataURL(e)}))}function Bl(e){return new Promise(((t,r)=>{const n=new Image;n.onload=()=>t(n),n.onerror=o=>r(o),n.src=e}))}function fr(){if(fr.cachedResult!==void 0)return fr.cachedResult;let e=bt.ETC;const{userAgent:t}=navigator;return/Chrom(e|ium)/i.test(t)?e=bt.CHROME:/iP(ad|od|hone)/i.test(t)&&/WebKit/i.test(t)?e=bt.IOS:/Safari/i.test(t)?e=bt.DESKTOP_SAFARI:/Firefox/i.test(t)?e=bt.FIREFOX:(/MSIE/i.test(t)||document.documentMode)&&(e=bt.IE),fr.cachedResult=e,fr.cachedResult}function Wl(e,t){const r=fr(),n=lg[r];let o=e,a=t,s=o*a;const l=o>a?a/o:o/a;for(;s>n*n;){const f=(n+o)/2,h=(n+a)/2;f<h?(a=h,o=h*l):(a=f*l,o=f),s=o*a}return{width:o,height:a}}function Dn(e,t){let r,n;try{if(r=new OffscreenCanvas(e,t),n=r.getContext("2d"),n===null)throw new Error("getContext of OffscreenCanvas returns null")}catch{r=document.createElement("canvas"),n=r.getContext("2d")}return r.width=e,r.height=t,[r,n]}function Vl(e,t){const{width:r,height:n}=Wl(e.width,e.height),[o,a]=Dn(r,n);return t&&/jpe?g/.test(t)&&(a.fillStyle="white",a.fillRect(0,0,o.width,o.height)),a.drawImage(e,0,0,o.width,o.height),o}function Ln(){return Ln.cachedResult!==void 0||(Ln.cachedResult=["iPad Simulator","iPhone Simulator","iPod Simulator","iPad","iPhone","iPod"].includes(navigator.platform)||navigator.userAgent.includes("Mac")&&typeof document<"u"&&"ontouchend"in document),Ln.cachedResult}function Pn(e,t={}){return new Promise((function(r,n){let o,a;var s=function(){try{return a=Vl(o,t.fileType||e.type),r([o,a])}catch(f){return n(f)}},l=function(f){try{var h=function(c){try{throw c}catch(i){return n(i)}};try{let c;return jl(e).then((function(i){try{return c=i,Bl(c).then((function(u){try{return o=u,(function(){try{return s()}catch(g){return n(g)}})()}catch(g){return h(g)}}),h)}catch(u){return h(u)}}),h)}catch(c){h(c)}}catch(c){return n(c)}};try{if(Ln()||[bt.DESKTOP_SAFARI,bt.MOBILE_SAFARI].includes(fr()))throw new Error("Skip createImageBitmap on IOS and Safari");return createImageBitmap(e).then((function(f){try{return o=f,s()}catch{return l()}}),l)}catch{l()}}))}function $n(e,t,r,n,o=1){return new Promise((function(a,s){let l;if(t==="image/png"){let h,c,i;return h=e.getContext("2d"),{data:c}=h.getImageData(0,0,e.width,e.height),i=Ht.encode([c.buffer],e.width,e.height,4096*o),l=new Blob([i],{type:t}),l.name=r,l.lastModified=n,f.call(this)}{let h=function(){return f.call(this)};if(t==="image/bmp")return new Promise((c=>zl.toBlob(e,c))).then(function(c){try{return l=c,l.name=r,l.lastModified=n,h.call(this)}catch(i){return s(i)}}.bind(this),s);{let c=function(){return h.call(this)};if(typeof OffscreenCanvas=="function"&&e instanceof OffscreenCanvas)return e.convertToBlob({type:t,quality:o}).then(function(i){try{return l=i,l.name=r,l.lastModified=n,c.call(this)}catch(u){return s(u)}}.bind(this),s);{let i;return i=e.toDataURL(t,o),oa(i,r,n).then(function(u){try{return l=u,c.call(this)}catch(g){return s(g)}}.bind(this),s)}}}function f(){return a(l)}}))}function Kt(e){e.width=0,e.height=0}function Nr(){return new Promise((function(e,t){let r,n,o,a;return Nr.cachedResult!==void 0?e(Nr.cachedResult):oa("data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/xABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAAAAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q==","test.jpg",Date.now()).then((function(s){try{return r=s,Pn(r).then((function(l){try{return n=l[1],$n(n,r.type,r.name,r.lastModified).then((function(f){try{return o=f,Kt(n),Pn(o).then((function(h){try{return a=h[0],Nr.cachedResult=a.width===1&&a.height===2,e(Nr.cachedResult)}catch(c){return t(c)}}),t)}catch(h){return t(h)}}),t)}catch(f){return t(f)}}),t)}catch(l){return t(l)}}),t)}))}function Kl(e){return new Promise(((t,r)=>{const n=new Gl;n.onload=o=>{const a=new DataView(o.target.result);if(a.getUint16(0,!1)!=65496)return t(-2);const s=a.byteLength;let l=2;for(;l<s;){if(a.getUint16(l+2,!1)<=8)return t(-1);const f=a.getUint16(l,!1);if(l+=2,f==65505){if(a.getUint32(l+=2,!1)!=1165519206)return t(-1);const h=a.getUint16(l+=6,!1)==18761;l+=a.getUint32(l+4,h);const c=a.getUint16(l,h);l+=2;for(let i=0;i<c;i++)if(a.getUint16(l+12*i,h)==274)return t(a.getUint16(l+12*i+8,h))}else{if((65280&f)!=65280)break;l+=a.getUint16(l,!1)}}return t(-1)},n.onerror=o=>r(o),n.readAsArrayBuffer(e)}))}function Yl(e,t){const{width:r}=e,{height:n}=e,{maxWidthOrHeight:o}=t;let a,s=e;return isFinite(o)&&(r>o||n>o)&&([s,a]=Dn(r,n),r>n?(s.width=o,s.height=n/r*o):(s.width=r/n*o,s.height=o),a.drawImage(e,0,0,s.width,s.height),Kt(e)),s}function Xl(e,t){const{width:r}=e,{height:n}=e,[o,a]=Dn(r,n);switch(t>4&&t<9?(o.width=n,o.height=r):(o.width=r,o.height=n),t){case 2:a.transform(-1,0,0,1,r,0);break;case 3:a.transform(-1,0,0,-1,r,n);break;case 4:a.transform(1,0,0,-1,0,n);break;case 5:a.transform(0,1,1,0,0,0);break;case 6:a.transform(0,1,-1,0,n,0);break;case 7:a.transform(0,-1,-1,0,n,r);break;case 8:a.transform(0,-1,1,0,0,r)}return a.drawImage(e,0,0,r,n),Kt(e),o}function ql(e,t,r=0){return new Promise((function(n,o){let a,s,l,f,h,c,i,u,g,p,m,x,C,O,v,E,I,_,M,w;function y(A=5){if(t.signal&&t.signal.aborted)throw t.signal.reason;a+=A,t.onProgress(Math.min(a,100))}function T(A){if(t.signal&&t.signal.aborted)throw t.signal.reason;a=Math.min(Math.max(A,a),100),t.onProgress(a)}return a=r,s=t.maxIteration||10,l=1024*t.maxSizeMB*1024,y(),Pn(e,t).then(function(A){try{return[,f]=A,y(),h=Yl(f,t),y(),new Promise((function(P,D){var L;if(!(L=t.exifOrientation))return Kl(e).then(function(S){try{return L=S,U.call(this)}catch(b){return D(b)}}.bind(this),D);function U(){return P(L)}return U.call(this)})).then(function(P){try{return c=P,y(),Nr().then(function(D){try{return i=D?h:Xl(h,c),y(),u=t.initialQuality||1,g=t.fileType||e.type,$n(i,g,e.name,e.lastModified,u).then(function(L){try{{let S=function(){if(s--&&(v>l||v>C)){let F,$;return F=w?.95*M.width:M.width,$=w?.95*M.height:M.height,[I,_]=Dn(F,$),_.drawImage(M,0,0,F,$),u*=g==="image/png"?.85:.95,$n(I,g,e.name,e.lastModified,u).then((function(G){try{return E=G,Kt(M),M=I,v=E.size,T(Math.min(99,Math.floor((O-v)/(O-l)*100))),S}catch(Y){return o(Y)}}),o)}return[1]},b=function(){return Kt(M),Kt(I),Kt(h),Kt(i),Kt(f),T(100),n(E)};if(p=L,y(),m=p.size>l,x=p.size>e.size,!m&&!x)return T(100),n(p);var U;return C=e.size,O=p.size,v=O,M=i,w=!t.alwaysKeepResolution&&m,(U=function(F){for(;F;){if(F.then)return void F.then(U,o);try{if(F.pop){if(F.length)return F.pop()?b.call(this):F;F=S}else F=F.call(this)}catch($){return o($)}}}.bind(this))(S)}}catch(S){return o(S)}}.bind(this),o)}catch(L){return o(L)}}.bind(this),o)}catch(D){return o(D)}}.bind(this),o)}catch(P){return o(P)}}.bind(this),o)}))}const cg=` let scriptImported = false self.addEventListener('message', async (e) => { const { file, id, imageCompressionLibUrl, options } = e.data @@ -58,20 +58,20 @@ self.addEventListener('message', async (e) => { self.postMessage({ error: e.message + '\\n' + e.stack, id }) } }) -`;let oa;function cg(e,t){return new Promise(((r,n)=>{oa||(oa=(function(s){const l=[];return l.push(s),URL.createObjectURL(new Blob(l))})(ig));const o=new Worker(oa);o.addEventListener("message",(function(s){if(t.signal&&t.signal.aborted)o.terminate();else if(s.data.progress===void 0){if(s.data.error)return n(new Error(s.data.error)),void o.terminate();r(s.data.file),o.terminate()}else t.onProgress(s.data.progress)})),o.addEventListener("error",n),t.signal&&t.signal.addEventListener("abort",(()=>{n(t.signal.reason),o.terminate()})),o.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}})}))}function ct(e,t){return new Promise((function(r,n){let o,a,s,l,f,h;if(o={...t},s=0,{onProgress:l}=o,o.maxSizeMB=o.maxSizeMB||Number.POSITIVE_INFINITY,f=typeof o.useWebWorker!="boolean"||o.useWebWorker,delete o.useWebWorker,o.onProgress=g=>{s=g,typeof l=="function"&&l(s)},!(e instanceof Blob||e instanceof lg))return n(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return n(new Error("The file given is not an image"));if(h=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!f||typeof Worker!="function"||h)return Xl(e,o).then(function(g){try{return a=g,u.call(this)}catch(p){return n(p)}}.bind(this),n);var c=function(){try{return u.call(this)}catch(g){return n(g)}}.bind(this),i=function(g){try{return Xl(e,o).then((function(p){try{return a=p,c()}catch(m){return n(m)}}),n)}catch(p){return n(p)}};try{return o.libURL=o.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",cg(e,o).then((function(g){try{return a=g,c()}catch{return i()}}),i)}catch{i()}function u(){try{a.name=e.name,a.lastModified=e.lastModified}catch{}try{o.preserveExif&&e.type==="image/jpeg"&&(!o.fileType||o.fileType&&o.fileType===e.type)&&(a=Fl(e,a))}catch{}return r(a)}}))}ct.getDataUrlFromFile=Gl,ct.getFilefromDataUrl=na,ct.loadImage=jl,ct.drawImageInCanvas=Wl,ct.drawFileInCanvas=Pn,ct.canvasToFile=$n,ct.getExifOrientation=Vl,ct.handleMaxWidthOrHeight=Kl,ct.followExifOrientation=Yl,ct.cleanupCanvasMemory=Kt,ct.isAutoOrientationInBrowser=Nr,ct.approximateBelowMaximumCanvasSizeOfBrowser=Bl,ct.copyExifWithoutOrientation=Fl,ct.getBrowserName=fr,ct.version="2.0.2";const dg=window.G7Core,Fn=e=>{const t=["B","KB","MB","GB"];let r=0,n=e;for(;n>=1024&&r<t.length-1;)n/=1024,r++;return`${n.toFixed(2)} ${t[r]}`},ql=e=>{if(e&&typeof e=="object"){const t=e;if(t.response?.data?.message)return t.response.data.message;if(t.response?.data?.errors){const r=Object.values(t.response.data.errors)[0];if(Array.isArray(r)&&r[0])return r[0]}if(t.message)return t.message}return e instanceof Error?e.message:"업로드 실패"},Zl=e=>e?e.startsWith("image/")?"fa-file-image":e.startsWith("video/")?"fa-file-video":e.startsWith("audio/")?"fa-file-audio":e==="application/pdf"?"fa-file-pdf":e.includes("word")||e.includes("document")?"fa-file-word":e.includes("excel")||e.includes("spreadsheet")?"fa-file-excel":e.includes("zip")||e.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",Re=(e,t)=>dg?.t?.(e,t)??e,gr=window.G7Core;function Ql(e){const{attachmentableType:t,attachmentableId:r,collection:n,maxFiles:o,maxSize:a,maxConcurrentUploads:s,accept:l,imageCompression:f,roleIds:h,autoUpload:c,initialFiles:i,endpoints:u,onFilesChange:g,onUploadComplete:p,onUploadError:m,onRemove:w,onReorder:C,uploadTriggerEvent:O,confirmBeforeRemove:v,uploadParams:E}=e,[I,_]=k.useState(i),[M,x]=k.useState([]),[y,T]=k.useState(!1),A=k.useRef(null),P=k.useRef(0),D=k.useRef(new Set),L=k.useRef(new Map),[U,S]=k.useState(!1),[b,F]=k.useState(null),[$,G]=k.useState(!1),Y=k.useRef(!1),[B,K]=k.useState([]),j=k.useRef(i),[H,q]=k.useState(!1),[ue,te]=k.useState(0),fe=k.useRef(0),[ee,ke]=k.useState(new Map),me=k.useRef(new Map);k.useEffect(()=>{g?.(M)},[M,g]);const Ge=k.useMemo(()=>{const X=j.current;return X===i?X:X.length!==i.length?(j.current=i,i):X.length===0&&i.length===0?X:i.some((ie,le)=>ie.id!==X[le]?.id)?(j.current=i,i):X},[i]);k.useEffect(()=>{if(Y.current)return;const X=Ge.filter(le=>!D.current.has(le.id)&&(!le.hash||!D.current.has(le.hash))),se=new Set(X.map(le=>le.id)),ie=Array.from(L.current.values()).filter(le=>!se.has(le.id)&&!D.current.has(le.id));_([...X,...ie])},[Ge]);const De=k.useCallback(async X=>{const se={maxSizeMB:f?.maxSizeMB??1,maxWidthOrHeight:f?.maxWidthOrHeight??1920,useWebWorker:!0};return await ct(X,se)},[f]),ye=k.useCallback(async X=>{const se=I.length+M.length,ie=Math.max(0,o-se),le=Array.from(X).slice(0,ie);if(X.length>ie){const ne=Array.from(X).slice(ie),Te=Re("attachment.upload_limit_exceeded",{max:o});m?.(Te,ne[0]??X[0])}for(const ne of le){if(ne.size>a*1024*1024){m?.(`[${ne.name}] 파일 크기(${Fn(ne.size)})가 최대 허용 크기(${a}MB)를 초과합니다.`,ne);continue}if(l){const ve="."+ne.name.split(".").pop()?.toLowerCase();if(!l.split(",").map(Ue=>Ue.trim().toLowerCase()).some(Ue=>{if(Ue.includes("/")){if(Ue.endsWith("/*")){const je=Ue.slice(0,-2);return ne.type.toLowerCase().startsWith(je+"/")}return ne.type.toLowerCase()===Ue}return ve===Ue})){m?.(`[${ne.name}] 허용되지 않는 파일 형식입니다. (허용: ${l})`,ne);continue}}const Te={id:`pending-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:ne,original_filename:ne.name,mime_type:ne.type,size:ne.size,size_formatted:Fn(ne.size),status:"pending",progress:0};if(ne.type.startsWith("image/")&&(Te.preview=URL.createObjectURL(ne)),x(ve=>[...ve,Te]),ne.type.startsWith("image/")&&f){x(ve=>ve.map(Se=>Se.id===Te.id?{...Se,status:"compressing"}:Se));try{const ve=await De(ne);x(Se=>Se.map(_e=>_e.id===Te.id?{..._e,compressedBlob:ve,size:ve.size,size_formatted:Fn(ve.size),status:"pending",progress:0}:_e))}catch{x(ve=>ve.map(Se=>Se.id===Te.id?{...Se,status:"error",error:"이미지 압축 실패"}:Se))}}}},[I.length,M.length,o,a,l,f,De,m]),Me=k.useCallback(async X=>{const se=new FormData,ie=X.compressedBlob?new File([X.compressedBlob],X.original_filename,{type:X.mime_type}):X.file;se.append("file",ie),t&&se.append("attachmentable_type",t),r&&se.append("attachmentable_id",String(r)),se.append("collection",n),h.forEach(le=>{se.append("role_ids[]",String(le))}),E&&Object.entries(E).forEach(([le,ne])=>{se.append(le,ne)});try{const le=await gr.api.post(u.upload,se,{headers:{"Content-Type":"multipart/form-data"},onUploadProgress:Te=>{const ve=Math.round(Te.loaded*100/(Te.total??1));x(Se=>Se.map(_e=>_e.id===X.id?{..._e,progress:ve}:_e))}});return le?.data?.data??le?.data}catch(le){const ne=ql(le);throw new Error(ne)}},[t,r,n,h,u.upload]),Ce=k.useCallback(async()=>{const X=M.filter(ve=>ve.status==="pending"||ve.status==="error");if(X.length===0)return[];const se=[],ie=new Map,le=[...X],ne=async()=>{for(;le.length>0&&P.current<s;){const ve=le.shift();P.current++,x(Se=>Se.map(_e=>_e.id===ve.id?{..._e,status:"uploading",progress:0}:_e));try{const Se=await Me(ve);if(Se)se.push(Se),ie.set(String(ve.id),String(Se.hash)),L.current.set(Se.id,Se),x(_e=>_e.filter(Ue=>Ue.id!==ve.id)),_(_e=>[..._e,Se]),K(_e=>{if(_e.length===0)return _e;const Ue=String(ve.id);return _e.some(Ee=>String(Ee)===Ue)?_e.map(Ee=>String(Ee)===Ue?String(Se.hash):Ee):_e}),ve.preview&&URL.revokeObjectURL(ve.preview);else{const _e=Re("attachment.upload_response_invalid");x(Ue=>Ue.map(je=>je.id===ve.id?{...je,status:"error",error:_e}:je)),m?.(`[${ve.original_filename}] ${_e}`,ve.file)}}catch(Se){const _e=Se instanceof Error?Se.message:"업로드 실패";x(Ue=>Ue.map(je=>je.id===ve.id?{...je,status:"error",error:_e}:je)),m?.(`[${ve.original_filename}] ${_e}`,ve.file)}finally{P.current--}}},Te=Array(Math.min(s,X.length)).fill(null).map(()=>ne());if(await Promise.all(Te),se.length>0&&(p?.(se),u.reorder&&B.length>0&&C)){const ve=[...I,...se],Se=B.map(je=>ie.get(String(je))??je),_e=new Map(Se.map((je,Ee)=>[String(je),Ee])),Ue=[...ve].sort((je,Ee)=>{const he=_e.get(String(je.hash))??ve.length,Ne=_e.get(String(Ee.hash))??ve.length;return he-Ne}).map((je,Ee)=>({...je,sort_order:Ee,order:Ee+1}));C(Ue)}return se},[M,s,Me,p,m,B,I,C,u.reorder]);k.useEffect(()=>{if(!O)return;const X=gr.componentEvent.on(O,async()=>{const se=await Ce();return{uploadedAttachments:se,existingFiles:I,allFiles:[...I,...se]}});return()=>{X()}},[O,Ce,I]),k.useEffect(()=>{c&&M.some(X=>X.status==="pending")&&Ce()},[c,M,Ce]);const we=k.useCallback(async X=>{G(!0);try{if(X.id&&u.delete){const ie=u.delete.replace(":id",String(X.id));await gr.api.delete(ie)}const se=X.hash||X.id;X.id&&(D.current.add(X.id),L.current.delete(X.id)),X.hash&&D.current.add(X.hash),_(ie=>ie.filter(le=>(le.hash||le.id)!==se)),w?.(X.hash||X.id),S(!1),F(null)}catch{console.error("첨부파일 삭제 실패")}finally{G(!1)}},[w,u.delete]),Fe=k.useCallback(async X=>{"hash"in X?v?(F(X),S(!0)):await we(X):(X.preview&&URL.revokeObjectURL(X.preview),x(se=>se.filter(ie=>ie.id!==X.id)))},[v,we]),Le=k.useCallback(X=>{x(se=>se.map(ie=>ie.id===X.id?{...ie,status:"pending",error:void 0}:ie))},[]),V=k.useCallback(async X=>{const{arrayMove:se}=await Promise.resolve().then(()=>Vh),{active:ie,over:le}=X;if(!le||ie.id===le.id)return;const ne=[...I,...M];let Te;if(B.length===0)Te=ne;else{const he=new Map(B.map((Ne,Ke)=>[String(Ne),Ke]));Te=[...ne].sort((Ne,Ke)=>{const $e="hash"in Ne?String(Ne.hash):String(Ne.id),Be="hash"in Ke?String(Ke.hash):String(Ke.id);return(he.get($e)??ne.length)-(he.get(Be)??ne.length)})}const ve=Te.findIndex(he=>"hash"in he?he.hash===ie.id:he.id===ie.id),Se=Te.findIndex(he=>"hash"in he?he.hash===le.id:he.id===le.id),_e=se(Te,ve,Se);K(_e.map(he=>"hash"in he?he.hash:he.id));const Ue=_e.filter(he=>"hash"in he),je=_e.filter(he=>!("hash"in he));Y.current=!0,_(Ue),x(je);const Ee=Ue.map((he,Ne)=>({...he,sort_order:Ne,order:Ne+1}));if(Ue.length>0&&u.reorder){const he=Ue.map((Ne,Ke)=>({id:Ne.id,order:Ke+1}));try{const Ne=await gr.api.patch(u.reorder,{order:he});gr.toast.success(Ne.message)}catch(Ne){const Ke=Ne instanceof Error?Ne.message:Re("attachment.reorder_failed");gr.toast.error(Ke)}}C?.(Ee)},[I,M,B,u.reorder,C]),re=k.useMemo(()=>[...I,...M].filter(X=>"is_image"in X?X.is_image:X.mime_type?.startsWith("image/")??!1),[I,M]),oe=k.useCallback(X=>{const se=re.findIndex(ie=>"hash"in X&&"hash"in ie?ie.hash===X.hash:"id"in X&&"id"in ie&&!("hash"in X)&&!("hash"in ie)?ie.id===X.id:!1);se!==-1&&(fe.current+=1,te(se),q(!0))},[re]);k.useEffect(()=>{let X=!1;return(async()=>{const ie=I.filter(ne=>ne.is_image),le=new Set(ie.map(ne=>ne.id));for(const[ne,Te]of me.current)le.has(ne)||(URL.revokeObjectURL(Te),me.current.delete(ne));for(const ne of ie){if(X)break;if(!me.current.has(ne.id))try{const Te=await gr.api.get(ne.download_url,{responseType:"blob"});if(!X&&Te){const ve=URL.createObjectURL(Te);me.current.set(ne.id,ve),ke(new Map(me.current))}}catch(Te){console.error("Failed to load authenticated image:",Te)}}})(),()=>{X=!0}},[I]),k.useEffect(()=>()=>{me.current.forEach(X=>URL.revokeObjectURL(X)),me.current.clear()},[]);const ae=k.useCallback(async X=>{const{executeImageDownload:se}=await Promise.resolve().then(()=>Nl);await se({src:X.download_url,downloadUrl:X.download_url,filename:X.original_filename,downloadRequiresAuth:!0})},[]),be=k.useMemo(()=>{const X=[...I,...M];if(B.length===0)return X;const se=new Map(B.map((ie,le)=>[String(ie),le]));return[...X].sort((ie,le)=>{const ne="hash"in ie?String(ie.hash):String(ie.id),Te="hash"in le?String(le.hash):String(le.id),ve=se.get(ne)??X.length,Se=se.get(Te)??X.length;return ve-Se})},[I,M,B]),xe=be.length,He=xe>0,Pe=xe<o,Ve=k.useCallback(()=>x([]),[]),Oe=k.useCallback(()=>M,[M]);return{existingFiles:I,pendingFiles:M,isDragOver:y,setIsDragOver:T,isDeleting:$,totalCount:xe,hasFiles:He,canAddMore:Pe,allItems:be,imageFiles:re,handleFiles:ye,handleRemove:Fe,handleRetry:Le,handleUploadAll:Ce,handleOpenGallery:oe,handleDownload:ae,handleDragEnd:V,inputRef:A,galleryOpen:H,setGalleryOpen:q,galleryStartIndex:ue,galleryKeyRef:fe,confirmDialogOpen:U,setConfirmDialogOpen:S,itemToDelete:b,executeRemoveAttachment:we,authenticatedImageUrls:ee,clear:Ve,getPendingFiles:Oe}}const Jl=({isDragOver:e,canAddMore:t,onFiles:r,setIsDragOver:n,inputRef:o,accept:a,maxFiles:s,maxSize:l,children:f})=>d.jsxs(W,{className:` +`;let aa;function dg(e,t){return new Promise(((r,n)=>{aa||(aa=(function(s){const l=[];return l.push(s),URL.createObjectURL(new Blob(l))})(cg));const o=new Worker(aa);o.addEventListener("message",(function(s){if(t.signal&&t.signal.aborted)o.terminate();else if(s.data.progress===void 0){if(s.data.error)return n(new Error(s.data.error)),void o.terminate();r(s.data.file),o.terminate()}else t.onProgress(s.data.progress)})),o.addEventListener("error",n),t.signal&&t.signal.addEventListener("abort",(()=>{n(t.signal.reason),o.terminate()})),o.postMessage({file:e,imageCompressionLibUrl:t.libURL,options:{...t,onProgress:void 0,signal:void 0}})}))}function ct(e,t){return new Promise((function(r,n){let o,a,s,l,f,h;if(o={...t},s=0,{onProgress:l}=o,o.maxSizeMB=o.maxSizeMB||Number.POSITIVE_INFINITY,f=typeof o.useWebWorker!="boolean"||o.useWebWorker,delete o.useWebWorker,o.onProgress=g=>{s=g,typeof l=="function"&&l(s)},!(e instanceof Blob||e instanceof ig))return n(new Error("The file given is not an instance of Blob or File"));if(!/^image/.test(e.type))return n(new Error("The file given is not an image"));if(h=typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope,!f||typeof Worker!="function"||h)return ql(e,o).then(function(g){try{return a=g,u.call(this)}catch(p){return n(p)}}.bind(this),n);var c=function(){try{return u.call(this)}catch(g){return n(g)}}.bind(this),i=function(g){try{return ql(e,o).then((function(p){try{return a=p,c()}catch(m){return n(m)}}),n)}catch(p){return n(p)}};try{return o.libURL=o.libURL||"https://cdn.jsdelivr.net/npm/browser-image-compression@2.0.2/dist/browser-image-compression.js",dg(e,o).then((function(g){try{return a=g,c()}catch{return i()}}),i)}catch{i()}function u(){try{a.name=e.name,a.lastModified=e.lastModified}catch{}try{o.preserveExif&&e.type==="image/jpeg"&&(!o.fileType||o.fileType&&o.fileType===e.type)&&(a=Ul(e,a))}catch{}return r(a)}}))}ct.getDataUrlFromFile=jl,ct.getFilefromDataUrl=oa,ct.loadImage=Bl,ct.drawImageInCanvas=Vl,ct.drawFileInCanvas=Pn,ct.canvasToFile=$n,ct.getExifOrientation=Kl,ct.handleMaxWidthOrHeight=Yl,ct.followExifOrientation=Xl,ct.cleanupCanvasMemory=Kt,ct.isAutoOrientationInBrowser=Nr,ct.approximateBelowMaximumCanvasSizeOfBrowser=Wl,ct.copyExifWithoutOrientation=Ul,ct.getBrowserName=fr,ct.version="2.0.2";const ug=window.G7Core,Fn=e=>{const t=["B","KB","MB","GB"];let r=0,n=e;for(;n>=1024&&r<t.length-1;)n/=1024,r++;return`${n.toFixed(2)} ${t[r]}`},Zl=e=>{if(e&&typeof e=="object"){const t=e;if(t.response?.data?.message)return t.response.data.message;if(t.response?.data?.errors){const r=Object.values(t.response.data.errors)[0];if(Array.isArray(r)&&r[0])return r[0]}if(t.message)return t.message}return e instanceof Error?e.message:"업로드 실패"},Ql=e=>e?e.startsWith("image/")?"fa-file-image":e.startsWith("video/")?"fa-file-video":e.startsWith("audio/")?"fa-file-audio":e==="application/pdf"?"fa-file-pdf":e.includes("word")||e.includes("document")?"fa-file-word":e.includes("excel")||e.includes("spreadsheet")?"fa-file-excel":e.includes("zip")||e.includes("compressed")?"fa-file-zipper":"fa-file":"fa-file",Re=(e,t)=>ug?.t?.(e,t)??e,gr=window.G7Core;function Jl(e){const{attachmentableType:t,attachmentableId:r,collection:n,maxFiles:o,maxSize:a,maxConcurrentUploads:s,accept:l,imageCompression:f,roleIds:h,autoUpload:c,initialFiles:i,endpoints:u,onFilesChange:g,onUploadComplete:p,onUploadError:m,onRemove:x,onReorder:C,uploadTriggerEvent:O,confirmBeforeRemove:v,uploadParams:E}=e,[I,_]=k.useState(i),[M,w]=k.useState([]),[y,T]=k.useState(!1),A=k.useRef(null),P=k.useRef(0),D=k.useRef(new Set),L=k.useRef(new Map),[U,S]=k.useState(!1),[b,F]=k.useState(null),[$,G]=k.useState(!1),Y=k.useRef(!1),[B,K]=k.useState([]),j=k.useRef(i),[H,q]=k.useState(!1),[ue,te]=k.useState(0),fe=k.useRef(0),[ee,xe]=k.useState(new Map),he=k.useRef(new Map);k.useEffect(()=>{g?.(M)},[M,g]);const Ge=k.useMemo(()=>{const X=j.current;return X===i?X:X.length!==i.length?(j.current=i,i):X.length===0&&i.length===0?X:i.some((ie,le)=>ie.id!==X[le]?.id)?(j.current=i,i):X},[i]);k.useEffect(()=>{if(Y.current)return;const X=Ge.filter(le=>!D.current.has(le.id)&&(!le.hash||!D.current.has(le.hash))),se=new Set(X.map(le=>le.id)),ie=Array.from(L.current.values()).filter(le=>!se.has(le.id)&&!D.current.has(le.id));_([...X,...ie])},[Ge]);const De=k.useCallback(async X=>{const se={maxSizeMB:f?.maxSizeMB??1,maxWidthOrHeight:f?.maxWidthOrHeight??1920,useWebWorker:!0};return await ct(X,se)},[f]),ye=k.useCallback(async X=>{const se=I.length+M.length,ie=Math.max(0,o-se),le=Array.from(X).slice(0,ie);if(X.length>ie){const ne=Array.from(X).slice(ie),Te=Re("attachment.upload_limit_exceeded",{max:o});m?.(Te,ne[0]??X[0])}for(const ne of le){if(ne.size>a*1024*1024){m?.(`[${ne.name}] 파일 크기(${Fn(ne.size)})가 최대 허용 크기(${a}MB)를 초과합니다.`,ne);continue}if(l){const ve="."+ne.name.split(".").pop()?.toLowerCase();if(!l.split(",").map(Ue=>Ue.trim().toLowerCase()).some(Ue=>{if(Ue.includes("/")){if(Ue.endsWith("/*")){const je=Ue.slice(0,-2);return ne.type.toLowerCase().startsWith(je+"/")}return ne.type.toLowerCase()===Ue}return ve===Ue})){m?.(`[${ne.name}] 허용되지 않는 파일 형식입니다. (허용: ${l})`,ne);continue}}const Te={id:`pending-${Date.now()}-${Math.random().toString(36).substr(2,9)}`,file:ne,original_filename:ne.name,mime_type:ne.type,size:ne.size,size_formatted:Fn(ne.size),status:"pending",progress:0};if(ne.type.startsWith("image/")&&(Te.preview=URL.createObjectURL(ne)),w(ve=>[...ve,Te]),ne.type.startsWith("image/")&&f){w(ve=>ve.map(Se=>Se.id===Te.id?{...Se,status:"compressing"}:Se));try{const ve=await De(ne);w(Se=>Se.map(_e=>_e.id===Te.id?{..._e,compressedBlob:ve,size:ve.size,size_formatted:Fn(ve.size),status:"pending",progress:0}:_e))}catch{w(ve=>ve.map(Se=>Se.id===Te.id?{...Se,status:"error",error:"이미지 압축 실패"}:Se))}}}},[I.length,M.length,o,a,l,f,De,m]),Me=k.useCallback(async X=>{const se=new FormData,ie=X.compressedBlob?new File([X.compressedBlob],X.original_filename,{type:X.mime_type}):X.file;se.append("file",ie),t&&se.append("attachmentable_type",t),r&&se.append("attachmentable_id",String(r)),se.append("collection",n),h.forEach(le=>{se.append("role_ids[]",String(le))}),E&&Object.entries(E).forEach(([le,ne])=>{se.append(le,ne)});try{const le=await gr.api.post(u.upload,se,{headers:{"Content-Type":"multipart/form-data"},onUploadProgress:Te=>{const ve=Math.round(Te.loaded*100/(Te.total??1));w(Se=>Se.map(_e=>_e.id===X.id?{..._e,progress:ve}:_e))}});return le?.data?.data??le?.data}catch(le){const ne=Zl(le);throw new Error(ne)}},[t,r,n,h,u.upload]),Ce=k.useCallback(async()=>{const X=M.filter(ve=>ve.status==="pending"||ve.status==="error");if(X.length===0)return[];const se=[],ie=new Map,le=[...X],ne=async()=>{for(;le.length>0&&P.current<s;){const ve=le.shift();P.current++,w(Se=>Se.map(_e=>_e.id===ve.id?{..._e,status:"uploading",progress:0}:_e));try{const Se=await Me(ve);if(Se)se.push(Se),ie.set(String(ve.id),String(Se.hash)),L.current.set(Se.id,Se),w(_e=>_e.filter(Ue=>Ue.id!==ve.id)),_(_e=>[..._e,Se]),K(_e=>{if(_e.length===0)return _e;const Ue=String(ve.id);return _e.some(Ee=>String(Ee)===Ue)?_e.map(Ee=>String(Ee)===Ue?String(Se.hash):Ee):_e}),ve.preview&&URL.revokeObjectURL(ve.preview);else{const _e=Re("attachment.upload_response_invalid");w(Ue=>Ue.map(je=>je.id===ve.id?{...je,status:"error",error:_e}:je)),m?.(`[${ve.original_filename}] ${_e}`,ve.file)}}catch(Se){const _e=Se instanceof Error?Se.message:"업로드 실패";w(Ue=>Ue.map(je=>je.id===ve.id?{...je,status:"error",error:_e}:je)),m?.(`[${ve.original_filename}] ${_e}`,ve.file)}finally{P.current--}}},Te=Array(Math.min(s,X.length)).fill(null).map(()=>ne());if(await Promise.all(Te),se.length>0&&(p?.(se),u.reorder&&B.length>0&&C)){const ve=[...I,...se],Se=B.map(je=>ie.get(String(je))??je),_e=new Map(Se.map((je,Ee)=>[String(je),Ee])),Ue=[...ve].sort((je,Ee)=>{const pe=_e.get(String(je.hash))??ve.length,Ne=_e.get(String(Ee.hash))??ve.length;return pe-Ne}).map((je,Ee)=>({...je,sort_order:Ee,order:Ee+1}));C(Ue)}return se},[M,s,Me,p,m,B,I,C,u.reorder]);k.useEffect(()=>{if(!O)return;const X=gr.componentEvent.on(O,async()=>{const se=await Ce();return{uploadedAttachments:se,existingFiles:I,allFiles:[...I,...se]}});return()=>{X()}},[O,Ce,I]),k.useEffect(()=>{c&&M.some(X=>X.status==="pending")&&Ce()},[c,M,Ce]);const we=k.useCallback(async X=>{G(!0);try{if(X.id&&u.delete){const ie=u.delete.replace(":id",String(X.id));await gr.api.delete(ie)}const se=X.hash||X.id;X.id&&(D.current.add(X.id),L.current.delete(X.id)),X.hash&&D.current.add(X.hash),_(ie=>ie.filter(le=>(le.hash||le.id)!==se)),x?.(X.hash||X.id),S(!1),F(null)}catch{console.error("첨부파일 삭제 실패")}finally{G(!1)}},[x,u.delete]),Fe=k.useCallback(async X=>{"hash"in X?v?(F(X),S(!0)):await we(X):(X.preview&&URL.revokeObjectURL(X.preview),w(se=>se.filter(ie=>ie.id!==X.id)))},[v,we]),Le=k.useCallback(X=>{w(se=>se.map(ie=>ie.id===X.id?{...ie,status:"pending",error:void 0}:ie))},[]),V=k.useCallback(async X=>{const{arrayMove:se}=await Promise.resolve().then(()=>Kh),{active:ie,over:le}=X;if(!le||ie.id===le.id)return;const ne=[...I,...M];let Te;if(B.length===0)Te=ne;else{const pe=new Map(B.map((Ne,Ke)=>[String(Ne),Ke]));Te=[...ne].sort((Ne,Ke)=>{const $e="hash"in Ne?String(Ne.hash):String(Ne.id),Be="hash"in Ke?String(Ke.hash):String(Ke.id);return(pe.get($e)??ne.length)-(pe.get(Be)??ne.length)})}const ve=Te.findIndex(pe=>"hash"in pe?pe.hash===ie.id:pe.id===ie.id),Se=Te.findIndex(pe=>"hash"in pe?pe.hash===le.id:pe.id===le.id),_e=se(Te,ve,Se);K(_e.map(pe=>"hash"in pe?pe.hash:pe.id));const Ue=_e.filter(pe=>"hash"in pe),je=_e.filter(pe=>!("hash"in pe));Y.current=!0,_(Ue),w(je);const Ee=Ue.map((pe,Ne)=>({...pe,sort_order:Ne,order:Ne+1}));if(Ue.length>0&&u.reorder){const pe=Ue.map((Ne,Ke)=>({id:Ne.id,order:Ke+1}));try{const Ne=await gr.api.patch(u.reorder,{order:pe});gr.toast.success(Ne.message)}catch(Ne){const Ke=Ne instanceof Error?Ne.message:Re("attachment.reorder_failed");gr.toast.error(Ke)}}C?.(Ee)},[I,M,B,u.reorder,C]),re=k.useMemo(()=>[...I,...M].filter(X=>"is_image"in X?X.is_image:X.mime_type?.startsWith("image/")??!1),[I,M]),oe=k.useCallback(X=>{const se=re.findIndex(ie=>"hash"in X&&"hash"in ie?ie.hash===X.hash:"id"in X&&"id"in ie&&!("hash"in X)&&!("hash"in ie)?ie.id===X.id:!1);se!==-1&&(fe.current+=1,te(se),q(!0))},[re]);k.useEffect(()=>{let X=!1;return(async()=>{const ie=I.filter(ne=>ne.is_image),le=new Set(ie.map(ne=>ne.id));for(const[ne,Te]of he.current)le.has(ne)||(URL.revokeObjectURL(Te),he.current.delete(ne));for(const ne of ie){if(X)break;if(!he.current.has(ne.id)){if(Yo(ne.download_url)){he.current.set(ne.id,ne.download_url),xe(new Map(he.current));continue}try{const Te=await gr.api.get(ne.download_url,{responseType:"blob"});if(!X&&Te){const ve=URL.createObjectURL(Te);he.current.set(ne.id,ve),xe(new Map(he.current))}}catch(Te){console.error("Failed to load authenticated image:",Te)}}}})(),()=>{X=!0}},[I]),k.useEffect(()=>()=>{he.current.forEach(X=>URL.revokeObjectURL(X)),he.current.clear()},[]);const ae=k.useCallback(async X=>{const{executeImageDownload:se}=await Promise.resolve().then(()=>Al);await se({src:X.download_url,downloadUrl:X.download_url,filename:X.original_filename,downloadRequiresAuth:!0})},[]),be=k.useMemo(()=>{const X=[...I,...M];if(B.length===0)return X;const se=new Map(B.map((ie,le)=>[String(ie),le]));return[...X].sort((ie,le)=>{const ne="hash"in ie?String(ie.hash):String(ie.id),Te="hash"in le?String(le.hash):String(le.id),ve=se.get(ne)??X.length,Se=se.get(Te)??X.length;return ve-Se})},[I,M,B]),ke=be.length,He=ke>0,Pe=ke<o,Ve=k.useCallback(()=>w([]),[]),Oe=k.useCallback(()=>M,[M]);return{existingFiles:I,pendingFiles:M,isDragOver:y,setIsDragOver:T,isDeleting:$,totalCount:ke,hasFiles:He,canAddMore:Pe,allItems:be,imageFiles:re,handleFiles:ye,handleRemove:Fe,handleRetry:Le,handleUploadAll:Ce,handleOpenGallery:oe,handleDownload:ae,handleDragEnd:V,inputRef:A,galleryOpen:H,setGalleryOpen:q,galleryStartIndex:ue,galleryKeyRef:fe,confirmDialogOpen:U,setConfirmDialogOpen:S,itemToDelete:b,executeRemoveAttachment:we,authenticatedImageUrls:ee,clear:Ve,getPendingFiles:Oe}}const Rl=({isDragOver:e,canAddMore:t,onFiles:r,setIsDragOver:n,inputRef:o,accept:a,maxFiles:s,maxSize:l,children:f})=>d.jsxs(W,{className:` relative border-2 border-dashed rounded-lg transition-colors ${e?"border-blue-500 bg-blue-50 dark:bg-blue-900/20":"border-gray-300 dark:border-gray-600"} - `,onDragOver:h=>{h.preventDefault(),t&&n(!0)},onDragLeave:()=>n(!1),onDrop:h=>{h.preventDefault(),n(!1),t&&r(h.dataTransfer.files)},children:[d.jsx("input",{ref:o,type:"file",multiple:!0,accept:a,onChange:h=>h.target.files&&r(h.target.files),className:"hidden"}),!f&&d.jsxs(W,{className:"p-8 text-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:()=>o.current?.click(),children:[d.jsx(Ct,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),d.jsx(Dt,{className:"text-sm text-gray-600 dark:text-gray-400",children:Re("attachment.drop_or_click")}),d.jsxs(Dt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[Re("attachment.upload_limit",{maxFiles:s,maxSize:l}),a&&` (${a})`]})]}),f]});function ug(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return k.useMemo(()=>n=>{t.forEach(o=>o(n))},t)}const Un=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Ar(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function aa(e){return"nodeType"in e}function yt(e){var t,r;return e?Ar(e)?e:aa(e)&&(t=(r=e.ownerDocument)==null?void 0:r.defaultView)!=null?t:window:window}function sa(e){const{Document:t}=yt(e);return e instanceof t}function Qr(e){return Ar(e)?!1:e instanceof yt(e).HTMLElement}function Rl(e){return e instanceof yt(e).SVGElement}function Mr(e){return e?Ar(e)?e.document:aa(e)?sa(e)?e:Qr(e)||Rl(e)?e.ownerDocument:document:document:document}const Yt=Un?k.useLayoutEffect:k.useEffect;function la(e){const t=k.useRef(e);return Yt(()=>{t.current=e}),k.useCallback(function(){for(var r=arguments.length,n=new Array(r),o=0;o<r;o++)n[o]=arguments[o];return t.current==null?void 0:t.current(...n)},[])}function fg(){const e=k.useRef(null),t=k.useCallback((n,o)=>{e.current=setInterval(n,o)},[]),r=k.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,r]}function Jr(e,t){t===void 0&&(t=[e]);const r=k.useRef(e);return Yt(()=>{r.current!==e&&(r.current=e)},t),r}function Rr(e,t){const r=k.useRef();return k.useMemo(()=>{const n=e(r.current);return r.current=n,n},[...t])}function zn(e){const t=la(e),r=k.useRef(null),n=k.useCallback(o=>{o!==r.current&&t?.(o,r.current),r.current=o},[]);return[r,n]}function ia(e){const t=k.useRef();return k.useEffect(()=>{t.current=e},[e]),t.current}let ca={};function en(e,t){return k.useMemo(()=>{if(t)return t;const r=ca[e]==null?0:ca[e]+1;return ca[e]=r,e+"-"+r},[e,t])}function ei(e){return function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return n.reduce((a,s)=>{const l=Object.entries(s);for(const[f,h]of l){const c=a[f];c!=null&&(a[f]=c+e*h)}return a},{...t})}}const Or=ei(1),tn=ei(-1);function gg(e){return"clientX"in e&&"clientY"in e}function da(e){if(!e)return!1;const{KeyboardEvent:t}=yt(e.target);return t&&e instanceof t}function hg(e){if(!e)return!1;const{TouchEvent:t}=yt(e.target);return t&&e instanceof t}function ua(e){if(hg(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:r}=e.touches[0];return{x:t,y:r}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:r}=e.changedTouches[0];return{x:t,y:r}}}return gg(e)?{x:e.clientX,y:e.clientY}:null}const rn=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:r}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(r?Math.round(r):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:r}=e;return"scaleX("+t+") scaleY("+r+")"}},Transform:{toString(e){if(e)return[rn.Translate.toString(e),rn.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:r,easing:n}=e;return t+" "+r+"ms "+n}}}),ti="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function pg(e){return e.matches(ti)?e:e.querySelector(ti)}const mg={display:"none"};function bg(e){let{id:t,value:r}=e;return k.createElement("div",{id:t,style:mg},r)}function yg(e){let{id:t,announcement:r,ariaLiveType:n="assertive"}=e;const o={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return k.createElement("div",{id:t,style:o,role:"status","aria-live":n,"aria-atomic":!0},r)}function vg(){const[e,t]=k.useState("");return{announce:k.useCallback(n=>{n!=null&&t(n)},[]),announcement:e}}const ri=k.createContext(null);function wg(e){const t=k.useContext(ri);k.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function xg(){const[e]=k.useState(()=>new Set),t=k.useCallback(n=>(e.add(n),()=>e.delete(n)),[e]);return[k.useCallback(n=>{let{type:o,event:a}=n;e.forEach(s=>{var l;return(l=s[o])==null?void 0:l.call(s,a)})},[e]),t]}const kg={draggable:` + `,onDragOver:h=>{h.preventDefault(),t&&n(!0)},onDragLeave:()=>n(!1),onDrop:h=>{h.preventDefault(),n(!1),t&&r(h.dataTransfer.files)},children:[d.jsx("input",{ref:o,type:"file",multiple:!0,accept:a,onChange:h=>h.target.files&&r(h.target.files),className:"hidden"}),!f&&d.jsxs(W,{className:"p-8 text-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:()=>o.current?.click(),children:[d.jsx(Ct,{className:"fa-solid fa-cloud-arrow-up text-4xl text-gray-400 dark:text-gray-500 mb-4"}),d.jsx(Dt,{className:"text-sm text-gray-600 dark:text-gray-400",children:Re("attachment.drop_or_click")}),d.jsxs(Dt,{className:"text-xs text-gray-500 dark:text-gray-500 mt-2",children:[Re("attachment.upload_limit",{maxFiles:s,maxSize:l}),a&&` (${a})`]})]}),f]});function fg(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return k.useMemo(()=>n=>{t.forEach(o=>o(n))},t)}const Un=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u";function Ar(e){const t=Object.prototype.toString.call(e);return t==="[object Window]"||t==="[object global]"}function sa(e){return"nodeType"in e}function yt(e){var t,r;return e?Ar(e)?e:sa(e)&&(t=(r=e.ownerDocument)==null?void 0:r.defaultView)!=null?t:window:window}function la(e){const{Document:t}=yt(e);return e instanceof t}function Qr(e){return Ar(e)?!1:e instanceof yt(e).HTMLElement}function ei(e){return e instanceof yt(e).SVGElement}function Mr(e){return e?Ar(e)?e.document:sa(e)?la(e)?e:Qr(e)||ei(e)?e.ownerDocument:document:document:document}const Yt=Un?k.useLayoutEffect:k.useEffect;function ia(e){const t=k.useRef(e);return Yt(()=>{t.current=e}),k.useCallback(function(){for(var r=arguments.length,n=new Array(r),o=0;o<r;o++)n[o]=arguments[o];return t.current==null?void 0:t.current(...n)},[])}function gg(){const e=k.useRef(null),t=k.useCallback((n,o)=>{e.current=setInterval(n,o)},[]),r=k.useCallback(()=>{e.current!==null&&(clearInterval(e.current),e.current=null)},[]);return[t,r]}function Jr(e,t){t===void 0&&(t=[e]);const r=k.useRef(e);return Yt(()=>{r.current!==e&&(r.current=e)},t),r}function Rr(e,t){const r=k.useRef();return k.useMemo(()=>{const n=e(r.current);return r.current=n,n},[...t])}function zn(e){const t=ia(e),r=k.useRef(null),n=k.useCallback(o=>{o!==r.current&&t?.(o,r.current),r.current=o},[]);return[r,n]}function ca(e){const t=k.useRef();return k.useEffect(()=>{t.current=e},[e]),t.current}let da={};function en(e,t){return k.useMemo(()=>{if(t)return t;const r=da[e]==null?0:da[e]+1;return da[e]=r,e+"-"+r},[e,t])}function ti(e){return function(t){for(var r=arguments.length,n=new Array(r>1?r-1:0),o=1;o<r;o++)n[o-1]=arguments[o];return n.reduce((a,s)=>{const l=Object.entries(s);for(const[f,h]of l){const c=a[f];c!=null&&(a[f]=c+e*h)}return a},{...t})}}const Or=ti(1),tn=ti(-1);function hg(e){return"clientX"in e&&"clientY"in e}function ua(e){if(!e)return!1;const{KeyboardEvent:t}=yt(e.target);return t&&e instanceof t}function pg(e){if(!e)return!1;const{TouchEvent:t}=yt(e.target);return t&&e instanceof t}function fa(e){if(pg(e)){if(e.touches&&e.touches.length){const{clientX:t,clientY:r}=e.touches[0];return{x:t,y:r}}else if(e.changedTouches&&e.changedTouches.length){const{clientX:t,clientY:r}=e.changedTouches[0];return{x:t,y:r}}}return hg(e)?{x:e.clientX,y:e.clientY}:null}const rn=Object.freeze({Translate:{toString(e){if(!e)return;const{x:t,y:r}=e;return"translate3d("+(t?Math.round(t):0)+"px, "+(r?Math.round(r):0)+"px, 0)"}},Scale:{toString(e){if(!e)return;const{scaleX:t,scaleY:r}=e;return"scaleX("+t+") scaleY("+r+")"}},Transform:{toString(e){if(e)return[rn.Translate.toString(e),rn.Scale.toString(e)].join(" ")}},Transition:{toString(e){let{property:t,duration:r,easing:n}=e;return t+" "+r+"ms "+n}}}),ri="a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]";function mg(e){return e.matches(ri)?e:e.querySelector(ri)}const bg={display:"none"};function yg(e){let{id:t,value:r}=e;return k.createElement("div",{id:t,style:bg},r)}function vg(e){let{id:t,announcement:r,ariaLiveType:n="assertive"}=e;const o={position:"fixed",top:0,left:0,width:1,height:1,margin:-1,border:0,padding:0,overflow:"hidden",clip:"rect(0 0 0 0)",clipPath:"inset(100%)",whiteSpace:"nowrap"};return k.createElement("div",{id:t,style:o,role:"status","aria-live":n,"aria-atomic":!0},r)}function wg(){const[e,t]=k.useState("");return{announce:k.useCallback(n=>{n!=null&&t(n)},[]),announcement:e}}const ni=k.createContext(null);function xg(e){const t=k.useContext(ni);k.useEffect(()=>{if(!t)throw new Error("useDndMonitor must be used within a children of <DndContext>");return t(e)},[e,t])}function kg(){const[e]=k.useState(()=>new Set),t=k.useCallback(n=>(e.add(n),()=>e.delete(n)),[e]);return[k.useCallback(n=>{let{type:o,event:a}=n;e.forEach(s=>{var l;return(l=s[o])==null?void 0:l.call(s,a)})},[e]),t]}const Cg={draggable:` 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. - `},Cg={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was moved over droppable area "+r.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was dropped over droppable area "+r.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function Sg(e){let{announcements:t=Cg,container:r,hiddenTextDescribedById:n,screenReaderInstructions:o=kg}=e;const{announce:a,announcement:s}=vg(),l=en("DndLiveRegion"),[f,h]=k.useState(!1);if(k.useEffect(()=>{h(!0)},[]),wg(k.useMemo(()=>({onDragStart(i){let{active:u}=i;a(t.onDragStart({active:u}))},onDragMove(i){let{active:u,over:g}=i;t.onDragMove&&a(t.onDragMove({active:u,over:g}))},onDragOver(i){let{active:u,over:g}=i;a(t.onDragOver({active:u,over:g}))},onDragEnd(i){let{active:u,over:g}=i;a(t.onDragEnd({active:u,over:g}))},onDragCancel(i){let{active:u,over:g}=i;a(t.onDragCancel({active:u,over:g}))}}),[a,t])),!f)return null;const c=k.createElement(k.Fragment,null,k.createElement(bg,{id:n,value:o.draggable}),k.createElement(yg,{id:l,announcement:s}));return r?Jt.createPortal(c,r):c}var et;(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"})(et||(et={}));function Hn(){}function ni(e,t){return k.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function _g(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return k.useMemo(()=>[...t].filter(n=>n!=null),[...t])}const Gt=Object.freeze({x:0,y:0});function oi(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function ai(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return r-n}function Eg(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return n-r}function si(e){let{left:t,top:r,height:n,width:o}=e;return[{x:t,y:r},{x:t+o,y:r},{x:t,y:r+n},{x:t+o,y:r+n}]}function li(e,t){if(!e||e.length===0)return null;const[r]=e;return r[t]}function ii(e,t,r){return t===void 0&&(t=e.left),r===void 0&&(r=e.top),{x:t+e.width*.5,y:r+e.height*.5}}const Tg=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=ii(t,t.left,t.top),a=[];for(const s of n){const{id:l}=s,f=r.get(l);if(f){const h=oi(ii(f),o);a.push({id:l,data:{droppableContainer:s,value:h}})}}return a.sort(ai)},Ng=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=si(t),a=[];for(const s of n){const{id:l}=s,f=r.get(l);if(f){const h=si(f),c=o.reduce((u,g,p)=>u+oi(h[p],g),0),i=Number((c/4).toFixed(4));a.push({id:l,data:{droppableContainer:s,value:i}})}}return a.sort(ai)};function Ag(e,t){const r=Math.max(t.top,e.top),n=Math.max(t.left,e.left),o=Math.min(t.left+t.width,e.left+e.width),a=Math.min(t.top+t.height,e.top+e.height),s=o-n,l=a-r;if(n<o&&r<a){const f=t.width*t.height,h=e.width*e.height,c=s*l,i=c/(f+h-c);return Number(i.toFixed(4))}return 0}const Mg=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=[];for(const a of n){const{id:s}=a,l=r.get(s);if(l){const f=Ag(l,t);f>0&&o.push({id:s,data:{droppableContainer:a,value:f}})}}return o.sort(Eg)};function Og(e,t,r){return{...e,scaleX:t&&r?t.width/r.width:1,scaleY:t&&r?t.height/r.height:1}}function ci(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Gt}function Ig(e){return function(r){for(var n=arguments.length,o=new Array(n>1?n-1:0),a=1;a<n;a++)o[a-1]=arguments[a];return o.reduce((s,l)=>({...s,top:s.top+e*l.y,bottom:s.bottom+e*l.y,left:s.left+e*l.x,right:s.right+e*l.x}),{...r})}}const Dg=Ig(1);function Lg(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 Pg(e,t,r){const n=Lg(t);if(!n)return e;const{scaleX:o,scaleY:a,x:s,y:l}=n,f=e.left-s-(1-o)*parseFloat(r),h=e.top-l-(1-a)*parseFloat(r.slice(r.indexOf(" ")+1)),c=o?e.width/o:e.width,i=a?e.height/a:e.height;return{width:c,height:i,top:h,right:f+c,bottom:h+i,left:f}}const $g={ignoreTransform:!1};function Ir(e,t){t===void 0&&(t=$g);let r=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:h,transformOrigin:c}=yt(e).getComputedStyle(e);h&&(r=Pg(r,h,c))}const{top:n,left:o,width:a,height:s,bottom:l,right:f}=r;return{top:n,left:o,width:a,height:s,bottom:l,right:f}}function di(e){return Ir(e,{ignoreTransform:!0})}function Fg(e){const t=e.innerWidth,r=e.innerHeight;return{top:0,left:0,right:t,bottom:r,width:t,height:r}}function Ug(e,t){return t===void 0&&(t=yt(e).getComputedStyle(e)),t.position==="fixed"}function zg(e,t){t===void 0&&(t=yt(e).getComputedStyle(e));const r=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(o=>{const a=t[o];return typeof a=="string"?r.test(a):!1})}function Gn(e,t){const r=[];function n(o){if(t!=null&&r.length>=t||!o)return r;if(sa(o)&&o.scrollingElement!=null&&!r.includes(o.scrollingElement))return r.push(o.scrollingElement),r;if(!Qr(o)||Rl(o)||r.includes(o))return r;const a=yt(e).getComputedStyle(o);return o!==e&&zg(o,a)&&r.push(o),Ug(o,a)?r:n(o.parentNode)}return e?n(e):r}function ui(e){const[t]=Gn(e,1);return t??null}function fa(e){return!Un||!e?null:Ar(e)?e:aa(e)?sa(e)||e===Mr(e).scrollingElement?window:Qr(e)?e:null:null}function fi(e){return Ar(e)?e.scrollX:e.scrollLeft}function gi(e){return Ar(e)?e.scrollY:e.scrollTop}function ga(e){return{x:fi(e),y:gi(e)}}var ot;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(ot||(ot={}));function hi(e){return!Un||!e?!1:e===document.scrollingElement}function pi(e){const t={x:0,y:0},r=hi(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},n={x:e.scrollWidth-r.width,y:e.scrollHeight-r.height},o=e.scrollTop<=t.y,a=e.scrollLeft<=t.x,s=e.scrollTop>=n.y,l=e.scrollLeft>=n.x;return{isTop:o,isLeft:a,isBottom:s,isRight:l,maxScroll:n,minScroll:t}}const Hg={x:.2,y:.2};function Gg(e,t,r,n,o){let{top:a,left:s,right:l,bottom:f}=r;n===void 0&&(n=10),o===void 0&&(o=Hg);const{isTop:h,isBottom:c,isLeft:i,isRight:u}=pi(e),g={x:0,y:0},p={x:0,y:0},m={height:t.height*o.y,width:t.width*o.x};return!h&&a<=t.top+m.height?(g.y=ot.Backward,p.y=n*Math.abs((t.top+m.height-a)/m.height)):!c&&f>=t.bottom-m.height&&(g.y=ot.Forward,p.y=n*Math.abs((t.bottom-m.height-f)/m.height)),!u&&l>=t.right-m.width?(g.x=ot.Forward,p.x=n*Math.abs((t.right-m.width-l)/m.width)):!i&&s<=t.left+m.width&&(g.x=ot.Backward,p.x=n*Math.abs((t.left+m.width-s)/m.width)),{direction:g,speed:p}}function jg(e){if(e===document.scrollingElement){const{innerWidth:a,innerHeight:s}=window;return{top:0,left:0,right:a,bottom:s,width:a,height:s}}const{top:t,left:r,right:n,bottom:o}=e.getBoundingClientRect();return{top:t,left:r,right:n,bottom:o,width:e.clientWidth,height:e.clientHeight}}function mi(e){return e.reduce((t,r)=>Or(t,ga(r)),Gt)}function Bg(e){return e.reduce((t,r)=>t+fi(r),0)}function Wg(e){return e.reduce((t,r)=>t+gi(r),0)}function Vg(e,t){if(t===void 0&&(t=Ir),!e)return;const{top:r,left:n,bottom:o,right:a}=t(e);ui(e)&&(o<=0||a<=0||r>=window.innerHeight||n>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const Kg=[["x",["left","right"],Bg],["y",["top","bottom"],Wg]];class ha{constructor(t,r){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const n=Gn(r),o=mi(n);this.rect={...t},this.width=t.width,this.height=t.height;for(const[a,s,l]of Kg)for(const f of s)Object.defineProperty(this,f,{get:()=>{const h=l(n),c=o[a]-h;return this.rect[f]+c},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class nn{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(r=>{var n;return(n=this.target)==null?void 0:n.removeEventListener(...r)})},this.target=t}add(t,r,n){var o;(o=this.target)==null||o.addEventListener(t,r,n),this.listeners.push([t,r,n])}}function Yg(e){const{EventTarget:t}=yt(e);return e instanceof t?e:Mr(e)}function pa(e,t){const r=Math.abs(e.x),n=Math.abs(e.y);return typeof t=="number"?Math.sqrt(r**2+n**2)>t:"x"in t&&"y"in t?r>t.x&&n>t.y:"x"in t?r>t.x:"y"in t?n>t.y:!1}var It;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(It||(It={}));function bi(e){e.preventDefault()}function Xg(e){e.stopPropagation()}var ze;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(ze||(ze={}));const yi={start:[ze.Space,ze.Enter],cancel:[ze.Esc],end:[ze.Space,ze.Enter,ze.Tab]},qg=(e,t)=>{let{currentCoordinates:r}=t;switch(e.code){case ze.Right:return{...r,x:r.x+25};case ze.Left:return{...r,x:r.x-25};case ze.Down:return{...r,y:r.y+25};case ze.Up:return{...r,y:r.y-25}}};class ma{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:r}}=t;this.props=t,this.listeners=new nn(Mr(r)),this.windowListeners=new nn(yt(r)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(It.Resize,this.handleCancel),this.windowListeners.add(It.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(It.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:r}=this.props,n=t.node.current;n&&Vg(n),r(Gt)}handleKeyDown(t){if(da(t)){const{active:r,context:n,options:o}=this.props,{keyboardCodes:a=yi,coordinateGetter:s=qg,scrollBehavior:l="smooth"}=o,{code:f}=t;if(a.end.includes(f)){this.handleEnd(t);return}if(a.cancel.includes(f)){this.handleCancel(t);return}const{collisionRect:h}=n.current,c=h?{x:h.left,y:h.top}:Gt;this.referenceCoordinates||(this.referenceCoordinates=c);const i=s(t,{active:r,context:n.current,currentCoordinates:c});if(i){const u=tn(i,c),g={x:0,y:0},{scrollableAncestors:p}=n.current;for(const m of p){const w=t.code,{isTop:C,isRight:O,isLeft:v,isBottom:E,maxScroll:I,minScroll:_}=pi(m),M=jg(m),x={x:Math.min(w===ze.Right?M.right-M.width/2:M.right,Math.max(w===ze.Right?M.left:M.left+M.width/2,i.x)),y:Math.min(w===ze.Down?M.bottom-M.height/2:M.bottom,Math.max(w===ze.Down?M.top:M.top+M.height/2,i.y))},y=w===ze.Right&&!O||w===ze.Left&&!v,T=w===ze.Down&&!E||w===ze.Up&&!C;if(y&&x.x!==i.x){const A=m.scrollLeft+u.x,P=w===ze.Right&&A<=I.x||w===ze.Left&&A>=_.x;if(P&&!u.y){m.scrollTo({left:A,behavior:l});return}P?g.x=m.scrollLeft-A:g.x=w===ze.Right?m.scrollLeft-I.x:m.scrollLeft-_.x,g.x&&m.scrollBy({left:-g.x,behavior:l});break}else if(T&&x.y!==i.y){const A=m.scrollTop+u.y,P=w===ze.Down&&A<=I.y||w===ze.Up&&A>=_.y;if(P&&!u.x){m.scrollTo({top:A,behavior:l});return}P?g.y=m.scrollTop-A:g.y=w===ze.Down?m.scrollTop-I.y:m.scrollTop-_.y,g.y&&m.scrollBy({top:-g.y,behavior:l});break}}this.handleMove(t,Or(tn(i,this.referenceCoordinates),g))}}}handleMove(t,r){const{onMove:n}=this.props;t.preventDefault(),n(r)}handleEnd(t){const{onEnd:r}=this.props;t.preventDefault(),this.detach(),r()}handleCancel(t){const{onCancel:r}=this.props;t.preventDefault(),this.detach(),r()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}ma.activators=[{eventName:"onKeyDown",handler:(e,t,r)=>{let{keyboardCodes:n=yi,onActivation:o}=t,{active:a}=r;const{code:s}=e.nativeEvent;if(n.start.includes(s)){const l=a.activatorNode.current;return l&&e.target!==l?!1:(e.preventDefault(),o?.({event:e.nativeEvent}),!0)}return!1}}];function vi(e){return!!(e&&"distance"in e)}function wi(e){return!!(e&&"delay"in e)}class ba{constructor(t,r,n){var o;n===void 0&&(n=Yg(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=r;const{event:a}=t,{target:s}=a;this.props=t,this.events=r,this.document=Mr(s),this.documentListeners=new nn(this.document),this.listeners=new nn(n),this.windowListeners=new nn(yt(s)),this.initialCoordinates=(o=ua(a))!=null?o:Gt,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:r,bypassActivationConstraint:n}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(It.Resize,this.handleCancel),this.windowListeners.add(It.DragStart,bi),this.windowListeners.add(It.VisibilityChange,this.handleCancel),this.windowListeners.add(It.ContextMenu,bi),this.documentListeners.add(It.Keydown,this.handleKeydown),r){if(n!=null&&n({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(wi(r)){this.timeoutId=setTimeout(this.handleStart,r.delay),this.handlePending(r);return}if(vi(r)){this.handlePending(r);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,r){const{active:n,onPending:o}=this.props;o(n,t,this.initialCoordinates,r)}handleStart(){const{initialCoordinates:t}=this,{onStart:r}=this.props;t&&(this.activated=!0,this.documentListeners.add(It.Click,Xg,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(It.SelectionChange,this.removeTextSelection),r(t))}handleMove(t){var r;const{activated:n,initialCoordinates:o,props:a}=this,{onMove:s,options:{activationConstraint:l}}=a;if(!o)return;const f=(r=ua(t))!=null?r:Gt,h=tn(o,f);if(!n&&l){if(vi(l)){if(l.tolerance!=null&&pa(h,l.tolerance))return this.handleCancel();if(pa(h,l.distance))return this.handleStart()}if(wi(l)&&pa(h,l.tolerance))return this.handleCancel();this.handlePending(l,h);return}t.cancelable&&t.preventDefault(),s(f)}handleEnd(){const{onAbort:t,onEnd:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleCancel(){const{onAbort:t,onCancel:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleKeydown(t){t.code===ze.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const Zg={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class ya extends ba{constructor(t){const{event:r}=t,n=Mr(r.target);super(t,Zg,n)}}ya.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return!r.isPrimary||r.button!==0?!1:(n?.({event:r}),!0)}}];const Qg={move:{name:"mousemove"},end:{name:"mouseup"}};var va;(function(e){e[e.RightClick=2]="RightClick"})(va||(va={}));class Jg extends ba{constructor(t){super(t,Qg,Mr(t.event.target))}}Jg.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return r.button===va.RightClick?!1:(n?.({event:r}),!0)}}];const wa={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class Rg extends ba{constructor(t){super(t,wa)}static setup(){return window.addEventListener(wa.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(wa.move.name,t)};function t(){}}}Rg.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;const{touches:o}=r;return o.length>1?!1:(n?.({event:r}),!0)}}];var on;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(on||(on={}));var jn;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(jn||(jn={}));function eh(e){let{acceleration:t,activator:r=on.Pointer,canScroll:n,draggingRect:o,enabled:a,interval:s=5,order:l=jn.TreeOrder,pointerCoordinates:f,scrollableAncestors:h,scrollableAncestorRects:c,delta:i,threshold:u}=e;const g=rh({delta:i,disabled:!a}),[p,m]=fg(),w=k.useRef({x:0,y:0}),C=k.useRef({x:0,y:0}),O=k.useMemo(()=>{switch(r){case on.Pointer:return f?{top:f.y,bottom:f.y,left:f.x,right:f.x}:null;case on.DraggableRect:return o}},[r,o,f]),v=k.useRef(null),E=k.useCallback(()=>{const _=v.current;if(!_)return;const M=w.current.x*C.current.x,x=w.current.y*C.current.y;_.scrollBy(M,x)},[]),I=k.useMemo(()=>l===jn.TreeOrder?[...h].reverse():h,[l,h]);k.useEffect(()=>{if(!a||!h.length||!O){m();return}for(const _ of I){if(n?.(_)===!1)continue;const M=h.indexOf(_),x=c[M];if(!x)continue;const{direction:y,speed:T}=Gg(_,x,O,t,u);for(const A of["x","y"])g[A][y[A]]||(T[A]=0,y[A]=0);if(T.x>0||T.y>0){m(),v.current=_,p(E,s),w.current=T,C.current=y;return}}w.current={x:0,y:0},C.current={x:0,y:0},m()},[t,E,n,m,a,s,JSON.stringify(O),JSON.stringify(g),p,h,I,c,JSON.stringify(u)])}const th={x:{[ot.Backward]:!1,[ot.Forward]:!1},y:{[ot.Backward]:!1,[ot.Forward]:!1}};function rh(e){let{delta:t,disabled:r}=e;const n=ia(t);return Rr(o=>{if(r||!n||!o)return th;const a={x:Math.sign(t.x-n.x),y:Math.sign(t.y-n.y)};return{x:{[ot.Backward]:o.x[ot.Backward]||a.x===-1,[ot.Forward]:o.x[ot.Forward]||a.x===1},y:{[ot.Backward]:o.y[ot.Backward]||a.y===-1,[ot.Forward]:o.y[ot.Forward]||a.y===1}}},[r,t,n])}function nh(e,t){const r=t!=null?e.get(t):void 0,n=r?r.node.current:null;return Rr(o=>{var a;return t==null?null:(a=n??o)!=null?a:null},[n,t])}function oh(e,t){return k.useMemo(()=>e.reduce((r,n)=>{const{sensor:o}=n,a=o.activators.map(s=>({eventName:s.eventName,handler:t(s.handler,n)}));return[...r,...a]},[]),[e,t])}var an;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(an||(an={}));var xa;(function(e){e.Optimized="optimized"})(xa||(xa={}));const xi=new Map;function ah(e,t){let{dragging:r,dependencies:n,config:o}=t;const[a,s]=k.useState(null),{frequency:l,measure:f,strategy:h}=o,c=k.useRef(e),i=w(),u=Jr(i),g=k.useCallback(function(C){C===void 0&&(C=[]),!u.current&&s(O=>O===null?C:O.concat(C.filter(v=>!O.includes(v))))},[u]),p=k.useRef(null),m=Rr(C=>{if(i&&!r)return xi;if(!C||C===xi||c.current!==e||a!=null){const O=new Map;for(let v of e){if(!v)continue;if(a&&a.length>0&&!a.includes(v.id)&&v.rect.current){O.set(v.id,v.rect.current);continue}const E=v.node.current,I=E?new ha(f(E),E):null;v.rect.current=I,I&&O.set(v.id,I)}return O}return C},[e,a,r,i,f]);return k.useEffect(()=>{c.current=e},[e]),k.useEffect(()=>{i||g()},[r,i]),k.useEffect(()=>{a&&a.length>0&&s(null)},[JSON.stringify(a)]),k.useEffect(()=>{i||typeof l!="number"||p.current!==null||(p.current=setTimeout(()=>{g(),p.current=null},l))},[l,i,g,...n]),{droppableRects:m,measureDroppableContainers:g,measuringScheduled:a!=null};function w(){switch(h){case an.Always:return!1;case an.BeforeDragging:return r;default:return!r}}}function ki(e,t){return Rr(r=>e?r||(typeof t=="function"?t(e):e):null,[t,e])}function sh(e,t){return ki(e,t)}function lh(e){let{callback:t,disabled:r}=e;const n=la(t),o=k.useMemo(()=>{if(r||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:a}=window;return new a(n)},[n,r]);return k.useEffect(()=>()=>o?.disconnect(),[o]),o}function Bn(e){let{callback:t,disabled:r}=e;const n=la(t),o=k.useMemo(()=>{if(r||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:a}=window;return new a(n)},[r]);return k.useEffect(()=>()=>o?.disconnect(),[o]),o}function ih(e){return new ha(Ir(e),e)}function Ci(e,t,r){t===void 0&&(t=ih);const[n,o]=k.useState(null);function a(){o(f=>{if(!e)return null;if(e.isConnected===!1){var h;return(h=f??r)!=null?h:null}const c=t(e);return JSON.stringify(f)===JSON.stringify(c)?f:c})}const s=lh({callback(f){if(e)for(const h of f){const{type:c,target:i}=h;if(c==="childList"&&i instanceof HTMLElement&&i.contains(e)){a();break}}}}),l=Bn({callback:a});return Yt(()=>{a(),e?(l?.observe(e),s?.observe(document.body,{childList:!0,subtree:!0})):(l?.disconnect(),s?.disconnect())},[e]),n}function ch(e){const t=ki(e);return ci(e,t)}const Si=[];function dh(e){const t=k.useRef(e),r=Rr(n=>e?n&&n!==Si&&e&&t.current&&e.parentNode===t.current.parentNode?n:Gn(e):Si,[e]);return k.useEffect(()=>{t.current=e},[e]),r}function uh(e){const[t,r]=k.useState(null),n=k.useRef(e),o=k.useCallback(a=>{const s=fa(a.target);s&&r(l=>l?(l.set(s,ga(s)),new Map(l)):null)},[]);return k.useEffect(()=>{const a=n.current;if(e!==a){s(a);const l=e.map(f=>{const h=fa(f);return h?(h.addEventListener("scroll",o,{passive:!0}),[h,ga(h)]):null}).filter(f=>f!=null);r(l.length?new Map(l):null),n.current=e}return()=>{s(e),s(a)};function s(l){l.forEach(f=>{const h=fa(f);h?.removeEventListener("scroll",o)})}},[o,e]),k.useMemo(()=>e.length?t?Array.from(t.values()).reduce((a,s)=>Or(a,s),Gt):mi(e):Gt,[e,t])}function _i(e,t){t===void 0&&(t=[]);const r=k.useRef(null);return k.useEffect(()=>{r.current=null},t),k.useEffect(()=>{const n=e!==Gt;n&&!r.current&&(r.current=e),!n&&r.current&&(r.current=null)},[e]),r.current?tn(e,r.current):Gt}function fh(e){k.useEffect(()=>{if(!Un)return;const t=e.map(r=>{let{sensor:n}=r;return n.setup==null?void 0:n.setup()});return()=>{for(const r of t)r?.()}},e.map(t=>{let{sensor:r}=t;return r}))}function gh(e,t){return k.useMemo(()=>e.reduce((r,n)=>{let{eventName:o,handler:a}=n;return r[o]=s=>{a(s,t)},r},{}),[e,t])}function Ei(e){return k.useMemo(()=>e?Fg(e):null,[e])}const Ti=[];function hh(e,t){t===void 0&&(t=Ir);const[r]=e,n=Ei(r?yt(r):null),[o,a]=k.useState(Ti);function s(){a(()=>e.length?e.map(f=>hi(f)?n:new ha(t(f),f)):Ti)}const l=Bn({callback:s});return Yt(()=>{l?.disconnect(),s(),e.forEach(f=>l?.observe(f))},[e]),o}function ph(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return Qr(t)?t:e}function mh(e){let{measure:t}=e;const[r,n]=k.useState(null),o=k.useCallback(h=>{for(const{target:c}of h)if(Qr(c)){n(i=>{const u=t(c);return i?{...i,width:u.width,height:u.height}:u});break}},[t]),a=Bn({callback:o}),s=k.useCallback(h=>{const c=ph(h);a?.disconnect(),c&&a?.observe(c),n(c?t(c):null)},[t,a]),[l,f]=zn(s);return k.useMemo(()=>({nodeRef:l,rect:r,setRef:f}),[r,l,f])}const bh=[{sensor:ya,options:{}},{sensor:ma,options:{}}],yh={current:{}},Wn={draggable:{measure:di},droppable:{measure:di,strategy:an.WhileDragging,frequency:xa.Optimized},dragOverlay:{measure:Ir}};class sn extends Map{get(t){var r;return t!=null&&(r=super.get(t))!=null?r:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:r}=t;return!r})}getNodeFor(t){var r,n;return(r=(n=this.get(t))==null?void 0:n.node.current)!=null?r:void 0}}const vh={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new sn,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:Hn},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:Wn,measureDroppableContainers:Hn,windowRect:null,measuringScheduled:!1},wh={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:Hn,draggableNodes:new Map,over:null,measureDroppableContainers:Hn},Vn=k.createContext(wh),Ni=k.createContext(vh);function xh(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new sn}}}function kh(e,t){switch(t.type){case et.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case et.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case et.DragEnd:case et.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case et.RegisterDroppable:{const{element:r}=t,{id:n}=r,o=new sn(e.droppable.containers);return o.set(n,r),{...e,droppable:{...e.droppable,containers:o}}}case et.SetDroppableDisabled:{const{id:r,key:n,disabled:o}=t,a=e.droppable.containers.get(r);if(!a||n!==a.key)return e;const s=new sn(e.droppable.containers);return s.set(r,{...a,disabled:o}),{...e,droppable:{...e.droppable,containers:s}}}case et.UnregisterDroppable:{const{id:r,key:n}=t,o=e.droppable.containers.get(r);if(!o||n!==o.key)return e;const a=new sn(e.droppable.containers);return a.delete(r),{...e,droppable:{...e.droppable,containers:a}}}default:return e}}function Ch(e){let{disabled:t}=e;const{active:r,activatorEvent:n,draggableNodes:o}=k.useContext(Vn),a=ia(n),s=ia(r?.id);return k.useEffect(()=>{if(!t&&!n&&a&&s!=null){if(!da(a)||document.activeElement===a.target)return;const l=o.get(s);if(!l)return;const{activatorNode:f,node:h}=l;if(!f.current&&!h.current)return;requestAnimationFrame(()=>{for(const c of[f.current,h.current]){if(!c)continue;const i=pg(c);if(i){i.focus();break}}})}},[n,t,o,s,a]),null}function Sh(e,t){let{transform:r,...n}=t;return e!=null&&e.length?e.reduce((o,a)=>a({transform:o,...n}),r):r}function _h(e){return k.useMemo(()=>({draggable:{...Wn.draggable,...e?.draggable},droppable:{...Wn.droppable,...e?.droppable},dragOverlay:{...Wn.dragOverlay,...e?.dragOverlay}}),[e?.draggable,e?.droppable,e?.dragOverlay])}function Eh(e){let{activeNode:t,measure:r,initialRect:n,config:o=!0}=e;const a=k.useRef(!1),{x:s,y:l}=typeof o=="boolean"?{x:o,y:o}:o;Yt(()=>{if(!s&&!l||!t){a.current=!1;return}if(a.current||!n)return;const h=t?.node.current;if(!h||h.isConnected===!1)return;const c=r(h),i=ci(c,n);if(s||(i.x=0),l||(i.y=0),a.current=!0,Math.abs(i.x)>0||Math.abs(i.y)>0){const u=ui(h);u&&u.scrollBy({top:i.y,left:i.x})}},[t,s,l,n,r])}const Ai=k.createContext({...Gt,scaleX:1,scaleY:1});var tr;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(tr||(tr={}));const Th=k.memo(function(t){var r,n,o,a;let{id:s,accessibility:l,autoScroll:f=!0,children:h,sensors:c=bh,collisionDetection:i=Mg,measuring:u,modifiers:g,...p}=t;const m=k.useReducer(kh,void 0,xh),[w,C]=m,[O,v]=xg(),[E,I]=k.useState(tr.Uninitialized),_=E===tr.Initialized,{draggable:{active:M,nodes:x,translate:y},droppable:{containers:T}}=w,A=M!=null?x.get(M):null,P=k.useRef({initial:null,translated:null}),D=k.useMemo(()=>{var Ee;return M!=null?{id:M,data:(Ee=A?.data)!=null?Ee:yh,rect:P}:null},[M,A]),L=k.useRef(null),[U,S]=k.useState(null),[b,F]=k.useState(null),$=Jr(p,Object.values(p)),G=en("DndDescribedBy",s),Y=k.useMemo(()=>T.getEnabled(),[T]),B=_h(u),{droppableRects:K,measureDroppableContainers:j,measuringScheduled:H}=ah(Y,{dragging:_,dependencies:[y.x,y.y],config:B.droppable}),q=nh(x,M),ue=k.useMemo(()=>b?ua(b):null,[b]),te=je(),fe=sh(q,B.draggable.measure);Eh({activeNode:M!=null?x.get(M):null,config:te.layoutShiftCompensation,initialRect:fe,measure:B.draggable.measure});const ee=Ci(q,B.draggable.measure,fe),ke=Ci(q?q.parentElement:null),me=k.useRef({activatorEvent:null,active:null,activeNode:q,collisionRect:null,collisions:null,droppableRects:K,draggableNodes:x,draggingNode:null,draggingNodeRect:null,droppableContainers:T,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),Ge=T.getNodeFor((r=me.current.over)==null?void 0:r.id),De=mh({measure:B.dragOverlay.measure}),ye=(n=De.nodeRef.current)!=null?n:q,Me=_?(o=De.rect)!=null?o:ee:null,Ce=!!(De.nodeRef.current&&De.rect),we=ch(Ce?null:ee),Fe=Ei(ye?yt(ye):null),Le=dh(_?Ge??q:null),V=hh(Le),re=Sh(g,{transform:{x:y.x-we.x,y:y.y-we.y,scaleX:1,scaleY:1},activatorEvent:b,active:D,activeNodeRect:ee,containerNodeRect:ke,draggingNodeRect:Me,over:me.current.over,overlayNodeRect:De.rect,scrollableAncestors:Le,scrollableAncestorRects:V,windowRect:Fe}),oe=ue?Or(ue,y):null,ae=uh(Le),be=_i(ae),xe=_i(ae,[ee]),He=Or(re,be),Pe=Me?Dg(Me,re):null,Ve=D&&Pe?i({active:D,collisionRect:Pe,droppableRects:K,droppableContainers:Y,pointerCoordinates:oe}):null,Oe=li(Ve,"id"),[X,se]=k.useState(null),ie=Ce?re:Or(re,xe),le=Og(ie,(a=X?.rect)!=null?a:null,ee),ne=k.useRef(null),Te=k.useCallback((Ee,he)=>{let{sensor:Ne,options:Ke}=he;if(L.current==null)return;const $e=x.get(L.current);if(!$e)return;const Be=Ee.nativeEvent,Ze=new Ne({active:L.current,activeNode:$e,event:Be,options:Ke,context:me,onAbort(qe){if(!x.get(qe))return;const{onDragAbort:_t}=$.current,Bt={id:qe};_t?.(Bt),O({type:"onDragAbort",event:Bt})},onPending(qe,jt,_t,Bt){if(!x.get(qe))return;const{onDragPending:Et}=$.current,Wt={id:qe,constraint:jt,initialCoordinates:_t,offset:Bt};Et?.(Wt),O({type:"onDragPending",event:Wt})},onStart(qe){const jt=L.current;if(jt==null)return;const _t=x.get(jt);if(!_t)return;const{onDragStart:Bt}=$.current,dt={activatorEvent:Be,active:{id:jt,data:_t.data,rect:P}};Jt.unstable_batchedUpdates(()=>{Bt?.(dt),I(tr.Initializing),C({type:et.DragStart,initialCoordinates:qe,active:jt}),O({type:"onDragStart",event:dt}),S(ne.current),F(Be)})},onMove(qe){C({type:et.DragMove,coordinates:qe})},onEnd:qt(et.DragEnd),onCancel:qt(et.DragCancel)});ne.current=Ze;function qt(qe){return async function(){const{active:_t,collisions:Bt,over:dt,scrollAdjustedTranslate:Et}=me.current;let Wt=null;if(_t&&Et){const{cancelDrop:nr}=$.current;Wt={activatorEvent:Be,active:_t,collisions:Bt,delta:Et,over:dt},qe===et.DragEnd&&typeof nr=="function"&&await Promise.resolve(nr(Wt))&&(qe=et.DragCancel)}L.current=null,Jt.unstable_batchedUpdates(()=>{C({type:qe}),I(tr.Uninitialized),se(null),S(null),F(null),ne.current=null;const nr=qe===et.DragEnd?"onDragEnd":"onDragCancel";if(Wt){const mr=$.current[nr];mr?.(Wt),O({type:nr,event:Wt})}})}}},[x]),ve=k.useCallback((Ee,he)=>(Ne,Ke)=>{const $e=Ne.nativeEvent,Be=x.get(Ke);if(L.current!==null||!Be||$e.dndKit||$e.defaultPrevented)return;const Ze={active:Be};Ee(Ne,he.options,Ze)===!0&&($e.dndKit={capturedBy:he.sensor},L.current=Ke,Te(Ne,he))},[x,Te]),Se=oh(c,ve);fh(c),Yt(()=>{ee&&E===tr.Initializing&&I(tr.Initialized)},[ee,E]),k.useEffect(()=>{const{onDragMove:Ee}=$.current,{active:he,activatorEvent:Ne,collisions:Ke,over:$e}=me.current;if(!he||!Ne)return;const Be={active:he,activatorEvent:Ne,collisions:Ke,delta:{x:He.x,y:He.y},over:$e};Jt.unstable_batchedUpdates(()=>{Ee?.(Be),O({type:"onDragMove",event:Be})})},[He.x,He.y]),k.useEffect(()=>{const{active:Ee,activatorEvent:he,collisions:Ne,droppableContainers:Ke,scrollAdjustedTranslate:$e}=me.current;if(!Ee||L.current==null||!he||!$e)return;const{onDragOver:Be}=$.current,Ze=Ke.get(Oe),qt=Ze&&Ze.rect.current?{id:Ze.id,rect:Ze.rect.current,data:Ze.data,disabled:Ze.disabled}:null,qe={active:Ee,activatorEvent:he,collisions:Ne,delta:{x:$e.x,y:$e.y},over:qt};Jt.unstable_batchedUpdates(()=>{se(qt),Be?.(qe),O({type:"onDragOver",event:qe})})},[Oe]),Yt(()=>{me.current={activatorEvent:b,active:D,activeNode:q,collisionRect:Pe,collisions:Ve,droppableRects:K,draggableNodes:x,draggingNode:ye,draggingNodeRect:Me,droppableContainers:T,over:X,scrollableAncestors:Le,scrollAdjustedTranslate:He},P.current={initial:Me,translated:Pe}},[D,q,Ve,Pe,x,ye,Me,K,T,X,Le,He]),eh({...te,delta:y,draggingRect:Pe,pointerCoordinates:oe,scrollableAncestors:Le,scrollableAncestorRects:V});const _e=k.useMemo(()=>({active:D,activeNode:q,activeNodeRect:ee,activatorEvent:b,collisions:Ve,containerNodeRect:ke,dragOverlay:De,draggableNodes:x,droppableContainers:T,droppableRects:K,over:X,measureDroppableContainers:j,scrollableAncestors:Le,scrollableAncestorRects:V,measuringConfiguration:B,measuringScheduled:H,windowRect:Fe}),[D,q,ee,b,Ve,ke,De,x,T,K,X,j,Le,V,B,H,Fe]),Ue=k.useMemo(()=>({activatorEvent:b,activators:Se,active:D,activeNodeRect:ee,ariaDescribedById:{draggable:G},dispatch:C,draggableNodes:x,over:X,measureDroppableContainers:j}),[b,Se,D,ee,C,G,x,X,j]);return k.createElement(ri.Provider,{value:v},k.createElement(Vn.Provider,{value:Ue},k.createElement(Ni.Provider,{value:_e},k.createElement(Ai.Provider,{value:le},h)),k.createElement(Ch,{disabled:l?.restoreFocus===!1})),k.createElement(Sg,{...l,hiddenTextDescribedById:G}));function je(){const Ee=U?.autoScrollEnabled===!1,he=typeof f=="object"?f.enabled===!1:f===!1,Ne=_&&!Ee&&!he;return typeof f=="object"?{...f,enabled:Ne}:{enabled:Ne}}}),Nh=k.createContext(null),Mi="button",Ah="Draggable";function Mh(e){let{id:t,data:r,disabled:n=!1,attributes:o}=e;const a=en(Ah),{activators:s,activatorEvent:l,active:f,activeNodeRect:h,ariaDescribedById:c,draggableNodes:i,over:u}=k.useContext(Vn),{role:g=Mi,roleDescription:p="draggable",tabIndex:m=0}=o??{},w=f?.id===t,C=k.useContext(w?Ai:Nh),[O,v]=zn(),[E,I]=zn(),_=gh(s,t),M=Jr(r);Yt(()=>(i.set(t,{id:t,key:a,node:O,activatorNode:E,data:M}),()=>{const y=i.get(t);y&&y.key===a&&i.delete(t)}),[i,t]);const x=k.useMemo(()=>({role:g,tabIndex:m,"aria-disabled":n,"aria-pressed":w&&g===Mi?!0:void 0,"aria-roledescription":p,"aria-describedby":c.draggable}),[n,g,m,w,p,c.draggable]);return{active:f,activatorEvent:l,activeNodeRect:h,attributes:x,isDragging:w,listeners:n?void 0:_,node:O,over:u,setNodeRef:v,setActivatorNodeRef:I,transform:C}}function Oh(){return k.useContext(Ni)}const Ih="Droppable",Dh={timeout:25};function Lh(e){let{data:t,disabled:r=!1,id:n,resizeObserverConfig:o}=e;const a=en(Ih),{active:s,dispatch:l,over:f,measureDroppableContainers:h}=k.useContext(Vn),c=k.useRef({disabled:r}),i=k.useRef(!1),u=k.useRef(null),g=k.useRef(null),{disabled:p,updateMeasurementsFor:m,timeout:w}={...Dh,...o},C=Jr(m??n),O=k.useCallback(()=>{if(!i.current){i.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{h(Array.isArray(C.current)?C.current:[C.current]),g.current=null},w)},[w]),v=Bn({callback:O,disabled:p||!s}),E=k.useCallback((x,y)=>{v&&(y&&(v.unobserve(y),i.current=!1),x&&v.observe(x))},[v]),[I,_]=zn(E),M=Jr(t);return k.useEffect(()=>{!v||!I.current||(v.disconnect(),i.current=!1,v.observe(I.current))},[I,v]),k.useEffect(()=>(l({type:et.RegisterDroppable,element:{id:n,key:a,disabled:r,node:I,rect:u,data:M}}),()=>l({type:et.UnregisterDroppable,key:a,id:n})),[n]),k.useEffect(()=>{r!==c.current.disabled&&(l({type:et.SetDroppableDisabled,id:n,key:a,disabled:r}),c.current.disabled=r)},[n,a,r,l]),{active:s,rect:u,isOver:f?.id===n,node:I,over:f,setNodeRef:_}}function ka(e,t,r){const n=e.slice();return n.splice(r<0?n.length+r:r,0,n.splice(t,1)[0]),n}function Ph(e,t){return e.reduce((r,n,o)=>{const a=t.get(n);return a&&(r[o]=a),r},Array(e.length))}function Kn(e){return e!==null&&e>=0}function $h(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(e[r]!==t[r])return!1;return!0}function Fh(e){return typeof e=="boolean"?{draggable:e,droppable:e}:e}const Yn=e=>{let{rects:t,activeIndex:r,overIndex:n,index:o}=e;const a=ka(t,n,r),s=t[o],l=a[o];return!l||!s?null:{x:l.left-s.left,y:l.top-s.top,scaleX:l.width/s.width,scaleY:l.height/s.height}},Oi="Sortable",Ii=k.createContext({activeIndex:-1,containerId:Oi,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:Yn,disabled:{draggable:!1,droppable:!1}});function Di(e){let{children:t,id:r,items:n,strategy:o=Yn,disabled:a=!1}=e;const{active:s,dragOverlay:l,droppableRects:f,over:h,measureDroppableContainers:c}=Oh(),i=en(Oi,r),u=l.rect!==null,g=k.useMemo(()=>n.map(_=>typeof _=="object"&&"id"in _?_.id:_),[n]),p=s!=null,m=s?g.indexOf(s.id):-1,w=h?g.indexOf(h.id):-1,C=k.useRef(g),O=!$h(g,C.current),v=w!==-1&&m===-1||O,E=Fh(a);Yt(()=>{O&&p&&c(g)},[O,g,p,c]),k.useEffect(()=>{C.current=g},[g]);const I=k.useMemo(()=>({activeIndex:m,containerId:i,disabled:E,disableTransforms:v,items:g,overIndex:w,useDragOverlay:u,sortedRects:Ph(g,f),strategy:o}),[m,i,E.draggable,E.droppable,v,g,w,f,u,o]);return k.createElement(Ii.Provider,{value:I},t)}const Li=e=>{let{id:t,items:r,activeIndex:n,overIndex:o}=e;return ka(r,n,o).indexOf(t)},Pi=e=>{let{containerId:t,isSorting:r,wasDragging:n,index:o,items:a,newIndex:s,previousItems:l,previousContainerId:f,transition:h}=e;return!h||!n||l!==a&&o===s?!1:r?!0:s!==o&&t===f},Uh={duration:200,easing:"ease"},$i="transform",zh=rn.Transition.toString({property:$i,duration:0,easing:"linear"}),Hh={roleDescription:"sortable"};function Gh(e){let{disabled:t,index:r,node:n,rect:o}=e;const[a,s]=k.useState(null),l=k.useRef(r);return Yt(()=>{if(!t&&r!==l.current&&n.current){const f=o.current;if(f){const h=Ir(n.current,{ignoreTransform:!0}),c={x:f.left-h.left,y:f.top-h.top,scaleX:f.width/h.width,scaleY:f.height/h.height};(c.x||c.y)&&s(c)}}r!==l.current&&(l.current=r)},[t,r,n,o]),k.useEffect(()=>{a&&s(null)},[a]),a}function Fi(e){let{animateLayoutChanges:t=Pi,attributes:r,disabled:n,data:o,getNewIndex:a=Li,id:s,strategy:l,resizeObserverConfig:f,transition:h=Uh}=e;const{items:c,containerId:i,activeIndex:u,disabled:g,disableTransforms:p,sortedRects:m,overIndex:w,useDragOverlay:C,strategy:O}=k.useContext(Ii),v=jh(n,g),E=c.indexOf(s),I=k.useMemo(()=>({sortable:{containerId:i,index:E,items:c},...o}),[i,o,E,c]),_=k.useMemo(()=>c.slice(c.indexOf(s)),[c,s]),{rect:M,node:x,isOver:y,setNodeRef:T}=Lh({id:s,data:I,disabled:v.droppable,resizeObserverConfig:{updateMeasurementsFor:_,...f}}),{active:A,activatorEvent:P,activeNodeRect:D,attributes:L,setNodeRef:U,listeners:S,isDragging:b,over:F,setActivatorNodeRef:$,transform:G}=Mh({id:s,data:I,attributes:{...Hh,...r},disabled:v.draggable}),Y=ug(T,U),B=!!A,K=B&&!p&&Kn(u)&&Kn(w),j=!C&&b,H=j&&K?G:null,ue=K?H??(l??O)({rects:m,activeNodeRect:D,activeIndex:u,overIndex:w,index:E}):null,te=Kn(u)&&Kn(w)?a({id:s,items:c,activeIndex:u,overIndex:w}):E,fe=A?.id,ee=k.useRef({activeId:fe,items:c,newIndex:te,containerId:i}),ke=c!==ee.current.items,me=t({active:A,containerId:i,isDragging:b,isSorting:B,id:s,index:E,items:c,newIndex:ee.current.newIndex,previousItems:ee.current.items,previousContainerId:ee.current.containerId,transition:h,wasDragging:ee.current.activeId!=null}),Ge=Gh({disabled:!me,index:E,node:x,rect:M});return k.useEffect(()=>{B&&ee.current.newIndex!==te&&(ee.current.newIndex=te),i!==ee.current.containerId&&(ee.current.containerId=i),c!==ee.current.items&&(ee.current.items=c)},[B,te,i,c]),k.useEffect(()=>{if(fe===ee.current.activeId)return;if(fe!=null&&ee.current.activeId==null){ee.current.activeId=fe;return}const ye=setTimeout(()=>{ee.current.activeId=fe},50);return()=>clearTimeout(ye)},[fe]),{active:A,activeIndex:u,attributes:L,data:I,rect:M,index:E,newIndex:te,items:c,isOver:y,isSorting:B,isDragging:b,listeners:S,node:x,overIndex:w,over:F,setNodeRef:Y,setActivatorNodeRef:$,setDroppableNodeRef:T,setDraggableNodeRef:U,transform:Ge??ue,transition:De()};function De(){if(Ge||ke&&ee.current.newIndex===E)return zh;if(!(j&&!da(P)||!h)&&(B||me))return rn.Transition.toString({...h,property:$i})}}function jh(e,t){var r,n;return typeof e=="boolean"?{draggable:e,droppable:!1}:{draggable:(r=e?.draggable)!=null?r:t.draggable,droppable:(n=e?.droppable)!=null?n:t.droppable}}function ln(e){if(!e)return!1;const t=e.data.current;return!!(t&&"sortable"in t&&typeof t.sortable=="object"&&"containerId"in t.sortable&&"items"in t.sortable&&"index"in t.sortable)}const Bh=[ze.Down,ze.Right,ze.Up,ze.Left],Ui=(e,t)=>{let{context:{active:r,collisionRect:n,droppableRects:o,droppableContainers:a,over:s,scrollableAncestors:l}}=t;if(Bh.includes(e.code)){if(e.preventDefault(),!r||!n)return;const f=[];a.getEnabled().forEach(i=>{if(!i||i!=null&&i.disabled)return;const u=o.get(i.id);if(u)switch(e.code){case ze.Down:n.top<u.top&&f.push(i);break;case ze.Up:n.top>u.top&&f.push(i);break;case ze.Left:n.left>u.left&&f.push(i);break;case ze.Right:n.left<u.left&&f.push(i);break}});const h=Ng({collisionRect:n,droppableRects:o,droppableContainers:f});let c=li(h,"id");if(c===s?.id&&h.length>1&&(c=h[1].id),c!=null){const i=a.get(r.id),u=a.get(c),g=u?o.get(u.id):null,p=u?.node.current;if(p&&g&&i&&u){const w=Gn(p).some((_,M)=>l[M]!==_),C=zi(i,u),O=Wh(i,u),v=w||!C?{x:0,y:0}:{x:O?n.width-g.width:0,y:O?n.height-g.height:0},E={x:g.left,y:g.top};return v.x&&v.y?E:tn(E,v)}}}};function zi(e,t){return!ln(e)||!ln(t)?!1:e.data.current.sortable.containerId===t.data.current.sortable.containerId}function Wh(e,t){return!ln(e)||!ln(t)||!zi(e,t)?!1:e.data.current.sortable.index<t.data.current.sortable.index}const Vh=Object.freeze(Object.defineProperty({__proto__:null,SortableContext:Di,arrayMove:ka,defaultAnimateLayoutChanges:Pi,defaultNewIndexGetter:Li,hasSortableData:ln,rectSortingStrategy:Yn,sortableKeyboardCoordinates:Ui,useSortable:Fi},Symbol.toStringTag,{value:"Module"})),Kh=window.G7Core,Hi=({file:e,onRemove:t,onRetry:r,onImageClick:n,onDownload:o,isPrimary:a,onPrimaryClick:s})=>{const{attributes:l,listeners:f,setNodeRef:h,transform:c,transition:i,isDragging:u}=Fi({id:"hash"in e?e.hash:e.id}),g={transform:rn.Transform.toString(c),transition:i,opacity:u?.5:1},p="status"in e&&e.status!==void 0,m=p?e.status:"done",w=m==="uploading",C=m==="compressing",O=m==="error",v=p?e.progress:100,E=p?e.preview:void 0,I=p?e.mime_type?.startsWith("image/")??!1:e.is_image,[_,M]=k.useState(void 0),x=!p&&I?e.download_url:void 0;k.useEffect(()=>{if(!x){M(void 0);return}let T=!0,A;return(async()=>{try{const D=await Kh.api.get(x,{responseType:"blob"});T&&D&&(A=URL.createObjectURL(D),M(A))}catch(D){console.error("Failed to load authenticated image:",D)}})(),()=>{T=!1,A&&URL.revokeObjectURL(A)}},[x]);const y=_;return d.jsxs(W,{ref:h,style:g,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[d.jsxs(W,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${I||!p?"cursor-pointer":""}`,onClick:()=>{w||C||(I&&n?n():!p&&o&&o())},children:[E||y?d.jsx(Lt,{src:E||y,alt:e.original_filename,className:"w-full h-full object-cover"}):d.jsx(Ct,{className:`fa-solid ${Zl(e.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(w||C)&&d.jsxs(W,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[d.jsx(W,{className:"w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden",children:d.jsx(W,{className:"h-full bg-blue-500 transition-all",style:{width:`${v}%`}})}),d.jsxs(R,{className:"text-white text-xs mt-1",children:[v,"%"]})]}),O&&d.jsx(W,{className:"absolute inset-0 bg-red-500/50 flex items-center justify-center",children:d.jsx(Ct,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),s&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),s()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${a?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:Re(a?"attachment.primary_image":"attachment.set_as_primary"),children:d.jsx(Ct,{className:"fa-solid fa-star text-xs"})}),d.jsxs(W,{className:"absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100",children:[d.jsx(Q,{type:"button",...l,...f,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:Re("attachment.drag_to_reorder"),onClick:T=>T.stopPropagation(),children:d.jsx(Ct,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&o&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),o()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:Re("common.download"),children:d.jsx(Ct,{className:"fa-solid fa-download text-sm"})}),O&&r&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),r()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:Re("common.retry"),children:d.jsx(Ct,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),t()},disabled:w||C,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:Re("common.delete"),children:d.jsx(Ct,{className:"fa-solid fa-times text-sm"})})]})]}),d.jsxs(W,{className:`p-2 ${!p&&o?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&o&&o()},children:[d.jsx(Dt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:e.original_filename,children:e.original_filename}),d.jsx(Dt,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.size_formatted})]})]})},Gi=({allItems:e,canAddMore:t,totalCount:r,maxFiles:n,maxSize:o,accept:a,onRemove:s,onRetry:l,onImageClick:f,onDownload:h,onDragEnd:c,onAddClick:i,enablePrimarySelection:u,primaryFileId:g,onPrimaryChange:p})=>{const m=_g(ni(ya,{activationConstraint:{distance:8}}),ni(ma,{coordinateGetter:Ui}));return d.jsxs(W,{className:"p-3",children:[d.jsx(Th,{sensors:m,collisionDetection:Tg,onDragEnd:c,children:d.jsx(Di,{items:e.map(w=>"hash"in w?w.hash:w.id),strategy:Yn,children:d.jsxs(W,{className:"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3",children:[e.map(w=>{const C="hash"in w,O=C?w.is_image:w.mime_type?.startsWith("image/")??!1;return d.jsx(Hi,{file:w,onRemove:()=>s(w),onRetry:"status"in w&&w.status==="error"?()=>l(w):void 0,onImageClick:O?()=>f(w):void 0,onDownload:C?()=>h(w):void 0,isPrimary:u&&C&&(w.hash||w.id)===g,onPrimaryClick:u&&C?()=>p?.(w.hash||w.id):void 0},C?w.hash:w.id)}),t&&d.jsxs(W,{className:"aspect-square flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:i,children:[d.jsx(Ct,{className:"fa-solid fa-plus text-2xl text-gray-400 dark:text-gray-500 mb-1"}),d.jsx(R,{className:"text-xs text-gray-500 dark:text-gray-400",children:Re("common.add")})]})]})})}),d.jsxs(W,{className:"mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center text-xs text-gray-500 dark:text-gray-400",children:[d.jsx(R,{children:Re("attachment.attached_count",{count:r,max:n})}),d.jsxs(R,{children:[Re("attachment.max_size",{size:o}),a&&` (${a})`]})]})]})},Yh=[],Xh=[],Xn=k.forwardRef(({uploadTriggerEvent:e,attachmentableType:t,attachmentableId:r,collection:n="default",maxFiles:o=10,maxSize:a=10,maxConcurrentUploads:s=3,accept:l,imageCompression:f,roleIds:h=Xh,autoUpload:c=!0,onFilesChange:i,onChange:u,onUploadComplete:g,onUploadError:p,onRemove:m,onReorder:w,initialFiles:C=Yh,className:O="",confirmBeforeRemove:v=!1,confirmRemoveTitle:E,confirmRemoveMessage:I,apiEndpoints:_,uploadParams:M,enablePrimarySelection:x=!1,primaryFileId:y,onPrimaryChange:T,id:A,editorAttrs:P},D)=>{const L={upload:_?.upload??"/api/attachments",delete:_?.delete??"/api/attachments/:id",reorder:_?.reorder??"/api/attachments/reorder"},U=k.useRef(u??i);U.current=u??i;const S=k.useCallback(G=>{const Y=U.current;if(!Y)return;Y({target:{value:G,files:G},preventDefault:()=>{},stopPropagation:()=>{}})},[]),b=Ql({attachmentableType:t,attachmentableId:r,collection:n,maxFiles:o,maxSize:a,maxConcurrentUploads:s,accept:l,imageCompression:f,roleIds:h,autoUpload:c,initialFiles:C,endpoints:L,onFilesChange:S,onUploadComplete:g,onUploadError:p,onRemove:m,onReorder:w,uploadTriggerEvent:e,confirmBeforeRemove:v,uploadParams:M});k.useImperativeHandle(D,()=>({uploadAll:b.handleUploadAll,clear:b.clear,getPendingFiles:b.getPendingFiles}));const F=k.useMemo(()=>b.imageFiles.map(G=>{const Y=!("hash"in G),B=G,K=G;return Y?{src:B.preview||"",filename:B.original_filename,title:B.original_filename,downloadRequiresAuth:!1}:{src:b.authenticatedImageUrls.get(K.id)||K.download_url,downloadUrl:K.download_url,filename:K.original_filename,title:K.original_filename,downloadRequiresAuth:!0}}),[b.imageFiles,b.authenticatedImageUrls]),$=k.useMemo(()=>({existingFiles:b.existingFiles,pendingFiles:b.pendingFiles,isDragOver:b.isDragOver,isDeleting:b.isDeleting,maxFiles:o,maxSize:a,accept:l,endpoints:L,confirmBeforeRemove:v,totalCount:b.totalCount,hasFiles:b.hasFiles,canAddMore:b.canAddMore,allItems:b.allItems,imageFiles:b.imageFiles,handleFiles:b.handleFiles,handleRemove:b.handleRemove,handleRetry:b.handleRetry,handleUploadAll:b.handleUploadAll,handleOpenGallery:b.handleOpenGallery,handleDownload:b.handleDownload,handleDragEnd:b.handleDragEnd,setIsDragOver:b.setIsDragOver,inputRef:b.inputRef,galleryOpen:b.galleryOpen,setGalleryOpen:b.setGalleryOpen,galleryStartIndex:b.galleryStartIndex,galleryKeyRef:b.galleryKeyRef,confirmDialogOpen:b.confirmDialogOpen,setConfirmDialogOpen:b.setConfirmDialogOpen,itemToDelete:b.itemToDelete,executeRemoveAttachment:b.executeRemoveAttachment,authenticatedImageUrls:b.authenticatedImageUrls}),[b,o,a,l,L,v]);return d.jsx($l,{value:$,children:d.jsxs(W,{id:A,className:O,...P,children:[d.jsx(Jl,{isDragOver:b.isDragOver,canAddMore:b.canAddMore,onFiles:b.handleFiles,setIsDragOver:b.setIsDragOver,inputRef:b.inputRef,accept:l,maxFiles:o,maxSize:a,children:b.hasFiles&&d.jsx(Gi,{allItems:b.allItems,canAddMore:b.canAddMore,totalCount:b.totalCount,maxFiles:o,maxSize:a,accept:l,onRemove:b.handleRemove,onRetry:b.handleRetry,onImageClick:b.handleOpenGallery,onDownload:b.handleDownload,onDragEnd:b.handleDragEnd,onAddClick:()=>b.inputRef.current?.click(),enablePrimarySelection:x,primaryFileId:y,onPrimaryChange:T})}),v&&d.jsx(ta,{isOpen:b.confirmDialogOpen,onClose:()=>{b.isDeleting||b.setConfirmDialogOpen(!1)},title:E??Re("attachment.delete_confirm_title"),message:I??Re("attachment.delete_confirm_message"),confirmText:Re("common.delete"),confirmButtonVariant:"danger",onConfirm:()=>{b.itemToDelete&&b.executeRemoveAttachment(b.itemToDelete)},isLoading:b.isDeleting,loadingText:Re("attachment.deleting")}),F.length>0&&b.galleryOpen&&d.jsx(Tr,{images:F,isOpen:!0,onClose:()=>b.setGalleryOpen(!1),startIndex:b.galleryStartIndex,showDownload:!0},`gallery-${b.galleryKeyRef.current}`)]})})});Xn.displayName="FileUploader";const qh=Object.freeze(Object.defineProperty({__proto__:null,FileDropZone:Jl,FileList:Gi,FileUploader:Xn,FileUploaderProvider:$l,SortableThumbnailItem:Hi,default:Xn,extractErrorMessage:ql,fileUploaderT:Re,formatFileSize:Fn,getFileIcon:Zl,useFileUploader:Ql,useFileUploaderContext:tg},Symbol.toStringTag,{value:"Module"})),ji=({provider:e,className:t="w-5 h-5"})=>{switch(e){case"google":return d.jsxs("svg",{className:t,viewBox:"0 0 24 24",children:[d.jsx("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),d.jsx("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),d.jsx("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}),d.jsx("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]});case"naver":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M16.273 12.845L7.376 0H0v24h7.727V11.155L16.624 24H24V0h-7.727z"})});case"kakao":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M12 3c5.799 0 10.5 3.664 10.5 8.185 0 4.52-4.701 8.184-10.5 8.184a13.5 13.5 0 0 1-1.727-.11l-4.408 2.883c-.501.265-.678.236-.472-.413l.892-3.678c-2.88-1.46-4.785-3.99-4.785-6.866C1.5 6.665 6.201 3 12 3zm5.519 8.625h-.634v2.916h-.97v-2.916h-.633v-.848h2.237v.848zm-3.294 2.916h-.97V10.77h.97v3.771zm-1.97 0h-.97v-2.77l-.658 1.53h-.629l-.665-1.53v2.77h-.97V10.77h1.13l.822 1.912.814-1.912h1.126v3.771zm-4.49 0h-.97V10.77h.97v3.771z"})});case"facebook":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"})});case"apple":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"})});default:return null}},Bi=(e,t)=>window.G7Core?.t?.(e,t)??e,Zh=[{id:"google",name:"Google",color:"bg-white dark:bg-gray-800",textColor:"text-gray-700 dark:text-gray-300"},{id:"naver",name:"네이버",color:"bg-[#03C75A]",textColor:"text-white"},{id:"kakao",name:"카카오",color:"bg-[#FEE500]",textColor:"text-[#000000]/85"},{id:"facebook",name:"Facebook",color:"bg-[#1877F2]",textColor:"text-white"},{id:"apple",name:"Apple",color:"bg-black dark:bg-white",textColor:"text-white dark:text-black"}],Wi=({providers:e=["google","naver","kakao"],mode:t="login",variant:r="full",className:n="",id:o,editorAttrs:a})=>{const s=h=>{const c=encodeURIComponent(window.location.href);window.location.href=`/api/auth/${h}?redirect=${c}`},l=h=>t==="register"?Bi("auth.social.register_with",{provider:h}):Bi("auth.social.login_with",{provider:h}),f=Zh.filter(h=>e.includes(h.id));return r==="icon"?d.jsx(W,{className:`flex justify-center gap-4 ${n}`,id:o,...a,children:f.map(h=>d.jsx(Q,{onClick:()=>s(h.id),className:`w-12 h-12 flex items-center justify-center rounded-full ${h.color} ${h.textColor} border border-gray-200 dark:border-gray-700 hover:opacity-90 transition-opacity`,"aria-label":l(h.name),children:d.jsx(ji,{provider:h.id,className:"w-5 h-5"})},h.id))}):d.jsx(W,{className:`space-y-3 ${n}`,id:o,...a,children:f.map(h=>d.jsxs(Q,{onClick:()=>s(h.id),className:`w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg font-medium border border-gray-200 dark:border-gray-700 ${h.color} ${h.textColor} hover:opacity-90 transition-opacity`,children:[d.jsx(ji,{provider:h.id,className:"w-5 h-5"}),d.jsx(R,{children:l(h.name)})]},h.id))})},Qh=Object.freeze(Object.defineProperty({__proto__:null,default:Wi},Symbol.toStringTag,{value:"Module"})),Jh=(e,t)=>window.G7Core?.t?.(e,t)??e,Rh={success:Je.CheckCircle,error:Je.XCircle,warning:Je.ExclamationTriangle,info:Je.InfoCircle},Vi={success:{bg:"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800",text:"text-green-800 dark:text-green-300"},error:{bg:"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",text:"text-red-800 dark:text-red-300"},warning:{bg:"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",text:"text-yellow-800 dark:text-yellow-300"},info:{bg:"bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",text:"text-blue-800 dark:text-blue-300"}},e0={"top-left":"top-4 left-4","top-center":"top-4 left-1/2 -translate-x-1/2","top-right":"top-4 right-4","bottom-left":"bottom-4 left-4","bottom-center":"bottom-4 left-1/2 -translate-x-1/2","bottom-right":"bottom-4 right-4"},Ca=({toasts:e,position:t="bottom-center",duration:r=3e3,onRemove:n})=>{const[o,a]=k.useState(new Map),s=k.useCallback(l=>{a(f=>{const h=new Map(f);return h.set(l,!1),h}),setTimeout(()=>{if(n)n(l);else{const f=window.G7Core;f?.state?.update&&f.state.update(h=>({toasts:(Array.isArray(h.toasts)?h.toasts:[]).filter(i=>i.id!==l)}))}},300)},[n]);return k.useEffect(()=>{!e||e.length===0||e.forEach(l=>{if(o.has(l.id))return;a(h=>{const c=new Map(h);return c.set(l.id,!0),c});const f=l.duration||r;f>0&&setTimeout(()=>{s(l.id)},f)})},[e,r,s,o]),!e||e.length===0?null:d.jsx(W,{className:`fixed z-[9999] flex flex-col gap-2 ${e0[t]}`,"aria-live":"polite","aria-atomic":"true",children:e.map(l=>d.jsx(t0,{toast:l,position:t,isVisible:o.get(l.id)??!0,onClose:()=>s(l.id)},l.id))})},t0=({toast:e,position:t,isVisible:r,onClose:n})=>{const o=Vi[e.type]||Vi.info,a=e.icon?e.icon:Rh[e.type],s=t.startsWith("bottom")?"translate-y-2":"-translate-y-2";return d.jsxs(W,{className:` + `},Sg={onDragStart(e){let{active:t}=e;return"Picked up draggable item "+t.id+"."},onDragOver(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was moved over droppable area "+r.id+".":"Draggable item "+t.id+" is no longer over a droppable area."},onDragEnd(e){let{active:t,over:r}=e;return r?"Draggable item "+t.id+" was dropped over droppable area "+r.id:"Draggable item "+t.id+" was dropped."},onDragCancel(e){let{active:t}=e;return"Dragging was cancelled. Draggable item "+t.id+" was dropped."}};function _g(e){let{announcements:t=Sg,container:r,hiddenTextDescribedById:n,screenReaderInstructions:o=Cg}=e;const{announce:a,announcement:s}=wg(),l=en("DndLiveRegion"),[f,h]=k.useState(!1);if(k.useEffect(()=>{h(!0)},[]),xg(k.useMemo(()=>({onDragStart(i){let{active:u}=i;a(t.onDragStart({active:u}))},onDragMove(i){let{active:u,over:g}=i;t.onDragMove&&a(t.onDragMove({active:u,over:g}))},onDragOver(i){let{active:u,over:g}=i;a(t.onDragOver({active:u,over:g}))},onDragEnd(i){let{active:u,over:g}=i;a(t.onDragEnd({active:u,over:g}))},onDragCancel(i){let{active:u,over:g}=i;a(t.onDragCancel({active:u,over:g}))}}),[a,t])),!f)return null;const c=k.createElement(k.Fragment,null,k.createElement(yg,{id:n,value:o.draggable}),k.createElement(vg,{id:l,announcement:s}));return r?Jt.createPortal(c,r):c}var et;(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"})(et||(et={}));function Hn(){}function oi(e,t){return k.useMemo(()=>({sensor:e,options:t??{}}),[e,t])}function Eg(){for(var e=arguments.length,t=new Array(e),r=0;r<e;r++)t[r]=arguments[r];return k.useMemo(()=>[...t].filter(n=>n!=null),[...t])}const Gt=Object.freeze({x:0,y:0});function ai(e,t){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function si(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return r-n}function Tg(e,t){let{data:{value:r}}=e,{data:{value:n}}=t;return n-r}function li(e){let{left:t,top:r,height:n,width:o}=e;return[{x:t,y:r},{x:t+o,y:r},{x:t,y:r+n},{x:t+o,y:r+n}]}function ii(e,t){if(!e||e.length===0)return null;const[r]=e;return r[t]}function ci(e,t,r){return t===void 0&&(t=e.left),r===void 0&&(r=e.top),{x:t+e.width*.5,y:r+e.height*.5}}const Ng=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=ci(t,t.left,t.top),a=[];for(const s of n){const{id:l}=s,f=r.get(l);if(f){const h=ai(ci(f),o);a.push({id:l,data:{droppableContainer:s,value:h}})}}return a.sort(si)},Ag=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=li(t),a=[];for(const s of n){const{id:l}=s,f=r.get(l);if(f){const h=li(f),c=o.reduce((u,g,p)=>u+ai(h[p],g),0),i=Number((c/4).toFixed(4));a.push({id:l,data:{droppableContainer:s,value:i}})}}return a.sort(si)};function Mg(e,t){const r=Math.max(t.top,e.top),n=Math.max(t.left,e.left),o=Math.min(t.left+t.width,e.left+e.width),a=Math.min(t.top+t.height,e.top+e.height),s=o-n,l=a-r;if(n<o&&r<a){const f=t.width*t.height,h=e.width*e.height,c=s*l,i=c/(f+h-c);return Number(i.toFixed(4))}return 0}const Og=e=>{let{collisionRect:t,droppableRects:r,droppableContainers:n}=e;const o=[];for(const a of n){const{id:s}=a,l=r.get(s);if(l){const f=Mg(l,t);f>0&&o.push({id:s,data:{droppableContainer:a,value:f}})}}return o.sort(Tg)};function Ig(e,t,r){return{...e,scaleX:t&&r?t.width/r.width:1,scaleY:t&&r?t.height/r.height:1}}function di(e,t){return e&&t?{x:e.left-t.left,y:e.top-t.top}:Gt}function Dg(e){return function(r){for(var n=arguments.length,o=new Array(n>1?n-1:0),a=1;a<n;a++)o[a-1]=arguments[a];return o.reduce((s,l)=>({...s,top:s.top+e*l.y,bottom:s.bottom+e*l.y,left:s.left+e*l.x,right:s.right+e*l.x}),{...r})}}const Lg=Dg(1);function Pg(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 $g(e,t,r){const n=Pg(t);if(!n)return e;const{scaleX:o,scaleY:a,x:s,y:l}=n,f=e.left-s-(1-o)*parseFloat(r),h=e.top-l-(1-a)*parseFloat(r.slice(r.indexOf(" ")+1)),c=o?e.width/o:e.width,i=a?e.height/a:e.height;return{width:c,height:i,top:h,right:f+c,bottom:h+i,left:f}}const Fg={ignoreTransform:!1};function Ir(e,t){t===void 0&&(t=Fg);let r=e.getBoundingClientRect();if(t.ignoreTransform){const{transform:h,transformOrigin:c}=yt(e).getComputedStyle(e);h&&(r=$g(r,h,c))}const{top:n,left:o,width:a,height:s,bottom:l,right:f}=r;return{top:n,left:o,width:a,height:s,bottom:l,right:f}}function ui(e){return Ir(e,{ignoreTransform:!0})}function Ug(e){const t=e.innerWidth,r=e.innerHeight;return{top:0,left:0,right:t,bottom:r,width:t,height:r}}function zg(e,t){return t===void 0&&(t=yt(e).getComputedStyle(e)),t.position==="fixed"}function Hg(e,t){t===void 0&&(t=yt(e).getComputedStyle(e));const r=/(auto|scroll|overlay)/;return["overflow","overflowX","overflowY"].some(o=>{const a=t[o];return typeof a=="string"?r.test(a):!1})}function Gn(e,t){const r=[];function n(o){if(t!=null&&r.length>=t||!o)return r;if(la(o)&&o.scrollingElement!=null&&!r.includes(o.scrollingElement))return r.push(o.scrollingElement),r;if(!Qr(o)||ei(o)||r.includes(o))return r;const a=yt(e).getComputedStyle(o);return o!==e&&Hg(o,a)&&r.push(o),zg(o,a)?r:n(o.parentNode)}return e?n(e):r}function fi(e){const[t]=Gn(e,1);return t??null}function ga(e){return!Un||!e?null:Ar(e)?e:sa(e)?la(e)||e===Mr(e).scrollingElement?window:Qr(e)?e:null:null}function gi(e){return Ar(e)?e.scrollX:e.scrollLeft}function hi(e){return Ar(e)?e.scrollY:e.scrollTop}function ha(e){return{x:gi(e),y:hi(e)}}var ot;(function(e){e[e.Forward=1]="Forward",e[e.Backward=-1]="Backward"})(ot||(ot={}));function pi(e){return!Un||!e?!1:e===document.scrollingElement}function mi(e){const t={x:0,y:0},r=pi(e)?{height:window.innerHeight,width:window.innerWidth}:{height:e.clientHeight,width:e.clientWidth},n={x:e.scrollWidth-r.width,y:e.scrollHeight-r.height},o=e.scrollTop<=t.y,a=e.scrollLeft<=t.x,s=e.scrollTop>=n.y,l=e.scrollLeft>=n.x;return{isTop:o,isLeft:a,isBottom:s,isRight:l,maxScroll:n,minScroll:t}}const Gg={x:.2,y:.2};function jg(e,t,r,n,o){let{top:a,left:s,right:l,bottom:f}=r;n===void 0&&(n=10),o===void 0&&(o=Gg);const{isTop:h,isBottom:c,isLeft:i,isRight:u}=mi(e),g={x:0,y:0},p={x:0,y:0},m={height:t.height*o.y,width:t.width*o.x};return!h&&a<=t.top+m.height?(g.y=ot.Backward,p.y=n*Math.abs((t.top+m.height-a)/m.height)):!c&&f>=t.bottom-m.height&&(g.y=ot.Forward,p.y=n*Math.abs((t.bottom-m.height-f)/m.height)),!u&&l>=t.right-m.width?(g.x=ot.Forward,p.x=n*Math.abs((t.right-m.width-l)/m.width)):!i&&s<=t.left+m.width&&(g.x=ot.Backward,p.x=n*Math.abs((t.left+m.width-s)/m.width)),{direction:g,speed:p}}function Bg(e){if(e===document.scrollingElement){const{innerWidth:a,innerHeight:s}=window;return{top:0,left:0,right:a,bottom:s,width:a,height:s}}const{top:t,left:r,right:n,bottom:o}=e.getBoundingClientRect();return{top:t,left:r,right:n,bottom:o,width:e.clientWidth,height:e.clientHeight}}function bi(e){return e.reduce((t,r)=>Or(t,ha(r)),Gt)}function Wg(e){return e.reduce((t,r)=>t+gi(r),0)}function Vg(e){return e.reduce((t,r)=>t+hi(r),0)}function Kg(e,t){if(t===void 0&&(t=Ir),!e)return;const{top:r,left:n,bottom:o,right:a}=t(e);fi(e)&&(o<=0||a<=0||r>=window.innerHeight||n>=window.innerWidth)&&e.scrollIntoView({block:"center",inline:"center"})}const Yg=[["x",["left","right"],Wg],["y",["top","bottom"],Vg]];class pa{constructor(t,r){this.rect=void 0,this.width=void 0,this.height=void 0,this.top=void 0,this.bottom=void 0,this.right=void 0,this.left=void 0;const n=Gn(r),o=bi(n);this.rect={...t},this.width=t.width,this.height=t.height;for(const[a,s,l]of Yg)for(const f of s)Object.defineProperty(this,f,{get:()=>{const h=l(n),c=o[a]-h;return this.rect[f]+c},enumerable:!0});Object.defineProperty(this,"rect",{enumerable:!1})}}class nn{constructor(t){this.target=void 0,this.listeners=[],this.removeAll=()=>{this.listeners.forEach(r=>{var n;return(n=this.target)==null?void 0:n.removeEventListener(...r)})},this.target=t}add(t,r,n){var o;(o=this.target)==null||o.addEventListener(t,r,n),this.listeners.push([t,r,n])}}function Xg(e){const{EventTarget:t}=yt(e);return e instanceof t?e:Mr(e)}function ma(e,t){const r=Math.abs(e.x),n=Math.abs(e.y);return typeof t=="number"?Math.sqrt(r**2+n**2)>t:"x"in t&&"y"in t?r>t.x&&n>t.y:"x"in t?r>t.x:"y"in t?n>t.y:!1}var It;(function(e){e.Click="click",e.DragStart="dragstart",e.Keydown="keydown",e.ContextMenu="contextmenu",e.Resize="resize",e.SelectionChange="selectionchange",e.VisibilityChange="visibilitychange"})(It||(It={}));function yi(e){e.preventDefault()}function qg(e){e.stopPropagation()}var ze;(function(e){e.Space="Space",e.Down="ArrowDown",e.Right="ArrowRight",e.Left="ArrowLeft",e.Up="ArrowUp",e.Esc="Escape",e.Enter="Enter",e.Tab="Tab"})(ze||(ze={}));const vi={start:[ze.Space,ze.Enter],cancel:[ze.Esc],end:[ze.Space,ze.Enter,ze.Tab]},Zg=(e,t)=>{let{currentCoordinates:r}=t;switch(e.code){case ze.Right:return{...r,x:r.x+25};case ze.Left:return{...r,x:r.x-25};case ze.Down:return{...r,y:r.y+25};case ze.Up:return{...r,y:r.y-25}}};class ba{constructor(t){this.props=void 0,this.autoScrollEnabled=!1,this.referenceCoordinates=void 0,this.listeners=void 0,this.windowListeners=void 0,this.props=t;const{event:{target:r}}=t;this.props=t,this.listeners=new nn(Mr(r)),this.windowListeners=new nn(yt(r)),this.handleKeyDown=this.handleKeyDown.bind(this),this.handleCancel=this.handleCancel.bind(this),this.attach()}attach(){this.handleStart(),this.windowListeners.add(It.Resize,this.handleCancel),this.windowListeners.add(It.VisibilityChange,this.handleCancel),setTimeout(()=>this.listeners.add(It.Keydown,this.handleKeyDown))}handleStart(){const{activeNode:t,onStart:r}=this.props,n=t.node.current;n&&Kg(n),r(Gt)}handleKeyDown(t){if(ua(t)){const{active:r,context:n,options:o}=this.props,{keyboardCodes:a=vi,coordinateGetter:s=Zg,scrollBehavior:l="smooth"}=o,{code:f}=t;if(a.end.includes(f)){this.handleEnd(t);return}if(a.cancel.includes(f)){this.handleCancel(t);return}const{collisionRect:h}=n.current,c=h?{x:h.left,y:h.top}:Gt;this.referenceCoordinates||(this.referenceCoordinates=c);const i=s(t,{active:r,context:n.current,currentCoordinates:c});if(i){const u=tn(i,c),g={x:0,y:0},{scrollableAncestors:p}=n.current;for(const m of p){const x=t.code,{isTop:C,isRight:O,isLeft:v,isBottom:E,maxScroll:I,minScroll:_}=mi(m),M=Bg(m),w={x:Math.min(x===ze.Right?M.right-M.width/2:M.right,Math.max(x===ze.Right?M.left:M.left+M.width/2,i.x)),y:Math.min(x===ze.Down?M.bottom-M.height/2:M.bottom,Math.max(x===ze.Down?M.top:M.top+M.height/2,i.y))},y=x===ze.Right&&!O||x===ze.Left&&!v,T=x===ze.Down&&!E||x===ze.Up&&!C;if(y&&w.x!==i.x){const A=m.scrollLeft+u.x,P=x===ze.Right&&A<=I.x||x===ze.Left&&A>=_.x;if(P&&!u.y){m.scrollTo({left:A,behavior:l});return}P?g.x=m.scrollLeft-A:g.x=x===ze.Right?m.scrollLeft-I.x:m.scrollLeft-_.x,g.x&&m.scrollBy({left:-g.x,behavior:l});break}else if(T&&w.y!==i.y){const A=m.scrollTop+u.y,P=x===ze.Down&&A<=I.y||x===ze.Up&&A>=_.y;if(P&&!u.x){m.scrollTo({top:A,behavior:l});return}P?g.y=m.scrollTop-A:g.y=x===ze.Down?m.scrollTop-I.y:m.scrollTop-_.y,g.y&&m.scrollBy({top:-g.y,behavior:l});break}}this.handleMove(t,Or(tn(i,this.referenceCoordinates),g))}}}handleMove(t,r){const{onMove:n}=this.props;t.preventDefault(),n(r)}handleEnd(t){const{onEnd:r}=this.props;t.preventDefault(),this.detach(),r()}handleCancel(t){const{onCancel:r}=this.props;t.preventDefault(),this.detach(),r()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll()}}ba.activators=[{eventName:"onKeyDown",handler:(e,t,r)=>{let{keyboardCodes:n=vi,onActivation:o}=t,{active:a}=r;const{code:s}=e.nativeEvent;if(n.start.includes(s)){const l=a.activatorNode.current;return l&&e.target!==l?!1:(e.preventDefault(),o?.({event:e.nativeEvent}),!0)}return!1}}];function wi(e){return!!(e&&"distance"in e)}function xi(e){return!!(e&&"delay"in e)}class ya{constructor(t,r,n){var o;n===void 0&&(n=Xg(t.event.target)),this.props=void 0,this.events=void 0,this.autoScrollEnabled=!0,this.document=void 0,this.activated=!1,this.initialCoordinates=void 0,this.timeoutId=null,this.listeners=void 0,this.documentListeners=void 0,this.windowListeners=void 0,this.props=t,this.events=r;const{event:a}=t,{target:s}=a;this.props=t,this.events=r,this.document=Mr(s),this.documentListeners=new nn(this.document),this.listeners=new nn(n),this.windowListeners=new nn(yt(s)),this.initialCoordinates=(o=fa(a))!=null?o:Gt,this.handleStart=this.handleStart.bind(this),this.handleMove=this.handleMove.bind(this),this.handleEnd=this.handleEnd.bind(this),this.handleCancel=this.handleCancel.bind(this),this.handleKeydown=this.handleKeydown.bind(this),this.removeTextSelection=this.removeTextSelection.bind(this),this.attach()}attach(){const{events:t,props:{options:{activationConstraint:r,bypassActivationConstraint:n}}}=this;if(this.listeners.add(t.move.name,this.handleMove,{passive:!1}),this.listeners.add(t.end.name,this.handleEnd),t.cancel&&this.listeners.add(t.cancel.name,this.handleCancel),this.windowListeners.add(It.Resize,this.handleCancel),this.windowListeners.add(It.DragStart,yi),this.windowListeners.add(It.VisibilityChange,this.handleCancel),this.windowListeners.add(It.ContextMenu,yi),this.documentListeners.add(It.Keydown,this.handleKeydown),r){if(n!=null&&n({event:this.props.event,activeNode:this.props.activeNode,options:this.props.options}))return this.handleStart();if(xi(r)){this.timeoutId=setTimeout(this.handleStart,r.delay),this.handlePending(r);return}if(wi(r)){this.handlePending(r);return}}this.handleStart()}detach(){this.listeners.removeAll(),this.windowListeners.removeAll(),setTimeout(this.documentListeners.removeAll,50),this.timeoutId!==null&&(clearTimeout(this.timeoutId),this.timeoutId=null)}handlePending(t,r){const{active:n,onPending:o}=this.props;o(n,t,this.initialCoordinates,r)}handleStart(){const{initialCoordinates:t}=this,{onStart:r}=this.props;t&&(this.activated=!0,this.documentListeners.add(It.Click,qg,{capture:!0}),this.removeTextSelection(),this.documentListeners.add(It.SelectionChange,this.removeTextSelection),r(t))}handleMove(t){var r;const{activated:n,initialCoordinates:o,props:a}=this,{onMove:s,options:{activationConstraint:l}}=a;if(!o)return;const f=(r=fa(t))!=null?r:Gt,h=tn(o,f);if(!n&&l){if(wi(l)){if(l.tolerance!=null&&ma(h,l.tolerance))return this.handleCancel();if(ma(h,l.distance))return this.handleStart()}if(xi(l)&&ma(h,l.tolerance))return this.handleCancel();this.handlePending(l,h);return}t.cancelable&&t.preventDefault(),s(f)}handleEnd(){const{onAbort:t,onEnd:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleCancel(){const{onAbort:t,onCancel:r}=this.props;this.detach(),this.activated||t(this.props.active),r()}handleKeydown(t){t.code===ze.Esc&&this.handleCancel()}removeTextSelection(){var t;(t=this.document.getSelection())==null||t.removeAllRanges()}}const Qg={cancel:{name:"pointercancel"},move:{name:"pointermove"},end:{name:"pointerup"}};class va extends ya{constructor(t){const{event:r}=t,n=Mr(r.target);super(t,Qg,n)}}va.activators=[{eventName:"onPointerDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return!r.isPrimary||r.button!==0?!1:(n?.({event:r}),!0)}}];const Jg={move:{name:"mousemove"},end:{name:"mouseup"}};var wa;(function(e){e[e.RightClick=2]="RightClick"})(wa||(wa={}));class Rg extends ya{constructor(t){super(t,Jg,Mr(t.event.target))}}Rg.activators=[{eventName:"onMouseDown",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;return r.button===wa.RightClick?!1:(n?.({event:r}),!0)}}];const xa={cancel:{name:"touchcancel"},move:{name:"touchmove"},end:{name:"touchend"}};class eh extends ya{constructor(t){super(t,xa)}static setup(){return window.addEventListener(xa.move.name,t,{capture:!1,passive:!1}),function(){window.removeEventListener(xa.move.name,t)};function t(){}}}eh.activators=[{eventName:"onTouchStart",handler:(e,t)=>{let{nativeEvent:r}=e,{onActivation:n}=t;const{touches:o}=r;return o.length>1?!1:(n?.({event:r}),!0)}}];var on;(function(e){e[e.Pointer=0]="Pointer",e[e.DraggableRect=1]="DraggableRect"})(on||(on={}));var jn;(function(e){e[e.TreeOrder=0]="TreeOrder",e[e.ReversedTreeOrder=1]="ReversedTreeOrder"})(jn||(jn={}));function th(e){let{acceleration:t,activator:r=on.Pointer,canScroll:n,draggingRect:o,enabled:a,interval:s=5,order:l=jn.TreeOrder,pointerCoordinates:f,scrollableAncestors:h,scrollableAncestorRects:c,delta:i,threshold:u}=e;const g=nh({delta:i,disabled:!a}),[p,m]=gg(),x=k.useRef({x:0,y:0}),C=k.useRef({x:0,y:0}),O=k.useMemo(()=>{switch(r){case on.Pointer:return f?{top:f.y,bottom:f.y,left:f.x,right:f.x}:null;case on.DraggableRect:return o}},[r,o,f]),v=k.useRef(null),E=k.useCallback(()=>{const _=v.current;if(!_)return;const M=x.current.x*C.current.x,w=x.current.y*C.current.y;_.scrollBy(M,w)},[]),I=k.useMemo(()=>l===jn.TreeOrder?[...h].reverse():h,[l,h]);k.useEffect(()=>{if(!a||!h.length||!O){m();return}for(const _ of I){if(n?.(_)===!1)continue;const M=h.indexOf(_),w=c[M];if(!w)continue;const{direction:y,speed:T}=jg(_,w,O,t,u);for(const A of["x","y"])g[A][y[A]]||(T[A]=0,y[A]=0);if(T.x>0||T.y>0){m(),v.current=_,p(E,s),x.current=T,C.current=y;return}}x.current={x:0,y:0},C.current={x:0,y:0},m()},[t,E,n,m,a,s,JSON.stringify(O),JSON.stringify(g),p,h,I,c,JSON.stringify(u)])}const rh={x:{[ot.Backward]:!1,[ot.Forward]:!1},y:{[ot.Backward]:!1,[ot.Forward]:!1}};function nh(e){let{delta:t,disabled:r}=e;const n=ca(t);return Rr(o=>{if(r||!n||!o)return rh;const a={x:Math.sign(t.x-n.x),y:Math.sign(t.y-n.y)};return{x:{[ot.Backward]:o.x[ot.Backward]||a.x===-1,[ot.Forward]:o.x[ot.Forward]||a.x===1},y:{[ot.Backward]:o.y[ot.Backward]||a.y===-1,[ot.Forward]:o.y[ot.Forward]||a.y===1}}},[r,t,n])}function oh(e,t){const r=t!=null?e.get(t):void 0,n=r?r.node.current:null;return Rr(o=>{var a;return t==null?null:(a=n??o)!=null?a:null},[n,t])}function ah(e,t){return k.useMemo(()=>e.reduce((r,n)=>{const{sensor:o}=n,a=o.activators.map(s=>({eventName:s.eventName,handler:t(s.handler,n)}));return[...r,...a]},[]),[e,t])}var an;(function(e){e[e.Always=0]="Always",e[e.BeforeDragging=1]="BeforeDragging",e[e.WhileDragging=2]="WhileDragging"})(an||(an={}));var ka;(function(e){e.Optimized="optimized"})(ka||(ka={}));const ki=new Map;function sh(e,t){let{dragging:r,dependencies:n,config:o}=t;const[a,s]=k.useState(null),{frequency:l,measure:f,strategy:h}=o,c=k.useRef(e),i=x(),u=Jr(i),g=k.useCallback(function(C){C===void 0&&(C=[]),!u.current&&s(O=>O===null?C:O.concat(C.filter(v=>!O.includes(v))))},[u]),p=k.useRef(null),m=Rr(C=>{if(i&&!r)return ki;if(!C||C===ki||c.current!==e||a!=null){const O=new Map;for(let v of e){if(!v)continue;if(a&&a.length>0&&!a.includes(v.id)&&v.rect.current){O.set(v.id,v.rect.current);continue}const E=v.node.current,I=E?new pa(f(E),E):null;v.rect.current=I,I&&O.set(v.id,I)}return O}return C},[e,a,r,i,f]);return k.useEffect(()=>{c.current=e},[e]),k.useEffect(()=>{i||g()},[r,i]),k.useEffect(()=>{a&&a.length>0&&s(null)},[JSON.stringify(a)]),k.useEffect(()=>{i||typeof l!="number"||p.current!==null||(p.current=setTimeout(()=>{g(),p.current=null},l))},[l,i,g,...n]),{droppableRects:m,measureDroppableContainers:g,measuringScheduled:a!=null};function x(){switch(h){case an.Always:return!1;case an.BeforeDragging:return r;default:return!r}}}function Ci(e,t){return Rr(r=>e?r||(typeof t=="function"?t(e):e):null,[t,e])}function lh(e,t){return Ci(e,t)}function ih(e){let{callback:t,disabled:r}=e;const n=ia(t),o=k.useMemo(()=>{if(r||typeof window>"u"||typeof window.MutationObserver>"u")return;const{MutationObserver:a}=window;return new a(n)},[n,r]);return k.useEffect(()=>()=>o?.disconnect(),[o]),o}function Bn(e){let{callback:t,disabled:r}=e;const n=ia(t),o=k.useMemo(()=>{if(r||typeof window>"u"||typeof window.ResizeObserver>"u")return;const{ResizeObserver:a}=window;return new a(n)},[r]);return k.useEffect(()=>()=>o?.disconnect(),[o]),o}function ch(e){return new pa(Ir(e),e)}function Si(e,t,r){t===void 0&&(t=ch);const[n,o]=k.useState(null);function a(){o(f=>{if(!e)return null;if(e.isConnected===!1){var h;return(h=f??r)!=null?h:null}const c=t(e);return JSON.stringify(f)===JSON.stringify(c)?f:c})}const s=ih({callback(f){if(e)for(const h of f){const{type:c,target:i}=h;if(c==="childList"&&i instanceof HTMLElement&&i.contains(e)){a();break}}}}),l=Bn({callback:a});return Yt(()=>{a(),e?(l?.observe(e),s?.observe(document.body,{childList:!0,subtree:!0})):(l?.disconnect(),s?.disconnect())},[e]),n}function dh(e){const t=Ci(e);return di(e,t)}const _i=[];function uh(e){const t=k.useRef(e),r=Rr(n=>e?n&&n!==_i&&e&&t.current&&e.parentNode===t.current.parentNode?n:Gn(e):_i,[e]);return k.useEffect(()=>{t.current=e},[e]),r}function fh(e){const[t,r]=k.useState(null),n=k.useRef(e),o=k.useCallback(a=>{const s=ga(a.target);s&&r(l=>l?(l.set(s,ha(s)),new Map(l)):null)},[]);return k.useEffect(()=>{const a=n.current;if(e!==a){s(a);const l=e.map(f=>{const h=ga(f);return h?(h.addEventListener("scroll",o,{passive:!0}),[h,ha(h)]):null}).filter(f=>f!=null);r(l.length?new Map(l):null),n.current=e}return()=>{s(e),s(a)};function s(l){l.forEach(f=>{const h=ga(f);h?.removeEventListener("scroll",o)})}},[o,e]),k.useMemo(()=>e.length?t?Array.from(t.values()).reduce((a,s)=>Or(a,s),Gt):bi(e):Gt,[e,t])}function Ei(e,t){t===void 0&&(t=[]);const r=k.useRef(null);return k.useEffect(()=>{r.current=null},t),k.useEffect(()=>{const n=e!==Gt;n&&!r.current&&(r.current=e),!n&&r.current&&(r.current=null)},[e]),r.current?tn(e,r.current):Gt}function gh(e){k.useEffect(()=>{if(!Un)return;const t=e.map(r=>{let{sensor:n}=r;return n.setup==null?void 0:n.setup()});return()=>{for(const r of t)r?.()}},e.map(t=>{let{sensor:r}=t;return r}))}function hh(e,t){return k.useMemo(()=>e.reduce((r,n)=>{let{eventName:o,handler:a}=n;return r[o]=s=>{a(s,t)},r},{}),[e,t])}function Ti(e){return k.useMemo(()=>e?Ug(e):null,[e])}const Ni=[];function ph(e,t){t===void 0&&(t=Ir);const[r]=e,n=Ti(r?yt(r):null),[o,a]=k.useState(Ni);function s(){a(()=>e.length?e.map(f=>pi(f)?n:new pa(t(f),f)):Ni)}const l=Bn({callback:s});return Yt(()=>{l?.disconnect(),s(),e.forEach(f=>l?.observe(f))},[e]),o}function mh(e){if(!e)return null;if(e.children.length>1)return e;const t=e.children[0];return Qr(t)?t:e}function bh(e){let{measure:t}=e;const[r,n]=k.useState(null),o=k.useCallback(h=>{for(const{target:c}of h)if(Qr(c)){n(i=>{const u=t(c);return i?{...i,width:u.width,height:u.height}:u});break}},[t]),a=Bn({callback:o}),s=k.useCallback(h=>{const c=mh(h);a?.disconnect(),c&&a?.observe(c),n(c?t(c):null)},[t,a]),[l,f]=zn(s);return k.useMemo(()=>({nodeRef:l,rect:r,setRef:f}),[r,l,f])}const yh=[{sensor:va,options:{}},{sensor:ba,options:{}}],vh={current:{}},Wn={draggable:{measure:ui},droppable:{measure:ui,strategy:an.WhileDragging,frequency:ka.Optimized},dragOverlay:{measure:Ir}};class sn extends Map{get(t){var r;return t!=null&&(r=super.get(t))!=null?r:void 0}toArray(){return Array.from(this.values())}getEnabled(){return this.toArray().filter(t=>{let{disabled:r}=t;return!r})}getNodeFor(t){var r,n;return(r=(n=this.get(t))==null?void 0:n.node.current)!=null?r:void 0}}const wh={activatorEvent:null,active:null,activeNode:null,activeNodeRect:null,collisions:null,containerNodeRect:null,draggableNodes:new Map,droppableRects:new Map,droppableContainers:new sn,over:null,dragOverlay:{nodeRef:{current:null},rect:null,setRef:Hn},scrollableAncestors:[],scrollableAncestorRects:[],measuringConfiguration:Wn,measureDroppableContainers:Hn,windowRect:null,measuringScheduled:!1},xh={activatorEvent:null,activators:[],active:null,activeNodeRect:null,ariaDescribedById:{draggable:""},dispatch:Hn,draggableNodes:new Map,over:null,measureDroppableContainers:Hn},Vn=k.createContext(xh),Ai=k.createContext(wh);function kh(){return{draggable:{active:null,initialCoordinates:{x:0,y:0},nodes:new Map,translate:{x:0,y:0}},droppable:{containers:new sn}}}function Ch(e,t){switch(t.type){case et.DragStart:return{...e,draggable:{...e.draggable,initialCoordinates:t.initialCoordinates,active:t.active}};case et.DragMove:return e.draggable.active==null?e:{...e,draggable:{...e.draggable,translate:{x:t.coordinates.x-e.draggable.initialCoordinates.x,y:t.coordinates.y-e.draggable.initialCoordinates.y}}};case et.DragEnd:case et.DragCancel:return{...e,draggable:{...e.draggable,active:null,initialCoordinates:{x:0,y:0},translate:{x:0,y:0}}};case et.RegisterDroppable:{const{element:r}=t,{id:n}=r,o=new sn(e.droppable.containers);return o.set(n,r),{...e,droppable:{...e.droppable,containers:o}}}case et.SetDroppableDisabled:{const{id:r,key:n,disabled:o}=t,a=e.droppable.containers.get(r);if(!a||n!==a.key)return e;const s=new sn(e.droppable.containers);return s.set(r,{...a,disabled:o}),{...e,droppable:{...e.droppable,containers:s}}}case et.UnregisterDroppable:{const{id:r,key:n}=t,o=e.droppable.containers.get(r);if(!o||n!==o.key)return e;const a=new sn(e.droppable.containers);return a.delete(r),{...e,droppable:{...e.droppable,containers:a}}}default:return e}}function Sh(e){let{disabled:t}=e;const{active:r,activatorEvent:n,draggableNodes:o}=k.useContext(Vn),a=ca(n),s=ca(r?.id);return k.useEffect(()=>{if(!t&&!n&&a&&s!=null){if(!ua(a)||document.activeElement===a.target)return;const l=o.get(s);if(!l)return;const{activatorNode:f,node:h}=l;if(!f.current&&!h.current)return;requestAnimationFrame(()=>{for(const c of[f.current,h.current]){if(!c)continue;const i=mg(c);if(i){i.focus();break}}})}},[n,t,o,s,a]),null}function _h(e,t){let{transform:r,...n}=t;return e!=null&&e.length?e.reduce((o,a)=>a({transform:o,...n}),r):r}function Eh(e){return k.useMemo(()=>({draggable:{...Wn.draggable,...e?.draggable},droppable:{...Wn.droppable,...e?.droppable},dragOverlay:{...Wn.dragOverlay,...e?.dragOverlay}}),[e?.draggable,e?.droppable,e?.dragOverlay])}function Th(e){let{activeNode:t,measure:r,initialRect:n,config:o=!0}=e;const a=k.useRef(!1),{x:s,y:l}=typeof o=="boolean"?{x:o,y:o}:o;Yt(()=>{if(!s&&!l||!t){a.current=!1;return}if(a.current||!n)return;const h=t?.node.current;if(!h||h.isConnected===!1)return;const c=r(h),i=di(c,n);if(s||(i.x=0),l||(i.y=0),a.current=!0,Math.abs(i.x)>0||Math.abs(i.y)>0){const u=fi(h);u&&u.scrollBy({top:i.y,left:i.x})}},[t,s,l,n,r])}const Mi=k.createContext({...Gt,scaleX:1,scaleY:1});var tr;(function(e){e[e.Uninitialized=0]="Uninitialized",e[e.Initializing=1]="Initializing",e[e.Initialized=2]="Initialized"})(tr||(tr={}));const Nh=k.memo(function(t){var r,n,o,a;let{id:s,accessibility:l,autoScroll:f=!0,children:h,sensors:c=yh,collisionDetection:i=Og,measuring:u,modifiers:g,...p}=t;const m=k.useReducer(Ch,void 0,kh),[x,C]=m,[O,v]=kg(),[E,I]=k.useState(tr.Uninitialized),_=E===tr.Initialized,{draggable:{active:M,nodes:w,translate:y},droppable:{containers:T}}=x,A=M!=null?w.get(M):null,P=k.useRef({initial:null,translated:null}),D=k.useMemo(()=>{var Ee;return M!=null?{id:M,data:(Ee=A?.data)!=null?Ee:vh,rect:P}:null},[M,A]),L=k.useRef(null),[U,S]=k.useState(null),[b,F]=k.useState(null),$=Jr(p,Object.values(p)),G=en("DndDescribedBy",s),Y=k.useMemo(()=>T.getEnabled(),[T]),B=Eh(u),{droppableRects:K,measureDroppableContainers:j,measuringScheduled:H}=sh(Y,{dragging:_,dependencies:[y.x,y.y],config:B.droppable}),q=oh(w,M),ue=k.useMemo(()=>b?fa(b):null,[b]),te=je(),fe=lh(q,B.draggable.measure);Th({activeNode:M!=null?w.get(M):null,config:te.layoutShiftCompensation,initialRect:fe,measure:B.draggable.measure});const ee=Si(q,B.draggable.measure,fe),xe=Si(q?q.parentElement:null),he=k.useRef({activatorEvent:null,active:null,activeNode:q,collisionRect:null,collisions:null,droppableRects:K,draggableNodes:w,draggingNode:null,draggingNodeRect:null,droppableContainers:T,over:null,scrollableAncestors:[],scrollAdjustedTranslate:null}),Ge=T.getNodeFor((r=he.current.over)==null?void 0:r.id),De=bh({measure:B.dragOverlay.measure}),ye=(n=De.nodeRef.current)!=null?n:q,Me=_?(o=De.rect)!=null?o:ee:null,Ce=!!(De.nodeRef.current&&De.rect),we=dh(Ce?null:ee),Fe=Ti(ye?yt(ye):null),Le=uh(_?Ge??q:null),V=ph(Le),re=_h(g,{transform:{x:y.x-we.x,y:y.y-we.y,scaleX:1,scaleY:1},activatorEvent:b,active:D,activeNodeRect:ee,containerNodeRect:xe,draggingNodeRect:Me,over:he.current.over,overlayNodeRect:De.rect,scrollableAncestors:Le,scrollableAncestorRects:V,windowRect:Fe}),oe=ue?Or(ue,y):null,ae=fh(Le),be=Ei(ae),ke=Ei(ae,[ee]),He=Or(re,be),Pe=Me?Lg(Me,re):null,Ve=D&&Pe?i({active:D,collisionRect:Pe,droppableRects:K,droppableContainers:Y,pointerCoordinates:oe}):null,Oe=ii(Ve,"id"),[X,se]=k.useState(null),ie=Ce?re:Or(re,ke),le=Ig(ie,(a=X?.rect)!=null?a:null,ee),ne=k.useRef(null),Te=k.useCallback((Ee,pe)=>{let{sensor:Ne,options:Ke}=pe;if(L.current==null)return;const $e=w.get(L.current);if(!$e)return;const Be=Ee.nativeEvent,Ze=new Ne({active:L.current,activeNode:$e,event:Be,options:Ke,context:he,onAbort(qe){if(!w.get(qe))return;const{onDragAbort:_t}=$.current,Bt={id:qe};_t?.(Bt),O({type:"onDragAbort",event:Bt})},onPending(qe,jt,_t,Bt){if(!w.get(qe))return;const{onDragPending:Et}=$.current,Wt={id:qe,constraint:jt,initialCoordinates:_t,offset:Bt};Et?.(Wt),O({type:"onDragPending",event:Wt})},onStart(qe){const jt=L.current;if(jt==null)return;const _t=w.get(jt);if(!_t)return;const{onDragStart:Bt}=$.current,dt={activatorEvent:Be,active:{id:jt,data:_t.data,rect:P}};Jt.unstable_batchedUpdates(()=>{Bt?.(dt),I(tr.Initializing),C({type:et.DragStart,initialCoordinates:qe,active:jt}),O({type:"onDragStart",event:dt}),S(ne.current),F(Be)})},onMove(qe){C({type:et.DragMove,coordinates:qe})},onEnd:qt(et.DragEnd),onCancel:qt(et.DragCancel)});ne.current=Ze;function qt(qe){return async function(){const{active:_t,collisions:Bt,over:dt,scrollAdjustedTranslate:Et}=he.current;let Wt=null;if(_t&&Et){const{cancelDrop:nr}=$.current;Wt={activatorEvent:Be,active:_t,collisions:Bt,delta:Et,over:dt},qe===et.DragEnd&&typeof nr=="function"&&await Promise.resolve(nr(Wt))&&(qe=et.DragCancel)}L.current=null,Jt.unstable_batchedUpdates(()=>{C({type:qe}),I(tr.Uninitialized),se(null),S(null),F(null),ne.current=null;const nr=qe===et.DragEnd?"onDragEnd":"onDragCancel";if(Wt){const mr=$.current[nr];mr?.(Wt),O({type:nr,event:Wt})}})}}},[w]),ve=k.useCallback((Ee,pe)=>(Ne,Ke)=>{const $e=Ne.nativeEvent,Be=w.get(Ke);if(L.current!==null||!Be||$e.dndKit||$e.defaultPrevented)return;const Ze={active:Be};Ee(Ne,pe.options,Ze)===!0&&($e.dndKit={capturedBy:pe.sensor},L.current=Ke,Te(Ne,pe))},[w,Te]),Se=ah(c,ve);gh(c),Yt(()=>{ee&&E===tr.Initializing&&I(tr.Initialized)},[ee,E]),k.useEffect(()=>{const{onDragMove:Ee}=$.current,{active:pe,activatorEvent:Ne,collisions:Ke,over:$e}=he.current;if(!pe||!Ne)return;const Be={active:pe,activatorEvent:Ne,collisions:Ke,delta:{x:He.x,y:He.y},over:$e};Jt.unstable_batchedUpdates(()=>{Ee?.(Be),O({type:"onDragMove",event:Be})})},[He.x,He.y]),k.useEffect(()=>{const{active:Ee,activatorEvent:pe,collisions:Ne,droppableContainers:Ke,scrollAdjustedTranslate:$e}=he.current;if(!Ee||L.current==null||!pe||!$e)return;const{onDragOver:Be}=$.current,Ze=Ke.get(Oe),qt=Ze&&Ze.rect.current?{id:Ze.id,rect:Ze.rect.current,data:Ze.data,disabled:Ze.disabled}:null,qe={active:Ee,activatorEvent:pe,collisions:Ne,delta:{x:$e.x,y:$e.y},over:qt};Jt.unstable_batchedUpdates(()=>{se(qt),Be?.(qe),O({type:"onDragOver",event:qe})})},[Oe]),Yt(()=>{he.current={activatorEvent:b,active:D,activeNode:q,collisionRect:Pe,collisions:Ve,droppableRects:K,draggableNodes:w,draggingNode:ye,draggingNodeRect:Me,droppableContainers:T,over:X,scrollableAncestors:Le,scrollAdjustedTranslate:He},P.current={initial:Me,translated:Pe}},[D,q,Ve,Pe,w,ye,Me,K,T,X,Le,He]),th({...te,delta:y,draggingRect:Pe,pointerCoordinates:oe,scrollableAncestors:Le,scrollableAncestorRects:V});const _e=k.useMemo(()=>({active:D,activeNode:q,activeNodeRect:ee,activatorEvent:b,collisions:Ve,containerNodeRect:xe,dragOverlay:De,draggableNodes:w,droppableContainers:T,droppableRects:K,over:X,measureDroppableContainers:j,scrollableAncestors:Le,scrollableAncestorRects:V,measuringConfiguration:B,measuringScheduled:H,windowRect:Fe}),[D,q,ee,b,Ve,xe,De,w,T,K,X,j,Le,V,B,H,Fe]),Ue=k.useMemo(()=>({activatorEvent:b,activators:Se,active:D,activeNodeRect:ee,ariaDescribedById:{draggable:G},dispatch:C,draggableNodes:w,over:X,measureDroppableContainers:j}),[b,Se,D,ee,C,G,w,X,j]);return k.createElement(ni.Provider,{value:v},k.createElement(Vn.Provider,{value:Ue},k.createElement(Ai.Provider,{value:_e},k.createElement(Mi.Provider,{value:le},h)),k.createElement(Sh,{disabled:l?.restoreFocus===!1})),k.createElement(_g,{...l,hiddenTextDescribedById:G}));function je(){const Ee=U?.autoScrollEnabled===!1,pe=typeof f=="object"?f.enabled===!1:f===!1,Ne=_&&!Ee&&!pe;return typeof f=="object"?{...f,enabled:Ne}:{enabled:Ne}}}),Ah=k.createContext(null),Oi="button",Mh="Draggable";function Oh(e){let{id:t,data:r,disabled:n=!1,attributes:o}=e;const a=en(Mh),{activators:s,activatorEvent:l,active:f,activeNodeRect:h,ariaDescribedById:c,draggableNodes:i,over:u}=k.useContext(Vn),{role:g=Oi,roleDescription:p="draggable",tabIndex:m=0}=o??{},x=f?.id===t,C=k.useContext(x?Mi:Ah),[O,v]=zn(),[E,I]=zn(),_=hh(s,t),M=Jr(r);Yt(()=>(i.set(t,{id:t,key:a,node:O,activatorNode:E,data:M}),()=>{const y=i.get(t);y&&y.key===a&&i.delete(t)}),[i,t]);const w=k.useMemo(()=>({role:g,tabIndex:m,"aria-disabled":n,"aria-pressed":x&&g===Oi?!0:void 0,"aria-roledescription":p,"aria-describedby":c.draggable}),[n,g,m,x,p,c.draggable]);return{active:f,activatorEvent:l,activeNodeRect:h,attributes:w,isDragging:x,listeners:n?void 0:_,node:O,over:u,setNodeRef:v,setActivatorNodeRef:I,transform:C}}function Ih(){return k.useContext(Ai)}const Dh="Droppable",Lh={timeout:25};function Ph(e){let{data:t,disabled:r=!1,id:n,resizeObserverConfig:o}=e;const a=en(Dh),{active:s,dispatch:l,over:f,measureDroppableContainers:h}=k.useContext(Vn),c=k.useRef({disabled:r}),i=k.useRef(!1),u=k.useRef(null),g=k.useRef(null),{disabled:p,updateMeasurementsFor:m,timeout:x}={...Lh,...o},C=Jr(m??n),O=k.useCallback(()=>{if(!i.current){i.current=!0;return}g.current!=null&&clearTimeout(g.current),g.current=setTimeout(()=>{h(Array.isArray(C.current)?C.current:[C.current]),g.current=null},x)},[x]),v=Bn({callback:O,disabled:p||!s}),E=k.useCallback((w,y)=>{v&&(y&&(v.unobserve(y),i.current=!1),w&&v.observe(w))},[v]),[I,_]=zn(E),M=Jr(t);return k.useEffect(()=>{!v||!I.current||(v.disconnect(),i.current=!1,v.observe(I.current))},[I,v]),k.useEffect(()=>(l({type:et.RegisterDroppable,element:{id:n,key:a,disabled:r,node:I,rect:u,data:M}}),()=>l({type:et.UnregisterDroppable,key:a,id:n})),[n]),k.useEffect(()=>{r!==c.current.disabled&&(l({type:et.SetDroppableDisabled,id:n,key:a,disabled:r}),c.current.disabled=r)},[n,a,r,l]),{active:s,rect:u,isOver:f?.id===n,node:I,over:f,setNodeRef:_}}function Ca(e,t,r){const n=e.slice();return n.splice(r<0?n.length+r:r,0,n.splice(t,1)[0]),n}function $h(e,t){return e.reduce((r,n,o)=>{const a=t.get(n);return a&&(r[o]=a),r},Array(e.length))}function Kn(e){return e!==null&&e>=0}function Fh(e,t){if(e===t)return!0;if(e.length!==t.length)return!1;for(let r=0;r<e.length;r++)if(e[r]!==t[r])return!1;return!0}function Uh(e){return typeof e=="boolean"?{draggable:e,droppable:e}:e}const Yn=e=>{let{rects:t,activeIndex:r,overIndex:n,index:o}=e;const a=Ca(t,n,r),s=t[o],l=a[o];return!l||!s?null:{x:l.left-s.left,y:l.top-s.top,scaleX:l.width/s.width,scaleY:l.height/s.height}},Ii="Sortable",Di=k.createContext({activeIndex:-1,containerId:Ii,disableTransforms:!1,items:[],overIndex:-1,useDragOverlay:!1,sortedRects:[],strategy:Yn,disabled:{draggable:!1,droppable:!1}});function Li(e){let{children:t,id:r,items:n,strategy:o=Yn,disabled:a=!1}=e;const{active:s,dragOverlay:l,droppableRects:f,over:h,measureDroppableContainers:c}=Ih(),i=en(Ii,r),u=l.rect!==null,g=k.useMemo(()=>n.map(_=>typeof _=="object"&&"id"in _?_.id:_),[n]),p=s!=null,m=s?g.indexOf(s.id):-1,x=h?g.indexOf(h.id):-1,C=k.useRef(g),O=!Fh(g,C.current),v=x!==-1&&m===-1||O,E=Uh(a);Yt(()=>{O&&p&&c(g)},[O,g,p,c]),k.useEffect(()=>{C.current=g},[g]);const I=k.useMemo(()=>({activeIndex:m,containerId:i,disabled:E,disableTransforms:v,items:g,overIndex:x,useDragOverlay:u,sortedRects:$h(g,f),strategy:o}),[m,i,E.draggable,E.droppable,v,g,x,f,u,o]);return k.createElement(Di.Provider,{value:I},t)}const Pi=e=>{let{id:t,items:r,activeIndex:n,overIndex:o}=e;return Ca(r,n,o).indexOf(t)},$i=e=>{let{containerId:t,isSorting:r,wasDragging:n,index:o,items:a,newIndex:s,previousItems:l,previousContainerId:f,transition:h}=e;return!h||!n||l!==a&&o===s?!1:r?!0:s!==o&&t===f},zh={duration:200,easing:"ease"},Fi="transform",Hh=rn.Transition.toString({property:Fi,duration:0,easing:"linear"}),Gh={roleDescription:"sortable"};function jh(e){let{disabled:t,index:r,node:n,rect:o}=e;const[a,s]=k.useState(null),l=k.useRef(r);return Yt(()=>{if(!t&&r!==l.current&&n.current){const f=o.current;if(f){const h=Ir(n.current,{ignoreTransform:!0}),c={x:f.left-h.left,y:f.top-h.top,scaleX:f.width/h.width,scaleY:f.height/h.height};(c.x||c.y)&&s(c)}}r!==l.current&&(l.current=r)},[t,r,n,o]),k.useEffect(()=>{a&&s(null)},[a]),a}function Ui(e){let{animateLayoutChanges:t=$i,attributes:r,disabled:n,data:o,getNewIndex:a=Pi,id:s,strategy:l,resizeObserverConfig:f,transition:h=zh}=e;const{items:c,containerId:i,activeIndex:u,disabled:g,disableTransforms:p,sortedRects:m,overIndex:x,useDragOverlay:C,strategy:O}=k.useContext(Di),v=Bh(n,g),E=c.indexOf(s),I=k.useMemo(()=>({sortable:{containerId:i,index:E,items:c},...o}),[i,o,E,c]),_=k.useMemo(()=>c.slice(c.indexOf(s)),[c,s]),{rect:M,node:w,isOver:y,setNodeRef:T}=Ph({id:s,data:I,disabled:v.droppable,resizeObserverConfig:{updateMeasurementsFor:_,...f}}),{active:A,activatorEvent:P,activeNodeRect:D,attributes:L,setNodeRef:U,listeners:S,isDragging:b,over:F,setActivatorNodeRef:$,transform:G}=Oh({id:s,data:I,attributes:{...Gh,...r},disabled:v.draggable}),Y=fg(T,U),B=!!A,K=B&&!p&&Kn(u)&&Kn(x),j=!C&&b,H=j&&K?G:null,ue=K?H??(l??O)({rects:m,activeNodeRect:D,activeIndex:u,overIndex:x,index:E}):null,te=Kn(u)&&Kn(x)?a({id:s,items:c,activeIndex:u,overIndex:x}):E,fe=A?.id,ee=k.useRef({activeId:fe,items:c,newIndex:te,containerId:i}),xe=c!==ee.current.items,he=t({active:A,containerId:i,isDragging:b,isSorting:B,id:s,index:E,items:c,newIndex:ee.current.newIndex,previousItems:ee.current.items,previousContainerId:ee.current.containerId,transition:h,wasDragging:ee.current.activeId!=null}),Ge=jh({disabled:!he,index:E,node:w,rect:M});return k.useEffect(()=>{B&&ee.current.newIndex!==te&&(ee.current.newIndex=te),i!==ee.current.containerId&&(ee.current.containerId=i),c!==ee.current.items&&(ee.current.items=c)},[B,te,i,c]),k.useEffect(()=>{if(fe===ee.current.activeId)return;if(fe!=null&&ee.current.activeId==null){ee.current.activeId=fe;return}const ye=setTimeout(()=>{ee.current.activeId=fe},50);return()=>clearTimeout(ye)},[fe]),{active:A,activeIndex:u,attributes:L,data:I,rect:M,index:E,newIndex:te,items:c,isOver:y,isSorting:B,isDragging:b,listeners:S,node:w,overIndex:x,over:F,setNodeRef:Y,setActivatorNodeRef:$,setDroppableNodeRef:T,setDraggableNodeRef:U,transform:Ge??ue,transition:De()};function De(){if(Ge||xe&&ee.current.newIndex===E)return Hh;if(!(j&&!ua(P)||!h)&&(B||he))return rn.Transition.toString({...h,property:Fi})}}function Bh(e,t){var r,n;return typeof e=="boolean"?{draggable:e,droppable:!1}:{draggable:(r=e?.draggable)!=null?r:t.draggable,droppable:(n=e?.droppable)!=null?n:t.droppable}}function ln(e){if(!e)return!1;const t=e.data.current;return!!(t&&"sortable"in t&&typeof t.sortable=="object"&&"containerId"in t.sortable&&"items"in t.sortable&&"index"in t.sortable)}const Wh=[ze.Down,ze.Right,ze.Up,ze.Left],zi=(e,t)=>{let{context:{active:r,collisionRect:n,droppableRects:o,droppableContainers:a,over:s,scrollableAncestors:l}}=t;if(Wh.includes(e.code)){if(e.preventDefault(),!r||!n)return;const f=[];a.getEnabled().forEach(i=>{if(!i||i!=null&&i.disabled)return;const u=o.get(i.id);if(u)switch(e.code){case ze.Down:n.top<u.top&&f.push(i);break;case ze.Up:n.top>u.top&&f.push(i);break;case ze.Left:n.left>u.left&&f.push(i);break;case ze.Right:n.left<u.left&&f.push(i);break}});const h=Ag({collisionRect:n,droppableRects:o,droppableContainers:f});let c=ii(h,"id");if(c===s?.id&&h.length>1&&(c=h[1].id),c!=null){const i=a.get(r.id),u=a.get(c),g=u?o.get(u.id):null,p=u?.node.current;if(p&&g&&i&&u){const x=Gn(p).some((_,M)=>l[M]!==_),C=Hi(i,u),O=Vh(i,u),v=x||!C?{x:0,y:0}:{x:O?n.width-g.width:0,y:O?n.height-g.height:0},E={x:g.left,y:g.top};return v.x&&v.y?E:tn(E,v)}}}};function Hi(e,t){return!ln(e)||!ln(t)?!1:e.data.current.sortable.containerId===t.data.current.sortable.containerId}function Vh(e,t){return!ln(e)||!ln(t)||!Hi(e,t)?!1:e.data.current.sortable.index<t.data.current.sortable.index}const Kh=Object.freeze(Object.defineProperty({__proto__:null,SortableContext:Li,arrayMove:Ca,defaultAnimateLayoutChanges:$i,defaultNewIndexGetter:Pi,hasSortableData:ln,rectSortingStrategy:Yn,sortableKeyboardCoordinates:zi,useSortable:Ui},Symbol.toStringTag,{value:"Module"})),Yh=window.G7Core,Gi=({file:e,onRemove:t,onRetry:r,onImageClick:n,onDownload:o,isPrimary:a,onPrimaryClick:s})=>{const{attributes:l,listeners:f,setNodeRef:h,transform:c,transition:i,isDragging:u}=Ui({id:"hash"in e?e.hash:e.id}),g={transform:rn.Transform.toString(c),transition:i,opacity:u?.5:1},p="status"in e&&e.status!==void 0,m=p?e.status:"done",x=m==="uploading",C=m==="compressing",O=m==="error",v=p?e.progress:100,E=p?e.preview:void 0,I=p?e.mime_type?.startsWith("image/")??!1:e.is_image,[_,M]=k.useState(void 0),w=!p&&I?e.download_url:void 0;k.useEffect(()=>{if(!w){M(void 0);return}if(Yo(w)){M(w);return}let T=!0,A;return(async()=>{try{const D=await Yh.api.get(w,{responseType:"blob"});T&&D&&(A=URL.createObjectURL(D),M(A))}catch(D){console.error("Failed to load authenticated image:",D)}})(),()=>{T=!1,A&&URL.revokeObjectURL(A)}},[w]);const y=_;return d.jsxs(W,{ref:h,style:g,className:"relative group bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[d.jsxs(W,{className:`aspect-square w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900 relative ${I||!p?"cursor-pointer":""}`,onClick:()=>{x||C||(I&&n?n():!p&&o&&o())},children:[E||y?d.jsx(Lt,{src:E||y,alt:e.original_filename,className:"w-full h-full object-cover"}):d.jsx(Ct,{className:`fa-solid ${Ql(e.mime_type)} text-4xl text-gray-400 dark:text-gray-500`}),(x||C)&&d.jsxs(W,{className:"absolute inset-0 bg-black/50 flex flex-col items-center justify-center",children:[d.jsx(W,{className:"w-3/4 h-2 bg-gray-700 dark:bg-gray-600 rounded-full overflow-hidden",children:d.jsx(W,{className:"h-full bg-blue-500 dark:bg-blue-400 transition-all",style:{width:`${v}%`}})}),d.jsxs(R,{className:"text-white text-xs mt-1",children:[v,"%"]})]}),O&&d.jsx(W,{className:"absolute inset-0 bg-red-500/50 dark:bg-red-600/50 flex items-center justify-center",children:d.jsx(Ct,{className:"fa-solid fa-exclamation-triangle text-2xl text-white"})}),s&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),s()},className:`absolute top-1.5 left-1.5 z-10 p-1 rounded-full transition-colors ${a?"bg-yellow-500 text-white":"bg-gray-800/50 text-gray-300 hover:bg-yellow-500 hover:text-white opacity-0 group-hover:opacity-100"}`,title:Re(a?"attachment.primary_image":"attachment.set_as_primary"),children:d.jsx(Ct,{className:"fa-solid fa-star text-xs"})}),d.jsxs(W,{className:"absolute inset-0 bg-black/0 group-hover:bg-black/40 transition-colors flex items-start justify-end p-1 gap-1 opacity-0 group-hover:opacity-100",children:[d.jsx(Q,{type:"button",...l,...f,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-gray-600 dark:text-gray-300 hover:bg-white cursor-grab active:cursor-grabbing",title:Re("attachment.drag_to_reorder"),onClick:T=>T.stopPropagation(),children:d.jsx(Ct,{className:"fa-solid fa-grip-vertical text-sm"})}),!p&&o&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),o()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-blue-500 hover:bg-white",title:Re("common.download"),children:d.jsx(Ct,{className:"fa-solid fa-download text-sm"})}),O&&r&&d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),r()},className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-orange-500 hover:bg-white",title:Re("common.retry"),children:d.jsx(Ct,{className:"fa-solid fa-arrow-rotate-right text-sm"})}),d.jsx(Q,{type:"button",onClick:T=>{T.stopPropagation(),t()},disabled:x||C,className:"p-1.5 bg-white/90 dark:bg-gray-800/90 rounded text-red-500 hover:bg-white disabled:opacity-50",title:Re("common.delete"),children:d.jsx(Ct,{className:"fa-solid fa-times text-sm"})})]})]}),d.jsxs(W,{className:`p-2 ${!p&&o?"cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700":""}`,onClick:()=>{!p&&o&&o()},children:[d.jsx(Dt,{className:"text-xs font-medium text-gray-900 dark:text-white truncate",title:e.original_filename,children:e.original_filename}),d.jsx(Dt,{className:"text-xs text-gray-500 dark:text-gray-400",children:e.size_formatted})]})]})},ji=({allItems:e,canAddMore:t,totalCount:r,maxFiles:n,maxSize:o,accept:a,onRemove:s,onRetry:l,onImageClick:f,onDownload:h,onDragEnd:c,onAddClick:i,enablePrimarySelection:u,primaryFileId:g,onPrimaryChange:p})=>{const m=Eg(oi(va,{activationConstraint:{distance:8}}),oi(ba,{coordinateGetter:zi}));return d.jsxs(W,{className:"p-3",children:[d.jsx(Nh,{sensors:m,collisionDetection:Ng,onDragEnd:c,children:d.jsx(Li,{items:e.map(x=>"hash"in x?x.hash:x.id),strategy:Yn,children:d.jsxs(W,{className:"grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3",children:[e.map(x=>{const C="hash"in x,O=C?x.is_image:x.mime_type?.startsWith("image/")??!1;return d.jsx(Gi,{file:x,onRemove:()=>s(x),onRetry:"status"in x&&x.status==="error"?()=>l(x):void 0,onImageClick:O?()=>f(x):void 0,onDownload:C?()=>h(x):void 0,isPrimary:u&&C&&(x.hash||x.id)===g,onPrimaryClick:u&&C?()=>p?.(x.hash||x.id):void 0},C?x.hash:x.id)}),t&&d.jsxs(W,{className:"aspect-square flex flex-col items-center justify-center border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer hover:border-gray-400 dark:hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors",onClick:i,children:[d.jsx(Ct,{className:"fa-solid fa-plus text-2xl text-gray-400 dark:text-gray-500 mb-1"}),d.jsx(R,{className:"text-xs text-gray-500 dark:text-gray-400",children:Re("common.add")})]})]})})}),d.jsxs(W,{className:"mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 flex justify-between items-center text-xs text-gray-500 dark:text-gray-400",children:[d.jsx(R,{children:Re("attachment.attached_count",{count:r,max:n})}),d.jsxs(R,{children:[Re("attachment.max_size",{size:o}),a&&` (${a})`]})]})]})},Xh=[],qh=[],Xn=k.forwardRef(({uploadTriggerEvent:e,attachmentableType:t,attachmentableId:r,collection:n="default",maxFiles:o=10,maxSize:a=10,maxConcurrentUploads:s=3,accept:l,imageCompression:f,roleIds:h=qh,autoUpload:c=!0,onFilesChange:i,onChange:u,onUploadComplete:g,onUploadError:p,onRemove:m,onReorder:x,initialFiles:C=Xh,className:O="",confirmBeforeRemove:v=!1,confirmRemoveTitle:E,confirmRemoveMessage:I,apiEndpoints:_,uploadParams:M,enablePrimarySelection:w=!1,primaryFileId:y,onPrimaryChange:T,id:A,editorAttrs:P},D)=>{const L={upload:_?.upload??"/api/attachments",delete:_?.delete??"/api/attachments/:id",reorder:_?.reorder??"/api/attachments/reorder"},U=k.useRef(u??i);U.current=u??i;const S=k.useCallback(G=>{const Y=U.current;if(!Y)return;Y({target:{value:G,files:G},preventDefault:()=>{},stopPropagation:()=>{}})},[]),b=Jl({attachmentableType:t,attachmentableId:r,collection:n,maxFiles:o,maxSize:a,maxConcurrentUploads:s,accept:l,imageCompression:f,roleIds:h,autoUpload:c,initialFiles:C,endpoints:L,onFilesChange:S,onUploadComplete:g,onUploadError:p,onRemove:m,onReorder:x,uploadTriggerEvent:e,confirmBeforeRemove:v,uploadParams:M});k.useImperativeHandle(D,()=>({uploadAll:b.handleUploadAll,clear:b.clear,getPendingFiles:b.getPendingFiles}));const F=k.useMemo(()=>b.imageFiles.map(G=>{const Y=!("hash"in G),B=G,K=G;return Y?{src:B.preview||"",filename:B.original_filename,title:B.original_filename,downloadRequiresAuth:!1}:{src:b.authenticatedImageUrls.get(K.id)||K.download_url,downloadUrl:K.download_url,filename:K.original_filename,title:K.original_filename,downloadRequiresAuth:!0}}),[b.imageFiles,b.authenticatedImageUrls]),$=k.useMemo(()=>({existingFiles:b.existingFiles,pendingFiles:b.pendingFiles,isDragOver:b.isDragOver,isDeleting:b.isDeleting,maxFiles:o,maxSize:a,accept:l,endpoints:L,confirmBeforeRemove:v,totalCount:b.totalCount,hasFiles:b.hasFiles,canAddMore:b.canAddMore,allItems:b.allItems,imageFiles:b.imageFiles,handleFiles:b.handleFiles,handleRemove:b.handleRemove,handleRetry:b.handleRetry,handleUploadAll:b.handleUploadAll,handleOpenGallery:b.handleOpenGallery,handleDownload:b.handleDownload,handleDragEnd:b.handleDragEnd,setIsDragOver:b.setIsDragOver,inputRef:b.inputRef,galleryOpen:b.galleryOpen,setGalleryOpen:b.setGalleryOpen,galleryStartIndex:b.galleryStartIndex,galleryKeyRef:b.galleryKeyRef,confirmDialogOpen:b.confirmDialogOpen,setConfirmDialogOpen:b.setConfirmDialogOpen,itemToDelete:b.itemToDelete,executeRemoveAttachment:b.executeRemoveAttachment,authenticatedImageUrls:b.authenticatedImageUrls}),[b,o,a,l,L,v]);return d.jsx(Fl,{value:$,children:d.jsxs(W,{id:A,className:O,...P,children:[d.jsx(Rl,{isDragOver:b.isDragOver,canAddMore:b.canAddMore,onFiles:b.handleFiles,setIsDragOver:b.setIsDragOver,inputRef:b.inputRef,accept:l,maxFiles:o,maxSize:a,children:b.hasFiles&&d.jsx(ji,{allItems:b.allItems,canAddMore:b.canAddMore,totalCount:b.totalCount,maxFiles:o,maxSize:a,accept:l,onRemove:b.handleRemove,onRetry:b.handleRetry,onImageClick:b.handleOpenGallery,onDownload:b.handleDownload,onDragEnd:b.handleDragEnd,onAddClick:()=>b.inputRef.current?.click(),enablePrimarySelection:w,primaryFileId:y,onPrimaryChange:T})}),v&&d.jsx(ra,{isOpen:b.confirmDialogOpen,onClose:()=>{b.isDeleting||b.setConfirmDialogOpen(!1)},title:E??Re("attachment.delete_confirm_title"),message:I??Re("attachment.delete_confirm_message"),confirmText:Re("common.delete"),confirmButtonVariant:"danger",onConfirm:()=>{b.itemToDelete&&b.executeRemoveAttachment(b.itemToDelete)},isLoading:b.isDeleting,loadingText:Re("attachment.deleting")}),F.length>0&&b.galleryOpen&&d.jsx(Tr,{images:F,isOpen:!0,onClose:()=>b.setGalleryOpen(!1),startIndex:b.galleryStartIndex,showDownload:!0},`gallery-${b.galleryKeyRef.current}`)]})})});Xn.displayName="FileUploader";const Zh=Object.freeze(Object.defineProperty({__proto__:null,FileDropZone:Rl,FileList:ji,FileUploader:Xn,FileUploaderProvider:Fl,SortableThumbnailItem:Gi,default:Xn,extractErrorMessage:Zl,fileUploaderT:Re,formatFileSize:Fn,getFileIcon:Ql,useFileUploader:Jl,useFileUploaderContext:rg},Symbol.toStringTag,{value:"Module"})),Bi=({provider:e,className:t="w-5 h-5"})=>{switch(e){case"google":return d.jsxs("svg",{className:t,viewBox:"0 0 24 24",children:[d.jsx("path",{fill:"#4285F4",d:"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"}),d.jsx("path",{fill:"#34A853",d:"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"}),d.jsx("path",{fill:"#FBBC05",d:"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"}),d.jsx("path",{fill:"#EA4335",d:"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"})]});case"naver":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M16.273 12.845L7.376 0H0v24h7.727V11.155L16.624 24H24V0h-7.727z"})});case"kakao":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M12 3c5.799 0 10.5 3.664 10.5 8.185 0 4.52-4.701 8.184-10.5 8.184a13.5 13.5 0 0 1-1.727-.11l-4.408 2.883c-.501.265-.678.236-.472-.413l.892-3.678c-2.88-1.46-4.785-3.99-4.785-6.866C1.5 6.665 6.201 3 12 3zm5.519 8.625h-.634v2.916h-.97v-2.916h-.633v-.848h2.237v.848zm-3.294 2.916h-.97V10.77h.97v3.771zm-1.97 0h-.97v-2.77l-.658 1.53h-.629l-.665-1.53v2.77h-.97V10.77h1.13l.822 1.912.814-1.912h1.126v3.771zm-4.49 0h-.97V10.77h.97v3.771z"})});case"facebook":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"})});case"apple":return d.jsx("svg",{className:t,viewBox:"0 0 24 24",children:d.jsx("path",{fill:"currentColor",d:"M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"})});default:return null}},Wi=(e,t)=>window.G7Core?.t?.(e,t)??e,Qh=[{id:"google",name:"Google",color:"bg-white dark:bg-gray-800",textColor:"text-gray-700 dark:text-gray-300"},{id:"naver",name:"네이버",color:"bg-[#03C75A]",textColor:"text-white"},{id:"kakao",name:"카카오",color:"bg-[#FEE500]",textColor:"text-[#000000]/85"},{id:"facebook",name:"Facebook",color:"bg-[#1877F2]",textColor:"text-white"},{id:"apple",name:"Apple",color:"bg-black dark:bg-white",textColor:"text-white dark:text-black"}],Vi=({providers:e=["google","naver","kakao"],mode:t="login",variant:r="full",className:n="",id:o,editorAttrs:a})=>{const s=h=>{const c=encodeURIComponent(window.location.href);window.location.href=`/api/auth/${h}?redirect=${c}`},l=h=>t==="register"?Wi("auth.social.register_with",{provider:h}):Wi("auth.social.login_with",{provider:h}),f=Qh.filter(h=>e.includes(h.id));return r==="icon"?d.jsx(W,{className:`flex justify-center gap-4 ${n}`,id:o,...a,children:f.map(h=>d.jsx(Q,{onClick:()=>s(h.id),className:`w-12 h-12 flex items-center justify-center rounded-full ${h.color} ${h.textColor} border border-gray-200 dark:border-gray-700 hover:opacity-90 transition-opacity`,"aria-label":l(h.name),children:d.jsx(Bi,{provider:h.id,className:"w-5 h-5"})},h.id))}):d.jsx(W,{className:`space-y-3 ${n}`,id:o,...a,children:f.map(h=>d.jsxs(Q,{onClick:()=>s(h.id),className:`w-full flex items-center justify-center gap-3 px-4 py-3 rounded-lg font-medium border border-gray-200 dark:border-gray-700 ${h.color} ${h.textColor} hover:opacity-90 transition-opacity`,children:[d.jsx(Bi,{provider:h.id,className:"w-5 h-5"}),d.jsx(R,{children:l(h.name)})]},h.id))})},Jh=Object.freeze(Object.defineProperty({__proto__:null,default:Vi},Symbol.toStringTag,{value:"Module"})),Rh=(e,t)=>window.G7Core?.t?.(e,t)??e,e0={success:Je.CheckCircle,error:Je.XCircle,warning:Je.ExclamationTriangle,info:Je.InfoCircle},Ki={success:{bg:"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800",text:"text-green-800 dark:text-green-300"},error:{bg:"bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800",text:"text-red-800 dark:text-red-300"},warning:{bg:"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800",text:"text-yellow-800 dark:text-yellow-300"},info:{bg:"bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800",text:"text-blue-800 dark:text-blue-300"}},t0={"top-left":"top-4 left-4","top-center":"top-4 left-1/2 -translate-x-1/2","top-right":"top-4 right-4","bottom-left":"bottom-4 left-4","bottom-center":"bottom-4 left-1/2 -translate-x-1/2","bottom-right":"bottom-4 right-4"},Sa=({toasts:e,position:t="bottom-center",duration:r=3e3,onRemove:n})=>{const[o,a]=k.useState(new Map),s=k.useCallback(l=>{a(f=>{const h=new Map(f);return h.set(l,!1),h}),setTimeout(()=>{if(n)n(l);else{const f=window.G7Core;f?.state?.update&&f.state.update(h=>({toasts:(Array.isArray(h.toasts)?h.toasts:[]).filter(i=>i.id!==l)}))}},300)},[n]);return k.useEffect(()=>{!e||e.length===0||e.forEach(l=>{if(o.has(l.id))return;a(h=>{const c=new Map(h);return c.set(l.id,!0),c});const f=l.duration||r;f>0&&setTimeout(()=>{s(l.id)},f)})},[e,r,s,o]),!e||e.length===0?null:d.jsx(W,{className:`fixed z-[9999] flex flex-col gap-2 ${t0[t]}`,"aria-live":"polite","aria-atomic":"true",children:e.map(l=>d.jsx(r0,{toast:l,position:t,isVisible:o.get(l.id)??!0,onClose:()=>s(l.id)},l.id))})},r0=({toast:e,position:t,isVisible:r,onClose:n})=>{const o=Ki[e.type]||Ki.info,a=e.icon?e.icon:e0[e.type],s=t.startsWith("bottom")?"translate-y-2":"-translate-y-2";return d.jsxs(W,{className:` flex items-center gap-3 p-4 rounded-lg border shadow-lg min-w-[300px] max-w-md transition-all duration-300 ${o.bg} ${r?"opacity-100 translate-y-0":`opacity-0 ${s}`} - `,role:"alert",children:[d.jsx(de,{name:a,className:`w-5 h-5 flex-shrink-0 ${o.text}`}),d.jsx(R,{className:`flex-1 text-sm font-medium ${o.text}`,children:e.message}),d.jsx(Q,{onClick:n,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${o.text} focus:outline-none`,"aria-label":Jh("common.close"),children:d.jsx(de,{name:Je.Times,className:"w-4 h-4"})})]})},r0=Object.freeze(Object.defineProperty({__proto__:null,Toast:Ca,default:Ca},Symbol.toStringTag,{value:"Module"})),n0=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...e)=>console.log("[Comp:PageTransition]",...e),warn:(...e)=>console.warn("[Comp:PageTransition]",...e),error:(...e)=>console.error("[Comp:PageTransition]",...e)},Sa=({className:e="",style:t})=>{const[r,n]=k.useState(!1);return k.useEffect(()=>{const o=window.G7Core?.TransitionManager;if(!o){n0.warn("TransitionManager를 찾을 수 없습니다.");return}return n(o.getIsPending()),o.subscribe(s=>{n(s)})},[]),r?d.jsxs(W,{className:`fixed top-0 left-0 right-0 z-50 ${e}`,style:t,role:"progressbar","aria-label":"페이지 로딩 중",children:[d.jsx(W,{className:"h-1 bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 animate-pulse",children:d.jsx(W,{className:"h-full bg-blue-700 animate-[loading_1.5s_ease-in-out_infinite]",style:{width:"30%",animation:"loading 1.5s ease-in-out infinite"}})}),d.jsx("style",{children:` + `,role:"alert",children:[d.jsx(de,{name:a,className:`w-5 h-5 flex-shrink-0 ${o.text}`}),d.jsx(R,{className:`flex-1 text-sm font-medium ${o.text}`,children:e.message}),d.jsx(Q,{onClick:n,className:`flex-shrink-0 p-1 rounded hover:bg-black/10 dark:hover:bg-white/10 ${o.text} focus:outline-none`,"aria-label":Rh("common.close"),children:d.jsx(de,{name:Je.Times,className:"w-4 h-4"})})]})},n0=Object.freeze(Object.defineProperty({__proto__:null,Toast:Sa,default:Sa},Symbol.toStringTag,{value:"Module"})),o0=window.G7Core?.createLogger?.("Comp:PageTransition")??{log:(...e)=>console.log("[Comp:PageTransition]",...e),warn:(...e)=>console.warn("[Comp:PageTransition]",...e),error:(...e)=>console.error("[Comp:PageTransition]",...e)},_a=({className:e="",style:t})=>{const[r,n]=k.useState(!1);return k.useEffect(()=>{const o=window.G7Core?.TransitionManager;if(!o){o0.warn("TransitionManager를 찾을 수 없습니다.");return}return n(o.getIsPending()),o.subscribe(s=>{n(s)})},[]),r?d.jsxs(W,{className:`fixed top-0 left-0 right-0 z-50 ${e}`,style:t,role:"progressbar","aria-label":"페이지 로딩 중",children:[d.jsx(W,{className:"h-1 bg-gradient-to-r from-blue-500 via-blue-600 to-blue-500 animate-pulse",children:d.jsx(W,{className:"h-full bg-blue-700 animate-[loading_1.5s_ease-in-out_infinite]",style:{width:"30%",animation:"loading 1.5s ease-in-out infinite"}})}),d.jsx("style",{children:` @keyframes loading { 0% { transform: translateX(-100%); @@ -83,5 +83,4 @@ self.addEventListener('message', async (e) => { transform: translateX(-100%); } } - `})]}):null},o0=Object.freeze(Object.defineProperty({__proto__:null,PageTransitionIndicator:Sa,default:Sa},Symbol.toStringTag,{value:"Module"})),Ki=window.G7Core?.createLogger?.("Comp:PageTransitionBlur")??{log:(...e)=>console.log("[Comp:PageTransitionBlur]",...e),warn:(...e)=>console.warn("[Comp:PageTransitionBlur]",...e)},_a=({className:e="",style:t})=>{const[r,n]=k.useState(!1);return k.useEffect(()=>{const o=window.G7Core?.TransitionManager;if(!o){Ki.warn("TransitionManager를 찾을 수 없습니다.");return}return n(o.getIsPending()),o.subscribe(s=>{Ki.log("isPending changed:",s),n(s)})},[]),r?d.jsx(W,{className:`fixed inset-0 z-40 bg-white/30 dark:bg-gray-900/30 backdrop-blur-sm transition-opacity duration-150 pointer-events-none ${e}`,style:t,role:"status","aria-label":"페이지 전환 중","data-testid":"page-transition-blur"}):null},a0=Object.freeze(Object.defineProperty({__proto__:null,PageTransitionBlur:_a,default:_a},Symbol.toStringTag,{value:"Module"})),Yi={sm:640,md:768,lg:1024,xl:1280,"2xl":1536},Xi=new Set(["hidden","block","inline-block","inline","flex","inline-flex","grid","inline-grid","table","table-row","table-cell","contents","list-item","flow-root"]),s0=new Set(["Div","Flex","Grid","Section","Article","Nav","Aside","Main","Header","Footer","Fragment","Form","Container"]),l0=new Set(["H1","H2","H3","H4","H5","H6","P","Span","Label","A","Strong","Em","Small","Badge"]),i0=new Set(["Input","Textarea","Select","Checkbox","Radio","Toggle","DatePicker","TagInput","SearchBar"]),c0=new Set(["Img","Image","Avatar","AvatarUploader","ImageGallery","ProductImageViewer","FileUploader"]),qi={DataGrid:(e,t)=>d.jsxs("div",{className:"w-full border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[d.jsx("div",{className:`h-10 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 ${e}`,children:d.jsxs("div",{className:"flex items-center h-full px-4 gap-8",children:[d.jsx("div",{className:"h-3 w-16 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-24 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-20 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-16 bg-gray-300 dark:bg-gray-600 rounded"})]})}),Array.from({length:t},(r,n)=>d.jsx("div",{className:`h-12 border-b border-gray-100 dark:border-gray-800 ${e}`,style:{animationDelay:`${n*75}ms`},children:d.jsxs("div",{className:"flex items-center h-full px-4 gap-8",children:[d.jsx("div",{className:"h-3 w-12 bg-gray-200 dark:bg-gray-700 rounded"}),d.jsx("div",{className:"h-3 flex-1 bg-gray-200 dark:bg-gray-700 rounded"}),d.jsx("div",{className:"h-3 w-16 bg-gray-100 dark:bg-gray-800 rounded"}),d.jsx("div",{className:"h-3 w-14 bg-gray-100 dark:bg-gray-800 rounded"})]})},n))]}),Pagination:e=>d.jsx("div",{className:"flex justify-center gap-1.5 py-3",children:Array.from({length:5},(t,r)=>d.jsx("div",{className:`w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),Tabs:e=>d.jsxs("div",{children:[d.jsx("div",{className:"flex gap-4 border-b border-gray-200 dark:border-gray-700 pb-2 mb-3",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),d.jsx("div",{className:`h-32 bg-gray-100 dark:bg-gray-800 rounded ${e}`})]}),TabNavigation:e=>d.jsx("div",{className:"flex gap-4 border-b border-gray-200 dark:border-gray-700 pb-2",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),ProductCard:e=>d.jsxs("div",{className:"rounded-lg overflow-hidden",children:[d.jsx("div",{className:`h-48 w-full bg-gray-200 dark:bg-gray-700 ${e}`}),d.jsxs("div",{className:"p-3 space-y-2",children:[d.jsx("div",{className:`h-3.5 w-3/4 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:`h-4 w-1/3 bg-gray-300 dark:bg-gray-600 rounded ${e}`})]})]}),UserInfo:e=>d.jsx("div",{className:`h-3 w-14 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),QuantitySelector:e=>d.jsx("div",{className:`h-9 w-28 bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 ${e}`}),Header:e=>d.jsx("header",{className:"bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700",children:d.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs("div",{className:"flex items-center justify-between h-16",children:[d.jsxs("div",{className:"flex items-center gap-8",children:[d.jsx("div",{className:`h-8 w-24 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:"hidden lg:flex items-center gap-6",children:Array.from({length:4},(t,r)=>d.jsx("div",{className:`h-3.5 w-16 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))})]}),d.jsx("div",{className:"flex items-center gap-4",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-8 w-8 bg-gray-200 dark:bg-gray-700 rounded-full ${e}`},r))})]})})}),Footer:e=>d.jsx("footer",{className:"bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-auto",children:d.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:d.jsxs("div",{className:"flex flex-col items-center gap-4",children:[d.jsx("div",{className:`h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:`h-3 w-48 bg-gray-100 dark:bg-gray-800 rounded ${e}`}),d.jsx("div",{className:"flex gap-3",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-6 w-6 bg-gray-200 dark:bg-gray-700 rounded-full ${e}`},r))}),d.jsx("div",{className:`h-3 w-40 bg-gray-100 dark:bg-gray-800 rounded ${e}`})]})})}),Modal:()=>d.jsx(d.Fragment,{}),Toast:()=>d.jsx(d.Fragment,{}),ConfirmDialog:()=>d.jsx(d.Fragment,{})};function d0(e){if(!e||e.length===0)return[];const t=e.filter(n=>n.if),r=e.filter(n=>!n.if);if(r.length>0&&t.length>0){const n=t.filter(o=>!Zi(o.if));return n.length>1&&u0(n)?[...r,Qi(n)]:[...r,...n]}if(t.length>1&&r.length===0){const n=t.filter(o=>!Zi(o.if));return n.length===0?[]:n.length===1?n:[Qi(n)]}return e}function Zi(e){const t=e.replace(/^\{\{|\}\}$/g,"").trim();return t.startsWith("!")||/\b(hasError|isError|isLoading|error|Error)\b/.test(t)||/===\s*0\s*(\)|\}|$)/.test(t)}function u0(e){if(e.length<2)return!1;const t=[];for(const n of e){const o=(n.if||"").replace(/^\{\{|\}\}$/g,"").trim(),a=o.match(/^(.+?)\s*===\s*['"]/);if(a)t.push(a[1].trim());else{const s=o.match(/(.+?)\s*===\s*['"]/);if(!s)return!1;t.push(s[1].trim())}}const r=t[0];return t.every(n=>n===r)}function Qi(e){return e.reduce((t,r)=>Ea(r)>Ea(t)?r:t)}function Ea(e){return e.children?e.children.length+e.children.reduce((t,r)=>t+Ea(r),0):0}function f0(e){return e.includes("{{")?e.replace(/\{\{([^}]+)\}\}/g,(t,r)=>{const n=r.match(/\?[\s]*['"]([^'"]*)['"]\s*:\s*['"]/);return n?n[1]:""}):e}function Ta(e,t){if(!e)return"";let r=e;return r=r.replace(/\b(bg-\S+|text-\S+|border\b|border-\S+|shadow\S*|ring-\S+|outline-\S+|divide-\S+|opacity-\S+|font-\S+|tracking-\S+|leading-\S+|whitespace-\S+|truncate|line-clamp-\S+|underline|no-underline|decoration-\S+|cursor-\S+|hover:\S+|focus:\S+|active:\S+|group-hover:\S+|transition\S*|duration-\S+|ease-\S+|animate-\S+|dark:\S+)\b/g,"").replace(/\s{2,}/g," ").trim(),(t==="Grid"||/\bgrid-cols-\d/.test(r))&&(r=r.replace(/\bgrid-cols-(\d+)/g,(n,o)=>{const a=parseInt(o);return a>4?`grid-cols-${Math.ceil(a/2)}`:`grid-cols-${a}`})),r}function g0(e){const{name:t,props:r}=e;if(!r||!t)return"";const n=[];if(t==="Flex"){n.push("flex");const o=r.direction||"row",a={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};a[o]&&n.push(a[o]);const s=r.justify||"start",l={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};l[s]&&n.push(l[s]);const f=r.align||"stretch",h={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};h[f]&&n.push(h[f]);const c=r.wrap||"nowrap",i={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};i[c]&&n.push(i[c]),r.gap&&r.gap>0&&n.push(`gap-${r.gap}`)}if(t==="Grid"){n.push("grid");const o=r.cols||1;if(n.push(`grid-cols-${o}`),r.responsive){const a=r.responsive;a.sm&&n.push(`sm:grid-cols-${a.sm}`),a.md&&n.push(`md:grid-cols-${a.md}`),a.lg&&n.push(`lg:grid-cols-${a.lg}`),a.xl&&n.push(`xl:grid-cols-${a.xl}`),a["2xl"]&&n.push(`2xl:grid-cols-${a["2xl"]}`)}r.gap!==void 0&&r.gap>0&&n.push(`gap-${r.gap}`),r.rowGap!==void 0&&r.rowGap>0&&n.push(`gap-y-${r.rowGap}`),r.colGap!==void 0&&r.colGap>0&&n.push(`gap-x-${r.colGap}`)}return n.join(" ")}function h0(e){switch(e){case"pulse":return"animate-pulse";case"wave":return"animate-pulse";case"none":return"";default:return"animate-pulse"}}function qn(e,t,r,n){return!e||e.length===0?[]:d0(e).map((a,s)=>Ji(a,t,r,s,n))}function p0(e,t){const r=e.props?.className||e.className||"";return e.responsive?t>=1024&&e.responsive.desktop?.props?.className?e.responsive.desktop.props.className:t>=768&&e.responsive.tablet?.props?.className?e.responsive.tablet.props.className:r:r}function m0(e,t){const r=e.split(/\s+/).filter(Boolean),n=[],o=[];for(const f of r){const h=f.match(/^(sm|md|lg|xl|2xl):(.+)$/);if(h){const[,c,i]=h;if(Xi.has(i)){n.push({breakpoint:Yi[c]||0,display:i});continue}(Yi[c]||0)<=t&&o.push(i);continue}if(Xi.has(f)){n.push({breakpoint:0,display:f});continue}o.push(f)}if(n.length===0)return{visible:!0,className:e};n.sort((f,h)=>f.breakpoint-h.breakpoint);let a="";for(const f of n)f.breakpoint<=t&&(a=f.display);a||(a=n[0].display);const s=a!=="hidden",l=s?[a,...o].join(" "):"";return{visible:s,className:l}}function b0(e){const t=e.match(/\bgrid-cols-\[([^\]]+)\]/);if(!t)return{className:e};const r=t[1].replace(/_/g," ");return{className:e.replace(/\bgrid-cols-\[([^\]]+)\]/,"").replace(/\s{2,}/g," ").trim(),style:{gridTemplateColumns:r}}}function Ji(e,t,r,n,o){const{name:a,children:s,iteration:l}=e,f=p0(e,o),h=g0(e),c=h?`${h} ${f}`.trim():f,i=f0(c),u=m0(i,o);if(!a||!u.visible)return null;const g=u.className;if(l)return d.jsx(k.Fragment,{children:Array.from({length:r},(m,w)=>Ji({...e,iteration:void 0},t,r,w,o))},n);if(qi[a])return d.jsx("div",{className:g,children:qi[a](t,r)},n);if(s0.has(a)){const m=Ta(g,a);if(!s||s.length===0){const E=/\b(h-\S|w-\S)/.test(m)?`${m} bg-gray-200 dark:bg-gray-700 rounded ${t}`:`${m} h-3.5 w-full bg-gray-200 dark:bg-gray-700 rounded ${t}`.trim();return d.jsx("div",{className:E},n)}const w=qn(s,t,r,o);if(!(w.length>0&&w.some(v=>v!==null)))return null;const O=b0(m);return d.jsx("div",{className:O.className,style:O.style,children:w},n)}const p=Ta(g,a);if(l0.has(a)){const m={H1:"h-7",H2:"h-6",H3:"h-5",H4:"h-5",H5:"h-4",H6:"h-4",P:"h-3.5",Span:"h-3",Label:"h-3",A:"h-3.5",Badge:"h-5 w-16"},w={H1:"w-3/5",H2:"w-2/5",H3:"w-1/3",H4:"w-1/3",H5:"w-1/4",H6:"w-1/4",P:"w-full",Span:"w-1/4",Label:"w-1/4",A:"w-1/3",Strong:"w-1/5",Em:"w-1/4",Small:"w-1/5",Badge:"w-16"},C=m[a]||"h-3.5",O=w[a]||"w-full",v=a.startsWith("H")?"mb-2 mt-1":"mb-1.5";return d.jsx("div",{className:`${C} ${O} bg-gray-200 dark:bg-gray-700 rounded ${t} ${v} ${p}`.trim()},n)}if(i0.has(a))return a==="Textarea"?d.jsx("div",{className:`h-20 w-full bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 mb-2 ${t}`},n):a==="Checkbox"||a==="Radio"||a==="Toggle"?d.jsx("div",{className:`h-5 w-5 bg-gray-200 dark:bg-gray-700 rounded mb-1.5 ${t}`},n):d.jsx("div",{className:`h-9 w-full bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 mb-2 ${t}`},n);if(c0.has(a))return a==="Avatar"||a==="AvatarUploader"?d.jsx("div",{className:`h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full ${t}`},n):a==="ProductImageViewer"?d.jsx("div",{className:`h-80 w-full bg-gray-200 dark:bg-gray-700 rounded ${t}`},n):d.jsx("div",{className:`h-40 w-full bg-gray-200 dark:bg-gray-700 rounded mb-2 ${t}`},n);if(a==="Button"){if(s&&s.length>0){const m=Ta(g,a),w=qn(s,t,r,o);if(!(w.length>0&&w.some(E=>E!==null)))return null;const v=/\b(border|rounded|px-\d|py-\d)/.test(f)?"border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-lg":"";return d.jsx("div",{className:`${m} ${v}`.trim(),children:w},n)}return d.jsx("div",{className:`h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded ${t}`},n)}return a==="Icon"?d.jsx("div",{className:`h-5 w-5 bg-gray-200 dark:bg-gray-700 rounded ${t}`},n):s&&s.length>0?d.jsx("div",{className:p,children:qn(s,t,r,o)},n):d.jsx("div",{className:`h-3.5 w-full bg-gray-200 dark:bg-gray-700 rounded mb-1.5 ${t}`},n)}const Na=({components:e,options:t})=>{const r=h0(t.animation),o=window.G7Core?.useResponsive,a=o?.(),s=a?a.width:typeof window<"u"?window.innerWidth:1024;return d.jsx("div",{className:"w-full",role:"status","aria-busy":"true","aria-label":"Loading...",children:qn(e,r,t.iteration_count,s)})},y0=Object.freeze(Object.defineProperty({__proto__:null,PageSkeleton:Na,default:Na},Symbol.toStringTag,{value:"Module"})),v0=(e,t)=>window.G7Core?.t?.(e,t)??e,Ri=({options:e})=>{const t=document.documentElement.classList.contains("dark"),r=t?"rgb(17,24,39)":"rgb(249,250,251)",n=t?"#6b7280":"#9ca3af",o=t?"#9ca3af":"#6b7280";return d.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:r,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[d.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${n}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),d.jsx("span",{style:{color:o,fontSize:"14px"},children:e?.text||v0("nav.loading")})]})},w0=Object.freeze(Object.defineProperty({__proto__:null,default:Ri},Symbol.toStringTag,{value:"Module"})),Dr=(e,t)=>window.G7Core?.t?.(e,t)??e,ec=({currentPage:e,totalPages:t,onPageChange:r,maxVisiblePages:n=5,showFirstLast:o=!0,showFirst:a,showLast:s,hasMorePages:l,className:f="",style:h,prevText:c,nextText:i,id:u,editorAttrs:g})=>{const p=t!=null,m=p?t:0,w=p?e<m:l??!1,C=(a??o)&&p,O=(s??o)&&p,v=k.useMemo(()=>{const M=[];if(!p)return M;if(m<=n+2)for(let x=1;x<=m;x++)M.push(x);else{const x=Math.floor(n/2);let y=Math.max(2,e-x),T=Math.min(m-1,e+x);e<=x+1?T=n:e>=m-x&&(y=m-n+1),M.push(1),y>2&&M.push("...");for(let A=y;A<=T;A++)M.push(A);T<m-1&&M.push("..."),m>1&&M.push(m)}return M},[e,m,p,n]),E=M=>{M<1||M===e||p&&M>m||r(M)},I=()=>{e>1&&r(e-1)},_=()=>{w&&r(e+1)};return d.jsxs(W,{className:`flex items-center gap-2 ${f}`,style:h,role:"navigation","aria-label":Dr("common.pagination"),id:u,...g,children:[C&&d.jsx(Q,{onClick:()=>E(1),disabled:e===1,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.first_page"),children:"«"}),d.jsx(Q,{onClick:I,disabled:e===1,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.prev_page"),children:c||d.jsx(d.Fragment,{children:"‹"})}),d.jsxs(W,{className:"flex items-center gap-1",children:[!p&&d.jsx(R,{className:"px-3 py-1 text-sm text-gray-700 dark:text-gray-300",children:e}),v.map((M,x)=>{if(M==="...")return d.jsx(R,{className:"px-3 py-1 text-sm text-gray-500 dark:text-gray-400",children:"..."},`ellipsis-${x}`);const y=M,T=y===e;return d.jsx(Q,{onClick:()=>E(y),className:`px-3 py-1 text-sm border rounded ${T?"bg-blue-500 dark:bg-blue-600 text-white border-blue-500 dark:border-blue-600":"border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800"}`,"aria-label":Dr("common.page_n",{n:y}),"aria-current":T?"page":void 0,children:y},y)})]}),d.jsx(Q,{onClick:_,disabled:!w,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.next_page"),children:i||d.jsx(d.Fragment,{children:"›"})}),O&&d.jsx(Q,{onClick:()=>E(m),disabled:e===m,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.last_page"),children:"»"})]})},x0=Object.freeze(Object.defineProperty({__proto__:null,Pagination:ec},Symbol.toStringTag,{value:"Module"})),tc=(e,t)=>window.G7Core?.t?.(e,t)??e,rc=({name:e="search",placeholder:t,value:r,onChange:n,onSubmit:o,showButton:a=!1,suggestions:s=[],onSuggestionClick:l,showSuggestions:f=!1,className:h="",style:c,id:i,editorAttrs:u})=>{const g=t??tc("common.search_placeholder"),[p,m]=k.useState(""),[w,C]=k.useState(!1),O=k.useRef(null),v=k.useRef(""),E=r!==void 0?r:p,I=f&&w&&s.length>0&&E.length>0;k.useEffect(()=>{r!==void 0&&(v.current=r)},[r]);const _=y=>{const T=y.target.value;m(T),n?.(y),v.current!==""&&T===""&&!y.nativeEvent.inputType&&setTimeout(()=>{const A=O.current?.querySelector("form");A&&A.requestSubmit()},0),v.current=T},M=y=>{m(y.text);const T={target:{value:y.text,name:e}};n?.(T),l?.(y),C(!1)},x=y=>{y.preventDefault(),o?.(y)};return d.jsxs(W,{ref:O,className:`relative ${h}`,style:c,id:i,...u,children:[d.jsx(so,{onSubmit:x,className:"relative",children:d.jsxs(W,{className:`relative flex items-center ${a?"gap-2":""}`,children:[d.jsxs(W,{className:"relative flex-1",children:[d.jsx(W,{className:"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",children:d.jsx(de,{name:Je.Search,className:"w-5 h-5 text-gray-400 dark:text-gray-500"})}),d.jsx(xt,{type:"search",name:e,value:E,onChange:_,onFocus:()=>C(!0),onBlur:()=>setTimeout(()=>C(!1),200),placeholder:g,className:"w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"})]}),a&&d.jsx(Q,{type:"submit",className:"px-4 py-2 bg-gray-800 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-500 transition-colors text-sm font-medium cursor-pointer h-[42px]",children:tc("common.search")})]})}),I&&d.jsx(W,{className:"absolute z-10 w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-64 overflow-y-auto",children:s.map(y=>d.jsx(W,{className:"px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 transition-colors",onClick:()=>M(y),children:d.jsxs(W,{className:"flex items-center gap-2",children:[d.jsx(de,{name:Je.Search,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),d.jsx(R,{className:"text-sm text-gray-700 dark:text-gray-300",children:y.text})]})},y.id))})]})},k0=Object.freeze(Object.defineProperty({__proto__:null,SearchBar:rc},Symbol.toStringTag,{value:"Module"})),vt=window.G7Core,C0={sm:"md",md:"lg",lg:"xl",xl:"2xl","2xl":"3xl"};function Aa({src:e,fallbackText:t="U",size:r="lg",uploadEndpoint:n="/api/me/avatar",deleteEndpoint:o="/api/me/avatar",onUploadSuccess:a,onUploadError:s,onDeleteSuccess:l,onDeleteError:f,showDeleteButton:h=!0,accept:c="image/*",maxSize:i=5,className:u="",uploadButtonText:g,deleteButtonText:p,readOnly:m=!1,confirmDelete:w=!0,deleteConfirmMessage:C,confirmUpload:O=!0,uploadConfirmMessage:v,uploadSuccessActions:E,uploadErrorActions:I,deleteSuccessActions:_,deleteErrorActions:M,id:x,editorAttrs:y}){const T=k.useRef(null),[A,P]=k.useState(!1),[D,L]=k.useState(!1),[U,S]=k.useState(void 0),[b,F]=k.useState(null),[$,G]=k.useState(!1),[Y,B]=k.useState(!1),[K,j]=k.useState(null),[H,q]=k.useState(null),ue=k.useRef(`avatar-upload-${Math.random().toString(36).slice(2,11)}`).current;k.useEffect(()=>{if(K){const oe=URL.createObjectURL(K);return q(oe),()=>URL.revokeObjectURL(oe)}else q(null)},[K]);const te=k.useCallback(oe=>vt?.t?.(oe)??oe,[]),fe=k.useCallback(()=>{const oe=document.cookie.match(/XSRF-TOKEN=([^;]+)/);return oe?decodeURIComponent(oe[1]):null},[]),ee=k.useCallback(()=>vt?.api?.getToken?.()??null,[]),ke=k.useCallback(async oe=>{B(!1),j(null),P(!0),F(null);try{const ae=new FormData;ae.append("avatar",oe);const be=fe(),xe=ee(),He=await fetch(n,{method:"POST",headers:{Accept:"application/json",...be&&{"X-XSRF-TOKEN":be},...xe&&{Authorization:`Bearer ${xe}`}},credentials:"include",body:ae});if(!He.ok){const Oe=await He.json().catch(()=>({}));throw new Error(Oe.message||te("attachment.upload_failed"))}const Pe=await He.json(),Ve=Pe.data||Pe;Ve.avatar&&S(Ve.avatar),a?.(Ve),vt?.toast?.success?.(te("mypage.profile.avatar_uploaded")),E?.length?E.forEach(Oe=>vt?.dispatch?.(Oe)):vt?.refetchDataSource?.("user")}catch(ae){const be=ae instanceof Error?ae:new Error(String(ae));F(be.message),s?.(be),vt?.toast?.error?.(be.message),I?.length&&I.forEach(xe=>vt?.dispatch?.(xe))}finally{P(!1),T.current&&(T.current.value="")}},[n,a,s,fe,ee,te]),me=k.useCallback(async oe=>{const ae=oe.target.files?.[0];if(!ae)return;F(null);const be=i*1024*1024;if(ae.size>be){const xe=new Error(te("attachment.file_too_large").replace(":max",`${i}MB`));F(xe.message),s?.(xe),vt?.toast?.error?.(xe.message);return}if(c&&!ae.type.match(c.replace("*",".*"))){const xe=new Error(te("attachment.invalid_file_type"));F(xe.message),s?.(xe),vt?.toast?.error?.(xe.message);return}O?(j(ae),B(!0)):ke(ae)},[i,c,s,O,ke,te]),Ge=k.useCallback(async()=>{G(!1),L(!0),F(null);try{const oe=fe(),ae=ee(),be=await fetch(o,{method:"DELETE",headers:{Accept:"application/json","Content-Type":"application/json",...oe&&{"X-XSRF-TOKEN":oe},...ae&&{Authorization:`Bearer ${ae}`}},credentials:"include"});if(!be.ok){const xe=await be.json().catch(()=>({}));throw new Error(xe.message||te("attachment.delete_failed"))}S(null),l?.(),vt?.toast?.success?.(te("mypage.profile.avatar_deleted")),_?.length?_.forEach(xe=>vt?.dispatch?.(xe)):vt?.refetchDataSource?.("user")}catch(oe){const ae=oe instanceof Error?oe:new Error(String(oe));F(ae.message),f?.(ae),vt?.toast?.error?.(ae.message),M?.length&&M.forEach(be=>vt?.dispatch?.(be))}finally{L(!1)}},[o,l,f,fe,ee,te]),De=k.useCallback(()=>{w?G(!0):Ge()},[w,Ge]),ye=k.useCallback(()=>{G(!1)},[]),Me=k.useCallback(()=>{B(!1),j(null),T.current&&(T.current.value="")},[]),Ce=k.useCallback(()=>{K&&ke(K)},[K,ke]),we=U===void 0?e:U,Fe=!!we,Le=C0[r]||"lg",V=$||Y,re=()=>d.jsxs(W,{className:"flex items-center justify-center gap-2",children:[d.jsx(xt,{ref:T,id:ue,type:"file",accept:c,className:"hidden",onChange:me,disabled:A}),!Fe&&d.jsxs(yr,{htmlFor:ue,className:`inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${A?"opacity-50 cursor-not-allowed":""}`,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"image",size:"sm"}),d.jsx(R,{children:g||te("mypage.profile.register_avatar")})]}),Fe&&d.jsxs(d.Fragment,{children:[d.jsxs(yr,{htmlFor:ue,className:`inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${A?"opacity-50 cursor-not-allowed":""}`,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"upload",size:"sm"}),d.jsx(R,{children:g||te("common.change")})]}),h&&d.jsxs(Q,{type:"button",className:"inline-flex items-center gap-2 px-4 py-2 border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 rounded-lg text-sm font-medium hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",onClick:De,disabled:D,children:[D?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"trash-2",size:"sm"}),d.jsx(R,{children:p||te("common.delete")})]})]})]});return d.jsxs(W,{className:`flex flex-col items-center gap-3 ${u}`,id:x,...y,children:[Y&&H?d.jsxs(W,{className:"relative",children:[d.jsx(Lt,{src:H,alt:"Preview",className:`rounded-full object-cover ${r==="sm"?"w-10 h-10":r==="md"?"w-12 h-12":r==="lg"?"w-16 h-16":r==="xl"?"w-24 h-24":"w-32 h-32"}`}),d.jsx(W,{className:"absolute inset-0 rounded-full bg-blue-500/20 border-2 border-blue-500 border-dashed"})]}):d.jsx(Ur,{avatar:we||void 0,name:t,size:Le}),!m&&d.jsxs(W,{className:"w-[220px] flex flex-col items-center gap-2",children:[Y&&K&&d.jsxs(W,{className:"w-full p-3 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-lg",children:[d.jsxs(W,{className:"flex items-center gap-2 mb-2",children:[d.jsx(de,{name:"upload",size:"sm",className:"text-blue-600 dark:text-blue-400 shrink-0"}),d.jsx(R,{className:"text-sm font-medium text-blue-800 dark:text-blue-300",children:v||te("mypage.profile.upload_avatar_confirm")})]}),d.jsxs(W,{className:"text-xs text-blue-600 dark:text-blue-400 truncate mb-3",title:K.name,children:[K.name," (",(K.size/1024/1024).toFixed(2),"MB)"]}),d.jsxs(W,{className:"grid grid-cols-2 gap-2",children:[d.jsxs(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50",onClick:Ce,disabled:A,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"check",size:"sm"}),d.jsx(R,{children:te("common.confirm")})]}),d.jsx(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:Me,children:d.jsx(R,{children:te("common.cancel")})})]})]}),$&&d.jsxs(W,{className:"w-full p-3 bg-red-50 dark:bg-red-950/50 border border-red-200 dark:border-red-800 rounded-lg",children:[d.jsxs(W,{className:"flex items-center gap-2 mb-2",children:[d.jsx(de,{name:"alert-triangle",size:"sm",className:"text-red-600 dark:text-red-400 shrink-0"}),d.jsx(R,{className:"text-sm font-medium text-red-800 dark:text-red-300",children:C||te("mypage.profile.delete_avatar_confirm")})]}),d.jsxs(W,{className:"grid grid-cols-2 gap-2",children:[d.jsxs(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50",onClick:Ge,disabled:D,children:[D?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"trash-2",size:"sm"}),d.jsx(R,{children:te("common.delete")})]}),d.jsx(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:ye,children:d.jsx(R,{children:te("common.cancel")})})]})]}),!V&&re()]}),b&&!V&&d.jsxs(W,{className:"flex items-center gap-2 text-sm text-red-600 dark:text-red-400",children:[d.jsx(de,{name:"alert-circle",size:"sm"}),d.jsx(R,{children:b})]})]})}const S0=Object.freeze(Object.defineProperty({__proto__:null,AvatarUploader:Aa,default:Aa},Symbol.toStringTag,{value:"Module"})),nc=()=>window.G7Core,Zn=(e,t)=>window.G7Core?.t?.(e,t)??e,oc={horizontal:"flex items-center gap-1.5",vertical:"flex flex-col items-start"},_0=()=>[{key:"view_profile",label:Zn("userinfo.view_profile"),icon:"user",path:"/users/{{userId}}"},{key:"view_posts",label:Zn("userinfo.view_posts"),icon:"file-lines",path:"/users/{{userId}}/posts"}],Ma=({text:e,title:t})=>e?d.jsx(R,{className:"text-sm text-gray-500 dark:text-gray-400",title:t,children:e}):null,ac=({author:e,name:t,userId:r,subText:n,subTextTitle:o,isGuest:a=!1,isWithdrawn:s=!1,showDropdown:l=!0,clickable:f=!0,profilePath:h="/users/{userId}",className:c="",layout:i="vertical",text:u,stopPropagation:g=!1,menuItems:p,hideMenuItems:m=[],appendMenuItems:w=[],id:C,editorAttrs:O})=>{const v=r??e?.uuid,E=a||e?.is_guest||!1,I=s||e?.status==="withdrawn",_=I?Zn("userinfo.withdrawn_user"):u??t??e?.name??"",[M,x]=k.useState(!1),[y,T]=k.useState({top:0,left:0}),A=k.useRef(null),P=k.useRef(null),D=k.useRef(null),L=h.replace("{userId}",String(v??"")),U=`${oc[i]} ${c}`;k.useEffect(()=>{if(!M)return;const j=q=>{const ue=q.target,te=A.current?.contains(ue),fe=P.current?.contains(ue);!te&&!fe&&x(!1)},H=()=>x(!1);return document.addEventListener("mousedown",j),window.addEventListener("scroll",H,!0),window.addEventListener("resize",H),()=>{document.removeEventListener("mousedown",j),window.removeEventListener("scroll",H,!0),window.removeEventListener("resize",H)}},[M]);const S=k.useCallback(()=>{if(p&&p.length>0)return p.filter(q=>q.show!==!1);let H=_0().filter(q=>!m.includes(q.key)&&q.show!==!1);return w&&w.length>0&&(H=[...H,...w.filter(q=>q.show!==!1)]),H},[p,m,w]),b=k.useCallback(()=>{if(D.current){const j=D.current.getBoundingClientRect();T({top:j.bottom+4,left:j.left})}},[]),F=k.useCallback((j,H)=>{if(H.stopPropagation(),H.preventDefault(),x(!1),j.onClick)j.onClick();else if(j.path){const q=j.path.replace(/\{\{userId\}\}/g,String(v??"")),ue=nc();ue?.navigate?ue.navigate(q):window.location.href=q}},[v]),$=k.useCallback(j=>{j.preventDefault(),g&&j.stopPropagation(),!(E||!v)&&(l?(M||b(),x(H=>!H)):f&&nc()?.navigate?.(L))},[g,E,v,l,M,b,f,L]);if(E||!v)return d.jsxs(W,{className:U,id:C,...O,children:[d.jsxs(W,{className:"flex items-center gap-1.5 text-gray-400 dark:text-gray-500",children:[d.jsx(R,{children:_}),d.jsx(R,{className:"text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded",children:Zn("userinfo.guest_badge")})]}),d.jsx(Ma,{text:n,title:o})]});if(I)return d.jsxs(W,{className:U,id:C,...O,children:[d.jsx(R,{className:"text-gray-400 dark:text-gray-500 line-through",children:_}),d.jsx(Ma,{text:n,title:o})]});const G=S(),B=/\btext-\S+/.test(c)?"":"text-gray-900 dark:text-white",K=l||f?`${B} hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer font-medium`.trim():`${B} font-medium`.trim();return d.jsxs(W,{ref:A,className:`relative inline-block ${c}`,id:C,...O,children:[d.jsxs(W,{className:oc[i],children:[d.jsx(Q,{ref:D,onClick:$,className:K,children:_}),d.jsx(Ma,{text:n,title:o})]}),l&&M&&G.length>0&&Jt.createPortal(d.jsx(W,{ref:P,className:"fixed w-40 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-[9999]",style:{top:y.top,left:y.left},"data-testid":"author-dropdown-menu",children:G.map(j=>d.jsxs(Q,{onClick:H=>F(j,H),className:"flex items-center justify-start gap-2 w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer","data-testid":`menu-item-${j.key}`,children:[j.icon&&d.jsx(de,{name:j.icon,className:"w-4 h-4"}),d.jsx(R,{children:j.label})]},j.key))}),document.body)]})},E0=Object.freeze(Object.defineProperty({__proto__:null,UserInfo:ac},Symbol.toStringTag,{value:"Module"})),sc=({tabs:e,activeTabId:t,onTabChange:r,variant:n="underline",className:o="",style:a,hiddenTabIds:s=[],mobileBreakpoint:l=768,id:f,editorAttrs:h})=>{const i=window.G7Core?.useResponsive,u=i?.(),g=u?u.width<l:typeof window<"u"&&window.innerWidth<l,p=v=>{!v.disabled&&v.id!==t&&r?.(v.id)},m=v=>{const E=v.target.value,I=Number(E),_=typeof E=="string"&&E!==""&&!Number.isNaN(I)?I:E,M=e.find(x=>String(x.id)===String(_));M&&p(M)},w=s.length>0?e.filter(v=>!s.includes(v.id)):e;if(g)return d.jsx(W,{className:`relative ${o}`,style:a,id:f,...h,children:d.jsx(Ya,{value:t!==void 0?String(t):"",onChange:m,options:w.map(v=>({value:String(v.id),label:v.badge!==void 0?`${v.label} (${v.badge})`:v.label,disabled:v.disabled})),className:"w-full"})});const C=v=>{const E=v.id===t,I="flex items-center gap-2 px-3 py-2 font-medium text-sm transition-all shrink-0 whitespace-nowrap";if(v.disabled)return`${I} opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600`;switch(n){case"pills":return E?`${I} bg-blue-600 dark:bg-blue-500 text-white rounded-lg`:`${I} text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg`;case"underline":return E?`${I} text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white`:`${I} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600`;default:return E?`${I} text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border-b-2 border-blue-600 dark:border-blue-400`:`${I} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 border-b-2 border-transparent`}},O=n==="underline"?"flex gap-0 border-b border-gray-200 dark:border-gray-700":"flex gap-2";return d.jsx(W,{className:`relative ${o}`,style:a,id:f,...h,children:d.jsx(Fr,{className:O,children:w.map(v=>d.jsxs(Q,{type:"button",onClick:()=>p(v),disabled:v.disabled,className:C(v),children:[v.iconName&&d.jsx(de,{name:v.iconName,size:"sm"}),d.jsx(R,{children:v.label}),v.badge!==void 0&&d.jsx(W,{className:"flex items-center justify-center min-w-5 h-5 px-1.5 bg-red-500 text-white text-xs font-bold rounded-full",children:d.jsx(R,{children:v.badge})})]},v.id))})})},T0=Object.freeze(Object.defineProperty({__proto__:null,TabNavigation:sc},Symbol.toStringTag,{value:"Module"})),N0={Header:()=>Promise.resolve().then(()=>ad),Footer:()=>Promise.resolve().then(()=>cd),MobileNav:()=>Promise.resolve().then(()=>dd),SlotContainer:()=>Promise.resolve().then(()=>nd),NotificationCenter:()=>Promise.resolve().then(()=>od),ProductCard:()=>Promise.resolve().then(()=>Pd),BrandMark:()=>Promise.resolve().then(()=>Fd),ImageGallery:()=>Promise.resolve().then(()=>Nl),ProductImageViewer:()=>Promise.resolve().then(()=>Bf),QuantitySelector:()=>Promise.resolve().then(()=>Wf),PostReactions:()=>Promise.resolve().then(()=>Yf),RichTextEditor:()=>Promise.resolve().then(()=>Xf),HtmlContent:()=>Promise.resolve().then(()=>qf),HtmlEditor:()=>Promise.resolve().then(()=>Zf),ExpandableContent:()=>Promise.resolve().then(()=>Qf),FileUploader:()=>Promise.resolve().then(()=>qh),ConfirmDialog:()=>Promise.resolve().then(()=>eg),SocialLoginButtons:()=>Promise.resolve().then(()=>Qh),Toast:()=>Promise.resolve().then(()=>r0),PageTransitionIndicator:()=>Promise.resolve().then(()=>o0),PageTransitionBlur:()=>Promise.resolve().then(()=>a0),PageSkeleton:()=>Promise.resolve().then(()=>y0),PageLoading:()=>Promise.resolve().then(()=>w0),ThemeToggle:()=>Promise.resolve().then(()=>Jc),Pagination:()=>Promise.resolve().then(()=>x0),SearchBar:()=>Promise.resolve().then(()=>k0),Avatar:()=>Promise.resolve().then(()=>td),AvatarUploader:()=>Promise.resolve().then(()=>S0),UserInfo:()=>Promise.resolve().then(()=>E0),Modal:()=>Promise.resolve().then(()=>Rf),TabNavigation:()=>Promise.resolve().then(()=>T0)},A0=({id:e,className:t,style:r,children:n,editorAttrs:o})=>d.jsx(W,{id:e,className:t,style:r,...o,children:n}),M0=({id:e,children:t,cols:r=1,responsive:n,gap:o,rowGap:a,colGap:s,autoRows:l,autoCols:f,flow:h="row",className:c="",style:i,onClick:u,editorAttrs:g})=>{const p=["grid"];if(r&&p.push(`grid-cols-${r}`),n&&(n.sm&&p.push(`sm:grid-cols-${n.sm}`),n.md&&p.push(`md:grid-cols-${n.md}`),n.lg&&p.push(`lg:grid-cols-${n.lg}`),n.xl&&p.push(`xl:grid-cols-${n.xl}`),n["2xl"]&&p.push(`2xl:grid-cols-${n["2xl"]}`)),o!==void 0&&o>0&&p.push(`gap-${o}`),a!==void 0&&a>0&&p.push(`gap-y-${a}`),s!==void 0&&s>0&&p.push(`gap-x-${s}`),l){const w={auto:"auto-rows-auto",min:"auto-rows-min",max:"auto-rows-max",fr:"auto-rows-fr"};p.push(w[l])}if(f){const w={auto:"auto-cols-auto",min:"auto-cols-min",max:"auto-cols-max",fr:"auto-cols-fr"};p.push(w[f])}const m={row:"grid-flow-row",col:"grid-flow-col",dense:"grid-flow-dense","row-dense":"grid-flow-row-dense","col-dense":"grid-flow-col-dense"};return p.push(m[h]),c&&p.push(c),d.jsx(W,{id:e,className:p.join(" "),style:i,onClick:u,...g,children:t})},O0=({id:e,children:t,direction:r="row",justify:n="start",align:o="stretch",wrap:a="nowrap",gap:s=0,grow:l=!1,shrink:f=!0,className:h="",style:c,onClick:i,editorAttrs:u})=>{const g=["flex"],p={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};g.push(p[r]);const m={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};g.push(m[a]);const w={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};g.push(w[n]);const C={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};return g.push(C[o]),s>0&&g.push(`gap-${s}`),l===!0?g.push("flex-grow"):typeof l=="number"&&l>0&&g.push(`flex-grow-${l}`),f===!1?g.push("shrink-0"):f===!0?g.push("flex-shrink"):typeof f=="number"&&f>0&&g.push(`flex-shrink-${f}`),h&&g.push(h),d.jsx(W,{id:e,className:g.join(" "),style:c,onClick:i,...u,children:t})},I0=({id:e,title:t,subtitle:r,children:n,padding:o="md",background:a="none",maxWidth:s,centered:l=!1,border:f=!1,shadow:h="none",rounded:c=!1,className:i="",style:u,onClick:g,editorAttrs:p})=>{const m=[],w={none:"p-0",sm:"p-2",md:"p-4",lg:"p-6",xl:"p-8"};m.push(w[o]);const C={none:"",white:"bg-white dark:bg-gray-700",gray:"bg-gray-50 dark:bg-gray-800",primary:"bg-blue-50 dark:bg-blue-900/20",secondary:"bg-gray-50 dark:bg-gray-800"};if(a!=="none"&&m.push(C[a]),s){const v={sm:"max-w-sm",md:"max-w-md",lg:"max-w-lg",xl:"max-w-xl","2xl":"max-w-2xl","4xl":"max-w-4xl","6xl":"max-w-6xl","7xl":"max-w-7xl",full:"max-w-full"};m.push(v[s])}l&&m.push("mx-auto"),f&&m.push("border","border-gray-200","dark:border-gray-700");const O={none:"",sm:"shadow-sm",md:"shadow-md",lg:"shadow-lg",xl:"shadow-xl"};return h!=="none"&&m.push(O[h]),c&&m.push("rounded-lg"),i&&m.push(i),d.jsxs(qa,{id:e,className:m.join(" "),style:u,onClick:g,...p,children:[(t||r)&&d.jsxs(W,{className:"mb-4",children:[t&&d.jsx(Va,{className:"text-2xl font-bold text-gray-900 dark:text-white",children:t}),r&&d.jsx(Dt,{className:"mt-1 text-sm text-gray-600 dark:text-gray-400",children:r})]}),n]})},D0=({id:e,leftWidth:t="250px",rightWidth:r="300px",leftSlot:n,centerSlot:o,rightSlot:a,className:s="",style:l,editorAttrs:f})=>{const h=`flex flex-row w-full h-full ${s}`.trim(),c={width:t,flexShrink:0},i={flex:1,minWidth:0},u={width:r,flexShrink:0};return d.jsxs(W,{id:e,className:h,style:l,...f,children:[d.jsx(W,{className:"flex flex-col",style:c,children:n}),d.jsx(W,{className:"flex flex-col",style:i,children:o}),d.jsx(W,{className:"flex flex-col",style:u,children:a})]})},L0={identifier:"sirsoft-basic",vendor:"sirsoft",name:{ko:"Basic",en:"Basic"},version:"1.1.0",license:"MIT",description:{ko:"그누보드7 기본 사용자 템플릿",en:"Gnuboard7 default user template"},type:"user",locales:["ko","en"],author:{name:"sirsoft",email:"contact@sirsoft.com",url:"https://sirsoft.com"},release_date:"2026-01-07",g7_version:">=7.0.6",dependencies:{modules:{"sirsoft-board":">=1.0.0","sirsoft-ecommerce":">=1.1.0","sirsoft-page":">=1.0.0"},plugins:{"sirsoft-daum_postcode":">=1.0.0"}},assets:{css:["dist/css/components.css"],js:["dist/js/components.iife.js"],fonts:[],images:[]},components:{basic:["a","button","checkbox","div","form","h1","h2","h3","h4","hr","icon","img","input","label","li","nav","p","section","select","span","svg","table","tbody","td","textarea","th","thead","tr","ul"],composite:["header","footer","mobile-nav","product-card","rich-text-editor","image-gallery","quantity-selector","post-reactions","social-login-buttons","file-uploader","card","modal","toast","pagination","stat-card","status-badge","tab-navigation","empty-state","loading-spinner","breadcrumb","search-bar","dropdown"],layout:["container","flex","grid","section"]},preview:{thumbnail:"preview/thumbnail.png",screenshots:["preview/screenshot1.png","preview/screenshot2.png"]},error_config:{layouts:{401:"errors/401",403:"errors/403",404:"errors/404",500:"errors/500",503:"errors/503",maintenance:"errors/maintenance"}},features:{dark_mode:!0,responsive:!0,multi_language:!0,multi_currency:!0},github_url:"https://github.com/gnuboard/g7-template-sirsoft-basic",github_changelog_url:"https://github.com/gnuboard/g7-template-sirsoft-basic/blob/main/CHANGELOG.md",externals:[{id:"fontawesome",type:"style",url:"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",preconnect:"https://cdnjs.cloudflare.com"},{id:"pretendard",type:"webfont",url:"https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css",preconnect:"https://cdn.jsdelivr.net",crossorigin:"anonymous"}]},P0={KRW:0,JPY:0};function $0(){try{const t=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(t?.currencies)?t.currencies:[]}catch{return[]}}function Lr(e,t){if(!Number.isFinite(e))return"0";const r=$0(),n=t||r.find(a=>a.is_default)?.code;if(!n)return e.toLocaleString();const o=r.find(a=>a.code===n)?.decimal_places??P0[n]??2;try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n,minimumFractionDigits:o,maximumFractionDigits:o}).format(e)}catch{return`${e.toLocaleString(void 0,{minimumFractionDigits:o,maximumFractionDigits:o})} ${n}`}}function Qn(e,t,r,n=0){if(!e)return;const o=f=>f?.value??f?.price??0;let a;for(const[f,h]of Object.entries(e))if(h?.is_default){a=f;break}a=a??(e.KRW?"KRW":void 0);const s=a?o(e[a]):0,l={};for(const[f,h]of Object.entries(e)){const c=o(h);let i;r?i=r[f]?.value??0:n>0&&f===a?i=n:n>0&&s>0?i=n*c/s:i=0;const u=(c+i)*t;l[f]={value:u,formatted:Lr(u,f)}}return l}function Jn(e){const t={};for(const n of e){const o=n?.multiCurrencyTotalPrice;if(o)for(const[a,s]of Object.entries(o)){const l=s?.value??s?.price??0;t[a]=(t[a]??0)+l}}const r={};for(const[n,o]of Object.entries(t))r[n]={value:o,formatted:Lr(o,n)};return r}function lc(e,t){if(!e||!t?.length)return 0;let r=0;for(const n of t){const o=e[n.id];if(o==null)continue;const a=n.values?.find(s=>s.id===Number(o));a&&(r+=a.price_adjustment??0)}return r}function ic(e,t){if(!e||!t?.length)return;const r={};let n=!1;for(const a of t){const s=e[a.id];if(s==null)continue;const f=a.values?.find(h=>h.id===Number(s))?.multi_currency_price_adjustment;if(f){n=!0;for(const[h,c]of Object.entries(f)){const i=c?.value??c?.price??0;r[h]=(r[h]??0)+i}}}if(!n)return;const o={};for(const[a,s]of Object.entries(r))o[a]={value:s};return o}function Rn(e){return e.name_localized?e.name_localized:typeof e.name=="string"?e.name:e.name?.ko??""}function F0(e,t,r){if(t&&r in t)return t[r];if(Array.isArray(e)){const n=e.find(o=>typeof o.key=="string"?o.key===r:o.key?.ko===r||Object.values(o.key).includes(r));return n?typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]:void 0}return e[r]}function U0(e,t){if(t)return t;if(!Array.isArray(e))return e;const r={};for(const n of e){const o=typeof n.key=="string"?n.key:n.key?.ko??Object.values(n.key)[0]??"",a=typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]??"";o&&(r[o]=a)}return r}function z0(e,t,r){return r.find(n=>!n?.option_values||!n?.is_active?!1:e.every(o=>{const a=Rn(o),s=t?.[a];return F0(n.option_values,n.option_values_localized,a)===s}))}function H0(e,t){const r=e.params;if(!r)return;const{optionGroups:n,options:o,selectedOptionItems:a,preferredCurrency:s}=r;let l;if(r.newGroupName&&r.newValue?l={...r.currentSelection??{},[r.newGroupName]:r.newValue}:l=r.currentSelection??{},!n?.every(_=>{const M=Rn(_);return l?.[M]}))return;const h=z0(n,l,o??[]);if(!h){t.setState?.({currentSelection:{},__mergeMode:"shallow"});return}const c=n.map(_=>l[Rn(_)]).join("_"),i=a??[];if(i.find(_=>_.id===c)){const _=window.G7Core;_?.toast?.warning?.(_?.t?.("sirsoft-ecommerce.shop.already_added_option")??"이미 추가된 옵션입니다."),t.setState?.({currentSelection:{},__mergeMode:"shallow"});return}const g=h.selling_price??0,p={};n.forEach(_=>{const M=Rn(_);p[M]=l[M]});const m=r.additionalOptionGroups??[],w={};for(const _ of m){if(!_.is_required)continue;const M=_.values?.find(x=>x.is_default);M&&(w[_.id]=M.id)}const C=lc(w,m),O=ic(w,m),v=g+C,E={id:c,optionId:h.id,options:p,optionValues:U0(h.option_values,h.option_values_localized),quantity:1,stock:h.stock_quantity??999,unitPrice:g,unitPriceFormatted:Lr(g,s),totalPrice:v,totalPriceFormatted:Lr(v,s),multiCurrencyUnitPrice:h.multi_currency_selling_price,multiCurrencyTotalPrice:Qn(h.multi_currency_selling_price,1,O,C),additionalOptionSelections:w,additionalOptionsTotal:C,additionalOptionsMultiCurrencyTotal:O},I=[...i,E];t.setState?.({selectedOptionItems:I,selectedTotalMultiCurrency:Jn(I),currentSelection:{},__mergeMode:"shallow"})}function G0(e,t){const r=e.params;if(!r)return;const{selectedOptionItems:n,preferredCurrency:o}=r,a=Number(r.itemIndex)||0,s=Number(r.newQuantity)||1,l=(n??[]).map((f,h)=>{if(h!==a)return f;const c=f?.unitPrice??0,i=f?.additionalOptionsTotal??0,u=(c+i)*s;return{...f,quantity:s,totalPrice:u,totalPriceFormatted:Lr(u,o),multiCurrencyTotalPrice:Qn(f?.multiCurrencyUnitPrice,s,f?.additionalOptionsMultiCurrencyTotal,i)}});t.setState?.({selectedOptionItems:l,selectedTotalMultiCurrency:Jn(l)})}function j0(e,t){const r=e.params;if(!r)return;const{selectedOptionItems:n,additionalOptionGroups:o,preferredCurrency:a}=r,s=Number(r.itemIndex)||0,l=Number(r.additionalOptionId),f=Object.prototype.hasOwnProperty.call(r,"customText"),h=r.valueId===""||r.valueId==null?null:Number(r.valueId),c=(n??[]).map((i,u)=>{if(u!==s)return i;const g={...i.additionalOptionSelections??{}},p={...i.additionalOptionCustomTexts??{}};if(f){const E=String(r.customText??"");E.trim()===""?delete p[l]:p[l]=E}else h==null?(delete g[l],delete p[l]):(g[l]=h,(o??[]).find(_=>_.id===l)?.values?.find(_=>_.id===h)?.allow_custom_text||delete p[l]);const m=lc(g,o),w=ic(g,o),C=i?.unitPrice??0,O=i?.quantity??1,v=(C+m)*O;return{...i,additionalOptionSelections:g,additionalOptionCustomTexts:p,additionalOptionsTotal:m,additionalOptionsMultiCurrencyTotal:w,totalPrice:v,totalPriceFormatted:Lr(v,a),multiCurrencyTotalPrice:Qn(i?.multiCurrencyUnitPrice,O,w,m)}});t.setState?.({selectedOptionItems:c,selectedTotalMultiCurrency:Jn(c)})}function B0(e,t){const r=e.params;if(!r)return;const n=Number(r.itemIndex)||0,o=(r.selectedOptionItems??[]).filter((a,s)=>s!==n);t.setState?.({selectedOptionItems:o,selectedTotalMultiCurrency:Jn(o)})}function W0(e,t){const r=e.params;if(!r)return;const n=Math.max(1,Number(r.newQuantity)||1);t.setState?.({noOptionQuantity:n,noOptionTotalMultiCurrency:Qn(r.multiCurrencyUnitPrice,n)})}const cn=window.G7Core?.createLogger?.("Handler:Cart")??{log:(...e)=>console.log("[Handler:Cart]",...e),warn:(...e)=>console.warn("[Handler:Cart]",...e),error:(...e)=>console.error("[Handler:Cart]",...e)};function hr(e){const t=window.G7Core;t?.state?.setLocal?(t.state.setLocal(e),cn.log("Local state updated:",e)):cn.warn("G7Core.state.setLocal not available")}function cc(){return window.G7Core?.state?.getLocal?.()||{}}function V0(){return window.G7Core?.state?.get?.()||{}}function K0(e,t="info"){const r=window.G7Core;r?.toast?.show?r.toast.show(e,t):cn.warn("G7Core.toast.show not available, message:",e)}function Y0(e){const t=window.G7Core;return t?.t?t.t(e):e}function dc(){const e=window.G7Core;if(e?.dispatch){const r=cc().selectedItems||[];e.dispatch({handler:"refetchDataSource",params:{dataSourceId:"cartItems"}}),cn.log("Refetching cartItems with selected_ids:",r)}else cn.warn("G7Core.dispatch not available")}function X0(e,t){const{itemId:r,selectedItems:n}=e?.params||{},o=n||[];let a;o.includes(r)?a=o.filter(h=>h!==r):a=[...o,r];const l=V0().cartItems?.data?.items||[],f=l.length>0&&a.length===l.length;hr({selectedItems:a,allSelected:f,selectedCount:a.length}),dc()}function q0(e,t){const{allItemIds:r,isSelected:n}=e?.params||{};hr({selectedItems:n?r:[],allSelected:n,selectedCount:n?r.length:0}),dc()}function Z0(e,t){const{optionName:r,optionValue:n}=e?.params||{},a=cc().selectedOptions||{};hr({selectedOptions:{...a,[r]:n}})}function Q0(e,t){const{targetIds:r}=e?.params||{};if(!r||r.length===0){K0(Y0("shop.cart.select_items_to_delete"),"warning");return}hr({deleteModal:{visible:!0,targetIds:r}})}function J0(e,t){const{targetItem:r}=e?.params||{};hr({optionModal:{visible:!0,targetItem:r},selectedOptions:r?.options||{},selectedQuantity:r?.quantity})}function R0(e,t){const{selectedItems:r}=e?.params||{};if(!r||r.length===0){hr({isCalculating:!1});return}hr({isCalculating:!0})}const pr=window.G7Core?.createLogger?.("Handler:CartOptionChange")??{log:(...e)=>console.log("[Handler:CartOptionChange]",...e),warn:(...e)=>console.warn("[Handler:CartOptionChange]",...e),error:(...e)=>console.error("[Handler:CartOptionChange]",...e)};function Oa(e){return e.name_localized?e.name_localized:typeof e.name=="string"?e.name:e.name?.ko??""}function ep(e,t,r){if(t&&r in t)return t[r];if(Array.isArray(e)){const n=e.find(o=>typeof o.key=="string"?o.key===r:o.key?.ko===r||Object.values(o.key).includes(r));return n?typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]:void 0}return e[r]}function uc(e,t,r){return!e||!t?.length?null:t.every(a=>{const s=Oa(a);return e[s]})?r?.find(a=>t.every(s=>{const l=Oa(s);return ep(a.option_values,a.option_values_localized,l)===e[l]}))||null:(pr.log("Not all options selected yet"),null)}function tp(e,t){const{selectedOptions:r,optionGroups:n,options:o}=e?.params||{};pr.log("findMatchingOption called:",{selectedOptions:r,optionGroups:n,optionsCount:o?.length});const a=uc(r,n,o);return pr.log("Matched option:",a),{matchedOption:a}}function rp(e,t){const{currentOptionValues:r,optionGroups:n,options:o}=e?.params||{};if(pr.log("initCartOptionSelection called:",{currentOptionValues:r,optionGroups:n,optionsCount:o?.length}),!n?.length||!r)return pr.log("No option groups or current values, returning empty"),{selectedOptions:{},matchedOption:null};const a={};for(const l of n){const f=Oa(l),h=r[f],c=l.values_localized??(Array.isArray(l.values)?l.values.map(i=>typeof i=="string"?i:i?.ko??""):[]);h&&c.includes(h)&&(a[f]=h)}pr.log("Initial selectedOptions:",a);const s=uc(a,n,o||[]);return pr.log("Matched option:",s),{selectedOptions:a,matchedOption:s}}const dn="g7_cart_key",np="/api/modules/sirsoft-ecommerce/cart/key",Ia="g7_guest_order_token",fc="g7_guest_order_number",Da="g7_guest_order_expires_at",at=window.G7Core?.createLogger?.("Handler:Storage")??{log:(...e)=>console.log("[Handler:Storage]",...e),warn:(...e)=>console.warn("[Handler:Storage]",...e),error:(...e)=>console.error("[Handler:Storage]",...e)};async function gc(){try{const e=await fetch(np,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"}});return e.ok?(await e.json()).data?.cart_key||null:(at.error("Failed to issue cartKey from API:",e.status),null)}catch(e){return at.error("Error issuing cartKey from API:",e),null}}function rr(e){const t=window.G7Core;t?.state?.set?(t.state.set(e),at.log("Global state updated:",e)):at.warn("G7Core.state.set not available")}function op(){try{const e=window.G7Config;return e?.user?e.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}async function ap(e,t){let r=null;try{r=localStorage.getItem(dn)}catch{}if(r)at.log("Existing cartKey loaded:",r);else if(r=await gc(),r)try{localStorage.setItem(dn,r),at.log("New cartKey issued from API:",r)}catch{}else at.error("Failed to issue cartKey from API");rr({cartKey:r})}function sp(e,t){try{return localStorage.getItem(dn)}catch{return null}}function hc(e,t){try{localStorage.removeItem(dn),at.log("cartKey removed from localStorage")}catch{}rr({cartKey:null})}async function lp(e,t){if(op()){hc();return}const n=await gc();if(n){try{localStorage.setItem(dn,n),at.log("New cartKey regenerated from API:",n)}catch{}rr({cartKey:n})}else at.error("Failed to regenerate cartKey from API")}function ip(e,t){const{key:r,value:n}=e?.params||{};if(!r){at.warn("saveToStorage: key is required");return}try{localStorage.setItem(r,n),at.log("Saved to storage:",r,n)}catch{}}function cp(e,t){const{key:r,defaultValue:n}=e?.params||{};if(!r)return at.warn("loadFromStorage: key is required"),n||null;try{return localStorage.getItem(r)||n||null}catch{return n||null}}function La(){try{sessionStorage.removeItem(Ia),sessionStorage.removeItem(fc),sessionStorage.removeItem(Da)}catch{}}function dp(e,t){let r=null,n=null;try{r=sessionStorage.getItem(Ia),n=sessionStorage.getItem(Da)}catch{}let o=!1;if(r){const a=n?new Date(n).getTime():NaN;(isNaN(a)||a<=Date.now())&&(La(),r=null,o=!0)}if(r){rr({guestOrderToken:r}),at.log("Guest order token loaded from sessionStorage");return}if(!o){const s=window.G7Core?.state?.get?.("_global")?.guestOrderToken;if(typeof s=="string"&&s!==""){at.log("Guest order token preserved from in-memory _global (sessionStorage empty)");return}}rr({guestOrderToken:null}),at.log("No guest order token (visitor not authenticated)")}function up(e,t){const{token:r,orderNumber:n,expiresAt:o}=e?.params||{};if(!r||!n||!o){at.warn("saveGuestOrderToken: token, orderNumber, expiresAt are required");return}try{sessionStorage.setItem(Ia,r),sessionStorage.setItem(fc,n),sessionStorage.setItem(Da,o)}catch{}rr({guestOrderToken:r})}function fp(e,t){La(),rr({guestOrderToken:null})}function gp(e,t){La(),rr({guestOrderToken:null})}function hp(e,t){const{product:r,priceField:n,currencyCode:o}=e,a=o||t.getState("_global.preferredCurrency")||t.getState("_global.defaultCurrency")||"",s=`multi_currency_${n}`,l=r[s];if(l&&l[a])return l[a].formatted;const f=`${n}_formatted`;return r[f]||String(r[n])}const pc={KRW:"₩",USD:"$",JPY:"¥",CNY:"元",EUR:"€",GBP:"£"},pp={KRW:0,JPY:0};function mc(){try{const t=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(t?.currencies)?t.currencies:[]}catch{return[]}}function mp(e,t){const{value:r,currencyCode:n,locale:o}=e,a=mc(),s=n||t.getState("_global.preferredCurrency")||t.getState("_global.defaultCurrency")||a.find(i=>i.is_default)?.code;if(!s)return r.toLocaleString(o);const l=a.find(i=>i.code===s),f=l?.symbol&&l.symbol.length>0?l.symbol:pc[s]??"",h=l?.decimal_places??pp[s]??2,c=r.toLocaleString(o,{minimumFractionDigits:h,maximumFractionDigits:h});return f?s==="KRW"||f==="₩"||f==="원"?`${c}원`:`${f}${c}`:`${c} ${s}`}function bp(e){const t=mc().find(r=>r.code===e);return t?.symbol&&t.symbol.length>0?t.symbol:pc[e]||e}const Pa=window.G7Core?.createLogger?.("Handler:Currency")??{log:(...e)=>console.log("[Handler:Currency]",...e),warn:(...e)=>console.warn("[Handler:Currency]",...e),error:(...e)=>console.error("[Handler:Currency]",...e)},bc="g7_preferred_currency";function yp(){try{const e=window.G7Core?.state?.get?.()||{};return e.defaultCurrency||e?.modules?.["sirsoft-ecommerce"]?.language_currency?.default_currency||""}catch{return""}}function vp(e,t){const{defaultCurrency:r=yp()}=e;let n=r;try{if(typeof window<"u"&&window.localStorage){const o=localStorage.getItem(bc);o&&yc(o)&&(n=o)}}catch{Pa.warn("localStorage 접근 불가, 기본 통화 사용:",r)}return t.setState("global",{preferredCurrency:n}),n}function wp(e,t){const{currencyCode:r}=e;if(!yc(r)){Pa.warn("유효하지 않은 통화 코드:",r);return}try{typeof window<"u"&&window.localStorage&&localStorage.setItem(bc,r)}catch{Pa.warn("localStorage 저장 실패")}t.setState("global",{preferredCurrency:r})}function yc(e){return typeof e=="string"&&/^[A-Z]{3}$/.test(e)}const Pr=window.G7Core?.createLogger?.("Handler:SetTheme")??{log:(...e)=>console.log("[Handler:SetTheme]",...e),warn:(...e)=>console.warn("[Handler:SetTheme]",...e),error:(...e)=>console.error("[Handler:SetTheme]",...e)},$a=["auto","light","dark"],vc="g7_color_scheme",xp=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,eo=e=>{const t=xp(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")};function kp(){try{const e=localStorage.getItem(vc),t=e&&$a.includes(e)?e:"auto";eo(t),Pr.log("Initial theme applied:",t)}catch(e){Pr.warn("Failed to load initial theme:",e),eo("auto")}}async function Cp(e,t){const r=e?.target;if(r&&typeof r=="string"&&$a.includes(r)){eo(r);return}kp()}async function Sp(e,t){let r;if(e?.target&&typeof e.target=="string"&&!e.target.includes("{{")&&(r=e.target),!r||typeof r!="string"){Pr.warn("Invalid theme:",r);return}if(!$a.includes(r)){Pr.warn("Unsupported theme:",r);return}const n=r;try{localStorage.setItem(vc,n)}catch(o){Pr.error("Failed to save theme to localStorage:",o);return}eo(n),Pr.log("Theme changed to:",n)}const _p=window.G7Core?.createLogger?.("Handler:RedirectToLoginWithReturn")??{log:(...e)=>console.log("[Handler:RedirectToLoginWithReturn]",...e),warn:(...e)=>console.warn("[Handler:RedirectToLoginWithReturn]",...e),error:(...e)=>console.error("[Handler:RedirectToLoginWithReturn]",...e)};async function Ep(e,t){const r=window.G7Core;if(!r?.dispatch){_p.error("G7Core.dispatch 가 초기화되지 않아 로그인 리다이렉트를 실행할 수 없습니다.");return}const n=window.location.pathname+window.location.search;await r.dispatch({handler:"navigate",params:{path:"/login",query:{redirect:n}}})}const Fa=window.G7Core?.createLogger?.("Handler:DownloadAttachment")??{log:(...e)=>console.log("[Handler:DownloadAttachment]",...e),warn:(...e)=>console.warn("[Handler:DownloadAttachment]",...e),error:(...e)=>console.error("[Handler:DownloadAttachment]",...e)};async function Tp(e,t){const r=window.G7Core,{url:n,filename:o}=e?.params||{};if(!n){Fa.warn("다운로드 URL 이 없어 요청을 건너뜁니다.");return}if(!r?.api?.get){Fa.error("G7Core.api.get 이 초기화되지 않아 다운로드를 실행할 수 없습니다.");return}try{const a=await r.api.get(n,{responseType:"blob"});if(a){const s=URL.createObjectURL(a),l=document.createElement("a");l.href=s,l.download=o||"",document.body.appendChild(l),l.click(),document.body.removeChild(l),URL.revokeObjectURL(s)}}catch(a){Fa.error("첨부파일 다운로드 실패",a),r?.toast?.error?.(r?.t?.("common.download_failed")??"Download failed")}}const to={"sirsoft-basic.addSelectedItemIfComplete":H0,"sirsoft-basic.updateSelectedItemQuantity":G0,"sirsoft-basic.removeSelectedItem":B0,"sirsoft-basic.updateNoOptionQuantity":W0,"sirsoft-basic.setBlockAdditionalOption":j0,"sirsoft-basic.getDisplayPrice":hp,"sirsoft-basic.formatCurrency":mp,"sirsoft-basic.getCurrencySymbol":bp,"sirsoft-basic.loadPreferredCurrency":vp,"sirsoft-basic.savePreferredCurrency":wp,setTheme:Sp,initTheme:Cp,redirectToLoginWithReturn:Ep,downloadAttachment:Tp,toggleCartItemSelection:X0,selectAllCartItems:q0,setCartOption:Z0,openCartDeleteModal:Q0,openCartOptionModal:J0,recalculateCart:R0,findMatchingOption:tp,initCartOptionSelection:rp,initCartKey:ap,getCartKey:sp,clearCartKey:hc,regenerateCartKey:lp,saveToStorage:ip,loadFromStorage:cp,initGuestOrderToken:dp,saveGuestOrderToken:up,clearGuestOrderToken:fp,clearGuestTokenOnEntry:gp},un=window.G7Core?.createLogger?.("Template:sirsoft-basic:IdentityLauncher")??{log:(...e)=>console.log("[Template:sirsoft-basic:IdentityLauncher]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:IdentityLauncher]",...e),error:(...e)=>console.error("[Template:sirsoft-basic:IdentityLauncher]",...e)};function Np(e){const t=window.G7Core,r=t?.state?.getLocal?.()??{},n=t?.state?.getGlobal?.()??t?.state?.get?.()??{},o=(...f)=>f.find(h=>typeof h=="string"&&h.length>0),a=o(e.target?.email,n?.currentUser?.email,n?.user?.email,n?.auth?.user?.email,e.purpose==="signup"?r?.registerForm?.email:void 0,e.purpose==="password_reset"?r?.passwordResetForm?.email:void 0,e.purpose==="password_reset"?r?.email:void 0),s=o(e.target?.phone,n?.currentUser?.phone,n?.user?.phone,n?.auth?.user?.phone);if(!a&&!s)return null;const l={};return a&&(l.email=a),s&&(l.phone=s),l}function Ap(){const t=window.G7Core?.api?.getToken?.()??null;return t?{Authorization:`Bearer ${t}`}:{}}function Mp(){try{const e=typeof window<"u"?window.localStorage?.getItem("g7_locale"):null;return e?{"Accept-Language":e}:{}}catch{return{}}}async function Op(e,t){const r={purpose:e.purpose};e.provider_id&&(r.provider_id=e.provider_id),t&&(r.target=t);const n=e.challenge_start_url||"/api/identity/challenges",o=await fetch(n,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...Mp(),...Ap()},body:JSON.stringify(r)}),a=await o.json().catch(()=>null);if(!o.ok||!a?.success){const l=a?.message??`Challenge 시작 실패 (HTTP ${o.status})`;throw new Error(l)}const s=a.data??a;return{id:s.id,expires_at:s.expires_at,render_hint:s.render_hint??e.render_hint??"text_code",public_payload:s.public_payload,redirect_url:s.redirect_url,max_attempts:typeof s.max_attempts=="number"?s.max_attempts:void 0}}async function Ip(e){const t=window.G7Core,r=t?.identity;if(!t?.dispatch||!t?.state||!r)return un.error("G7Core 또는 G7Core.identity 가 초기화되지 않아 launcher 를 실행할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};if(e.render_hint==="external_redirect"||e.redirect_url)return r.redirectExternally(e);const n=Np(e);let o;try{o=await Op(e,n)}catch(p){un.error("Challenge 시작 실패:",p);try{await t.dispatch({handler:"toast",params:{type:"error",message:p instanceof Error?p.message:"본인인증 시작에 실패했습니다."}})}catch{}return{status:"failed",failureCode:"CHALLENGE_START_FAILED",reason:p instanceof Error?p.message:String(p)}}if(o.redirect_url)return r.redirectExternally({...e,render_hint:"external_redirect",redirect_url:o.redirect_url});const a=o.expires_at?new Date(o.expires_at).getTime():0,s=a>0?Math.max(0,Math.floor((a-Date.now())/1e3)):0,l=typeof o.max_attempts=="number"?o.max_attempts:0;t.state.set({identityChallenge:{policy_key:e.policy_key,purpose:e.purpose,provider_id:e.provider_id??null,render_hint:o.render_hint,challenge_id:o.id,expires_at:o.expires_at,public_payload:o.public_payload??{},target:n??null,code:"",error:null,attempts:0,maxAttempts:l,remainingSeconds:s,resendCooldown:0}});let f=a,h=0;const c=window.setInterval(()=>{const p=f>0?Math.max(0,Math.floor((f-Date.now())/1e3)):0;h=Math.max(0,h-1),t.state.set({identityChallenge:{remainingSeconds:p,resendCooldown:h}})},1e3);let i=null;try{i=t.state.subscribe?.(p=>{const m=p?.identityChallenge?.expires_at;if(typeof m=="string"){const C=new Date(m).getTime();Number.isFinite(C)&&C!==f&&(f=C)}const w=p?.identityChallenge?.resendCooldown;typeof w=="number"&&w>h&&(h=w)})??null}catch{}const u=r.createDeferred();try{await t.dispatch({handler:"openModal",target:"identity-challenge-modal"})}catch(p){return un.error("openModal 실패 — 풀페이지로 폴백:",p),window.clearInterval(c),i?.(),r.redirectExternally({...e,render_hint:"external_redirect",redirect_url:`/identity/challenge?challenge_id=${encodeURIComponent(o.id)}&return=${encodeURIComponent(window.location.href)}`})}const g=await u;return window.clearInterval(c),i?.(),g}function Dp(){const e=window.G7Core?.identity;if(!e?.setLauncher){un.warn("G7Core.identity 가 아직 초기화되지 않아 launcher 등록을 건너뜁니다. 코어 부트스트랩 순서를 확인하세요.");return}e.setLauncher(Ip),un.log("IDV launcher registered (sirsoft-basic)")}const wc=window.G7Core?.createLogger?.("Template:sirsoft-basic:device")??{log:(...e)=>console.log("[Template:sirsoft-basic:device]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:device]",...e)};function Lp(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),r=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),n=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(r)?!0:/macintosh|mac os x/.test(t)&&n>1}function Pp(){const e=window.G7Core;if(!e?.state?.get||!e?.state?.set){wc.warn("G7Core.state not available; skip iOS device correction");return}if(!Lp())return;const r=(e.state.get()||{}).appConfig||{};r.isIos!==!0&&(e.state.set({appConfig:{...r,isIos:!0}}),wc.log("appConfig.isIos corrected to true by client signals (iPadOS desktop-UA)"))}function $p(e){const t=e.icons;return Array.isArray(t)?t.filter(r=>!!r&&typeof r=="object"&&typeof r.value=="string"):[]}function Fp(e,t){const r=e.label;return typeof r=="string"?r.startsWith("$t:")?t(r.slice(3)):r:e.value}function Up(e,t,r){if(r==="")return!0;const n=r.toLowerCase();return e.value.toLowerCase().includes(n)||t.toLowerCase().includes(n)?!0:(e.keywords??[]).some(o=>o.toLowerCase().includes(n))}function zp({entry:e}){const t=e.preview;return t?.html?d.jsx("span",{className:"g7le-icon-preview",dangerouslySetInnerHTML:{__html:t.html},style:za}):t?.className?d.jsx("span",{className:`g7le-icon-preview ${t.className}`,style:za}):d.jsx("i",{className:`fas fa-${e.value}`,style:za,"aria-hidden":"true"})}function Hp({control:e,value:t,onChange:r,t:n}){const o=$p(e),a=t==null?"":String(t),s=typeof e.iconColumns=="number"?e.iconColumns:6,l=e.iconSearchPlaceholder,f=typeof l=="string"&&l.startsWith("$t:")?n(l.slice(3)):n("layout_editor.icon_picker.search_placeholder"),[h,c]=k.useState("");if(o.length===0)return d.jsx("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:xc,children:d.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-free","data-testid":"g7le-widget-icon-picker-free",value:a,placeholder:n("layout_editor.icon_picker.free_input_hint"),onChange:g=>r(g.target.value===""?void 0:g.target.value),style:kc})});const i=g=>Fp(g,n),u=o.filter(g=>Up(g,i(g),h));return d.jsxs("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:xc,children:[d.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-search","data-testid":"g7le-widget-icon-picker-search",value:h,placeholder:f,onChange:g=>c(g.target.value),style:kc}),d.jsxs("div",{className:"g7le-icon-grid","data-testid":"g7le-widget-icon-picker-grid",style:{...Gp,gridTemplateColumns:`repeat(${s}, minmax(0, 1fr))`},children:[d.jsx("button",{type:"button","data-testid":"g7le-icon-cell-none",title:n("layout_editor.icon_picker.none"),onClick:()=>r(void 0),"aria-pressed":a==="",style:a===""?Cc:Ua,children:d.jsx("span",{style:jp,children:"—"})}),u.map(g=>{const p=g.value===a;return d.jsx("button",{type:"button","data-testid":`g7le-icon-cell-${g.value}`,title:i(g),"aria-pressed":p,onClick:()=>r(g.value),style:p?Cc:Ua,children:d.jsx(zp,{entry:g})},g.value)})]}),u.length===0&&d.jsx("div",{"data-testid":"g7le-widget-icon-picker-empty",style:Bp,children:n("layout_editor.icon_picker.no_results")})]})}const xc={display:"flex",flexDirection:"column",gap:6,width:"100%"},kc={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%"},Gp={display:"grid",gap:4,maxHeight:220,overflowY:"auto",padding:2},Ua={display:"flex",alignItems:"center",justifyContent:"center",minHeight:34,padding:4,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",cursor:"pointer"},Cc={...Ua,border:"2px solid #2563eb",background:"#eff6ff"},za={display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:16,lineHeight:1},jp={fontSize:10,color:"#475569",fontFamily:"monospace",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"100%"},Bp={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"4px 2px"},Sc=window.G7Core?.createLogger?.("Template:sirsoft-basic:EditorWidgets")??{log:(...e)=>console.log("[Template:sirsoft-basic:EditorWidgets]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:EditorWidgets]",...e)};function Wp(){if(typeof window>"u")return;const e=window.G7Core?.layoutEditor;if(!e?.registerWidget){Sc.warn("G7Core.layoutEditor 미노출 — icon-picker 위젯 등록 보류");return}e.registerWidget("icon-picker",Hp),Sc.log("편집기 위젯 'icon-picker' 등록됨(Font Awesome 검색 그리드)")}const Ha=window.G7Core?.createLogger?.("Template:sirsoft-basic")??{log:(...e)=>console.log("[Template:sirsoft-basic]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic]",...e),error:(...e)=>console.error("[Template:sirsoft-basic]",...e)};typeof window<"u"&&(window.G7TemplateHandlers=to);function _c(){if(typeof window<"u"){let e=0;const t=50,r=()=>{const n=window.G7Core?.getActionDispatcher?.();n?(Object.entries(to).forEach(([o,a])=>{n.registerHandler(o,a)}),Ha.log(`${Object.keys(to).length} custom handler(s) registered:`,Object.keys(to)),Dp(),Pp()):(e++,e<=t?(Ha.warn(`ActionDispatcher not found, retrying... (${e}/${t})`),setTimeout(r,100)):Ha.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};document.readyState==="complete"?r():window.addEventListener("load",r)}}return Wp(),_c(),J.A=ao,J.Avatar=Ur,J.AvatarUploader=Aa,J.BrandMark=xo,J.Button=Q,J.Checkbox=Xa,J.Code=qc,J.ConfirmDialog=ta,J.Container=A0,J.Div=W,J.ExpandableContent=Jo,J.FileInput=ja,J.FileUploader=Xn,J.Flex=O0,J.Footer=ts,J.Form=so,J.Grid=M0,J.H1=Fc,J.H2=Va,J.H3=gn,J.H4=Ka,J.Header=Ra,J.Hr=lo,J.HtmlContent=qo,J.HtmlEditor=Dl,J.Icon=de,J.IconName=Je,J.ImageGallery=Tr,J.Img=Lt,J.Input=xt,J.Label=yr,J.Li=Xt,J.MobileNav=rs,J.Modal=Ro,J.Nav=Fr,J.NotificationCenter=mn,J.Ol=Uc,J.Optgroup=jc,J.Option=Gc,J.P=Dt,J.PageLoading=Ri,J.PageSkeleton=Na,J.PageTransitionBlur=_a,J.PageTransitionIndicator=Sa,J.Pagination=ec,J.PasswordInput=Wa,J.PostReactions=Ol,J.ProductCard=ps,J.ProductImageViewer=An,J.QuantitySelector=Al,J.RichTextEditor=Il,J.SearchBar=rc,J.Section=qa,J.SectionLayout=I0,J.Select=Ya,J.SlotContainer=pn,J.SocialLoginButtons=Wi,J.Span=R,J.Svg=fn,J.TabNavigation=sc,J.Table=Bc,J.Tbody=Vc,J.Td=Xc,J.Textarea=no,J.Th=Yc,J.Thead=Wc,J.ThemeToggle=hn,J.ThreeColumnLayout=D0,J.Toast=Ca,J.Tr=Kc,J.Ul=oo,J.UserInfo=ac,J.availablePasswordRules=ro,J.compositeComponents=N0,J.defaultPasswordRules=Ba,J.initTemplate=_c,J.templateMetadata=L0,Object.defineProperty(J,Symbol.toStringTag,{value:"Module"}),J})({},ReactJSXRuntime,React,ReactDOM); -//# sourceMappingURL=components.iife.js.map + `})]}):null},a0=Object.freeze(Object.defineProperty({__proto__:null,PageTransitionIndicator:_a,default:_a},Symbol.toStringTag,{value:"Module"})),Yi=window.G7Core?.createLogger?.("Comp:PageTransitionBlur")??{log:(...e)=>console.log("[Comp:PageTransitionBlur]",...e),warn:(...e)=>console.warn("[Comp:PageTransitionBlur]",...e)},Ea=({className:e="",style:t})=>{const[r,n]=k.useState(!1);return k.useEffect(()=>{const o=window.G7Core?.TransitionManager;if(!o){Yi.warn("TransitionManager를 찾을 수 없습니다.");return}return n(o.getIsPending()),o.subscribe(s=>{Yi.log("isPending changed:",s),n(s)})},[]),r?d.jsx(W,{className:`fixed inset-0 z-40 bg-white/30 dark:bg-gray-900/30 backdrop-blur-sm transition-opacity duration-150 pointer-events-none ${e}`,style:t,role:"status","aria-label":"페이지 전환 중","data-testid":"page-transition-blur"}):null},s0=Object.freeze(Object.defineProperty({__proto__:null,PageTransitionBlur:Ea,default:Ea},Symbol.toStringTag,{value:"Module"})),Xi={sm:640,md:768,lg:1024,xl:1280,"2xl":1536},qi=new Set(["hidden","block","inline-block","inline","flex","inline-flex","grid","inline-grid","table","table-row","table-cell","contents","list-item","flow-root"]),l0=new Set(["Div","Flex","Grid","Section","Article","Nav","Aside","Main","Header","Footer","Fragment","Form","Container"]),i0=new Set(["H1","H2","H3","H4","H5","H6","P","Span","Label","A","Strong","Em","Small","Badge"]),c0=new Set(["Input","Textarea","Select","Checkbox","Radio","Toggle","DatePicker","TagInput","SearchBar"]),d0=new Set(["Img","Image","Avatar","AvatarUploader","ImageGallery","ProductImageViewer","FileUploader"]),Zi={DataGrid:(e,t)=>d.jsxs("div",{className:"w-full border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden",children:[d.jsx("div",{className:`h-10 bg-gray-100 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 ${e}`,children:d.jsxs("div",{className:"flex items-center h-full px-4 gap-8",children:[d.jsx("div",{className:"h-3 w-16 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-24 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-20 bg-gray-300 dark:bg-gray-600 rounded"}),d.jsx("div",{className:"h-3 w-16 bg-gray-300 dark:bg-gray-600 rounded"})]})}),Array.from({length:t},(r,n)=>d.jsx("div",{className:`h-12 border-b border-gray-100 dark:border-gray-800 ${e}`,style:{animationDelay:`${n*75}ms`},children:d.jsxs("div",{className:"flex items-center h-full px-4 gap-8",children:[d.jsx("div",{className:"h-3 w-12 bg-gray-200 dark:bg-gray-700 rounded"}),d.jsx("div",{className:"h-3 flex-1 bg-gray-200 dark:bg-gray-700 rounded"}),d.jsx("div",{className:"h-3 w-16 bg-gray-100 dark:bg-gray-800 rounded"}),d.jsx("div",{className:"h-3 w-14 bg-gray-100 dark:bg-gray-800 rounded"})]})},n))]}),Pagination:e=>d.jsx("div",{className:"flex justify-center gap-1.5 py-3",children:Array.from({length:5},(t,r)=>d.jsx("div",{className:`w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),Tabs:e=>d.jsxs("div",{children:[d.jsx("div",{className:"flex gap-4 border-b border-gray-200 dark:border-gray-700 pb-2 mb-3",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),d.jsx("div",{className:`h-32 bg-gray-100 dark:bg-gray-800 rounded ${e}`})]}),TabNavigation:e=>d.jsx("div",{className:"flex gap-4 border-b border-gray-200 dark:border-gray-700 pb-2",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-4 w-20 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))}),ProductCard:e=>d.jsxs("div",{className:"rounded-lg overflow-hidden",children:[d.jsx("div",{className:`h-48 w-full bg-gray-200 dark:bg-gray-700 ${e}`}),d.jsxs("div",{className:"p-3 space-y-2",children:[d.jsx("div",{className:`h-3.5 w-3/4 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:`h-4 w-1/3 bg-gray-300 dark:bg-gray-600 rounded ${e}`})]})]}),UserInfo:e=>d.jsx("div",{className:`h-3 w-14 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),QuantitySelector:e=>d.jsx("div",{className:`h-9 w-28 bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 ${e}`}),Header:e=>d.jsx("header",{className:"bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700",children:d.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8",children:d.jsxs("div",{className:"flex items-center justify-between h-16",children:[d.jsxs("div",{className:"flex items-center gap-8",children:[d.jsx("div",{className:`h-8 w-24 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:"hidden lg:flex items-center gap-6",children:Array.from({length:4},(t,r)=>d.jsx("div",{className:`h-3.5 w-16 bg-gray-200 dark:bg-gray-700 rounded ${e}`},r))})]}),d.jsx("div",{className:"flex items-center gap-4",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-8 w-8 bg-gray-200 dark:bg-gray-700 rounded-full ${e}`},r))})]})})}),Footer:e=>d.jsx("footer",{className:"bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 mt-auto",children:d.jsx("div",{className:"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8",children:d.jsxs("div",{className:"flex flex-col items-center gap-4",children:[d.jsx("div",{className:`h-4 w-32 bg-gray-200 dark:bg-gray-700 rounded ${e}`}),d.jsx("div",{className:`h-3 w-48 bg-gray-100 dark:bg-gray-800 rounded ${e}`}),d.jsx("div",{className:"flex gap-3",children:Array.from({length:3},(t,r)=>d.jsx("div",{className:`h-6 w-6 bg-gray-200 dark:bg-gray-700 rounded-full ${e}`},r))}),d.jsx("div",{className:`h-3 w-40 bg-gray-100 dark:bg-gray-800 rounded ${e}`})]})})}),Modal:()=>d.jsx(d.Fragment,{}),Toast:()=>d.jsx(d.Fragment,{}),ConfirmDialog:()=>d.jsx(d.Fragment,{})};function u0(e){if(!e||e.length===0)return[];const t=e.filter(n=>n.if),r=e.filter(n=>!n.if);if(r.length>0&&t.length>0){const n=t.filter(o=>!Qi(o.if));return n.length>1&&f0(n)?[...r,Ji(n)]:[...r,...n]}if(t.length>1&&r.length===0){const n=t.filter(o=>!Qi(o.if));return n.length===0?[]:n.length===1?n:[Ji(n)]}return e}function Qi(e){const t=e.replace(/^\{\{|\}\}$/g,"").trim();return t.startsWith("!")||/\b(hasError|isError|isLoading|error|Error)\b/.test(t)||/===\s*0\s*(\)|\}|$)/.test(t)}function f0(e){if(e.length<2)return!1;const t=[];for(const n of e){const o=(n.if||"").replace(/^\{\{|\}\}$/g,"").trim(),a=o.match(/^(.+?)\s*===\s*['"]/);if(a)t.push(a[1].trim());else{const s=o.match(/(.+?)\s*===\s*['"]/);if(!s)return!1;t.push(s[1].trim())}}const r=t[0];return t.every(n=>n===r)}function Ji(e){return e.reduce((t,r)=>Ta(r)>Ta(t)?r:t)}function Ta(e){return e.children?e.children.length+e.children.reduce((t,r)=>t+Ta(r),0):0}function g0(e){return e.includes("{{")?e.replace(/\{\{([^}]+)\}\}/g,(t,r)=>{const n=r.match(/\?[\s]*['"]([^'"]*)['"]\s*:\s*['"]/);return n?n[1]:""}):e}function Na(e,t){if(!e)return"";let r=e;return r=r.replace(/\b(bg-\S+|text-\S+|border\b|border-\S+|shadow\S*|ring-\S+|outline-\S+|divide-\S+|opacity-\S+|font-\S+|tracking-\S+|leading-\S+|whitespace-\S+|truncate|line-clamp-\S+|underline|no-underline|decoration-\S+|cursor-\S+|hover:\S+|focus:\S+|active:\S+|group-hover:\S+|transition\S*|duration-\S+|ease-\S+|animate-\S+|dark:\S+)\b/g,"").replace(/\s{2,}/g," ").trim(),(t==="Grid"||/\bgrid-cols-\d/.test(r))&&(r=r.replace(/\bgrid-cols-(\d+)/g,(n,o)=>{const a=parseInt(o);return a>4?`grid-cols-${Math.ceil(a/2)}`:`grid-cols-${a}`})),r}function h0(e){const{name:t,props:r}=e;if(!r||!t)return"";const n=[];if(t==="Flex"){n.push("flex");const o=r.direction||"row",a={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};a[o]&&n.push(a[o]);const s=r.justify||"start",l={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};l[s]&&n.push(l[s]);const f=r.align||"stretch",h={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};h[f]&&n.push(h[f]);const c=r.wrap||"nowrap",i={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};i[c]&&n.push(i[c]),r.gap&&r.gap>0&&n.push(`gap-${r.gap}`)}if(t==="Grid"){n.push("grid");const o=r.cols||1;if(n.push(`grid-cols-${o}`),r.responsive){const a=r.responsive;a.sm&&n.push(`sm:grid-cols-${a.sm}`),a.md&&n.push(`md:grid-cols-${a.md}`),a.lg&&n.push(`lg:grid-cols-${a.lg}`),a.xl&&n.push(`xl:grid-cols-${a.xl}`),a["2xl"]&&n.push(`2xl:grid-cols-${a["2xl"]}`)}r.gap!==void 0&&r.gap>0&&n.push(`gap-${r.gap}`),r.rowGap!==void 0&&r.rowGap>0&&n.push(`gap-y-${r.rowGap}`),r.colGap!==void 0&&r.colGap>0&&n.push(`gap-x-${r.colGap}`)}return n.join(" ")}function p0(e){switch(e){case"pulse":return"animate-pulse";case"wave":return"animate-pulse";case"none":return"";default:return"animate-pulse"}}function qn(e,t,r,n){return!e||e.length===0?[]:u0(e).map((a,s)=>Ri(a,t,r,s,n))}function m0(e,t){const r=e.props?.className||e.className||"";return e.responsive?t>=1024&&e.responsive.desktop?.props?.className?e.responsive.desktop.props.className:t>=768&&e.responsive.tablet?.props?.className?e.responsive.tablet.props.className:r:r}function b0(e,t){const r=e.split(/\s+/).filter(Boolean),n=[],o=[];for(const f of r){const h=f.match(/^(sm|md|lg|xl|2xl):(.+)$/);if(h){const[,c,i]=h;if(qi.has(i)){n.push({breakpoint:Xi[c]||0,display:i});continue}(Xi[c]||0)<=t&&o.push(i);continue}if(qi.has(f)){n.push({breakpoint:0,display:f});continue}o.push(f)}if(n.length===0)return{visible:!0,className:e};n.sort((f,h)=>f.breakpoint-h.breakpoint);let a="";for(const f of n)f.breakpoint<=t&&(a=f.display);a||(a=n[0].display);const s=a!=="hidden",l=s?[a,...o].join(" "):"";return{visible:s,className:l}}function y0(e){const t=e.match(/\bgrid-cols-\[([^\]]+)\]/);if(!t)return{className:e};const r=t[1].replace(/_/g," ");return{className:e.replace(/\bgrid-cols-\[([^\]]+)\]/,"").replace(/\s{2,}/g," ").trim(),style:{gridTemplateColumns:r}}}function Ri(e,t,r,n,o){const{name:a,children:s,iteration:l}=e,f=m0(e,o),h=h0(e),c=h?`${h} ${f}`.trim():f,i=g0(c),u=b0(i,o);if(!a||!u.visible)return null;const g=u.className;if(l)return d.jsx(k.Fragment,{children:Array.from({length:r},(m,x)=>Ri({...e,iteration:void 0},t,r,x,o))},n);if(Zi[a])return d.jsx("div",{className:g,children:Zi[a](t,r)},n);if(l0.has(a)){const m=Na(g,a);if(!s||s.length===0){const E=/\b(h-\S|w-\S)/.test(m)?`${m} bg-gray-200 dark:bg-gray-700 rounded ${t}`:`${m} h-3.5 w-full bg-gray-200 dark:bg-gray-700 rounded ${t}`.trim();return d.jsx("div",{className:E},n)}const x=qn(s,t,r,o);if(!(x.length>0&&x.some(v=>v!==null)))return null;const O=y0(m);return d.jsx("div",{className:O.className,style:O.style,children:x},n)}const p=Na(g,a);if(i0.has(a)){const m={H1:"h-7",H2:"h-6",H3:"h-5",H4:"h-5",H5:"h-4",H6:"h-4",P:"h-3.5",Span:"h-3",Label:"h-3",A:"h-3.5",Badge:"h-5 w-16"},x={H1:"w-3/5",H2:"w-2/5",H3:"w-1/3",H4:"w-1/3",H5:"w-1/4",H6:"w-1/4",P:"w-full",Span:"w-1/4",Label:"w-1/4",A:"w-1/3",Strong:"w-1/5",Em:"w-1/4",Small:"w-1/5",Badge:"w-16"},C=m[a]||"h-3.5",O=x[a]||"w-full",v=a.startsWith("H")?"mb-2 mt-1":"mb-1.5";return d.jsx("div",{className:`${C} ${O} bg-gray-200 dark:bg-gray-700 rounded ${t} ${v} ${p}`.trim()},n)}if(c0.has(a))return a==="Textarea"?d.jsx("div",{className:`h-20 w-full bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 mb-2 ${t}`},n):a==="Checkbox"||a==="Radio"||a==="Toggle"?d.jsx("div",{className:`h-5 w-5 bg-gray-200 dark:bg-gray-700 rounded mb-1.5 ${t}`},n):d.jsx("div",{className:`h-9 w-full bg-gray-100 dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 mb-2 ${t}`},n);if(d0.has(a))return a==="Avatar"||a==="AvatarUploader"?d.jsx("div",{className:`h-10 w-10 bg-gray-200 dark:bg-gray-700 rounded-full ${t}`},n):a==="ProductImageViewer"?d.jsx("div",{className:`h-80 w-full bg-gray-200 dark:bg-gray-700 rounded ${t}`},n):d.jsx("div",{className:`h-40 w-full bg-gray-200 dark:bg-gray-700 rounded mb-2 ${t}`},n);if(a==="Button"){if(s&&s.length>0){const m=Na(g,a),x=qn(s,t,r,o);if(!(x.length>0&&x.some(E=>E!==null)))return null;const v=/\b(border|rounded|px-\d|py-\d)/.test(f)?"border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-lg":"";return d.jsx("div",{className:`${m} ${v}`.trim(),children:x},n)}return d.jsx("div",{className:`h-9 w-24 bg-gray-200 dark:bg-gray-700 rounded ${t}`},n)}return a==="Icon"?d.jsx("div",{className:`h-5 w-5 bg-gray-200 dark:bg-gray-700 rounded ${t}`},n):s&&s.length>0?d.jsx("div",{className:p,children:qn(s,t,r,o)},n):d.jsx("div",{className:`h-3.5 w-full bg-gray-200 dark:bg-gray-700 rounded mb-1.5 ${t}`},n)}const Aa=({components:e,options:t})=>{const r=p0(t.animation),o=window.G7Core?.useResponsive,a=o?.(),s=a?a.width:typeof window<"u"?window.innerWidth:1024;return d.jsx("div",{className:"w-full",role:"status","aria-busy":"true","aria-label":"Loading...",children:qn(e,r,t.iteration_count,s)})},v0=Object.freeze(Object.defineProperty({__proto__:null,PageSkeleton:Aa,default:Aa},Symbol.toStringTag,{value:"Module"})),w0=(e,t)=>window.G7Core?.t?.(e,t)??e,ec=({options:e})=>{const t=document.documentElement.classList.contains("dark"),r=t?"rgb(17,24,39)":"rgb(249,250,251)",n=t?"#6b7280":"#9ca3af",o=t?"#9ca3af":"#6b7280";return d.jsxs("div",{style:{position:"absolute",inset:0,zIndex:2147483647,overflow:"hidden",background:r,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"flex-start",paddingTop:"15%",gap:"12px"},children:[d.jsx("div",{style:{width:"32px",height:"32px",border:`3px solid ${n}`,borderTopColor:"transparent",borderRadius:"50%",animation:"g7-spin 0.8s linear infinite"}}),d.jsx("span",{style:{color:o,fontSize:"14px"},children:e?.text||w0("nav.loading")})]})},x0=Object.freeze(Object.defineProperty({__proto__:null,default:ec},Symbol.toStringTag,{value:"Module"})),Dr=(e,t)=>window.G7Core?.t?.(e,t)??e,tc=({currentPage:e,totalPages:t,onPageChange:r,maxVisiblePages:n=5,showFirstLast:o=!0,showFirst:a,showLast:s,hasMorePages:l,className:f="",style:h,prevText:c,nextText:i,id:u,editorAttrs:g})=>{const p=t!=null,m=p?t:0,x=p?e<m:l??!1,C=(a??o)&&p,O=(s??o)&&p,v=k.useMemo(()=>{const M=[];if(!p)return M;if(m<=n+2)for(let w=1;w<=m;w++)M.push(w);else{const w=Math.floor(n/2);let y=Math.max(2,e-w),T=Math.min(m-1,e+w);e<=w+1?T=n:e>=m-w&&(y=m-n+1),M.push(1),y>2&&M.push("...");for(let A=y;A<=T;A++)M.push(A);T<m-1&&M.push("..."),m>1&&M.push(m)}return M},[e,m,p,n]),E=M=>{M<1||M===e||p&&M>m||r(M)},I=()=>{e>1&&r(e-1)},_=()=>{x&&r(e+1)};return d.jsxs(W,{className:`flex items-center gap-2 ${f}`,style:h,role:"navigation","aria-label":Dr("common.pagination"),id:u,...g,children:[C&&d.jsx(Q,{onClick:()=>E(1),disabled:e===1,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.first_page"),children:"«"}),d.jsx(Q,{onClick:I,disabled:e===1,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.prev_page"),children:c||d.jsx(d.Fragment,{children:"‹"})}),d.jsxs(W,{className:"flex items-center gap-1",children:[!p&&d.jsx(R,{className:"px-3 py-1 text-sm text-gray-700 dark:text-gray-300",children:e}),v.map((M,w)=>{if(M==="...")return d.jsx(R,{className:"px-3 py-1 text-sm text-gray-500 dark:text-gray-400",children:"..."},`ellipsis-${w}`);const y=M,T=y===e;return d.jsx(Q,{onClick:()=>E(y),className:`px-3 py-1 text-sm border rounded ${T?"bg-blue-500 dark:bg-blue-600 text-white border-blue-500 dark:border-blue-600":"border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800"}`,"aria-label":Dr("common.page_n",{n:y}),"aria-current":T?"page":void 0,children:y},y)})]}),d.jsx(Q,{onClick:_,disabled:!x,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.next_page"),children:i||d.jsx(d.Fragment,{children:"›"})}),O&&d.jsx(Q,{onClick:()=>E(m),disabled:e===m,className:"px-3 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800","aria-label":Dr("common.last_page"),children:"»"})]})},k0=Object.freeze(Object.defineProperty({__proto__:null,Pagination:tc},Symbol.toStringTag,{value:"Module"})),rc=(e,t)=>window.G7Core?.t?.(e,t)??e,nc=({name:e="search",placeholder:t,value:r,onChange:n,onSubmit:o,showButton:a=!1,suggestions:s=[],onSuggestionClick:l,showSuggestions:f=!1,className:h="",style:c,id:i,editorAttrs:u})=>{const g=t??rc("common.search_placeholder"),[p,m]=k.useState(""),[x,C]=k.useState(!1),O=k.useRef(null),v=k.useRef(""),E=r!==void 0?r:p,I=f&&x&&s.length>0&&E.length>0;k.useEffect(()=>{r!==void 0&&(v.current=r)},[r]);const _=y=>{const T=y.target.value;m(T),n?.(y),v.current!==""&&T===""&&!y.nativeEvent.inputType&&setTimeout(()=>{const A=O.current?.querySelector("form");A&&A.requestSubmit()},0),v.current=T},M=y=>{m(y.text);const T={target:{value:y.text,name:e}};n?.(T),l?.(y),C(!1)},w=y=>{y.preventDefault(),o?.(y)};return d.jsxs(W,{ref:O,className:`relative ${h}`,style:c,id:i,...u,children:[d.jsx(so,{onSubmit:w,className:"relative",children:d.jsxs(W,{className:`relative flex items-center ${a?"gap-2":""}`,children:[d.jsxs(W,{className:"relative flex-1",children:[d.jsx(W,{className:"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none",children:d.jsx(de,{name:Je.Search,className:"w-5 h-5 text-gray-400 dark:text-gray-500"})}),d.jsx(xt,{type:"search",name:e,value:E,onChange:_,onFocus:()=>C(!0),onBlur:()=>setTimeout(()=>C(!1),200),placeholder:g,className:"w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm"})]}),a&&d.jsx(Q,{type:"submit",className:"px-4 py-2 bg-gray-800 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-900 dark:hover:bg-gray-500 transition-colors text-sm font-medium cursor-pointer h-[42px]",children:rc("common.search")})]})}),I&&d.jsx(W,{className:"absolute z-10 w-full mt-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg max-h-64 overflow-y-auto",children:s.map(y=>d.jsx(W,{className:"px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer border-b border-gray-100 dark:border-gray-700 last:border-b-0 transition-colors",onClick:()=>M(y),children:d.jsxs(W,{className:"flex items-center gap-2",children:[d.jsx(de,{name:Je.Search,className:"w-4 h-4 text-gray-400 dark:text-gray-500"}),d.jsx(R,{className:"text-sm text-gray-700 dark:text-gray-300",children:y.text})]})},y.id))})]})},C0=Object.freeze(Object.defineProperty({__proto__:null,SearchBar:nc},Symbol.toStringTag,{value:"Module"})),vt=window.G7Core,S0={sm:"md",md:"lg",lg:"xl",xl:"2xl","2xl":"3xl"};function Ma({src:e,fallbackText:t="U",size:r="lg",uploadEndpoint:n="/api/me/avatar",deleteEndpoint:o="/api/me/avatar",onUploadSuccess:a,onUploadError:s,onDeleteSuccess:l,onDeleteError:f,showDeleteButton:h=!0,accept:c="image/*",maxSize:i=5,className:u="",uploadButtonText:g,deleteButtonText:p,readOnly:m=!1,confirmDelete:x=!0,deleteConfirmMessage:C,confirmUpload:O=!0,uploadConfirmMessage:v,uploadSuccessActions:E,uploadErrorActions:I,deleteSuccessActions:_,deleteErrorActions:M,id:w,editorAttrs:y}){const T=k.useRef(null),[A,P]=k.useState(!1),[D,L]=k.useState(!1),[U,S]=k.useState(void 0),[b,F]=k.useState(null),[$,G]=k.useState(!1),[Y,B]=k.useState(!1),[K,j]=k.useState(null),[H,q]=k.useState(null),ue=k.useRef(`avatar-upload-${Math.random().toString(36).slice(2,11)}`).current;k.useEffect(()=>{if(K){const oe=URL.createObjectURL(K);return q(oe),()=>URL.revokeObjectURL(oe)}else q(null)},[K]);const te=k.useCallback(oe=>vt?.t?.(oe)??oe,[]),fe=k.useCallback(()=>{const oe=document.cookie.match(/XSRF-TOKEN=([^;]+)/);return oe?decodeURIComponent(oe[1]):null},[]),ee=k.useCallback(()=>vt?.api?.getToken?.()??null,[]),xe=k.useCallback(async oe=>{B(!1),j(null),P(!0),F(null);try{const ae=new FormData;ae.append("avatar",oe);const be=fe(),ke=ee(),He=await fetch(n,{method:"POST",headers:{Accept:"application/json",...be&&{"X-XSRF-TOKEN":be},...ke&&{Authorization:`Bearer ${ke}`}},credentials:"include",body:ae});if(!He.ok){const Oe=await He.json().catch(()=>({}));throw new Error(Oe.message||te("attachment.upload_failed"))}const Pe=await He.json(),Ve=Pe.data||Pe;Ve.avatar&&S(Ve.avatar),a?.(Ve),vt?.toast?.success?.(te("mypage.profile.avatar_uploaded")),E?.length?E.forEach(Oe=>vt?.dispatch?.(Oe)):vt?.refetchDataSource?.("user")}catch(ae){const be=ae instanceof Error?ae:new Error(String(ae));F(be.message),s?.(be),vt?.toast?.error?.(be.message),I?.length&&I.forEach(ke=>vt?.dispatch?.(ke))}finally{P(!1),T.current&&(T.current.value="")}},[n,a,s,fe,ee,te]),he=k.useCallback(async oe=>{const ae=oe.target.files?.[0];if(!ae)return;F(null);const be=i*1024*1024;if(ae.size>be){const ke=new Error(te("attachment.file_too_large").replace(":max",`${i}MB`));F(ke.message),s?.(ke),vt?.toast?.error?.(ke.message);return}if(c&&!ae.type.match(c.replace("*",".*"))){const ke=new Error(te("attachment.invalid_file_type"));F(ke.message),s?.(ke),vt?.toast?.error?.(ke.message);return}O?(j(ae),B(!0)):xe(ae)},[i,c,s,O,xe,te]),Ge=k.useCallback(async()=>{G(!1),L(!0),F(null);try{const oe=fe(),ae=ee(),be=await fetch(o,{method:"DELETE",headers:{Accept:"application/json","Content-Type":"application/json",...oe&&{"X-XSRF-TOKEN":oe},...ae&&{Authorization:`Bearer ${ae}`}},credentials:"include"});if(!be.ok){const ke=await be.json().catch(()=>({}));throw new Error(ke.message||te("attachment.delete_failed"))}S(null),l?.(),vt?.toast?.success?.(te("mypage.profile.avatar_deleted")),_?.length?_.forEach(ke=>vt?.dispatch?.(ke)):vt?.refetchDataSource?.("user")}catch(oe){const ae=oe instanceof Error?oe:new Error(String(oe));F(ae.message),f?.(ae),vt?.toast?.error?.(ae.message),M?.length&&M.forEach(be=>vt?.dispatch?.(be))}finally{L(!1)}},[o,l,f,fe,ee,te]),De=k.useCallback(()=>{x?G(!0):Ge()},[x,Ge]),ye=k.useCallback(()=>{G(!1)},[]),Me=k.useCallback(()=>{B(!1),j(null),T.current&&(T.current.value="")},[]),Ce=k.useCallback(()=>{K&&xe(K)},[K,xe]),we=U===void 0?e:U,Fe=!!we,Le=S0[r]||"lg",V=$||Y,re=()=>d.jsxs(W,{className:"flex items-center justify-center gap-2",children:[d.jsx(xt,{ref:T,id:ue,type:"file",accept:c,className:"hidden",onChange:he,disabled:A}),!Fe&&d.jsxs(yr,{htmlFor:ue,className:`inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${A?"opacity-50 cursor-not-allowed":""}`,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"image",size:"sm"}),d.jsx(R,{children:g||te("mypage.profile.register_avatar")})]}),Fe&&d.jsxs(d.Fragment,{children:[d.jsxs(yr,{htmlFor:ue,className:`inline-flex items-center gap-2 px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer ${A?"opacity-50 cursor-not-allowed":""}`,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"upload",size:"sm"}),d.jsx(R,{children:g||te("common.change")})]}),h&&d.jsxs(Q,{type:"button",className:"inline-flex items-center gap-2 px-4 py-2 border border-red-300 dark:border-red-600 text-red-600 dark:text-red-400 rounded-lg text-sm font-medium hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",onClick:De,disabled:D,children:[D?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"trash-2",size:"sm"}),d.jsx(R,{children:p||te("common.delete")})]})]})]});return d.jsxs(W,{className:`flex flex-col items-center gap-3 ${u}`,id:w,...y,children:[Y&&H?d.jsxs(W,{className:"relative",children:[d.jsx(Lt,{src:H,alt:"Preview",className:`rounded-full object-cover ${r==="sm"?"w-10 h-10":r==="md"?"w-12 h-12":r==="lg"?"w-16 h-16":r==="xl"?"w-24 h-24":"w-32 h-32"}`}),d.jsx(W,{className:"absolute inset-0 rounded-full bg-blue-500/20 border-2 border-blue-500 border-dashed"})]}):d.jsx(Ur,{avatar:we||void 0,name:t,size:Le}),!m&&d.jsxs(W,{className:"w-[220px] flex flex-col items-center gap-2",children:[Y&&K&&d.jsxs(W,{className:"w-full p-3 bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-lg",children:[d.jsxs(W,{className:"flex items-center gap-2 mb-2",children:[d.jsx(de,{name:"upload",size:"sm",className:"text-blue-600 dark:text-blue-400 shrink-0"}),d.jsx(R,{className:"text-sm font-medium text-blue-800 dark:text-blue-300",children:v||te("mypage.profile.upload_avatar_confirm")})]}),d.jsxs(W,{className:"text-xs text-blue-600 dark:text-blue-400 truncate mb-3",title:K.name,children:[K.name," (",(K.size/1024/1024).toFixed(2),"MB)"]}),d.jsxs(W,{className:"grid grid-cols-2 gap-2",children:[d.jsxs(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50",onClick:Ce,disabled:A,children:[A?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"check",size:"sm"}),d.jsx(R,{children:te("common.confirm")})]}),d.jsx(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:Me,children:d.jsx(R,{children:te("common.cancel")})})]})]}),$&&d.jsxs(W,{className:"w-full p-3 bg-red-50 dark:bg-red-950/50 border border-red-200 dark:border-red-800 rounded-lg",children:[d.jsxs(W,{className:"flex items-center gap-2 mb-2",children:[d.jsx(de,{name:"alert-triangle",size:"sm",className:"text-red-600 dark:text-red-400 shrink-0"}),d.jsx(R,{className:"text-sm font-medium text-red-800 dark:text-red-300",children:C||te("mypage.profile.delete_avatar_confirm")})]}),d.jsxs(W,{className:"grid grid-cols-2 gap-2",children:[d.jsxs(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md text-sm font-medium transition-colors disabled:opacity-50",onClick:Ge,disabled:D,children:[D?d.jsx(de,{name:"spinner",size:"sm",className:"animate-spin"}):d.jsx(de,{name:"trash-2",size:"sm"}),d.jsx(R,{children:te("common.delete")})]}),d.jsx(Q,{type:"button",className:"inline-flex items-center justify-center gap-1.5 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-md text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors",onClick:ye,children:d.jsx(R,{children:te("common.cancel")})})]})]}),!V&&re()]}),b&&!V&&d.jsxs(W,{className:"flex items-center gap-2 text-sm text-red-600 dark:text-red-400",children:[d.jsx(de,{name:"alert-circle",size:"sm"}),d.jsx(R,{children:b})]})]})}const _0=Object.freeze(Object.defineProperty({__proto__:null,AvatarUploader:Ma,default:Ma},Symbol.toStringTag,{value:"Module"})),oc=()=>window.G7Core,Zn=(e,t)=>window.G7Core?.t?.(e,t)??e,ac={horizontal:"flex items-center gap-1.5",vertical:"flex flex-col items-start"},E0=()=>[{key:"view_profile",label:Zn("userinfo.view_profile"),icon:"user",path:"/users/{{userId}}"},{key:"view_posts",label:Zn("userinfo.view_posts"),icon:"file-lines",path:"/users/{{userId}}/posts"}],Oa=({text:e,title:t})=>e?d.jsx(R,{className:"text-sm text-gray-500 dark:text-gray-400",title:t,children:e}):null,sc=({author:e,name:t,userId:r,subText:n,subTextTitle:o,isGuest:a=!1,isWithdrawn:s=!1,showDropdown:l=!0,clickable:f=!0,profilePath:h="/users/{userId}",className:c="",layout:i="vertical",text:u,stopPropagation:g=!1,menuItems:p,hideMenuItems:m=[],appendMenuItems:x=[],id:C,editorAttrs:O})=>{const v=r??e?.uuid,E=a||e?.is_guest||!1,I=s||e?.status==="withdrawn",_=I?Zn("userinfo.withdrawn_user"):u??t??e?.name??"",[M,w]=k.useState(!1),[y,T]=k.useState({top:0,left:0}),A=k.useRef(null),P=k.useRef(null),D=k.useRef(null),L=h.replace("{userId}",String(v??"")),U=`${ac[i]} ${c}`;k.useEffect(()=>{if(!M)return;const j=q=>{const ue=q.target,te=A.current?.contains(ue),fe=P.current?.contains(ue);!te&&!fe&&w(!1)},H=()=>w(!1);return document.addEventListener("mousedown",j),window.addEventListener("scroll",H,!0),window.addEventListener("resize",H),()=>{document.removeEventListener("mousedown",j),window.removeEventListener("scroll",H,!0),window.removeEventListener("resize",H)}},[M]);const S=k.useCallback(()=>{if(p&&p.length>0)return p.filter(q=>q.show!==!1);let H=E0().filter(q=>!m.includes(q.key)&&q.show!==!1);return x&&x.length>0&&(H=[...H,...x.filter(q=>q.show!==!1)]),H},[p,m,x]),b=k.useCallback(()=>{if(D.current){const j=D.current.getBoundingClientRect();T({top:j.bottom+4,left:j.left})}},[]),F=k.useCallback((j,H)=>{if(H.stopPropagation(),H.preventDefault(),w(!1),j.onClick)j.onClick();else if(j.path){const q=j.path.replace(/\{\{userId\}\}/g,String(v??"")),ue=oc();ue?.navigate?ue.navigate(q):window.location.href=q}},[v]),$=k.useCallback(j=>{j.preventDefault(),g&&j.stopPropagation(),!(E||!v)&&(l?(M||b(),w(H=>!H)):f&&oc()?.navigate?.(L))},[g,E,v,l,M,b,f,L]);if(E||!v)return d.jsxs(W,{className:U,id:C,...O,children:[d.jsxs(W,{className:"flex items-center gap-1.5 text-gray-400 dark:text-gray-500",children:[d.jsx(R,{children:_}),d.jsx(R,{className:"text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded",children:Zn("userinfo.guest_badge")})]}),d.jsx(Oa,{text:n,title:o})]});if(I)return d.jsxs(W,{className:U,id:C,...O,children:[d.jsx(R,{className:"text-gray-400 dark:text-gray-500 line-through",children:_}),d.jsx(Oa,{text:n,title:o})]});const G=S(),B=/\btext-\S+/.test(c)?"":"text-gray-900 dark:text-white",K=l||f?`${B} hover:text-blue-600 dark:hover:text-blue-400 cursor-pointer font-medium`.trim():`${B} font-medium`.trim();return d.jsxs(W,{ref:A,className:`relative inline-block ${c}`,id:C,...O,children:[d.jsxs(W,{className:ac[i],children:[d.jsx(Q,{ref:D,onClick:$,className:K,children:_}),d.jsx(Oa,{text:n,title:o})]}),l&&M&&G.length>0&&Jt.createPortal(d.jsx(W,{ref:P,className:"fixed w-40 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg py-1 z-[9999]",style:{top:y.top,left:y.left},"data-testid":"author-dropdown-menu",children:G.map(j=>d.jsxs(Q,{onClick:H=>F(j,H),className:"flex items-center justify-start gap-2 w-full text-left px-3 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer","data-testid":`menu-item-${j.key}`,children:[j.icon&&d.jsx(de,{name:j.icon,className:"w-4 h-4"}),d.jsx(R,{children:j.label})]},j.key))}),document.body)]})},T0=Object.freeze(Object.defineProperty({__proto__:null,UserInfo:sc},Symbol.toStringTag,{value:"Module"})),lc=({tabs:e,activeTabId:t,onTabChange:r,variant:n="underline",className:o="",style:a,hiddenTabIds:s=[],mobileBreakpoint:l=768,id:f,editorAttrs:h})=>{const i=window.G7Core?.useResponsive,u=i?.(),g=u?u.width<l:typeof window<"u"&&window.innerWidth<l,p=v=>{!v.disabled&&v.id!==t&&r?.(v.id)},m=v=>{const E=v.target.value,I=Number(E),_=typeof E=="string"&&E!==""&&!Number.isNaN(I)?I:E,M=e.find(w=>String(w.id)===String(_));M&&p(M)},x=s.length>0?e.filter(v=>!s.includes(v.id)):e;if(g)return d.jsx(W,{className:`relative ${o}`,style:a,id:f,...h,children:d.jsx(Xa,{value:t!==void 0?String(t):"",onChange:m,options:x.map(v=>({value:String(v.id),label:v.badge!==void 0?`${v.label} (${v.badge})`:v.label,disabled:v.disabled})),className:"w-full"})});const C=v=>{const E=v.id===t,I="flex items-center gap-2 px-3 py-2 font-medium text-sm transition-all shrink-0 whitespace-nowrap";if(v.disabled)return`${I} opacity-50 cursor-not-allowed text-gray-400 dark:text-gray-600`;switch(n){case"pills":return E?`${I} bg-blue-600 dark:bg-blue-500 text-white rounded-lg`:`${I} text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg`;case"underline":return E?`${I} text-gray-900 dark:text-white border-b-2 border-gray-900 dark:border-white`:`${I} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600`;default:return E?`${I} text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-900/20 border-b-2 border-blue-600 dark:border-blue-400`:`${I} text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 border-b-2 border-transparent`}},O=n==="underline"?"flex gap-0 border-b border-gray-200 dark:border-gray-700":"flex gap-2";return d.jsx(W,{className:`relative ${o}`,style:a,id:f,...h,children:d.jsx(Fr,{className:O,children:x.map(v=>d.jsxs(Q,{type:"button",onClick:()=>p(v),disabled:v.disabled,className:C(v),children:[v.iconName&&d.jsx(de,{name:v.iconName,size:"sm"}),d.jsx(R,{children:v.label}),v.badge!==void 0&&d.jsx(W,{className:"flex items-center justify-center min-w-5 h-5 px-1.5 bg-red-500 text-white text-xs font-bold rounded-full",children:d.jsx(R,{children:v.badge})})]},v.id))})})},N0=Object.freeze(Object.defineProperty({__proto__:null,TabNavigation:lc},Symbol.toStringTag,{value:"Module"})),A0={Header:()=>Promise.resolve().then(()=>sd),Footer:()=>Promise.resolve().then(()=>dd),MobileNav:()=>Promise.resolve().then(()=>ud),SlotContainer:()=>Promise.resolve().then(()=>od),NotificationCenter:()=>Promise.resolve().then(()=>ad),ProductCard:()=>Promise.resolve().then(()=>$d),BrandMark:()=>Promise.resolve().then(()=>Ud),ImageGallery:()=>Promise.resolve().then(()=>Al),ProductImageViewer:()=>Promise.resolve().then(()=>Wf),QuantitySelector:()=>Promise.resolve().then(()=>Vf),PostReactions:()=>Promise.resolve().then(()=>Xf),RichTextEditor:()=>Promise.resolve().then(()=>qf),HtmlContent:()=>Promise.resolve().then(()=>Zf),HtmlEditor:()=>Promise.resolve().then(()=>Qf),ExpandableContent:()=>Promise.resolve().then(()=>Jf),FileUploader:()=>Promise.resolve().then(()=>Zh),ConfirmDialog:()=>Promise.resolve().then(()=>tg),SocialLoginButtons:()=>Promise.resolve().then(()=>Jh),Toast:()=>Promise.resolve().then(()=>n0),PageTransitionIndicator:()=>Promise.resolve().then(()=>a0),PageTransitionBlur:()=>Promise.resolve().then(()=>s0),PageSkeleton:()=>Promise.resolve().then(()=>v0),PageLoading:()=>Promise.resolve().then(()=>x0),ThemeToggle:()=>Promise.resolve().then(()=>Rc),Pagination:()=>Promise.resolve().then(()=>k0),SearchBar:()=>Promise.resolve().then(()=>C0),Avatar:()=>Promise.resolve().then(()=>rd),AvatarUploader:()=>Promise.resolve().then(()=>_0),UserInfo:()=>Promise.resolve().then(()=>T0),Modal:()=>Promise.resolve().then(()=>eg),TabNavigation:()=>Promise.resolve().then(()=>N0)},M0=({id:e,className:t,style:r,children:n,editorAttrs:o})=>d.jsx(W,{id:e,className:t,style:r,...o,children:n}),O0=({id:e,children:t,cols:r=1,responsive:n,gap:o,rowGap:a,colGap:s,autoRows:l,autoCols:f,flow:h="row",className:c="",style:i,onClick:u,editorAttrs:g})=>{const p=["grid"];if(r&&p.push(`grid-cols-${r}`),n&&(n.sm&&p.push(`sm:grid-cols-${n.sm}`),n.md&&p.push(`md:grid-cols-${n.md}`),n.lg&&p.push(`lg:grid-cols-${n.lg}`),n.xl&&p.push(`xl:grid-cols-${n.xl}`),n["2xl"]&&p.push(`2xl:grid-cols-${n["2xl"]}`)),o!==void 0&&o>0&&p.push(`gap-${o}`),a!==void 0&&a>0&&p.push(`gap-y-${a}`),s!==void 0&&s>0&&p.push(`gap-x-${s}`),l){const x={auto:"auto-rows-auto",min:"auto-rows-min",max:"auto-rows-max",fr:"auto-rows-fr"};p.push(x[l])}if(f){const x={auto:"auto-cols-auto",min:"auto-cols-min",max:"auto-cols-max",fr:"auto-cols-fr"};p.push(x[f])}const m={row:"grid-flow-row",col:"grid-flow-col",dense:"grid-flow-dense","row-dense":"grid-flow-row-dense","col-dense":"grid-flow-col-dense"};return p.push(m[h]),c&&p.push(c),d.jsx(W,{id:e,className:p.join(" "),style:i,onClick:u,...g,children:t})},I0=({id:e,children:t,direction:r="row",justify:n="start",align:o="stretch",wrap:a="nowrap",gap:s=0,grow:l=!1,shrink:f=!0,className:h="",style:c,onClick:i,editorAttrs:u})=>{const g=["flex"],p={row:"flex-row","row-reverse":"flex-row-reverse",col:"flex-col","col-reverse":"flex-col-reverse"};g.push(p[r]);const m={wrap:"flex-wrap",nowrap:"flex-nowrap","wrap-reverse":"flex-wrap-reverse"};g.push(m[a]);const x={start:"justify-start",end:"justify-end",center:"justify-center",between:"justify-between",around:"justify-around",evenly:"justify-evenly"};g.push(x[n]);const C={start:"items-start",end:"items-end",center:"items-center",baseline:"items-baseline",stretch:"items-stretch"};return g.push(C[o]),s>0&&g.push(`gap-${s}`),l===!0?g.push("flex-grow"):typeof l=="number"&&l>0&&g.push(`flex-grow-${l}`),f===!1?g.push("shrink-0"):f===!0?g.push("flex-shrink"):typeof f=="number"&&f>0&&g.push(`flex-shrink-${f}`),h&&g.push(h),d.jsx(W,{id:e,className:g.join(" "),style:c,onClick:i,...u,children:t})},D0=({id:e,title:t,subtitle:r,children:n,padding:o="md",background:a="none",maxWidth:s,centered:l=!1,border:f=!1,shadow:h="none",rounded:c=!1,className:i="",style:u,onClick:g,editorAttrs:p})=>{const m=[],x={none:"p-0",sm:"p-2",md:"p-4",lg:"p-6",xl:"p-8"};m.push(x[o]);const C={none:"",white:"bg-white dark:bg-gray-700",gray:"bg-gray-50 dark:bg-gray-800",primary:"bg-blue-50 dark:bg-blue-900/20",secondary:"bg-gray-50 dark:bg-gray-800"};if(a!=="none"&&m.push(C[a]),s){const v={sm:"max-w-sm",md:"max-w-md",lg:"max-w-lg",xl:"max-w-xl","2xl":"max-w-2xl","4xl":"max-w-4xl","6xl":"max-w-6xl","7xl":"max-w-7xl",full:"max-w-full"};m.push(v[s])}l&&m.push("mx-auto"),f&&m.push("border","border-gray-200","dark:border-gray-700");const O={none:"",sm:"shadow-sm",md:"shadow-md",lg:"shadow-lg",xl:"shadow-xl"};return h!=="none"&&m.push(O[h]),c&&m.push("rounded-lg"),i&&m.push(i),d.jsxs(Za,{id:e,className:m.join(" "),style:u,onClick:g,...p,children:[(t||r)&&d.jsxs(W,{className:"mb-4",children:[t&&d.jsx(Ka,{className:"text-2xl font-bold text-gray-900 dark:text-white",children:t}),r&&d.jsx(Dt,{className:"mt-1 text-sm text-gray-600 dark:text-gray-400",children:r})]}),n]})},L0=({id:e,leftWidth:t="250px",rightWidth:r="300px",leftSlot:n,centerSlot:o,rightSlot:a,className:s="",style:l,editorAttrs:f})=>{const h=`flex flex-row w-full h-full ${s}`.trim(),c={width:t,flexShrink:0},i={flex:1,minWidth:0},u={width:r,flexShrink:0};return d.jsxs(W,{id:e,className:h,style:l,...f,children:[d.jsx(W,{className:"flex flex-col",style:c,children:n}),d.jsx(W,{className:"flex flex-col",style:i,children:o}),d.jsx(W,{className:"flex flex-col",style:u,children:a})]})},P0={identifier:"sirsoft-basic",vendor:"sirsoft",name:{ko:"Basic",en:"Basic"},version:"1.1.1",license:"MIT",description:{ko:"그누보드7 기본 사용자 템플릿",en:"Gnuboard7 default user template"},type:"user",locales:["ko","en"],author:{name:"sirsoft",email:"contact@sirsoft.com",url:"https://sirsoft.com"},release_date:"2026-01-07",g7_version:">=7.0.6",dependencies:{modules:{"sirsoft-board":">=1.0.0","sirsoft-ecommerce":">=1.1.0","sirsoft-page":">=1.0.0"},plugins:{"sirsoft-daum_postcode":">=1.0.0"}},assets:{css:["dist/css/components.css"],js:["dist/js/components.iife.js"],fonts:[],images:[]},components:{basic:["a","button","checkbox","div","form","h1","h2","h3","h4","hr","icon","img","input","label","li","nav","p","section","select","span","svg","table","tbody","td","textarea","th","thead","tr","ul"],composite:["header","footer","mobile-nav","product-card","rich-text-editor","image-gallery","quantity-selector","post-reactions","social-login-buttons","file-uploader","card","modal","toast","pagination","stat-card","status-badge","tab-navigation","empty-state","loading-spinner","breadcrumb","search-bar","dropdown"],layout:["container","flex","grid","section"]},preview:{thumbnail:"preview/thumbnail.png",screenshots:["preview/screenshot1.png","preview/screenshot2.png"]},error_config:{layouts:{401:"errors/401",403:"errors/403",404:"errors/404",500:"errors/500",503:"errors/503",maintenance:"errors/maintenance"}},features:{dark_mode:!0,responsive:!0,multi_language:!0,multi_currency:!0},github_url:"https://github.com/gnuboard/g7-template-sirsoft-basic",github_changelog_url:"https://github.com/gnuboard/g7-template-sirsoft-basic/blob/main/CHANGELOG.md",externals:[{id:"fontawesome",type:"style",url:"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css",preconnect:"https://cdnjs.cloudflare.com"},{id:"pretendard",type:"webfont",url:"https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/static/pretendard.min.css",preconnect:"https://cdn.jsdelivr.net",crossorigin:"anonymous"}]},$0={KRW:0,JPY:0};function F0(){try{const t=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(t?.currencies)?t.currencies:[]}catch{return[]}}function Lr(e,t){if(!Number.isFinite(e))return"0";const r=F0(),n=t||r.find(a=>a.is_default)?.code;if(!n)return e.toLocaleString();const o=r.find(a=>a.code===n)?.decimal_places??$0[n]??2;try{return new Intl.NumberFormat(void 0,{style:"currency",currency:n,minimumFractionDigits:o,maximumFractionDigits:o}).format(e)}catch{return`${e.toLocaleString(void 0,{minimumFractionDigits:o,maximumFractionDigits:o})} ${n}`}}function Qn(e,t,r,n=0){if(!e)return;const o=f=>f?.value??f?.price??0;let a;for(const[f,h]of Object.entries(e))if(h?.is_default){a=f;break}a=a??(e.KRW?"KRW":void 0);const s=a?o(e[a]):0,l={};for(const[f,h]of Object.entries(e)){const c=o(h);let i;r?i=r[f]?.value??0:n>0&&f===a?i=n:n>0&&s>0?i=n*c/s:i=0;const u=(c+i)*t;l[f]={value:u,formatted:Lr(u,f)}}return l}function Jn(e){const t={};for(const n of e){const o=n?.multiCurrencyTotalPrice;if(o)for(const[a,s]of Object.entries(o)){const l=s?.value??s?.price??0;t[a]=(t[a]??0)+l}}const r={};for(const[n,o]of Object.entries(t))r[n]={value:o,formatted:Lr(o,n)};return r}function ic(e,t){if(!e||!t?.length)return 0;let r=0;for(const n of t){const o=e[n.id];if(o==null)continue;const a=n.values?.find(s=>s.id===Number(o));a&&(r+=a.price_adjustment??0)}return r}function cc(e,t){if(!e||!t?.length)return;const r={};let n=!1;for(const a of t){const s=e[a.id];if(s==null)continue;const f=a.values?.find(h=>h.id===Number(s))?.multi_currency_price_adjustment;if(f){n=!0;for(const[h,c]of Object.entries(f)){const i=c?.value??c?.price??0;r[h]=(r[h]??0)+i}}}if(!n)return;const o={};for(const[a,s]of Object.entries(r))o[a]={value:s};return o}function Rn(e){return e.name_localized?e.name_localized:typeof e.name=="string"?e.name:e.name?.ko??""}function U0(e,t,r){if(t&&r in t)return t[r];if(Array.isArray(e)){const n=e.find(o=>typeof o.key=="string"?o.key===r:o.key?.ko===r||Object.values(o.key).includes(r));return n?typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]:void 0}return e[r]}function z0(e,t){if(t)return t;if(!Array.isArray(e))return e;const r={};for(const n of e){const o=typeof n.key=="string"?n.key:n.key?.ko??Object.values(n.key)[0]??"",a=typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]??"";o&&(r[o]=a)}return r}function H0(e,t,r){return r.find(n=>!n?.option_values||!n?.is_active?!1:e.every(o=>{const a=Rn(o),s=t?.[a];return U0(n.option_values,n.option_values_localized,a)===s}))}function G0(e,t){const r=e.params;if(!r)return;const{optionGroups:n,options:o,selectedOptionItems:a,preferredCurrency:s}=r;let l;if(r.newGroupName&&r.newValue?l={...r.currentSelection??{},[r.newGroupName]:r.newValue}:l=r.currentSelection??{},!n?.every(_=>{const M=Rn(_);return l?.[M]}))return;const h=H0(n,l,o??[]);if(!h){t.setState?.({currentSelection:{},__mergeMode:"shallow"});return}const c=n.map(_=>l[Rn(_)]).join("_"),i=a??[];if(i.find(_=>_.id===c)){const _=window.G7Core;_?.toast?.warning?.(_?.t?.("sirsoft-ecommerce.shop.already_added_option")??"이미 추가된 옵션입니다."),t.setState?.({currentSelection:{},__mergeMode:"shallow"});return}const g=h.selling_price??0,p={};n.forEach(_=>{const M=Rn(_);p[M]=l[M]});const m=r.additionalOptionGroups??[],x={};for(const _ of m){if(!_.is_required)continue;const M=_.values?.find(w=>w.is_default);M&&(x[_.id]=M.id)}const C=ic(x,m),O=cc(x,m),v=g+C,E={id:c,optionId:h.id,options:p,optionValues:z0(h.option_values,h.option_values_localized),quantity:1,stock:h.stock_quantity??999,unitPrice:g,unitPriceFormatted:Lr(g,s),totalPrice:v,totalPriceFormatted:Lr(v,s),multiCurrencyUnitPrice:h.multi_currency_selling_price,multiCurrencyTotalPrice:Qn(h.multi_currency_selling_price,1,O,C),additionalOptionSelections:x,additionalOptionsTotal:C,additionalOptionsMultiCurrencyTotal:O},I=[...i,E];t.setState?.({selectedOptionItems:I,selectedTotalMultiCurrency:Jn(I),currentSelection:{},__mergeMode:"shallow"})}function j0(e,t){const r=e.params;if(!r)return;const{selectedOptionItems:n,preferredCurrency:o}=r,a=Number(r.itemIndex)||0,s=Number(r.newQuantity)||1,l=(n??[]).map((f,h)=>{if(h!==a)return f;const c=f?.unitPrice??0,i=f?.additionalOptionsTotal??0,u=(c+i)*s;return{...f,quantity:s,totalPrice:u,totalPriceFormatted:Lr(u,o),multiCurrencyTotalPrice:Qn(f?.multiCurrencyUnitPrice,s,f?.additionalOptionsMultiCurrencyTotal,i)}});t.setState?.({selectedOptionItems:l,selectedTotalMultiCurrency:Jn(l)})}function B0(e,t){const r=e.params;if(!r)return;const{selectedOptionItems:n,additionalOptionGroups:o,preferredCurrency:a}=r,s=Number(r.itemIndex)||0,l=Number(r.additionalOptionId),f=Object.prototype.hasOwnProperty.call(r,"customText"),h=r.valueId===""||r.valueId==null?null:Number(r.valueId),c=(n??[]).map((i,u)=>{if(u!==s)return i;const g={...i.additionalOptionSelections??{}},p={...i.additionalOptionCustomTexts??{}};if(f){const E=String(r.customText??"");E.trim()===""?delete p[l]:p[l]=E}else h==null?(delete g[l],delete p[l]):(g[l]=h,(o??[]).find(_=>_.id===l)?.values?.find(_=>_.id===h)?.allow_custom_text||delete p[l]);const m=ic(g,o),x=cc(g,o),C=i?.unitPrice??0,O=i?.quantity??1,v=(C+m)*O;return{...i,additionalOptionSelections:g,additionalOptionCustomTexts:p,additionalOptionsTotal:m,additionalOptionsMultiCurrencyTotal:x,totalPrice:v,totalPriceFormatted:Lr(v,a),multiCurrencyTotalPrice:Qn(i?.multiCurrencyUnitPrice,O,x,m)}});t.setState?.({selectedOptionItems:c,selectedTotalMultiCurrency:Jn(c)})}function W0(e,t){const r=e.params;if(!r)return;const n=Number(r.itemIndex)||0,o=(r.selectedOptionItems??[]).filter((a,s)=>s!==n);t.setState?.({selectedOptionItems:o,selectedTotalMultiCurrency:Jn(o)})}function V0(e,t){const r=e.params;if(!r)return;const n=Math.max(1,Number(r.newQuantity)||1);t.setState?.({noOptionQuantity:n,noOptionTotalMultiCurrency:Qn(r.multiCurrencyUnitPrice,n)})}const cn=window.G7Core?.createLogger?.("Handler:Cart")??{log:(...e)=>console.log("[Handler:Cart]",...e),warn:(...e)=>console.warn("[Handler:Cart]",...e),error:(...e)=>console.error("[Handler:Cart]",...e)};function hr(e){const t=window.G7Core;t?.state?.setLocal?(t.state.setLocal(e),cn.log("Local state updated:",e)):cn.warn("G7Core.state.setLocal not available")}function dc(){return window.G7Core?.state?.getLocal?.()||{}}function K0(){return window.G7Core?.state?.get?.()||{}}function Y0(e,t="info"){const r=window.G7Core;r?.toast?.show?r.toast.show(e,t):cn.warn("G7Core.toast.show not available, message:",e)}function X0(e){const t=window.G7Core;return t?.t?t.t(e):e}function uc(){const e=window.G7Core;if(e?.dispatch){const r=dc().selectedItems||[];e.dispatch({handler:"refetchDataSource",params:{dataSourceId:"cartItems"}}),cn.log("Refetching cartItems with selected_ids:",r)}else cn.warn("G7Core.dispatch not available")}function q0(e,t){const{itemId:r,selectedItems:n}=e?.params||{},o=n||[];let a;o.includes(r)?a=o.filter(h=>h!==r):a=[...o,r];const l=K0().cartItems?.data?.items||[],f=l.length>0&&a.length===l.length;hr({selectedItems:a,allSelected:f}),uc()}function Z0(e,t){const{allItemIds:r,isSelected:n}=e?.params||{};hr({selectedItems:n?r:[],allSelected:n}),uc()}function Q0(e,t){const{optionName:r,optionValue:n}=e?.params||{},a=dc().selectedOptions||{};hr({selectedOptions:{...a,[r]:n}})}function J0(e,t){const{targetIds:r}=e?.params||{};if(!r||r.length===0){Y0(X0("shop.cart.select_items_to_delete"),"warning");return}hr({deleteModal:{visible:!0,targetIds:r}})}function R0(e,t){const{targetItem:r}=e?.params||{};hr({optionModal:{visible:!0,targetItem:r},selectedOptions:r?.options||{},selectedQuantity:r?.quantity})}function ep(e,t){const{selectedItems:r}=e?.params||{};if(!r||r.length===0){hr({isCalculating:!1});return}hr({isCalculating:!0})}const pr=window.G7Core?.createLogger?.("Handler:CartOptionChange")??{log:(...e)=>console.log("[Handler:CartOptionChange]",...e),warn:(...e)=>console.warn("[Handler:CartOptionChange]",...e),error:(...e)=>console.error("[Handler:CartOptionChange]",...e)};function Ia(e){return e.name_localized?e.name_localized:typeof e.name=="string"?e.name:e.name?.ko??""}function tp(e,t,r){if(t&&r in t)return t[r];if(Array.isArray(e)){const n=e.find(o=>typeof o.key=="string"?o.key===r:o.key?.ko===r||Object.values(o.key).includes(r));return n?typeof n.value=="string"?n.value:n.value?.ko??Object.values(n.value)[0]:void 0}return e[r]}function fc(e,t,r){return!e||!t?.length?null:t.every(a=>{const s=Ia(a);return e[s]})?r?.find(a=>t.every(s=>{const l=Ia(s);return tp(a.option_values,a.option_values_localized,l)===e[l]}))||null:(pr.log("Not all options selected yet"),null)}function rp(e,t){const{selectedOptions:r,optionGroups:n,options:o}=e?.params||{};pr.log("findMatchingOption called:",{selectedOptions:r,optionGroups:n,optionsCount:o?.length});const a=fc(r,n,o);return pr.log("Matched option:",a),{matchedOption:a}}function np(e,t){const{currentOptionValues:r,optionGroups:n,options:o}=e?.params||{};if(pr.log("initCartOptionSelection called:",{currentOptionValues:r,optionGroups:n,optionsCount:o?.length}),!n?.length||!r)return pr.log("No option groups or current values, returning empty"),{selectedOptions:{},matchedOption:null};const a={};for(const l of n){const f=Ia(l),h=r[f],c=l.values_localized??(Array.isArray(l.values)?l.values.map(i=>typeof i=="string"?i:i?.ko??""):[]);h&&c.includes(h)&&(a[f]=h)}pr.log("Initial selectedOptions:",a);const s=fc(a,n,o||[]);return pr.log("Matched option:",s),{selectedOptions:a,matchedOption:s}}const dn="g7_cart_key",op="/api/modules/sirsoft-ecommerce/cart/key",Da="g7_guest_order_token",gc="g7_guest_order_number",La="g7_guest_order_expires_at",at=window.G7Core?.createLogger?.("Handler:Storage")??{log:(...e)=>console.log("[Handler:Storage]",...e),warn:(...e)=>console.warn("[Handler:Storage]",...e),error:(...e)=>console.error("[Handler:Storage]",...e)};async function hc(){try{const e=await fetch(op,{method:"POST",headers:{"Content-Type":"application/json",Accept:"application/json"}});return e.ok?(await e.json()).data?.cart_key||null:(at.error("Failed to issue cartKey from API:",e.status),null)}catch(e){return at.error("Error issuing cartKey from API:",e),null}}function rr(e){const t=window.G7Core;t?.state?.set?(t.state.set(e),at.log("Global state updated:",e)):at.warn("G7Core.state.set not available")}function ap(){try{const e=window.G7Config;return e?.user?e.user:(window.G7Core?.state?.get?.()||{}).currentUser||null}catch{return null}}async function sp(e,t){let r=null;try{r=localStorage.getItem(dn)}catch{}if(r)at.log("Existing cartKey loaded:",r);else if(r=await hc(),r)try{localStorage.setItem(dn,r),at.log("New cartKey issued from API:",r)}catch{}else at.error("Failed to issue cartKey from API");rr({cartKey:r})}function lp(e,t){try{return localStorage.getItem(dn)}catch{return null}}function pc(e,t){try{localStorage.removeItem(dn),at.log("cartKey removed from localStorage")}catch{}rr({cartKey:null})}async function ip(e,t){if(ap()){pc();return}const n=await hc();if(n){try{localStorage.setItem(dn,n),at.log("New cartKey regenerated from API:",n)}catch{}rr({cartKey:n})}else at.error("Failed to regenerate cartKey from API")}function cp(e,t){const{key:r,value:n}=e?.params||{};if(!r){at.warn("saveToStorage: key is required");return}try{localStorage.setItem(r,n),at.log("Saved to storage:",r,n)}catch{}}function dp(e,t){const{key:r,defaultValue:n}=e?.params||{};if(!r)return at.warn("loadFromStorage: key is required"),n||null;try{return localStorage.getItem(r)||n||null}catch{return n||null}}function Pa(){try{sessionStorage.removeItem(Da),sessionStorage.removeItem(gc),sessionStorage.removeItem(La)}catch{}}function up(e,t){let r=null,n=null;try{r=sessionStorage.getItem(Da),n=sessionStorage.getItem(La)}catch{}let o=!1;if(r){const a=n?new Date(n).getTime():NaN;(isNaN(a)||a<=Date.now())&&(Pa(),r=null,o=!0)}if(r){rr({guestOrderToken:r}),at.log("Guest order token loaded from sessionStorage");return}if(!o){const s=window.G7Core?.state?.get?.("_global")?.guestOrderToken;if(typeof s=="string"&&s!==""){at.log("Guest order token preserved from in-memory _global (sessionStorage empty)");return}}rr({guestOrderToken:null}),at.log("No guest order token (visitor not authenticated)")}function fp(e,t){const{token:r,orderNumber:n,expiresAt:o}=e?.params||{};if(!r||!n||!o){at.warn("saveGuestOrderToken: token, orderNumber, expiresAt are required");return}try{sessionStorage.setItem(Da,r),sessionStorage.setItem(gc,n),sessionStorage.setItem(La,o)}catch{}rr({guestOrderToken:r})}function gp(e,t){Pa(),rr({guestOrderToken:null})}function hp(e,t){Pa(),rr({guestOrderToken:null})}function pp(e,t){const{product:r,priceField:n,currencyCode:o}=e,a=o||t.getState("_global.preferredCurrency")||t.getState("_global.defaultCurrency")||"",s=`multi_currency_${n}`,l=r[s];if(l&&l[a])return l[a].formatted;const f=`${n}_formatted`;return r[f]||String(r[n])}const mc={KRW:"₩",USD:"$",JPY:"¥",CNY:"元",EUR:"€",GBP:"£"},mp={KRW:0,JPY:0};function bc(){try{const t=(window.G7Core?.state?.get?.()||{})?.modules?.["sirsoft-ecommerce"]?.language_currency;return Array.isArray(t?.currencies)?t.currencies:[]}catch{return[]}}function bp(e,t){const{value:r,currencyCode:n,locale:o}=e,a=bc(),s=n||t.getState("_global.preferredCurrency")||t.getState("_global.defaultCurrency")||a.find(i=>i.is_default)?.code;if(!s)return r.toLocaleString(o);const l=a.find(i=>i.code===s),f=l?.symbol&&l.symbol.length>0?l.symbol:mc[s]??"",h=l?.decimal_places??mp[s]??2,c=r.toLocaleString(o,{minimumFractionDigits:h,maximumFractionDigits:h});return f?s==="KRW"||f==="₩"||f==="원"?`${c}원`:`${f}${c}`:`${c} ${s}`}function yp(e){const t=bc().find(r=>r.code===e);return t?.symbol&&t.symbol.length>0?t.symbol:mc[e]||e}const $a=window.G7Core?.createLogger?.("Handler:Currency")??{log:(...e)=>console.log("[Handler:Currency]",...e),warn:(...e)=>console.warn("[Handler:Currency]",...e),error:(...e)=>console.error("[Handler:Currency]",...e)},yc="g7_preferred_currency";function vp(){try{const e=window.G7Core?.state?.get?.()||{};return e.defaultCurrency||e?.modules?.["sirsoft-ecommerce"]?.language_currency?.default_currency||""}catch{return""}}function wp(e,t){const{defaultCurrency:r=vp()}=e;let n=r;try{if(typeof window<"u"&&window.localStorage){const o=localStorage.getItem(yc);o&&vc(o)&&(n=o)}}catch{$a.warn("localStorage 접근 불가, 기본 통화 사용:",r)}return t.setState("global",{preferredCurrency:n}),n}function xp(e,t){const{currencyCode:r}=e;if(!vc(r)){$a.warn("유효하지 않은 통화 코드:",r);return}try{typeof window<"u"&&window.localStorage&&localStorage.setItem(yc,r)}catch{$a.warn("localStorage 저장 실패")}t.setState("global",{preferredCurrency:r})}function vc(e){return typeof e=="string"&&/^[A-Z]{3}$/.test(e)}const Pr=window.G7Core?.createLogger?.("Handler:SetTheme")??{log:(...e)=>console.log("[Handler:SetTheme]",...e),warn:(...e)=>console.warn("[Handler:SetTheme]",...e),error:(...e)=>console.error("[Handler:SetTheme]",...e)},Fa=["auto","light","dark"],wc="g7_color_scheme",kp=e=>e==="auto"?window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light":e,eo=e=>{const t=kp(e);document.documentElement.setAttribute("data-theme",t),t==="dark"?document.documentElement.classList.add("dark"):document.documentElement.classList.remove("dark")};function Cp(){try{const e=localStorage.getItem(wc),t=e&&Fa.includes(e)?e:"auto";eo(t),Pr.log("Initial theme applied:",t)}catch(e){Pr.warn("Failed to load initial theme:",e),eo("auto")}}async function Sp(e,t){const r=e?.target;if(r&&typeof r=="string"&&Fa.includes(r)){eo(r);return}Cp()}async function _p(e,t){let r;if(e?.target&&typeof e.target=="string"&&!e.target.includes("{{")&&(r=e.target),!r||typeof r!="string"){Pr.warn("Invalid theme:",r);return}if(!Fa.includes(r)){Pr.warn("Unsupported theme:",r);return}const n=r;try{localStorage.setItem(wc,n)}catch(o){Pr.error("Failed to save theme to localStorage:",o);return}eo(n),Pr.log("Theme changed to:",n)}const Ep=window.G7Core?.createLogger?.("Handler:RedirectToLoginWithReturn")??{log:(...e)=>console.log("[Handler:RedirectToLoginWithReturn]",...e),warn:(...e)=>console.warn("[Handler:RedirectToLoginWithReturn]",...e),error:(...e)=>console.error("[Handler:RedirectToLoginWithReturn]",...e)};async function Tp(e,t){const r=window.G7Core;if(!r?.dispatch){Ep.error("G7Core.dispatch 가 초기화되지 않아 로그인 리다이렉트를 실행할 수 없습니다.");return}const n=window.location.pathname+window.location.search;await r.dispatch({handler:"navigate",params:{path:"/login",query:{redirect:n}}})}const Ua=window.G7Core?.createLogger?.("Handler:DownloadAttachment")??{log:(...e)=>console.log("[Handler:DownloadAttachment]",...e),warn:(...e)=>console.warn("[Handler:DownloadAttachment]",...e),error:(...e)=>console.error("[Handler:DownloadAttachment]",...e)};async function Np(e,t){const r=window.G7Core,{url:n,filename:o}=e?.params||{};if(!n){Ua.warn("다운로드 URL 이 없어 요청을 건너뜁니다.");return}if(!r?.api?.get){Ua.error("G7Core.api.get 이 초기화되지 않아 다운로드를 실행할 수 없습니다.");return}try{const a=await r.api.get(n,{responseType:"blob"});if(a){const s=URL.createObjectURL(a),l=document.createElement("a");l.href=s,l.download=o||"",document.body.appendChild(l),l.click(),document.body.removeChild(l),URL.revokeObjectURL(s)}}catch(a){Ua.error("첨부파일 다운로드 실패",a),r?.toast?.error?.(r?.t?.("common.download_failed")??"Download failed")}}const to={"sirsoft-basic.addSelectedItemIfComplete":G0,"sirsoft-basic.updateSelectedItemQuantity":j0,"sirsoft-basic.removeSelectedItem":W0,"sirsoft-basic.updateNoOptionQuantity":V0,"sirsoft-basic.setBlockAdditionalOption":B0,"sirsoft-basic.getDisplayPrice":pp,"sirsoft-basic.formatCurrency":bp,"sirsoft-basic.getCurrencySymbol":yp,"sirsoft-basic.loadPreferredCurrency":wp,"sirsoft-basic.savePreferredCurrency":xp,setTheme:_p,initTheme:Sp,redirectToLoginWithReturn:Tp,downloadAttachment:Np,toggleCartItemSelection:q0,selectAllCartItems:Z0,setCartOption:Q0,openCartDeleteModal:J0,openCartOptionModal:R0,recalculateCart:ep,findMatchingOption:rp,initCartOptionSelection:np,initCartKey:sp,getCartKey:lp,clearCartKey:pc,regenerateCartKey:ip,saveToStorage:cp,loadFromStorage:dp,initGuestOrderToken:up,saveGuestOrderToken:fp,clearGuestOrderToken:gp,clearGuestTokenOnEntry:hp},un=window.G7Core?.createLogger?.("Template:sirsoft-basic:IdentityLauncher")??{log:(...e)=>console.log("[Template:sirsoft-basic:IdentityLauncher]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:IdentityLauncher]",...e),error:(...e)=>console.error("[Template:sirsoft-basic:IdentityLauncher]",...e)};function Ap(e){const t=window.G7Core,r=t?.state?.getLocal?.()??{},n=t?.state?.getGlobal?.()??t?.state?.get?.()??{},o=(...f)=>f.find(h=>typeof h=="string"&&h.length>0),a=o(e.target?.email,n?.currentUser?.email,n?.user?.email,n?.auth?.user?.email,e.purpose==="signup"?r?.registerForm?.email:void 0,e.purpose==="password_reset"?r?.passwordResetForm?.email:void 0,e.purpose==="password_reset"?r?.email:void 0),s=o(e.target?.phone,n?.currentUser?.phone,n?.user?.phone,n?.auth?.user?.phone);if(!a&&!s)return null;const l={};return a&&(l.email=a),s&&(l.phone=s),l}function Mp(){const t=window.G7Core?.api?.getToken?.()??null;return t?{Authorization:`Bearer ${t}`}:{}}function Op(){try{const e=typeof window<"u"?window.localStorage?.getItem("g7_locale"):null;return e?{"Accept-Language":e}:{}}catch{return{}}}async function Ip(e,t){const r={purpose:e.purpose};e.provider_id&&(r.provider_id=e.provider_id),t&&(r.target=t);const n=e.challenge_start_url||"/api/identity/challenges",o=await fetch(n,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",Accept:"application/json",...Op(),...Mp()},body:JSON.stringify(r)}),a=await o.json().catch(()=>null);if(!o.ok||!a?.success){const l=a?.message??`Challenge 시작 실패 (HTTP ${o.status})`;throw new Error(l)}const s=a.data??a;return{id:s.id,expires_at:s.expires_at,render_hint:s.render_hint??e.render_hint??"text_code",public_payload:s.public_payload,redirect_url:s.redirect_url,max_attempts:typeof s.max_attempts=="number"?s.max_attempts:void 0}}async function Dp(e){const t=window.G7Core,r=t?.identity;if(!t?.dispatch||!t?.state||!r)return un.error("G7Core 또는 G7Core.identity 가 초기화되지 않아 launcher 를 실행할 수 없습니다."),{status:"failed",failureCode:"G7_NOT_READY"};if(e.render_hint==="external_redirect"||e.redirect_url)return r.redirectExternally(e);const n=Ap(e);let o;try{o=await Ip(e,n)}catch(p){un.error("Challenge 시작 실패:",p);try{await t.dispatch({handler:"toast",params:{type:"error",message:p instanceof Error?p.message:"본인인증 시작에 실패했습니다."}})}catch{}return{status:"failed",failureCode:"CHALLENGE_START_FAILED",reason:p instanceof Error?p.message:String(p)}}if(o.redirect_url)return r.redirectExternally({...e,render_hint:"external_redirect",redirect_url:o.redirect_url});const a=o.expires_at?new Date(o.expires_at).getTime():0,s=a>0?Math.max(0,Math.floor((a-Date.now())/1e3)):0,l=typeof o.max_attempts=="number"?o.max_attempts:0;t.state.set({identityChallenge:{policy_key:e.policy_key,purpose:e.purpose,provider_id:e.provider_id??null,render_hint:o.render_hint,challenge_id:o.id,expires_at:o.expires_at,public_payload:o.public_payload??{},target:n??null,code:"",error:null,attempts:0,maxAttempts:l,remainingSeconds:s,resendCooldown:0}});let f=a,h=0;const c=window.setInterval(()=>{const p=f>0?Math.max(0,Math.floor((f-Date.now())/1e3)):0;h=Math.max(0,h-1),t.state.set({identityChallenge:{remainingSeconds:p,resendCooldown:h}})},1e3);let i=null;try{i=t.state.subscribe?.(p=>{const m=p?.identityChallenge?.expires_at;if(typeof m=="string"){const C=new Date(m).getTime();Number.isFinite(C)&&C!==f&&(f=C)}const x=p?.identityChallenge?.resendCooldown;typeof x=="number"&&x>h&&(h=x)})??null}catch{}const u=r.createDeferred();try{await t.dispatch({handler:"openModal",target:"identity-challenge-modal"})}catch(p){return un.error("openModal 실패 — 풀페이지로 폴백:",p),window.clearInterval(c),i?.(),r.redirectExternally({...e,render_hint:"external_redirect",redirect_url:`/identity/challenge?challenge_id=${encodeURIComponent(o.id)}&return=${encodeURIComponent(window.location.href)}`})}const g=await u;return window.clearInterval(c),i?.(),g}function Lp(){const e=window.G7Core?.identity;if(!e?.setLauncher){un.warn("G7Core.identity 가 아직 초기화되지 않아 launcher 등록을 건너뜁니다. 코어 부트스트랩 순서를 확인하세요.");return}e.setLauncher(Dp),un.log("IDV launcher registered (sirsoft-basic)")}const xc=window.G7Core?.createLogger?.("Template:sirsoft-basic:device")??{log:(...e)=>console.log("[Template:sirsoft-basic:device]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:device]",...e)};function Pp(){if(typeof navigator>"u")return!1;const e=navigator,t=(e.userAgent||"").toLowerCase(),r=((e.userAgentData?.platform??e.platform)||"").toLowerCase(),n=e.maxTouchPoints??0;return/iphone|ipad|ipod/.test(t)||/iphone|ipad|ipod|ios/.test(r)?!0:/macintosh|mac os x/.test(t)&&n>1}function $p(){const e=window.G7Core;if(!e?.state?.get||!e?.state?.set){xc.warn("G7Core.state not available; skip iOS device correction");return}if(!Pp())return;const r=(e.state.get()||{}).appConfig||{};r.isIos!==!0&&(e.state.set({appConfig:{...r,isIos:!0}}),xc.log("appConfig.isIos corrected to true by client signals (iPadOS desktop-UA)"))}function Fp(e){const t=e.icons;return Array.isArray(t)?t.filter(r=>!!r&&typeof r=="object"&&typeof r.value=="string"):[]}function Up(e,t){const r=e.label;return typeof r=="string"?r.startsWith("$t:")?t(r.slice(3)):r:e.value}function zp(e,t,r){if(r==="")return!0;const n=r.toLowerCase();return e.value.toLowerCase().includes(n)||t.toLowerCase().includes(n)?!0:(e.keywords??[]).some(o=>o.toLowerCase().includes(n))}function Hp({entry:e}){const t=e.preview;return t?.html?d.jsx("span",{className:"g7le-icon-preview",dangerouslySetInnerHTML:{__html:t.html},style:Ha}):t?.className?d.jsx("span",{className:`g7le-icon-preview ${t.className}`,style:Ha}):d.jsx("i",{className:`fas fa-${e.value}`,style:Ha,"aria-hidden":"true"})}function Gp({control:e,value:t,onChange:r,t:n}){const o=Fp(e),a=t==null?"":String(t),s=typeof e.iconColumns=="number"?e.iconColumns:6,l=e.iconSearchPlaceholder,f=typeof l=="string"&&l.startsWith("$t:")?n(l.slice(3)):n("layout_editor.icon_picker.search_placeholder"),[h,c]=k.useState("");if(o.length===0)return d.jsx("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:kc,children:d.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-free","data-testid":"g7le-widget-icon-picker-free",value:a,placeholder:n("layout_editor.icon_picker.free_input_hint"),onChange:g=>r(g.target.value===""?void 0:g.target.value),style:Cc})});const i=g=>Up(g,n),u=o.filter(g=>zp(g,i(g),h));return d.jsxs("div",{className:"g7le-widget g7le-widget--icon-picker","data-testid":"g7le-widget-icon-picker",style:kc,children:[d.jsx("input",{type:"text",className:"g7le-widget g7le-widget--icon-search","data-testid":"g7le-widget-icon-picker-search",value:h,placeholder:f,onChange:g=>c(g.target.value),style:Cc}),d.jsxs("div",{className:"g7le-icon-grid","data-testid":"g7le-widget-icon-picker-grid",style:{...jp,gridTemplateColumns:`repeat(${s}, minmax(0, 1fr))`},children:[d.jsx("button",{type:"button","data-testid":"g7le-icon-cell-none",title:n("layout_editor.icon_picker.none"),onClick:()=>r(void 0),"aria-pressed":a==="",style:a===""?Sc:za,children:d.jsx("span",{style:Bp,children:"—"})}),u.map(g=>{const p=g.value===a;return d.jsx("button",{type:"button","data-testid":`g7le-icon-cell-${g.value}`,title:i(g),"aria-pressed":p,onClick:()=>r(g.value),style:p?Sc:za,children:d.jsx(Hp,{entry:g})},g.value)})]}),u.length===0&&d.jsx("div",{"data-testid":"g7le-widget-icon-picker-empty",style:Wp,children:n("layout_editor.icon_picker.no_results")})]})}const kc={display:"flex",flexDirection:"column",gap:6,width:"100%"},Cc={padding:"5px 8px",fontSize:12,border:"1px solid #cbd5e1",borderRadius:6,width:"100%"},jp={display:"grid",gap:4,maxHeight:220,overflowY:"auto",padding:2},za={display:"flex",alignItems:"center",justifyContent:"center",minHeight:34,padding:4,border:"1px solid #e2e8f0",borderRadius:6,background:"#fff",cursor:"pointer"},Sc={...za,border:"2px solid #2563eb",background:"#eff6ff"},Ha={display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:16,lineHeight:1},Bp={fontSize:10,color:"#475569",fontFamily:"monospace",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:"100%"},Wp={fontSize:11,color:"#94a3b8",fontStyle:"italic",padding:"4px 2px"},_c=window.G7Core?.createLogger?.("Template:sirsoft-basic:EditorWidgets")??{log:(...e)=>console.log("[Template:sirsoft-basic:EditorWidgets]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic:EditorWidgets]",...e)};function Vp(){if(typeof window>"u")return;const e=window.G7Core?.layoutEditor;if(!e?.registerWidget){_c.warn("G7Core.layoutEditor 미노출 — icon-picker 위젯 등록 보류");return}e.registerWidget("icon-picker",Gp),_c.log("편집기 위젯 'icon-picker' 등록됨(Font Awesome 검색 그리드)")}const Ga=window.G7Core?.createLogger?.("Template:sirsoft-basic")??{log:(...e)=>console.log("[Template:sirsoft-basic]",...e),warn:(...e)=>console.warn("[Template:sirsoft-basic]",...e),error:(...e)=>console.error("[Template:sirsoft-basic]",...e)};typeof window<"u"&&(window.G7TemplateHandlers=to);function Ec(){if(typeof window<"u"){let e=0;const t=50,r=()=>{const n=window.G7Core?.getActionDispatcher?.();n?(Object.entries(to).forEach(([o,a])=>{n.registerHandler(o,a)}),Ga.log(`${Object.keys(to).length} custom handler(s) registered:`,Object.keys(to)),Lp(),$p()):(e++,e<=t?(Ga.warn(`ActionDispatcher not found, retrying... (${e}/${t})`),setTimeout(r,100)):Ga.error("Failed to register handlers: ActionDispatcher not available after maximum retries"))};document.readyState==="complete"?r():window.addEventListener("load",r)}}return Vp(),Ec(),J.A=ao,J.Avatar=Ur,J.AvatarUploader=Ma,J.BrandMark=xo,J.Button=Q,J.Checkbox=qa,J.Code=Zc,J.ConfirmDialog=ra,J.Container=M0,J.Div=W,J.ExpandableContent=Ro,J.FileInput=Ba,J.FileUploader=Xn,J.Flex=I0,J.Footer=rs,J.Form=so,J.Grid=O0,J.H1=Uc,J.H2=Ka,J.H3=gn,J.H4=Ya,J.Header=es,J.Hr=lo,J.HtmlContent=Zo,J.HtmlEditor=Ll,J.Icon=de,J.IconName=Je,J.ImageGallery=Tr,J.Img=Lt,J.Input=xt,J.Label=yr,J.Li=Xt,J.MobileNav=ns,J.Modal=ea,J.Nav=Fr,J.NotificationCenter=mn,J.Ol=zc,J.Optgroup=Bc,J.Option=jc,J.P=Dt,J.PageLoading=ec,J.PageSkeleton=Aa,J.PageTransitionBlur=Ea,J.PageTransitionIndicator=_a,J.Pagination=tc,J.PasswordInput=Va,J.PostReactions=Il,J.ProductCard=ms,J.ProductImageViewer=An,J.QuantitySelector=Ml,J.RichTextEditor=Dl,J.SearchBar=nc,J.Section=Za,J.SectionLayout=D0,J.Select=Xa,J.SlotContainer=pn,J.SocialLoginButtons=Vi,J.Span=R,J.Svg=fn,J.TabNavigation=lc,J.Table=Wc,J.Tbody=Kc,J.Td=qc,J.Textarea=no,J.Th=Xc,J.Thead=Vc,J.ThemeToggle=hn,J.ThreeColumnLayout=L0,J.Toast=Sa,J.Tr=Yc,J.Ul=oo,J.UserInfo=sc,J.availablePasswordRules=ro,J.compositeComponents=A0,J.defaultPasswordRules=Wa,J.initTemplate=Ec,J.templateMetadata=P0,Object.defineProperty(J,Symbol.toStringTag,{value:"Module"}),J})({},ReactJSXRuntime,React,ReactDOM); diff --git a/templates/_bundled/sirsoft-basic/dist/src/components/composite/assetOrigin.d.ts b/templates/_bundled/sirsoft-basic/dist/src/components/composite/assetOrigin.d.ts new file mode 100644 index 00000000..08a4a4e9 --- /dev/null +++ b/templates/_bundled/sirsoft-basic/dist/src/components/composite/assetOrigin.d.ts @@ -0,0 +1,23 @@ +/** + * 자산 URL 출처 판정 유틸리티 + * + * 공개 자산 스토리지(S3/CDN)를 켜면 첨부·이미지의 `download_url` 이 외부 origin + * 절대 URL 이 된다. 이런 URL 은 인증이 필요 없는 공개 자산이므로 XHR(Blob) 로 + * 가져오면 안 된다 — 교차 출처 XHR 은 CDN 이 CORS 헤더를 주지 않는 한 응답을 + * 읽지 못해 이미지가 통째로 실패하고, 응답을 읽을 수 있는 CDN 이라면 이번에는 + * 세션 토큰이 제3자 origin 으로 나간다. 교차 출처 URL 은 `<img src>` / 링크로 + * 직접 사용한다. + * + * @module composite/assetOrigin + */ +/** + * URL 이 현재 문서와 다른 출처인지 판정합니다. + * + * 상대 경로는 항상 동일 출처입니다. 절대 URL(`https://…`)과 프로토콜 상대 + * URL(`//host/…`)만 출처를 비교하며, 파싱 불가하거나 브라우저 밖이면 + * 동일 출처로 간주합니다(기존 인증 경로 유지). + * + * @param url 판정할 URL + * @returns 교차 출처 여부 + */ +export declare const isCrossOriginAssetUrl: (url?: string | null) => boolean; diff --git a/templates/_bundled/sirsoft-basic/lang/partial/en/search.json b/templates/_bundled/sirsoft-basic/lang/partial/en/search.json index d1120e4b..234de360 100644 --- a/templates/_bundled/sirsoft-basic/lang/partial/en/search.json +++ b/templates/_bundled/sirsoft-basic/lang/partial/en/search.json @@ -39,6 +39,10 @@ "pages_in_all": "No pages found", "suggestion": "Try a different search term" }, + "failed": { + "title": "An error occurred while searching", + "suggestion": "Please try again in a moment" + }, "page_published_at": "Published: {{date}}", "view_more": "View more", "view_all": "View all", diff --git a/templates/_bundled/sirsoft-basic/lang/partial/ko/search.json b/templates/_bundled/sirsoft-basic/lang/partial/ko/search.json index cb9264cc..60beafce 100644 --- a/templates/_bundled/sirsoft-basic/lang/partial/ko/search.json +++ b/templates/_bundled/sirsoft-basic/lang/partial/ko/search.json @@ -39,6 +39,10 @@ "pages_in_all": "페이지 검색 결과가 없습니다", "suggestion": "다른 검색어로 시도해 보세요" }, + "failed": { + "title": "검색 중 오류가 발생했습니다", + "suggestion": "잠시 후 다시 시도해 주세요" + }, "page_published_at": "발행일: {{date}}", "view_more": "더보기", "view_all": "전체보기", diff --git a/templates/_bundled/sirsoft-basic/layouts/mypage/change-password.json b/templates/_bundled/sirsoft-basic/layouts/mypage/change-password.json index 3bd61e30..279cce89 100644 --- a/templates/_bundled/sirsoft-basic/layouts/mypage/change-password.json +++ b/templates/_bundled/sirsoft-basic/layouts/mypage/change-password.json @@ -196,7 +196,7 @@ "handler": "toast", "params": { "type": "error", - "message": "{{response.message ?? '$t:mypage.profile.change_password_failed'}}" + "message": "{{error.message ?? '$t:mypage.profile.change_password_failed'}}" } } ] diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json b/templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json index 98de7a0b..cb411d05 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/mypage/profile/_edit.json @@ -524,7 +524,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.name[0]}}" + "text": "{{_local.profileErrors?.name?.[0] ?? ''}}" } ] }, @@ -562,7 +562,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.nickname[0]}}" + "text": "{{_local.profileErrors?.nickname?.[0] ?? ''}}" } ] } @@ -651,7 +651,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.mobile[0]}}" + "text": "{{_local.profileErrors?.mobile?.[0] ?? ''}}" } ] }, @@ -689,7 +689,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.phone[0]}}" + "text": "{{_local.profileErrors?.phone?.[0] ?? ''}}" } ] }, @@ -727,7 +727,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.homepage[0]}}" + "text": "{{_local.profileErrors?.homepage?.[0] ?? ''}}" } ] } @@ -799,7 +799,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.country[0]}}" + "text": "{{_local.profileErrors?.country?.[0] ?? ''}}" } ] }, @@ -836,7 +836,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.language[0]}}" + "text": "{{_local.profileErrors?.language?.[0] ?? ''}}" } ] }, @@ -875,7 +875,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.timezone[0]}}" + "text": "{{_local.profileErrors?.timezone?.[0] ?? ''}}" } ] }, @@ -963,7 +963,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors?.zipcode ? _local.profileErrors.zipcode[0] : (_local.profileErrors?.address ? _local.profileErrors.address[0] : (_local.profileErrors?.address_detail ? _local.profileErrors.address_detail[0] : ''))}}" + "text": "{{_local.profileErrors?.zipcode?.[0] ?? _local.profileErrors?.address?.[0] ?? _local.profileErrors?.address_detail?.[0] ?? ''}}" } ] } @@ -1038,7 +1038,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.signature[0]}}" + "text": "{{_local.profileErrors?.signature?.[0] ?? ''}}" }, { "type": "basic", @@ -1084,7 +1084,7 @@ "props": { "className": "text-sm text-red-600 dark:text-red-400" }, - "text": "{{_local.profileErrors.bio[0]}}" + "text": "{{_local.profileErrors?.bio?.[0] ?? ''}}" }, { "type": "basic", @@ -1182,10 +1182,23 @@ { "type": "basic", "name": "Checkbox", + "comment": "저장값이 boolean 이 아닐 수 있는 구간(설정 미저장 등)에도 checked 가 확정되도록 명시 바인딩 — 자동바인딩은 boolean 일 때만 checked 로 붙는다", "props": { "name": "notify_post_complete", + "checked": "{{!!_local.profileEditForm?.notify_post_complete}}", + "autoBinding": false, "className": "w-5 h-5 mt-0.5 text-gray-900 dark:text-white bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-gray-900 dark:focus:ring-white" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "profileEditForm.notify_post_complete": "{{$event.target.checked}}" + } + } + ] }, { "type": "basic", @@ -1221,10 +1234,23 @@ { "type": "basic", "name": "Checkbox", + "comment": "저장값이 boolean 이 아닐 수 있는 구간(설정 미저장 등)에도 checked 가 확정되도록 명시 바인딩 — 자동바인딩은 boolean 일 때만 checked 로 붙는다", "props": { "name": "notify_post_reply", + "checked": "{{!!_local.profileEditForm?.notify_post_reply}}", + "autoBinding": false, "className": "w-5 h-5 mt-0.5 text-gray-900 dark:text-white bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-gray-900 dark:focus:ring-white" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "profileEditForm.notify_post_reply": "{{$event.target.checked}}" + } + } + ] }, { "type": "basic", @@ -1260,10 +1286,23 @@ { "type": "basic", "name": "Checkbox", + "comment": "저장값이 boolean 이 아닐 수 있는 구간(설정 미저장 등)에도 checked 가 확정되도록 명시 바인딩 — 자동바인딩은 boolean 일 때만 checked 로 붙는다", "props": { "name": "notify_comment", + "checked": "{{!!_local.profileEditForm?.notify_comment}}", + "autoBinding": false, "className": "w-5 h-5 mt-0.5 text-gray-900 dark:text-white bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-gray-900 dark:focus:ring-white" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "profileEditForm.notify_comment": "{{$event.target.checked}}" + } + } + ] }, { "type": "basic", @@ -1299,10 +1338,23 @@ { "type": "basic", "name": "Checkbox", + "comment": "저장값이 boolean 이 아닐 수 있는 구간(설정 미저장 등)에도 checked 가 확정되도록 명시 바인딩 — 자동바인딩은 boolean 일 때만 checked 로 붙는다", "props": { "name": "notify_reply_comment", + "checked": "{{!!_local.profileEditForm?.notify_reply_comment}}", + "autoBinding": false, "className": "w-5 h-5 mt-0.5 text-gray-900 dark:text-white bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-gray-900 dark:focus:ring-white" - } + }, + "actions": [ + { + "type": "change", + "handler": "setState", + "params": { + "target": "local", + "profileEditForm.notify_reply_comment": "{{$event.target.checked}}" + } + } + ] }, { "type": "basic", diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_results.json b/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_results.json index 33831d0c..d55243bf 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_results.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_results.json @@ -26,10 +26,10 @@ ] }, { - "comment": "게시글 탭 - 결과 없을 때", + "comment": "게시글 탭 - 결과 없을 때 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{query?.type === 'posts' && (searchResults?.data?.posts_count ?? 0) === 0}}", + "if": "{{query?.type === 'posts' && (searchResults?.data?.posts_count ?? 0) === 0 && !(searchResults?.data?.categories_failed?.posts ?? false)}}", "props": { "className": "py-16 text-center" }, "children": [ { @@ -44,6 +44,25 @@ { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.empty.suggestion" } ] }, + { + "comment": "게시글 탭 - 검색 실패", + "type": "basic", + "name": "Div", + "if": "{{query?.type === 'posts' && (searchResults?.data?.categories_failed?.posts ?? false)}}", + "props": { "className": "py-16 text-center" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "mb-5 text-red-300 dark:text-red-500/60" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "4x" } } + ] + }, + { "type": "basic", "name": "H3", "props": { "className": "text-lg font-medium text-gray-900 dark:text-white mb-2" }, "text": "$t:search.failed.title" }, + { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.failed.suggestion" } + ] + }, { "comment": "상품 탭 - 결과 있을 때", "type": "basic", @@ -62,10 +81,10 @@ ] }, { - "comment": "상품 탭 - 결과 없을 때", + "comment": "상품 탭 - 결과 없을 때 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{query?.type === 'products' && (searchResults?.data?.products_count ?? 0) === 0}}", + "if": "{{query?.type === 'products' && (searchResults?.data?.products_count ?? 0) === 0 && !(searchResults?.data?.categories_failed?.products ?? false)}}", "props": { "className": "py-16 text-center" }, "children": [ { @@ -80,6 +99,25 @@ { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.empty.suggestion" } ] }, + { + "comment": "상품 탭 - 검색 실패", + "type": "basic", + "name": "Div", + "if": "{{query?.type === 'products' && (searchResults?.data?.categories_failed?.products ?? false)}}", + "props": { "className": "py-16 text-center" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "mb-5 text-red-300 dark:text-red-500/60" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "4x" } } + ] + }, + { "type": "basic", "name": "H3", "props": { "className": "text-lg font-medium text-gray-900 dark:text-white mb-2" }, "text": "$t:search.failed.title" }, + { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.failed.suggestion" } + ] + }, { "comment": "페이지 탭 - 결과 있을 때", "type": "basic", @@ -90,10 +128,10 @@ ] }, { - "comment": "페이지 탭 - 결과 없을 때", + "comment": "페이지 탭 - 결과 없을 때 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{query?.type === 'pages' && (searchResults?.data?.pages_count ?? 0) === 0}}", + "if": "{{query?.type === 'pages' && (searchResults?.data?.pages_count ?? 0) === 0 && !(searchResults?.data?.categories_failed?.pages ?? false)}}", "props": { "className": "py-16 text-center" }, "children": [ { @@ -107,6 +145,25 @@ { "type": "basic", "name": "H3", "props": { "className": "text-lg font-medium text-gray-900 dark:text-white mb-2" }, "text": "$t:search.empty.pages" }, { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.empty.suggestion" } ] + }, + { + "comment": "페이지 탭 - 검색 실패", + "type": "basic", + "name": "Div", + "if": "{{query?.type === 'pages' && (searchResults?.data?.categories_failed?.pages ?? false)}}", + "props": { "className": "py-16 text-center" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "mb-5 text-red-300 dark:text-red-500/60" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "4x" } } + ] + }, + { "type": "basic", "name": "H3", "props": { "className": "text-lg font-medium text-gray-900 dark:text-white mb-2" }, "text": "$t:search.failed.title" }, + { "type": "basic", "name": "P", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, "text": "$t:search.failed.suggestion" } + ] } ] } \ No newline at end of file diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_states.json b/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_states.json index 9ff893bf..449b043a 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_states.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/search/_search_states.json @@ -34,10 +34,10 @@ ] }, { - "comment": "상한 초과 안내 — 검색어를 좁히면 정확한 건수와 마지막 페이지를 볼 수 있다", + "comment": "상한 초과 안내 — 검색어를 좁히면 정확한 건수와 마지막 페이지를 볼 수 있다. 검색 실패도 total_is_exact=false 를 싣지만(실패한 0건을 '정확한 0건'으로 말하지 않기 위함) 그때는 검색어를 좁혀도 해소되지 않으므로 이 조치 안내를 그리지 않는다", "type": "basic", "name": "P", - "if": "{{(searchResults?.data?.total_is_exact ?? true) === false}}", + "if": "{{(searchResults?.data?.total_is_exact ?? true) === false && !(searchResults?.data?.search_failed ?? false)}}", "props": { "className": "mt-1 text-xs text-gray-500 dark:text-gray-400" }, "text": "$t:search.refine_query_hint" } @@ -114,11 +114,40 @@ } ] }, + { + "comment": "전체 탭 검색 실패 — 결과가 0건이고 실패가 있으면 '결과 없음' 대신 오류 안내 (키 부재(구버전 코어)는 종전 렌더 유지)", + "type": "basic", + "name": "Div", + "if": "{{query?.q && searchResults?.data && (searchResults?.data?.total ?? 0) === 0 && (query?.type ?? 'all') === 'all' && (searchResults?.data?.search_failed ?? false)}}", + "props": { "className": "py-16 text-center" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "mb-5 text-red-300 dark:text-red-500/60" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "4x" } } + ] + }, + { + "type": "basic", + "name": "H3", + "props": { "className": "text-lg font-medium text-gray-900 dark:text-white mb-2" }, + "text": "$t:search.failed.title" + }, + { + "type": "basic", + "name": "P", + "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, + "text": "$t:search.failed.suggestion" + } + ] + }, { "comment": "검색 결과 없음 (전체 탭)", "type": "basic", "name": "Div", - "if": "{{query?.q && searchResults?.data && (searchResults?.data?.total ?? 0) === 0 && (query?.type ?? 'all') === 'all'}}", + "if": "{{query?.q && searchResults?.data && (searchResults?.data?.total ?? 0) === 0 && (query?.type ?? 'all') === 'all' && !(searchResults?.data?.search_failed ?? false)}}", "props": { "className": "py-16 text-center" }, "children": [ { diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/search/pages/_section.json b/templates/_bundled/sirsoft-basic/layouts/partials/search/pages/_section.json index cffea99c..b51733b3 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/search/pages/_section.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/search/pages/_section.json @@ -96,14 +96,33 @@ ] }, { - "comment": "결과 없음 안내", + "comment": "결과 없음 안내 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{!searchResults?.data?.pages?.items || searchResults?.data?.pages?.items?.length === 0}}", + "if": "{{(!searchResults?.data?.pages?.items || searchResults?.data?.pages?.items?.length === 0) && !(searchResults?.data?.categories_failed?.pages ?? false)}}", "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, "children": [ { "type": "basic", "name": "Span", "text": "$t:search.empty.pages_in_all" } ] + }, + { + "comment": "검색 실패 안내", + "type": "basic", + "name": "Div", + "if": "{{searchResults?.data?.categories_failed?.pages ?? false}}", + "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "flex items-center justify-center gap-2" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "sm", "className": "text-red-500 dark:text-red-400" } }, + { "type": "basic", "name": "Span", "text": "$t:search.failed.title" } + ] + }, + { "type": "basic", "name": "P", "props": { "className": "mt-1 text-xs text-gray-400 dark:text-gray-500" }, "text": "$t:search.failed.suggestion" } + ] } ] } diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/search/posts/_section.json b/templates/_bundled/sirsoft-basic/layouts/partials/search/posts/_section.json index 6404fc97..28ad2188 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/search/posts/_section.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/search/posts/_section.json @@ -138,14 +138,33 @@ ] }, { - "comment": "결과 없음 안내", + "comment": "결과 없음 안내 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{!searchResults?.data?.posts?.items || searchResults?.data?.posts?.items?.length === 0}}", + "if": "{{(!searchResults?.data?.posts?.items || searchResults?.data?.posts?.items?.length === 0) && !(searchResults?.data?.categories_failed?.posts ?? false)}}", "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, "children": [ { "type": "basic", "name": "Span", "text": "$t:search.empty.posts_in_all" } ] + }, + { + "comment": "검색 실패 안내", + "type": "basic", + "name": "Div", + "if": "{{searchResults?.data?.categories_failed?.posts ?? false}}", + "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "flex items-center justify-center gap-2" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "sm", "className": "text-red-500 dark:text-red-400" } }, + { "type": "basic", "name": "Span", "text": "$t:search.failed.title" } + ] + }, + { "type": "basic", "name": "P", "props": { "className": "mt-1 text-xs text-gray-400 dark:text-gray-500" }, "text": "$t:search.failed.suggestion" } + ] } ] } \ No newline at end of file diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/search/products/_section.json b/templates/_bundled/sirsoft-basic/layouts/partials/search/products/_section.json index 04108f78..d5179964 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/search/products/_section.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/search/products/_section.json @@ -59,14 +59,33 @@ ] }, { - "comment": "결과 없음 안내", + "comment": "결과 없음 안내 (검색 실패는 결과 없음이 아니다 — categories_failed 로 구분, 키 부재(구버전 코어)는 종전 렌더 유지)", "type": "basic", "name": "Div", - "if": "{{!searchResults?.data?.products || searchResults?.data?.products?.length === 0}}", + "if": "{{(!searchResults?.data?.products || searchResults?.data?.products?.length === 0) && !(searchResults?.data?.categories_failed?.products ?? false)}}", "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, "children": [ { "type": "basic", "name": "Span", "text": "$t:search.empty.products_in_all" } ] + }, + { + "comment": "검색 실패 안내", + "type": "basic", + "name": "Div", + "if": "{{searchResults?.data?.categories_failed?.products ?? false}}", + "props": { "className": "px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400" }, + "children": [ + { + "type": "basic", + "name": "Div", + "props": { "className": "flex items-center justify-center gap-2" }, + "children": [ + { "type": "basic", "name": "Icon", "props": { "name": "triangle-exclamation", "size": "sm", "className": "text-red-500 dark:text-red-400" } }, + { "type": "basic", "name": "Span", "text": "$t:search.failed.title" } + ] + }, + { "type": "basic", "name": "P", "props": { "className": "mt-1 text-xs text-gray-400 dark:text-gray-500" }, "text": "$t:search.failed.suggestion" } + ] } ] } \ No newline at end of file diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_cart_list.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_cart_list.json index 04b5e9b1..e5bac528 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_cart_list.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_cart_list.json @@ -31,7 +31,7 @@ "id": "selectAll", "name": "selectAll", "checked": "{{_local.allSelected}}", - "className": "w-5 h-5 rounded border-gray-300 accent-gray-900 focus:ring-gray-500" + "className": "w-5 h-5 rounded border-gray-300 dark:border-gray-600 accent-gray-900 focus:ring-gray-500" }, "actions": [ { @@ -54,7 +54,8 @@ "type": "basic", "name": "Span", "props": { "className": "text-sm text-gray-500 dark:text-gray-400" }, - "text": "({{_local.selectedCount}}/{{cartItems.data.items.length}})" + "comment": "선택 개수는 선택 상태의 SSoT 인 selectedItems 에서 파생한다 (주문 요약 _cart_summary.json 과 동일 표현식)", + "text": "({{_local.selectedItems?.length ?? 0}}/{{cartItems.data.item_count ?? 0}})" } ] }, @@ -66,7 +67,7 @@ "disabled": "{{!_local.selectedItems || _local.selectedItems.length === 0}}" }, "children": [ - { "type": "basic", "name": "Icon", "props": { "name": "trash-can", "className": "w-4 h-4" } }, + { "type": "basic", "name": "Icon", "props": { "name": "trash-can", "className": "text-base" } }, { "type": "basic", "name": "Span", "text": "$t:shop.cart.delete_selected" } ], "actions": [ @@ -122,14 +123,14 @@ "if": "{{!cartItems.data.items || cartItems.data.items.length === 0}}", "props": { "className": "text-center py-16" }, "children": [ - { "type": "basic", "name": "Icon", "props": { "name": "shopping-cart", "className": "w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" } }, + { "type": "basic", "name": "Icon", "props": { "name": "shopping-cart", "className": "text-6xl mx-auto mb-4 text-gray-300 dark:text-gray-600" } }, { "type": "basic", "name": "P", "props": { "className": "text-gray-500 dark:text-gray-400 mb-4" }, "text": "$t:shop.cart.empty" }, { "type": "basic", "name": "Button", "props": { "className": "inline-flex items-center gap-2 px-4 py-2 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-lg font-medium cursor-pointer" }, "children": [ - { "type": "basic", "name": "Icon", "props": { "name": "arrow-left", "className": "w-4 h-4" } }, + { "type": "basic", "name": "Icon", "props": { "name": "arrow-left", "className": "text-base" } }, { "type": "basic", "name": "Span", "text": "$t:shop.cart.continue_shopping" } ], "actions": [{ "type": "click", "handler": "navigate", "params": { "path": "{{_global.shopBase}}/products" } }] diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_discount.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_discount.json index f1318d7a..a99123c6 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_discount.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_discount.json @@ -95,7 +95,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:shop.coupon_download.fetch_failed" + "message": "{{error.message ?? '$t:shop.coupon_download.fetch_failed'}}" } } ] @@ -442,7 +442,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:shop.checkout.discount_code_invalid" + "message": "{{error.message ?? '$t:shop.checkout.discount_code_invalid'}}" } } ] diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_summary.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_summary.json index c55ccf7f..ecf2b932 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_summary.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_checkout_summary.json @@ -518,7 +518,7 @@ { "comment": "verify 실패는 결제 자체를 막지 않음 — 주문은 이미 생성되었으므로 사용자가 나중에 lookup 폼으로 재인증 가능. 토스트로만 안내.", "handler": "toast", - "params": { "type": "warning", "message": "$t:shop.checkout.guest_token_issue_failed" } + "params": { "type": "warning", "message": "{{error.message ?? '$t:shop.checkout.guest_token_issue_failed'}}" } } ] } diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_modal_cart_option_change.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_modal_cart_option_change.json index 104ce131..7596f9a8 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/_modal_cart_option_change.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/_modal_cart_option_change.json @@ -345,7 +345,7 @@ "name": "Div", "props": { "className": "flex justify-between items-start" }, "children": [ - { "type": "basic", "name": "Span", "props": { "className": "text-sm text-gray-700 dark:text-gray-300" }, "text": "{{$parent._local.matchedOption.option_name_localized ?? $parent._local.matchedOption.option_name?.ko ?? ''}}" }, + { "type": "basic", "name": "Span", "props": { "className": "text-sm text-gray-700 dark:text-gray-300" }, "text": "{{$parent._local.matchedOption?.option_name_localized ?? $parent._local.matchedOption?.option_name?.ko ?? ''}}" }, { "type": "basic", "name": "Div", @@ -355,7 +355,7 @@ "type": "basic", "name": "Div", "props": { "className": "text-sm font-bold text-gray-900 dark:text-white" }, - "text": "{{$parent._local.matchedOption.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? $parent._local.matchedOption.selling_price_formatted}}" + "text": "{{$parent._local.matchedOption?.multi_currency_selling_price?.[_global.preferredCurrency ?? _global.defaultCurrency]?.formatted ?? $parent._local.matchedOption?.selling_price_formatted ?? ''}}" }, { "comment": "보조 통화들", @@ -363,7 +363,7 @@ "name": "Div", "props": { "className": "flex flex-wrap gap-x-2 justify-end" }, "iteration": { - "source": "{{Object.entries($parent._local.matchedOption.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}", + "source": "{{Object.entries($parent._local.matchedOption?.multi_currency_selling_price ?? {}).filter(([code]) => code !== (_global.preferredCurrency ?? _global.defaultCurrency))}}", "item_var": "currency" }, "children": [ @@ -393,16 +393,16 @@ { "type": "basic", "name": "Div", - "if": "{{$parent._local.matchedOption.stock_quantity <= 10}}", - "props": { "className": "{{$parent._local.matchedOption.stock_quantity <= 0 ? 'text-xs text-red-500 dark:text-red-400 mt-1 font-medium' : 'text-xs text-gray-500 dark:text-gray-400 mt-1'}}" }, - "text": "{{$parent._local.matchedOption.stock_quantity <= 0 ? '$t:shop.cart.option_modal.out_of_stock' : '$t:shop.cart.option_modal.stock_count|count=' + $parent._local.matchedOption.stock_quantity}}" + "if": "{{$parent._local.matchedOption?.stock_quantity <= 10}}", + "props": { "className": "{{$parent._local.matchedOption?.stock_quantity <= 0 ? 'text-xs text-red-500 dark:text-red-400 mt-1 font-medium' : 'text-xs text-gray-500 dark:text-gray-400 mt-1'}}" }, + "text": "{{$parent._local.matchedOption?.stock_quantity <= 0 ? '$t:shop.cart.option_modal.out_of_stock' : '$t:shop.cart.option_modal.stock_count|count=' + ($parent._local.matchedOption?.stock_quantity ?? 0)}}" }, { "type": "basic", "name": "Div", - "if": "{{$parent._local.matchedOption.stock_quantity > 10}}", + "if": "{{$parent._local.matchedOption?.stock_quantity > 10}}", "props": { "className": "text-xs text-gray-500 dark:text-gray-400 mt-1" }, - "text": "$t:shop.cart.option_modal.stock_count|count={{$parent._local.matchedOption.stock_quantity}}" + "text": "$t:shop.cart.option_modal.stock_count|count={{$parent._local.matchedOption?.stock_quantity ?? 0}}" } ] }, diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_modal_inquiry_delete.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_modal_inquiry_delete.json index cc6cd954..1628faa5 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_modal_inquiry_delete.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_modal_inquiry_delete.json @@ -161,7 +161,7 @@ ], "onError": [ { "handler": "setState", "params": { "target": "global", "qnaDeleting": false } }, - { "handler": "toast", "params": { "message": "$t:shop.qna.delete_failed", "type": "error" } } + { "handler": "toast", "params": { "message": "{{error.message ?? '$t:shop.qna.delete_failed'}}", "type": "error" } } ] } ] diff --git a/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_tab_qna.json b/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_tab_qna.json index 25fbcc66..7e8cfe6e 100644 --- a/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_tab_qna.json +++ b/templates/_bundled/sirsoft-basic/layouts/partials/shop/detail/_tab_qna.json @@ -691,7 +691,7 @@ ], "onError": [ { "handler": "setState", "params": { "target": "local", "qnaReplySubmitting": false } }, - { "handler": "toast", "params": { "message": "$t:shop.qna.reply_update_failed", "type": "error" } } + { "handler": "toast", "params": { "message": "{{error.message ?? '$t:shop.qna.reply_update_failed'}}", "type": "error" } } ] } ] @@ -837,7 +837,7 @@ ], "onError": [ { "handler": "setState", "params": { "target": "local", "qnaNewReplySubmitting": false } }, - { "handler": "toast", "params": { "message": "$t:shop.qna.reply_submit_failed", "type": "error" } } + { "handler": "toast", "params": { "message": "{{error.message ?? '$t:shop.qna.reply_submit_failed'}}", "type": "error" } } ] } ] diff --git a/templates/_bundled/sirsoft-basic/layouts/search/index.json b/templates/_bundled/sirsoft-basic/layouts/search/index.json index a5d23223..97068fee 100644 --- a/templates/_bundled/sirsoft-basic/layouts/search/index.json +++ b/templates/_bundled/sirsoft-basic/layouts/search/index.json @@ -46,9 +46,10 @@ "id": "searchResults", "label_key": "$t:editor.data_source.searchResults", "type": "api", - "comment": "탭·게시판필터·정렬·페이지·커서는 전역 상태가 아니라 URL 쿼리를 읽는다. 전역 상태에 두면 주소가 바뀌지 않아 새로고침·뒤로가기에서 전부 초기값으로 돌아가고 결과를 그대로 공유할 수도 없다. cursor 는 커서로 넘길 수 있는 정렬(최신순 등)에서 이전/다음 이동 시 채워진다. 비어 있으면 서버가 첫 페이지로 읽고 다음 커서를 발급한다 — 관련도순은 계산값 정렬이라 서버가 커서를 발급하지 않고 page 로만 움직인다", + "comment": "탭·게시판필터·정렬·페이지·커서는 전역 상태가 아니라 URL 쿼리를 읽는다. 전역 상태에 두면 주소가 바뀌지 않아 새로고침·뒤로가기에서 전부 초기값으로 돌아가고 결과를 그대로 공유할 수도 없다. cursor 는 커서로 넘길 수 있는 정렬(최신순 등)에서 이전/다음 이동 시 채워진다. 비어 있으면 서버가 첫 페이지로 읽고 다음 커서를 발급한다 — 관련도순은 계산값 정렬이라 서버가 커서를 발급하지 않고 page 로만 움직인다. auth_mode=optional: 검색 라우트는 optional.sanctum 으로 로그인 회원의 열람 범위(회원 전용 게시판 포함)를 해석하므로, 토큰이 있으면 반드시 실어 보낸다 — 미선언(none)이면 로그인 회원도 비회원 범위로 검색되어 서버측 권한 해석이 무력화된다", "endpoint": "/api/search?q={{query?.q}}&type={{query?.type ?? 'all'}}&board_slug={{query?.board_slug ?? ''}}&sort={{query?.sort ?? 'relevance'}}&page={{query?.page ?? 1}}&cursor={{query?.cursor ?? ''}}&per_page={{(query?.type === 'products') ? 12 : 10}}", "method": "GET", + "auth_mode": "optional", "auto_fetch": true, "loading_strategy": "progressive", "condition": "{{query?.q}}", @@ -169,4 +170,4 @@ } ] } -} \ No newline at end of file +} diff --git a/templates/_bundled/sirsoft-basic/layouts/shop/order_complete.json b/templates/_bundled/sirsoft-basic/layouts/shop/order_complete.json index 8c43b195..c397b864 100644 --- a/templates/_bundled/sirsoft-basic/layouts/shop/order_complete.json +++ b/templates/_bundled/sirsoft-basic/layouts/shop/order_complete.json @@ -604,7 +604,7 @@ { "handler": "toast", "params": { "type": "success", "message": "$t:shop.order_complete.address_saved" } } ], "onError": [ - { "handler": "toast", "params": { "type": "error", "message": "$t:shop.order_complete.address_save_failed" } } + { "handler": "toast", "params": { "type": "error", "message": "{{error.message ?? '$t:shop.order_complete.address_save_failed'}}" } } ] } ] diff --git a/templates/_bundled/sirsoft-basic/layouts/shop/reorder.json b/templates/_bundled/sirsoft-basic/layouts/shop/reorder.json index fe68ee6a..311e3658 100644 --- a/templates/_bundled/sirsoft-basic/layouts/shop/reorder.json +++ b/templates/_bundled/sirsoft-basic/layouts/shop/reorder.json @@ -53,7 +53,7 @@ "handler": "toast", "params": { "type": "error", - "message": "$t:shop.reorder.failed" + "message": "{{error.message ?? '$t:shop.reorder.failed'}}" } } ] diff --git a/templates/_bundled/sirsoft-basic/package-lock.json b/templates/_bundled/sirsoft-basic/package-lock.json index 49e8c1cd..d1d323cf 100644 --- a/templates/_bundled/sirsoft-basic/package-lock.json +++ b/templates/_bundled/sirsoft-basic/package-lock.json @@ -1,12 +1,12 @@ { "name": "sirsoft-basic", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sirsoft-basic", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", diff --git a/templates/_bundled/sirsoft-basic/package.json b/templates/_bundled/sirsoft-basic/package.json index 803e069e..c713487d 100644 --- a/templates/_bundled/sirsoft-basic/package.json +++ b/templates/_bundled/sirsoft-basic/package.json @@ -1,6 +1,6 @@ { "name": "sirsoft-basic", - "version": "1.1.0", + "version": "1.1.1", "description": "Gnuboard7 Basic User Template Components - Nexibase Style", "type": "module", "main": "dist/components.js", diff --git a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/board-401-error-handling.test.tsx b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/board-401-error-handling.test.tsx index da4ef5e9..192c9119 100644 --- a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/board-401-error-handling.test.tsx +++ b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/board-401-error-handling.test.tsx @@ -9,8 +9,7 @@ * 4. auth_required가 아닌 auth_mode: "optional"이 사용되는가 * 5. 기존 403/404 핸들러가 유지되는가 * - * @scenario entry_point:board_list_401,board_show_401 - * @effects redirect_preserves_pathname,login_returns_to_original_path + * @effects redirect_preserves_pathname, login_returns_to_original_path */ import { describe, it, expect } from 'vitest'; @@ -27,6 +26,10 @@ function getDataSourceErrorHandling(layout: any, dataSourceId: string) { } describe('게시판 401 errorHandling — sequence + redirect 방식 (이슈 #228 B-5)', () => { + /** + * @scenario entry_point=board_show_401 + * @effects redirect_preserves_pathname, login_returns_to_original_path + */ describe('show.json — auth_mode 및 데이터소스 401', () => { // redirect: /board/{{route.slug}}/posts/{{route.id}} const ds = getDataSource(showLayout, 'post'); @@ -89,6 +92,10 @@ describe('게시판 401 errorHandling — sequence + redirect 방식 (이슈 #22 }); }); + /** + * @scenario entry_point=board_list_401 + * @effects redirect_preserves_pathname, login_returns_to_original_path + */ describe('index.json — auth_mode 및 데이터소스 401', () => { const ds = getDataSource(indexLayout, 'posts'); const dsErrorHandling = ds?.errorHandling ?? null; diff --git a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart-select-count-initial.test.tsx b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart-select-count-initial.test.tsx new file mode 100644 index 00000000..bcc8d3c9 --- /dev/null +++ b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart-select-count-initial.test.tsx @@ -0,0 +1,418 @@ +/** + * @file cart-select-count-initial.test.tsx + * @description 장바구니 전체 선택 개수 최초 로딩 표시 회귀 (공개 이슈 #92) + * + * 결함: 장바구니를 처음 열면 전체 선택 옆 개수가 `(/27)` 처럼 선택 개수 없이 나온다. + * 표시식이 `{{_local.selectedCount}}` 인데 `selectedCount` 는 체크박스 change 핸들러에서만 + * 기록되므로 최초 마운트 시점에는 undefined 이고, 엔진은 undefined 를 빈 문자열로 서식한다 + * (예외·콘솔 에러 없음 — 표시 전용 결함). 체크박스를 한 번 건드리면 정상화된다. + * + * 정정: 표시식을 선택 상태의 SSoT 인 `_local.selectedItems` 파생으로 교체한다. + * 같은 화면 주문 요약(`_cart_summary.json`)이 이미 쓰던 표현식과 동일 형태로 통일해 + * 두 카운터의 계산 방식 비대칭도 함께 해소한다. + * + * 함께 검증(동일 근본 원인 — `null` 로만 선언된 키를 옵셔널 없이 하위 접근): + * - `_modal_cart_option_change.json` 재고 안내 (`matchedOption`) + * - `_edit.json` 프로필 오류 메시지 (`profileErrors` — 가드/표시식 옵셔널 엇갈림) + * + * @vitest-environment jsdom + */ + +import React from 'react'; +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { + createLayoutTest, + createMockComponentRegistryWithBasics, + screen, +} from '@core/template-engine/__tests__/utils/layoutTestUtils'; + +import cartListPartial from '../../../layouts/partials/shop/_cart_list.json'; +import cartOptionChangeModal from '../../../layouts/partials/shop/_modal_cart_option_change.json'; +import profileEditPartial from '../../../layouts/partials/mypage/profile/_edit.json'; + +// ========== 공통 유틸 ========== + +/** + * JSON 트리를 순회하며 모든 노드에 콜백을 적용합니다. + * + * @param node 순회 시작 노드 + * @param visit 노드 방문 콜백 + */ +function walk(node: unknown, visit: (n: Record<string, unknown>) => void): void { + if (Array.isArray(node)) { + node.forEach((n) => walk(n, visit)); + return; + } + if (!node || typeof node !== 'object') return; + const obj = node as Record<string, unknown>; + visit(obj); + Object.values(obj).forEach((v) => walk(v, visit)); +} + +/** + * JSON 트리에서 특정 문자열을 포함하는 문자열 값을 모두 수집합니다. + * + * @param node 순회 시작 노드 + * @param needle 찾을 부분 문자열 + * @returns 매칭된 문자열 값 목록 + */ +function collectStringsContaining(node: unknown, needle: string): string[] { + const out: string[] = []; + walk(node, (obj) => { + for (const v of Object.values(obj)) { + if (typeof v === 'string' && v.includes(needle)) out.push(v); + } + }); + return out; +} + +/** + * 장바구니 데이터소스 mock 응답을 만듭니다. + * + * @param count 장바구니 상품 수 + * @returns cart/query 응답 형태 + */ +function cartResponse(count: number) { + const items = Array.from({ length: count }, (_, i) => ({ + id: i + 1, + quantity: 1, + product: { name: `상품 ${i + 1}` }, + product_option: { option_name: '기본', price: 1000 }, + })); + return { + data: { + items, + item_ids: items.map((it) => it.id), + item_count: count, + calculation: null, + }, + }; +} + +/** 실 파일(`_cart_list.json`)을 그대로 감싼 테스트용 래퍼 레이아웃 */ +const wrapperLayout = { + version: '1.0.0', + layout_name: 'shop/cart#test-wrapper', + data_sources: [ + { + id: 'cartItems', + type: 'api', + endpoint: '/api/modules/sirsoft-ecommerce/cart/query', + method: 'POST', + auto_fetch: true, + }, + ], + components: [cartListPartial], +}; + +/** 카운터 Span 의 현재 텍스트를 읽는다 (`(선택/전체)` 형태) */ +function readCounterText(): string { + // `(3/3)` 정상, `(/3)` 결함 — 두 형태를 모두 잡는 패턴 + const el = screen.getByText(/^\(\d*\/\d+\)$/); + return el.textContent ?? ''; +} + +let registry: ReturnType<typeof createMockComponentRegistryWithBasics>; + +beforeEach(() => { + registry = createMockComponentRegistryWithBasics(); + // basics 에 없는 컴포넌트 보강 + registry.register('basic', 'Checkbox', ({ id, name, checked, className }: any) => + React.createElement('input', { + type: 'checkbox', + id, + name, + checked: !!checked, + className, + readOnly: true, + }) + ); + registry.register('basic', 'Icon', ({ name, className }: any) => + React.createElement('i', { 'data-icon': name, className }) + ); +}); + +// ========== 1. 렌더링 회귀 (이슈 #92 본체) ========== + +describe('_cart_list.json — 전체 선택 개수 최초 표시 (#92)', () => { + const ITEM_COUNT = 3; + + /** + * 최초 마운트 상태를 재현해 카운터 텍스트를 읽는다. + * `selectedCount` 는 의도적으로 주입하지 않는다 — 체크박스를 아직 건드리지 않은 상태. + * + * @param selectedItems 선택된 상품 id 목록 + * @returns 렌더 유틸과 카운터 텍스트 + */ + async function mountAndRead(selectedItems: number[]) { + const testUtils = createLayoutTest(wrapperLayout as any, { + componentRegistry: registry, + initialState: { + _local: { + selectedItems, + allSelected: selectedItems.length === ITEM_COUNT, + }, + }, + }); + testUtils.mockApi('cartItems', { response: cartResponse(ITEM_COUNT) }); + await testUtils.render(); + const text = readCounterText(); + return { testUtils, text }; + } + + /** + * @scenario mount_path=initial_load, selection=all + * @effects counter_matches_checked_state, counter_matches_summary_panel + */ + it('최초 로딩 + 전체 선택 상태에서 (3/3) 을 표시한다', async () => { + const { testUtils, text } = await mountAndRead([1, 2, 3]); + expect(text).toBe('(3/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=initial_load, selection=partial + * @effects counter_matches_checked_state + */ + it('최초 로딩 + 부분 선택 상태에서 (2/3) 을 표시한다', async () => { + const { testUtils, text } = await mountAndRead([1, 2]); + expect(text).toBe('(2/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=initial_load, selection=none + * @effects counter_matches_checked_state + */ + it('최초 로딩 + 미선택 상태에서 (0/3) 을 표시한다 (빈 문자열 금지)', async () => { + const { testUtils, text } = await mountAndRead([]); + expect(text).toBe('(0/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=reload, selection=all + * @effects counter_matches_checked_state, counter_matches_summary_panel + */ + it('새로고침(재마운트) 후에도 전체 선택 카운터가 (3/3) 이다', async () => { + const first = await mountAndRead([1, 2, 3]); + first.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([1, 2, 3]); + expect(text).toBe('(3/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=reload, selection=partial + * @effects counter_matches_checked_state + */ + it('새로고침(재마운트) 후 부분 선택 카운터가 (2/3) 이다', async () => { + const first = await mountAndRead([1, 2]); + first.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([1, 2]); + expect(text).toBe('(2/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=reload, selection=none + * @effects counter_matches_checked_state + */ + it('새로고침(재마운트) 후 미선택 카운터가 (0/3) 이다', async () => { + const first = await mountAndRead([]); + first.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([]); + expect(text).toBe('(0/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=spa_return, selection=all + * @effects counter_matches_checked_state, counter_matches_summary_panel + */ + it('SPA 이동 후 복귀(마운트 해제 → 재마운트)에도 (3/3) 이다', async () => { + const away = await mountAndRead([1, 2, 3]); + away.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([1, 2, 3]); + expect(text).toBe('(3/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=spa_return, selection=partial + * @effects counter_matches_checked_state + */ + it('SPA 복귀 + 부분 선택에서 (2/3) 이다', async () => { + const away = await mountAndRead([1, 2]); + away.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([1, 2]); + expect(text).toBe('(2/3)'); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=spa_return, selection=none + * @effects counter_matches_checked_state + */ + it('SPA 복귀 + 미선택에서 (0/3) 이다', async () => { + const away = await mountAndRead([]); + away.testUtils.cleanup(); + const { testUtils, text } = await mountAndRead([]); + expect(text).toBe('(0/3)'); + testUtils.cleanup(); + }); +}); + +describe('_cart_list.json — 빈 장바구니 (T8 에지)', () => { + /** + * 브라우저 실측에서 이 축은 기존 장바구니를 통째로 비워야만 재현되므로, + * 데이터 파괴 없이 여기서 결정적으로 고정한다. + * + * @scenario mount_path=reload, selection=none + * @effects counter_matches_checked_state + */ + it('상품이 0건이면 전체 선택 카드 자체가 렌더되지 않는다', async () => { + const testUtils = createLayoutTest(wrapperLayout as any, { + componentRegistry: registry, + initialState: { _local: { selectedItems: [], allSelected: false } }, + }); + testUtils.mockApi('cartItems', { response: cartResponse(0) }); + await testUtils.render(); + + // 감싸는 if 가 `items.length > 0` 이므로 카운터 Span 자체가 없다 + expect(screen.queryByText(/^\(\d*\/\d+\)$/)).toBeNull(); + testUtils.cleanup(); + }); + + /** + * @scenario mount_path=spa_return, selection=none + * @effects counter_matches_checked_state + */ + it('분모·분자 모두 폴백이 있어 데이터가 비어도 빈 문자열이 되지 않는다', () => { + const counter = collectStringsContaining(cartListPartial, '/{{cartItems').find((t) => + t.startsWith('(') + ); + // 값이 없을 때 나오는 문자열을 실제로 계산해 본다 (엔진과 동일한 네이티브 평가) + const exprs = [...(counter ?? '').matchAll(/\{\{([\s\S]*?)\}\}/g)].map((m) => m[1]); + expect(exprs.length).toBe(2); + const rendered = exprs.map((e) => + // eslint-disable-next-line no-new-func + new Function('_local', 'cartItems', `return (${e});`)({}, { data: {} }) + ); + expect(rendered).toEqual([0, 0]); + }); +}); + +// ========== 2. 정적 회귀 가드 ========== + +describe('_cart_list.json — 카운터 표시식 정적 가드 (#92)', () => { + const counterTexts = collectStringsContaining(cartListPartial, '/{{cartItems'); + + it('카운터 표시식이 존재한다', () => { + expect(counterTexts.length, '카운터 표시식을 찾지 못했다').toBeGreaterThan(0); + }); + + it('초기화되지 않는 _local.selectedCount 를 더 이상 참조하지 않는다', () => { + const json = JSON.stringify(cartListPartial); + expect(json).not.toContain('_local.selectedCount'); + }); + + it('선택 개수는 SSoT(selectedItems) 파생 + 0 폴백으로 표시한다', () => { + const counter = counterTexts.find((t) => t.startsWith('(')); + expect(counter, '`(선택/전체)` 형태의 카운터가 없다').toBeTruthy(); + expect(counter).toContain('_local.selectedItems?.length ?? 0'); + }); + + it('분모는 주문 요약과 동일한 item_count 폴백을 쓴다', () => { + const counter = counterTexts.find((t) => t.startsWith('(')); + expect(counter).toContain('cartItems.data.item_count ?? 0'); + }); +}); + +describe('_modal_cart_option_change.json — 재고 표시 옵셔널 가드 (B-6/B-7)', () => { + const refs = collectStringsContaining(cartOptionChangeModal, 'matchedOption'); + + /** + * @scenario mount_path=initial_load, selection=all + * @effects modal_stock_display_guarded + */ + it('matchedOption 하위 접근은 전부 옵셔널 체이닝을 쓴다', () => { + expect(refs.length).toBeGreaterThan(0); + const unguarded = refs.filter((r) => /matchedOption\.[A-Za-z_]/.test(r)); + expect( + unguarded, + `matchedOption 뒤에 '?.' 없는 역참조: ${unguarded.join(' | ')}` + ).toEqual([]); + }); +}); + +describe('_edit.json — 알림 수신 Checkbox 명시 바인딩', () => { + /** 게시판 알림 설정 체크박스 4종 */ + const NOTIFY_FIELDS = [ + 'notify_post_complete', + 'notify_post_reply', + 'notify_comment', + 'notify_reply_comment', + ]; + + /** 이름으로 Checkbox 노드를 찾는다 */ + function findCheckbox(name: string): any { + let found: any = null; + walk(profileEditPartial, (n) => { + if (n.name === 'Checkbox' && (n.props as any)?.name === name) found = n; + }); + return found; + } + + /** + * 자동바인딩은 저장값이 boolean 일 때만 checked 로 붙고, 아니면 빈 문자열을 보내 + * 저장값을 null 로 고착시킨다. 값의 타입을 화면이 전제하지 않도록 명시 바인딩을 쓴다. + * + * 마운트 경로·선택 상태 축과 무관한 정적 계약이라 @scenario 는 달지 않는다 + * (조합 커버리지는 위 9건이 이미 전수 담당한다). + * + * @effects profile_notification_checkbox_explicit_binding + */ + it.each(NOTIFY_FIELDS)('%s 가 autoBinding:false + checked + change 액션을 갖는다', (field) => { + const node = findCheckbox(field); + expect(node, `${field} Checkbox 를 찾지 못했다`).toBeTruthy(); + + expect(node.props.autoBinding).toBe(false); + expect(node.props.checked).toBe(`{{!!_local.profileEditForm?.${field}}}`); + + const change = (node.actions ?? []).find((a: any) => a.type === 'change'); + expect(change, `${field} 에 change 액션이 없다`).toBeTruthy(); + expect(change.handler).toBe('setState'); + expect(change.params.target).toBe('local'); + expect(change.params[`profileEditForm.${field}`]).toBe('{{$event.target.checked}}'); + }); +}); + +describe('_edit.json — 프로필 오류 메시지 옵셔널 가드 (회색지대)', () => { + const refs = collectStringsContaining(profileEditPartial, 'profileErrors'); + + /** + * @scenario mount_path=initial_load, selection=none + * @effects profile_error_display_guarded + */ + it('profileErrors 하위 접근은 전부 옵셔널 체이닝을 쓴다', () => { + expect(refs.length).toBeGreaterThan(0); + const unguarded = refs.filter((r) => /profileErrors\.[A-Za-z_]/.test(r)); + expect( + unguarded, + `profileErrors 뒤에 '?.' 없는 역참조: ${unguarded.join(' | ')}` + ).toEqual([]); + }); + + /** + * @scenario mount_path=initial_load, selection=partial + * @effects profile_error_display_guarded + */ + it('오류 메시지 표시식은 배열 인덱스에도 옵셔널 + 빈 문자열 폴백을 쓴다', () => { + const messageTexts = refs.filter((r) => /profileErrors\??\.[A-Za-z_]+\??\.\[0\]/.test(r)); + expect(messageTexts.length, '오류 메시지 표시식을 찾지 못했다').toBeGreaterThan(0); + for (const t of messageTexts) { + expect(t, `폴백 없는 오류 메시지 표시식: ${t}`).toContain("?? ''"); + } + }); +}); diff --git a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart.test.tsx b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart.test.tsx index e0cb439d..74cc26ec 100644 --- a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart.test.tsx +++ b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart.test.tsx @@ -201,7 +201,7 @@ const cartLayoutFixture = { type: 'basic', name: 'Span', props: { className: 'text-gray-500', 'data-testid': 'selected-count' }, - text: '({{_local.selectedCount ?? 0}}/{{cartItems.data.items?.length ?? 0}})', + text: '({{_local.selectedItems?.length ?? 0}}/{{cartItems.data.item_count ?? 0}})', }, ], }, @@ -605,7 +605,7 @@ describe('장바구니 페이지 레이아웃 렌더링', () => { const testUtils = createLayoutTest(cartLayoutFixture, { componentRegistry: registry, initialState: { - _local: { selectedItems: [1, 2], allSelected: true, selectedCount: 2 }, + _local: { selectedItems: [1, 2], allSelected: true }, }, }); @@ -643,7 +643,7 @@ describe('장바구니 페이지 레이아웃 렌더링', () => { const testUtils = createLayoutTest(cartLayoutFixture, { componentRegistry: registry, initialState: { - _local: { selectedItems: [1], allSelected: false, selectedCount: 1 }, + _local: { selectedItems: [1], allSelected: false }, }, }); @@ -680,7 +680,7 @@ describe('장바구니 페이지 레이아웃 렌더링', () => { const testUtils = createLayoutTest(cartLayoutFixture, { componentRegistry: registry, initialState: { - _local: { selectedItems: [1, 2], allSelected: true, selectedCount: 2 }, + _local: { selectedItems: [1, 2], allSelected: true }, }, }); @@ -717,7 +717,7 @@ describe('장바구니 페이지 레이아웃 렌더링', () => { const testUtils = createLayoutTest(cartLayoutFixture, { componentRegistry: registry, initialState: { - _local: { selectedItems: [], allSelected: false, selectedCount: 0 }, + _local: { selectedItems: [], allSelected: false }, }, }); diff --git a/templates/_bundled/sirsoft-basic/src/__tests__/layouts/search-failed-state.test.tsx b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/search-failed-state.test.tsx new file mode 100644 index 00000000..a667c00c --- /dev/null +++ b/templates/_bundled/sirsoft-basic/src/__tests__/layouts/search-failed-state.test.tsx @@ -0,0 +1,422 @@ +/** + * @file search-failed-state.test.tsx + * @description 통합검색 실패 표면화 렌더링 테스트 (공개 이슈 #103) + * + * 카테고리 검색이 예외로 실패하면(`categories_failed` / `search_failed`) 화면은 + * "검색 결과가 없습니다" 가 아니라 "검색 중 오류" 안내를 그려야 한다. + * + * 실제 레이아웃 JSON 을 import 해서 렌더한다 — 손으로 쓴 조각만 검증하면 실파일이 + * 규약을 어겨도 green 인 채로 유출된다. 부재 단언은 baseline 케이스에서 그 문구의 + * 존재를 먼저 확정한 뒤에만 의미를 갖는다. + * + * @vitest-environment jsdom + */ + +import React from 'react'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { + createLayoutTest, + screen, +} from '@/core/template-engine/__tests__/utils/layoutTestUtils'; +import { ComponentRegistry } from '@/core/template-engine/ComponentRegistry'; + +// 실제 레이아웃 JSON — failed 분기 회귀 고정 +import searchResultsPartial from '../../../layouts/partials/search/_search_results.json'; +import searchStatesPartial from '../../../layouts/partials/search/_search_states.json'; +import postsSectionPartial from '../../../layouts/partials/search/posts/_section.json'; +import productsSectionPartial from '../../../layouts/partials/search/products/_section.json'; +import pagesSectionPartial from '../../../layouts/partials/search/pages/_section.json'; + +// 실제 ko 언어 파일 — 문구 단언이 실키와 어긋나면 red +import koSearch from '../../../lang/partial/ko/search.json'; + +// ========== 테스트용 컴포넌트 정의 ========== + +const TestDiv: React.FC<{ className?: string; children?: React.ReactNode }> = ({ className, children }) => ( + <div className={className}>{children}</div> +); +const TestSpan: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = ({ children, text }) => ( + <span>{children ?? text}</span> +); +const TestP: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = ({ children, text }) => ( + <p>{children ?? text}</p> +); +const TestH3: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = ({ children, text }) => ( + <h3>{children ?? text}</h3> +); +const TestButton: React.FC<{ className?: string; children?: React.ReactNode; text?: string }> = ({ children, text }) => ( + <button type="button">{children ?? text}</button> +); +const TestIcon: React.FC<{ name?: string }> = ({ name }) => <i data-icon={name} />; +const TestHtmlContent: React.FC<{ content?: string }> = ({ content }) => ( + <div dangerouslySetInnerHTML={{ __html: content ?? '' }} /> +); +const TestFragment: React.FC<{ children?: React.ReactNode }> = ({ children }) => <>{children}</>; + +function setupTestRegistry(): ComponentRegistry { + const registry = ComponentRegistry.getInstance(); + (registry as any).registry = { + Fragment: { component: TestFragment, metadata: { name: 'Fragment', type: 'layout' } }, + Div: { component: TestDiv, metadata: { name: 'Div', type: 'basic' } }, + Span: { component: TestSpan, metadata: { name: 'Span', type: 'basic' } }, + P: { component: TestP, metadata: { name: 'P', type: 'basic' } }, + H3: { component: TestH3, metadata: { name: 'H3', type: 'basic' } }, + H4: { component: TestH3, metadata: { name: 'H4', type: 'basic' } }, + Ul: { component: TestDiv, metadata: { name: 'Ul', type: 'basic' } }, + Li: { component: TestDiv, metadata: { name: 'Li', type: 'basic' } }, + Button: { component: TestButton, metadata: { name: 'Button', type: 'basic' } }, + Icon: { component: TestIcon, metadata: { name: 'Icon', type: 'basic' } }, + HtmlContent: { component: TestHtmlContent, metadata: { name: 'HtmlContent', type: 'composite' } }, + }; + return registry; +} + +// ========== partial 참조 인라인 해석 ========== + +/** + * 서버측 partial 해석을 테스트에서 재현합니다. + * + * `{ "partial": "..." }` 노드를 매핑된 실제 JSON 으로 치환하고, 매핑이 없는 참조는 + * 빈 Div 로 둡니다(이 테스트의 단언 대상 분기가 아니라는 뜻). + * + * @param node 레이아웃 JSON 노드 + * @param map partial 경로 => 실제 JSON + * @returns 치환된 사본 + */ +function resolvePartials(node: unknown, map: Record<string, unknown>): unknown { + if (Array.isArray(node)) { + return node.map((n) => resolvePartials(n, map)); + } + if (!node || typeof node !== 'object') return node; + const obj = node as Record<string, unknown>; + if (typeof obj.partial === 'string') { + const target = map[obj.partial]; + return target + ? resolvePartials(JSON.parse(JSON.stringify(target)), map) + : { type: 'basic', name: 'Div' }; + } + const out: Record<string, unknown> = {}; + for (const [key, value] of Object.entries(obj)) { + out[key] = resolvePartials(value, map); + } + return out; +} + +const PARTIAL_MAP: Record<string, unknown> = { + 'partials/search/posts/_section.json': postsSectionPartial, + 'partials/search/products/_section.json': productsSectionPartial, + 'partials/search/pages/_section.json': pagesSectionPartial, +}; + +/** + * 실제 partial JSON 을 렌더 가능한 레이아웃으로 감쌉니다. + * + * @param partial 레이아웃 partial JSON + * @returns 레이아웃 정의 + */ +function wrapLayout(partial: unknown) { + return { + version: '1.0.0', + layout_name: 'search_failed_probe', + state: {}, + components: [resolvePartials(JSON.parse(JSON.stringify(partial)), PARTIAL_MAP)], + }; +} + +// ========== 데이터 헬퍼 ========== + +const FAILED_TITLE = (koSearch as any).failed?.title ?? '$t:search.failed.title'; + +/** + * 검색 응답 데이터를 만듭니다. + * + * @param overrides 덮어쓸 필드 + * @returns searchResults 데이터소스 값 + */ +function searchData(overrides: Record<string, unknown> = {}) { + return { + searchResults: { + data: { + q: '문의', + total: 0, + posts_count: 0, + products_count: 0, + pages_count: 0, + posts: { items: [] }, + products: [], + pages: { items: [] }, + categories_failed: { posts: false, products: false, pages: false }, + search_failed: false, + ...overrides, + }, + }, + }; +} + +const RENDER_OPTIONS = (queryParams: Record<string, string>, data: Record<string, unknown>) => ({ + componentRegistry: ComponentRegistry.getInstance(), + queryParams, + initialData: data, + translations: { search: koSearch }, +}); + +// ========== 테스트 케이스 ========== + +describe('통합검색 실패 표면화 렌더링 (#103)', () => { + beforeEach(() => { + setupTestRegistry(); + }); + + describe('_search_results.json 카테고리 탭 분기', () => { + const tabCases: Array<[string, string, string]> = [ + ['posts', 'posts', (koSearch as any).empty.posts], + ['products', 'products', (koSearch as any).empty.products], + ['pages', 'pages', (koSearch as any).empty.pages], + ]; + + it.each(tabCases)( + '%s 탭: 실패가 아니면 기존 "결과 없음" 문구가 렌더된다 (baseline)', + async (_label, type, emptyText) => { + const testUtils = createLayoutTest(wrapLayout(searchResultsPartial), RENDER_OPTIONS( + { q: '문의', type }, + searchData() + )); + + await testUtils.render(); + + // 존재 확정 — 이 문구가 없으면 아래 실패 케이스의 부재 단언이 무의미하다 + expect(screen.getByText(emptyText)).toBeInTheDocument(); + expect(screen.queryByText(FAILED_TITLE)).not.toBeInTheDocument(); + + testUtils.cleanup(); + } + ); + + /** + * @scenario category=posts, scope=category_tab, failure=single + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=products, scope=category_tab, failure=single + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=pages, scope=category_tab, failure=single + * @effects error_notice_instead_of_empty + */ + it.each(tabCases)( + '%s 탭: 카테고리 실패 시 "결과 없음" 대신 오류 안내가 렌더된다', + async (_label, type, emptyText) => { + const testUtils = createLayoutTest(wrapLayout(searchResultsPartial), RENDER_OPTIONS( + { q: '문의', type }, + searchData({ + categories_failed: { posts: false, products: false, pages: false, [type]: true }, + search_failed: true, + }) + )); + + await testUtils.render(); + + expect(screen.getByText(FAILED_TITLE)).toBeInTheDocument(); + expect(screen.queryByText(emptyText)).not.toBeInTheDocument(); + + testUtils.cleanup(); + } + ); + + /** + * @scenario category=posts, scope=category_tab, failure=all + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=products, scope=category_tab, failure=all + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=pages, scope=category_tab, failure=all + * @effects error_notice_instead_of_empty + */ + it.each(tabCases)( + '%s 탭: 전 카테고리 실패 시에도 그 탭이 오류 안내를 렌더한다', + async (_label, type, emptyText) => { + const testUtils = createLayoutTest(wrapLayout(searchResultsPartial), RENDER_OPTIONS( + { q: '문의', type }, + searchData({ + categories_failed: { posts: true, products: true, pages: true }, + search_failed: true, + }) + )); + + await testUtils.render(); + + expect(screen.getByText(FAILED_TITLE)).toBeInTheDocument(); + expect(screen.queryByText(emptyText)).not.toBeInTheDocument(); + + testUtils.cleanup(); + } + ); + }); + + describe('전체 탭 섹션 (partial 인라인 해석)', () => { + const sectionCases: Array<[string, string, string]> = [ + // [실패 카테고리, 그 카테고리의 in_all 빈 문구 키, 정상 렌더를 확인할 다른 카테고리의 빈 문구 키] + ['posts', 'posts_in_all', 'pages_in_all'], + ['products', 'products_in_all', 'pages_in_all'], + ['pages', 'pages_in_all', 'posts_in_all'], + ]; + + /** + * @scenario category=posts, scope=all_tab, failure=single + * @effects error_notice_instead_of_empty, unaffected_category_renders_normally + */ + /** + * @scenario category=products, scope=all_tab, failure=single + * @effects error_notice_instead_of_empty, unaffected_category_renders_normally + */ + /** + * @scenario category=pages, scope=all_tab, failure=single + * @effects error_notice_instead_of_empty, unaffected_category_renders_normally + */ + it.each(sectionCases)( + '%s 섹션만 실패하면 그 섹션만 오류 안내를 그리고 다른 섹션은 정상 렌더된다', + async (failedCategory, failedEmptyKey, normalEmptyKey) => { + const testUtils = createLayoutTest(wrapLayout(searchResultsPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData({ + total: 3, + products_count: failedCategory === 'products' ? 0 : 3, + categories_failed: { posts: false, products: false, pages: false, [failedCategory]: true }, + search_failed: true, + }) + )); + + await testUtils.render(); + + // 실패 섹션: 오류 안내 (in_all 빈 문구 대신) + expect(screen.getByText(FAILED_TITLE)).toBeInTheDocument(); + expect(screen.queryByText((koSearch as any).empty[failedEmptyKey])).not.toBeInTheDocument(); + // 실패하지 않은 섹션은 종전대로 빈 문구를 그린다 + expect(screen.getByText((koSearch as any).empty[normalEmptyKey])).toBeInTheDocument(); + + testUtils.cleanup(); + } + ); + + it('실패가 없으면 posts 섹션은 종전대로 빈 문구를 그린다 (baseline)', async () => { + const testUtils = createLayoutTest(wrapLayout(searchResultsPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData({ total: 3, products_count: 3 }) + )); + + await testUtils.render(); + + expect(screen.getByText((koSearch as any).empty.posts_in_all)).toBeInTheDocument(); + expect(screen.queryByText(FAILED_TITLE)).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); + }); + + describe('_search_states.json 전체 탭', () => { + it('전체 탭 0건 + 실패 없음이면 "검색 결과가 없습니다" 를 그린다 (baseline)', async () => { + const testUtils = createLayoutTest(wrapLayout(searchStatesPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData() + )); + + await testUtils.render(); + + expect(screen.getByText((koSearch as any).empty.all)).toBeInTheDocument(); + expect(screen.queryByText(FAILED_TITLE)).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); + + /** + * @scenario category=posts, scope=all_tab, failure=all + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=products, scope=all_tab, failure=all + * @effects error_notice_instead_of_empty + */ + /** + * @scenario category=pages, scope=all_tab, failure=all + * @effects error_notice_instead_of_empty + */ + it('전체 탭 0건 + 실패 존재면 "결과 없음" 대신 오류 안내를 그린다', async () => { + const testUtils = createLayoutTest(wrapLayout(searchStatesPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData({ + categories_failed: { posts: true, products: true, pages: true }, + search_failed: true, + }) + )); + + await testUtils.render(); + + expect(screen.getByText(FAILED_TITLE)).toBeInTheDocument(); + expect(screen.queryByText((koSearch as any).empty.all)).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); + + /** + * 실패 페이로드는 "정확한 0건" 이라고 말하지 않으려고 total_is_exact=false 를 싣는다. + * 그런데 그 값은 원래 "총 건수 상한 초과" 신호이기도 해서, 가드가 없으면 실패 화면에 + * "검색어를 더 구체적으로 입력하면 정확한 건수를 볼 수 있습니다" 라는 상한 초과용 + * 조치 안내가 오류 안내와 나란히 렌더된다 — 검색어를 좁혀도 서버 오류는 해소되지 않으므로 + * 사용자에게 잘못된 조치를 지시하게 된다. + * + * @scenario category=posts, scope=all_tab, failure=all + * @effects error_notice_instead_of_empty + */ + it('전체 탭 실패 시 상한 초과용 "검색어를 좁히세요" 안내를 그리지 않는다', async () => { + const refineHint = (koSearch as any).refine_query_hint; + expect(refineHint).toBeTruthy(); + + // 존재 확정: 실패가 아닌 상한 초과 상황에서는 그 안내가 실제로 렌더된다. + const exceeded = createLayoutTest(wrapLayout(searchStatesPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData({ total: 10000, total_is_exact: false }) + )); + await exceeded.render(); + expect(screen.getByText(refineHint)).toBeInTheDocument(); + exceeded.cleanup(); + + // 실패 상황에서는 같은 안내가 사라져야 한다. + const failed = createLayoutTest(wrapLayout(searchStatesPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + searchData({ + total_is_exact: false, + categories_failed: { posts: true, products: true, pages: true }, + search_failed: true, + }) + )); + await failed.render(); + + expect(screen.getByText(FAILED_TITLE)).toBeInTheDocument(); + expect(screen.queryByText(refineHint)).not.toBeInTheDocument(); + + failed.cleanup(); + }); + + it('실패 키가 없는 구버전 응답에서는 종전 렌더가 유지된다 (하위호환)', async () => { + const data = searchData(); + delete (data.searchResults as any).data.categories_failed; + delete (data.searchResults as any).data.search_failed; + + const testUtils = createLayoutTest(wrapLayout(searchStatesPartial), RENDER_OPTIONS( + { q: '문의', type: 'all' }, + data + )); + + await testUtils.render(); + + expect(screen.getByText((koSearch as any).empty.all)).toBeInTheDocument(); + expect(screen.queryByText(FAILED_TITLE)).not.toBeInTheDocument(); + + testUtils.cleanup(); + }); + }); +}); diff --git a/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx b/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx index 13ff954d..642f6a04 100644 --- a/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx +++ b/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/SortableThumbnailItem.tsx @@ -19,6 +19,7 @@ import { Img } from '../../basic/Img'; import type { Attachment, PendingFile } from './types'; import { getFileIcon, t } from './utils'; +import { isCrossOriginAssetUrl } from '../assetOrigin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const G7Core = (window as any).G7Core; @@ -79,6 +80,13 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ return; } + // 공개 자산 디스크(S3/CDN)가 준 교차 출처 URL 은 인증이 필요 없는 공개 자산이다. + // XHR 로 가져오면 CORS 미설정 CDN 에서 실패하므로 URL 을 그대로 사용한다. + if (isCrossOriginAssetUrl(downloadUrl)) { + setAuthenticatedImageUrl(downloadUrl); + return; + } + let isMounted = true; let objectUrl: string | undefined; @@ -148,9 +156,9 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ {/* 업로드/압축 진행률 오버레이 */} {(isUploading || isCompressing) && ( <Div className="absolute inset-0 bg-black/50 flex flex-col items-center justify-center"> - <Div className="w-3/4 h-2 bg-gray-700 rounded-full overflow-hidden"> + <Div className="w-3/4 h-2 bg-gray-700 dark:bg-gray-600 rounded-full overflow-hidden"> <Div - className="h-full bg-blue-500 transition-all" + className="h-full bg-blue-500 dark:bg-blue-400 transition-all" style={{ width: `${progress}%` }} /> </Div> @@ -160,7 +168,7 @@ export const SortableThumbnailItem: React.FC<SortableThumbnailItemProps> = ({ {/* 에러 오버레이 */} {hasError && ( - <Div className="absolute inset-0 bg-red-500/50 flex items-center justify-center"> + <Div className="absolute inset-0 bg-red-500/50 dark:bg-red-600/50 flex items-center justify-center"> <I className="fa-solid fa-exclamation-triangle text-2xl text-white" /> </Div> )} diff --git a/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/useFileUploader.ts b/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/useFileUploader.ts index e4aec392..a4df2649 100644 --- a/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/useFileUploader.ts +++ b/templates/_bundled/sirsoft-basic/src/components/composite/FileUploader/useFileUploader.ts @@ -15,6 +15,7 @@ import imageCompression from 'browser-image-compression'; import type { Attachment, PendingFile, FileUploaderProps, ApiEndpoints } from './types'; import { formatFileSize, extractErrorMessage, t } from './utils'; +import { isCrossOriginAssetUrl } from '../assetOrigin'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const G7Core = (window as any).G7Core; @@ -704,6 +705,14 @@ export function useFileUploader(options: UseFileUploaderOptions): UseFileUploade // ref 기반 has 체크 (stale closure 방지) if (authenticatedImageUrlsRef.current.has(file.id)) continue; + // 공개 자산 디스크(S3/CDN)의 교차 출처 URL 은 공개 자산이므로 XHR 없이 직접 사용한다. + // (CORS 미설정 CDN 에서의 실패 방지 + 제3자 origin 으로의 토큰 노출 차단) + if (isCrossOriginAssetUrl(file.download_url)) { + authenticatedImageUrlsRef.current.set(file.id, file.download_url); + setAuthenticatedImageUrls(new Map(authenticatedImageUrlsRef.current)); + continue; + } + try { const blob = await G7Core.api.get(file.download_url, { responseType: 'blob', diff --git a/templates/_bundled/sirsoft-basic/src/components/composite/ImageGallery.tsx b/templates/_bundled/sirsoft-basic/src/components/composite/ImageGallery.tsx index e9999995..d01c6bd3 100644 --- a/templates/_bundled/sirsoft-basic/src/components/composite/ImageGallery.tsx +++ b/templates/_bundled/sirsoft-basic/src/components/composite/ImageGallery.tsx @@ -20,6 +20,7 @@ import 'yet-another-react-lightbox/plugins/thumbnails.css'; import { Button } from '../basic/Button'; import { I } from '../basic/I'; +import { isCrossOriginAssetUrl } from './assetOrigin'; import { Div } from '../basic/Div'; import { Img } from '../basic/Img'; import type { EditorAttrs } from '../../types'; @@ -134,7 +135,9 @@ export const executeImageDownload = async (image: GalleryImage): Promise<void> = const downloadUrl = image.downloadUrl || image.src; const filename = image.filename || image.title || 'image'; - if (image.downloadRequiresAuth) { + // 공개 자산 디스크(S3/CDN)의 교차 출처 URL 은 인증이 필요 없는 공개 자산이다. + // 인증 XHR 로 가져오면 CORS 미설정 CDN 에서 실패하므로 일반 링크 다운로드를 쓴다. + if (image.downloadRequiresAuth && !isCrossOriginAssetUrl(downloadUrl)) { await downloadAuthenticatedFile(downloadUrl, filename); } else { downloadFile(downloadUrl, filename); diff --git a/templates/_bundled/sirsoft-basic/src/components/composite/__tests__/Footer.responsive.test.tsx b/templates/_bundled/sirsoft-basic/src/components/composite/__tests__/Footer.responsive.test.tsx index 6a76c32e..5cdbf465 100644 --- a/templates/_bundled/sirsoft-basic/src/components/composite/__tests__/Footer.responsive.test.tsx +++ b/templates/_bundled/sirsoft-basic/src/components/composite/__tests__/Footer.responsive.test.tsx @@ -41,6 +41,7 @@ describe('Footer — G7 표준 responsive 분기', () => { vi.clearAllMocks(); }); + /** @scenario preview_device=desktop, custom_width_input=in_range */ it('데스크톱(1280) — 링크 그룹 그리드 5열', () => { setWidth(1280); const { container } = render(<Footer />); @@ -49,6 +50,7 @@ describe('Footer — G7 표준 responsive 분기', () => { expect(grid.style.gridTemplateColumns).toBe('repeat(5, minmax(0, 1fr))'); }); + /** @scenario preview_device=tablet, custom_width_input=in_range */ it('태블릿(820) — 그리드 2열', () => { setWidth(820); const { container } = render(<Footer />); @@ -56,6 +58,7 @@ describe('Footer — G7 표준 responsive 분기', () => { expect(grid.style.gridTemplateColumns).toBe('repeat(2, minmax(0, 1fr))'); }); + /** @scenario preview_device=mobile, custom_width_input=in_range */ it('모바일(390) — 그리드 1열 + 저작권 영역 세로(column)', () => { setWidth(390); const { container } = render(<Footer />); diff --git a/templates/_bundled/sirsoft-basic/src/components/composite/assetOrigin.ts b/templates/_bundled/sirsoft-basic/src/components/composite/assetOrigin.ts new file mode 100644 index 00000000..62679c79 --- /dev/null +++ b/templates/_bundled/sirsoft-basic/src/components/composite/assetOrigin.ts @@ -0,0 +1,38 @@ +/** + * 자산 URL 출처 판정 유틸리티 + * + * 공개 자산 스토리지(S3/CDN)를 켜면 첨부·이미지의 `download_url` 이 외부 origin + * 절대 URL 이 된다. 이런 URL 은 인증이 필요 없는 공개 자산이므로 XHR(Blob) 로 + * 가져오면 안 된다 — 교차 출처 XHR 은 CDN 이 CORS 헤더를 주지 않는 한 응답을 + * 읽지 못해 이미지가 통째로 실패하고, 응답을 읽을 수 있는 CDN 이라면 이번에는 + * 세션 토큰이 제3자 origin 으로 나간다. 교차 출처 URL 은 `<img src>` / 링크로 + * 직접 사용한다. + * + * @module composite/assetOrigin + */ + +/** + * URL 이 현재 문서와 다른 출처인지 판정합니다. + * + * 상대 경로는 항상 동일 출처입니다. 절대 URL(`https://…`)과 프로토콜 상대 + * URL(`//host/…`)만 출처를 비교하며, 파싱 불가하거나 브라우저 밖이면 + * 동일 출처로 간주합니다(기존 인증 경로 유지). + * + * @param url 판정할 URL + * @returns 교차 출처 여부 + */ +export const isCrossOriginAssetUrl = (url?: string | null): boolean => { + if (!url || typeof window === 'undefined' || !window.location) { + return false; + } + + if (!/^(https?:)?\/\//i.test(url)) { + return false; + } + + try { + return new URL(url, window.location.href).origin !== window.location.origin; + } catch { + return false; + } +}; diff --git a/templates/_bundled/sirsoft-basic/src/handlers/__tests__/cartHandlers.test.ts b/templates/_bundled/sirsoft-basic/src/handlers/__tests__/cartHandlers.test.ts index 0b192970..2c7c53d5 100644 --- a/templates/_bundled/sirsoft-basic/src/handlers/__tests__/cartHandlers.test.ts +++ b/templates/_bundled/sirsoft-basic/src/handlers/__tests__/cartHandlers.test.ts @@ -117,8 +117,14 @@ describe('cartHandlers', () => { })); }); - // #131: 선택 변경 후 selectedCount 업데이트 - it('#131 선택 변경 후 selectedCount가 업데이트된다', () => { + /** + * #131: 선택 개수는 selectedItems 파생 — 별도 카운터를 기록하지 않는다 (공개 이슈 #92). + * 화면 카운터가 파생해 쓰는 SSoT 를 핸들러가 실제로 갱신하는지 고정한다. + * + * @scenario mount_path=initial_load, selection=partial + * @effects counter_matches_checked_state + */ + it('#131 선택 변경 시 selectedItems 만 갱신하고 selectedCount 는 기록하지 않는다', () => { // Given mockG7Core.state.get.mockReturnValue({ cartItems: { @@ -137,10 +143,14 @@ describe('cartHandlers', () => { // When toggleCartItemSelectionHandler(action); - // Then - expect(mockG7Core.state.setLocal).toHaveBeenCalledWith(expect.objectContaining({ - selectedCount: 1, - })); + // Then: 선택 목록이 SSoT — 화면 카운터는 여기서 파생한다 + expect(mockG7Core.state.setLocal).toHaveBeenCalledWith({ + selectedItems: [2], + allSelected: false, + }); + // 파생 가능한 값을 별도 키로 중복 저장하면 최초 마운트 구간에서 비어 보인다 (#92) + const payload = mockG7Core.state.setLocal.mock.calls[0][0]; + expect(payload).not.toHaveProperty('selectedCount'); }); }); @@ -162,7 +172,6 @@ describe('cartHandlers', () => { expect(mockG7Core.state.setLocal).toHaveBeenCalledWith({ selectedItems: [], allSelected: false, - selectedCount: 0, }); }); @@ -183,7 +192,6 @@ describe('cartHandlers', () => { expect(mockG7Core.state.setLocal).toHaveBeenCalledWith({ selectedItems: [1, 2, 3], allSelected: true, - selectedCount: 3, }); }); }); diff --git a/templates/_bundled/sirsoft-basic/src/handlers/__tests__/redirectToLoginWithReturn.test.ts b/templates/_bundled/sirsoft-basic/src/handlers/__tests__/redirectToLoginWithReturn.test.ts index 27b87132..335e2cbc 100644 --- a/templates/_bundled/sirsoft-basic/src/handlers/__tests__/redirectToLoginWithReturn.test.ts +++ b/templates/_bundled/sirsoft-basic/src/handlers/__tests__/redirectToLoginWithReturn.test.ts @@ -6,8 +6,10 @@ * G7Core.dispatch 로 navigate 를 발화한다. errorHandling context 에 route/query 가 없는 * 제약을 우회하는 핵심 동작이므로 실제 호출 인자를 단언한다(거짓 통과 방지). * - * @scenario has_query:with_query,without_query - * @effects redirect_preserves_pathname,redirect_preserves_query_string + * 이 핸들러는 세 진입점이 공유하므로 특정 entry_point 축에 매이지 않는다 — + * query 유무(with/without) 양쪽을 여기서 한 번 실측하고 effects 로만 마킹한다. + * + * @effects redirect_preserves_pathname, redirect_preserves_query_string */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; diff --git a/templates/_bundled/sirsoft-basic/src/handlers/cartHandlers.ts b/templates/_bundled/sirsoft-basic/src/handlers/cartHandlers.ts index 4431cf3c..7aec6e61 100644 --- a/templates/_bundled/sirsoft-basic/src/handlers/cartHandlers.ts +++ b/templates/_bundled/sirsoft-basic/src/handlers/cartHandlers.ts @@ -126,10 +126,11 @@ export function toggleCartItemSelectionHandler( const allItems = globalState.cartItems?.data?.items || []; const allSelected = allItems.length > 0 && newSelected.length === allItems.length; + // 선택 개수는 selectedItems 에서 파생해 표시하므로 별도 카운터를 기록하지 않는다. + // (카운터를 따로 두면 최초 마운트처럼 핸들러가 아직 실행되지 않은 구간에서 값이 비어 있다 — 공개 이슈 #92) setLocalState({ selectedItems: newSelected, allSelected, - selectedCount: newSelected.length, }); // 선택 변경 후 계산 결과 갱신을 위해 데이터소스 재조회 @@ -151,10 +152,10 @@ export function selectAllCartItemsHandler( ): void { const { allItemIds, isSelected } = action?.params || {}; + // 선택 개수는 selectedItems 파생 (별도 카운터 미기록 — 공개 이슈 #92) setLocalState({ selectedItems: isSelected ? allItemIds : [], allSelected: isSelected, - selectedCount: isSelected ? allItemIds.length : 0, }); // 선택 변경 후 계산 결과 갱신을 위해 데이터소스 재조회 diff --git a/templates/_bundled/sirsoft-basic/template.json b/templates/_bundled/sirsoft-basic/template.json index 3f8b4051..6930220e 100644 --- a/templates/_bundled/sirsoft-basic/template.json +++ b/templates/_bundled/sirsoft-basic/template.json @@ -5,7 +5,7 @@ "ko": "Basic", "en": "Basic" }, - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "description": { "ko": "그누보드7 기본 사용자 템플릿", diff --git a/templates/_bundled/sirsoft-basic/tests/Playwright/specs/search-category-failure.spec.ts b/templates/_bundled/sirsoft-basic/tests/Playwright/specs/search-category-failure.spec.ts new file mode 100644 index 00000000..69d06f8a --- /dev/null +++ b/templates/_bundled/sirsoft-basic/tests/Playwright/specs/search-category-failure.spec.ts @@ -0,0 +1,149 @@ +/** + * 통합검색 카테고리 실패 표면화 — 브라우저 렌더 검증 (공개 이슈 #103) + * + * 배경: + * - 카테고리 검색이 서버에서 예외로 실패하면 종전에는 HTTP 200 + "검색 결과가 없습니다" + * 로 위장됐다. 수정 후에는 응답의 `categories_failed`/`search_failed` 를 근거로 + * 화면이 "검색 중 오류" 안내를 그려야 한다. + * - 라이브 DB 에 임의 예외를 만들지 않는다 — `page.route` 로 실패 응답을 주입한다. + * (인덱스 부재 축은 LIKE 폴백으로 흡수되므로 Chrome MCP 라이브 시나리오가 담당) + * + * 축 마킹은 아래 태그에 `키=값, 키=값` 한 줄로 적는다 — 매니페스트 대조기가 읽는 + * 형식이 그것뿐이라, 다른 이름의 태그에 적으면 조합이 조용히 0건으로 집계된다. + * 설명문에도 그 태그 이름을 리터럴로 쓰지 않는다(docblock 의 첫 매치를 가로챈다). + * 아래 파일 단위 마킹이 기본축이고, 개별 test 의 라인 마킹이 나머지 조합을 채운다. + * + * @scenario category=posts, scope=all_tab, failure=single + * @effects failed_flag_in_response, + * error_notice_instead_of_empty, + * unaffected_category_renders_normally + */ +import { test, expect, type Page } from '@playwright/test'; + +/** 실패 안내 문구 (ko/en — 사이트 로케일에 무관하게 매칭) */ +const FAILED_TITLE = /검색 중 오류가 발생했습니다|An error occurred while searching/; + +/** "결과 없음" 문구 (ko/en) */ +const EMPTY_ALL = /^검색 결과가 없습니다\.$|^No results found\.$/; + +/** + * 검색 응답 데이터 골격을 만듭니다 (buildResponse 가 조립하는 실제 키 집합의 부분집합). + * + * @param overrides 덮어쓸 필드 + * @returns 응답 data 값 + */ +function searchData(overrides: Record<string, unknown> = {}): Record<string, unknown> { + return { + q: '문의', + total: 0, + all_count: 0, + all_count_is_exact: true, + total_relation: 'exact', + total_is_exact: true, + result_cap: 10000, + posts_count: 0, + products_count: 0, + pages_count: 0, + posts: { items: [], available_boards: [] }, + products: [], + pages: { items: [] }, + counts_are_exact: { posts: true, products: true, pages: true }, + categories_failed: { posts: false, products: false, pages: false }, + search_failed: false, + ...overrides, + }; +} + +/** + * `/api/search` 응답을 fixture 로 대체합니다. + * + * @param page Playwright 페이지 + * @param data 응답 data 값 + */ +async function mockSearchResponse(page: Page, data: Record<string, unknown>): Promise<void> { + await page.route('**/api/search**', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ success: true, message: 'ok', data }), + }); + }); +} + +test.describe('통합검색 카테고리 실패 표면화 (#103)', () => { + test('posts 만 실패한 전체 탭 — posts 섹션은 오류 안내, 다른 카테고리는 정상 렌더', async ({ page }) => { + await mockSearchResponse(page, searchData({ + total: 1, + all_count: 1, + pages_count: 1, + pages: { + items: [{ + id: 1, + title: '이용약관', + title_highlighted: '이용약관', + content_preview: '본 약관은 문의 안내를 포함합니다', + content_preview_highlighted: '본 약관은 문의 안내를 포함합니다', + published_at: '2026-02-01', + url: '/page/terms', + }], + }, + counts_are_exact: { posts: false, products: true, pages: true }, + categories_failed: { posts: true, products: false, pages: false }, + search_failed: true, + })); + + await page.goto('/search?q=문의'); + + // posts 섹션: 오류 안내가 렌더된다 ("결과 없음" 이 아니라) + await expect(page.getByText(FAILED_TITLE).first()).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(/게시글 검색 결과가 없습니다|No posts found/)).toHaveCount(0); + + // 실패하지 않은 pages 카테고리는 결과를 정상 렌더한다 + // ('이용약관' 은 사이트 네비의 페이지 링크와도 매칭되므로 고유한 미리보기 문구로 단언) + await expect(page.getByText('본 약관은 문의 안내를 포함합니다').first()).toBeVisible(); + }); + + // @scenario category=posts, scope=category_tab, failure=single + // @effects failed_flag_in_response, error_notice_instead_of_empty + test('posts 탭 실패 — 탭 화면이 "결과 없음" 대신 오류 안내를 그린다', async ({ page }) => { + await mockSearchResponse(page, searchData({ + counts_are_exact: { posts: false, products: true, pages: true }, + categories_failed: { posts: true, products: false, pages: false }, + search_failed: true, + })); + + await page.goto('/search?q=문의&type=posts'); + + await expect(page.getByText(FAILED_TITLE).first()).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(/게시글 검색 결과가 없습니다|No posts found/)).toHaveCount(0); + }); + + // 이 한 케이스가 세 카테고리 동시 실패를 덮으므로 카테고리 축을 각각 마킹한다. + // @scenario category=posts, scope=all_tab, failure=all + // @effects failed_flag_in_response, error_notice_instead_of_empty + // @scenario category=products, scope=all_tab, failure=all + // @effects failed_flag_in_response, error_notice_instead_of_empty + // @scenario category=pages, scope=all_tab, failure=all + // @effects failed_flag_in_response, error_notice_instead_of_empty + test('전 카테고리 실패한 전체 탭 — 전역 오류 안내를 그린다', async ({ page }) => { + await mockSearchResponse(page, searchData({ + counts_are_exact: { posts: false, products: false, pages: false }, + categories_failed: { posts: true, products: true, pages: true }, + search_failed: true, + })); + + await page.goto('/search?q=문의'); + + await expect(page.getByText(FAILED_TITLE).first()).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(EMPTY_ALL)).toHaveCount(0); + }); + + test('실패 없는 0건 응답 — 종전 "결과 없음" 렌더가 유지된다 (회귀 가드)', async ({ page }) => { + await mockSearchResponse(page, searchData()); + + await page.goto('/search?q=문의'); + + await expect(page.getByText(EMPTY_ALL).first()).toBeVisible({ timeout: 15_000 }); + await expect(page.getByText(FAILED_TITLE)).toHaveCount(0); + }); +}); diff --git a/templates/_bundled/sirsoft-basic/tests/scenarios/board-guest-access-login-redirect.yaml b/templates/_bundled/sirsoft-basic/tests/scenarios/board-guest-access-login-redirect.yaml index 647ea5dd..2288bcda 100644 --- a/templates/_bundled/sirsoft-basic/tests/scenarios/board-guest-access-login-redirect.yaml +++ b/templates/_bundled/sirsoft-basic/tests/scenarios/board-guest-access-login-redirect.yaml @@ -11,9 +11,15 @@ description: | - 비밀글 로그인 버튼이 미설정 {{_global.currentPath}} 를 redirect 로 써 항상 빈 값 → 동일 핸들러로 교체. - 로그인 폼은 기존 규약 {{query.redirect ?? '/'}} 로 복귀(변경 없음). +# axis 의미: +# 갈라지는 축은 진입 지점 하나다 — 세 지점 각각이 redirectToLoginWithReturn 을 실제로 +# 물고 있는지가 화면마다 달라진다. +# 원경로 query 유무는 그 축과 교차하지 않는다. 세 진입점이 같은 핸들러 하나로 수렴하고, +# query 보존은 그 핸들러가 window.location 을 읽는 방식이 정하므로 진입점과 무관하다. +# 따라서 3×2 조합이 아니라 (진입점 3) + (핸들러 성질 1) 이며, query 보존은 +# redirect_preserves_query_string effect 로 핸들러 단위테스트가 with/without 양쪽을 실측한다. axes: entry_point: [board_list_401, board_show_401, secret_post_login_button] # 진입 지점 3종 - has_query: [with_query, without_query] # 원경로 query 유무 effects: - redirect_preserves_pathname # /login?redirect=<pathname> @@ -30,10 +36,12 @@ validation: audit_rule: frontend-change-requires-e2e browser_check: Chrome MCP # Playwright 인프라 미구축 — Chrome MCP 정밀 점검으로 브라우저 검증 대체 note: | - test-scenario-coverage 는 axes cross-product 와 effects 가 테스트 docblock @scenario/@effects 로 - 커버되길 요구한다. Playwright spec 부재이므로 위 단위/레이아웃 테스트 docblock 에 @scenario(axis 키) - /@effects 를 마킹해 cross-product·effects 를 커버한다. - - 핸들러 단위테스트: has_query(with/without) + redirect_preserves_pathname/query_string - - 401 레이아웃 테스트: entry_point(board_list_401,board_show_401) + redirect_preserves_pathname/login_returns_to_original_path - - 비밀글 레이아웃 테스트: entry_point(secret_post_login_button) + secret_button_redirect_not_empty/login_returns_to_original_path + test-scenario-coverage 는 axes cross product 와 effects 가 테스트 docblock 의 + `@scenario k=v` / `@effects e1, e2` 마킹으로 커버되길 요구한다 (구분자는 `=` 와 `,` 다 — + `k:v` 로 적으면 파서가 조합을 읽지 못해 아무것도 커버하지 못한 채 조용히 통과한다). + Playwright spec 부재이므로 아래 단위/레이아웃 테스트 docblock 이 그 역할을 대신한다. + - 401 레이아웃 테스트: describe 별로 entry_point=board_show_401 / board_list_401 분리 마킹 + - 비밀글 레이아웃 테스트: entry_point=secret_post_login_button + - 핸들러 단위테스트: 특정 진입점에 매이지 않으므로 @effects 만 — + redirect_preserves_pathname / redirect_preserves_query_string 을 with/without 양쪽 실측 sirsoft-basic 템플릿 Playwright E2E 인프라 구축은 별도 후속 과제(구축 후 정식 spec 으로 이관). \ No newline at end of file diff --git a/templates/_bundled/sirsoft-basic/tests/scenarios/cart-select-count-display.yaml b/templates/_bundled/sirsoft-basic/tests/scenarios/cart-select-count-display.yaml new file mode 100644 index 00000000..0e2ce4bb --- /dev/null +++ b/templates/_bundled/sirsoft-basic/tests/scenarios/cart-select-count-display.yaml @@ -0,0 +1,53 @@ +feature: 장바구니 전체 선택 개수 표시 (선택 상태 파생 카운터) + +description: | + 장바구니 상단 "전체 선택" 옆 개수는 선택 상태의 SSoT 인 `_local.selectedItems` 에서 파생해 + `(선택/전체)` 로 표시한다. 어떤 마운트 경로(최초 진입 / 새로고침 / SPA 복귀)에서도, 어떤 선택 + 상태(전체 / 부분 / 미선택)에서도 값이 비지 않고 체크박스·주문 요약과 일치해야 한다. + + 수정 이력 (공개 이슈 #92): + - 표시식이 `{{_local.selectedCount}}` 였다. `selectedCount` 는 체크박스 change 핸들러에서만 + 기록되므로 최초 마운트 구간에서는 undefined 이고, 엔진이 undefined 를 빈 문자열로 서식해 + `(/27)` 로 보였다. 예외·콘솔 에러가 없어 표시로만 드러나는 결함이었다. + - 표시식을 `{{_local.selectedItems?.length ?? 0}}/{{cartItems.data.item_count ?? 0}}` 로 교체. + 같은 화면 주문 요약(`_cart_summary.json`)이 쓰던 표현식과 동일 형태여서 두 카운터의 + 계산 방식 비대칭도 함께 해소했다. + - 핸들러(`cartHandlers.ts`)의 `selectedCount` 기록은 소비처가 사라져 제거했다. + + 같은 결함 클래스(초기값 없는 키를 옵셔널 없이 하위 접근)의 방어 수정도 이 시나리오가 함께 고정한다: + - 장바구니 옵션변경 모달 재고 안내(`matchedOption`) + - 마이페이지 프로필 오류 메시지(`profileErrors` — 가드와 표시식의 옵셔널 엇갈림) + - 마이페이지 알림 수신 체크박스 4종 — 값의 타입(boolean)을 화면이 전제하지 않도록 + 자동바인딩 대신 `autoBinding:false` + `checked` + `change` 명시 바인딩으로 전환 + +axes: + mount_path: [initial_load, reload, spa_return] # 진입 / 새로고침 / SPA 이동 후 복귀 + selection: [all, partial, none] # 선택 상태 3종 + +effects: + - counter_matches_checked_state # 카운터 선택 수 = 실제 체크된 항목 수 (빈 문자열 금지) + - counter_matches_summary_panel # 카운터 = 주문 요약 "선택 상품 N / M개" 와 동일 값 + - modal_stock_display_guarded # 옵션변경 모달 재고 표기가 값 부재 구간에서 공백/NaN 이 아님 + - profile_error_display_guarded # 프로필 오류 메시지 표시식이 가드 소실 시에도 안전 + - profile_notification_checkbox_explicit_binding # 알림 수신 체크박스가 저장값 타입에 무관하게 checked 를 확정 (자동바인딩 미의존) + +test_files: + - templates/_bundled/sirsoft-basic/src/__tests__/layouts/cart-select-count-initial.test.tsx + - templates/_bundled/sirsoft-basic/src/handlers/__tests__/cartHandlers.test.ts + +validation: + audit_rule: frontend-change-requires-e2e + browser_check: Chrome MCP # sirsoft-basic Playwright E2E 인프라 미구축 — Chrome MCP 정밀 점검으로 대체 + note: | + test-scenario-coverage 는 axes cross product(3×3=9)와 effects 를 테스트 docblock + `@scenario k=v, k=v` / `@effects e1, e2` 마킹으로 커버하길 요구한다. Playwright spec 이 + 없으므로 레이아웃 렌더링 테스트의 docblock 으로 9개 조합을 전수 마킹했다. + + 마운트 경로 축의 한계: jsdom 렌더링 테스트에서 initial_load / reload / spa_return 은 모두 + "초기 _local 시드 상태로부터의 새 마운트" 라는 같은 코드 경로다. 세 경로가 실제로 갈라지는 + 지점(브라우저 새로고침, SPA 라우팅 복귀 시 데이터소스 재조회)의 구분은 Chrome MCP 정밀 점검 + 매트릭스 T1/T6/T7 이 실측으로 담당한다. 단위 계층은 "어느 마운트에서도 카운터가 비지 않는다" + 를 고정하는 역할이다. + + counter_matches_summary_panel 은 두 카운터가 동일 표현식(selectedItems 파생 + item_count 분모) + 을 쓰는지로 판정한다 — 표현식이 같으면 값이 갈릴 수 없다. diff --git a/templates/_bundled/sirsoft-basic/tests/scenarios/search-category-failure.yaml b/templates/_bundled/sirsoft-basic/tests/scenarios/search-category-failure.yaml new file mode 100644 index 00000000..2f889028 --- /dev/null +++ b/templates/_bundled/sirsoft-basic/tests/scenarios/search-category-failure.yaml @@ -0,0 +1,52 @@ +feature: 통합검색 카테고리 실패 표면화 (공개 이슈 #103) + +description: | + 카테고리 검색이 서버 예외로 실패하면 종전에는 리스너 catch 가 결과 키를 설정하지 + 않은 채 삼켜 HTTP 200 + "검색 결과가 없습니다" 로 위장됐다. 수정 후 계약: + + - 서버: 실패 카테고리는 `SearchCategoryPayload::failed()` 페이로드(failed=true, + total_is_exact=false)로 내보내고, 코어(PublicSearchController)가 + `categories_failed`/`search_failed` 를 counts_are_exact 와 동형으로 일괄 조립한다. + 예외 스택은 Log::error context 로 남는다. + - FULLTEXT 인덱스 부재는 실패가 아니다 — 드라이버가 지원해도 대상 테이블·컬럼 조합을 + 커버하는 인덱스가 없으면 MATCH 대신 LIKE 폴백으로 내려가고(1191 예방), 그 사실을 + 테이블+컬럼 조합당 프로세스 1회 warning 으로 남긴다. + - 화면(sirsoft-basic): `categories_failed`/`search_failed` 를 근거로 "검색 결과가 + 없습니다" 와 구분되는 "검색 중 오류" 안내를 그린다. 키 부재(구버전 코어) 조합에서는 + `?? false` 기본값으로 종전 렌더를 유지한다. + +axes: + category: [posts, products, pages] # 실패 카테고리 + scope: [all_tab, category_tab] # 전체 탭 섹션 / 카테고리 탭 전면 + failure: [single, all] # 단일 카테고리 실패 / 전 카테고리 실패 + +effects: + - failed_flag_in_response # 응답에 categories_failed / search_failed / 카테고리 failed 키 + - error_notice_instead_of_empty # "결과 없음" 대신 오류 안내 렌더 + - unaffected_category_renders_normally # 실패하지 않은 카테고리는 종전 렌더 유지 + - exception_stack_logged # 리스너 catch 가 exception 인스턴스를 Log::error context 로 남김 + - like_fallback_on_missing_index # 인덱스 부재 시 MATCH 대신 LIKE 폴백 (1191 예방) + - missing_index_fallback_logged_once # 인덱스 부재 폴백 warning 이 조합당 1회 + +test_files: + - tests/Unit/Search/DatabaseFulltextEngineIndexGateTest.php + - tests/Feature/Api/Public/PublicSearchControllerFailureFlagTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/SearchPostsListenerTest.php + - modules/_bundled/sirsoft-page/tests/Unit/Listeners/SearchPagesListenerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/SearchProductsListenerTest.php + - templates/_bundled/sirsoft-basic/src/__tests__/layouts/search-failed-state.test.tsx + - templates/_bundled/sirsoft-basic/tests/Playwright/specs/search-category-failure.spec.ts + +validation: + audit_rule: test-scenario-coverage + note: | + axes cross product(3×2×2=12) 중 실제로 화면이 갈라지는 조합만 브라우저 계층이 + 담당한다 — 카테고리 축의 실패 렌더 분기 구분은 Vitest(search-failed-state)가 3분기 + × (탭/섹션) 를 전수 커버하고, Playwright 는 (posts,all_tab,single) / + (posts,category_tab,single) / (all_tab,all) + 회귀 가드(무실패)를 라이브 렌더로 + 고정한다. products/pages 의 category_tab 실패 렌더는 같은 레이아웃 분기 구조의 + 복제이므로 Vitest 계층이 등가 커버한다. + + like_fallback_on_missing_index / missing_index_fallback_logged_once 는 DB 인덱스 + 상태가 전제라 브라우저 fixture 로 재현하지 않는다 — PHPUnit(IndexGateTest) + + Chrome MCP 라이브 매트릭스(인덱스 제거/복원 T4-a/T6)가 담당한다. diff --git a/tests/Feature/Api/Admin/LanguagePackControllerTest.php b/tests/Feature/Api/Admin/LanguagePackControllerTest.php index cdd41ff5..899d4af2 100644 --- a/tests/Feature/Api/Admin/LanguagePackControllerTest.php +++ b/tests/Feature/Api/Admin/LanguagePackControllerTest.php @@ -8,6 +8,7 @@ use App\Models\LanguagePack; use App\Models\Permission; use App\Models\Role; use App\Models\User; +use App\Services\LanguagePackService; use Database\Seeders\RolePermissionSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; @@ -889,4 +890,63 @@ class LanguagePackControllerTest extends TestCase ]], ]; } + + /** + * 실패 응답에 예외 원문이 실리지 않는다. + * + * 종전에는 `$e->getMessage()` 를 응답 errors/messageParams 로 그대로 넘겨, + * 검증 실패 메시지에 내부 예외 문자열(경로·드라이버 오류 등)이 노출됐다. + * 디버그 노출은 ResponseHelper 가 app.debug 에서만 Throwable 을 펼치는 + * 기존 메커니즘에 위임한다. + * + * @scenario endpoint=manifest_preview + * + * @effects language_pack_manifest_preview_failure_omits_exception_text + */ + public function test_manifest_preview_failure_does_not_leak_exception_message(): void + { + config(['app.debug' => false]); + + Sanctum::actingAs($this->manager); + + // ZIP 이 아닌 파일 → previewManifest 내부에서 예외 발생 + $response = $this->postJson('/api/admin/language-packs/manifest-preview', [ + 'file' => UploadedFile::fake()->createWithContent('broken.zip', 'not-a-zip-archive'), + ]); + + $response->assertStatus(422); + + $body = $response->getContent(); + foreach (['ZipArchive', 'SQLSTATE', '.php', 'vendor/', 'Exception'] as $needle) { + $this->assertStringNotContainsString( + $needle, + $body, + "실패 응답에 내부 예외 흔적('{$needle}')이 포함되어 있습니다: {$body}" + ); + } + } + + /** + * 캐시 갱신 실패 응답에도 예외 원문이 실리지 않는다 (500 축). + * + * @scenario endpoint=refresh_cache + * + * @effects language_pack_refresh_cache_failure_omits_exception_text + */ + public function test_refresh_cache_failure_does_not_leak_exception_message(): void + { + config(['app.debug' => false]); + + $this->mock(LanguagePackService::class, function ($mock) { + $mock->shouldReceive('refreshCache') + ->andThrow(new \RuntimeException('SQLSTATE[HY000]: internal detail')); + }); + + Sanctum::actingAs($this->manager); + + $response = $this->postJson('/api/admin/language-packs/refresh-cache'); + + $response->assertStatus(500); + $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); + } } diff --git a/tests/Feature/Api/Admin/RoleControllerTest.php b/tests/Feature/Api/Admin/RoleControllerTest.php index 0c243663..05fe397c 100644 --- a/tests/Feature/Api/Admin/RoleControllerTest.php +++ b/tests/Feature/Api/Admin/RoleControllerTest.php @@ -444,6 +444,11 @@ class RoleControllerTest extends TestCase */ public function test_store_creates_role_with_permissions(): void { + // 권한 부여 상한(KVE-2026-1919 C-4): 비-슈퍼관리자는 자신이 보유하지 않은 + // 권한을 역할에 부여할 수 없다. 이 테스트는 임의 권한 부여의 정상 경로를 + // 검증하므로 액터를 슈퍼 관리자로 승격한다. + $this->admin->update(['is_super' => true]); + $permission1 = $this->createTestPermission('test.perm.one'); $permission2 = $this->createTestPermission('test.perm.two'); @@ -637,6 +642,10 @@ class RoleControllerTest extends TestCase */ public function test_update_syncs_permissions(): void { + // 권한 부여 상한(KVE-2026-1919 C-4): 임의 권한 동기화의 정상 경로를 검증하므로 + // 액터를 슈퍼 관리자로 승격한다. + $this->admin->update(['is_super' => true]); + $role = $this->createTestRole(); $oldPermission = $this->createTestPermission('old.permission'); $newPermission = $this->createTestPermission('new.permission'); diff --git a/tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php b/tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php index 277cc154..37ce2523 100644 --- a/tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php +++ b/tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php @@ -3,9 +3,12 @@ namespace Tests\Feature\Api\Admin; use App\Enums\ExtensionOwnerType; +use App\Extension\HookManager; use App\Models\Permission; use App\Models\Role; use App\Models\User; +use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; use PHPUnit\Framework\Attributes\Test; @@ -57,7 +60,7 @@ class SettingsControllerDriverOptionsTest extends TestCase $permissionIds[] = $permission->id; } - $roleIdentifier = 'admin_test_' . uniqid(); + $roleIdentifier = 'admin_test_'.uniqid(); $adminRole = Role::create([ 'identifier' => $roleIdentifier, 'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Admin']), @@ -96,7 +99,7 @@ class SettingsControllerDriverOptionsTest extends TestCase private function authRequest(): static { return $this->withHeaders([ - 'Authorization' => 'Bearer ' . $this->token, + 'Authorization' => 'Bearer '.$this->token, 'Accept' => 'application/json', ]); } @@ -114,6 +117,7 @@ class SettingsControllerDriverOptionsTest extends TestCase 'data' => [ 'available_drivers' => [ 'storage', + 'public_asset', 'cache', 'session', 'queue', @@ -127,6 +131,10 @@ class SettingsControllerDriverOptionsTest extends TestCase /** * available_drivers의 각 드라이버에 id와 label 필드가 있는지 검증합니다. + * + * @scenario engine_source=core + * + * @effects admin_driver_options_include_search_category */ #[Test] public function available_drivers_have_correct_structure(): void @@ -137,8 +145,8 @@ class SettingsControllerDriverOptionsTest extends TestCase $data = $response->json('data.available_drivers'); - // 7개 카테고리 존재 - $this->assertCount(7, $data); + // 9개 카테고리 존재 (search 는 폴백 가드 편입으로 추가 — A5b) + $this->assertCount(9, $data); // 각 카테고리에 최소 1개 이상의 드라이버 존재 foreach ($data as $category => $drivers) { @@ -182,4 +190,167 @@ class SettingsControllerDriverOptionsTest extends TestCase $this->assertContains('mailgun', $ids); $this->assertContains('ses', $ids); } + + /** + * search 카테고리에 코어 검색엔진이 포함되는지 검증합니다. + * + * 카테고리 개수만 세면 어떤 카테고리가 늘었는지 알 수 없다 — search 가 빠진 채 + * 다른 카테고리가 하나 늘어도 개수 단언은 통과한다. + * + * @scenario engine_source=core + * + * @effects admin_driver_options_include_search_category + */ + #[Test] + public function search_drivers_include_core_engine(): void + { + $response = $this->authRequest()->getJson('/api/admin/settings'); + + $response->assertStatus(200); + + $drivers = $response->json('data.available_drivers.search'); + + $this->assertIsArray($drivers, 'search 카테고리가 응답에 없습니다.'); + $this->assertContains('mysql-fulltext', array_column($drivers, 'id')); + } + + /** + * 플러그인이 Scout 엔진 등록 훅으로 추가한 검색엔진이 카탈로그에 나타납니다. + * + * 관리자 화면 셀렉트가 이 카탈로그를 그대로 바인딩하므로, 여기 없으면 플러그인이 + * 등록한 검색엔진을 운영자가 고를 수 없다. + * + * @scenario engine_source=plugin + * + * @effects admin_driver_options_include_search_category + */ + #[Test] + public function search_drivers_include_plugin_registered_engine(): void + { + HookManager::addFilter( + 'core.search.engine_drivers', + fn (array $drivers) => array_merge($drivers, ['meilisearch' => \stdClass::class]) + ); + + $response = $this->authRequest()->getJson('/api/admin/settings'); + + $ids = array_column($response->json('data.available_drivers.search'), 'id'); + + $this->assertContains('meilisearch', $ids, '플러그인이 등록한 검색엔진이 카탈로그에 없습니다.'); + $this->assertContains('mysql-fulltext', $ids, '코어 검색엔진이 사라졌습니다.'); + } + + /** + * public_asset 카테고리에 코어 3종(none/public/s3)이 포함되는지 검증합니다. + * + * @effects settings_catalog_includes_plugin_registered_disks + */ + #[Test] + public function public_asset_drivers_include_core_options(): void + { + $response = $this->authRequest()->getJson('/api/admin/settings'); + + $drivers = $response->json('data.available_drivers.public_asset'); + $ids = array_column($drivers, 'id'); + + $this->assertSame(['none', 'public', 's3'], $ids); + } + + /** + * 카탈로그에 있는 공개 자산 디스크 저장이 성공하는지 검증합니다. + */ + #[Test] + public function saving_valid_public_asset_disk_succeeds(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', [ + '_tab' => 'drivers', + 'drivers' => $this->driversPayload(['public_asset_disk' => 'public']), + ]); + + $response->assertStatus(200); + + $saved = (new JsonConfigRepository)->getCategory('drivers'); + $this->assertSame('public', $saved['public_asset_disk'] ?? null); + } + + /** + * 저장 → drivers.json → core.storage.public_asset_disk 주입 사슬을 + * 한 테스트 안에서 종단으로 검증합니다. + * + * 주입 자동 실행은 testing 격리 가드(applyPublicAssetDiskConfig)가 막고 있으므로 + * (dev 공유 drivers.json 유입 차단), 가드를 통과한 뒤의 주입부만 실제 저장소로 + * 호출해 사슬을 잇습니다. mock 저장소를 쓰는 단위 테스트와 달리 이 테스트는 + * 저장 산출물(drivers.json)이 주입 입력이 되는 것까지 고정합니다. + * + * @effects global_setting_injects_core_config_with_none_normalized + */ + #[Test] + public function saved_public_asset_disk_reaches_core_config_through_injection(): void + { + config(['core.storage.public_asset_disk' => 'stale_value']); + + $this->authRequest()->postJson('/api/admin/settings', [ + '_tab' => 'drivers', + 'drivers' => $this->driversPayload(['public_asset_disk' => 'public']), + ])->assertStatus(200); + + $this->injectFromStoredSettings(); + + $this->assertSame('public', config('core.storage.public_asset_disk')); + + // 'none' 저장은 미설정('')으로 정규화되어 스트리밍으로 되돌아간다 + $this->authRequest()->postJson('/api/admin/settings', [ + '_tab' => 'drivers', + 'drivers' => $this->driversPayload(['public_asset_disk' => 'none']), + ])->assertStatus(200); + + $this->injectFromStoredSettings(); + + $this->assertSame('', config('core.storage.public_asset_disk')); + } + + /** + * 실제 저장 산출물(drivers.json)을 읽어 주입부를 호출합니다. + */ + private function injectFromStoredSettings(): void + { + $provider = new SettingsServiceProvider($this->app); + $method = new \ReflectionMethod($provider, 'injectPublicAssetDiskConfig'); + $method->invoke($provider, new JsonConfigRepository); + } + + /** + * 카탈로그에 없는 공개 자산 디스크 저장이 422 로 거부되는지 검증합니다. + * + * @effects invalid_disk_rejected_with_422 + */ + #[Test] + public function saving_unknown_public_asset_disk_fails_with_422(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', [ + '_tab' => 'drivers', + 'drivers' => $this->driversPayload(['public_asset_disk' => 'nonexistent_disk']), + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.public_asset_disk']); + } + + /** + * drivers 탭 저장에 필요한 최소 필수 필드 payload 를 반환합니다. + * + * @param array<string, mixed> $overrides 덮어쓸 필드 + * @return array<string, mixed> drivers payload + */ + private function driversPayload(array $overrides = []): array + { + return array_merge([ + 'storage_driver' => 'local', + 'cache_driver' => 'file', + 'session_driver' => 'file', + 'queue_driver' => 'sync', + 'log_driver' => 'daily', + 'log_level' => 'error', + ], $overrides); + } } diff --git a/tests/Feature/Api/Admin/SettingsControllerS3ValidationTest.php b/tests/Feature/Api/Admin/SettingsControllerS3ValidationTest.php new file mode 100644 index 00000000..1edec331 --- /dev/null +++ b/tests/Feature/Api/Admin/SettingsControllerS3ValidationTest.php @@ -0,0 +1,376 @@ +<?php + +namespace Tests\Feature\Api\Admin; + +use App\Enums\ExtensionOwnerType; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use App\Services\DriverRegistryService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; +use PHPUnit\Framework\Attributes\DataProvider; +use Tests\TestCase; + +/** + * 드라이버 탭 S3 검증 테스트 (공개 #99 / 내부 #563, A4·A7) + * + * s3_region 형식 검증 완화(리전 하드코딩 제거), s3_endpoint/s3_use_path_style + * 신규 키 검증, 드라이버 가용성 서버 게이트를 검증합니다. + */ +class SettingsControllerS3ValidationTest extends TestCase +{ + use RefreshDatabase; + + private User $admin; + + private string $token; + + protected function setUp(): void + { + parent::setUp(); + + $this->admin = $this->createAdminUser(); + $this->token = $this->admin->createToken('test-token')->plainTextToken; + } + + /** + * 설정 권한을 가진 관리자 사용자를 생성합니다. + */ + private function createAdminUser(): User + { + $user = User::factory()->create([ + 'password' => Hash::make('password123'), + ]); + + $permissionIds = []; + foreach (['core.settings.read', 'core.settings.update'] as $permIdentifier) { + $permission = Permission::firstOrCreate( + ['identifier' => $permIdentifier], + [ + 'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), + 'description' => json_encode(['ko' => $permIdentifier.' 권한', 'en' => $permIdentifier.' Permission']), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => 'admin', + ] + ); + $permissionIds[] = $permission->id; + } + + $adminRole = Role::firstOrCreate( + ['identifier' => 'admin'], + [ + 'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'description' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'is_active' => true, + ] + ); + $adminRole->permissions()->syncWithoutDetaching($permissionIds); + $user->roles()->attach($adminRole->id); + + return $user; + } + + /** + * 인증 헤더를 부착한 요청 헬퍼입니다. + */ + private function authRequest(): static + { + return $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Accept' => 'application/json', + ]); + } + + /** + * drivers 탭 저장의 기본 payload 를 반환합니다. + * + * @param array $overrides drivers 오버라이드 값 + * @return array 요청 payload + */ + private function driversPayload(array $overrides = []): array + { + return [ + '_tab' => 'drivers', + 'drivers' => array_merge([ + 'storage_driver' => 'local', + 'cache_driver' => 'file', + 'session_driver' => 'file', + 'queue_driver' => 'database', + 'log_driver' => 'daily', + 'log_level' => 'error', + 'websocket_enabled' => false, + ], $overrides), + ]; + } + + /** + * 리전 형식 검증 — 유효한 값(구 화이트리스트·신규 AWS 리전·R2 auto·MinIO 커스텀)은 + * 저장이 허용되어야 합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects s3_disk_resolves + */ + #[DataProvider('validRegionProvider')] + public function test_store_accepts_valid_region_shapes(string $region): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => $region, + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + ])); + + $response->assertStatus(200)->assertJson(['success' => true]); + } + + /** + * 유효한 리전 형식 목록 (구 하드코딩 5종 밖 값 포함 — A4 회귀 가드) + * + * @return array<string, array{string}> + */ + public static function validRegionProvider(): array + { + return [ + '구 화이트리스트 값' => ['ap-northeast-2'], + '구 목록 밖 AWS 리전' => ['eu-central-1'], + 'Cloudflare R2 표준값' => ['auto'], + 'MinIO 관례값' => ['us-east-1'], + ]; + } + + /** + * 리전 형식 검증 — 불량 형식은 422 로 거부되어야 합니다. + * + * @scenario region_shape=invalid_format,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects invalid_region_rejected + */ + #[DataProvider('invalidRegionProvider')] + public function test_store_rejects_invalid_region_shapes(string $region): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => $region, + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.s3_region']); + } + + /** + * 불량 리전 형식 목록 + * + * @return array<string, array{string}> + */ + public static function invalidRegionProvider(): array + { + return [ + '특수문자' => ['S3!bad'], + '대문자' => ['AP-NORTHEAST-2'], + '공백 포함' => ['ap northeast 2'], + ]; + } + + /** + * s3_endpoint 는 URL 형식이어야 합니다 (불량 형식 422). + * + * @scenario region_shape=aws_region,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_store_validates_s3_endpoint_url_format(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => 'auto', + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + 's3_endpoint' => 'not-a-url', + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.s3_endpoint']); + } + + /** + * s3_endpoint/s3_use_path_style 정상 값은 저장 왕복이 성립해야 합니다. + * + * @scenario region_shape=custom_minio,endpoint=path_style,driver_usability=adapter_present,attachment_disk=follow_s3 + * + * @effects endpoint_injected, attachment_upload_follows_driver + */ + public function test_store_saves_s3_endpoint_and_path_style_roundtrip(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => 'us-east-1', + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + 's3_endpoint' => 'http://127.0.0.1:9000', + 's3_use_path_style' => true, + ])); + + $response->assertStatus(200)->assertJson(['success' => true]); + + $show = $this->authRequest()->getJson('/api/admin/settings'); + $show->assertStatus(200); + + $drivers = $show->json('data.drivers'); + $this->assertSame('http://127.0.0.1:9000', $drivers['s3_endpoint'] ?? null); + $this->assertTrue((bool) ($drivers['s3_use_path_style'] ?? false)); + } + + /** + * s3_use_path_style 은 boolean 이어야 합니다. + * + * @scenario region_shape=aws_region,endpoint=path_style,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_store_validates_s3_use_path_style_boolean(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => 'auto', + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + 's3_use_path_style' => 'not-bool', + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.s3_use_path_style']); + } + + /** + * 드라이버 가용성 서버 게이트 — 사용 불능 드라이버 저장은 422 로 차단되어야 합니다 (A7). + * + * memcached PHP 확장 존재 여부는 환경 의존이므로, 확장이 있는 환경에서는 + * isDriverUsable 단위 검증(DriverRegistryServiceUsabilityTest)이 판정 로직을 담당하고 + * 이 테스트는 통과로 간주합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_absent,attachment_disk=local_default + * + * @effects unusable_driver_rejected + */ + public function test_store_rejects_unusable_driver(): void + { + if (extension_loaded('memcached')) { + $this->markTestSkipped('memcached 확장이 설치된 환경 — 사용 불능 상태를 만들 수 없어 단위 테스트가 판정을 담당'); + } + + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'cache_driver' => 'memcached', + 'memcached_host' => '127.0.0.1', + 'memcached_port' => 11211, + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.cache_driver']); + } + + /** + * 드라이버 가용성 게이트의 컨트롤러↔서비스 결합을 환경 무관하게 검증합니다 (A7). + * + * 위 테스트는 확장 미설치 환경에서만 실 422 를 밟으므로, 레지스트리를 컨테이너 + * partialMock 으로 교체해 "사용 불능" 판정 시 저장 요청이 422 + 사유 문구로 + * 차단되는 결합점을 어느 환경에서든 단언한다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_absent,attachment_disk=local_default + * + * @effects unusable_driver_rejected + */ + public function test_store_rejects_unusable_driver_via_registry_gate(): void + { + $this->partialMock(DriverRegistryService::class, function ($mock) { + $mock->shouldReceive('isDriverUsable') + ->andReturnUsing(fn (string $category, string $driverId) => ! ($category === 'cache' && $driverId === 'redis')); + $mock->shouldReceive('usabilityFailureReason') + ->andReturn('mocked: redis client missing'); + }); + + $response = $this->authRequest()->postJson('/api/admin/settings', $this->driversPayload([ + 'cache_driver' => 'redis', + 'redis_host' => '127.0.0.1', + 'redis_port' => 6379, + ])); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['drivers.cache_driver']); + + // 사유가 422 응답 본문까지 도달해야 운영자가 원인을 알 수 있다 + $messages = $response->json('errors')['drivers.cache_driver'] ?? []; + $this->assertStringContainsString('mocked: redis client missing', implode(' ', $messages)); + } + + /** + * 연결 테스트 요청도 동일한 리전 완화·신규 키 규칙을 적용해야 합니다 (게이트 대칭). + * + * @scenario region_shape=auto,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_test_driver_request_accepts_relaxed_region_and_new_keys(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings/test-driver', [ + 'storage_driver' => 's3', + 's3_bucket' => 'my-bucket', + 's3_region' => 'auto', + 's3_access_key' => 'key', + 's3_secret_key' => 'secret', + 's3_endpoint' => 'http://127.0.0.1:9', + 's3_use_path_style' => true, + ]); + + // 검증 통과(422 아님) — 연결 자체는 도달 불가 endpoint 라 실패 결과가 반환된다 + $response->assertStatus(200); + $this->assertFalse($response->json('data.results.s3.success')); + } + + /** + * 연결 테스트 요청이 websocket server 3필드를 수용해야 합니다 (A6 요청부). + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects test_covers_server_websocket + */ + public function test_test_driver_request_accepts_websocket_server_fields(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings/test-driver', [ + 'websocket_enabled' => true, + 'websocket_host' => 'localhost', + 'websocket_port' => 65500, + 'websocket_scheme' => 'http', + 'websocket_server_host' => '127.0.0.1', + 'websocket_server_port' => 65501, + 'websocket_server_scheme' => 'http', + 'websocket_app_key' => 'k', + ]); + + // 검증 통과 — 서버 미기동이므로 연결 실패 결과가 반환될 뿐 422 는 아니어야 한다 + $response->assertStatus(200); + $this->assertFalse($response->json('data.results.websocket.success')); + } + + /** + * 비인증 요청은 401 로 차단되어야 합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects unauthenticated_returns_401 + */ + public function test_store_and_test_driver_return_401_without_authentication(): void + { + $this->postJson('/api/admin/settings', $this->driversPayload())->assertStatus(401); + $this->postJson('/api/admin/settings/test-driver', ['storage_driver' => 's3'])->assertStatus(401); + } +} diff --git a/tests/Feature/Api/Admin/SettingsControllerTest.php b/tests/Feature/Api/Admin/SettingsControllerTest.php index a913856c..9fabdaa8 100644 --- a/tests/Feature/Api/Admin/SettingsControllerTest.php +++ b/tests/Feature/Api/Admin/SettingsControllerTest.php @@ -10,6 +10,7 @@ use App\Models\Attachment; use App\Models\Permission; use App\Models\Role; use App\Models\User; +use App\Services\AttachmentService; use App\Services\SettingsService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; @@ -1373,6 +1374,42 @@ class SettingsControllerTest extends TestCase ->assertJson(['success' => true]); } + /** + * 드라이버 탭 저장 왕복에 S3 신규 키(s3_endpoint/s3_use_path_style)가 포함되어야 합니다 (#563). + */ + public function test_store_drivers_roundtrip_includes_new_s3_keys(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings', [ + '_tab' => 'drivers', + 'drivers' => [ + 'storage_driver' => 's3', + 's3_bucket' => 'roundtrip-bucket', + 's3_region' => 'auto', + 's3_access_key' => 'roundtrip-key', + 's3_secret_key' => 'roundtrip-secret', + 's3_endpoint' => 'https://roundtrip.r2.cloudflarestorage.com', + 's3_use_path_style' => true, + 'cache_driver' => 'file', + 'session_driver' => 'file', + 'queue_driver' => 'database', + 'log_driver' => 'daily', + 'log_level' => 'error', + 'websocket_enabled' => false, + ], + ]); + + $response->assertStatus(200) + ->assertJson(['success' => true]); + + $drivers = $this->authRequest()->getJson('/api/admin/settings') + ->assertStatus(200) + ->json('data.drivers'); + + $this->assertSame('auto', $drivers['s3_region'] ?? null); + $this->assertSame('https://roundtrip.r2.cloudflarestorage.com', $drivers['s3_endpoint'] ?? null); + $this->assertTrue((bool) ($drivers['s3_use_path_style'] ?? false)); + } + // ======================================================================== // 시스템 정보 probe 실패 격리 테스트 — gnuboard/g7#59 // (구조/캐시/정상값은 위 systemInfo 테스트가 이미 커버. 여기서는 호스팅 @@ -1461,6 +1498,7 @@ class SettingsControllerTest extends TestCase $app->make(ConfigRepositoryInterface::class), $app->make(AttachmentRepositoryInterface::class), $app->make(CacheInterface::class), + $app->make(AttachmentService::class), ); } @@ -1560,6 +1598,7 @@ class SettingsControllerTest extends TestCase $app->make(ConfigRepositoryInterface::class), $app->make(AttachmentRepositoryInterface::class), $app->make(CacheInterface::class), + $app->make(AttachmentService::class), ); $this->stub = $stub; } @@ -1614,6 +1653,7 @@ class SettingsControllerTest extends TestCase $app->make(ConfigRepositoryInterface::class), $app->make(AttachmentRepositoryInterface::class), $app->make(CacheInterface::class), + $app->make(AttachmentService::class), ); } diff --git a/tests/Feature/Api/Admin/SettingsDatabaseBackupTest.php b/tests/Feature/Api/Admin/SettingsDatabaseBackupTest.php new file mode 100644 index 00000000..78110d83 --- /dev/null +++ b/tests/Feature/Api/Admin/SettingsDatabaseBackupTest.php @@ -0,0 +1,154 @@ +<?php + +namespace Tests\Feature\Api\Admin; + +use App\Enums\ExtensionOwnerType; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * DB 백업 엔드포인트의 미제공 응답 검증 (공개 #115 부록 B3) + * + * `POST /api/admin/settings/backup-database` 는 존재하지 않는 + * `SettingsService::backupDatabase()` 를 호출해 왔다. PHP 가 던지는 것은 `Error` 라 + * 컨트롤러의 `catch (\Exception)` 에 걸리지 않고 그대로 500 이 됐다. + * 화면 버튼이 없어 운영 중에는 드러나지 않지만, 공개 API 레퍼런스에는 정상 기능으로 + * 실려 있어 문서를 보고 호출한 쪽에는 무조건 서버 오류였다. + * + * 기능은 아직 없으므로 "서버가 고장났다"(500)가 아니라 "이 기능은 제공하지 않는다"(501)로 + * 정직하게 답하는 것이 계약이다. + */ +class SettingsDatabaseBackupTest extends TestCase +{ + use RefreshDatabase; + + private User $admin; + + private string $token; + + protected function setUp(): void + { + parent::setUp(); + + $this->admin = $this->createAdminUser(); + $this->token = $this->admin->createToken('test-token')->plainTextToken; + } + + /** + * 설정 갱신 권한을 가진 관리자 생성 + */ + private function createAdminUser(): User + { + $user = User::factory()->create([ + 'password' => Hash::make('password123'), + ]); + + $permissionIds = []; + foreach (['core.settings.read', 'core.settings.update'] as $permIdentifier) { + $permission = Permission::firstOrCreate( + ['identifier' => $permIdentifier], + [ + 'name' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), + 'description' => json_encode(['ko' => $permIdentifier, 'en' => $permIdentifier]), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => 'admin', + ] + ); + $permissionIds[] = $permission->id; + } + + $roleIdentifier = 'admin_test_'.uniqid(); + $adminRole = Role::create([ + 'identifier' => $roleIdentifier, + 'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Admin']), + 'description' => json_encode(['ko' => '테스트', 'en' => 'Test']), + 'is_active' => true, + ]); + + $adminBaseRole = Role::firstOrCreate( + ['identifier' => 'admin'], + [ + 'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'is_active' => true, + ] + ); + + $adminRole->permissions()->sync($permissionIds); + + $user->roles()->attach($adminBaseRole->id, [ + 'assigned_at' => now(), + 'assigned_by' => null, + ]); + $user->roles()->attach($adminRole->id, [ + 'assigned_at' => now(), + 'assigned_by' => null, + ]); + + return $user->fresh(); + } + + /** + * 인증된 요청 헬퍼 + */ + private function authRequest(): static + { + return $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Accept' => 'application/json', + ]); + } + + /** + * 미제공 기능은 500 이 아니라 501 로 응답해야 한다. + */ + #[Test] + public function database_backup_responds_with_not_implemented(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings/backup-database'); + + $response->assertStatus(501); + $response->assertJson(['success' => false]); + } + + /** + * 응답 메시지는 다국어 키가 해석된 사람 말이어야 한다. + * + * 키 원문(`settings.database_backup_unavailable`)이 그대로 나가면 호출자는 + * 무슨 상태인지 알 수 없다. + */ + #[Test] + public function not_implemented_response_carries_a_translated_message(): void + { + $response = $this->authRequest()->postJson('/api/admin/settings/backup-database'); + + $message = (string) $response->json('message'); + + $this->assertNotSame('', $message); + $this->assertStringNotContainsString('settings.', $message); + $this->assertSame(__('settings.database_backup_unavailable'), $message); + } + + /** + * 미구현 응답으로 바꾸더라도 권한 경계는 그대로여야 한다. + * + * 501 을 먼저 돌려주느라 인증·권한 검사를 건너뛰면, 권한 없는 요청에까지 + * 내부 구현 상태를 알려주게 된다. + */ + #[Test] + public function guest_still_receives_unauthenticated(): void + { + $response = $this->withHeaders(['Accept' => 'application/json']) + ->postJson('/api/admin/settings/backup-database'); + + $response->assertStatus(401); + } +} diff --git a/tests/Feature/Api/Admin/SettingsSingleValueUpdateTest.php b/tests/Feature/Api/Admin/SettingsSingleValueUpdateTest.php new file mode 100644 index 00000000..4699fb85 --- /dev/null +++ b/tests/Feature/Api/Admin/SettingsSingleValueUpdateTest.php @@ -0,0 +1,378 @@ +<?php + +namespace Tests\Feature\Api\Admin; + +use App\Enums\ExtensionOwnerType; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use App\Repositories\JsonConfigRepository; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; +use Illuminate\Testing\TestResponse; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * 단건 설정 저장(`PUT /api/admin/settings/{key}`)의 값 형태 검증 + * + * 종전 규칙은 `required|string|max:1000` 이었다. 그래서 두 가지가 깨졌다. + * - 한 번 입력한 값을 빈 칸으로 되돌릴 수 없다 (`required` 가 빈 문자열을 거부해 422). + * - 폼 전송의 boolean/정수가 문자열로 저장되어, 값을 읽는 쪽의 타입 비교가 어긋난다. + * + * 값의 타입은 `config/settings/defaults.json` 의 기본값이 SSoT 다. + */ +class SettingsSingleValueUpdateTest extends TestCase +{ + use RefreshDatabase; + + private string $token; + + protected function setUp(): void + { + parent::setUp(); + + $this->token = $this->createAdminUser()->createToken('test-token')->plainTextToken; + } + + /** + * 관리자 사용자 생성 + */ + private function createAdminUser(): User + { + $user = User::factory()->create(['password' => Hash::make('password123')]); + + $permissionIds = []; + foreach (['core.settings.read', 'core.settings.update'] as $identifier) { + $permissionIds[] = Permission::firstOrCreate( + ['identifier' => $identifier], + [ + 'name' => json_encode(['ko' => $identifier, 'en' => $identifier]), + 'description' => json_encode(['ko' => $identifier, 'en' => $identifier]), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => 'admin', + ] + )->id; + } + + $scopedRole = Role::create([ + 'identifier' => 'admin_test_'.uniqid(), + 'name' => json_encode(['ko' => '테스트 관리자', 'en' => 'Test Admin']), + 'description' => json_encode(['ko' => '테스트', 'en' => 'Test']), + 'is_active' => true, + ]); + $scopedRole->permissions()->sync($permissionIds); + + $adminRole = Role::firstOrCreate( + ['identifier' => 'admin'], + [ + 'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'is_active' => true, + ] + ); + + $user->roles()->attach($adminRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + $user->roles()->attach($scopedRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + + return $user->fresh(); + } + + /** + * 단건 저장 요청을 보냅니다. + * + * @param string $key 설정 키 + * @param array<string, mixed> $payload 요청 본문 + */ + private function putSetting(string $key, array $payload): TestResponse + { + return $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->token, + 'Accept' => 'application/json', + ])->putJson('/api/admin/settings/'.$key, $payload); + } + + /** + * 저장된 값을 저장소에서 직접 읽습니다. + * + * @param string $key 설정 키 + * @return mixed 저장값 + */ + private function stored(string $key): mixed + { + return (new JsonConfigRepository)->get($key); + } + + // ─── 빈 값 되돌리기 ────────────────────────────────────── + + /** + * 문자열 설정을 빈 값으로 되돌릴 수 있습니다. (실패-먼저) + * + * @scenario declared_type=string, value_disposition=cleared + * + * @effects setting_cleared_to_empty_string + */ + #[Test] + public function string_setting_can_be_cleared_to_empty(): void + { + $this->putSetting('seo.meta_keywords', ['value' => 'g7, cms'])->assertOk(); + $this->assertSame('g7, cms', $this->stored('seo.meta_keywords')); + + $this->putSetting('seo.meta_keywords', ['value' => ''])->assertOk(); + + $this->assertSame('', $this->stored('seo.meta_keywords'), '빈 값으로 되돌릴 수 없습니다.'); + } + + /** + * null 도 빈 값으로 받아들이며, 문자열 설정에는 빈 문자열로 남깁니다. + * + * `ConvertEmptyStringsToNull` 미들웨어가 `''` 를 null 로 바꾸므로 두 입력은 + * 요청 시점에 이미 같다. 저장 형태는 선언 타입(기본값 `''`)에 맞춘다. + * + * @scenario declared_type=string, value_disposition=cleared + * + * @effects setting_cleared_to_empty_string + */ + #[Test] + public function null_value_is_accepted(): void + { + $this->putSetting('seo.meta_keywords', ['value' => null])->assertOk(); + + $this->assertSame('', $this->stored('seo.meta_keywords')); + } + + /** + * `value` 키 자체가 빠진 요청은 거부합니다. + * + * "값을 비운다" 와 "값을 안 보냈다" 는 다르다 — 후자는 잘못된 payload 다. + * + * @scenario declared_type=string, value_disposition=rejected + * + * @effects missing_value_key_rejected_with_422 + */ + #[Test] + public function missing_value_key_is_rejected(): void + { + $this->putSetting('seo.meta_keywords', []) + ->assertStatus(422) + ->assertJsonValidationErrors(['value']); + } + + /** + * 문자열 설정은 값이 숫자·불리언처럼 보여도 문자열 그대로 저장합니다. + * + * 정규화는 선언 타입이 boolean·정수일 때만 캐스팅해야 한다. 문자열 설정까지 캐스팅하면 + * 앞자리 0 이 사라지거나(`"0123"` → 123) 문구가 boolean 이 되어, 운영자가 넣은 값이 + * 조용히 다른 것이 된다. + * + * 앞뒤 공백은 프레임워크의 `TrimStrings` 미들웨어가 요청 시점에 이미 제거하므로 + * 이 경로의 판정 대상이 아니다. + * + * @scenario declared_type=string, value_disposition=accepted + * + * @effects string_setting_stored_verbatim + */ + #[Test] + public function string_setting_is_stored_verbatim(): void + { + foreach (['0123', 'false', '42'] as $value) { + $this->putSetting('seo.meta_keywords', ['value' => $value])->assertOk(); + + $stored = $this->stored('seo.meta_keywords'); + $this->assertIsString($stored, "'{$value}' 가 문자열이 아닌 타입으로 캐스팅되었습니다."); + $this->assertSame($value, $stored, '문자열 값이 변형되었습니다.'); + } + } + + // ─── 타입 보존 ────────────────────────────────────── + + /** + * boolean 설정에 문자열 "true" 를 보내도 boolean 으로 저장됩니다. (실패-먼저) + * + * @scenario declared_type=boolean, value_disposition=accepted + * + * @effects boolean_setting_stored_as_boolean + */ + #[Test] + public function boolean_setting_stores_boolean_not_string(): void + { + $this->putSetting('seo.sitemap_enabled', ['value' => 'false'])->assertOk(); + + $stored = $this->stored('seo.sitemap_enabled'); + $this->assertIsBool($stored, '문자열이 그대로 저장되었습니다.'); + $this->assertFalse($stored); + + $this->putSetting('seo.sitemap_enabled', ['value' => '1'])->assertOk(); + $this->assertTrue($this->stored('seo.sitemap_enabled')); + } + + /** + * JSON 본문의 진짜 boolean 도 그대로 저장됩니다. + * + * @scenario declared_type=boolean, value_disposition=accepted + * + * @effects boolean_setting_stored_as_boolean + */ + #[Test] + public function native_boolean_is_preserved(): void + { + $this->putSetting('general.maintenance_mode', ['value' => true])->assertOk(); + + $this->assertTrue($this->stored('general.maintenance_mode')); + } + + /** + * boolean 으로 해석할 수 없는 값은 거부합니다. + * + * 조용히 false 로 캐스팅하면 오타 입력이 "정상 저장" 으로 통과한다. + * + * @scenario declared_type=boolean, value_disposition=rejected + * + * @effects uninterpretable_value_rejected_with_422 + */ + #[Test] + public function uninterpretable_boolean_is_rejected(): void + { + $this->putSetting('seo.sitemap_enabled', ['value' => 'maybe']) + ->assertStatus(422) + ->assertJsonValidationErrors(['value']); + } + + /** + * 정수 설정에 숫자 문자열을 보내면 정수로 저장됩니다. (실패-먼저) + * + * @scenario declared_type=integer, value_disposition=accepted + * + * @effects integer_setting_stored_as_integer + */ + #[Test] + public function integer_setting_stores_integer_not_string(): void + { + $this->putSetting('upload.max_file_size', ['value' => '25'])->assertOk(); + + $stored = $this->stored('upload.max_file_size'); + $this->assertIsInt($stored, '문자열이 그대로 저장되었습니다.'); + $this->assertSame(25, $stored); + } + + /** + * 정수로 해석할 수 없는 값은 거부합니다. + * + * @scenario declared_type=integer, value_disposition=rejected + * + * @effects uninterpretable_value_rejected_with_422 + */ + #[Test] + public function uninterpretable_integer_is_rejected(): void + { + $this->putSetting('upload.max_file_size', ['value' => 'twenty']) + ->assertStatus(422) + ->assertJsonValidationErrors(['value']); + } + + /** + * 비-문자열 설정에 빈 문자열을 보내면 null 로 비웁니다. + * + * @scenario declared_type=integer, value_disposition=cleared + * + * @effects setting_cleared_to_null + */ + #[Test] + public function empty_string_clears_non_string_setting(): void + { + $this->putSetting('upload.max_file_size', ['value' => '25'])->assertOk(); + $this->putSetting('upload.max_file_size', ['value' => ''])->assertOk(); + + $this->assertNull($this->stored('upload.max_file_size')); + } + + /** + * 배열 값도 저장할 수 있습니다. + * + * @scenario declared_type=array, value_disposition=accepted + * + * @effects array_setting_stored_as_array + */ + #[Test] + public function array_value_is_accepted(): void + { + $this->putSetting('seo.bot_user_agents', ['value' => ['Googlebot', 'Bingbot']])->assertOk(); + + $this->assertSame(['Googlebot', 'Bingbot'], $this->stored('seo.bot_user_agents')); + } + + /** + * boolean 설정도 빈 값으로 되돌릴 수 있습니다. + * + * @scenario declared_type=boolean, value_disposition=cleared + * + * @effects setting_cleared_to_null + */ + #[Test] + public function boolean_setting_can_be_cleared(): void + { + $this->putSetting('seo.sitemap_enabled', ['value' => 'true'])->assertOk(); + $this->putSetting('seo.sitemap_enabled', ['value' => ''])->assertOk(); + + $this->assertNull($this->stored('seo.sitemap_enabled'), 'boolean 설정을 비울 수 없습니다.'); + } + + /** + * 배열 설정도 빈 값으로 되돌릴 수 있습니다. + * + * @scenario declared_type=array, value_disposition=cleared + * + * @effects setting_cleared_to_null + */ + #[Test] + public function array_setting_can_be_cleared(): void + { + $this->putSetting('seo.bot_user_agents', ['value' => ['Googlebot']])->assertOk(); + $this->putSetting('seo.bot_user_agents', ['value' => ''])->assertOk(); + + $this->assertNull($this->stored('seo.bot_user_agents'), '배열 설정을 비울 수 없습니다.'); + } + + // ─── 길이 상한 ────────────────────────────────────── + + /** + * 문자열 상한(1000자)은 그대로 유지됩니다. + * + * @scenario declared_type=string, value_disposition=rejected + * + * @effects oversized_value_rejected_with_422 + */ + #[Test] + public function oversized_string_is_rejected(): void + { + $this->putSetting('seo.meta_keywords', ['value' => str_repeat('a', 1001)]) + ->assertStatus(422) + ->assertJsonValidationErrors(['value']); + } + + /** + * 배열 값의 JSON 직렬화 상한(5000자)도 거부합니다. + * + * 문자열 상한만 있으면 배열 축으로 얼마든지 큰 값이 들어온다 — 설정 JSON 파일이 + * 요청 하나로 무한정 커진다. + * + * @scenario declared_type=array, value_disposition=rejected + * + * @effects oversized_array_rejected_with_422 + */ + #[Test] + public function oversized_array_is_rejected(): void + { + // 항목당 12자 남짓 × 500 → JSON 직렬화 6000자 초과 + $agents = array_map(static fn (int $i): string => 'Bot'.str_pad((string) $i, 8, '0', STR_PAD_LEFT), range(1, 500)); + + $this->assertGreaterThan(5000, mb_strlen((string) json_encode($agents)), '픽스처가 상한을 넘지 못합니다.'); + + $this->putSetting('seo.bot_user_agents', ['value' => $agents]) + ->assertStatus(422) + ->assertJsonValidationErrors(['value']); + } +} diff --git a/tests/Feature/Api/Admin/TemplateControllerTest.php b/tests/Feature/Api/Admin/TemplateControllerTest.php index 640bc3fd..a18dcce2 100644 --- a/tests/Feature/Api/Admin/TemplateControllerTest.php +++ b/tests/Feature/Api/Admin/TemplateControllerTest.php @@ -155,6 +155,7 @@ class TemplateControllerTest extends TestCase // 기본 동작 설정 $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getInstalledTemplatesWithDetails')->andReturn([]); $mock->shouldReceive('getUninstalledTemplates')->andReturn([]); $mock->shouldReceive('getTemplateInfo')->andReturn(null); @@ -205,6 +206,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getInstalledTemplatesWithDetails')->andReturn($installedTemplates); $mock->shouldReceive('getUninstalledTemplates')->andReturn([]); $this->app->instance(TemplateManagerInterface::class, $mock); @@ -260,6 +262,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getInstalledTemplatesWithDetails')->andReturn($installedTemplates); $mock->shouldReceive('getUninstalledTemplates')->andReturn([]); $this->app->instance(TemplateManagerInterface::class, $mock); @@ -301,6 +304,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getInstalledTemplatesWithDetails')->andReturn($installedTemplates); $mock->shouldReceive('getUninstalledTemplates')->andReturn([]); $this->app->instance(TemplateManagerInterface::class, $mock); @@ -429,6 +433,7 @@ class TemplateControllerTest extends TestCase // Mock 설정 $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplateInfo')->andReturn([ 'id' => $template->id, 'identifier' => $template->identifier, @@ -520,6 +525,7 @@ class TemplateControllerTest extends TestCase // Mock 설정 $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplateInfo')->andReturn([ 'id' => $template->id, 'identifier' => $template->identifier, @@ -554,6 +560,7 @@ class TemplateControllerTest extends TestCase // Mock 설정 — externals 에 정규화 대상(HTTP URL, 중복) 포함 $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplateInfo')->andReturn([ 'id' => $template->id, 'identifier' => $template->identifier, @@ -611,6 +618,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplateInfo')->andReturn([ 'id' => $template->id, 'identifier' => $template->identifier, @@ -647,6 +655,7 @@ class TemplateControllerTest extends TestCase // Mock 설정 $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn([ 'identifier' => $template->identifier, 'vendor' => $template->vendor, @@ -712,6 +721,7 @@ class TemplateControllerTest extends TestCase // Arrange $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('activateTemplate')->andThrow( ValidationException::withMessages([ 'template' => ['No active template found'], @@ -773,6 +783,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn([ 'identifier' => $templateIdentifier, 'vendor' => 'test', @@ -822,6 +833,7 @@ class TemplateControllerTest extends TestCase // Arrange $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn(null); $mock->shouldReceive('installTemplate')->andThrow( ValidationException::withMessages([ @@ -1160,6 +1172,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn([ 'identifier' => $template->identifier, ]); @@ -1245,6 +1258,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn([ 'identifier' => $template->identifier, ]); @@ -1299,6 +1313,7 @@ class TemplateControllerTest extends TestCase // Arrange $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('deactivateTemplate')->andThrow( ValidationException::withMessages([ 'template' => ['No active template found'], @@ -1327,6 +1342,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('getTemplate')->andReturn([ 'identifier' => $template->identifier, ]); @@ -1391,6 +1407,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('activateTemplate') ->with($template->identifier, false) ->andReturn([ @@ -1446,6 +1463,7 @@ class TemplateControllerTest extends TestCase $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('activateTemplate') ->with($template->identifier, true) ->andReturnUsing(function () use ($template) { @@ -1496,6 +1514,7 @@ class TemplateControllerTest extends TestCase // template.json: {"modules": {"sirsoft-board": ">=1.0.0", "sirsoft-ecommerce": ">=1.0.0"}} $mock = Mockery::mock(TemplateManagerInterface::class); $mock->shouldReceive('loadTemplates')->andReturnNull(); + $mock->shouldReceive('ensureLoaded')->andReturnNull(); $mock->shouldReceive('activateTemplate') ->with($template->identifier, false) ->andReturn([ diff --git a/tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php b/tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php index dc7e5fab..aebe2566 100644 --- a/tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php +++ b/tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Api\Admin; use App\Enums\ExtensionStatus; +use App\Extension\HookManager; use App\Models\Permission; use App\Models\Role; use App\Models\Template; @@ -103,6 +104,46 @@ class TemplateLayoutAttachmentControllerTest extends TestCase Storage::disk($attachment->disk)->assertExists('template-layout-attachments/'.$attachment->path); } + public function test_upload_fires_hooks(): void + { + // Arrange + $beforeUploadFired = false; + $afterUploadFired = false; + $filterApplied = false; + + HookManager::addAction('core.template_layout_attachment.before_upload', function () use (&$beforeUploadFired) { + $beforeUploadFired = true; + }); + + HookManager::addFilter('core.template_layout_attachment.filter_upload_file', function ($file) use (&$filterApplied) { + $filterApplied = true; + + return $file; + }); + + HookManager::addAction('core.template_layout_attachment.after_upload', function () use (&$afterUploadFired) { + $afterUploadFired = true; + }); + + // Act + $response = $this->withHeaders($this->authHeaders()) + ->postJson("/api/admin/templates/{$this->template->identifier}/layout-attachments", [ + 'file' => UploadedFile::fake()->image('hooked.png', 10, 10), + 'layout_name' => 'home', + ]); + + // Assert + $response->assertStatus(200)->assertJson(['success' => true]); + $this->assertTrue($beforeUploadFired, 'before_upload hook should be fired'); + $this->assertTrue($filterApplied, 'filter_upload_file hook should be applied'); + $this->assertTrue($afterUploadFired, 'after_upload hook should be fired'); + + // Cleanup hooks + HookManager::clearAction('core.template_layout_attachment.before_upload'); + HookManager::clearFilter('core.template_layout_attachment.filter_upload_file'); + HookManager::clearAction('core.template_layout_attachment.after_upload'); + } + public function test_index_lists_template_attachments(): void { TemplateLayoutAttachment::create([ diff --git a/tests/Feature/Api/Admin/UserBulkStatusTest.php b/tests/Feature/Api/Admin/UserBulkStatusTest.php index d670341e..18b42a7b 100644 --- a/tests/Feature/Api/Admin/UserBulkStatusTest.php +++ b/tests/Feature/Api/Admin/UserBulkStatusTest.php @@ -53,7 +53,7 @@ class UserBulkStatusTest extends TestCase 'description' => json_encode(['ko' => '사용자 정보 수정 권한', 'en' => 'Permission to update users']), 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', - 'type' => 'admin', + 'type' => 'admin', ] ); @@ -65,7 +65,7 @@ class UserBulkStatusTest extends TestCase 'description' => json_encode(['ko' => '시스템 관리자', 'en' => 'System Administrator']), 'extension_type' => ExtensionOwnerType::Core, 'extension_identifier' => 'core', - 'type' => 'admin', + 'type' => 'admin', 'is_active' => true, ] ); @@ -134,6 +134,28 @@ class UserBulkStatusTest extends TestCase } } + /** + * 일괄 상태 변경 응답 메시지의 :count 플레이스홀더가 실제 처리 건수로 치환된다. + * + * 7.0.7 사전점검 브라우저 실측에서 응답 message 가 + * ":count명의 사용자 상태가 변경되었습니다." 원문 그대로 내려오는 결함 발견 — + * 컨트롤러가 메시지 키만 넘기고 치환 파라미터를 생략한 회귀를 고정한다. + */ + public function test_bulk_status_message_substitutes_count_placeholder(): void + { + $users = User::factory()->count(2)->create(['status' => 'inactive']); + + $response = $this->authRequest()->patchJson('/api/admin/users/bulk-status', [ + 'ids' => $users->pluck('uuid')->toArray(), + 'status' => 'active', + ]); + + $response->assertOk(); + $message = (string) $response->json('message'); + $this->assertStringNotContainsString(':count', $message, '응답 메시지에 미치환 플레이스홀더가 남아 있으면 안 됩니다'); + $this->assertStringContainsString('2', $message, '응답 메시지에 처리 건수가 치환되어야 합니다'); + } + /** * 활성 사용자들을 비활성으로 일괄 변경할 수 있다. */ @@ -240,14 +262,14 @@ class UserBulkStatusTest extends TestCase $afterHookCalled = false; // before_bulk_update 훅 등록 - app(\App\Extension\HookManager::class)->addAction('sirsoft-core.user.before_bulk_update', function ($hookIds, $hookStatus) use (&$beforeHookCalled, $ids) { + app(HookManager::class)->addAction('sirsoft-core.user.before_bulk_update', function ($hookIds, $hookStatus) use (&$beforeHookCalled, $ids) { $beforeHookCalled = true; $this->assertEquals($ids, $hookIds); $this->assertEquals('active', $hookStatus); }); // after_bulk_update 훅 등록 - app(\App\Extension\HookManager::class)->addAction('sirsoft-core.user.after_bulk_update', function ($hookIds, $hookStatus, $hookUpdatedCount) use (&$afterHookCalled, $ids) { + app(HookManager::class)->addAction('sirsoft-core.user.after_bulk_update', function ($hookIds, $hookStatus, $hookUpdatedCount) use (&$afterHookCalled, $ids) { $afterHookCalled = true; $this->assertEquals($ids, $hookIds); $this->assertEquals('active', $hookStatus); diff --git a/tests/Feature/Api/Auth/ProfileControllerTest.php b/tests/Feature/Api/Auth/ProfileControllerTest.php index 54f2c0e7..ead5c62f 100644 --- a/tests/Feature/Api/Auth/ProfileControllerTest.php +++ b/tests/Feature/Api/Auth/ProfileControllerTest.php @@ -3,12 +3,14 @@ namespace Tests\Feature\Api\Auth; use App\Enums\AttachmentSourceType; +use App\Enums\PermissionType; use App\Models\Attachment; use App\Models\Permission; use App\Models\Role; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -548,7 +550,7 @@ class ProfileControllerTest extends TestCase // 새 비밀번호로 로그인 가능 확인 $this->user->refresh(); - $this->assertTrue(\Illuminate\Support\Facades\Hash::check('newpassword456', $this->user->password)); + $this->assertTrue(Hash::check('newpassword456', $this->user->password)); } /** @@ -676,6 +678,13 @@ class ProfileControllerTest extends TestCase $this->assertNotEquals($originalEmail, $this->user->email); $this->assertNotEquals($originalName, $this->user->name); + // 접미사 형식: 날짜 + 사용자 ID (같은 날 같은 이메일 재탈퇴 충돌 차단, 공개이슈 #112) + $this->assertMatchesRegularExpression( + '/_deleted_\d{8}_'.$this->user->id.'$/u', + $this->user->email, + '이메일 접미사에 사용자 ID 가 없으면 같은 날 재탈퇴에서 unique 충돌이 난다' + ); + // 탈퇴 상태로 변경되었는지 확인 $this->assertEquals('withdrawn', $this->user->status); $this->assertNotNull($this->user->withdrawn_at); @@ -724,6 +733,9 @@ class ProfileControllerTest extends TestCase // Attachment 레코드 삭제 확인 $this->assertDatabaseMissing('attachments', ['id' => $attachment->id]); + + // 파일 삭제는 DB 커밋이 확정된 뒤에 수행된다 (공개이슈 #112) + Storage::disk('attachments')->assertMissing('attachments/avatars/test-avatar.jpg'); } /** @@ -742,7 +754,8 @@ class ProfileControllerTest extends TestCase 'Accept' => 'application/json', ])->deleteJson('/api/me'); - $response->assertStatus(500); // Exception 발생 + // 관리 anchor 보호는 잘못된 요청이지 서버 오류가 아니다 (공개이슈 #112) + $response->assertStatus(422); // 슈퍼 관리자는 변경되지 않음 $superAdmin->refresh(); @@ -756,9 +769,9 @@ class ProfileControllerTest extends TestCase public function test_withdraw_prevents_admin(): void { // 관리자 역할 생성 및 할당 - $role = \App\Models\Role::factory()->create(['identifier' => 'admin']); - $permission = \App\Models\Permission::factory()->create([ - 'type' => \App\Enums\PermissionType::Admin, + $role = Role::factory()->create(['identifier' => 'admin']); + $permission = Permission::factory()->create([ + 'type' => PermissionType::Admin, ]); $role->permissions()->attach($permission); @@ -773,7 +786,8 @@ class ProfileControllerTest extends TestCase 'Accept' => 'application/json', ])->deleteJson('/api/me'); - $response->assertStatus(500); // ValidationException 발생 + // 관리자 탈퇴 차단은 잘못된 요청이지 서버 오류가 아니다 (공개이슈 #112) + $response->assertStatus(422); // 관리자는 변경되지 않음 $admin->refresh(); @@ -794,6 +808,25 @@ class ProfileControllerTest extends TestCase $this->user->refresh(); $this->assertStringContainsString('_탈퇴', $this->user->nickname); + + // 컬럼 상한(50자) 안에서 접미사가 붙는다 — 넘기면 저장 자체가 실패한다 + $this->assertLessThanOrEqual(User::NICKNAME_MAX_LENGTH, mb_strlen($this->user->nickname)); + } + + /** + * 닉네임이 상한에 가까우면 앞을 잘라 접미사를 붙인다 (공개이슈 #112) + */ + public function test_withdraw_truncates_nickname_at_column_limit(): void + { + $this->user->update(['nickname' => str_repeat('가', User::NICKNAME_MAX_LENGTH)]); + + $response = $this->authRequest()->deleteJson('/api/me'); + + $response->assertStatus(200); + + $this->user->refresh(); + $this->assertSame(User::NICKNAME_MAX_LENGTH, mb_strlen($this->user->nickname)); + $this->assertStringEndsWith('_탈퇴', $this->user->nickname); } /** diff --git a/tests/Feature/Api/Auth/WithdrawAtomicityTest.php b/tests/Feature/Api/Auth/WithdrawAtomicityTest.php new file mode 100644 index 00000000..e14e5e52 --- /dev/null +++ b/tests/Feature/Api/Auth/WithdrawAtomicityTest.php @@ -0,0 +1,242 @@ +<?php + +namespace Tests\Feature\Api\Auth; + +use App\Enums\AttachmentSourceType; +use App\Enums\PermissionType; +use App\Enums\UserStatus; +use App\Models\Attachment; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use App\Services\UserService; +use Illuminate\Database\Schema\Builder as SchemaBuilder; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Storage; +use Illuminate\Validation\ValidationException; +use Tests\TestCase; + +/** + * 회원 탈퇴 원자성/접미사 충돌 회귀 테스트 (공개이슈 #112) + * + * 재현 경로: 가입 → 탈퇴 → 같은 이메일 재가입 → 같은 날 재탈퇴. + * 수정 전에는 email unique 위반으로 500 이 났고, 그 시점에 토큰·동의 이력은 + * 이미 삭제된 뒤라 "로그인은 되는데 탈퇴는 실패한" 반파괴 상태가 남았다. + * + * @scenario entry_point=self_withdraw_api, email_state=reused_same_day, failure_injection=last_write_fails, field_length=exceeds_limit + * + * @effects withdraw_is_atomic_all_or_nothing, consents_and_tokens_restored_on_failure, avatar_file_kept_on_failure, same_day_rewithdraw_with_same_email_succeeds, long_values_truncated_within_column_limits, withdraw_is_idempotent, failed_withdraw_leaves_no_activity_log + */ +class WithdrawAtomicityTest extends TestCase +{ + use RefreshDatabase; + + /** + * W1: 같은 날 같은 이메일로 재가입한 회원이 다시 탈퇴해도 실패하지 않는다. + */ + public function test_same_day_rewithdraw_with_same_email_succeeds(): void + { + Carbon::setTestNow(Carbon::parse('2026-08-13 10:00:00')); + + $first = User::factory()->create(['email' => 'g7fix112.a@example.com']); + $second = User::factory()->create(['email' => 'placeholder@example.com']); + + $service = app(UserService::class); + + $this->assertTrue($service->withdrawUser($first)); + + // 같은 이메일로 재가입한 상황 재현 + $second->forceFill(['email' => 'g7fix112.a@example.com'])->save(); + + $this->assertTrue($service->withdrawUser($second)); + + $first->refresh(); + $second->refresh(); + + $this->assertSame(UserStatus::Withdrawn->value, $first->status); + $this->assertSame(UserStatus::Withdrawn->value, $second->status); + $this->assertNotSame($first->email, $second->email); + + // 접미사에 사용자 ID 가 포함되어 구조적으로 유일하다 + $this->assertStringEndsWith('_'.$first->id, $first->email); + $this->assertStringEndsWith('_'.$second->id, $second->email); + } + + /** + * W2: 마지막 단계가 실패하면 동의 이력·토큰 삭제도 함께 되돌아간다. + */ + public function test_failed_withdraw_rolls_back_consents_and_tokens(): void + { + $user = User::factory()->create(['email' => 'g7fix112.b@example.com']); + $user->createToken('t'); + + // 약관 동의 이력도 선행 삭제 대상이다 — 롤백 대상에 포함되는지 함께 본다. + $user->consents()->create([ + 'consent_type' => 'terms', + 'agreed_at' => now(), + ]); + + $this->assertSame(1, $user->tokens()->count()); + $this->assertSame(1, $user->consents()->count()); + + $service = app(UserService::class); + + try { + $service->withdrawUser($this->bindFailingWithdraw($user)); + $this->fail('탈퇴가 실패해야 합니다.'); + } catch (ValidationException $e) { + // 기대한 실패 + } + + $user->refresh(); + + $this->assertSame(1, $user->tokens()->count(), '토큰이 원상 복구되어야 합니다.'); + $this->assertSame(1, $user->consents()->count(), '약관 동의 이력이 원상 복구되어야 합니다.'); + $this->assertNotSame(UserStatus::Withdrawn->value, $user->status); + $this->assertStringNotContainsString('_deleted_', $user->email); + } + + /** + * W3: 탈퇴가 실패하면 아바타 파일도 남아 있어야 한다. + */ + public function test_failed_withdraw_keeps_avatar_file(): void + { + Storage::fake('attachments'); + + $user = User::factory()->create(['email' => 'g7fix112.c@example.com']); + + $attachment = Attachment::factory()->create([ + 'attachmentable_type' => User::class, + 'attachmentable_id' => $user->id, + 'collection' => 'avatar', + 'disk' => 'attachments', + 'path' => 'attachments/avatars/w3.jpg', + 'source_type' => AttachmentSourceType::Core, + ]); + Storage::disk('attachments')->put('attachments/avatars/w3.jpg', 'fake'); + + try { + app(UserService::class)->withdrawUser($this->bindFailingWithdraw($user)); + $this->fail('탈퇴가 실패해야 합니다.'); + } catch (ValidationException $e) { + // 기대한 실패 + } + + Storage::disk('attachments')->assertExists('attachments/avatars/w3.jpg'); + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * W4: 이미 탈퇴한 계정에 다시 호출해도 접미사가 겹쳐 붙지 않는다 (멱등). + */ + public function test_withdraw_is_idempotent(): void + { + $user = User::factory()->create(['email' => 'g7fix112.d@example.com', 'nickname' => '닉']); + + $service = app(UserService::class); + $service->withdrawUser($user); + + $user->refresh(); + $emailAfterFirst = $user->email; + $nicknameAfterFirst = $user->nickname; + + $this->assertTrue($user->withdraw()); + + $user->refresh(); + $this->assertSame($emailAfterFirst, $user->email); + $this->assertSame($nicknameAfterFirst, $user->nickname); + $this->assertSame(1, substr_count($user->email, '_deleted_')); + } + + /** + * W5: 컬럼 길이 상한에 걸리는 값도 절단 후 정상 저장된다. + */ + public function test_long_values_are_truncated_within_column_limits(): void + { + $limit = SchemaBuilder::$defaultStringLength ?? 255; + + // 접미사를 붙이면 반드시 상한을 넘도록 컬럼 길이를 꽉 채운다 — + // 여유가 있으면 절단 경로가 실행되지 않아 절단 없이도 통과한다. + $user = User::factory()->create([ + 'email' => str_repeat('a', $limit - 12).'@example.com', + 'name' => str_repeat('이', $limit), + 'nickname' => str_repeat('가', User::NICKNAME_MAX_LENGTH), + ]); + + $this->assertSame($limit, mb_strlen($user->email), '이메일이 컬럼 상한을 꽉 채워야 합니다'); + $this->assertSame($limit, mb_strlen($user->name)); + + $this->assertTrue(app(UserService::class)->withdrawUser($user)); + + $user->refresh(); + + // 절단이 실제로 일어났는지 — 원값 길이 그대로면 접미사가 안 붙은 것이다 + $this->assertSame($limit, mb_strlen($user->email), '이메일이 상한에 맞춰 절단되어야 합니다'); + $this->assertSame($limit, mb_strlen($user->name)); + $this->assertSame(User::NICKNAME_MAX_LENGTH, mb_strlen($user->nickname)); + + $this->assertStringContainsString('_deleted_', $user->email); + $this->assertStringEndsWith('_'.$user->id, $user->email); + $this->assertStringContainsString('_탈퇴_', $user->name); + $this->assertStringEndsWith('_탈퇴', $user->nickname); + } + + /** + * W7: 탈퇴가 실패하면 활동 로그도 남지 않는다. + * + * 관리자 계정의 탈퇴 차단은 500 이 아니라 422 로 나가야 한다 — 종전에는 예외가 + * 그대로 올라가 서버 오류로 표시됐다. + * + * @effects blocked_withdraw_returns_422_not_500 + */ + public function test_failed_withdraw_does_not_log_activity(): void + { + $role = Role::factory()->create(['identifier' => 'admin']); + $permission = Permission::factory()->create([ + 'type' => PermissionType::Admin, + ]); + $role->permissions()->attach($permission); + + $admin = User::factory()->create(['email' => 'g7fix112.admin@example.com']); + $admin->roles()->attach($role); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$admin->createToken('t')->plainTextToken, + 'Accept' => 'application/json', + ])->deleteJson('/api/me'); + + $response->assertStatus(422); + + $this->assertDatabaseMissing('activity_logs', [ + 'action' => 'profile.withdraw', + 'user_id' => $admin->id, + ]); + } + + /** + * 마지막 쓰기(withdraw 저장)를 실패시키는 사용자 인스턴스를 돌려줍니다. + * + * 접미사가 붙은 이메일을 미리 점유해 두면 unique 위반으로 저장이 실패합니다 — + * 실제 운영에서 발생한 실패 모드와 동일한 경로입니다. + * + * @param User $user 대상 사용자 + * @return User 같은 사용자 인스턴스 + */ + private function bindFailingWithdraw(User $user): User + { + $suffix = '_deleted_'.now()->format('Ymd').'_'.$user->id; + + // 미끼 유저가 같은 접미사 이메일을 선점 → withdraw() 저장이 unique 위반으로 실패 + User::factory()->create(['email' => $user->email.$suffix]); + + return $user; + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); + } +} diff --git a/tests/Feature/Api/Identity/IdentityChallengeFlowTest.php b/tests/Feature/Api/Identity/IdentityChallengeFlowTest.php index e63a37d7..65e7d338 100644 --- a/tests/Feature/Api/Identity/IdentityChallengeFlowTest.php +++ b/tests/Feature/Api/Identity/IdentityChallengeFlowTest.php @@ -34,6 +34,11 @@ class IdentityChallengeFlowTest extends TestCase $this->seed(IdentityMessageDefinitionSeeder::class); } + /** + * @scenario actor_role=guest, challenge_owner=none, endpoint=request + * + * @effects guest_can_request_challenge + */ public function test_request_challenge_returns_201_and_creates_log(): void { $response = $this->postJson('/api/identity/challenges', [ diff --git a/tests/Feature/Api/Identity/IdentityPermissionContractTest.php b/tests/Feature/Api/Identity/IdentityPermissionContractTest.php new file mode 100644 index 00000000..1a2cb0c9 --- /dev/null +++ b/tests/Feature/Api/Identity/IdentityPermissionContractTest.php @@ -0,0 +1,210 @@ +<?php + +namespace Tests\Feature\Api\Identity; + +use App\Extension\HookManager; +use App\Http\Requests\Identity\CancelChallengeRequest; +use App\Http\Requests\Identity\RequestChallengeRequest; +use App\Http\Requests\Identity\VerifyChallengeRequest; +use App\Models\Permission; +use App\Models\Role; +use Database\Seeders\RolePermissionSeeder; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Notification; +use Illuminate\Support\Facades\Route as RouteFacade; +use Tests\TestCase; + +/** + * IDV 권한 가드의 "데이터 준비" 와 "라우트 계약" 검증. + * + * PermissionGuardTest 가 요청 시점의 통과/거부를 다룬다면, 본 테스트는 그 판정이 읽는 + * 시드 데이터(권한 메타·역할별 scope)와 라우트 구성(미들웨어·확장 지점)이 규약대로인지를 본다. + * 이 둘이 어긋나면 가드는 "정상 동작" 하면서도 잘못된 대상을 통과시킨다. + */ +class IdentityPermissionContractTest extends TestCase +{ + use RefreshDatabase; + + /** IDV 사용자 권한 3종. */ + private const IDV_PERMISSIONS = [ + 'core.identity.request', + 'core.identity.verify', + 'core.identity.cancel', + ]; + + protected function setUp(): void + { + parent::setUp(); + Notification::fake(); + $this->seed(RolePermissionSeeder::class); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=self, endpoint=verify + * + * @effects role_permission_seeder_syncs_idv_permissions_with_owner_metadata + */ + public function test_seeder_syncs_idv_permissions_with_owner_metadata(): void + { + foreach (self::IDV_PERMISSIONS as $identifier) { + $permission = Permission::where('identifier', $identifier)->first(); + $this->assertNotNull($permission, "{$identifier} 권한이 시드되지 않았다"); + } + + // verify/cancel 은 소유자 판정 대상이므로 resource_route_key + owner_key 가 있어야 한다. + // 이 메타가 없으면 PermissionMiddleware 가 scope=self 를 평가할 근거를 잃고 통과시킨다. + foreach (['core.identity.verify', 'core.identity.cancel'] as $identifier) { + $permission = Permission::where('identifier', $identifier)->first(); + $this->assertSame('challenge', $permission->resource_route_key, "{$identifier} 의 resource_route_key 불일치"); + $this->assertSame('user_id', $permission->owner_key, "{$identifier} 의 owner_key 불일치"); + } + + // request 는 대상 challenge 가 아직 없으므로 소유자 메타를 갖지 않는다. + $request = Permission::where('identifier', 'core.identity.request')->first(); + $this->assertNull($request->resource_route_key); + $this->assertNull($request->owner_key); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=other, endpoint=cancel + * + * @effects role_permission_seeder_assigns_self_scope_to_user_role_idv_perms + */ + public function test_seeder_assigns_self_scope_to_user_role_idv_permissions(): void + { + $userRole = Role::where('identifier', 'user')->first(); + $this->assertNotNull($userRole); + + $scopes = $userRole->permissions() + ->whereIn('identifier', self::IDV_PERMISSIONS) + ->pluck('role_permissions.scope_type', 'identifier') + ->all(); + + // verify/cancel 만 self — 이 값이 비면 남의 challenge 도 통과한다 + $this->assertSame('self', $scopes['core.identity.verify'] ?? null); + $this->assertSame('self', $scopes['core.identity.cancel'] ?? null); + + // request 는 대상이 없어 scope 를 갖지 않는다 + $this->assertNull($scopes['core.identity.request'] ?? null); + } + + /** + * @scenario actor_role=guest, challenge_owner=other, endpoint=cancel + * + * @effects role_permission_seeder_grants_idv_perms_to_guest_without_scope + */ + public function test_seeder_grants_idv_permissions_to_guest_role_without_scope(): void + { + $guestRole = Role::where('identifier', 'guest')->first(); + $this->assertNotNull($guestRole, 'guest 역할이 시드되지 않았다'); + + $scopes = $guestRole->permissions() + ->whereIn('identifier', self::IDV_PERMISSIONS) + ->pluck('role_permissions.scope_type', 'identifier') + ->all(); + + // guest 는 IDV 3종 전부 보유 (Mode B 가입 흐름) + foreach (self::IDV_PERMISSIONS as $identifier) { + $this->assertArrayHasKey($identifier, $scopes, "guest 역할에 {$identifier} 미부여"); + // scope 가 붙으면 user_id 없는 게스트가 항상 거부되어 가입 흐름이 막힌다 + $this->assertNull($scopes[$identifier], "guest 의 {$identifier} 에 scope 가 붙어 있다"); + } + } + + /** + * @scenario actor_role=guest, challenge_owner=other, endpoint=verify + * + * @effects guest_role_idv_permission_check_via_guest_role_cache + */ + public function test_guest_role_idv_permission_is_visible_through_guest_role_lookup(): void + { + $guestRole = Role::where('identifier', 'guest')->first(); + + // 비로그인 요청의 권한 판정은 이 guest 역할 조회 결과에 전적으로 의존한다. + // 조회가 비면 게스트의 IDV 흐름 전체가 403 이 된다. + $identifiers = $guestRole->permissions()->pluck('identifier')->all(); + + foreach (self::IDV_PERMISSIONS as $identifier) { + $this->assertContains($identifier, $identifiers); + } + } + + /** + * @scenario actor_role=guest, challenge_owner=none, endpoint=request + * + * @effects cancel_endpoint_via_permission_middleware_does_not_require_legacy_auth_sanctum + */ + public function test_idv_challenge_routes_use_optional_sanctum_not_auth_sanctum(): void + { + $names = [ + 'api.identity.challenges.request', + 'api.identity.challenges.verify', + 'api.identity.challenges.cancel', + ]; + + foreach ($names as $name) { + $route = RouteFacade::getRoutes()->getByName($name); + $this->assertNotNull($route, "{$name} 라우트가 없다"); + + $middleware = $route->gatherMiddleware(); + + // auth:sanctum 이 붙으면 비로그인 게스트가 401 로 막혀 Mode B 가입이 불가능해진다. + $this->assertNotContains('auth:sanctum', $middleware, "{$name} 에 auth:sanctum 이 붙어 있다"); + $this->assertContains('optional.sanctum', $middleware, "{$name} 에 optional.sanctum 이 없다"); + + // 권한 판정은 permission 미들웨어가 단독으로 담당한다 + $hasPermission = collect($middleware)->contains(fn ($m) => str_starts_with((string) $m, 'permission:user,core.identity.')); + $this->assertTrue($hasPermission, "{$name} 에 permission 미들웨어가 없다"); + } + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=none, endpoint=request + * + * @effects request_challenge_route_validation_rules_extensible_via_hook_filter + */ + public function test_request_challenge_rules_are_extensible_via_hook_filter(): void + { + HookManager::addFilter('core.identity.request_validation_rules', function (array $rules) { + $rules['extra_field'] = ['required', 'string']; + + return $rules; + }, 10, 'test-listener'); + + $rules = (new RequestChallengeRequest)->rules(); + + $this->assertArrayHasKey('extra_field', $rules, '확장이 추가한 규칙이 반영되지 않았다'); + $this->assertArrayHasKey('purpose', $rules, '기본 규칙이 확장 때문에 사라졌다'); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=self, endpoint=verify + * + * @effects verify_challenge_route_validation_rules_extensible_via_hook_filter + */ + public function test_verify_challenge_rules_are_extensible_via_hook_filter(): void + { + HookManager::addFilter('core.identity.verify_validation_rules', function (array $rules) { + $rules['device_token'] = ['nullable', 'string']; + + return $rules; + }, 10, 'test-listener'); + + $rules = (new VerifyChallengeRequest)->rules(); + + $this->assertArrayHasKey('device_token', $rules); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=other, endpoint=verify + * + * @effects cancel_form_request_authorize_returns_true_letting_route_middleware_decide + */ + public function test_cancel_form_request_authorize_defers_to_route_middleware(): void + { + // FormRequest 가 자체 인증/권한 판정을 하면 라우트 미들웨어와 판정이 둘로 갈린다. + // 코어 규약은 authorize(): true 고정 + permission 미들웨어 단일 게이트다. + $this->assertTrue((new CancelChallengeRequest)->authorize()); + $this->assertSame([], (new CancelChallengeRequest)->rules()); + } +} diff --git a/tests/Feature/Api/Identity/PermissionGuardTest.php b/tests/Feature/Api/Identity/PermissionGuardTest.php index a02014a9..4511e43c 100644 --- a/tests/Feature/Api/Identity/PermissionGuardTest.php +++ b/tests/Feature/Api/Identity/PermissionGuardTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Api\Identity; use App\Enums\IdentityVerificationStatus; use App\Models\IdentityVerificationLog; +use App\Models\Permission; use App\Models\Role; use App\Models\User; use Database\Seeders\RolePermissionSeeder; @@ -44,6 +45,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(200); } + /** + * @scenario actor_role=guest, challenge_owner=none, endpoint=request + * + * @effects guest_can_request_challenge + */ public function test_challenge_request_works_for_unauthenticated_guests(): void { // IDV 의 핵심 설계: 가입 전 Mode B 에서도 challenge 발급 가능해야 하므로 optional.sanctum @@ -124,6 +130,11 @@ class PermissionGuardTest extends TestCase $this->getJson('/api/admin/identity/providers')->assertStatus(401); } + /** + * @scenario actor_role=guest, challenge_owner=other, endpoint=verify + * + * @effects guest_can_verify_any_challenge, missing_challenge_returns_404_after_guard_pass + */ public function test_guest_can_verify_challenge(): void { // guest 역할에 core.identity.verify 권한 부여됨 — Mode B 가입 흐름 @@ -135,6 +146,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(404); // 401(권한 거부) 아닌 404(모델 미존재) — 가드는 통과 } + /** + * @scenario actor_role=guest, challenge_owner=other, endpoint=cancel + * + * @effects guest_can_cancel_any_challenge, permission_middleware_resolves_challenge_via_route_model_binding + */ public function test_guest_can_cancel_challenge(): void { // guest 의 cancel 도 동일 — 권한 가드 통과 + 모델 미존재로 404 @@ -143,6 +159,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(404); } + /** + * @scenario actor_role=user_with_perm, challenge_owner=self, endpoint=cancel + * + * @effects user_with_perm_can_cancel_own_challenge + */ public function test_user_can_cancel_own_challenge(): void { $user = $this->createUserWithUserRole(); @@ -155,6 +176,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(200); } + /** + * @scenario actor_role=user_with_perm, challenge_owner=other, endpoint=cancel + * + * @effects user_with_perm_cannot_cancel_others_challenge_403 + */ public function test_user_cannot_cancel_others_challenge(): void { $owner = $this->createUserWithUserRole(); @@ -169,6 +195,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(403); } + /** + * @scenario actor_role=admin, challenge_owner=other, endpoint=cancel + * + * @effects admin_can_cancel_any_challenge_via_scope_null + */ public function test_admin_can_cancel_any_challenge(): void { $owner = $this->createUserWithUserRole(); @@ -183,6 +214,11 @@ class PermissionGuardTest extends TestCase $response->assertStatus(200); } + /** + * @scenario actor_role=user_without_perm, challenge_owner=self, endpoint=cancel + * + * @effects user_without_perm_gets_403_on_cancel + */ public function test_user_without_identity_permission_gets_403_on_cancel(): void { $user = $this->createUserWithUserRole(); @@ -191,7 +227,7 @@ class PermissionGuardTest extends TestCase // user 역할에서 IDV cancel 권한만 제거 → 가드 차단 $userRole = Role::where('identifier', 'user')->first(); - $cancelPerm = \App\Models\Permission::where('identifier', 'core.identity.cancel')->first(); + $cancelPerm = Permission::where('identifier', 'core.identity.cancel')->first(); $userRole->permissions()->detach($cancelPerm->id); $response = $this->withHeader('Authorization', 'Bearer '.$token) @@ -200,6 +236,237 @@ class PermissionGuardTest extends TestCase $response->assertStatus(403); } + /** + * @scenario actor_role=user_with_perm, challenge_owner=none, endpoint=request + * + * @effects user_with_perm_can_request_challenge + */ + public function test_user_with_permission_can_request_challenge(): void + { + $user = $this->createUserWithUserRole(); + $token = $user->createToken('test')->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/identity/challenges', [ + 'purpose' => 'sensitive_action', + 'target' => ['email' => $user->email], + ]); + + $response->assertStatus(201); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=self, endpoint=verify + * + * @effects user_with_perm_can_verify_own_challenge + */ + public function test_user_can_verify_own_challenge(): void + { + $user = $this->createUserWithUserRole(); + $token = $user->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($user); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + // scope=self 가드 통과 — 권한 거부(403)가 아니어야 한다 + $this->assertNotSame(403, $response->getStatusCode()); + } + + /** + * @scenario actor_role=user_with_perm, challenge_owner=other, endpoint=verify + * + * @effects user_with_perm_cannot_verify_others_challenge_403 + */ + public function test_user_cannot_verify_others_challenge(): void + { + $owner = $this->createUserWithUserRole(); + $intruder = $this->createUserWithUserRole(); + $token = $intruder->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($owner); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + // 남의 challenge 는 코드가 맞아도 scope=self 가드에서 먼저 차단된다 + $response->assertStatus(403); + } + + /** + * @scenario actor_role=user_without_perm, challenge_owner=none, endpoint=request + * + * @effects user_without_perm_gets_403_on_request + */ + public function test_user_without_permission_gets_403_on_request(): void + { + $user = $this->createUserWithUserRole(); + $token = $user->createToken('test')->plainTextToken; + + $this->detachIdentityPermission('core.identity.request'); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/identity/challenges', [ + 'purpose' => 'sensitive_action', + 'target' => ['email' => $user->email], + ]); + + $response->assertStatus(403); + } + + /** + * @scenario actor_role=user_without_perm, challenge_owner=self, endpoint=verify + * + * @effects user_without_perm_gets_403_on_verify + */ + public function test_user_without_permission_gets_403_on_verify_own(): void + { + $user = $this->createUserWithUserRole(); + $token = $user->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($user); + + $this->detachIdentityPermission('core.identity.verify'); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + // 본인 challenge 여도 권한 자체가 없으면 403 + $response->assertStatus(403); + } + + /** + * @scenario actor_role=user_without_perm, challenge_owner=other, endpoint=verify + * + * @effects user_without_perm_gets_403_on_verify + */ + public function test_user_without_permission_gets_403_on_verify_others(): void + { + $owner = $this->createUserWithUserRole(); + $intruder = $this->createUserWithUserRole(); + $token = $intruder->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($owner); + + $this->detachIdentityPermission('core.identity.verify'); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + $response->assertStatus(403); + } + + /** + * @scenario actor_role=user_without_perm, challenge_owner=other, endpoint=cancel + * + * @effects user_without_perm_gets_403_on_cancel + */ + public function test_user_without_permission_gets_403_on_cancel_others(): void + { + $owner = $this->createUserWithUserRole(); + $intruder = $this->createUserWithUserRole(); + $token = $intruder->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($owner); + + $this->detachIdentityPermission('core.identity.cancel'); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/cancel"); + + $response->assertStatus(403); + } + + /** + * @scenario actor_role=admin, challenge_owner=none, endpoint=request + * + * @effects user_with_perm_can_request_challenge + */ + public function test_admin_can_request_challenge(): void + { + $admin = $this->createAdmin(); + $token = $admin->createToken('test')->plainTextToken; + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/identity/challenges', [ + 'purpose' => 'sensitive_action', + 'target' => ['email' => $admin->email], + ]); + + $response->assertStatus(201); + } + + /** + * @scenario actor_role=admin, challenge_owner=self, endpoint=verify + * + * @effects admin_can_verify_any_challenge_via_scope_null + */ + public function test_admin_can_verify_own_challenge(): void + { + $admin = $this->createAdmin(); + $token = $admin->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($admin); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + $this->assertNotSame(403, $response->getStatusCode()); + } + + /** + * @scenario actor_role=admin, challenge_owner=other, endpoint=verify + * + * @effects admin_can_verify_any_challenge_via_scope_null + */ + public function test_admin_can_verify_any_challenge(): void + { + $owner = $this->createUserWithUserRole(); + $admin = $this->createAdmin(); + $token = $admin->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($owner); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/verify", [ + 'code' => '123456', + ]); + + // admin 은 scope=null 이라 남의 challenge 도 가드를 통과한다 + $this->assertNotSame(403, $response->getStatusCode()); + } + + /** + * @scenario actor_role=admin, challenge_owner=self, endpoint=cancel + * + * @effects admin_can_cancel_any_challenge_via_scope_null + */ + public function test_admin_can_cancel_own_challenge(): void + { + $admin = $this->createAdmin(); + $token = $admin->createToken('test')->plainTextToken; + $challenge = $this->createChallengeFor($admin); + + $response = $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson("/api/identity/challenges/{$challenge->id}/cancel"); + + $response->assertStatus(200); + } + + /** + * user 역할에서 지정 IDV 권한을 떼어냅니다 (권한 미보유 상태 재현). + */ + private function detachIdentityPermission(string $identifier): void + { + $userRole = Role::where('identifier', 'user')->first(); + $permission = Permission::where('identifier', $identifier)->first(); + $userRole->permissions()->detach($permission->id); + } + /** * user 역할이 attach 된 사용자를 생성합니다. * diff --git a/tests/Feature/Api/Identity/ResolvePolicyEndpointTest.php b/tests/Feature/Api/Identity/ResolvePolicyEndpointTest.php new file mode 100644 index 00000000..3dcab909 --- /dev/null +++ b/tests/Feature/Api/Identity/ResolvePolicyEndpointTest.php @@ -0,0 +1,94 @@ +<?php + +namespace Tests\Feature\Api\Identity; + +use App\Extension\IdentityVerification\Providers\MailIdentityProvider; +use App\Models\IdentityPolicy; +use App\Services\IdentityPolicyService; +use Tests\TestCase; + +/** + * 정책 프리페치 엔드포인트의 provider_id 해석 (A6a) + * + * `GET /api/identity/policies/resolve` 는 저장된 `provider_id` 를 그대로 내보냈다. + * 바로 옆의 428 강제 경로(`IdentityPolicyService::resolveProviderId()`)는 같은 값을 + * 레지스트리와 대조하고 미등록이면 purpose 기반 폴백으로 대체하는데, 이쪽만 raw 였다 — + * 같은 데이터에 두 개의 게터가 서로 다른 답을 주는 안티패턴이고, 제거된 플러그인의 + * provider ID 가 공개 응답(optional.sanctum)에 그대로 실린다. + */ +class ResolvePolicyEndpointTest extends TestCase +{ + private string $endpoint = '/api/identity/policies/resolve'; + + /** + * 정책 행을 만듭니다. + * + * @param string|null $providerId 저장할 provider ID + * @return IdentityPolicy 생성된 정책 + */ + private function makePolicy(?string $providerId): IdentityPolicy + { + return IdentityPolicy::updateOrCreate([ + 'key' => 'test.resolve.policy', + ], [ + 'scope' => 'route', + 'target' => 'api.test.resolve', + 'purpose' => 'sensitive_action', + 'provider_id' => $providerId, + 'enabled' => true, + 'grace_minutes' => 10, + 'applies_to' => 'both', + 'fail_mode' => 'block', + 'source_type' => 'core', + ]); + } + + /** + * 미등록 provider 는 해석된 값(폴백)으로 대체되어 나간다. (실패-먼저) + */ + public function test_unregistered_provider_is_resolved_not_echoed(): void + { + $this->makePolicy('ghost_provider'); + + $response = $this->getJson($this->endpoint.'?scope=route&target=api.test.resolve') + ->assertOk(); + + $this->assertNotSame( + 'ghost_provider', + $response->json('data.provider_id'), + '제거된 플러그인의 provider ID 가 공개 응답에 그대로 실렸습니다.' + ); + + // 428 강제 경로와 같은 게터를 경유해야 두 경로의 답이 갈리지 않는다 + $policy = IdentityPolicy::where('key', 'test.resolve.policy')->firstOrFail(); + $this->assertSame( + app(IdentityPolicyService::class)->resolveProviderId($policy), + $response->json('data.provider_id'), + '프리페치 응답과 428 강제 경로의 provider 해석이 어긋납니다.' + ); + } + + /** + * 등록된 provider 는 그대로 유지된다. (비회귀 pin) + */ + public function test_registered_provider_is_returned_as_is(): void + { + // 코어 메일 provider 는 항상 등록되어 있다 (ID 는 provider 자신이 선언한 값) + $registeredId = app(MailIdentityProvider::class)->getId(); + $this->makePolicy($registeredId); + + $this->getJson($this->endpoint.'?scope=route&target=api.test.resolve') + ->assertOk() + ->assertJsonPath('data.provider_id', $registeredId); + } + + /** + * 매칭 정책이 없으면 null 을 반환한다. (기존 계약 pin) + */ + public function test_no_matching_policy_returns_null(): void + { + $this->getJson($this->endpoint.'?scope=route&target=api.no.such.target') + ->assertOk() + ->assertJsonPath('data', null); + } +} diff --git a/tests/Feature/Api/Public/AttachmentS3DiskServingTest.php b/tests/Feature/Api/Public/AttachmentS3DiskServingTest.php new file mode 100644 index 00000000..a439e1b9 --- /dev/null +++ b/tests/Feature/Api/Public/AttachmentS3DiskServingTest.php @@ -0,0 +1,133 @@ +<?php + +namespace Tests\Feature\Api\Public; + +use App\Contracts\Extension\StorageInterface; +use App\Models\Attachment; +use App\Services\AttachmentService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Mockery; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Tests\TestCase; + +/** + * S3 디스크 첨부의 공개 이미지 서빙 회귀 테스트 (공개 #99 / 내부 #563) + * + * attachment.disk 가 s3 로 전환된 상태에서 이미지 인라인 서빙 분기가 로컬 절대 + * 경로(filemtime/filesize)를 전제해 500 이 나던 결함의 회귀 가드. + * 실환경 재현: 실 AWS 업로드 후 GET /api/attachment/{hash} → + * "filemtime(): stat failed for \\2026/08/13/….png" 500 (2026-08-13 라이브 로그). + * + * S3 디스크의 getBasePath('') 는 로컬 stat 가능한 경로가 아니므로(어댑터 prefix + * 가 빈 문자열), 그 의미를 재현하는 StorageInterface 대역으로 원격 디스크 행을 + * 만든다. Storage::fake('s3') 는 로컬 디스크라 이 결함을 재현하지 못한다. + */ +class AttachmentS3DiskServingTest extends TestCase +{ + use RefreshDatabase; + + /** + * 원격(S3) 디스크 의미를 재현하는 스토리지 대역을 컨텍스트 바인딩합니다. + * + * @param string $content 스트림으로 서빙될 바이트 + */ + private function bindRemoteDiskStorage(string $content = 'png-bytes'): void + { + $storage = Mockery::mock(StorageInterface::class); + $storage->shouldReceive('withDisk')->andReturnSelf(); + $storage->shouldReceive('exists')->andReturn(true); + // S3 어댑터의 path('') 의미 재현 — 로컬 stat 불가능한 값 (라이브 로그의 '\2026/…' 원인) + $storage->shouldReceive('getBasePath')->andReturn(''); + $storage->shouldReceive('response')->andReturnUsing( + fn () => new StreamedResponse( + function () use ($content) { + echo $content; + }, + 200, + ['Content-Type' => 'image/png', 'Content-Length' => (string) strlen($content)] + ) + ); + + $this->app->when(AttachmentService::class) + ->needs(StorageInterface::class) + ->give(fn () => $storage); + } + + /** + * s3 디스크 이미지 행을 생성합니다. + */ + private function makeS3ImageAttachment(): Attachment + { + return Attachment::factory()->create([ + 'disk' => 's3', + 'path' => '2026/08/13/regression-guard.png', + 'mime_type' => 'image/png', + ]); + } + + /** + * s3 디스크 이미지 첨부가 200 스트림 + 캐싱 헤더로 서빙되어야 합니다. + * + * 종전 코드는 getBasePath 로 로컬 절대 경로를 조립해 filemtime 에서 죽었다(500). + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=follow_s3 + * + * @effects attachment_upload_follows_driver + */ + public function test_s3_disk_image_attachment_serves_200_with_cache_headers(): void + { + $this->bindRemoteDiskStorage(); + $attachment = $this->makeS3ImageAttachment(); + + $response = $this->get('/api/attachment/'.$attachment->hash); + + $response->assertStatus(200); + $this->assertNotEmpty($response->headers->get('ETag')); + $this->assertStringContainsString('image/png', (string) $response->headers->get('Content-Type')); + $this->assertNotEmpty((string) $response->headers->get('Cache-Control')); + } + + /** + * 동일 ETag 의 If-None-Match 재요청은 304 로 응답해야 합니다 (캐싱 계약 유지). + * + * ETag 는 파일 stat 이 아닌 행 메타(디스크·경로·수정시각·크기) 기반이어야 + * 원격 디스크에서도 결정적이다. + */ + public function test_s3_disk_image_attachment_etag_roundtrip_yields_304(): void + { + $this->bindRemoteDiskStorage(); + $attachment = $this->makeS3ImageAttachment(); + + $first = $this->get('/api/attachment/'.$attachment->hash); + $first->assertStatus(200); + $etag = $first->headers->get('ETag'); + $this->assertNotEmpty($etag); + + $second = $this->withHeaders(['If-None-Match' => $etag]) + ->get('/api/attachment/'.$attachment->hash); + + $second->assertStatus(304); + $this->assertSame($etag, $second->headers->get('ETag')); + } + + /** + * 로컬 디스크 이미지 행의 서빙 계약(200 + ETag)이 유지되어야 합니다 (혼재 회귀 가드). + * + * 기존 행은 행 disk 로 계속 서빙된다 — s3 전환 후에도 과거 로컬 행이 깨지면 안 된다. + */ + public function test_local_disk_image_attachment_still_serves(): void + { + $this->bindRemoteDiskStorage(); + + $attachment = Attachment::factory()->create([ + 'disk' => 'attachments', + 'path' => '2026/08/13/legacy-local.png', + 'mime_type' => 'image/png', + ]); + + $response = $this->get('/api/attachment/'.$attachment->hash); + + $response->assertStatus(200); + $this->assertNotEmpty($response->headers->get('ETag')); + } +} diff --git a/tests/Feature/Api/Public/LayoutServingTest.php b/tests/Feature/Api/Public/LayoutServingTest.php index 5e159a8c..2210e997 100644 --- a/tests/Feature/Api/Public/LayoutServingTest.php +++ b/tests/Feature/Api/Public/LayoutServingTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Api\Public; +use App\Contracts\Extension\CacheInterface; use App\Enums\ExtensionStatus; use App\Models\Template; use App\Models\TemplateLayout; @@ -210,9 +211,13 @@ class LayoutServingTest extends TestCase ], ]); - // PublicLayoutController 는 CacheInterface + 버전 키 ("layout.{id}.{name}.v{v}") 사용 - $cache = app(\App\Contracts\Extension\CacheInterface::class); - $cacheKey = "layout.{$template->identifier}.{$layout->name}.v0"; + // PublicLayoutController 는 CacheInterface + 버전 키 ("layout.{id}.{name}.v{v}") 사용. + // `?v` 생략 시 현재 확장 캐시 버전으로 폴백하므로 (#588 — `.v0` 사각 키 방지) + // 버전을 시드해 결정적 키로 검증한다 + Cache::put('g7:core:ext.cache_version', 1234); + + $cache = app(CacheInterface::class); + $cacheKey = "layout.{$template->identifier}.{$layout->name}.v1234"; $cache->forget($cacheKey); // Act: 첫 번째 요청 (캐시 미스, DB 조회) diff --git a/tests/Feature/Api/Public/PublicSearchControllerFailureFlagTest.php b/tests/Feature/Api/Public/PublicSearchControllerFailureFlagTest.php new file mode 100644 index 00000000..c6ef081a --- /dev/null +++ b/tests/Feature/Api/Public/PublicSearchControllerFailureFlagTest.php @@ -0,0 +1,93 @@ +<?php + +namespace Tests\Feature\Api\Public; + +use App\Enums\TotalRelation; +use App\Extension\HookManager; +use App\Search\SearchCategoryPayload; +use Tests\TestCase; + +/** + * 통합 검색 실패 표면화 계약 테스트 (공개 이슈 #103) + * + * 카테고리 검색이 예외로 실패하면 화면은 "검색 결과 없음" 과 구분되는 오류 안내를 + * 그려야 한다. 그 판정 근거인 `categories_failed` / `search_failed` 를 코어가 + * `counts_are_exact` 와 동형으로 **일괄 조립**하는 계약을 고정한다 — 모듈별 복사에 + * 맡기면 빠지는 카테고리가 생긴다. + * + * @scenario case=search_failure_flags + * + * @effects failed_flag_in_response, + * failure_flags_default_to_false + */ +class PublicSearchControllerFailureFlagTest extends TestCase +{ + protected function tearDown(): void + { + HookManager::clearFilter('core.search.results'); + + parent::tearDown(); + } + + /** + * 실패한 카테고리가 categories_failed / search_failed 로 표면화되는지 확인 + * + * @effects failed_flag_in_response + */ + public function test_failed_category_surfaces_in_response_flags(): void + { + // Given: 한 카테고리는 실패, 다른 하나는 정상인 검색 결과 + HookManager::addFilter('core.search.results', function (array $results): array { + $results['alpha'] = SearchCategoryPayload::failed(); + $results['beta'] = [ + 'total' => 3, + 'total_relation' => TotalRelation::Exact->value, + 'total_is_exact' => true, + 'items' => [], + ]; + + return $results; + }, 5); + + // When: 통합 검색을 호출 + $response = $this->getJson('/api/search?q=테스트'); + + // Then: HTTP 200 이되 실패가 응답 키로 구분된다 + $response->assertOk(); + $response->assertJsonPath('data.categories_failed.alpha', true); + $response->assertJsonPath('data.categories_failed.beta', false); + $response->assertJsonPath('data.search_failed', true); + // 실패한 0건은 "정확한 0건" 이 아니다 + $response->assertJsonPath('data.counts_are_exact.alpha', false); + } + + /** + * 실패가 없으면 두 키 모두 false 로 존재하는지 확인 + * + * 키 자체가 없으면 화면이 "실패 정보 없음(구버전)" 과 "실패 없음" 을 구분하지 못한다. + * + * @effects failure_flags_default_to_false + */ + public function test_flags_default_to_false_when_no_failure(): void + { + // Given: 정상 카테고리만 있는 검색 결과 + HookManager::addFilter('core.search.results', function (array $results): array { + $results['alpha'] = [ + 'total' => 7, + 'total_relation' => TotalRelation::Exact->value, + 'total_is_exact' => true, + 'items' => [], + ]; + + return $results; + }, 5); + + // When: 통합 검색을 호출 + $response = $this->getJson('/api/search?q=테스트'); + + // Then: 실패 키는 존재하되 전부 false + $response->assertOk(); + $response->assertJsonPath('data.categories_failed.alpha', false); + $response->assertJsonPath('data.search_failed', false); + } +} diff --git a/tests/Feature/Auth/PasswordPolicyTest.php b/tests/Feature/Auth/PasswordPolicyTest.php index 910ac599..af9cb637 100644 --- a/tests/Feature/Auth/PasswordPolicyTest.php +++ b/tests/Feature/Auth/PasswordPolicyTest.php @@ -64,6 +64,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects password_shorter_than_setting_is_rejected */ public function 설정한_최소_길이가_회원가입에_적용된다(): void { @@ -77,6 +79,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects password_meeting_setting_is_accepted */ public function 설정한_최소_길이를_만족하면_통과한다(): void { @@ -89,6 +93,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects special_char_requirement_is_enforced_when_enabled */ public function 특수문자_필수_설정이_적용된다(): void { @@ -104,6 +110,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects login_request_has_no_length_rule */ public function 로그인_규칙에는_길이_제한이_없다(): void { @@ -117,6 +125,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects policy_rule_reads_min_length_from_settings, policy_rule_reads_special_requirement_from_settings */ public function 정책_규칙이_설정값을_읽는다(): void { @@ -131,6 +141,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects empty_security_settings_fall_back_to_defaults */ public function 설정이_비어도_기본값으로_동작한다(): void { @@ -144,6 +156,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects all_password_setting_paths_share_one_rule */ public function 비로그인_비밀번호_요청은_모두_정책을_사용한다(): void { @@ -180,6 +194,8 @@ class PasswordPolicyTest extends TestCase /** * @test + * + * @effects saved_security_settings_are_visible_on_reload */ public function 두_보안_설정_키가_저장_후_재조회에_반영된다(): void { diff --git a/tests/Feature/Console/Commands/ExecuteUpgradeStepsStandaloneTest.php b/tests/Feature/Console/Commands/ExecuteUpgradeStepsStandaloneTest.php index 4059112d..ddda2859 100644 --- a/tests/Feature/Console/Commands/ExecuteUpgradeStepsStandaloneTest.php +++ b/tests/Feature/Console/Commands/ExecuteUpgradeStepsStandaloneTest.php @@ -353,6 +353,7 @@ class ExecuteUpgradeStepsStandaloneTest extends TestCase $module->shouldReceive('loadModules')->andReturnNull()->byDefault(); $plugin->shouldReceive('loadPlugins')->andReturnNull()->byDefault(); $template->shouldReceive('loadTemplates')->andReturnNull()->byDefault(); + $template->shouldReceive('ensureLoaded')->andReturnNull()->byDefault(); $this->app->instance(CoreUpdateService::class, $service); $this->app->instance(ModuleManager::class, $module); diff --git a/tests/Feature/Console/Commands/Module/ModuleArtisanCommandsTest.php b/tests/Feature/Console/Commands/Module/ModuleArtisanCommandsTest.php index 8e52de93..dfb1a5f3 100644 --- a/tests/Feature/Console/Commands/Module/ModuleArtisanCommandsTest.php +++ b/tests/Feature/Console/Commands/Module/ModuleArtisanCommandsTest.php @@ -4,10 +4,17 @@ namespace Tests\Feature\Console\Commands\Module; use App\Enums\ExtensionOwnerType; use App\Enums\ExtensionStatus; +use App\Enums\LayoutSourceType; +use App\Extension\Cache\CoreCacheDriver; +use App\Extension\ExtensionMiddlewareRegistry; use App\Extension\ModuleManager; +use App\Extension\Traits\ClearsTemplateCaches; use App\Models\Module; use App\Models\Permission; +use App\Models\Template; +use App\Models\TemplateLayout; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Cache; use Tests\Helpers\ProtectsExtensionDirectories; use Tests\TestCase; @@ -163,24 +170,97 @@ class ModuleArtisanCommandsTest extends TestCase /** * module:cache-clear 커맨드가 모든 캐시를 삭제하는지 테스트 + * + * 실재하는 상태 키(ext.modules.*_identifiers)가 소멸하고 캐시 버전이 bump 되는지 + * 검증한다 (#588 — 기존 forget 대상은 writer 없는 유령 키였다). */ public function test_cache_clear_command_clears_all_caches(): void { + $coreCache = new CoreCacheDriver(config('cache.default', 'array')); + $coreCache->put('ext.modules.active_identifiers', ['stale-module'], 3600); + $coreCache->put('ext.modules.installed_identifiers', ['stale-module'], 3600); + // 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함 + // remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다 + $coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]); + Cache::put('g7:core:ext.cache_version', 1000); + $this->artisan('module:cache-clear') ->expectsOutput(__('modules.commands.cache_clear.clearing_all')) ->assertExitCode(0); + + $this->assertNull( + $coreCache->get('ext.modules.active_identifiers'), + 'module:cache-clear 후 활성 모듈 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('ext.modules.installed_identifiers'), + 'module:cache-clear 후 설치 모듈 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('extension.middleware_index'), + 'module:cache-clear 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.' + ); + $this->assertGreaterThan( + 1000, + ClearsTemplateCaches::getExtensionCacheVersion(), + 'module:cache-clear 후 확장 캐시 버전이 bump 되어야 합니다.' + ); } /** * module:cache-clear 커맨드가 특정 모듈의 캐시만 삭제하는지 테스트 + * + * 해석 캐시(layout_resolver.{templateId}.{name})는 버전 접미사 없는 고정 키로 + * 레이아웃 row ID 를 저장하므로, 커맨드가 능동 삭제하지 않으면 TTL 동안 + * 이전 해석 결과가 유지된다 (#588 동종 보강). */ public function test_cache_clear_command_clears_single_module_cache(): void { $identifier = 'sirsoft-board'; + // 모듈 소유 레이아웃 row — 해석 캐시 키 도출(getLayoutsByModule)의 원천 + $template = Template::factory()->create([ + 'identifier' => 'cachetest-admin_basic', + 'type' => 'admin', + 'status' => ExtensionStatus::Active->value, + ]); + TemplateLayout::create([ + 'template_id' => $template->id, + 'name' => 'sirsoft-board.cache_probe', + 'source_type' => LayoutSourceType::Module->value, + 'source_identifier' => $identifier, + 'content' => ['meta' => ['title' => 'probe'], 'components' => []], + ]); + + $coreCache = new CoreCacheDriver(config('cache.default', 'array')); + $coreCache->put('ext.modules.active_identifiers', ['stale-module'], 3600); + $coreCache->put("layout_resolver.{$template->id}.sirsoft-board.cache_probe", 12345, 3600); + // 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함 + // remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다 + $coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]); + Cache::put('g7:core:ext.cache_version', 1000); + $this->artisan('module:cache-clear', ['identifier' => $identifier]) ->expectsOutput(__('modules.commands.cache_clear.clearing_single', ['module' => $identifier])) ->assertExitCode(0); + + $this->assertNull( + $coreCache->get('ext.modules.active_identifiers'), + 'module:cache-clear(단일) 후 모듈 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get("layout_resolver.{$template->id}.sirsoft-board.cache_probe"), + 'module:cache-clear(단일) 후 해당 모듈 레이아웃의 해석 캐시(layout_resolver.*)가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('extension.middleware_index'), + 'module:cache-clear(단일) 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.' + ); + $this->assertGreaterThan( + 1000, + ClearsTemplateCaches::getExtensionCacheVersion(), + 'module:cache-clear(단일) 후 확장 캐시 버전이 bump 되어야 합니다.' + ); } /** diff --git a/tests/Feature/Console/Commands/Plugin/PluginArtisanCommandsTest.php b/tests/Feature/Console/Commands/Plugin/PluginArtisanCommandsTest.php index 7bd285cc..b4932d10 100644 --- a/tests/Feature/Console/Commands/Plugin/PluginArtisanCommandsTest.php +++ b/tests/Feature/Console/Commands/Plugin/PluginArtisanCommandsTest.php @@ -4,9 +4,13 @@ namespace Tests\Feature\Console\Commands\Plugin; use App\Enums\ExtensionOwnerType; use App\Enums\ExtensionStatus; +use App\Extension\Cache\CoreCacheDriver; +use App\Extension\ExtensionMiddlewareRegistry; use App\Extension\PluginManager; +use App\Extension\Traits\ClearsTemplateCaches; use App\Models\Permission; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; use Tests\Helpers\ProtectsExtensionDirectories; use Tests\TestCase; @@ -183,12 +187,41 @@ class PluginArtisanCommandsTest extends TestCase /** * plugin:cache-clear 커맨드가 모든 캐시를 삭제하는지 테스트 + * + * 실재하는 상태 키(ext.plugins.*_identifiers)가 소멸하고 캐시 버전이 bump 되는지 + * 검증한다 (#588 — 기존 forget 대상은 writer 없는 유령 키였다). */ public function test_cache_clear_command_clears_all_caches(): void { + $coreCache = new CoreCacheDriver(config('cache.default', 'array')); + $coreCache->put('ext.plugins.active_identifiers', ['stale-plugin'], 3600); + $coreCache->put('ext.plugins.installed_identifiers', ['stale-plugin'], 3600); + // 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함 + // remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다 + $coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]); + Cache::put('g7:core:ext.cache_version', 1000); + $this->artisan('plugin:cache-clear') ->expectsOutput(__('plugins.commands.cache_clear.clearing_all')) ->assertExitCode(0); + + $this->assertNull( + $coreCache->get('ext.plugins.active_identifiers'), + 'plugin:cache-clear 후 활성 플러그인 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('ext.plugins.installed_identifiers'), + 'plugin:cache-clear 후 설치 플러그인 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('extension.middleware_index'), + 'plugin:cache-clear 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.' + ); + $this->assertGreaterThan( + 1000, + ClearsTemplateCaches::getExtensionCacheVersion(), + 'plugin:cache-clear 후 확장 캐시 버전이 bump 되어야 합니다.' + ); } /** @@ -198,9 +231,30 @@ class PluginArtisanCommandsTest extends TestCase { $identifier = 'sirsoft-daum_postcode'; + $coreCache = new CoreCacheDriver(config('cache.default', 'array')); + $coreCache->put('ext.plugins.active_identifiers', ['stale-plugin'], 3600); + // 실제 writer(ExtensionMiddlewareRegistry::buildIndex)와 동형으로 태그 포함 + // remember — flush() 는 taggable 스토어에서 태그 인덱스 기반으로 삭제한다 + $coreCache->remember('extension.middleware_index', fn () => [['stale' => true]], 3600, [ExtensionMiddlewareRegistry::CACHE_TAG]); + Cache::put('g7:core:ext.cache_version', 1000); + $this->artisan('plugin:cache-clear', ['identifier' => $identifier]) ->expectsOutput(__('plugins.commands.cache_clear.clearing_single', ['plugin' => $identifier])) ->assertExitCode(0); + + $this->assertNull( + $coreCache->get('ext.plugins.active_identifiers'), + 'plugin:cache-clear(단일) 후 플러그인 상태 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $coreCache->get('extension.middleware_index'), + 'plugin:cache-clear(단일) 후 확장 미들웨어 인덱스 캐시(extension.middleware_index)가 삭제되어야 합니다.' + ); + $this->assertGreaterThan( + 1000, + ClearsTemplateCaches::getExtensionCacheVersion(), + 'plugin:cache-clear(단일) 후 확장 캐시 버전이 bump 되어야 합니다.' + ); } /** diff --git a/tests/Feature/Console/Commands/Template/TemplateArtisanCommandsTest.php b/tests/Feature/Console/Commands/Template/TemplateArtisanCommandsTest.php index be2f464e..5cd13b55 100644 --- a/tests/Feature/Console/Commands/Template/TemplateArtisanCommandsTest.php +++ b/tests/Feature/Console/Commands/Template/TemplateArtisanCommandsTest.php @@ -3,6 +3,7 @@ namespace Tests\Feature\Console\Commands\Template; use App\Enums\ExtensionStatus; +use App\Extension\Cache\CoreCacheDriver; use App\Extension\TemplateManager; use App\Models\Template; use App\Models\TemplateLayout; @@ -366,20 +367,41 @@ class TemplateArtisanCommandsTest extends TestCase $this->artisan('template:list', ['--status' => 'inactive']) ->assertExitCode(0); - // cache-clear 커맨드 (특정 템플릿) + // cache-clear 커맨드 (특정 템플릿) — 공개 config 캐시 실소멸까지 검증 (#588, 공개 #119) + $coreCache = new CoreCacheDriver(config('cache.default', 'array')); + $coreCache->put("template.config.{$identifier}", [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + $this->artisan('template:cache-clear', ['identifier' => $identifier]) ->expectsOutput(__('templates.commands.cache_clear.clearing_single', ['template' => $identifier])) ->assertExitCode(0); + $this->assertNull( + $coreCache->get("template.config.{$identifier}"), + 'template:cache-clear(단일) 후 공개 config 캐시가 삭제되어야 합니다.' + ); + // 활성화하여 cache-clear 전체 테스트 $this->artisan('template:activate', ['identifier' => $identifier]) ->assertExitCode(0); - // cache-clear 커맨드 (전체) + // cache-clear 커맨드 (전체) — 전체 모드도 config 캐시 실소멸 검증 + $coreCache->put("template.config.{$identifier}", [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + $this->artisan('template:cache-clear') ->expectsOutput(__('templates.commands.cache_clear.clearing_all')) ->assertExitCode(0); + $this->assertNull( + $coreCache->get("template.config.{$identifier}"), + 'template:cache-clear(전체) 후 공개 config 캐시가 삭제되어야 합니다.' + ); + // 비활성화 $this->artisan('template:deactivate', ['identifier' => $identifier]) ->assertExitCode(0); diff --git a/tests/Feature/Console/CoreScheduleRegistrationTest.php b/tests/Feature/Console/CoreScheduleRegistrationTest.php new file mode 100644 index 00000000..b1317274 --- /dev/null +++ b/tests/Feature/Console/CoreScheduleRegistrationTest.php @@ -0,0 +1,337 @@ +<?php + +namespace Tests\Feature\Console; + +use App\Models\Schedule as ScheduleModel; +use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; +use Illuminate\Console\Scheduling\Event; +use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Config; +use Mockery; +use Tests\TestCase; + +/** + * 코어 만료 데이터 정리 스케줄 등록 회귀 테스트 (공개이슈 #110) + * + * 이 축의 테스트가 0건이었던 것이 "커맨드는 있는데 스케줄만 없다" 는 결함군이 + * 무가드로 통과된 구조적 원인입니다. 등록 전수와 실행 시각 기준을 함께 고정합니다. + * + * 서머타임 주의: 서머타임이 있는 시간대에서는 전환일에 특정 시각이 건너뛰거나 + * 두 번 발생할 수 있습니다. 아래 정리 배치는 모두 멱등이라 이중 실행이 데이터를 + * 해치지 않고, 건너뛴 날은 다음 날 배치가 흡수합니다. + * + * @scenario schedule_axis=code_registered, schedule_axis=admin_registered, timezone_setting=configured, timezone_setting=empty + * + * @effects all_gc_commands_are_registered_in_schedule, gc_commands_run_on_one_server_only, sanctum_prune_uses_hours_option, schedule_timezone_follows_site_general_timezone, schedule_timezone_falls_back_to_app_timezone_when_unset, app_timezone_stays_utc, admin_registered_schedule_next_run_uses_same_basis, new_commands_are_in_schedule_security_allowlist, new_commands_are_exposed_in_dev_dashboard + */ +class CoreScheduleRegistrationTest extends TestCase +{ + use RefreshDatabase; + + /** @var string 부팅 시점에 확정된 예약 시각 기준 */ + private string $bootScheduleTimezone = 'UTC'; + + /** + * B1: 코어 GC 스케줄 전수가 등록되어 있고 주기/onOneServer 가 의도대로다. + */ + public function test_core_gc_schedules_are_registered(): void + { + $expected = [ + 'layout-previews:cleanup' => '*/30 * * * *', + 'sanctum:prune-expired' => '0 4 * * *', + 'auth:clear-resets' => '5 4 * * *', + 'queue:prune-failed' => '10 4 * * *', + 'queue:prune-batches' => '15 4 * * *', + 'notification:cleanup' => '20 4 * * *', + 'ext-bundles:cleanup' => '25 4 * * *', + 'seo:prune-stats' => '30 4 * * *', + 'schedules:prune-history' => '35 4 * * *', + 'identity:prune-logs' => '40 4 * * *', + 'activity-log:prune' => '45 4 * * *', + 'notification-log:prune' => '50 4 * * *', + 'attachments:prune-orphans' => '55 4 * * *', + 'storage:prune-leftovers' => '0 5 * * *', + 'identity:expire-challenges' => '0 * * * *', + ]; + + foreach ($expected as $command => $expression) { + $event = $this->findScheduledEvent($command); + + $this->assertNotNull($event, "{$command} 스케줄이 등록되어야 합니다."); + $this->assertSame($expression, $event->expression, "{$command} 실행 주기"); + $this->assertTrue($event->onOneServer, "{$command} 는 다중 서버에서 1대만 실행해야 합니다."); + } + } + + /** + * B1: sanctum 정리는 --hours=24 로 실행된다. + */ + public function test_sanctum_prune_uses_expected_hours_option(): void + { + $event = $this->findScheduledEvent('sanctum:prune-expired'); + + $this->assertNotNull($event); + $this->assertStringContainsString('--hours', (string) $event->command); + $this->assertStringContainsString('24', (string) $event->command); + } + + /** + * B1-t: 예약 시각 기준은 사이트 설정 시간대(general.timezone)를 따른다. + */ + public function test_schedule_timezone_follows_site_general_timezone(): void + { + // 테스트가 주입한 값을 그대로 되읽으면 파생 로직을 지워도 통과한다 — + // 사이트 설정에서 app.schedule_timezone 이 실제로 파생되는지를 본다. + Config::set('app', Arr::except(config('app'), ['schedule_timezone'])); + + $this->applyGeneralSettings(['timezone' => 'Europe/Berlin']); + + $this->assertSame( + 'Europe/Berlin', + config('app.schedule_timezone'), + '사이트 설정 시간대가 예약 시각 기준으로 파생되지 않습니다' + ); + + // 사용자 표시용 기본 타임존도 같은 값에서 파생된다 (기존 계약 유지) + $this->assertSame('Europe/Berlin', config('app.default_user_timezone')); + + // 저장 타임존은 UTC 불변 — 전 도메인의 저장값 해석 기준이라 건드리지 않는다. + $this->assertSame('UTC', config('app.timezone')); + } + + /** + * B1-t: 사이트 설정에 시간대가 없으면 파생 키 자체를 만들지 않는다. + */ + public function test_schedule_timezone_is_not_derived_when_setting_is_empty(): void + { + Config::set('app', Arr::except(config('app'), ['schedule_timezone'])); + + $this->applyGeneralSettings(['site_name' => '시간대 없는 사이트']); + + $this->assertFalse( + Config::has('app.schedule_timezone'), + '시간대 미설정인데 파생 키가 생겨 Laravel 폴백이 차단됩니다' + ); + } + + /** + * B1-o: 정리 예약 전건이 관리자 화면에서도 등록 가능해야 한다 (artisan 허용목록). + * + * 허용목록에 없으면 운영자가 주기를 바꾸거나 임시로 다시 걸 수 없고, 그 사실이 + * 등록 시점의 거부로만 드러난다. + */ + public function test_gc_commands_are_registerable_through_the_admin_allowlist(): void + { + $allowlist = (array) config('schedule_security.artisan.allowlist'); + $commands = $this->gcCommandNames(); + + $this->assertNotEmpty($commands, '정리 예약을 소스에서 도출하지 못했습니다.'); + + foreach ($commands as $command) { + $this->assertArrayHasKey( + $command, + $allowlist, + "{$command} 가 artisan 허용목록에 없어 관리자가 예약으로 등록할 수 없습니다." + ); + } + } + + /** + * B1-o: 이 저장소가 소유한 정리 커맨드는 개발 대시보드에서 수동 실행 가능해야 한다. + * + * 배치가 실패한 날 운영자가 손으로 돌릴 통로가 없으면 다음 배치까지 방치된다. + * 프레임워크·벤더 커맨드(`sanctum:prune-expired` 등)는 대시보드 노출 대상이 아니므로 + * 소유 커맨드(`app/Console/Commands`)로 한정한다 — 노출 정합 검사와 같은 범위다. + */ + public function test_owned_gc_commands_are_exposed_in_the_dev_dashboard(): void + { + $dashboard = (string) file_get_contents(resource_path('views/dev-dashboard.blade.php')); + $owned = $this->ownedCommandNames(); + + $checked = 0; + + foreach ($this->gcCommandNames() as $command) { + if (! in_array($command, $owned, true)) { + continue; + } + + $checked++; + $this->assertStringContainsString( + "runCommand('{$command}')", + $dashboard, + "{$command} 실행 버튼이 개발 대시보드에 없습니다." + ); + } + + $this->assertGreaterThan(0, $checked, '소유 정리 커맨드를 하나도 판정하지 못했습니다.'); + } + + /** + * 이 저장소가 소유한(= `app/Console/Commands` 에 정의된) artisan 커맨드 이름 목록. + * + * @return array<int, string> 커맨드 이름 목록 + */ + private function ownedCommandNames(): array + { + $root = base_path('app'.DIRECTORY_SEPARATOR.'Console'.DIRECTORY_SEPARATOR.'Commands'); + $names = []; + + foreach (Artisan::all() as $name => $command) { + $file = (new \ReflectionClass($command))->getFileName(); + + if ($file !== false && str_starts_with((string) $file, $root)) { + $names[] = $name; + } + } + + return $names; + } + + /** + * 정리 예약 커맨드 이름을 예약 정의 소스에서 도출합니다. + * + * 손으로 적은 목록은 예약이 늘어도 그대로라 전수를 증명하지 못합니다 — + * 다중 서버 1대 실행(`onOneServer`)으로 선언된 artisan 예약을 그대로 읽습니다. + * 예약 문자열의 옵션은 런타임 validator(`parseArtisanCommand`)와 동일하게 명령명만 남깁니다. + * + * @return array<int, string> 커맨드 이름 목록 + */ + private function gcCommandNames(): array + { + $source = (string) file_get_contents(base_path('routes/console.php')); + + $begin = strpos($source, '// gc-schedules:begin'); + $end = strpos($source, '// gc-schedules:end'); + + $this->assertNotFalse($begin, '정리 예약 구간 시작 표식이 없습니다.'); + $this->assertNotFalse($end, '정리 예약 구간 종료 표식이 없습니다.'); + + // 표식 밖의 예약(geoip 갱신·sitemap 생성 등)은 정리 배치가 아니다 — + // `onOneServer()` 만으로 거르면 그런 예약까지 대상이 되어 오탐이 난다. + $region = substr($source, $begin, $end - $begin); + + preg_match_all("/Schedule::command\(\s*'([^']+)'/", $region, $matches); + + return array_values(array_unique(array_map( + static fn (string $command): string => (string) strtok($command, ' '), + $matches[1] ?? [] + ))); + } + + /** + * B1-t: 등록된 예약 이벤트 전부가 같은 시각 기준을 공유한다. + */ + public function test_every_scheduled_event_shares_the_same_timezone(): void + { + $events = app(Schedule::class)->events(); + + $this->assertNotEmpty($events); + + // 기대값을 특정 시간대로 박으면 사이트 설정이 다른 환경·CI 에서 실패한다. + // Schedule 인스턴스가 부팅 시점에 해석한 값과 대조한다. + foreach ($events as $event) { + $this->assertSame( + $this->bootScheduleTimezone, + (string) $event->timezone, + '예약 이벤트가 서로 다른 시각 기준을 씁니다: '.$event->command + ); + } + } + + /** + * 사이트 general 설정을 주고 Provider 의 파생 로직을 실행합니다. + * + * @param array<string, mixed> $general general 카테고리 설정값 + */ + private function applyGeneralSettings(array $general): void + { + $repository = Mockery::mock(JsonConfigRepository::class); + $repository->shouldReceive('getCategory')->with('general')->andReturn($general); + $repository->shouldReceive('getCategory')->andReturn([]); + + $provider = new SettingsServiceProvider($this->app); + $apply = new \ReflectionMethod($provider, 'applyAppConfig'); + $apply->setAccessible(true); + $apply->invoke($provider, $repository); + } + + /** + * B1-t: 사이트 설정 시간대가 비어 있으면 종전대로 app.timezone 으로 폴백한다. + */ + public function test_schedule_timezone_falls_back_to_app_timezone(): void + { + $kernel = app(ConsoleKernelContract::class); + $resolve = new \ReflectionMethod($kernel, 'scheduleTimezone'); + $resolve->setAccessible(true); + + // 사이트 설정에 시간대가 없으면 Provider 가 키 자체를 세팅하지 않는다(= 부재). + Config::set('app', Arr::except(config('app'), ['schedule_timezone'])); + $this->assertFalse(Config::has('app.schedule_timezone')); + $this->assertSame(config('app.timezone'), $resolve->invoke($kernel)); + + Config::set('app.schedule_timezone', 'Asia/Seoul'); + $this->assertSame('Asia/Seoul', $resolve->invoke($kernel)); + } + + /** + * B1-a: 관리자 등록 예약의 다음 실행 시각도 같은 기준으로 계산된다. + */ + public function test_admin_registered_schedule_next_run_uses_site_timezone(): void + { + $schedule = new ScheduleModel([ + 'expression' => '0 4 * * *', + 'is_active' => true, + ]); + $schedule->calculateNextRunAt(); + + $this->assertNotNull($schedule->next_run_at); + + // 사이트 설정 시간대(Asia/Seoul, UTC+9)의 04:00 = UTC 19:00 + $this->assertSame( + '04:00', + $schedule->next_run_at->copy()->setTimezone('Asia/Seoul')->format('H:i') + ); + $this->assertSame('19:00', $schedule->next_run_at->copy()->utc()->format('H:i')); + } + + /** + * 테스트 시간대 고정 — 사이트 설정 시간대를 Asia/Seoul 로 두고 앱을 부팅한다. + */ + protected function setUp(): void + { + parent::setUp(); + + // 예약 이벤트의 시각 기준은 부팅 시점에 확정된다 — 아래 주입은 그 뒤라 + // 이미 등록된 이벤트에는 영향이 없다. 대조용으로 부팅 값을 먼저 붙잡는다. + $this->bootScheduleTimezone = (string) ( + config('app.schedule_timezone') ?? config('app.timezone', 'UTC') + ); + + // 아래 계산 시점 해석(관리자 등록 예약)을 고정하기 위한 주입. + Config::set('app.schedule_timezone', 'Asia/Seoul'); + } + + /** + * 부팅된 앱의 스케줄러에서 커맨드 이름으로 이벤트를 찾습니다. + * + * @param string $command artisan 커맨드 이름 + * @return Event|null 등록된 스케줄 이벤트 + */ + private function findScheduledEvent(string $command): ?Event + { + foreach (app(Schedule::class)->events() as $event) { + if (str_contains((string) $event->command, "'{$command}'") + || str_contains((string) $event->command, " {$command} ") + || str_ends_with((string) $event->command, " {$command}")) { + return $event; + } + } + + return null; + } +} diff --git a/tests/Feature/Console/DataRetentionCommandsTest.php b/tests/Feature/Console/DataRetentionCommandsTest.php new file mode 100644 index 00000000..5624765c --- /dev/null +++ b/tests/Feature/Console/DataRetentionCommandsTest.php @@ -0,0 +1,247 @@ +<?php + +namespace Tests\Feature\Console; + +use App\Enums\IdentityVerificationStatus; +use App\Extension\HookManager; +use App\Models\ActivityLog; +use App\Models\IdentityVerificationLog; +use App\Models\NotificationLog; +use App\Models\Schedule as ScheduleModel; +use App\Models\ScheduleHistory; +use App\Models\SeoCacheStat; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; +use Illuminate\Support\Str; +use Tests\TestCase; + +/** + * 만료/누적 데이터 정리 커맨드 테스트 (공개이슈 #110 전수 해소분) + * + * 각 커맨드가 보존 기간을 넘긴 행만 지우고 기간 이내 행은 보존하는지 고정합니다. + * + * @scenario row_age=within_retention, row_age=beyond_retention + * + * @effects rows_beyond_retention_are_deleted, rows_within_retention_are_kept, identity_challenges_transition_without_deletion, automated_prune_emits_its_own_hooks + */ +class DataRetentionCommandsTest extends TestCase +{ + use RefreshDatabase; + + /** + * B2: seo:prune-stats — 보존 기간 초과 통계만 삭제. + */ + public function test_seo_prune_stats_removes_only_expired_rows(): void + { + $old = SeoCacheStat::create(['url' => '/old', 'locale' => 'ko', 'type' => 'hit']); + $old->forceFill(['created_at' => now()->subDays(40)])->saveQuietly(); + + $recent = SeoCacheStat::create(['url' => '/recent', 'locale' => 'ko', 'type' => 'hit']); + $recent->forceFill(['created_at' => now()->subDays(5)])->saveQuietly(); + + $this->artisan('seo:prune-stats', ['--days' => 30])->assertSuccessful(); + + $this->assertDatabaseMissing('seo_cache_stats', ['id' => $old->id]); + $this->assertDatabaseHas('seo_cache_stats', ['id' => $recent->id]); + } + + /** + * B3: schedules:prune-history — 보존 기간 초과 이력만 삭제. + */ + public function test_schedule_history_prune_removes_only_expired_rows(): void + { + $schedule = ScheduleModel::create([ + 'name' => '테스트 스케줄', + 'type' => 'artisan', + 'command' => 'inspire', + 'expression' => '0 4 * * *', + 'frequency' => 'daily', + 'is_active' => true, + ]); + + $old = ScheduleHistory::create([ + 'schedule_id' => $schedule->id, + 'started_at' => now()->subDays(120), + 'status' => 'success', + ]); + $recent = ScheduleHistory::create([ + 'schedule_id' => $schedule->id, + 'started_at' => now()->subDays(10), + 'status' => 'success', + ]); + + $this->artisan('schedules:prune-history', ['--days' => 90])->assertSuccessful(); + + $this->assertDatabaseMissing('schedule_histories', ['id' => $old->id]); + $this->assertDatabaseHas('schedule_histories', ['id' => $recent->id]); + } + + /** + * B4: identity:expire-challenges — 만료 challenge 만 상태 전환(비파괴). + */ + public function test_identity_expire_challenges_transitions_only_past_due(): void + { + $pastDue = $this->createIdentityLog(IdentityVerificationStatus::Requested->value, now()->subMinutes(10)); + $future = $this->createIdentityLog(IdentityVerificationStatus::Requested->value, now()->addMinutes(10)); + + $this->artisan('identity:expire-challenges')->assertSuccessful(); + + $this->assertSame( + IdentityVerificationStatus::Expired->value, + IdentityVerificationLog::find($pastDue->id)->status->value + ); + $this->assertSame( + IdentityVerificationStatus::Requested->value, + IdentityVerificationLog::find($future->id)->status->value + ); + + // 비파괴 — 행 자체는 남는다. + $this->assertDatabaseHas('identity_verification_logs', ['id' => $pastDue->id]); + } + + /** + * B6: identity:prune-logs — 보관 기간 초과 이력만 파기. + */ + public function test_identity_prune_logs_removes_only_expired_rows(): void + { + $old = $this->createIdentityLog(IdentityVerificationStatus::Verified->value, now()->subDays(200)); + $old->forceFill(['created_at' => now()->subDays(200)])->saveQuietly(); + + $recent = $this->createIdentityLog(IdentityVerificationStatus::Verified->value, now()->subDays(10)); + $recent->forceFill(['created_at' => now()->subDays(10)])->saveQuietly(); + + $this->artisan('identity:prune-logs', ['--days' => 180])->assertSuccessful(); + + $this->assertDatabaseMissing('identity_verification_logs', ['id' => $old->id]); + $this->assertDatabaseHas('identity_verification_logs', ['id' => $recent->id]); + } + + /** + * B7: activity-log:prune — 보존 기간 초과 로그만 삭제. + */ + public function test_activity_log_prune_removes_only_expired_rows(): void + { + $old = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.old', + 'created_at' => now()->subDays(400), + ]); + $recent = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.recent', + 'created_at' => now()->subDays(10), + ]); + + $this->artisan('activity-log:prune', ['--days' => 365])->assertSuccessful(); + + $this->assertDatabaseMissing('activity_logs', ['id' => $old->id]); + $this->assertDatabaseHas('activity_logs', ['id' => $recent->id]); + } + + /** + * B8: notification-log:prune — 보존 기간 초과 발송 이력만 삭제. + */ + public function test_notification_log_prune_removes_only_expired_rows(): void + { + $old = NotificationLog::create([ + 'channel' => 'mail', + 'notification_type' => 'test', + 'recipient_identifier' => 'a@example.com', + 'status' => 'sent', + ]); + $old->forceFill(['created_at' => now()->subDays(120)])->saveQuietly(); + + $recent = NotificationLog::create([ + 'channel' => 'mail', + 'notification_type' => 'test', + 'recipient_identifier' => 'b@example.com', + 'status' => 'sent', + ]); + $recent->forceFill(['created_at' => now()->subDays(10)])->saveQuietly(); + + $this->artisan('notification-log:prune', ['--days' => 90])->assertSuccessful(); + + $this->assertDatabaseMissing('notification_logs', ['id' => $old->id]); + $this->assertDatabaseHas('notification_logs', ['id' => $recent->id]); + } + + /** + * 자동 파기는 운영자 일괄 삭제와 별개의 훅을 발행한다. + * + * 운영자 삭제 훅(`*.before_delete_many` 등)에는 본인인증 같은 대화형 가드가 물려 + * 있어 무인 예약이 탈 수 없다. 그렇다고 훅 없이 지우면 확장은 가장 큰 삭제 경로를 + * 볼 방법이 사라진다 — 전용 훅이 그 자리를 대신한다. + * + * @effects automated_prune_emits_its_own_hooks + */ + public function test_automated_prune_emits_its_own_hooks(): void + { + $fired = []; + + foreach ([ + 'core.activity_log.before_prune', + 'core.activity_log.after_prune', + 'core.notification_log.before_prune', + 'core.notification_log.after_prune', + 'core.schedule.before_prune_history', + 'core.schedule.after_prune_history', + ] as $hook) { + HookManager::addAction($hook, function () use (&$fired, $hook) { + $fired[] = $hook; + }); + } + + $this->artisan('activity-log:prune', ['--days' => 365])->assertSuccessful(); + $this->artisan('notification-log:prune', ['--days' => 90])->assertSuccessful(); + $this->artisan('schedules:prune-history', ['--days' => 90])->assertSuccessful(); + + $this->assertSame([ + 'core.activity_log.before_prune', + 'core.activity_log.after_prune', + 'core.notification_log.before_prune', + 'core.notification_log.after_prune', + 'core.schedule.before_prune_history', + 'core.schedule.after_prune_history', + ], $fired, '자동 파기 커맨드가 도메인 계층을 건너뛰면 이 훅들이 발행되지 않습니다.'); + } + + /** + * 보존 기간 하한은 도메인 계층이 소유한다 — `--days=0` 이어도 오늘 데이터는 남는다. + * + * 커맨드가 클램프를 들고 있으면 다른 호출자가 생길 때 그 보호가 따라오지 않는다. + * + * @effects retention_floor_is_owned_by_the_domain_layer + */ + public function test_retention_floor_is_enforced_below_the_command(): void + { + $today = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.today', + 'description' => '오늘 기록', + ]); + + $this->artisan('activity-log:prune', ['--days' => 0])->assertSuccessful(); + + $this->assertDatabaseHas('activity_logs', ['id' => $today->id]); + } + + /** + * 본인인증 로그 생성 헬퍼 + * + * @param string $status 상태값 + * @param Carbon $expiresAt 만료 시각 + * @return IdentityVerificationLog 생성된 로그 + */ + private function createIdentityLog(string $status, $expiresAt): IdentityVerificationLog + { + return IdentityVerificationLog::create([ + 'id' => (string) Str::uuid(), + 'provider_id' => 'test-provider', + 'purpose' => 'signup', + 'channel' => 'sms', + 'target_hash' => hash('sha256', Str::random()), + 'status' => $status, + 'expires_at' => $expiresAt, + ]); + } +} diff --git a/tests/Feature/Console/PruneOrphanAttachmentsCommandTest.php b/tests/Feature/Console/PruneOrphanAttachmentsCommandTest.php new file mode 100644 index 00000000..3fb57135 --- /dev/null +++ b/tests/Feature/Console/PruneOrphanAttachmentsCommandTest.php @@ -0,0 +1,282 @@ +<?php + +namespace Tests\Feature\Console; + +use App\Enums\AttachmentSourceType; +use App\Extension\Storage\CoreStorageDriver; +use App\Models\Attachment; +use App\Services\AttachmentService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * 고아 첨부 정리 커맨드 테스트 + * + * 판정식이 하나라도 느슨해지면 현역 첨부가 지워진다. 네 조건(소유자 없음 / 보존기간 경과 / + * 확장 소유 아님 / 현역 아님)을 각각 회귀로 고정합니다. + */ +class PruneOrphanAttachmentsCommandTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + + // 개발 환경 설정 파일에 실제 사이트 로고가 남아 있으면 그 ID 가 보호 목록에 실려 + // 같은 ID 로 생성된 픽스처까지 보호된다. 보호 목록은 각 테스트가 명시적으로 정한다. + Config::set('g7_settings.core.general.site_logo', []); + } + + /** + * 첨부 파일과 기록을 함께 만듭니다. + * + * @param array<string, mixed> $attributes 덮어쓸 속성 + * @param int $ageDays 업로드 경과 일수 + * @return Attachment 생성된 첨부 + */ + private function makeAttachment(array $attributes = [], int $ageDays = 40): Attachment + { + $filename = uniqid('orphan-').'.png'; + $path = "2026/08/14/{$filename}"; + $disk = config('attachment.disk'); + + (new CoreStorageDriver($disk))->put('', $path, 'bytes'); + + $attachment = Attachment::create(array_merge([ + 'attachmentable_type' => null, + 'attachmentable_id' => null, + 'source_type' => AttachmentSourceType::Core, + 'source_identifier' => null, + 'original_filename' => $filename, + 'stored_filename' => $filename, + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/png', + 'size' => 5, + 'collection' => 'default', + 'order' => 0, + ], $attributes)); + + $attachment->forceFill(['created_at' => now()->subDays($ageDays)])->saveQuietly(); + + return $attachment->refresh(); + } + + /** + * @effects core_orphan_prune_gate_off_by_default + */ + #[Test] + public function scheduled_run_deletes_nothing_when_the_toggle_is_off(): void + { + $attachment = $this->makeAttachment(); + + $this->artisan('attachments:prune-orphans --scheduled')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * @effects core_orphan_prune_gate_on + */ + #[Test] + public function scheduled_run_prunes_when_the_toggle_is_on(): void + { + Config::set('g7_settings.core.upload.orphan_cleanup_enabled', true); + Config::set('g7_settings.core.upload.orphan_retention_days', 30); + + $attachment = $this->makeAttachment(); + + $this->artisan('attachments:prune-orphans --scheduled')->assertSuccessful(); + + $this->assertDatabaseMissing('attachments', ['id' => $attachment->id]); + } + + /** + * 소유자가 있는 첨부는 어떤 경우에도 고아가 아니다. + * + * @scenario age=past_retention, owner_state=has_owner, protected=not_protected, source_owner=core + * + * @effects core_orphan_prune_skips_owned + */ + #[Test] + public function attachment_with_an_owner_is_never_pruned(): void + { + $attachment = $this->makeAttachment([ + 'attachmentable_type' => 'App\\Models\\User', + 'attachmentable_id' => 1, + ]); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * 확장이 소유한 첨부는 그 확장의 라이프사이클 소관이라 코어가 지우지 않는다. + * + * @scenario age=past_retention, owner_state=no_owner, protected=not_protected, source_owner=extension + * + * @effects core_orphan_prune_skips_extension_owned + */ + #[Test] + public function attachment_owned_by_an_extension_is_never_pruned(): void + { + $attachment = $this->makeAttachment([ + 'source_type' => AttachmentSourceType::Module, + 'source_identifier' => 'sirsoft-board', + ]); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * 현역 사이트 로고는 소유자가 없어도 설정이 직접 참조하므로 보호해야 한다. + * + * @scenario age=past_retention, owner_state=no_owner, protected=current_site_logo, source_owner=core + * + * @effects core_orphan_prune_protects_site_logo + */ + #[Test] + public function the_current_site_logo_is_protected(): void + { + $logo = $this->makeAttachment(['collection' => 'site_logo']); + $orphan = $this->makeAttachment(); + + Config::set('g7_settings.core.general.site_logo', [$logo->id]); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $logo->id]); + $this->assertDatabaseMissing('attachments', ['id' => $orphan->id]); + } + + /** + * 로고 보호는 커맨드를 거치지 않아도 성립해야 한다 (조회의 불변식). + * + * 보호 대상을 호출자가 넘기는 값으로 두면 그 인자를 빠뜨린 호출 하나가 운영 중인 로고를 + * 파기한다. 나머지 세 조건은 조회 자체의 불변식이므로 보호 조건만 규약으로 남을 수 없다. + * + * @scenario age=past_retention, owner_state=no_owner, protected=current_site_logo, source_owner=core + * + * @effects core_orphan_prune_protects_site_logo + */ + #[Test] + public function the_site_logo_is_protected_even_when_the_service_is_called_directly(): void + { + $logo = $this->makeAttachment(['collection' => 'site_logo']); + $orphan = $this->makeAttachment(); + + Config::set('g7_settings.core.general.site_logo', [$logo->id]); + + app(AttachmentService::class)->pruneOrphans(1, 500); + + $this->assertDatabaseHas('attachments', ['id' => $logo->id]); + $this->assertDatabaseMissing('attachments', ['id' => $orphan->id]); + } + + /** + * 보호는 **현역** 로고에만 걸린다 — 설정에서 빠진 구 로고는 회수 대상이다. + * + * 로고 교체는 저장이 성공한 뒤에야 구 파일을 파기한다. 저장이 실패하면 파기하지 않으므로 + * (설정에는 없는데 파일만 남는 상태를 막기 위한 순서다) 그렇게 남은 첨부를 거둘 마지막 + * 통로가 이 GC 다. 보호 조건이 컬렉션 전체로 넓어지면 그 통로가 막혀 구 로고가 영구 + * 잔존하므로, 보호 방향(현역 보존)과 반대 방향(구 로고 회수)을 함께 고정한다. + * + * @scenario age=past_retention, owner_state=no_owner, protected=not_protected, source_owner=core + * + * @effects core_orphan_prune_collects_replaced_site_logo + */ + #[Test] + public function a_replaced_site_logo_is_collected_once_it_leaves_the_setting(): void + { + $previous = $this->makeAttachment(['collection' => 'site_logo']); + $current = $this->makeAttachment(['collection' => 'site_logo']); + + // 교체 후 설정에는 새 로고만 남아 있다. + Config::set('g7_settings.core.general.site_logo', [$current->id]); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $current->id]); + $this->assertDatabaseMissing('attachments', ['id' => $previous->id]); + } + + /** + * @scenario age=within_retention, owner_state=no_owner, protected=not_protected, source_owner=core + * + * @effects core_orphan_prune_keeps_within_retention + */ + #[Test] + public function attachment_within_retention_is_kept(): void + { + $attachment = $this->makeAttachment(ageDays: 3); + + $this->artisan('attachments:prune-orphans --days=30')->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * @effects core_orphan_prune_dry_run + */ + #[Test] + public function dry_run_reports_targets_without_deleting(): void + { + $attachment = $this->makeAttachment(); + + $this->artisan('attachments:prune-orphans --days=1 --dry-run') + ->expectsOutputToContain('[DRY RUN]') + ->assertSuccessful(); + + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * @scenario age=past_retention, owner_state=no_owner, protected=not_protected, source_owner=core + * + * @effects core_orphan_prune_deletes_file + */ + #[Test] + public function pruning_removes_the_physical_file_as_well(): void + { + $attachment = $this->makeAttachment(); + $storage = new CoreStorageDriver($attachment->disk); + + $this->assertTrue($storage->exists('', $attachment->path)); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertFalse($storage->exists('', $attachment->path)); + } + + /** + * 소프트삭제된 고아도 회수된다. + * + * 후보 조회는 소프트삭제 행까지 포함하므로, 삭제 경로가 기본 조회를 쓰면 대상을 찾지 못해 + * 매 회차 같은 행을 실패로만 계상하고 파일·기록이 영원히 남는다. + * + * @scenario age=past_retention, owner_state=no_owner, protected=not_protected, source_owner=core + * + * @effects core_orphan_prune_deletes_soft_deleted + */ + #[Test] + public function soft_deleted_orphan_is_purged_too(): void + { + $attachment = $this->makeAttachment(); + $storage = new CoreStorageDriver($attachment->disk); + + $attachment->delete(); + + $this->assertNotNull(Attachment::withTrashed()->find($attachment->id)); + + $this->artisan('attachments:prune-orphans --days=1')->assertSuccessful(); + + $this->assertNull(Attachment::withTrashed()->find($attachment->id)); + $this->assertFalse($storage->exists('', $attachment->path)); + } +} diff --git a/tests/Feature/DevTools/BrowserLogRotationTest.php b/tests/Feature/DevTools/BrowserLogRotationTest.php new file mode 100644 index 00000000..87ce1c52 --- /dev/null +++ b/tests/Feature/DevTools/BrowserLogRotationTest.php @@ -0,0 +1,122 @@ +<?php + +namespace Tests\Feature\DevTools; + +use App\Support\DevTools\BrowserLogWriter; +use Illuminate\Http\Request; +use Monolog\Handler\RotatingFileHandler; +use Psr\Log\LoggerInterface; +use Tests\TestCase; + +/** + * 브라우저 콘솔 로그 파일 로테이션 회귀 테스트. + * + * 배경: + * `BrowserLogWriter::channel()` 이 `single` 드라이버로 로거를 만들면 + * `storage/logs/browser.log` 단일 파일에 로테이션 없이 무한 append 된다 — + * 개발 도구를 켜 둔 사이트에서 이 파일이 저장소 단일 최대 파일(수백 MB)로 + * 자라도 어떤 경로도 회수하지 않는다. 날짜별 파일 + 유한 보존일을 고정한다. + * + * @scenario vector=browser_log_write, actor_permission=debug_gate_enabled + * + * @effects browser_logs_rotate_daily_with_finite_retention + */ +class BrowserLogRotationTest extends TestCase +{ + /** + * 브라우저 로그는 날짜별 파일에 기록되고, 단일 browser.log 에는 더 이상 쓰지 않는다. + */ + public function test_browser_logs_are_written_to_a_dated_file(): void + { + $datedPath = storage_path('logs/browser-'.now()->format('Y-m-d').'.log'); + $singlePath = storage_path('logs/browser.log'); + + $datedExistedBefore = file_exists($datedPath); + // 레거시 단일 파일은 수백 MB 일 수 있다 — 내용을 읽지 않고 크기만 대조한다. + $singleSizeBefore = file_exists($singlePath) ? filesize($singlePath) : null; + + $marker = 'browser-log-rotation-regression-'.uniqid(); + + try { + $request = Request::create('/_boost/browser-logs', 'POST', [ + 'logs' => [ + ['type' => 'log', 'data' => [$marker], 'url' => 'https://example.test/'], + ], + ]); + + BrowserLogWriter::handle($request); + + $this->assertFileExists($datedPath, '브라우저 로그가 날짜별 파일에 기록되지 않았다'); + $this->assertStringContainsString( + $marker, + $this->tail($datedPath), + '날짜별 파일 꼬리에서 이번 기록을 찾지 못했다' + ); + + clearstatcache(); + $singleSizeAfter = file_exists($singlePath) ? filesize($singlePath) : null; + $this->assertSame( + $singleSizeBefore, + $singleSizeAfter, + '레거시 단일 browser.log 에 여전히 기록이 추가되고 있다' + ); + } finally { + if (! $datedExistedBefore && file_exists($datedPath)) { + @unlink($datedPath); + } + } + } + + /** + * 로거는 로테이션 핸들러를 쓰고 보존일이 유한해야 한다 (0 = 무제한 금지). + */ + public function test_browser_log_channel_uses_rotating_handler_with_finite_retention(): void + { + $method = new \ReflectionMethod(BrowserLogWriter::class, 'channel'); + /** @var LoggerInterface $logger */ + $logger = $method->invoke(null); + + $handlers = $logger->getLogger()->getHandlers(); + $this->assertNotEmpty($handlers, '로그 핸들러를 확인할 수 없다'); + + $rotating = null; + foreach ($handlers as $handler) { + if ($handler instanceof RotatingFileHandler) { + $rotating = $handler; + } + } + + $this->assertNotNull($rotating, '브라우저 로그 채널이 로테이션(daily) 핸들러를 쓰지 않는다'); + + $maxFiles = new \ReflectionProperty(RotatingFileHandler::class, 'maxFiles'); + $this->assertGreaterThan( + 0, + $maxFiles->getValue($rotating), + '보존일이 무제한(0)이다 — 날짜별 파일이 무한 누적된다' + ); + } + + /** + * 파일 꼬리(마지막 16KB)를 읽습니다 — 큰 로그 파일 전체 적재 방지. + * + * @param string $path 대상 파일 경로 + * @return string 꼬리 내용 + */ + private function tail(string $path): string + { + $size = (int) filesize($path); + $handle = fopen($path, 'rb'); + + if ($handle === false) { + return ''; + } + + try { + fseek($handle, max(0, $size - 16384)); + + return (string) stream_get_contents($handle); + } finally { + fclose($handle); + } + } +} diff --git a/tests/Feature/Http/CoreExceptionMessageLeakTest.php b/tests/Feature/Http/CoreExceptionMessageLeakTest.php new file mode 100644 index 00000000..2e70a67b --- /dev/null +++ b/tests/Feature/Http/CoreExceptionMessageLeakTest.php @@ -0,0 +1,92 @@ +<?php + +namespace Tests\Feature\Http; + +use App\Models\User; +use App\Services\UserService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Validation\ValidationException; +use Tests\TestCase; + +/** + * 코어 서비스의 generic catch 가 예외 원문(SQLSTATE·쿼리·DB명)을 사용자 응답 메시지에 + * 노출하지 않는지 검증합니다. + * + * 결함(N-9): UserService 의 catch 블록이 __('...', ['error' => $e->getMessage()]) 로 예외 + * 원문을 사용자 메시지에 실었다(회원 대상 경로 포함). 원본은 Log 로만 남기고 사용자 메시지는 + * 원문 없는 고정 i18n 키로 교체한다. + * + * 범위 주의: Module/Plugin/Template/Settings 의 설치·활성화·제거 실패 메시지는 관리자 전용 + * 진단 정보로서 예외 원문 노출을 유지한다(exceptions.md — 관리자 면의 진단 원문 노출은 허용). + * 따라서 그 키들은 본 테스트의 대상이 아니다. + */ +class CoreExceptionMessageLeakTest extends TestCase +{ + use RefreshDatabase; + + /** + * 사용자 생성 실패(DB 예외) 시 예외 메시지에 SQL 원문이 없어야 합니다. + * + * @scenario case=user_create_failure_masked + * + * @effects failure_response_omits_sql_text + */ + public function test_user_create_failure_message_does_not_leak_sql(): void + { + $existing = User::factory()->create(); + + // 동일 이메일로 생성 시도 → unique 제약 위반(QueryException) → generic catch 진입 + $data = User::factory()->raw(['email' => $existing->email]); + + $service = app(UserService::class); + + try { + $service->createUser($data); + $this->fail('중복 이메일로 생성이 실패해 ValidationException 이 발생해야 합니다.'); + } catch (ValidationException $e) { + $this->assertMessagesHaveNoSqlLeak($e->errors()); + } + } + + /** + * 영향받은 다국어 키가 원문 placeholder(:error) 를 담지 않고 정상 해석되어야 합니다. + * + * @scenario case=masked_lang_keys + * + * @effects masked_lang_keys_have_no_error_placeholder + */ + public function test_affected_lang_keys_have_no_error_placeholder(): void + { + $keys = [ + 'user.create_failed', + 'user.update_failed', + 'user.delete_failed', + ]; + + foreach (['ko', 'en'] as $locale) { + foreach ($keys as $key) { + $message = __($key, [], $locale); + + $this->assertStringNotContainsString(':error', $message, "{$key} ({$locale}) 에 :error placeholder 가 남아 있습니다."); + $this->assertNotSame($key, $message, "{$key} ({$locale}) 가 해석되지 않았습니다."); + } + } + } + + /** + * 메시지 집합에 SQL 원문/키워드/placeholder 잔재가 없음을 단언합니다. + * + * @param array<string, array<int, string>> $errors ValidationException errors 배열 + */ + private function assertMessagesHaveNoSqlLeak(array $errors): void + { + $messages = collect($errors)->flatten()->implode(' '); + + $this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $messages); + $this->assertStringNotContainsStringIgnoringCase('select ', $messages); + $this->assertStringNotContainsStringIgnoringCase('insert ', $messages); + $this->assertStringNotContainsStringIgnoringCase('update ', $messages); + $this->assertStringNotContainsStringIgnoringCase('delete ', $messages); + $this->assertStringNotContainsString(':error', $messages); + } +} diff --git a/tests/Feature/Http/GenericCatchStatusCodeContractTest.php b/tests/Feature/Http/GenericCatchStatusCodeContractTest.php new file mode 100644 index 00000000..5c7474a3 --- /dev/null +++ b/tests/Feature/Http/GenericCatchStatusCodeContractTest.php @@ -0,0 +1,380 @@ +<?php + +namespace Tests\Feature\Http; + +use Tests\TestCase; + +/** + * generic catch 의 상태코드 · 예외 원문 노출 전수 계약 (#104) + * + * `catch (\Exception)` / `catch (\Throwable)` 는 도메인 예외가 아닌 것을 잡는 자리다. + * 여기서 4xx 를 돌려주면 인프라 장애·코드 결함이 "입력 오류" 로 위장되어 장애 인지가 늦어진다. + * 그리고 이미 번역된 예외 메시지를 응답의 메시지 **키** 자리에 넘기면 키 해석에 실패해 + * 원문(스택 힌트·SQL 상태코드 포함)이 그대로 사용자 화면에 나간다. + * + * 이 계약은 코어 한 곳에서만 세워졌던 것이 아니다 — 같은 형태가 코어와 번들 확장 양쪽에 + * 흩어져 있었다. 판정기를 한 확장 안에 두면 그 확장 밖에서 같은 결함이 재발해도 red 가 + * 되지 않으므로, 코어 + 모든 번들 모듈/플러그인의 컨트롤러를 한 번에 훑는다. + */ +class GenericCatchStatusCodeContractTest extends TestCase +{ + /** + * 의도적으로 4xx 를 유지하는 지점 (사유를 함께 기록한다). + * + * @var array<string, string> + */ + private const INTENTIONAL_4XX = [ + // 업로드된 zip 의 manifest 해석 실패는 사용자가 올린 파일의 문제다 — + // 미리보기 단계이므로 설치 부작용이 없고, 운영자는 파일을 고쳐 다시 올리면 된다. + 'app/Http/Controllers/Api/Admin/LanguagePackController.php::manifestPreview' => '업로드 파일 해석 실패 = 클라이언트 입력 오류', + 'app/Http/Controllers/Api/Admin/ModuleController.php::manifestPreview' => '업로드 파일 해석 실패 = 클라이언트 입력 오류', + 'app/Http/Controllers/Api/Admin/PluginController.php::manifestPreview' => '업로드 파일 해석 실패 = 클라이언트 입력 오류', + 'app/Http/Controllers/Api/Admin/TemplateController.php::manifestPreview' => '업로드 파일 해석 실패 = 클라이언트 입력 오류', + // 카테고리 이미지 서빙은 요청 파라미터 기반 파일 해석(검증성 로직) 비중이 크다. + 'modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Admin/CategoryController.php::downloadImage' => '검증성 로직(요청 파라미터 기반 파일 해석) 비중이 큼', + 'modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/CategoryImageController.php::download' => '검증성 로직(요청 파라미터 기반 파일 해석) 비중이 큼', + ]; + + /** + * 예외 원문을 메시지 키로 넘기는 것이 아직 남아 있는 지점. + * + * 코어 확장 설치 경로는 `\RuntimeException(__('key', [...]))` 형태로 서비스·헬퍼 + * 40여 곳에서 던져진다. 키를 들고 다니는 예외로 승격하려면 설치/업데이트 경로 전체를 + * 건드려야 하므로 별도 작업으로 분리한다 — 여기 남겨 두는 이유는 "판정기가 못 봐서 + * 통과한 것" 과 "알고 남긴 것" 을 구분하기 위해서다. 목록이 늘어나면 실패한다. + * + * @var array<int, string> + */ + private const KNOWN_EXCEPTION_TEXT_AS_KEY = [ + 'app/Http/Controllers/Api/Admin/ModuleController.php:437', + 'app/Http/Controllers/Api/Admin/ModuleController.php:461', + 'app/Http/Controllers/Api/Admin/PluginController.php:448', + 'app/Http/Controllers/Api/Admin/PluginController.php:472', + 'app/Http/Controllers/Api/Admin/TemplateController.php:352', + 'app/Http/Controllers/Api/Admin/TemplateController.php:376', + ]; + + /** + * 광역 `\RuntimeException` catch 가 4xx 를 반환하는 것이 아직 남아 있는 지점. + * + * `\RuntimeException` 은 도메인 예외의 부모가 되기 쉬워, 도메인 실패를 typed 로 + * 승격한 뒤에도 이 catch 를 남겨 두면 남는 것은 인프라 예외뿐인데 그것까지 4xx 로 + * 뭉갠다. 위 `KNOWN_EXCEPTION_TEXT_AS_KEY` 와 같은 줄이며, 같은 설치 경로 개편에서 + * 함께 해소된다. + * + * @var array<string, string> + */ + private const KNOWN_BROAD_RUNTIME_CATCH_4XX = [ + 'app/Http/Controllers/Api/Admin/ModuleController.php::installFromFile' => '설치 경로가 도메인 실패를 키 없는 RuntimeException 으로 던진다 — 예외 승격과 함께 해소', + 'app/Http/Controllers/Api/Admin/ModuleController.php::installFromGithub' => '동일', + 'app/Http/Controllers/Api/Admin/PluginController.php::installFromFile' => '동일', + 'app/Http/Controllers/Api/Admin/PluginController.php::installFromGithub' => '동일', + 'app/Http/Controllers/Api/Admin/TemplateController.php::installFromFile' => '동일', + 'app/Http/Controllers/Api/Admin/TemplateController.php::installFromGithub' => '동일', + ]; + + /** + * 스캔 대상 컨트롤러 루트 목록 (코어 + 번들 모듈/플러그인). + * + * @return array<int, string> 절대 경로 목록 + */ + private function controllerRoots(): array + { + $roots = [base_path('app/Http/Controllers')]; + + // 컨트롤러 위치는 확장마다 규약이 갈린다 — 모듈·일부 플러그인은 + // `src/Http/Controllers`, PG 플러그인 4종은 `src/Controllers` 를 쓴다. + // 한 규약만 보면 다른 규약을 쓰는 확장이 통째로 사각이 된다(실제로 PG + // 플러그인 44파일이 그랬고, 그 안에 위반 1건이 살아 있었다). + $conventions = ['src/Http/Controllers', 'src/Controllers']; + + foreach (['modules', 'plugins'] as $kind) { + $bundled = base_path($kind.'/_bundled'); + if (! is_dir($bundled)) { + continue; + } + foreach (scandir($bundled) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + foreach ($conventions as $convention) { + $dir = $bundled.'/'.$entry.'/'.$convention; + if (is_dir($dir)) { + $roots[] = $dir; + } + } + } + } + + return $roots; + } + + /** + * 루트 아래 PHP 파일을 저장소 상대 경로로 수집합니다. + * + * @return array<int, array{path: string, rel: string}> + */ + private function collectFiles(): array + { + $files = []; + $prefix = str_replace('\\', '/', base_path()).'/'; + + foreach ($this->controllerRoots() as $root) { + $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root)); + foreach ($it as $f) { + if (! $f->isFile() || ! str_ends_with($f->getFilename(), '.php')) { + continue; + } + $full = str_replace('\\', '/', $f->getPathname()); + $files[] = ['path' => $f->getPathname(), 'rel' => str_replace($prefix, '', $full)]; + } + } + + usort($files, fn ($a, $b) => strcmp($a['rel'], $b['rel'])); + + return $files; + } + + /** + * generic catch 블록의 응답 상태코드를 추출합니다. + * + * @return array<int, array{file: string, method: string, line: int, status: int}> + */ + private function collectGenericCatchStatuses(): array + { + $found = []; + + foreach ($this->collectFiles() as $file) { + $lines = file($file['path'], FILE_IGNORE_NEW_LINES); + $method = '?'; + + foreach ($lines as $i => $line) { + if (preg_match('/public function (\w+)\(/', $line, $m)) { + $method = $m[1]; + } + + // 광역 catch: \Exception / \Throwable / \RuntimeException 단독 + // + // `\RuntimeException` 을 포함하는 이유는 그것이 도메인 예외의 부모가 되기 + // 때문이다 — 서비스가 도메인 실패를 typed 로 승격하고 나면 이 catch 에 + // 남는 것은 인프라 예외뿐인데, 4xx 를 돌려주면 그대로 위장된다. 실제로 + // `User/ProductInquiryController` 4곳이 이 사각에 있었다. + if (! preg_match('/\}\s*catch\s*\(\s*\\\\?(Exception|Throwable|RuntimeException)\s*(\$\w+)?\s*\)/', $line)) { + continue; + } + + $status = null; + for ($j = $i + 1; $j < min($i + 25, count($lines)); $j++) { + if (preg_match('/\}\s*catch\s*\(/', $lines[$j])) { + break; + } + if (preg_match('/^\s*(\d{3}),?\s*$/', $lines[$j], $sm)) { + $status = (int) $sm[1]; + break; + } + // ->error('key', 404) 처럼 한 줄에 쓰인 형태. + // 첫 인자에 함수 호출이 올 수 있으므로(`error($e->getMessage(), 422)`) + // 괄호를 배제하지 않는다 — 배제하면 그 형태를 통째로 놓친다. + if (preg_match('/(?:error|moduleError)\(.*?,\s*(\d{3})\s*[,)]/', $lines[$j], $sm)) { + $status = (int) $sm[1]; + break; + } + // 상태코드 인자를 아예 생략한 한 줄 호출 — ResponseHelper 기본값 400 이 적용된다. + // 숫자만 찾는 판정은 이 형태를 통째로 놓친다 (실제 사각이었다: WishlistController). + if (preg_match("/moduleError\(\s*'[^']+',\s*'[^']+'\s*\)/", $lines[$j]) + || preg_match("/[^e]error\(\s*'[^']+'\s*\)/", $lines[$j])) { + $status = 400; + break; + } + } + + if ($status !== null) { + $found[] = ['file' => $file['rel'], 'method' => $method, 'line' => $i + 1, 'status' => $status]; + } + } + } + + return $found; + } + + /** + * 판정기가 실제로 코어와 확장 양쪽을 보고 있는지 먼저 확인한다. + * + * 모집단이 비거나 한쪽으로 쏠리면 이후 단언은 초록이어도 아무것도 증명하지 못한다. + */ + public function test_scanner_population_covers_core_and_bundled_extensions(): void + { + $files = $this->collectFiles(); + $rels = array_column($files, 'rel'); + + $this->assertNotEmpty($rels, '컨트롤러 스캔 결과가 비어 있습니다 — 판정기가 대상을 못 찾고 있습니다.'); + + $core = array_filter($rels, fn ($r) => str_starts_with($r, 'app/Http/Controllers/')); + $ext = array_filter($rels, fn ($r) => str_starts_with($r, 'modules/_bundled/') || str_starts_with($r, 'plugins/_bundled/')); + + $this->assertNotEmpty($core, '코어 컨트롤러가 모집단에 없습니다.'); + $this->assertNotEmpty($ext, '번들 확장 컨트롤러가 모집단에 없습니다.'); + + // 이 브랜치가 계약을 세운 세 영역이 모두 잡혀야 한다 + $this->assertContains('modules/_bundled/sirsoft-ecommerce/src/Http/Controllers/Public/OrderController.php', $rels); + $this->assertContains('modules/_bundled/sirsoft-board/src/Http/Controllers/Admin/BoardTypeController.php', $rels); + $this->assertContains('app/Http/Controllers/Api/Admin/LanguagePackController.php', $rels); + + // 두 컨트롤러 규약이 모두 잡혀야 한다. `src/Http/Controllers` 앵커만 두면 + // `src/Controllers` 를 쓰는 확장이 빠져도 이 테스트가 초록으로 남는다. + $this->assertContains('plugins/_bundled/sirsoft-pay_nicepayments/src/Controllers/AdminEscrowController.php', $rels); + + // 규약별 확장 수를 세어, 어느 한쪽 규약이 통째로 빠지는 회귀를 막는다. + $byConvention = []; + foreach ($rels as $rel) { + if (preg_match('#^(modules|plugins)/_bundled/([^/]+)/(src/Http/Controllers|src/Controllers)/#', $rel, $m)) { + $byConvention[$m[3]][$m[2]] = true; + } + } + + $this->assertArrayHasKey('src/Http/Controllers', $byConvention, '`src/Http/Controllers` 규약 확장이 모집단에 없습니다.'); + $this->assertArrayHasKey('src/Controllers', $byConvention, '`src/Controllers` 규약 확장이 모집단에 없습니다 — PG 플러그인이 사각이 됩니다.'); + } + + /** + * generic catch 는 5xx 를 반환해야 한다 (의도적 예외는 상수에 명시). + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects update_shipping_address_infrastructure_exception_returns_500_not_422, guest_confirm_option_has_no_domain_exception_so_generic_catch_returns_500 + */ + public function test_generic_catch_blocks_never_return_4xx(): void + { + $sites = $this->collectGenericCatchStatuses(); + + $this->assertNotEmpty($sites, 'generic catch 를 하나도 찾지 못했습니다 — 판정기 정규식을 확인하세요.'); + + $declared = self::INTENTIONAL_4XX + self::KNOWN_BROAD_RUNTIME_CATCH_4XX; + + $violations = []; + $hit = []; + foreach ($sites as $s) { + if ($s['status'] < 400 || $s['status'] >= 500) { + continue; + } + $key = $s['file'].'::'.$s['method']; + if (array_key_exists($key, $declared)) { + $hit[$key] = true; + + continue; + } + $violations[] = "{$s['file']}:{$s['line']} ({$s['method']}) -> {$s['status']}"; + } + + $this->assertSame( + [], + $violations, + "광역 catch 가 4xx 를 반환합니다 (인프라 장애가 입력 오류로 위장됨):\n".implode("\n", $violations) + ); + + // 면제 목록도 모집단이다 — 해소된 지점이 남아 있으면 "알고 남긴 것" 과 + // "이미 고쳐진 것" 이 구분되지 않아, 다음 사람이 그 줄을 근거로 삼는다. + // `KNOWN_EXCEPTION_TEXT_AS_KEY` 는 이 검사를 갖고 있었는데 4xx 축에는 없었다. + $stale = array_values(array_diff(array_keys($declared), array_keys($hit))); + + $this->assertSame( + [], + $stale, + "면제 목록에 이미 해소된 항목이 남아 있습니다 (목록을 줄이세요):\n".implode("\n", $stale) + ); + } + + /** + * `$e->getMessage(),` 한 줄이 응답 헬퍼의 **메시지 키 자리**인지 판정한다. + * + * 여는 호출을 위로 되짚어 `error(` / `moduleError(` 를 찾고, 그 사이에 놓인 + * 인자 줄 수로 위치를 센다. `error()` 는 0번째, `moduleError()` 는 확장 식별자 + * 다음인 1번째가 메시지 키다. 그 밖의 호출(도메인 서비스·리다이렉트 헬퍼)은 + * 아무리 같은 모양이어도 응답 키가 아니므로 위반이 아니다. + * + * @param array<int, string> $lines 파일 전체 줄 + * @param int $index `$e->getMessage(),` 가 있는 줄의 0-based 인덱스 + * @return bool 메시지 키 자리면 true + */ + private function isResponseMessageKeyPosition(array $lines, int $index): bool + { + $argLinesBefore = 0; + + // 여는 줄은 멀지 않다 — 인자 6개를 넘겨 쓰는 응답 헬퍼는 없다. + for ($j = $index - 1; $j >= 0 && $j >= $index - 7; $j--) { + $candidate = trim($lines[$j]); + + if ($candidate === '') { + continue; + } + + // 여는 호출 발견 — 위치로 판정한다. + if (preg_match('/(?:ResponseHelper::)?(?:->)?\b(error|moduleError)\($/', $candidate, $m)) { + $keyPosition = $m[1] === 'moduleError' ? 1 : 0; + + return $argLinesBefore === $keyPosition; + } + + // 여는 줄에 도달하기 전의 다른 호출/블록 경계면 응답 헬퍼가 아니다. + if (str_ends_with($candidate, '(') || str_ends_with($candidate, '{') || str_ends_with($candidate, ';')) { + return false; + } + + $argLinesBefore++; + } + + return false; + } + + /** + * 예외 메시지 원문을 응답 메시지 키 자리에 넘기지 않는다. + * + * `error($e->getMessage(), 422)` 는 이미 번역된 문장을 키로 해석하려다 실패해 + * 원문을 그대로 노출한다. + * + * @scenario actor=member, change_mode=manual, address_region=domestic, e2e_browser=chromium + * + * @effects update_shipping_address_500_response_excludes_exception_message, guest_action_5xx_responses_exclude_exception_message + */ + public function test_exception_message_is_not_passed_as_response_message_key(): void + { + $violations = []; + + foreach ($this->collectFiles() as $file) { + $lines = file($file['path'], FILE_IGNORE_NEW_LINES); + + foreach ($lines as $i => $line) { + // 한 줄 형태: error($e->getMessage(), 422) / moduleError('mod', $e->getMessage(), …) + if (preg_match('/(?:error|moduleError)\((?:[^,]+,\s*)?\$e->getMessage\(\)/', $line)) { + $violations[] = $file['rel'].':'.($i + 1); + + continue; + } + + // 여러 줄 형태: 인자가 줄마다 놓인 호출에서 `$e->getMessage(),` 만 있는 줄. + // 이 모양 자체는 응답과 무관한 곳에도 흔하다 — 도메인 서비스 호출 + // (`failPayment($order, 'CODE', $e->getMessage())`)이나 리다이렉트 헬퍼 + // 인자가 그렇다. 여는 호출을 되짚어 **메시지 키 자리**일 때만 위반이다. + if (preg_match('/^\s*\$e->getMessage\(\),\s*$/', $line) + && $this->isResponseMessageKeyPosition($lines, $i)) { + $violations[] = $file['rel'].':'.($i + 1); + } + } + } + + $unexpected = array_values(array_diff($violations, self::KNOWN_EXCEPTION_TEXT_AS_KEY)); + $stale = array_values(array_diff(self::KNOWN_EXCEPTION_TEXT_AS_KEY, $violations)); + + $this->assertSame( + [], + $unexpected, + "예외 메시지 원문이 응답 메시지 키로 전달됩니다:\n".implode("\n", $unexpected) + ); + + $this->assertSame( + [], + $stale, + "KNOWN_EXCEPTION_TEXT_AS_KEY 에 이미 해소된 항목이 남아 있습니다 (목록을 줄이세요):\n".implode("\n", $stale) + ); + } +} diff --git a/tests/Feature/Http/RedirectQueryExceptionTextContractTest.php b/tests/Feature/Http/RedirectQueryExceptionTextContractTest.php new file mode 100644 index 00000000..c9cd9f40 --- /dev/null +++ b/tests/Feature/Http/RedirectQueryExceptionTextContractTest.php @@ -0,0 +1,156 @@ +<?php + +namespace Tests\Feature\Http; + +use Tests\TestCase; + +/** + * 실패 리다이렉트 쿼리에 예외 원문을 싣지 않는다는 전수 계약 + * + * 결제 콜백은 응답 본문이 아니라 **리다이렉트 쿼리스트링**으로 실패 사유를 전달한다. + * 그래서 응답 메시지 키를 보는 계약(GenericCatchStatusCodeContractTest)의 축 밖에 있고, + * 여기에 `$e->getMessage()` 를 실으면 브라우저 주소창과 참조 로그에 내부 오류 원문 + * (SQL 상태코드·클래스명·경로)이 그대로 남는다. 결제 실패 페이지는 이 값을 화면에 + * 그대로 출력하므로 구매자에게도 노출된다. + * + * 사유 식별은 같은 쿼리의 `error` 코드가 담당하고, 사람에게 보일 문구는 다국어 키로 + * 해석한 문장이어야 한다. + */ +class RedirectQueryExceptionTextContractTest extends TestCase +{ + /** + * 아직 해소하지 못한 지점 (사유와 함께 선언한다). + * + * 목록이 늘어나면 실패하고, 해소된 항목이 남아 있어도 실패한다. + * + * @var array<int, string> + */ + private const KNOWN_REDIRECT_EXCEPTION_TEXT = []; + + /** + * 스캔 대상 컨트롤러 루트 목록 (코어 + 번들 모듈/플러그인). + * + * 컨트롤러 위치 규약은 확장마다 갈린다 — 결제대행사 플러그인은 `src/Controllers`, + * 그 밖은 `src/Http/Controllers` 를 쓴다. 한 규약만 보면 다른 규약이 통째로 사각이 된다. + * + * @return array<int, string> 절대 경로 목록 + */ + private function controllerRoots(): array + { + $roots = [base_path('app/Http/Controllers')]; + $conventions = ['src/Http/Controllers', 'src/Controllers']; + + foreach (['modules', 'plugins'] as $kind) { + $bundled = base_path($kind.'/_bundled'); + if (! is_dir($bundled)) { + continue; + } + foreach (scandir($bundled) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + foreach ($conventions as $convention) { + $dir = $bundled.'/'.$entry.'/'.$convention; + if (is_dir($dir)) { + $roots[] = $dir; + } + } + } + } + + return $roots; + } + + /** + * 스캔 대상 PHP 파일을 저장소 상대 경로와 함께 수집합니다. + * + * @return array<int, array{path: string, rel: string}> 파일 목록 + */ + private function collectFiles(): array + { + $files = []; + $prefix = str_replace('\\', '/', base_path()).'/'; + + foreach ($this->controllerRoots() as $root) { + $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root)); + foreach ($it as $f) { + if (! $f->isFile() || ! str_ends_with($f->getFilename(), '.php')) { + continue; + } + $full = str_replace('\\', '/', $f->getPathname()); + $files[] = ['path' => $f->getPathname(), 'rel' => str_replace($prefix, '', $full)]; + } + } + + return $files; + } + + /** + * 판정기가 실제로 결제대행사 플러그인을 보고 있는지 스스로 단언합니다. + * + * 모집단이 비어 있으면 이 계약은 아무것도 증명하지 못한 채 초록이 된다. + * + * @scenario contract_check=scanner_population + * + * @effects redirect_contract_scanner_population_covers_payment_gateway_plugins + */ + public function test_scanner_population_covers_payment_gateway_plugins(): void + { + $rels = array_column($this->collectFiles(), 'rel'); + + $this->assertNotEmpty($rels, '스캔 대상 파일이 비어 있다.'); + + foreach (['sirsoft-pay_kginicis', 'sirsoft-pay_nhnkcp', 'sirsoft-tosspayments', 'sirsoft-pay_nicepayments'] as $plugin) { + $hit = array_filter($rels, fn ($r) => str_contains($r, 'plugins/_bundled/'.$plugin.'/')); + $this->assertNotEmpty($hit, $plugin.' 의 컨트롤러가 모집단에 없다.'); + } + + // 리다이렉트 쿼리를 조립하는 지점 자체가 모집단에 있어야 한다. + $withFailUrl = array_filter( + $this->collectFiles(), + fn ($f) => str_contains((string) file_get_contents($f['path']), 'resolveFailUrl(') + ); + $this->assertNotEmpty($withFailUrl, 'resolveFailUrl 을 쓰는 파일이 모집단에 없다.'); + } + + /** + * 리다이렉트 쿼리의 사람이 볼 값에 예외 원문을 싣지 않는다. + * + * @scenario contract_check=redirect_query_text + * + * @effects redirect_query_omits_exception_text_repository_wide + */ + public function test_redirect_query_does_not_carry_exception_text(): void + { + $violations = []; + + foreach ($this->collectFiles() as $file) { + $source = (string) file_get_contents($file['path']); + if (! str_contains($source, '$e->getMessage()')) { + continue; + } + + $lines = preg_split('/\r\n|\r|\n/', $source) ?: []; + foreach ($lines as $i => $line) { + // `'message' => $e->getMessage(),` 처럼 사람이 볼 쿼리 값 자리에 원문을 넣는 형태. + if (! preg_match('/[\'"](message|msg|reason|detail)[\'"]\s*=>\s*\$e->getMessage\(\)/', $line)) { + continue; + } + + // 리다이렉트 URL 조립 블록 안인지 — 위쪽 12줄에서 조립 호출을 되짚는다. + $context = implode("\n", array_slice($lines, max(0, $i - 12), min(12, $i))); + if (! preg_match('/resolveFailUrl\(|redirect\(|RedirectResponse|->away\(/', $context)) { + continue; + } + + $violations[] = $file['rel'].':'.($i + 1); + } + } + + $unexpected = array_values(array_diff($violations, self::KNOWN_REDIRECT_EXCEPTION_TEXT)); + $stale = array_values(array_diff(self::KNOWN_REDIRECT_EXCEPTION_TEXT, $violations)); + + $this->assertSame([], $unexpected, "리다이렉트 쿼리에 예외 원문을 싣는 지점:\n".implode("\n", $unexpected)); + $this->assertSame([], $stale, "이미 해소되어 면제 목록에서 지워야 하는 항목:\n".implode("\n", $stale)); + } +} diff --git a/tests/Feature/Middleware/SetLocaleExpiredTokenTest.php b/tests/Feature/Middleware/SetLocaleExpiredTokenTest.php new file mode 100644 index 00000000..4586234f --- /dev/null +++ b/tests/Feature/Middleware/SetLocaleExpiredTokenTest.php @@ -0,0 +1,89 @@ +<?php + +namespace Tests\Feature\Middleware; + +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Route; +use Laravel\Sanctum\PersonalAccessToken; +use Tests\TestCase; + +/** + * SetLocale 미들웨어가 만료된 Sanctum 토큰을 인정하지 않는지 검증합니다. + * + * 결함(㉗): resolveUser() 가 토큰의 만료(expires_at) 여부를 검사하지 않아 + * 만료된 토큰으로도 그 회원의 로케일이 해석되었다. OptionalSanctumMiddleware 와 + * 동일한 만료 검사를 이식하여 만료 토큰은 guest(null) 로 취급한다. + */ +class SetLocaleExpiredTokenTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + + // 인증 불필요 공개 라우트 — auth:sanctum 이 만료 토큰을 먼저 차단하지 않도록 + // SetLocale(api 그룹) 단독 동작을 관찰한다. + Route::middleware(['api'])->get('/api/test-expired-locale', function () { + return response()->json([ + 'locale' => App::getLocale(), + ]); + }); + } + + /** + * 만료된 토큰으로 요청하면 그 회원의 언어 설정을 따르지 않고 + * guest 로케일(Accept-Language)로 폴백해야 합니다. + * + * @scenario case=expired_token_locale + * + * @effects expired_token_treated_as_guest_for_locale + */ + public function test_expired_token_does_not_resolve_member_locale(): void + { + $user = User::factory()->create(['language' => 'ko']); + $newToken = $user->createToken('test-token'); + $plainTextToken = $newToken->plainTextToken; + + // 토큰 만료 시각을 과거로 직접 세팅 (Sanctum 기본 expiration 은 null 이라 미설정됨) + PersonalAccessToken::findToken($plainTextToken)->update([ + 'expires_at' => now()->subDay(), + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$plainTextToken, + 'Accept-Language' => 'en-US,en;q=0.9', + ])->getJson('/api/test-expired-locale'); + + $response->assertStatus(200); + + // 회원 언어(ko)를 따르지 않고 Accept-Language(en) 로 폴백해야 한다. + $this->assertNotSame('ko', $response->json('locale')); + $this->assertSame('en', $response->json('locale')); + } + + /** + * 유효한 토큰은 그 회원의 언어 설정을 따라야 합니다 (기존 동작 유지). + * + * @scenario case=valid_token_locale + * + * @effects valid_token_applies_member_locale + */ + public function test_valid_token_resolves_member_locale(): void + { + $user = User::factory()->create(['language' => 'ko']); + $plainTextToken = $user->createToken('test-token')->plainTextToken; + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$plainTextToken, + 'Accept-Language' => 'en-US,en;q=0.9', + ])->getJson('/api/test-expired-locale'); + + $response->assertStatus(200); + + // 회원 언어(ko)가 Accept-Language(en)보다 우선한다. + $this->assertSame('ko', $response->json('locale')); + } +} diff --git a/tests/Feature/Rules/NoExternalUrlsTest.php b/tests/Feature/Rules/NoExternalUrlsTest.php index 929cc055..5083fd6a 100644 --- a/tests/Feature/Rules/NoExternalUrlsTest.php +++ b/tests/Feature/Rules/NoExternalUrlsTest.php @@ -2,6 +2,10 @@ namespace Tests\Feature\Rules; +use App\Http\Requests\Layout\StoreLayoutRequest; +use App\Http\Requests\Layout\UpdateLayoutContentRequest; +use App\Http\Requests\Layout\UpdateLayoutExtensionContentRequest; +use App\Http\Requests\Layout\UpdateLayoutRequest; use App\Rules\NoExternalUrls; use Tests\TestCase; @@ -339,6 +343,58 @@ class NoExternalUrlsTest extends TestCase $this->assertFalse($failed, 'Relative paths should pass validation'); } + /** + * init_actions(로드 시 자동 실행)의 외부 navigate URL 차단 테스트 + * + * init_actions 는 페이지 진입 즉시 실행되므로 외부 URL 은 자동 리다이렉트/유출 경로다. + * 컴포넌트 actions 와 동일 강도로 차단해야 한다. + * + * @effects init_actions_external_url_rejected + */ + public function test_fails_with_external_url_in_init_actions(): void + { + $layout = [ + 'version' => '1.0.0', + 'layout_name' => 'test', + 'init_actions' => [ + ['handler' => 'navigate', 'params' => ['path' => 'https://evil.com/redirect']], + ], + 'components' => [], + ]; + + $failed = false; + $this->rule->validate('layout', $layout, function () use (&$failed) { + $failed = true; + }); + + $this->assertTrue($failed, 'init_actions 의 외부 URL 은 차단되어야 합니다'); + } + + /** + * init_actions 의 same-origin 경로는 통과 테스트 (과차단 회귀 방지) + * + * @effects same_origin_path_in_init_actions_allowed + */ + public function test_passes_with_same_origin_path_in_init_actions(): void + { + $layout = [ + 'version' => '1.0.0', + 'layout_name' => 'test', + 'init_actions' => [ + ['handler' => 'apiCall', 'params' => ['endpoint' => '/api/admin/bootstrap']], + ['handler' => 'navigate', 'params' => ['path' => '/dashboard']], + ], + 'components' => [], + ]; + + $failed = false; + $this->rule->validate('layout', $layout, function () use (&$failed) { + $failed = true; + }); + + $this->assertFalse($failed, 'init_actions 의 same-origin 경로는 통과해야 합니다'); + } + /** * 복잡한 중첩 구조에서 다중 외부 URL 차단 테스트 */ @@ -377,4 +433,139 @@ class NoExternalUrlsTest extends TestCase $this->assertTrue($failed, 'Multiple external URLs should fail validation'); } + + // ========================================== + // 문자열 스칼라 값 검사 (KVE-2026-1915 후속) + // ========================================== + // + // 이 규칙은 배열 트리 순회용으로 설계됐으나 실제로는 문자열 endpoint 필드 + // (`content.endpoint`·`content.data_sources.*.endpoint`)에도 부착되어 있다. + // 배열이 아니면 즉시 return 하던 종전 구현에서는 그 부착이 조용한 no-op 이었다. + + /** + * 문자열 값으로 직접 부착된 경우에도 외부 URL 을 차단한다. + * + * @effects plain_string_value_is_inspected_not_skipped + */ + public function test_fails_with_external_url_as_plain_string_value(): void + { + $failed = false; + $this->rule->validate('content.endpoint', 'https://evil.com/steal', function () use (&$failed) { + $failed = true; + }); + + $this->assertTrue($failed, '문자열 필드에 부착된 경우에도 외부 URL 은 차단되어야 합니다'); + } + + /** + * 문자열 값의 same-origin 경로는 통과 (과차단 회귀 방지). + * + * @effects same_origin_plain_string_allowed + */ + public function test_passes_with_same_origin_path_as_plain_string_value(): void + { + $failed = false; + $this->rule->validate('content.endpoint', '/api/admin/users', function () use (&$failed) { + $failed = true; + }); + + $this->assertFalse($failed, '문자열 필드의 same-origin 경로는 통과해야 합니다'); + } + + /** + * 문자열 값의 위험 스킴(javascript:)도 차단한다. + * + * @effects plain_string_value_is_inspected_not_skipped + */ + public function test_fails_with_dangerous_scheme_as_plain_string_value(): void + { + $failed = false; + $this->rule->validate('content.endpoint', 'javascript:alert(1)', function () use (&$failed) { + $failed = true; + }); + + $this->assertTrue($failed, '문자열 필드의 javascript: 스킴은 차단되어야 합니다'); + } + + // ========================================== + // FormRequest 결선 (규칙이 content 배열에 실제로 물려 있는지) + // ========================================== + // + // 편집기 저장 경로(LayoutController::update → UpdateLayoutContentRequest)의 content + // 트리에 이 규칙이 붙어 있지 않으면, init_actions·props·actions 의 외부 URL 차단이 + // 그 경로에서만 조용히 발화하지 않는다. + + /** + * FormRequest 의 content 배열 규칙에서 NoExternalUrls 인스턴스를 찾는다. + * + * @param array<string, mixed> $rules FormRequest rules() 결과 + * @return bool 부착되어 있으면 true + */ + private function contentRuleHasNoExternalUrls(array $rules): bool + { + foreach ((array) ($rules['content'] ?? []) as $rule) { + if ($rule instanceof NoExternalUrls) { + return true; + } + } + + return false; + } + + /** + * @effects rule_attached_to_every_layout_content_form_request + */ + public function test_update_content_request_attaches_rule_to_content_array(): void + { + $request = UpdateLayoutContentRequest::create('/x', 'PUT', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $request->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasNoExternalUrls($request->rules()), + 'UpdateLayoutContentRequest 의 content 배열 규칙에 NoExternalUrls 가 부착되어야 합니다' + ); + } + + /** + * @effects rule_attached_to_every_layout_content_form_request + */ + public function test_update_extension_content_request_attaches_rule_to_content_array(): void + { + $request = UpdateLayoutExtensionContentRequest::create('/x', 'PUT', [ + 'content' => ['priority' => 0], + ]); + $request->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasNoExternalUrls($request->rules()), + 'UpdateLayoutExtensionContentRequest 의 content 배열 규칙에 NoExternalUrls 가 부착되어야 합니다' + ); + } + + /** + * @effects rule_attached_to_every_layout_content_form_request + */ + public function test_store_and_update_requests_attach_rule_to_content_array(): void + { + $store = StoreLayoutRequest::create('/x', 'POST', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $store->setContainer($this->app); + + $update = UpdateLayoutRequest::create('/x', 'PUT', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $update->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasNoExternalUrls($store->rules()), + 'StoreLayoutRequest 의 content 배열 규칙에 NoExternalUrls 가 부착되어야 합니다' + ); + $this->assertTrue( + $this->contentRuleHasNoExternalUrls($update->rules()), + 'UpdateLayoutRequest 의 content 배열 규칙에 NoExternalUrls 가 부착되어야 합니다' + ); + } } diff --git a/tests/Feature/Rules/SafeLayoutExpressionsTest.php b/tests/Feature/Rules/SafeLayoutExpressionsTest.php new file mode 100644 index 00000000..4867f10b --- /dev/null +++ b/tests/Feature/Rules/SafeLayoutExpressionsTest.php @@ -0,0 +1,663 @@ +<?php + +namespace Tests\Feature\Rules; + +use App\Extension\HookManager; +use App\Http\Requests\Layout\StoreLayoutRequest; +use App\Http\Requests\Layout\UpdateLayoutContentRequest; +use App\Http\Requests\Layout\UpdateLayoutExtensionContentRequest; +use App\Http\Requests\Layout\UpdateLayoutRequest; +use App\Rules\SafeLayoutExpressions; +use App\Support\TrustedScriptHosts; +use PHPUnit\Framework\Attributes\DataProvider; +use Tests\TestCase; + +/** + * SafeLayoutExpressions 규칙 테스트 (KVE-2026-1915 B-3) + * + * 저장측 심층 방어: 표현식 샌드박스 우회 토큰과 외부 리소스 URL 을 거부하고, + * 정상 표현식(화살표 함수·템플릿 리터럴·스프레드 등)은 통과시키는지 검증합니다. + * + * 시나리오 축(case)은 배포 번들 E2E(layout-expression-sandbox.spec.ts)가 커버하고, + * 이 파일은 저장 단계에서 같은 효과를 떠받칩니다 — 런타임 평가기가 실행을 막기 전에 + * 위험 표현식이 애초에 저장되지 않도록 거부합니다. + * + * 효과 요약(마커 아님 — 평문): sandbox_escape_blocked. 실제 마커는 그 효과를 단언하는 + * 개별 메서드에만 둔다 — 클래스 레벨에 몰아 적으면 메서드를 전부 지워도 커버리지가 + * green 으로 남는다. + */ +class SafeLayoutExpressionsTest extends TestCase +{ + private SafeLayoutExpressions $rule; + + protected function setUp(): void + { + parent::setUp(); + $this->rule = new SafeLayoutExpressions; + } + + protected function tearDown(): void + { + HookManager::clearFilter(TrustedScriptHosts::FILTER_HOOK); + parent::tearDown(); + } + + /** + * 신뢰 호스트를 훅으로 등록합니다 (확장 manifest 선언을 모사). + * + * @param string $host 신뢰 호스트명 + */ + private function trustHost(string $host): void + { + HookManager::addFilter( + TrustedScriptHosts::FILTER_HOOK, + fn (array $hosts) => array_merge($hosts, [$host]), + priority: 1 + ); + } + + private function fails(array $layout): bool + { + $failed = false; + $this->rule->validate('content', $layout, function () use (&$failed) { + $failed = true; + }); + + return $failed; + } + + // ========================================== + // 정상 표현식 통과 (회귀 방지) + // ========================================== + + public function test_passes_normal_layout(): void + { + $layout = [ + 'version' => '1.0.0', + 'components' => [ + ['name' => 'Div', 'props' => ['text' => '{{user?.name ?? "Guest"}}']], + ], + ]; + + $this->assertFalse($this->fails($layout), '정상 레이아웃은 통과해야 합니다'); + } + + public function test_passes_arrow_function_expression(): void + { + // 화살표 함수/스프레드/필터는 정상 (176개 레이아웃에서 사용) + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['value' => "{{((_global.f ?? []) || []).filter(v => v !== 'x')}}"], + ]], + ]; + + $this->assertFalse($this->fails($layout), '화살표 함수 표현식은 통과해야 합니다'); + } + + public function test_passes_template_literal_navigation(): void + { + // 템플릿 리터럴은 내비게이션 경로 조립에 정상 사용 + $layout = [ + 'actions' => [[ + 'handler' => 'navigate', + 'params' => ['path' => '{{`/mypage/${$args[0]}`}}'], + ]], + ]; + + $this->assertFalse($this->fails($layout), '템플릿 리터럴은 통과해야 합니다'); + } + + public function test_passes_same_origin_endpoint(): void + { + $layout = [ + 'data_sources' => [ + ['id' => 'products', 'endpoint' => '/api/modules/sirsoft-ecommerce/products'], + ['id' => 'post', 'endpoint' => '/api/modules/sirsoft-board/admin/board/{{route.slug}}/posts/{{route.id}}'], + ], + ]; + + $this->assertFalse($this->fails($layout), 'same-origin 경로 엔드포인트는 통과해야 합니다'); + } + + // ========================================== + // 위험 표현식 차단 + // ========================================== + + /** @effects sandbox_escape_blocked */ + public function test_fails_constructor_chain_dot_access(): void + { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{''.constructor.constructor('return 1')()}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), 'constructor 체인 접근은 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_constructor_computed_string_access(): void + { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{''['constructor']}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), 'computed constructor 접근은 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_proto_access(): void + { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => '{{obj.__proto__}}'], + ]], + ]; + + $this->assertTrue($this->fails($layout), '__proto__ 접근은 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_function_constructor_call(): void + { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{Function('return 1')()}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), 'Function 생성자 호출은 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_dynamic_import_call(): void + { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{import('https://evil.com/x.js')}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), '동적 import() 호출은 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_legacy_accessor_prototype_reach(): void + { + // legacy 접근자 4종 — 프로퍼티를 문자열 인자로 지목해 프로토타입을 읽고 쓴다. + // Object 리플렉션 static 을 제거해도 이 경로가 같은 능력을 상속으로 제공한다. + foreach (['__lookupGetter__', '__lookupSetter__', '__defineGetter__', '__defineSetter__'] as $accessor) { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{({}).{$accessor}('__proto__')}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), "{$accessor} 접근은 차단되어야 합니다"); + } + } + + public function test_passes_prototype_word_in_plain_comparison(): void + { + // 과차단 회귀 방지 — 런타임 평가기가 허용하는 정상 비교는 저장측도 통과해야 한다. + // 신규 legacy 접근자 패턴이 단어 경계를 쓰므로 이 형태를 건드리지 않는다. + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{ mode === 'prototype' ? 'a' : 'b' }}"], + ]], + ]; + + $this->assertFalse($this->fails($layout), '정상 문자열 비교는 통과해야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_nested_array_computed_key(): void + { + // 중첩 배열 키 `[['constructor']]` — 구 브래킷 한정 패턴이 놓치던 형태 (KVE-2026-1915) + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{''[['constructor']][['constructor']]('return 1')()}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), '중첩 배열 computed 키는 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_reflection_static_string_argument(): void + { + // 리플렉션 static 의 문자열 인자 `..., 'constructor')` — 구 패턴이 놓치던 형태 + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{Object.getOwnPropertyDescriptor(Object.getPrototypeOf(String), 'constructor').value('x')()}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), '리플렉션 static 문자열 인자는 차단되어야 합니다'); + } + + /** @effects sandbox_escape_blocked */ + public function test_fails_object_reflection_static_names(): void + { + foreach (['Object.getPrototypeOf(x)', 'Object.setPrototypeOf(x, y)', 'Object.defineProperty(x, y, z)'] as $expr) { + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['text' => "{{{$expr}}}"], + ]], + ]; + + $this->assertTrue($this->fails($layout), "Object 리플렉션 static({$expr})은 차단되어야 합니다"); + } + } + + public function test_passes_object_safe_data_methods(): void + { + // 안전한 Object 데이터 메서드는 통과 (실제 레이아웃에서 사용) + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'props' => ['value' => '{{Object.assign(Object.create(null), _local.filters, { [k]: v })}}'], + ]], + ]; + + $this->assertFalse($this->fails($layout), 'Object.keys/assign/create 등 데이터 메서드는 통과해야 합니다'); + } + + public function test_passes_string_literal_comparison_of_reserved_word(): void + { + // 금지 프로퍼티 이름을 프로퍼티로 접근하지 않고 단순 문자열로 비교하는 정상 표현식은 + // 통과해야 한다 — 런타임 평가기가 허용하므로 저장측이 막으면 저장이 불가한 비대칭이 된다. + // (편집기가 이런 레이아웃을 저장할 때 오탐으로 거부되면 안 됨) + $layout = [ + 'components' => [[ + 'name' => 'Div', + 'if' => "{{_local.mode === 'prototype'}}", + 'props' => ['text' => "{{status === 'constructor' ? 'A' : 'B'}}"], + ]], + ]; + + $this->assertFalse($this->fails($layout), '예약어를 문자열로 비교하는 정상 표현식은 통과해야 합니다'); + } + + /** + * 저장소가 배포하는 레이아웃 전수 스윕 — 저장측 규칙이 기존 레이아웃을 거부하지 않는다. + * + * 합성 페이로드만으로는 "정상 표현식 통과"를 증명하지 못한다. 픽스처는 늘 현재 규칙을 + * 지키도록 쓰이므로, 규칙이 조여졌을 때 **이미 배포된 레이아웃이 저장 불가가 되는** + * 과차단 회귀는 실제 모집단으로만 드러난다(예: `=>` 를 금지 토큰에 넣으면 이 테스트가 + * red 가 된다). 정적 룰은 audit 실행 시점에만 도는 별개 축이므로, 저장측 PHP 규칙 자신의 + * 모집단 검사를 여기에 둔다. + */ + public function test_passes_every_shipped_layout(): void + { + // 모집단은 선언 주체별로 나눠 센다. 합산 하한만 두면 한 주체가 통째로 빠져도 + // 나머지가 하한을 넘겨 green 이 된다(예: templates 327건이 사라져도 modules+plugins + // 272건이 남는다). 주체별 하한이라야 경로 이동을 그 주체에서 잡는다. + $groups = [ + // 코어 레이아웃 루트는 현재 추적 파일 0건이다. 스캔에는 포함하되 하한은 두지 + // 않는다 — 하한을 걸면 코어 레이아웃이 없다는 사실만으로 상시 red 가 된다. + 'core' => [[base_path('resources/layouts')], 0], + 'templates' => [glob(base_path('templates/_bundled/*/layouts')) ?: [], 250], + 'modules' => [glob(base_path('modules/_bundled/*/resources/layouts')) ?: [], 200], + 'plugins' => [glob(base_path('plugins/_bundled/*/resources/layouts')) ?: [], 10], + ]; + + $rejected = []; + $scannedByGroup = []; + + foreach ($groups as $group => [$roots, $_floor]) { + $scannedByGroup[$group] = 0; + + foreach ($roots as $root) { + if (! is_dir($root)) { + continue; + } + + $files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root)); + foreach ($files as $file) { + if ($file->getExtension() !== 'json') { + continue; + } + + $decoded = json_decode(file_get_contents($file->getPathname()), true); + if (! is_array($decoded)) { + continue; + } + + $scannedByGroup[$group]++; + if ($this->fails($decoded)) { + $rejected[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $file->getPathname()); + } + } + } + } + + // 모집단이 비면 이 테스트는 아무것도 증명하지 않는다 — 경로 오타/이동으로 조용히 + // green 이 되는 것을 막는다. + foreach ($groups as $group => [$_roots, $floor]) { + if ($floor === 0) { + continue; + } + + $this->assertGreaterThanOrEqual( + $floor, + $scannedByGroup[$group], + "{$group} 레이아웃 모집단이 하한({$floor})을 밑돕니다(스캔 {$scannedByGroup[$group]}건) — 경로가 바뀌었는지 확인하세요" + ); + } + + $this->assertSame( + [], + $rejected, + '배포 레이아웃이 저장측 규칙에 거부되면 그 화면은 편집 후 저장이 불가해집니다: ' + .implode(', ', array_slice($rejected, 0, 10)) + ); + } + + // ========================================== + // 외부 리소스 URL 차단 + // ========================================== + + public function test_fails_external_script_src(): void + { + $layout = [ + 'scripts' => [ + ['id' => 'evil', 'src' => 'https://evil.com/x.js'], + ], + ]; + + $this->assertTrue($this->fails($layout), '외부 스크립트 src 는 차단되어야 합니다'); + } + + public function test_fails_protocol_relative_script_src(): void + { + $layout = [ + 'scripts' => [ + ['id' => 'evil', 'src' => '//evil.com/x.js'], + ], + ]; + + $this->assertTrue($this->fails($layout), 'protocol-relative src 는 차단되어야 합니다'); + } + + public function test_passes_same_origin_script_src(): void + { + $layout = [ + 'scripts' => [ + ['id' => 'local', 'src' => '/js/widget.js'], + ], + ]; + + $this->assertFalse($this->fails($layout), 'same-origin 스크립트 src 는 통과해야 합니다'); + } + + public function test_passes_trusted_host_script_src(): void + { + // 확장이 manifest 로 선언한 신뢰 호스트의 외부 스크립트는 통과한다 + $this->trustHost('cdn.ckeditor.com'); + + $layout = [ + 'scripts' => [ + ['id' => 'ckeditor', 'src' => 'https://cdn.ckeditor.com/ckeditor5/43.3.1/ckeditor5.umd.js'], + ], + ]; + + $this->assertFalse($this->fails($layout), '신뢰 호스트의 외부 스크립트 src 는 통과해야 합니다'); + } + + public function test_passes_trusted_host_protocol_relative_data_source(): void + { + $this->trustHost('t1.daumcdn.net'); + + $layout = [ + 'data_sources' => [ + ['id' => 'daum', 'endpoint' => '//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js'], + ], + ]; + + $this->assertFalse($this->fails($layout), '신뢰 호스트의 protocol-relative endpoint 는 통과해야 합니다'); + } + + /** + * 신뢰 호스트를 userinfo 로 위장한 authority 우회는 거부한다. + * + * `https://evil.com\@cdn.ckeditor.com/x.js` 는 정규화 없이 parse_url 로 읽으면 + * 호스트가 `cdn.ckeditor.com`(신뢰 목록)으로 보이지만, 브라우저는 백슬래시를 + * 슬래시로 접어 `evil.com` 에서 로드한다. 신뢰 호스트 판정이 same-origin 판정과 + * 다른 정규화를 쓰면 이 형태만 저장측을 통과한다. + */ + public function test_fails_trusted_host_userinfo_disguise(): void + { + $this->trustHost('cdn.ckeditor.com'); + + $bs = chr(92); + $layout = [ + 'scripts' => [ + ['id' => 'x', 'src' => 'https://evil.com'.$bs.'@cdn.ckeditor.com/x.js'], + ], + ]; + + $this->assertTrue( + $this->fails($layout), + '신뢰 호스트를 userinfo 로 위장한 외부 스크립트는 거부해야 합니다' + ); + } + + /** + * (과차단 회귀) 런타임이 신뢰 호스트로 해석하는 형태는 저장측도 통과시킨다. + * + * `/\/cdn.ckeditor.com/x.js` 는 문자열상 path 지만 브라우저는 authority 로 읽어 + * 신뢰 호스트에서 로드한다. 저장측만 거부하면 "런타임은 되는데 저장이 안 되는" + * 역방향 비대칭이 된다. + */ + public function test_passes_trusted_host_reached_through_backslash_authority(): void + { + $this->trustHost('cdn.ckeditor.com'); + + $layout = [ + 'scripts' => [ + ['id' => 'x', 'src' => '/'.chr(92).'/cdn.ckeditor.com/x.js'], + ], + ]; + + $this->assertFalse( + $this->fails($layout), + '런타임이 신뢰 호스트로 해석하는 형태는 저장측도 통과해야 합니다' + ); + } + + public function test_still_fails_untrusted_host_when_other_host_trusted(): void + { + // cdn.ckeditor.com 만 신뢰해도 evil.com 은 여전히 차단된다 + $this->trustHost('cdn.ckeditor.com'); + + $layout = [ + 'scripts' => [ + ['id' => 'evil', 'src' => 'https://evil.com/x.js'], + ], + ]; + + $this->assertTrue($this->fails($layout), '신뢰 목록에 없는 외부 스크립트는 차단되어야 합니다'); + } + + public function test_fails_external_data_source_endpoint(): void + { + $layout = [ + 'data_sources' => [ + ['id' => 'x', 'endpoint' => 'https://evil.com/steal'], + ], + ]; + + $this->assertTrue($this->fails($layout), '외부 data_sources 엔드포인트는 차단되어야 합니다'); + } + + /** + * authority 우회 후보 URL (KVE-2026-1915 후속) + * + * `/\/evil.com/x.js` 는 `//` 로 시작하지 않고 scheme 도 없으며 `/` 로 시작하므로 + * 문자열 접두 검사만으로는 same-origin path 로 판정된다. 그러나 브라우저의 URL + * 파서는 (a) special scheme 에서 `\` 를 `/` 와 동등하게 처리하고 (b) 파싱 전에 + * ASCII tab·개행을 제거하므로, 실제로는 `https://evil.com/x.js` 로 해석되어 + * 원격 스크립트가 로드된다. + * + * 실측(node `new URL`, base `https://g7_2.dev/`): 아래 4형태 전부 origin 이 + * `https://evil.com` 으로 해석된다. + * + * @return array<string, array{string}> + */ + public static function authorityBypassProvider(): array + { + $bs = chr(92); + + return [ + 'slash-backslash-slash' => ['/'.$bs.'/evil.com/x.js'], + 'slash-backslash' => ['/'.$bs.'evil.com/x.js'], + 'slash-double-backslash' => ['/'.$bs.$bs.'evil.com/x.js'], + 'slash-tab-slash' => ['/'.chr(9).'/evil.com/x.js'], + 'slash-lf-slash' => ['/'.chr(10).'/evil.com/x.js'], + 'slash-cr-slash' => ['/'.chr(13).'/evil.com/x.js'], + ]; + } + + /** + * @param string $src authority 우회 후보 URL + */ + #[DataProvider('authorityBypassProvider')] + public function test_fails_authority_bypass_script_src(string $src): void + { + $layout = [ + 'scripts' => [ + ['id' => 'evil', 'src' => $src], + ], + ]; + + $this->assertTrue( + $this->fails($layout), + 'authority 우회 src 는 차단되어야 합니다: '.json_encode($src) + ); + } + + /** + * @param string $endpoint authority 우회 후보 URL + */ + #[DataProvider('authorityBypassProvider')] + public function test_fails_authority_bypass_data_source_endpoint(string $endpoint): void + { + $layout = [ + 'data_sources' => [ + ['id' => 'evil', 'endpoint' => $endpoint], + ], + ]; + + $this->assertTrue( + $this->fails($layout), + 'authority 우회 endpoint 는 차단되어야 합니다: '.json_encode($endpoint) + ); + } + + /** + * 백슬래시·탭이 authority 위치가 아닌 경로 안에 있으면 same-origin 이므로 통과 + * + * 과차단 회귀 방지 — 브라우저도 이 둘을 same-origin 으로 해석한다(실측). + */ + public function test_passes_same_origin_path_containing_backslash_or_tab(): void + { + $layout = [ + 'scripts' => [ + ['id' => 'a', 'src' => '/js/a'.chr(92).'b.js'], + ['id' => 'b', 'src' => '/js/c'.chr(9).'d.js'], + ], + ]; + + $this->assertFalse( + $this->fails($layout), + '경로 중간의 백슬래시·탭은 authority 를 만들지 않으므로 통과해야 합니다' + ); + } + + // ========================================== + // FormRequest 결선 (규칙이 content 배열에 실제로 물려 있는지) + // ========================================== + // + // 규칙이 올바르게 동작해도 content **배열** 규칙에 부착되지 않으면 저장 경로는 그대로 + // 통과한다. 과거 두 content 전용 요청은 SafeLayoutExpressions 를 문자열 endpoint 에만 + // 걸어 두어(is_array 가드로 early-return) 트리 전체가 미검사로 남았다. 아래 테스트는 + // 네 요청 모두 content 배열 규칙에 규칙이 부착돼 있음을 고정한다. + + /** + * FormRequest 의 content 배열 규칙에서 SafeLayoutExpressions 인스턴스를 찾는다. + */ + private function contentRuleHasSafeExpressions(array $rules): bool + { + $contentRules = $rules['content'] ?? []; + + foreach ((array) $contentRules as $rule) { + if ($rule instanceof SafeLayoutExpressions) { + return true; + } + } + + return false; + } + + public function test_update_content_request_attaches_rule_to_content_array(): void + { + $request = UpdateLayoutContentRequest::create('/x', 'PUT', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $request->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasSafeExpressions($request->rules()), + 'UpdateLayoutContentRequest 의 content 배열 규칙에 SafeLayoutExpressions 가 부착되어야 합니다' + ); + } + + public function test_update_extension_content_request_attaches_rule_to_content_array(): void + { + $request = UpdateLayoutExtensionContentRequest::create('/x', 'PUT', [ + 'content' => ['priority' => 0], + ]); + $request->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasSafeExpressions($request->rules()), + 'UpdateLayoutExtensionContentRequest 의 content 배열 규칙에 SafeLayoutExpressions 가 부착되어야 합니다' + ); + } + + public function test_store_and_update_requests_attach_rule_to_content_array(): void + { + $store = StoreLayoutRequest::create('/x', 'POST', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $store->setContainer($this->app); + + $update = UpdateLayoutRequest::create('/x', 'PUT', [ + 'content' => ['version' => '1.0.0', 'components' => []], + ]); + $update->setContainer($this->app); + + $this->assertTrue( + $this->contentRuleHasSafeExpressions($store->rules()), + 'StoreLayoutRequest 의 content 배열 규칙에 SafeLayoutExpressions 가 부착되어야 합니다' + ); + $this->assertTrue( + $this->contentRuleHasSafeExpressions($update->rules()), + 'UpdateLayoutRequest 의 content 배열 규칙에 SafeLayoutExpressions 가 부착되어야 합니다' + ); + } +} diff --git a/tests/Feature/Security/RankCeilingGuardTest.php b/tests/Feature/Security/RankCeilingGuardTest.php new file mode 100644 index 00000000..c788193f --- /dev/null +++ b/tests/Feature/Security/RankCeilingGuardTest.php @@ -0,0 +1,1557 @@ +<?php + +namespace Tests\Feature\Security; + +use App\Enums\ExtensionOwnerType; +use App\Exceptions\CannotModifyProtectedRoleException; +use App\Models\Attachment; +use App\Models\Menu; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use App\Services\MenuService; +use App\Services\RoleService; +use Database\Seeders\RolePermissionSeeder; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +/** + * KVE-2026-1919 등급 상한(rank ceiling) 가드 회귀 테스트 + * + * 삭제/탈퇴 경로에만 있던 슈퍼 관리자·보호 역할 보호를 수정·상태변경·권한부여 + * 경로까지 대칭 적용했는지 검증합니다. 각 케이스는 (a) 익스플로잇 차단 + + * (b) 슈퍼 관리자 정상 흐름 회귀 없음 양면을 확인합니다. + * + * 시나리오 축·효과는 매니페스트 tests/scenarios/user-grade-rank-ceiling.yaml 참조. + * 각 test 메서드의 `@scenario actor=… , operation=… , outcome=…` 마커가 축 조합을, + * `@effects …` 가 효과를 커버한다(메서드당 단일 조합). + * + * 축 요약(마커 아님 — 평문): actor 는 sub_admin·super_admin, operation 은 modify·status· + * unlock·grant_permission·assign_role·remove_role·update_role·toggle_role, outcome 은 blocked·allowed 다. + * 이 요약을 클래스 레벨 `@scenario` 로 적으면 시나리오 마커 파서가 + * `=` 없는 토큰을 버리고 첫 값만 취해 **실재하지 않는 조합 1건**으로 집계한다 — 커버리지를 + * 부풀리는 방향이라 요약은 반드시 평문으로 둔다. 실제 커버는 메서드 레벨 마커가 담당한다. + */ +class RankCeilingGuardTest extends TestCase +{ + use RefreshDatabase; + + protected function setUp(): void + { + parent::setUp(); + $this->seed(RolePermissionSeeder::class); + } + + /** + * 슈퍼 관리자 사용자를 생성합니다 (is_super + admin 역할 = 전 권한). + */ + private function createSuperAdmin(): User + { + $user = User::factory()->create(['is_super' => true, 'status' => 'active']); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $user->roles()->attach($adminRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + + return $user->fresh(); + } + + /** + * 위임 부관리자(비-슈퍼)를 생성합니다. + * + * @param array<string, string|null> $permissionScopes identifier => scope_type(null=global) + */ + private function createSubAdmin(array $permissionScopes): User + { + $user = User::factory()->create(['is_super' => false, 'status' => 'active']); + + $role = Role::create([ + 'identifier' => 'sub_admin_'.uniqid(), + 'name' => json_encode(['ko' => '부관리자', 'en' => 'Sub Admin']), + 'description' => json_encode(['ko' => '위임 부관리자', 'en' => 'Delegated sub admin']), + 'extension_type' => null, + 'is_active' => true, + ]); + + $pivot = []; + foreach ($permissionScopes as $identifier => $scope) { + $permission = Permission::where('identifier', $identifier)->firstOrFail(); + $pivot[$permission->id] = ['scope_type' => $scope, 'granted_at' => now()]; + } + $role->permissions()->sync($pivot); + + $user->roles()->attach($role->id, ['assigned_at' => now(), 'assigned_by' => null]); + + return $user->fresh(); + } + + private function tokenFor(User $user): string + { + return $user->createToken('test-token')->plainTextToken; + } + + private function authAs(User $user): static + { + return $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->tokenFor($user), + 'Accept' => 'application/json', + ]); + } + + /** + * UpdateUserRequest 를 통과하는 유효한 수정 페이로드를 만듭니다. + * + * 가드는 서비스 계층에 있으므로, 요청은 FormRequest 검증(name/email/roles 필수)을 + * 먼저 통과해야 서비스에 도달합니다. 실제 공격자도 유효한 요청을 보냅니다. + * + * @param array<string, mixed> $overrides 덮어쓸 필드 + * @return array<string, mixed> + */ + private function updatePayload(User $target, array $overrides = []): array + { + $roleIds = $target->roles->pluck('id')->all(); + if (empty($roleIds)) { + $userRole = Role::where('identifier', 'user')->first(); + $roleIds = $userRole ? [$userRole->id] : [Role::where('identifier', 'admin')->firstOrFail()->id]; + } + + return array_merge([ + 'name' => $target->name, + 'email' => $target->email, + 'role_ids' => $roleIds, + ], $overrides); + } + + private function roleIdentifier(): string + { + return 'test_role_'.substr(uniqid(), -8); + } + + // ========================================================================= + // C-1 — updateUser 등급 가드 + // ========================================================================= + + /** + * 부관리자(core.users.update 글로벌)가 슈퍼 관리자의 비밀번호를 변경하려 하면 403. + * + * @scenario actor=sub_admin, operation=modify, outcome=blocked + * + * @effects sub_admin_cannot_modify_super_admin, rejected_target_state_and_tokens_unchanged + */ + public function test_sub_admin_cannot_change_super_admin_password(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $superAdmin = $this->createSuperAdmin(); + $originalHash = $superAdmin->password; + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$superAdmin->uuid}", $this->updatePayload($superAdmin, [ + 'password' => 'HackedPassword123!', + 'password_confirmation' => 'HackedPassword123!', + ])); + + $response->assertStatus(403); + + // 대상 슈퍼 관리자 비밀번호가 불변인지 확인 + $superAdmin->refresh(); + $this->assertSame($originalHash, $superAdmin->password); + } + + /** + * 부관리자가 슈퍼 관리자의 status 를 blocked 로 바꾸려 하면 403 + 상태 불변. + * + * @scenario actor=sub_admin, operation=status, outcome=blocked + * + * @effects sub_admin_cannot_modify_super_admin, rejected_target_state_and_tokens_unchanged + */ + public function test_sub_admin_cannot_block_super_admin(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $superAdmin = $this->createSuperAdmin(); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$superAdmin->uuid}", $this->updatePayload($superAdmin, [ + 'status' => 'blocked', + ])); + + $response->assertStatus(403); + $superAdmin->refresh(); + $this->assertSame('active', $superAdmin->status); + } + + /** + * (회귀) 슈퍼 관리자 액터는 다른 슈퍼 관리자를 정상 수정할 수 있다. + * + * @scenario actor=super_admin, operation=modify, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_update_another_super_admin(): void + { + $actor = $this->createSuperAdmin(); + $target = $this->createSuperAdmin(); + + $response = $this->authAs($actor)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'name' => 'Renamed By Super', + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertSame('Renamed By Super', $target->name); + } + + /** + * (회귀) 부관리자는 일반 사용자는 정상 수정할 수 있다. + * + * @scenario actor=sub_admin, operation=modify, outcome=allowed + * + * @effects super_admin_write_paths_share_rank_ceiling + */ + public function test_sub_admin_can_update_regular_user(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $target = User::factory()->create(['status' => 'active', 'name' => 'Before']); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'name' => 'After', + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertSame('After', $target->name); + } + + // ========================================================================= + // C-2 — bulkUpdateStatus 슈퍼 관리자 제외 + // ========================================================================= + + /** + * 부관리자의 bulk-status 에 슈퍼 관리자 uuid 가 포함돼도 슈퍼는 무력화되지 않는다. + * + * @scenario actor=sub_admin, operation=status, outcome=blocked + * + * @effects bulk_status_excludes_super_admin_for_sub_admin, rejected_target_state_and_tokens_unchanged + */ + public function test_bulk_status_excludes_super_admin_for_sub_admin(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $superAdmin = $this->createSuperAdmin(); + $regular = User::factory()->create(['status' => 'active']); + + // 슈퍼 관리자의 살아 있는 세션. bulkUpdateStatus 는 Active 외 상태로 바꿀 때 + // 대상들의 토큰을 일괄 삭제하므로(UserService::deleteTokensByUserIds), 등급 필터가 + // 슈퍼를 대상 목록에서 빼는 데 실패하면 **상태는 그대로여도 세션만 날아간다**. + // 상태만 단언하면 그 회귀를 놓친다 — 계획 E-3 T4 의 요구는 "슈퍼 세션 유지" 다. + $superToken = $superAdmin->createToken('super-session')->plainTextToken; + // 대조군. 이 토큰이 삭제되는 것으로 "토큰 일괄 삭제가 실제로 도는 경로" 임이 + // 증명되므로, 아래 슈퍼 토큰 생존 단언이 무증상 통과가 아니게 된다. + $regular->createToken('regular-session'); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$superAdmin->uuid, $regular->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + + // 슈퍼 관리자는 제외 → 상태 불변, 일반 사용자만 적용 + $superAdmin->refresh(); + $regular->refresh(); + $this->assertSame('active', $superAdmin->status); + $this->assertSame('blocked', $regular->status); + + // 세션: 적용 대상(regular)은 토큰이 지워지고, 제외 대상(super)은 유지된다. + // 두 단언이 짝을 이뤄야 "삭제가 아예 안 도는" 상태에서 조용히 green 이 되지 않는다. + $this->assertNotNull($superToken); + $this->assertSame(0, $regular->tokens()->count(), '적용 대상의 토큰은 삭제되어야 합니다'); + $this->assertSame(1, $superAdmin->tokens()->count(), '슈퍼 관리자 세션이 유지되어야 합니다'); + } + + /** + * (회귀) 슈퍼 관리자 액터의 bulk-status 는 일반 사용자를 정상 변경한다. + * + * C-2 필터가 대상만 보고 슈퍼를 무조건 제외하도록 조여지면 슈퍼 actor 경로가 + * 조용히 과차단된다. 이 회귀는 예외/오류 없이 "아무도 변경되지 않음" 으로만 + * 드러나므로 정상 수행 경로를 명시 고정한다. + * + * @scenario actor=super_admin, operation=status, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_bulk_status_super_admin_actor_updates_regular_users(): void + { + $actor = $this->createSuperAdmin(); + $a = User::factory()->create(['status' => 'active']); + $b = User::factory()->create(['status' => 'active']); + + $response = $this->authAs($actor)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$a->uuid, $b->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + $a->refresh(); + $b->refresh(); + $this->assertSame('blocked', $a->status); + $this->assertSame('blocked', $b->status); + } + + /** + * (회귀) 슈퍼 관리자 액터는 슈퍼 관리자 대상도 bulk-status 로 변경할 수 있다. + * + * 등급 상한은 액터 등급 기준이다(mayModify: 슈퍼 대상은 슈퍼 액터만 수정 가능). + * 필터가 액터를 무시하고 슈퍼 대상을 일괄 제외하도록 바뀌면 이 경로가 막히므로 + * 슈퍼↔슈퍼 정상 수행을 고정한다. + * + * @scenario actor=super_admin, operation=status, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_bulk_status_super_admin_actor_can_modify_super_target(): void + { + $actor = $this->createSuperAdmin(); + $superTarget = $this->createSuperAdmin(); + + $response = $this->authAs($actor)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$superTarget->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + $superTarget->refresh(); + $this->assertSame('blocked', $superTarget->status); + } + + // ========================================================================= + // C-2b — bulkUpdateStatus 스코프 축 재적용 + // + // 등급 축(슈퍼 보호)과 스코프 축은 별개다. PermissionMiddleware 의 스코프 검사는 + // 라우트 모델이 resolve 될 때만 동작하므로(모델 없으면 "목록 엔드포인트" 로 보고 스킵), + // `{user}` 파라미터가 없는 정적 bulk 라우트에서는 통째로 건너뛰어진다. + // 상세 경로가 403 으로 막는 대상을 bulk 로는 바꿀 수 있으면 그 경로가 우회로다. + // ========================================================================= + + /** + * self 스코프 액터가 bulk-status 로 타인을 변경하려 하면 대상에서 제외된다. + * + * 기본 탑재 역할 `manager` 가 정확히 이 구성이다(core.users.update = self). + * 상세 경로 PUT /users/{other} 는 스코프 미들웨어가 403 으로 막지만, bulk 라우트는 + * 라우트 모델이 없어 그 검사가 스킵된다 — 서비스가 같은 판정을 재적용해야 한다. + * + * @scenario actor=sub_admin, operation=status, outcome=blocked + * + * @effects bulk_status_reapplies_scope_gate_for_scoped_actor, rejected_target_state_and_tokens_unchanged + */ + public function test_self_scoped_admin_cannot_bulk_status_other_users(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => 'self']); + $other = User::factory()->create(['status' => 'active']); + $otherToken = $other->createToken('victim-token')->plainTextToken; + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$other->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + + // 스코프 밖 대상은 제외 → 상태·토큰 불변 + $other->refresh(); + $this->assertSame('active', $other->status); + $this->assertSame(0, $response->json('data.updated_count')); + $this->assertNotNull($otherToken); + $this->assertSame(1, $other->tokens()->count()); + } + + /** + * (대칭 앵커) 같은 액터·같은 대상이 상세 경로에서는 이미 403 이다. + * + * bulk 경로만 통과하는 비대칭을 고정하기 위해, 두 경로를 같은 테스트군에 나란히 둔다. + * + * @scenario actor=sub_admin, operation=modify, outcome=blocked + * + * @effects detail_route_enforces_scope_gate + */ + public function test_self_scoped_admin_cannot_update_other_user_on_detail_route(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => 'self']); + $other = User::factory()->create(['status' => 'active', 'name' => '원본 이름']); + + $response = $this->authAs($subAdmin)->putJson( + '/api/admin/users/'.$other->uuid, + $this->updatePayload($other, ['name' => '변조된 이름']) + ); + + $response->assertStatus(403); + $other->refresh(); + $this->assertSame('원본 이름', $other->name); + } + + /** + * (과차단 회귀) role 스코프 액터는 같은 역할을 가진 대상을 bulk-status 로 변경할 수 있다. + * + * 스코프 가드를 "비-글로벌 액터는 bulk 전면 금지" 로 조이면 이 경로가 조용히 + * 막힌다(예외 없이 updated_count=0). 스코프 판정은 액터의 유효 스코프에 따라 + * 대상별로 이뤄져야 한다 — self 는 자기만, role 은 같은 역할까지, 글로벌은 전체. + * + * @scenario actor=sub_admin, operation=status, outcome=allowed + * + * @effects bulk_status_allows_scoped_actor_within_own_scope + */ + public function test_role_scoped_admin_can_bulk_status_user_sharing_role(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => 'role']); + $sharedRoleId = $subAdmin->roles->firstOrFail()->id; + + $peer = User::factory()->create(['status' => 'active']); + $peer->roles()->attach($sharedRoleId, ['assigned_at' => now(), 'assigned_by' => null]); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$peer->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + $peer->refresh(); + $this->assertSame('blocked', $peer->status); + $this->assertSame(1, $response->json('data.updated_count')); + } + + /** + * role 스코프 액터가 자기 역할 밖 대상을 bulk-status 로 변경하려 하면 제외된다. + * + * self 축과 같은 결함이 role 축에도 성립하는지 고정한다 — 스코프 판정을 + * `PermissionHelper::checkScopeAccess`(상세 경로와 동일 SSoT)에 위임하지 않고 + * 자체 재구현하면 role 분기만 빠지는 형태가 나온다. + * + * @scenario actor=sub_admin, operation=status, outcome=blocked + * + * @effects bulk_status_reapplies_scope_gate_for_scoped_actor + */ + public function test_role_scoped_admin_cannot_bulk_status_user_outside_role(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => 'role']); + $outsider = User::factory()->create(['status' => 'active']); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$outsider->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + $outsider->refresh(); + $this->assertSame('active', $outsider->status); + $this->assertSame(0, $response->json('data.updated_count')); + } + + /** + * (과차단 회귀) 글로벌 스코프 액터는 bulk-status 로 타인을 정상 변경한다. + * + * 스코프 가드가 액터의 유효 스코프를 무시하고 일괄 차단하면 위임 관리자의 + * 정상 일괄 작업이 통째로 무력화된다. + * + * @scenario actor=sub_admin, operation=status, outcome=allowed + * + * @effects bulk_status_allows_scoped_actor_within_own_scope + */ + public function test_global_scoped_admin_can_bulk_status_other_users(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $a = User::factory()->create(['status' => 'active']); + $b = User::factory()->create(['status' => 'active']); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/users/bulk-status', [ + 'ids' => [$a->uuid, $b->uuid], + 'status' => 'blocked', + ]); + + $response->assertOk(); + $a->refresh(); + $b->refresh(); + $this->assertSame('blocked', $a->status); + $this->assertSame('blocked', $b->status); + $this->assertSame(2, $response->json('data.updated_count')); + } + + // ========================================================================= + // 편차 1 — 역할 부여 게이트의 프론트 노출값(can_assign_roles) 정렬 + // + // 역할 부여는 사용자 관리(core.users.*)의 일부이지 역할 정의 수정 + // (core.permissions.update)이 아니다. 이 플래그가 라우트 권한 SSoT 와 다른 + // 리소스를 가리키면 화면 게이트와 실제 서버 동작이 갈린다. + // 키 존재만 단언하면 원복돼도 스위트가 green 이므로 **값**을 양방향으로 고정한다. + // ========================================================================= + + /** + * `core.users.update` 보유자에게 can_assign_roles=true. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects can_assign_roles_gate_follows_users_update + */ + public function test_can_assign_roles_is_true_for_users_update_holder(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.permissions.read' => null, + ]); + + $response = $this->authAs($subAdmin)->getJson('/api/admin/roles/active'); + + $response->assertOk(); + $this->assertTrue( + $response->json('data.abilities.can_assign_roles'), + 'core.users.update 보유자는 역할을 부여할 수 있어야 합니다' + ); + } + + /** + * `core.permissions.update` 만으로는 can_assign_roles=false. + * + * 이 플래그가 다시 core.permissions.update 로 되돌아가면 이 테스트가 red 가 된다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=blocked + * + * @effects can_assign_roles_gate_follows_users_update + */ + public function test_can_assign_roles_is_false_with_permissions_update_only(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + + $response = $this->authAs($subAdmin)->getJson('/api/admin/roles/active'); + + $response->assertOk(); + $this->assertFalse( + $response->json('data.abilities.can_assign_roles'), + '역할 정의 수정 권한만으로는 사용자에게 역할을 부여할 수 없어야 합니다' + ); + } + + // ========================================================================= + // C-3 — unlockAccount 등급 가드 + // ========================================================================= + + /** + * 부관리자는 슈퍼 관리자 계정을 잠금 해제(조작)할 수 없다. + * + * @scenario actor=sub_admin, operation=unlock, outcome=blocked + * + * @effects sub_admin_cannot_modify_super_admin + */ + public function test_sub_admin_cannot_unlock_super_admin(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $superAdmin = $this->createSuperAdmin(); + + $response = $this->authAs($subAdmin)->postJson("/api/admin/users/{$superAdmin->uuid}/unlock"); + + $response->assertStatus(403); + } + + // ========================================================================= + // C-4 — 권한 부여 ceiling + // ========================================================================= + + /** + * 부관리자(core.permissions.create 보유)가 자신이 보유하지 않은 권한을 + * 역할에 부여하려 하면 403. + * + * @scenario actor=sub_admin, operation=grant_permission, outcome=blocked + * + * @effects sub_admin_cannot_grant_unheld_or_broader_permission + */ + public function test_sub_admin_cannot_grant_permission_it_does_not_hold(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.create' => null, + 'core.permissions.read' => null, + ]); + $deletePerm = Permission::where('identifier', 'core.users.delete')->firstOrFail(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/roles', [ + 'identifier' => $this->roleIdentifier(), + 'name' => ['ko' => '신규역할', 'en' => 'New Role'], + 'permissions' => [ + ['id' => $deletePerm->id, 'scope_type' => null], + ], + ]); + + $response->assertStatus(403); + } + + /** + * 상한 위반으로 거부된 역할 생성은 역할 행 자체를 남기지 않는다. + * + * 상한 검사가 역할 생성 **뒤**에 있으면 403 을 받은 요청이 권한 0개짜리 고아 역할을 + * 남긴다. 사용자 경로(UserService)는 이미 "가드 → 쓰기" 순서로 해결한 사안이며, + * 역할 경로도 같은 순서여야 한다. 403 만 단언하면 이 부작용이 고정되지 않는다. + * + * @scenario actor=sub_admin, operation=grant_permission, outcome=blocked + * + * @effects rejected_role_write_leaves_no_partial_state + */ + public function test_rejected_role_creation_leaves_no_orphan_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.create' => null, + 'core.permissions.read' => null, + ]); + $deletePerm = Permission::where('identifier', 'core.users.delete')->firstOrFail(); + $identifier = $this->roleIdentifier(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/roles', [ + 'identifier' => $identifier, + 'name' => ['ko' => '신규역할', 'en' => 'New Role'], + 'permissions' => [ + ['id' => $deletePerm->id, 'scope_type' => null], + ], + ]); + + $response->assertStatus(403); + $this->assertNull( + Role::where('identifier', $identifier)->first(), + '거부된 역할 생성이 고아 역할 행을 남기면 안 됩니다' + ); + } + + /** + * 상한 위반으로 거부된 역할 수정은 속성 변경도 반영하지 않는다. + * + * 상한 검사가 속성 update **뒤**에 있으면 403 인데 name/is_active 는 이미 바뀐다. + * + * @scenario actor=sub_admin, operation=update_role, outcome=blocked + * + * @effects rejected_role_write_leaves_no_partial_state + */ + public function test_rejected_role_update_does_not_apply_attribute_changes(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.create' => null, + 'core.permissions.read' => null, + 'core.permissions.update' => null, + ]); + $deletePerm = Permission::where('identifier', 'core.users.delete')->firstOrFail(); + + $role = Role::create([ + 'identifier' => $this->roleIdentifier(), + 'name' => json_encode(['ko' => '원본역할', 'en' => 'Original Role']), + 'description' => json_encode(['ko' => '설명', 'en' => 'Desc']), + 'extension_type' => null, + 'is_active' => true, + ]); + + // 저장 표현(인코딩 형태)에 의존하지 않도록, 증명하려는 명제 그대로 + // "요청 전후 저장값이 동일하다" 를 단언한다. + $nameBefore = $role->getRawOriginal('name'); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/roles/{$role->id}", [ + 'identifier' => $role->identifier, + 'name' => ['ko' => '변조된역할', 'en' => 'Tampered Role'], + 'permissions' => [ + ['id' => $deletePerm->id, 'scope_type' => null], + ], + ]); + + $response->assertStatus(403); + $role->refresh(); + + $this->assertSame( + $nameBefore, + $role->getRawOriginal('name'), + '거부된 역할 수정이 name 속성을 변경하면 안 됩니다' + ); + } + + /** + * syncPermissions 를 직접 호출해도 보호 역할(코어/확장 소유) 가드가 걸린다. + * + * `updateRole`·`toggleRoleStatus` 는 진입부에 보호 역할 가드를 두는데 형제 public + * 메서드인 `syncPermissions` 만 비어 있으면, 이 서비스를 주입한 확장이 + * `syncPermissions($coreAdminRole, …)` 로 코어 역할 보호를 우회한다. + * + * @scenario actor=sub_admin, operation=update_role, outcome=blocked + * + * @effects sub_admin_cannot_modify_protected_role + */ + public function test_sync_permissions_directly_rejects_protected_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + $this->actingAs($subAdmin); + + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $readPerm = Permission::where('identifier', 'core.permissions.read')->firstOrFail(); + $before = $adminRole->permissions()->pluck('permissions.id')->sort()->values()->all(); + + // expectException 은 throw 시점에 테스트를 끝내므로 뒤의 단언이 실행되지 않는다. + // 형제 두 경로(고아 역할·속성 변경)에는 상태 불변 단언이 붙어 있는데 이 경로만 + // 예외 종류만 보고 있었다 — 가드가 sync 뒤로 밀리면 권한이 이미 치환된 뒤 403 이 + // 되고, 그 회귀를 예외 단언은 잡지 못한다. + try { + app(RoleService::class)->syncPermissions($adminRole, [ + ['id' => $readPerm->id, 'scope_type' => null], + ]); + $this->fail('보호 역할의 syncPermissions 는 거부되어야 합니다'); + } catch (CannotModifyProtectedRoleException) { + // 기대 경로 + } + + $after = $adminRole->fresh()->permissions()->pluck('permissions.id')->sort()->values()->all(); + $this->assertSame($before, $after, '거부된 syncPermissions 가 권한 집합을 바꾸면 안 됩니다'); + } + + /** + * 부관리자가 자신이 보유한 권한을 자신의 범위 이내로 부여하는 것은 허용된다(회귀). + * + * @scenario actor=sub_admin, operation=grant_permission, outcome=allowed + * + * @effects sub_admin_cannot_grant_unheld_or_broader_permission + */ + public function test_sub_admin_can_grant_held_permission_within_scope(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.create' => null, + 'core.permissions.read' => null, + 'core.users.read' => null, + ]); + $readPerm = Permission::where('identifier', 'core.users.read')->firstOrFail(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/roles', [ + 'identifier' => $this->roleIdentifier(), + 'name' => ['ko' => '조회역할', 'en' => 'Read Role'], + 'permissions' => [ + ['id' => $readPerm->id, 'scope_type' => null], + ], + ]); + + $response->assertStatus(201); + } + + /** + * 부관리자가 자신의 범위(self)보다 넓은 범위(global)로 권한을 부여하려 하면 403. + * + * @scenario actor=sub_admin, operation=grant_permission, outcome=blocked + * + * @effects sub_admin_cannot_grant_unheld_or_broader_permission + */ + public function test_sub_admin_cannot_grant_broader_scope_than_own(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.create' => null, + 'core.permissions.read' => null, + 'core.users.read' => 'self', + ]); + $readPerm = Permission::where('identifier', 'core.users.read')->firstOrFail(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/roles', [ + 'identifier' => $this->roleIdentifier(), + 'name' => ['ko' => '광역역할', 'en' => 'Wide Role'], + 'permissions' => [ + ['id' => $readPerm->id, 'scope_type' => null], // global = self 보다 넓음 + ], + ]); + + $response->assertStatus(403); + } + + /** + * (회귀) 슈퍼 관리자는 임의 권한을 역할에 부여할 수 있다. + * + * @scenario actor=super_admin, operation=grant_permission, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_grant_any_permission(): void + { + $superAdmin = $this->createSuperAdmin(); + $deletePerm = Permission::where('identifier', 'core.users.delete')->firstOrFail(); + + $response = $this->authAs($superAdmin)->postJson('/api/admin/roles', [ + 'identifier' => $this->roleIdentifier(), + 'name' => ['ko' => '전권역할', 'en' => 'Full Role'], + 'permissions' => [ + ['id' => $deletePerm->id, 'scope_type' => null], + ], + ]); + + $response->assertStatus(201); + } + + // ========================================================================= + // C-4b — 역할 재할당 ceiling (updateUser role_ids) + // ========================================================================= + + /** + * 부관리자(core.users.update + core.permissions.update 보유)가 자신이 전부 부여할 수 + * 없는 권한을 담은 역할(admin)을 사용자에게 붙이려 하면 403 + 미부여. + * + * C-4 가 RoleService(역할→권한 부여)만 막으면, role_ids 경로로 고권한 역할을 통째로 + * 붙여 동일한 권한 상승을 달성할 수 있다(KVE-2026-1919 완결 — 계획 107). + * + * @scenario actor=sub_admin, operation=assign_role, outcome=blocked + * + * @effects sub_admin_cannot_assign_role_it_cannot_fully_grant + */ + public function test_sub_admin_cannot_assign_role_it_cannot_fully_grant(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.permissions.update' => null, + ]); + $target = User::factory()->create(['status' => 'active']); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'role_ids' => [$adminRole->id], + ])); + + $response->assertStatus(403); + $target->refresh(); + $this->assertFalse($target->roles->contains('id', $adminRole->id), 'admin 역할이 부여되면 안 됩니다'); + } + + /** + * (회귀) 슈퍼 관리자는 admin 역할을 사용자에게 부여할 수 있다. + * + * @scenario actor=super_admin, operation=assign_role, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_assign_admin_role(): void + { + $actor = $this->createSuperAdmin(); + $target = User::factory()->create(['status' => 'active']); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + + $response = $this->authAs($actor)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'role_ids' => [$adminRole->id], + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertTrue($target->roles->contains('id', $adminRole->id)); + } + + /** + * (회귀) 부관리자는 자신이 전부 부여 가능한 권한만 담은 역할은 붙일 수 있다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects sub_admin_cannot_assign_role_it_cannot_fully_grant + */ + public function test_sub_admin_can_assign_role_within_its_ceiling(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.permissions.update' => null, + 'core.users.read' => null, + ]); + $target = User::factory()->create(['status' => 'active']); + + $readOnlyRole = Role::create([ + 'identifier' => 'read_only_'.substr(uniqid(), -8), + 'name' => json_encode(['ko' => '읽기역할', 'en' => 'Read Role']), + 'extension_type' => null, + 'is_active' => true, + ]); + $readPerm = Permission::where('identifier', 'core.users.read')->firstOrFail(); + $readOnlyRole->permissions()->sync([$readPerm->id => ['scope_type' => null, 'granted_at' => now()]]); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'role_ids' => [$readOnlyRole->id], + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertTrue($target->roles->contains('id', $readOnlyRole->id)); + } + + /** + * (회귀) 대상이 이미 보유한 역할을 유지하는 수정은 ceiling 에 걸리지 않는다. + * + * ceiling 은 새로 추가되는 역할에만 적용된다 — 기존 역할 유지는 권한 상승이 아니므로 + * 부관리자가 admin 보유 사용자의 이름을 바꾸는 정상 흐름을 막지 않는다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects sub_admin_cannot_assign_role_it_cannot_fully_grant + */ + public function test_keeping_existing_high_role_is_not_blocked(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.permissions.update' => null, + ]); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $target = User::factory()->create(['status' => 'active', 'name' => 'Before']); + $target->roles()->attach($adminRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target->fresh(), [ + 'name' => 'After', + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertSame('After', $target->name); + $this->assertTrue($target->roles->contains('id', $adminRole->id), '기존 admin 역할이 유지되어야 합니다'); + } + + /** + * 부관리자(core.users.create + core.permissions.update)가 신규 사용자를 만들면서 + * 자신이 전부 부여할 수 없는 역할(admin)을 붙이려 하면 403. + * + * 생성 경로도 수정 경로와 동일한 재할당 상한을 받아야 우회로가 되지 않는다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=blocked + * + * @effects sub_admin_cannot_assign_role_it_cannot_fully_grant + */ + public function test_sub_admin_cannot_create_user_with_role_it_cannot_fully_grant(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.create' => null, + 'core.permissions.update' => null, + ]); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/users', [ + 'name' => '신규관리자', + 'email' => 'ceil_'.substr(uniqid(), -8).'@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'role_ids' => [$adminRole->id], + ]); + + $response->assertStatus(403); + } + + /** + * (회귀) 부관리자는 기본 'user' 역할을 사용자에게 부여할 수 있어야 한다. + * + * 기본 'user' 역할은 모든 회원이 갖는 baseline 역할이다(자동 배정 경로가 + * 심는 바로 그 역할). 그 역할의 권한(알림 self-service·본인인증)은 권한 상승 + * 벡터가 아니므로 재할당 상한에 걸려선 안 된다. core.permissions.update 를 가진 + * 부관리자가 baseline 역할을 명시 지정했다고 403 이 되면 정상 회원 생성/수정이 + * 깨진다(KVE-2026-1919 회귀). auto-assign 경로(상한 미적용)와의 비대칭도 제거한다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects sub_admin_can_assign_baseline_default_user_role + */ + public function test_sub_admin_can_assign_default_user_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.permissions.update' => null, + ]); + $target = User::factory()->create(['status' => 'active']); + $userRole = Role::where('identifier', 'user')->firstOrFail(); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'role_ids' => [$userRole->id], + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertTrue($target->roles->contains('id', $userRole->id), '기본 user 역할이 부여되어야 합니다'); + } + + /** + * (회귀) 부관리자는 신규 사용자를 만들면서 기본 'user' 역할을 명시 지정할 수 있어야 한다. + * + * 생성 경로도 baseline 역할 배정을 상한에서 면제해야 한다 — 수정 경로와 동일 강도. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects sub_admin_can_assign_baseline_default_user_role + */ + public function test_sub_admin_can_create_user_with_default_user_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.create' => null, + 'core.permissions.update' => null, + ]); + $userRole = Role::where('identifier', 'user')->firstOrFail(); + + $response = $this->authAs($subAdmin)->postJson('/api/admin/users', [ + 'name' => '신규회원', + 'email' => 'baseline_'.substr(uniqid(), -8).'@example.com', + 'password' => 'Password123!', + 'password_confirmation' => 'Password123!', + 'role_ids' => [$userRole->id], + ]); + + $response->assertStatus(201); + } + + /** + * (회귀) 역할 부여는 core.permissions.update(역할 정의 수정)를 요구하지 않는다. + * + * 부관리자가 core.users.update 만 갖고 core.permissions.update 는 없어도, 상한 이내의 + * 역할이라면 사용자에게 부여할 수 있어야 한다. 과거에는 core.permissions.update 가 없으면 + * role_ids 를 조용히 버려(silent no-op) 200 성공을 반환하면서도 역할이 바뀌지 않았다 + * (공개 제보 — 매니저 계정이 자기 수정 화면에서 하위 역할조차 부여하지 못함). 이제는 + * 역할 부여가 "사용자 관리"의 일부로 재분류되어 상한(ceiling)만으로 통제된다. + * + * @scenario actor=sub_admin, operation=assign_role, outcome=allowed + * + * @effects role_assignment_does_not_require_permissions_update + */ + public function test_sub_admin_can_assign_role_within_ceiling_without_permissions_update(): void + { + // core.permissions.update 없이 core.users.update 만 보유 + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.users.read' => null, + ]); + $target = User::factory()->create(['status' => 'active']); + + // 부관리자가 전부 부여 가능한 권한(core.users.read)만 담은 역할 → 상한 이내 + $readOnlyRole = Role::create([ + 'identifier' => 'read_only_nopu_'.substr(uniqid(), -8), + 'name' => json_encode(['ko' => '읽기역할', 'en' => 'Read Role']), + 'extension_type' => null, + 'is_active' => true, + ]); + $readPerm = Permission::where('identifier', 'core.users.read')->firstOrFail(); + $readOnlyRole->permissions()->sync([$readPerm->id => ['scope_type' => null, 'granted_at' => now()]]); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target, [ + 'role_ids' => [$readOnlyRole->id], + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertTrue( + $target->roles->contains('id', $readOnlyRole->id), + 'core.permissions.update 없이도 상한 이내 역할은 부여되어야 합니다 (silent no-op 회귀 방지)' + ); + } + + /** + * 부관리자(core.users.update, core.permissions.update 없음)가 자신이 전부 부여할 수 없는 + * 역할(admin)을 다른 사용자에게서 **제거**하려 하면 403 + 역할 불변. + * + * 역할 조작 상한은 추가·제거 양방향에 대칭 적용된다. 추가만 검사하면 core.users.update 만 + * 가진 하위 관리자가 다른 관리자의 상위 역할(admin)을 박탈하는 하향 조작이 상한 없이 + * 뚫린다(KVE-2026-1919 완결 — 제거 방향). 이 결함은 예외·경고 없이 200 성공으로만 + * 드러나므로 차단 + 역할 불변을 명시 고정한다. + * + * @scenario actor=sub_admin, operation=remove_role, outcome=blocked + * + * @effects sub_admin_cannot_remove_role_above_its_ceiling, rejected_target_state_and_tokens_unchanged + */ + public function test_sub_admin_cannot_remove_role_it_cannot_fully_grant(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $userRole = Role::where('identifier', 'user')->firstOrFail(); + $target = User::factory()->create(['status' => 'active']); + $target->roles()->attach($adminRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + + // admin 을 떼고 baseline user 만 남기려는 시도 — 제거되는 admin 이 상한 밖 + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target->fresh(), [ + 'role_ids' => [$userRole->id], + ])); + + $response->assertStatus(403); + $target->refresh(); + $this->assertTrue($target->roles->contains('id', $adminRole->id), 'admin 역할이 제거되면 안 됩니다'); + } + + /** + * (회귀) 부관리자는 자신이 전부 부여 가능한 역할은 사용자에게서 제거할 수 있다. + * + * 제거 방향 상한이 과차단되면 정상적인 하위 역할 정리까지 막힌다. 상한 이내 역할의 + * 제거가 정상 동작함을 명시 고정한다. + * + * @scenario actor=sub_admin, operation=remove_role, outcome=allowed + * + * @effects sub_admin_cannot_remove_role_above_its_ceiling + */ + public function test_sub_admin_can_remove_role_within_its_ceiling(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.users.update' => null, + 'core.users.read' => null, + ]); + $userRole = Role::where('identifier', 'user')->firstOrFail(); + $target = User::factory()->create(['status' => 'active']); + + $readOnlyRole = Role::create([ + 'identifier' => 'read_only_rm_'.substr(uniqid(), -8), + 'name' => json_encode(['ko' => '읽기역할', 'en' => 'Read Role']), + 'extension_type' => null, + 'is_active' => true, + ]); + $readPerm = Permission::where('identifier', 'core.users.read')->firstOrFail(); + $readOnlyRole->permissions()->sync([$readPerm->id => ['scope_type' => null, 'granted_at' => now()]]); + + $target->roles()->attach($userRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + $target->roles()->attach($readOnlyRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + + // read-only 역할 제거, baseline user 유지 — 제거되는 역할이 상한 이내 + $response = $this->authAs($subAdmin)->putJson("/api/admin/users/{$target->uuid}", $this->updatePayload($target->fresh(), [ + 'role_ids' => [$userRole->id], + ])); + + $response->assertOk(); + $target->refresh(); + $this->assertFalse($target->roles->contains('id', $readOnlyRole->id), '상한 이내 역할은 제거되어야 합니다'); + $this->assertTrue($target->roles->contains('id', $userRole->id), 'baseline user 역할은 유지되어야 합니다'); + } + + // ========================================================================= + // C-5 — 코어/확장 소유 역할 수정 보호 + // ========================================================================= + + /** + * 부관리자(core.permissions.update 보유)는 코어 admin 역할을 수정할 수 없다. + * + * @scenario actor=sub_admin, operation=update_role, outcome=blocked + * + * @effects sub_admin_cannot_toggle_or_update_core_admin_role + */ + public function test_sub_admin_cannot_update_core_admin_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/roles/{$adminRole->id}", [ + 'name' => ['ko' => '탈취관리자', 'en' => 'Hijacked'], + ]); + + $response->assertStatus(403); + } + + /** + * 부관리자는 코어 admin 역할의 활성 상태를 토글할 수 없다. + * + * @scenario actor=sub_admin, operation=toggle_role, outcome=blocked + * + * @effects sub_admin_cannot_toggle_or_update_core_admin_role + */ + public function test_sub_admin_cannot_toggle_core_admin_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $wasActive = $adminRole->is_active; + + $response = $this->authAs($subAdmin)->patchJson("/api/admin/roles/{$adminRole->id}/toggle-status"); + + $response->assertStatus(403); + $adminRole->refresh(); + $this->assertSame($wasActive, $adminRole->is_active); + } + + /** + * (회귀) 슈퍼 관리자는 코어 역할도 수정할 수 있다. + * + * @scenario actor=super_admin, operation=update_role, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_update_core_role(): void + { + $superAdmin = $this->createSuperAdmin(); + // 코어 소유이지만 admin 외의 역할을 골라 이름 변경 (admin 은 다른 불변식이 있을 수 있음) + $coreRole = Role::where('extension_type', ExtensionOwnerType::Core) + ->where('identifier', '!=', 'admin') + ->first(); + + if (! $coreRole) { + $this->markTestSkipped('코어 소유 비-admin 역할이 시드되지 않음'); + } + + $response = $this->authAs($superAdmin)->putJson("/api/admin/roles/{$coreRole->id}", [ + 'is_active' => $coreRole->is_active, + ]); + + $response->assertOk(); + } + + // ========================================================================= + // 과차단 회귀 축 (allowed) — 가드가 정상 조작까지 막지 않는지 + // ========================================================================= + // + // 상한 가드는 "막아야 할 것을 막는가" 만큼 "막지 말아야 할 것을 통과시키는가" 도 + // 고정해야 한다. 차단 축만 있으면 가드가 과차단으로 회귀해도 스위트가 green 이다 + // (C-4b 의 baseline user 역할 회귀가 정확히 그 형태로 운영에서 먼저 드러났다). + + /** + * 부관리자는 일반 사용자의 상태를 정상 변경할 수 있다. + * + * @scenario actor=sub_admin, operation=status, outcome=allowed + * + * @effects sub_admin_retains_normal_operations_on_non_super_targets + */ + public function test_sub_admin_can_change_regular_user_status(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $target = User::factory()->create(['is_super' => false, 'status' => 'active']); + + $response = $this->authAs($subAdmin)->putJson( + "/api/admin/users/{$target->uuid}", + $this->updatePayload($target, ['status' => 'blocked']) + ); + + $response->assertOk(); + $this->assertSame('blocked', $target->fresh()->status->value ?? $target->fresh()->status); + } + + /** + * 부관리자는 일반 사용자 계정을 정상 잠금 해제할 수 있다. + * + * @scenario actor=sub_admin, operation=unlock, outcome=allowed + * + * @effects sub_admin_retains_normal_operations_on_non_super_targets + */ + public function test_sub_admin_can_unlock_regular_user(): void + { + $subAdmin = $this->createSubAdmin(['core.users.update' => null]); + $target = User::factory()->create(['is_super' => false, 'status' => 'active']); + + $response = $this->authAs($subAdmin)->postJson("/api/admin/users/{$target->uuid}/unlock"); + + $response->assertOk(); + } + + /** + * 부관리자는 코어/확장 소유가 아닌 일반 역할을 정상 수정할 수 있다. + * + * @scenario actor=sub_admin, operation=update_role, outcome=allowed + * + * @effects sub_admin_retains_normal_operations_on_unprotected_roles + */ + public function test_sub_admin_can_update_non_core_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + $role = Role::create([ + 'identifier' => $this->roleIdentifier(), + 'name' => json_encode(['ko' => '일반역할', 'en' => 'Plain Role']), + 'description' => json_encode(['ko' => '보호 대상 아님', 'en' => 'Not protected']), + 'extension_type' => null, + 'is_active' => true, + ]); + + $response = $this->authAs($subAdmin)->putJson("/api/admin/roles/{$role->id}", [ + 'name' => ['ko' => '이름변경', 'en' => 'Renamed'], + ]); + + $response->assertOk(); + } + + /** + * 부관리자는 일반 역할의 활성 상태를 정상 토글할 수 있다. + * + * @scenario actor=sub_admin, operation=toggle_role, outcome=allowed + * + * @effects sub_admin_retains_normal_operations_on_unprotected_roles + */ + public function test_sub_admin_can_toggle_non_core_role(): void + { + $subAdmin = $this->createSubAdmin([ + 'core.permissions.update' => null, + 'core.permissions.read' => null, + ]); + $role = Role::create([ + 'identifier' => $this->roleIdentifier(), + 'name' => json_encode(['ko' => '일반역할', 'en' => 'Plain Role']), + 'description' => json_encode(['ko' => '보호 대상 아님', 'en' => 'Not protected']), + 'extension_type' => null, + 'is_active' => true, + ]); + + $response = $this->authAs($subAdmin)->patchJson("/api/admin/roles/{$role->id}/toggle-status"); + + $response->assertOk(); + $this->assertFalse($role->fresh()->is_active, '일반 역할은 토글되어야 합니다'); + } + + /** + * (회귀) 슈퍼 관리자는 슈퍼 관리자 계정도 잠금 해제할 수 있다. + * + * @scenario actor=super_admin, operation=unlock, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_unlock_super_admin(): void + { + $superAdmin = $this->createSuperAdmin(); + $target = $this->createSuperAdmin(); + + $response = $this->authAs($superAdmin)->postJson("/api/admin/users/{$target->uuid}/unlock"); + + $response->assertOk(); + } + + /** + * (회귀) 슈퍼 관리자는 상위 역할도 제거할 수 있다 — 제거 방향 상한이 슈퍼를 막지 않는다. + * + * @scenario actor=super_admin, operation=remove_role, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_remove_admin_role(): void + { + $superAdmin = $this->createSuperAdmin(); + $adminRole = Role::where('identifier', 'admin')->firstOrFail(); + $userRole = Role::where('identifier', 'user')->firstOrFail(); + + $target = User::factory()->create(['is_super' => false, 'status' => 'active']); + $target->roles()->attach($adminRole->id, ['assigned_at' => now(), 'assigned_by' => null]); + $target->refresh(); + + $response = $this->authAs($superAdmin)->putJson( + "/api/admin/users/{$target->uuid}", + $this->updatePayload($target, ['role_ids' => [$userRole->id]]) + ); + + $response->assertOk(); + $this->assertFalse( + $target->fresh()->roles->contains('id', $adminRole->id), + '슈퍼 관리자는 상위 역할을 제거할 수 있어야 합니다' + ); + } + + /** + * (회귀) 슈퍼 관리자는 코어 역할의 활성 상태를 토글할 수 있다. + * + * deleteRole 은 슈퍼에게도 코어 역할 삭제를 막지만 toggle 은 허용한다 — 슈퍼는 이미 + * 전권(is_super)이라 권한 상승이 아니므로 양성 비대칭이다. 그 상태를 고정한다. + * + * @scenario actor=super_admin, operation=toggle_role, outcome=allowed + * + * @effects super_admin_actor_performs_same_operations + */ + public function test_super_admin_can_toggle_core_role(): void + { + $superAdmin = $this->createSuperAdmin(); + $coreRole = Role::where('extension_type', ExtensionOwnerType::Core) + ->where('identifier', '!=', 'admin') + ->first(); + + if (! $coreRole) { + $this->markTestSkipped('코어 소유 비-admin 역할이 시드되지 않음'); + } + + $wasActive = $coreRole->is_active; + + $response = $this->authAs($superAdmin)->patchJson("/api/admin/roles/{$coreRole->id}/toggle-status"); + + $response->assertOk(); + $this->assertNotSame($wasActive, $coreRole->fresh()->is_active, '코어 역할 토글이 반영되어야 합니다'); + } + + // ========================================================================= + // C-2c — 첨부 순서 변경(정적 라우트)의 스코프 축 재적용 + // + // bulk-status 와 같은 형태다: `PATCH admin/attachments/reorder` 에는 `{attachment}` + // 파라미터가 없어 PermissionMiddleware 의 스코프 검사가 스킵된다. 그런데 배포 기본 + // 역할 `manager` 는 core.attachments.update 를 self 스코프로 보유한다 — 이론 구성이 + // 아니라 기본값에서 성립하는 우회로였다. + // + // 사용자 일괄 상태변경과 달리 "제외" 가 아니라 "거부" 다. 순서는 집합 전체에 대한 + // 하나의 배열이라 일부만 반영하면 나머지와 어긋난 순서가 저장된다. + // ========================================================================= + + /** + * self 스코프 액터는 타인 소유 첨부의 순서를 바꿀 수 없다. + * + * @effects attachment_reorder_reapplies_scope_gate, rejected_target_state_and_tokens_unchanged + */ + public function test_self_scoped_admin_cannot_reorder_others_attachments(): void + { + $subAdmin = $this->createSubAdmin(['core.attachments.update' => 'self']); + $other = User::factory()->create(); + + $mine = Attachment::factory()->create(['created_by' => $subAdmin->id, 'order' => 1]); + $theirs = Attachment::factory()->create(['created_by' => $other->id, 'order' => 2]); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/attachments/reorder', [ + 'order' => [ + ['id' => $mine->id, 'order' => 2], + ['id' => $theirs->id, 'order' => 1], + ], + ]); + + $response->assertStatus(403); + + // 거부는 전량 거부다 — 내 첨부까지 반영되지 않아야 순서가 어긋나지 않는다. + $this->assertSame(1, $mine->fresh()->order, '거부된 요청은 어떤 순서도 바꾸지 않아야 합니다'); + $this->assertSame(2, $theirs->fresh()->order); + } + + /** + * (과차단 회귀) self 스코프 액터도 자기 소유 첨부는 순서를 바꿀 수 있다. + * + * @effects attachment_reorder_allows_in_scope_targets + */ + public function test_self_scoped_admin_can_reorder_own_attachments(): void + { + $subAdmin = $this->createSubAdmin(['core.attachments.update' => 'self']); + + $first = Attachment::factory()->create(['created_by' => $subAdmin->id, 'order' => 1]); + $second = Attachment::factory()->create(['created_by' => $subAdmin->id, 'order' => 2]); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/attachments/reorder', [ + 'order' => [ + ['id' => $first->id, 'order' => 2], + ['id' => $second->id, 'order' => 1], + ], + ]); + + $response->assertOk(); + $this->assertSame(2, $first->fresh()->order); + $this->assertSame(1, $second->fresh()->order); + } + + /** + * (과차단 회귀) 글로벌 스코프 액터는 타인 소유 첨부도 정렬할 수 있다. + * + * @effects attachment_reorder_allows_in_scope_targets + */ + public function test_global_scoped_admin_can_reorder_any_attachment(): void + { + $subAdmin = $this->createSubAdmin(['core.attachments.update' => null]); + $other = User::factory()->create(); + + $theirs = Attachment::factory()->create(['created_by' => $other->id, 'order' => 1]); + + $response = $this->authAs($subAdmin)->patchJson('/api/admin/attachments/reorder', [ + 'order' => [['id' => $theirs->id, 'order' => 5]], + ]); + + $response->assertOk(); + $this->assertSame(5, $theirs->fresh()->order); + } + + // ========================================================================= + // C-2d — 메뉴 순서 변경(정적 라우트)의 스코프 축 재적용 + // + // 첨부 순서와 같은 형태. 배포 기본 역할은 core.menus.update 를 글로벌로 주므로 + // 기본값 노출은 아니지만, 운영자가 역할 화면에서 스코프를 좁히는 순간 성립한다. + // ========================================================================= + + /** + * self 스코프 액터는 타인이 만든 메뉴의 순서를 바꿀 수 없다. + * + * @effects menu_order_reapplies_scope_gate, rejected_target_state_and_tokens_unchanged + */ + public function test_self_scoped_admin_cannot_reorder_others_menus(): void + { + $subAdmin = $this->createSubAdmin(['core.menus.update' => 'self']); + $other = User::factory()->create(); + + $mine = Menu::factory()->create(['created_by' => $subAdmin->id, 'order' => 1]); + $theirs = Menu::factory()->create(['created_by' => $other->id, 'order' => 2]); + + $response = $this->authAs($subAdmin)->putJson('/api/admin/menus/order', [ + 'parent_menus' => [ + ['id' => $mine->id, 'order' => 2], + ['id' => $theirs->id, 'order' => 1], + ], + ]); + + $response->assertStatus(403); + + // 전량 거부 — 내 메뉴 순서도 바뀌지 않아야 트리가 어긋나지 않는다. + $this->assertSame(1, $mine->fresh()->order, '거부된 요청은 어떤 순서도 바꾸지 않아야 합니다'); + $this->assertSame(2, $theirs->fresh()->order); + } + + /** + * (과차단 회귀) 글로벌 스코프 액터는 메뉴 순서를 정상 변경할 수 있다. + * + * @effects menu_order_allows_in_scope_targets + */ + public function test_global_scoped_admin_can_reorder_menus(): void + { + $subAdmin = $this->createSubAdmin(['core.menus.update' => null]); + $other = User::factory()->create(); + + $theirs = Menu::factory()->create(['created_by' => $other->id, 'order' => 1]); + + $response = $this->authAs($subAdmin)->putJson('/api/admin/menus/order', [ + 'parent_menus' => [['id' => $theirs->id, 'order' => 3]], + ]); + + $response->assertOk(); + $this->assertSame(3, $theirs->fresh()->order); + } + + /** + * (형제 public 메서드 패리티) `updateMenuOrder` 를 직접 호출해도 스코프 가드가 걸린다. + * + * `updateMenuOrderWithHierarchy` 는 진입부에 스코프 가드를 두는데 같은 리소스를 같은 + * 방식으로 쓰는 형제 public 메서드 `updateMenuOrder` 만 비어 있으면, 이 서비스를 주입한 + * 확장이 그쪽으로 코어 스코프 보호를 우회한다. `RoleService::syncPermissions` 에 가드를 + * 넣은 것과 같은 판정이다 — 코어 내 호출부가 0 이라는 사실은 방어가 아니다. + * + * @effects menu_order_service_method_guard_parity + */ + public function test_update_menu_order_directly_reapplies_scope_gate(): void + { + $subAdmin = $this->createSubAdmin(['core.menus.update' => 'self']); + $other = User::factory()->create(); + + $mine = Menu::factory()->create(['created_by' => $subAdmin->id, 'order' => 1]); + $theirs = Menu::factory()->create(['created_by' => $other->id, 'order' => 2]); + + $this->actingAs($subAdmin); + + // expectException 은 throw 시점에 테스트를 끝내므로 상태 불변 단언이 실행되지 않는다. + // 형제 경로(HTTP)에 붙어 있는 전량 거부 단언과 같은 강도로 확인한다. + try { + app(MenuService::class)->updateMenuOrder([$mine->id, $theirs->id]); + $this->fail('스코프 밖 메뉴가 섞인 순서 변경은 거부되어야 합니다'); + } catch (AuthorizationException) { + // 기대 경로 + } + + $this->assertSame(1, $mine->fresh()->order, '거부된 요청은 어떤 순서도 바꾸지 않아야 합니다'); + $this->assertSame(2, $theirs->fresh()->order); + } + + /** + * (과차단 회귀) self 스코프 액터가 **타인 소유 상위 메뉴 아래로** 자기 메뉴를 옮기는 것은 허용된다. + * + * 순서 변경 스코프 가드는 이동 항목의 **자신만** 검사한다. 착수 단계에서 `new_parent_id` + * 까지 검사했다가 철회한 이력이 있는데(결함은 "남의 메뉴 순서를 바꾼다" 이지 "남의 메뉴 + * 아래에 못 붙인다" 가 아니다), 그 철회가 주석으로만 고정돼 있어 되돌리는 변경을 넣어도 + * 스위트가 green 이었다. 이 테스트가 그 축을 고정한다. + * + * @effects menu_order_move_under_others_parent_allowed + */ + public function test_self_scoped_admin_can_move_own_menu_under_others_parent(): void + { + $subAdmin = $this->createSubAdmin(['core.menus.update' => 'self']); + $other = User::factory()->create(); + + $mine = Menu::factory()->create(['created_by' => $subAdmin->id, 'order' => 1, 'parent_id' => null]); + $theirsParent = Menu::factory()->create(['created_by' => $other->id, 'order' => 2, 'parent_id' => null]); + + $response = $this->authAs($subAdmin)->putJson('/api/admin/menus/order', [ + 'parent_menus' => [['id' => $mine->id, 'order' => 1]], + 'moved_items' => [['id' => $mine->id, 'new_parent_id' => $theirsParent->id]], + ]); + + $response->assertOk(); + $this->assertSame( + $theirsParent->id, + $mine->fresh()->parent_id, + '자기 메뉴를 공용/타인 상위 메뉴 아래에 다는 정상 사용이 막히면 안 됩니다' + ); + } +} diff --git a/tests/Feature/Security/ScheduleCommandGuardTest.php b/tests/Feature/Security/ScheduleCommandGuardTest.php index 9d802b74..07b23d49 100644 --- a/tests/Feature/Security/ScheduleCommandGuardTest.php +++ b/tests/Feature/Security/ScheduleCommandGuardTest.php @@ -516,10 +516,224 @@ class ScheduleCommandGuardTest extends TestCase $response->assertJsonValidationErrors('command'); } + // ======================================================================== + // Shell 인터프리터 정책 (KVE-2026-1653) + // ======================================================================== + + /** + * 게이트를 켜고 `bash` 등 인터프리터를 화이트리스트에 등록해도, 인터프리터에 + * 인라인 명령/코드를 넘기는 저장 요청은 422 로 거부된다. + * + * KVE-2026-1653 본체: `bash` 를 화이트리스트에 등록하면 `bash -c id` 가 통과해 + * 임의 OS 명령이 실행됐다. 이제 첫 인자가 하이픈(코드 플래그)이면 저장 시점에 거부된다. + * + * @param string $command 인라인 코드/명령 command + * + * @scenario command_class=inline_code, enforcement_point=store + * + * @effects inline_code_flags_rejected + */ + #[DataProvider('interpreterInlineCodeProvider')] + public function test_store_rejects_interpreter_inline_code_even_when_allowlisted(string $command): void + { + $this->enableInterpreterShell(); + + $response = $this->authRequest()->postJson(route('api.admin.schedules.store'), [ + 'name' => '인라인 코드 시도', + 'type' => ScheduleType::Shell->value, + 'command' => $command, + 'expression' => '* * * * *', + 'frequency' => 'everyMinute', + ]); + + $response->assertStatus(422, "인라인 코드가 저장됨: {$command}"); + $response->assertJsonValidationErrors('command'); + } + + /** + * 인터프리터 + 절대경로 스크립트 실행은 게이트를 켜고 등록하면 201 로 허용된다 + * (범용 크론 기능 회귀 가드). + * + * @param string $command 스크립트 파일 실행 command + * + * @scenario command_class=script_ok, enforcement_point=store + * + * @effects interpreter_script_files_still_run + */ + #[DataProvider('interpreterScriptProvider')] + public function test_store_allows_interpreter_running_absolute_script(string $command): void + { + $this->enableInterpreterShell(); + + $response = $this->authRequest()->postJson(route('api.admin.schedules.store'), [ + 'name' => '정상 스크립트 스케줄', + 'type' => ScheduleType::Shell->value, + 'command' => $command, + 'expression' => '* * * * *', + 'frequency' => 'everyMinute', + ]); + + $response->assertStatus(201, "정상 스크립트 실행이 거부됨: {$command}"); + } + + /** + * `php artisan ...` 를 shell 로 실행하려는 저장 요청은 422 로 거부된다 (Artisan 축 우회 방지). + * + * @param string $command artisan 우회 command + * + * @scenario command_class=artisan_bypass, enforcement_point=store + * + * @effects artisan_via_shell_rejected + */ + #[DataProvider('artisanBypassShellProvider')] + public function test_store_rejects_artisan_via_shell(string $command): void + { + $this->enableInterpreterShell(); + + $response = $this->authRequest()->postJson(route('api.admin.schedules.store'), [ + 'name' => 'artisan 우회 시도', + 'type' => ScheduleType::Shell->value, + 'command' => $command, + 'expression' => '* * * * *', + 'frequency' => 'everyMinute', + ]); + + $response->assertStatus(422, "artisan 우회가 저장됨: {$command}"); + $response->assertJsonValidationErrors('command'); + } + + /** + * 완전 거부형 실행기(env/make/xargs/sudo/busybox)는 화이트리스트에 등재돼도 422 로 거부된다. + * + * @param string $command 완전 거부형 command + * + * @scenario command_class=reject_binary, enforcement_point=store + * + * @effects reject_binaries_blocked_even_when_allowlisted + */ + #[DataProvider('rejectBinaryShellProvider')] + public function test_store_rejects_reject_binaries_even_when_allowlisted(string $command): void + { + $this->enableInterpreterShell(); + + $response = $this->authRequest()->postJson(route('api.admin.schedules.store'), [ + 'name' => '완전 거부형 시도', + 'type' => ScheduleType::Shell->value, + 'command' => $command, + 'expression' => '* * * * *', + 'frequency' => 'everyMinute', + ]); + + $response->assertStatus(422, "완전 거부형이 저장됨: {$command}"); + $response->assertJsonValidationErrors('command'); + } + + /** + * 기존 shell 스케줄의 command 만 인라인 코드로 바꾸는 update 요청도 거부된다. + * + * @scenario command_class=inline_code, enforcement_point=update + * + * @effects update_falls_back_to_the_stored_type_when_the_request_omits_it + */ + public function test_update_rejects_interpreter_inline_code_without_type_in_request(): void + { + $this->enableInterpreterShell(); + + $schedule = Schedule::create([ + 'name' => '기존 스크립트 스케줄', + 'type' => ScheduleType::Shell, + 'command' => 'bash /app/deploy.sh', + 'expression' => '* * * * *', + 'is_active' => true, + ]); + + $response = $this->authRequest()->putJson( + route('api.admin.schedules.update', ['schedule' => $schedule->id]), + ['command' => 'bash -c id'] + ); + + $response->assertStatus(422); + $response->assertJsonValidationErrors('command'); + } + + /** + * 게이트를 켜고 `bash` 를 등록한 상태에서 DB 에 직접 심어진 `bash -c id` 는 + * 실행 시점에 차단된다 (마지막 방어선 — 저장 검증 우회 값 방어). + * + * @scenario command_class=inline_code, enforcement_point=run + * + * @effects run_time_last_line_of_defense_blocks_inline_code + */ + public function test_run_blocks_interpreter_inline_code_injected_directly(): void + { + $this->enableInterpreterShell(); + Process::fake(); + + $schedule = Schedule::create([ + 'name' => 'DB 직접 주입 — 인라인 코드', + 'type' => ScheduleType::Shell, + 'command' => 'bash -c id', + 'expression' => '* * * * *', + 'is_active' => true, + ]); + + $this->assertScheduleRunIsBlocked($schedule); + Process::assertNothingRan(); + } + + /** + * 슈퍼 관리자도 인터프리터 인라인 코드는 저장할 수 없다 (권한 분기 무관 동일 차단). + * + * @scenario command_class=inline_code, enforcement_point=store, caller_role=super_admin + * + * @effects inline_code_flags_rejected + */ + public function test_store_rejects_interpreter_inline_code_for_super_admin(): void + { + $this->enableInterpreterShell(); + + // 검증 계층까지 도달하도록 스케줄 권한을 갖춘 계정에 is_super 를 부여한다 — + // 슈퍼 관리자도 인라인 코드는 저장 단계에서 막힘을 증명하기 위함(권한 게이트가 아니라 검증). + $superAdmin = $this->createScheduleOperator(); + $superAdmin->forceFill(['is_super' => true])->save(); + $superToken = $superAdmin->createToken('super-token')->plainTextToken; + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$superToken, + 'Accept' => 'application/json', + ])->postJson(route('api.admin.schedules.store'), [ + 'name' => '슈퍼 관리자 인라인 코드', + 'type' => ScheduleType::Shell->value, + 'command' => 'bash -c id', + 'expression' => '* * * * *', + 'frequency' => 'everyMinute', + ]); + + $response->assertStatus(422); + $response->assertJsonValidationErrors('command'); + } + // ======================================================================== // Helpers // ======================================================================== + /** + * shell 게이트를 켜고 인터프리터·완전거부형을 화이트리스트에 등록합니다. + * + * 인터프리터를 **등재한 상태**로 검증해 "등재돼도 인라인코드/artisan/완전거부형은 + * 차단됨" 을 증명한다 (범용 크론 용도로 인터프리터 등록은 정상 운영이다). + */ + private function enableInterpreterShell(): void + { + config([ + 'schedule_security.shell.enabled' => true, + 'schedule_security.shell.allowed_binaries' => [ + 'bash', 'sh', 'python', 'python3', 'php', 'node', 'perl', 'ruby', + 'env', 'make', 'xargs', 'sudo', 'busybox', + ], + ]); + } + /** * 파서 분기 페이로드가 실행되었을 때 생성되는 마커 파일 경로. * @@ -655,6 +869,10 @@ class ScheduleCommandGuardTest extends TestCase 'about — 환경 정보 노출' => ['about'], 'queue:failed' => ['queue:failed'], 'storage:link' => ['storage:link'], + // KVE-2026-1897 회귀 가드: 언어팩 설치 명령은 예약 실행 허용목록 밖이므로 저장 시점에 거부된다 + // (커밋 #527 이 denylist→allowlist 로 전환한 뒤의 기대 동작을 고정). + 'language-pack:install — 확장 관리 명령' => ['language-pack:install g7-core-ja'], + 'language-pack:uninstall — 확장 관리 명령' => ['language-pack:uninstall g7-core-ja'], ]; } @@ -732,4 +950,66 @@ class ScheduleCommandGuardTest extends TestCase 'SEO 레이아웃 지정' => ['seo:warmup --layout=home'], ]; } + + /** + * 인터프리터에 인라인 코드/명령을 넘기는 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + public static function interpreterInlineCodeProvider(): array + { + return [ + 'bash -c id (본체)' => ['bash -c id'], + 'sh -c id' => ['sh -c id'], + 'python -c 코드' => ['python -c import_os'], + 'php -r 코드' => ['php -r phpinfo'], + 'node -e 코드' => ['node -e process'], + 'perl -e 코드' => ['perl -e system'], + 'bash -x 스크립트 앞 옵션' => ['bash -x /app/x.sh'], + ]; + } + + /** + * 인터프리터 + 절대경로 스크립트 command 목록 (전부 허용). + * + * @return array<string, array{string}> + */ + public static function interpreterScriptProvider(): array + { + return [ + 'python 스크립트' => ['python /app/job.py'], + 'php 스크립트' => ['php /app/legacy.php'], + 'node 스크립트' => ['node /app/worker.js'], + '스크립트 + 인자' => ['python /app/job.py --daily'], + ]; + } + + /** + * shell 로 artisan 을 실행하려는 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + public static function artisanBypassShellProvider(): array + { + return [ + 'php artisan' => ['php artisan'], + 'php 절대경로 artisan' => ['php /var/www/html/artisan'], + ]; + } + + /** + * 완전 거부형 실행기 command 목록 (전부 차단, 메타문자 없음). + * + * @return array<string, array{string}> + */ + public static function rejectBinaryShellProvider(): array + { + return [ + 'env bash' => ['env bash /app/x.sh'], + 'make -f' => ['make -f /tmp/Makefile'], + 'xargs' => ['xargs id'], + 'sudo' => ['sudo id'], + 'busybox' => ['busybox sh'], + ]; + } } diff --git a/tests/Feature/Security/SecurityExceptionRendererTest.php b/tests/Feature/Security/SecurityExceptionRendererTest.php new file mode 100644 index 00000000..506e85ea --- /dev/null +++ b/tests/Feature/Security/SecurityExceptionRendererTest.php @@ -0,0 +1,61 @@ +<?php + +namespace Tests\Feature\Security; + +use App\Exceptions\CannotModifyProtectedRoleException; +use App\Exceptions\CannotModifySuperAdminException; +use App\Exceptions\PermissionEscalationException; +use Illuminate\Support\Facades\Route; +use PHPUnit\Framework\Attributes\DataProvider; +use Tests\TestCase; + +/** + * 등급 상한 예외 3종의 전역 렌더러(bootstrap/app.php) 계약 테스트 + * + * KVE-2026-1919 가 도입한 렌더러는 심층 방어다 — 현재 호출부(UserController / + * RoleController)는 전부 로컬 catch 로 403 을 만들기 때문에, 렌더러 블록을 통째로 + * 지워도 기존 스위트는 전부 green 이 된다. 그러면 "새 호출부가 로컬 catch 없이 + * 던져도 조용한 500 대신 403" 이라는 렌더러의 존재 이유가 검증되지 않은 채 남는다. + * + * 그래서 로컬 catch 가 없는 경로를 테스트 안에서 만들어 렌더러 자신을 직접 건다. + */ +class SecurityExceptionRendererTest extends TestCase +{ + /** + * 로컬 catch 없이 던져진 등급 상한 예외 3종이 500 이 아닌 403 으로 렌더된다. + * + * @param string $exceptionClass 던질 예외 클래스 + * @param string $langKey 기대 메시지의 lang 키 + * + * @effects ceiling_exceptions_render_as_403_without_local_catch + */ + #[DataProvider('ceilingExceptionProvider')] + public function test_ceiling_exceptions_render_as_403_without_local_catch(string $exceptionClass, string $langKey): void + { + Route::middleware('api')->get('/api/_test/ceiling-render', function () use ($exceptionClass) { + throw new $exceptionClass; + }); + + $response = $this->getJson('/api/_test/ceiling-render'); + + $response->assertStatus(403); + $response->assertJson([ + 'success' => false, + 'message' => __($langKey), + ]); + } + + /** + * 예외 3종 × 대응 lang 키 + * + * @return array<string, array{0: class-string, 1: string}> + */ + public static function ceilingExceptionProvider(): array + { + return [ + 'super admin 보호' => [CannotModifySuperAdminException::class, 'exceptions.cannot_modify_super_admin'], + '보호 역할' => [CannotModifyProtectedRoleException::class, 'exceptions.cannot_modify_protected_role'], + '권한 상승' => [PermissionEscalationException::class, 'exceptions.cannot_grant_unheld_permission'], + ]; + } +} diff --git a/tests/Feature/Services/AdminWithdrawPathUnificationTest.php b/tests/Feature/Services/AdminWithdrawPathUnificationTest.php new file mode 100644 index 00000000..34f40ab0 --- /dev/null +++ b/tests/Feature/Services/AdminWithdrawPathUnificationTest.php @@ -0,0 +1,297 @@ +<?php + +namespace Tests\Feature\Services; + +use App\Enums\PermissionType; +use App\Enums\UserStatus; +use App\Extension\HookManager; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use App\Services\UserService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Validation\ValidationException; +use Tests\TestCase; + +/** + * 관리자 탈퇴 경로 통일 테스트 (공개이슈 #112 전수 해소분) + * + * 관리자가 상태만 '탈퇴'로 바꾸는 경로는 상태 컬럼만 갱신해 익명화와 + * before/after_withdraw 훅을 통째로 건너뛰었다. 본인 탈퇴와 결과가 같아야 한다. + * + * @scenario entry_point=admin_update_status, entry_point=admin_bulk_status, target_account=admin + * + * @effects admin_status_change_runs_full_withdrawal, admin_status_change_fires_withdraw_hooks, bulk_status_withdraw_delegates_per_user, bulk_status_reports_failed_count, admin_account_withdraw_blocked_consistently + */ +class AdminWithdrawPathUnificationTest extends TestCase +{ + use RefreshDatabase; + + /** + * W8: 단건 상태 변경으로 탈퇴시켜도 익명화와 훅이 동일하게 수행된다. + */ + public function test_update_user_with_withdrawn_status_runs_full_withdrawal(): void + { + $user = User::factory()->create([ + 'email' => 'g7fix112.w8@example.com', + 'nickname' => '닉네임', + ]); + $user->createToken('t'); + + $fired = $this->captureWithdrawHooks(); + + app(UserService::class)->updateUser($user, [ + 'status' => UserStatus::Withdrawn->value, + ]); + + $user->refresh(); + + $this->assertSame(UserStatus::Withdrawn->value, $user->status); + $this->assertNotNull($user->withdrawn_at); + $this->assertStringContainsString('_deleted_', $user->email); + $this->assertStringEndsWith('_탈퇴', $user->nickname); + $this->assertSame(0, $user->tokens()->count()); + + $this->assertTrue($fired['before'], 'before_withdraw 훅이 발화해야 합니다.'); + $this->assertTrue($fired['after'], 'after_withdraw 훅이 발화해야 합니다.'); + } + + /** + * W8: 같은 요청의 다른 필드 갱신도 함께 반영된다. + */ + public function test_update_user_with_withdrawn_status_still_applies_other_fields(): void + { + $user = User::factory()->create(['email' => 'g7fix112.w8b@example.com']); + + app(UserService::class)->updateUser($user, [ + 'status' => UserStatus::Withdrawn->value, + 'admin_memo' => '관리자 처리', + ]); + + $user->refresh(); + + $this->assertSame('관리자 처리', $user->admin_memo); + $this->assertSame(UserStatus::Withdrawn->value, $user->status); + } + + /** + * 탈퇴가 실패하면 같은 요청의 프로필 갱신도 함께 되돌아간다. + * + * 프로필 갱신과 탈퇴를 따로 커밋하면, 탈퇴가 실패했을 때 이름·메모 변경만 남아 + * "전부 성공하거나 전부 취소" 가 이 경로에서만 깨진다. 익명화 결과와 같은 이메일을 + * 미리 점유해 마지막 쓰기(withdraw)를 실패시킨다. + * + * @effects admin_status_change_is_atomic_with_profile_update + */ + public function test_failed_withdrawal_rolls_back_the_profile_update(): void + { + $user = User::factory()->create([ + 'email' => 'g7fix112.w8c@example.com', + 'name' => '원래 이름', + ]); + + // withdraw() 가 만들 이메일을 미리 점유 → unique 충돌로 마지막 쓰기 실패 + User::factory()->create([ + 'email' => 'g7fix112.w8c@example.com_deleted_'.now()->format('Ymd').'_'.$user->id, + ]); + + try { + app(UserService::class)->updateUser($user, [ + 'status' => UserStatus::Withdrawn->value, + 'name' => '바뀐 이름', + ]); + $this->fail('탈퇴 실패 시 예외가 전파되어야 합니다.'); + } catch (ValidationException $e) { + // 기대한 실패 + } + + $user->refresh(); + + $this->assertSame('원래 이름', $user->name, '탈퇴가 실패했는데 이름 변경이 남았습니다.'); + $this->assertNotSame(UserStatus::Withdrawn->value, $user->status); + } + + /** + * W9: 일괄 상태 변경도 건별 정식 탈퇴로 처리되고, 차단 대상은 실패 건수로 보고된다. + */ + public function test_bulk_status_withdrawn_delegates_per_user_and_reports_failures(): void + { + $normal = User::factory()->create(['email' => 'g7fix112.w9a@example.com']); + $admin = $this->createAdminUser('g7fix112.w9b@example.com'); + + $result = app(UserService::class)->bulkUpdateStatus( + [$normal->uuid, $admin->uuid], + UserStatus::Withdrawn->value + ); + + $this->assertSame(1, $result['updated_count']); + $this->assertSame(1, $result['failed_count']); + + $normal->refresh(); + $admin->refresh(); + + $this->assertStringContainsString('_deleted_', $normal->email); + $this->assertSame('g7fix112.w9b@example.com', $admin->email, '관리자는 변경되지 않아야 합니다.'); + $this->assertNull($admin->withdrawn_at); + + // 건수만으로는 화면이 "왜 안 됐는지" 를 말할 수 없다 — 사유가 응답에 실려야 한다. + $this->assertNotEmpty($result['failed_reasons'] ?? [], '실패 사유가 응답에 없습니다.'); + $this->assertSame( + [__('user.withdraw_admin_forbidden')], + $result['failed_reasons'], + '관리자 차단 사유가 그대로 전달되어야 합니다.' + ); + } + + /** + * W9-b: 전건이 차단되어도 200 으로 나가므로, 응답만으로 "아무 일도 없었음" 이 구분되어야 한다. + * + * 이 구분이 없으면 화면은 성공 안내를 띄우고 운영자는 처리된 줄 안다 (실제 제보 경로). + */ + public function test_bulk_status_withdrawn_reports_zero_updated_when_every_target_is_blocked(): void + { + $admin = $this->createAdminUser('g7fix112.w9c@example.com'); + + $result = app(UserService::class)->bulkUpdateStatus( + [$admin->uuid], + UserStatus::Withdrawn->value + ); + + $this->assertSame(0, $result['updated_count']); + $this->assertSame(1, $result['failed_count']); + $this->assertSame([__('user.withdraw_admin_forbidden')], $result['failed_reasons']); + + $admin->refresh(); + $this->assertSame('g7fix112.w9c@example.com', $admin->email); + $this->assertSame(UserStatus::Active->value, $admin->status); + } + + /** + * W9-c: 탈퇴가 아닌 일괄 상태 변경도 같은 응답 형태를 유지한다. + * + * 형태가 갈리면 화면 표현식이 한쪽 경로에서만 undefined 를 만난다. + */ + public function test_bulk_status_response_shape_is_consistent_for_non_withdraw_statuses(): void + { + $user = User::factory()->create(['email' => 'g7fix112.w9d@example.com']); + + $result = app(UserService::class)->bulkUpdateStatus( + [$user->uuid], + UserStatus::Inactive->value + ); + + $this->assertArrayHasKey('updated_count', $result); + $this->assertArrayHasKey('failed_count', $result); + $this->assertArrayHasKey('failed_reasons', $result); + $this->assertSame([], $result['failed_reasons']); + } + + /** + * W9-d: 실패 사유에는 화면 문구 조립을 깨는 문자가 들어가지 않는다. + * + * 사유는 `$t:key|reason=...` 파라미터로 전달되는데, 파서가 `|` `&` `=` 를 + * 구분자로 읽으므로 그 문자가 섞이면 안내가 조용히 잘린다. 알 수 없는 예외의 + * 원문 메시지를 그대로 싣지 않는 것이 이 불변식을 지키는 방법이다. + */ + public function test_bulk_withdraw_reasons_are_safe_for_message_parameters(): void + { + $admin = $this->createAdminUser('g7fix112.w9e@example.com'); + + $result = app(UserService::class)->bulkUpdateStatus( + [$admin->uuid, 'ffffffff-0000-0000-0000-000000000000'], + UserStatus::Withdrawn->value + ); + + $this->assertNotEmpty($result['failed_reasons']); + + foreach ($result['failed_reasons'] as $reason) { + $this->assertDoesNotMatchRegularExpression( + '/[|&=]/u', + $reason, + "실패 사유에 파라미터 구분자가 들어 있습니다: {$reason}" + ); + } + } + + /** + * W9: 탈퇴 외 상태의 일괄 변경은 종전 동작을 유지한다. + */ + public function test_bulk_status_other_status_keeps_previous_behaviour(): void + { + $user = User::factory()->create(['email' => 'g7fix112.w9c@example.com']); + + $result = app(UserService::class)->bulkUpdateStatus( + [$user->uuid], + UserStatus::Inactive->value + ); + + $this->assertSame(1, $result['updated_count']); + + $user->refresh(); + $this->assertSame(UserStatus::Inactive->value, $user->status); + $this->assertSame('g7fix112.w9c@example.com', $user->email); + } + + /** + * W10: 관리자를 상태 변경으로 탈퇴시키려는 시도는 일관되게 차단된다. + */ + public function test_update_user_cannot_withdraw_admin_via_status(): void + { + $admin = $this->createAdminUser('g7fix112.w10@example.com'); + + $this->expectException(ValidationException::class); + + try { + app(UserService::class)->updateUser($admin, [ + 'status' => UserStatus::Withdrawn->value, + 'admin_memo' => '차단되어야 함', + ]); + } finally { + $admin->refresh(); + + // 차단 대상이면 다른 필드도 저장되지 않아야 한다. + $this->assertSame('g7fix112.w10@example.com', $admin->email); + $this->assertNotSame(UserStatus::Withdrawn->value, $admin->status); + $this->assertNull($admin->admin_memo); + } + } + + /** + * 탈퇴 훅 발화 여부를 수집합니다. + * + * @return \ArrayObject<string, bool> 훅 발화 기록 (참조로 갱신됨) + */ + private function captureWithdrawHooks(): \ArrayObject + { + // 배열은 값 타입이라 반환하면 사본이 된다 — 참조 의미를 갖는 객체로 수집한다. + $fired = new \ArrayObject(['before' => false, 'after' => false]); + + HookManager::addAction('core.user.before_withdraw', function () use ($fired) { + $fired['before'] = true; + }, 10); + + HookManager::addAction('core.user.after_withdraw', function () use ($fired) { + $fired['after'] = true; + }, 10); + + return $fired; + } + + /** + * 관리자 역할을 가진 사용자를 생성합니다. + * + * @param string $email 이메일 + * @return User 생성된 사용자 + */ + private function createAdminUser(string $email): User + { + $role = Role::factory()->create(['identifier' => 'admin_'.uniqid()]); + $permission = Permission::factory()->create(['type' => PermissionType::Admin]); + $role->permissions()->attach($permission); + + $user = User::factory()->create(['email' => $email]); + $user->roles()->attach($role); + + return $user->fresh(); + } +} diff --git a/tests/Feature/Services/MultiStepWriteAtomicityTest.php b/tests/Feature/Services/MultiStepWriteAtomicityTest.php new file mode 100644 index 00000000..b924f7a2 --- /dev/null +++ b/tests/Feature/Services/MultiStepWriteAtomicityTest.php @@ -0,0 +1,320 @@ +<?php + +namespace Tests\Feature\Services; + +use App\Contracts\Repositories\AttachmentRepositoryInterface; +use App\Contracts\Repositories\MenuRepositoryInterface; +use App\Contracts\Repositories\PasswordResetTokenRepositoryInterface; +use App\Contracts\Repositories\RoleRepositoryInterface; +use App\Contracts\Repositories\ScheduleRepositoryInterface; +use App\Contracts\Repositories\UserRepositoryInterface; +use App\Enums\AttachmentSourceType; +use App\Enums\PermissionType; +use App\Models\Attachment; +use App\Models\Menu; +use App\Models\PasswordResetToken; +use App\Models\Permission; +use App\Models\Role; +use App\Models\Schedule; +use App\Models\ScheduleHistory; +use App\Models\User; +use App\Services\AttachmentService; +use App\Services\AuthService; +use App\Services\MenuService; +use App\Services\RoleService; +use App\Services\ScheduleService; +use App\Services\UserService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Storage; +use Mockery; +use PHPUnit\Framework\AssertionFailedError; +use RuntimeException; +use Tests\TestCase; + +/** + * 다단계 쓰기 원자성 회귀 테스트 (공개이슈 #112 동종 전수 해소분) + * + * 표준 패턴: 마지막 쓰기를 담당하는 Repository 를 컨테이너 목킹으로 실패시키고, + * 선행 단계가 원상 복구되는지 단언합니다. + * + * @scenario failure_injection=last_write_fails + * + * @effects withdraw_is_atomic_all_or_nothing, avatar_file_removed_only_after_commit + */ +class MultiStepWriteAtomicityTest extends TestCase +{ + use RefreshDatabase; + + /** + * deleteUser: 마지막 delete 실패 시 역할·동의·토큰·아바타 파일이 원상 복구된다. + */ + public function test_delete_user_rolls_back_roles_and_tokens(): void + { + Storage::fake('attachments'); + + $user = User::factory()->create(['email' => 'atomic.delete@example.com']); + $role = Role::factory()->create(['identifier' => 'member_'.uniqid()]); + $user->roles()->attach($role); + $user->createToken('t'); + $user->consents()->create([ + 'consent_type' => 'terms', + 'is_agreed' => true, + 'agreed_at' => now(), + ]); + + // 아바타는 커밋 후에만 지워야 한다 — 롤백된 계정의 파일이 사라지면 복구 불가다. + $avatar = Attachment::factory()->create([ + 'attachmentable_type' => User::class, + 'attachmentable_id' => $user->id, + 'collection' => 'avatar', + 'disk' => 'attachments', + 'path' => 'attachments/avatars/atomic-delete.jpg', + 'source_type' => AttachmentSourceType::Core, + ]); + Storage::disk('attachments')->put('attachments/avatars/atomic-delete.jpg', 'fake'); + + $this->mockRepositoryFailure(UserRepositoryInterface::class, 'delete'); + + $this->expectRollbackFailure(fn () => app(UserService::class)->deleteUser($user)); + + $this->assertDatabaseHas('users', ['id' => $user->id]); + $this->assertSame(1, $user->roles()->count(), '역할 연결이 복구되어야 합니다.'); + $this->assertSame(1, $user->tokens()->count(), '토큰이 복구되어야 합니다.'); + $this->assertSame(1, $user->consents()->count(), '동의 이력이 복구되어야 합니다.'); + + Storage::disk('attachments')->assertExists('attachments/avatars/atomic-delete.jpg'); + $this->assertDatabaseHas('attachments', ['id' => $avatar->id]); + } + + /** + * deleteRole: 마지막 delete 실패 시 사용자·권한 연결이 원상 복구된다. + */ + public function test_delete_role_rolls_back_relations(): void + { + $role = Role::factory()->create(['identifier' => 'member_'.uniqid()]); + $user = User::factory()->create(['email' => 'atomic.role@example.com']); + $user->roles()->attach($role); + + $this->mockRepositoryFailure(RoleRepositoryInterface::class, 'delete'); + + $this->expectRollbackFailure(fn () => app(RoleService::class)->deleteRole($role)); + + $this->assertDatabaseHas('roles', ['id' => $role->id]); + $this->assertSame(1, $role->users()->count(), '역할 보유자가 복구되어야 합니다.'); + } + + /** + * register: 중간 실패 시 계정이 남지 않는다 (이메일 점유 유령 계정 차단). + */ + public function test_register_rolls_back_user_on_failure(): void + { + $this->mockRepositoryFailure(RoleRepositoryInterface::class, 'findByIdentifier'); + + $this->expectRollbackFailure(fn () => app(AuthService::class)->register([ + 'name' => '테스트', + 'email' => 'atomic.register@example.com', + 'password' => 'Password!234', + ])); + + $this->assertDatabaseMissing('users', ['email' => 'atomic.register@example.com']); + } + + /** + * resetPassword: 토큰 삭제 실패 시 비밀번호도 원상 복구된다 (토큰 재사용 차단). + */ + public function test_reset_password_rolls_back_password_when_token_delete_fails(): void + { + $user = User::factory()->create([ + 'email' => 'atomic.reset@example.com', + 'password' => Hash::make('OldPassword!1'), + ]); + $originalHash = $user->password; + + $token = 'reset-token-value'; + $hashedToken = Hash::make($token); + + app(PasswordResetTokenRepositoryInterface::class) + ->updateOrCreateByEmail($user->email, [ + 'token' => $hashedToken, + 'created_at' => now(), + ]); + + // 트랜잭션의 **마지막** 쓰기는 토큰 삭제다. 1단계(비밀번호 갱신)를 실패시키면 + // 트랜잭션이 없어도 통과하므로 회귀를 검출하지 못한다. + $failingRecord = new class extends PasswordResetToken + { + public function delete(): bool + { + throw new RuntimeException('마지막 쓰기 실패'); + } + }; + $failingRecord->forceFill([ + 'email' => $user->email, + 'token' => $hashedToken, + 'created_at' => now(), + ])->syncOriginal(); + + $tokenRepository = Mockery::mock(app(PasswordResetTokenRepositoryInterface::class)); + $tokenRepository->shouldReceive('findByEmail')->andReturn($failingRecord); + $this->app->instance(PasswordResetTokenRepositoryInterface::class, $tokenRepository); + + $this->expectRollbackFailure( + fn () => app(AuthService::class)->resetPassword($token, $user->email, 'NewPassword!1') + ); + + $user->refresh(); + $this->assertSame($originalHash, $user->password); + $this->assertDatabaseHas('password_reset_tokens', ['email' => $user->email]); + } + + /** + * AttachmentService::delete — DB 삭제 실패 시 파일이 남는다 (dangling 없음). + */ + public function test_attachment_delete_keeps_file_when_db_delete_fails(): void + { + Storage::fake('attachments'); + + $attachment = Attachment::factory()->create([ + 'disk' => 'attachments', + 'path' => 'attachments/atomic.jpg', + 'source_type' => AttachmentSourceType::Core, + ]); + Storage::disk('attachments')->put('attachments/atomic.jpg', 'fake'); + + $this->mockRepositoryFailure(AttachmentRepositoryInterface::class, 'forceDelete'); + + $this->expectRollbackFailure(fn () => app(AttachmentService::class)->delete($attachment->id)); + + Storage::disk('attachments')->assertExists('attachments/atomic.jpg'); + $this->assertDatabaseHas('attachments', ['id' => $attachment->id]); + } + + /** + * ScheduleService::delete — 스케줄 삭제 실패 시 실행 이력이 복구된다. + */ + public function test_schedule_delete_rolls_back_histories(): void + { + $schedule = Schedule::create([ + 'name' => '원자성 테스트', + 'type' => 'artisan', + 'command' => 'inspire', + 'expression' => '0 4 * * *', + 'frequency' => 'daily', + 'is_active' => true, + ]); + ScheduleHistory::create([ + 'schedule_id' => $schedule->id, + 'started_at' => now(), + 'status' => 'success', + ]); + + $this->mockRepositoryFailure(ScheduleRepositoryInterface::class, 'delete'); + + $this->expectRollbackFailure(fn () => app(ScheduleService::class)->delete($schedule)); + + $this->assertDatabaseHas('schedules', ['id' => $schedule->id]); + $this->assertSame(1, $schedule->histories()->count(), '실행 이력이 복구되어야 합니다.'); + } + + /** + * updateUser — 역할 동기화 실패 시 프로필 갱신·토큰 정리가 원상 복구된다. + */ + public function test_update_user_rolls_back_profile_and_tokens(): void + { + $user = User::factory()->create([ + 'email' => 'atomic.update@example.com', + 'name' => '원래 이름', + ]); + $user->createToken('t'); + + // 역할 동기화는 `core.permissions.update` 권한이 있어야 수행된다 — 권한 없이 호출하면 + // role_ids 가 통째로 무시되어 마지막 단계 자체가 실행되지 않는다. + $this->actingAs($this->createUserWithPermission('core.permissions.update')); + + // 마지막 단계(역할 동기화)를 실패시킨다 — 존재하지 않는 역할 ID 는 외래키 제약에 + // 걸린다. 그 전에 프로필 갱신과 토큰 삭제가 이미 수행된 상태다. + $missingRoleId = (int) (Role::max('id') ?? 0) + 1000; + + $this->expectRollbackFailure(fn () => app(UserService::class)->updateUser($user, [ + 'name' => '바뀐 이름', + 'status' => 'inactive', + 'role_ids' => [$missingRoleId], + ])); + + $user->refresh(); + + $this->assertSame('원래 이름', $user->name, '프로필 갱신이 복구되어야 합니다.'); + $this->assertSame(1, $user->tokens()->count(), '토큰이 복구되어야 합니다.'); + } + + /** + * MenuService::deleteMenu — 메뉴 삭제 실패 시 역할 연결이 복구된다. + */ + public function test_menu_delete_rolls_back_role_links(): void + { + $menu = Menu::factory()->create(); + $role = Role::factory()->create(['identifier' => 'member_'.uniqid()]); + $menu->roles()->attach($role); + + $this->mockRepositoryFailure(MenuRepositoryInterface::class, 'delete'); + + $this->expectRollbackFailure(fn () => app(MenuService::class)->deleteMenu($menu)); + + $this->assertDatabaseHas('menus', ['id' => $menu->id]); + $this->assertSame(1, $menu->roles()->count(), '역할 연결이 복구되어야 합니다.'); + } + + /** + * 지정한 권한을 가진 사용자를 만듭니다 (역할 경유). + * + * @param string $identifier 권한 식별자 + * @return User 권한 보유 사용자 + */ + private function createUserWithPermission(string $identifier): User + { + $permission = Permission::factory()->create([ + 'identifier' => $identifier, + 'type' => PermissionType::Admin, + ]); + + $role = Role::factory()->create(['identifier' => 'perm_'.uniqid()]); + $role->permissions()->attach($permission); + + $actor = User::factory()->create(['email' => 'atomic.actor.'.uniqid().'@example.com']); + $actor->roles()->attach($role); + + return $actor; + } + + /** + * 지정한 Repository 메서드만 실패시키고 나머지는 실제 구현에 위임합니다. + * + * @param class-string $interface Repository 인터페이스 + * @param string $method 실패시킬 메서드 + */ + private function mockRepositoryFailure(string $interface, string $method): void + { + // 실제 인스턴스를 감싸는 프록시 부분 목 — 지정 메서드만 실패시키고 + // 나머지 호출은 실제 구현으로 그대로 흘려보낸다. + $mock = Mockery::mock(app($interface)); + $mock->shouldReceive($method)->andThrow(new RuntimeException('마지막 쓰기 실패')); + + $this->app->instance($interface, $mock); + } + + /** + * 롤백을 유발하는 호출이 예외로 끝나는지 확인합니다. + * + * @param callable $operation 실행할 동작 + */ + private function expectRollbackFailure(callable $operation): void + { + try { + $operation(); + $this->fail('마지막 쓰기 실패로 예외가 발생해야 합니다.'); + } catch (\Throwable $e) { + $this->assertNotInstanceOf(AssertionFailedError::class, $e); + } + } +} diff --git a/tests/Feature/Services/SettingsServiceSiteLogoTest.php b/tests/Feature/Services/SettingsServiceSiteLogoTest.php new file mode 100644 index 00000000..581b0fda --- /dev/null +++ b/tests/Feature/Services/SettingsServiceSiteLogoTest.php @@ -0,0 +1,189 @@ +<?php + +namespace Tests\Feature\Services; + +use App\Contracts\Repositories\ConfigRepositoryInterface; +use App\Enums\AttachmentSourceType; +use App\Extension\Storage\CoreStorageDriver; +use App\Models\Attachment; +use App\Services\SettingsService; +use Illuminate\Foundation\Testing\RefreshDatabase; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * 사이트 로고 교체 시 구 로고가 재편입되지 않는지 검증 (회귀) + * + * 저장할 목록은 **이번 요청이 제출한 것**이 정한다. 컬렉션 전체를 다시 수집하면 저장에 + * 실패했거나 작성 중 이탈해 남은 미참조 첨부까지 설정에 되살아나, 운영자가 올린 적 없는 + * 로고가 화면에 다시 나타난다(브라우저 실측 — 신규 1건만 제출했는데 저장 결과가 2건). + */ +class SettingsServiceSiteLogoTest extends TestCase +{ + use RefreshDatabase; + + /** + * site_logo 컬렉션 첨부를 파일과 함께 만듭니다. + * + * @return Attachment 생성된 첨부 + */ + private function makeLogo(): Attachment + { + $filename = uniqid('logo-').'.png'; + $path = "2026/08/14/{$filename}"; + $disk = config('attachment.disk'); + + (new CoreStorageDriver($disk))->put('', $path, 'bytes'); + + return Attachment::create([ + 'attachmentable_type' => null, + 'attachmentable_id' => null, + 'source_type' => AttachmentSourceType::Core, + 'source_identifier' => null, + 'original_filename' => $filename, + 'stored_filename' => $filename, + 'disk' => $disk, + 'path' => $path, + 'mime_type' => 'image/png', + 'size' => 5, + 'collection' => 'site_logo', + 'order' => 0, + ]); + } + + /** + * @effects site_logo_replacement_drops_previous + */ + #[Test] + public function replacing_the_logo_removes_the_previous_one_instead_of_re_collecting_it(): void + { + $service = app(SettingsService::class); + $config = app(ConfigRepositoryInterface::class); + + // 1차 저장 — 첫 로고가 설정에 실린다. + $first = $this->makeLogo(); + $service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$first->id]]]); + + $this->assertSame([$first->id], $config->getCategory('general')['site_logo']); + + // 2차 저장 — 운영자가 첫 로고를 빼고 새 로고를 올린 상태. + // 화면은 업로드를 끝낸 뒤 저장을 보내므로 새 로고 id 가 제출값에 실린다. + $second = $this->makeLogo(); + $service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$second->id]]]); + + $saved = $config->getCategory('general')['site_logo']; + + $this->assertSame([$second->id], $saved, '설정에는 최신 로고만 남아야 한다.'); + $this->assertDatabaseMissing('attachments', ['id' => $first->id]); + } + + /** + * 저장에 실패했거나 이탈로 남은 미참조 첨부가 설정에 되살아나면 안 된다. + * + * @effects site_logo_unreferenced_collection_member_not_re_absorbed + */ + #[Test] + public function an_unreferenced_collection_member_is_not_re_absorbed(): void + { + $service = app(SettingsService::class); + $config = app(ConfigRepositoryInterface::class); + + // 설정에 실린 적 없는 첨부 (직전 저장 실패로 남은 상태) + $orphan = $this->makeLogo(); + + // 운영자는 새 로고 1건만 올려 저장한다. + $fresh = $this->makeLogo(); + $service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$fresh->id]]]); + + $this->assertSame( + [$fresh->id], + $config->getCategory('general')['site_logo'], + '제출하지 않은 미참조 첨부가 설정에 편입되면 안 된다.' + ); + $this->assertDatabaseHas('attachments', ['id' => $orphan->id]); + } + + /** + * 제출값에 이미 삭제된 첨부 id 가 섞여 있어도 그 id 는 저장되지 않아야 한다. + * + * @effects site_logo_stale_submitted_id_filtered + */ + #[Test] + public function a_submitted_id_that_no_longer_exists_is_filtered_out(): void + { + $service = app(SettingsService::class); + $config = app(ConfigRepositoryInterface::class); + + $logo = $this->makeLogo(); + $service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$logo->id, 999999]]]); + + $this->assertSame([$logo->id], $config->getCategory('general')['site_logo']); + } + + /** + * site_logo 를 제출하지 않은 저장(다른 필드만 변경)은 로고를 건드리지 않아야 한다. + * + * @effects site_logo_untouched_when_not_submitted + */ + #[Test] + public function saving_other_fields_does_not_remove_the_logo(): void + { + $service = app(SettingsService::class); + $config = app(ConfigRepositoryInterface::class); + + $logo = $this->makeLogo(); + $service->saveSettings(['_tab' => 'general', 'general' => ['site_logo' => [$logo->id]]]); + + $service->saveSettings(['_tab' => 'general', 'general' => ['site_name' => '테스트 사이트']]); + + $this->assertSame([$logo->id], $config->getCategory('general')['site_logo']); + $this->assertDatabaseHas('attachments', ['id' => $logo->id]); + } + + /** + * 저장이 실패하면 뺀 로고의 파일도 남아 있어야 한다 (회귀). + * + * 파기를 저장보다 먼저 하면, 저장이 실패했을 때 설정에는 구 로고 id 가 그대로 남는데 + * 파일은 이미 사라져 로고가 깨진다. 파기는 저장이 확정된 뒤에만 수행해야 한다. + * + * @effects site_logo_failed_save_keeps_removed_attachment + */ + #[Test] + public function a_failed_save_does_not_destroy_the_removed_logo(): void + { + $config = app(ConfigRepositoryInterface::class); + + // 로고가 저장된 상태를 만든다. + $logo = $this->makeLogo(); + app(SettingsService::class)->saveSettings([ + '_tab' => 'general', + 'general' => ['site_logo' => [$logo->id]], + ]); + + $this->assertSame([$logo->id], $config->getCategory('general')['site_logo']); + + // 저장 단계만 실패하도록 만든다 (조회는 실제 동작 유지). + $failing = \Mockery::mock($config); + $failing->shouldReceive('saveCategory')->andReturnFalse(); + $this->app->instance(ConfigRepositoryInterface::class, $failing); + + // 운영자가 로고를 빼고 저장 → 저장 실패 + app(SettingsService::class)->saveSettings([ + '_tab' => 'general', + 'general' => ['site_logo' => []], + ]); + + $this->app->instance(ConfigRepositoryInterface::class, $config); + + $this->assertDatabaseHas('attachments', ['id' => $logo->id]); + $this->assertTrue( + (new CoreStorageDriver($logo->disk))->exists('', $logo->path), + '저장이 실패했으면 파일도 남아 있어야 한다.' + ); + $this->assertSame( + [$logo->id], + $config->getCategory('general')['site_logo'], + '저장이 실패했으므로 설정도 그대로여야 한다.' + ); + } +} diff --git a/tests/Feature/Services/TemplateServiceInstallationTest.php b/tests/Feature/Services/TemplateServiceInstallationTest.php index b1c62a57..0498d10b 100644 --- a/tests/Feature/Services/TemplateServiceInstallationTest.php +++ b/tests/Feature/Services/TemplateServiceInstallationTest.php @@ -36,10 +36,13 @@ class TemplateServiceInstallationTest extends TestCase // TemplateManager Mock 생성 $this->templateManager = Mockery::mock(TemplateManagerInterface::class); - // loadTemplates() 호출 허용 (생성자에서 호출됨) + // loadTemplates()/ensureLoaded() 호출 허용 (생성자에서 ensureLoaded 호출됨) $this->templateManager->shouldReceive('loadTemplates') ->zeroOrMoreTimes() ->andReturnNull(); + $this->templateManager->shouldReceive('ensureLoaded') + ->zeroOrMoreTimes() + ->andReturnNull(); // ModuleManager Mock 생성 $this->moduleManager = Mockery::mock(ModuleManagerInterface::class); diff --git a/tests/Feature/Services/TemplateServiceLanguageMergeTest.php b/tests/Feature/Services/TemplateServiceLanguageMergeTest.php index 500fbeea..b1603c8b 100644 --- a/tests/Feature/Services/TemplateServiceLanguageMergeTest.php +++ b/tests/Feature/Services/TemplateServiceLanguageMergeTest.php @@ -52,6 +52,9 @@ class TemplateServiceLanguageMergeTest extends TestCase $this->templateManager->shouldReceive('loadTemplates') ->zeroOrMoreTimes() ->andReturnNull(); + $this->templateManager->shouldReceive('ensureLoaded') + ->zeroOrMoreTimes() + ->andReturnNull(); // ModuleManager Mock 생성 $this->moduleManager = Mockery::mock(ModuleManagerInterface::class); diff --git a/tests/Feature/Services/TemplateServiceRoutesMergeTest.php b/tests/Feature/Services/TemplateServiceRoutesMergeTest.php index 8a78c789..820dc55b 100644 --- a/tests/Feature/Services/TemplateServiceRoutesMergeTest.php +++ b/tests/Feature/Services/TemplateServiceRoutesMergeTest.php @@ -108,6 +108,9 @@ class TemplateServiceRoutesMergeTest extends TestCase $this->assertIsArray($result['data']['routes']); } + /** + * @effects code_editor_restores_layout_from_route_query + */ #[Test] public function get_layout_route_path_map_maps_layout_name_to_route_path(): void { @@ -514,4 +517,89 @@ class TemplateServiceRoutesMergeTest extends TestCase } } } + + /** + * 편집기 라우트 병합이 직전 호출의 열화 판정을 승계하지 않는지 확인합니다. + * + * 열화 플래그는 인스턴스 프로퍼티인데, 진입 시 리셋하는 것은 + * `getRoutesDataWithModules()` 뿐이었다. 서비스가 공유 인스턴스가 된 뒤로는 + * 편집기 경로가 한 번 열화를 만나면 그 판정이 인스턴스에 남아, 모듈 디렉토리가 + * 복구된 뒤의 병합까지 열화로 보고된다. 열화 응답은 캐시되지 않으므로 + * 편집기 라우트 트리가 매 요청 디스크 병합으로 떨어진다. + */ + #[Test] + public function editor_route_merge_does_not_inherit_previous_degraded_state(): void + { + Template::factory()->create([ + 'identifier' => 'sirsoft-admin_basic', + 'type' => 'admin', + 'status' => ExtensionStatus::Active->value, + ]); + + $this->templateManager->method('getTemplateInfo')->willReturn([ + 'identifier' => 'sirsoft-admin_basic', + 'type' => 'admin', + ]); + + $absentModule = $this->createMock(ModuleInterface::class); + $absentModule->method('getIdentifier')->willReturn('vendor-absent_module'); + $healthyModule = $this->createMock(ModuleInterface::class); + $healthyModule->method('getIdentifier')->willReturn('sirsoft-board'); + + // 한 번의 병합에서도 활성 모듈 목록은 여러 번 조회된다(라우트 로더 + base/모달 수집). + // 호출 횟수가 아니라 "지금 어떤 상태인가" 로 갈라야 테스트가 구현 세부에 묶이지 않는다. + $activeModules = ['vendor-absent_module' => $absentModule]; + $this->moduleManager->method('getActiveModules') + ->willReturnCallback(function () use (&$activeModules) { + return $activeModules; + }); + + // 1회차 = 업데이트 스왑 창(디렉토리 부재) + $this->templateService->getEditorRoutesDataWithModules('sirsoft-admin_basic'); + $this->assertTrue( + $this->templateService->lastRouteMergeWasDegraded(), + '편집기 경로가 활성 모듈 디렉토리 부재를 열화로 표시하지 않았습니다.' + ); + + // 2회차 = 디렉토리 복구 후 + $activeModules = ['sirsoft-board' => $healthyModule]; + $this->templateService->getEditorRoutesDataWithModules('sirsoft-admin_basic'); + + $this->assertFalse( + $this->templateService->lastRouteMergeWasDegraded(), + '편집기 라우트 병합이 직전 호출의 열화 판정을 승계했습니다 — 공유 인스턴스에 열화가 눌어붙습니다.' + ); + } + + /** + * 진입 리셋을 넣은 뒤에도 편집기 경로의 열화 검출이 살아 있는지 확인합니다 (과잉 리셋 방지). + */ + #[Test] + public function editor_route_merge_still_flags_missing_module_directory(): void + { + Template::factory()->create([ + 'identifier' => 'sirsoft-admin_basic', + 'type' => 'admin', + 'status' => ExtensionStatus::Active->value, + ]); + + $this->templateManager->method('getTemplateInfo')->willReturn([ + 'identifier' => 'sirsoft-admin_basic', + 'type' => 'admin', + ]); + + $absentModule = $this->createMock(ModuleInterface::class); + $absentModule->method('getIdentifier')->willReturn('vendor-absent_module'); + $this->moduleManager->method('getActiveModules')->willReturn([ + 'vendor-absent_module' => $absentModule, + ]); + + $result = $this->templateService->getEditorRoutesDataWithModules('sirsoft-admin_basic'); + + $this->assertTrue($result['success']); + $this->assertTrue( + $this->templateService->lastRouteMergeWasDegraded(), + '진입 리셋이 편집기 경로의 열화 검출까지 지웠습니다.' + ); + } } diff --git a/tests/Feature/Services/TemplateServiceTest.php b/tests/Feature/Services/TemplateServiceTest.php index fdc800e7..bb110caf 100644 --- a/tests/Feature/Services/TemplateServiceTest.php +++ b/tests/Feature/Services/TemplateServiceTest.php @@ -40,6 +40,7 @@ class TemplateServiceTest extends TestCase // loadTemplates() 호출 허용 (생성자에서 호출됨) $this->templateManager->shouldReceive('loadTemplates')->andReturn(null); + $this->templateManager->shouldReceive('ensureLoaded')->andReturn(null); // ModuleManager Mock 생성 $this->moduleManager = Mockery::mock(ModuleManagerInterface::class); diff --git a/tests/Feature/Settings/IdentityFrontendExposureTest.php b/tests/Feature/Settings/IdentityFrontendExposureTest.php new file mode 100644 index 00000000..eb2ae518 --- /dev/null +++ b/tests/Feature/Settings/IdentityFrontendExposureTest.php @@ -0,0 +1,42 @@ +<?php + +namespace Tests\Feature\Settings; + +use App\Services\SettingsService; +use Tests\TestCase; + +/** + * 본인인증 provider 저장값의 공개 SSR 노출 정리 (A6b) + * + * `identity` 카테고리는 `expose: true` 이고 `default_provider` 에 필드 단위 차단이 없어, + * 저장값이 전 공개 페이지의 SSR 부트스트랩과 봇 SSR 에 그대로 실렸다. 소비처가 없는데도 + * 제거된 플러그인의 provider ID 가 계속 노출되는 상태다. + */ +class IdentityFrontendExposureTest extends TestCase +{ + /** + * default_provider 는 프론트엔드 설정에 노출되지 않는다. (실패-먼저) + */ + public function test_default_provider_is_not_exposed_to_frontend(): void + { + $frontend = app(SettingsService::class)->getFrontendSettings(); + + $this->assertArrayNotHasKey( + 'default_provider', + $frontend['identity'] ?? [], + '본인인증 provider 저장값이 공개 프론트엔드 설정에 노출되었습니다.' + ); + } + + /** + * identity 카테고리의 나머지 필드도 그대로 미노출을 유지한다. (비회귀 pin) + */ + public function test_other_identity_fields_stay_hidden(): void + { + $identity = app(SettingsService::class)->getFrontendSettings()['identity'] ?? []; + + foreach (['purpose_providers', 'challenge_ttl_minutes', 'max_attempts'] as $field) { + $this->assertArrayNotHasKey($field, $identity); + } + } +} diff --git a/tests/Feature/Settings/SearchDriverFallbackTest.php b/tests/Feature/Settings/SearchDriverFallbackTest.php new file mode 100644 index 00000000..f4f44086 --- /dev/null +++ b/tests/Feature/Settings/SearchDriverFallbackTest.php @@ -0,0 +1,186 @@ +<?php + +namespace Tests\Feature\Settings; + +use App\Extension\HookManager; +use App\Providers\CoreServiceProvider; +use App\Repositories\JsonConfigRepository; +use App\Services\DriverRegistryService; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; +use Tests\TestCase; + +/** + * 검색엔진 드라이버의 폴백 가드 편입 (A5b) + * + * 드라이버 폴백 가드는 `DriverRegistryService` 의 카테고리 레지스트리를 순회한다. + * `search` 만 그 레지스트리에 없어 가드 대상 밖이었다 — 검색엔진 플러그인을 저장한 뒤 + * 그 플러그인을 제거하면 `scout.driver` 가 죽은 값으로 남고, 공개 검색이 500 으로 떨어진다. + * (다른 카테고리는 기본 드라이버로 폴백해 사이트가 살아남는다) + */ +class SearchDriverFallbackTest extends TestCase +{ + private DriverRegistryService $registry; + + protected function setUp(): void + { + parent::setUp(); + + $this->registry = app(DriverRegistryService::class); + } + + /** + * search 카테고리가 드라이버 레지스트리에 등재되어 있다. (실패-먼저) + * + * @scenario engine_source=core + * + * @effects search_category_registered_in_driver_registry + */ + public function test_search_category_is_registered(): void + { + $this->assertContains( + 'search', + $this->registry->getCategories(), + '검색엔진이 드라이버 카테고리에 없어 폴백 가드 대상에서 빠집니다.' + ); + } + + /** + * 코어 검색엔진과 기본 폴백 드라이버가 선언되어 있다. (실패-먼저) + * + * @scenario engine_source=core + * + * @effects core_search_engine_declared_as_core_driver, search_default_fallback_is_core_engine + */ + public function test_core_search_driver_and_default_are_declared(): void + { + $this->assertTrue($this->registry->isCoreDriver('search', 'mysql-fulltext')); + $this->assertSame('mysql-fulltext', $this->registry->getDefaultDriver('search')); + } + + /** + * 설정 키와 Config 키가 실제 적용 지점과 일치한다. (실패-먼저) + * + * @scenario engine_source=core + * + * @effects search_settings_key_matches_saved_field, search_config_key_matches_apply_site + */ + public function test_settings_and_config_keys_match_apply_site(): void + { + $this->assertSame( + ['category' => 'drivers', 'key' => 'search_engine_driver'], + $this->registry->getSettingsKey('search') + ); + $this->assertSame('scout.driver', $this->registry->getConfigKey('search')); + } + + /** + * 플러그인이 등록한 검색엔진은 사용 가능으로 판정된다. (실패-먼저) + * + * 검색엔진은 전용 훅(`core.search.engine_drivers`)으로 등록되므로, 그 훅을 함께 읽지 + * 않으면 살아 있는 플러그인 엔진까지 "사용 불가" 로 오판해 폴백된다. + * + * @scenario engine_source=plugin_scout_hook + * + * @effects scout_hook_registered_engine_is_available + */ + public function test_plugin_registered_engine_is_available(): void + { + HookManager::addFilter('core.search.engine_drivers', function (array $drivers) { + $drivers['meilisearch'] = \stdClass::class; + + return $drivers; + }); + + $this->assertTrue( + $this->registry->isDriverAvailable('search', 'meilisearch'), + '플러그인이 등록한 검색엔진이 사용 불가로 판정되었습니다.' + ); + + $ids = array_map(fn ($d) => $d['id'], $this->registry->getAvailableDrivers('search')); + $this->assertContains('mysql-fulltext', $ids); + $this->assertContains('meilisearch', $ids); + } + + /** + * 제거된 플러그인의 검색엔진은 사용 불가로 판정된다. (실패-먼저) + * + * @scenario engine_source=removed + * + * @effects removed_plugin_engine_is_unavailable + */ + public function test_removed_plugin_engine_is_unavailable(): void + { + $this->assertFalse( + $this->registry->isDriverAvailable('search', 'ghost_engine'), + '등록되지 않은 검색엔진이 사용 가능으로 판정되었습니다.' + ); + } + + /** + * 일반 드라이버 훅으로 등록한 검색엔진도 함께 인식된다. (하위호환) + * + * @scenario engine_source=plugin_generic_hook + * + * @effects generic_hook_registered_engine_is_available + */ + public function test_generic_driver_hook_still_recognized(): void + { + HookManager::addFilter('core.settings.available_search_drivers', function (array $drivers) { + $drivers[] = ['id' => 'legacy_engine', 'label' => ['ko' => 'Legacy', 'en' => 'Legacy']]; + + return $drivers; + }); + + $this->assertTrue($this->registry->isDriverAvailable('search', 'legacy_engine')); + } + + /** + * 죽은 검색엔진이 저장돼 있으면 폴백 가드가 실제로 코어 엔진으로 되돌린다. (실패-먼저) + * + * 앞의 케이스들은 레지스트리 판정층만 본다 — "search 가 등재됐다" 는 사실이지 + * "그래서 공개 검색이 살아남는다" 는 결과가 아니다. 이 케이스는 폴백 가드 + * (`CoreServiceProvider::applyExtensionDriverConfigs`)를 실제로 태워 + * `scout.driver` 가 되돌아가고 경고가 남는 것까지 확인한다. + * + * 로직을 테스트 안에서 재구현하지 않는다 — 재구현하면 프로덕션 코드가 바뀌어도 + * 테스트는 자기 사본을 검사하며 계속 통과한다. + * + * @scenario engine_source=removed + * + * @effects removed_plugin_engine_is_unavailable, search_config_key_matches_apply_site, search_default_fallback_is_core_engine + */ + public function test_dead_search_engine_falls_back_to_core_engine_at_apply_site(): void + { + Config::set('scout.driver', 'ghost_engine'); + + // 저장본에 죽은 엔진만 남은 상태 (= 플러그인 제거 후). 다른 카테고리는 미선택으로 + // 두어 이 케이스가 search 폴백만 태우게 한다. 실제 설정 파일은 건드리지 않는다. + $configRepository = $this->createMock(JsonConfigRepository::class); + $configRepository->method('getCategory')->willReturnCallback( + fn (string $category) => $category === 'drivers' + ? ['search_engine_driver' => 'ghost_engine'] + : [] + ); + $this->app->instance(JsonConfigRepository::class, $configRepository); + + Log::spy(); + + $provider = new CoreServiceProvider($this->app); + $applyDriverConfigs = new \ReflectionMethod($provider, 'applyExtensionDriverConfigs'); + $applyDriverConfigs->setAccessible(true); + $applyDriverConfigs->invoke($provider); + + $this->assertSame( + 'mysql-fulltext', + Config::get('scout.driver'), + '죽은 검색엔진이 그대로 남았습니다 — 이 상태의 공개 검색은 500 으로 떨어집니다.' + ); + + Log::shouldHaveReceived('warning')->withArgs( + fn (string $message) => str_contains($message, 'ghost_engine') + && str_contains($message, 'search') + && str_contains($message, 'mysql-fulltext') + )->atLeast()->once(); + } +} diff --git a/tests/Feature/Settings/SettingsConfigMirrorTest.php b/tests/Feature/Settings/SettingsConfigMirrorTest.php new file mode 100644 index 00000000..144cca0d --- /dev/null +++ b/tests/Feature/Settings/SettingsConfigMirrorTest.php @@ -0,0 +1,354 @@ +<?php + +namespace Tests\Feature\Settings; + +use App\Contracts\Extension\PluginInterface; +use App\Contracts\Extension\StorageInterface; +use App\Extension\PluginManager; +use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; +use App\Services\PluginSettingsService; +use App\Services\SettingsService; +use App\Support\ExtensionSettingsMirror; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Config; +use Mockery; +use Modules\Sirsoft\Page\Services\PageSettingsService; +use Tests\TestCase; + +/** + * 설정 config 미러 재채움 회귀 테스트 (공개이슈 #109) + * + * `g7_settings.*` 미러는 종전에 부팅 때 한 번만 채워졌다. FPM 은 요청마다 부팅해 + * 문제가 드러나지 않지만, 큐 워커·schedule:work·Reverb 처럼 상주하는 프로세스에서는 + * 관리자가 저장해도 그 프로세스가 옛 값을 영원히 읽는다. + * + * 아래 테스트는 **한 프로세스 안에서** 저장 후 미러가 갱신되는지를 본다 — + * 재부팅으로는 결함이 드러나지 않기 때문이다. + * + * @scenario scope=core, scope=plugin, trigger=save, trigger=boot, field_sensitivity=sensitive + * + * @effects core_mirror_refreshed_after_save, core_mirror_refreshed_after_set_setting, plugin_mirror_refreshed_after_save, plugin_mirror_merges_defaults, sensitive_fields_absent_from_mirror_on_boot, sensitive_fields_absent_from_mirror_on_refresh, sensitive_fields_still_readable_via_dedicated_getter, page_module_mirror_exists + */ +class SettingsConfigMirrorTest extends TestCase +{ + use RefreshDatabase; + + /** + * A1: 코어 설정 저장 후 같은 프로세스의 미러가 신값을 읽는다. + */ + public function test_core_settings_mirror_is_refreshed_after_save(): void + { + Config::set('g7_settings.core.general.site_name', '옛 이름'); + + app(SettingsService::class)->saveSettings([ + 'general' => ['site_name' => '새 이름'], + ]); + + $this->assertSame('새 이름', g7_core_settings('general.site_name')); + } + + /** + * A1: setSetting() 경로도 같은 무효화 처리를 받는다. + */ + public function test_set_setting_also_refreshes_core_mirror(): void + { + Config::set('g7_settings.core.general.site_name', '옛 이름'); + + app(SettingsService::class)->setSetting('general.site_name', '단건 저장 이름'); + + $this->assertSame('단건 저장 이름', g7_core_settings('general.site_name')); + } + + /** + * A1/A3: 부팅 경로도 컨테이너의 미러 인스턴스를 거친다. + * + * 부팅과 저장이 서로 다른 인스턴스를 만들면 한쪽 구현만 고쳐도 초록으로 남는다 — + * 컨테이너 단일 해석이 "두 경로가 같은 코드를 쓴다" 의 실질적 근거다. + * + * @effects boot_and_save_share_one_fill_implementation + */ + public function test_boot_path_fills_the_mirror_through_the_container_instance(): void + { + $spy = new class extends ExtensionSettingsMirror + { + /** @var int refreshCore 호출 횟수 */ + public int $coreRefreshes = 0; + + public function refreshCore(?JsonConfigRepository $configRepository = null): void + { + $this->coreRefreshes++; + } + }; + + $this->app->instance(ExtensionSettingsMirror::class, $spy); + + $provider = new SettingsServiceProvider($this->app); + $load = new \ReflectionMethod($provider, 'loadCoreSettingsToConfig'); + $load->setAccessible(true); + $load->invoke($provider, new JsonConfigRepository); + + $this->assertSame( + 1, + $spy->coreRefreshes, + '부팅 경로가 컨테이너 미러를 거치지 않고 자체 인스턴스를 만듭니다' + ); + } + + /** + * A2/A4: 플러그인 미러는 defaults 병합·복호화를 마친 값(전용 게터와 같은 형태)을 담는다. + * + * @effects plugin_mirror_matches_dedicated_getter_shape + */ + public function test_plugin_settings_mirror_uses_the_service_value_not_raw_file(): void + { + $this->bindStubPlugin([ + 'api_key' => ['sensitive' => true], + 'endpoint' => ['sensitive' => false], + ], [ + 'api_key' => 'super-secret-value', + 'endpoint' => 'https://example.com', + 'from_defaults' => 'default-value', + ]); + + app(ExtensionSettingsMirror::class)->refreshPlugin('stub-plugin'); + + $mirror = g7_plugin_settings('stub-plugin'); + + $this->assertIsArray($mirror); + // raw 파일 읽기였다면 defaults 만 있는 키는 미러에 없다 + $this->assertSame('default-value', $mirror['from_defaults'] ?? null); + $this->assertSame('https://example.com', $mirror['endpoint'] ?? null); + + // 키 몇 개만 보면 형태가 갈려도 통과한다 — 전용 게터 결과에서 민감 키만 뺀 것과 + // 정확히 같아야 한다(형태 동일성). + $viaGetter = (array) app(PluginSettingsService::class)->get('stub-plugin'); + + $this->assertSame( + Arr::except($viaGetter, ['api_key']), + $mirror, + '미러가 전용 게터와 다른 형태의 값을 담고 있습니다' + ); + } + + /** + * A6/A7: 민감 필드는 미러에 담기지 않는다 (건별 재채움 / 전체 재채움 양쪽). + * + * 미러는 봇 전용 화면 생성기가 키 제한 없이 조회하는 경로이므로, 민감값이 실리면 + * 레이아웃이 그 키를 참조하는 순간 평문이 봇 HTML 로 나간다. + */ + public function test_sensitive_plugin_fields_never_reach_the_mirror(): void + { + $this->bindStubPlugin([ + 'api_key' => ['sensitive' => true], + 'endpoint' => ['sensitive' => false], + ], [ + 'api_key' => 'super-secret-value', + 'endpoint' => 'https://example.com', + ]); + + $mirror = app(ExtensionSettingsMirror::class); + + $mirror->refreshPlugin('stub-plugin'); + + $this->assertArrayNotHasKey('api_key', (array) g7_plugin_settings('stub-plugin')); + $this->assertArrayNotHasKey('api_key', (array) Config::get('g7_settings.plugins.stub-plugin')); + $this->assertSame('https://example.com', g7_plugin_settings('stub-plugin', 'endpoint')); + + // 부팅 경로(전체 재채움)에서도 동일 — 한쪽만 막고 다른 쪽에서 새는 회귀 차단 + $mirror->refreshAllPlugins(); + + $this->assertArrayNotHasKey('api_key', (array) g7_plugin_settings('stub-plugin')); + + // 전용 게터 경로는 살아 있다 (기능 손실 없음) + $this->assertSame('super-secret-value', app(PluginSettingsService::class)->get('stub-plugin', 'api_key')); + } + + /** + * A2/A7: 플러그인 저장 경로가 미러 재채움을 호출하고, 그 결과에 민감값이 없다. + * + * @effects sensitive_fields_absent_from_mirror_after_save + */ + public function test_plugin_save_triggers_mirror_refresh(): void + { + $spy = new class extends ExtensionSettingsMirror + { + /** @var array<int, string> 재채움 요청을 받은 식별자 */ + public array $refreshed = []; + + public function refreshPlugin(string $identifier): void + { + $this->refreshed[] = $identifier; + } + }; + + $this->app->instance(ExtensionSettingsMirror::class, $spy); + + $this->bindStubPlugin([ + 'endpoint' => ['sensitive' => false], + 'api_key' => ['sensitive' => true], + ], [ + 'endpoint' => 'https://example.com', + 'api_key' => 'initial-secret', + ]); + + app(PluginSettingsService::class)->save('stub-plugin', [ + 'endpoint' => 'https://changed.example.com', + 'api_key' => 'rotated-secret', + ]); + + $this->assertContains('stub-plugin', $spy->refreshed, '플러그인 저장이 미러를 갱신하지 않았습니다'); + + // 호출만 세면 재채움이 빈 값을 써도 통과한다 — 실제 미러에 신값이 담기는지 함께 본다. + $this->app->forgetInstance(ExtensionSettingsMirror::class); + app(ExtensionSettingsMirror::class)->refreshPlugin('stub-plugin'); + + $this->assertSame( + 'https://changed.example.com', + g7_plugin_settings('stub-plugin', 'endpoint'), + '재채움이 저장된 신값을 담지 않았습니다' + ); + + // 실제 저장 경로를 거친 뒤에도 민감값은 미러에 실리지 않는다 — + // 직접 refreshPlugin() 만 호출한 케이스는 저장이 만든 페이로드를 통과시키지 않는다. + $mirror = (array) g7_plugin_settings('stub-plugin'); + + $this->assertArrayNotHasKey('api_key', $mirror); + $this->assertStringNotContainsString( + 'rotated-secret', + (string) json_encode($mirror, JSON_UNESCAPED_UNICODE), + '저장 직후 재채움 경로로 민감값이 미러에 실렸습니다' + ); + + // 전용 게터로는 회전된 신값이 읽힌다 (기능 손실 없음). + $this->assertSame('rotated-secret', app(PluginSettingsService::class)->get('stub-plugin', 'api_key')); + } + + /** + * A8: 백업 복원도 미러를 다시 채운다. + * + * 복원은 설정 전체를 갈아엎는 쓰기다. 캐시만 비우면 같은 프로세스가 복원 전 값을 + * 계속 읽어, 운영자가 "복원했는데 아무것도 안 바뀐" 상태를 본다. + * + * @effects core_mirror_refreshed_after_restore + */ + public function test_restore_refreshes_the_core_mirror(): void + { + $service = app(SettingsService::class); + + $service->saveSettings(['general' => ['site_name' => '복원 전 이름']]); + $backupPath = $service->backupSettings(); + + $service->saveSettings(['general' => ['site_name' => '복원 후 바뀐 이름']]); + $this->assertSame('복원 후 바뀐 이름', g7_core_settings('general.site_name')); + + $this->assertTrue($service->restoreSettings($backupPath)); + + $this->assertSame( + '복원 전 이름', + g7_core_settings('general.site_name'), + '복원 후에도 미러가 복원 전 값을 유지합니다' + ); + } + + /** + * A9: 플러그인 설정 초기화도 미러를 다시 채운다. + * + * @effects plugin_mirror_refreshed_after_reset + */ + public function test_plugin_reset_refreshes_the_mirror(): void + { + $this->bindStubPlugin( + ['endpoint' => ['sensitive' => false]], + ['endpoint' => 'https://default.example.com'] + ); + + $service = app(PluginSettingsService::class); + + $service->save('stub-plugin', ['endpoint' => 'https://saved.example.com']); + $this->assertSame('https://saved.example.com', g7_plugin_settings('stub-plugin', 'endpoint')); + + $service->reset('stub-plugin'); + + $this->assertSame( + 'https://default.example.com', + g7_plugin_settings('stub-plugin', 'endpoint'), + '초기화 후에도 미러가 초기화 전 값을 유지합니다' + ); + } + + /** + * A5: sirsoft-page 미러가 존재하고 defaults.json 값을 담는다. + */ + public function test_page_module_mirror_exists(): void + { + if (! class_exists(PageSettingsService::class)) { + $this->markTestSkipped('sirsoft-page 모듈이 설치되어 있지 않습니다.'); + } + + Config::set('g7_settings.modules.sirsoft-page', null); + + app(ExtensionSettingsMirror::class)->refreshModule('sirsoft-page'); + + $expected = app(PageSettingsService::class) + ->getSetting('attachment.max_count'); + + $this->assertNotNull($expected, 'sirsoft-page 설정 서비스가 값을 돌려주지 않습니다'); + $this->assertSame( + $expected, + g7_module_settings('sirsoft-page', 'attachment.max_count'), + 'sirsoft-page 미러가 설정 서비스와 다른 값을 담고 있습니다' + ); + } + + /** + * 스키마/기본값을 지정한 스텁 플러그인을 활성 플러그인으로 등록합니다. + * + * 어떤 플러그인이 설치돼 있느냐에 결과가 좌우되지 않도록 통제된 스키마를 쓴다 — + * 검증 대상은 미러의 민감 필드 제거·값 형태 로직 자체다. + * + * @param array<string, array<string, mixed>> $schema 설정 스키마 + * @param array<string, mixed> $defaults 설정 기본값 + */ + private function bindStubPlugin(array $schema, array $defaults): void + { + // 저장 내용을 실제로 보관하는 인메모리 스토리지 — put 한 값이 get 으로 돌아와야 + // "저장 → 재채움 → 신값" 사슬을 값 수준에서 검증할 수 있다. + $written = new \ArrayObject; + + $storage = Mockery::mock(StorageInterface::class); + $storage->shouldReceive('exists')->andReturnUsing( + fn ($dir, $file) => isset($written[$file]) + ); + $storage->shouldReceive('get')->andReturnUsing( + fn ($dir, $file) => $written[$file] ?? '' + ); + $storage->shouldReceive('put')->andReturnUsing(function ($dir, $file, $content) use ($written) { + $written[$file] = $content; + + return true; + }); + $storage->shouldReceive('delete')->andReturnUsing(function ($dir, $file) use ($written) { + unset($written[$file]); + + return true; + }); + + $plugin = Mockery::mock(PluginInterface::class); + $plugin->shouldReceive('getStorage')->andReturn($storage); + $plugin->shouldReceive('getIdentifier')->andReturn('stub-plugin'); + $plugin->shouldReceive('getSettingsSchema')->andReturn($schema); + $plugin->shouldReceive('getConfigValues')->andReturn($defaults); + $plugin->shouldReceive('hasSettings')->andReturn(true); + $plugin->shouldReceive('getSettingsDefaultsPath')->andReturn(null); + + $pluginManager = Mockery::mock(PluginManager::class); + $pluginManager->shouldReceive('getPlugin')->with('stub-plugin')->andReturn($plugin); + $pluginManager->shouldReceive('getPlugin')->andReturn(null); + $pluginManager->shouldReceive('getActivePlugins')->andReturn(['stub-plugin' => $plugin]); + + $this->app->instance(PluginManager::class, $pluginManager); + $this->app->forgetInstance(PluginSettingsService::class); + } +} diff --git a/tests/Feature/Template/PublicLayoutControllerTest.php b/tests/Feature/Template/PublicLayoutControllerTest.php index 12e89314..14833fb4 100644 --- a/tests/Feature/Template/PublicLayoutControllerTest.php +++ b/tests/Feature/Template/PublicLayoutControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Template; +use App\Contracts\Extension\CacheInterface; use App\Models\Permission; use App\Models\Role; use App\Models\Template; @@ -203,9 +204,13 @@ class PublicLayoutControllerTest extends TestCase // 캐시 초기화 — CacheInterface 를 통해 코어 캐시 prefix 일관 사용 // (Laravel Cache facade 의 global `CACHE_PREFIX` 와 CoreCacheDriver 의 - // `g7:core` prefix 가 합쳐진 실제 키와 동등하게 조회/삭제하기 위함) - $coreCache = app(\App\Contracts\Extension\CacheInterface::class); - $coreCache->forget("layout.{$template->identifier}.{$layout->name}.v0"); + // `g7:core` prefix 가 합쳐진 실제 키와 동등하게 조회/삭제하기 위함). + // `?v` 생략 시 현재 확장 캐시 버전으로 폴백하므로 (#588 — `.v0` 사각 키 방지) + // 버전을 시드해 결정적 키로 검증한다 + Cache::put('g7:core:ext.cache_version', 1234); + + $coreCache = app(CacheInterface::class); + $coreCache->forget("layout.{$template->identifier}.{$layout->name}.v1234"); $coreCache->forget("template.{$template->id}.layout.{$layout->name}"); // Act: 첫 번째 요청 (캐시 생성) @@ -214,7 +219,7 @@ class PublicLayoutControllerTest extends TestCase // 캐시가 생성되었는지 CacheInterface 로 확인 (컨트롤러가 사용하는 동일 경로) $this->assertTrue( - $coreCache->has("layout.{$template->identifier}.{$layout->name}.v0"), + $coreCache->has("layout.{$template->identifier}.{$layout->name}.v1234"), '레이아웃 응답이 CoreCacheDriver(g7:core 접두사) 에 캐시되어야 함', ); @@ -252,7 +257,7 @@ class PublicLayoutControllerTest extends TestCase 'content' => ['meta' => ['title' => 'Dashboard'], 'data_sources' => [], 'components' => []], ]); - $coreCache = app(\App\Contracts\Extension\CacheInterface::class); + $coreCache = app(CacheInterface::class); $version = 1781151505; $nonce = 7; // 편집 모드(.meta) — `core.templates.layouts.edit` 권한 보유 사용자로 인증. diff --git a/tests/Feature/Template/PublicTemplateControllerTest.php b/tests/Feature/Template/PublicTemplateControllerTest.php index 7b6b8b51..7fe04470 100644 --- a/tests/Feature/Template/PublicTemplateControllerTest.php +++ b/tests/Feature/Template/PublicTemplateControllerTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Template; +use App\Contracts\Extension\CacheInterface; use App\Enums\ExtensionStatus; use App\Models\Template; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -141,9 +142,12 @@ class PublicTemplateControllerTest extends TestCase 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], ]); - // 캐시 초기화 (CacheInterface 사용 — v{version} 접미사 포함, 기본 v=0) - $cache = app(\App\Contracts\Extension\CacheInterface::class); - $cacheKey = "template.routes.{$template->identifier}.v0"; + // `?v` 생략 시 현재 확장 캐시 버전으로 폴백하므로 (#588 — `.v0` 사각 키 방지) + // 버전을 시드해 결정적 키로 검증한다 (getExtensionCacheVersion 은 0 을 반환하지 않음) + Cache::put('g7:core:ext.cache_version', 1234); + + $cache = app(CacheInterface::class); + $cacheKey = "template.routes.{$template->identifier}.v1234"; $cache->forget($cacheKey); // Act: 첫 번째 요청 (캐시 생성) @@ -160,4 +164,98 @@ class PublicTemplateControllerTest extends TestCase // 두 응답의 데이터가 동일한지 확인 $this->assertEquals($response1->json('data'), $response2->json('data')); } + + /** + * config.json 서빙이 캐시를 생성하고 installed manifest 버전을 반환하는지 테스트 (#588, 공개 #119) + * + * @scenario cache_state=cold,template_type=admin + * + * @effects serve_config_caches_manifest_and_embeds_cache_version + */ + public function test_serve_config_caches_manifest_and_returns_version(): void + { + // Arrange: 활성화된 템플릿 생성 (실제 번들 파일을 읽는 경량 픽스처) + $template = Template::create([ + 'identifier' => 'sirsoft-admin_basic', + 'vendor' => 'sirsoft', + 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], + 'version' => '1.0.0', + 'type' => 'admin', + 'status' => ExtensionStatus::Active->value, + 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], + ]); + + $cache = app(CacheInterface::class); + $cacheKey = "template.config.{$template->identifier}"; + $cache->forget($cacheKey); + + // Act + $response = $this->getJson("/api/templates/{$template->identifier}/config.json"); + + // Assert: 성공 + installed template.json 의 실제 version 반환 + $response->assertStatus(200)->assertJson(['success' => true]); + + $manifest = json_decode( + file_get_contents(base_path("templates/{$template->identifier}/template.json")), + true + ); + $this->assertSame($manifest['version'], $response->json('data.version')); + + // cache_version 은 캐시 read 후 현재값이 주입되는 유효 정수 + $this->assertIsInt($response->json('data.cache_version')); + $this->assertGreaterThan(0, $response->json('data.cache_version')); + + // 서버 캐시 키가 생성됨 + $this->assertNotNull($cache->get($cacheKey), 'config 조회 후 서버 캐시가 생성되어야 합니다.'); + } + + /** + * config.json 캐시 삭제 후 최신 manifest 가 반환되는지 테스트 (#588, 공개 #119) + * + * 캐시 히트 증명(변조본 서빙) → forget → 실제 파일 버전 복귀 체인. + * 라이브 재현(sentinel 주입 → cache-clear 무효 → 직접 forget 으로만 해소)의 자동화판. + * + * @scenario cache_state=warm_stale,template_type=admin + * + * @effects serve_config_returns_installed_manifest_after_invalidation + */ + public function test_serve_config_returns_fresh_manifest_after_cache_cleared(): void + { + // Arrange + $template = Template::create([ + 'identifier' => 'sirsoft-admin_basic', + 'vendor' => 'sirsoft', + 'name' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], + 'version' => '1.0.0', + 'type' => 'admin', + 'status' => ExtensionStatus::Active->value, + 'description' => ['ko' => '기본 관리자 템플릿', 'en' => 'Basic Admin Template'], + ]); + + $cache = app(CacheInterface::class); + $cacheKey = "template.config.{$template->identifier}"; + + // 캐시에 변조된 manifest 심기 (파일 무수정 — stale 상태 재현) + $cache->put($cacheKey, [ + 'success' => true, + 'data' => ['version' => '9.9.9-stale-sentinel'], + ], 3600); + + // Act & Assert 1: 캐시 히트 경로 증명 — 변조본이 그대로 서빙됨 + $stale = $this->getJson("/api/templates/{$template->identifier}/config.json"); + $stale->assertStatus(200); + $this->assertSame('9.9.9-stale-sentinel', $stale->json('data.version')); + + // Act & Assert 2: forget 후 실제 파일 버전 복귀 + $cache->forget($cacheKey); + + $fresh = $this->getJson("/api/templates/{$template->identifier}/config.json"); + $fresh->assertStatus(200); + + $manifest = json_decode( + file_get_contents(base_path("templates/{$template->identifier}/template.json")), + true + ); + $this->assertSame($manifest['version'], $fresh->json('data.version')); + } } diff --git a/tests/Feature/Template/TemplateCacheManagementTest.php b/tests/Feature/Template/TemplateCacheManagementTest.php index 22672db2..2d69c546 100644 --- a/tests/Feature/Template/TemplateCacheManagementTest.php +++ b/tests/Feature/Template/TemplateCacheManagementTest.php @@ -284,6 +284,196 @@ class TemplateCacheManagementTest extends TestCase } } + /** + * template:cache-clear 커맨드가 공개 config 캐시를 삭제하는지 테스트 (#588, 공개 #119) + * + * `template.config.{identifier}` 는 버전 접미사 없는 고정 키라 캐시 버전 bump 로 + * 무효화되지 않는다 — 커맨드가 능동 forget 하지 않으면 TTL(1시간) 동안 + * 이전 manifest 가 계속 서빙된다. + * + * @scenario cache_state=warm_stale,lifecycle_trigger=cache_clear_command_single,template_type=admin + * + * @effects cache_clear_command_forgets_template_config_key + */ + public function test_template_cache_clear_command_forgets_public_config_cache(): void + { + // Arrange + $this->templateManager->installTemplate('sirsoft-admin_basic'); + + $this->coreCache()->put('template.config.sirsoft-admin_basic', [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + + // Act + $this->artisan('template:cache-clear', ['identifier' => 'sirsoft-admin_basic']) + ->assertExitCode(0); + + // Assert + $this->assertNull( + $this->coreCache()->get('template.config.sirsoft-admin_basic'), + 'template:cache-clear 후 공개 config 캐시(template.config.{identifier})가 삭제되어야 합니다.' + ); + } + + /** + * template:cache-clear 전체 모드가 공개 config 캐시를 삭제하는지 테스트 (#588, 공개 #119) + * + * @scenario cache_state=warm_stale,lifecycle_trigger=cache_clear_command_all,template_type=admin + * + * @effects cache_clear_command_forgets_template_config_key + */ + public function test_template_cache_clear_all_forgets_public_config_cache(): void + { + // Arrange + $this->templateManager->installTemplate('sirsoft-admin_basic'); + + $this->coreCache()->put('template.config.sirsoft-admin_basic', [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + + // Act (무인자 = 전체 모드) + $this->artisan('template:cache-clear') + ->assertExitCode(0); + + // Assert + $this->assertNull( + $this->coreCache()->get('template.config.sirsoft-admin_basic'), + 'template:cache-clear 전체 모드 후 공개 config 캐시가 삭제되어야 합니다.' + ); + } + + /** + * clearTemplateCache() 가 config + components_manifest 고정 키를 삭제하는지 테스트 (#588) + * + * components_manifest 는 숫자 id / identifier 두 변종 키가 실재한다 + * (ComponentExists 는 int|string 을 수용). 템플릿 업데이트로 components.json 이 + * 변해도 이 키가 남으면 최대 1시간 구 매니페스트로 레이아웃을 검증한다. + * + * @scenario cache_state=warm_stale,lifecycle_trigger=template_update,template_type=admin + * + * @effects template_update_invalidates_public_config_cache,clear_template_cache_forgets_components_manifest_variants + */ + public function test_clear_template_cache_forgets_config_and_components_manifest(): void + { + // Arrange + $this->templateManager->installTemplate('sirsoft-admin_basic'); + $template = Template::where('identifier', 'sirsoft-admin_basic')->first(); + + $this->coreCache()->put('template.config.sirsoft-admin_basic', [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + $this->coreCache()->put("template.{$template->id}.components_manifest", ['components' => []], 3600); + $this->coreCache()->put('template.sirsoft-admin_basic.components_manifest', ['components' => []], 3600); + + // Act — 라이프사이클(update/deactivate/uninstall/cache-clear)이 공유하는 무효화 단일 지점 + $this->templateManager->clearTemplateCache('sirsoft-admin_basic'); + + // Assert + $this->assertNull( + $this->coreCache()->get('template.config.sirsoft-admin_basic'), + 'clearTemplateCache() 후 공개 config 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $this->coreCache()->get("template.{$template->id}.components_manifest"), + 'clearTemplateCache() 후 숫자 id 변종 components_manifest 캐시가 삭제되어야 합니다.' + ); + $this->assertNull( + $this->coreCache()->get('template.sirsoft-admin_basic.components_manifest'), + 'clearTemplateCache() 후 identifier 변종 components_manifest 캐시가 삭제되어야 합니다.' + ); + } + + /** + * 비활성화/삭제 라이프사이클이 공개 config 캐시를 삭제하는지 테스트 (#588, 공개 #119) + * + * 활성 시절 warm 된 config 캐시가 남으면 비활성/삭제된 템플릿의 manifest 를 + * 최대 1시간 200 으로 계속 서빙한다 (캐시 히트가 상태 검사보다 먼저). + * + * @scenario cache_state=warm_stale,lifecycle_trigger=deactivate,template_type=admin + * + * @effects deactivate_uninstall_invalidate_public_config_cache + */ + public function test_template_deactivate_and_uninstall_invalidate_public_config_cache(): void + { + // Arrange + $this->templateManager->installTemplate('sirsoft-admin_basic'); + $this->templateManager->activateTemplate('sirsoft-admin_basic'); + + $warmConfig = function (): void { + $this->coreCache()->put('template.config.sirsoft-admin_basic', [ + 'success' => true, + 'data' => ['version' => '0.0.1-stale'], + ], 3600); + }; + + // Act & Assert — 비활성화 경로 + $warmConfig(); + $this->templateManager->deactivateTemplate('sirsoft-admin_basic'); + $this->assertNull( + $this->coreCache()->get('template.config.sirsoft-admin_basic'), + '비활성화 후 공개 config 캐시가 삭제되어야 합니다.' + ); + + // Act & Assert — 삭제 경로 + $warmConfig(); + $this->templateManager->uninstallTemplate('sirsoft-admin_basic'); + $this->assertNull( + $this->coreCache()->get('template.config.sirsoft-admin_basic'), + '삭제 후 공개 config 캐시가 삭제되어야 합니다.' + ); + } + + /** + * 비활성화 시 편집기용 `.with_source_meta` 변종 레이아웃 캐시도 삭제되는지 테스트 (#588) + * + * LayoutService 는 편집기 병합 응답을 `.with_source_meta` 접미사 키로 별도 캐싱한다. + * 라이프사이클 무효화(forgetLayoutCacheKeys)가 접미사 없는 키만 지우면 + * activate/deactivate/uninstall/refresh 후에도 편집기용 병합 캐시가 잔존한다. + * + * @scenario cache_state=warm_stale,lifecycle_trigger=uninstall,template_type=admin + * + * @effects with_source_meta_layout_cache_variants_forgotten + */ + public function test_deactivate_template_clears_with_source_meta_layout_caches(): void + { + // Arrange + $this->templateManager->installTemplate('sirsoft-admin_basic'); + $this->templateManager->activateTemplate('sirsoft-admin_basic'); + + $template = Template::where('identifier', 'sirsoft-admin_basic')->first(); + $layouts = TemplateLayout::where('template_id', $template->id)->get(); + $this->assertGreaterThan(0, $layouts->count()); + + $warmedKeys = []; + foreach ($layouts as $layout) { + $key = "template.{$template->id}.layout.{$layout->name}.with_source_meta"; + $this->coreCache()->put($key, ['test' => 'meta'], 3600); + $warmedKeys[] = $key; + + // 소스 해시 변종 (LayoutService::getMergedLayoutCacheKey 와 동형 — 접미사는 해시 뒤) + if ($layout->source_type && $layout->source_identifier) { + $sourceHash = md5($layout->source_type->value.$layout->source_identifier); + $hashKey = "template.{$template->id}.layout.{$layout->name}.{$sourceHash}.with_source_meta"; + $this->coreCache()->put($hashKey, ['test' => 'meta'], 3600); + $warmedKeys[] = $hashKey; + } + } + + // Act + $this->templateManager->deactivateTemplate('sirsoft-admin_basic'); + + // Assert + foreach ($warmedKeys as $key) { + $this->assertNull( + $this->coreCache()->get($key), + "비활성화 후 편집기용 병합 캐시({$key})가 삭제되어야 합니다." + ); + } + } + /** * 테스트용 레이아웃 JSON 파일 생성 */ diff --git a/tests/Feature/Template/TemplateLanguageServingTest.php b/tests/Feature/Template/TemplateLanguageServingTest.php index 81f43f58..ec5f8478 100644 --- a/tests/Feature/Template/TemplateLanguageServingTest.php +++ b/tests/Feature/Template/TemplateLanguageServingTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature\Template; +use App\Contracts\Extension\CacheInterface; use App\Enums\ExtensionStatus; use App\Models\Template; use App\Services\TemplateService; @@ -129,9 +130,12 @@ class TemplateLanguageServingTest extends TestCase */ public function test_language_file_is_cached(): void { - // PublicBaseController::cached() 는 CacheInterface + v{version} 접미사 (기본 v=0) - $cache = app(\App\Contracts\Extension\CacheInterface::class); - $cacheKey = 'template.language.sirsoft-admin_basic.ko.v0'; + // `?v` 생략 시 현재 확장 캐시 버전으로 폴백하므로 (#588 — `.v0` 사각 키 방지) + // 버전을 시드해 결정적 키로 검증한다 + Cache::put('g7:core:ext.cache_version', 1234); + + $cache = app(CacheInterface::class); + $cacheKey = 'template.language.sirsoft-admin_basic.ko.v1234'; $cache->forget($cacheKey); // 첫 번째 요청 @@ -179,4 +183,4 @@ class TemplateLanguageServingTest extends TestCase $response = $this->getJson('/api/templates/sirsoft-admin_basic/lang/KO.json'); $response->assertStatus(404); } -} \ No newline at end of file +} diff --git a/tests/Feature/User/UserWithdrawErrorLeakTest.php b/tests/Feature/User/UserWithdrawErrorLeakTest.php new file mode 100644 index 00000000..fe8f553f --- /dev/null +++ b/tests/Feature/User/UserWithdrawErrorLeakTest.php @@ -0,0 +1,80 @@ +<?php + +namespace Tests\Feature\User; + +use App\Extension\HookManager; +use App\Models\User; +use App\Services\UserService; +use Illuminate\Database\QueryException; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Validation\ValidationException; +use Tests\TestCase; + +/** + * 회원 탈퇴 실패 시 SQL 원문이 사용자 응답/예외 메시지에 노출되지 않는지 검증합니다. + * + * 결함(㉚-2): withdrawUser() catch 블록이 __('user.withdraw_failed', ['error' => $e->getMessage()]) + * 로 예외 원문(SQLSTATE·쿼리·테이블명 등)을 사용자에게 노출했다. 원본은 Log 로만 남기고 + * 사용자 메시지는 원문 없는 고정 i18n 키로 교체한다. + */ +class UserWithdrawErrorLeakTest extends TestCase +{ + use RefreshDatabase; + + /** + * 탈퇴 처리 중 QueryException 이 발생해도 그 예외 메시지에 SQL 원문이 없어야 합니다. + * + * @scenario case=user_withdraw_failure_masked + * + * @effects failure_response_omits_sql_text + */ + public function test_withdraw_failure_message_does_not_leak_sql(): void + { + $user = User::factory()->create(); + + // 탈퇴 트랜잭션 진입 직후(첫 훅) QueryException 을 유발한다. + // DDL 로 유발하면 암묵 커밋이 RefreshDatabase savepoint 를 깨므로, 훅에서 + // 예외를 던져 트랜잭션이 정상 롤백되도록 한다. 예외 메시지에는 실제 SQL 원문 + // (SQLSTATE·쿼리·테이블명)을 담아, 이 원문이 사용자 메시지로 새지 않음을 검증한다. + $sqlOriginal = "SQLSTATE[42S02]: Base table or view not found: 1146 Table 'g7.user_consents' doesn't exist " + .'(Connection: mysql, SQL: delete from `user_consents` where `user_id` = '.$user->id.')'; + + HookManager::addAction('core.user.before_withdraw', function () use ($sqlOriginal): void { + throw new QueryException('mysql', 'delete from `user_consents`', [], new \RuntimeException($sqlOriginal)); + }); + + $service = app(UserService::class); + + try { + $service->withdrawUser($user); + $this->fail('탈퇴가 실패해 ValidationException 이 발생해야 합니다.'); + } catch (ValidationException $e) { + $messages = collect($e->errors())->flatten()->implode(' '); + + // 원문(SQL 상태코드·쿼리 키워드·테이블명·placeholder 잔재)이 없어야 한다. + $this->assertStringNotContainsStringIgnoringCase('SQLSTATE', $messages); + $this->assertStringNotContainsStringIgnoringCase('select ', $messages); + $this->assertStringNotContainsStringIgnoringCase('delete from', $messages); + $this->assertStringNotContainsString('user_consents', $messages); + $this->assertStringNotContainsString(':error', $messages); + } + } + + /** + * 탈퇴 실패 다국어 키가 원문 placeholder(:error) 를 담지 않아야 합니다. + * + * @scenario case=masked_lang_keys + * + * @effects masked_lang_keys_have_no_error_placeholder + */ + public function test_withdraw_failed_lang_key_has_no_error_placeholder(): void + { + foreach (['ko', 'en'] as $locale) { + $message = __('user.withdraw_failed', [], $locale); + + $this->assertStringNotContainsString(':error', $message); + // 키가 해석되어 실제 문구가 반환되어야 한다 (키 원문 그대로가 아님). + $this->assertNotSame('user.withdraw_failed', $message); + } + } +} diff --git a/tests/Feature/Validation/GlobalAttributeLabelTest.php b/tests/Feature/Validation/GlobalAttributeLabelTest.php new file mode 100644 index 00000000..b928ee14 --- /dev/null +++ b/tests/Feature/Validation/GlobalAttributeLabelTest.php @@ -0,0 +1,305 @@ +<?php + +namespace Tests\Feature\Validation; + +use App\Enums\ExtensionOwnerType; +use App\Models\Permission; +use App\Models\Role; +use App\Models\User; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\App; +use Tests\TestCase; + +/** + * 전역 validation attributes 라벨 정화 회귀 테스트 (공개이슈 #113) + * + * 전역 `validation.attributes` 는 모든 FormRequest 가 공유하는 라벨 사전입니다. + * 여기에 특정 도메인 문구(예: 'SMTP 비밀번호')를 넣으면, 그 필드명을 쓰는 모든 + * 화면의 오류 문구가 오염됩니다. 이 테스트는 ① 범용 키가 범용 라벨을 유지하고, + * ② 도메인 전용 키(smtp_*, user_ids 등)가 의도 소비자에서만 쓰이는지 고정합니다. + */ +class GlobalAttributeLabelTest extends TestCase +{ + use RefreshDatabase; + + /** + * D1: 회원가입 password 타입 오류 문구에 SMTP 라벨이 새지 않는다. + * + * @scenario entry_point=register, attribute_label_scope=generic_password, locale=ko + * + * @effects smtp_label_never_appears_in_password_setting_paths + */ + public function test_register_password_type_error_uses_generic_password_label(): void + { + $response = $this->withHeaders(['Accept-Language' => 'ko']) + ->postJson('/api/auth/register', [ + 'email' => 'g7fix113.d1@example.com', + 'password' => 12345678, + 'password_confirmation' => 12345678, + 'name' => '테스트', + ]); + + $response->assertStatus(422); + + $message = json_encode($response->json('errors.password'), JSON_UNESCAPED_UNICODE); + + $this->assertStringNotContainsString('SMTP', (string) $message); + $this->assertStringContainsString('비밀번호', (string) $message); + } + + /** + * D2: 비밀번호 확인 엔드포인트(messages() 미정의)의 required 문구 — 제보 문구 경로. + * + * @scenario entry_point=verify_password, attribute_label_scope=generic_password, locale=ko + * + * @effects smtp_label_never_appears_in_password_setting_paths + */ + public function test_verify_password_required_message_uses_generic_password_label(): void + { + $user = User::factory()->create(); + + $response = $this->withToken($user->createToken('t')->plainTextToken) + ->postJson('/api/me/verify-password', []); + + $response->assertStatus(422); + + $message = (string) json_encode($response->json('errors.password'), JSON_UNESCAPED_UNICODE); + + $this->assertStringNotContainsString('SMTP', $message); + $this->assertStringContainsString('비밀번호', $message); + } + + /** + * D3: PasswordPolicy 규칙 실패 메시지의 :attribute 치환값. + * + * @scenario entry_point=register, attribute_label_scope=generic_password, input_state=violates_length, locale=ko + * + * @effects policy_message_attribute_uses_generic_password_label + */ + public function test_password_policy_message_uses_generic_password_label(): void + { + $response = $this->withHeaders(['Accept-Language' => 'ko']) + ->postJson('/api/auth/register', [ + 'email' => 'g7fix113.d3@example.com', + 'password' => 'a1', + 'password_confirmation' => 'a1', + 'name' => '테스트', + ]); + + $response->assertStatus(422); + + $message = (string) json_encode($response->json('errors.password'), JSON_UNESCAPED_UNICODE); + + $this->assertStringNotContainsString('SMTP', $message); + // 부정 단언만 두면 라벨이 통째로 빠져도 통과한다 — 실제 라벨이 들어갔는지 함께 본다. + $this->assertStringContainsString('비밀번호', $message); + } + + /** + * D4: SMTP 연결 테스트는 도메인 라벨('SMTP 호스트'/'SMTP 비밀번호')을 유지한다. + * + * @scenario attribute_label_scope=smtp_password, locale=ko + * + * @effects smtp_screens_keep_their_domain_labels + */ + public function test_test_mail_request_keeps_smtp_labels(): void + { + $admin = $this->createAdmin(); + + // host 누락 (required, messages() 경로) + $response = $this->withToken($admin->createToken('t')->plainTextToken) + ->postJson('/api/admin/settings/test-mail', [ + 'to_email' => 'a@example.com', + 'from_address' => 'b@example.com', + 'from_name' => '발신자', + 'mailer' => 'smtp', + 'port' => 587, + ]); + + $response->assertStatus(422); + $this->assertStringContainsString( + 'SMTP', + (string) json_encode($response->json('errors.host'), JSON_UNESCAPED_UNICODE) + ); + + // password 배열 전송 (string 규칙, attributes() 경로) + $response = $this->withToken($admin->createToken('t2')->plainTextToken) + ->postJson('/api/admin/settings/test-mail', [ + 'to_email' => 'a@example.com', + 'from_address' => 'b@example.com', + 'from_name' => '발신자', + 'mailer' => 'smtp', + 'host' => 'smtp.example.com', + 'port' => 587, + 'password' => ['x'], + ]); + + $response->assertStatus(422); + $this->assertStringContainsString( + 'SMTP', + (string) json_encode($response->json('errors.password'), JSON_UNESCAPED_UNICODE) + ); + } + + /** + * D5: 사용자 일괄 상태변경은 '사용자 ID 목록', 전역 ids 라벨은 범용 문구. + * + * @scenario attribute_label_scope=user_scoped_ids, locale=ko + * + * @effects bulk_status_screen_keeps_user_scoped_ids_label, generic_ids_label_stays_generic_on_other_screens + */ + public function test_bulk_user_status_keeps_user_scoped_ids_label(): void + { + $admin = $this->createAdmin(); + + $response = $this->withToken($admin->createToken('t')->plainTextToken) + ->patchJson('/api/admin/users/bulk-status', [ + 'status' => 'active', + ]); + + $response->assertStatus(422); + $this->assertStringContainsString( + '사용자 ID 목록', + (string) json_encode($response->json('errors.ids'), JSON_UNESCAPED_UNICODE) + ); + + // 전역 라벨 자체는 범용으로 원복되어 있어야 한다. + $this->assertSame('ID 목록', __('validation.attributes.ids')); + $this->assertSame('사용자 ID 목록', __('validation.attributes.user_ids')); + + // 같은 전역 키를 쓰는 다른 화면은 범용 라벨을 받는다 (HTTP 경로로 확인). + // read-batch 는 ids 에 커스텀 메시지가 없어 전역 attributes 라벨이 그대로 드러난다. + $other = $this->withHeaders(['Accept-Language' => 'ko']) + ->withToken($admin->createToken('t2')->plainTextToken) + ->postJson('/api/admin/notifications/read-batch', []); + + $other->assertStatus(422); + + $otherMessage = (string) json_encode($other->json('errors.ids'), JSON_UNESCAPED_UNICODE); + + $this->assertStringNotContainsString('사용자 ID 목록', $otherMessage); + $this->assertStringContainsString('ID 목록', $otherMessage); + } + + /** + * D6: en / ja 로케일에서도 범용 키가 범용 라벨을 유지한다. + * + * @scenario entry_point=register, attribute_label_scope=generic_password, locale=en + * + * @effects generic_labels_hold_in_every_locale + */ + public function test_generic_labels_are_clean_in_other_locales(): void + { + // en 은 HTTP 경로로도 확인한다 — 사전값만 보면 요청 경로의 회귀를 놓친다. + $response = $this->withHeaders(['Accept-Language' => 'en']) + ->postJson('/api/auth/register', [ + 'email' => 'g7fix113.d6@example.com', + 'password' => 12345678, + 'password_confirmation' => 12345678, + 'name' => 'Matrix', + ]); + + $response->assertStatus(422); + + $enMessage = (string) json_encode($response->json('errors.password'), JSON_UNESCAPED_UNICODE); + + $this->assertStringNotContainsString('SMTP', $enMessage); + $this->assertStringContainsString('password', $enMessage); + + App::setLocale('en'); + + $this->assertSame('password', __('validation.attributes.password')); + $this->assertSame('SMTP password', __('validation.attributes.smtp_password')); + $this->assertSame('ID list', __('validation.attributes.ids')); + $this->assertSame('user ID list', __('validation.attributes.user_ids')); + + App::setLocale('ko'); + + // ja 는 언어팩(런타임 설치본)이라 테스트 환경 로케일로 해석되지 않는다 — + // 번들 원본을 직접 읽어 3로케일 동형 적용을 고정한다. + $ja = require base_path('lang-packs/_bundled/g7-core-ja/backend/ja/validation.php'); + + $this->assertSame('パスワード', $ja['attributes']['password']); + $this->assertSame('SMTPパスワード', $ja['attributes']['smtp_password']); + $this->assertSame('ID一覧', $ja['attributes']['ids']); + $this->assertSame('ユーザーID一覧', $ja['attributes']['user_ids']); + $this->assertSame('言語', $ja['attributes']['language']); + $this->assertSame('通貨', $ja['attributes']['currency']); + } + + /** + * D-extra: 범용 키 전수 — 도메인 문구가 다시 새어 들어오는 회귀를 차단한다. + * + * @scenario attribute_label_scope=generic_ids, locale=ko + * + * @effects generic_attribute_keys_carry_no_domain_prefix + */ + public function test_generic_attribute_keys_are_free_of_domain_prefixes(): void + { + $expected = [ + 'password' => '비밀번호', + 'host' => '호스트', + 'port' => '포트', + 'username' => '사용자명', + 'ids' => 'ID 목록', + 'language' => '언어', + 'currency' => '통화', + 'cache_ttl' => '캐시 유지시간', + 'channels' => '채널', + ]; + + foreach ($expected as $key => $label) { + $this->assertSame($label, __("validation.attributes.{$key}"), "attributes.{$key}"); + } + + $domain = [ + 'smtp_host', 'smtp_port', 'smtp_username', 'smtp_password', + 'user_ids', 'default_language', 'default_currency', + 'seo_page_cache_ttl', 'notification_channels', + ]; + + foreach ($domain as $key) { + $this->assertNotSame( + "validation.attributes.{$key}", + __("validation.attributes.{$key}"), + "도메인 전용 키 미정의: {$key}" + ); + } + } + + /** + * 관리자 사용자 생성 헬퍼 + */ + private function createAdmin(): User + { + $user = User::factory()->create(['email' => 'g7fix113.admin@example.com']); + + $permissionIds = []; + foreach (['core.settings.update', 'core.users.update', 'core.notifications.update'] as $identifier) { + $permission = Permission::firstOrCreate( + ['identifier' => $identifier], + [ + 'name' => json_encode(['ko' => $identifier, 'en' => $identifier]), + 'description' => json_encode(['ko' => $identifier, 'en' => $identifier]), + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => 'admin', + ] + ); + $permissionIds[] = $permission->id; + } + + $role = Role::firstOrCreate( + ['identifier' => 'admin'], + [ + 'name' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'description' => json_encode(['ko' => '관리자', 'en' => 'Administrator']), + 'is_active' => true, + ] + ); + $role->permissions()->syncWithoutDetaching($permissionIds); + $user->roles()->attach($role->id); + + return $user->fresh(); + } +} diff --git a/tests/Fixtures/Extension/FakeExtensionScheduleServiceProvider.php b/tests/Fixtures/Extension/FakeExtensionScheduleServiceProvider.php new file mode 100644 index 00000000..e455725a --- /dev/null +++ b/tests/Fixtures/Extension/FakeExtensionScheduleServiceProvider.php @@ -0,0 +1,26 @@ +<?php + +namespace Modules\G7Testing\Providers; + +use Illuminate\Support\ServiceProvider; +use Modules\G7Testing\Console\FakeExtensionScheduleCommand; + +/** + * 스케줄 Artisan 허용목록의 "확장 소유 명령" 계층을 HTTP 문맥에서 검증하기 위한 + * 테스트 전용 서비스 프로바이더. + * + * 실제 확장 프로바이더는 `$this->app->runningInConsole()` 일 때만 커맨드를 Artisan 에 + * 등록하므로, HTTP 요청(관리자 화면의 스케줄 저장 검증)에서는 `Artisan::all()` 에 + * 확장 커맨드가 존재하지 않는다. 검증기는 이 경우 프로바이더가 선언한 `$commands` + * 기본값을 폴백으로 해석해야 한다 — 이 픽스처는 그 상황(커맨드 미등록 + 프로바이더의 + * `$commands` 선언만 존재)을 재현한다. + * + * `Tests\` PSR-4 네임스페이스가 아니므로 오토로드되지 않는다 — 사용처에서 require 한다. + */ +class FakeExtensionScheduleServiceProvider extends ServiceProvider +{ + /** @var array<int, class-string> 실제 확장 프로바이더와 동일한 선언 형태 */ + protected array $commands = [ + FakeExtensionScheduleCommand::class, + ]; +} diff --git a/tests/Playwright/README.md b/tests/Playwright/README.md index 3ca21cd8..fd7d0826 100644 --- a/tests/Playwright/README.md +++ b/tests/Playwright/README.md @@ -8,9 +8,12 @@ G7 코어의 Playwright (TypeScript) 기반 결정론적 E2E 인프라. # PowerShell (개발 환경) $env:PLAYWRIGHT_BASE_URL='https://g7.dev' npm run test:e2e # 전체 spec -npm run test:e2e:smoke # @smoke 태그 (homepage / login / admin-dashboard 3건) -npm run test:e2e:wysiwyg # 위지윅 매트릭스 90건 (Phase M) +npm run test:e2e:smoke # @smoke 태그 (--grep @smoke) npm run test:e2e:ui # Playwright UI 모드 (디버깅) + +# 특정 spec / 디렉토리만 +npx playwright test tests/Playwright/specs/smoke/admin-base.spec.ts +npx playwright test tests/Playwright/specs/layout-editor --workers=2 ``` ```bash @@ -18,6 +21,13 @@ npm run test:e2e:ui # Playwright UI 모드 (디버깅) PLAYWRIGHT_BASE_URL=https://g7.dev npx playwright test ``` +루트 `package.json` 이 제공하는 E2E 스크립트는 `test:e2e` · `test:e2e:smoke` · `test:e2e:ui` +3개다. 그 밖의 범위 지정은 `npx playwright test` 에 경로나 `--grep` 을 직접 준다. + +브라우저는 Chromium 전용이다 (`playwright.config.ts` 의 `projects` 에 chromium 하나). +Firefox/WebKit 도입 조건은 [docs/testing/e2e-testing.md](../../docs/testing/e2e-testing.md) +§3 "브라우저 범위" 참조. + ## Base URL 해석 우선순위 (하드코딩 회피) `playwright.config.ts` 의 `resolveBaseUrl()` 가 다음 순서로 결정: @@ -30,24 +40,30 @@ PLAYWRIGHT_BASE_URL=https://g7.dev npx playwright test ## 디렉토리 구조 -``` +```text tests/Playwright/ ├── fixtures/ -│ └── auth.ts (Sanctum 토큰 발급 + addInitScript 주입) +│ ├── auth.ts (Sanctum 토큰 발급 + addInitScript 주입) +│ ├── ecommerce-auth.ts (구매자 세션 fixture) +│ ├── layout-editor.ts (편집기 진입/저장 헬퍼) +│ ├── seed-layout.ts (시드 화면 준비/정리) +│ └── seed-layouts/ (시드 화면 원본 JSON) +├── global-setup.ts (시드 화면 설치) +├── global-teardown.ts (시드 화면 제거) ├── specs/ -│ ├── smoke/ (Phase 1 — 3건) -│ │ ├── homepage.spec.ts -│ │ ├── login.spec.ts -│ │ └── admin-dashboard.spec.ts -│ └── wysiwyg/ (Phase 2 — 90건, 위지윅 편집기 Phase M) -│ ├── auth-guard.spec.ts (12: access_outcome × user_permission) -│ ├── handler-suppression.spec.ts (6: suppressed_handler 축) -│ ├── anchor-intercept.spec.ts (45: anchor_kind × modifier_key) -│ └── url-template-param.spec.ts (27: url_template_param × anchor_kind) +│ ├── smoke/ (핵심 화면 — 14 파일) +│ ├── admin/ (관리자 화면 — 17 파일) +│ ├── layout-editor/ (레이아웃 편집기 — 46 파일) +│ ├── pagination/ (목록 페이지네이션 — 4 파일) +│ ├── search/ (검색 — 1 파일) +│ └── *.spec.ts (엔진/렌더링 회귀 — 14 파일) ├── .auth/ (gitignore — storageState 캐시) └── README.md (본 문서) ``` +`@smoke` 표기는 70곳(25개 파일)이며, 그중 1곳은 `test.describe` 단위라 `--grep @smoke` 로 +선택되는 테스트 수는 표기 수보다 많다. 파일·건수는 변하므로 판단이 필요할 때 직접 센다. + ## 코어/확장 분리 원칙 기존 PHPUnit / Vitest 와 동일 원칙. **모듈/플러그인/템플릿 E2E spec 을 코어 디렉토리에 작성 금지.** @@ -98,19 +114,20 @@ E2E spec 이 "특정 유저 + 특정 역할 + 특정 도메인 상황" 에서 | 플러그인 도메인 데이터 (결제 키 등) | 플러그인 | `plugins/_bundled/{id}/src/Console/Commands/PlaywrightSeed{id}.php` | | 외부 의존 (토스 결제창 응답 등) | spec 안 mock | `page.route('https://api.tosspayments.com/**', ...)` | -**핵심 원칙**: 코어는 모듈 도메인을 모른다. 모듈 도메인 시드 커맨드를 코어에 두면 코어↔모듈 의존이 뒤집힘 (audit 룰 위반). +**핵심 원칙**: 코어는 모듈 도메인을 모른다. 모듈 도메인 시드 커맨드를 코어에 두면 코어↔모듈 의존이 뒤집힘 (정적 검사 위반). ## 시나리오 매니페스트 ↔ spec 매핑 -| YAML axis | spec 파일 | 케이스 | -|---|---|---| -| `tests/scenarios/wysiwyg-editor-access-guard.yaml` `cross_product[0]` (access_outcome × user_permission) | `specs/wysiwyg/auth-guard.spec.ts` | 12 | -| 동일 yaml `cross_product[1]` (anchor_kind × modifier_key) | `specs/wysiwyg/anchor-intercept.spec.ts` | 45 | -| 동일 yaml `cross_product[2]` (suppressed_handler) | `specs/wysiwyg/handler-suppression.spec.ts` | 6 | -| 동일 yaml `cross_product[3]` (url_template_param × anchor_kind) | `specs/wysiwyg/url-template-param.spec.ts` | 27 | +시나리오 매니페스트(`tests/scenarios/*.yaml`)의 축과 spec 은 파일명으로 대응시킨다 — +`layout-editor-table-editor.yaml` ↔ `specs/layout-editor/table-editor.spec.ts` 처럼 +매니페스트 이름에서 도메인 접두어를 뗀 형태가 spec 파일명이 된다. -각 spec 은 `test.describe.parallel(axisName)` 으로 묶고 matrix 배열로 매개변수화. -케이스 docblock 에 `// @scenario k1=v1, k2=v2` / `// @effects e1, e2` 마킹. +각 spec 은 `test.describe.parallel(axisName)` 으로 묶고 matrix 배열로 매개변수화한다. +케이스 docblock 에 `// @scenario k1=v1, k2=v2` / `// @effects e1, e2` 를 마킹하면 +매니페스트의 `cross_product` · `effects` 와 자동으로 대조된다. + +`@scenario` 의 축 구분자는 **쉼표뿐이다.** `k1=v1 × k2=v2` 처럼 곱셈 기호로 적으면 +첫 축만 인식되고 나머지는 조용히 누락된다. ## 외부 의존 시나리오 — mock-first 패턴 (결제 등) @@ -156,7 +173,7 @@ await page.route('https://api.tosspayments.com/v1/payments/confirm', route => ro $env:PLAYWRIGHT_BASE_URL='https://g7.dev'; npm run test:e2e:ui # 특정 spec 만 실행 -PLAYWRIGHT_BASE_URL=https://g7.dev npx playwright test tests/Playwright/specs/wysiwyg/auth-guard.spec.ts +PLAYWRIGHT_BASE_URL=https://g7.dev npx playwright test tests/Playwright/specs/layout-editor/auth-guard-and-locales.spec.ts # trace 열기 npx playwright show-trace test-results/<...>/trace.zip @@ -167,6 +184,6 @@ Playwright 가 회귀를 발견하면 → Claude `chrome-devtools-mcp` 로 라 ## 참고 - 가이드: `docs/testing/e2e-testing.md` -- 시나리오 매니페스트: `tests/scenarios/wysiwyg-editor-access-guard.yaml` -- 단위 시뮬레이션 (Vitest): `resources/js/core/template-engine/wysiwyg/__tests__/previewNavigation.test.ts` 등 +- 시나리오 매니페스트: `tests/scenarios/` (레이아웃 편집기 축은 `layout-editor-*.yaml`) +- 단위 시뮬레이션 (Vitest): `resources/js/core/template-engine/__tests__/` - 모듈 sample skeleton: `modules/_bundled/sirsoft-ecommerce/tests/Playwright/README.md` diff --git a/tests/Playwright/specs/admin/admin-trailing-slash-route.spec.ts b/tests/Playwright/specs/admin/admin-trailing-slash-route.spec.ts index 707ec5ca..468a1a1b 100644 --- a/tests/Playwright/specs/admin/admin-trailing-slash-route.spec.ts +++ b/tests/Playwright/specs/admin/admin-trailing-slash-route.spec.ts @@ -10,8 +10,8 @@ * 단위 테스트(Router.test.ts)가 매칭 로직을 잠그지만, 실제 브라우저에서 * 재현한 증상이므로(단위 green + 브라우저 깨짐 방지) 브라우저 레벨로도 잠근다. * - * @scenario admin_trailing_slash_unauthenticated + admin_no_trailing_slash_unauthenticated - * @effects redirect_to_login_not_404 + * 축 요약(마커 아님 — 평문): admin_trailing_slash_unauthenticated, admin_no_trailing_slash_unauthenticated. + * 효과 요약(마커 아님 — 평문): redirect_to_login_not_404. */ import { test, expect } from '../../fixtures/auth'; diff --git a/tests/Playwright/specs/admin/list-bulk-selection-scope.spec.ts b/tests/Playwright/specs/admin/list-bulk-selection-scope.spec.ts new file mode 100644 index 00000000..1149a879 --- /dev/null +++ b/tests/Playwright/specs/admin/list-bulk-selection-scope.spec.ts @@ -0,0 +1,136 @@ +/** + * E2E: 일괄 처리 목록의 선택 범위 (selectionScope) + * + * 체크박스 선택은 화면 밖(전역 상태)에 저장되므로, 검색으로 그 행이 목록에서 빠져도 + * 선택은 남는다. 그 상태에서 일괄 처리를 누르면 **운영자가 보고 있지도, 체크하지도 않은 + * 행이 대상**이 된다. 확인 창은 건수만 말하고("N명의 사용자를 탈퇴 처리합니다") 대상을 + * 부르지 않으므로 실행 전에 알아챌 방법이 없고, 서버는 정상 처리하므로 오류도 남지 않는다. + * + * 관측 가능한 결과는 **나가는 요청의 ids** 다 — 화면만 봐서는 구분되지 않으므로 여기서 + * 잠근다. 단위 테스트(DataGrid.test.tsx)는 컴포넌트가 정리를 수행하는지를 보고, 이 spec 은 + * 그 정리가 실제 화면·실제 전역 상태·실제 요청까지 도달하는지를 본다. + * + * // @scenario surface=admin_user_list, transition=search_filter_change, scope=page + * // @effects hidden_row_selection_dropped, bulk_button_disabled_when_selection_empties, bulk_request_carries_only_visible_checked_rows + */ +import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; + +const ADMIN_PERMISSIONS = ['core.users.read', 'core.users.update', 'core.users.create']; + +/** 이 실행에서만 쓰는 접두사 — 검색으로 한 명씩 고립시키기 위해 필요하다. */ +const RUN_TAG = `e2esel${Date.now().toString(36)}`; + +/** + * 테스트용 회원을 만들고 uuid 를 돌려줍니다. + * + * @param page Playwright 페이지 + * @param token 관리자 토큰 + * @param slug 이메일 로컬파트 구분자 + * @return 생성된 회원 uuid + */ +async function createTestUser(page: any, token: string, slug: string): Promise<string> { + const headers = { Authorization: `Bearer ${token}`, Accept: 'application/json' }; + + const rolesResponse = await page.request.get('/api/admin/roles/active', { headers }); + const roles = (await rolesResponse.json())?.data?.data ?? []; + const roleId = (roles.find((role: any) => role.identifier === 'user') ?? roles[roles.length - 1])?.id; + + expect(roleId, '일반 사용자 역할을 찾지 못했습니다').toBeTruthy(); + + const response = await page.request.post('/api/admin/users', { + headers, + data: { + name: `E2E선택범위 ${slug}`, + email: `${RUN_TAG}${slug}@example.com`, + password: 'Password!234', + password_confirmation: 'Password!234', + status: 'active', + role_ids: [roleId], + }, + }); + + expect(response.status(), `테스트 회원 생성 실패: ${await response.text()}`).toBeLessThan(300); + + return (await response.json())?.data?.uuid; +} + +/** + * 화면의 검색창으로 목록을 걸러냅니다. + * + * `page.goto` 로 URL 을 갈아끼우면 전체 페이지가 다시 로드되면서 전역 상태가 통째로 + * 비워진다 — 그러면 선택이 사라진 이유가 "컴포넌트가 정리해서" 인지 "페이지가 새로 + * 뜨면서" 인지 구분되지 않고, 수정이 없어도 통과하는 테스트가 된다. 결함이 실제로 + * 관측된 경로는 앱이 떠 있는 채로 목록만 다시 부르는 화면 내 검색이므로 그 경로를 쓴다. + * + * @param page Playwright 페이지 + * @param keyword 검색어 + * @return void + */ +async function searchUserList(page: any, keyword: string): Promise<void> { + const searchInput = page.locator('#search_input'); + await searchInput.waitFor({ state: 'visible', timeout: 20_000 }); + await searchInput.fill(keyword); + await searchInput.press('Enter'); + + await expect(page.locator('#users_data_grid__body tbody tr')).toHaveCount(1, { timeout: 20_000 }); + await expect(page.locator('#users_data_grid__body tbody tr').first()).toContainText(keyword, { + timeout: 20_000, + }); +} + +/** + * 행 체크박스를 켜고 실제로 켜졌는지 확인합니다. + * + * `check()` 는 클릭 직후 상태를 확인하는데, 이 체크박스는 전역 상태를 거쳐 다시 내려오는 + * 제어 컴포넌트라 그 시점에 아직 false 다 (실측). 클릭과 확인을 분리한다. + * + * @param page Playwright 페이지 + * @return void + */ +async function checkFirstRow(page: any): Promise<void> { + const checkbox = page.locator('#users_data_grid__body tbody tr input[type="checkbox"]').first(); + await checkbox.click(); + await expect(checkbox).toBeChecked({ timeout: 10_000 }); +} + +test('@smoke 회원 목록: 검색으로 사라진 행은 선택이 풀리고 일괄 처리 대상에서도 빠진다', async ({ page }) => { + const token = issueToken(...ADMIN_PERMISSIONS); + await authenticatePage(page, token); + + const hiddenUserUuid = await createTestUser(page, token, 'hidden'); + const visibleUserUuid = await createTestUser(page, token, 'visible'); + + // 1) hidden 만 보이는 목록에서 그 행을 체크한다. + await page.goto('/admin/users'); + await searchUserList(page, `${RUN_TAG}hidden`); + await checkFirstRow(page); + + const bulkWithdraw = page.locator('#bulk_withdraw'); + await expect(bulkWithdraw).toBeEnabled({ timeout: 10_000 }); + + // 2) 앱이 떠 있는 채로 검색만 바꾼다 — hidden 은 화면에서 사라지지만 전역 상태는 살아 있다. + await searchUserList(page, `${RUN_TAG}visible`); + + // 사라진 행의 선택이 남아 있으면 버튼이 살아 있다. 그 상태가 결함이다. + await expect(bulkWithdraw).toBeDisabled({ timeout: 20_000 }); + + // 3) 보이는 행만 체크하고 일괄 탈퇴를 실행한다. + await checkFirstRow(page); + await expect(bulkWithdraw).toBeEnabled({ timeout: 10_000 }); + + const bulkRequest = page.waitForRequest( + (request: any) => request.url().includes('/api/admin/users/bulk-status') && request.method() === 'PATCH' + ); + + await bulkWithdraw.click(); + + // 확인 모달의 실행 버튼은 "확인"(공통 문구)이다 — 모달이 뜬 뒤에 누른다. + const confirmButton = page.getByRole('button', { name: /^(확인|Confirm)$/ }).last(); + await expect(confirmButton).toBeVisible({ timeout: 10_000 }); + await confirmButton.click(); + + const sentIds = (await (await bulkRequest).postDataJSON())?.ids ?? []; + + expect(sentIds, '화면에 보이던 행이 대상에서 빠졌습니다').toContain(visibleUserUuid); + expect(sentIds, '화면에서 사라진 행이 일괄 처리 대상에 실렸습니다').not.toContain(hiddenUserUuid); +}); diff --git a/tests/Playwright/specs/admin/settings-drivers-s3-endpoint.spec.ts b/tests/Playwright/specs/admin/settings-drivers-s3-endpoint.spec.ts new file mode 100644 index 00000000..7c26aa35 --- /dev/null +++ b/tests/Playwright/specs/admin/settings-drivers-s3-endpoint.spec.ts @@ -0,0 +1,192 @@ +/** + * E2E: 환경설정 > 드라이버 — S3 리전 자유입력·엔드포인트·path-style (#99/#563) + * + * 시나리오 매니페스트: tests/scenarios/s3-storage-driver.yaml — 마킹은 각 테스트의 + * scenario(k=v 조합)·effects 주석이 담당하며, 헤더 요약 마킹은 파서 형식이 아니다 + * + * 배경: S3 리전이 5개 하드코딩 Select 라 Cloudflare R2(auto)·신규 AWS 리전을 입력할 수 + * 없었고, S3 호환 스토리지의 API 주소(endpoint)를 넣을 자리가 없어 s3_url(공개 URL)에 + * 잘못 넣는 혼란이 있었다. 리전 Input 전환 + endpoint/path-style 신설을 브라우저에서 + * 확인한다. + * + * 검증: + * 1. s3 선택 시 리전이 자유 입력 Input(Select 아님)·endpoint Input·path-style Toggle 이 + * 번역문과 함께 마운트된다 + * 2. 불량 형식 리전은 저장 버튼 클릭이 422 로 거부되고 리전 필드에 inline 에러가 + * 표시된다 (s3 저장 게이트가 요구하는 연결 테스트만 route 스텁으로 성공 처리 — + * 외부 S3 실연결이 E2E 환경에 없기 때문이며, 저장 요청·에러 렌더는 전부 실경로) + * 3. local 드라이버 저장 왕복(외부 연결 불요 경로)이 신규 키를 보존한다 + */ +import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; + +/** 관리자 환경설정 드라이버 탭 진입 + s3 선택 */ +async function gotoDriversTabWithS3(page: import('@playwright/test').Page): Promise<void> { + await page.goto('/admin/settings?tab=drivers'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + // 템플릿 Select 는 options 모드에서 네이티브 select 가 아니라 + // 커스텀 드롭다운(button[aria-haspopup=listbox] + role=option)을 렌더한다. + const storageSelect = page.locator('[name="drivers.storage_driver"]').first(); + await expect(storageSelect).toBeAttached({ timeout: 20_000 }); + await storageSelect.locator('button[aria-haspopup="listbox"]').first().click(); + await page.getByRole('option', { name: 'Amazon S3' }).click(); + await expect(page.locator('#s3_settings')).toBeAttached({ timeout: 10_000 }); +} + +// @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default +// @effects region_free_input_mounted +test('@smoke #99 - s3 선택 시 리전이 자유 입력 Input 으로 마운트된다 (Select 아님)', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await gotoDriversTabWithS3(page); + expect(page.url()).not.toMatch(/\/admin\/login/); + + // 존재 확정: 리전 필드는 자유 입력 input 이다 (종전에는 커스텀 드롭다운이라 input 자체가 없었다) + const regionInput = page.locator('input[name="drivers.s3_region"]').first(); + await expect(regionInput).toBeAttached(); + + // 부재 단언: 종전 리전 드롭다운(커스텀 Select 루트)이 제거되었다 + await expect(page.locator('[name="drivers.s3_region"] button[aria-haspopup="listbox"]')).toHaveCount(0); + + // 자유 입력이 실제로 동작한다 (R2 표준값) + await regionInput.fill('auto'); + await expect(regionInput).toHaveValue('auto'); +}); + +// @scenario region_shape=custom_minio,endpoint=path_style,driver_usability=adapter_present,attachment_disk=local_default +// @effects endpoint_field_mounted, path_style_toggle_mounted +test('#99 - endpoint Input 과 path-style Toggle 이 번역문과 함께 마운트된다', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await gotoDriversTabWithS3(page); + + const endpointField = page.locator('#s3_endpoint_field'); + await expect(endpointField).toBeAttached(); + await expect(endpointField.locator('input[name="drivers.s3_endpoint"]')).toBeAttached(); + + const toggleField = page.locator('#s3_use_path_style_field'); + await expect(toggleField).toBeAttached(); + await expect(toggleField.locator('input[type="checkbox"]').first()).toBeAttached(); + + // 다국어 키 미해석 회귀 가드 — s3 블록 전체에 "$t:" 원문이 노출되면 안 된다 + const blockText = (await page.locator('#s3_settings').innerText()).trim(); + expect(blockText).not.toContain('$t:'); + expect(blockText.length).toBeGreaterThan(0); +}); + +// @scenario region_shape=invalid_format,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default +// @effects invalid_region_rejected +test('#99 - 불량 형식 리전은 저장 클릭이 422 로 거부되고 리전 필드에 오류가 표시된다', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + // s3 저장 게이트는 연결 테스트 성공을 요구한다. 외부 S3 실연결이 E2E 환경에 + // 없으므로 연결 테스트 응답만 성공으로 스텁한다 — 저장 요청 자체는 실서버로 + // 나가 422 계약과 inline 에러 렌더를 실경로로 검증한다. + await page.route('**/api/admin/settings/test-driver', (route) => + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + success: true, + data: { results: { s3: { success: true, message: 'stubbed' } } }, + }), + }) + ); + + await gotoDriversTabWithS3(page); + + // s3 필드 입력 — 불량 형식 리전 포함 (대문자·특수문자) + await page.locator('input[name="drivers.s3_bucket"]').fill('bucket'); + await page.locator('input[name="drivers.s3_region"]').fill('S3!bad'); + await page.locator('input[name="drivers.s3_access_key"]').fill('key'); + await page.locator('input[name="drivers.s3_secret_key"]').fill('secret'); + + // 연결 테스트(스텁 성공) → 저장 게이트 해제 → 저장 버튼 활성 전이 + await page.locator('#s3_test_section button').click(); + const saveButton = page.locator('#save_button'); + await expect(saveButton).toBeEnabled({ timeout: 10_000 }); + + // 저장 클릭 → 실서버 422 + const [saveResponse] = await Promise.all([ + page.waitForResponse( + (r) => + r.request().method() === 'POST' && + r.url().includes('/api/admin/settings') && + !r.url().includes('test-driver') + ), + saveButton.click(), + ]); + expect(saveResponse.status()).toBe(422); + + // 서버가 돌려준 리전 오류 문구가 리전 필드 inline 에러로 렌더된다 + const body = await saveResponse.json(); + const regionMessage = body?.errors?.['drivers.s3_region']?.[0]; + expect(regionMessage).toBeTruthy(); + await expect(page.locator('input[name="drivers.s3_region"]')).toHaveClass(/input-error/); + await expect(page.locator('#s3_settings')).toContainText(regionMessage); +}); + +// @scenario region_shape=auto,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default +// @effects local_roundtrip_preserves_new_keys +test('#99 - local 드라이버 저장 왕복이 신규 키(s3_endpoint/path-style)를 보존한다', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await page.goto('/admin/settings?tab=drivers'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + const result = await page.evaluate(async (bearer) => { + const headers = { + Authorization: `Bearer ${bearer}`, + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + // 현재 값을 읽어 그대로 되돌려 놓는 왕복 — 검수 서버 상태를 바꾸지 않는다 + const before = await ( + await fetch('/api/admin/settings', { headers }) + ).json(); + const drivers = before?.data?.drivers ?? {}; + + const save = await fetch('/api/admin/settings', { + method: 'POST', + headers, + body: JSON.stringify({ + _tab: 'drivers', + drivers: { + ...drivers, + storage_driver: 'local', + s3_region: 'auto', + s3_endpoint: 'http://127.0.0.1:9000', + s3_use_path_style: true, + }, + }), + }); + + const after = await ( + await fetch('/api/admin/settings', { headers }) + ).json(); + + // 원상 복구 (검수 서버 상태 보존) + await fetch('/api/admin/settings', { + method: 'POST', + headers, + body: JSON.stringify({ _tab: 'drivers', drivers }), + }); + + return { + saveStatus: save.status, + savedEndpoint: after?.data?.drivers?.s3_endpoint, + savedPathStyle: after?.data?.drivers?.s3_use_path_style, + savedRegion: after?.data?.drivers?.s3_region, + }; + }, token); + + expect(result.saveStatus).toBe(200); + expect(result.savedRegion).toBe('auto'); + expect(result.savedEndpoint).toBe('http://127.0.0.1:9000'); + expect(Boolean(result.savedPathStyle)).toBe(true); +}); diff --git a/tests/Playwright/specs/admin/settings-upload-orphan-cleanup.spec.ts b/tests/Playwright/specs/admin/settings-upload-orphan-cleanup.spec.ts new file mode 100644 index 00000000..c30fba0a --- /dev/null +++ b/tests/Playwright/specs/admin/settings-upload-orphan-cleanup.spec.ts @@ -0,0 +1,99 @@ +/** + * E2E: 환경설정 > 업로드 — "고아 첨부 정리" 카드 (공개 #115) + * + * @scenario admin_settings_upload_orphan_cleanup + * @effects core_settings_orphan_toggle_default_off, core_settings_orphan_keys_declared + * + * 배경: 폼을 저장하지 않고 나가 소유자 없이 남은 첨부를 매일 파기하는 기능의 운영자 스위치다. + * 사용자 파일을 실제로 지우는 설정이라 "기본 꺼짐" 이 인수 기준이고, 그 상태는 서버 기본값과 + * 화면 렌더가 함께 맞아야 성립한다. 단위 테스트(admin-settings-orphan-cleanup)는 레이아웃 JSON + * 의 바인딩 키만 검증하므로, 실제 값이 실려 렌더되는지는 브라우저로만 확인된다. + * + * 검증: + * 1. 업로드 탭에 카드가 마운트되고 항목명·설명이 raw 키가 아닌 번역문으로 표시된다 + * 2. 자동 정리 토글이 기본 꺼짐으로 보인다 (설정을 만지지 않은 사이트의 인수 기준) + * 3. 보존기간에 0 을 넣고 저장하면 422 로 거절되고, 안내 문구에 항목명이 사람 말로 나온다 + */ +import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; + +const CARD = '#card_orphan_cleanup'; +const TOGGLE_ROW = '#field_orphan_cleanup_enabled'; +const RETENTION_ROW = '#field_orphan_retention_days'; + +/** 관리자 환경설정 업로드 탭 진입 */ +async function gotoUploadTab(page: import('@playwright/test').Page): Promise<void> { + await page.goto('/admin/settings?tab=upload'); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + await expect(page.locator(CARD)).toBeAttached({ timeout: 20_000 }); +} + +// @scenario tab=upload, permitted=yes +// @effects core_settings_orphan_keys_declared +test('#115 - 업로드 탭에 "고아 첨부 정리" 카드가 번역문과 함께 마운트된다', async ({ page }) => { + const consoleErrors: string[] = []; + page.on('console', (message) => { + if (message.type() === 'error') consoleErrors.push(message.text()); + }); + + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await gotoUploadTab(page); + expect(page.url()).not.toMatch(/\/admin\/login/); + + const card = page.locator(CARD); + const text = (await card.innerText()).trim(); + + // 다국어 키가 해석되지 않으면 "$t:admin.settings.upload.orphan_..." 원문이 그대로 노출된다. + expect(text).not.toContain('$t:'); + expect(text).not.toContain('orphan_cleanup'); + expect(text.length).toBeGreaterThan(0); + + await expect(page.locator(TOGGLE_ROW)).toBeAttached(); + await expect(page.locator(RETENTION_ROW)).toBeAttached(); + + expect(consoleErrors).toEqual([]); +}); + +// @scenario tab=upload, permitted=yes +// @effects core_settings_orphan_toggle_default_off +test('#115 - 자동 정리 토글은 기본 꺼짐으로 렌더된다', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await gotoUploadTab(page); + + const toggle = page.locator(TOGGLE_ROW).locator('input[type="checkbox"]').first(); + await expect(toggle).toBeAttached(); + + // 설정을 만지지 않은 사이트에서 자동 삭제 0 이 인수 기준이다 — 화면도 그 상태를 보여야 한다. + expect(await toggle.isChecked()).toBe(false); +}); + +// @scenario tab=upload, permitted=yes, value=invalid +// @effects core_settings_orphan_keys_declared +test('#115 - 보존기간 0 은 항목명이 드러난 안내와 함께 거절된다', async ({ page }) => { + const token = issueToken('core.settings.read', 'core.settings.update'); + await authenticatePage(page, token); + + await gotoUploadTab(page); + + const input = page.locator(RETENTION_ROW).locator('input').first(); + await input.fill('0'); + + const response = page.waitForResponse( + (res) => res.url().includes('/api/admin/settings') && res.request().method() === 'POST', + { timeout: 20_000 }, + ); + + await page.getByRole('button', { name: /저장|Save/ }).first().click(); + + const res = await response; + expect(res.status()).toBe(422); + + const body = await res.json(); + const message = JSON.stringify(body); + + // 원시 키(`upload.orphan retention days`)가 그대로 나오면 attributes 정의가 빠진 것이다. + expect(message).not.toContain('orphan retention days'); +}); diff --git a/tests/Playwright/specs/admin/user-form-withdraw-confirm.spec.ts b/tests/Playwright/specs/admin/user-form-withdraw-confirm.spec.ts new file mode 100644 index 00000000..fcd24d11 --- /dev/null +++ b/tests/Playwright/specs/admin/user-form-withdraw-confirm.spec.ts @@ -0,0 +1,190 @@ +/** + * 관리자 회원 수정 폼 — 상태 '탈퇴' 저장 확인 절차 (공개이슈 #112) + * + * 서버에서 관리자 상태 변경 경로가 정식 탈퇴(비가역 익명화)로 통일되었으므로, + * 이 폼의 저장이 경고 없이 회원 정보를 파기하지 않아야 한다. + * + * // @scenario entry_point=admin_form_ui, target_account=normal, confirm_action=cancel, confirm_action=submit + * // @effects admin_form_shows_confirm_dialog_before_destructive_save, admin_form_cancel_leaves_user_untouched, admin_form_non_withdraw_status_saves_without_dialog + */ +import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; + +const ADMIN_PERMISSIONS = ['core.users.read', 'core.users.update', 'core.users.create']; + +/** 테스트용 회원을 생성하고 uuid 를 돌려준다. */ +async function createTestUser(page: any, token: string, email: string): Promise<string> { + const headers = { Authorization: `Bearer ${token}`, Accept: 'application/json' }; + + // 역할은 필수 입력이다. 관리자 역할을 붙이면 탈퇴 자체가 차단 대상이 되므로 + // 일반 사용자 역할을 고른다 (목록 첫 항목은 admin 이다). + const rolesResponse = await page.request.get('/api/admin/roles/active', { headers }); + const roles = (await rolesResponse.json())?.data?.data ?? []; + const roleId = (roles.find((role: any) => role.identifier === 'user') ?? roles[roles.length - 1])?.id; + + expect(roleId, '일반 사용자 역할을 찾지 못했습니다').toBeTruthy(); + + const response = await page.request.post('/api/admin/users', { + headers, + data: { + name: 'E2E 탈퇴확인', + email, + password: 'Password!234', + password_confirmation: 'Password!234', + status: 'active', + role_ids: [roleId], + }, + }); + + expect(response.status(), `테스트 회원 생성 실패: ${await response.text()}`).toBeLessThan(300); + + const body = await response.json(); + + return body?.data?.uuid ?? body?.data?.id; +} + + +/** 상태 드롭다운에서 라벨을 고르고, 선택이 폼에 반영될 때까지 기다린다. */ +async function selectStatus(page: any, label: RegExp): Promise<void> { + const statusSelect = page.locator('[name="status"][aria-haspopup], [name="status"]').first(); + await statusSelect.waitFor({ state: 'visible', timeout: 20_000 }); + + // 폼이 API 응답으로 시드될 때까지 기다린다 — 시드 전에 저장하면 필수 항목이 빠져 422 가 된다. + await expect(page.locator('[name="email"]')).not.toHaveValue('', { timeout: 20_000 }); + + await statusSelect.click(); + await page.getByRole('option', { name: label }).first().click(); + await expect(statusSelect).toContainText(label, { timeout: 10_000 }); +} + +test('@smoke 관리자 회원 수정 폼: 상태 탈퇴 저장은 확인을 거치고, 취소하면 회원 정보가 그대로 남는다', async ({ page }) => { + const token = issueToken(...ADMIN_PERMISSIONS); + await authenticatePage(page, token); + + const email = `g7fix112.e2e.${Date.now()}@example.com`; + const uuid = await createTestUser(page, token, email); + + await page.goto(`/admin/users/${uuid}/edit`); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + expect(page.url(), '로그인으로 리다이렉트되었습니다').not.toMatch(/\/admin\/login/); + + await selectStatus(page, /탈퇴|Withdraw/i); + + // 저장 → 확인 다이얼로그가 먼저 뜨고, 저장 요청은 나가지 않는다. + let saveRequests = 0; + page.on('request', (req) => { + if (req.url().includes(`/api/admin/users/${uuid}`) && req.method() === 'PUT') { + saveRequests += 1; + } + }); + + await page.locator('#footer_save_button').click(); + + const dialog = page.getByRole('dialog'); + await dialog.waitFor({ state: 'visible', timeout: 10_000 }); + + // 문구가 비가역성과 파기 대상을 알린다 + await expect(dialog).toContainText(/되돌릴 수 없|cannot be undone/i); + await expect(dialog).toContainText(/익명화|anonymiz/i); + + expect(saveRequests, '확인 전에 저장 요청이 나갔습니다').toBe(0); + + // 취소 → 회원 정보 불변 + await dialog.getByRole('button').first().click(); + await expect(page.getByRole('dialog')).toHaveCount(0, { timeout: 10_000 }); + expect(saveRequests, '취소했는데 저장 요청이 나갔습니다').toBe(0); + + const stillOriginal = await page.request.get(`/api/admin/users/${uuid}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + const stillBody = await stillOriginal.json(); + expect(stillBody?.data?.email, '취소했는데 이메일이 바뀌었습니다').toBe(email); + + // 재시도 후 확인 → 저장되고 익명화가 반영된다 + // 다이얼로그 언마운트 직후의 재클릭은 낡은 폼 스냅샷으로 제출될 수 있어 렌더가 가라앉기를 기다린다 + await page.waitForTimeout(1500); + await page.locator('#footer_save_button').click(); + await expect(page.getByRole('dialog')).toHaveCount(1, { timeout: 15_000 }); + + const reopened = page.getByRole('dialog'); + + const savePromise = page.waitForResponse( + (res) => res.url().includes(`/api/admin/users/${uuid}`) && res.request().method() === 'PUT', + { timeout: 20_000 }, + ); + await reopened.getByRole('button').last().click(); + const saveResponse = await savePromise; + + expect(saveResponse.status(), '탈퇴 저장이 실패했습니다').toBe(200); + + const after = await page.request.get(`/api/admin/users/${uuid}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + const afterBody = await after.json(); + + expect(afterBody?.data?.status).toBe('withdrawn'); + expect(afterBody?.data?.email, '탈퇴했는데 이메일이 익명화되지 않았습니다').not.toBe(email); + expect(String(afterBody?.data?.email)).toContain('_deleted_'); +}); + +test('관리자 회원 수정 폼: 탈퇴 외 상태 저장은 확인 없이 즉시 저장된다', async ({ page }) => { + const token = issueToken(...ADMIN_PERMISSIONS); + await authenticatePage(page, token); + + const email = `g7fix112.e2e.keep.${Date.now()}@example.com`; + const uuid = await createTestUser(page, token, email); + + await page.goto(`/admin/users/${uuid}/edit`); + await page.waitForLoadState('domcontentloaded', { timeout: 30_000 }); + + await selectStatus(page, /비활성|Inactive/i); + + const savePromise = page.waitForResponse( + (res) => res.url().includes(`/api/admin/users/${uuid}`) && res.request().method() === 'PUT', + { timeout: 20_000 }, + ); + await page.locator('#footer_save_button').click(); + const saveResponse = await savePromise; + + expect(saveResponse.status()).toBe(200); + + // 저장이 확정된 뒤에 다이얼로그 부재를 단언한다 (부재 단언은 렌더 전에도 통과한다) + await expect(page.getByRole('dialog')).toBeHidden(); + + const after = await page.request.get(`/api/admin/users/${uuid}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + const afterBody = await after.json(); + + expect(afterBody?.data?.status).toBe('inactive'); + expect(afterBody?.data?.email, '탈퇴가 아닌데 이메일이 익명화되었습니다').toBe(email); +}); + +test('관리자 회원 수정 폼: 회원 정보가 채워지기 전에는 저장할 수 없다', async ({ page }) => { + const token = issueToken(...ADMIN_PERMISSIONS); + await authenticatePage(page, token); + + const email = `g7fix112.e2e.seed.${Date.now()}@example.com`; + const uuid = await createTestUser(page, token, email); + + const saveRequests: string[] = []; + page.on('request', (req) => { + if (req.method() === 'PUT' && req.url().includes(`/api/admin/users/${uuid}`)) { + saveRequests.push(String(req.postData()).slice(0, 80)); + } + }); + + await page.goto(`/admin/users/${uuid}/edit`); + + // 버튼이 보이자마자 누른다 — 폼이 API 응답으로 채워지기 전 시점이다. + const save = page.locator('#footer_save_button'); + await save.waitFor({ state: 'visible', timeout: 20_000 }); + await save.click({ force: true }).catch(() => undefined); + await page.waitForTimeout(2_000); + + // 빈 본문 저장이 나가면 화면에는 값이 보이는데 "이름은 필수입니다" 로 거절된다. + expect(saveRequests, `시드 전에 저장 요청이 나갔습니다: ${saveRequests.join(' | ')}`).toHaveLength(0); + + // 시드가 끝나면 정상적으로 저장할 수 있다 (버튼이 영구 비활성으로 굳지 않는다). + await expect(page.locator('[name="email"]')).toHaveValue(email, { timeout: 20_000 }); + await expect(save).toBeEnabled({ timeout: 10_000 }); +}); diff --git a/tests/Playwright/specs/datagrid-cellchildren-pipe.spec.ts b/tests/Playwright/specs/datagrid-cellchildren-pipe.spec.ts index 9ffd9b05..50968cc4 100644 --- a/tests/Playwright/specs/datagrid-cellchildren-pipe.spec.ts +++ b/tests/Playwright/specs/datagrid-cellchildren-pipe.spec.ts @@ -13,8 +13,8 @@ * 잠근다 — 셀에 파이프를 넣은 화면이 코어 기본 레이아웃에 없으므로, 배포된 * 번들의 `G7Core.renderItemChildren` 를 그대로 호출해 결과를 검증한다. * - * @scenario cellchildren_pipe_with_args + cellchildren_pipe_without_args + cellchildren_props_pipe - * @effects pipe_formatted_value_rendered + * 축 요약(마커 아님 — 평문): cellchildren_pipe_with_args, cellchildren_pipe_without_args, cellchildren_props_pipe. + * 효과 요약(마커 아님 — 평문): pipe_formatted_value_rendered. */ import { test, expect } from '../fixtures/auth'; diff --git a/tests/Playwright/specs/layout-editor/action-flex-condition.spec.ts b/tests/Playwright/specs/layout-editor/action-flex-condition.spec.ts index 1f69ba0f..78ca384f 100644 --- a/tests/Playwright/specs/layout-editor/action-flex-condition.spec.ts +++ b/tests/Playwright/specs/layout-editor/action-flex-condition.spec.ts @@ -11,8 +11,8 @@ * 이라, 본 spec 은 탭 노출/빌더 렌더/조작 결과의 가시 상태만 확인하고 합성 로직 정합성은 * 단위/RTL(actionRecipeEngine/conditionRecipeEngine/s7-property-controls)이 잠근다. * - * @scenario property_modal_action_tab + property_modal_visibility_tab + flex_editor_section - * @effects action_tab_visible_when_events + visibility_builder_renders + flex_section_for_container + * 축 요약(마커 아님 — 평문): property_modal_action_tab, property_modal_visibility_tab, flex_editor_section. + * 효과 요약(마커 아님 — 평문): action_tab_visible_when_events, visibility_builder_renders, flex_section_for_container. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -64,6 +64,7 @@ async function selectNode(page: PwPage, editorPath: string): Promise<boolean> { } test.describe('@layout-editor 동작/표시조건/정렬박스 (S7)', () => { + /** @effects visibility_builder_renders */ test('요소 선택 → 속성 모달에 표시조건 탭 + 조건 빌더 렌더', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -128,7 +129,7 @@ test.describe('@layout-editor 동작/표시조건/정렬박스 (S7)', () => { // ── D-D 회귀 — 컴포넌트 [동작] 탭 ── // @scenario property_modal_action_tab - // @effects nested_onsuccess_picker_not_empty + action_card_drag_handle_draggable + // @effects nested_onsuccess_picker_not_empty, action_card_drag_handle_draggable /** [동작] 탭을 가진(events capability) 노드를 찾아 동작 탭을 연다. 못 찾으면 null. * Button 등 onClick capability 보유 가능성이 높은 노드를 우선 시도한다(앞쪽 컨테이너는 @@ -163,6 +164,7 @@ test.describe('@layout-editor 동작/표시조건/정렬박스 (S7)', () => { return false; } + /** @effects action_tab_visible_when_events */ test('동작 탭 — apiCall 의 성공 시 동작(중첩) add picker 가 코어 핸들러를 노출(빈 상태 금지)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/action-reorder-drag.spec.ts b/tests/Playwright/specs/layout-editor/action-reorder-drag.spec.ts index 7e7a693d..5f446c00 100644 --- a/tests/Playwright/specs/layout-editor/action-reorder-drag.spec.ts +++ b/tests/Playwright/specs/layout-editor/action-reorder-drag.spec.ts @@ -14,7 +14,7 @@ * 결선돼 Playwright dispatchEvent 로 검증 가능하다(캔버스 노드 재배치의 dnd-kit 비호환과 별개). * * @scenario page_settings_init_actions_reorder - * @effects reorder_buttons_removed + drag_handle_reorders + dropline_shows_target + * 효과 요약(마커 아님 — 평문): reorder_buttons_removed, drag_handle_reorders, dropline_shows_target. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -82,6 +82,7 @@ async function cardTitles(page: PwPage): Promise<string[]> { } test.describe('@layout-editor 화면 동작 순서 변경 드래그 (S10-1 후속)', () => { + /** @effects reorder_buttons_removed */ test('순서 변경 ▲▼ 버튼 부재 + ⠿ 드래그 핸들 존재 + 삽입선 비활성(초기)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -98,6 +99,7 @@ test.describe('@layout-editor 화면 동작 순서 변경 드래그 (S10-1 후 await expect(page.locator('[data-testid="g7le-init-action-self-dropline-end"]')).toHaveAttribute('data-active', 'false'); }); + /** @effects drag_handle_reorders, dropline_shows_target */ test('⠿ 드래그로 순서 변경 + 드롭 예정 지점 삽입선 활성', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -138,6 +140,7 @@ test.describe('@layout-editor 화면 동작 순서 변경 드래그 (S10-1 후 await expect(page.locator('[data-testid="g7le-init-action-self-dropline-end"]')).toHaveAttribute('data-active', 'false'); }); + /** @effects drag_handle_reorders */ test('연속 드래그 — 누적 순서 정합(다시 원래 순서로)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/array-editor.spec.ts b/tests/Playwright/specs/layout-editor/array-editor.spec.ts index 037e6e5a..3b1fa69e 100644 --- a/tests/Playwright/specs/layout-editor/array-editor.spec.ts +++ b/tests/Playwright/specs/layout-editor/array-editor.spec.ts @@ -14,8 +14,8 @@ * 을 검증한다. 추가/삭제/정렬/필드편집/디그레이드 회귀는 단위(ArrayItemsEditor.test.tsx)가 * 잠그고, 본 E2E 는 라이브 반영·저장을 브라우저로 확인한다. * - * @scenario array_node_editor + add_item + edit_text_field + live_persist - * @effects property_modal_dispatches_array_node_editor_in_props_tab_by_kind, add_item_appends_newitem_skeleton_patches_whole_node, text_field_updates_item_immediately, live_add_tab_edit_label_reorder_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout + * 축 요약(마커 아님 — 평문): array_node_editor, add_item, edit_text_field, live_persist. + * 효과 요약(마커 아님 — 평문): property_modal_dispatches_array_node_editor_in_props_tab_by_kind, add_item_appends_newitem_skeleton_patches_whole_node, text_field_updates_item_immediately, live_add_tab_edit_label_reorder_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -122,6 +122,7 @@ async function appendTabNavigationTo(page: Page, containerPath: string): Promise } test.describe('@layout-editor array 노드 에디터(빌트인 ARRAY-PROP)', () => { + /** @effects property_modal_dispatches_array_node_editor_in_props_tab_by_kind, add_item_appends_newitem_skeleton_patches_whole_node */ test('TabNavigation 선택 시 array 에디터가 속성 탭에 마운트되고 항목 추가가 행 반영', async ({ page }) => { await gotoEditor(page); const navPath = await addTabNavigation(page); @@ -137,6 +138,7 @@ test.describe('@layout-editor array 노드 에디터(빌트인 ARRAY-PROP)', () expect(rowsAfter).toBe(rowsBefore + 1); }); + /** @effects text_field_updates_item_immediately */ test('항목 id text 필드 편집이 즉시 반영(입력값 유지)', async ({ page }) => { await gotoEditor(page); const navPath = await addTabNavigation(page); @@ -158,6 +160,7 @@ test.describe('@layout-editor array 노드 에디터(빌트인 ARRAY-PROP)', () // 시드 화면(e2e_sandbox)을 대상으로 한다. 이전에는 "추가한 TabNavigation 삭제 후 재저장" 으로 // 원복했으나, 원복 자체가 또 한 번의 저장이라 실패하면 잔여물이 남았다. 시드 화면은 globalSetup 이 // 매 실행 fixture 원본으로 덮어쓰므로 원복 절차가 필요 없다. + /** @effects live_add_tab_edit_label_reorder_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout */ test('array 편집 후 저장 → PUT 200', async ({ page }) => { test.setTimeout(60_000); // 저장 + 모달 닫힘 대기 합산 — 기본 30s 부족 await gotoEditor(page, sandboxRouteParam()); diff --git a/tests/Playwright/specs/layout-editor/auth-guard-and-locales.spec.ts b/tests/Playwright/specs/layout-editor/auth-guard-and-locales.spec.ts index ad1c4003..e7ecbf9a 100644 --- a/tests/Playwright/specs/layout-editor/auth-guard-and-locales.spec.ts +++ b/tests/Playwright/specs/layout-editor/auth-guard-and-locales.spec.ts @@ -20,12 +20,13 @@ * 렌더됐다. 수정: editor-spec sampleGlobal 에 `$locale`/`$locales` 선언 + * PreviewCanvas 가 `$`-prefixed 키를 렌더 컨텍스트 최상위로 lift. * - * @scenario editor_entry_unauthorized + editor_entry_authorized + sample_locales_dropdown - * @effects access_error_panel_unauthorized + chrome_not_rendered_when_unauthorized + route_tree_source_tagged + locale_dropdown_populated + * 축 요약(마커 아님 — 평문): editor_entry_unauthorized, editor_entry_authorized, sample_locales_dropdown. + * 효과 요약(마커 아님 — 평문): access_error_panel_unauthorized, chrome_not_rendered_when_unauthorized, route_tree_source_tagged, locale_dropdown_populated. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; test.describe('@layout-editor 진입 인증 가드', () => { + /** @effects access_error_panel_unauthorized, chrome_not_rendered_when_unauthorized */ test('비로그인(토큰 없음) 진입 시 chrome 미렌더 + unauthorized 안내', async ({ page }) => { // authenticatePage 미호출 — localStorage 에 auth_token 없음(세션 만료/로그아웃 상태 모사). await page.goto('/admin/layout-editor/sirsoft-admin_basic?route=%2Flogin'); @@ -41,6 +42,7 @@ test.describe('@layout-editor 진입 인증 가드', () => { await expect(page.getByTestId('g7le-chrome-header')).toHaveCount(0); }); + /** @effects route_tree_source_tagged */ test('편집 권한 보유 시 정상 진입 (chrome 렌더 + 라우트 트리 source 태깅 정상)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -59,6 +61,7 @@ test.describe('@layout-editor 진입 인증 가드', () => { }); test.describe('@layout-editor 샘플 로케일 목록', () => { + /** @effects locale_dropdown_populated */ test('로그인 화면 로케일 선택 드롭다운이 $locales 로 채워진다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/background-image-render.spec.ts b/tests/Playwright/specs/layout-editor/background-image-render.spec.ts index b16ddd3a..c622c80a 100644 --- a/tests/Playwright/specs/layout-editor/background-image-render.spec.ts +++ b/tests/Playwright/specs/layout-editor/background-image-render.spec.ts @@ -8,8 +8,8 @@ * 잡지 못해(브라우저 렌더만이 포착), Playwright 로 실제 캔버스 DOM 의 inline * `background-image: url(...)` 적용을 검증한다. * - * @scenario background_image_apply + background_image_mode_switch - * @effects canvas_background_image_url + canvas_background_size_mode + * 축 요약(마커 아님 — 평문): background_image_apply, background_image_mode_switch. + * 효과 요약(마커 아님 — 평문): canvas_background_image_url, canvas_background_size_mode. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -58,6 +58,7 @@ async function openStyleTab(page: Page): Promise<void> { } test.describe('@layout-editor 배경 이미지 캔버스 반영', () => { + /** @effects canvas_background_image_url */ test('속성 모달 image 위젯 → URL 입력 → 캔버스 노드 inline background-image url(...) 반영', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -104,6 +105,7 @@ test.describe('@layout-editor 배경 이미지 캔버스 반영', () => { expect(styleSnap.repeat).toBe('no-repeat'); }); + /** @effects canvas_background_size_mode */ test('표시 모드 전환(맞춤/타일) → 캔버스 backgroundSize/Repeat 정합', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/bundle-split.spec.ts b/tests/Playwright/specs/layout-editor/bundle-split.spec.ts index 93bc8667..517abcd1 100644 --- a/tests/Playwright/specs/layout-editor/bundle-split.spec.ts +++ b/tests/Playwright/specs/layout-editor/bundle-split.spec.ts @@ -12,7 +12,7 @@ * 마운트 성공이 곧 단일 React 인스턴스 + 컨텍스트/싱글톤 동일성(shim __runtime 공유)을 증명한다. * * @scenario bundle_split_lazy_editor - * @effects editor_bundle_absent_on_normal_page + editor_bundle_loaded_and_mounted_on_editor_route + * 효과 요약(마커 아님 — 평문): editor_bundle_absent_on_normal_page, editor_bundle_loaded_and_mounted_on_editor_route. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -37,6 +37,7 @@ function collectEditorBundleRequests(page: PwPage): string[] { } test.describe('Layout Editor — 번들 분리 (lazy 로드)', () => { + /** @effects editor_bundle_absent_on_normal_page */ test('일반 admin 페이지는 layout-editor.min.js 를 요청하지 않는다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -50,6 +51,7 @@ test.describe('Layout Editor — 번들 분리 (lazy 로드)', () => { expect(editorRequests, '일반 페이지에서 편집기 번들이 로드됨(회귀)').toEqual([]); }); + /** @effects editor_bundle_loaded_and_mounted_on_editor_route */ test('편집기 URL 진입 시 layout-editor.min.js 를 로드하고 마운트한다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/children-list-editor.spec.ts b/tests/Playwright/specs/layout-editor/children-list-editor.spec.ts index ad24f51a..ab8b0c24 100644 --- a/tests/Playwright/specs/layout-editor/children-list-editor.spec.ts +++ b/tests/Playwright/specs/layout-editor/children-list-editor.spec.ts @@ -14,8 +14,8 @@ * 를 검증한다. 항목 텍스트 다국어(커스텀 키 생성)는 단위(ChildrenListControl.test.tsx)가 * CRUD round-trip 을 잠그고, 본 E2E 는 구조 편집(추가/정렬/저장)을 브라우저로 확인한다. * - * @scenario children_node_editor + add_remove_move + live_persist - * @effects property_modal_dispatches_children_node_editor_in_props_tab_by_kind_not_name, add_item_appends_childcomponent_defaultnode_to_children, move_up_down_swaps_adjacent_children_with_boundary_guard, live_add_li_edit_text_reorder_save_persists_to_user_page, keyboard_arrowup_escape_escalates_selection_to_parent, overlapping_child_selected_type_chip_escalates_to_parent, editor_save_specs_target_sandbox_layout_not_product_layout, editor_save_persists_and_survives_reload_on_sandbox + * 축 요약(마커 아님 — 평문): children_node_editor, add_remove_move, live_persist. + * 효과 요약(마커 아님 — 평문): property_modal_dispatches_children_node_editor_in_props_tab_by_kind_not_name, add_item_appends_childcomponent_defaultnode_to_children, move_up_down_swaps_adjacent_children_with_boundary_guard, live_add_li_edit_text_reorder_save_persists_to_user_page, keyboard_arrowup_escape_escalates_selection_to_parent, overlapping_child_selected_type_chip_escalates_to_parent, editor_save_specs_target_sandbox_layout_not_product_layout, editor_save_persists_and_survives_reload_on_sandbox. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -127,6 +127,7 @@ async function appendUlTo(page: Page, containerPath: string): Promise<string> { } test.describe('@layout-editor children 노드 에디터(목록 빌트인)', () => { + /** @effects property_modal_dispatches_children_node_editor_in_props_tab_by_kind_not_name, add_item_appends_childcomponent_defaultnode_to_children, move_up_down_swaps_adjacent_children_with_boundary_guard */ test('Ul 선택 시 children 에디터가 속성 탭에 마운트되고 Li 추가/정렬이 캔버스 반영', async ({ page }) => { await gotoEditor(page); const ulPath = await addUl(page); @@ -167,6 +168,7 @@ test.describe('@layout-editor children 노드 에디터(목록 빌트인)', () = // 시드 화면(e2e_sandbox)을 대상으로 한다. 이전에는 "추가한 Ul 삭제 후 재저장" 으로 원복했으나, // 원복 자체가 또 한 번의 저장이라 실패하면 잔여물이 남았다. 시드 화면은 globalSetup 이 매 실행 // fixture 원본으로 덮어쓰므로 원복 절차가 필요 없다. + /** @effects live_add_li_edit_text_reorder_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout, editor_save_persists_and_survives_reload_on_sandbox */ test('children 편집 후 저장 → PUT 200 + reload 영속', async ({ page }) => { test.setTimeout(60_000); // 저장 + reload 합산 — 기본 30s 부족 @@ -213,6 +215,7 @@ test.describe('@layout-editor children 노드 에디터(목록 빌트인)', () = // 겹친 부모 선택 — 캔버스 클릭은 늘 가장 깊은 자식을 잡으므로(closest), 부모/자식 크기가 // 같은 Ul>Li 같은 경우 자식만 잡힌다. 선택 박스 위 타입 칩(↑)을 클릭하거나 키보드 ↑/Esc 로 // 한 단계씩 상위(부모)를 선택한다(상용 편집기 공통 — 부모는 별도 어포던스). + /** @effects keyboard_arrowup_escape_escalates_selection_to_parent, overlapping_child_selected_type_chip_escalates_to_parent */ test('겹친 자식 선택 후 타입 칩(↑)/키보드 ↑로 부모(Ul) escalation', async ({ page }) => { await gotoEditor(page); const ulPath = await addUl(page); diff --git a/tests/Playwright/specs/layout-editor/chrome-controls.spec.ts b/tests/Playwright/specs/layout-editor/chrome-controls.spec.ts index d16eb442..e1ccc811 100644 --- a/tests/Playwright/specs/layout-editor/chrome-controls.spec.ts +++ b/tests/Playwright/specs/layout-editor/chrome-controls.spec.ts @@ -12,8 +12,8 @@ * 레이아웃이므로 템플릿 자체 E2E(templates/_bundled/sirsoft-admin_basic/tests/Playwright) * 가 커버한다 — 본 코어 spec 은 위지윅 chrome 책임만. * - * @scenario preview_device + custom_width_input + toolbar_action + selected_box_size + affordance_kind + mobile_closed_drawer_clipping + route_tree_node_source_kind - * @effects preview_custom_device_uses_clamped_input_width_as_override + toolbar_exit_navigates_to_template_list_by_type + toolbar_edit_code_opens_text_layout_editor_in_new_window_with_route + toolbar_save_shows_spinner_while_pending_then_restores + insertion_affordances_cross_layout_when_box_lt_44_no_overlap_no_clip + mobile_preview_closed_drawer_clipped_inside_frame_not_visible + route_tree_item_shows_layout_file_path_under_label_with_source_prefix + * 축 요약(마커 아님 — 평문): preview_device, custom_width_input, toolbar_action, selected_box_size, affordance_kind, mobile_closed_drawer_clipping, route_tree_node_source_kind. + * 효과 요약(마커 아님 — 평문): preview_custom_device_uses_clamped_input_width_as_override, toolbar_exit_navigates_to_template_list_by_type, toolbar_edit_code_opens_text_layout_editor_in_new_window_with_route, toolbar_save_shows_spinner_while_pending_then_restores, insertion_affordances_cross_layout_when_box_lt_44_no_overlap_no_clip, mobile_preview_closed_drawer_clipped_inside_frame_not_visible, route_tree_item_shows_layout_file_path_under_label_with_source_prefix. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -30,6 +30,7 @@ async function enterEditor(page: import('@playwright/test').Page): Promise<void> } test.describe('@layout-editor chrome controls', () => { + /** @effects preview_custom_device_uses_clamped_input_width_as_override */ test('결함 2 — custom 디바이스 선택 시 width 입력 노출 + 프레임 폭 반영', async ({ page }) => { await enterEditor(page); @@ -50,6 +51,7 @@ test.describe('@layout-editor chrome controls', () => { .toBe(500); }); + /** @effects toolbar_exit_navigates_to_template_list_by_type */ test('결함 4 — 나가기 버튼 클릭 시 user 템플릿 목록(/admin/templates/user)으로 이탈', async ({ page }) => { // sirsoft-basic 은 user 템플릿 → 나가기 목적지는 /admin/templates/user await enterEditor(page); @@ -60,6 +62,7 @@ test.describe('@layout-editor chrome controls', () => { expect(page.url()).toMatch(/\/admin\/templates\/user/); }); + /** @effects toolbar_edit_code_opens_text_layout_editor_in_new_window_with_route */ test('결함 5 — 코드편집 버튼 클릭 시 선택 route 를 ?route= 로 실어 새 창으로 연다', async ({ page, context }) => { // EDITOR_URL 은 ?route=%2F (홈) 선택 상태 → 코드편집기를 새 창(_blank)으로 열고 // ?route=/ 동기화. 위지윅 편집 화면(page)은 그대로 유지된다. @@ -95,6 +98,7 @@ test.describe('@layout-editor chrome controls', () => { await expect(page.getByTestId('g7le-palette-item-Button-tag')).toHaveText('<Button>'); }); + /** @effects route_tree_item_shows_layout_file_path_under_label_with_source_prefix */ test('후속 — 좌측 트리 라우트 노드에 레이아웃 파일 경로가 라벨 하단에 노출 (출처 prefix)', async ({ page }) => { // sirsoft-basic 은 템플릿 소스 → 경로 표기는 "layouts/{layoutName}.json". // 트리의 라우트 항목마다 라벨 아래 회색 보조 줄(g7le-route-tree-layout-path)이 @@ -133,6 +137,7 @@ test.describe('@layout-editor chrome controls', () => { expect(result.groupHasPath).toBe(false); }); + /** @effects insertion_affordances_cross_layout_when_box_lt_44_no_overlap_no_clip */ test('결함 7 재조사 — 작은 요소 선택 시 삽입 어포던스가 박스 중심 십자로 비겹침·비잘림', async ({ page }) => { // 인라인 텍스트(Span) 추가 → 작은 박스(폭<44) 선택 → 4방향 + 버튼이 박스 // 중심 기준 위/아래/좌/우 십자로 벌어져 서로/박스와 겹치지 않고, 캔버스에도 @@ -206,6 +211,7 @@ test.describe('@layout-editor chrome controls', () => { expect(geom.leftRightSameCy).toBe(true); }); + /** @effects mobile_preview_closed_drawer_clipped_inside_frame_not_visible */ test('회귀 — 모바일 프리뷰의 닫힌 메뉴 드로어가 frame 밖으로 노출되지 않는다', async ({ page }) => { // 닫힌 모바일 드로어(mobile_nav_drawer)는 `position: fixed` + `translate-x-full` // 로 화면 밖에 밀려 있어야 한다. frame 이 자체 transform 을 잃으면 fixed 자손의 diff --git a/tests/Playwright/specs/layout-editor/condition-preset-variant-defense.spec.ts b/tests/Playwright/specs/layout-editor/condition-preset-variant-defense.spec.ts index ce9db17f..55707858 100644 --- a/tests/Playwright/specs/layout-editor/condition-preset-variant-defense.spec.ts +++ b/tests/Playwright/specs/layout-editor/condition-preset-variant-defense.spec.ts @@ -19,8 +19,8 @@ * dnd-kit 비호환(feedback_chrome_mcp_dnd_kit_incompatible) 회피 — 노드 선택은 합성 마우스 * 이벤트 시퀀스로 드래그 핸들 pointer 가로채기를 우회한다. * - * @scenario property_modal_visibility_preset_recognition + visibility_compound_stays_advanced + numeric_array_preset_recognition + explicit_boolean_compare_preset_recognition + comparison_pattern_preset_recognition + coverage_gate_presets_served - * @effects visibility_builder_renders + variant_defense_no_false_positive + stage2_numeric_array_presets_served + stage3_boolean_compare_presets_served + stage4_comparison_presets_served + stage5_gate_presets_served + * 축 요약(마커 아님 — 평문): property_modal_visibility_preset_recognition, visibility_compound_stays_advanced, numeric_array_preset_recognition, explicit_boolean_compare_preset_recognition, comparison_pattern_preset_recognition, coverage_gate_presets_served. + * 효과 요약(마커 아님 — 평문): visibility_builder_renders, variant_defense_no_false_positive, stage2_numeric_array_presets_served, stage3_boolean_compare_presets_served, stage4_comparison_presets_served, stage5_gate_presets_served. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -83,6 +83,7 @@ async function openVisibilityTab(page: PwPage, path: string): Promise<boolean> { } test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', () => { + /** @effects visibility_builder_renders */ test('표시조건 탭이 빌더 또는 no-recipes 로 렌더 (인프라 배선 회귀 가드)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -110,6 +111,7 @@ test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', expect(opened).toBe(true); }); + /** @effects stage2_numeric_array_presets_served */ test('P2/P3 operator 가 served editor-spec 에 노출 + 숫자/배열 패턴 인식', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -136,6 +138,7 @@ test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', } }); + /** @effects stage3_boolean_compare_presets_served */ test('P5 valueIsTrue/valueIsFalse 가 served editor-spec 에 노출', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -166,6 +169,7 @@ test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', } }); + /** @effects stage4_comparison_presets_served */ test('단계 4 비교 패턴 operator 가 served editor-spec 에 노출', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -197,6 +201,7 @@ test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', } }); + /** @effects stage5_gate_presets_served */ test('60% 게이트 토대 프리셋 18종이 양 템플릿 served editor-spec 에 전부 노출', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -231,6 +236,7 @@ test.describe('@layout-editor 표시조건 변종 방어 인프라 (단계 0)', } }); + /** @effects variant_defense_no_false_positive */ test('콘솔/네트워크 에러 없이 표시조건 모달 진입 (variant-defense 무결성)', async ({ page }) => { const consoleErrors: string[] = []; page.on('console', (msg) => { diff --git a/tests/Playwright/specs/layout-editor/default-node-design.spec.ts b/tests/Playwright/specs/layout-editor/default-node-design.spec.ts index 0e003841..670eac5c 100644 --- a/tests/Playwright/specs/layout-editor/default-node-design.spec.ts +++ b/tests/Playwright/specs/layout-editor/default-node-design.spec.ts @@ -14,10 +14,10 @@ * - sirsoft-basic: Button → rounded-lg, Input → border 패턴 * - sirsoft-admin_basic: Button → bg-blue-600, Input → "input" 유틸 클래스 * - * @scenario palette_open + default_node_source:editorSpecDefaultNode - * @effects palette_default_node_className_replicated_into_inserted_node - * @effects palette_default_node_className_reflects_template_design_system_tokens - * @effects palette_default_node_button_omits_variant_size_uses_className_only + * 축 요약(마커 아님 — 평문): palette_open, default_node_source:editorSpecDefaultNode. + * 효과 요약(마커 아님 — 평문): palette_default_node_className_replicated_into_inserted_node. + * 효과 요약(마커 아님 — 평문): palette_default_node_className_reflects_template_design_system_tokens. + * 효과 요약(마커 아님 — 평문): palette_default_node_button_omits_variant_size_uses_className_only. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -32,6 +32,7 @@ const probes: Probe[] = [ test.describe('@layout-editor defaultNode 공통 디자인 반영', () => { for (const { template, component, token } of probes) { + /** @effects palette_default_node_className_replicated_into_inserted_node, palette_default_node_className_reflects_template_design_system_tokens, palette_default_node_button_omits_variant_size_uses_className_only */ test(`${template} — ${component} 추가 시 캔버스에 "${token}" className 반영`, async ({ page }) => { const tokenAuth = issueToken('core.templates.layouts.edit'); await authenticatePage(page, tokenAuth); diff --git a/tests/Playwright/specs/layout-editor/drag-drop-reorder.spec.ts b/tests/Playwright/specs/layout-editor/drag-drop-reorder.spec.ts index 1ea6b85c..e7c6d9ec 100644 --- a/tests/Playwright/specs/layout-editor/drag-drop-reorder.spec.ts +++ b/tests/Playwright/specs/layout-editor/drag-drop-reorder.spec.ts @@ -22,7 +22,7 @@ * 슬롯 레벨 한정(자기 자손·래퍼 밖 break-out 슬롯 부재). * * @scenario drag_drop reorder - * @effects drag_handle_present_for_draggable + explicit_drop_slots_render + drop_slots_scoped_to_relevant_levels + editor_attrs_passthrough_marks_nesting_nodes + drop_slots_span_all_accepting_containers + handle_zindex_depth_ordered + selection_based_drag_substitution + child_reselect_under_selected_parent + inner_child_nests_into_sibling_filled_card + nav_destination_unreachable_affordance_hidden + data_bound_node_draggable_and_selectable + iteration_instance_not_individually_draggable + iteration_virtual_group_select_move_ghost + * 효과 요약(마커 아님 — 평문): drag_handle_present_for_draggable, explicit_drop_slots_render, drop_slots_scoped_to_relevant_levels, editor_attrs_passthrough_marks_nesting_nodes, drop_slots_span_all_accepting_containers, handle_zindex_depth_ordered, selection_based_drag_substitution, child_reselect_under_selected_parent, inner_child_nests_into_sibling_filled_card, nav_destination_unreachable_affordance_hidden, data_bound_node_draggable_and_selectable, iteration_instance_not_individually_draggable, iteration_virtual_group_select_move_ghost. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -104,6 +104,7 @@ test.describe('@layout-editor drag-drop reorder', () => { }).catch(() => { /* 페이지 닫힘 등 무시 */ }); }); + /** @effects drag_handle_present_for_draggable */ test('드래그 핸들이 draggable 노드에만 렌더된다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -126,6 +127,7 @@ test.describe('@layout-editor drag-drop reorder', () => { expect(mapped, '모든 일반 드래그 핸들이 실제 editor-path 노드에 매핑되어야 함').toBe(true); }); + /** @effects explicit_drop_slots_render */ test('드래그 중 고스트가 body 포털 + 실제 컴포넌트 복제 + 커서 정합, 드롭 슬롯 표시', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -184,6 +186,7 @@ test.describe('@layout-editor drag-drop reorder', () => { expect(slotsAfter, '드롭 후 슬롯은 사라져야 함').toBe(0); }); + /** @effects drop_slots_scoped_to_relevant_levels */ test('드롭 슬롯이 드래그 노드의 관련 레벨로 한정 — 자기 자손/래퍼 밖 break-out 슬롯 없음', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -252,6 +255,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * 본 테스트는 stat 카드(composite StatCard)와 그 조상 컨테이너(layout)가 모두 * [data-editor-path] 로 잡히는지 확인 — editorAttrs 가 composite/layout 루트에 도달했다는 증거. */ + /** @effects editor_attrs_passthrough_marks_nesting_nodes */ test('composite/layout nesting 노드가 editorAttrs spread 로 [data-editor-path] 표식을 보유', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -299,6 +303,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * 드래그 시작하면 그 노드의 **부모가 아닌 다른 컨테이너**에도 드롭 슬롯이 생성된다. * (이전: 부모/직접 형제만 슬롯 → 다른 컨테이너로 못 넣음.) */ + /** @effects drop_slots_span_all_accepting_containers */ test('드롭 슬롯이 드래그 노드의 부모 외 다른 컨테이너에도 생성 (결함 1: 컨테이너 간 이동)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -344,6 +349,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * z 를 낮추면 부모 핸들이 자식 영역을 덮어 "부모 선택 상태에서 자식 재선택 불가" 결함이 * 났다. 검증: 부모-자식 핸들 쌍에서 자식 z > 부모 z. */ + /** @effects handle_zindex_depth_ordered */ test('드래그 핸들 z-index 가 트리 깊이순 — 자식이 부모보다 위 (결함 2: 클릭 우선순위)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -384,6 +390,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * 노드로 치환), (b) 자식을 단순 클릭하면 자식이 재선택된다. 검증: 부모 선택 후 자식 영역에서 * 드래그 시작 시 부모 자기/자손 컨테이너 슬롯이 제외됨(=부모가 드래그 대상으로 치환된 증거). */ + /** @effects selection_based_drag_substitution, child_reselect_under_selected_parent */ test('부모 선택 후 자식 영역 드래그 → 선택 부모가 끌림 + 자식 클릭 시 재선택 (결함 2: 선택 기준 드래그)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -481,6 +488,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * 으로 확장했다. 검증: 카드 내부 자식을 드래그 시작하면 **형제 카드(또는 그 자손)에 드롭 슬롯** * 이 생성된다(아이콘을 다른 카드로 옮길 수 있음). */ + /** @effects inner_child_nests_into_sibling_filled_card */ test('카드 내부 자식을 드래그하면 형제(채워진) 카드에 드롭 슬롯 생성', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -536,6 +544,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * 선택해도 "편집기에서 이동할 수 없습니다" 안내(destination_unreachable)는 표시되지 * 않는다. 어차피 편집기에서 미작동하므로 어포던스 자체를 제거(드래그 재배치 오해 방지). */ + /** @effects nav_destination_unreachable_affordance_hidden */ test('네비 어포던스 미표시 — destination_unreachable 어포던스가 DOM 에 없음', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -560,6 +569,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * sirsoft-basic home 에는 data_bound composite 가 적을 수 있어, "핸들이 달린 * data_bound 노드가 1개 이상 존재"로 확인(없으면 skip 처리하지 않고 핸들↔노드 매핑만). */ + /** @effects data_bound_node_draggable_and_selectable */ test('data_bound 요소도 드래그 핸들을 가져 선택/이동 가능', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -580,6 +590,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * iteration 펼침 인스턴스(`...iteration.N`)와 그 내부 노드는 **개별** 드래그 핸들을 * 갖지 않는다 — 개별 인스턴스를 직접 옮기면 안 되기 때문(묶음은 아래 가상 묶음으로). */ + /** @effects iteration_instance_not_individually_draggable */ test('반복 인스턴스/내부 노드는 개별 드래그 핸들 미생성', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -613,6 +624,7 @@ test.describe('@layout-editor drag-drop reorder', () => { * iteration 원본 path 로 1개, box 는 인스턴스 union. 클릭 시 묶음 union 이 선택되고, * 드래그 시작 시 원본 노드가 드래그 대상(자기/자손 슬롯 제외) + 고스트 표시. */ + /** @effects iteration_virtual_group_select_move_ghost */ test('이터레이션 가상 묶음 — 묶음 핸들 생성 + union 선택 + 원본 노드 드래그 + 고스트', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/empty-container-form-propcontrols.spec.ts b/tests/Playwright/specs/layout-editor/empty-container-form-propcontrols.spec.ts index 5aee8938..b4771d39 100644 --- a/tests/Playwright/specs/layout-editor/empty-container-form-propcontrols.spec.ts +++ b/tests/Playwright/specs/layout-editor/empty-container-form-propcontrols.spec.ts @@ -16,8 +16,8 @@ * PasswordInput.test.tsx 가 data-editor-* 루트 부착을 잠근다. 본 Playwright 는 실제 편집기 * 팔레트 필터 연동 + propControl→DOM 라운드트립을 브라우저로 검증한다. * - * @scenario empty_container_first_child + form_propcontrol_edit - * @effects empty_container_global_palette_filters_by_own_accepts_not_parent + palette_filters_by_parent_accepts + * 축 요약(마커 아님 — 평문): empty_container_first_child, form_propcontrol_edit. + * 효과 요약(마커 아님 — 평문): empty_container_global_palette_filters_by_own_accepts_not_parent, palette_filters_by_parent_accepts. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -92,6 +92,7 @@ async function addEmptyDiv(page: Page): Promise<string> { } test.describe('@layout-editor 빈 컨테이너 첫 자식 + 폼 propControl', () => { + /** @effects empty_container_global_palette_filters_by_own_accepts_not_parent, palette_filters_by_parent_accepts */ test('빈 Div 선택 시 팔레트가 그 Div 의 accepts(폼 포함)로 필터된다 (결함#1)', async ({ page }) => { await gotoEditor(page); diff --git a/tests/Playwright/specs/layout-editor/inline-edit-i18n.spec.ts b/tests/Playwright/specs/layout-editor/inline-edit-i18n.spec.ts index db1f88b0..4d939c9b 100644 --- a/tests/Playwright/specs/layout-editor/inline-edit-i18n.spec.ts +++ b/tests/Playwright/specs/layout-editor/inline-edit-i18n.spec.ts @@ -8,8 +8,8 @@ * E. text 미보유/데이터 결정 노드 → 더블클릭해도 인라인 편집 비활성(편집기 미노출). * F. 속성 모달 [번역] 탭 — 커스텀 키 노드는 로케일별 일괄 편집 폼, 평문은 "키 아님" 안내. * - * @scenario locale_switch_canvas_only + inline_edit_existing_key_to_new_custom_key + inline_edit_real_mouse_via_dnd_handle + canvas_renders_value_not_raw_key + inline_edit_disabled_binding + toolbar_spec_based + translation_tab_bulk_edit - * @effects locale_switcher_changes_only_canvas_content_locale_not_chrome + inline_edit_plain_text_generates_custom_key_via_post_endpoint + inline_edit_replaces_comp_text_with_t_custom_key + inline_edit_real_mouse_double_click_via_dnd_handle_forward + canvas_rerenders_custom_key_value_after_server_lang_refetch + inline_edit_disabled_for_binding_expression_text + inline_toolbar_buttons_filtered_by_componentCapability_styleControls + inline_toolbar_omits_list_table_image_buttons + translation_field_displays_all_active_locales_in_modal + * 축 요약(마커 아님 — 평문): locale_switch_canvas_only, inline_edit_existing_key_to_new_custom_key, inline_edit_real_mouse_via_dnd_handle, canvas_renders_value_not_raw_key, inline_edit_disabled_binding, toolbar_spec_based, translation_tab_bulk_edit. + * 효과 요약(마커 아님 — 평문): locale_switcher_changes_only_canvas_content_locale_not_chrome, inline_edit_plain_text_generates_custom_key_via_post_endpoint, inline_edit_replaces_comp_text_with_t_custom_key, inline_edit_real_mouse_double_click_via_dnd_handle_forward, canvas_rerenders_custom_key_value_after_server_lang_refetch, inline_edit_disabled_for_binding_expression_text, inline_toolbar_buttons_filtered_by_componentCapability_styleControls, inline_toolbar_omits_list_table_image_buttons, translation_field_displays_all_active_locales_in_modal. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -67,6 +67,7 @@ async function selectNode(page: Page, editorPath: string): Promise<void> { } test.describe('@layout-editor 다국어 인라인 편집 (Phase 6 S8-2)', () => { + /** @effects locale_switcher_changes_only_canvas_content_locale_not_chrome */ test('A. 로케일 전환 — 캔버스 콘텐츠 로케일만 변경(편집기 chrome 불변)', async ({ page }) => { await openEditorLogin(page); // 로케일 스위처가 노출되면(활성 로케일 2개 이상) 전환 동작 검증, 1개면 단일 라벨. @@ -88,6 +89,7 @@ test.describe('@layout-editor 다국어 인라인 편집 (Phase 6 S8-2)', () => } }); + /** @effects inline_edit_plain_text_generates_custom_key_via_post_endpoint, inline_edit_replaces_comp_text_with_t_custom_key, inline_edit_real_mouse_double_click_via_dnd_handle_forward, canvas_rerenders_custom_key_value_after_server_lang_refetch */ test('B+C+D. 기존 $t: 키 텍스트 더블클릭(실제 마우스) → 인라인 편집기 → Enter 확정 → POST 키 생성 → 캔버스가 raw 키가 아닌 입력값 표시', async ({ page }) => { await openEditorLogin(page); @@ -126,6 +128,7 @@ test.describe('@layout-editor 다국어 인라인 편집 (Phase 6 S8-2)', () => .toBe('환영 인사 변경'); }); + /** @effects inline_toolbar_buttons_filtered_by_componentCapability_styleControls, inline_toolbar_omits_list_table_image_buttons */ test('D. 서식 툴바 — styleControls 기반 버튼만, 목록/표/이미지 버튼 부재', async ({ page }) => { await openEditorLogin(page); await dblClickNode(page, HEADING); @@ -140,6 +143,7 @@ test.describe('@layout-editor 다국어 인라인 편집 (Phase 6 S8-2)', () => } }); + /** @effects inline_edit_disabled_for_binding_expression_text */ test('E. 바인딩식/데이터 결정 노드 → 더블클릭해도 인라인 편집 미진입', async ({ page }) => { await openEditorLogin(page); // 카드 전체(Div, text 없음) 더블클릭 → 편집기 미노출(편집 불가 노드). @@ -150,6 +154,7 @@ test.describe('@layout-editor 다국어 인라인 편집 (Phase 6 S8-2)', () => await expect(page.getByTestId('g7le-inline-text-editable')).toHaveCount(0); }); + /** @effects translation_field_displays_all_active_locales_in_modal */ test('F. 속성 모달 [번역] 탭 — 평문 노드는 "키 아님" 안내', async ({ page }) => { await openEditorLogin(page); await selectNode(page, HEADING); diff --git a/tests/Playwright/specs/layout-editor/iteration-edit-save-return-inline.spec.ts b/tests/Playwright/specs/layout-editor/iteration-edit-save-return-inline.spec.ts index c2330aed..41c0c3fc 100644 --- a/tests/Playwright/specs/layout-editor/iteration-edit-save-return-inline.spec.ts +++ b/tests/Playwright/specs/layout-editor/iteration-edit-save-return-inline.spec.ts @@ -4,8 +4,8 @@ * 반복 항목(iteration_item) 편집 모드의 4종을 브라우저에서 가드한다. 단위 테스트는 * URL pushState/popstate, 오버레이 레이어 hit-test, 캐시 무효화→재fetch render-cycle 을 모사 못 함. * - * @scenario iteration_edit_entry + save_invalidation + exit_return + inline_double_click + lock_visibility - * @effects url_direct_entry_renders_iteration_mode_with_host + exit_iteration_edit_returns_to_host_route + iteration_save_invalidates_host_route_cache_and_busts_get + double_click_plain_text_enters_inline_edit + data_bound_node_shows_data_area_notice + * 축 요약(마커 아님 — 평문): iteration_edit_entry, save_invalidation, exit_return, inline_double_click, lock_visibility. + * 효과 요약(마커 아님 — 평문): url_direct_entry_renders_iteration_mode_with_host, exit_iteration_edit_returns_to_host_route, iteration_save_invalidates_host_route_cache_and_busts_get, double_click_plain_text_enters_inline_edit, data_bound_node_shows_data_area_notice. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import { editorPath as resolveEditorPath } from '../../fixtures/layout-editor'; @@ -44,6 +44,7 @@ async function gotoIterationEdit(page: import('@playwright/test').Page): Promise test.describe('@layout-editor 반복 항목 편집 — 저장/복귀/인라인/잠금가시', () => { // URL 다이렉트 진입 시 반복 항목 편집 모드 + 호스트(인기글) 인플레이스 렌더. + /** @effects url_direct_entry_renders_iteration_mode_with_host */ test('URL 다이렉트 진입 → iteration_item 모드 + 호스트 렌더', async ({ page }) => { await gotoIterationEdit(page); await expect(page.locator('[data-mode="iteration_item"]')).toBeAttached({ timeout: 20_000 }); @@ -54,6 +55,7 @@ test.describe('@layout-editor 반복 항목 편집 — 저장/복귀/인라인/ }); // 편집 종료 시 호스트 라우트로 복귀("라우트 선택" 화면 회귀 방지). + /** @effects exit_iteration_edit_returns_to_host_route */ test('편집 종료 → 호스트 라우트(?route=/boards/popular)로 복귀', async ({ page }) => { await gotoIterationEdit(page); // "반복 항목 편집 종료" 버튼. @@ -67,6 +69,7 @@ test.describe('@layout-editor 반복 항목 편집 — 저장/복귀/인라인/ // 렌더된 텍스트만으로는 평문/바인딩을 구분할 수 없으므로(렌더 결과엔 중괄호가 없음 — 인기글 // 카드는 전부 바인딩), 불변식은 "인라인 편집기(contenteditable) 또는 데이터 영역 안내 중 // 하나가 반드시 뜬다"로 가드한다(평문→인라인 D-33 / 바인딩→안내 D-34, 무반응이면 실패). + /** @effects double_click_plain_text_enters_inline_edit */ test('항목 안 텍스트 더블클릭 → 인라인 편집 또는 데이터 영역 안내(무반응 금지)', async ({ page }) => { await gotoIterationEdit(page); const found = await page.evaluate((srcPath) => { @@ -93,6 +96,7 @@ test.describe('@layout-editor 반복 항목 편집 — 저장/복귀/인라인/ // 항목 안 데이터 바인딩 노드 선택 시 "데이터 영역은 직접 편집할 수 없습니다" 안내 표시 // (일반 편집기와 동일 가시). iteration 범위 안이라고 무반응이면 안 된다. + /** @effects data_bound_node_shows_data_area_notice */ test('데이터 바인딩 노드 선택 → "데이터 영역" 안내 표시', async ({ page }) => { await gotoIterationEdit(page); // 바인딩 텍스트(제목 등) 좌표 클릭. diff --git a/tests/Playwright/specs/layout-editor/iteration-id-unique.spec.ts b/tests/Playwright/specs/layout-editor/iteration-id-unique.spec.ts index e23b6e37..84948738 100644 --- a/tests/Playwright/specs/layout-editor/iteration-id-unique.spec.ts +++ b/tests/Playwright/specs/layout-editor/iteration-id-unique.spec.ts @@ -14,12 +14,13 @@ * 본 spec 은 실제 브라우저 DOM 에서 중복 id 0 + 미보간 리터럴 0 을 잠근다(단위/시뮬레이션은 * 보간 부재로 인한 reconciliation 합쳐짐을 포착하지 못해 실측이 필수 — feedback #238 계열). * - * @scenario admin_dashboard_render + layout_editor_core_id_chip - * @effects html_id_unique_no_duplicates + no_uninterpolated_literal_id + core_id_chip_affordance + * 축 요약(마커 아님 — 평문): admin_dashboard_render, layout_editor_core_id_chip. + * 효과 요약(마커 아님 — 평문): html_id_unique_no_duplicates, no_uninterpolated_literal_id, core_id_chip_affordance. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; test.describe('@layout-editor iteration id 유일성', () => { + /** @effects html_id_unique_no_duplicates, no_uninterpolated_literal_id */ test('관리자 대시보드 렌더 시 HTML id 중복 0 + 미보간 리터럴 0', async ({ page }) => { const token = issueToken( 'core.dashboard.read', @@ -67,6 +68,7 @@ test.describe('@layout-editor iteration id 유일성', () => { expect(report.duplicates, `중복 HTML id: ${report.duplicates.join(', ')}`).toEqual([]); }); + /** @effects core_id_chip_affordance */ test('레이아웃 편집기 요소 ID 칸에 데이터 칩 진입점(BindingChipTextInput) 노출', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/locked-area-insertion.spec.ts b/tests/Playwright/specs/layout-editor/locked-area-insertion.spec.ts index 0c7f5f60..89380514 100644 --- a/tests/Playwright/specs/layout-editor/locked-area-insertion.spec.ts +++ b/tests/Playwright/specs/layout-editor/locked-area-insertion.spec.ts @@ -17,7 +17,7 @@ * 콘텐츠로 보존한다. * * @scenario selected_node_state=extension_locked_selected,data_bound_selected - * @effects locked_node_shows_insertion_plus_via_anchor + insertion_inserts_outside_locked_group_as_sibling + * 효과 요약(마커 아님 — 평문): locked_node_shows_insertion_plus_via_anchor, insertion_inserts_outside_locked_group_as_sibling. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -94,6 +94,7 @@ async function selectByText(page: Page, matcher: string, maxLen: number): Promis } test.describe('@layout-editor 잠긴 영역 요소 추가(+) 복원', () => { + /** @effects locked_node_shows_insertion_plus_via_anchor */ test('확장 주입 조각 선택 → 잠금이어도 + 버튼이 조각 박스 경계에 화면 내 노출', async ({ page }) => { await openRegisterEditor(page); const res = await selectByText(page, '마케팅 동의|광고성 이메일', 30); @@ -105,6 +106,7 @@ test.describe('@layout-editor 잠긴 영역 요소 추가(+) 복원', () => { expect(res.enabledInViewCount as number).toBeGreaterThan(0); }); + /** @effects locked_node_shows_insertion_plus_via_anchor */ test('데이터 반복(인기글/최근글) 영역 선택 → data_bound 여도 + 버튼이 화면 내 노출', async ({ page }) => { await openHomeEditor(page); const res = await selectByText(page, '최근 게시글|샘플 게시글|게시글', 30); diff --git a/tests/Playwright/specs/layout-editor/min-width-no-squish.spec.ts b/tests/Playwright/specs/layout-editor/min-width-no-squish.spec.ts index 468d73cf..af2fe3be 100644 --- a/tests/Playwright/specs/layout-editor/min-width-no-squish.spec.ts +++ b/tests/Playwright/specs/layout-editor/min-width-no-squish.spec.ts @@ -15,8 +15,8 @@ * 세 번째 케이스(클리핑 차단)는 이 수정의 회귀 가드가 아니라 그 **전제**를 잠근다 — 상세는 * 해당 테스트 주석 참조. * - * @scenario editor_min_width + narrow_viewport_no_squish + horizontal_scroll - * @effects shell_keeps_min_width_below_viewport, toolbar_items_keep_natural_width_no_wrap, narrow_window_produces_horizontal_scroll_not_compression + * 축 요약(마커 아님 — 평문): editor_min_width, narrow_viewport_no_squish, horizontal_scroll. + * 효과 요약(마커 아님 — 평문): shell_keeps_min_width_below_viewport, toolbar_items_keep_natural_width_no_wrap, narrow_window_produces_horizontal_scroll_not_compression. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -36,6 +36,7 @@ async function gotoEditor(page: Page, route = '%2F'): Promise<void> { } test.describe('레이아웃 편집기 — 좁은 창 압착 차단', () => { + /** @effects shell_keeps_min_width_below_viewport */ test('좁은 창에서도 셸이 최소 너비를 유지한다 (압착 없음)', async ({ page }) => { await page.setViewportSize({ width: NARROW_WIDTH, height: 900 }); await gotoEditor(page); @@ -47,6 +48,7 @@ test.describe('레이아웃 편집기 — 좁은 창 압착 차단', () => { expect(shellWidth).toBeGreaterThanOrEqual(EDITOR_MIN_WIDTH); }); + /** @effects toolbar_items_keep_natural_width_no_wrap */ test('좁은 창에서 툴바 항목이 깎이거나 줄바꿈되지 않는다', async ({ page }) => { // 넓은 창에서 각 툴바 항목의 자연 폭·높이를 먼저 측정한다. await page.setViewportSize({ width: 1600, height: 900 }); @@ -69,6 +71,7 @@ test.describe('레이아웃 편집기 — 좁은 창 압착 차단', () => { // 이 케이스가 잠그는 것은 별개의 전제다: 넘친 폭이 `overflow-x: hidden` 으로 **잘리지 않고** // 스크롤로 접근 가능해야 최소 너비 설계가 성립한다. 호스트/템플릿 CSS 가 나중에 body 나 // #app 에 overflow-x:hidden 을 걸면 편집기 우측이 영영 닿을 수 없게 되는데, 그것을 여기서 잡는다. + /** @effects narrow_window_produces_horizontal_scroll_not_compression */ test('넘친 폭이 잘리지 않고 가로 스크롤로 접근 가능하다 (클리핑 차단)', async ({ page }) => { await page.setViewportSize({ width: NARROW_WIDTH, height: 900 }); await gotoEditor(page); diff --git a/tests/Playwright/specs/layout-editor/modal-edit-confine.spec.ts b/tests/Playwright/specs/layout-editor/modal-edit-confine.spec.ts index 4df7d7bf..7c52825f 100644 --- a/tests/Playwright/specs/layout-editor/modal-edit-confine.spec.ts +++ b/tests/Playwright/specs/layout-editor/modal-edit-confine.spec.ts @@ -5,8 +5,8 @@ * 비어 모달을 편집할 경로가 없었고, 모달 편집 진입 시 딤(잠긴 호스트) 영역으로 드래그/드롭이 * 가능했으며, `_user_base` 모달 저장이 extension_point 검증으로 422 였다. * - * @scenario edit_mode + url_sync + dnd_confine - * @effects modal_collection_includes_partial_modals + modal_edit_renders_dim_and_blocker + dnd_handles_confined_to_modal_subtree + modal_edit_exit_clears_dim + * 축 요약(마커 아님 — 평문): edit_mode, url_sync, dnd_confine. + * 효과 요약(마커 아님 — 평문): modal_collection_includes_partial_modals, modal_edit_renders_dim_and_blocker, dnd_handles_confined_to_modal_subtree, modal_edit_exit_clears_dim. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -24,6 +24,7 @@ async function enterEditor(page: import('@playwright/test').Page, url: string): } test.describe('@layout-editor 모달 편집 confine', () => { + /** @effects modal_collection_includes_partial_modals */ test('편집기 routes 응답이 partial 모달을 수집(트리 노출 SSoT)', async ({ page }) => { // 백엔드 모달 수집(collectEditorBaseAndModals)이 partial 참조 + 하위 디렉토리까지 읽어 // 모달을 반환하는지 검증 — 트리 [모달]/"이 화면의 모달" 그룹의 데이터 출처. locale 무관. @@ -52,6 +53,7 @@ test.describe('@layout-editor 모달 편집 confine', () => { expect(result.hosts).toContain('_user_base'); }); + /** @effects modal_edit_renders_dim_and_blocker, dnd_handles_confined_to_modal_subtree */ test('모달 편집 진입 시 딤 음영 + 차단 밴드 + 핸들이 모달 서브트리로 confine', async ({ page }) => { await enterEditor(page, MODAL_DIRECT_URL); // 모달 편집 모드 진입 + 딤 음영 마운트 대기. @@ -79,6 +81,7 @@ test.describe('@layout-editor 모달 편집 confine', () => { expect(handleClassification.outModal).toBe(0); // 딤 호스트엔 핸들 없음 }); + /** @effects modal_edit_exit_clears_dim */ test('모달 편집 종료 → 딤 해제 + 편집 모드 이탈', async ({ page }) => { await enterEditor(page, MODAL_DIRECT_URL); await expect(page.locator('[data-mode="modal"]')).toBeAttached({ timeout: 30_000 }); diff --git a/tests/Playwright/specs/layout-editor/orphan-translation-manager.spec.ts b/tests/Playwright/specs/layout-editor/orphan-translation-manager.spec.ts index f88e7aee..6a0837b8 100644 --- a/tests/Playwright/specs/layout-editor/orphan-translation-manager.spec.ts +++ b/tests/Playwright/specs/layout-editor/orphan-translation-manager.spec.ts @@ -8,8 +8,8 @@ * 저장 시점에 백엔드가 그 키를 orphaned 로 전이하고, 관리 모달에 "미사용" 배지로 노출된다. * T4. 정리 — orphaned 키를 일괄 삭제하면 목록에서 사라진다. * - * @scenario toolbar_translations_button_opens_manager + manager_lists_and_filters + orphan_created_on_save_shows_badge + orphan_bulk_delete - * @effects toolbar_translations_button_opens_manager_modal + manager_filters_by_status_all_active_orphaned + manager_shows_orphaned_badge + save_marks_unreferenced_custom_key_as_orphaned + manager_purges_all_orphaned_keys + manager_lists_custom_keys_for_layout_with_bearer_header + * 축 요약(마커 아님 — 평문): toolbar_translations_button_opens_manager, manager_lists_and_filters, orphan_created_on_save_shows_badge, orphan_bulk_delete. + * 효과 요약(마커 아님 — 평문): toolbar_translations_button_opens_manager_modal, manager_filters_by_status_all_active_orphaned, manager_shows_orphaned_badge, save_marks_unreferenced_custom_key_as_orphaned, manager_purges_all_orphaned_keys, manager_lists_custom_keys_for_layout_with_bearer_header. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -34,6 +34,7 @@ async function openManager(page: Page): Promise<void> { } test.describe('@layout-editor 커스텀 다국어 관리 모달 (S8-2 후속)', () => { + /** @effects toolbar_translations_button_opens_manager_modal */ test('T1. 🌐 버튼 → 관리 모달 진입', async ({ page }) => { await openEditorLogin(page); await openManager(page); @@ -43,6 +44,7 @@ test.describe('@layout-editor 커스텀 다국어 관리 모달 (S8-2 후속)', await expect(page.getByTestId('g7le-translation-manager')).toHaveCount(0); }); + /** @effects manager_filters_by_status_all_active_orphaned */ test('T2. 목록/필터 동작 (비어 있으면 empty, 있으면 필터 탭)', async ({ page }) => { await openEditorLogin(page); await openManager(page); @@ -59,6 +61,7 @@ test.describe('@layout-editor 커스텀 다국어 관리 모달 (S8-2 후속)', } }); + /** @effects manager_lists_custom_keys_for_layout_with_bearer_header */ test('T3+T4. 모달 fetch 가 Bearer 토큰을 첨부하고 응답을 렌더한다', async ({ page }) => { await openEditorLogin(page); // 관리 모달 fetch 요청의 Authorization 헤더를 가로채 검증. diff --git a/tests/Playwright/specs/layout-editor/overlay-affordance-zindex.spec.ts b/tests/Playwright/specs/layout-editor/overlay-affordance-zindex.spec.ts index 06005c28..ea866f97 100644 --- a/tests/Playwright/specs/layout-editor/overlay-affordance-zindex.spec.ts +++ b/tests/Playwright/specs/layout-editor/overlay-affordance-zindex.spec.ts @@ -13,7 +13,7 @@ * ⓘ 메뉴 오픈·드래그 시작 보존을 검증한다. * * @scenario context_plus_button selected_node_state=basic_selected - * @effects overlay_affordance_above_drag_handle_zindex + overlay_affordance_click_not_intercepted_by_drag_handle + drag_handle_depth_clamped_below_affordance_band + drag_initiation_preserved_on_element_body_after_affordance_zindex_fix + * 효과 요약(마커 아님 — 평문): overlay_affordance_above_drag_handle_zindex, overlay_affordance_click_not_intercepted_by_drag_handle, drag_handle_depth_clamped_below_affordance_band, drag_initiation_preserved_on_element_body_after_affordance_zindex_fix. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -85,6 +85,7 @@ test.describe('@layout-editor overlay affordance z-index', () => { }); }); + /** @effects overlay_affordance_above_drag_handle_zindex */ test('선택 시 +/ⓘ 버튼이 어포던스 밴드 z-index(120)로 렌더', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -116,6 +117,7 @@ test.describe('@layout-editor overlay affordance z-index', () => { } }); + /** @effects overlay_affordance_click_not_intercepted_by_drag_handle */ test('+/ⓘ 버튼 정중앙의 topmost 요소가 버튼 자신 — 드래그 핸들에 가로채이지 않음', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -225,6 +227,7 @@ test.describe('@layout-editor overlay affordance z-index', () => { await expect(page.getByTestId('g7le-context-menu-delete')).toBeVisible(); }); + /** @effects drag_handle_depth_clamped_below_affordance_band */ test('드래그 핸들 z-index 가 어포던스 밴드(120) 아래로 클램프 — 어떤 깊이에서도', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -252,6 +255,7 @@ test.describe('@layout-editor overlay affordance z-index', () => { expect(result.monotonic, '드래그 핸들 z-index 는 base(20) 이상 깊이순 유지').toBe(true); }); + /** @effects drag_initiation_preserved_on_element_body_after_affordance_zindex_fix */ test('드래그 시작은 보존 — 요소 본문 잡고 끌면 고스트 + 드롭 슬롯 생성 (DnD 무회귀)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/page-state-switcher.spec.ts b/tests/Playwright/specs/layout-editor/page-state-switcher.spec.ts index 885dbec5..9b8af1a5 100644 --- a/tests/Playwright/specs/layout-editor/page-state-switcher.spec.ts +++ b/tests/Playwright/specs/layout-editor/page-state-switcher.spec.ts @@ -11,11 +11,12 @@ * 잠그며, 본 spec 은 토글 표시/전환의 브라우저 가시 결과를 측정한다. * * @scenario layout-editor-page-state-switcher - * @effects login_route_shows_state_toggle_with_two_options + root_route_hides_state_toggle + login_failed_state_renders_form_error_text_on_canvas + profile_edit_password_entry_vs_actual_edit_section_toggle + admin_users_edit_vs_create_title_via_route_id_removal + admin_settings_general_vs_seo_tab_via_query_tab + checkout_validation_failed_renders_dotted_key_inline_errors + guest_order_form_guest_vs_member_branch_toggle_via_currentuser_patch + * 효과 요약(마커 아님 — 평문): login_route_shows_state_toggle_with_two_options, root_route_hides_state_toggle, login_failed_state_renders_form_error_text_on_canvas, profile_edit_password_entry_vs_actual_edit_section_toggle, admin_users_edit_vs_create_title_via_route_id_removal, admin_settings_general_vs_seo_tab_via_query_tab, checkout_validation_failed_renders_dotted_key_inline_errors, guest_order_form_guest_vs_member_branch_toggle_via_currentuser_patch. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { + /** @effects login_route_shows_state_toggle_with_two_options */ test('states 선언 라우트(/login) → 상태 토글 표시 + 항목 2개', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -33,6 +34,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { await expect(options).toHaveCount(2); }); + /** @effects root_route_hides_state_toggle */ test('states 미선언 라우트(/) → 상태 토글 미표시 (디그레이드)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -49,6 +51,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { await expect(page.getByTestId('g7le-state-switcher')).toHaveCount(0); }); + /** @effects login_failed_state_renders_form_error_text_on_canvas */ test('로그인 실패 상태 전환 → 캔버스에 폼 오류 표시 (formErrors 경로 주입)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -82,6 +85,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { // initialState.local.isPasswordVerified 패치가 레이아웃 init_actions 의 기본값을 이겨 // 각 단계 화면이 미리보기된다. 신규 다국어 키(editor.state.profile_*)가 캐시 무효화 // 구멍 없이 친화명으로 해석되는지도 함께 측정한다(라벨 raw 회귀 차단). + /** @effects profile_edit_password_entry_vs_actual_edit_section_toggle */ test('profile/edit → 비밀번호 확인 / 실제 수정 단계 토글 + 신규 라벨 해석', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -114,6 +118,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { // route 패치 — admin_user_form 의 route.id 유무(수정↔신규) 변종 (미커버 발굴분). // admin 라우트는 `*/admin/...` 프리픽스라 URL 직접 인코딩 대신 라우트 트리 클릭으로 진입. + /** @effects admin_users_edit_vs_create_title_via_route_id_removal */ test('admin users/:id/edit → route.id 제거로 수정↔신규 작성 모드 전환', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -139,6 +144,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { }); // query 패치 — admin_settings 의 query.tab 변종 (미커버 발굴분). + /** @effects admin_settings_general_vs_seo_tab_via_query_tab */ test('admin settings → query.tab 으로 일반↔SEO 탭 전환', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -162,6 +168,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { // `_local.errors?.['orderer.name']` 처럼 **키 자체에 점이 박힌** flat 키로 읽는다. 페이지 // 상태 formErrors 키를 대괄호 표기(`_local.errors['orderer.name']`)로 지정하면 // tokenizePath 가 점을 리터럴 leaf 로 보존해 인라인 에러가 캔버스에 표현된다. + /** @effects checkout_validation_failed_renders_dotted_key_inline_errors */ test('checkout → validation_failed 전환 시 점 박힌 키 인라인 에러 노출', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -211,6 +218,7 @@ test.describe('@layout-editor 페이지 상태 토글 (S6-3)', () => { // 안내). 편집기 sampleGlobal 은 로그인 고정이라 상태 그룹이 없으면 비회원 조회 폼이 영영 // 미표시되어 편집 불가였다. guest 상태(currentUser:null 패치)로 폼을, member 상태(패치 // 없음)로 안내 분기를 각각 캔버스에 노출하는지 측정한다(라이브 실측 회귀 차단). + /** @effects guest_order_form_guest_vs_member_branch_toggle_via_currentuser_patch */ test('guest_order_form → 비회원(조회 폼) ↔ 회원(마이페이지 안내) 분기 전환', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/palette-i18n-and-globals.spec.ts b/tests/Playwright/specs/layout-editor/palette-i18n-and-globals.spec.ts index 8e412b87..7255977a 100644 --- a/tests/Playwright/specs/layout-editor/palette-i18n-and-globals.spec.ts +++ b/tests/Playwright/specs/layout-editor/palette-i18n-and-globals.spec.ts @@ -24,8 +24,8 @@ * - guest_only 레이아웃은 체인 레벨에서 코어 keyspace currentUser 를 시드에서 제외(흡수). * "이미 로그인되어 있습니다" 토스트 미발화 가드. * - * @scenario palette_i18n + global_seed + sample_global_chain + guest_only_no_currentUser + save_success - * @effects undo_history_reset + banner_visible + guest_only_no_redirect_toast, editor_save_specs_target_sandbox_layout_not_product_layout + * 축 요약(마커 아님 — 평문): palette_i18n, global_seed, sample_global_chain, guest_only_no_currentUser, save_success. + * 효과 요약(마커 아님 — 평문): undo_history_reset, banner_visible, guest_only_no_redirect_toast, editor_save_specs_target_sandbox_layout_not_product_layout. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import { sandboxRouteParam } from '../../fixtures/seed-layout'; @@ -105,6 +105,7 @@ test.describe('@layout-editor palette i18n + global seed', () => { await expect(myPageBtn).toBeVisible(); }); + /** @effects guest_only_no_redirect_toast */ test('guest_only 레이아웃 — sampleGlobal 체인이 currentUser 시드 제외 + 토스트 미발화', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -139,6 +140,7 @@ test.describe('@layout-editor palette i18n + global seed', () => { // 저장(PUT)하는 테스트는 편집 결과가 그대로 영속되므로 제품 화면(home)이 아니라 E2E 전용 // 시드 화면(e2e_sandbox)을 대상으로 한다. 시드 화면은 globalSetup 이 매 실행 fixture 원본으로 // 덮어쓰므로 회차 간 Div 누적이 성립하지 않는다. + /** @effects undo_history_reset, banner_visible, editor_save_specs_target_sandbox_layout_not_product_layout */ test('저장 success kind → SaveFeedbackBanner 표시 + history reset (결함 E + I 회귀 가드)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/prop-i18n-text-field.spec.ts b/tests/Playwright/specs/layout-editor/prop-i18n-text-field.spec.ts index ecd2537b..cd194215 100644 --- a/tests/Playwright/specs/layout-editor/prop-i18n-text-field.spec.ts +++ b/tests/Playwright/specs/layout-editor/prop-i18n-text-field.spec.ts @@ -28,8 +28,8 @@ * 단위(prop-i18n-text-field.test 칸자리 칩 4건)로 검증한다. 본 spec 은 종전대로 평문 미리보기 input·키 * 생성·펼침 폼 노출·저장 영속(비-칩 경로)을 라이브로 잠근다. * - * @scenario prop_i18n_text_field_inline_preview + prop_i18n_create_key_via_post + prop_i18n_expand_ko_en_ja + children_item_text_i18n_widget + token_persists_on_save - * @effects text_propcontrol_renders_i18n_widget_not_raw_key_input, prop_i18n_plain_input_creates_custom_key_via_post, prop_i18n_expand_shows_all_active_locales_bulk_form, children_item_text_uses_shared_i18n_widget_ssot, custom_key_token_recorded_in_prop_value_and_node_text_persists_on_put, editor_save_specs_target_sandbox_layout_not_product_layout + * 축 요약(마커 아님 — 평문): prop_i18n_text_field_inline_preview, prop_i18n_create_key_via_post, prop_i18n_expand_ko_en_ja, children_item_text_i18n_widget, token_persists_on_save. + * 효과 요약(마커 아님 — 평문): text_propcontrol_renders_i18n_widget_not_raw_key_input, prop_i18n_plain_input_creates_custom_key_via_post, prop_i18n_expand_shows_all_active_locales_bulk_form, children_item_text_uses_shared_i18n_widget_ssot, custom_key_token_recorded_in_prop_value_and_node_text_persists_on_put, editor_save_specs_target_sandbox_layout_not_product_layout. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -116,6 +116,7 @@ async function openPropsTab(page: Page): Promise<void> { } test.describe('@layout-editor 텍스트 propControl 동적 다국어 (부록7 7-b)', () => { + /** @effects text_propcontrol_renders_i18n_widget_not_raw_key_input, prop_i18n_plain_input_creates_custom_key_via_post, prop_i18n_expand_shows_all_active_locales_bulk_form */ test('Input placeholder propControl 이 raw 키 아닌 i18n 위젯(미리보기+🌐)으로 렌더되고 평문 입력→키 생성→펼침', async ({ page }) => { await gotoEditor(page); const vdiv = await addEmptyDiv(page); @@ -159,6 +160,7 @@ test.describe('@layout-editor 텍스트 propControl 동적 다국어 (부록7 7- // 저장(PUT)하는 테스트는 편집 결과가 그대로 영속되므로 제품 화면(home)이 아니라 E2E 전용 // 시드 화면(e2e_sandbox)을 대상으로 한다. 시드 화면은 globalSetup 이 매 실행 fixture 원본으로 // 덮어쓰므로 회차 간 누적이 성립하지 않는다. + /** @effects children_item_text_uses_shared_i18n_widget_ssot, custom_key_token_recorded_in_prop_value_and_node_text_persists_on_put, editor_save_specs_target_sandbox_layout_not_product_layout */ test('children 항목 텍스트도 동일 i18n 위젯으로 편집(평문→키 생성) + 저장 PUT 200 영속', async ({ page }) => { await gotoEditor(page, sandboxRouteParam()); const vdiv = await addEmptyDiv(page, true); @@ -220,8 +222,8 @@ test.describe('@layout-editor 텍스트 propControl 동적 다국어 (부록7 7- // 가로 스크롤 회귀 — 속성 모달 본문은 긴 텍스트 입력·🌐 펼침 폼에서도 가로로 // 넘치지 않아야 한다. 컨트롤 // 행/위젯 컨테이너 + 펼침 폼의 minWidth:0 가드를 라이브 브라우저로 잠근다. - // @scenario property_modal_no_horizontal_scroll_on_long_input + property_modal_no_horizontal_scroll_on_expand - // @effects control_row_and_widget_minwidth0_prevent_body_horizontal_scroll, expand_locale_form_minwidth0_prevents_body_horizontal_scroll + // 축 요약(마커 아님 — 평문): property_modal_no_horizontal_scroll_on_long_input, property_modal_no_horizontal_scroll_on_expand. + /** @effects control_row_and_widget_minwidth0_prevent_body_horizontal_scroll, expand_locale_form_minwidth0_prevents_body_horizontal_scroll */ test('속성 모달 본문은 긴 입력·🌐 펼침에서도 가로 스크롤이 없다', async ({ page }) => { await gotoEditor(page); const vdiv = await addEmptyDiv(page); diff --git a/tests/Playwright/specs/layout-editor/property-edit-ux.spec.ts b/tests/Playwright/specs/layout-editor/property-edit-ux.spec.ts index c49d3f1b..e8bb41fd 100644 --- a/tests/Playwright/specs/layout-editor/property-edit-ux.spec.ts +++ b/tests/Playwright/specs/layout-editor/property-edit-ux.spec.ts @@ -12,8 +12,8 @@ * 실제 편집기 DOM 에서 선택→모달→패치 라운드트립과 딤/잠금 해제를 브라우저로 검증한다 * (단위 시뮬레이션이 모사 못 하는 modal.stack 파생 + 실제 React 재렌더 경로). * - * @scenario iteration_shared_edit + affordance_gap + edit_lock_dim + edit_lock_release + permission_candidate_add - * @effects all_iteration_instances_patched + affordance_offset_30 + canvas_dim_on_modal + canvas_undim_on_close + permission_add_enabled + * 축 요약(마커 아님 — 평문): iteration_shared_edit, affordance_gap, edit_lock_dim, edit_lock_release, permission_candidate_add. + * 효과 요약(마커 아님 — 평문): all_iteration_instances_patched, affordance_offset_30, canvas_dim_on_modal, canvas_undim_on_close, permission_add_enabled. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -54,6 +54,7 @@ async function openModalStyleTab(page: Page): Promise<void> { } test.describe('@layout-editor 속성 편집 UX', () => { + /** @effects all_iteration_instances_patched */ test('항목1 — 이터레이션 인스턴스 속성 편집이 모든 인스턴스에 함께 반영', async ({ page }) => { await gotoEditor(page); @@ -101,6 +102,7 @@ test.describe('@layout-editor 속성 편집 UX', () => { .toBe(count); }); + /** @effects affordance_offset_30 */ test('항목2/3 — 큰(inside) 편집 가능 요소의 + 버튼·ⓘ 버튼 30px 바깥 여백', async ({ page }) => { await gotoEditor(page, '%2Flogin'); @@ -141,6 +143,7 @@ test.describe('@layout-editor 속성 편집 UX', () => { expect(measured.aboveTop).toBe('-30px'); }); + /** @effects canvas_dim_on_modal, canvas_undim_on_close */ test('항목7 — 속성 편집 중 캔버스 딤/잠금 + 닫기(편집 후 포함) 시 자동 해제', async ({ page }) => { await gotoEditor(page); @@ -205,6 +208,7 @@ test.describe('@layout-editor 속성 편집 UX', () => { await expect(page.locator('[data-testid="g7le-property-modal"]')).toHaveCount(0); }); + /** @effects permission_add_enabled */ test('표시 권한 — 고급 탭 TagInput 후보가 편집기 전용 엔드포인트에서 fetch 되어 추가 활성', async ({ page }) => { await gotoEditor(page); diff --git a/tests/Playwright/specs/layout-editor/property-editor-modal.spec.ts b/tests/Playwright/specs/layout-editor/property-editor-modal.spec.ts index 254705b8..01b67270 100644 --- a/tests/Playwright/specs/layout-editor/property-editor-modal.spec.ts +++ b/tests/Playwright/specs/layout-editor/property-editor-modal.spec.ts @@ -10,8 +10,8 @@ * - guest_only 레이아웃: 템플릿이 currentUser 를 시드해도 비로그인 페이지에서 제외 → * "이미 로그인되어 있습니다" 토스트 미발화 * - * @scenario property_modal_open + style_control_patch + sample_global_from_template + guest_only_no_currentUser - * @effects modal_visible + canvas_live_patch + site_name_seeded + guest_only_no_redirect_toast + * 축 요약(마커 아님 — 평문): property_modal_open, style_control_patch, sample_global_from_template, guest_only_no_currentUser. + * 효과 요약(마커 아님 — 평문): modal_visible, canvas_live_patch, site_name_seeded, guest_only_no_redirect_toast. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -56,6 +56,7 @@ async function selectNodeWithInfoButton(page: import('@playwright/test').Page): } test.describe('@layout-editor 속성 편집 모달 (S6-2)', () => { + /** @effects modal_visible, canvas_live_patch */ test('요소 선택 → ⓘ → 속성 설정 → 스타일 탭 컨트롤 렌더 + 패치', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -93,6 +94,7 @@ test.describe('@layout-editor 속성 편집 모달 (S6-2)', () => { expect(hasStyleTab + hasNoEditable).toBeGreaterThan(0); }); + /** @effects site_name_seeded */ test('sampleGlobal: 번들 템플릿 시드로 _global.currentUser / settings 가 채워진다 (코어 시드 폐기 후)', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -116,6 +118,7 @@ test.describe('@layout-editor 속성 편집 모달 (S6-2)', () => { ); }); + /** @effects guest_only_no_redirect_toast */ test('guest_only 레이아웃: 템플릿 currentUser 시드 제외 → "이미 로그인" 토스트 미발화', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/property-editor-new-style-controls.spec.ts b/tests/Playwright/specs/layout-editor/property-editor-new-style-controls.spec.ts index d2a61187..50c92b37 100644 --- a/tests/Playwright/specs/layout-editor/property-editor-new-style-controls.spec.ts +++ b/tests/Playwright/specs/layout-editor/property-editor-new-style-controls.spec.ts @@ -9,8 +9,8 @@ * D. 텍스트 정렬 justify 옵션 노출 + 적용 → text-justify. * E. 고급 탭 — 권한 후보가 식별자(key)와 함께 표시되고, 카테고리(부모)는 제외(리프만). * - * @scenario new_style_controls_live_apply + border_color_preset + text_format_toggle + justify_align + permission_identifier_leaf_only - * @effects box_class_tokens + border_color_token + italic_underline_tokens + text_justify_token + advanced_permission_id_visible + * 축 요약(마커 아님 — 평문): new_style_controls_live_apply, border_color_preset, text_format_toggle, justify_align, permission_identifier_leaf_only. + * 효과 요약(마커 아님 — 평문): box_class_tokens, border_color_token, italic_underline_tokens, text_justify_token, advanced_permission_id_visible. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import { editorPath } from '../../fixtures/layout-editor'; @@ -81,6 +81,7 @@ const tokensOf = (page: import('@playwright/test').Page, path: string) => page.evaluate((p) => (document.querySelector(`[data-editor-path="${p}"]`)?.className ?? '').split(/\s+/).filter(Boolean), path); test.describe('@layout-editor 신규 스타일 컨트롤', () => { + /** @effects box_class_tokens */ test('A. 컨테이너 박스 컨트롤 — 그림자/라운드/스크롤 select 가 캔버스에 반영', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -121,6 +122,7 @@ test.describe('@layout-editor 신규 스타일 컨트롤', () => { await expect.poll(() => tokensOf(page, CARD)).toContain('opacity-50'); }); + /** @effects border_color_token */ test('B. 테두리 색 프리셋 → border-{color} 토큰', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -130,6 +132,7 @@ test.describe('@layout-editor 신규 스타일 컨트롤', () => { await expect.poll(() => tokensOf(page, CARD)).toContain('border-blue-600'); }); + /** @effects italic_underline_tokens */ test('C. 텍스트 서식 — 기울임/밑줄 토글이 제목에 반영', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD_H2); @@ -141,6 +144,7 @@ test.describe('@layout-editor 신규 스타일 컨트롤', () => { await expect.poll(() => tokensOf(page, CARD_H2)).toContain('underline'); }); + /** @effects text_justify_token */ test('D. 텍스트 정렬 justify 옵션 적용 → text-justify', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD_H2); @@ -148,6 +152,7 @@ test.describe('@layout-editor 신규 스타일 컨트롤', () => { await expect.poll(() => tokensOf(page, CARD_H2)).toContain('text-justify'); }); + /** @effects advanced_permission_id_visible */ test('E. 고급 탭 — 권한 후보에 식별자 표시 + 카테고리(부모) 제외', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); diff --git a/tests/Playwright/specs/layout-editor/property-editor-s7-followup.spec.ts b/tests/Playwright/specs/layout-editor/property-editor-s7-followup.spec.ts index d67ef3a1..3b1ce660 100644 --- a/tests/Playwright/specs/layout-editor/property-editor-s7-followup.spec.ts +++ b/tests/Playwright/specs/layout-editor/property-editor-s7-followup.spec.ts @@ -7,8 +7,8 @@ * C. (A 로 해소) 수평 컨테이너(정렬 박스)에서 좌/우 삽입 어포던스 활성 — block 은 상/하만. * D. 표(Table) 추가 → 머리글/본문 행이 실제로 렌더(빈 table 회귀 차단). * - * @scenario flex_toggle_node_derived + spacing_per_side_independent + insertion_dir_by_flow + table_renders_rows - * @effects flex_container_controls_and_disable + pt_pl_coexist + left_right_enabled_in_flex + table_tr_td_rendered + * 축 요약(마커 아님 — 평문): flex_toggle_node_derived, spacing_per_side_independent, insertion_dir_by_flow, table_renders_rows. + * 효과 요약(마커 아님 — 평문): flex_container_controls_and_disable, pt_pl_coexist, left_right_enabled_in_flex, table_tr_td_rendered. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import { editorPath as resolveEditorPath } from '../../fixtures/layout-editor'; @@ -52,6 +52,7 @@ async function openPropsFor(page: import('@playwright/test').Page, editorPath: s } test.describe('@layout-editor S7 후속 회귀', () => { + /** @effects flex_container_controls_and_disable */ test('A. 정렬 박스 만들기 → 컨테이너 컨트롤 + 해제 토글로 원복 (노드 파생 판정)', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -82,6 +83,7 @@ test.describe('@layout-editor S7 후속 회귀', () => { await expect(page.getByTestId('g7le-flex-enable')).toBeAttached(); }); + /** @effects pt_pl_coexist */ test('B. 여백 개별 모드 — 상/좌를 다른 값으로 → pt-/pl- 토큰 공존', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -106,6 +108,7 @@ test.describe('@layout-editor S7 후속 회귀', () => { ).toBe(true); }); + /** @effects left_right_enabled_in_flex */ test('C. 흐름별 삽입 방향 — flex-row 자식은 좌/우 활성, block 자식은 상/하만', async ({ page }) => { await openEditorLogin(page); @@ -132,6 +135,7 @@ test.describe('@layout-editor S7 후속 회귀', () => { expect(await page.getByTestId('g7le-insertion-below').getAttribute('data-disabled')).toBe('false'); }); + /** @effects table_tr_td_rendered */ test('D. 표 추가 → 머리글/본문 행이 실제 렌더(빈 table 회귀 차단)', async ({ page }) => { await openEditorLogin(page); diff --git a/tests/Playwright/specs/layout-editor/property-editor-scope.spec.ts b/tests/Playwright/specs/layout-editor/property-editor-scope.spec.ts index 13727f37..1d794874 100644 --- a/tests/Playwright/specs/layout-editor/property-editor-scope.spec.ts +++ b/tests/Playwright/specs/layout-editor/property-editor-scope.spec.ts @@ -5,17 +5,11 @@ * 스타일/flex/표시조건을 해당 위치(responsive.{bp}.props / dark: 토큰 / responsive.{bp}.if)에 * 무손실로 기록·역해석하고, 저장·새로고침 후 복원되는지 검증한다. * - * @scenario color_scheme × device × apply_type × edit_target - * @effects device_scope_writes_responsive_breakpoint_props_base_unchanged - * + dark_classToken_coexists_with_light_token_in_one_className - * + dark_inline_control_readonly_value_never_overwritten - * + scope_without_override_shows_base_inherited_value_as_placeholder - * + custom_range_scope_writes_responsive_range_key - * + device_flex_disable_writes_explicit_off_token_breaking_base_inheritance - * + modal_scope_snapshots_toolbar_on_open - * + modal_scope_independent_after_toolbar_change - * + save_persists_responsive_and_if_on_reload - * + preview_color_scheme_toggle_applies_dark_wrapper_to_preview_frame_only + * 축 요약(마커 아님 — 평문): color_scheme, device, apply_type, edit_target. 요약을 시나리오 축 마커 + * 로 적으면 마커 파서가 `=` 없는 토큰을 버려 빈 조합 `{}` 이 + * 된다 — 실재하지 않는 조합이 커버 집합에 들어가는 방향이라 요약은 평문으로 둔다. + * + * 효과 요약(마커 아님 — 평문): device_scope_writes_responsive_breakpoint_props_base_unchanged, dark_classToken_coexists_with_light_token_in_one_className, dark_inline_control_readonly_value_never_overwritten, scope_without_override_shows_base_inherited_value_as_placeholder, custom_range_scope_writes_responsive_range_key, device_flex_disable_writes_explicit_off_token_breaking_base_inheritance, modal_scope_snapshots_toolbar_on_open, modal_scope_independent_after_toolbar_change, save_persists_responsive_and_if_on_reload, preview_color_scheme_toggle_applies_dark_wrapper_to_preview_frame_only. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -79,6 +73,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( await expect(page.getByTestId('g7le-style-bp-add-custom')).toBeVisible(); }); + /** @effects device_scope_writes_responsive_breakpoint_props_base_unchanged */ test('T2: 디바이스 탭에서 textAlign 변경 → 기본값 className 시드되어 base 토큰 보존(B안)', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -105,6 +100,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( } }); + /** @effects scope_without_override_shows_base_inherited_value_as_placeholder */ test('T2b: 디바이스 override 후 "기본값으로 초기화" → 표시점/override 제거', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -122,6 +118,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( } }); + /** @effects dark_inline_control_readonly_value_never_overwritten */ test('T3: 다크 세부탭에서 인라인 색상 컨트롤은 읽기전용 안내', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -137,6 +134,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( } }); + /** @effects dark_classToken_coexists_with_light_token_in_one_className */ test('T3b: 색 컨트롤 classToken — 라이트 프리셋 적용 + 다크 프리셋 dark: 토큰 공존', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -166,6 +164,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( } }); + /** @effects custom_range_scope_writes_responsive_range_key */ test('T6: 커스텀 크기 탭 추가 → 600-900 탭 활성 + 우선순위 안내', async ({ page }) => { await openEditorLogin(page); await openPropsFor(page, CARD); @@ -192,6 +191,7 @@ test.describe('@layout-editor 스타일 탭 색 모드 × 디바이스 scope', ( } }); + /** @effects preview_color_scheme_toggle_applies_dark_wrapper_to_preview_frame_only */ test('T12: 툴바 라이트/다크 토글 → 프리뷰 프레임만.g7le-preview-dark (편집기 chrome 비다크 격리)', async ({ page }) => { await openEditorLogin(page); diff --git a/tests/Playwright/specs/layout-editor/responsive-branch-drag.spec.ts b/tests/Playwright/specs/layout-editor/responsive-branch-drag.spec.ts index fa17f59a..6314f47c 100644 --- a/tests/Playwright/specs/layout-editor/responsive-branch-drag.spec.ts +++ b/tests/Playwright/specs/layout-editor/responsive-branch-drag.spec.ts @@ -17,7 +17,7 @@ * 검수)로 검증한다(feedback_chrome_mcp_dnd_kit_incompatible). * * @scenario responsive_branch_edit drag_drop - * @effects branch_drop_slots_render + branch_node_has_drag_handle + * 효과 요약(마커 아님 — 평문): branch_drop_slots_render, branch_node_has_drag_handle. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -124,6 +124,7 @@ test.describe('@layout-editor responsive 분기 드래그/슬롯', () => { }); }); + /** @effects branch_node_has_drag_handle */ test('모바일 보기에서 responsive 분기 자식이 드래그 핸들을 가진다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -141,6 +142,7 @@ test.describe('@layout-editor responsive 분기 드래그/슬롯', () => { ).toBeGreaterThan(0); }); + /** @effects branch_drop_slots_render */ test('분기 안 노드를 드래그하면 같은 분기 안에 드롭 슬롯이 생성된다 (결함① 회귀 가드)', async ({ page, }) => { diff --git a/tests/Playwright/specs/layout-editor/responsive-device-list-and-separation.spec.ts b/tests/Playwright/specs/layout-editor/responsive-device-list-and-separation.spec.ts index e86c85be..a63b693f 100644 --- a/tests/Playwright/specs/layout-editor/responsive-device-list-and-separation.spec.ts +++ b/tests/Playwright/specs/layout-editor/responsive-device-list-and-separation.spec.ts @@ -11,7 +11,7 @@ * `search_row`(portable **children** 교체)를 함께 가져, 두 판정 분기를 한 화면에서 검증한다. * * @scenario responsive_branch_edit device_list style_scope_tab branch_separation - * @effects device_toggle_has_portable + style_tab_has_portable + separation_mode_by_children + * 효과 요약(마커 아님 — 평문): device_toggle_has_portable, style_tab_has_portable, separation_mode_by_children. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -82,6 +82,7 @@ async function selectNodeById(page: Page, id: string): Promise<void> { } test.describe('@layout-editor responsive 디바이스 목록 + 분리 판정', () => { + /** @effects device_toggle_has_portable */ test('디바이스 토글에 portable(모바일+태블릿) 버튼이 노출된다 ', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -94,6 +95,7 @@ test.describe('@layout-editor responsive 디바이스 목록 + 분리 판정', ( } }); + /** @effects style_tab_has_portable */ test('스타일 탭 디바이스 세부탭에 portable 세부탭이 노출된다 ', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -126,6 +128,7 @@ test.describe('@layout-editor responsive 디바이스 목록 + 분리 판정', ( await expect(page.locator('[data-testid="g7le-style-bp-portable"]')).toBeAttached(); }); + /** @effects separation_mode_by_children */ test('분리 판정은 children 유무 기준 — props-only=생성 / children=해제 ', async ({ page, }) => { diff --git a/tests/Playwright/specs/layout-editor/route-connected-extensions.spec.ts b/tests/Playwright/specs/layout-editor/route-connected-extensions.spec.ts index 97bc9638..fff57c11 100644 --- a/tests/Playwright/specs/layout-editor/route-connected-extensions.spec.ts +++ b/tests/Playwright/specs/layout-editor/route-connected-extensions.spec.ts @@ -9,8 +9,8 @@ * 확장 편집 모드에서 목록이 줄어들던 결함, (2) 확장/모달 편집 모드에서 라우트를 클릭하면 * "이 레이아웃에는 표시할 컴포넌트가 없습니다" 가 뜨던 결함을 가드한다. * - * @scenario ext_type + host_match + conn_count + conn_group_state + enter_mode_from_child + select_route_from_mode + host_highlight - * @effects connected_extensions_attached_statically_by_host_layouts_without_canvas_load + connected_group_collapsed_by_default + connected_group_toggles_on_header_click + connected_count_stable_when_entering_extension_edit_mode + extension_child_click_enters_extension_edit_keeping_host_route_highlight + select_route_from_separate_edit_mode_restores_route_mode_and_renders_canvas + * 축 요약(마커 아님 — 평문): ext_type, host_match, conn_count, conn_group_state, enter_mode_from_child, select_route_from_mode, host_highlight. + * 효과 요약(마커 아님 — 평문): connected_extensions_attached_statically_by_host_layouts_without_canvas_load, connected_group_collapsed_by_default, connected_group_toggles_on_header_click, connected_count_stable_when_entering_extension_edit_mode, extension_child_click_enters_extension_edit_keeping_host_route_highlight, select_route_from_separate_edit_mode_restores_route_mode_and_renders_canvas. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -46,6 +46,7 @@ async function readConnCount(page: Page): Promise<number> { } test.describe('@layout-editor route↔connected extensions', () => { + /** @effects connected_extensions_attached_statically_by_host_layouts_without_canvas_load, connected_group_collapsed_by_default */ test('클릭 없이 host_layouts 정적 매칭으로 연결 확장이 표시 + 기본 접힘', async ({ page }) => { await enterEditor(page); @@ -60,6 +61,7 @@ test.describe('@layout-editor route↔connected extensions', () => { await expect(header).toHaveAttribute('aria-expanded', 'false'); }); + /** @effects connected_group_toggles_on_header_click */ test('연결 그룹 헤더 클릭 → 펼침 → 자식 표시', async ({ page }) => { await enterEditor(page); const header = connGroupHeader(page); @@ -79,6 +81,7 @@ test.describe('@layout-editor route↔connected extensions', () => { expect(expandedCount).toBeGreaterThan(0); }); + /** @effects connected_count_stable_when_entering_extension_edit_mode, extension_child_click_enters_extension_edit_keeping_host_route_highlight */ test('확장 편집 모드 진입 후에도 연결 목록 count 유지 + 호스트 라우트 강조', async ({ page }) => { await enterEditor(page); const header = connGroupHeader(page); @@ -110,6 +113,7 @@ test.describe('@layout-editor route↔connected extensions', () => { expect(hostHighlighted).toBe(true); }); + /** @effects select_route_from_separate_edit_mode_restores_route_mode_and_renders_canvas */ test('확장 편집 모드에서 라우트 클릭 → 캔버스 정상 렌더(빈 화면 회귀 가드)', async ({ page }) => { await enterEditor(page); const header = connGroupHeader(page); diff --git a/tests/Playwright/specs/layout-editor/s6-2-followup.spec.ts b/tests/Playwright/specs/layout-editor/s6-2-followup.spec.ts index d7f74c04..9315fbbd 100644 --- a/tests/Playwright/specs/layout-editor/s6-2-followup.spec.ts +++ b/tests/Playwright/specs/layout-editor/s6-2-followup.spec.ts @@ -10,8 +10,8 @@ * - 항목7: 선택 노드(Button 포함)에 리사이즈 핸들 표시 + 드래그로 px 변화 * - 항목8: 툴바 ↺ 초기화 버튼(dirty 시 활성) * - * @scenario property_modal_draggable + dimension_widget + image_manager_toolbar + session_cache_restore + dirty_badge + beforeunload_guard + resize_handle + reset_button - * @effects modal_draggable + dimension_free_input + image_modal_open + edit_preserved_on_route_switch + tree_dirty_dot + unload_warning + resize_px_change + reset_reverts + * 축 요약(마커 아님 — 평문): property_modal_draggable, dimension_widget, image_manager_toolbar, session_cache_restore, dirty_badge, beforeunload_guard, resize_handle, reset_button. + * 효과 요약(마커 아님 — 평문): modal_draggable, dimension_free_input, image_modal_open, edit_preserved_on_route_switch, tree_dirty_dot, unload_warning, resize_px_change, reset_reverts. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -83,6 +83,7 @@ async function openPropertyModal(page: import('@playwright/test').Page): Promise } test.describe('@layout-editor S6-2 후속', () => { + /** @effects modal_draggable */ test('항목1 — 속성 편집 모달이 draggable + 백드롭 pointerEvents:none', async ({ page }) => { await gotoEditor(page); await openPropertyModal(page); @@ -99,6 +100,7 @@ test.describe('@layout-editor S6-2 후속', () => { expect(pe).toBe('none'); }); + /** @effects dimension_free_input */ test('항목2 — 스타일 탭 width 컨트롤이 dimension 위젯(자유 입력)으로 렌더', async ({ page }) => { await gotoEditor(page); await openPropertyModal(page); @@ -124,6 +126,7 @@ test.describe('@layout-editor S6-2 후속', () => { } }); + /** @effects image_modal_open */ test('항목3 — 툴바 🖼 이미지 버튼 → 이미지 관리 모달', async ({ page }) => { await gotoEditor(page); const imagesBtn = page.getByTestId('g7le-toolbar-images'); @@ -134,6 +137,7 @@ test.describe('@layout-editor S6-2 후속', () => { // 툴바 진입은 onSelect 미전달 → "배경으로 사용" 버튼 없음(빈 목록이면 empty 안내) }); + /** @effects reset_reverts, tree_dirty_dot */ test('항목8 — ↺ 초기화 버튼은 dirty 일 때만 활성', async ({ page }) => { await gotoEditor(page); const resetBtn = page.getByTestId('g7le-toolbar-reset'); @@ -142,6 +146,7 @@ test.describe('@layout-editor S6-2 후속', () => { await expect(resetBtn).toBeDisabled(); }); + /** @effects resize_px_change */ test('항목7 — 선택 노드에 리사이즈 핸들 표시', async ({ page }) => { await gotoEditor(page); // 선택 오버레이 등장 후, width/height 컨트롤 보유 노드면 리사이즈 핸들이 보인다. diff --git a/tests/Playwright/specs/layout-editor/sample-data-canvas-render.spec.ts b/tests/Playwright/specs/layout-editor/sample-data-canvas-render.spec.ts index 75a0508d..d1997f50 100644 --- a/tests/Playwright/specs/layout-editor/sample-data-canvas-render.spec.ts +++ b/tests/Playwright/specs/layout-editor/sample-data-canvas-render.spec.ts @@ -10,13 +10,14 @@ * 채워져 가격(`원`) 텍스트가 나타나는지(= sampleData 가 실제 캔버스 바인딩을 채움) 검증한다. * Vitest 는 JSON+격리 렌더, 본 spec 은 실제 편집기 chrome + 캔버스 통합 렌더. * - * @scenario route_select:data_dependent + sample_provider_source:bundled_editor_spec - * @effects canvas_data_dependent_screen_filled_with_sample_data - * @effects preview_canvas_uses_merged_editor_spec_sampledata + * 축 요약(마커 아님 — 평문): route_select:data_dependent, sample_provider_source:bundled_editor_spec. + * 효과 요약(마커 아님 — 평문): canvas_data_dependent_screen_filled_with_sample_data. + * 효과 요약(마커 아님 — 평문): preview_canvas_uses_merged_editor_spec_sampledata. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; test.describe('@layout-editor 캔버스 샘플 데이터 렌더', () => { + /** @effects canvas_data_dependent_screen_filled_with_sample_data, preview_canvas_uses_merged_editor_spec_sampledata */ test('sirsoft-basic — 상품 상세 라우트 선택 시 캔버스가 product 샘플로 채워진다', async ({ page }) => { const tokenAuth = issueToken('core.templates.layouts.edit'); await authenticatePage(page, tokenAuth); diff --git a/tests/Playwright/specs/layout-editor/sample-gate-body-render.spec.ts b/tests/Playwright/specs/layout-editor/sample-gate-body-render.spec.ts index d6920bc7..0ce77c03 100644 --- a/tests/Playwright/specs/layout-editor/sample-gate-body-render.spec.ts +++ b/tests/Playwright/specs/layout-editor/sample-gate-body-render.spec.ts @@ -17,9 +17,9 @@ * Vitest 는 JSON+격리 렌더로 시뮬레이터 경로를 잠그고, 본 spec 은 실제 편집기 chrome + * 캔버스 통합 렌더에서 게이트 본체가 실제로 보이는지(브라우저 가시 결과)를 측정한다. * - * @scenario gate_body_kind:computed_if_gate + gate_body_kind:lazy_datasource_subtab - * @effects shipping_policy_edit_country_setting_panel_renders_via_computed_gate - * @effects mypage_board_my_comments_subtab_renders_via_lazy_datasource_sample_injection + * 축 요약(마커 아님 — 평문): gate_body_kind:computed_if_gate, gate_body_kind:lazy_datasource_subtab. + * 효과 요약(마커 아님 — 평문): shipping_policy_edit_country_setting_panel_renders_via_computed_gate. + * 효과 요약(마커 아님 — 평문): mypage_board_my_comments_subtab_renders_via_lazy_datasource_sample_injection. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -27,6 +27,7 @@ test.describe('@layout-editor 게이트 본체 캔버스 노출', () => { // (1) _computed 게이트 — 배송정책 수정 폼의 국가별 설정 본체는 `if: {{_computed.activeCountrySetting}}` // 게이트 뒤에 있다. PreviewCanvas 가 computed 블록을 미주입하면 _computed.* 가 항상 undefined → // 국가별 설정 패널이 통째로 비어 렌더된다. 부과정책 상태로 게이트를 열어 본체 렌더를 측정. + /** @effects shipping_policy_edit_country_setting_panel_renders_via_computed_gate */ test('배송정책 수정 → _computed 게이트 국가별 설정 본체가 캔버스에 렌더된다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -62,6 +63,7 @@ test.describe('@layout-editor 게이트 본체 캔버스 노출', () => { // (2) 지연 데이터소스 게이트 — 마이페이지 게시판 활동의 "내 댓글" 서브탭 본체는 지연 소스 // myComments(auto_fetch:false) 에 바인딩된다. 샘플 모드 지연 소스 주입이 없으면 서브탭 게이트를 // 페이지 상태로 열어도 데이터가 비어 본체가 미렌더된다. my_comments 상태 전환 후 본체 렌더 측정. + /** @effects mypage_board_my_comments_subtab_renders_via_lazy_datasource_sample_injection */ test('마이페이지 게시판 → my_comments 서브탭 지연 소스 본체가 캔버스에 렌더된다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); diff --git a/tests/Playwright/specs/layout-editor/scroll-and-extension-lock-followup.spec.ts b/tests/Playwright/specs/layout-editor/scroll-and-extension-lock-followup.spec.ts index bba38618..0272b0c5 100644 --- a/tests/Playwright/specs/layout-editor/scroll-and-extension-lock-followup.spec.ts +++ b/tests/Playwright/specs/layout-editor/scroll-and-extension-lock-followup.spec.ts @@ -15,7 +15,7 @@ * EditorCanvasOverlay)로 커버한다. * * @scenario lock_kind selected_node_state=extension_originated - * @effects extension_node_context_menu_blocked + extension_node_blackbox_selection_normalized + editor_canvas_single_window_scroll + fullscreen_layout_expanded_in_editor + * 효과 요약(마커 아님 — 평문): extension_node_context_menu_blocked, extension_node_blackbox_selection_normalized, editor_canvas_single_window_scroll, fullscreen_layout_expanded_in_editor. */ import type { Page } from '@playwright/test'; import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -49,6 +49,7 @@ async function openAdminUserForm(page: Page): Promise<void> { } test.describe('@layout-editor 스크롤 + 확장 잠금 후속', () => { + /** @effects extension_node_context_menu_blocked, extension_node_blackbox_selection_normalized */ test('확장 주입 조각 내부 자식 클릭 → 진입점 통짜 선택 + ⓘ 메뉴 차단 + 확장 편집 어포던스', async ({ page }) => { await openAdminUserForm(page); @@ -92,6 +93,7 @@ test.describe('@layout-editor 스크롤 + 확장 잠금 후속', () => { expect(result.editExtension).toBe(true); }); + /** @effects editor_canvas_single_window_scroll */ test('일반(min-h-screen) 페이지 — 캔버스 별도 스크롤 없음 + 브라우저 단일 스크롤 + 헤더/트리 고정', async ({ page }) => { await openBasicEditor(page); @@ -125,6 +127,7 @@ test.describe('@layout-editor 스크롤 + 확장 잠금 후속', () => { expect(metrics.treeStaysFixed).toBe(true); }); + /** @effects fullscreen_layout_expanded_in_editor */ test('admin 풀스크린(h-screen) 레이아웃 — 편집 캔버스에서 콘텐츠 자연 높이로 펼침(내부 스크롤 없음)', async ({ page }) => { await openAdminUserForm(page); diff --git a/tests/Playwright/specs/layout-editor/separate-edit-modes.spec.ts b/tests/Playwright/specs/layout-editor/separate-edit-modes.spec.ts index 0501a956..56a2ced7 100644 --- a/tests/Playwright/specs/layout-editor/separate-edit-modes.spec.ts +++ b/tests/Playwright/specs/layout-editor/separate-edit-modes.spec.ts @@ -4,8 +4,8 @@ * Phase 1 에서 액션 골격만 도입됐던 4개 별도 편집 모드의 캔버스 단독 렌더 + 진입/이탈 + * URL 동기화를 브라우저에서 검증한다(단위 테스트가 못 잡는 render-cycle + URL pushState). * - * @scenario edit_mode + url_sync + followup_action - * @effects route_tree_renders_base_modal_extension_groups + base_edit_loads_and_renders_base_layout_standalone + modal_edit_renders_modal_open_standalone_no_dim + extension_edit_loads_via_layout_extensions_api + enter_edit_mode_pushes_edit_query_to_url + refresh_with_edit_query_restores_edit_mode + back_button_exits_edit_mode_to_route + * 축 요약(마커 아님 — 평문): edit_mode, url_sync, followup_action. + * 효과 요약(마커 아님 — 평문): route_tree_renders_base_modal_extension_groups, base_edit_loads_and_renders_base_layout_standalone, modal_edit_renders_modal_open_standalone_no_dim, extension_edit_loads_via_layout_extensions_api, enter_edit_mode_pushes_edit_query_to_url, refresh_with_edit_query_restores_edit_mode, back_button_exits_edit_mode_to_route. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -58,6 +58,7 @@ async function editorNodeCount(page: import('@playwright/test').Page): Promise<n } test.describe('@layout-editor 별도 편집 모드', () => { + /** @effects base_edit_loads_and_renders_base_layout_standalone, enter_edit_mode_pushes_edit_query_to_url, route_tree_renders_base_modal_extension_groups */ test('base 진입 → URL ?edit= pushState + base 캔버스 단독 렌더', async ({ page }) => { await enterEditor(page); @@ -71,6 +72,7 @@ test.describe('@layout-editor 별도 편집 모드', () => { await expect.poll(() => editorNodeCount(page)).toBeGreaterThan(0); }); + /** @effects back_button_exits_edit_mode_to_route */ test('base 진입 후 뒤로가기 → 편집 모드 종료(URL ?edit= 제거)', async ({ page }) => { await enterEditor(page); await clickTreeItem(page, /_admin_base/); @@ -82,6 +84,7 @@ test.describe('@layout-editor 별도 편집 모드', () => { await expect(page.locator('[data-mode="base"]')).toHaveCount(0); }); + /** @effects extension_edit_loads_via_layout_extensions_api */ test('확장 주입 그룹 항목 진입 → 확장 조각 캔버스 렌더 + URL ?edit=__extension__', async ({ page }) => { await enterEditor(page); @@ -93,6 +96,7 @@ test.describe('@layout-editor 별도 편집 모드', () => { await expect.poll(() => editorNodeCount(page)).toBeGreaterThan(0); }); + /** @effects refresh_with_edit_query_restores_edit_mode */ test('?edit=__extension__ URL 직접 진입(새로고침) → 확장 편집 모드 복원', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -107,6 +111,7 @@ test.describe('@layout-editor 별도 편집 모드', () => { await expect.poll(() => editorNodeCount(page)).toBeGreaterThan(0); }); + /** @effects modal_edit_renders_modal_open_standalone_no_dim */ test('모달 그룹 항목 진입 → 모달 단독 open 렌더', async ({ page }) => { await enterEditor(page); diff --git a/tests/Playwright/specs/layout-editor/state-scope-optional-segment.spec.ts b/tests/Playwright/specs/layout-editor/state-scope-optional-segment.spec.ts index 26288458..0af08600 100644 --- a/tests/Playwright/specs/layout-editor/state-scope-optional-segment.spec.ts +++ b/tests/Playwright/specs/layout-editor/state-scope-optional-segment.spec.ts @@ -15,9 +15,10 @@ * 열어 상태 토글이 화면에 나타나는지를 확인한다 — 스펙과 라우트 평가가 실제로 만나는 * 지점은 브라우저에만 있다. * - * @scenario route_setting × surface - * @effects editor_state_switcher_visible_on_default_shop_route - * + editor_state_switcher_visible_after_route_path_change + * 축 요약(마커 아님 — 평문): route_setting, surface. 요약을 시나리오 축 마커로 적으면 파서가 + * `=` 없는 토큰을 버려 빈 조합 `{}` 이 된다. + * + * 효과 요약(마커 아님 — 평문): editor_state_switcher_visible_on_default_shop_route, editor_state_switcher_visible_after_route_path_change. * * 활성화 절차: PLAYWRIGHT_BASE_URL 과 PlaywrightIssueToken 발급이 가능한 환경에서 * `test.describe.skip` → `test.describe` 로 바꾼다. 상점 주소를 바꾸는 축은 이커머스 @@ -29,6 +30,7 @@ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth' const PRODUCTS_ROUTE = '/shop/products'; test.describe.skip('레이아웃 편집기 — 상점 화면 상태 전환 노출', () => { + /** @effects editor_state_switcher_visible_on_default_shop_route */ test('기본 상점 주소에서 상태 전환 토글이 보인다', async ({ page }) => { const token = issueToken('core.templates.layouts.edit'); await authenticatePage(page, token); @@ -44,6 +46,7 @@ test.describe.skip('레이아웃 편집기 — 상점 화면 상태 전환 노 await expect(switcher).toBeVisible({ timeout: 20_000 }); }); + /** @effects editor_state_switcher_visible_after_route_path_change */ test('상점 주소를 바꾼 사이트에서도 상태 전환 토글이 보인다', async ({ page }) => { // 이 케이스는 basic_info.route_path 가 기본값이 아닌 환경에서만 의미가 있다. // 검수 환경의 실제 설정값을 읽어 라우트를 만든다 — 기본값이면 위 케이스와 같아진다. diff --git a/tests/Playwright/specs/layout-editor/table-editor.spec.ts b/tests/Playwright/specs/layout-editor/table-editor.spec.ts index ffc0b5ec..a568d3a6 100644 --- a/tests/Playwright/specs/layout-editor/table-editor.spec.ts +++ b/tests/Playwright/specs/layout-editor/table-editor.spec.ts @@ -14,8 +14,8 @@ * 을 검증한다. 행/열/병합/해제/테두리/셀텍스트 round-trip 은 단위(tableGridModel.test.ts / * TableEditor.test.tsx)가 잠그고, 본 E2E 는 구조 편집의 라이브 반영·저장을 브라우저로 확인한다. * - * @scenario table_node_editor + add_row_col + merge + live_persist - * @effects property_modal_dispatches_table_node_editor_in_props_tab_by_kind, add_row_inserts_blank_row_keeps_col_count, add_column_inserts_blank_col, shift_select_range_then_merge_sets_origin_span_removes_absorbed, live_add_row_col_merge_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout + * 축 요약(마커 아님 — 평문): table_node_editor, add_row_col, merge, live_persist. + * 효과 요약(마커 아님 — 평문): property_modal_dispatches_table_node_editor_in_props_tab_by_kind, add_row_inserts_blank_row_keeps_col_count, add_column_inserts_blank_col, shift_select_range_then_merge_sets_origin_span_removes_absorbed, live_add_row_col_merge_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -138,6 +138,7 @@ async function addTable(page: Page, sandbox = false): Promise<string> { } test.describe('@layout-editor table 노드 에디터(빌트인 STRUCT-TREE)', () => { + /** @effects property_modal_dispatches_table_node_editor_in_props_tab_by_kind, add_row_inserts_blank_row_keeps_col_count, add_column_inserts_blank_col */ test('Table 선택 시 table 에디터가 속성 탭에 마운트되고 행/열 추가가 grid 반영', async ({ page }) => { await gotoEditor(page); const tablePath = await addTable(page); @@ -165,6 +166,7 @@ test.describe('@layout-editor table 노드 에디터(빌트인 STRUCT-TREE)', () expect(colsAfter).toBe(colsBefore + 1); }); + /** @effects shift_select_range_then_merge_sets_origin_span_removes_absorbed */ test('셀 선택 + Shift 영역 선택 → 병합 버튼 활성 → 병합 반영', async ({ page }) => { await gotoEditor(page); const tablePath = await addTable(page); @@ -187,6 +189,7 @@ test.describe('@layout-editor table 노드 에디터(빌트인 STRUCT-TREE)', () // 저장(PUT)하는 테스트는 편집 결과가 그대로 영속되므로 제품 화면(home)이 아니라 E2E 전용 // 시드 화면(e2e_sandbox)을 대상으로 한다. 제품 화면에 저장하면 실행마다 빈 표가 누적된다 // (실측: 7개, 20,321 → 33,696 bytes). 시드 화면은 globalSetup 이 매 실행 원본으로 덮어쓴다. + /** @effects live_add_row_col_merge_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout */ test('table 편집 후 저장 → PUT 200', async ({ page }) => { await gotoEditor(page, sandboxRouteParam()); const tablePath = await addTable(page, true); diff --git a/tests/Playwright/specs/layout-editor/table-inplace-overlay.spec.ts b/tests/Playwright/specs/layout-editor/table-inplace-overlay.spec.ts index 9e1cc519..666ecd0f 100644 --- a/tests/Playwright/specs/layout-editor/table-inplace-overlay.spec.ts +++ b/tests/Playwright/specs/layout-editor/table-inplace-overlay.spec.ts @@ -15,8 +15,8 @@ * 을 검증한다. 변형 정합(span 보정/흡수셀/밴드 이동)은 단위(TableInplaceOverlay.test.tsx / * tableGridModel.test.ts)가 잠그고, 본 E2E 는 인플레이스 라이브 반영·저장을 브라우저로 확인. * - * @scenario table_canvas_inplace_overlay + inplace_add_row_col + inplace_merge + live_persist - * @effects editorcanvasoverlay_dispatches_canvasoverlay_by_kind_with_measured_cellboxes, table_inplace_overlay_registered_via_registercoreeditors_kind_agnostic, inplace_gutter_add_row_col_shares_tablegridmutations_with_property_panel, inplace_shift_select_merge_sets_origin_span_removes_absorbed, live_inplace_cell_edit_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout + * 축 요약(마커 아님 — 평문): table_canvas_inplace_overlay, inplace_add_row_col, inplace_merge, live_persist. + * 효과 요약(마커 아님 — 평문): editorcanvasoverlay_dispatches_canvasoverlay_by_kind_with_measured_cellboxes, table_inplace_overlay_registered_via_registercoreeditors_kind_agnostic, inplace_gutter_add_row_col_shares_tablegridmutations_with_property_panel, inplace_shift_select_merge_sets_origin_span_removes_absorbed, live_inplace_cell_edit_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -135,6 +135,7 @@ async function selectFirstCell(page: Page): Promise<boolean> { } test.describe('@layout-editor table 캔버스 인플레이스 오버레이(빌트인)', () => { + /** @effects editorcanvasoverlay_dispatches_canvasoverlay_by_kind_with_measured_cellboxes, table_inplace_overlay_registered_via_registercoreeditors_kind_agnostic */ test('표 선택 시 오버레이 마운트(모서리 추가) + 셀 선택 시 거터/도구 노출', async ({ page }) => { await gotoEditor(page); const tablePath = await addTable(page); @@ -179,6 +180,7 @@ test.describe('@layout-editor table 캔버스 인플레이스 오버레이(빌 expect(handleGotPointerDown).toBe(true); }); + /** @effects inplace_gutter_add_row_col_shares_tablegridmutations_with_property_panel */ test('선택 셀 행/열 추가 거터 → 캔버스 행/열 증가', async ({ page }) => { await gotoEditor(page); const tablePath = await addTable(page); @@ -192,6 +194,7 @@ test.describe('@layout-editor table 캔버스 인플레이스 오버레이(빌 expect(await page.locator(`[data-editor-name="Tr"]`).count()).toBeGreaterThan(rowsBefore); }); + /** @effects inplace_shift_select_merge_sets_origin_span_removes_absorbed */ test('선택 셀 오른쪽 병합 → colspan 반영', async ({ page }) => { await gotoEditor(page); const tablePath = await addTable(page); @@ -218,6 +221,7 @@ test.describe('@layout-editor table 캔버스 인플레이스 오버레이(빌 // 오염 시점과 정확히 같은 크기로 되돌아왔다(편집기가 들고 있던 문서를 통째로 다시 저장). // 그래서 원복에 기대지 않고 저장 대상 자체를 시드 화면으로 분리했다. 시드 화면은 globalSetup 이 // 매 실행 fixture 원본으로 덮어쓰므로 회차 간 누적이 성립하지 않는다. + /** @effects live_inplace_cell_edit_save_persists_to_user_page, editor_save_specs_target_sandbox_layout_not_product_layout */ test('인플레이스 편집 후 저장 → PUT 200', async ({ page }) => { await gotoEditor(page, sandboxRouteParam()); const tablePath = await addTable(page, true); diff --git a/tests/Playwright/specs/layout-editor/tabnav-inplace.spec.ts b/tests/Playwright/specs/layout-editor/tabnav-inplace.spec.ts index 3497b1bc..26c0d012 100644 --- a/tests/Playwright/specs/layout-editor/tabnav-inplace.spec.ts +++ b/tests/Playwright/specs/layout-editor/tabnav-inplace.spec.ts @@ -8,8 +8,8 @@ * 실제 마운트되어 탭 헤더에 +추가/✕삭제/◀▶이동 어포던스를 노출함을 브라우저로 실증한다 * (확장점이 문서상 확장점이 아니라 실동작 경로임을 증명). * - * @scenario template_registered_canvas_overlay + tabnav_inplace_affordances + add_remove_persist - * @effects template_registerCanvasOverlay_mounts_in_live_editor, tabnav_header_shows_add_remove_move_affordances, inplace_add_patches_node_props_tabs_same_ssot_as_property_panel + * 축 요약(마커 아님 — 평문): template_registered_canvas_overlay, tabnav_inplace_affordances, add_remove_persist. + * 효과 요약(마커 아님 — 평문): template_registerCanvasOverlay_mounts_in_live_editor, tabnav_header_shows_add_remove_move_affordances, inplace_add_patches_node_props_tabs_same_ssot_as_property_panel. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; import type { Page } from '@playwright/test'; @@ -76,6 +76,7 @@ async function selectByPath(page: Page, path: string): Promise<void> { } test.describe('@layout-editor TabNavigation 인플레이스(템플릿 registerCanvasOverlay 레퍼런스)', () => { + /** @effects template_registerCanvasOverlay_mounts_in_live_editor, tabnav_header_shows_add_remove_move_affordances, inplace_add_patches_node_props_tabs_same_ssot_as_property_panel */ test('admin 편집기에서 TabNavigation 선택 시 템플릿 등록 인플레이스 어포던스가 마운트된다', async ({ page }) => { await gotoAdminEditor(page); const navPath = await addTabNavigation(page); diff --git a/tests/Playwright/specs/layout-editor/tree-version-badge-and-modal-scroll.spec.ts b/tests/Playwright/specs/layout-editor/tree-version-badge-and-modal-scroll.spec.ts index acdc6946..360b0677 100644 --- a/tests/Playwright/specs/layout-editor/tree-version-badge-and-modal-scroll.spec.ts +++ b/tests/Playwright/specs/layout-editor/tree-version-badge-and-modal-scroll.spec.ts @@ -14,8 +14,8 @@ * "버전 기록"이 활성화되어 확장 전용 버전 API 의 목록 모달이 열린다. 저장/복원 동기화는 * 단위(useExtensionDocument/리듀서/VersionHistoryModal) + 백엔드 Feature 테스트가 커버. * - * @scenario edit_mode + layout_history + scroll_lock_source - * @effects editor_routes_response_includes_layout_versions_map + tree_version_badge_matches_versions_map + no_badge_for_layouts_without_history + modal_edit_keeps_canvas_scrollbar + extension_badge_matches_current_version + extension_mode_version_history_modal_opens + * 축 요약(마커 아님 — 평문): edit_mode, layout_history, scroll_lock_source. + * 효과 요약(마커 아님 — 평문): editor_routes_response_includes_layout_versions_map, tree_version_badge_matches_versions_map, no_badge_for_layouts_without_history, modal_edit_keeps_canvas_scrollbar, extension_badge_matches_current_version, extension_mode_version_history_modal_opens. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -33,6 +33,7 @@ async function enterEditor(page: import('@playwright/test').Page, url: string): } test.describe('@layout-editor 트리 버전 배지 + 모달 편집 스크롤 유지', () => { + /** @effects editor_routes_response_includes_layout_versions_map, tree_version_badge_matches_versions_map, no_badge_for_layouts_without_history */ test('라우트 트리 버전 배지가 routes 응답 layout_versions 맵과 1:1 정합', async ({ page }) => { await enterEditor(page, EDITOR_URL); await page.waitForSelector('[data-testid="g7le-route-tree-item"]', { timeout: 30_000 }); @@ -77,6 +78,7 @@ test.describe('@layout-editor 트리 버전 배지 + 모달 편집 스크롤 유 } }); + /** @effects modal_edit_keeps_canvas_scrollbar */ test('모달 편집 모드에서 body 스크롤 락이 무력화되어 캔버스 스크롤바 유지', async ({ page }) => { await enterEditor(page, MODAL_DIRECT_URL); await expect(page.locator('[data-mode="modal"]')).toBeAttached({ timeout: 30_000 }); @@ -101,6 +103,7 @@ test.describe('@layout-editor 트리 버전 배지 + 모달 편집 스크롤 유 expect(state.inline).not.toBe('hidden'); }); + /** @effects extension_badge_matches_current_version, extension_mode_version_history_modal_opens */ test('확장 노드 버전 배지가 layout-extensions 응답 current_version 과 정합 + 확장 편집 모드 버전 기록 모달', async ({ page }) => { await enterEditor(page, EDITOR_URL); await page.waitForSelector('[data-testid="g7le-route-tree-item"]', { timeout: 30_000 }); diff --git a/tests/Playwright/specs/layout-editor/version-preview.spec.ts b/tests/Playwright/specs/layout-editor/version-preview.spec.ts index 222954f5..1eb3a406 100644 --- a/tests/Playwright/specs/layout-editor/version-preview.spec.ts +++ b/tests/Playwright/specs/layout-editor/version-preview.spec.ts @@ -10,8 +10,8 @@ * 버전 목록·복원의 응답 정합/권한 가드는 LayoutControllerTest.php 가 커버 — 본 spec 은 * 위지윅 chrome 의 버튼 활성/모달 노출/새 창 열림 책임만. * - * @scenario toolbar_button + edit_mode + layout_name_present + versions_state + preview_result - * @effects preview_button_enabled_when_layout_selected + preview_creates_temp_record_and_opens_window_with_token + version_button_disabled_when_no_layout_name + version_modal_lists_saved_versions_with_change_summary + version_modal_empty_state_when_no_versions + * 축 요약(마커 아님 — 평문): toolbar_button, edit_mode, layout_name_present, versions_state, preview_result. + * 효과 요약(마커 아님 — 평문): preview_button_enabled_when_layout_selected, preview_creates_temp_record_and_opens_window_with_token, version_button_disabled_when_no_layout_name, version_modal_lists_saved_versions_with_change_summary, version_modal_empty_state_when_no_versions. */ import { test, expect, issueToken, authenticatePage } from '../../fixtures/auth'; @@ -30,6 +30,7 @@ async function enterEditor( } test.describe('@layout-editor version history + preview', () => { + /** @effects version_modal_lists_saved_versions_with_change_summary, version_modal_empty_state_when_no_versions */ test('🕘 버전 기록 버튼 클릭 시 모달 열림 + 목록 또는 빈 상태 노출', async ({ page }) => { await enterEditor(page, EDITOR_URL_HOME); await page.waitForSelector('[data-testid="g7le-preview-frame"]', { timeout: 30_000 }); @@ -57,6 +58,7 @@ test.describe('@layout-editor version history + preview', () => { await expect(page.getByTestId('g7le-version-history')).toHaveCount(0); }); + /** @effects preview_button_enabled_when_layout_selected, preview_creates_temp_record_and_opens_window_with_token */ test('👁 미리보기 버튼 클릭 시 /preview/{token} 새 창 열림', async ({ page, context }) => { await enterEditor(page, EDITOR_URL_HOME); await page.waitForSelector('[data-testid="g7le-preview-frame"]', { timeout: 30_000 }); @@ -77,6 +79,7 @@ test.describe('@layout-editor version history + preview', () => { await popup.close(); }); + /** @effects version_button_disabled_when_no_layout_name */ test('라우트 미선택 시 버전/미리보기 버튼 비활성', async ({ page }) => { await enterEditor(page, EDITOR_URL_NO_ROUTE); // 라우트 미선택 상태(layoutName 없음) — 두 버튼 모두 disabled diff --git a/tests/Playwright/specs/layout-expression-sandbox.spec.ts b/tests/Playwright/specs/layout-expression-sandbox.spec.ts new file mode 100644 index 00000000..f7ece623 --- /dev/null +++ b/tests/Playwright/specs/layout-expression-sandbox.spec.ts @@ -0,0 +1,193 @@ +/** + * E2E: 레이아웃 표현식 평가 샌드박스 (배포 번들 기준, KVE-2026-1915) + * + * 배경: 표현식 평가가 `new Function`/`with(ctx)` 로 실행되어 + * `''.constructor.constructor('code')()` 형태의 샌드박스 탈출이 가능했다. + * engine-v1.59.0 에서 화이트리스트 AST 평가기(SafeExpressionEvaluator)로 교체했다. + * + * 단위 테스트(SafeExpressionEvaluator.test.ts)가 소스 레벨을 잠그지만, 실제 사용자가 + * 실행하는 것은 빌드된 번들이므로 브라우저에서 배포 번들의 공개 API(`G7Core.evaluateCondition`)를 + * 그대로 호출해 (a) 정상 표현식 회귀 없음 + (b) 익스플로잇 미실행 양면을 잠근다. + * + * 시나리오 축(case)·효과는 매니페스트 tests/scenarios/layout-expression-sandbox.yaml 참조. + * 각 test 의 `// @scenario case=…` 라인 마커가 축 조합을, `// @effects …` 가 효과를 커버한다. + * + * 효과 목록을 이 파일 레벨에 몰아 적지 않는다 — 커버리지 룰은 마커 레벨을 구분하지 않으므로, + * 파일 레벨 목록이 있으면 해당 test 를 지워도 효과가 "언급됨" 으로 집계돼 삭제가 무증상 green + * 이 된다. 마커는 test 에만 둔다. + */ +import { test, expect } from '../fixtures/auth'; + +test.describe('레이아웃 표현식 평가 샌드박스', () => { + test.beforeEach(async ({ page }) => { + // 코어 번들이 부팅된 화면이면 충분하다(로그인 화면도 엔진을 부팅한다). + await page.goto('/admin/login'); + await page.waitForFunction( + () => typeof (window as any).G7Core?.evaluateCondition === 'function', + null, + { timeout: 30_000 }, + ); + }); + + // @scenario case=safe_expression + // @effects same_expression_same_value_across_paths + test('정상 표현식(삼항·옵셔널체이닝·nullish)이 배포 번들에서 정상 평가된다', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + const ctx = { user: { name: '홍길동' }, count: 2 }; + return { + ternary: g7.evaluateCondition('{{user?.name ? true : false}}', ctx), + nullish: g7.evaluateCondition('{{(user?.missing ?? "fallback") === "fallback"}}', ctx), + compare: g7.evaluateCondition('{{count > 1}}', ctx), + }; + }); + + expect(result.ternary).toBe(true); + expect(result.nullish).toBe(true); + expect(result.compare).toBe(true); + }); + + // @scenario case=arrow_template_literal + // @effects same_expression_same_value_across_paths + test('화살표 함수·스프레드·템플릿 리터럴·new Date 가 배포 번들에서 동작한다', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + const ctx = { items: [{ v: 'a' }, { v: 'b' }, { v: 'a' }], $args: ['x'] }; + return { + // 화살표 함수 + includes + arrow: g7.evaluateCondition("{{(items ?? []).filter(i => i.v === 'a').length === 2}}", ctx), + // 스프레드 + Set + Array.from(new Set(...)) + spreadSet: g7.evaluateCondition("{{Array.from(new Set((items ?? []).map(i => i.v))).length === 2}}", ctx), + // 템플릿 리터럴 (내비게이션 경로 조립 형태) + template: g7.evaluateCondition('{{`/mypage/${$args[0]}` === "/mypage/x"}}', ctx), + // new Date + date: g7.evaluateCondition('{{new Date("2020-01-01").getTime() > 0}}', ctx), + }; + }); + + expect(result.arrow).toBe(true); + expect(result.spreadSet).toBe(true); + expect(result.template).toBe(true); + expect(result.date).toBe(true); + }); + + // @scenario case=destructuring_param + // @effects same_expression_same_value_across_paths + test('화살표 파라미터 배열 구조분해(([k, v])·홀)가 배포 번들에서 동작한다 (engine-v1.60.5 회귀)', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + const ctx = { + sel: { 5: 50, 6: null }, + perms: { 'admin.manage': true, read: true, write: false }, + errors: { name: ['이름은 필수입니다'] }, + }; + return { + // _purchase_card.json 장바구니/바로구매 본문 형태 — 홀 + 쌍 분해 + hole: g7.evaluateCondition('{{Object.entries(sel ?? {}).filter(([, vid]) => vid != null).length === 1}}', ctx), + pair: g7.evaluateCondition('{{Object.entries(sel ?? {}).filter(([, v]) => v != null).map(([gid, vid]) => Number(gid) + Number(vid)).includes(55)}}', ctx), + single: g7.evaluateCondition('{{Object.entries(sel ?? {}).map(([k]) => k).includes("5")}}', ctx), + // 게시판 환경설정 권한 computed 형태: filter([key]) + startsWith + map([key]) + entriesFilterMap: g7.evaluateCondition( + "{{Object.entries(perms ?? {}).filter(([key]) => !key.startsWith('admin.')).map(([key]) => key).length === 2}}", + ctx, + ), + // 검증 오류 표시 형태: ([field, messages]) + fieldMessages: g7.evaluateCondition( + "{{Object.entries(errors ?? {}).map(([field, messages]) => field + ':' + messages[0])[0] === 'name:이름은 필수입니다'}}", + ctx, + ), + }; + }); + + expect(result.hole).toBe(true); + expect(result.pair).toBe(true); + expect(result.single).toBe(true); + expect(result.entriesFilterMap).toBe(true); + expect(result.fieldMessages).toBe(true); + }); + + // @scenario case=constructor_escape + // @effects sandbox_escape_blocked, dangerous_payload_does_not_set_global + test('constructor 체인 샌드박스 탈출이 코드를 실행하지 못한다', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + // 익스플로잇: 성공하면 window.__g7jsi 전역이 설정된다. + delete (window as any).__g7jsi; + const payloads = [ + "{{''.constructor.constructor('window.__g7jsi = 1')()}}", + "{{''['constructor']['constructor']('window.__g7jsi = 2')()}}", + '{{({}).__proto__}}', + "{{Function('window.__g7jsi = 3')()}}", + ]; + // 각 페이로드를 평가 — 평가기는 거부(예외→false)해야 하며 코드는 실행되지 않아야 한다. + const results = payloads.map((p) => { + try { + return g7.evaluateCondition(p, {}); + } catch { + return false; + } + }); + return { + globalSet: (window as any).__g7jsi !== undefined, + allBlocked: results.every((r) => !r), + }; + }); + + // 익스플로잇 코드가 실행되지 않았다(전역 canary 미설정). + expect(result.globalSet).toBe(false); + // 위험 표현식은 truthy 로 평가되지 않는다(거부). + expect(result.allBlocked).toBe(true); + }); + + // @scenario case=nonstring_key_escape + // @effects sandbox_escape_blocked, dangerous_payload_does_not_set_global + test('비-문자열 computed 키·Object 리플렉션 탈출이 배포 번들에서 코드를 실행하지 못한다', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + delete (window as any).__g7jsi; + const payloads = [ + // 배열 키가 ToPropertyKey 강제변환으로 constructor 도달 (engine-v1.60.1 이전 우회) + "{{''[['constructor']][['constructor']]('window.__g7jsi = 1')()}}", + // 문자열 조립 난독화 — 정적 룰이 못 잡는 형태(런타임 정규화가 최종 게이트) + "{{''[['const' + 'ructor']][['const' + 'ructor']]('window.__g7jsi = 2')()}}", + // 객체 toString 강제변환 키 + "{{''[{ toString: () => 'constructor' }]}}", + // Object 리플렉션 static 으로 Function 도달 + "{{Object.getOwnPropertyDescriptor(Object.getPrototypeOf(String), 'constructor').value('window.__g7jsi = 3')()}}", + // 프로토타입 오염 시도 + '{{Object.setPrototypeOf({}, { polluted: 1 })}}', + ]; + const results = payloads.map((p) => { + try { + return g7.evaluateCondition(p, {}); + } catch { + return false; + } + }); + return { + globalSet: (window as any).__g7jsi !== undefined, + allBlocked: results.every((r) => !r), + }; + }); + + expect(result.globalSet).toBe(false); + expect(result.allBlocked).toBe(true); + }); + + // @scenario case=object_facade_safe + // @effects same_expression_same_value_across_paths + test('안전한 Object 데이터 메서드(keys/assign/create)가 배포 번들에서 정상 동작한다', async ({ page }) => { + const result = await page.evaluate(() => { + const g7 = (window as any).G7Core; + const ctx = { o: { a: 1, b: 2 }, base: { x: 1 }, k: 'y', v: 2 }; + return { + keys: g7.evaluateCondition("{{Object.keys(o).join(',') === 'a,b'}}", ctx), + // 실제 레이아웃(_tab_reviews.json)의 필터 맵 생성 패턴 + createAssign: g7.evaluateCondition('{{Object.assign(Object.create(null), base, { [k]: v }).y === 2}}', ctx), + }; + }); + + expect(result.keys).toBe(true); + expect(result.createAssign).toBe(true); + }); +}); diff --git a/tests/Playwright/specs/layout-script-src-trusted-host.spec.ts b/tests/Playwright/specs/layout-script-src-trusted-host.spec.ts new file mode 100644 index 00000000..b06eaa0b --- /dev/null +++ b/tests/Playwright/specs/layout-script-src-trusted-host.spec.ts @@ -0,0 +1,122 @@ +/** + * E2E: 레이아웃 외부 스크립트 신뢰 출처 허용목록 (배포 번들 기준, KVE-2026-1915) + * + * 배경: B-2 원격 스크립트 차단이 same-origin 이 아닌 모든 스크립트를 무조건 막아 + * 번들 확장(CKEditor5 cdn.ckeditor.com, Daum 우편번호 t1.daumcdn.net)의 CDN 스크립트까지 + * 깨뜨렸다. 확장이 manifest(`trusted_script_hosts`)로 선언한 호스트를 코어가 집계해 + * `window.G7Config.trustedScriptHosts` 로 노출하고, 런타임 스크립트 로더가 그 목록만 + * 예외로 허용하도록 고쳤다. + * + * 이 스펙은 서버→블레이드→배포 번들로 이어지는 주입 배선이 실제로 연결돼 있는지 + * (G7Config.trustedScriptHosts 노출), 활성 확장의 선언이 그 목록에 반영되는지, 그리고 + * 실제 화면 로드에서 신뢰 목록 밖 외부 스크립트가 하나도 실행되지 않는지(차단 관찰)를 잠근다. + * + * 차단 결정 함수(isAllowedScriptSrc) 자체는 minify 된 배포 번들에서 이름으로 호출할 수 없어 + * 단위 스위트(TemplateApp.scriptSrc.test.ts)가 고정한다. 이 스펙은 그 결정의 + * 관찰 가능한 결과 — "실제 페이지에 same-origin/신뢰호스트 외 스크립트가 로드되지 않음" — + * 을 네트워크 레벨에서 무조건 측정한다. + * + * authority 우회(`/\/evil.com/x.js` · 탭·개행 삽입)도 이 관찰에 포함된다. 그 형태는 + * 브라우저가 외부 origin 으로 해석하므로, 판정이 뚫리면 네트워크에 cross-origin script + * 요청으로 나타나 아래 offenders 단언에 걸린다 — 요청 URL 은 이미 해석된 절대 URL 이라 + * 우회 문자열 형태와 무관하게 origin 으로 판정된다. + * + * 시나리오 축(case)·효과는 매니페스트 tests/scenarios/trusted-script-hosts.yaml 참조. + * 각 test 의 `// @scenario case=…` 라인 마커가 축 조합을, `// @effects …` 가 효과를 커버한다. + * + * 효과 목록을 이 파일 레벨에 몰아 적지 않는다 — 커버리지 룰은 마커 레벨을 구분하지 않으므로, + * 파일 레벨 목록이 있으면 해당 test 를 지워도 효과가 "언급됨" 으로 집계돼 삭제가 무증상 green + * 이 된다. 마커는 test 에만 둔다. + */ +import { test, expect } from '../fixtures/auth'; + +test.describe('레이아웃 외부 스크립트 신뢰 출처 허용목록', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/admin/login'); + await page.waitForFunction( + () => typeof (window as any).G7Config !== 'undefined', + null, + { timeout: 30_000 }, + ); + }); + + // @scenario case=trusted_hosts_exposed_in_config + // @effects trusted_script_host_allowlist_wired + test('배포 번들에 G7Config.trustedScriptHosts 가 배열로 노출된다', async ({ page }) => { + const shape = await page.evaluate(() => { + const hosts = (window as any).G7Config?.trustedScriptHosts; + return { isArray: Array.isArray(hosts), allStrings: Array.isArray(hosts) && hosts.every((h: unknown) => typeof h === 'string') }; + }); + + expect(shape.isArray).toBe(true); + expect(shape.allStrings).toBe(true); + }); + + // @scenario case=active_extension_host_reflected + // @effects trusted_script_host_allowlist_wired + test('활성 확장이 선언한 신뢰 호스트가 허용목록에 반영된다', async ({ page }) => { + const result = await page.evaluate(() => { + const cfg = (window as any).G7Config ?? {}; + const hosts: string[] = Array.isArray(cfg.trustedScriptHosts) ? cfg.trustedScriptHosts : []; + const plugins: Array<{ identifier?: string }> = Array.isArray(cfg.activePlugins) ? cfg.activePlugins : []; + const has = (id: string) => plugins.some((p) => p.identifier === id); + return { + ckeditorActive: has('sirsoft-ckeditor5'), + daumActive: has('sirsoft-daum_postcode'), + hasCkeditorHost: hosts.includes('cdn.ckeditor.com'), + hasDaumHost: hosts.includes('t1.daumcdn.net'), + }; + }); + + // 확장이 활성일 때만 그 선언 호스트가 목록에 있어야 한다(비활성이면 스킵 — 서버 상태 독립). + if (result.ckeditorActive) { + expect(result.hasCkeditorHost).toBe(true); + } + if (result.daumActive) { + expect(result.hasDaumHost).toBe(true); + } + }); + + // @scenario case=untrusted_external_script_not_loaded + // @effects untrusted_external_script_blocked + test('실제 화면 로드에서 same-origin·신뢰호스트 외 스크립트는 하나도 로드되지 않는다', async ({ page }) => { + // 새 로드의 모든 script 네트워크 요청을 수집한다 + const scriptUrls: string[] = []; + page.on('request', (req) => { + if (req.resourceType() === 'script') { + scriptUrls.push(req.url()); + } + }); + + // beforeEach 가 이미 진입한 화면을 다시 로드해 엔진 부팅부터의 스크립트 요청을 포착한다 + await page.reload({ waitUntil: 'networkidle' }); + + const cfg = await page.evaluate(() => ({ + origin: window.location.origin, + hosts: (Array.isArray((window as any).G7Config?.trustedScriptHosts) + ? (window as any).G7Config.trustedScriptHosts + : []) as string[], + })); + const trusted = new Set(cfg.hosts.map((h) => h.toLowerCase())); + + // 측정 대상이 실제로 존재하는지(공허한 통과 방지) 확인 — 최소한 번들 스크립트는 로드된다 + expect(scriptUrls.length).toBeGreaterThan(0); + + const offenders = scriptUrls.filter((url) => { + let u: URL; + try { + u = new URL(url); + } catch { + return false; // 파싱 불가 URL 은 판정 대상 아님 + } + if (u.protocol !== 'http:' && u.protocol !== 'https:') { + return false; // data:/blob: 등은 별개 축 + } + const sameOrigin = u.origin === cfg.origin; + const trustedHost = trusted.has(u.hostname.toLowerCase()); + return ! sameOrigin && ! trustedHost; + }); + + expect(offenders, `허용되지 않은 외부 스크립트가 로드됨: ${offenders.join(', ')}`).toEqual([]); + }); +}); diff --git a/tests/Playwright/specs/smoke/admin-base.spec.ts b/tests/Playwright/specs/smoke/admin-base.spec.ts index fac61346..d70dc0fb 100644 --- a/tests/Playwright/specs/smoke/admin-base.spec.ts +++ b/tests/Playwright/specs/smoke/admin-base.spec.ts @@ -195,7 +195,13 @@ test('@smoke #399 Phase 1.8 - admin 페이지 헤더의 refresh 버튼이 .btn-i // Phase 1.8: page_header 안의 refresh_button 이 인라인 Tailwind 5토큰에서 // 표준 .btn-icon 시맨틱 자산으로 통합됐는지 확인. - await expect(page.locator('#refresh_button').first()).toHaveClass(/btn-icon/); + // + // 마운트 대기는 형제 검증(Phase 1.7 의 10s, admin-role-list-refresh.spec 의 15s)과 같은 + // 폭으로 맞춘다 — 기본 5s 로 두면 여러 spec 이 병렬로 도는 실행에서 SPA 마운트가 5s 를 + // 넘겨 간헐 red 가 난다(클래스 불일치가 아니라 element not found 로 나타나 오진하기 쉽다). + const refreshButton = page.locator('#refresh_button').first(); + await refreshButton.waitFor({ state: 'visible', timeout: 15_000 }); + await expect(refreshButton).toHaveClass(/btn-icon/); // Phase 1.9: page_header 의 우측 버튼 묶음 컨테이너가 // 인라인 'flex items-center gap-2' 가 아닌 기존 시맨틱 .flex-center 를 활용한 diff --git a/tests/TestCase.php b/tests/TestCase.php index 9e1b9fed..917c5b07 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -150,9 +150,34 @@ abstract class TestCase extends BaseTestCase // 누수를 차단한다 (프로덕션은 단일 부팅이라 무관 — 테스트 격리 전용). EnforceIdentityPolicyListener::resetDynamicSubscriptions(); + $this->purgeIdleDatabaseConnections(); + parent::tearDown(); } + /** + * 트랜잭션이 걸려 있지 않은 DB 연결을 모두 닫습니다. + * + * 트랜잭션 트레이트 없이 DB 를 쓴 테스트의 연결을 닫는다. RefreshDatabase 계열은 + * 롤백 후 스스로 disconnect 하지만, 순수 TestCase 는 아무도 닫지 않아 프로세스 내 + * 테스트 수만큼 연결이 누적된다 — DB 를 쓰는 테스트가 151개(max_connections)를 + * 넘는 클래스/스위트는 그 지점부터 전부 1040 Too many connections 로 실패한다. + * 트랜잭션 진행 중인 연결은 건드리지 않는다 (트레이트의 롤백 콜백이 처리). + */ + protected function purgeIdleDatabaseConnections(): void + { + if ($this->app === null) { + return; + } + + $db = $this->app['db']; + foreach ($db->getConnections() as $name => $connection) { + if ($connection->transactionLevel() === 0) { + $db->purge($name); + } + } + } + /** * 이 테스트가 허용하는 확장 목록을 계산합니다. * diff --git a/tests/Unit/Cache/Migration/ArtisanCacheClearTest.php b/tests/Unit/Cache/Migration/ArtisanCacheClearTest.php index 116b9f00..b951ad44 100644 --- a/tests/Unit/Cache/Migration/ArtisanCacheClearTest.php +++ b/tests/Unit/Cache/Migration/ArtisanCacheClearTest.php @@ -16,7 +16,11 @@ use Tests\TestCase; * 새 CacheInterface 를 통해 캐시를 삭제하는지 검증. * * 실제 명령어 실행은 모듈 manager / plugin manager 의존성이 크므로, - * 명령어가 사용하는 키 형식과 forget 동작을 키 차원에서 검증합니다. + * CacheInterface 의 다건 put/forget 왕복 동작을 키 차원에서 검증합니다. + * + * 주의: 아래 키 문자열은 이관 당시의 픽스처다. 현행 커맨드는 유령 키 forget 을 + * 제거하고 상태 키 무효화 + 레이아웃 캐시 + 버전 bump 로 동작한다 (#588) — + * 커맨드의 실동작 검증은 {Module,Plugin,Template}ArtisanCommandsTest 가 담당한다. */ class ArtisanCacheClearTest extends TestCase { @@ -38,9 +42,7 @@ class ArtisanCacheClearTest extends TestCase } /** - * G-1-1: module:cache-clear 명령어 — 모듈 관련 캐시 삭제 검증. - * - * ClearModuleCacheCommand::clearModuleCache() 가 사용하는 키 패턴을 검증. + * G-1-1: 모듈 키 픽스처 다건 put→forget 왕복 (드라이버 동작 검증 — 커맨드 미호출). */ #[Test] public function g_1_1_module_cache_clear_removes_per_module_keys(): void @@ -58,7 +60,7 @@ class ArtisanCacheClearTest extends TestCase $this->cache->put($k, 'data'); } - // 명령어 핸들러 시뮬레이션 — CacheInterface.forget 호출 + // CacheInterface.forget 다건 호출 (드라이버 동작 검증) foreach ($keys as $k) { $this->cache->forget($k); } @@ -69,7 +71,7 @@ class ArtisanCacheClearTest extends TestCase } /** - * G-1-2: plugin:cache-clear 명령어 — 플러그인 캐시 삭제. + * G-1-2: 플러그인 키 픽스처 다건 put→forget 왕복 (드라이버 동작 검증 — 커맨드 미호출). */ #[Test] public function g_1_2_plugin_cache_clear_removes_per_plugin_keys(): void @@ -96,7 +98,7 @@ class ArtisanCacheClearTest extends TestCase } /** - * G-1-3: template:cache-clear 명령어 — 템플릿 + 레이아웃 + routes + 다국어 캐시 삭제. + * G-1-3: 템플릿/레이아웃 키 픽스처 다건 put→forget 왕복 (드라이버 동작 검증 — 커맨드 미호출). */ #[Test] public function g_1_3_template_cache_clear_removes_template_layout_routes(): void @@ -127,7 +129,7 @@ class ArtisanCacheClearTest extends TestCase } /** - * G-1-4: 인수 없이 호출 시 모든 모듈 캐시 + 전역 키 삭제. + * G-1-4: 전역 + 개별 키 혼합 다건 put→forget 왕복 (드라이버 동작 검증 — 커맨드 미호출). */ #[Test] public function g_1_4_clear_without_args_clears_all(): void @@ -143,7 +145,7 @@ class ArtisanCacheClearTest extends TestCase $this->cache->put("module.info.{$id}", 'data'); } - // 명령어 시뮬레이션 — 전역 + 개별 모두 삭제 + // 전역 + 개별 모두 forget (드라이버 동작 검증) $globalKeys = ['modules.all', 'modules.active', 'modules.installed']; foreach ($globalKeys as $k) { $this->cache->forget($k); diff --git a/tests/Unit/Console/Commands/Traits/HasUnifiedConfirmTest.php b/tests/Unit/Console/Commands/Traits/HasUnifiedConfirmTest.php index 2bd1695e..a9143807 100644 --- a/tests/Unit/Console/Commands/Traits/HasUnifiedConfirmTest.php +++ b/tests/Unit/Console/Commands/Traits/HasUnifiedConfirmTest.php @@ -134,19 +134,64 @@ class HasUnifiedConfirmTest extends TestCase $this->assertStringContainsString('진행할까요? (yes/no) [yes]:', $display); } - private function makeTester(bool $default): CommandTester + /** + * 응답을 받을 수 없는 실행에서는 질문하지 않고 기본값을 돌려준다. + * + * Symfony 는 `posix_isatty()` 로 비대화 실행을 자동 감지하는데 Windows PHP 에는 + * posix 확장이 없어 그 분기가 실행되지 않는다. 그래서 `--no-interaction` 없이 + * 에이전트·CI·스케줄러에서 부르면 `isInteractive()` 가 true 로 남아 `ask()` 가 + * 응답을 기다리며 무한 대기한다. 출력도 남지 않아 "명령이 느리다" 로만 보인다. + */ + public function test_returns_default_without_asking_when_prompt_cannot_be_answered(): void { - $command = new class($default) extends Command + $tester = $this->makeTester(true, canPrompt: false); + + $tester->execute([]); + + $display = $tester->getDisplay(); + $this->assertSame('true', trim($display)); + $this->assertStringNotContainsString('진행할까요?', $display); + } + + /** + * 기본값이 false 인 파괴적 확인은 응답을 받을 수 없을 때 중단으로 떨어져야 한다. + */ + public function test_returns_false_default_without_asking_when_prompt_cannot_be_answered(): void + { + $tester = $this->makeTester(false, canPrompt: false); + + $tester->execute([]); + + $display = $tester->getDisplay(); + $this->assertSame('false', trim($display)); + $this->assertStringNotContainsString('진행할까요?', $display); + } + + /** + * @param bool $default 프롬프트 기본값 + * @param bool|null $canPrompt 응답 가능 여부 강제 (null 이면 실제 판정 사용) + */ + private function makeTester(bool $default, ?bool $canPrompt = null): CommandTester + { + $command = new class($default, $canPrompt) extends Command { - use HasUnifiedConfirm; + // 트레이트 메서드는 parent:: 로 부를 수 없어 별칭으로 원 구현을 남긴다 + use HasUnifiedConfirm { + canPromptForAnswer as private traitCanPromptForAnswer; + } protected $signature = 'test:unified-confirm'; - public function __construct(private bool $defaultValue) + public function __construct(private bool $defaultValue, private ?bool $canPrompt = null) { parent::__construct(); } + protected function canPromptForAnswer(): bool + { + return $this->canPrompt ?? $this->traitCanPromptForAnswer(); + } + public function handle(): int { $result = $this->unifiedConfirm('진행할까요?', $this->defaultValue); diff --git a/tests/Unit/Extension/ExtensionPendingHelperTest.php b/tests/Unit/Extension/ExtensionPendingHelperTest.php index a0b9dca0..7f6d1868 100644 --- a/tests/Unit/Extension/ExtensionPendingHelperTest.php +++ b/tests/Unit/Extension/ExtensionPendingHelperTest.php @@ -403,6 +403,169 @@ class ExtensionPendingHelperTest extends TestCase $this->assertEmpty($tempDirs, '임시 디렉토리가 정리되어야 합니다'); } + // ======================================================================== + // Windows 파일 잠금 대응 — rename 실패 시 파일 단위 폴백 (회귀 테스트) + // + // Windows 에서 디렉토리 rename 은 하위 트리에 열린 핸들(파일 워처의 디렉토리 + // 핸들, 열린 파일 등)이 하나라도 있으면 실패한다. 프로세스 식별/종료는 신뢰할 + // 수 없으므로, rename 이 차단되면 파일 단위 연산(잠금의 영향을 받지 않음)으로 + // 폴백하여 업데이트가 반드시 완료되어야 한다. + // ======================================================================== + + /** + * 기존 활성 디렉토리의 rename(활성 → _old)이 차단될 때 + * 파일 단위 제자리 동기화로 폴백하여 교체가 완료되는지 확인합니다. + */ + public function test_copy_to_active_syncs_in_place_when_target_rename_is_blocked(): void + { + $targetPath = $this->modulesPath.'/test-pending-mod'; + File::ensureDirectoryExists($targetPath); + File::put($targetPath.'/module.json', '{"identifier":"test-pending-mod","version":"0.9.0"}'); + File::put($targetPath.'/old-file.txt', 'stale content'); + + $sourcePath = $this->modulesPath.'/_pending/test-pending-mod'; + + // File Facade Spy: 활성 디렉토리 자체의 rename 만 차단 (워처의 디렉토리 핸들 잠금 재현) + $originalFilesystem = File::getFacadeRoot(); + $mock = Mockery::mock($originalFilesystem)->makePartial(); + $mock->shouldReceive('moveDirectory') + ->andReturnUsing(function (string $from, string $to, bool $overwrite = false) use ($originalFilesystem, $targetPath) { + if (rtrim($from, '/\\') === rtrim($targetPath, '/\\')) { + return false; // 활성 → _old rename 차단 + } + + return $originalFilesystem->moveDirectory($from, $to, $overwrite); + }); + File::swap($mock); + + try { + ExtensionPendingHelper::copyToActive($sourcePath, $targetPath); + } finally { + File::swap($originalFilesystem); + } + + // 새 버전 파일이 제자리에 반영되어야 함 + $this->assertDirectoryExists($targetPath); + $this->assertFileExists($targetPath.'/src/Module.php'); + $manifest = json_decode(File::get($targetPath.'/module.json'), true); + $this->assertEquals('1.0.0', $manifest['version'], '새 버전 manifest 로 교체되어야 함'); + + // 새 버전에 없는 잔존 파일은 제거되어야 함 + $this->assertFileDoesNotExist($targetPath.'/old-file.txt'); + + // 임시 디렉토리가 정리되어야 함 + $this->assertEmpty(glob($this->modulesPath.'/_pending/test-pending-mod_updating_*')); + } + + /** + * 스테이징 디렉토리의 rename(_updating_ → 활성)이 차단될 때 + * 파일 단위 복사로 폴백하여 교체가 완료되는지 확인합니다. + * + * 실사용 로그의 실패 지점: 활성 → _old 는 성공했으나, 방금 복사된 스테이징 + * 트리를 워처가 이미 열어 _updating_ → 활성 rename 이 차단된 케이스. + */ + public function test_copy_to_active_copies_per_file_when_staging_rename_is_blocked(): void + { + $targetPath = $this->modulesPath.'/test-pending-mod'; + File::ensureDirectoryExists($targetPath); + File::put($targetPath.'/module.json', '{"identifier":"test-pending-mod","version":"0.9.0"}'); + File::put($targetPath.'/old-file.txt', 'stale content'); + + $sourcePath = $this->modulesPath.'/_pending/test-pending-mod'; + + // File Facade Spy: _updating_ → 활성 rename 만 차단 + $originalFilesystem = File::getFacadeRoot(); + $mock = Mockery::mock($originalFilesystem)->makePartial(); + $mock->shouldReceive('moveDirectory') + ->andReturnUsing(function (string $from, string $to, bool $overwrite = false) use ($originalFilesystem) { + if (str_contains($from, '_updating_')) { + return false; // 스테이징 → 활성 rename 차단 + } + + return $originalFilesystem->moveDirectory($from, $to, $overwrite); + }); + File::swap($mock); + + try { + ExtensionPendingHelper::copyToActive($sourcePath, $targetPath); + } finally { + File::swap($originalFilesystem); + } + + // 새 버전 파일이 반영되어야 함 + $this->assertDirectoryExists($targetPath); + $this->assertFileExists($targetPath.'/src/Module.php'); + $manifest = json_decode(File::get($targetPath.'/module.json'), true); + $this->assertEquals('1.0.0', $manifest['version'], '새 버전 manifest 로 교체되어야 함'); + + // 원자적 교체(활성 → _old)를 거쳤으므로 잔존 파일도 없어야 함 + $this->assertFileDoesNotExist($targetPath.'/old-file.txt'); + + // 임시/백업 디렉토리가 정리되어야 함 + $this->assertEmpty(glob($this->modulesPath.'/_pending/test-pending-mod_updating_*')); + $this->assertEmpty(glob($this->modulesPath.'/_pending/test-pending-mod_old_*')); + } + + /** + * 실제 Windows 파일 잠금(열린 파일 핸들) 상황에서 교체가 완료되는지 확인합니다. + * + * Windows 는 하위 트리에 열린 파일이 있으면 그 경로의 모든 상위 디렉토리 + * rename 을 차단한다. 열린 파일이라도 일반적인 공유 모드(read/write 공유)로 + * 열려 있으면 내용 덮어쓰기는 허용되므로, 파일 단위 폴백은 성공해야 한다. + */ + public function test_copy_to_active_survives_open_file_handle_on_windows(): void + { + if (PHP_OS_FAMILY !== 'Windows') { + $this->markTestSkipped('Windows 파일 잠금 동작 검증 전용 테스트'); + } + + $targetPath = $this->modulesPath.'/test-pending-mod'; + File::ensureDirectoryExists($targetPath); + File::put($targetPath.'/module.json', '{"identifier":"test-pending-mod","version":"0.9.0"}'); + File::put($targetPath.'/old-file.txt', 'stale content'); + + $sourcePath = $this->modulesPath.'/_pending/test-pending-mod'; + + // 활성 디렉토리 내 파일에 핸들을 열어 둔 채 교체 시도 (디렉토리 rename 차단 재현) + $handle = fopen($targetPath.'/module.json', 'rb'); + $this->assertIsResource($handle); + + try { + ExtensionPendingHelper::copyToActive($sourcePath, $targetPath); + } finally { + fclose($handle); + } + + $this->assertFileExists($targetPath.'/src/Module.php'); + $manifest = json_decode(File::get($targetPath.'/module.json'), true); + $this->assertEquals('1.0.0', $manifest['version'], '핸들이 열린 파일도 내용이 교체되어야 함'); + $this->assertFileDoesNotExist($targetPath.'/old-file.txt'); + } + + /** + * 이전 실패 실행이 남긴 _updating_/_old_ 잔존 디렉토리가 + * 다음 교체 시작 시 자동 정리되는지 확인합니다. + */ + public function test_copy_to_active_cleans_leftover_swap_directories(): void + { + $pendingPath = $this->modulesPath.'/_pending'; + File::ensureDirectoryExists($pendingPath.'/test-pending-mod_updating_stale123'); + File::put($pendingPath.'/test-pending-mod_updating_stale123/junk.txt', 'junk'); + File::ensureDirectoryExists($pendingPath.'/test-pending-mod_old_stale456'); + File::put($pendingPath.'/test-pending-mod_old_stale456/junk.txt', 'junk'); + + $targetPath = $this->modulesPath.'/test-pending-mod'; + File::ensureDirectoryExists($targetPath); + File::put($targetPath.'/module.json', '{"identifier":"test-pending-mod","version":"0.9.0"}'); + + $sourcePath = $this->modulesPath.'/_pending/test-pending-mod'; + ExtensionPendingHelper::copyToActive($sourcePath, $targetPath); + + $this->assertDirectoryDoesNotExist($pendingPath.'/test-pending-mod_updating_stale123'); + $this->assertDirectoryDoesNotExist($pendingPath.'/test-pending-mod_old_stale456'); + $this->assertFileExists($targetPath.'/src/Module.php'); + } + /** * EXCLUDED_DIRECTORIES 상수에 node_modules가 포함되어 있는지 확인합니다. */ diff --git a/tests/Unit/Extension/PluginMenuSyncTest.php b/tests/Unit/Extension/PluginMenuSyncTest.php new file mode 100644 index 00000000..5dd013ab --- /dev/null +++ b/tests/Unit/Extension/PluginMenuSyncTest.php @@ -0,0 +1,193 @@ +<?php + +namespace Tests\Unit\Extension; + +use App\Enums\ExtensionOwnerType; +use App\Extension\AbstractPlugin; +use App\Extension\PluginManager; +use App\Models\Menu; +use Illuminate\Foundation\Testing\RefreshDatabase; +use ReflectionMethod; +use Tests\TestCase; + +/** + * 메뉴 동기화 검증용 플러그인 스텁. + * + * 실제 플러그인 디렉토리를 만들지 않고 식별자만 고정하기 위해 `getPluginPath()` 를 덮어쓴다 + * (AbstractPlugin 은 경로 basename 으로 식별자를 정한다). + */ +class MenuSyncProbePlugin extends AbstractPlugin +{ + /** + * @param array<int, array<string, mixed>> $menus 선언할 관리자 메뉴 + */ + public function __construct(private array $menus = []) {} + + /** + * @return string 식별자를 결정하는 가상 경로 + */ + protected function getPluginPath(): string + { + return base_path('plugins/sirsoft-menu_sync_probe'); + } + + /** + * @return array<int, array<string, mixed>> 관리자 메뉴 선언 + */ + public function getAdminMenus(): array + { + return $this->menus; + } +} + +/** + * 플러그인 관리자 메뉴가 선언형 산출물 동기화로 만들어지는지 검증 (회귀) + * + * 메뉴 동기화가 활성화(activate) 시점에만 일어나면, 이미 활성 상태인 사이트가 플러그인을 + * 업데이트해 새 화면을 받아도 메뉴가 생기지 않는다. 그 화면은 주소를 직접 입력해야만 닿을 수 + * 있어 운영자에게는 없는 기능이 된다(브라우저 실측 — 업데이트 반영 후 메뉴 0건). + * + * 설치·업데이트가 공통으로 지나는 syncDeclarativeArtifacts 에서 메뉴를 동기화하는지 본다. + * 파일시스템(활성 디렉토리)은 건드리지 않고 DB 만 확인한다. + */ +class PluginMenuSyncTest extends TestCase +{ + use RefreshDatabase; + + /** + * 파일 무접촉으로 메뉴 동기화 메서드만 호출합니다. + * + * @param object $plugin 대상 플러그인 스텁 + */ + private function syncMenus(object $plugin): void + { + $method = new ReflectionMethod(PluginManager::class, 'createPluginMenus'); + $method->setAccessible(true); + $method->invoke(app(PluginManager::class), $plugin); + } + + /** + * getAdminMenus 를 선언한 플러그인 스텁을 만듭니다. + * + * @param array<int, array<string, mixed>> $menus 선언할 메뉴 목록 + */ + private function makePlugin(array $menus): object + { + return new MenuSyncProbePlugin($menus); + } + + /** + * PluginManager 메서드 본문이 호출하는 `$this->...()` 이름을 원문에서 수집합니다. + * + * 단계 목록을 테스트에 손으로 적어두면 새 산출물 종류가 늘었을 때 그 사실이 검사에 + * 반영되지 않으므로, 두 경로 모두 소스에서 도출해 비교한다. + * + * @param string $method 대상 메서드명 + * @return array<int, string> 호출된 메서드명 목록 (중복 제거) + */ + private function selfCallsIn(string $method): array + { + $reflection = new ReflectionMethod(PluginManager::class, $method); + + $source = implode('', array_slice( + file($reflection->getFileName()), + $reflection->getStartLine() - 1, + $reflection->getEndLine() - $reflection->getStartLine() + 1, + )); + + preg_match_all('/\$this->([A-Za-z_][A-Za-z0-9_]*)\s*\(/', $source, $matches); + + return array_values(array_unique($matches[1])); + } + + /** + * 설치 경로가 선언형 산출물 동기화 단계를 빠짐없이 지나는지 (회귀) + * + * 메뉴 동기화를 syncDeclarativeArtifacts 에만 넣으면 그 경로를 지나는 것은 업데이트뿐이라, + * 신규 설치는 플러그인이 activate() 를 직접 구현한 경우에만 메뉴가 생긴다. AbstractPlugin + * 의 기본 activate() 는 `return true` 뿐이므로, getAdminMenus() 만 선언한 플러그인은 + * 설치·활성화를 마쳐도 메뉴가 없고 예외도 경고도 남지 않는다. 모듈은 installModule() 이 + * createModuleMenus() 를 직접 호출해 이 구멍이 없다. + * + * 단계 목록은 syncDeclarativeArtifacts 원문에서 도출하므로, 앞으로 선언형 산출물이 + * 추가될 때 설치 경로에 반영하지 않으면 그 사실이 여기서 드러난다. + * + * @effects plugin_install_path_syncs_declared_menus + */ + public function test_install_path_covers_every_declarative_sync_step(): void + { + // stale 정리는 기존 레코드를 대상으로 하므로 신규 설치 경로에는 대상이 없다. + $required = array_diff($this->selfCallsIn('syncDeclarativeArtifacts'), ['cleanupStalePluginEntries']); + + $missing = array_values(array_diff($required, $this->selfCallsIn('installPlugin'))); + + $this->assertSame( + [], + $missing, + 'installPlugin() 이 선언형 산출물 동기화 단계를 건너뛰었다: '.implode(', ', $missing), + ); + } + + /** + * @scenario declaration=declared, run=first_run + * + * @effects plugin_admin_menu_synced_on_declarative_sync + */ + public function test_declared_admin_menu_is_created(): void + { + $this->syncMenus($this->makePlugin([[ + 'name' => ['ko' => '검수 메뉴', 'en' => 'Probe Menu'], + 'slug' => 'menu-sync-probe', + 'url' => '/admin/plugins/sirsoft-menu_sync_probe/probe', + 'icon' => 'fas fa-vial', + 'order' => 50, + ]])); + + $menu = Menu::where('slug', 'menu-sync-probe')->first(); + + $this->assertNotNull($menu, '플러그인이 선언한 관리자 메뉴가 생성되어야 한다.'); + $this->assertSame(ExtensionOwnerType::Plugin->value, $menu->extension_type instanceof ExtensionOwnerType + ? $menu->extension_type->value + : $menu->extension_type); + $this->assertSame('sirsoft-menu_sync_probe', $menu->extension_identifier); + $this->assertSame('/admin/plugins/sirsoft-menu_sync_probe/probe', $menu->url); + } + + /** + * 재호출은 행을 중복 생성하지 않는다 (업데이트마다 실행되므로 멱등이어야 한다). + * + * @scenario declaration=declared, run=repeated_run + * + * @effects plugin_admin_menu_sync_is_idempotent + */ + public function test_repeated_sync_does_not_duplicate_menu(): void + { + $plugin = $this->makePlugin([[ + 'name' => ['ko' => '검수 메뉴', 'en' => 'Probe Menu'], + 'slug' => 'menu-sync-probe', + 'url' => '/admin/plugins/sirsoft-menu_sync_probe/probe', + 'order' => 50, + ]]); + + $this->syncMenus($plugin); + $this->syncMenus($plugin); + + $this->assertSame(1, Menu::where('slug', 'menu-sync-probe')->count()); + } + + /** + * 메뉴를 선언하지 않은 플러그인은 아무것도 만들지 않는다. + * + * @scenario declaration=not_declared, run=first_run + * + * @effects plugin_without_menu_declaration_creates_nothing + */ + public function test_plugin_without_menus_creates_nothing(): void + { + $before = Menu::count(); + + $this->syncMenus($this->makePlugin([])); + + $this->assertSame($before, Menu::count()); + } +} diff --git a/tests/Unit/Extension/Storage/CoreStorageDriverTest.php b/tests/Unit/Extension/Storage/CoreStorageDriverTest.php new file mode 100644 index 00000000..8275d733 --- /dev/null +++ b/tests/Unit/Extension/Storage/CoreStorageDriverTest.php @@ -0,0 +1,265 @@ +<?php + +namespace Tests\Unit\Extension\Storage; + +use App\Extension\HookManager; +use App\Extension\Storage\CoreStorageDriver; +use App\Extension\Storage\ModuleStorageDriver; +use App\Extension\Storage\PluginStorageDriver; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Storage; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * CoreStorageDriver + url() 공개 URL 계약(ResolvesPublicUrl) 단위 테스트 + * + * url() 의 직접 URL 판정(public / url 설정 디스크 / url 미설정)과 + * `core.storage.filter_url` 훅 계약(공급/수정/차단/컨텍스트)을 드라이버 3종에서 검증합니다. + */ +class CoreStorageDriverTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + // 가짜 CDN 디스크 — S3 실체 없이 url 설정 디스크의 직접 URL 생성 검증 + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + } + + protected function tearDown(): void + { + HookManager::resetAll(); + Storage::disk('public')->deleteDirectory('core-url-test'); + + parent::tearDown(); + } + + #[Test] + public function it_can_put_get_and_delete_file(): void + { + $driver = new CoreStorageDriver('local'); + + $this->assertTrue($driver->put('temp', 'core-test/file.txt', 'content')); + $this->assertSame('content', $driver->get('temp', 'core-test/file.txt')); + $this->assertTrue($driver->exists('temp', 'core-test/file.txt')); + $this->assertTrue($driver->delete('temp', 'core-test/file.txt')); + $this->assertFalse($driver->exists('temp', 'core-test/file.txt')); + } + + /** + * @scenario consumer=product, disk_setting=none, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=legacy_local_row + * + * @effects url_null_for_disk_without_url_config, download_url_falls_back_to_api_path_when_direct_unavailable + */ + #[Test] + public function it_returns_null_url_for_disk_without_url_config(): void + { + // local 디스크는 filesystems.disks.local.url 미설정 — 직접 URL 불가 + $driver = new CoreStorageDriver('local'); + + $this->assertNull($driver->url('images', 'core-url-test/a.jpg')); + } + + /** + * @scenario consumer=product, disk_setting=public, e2e=drivers_tab_card, hook=unregistered, override=none_override, row_state=legacy_local_row + * + * @effects url_returned_for_public_and_url_configured_disk + */ + #[Test] + public function it_returns_url_for_public_disk(): void + { + // 기존 동작 보존 — public 디스크는 항상 직접 URL + $driver = new CoreStorageDriver('public'); + + $url = $driver->url('images', 'core-url-test/a.jpg'); + + $this->assertNotNull($url); + $this->assertStringContainsString('images/core-url-test/a.jpg', $url); + } + + /** + * @scenario consumer=category, disk_setting=fake_cdn, e2e=drivers_tab_card, hook=unregistered, override=follow_core, row_state=new_remote_row + * + * @effects url_returned_for_public_and_url_configured_disk + */ + #[Test] + public function it_returns_url_for_disk_with_url_config(): void + { + $driver = new CoreStorageDriver('fake_cdn'); + + $this->assertSame( + 'https://cdn.test/assets/images/core-url-test/a.jpg', + $driver->url('images', 'core-url-test/a.jpg') + ); + } + + /** + * @scenario consumer=category, disk_setting=s3_without_url, e2e=save_roundtrip, hook=unregistered, override=none_override, row_state=legacy_local_row + * + * @effects blank_url_config_defended + */ + #[Test] + public function it_returns_null_url_when_disk_url_config_is_blank(): void + { + // AWS_URL 빈값/공백 방어 — url 이 빈 문자열이거나 공백뿐이면 직접 URL 불가. + // 두 형태를 모두 고정한다: 구현이 trim() 단일 분기라 지금은 같은 경로를 타지만, + // 판정이 `!== ''` 로 좁혀지면 공백 케이스만 깨지므로 한쪽만으로는 검출되지 않는다. + foreach (['', ' '] as $blank) { + Config::set('filesystems.disks.fake_cdn.url', $blank); + $driver = new CoreStorageDriver('fake_cdn'); + + $this->assertNull( + $driver->url('images', 'core-url-test/a.jpg'), + sprintf('url 설정이 %s 이면 직접 URL 을 만들지 않아야 한다', var_export($blank, true)) + ); + } + } + + #[Test] + public function module_and_plugin_drivers_return_url_for_url_configured_disk(): void + { + $moduleDriver = new ModuleStorageDriver('test-module', 'fake_cdn'); + $pluginDriver = new PluginStorageDriver('test-plugin', 'fake_cdn'); + + $this->assertSame( + 'https://cdn.test/assets/test-module/images/products/1/a.jpg', + $moduleDriver->url('images', 'products/1/a.jpg') + ); + $this->assertSame( + 'https://cdn.test/assets/test-plugin/images/2026/01/01/b.png', + $pluginDriver->url('images', '2026/01/01/b.png') + ); + } + + /** + * @scenario consumer=editor, disk_setting=s3_without_url, e2e=drivers_tab_card, hook=supply, override=none_override, row_state=new_remote_row + * + * @effects filter_url_hook_always_fires_with_six_context_keys, hook_supply_modify_block_respected + */ + #[Test] + public function filter_url_hook_fires_even_when_url_is_null(): void + { + // 확정 #5 — null 일 때도 항상 발화 (훅이 URL 을 공급할 수 있다) + HookManager::addFilter('core.storage.filter_url', function ($url, array $context) { + $this->assertNull($url); + + return 'https://hook.test/supplied/'.$context['path']; + }); + + $driver = new CoreStorageDriver('local'); + + $this->assertSame( + 'https://hook.test/supplied/a.jpg', + $driver->url('images', 'a.jpg') + ); + } + + /** + * @scenario consumer=product, disk_setting=fake_cdn, e2e=ecommerce_settings_field, hook=supply, override=none_override, row_state=legacy_local_row + * + * @effects hook_supply_modify_block_respected + */ + #[Test] + public function filter_url_hook_can_modify_generated_url(): void + { + HookManager::addFilter('core.storage.filter_url', function ($url) { + return str_replace('https://cdn.test', 'https://cdn2.test', (string) $url); + }); + + $driver = new CoreStorageDriver('fake_cdn'); + + $this->assertSame( + 'https://cdn2.test/assets/images/a.jpg', + $driver->url('images', 'a.jpg') + ); + } + + /** + * @scenario consumer=review, disk_setting=fake_cdn, e2e=save_roundtrip, hook=block, override=none_override, row_state=legacy_local_row + * + * @effects hook_supply_modify_block_respected + */ + #[Test] + public function filter_url_hook_can_block_url_by_returning_empty(): void + { + // 반환이 빈문자열/비문자열이면 호출측 스트리밍 폴백 (null) + HookManager::addFilter('core.storage.filter_url', fn ($url) => ''); + + $driver = new CoreStorageDriver('fake_cdn'); + + $this->assertNull($driver->url('images', 'a.jpg')); + } + + /** + * @effects hook_supply_modify_block_respected + */ + #[Test] + public function filter_url_hook_non_string_or_blank_returns_fall_back_to_null(): void + { + // 훅이 계약 밖의 값을 돌려줘도 예외 없이 스트리밍 폴백 (방어 계약) + foreach ([null, false, ['url' => 'https://x.test/a.jpg'], ' '] as $badReturn) { + HookManager::resetAll(); + HookManager::addFilter('core.storage.filter_url', fn ($url) => $badReturn); + + $driver = new CoreStorageDriver('fake_cdn'); + + $this->assertNull( + $driver->url('images', 'a.jpg'), + '훅 반환값 '.var_export($badReturn, true).' 은 null 폴백이어야 한다' + ); + } + } + + /** + * @scenario consumer=review, disk_setting=none, e2e=drivers_tab_card, hook=supply, override=none_override, row_state=legacy_local_row + * + * @effects filter_url_hook_always_fires_with_six_context_keys + */ + #[Test] + public function filter_url_hook_receives_six_context_keys(): void + { + $captured = null; + HookManager::addFilter('core.storage.filter_url', function ($url, array $context) use (&$captured) { + $captured = $context; + + return $url; + }); + + (new CoreStorageDriver('fake_cdn'))->url('images', 'sub/a.jpg'); + + $this->assertSame([ + 'scope' => 'core', + 'identifier' => null, + 'disk' => 'fake_cdn', + 'category' => 'images', + 'path' => 'sub/a.jpg', + 'full_path' => 'images/sub/a.jpg', + ], $captured); + } + + #[Test] + public function filter_url_hook_context_identifies_module_and_plugin_scope(): void + { + $contexts = []; + HookManager::addFilter('core.storage.filter_url', function ($url, array $context) use (&$contexts) { + $contexts[] = $context; + + return $url; + }); + + (new ModuleStorageDriver('test-module', 'local'))->url('images', 'a.jpg'); + (new PluginStorageDriver('test-plugin', 'local'))->url('images', 'b.jpg'); + + $this->assertSame('module', $contexts[0]['scope']); + $this->assertSame('test-module', $contexts[0]['identifier']); + $this->assertSame('test-module/images/a.jpg', $contexts[0]['full_path']); + $this->assertSame('plugin', $contexts[1]['scope']); + $this->assertSame('test-plugin', $contexts[1]['identifier']); + $this->assertSame('test-plugin/images/b.jpg', $contexts[1]['full_path']); + } +} diff --git a/tests/Unit/Extension/Storage/ModuleStorageDriverTest.php b/tests/Unit/Extension/Storage/ModuleStorageDriverTest.php index a5261c7f..dd666135 100644 --- a/tests/Unit/Extension/Storage/ModuleStorageDriverTest.php +++ b/tests/Unit/Extension/Storage/ModuleStorageDriverTest.php @@ -2,11 +2,14 @@ namespace Tests\Unit\Extension\Storage; +use App\Extension\HookManager; use App\Extension\Storage\ModuleStorageDriver; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Storage; -use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Tests\TestCase; /** * ModuleStorageDriver 단위 테스트 @@ -32,6 +35,7 @@ class ModuleStorageDriverTest extends TestCase { // 테스트 데이터 정리 Storage::disk('modules')->deleteDirectory('test-module'); + HookManager::resetAll(); parent::tearDown(); } @@ -189,6 +193,74 @@ class ModuleStorageDriverTest extends TestCase Storage::disk('public')->deleteDirectory('test-module'); } + /** + * url 이 설정된 디스크(S3+CDN 등)면 모듈 드라이버도 직접 URL 을 돌려준다. + * + * @effects url_returned_for_public_and_url_configured_disk + */ + #[Test] + public function it_returns_direct_url_for_url_configured_disk(): void + { + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + + $cdnDriver = new ModuleStorageDriver('test-module', 'fake_cdn'); + + $this->assertSame( + 'https://cdn.test/assets/test-module/images/test.jpg', + $cdnDriver->url('images', 'test.jpg') + ); + } + + /** + * url 값이 빈 문자열/공백이면 직접 URL 로 보지 않는다 (AWS_URL 미설정 방어). + * + * @effects blank_url_config_defended + */ + #[Test] + public function it_returns_null_when_disk_url_is_blank(): void + { + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => ' ', + ]); + + $this->assertNull((new ModuleStorageDriver('test-module', 'fake_cdn'))->url('images', 'test.jpg')); + } + + /** + * 모듈 드라이버의 `core.storage.filter_url` 컨텍스트는 scope=module + + * 확장 식별자를 싣는다 (코어/플러그인과 구분되는 축). + * + * @effects filter_url_hook_always_fires_with_six_context_keys + */ + #[Test] + public function it_passes_module_scope_and_identifier_to_url_filter_hook(): void + { + $captured = null; + + HookManager::addFilter('core.storage.filter_url', function ($url, $context) use (&$captured) { + $captured = $context; + + return $url; + }); + + (new ModuleStorageDriver('test-module', 'modules'))->url('images', 'sub/a.jpg'); + + $this->assertSame([ + 'scope' => 'module', + 'identifier' => 'test-module', + 'disk' => 'modules', + 'category' => 'images', + 'path' => 'sub/a.jpg', + 'full_path' => 'test-module/images/sub/a.jpg', + ], $captured); + } + #[Test] public function it_uses_correct_path_pattern(): void { @@ -218,7 +290,7 @@ class ModuleStorageDriverTest extends TestCase ]); // Then: StreamedResponse 반환 - $this->assertInstanceOf(\Symfony\Component\HttpFoundation\StreamedResponse::class, $response); + $this->assertInstanceOf(StreamedResponse::class, $response); } #[Test] diff --git a/tests/Unit/Extension/Storage/PluginStorageDriverTest.php b/tests/Unit/Extension/Storage/PluginStorageDriverTest.php index c400ce86..6f012f35 100644 --- a/tests/Unit/Extension/Storage/PluginStorageDriverTest.php +++ b/tests/Unit/Extension/Storage/PluginStorageDriverTest.php @@ -2,11 +2,14 @@ namespace Tests\Unit\Extension\Storage; +use App\Extension\HookManager; use App\Extension\Storage\PluginStorageDriver; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Storage; -use Tests\TestCase; use PHPUnit\Framework\Attributes\Test; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Tests\TestCase; /** * PluginStorageDriver 단위 테스트 @@ -32,6 +35,7 @@ class PluginStorageDriverTest extends TestCase { // 테스트 데이터 정리 Storage::disk('plugins')->deleteDirectory('test-plugin'); + HookManager::resetAll(); parent::tearDown(); } @@ -189,6 +193,74 @@ class PluginStorageDriverTest extends TestCase Storage::disk('public')->deleteDirectory('test-plugin'); } + /** + * url 이 설정된 디스크(S3+CDN 등)면 플러그인 드라이버도 직접 URL 을 돌려준다. + * + * @effects url_returned_for_public_and_url_configured_disk + */ + #[Test] + public function it_returns_direct_url_for_url_configured_disk(): void + { + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + + $cdnDriver = new PluginStorageDriver('test-plugin', 'fake_cdn'); + + $this->assertSame( + 'https://cdn.test/assets/test-plugin/images/test.jpg', + $cdnDriver->url('images', 'test.jpg') + ); + } + + /** + * url 값이 빈 문자열/공백이면 직접 URL 로 보지 않는다 (AWS_URL 미설정 방어). + * + * @effects blank_url_config_defended + */ + #[Test] + public function it_returns_null_when_disk_url_is_blank(): void + { + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => ' ', + ]); + + $this->assertNull((new PluginStorageDriver('test-plugin', 'fake_cdn'))->url('images', 'test.jpg')); + } + + /** + * 플러그인 드라이버의 `core.storage.filter_url` 컨텍스트는 scope=plugin + + * 확장 식별자를 싣는다 (코어/모듈과 구분되는 축). + * + * @effects filter_url_hook_always_fires_with_six_context_keys + */ + #[Test] + public function it_passes_plugin_scope_and_identifier_to_url_filter_hook(): void + { + $captured = null; + + HookManager::addFilter('core.storage.filter_url', function ($url, $context) use (&$captured) { + $captured = $context; + + return $url; + }); + + (new PluginStorageDriver('test-plugin', 'plugins'))->url('images', 'sub/a.jpg'); + + $this->assertSame([ + 'scope' => 'plugin', + 'identifier' => 'test-plugin', + 'disk' => 'plugins', + 'category' => 'images', + 'path' => 'sub/a.jpg', + 'full_path' => 'test-plugin/images/sub/a.jpg', + ], $captured); + } + #[Test] public function it_uses_correct_path_pattern(): void { @@ -218,7 +290,7 @@ class PluginStorageDriverTest extends TestCase ]); // Then: StreamedResponse 반환 - $this->assertInstanceOf(\Symfony\Component\HttpFoundation\StreamedResponse::class, $response); + $this->assertInstanceOf(StreamedResponse::class, $response); } #[Test] diff --git a/tests/Unit/Extension/Storage/StorageCategoryDiskTest.php b/tests/Unit/Extension/Storage/StorageCategoryDiskTest.php new file mode 100644 index 00000000..71961ebc --- /dev/null +++ b/tests/Unit/Extension/Storage/StorageCategoryDiskTest.php @@ -0,0 +1,212 @@ +<?php + +namespace Tests\Unit\Extension\Storage; + +use App\Extension\AbstractModule; +use App\Extension\AbstractPlugin; +use Illuminate\Support\Facades\Config; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * 카테고리별 스토리지 디스크 결정(getStorageDiskFor/getStorageFor/resolvePublicAssetDisk) 단위 테스트 + * + * 기본 동작 100% 보존(기본값 = getStorageDisk), 디스크 단위 memoize, + * 공개 자산 디스크 해석 우선순위(확장 개별 > 코어 전역 > 미설정)와 + * 고아 디스크 폴백을 검증합니다. + */ +class StorageCategoryDiskTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + Config::set('filesystems.disks.fake_cdn', [ + 'driver' => 'local', + 'root' => storage_path('framework/testing/disks/fake_cdn'), + 'url' => 'https://cdn.test/assets', + ]); + Config::set('core.storage.public_asset_disk', ''); + } + + /** + * resolvePublicAssetDisk 를 노출한 테스트 모듈 더블을 생성합니다. + * + * @return AbstractModule&object{exposedResolvePublicAssetDisk: callable} 테스트 모듈 + */ + private function makeModule() + { + return new class extends AbstractModule + { + public function getName(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + + public function getVersion(): string + { + return '1.0.0'; + } + + public function getDescription(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + + public function exposedResolvePublicAssetDisk(?string $override = null): ?string + { + return $this->resolvePublicAssetDisk($override); + } + }; + } + + /** + * 테스트 플러그인 더블을 생성합니다. + * + * @return AbstractPlugin 테스트 플러그인 + */ + private function makePlugin(): AbstractPlugin + { + return new class extends AbstractPlugin + { + public function getName(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + + public function getVersion(): string + { + return '1.0.0'; + } + + public function getDescription(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + }; + } + + #[Test] + public function get_storage_disk_for_defaults_to_base_disk(): void + { + $module = $this->makeModule(); + $plugin = $this->makePlugin(); + + // 오버라이드 없는 기본 구현 — 모든 카테고리가 기본 디스크 (현행 100% 보존) + $this->assertSame($module->getStorageDisk(), $module->getStorageDiskFor('images')); + $this->assertSame($module->getStorageDisk(), $module->getStorageDiskFor('settings')); + $this->assertSame($plugin->getStorageDisk(), $plugin->getStorageDiskFor('images')); + $this->assertSame($plugin->getStorageDisk(), $plugin->getStorageDiskFor('settings')); + } + + #[Test] + public function get_storage_for_reuses_base_instance_when_disk_matches(): void + { + $module = $this->makeModule(); + + $this->assertSame($module->getStorage(), $module->getStorageFor('images')); + } + + /** + * @scenario consumer=review, disk_setting=public, e2e=drivers_tab_card, hook=block, override=module_override, row_state=new_remote_row + */ + #[Test] + public function get_storage_for_memoizes_per_disk(): void + { + $module = new class extends AbstractModule + { + public function getName(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + + public function getVersion(): string + { + return '1.0.0'; + } + + public function getDescription(): array + { + return ['ko' => '테스트', 'en' => 'Test']; + } + + public function getStorageDiskFor(string $category): string + { + return $category === 'images' ? 'fake_cdn' : $this->getStorageDisk(); + } + }; + + $imagesStorage = $module->getStorageFor('images'); + + $this->assertSame('fake_cdn', $imagesStorage->getDisk()); + $this->assertNotSame($module->getStorage(), $imagesStorage); + // 디스크 단위 memoize — 반복 호출이 같은 인스턴스 반환 + $this->assertSame($imagesStorage, $module->getStorageFor('images')); + // 기본 디스크 카테고리는 기본 인스턴스 재사용 + $this->assertSame($module->getStorage(), $module->getStorageFor('settings')); + } + + /** + * @scenario consumer=product, disk_setting=none, e2e=drivers_tab_card, hook=unregistered, override=module_override, row_state=legacy_local_row + * + * @effects none_override_forces_streaming_over_global + */ + #[Test] + public function resolve_public_asset_disk_prefers_override_over_global(): void + { + Config::set('core.storage.public_asset_disk', 'public'); + $module = $this->makeModule(); + + $this->assertSame('fake_cdn', $module->exposedResolvePublicAssetDisk('fake_cdn')); + } + + /** + * @scenario consumer=editor, disk_setting=none, e2e=save_roundtrip, hook=supply, override=follow_core, row_state=new_remote_row + * + * @effects none_override_forces_streaming_over_global + */ + #[Test] + public function resolve_public_asset_disk_falls_back_to_global_when_override_empty(): void + { + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + $module = $this->makeModule(); + + $this->assertSame('fake_cdn', $module->exposedResolvePublicAssetDisk('')); + $this->assertSame('fake_cdn', $module->exposedResolvePublicAssetDisk(null)); + } + + #[Test] + public function resolve_public_asset_disk_returns_null_when_unset(): void + { + $module = $this->makeModule(); + + // 전역 미설정 + 오버라이드 없음 → 스트리밍 유지 + $this->assertNull($module->exposedResolvePublicAssetDisk()); + } + + #[Test] + public function resolve_public_asset_disk_treats_none_as_streaming(): void + { + Config::set('core.storage.public_asset_disk', 'fake_cdn'); + $module = $this->makeModule(); + + // 'none' 오버라이드 = 전역이 CDN 이어도 이 확장은 스트리밍 강제 + $this->assertNull($module->exposedResolvePublicAssetDisk('none')); + } + + /** + * @scenario consumer=product, disk_setting=s3_without_url, e2e=drivers_tab_card, hook=block, override=module_override, row_state=new_remote_row + * + * @effects orphan_disk_falls_back_to_streaming + */ + #[Test] + public function resolve_public_asset_disk_falls_back_on_orphan_disk(): void + { + // 플러그인 비활성화로 config 에서 사라진 디스크 → 스트리밍 자동 폴백 + Config::set('core.storage.public_asset_disk', 'vanished_plugin_disk'); + $module = $this->makeModule(); + + $this->assertNull($module->exposedResolvePublicAssetDisk()); + $this->assertNull($module->exposedResolvePublicAssetDisk('vanished_plugin_disk')); + } +} diff --git a/tests/Unit/Extension/TemplateManagerEnsureLoadedTest.php b/tests/Unit/Extension/TemplateManagerEnsureLoadedTest.php new file mode 100644 index 00000000..7dceb974 --- /dev/null +++ b/tests/Unit/Extension/TemplateManagerEnsureLoadedTest.php @@ -0,0 +1,125 @@ +<?php + +namespace Tests\Unit\Extension; + +use App\Contracts\Extension\TemplateManagerInterface; +use App\Extension\TemplateManager; +use App\Services\TemplateService; +use Tests\TestCase; + +/** + * 템플릿 로드 멱등성 + 서비스 공유 인스턴스화 (C-2) + * + * `TemplateService` 생성자가 가드 없이 `TemplateManager::loadTemplates()` 를 호출했다. + * 그 메서드는 공유 싱글톤의 템플릿 맵을 통째로 리셋한 뒤 디렉토리를 다시 스캔하므로, + * 서비스가 주입될 때마다 풀스캔이 반복되고 싱글톤 상태가 매번 변형됐다. + * `TemplateManager` 안에서 `TemplateService` 를 해석하는 재진입 경로까지 있다. + * + * 생성자 로드를 단순 제거할 수는 없다 — 웹/serve/test 는 CoreServiceProvider::boot 가 + * 로드를 보장하지만 그 외 콘솔 경로는 로딩을 건너뛰므로, 커맨드가 생성자 로드에 의존한다. + * 그래서 "아직 로드하지 않았을 때만 로드하는" 멱등 진입점을 둔다. + */ +class TemplateManagerEnsureLoadedTest extends TestCase +{ + /** + * 매니저의 템플릿 맵을 반환합니다. (protected 프로퍼티 조회) + * + * @param TemplateManager $manager 대상 매니저 + * @return array 템플릿 맵 + */ + private function templatesOf(TemplateManager $manager): array + { + $prop = (new \ReflectionClass($manager))->getProperty('templates'); + $prop->setAccessible(true); + + return $prop->getValue($manager); + } + + /** + * 계약에 멱등 로드 진입점이 선언되어 있다. (실패-먼저) + */ + public function test_interface_declares_ensure_loaded(): void + { + $this->assertTrue( + method_exists(TemplateManagerInterface::class, 'ensureLoaded'), + '멱등 로드 진입점이 계약에 없습니다.' + ); + } + + /** + * ensureLoaded 는 두 번 호출해도 재스캔하지 않는다. (실패-먼저) + */ + public function test_ensure_loaded_is_idempotent(): void + { + /** @var TemplateManager $manager */ + $manager = $this->app->make(TemplateManager::class); + $manager->loadTemplates(); + + $before = $this->templatesOf($manager); + // 로드 완료 표식을 남긴 뒤에는 맵을 리셋하지 않아야 한다 + $manager->ensureLoaded(); + + $this->assertSame( + array_keys($before), + array_keys($this->templatesOf($manager)), + 'ensureLoaded 가 이미 로드된 맵을 다시 만들었습니다.' + ); + } + + /** + * 명시적 loadTemplates() 는 강제 재스캔 계약을 유지한다. (비회귀 pin) + */ + public function test_explicit_load_templates_still_rescans(): void + { + /** @var TemplateManager $manager */ + $manager = $this->app->make(TemplateManager::class); + $manager->ensureLoaded(); + + $prop = (new \ReflectionClass($manager))->getProperty('templates'); + $prop->setAccessible(true); + $prop->setValue($manager, ['stale-marker' => 'x']); + + $manager->loadTemplates(); + + $this->assertArrayNotHasKey( + 'stale-marker', + $this->templatesOf($manager), + '명시적 재스캔이 기존 맵을 갱신하지 않았습니다.' + ); + } + + /** + * TemplateService 생성자는 공유 싱글톤의 템플릿 맵을 리셋하지 않는다. (실패-먼저) + */ + public function test_service_construction_does_not_reset_shared_map(): void + { + /** @var TemplateManager $manager */ + $manager = $this->app->make(TemplateManager::class); + $manager->loadTemplates(); + + $prop = (new \ReflectionClass($manager))->getProperty('templates'); + $prop->setAccessible(true); + $prop->setValue($manager, ['sentinel-template' => ['identifier' => 'sentinel-template']]); + + $this->app->forgetInstance(TemplateService::class); + $this->app->make(TemplateService::class); + + $this->assertArrayHasKey( + 'sentinel-template', + $this->templatesOf($manager), + 'TemplateService 주입이 공유 템플릿 맵을 통째로 재스캔했습니다.' + ); + } + + /** + * TemplateService 는 공유 인스턴스로 해석된다. (실패-먼저) + */ + public function test_template_service_is_shared_instance(): void + { + $this->assertSame( + $this->app->make(TemplateService::class), + $this->app->make(TemplateService::class), + 'TemplateService 가 해석할 때마다 새로 만들어집니다.' + ); + } +} diff --git a/tests/Unit/Extension/TemplateUpdateConfigCacheInvalidationTest.php b/tests/Unit/Extension/TemplateUpdateConfigCacheInvalidationTest.php new file mode 100644 index 00000000..0c16b976 --- /dev/null +++ b/tests/Unit/Extension/TemplateUpdateConfigCacheInvalidationTest.php @@ -0,0 +1,86 @@ +<?php + +namespace Tests\Unit\Extension; + +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * 템플릿 업데이트 성공 cleanup 의 config 캐시 무효화 구조 테스트 (#588, 공개 #119) + * + * `template.config.{identifier}` 는 버전 접미사 없는 고정 키라 캐시 버전 bump 로 + * 무효화되지 않는다. updateTemplate() 성공 cleanup 이 clearTemplateCache() 를 + * 직접 호출하지 않으면, 비활성 업데이트·레이아웃 무변경 업데이트 경로에서 공개 + * config.json 이 TTL(1시간) 동안 이전 manifest 를 계속 서빙한다. + * + * 실제 updateTemplate() 통합 실행은 업데이트 소스(_pending/신버전) 픽스처가 필요해 + * 비용이 크므로, clearTemplateCache() 의 키 삭제는 TemplateCacheManagementTest 가 + * 동작으로 검증하고 본 테스트는 호출 존재를 소스 구조로 고정한다 + * (선례: ExtensionUpdateStatusCacheSymmetryTest). + */ +class TemplateUpdateConfigCacheInvalidationTest extends TestCase +{ + /** + * @scenario cache_state=warm_current,lifecycle_trigger=template_update,template_type=user + * + * @effects template_update_invalidates_public_config_cache + */ + #[Test] + public function update_template_cleanup_calls_clear_template_cache(): void + { + $body = $this->methodSource( + base_path('app/Extension/TemplateManager.php'), + 'updateTemplate' + ); + + $this->assertStringContainsString( + '$this->clearTemplateCache(', + $body, + 'updateTemplate() 성공 cleanup 이 clearTemplateCache() 를 호출해야 합니다 — ' + .'누락 시 공개 config.json 이 캐시 TTL 동안 이전 manifest 로 응답됩니다.' + ); + } + + /** + * 대상 메서드 본문을 주석 제거 후 반환합니다. + * + * 주석을 남겨 두면 설명 문장에 들어 있는 호출 표기가 검사를 통과시킨다. + * + * @param string $path 대상 파일 절대 경로 + * @param string $method 대상 메서드명 + * @return string 주석이 제거된 메서드 본문 + */ + private function methodSource(string $path, string $method): string + { + $this->assertFileExists($path); + + $stripped = ''; + foreach (token_get_all(file_get_contents($path)) as $token) { + if (is_array($token) && in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true)) { + continue; + } + $stripped .= is_array($token) ? $token[1] : $token; + } + + $start = strpos($stripped, "public function {$method}("); + $this->assertNotFalse($start, "{$path} 에서 {$method}() 메서드를 찾지 못했습니다."); + + // 중괄호 균형으로 메서드 본문 끝을 찾는다 + $open = strpos($stripped, '{', $start); + $depth = 0; + $length = strlen($stripped); + + for ($i = $open; $i < $length; $i++) { + if ($stripped[$i] === '{') { + $depth++; + } elseif ($stripped[$i] === '}') { + $depth--; + if ($depth === 0) { + return substr($stripped, $open, $i - $open + 1); + } + } + } + + $this->fail("{$path} 의 {$method}() 본문 끝을 찾지 못했습니다."); + } +} diff --git a/tests/Unit/Models/AttachmentUrlTest.php b/tests/Unit/Models/AttachmentUrlTest.php new file mode 100644 index 00000000..12963cbf --- /dev/null +++ b/tests/Unit/Models/AttachmentUrlTest.php @@ -0,0 +1,33 @@ +<?php + +namespace Tests\Unit\Models; + +use App\Models\Attachment; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +/** + * 코어 Attachment URL 조립 단일 지점(urlForHash/download_url) 회귀 테스트 + * + * URL 중복 조립 정리(#100) 후에도 응답 문자열이 기존과 동일함을 고정합니다. + */ +class AttachmentUrlTest extends TestCase +{ + /** + * @effects board_response_strings_unchanged + */ + #[Test] + public function url_for_hash_builds_core_attachment_url(): void + { + $this->assertSame('/api/attachment/abc123def456', Attachment::urlForHash('abc123def456')); + } + + #[Test] + public function download_url_accessor_delegates_to_url_for_hash(): void + { + $attachment = new Attachment; + $attachment->hash = 'abc123def456'; + + $this->assertSame('/api/attachment/abc123def456', $attachment->download_url); + } +} diff --git a/tests/Unit/Providers/SettingsServiceProviderPublicAssetDiskTest.php b/tests/Unit/Providers/SettingsServiceProviderPublicAssetDiskTest.php new file mode 100644 index 00000000..f38aadb5 --- /dev/null +++ b/tests/Unit/Providers/SettingsServiceProviderPublicAssetDiskTest.php @@ -0,0 +1,115 @@ +<?php + +namespace Tests\Unit\Providers; + +use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; +use Illuminate\Support\Facades\Config; +use ReflectionMethod; +use Tests\TestCase; + +// @scenario disk_setting=none, consumer=product, row_state=new_remote_row, override=follow_core, hook=unregistered, e2e=save_roundtrip + +/** + * SettingsServiceProvider 공개 자산 디스크 주입 사슬 테스트 (공개#100) + * + * drivers.public_asset_disk 저장값 → core.storage.public_asset_disk 주입과 + * 'none'(스트리밍 유지)/빈값 → '' 정규화를 검증합니다. 주입부 + * (injectPublicAssetDiskConfig)는 테스트 격리 가드와 분리되어 있어 직접 단언이 + * 가능하며, 가드(applyPublicAssetDiskConfig)가 testing 환경에서 주입을 실제로 + * 차단하는지도 함께 고정합니다. + * + * @effects global_setting_injects_core_config_with_none_normalized + */ +class SettingsServiceProviderPublicAssetDiskTest extends TestCase +{ + /** + * 지정 메서드를 리플렉션으로 호출합니다. + * + * @param string $method 호출할 private 메서드명 + * @param array $driverSettings drivers 카테고리 설정 데이터 + */ + private function callWithDriverSettings(string $method, array $driverSettings): void + { + $configRepository = $this->createMock(JsonConfigRepository::class); + $configRepository->method('getCategory') + ->with('drivers') + ->willReturn($driverSettings); + + $provider = new SettingsServiceProvider($this->app); + $reflection = new ReflectionMethod($provider, $method); + $reflection->invoke($provider, $configRepository); + } + + /** + * 저장값이 core.storage.public_asset_disk 로 주입되는지 테스트합니다. + */ + public function test_stored_disk_injected_into_core_config(): void + { + Config::set('core.storage.public_asset_disk', ''); + + $this->callWithDriverSettings('injectPublicAssetDiskConfig', [ + 'public_asset_disk' => 'fake_cdn', + ]); + + $this->assertSame('fake_cdn', config('core.storage.public_asset_disk')); + } + + /** + * 'none'(스트리밍 유지 선택)이 미설정('')으로 정규화되는지 테스트합니다. + */ + public function test_none_sentinel_normalized_to_empty(): void + { + Config::set('core.storage.public_asset_disk', 'stale_value'); + + $this->callWithDriverSettings('injectPublicAssetDiskConfig', [ + 'public_asset_disk' => 'none', + ]); + + $this->assertSame('', config('core.storage.public_asset_disk')); + } + + /** + * 저장 키 부재 시 미설정('')으로 주입되는지 테스트합니다. + */ + public function test_missing_key_injected_as_empty(): void + { + Config::set('core.storage.public_asset_disk', 'stale_value'); + + $this->callWithDriverSettings('injectPublicAssetDiskConfig', []); + + $this->assertSame('', config('core.storage.public_asset_disk')); + } + + /** + * 빈 문자열 저장값이 그대로 미설정('')인지 테스트합니다. + */ + public function test_blank_value_injected_as_empty(): void + { + Config::set('core.storage.public_asset_disk', 'stale_value'); + + $this->callWithDriverSettings('injectPublicAssetDiskConfig', [ + 'public_asset_disk' => '', + ]); + + $this->assertSame('', config('core.storage.public_asset_disk')); + } + + /** + * testing 환경에서 가드가 주입을 차단하는지 테스트합니다 (테스트 격리). + * + * 이 테스트 자체가 testing 환경에서 실행되므로, 가드 메서드 + * (applyPublicAssetDiskConfig)를 호출해도 dev 공유 drivers.json 값이 + * 흘러들지 않아야 합니다. + */ + public function test_guard_skips_injection_in_testing_env(): void + { + Config::set('core.storage.public_asset_disk', ''); + + $this->callWithDriverSettings('applyPublicAssetDiskConfig', [ + 'public_asset_disk' => 'fake_cdn', + ]); + + $this->assertSame('', config('core.storage.public_asset_disk')); + } +} diff --git a/tests/Unit/Providers/SettingsServiceProviderS3ConfigTest.php b/tests/Unit/Providers/SettingsServiceProviderS3ConfigTest.php new file mode 100644 index 00000000..f8fd372d --- /dev/null +++ b/tests/Unit/Providers/SettingsServiceProviderS3ConfigTest.php @@ -0,0 +1,277 @@ +<?php + +namespace Tests\Unit\Providers; + +use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; +use Illuminate\Support\Facades\Config; +use Mockery; +use ReflectionMethod; +use Tests\TestCase; + +/** + * SettingsServiceProvider S3 설정 주입 테스트 (공개 #99 / 내부 #563) + * + * applyS3Config()의 endpoint/path-style 주입, s3_url→url 매핑, + * applyDriverConfig()의 첨부 디스크 전환(A8), 죽은 주입 제거(a2·a3) 회귀를 검증합니다. + */ +class SettingsServiceProviderS3ConfigTest extends TestCase +{ + /** + * applyS3Config를 리플렉션으로 호출합니다. + * + * @param array $driverSettings drivers 카테고리 설정 데이터 + */ + private function callApplyS3Config(array $driverSettings): void + { + $provider = new SettingsServiceProvider($this->app); + $method = new ReflectionMethod($provider, 'applyS3Config'); + $method->invoke($provider, $driverSettings); + } + + /** + * applyDriverConfig를 리플렉션으로 호출합니다 (drivers 카테고리 mock 주입). + * + * @param array $driverSettings drivers 카테고리 설정 데이터 + */ + private function callApplyDriverConfig(array $driverSettings): void + { + $repository = Mockery::mock(JsonConfigRepository::class); + $repository->shouldReceive('getCategory')->with('drivers')->andReturn($driverSettings); + + $provider = new SettingsServiceProvider($this->app); + $method = new ReflectionMethod($provider, 'applyDriverConfig'); + $method->invoke($provider, $repository); + } + + /** + * s3_endpoint 가 filesystems.disks.s3.endpoint 로 주입되는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=custom_url,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_s3_endpoint_injected_into_disk_config(): void + { + Config::set('filesystems.disks.s3.endpoint', null); + + $this->callApplyS3Config([ + 's3_endpoint' => 'https://account.r2.cloudflarestorage.com', + ]); + + $this->assertSame( + 'https://account.r2.cloudflarestorage.com', + config('filesystems.disks.s3.endpoint'), + 's3_endpoint 는 SDK 요청 대상(endpoint)에 주입되어야 함 — 미주입 시 S3 호환 스토리지 연결 불가 (#99)' + ); + } + + /** + * s3_use_path_style 이 use_path_style_endpoint 로 주입되는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=path_style,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_s3_use_path_style_injected_into_disk_config(): void + { + Config::set('filesystems.disks.s3.use_path_style_endpoint', false); + + $this->callApplyS3Config(['s3_use_path_style' => true]); + + $this->assertTrue((bool) config('filesystems.disks.s3.use_path_style_endpoint')); + } + + /** + * 빈 endpoint/path-style 은 주입되지 않는지 테스트합니다 (AWS 기본 경로 보존). + */ + public function test_empty_s3_endpoint_and_path_style_not_injected(): void + { + Config::set('filesystems.disks.s3.endpoint', null); + Config::set('filesystems.disks.s3.use_path_style_endpoint', false); + + $this->callApplyS3Config([ + 's3_endpoint' => '', + 's3_use_path_style' => false, + 's3_bucket' => 'my-bucket', + ]); + + $this->assertNull(config('filesystems.disks.s3.endpoint')); + $this->assertFalse((bool) config('filesystems.disks.s3.use_path_style_endpoint')); + } + + /** + * s3_url 은 public URL(url)에만 매핑되고 endpoint 에는 매핑되지 않는지 테스트합니다. + */ + public function test_s3_url_maps_to_public_url_not_endpoint(): void + { + Config::set('filesystems.disks.s3.url', null); + Config::set('filesystems.disks.s3.endpoint', null); + + $this->callApplyS3Config(['s3_url' => 'https://cdn.example.com']); + + $this->assertSame('https://cdn.example.com', config('filesystems.disks.s3.url')); + $this->assertNull( + config('filesystems.disks.s3.endpoint'), + 's3_url 은 공개 URL base — SDK 요청 대상(endpoint)과 별개 축이어야 함' + ); + } + + /** + * storage_driver=s3 + ATTACHMENT_DISK 미명시 시 첨부 디스크가 s3 로 전환되는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=follow_s3 + * + * @effects attachment_upload_follows_driver + */ + public function test_attachment_disk_follows_s3_when_env_not_explicit(): void + { + Config::set('attachment.disk', 'attachments'); + Config::set('attachment.disk_explicit', null); + + $this->callApplyDriverConfig(['storage_driver' => 's3']); + + $this->assertSame('s3', config('attachment.disk')); + } + + /** + * ATTACHMENT_DISK 명시 시 env 가 항상 우선하는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=env_override + * + * @effects attachment_upload_follows_driver + */ + public function test_attachment_disk_env_override_wins_over_s3(): void + { + Config::set('attachment.disk', 'custom-disk'); + Config::set('attachment.disk_explicit', 'custom-disk'); + + $this->callApplyDriverConfig(['storage_driver' => 's3']); + + $this->assertSame('custom-disk', config('attachment.disk')); + } + + /** + * ATTACHMENT_DISK= 빈 값(키 존재·값 없음)은 "미명시" 로 간주되어야 합니다. + * + * `cp .env.example .env` 설치 절차에서 `ATTACHMENT_DISK=` 가 그대로 복사되면 + * env() 는 기본값 대신 빈 문자열을 돌려준다 — `=== null` 판별이면 s3 전환이 + * 영구 미발동하고, `attachment.disk` 도 '' 가 되어 첨부 업로드가 전면 실패한다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=follow_s3 + * + * @effects attachment_upload_follows_driver + */ + public function test_attachment_disk_follows_s3_when_env_is_blank_string(): void + { + Config::set('attachment.disk', 'attachments'); + Config::set('attachment.disk_explicit', ''); + + $this->callApplyDriverConfig(['storage_driver' => 's3']); + + $this->assertSame( + 's3', + config('attachment.disk'), + 'ATTACHMENT_DISK= 빈 값은 미명시와 동일해야 함 — 빈 문자열을 명시로 취급하면 신규 설치에서 전환이 절대 발동하지 않는다' + ); + } + + /** + * storage_driver=local 이면 첨부 디스크가 기본값을 유지하는지 테스트합니다. + */ + public function test_attachment_disk_unchanged_when_storage_driver_local(): void + { + Config::set('attachment.disk', 'attachments'); + Config::set('attachment.disk_explicit', null); + + $this->callApplyDriverConfig(['storage_driver' => 'local']); + + $this->assertSame('attachments', config('attachment.disk')); + } + + /** + * A2 실효성 — REDIS_CLIENT=phpredis 활성 배포(.env.example 표준 설치) + 확장 부재 + * 조합에서 predis 폴백이 발동해야 합니다. + * + * 종전 게이트(env('REDIS_CLIENT') === null)는 표준 설치에서 env 값이 'phpredis' 라 + * 영구 미발동 — A2 가 막으려던 `Class "Redis" not found` 전면 다운이 그대로 남는다. + * 확장 설치 머신에서는 부재 상태를 재현할 수 없으므로(A5 선례: mock 불가 시 구성 + * 검증으로 대체), 판정 술어를 확장 로드 여부 인자로 분리해 검증한다. + */ + public function test_predis_fallback_fires_for_phpredis_client_without_extension(): void + { + $provider = new SettingsServiceProvider($this->app); + $method = new ReflectionMethod($provider, 'shouldFallBackToPredis'); + + // 표준 설치 상태: config 클라이언트 phpredis + 확장 부재 → 폴백해야 함 + Config::set('database.redis.client', 'phpredis'); + $this->assertTrue( + $method->invoke($provider, false), + 'REDIS_CLIENT=phpredis 활성 배포 + 확장 부재 = 표준 설치 시나리오 — 폴백 미발동이면 redis 저장 시 전면 다운 (#99 A2)' + ); + + // 확장이 있으면 phpredis 경로 유지 + $this->assertFalse($method->invoke($provider, true)); + + // predis 명시 설정이면 이미 동작 경로 — 개입하지 않음 + Config::set('database.redis.client', 'predis'); + $this->assertFalse($method->invoke($provider, false)); + } + + /** + * applyRedisConfig 통합 — 폴백 판정이 env() 가 아닌 config 값을 기준으로 해야 합니다. + * + * env() 직접 호출은 config:cache 환경에서 null 로 고정된다 (A8 disk_explicit 와 동형). + * 이 머신은 확장 설치 여부에 따라 결과가 갈리므로 분기 단언한다. + */ + public function test_apply_redis_config_fallback_uses_config_not_env(): void + { + Config::set('database.redis.client', 'phpredis'); + + $provider = new SettingsServiceProvider($this->app); + $method = new ReflectionMethod($provider, 'applyRedisConfig'); + $method->invoke($provider, []); + + if (extension_loaded('redis')) { + $this->assertSame('phpredis', config('database.redis.client'), '확장이 있으면 phpredis 유지'); + } else { + $this->assertSame('predis', config('database.redis.client'), '확장 부재 시 predis 폴백 (표준 설치 REDIS_CLIENT=phpredis 포함)'); + } + } + + /** + * upload.disk 죽은 주입 제거 회귀 — upload 카테고리의 disk 값이 + * filesystems.default 를 더 이상 덮어쓰지 않아야 합니다 (a2). + */ + public function test_upload_disk_no_longer_overrides_filesystems_default(): void + { + Config::set('filesystems.default', 'local'); + + $repository = Mockery::mock(JsonConfigRepository::class); + $repository->shouldReceive('getCategory')->with('upload')->andReturn([ + 'disk' => 'rogue-disk', + 'max_file_size' => 10, + ]); + + $provider = new SettingsServiceProvider($this->app); + $method = new ReflectionMethod($provider, 'applyUploadConfig'); + $method->invoke($provider, $repository); + + $this->assertSame( + 'local', + config('filesystems.default'), + 'upload.disk 는 저장 rule 없는 유령 키 — filesystems.default 를 storage_driver 와 경쟁 기록하면 안 됨' + ); + } + + /** + * cache.driver/prefix 죽은 주입 제거 회귀 — applyCacheConfig 메서드 자체가 제거되어야 합니다 (a3). + */ + public function test_apply_cache_config_dead_injection_removed(): void + { + $this->assertFalse( + method_exists(SettingsServiceProvider::class, 'applyCacheConfig'), + 'cache.driver/prefix 는 저장 경로 없는 죽은 주입 — drivers.cache_driver(cache.default SSoT)와 경쟁하므로 제거 유지' + ); + } +} diff --git a/tests/Unit/Providers/SettingsServiceProviderTest.php b/tests/Unit/Providers/SettingsServiceProviderTest.php index 1f2a4984..3c032023 100644 --- a/tests/Unit/Providers/SettingsServiceProviderTest.php +++ b/tests/Unit/Providers/SettingsServiceProviderTest.php @@ -2,9 +2,9 @@ namespace Tests\Unit\Providers; -use App\Providers\SettingsServiceProvider; +use App\Repositories\JsonConfigRepository; +use App\Support\ExtensionSettingsMirror; use Carbon\Carbon; -use ReflectionClass; use Tests\TestCase; /** @@ -56,13 +56,48 @@ class SettingsServiceProviderTest extends TestCase /** * CORE_CATEGORIES에 seo 카테고리가 포함되어 봇 감지 설정이 로딩되는지 테스트합니다. + * + * 목록의 소유자는 ExtensionSettingsMirror 다 — Provider 에 사본을 두고 그 사본을 + * 단언하면, 미러의 목록에서 카테고리가 빠져도 이 테스트가 초록으로 남는다. */ public function test_seo_category_included_in_core_categories(): void { - $reflection = new ReflectionClass(SettingsServiceProvider::class); - $categories = $reflection->getConstant('CORE_CATEGORIES'); + $this->assertContains( + 'seo', + ExtensionSettingsMirror::CORE_CATEGORIES, + 'CORE_CATEGORIES에 seo 카테고리가 포함되어야 합니다.' + ); + } - $this->assertContains('seo', $categories, 'CORE_CATEGORIES에 seo 카테고리가 포함되어야 합니다.'); + /** + * CORE_CATEGORIES 가 실제로 미러에 채워지는 목록인지 테스트합니다. + * + * 이 단언이 없으면 위/아래의 상수 포함 검사는 "상수에 문자열이 있다" 만 증명한다. + * 상수와 채움 동작이 실제로 이어져 있어야 그 검사에 회귀 차단 효력이 생긴다. + */ + public function test_core_categories_constant_is_what_the_mirror_fills(): void + { + $probe = new class extends JsonConfigRepository + { + /** + * 어느 카테고리를 물어도 비어 있지 않은 값을 돌려준다 (채움 대상 판정용). + * + * @param string $category 카테고리명 + * @return array<string, mixed> 탐침 값 + */ + public function getCategory(string $category): array + { + return ['__probe' => $category]; + } + }; + + app(ExtensionSettingsMirror::class)->refreshCore($probe); + + $this->assertSame( + ExtensionSettingsMirror::CORE_CATEGORIES, + array_keys(config('g7_settings.core')), + 'g7_settings.core 에 채워지는 카테고리가 CORE_CATEGORIES 와 일치해야 합니다.' + ); } /** @@ -85,10 +120,11 @@ class SettingsServiceProviderTest extends TestCase */ public function test_identity_category_included_in_core_categories(): void { - $reflection = new ReflectionClass(SettingsServiceProvider::class); - $categories = $reflection->getConstant('CORE_CATEGORIES'); - - $this->assertContains('identity', $categories, 'CORE_CATEGORIES 에 identity 카테고리가 포함되어야 합니다.'); + $this->assertContains( + 'identity', + ExtensionSettingsMirror::CORE_CATEGORIES, + 'CORE_CATEGORIES 에 identity 카테고리가 포함되어야 합니다.' + ); } /** diff --git a/tests/Unit/Repositories/DeletesInBatchesTest.php b/tests/Unit/Repositories/DeletesInBatchesTest.php new file mode 100644 index 00000000..c1c97403 --- /dev/null +++ b/tests/Unit/Repositories/DeletesInBatchesTest.php @@ -0,0 +1,118 @@ +<?php + +namespace Tests\Unit\Repositories; + +use App\Models\ActivityLog; +use App\Repositories\Concerns\DeletesInBatches; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Tests\TestCase; + +/** + * 배치 삭제 트레이트 테스트 + * + * 정리 예약은 도입 직후 첫 실행에서 이미 쌓여 있던 과거 데이터를 전부 지운다. + * 한 문장으로 지우면 그 한 건이 테이블을 오래 잠그므로 배치로 끊어 지운다 — + * 끊어 지우면서도 대상이 남김없이 사라지는지를 고정한다. + * + * @scenario row_age=beyond_retention + * + * @effects large_purge_is_split_into_batches + */ +class DeletesInBatchesTest extends TestCase +{ + use RefreshDatabase; + + /** + * 배치 크기를 넘는 대상도 남김없이 삭제된다. + * + * 단일 배치만 처리하고 끝내는 구현이면 나머지가 남아 red 가 된다. + */ + public function test_deletes_every_matching_row_across_multiple_batches(): void + { + foreach (range(1, 7) as $i) { + $log = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.batch', + 'description' => "오래된 기록 {$i}", + ]); + $log->forceFill(['created_at' => now()->subDays(400)])->saveQuietly(); + } + + $deleted = $this->runner()->purge( + ActivityLog::where('created_at', '<', now()->subDays(365)) + ); + + $this->assertSame(7, $deleted); + $this->assertSame(0, ActivityLog::where('action', 'test.batch')->count()); + } + + /** + * 조건 밖의 행은 배치가 몇 회전을 돌아도 건드리지 않는다. + */ + public function test_keeps_rows_outside_the_condition(): void + { + foreach (range(1, 5) as $i) { + $old = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.batch.old', + 'description' => "오래된 기록 {$i}", + ]); + $old->forceFill(['created_at' => now()->subDays(400)])->saveQuietly(); + } + + $recent = ActivityLog::create([ + 'log_type' => 'system', + 'action' => 'test.batch.recent', + 'description' => '최근 기록', + ]); + + $this->runner()->purge( + ActivityLog::where('created_at', '<', now()->subDays(365)) + ); + + $this->assertDatabaseHas('activity_logs', ['id' => $recent->id]); + $this->assertSame(0, ActivityLog::where('action', 'test.batch.old')->count()); + } + + /** + * 대상이 없으면 0 을 돌려주고 끝난다 (무한 루프 방지). + */ + public function test_returns_zero_when_nothing_matches(): void + { + $deleted = $this->runner()->purge( + ActivityLog::where('created_at', '<', now()->subDays(365)) + ); + + $this->assertSame(0, $deleted); + } + + /** + * 배치 크기를 작게 잡은 실행기를 만듭니다. + * + * 기본 배치(1000)로는 여러 회전을 만들려면 그만큼 행을 쌓아야 해서, + * 회전 자체를 검증하려면 크기를 줄이는 편이 정확하다. + * + * @return object purge(Builder): int 를 제공하는 실행기 + */ + private function runner(): object + { + return new class + { + use DeletesInBatches; + + /** + * 조건에 맞는 행을 배치로 삭제합니다. + * + * 회전을 강제하려고 배치 크기를 2 로 낮춰 호출합니다. + * + * @param Builder $query 삭제 대상 쿼리 + * @return int 삭제된 건수 + */ + public function purge(Builder $query): int + { + return $this->deleteInBatches($query, 2); + } + }; + } +} diff --git a/tests/Unit/Search/DatabaseFulltextEngineIndexGateTest.php b/tests/Unit/Search/DatabaseFulltextEngineIndexGateTest.php new file mode 100644 index 00000000..02f608ae --- /dev/null +++ b/tests/Unit/Search/DatabaseFulltextEngineIndexGateTest.php @@ -0,0 +1,244 @@ +<?php + +declare(strict_types=1); + +namespace Tests\Unit\Search; + +use App\Search\Contracts\FulltextSearchable; +use App\Search\Engines\DatabaseFulltextEngine; +use App\Search\KeywordSearch; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Schema; +use Laravel\Scout\Builder; +use Laravel\Scout\Searchable; +use Tests\TestCase; + +/** + * FULLTEXT 인덱스 부재 게이트 검증 (공개 이슈 #103) + * + * 회귀 배경: `applyFulltextPredicate()` / `performSearch()` 의 게이트가 드라이버명만 + * 검사해, MySQL 인데 대상 테이블에 FULLTEXT 인덱스가 없는 설치(부분 실패 설치 등)에서 + * `1191 Can't find FULLTEXT index` 가 발생했다. 그 오류는 리스너의 catch 에 삼켜져 + * 화면에는 "검색 결과 0건" 으로만 나타났다. + * + * 수정 후 계약: 드라이버가 지원해도 대상 테이블·컬럼 조합을 커버하는 FULLTEXT 인덱스가 + * 없으면 LIKE 로 내려가고, 그 사실을 조합당 프로세스 1회 경고로 남긴다. + */ +class DatabaseFulltextEngineIndexGateTest extends TestCase +{ + /** + * 프리픽스 미포함 테이블명 — FULLTEXT 인덱스를 만들지 않는 probe 테이블. + */ + private const TABLE = 'ft_gate_fixtures'; + + protected function setUp(): void + { + parent::setUp(); + + if (! DatabaseFulltextEngine::supportsFulltext()) { + $this->markTestSkipped('FULLTEXT 를 제공하지 않는 DBMS 입니다.'); + } + + DatabaseFulltextEngine::forgetFulltextIndexCatalog(); + + Schema::dropIfExists(self::TABLE); + Schema::create(self::TABLE, function ($table): void { + $table->id(); + $table->string('title'); + $table->text('content'); + }); + } + + protected function tearDown(): void + { + Schema::dropIfExists(self::TABLE); + DatabaseFulltextEngine::forgetFulltextIndexCatalog(); + + parent::tearDown(); + } + + /** + * T1-a: 인덱스가 없는 테이블은 MATCH 대신 LIKE 폴백으로 조립되어야 한다. + * + * 수정 전에는 드라이버만 검사해 MATCH 가 조립됐다 (실행 시 1191). + * + * @effects like_fallback_on_missing_index + */ + public function test_like_fallback_when_table_has_no_fulltext_index(): void + { + $query = FulltextGateFixture::query(); + + KeywordSearch::apply($query, ['title', 'content'], '문의'); + + $sql = strtolower($query->toSql()); + + $this->assertStringNotContainsString('match(', $sql, '인덱스 없는 테이블에 MATCH 가 조립되면 실행 시 1191 이 됩니다.'); + $this->assertStringContainsString('like', $sql, 'LIKE 폴백이 조립되어야 합니다.'); + } + + /** + * T1-b: 복합 인덱스는 단일 컬럼 MATCH 를 커버하지 못한다 (집합 동등 비교, 순서 무관). + */ + public function test_composite_index_does_not_cover_single_column(): void + { + DatabaseFulltextEngine::primeFulltextIndexCatalog([ + 'board_posts' => [['title', 'content']], + ]); + + $this->assertFalse( + DatabaseFulltextEngine::fulltextIndexCoversColumns('board_posts', ['title']), + '복합 (title,content) 인덱스는 MATCH(title) 단독을 커버하지 못합니다.' + ); + $this->assertTrue( + DatabaseFulltextEngine::fulltextIndexCoversColumns('board_posts', ['title', 'content']), + '컬럼 집합이 정확히 일치하면 커버합니다.' + ); + $this->assertTrue( + DatabaseFulltextEngine::fulltextIndexCoversColumns('board_posts', ['content', 'title']), + '집합 비교는 순서 무관이어야 합니다 — 깨지면 정상 경로가 LIKE 로 강등됩니다.' + ); + } + + /** + * T1-c: Scout 경로(performSearch)는 컬럼마다 단일 MATCH 를 만들므로, + * 각 컬럼이 개별 인덱스로 커버되지 않으면 LIKE 로 내려가야 한다. + */ + public function test_scout_search_falls_back_when_columns_lack_individual_indexes(): void + { + // 복합 인덱스만 존재하는 상황을 시드 — per-column MATCH 는 커버되지 않는다. + DatabaseFulltextEngine::primeFulltextIndexCatalog([ + self::TABLE => [['title', 'content']], + ]); + + $engine = new DatabaseFulltextEngine; + $results = $engine->paginate(new Builder(new FulltextGateFixture, '문의'), 10, 1); + + $this->assertNotNull($results['query']); + + $sql = strtolower($results['query']->toSql()); + + $this->assertStringNotContainsString('match(', $sql, 'per-column MATCH 는 복합 인덱스로 커버되지 않으므로 조립되면 안 됩니다.'); + $this->assertStringContainsString('like', $sql); + } + + /** + * T1-f: 두 게이트의 LIKE 폴백이 같은 술어를 만든다 — 검색어의 `%`/`_` 는 + * 와일드카드가 아니라 글자로 취급되어야 한다. + * + * 게이트 ①(applyFulltextPredicate)은 `KeywordSearch::applyLikeMatch()` 로 내려가 + * escape 를 거치는데, 게이트 ②(performSearch)가 LIKE 를 손으로 조립하면 같은 + * "부분일치" 인데도 `100%` 같은 검색어에서 결과가 갈린다. 게이트 ② 신설로 + * MySQL 트래픽이 처음 이 경로에 유입되므로 두 경로의 술어를 여기서 고정한다. + * + * @effects like_fallback_on_missing_index + */ + public function test_scout_like_fallback_escapes_wildcards_like_the_shared_path(): void + { + $keyword = '100%_할인'; + $expected = '%'.KeywordSearch::escapeLikeWildcards($keyword).'%'; + + // 게이트 ① — 공용 경로. 이 경로가 escape 된 패턴을 만든다는 것을 먼저 확정한다. + $shared = FulltextGateFixture::query(); + KeywordSearch::apply($shared, ['title', 'content'], $keyword); + + $this->assertContains( + $expected, + $shared->getBindings(), + '공용 폴백 경로가 escape 된 패턴을 만들지 않으면 이 테스트의 기준값이 무의미합니다.' + ); + + // 게이트 ② — Scout 경로. 복합 인덱스만 시드해 per-column MATCH 를 미커버로 만든다. + DatabaseFulltextEngine::primeFulltextIndexCatalog([ + self::TABLE => [['title', 'content']], + ]); + + $engine = new DatabaseFulltextEngine; + $results = $engine->paginate(new Builder(new FulltextGateFixture, $keyword), 10, 1); + + $this->assertNotNull($results['query']); + + $this->assertContains( + $expected, + $results['query']->getBindings(), + 'Scout 폴백이 % 와 _ 를 escape 하지 않으면 검색어가 와일드카드로 해석되어 공용 경로와 결과가 갈립니다.' + ); + } + + /** + * T1-d: 인덱스 부재 폴백 경고는 테이블+컬럼 조합당 프로세스 1회만 남는다. + * + * @effects missing_index_fallback_logged_once + */ + public function test_missing_index_fallback_is_logged_once_per_table_and_columns(): void + { + Log::spy(); + + $apply = function (array $columns): void { + $query = FulltextGateFixture::query(); + KeywordSearch::apply($query, $columns, '문의'); + }; + + $apply(['title', 'content']); + $apply(['title', 'content']); + + Log::shouldHaveReceived('warning') + ->withArgs(fn (string $message) => str_contains($message, 'FULLTEXT')) + ->once(); + + $apply(['title']); + + Log::shouldHaveReceived('warning') + ->withArgs(fn (string $message) => str_contains($message, 'FULLTEXT')) + ->twice(); + } + + /** + * T1-e: addFulltextIndex 성공 직후 카탈로그가 무효화되어, 새 인덱스가 + * forget 호출 없이도 다음 판정에 반영되어야 한다. + */ + public function test_add_fulltext_index_invalidates_catalog(): void + { + $this->assertFalse( + DatabaseFulltextEngine::fulltextIndexCoversColumns(self::TABLE, ['title']), + 'probe 테이블은 인덱스 없이 생성되므로 커버되지 않아야 합니다 (카탈로그 적재 확인).' + ); + + DatabaseFulltextEngine::addFulltextIndex(self::TABLE, 'ft_'.self::TABLE.'_title', 'title'); + + $this->assertTrue( + DatabaseFulltextEngine::fulltextIndexCoversColumns(self::TABLE, ['title']), + 'DDL 성공 직후 카탈로그가 무효화되어 새 인덱스가 반영되어야 합니다.' + ); + } +} + +/** + * 인덱스 게이트 테스트 전용 픽스처 모델 (FULLTEXT 인덱스 없는 테이블). + */ +class FulltextGateFixture extends Model implements FulltextSearchable +{ + use Searchable; + + protected $table = 'ft_gate_fixtures'; + + public $timestamps = false; + + protected $guarded = []; + + /** + * {@inheritDoc} + */ + public function searchableColumns(): array + { + return ['title', 'content']; + } + + /** + * {@inheritDoc} + */ + public function searchableWeights(): array + { + return ['title' => 3.0, 'content' => 1.0]; + } +} diff --git a/tests/Unit/Search/DatabaseFulltextEngineTest.php b/tests/Unit/Search/DatabaseFulltextEngineTest.php index c1afaf69..ea887623 100644 --- a/tests/Unit/Search/DatabaseFulltextEngineTest.php +++ b/tests/Unit/Search/DatabaseFulltextEngineTest.php @@ -26,6 +26,20 @@ class DatabaseFulltextEngineTest extends TestCase { parent::setUp(); $this->engine = new DatabaseFulltextEngine; + + // 픽스처 테이블(test_table)은 실존하지 않으므로 인덱스 카탈로그를 시드해 + // MATCH 조립 경로를 검증한다 — 시드하지 않으면 인덱스 부재 게이트(#103)가 + // LIKE 로 내려 보내 조립 단언이 무의미해진다. + DatabaseFulltextEngine::primeFulltextIndexCatalog([ + 'test_table' => [['name']], + ]); + } + + protected function tearDown(): void + { + DatabaseFulltextEngine::forgetFulltextIndexCatalog(); + + parent::tearDown(); } /** diff --git a/tests/Unit/Search/SearchHighlighterTest.php b/tests/Unit/Search/SearchHighlighterTest.php new file mode 100644 index 00000000..4d9b212a --- /dev/null +++ b/tests/Unit/Search/SearchHighlighterTest.php @@ -0,0 +1,110 @@ +<?php + +namespace Tests\Unit\Search; + +use App\Search\SearchHighlighter; +use PHPUnit\Framework\TestCase; + +/** + * 검색 하이라이트/평문화 공유 헬퍼 회귀 테스트. + * + * 게시판·상품·페이지 검색 리스너가 공유하는 SSoT 이므로, XSS 이스케이프와 + * 엔티티-태그 부활 차단을 이 한 곳에서 고정한다. + */ +class SearchHighlighterTest extends TestCase +{ + /** + * @scenario case=highlighter_escape + * + * @effects highlight_escapes_markup_and_marks_keyword + */ + public function test_highlight_escapes_markup_in_source_text(): void + { + $result = SearchHighlighter::highlight('<img src=x onerror=alert(1)> 날씨', '날씨'); + + // 원문 태그는 이스케이프되어 실행 계약으로 나가지 않는다. + $this->assertStringNotContainsString('<img', $result); + $this->assertStringContainsString('<img', $result); + $this->assertStringNotContainsString('onerror=alert(1)>', $result); + // 검색어만 <mark> 로 감싸진다. + $this->assertStringContainsString('<mark>날씨</mark>', $result); + } + + /** + * @scenario case=highlighter_escape + * + * @effects highlight_escapes_markup_and_marks_keyword + */ + public function test_highlight_escapes_when_keyword_absent(): void + { + $result = SearchHighlighter::highlight('<script>alert(1)</script>', '없는키워드'); + + $this->assertStringNotContainsString('<script>', $result); + $this->assertStringContainsString('<script>', $result); + $this->assertStringNotContainsString('<mark>', $result); + } + + /** + * @scenario case=highlighter_escape + * + * @effects highlight_escapes_markup_and_marks_keyword + */ + public function test_highlight_returns_empty_for_null_or_empty(): void + { + $this->assertSame('', SearchHighlighter::highlight(null, '키워드')); + $this->assertSame('', SearchHighlighter::highlight('', '키워드')); + } + + /** + * @scenario case=highlighter_escape + * + * @effects highlight_escapes_markup_and_marks_keyword + */ + public function test_highlight_handles_empty_keyword_by_escaping_only(): void + { + $result = SearchHighlighter::highlight('<b>hello</b>', ''); + + $this->assertSame('<b>hello</b>', $result); + } + + /** + * @scenario case=highlighter_plain_text + * + * @effects plain_text_never_resurrects_entity_encoded_tags + */ + public function test_to_plain_text_strips_real_tags(): void + { + $result = SearchHighlighter::toPlainText('<p>hello <strong>world</strong></p>'); + + $this->assertSame('hello world', $result); + } + + /** + * @scenario case=highlighter_plain_text + * + * @effects plain_text_never_resurrects_entity_encoded_tags + */ + public function test_to_plain_text_does_not_resurrect_entity_encoded_tags(): void + { + // 저장 시점에 엔티티로 인코딩된 태그: strip_tags 를 먼저 돌리면 통과 후 디코드로 부활한다. + $encoded = '<script>alert(1)</script> 본문'; + + $result = SearchHighlighter::toPlainText($encoded); + + // 디코드 후 태그 제거 순서라 부활한 태그가 남지 않는다. + $this->assertStringNotContainsString('<script>', $result); + $this->assertStringContainsString('alert(1)', $result); + $this->assertStringContainsString('본문', $result); + } + + /** + * @scenario case=highlighter_plain_text + * + * @effects plain_text_never_resurrects_entity_encoded_tags + */ + public function test_to_plain_text_returns_empty_for_null(): void + { + $this->assertSame('', SearchHighlighter::toPlainText(null)); + $this->assertSame('', SearchHighlighter::toPlainText('')); + } +} diff --git a/tests/Unit/Seo/ComponentHtmlMapperTest.php b/tests/Unit/Seo/ComponentHtmlMapperTest.php index ef3690da..9612c8d5 100644 --- a/tests/Unit/Seo/ComponentHtmlMapperTest.php +++ b/tests/Unit/Seo/ComponentHtmlMapperTest.php @@ -2202,6 +2202,8 @@ class ComponentHtmlMapperTest extends TestCase /** * url 필드가 비어있을 때 download_url로 fallback합니다. + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable */ public function test_image_gallery_download_url_fallback(): void { @@ -2242,6 +2244,9 @@ class ComponentHtmlMapperTest extends TestCase /** * url 필드가 존재하면 download_url보다 우선합니다. + * (공개 자산 디스크 설정 시 download_url 이 직접 CDN URL 이어도 동일 경로로 소비) + * + * @effects download_url_falls_back_to_api_path_when_direct_unavailable */ public function test_image_gallery_url_takes_priority(): void { @@ -2273,6 +2278,97 @@ class ComponentHtmlMapperTest extends TestCase $this->assertStringNotContainsString('product-image/abc123', $html); } + /** + * download_url 이 절대 CDN URL 일 때(공개 자산 디스크 설정) iterate 가드가 + * url() 이중 접두 없이 그대로 통과시킵니다. + * + * 공개#100 이후 download_url 은 상대 API 경로만이 아니라 절대 CDN URL 일 수 + * 있다 — 가드가 http 프리픽스를 오판하면 `http://origin/https://cdn...` 처럼 + * 깨진 src 가 봇 화면에 나간다. + * + * @effects seo_guard_passes_absolute_cdn_url_unmodified + */ + public function test_image_gallery_absolute_cdn_download_url_passes_unmodified(): void + { + $components = [ + [ + 'name' => 'ProductImageViewer', + 'props' => [ + 'images' => '{{product.images}}', + ], + ], + ]; + + $context = [ + 'product' => [ + 'images' => [ + [ + 'url' => '', + 'download_url' => 'https://cdn.test/assets/sirsoft-ecommerce/images/products/1/a.jpg', + 'alt_text_current' => 'CDN 이미지', + ], + ], + ], + ]; + + $html = $this->render($components, $context); + + $this->assertStringContainsString( + 'src="https://cdn.test/assets/sirsoft-ecommerce/images/products/1/a.jpg"', + $html, + ); + // url() 이중 접두 회귀 방어 — 절대 URL 앞에 오리진이 덧붙으면 안 된다 + $this->assertStringNotContainsString('/https://cdn.test/', $html); + } + + /** + * ProductCard(fields attrs 가드)의 thumbnail_url 이 절대 CDN URL 일 때 + * 그대로 통과하고, 상대 경로일 때만 절대화됩니다. + * + * @effects seo_guard_passes_absolute_cdn_url_unmodified + */ + public function test_product_card_thumbnail_absolute_cdn_url_passes_unmodified(): void + { + $components = [ + [ + 'type' => 'composite', + 'name' => 'ProductCard', + 'props' => ['product' => '{{product}}'], + ], + ]; + + $context = [ + 'product' => [ + 'id' => 7, + 'name' => 'CDN Thumb', + 'name_localized' => 'CDN 썸네일 상품', + 'thumbnail_url' => 'https://cdn.test/assets/sirsoft-ecommerce/images/products/7/t.jpg', + 'selling_price_formatted' => '1,000원', + 'list_price_formatted' => '1,000원', + 'discount_rate' => 0, + 'primary_category' => '', + 'brand_name' => '', + 'sales_status' => '', + 'sales_status_label' => '', + 'labels' => [], + ], + ]; + + $html = $this->render($components, $context); + + $this->assertStringContainsString( + 'src="https://cdn.test/assets/sirsoft-ecommerce/images/products/7/t.jpg"', + $html, + ); + $this->assertStringNotContainsString('/https://cdn.test/', $html); + + // 대조군 — 상대 경로는 여전히 절대화된다 (가드의 양방향 동작 고정) + $context['product']['thumbnail_url'] = '/storage/products/rel.jpg'; + $relativeHtml = $this->render($components, $context); + + $this->assertStringContainsString('src="'.url('/storage/products/rel.jpg').'"', $relativeHtml); + } + /** * alt_text_current 필드로 alt 속성이 렌더링됩니다. */ diff --git a/tests/Unit/Seo/Listeners/SeoSettingsCacheListenerTest.php b/tests/Unit/Seo/Listeners/SeoSettingsCacheListenerTest.php index 661de327..6d451957 100644 --- a/tests/Unit/Seo/Listeners/SeoSettingsCacheListenerTest.php +++ b/tests/Unit/Seo/Listeners/SeoSettingsCacheListenerTest.php @@ -4,7 +4,6 @@ namespace Tests\Unit\Seo\Listeners; use App\Listeners\SeoSettingsCacheListener; use App\Seo\Contracts\SeoCacheManagerInterface; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; use Mockery; use Tests\TestCase; @@ -115,6 +114,61 @@ class SeoSettingsCacheListenerTest extends TestCase $this->addToAssertionCount(1); } + // ─── 단건 저장(after_set) ────────────────────────────── + + /** + * 단건 저장 훅도 구독하는지 확인합니다. (실패-먼저) + * + * 단건 저장(`PUT /api/admin/settings/{key}`)은 `after_set` 을 발화하는데 리스너가 + * `after_save` 만 구독해, SEO 설정을 단건으로 바꾸면 캐시가 무효화되지 않았다. + * `after_save` 를 추가 발화하지 않는 이유: 활동로그 리스너가 두 훅을 각각 기록해 + * 저장 1회가 로그 2건이 된다. + */ + public function test_get_subscribed_hooks_includes_after_set(): void + { + $hooks = SeoSettingsCacheListener::getSubscribedHooks(); + + $this->assertArrayHasKey('core.settings.after_set', $hooks); + $this->assertEquals('onSettingSet', $hooks['core.settings.after_set']['method']); + $this->assertEquals(20, $hooks['core.settings.after_set']['priority']); + } + + /** + * seo 카테고리 단건 저장 시 전체 캐시를 삭제합니다. (실패-먼저) + */ + public function test_on_setting_set_clears_cache_for_seo_key(): void + { + $this->cacheMock->shouldReceive('clearAll')->once(); + + $this->listener->onSettingSet('seo.title_suffix', ' | G7', true); + + $this->addToAssertionCount(1); + } + + /** + * 다른 카테고리의 단건 저장은 SEO 캐시를 건드리지 않습니다. + */ + public function test_on_setting_set_ignores_non_seo_key(): void + { + $this->cacheMock->shouldNotReceive('clearAll'); + + $this->listener->onSettingSet('general.site_name', 'Test', true); + + $this->addToAssertionCount(1); + } + + /** + * 저장 실패(result=false)면 캐시를 비우지 않습니다. + */ + public function test_on_setting_set_ignores_failed_save(): void + { + $this->cacheMock->shouldNotReceive('clearAll'); + + $this->listener->onSettingSet('seo.title_suffix', ' | G7', false); + + $this->addToAssertionCount(1); + } + // ─── handle (인터페이스 준수) ─────────────────────────── /** diff --git a/tests/Unit/Seo/SeoMetaResolverTest.php b/tests/Unit/Seo/SeoMetaResolverTest.php index e61c569d..9957dd86 100644 --- a/tests/Unit/Seo/SeoMetaResolverTest.php +++ b/tests/Unit/Seo/SeoMetaResolverTest.php @@ -33,6 +33,9 @@ class SeoMetaResolverTest extends TestCase Config::set('g7_settings.core.seo.google_analytics_id', 'GA-12345'); Config::set('g7_settings.core.seo.google_site_verification', ''); Config::set('g7_settings.core.seo.naver_site_verification', ''); + // 운영자가 로컬에 저장한 값(storage/app/settings/seo.json)이 부팅 시 config 로 + // 실려 들어오므로, 미고정 시 og site_name fallback 단언이 환경에 좌우된다. + Config::set('g7_settings.core.seo.og_default_site_name', ''); Config::set('g7_settings.core.general.site_name', '그누보드7 쇼핑몰'); } @@ -374,6 +377,35 @@ class SeoMetaResolverTest extends TestCase $this->assertSame('나이키 에어맥스', $jsonLd['description']); } + /** + * JSON-LD 는 `<script type="application/ld+json">` 안에 임베드되므로, 사용자 + * 제어 값(검색어 등)의 `</script>` 가 스크립트 컨텍스트를 조기 종료해 실행 가능한 + * script 요소를 재생성하지 못하도록 `<`·`>` 를 유니코드 이스케이프해야 한다. + * (봇 렌더는 _escaped_fragment_ 로 일반 UA 도 강제되므로 반사형 XSS 벡터가 된다.) + */ + public function test_structured_data_escapes_script_breakout_in_json_ld(): void + { + $seoConfig = [ + 'structured_data' => [ + '@type' => 'SearchResultsPage', + 'name' => 'G7 - {{query.q}}', + ], + ]; + + $context = ['query' => ['q' => '</script><script>alert(1)</script>']]; + + $result = $this->resolver->resolve($seoConfig, $context, null, null, []); + + // 원문 태그가 그대로 실리면 스크립트 컨텍스트가 조기 종료돼 실행 가능한 script 요소가 + // 재생성된다(반사형 XSS). JSON_HEX_TAG 가 태그 문자를 유니코드 이스케이프하므로 산출물에는 + // 리터럴 태그 열림/닫힘 문자가 없어야 한다. + $this->assertDoesNotMatchRegularExpression('#</?script#', $result['jsonLd']); + $this->assertStringContainsString(chr(0x5C).'u003C', $result['jsonLd']); + // 이스케이프에도 불구하고 JSON 파싱 시 원래 값이 복원된다(구조화 데이터 의미 보존). + $decoded = json_decode($result['jsonLd'], true); + $this->assertSame('G7 - </script><script>alert(1)</script>', $decoded['name']); + } + /** * 구조화 데이터가 없으면 jsonLd는 null입니다. */ @@ -1562,7 +1594,7 @@ class SeoMetaResolverTest extends TestCase 'page_type' => 'product', 'vars' => [ // 표현식이 다국어 객체를 직접 반환 — substituteVars 까지 array 전달 - 'product_name' => "{{product.data.name}}", + 'product_name' => '{{product.data.name}}', 'commerce_name' => '$module_settings:basic_info.shop_name', ], ]; @@ -1638,4 +1670,32 @@ class SeoMetaResolverTest extends TestCase $this->assertSame('운동화', $result['og']['description']); $this->assertSame('https://e.co/ko.jpg', $result['og']['image']); } + + /** + * 접미사 조립: 저장 시 TrimStrings 가 선행 공백을 제거하므로("| 그누보드7"), + * 제목이 있으면 공백을 복원해 잇고, 제목이 비면 매달린 구분자를 떼어낸다. + * (blade 는 title.titleSuffix 단순 연결 — 조립 규칙은 이 메서드가 SSoT) + */ + public function test_compose_title_suffix_restores_space_between_title_and_trimmed_suffix(): void + { + $this->assertSame(' | 그누보드7', SeoMetaResolver::composeTitleSuffix('가죽 크로스백', '| 그누보드7')); + } + + public function test_compose_title_suffix_keeps_explicit_leading_space_as_is(): void + { + $this->assertSame(' | 그누보드7', SeoMetaResolver::composeTitleSuffix('가죽 크로스백', ' | 그누보드7')); + } + + public function test_compose_title_suffix_strips_dangling_separator_when_title_is_empty(): void + { + $this->assertSame('그누보드7', SeoMetaResolver::composeTitleSuffix('', '| 그누보드7')); + $this->assertSame('그누보드7', SeoMetaResolver::composeTitleSuffix('', ' - 그누보드7')); + } + + public function test_compose_title_suffix_returns_empty_when_suffix_is_blank(): void + { + $this->assertSame('', SeoMetaResolver::composeTitleSuffix('가죽 크로스백', '')); + $this->assertSame('', SeoMetaResolver::composeTitleSuffix('', ' ')); + $this->assertSame('', SeoMetaResolver::composeTitleSuffix('', '| ')); + } } diff --git a/tests/Unit/Services/DriverConnectionTesterTest.php b/tests/Unit/Services/DriverConnectionTesterTest.php new file mode 100644 index 00000000..61350312 --- /dev/null +++ b/tests/Unit/Services/DriverConnectionTesterTest.php @@ -0,0 +1,236 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Services\DriverConnectionTester; +use Illuminate\Support\Facades\Http; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter; +use Tests\TestCase; + +/** + * DriverConnectionTester 테스트 (공개 #99 / 내부 #563, A5·A6) + * + * testS3 의 endpoint/path-style 반영·어댑터 선검사, testWebsocket 의 + * client/server 양측 endpoint probe 를 검증합니다. + * + * 한계: Http::fake 는 요청 URL 계약까지만 검증한다 (요청 본문 미검증 — + * feedback_http_fake_does_not_validate_request_body). S3 종단 검증은 + * Chrome MCP MinIO 매트릭스가 담당한다. + */ +class DriverConnectionTesterTest extends TestCase +{ + private DriverConnectionTester $tester; + + protected function setUp(): void + { + parent::setUp(); + + $this->tester = new DriverConnectionTester; + } + + /** + * 필수 설정 누락 시 s3_missing_config 메시지를 반환하는지 테스트합니다. + */ + public function test_s3_missing_config_returns_failure(): void + { + $result = $this->tester->testS3(['s3_bucket' => 'only-bucket']); + + $this->assertFalse($result['success']); + $this->assertSame(__('settings.s3_missing_config'), $result['message']); + } + + /** + * 커스텀 endpoint 가 실제 아웃바운드 대상에 반영되는지 테스트합니다. + * + * 도달 불가한 로컬 endpoint 로 시도 → 연결 실패의 error 문자열에 해당 endpoint + * 호스트가 나타나야 한다 (AWS 리전 도메인이 아니라). 이것이 endpoint 미반영 + * 결함(#99 ③ — s3_url 만 있고 SDK 요청 대상은 amazonaws.com 고정)의 회귀 가드다. + * + * @scenario region_shape=custom_minio,endpoint=path_style,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_s3_custom_endpoint_reaches_outbound_target(): void + { + $result = $this->tester->testS3([ + 's3_bucket' => 'test-bucket', + 's3_region' => 'us-east-1', + 's3_access_key' => 'test-key', + 's3_secret_key' => 'test-secret', + 's3_endpoint' => 'http://127.0.0.1:9', + 's3_use_path_style' => true, + ]); + + $this->assertFalse($result['success']); + $this->assertArrayHasKey('error', $result); + $this->assertStringContainsString( + '127.0.0.1', + $result['error'], + '아웃바운드가 커스텀 endpoint 를 향해야 함 — amazonaws.com 고정이면 endpoint 미반영 회귀' + ); + $this->assertStringNotContainsString('amazonaws.com', $result['error']); + } + + /** + * path-style 토글이 아웃바운드 주소 형태를 실제로 바꾸는지 테스트합니다. + * + * IP endpoint 는 SDK 가 path-style 을 자동 강제해 off↔on 이 구분되지 않으므로 + * hostname endpoint 로 검증한다: off = virtual-hosted(버킷이 호스트에 접두) / + * on = path-style(버킷이 경로로 이동). off 상태로도 통과하던 endpoint 단언의 + * 사각을 메우는 회귀 가드다. + * + * @scenario region_shape=custom_minio,endpoint=path_style,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects endpoint_injected + */ + public function test_s3_path_style_toggle_changes_outbound_address_shape(): void + { + $config = [ + 's3_bucket' => 'test-bucket', + 's3_region' => 'us-east-1', + 's3_access_key' => 'test-key', + 's3_secret_key' => 'test-secret', + 's3_endpoint' => 'http://s3-parity.invalid:9', + ]; + + $virtualHosted = $this->tester->testS3($config + ['s3_use_path_style' => false]); + $pathStyle = $this->tester->testS3($config + ['s3_use_path_style' => true]); + + $this->assertFalse($virtualHosted['success']); + $this->assertFalse($pathStyle['success']); + + // off: 버킷명이 호스트에 접두된 virtual-hosted 주소를 향해야 한다 + $this->assertStringContainsString( + 'test-bucket.s3-parity.invalid', + $virtualHosted['error'], + 'path-style off 는 virtual-hosted 주소여야 함' + ); + + // on: 버킷 접두 호스트가 사라져야 한다 — off 와 동일하면 path-style 주입 회귀 + $this->assertStringNotContainsString( + 'test-bucket.s3-parity.invalid', + $pathStyle['error'], + 'path-style on 인데 버킷 접두 호스트가 남아 있으면 use_path_style_endpoint 미반영 회귀' + ); + $this->assertStringContainsString('s3-parity.invalid', $pathStyle['error']); + } + + /** + * 어댑터 선검사 계약 — 어댑터가 설치된 환경에서는 missing 분기가 발동하지 않고, + * 코드가 어댑터 존재를 검사하고 있는지(클래스 상수 참조) 검증합니다. + * + * 어댑터는 composer 필수 의존성이라 부재 상태를 만들 수 없으므로 구성 검증으로 대체한다 + * (계획서 A5 — "mock 불가 시 구성 검증으로 대체"). + */ + public function test_s3_adapter_precheck_present_in_code(): void + { + $this->assertTrue( + class_exists(AwsS3V3Adapter::class), + '어댑터는 코어 필수 의존성 (#99 A1)' + ); + + $source = file_get_contents(app_path('Services/DriverConnectionTester.php')); + $this->assertStringContainsString( + 'AwsS3V3Adapter::class', + $source, + 'testS3 는 어댑터 존재 선검사를 유지해야 함 — 테스트(SDK 직접)와 실경로(Flysystem)의 분리가 결함을 가리는 구조 방지 (A5)' + ); + $this->assertStringContainsString('s3_adapter_missing', $source); + } + + /** + * testWebsocket 이 client 와 server endpoint 를 모두 probe 하는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects test_covers_server_websocket + */ + public function test_websocket_probes_both_client_and_server_endpoints(): void + { + Http::fake([ + 'client.test:8443*' => Http::response('ok', 200), + 'server.test:8080*' => Http::response('ok', 200), + ]); + + $result = $this->tester->testWebsocket([ + 'websocket_host' => 'client.test', + 'websocket_port' => 8443, + 'websocket_scheme' => 'https', + 'websocket_server_host' => 'server.test', + 'websocket_server_port' => 8080, + 'websocket_server_scheme' => 'http', + ]); + + $this->assertTrue($result['success']); + Http::assertSent(fn ($request) => str_starts_with($request->url(), 'https://client.test')); + Http::assertSent(fn ($request) => str_starts_with($request->url(), 'http://server.test')); + } + + /** + * server endpoint 실패 시 어느 쪽 실패인지 구분된 메시지를 반환하는지 테스트합니다. + */ + public function test_websocket_server_endpoint_failure_is_distinguished(): void + { + Http::fake([ + 'client.test:8443*' => Http::response('ok', 200), + 'server.test:8080*' => Http::response('error', 500), + ]); + + $result = $this->tester->testWebsocket([ + 'websocket_host' => 'client.test', + 'websocket_port' => 8443, + 'websocket_scheme' => 'https', + 'websocket_server_host' => 'server.test', + 'websocket_server_port' => 8080, + 'websocket_server_scheme' => 'http', + ]); + + $this->assertFalse($result['success']); + $this->assertSame( + __('settings.websocket_server_test_failed'), + $result['message'], + '백엔드 발송은 server endpoint 를 쓴다 — client 만 검사하면 "테스트 성공 + 실제 발송 실패" (A6)' + ); + } + + /** + * server 필드가 비어 있으면 client 값으로 폴백하며 중복 probe 를 생략하는지 테스트합니다. + */ + public function test_websocket_server_fields_fallback_to_client(): void + { + Http::fake([ + 'client.test:8443*' => Http::response('ok', 200), + ]); + + $result = $this->tester->testWebsocket([ + 'websocket_host' => 'client.test', + 'websocket_port' => 8443, + 'websocket_scheme' => 'https', + ]); + + $this->assertTrue($result['success']); + Http::assertSentCount(1); + } + + /** + * client endpoint 실패 시 기존 메시지 계약이 유지되는지 테스트합니다 (하위호환). + */ + public function test_websocket_client_failure_keeps_existing_contract(): void + { + Http::fake([ + 'client.test:8443*' => Http::response('error', 500), + ]); + + $result = $this->tester->testWebsocket([ + 'websocket_host' => 'client.test', + 'websocket_port' => 8443, + 'websocket_scheme' => 'https', + 'websocket_server_host' => 'server.test', + 'websocket_server_port' => 8080, + 'websocket_server_scheme' => 'http', + ]); + + $this->assertFalse($result['success']); + $this->assertSame(__('settings.websocket_test_failed'), $result['message']); + } +} diff --git a/tests/Unit/Services/DriverExtensionIntegrationTest.php b/tests/Unit/Services/DriverExtensionIntegrationTest.php index c31629e6..3f6f105e 100644 --- a/tests/Unit/Services/DriverExtensionIntegrationTest.php +++ b/tests/Unit/Services/DriverExtensionIntegrationTest.php @@ -27,7 +27,7 @@ class DriverExtensionIntegrationTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->driverRegistry = new DriverRegistryService(); + $this->driverRegistry = new DriverRegistryService; // 테스트용 디렉토리 정리 Storage::disk('modules')->deleteDirectory('test-driver-module'); @@ -51,12 +51,14 @@ class DriverExtensionIntegrationTest extends TestCase // ======================================================================== /** - * 7개 카테고리 모두에 플러그인 드라이버를 등록하면 available 목록에 포함되는지 검증합니다. + * 8개 카테고리 모두에 플러그인 드라이버를 등록하면 available 목록에 포함되는지 검증합니다. + * + * @effects settings_catalog_includes_plugin_registered_disks */ #[Test] public function plugin_can_register_drivers_for_all_categories(): void { - $categories = ['storage', 'cache', 'session', 'queue', 'log', 'websocket', 'mail']; + $categories = ['storage', 'public_asset', 'cache', 'session', 'queue', 'log', 'websocket', 'mail']; foreach ($categories as $category) { HookManager::addFilter( @@ -568,6 +570,7 @@ class DriverExtensionIntegrationTest extends TestCase { $expectedDefaults = [ 'storage' => ['local', 'filesystems.default'], + 'public_asset' => ['none', 'core.storage.public_asset_disk'], 'cache' => ['file', 'cache.default'], 'session' => ['database', 'session.driver'], 'queue' => ['database', 'queue.default'], diff --git a/tests/Unit/Services/DriverRegistryServiceTest.php b/tests/Unit/Services/DriverRegistryServiceTest.php index fb46df27..cfb64a1a 100644 --- a/tests/Unit/Services/DriverRegistryServiceTest.php +++ b/tests/Unit/Services/DriverRegistryServiceTest.php @@ -24,7 +24,7 @@ class DriverRegistryServiceTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->service = new DriverRegistryService(); + $this->service = new DriverRegistryService; } protected function tearDown(): void @@ -35,16 +35,23 @@ class DriverRegistryServiceTest extends TestCase } /** - * 코어 드라이버가 7개 카테고리를 모두 포함하는지 검증합니다. + * 코어 드라이버가 9개 카테고리를 모두 포함하는지 검증합니다. + * + * search 는 폴백 가드 편입(A5b)으로 추가되었다 — 이 목록에서 빠지면 검색엔진 플러그인 + * 제거 시 죽은 `scout.driver` 가 그대로 남아 공개 검색이 500 이 된다. + * + * @scenario engine_source=core + * + * @effects search_category_registered_in_driver_registry */ #[Test] - public function it_returns_all_seven_categories(): void + public function it_returns_all_nine_categories(): void { $categories = $this->service->getCategories(); - $this->assertCount(7, $categories); + $this->assertCount(9, $categories); $this->assertEquals( - ['storage', 'cache', 'session', 'queue', 'log', 'websocket', 'mail'], + ['storage', 'public_asset', 'cache', 'session', 'queue', 'log', 'websocket', 'mail', 'search'], $categories ); } @@ -76,6 +83,40 @@ class DriverRegistryServiceTest extends TestCase $this->assertContains('ses', $driverIds); } + /** + * 공개 자산 카테고리 코어 드라이버를 검증합니다. + * + * 화면 Select 가 그대로 그리는 목록이므로 순서까지 고정합니다 + * ('none' 이 선두가 아니면 기본 선택값이 바뀐다). + * + * @effects settings_catalog_includes_plugin_registered_disks + */ + #[Test] + public function it_returns_public_asset_core_drivers_in_order(): void + { + $drivers = $this->service->getAvailableDrivers('public_asset'); + + $this->assertSame(['none', 'public', 's3'], array_column($drivers, 'id')); + } + + /** + * 공개 자산 디스크의 사용 가능 여부 판별을 검증합니다. + * + * SaveSettingsRequest 의 closure rule 이 이 판정에 의존하므로, + * 미등록 값이 true 로 새면 422 게이트가 통째로 무력화됩니다. + * + * @effects invalid_disk_rejected_with_422 + */ + #[Test] + public function it_reports_public_asset_driver_availability(): void + { + $this->assertTrue($this->service->isDriverAvailable('public_asset', 'none')); + $this->assertTrue($this->service->isDriverAvailable('public_asset', 'public')); + $this->assertTrue($this->service->isDriverAvailable('public_asset', 's3')); + + $this->assertFalse($this->service->isDriverAvailable('public_asset', 'nonexistent_disk')); + } + /** * 모든 카테고리의 드라이버를 한 번에 반환하는지 검증합니다. */ @@ -85,13 +126,15 @@ class DriverRegistryServiceTest extends TestCase $all = $this->service->getAllAvailableDrivers(); $this->assertArrayHasKey('storage', $all); + $this->assertArrayHasKey('public_asset', $all); $this->assertArrayHasKey('cache', $all); $this->assertArrayHasKey('session', $all); $this->assertArrayHasKey('queue', $all); $this->assertArrayHasKey('log', $all); $this->assertArrayHasKey('websocket', $all); $this->assertArrayHasKey('mail', $all); - $this->assertCount(7, $all); + $this->assertArrayHasKey('search', $all); + $this->assertCount(9, $all); } /** @@ -137,6 +180,7 @@ class DriverRegistryServiceTest extends TestCase public function it_returns_correct_default_drivers(): void { $this->assertEquals('local', $this->service->getDefaultDriver('storage')); + $this->assertEquals('none', $this->service->getDefaultDriver('public_asset')); $this->assertEquals('file', $this->service->getDefaultDriver('cache')); $this->assertEquals('database', $this->service->getDefaultDriver('session')); $this->assertEquals('database', $this->service->getDefaultDriver('queue')); diff --git a/tests/Unit/Services/DriverRegistryServiceUsabilityTest.php b/tests/Unit/Services/DriverRegistryServiceUsabilityTest.php new file mode 100644 index 00000000..75ee8922 --- /dev/null +++ b/tests/Unit/Services/DriverRegistryServiceUsabilityTest.php @@ -0,0 +1,132 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Services\DriverRegistryService; +use League\Flysystem\AwsS3V3\AwsS3V3Adapter; +use Predis\Client; +use Tests\TestCase; + +/** + * DriverRegistryService 드라이버 가용성 판정 테스트 (공개 #99 / 내부 #563, A7) + * + * isDriverUsable()/usabilityFailureReason() 의 런타임 능력 판정과 + * a5(CONFIG_KEYS 로그 키 정정)·a6(websocket 설정 키 제외) 회귀를 검증합니다. + */ +class DriverRegistryServiceUsabilityTest extends TestCase +{ + private DriverRegistryService $service; + + protected function setUp(): void + { + parent::setUp(); + + $this->service = new DriverRegistryService; + } + + /** + * s3 드라이버 가용성이 Flysystem 어댑터 존재와 일치하는지 테스트합니다. + * + * 어댑터는 composer require 로 코어에 포함되므로(#99 A1) 항상 true 여야 합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=local_default + * + * @effects s3_disk_resolves + */ + public function test_s3_usability_matches_adapter_presence(): void + { + $expected = class_exists(AwsS3V3Adapter::class); + + $this->assertSame($expected, $this->service->isDriverUsable('storage', 's3')); + $this->assertTrue( + $expected, + 'league/flysystem-aws-s3-v3 는 코어 필수 의존성 — 부재 시 S3 선택이 즉사한다 (#99 A1)' + ); + $this->assertNull($this->service->usabilityFailureReason('storage', 's3')); + } + + /** + * redis 드라이버 가용성이 phpredis 확장 또는 predis 존재와 일치하는지 테스트합니다. + * + * predis 는 composer require 로 코어에 포함되므로(#99 A2) 항상 true 여야 합니다. + */ + public function test_redis_usability_matches_client_presence(): void + { + $expected = extension_loaded('redis') || class_exists(Client::class); + + $this->assertSame($expected, $this->service->isDriverUsable('cache', 'redis')); + $this->assertTrue( + $expected, + 'predis/predis 는 코어 필수 의존성 — 확장 없는 서버에서 redis 선택 시 전면 다운 방어 (#99 A2)' + ); + + // predis 명시 설정 분기 — 판정은 설정 클라이언트 기준 (predis 는 코어 의존성이라 참) + config(['database.redis.client' => 'predis']); + $this->assertTrue($this->service->isDriverUsable('cache', 'redis')); + } + + /** + * memcached 드라이버 가용성이 PHP 확장 존재와 일치하는지 테스트합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_absent,attachment_disk=local_default + * + * @effects unusable_driver_rejected + */ + public function test_memcached_usability_matches_extension(): void + { + $expected = extension_loaded('memcached'); + + $this->assertSame($expected, $this->service->isDriverUsable('cache', 'memcached')); + + if (! $expected) { + $this->assertNotNull( + $this->service->usabilityFailureReason('cache', 'memcached'), + '사용 불능 드라이버는 사유 문자열을 제공해야 함 (422 메시지 파라미터)' + ); + } + } + + /** + * 능력 요구가 없는 드라이버(local/file 등)와 미지정 드라이버는 항상 사용 가능해야 합니다. + */ + public function test_unknown_and_capability_free_drivers_are_usable(): void + { + $this->assertTrue($this->service->isDriverUsable('storage', 'local')); + $this->assertTrue($this->service->isDriverUsable('cache', 'file')); + $this->assertTrue($this->service->isDriverUsable('queue', 'database')); + // 플러그인 등록 드라이버 — 가용성은 해당 플러그인 SP 가 보증하므로 코어는 통과 + $this->assertTrue($this->service->isDriverUsable('storage', 'plugin-custom-driver')); + } + + /** + * a5 회귀 — log 카테고리 Config 키가 실제 적용 키(stack.channels)와 일치해야 합니다. + */ + public function test_log_config_key_matches_actual_applied_key(): void + { + $this->assertSame( + 'logging.channels.stack.channels', + $this->service->getConfigKey('log'), + 'logging.default 는 어디에서도 읽지 않는 죽은 키 — applyLogConfig 실제 기록 키와 일치해야 폴백이 실효한다 (a5)' + ); + } + + /** + * log 카테고리의 Config 기록 값은 채널 배열 형태여야 합니다. + */ + public function test_log_config_value_is_channel_array(): void + { + $this->assertSame(['daily'], $this->service->getConfigValueForDriver('log', 'daily')); + $this->assertSame('file', $this->service->getConfigValueForDriver('cache', 'file')); + } + + /** + * a6 회귀 — websocket 은 드라이버 ID 선택 설정이 없으므로 설정 키 매핑에서 제외되어야 합니다. + */ + public function test_websocket_settings_key_excluded(): void + { + $this->assertNull( + $this->service->getSettingsKey('websocket'), + "websocket 은 불리언 websocket_enabled 뿐 — 종전 'websocket_driver' 는 어떤 저장 경로에도 없는 유령 키였다 (a6)" + ); + } +} diff --git a/tests/Unit/Services/LanguagePack/LanguagePackServiceTest.php b/tests/Unit/Services/LanguagePack/LanguagePackServiceTest.php index 80912ea5..84a5270a 100644 --- a/tests/Unit/Services/LanguagePack/LanguagePackServiceTest.php +++ b/tests/Unit/Services/LanguagePack/LanguagePackServiceTest.php @@ -13,6 +13,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Http; use Tests\TestCase; /** @@ -768,6 +769,51 @@ class LanguagePackServiceTest extends TestCase } } + /** + * URL 설치가 성공해도 다운로드 임시 디렉토리(storage/app/temp/{uuid})가 남지 않는다. + * + * 임시 디렉토리 삭제가 catch 경로에만 있으면 실패 시에는 정리되고 성공 시에는 + * 100% 잔존한다 — 예외도 로그도 없이 설치할 때마다 ZIP 사본이 쌓이는 누수다. + * 성공 경로를 실제로 통과시켜 잔존물이 0건임을 고정한다. + * + * @scenario vector=install_from_url, actor_permission=cli_system + * + * @effects successful_install_leaves_no_temp_directory + */ + public function test_install_from_url_success_leaves_no_temp_directory(): void + { + $identifier = 'tmpleak-core-ja'; + $zipPath = tempnam(sys_get_temp_dir(), 'lp_zip'); + $zip = new \ZipArchive; + $zip->open($zipPath, \ZipArchive::OVERWRITE); + $zip->addFromString( + 'language-pack.json', + (string) json_encode($this->coreJaManifest($identifier, 'tmpleak', '1.0.0'), JSON_UNESCAPED_UNICODE) + ); + $zip->close(); + + Http::fake(['https://packs.example.com/*' => Http::response((string) file_get_contents($zipPath), 200)]); + + $tempRoot = storage_path('app/temp'); + $before = File::isDirectory($tempRoot) ? File::directories($tempRoot) : []; + + try { + $pack = $this->service->installFromUrl('https://packs.example.com/pack.zip', null); + + $this->assertSame($identifier, $pack->identifier); + + $after = File::isDirectory($tempRoot) ? File::directories($tempRoot) : []; + $this->assertSame( + [], + array_values(array_diff($after, $before)), + '설치가 성공했는데 storage/app/temp 에 다운로드 임시 디렉토리가 남았다' + ); + } finally { + @unlink($zipPath); + File::deleteDirectory(base_path('lang-packs/'.$identifier)); + } + } + /** * 코어 ja 번들 픽스처용 manifest 를 만듭니다. * diff --git a/tests/Unit/Services/LanguagePackServiceUpdateCheckScopeTest.php b/tests/Unit/Services/LanguagePackServiceUpdateCheckScopeTest.php new file mode 100644 index 00000000..446cf6c4 --- /dev/null +++ b/tests/Unit/Services/LanguagePackServiceUpdateCheckScopeTest.php @@ -0,0 +1,79 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Contracts\Repositories\LanguagePackRepositoryInterface; +use App\Models\LanguagePack; +use App\Services\LanguagePackService; +use Illuminate\Foundation\Testing\DatabaseTransactions; +use Tests\TestCase; + +/** + * 언어팩 업데이트 확인의 순회 범위 계약 테스트 (공개 이슈 #102 동형) + * + * 종전에는 `repository->paginate([], 1000)` 로 전량 순회를 의도했는데, page 인자를 + * 지정하지 않아 HTTP `page` 파라미터가 암묵 해석됐다. `?page=2` 상태로 업데이트 확인을 + * 호출하면 offset=1000 이 적용되어 `checked: 0` 의 조용한 오답이 됐다. + * + * 수정 후 계약: 업데이트 확인은 요청의 page 파라미터와 무관하게 설치된 전체 팩을 순회한다. + * + * @scenario case=update_check_scope + * + * @effects update_check_ignores_request_page + */ +class LanguagePackServiceUpdateCheckScopeTest extends TestCase +{ + use DatabaseTransactions; + + /** + * 요청에 page=2 가 실려 있어도 순회 대상이 설치된 전체 팩인지 확인 + * + * 순회 계층에서 건수를 정확히 단언한다 — 서비스 `checked` 는 GitHub URL 도 번들 + * manifest 도 없는 팩을 설계상 건너뛰므로 설치본 수와 같지 않고, 그 판정을 테스트가 + * 재구현하면 규칙이 두 벌이 되어 한쪽만 바뀐다. 결함이 있던 자리는 순회 그 자체다. + */ + public function test_update_check_traverses_all_packs_regardless_of_request_page(): void + { + // Given: bundled manifest 가 실존하는 설치 팩 1건 (GitHub 미설정 → bundled 폴백, 네트워크 없음) + LanguagePack::query()->firstOrCreate( + ['identifier' => 'g7-core-ja'], + [ + 'vendor' => 'g7', + 'scope' => 'core', + 'target_identifier' => 'core', + 'locale' => 'ja', + 'locale_name' => 'Japanese', + 'locale_native_name' => '日本語', + 'text_direction' => 'ltr', + 'version' => '0.0.1', + 'license' => 'MIT', + 'status' => 'active', + 'source_type' => 'bundled', + 'manifest' => ['identifier' => 'g7-core-ja', 'version' => '0.0.1'], + ] + ); + + $installed = LanguagePack::query()->count(); + $this->assertGreaterThan(0, $installed, '설치된 팩이 없어 순회 범위를 측정할 수 없습니다.'); + + // 페이지네이터의 현재 페이지 암묵 해석이 읽는 자리 — #102 재현 조건 + request()->merge(['page' => 2]); + + $traversed = app(LanguagePackRepositoryInterface::class)->allForUpdateCheck(); + + $this->assertCount( + $installed, + $traversed, + "page=2 요청 상태에서 순회 대상이 {$traversed->count()} 건이었습니다 (설치 {$installed} 건) — 페이지네이션 암묵 page 해석(#102 동형)입니다." + ); + + // 서비스 경로도 같은 순회를 타는지 — 종단에서 0건 오답이 재발하지 않음을 고정 + $result = app(LanguagePackService::class)->checkUpdates(); + + $this->assertGreaterThan( + 0, + $result['checked'], + '업데이트 확인이 0건 순회했습니다 — 순회는 정상인데 서비스가 다른 경로를 쓰고 있습니다.' + ); + } +} diff --git a/tests/Unit/Services/ScheduleServiceTest.php b/tests/Unit/Services/ScheduleServiceTest.php index b1d8fabb..80faea9a 100644 --- a/tests/Unit/Services/ScheduleServiceTest.php +++ b/tests/Unit/Services/ScheduleServiceTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Services; +use App\Enums\ScheduleResultStatus; use App\Enums\ScheduleType; use App\Extension\HookManager; use App\Models\Schedule; @@ -9,6 +10,8 @@ use App\Models\ScheduleHistory; use App\Services\ScheduleService; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Facades\Process; +use Illuminate\Validation\ValidationException; use Tests\TestCase; /** @@ -87,6 +90,45 @@ class ScheduleServiceTest extends TestCase $this->assertSame(1, $result->total()); } + // ======================================================================== + // runSchedule() - 실행 직전 방어선 (KVE-2026-1653) + // ======================================================================== + + /** + * 게이트를 켜고 `bash` 를 등록한 상태라도, DB 에 직접 심어진 `bash -c id` 는 + * 실행 직전 방어선에서 차단된다 (저장 검증을 우회해 들어온 값 방어). + * + * @scenario command_class=inline_code, enforcement_point=run + * + * @effects run_time_last_line_of_defense_blocks_inline_code + */ + public function test_run_schedule_blocks_interpreter_inline_code(): void + { + config([ + 'schedule_security.shell.enabled' => true, + 'schedule_security.shell.allowed_binaries' => ['bash'], + ]); + Process::fake(); + + $schedule = Schedule::create([ + 'name' => 'DB 직접 주입 — 인라인 코드', + 'type' => ScheduleType::Shell, + 'command' => 'bash -c id', + 'expression' => '* * * * *', + 'is_active' => true, + ]); + + try { + $this->scheduleService->runSchedule($schedule); + $this->fail('인라인 코드 스케줄이 실행됨'); + } catch (ValidationException) { + // 실행 직전 방어선이 차단 — 아래에서 미실행·실패 기록을 확인한다 + } + + Process::assertNothingRan(); + $this->assertSame(ScheduleResultStatus::Failed, $schedule->fresh()->last_result); + } + // ======================================================================== // delete() - 관계 레코드 명시적 삭제 검증 // ======================================================================== diff --git a/tests/Unit/Services/SettingsServiceAppConfigTest.php b/tests/Unit/Services/SettingsServiceAppConfigTest.php index 4b445dd3..615e5b50 100644 --- a/tests/Unit/Services/SettingsServiceAppConfigTest.php +++ b/tests/Unit/Services/SettingsServiceAppConfigTest.php @@ -109,6 +109,13 @@ class SettingsServiceAppConfigTest extends TestCase /** * getAppConfigForFrontend()가 core.frontend.filter_app_config 훅으로 확장이 주입한 값을 * appConfig 에 반영하는지 테스트합니다. (예: 이커머스 모듈이 요청 기기 유형 isIos 를 주입) + * + * 체크아웃 브랜드 마크 시나리오의 iOS 게이팅 체인 중 "모듈이 감지한 기기 정보가 코어 + * appConfig 를 거쳐 프론트로 전달되는" 구간을 이 테스트가 떠받친다. + * + * @scenario requires_ios=true, device=ios + * + * @effects is_ios_flows_to_global_appconfig */ public function test_get_app_config_applies_frontend_filter_hook(): void { diff --git a/tests/Unit/Services/SettingsServiceSetSettingSideEffectsTest.php b/tests/Unit/Services/SettingsServiceSetSettingSideEffectsTest.php new file mode 100644 index 00000000..af13881c --- /dev/null +++ b/tests/Unit/Services/SettingsServiceSetSettingSideEffectsTest.php @@ -0,0 +1,192 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Contracts\Extension\CacheInterface; +use App\Contracts\Repositories\AttachmentRepositoryInterface; +use App\Contracts\Repositories\ConfigRepositoryInterface; +use App\Seo\Contracts\SeoCacheManagerInterface; +use App\Services\SettingsService; +use Illuminate\Support\Facades\Artisan; +use Tests\TestCase; + +/** + * 코어 단건 설정 저장(setSetting)의 부수효과 정합 테스트 (공개 #114 동종, B-3) + * + * 단건 저장은 `PUT /api/admin/settings/{key}` 로 실제 도달 가능한 경로인데, 벌크 저장이 + * 수행하는 부수효과 중 일부를 건너뛰었다: + * + * - `general.asset_url_mode` 변경 시 SEO 프리렌더 캐시 삭제 + * - `drivers` 카테고리 저장 시 큐 워커 재시작 신호 + * + * 벌크 위임은 채택하지 않는다 — 입력 shape 이 다르고(벌크는 화면 키 + reverseFrontendKeys + * 전제, 단건은 원본 dot-key), 벌크의 shallow `array_merge` 로는 `identity.purpose_providers.X.Y` + * 같은 깊은 키를 저장할 때 형제 매핑이 통째로 소실된다. 부수효과만 이식한다. + */ +class SettingsServiceSetSettingSideEffectsTest extends TestCase +{ + /** + * ConfigRepository/AttachmentRepository 를 mock 한 SettingsService 를 만듭니다. + * + * @param mixed $existingValue `get()` 이 돌려줄 기존 값 + * @param bool $setResult `set()` 의 반환값 + * @return SettingsService 대상 서비스 + */ + private function makeService(mixed $existingValue = null, bool $setResult = true): SettingsService + { + $configRepository = $this->mock(ConfigRepositoryInterface::class)->shouldIgnoreMissing(); + $configRepository->shouldReceive('get')->andReturn($existingValue); + $configRepository->shouldReceive('set')->andReturn($setResult); + + $this->mock(AttachmentRepositoryInterface::class)->shouldIgnoreMissing(); + $this->mock(CacheInterface::class)->shouldIgnoreMissing(); + + return app(SettingsService::class); + } + + /** + * 자산 URL 방식을 단건으로 바꾸면 SEO 프리렌더 캐시가 삭제된다. (실패-먼저) + * + * @effects seo_prerender_cache_cleared_on_asset_url_mode_change + */ + public function test_asset_url_mode_change_clears_seo_cache(): void + { + $this->mock(SeoCacheManagerInterface::class) + ->shouldReceive('clearAll') + ->once(); + + $service = $this->makeService(existingValue: 'relative'); + + $this->assertTrue($service->setSetting('general.asset_url_mode', 'absolute')); + } + + /** + * 값이 그대로면 SEO 캐시를 건드리지 않는다. + * + * @effects seo_prerender_cache_untouched_when_value_unchanged + */ + public function test_same_asset_url_mode_does_not_clear_seo_cache(): void + { + $this->mock(SeoCacheManagerInterface::class) + ->shouldReceive('clearAll') + ->never(); + + $service = $this->makeService(existingValue: 'absolute'); + + $this->assertTrue($service->setSetting('general.asset_url_mode', 'absolute')); + } + + /** + * 다른 키 저장은 SEO 프리렌더 캐시를 건드리지 않는다. + * + * @effects seo_prerender_cache_untouched_when_value_unchanged + */ + public function test_unrelated_key_does_not_clear_seo_cache(): void + { + $this->mock(SeoCacheManagerInterface::class) + ->shouldReceive('clearAll') + ->never(); + + $service = $this->makeService(existingValue: 'old'); + + $this->assertTrue($service->setSetting('general.site_name', 'new')); + } + + /** + * drivers 카테고리 단건 저장은 큐 워커 재시작 신호를 보낸다. (실패-먼저) + * + * @effects queue_workers_restarted_on_driver_change + */ + public function test_driver_setting_restarts_queue_workers(): void + { + Artisan::shouldReceive('call')->with('config:clear')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('config:cache')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('queue:restart')->once(); + + $service = $this->makeService(existingValue: 'sync'); + + $this->assertTrue($service->setSetting('drivers.queue_driver', 'redis')); + } + + /** + * drivers 외 카테고리는 큐 워커를 재시작하지 않는다. + * + * @effects queue_workers_not_restarted_for_other_categories + */ + public function test_non_driver_setting_does_not_restart_queue_workers(): void + { + Artisan::shouldReceive('call')->with('config:clear')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('config:cache')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('queue:restart')->never(); + + $service = $this->makeService(existingValue: 'old'); + + $this->assertTrue($service->setSetting('general.site_name', 'new')); + } + + /** + * 저장 실패 시에는 부수효과가 실행되지 않는다. + * + * @effects side_effects_skipped_when_save_fails + */ + public function test_failed_save_skips_side_effects(): void + { + Artisan::shouldReceive('call')->with('config:clear')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('config:cache')->zeroOrMoreTimes(); + Artisan::shouldReceive('call')->with('queue:restart')->never(); + + $this->mock(SeoCacheManagerInterface::class) + ->shouldReceive('clearAll') + ->never(); + + $service = $this->makeService(existingValue: 'sync', setResult: false); + + $this->assertFalse($service->setSetting('drivers.queue_driver', 'redis')); + } + + /** + * 단건 저장은 받은 키를 변형 없이 저장소에 넘긴다. (벌크 위임 금지 — 계약 고정) + * + * 벌크 저장은 입력을 탭/필드로 쪼개 `reverseFrontendKeys()`(화면 키 → 저장소 키)를 태운 뒤 + * shallow `array_merge` 로 병합한다. 단건 경로를 벌크에 위임하면 두 가지가 깨진다. + * + * - 저장소 키를 직접 넘기는 프로그램 호출자(본인인증 플러그인 설치/삭제의 + * `identity.purpose_providers.*`)의 키가 역변환 대상에 걸릴 수 있다. + * - shallow merge 는 깊은 키를 평평한 문자열 키로 취급해 형제 provider 매핑을 통째로 + * 지운다 — 다른 목적(purpose)의 인증수단 설정이 조용히 사라진다. + * + * 그래서 단건 경로는 받은 키를 그대로 `ConfigRepository::set()` 에 넘긴다. + * 이 테스트는 위임·키 분해·역변환 중 어느 것이 끼어도 실패한다. + * + * @effects single_key_save_uses_storage_key_verbatim + */ + public function test_single_key_save_passes_the_storage_key_verbatim(): void + { + Artisan::shouldReceive('call')->zeroOrMoreTimes(); + + $written = []; + $configRepository = $this->mock(ConfigRepositoryInterface::class)->shouldIgnoreMissing(); + $configRepository->shouldReceive('get')->andReturn(null); + $configRepository->shouldReceive('set')->andReturnUsing(function (string $key, mixed $value) use (&$written): bool { + $written[] = $key; + + return true; + }); + + $this->mock(AttachmentRepositoryInterface::class)->shouldIgnoreMissing(); + $this->mock(CacheInterface::class)->shouldIgnoreMissing(); + + $service = app(SettingsService::class); + + // ① 화면 키가 따로 있는 저장소 키 — 역변환이 끼면 `cache.cache_enabled` 가 된다 + $service->setSetting('cache.enabled', true); + // ② 플러그인이 실제로 넘기는 깊은 키 — 형제 provider 매핑이 보존되어야 하는 자리 + $service->setSetting('identity.purpose_providers.signup.sms', 'nhnkcp'); + + $this->assertSame( + ['cache.enabled', 'identity.purpose_providers.signup.sms'], + $written, + '단건 저장이 받은 키를 변형했습니다 — 프로그램 호출자가 엉뚱한 키에 저장됩니다.' + ); + } +} diff --git a/tests/Unit/Services/StorageLeftoverPruneServiceTest.php b/tests/Unit/Services/StorageLeftoverPruneServiceTest.php new file mode 100644 index 00000000..9a9ab4f5 --- /dev/null +++ b/tests/Unit/Services/StorageLeftoverPruneServiceTest.php @@ -0,0 +1,255 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Services\StorageLeftoverPruneService; +use Illuminate\Support\Facades\File; +use Tests\TestCase; + +/** + * 스토리지 잔존물 정리 서비스 회귀 테스트 (`storage:prune-leftovers`) + * + * 확장/코어 업데이트·설치가 중단되며 남긴 임시 산출물(#120 동형 — 산출물 누적 + + * 회수 경로 부재)을 mtime 기준으로 회수하되, 파괴 방지 가드(정상 대기 확장 불가침 · + * 최신 백업 상시 보존 · dry-run 무삭제)를 함께 고정한다. + * + * 픽스처는 실제 저장소 경로가 아닌 격리 루트에 만든다 — 서비스 생성자의 루트 주입이 + * 그 격리 지점이다. + */ +class StorageLeftoverPruneServiceTest extends TestCase +{ + /** @var string 픽스처 격리 루트 */ + private string $fixtureRoot; + + /** @var string base_path 대응 루트 */ + private string $baseRoot; + + /** @var string storage_path 대응 루트 */ + private string $storageRoot; + + private StorageLeftoverPruneService $service; + + protected function setUp(): void + { + parent::setUp(); + + $this->fixtureRoot = storage_path('framework/testing/leftover-prune-'.uniqid()); + $this->baseRoot = $this->fixtureRoot.DIRECTORY_SEPARATOR.'base'; + $this->storageRoot = $this->fixtureRoot.DIRECTORY_SEPARATOR.'storage'; + + File::ensureDirectoryExists($this->baseRoot); + File::ensureDirectoryExists($this->storageRoot); + + $this->service = new StorageLeftoverPruneService($this->baseRoot, $this->storageRoot); + } + + protected function tearDown(): void + { + File::deleteDirectory($this->fixtureRoot); + + parent::tearDown(); + } + + /** + * 보존일이 지난 스테이징·임시·vendor 번들 잔존물만 삭제되고, 신선한 것은 남는다. + * + * @scenario leftover_target=update_staging, leftover_mode=real_delete, leftover_guard=aged_out + * + * @effects aged_leftovers_are_pruned_after_retention, fresh_leftovers_are_kept + */ + public function test_aged_leftovers_are_pruned_and_fresh_ones_are_kept(): void + { + // 업데이트 스테이징 (타임스탬프 접미사) + 교체 임시 마커 + $agedStaging = $this->makeDir('base/modules/_pending/acme-shop_20260701_010101', 5); + $agedSwap = $this->makeDir('base/modules/_pending/acme-shop_updating_0123456789abc', 5); + $freshStaging = $this->makeDir('base/plugins/_pending/acme-pay_20260817_010101', 0); + + // 코어 임시 (디렉토리 + 파일 혼재) + $agedTempDir = $this->makeDir('storage/app/temp/module_update_deadbeef00000', 5); + $agedTempFile = $this->makeFile('storage/app/temp/core_remote_changelog.md', 5); + $freshTempDir = $this->makeDir('storage/app/temp/plugin_update_cafebabe00000', 0); + + // vendor 번들 스테이징 + $agedBuild = $this->makeDir('storage/app/vendor-bundle-staging/build-0011223344556.77889900', 5); + $freshBuild = $this->makeDir('storage/app/vendor-bundle-staging/build-aabbccddeeff0.11223344', 0); + + $result = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + + $this->assertDirectoryDoesNotExist($agedStaging); + $this->assertDirectoryDoesNotExist($agedSwap); + $this->assertDirectoryExists($freshStaging, '보존일 이내 스테이징이 삭제되었다'); + + $this->assertDirectoryDoesNotExist($agedTempDir); + $this->assertFileDoesNotExist($agedTempFile); + $this->assertDirectoryExists($freshTempDir, '보존일 이내 임시 디렉토리가 삭제되었다'); + + $this->assertDirectoryDoesNotExist($agedBuild); + $this->assertDirectoryExists($freshBuild, '보존일 이내 번들 스테이징이 삭제되었다'); + + $this->assertCount(2, $result['staging']); + $this->assertCount(2, $result['temp']); + $this->assertCount(1, $result['vendor_bundle_staging']); + } + + /** + * 백업은 보존일 경과분만 삭제하되, identifier 별 최신 1개는 나이와 무관하게 보존한다. + * + * @scenario leftover_target=extension_backups, leftover_mode=real_delete, leftover_guard=latest_backup_kept + * + * @effects latest_backup_is_always_kept + */ + public function test_latest_backup_is_kept_regardless_of_age(): void + { + // 확장 백업 — 같은 identifier 의 오래된 2개: 최신(40일)은 보존, 그 이전(50일)은 삭제 + $extOldest = $this->makeDir('storage/app/extension_backups/modules/acme-shop_20260601_010101', 50); + $extNewest = $this->makeDir('storage/app/extension_backups/modules/acme-shop_20260708_010101', 40); + // 다른 identifier 의 보존일 이내 백업은 최신이 아니어도 남는다 + $extRecentA = $this->makeDir('storage/app/extension_backups/modules/acme-blog_20260810_010101', 7); + $extRecentB = $this->makeDir('storage/app/extension_backups/modules/acme-blog_20260815_010101', 2); + + // 코어 백업 — 최신 1개 보존, 그 이전 삭제 + $coreOldest = $this->makeDir('storage/app/core_backups/core_20260601_020202', 50); + $coreNewest = $this->makeDir('storage/app/core_backups/core_20260708_020202', 40); + + $result = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + + $this->assertDirectoryDoesNotExist($extOldest); + $this->assertDirectoryExists($extNewest, '나이와 무관하게 최신 백업 1개는 보존해야 한다 (롤백 능력)'); + $this->assertDirectoryExists($extRecentA, '보존일 이내 백업은 최신이 아니어도 남아야 한다'); + $this->assertDirectoryExists($extRecentB); + + $this->assertDirectoryDoesNotExist($coreOldest); + $this->assertDirectoryExists($coreNewest, '코어도 최신 백업 1개는 상시 보존해야 한다'); + + $this->assertSame([$extOldest], $result['extension_backups']); + $this->assertSame([$coreOldest], $result['core_backups']); + } + + /** + * `_pending` 직하위의 정상 대기 확장 디렉토리(타임스탬프 접미사 없음)는 불가침이다. + * + * @scenario leftover_target=update_staging, leftover_mode=real_delete, leftover_guard=normal_pending_untouched + * + * @effects normal_pending_extension_dirs_are_untouched + */ + public function test_normal_pending_extension_directories_are_untouched(): void + { + // 다운로드 대기 정상 확장 — 오래되어도 삭제 대상이 아니다 + $pendingModule = $this->makeDir('base/modules/_pending/acme-shop', 90); + $pendingTemplate = $this->makeDir('base/templates/_pending/acme-admin_basic', 90); + + $result = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + + $this->assertDirectoryExists($pendingModule, '정상 대기 확장 디렉토리가 삭제되었다'); + $this->assertDirectoryExists($pendingTemplate); + $this->assertSame([], $result['staging']); + } + + /** + * dry-run 은 아무것도 삭제하지 않고 삭제 대상 목록만 돌려준다. + * + * @scenario leftover_target=core_temp, leftover_mode=dry_run, leftover_guard=aged_out + * + * @effects dry_run_deletes_nothing + */ + public function test_dry_run_deletes_nothing_and_reports_targets(): void + { + $agedStaging = $this->makeDir('base/modules/_pending/acme-shop_20260701_010101', 5); + $agedTemp = $this->makeDir('storage/app/temp/ext_zip_0123456789abc', 5); + $legacyLog = $this->makeFile('storage/logs/browser.log', 1); + + $result = $this->service->prune(days: 3, backupDays: 30, dryRun: true); + + $this->assertDirectoryExists($agedStaging, 'dry-run 이 스테이징을 실제로 삭제했다'); + $this->assertDirectoryExists($agedTemp, 'dry-run 이 임시 디렉토리를 실제로 삭제했다'); + $this->assertFileExists($legacyLog, 'dry-run 이 레거시 로그를 실제로 삭제했다'); + + $this->assertSame([$agedStaging], $result['staging']); + $this->assertSame([$agedTemp], $result['temp']); + $this->assertSame([$legacyLog], $result['legacy_browser_log']); + } + + /** + * 정리 직후 재실행은 아무것도 삭제하지 않는다 (멱등). + * + * @scenario leftover_target=vendor_bundle_staging, leftover_mode=real_delete, leftover_guard=idempotent_second_run + * + * @effects leftover_prune_is_idempotent + */ + public function test_second_run_prunes_nothing(): void + { + $this->makeDir('base/modules/_pending/acme-shop_20260701_010101', 5); + $this->makeDir('storage/app/temp/module_update_deadbeef00000', 5); + $this->makeDir('storage/app/vendor-bundle-staging/build-0011223344556.77889900', 5); + $this->makeDir('storage/app/extension_backups/modules/acme-shop_20260601_010101', 50); + $this->makeDir('storage/app/extension_backups/modules/acme-shop_20260708_010101', 40); + $this->makeDir('storage/app/core_backups/core_20260601_020202', 50); + $this->makeDir('storage/app/core_backups/core_20260708_020202', 40); + $this->makeFile('storage/logs/browser.log', 1); + + $first = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + $second = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + + $this->assertGreaterThan(0, array_sum(array_map('count', $first)), '1회차가 아무것도 지우지 않으면 멱등 검증이 무의미하다'); + $this->assertSame(0, array_sum(array_map('count', $second)), '2회차가 무언가를 또 지웠다 — 멱등이 아니다'); + } + + /** + * 레거시 단일 browser.log 는 나이와 무관하게 삭제하되, 날짜별 로그는 건드리지 않는다. + * + * @scenario leftover_target=legacy_browser_log, leftover_mode=real_delete, leftover_guard=aged_out + * + * @effects legacy_browser_log_is_removed + */ + public function test_legacy_browser_log_is_removed_but_dated_logs_are_kept(): void + { + $legacy = $this->makeFile('storage/logs/browser.log', 0); + $dated = $this->makeFile('storage/logs/browser-2026-08-17.log', 10); + + $result = $this->service->prune(days: 3, backupDays: 30, dryRun: false); + + $this->assertFileDoesNotExist($legacy, '레거시 단일 browser.log 는 writer 가 사라졌으므로 무조건 삭제한다'); + $this->assertFileExists($dated, '날짜별 로그는 daily 드라이버가 자체 정리한다 — 대상이 아니다'); + $this->assertSame([$legacy], $result['legacy_browser_log']); + } + + /** + * 픽스처 디렉토리를 만들고 mtime 을 지정 일수만큼 과거로 되돌립니다. + * + * 내부에 파일을 하나 두어 실제 잔존물 형태(비어 있지 않은 디렉토리)를 재현하고, + * 내용 생성이 디렉토리 mtime 을 갱신하므로 touch 는 마지막에 수행한다. + * + * @param string $relative 픽스처 루트 기준 상대 경로 + * @param int $ageDays 현재로부터 며칠 전의 mtime 으로 둘지 + * @return string 절대 경로 + */ + private function makeDir(string $relative, int $ageDays): string + { + $path = $this->fixtureRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative); + File::ensureDirectoryExists($path); + file_put_contents($path.DIRECTORY_SEPARATOR.'payload.txt', 'leftover'); + + $mtime = time() - $ageDays * 86400; + touch($path.DIRECTORY_SEPARATOR.'payload.txt', $mtime); + touch($path, $mtime); + + return $path; + } + + /** + * 픽스처 파일을 만들고 mtime 을 지정 일수만큼 과거로 되돌립니다. + * + * @param string $relative 픽스처 루트 기준 상대 경로 + * @param int $ageDays 현재로부터 며칠 전의 mtime 으로 둘지 + * @return string 절대 경로 + */ + private function makeFile(string $relative, int $ageDays): string + { + $path = $this->fixtureRoot.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative); + File::ensureDirectoryExists(dirname($path)); + file_put_contents($path, 'leftover'); + touch($path, time() - $ageDays * 86400); + + return $path; + } +} diff --git a/tests/Unit/Services/TemplateLayoutAttachmentServingTest.php b/tests/Unit/Services/TemplateLayoutAttachmentServingTest.php new file mode 100644 index 00000000..b215918b --- /dev/null +++ b/tests/Unit/Services/TemplateLayoutAttachmentServingTest.php @@ -0,0 +1,124 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Contracts\Extension\StorageInterface; +use App\Contracts\Repositories\TemplateLayoutAttachmentRepositoryInterface; +use App\Contracts\Repositories\TemplateRepositoryInterface; +use App\Models\Template; +use App\Models\TemplateLayoutAttachment; +use App\Services\TemplateLayoutAttachmentService; +use Mockery; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Tests\TestCase; + +/** + * 템플릿 레이아웃 첨부 디스크 인지 서빙 테스트 (공개 #99 / 내부 #563) + * + * PublicAttachmentController 이미지 서빙과 동형 결함 — getServableFilePath 가 + * getBasePath 로 로컬 절대 경로를 조립해 S3 디스크 행에서 fileResponse(filemtime)가 + * 500 이 나던 구조의 회귀 가드. 서빙은 StorageInterface::response() 스트림으로 + * 디스크 무관하게 성립해야 한다. + */ +class TemplateLayoutAttachmentServingTest extends TestCase +{ + private TemplateLayoutAttachmentService $service; + + private $templateRepository; + + private $storage; + + protected function setUp(): void + { + parent::setUp(); + + $repository = Mockery::mock(TemplateLayoutAttachmentRepositoryInterface::class); + $this->templateRepository = Mockery::mock(TemplateRepositoryInterface::class); + $this->storage = Mockery::mock(StorageInterface::class); + + $this->service = new TemplateLayoutAttachmentService( + $repository, + $this->templateRepository, + $this->storage + ); + } + + /** + * 소속 템플릿 첨부를 만듭니다 (DB 미저장). + */ + private function makeAttachment(int $templateId, string $disk = 's3'): TemplateLayoutAttachment + { + $attachment = new TemplateLayoutAttachment([ + 'layout_name' => 'home', + 'disk' => $disk, + 'path' => 'sirsoft-basic/home/bg.png', + 'original_name' => 'bg.png', + 'mime_type' => 'image/png', + 'size' => 70, + ]); + $attachment->template_id = $templateId; + + return $attachment; + } + + /** + * s3 디스크 행이 StorageInterface::response() 스트림으로 서빙 정보를 반환해야 합니다. + * + * @scenario region_shape=aws_region,endpoint=absent,driver_usability=adapter_present,attachment_disk=follow_s3 + * + * @effects attachment_upload_follows_driver + */ + public function test_s3_disk_attachment_returns_streamed_response(): void + { + $template = new Template; + $template->id = 7; + $this->templateRepository->shouldReceive('findByIdentifier') + ->with('sirsoft-basic')->andReturn($template); + + $stream = new StreamedResponse(function () { + echo 'bytes'; + }, 200, ['Content-Type' => 'image/png']); + + $this->storage->shouldReceive('withDisk')->with('s3')->andReturnSelf(); + $this->storage->shouldReceive('response')->andReturn($stream); + + $result = $this->service->getServableResponse('sirsoft-basic', $this->makeAttachment(7)); + + $this->assertNotNull($result); + $this->assertSame($stream, $result['response']); + $this->assertNotEmpty($result['etag_source']); + } + + /** + * 경로 템플릿과 첨부 소속이 다르면 null 이어야 합니다 (교차 템플릿 접근 차단 유지). + */ + public function test_cross_template_access_returns_null(): void + { + $template = new Template; + $template->id = 7; + $this->templateRepository->shouldReceive('findByIdentifier') + ->with('sirsoft-basic')->andReturn($template); + + $result = $this->service->getServableResponse('sirsoft-basic', $this->makeAttachment(99)); + + $this->assertNull($result); + } + + /** + * 스토리지에 파일이 없으면(response null) null 이어야 합니다. + */ + public function test_missing_file_returns_null(): void + { + $template = new Template; + $template->id = 7; + $this->templateRepository->shouldReceive('findByIdentifier') + ->with('sirsoft-basic')->andReturn($template); + + $this->storage->shouldReceive('withDisk')->with('s3')->andReturnSelf(); + $this->storage->shouldReceive('response')->andReturnNull(); + + $result = $this->service->getServableResponse('sirsoft-basic', $this->makeAttachment(7)); + + $this->assertNull($result); + } +} diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php index 1fb9a70c..07d941f9 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -5,6 +5,7 @@ namespace Tests\Unit\Services; use App\Enums\ExtensionOwnerType; use App\Enums\PermissionType; use App\Exceptions\CannotDeleteSuperAdminException; +use App\Exceptions\PermissionEscalationException; use App\Models\Permission; use App\Models\Role; use App\Models\User; @@ -506,15 +507,20 @@ class UserServiceTest extends TestCase } /** - * core.permissions.update 권한 없으면 타인의 역할 변경 불가 (무시됨) + * 역할 부여는 core.permissions.update(역할 정의 수정)를 요구하지 않는다 — 상한(ceiling) + * 이내라면 그 권한이 없어도 역할을 부여할 수 있다. (역할 부여 = 사용자 관리의 일부) + * + * 회귀 방지: 과거에는 core.permissions.update 가 없으면 role_ids 를 조용히 버려(silent + * no-op) 200 성공을 반환하면서도 역할이 바뀌지 않았다. 이제는 상한만 통과하면 부여된다. */ - public function test_update_user_ignores_role_ids_without_permission(): void + public function test_update_user_assigns_role_within_ceiling_without_permissions_update(): void { $originalRole = Role::create([ 'identifier' => 'original-role', 'name' => ['ko' => '원래 역할', 'en' => 'Original Role'], ]); + // 권한이 전혀 없는 역할 → 어떤 액터의 상한도 넘지 않는다. $newRole = Role::create([ 'identifier' => 'new-role-test', 'name' => ['ko' => '새 역할', 'en' => 'New Role'], @@ -523,20 +529,56 @@ class UserServiceTest extends TestCase $targetUser = User::factory()->create(); $targetUser->roles()->attach($originalRole->id); - // 권한 없는 사용자로 인증 + // core.permissions.update 가 없는 액터로 인증 (사용자 관리 권한은 라우트 단계에서 + // 강제되며, 서비스 계층 테스트에서는 상한만 검사된다) $authUser = User::factory()->create(); Auth::login($authUser); - // 타인의 역할 변경 시도 $updatedUser = $this->userService->updateUser($targetUser, [ 'name' => 'Changed Name', 'role_ids' => [$newRole->id], ]); - // 이름은 변경되지만 역할은 변경되지 않아야 함 + // 이름과 역할 모두 변경되어야 한다 (silent no-op 아님) $this->assertEquals('Changed Name', $updatedUser->name); - $this->assertTrue($updatedUser->roles->contains('id', $originalRole->id)); - $this->assertFalse($updatedUser->roles->contains('id', $newRole->id)); + $this->assertTrue($updatedUser->roles->contains('id', $newRole->id)); + $this->assertFalse($updatedUser->roles->contains('id', $originalRole->id)); + } + + /** + * 상한을 넘는 역할(액터가 보유하지 않은 권한을 담은 역할)은 부여 시 명시적으로 거부된다. + * + * 회귀 방지: 조용히 무시(silent no-op)하는 대신 PermissionEscalationException 을 던져 + * 컨트롤러가 403 으로 매핑하도록 한다. + */ + public function test_update_user_rejects_role_beyond_actor_ceiling(): void + { + $elevatedPermission = Permission::create([ + 'identifier' => 'core.templates.uninstall', + 'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'], + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => PermissionType::Admin, + ]); + + // 액터가 보유하지 않은 권한을 담은 역할 → 상한 초과 + $elevatedRole = Role::create([ + 'identifier' => 'elevated-role', + 'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'], + ]); + $elevatedRole->permissions()->attach($elevatedPermission->id); + + $targetUser = User::factory()->create(); + + // 그 권한을 보유하지 않은 액터 + $authUser = User::factory()->create(); + Auth::login($authUser); + + $this->expectException(PermissionEscalationException::class); + + $this->userService->updateUser($targetUser, [ + 'role_ids' => [$elevatedRole->id], + ]); } /** @@ -591,11 +633,13 @@ class UserServiceTest extends TestCase // ======================================================================== /** - * core.permissions.update 권한 없으면 기본 역할(user) 자동 할당 + * 역할을 지정하지 않고 생성하면 기본 역할('user')이 자동 배정된다. + * + * 회귀 방지: 기본 역할 배정은 권한 게이트가 아니라 "요청 역할 없음" 조건에 묶인다. + * (과거에는 core.permissions.update 미보유 조건에 묶여 있어 지정 역할을 덮어썼다) */ - public function test_create_user_assigns_default_role_without_permission(): void + public function test_create_user_assigns_default_role_when_none_requested(): void { - // 기본 user 역할 생성 $userRole = Role::create([ 'identifier' => 'user', 'name' => ['ko' => '사용자', 'en' => 'User'], @@ -604,12 +648,41 @@ class UserServiceTest extends TestCase 'is_active' => true, ]); + $authUser = User::factory()->create(); + Auth::login($authUser); + + $user = $this->userService->createUser([ + 'name' => 'Test User', + 'email' => 'testcreate@test.com', + 'password' => 'password123', + ]); + + $this->assertTrue($user->roles->contains('id', $userRole->id)); + } + + /** + * core.permissions.update 가 없어도 상한 이내의 지정 역할은 그대로 부여된다 + * (기본 역할로 덮어쓰지 않는다). + * + * 회귀 방지: 과거에는 core.permissions.update 미보유 시 지정 역할을 버리고 기본 역할을 + * 강제 배정했다. 이제는 상한만 통과하면 지정 역할이 부여된다. + */ + public function test_create_user_assigns_requested_role_within_ceiling_without_permissions_update(): void + { + $userRole = Role::create([ + 'identifier' => 'user', + 'name' => ['ko' => '사용자', 'en' => 'User'], + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'is_active' => true, + ]); + + // 권한이 없는 역할 → 상한 이내 $customRole = Role::create([ 'identifier' => 'custom-role', 'name' => ['ko' => '커스텀 역할', 'en' => 'Custom Role'], ]); - // 권한 없는 사용자로 인증 $authUser = User::factory()->create(); Auth::login($authUser); @@ -620,9 +693,41 @@ class UserServiceTest extends TestCase 'role_ids' => [$customRole->id], ]); - // 커스텀 역할 대신 기본 역할이 할당되어야 함 - $this->assertTrue($user->roles->contains('id', $userRole->id)); - $this->assertFalse($user->roles->contains('id', $customRole->id)); + // 지정 역할이 부여되고 기본 역할로 덮어쓰이지 않는다 + $this->assertTrue($user->roles->contains('id', $customRole->id)); + $this->assertFalse($user->roles->contains('id', $userRole->id)); + } + + /** + * 상한을 넘는 역할을 지정해 생성하면 명시적으로 거부된다. + */ + public function test_create_user_rejects_role_beyond_actor_ceiling(): void + { + $elevatedPermission = Permission::create([ + 'identifier' => 'core.templates.uninstall', + 'name' => ['ko' => '템플릿 삭제', 'en' => 'Uninstall Templates'], + 'extension_type' => ExtensionOwnerType::Core, + 'extension_identifier' => 'core', + 'type' => PermissionType::Admin, + ]); + + $elevatedRole = Role::create([ + 'identifier' => 'elevated-create-role', + 'name' => ['ko' => '상위 역할', 'en' => 'Elevated Role'], + ]); + $elevatedRole->permissions()->attach($elevatedPermission->id); + + $authUser = User::factory()->create(); + Auth::login($authUser); + + $this->expectException(PermissionEscalationException::class); + + $this->userService->createUser([ + 'name' => 'New User', + 'email' => 'newuser@test.com', + 'password' => 'password123', + 'role_ids' => [$elevatedRole->id], + ]); } /** diff --git a/tests/Unit/Services/UserServiceWithdrawTest.php b/tests/Unit/Services/UserServiceWithdrawTest.php new file mode 100644 index 00000000..72c09dd5 --- /dev/null +++ b/tests/Unit/Services/UserServiceWithdrawTest.php @@ -0,0 +1,143 @@ +<?php + +namespace Tests\Unit\Services; + +use App\Enums\UserStatus; +use App\Models\User; +use Illuminate\Database\Schema\Builder as SchemaBuilder; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; +use Tests\TestCase; + +/** + * User::withdraw() 익명화 규칙 단위 테스트 (공개이슈 #112) + * + * 서비스 계층의 원자성은 `tests/Feature/Api/Auth/WithdrawAtomicityTest.php` 가 다루고, + * 여기서는 모델이 만드는 **익명화 값 자체**의 규칙을 고정한다 — + * 접미사 형식·유일성·멱등·컬럼 길이 클램프. + * + * @scenario target_account=normal, target_account=already_withdrawn, field_length=exceeds_limit + * + * @effects suffix_includes_user_id_for_structural_uniqueness, long_values_truncated_within_column_limits, withdraw_is_idempotent + */ +class UserServiceWithdrawTest extends TestCase +{ + use RefreshDatabase; + + /** + * 이메일 접미사는 날짜 + 사용자 ID 로 구성된다. + */ + public function test_email_suffix_contains_date_and_user_id(): void + { + Carbon::setTestNow(Carbon::parse('2026-08-13 09:00:00')); + + $user = User::factory()->create(['email' => 'suffix@example.com']); + + $this->assertTrue($user->withdraw()); + + $this->assertSame('suffix@example.com_deleted_20260813_'.$user->id, $user->email); + } + + /** + * 같은 날 같은 원본 이메일이어도 사용자마다 결과가 다르다 (구조적 유일). + */ + public function test_same_original_email_yields_distinct_anonymized_values(): void + { + Carbon::setTestNow(Carbon::parse('2026-08-13 09:00:00')); + + $first = User::factory()->create(['email' => 'dup@example.com']); + $first->withdraw(); + + $second = User::factory()->create(['email' => 'dup@example.com']); + $second->withdraw(); + + $this->assertNotSame($first->email, $second->email); + } + + /** + * 이름·닉네임 접미사 규칙. + */ + public function test_name_and_nickname_suffixes(): void + { + Carbon::setTestNow(Carbon::parse('2026-08-13 09:00:00')); + + $user = User::factory()->create(['name' => '홍길동', 'nickname' => '길동이']); + + $user->withdraw(); + + $this->assertSame('홍길동_탈퇴_20260813', $user->name); + $this->assertSame('길동이_탈퇴', $user->nickname); + } + + /** + * 닉네임이 없으면 접미사를 붙이지 않는다. + */ + public function test_null_nickname_is_left_untouched(): void + { + $user = User::factory()->create(['nickname' => null]); + + $user->withdraw(); + + $this->assertNull($user->nickname); + } + + /** + * 이미 탈퇴한 계정은 값이 변하지 않는다 (멱등). + */ + public function test_withdraw_is_idempotent(): void + { + $user = User::factory()->create(['email' => 'idem@example.com', 'nickname' => '닉']); + + $user->withdraw(); + $snapshot = [$user->email, $user->name, $user->nickname, (string) $user->withdrawn_at]; + + $this->assertTrue($user->withdraw()); + + $this->assertSame($snapshot, [$user->email, $user->name, $user->nickname, (string) $user->withdrawn_at]); + } + + /** + * 컬럼 상한을 넘기지 않도록 원값 앞부분을 잘라 붙인다. + */ + public function test_values_are_clamped_to_column_limits(): void + { + $limit = SchemaBuilder::$defaultStringLength ?? 255; + + $user = User::factory()->create([ + 'email' => str_repeat('a', $limit - 12).'@example.com', + 'name' => str_repeat('나', $limit), + 'nickname' => str_repeat('다', User::NICKNAME_MAX_LENGTH), + ]); + + $user->withdraw(); + + $this->assertSame($limit, mb_strlen($user->email)); + $this->assertSame($limit, mb_strlen($user->name)); + $this->assertSame(User::NICKNAME_MAX_LENGTH, mb_strlen($user->nickname)); + + // 잘렸어도 접미사는 온전히 남는다 (식별 규칙이 깨지면 안 된다) + $this->assertStringEndsWith('_deleted_'.now()->format('Ymd').'_'.$user->id, $user->email); + $this->assertStringEndsWith('_탈퇴', $user->nickname); + } + + /** + * 상태·탈퇴 일시가 함께 기록된다. + */ + public function test_status_and_withdrawn_at_are_set(): void + { + $user = User::factory()->create(); + + $user->withdraw(); + + $this->assertSame(UserStatus::Withdrawn->value, $user->status); + $this->assertNotNull($user->withdrawn_at); + $this->assertTrue($user->isWithdrawn()); + } + + protected function tearDown(): void + { + Carbon::setTestNow(); + + parent::tearDown(); + } +} diff --git a/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php index 98c6c34a..c4132299 100644 --- a/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php +++ b/tests/Unit/Support/ApiDoc/ApiDocPipelineTest.php @@ -2049,4 +2049,98 @@ MD; $cells = preg_split('/(?<!\\\\)\|/', trim($line, '| ')); $this->assertCount(3, $cells, "403 행은 3열(상태코드/의미/발생 조건)이어야 한다: {$line}"); } + + /** + * 403 행의 권한 식별자는 사람 서술이 아니라 라우트에서 파생된 사실이므로, 라우트의 요구 + * 권한이 바뀌면 재생성이 그 식별자를 갱신해야 한다. + * + * 배경: 에러 표 병합은 같은 상태코드에서 기존 행을 이기게 둔다(409·429 같은 도메인 특이 + * 조건을 사람이 보강해 두기 때문). 그 규칙이 403 에도 그대로 걸리면 라우트 권한을 바꿔도 + * 옛 식별자가 영구히 남아, 문서가 조용히 틀린 권한을 안내한다. + */ + #[Test] + public function 라우트_권한이_바뀌면_403_행의_권한_식별자가_갱신된다(): void + { + $scaffolder = new ApiDocScaffolder; + + $makeRoute = fn (string $permission): array => [ + 'method' => 'GET', + 'uri' => '/api/admin/widgets', + 'name' => 'api.admin.widgets.index', + 'controller' => 'WidgetController', + 'controller_method' => 'index', + 'permission' => $permission, + 'middleware' => ['api', 'auth:sanctum', 'permission:admin,'.$permission], + 'path_params' => [], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'collection', 'fields' => [], 'pagination' => false]; + $probe = ['status' => 200, 'skipped_reason' => null]; + $header = "# W\n"; + $keys = ['api.admin.widgets.index']; + + $before = $scaffolder->endpointSection($makeRoute('core.widgets.read'), $request, $schema, $probe); + $doc = $scaffolder->mergeDocument(null, $header, [$before], $keys); + $this->assertStringContainsString('요구 권한(`core.widgets.read`)이 없는 경우', $doc); + + // 라우트의 요구 권한이 바뀐 뒤 재생성한다. + $after = $scaffolder->endpointSection($makeRoute('core.widgets.manage'), $request, $schema, $probe); + $regenerated = $scaffolder->mergeDocument($doc, $header, [$after], $keys); + + $this->assertStringContainsString( + '요구 권한(`core.widgets.manage`)이 없는 경우', + $regenerated, + '라우트 권한이 바뀌면 403 행의 식별자가 갱신되어야 한다' + ); + $this->assertStringNotContainsString( + 'core.widgets.read', + $regenerated, + '낡은 권한 식별자가 남으면 문서가 틀린 권한을 안내한다' + ); + } + + /** + * 식별자 갱신이 사람이 보강한 문구까지 덮어써서는 안 된다 — 갱신 대상은 백틱 식별자뿐이다. + */ + #[Test] + public function forbidden_행_식별자_갱신은_사람이_보강한_조건_문구를_보존한다(): void + { + $scaffolder = new ApiDocScaffolder; + + $makeRoute = fn (string $permission): array => [ + 'method' => 'DELETE', + 'uri' => '/api/admin/widgets/{widget}', + 'name' => 'api.admin.widgets.destroy', + 'controller' => 'WidgetController', + 'controller_method' => 'destroy', + 'permission' => $permission, + 'middleware' => ['api', 'auth:sanctum', 'permission:admin,'.$permission], + 'path_params' => ['widget'], + ]; + $request = ['request_class' => null, 'params' => [], 'hook_filters' => []]; + $schema = ['envelope' => ['data'], 'shape' => 'object', 'fields' => [], 'pagination' => false]; + $probe = ['status' => 200, 'skipped_reason' => null]; + $header = "# W\n"; + $keys = ['api.admin.widgets.destroy']; + + $before = $scaffolder->endpointSection($makeRoute('core.widgets.delete'), $request, $schema, $probe); + $doc = $scaffolder->mergeDocument(null, $header, [$before], $keys); + + // 사람이 403 행에 도메인 조건을 덧붙인다. + $withHuman = str_replace( + '| 403 | Forbidden | 요구 권한(`core.widgets.delete`)이 없는 경우 |', + '| 403 | Forbidden | 요구 권한(`core.widgets.delete`)이 없거나, 시스템 위젯을 삭제하려는 경우 |', + $doc + ); + $this->assertStringContainsString('시스템 위젯을 삭제하려는 경우', $withHuman); + + $after = $scaffolder->endpointSection($makeRoute('core.widgets.manage'), $request, $schema, $probe); + $regenerated = $scaffolder->mergeDocument($withHuman, $header, [$after], $keys); + + $this->assertStringContainsString( + '요구 권한(`core.widgets.manage`)이 없거나, 시스템 위젯을 삭제하려는 경우', + $regenerated, + '식별자만 갱신되고 사람이 보강한 조건은 그대로 남아야 한다' + ); + } } diff --git a/tests/Unit/Support/ScheduleCommandValidatorTest.php b/tests/Unit/Support/ScheduleCommandValidatorTest.php index 94a8d2f8..defbdb08 100644 --- a/tests/Unit/Support/ScheduleCommandValidatorTest.php +++ b/tests/Unit/Support/ScheduleCommandValidatorTest.php @@ -139,6 +139,265 @@ class ScheduleCommandValidatorTest extends TestCase $this->assertNull(ScheduleCommandValidator::tokenizeShellCommand(' ')); } + // ======================================================================== + // Shell — 인터프리터 정책 (KVE-2026-1653) + // ======================================================================== + + /** + * 인터프리터에 인라인 코드/명령을 넘기는 형태는 화이트리스트에 등재돼 있어도 차단한다. + * + * KVE-2026-1653 본체: 운영자가 `bash` 를 화이트리스트에 등록하면 `bash -c id` 가 + * 통과해 임의 OS 명령이 실행됐다. 스크립트 자리(첫 인자)가 하이픈이면 인라인 코드 플래그다. + * + * @param string $command 인라인 코드/명령을 넘기는 command + * + * @scenario command_class=inline_code, enforcement_point=validator_unit + * + * @effects inline_code_flags_rejected + */ + #[Test] + #[DataProvider('inlineCodeShellCommandProvider')] + public function it_blocks_inline_code_through_interpreters_even_when_allowlisted(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "인라인 코드가 통과함: {$command}"); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, $verdict['reason']); + } + + /** + * 변형 표기(Windows `.exe` / 버전 접미사)로 등재된 인터프리터도 분류를 비켜가지 못한다. + * + * 분류 목록(`script_interpreters`/`reject_binaries`)은 기본 이름만 담으므로, + * 정확 일치 분류 시절에는 운영자가 `python.exe`·`python3.12` 로 등재하면 + * "일반 스크립트 — 통과" 분기를 타 인라인 코드 차단이 통째로 무력화됐다. + * + * @param string $command 변형 표기 인터프리터로 인라인 코드를 넘기는 command + * @param string $binary 화이트리스트에 등재한 변형 표기 + * @param string $expectedReason 기대 거부 사유 + * + * @scenario command_class=inline_code, enforcement_point=validator_unit + * + * @effects inline_code_flags_rejected + */ + #[Test] + #[DataProvider('variantInterpreterShellCommandProvider')] + public function it_classifies_variant_interpreter_names_before_gating(string $command, string $binary, string $expectedReason): void + { + $this->enableShell([$binary]); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "변형 표기 인터프리터가 통과함: {$command}"); + $this->assertSame($expectedReason, $verdict['reason']); + } + + /** + * 변형 표기로 등재된 인터프리터의 절대경로 스크립트 실행은 그대로 통과한다 (과차단 회귀 가드). + * + * @scenario command_class=script_ok, enforcement_point=validator_unit + * + * @effects interpreter_script_files_still_run + */ + #[Test] + public function it_allows_variant_named_interpreters_running_absolute_scripts(): void + { + $this->enableShell(['python.exe']); + + $verdict = ScheduleCommandValidator::inspectShellCommand('python.exe /opt/scripts/job.py'); + + $this->assertTrue($verdict['allowed'], '변형 표기 인터프리터의 정상 스크립트 실행이 차단됨'); + $this->assertNull($verdict['reason']); + } + + /** + * 인터프리터 + 절대경로 스크립트 파일은 통과한다 (범용 크론 기능 회귀 가드). + * + * @param string $command 스크립트 파일을 실행하는 command + * + * @scenario command_class=script_ok, enforcement_point=validator_unit + * + * @effects interpreter_script_files_still_run + */ + #[Test] + #[DataProvider('interpreterScriptShellCommandProvider')] + public function it_allows_interpreters_running_absolute_path_scripts(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertTrue($verdict['allowed'], "정상 스크립트 실행이 차단됨: {$command}"); + $this->assertNull($verdict['reason']); + } + + /** + * 인터프리터 뒤 스크립트 자리가 artisan 이면 차단한다 (Artisan 축 우회 방지). + * + * @param string $command artisan 을 스크립트로 넘기는 command + * + * @scenario command_class=artisan_bypass, enforcement_point=validator_unit + * + * @effects artisan_via_shell_rejected + */ + #[Test] + #[DataProvider('artisanBypassShellCommandProvider')] + public function it_blocks_artisan_as_the_interpreter_script(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "artisan 우회가 통과함: {$command}"); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_INTERPRETER, $verdict['reason']); + } + + /** + * 완전 거부형 실행기는 화이트리스트에 등재돼 있어도 차단한다. + * + * env/awk/make/xargs/sudo/busybox 등은 첫 인자가 코드·감싼 명령이라 "스크립트 파일" + * 모델이 성립하지 않는다. + * + * @param string $command 완전 거부형 command + * + * @scenario command_class=reject_binary, enforcement_point=validator_unit + * + * @effects reject_binaries_blocked_even_when_allowlisted + */ + #[Test] + #[DataProvider('rejectBinaryShellCommandProvider')] + public function it_blocks_reject_binaries_even_when_allowlisted(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "완전 거부형이 통과함: {$command}"); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_INTERPRETER, $verdict['reason']); + } + + /** + * 인터프리터 뒤 스크립트 경로 형태 위반(상대경로 / `..` / `#`)은 차단한다. + * + * @param string $command 경로 형태 위반 command + * + * @scenario command_class=script_path, enforcement_point=validator_unit + * + * @effects script_path_traversal_rejected + */ + #[Test] + #[DataProvider('scriptPathShellCommandProvider')] + public function it_blocks_unsafe_interpreter_script_paths(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "안전하지 않은 경로가 통과함: {$command}"); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_SCRIPT_PATH, $verdict['reason']); + } + + /** + * 스크립트 없이 인터프리터만 등록하면(REPL/셸 기동) 차단한다. + * + * @param string $command 인자 없는 인터프리터 command + * + * @scenario command_class=no_script, enforcement_point=validator_unit + * + * @effects interpreter_without_script_rejected + */ + #[Test] + #[DataProvider('noScriptShellCommandProvider')] + public function it_blocks_interpreters_without_a_script(string $command): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertFalse($verdict['allowed'], "인자 없는 인터프리터가 통과함: {$command}"); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_INTERPRETER, $verdict['reason']); + } + + /** + * 인터프리터가 아닌 일반 스크립트는 종전처럼 통과한다 (기존 계약 보존 회귀 가드). + * + * @param string $command 일반 스크립트 command + * + * @scenario command_class=legit_plain_script, enforcement_point=validator_unit + * + * @effects plain_scripts_still_run_without_path_restrictions + */ + #[Test] + #[DataProvider('plainScriptShellCommandProvider')] + public function it_still_allows_plain_non_interpreter_scripts(string $command): void + { + $this->enableShell(['backup.sh']); + + $verdict = ScheduleCommandValidator::inspectShellCommand($command); + + $this->assertTrue($verdict['allowed'], "일반 스크립트가 차단됨: {$command}"); + $this->assertNull($verdict['reason']); + } + + /** + * 게이트가 꺼져 있으면 disabled 사유를 돌려준다. + * + * @scenario command_class=inline_code, enforcement_point=validator_unit + * + * @effects rejection_reason_is_reported_per_category + */ + #[Test] + public function it_reports_disabled_when_the_shell_gate_is_off(): void + { + config([ + 'schedule_security.shell.enabled' => false, + 'schedule_security.shell.allowed_binaries' => ['bash'], + ]); + + $verdict = ScheduleCommandValidator::inspectShellCommand('bash /app/x.sh'); + + $this->assertFalse($verdict['allowed']); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_DISABLED, $verdict['reason']); + } + + /** + * 화이트리스트 밖 명령은 not_allowlisted 사유를 돌려준다. + * + * @scenario command_class=inline_code, enforcement_point=validator_unit + * + * @effects rejection_reason_is_reported_per_category + */ + #[Test] + public function it_reports_not_allowlisted_for_unregistered_binaries(): void + { + $this->enableShell(['backup.sh']); + + $verdict = ScheduleCommandValidator::inspectShellCommand('bash -c id'); + + $this->assertFalse($verdict['allowed']); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_NOT_ALLOWED, $verdict['reason']); + } + + /** + * 메타문자가 섞인 명령은 metacharacter 사유를 돌려준다. + * + * @scenario command_class=reject_binary, enforcement_point=validator_unit + * + * @effects rejection_reason_is_reported_per_category + */ + #[Test] + public function it_reports_metacharacter_for_commands_with_shell_metacharacters(): void + { + $this->enableShell(self::interpreterBinaries()); + + $verdict = ScheduleCommandValidator::inspectShellCommand('awk \'BEGIN{system("id")}\''); + + $this->assertFalse($verdict['allowed']); + $this->assertSame(ScheduleCommandValidator::SHELL_REASON_METACHARACTER, $verdict['reason']); + } + // ======================================================================== // Artisan — 차단목록 // ======================================================================== @@ -416,6 +675,40 @@ class ScheduleCommandValidatorTest extends TestCase $this->assertSame(ScheduleCommandValidator::ARTISAN_REASON_OPTION, $verdict['reason']); } + /** + * Artisan 에 커맨드가 등록되지 않은 문맥(HTTP 요청)에서도, 활성 프로바이더가 + * `$commands` 로 선언한 확장 소유 명령은 자동 허용된다. + * + * 실제 확장 프로바이더는 `runningInConsole()` 일 때만 커맨드를 등록하므로, + * 관리자 화면의 스케줄 저장 검증(HTTP)에서는 `Artisan::all()` 에 확장 커맨드가 + * 없다. 등록 인스턴스가 없으면 프로바이더 선언(클래스 네임스페이스 + 실제 명령명 + * 대조)을 폴백으로 해석해야 한다 — 없으면 "설치된 확장 명령 자동 허용" 약속이 + * 웹 저장 경로에서 통째로 동작하지 않는다 (7.0.7 검수 발견). + * + * @scenario command_class=extension_owned, enforcement_point=validator_unit + * + * @effects extension_owned_commands_resolved_without_console_registration + */ + #[Test] + public function it_allows_provider_declared_extension_commands_without_console_registration(): void + { + require_once __DIR__.'/../../Fixtures/Extension/FakeExtensionScheduleCommand.php'; + require_once __DIR__.'/../../Fixtures/Extension/FakeExtensionScheduleServiceProvider.php'; + + // Artisan::registerCommand 를 호출하지 않는다 — HTTP 문맥 재현. + $this->app->register(\Modules\G7Testing\Providers\FakeExtensionScheduleServiceProvider::class); + + $verdict = ScheduleCommandValidator::inspectArtisanCommand('g7-testing-fake-extension-command --scope=all'); + + $this->assertTrue($verdict['allowed'], '프로바이더 선언 확장 명령이 HTTP 문맥에서 거부됨'); + $this->assertSame('g7-testing-fake-extension-command', $verdict['name']); + + // 자기 정의에 없는 옵션은 같은 폴백 경로에서도 거부되어야 한다. + $optionVerdict = ScheduleCommandValidator::inspectArtisanCommand('g7-testing-fake-extension-command --g7-not-a-real-option'); + $this->assertFalse($optionVerdict['allowed']); + $this->assertSame(ScheduleCommandValidator::ARTISAN_REASON_OPTION, $optionVerdict['reason']); + } + /** * 확장 네임스페이스를 비워도 코어 허용목록은 그대로 동작한다 (계층 독립성). * @@ -677,4 +970,163 @@ class ScheduleCommandValidatorTest extends TestCase '옵션 구분자 단독' => ['cache:clear --', ScheduleCommandValidator::ARTISAN_REASON_MALFORMED], ]; } + + // ======================================================================== + // Data Providers — Shell 인터프리터 정책 (KVE-2026-1653) + // ======================================================================== + + /** + * 인터프리터 정책 테스트에서 화이트리스트에 등재하는 실행 파일 목록. + * + * 인터프리터·완전거부형을 **등재한 상태**로 판정해 "등재돼도 차단됨" 을 증명한다. + * + * @return array<int, string> + */ + private static function interpreterBinaries(): array + { + return [ + 'bash', 'sh', 'python', 'python3', 'php', 'node', 'perl', 'ruby', + 'env', 'awk', 'make', 'xargs', 'sudo', 'busybox', 'sed', + 'backup.sh', + ]; + } + + /** + * 인터프리터에 인라인 코드/명령을 넘기는 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + /** + * 변형 표기 인터프리터로 인라인 코드/감싼 명령을 넘기는 command 목록 (전부 차단). + * + * @return array<string, array{0: string, 1: string, 2: string}> + */ + public static function variantInterpreterShellCommandProvider(): array + { + return [ + 'python.exe -c 코드 (Windows 확장자)' => [ + 'python.exe -c import_os', 'python.exe', ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, + ], + 'python3.12 -c 코드 (버전 접미사)' => [ + 'python3.12 -c import_os', 'python3.12', ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, + ], + 'php8.2 -r 코드 (버전 접미사)' => [ + 'php8.2 -r phpinfo', 'php8.2', ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, + ], + 'powershell.exe -Command (Windows 확장자)' => [ + 'powershell.exe -Command Get-Process', 'powershell.exe', ScheduleCommandValidator::SHELL_REASON_INLINE_CODE, + ], + 'sudo.exe 감싼 명령 (거부형 + 확장자)' => [ + 'sudo.exe /usr/bin/id', 'sudo.exe', ScheduleCommandValidator::SHELL_REASON_INTERPRETER, + ], + ]; + } + + public static function inlineCodeShellCommandProvider(): array + { + return [ + 'bash -c id (본체)' => ['bash -c id'], + 'sh -c id' => ['sh -c id'], + 'python -c 코드' => ['python -c import_os'], + 'python3 -c 코드' => ['python3 -c import_os'], + 'php -r 코드' => ['php -r phpinfo'], + 'node -e 코드' => ['node -e process'], + 'perl -e 코드' => ['perl -e system'], + 'ruby -e 코드' => ['ruby -e exec'], + 'bash -x 스크립트 앞 옵션' => ['bash -x /app/x.sh'], + 'python -m 모듈' => ['python -m http'], + ]; + } + + /** + * 인터프리터 + 절대경로 스크립트 (전부 허용 — 범용 크론 회귀 가드). + * + * @return array<string, array{string}> + */ + public static function interpreterScriptShellCommandProvider(): array + { + return [ + 'python 스크립트' => ['python /app/job.py'], + 'python3 스크립트' => ['python3 /app/job.py'], + 'node 스크립트' => ['node /app/worker.js'], + 'php 스크립트' => ['php /app/legacy.php'], + 'bash 스크립트' => ['bash /app/deploy.sh'], + 'ruby 스크립트' => ['ruby /app/task.rb'], + 'perl 스크립트' => ['perl /app/task.pl'], + '스크립트 + 인자' => ['python /app/job.py --daily --verbose'], + ]; + } + + /** + * 인터프리터 뒤 스크립트 자리가 artisan 인 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + public static function artisanBypassShellCommandProvider(): array + { + return [ + 'php artisan tinker' => ['php artisan'], + 'php 절대경로 artisan' => ['php /var/www/html/artisan'], + ]; + } + + /** + * 완전 거부형 실행기 command 목록 (전부 차단, 메타문자 없음). + * + * @return array<string, array{string}> + */ + public static function rejectBinaryShellCommandProvider(): array + { + return [ + 'env bash' => ['env bash /app/x.sh'], + 'make -f' => ['make -f /tmp/Makefile'], + 'xargs' => ['xargs id'], + 'sudo' => ['sudo id'], + 'busybox' => ['busybox sh'], + 'sed -e' => ['sed -e s/a/b/ /tmp/input.txt'], + ]; + } + + /** + * 인터프리터 뒤 스크립트 경로 형태 위반 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + public static function scriptPathShellCommandProvider(): array + { + return [ + '상대경로 (python)' => ['python job.py'], + '상대경로 (php ./)' => ['php ./x.php'], + '트래버설' => ['php /app/../../tmp/x.php'], + '해시 위장' => ['php /app/x.php#foo'], + ]; + } + + /** + * 스크립트 없이 인터프리터만 있는 command 목록 (전부 차단). + * + * @return array<string, array{string}> + */ + public static function noScriptShellCommandProvider(): array + { + return [ + 'bash 단독' => ['bash'], + 'python 단독' => ['python'], + ]; + } + + /** + * 인터프리터가 아닌 일반 스크립트 command 목록 (전부 허용 — 기존 계약 보존). + * + * @return array<string, array{string}> + */ + public static function plainScriptShellCommandProvider(): array + { + return [ + '실행 파일 단독' => ['backup.sh'], + '인자 포함' => ['backup.sh --full /var/data'], + '절대경로 인자' => ['/usr/local/bin/backup.sh --full'], + '하이픈 인자 (일반 스크립트는 제한 없음)' => ['backup.sh -C /var/data'], + ]; + } } diff --git a/tests/Unit/Support/TrustedScriptHostsTest.php b/tests/Unit/Support/TrustedScriptHostsTest.php new file mode 100644 index 00000000..141b9c80 --- /dev/null +++ b/tests/Unit/Support/TrustedScriptHostsTest.php @@ -0,0 +1,397 @@ +<?php + +namespace Tests\Unit\Support; + +use App\Extension\AbstractModule; +use App\Extension\AbstractPlugin; +use App\Extension\HookManager; +use App\Extension\ModuleManager; +use App\Extension\PluginManager; +use App\Extension\TemplateManager; +use App\Support\TrustedScriptHosts; +use PHPUnit\Framework\Attributes\DataProvider; +use Tests\TestCase; + +/** + * TrustedScriptHosts 집계/판정 테스트 (KVE-2026-1915 신뢰 출처 허용목록) + * + * 확장이 manifest 로 선언한 외부 스크립트 호스트를 집계하고, URL 의 호스트가 그 목록에 + * 속하는지 판정하는 로직을 고정합니다. 런타임·저장측·audit 이 모두 이 SSoT 를 참조하므로 + * 호스트 추출/정규화가 틀리면 세 계층이 함께 어긋납니다. + * + * 선언 주체는 모듈·플러그인·템플릿 셋이고 집계 경로가 서로 다릅니다 — 모듈/플러그인은 + * PHP 추상 클래스의 getTrustedScriptHosts(), 템플릿은 manifest 배열 직접 읽기. 한 주체만 + * 검사하면 나머지 주체의 집계 누락이 드러나지 않습니다(템플릿 축이 그렇게 빠져 있던 것이 + * B-2b 결함이었습니다). + * + * 이 관심사는 시나리오 매니페스트(tests/scenarios/trusted-script-hosts.yaml)의 + * sub_flow `trusted_host_declaring_subject` 로 등록되어 있습니다. 브라우저 관찰 축(case)과 + * 교차하지 않는 별개 관심사라 축으로 올리면 어떤 조합도 관측 지점을 갖지 못합니다 — + * 그래서 축이 아니라 sub_flow 이고, 커버는 아래 메서드들의 효과 마커가 담당합니다. + * + * 주의: 산문에 마커 문법을 그대로 인용하면 시나리오 마커 파서가 + * 그 문장을 마커로 읽어 실재하지 않는 조합을 만듭니다. 마커 이름은 평문으로만 언급합니다. + * + * 효과 요약(마커 아님 — 평문): every_extension_type_declaration_is_collected, + * malformed_or_absent_declaration_is_ignored. 실제 마커는 아래 메서드에만 둡니다. + */ +class TrustedScriptHostsTest extends TestCase +{ + protected function tearDown(): void + { + HookManager::clearFilter(TrustedScriptHosts::FILTER_HOOK); + parent::tearDown(); + } + + // ── hostOf: URL → 호스트 추출 ────────────────────────── + + public function test_host_of_extracts_scheme_url_host(): void + { + $this->assertSame('cdn.ckeditor.com', TrustedScriptHosts::hostOf('https://cdn.ckeditor.com/ckeditor5/x.js')); + } + + public function test_host_of_extracts_protocol_relative_host(): void + { + $this->assertSame('t1.daumcdn.net', TrustedScriptHosts::hostOf('//t1.daumcdn.net/mapjsapi/postcode.js')); + } + + public function test_host_of_returns_null_for_same_origin_path(): void + { + $this->assertNull(TrustedScriptHosts::hostOf('/api/modules/x/y.js')); + } + + public function test_host_of_lowercases(): void + { + $this->assertSame('cdn.ckeditor.com', TrustedScriptHosts::hostOf('https://CDN.CKEditor.COM/x.js')); + } + + // ── isTrustedUrl: 명시 목록 대조 ─────────────────────── + + public function test_is_trusted_url_allows_listed_host(): void + { + $this->assertTrue( + TrustedScriptHosts::isTrustedUrl('https://cdn.ckeditor.com/x.js', ['cdn.ckeditor.com']) + ); + } + + public function test_is_trusted_url_blocks_unlisted_host(): void + { + $this->assertFalse( + TrustedScriptHosts::isTrustedUrl('https://evil.com/x.js', ['cdn.ckeditor.com']) + ); + } + + public function test_is_trusted_url_blocks_same_origin_path(): void + { + // same-origin 경로는 호스트가 없으므로 신뢰 목록 판정 대상이 아니다(별도 path 규칙이 허용) + $this->assertFalse( + TrustedScriptHosts::isTrustedUrl('/local/x.js', ['cdn.ckeditor.com']) + ); + } + + // ── hostOf: authority 우회 정규화 (KVE-2026-1915 3층 동형) ── + // + // 브라우저(WHATWG URL)는 파싱 전에 ASCII tab·LF·CR 을 제거하고 special scheme 에서 + // 백슬래시를 슬래시와 동등 처리한다. 그 정규화 없이 parse_url 로 호스트를 뽑으면 + // `https://evil.com\@cdn.ckeditor.com/x.js` 의 호스트를 `cdn.ckeditor.com`(userinfo 해석) + // 으로 보게 되는데, 브라우저는 같은 문자열을 `evil.com` 에서 로드한다. + // 런타임(TemplateApp)·정적검사(layout-scripts-src-same-origin)는 이미 정규화하므로 + // 이 판정만 갈리면 "저장은 되는데 로드는 안 되는" 비대칭이 생긴다. + + /** + * @return array<string, array{0: string, 1: string|null}> + */ + public static function authorityNormalizationProvider(): array + { + $bs = chr(92); // 백슬래시 + $tab = chr(9); + $lf = chr(10); + $cr = chr(13); + + return [ + // 신뢰 호스트를 userinfo 로 위장 → 실제 출처는 evil.com + 'backslash userinfo (scheme)' => ['https://evil.com'.$bs.'@cdn.ckeditor.com/x.js', 'evil.com'], + 'backslash userinfo (protocol-relative)' => ['//evil.com'.$bs.'@cdn.ckeditor.com/x.js', 'evil.com'], + 'tab userinfo' => ['https://evil.com'.$tab.'@cdn.ckeditor.com/x.js', 'cdn.ckeditor.com'], + 'lf in host' => ['https://evil'.$lf.'.com/x.js', 'evil.com'], + 'cr in host' => ['https://evil'.$cr.'.com/x.js', 'evil.com'], + // 역방향: 문자열상 path 지만 브라우저는 authority 로 해석 → 호스트가 나와야 한다 + 'backslash authority from path' => ['/'.$bs.'/cdn.ckeditor.com/x.js', 'cdn.ckeditor.com'], + ]; + } + + #[DataProvider('authorityNormalizationProvider')] + public function test_host_of_normalizes_authority_like_browser(string $url, ?string $expected): void + { + $this->assertSame($expected, TrustedScriptHosts::hostOf($url)); + } + + public function test_is_trusted_url_rejects_backslash_userinfo_disguise(): void + { + $this->assertFalse( + TrustedScriptHosts::isTrustedUrl( + 'https://evil.com'.chr(92).'@cdn.ckeditor.com/x.js', + ['cdn.ckeditor.com'] + ) + ); + } + + public function test_is_trusted_url_allows_backslash_authority_to_trusted_host(): void + { + // 런타임은 이 형태를 `//cdn.ckeditor.com/x.js` 로 해석해 허용한다. + // 저장측이 거부하면 신뢰 호스트 스크립트가 저장 불가가 되는 과차단 비대칭이다. + $this->assertTrue( + TrustedScriptHosts::isTrustedUrl( + '/'.chr(92).'/cdn.ckeditor.com/x.js', + ['cdn.ckeditor.com'] + ) + ); + } + + public function test_host_of_still_returns_null_for_plain_path_with_inner_backslash(): void + { + // 경로 중간의 백슬래시는 authority 를 만들지 않는다 (과차단 회귀 방지) + $this->assertNull(TrustedScriptHosts::hostOf('/js/a'.chr(92).'b.js')); + } + + // ── hosts(): 활성 모듈·플러그인 manifest 집계 ─────────── + // + // 모듈/플러그인은 PHP 추상 클래스의 `getTrustedScriptHosts()` 로 manifest 를 읽어 + // 집계된다(템플릿과 경로가 다르다). 이 축이 없으면 추상 클래스의 manifest 파싱이 + // 깨져도 템플릿 테스트만 green 이라 드러나지 않는다. + + /** + * 모듈 manifest 의 선언이 읽히고, 비문자열·공백 항목은 걸러진다. + * + * @effects every_extension_type_declaration_is_collected, malformed_or_absent_declaration_is_ignored + */ + public function test_module_reads_declared_hosts_from_manifest(): void + { + $module = $this->fakeExtension(AbstractModule::class, [ + 'trusted_script_hosts' => [' cdn.module.example ', 123, null, ''], + ]); + + $this->assertSame(['cdn.module.example'], $module->getTrustedScriptHosts()); + } + + /** + * 모듈 manifest 가 배열이 아니면 빈 목록으로 처리된다 (손상된 manifest 방어). + * + * @effects malformed_or_absent_declaration_is_ignored + */ + public function test_module_ignores_non_array_declaration(): void + { + $module = $this->fakeExtension(AbstractModule::class, ['trusted_script_hosts' => 'not-an-array']); + + $this->assertSame([], $module->getTrustedScriptHosts()); + } + + /** + * 플러그인 manifest 의 선언이 읽히고, 비문자열·공백 항목은 걸러진다. + * + * @effects every_extension_type_declaration_is_collected, malformed_or_absent_declaration_is_ignored + */ + public function test_plugin_reads_declared_hosts_from_manifest(): void + { + $plugin = $this->fakeExtension(AbstractPlugin::class, [ + 'trusted_script_hosts' => ['cdn.plugin.example', false], + ]); + + $this->assertSame(['cdn.plugin.example'], $plugin->getTrustedScriptHosts()); + } + + /** + * 선언이 아예 없는 manifest 는 빈 목록이다 (미선언 확장이 다수인 정상 경로). + * + * @effects malformed_or_absent_declaration_is_ignored + */ + public function test_plugin_without_declaration_returns_empty(): void + { + $plugin = $this->fakeExtension(AbstractPlugin::class, ['name' => 'x']); + + $this->assertSame([], $plugin->getTrustedScriptHosts()); + } + + /** + * 활성 모듈·플러그인이 선언한 호스트가 집계에 합류한다. + * + * @effects every_extension_type_declaration_is_collected + */ + public function test_hosts_includes_active_module_and_plugin_declared_hosts(): void + { + $this->fakeActiveExtensions( + modules: [$this->fakeExtension(AbstractModule::class, ['trusted_script_hosts' => ['cdn.module.example']])], + plugins: [$this->fakeExtension(AbstractPlugin::class, ['trusted_script_hosts' => ['cdn.plugin.example']])] + ); + + $hosts = TrustedScriptHosts::hosts(); + + $this->assertContains('cdn.module.example', $hosts); + $this->assertContains('cdn.plugin.example', $hosts); + } + + /** + * 모듈 집계가 실패해도 나머지 주체의 집계는 계속된다 (fine-grained 실패 격리). + * + * @effects malformed_or_absent_declaration_is_ignored + */ + public function test_hosts_survives_module_aggregation_failure(): void + { + $moduleManager = \Mockery::mock(ModuleManager::class); + $moduleManager->shouldReceive('getActiveModules')->andThrow(new \RuntimeException('boom')); + $this->instance(ModuleManager::class, $moduleManager); + + $pluginManager = \Mockery::mock(PluginManager::class); + $pluginManager->shouldReceive('getActivePlugins')->andReturn([ + $this->fakeExtension(AbstractPlugin::class, ['trusted_script_hosts' => ['cdn.plugin.example']]), + ]); + $this->instance(PluginManager::class, $pluginManager); + + $this->assertContains('cdn.plugin.example', TrustedScriptHosts::hosts()); + } + + // ── hosts(): 집계 + 훅 확장 ──────────────────────────── + + public function test_hosts_includes_hook_declared_hosts(): void + { + HookManager::addFilter( + TrustedScriptHosts::FILTER_HOOK, + fn (array $hosts) => array_merge($hosts, ['cdn.example.com']), + priority: 1 + ); + + $this->assertContains('cdn.example.com', TrustedScriptHosts::hosts()); + } + + public function test_hosts_normalizes_and_dedupes(): void + { + HookManager::addFilter( + TrustedScriptHosts::FILTER_HOOK, + fn (array $hosts) => array_merge($hosts, [' CDN.Example.com ', 'cdn.example.com']), + priority: 1 + ); + + $hosts = TrustedScriptHosts::hosts(); + $matches = array_filter($hosts, fn ($h) => $h === 'cdn.example.com'); + + $this->assertCount(1, $matches, '정규화(trim/lowercase) 후 중복이 제거되어야 합니다'); + } + + // ── hosts(): 활성 템플릿 manifest 집계 ───────────────── + // + // 템플릿은 모듈/플러그인과 달리 PHP 추상 클래스가 없고 manifest 배열로 다뤄지므로, + // `getActiveTemplate()` 이 돌려주는 template.json 배열에서 직접 읽는다. + // 이 축이 빠지면 템플릿이 선언한 호스트를 정적 검사는 통과시키는데 런타임·저장측이 + // 차단해, 문서가 지원한다고 적은 기능이 화면에서 조용히 동작하지 않는다. + + /** + * 활성 관리자 템플릿이 선언한 호스트가 집계된다. + * + * @effects every_extension_type_declaration_is_collected + */ + public function test_hosts_includes_active_admin_template_declared_hosts(): void + { + $this->fakeActiveTemplates( + admin: ['identifier' => 'vendor-admin_x', 'trusted_script_hosts' => ['cdn.admin-template.example']], + user: null + ); + + $this->assertContains('cdn.admin-template.example', TrustedScriptHosts::hosts()); + } + + /** + * 활성 사용자 템플릿이 선언한 호스트가 집계된다. + * + * @effects every_extension_type_declaration_is_collected + */ + public function test_hosts_includes_active_user_template_declared_hosts(): void + { + $this->fakeActiveTemplates( + admin: null, + user: ['identifier' => 'vendor-user_x', 'trusted_script_hosts' => ['cdn.user-template.example']] + ); + + $this->assertContains('cdn.user-template.example', TrustedScriptHosts::hosts()); + } + + /** + * 템플릿 manifest 의 비문자열/비배열 값은 무시된다 (손상된 manifest 방어). + * + * @effects malformed_or_absent_declaration_is_ignored + */ + public function test_hosts_ignores_malformed_template_declarations(): void + { + $this->fakeActiveTemplates( + admin: ['identifier' => 'vendor-admin_x', 'trusted_script_hosts' => 'not-an-array'], + user: ['identifier' => 'vendor-user_x', 'trusted_script_hosts' => ['cdn.ok.example', 123, null]] + ); + + $hosts = TrustedScriptHosts::hosts(); + + $this->assertContains('cdn.ok.example', $hosts); + $this->assertNotContains('not-an-array', $hosts); + } + + /** + * 활성 템플릿이 없어도 집계가 실패하지 않는다. + * + * @effects malformed_or_absent_declaration_is_ignored + */ + public function test_hosts_survives_absent_active_templates(): void + { + $this->fakeActiveTemplates(admin: null, user: null); + + $this->assertIsArray(TrustedScriptHosts::hosts()); + } + + /** + * manifest 를 주입한 확장(모듈/플러그인) 인스턴스를 만듭니다. + * + * `loadManifest()` 만 대체하므로 `getTrustedScriptHosts()` 의 실제 파싱·필터 로직이 + * 그대로 실행됩니다. + * + * @param class-string $abstract AbstractModule::class 또는 AbstractPlugin::class + * @param array<string, mixed> $manifest 주입할 manifest 배열 + * @return mixed 부분 목 확장 인스턴스 + */ + private function fakeExtension(string $abstract, array $manifest): mixed + { + $extension = \Mockery::mock($abstract)->makePartial(); + $extension->shouldAllowMockingProtectedMethods(); + $extension->shouldReceive('loadManifest')->andReturn($manifest); + + return $extension; + } + + /** + * ModuleManager/PluginManager 를 대체해 활성 확장 목록을 주입합니다. + * + * @param array<int, mixed> $modules 활성 모듈 인스턴스 목록 + * @param array<int, mixed> $plugins 활성 플러그인 인스턴스 목록 + */ + private function fakeActiveExtensions(array $modules, array $plugins): void + { + $moduleManager = \Mockery::mock(ModuleManager::class); + $moduleManager->shouldReceive('getActiveModules')->andReturn($modules); + $this->instance(ModuleManager::class, $moduleManager); + + $pluginManager = \Mockery::mock(PluginManager::class); + $pluginManager->shouldReceive('getActivePlugins')->andReturn($plugins); + $this->instance(PluginManager::class, $pluginManager); + } + + /** + * TemplateManager 를 대체해 활성 템플릿 manifest 를 주입합니다. + * + * @param array<string, mixed>|null $admin 활성 관리자 템플릿 manifest + * @param array<string, mixed>|null $user 활성 사용자 템플릿 manifest + */ + private function fakeActiveTemplates(?array $admin, ?array $user): void + { + $manager = \Mockery::mock(TemplateManager::class); + $manager->shouldReceive('getActiveTemplate')->with('admin')->andReturn($admin); + $manager->shouldReceive('getActiveTemplate')->with('user')->andReturn($user); + + $this->instance(TemplateManager::class, $manager); + } +} diff --git a/tests/Unit/TestCaseConnectionPurgeTest.php b/tests/Unit/TestCaseConnectionPurgeTest.php new file mode 100644 index 00000000..f62d12d2 --- /dev/null +++ b/tests/Unit/TestCaseConnectionPurgeTest.php @@ -0,0 +1,49 @@ +<?php + +namespace Tests\Unit; + +use Illuminate\Support\Facades\DB; +use PHPUnit\Framework\Attributes\Test; +use Tests\TestCase; + +/** + * TestCase 유휴 DB 연결 정리 테스트 + * + * 트랜잭션 트레이트 없는 순수 TestCase 테스트의 DB 연결이 tearDown 에서 닫히는지 + * 검증합니다. 이 정리가 회귀하면 프로세스 내 테스트 수만큼 연결이 누적되어 + * DB 를 쓰는 테스트가 max_connections 를 넘는 시점부터 전부 + * 1040 Too many connections 로 실패합니다. + */ +class TestCaseConnectionPurgeTest extends TestCase +{ + #[Test] + public function idle_connection_is_purged(): void + { + DB::select('select 1'); + $this->assertNotEmpty($this->app['db']->getConnections(), '쿼리 후 연결이 열려 있어야 합니다'); + + $this->purgeIdleDatabaseConnections(); + + $this->assertSame([], $this->app['db']->getConnections(), '유휴 연결은 모두 닫혀야 합니다'); + } + + #[Test] + public function transacting_connection_survives_purge(): void + { + DB::beginTransaction(); + + try { + $this->purgeIdleDatabaseConnections(); + + // 트랜잭션 진행 중 연결은 보존 — 트레이트의 롤백 콜백이 담당한다 + $this->assertArrayHasKey( + DB::getDefaultConnection(), + $this->app['db']->getConnections(), + '트랜잭션 진행 중인 연결은 purge 대상이 아니어야 합니다', + ); + $this->assertSame(1, DB::connection()->transactionLevel()); + } finally { + DB::rollBack(); + } + } +} diff --git a/tests/scenarios/attachment-orphan-cleanup.yaml b/tests/scenarios/attachment-orphan-cleanup.yaml new file mode 100644 index 00000000..f2bea11e --- /dev/null +++ b/tests/scenarios/attachment-orphan-cleanup.yaml @@ -0,0 +1,79 @@ +feature: 코어 고아 첨부 회수 (Orphan Attachment Cleanup) + +description: | + 폼 저장 전에 즉시 업로드되는 첨부(FileUploader 등)는 소유자 없이 먼저 만들어지므로, + 폼을 저장하지 않고 이탈하면 파일과 기록이 그대로 남는다. 공개 이슈 #115 동류 결함. + + 판정식은 네 조건의 AND 다 — 하나라도 느슨해지면 현역 첨부가 지워진다. + 1. 소유자(attachmentable_type/id)가 없다 + 2. 업로드 후 보존기간이 지났다 (폼 작성 중 유예) + 3. 확장 소유가 아니다 (source_identifier 없음 — 확장 라이프사이클 소관) + 4. 현역으로 쓰이는 첨부가 아니다 (현재 사이트 로고 등 보호 목록) + + 사이트 로고는 소유자 없이 설정값이 직접 참조하는 첨부라 판정식만으로는 고아와 구분되지 + 않는다. 그래서 설정에 실린 ID 를 보호 목록으로 삼으며, 그 해석은 정리 서비스 자신이 + 수행한다 — 호출자가 넘기는 값으로 두면 인자를 빠뜨린 호출 하나가 운영 중인 로고를 파기한다. + + 함께 고친 것: 로고 교체 시 구 로고가 설정에 재편입되던 결함. 종전 저장 흐름은 컬렉션 + 전체를 다시 수집했으므로(방금 올린 로고가 제출 payload 에 아직 없어서), 운영자가 화면에서 + 뺀 로고를 실제로 파기하지 않으면 교체할 때마다 파일·기록·설정 세 곳에 누적됐다. + 지금은 이번 요청이 제출한 목록이 저장 대상을 정한다 — sub_flow `site_logo_replacement` 참조. + +axes: + owner_state: [no_owner, has_owner] + source_owner: [core, extension] + age: [within_retention, past_retention] + protected: [not_protected, current_site_logo] + +exclusions: + - { owner_state: has_owner, source_owner: extension, reason: "소유자가 있으면 source 와 무관하게 대상이 아니다 — 코어 소유 1건으로 충분" } + - { owner_state: has_owner, age: within_retention, reason: "소유자가 있으면 나이 판정에 도달하지 않는다" } + - { owner_state: has_owner, protected: current_site_logo, reason: "사이트 로고는 소유자 없이 설정이 직접 참조하는 첨부라 소유자 보유와 양립하지 않는다" } + - { source_owner: extension, age: within_retention, reason: "확장 소유는 나이와 무관하게 코어 GC 대상이 아니다 — 경과분 1건으로 충분" } + - { source_owner: extension, protected: current_site_logo, reason: "사이트 로고는 코어 소유 컬렉션이라 확장 소유와 양립하지 않는다" } + - { protected: current_site_logo, age: within_retention, reason: "보호 목록은 보존기간 경과분에서만 판정에 관여한다 — 미경과분은 나이에서 이미 제외" } + +sub_flows: + - id: dry_run + description: 판정만 수행하고 파일·기록을 건드리지 않는 미리보기 경로 + effects: + - core_orphan_prune_dry_run + - id: schedule_gate + description: | + 자동 정리는 사용자 파일을 지우므로 기본 꺼짐이다. 커맨드가 `--scheduled` 에서 + 설정을 false 폴백으로 확인한 뒤에만 수행한다. + effects: + - core_orphan_prune_gate_off_by_default + - core_orphan_prune_gate_on + - id: site_logo_replacement + description: | + 저장할 로고 목록은 이번 요청이 제출한 것이 정한다. 컬렉션 전체를 다시 훑으면 저장에 + 실패했거나 이탈로 남은 미참조 첨부가 설정에 되살아난다(브라우저 실측 — 신규 1건만 + 제출했는데 저장 결과가 2건). + effects: + - site_logo_replacement_drops_previous + - site_logo_unreferenced_collection_member_not_re_absorbed + - site_logo_stale_submitted_id_filtered + - site_logo_untouched_when_not_submitted + - site_logo_failed_save_keeps_removed_attachment + - id: settings_surface + description: 화면 바인딩 키가 코어 기본값·서버 검증과 일치하는지 + effects: + - core_settings_orphan_toggle_default_off + - core_settings_orphan_keys_declared + +effects: + - core_orphan_prune_skips_owned + - core_orphan_prune_skips_extension_owned + - core_orphan_prune_protects_site_logo + - core_orphan_prune_collects_replaced_site_logo + - core_orphan_prune_keeps_within_retention + - core_orphan_prune_deletes_file + - core_orphan_prune_deletes_soft_deleted + +test_files: + - tests/Feature/Console/PruneOrphanAttachmentsCommandTest.php + - tests/Feature/Services/SettingsServiceSiteLogoTest.php + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-orphan-cleanup.test.ts + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-logo-remove-expression.test.ts + - tests/Playwright/specs/admin/settings-upload-orphan-cleanup.spec.ts diff --git a/tests/scenarios/board-activity-secret-masking.yaml b/tests/scenarios/board-activity-secret-masking.yaml new file mode 100644 index 00000000..f6678bdf --- /dev/null +++ b/tests/scenarios/board-activity-secret-masking.yaml @@ -0,0 +1,43 @@ +feature: 사용자 활동/공개 프로필 목록의 비밀 본문 마스킹 (KVE-2026-1914 형제) + +description: | + 공개 프로필(`GET users/{uuid}/posts`)과 마이페이지 활동(`GET me/board-activities`)은 본문 + 일부(`content_plain`)를 함께 싣는데 Resource 를 거치지 않고 저장소 배열을 그대로 응답에 + 싣는다. 공개 프로필 라우트는 `optional.sanctum` 이라 **미인증**으로 도달하므로, 타인의 + 비밀글·블라인드 글 본문이 그대로 나갔다. + + 결함의 형태는 **사문화된 옵트인 플래그**였다 — 저장소가 `is_public` 을 읽어 필터를 걸도록 + 돼 있었지만 그 키를 설정하는 코드가 어디에도 없어 한 번도 적용되지 않았다. 판정을 열람자 + 신원(`viewer_id`) 기반 fail-closed 로 뒤집었다. + + 두 활동 유형이 **서로 다른 코드 경로**를 탄다: + + - activity_type=authored — 내가 쓴 글. 열람자 ≠ 대상 사용자면 마스킹(`$isOwnView`). + - activity_type=commented — 내가 댓글 단 글. 이 목록에는 **타인이 쓴 비밀글**이 섞이므로 + 글 단위로 판정한다(`$post->user_id !== $viewerId`). authored 축만 검증하면 이 분기가 + 무보호로 남는다. + + 가리는 것은 **본문뿐**이다. 행과 제목은 게시판 목록에서 이미 같은 수준으로 노출되며 + (`PostResource` 의 목록 규칙: 제목은 노출, 본문만 차단) 프로필 UI 도 비밀글/블라인드 + 배지를 그린다 — 행을 지우면 결함 차단에 필요한 범위를 넘어 기능이 깎인다. 착수 단계에서 + 행 제거로 구현했다가 철회한 이력이 있어, 행·제목 유지를 효과로 고정한다. + +axes: + viewer: [guest, other_user, owner] + activity_type: [authored, commented] + +exclusions: + - { viewer: guest, activity_type: commented, reason: "마이페이지 활동 라우트는 auth:sanctum — 게스트가 도달할 수 없다. 공개 프로필 라우트는 authored 만 서빙한다" } + +effects: + - public_profile_masks_secret_post_content_for_others + - public_profile_masks_blinded_post_content_for_others + - public_profile_shows_own_secret_content_to_owner + - commented_activity_masks_others_secret_content + # 과차단 회귀 축 — fail-closed 로 뒤집으면서 본인 시야까지 좁히면 기능 축소다. + - commented_activity_shows_own_secret_content_to_owner + # 철회한 과잉 수정(행 제거)의 재도입 차단 — 본문 비노출만 단언하면 행을 통째로 지워도 green 이다. + - activity_rows_and_titles_remain_visible + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/User/UserActivitySecretExposureTest.php diff --git a/tests/scenarios/board-secret-content-gate.yaml b/tests/scenarios/board-secret-content-gate.yaml new file mode 100644 index 00000000..85d9f9d0 --- /dev/null +++ b/tests/scenarios/board-secret-content-gate.yaml @@ -0,0 +1,48 @@ +feature: 비밀글 하위 리소스(첨부·댓글) 게이팅 (KVE-2026-1914 A-3/A-4) + +description: | + 부모 게시글이 비밀글이면, 본문뿐 아니라 하위 리소스(첨부 다운로드/미리보기, 댓글 목록)도 + 열람 권한(작성자/manager/posts.read-secret) 없는 요청에는 원문을 노출하지 않는다. + + 게이트 SSoT 는 SecretContentGate 이며, 첨부 서빙(preview/download)·댓글 목록·본문이 모두 + 같은 판정을 재적용한다. 미리보기 라우트는 permission 미들웨어 없이(공개 썸네일 정책) + 컨트롤러/서비스의 비밀 게이트만이 게스트를 막는다. + + 이중 방어(A-4b): 댓글 목록은 1차로 CommentController::index 가 비뷰어에게 빈 컬렉션을 + 반환하고, 2차로 CommentResource 가 컨트롤러가 넘긴 부모 post 로 댓글당 원문을 재확인한다 + (요청 속성 재사용 — 댓글당 board_posts lazy-load 없이). + +axes: + viewer: [guest, regular, owner, manager] + +effects: + - regular_user_cannot_preview_secret_post_attachment + - regular_user_cannot_download_secret_post_attachment + - guest_cannot_preview_secret_post_attachment + - guest_cannot_download_secret_post_attachment + - owner_can_access_secret_post_attachment + - manager_can_access_secret_post_attachment + - normal_post_attachment_still_public + - guest_can_preview_normal_post_attachment + # 서명 preview URL 축 — <img> 무인증 렌더 경로 (게이트 약화 없는 자격 위임, 7.0.7 사전점검 실측 발견) + - guest_with_valid_signature_passes_secret_preview_gate + - guest_with_tampered_signature_still_blocked + - guest_with_expired_signature_still_blocked + - secret_post_detail_serializes_signed_preview_url + - normal_post_detail_serializes_plain_preview_url + - regular_user_gets_empty_comment_list_on_secret_post + - guest_blocked_from_secret_post_comments + - owner_sees_secret_post_comments + - manager_sees_secret_post_comments + - normal_post_comments_still_visible + - second_defense_masks_secret_comment_content + - owner_second_defense_not_masked + - comment_list_no_per_comment_post_query + +test_files: + - modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostAttachmentAccessTest.php + - modules/_bundled/sirsoft-board/tests/Feature/User/SecretPostCommentAccessTest.php + +rules_layer_coverage: + - rule: list-resource-resource-relation-requery-n-plus-one + coverage: CommentResource 는 컨트롤러가 넘긴 부모 post 를 재사용 — 댓글당 board_posts 조회 0건 (comment_list_no_per_comment_post_query 가 고정) diff --git a/tests/scenarios/ckeditor5-unused-image-cleanup.yaml b/tests/scenarios/ckeditor5-unused-image-cleanup.yaml new file mode 100644 index 00000000..2ba78af0 --- /dev/null +++ b/tests/scenarios/ckeditor5-unused-image-cleanup.yaml @@ -0,0 +1,167 @@ +feature: 에디터 업로드 이미지 미참조 정리 (CKEditor5 Unused Image Cleanup) + +description: | + 에디터로 올렸지만 어떤 콘텐츠에서도 쓰이지 않는 이미지를 파일·기록 함께 회수한다. + 공개 이슈 #115 — 종전에는 회수 경로가 플러그인 언인스톨(테이블 드롭)뿐이었다. + + 핵심 동작: + - 참조 판정 토큰 2종: hash(API 폴백 URL형) + 저장 파일명(디스크 직접 URL형). + 한쪽만 검사하면 다른 형태로 저장된 이미지를 미참조로 오판해 삭제한다. + - 참조 소스는 코어 6개 + 확장이 filter 훅으로 등록(게시글/페이지/상품 설명). + 로그 사본 테이블은 제외 — 소스로 삼으면 "로그 삭제 → 이미지 고아화" 역전이 생긴다. + - 삭제 순서는 파일 먼저, 기록 나중. 파일이 이미 없으면 고아 기록이므로 기록만 지운다 + (그러지 않으면 매 회차 같은 행을 재시도하는 루프가 된다). + - 자동 정리는 기본 꺼짐(운영자 옵트인). 스케줄 게이트가 설정 조회 실패 시 true 로 + 폴백하므로 커맨드가 `--scheduled` 에서 false 폴백으로 재확인한다. + - 관리 화면은 참조 상태를 저장하지 않고 조회 시 판정한다(참조는 수시 변동). + 기본 목록은 현재 페이지만, 참조 필터는 최신 500건 윈도우 + scan_limited 고지. + +# axes 는 "한 업로드 행의 정리 여부를 가르는 판정 입력" 만 둔다 — 이 셋은 한 테스트가 +# 동시에 지정하는 값이라 조합마다 실제 케이스가 대응된다. +# 실행 경로(스케줄 게이트 / 수동 CLI / 관리 화면)와 dry-run 은 판정과 직교한 흐름이라 +# sub_flows 로 분리했다. 한 축으로 묶으면 "화면 삭제 + dry-run" 같은 존재하지 않는 조합이 +# cross product 에 생기고, 그 조합에 @scenario 를 붙이는 순간 마킹이 사실이 아니게 된다. +axes: + url_variant: [hash_api_fallback, direct_url_uuid, absent] + age: [within_retention, past_retention] + disk_state: [default_local, secondary_disk, file_missing] + +exclusions: + - { url_variant: hash_api_fallback, disk_state: file_missing, reason: "참조된 이미지는 삭제 대상이 아니라 파일 부재 분기(고아 기록 수거)에 도달하지 않는다" } + - { url_variant: direct_url_uuid, disk_state: file_missing, reason: "동일" } + - { url_variant: hash_api_fallback, age: within_retention, reason: "보존기간 안이면 참조 판정 전에 후보에서 빠진다 — 참조 변종은 경과분에서만 의미가 있다" } + - { url_variant: direct_url_uuid, age: within_retention, reason: "동일" } + - { url_variant: hash_api_fallback, disk_state: secondary_disk, reason: "참조 보존은 파일을 건드리지 않으므로 disk 축과 무관 — 기본 디스크 1건으로 충분" } + - { url_variant: direct_url_uuid, disk_state: secondary_disk, reason: "동일" } + - { age: within_retention, disk_state: secondary_disk, reason: "보존기간 안이면 후보 조회 단계에서 빠져 파일 접근에 도달하지 않는다 — disk 상태가 결과를 바꾸지 않는다" } + - { age: within_retention, disk_state: file_missing, reason: "동일" } + +effects: + # 참조 판정 + - reference_sources_default_core_only + - reference_sources_hook_append + - reference_match_hash_api_url + - reference_match_direct_url + - reference_unreferenced + - reference_like_escape + - reference_invalid_source_skipped + - reference_invalid_source_warns + - reference_map_batch + - reference_source_listener_subscribes_as_filter + - reference_source_listener_appends_without_dropping + - reference_source_listener_schema_contract + # 정리 동작 + - prune_deletes_file_and_record + - prune_keeps_referenced_hash_form + - prune_keeps_referenced_direct_url + - prune_keeps_within_retention + - prune_dry_run_no_delete + - prune_limit_oldest_first + - prune_missing_file_removes_record + - prune_delete_failure_keeps_record + - prune_logs_run_summary + - prune_respects_row_disk + - delete_upload_shared_by_ui_and_command + # 스케줄 게이트 (인수 기준: 설정을 만지지 않은 사이트에서 자동 삭제 0) + - schedule_gate_off_by_default + - schedule_gate_off_explicit + - schedule_gate_on + - retention_disabled_guard + - days_option_overrides_setting + - manual_run_dry_run_no_delete + - manual_run_ignores_toggle + # 관리 API + - admin_index_lists_uploads + - admin_index_column_pruning + - admin_index_referenced_badge + - admin_index_unreferenced_filter + - admin_index_scan_window_limited + - admin_index_rejects_unknown_sort + - admin_index_requires_read_permission + - admin_index_requires_authentication + - admin_destroy_removes_file_and_record + - admin_destroy_not_found + - admin_destroy_requires_delete_permission + - admin_bulk_delete_removes_selected + - admin_bulk_delete_rejects_empty + - admin_bulk_delete_rejects_unknown_id + # 화면 계약 + - admin_ui_selection_scope_page + - admin_ui_merge_query + - admin_ui_sort_subset + - admin_ui_reference_badge + - settings_cleanup_toggle_default_off + - settings_cleanup_keys_declared + - settings_cleanup_retention_round_trip + +sub_flows: + - id: schedule_gate + description: | + 자동 정리는 사용자 파일을 지우므로 기본 꺼짐이다. 스케줄 등록부의 enabled_config 게이트는 + 설정 조회가 실패하면 true 로 폴백하므로, 커맨드가 `--scheduled` 에서 같은 설정을 false + 폴백으로 재확인한다. 인수 기준은 "설정을 만지지 않은 사이트에서 자동 삭제 0". + effects: + - schedule_gate_off_by_default + - schedule_gate_off_explicit + - schedule_gate_on + - manual_run_ignores_toggle + - id: retention_resolution + description: 보존기간 해석 순서(옵션 > 플러그인 설정 > 기본 30일)와 1일 미만 자기 차단 + effects: + - retention_disabled_guard + - days_option_overrides_setting + - id: dry_run + description: 판정만 수행하고 파일·기록을 건드리지 않는 미리보기 경로 + effects: + - prune_dry_run_no_delete + - manual_run_dry_run_no_delete + - id: admin_api + description: 관리 화면이 쓰는 조회·단건 삭제·일괄 삭제 API 와 권한 경계 + effects: + - admin_index_lists_uploads + - admin_index_referenced_badge + - admin_index_unreferenced_filter + - admin_index_scan_window_limited + - admin_index_rejects_unknown_sort + - admin_index_requires_read_permission + - admin_index_requires_authentication + - admin_destroy_removes_file_and_record + - admin_destroy_not_found + - admin_destroy_requires_delete_permission + - admin_bulk_delete_removes_selected + - admin_bulk_delete_rejects_empty + - admin_bulk_delete_rejects_unknown_id + - delete_upload_shared_by_ui_and_command + - id: reference_source_registry + description: 참조 소스 확장점(filter 훅)과 불량 선언 방어 + effects: + - reference_sources_default_core_only + - reference_sources_hook_append + - reference_invalid_source_skipped + - reference_invalid_source_warns + - reference_map_batch + - reference_source_listener_subscribes_as_filter + - reference_source_listener_appends_without_dropping + - reference_source_listener_schema_contract + - id: admin_screen_contract + description: 화면이 지키기로 한 계약 (선택 범위 / 목록 상태 승계 / 정렬 부분집합 / 설정 기본값) + effects: + - admin_ui_selection_scope_page + - admin_ui_merge_query + - admin_ui_sort_subset + - admin_ui_reference_badge + - settings_cleanup_toggle_default_off + - settings_cleanup_keys_declared + - settings_cleanup_retention_round_trip + +test_files: + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageReferenceScanServiceTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageCleanupServiceTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Console/PruneUnusedImagesCommandTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Feature/Http/Controllers/Admin/ImageUploadAdminControllerTest.php + - plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/ckeditor5Uploads.test.tsx + - plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsCleanup.test.tsx + - plugins/_bundled/sirsoft-ckeditor5/tests/Playwright/specs/admin/uploads-admin.spec.ts + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php + - modules/_bundled/sirsoft-page/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Listeners/Ckeditor5ReferenceSourcesListenerTest.php diff --git a/tests/scenarios/core-gc-schedules.yaml b/tests/scenarios/core-gc-schedules.yaml new file mode 100644 index 00000000..14ac7d28 --- /dev/null +++ b/tests/scenarios/core-gc-schedules.yaml @@ -0,0 +1,110 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경의 fallback YAML 파서가 nested axes 를 읽지 못해 +# 검출되지 않는다(다른 시나리오와 동일 한계). 축 조합은 PHPUnit Feature 테스트 +# (CoreScheduleRegistrationTest · DataRetentionCommandsTest)와 모듈 커맨드 테스트가 +# 커버하고 green 이다. + +feature: 만료 데이터 자동 정리 예약과 예약 시각 기준 (공개이슈 #110) + +description: | + 수명이 지난 데이터를 정리하는 예약 작업을 등록하고, 예약의 실행 시각 해석 기준을 + 사이트 설정 시간대로 통일한다. + + 배경: + - 만료 토큰 정리 커맨드는 있었지만 어디에도 예약되지 않아 행이 영구 누적됐다. + - 정리 코드는 있는데 호출자가 0건인 대상(임시 주문·SEO 통계·예약 이력·본인인증 + challenge)이 있었고, 보존 정책이 아예 없는 대상(본인인증 이력·활동 로그·알림 + 발송 이력)도 있었다. + - `app.timezone` 이 UTC 이고 예약에 시간대 지정이 없어, '새벽 4시' 로 등록해도 + 한국 기준 낮에 실행됐다. 관리자가 화면에서 등록한 cron 도 같은 결함을 가졌다. + - 이 축의 회귀 테스트가 0건이라 결함군이 무가드로 통과됐다. + + 적용 지점: + - `routes/console.php` 의 만료 데이터 정리 구획 (일 1회 새벽 배치 + 시간별 1건). + - 신설 코어 커맨드 6종 + 이커머스 모듈 커맨드 1종. + - 시각 기준: 설정 반영부가 `app.schedule_timezone` 을 세팅해 Schedule 인스턴스 + 전체에 일괄 적용 (이벤트별 지정 방식은 누락이 생겨 채택하지 않음). + - 관리자 등록 예약: 다음 실행 시각 계산도 같은 기준. + +axes: + target: + - sanctum_tokens + - password_reset_tokens + - failed_jobs + - job_batches + - notifications + - extension_bundles + - seo_cache_stats + - schedule_histories + - identity_challenges + - identity_logs + - activity_logs + - notification_logs + - ecommerce_temp_orders + row_age: [within_retention, beyond_retention] + schedule_axis: [code_registered, admin_registered] + timezone_setting: [configured, empty] + # 스토리지 잔존물 GC (#120 후속 — storage:prune-leftovers): 대상군 × dry-run × 보존 가드 + leftover_target: + - update_staging + - core_temp + - vendor_bundle_staging + - extension_backups + - core_backups + - legacy_browser_log + leftover_mode: [real_delete, dry_run] + leftover_guard: [aged_out, fresh_kept, latest_backup_kept, normal_pending_untouched, idempotent_second_run] + +exclusions: + - { target: identity_challenges, row_age: beyond_retention, reason: "상태 전환만 수행하며 삭제하지 않음 — 만료 여부는 expires_at 이 판정" } + - { schedule_axis: admin_registered, target: sanctum_tokens, reason: "관리자 등록 축은 시각 기준만 검증 (대상별 동작은 코드 예약 축이 커버)" } + +effects: + - all_gc_commands_are_registered_in_schedule + - gc_commands_run_on_one_server_only + - sanctum_prune_uses_hours_option + - rows_beyond_retention_are_deleted + - rows_within_retention_are_kept + - identity_challenges_transition_without_deletion + - schedule_timezone_follows_site_general_timezone + - schedule_timezone_falls_back_to_app_timezone_when_unset + - app_timezone_stays_utc + - admin_registered_schedule_next_run_uses_same_basis + - new_commands_are_in_schedule_security_allowlist + - new_commands_are_exposed_in_dev_dashboard + - automated_prune_emits_its_own_hooks + - retention_floor_is_owned_by_the_domain_layer + - large_purge_is_split_into_batches + # 스토리지 잔존물 GC (#120 후속) + - aged_leftovers_are_pruned_after_retention + - fresh_leftovers_are_kept + - latest_backup_is_always_kept + - normal_pending_extension_dirs_are_untouched + - dry_run_deletes_nothing + - leftover_prune_is_idempotent + - legacy_browser_log_is_removed + - browser_logs_rotate_daily_with_finite_retention + - successful_install_leaves_no_temp_directory + +# E2E axis 없음 (스토리지 잔존물 GC): 브라우저 표면은 dev-dashboard 의 실행 버튼뿐이고 +# blade 는 frontend-change-requires-e2e 룰 대상 경로가 아니다. 판정(패턴 매칭·mtime· +# 보존 가드)은 전부 서버측 파일시스템 연산이라 서버측 단위 테스트가 같은 것을 더 촘촘히 +# 고정한다. 대시보드 버튼 노출 자체는 CoreScheduleRegistrationTest 가 소스 대조로 단언한다. + +test_files: + - tests/Feature/Console/CoreScheduleRegistrationTest.php + - tests/Feature/Console/DataRetentionCommandsTest.php + - tests/Unit/Repositories/DeletesInBatchesTest.php + - tests/Unit/Services/StorageLeftoverPruneServiceTest.php + - tests/Feature/DevTools/BrowserLogRotationTest.php + - tests/Unit/Services/LanguagePack/LanguagePackServiceTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Console/PruneExpiredTempOrdersCommandTest.php + +rules_layer_coverage: + - rule: offset-chunk-mutates-filter + coverage: | + 정리는 기본키 배치로 끊어 지운다 — OFFSET 이 개입하지 않으므로 커서가 밀려 + 미처리 행을 건너뛰는 결함이 성립하지 않는다. 매 회전이 같은 술어를 다시 + 평가하고, 직전 회전이 지운 만큼 모집단이 줄어든다. + - rule: console-confirm + coverage: 정리 커맨드는 확인 프롬프트가 없는 무인 실행 전용 diff --git a/tests/scenarios/ecommerce-secret-inquiry-masking.yaml b/tests/scenarios/ecommerce-secret-inquiry-masking.yaml new file mode 100644 index 00000000..5c774791 --- /dev/null +++ b/tests/scenarios/ecommerce-secret-inquiry-masking.yaml @@ -0,0 +1,57 @@ + +feature: 상품 문의 목록 비밀글 마스킹 (KVE-2026-1914 A-1/A-2) + +description: | + 상품 문의 목록은 게시판 모듈의 `get_by_ids` 훅으로 게시글 본문을 받아 직렬화한다. 비밀 + 문의의 원문(content/title/reply/attachments)이 열람 권한 없는 요청에 새지 않아야 한다. + + 방어는 두 계층이며 **각 계층이 독립적으로 성립**해야 한다: + + - layer=hook — 게시판 리스너(EcommerceInquiryHookListener::getByIds)가 SecretContentGate + 판정으로 원문을 마스킹하고, 권위 플래그 `can_view_secret` 를 payload 에 실어 보낸다. + - layer=service — 이커머스 서비스(ProductInquiryService::getProductInquiries)가 그 권위 + 플래그만 신뢰해 재마스킹한다. 자기 권한을 재계산하지 않으므로 게이트 강도가 훅과 갈리지 + 않고, 플래그가 없으면 fail-closed(마스킹)다 — 훅 미치환·타 확장 치환 회귀를 덮는다. + + 클라이언트가 보내는 `exclude_secret` 는 보안 판정에 쓰이지 않는다(단순 표시 필터). 노출 + 여부는 서버가 요청자 신원으로만 결정한다. + + 같은 훅 경계에 입력 경계 규율이 하나 더 걸린다(sub_flow `listener_ip_boundary`): 리스너는 + 요청 객체에 손을 뻗지 않고(`request()->ip()` 직접 참조 금지) 요청 경계인 서비스가 payload + 로 실어 준 `ip_address` 만 기록한다. viewer 축과 교차하지 않는 별개 관심사라 축이 아니라 + sub_flow 로 둔다. + +axes: + layer: [hook, service] + viewer: [non_viewer, owner] + +effects: + - hook_masks_secret_post_for_non_viewer + - hook_exposes_secret_post_to_owner + - hook_emits_authoritative_can_view_secret_flag + - service_remasks_when_hook_omits_masking + - service_fails_closed_when_flag_absent + - service_remasks_title_with_shared_placeholder + - service_exposes_original_when_flag_true + +sub_flows: + - id: listener_ip_boundary + description: | + 리스너 입력 경계 — 생성·답변 두 경로 모두 서비스가 payload 로 주입한 ip_address 를 + 그대로 기록하고, payload 에 없으면 요청 객체로 되돌아가지 않고 폴백(0.0.0.0)한다. + 회귀 가드는 request IP 를 비-0.0.0.0 으로 세팅한 상태에서 폴백을 확인한다 — + 0.0.0.0 기본 request 로는 request()->ip() 재도입이 silent green 이 된다. + effects: + - listener_uses_service_provided_ip_not_request + - listener_does_not_reach_into_request_for_ip + # 주입 측 — Listener 경계는 서비스가 IP 를 실어 보낼 때만 성립한다. 수용 측만 + # 고정하면 주입 코드를 지워도 스위트가 green 이고 문의 IP 가 조용히 0.0.0.0 이 된다. + # 생성·답변은 서로 다른 서비스 메서드(createInquiry / createReply)라 주입 코드가 각각 + # 있다 — 한 효과로 묶으면 생성 경로 테스트 하나가 답변 경로까지 덮는 것처럼 집계된다. + - service_injects_client_ip_into_hook_payload + - service_injects_client_ip_into_reply_hook_payload + +test_files: + - modules/_bundled/sirsoft-board/tests/Unit/Listeners/EcommerceInquiryHookListenerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/PublicProductInquiryControllerTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/UserProductInquiryControllerTest.php diff --git a/tests/scenarios/exception-response-exposure.yaml b/tests/scenarios/exception-response-exposure.yaml new file mode 100644 index 00000000..cca22e56 --- /dev/null +++ b/tests/scenarios/exception-response-exposure.yaml @@ -0,0 +1,46 @@ +feature: 코어 실패 응답의 예외 원문 비노출 (Exception Response Exposure) + +description: | + 언어팩 관리 API 가 실패했을 때, 응답의 `message` 자리에 이미 번역된 예외 문구가 + 넘어가면 키 해석에 실패해 원문이 그대로 관리자 화면에 나간다. 원문에는 압축 라이브러리 + 오류·파일 경로·SQL 상태코드가 섞여 있다. + + 핵심 설계: + - `ResponseHelper::error()` 의 첫 인자는 **다국어 키 전용**이다. 번역문을 그 자리에 + 넘기지 않는다. + - 진단 정보 노출은 `ResponseHelper` 가 `app.debug` 에서만 Throwable 을 펼치는 + 기존 메커니즘에 위임한다 — 호출부가 직접 원문을 문자열로 조립하지 않는다. + - 상태코드는 바꾸지 않는다. manifestPreview 는 업로드 파일 해석 실패가 대부분 + 클라이언트 입력 오류라 422 를 유지하고, refreshCache 는 500 이다. + + 두 테스트는 `app.debug=false` 를 명시적으로 세운 뒤 응답 본문 전체에 내부 흔적 + (`ZipArchive` `SQLSTATE` `.php` `vendor/` `Exception`)이 없음을 단언한다. 디버그를 + 끄지 않으면 헬퍼가 의도적으로 노출하는 경로와 구분되지 않는다. + +axes: + endpoint: [manifest_preview, refresh_cache] + +exclusions: [] + +effects: + - language_pack_manifest_preview_failure_omits_exception_text + - language_pack_refresh_cache_failure_omits_exception_text + +scenarios: + - id: failure_responses_never_carry_exception_text_in_the_message_slot + description: 설치 전 검증 실패(422)와 캐시 갱신 실패(500) 모두 안내 문구만 전달하고 예외 원문을 싣지 않는다 + effects: + - language_pack_manifest_preview_failure_omits_exception_text + - language_pack_refresh_cache_failure_omits_exception_text + +test_files: + - tests/Feature/Api/Admin/LanguagePackControllerTest.php + +notes: | + 이 매니페스트는 5차 감사에서 신설됐다. 두 테스트는 브랜치가 추가했지만 + `@scenario`/`@effects` 가 없어 어느 매니페스트에도 묶이지 않았고, 선언이 아예 없는 + effect 는 test-scenario-coverage 의 셀 대상이 아니라 조용히 통과했다. + + 같은 파일이 `language-pack-php-whitelist-and-activation-boundary.yaml` 의 test_files + 에도 등록돼 있으나 그쪽이 다루는 축은 PHP 구문 화이트리스트와 활성화 권한 경계다. + 실패 응답의 원문 노출은 그 시나리오의 축이 아니므로 별도 매니페스트로 둔다. diff --git a/tests/scenarios/file-serving-parent-state-gate.yaml b/tests/scenarios/file-serving-parent-state-gate.yaml new file mode 100644 index 00000000..284b7c43 --- /dev/null +++ b/tests/scenarios/file-serving-parent-state-gate.yaml @@ -0,0 +1,68 @@ + +feature: hash 기반 파일 서빙의 부모 상태 게이트 (KVE-2026-1914 A-6 S-1/S-2) + +description: | + hash·ID 만으로 도달하는 파일 서빙 라우트는 그 자체로는 인가 정보를 갖지 않는다. 따라서 + **부모 엔티티의 공개 상태**를 서빙 시점에 재확인해야 하며, 같은 자원의 preview 와 download 가 + 같은 강도의 게이트를 공유해야 한다 — 한쪽만 막으면 다른 쪽이 조용한 우회로가 된다. + + - resource=page_attachment (S-1): 부모 페이지가 미발행이면 첨부 preview 도 차단한다. 종전에는 + download 에만 `pages.read` 게이트가 있고 preview 는 무게이트였다. 차단 응답은 404(존재 은닉). + - resource=review_image (S-2): 부모 리뷰가 VISIBLE 이 아니면(관리자 숨김·승인 대기) 이미지 + 서빙을 차단한다. 게이트는 컨트롤러가 아니라 **서비스 계층**(ProductReviewImageService)에 두어 + 그 서비스를 쓰는 모든 호출부가 자동으로 커버되게 한다. 서비스가 null 을 반환하고 컨트롤러가 + 404 로 응답한다. + + 발행/공개 상태인 자원은 종전대로 무인가 서빙된다(과잉 차단 회귀 방지 축). + + 권한 보유자(pages.read 관리자)는 미발행 첨부도 미리볼 수 있어야 한다 — 초안 편집 화면의 + 썸네일이 그 경로다. 이는 parent_state=restricted 조합 안의 허용 분기이며 별도 축이 아니다 + (review_image 에는 대응하는 권한 분기가 없어, viewer 를 축으로 세우면 실재하지 않는 조합이 + cross product 에 생긴다). + + 브라우저 <img src> 는 Authorization 헤더를 실을 수 없어, 게이트 도입 후 관리자 + 상세·미발행 미리보기 화면의 첨부 썸네일이 무인증 요청 → 404 로 깨졌다(7.0.7 사전점검 + 브라우저 실측으로 발견). 게이트를 통과한 응답의 직렬화 시점(PageResource / + PublicPageResource 미발행 분기)에 한시 서명 preview URL 을 발급하고, 서빙 엔드포인트가 + 유효 서명을 무인증 허용한다. 무서명 게이트는 종전과 동일하며, 발행 페이지는 무서명 + 공개 URL 을 유지한다(SEO·캐시 안정성). + + review_image 도 같은 결로 관리자 리뷰 화면(목록·상세)의 숨김 리뷰 썸네일이 깨졌다. + 숨김 리뷰가 이미지와 함께 직렬화되는 경로는 관리자 응답뿐이므로(공개 목록은 VISIBLE + 필터), ProductReviewResource 가 숨김 리뷰의 download_url 을 한시 서명 URL 로 직렬화하고 + 서빙 엔드포인트가 유효 서명을 허용한다. 전시중 리뷰는 무서명 URL(직접 URL/CDN 포함) 유지. + +axes: + resource: [page_attachment, review_image] + parent_state: [public, restricted] + +effects: + - unpublished_page_attachment_preview_blocked_for_guest + - unpublished_page_attachment_preview_blocked_without_permission + - unpublished_page_attachment_preview_allowed_with_pages_read + - unpublished_page_attachment_download_blocked_for_guest + # download 축 허용 케이스 — preview 축(4건) 대비 download 축이 차단 1건뿐이라 + # 게이트가 download 만 과차단으로 회귀해도 green 이었다. + - unpublished_page_attachment_download_allowed_with_pages_read + - published_page_attachment_download_still_public + - published_page_attachment_preview_still_public + # 서명 preview URL 축 — <img> 무인증 렌더 경로 (게이트 약화 없는 자격 위임) + - unpublished_page_attachment_preview_allowed_with_valid_signature + - unpublished_page_attachment_preview_blocked_with_tampered_signature + - unpublished_page_attachment_preview_blocked_with_expired_signature + - admin_response_serializes_signed_preview_url_for_unpublished_page + - published_page_serializes_plain_preview_url + - hidden_review_image_download_blocked + - visible_review_image_download_still_served + # 리뷰 이미지 서명 download URL 축 — 숨김 리뷰의 관리자 화면 <img> 렌더 경로 (페이지·게시판과 동일 설계) + - hidden_review_image_download_allowed_with_valid_signature + - hidden_review_image_download_blocked_with_tampered_signature + - hidden_review_image_download_blocked_with_expired_signature + - admin_review_serializes_signed_download_url_for_hidden_review + - visible_review_serializes_plain_download_url + +test_files: + - modules/_bundled/sirsoft-page/tests/Feature/User/PublicPageAttachmentAccessTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/ReviewImageControllerTest.php + # 숨김 리뷰 이미지 서명 download URL 의 직렬화 축 (관리자 상세 응답) + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Admin/ProductReviewControllerTest.php diff --git a/tests/scenarios/guest-order.yaml b/tests/scenarios/guest-order.yaml index 42545648..b0f7388e 100644 --- a/tests/scenarios/guest-order.yaml +++ b/tests/scenarios/guest-order.yaml @@ -52,6 +52,20 @@ effects: - guest_orders_show_response_excludes_admin_memo_user_user_id_promotions_snapshot - guest_orders_show_response_includes_mc_subtotal_discount_shipping_total_amount # 단계 5 그룹 C 보강 - guest_orders_cancel_estimate_refund_update_shipping_confirm_option_require_token + # 실패 원인 구분(도메인 422 / 검증 422 / 인프라 500)은 입력 axis 가 아니라 결과 축이므로 + # error_class axis 를 신설하지 않고 effects 로 검증한다 (member-order-change-address.yaml 과 동일 판단). + # 인프라 500 은 서비스 mock 으로만 유발 가능해 PHPUnit 이 SSoT 이고, 브라우저는 200/422 축만 담당한다. + # 예외 → 상태코드 분리 (공개 #104) — 비회원 액션 전 지점 + - guest_update_shipping_address_domain_exception_returns_422_infra_returns_500 + - guest_cancel_domain_exception_returns_422_infra_returns_500 + - guest_confirm_option_has_no_domain_exception_so_generic_catch_returns_500 + - guest_action_5xx_responses_exclude_exception_message + # 국내/해외 배송지 조합 정합성 (부수 발견 F3) + - guest_shipping_address_intl_combination_without_country_code_returns_422_validation_errors + - guest_shipping_address_switching_to_intl_clears_domestic_columns + - guest_shipping_address_domestic_country_code_requires_zipcode_and_address + - guest_shipping_address_foreign_country_code_requires_intl_fields + - guest_shipping_address_round_trip_between_domestic_and_intl_clears_opposite_columns # 프론트엔드 진입 (단계 5 그룹 A) - buy_now_when_guest_creates_temp_order_then_navigates_to_login_with_redirect_query - cart_order_when_guest_creates_temp_order_then_navigates_to_login_with_redirect_query diff --git a/tests/scenarios/idv-permission-guard.yaml b/tests/scenarios/idv-permission-guard.yaml index 40517522..12ce7f29 100644 --- a/tests/scenarios/idv-permission-guard.yaml +++ b/tests/scenarios/idv-permission-guard.yaml @@ -14,19 +14,59 @@ description: | - 모달 취소 버튼 sequence 가 [apiCall(cancel), resolveIdentityChallenge, closeModal] 호출 - cancel apiCall 은 challenge_id 가용성 if 가드 + onError suppress +# axis 의미: +# 가드 판정(통과/403)을 가르는 축은 셋뿐이다 — 누가(actor_role), 어느 엔드포인트(endpoint), +# 그 challenge 가 누구 것인가(challenge_owner). challenge_status 는 가드를 통과한 *뒤*의 +# 결과(404/422)를 가르고, modal_trigger_state 는 프론트 모달 시퀀스의 축이라 +# 가드 판정과 곱해지지 않는다 → sub_flows 로 분리한다. axes: actor_role: [guest, user_with_perm, user_without_perm, admin] endpoint: [request, verify, cancel] challenge_owner: [self, other, none] - challenge_status: [sent, verified, expired, cancelled, missing] - modal_trigger_state: [challenge_id_present, challenge_id_absent] exclusions: - { challenge_owner: none, endpoint: verify, reason: "verify 는 challenge 모델 바인딩 필요 — none 시 404 (가드 통과 후 모델 미해석)" } - { challenge_owner: none, endpoint: cancel, reason: "cancel 도 challenge 모델 바인딩 필요" } - - { actor_role: guest, challenge_owner: self, reason: "guest 는 user_id 미보유 — self 매칭 무의미" } - - { actor_role: guest, challenge_owner: other, reason: "guest 는 scope 검사 스킵으로 owner 무관" } - - { modal_trigger_state: challenge_id_absent, endpoint: cancel, reason: "모달 if 가드로 호출 자체 스킵 — endpoint 액션 미발생" } + - { actor_role: guest, challenge_owner: self, reason: "guest 는 user_id 미보유 — self 매칭이 성립하지 않는다 (guest 가 다루는 challenge 는 항상 owner=other 로 표현된다)" } + - { endpoint: request, challenge_owner: self, reason: "발급은 대상 challenge 가 아직 없다 — owner 축이 성립하지 않음" } + - { endpoint: request, challenge_owner: other, reason: "동일 — 발급 시점에는 소유자 판정 대상이 없다" } + +sub_flows: + - id: challenge_status_outcomes + description: | + 가드를 통과한 뒤 challenge 상태(sent·verified·expired·cancelled·missing)가 가르는 결과. + 가드 판정과 곱해지지 않는다 — 권한이 없으면 상태와 무관하게 403 이고, + 권한이 있으면 상태가 404/422 를 가른다. + effects: + - missing_challenge_returns_404_after_guard_pass + - permission_middleware_resolves_challenge_via_route_model_binding + - id: role_permission_seeding + description: | + 역할별 IDV 권한 시딩과 scope 메타. 요청 시점 판정이 아니라 그 판정이 읽는 데이터의 준비 단계다. + effects: + - role_permission_seeder_syncs_idv_permissions_with_owner_metadata + - role_permission_seeder_assigns_self_scope_to_user_role_idv_perms + - role_permission_seeder_grants_idv_perms_to_guest_without_scope + - guest_role_idv_permission_check_via_guest_role_cache + - id: modal_cancel_sequence + description: | + 프론트 모달 취소 버튼의 액션 시퀀스. 축: modal_trigger_state(challenge_id_present·challenge_id_absent). + 서버 가드와 독립이며, challenge_id 부재 시에는 호출 자체가 스킵되어 엔드포인트에 도달하지 않는다. + effects: + - modal_cancel_button_calls_server_cancel_api_first + - modal_cancel_apiCall_uses_global_identityChallenge_challenge_id + - modal_cancel_apiCall_skipped_when_challenge_id_absent + - modal_cancel_apiCall_failure_does_not_block_modal_close + - modal_cancel_sequence_order_is_apiCall_resolve_close + - admin_basic_modal_cancel_flow_matches_basic_template + - id: route_contract + description: | + 라우트 계약 — 미들웨어 구성과 확장 지점. 특정 행위자·엔드포인트 조합의 산물이 아니다. + effects: + - cancel_endpoint_via_permission_middleware_does_not_require_legacy_auth_sanctum + - request_challenge_route_validation_rules_extensible_via_hook_filter + - verify_challenge_route_validation_rules_extensible_via_hook_filter + - cancel_form_request_authorize_returns_true_letting_route_middleware_decide effects: - guest_can_request_challenge @@ -61,11 +101,14 @@ effects: test_files: - tests/Feature/Api/Identity/PermissionGuardTest.php + - tests/Feature/Api/Identity/IdentityPermissionContractTest.php - tests/Feature/Api/Identity/IdentityChallengeFlowTest.php - tests/Feature/Api/Identity/IdentityChallengeShowTest.php - - tests/Feature/Api/Identity/IdentityCallbackTest.php - - tests/Feature/Api/Auth/RegisterPolicyDrivenSignupTest.php - - tests/Feature/Api/Auth/ForgotPasswordViaIdentityTest.php - - tests/Feature/Identity/MailIdentityProviderDispatchTest.php + +# 본 매니페스트의 test_files 에서 제외한 인접 테스트와 그 사유: +# - IdentityCallbackTest / RegisterPolicyDrivenSignupTest / ForgotPasswordViaIdentityTest / +# MailIdentityProviderDispatchTest 는 IDV "흐름"(콜백·가입 정책·비밀번호 재설정·메일 발송)을 +# 검증하며, 본 매니페스트가 선언한 권한 가드 effect 를 단언하지 않는다. +# 인접하다는 이유로 test_files 에 두면 "이 파일이 가드를 떠받친다" 는 거짓 근거가 된다. - templates/_bundled/sirsoft-basic/__tests__/layouts/identity-challenge-modal.test.tsx - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-identity-challenge-modal.test.tsx diff --git a/tests/scenarios/layout-editor-action-flex-condition.yaml b/tests/scenarios/layout-editor-action-flex-condition.yaml index aba1384c..5d0cefbd 100644 --- a/tests/scenarios/layout-editor-action-flex-condition.yaml +++ b/tests/scenarios/layout-editor-action-flex-condition.yaml @@ -2,27 +2,69 @@ feature: 레이아웃 편집기 액션 편집 · flex 편집 · 표시 조건 phase: 5 session: S7 issue: 383 +# axis 의미: +# 본 매니페스트가 묶는 세 가지(액션 편집 / flex 편집 / 표시 조건)는 편집기 속성 패널의 +# 서로 다른 탭이며 상호작용하지 않는다. flex 방향이 액션 레시피 조립을 바꾸지 않고, +# 조건 조합자가 flex 정렬을 바꾸지 않는다. 세 도메인의 축을 곱하면 134만 조합이 되지만 +# 그중 실재하는 판정은 도메인 내부의 것뿐이다. +# +# axes 로 남긴 것: 액션 레시피 × 후속 체인. +# 레시피 종류에 따라 후속 체인이 성립하는지가 갈리므로(openModal 은 체인 없음) +# 이 둘만 실제로 교차한다. axes: - event_kind: [onClick, onHover, onComponentEvent, lifecycle_onMount, lifecycle_onUnmount] action_recipe: [goToPage, showMessage, changeState, refreshData, callServerThen, openModal_lazy_id, custom_extension_recipe] - followup_chain: [none, single_success, single_error, success_and_error_both, nested_multi_step] - flex_role: [container, item, auto_div_detected_as_flex, auto_div_detected_as_block] - flex_direction: [row, column] - flex_wrap: [no_wrap, wrap] - flex_justify: [start, center, end, between, around] - flex_align: [start, center, end, stretch] - item_grow: [fixed, fill, ratio] - condition_category: [A_preset, B_datasource, C_array, D_field, E_state, F_route, G_form, H_ability] - condition_combinator: [single, and, or, mixed_and_or] - condition_param_kind: [no_param, ds_id, state_key, field_name, value_literal, ability_key] - click_in_edit_mode_should_be_intercepted: [yes_navigate, yes_a_href, no_static] + followup_chain: [none, success_and_error_both] exclusions: - - { action_recipe: openModal_lazy_id, followup_chain: nested_multi_step, + - { action_recipe: openModal_lazy_id, followup_chain: success_and_error_both, reason: "openModal 은 onSuccess/onError 체인 의미 없음 — 즉시 모달 오픈" } - - { flex_role: item, flex_direction: row, - reason: "item 편집은 부모 컨테이너 컨텍스트에서만 — 방향 축 무관" } - - { condition_category: A_preset, condition_param_kind: ds_id, - reason: "A 프리셋은 파라미터 없음" } + - { action_recipe: goToPage, followup_chain: success_and_error_both, + reason: "페이지 이동은 응답이 없어 후속 체인이 성립하지 않음" } + - { action_recipe: showMessage, followup_chain: success_and_error_both, + reason: "토스트 표시는 응답이 없어 후속 체인이 성립하지 않음" } + - { action_recipe: changeState, followup_chain: success_and_error_both, + reason: "상태 변경은 동기 — 후속 체인 없음" } + - { action_recipe: refreshData, followup_chain: success_and_error_both, + reason: "refreshData 레시피의 build 에 onSuccess/onError 자리가 없다(refetchDataSource + dataSourceId 뿐) — 후속 체인을 갖는 레시피는 callServerThen 하나다" } + +sub_flows: + - id: action_event_slot + description: | + 액션이 걸리는 이벤트 슬롯. 레시피 조립 결과와 곱해지지 않는다 — 같은 레시피가 + 어느 슬롯에 걸리든 산출 JSON 은 동일하고, 슬롯은 스펙 선언 여부만 가른다. + 축: event_kind(onClick·onHover·onComponentEvent·lifecycle_onMount·lifecycle_onUnmount) + effects: + - spec_undeclared_event_hides_action_slot + - nested_action_list_widget_assembles_recursive_actions + - action_recipe_reverse_matches_existing_action_to_recipe + - action_recipe_does_not_expose_handler_jargon_to_user + - id: flex_editing + description: | + flex 컨테이너/아이템 편집. 액션·조건과 무관한 스타일 축이다. + 축: flex_role(container·item·auto_div_detected_as_flex·auto_div_detected_as_block) × + flex_direction(row·column) × flex_wrap × flex_justify × flex_align × item_grow + effects: + - flex_container_classToken_emits_per_spec_apply_type + - flex_auto_detection_uses_computed_style_not_classname + - flex_auto_detection_works_across_tailwind_bootstrap_native_css + - flex_item_align_self_emits_correct_recipe + - flex_item_order_property_set + - flex_make_box_button_when_not_rendered_as_flex + - id: display_condition + description: | + 표시 조건(if) 빌더. 조건 카테고리 × 조합자 × 파라미터 종류가 이 도메인 안에서 교차한다. + 축: condition_category(A~H) × condition_combinator(single·and·or·mixed_and_or) × + condition_param_kind(no_param·ds_id·state_key·field_name·value_literal·ability_key) + effects: + - condition_recipe_substitutes_params_into_expr_template + - condition_recipe_wraps_entire_combined_expression_in_single_braces + - condition_builder_does_not_emit_nested_interpolation_braces + - condition_recipe_uses_optional_chaining_for_null_safety + - condition_recipe_array_length_uses_length_nullish_zero_fallback + - condition_synthesized_expr_evaluates_via_condition_evaluator + - condition_builder_reverse_matches_single_preset + - condition_builder_preserves_advanced_handwritten_expr + - condition_param_widget_dispatches_via_widget_registry + effects: - action_recipe_build_emits_correct_handler_name_and_top_level_target - action_recipe_top_level_onSuccess_onError_keys_not_in_params diff --git a/tests/scenarios/layout-editor-chrome-controls.yaml b/tests/scenarios/layout-editor-chrome-controls.yaml index 7d58053f..590b3076 100644 --- a/tests/scenarios/layout-editor-chrome-controls.yaml +++ b/tests/scenarios/layout-editor-chrome-controls.yaml @@ -2,23 +2,64 @@ feature: 레이아웃 편집기 chrome 컨트롤 (디바이스 custom width / phase: 3 session: S5b-pre issue: 383 +# axis 의미: +# 본 매니페스트가 묶는 것은 "편집기 chrome" 이라는 화면 영역이지 하나의 상호작용 +# 흐름이 아니다. 디바이스 미리보기 / 툴바 / 코드편집기 진입 / 오버레이 배치 / 팔레트는 +# 서로 곱해지지 않는다 (툴바 버튼 상태가 오버레이 배치 판정을 바꾸지 않는다). +# 그래서 상호작용이 실재하는 축만 axes 에 두고, 나머지는 sub_flows 로 분리한다. +# +# axes 로 남긴 것: 미리보기 디바이스 × custom 폭 입력. +# custom 폭은 device=custom 일 때만 반영되고 그 외 디바이스에서는 무시되어야 하므로 +# 두 축이 실제로 교차한다 (무시 여부가 조합으로 갈린다). axes: preview_device: [desktop, tablet, mobile, custom] - custom_width_input: [below_min_320, in_range, above_max_1920, non_numeric] - toolbar_action: [exit_clean, exit_dirty_confirm, edit_code_navigate, save_in_progress, save_done] - toolbar_button_state: [enabled_hover, disabled_no_hover, focused_after_click] - edited_template_type: [user, admin] - code_editor_entry: [from_wysiwyg_with_route, direct_route_query, file_select_in_editor, extension_select_in_editor] - selected_box_size: [normal_ge_44, small_lt_44_width, small_lt_44_height, scaled_small] - affordance_kind: [info_menu, base_lock, extension_lock, nav_route_in_tree, nav_unreachable, insertion_cross] - palette_entry_type: [basic, composite, layout] + custom_width_input: [below_min_320, in_range, above_max_1920] exclusions: - - { toolbar_action: edit_code_navigate, toolbar_button_state: disabled_no_hover, - reason: "onEditCode 미주입(disabled) 이면 navigate 자체가 일어나지 않음" } - - { custom_width_input: non_numeric, preview_device: desktop, - reason: "custom width 입력은 preview_device=custom 일 때만 노출" } - - { code_editor_entry: extension_select_in_editor, edited_template_type: user, - reason: "확장 선택은 라우트 없음 → ?route= 를 비움. 템플릿 타입과 무관한 동작" } + - { preview_device: desktop, custom_width_input: below_min_320, + reason: "custom 이 아닌 디바이스는 custom 폭을 무시 — 대표로 in_range 1건만 검증" } + - { preview_device: desktop, custom_width_input: above_max_1920, reason: "동일" } + - { preview_device: tablet, custom_width_input: below_min_320, reason: "동일" } + - { preview_device: tablet, custom_width_input: above_max_1920, reason: "동일" } + - { preview_device: mobile, custom_width_input: below_min_320, reason: "동일" } + - { preview_device: mobile, custom_width_input: above_max_1920, reason: "동일" } + +sub_flows: + - id: toolbar_actions + description: | + 툴바 버튼(나가기 / 코드편집 / 저장)의 동작과 시각 상태. 미리보기 디바이스와 + 곱해지지 않는다 — 어느 디바이스에서 보든 툴바 동작은 같다. + 축: toolbar_action(exit_clean·edit_code_navigate·save_in_progress·save_done) × + toolbar_button_state(enabled_hover·disabled_no_hover·focused_after_click) × + edited_template_type(user·admin — 나가기 목적지 분기) + effects: + - toolbar_exit_navigates_to_template_list_by_type + - toolbar_edit_code_opens_text_layout_editor_in_new_window_with_route + - toolbar_save_shows_spinner_while_pending_then_restores + - toolbar_buttons_hover_transition_when_enabled_not_when_disabled + - toolbar_buttons_no_residual_focus_outline_after_click + - id: code_editor_route_sync + description: | + 코드편집기(텍스트 화면)의 `?route=` 쿼리 동기화. 진입 경로별로 쿼리를 싣거나 비운다. + 축: code_editor_entry(from_wysiwyg_with_route·direct_route_query·file_select_in_editor·extension_select_in_editor) + effects: + - code_editor_restores_layout_from_route_query + - code_editor_file_select_syncs_route_query + - code_editor_extension_select_clears_route_query + - id: overlay_affordance_placement + description: | + 선택 박스 크기에 따른 오버레이 버튼 배치. 툴바·팔레트와 무관한 기하 판정이다. + 축: selected_box_size(normal_ge_44·small_lt_44_width·small_lt_44_height·scaled_small) × + affordance_kind(info_menu·base_lock·extension_lock·nav_route_in_tree·nav_unreachable·insertion_cross) + effects: + - overlay_affordances_inside_when_box_ge_44 + - overlay_affordances_outside_when_box_lt_44_no_overlap + - insertion_affordances_cross_layout_when_box_lt_44_no_overlap_no_clip + - id: palette_entry_badge + description: | + 팔레트 카드의 React 컴포넌트 태그 배지. 축: palette_entry_type(basic·composite·layout) + effects: + - palette_card_shows_react_component_tag_badge + effects: - preview_custom_device_uses_clamped_input_width_as_override - preview_custom_width_clamped_320_to_1920 diff --git a/tests/scenarios/layout-editor-route-connected-extensions.yaml b/tests/scenarios/layout-editor-route-connected-extensions.yaml index 1a421d7c..4ca83303 100644 --- a/tests/scenarios/layout-editor-route-connected-extensions.yaml +++ b/tests/scenarios/layout-editor-route-connected-extensions.yaml @@ -2,18 +2,42 @@ feature: 레이아웃 편집기 라우트↔모달/확장 연결 목록 (화면 phase: post session: route-connected-extensions issue: 383 +# axis 의미: +# 이 기능의 유일한 상호작용 매트릭스는 "무엇을(connection_kind) / 호스트가 맞는가(host_match) / +# 몇 건인가(conn_count)" 셋이다. 그룹 UI 상태와 편집 모드 전환은 이 셋과 곱해지지 않고 +# 독립적으로 동작하므로 sub_flows 로 분리한다 (곱하면 640 조합이 되지만 그중 의미 있는 +# 판정은 아래 9건뿐이다). axes: - ext_type: [overlay, extension_point] + connection_kind: [modal, overlay, extension_point] host_match: [match, no_match] - modal_host_match: [match, no_match] conn_count: [zero, one, many] - conn_group_state: [collapsed_default, expanded_after_click] - enter_mode_from_child: [modal_child, extension_child] - select_route_from_mode: [from_extension, from_modal, from_base, from_route] - host_highlight: [modal_child_host, extension_child_host] exclusions: - - { conn_count: zero, conn_group_state: expanded_after_click, - reason: "연결 0건이면 그룹 헤더 자체가 미부착되어 펼칠 대상이 없음" } + - { host_match: no_match, conn_count: one, + reason: "호스트 레이아웃이 불일치하면 부착 건수는 항상 0 — one/many 가 성립하지 않음" } + - { host_match: no_match, conn_count: many, + reason: "동일 — 불일치는 곧 0건" } + - { host_match: match, conn_count: zero, + reason: "호스트가 일치하면 최소 1건이 부착됨 — 0건은 불일치 경로에서만 발생" } + +sub_flows: + - id: connected_group_ui + description: | + 연결 그룹 헤더의 접힘/펼침. 부착 판정과 곱해지지 않는다 — 부착이 끝난 뒤의 + 표시 상태이며, 0건이면 헤더 자체가 없어 펼칠 대상이 없다. + effects: + - connected_group_collapsed_by_default + - connected_group_toggles_on_header_click + - id: edit_mode_transition + description: | + 연결 자식(모달/확장) 클릭으로 별도 편집 모드에 들어간 뒤의 상태 보존과 복귀. + 부착 매트릭스와 독립이며, 진입 경로(모달/확장)와 복귀 시작 모드(extension/modal/base/route)가 + 각각 자기 축을 가진다. + effects: + - connected_count_stable_when_entering_extension_edit_mode + - modal_child_click_enters_modal_edit_keeping_host_route_highlight + - extension_child_click_enters_extension_edit_keeping_host_route_highlight + - select_route_from_separate_edit_mode_restores_route_mode_and_renders_canvas + effects: - connected_extensions_attached_statically_by_host_layouts_without_canvas_load - connected_extension_point_attached_when_host_layout_in_host_layouts diff --git a/tests/scenarios/layout-expression-sandbox.yaml b/tests/scenarios/layout-expression-sandbox.yaml new file mode 100644 index 00000000..9c17dfd9 --- /dev/null +++ b/tests/scenarios/layout-expression-sandbox.yaml @@ -0,0 +1,52 @@ +feature: 레이아웃 표현식 평가 샌드박스 (KVE-2026-1915) + +description: | + 레이아웃 표현식은 AST 화이트리스트 인터프리터(SafeExpressionEvaluator)로 평가된다. + 정상 표현식(삼항·옵셔널체이닝·nullish·화살표함수·스프레드·템플릿리터럴·new Date)은 그대로 + 평가되고, constructor 체인/Function 생성자를 통한 샌드박스 탈출은 코드를 실행하지 못한다. + + 단위 테스트가 소스를 잠그지만 사용자가 실행하는 것은 배포 번들이므로, 브라우저에서 배포 + 번들의 공개 API(`G7Core.evaluateCondition`)를 직접 호출해 (a) 정상 회귀 없음 + (b) 익스플로잇 + 미실행(전역 canary `window.__g7jsi` 미설정) 양면을 잠근다. + +axes: + case: [safe_expression, arrow_template_literal, destructuring_param, constructor_escape, nonstring_key_escape, object_facade_safe] + +effects: + - same_expression_same_value_across_paths + - sandbox_escape_blocked + - dangerous_payload_does_not_set_global + +# 저장측 외부 URL 차단은 표현식 실행 축(case)과 곱해지지 않는 별개 관심사다 — case 는 "그 식이 +# 실행되는가" 를, URL 축은 "그 값이 외부 origin 을 가리키는가" 를 본다. 축으로 올리면 조합을 +# 관측하는 지점이 없어 어떤 칸도 커버되지 않으므로 sub_flow 로 분리한다. +sub_flows: + - id: external_url_storage_gate + description: | + 레이아웃 저장 시 외부 origin URL 을 거부하는 축. 세 갈래를 함께 잠근다. + + - 순회 대상: props·actions 뿐 아니라 init_actions(로드 시 자동 실행 → 외부 nav/apiCall 이 + 자동 리다이렉트·유출로 이어진다)까지. state/computed 는 데이터 값이라 의도적 제외이며, + 실제 위험은 바인딩되는 sink(컴포넌트 prop)에서 발생하고 그 sink 는 이미 검사된다. + - 값 형태: 규칙이 배열 트리뿐 아니라 **문자열 스칼라**도 처리해야 한다. 문자열 필드에서 + 조기 반환하면 그 부착이 조용한 no-op 이 된다. + - 부착 패리티: Store·Update·UpdateContent·UpdateExtensionContent 4 경로가 동일 강도여야 + 한다. 편집기 저장 경로만 빠지면 그 경로가 우회로다. + effects: + - init_actions_external_url_rejected + - same_origin_path_in_init_actions_allowed + - plain_string_value_is_inspected_not_skipped + - same_origin_plain_string_allowed + - rule_attached_to_every_layout_content_form_request + +test_files: + # 배포 번들 종단 (축·효과 마커 보유) + - tests/Playwright/specs/layout-expression-sandbox.spec.ts + # 소스 잠금 — 평가기 본체와 소비 싱크(바인딩·computed·scripts[].if·렌더 조건), 그리고 + # 저장 단계 거부. 축(case)은 배포 번들 E2E 가 커버하고 이 파일들은 효과를 떠받친다. + - resources/js/core/template-engine/__tests__/SafeExpressionEvaluator.test.ts + - resources/js/core/template-engine/__tests__/ConditionEvaluator.sandbox.test.ts + - resources/js/core/__tests__/TemplateApp.safeComputed.test.ts + - tests/Feature/Rules/SafeLayoutExpressionsTest.php + # sub_flow external_url_storage_gate 를 떠받친다. + - tests/Feature/Rules/NoExternalUrlsTest.php diff --git a/tests/scenarios/list-bulk-selection-scope.yaml b/tests/scenarios/list-bulk-selection-scope.yaml new file mode 100644 index 00000000..dd93d2bc --- /dev/null +++ b/tests/scenarios/list-bulk-selection-scope.yaml @@ -0,0 +1,44 @@ +feature: 일괄 처리 목록의 선택 범위 (DataGrid selectionScope) + +description: | + 체크박스 선택은 화면 밖(전역/로컬 상태)에 저장되므로, 검색·필터·페이지 이동으로 행이 + 목록에서 빠져도 선택은 남는다. 그 상태에서 일괄 처리를 누르면 운영자가 보고 있지도, + 체크하지도 않은 행이 대상이 된다. 확인 창은 건수만 말하고 대상을 부르지 않으므로 실행 + 전에 알아챌 방법이 없고, 서버는 정상 처리하므로 오류도 남지 않는다. + + 수정: DataGrid 가 `selectionScope` 를 갖고, `"page"` 인 목록은 지금 그려진 행에 없는 + 선택을 버린다. 선택 자체가 저장 대상인 폼(쿠폰 적용 대상 고르기 등)은 `"free"` 로 + 종전 동작을 유지한다. 값은 소비 화면이 반드시 명시하며 정적 검사가 미선언을 차단한다. + + 범위: 저장소의 모든 소비 화면 16곳 (page 12 / free 4). + +axes: + scope: [page, free] + transition: [search_filter_change, page_change, initial_mount] + +exclusions: + - { scope: free, transition: page_change, reason: "free 는 정의상 어떤 전이에서도 선택을 건드리지 않는다 — 전이별 분해가 의미 없어 대표 1건(search_filter_change)으로 고정" } + - { scope: free, transition: initial_mount, reason: "동일" } + +effects: + - hidden_row_selection_dropped + - visible_row_selection_survives + - selection_cleaned_even_when_selectable_off + - free_scope_leaves_selection_untouched + - no_callback_when_nothing_to_drop + - bulk_button_disabled_when_selection_empties + - bulk_request_carries_only_visible_checked_rows + +test_files: + - templates/_bundled/sirsoft-admin_basic/src/components/composite/__tests__/DataGrid.test.tsx + - tests/Playwright/specs/admin/list-bulk-selection-scope.spec.ts + +rules_layer_coverage: + - rule: layout-datagrid-selection-scope + coverage: | + 소비 화면 전수(16곳)가 scope 를 명시했는지는 테스트가 아니라 룰이 증명한다 — + 화면을 axis 로 두면 화면이 늘 때마다 테스트를 복제해야 하고, 정작 새로 생긴 + 화면의 누락은 잡히지 않는다. 룰은 onSelectionChange 가 배선된 DataGrid 전수에서 + 미선언·비허용값을 차단한다. + 설계상 free 고정 화면: 쿠폰 적용 대상 선택기(여러 페이지에 걸쳐 고른다), + 주문 상세 클레임 대상 선택기(선택이 곧 입력값). diff --git a/tests/scenarios/member-order-change-address.yaml b/tests/scenarios/member-order-change-address.yaml index 8e1996d5..0fe6273f 100644 --- a/tests/scenarios/member-order-change-address.yaml +++ b/tests/scenarios/member-order-change-address.yaml @@ -25,10 +25,19 @@ description: | axes: actor: [member, guest] change_mode: [saved, manual] + address_region: [domestic, intl, intl_without_country_code] e2e_browser: [chromium] exclusions: - { actor: guest, change_mode: saved, reason: "비회원은 주소록이 없어 saved(저장된 배송지 선택) 모드 미지원 — 직접입력만" } + - { change_mode: saved, address_region: intl_without_country_code, reason: "저장된 주소의 country_code 가 그대로 승계되므로 '국가 코드 미전송' 조합이 성립하지 않음" } + +# saved × intl 은 종전에 "저장된 주소를 그대로 승계하므로 조합이 성립하지 않는다" 는 사유로 +# 제외돼 있었다. 그 사유가 사실이 아니었다 — address_id 분기가 국내 6필드만 재구성해 +# 해외 주소를 고르면 주소가 통째로 사라졌고, 제외된 칸이 정확히 결함이 살던 자리였다. + +# 실패 원인 구분(도메인 422 / 검증 422 / 인프라 500)은 입력 axis 가 아니라 결과 축이므로 +# effects 로 검증한다. 인프라 500 은 서비스 mock 으로만 유발 가능해 PHPUnit 이 SSoT 다. effects: # 모달 컨텍스트 — _global 전달 (회원/비회원 공통) @@ -55,6 +64,21 @@ effects: - guest_update_shipping_address_succeeds_with_valid_token - guest_update_shipping_address_blocked_404_without_or_invalid_token - guest_update_shipping_address_validation_422_when_recipient_fields_missing + # 예외 → 상태코드 분리 (공개 #104) — 회원/비회원 공통 + - update_shipping_address_domain_exception_returns_422_with_cannot_modify_address + - update_shipping_address_infrastructure_exception_returns_500_not_422 + - update_shipping_address_500_response_excludes_exception_message + # 국내/해외 주소 조합 정합성 (부수 발견 F3) + - shipping_address_country_code_is_the_axis_for_domestic_vs_intl_validation + - shipping_address_intl_combination_without_country_code_returns_422_validation_errors + - shipping_address_domestic_country_code_requires_zipcode_and_address + - shipping_address_foreign_country_code_requires_address_line_1_intl_city_intl_postal_code + - shipping_address_switching_to_intl_clears_domestic_columns_with_empty_string + - shipping_address_switching_to_domestic_clears_intl_columns_with_null + - shipping_address_manual_domestic_input_on_intl_order_is_saved_not_discarded + # 저장된 배송지(address_id) 승계 정합성 + - shipping_address_saved_intl_address_preserves_intl_columns + - shipping_address_saved_mode_preserves_delivery_memo_from_request scenarios: - id: member_change_address_saved_e2e @@ -82,9 +106,51 @@ scenarios: effects: - guest_update_shipping_address_blocked_404_without_or_invalid_token + - id: change_address_error_class_separation + description: | + 배송지 변경 실패를 원인별로 구분한다 (공개 #104). 도메인 규칙 위반(배송 전 상태 아님)은 + 422 + 기존 안내, 그 외 예외(인프라/코드 결함)는 500. 종전에는 모두 422 로 뭉개져 + 인프라 장애가 "배송 전에만 변경 가능" 이라는 엉뚱한 입력 오류로 위장됐다. + 500 축은 서비스 mock 이 필요해 PHPUnit 이 SSoT 이고, 브라우저는 200/422 축만 담당한다. + effects: + - update_shipping_address_domain_exception_returns_422_with_cannot_modify_address + - update_shipping_address_infrastructure_exception_returns_500_not_422 + - update_shipping_address_500_response_excludes_exception_message + + - id: change_address_domestic_intl_combination + description: | + country_code 를 국내/해외 판정의 기준축으로 삼는다 (부수 발견 F3). 종전에는 zipcode/address 를 + address_line_1 유무로만 강제해, 해외 필드만 채우고 country_code 를 생략하면 검증을 통과한 뒤 + 서비스가 국내 분기로 떨어져 NOT NULL 컬럼에 null 을 쓰다 SQL 무결성 위반이 났다. + 전환 시 반대편 컬럼을 초기화해 구 주소 잔존(2차 결함)도 함께 막는다. + effects: + - shipping_address_country_code_is_the_axis_for_domestic_vs_intl_validation + - shipping_address_intl_combination_without_country_code_returns_422_validation_errors + - shipping_address_domestic_country_code_requires_zipcode_and_address + - shipping_address_foreign_country_code_requires_address_line_1_intl_city_intl_postal_code + - shipping_address_switching_to_intl_clears_domestic_columns_with_empty_string + - shipping_address_switching_to_domestic_clears_intl_columns_with_null + - shipping_address_manual_domestic_input_on_intl_order_is_saved_not_discarded + + - id: change_address_saved_address_carry_over + description: | + 저장된 배송지(address_id) 선택 시 그 주소를 손실 없이 승계한다. 국내 6필드만 + 재구성하던 종전 형태에서는 해외 배송지를 고르면 해외 컬럼이 전부 null 로 덮이고 + 국내 컬럼도 빈 문자열이라 주소가 통째로 사라졌으며, 함께 보낸 배송 메모도 버려졌다. + 응답은 200 이고 오류도 남지 않아 저장 시점에는 드러나지 않는다. + effects: + - shipping_address_saved_intl_address_preserves_intl_columns + - shipping_address_saved_mode_preserves_delivery_memo_from_request + test_files: - # 백엔드 (비회원 endpoint — 성공/토큰가드 404/검증 422) + # 백엔드 (비회원 endpoint — 성공/토큰가드 404/검증 422/국내·해외 조합) - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/Public/OrderControllerTest.php + # 백엔드 (회원 endpoint — 예외→상태코드 분리, 국내/해외 조합, 왕복 전환 초기화) + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Http/Controllers/User/OrderShippingAddressTest.php + # 주문 도메인의 다른 액션(취소·구매확정·재주문·입금확인·쿠폰 다운로드) 페어는 + # 배송지 변경의 축이 아니다 → modules/_bundled/sirsoft-ecommerce/tests/scenarios/order-action-exception-status.yaml + # 백엔드 (generic catch 상태코드 전수 계약 — 코어 + 전 번들 확장 컨트롤러) + - tests/Feature/Http/GenericCatchStatusCodeContractTest.php # 프론트엔드 핸들러 단위 (endpoint/body/토큰 분기) - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/handlers/userChangeShippingAddressHandlers.test.ts # 프론트엔드 모달 구조 (Vitest — _global 바인딩, 회원/비회원 분기, 핸들러 호출, 스피너) diff --git a/tests/scenarios/password-policy.yaml b/tests/scenarios/password-policy.yaml index c8634ccf..ee77db39 100644 --- a/tests/scenarios/password-policy.yaml +++ b/tests/scenarios/password-policy.yaml @@ -25,15 +25,42 @@ axes: - register - reset_password - change_password + - verify_password - admin_create_user - admin_update_user - update_profile min_length_setting: [default, raised, lowered] special_char_setting: [required, not_required] input_state: [satisfies_policy, violates_length, violates_special] + # 오류 문구의 :attribute 치환 라벨 축 (공개이슈 #113) + # 비밀번호 외 범용 키(ids)도 같은 결함군이라 함께 고정한다 — + # 전역 사전 하나를 도메인 문구로 덮으면 그 키를 쓰는 모든 화면이 오염된다. + attribute_label_scope: [generic_password, smtp_password, generic_ids, user_scoped_ids] + locale: [ko, en] exclusions: - { special_char_setting: not_required, input_state: violates_special, reason: "특수문자 미요구 설정에서는 위반 상태가 성립하지 않음" } + - { attribute_label_scope: smtp_password, entry_point: register, reason: "SMTP 라벨은 메일 설정 화면 전용 — 비밀번호를 정하는 경로에는 나타나지 않아야 한다" } + - { attribute_label_scope: smtp_password, entry_point: reset_password, reason: "동일" } + - { attribute_label_scope: smtp_password, entry_point: change_password, reason: "동일" } + - { attribute_label_scope: smtp_password, entry_point: admin_create_user, reason: "동일" } + - { attribute_label_scope: smtp_password, entry_point: admin_update_user, reason: "동일" } + - { attribute_label_scope: smtp_password, entry_point: update_profile, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: register, reason: "ids 라벨은 목록 대상 일괄 처리 화면 전용 — 비밀번호를 정하는 경로에는 나타나지 않는다" } + - { attribute_label_scope: generic_ids, entry_point: reset_password, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: change_password, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: verify_password, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: admin_create_user, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: admin_update_user, reason: "동일" } + - { attribute_label_scope: generic_ids, entry_point: update_profile, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: register, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: reset_password, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: change_password, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: verify_password, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: admin_create_user, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: admin_update_user, reason: "동일" } + - { attribute_label_scope: user_scoped_ids, entry_point: update_profile, reason: "동일" } + - { attribute_label_scope: smtp_password, entry_point: verify_password, reason: "동일" } effects: - policy_rule_reads_min_length_from_settings @@ -46,10 +73,19 @@ effects: - empty_security_settings_fall_back_to_defaults - saved_security_settings_are_visible_on_reload - admin_screen_min_length_bounds_follow_settings_limits + # 라벨 축 (공개이슈 #113) + - policy_message_attribute_uses_generic_password_label + - smtp_label_never_appears_in_password_setting_paths + - smtp_screens_keep_their_domain_labels + - generic_labels_hold_in_every_locale + - bulk_status_screen_keeps_user_scoped_ids_label + - generic_ids_label_stays_generic_on_other_screens + - generic_attribute_keys_carry_no_domain_prefix test_files: - tests/Feature/Auth/PasswordPolicyTest.php - tests/Feature/Settings/SettingsBoundaryContractTest.php + - tests/Feature/Validation/GlobalAttributeLabelTest.php - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-security-password-policy.test.ts rules_layer_coverage: diff --git a/tests/scenarios/plugin-admin-menu-sync.yaml b/tests/scenarios/plugin-admin-menu-sync.yaml new file mode 100644 index 00000000..7eb35b80 --- /dev/null +++ b/tests/scenarios/plugin-admin-menu-sync.yaml @@ -0,0 +1,45 @@ +feature: 플러그인 관리자 메뉴 동기화 (Plugin Admin Menu Sync) + +description: | + 플러그인도 자기 화면을 가질 수 있으므로 `getAdminMenus()` 로 관리자 메뉴를 선언한다. + 그 선언을 실제 메뉴 행으로 만드는 것은 코어(PluginManager)의 책임이다. + + 이 동기화가 특정 진입점에만 걸려 있으면 나머지 진입점에서는 메뉴가 조용히 없다 — + 예외도 경고도 로그도 남지 않고, 선언한 화면이 주소를 직접 입력해야만 닿는 상태가 된다. + 실제로 두 번 드러났다. + + 1. 활성화(activate)에만 있었을 때: 이미 활성인 사이트가 업데이트로 새 화면을 받아도 + 메뉴가 생기지 않았다 (공개 #115 검수 F1, 브라우저 실측 — 반영 후 메뉴 0건) + 2. 선언형 산출물 동기화에만 있었을 때: 그 경로를 지나는 것은 업데이트뿐이라, 신규 설치는 + 플러그인이 activate() 를 직접 구현한 경우에만 메뉴가 생겼다. AbstractPlugin 의 기본 + activate() 는 `return true` 뿐이므로 getAdminMenus() 만 선언한 플러그인은 설치·활성화를 + 마쳐도 메뉴가 없었다. 모듈은 installModule() 이 createModuleMenus() 를 직접 호출해 + 이 구멍이 없었다 (형제 경로 비대칭) + + 그래서 판정 대상은 "메뉴가 만들어지는가" 하나가 아니라 **진입점마다 만들어지는가** 다. + 진입점은 선언 여부·재실행과 직교하므로 축이 아니라 sub_flow 로 둔다 — 축으로 묶으면 + "선언하지 않은 플러그인의 설치 경로" 같은 판정이 필요 없는 조합이 cross product 에 생긴다. + +axes: + declaration: [declared, not_declared] + run: [first_run, repeated_run] + +exclusions: + - { declaration: not_declared, run: repeated_run, reason: "선언이 없으면 몇 번을 실행해도 만들 것이 없다 — 1회 실행으로 판정이 끝난다" } + +sub_flows: + - id: lifecycle_entry_points + description: | + 설치·업데이트 어느 쪽으로 들어와도 같은 선언형 산출물 단계를 지나야 한다. + 단계 목록을 검사 쪽에 손으로 적어두면 새 산출물 종류가 늘었을 때 그 사실이 반영되지 + 않으므로, 두 경로 모두 소스에서 도출해 대조한다. + effects: + - plugin_install_path_syncs_declared_menus + +effects: + - plugin_admin_menu_synced_on_declarative_sync + - plugin_admin_menu_sync_is_idempotent + - plugin_without_menu_declaration_creates_nothing + +test_files: + - tests/Unit/Extension/PluginMenuSyncTest.php diff --git a/tests/scenarios/public-asset-cdn.yaml b/tests/scenarios/public-asset-cdn.yaml new file mode 100644 index 00000000..c161affb --- /dev/null +++ b/tests/scenarios/public-asset-cdn.yaml @@ -0,0 +1,89 @@ +feature: 공개 자산 직접 URL(CDN) 서빙 (공개#100) + +description: | + StorageInterface::url() 의 public 하드코딩을 완화하고 `core.storage.filter_url` + 훅을 신설해, 완전 공개 자산(상품/카테고리/리뷰/에디터 이미지)의 직접 URL(CDN) + 서빙을 옵트인으로 연다. 미설정 시 기존 PHP 스트리밍 100% 보존. + + 결정 사슬: + - url(): public 디스크 또는 filesystems.disks.{disk}.url 설정 디스크 → 직접 URL, + 그 외 null. 결과는 디스크 무관 core.storage.filter_url 훅 상시 통과(공급/수정/차단). + - 디스크 결정: 확장 개별 설정(public_asset_disk) > 코어 전역 + (drivers.public_asset_disk → core.storage.public_asset_disk) > 미설정(스트리밍). + 'none'/고아 디스크는 스트리밍 폴백. + - 소비처: 행 disk 로 url() 시도 → null 이면 기존 API 서빙 경로 폴백. + 서빙/삭제/이동은 행 disk 기준(혼재 운용). + - 권한 검사가 걸린 첨부(board/page/코어)는 배선 자체가 없음 — 직접 URL 이 + 권한을 우회하므로 스트리밍 유지가 보안 전제. + +coverage_strategy: pairwise + +axes: + disk_setting: [none, public, fake_cdn, s3_without_url] + consumer: [product, category, review, editor] + row_state: [legacy_local_row, new_remote_row] + override: [follow_core, module_override, none_override] + hook: [unregistered, supply, block] + e2e: [drivers_tab_card, ecommerce_settings_field, save_roundtrip] + +effects: + - url_null_for_disk_without_url_config + - url_returned_for_public_and_url_configured_disk + - blank_url_config_defended + - filter_url_hook_always_fires_with_six_context_keys + - hook_supply_modify_block_respected + - download_url_falls_back_to_api_path_when_direct_unavailable + - mixed_rows_resolve_per_row_disk + - serve_and_delete_follow_row_disk + - orphan_disk_falls_back_to_streaming + - none_override_forces_streaming_over_global + - settings_catalog_includes_plugin_registered_disks + - invalid_disk_rejected_with_422 + - board_response_strings_unchanged + - settings_load_recursion_free + - global_setting_injects_core_config_with_none_normalized + - seo_guard_passes_absolute_cdn_url_unmodified + - cross_origin_asset_request_omits_session_token + - s3_credentials_visible_when_public_asset_disk_is_s3 + +test_files: + - tests/Unit/Extension/Storage/CoreStorageDriverTest.php + - tests/Unit/Extension/Storage/ModuleStorageDriverTest.php + - tests/Unit/Extension/Storage/PluginStorageDriverTest.php + - tests/Unit/Extension/Storage/StorageCategoryDiskTest.php + - tests/Unit/Services/DriverRegistryServiceTest.php + - tests/Unit/Services/DriverExtensionIntegrationTest.php + - tests/Unit/Models/AttachmentUrlTest.php + - tests/Unit/Seo/ComponentHtmlMapperTest.php + - tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php + - tests/Unit/Providers/SettingsServiceProviderPublicAssetDiskTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Models/HasDirectAssetUrlTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductImageServiceRowDiskTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/CategoryImageServiceRowDiskTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceRowDiskTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/StorageDiskRecursionTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductServiceDetailImageUrlTest.php + - modules/_bundled/sirsoft-board/tests/Unit/AttachmentPreviewUrlTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Resources/PostResourceThumbnailUrlTest.php + - modules/_bundled/sirsoft-board/tests/Unit/Repositories/BoardRepositoryPopularAvatarTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Ckeditor5ImageUploadUrlTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/ImageServeServiceRowDiskTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/StorageDiskRecursionTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsCatalogAttachTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Http/Requests/StoreEcommerceSettingsRequestPublicAssetDiskTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/PluginSettingsPublicAssetDiskValidationTest.php + - templates/_bundled/sirsoft-admin_basic/tests/Playwright/specs/public-asset-storage.spec.ts + - modules/_bundled/sirsoft-ecommerce/tests/Playwright/specs/admin/public-asset-disk-override.spec.ts + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-public-asset-storage.test.tsx + - modules/_bundled/sirsoft-ecommerce/resources/js/__tests__/layouts/adminEcommerceSettingsPublicAssetDisk.test.tsx + - plugins/_bundled/sirsoft-ckeditor5/resources/js/__tests__/layouts/pluginSettingsPublicAssetDisk.test.tsx + - resources/js/core/api/__tests__/ApiClient.test.ts + - templates/_bundled/sirsoft-admin_basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx + - templates/_bundled/sirsoft-basic/__tests__/components/assetOrigin.crossOriginAuth.test.tsx + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-visibility.test.tsx + +rules_layer_coverage: + - rule: no-storage-disk-direct + coverage: 소비처는 StorageInterface 경유 — Storage facade 직접 호출 도입 없음 + - rule: repository-interface-injection + coverage: 서비스 시그니처 무변경 — contextual binding 만 카테고리 디스크로 교체 diff --git a/tests/scenarios/redirect-query-exception-exposure.yaml b/tests/scenarios/redirect-query-exception-exposure.yaml new file mode 100644 index 00000000..fca13c1f --- /dev/null +++ b/tests/scenarios/redirect-query-exception-exposure.yaml @@ -0,0 +1,53 @@ +feature: 실패 리다이렉트 쿼리의 예외 원문 비노출 (Redirect Query Exception Exposure) + +description: | + 결제 콜백은 실패 사유를 응답 본문이 아니라 **리다이렉트 쿼리스트링**으로 전달한다. + 그래서 응답 메시지 키를 보는 계약(`tests/Feature/Http/GenericCatchStatusCodeContractTest.php`) + 의 축 밖에 있다. 그 자리에 예외 원문을 실으면 브라우저 주소창과 참조 로그에 SQL + 상태코드·클래스명·경로가 남고, 결제 실패 페이지가 그 값을 그대로 출력하므로 구매자에게도 + 노출된다. + + 핵심 설계: + - 사유 **식별**은 쿼리의 `error` 코드가 담당한다. + - 사람에게 보일 **문구**는 다국어 키로 해석한 문장이어야 한다 (`errors.payment_failed`). + - 예외 원문은 로그에만 남긴다. + + 판정은 소스 전수 스캔이다. 결제대행사 플러그인은 컨트롤러를 `src/Controllers` 에 두고 + 그 밖의 확장은 `src/Http/Controllers` 를 쓰므로, 한 규약만 보면 다른 규약이 통째로 + 사각이 된다. 그래서 모집단이 실제로 그 플러그인들과 리다이렉트 조립 지점을 담고 있는지 + 판정기가 스스로 단언하는 축을 따로 둔다 — 모집단이 비면 위반 0 은 아무것도 증명하지 않는다. + + 면제 목록은 비어 있고, 늘어나도 줄어도(해소된 항목이 남아도) 실패한다. + +axes: + contract_check: [scanner_population, redirect_query_text] + +exclusions: [] + +effects: + - redirect_contract_scanner_population_covers_payment_gateway_plugins + - redirect_query_omits_exception_text_repository_wide + +scenarios: + - id: scanner_actually_sees_the_payment_gateway_convention + description: 판정기 모집단이 결제대행사 플러그인 4종과 리다이렉트 URL 조립 지점을 담고 있다 + effects: + - redirect_contract_scanner_population_covers_payment_gateway_plugins + + - id: no_controller_puts_exception_text_in_a_human_visible_redirect_param + description: 코어와 전 번들 확장의 컨트롤러 어디에도 리다이렉트 쿼리의 message/msg/reason/detail 자리에 예외 원문을 싣는 지점이 없다 + effects: + - redirect_query_omits_exception_text_repository_wide + +test_files: + - tests/Feature/Http/RedirectQueryExceptionTextContractTest.php + +notes: | + 이 매니페스트는 5차 감사에서 신설됐다. 대상 테스트는 브랜치가 추가했지만 `@scenario`· + `@effects` 가 하나도 없어 어느 매니페스트에도 묶이지 않았다. 형제 파일 + `GenericCatchStatusCodeContractTest.php` 는 매니페스트에 등록돼 있었는데 이 파일만 + 빠져 있었고, 마커도 선언도 없으면 test-scenario-coverage 의 두 방향 검사 어느 쪽에도 + 걸리지 않아 조용히 통과한다. + + 응답 본문 축은 `tests/scenarios/exception-response-exposure.yaml` 이 담당한다. 이쪽은 + 통로가 리다이렉트 쿼리라 노출 대상(구매자)과 판정 방식(소스 전수 스캔)이 달라 분리한다. diff --git a/tests/scenarios/s3-storage-driver.yaml b/tests/scenarios/s3-storage-driver.yaml new file mode 100644 index 00000000..02099562 --- /dev/null +++ b/tests/scenarios/s3-storage-driver.yaml @@ -0,0 +1,74 @@ +feature: S3 스토리지 드라이버 실동작 (공개 #99 / 내부 #563) + +description: | + 관리자 > 환경설정 > 드라이버의 S3 선택이 실제 파일 저장으로 이어지게 하는 결함 수정. + + 핵심 동작: + - league/flysystem-aws-s3-v3 / predis 코어 의존성 포함 (해석 시점 즉사 소멸) + - s3_endpoint / s3_use_path_style 신설 → filesystems.disks.s3.{endpoint,use_path_style_endpoint} 주입 + (S3 호환 스토리지 R2/MinIO/NCP 연결 경로) + - s3_region 3중 하드코딩 제거 → 형식 검증(regex ^[a-z0-9-]+$, max 64) 완화 + - 드라이버 가용성 서버 게이트: 어댑터 클래스/PHP 확장 부재 드라이버 저장·테스트 422 + - 코어 첨부 업로드 디스크: ATTACHMENT_DISK env > storage_driver=s3 → s3 > attachments + (기존 행은 행 disk 서빙 — 혼재 안전) + - 연결 테스트: 어댑터 선검사 + endpoint/path-style 반영, websocket 은 client/server + 양측 endpoint probe (백엔드 발송은 server endpoint 사용) + +axes: + region_shape: [aws_region, auto, custom_minio, invalid_format] + endpoint: [absent, custom_url, path_style] + driver_usability: [adapter_present, adapter_absent] + attachment_disk: [env_override, follow_s3, local_default] + +exclusions: + - { region_shape: invalid_format, endpoint: custom_url, reason: "리전 검증이 선행 422 — endpoint 축 도달 전 차단" } + - { region_shape: invalid_format, endpoint: path_style, reason: "동일" } + - { region_shape: invalid_format, attachment_disk: env_override, reason: "저장 실패 시 디스크 전환 미발생" } + - { region_shape: invalid_format, attachment_disk: follow_s3, reason: "동일" } + - { driver_usability: adapter_absent, endpoint: custom_url, reason: "가용성 게이트가 선행 422 — 어댑터는 코어 필수 의존성이라 부재 상태는 memcached 확장 축으로 대변" } + - { driver_usability: adapter_absent, endpoint: path_style, reason: "동일" } + - { driver_usability: adapter_absent, attachment_disk: follow_s3, reason: "저장 차단 시 디스크 전환 미발생" } + - { driver_usability: adapter_absent, attachment_disk: env_override, reason: "동일" } + - { driver_usability: adapter_absent, region_shape: invalid_format, reason: "리전 검증이 선행 422 — 가용성 축 도달 전 차단" } + - { driver_usability: adapter_absent, region_shape: auto, reason: "가용성 게이트는 드라이버 ID 만 판정 — 리전 값과 직교, aws_region 대표 조합으로 검증" } + - { driver_usability: adapter_absent, region_shape: custom_minio, reason: "동일" } + - { attachment_disk: env_override, region_shape: auto, reason: "디스크 전환 판정은 storage_driver 값만 본다 — 리전·endpoint 와 직교, aws_region×absent 대표 조합으로 검증" } + - { attachment_disk: env_override, region_shape: custom_minio, reason: "동일" } + - { attachment_disk: env_override, endpoint: custom_url, reason: "동일" } + - { attachment_disk: env_override, endpoint: path_style, reason: "동일" } + - { attachment_disk: follow_s3, region_shape: auto, reason: "디스크 전환 판정은 storage_driver 값만 본다 — aws_region×absent(단위)·custom_minio×path_style(Feature 왕복) 두 대표 조합으로 검증" } + - { attachment_disk: follow_s3, endpoint: custom_url, reason: "동일" } + - { attachment_disk: follow_s3, region_shape: aws_region, endpoint: path_style, reason: "동일" } + - { attachment_disk: follow_s3, region_shape: custom_minio, endpoint: absent, reason: "동일" } + - { region_shape: auto, endpoint: absent, reason: "R2 의 auto 리전은 커스텀 endpoint 전제 — endpoint 없는 auto 는 실사용 조합이 아님" } + - { region_shape: auto, endpoint: path_style, reason: "R2 는 virtual-host 스타일 — path_style 조합은 custom_minio 축이 대표" } + - { region_shape: custom_minio, endpoint: absent, reason: "MinIO 리전 값은 커스텀 endpoint 전제 — endpoint 없는 조합은 실사용이 아님" } + - { region_shape: custom_minio, endpoint: custom_url, reason: "MinIO 는 path-style 대표 조합으로 검증 — virtual-host 모드도 endpoint 주입 경로는 동일" } + +effects: + - s3_disk_resolves + - endpoint_injected + - attachment_upload_follows_driver + - unusable_driver_rejected + - invalid_region_rejected + - test_covers_server_websocket + - unauthenticated_returns_401 + # E2E/렌더 표면 효과 (Playwright·Vitest 가 마킹) + - region_free_input_mounted + - endpoint_field_mounted + - path_style_toggle_mounted + - local_roundtrip_preserves_new_keys + +# E2E 표면 명세 (문서화) — 커버리지 집계는 아래 test_files 의 @scenario/@effects 마킹이 담당 +e2e: + surfaces: [region_free_input, endpoint_field, path_style_toggle, save_gating] + +test_files: + - tests/Playwright/specs/admin/settings-drivers-s3-endpoint.spec.ts + - tests/Unit/Providers/SettingsServiceProviderS3ConfigTest.php + - tests/Unit/Services/DriverConnectionTesterTest.php + - tests/Unit/Services/DriverRegistryServiceUsabilityTest.php + - tests/Feature/Api/Admin/SettingsControllerS3ValidationTest.php + - tests/Feature/Api/Public/AttachmentS3DiskServingTest.php + - tests/Unit/Services/TemplateLayoutAttachmentServingTest.php + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-settings-drivers-s3-fields.test.tsx diff --git a/tests/scenarios/schedule-artisan-command-allowlist.yaml b/tests/scenarios/schedule-artisan-command-allowlist.yaml index 7b3801c9..0ea9e585 100644 --- a/tests/scenarios/schedule-artisan-command-allowlist.yaml +++ b/tests/scenarios/schedule-artisan-command-allowlist.yaml @@ -59,6 +59,7 @@ effects: - denylist_is_evaluated_before_the_allowlist - make_prefix_blocks_all_code_generating_commands - extension_owned_commands_are_allowed_by_namespace + - extension_owned_commands_resolved_without_console_registration - extension_gate_can_be_turned_off - extension_commands_only_accept_their_own_options # 사용법 제한 diff --git a/tests/scenarios/schedule-shell-interpreter-guard.yaml b/tests/scenarios/schedule-shell-interpreter-guard.yaml new file mode 100644 index 00000000..e17faf5c --- /dev/null +++ b/tests/scenarios/schedule-shell-interpreter-guard.yaml @@ -0,0 +1,76 @@ +feature: 예약 작업 Shell 인터프리터 인라인 코드 우회 차단 + +description: | + Shell 타입 스케줄은 범용 크론탭 대체다 — 그누보드 밖의 python/node/php/bash 스크립트를 + 주기 실행할 수 있어야 한다. 그래서 인터프리터를 통째로 막지 않고 화이트리스트에 등록할 수 + 있게 둔다. 위험한 것은 인터프리터 자체가 아니라 인터프리터에게 인라인 코드/명령 문자열을 + 넘기는 것이다. + + 종전 `isShellCommandAllowed()` 는 첫 토큰 basename 만 화이트리스트와 대조하고 뒤 인자를 + 검증하지 않았다. 운영자가 `bash` 를 등록하면 `bash -c id` 가 통과해 임의 OS 명령이 실행됐다 + (KVE-2026-1653). 이제 첫 토큰이 스크립트 실행형 인터프리터면 첫 인자(스크립트 자리)에 + 형태 규칙을 적용한다 — 하이픈 선두(인라인 코드 플래그) 거부, 절대경로 요구, `..`/`#` 금지, + basename≠artisan. 완전 거부형(env/awk/make/xargs/sudo/busybox 등)은 등재돼 있어도 거부한다. + 인터프리터가 아닌 일반 스크립트(backup.sh 등)는 기존 계약대로 통과한다. + + 저장 시점(store/update Rule)과 실행 시점(runSchedule 마지막 방어선)이 같은 판정 함수 + `inspectShellCommand()` 를 공유한다 — 한쪽만 고치면 DB 직접 수정이 우회로가 된다. + +axes: + command_class: [inline_code, script_ok, artisan_bypass, reject_binary, script_path, no_script, legit_plain_script] + enforcement_point: [validator_unit, store, update, run] + +cross_product: | + command_class × enforcement_point 이 핵심이다. 저장 시점과 실행 시점이 같은 판정 함수를 + 공유해야 하고(한쪽만 고치면 DB 직접 수정이 우회로가 된다), 명령 부류마다 거부 사유가 달라 + 운영자에게 보이는 메시지가 갈린다. + + 전 부류는 validator_unit 에서 전수 검증한다. store/update/run 은 같은 판정 함수를 호출할 + 뿐 별도 분기가 없으므로, HTTP·실행 계층에서는 대표 부류만 검증한다: + - store: inline_code(차단)·script_ok(허용)·artisan_bypass(차단)·reject_binary(차단) + - update: 저장된 타입 폴백 배선이 고유 축이므로 대표 1건(inline_code) + - run: 마지막 방어선의 대표 1건(inline_code) — Feature + Service 두 계층에서 + + caller_role(위임관리자 / 슈퍼관리자)은 별도 axis 로 두지 않는다. 판정은 권한과 무관한 + 단일 함수라 결과가 갈리지 않기 때문이다 — 슈퍼관리자도 인라인 코드를 저장할 수 없음을 + 대표 1건으로 고정한다(effects inline_code_flags_rejected). + +exclusions: + - { command_class: script_path, enforcement_point: store, reason: "경로 형태 판정은 validator 단일 지점 — HTTP 계층은 같은 Rule 을 호출할 뿐 별도 분기 없음" } + - { command_class: no_script, enforcement_point: store, reason: "동일 — 판정 분기 없음" } + - { command_class: legit_plain_script, enforcement_point: store, reason: "동일 — 정상 스크립트 허용은 script_ok(store)가 대표" } + - { command_class: script_ok, enforcement_point: update, reason: "update 고유 축은 '요청에 type 없을 때 저장 타입 폴백' 배선 — 판정은 store 와 같은 Rule, 대표 1건(inline_code)으로 검증" } + - { command_class: artisan_bypass, enforcement_point: update, reason: "동일 — 대표 1건으로 검증" } + - { command_class: reject_binary, enforcement_point: update, reason: "동일 — 대표 1건으로 검증" } + - { command_class: script_path, enforcement_point: update, reason: "동일 — 대표 1건으로 검증" } + - { command_class: no_script, enforcement_point: update, reason: "동일 — 대표 1건으로 검증" } + - { command_class: legit_plain_script, enforcement_point: update, reason: "동일 — 대표 1건으로 검증" } + - { command_class: script_ok, enforcement_point: run, reason: "실행 시점 방어선의 대표는 inline_code — 허용 스크립트 실행 회귀는 store(script_ok)가 대표" } + - { command_class: artisan_bypass, enforcement_point: run, reason: "저장 시점에 거부되어 DB 에 남지 않는다. 실행 시점 대표는 inline_code" } + - { command_class: reject_binary, enforcement_point: run, reason: "동일 — 실행 시점 대표는 inline_code" } + - { command_class: script_path, enforcement_point: run, reason: "동일 — 실행 시점 대표는 inline_code" } + - { command_class: no_script, enforcement_point: run, reason: "동일 — 실행 시점 대표는 inline_code" } + - { command_class: legit_plain_script, enforcement_point: run, reason: "정상 스크립트 실행 자체는 기존 shell 실행 테스트가 이미 고정 — 본 기능의 신규 축이 아님" } + +effects: + # 판정 — 인터프리터 정책 + - inline_code_flags_rejected + - interpreter_script_files_still_run + - artisan_via_shell_rejected + - reject_binaries_blocked_even_when_allowlisted + - script_path_traversal_rejected + - interpreter_without_script_rejected + - plain_scripts_still_run_without_path_restrictions + - rejection_reason_is_reported_per_category + # 엔드투엔드 — 저장·실행 두 지점 + - update_falls_back_to_the_stored_type_when_the_request_omits_it + - run_time_last_line_of_defense_blocks_inline_code + +# E2E axis 없음: 판정은 전부 서버측이고 레이아웃 JSON·컴포넌트 TSX·엔진 TS 변경이 없다. +# 예약 작업 관리 화면은 config/core.php 에서 is_active=false 로 비노출 상태라 브라우저 경로 +# 자체가 존재하지 않는다 (frontend-change-requires-e2e 룰의 대상이 아님). + +test_files: + - tests/Unit/Support/ScheduleCommandValidatorTest.php + - tests/Feature/Security/ScheduleCommandGuardTest.php + - tests/Unit/Services/ScheduleServiceTest.php diff --git a/tests/scenarios/search-driver-fallback.yaml b/tests/scenarios/search-driver-fallback.yaml new file mode 100644 index 00000000..d165ddc8 --- /dev/null +++ b/tests/scenarios/search-driver-fallback.yaml @@ -0,0 +1,36 @@ +feature: 검색엔진 드라이버 폴백 가드 (A5b) + +description: | + 드라이버 폴백 가드는 `DriverRegistryService` 의 카테고리 레지스트리를 순회하며, 저장된 + 드라이버가 현재 사용 불가면 기본 드라이버로 되돌린다. 검색엔진(`drivers.search_engine_driver`) + 만 그 레지스트리에 없어 가드 대상 밖이었다. + + 결과: 검색엔진 플러그인을 선택해 저장한 뒤 그 플러그인을 제거하면 `scout.driver` 가 죽은 + 값으로 남고, 공개 검색이 500 으로 떨어진다. 다른 카테고리는 같은 상황에서 기본 드라이버로 + 폴백해 사이트가 살아남는다 — 같은 부류의 결함인데 이 축만 방어가 없었다. + + 검색엔진은 등록 훅이 다르다(`core.search.engine_drivers` — Scout 엔진 맵). 플러그인에 + 제2의 등록 훅을 요구하지 않도록, 카탈로그 조회가 그 훅을 함께 읽는다. 일반 드라이버 훅 + (`core.settings.available_search_drivers`)도 그대로 인식해 양쪽 등록 방식을 모두 받는다. + +axes: + # 엔진이 어디서 왔는가. 레지스트리 표면(카테고리 등재·기본 드라이버·설정/Config 키)은 + # 엔진 출처와 직교하는 단일 사실이라 축이 아니라 effects 로 판정한다 — 축으로 두면 + # 전개만 4배가 되고 조합마다 검증할 내용이 같아진다. + engine_source: [core, plugin_scout_hook, plugin_generic_hook, removed] + +effects: + - search_category_registered_in_driver_registry # 폴백 가드 순회 대상에 편입 + - core_search_engine_declared_as_core_driver # mysql-fulltext 가 코어 드라이버로 인식 + - search_default_fallback_is_core_engine # 폴백 대상이 mysql-fulltext + - search_settings_key_matches_saved_field # drivers.search_engine_driver 와 일치 + - search_config_key_matches_apply_site # scout.driver — 실제 적용 지점과 동일 + - scout_hook_registered_engine_is_available # 살아있는 플러그인 엔진을 사용 가능으로 판정 + - generic_hook_registered_engine_is_available # 일반 드라이버 훅 등록분도 인식 (하위호환) + - removed_plugin_engine_is_unavailable # 제거된 엔진은 사용 불가 → 기본 엔진 폴백 + - admin_driver_options_include_search_category # 관리자 응답의 available_drivers 에 포함 + +test_files: + - tests/Feature/Settings/SearchDriverFallbackTest.php + - tests/Unit/Services/DriverRegistryServiceTest.php + - tests/Feature/Api/Admin/SettingsControllerDriverOptionsTest.php diff --git a/tests/scenarios/security-fixes-2026-08.yaml b/tests/scenarios/security-fixes-2026-08.yaml new file mode 100644 index 00000000..7b7d263f --- /dev/null +++ b/tests/scenarios/security-fixes-2026-08.yaml @@ -0,0 +1,39 @@ +feature: 코어 보안 수정 묶음 (2026-08 비공개 제보 대응 — 만료 토큰 로케일 / 실패 응답 마스킹 / 검색 하이라이트 이스케이프) + +description: | + 2026-08 비공개 보안 제보 대응으로 코어에 반영된 세 수정의 회귀 고정. + + 1) 만료 토큰 로케일(㉗): SetLocale 미들웨어가 만료된 Sanctum 토큰을 유효한 회원으로 + 취급해 회원 로케일을 적용하던 문제. 만료 토큰은 guest 와 동일하게 처리한다. + 2) 실패 응답 마스킹(㉚-2/N-9): UserService create/update/delete/withdraw 실패 시 + 예외 원문(SQLSTATE·쿼리)이 사용자 응답 메시지에 노출되던 문제. 고정 i18n 키만 + 응답하고 원문은 Log::error 로만 기록한다. 대상 lang 키의 :error 치환자 제거 포함. + (Module/Plugin/Template/Settings 서비스는 관리자 전용 진단면이라 의도적으로 제외 — + exceptions.md "예외 → 응답 매핑" 의 노출 폭 정책) + 3) 검색 하이라이트 SSoT(⑧/N-8): app/Search/SearchHighlighter — 이스케이프 후 + <mark> 삽입, 프리뷰는 decode → strip_tags 순서(엔티티 인코딩 태그 부활 방지). + board/ecommerce/page 3개 리스너가 이 헬퍼를 공유한다(각 확장 매니페스트 참조). + +axes: + case: [expired_token_locale, valid_token_locale, user_create_failure_masked, user_withdraw_failure_masked, masked_lang_keys, highlighter_escape, highlighter_plain_text] + +exclusions: [] + +effects: + - expired_token_treated_as_guest_for_locale + - valid_token_applies_member_locale + - failure_response_omits_sql_text + - masked_lang_keys_have_no_error_placeholder + - highlight_escapes_markup_and_marks_keyword + - plain_text_never_resurrects_entity_encoded_tags + +test_files: + - tests/Feature/Middleware/SetLocaleExpiredTokenTest.php + - tests/Feature/Http/CoreExceptionMessageLeakTest.php + - tests/Feature/User/UserWithdrawErrorLeakTest.php + - tests/Unit/Search/SearchHighlighterTest.php + +notes: | + 통합 검색 라우트의 optional.sanctum 부재(인증 회원이 guest 로 판정되던 문제)는 + board 확장의 security-fixes-2026-08.yaml (available_boards_bearer_http 케이스)이 + HTTP 전 구간으로 고정한다 — 서비스 직접 호출 테스트는 이 사각을 보지 못한다. diff --git a/tests/scenarios/settings-config-mirror.yaml b/tests/scenarios/settings-config-mirror.yaml new file mode 100644 index 00000000..38bf9ff6 --- /dev/null +++ b/tests/scenarios/settings-config-mirror.yaml @@ -0,0 +1,67 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경의 fallback YAML 파서가 nested axes 를 읽지 못해 +# 검출되지 않는다(다른 시나리오와 동일 한계). 축 조합은 PHPUnit Feature 테스트 +# (SettingsConfigMirrorTest · PageSettingsMirrorTest · EcommerceSettingsMirrorTest)가 +# 커버하고 green 이다. + +feature: 설정 config 미러 재채움 (공개이슈 #109) + +description: | + `g7_settings.*` in-memory 미러를 부팅뿐 아니라 저장 시점에도 다시 채워, 상주 프로세스가 + 옛 설정을 읽지 않게 한다. + + 배경: + - 미러는 부팅 때 한 번만 채워졌다. FPM 은 요청마다 부팅해 드러나지 않지만, + 큐 워커·schedule:work·Reverb 는 프로세스가 상주해 저장 후에도 옛 값을 영원히 읽었다. + - 플러그인 미러는 setting.json 을 raw 로 읽어 defaults 병합·정규화가 빠지고 + 암호문이 그대로 실렸다 — 전용 게터와 값의 형태가 달랐다. + - sirsoft-page 는 설정 서비스 자체가 없어 미러가 영구 미존재였고, 소비자는 항상 + 인자 기본값으로 폴백했다 (기존 테스트가 config 직접 주입으로 우회해 미검출). + + 적용 지점: + - 채움 로직 단일 소유자 신설 — 부팅 Provider 와 저장 경로가 같은 코드를 호출한다. + - 코어: 설정 캐시 무효화 지점 (saveSettings / saveAdvancedSettings / setSetting 공통). + - 플러그인: 저장 성공 후 해당 플러그인만 재채움. + - 모듈: 코어 헬퍼를 각 모듈 서비스의 캐시 초기화 지점에서 호출. + - 민감 항목(`sensitive: true`)은 미러에서 키 자체를 제거 (암호문도 싣지 않음). + +axes: + scope: [core, module, plugin] + trigger: [boot, save, cache_clear] + field_sensitivity: [normal, sensitive] + value_origin: [defaults_only, saved_override] + consumer: [mirror_helper, dedicated_getter] + +exclusions: + - { scope: core, field_sensitivity: sensitive, reason: "코어 설정 카테고리에는 sensitive 스키마 선언이 없다" } + - { trigger: cache_clear, scope: core, reason: "코어는 캐시 무효화 자체가 저장 경로와 동일 지점" } + - { field_sensitivity: sensitive, consumer: mirror_helper, value_origin: defaults_only, reason: "미러에서 제거되므로 값의 출처와 무관" } + +effects: + - core_mirror_refreshed_after_save + - core_mirror_refreshed_after_set_setting + - module_mirror_refreshed_after_save + - module_mirror_refreshed_after_cache_clear + - plugin_mirror_refreshed_after_save + - plugin_mirror_refreshed_after_reset + - core_mirror_refreshed_after_restore + - plugin_mirror_merges_defaults + - plugin_mirror_matches_dedicated_getter_shape + - sensitive_fields_absent_from_mirror_on_boot + - sensitive_fields_absent_from_mirror_on_refresh + - sensitive_fields_absent_from_mirror_after_save + - sensitive_fields_still_readable_via_dedicated_getter + - page_module_mirror_exists + - page_module_consumers_read_configured_values + - boot_and_save_share_one_fill_implementation + +test_files: + - tests/Feature/Settings/SettingsConfigMirrorTest.php + - modules/_bundled/sirsoft-page/tests/Feature/PageSettingsMirrorTest.php + - modules/_bundled/sirsoft-ecommerce/tests/Feature/Services/EcommerceSettingsMirrorTest.php + +rules_layer_coverage: + - rule: service-provider + coverage: 미러 채움은 DI 조기 해석 없이 지연 해석 (register 단계 안전) + - rule: settings_service_testing_path_isolation + coverage: 신설 페이지 설정 서비스도 testing 환경 저장 경로 격리 적용 diff --git a/tests/scenarios/settings-single-key-save.yaml b/tests/scenarios/settings-single-key-save.yaml new file mode 100644 index 00000000..2d6791a2 --- /dev/null +++ b/tests/scenarios/settings-single-key-save.yaml @@ -0,0 +1,70 @@ +feature: 단건 설정 저장의 값 형태 계약과 부수 효과 + +description: | + `PUT /api/admin/settings/{key}` 는 키를 URL 로 받고 값 하나만 싣는다. 벌크 저장 + (`SaveSettingsRequest`)처럼 키마다 규칙을 둘 수 없어, 종전 규칙은 전 키 공통으로 + `required|string|max:1000` 하나였다. 그래서 둘이 깨져 있었다. + + · 한 번 넣은 값을 화면에서 지울 수 없다 — `required` 가 빈 값을 422 로 막는다. + · 폼 전송의 boolean·정수가 문자열로 저장되어, 그 값을 읽는 쪽의 타입 비교 + (`=== true`)가 조용히 어긋난다. 예외도 경고도 없다. + + 값의 타입은 `config/settings/defaults.json` 의 기본값이 SSoT 다 — 기본값의 타입이 곧 + 그 설정의 선언 타입이다. 요청 경계에서 선언 타입으로 정규화하고, 해석하지 못한 값은 + 건드리지 않은 채 규칙이 거절한다(조용히 false·null 로 캐스팅하면 오타가 정상 저장으로 + 통과한다). + +axis_notes: + value_disposition: | + 이 엔드포인트가 값 하나에 대해 내리는 결정은 셋뿐이다 — 저장한다 / 비운다 / 거절한다. + "비운다" 의 저장 형태는 선언 타입이 정한다: 문자열은 `''`, 그 외는 `null`. + `ConvertEmptyStringsToNull` 미들웨어가 요청 시점에 이미 `''` 를 null 로 바꾸므로, + 두 입력(빈 문자열 / null)은 검증에 도달할 때 구분되지 않는다. + declared_type: | + float 축은 두지 않는다 — `defaults.json` 에 float 선언 키가 없어 조합이 실재하지 + 않는다. 정규화 코드에는 float 분기가 있고(정수 분기와 동일 형태), 선언이 생기는 + 시점에 이 축을 넓힌다. + +axes: + declared_type: [string, boolean, integer, array] + value_disposition: [accepted, cleared, rejected] + +effects: + # 값 형태 계약 + - string_setting_stored_verbatim # 문자열은 트림·캐스트 없이 원본 그대로 + - setting_cleared_to_empty_string # 문자열 설정을 비우면 `''` + - setting_cleared_to_null # 비-문자열 설정을 비우면 null + - boolean_setting_stored_as_boolean # "true"/"1" → true (문자열 저장 금지) + - integer_setting_stored_as_integer # "25" → 25 + - array_setting_stored_as_array + - uninterpretable_value_rejected_with_422 # 해석 불가 입력을 조용히 캐스팅하지 않는다 + - missing_value_key_rejected_with_422 # "비운다" 와 "안 보냈다" 는 다르다 + - oversized_value_rejected_with_422 # 문자열 1000자 + - oversized_array_rejected_with_422 # 배열 JSON 5000자 — 문자열 상한만으로는 새는 축 + + # 저장 부수 효과 (값 형태와 직교 — sub_flow 로 분리) + - seo_prerender_cache_cleared_on_asset_url_mode_change + - seo_prerender_cache_untouched_when_value_unchanged + - queue_workers_restarted_on_driver_change + - queue_workers_not_restarted_for_other_categories + - side_effects_skipped_when_save_fails + - single_key_save_uses_storage_key_verbatim # 화면 키 역변환 미적용 (프로그램 호출자 보호) + +sub_flows: + - id: single_key_side_effects + description: > + 벌크 저장에만 있던 부수 효과(SEO 프리렌더 캐시 무효화 · 큐 워커 재시작)를 단건 + 경로에도 건다. 단건 저장은 값 하나만 바꾸므로, 바뀌지 않았을 때와 저장이 실패했을 + 때는 아무것도 하지 않아야 한다 — 그 두 음성 케이스가 이 흐름의 본체다. + 값의 형태와는 직교하므로 cross product 축으로 두지 않는다. + effects: + - seo_prerender_cache_cleared_on_asset_url_mode_change + - seo_prerender_cache_untouched_when_value_unchanged + - queue_workers_restarted_on_driver_change + - queue_workers_not_restarted_for_other_categories + - side_effects_skipped_when_save_fails + - single_key_save_uses_storage_key_verbatim + +test_files: + - tests/Feature/Api/Admin/SettingsSingleValueUpdateTest.php + - tests/Unit/Services/SettingsServiceSetSettingSideEffectsTest.php diff --git a/tests/scenarios/template-public-config-cache-invalidation.yaml b/tests/scenarios/template-public-config-cache-invalidation.yaml new file mode 100644 index 00000000..cfa96e72 --- /dev/null +++ b/tests/scenarios/template-public-config-cache-invalidation.yaml @@ -0,0 +1,33 @@ +# audit:allow test-scenario-coverage reason: axes 30 cross product(pairwise) 중 동작 분기가 실재하는 조합만 docblock 매핑. cache_state=cold 는 라이프사이클 트리거와 직교(캐시가 없으면 forget 은 무조건 no-op 멱등)라 트리거 조합 확장이 형식적이고, template_type 은 무효화 경로가 타입 무관 단일 코드(clearTemplateCache)라 admin 대표 + 구조 테스트의 user 로 충분. effects 7건은 전수 매핑됨. + +feature: template_public_config_cache_invalidation + +description: | + 공개 템플릿 설정(config.json) 서빙 캐시 `template.config.{identifier}` 는 버전 + 접미사 없는 고정 키라 캐시 버전 bump 로 무효화되지 않는다. 템플릿 라이프사이클 + (cache-clear 커맨드 단일/전체, update, deactivate, uninstall) 전 경로가 + `TemplateManager::clearTemplateCache()` 단일 지점을 경유해 이 키(+ components_manifest + 두 변종, 현재 버전 routes/language 키, `.with_source_meta` 편집기 변종)를 능동 + 삭제함을 고정한다 (#588, 공개 #119). + +coverage_strategy: pairwise + +axes: + cache_state: [cold, warm_current, warm_stale] + lifecycle_trigger: [cache_clear_command_single, cache_clear_command_all, template_update, deactivate, uninstall] + template_type: [admin, user] + +effects: + - cache_clear_command_forgets_template_config_key + - template_update_invalidates_public_config_cache + - deactivate_uninstall_invalidate_public_config_cache + - serve_config_caches_manifest_and_embeds_cache_version + - serve_config_returns_installed_manifest_after_invalidation + - clear_template_cache_forgets_components_manifest_variants + - with_source_meta_layout_cache_variants_forgotten + +test_files: + - tests/Feature/Template/TemplateCacheManagementTest.php + - tests/Feature/Template/PublicTemplateControllerTest.php + - tests/Feature/Console/Commands/Template/TemplateArtisanCommandsTest.php + - tests/Unit/Extension/TemplateUpdateConfigCacheInvalidationTest.php diff --git a/tests/scenarios/trusted-script-hosts.yaml b/tests/scenarios/trusted-script-hosts.yaml new file mode 100644 index 00000000..eedd9e94 --- /dev/null +++ b/tests/scenarios/trusted-script-hosts.yaml @@ -0,0 +1,40 @@ +feature: 레이아웃 scripts[].src 신뢰 호스트 allowlist (KVE-2026-1915 B-2) + +description: | + 레이아웃이 로드하는 외부 스크립트(scripts[].src)는 same-origin 또는 manifest 가 선언한 + 신뢰 호스트(활성 확장이 등록한 CDN 등)만 허용한다. 편집자는 신뢰 호스트를 추가할 수 없고 + (manifest 는 배포물), 그 외 외부 origin 스크립트는 로드되지 않는다. + + 집계 체인은 런타임·저장측·정적검사 3층이 동형이며, 저장측(SafeLayoutExpressions/NoExternalUrls) + 이 외부 URL 저장을 422 로 차단한다. 본 E2E 는 실제 화면 로드의 모든 script 요청이 same-origin + 또는 신뢰 호스트임을 네트워크 레벨에서 무조건 측정한다. + +axes: + case: [trusted_hosts_exposed_in_config, active_extension_host_reflected, untrusted_external_script_not_loaded] + +effects: + - trusted_script_host_allowlist_wired + - untrusted_external_script_blocked + +# 선언 주체(모듈/플러그인/템플릿)는 브라우저 관찰 축(case)과 교차하지 않는 별개 관심사다 — +# case 는 배포 번들이 무엇을 로드하는가를, 선언 주체는 서버가 무엇을 집계하는가를 본다. +# 축으로 올리면 9조합이 되지만 그 조합을 관측하는 지점이 없어(마커가 두 파일에 갈려 있어) +# 어떤 조합도 커버되지 않는다. 관심사를 sub_flow 로 분리한다. +sub_flows: + - id: trusted_host_declaring_subject + description: | + 신뢰 호스트 집계는 활성 모듈·플러그인·템플릿 세 주체를 모두 읽는다. 템플릿은 PHP 확장 + 추상 클래스가 없어(AbstractTemplate 부재) 모듈/플러그인과 집계 경로가 다르므로 별도 + 회귀 가드가 필요하다 — 이 구분이 없던 동안 코어가 템플릿만 집계하지 않는 상태 + (정적검사·문서는 인정, 런타임·저장측은 차단)가 드러나지 않았다. + 한 주체의 집계가 실패해도 나머지 주체 집계는 계속되어야 한다(실패 격리). + effects: + - every_extension_type_declaration_is_collected + - malformed_or_absent_declaration_is_ignored + +test_files: + - tests/Playwright/specs/layout-script-src-trusted-host.spec.ts + - tests/Unit/Support/TrustedScriptHostsTest.php + # 차단 결정 함수(isAllowedScriptSrc)는 minify 된 배포 번들에서 이름으로 호출할 수 없어 + # E2E 는 그 결과만 관찰한다. 결정 자체(신뢰 판정·authority 우회 차단)는 여기서 잠근다. + - resources/js/core/__tests__/TemplateApp.scriptSrc.test.ts diff --git a/tests/scenarios/upgrade-step-data-snapshot.yaml b/tests/scenarios/upgrade-step-data-snapshot.yaml index 78cbf574..8bfdba17 100644 --- a/tests/scenarios/upgrade-step-data-snapshot.yaml +++ b/tests/scenarios/upgrade-step-data-snapshot.yaml @@ -54,7 +54,64 @@ exclusions: # 포함되어 PHP compile-time fatal 회피. 정적 검증(DataSnapshotTest) + 실행 검증 # (ExtensionAbstractUpgradeStepFullFlowTest::test_two_extensions_..._isolated_by_namespace) +effects: + # DataSnapshot — 스냅샷 로드/네임스페이스 격리 + - empty_snapshot_when_manifest_absent + - versioned_namespace_converts_dots_and_hyphens_to_underscore + - versioned_namespace_branches_by_core_bundled_module_bundled_plugin_external + - same_step_version_different_extensions_get_distinct_namespaces + - manifest_kind_resolves_to_applier_class_and_instantiates + - applier_file_missing_or_namespace_mismatch_throws_runtime_exception + # AbstractUpgradeStep — 위임 규약 + - run_delegates_in_order_data_snapshot_then_migrations_then_post_run + - migrations_execute_in_filename_sort_order + - two_digit_prefix_stripped_before_class_name_mapping + - data_dir_absent_silently_skips_without_fatal + # 코어 upgrade step 가드 + - core_beta_5_or_later_step_without_abstract_inheritance_throws + - core_beta_5_or_later_step_with_inheritance_runs + - core_legacy_beta_4_or_earlier_step_passes_without_inheritance + # 확장 upgrade step 가드 + - bundled_module_ge_beta_5_without_inheritance_throws + - bundled_module_with_inheritance_runs + - bundled_module_legacy_g7_version_bypasses_guard + - bundled_module_callable_closure_bypasses_guard + - step_version_below_extension_since_version_bypasses_guard + - bundled_plugin_mirrors_module_guard_branches + # 확장 전체 흐름 + - extension_full_flow_preserves_applier_migration_post_run_order + - data_dir_resolves_to_extension_directory_via_reflection + - manifest_json_absent_is_graceful_silent + - manifest_applier_file_missing_throws_explicit_runtime_exception + - two_modules_same_step_version_same_migration_class_both_run_own_code + # IDV 권한 rename 마이그레이션 + - permissions_applier_renames_in_place_when_new_row_absent + - identity_permission_pivot_merges_on_collision_path + - old_pivot_removed_and_new_pivot_preserved_for_dual_granted_role + - permission_rename_is_idempotent_across_repeated_runs + # beta.5 복구 핫픽스 + - four_domain_active_directories_recovered + - external_extension_loss_triggers_one_shot_deactivation_with_notice + - pending_stub_regenerated + - lang_packs_bundled_fallback_applied + - public_storage_symlink_restored + # since-version 판정 헬퍼 + - resolve_since_version_branches_null_empty_legacy_beta5_or_and_parse_failure + - assert_abstract_inheritance_branches_by_step_version_vs_since_version + - guard_exception_message_includes_extension_type_identifier_step_version_and_core_version + test_files: + - tests/Unit/Extension/Upgrade/DataSnapshotTest.php + - tests/Unit/Extension/AbstractUpgradeStepTest.php + - tests/Unit/Extension/Helpers/ExtensionUpgradeGuardHelperTest.php + - tests/Feature/Upgrades/RunUpgradeStepsAbstractGuardTest.php + - tests/Feature/Upgrades/Upgrade_7_0_0_beta_5PermissionRenameTest.php + - tests/Feature/Upgrades/Upgrade_7_0_0_beta_5RecoveryTest.php + - tests/Feature/Upgrades/ExtensionAbstractGuardTest.php + - tests/Feature/Upgrades/ExtensionAbstractUpgradeStepFullFlowTest.php + +# 파일별 커버 범위 상세 (test_files 의 각 항목이 무엇을 보장하는지) +test_coverage_detail: unit: - path: tests/Unit/Extension/Upgrade/DataSnapshotTest.php covers: diff --git a/tests/scenarios/upload-filter-hook-parity.yaml b/tests/scenarios/upload-filter-hook-parity.yaml new file mode 100644 index 00000000..96e8a373 --- /dev/null +++ b/tests/scenarios/upload-filter-hook-parity.yaml @@ -0,0 +1,26 @@ +# audit:allow test-scenario-coverage reason: | +# 백엔드 전용(훅 발화·필터 반환값 소비) 변경으로 PHPUnit 단위/Feature 테스트가 +# 표면×구독자 축 조합을 직접 커버하고 green 이다. cross product 자동 전개 불필요. + +feature: 업로드 filter_upload_file 필터 훅 대칭화 (공개 #96) + +description: | + before/after 액션만 있고 filter_upload_file 이 없던 업로드 지점(리뷰 이미지·ckeditor5 이미지)과 + 훅 트라이어드가 전무하던 지점(템플릿 레이아웃 첨부)에 필터 훅을 대칭 제공한다. + 구독자가 없으면 applyFilters 는 입력을 그대로 반환하므로 기존 동작 불변. + +axes: + surface: [review_image, ckeditor5_image, template_layout_attachment] + subscriber: [none, registered] + +effects: + - before_upload_action_fired + - filter_upload_file_applied # 등록된 필터가 업로드 경로에서 발화 + - filter_return_value_consumed # 필터 반환 파일이 저장 파일명/메타의 근거가 됨 + - after_upload_action_fired + - no_subscriber_passthrough # 구독자 0일 때 기존 동작 불변 (기존 upload 테스트가 커버) + +test_files: + - modules/_bundled/sirsoft-ecommerce/tests/Unit/Services/ProductReviewImageServiceTest.php + - plugins/_bundled/sirsoft-ckeditor5/tests/Unit/Services/ImageUploadServiceTest.php + - tests/Feature/Api/Admin/TemplateLayoutAttachmentControllerTest.php diff --git a/tests/scenarios/user-grade-rank-ceiling.yaml b/tests/scenarios/user-grade-rank-ceiling.yaml new file mode 100644 index 00000000..a2bcd4de --- /dev/null +++ b/tests/scenarios/user-grade-rank-ceiling.yaml @@ -0,0 +1,95 @@ +feature: 사용자 등급 상한 + 권한 부여 상한 (KVE-2026-1919) + +description: | + 슈퍼 관리자 보호와 권한 상승 상한을 삭제/탈퇴 경로뿐 아니라 수정·상태변경·권한부여·역할부여 + 전 경로에 대칭 적용한다. + + - UserGradeGuard: 비-슈퍼 액터는 슈퍼 관리자 계정을 수정/상태변경/unlock 할 수 없다. + - PermissionEscalationGuard: 액터는 자신이 보유하지 않았거나 더 넓은 범위의 권한을 부여할 수 + 없고, 그 권한을 담은 역할을 사용자에게 통째로 배정(role_ids)할 수도 없다. 역할 조작 상한은 + 추가·제거 양방향에 대칭 적용된다 — 상한 밖 역할은 붙이지도 떼지도 못한다(하향 조작 차단). + - 코어 admin 역할의 update/toggle-status 는 차단된다. 이 보호는 syncPermissions 를 직접 + 호출하는 경로에도 적용된다(형제 public 메서드가 우회로가 되지 않도록). + - bulk-status 는 정적 라우트라 스코프 미들웨어가 통째로 우회된다(라우트 모델이 없으면 + "목록 엔드포인트" 로 보고 스킵). 따라서 서비스 계층이 **등급 축과 스코프 축을 모두** + 재적용한다 — 한 축만 막으면 나머지가 우회로다. 스코프 판정은 상세 경로와 동일한 + SSoT(PermissionHelper::checkScopeAccess)에 위임한다. + - 상한 위반으로 거부된 역할 생성/수정은 부분 상태를 남기지 않는다(가드 → 쓰기 순서). + - 슈퍼 관리자 액터는 동일 작업을 정상 수행한다(회귀 없음). + +axes: + actor: [sub_admin, super_admin] + operation: [modify, status, unlock, grant_permission, assign_role, remove_role, update_role, toggle_role] + outcome: [blocked, allowed] + +exclusions: + - { actor: super_admin, outcome: blocked, reason: "슈퍼 관리자 액터는 상한 우회 — 차단 케이스 없음" } + +effects: + - super_admin_write_paths_share_rank_ceiling + - sub_admin_cannot_modify_super_admin + - sub_admin_cannot_grant_unheld_or_broader_permission + - sub_admin_cannot_assign_role_it_cannot_fully_grant + - sub_admin_cannot_remove_role_above_its_ceiling + - sub_admin_cannot_toggle_or_update_core_admin_role + - bulk_status_excludes_super_admin_for_sub_admin + - super_admin_actor_performs_same_operations + - rejected_target_state_and_tokens_unchanged + - sub_admin_can_assign_baseline_default_user_role + - role_assignment_does_not_require_permissions_update + # 과차단 회귀 축 — 가드가 정상 조작까지 막지 않는지. 차단 축만 고정하면 가드가 + # 과차단으로 회귀해도 스위트가 green 이다(C-4b baseline 역할 회귀가 그 형태였다). + - sub_admin_retains_normal_operations_on_non_super_targets + - sub_admin_retains_normal_operations_on_unprotected_roles + # 스코프 축 — 등급 축(슈퍼 보호)과 별개다. 상세 경로가 403 으로 막는 대상을 정적 + # bulk 라우트로는 바꿀 수 있으면 그 경로가 우회로다. + - bulk_status_reapplies_scope_gate_for_scoped_actor + - bulk_status_allows_scoped_actor_within_own_scope + - detail_route_enforces_scope_gate + # 거부의 원자성 — 403 만 단언하면 부분 쓰기(고아 역할 행 / 반영된 속성 변경)를 놓친다. + - rejected_role_write_leaves_no_partial_state + - sub_admin_cannot_modify_protected_role + # 프론트 게이트 노출값 — 키 존재만 단언하면 다른 리소스 권한으로 되돌아가도 green 이다. + - can_assign_roles_gate_follows_users_update + +sub_flows: + - id: reorder_scope_reapply + description: | + 정적 순서변경 라우트(`PATCH admin/attachments/reorder` · `PUT admin/menus/order`)의 + 스코프 축 재적용. bulk-status 와 같은 형태다 — 라우트 모델이 없어 PermissionMiddleware + 의 스코프 검사가 스킵되는데, 배포 기본 역할 `manager` 는 그 권한을 self 스코프로 + 보유한다. 서비스가 PermissionHelper::filterByScope 로 같은 판정을 재적용한다. + + 사용자 일괄 상태변경과 달리 "제외" 가 아니라 **전량 거부** 다 — 순서는 집합 전체에 + 대한 하나의 배열이라 일부만 반영하면 나머지와 어긋난 순서가 저장된다. + + actor(등급) 축과 교차하지 않는 별개 관심사라 축이 아니라 sub_flow 로 둔다. 축으로 + 올리면 operation 이 10값이 되어 super_admin × reorder 조합까지 요구되는데, 슈퍼 + 관리자는 글로벌 스코프라 그 칸이 검증하는 것이 없다(sub_flow `listener_ip_boundary` + 와 같은 판단). + effects: + - attachment_reorder_reapplies_scope_gate + - attachment_reorder_allows_in_scope_targets + - menu_order_reapplies_scope_gate + - menu_order_allows_in_scope_targets + # 형제 public 메서드 패리티 — HTTP 경로만 막으면 서비스를 주입한 확장이 우회한다. + - menu_order_service_method_guard_parity + # 과차단 회귀 축 — 이동 항목의 새 부모까지 검사하면 공용 상위 메뉴 아래에 자기 메뉴를 + # 다는 정상 사용이 막힌다(착수했다가 철회한 범위를 코드 주석이 아니라 테스트로 고정). + - menu_order_move_under_others_parent_allowed + + - id: security_exception_renderer + description: | + 등급 상한 예외 3종의 전역 렌더러(`bootstrap/app.php`) 계약. 심층 방어라 actor·operation + 축과 교차하지 않는 별개 관심사이므로 sub_flow 로 둔다. + + 현재 호출부(UserController · RoleController)는 전부 로컬 catch 로 403 을 만들기 때문에, + 렌더러 블록을 통째로 지워도 나머지 스위트는 전건 green 이 된다 — "새 호출부가 로컬 catch + 없이 던져도 조용한 500 대신 403" 이라는 렌더러의 존재 이유는 그 경로를 직접 거는 + 테스트만이 잠근다. + effects: + - ceiling_exceptions_render_as_403_without_local_catch + +test_files: + - tests/Feature/Security/RankCeilingGuardTest.php + - tests/Feature/Security/SecurityExceptionRendererTest.php diff --git a/tests/scenarios/user-withdraw-atomicity.yaml b/tests/scenarios/user-withdraw-atomicity.yaml new file mode 100644 index 00000000..3e6d8886 --- /dev/null +++ b/tests/scenarios/user-withdraw-atomicity.yaml @@ -0,0 +1,80 @@ +# audit:allow test-scenario-coverage reason: | +# cross product 자동 전개는 audit 실행 환경의 fallback YAML 파서가 nested axes 를 읽지 못해 +# 검출되지 않는다(다른 시나리오와 동일 한계). 축 조합은 PHPUnit Feature 테스트 +# (WithdrawAtomicityTest · AdminWithdrawPathUnificationTest · MultiStepWriteAtomicityTest)와 +# Vitest 레이아웃 테스트 · Playwright E2E 가 커버하고 green 이다. + +feature: 회원 탈퇴 원자성 및 탈퇴 경로 통일 (공개이슈 #112) + +description: | + 회원 탈퇴가 전부 성공하거나 전부 취소되게 하고, 탈퇴에 이르는 모든 경로가 + 같은 처리를 받도록 통일한다. + + 배경: + - `withdrawUser()` 에 트랜잭션이 없어, 마지막 단계(익명화 저장)가 실패하면 + 약관 동의 이력·토큰·아바타는 이미 지워진 채 계정만 활성으로 남았다. + - 익명화 접미사가 날짜(`Ymd`)뿐이라, 같은 이메일로 재가입한 회원이 같은 날 + 다시 탈퇴하면 email unique 위반으로 500 이 났다. + - 접미사를 붙이면 컬럼 길이를 넘길 수 있어(이름·이메일·닉네임) 저장이 실패했다. + - 관리자의 상태 변경(단건·일괄)은 status 컬럼만 바꿔 익명화와 탈퇴 훅을 건너뛰었다. + - 관리자/수퍼관리자 탈퇴 차단이 500 으로 나갔다. + + 적용 지점: + - 모델: 멱등 가드 + 사용자 ID 를 포함한 유일 접미사 + 컬럼 길이 클램프 + (길이 상한은 스키마 기본 길이에서 파생 — 리터럴 금지). + - 서비스: DB 변경은 트랜잭션, 파일 삭제와 after 훅은 커밋 후. + - 관리자 경로: `updateUser` / `bulkUpdateStatus` 의 탈퇴 전환은 `withdrawUser()` 위임. + - 화면: 관리자 회원 수정 폼의 '탈퇴' 저장은 확인 창을 거친다. + +axes: + entry_point: + - self_withdraw_api + - admin_update_status + - admin_bulk_status + - admin_form_ui + target_account: [normal, admin, super_admin, already_withdrawn] + email_state: [unique, reused_same_day] + field_length: [within_limit, exceeds_limit] + failure_injection: [none, last_write_fails] + confirm_action: [not_applicable, cancel, submit] + +exclusions: + - { entry_point: self_withdraw_api, confirm_action: cancel, reason: "본인 탈퇴 모달은 이번 변경 대상이 아님" } + - { entry_point: admin_bulk_status, confirm_action: cancel, reason: "일괄 탈퇴 확인 모달은 기존 구현 유지" } + - { target_account: already_withdrawn, failure_injection: last_write_fails, reason: "멱등 가드로 쓰기 자체가 발생하지 않음" } + +effects: + - withdraw_is_atomic_all_or_nothing + - consents_and_tokens_restored_on_failure + - avatar_file_kept_on_failure + - avatar_file_removed_only_after_commit + - same_day_rewithdraw_with_same_email_succeeds + - suffix_includes_user_id_for_structural_uniqueness + - long_values_truncated_within_column_limits + - withdraw_is_idempotent + - admin_status_change_runs_full_withdrawal + - admin_status_change_fires_withdraw_hooks + - admin_status_change_is_atomic_with_profile_update + - bulk_status_withdraw_delegates_per_user + - bulk_status_reports_failed_count + - admin_account_withdraw_blocked_consistently + - blocked_withdraw_returns_422_not_500 + - failed_withdraw_leaves_no_activity_log + - admin_form_shows_confirm_dialog_before_destructive_save + - admin_form_cancel_leaves_user_untouched + - admin_form_non_withdraw_status_saves_without_dialog + - create_mode_hides_withdrawn_option + +test_files: + - tests/Feature/Api/Auth/WithdrawAtomicityTest.php + - tests/Feature/Services/AdminWithdrawPathUnificationTest.php + - tests/Feature/Services/MultiStepWriteAtomicityTest.php + - tests/Feature/Api/Auth/ProfileControllerTest.php + - templates/_bundled/sirsoft-admin_basic/__tests__/layouts/admin-user-form-withdraw-confirm.test.tsx + - tests/Playwright/specs/admin/user-form-withdraw-confirm.spec.ts + +rules_layer_coverage: + - rule: service-direct-data-access + coverage: 탈퇴/삭제 경로는 Repository 인터페이스 경유 유지 + - rule: i18n-throw-hardcoded-korean + coverage: 차단 문구는 lang/{ko,en}/user.php 의 withdraw_admin_forbidden 키 diff --git a/upgrades/Upgrade_7_0_7.php b/upgrades/Upgrade_7_0_7.php new file mode 100644 index 00000000..c028a7b1 --- /dev/null +++ b/upgrades/Upgrade_7_0_7.php @@ -0,0 +1,33 @@ +<?php + +namespace App\Upgrades; + +use App\Extension\AbstractUpgradeStep; + +/** + * 코어 7.0.7 업그레이드 스텝 + * + * 모든 비즈니스 로직은 본 클래스 파일이 아닌 `upgrades/data/7.0.7/` 안에 격리된다: + * + * - migrations/ + * 01_DropLeftoverActivityLogsCreatedAtIndex.php + * 7.0.6 색인 교체 스텝의 초기 배포본이 기본 접두사(g7_)가 아닌 설치본에 + * 남긴 활동 로그의 구 정렬 색인을 정리한다. 구 색인은 새 색인의 좌측 + * 프리픽스라 쓰기 비용만 늘린다. + * + * 실행 순서는 파일명 정렬(`sort()`)을 따른다. + * + * 본 클래스는 `AbstractUpgradeStep` 의 default `run()` 에 위임 — 별도 override 없음. + * + * @upgrade-path 7.0.x → 7.0.7 + * + * 의존성 제약: 본 스텝은 변환/핫픽스를 `data/7.0.7/migrations/` 의 버전 namespace + * 클래스에 위임한다. 미래 버전에서 *그 디렉토리는 동결* (수정 금지) 되어 "각 스텝별 동작 + * 100% 동일 보장" invariant 가 성립. + * + * 상세: docs/extension/upgrade-step-guide.md §13 "버전별 데이터 스냅샷" + */ +class Upgrade_7_0_7 extends AbstractUpgradeStep +{ + // 모든 로직 위임 — data/7.0.7/ 가 SSoT. +} diff --git a/upgrades/data/7.0.6/migrations/05_AddTiebreakToCoreListIndexes.php b/upgrades/data/7.0.6/migrations/05_AddTiebreakToCoreListIndexes.php index 66329e66..66a3f7aa 100644 --- a/upgrades/data/7.0.6/migrations/05_AddTiebreakToCoreListIndexes.php +++ b/upgrades/data/7.0.6/migrations/05_AddTiebreakToCoreListIndexes.php @@ -4,6 +4,7 @@ namespace App\Upgrades\Data\V7_0_6\Migrations; use App\Extension\Upgrade\DataMigration; use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; /** @@ -20,20 +21,29 @@ use Illuminate\Support\Facades\Schema; * 대기합니다. 새 색인을 먼저 만들고 기존 색인을 나중에 지우므로, 중간에 중단되어도 조회가 * 색인 없이 남는 구간은 없습니다. * - * idempotent: 이미 교체된 대상은 건너뜁니다. V-1 안전: Facades\Schema 만 사용합니다. + * idempotent: 이미 교체된 대상은 건너뜁니다. V-1 안전: Facades\Schema / Facades\DB 만 사용합니다. */ class AddTiebreakToCoreListIndexes implements DataMigration { /** - * 대상 [테이블 => [신규 색인명, 컬럼, 교체 대상 기존 색인명(없으면 null)]] + * 대상을 반환합니다: [테이블 => [신규 색인명, 컬럼, 교체 대상 기존 색인명(없으면 null)]] * - * @var array<string, array{0: string, 1: array<int, string>, 2: string|null}> + * activity_logs 의 기존 색인은 Laravel 자동 색인명(접두사 포함 테이블명에서 파생)이라 + * 리터럴 `g7_` 을 박으면 다른 접두사 설치에서 조용한 no-op(중복 색인 잔존)이 된다 — + * `DB::getTablePrefix()` 로 동적 조립한다. + * + * @return array<string, array{0: string, 1: array<int, string>, 2: string|null}> */ - private const TARGETS = [ - 'activity_logs' => ['idx_activity_logs_created_id', ['created_at', 'id'], 'g7_activity_logs_created_at_index'], - 'notification_logs' => ['idx_notification_logs_created_id', ['created_at', 'id'], null], - 'users' => ['idx_users_created_id', ['created_at', 'id'], 'idx_users_created_at'], - ]; + private function targets(): array + { + $prefix = DB::getTablePrefix(); + + return [ + 'activity_logs' => ['idx_activity_logs_created_id', ['created_at', 'id'], $prefix.'activity_logs_created_at_index'], + 'notification_logs' => ['idx_notification_logs_created_id', ['created_at', 'id'], null], + 'users' => ['idx_users_created_id', ['created_at', 'id'], 'idx_users_created_at'], + ]; + } /** * 마이그레이션 식별자를 반환합니다. @@ -52,7 +62,7 @@ class AddTiebreakToCoreListIndexes implements DataMigration */ public function run(UpgradeContext $context): void { - foreach (self::TARGETS as $table => [$newIndex, $columns, $oldIndex]) { + foreach ($this->targets() as $table => [$newIndex, $columns, $oldIndex]) { if (! Schema::hasTable($table)) { $context->logger->info("[core:7.0.6] 테이블 부재 — 색인 교체 스킵: {$table}"); diff --git a/upgrades/data/7.0.7/migrations/01_DropLeftoverActivityLogsCreatedAtIndex.php b/upgrades/data/7.0.7/migrations/01_DropLeftoverActivityLogsCreatedAtIndex.php new file mode 100644 index 00000000..a83f759a --- /dev/null +++ b/upgrades/data/7.0.7/migrations/01_DropLeftoverActivityLogsCreatedAtIndex.php @@ -0,0 +1,75 @@ +<?php + +namespace App\Upgrades\Data\V7_0_7\Migrations; + +use App\Extension\Upgrade\DataMigration; +use App\Extension\UpgradeContext; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; + +/** + * 7.0.6 색인 교체가 남긴 활동 로그의 구 색인을 정리합니다. + * + * 7.0.6 스텝(AddTiebreakToCoreListIndexes)의 초기 배포본은 교체 대상 기존 색인명을 + * `g7_` 접두사 리터럴로 지목했다. 기본 접두사가 아닌 설치본에서는 그 이름이 실재하지 + * 않아 삭제가 조용히 건너뛰어졌고, 새 색인(`idx_activity_logs_created_id`)과 구 색인 + * (`{접두사}activity_logs_created_at_index`)이 함께 남았다 — 구 색인은 새 색인의 좌측 + * 프리픽스라 조회 이득 없이 쓰기 비용만 늘린다. 스텝 소스는 동적 접두사로 교정됐지만 + * 이미 7.0.6 을 거친 설치본은 그 스텝을 다시 실행하지 않으므로 여기서 정리한다. + * + * idempotent: 새 색인이 있고 구 색인이 남은 경우에만 삭제한다. 기본 접두사(g7_) + * 설치본과 7.0.7 교정판으로 업그레이드한 설치본에서는 구 색인이 이미 없어 no-op 이다. + * V-1 안전: Facades\Schema / Facades\DB 만 사용합니다. + */ +class DropLeftoverActivityLogsCreatedAtIndex implements DataMigration +{ + /** + * 마이그레이션 식별자를 반환합니다. + * + * @return string 마이그레이션 이름 + */ + public function name(): string + { + return 'DropLeftoverActivityLogsCreatedAtIndex'; + } + + /** + * 잔존 구 색인을 삭제합니다. + * + * @param UpgradeContext $context 업그레이드 컨텍스트 + */ + public function run(UpgradeContext $context): void + { + $table = 'activity_logs'; + + if (! Schema::hasTable($table)) { + $context->logger->info('[core:7.0.7] 테이블 부재 — 잔존 색인 정리 스킵: '.$table); + + return; + } + + $existing = array_column(Schema::getIndexes($table), 'name'); + $newIndex = 'idx_activity_logs_created_id'; + $oldIndex = DB::getTablePrefix().'activity_logs_created_at_index'; + + // 새 색인이 없으면 교체 자체가 미완이다 — 구 색인이 유일한 정렬 색인일 수 + // 있으므로 지우지 않는다(7.0.6 스텝이 담당할 상태). + if (! in_array($newIndex, $existing, true)) { + $context->logger->info("[core:7.0.7] 새 색인 부재 — 잔존 색인 정리 스킵: {$newIndex}"); + + return; + } + + if (! in_array($oldIndex, $existing, true)) { + $context->logger->info('[core:7.0.7] 잔존 구 색인 없음 — 정리 불필요: '.$table); + + return; + } + + Schema::table($table, function ($blueprint) use ($oldIndex) { + $blueprint->dropIndex($oldIndex); + }); + + $context->logger->info("[core:7.0.7] 7.0.6 교체가 남긴 구 색인 제거: {$oldIndex}"); + } +} diff --git a/vendor-bundle.json b/vendor-bundle.json index 3e5c26e5..5648dba6 100644 --- a/vendor-bundle.json +++ b/vendor-bundle.json @@ -1,15 +1,15 @@ { "schema_version": "1.0", - "generated_at": "2026-07-10T21:33:36+00:00", + "generated_at": "2026-08-12T08:04:04+00:00", "generator": "g7 vendor-bundle:build", "target": "core", - "composer_json_sha256": "76294945639dbbe4b6d054c0aea4b78377a55bcdffcd1c9136beb294bc7cfc90", - "composer_lock_sha256": "b4d173c40088a78c9fe42b3ca6d23436bee6cc850a17bb653cbc1aca59ca3a81", - "zip_sha256": "d5d21fdd930350332d2d58e5ab386ba9203795f72f7767eb3c214d4702152245", - "zip_size": 14750349, - "package_count": 104, + "composer_json_sha256": "3c7e6e17966f09f12a0282b12b18eba46c5c4f06d63c2355a514a3160af1d77a", + "composer_lock_sha256": "49bd7586c511e420bb76418579b8ca94676870d4940fdaf6bb6e0c769dc54ce0", + "zip_sha256": "f193db3da359dfa2ce8f1da9ab1c4d9c090b22f4e607ae63bc1955c7c2317db2", + "zip_size": 15212477, + "package_count": 106, "php_requirement": "^8.2", - "g7_version": "7.0.3", + "g7_version": "7.0.7", "packages": [ { "name": "aws/aws-crt-php", @@ -171,6 +171,11 @@ "version": "3.35.1", "type": "library" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.35.2", + "type": "library" + }, { "name": "league/flysystem-local", "version": "3.31.0", @@ -241,6 +246,11 @@ "version": "1.9.5", "type": "library" }, + { + "name": "predis/predis", + "version": "v2.4.1", + "type": "library" + }, { "name": "psr/clock", "version": "1.0.0", diff --git a/vendor-bundle.zip b/vendor-bundle.zip index 2bffbd05..b68aa57c 100644 Binary files a/vendor-bundle.zip and b/vendor-bundle.zip differ